From 21941aad6d7fe873eea1fd054d3f8c507cf7d4a2 Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Mon, 29 Nov 2021 09:29:50 +0100 Subject: [PATCH 01/10] Adding documentation for threading basics. --- docs/01-threading-basics.md | 101 ++++++++++++++++++++++++++++++++++++ docs/README.md | 15 ++++++ 2 files changed, 116 insertions(+) create mode 100644 docs/01-threading-basics.md create mode 100644 docs/README.md diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md new file mode 100644 index 0000000..cd4bced --- /dev/null +++ b/docs/01-threading-basics.md @@ -0,0 +1,101 @@ + + +Threading Basics +================ +## Introduction +In the historic single-threaded execution of Arduino programs the complete program logic is contained within the `*.ino` file. It contains both a `setup()` function, which is executed only once at program start, and a `loop()` function, which is executed indefinitely. In order to support multi-threaded (or parallel) sketch execution a new file type called the `*.inot` file is introduced. While a Arduino program can contain one `*.ino` file, they can contain multiple `*.inot` files. Each `*.inot` file contains it's own `setup()` and `loop()` and represent separate thread. + +The advantage of this approach is that a complicated and a long `loop()` function (consisting of nested [finite state machines](https://en.wikipedia.org/wiki/Finite-state_machine)) found in typical Arduino sketches can be broken up in several, parallel executed `loop()` functions with a much smaller scope. This increases program readability and maintainability, directly reducing to bugs (software errors). + +#### Example (Single-Threaded): +This sketch demonstrates how one would implement a program which requires the execution of three different actions on three different periodic intervals. +**Blink_Three_LEDs.ino**: +```C++ +void setup() +{ + pinMode(LED_RED, OUTPUT); + pinMode(LED_GREEN, OUTPUT); + pinMode(LED_BLUE, OUTPUT); +} + +int const DELAY_RED_msec = 900; +int const DELAY_GREEN_msec = 500; +int const DELAY_BLUE_msec = 750; + +void loop() +{ + static unsigned long prev_red = millis(); + static unsigned long prev_green = millis(); + static unsigned long prev_blue = millis(); + + unsigned long const now = millis(); + + if ((now - prev_red) > DELAY_RED_msec) { + prev_red = now; + digitalWrite(LED_RED, !digitalRead(LED_RED)); + } + + if ((now - prev_green) > DELAY_GREEN_msec) { + prev_green = now; + digitalWrite(LED_GREEN, !digitalRead(LED_GREEN)); + } + + if ((now - prev_blue) > DELAY_BLUE_msec) { + prev_blue = now; + digitalWrite(LED_BLUE, !digitalRead(LED_BLUE)); + } +} +``` +#### Example (Multi-Threaded): +The same functionality can be provided via multi-threaded execution in a much simpler way. +**Blink_Three_LEDs.ino** +```C++ +void setup() { + LedRed.start(); + LedGreen.start(); + LedBlue.start(); +} + +void loop() { + +} +``` +**LedRed.inot** +```C++ +void setup() { + pinMode(LED_RED, OUTPUT); +} + +int const DELAY_RED_msec = 900; + +void loop() { + digitalWrite(LED_RED, !digitalRead(LED_RED)); + delay(DELAY_RED_msec); +} +``` +**LedGreen.inot** +```C++ +void setup() { + pinMode(LED_GREEN, OUTPUT); +} + +int const DELAY_GREEN_msec = 500; + +void loop() { + digitalWrite(LED_GREEN, !digitalRead(LED_GREEN)); + delay(DELAY_GREEN_msec); +} +``` +**LedBlue.inot** +```C++ +void setup() { + pinMode(LED_BLUE, OUTPUT); +} + +int const DELAY_BLUE_msec = 750; + +void loop() { + digitalWrite(LED_BLUE, !digitalRead(LED_BLUE)); + delay(DELAY_BLUE_msec); +} +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..c8cbfae --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ + + +`Arduino_Threads/docs` +====================== +The Arduino threading APIs brings multi-threading to the world of Arduino. + +The following Arduino architectures and boards are supported: +* `mbed_portenta`: [Portenta H7](https://store.arduino.cc/products/portenta-h7) +* `mbed_nano`: [Nano 33 BLE](https://store.arduino.cc/arduino-nano-33-ble), [Nano RP2040 Connect](https://store.arduino.cc/nano-rp2040-connect) +* `mbed_edge`: [Edge Control](https://store.arduino.cc/products/arduino-edge-control) + +Threading on Arduino can be achieved by leveraging the [Arduino_Threads](https://github.com/bcmi-labs/Arduino_Threads) library in combination with [arduino-cli](https://github.com/facchinm/arduino-cli/commits/arduino_threads_rebased). + +### Table of Contents +* [Threading Basics](01-threading-basics.md) From 84a9e976c0f2f8220263d7e7121dfca125562049 Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Mon, 29 Nov 2021 11:18:53 +0100 Subject: [PATCH 02/10] Adding documentation for Shared/Sink/Source semantics. --- docs/02-data-exchange.md | 73 ++++++++++++++++++++++++++++++++++++++++ docs/README.md | 1 + 2 files changed, 74 insertions(+) create mode 100644 docs/02-data-exchange.md diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md new file mode 100644 index 0000000..571bdbf --- /dev/null +++ b/docs/02-data-exchange.md @@ -0,0 +1,73 @@ + + +Data Exchange between Threads +============================= +## Introduction +When a Arduino sketch formerly consisting of a single `loop()` is split into multiple `loop()` functions contained in multiple `*.inot` files it becomes necessary to device a thead-safe mechanism to exchange data between those threads. `Arduino_Threads` supports two different semantics for data exchange between threads, `Shared` variables and `Sink`/`Source` semantic. + +## `Shared` +A `Shared` variable is a global variable accessible to all threads. It is declared within `SharedVariables.h` which is automatically included on the top of each `*.inot`-file. Shared variables are declared using the `SHARED` macro in the following way: +```C++ +/* SharedVariables.h */ +SHARED(counter, int); /* A globally available, threadsafe, shared variable of type 'int'. */ +``` +A shared variable can contain up to `SHARED_QUEUE_SIZE` entries and semantically represents a FIFO ([First-In/First-Out](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics))) queue. New values can be inserted into the queue by naturally using the assignment operator `=` as if it was just any ordinary [plain-old-data](https://en.wikipedia.org/wiki/Passive_data_structure) (POD) type, i.e. `int`, `char`, ... +```C++ +/* Thread_1.inot */ +counter = 10; /* Store a value into the shared variable in a threadsafe manner. */ +``` +If the internal queue is full the oldest element is discarded and the latest element is inserted into the queue. + +Retrieving stored data works also very naturally like it would for any POD data type: +```C++ +/* Thread_2.inot */ +Serial.println(counter); /* Retrieves a value from the shared variable in a threadsafe manner. */ +``` +Should the internal queue be empty when trying to read the latest available value then the thread reading the shared variable will be suspended and the next available thread will be schedulded. Once a new value is stored inside the shared variable the suspended thread resumes operation and consumes the value which has been stored in the internal queue. + +Since shared variables are globally accessible from every thread each thread can read from or write to the shared variable. The user is responsible for using the shared variable in a responsible and sensible way, i.e. reading a shared variable from different threads is generally a bad idea, as on every read a item is removed from the queue within the shared variable and other threads can't access the read value anymore. + +## `Sink`/`Source` +The idea behind the `Sink`/`Source` semantics is to model data exchange between one data producer (`Source`) and zero, one or multiple data consumers (`Sink`). A data producer or `Source` can be declared in any `*.ino` or `*.inot`-file using the `SOURCE` macro: +```C++ +/* DataProducerThread.inot */ +SOURCE(counter, int); /* Declaration of a data source of type `int`. */ +``` +In a similar way, a data consumer can be declared in any `*.ino` or `*.inot`-file using the `SINK` macro. In difference to `Shared` where the size of the internal queue is globally set for all shared variables you can define your desired internal buffer size separately for each `Sink`. +```C++ +/* DataConsumerThread_1.inot */ +SINK(counter, int); /* Declaration of a data sink of type `int` with a internal queue size of '1'. */ +``` +```C++ +/* DataConsumerThread_2.inot */ +SINK(counter, int, 10); /* Declaration of a data sink of type `int` with a internal queue size of '10'. */ +``` +In order to actually facilitate the flow of data from a source to a sink the sinks must be connected to the desired data source. This is done within the main `ino`-file: +```C++ +/* MySinkSourceDemo.ino */ +DataProducerThread.counter.connectTo(DataConsumerThread_1.counter); +DataProducerThread.counter.connectTo(DataConsumerThread_2.counter); +``` +Whenever a new value is assigned to a data source, i.e. +```C++ +/* DataProducerThread.inot */ +counter = 10; +``` +data is automatically copied and stored within the internal queues of all connected data sinks, from where it can be retrieved, i.e. +```C++ +/* DataConsumerThread_1.inot */ +Serial.println(counter); +``` +```C++ +/* DataConsumerThread_2.inot */ +Serial.println(counter); +``` +If a thread tries to read from an empty `Sink` the thread is suspended and the next ready thread is scheduled. When a new value is written to a `Source` and consequently copied to a `Sink` the suspended thread is resumed and continuous execution (i.e. read the data and act upon it). + +Since data is actually copied multiple threads can read data from a single source without data being lost. This is an advantage compared to a simple shared variable. Furthermore you can not write to `Sink` or read from a `Source` - attempting to do so results in a compilation error. + +## Comparison +| | :+1: | :-1: | +|:---:|:---:|:---:| +| `Shared` | :+1: Needs only to declared once (`SharedVariables.h`). | :-1: Basically a global variable, with all the disadvantages those entail.
:-1: Size of internal queue fixed for ALL shared variables.
:-1: No protection against misuse (i.e. reading from multiple threads).
| +| `Sink`/`Source` | :+1: Define internal queue size separately for each `Sink`.
:+1: Supports multiple data consumers for a single data producer.
:+1: Read/Write protection - Can't read from `Source`, can't write to `Sink`.
:+1: Mandatory connecting (plumbing) within main `*.ino`-file makes data flows easily visible.
| :-1: Needs manual connection (plumbing) to connect `Sink`'s to `Source`'s. | diff --git a/docs/README.md b/docs/README.md index c8cbfae..717fcf1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,3 +13,4 @@ Threading on Arduino can be achieved by leveraging the [Arduino_Threads](https:/ ### Table of Contents * [Threading Basics](01-threading-basics.md) +* [Data exchange between threads](02-data-exchange.md) From 58d759126ce4f95bfaf2f6d9b94b8bcdc13e9a0c Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Tue, 30 Nov 2021 07:01:45 +0100 Subject: [PATCH 03/10] Adding documentation for threadsafe Serial. --- docs/03-threadsafe-serial.md | 131 +++++++++++++++++++++++++++++++++++ docs/README.md | 1 + 2 files changed, 132 insertions(+) create mode 100644 docs/03-threadsafe-serial.md diff --git a/docs/03-threadsafe-serial.md b/docs/03-threadsafe-serial.md new file mode 100644 index 0000000..1bb4abc --- /dev/null +++ b/docs/03-threadsafe-serial.md @@ -0,0 +1,131 @@ + + +Threadsafe `Serial` +=================== +## Introduction +While both `SPI` and `Wire` are communication protocols which explicitly exist to facilitate communication between one server device and multiple client devices there are no such considerations for the `Serial` interface. `Serial` communication usually exists in a one-to-one mapping between two communication partners of equal power (both can initiate communication on their own right, there is no server/client relationship). + +One example would be an Arduino sketch sending AT commands to a modem and interpreting the result of those commands. Another example would be a GPS unit sending NMEA encoded location data to the Arduino for parsing. In both cases the only sensible software representation for such functionality (AT command module or NMEA message parser) is a single thread. Also in both cases it is undesirable for other threads to inject other kind of data into the serial communication as this would only confuse i.e. the AT controlled modem. A good example for multiple threads writing to serial would be logging. A plausible example for multiple threads reading from `Serial` would be to i.e. split the NMEA parser across multiple threads, i.e. one thread only parses RMC-messages, another parses GGA-messages, etc. In any case the threadsafe `Serial` has to both support single-writer/single-reader and multiple-write/multiple-reader scenarios. + +## Initialize Serial with `begin()` +In order to initialize the serial interface for any given thread `Serial.begin(baudrate)` needs to be called in any thread's `setup()` which desires to have **writing** access to the serial interface. Since it does not make sense to support multiple different baudrates (i.e. Thread_A writing with 9600 baud and Thread_B writing with 115200 baud - if you really need this, spread the attached serial devices to different serial ports), the first thread to call `Serial.begin()` locks in the configuration for all other threads. A sensible approach is to call `Serial.begin()` within the main `*.ino`-file and only then start the other threads, i.e. +```C++ +/* MyThreadsafeSerialDemo.ino */ +void setup() +{ + Serial.begin(9600); + while (!Serial) { } + /* ... */ + Thread_1.start(); + Thread_2.start(); + /* ... */ +} +``` +```C++ +/* Thread_1.inot */ +void setup() { + Serial.begin(9600); +} +``` +```C++ +/* Thread_2.inot */ +void setup() { + Serial.begin(9600); +} +``` + +## Write to Serial with `print()`/`println()` +([`examples/Threadsafe_IO/Serial_Writer`](../examples/Threadsafe_IO/Serial_Writer)) + +Since the threadsafe `Serial` is derived from [`arduino::HardwareSerial`](https://github.com/arduino/ArduinoCore-API/blob/master/api/HardwareSerial.h) it does support the full range of the usual `Serial` API. This means that you can simply write to the `Serial` interface you've been using with the single threaded application. +```C++ +Serial.print("This is a test message #"); +Serial.println(counter); +``` + +### Prevent message break-up using `block()`/`unblock()` +([`examples/Threadsafe_IO/Serial_Writer`](../examples/Threadsafe_IO/Serial_Writer)) + +Due to the pre-emptive nature of the underlying mbed-os a multi-line `Serial.print/println()` could be interrupted at any point in time. When multiple threads are writing to the Serial interface, this can lead to jumbled-up messages. +```C++ +/* Thread_1.inot */ +void loop() { + Serial.print("This "); + Serial.print("is "); + Serial.print("a "); + Serial.print("multi-line "); + Serial.print("log "); + /* Interruption by scheduler and context switch. */ + Serial.print("message "); + Serial.print("from "); + Serial.print("thread #1."); + Serial.println(); +} +``` +```C++ +/* Thread_2.inot */ +void loop() { + Serial.print("This "); + Serial.print("is "); + Serial.print("a "); + Serial.print("multi-line "); + Serial.print("log "); + Serial.print("message "); + Serial.print("from "); + Serial.print("thread #2."); + Serial.println(); +} +``` +The resulting serial output of `Thread_1` being interrupted at the marked spot and `Thread_2` being scheduled can be seen below: +```bash +This is a multi-line log This is a multi-line log message from thread #2. +message from thread #1. + +``` +In order to prevent such break-ups a `block()`/`unblock()` API is introduced which ensures that the messages are printed in the intended order, i.e. +```C++ +/* Thread_1.inot */ +void loop() { + Serial.block(); + Serial.print("This "); + Serial.print("is "); + /* ... */ + Serial.print("thread #1."); + Serial.println(); + Serial.unblock(); +} +``` +```C++ +/* Thread_2.inot */ +void loop() { + Serial.block(); + Serial.print("This "); + Serial.print("is "); + /* ... */ + Serial.print("thread #2."); + Serial.println(); + Serial.unblock(); +} +``` +Now the thread messages are printed in the order one would expect: +```bash +This is a multi-line log message from thread #2. +This is a multi-line log message from thread #1. +``` + +## Read from `Serial` +([`examples/Threadsafe_IO/Serial_Reader`](../examples/Threadsafe_IO/Serial_Reader)) + +Reading from the `Serial` interface can be accomplished using the `read()`, `peek()` and `available()` APIs. +```C++ +/* Thread_1.inot */ +void loop() +{ + /* Read data from the serial interface into a String. */ + String serial_msg; + while (Serial.available()) + serial_msg += (char)Serial.read(); + /* ... */ +} +``` +Whenever a thread first call any of those three APIs a thread-dedicated receive ringbuffer is created and any incoming serial communication from that point on is copied in the threads dedicated receive ringbuffer. Having a dedicated receive ringbuffer per thread prevents "data stealing" from other threads in a multiple reader scenario, where the first thread to call `read()` would in fact receive the data and all other threads would miss out on it. diff --git a/docs/README.md b/docs/README.md index 717fcf1..c20bda8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,3 +14,4 @@ Threading on Arduino can be achieved by leveraging the [Arduino_Threads](https:/ ### Table of Contents * [Threading Basics](01-threading-basics.md) * [Data exchange between threads](02-data-exchange.md) +* [Threadsafe `Serial`](03-threadsafe-serial.md) From a6906eca583e01c9518535a1d5abf8cd91bfc31b Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Tue, 30 Nov 2021 09:57:57 +0100 Subject: [PATCH 04/10] Adding documentation for threadsafe Wire. --- docs/04-threadsafe-wire.md | 83 ++++++++++++++++++++++++++++++++++++++ docs/README.md | 1 + 2 files changed, 84 insertions(+) create mode 100644 docs/04-threadsafe-wire.md diff --git a/docs/04-threadsafe-wire.md b/docs/04-threadsafe-wire.md new file mode 100644 index 0000000..ead6c3d --- /dev/null +++ b/docs/04-threadsafe-wire.md @@ -0,0 +1,83 @@ + + +Threadsafe `Wire` +================= +## Introduction +A key problem of multi-tasking is the prevention of erroneous state when multiple threads share a single resource. The following example borrowed from a typical application demonstrates the problems resulting from multiple threads sharing a single resource: + +Imagine a embedded system where multiple `Wire` client devices are physically connected to a single `Wire` server. Each `Wire` client device is managed by a dedicated software thread. Each thread polls its `Wire` client device periodically. Access to the `Wire` bus is managed via the `Wire` library and typically follows the pattern described below: +```C++ +/* Wire Write Access */ +Wire.beginTransmission(addr); +Wire.write(val); +Wire.endTransmission(); + +/* Wire Read Access */ +Wire.beginTransmission(addr); +Wire.write(val); +Wire.endTransmission(); +Wire.requestFrom(addr, bytes) +while(Wire.available()) { + int val = Wire.read(); +} +``` +Since we are using a [preemptive](https://os.mbed.com/docs/mbed-os/v6.11/program-setup/concepts.html#threads) [RTOS](https://en.wikipedia.org/wiki/Real-time_operating_system) [ARM Mbed OS](https://os.mbed.com/mbed-os/) with a tick time of 10 ms for achieving multi-tasking capability and under the assumption that all threads share the same priority (which leads to a [round-robin scheduling](https://en.wikipedia.org/wiki/Round-robin_scheduling)) it can easily happen that one thread is half-way through its `Wire` access when the scheduler interrupts it and schedules the next thread which in turn starts/continues/ends its own `Wire` access. + +As a result this interruption by the scheduler will break `Wire` access for both devices and leave the `Wire` controller in an undefined state. + +In Arduino Parallela we introduced the concept of `BusDevice`s which are meant to unify the way sketches access peripherals through heterogeneous busses such as `Wire`, `SPI` and `Serial`. A `BusDevice` is declared simply by specifying the type of interface and its parameters: +```C++ +BusDevice lsm6dsox(Wire, LSM6DSOX_ADDRESS); +/* or */ +BusDevice lsm6dsox(Wire, LSM6DSOX_ADDRESS, true /* restart */); +/* or */ +BusDevice lsm6dsox(Wire, LSM6DSOX_ADDRESS, false /* restart */, true, /* stop */); +``` + +### `transfer`/`wait` **asynchronous** threadsafe `Wire` access +Once a `BusDevice` is declared it can be used to transfer data to and from the peripheral by means of the `transfer()` API. As opposed to traditional Arduino bus APIs `transfer()` is asynchronous and thus won't block execution unless the `wait()` function is called. +Note that since we are in a parallel programming environment this means that calls to `transfer()` on the same bus from different sketches will be arbitrated and that the `wait()` API will suspend the sketch until the transfer is complete, thus allowing other processes to execute or going to low power state. +```C++ +byte lsm6dsox_read_reg(byte const reg_addr) +{ + byte write_buf = reg_addr; + byte read_buf = 0; + + IoRequest req(write_buf, read_buf); + IoResponse rsp = lsm6dsox.transfer(req); + /* Do other stuff */ + rsp->wait(); /* Wait for the completion of the IO Request. */ + + return read_buf; +} +``` + +### `transfer_and_wait` **synchronous** threadsafe `Wire` access +([`examples/Threadsafe_IO/Wire`](../examples/Threadsafe_IO/Wire)) + +As the use of the `transfer` API might be confusing there's also a synchronous API call combining the request of the transfer and waiting for it's result using `transfer_and_wait`. +```C++ +byte lsm6dsox_read_reg(byte const reg_addr) +{ + byte write_buf = reg_addr; + byte read_buf = 0; + + IoRequest req(write_buf, read_buf); + IoResponse rsp = transfer_and_wait(lsm6dsox, req); /* Transmit IO request for execution and wait for completion of request. */ + + return read_buf; +} +``` + +### `Adafruit_BusIO` style **synchronous** threadsafe `Wire` access +([`examples/Threadsafe_IO/Wire_BusIO`](../examples/Threadsafe_IO/Wire_BusIO)) + +For a further simplification [Adafruit_BusIO](https://github.com/adafruit/Adafruit_BusIO) style APIs are provided: +```C++ +byte lsm6dsox_read_reg(byte reg_addr) +{ + byte read_buf = 0; + lsm6dsox.wire().write_then_read(®_addr, 1, &read_buf, 1); + return read_buf; +} +``` diff --git a/docs/README.md b/docs/README.md index c20bda8..e0e2d03 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,3 +15,4 @@ Threading on Arduino can be achieved by leveraging the [Arduino_Threads](https:/ * [Threading Basics](01-threading-basics.md) * [Data exchange between threads](02-data-exchange.md) * [Threadsafe `Serial`](03-threadsafe-serial.md) +* [Threadsafe `Wire`](04-threadsafe-wire.md) From e92915711382cc0b517ca97d4ed35230a0f9da94 Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Tue, 30 Nov 2021 10:04:47 +0100 Subject: [PATCH 05/10] Adding download link for multi-threading supporting arduino-cli. --- docs/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/README.md b/docs/README.md index e0e2d03..5e088e4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,8 @@ The following Arduino architectures and boards are supported: Threading on Arduino can be achieved by leveraging the [Arduino_Threads](https://github.com/bcmi-labs/Arduino_Threads) library in combination with [arduino-cli](https://github.com/facchinm/arduino-cli/commits/arduino_threads_rebased). +[Download](https://downloads.arduino.cc/arduino-cli/arduino-cli_arduino_threads_Linux_64bit.tar.gz) `arduino-cli` supporting multi-threading. Currently only available for Linux. + ### Table of Contents * [Threading Basics](01-threading-basics.md) * [Data exchange between threads](02-data-exchange.md) From 22ab392e9b76b71dc65c6475a3e052749642176e Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Thu, 2 Dec 2021 07:42:16 +0100 Subject: [PATCH 06/10] Adding documentation for threadsafe SPI. --- docs/05-threadsafe-spi.md | 76 +++++++++++++++++++++++++++++++++++++++ docs/README.md | 1 + 2 files changed, 77 insertions(+) create mode 100644 docs/05-threadsafe-spi.md diff --git a/docs/05-threadsafe-spi.md b/docs/05-threadsafe-spi.md new file mode 100644 index 0000000..f3a139e --- /dev/null +++ b/docs/05-threadsafe-spi.md @@ -0,0 +1,76 @@ + + +Threadsafe `SPI` +=============== +## Introduction +`SPI` communication shares the same problems that [have been demonstrated](04-threadsafe-wire.md) for using `Wire` in a multithreaded environment. This is due to the fact that every `SPI` transaction consists of multiple function calls, i.e. +```C++ +digitalWrite(cs, LOW); +SPI.beginTransaction(SPI_SETTINGS); +rx = SPI.transfer(tx); +SPI.endTransaction(); +digitalWrite(cs, HIGH); +``` +Due to the preemptive nature of the underlying RTOS the execution of the `SPI` transaction can be interrupted at any point in time. This would leave both the `SPI` driver code and the client device in a undefined state. Similar as has been done for `Wire` this is solved by introducing a `BusDevice` which allows for single-function call, threadsafe, atomic SPI transactions. A `BusDevice` is declared simply by specifying the type of interface and its parameters: +```C++ +int const DEVICE_CS_PIN = 4; +void device_cs_select() { /* ... */ } +void device_cs_deselect() { /* ... */ } +/* ... */ +BusDevice bmp388(SPI, DEVICE_CS_PIN, spi_settings); +/* or */ +BusDevice bmp388(SPI, DEVICE_CS_PIN, spi_clock, spi_bit_order, spi_bit_mode); +/* or */ +BusDevice bmp388(SPI, device_cs_select, device_cs_deselect, spi_settings); +``` + +### `transfer`/`wait` **asynchronous** threadsafe `SPI` access +Once a `BusDevice` is declared it can be used to transfer data to and from the peripheral by means of the `transfer()` API. As opposed to traditional Arduino bus APIs `transfer()` is asynchronous and thus won't block execution unless the `wait()` function is called. +Note that since we are in a parallel programming environment this means that calls to `transfer()` on the same bus from different sketches will be arbitrated and that the `wait()` API will suspend the sketch until the transfer is complete, thus allowing other processes to execute or going to low power state. +```C++ +byte bmp388_read_reg(byte const reg_addr) +{ + /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ + byte read_write_buf[] = {static_cast(0x80 | reg_addr), 0, 0}; + + IoRequest req(read_write_buf, sizeof(read_write_buf), nullptr, 0); + IoResponse rsp = bmp388.transfer(req); + /* Do other stuff */ + rsp->wait(); /* Wait for the completion of the IO Request. */ + + return read_write_buf[2]; +} +``` + +### `transfer_and_wait` **synchronous** threadsafe `SPI` access +([`examples/Threadsafe_IO/SPI`](../examples/Threadsafe_IO/SPI)) + +As the use of the `transfer` API might be confusing there's also a synchronous API call combining the request of the transfer and waiting for it's result using `transfer_and_wait`. +```C++ +byte bmp388_read_reg(byte const reg_addr) +{ + /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ + byte read_write_buf[] = {static_cast(0x80 | reg_addr), 0, 0}; + + IoRequest req(read_write_buf, sizeof(read_write_buf), nullptr, 0); + IoResponse rsp = transfer_and_wait(bmp388, req); + + return read_write_buf[2]; +} +``` + +### `Adafruit_BusIO` style **synchronous** threadsafe `SPI` access +([`examples/Threadsafe_IO/SPI_BusIO`](../examples/Threadsafe_IO/SPI_BusIO)) + +For a further simplification [Adafruit_BusIO](https://github.com/adafruit/Adafruit_BusIO) style APIs are provided: +```C++ +byte bmp388_read_reg(byte const reg_addr) +{ + /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ + byte write_buf[2] = {static_cast(0x80 | reg_addr), 0}; + byte read_buf = 0; + + bmp388.spi().write_then_read(write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf)); + return read_buf; +} +``` diff --git a/docs/README.md b/docs/README.md index 5e088e4..bacdc71 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,3 +18,4 @@ Threading on Arduino can be achieved by leveraging the [Arduino_Threads](https:/ * [Data exchange between threads](02-data-exchange.md) * [Threadsafe `Serial`](03-threadsafe-serial.md) * [Threadsafe `Wire`](04-threadsafe-wire.md) +* [Threadsafe `SPI`](05-threadsafe-spi.md) From b1f3517b66597652a666fab6e52f96a8914871af Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Tue, 30 Nov 2021 10:04:47 +0100 Subject: [PATCH 07/10] Adding download links for all platforms. --- docs/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index bacdc71..3b6f6c9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,14 @@ The following Arduino architectures and boards are supported: Threading on Arduino can be achieved by leveraging the [Arduino_Threads](https://github.com/bcmi-labs/Arduino_Threads) library in combination with [arduino-cli](https://github.com/facchinm/arduino-cli/commits/arduino_threads_rebased). -[Download](https://downloads.arduino.cc/arduino-cli/arduino-cli_arduino_threads_Linux_64bit.tar.gz) `arduino-cli` supporting multi-threading. Currently only available for Linux. +Download pre-built `arduino-cli`: +* [MacOS_64Bit](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_macOS_64bit.tar.gz) +* [Linux_32Bit](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_Linux_32bit.tar.gz) +* [Linux_64Bit](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_Linux_64bit.tar.gz) +* [Linux_ARMv6](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_Linux_ARMv6.tar.gz) +* [Linux_ARMv7](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_Linux_ARMv7.tar.gz) +* [Windows_32Bit](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_Windows_32bit.zip) +* [Windows_64Bit](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_Windows_64bit.zip) ### Table of Contents * [Threading Basics](01-threading-basics.md) From 7ea808bd3e9680153d1e60da398cb0116d550aae Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Tue, 21 Dec 2021 06:34:41 +0100 Subject: [PATCH 08/10] Fix: a arduino sketch can contain multiple ino-files, not just a single one. (But you can only define setup/loop once. See https://github.com/bcmi-labs/Arduino_Threads/pull/57#discussion_r772261027. --- docs/01-threading-basics.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index cd4bced..fc654e8 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -3,7 +3,7 @@ Threading Basics ================ ## Introduction -In the historic single-threaded execution of Arduino programs the complete program logic is contained within the `*.ino` file. It contains both a `setup()` function, which is executed only once at program start, and a `loop()` function, which is executed indefinitely. In order to support multi-threaded (or parallel) sketch execution a new file type called the `*.inot` file is introduced. While a Arduino program can contain one `*.ino` file, they can contain multiple `*.inot` files. Each `*.inot` file contains it's own `setup()` and `loop()` and represent separate thread. +In the historic single-threaded execution of Arduino sketches the complete program logic is contained within the `*.ino` file. It contains both a `setup()` function, which is executed only once at program start, and a `loop()` function, which is executed indefinitely. In order to support multi-threaded (or parallel) sketch execution a new file type called the `*.inot` file is introduced. While a Arduino project can contain multiple `*.ino` file you can define `setup()` or `loop()` only once. On the contrary a Arduino project can contain multiple `*.inot` files. Each `*.inot` file represents a separate thread and contains it's own `setup()` and `loop()` functions. The advantage of this approach is that a complicated and a long `loop()` function (consisting of nested [finite state machines](https://en.wikipedia.org/wiki/Finite-state_machine)) found in typical Arduino sketches can be broken up in several, parallel executed `loop()` functions with a much smaller scope. This increases program readability and maintainability, directly reducing to bugs (software errors). From 7976e7efa612e147e8ec582a2f557b63385add18 Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Tue, 21 Dec 2021 06:44:27 +0100 Subject: [PATCH 09/10] Adding information on limitation of inot-file names. --- docs/01-threading-basics.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index fc654e8..f100c7b 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -99,3 +99,9 @@ void loop() { delay(DELAY_BLUE_msec); } ``` +As you can see from the example the name of the `*.inot`-file is used to generate a class and instantiate a object with the same name as the `*.inot`-file. Hence the `*.inot`-file can be only named in concordance with the rules to declare a variable in C++, which are: `*.inot`-file names +* must begin with a letter of the alphabet or an underscore(_). +* can also contain letters and numbers after the first initial letter. +* are case sensitive. +* no spaces or special characters are allowed. +* cannot be a C++ keyword (i.e. `register`, `volatile`, `while`, etc.). From 8721ef72e04d2610d13534906398d7f22a8603a5 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 8 Jun 2022 11:20:47 +0200 Subject: [PATCH 10/10] Documentation suggestions (#59) * Add questions and suggestions to main readme * Add questions and suggestions to the main docs readme * Add numbers to toc * Add introduction about single-thread limitations * Add reference to example * Add Caveats section * Rephrase benefits paragraph * Add note about single threaded example * Add question about multi core * Perform minor rewording * Add recommendation for naming * Add questions, comments and rephrasing to data exchange * Add note about hierarchy graphic * Add rephrasings and questions to Thread-safe serial * Add rewording and questions to threadsafe wire * Add questions, comments and rephrasing to threadsafe SPI * Clarify on comment * Explain threading and blocking * Remove questions * Update docs/01-threading-basics.md Co-authored-by: Alexander Entinger * Update docs/01-threading-basics.md Co-authored-by: Alexander Entinger * Update docs/01-threading-basics.md Co-authored-by: Alexander Entinger * Update README.md Co-authored-by: Alexander Entinger * Update docs/01-threading-basics.md Co-authored-by: Alexander Entinger * Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger * Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger * Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger * Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger * Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger * Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger * Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger * Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger * Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger * Update docs/04-threadsafe-wire.md Co-authored-by: Alexander Entinger * Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger * Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger * Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger * Update docs/README.md Co-authored-by: Alexander Entinger * Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger * Update docs/01-threading-basics.md * Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger --- README.md | 57 ++++++++++++++++-------------- docs/01-threading-basics.md | 30 +++++++++++----- docs/02-data-exchange.md | 22 +++++++----- docs/03-threadsafe-serial.md | 21 ++++++----- docs/04-threadsafe-wire.md | 67 ++++++++++++++++++------------------ docs/05-threadsafe-spi.md | 50 ++++++++++++++------------- docs/README.md | 23 ++++++++----- 7 files changed, 152 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index c1f173b..df282d1 100644 --- a/README.md +++ b/README.md @@ -7,60 +7,65 @@ [![Check Arduino status](https://github.com/arduino-libraries/Arduino_Threads/actions/workflows/check-arduino.yml/badge.svg)](https://github.com/arduino-libraries/Arduino_Threads/actions/workflows/check-arduino.yml) [![Spell Check status](https://github.com/arduino-libraries/Arduino_Threads/actions/workflows/spell-check.yml/badge.svg)](https://github.com/arduino-libraries/Arduino_Threads/actions/workflows/spell-check.yml) -This library makes it easy to use the multi-threading capability of [Arduino](https://www.arduino.cc/) boards that use an [Mbed OS](https://os.mbed.com/docs/mbed-os/latest/introduction/index.html)-based core library. Additionally this library provides thread-safe access to `Wire`, `SPI` and `Serial` which is relevant when creating multi-threaded sketches in order to avoid common pitfalls such as race-conditions and invalid state. +This library makes it easy to use the multi-threading capability of [Arduino](https://www.arduino.cc/) boards that use an [Mbed OS](https://os.mbed.com/docs/mbed-os/latest/introduction/index.html)-based core library. Additionally this library provides thread-safe access to `Wire`, `SPI` and `Serial` which is relevant when creating multi-threaded sketches in order to avoid common pitfalls such as race-conditions and invalid state. ​ -## :zap: Features +## :star: Features ### :thread: Multi-threaded sketch execution -Instead of one big state-machine-of-doom you can split your application into multiple independent threads, each with it's own `setup()` and `loop()` function. Instead of implementing your application in a single `.ino` file, each independent thread is implemented in a dedicated `.inot` representing a clear separation of concerns on a file level. +Instead of one big state-machine-of-doom you can split your application into multiple independent threads, each with it's own `setup()` and `loop()` function. Instead of implementing your application in a single `.ino` file, each independent thread is implemented in a dedicated `.inot` file (t suffix stands for **t**hread) representing a clear separation of concerns on a file level. ### :calling: Easy communication between multiple threads -Easy inter-thread-communication is facilitated via a `Shared` abstraction providing thread-safe sink/source semantics allowing to safely exchange data of any type between threads. +Easy inter-thread-communication is facilitated via a `Shared` abstraction object providing thread-safe sink/source semantics allowing to safely exchange data of any type between threads. -### :electric_plug: Threadsafe, asynchronous and convenient Input/Output API -#### Threadsafe -A key problem of multi-tasking is the **prevention of erroneous state when multiple threads share a single resource**. The following example borrowed from a typical application demonstrates the problems resulting from multiple threads sharing a single resource: +### :shield: Thread-safe I/O +A key problem of multi-tasking is the **prevention of erroneous state when multiple threads share a single resource**. The following example borrowed from a typical application demonstrates the problems resulting from multiple threads accessing a single resource: -Imagine a embedded system where multiple `Wire` client devices are physically connected to a single `Wire` server. Each `Wire` client device is managed by a separate software thread. Each thread polls its `Wire` client device periodically. Access to the I2C bus is managed via the `Wire` library and typically follows this pattern: +Imagine an embedded system where multiple `Wire` client devices are physically connected to a single `Wire` server. Each `Wire` client device is managed by a separate software thread. Each thread polls its `Wire` client device periodically. Access to the I2C bus is managed via the `Wire` library and typically follows this pattern: ```C++ /* Wire Write Access */ -Wire.beginTransmission(addr); -Wire.write(val); +Wire.beginTransmission(address); +Wire.write(value); +// Interrupting the current thread e.g. at this point can lead to an erroneous state +// if another thread performs Wire I/O before the transmission ends. Wire.endTransmission(); /* Wire Read Access */ -Wire.beginTransmission(addr); -Wire.write(val); -Wire.endTransmission(); -Wire.requestFrom(addr, bytes) +Wire.requestFrom(address, bytes) while(Wire.available()) { - int val = Wire.read(); + int value = Wire.read(); } ``` -Since we are using [ARM Mbed OS](https://os.mbed.com/mbed-os/) which is a [preemptive](https://en.wikipedia.org/wiki/Preemption_(computing)) [RTOS](https://en.wikipedia.org/wiki/Real-time_operating_system) for achieving multi-tasking capability and under the assumption that all threads share the same priority (which leads to a [round-robin](https://en.wikipedia.org/wiki/Round-robin_scheduling) scheduling) it can easily happen that one thread is half-way through its Wire IO access when the scheduler interrupts it and schedules the next thread which in turn starts/continues/ends its own Wire IO access. +Since we are using [ARM Mbed OS](https://os.mbed.com/mbed-os/) which is a [preemptive](https://en.wikipedia.org/wiki/Preemption_(computing)) [RTOS](https://en.wikipedia.org/wiki/Real-time_operating_system) for achieving multi-tasking capability and under the assumption that all threads share the same priority (which leads to a [round-robin](https://en.wikipedia.org/wiki/Round-robin_scheduling) scheduling) it can easily happen that one thread is half-way through its Wire I/O access when the scheduler interrupts its execution and schedules the next thread which in turn starts, continues or ends its own Wire I/O access. + +As a result this interruption by the scheduler will break Wire I/O access for both devices and leave the Wire I/O controller in an undefined state :fire:. + +`Arduino_Threads` solves this problem by encapsulating the complete I/O access (e.g. reading from a `Wire` client device) within a single function call which generates an I/O request to be asynchronously executed by a high-priority I/O thread. The high-priority I/O thread is the **only** instance which directly communicates with physical hardware. + +### :runner: Asynchronous +The mechanisms implemented in this library allow any thread to dispatch an I/O request asynchronously and either continue its operation or [yield](https://en.wikipedia.org/wiki/Yield_(multithreading)) control to the next scheduled thread. All I/O requests are stored in a queue and are executed within a high-priority I/O thread after a [context-switch](https://en.wikipedia.org/wiki/Context_switch). An example of this can be seen [here](examples/Threadsafe_IO/Threadsafe_SPI/Threadsafe_SPI.ino)). + +### :relieved: Convenient API +Although you are free to directly manipulate I/O requests and responses (e.g. [Threadsafe_Wire](examples/Threadsafe_IO/Threadsafe_Wire/Threadsafe_Wire.ino)) there are convenient `read`/`write`/`write_then_read` abstractions inspired by the [Adafruit_BusIO](https://github.com/adafruit/Adafruit_BusIO) library (e.g. [Threadsafe_Wire_BusIO](examples/Threadsafe_IO/Threadsafe_Wire_BusIO/Threadsafe_Wire_BusIO.ino)). + + -As a result this interruption by the scheduler will break Wire IO access for both devices and leave the Wire IO controller in an undefined state. :fire:. +## :zap: Caveats -`Arduino_Threads` solves this problem by encapsulating a complete IO access (e.g. reading from a `Wire` client device) within a single function call which generates an IO request to be asynchronously executed by a high-priority IO thread. The high-priority IO thread is the **only** instance which actually directly communicates with physical hardware. -#### Asynchronous -The mechanisms implemented in this library allow any thread to dispatch an IO request asynchronously and either continue operation or [yield](https://en.wikipedia.org/wiki/Yield_(multithreading))-ing control to the next scheduled thread. All IO requests are stored in a queue and are executed within a high-priority IO thread after a context-switch. An example of this can be seen [here](examples/Threadsafe_IO/Threadsafe_SPI/Threadsafe_SPI.ino)). -#### Convenient API -Although you are free to directly manipulate IO requests and responses (e.g. [Threadsafe_Wire](examples/Threadsafe_IO/Threadsafe_Wire/Threadsafe_Wire.ino)) there do exist convenient `read`/`write`/`write_then_read` abstractions inspired by the [Adafruit_BusIO](https://github.com/adafruit/Adafruit_BusIO) library (e.g. [Threadsafe_Wire_BusIO](examples/Threadsafe_IO/Threadsafe_Wire_BusIO/Threadsafe_Wire_BusIO.ino)). ## :mag_right: Resources * [How to install a library](https://www.arduino.cc/en/guide/libraries) -* [Help Center](https://support.arduino.cc/) -* [Forum](https://forum.arduino.cc) +* [Help Center](https://support.arduino.cc/) - Get help from Arduino's official support team +* [Forum](https://forum.arduino.cc) - Get support from the community ## :bug: Bugs & Issues -If you want to report an issue with this library, you can submit it to the [issue tracker](issues) of this repository. Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. Make sure you're using an original Arduino board. +If you found an issue in this library, you can submit it to the [issue tracker](issues) of this repository. Remember to include as much detail as you can about your hardware set-up, code and steps for reproducing the issue. To prevent hardware related incompatibilities make sure to use an [original Arduino board](https://support.arduino.cc/hc/en-us/articles/360020652100-How-to-spot-a-counterfeit-Arduino). -## :technologist: Development +## :technologist: Contribute There are many ways to contribute: diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index f100c7b..3de1b15 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -3,13 +3,18 @@ Threading Basics ================ ## Introduction -In the historic single-threaded execution of Arduino sketches the complete program logic is contained within the `*.ino` file. It contains both a `setup()` function, which is executed only once at program start, and a `loop()` function, which is executed indefinitely. In order to support multi-threaded (or parallel) sketch execution a new file type called the `*.inot` file is introduced. While a Arduino project can contain multiple `*.ino` file you can define `setup()` or `loop()` only once. On the contrary a Arduino project can contain multiple `*.inot` files. Each `*.inot` file represents a separate thread and contains it's own `setup()` and `loop()` functions. +Previously Arduino sketches didn't support the concept of multitasking, unless you took specific measures to support it. With this so called single-threaded approach instructions in your Arduino sketch are executed one after another. If an instruction or a function call respectively makes the runtime environment wait for its execution to complete, it's called a "blocking" function. You may have encountered the limitations of this when interacting with multiple sensors and actuators at once. For example if you let a servo motor react to the data read from a distance sensor. While the servo motor is moving to its target position no further reading of the distance sensor can be done because the program waits until the servo is done moving. To solve this issue multitasking can be used which allows to "simultaneously" execute multiple task such as reading from a sensor and controlling a motor. In the context of multitasking a thread is basically a mechanism (provided usually by the operating system) to encapsulate a task to be run concurrently with others. -The advantage of this approach is that a complicated and a long `loop()` function (consisting of nested [finite state machines](https://en.wikipedia.org/wiki/Finite-state_machine)) found in typical Arduino sketches can be broken up in several, parallel executed `loop()` functions with a much smaller scope. This increases program readability and maintainability, directly reducing to bugs (software errors). +In the historic single-threaded execution of Arduino sketches the complete program logic is contained within the `*.ino` file. It contains both a `setup()` function, which is executed only once at program start, and a `loop()` function, which is executed indefinitely. A single-threaded Arduino project can theoretically contain multiple `*.ino` files but you can define `setup()` or `loop()` only once. +In order to support multi-threaded (or parallel) sketch execution a new file type called the `*.inot` file is introduced. + +The advantage of this approach is that a complex and lengthy `loop()` function (potentially consisting of nested [finite state machines](https://en.wikipedia.org/wiki/Finite-state_machine)) found in typical Arduino sketches can be broken up into several, parallelly executed `loop()` functions with a much smaller scope. This not only increases program readability and maintainability but as a result leads to a reduction of software errors (bugs). #### Example (Single-Threaded): -This sketch demonstrates how one would implement a program which requires the execution of three different actions on three different periodic intervals. +This sketch demonstrates how one would implement a program which requires the execution of three different actions on three different periodic intervals. In this example we blink three different LEDs at three different intervals. + **Blink_Three_LEDs.ino**: + ```C++ void setup() { @@ -46,18 +51,23 @@ void loop() } } ``` +You can imagine that with increasing complexity of a sketch it gets quite difficult to keep track of the different states used for the different sub-tasks (here: blinking each of the LEDs). + #### Example (Multi-Threaded): -The same functionality can be provided via multi-threaded execution in a much simpler way. + +The same functionality can be provided via multi-threaded execution in a much cleaner way. + **Blink_Three_LEDs.ino** + ```C++ void setup() { + // Start the task defined in the corresponding .inot file LedRed.start(); LedGreen.start(); LedBlue.start(); } void loop() { - } ``` **LedRed.inot** @@ -99,9 +109,11 @@ void loop() { delay(DELAY_BLUE_msec); } ``` -As you can see from the example the name of the `*.inot`-file is used to generate a class and instantiate a object with the same name as the `*.inot`-file. Hence the `*.inot`-file can be only named in concordance with the rules to declare a variable in C++, which are: `*.inot`-file names -* must begin with a letter of the alphabet or an underscore(_). -* can also contain letters and numbers after the first initial letter. +As you can see from the example the name of the `*.inot`-file is used to generate a class and instantiate an object with the **same name** as the `*.inot`-file. Hence the `*.inot`-file can be only named in concordance with the rules to declare a variable in C++. `*.inot`-file names: +* must begin with a letter of the alphabet or an underscore (_). +* can contain letters and numbers after the first initial letter. * are case sensitive. -* no spaces or special characters are allowed. +* cannot contain spaces or special characters. * cannot be a C++ keyword (i.e. `register`, `volatile`, `while`, etc.). + +To be consistent with the Arduino programming style we recommend using [camel case](https://en.wikipedia.org/wiki/Camel_case) for the file names. \ No newline at end of file diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index 571bdbf..e30aeaf 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -3,15 +3,19 @@ Data Exchange between Threads ============================= ## Introduction -When a Arduino sketch formerly consisting of a single `loop()` is split into multiple `loop()` functions contained in multiple `*.inot` files it becomes necessary to device a thead-safe mechanism to exchange data between those threads. `Arduino_Threads` supports two different semantics for data exchange between threads, `Shared` variables and `Sink`/`Source` semantic. +When an Arduino sketch formerly consisting of just a single `loop()` is split into multiple `loop()` functions contained in multiple `*.inot` files it becomes necessary to define a thread-safe mechanism to exchange data between those threads. For example, one `*.inot` file could calculate a math formula and send the result back to the main `*.ino` file to act upon it. Meanwhile the main `*.ino` file can take care of other tasks. + + `Arduino_Threads` supports two different mechanisms for data exchange between threads: `Shared` variables and `Sink`/`Source` semantics. Both have their pros and cons as described below. ## `Shared` -A `Shared` variable is a global variable accessible to all threads. It is declared within `SharedVariables.h` which is automatically included on the top of each `*.inot`-file. Shared variables are declared using the `SHARED` macro in the following way: +A `Shared` variable is a global variable accessible to all threads. It can be declared within a header file named `SharedVariables.h` which is automatically included at the top of each `*.inot`-file : ```C++ /* SharedVariables.h */ SHARED(counter, int); /* A globally available, threadsafe, shared variable of type 'int'. */ ``` -A shared variable can contain up to `SHARED_QUEUE_SIZE` entries and semantically represents a FIFO ([First-In/First-Out](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics))) queue. New values can be inserted into the queue by naturally using the assignment operator `=` as if it was just any ordinary [plain-old-data](https://en.wikipedia.org/wiki/Passive_data_structure) (POD) type, i.e. `int`, `char`, ... +Writing to and reading from the shared variable may not always happen concurrently. I.e. a thread reading sensor data may update the shared variable faster than a slower reader thread would extract those values. Therefore the shared variable is modeled as a queue which can store (buffer) a certain number of entries. That way the slower reader thread can access all the values in the same order as they have been written. +New values can be inserted naturally by using the assignment operator `=` as if it was just any ordinary variable type, i.e. `int`, `char`, ... + ```C++ /* Thread_1.inot */ counter = 10; /* Store a value into the shared variable in a threadsafe manner. */ @@ -23,12 +27,12 @@ Retrieving stored data works also very naturally like it would for any POD data /* Thread_2.inot */ Serial.println(counter); /* Retrieves a value from the shared variable in a threadsafe manner. */ ``` -Should the internal queue be empty when trying to read the latest available value then the thread reading the shared variable will be suspended and the next available thread will be schedulded. Once a new value is stored inside the shared variable the suspended thread resumes operation and consumes the value which has been stored in the internal queue. -Since shared variables are globally accessible from every thread each thread can read from or write to the shared variable. The user is responsible for using the shared variable in a responsible and sensible way, i.e. reading a shared variable from different threads is generally a bad idea, as on every read a item is removed from the queue within the shared variable and other threads can't access the read value anymore. +Should the internal queue be empty when trying to read the latest available value then the thread reading the shared variable will be suspended and the next available thread will be scheduled. Once a new value is stored inside the shared variable the suspended thread resumes operation and consumes the value which has been stored in the internal queue. +Since shared variables are globally accessible from every thread, each thread can read from or write to the shared variable. The user is responsible for using the shared variable in a responsible and sensible way, i.e. reading a shared variable from different threads is generally a bad idea, as on every read an item is removed from the queue within the shared variable and other threads can't access the read value anymore . ## `Sink`/`Source` -The idea behind the `Sink`/`Source` semantics is to model data exchange between one data producer (`Source`) and zero, one or multiple data consumers (`Sink`). A data producer or `Source` can be declared in any `*.ino` or `*.inot`-file using the `SOURCE` macro: +The idea behind the `Sink`/`Source` semantics is to model data exchange between one data producer (`Source`) and one or multiple data consumers (`Sink`). A data producer or `Source` can be declared in any `*.ino` or `*.inot`-file using the `SOURCE` macro: ```C++ /* DataProducerThread.inot */ SOURCE(counter, int); /* Declaration of a data source of type `int`. */ @@ -64,10 +68,10 @@ Serial.println(counter); ``` If a thread tries to read from an empty `Sink` the thread is suspended and the next ready thread is scheduled. When a new value is written to a `Source` and consequently copied to a `Sink` the suspended thread is resumed and continuous execution (i.e. read the data and act upon it). -Since data is actually copied multiple threads can read data from a single source without data being lost. This is an advantage compared to a simple shared variable. Furthermore you can not write to `Sink` or read from a `Source` - attempting to do so results in a compilation error. +Since the data added to the source is copied multiple threads can read data from a single source without data being lost. This is an advantage compared to a simple shared variable. Furthermore you cannot accidentally write to a `Sink` or read from a `Source`. Attempting to do so results in a compilation error. ## Comparison | | :+1: | :-1: | |:---:|:---:|:---:| -| `Shared` | :+1: Needs only to declared once (`SharedVariables.h`). | :-1: Basically a global variable, with all the disadvantages those entail.
:-1: Size of internal queue fixed for ALL shared variables.
:-1: No protection against misuse (i.e. reading from multiple threads).
| -| `Sink`/`Source` | :+1: Define internal queue size separately for each `Sink`.
:+1: Supports multiple data consumers for a single data producer.
:+1: Read/Write protection - Can't read from `Source`, can't write to `Sink`.
:+1: Mandatory connecting (plumbing) within main `*.ino`-file makes data flows easily visible.
| :-1: Needs manual connection (plumbing) to connect `Sink`'s to `Source`'s. | +| `Shared` | :+1: Needs to be declared only once (in `SharedVariables.h`). | :-1: Basically a global variable, with all the disadvantages those entail.
:-1: Size of internal queue fixed for ALL shared variables.
:-1: No protection against misuse (i.e. reading from multiple threads).
| +| `Sink`/`Source` | :+1: Define internal queue size separately for each `Sink`.
:+1: Supports multiple data consumers for a single data producer.
:+1: Read/Write protection: Can't read from `Source`, can't write to `Sink`.
:+1: Mandatory connecting (plumbing) within main `*.ino`-file makes data flows easily visible.
| :-1: Needs manual connection (plumbing) to connect `Sink`'s to `Source`'s. | diff --git a/docs/03-threadsafe-serial.md b/docs/03-threadsafe-serial.md index 1bb4abc..a60811c 100644 --- a/docs/03-threadsafe-serial.md +++ b/docs/03-threadsafe-serial.md @@ -1,14 +1,16 @@ -Threadsafe `Serial` +Thread-safe `Serial` =================== ## Introduction While both `SPI` and `Wire` are communication protocols which explicitly exist to facilitate communication between one server device and multiple client devices there are no such considerations for the `Serial` interface. `Serial` communication usually exists in a one-to-one mapping between two communication partners of equal power (both can initiate communication on their own right, there is no server/client relationship). -One example would be an Arduino sketch sending AT commands to a modem and interpreting the result of those commands. Another example would be a GPS unit sending NMEA encoded location data to the Arduino for parsing. In both cases the only sensible software representation for such functionality (AT command module or NMEA message parser) is a single thread. Also in both cases it is undesirable for other threads to inject other kind of data into the serial communication as this would only confuse i.e. the AT controlled modem. A good example for multiple threads writing to serial would be logging. A plausible example for multiple threads reading from `Serial` would be to i.e. split the NMEA parser across multiple threads, i.e. one thread only parses RMC-messages, another parses GGA-messages, etc. In any case the threadsafe `Serial` has to both support single-writer/single-reader and multiple-write/multiple-reader scenarios. +One example would be an Arduino sketch sending AT commands to a modem over a Serial connection and interpreting the result of those commands. Another example would be a GPS unit sending NMEA encoded location data to the Arduino for parsing. In both cases the only sensible software representation for such functionality (AT command module or NMEA message parser) is a single thread. Also in both cases it is undesirable for other threads to inject other kind of data into the serial communication as this would only confuse i.e. the AT controlled modem which reads that data. + +A good example for multiple threads writing to `Serial` would be logging where mixing messages from different sources doesn't cause any harm. A possible example for multiple threads reading from `Serial` would be to i.e. split an NMEA parser across multiple threads, i.e. one thread only parses RMC-messages, another parses GGA-messages, etc. In any case the thread-safe `Serial` supports both single-writer/single-reader and multiple-write/multiple-reader scenarios. ## Initialize Serial with `begin()` -In order to initialize the serial interface for any given thread `Serial.begin(baudrate)` needs to be called in any thread's `setup()` which desires to have **writing** access to the serial interface. Since it does not make sense to support multiple different baudrates (i.e. Thread_A writing with 9600 baud and Thread_B writing with 115200 baud - if you really need this, spread the attached serial devices to different serial ports), the first thread to call `Serial.begin()` locks in the configuration for all other threads. A sensible approach is to call `Serial.begin()` within the main `*.ino`-file and only then start the other threads, i.e. +In order to initialize the serial interface for any given thread `Serial.begin(baudrate)` needs to be called in each thread's `setup()` which desires to have **writing** access to the serial interface. Since it does not make sense to support multiple different baudrates (i.e. Thread_A writing with 9600 baud and Thread_B writing with 115200 baud - if you really need this, spread the attached serial devices to different serial ports), the first thread to call `Serial.begin()` locks in the baud rate configuration for all other threads. A sensible approach is to call `Serial.begin()` within the main `*.ino`-file and only then start the other threads, i.e. ```C++ /* MyThreadsafeSerialDemo.ino */ void setup() @@ -24,20 +26,20 @@ void setup() ```C++ /* Thread_1.inot */ void setup() { - Serial.begin(9600); + Serial.begin(9600); // Baud rate will be neglected as it's set in the main file } ``` ```C++ /* Thread_2.inot */ void setup() { - Serial.begin(9600); + Serial.begin(9600); // Baud rate will be neglected as it's set in the main file } ``` -## Write to Serial with `print()`/`println()` +## Write to Serial using `print()`/`println()` ([`examples/Threadsafe_IO/Serial_Writer`](../examples/Threadsafe_IO/Serial_Writer)) -Since the threadsafe `Serial` is derived from [`arduino::HardwareSerial`](https://github.com/arduino/ArduinoCore-API/blob/master/api/HardwareSerial.h) it does support the full range of the usual `Serial` API. This means that you can simply write to the `Serial` interface you've been using with the single threaded application. +Since the thread-safe `Serial` is derived from [`arduino::HardwareSerial`](https://github.com/arduino/ArduinoCore-API/blob/master/api/HardwareSerial.h) it supports the complete `Serial` API. This means that you can simply write to the `Serial` interface as you would do with a single threaded application. ```C++ Serial.print("This is a test message #"); Serial.println(counter); @@ -46,7 +48,8 @@ Serial.println(counter); ### Prevent message break-up using `block()`/`unblock()` ([`examples/Threadsafe_IO/Serial_Writer`](../examples/Threadsafe_IO/Serial_Writer)) -Due to the pre-emptive nature of the underlying mbed-os a multi-line `Serial.print/println()` could be interrupted at any point in time. When multiple threads are writing to the Serial interface, this can lead to jumbled-up messages. +Due to the pre-emptive nature of the underlying mbed-os threading mechanism a multi-line sequence of `Serial.print/println()` could be interrupted at any point in time. When multiple threads are writing to the same Serial interface, this can lead to jumbled-up messages. + ```C++ /* Thread_1.inot */ void loop() { @@ -128,4 +131,4 @@ void loop() /* ... */ } ``` -Whenever a thread first call any of those three APIs a thread-dedicated receive ringbuffer is created and any incoming serial communication from that point on is copied in the threads dedicated receive ringbuffer. Having a dedicated receive ringbuffer per thread prevents "data stealing" from other threads in a multiple reader scenario, where the first thread to call `read()` would in fact receive the data and all other threads would miss out on it. +Whenever a thread first calls any of those three APIs a thread-specific receive ring buffer is created. From that moment on any incoming serial communication is copied into that buffer. Maintaining a copy of the serial data in a dedicated buffer per thread prevents "data stealing" from other threads in a multiple reader scenario, where the first thread to call `read()` would in fact receive the data and all other threads would miss out on it. diff --git a/docs/04-threadsafe-wire.md b/docs/04-threadsafe-wire.md index ead6c3d..4c16836 100644 --- a/docs/04-threadsafe-wire.md +++ b/docs/04-threadsafe-wire.md @@ -1,27 +1,24 @@ -Threadsafe `Wire` +Thread-safe `Wire` ================= ## Introduction -A key problem of multi-tasking is the prevention of erroneous state when multiple threads share a single resource. The following example borrowed from a typical application demonstrates the problems resulting from multiple threads sharing a single resource: +A common problem of multi-tasking is the prevention of erroneous state when multiple threads share a single resource. The following example borrowed from a typical application demonstrates these problems: -Imagine a embedded system where multiple `Wire` client devices are physically connected to a single `Wire` server. Each `Wire` client device is managed by a dedicated software thread. Each thread polls its `Wire` client device periodically. Access to the `Wire` bus is managed via the `Wire` library and typically follows the pattern described below: +Imagine an embedded system where multiple `Wire` client devices are physically connected to a single `Wire` server. Each `Wire` client device is managed by a dedicated software thread. Each thread polls its `Wire` client device periodically. Access to the `Wire` bus is managed via the `Wire` library and typically follows the pattern described below: ```C++ /* Wire Write Access */ -Wire.beginTransmission(addr); -Wire.write(val); +Wire.beginTransmission(address); +Wire.write(value); Wire.endTransmission(); /* Wire Read Access */ -Wire.beginTransmission(addr); -Wire.write(val); -Wire.endTransmission(); -Wire.requestFrom(addr, bytes) +Wire.requestFrom(address, bytes) while(Wire.available()) { - int val = Wire.read(); + int value = Wire.read(); } ``` -Since we are using a [preemptive](https://os.mbed.com/docs/mbed-os/v6.11/program-setup/concepts.html#threads) [RTOS](https://en.wikipedia.org/wiki/Real-time_operating_system) [ARM Mbed OS](https://os.mbed.com/mbed-os/) with a tick time of 10 ms for achieving multi-tasking capability and under the assumption that all threads share the same priority (which leads to a [round-robin scheduling](https://en.wikipedia.org/wiki/Round-robin_scheduling)) it can easily happen that one thread is half-way through its `Wire` access when the scheduler interrupts it and schedules the next thread which in turn starts/continues/ends its own `Wire` access. +Since we are using the [preemptive](https://os.mbed.com/docs/mbed-os/v6.11/program-setup/concepts.html#threads) [RTOS](https://en.wikipedia.org/wiki/Real-time_operating_system) [ARM Mbed OS](https://os.mbed.com/mbed-os/) with a tick time of 10 ms for achieving multi-tasking capability and under the assumption that all threads share the same priority (which leads to a [round-robin scheduling](https://en.wikipedia.org/wiki/Round-robin_scheduling)) it can easily happen that one thread is half-way through its `Wire` access when the scheduler interrupts it and schedules the next thread which in turn starts/continues/ends its own `Wire` access. As a result this interruption by the scheduler will break `Wire` access for both devices and leave the `Wire` controller in an undefined state. @@ -34,50 +31,54 @@ BusDevice lsm6dsox(Wire, LSM6DSOX_ADDRESS, true /* restart */); BusDevice lsm6dsox(Wire, LSM6DSOX_ADDRESS, false /* restart */, true, /* stop */); ``` -### `transfer`/`wait` **asynchronous** threadsafe `Wire` access -Once a `BusDevice` is declared it can be used to transfer data to and from the peripheral by means of the `transfer()` API. As opposed to traditional Arduino bus APIs `transfer()` is asynchronous and thus won't block execution unless the `wait()` function is called. -Note that since we are in a parallel programming environment this means that calls to `transfer()` on the same bus from different sketches will be arbitrated and that the `wait()` API will suspend the sketch until the transfer is complete, thus allowing other processes to execute or going to low power state. +### Asynchronous thread-safe `Wire` access with `transfer`/`wait` +Once a `BusDevice` is declared it can be used to transfer data to and from the peripheral by means of the `transfer()` API. As opposed to the traditional Arduino bus APIs, `transfer()` is asynchronous and thus won't block execution unless the `wait()` function is called. +Note that we are in a parallel programming environment which means that calls to `transfer()` on the same bus from different sketches will be arbitrated. + ```C++ byte lsm6dsox_read_reg(byte const reg_addr) { - byte write_buf = reg_addr; - byte read_buf = 0; + byte write_buffer = reg_addr; + byte read_buffer = 0; - IoRequest req(write_buf, read_buf); - IoResponse rsp = lsm6dsox.transfer(req); - /* Do other stuff */ - rsp->wait(); /* Wait for the completion of the IO Request. */ + IoRequest request(write_buffer, read_buffer); + IoResponse response = lsm6dsox.transfer(request); + + /* Wait for the completion of the IO Request. + Allows other threads to run */ + response->wait(); - return read_buf; + return read_buffer; } ``` -### `transfer_and_wait` **synchronous** threadsafe `Wire` access +### Synchronous thread-safe `Wire` access with `transfer_and_wait` ([`examples/Threadsafe_IO/Wire`](../examples/Threadsafe_IO/Wire)) -As the use of the `transfer` API might be confusing there's also a synchronous API call combining the request of the transfer and waiting for it's result using `transfer_and_wait`. +As the use of the `transfer` API might be difficult to grasp there's also a synchronous API call combining the request of the transfer and waiting for its result using `transfer_and_wait`. ```C++ byte lsm6dsox_read_reg(byte const reg_addr) { - byte write_buf = reg_addr; - byte read_buf = 0; + byte write_buffer = reg_addr; + byte read_buffer = 0; - IoRequest req(write_buf, read_buf); - IoResponse rsp = transfer_and_wait(lsm6dsox, req); /* Transmit IO request for execution and wait for completion of request. */ + IoRequest request(write_buffer, read_buffer); + IoResponse response = transfer_and_wait(lsm6dsox, request); /* Transmit IO request for execution and wait for completion of request. */ - return read_buf; + return read_buffer; } ``` -### `Adafruit_BusIO` style **synchronous** threadsafe `Wire` access +### `Adafruit_BusIO` style **synchronous** thread-safe `Wire` access ([`examples/Threadsafe_IO/Wire_BusIO`](../examples/Threadsafe_IO/Wire_BusIO)) -For a further simplification [Adafruit_BusIO](https://github.com/adafruit/Adafruit_BusIO) style APIs are provided: +For further simplification [Adafruit_BusIO](https://github.com/adafruit/Adafruit_BusIO) style APIs are provided: + ```C++ byte lsm6dsox_read_reg(byte reg_addr) { - byte read_buf = 0; - lsm6dsox.wire().write_then_read(®_addr, 1, &read_buf, 1); - return read_buf; + byte read_buffer = 0; + lsm6dsox.wire().write_then_read(®_addr, 1, &read_buffer, 1); + return read_buffer; } ``` diff --git a/docs/05-threadsafe-spi.md b/docs/05-threadsafe-spi.md index f3a139e..c7189b6 100644 --- a/docs/05-threadsafe-spi.md +++ b/docs/05-threadsafe-spi.md @@ -1,9 +1,10 @@ -Threadsafe `SPI` +Thread-safe `SPI` =============== ## Introduction -`SPI` communication shares the same problems that [have been demonstrated](04-threadsafe-wire.md) for using `Wire` in a multithreaded environment. This is due to the fact that every `SPI` transaction consists of multiple function calls, i.e. +`SPI` communication shares the same problems that [have been outlined](04-threadsafe-wire.md) for using `Wire` in a multithreaded environment. This is due to the fact that every `SPI` transaction consists of multiple function calls, i.e. + ```C++ digitalWrite(cs, LOW); SPI.beginTransaction(SPI_SETTINGS); @@ -11,7 +12,7 @@ rx = SPI.transfer(tx); SPI.endTransaction(); digitalWrite(cs, HIGH); ``` -Due to the preemptive nature of the underlying RTOS the execution of the `SPI` transaction can be interrupted at any point in time. This would leave both the `SPI` driver code and the client device in a undefined state. Similar as has been done for `Wire` this is solved by introducing a `BusDevice` which allows for single-function call, threadsafe, atomic SPI transactions. A `BusDevice` is declared simply by specifying the type of interface and its parameters: +Due to the preemptive nature of the underlying RTOS the execution of the `SPI` transaction can be interrupted at any point in time. This would leave both the `SPI` driver code and the client device in a undefined state. Similar to what has been implemented for `Wire`, Arduino Parallela solves this by introducing a `BusDevice` which allows for single-function calls. The Parallela API allows for thread-safe, atomic SPI transactions. A `BusDevice` is declared simply by specifying the type of the interface and its parameters: ```C++ int const DEVICE_CS_PIN = 4; void device_cs_select() { /* ... */ } @@ -24,53 +25,56 @@ BusDevice bmp388(SPI, DEVICE_CS_PIN, spi_clock, spi_bit_order, spi_bit_mode); BusDevice bmp388(SPI, device_cs_select, device_cs_deselect, spi_settings); ``` -### `transfer`/`wait` **asynchronous** threadsafe `SPI` access -Once a `BusDevice` is declared it can be used to transfer data to and from the peripheral by means of the `transfer()` API. As opposed to traditional Arduino bus APIs `transfer()` is asynchronous and thus won't block execution unless the `wait()` function is called. -Note that since we are in a parallel programming environment this means that calls to `transfer()` on the same bus from different sketches will be arbitrated and that the `wait()` API will suspend the sketch until the transfer is complete, thus allowing other processes to execute or going to low power state. +### Asynchronous thread-safe `SPI` access with `transfer`/`wait` +Once a `BusDevice` is declared it can be used to transfer data to and from the peripheral by means of the `transfer()` API. As opposed to the traditional Arduino bus APIs, `transfer()` is asynchronous and thus won't block execution unless the `wait()` function is called. +Note that we are in a parallel programming environment which means that calls to `transfer()` on the same bus from different sketches will be arbitrated. + ```C++ byte bmp388_read_reg(byte const reg_addr) { /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ - byte read_write_buf[] = {static_cast(0x80 | reg_addr), 0, 0}; + byte read_write_buffer[] = {0x80 | reg_addr, 0, 0}; - IoRequest req(read_write_buf, sizeof(read_write_buf), nullptr, 0); - IoResponse rsp = bmp388.transfer(req); + IoRequest request(read_write_buffer, sizeof(read_write_buffer), nullptr, 0); + IoResponse response = bmp388.transfer(request); /* Do other stuff */ - rsp->wait(); /* Wait for the completion of the IO Request. */ - - return read_write_buf[2]; + response->wait(); /* Wait for the completion of the IO Request. */ + auto value = read_write_buffer[2]; + return value; } ``` -### `transfer_and_wait` **synchronous** threadsafe `SPI` access +### Synchronous thread-safe `SPI` access with `transfer_and_wait` ([`examples/Threadsafe_IO/SPI`](../examples/Threadsafe_IO/SPI)) -As the use of the `transfer` API might be confusing there's also a synchronous API call combining the request of the transfer and waiting for it's result using `transfer_and_wait`. +As the use of the `transfer` API might be difficult to grasp there's also a synchronous API call combining the request of the transfer and waiting for its result using `transfer_and_wait`. ```C++ byte bmp388_read_reg(byte const reg_addr) { /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ - byte read_write_buf[] = {static_cast(0x80 | reg_addr), 0, 0}; + byte read_write_buffer[] = {0x80 | reg_addr, 0, 0}; - IoRequest req(read_write_buf, sizeof(read_write_buf), nullptr, 0); - IoResponse rsp = transfer_and_wait(bmp388, req); + IoRequest request(read_write_buffer, sizeof(read_write_buffer), nullptr, 0); + IoResponse response = transfer_and_wait(bmp388, request); - return read_write_buf[2]; + auto value = read_write_buffer[2]; + return value; } ``` -### `Adafruit_BusIO` style **synchronous** threadsafe `SPI` access +### `Adafruit_BusIO` style **synchronous** thread-safe `SPI` access ([`examples/Threadsafe_IO/SPI_BusIO`](../examples/Threadsafe_IO/SPI_BusIO)) For a further simplification [Adafruit_BusIO](https://github.com/adafruit/Adafruit_BusIO) style APIs are provided: + ```C++ byte bmp388_read_reg(byte const reg_addr) { /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ - byte write_buf[2] = {static_cast(0x80 | reg_addr), 0}; - byte read_buf = 0; + byte write_buffer[2] = {0x80 | reg_addr, 0}; + byte read_buffer = 0; - bmp388.spi().write_then_read(write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf)); - return read_buf; + bmp388.spi().write_then_read(write_buffer, sizeof(write_buffer), &read_buffer, sizeof(read_buffer)); + return read_buffer; } ``` diff --git a/docs/README.md b/docs/README.md index 3b6f6c9..7511394 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,16 +2,18 @@ `Arduino_Threads/docs` ====================== -The Arduino threading APIs brings multi-threading to the world of Arduino. +The Arduino threading APIs brings multi-threading to the world of Arduino. If you're new to the concept of threads we suggest you have first a look at the [Threading Basics](01-threading-basics.md) document. -The following Arduino architectures and boards are supported: +The following Arduino architectures and boards are currently supported: * `mbed_portenta`: [Portenta H7](https://store.arduino.cc/products/portenta-h7) * `mbed_nano`: [Nano 33 BLE](https://store.arduino.cc/arduino-nano-33-ble), [Nano RP2040 Connect](https://store.arduino.cc/nano-rp2040-connect) * `mbed_edge`: [Edge Control](https://store.arduino.cc/products/arduino-edge-control) +* `mbed_nicla`: [Nicla Sense ME](https://store.arduino.cc/products/nicla-sense-me), [Nicla Vision](http://store.arduino.cc/products/nicla-vision) -Threading on Arduino can be achieved by leveraging the [Arduino_Threads](https://github.com/bcmi-labs/Arduino_Threads) library in combination with [arduino-cli](https://github.com/facchinm/arduino-cli/commits/arduino_threads_rebased). -Download pre-built `arduino-cli`: +Threading with the above mentioned Arduino cores can be achieved by leveraging the [Arduino_Threads](https://github.com/bcmi-labs/Arduino_Threads) library in combination with the [Arduino Command Line Interface](https://github.com/facchinm/arduino-cli/commits/arduino_threads_rebased) (arduino-cli). + +Download a preliminary, pre-built `arduino-cli` binary for your operating system that supports threading: * [MacOS_64Bit](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_macOS_64bit.tar.gz) * [Linux_32Bit](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_Linux_32bit.tar.gz) * [Linux_64Bit](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_Linux_64bit.tar.gz) @@ -21,8 +23,11 @@ Download pre-built `arduino-cli`: * [Windows_64Bit](https://downloads.arduino.cc/tools/arduino-cli/inot_support/arduino-cli_git-snapshot_Windows_64bit.zip) ### Table of Contents -* [Threading Basics](01-threading-basics.md) -* [Data exchange between threads](02-data-exchange.md) -* [Threadsafe `Serial`](03-threadsafe-serial.md) -* [Threadsafe `Wire`](04-threadsafe-wire.md) -* [Threadsafe `SPI`](05-threadsafe-spi.md) + +In the following documents you will find more information about the topics covered by this library. + +1. [Threading Basics](01-threading-basics.md) +2. [Data exchange between threads](02-data-exchange.md) +3. [Threadsafe `Serial`](03-threadsafe-serial.md) +4. [Threadsafe `Wire`](04-threadsafe-wire.md) +5. [Threadsafe `SPI`](05-threadsafe-spi.md)