From c344ef24ef71783008c1a12ede5f59c407f25d32 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 16:28:21 +0100 Subject: [PATCH 01/41] Add questions and suggestions to main readme --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index c1f173b..8152b37 100644 --- a/README.md +++ b/README.md @@ -11,56 +11,56 @@ This library makes it easy to use the multi-threading capability of [Arduino](ht ## :zap: 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:. -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:. +`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. -`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. +### :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)). -#### 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)). +### :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)). -#### 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: From 54d78bcf5f252be10decb9de8ec20fbe88e06c3a Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 16:37:33 +0100 Subject: [PATCH 02/41] Add questions and suggestions to the main docs readme --- docs/README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 3b6f6c9..4b8e4a9 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) -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,6 +23,9 @@ 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 + +In the following documents you will find more information about the topics covered by this library. + * [Threading Basics](01-threading-basics.md) * [Data exchange between threads](02-data-exchange.md) * [Threadsafe `Serial`](03-threadsafe-serial.md) From 06e227d805f1d6cacd676823883b71c495283578 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 16:40:41 +0100 Subject: [PATCH 03/41] Add numbers to toc --- docs/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/README.md b/docs/README.md index 4b8e4a9..1c47f99 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,8 +26,8 @@ Download a preliminary, pre-built `arduino-cli` binary for your operating system In the following documents you will find more information about the topics covered by this library. -* [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) +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) From 6d8341d761cfdedb9257c9c5b16de2783cb6f766 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 17:05:19 +0100 Subject: [PATCH 04/41] Add introduction about single-thread limitations --- docs/01-threading-basics.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index f100c7b..60300a7 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -3,6 +3,8 @@ Threading Basics ================ ## Introduction +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. 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 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 fb8bb868d38abaab819935a858ab4834ebdda2b6 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 17:18:48 +0100 Subject: [PATCH 05/41] Add reference to example --- docs/01-threading-basics.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index 60300a7..2b63b54 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -5,7 +5,8 @@ Threading Basics ## Introduction 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. 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 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. +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 an Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once among those . However, an Arduino project can contain multiple `*.inot` files. Each `*.inot` file represents a separate thread and contains it's own `setup()` and `loop()` functions. Consequently each one of the `*.inot` files can take up a specific task that they perform independent from each other. To get back to our above mentioned example, the program in one `*.inot` file could read from the distance sensor, while the one in the other file could take care of controlling the servo motor. 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 f50b2b372615f6dcdc8f292d826c72358966289d Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 17:21:29 +0100 Subject: [PATCH 06/41] Add Caveats section --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8152b37..a96d821 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ [![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` file (t suffix stands for **t**hread) representing a clear separation of concerns on a file level. @@ -50,6 +50,10 @@ Although you are free to directly manipulate I/O requests and responses (e.g. [T +## :zap: Caveats + + + ## :mag_right: Resources * [How to install a library](https://www.arduino.cc/en/guide/libraries) From d196e43258f611e44506826877f019a481742fa8 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 17:28:28 +0100 Subject: [PATCH 07/41] Rephrase benefits paragraph --- 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 2b63b54..fdd5de2 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -8,7 +8,7 @@ Previously Arduino sketches didn't support the concept of multitasking, unless y 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 an Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once among those . However, an Arduino project can contain multiple `*.inot` files. Each `*.inot` file represents a separate thread and contains it's own `setup()` and `loop()` functions. Consequently each one of the `*.inot` files can take up a specific task that they perform independent from each other. To get back to our above mentioned example, the program in one `*.inot` file could read from the distance sensor, while the one in the other file could take care of controlling the servo motor. -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). +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. From 0eea4ba8e9ec70547bdd7a29a2e92b57f77e6486 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 17:33:59 +0100 Subject: [PATCH 08/41] Add note about single threaded example --- docs/01-threading-basics.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index fdd5de2..7b8afc2 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -11,8 +11,10 @@ In order to support multi-threaded (or parallel) sketch execution a new file typ 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() { @@ -49,6 +51,8 @@ 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. **Blink_Three_LEDs.ino** From 39c1a27b6656b754fb237e34995b5bc6da1da464 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 17:43:56 +0100 Subject: [PATCH 09/41] Add question about multi core --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a96d821..f75e3e6 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ Although you are free to directly manipulate I/O requests and responses (e.g. [T ## :zap: Caveats + ## :mag_right: Resources From f42f3b90f9aa4ae0c7a70d923c44308cc8350fed Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 17:44:21 +0100 Subject: [PATCH 10/41] Perform minor rewording --- docs/01-threading-basics.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index 7b8afc2..f44ef58 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -54,17 +54,20 @@ 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** @@ -106,9 +109,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. +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.). From ae19ee8da7a1bc141e908f6529b892e37c08cbdb Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 17:44:28 +0100 Subject: [PATCH 11/41] Add recommendation for naming --- docs/01-threading-basics.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index f44ef58..4a7f2dc 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -115,3 +115,5 @@ As you can see from the example the name of the `*.inot`-file is used to generat * are case sensitive. * 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 From 1a3fa18c0432f133e9bcc693168c65621a47a7a5 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 20:06:00 +0100 Subject: [PATCH 12/41] Add questions, comments and rephrasing to data exchange --- docs/02-data-exchange.md | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index 571bdbf..3e6923d 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -3,37 +3,40 @@ 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 . 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`, ... + A shared variable semantically represents a FIFO ([First-In/First-Out](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics))) queue and can therefore contain multiple values . The limit for the amount of queue entries can be specified and is 16 by default. If you need to specify a higher limit (e.g. 32 entries) you can define the shared variable like this: `Shared sensorReadings` . New values can be inserted into the queue by naturally using the assignment operator `=` the same way you do with 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. +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. +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. +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 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`. +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'. */ @@ -42,13 +45,13 @@ SINK(counter, int); /* Declaration of a data sink of type `int` with a internal /* 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: +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. +Whenever a new value is assigned to a data source , i.e. ```C++ /* DataProducerThread.inot */ counter = 10; @@ -62,12 +65,12 @@ Serial.println(counter); /* 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). +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. | From fb19b50456db0f72edd47fecd3d0332749b280ce Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Wed, 29 Dec 2021 20:06:29 +0100 Subject: [PATCH 13/41] Add note about hierarchy graphic --- docs/01-threading-basics.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index 4a7f2dc..5220dba 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -8,6 +8,8 @@ Previously Arduino sketches didn't support the concept of multitasking, unless y 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 an Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once among those . However, an Arduino project can contain multiple `*.inot` files. Each `*.inot` file represents a separate thread and contains it's own `setup()` and `loop()` functions. Consequently each one of the `*.inot` files can take up a specific task that they perform independent from each other. To get back to our above mentioned example, the program in one `*.inot` file could read from the distance sensor, while the one in the other file could take care of controlling the servo motor. + + 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): From 40bbd862c4c1b5e0a0200a959a9101561b27a0b7 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 3 Jan 2022 14:24:29 +0100 Subject: [PATCH 14/41] Add rephrasings and questions to Thread-safe serial --- docs/03-threadsafe-serial.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/03-threadsafe-serial.md b/docs/03-threadsafe-serial.md index 1bb4abc..f31b073 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. From d03a6f32192706eb9fe45e697fa393fa94dd88ec Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 3 Jan 2022 16:00:17 +0100 Subject: [PATCH 15/41] Add rewording and questions to threadsafe wire --- docs/04-threadsafe-wire.md | 69 +++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/docs/04-threadsafe-wire.md b/docs/04-threadsafe-wire.md index ead6c3d..5784d19 100644 --- a/docs/04-threadsafe-wire.md +++ b/docs/04-threadsafe-wire.md @@ -1,31 +1,28 @@ -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. -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: +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 */ @@ -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 and that calling the `wait()` API will suspend the sketch until the transfer is complete. This allows other processes to run or to go into low power state. + ```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; } ``` From 6b4dcb1cf5d5242ac3b7fd37e03ed177355fc5fa Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 3 Jan 2022 17:07:50 +0100 Subject: [PATCH 16/41] Add questions, comments and rephrasing to threadsafe SPI --- docs/05-threadsafe-spi.md | 52 +++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/docs/05-threadsafe-spi.md b/docs/05-threadsafe-spi.md index f3a139e..9108621 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 and that calling the `wait()` API will suspend the sketch until the transfer is complete. This allows other processes to run or to go into 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}; + byte read_write_buffer[] = {static_cast(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[] = {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); + 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: +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] = {static_cast(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; } ``` From 6a5ba462da9f65c666838cc9b1d301d0e9d393ff Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 3 Jan 2022 17:27:05 +0100 Subject: [PATCH 17/41] Clarify on comment --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f75e3e6..f4ec075 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ As a result this interruption by the scheduler will break Wire I/O access for bo `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)). +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)). From 1adf4cb91e2dd004fbb14bd98f8afe626a0b7e94 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 3 Jan 2022 17:27:45 +0100 Subject: [PATCH 18/41] Explain threading and blocking --- 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 5220dba..89118f7 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -3,7 +3,7 @@ Threading Basics ================ ## Introduction -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. 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. +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. 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 an Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once among those . However, an Arduino project can contain multiple `*.inot` files. Each `*.inot` file represents a separate thread and contains it's own `setup()` and `loop()` functions. Consequently each one of the `*.inot` files can take up a specific task that they perform independent from each other. To get back to our above mentioned example, the program in one `*.inot` file could read from the distance sensor, while the one in the other file could take care of controlling the servo motor. From 254880a009b7350e5ee3b57d94ba2b831808bdff Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Mon, 3 Jan 2022 17:29:47 +0100 Subject: [PATCH 19/41] Remove questions --- README.md | 10 +++++----- docs/01-threading-basics.md | 4 ++-- docs/02-data-exchange.md | 22 +++++++++++----------- docs/03-threadsafe-serial.md | 4 ++-- docs/04-threadsafe-wire.md | 8 ++++---- docs/05-threadsafe-spi.md | 8 ++++---- docs/README.md | 4 ++-- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index f4ec075..671c384 100644 --- a/README.md +++ b/README.md @@ -43,17 +43,17 @@ As a result this interruption by the scheduler will break Wire I/O access for bo `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)). +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)). +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)). + - ## :zap: Caveats - - + + ## :mag_right: Resources diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index 89118f7..5667cc3 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -6,9 +6,9 @@ Threading Basics 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. 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 an Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once among those . However, an Arduino project can contain multiple `*.inot` files. Each `*.inot` file represents a separate thread and contains it's own `setup()` and `loop()` functions. Consequently each one of the `*.inot` files can take up a specific task that they perform independent from each other. To get back to our above mentioned example, the program in one `*.inot` file could read from the distance sensor, while the one in the other file could take care of controlling the servo motor. +In order to support multi-threaded (or parallel) sketch execution a new file type called the `*.inot` file is introduced. While an Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once among those + - 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). diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index 3e6923d..d79635e 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -8,35 +8,35 @@ When an Arduino sketch formerly consisting of just a single `loop()` is split in `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 can be declared within a header file named `SharedVariables.h` which is automatically included at 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 semantically represents a FIFO ([First-In/First-Out](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics))) queue and can therefore contain multiple values . The limit for the amount of queue entries can be specified and is 16 by default. If you need to specify a higher limit (e.g. 32 entries) you can define the shared variable like this: `Shared sensorReadings` . New values can be inserted into the queue by naturally using the assignment operator `=` the same way you do with 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. +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 an item is removed from the queue within the shared variable and other threads can't access the read value anymore . + +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 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`. +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'. */ @@ -45,13 +45,13 @@ SINK(counter, int); /* Declaration of a data sink of type `int` with a internal /* 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: +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. +Whenever a new value is assigned to a data source , i.e. ```C++ /* DataProducerThread.inot */ counter = 10; @@ -65,12 +65,12 @@ Serial.println(counter); /* 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). +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 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 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).
| +| `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 f31b073..a60811c 100644 --- a/docs/03-threadsafe-serial.md +++ b/docs/03-threadsafe-serial.md @@ -5,7 +5,7 @@ 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 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. +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. @@ -48,7 +48,7 @@ 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 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. +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 */ diff --git a/docs/04-threadsafe-wire.md b/docs/04-threadsafe-wire.md index 5784d19..0c87931 100644 --- a/docs/04-threadsafe-wire.md +++ b/docs/04-threadsafe-wire.md @@ -22,7 +22,7 @@ Since we are using the [preemptive](https://os.mbed.com/docs/mbed-os/v6.11/progr 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: +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 */ @@ -33,7 +33,7 @@ BusDevice lsm6dsox(Wire, LSM6DSOX_ADDRESS, false /* restart */, true, /* stop */ ### 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 and that calling the `wait()` API will suspend the sketch until the transfer is complete. This allows other processes to run or to go into low power state. +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) @@ -55,7 +55,7 @@ byte lsm6dsox_read_reg(byte const reg_addr) ### 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 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`. +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) { @@ -72,7 +72,7 @@ byte lsm6dsox_read_reg(byte const reg_addr) ### `Adafruit_BusIO` style **synchronous** thread-safe `Wire` access ([`examples/Threadsafe_IO/Wire_BusIO`](../examples/Threadsafe_IO/Wire_BusIO)) -For 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) diff --git a/docs/05-threadsafe-spi.md b/docs/05-threadsafe-spi.md index 9108621..5089a6e 100644 --- a/docs/05-threadsafe-spi.md +++ b/docs/05-threadsafe-spi.md @@ -12,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 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: +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() { /* ... */ } @@ -27,7 +27,7 @@ BusDevice bmp388(SPI, device_cs_select, device_cs_deselect, spi_settings); ### 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 and that calling the `wait()` API will suspend the sketch until the transfer is complete. This allows other processes to run or to go into low power state. +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) @@ -47,7 +47,7 @@ byte bmp388_read_reg(byte const reg_addr) ### 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 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`. +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) { @@ -65,7 +65,7 @@ byte bmp388_read_reg(byte const reg_addr) ### `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: +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) diff --git a/docs/README.md b/docs/README.md index 1c47f99..12a3894 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,9 +9,9 @@ The following Arduino architectures and boards are currently supported: * `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 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). + +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) From ee2431657a871e5b28078c3fb28f0d29ae083fde Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Thu, 3 Mar 2022 16:08:47 +0100 Subject: [PATCH 20/41] Update docs/01-threading-basics.md Co-authored-by: Alexander Entinger --- 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 5667cc3..b2cbf93 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -6,7 +6,7 @@ Threading Basics 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. 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 an Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once among those +In order to support multi-threaded (or parallel) sketch execution a new file type called the `*.inot` file is introduced. While a single-threaded Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once. From 31ca9fd3c2074931fbd20bc3122ef23275daad6e Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Thu, 3 Mar 2022 16:08:56 +0100 Subject: [PATCH 21/41] Update docs/01-threading-basics.md Co-authored-by: Alexander Entinger --- docs/01-threading-basics.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index b2cbf93..e2793d1 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -9,7 +9,6 @@ In the historic single-threaded execution of Arduino sketches the complete progr In order to support multi-threaded (or parallel) sketch execution a new file type called the `*.inot` file is introduced. While a single-threaded Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once. - 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): From b1093c872e1b576c1e7c978898d162a6c3439946 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Thu, 3 Mar 2022 16:09:01 +0100 Subject: [PATCH 22/41] Update docs/01-threading-basics.md Co-authored-by: Alexander Entinger --- docs/01-threading-basics.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/01-threading-basics.md b/docs/01-threading-basics.md index e2793d1..bf98c36 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -8,7 +8,6 @@ Previously Arduino sketches didn't support the concept of multitasking, unless y 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 single-threaded Arduino project can contain multiple `*.ino` files you can define `setup()` or `loop()` only once. - 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): From 594d951da2c33a49ac06fd3f4c3bb808cdd4e87e Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 11:49:59 +0200 Subject: [PATCH 23/41] Update README.md Co-authored-by: Alexander Entinger --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 671c384..df282d1 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ As a result this interruption by the scheduler will break Wire I/O access for bo 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)). +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)). From a6b07eba0bed04a468d91857e85fc089bfcbf559 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 11:52:33 +0200 Subject: [PATCH 24/41] Update docs/01-threading-basics.md Co-authored-by: Alexander Entinger --- 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 bf98c36..bb8a9e2 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -6,7 +6,7 @@ Threading Basics 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. 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 single-threaded Arduino project can contain multiple `*.ino` files 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). From bb0a3a5398e7090f8e9e51e9d00b82e3fbc7447c Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 12:03:10 +0200 Subject: [PATCH 25/41] Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger --- docs/02-data-exchange.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index d79635e..2b9995d 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -3,7 +3,7 @@ Data Exchange between Threads ============================= ## Introduction -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. +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. From aa44e4a9ef1e9e41a9accfb6c98680240f65a27a Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 12:03:33 +0200 Subject: [PATCH 26/41] Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger --- docs/02-data-exchange.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index 2b9995d..b59144b 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -19,7 +19,7 @@ SHARED(counter, int); /* A globally available, threadsafe, shared variable of ty /* 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. +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++ From 190af3de81fd2dab9b1e710d5374335be888c789 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:11:06 +0200 Subject: [PATCH 27/41] Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger --- docs/02-data-exchange.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index b59144b..df5e4f8 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -27,7 +27,7 @@ Retrieving stored data works also very naturally like it would for any POD data 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 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` From 6b17c7fe2d258f9455f697581b790ebd33adf52b Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:11:18 +0200 Subject: [PATCH 28/41] Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger --- docs/02-data-exchange.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index df5e4f8..94c8eb2 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -31,7 +31,7 @@ Should the internal queue be empty when trying to read the latest available valu 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`. */ From b7d1c72f6b4fbec2c0089a6ab78e35a9c91d4b72 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:11:30 +0200 Subject: [PATCH 29/41] Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger --- docs/02-data-exchange.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index 94c8eb2..ef4fb3b 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -36,7 +36,7 @@ The idea behind the `Sink`/`Source` semantics is to model data exchange between /* 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`. +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'. */ From 3ae235263db9708bcd2ab825044ea6f2fc8039d3 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:11:40 +0200 Subject: [PATCH 30/41] Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger --- docs/02-data-exchange.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index ef4fb3b..82eb63a 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -45,7 +45,7 @@ SINK(counter, int); /* Declaration of a data sink of type `int` with a internal /* 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: +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); From b7fba916139f37886c441ae41afa0f91315c894b Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:13:36 +0200 Subject: [PATCH 31/41] Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger --- docs/02-data-exchange.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index 82eb63a..0334653 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -65,7 +65,7 @@ Serial.println(counter); /* 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). +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 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. From 01c5e6ad5225737ba16b861b3bbe4722e8f42685 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:13:42 +0200 Subject: [PATCH 32/41] Update docs/02-data-exchange.md Co-authored-by: Alexander Entinger --- docs/02-data-exchange.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index 0334653..8915482 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -51,7 +51,7 @@ In order to actually facilitate the flow of data from a source to a sink the sin DataProducerThread.counter.connectTo(DataConsumerThread_1.counter); DataProducerThread.counter.connectTo(DataConsumerThread_2.counter); ``` -Whenever a new value is assigned to a data source , i.e. +Whenever a new value is assigned to a data source, i.e. ```C++ /* DataProducerThread.inot */ counter = 10; From 3301b19763db797c6218b8cb2e47bdd010d028e8 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:13:48 +0200 Subject: [PATCH 33/41] Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger --- docs/05-threadsafe-spi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-threadsafe-spi.md b/docs/05-threadsafe-spi.md index 5089a6e..0a33c2f 100644 --- a/docs/05-threadsafe-spi.md +++ b/docs/05-threadsafe-spi.md @@ -27,7 +27,7 @@ BusDevice bmp388(SPI, device_cs_select, device_cs_deselect, spi_settings); ### 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 +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) From 5411ddee8b60fbc272ccaae0af3bb065b9158777 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:13:55 +0200 Subject: [PATCH 34/41] Update docs/04-threadsafe-wire.md Co-authored-by: Alexander Entinger --- docs/04-threadsafe-wire.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/04-threadsafe-wire.md b/docs/04-threadsafe-wire.md index 0c87931..4c16836 100644 --- a/docs/04-threadsafe-wire.md +++ b/docs/04-threadsafe-wire.md @@ -33,7 +33,7 @@ BusDevice lsm6dsox(Wire, LSM6DSOX_ADDRESS, false /* restart */, true, /* stop */ ### 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 +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) From f3534f332c257702643302b692ea01ac487b902d Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:14:42 +0200 Subject: [PATCH 35/41] Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger --- docs/05-threadsafe-spi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-threadsafe-spi.md b/docs/05-threadsafe-spi.md index 0a33c2f..15ca8e6 100644 --- a/docs/05-threadsafe-spi.md +++ b/docs/05-threadsafe-spi.md @@ -33,7 +33,7 @@ Note that we are in a parallel programming environment which means that calls to byte bmp388_read_reg(byte const reg_addr) { /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ - byte read_write_buffer[] = {static_cast(0x80 | reg_addr), 0, 0}; + byte read_write_buffer[] = {0x80 | reg_addr, 0, 0}; IoRequest request(read_write_buffer, sizeof(read_write_buffer), nullptr, 0); IoResponse response = bmp388.transfer(request); From c29ef48362a453f428f16ce5e4ab24190b8b5394 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:14:55 +0200 Subject: [PATCH 36/41] Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger --- docs/05-threadsafe-spi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-threadsafe-spi.md b/docs/05-threadsafe-spi.md index 15ca8e6..10dde33 100644 --- a/docs/05-threadsafe-spi.md +++ b/docs/05-threadsafe-spi.md @@ -52,7 +52,7 @@ As the use of the `transfer` API might be difficult to grasp there's also a sync byte bmp388_read_reg(byte const reg_addr) { /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ - byte read_write_buffer[] = {static_cast(0x80 | reg_addr), 0, 0}; + byte read_write_buffer[] = {0x80 | reg_addr, 0, 0}; IoRequest request(read_write_buffer, sizeof(read_write_buffer), nullptr, 0); IoResponse response = transfer_and_wait(bmp388, request); From e52e8ecdb2ec79bba8aaa16bbc356dc63ac62a91 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:15:04 +0200 Subject: [PATCH 37/41] Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger --- docs/05-threadsafe-spi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-threadsafe-spi.md b/docs/05-threadsafe-spi.md index 10dde33..72cf462 100644 --- a/docs/05-threadsafe-spi.md +++ b/docs/05-threadsafe-spi.md @@ -71,7 +71,7 @@ For a further simplification [Adafruit_BusIO](https://github.com/adafruit/Adafru byte bmp388_read_reg(byte const reg_addr) { /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ - byte write_buffer[2] = {static_cast(0x80 | reg_addr), 0}; + byte write_buffer[2] = {0x80 | reg_addr, 0}; byte read_buffer = 0; bmp388.spi().write_then_read(write_buffer, sizeof(write_buffer), &read_buffer, sizeof(read_buffer)); From 5d8e55cdfbd8b8f77b109eadb114402fd9652e30 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:15:48 +0200 Subject: [PATCH 38/41] Update docs/README.md Co-authored-by: Alexander Entinger --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index 12a3894..7511394 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ 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 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). From c7aa0fae54c7796368ea528ba28d5cc10d414325 Mon Sep 17 00:00:00 2001 From: Sebastian Romero Date: Tue, 7 Jun 2022 13:31:12 +0200 Subject: [PATCH 39/41] Update docs/05-threadsafe-spi.md Co-authored-by: Alexander Entinger --- docs/05-threadsafe-spi.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/05-threadsafe-spi.md b/docs/05-threadsafe-spi.md index 72cf462..c7189b6 100644 --- a/docs/05-threadsafe-spi.md +++ b/docs/05-threadsafe-spi.md @@ -47,7 +47,7 @@ byte bmp388_read_reg(byte const reg_addr) ### 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 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`. +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) { From 048e447e80b82d064b597f1bd13ac0681713b897 Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Wed, 8 Jun 2022 11:19:59 +0200 Subject: [PATCH 40/41] Update docs/01-threading-basics.md --- 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 bb8a9e2..3de1b15 100644 --- a/docs/01-threading-basics.md +++ b/docs/01-threading-basics.md @@ -5,7 +5,7 @@ Threading Basics ## Introduction 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. -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 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). From 5e8e71ffd266fcd7bf86f04b181f048ceb9c9cc8 Mon Sep 17 00:00:00 2001 From: Alexander Entinger Date: Wed, 8 Jun 2022 11:20:15 +0200 Subject: [PATCH 41/41] Update docs/02-data-exchange.md --- docs/02-data-exchange.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/02-data-exchange.md b/docs/02-data-exchange.md index 8915482..75c08f4 100644 --- a/docs/02-data-exchange.md +++ b/docs/02-data-exchange.md @@ -13,7 +13,8 @@ A `Shared` variable is a global variable accessible to all threads. It can be de /* SharedVariables.h */ SHARED(counter, int); /* A globally available, threadsafe, shared variable of type 'int'. */ ``` - +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 modelled 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 */