diff --git a/.codespellrc b/.codespellrc index 5af8717..9e62ae5 100644 --- a/.codespellrc +++ b/.codespellrc @@ -2,7 +2,7 @@ [codespell] # In the event of a false positive, add the problematic word, in all lowercase, to a comma-separated list here: ignore-words-list = inot -skip = ./.git builtin = clear,informal,en-GB_to_en-US check-filenames = check-hidden = +skip = ./.git diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b93afff --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# See: https://docs.github.com/en/code-security/supply-chain-security/configuration-options-for-dependency-updates#about-the-dependabotyml-file +version: 2 + +updates: + # Configure check for outdated GitHub Actions actions in workflows. + # See: https://docs.github.com/en/code-security/supply-chain-security/keeping-your-actions-up-to-date-with-dependabot + - package-ecosystem: github-actions + directory: / # Check the repository's workflows under /.github/workflows/ + schedule: + interval: daily + labels: + - "topic: infrastructure" diff --git a/.github/workflows/check-arduino.yml b/.github/workflows/check-arduino.yml index 6e3035d..0d03f48 100644 --- a/.github/workflows/check-arduino.yml +++ b/.github/workflows/check-arduino.yml @@ -21,7 +21,7 @@ jobs: - name: Arduino Lint uses: arduino/arduino-lint-action@v1 with: - compliance: strict + compliance: specification # Change this to "update" once the library is added to the index. library-manager: submit # Always use this setting for official repositories. Remove for 3rd party projects. diff --git a/README.md b/README.md index 46907a7..e5e7c93 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ + + `Arduino_Threads` ================= @@ -5,15 +7,53 @@ [![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. +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 -### :thread: Multi-threaded sketch execution +### :thread: Multi-Threading +#### :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. -### :calling: Easy communication between multiple threads +#### :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. +### :thread: Threadsafe IO +#### :thread: 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: + +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: + +```C++ +/* Wire Write Access */ +Wire.beginTransmission(addr); +Wire.write(val); +Wire.endTransmission(); + +/* Wire Read Access */ +Wire.beginTransmission(addr); +Wire.write(val); +Wire.endTransmission(); +Wire.requestFrom(addr, bytes) +while(Wire.available()) { + int val = Wire.read(); +} +``` + +Since we are using [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. + +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_ThreadsafeIO` 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. + +#### :zzz: 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_SPI/Threadsafe_SPI.ino)). + +#### :sparkling_heart: Convenient API + +Although you are free to directly manipulate IO requests and responses (e.g. [Threadsafe_Wire](examples/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_Wire_BusIO/Threadsafe_Wire_BusIO.ino)). + + ## :mag_right: Resources * [How to install a library](https://www.arduino.cc/en/guide/libraries) diff --git a/examples/Threadsafe_SPI/Threadsafe_SPI.ino b/examples/Threadsafe_SPI/Threadsafe_SPI.ino new file mode 100644 index 0000000..ddd7b8c --- /dev/null +++ b/examples/Threadsafe_SPI/Threadsafe_SPI.ino @@ -0,0 +1,89 @@ +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +/************************************************************************************** + * CONSTANTS + **************************************************************************************/ + +static int const BMP388_CS_PIN = 2; +static int const BMP388_INT_PIN = 6; +static byte const BMP388_CHIP_ID_REG_ADDR = 0x00; + +static size_t constexpr NUM_THREADS = 20; + +/************************************************************************************** + * FUNCTION DECLARATION + **************************************************************************************/ + +byte bmp388_read_reg(byte const reg_addr); +void bmp388_thread_func(); + +/************************************************************************************** + * GLOBAL VARIABLES + **************************************************************************************/ + +BusDevice bmp388(SPI, BMP388_CS_PIN, 1000000, MSBFIRST, SPI_MODE0); + +static char thread_name[NUM_THREADS][32]; + +/************************************************************************************** + * SETUP/LOOP + **************************************************************************************/ + +void setup() +{ + Serial.begin(9600); + while (!Serial) { } + + pinMode(BMP388_CS_PIN, OUTPUT); + digitalWrite(BMP388_CS_PIN, HIGH); + + for(size_t i = 0; i < NUM_THREADS; i++) + { + snprintf(thread_name[i], sizeof(thread_name[i]), "Thread #%02d", i); + rtos::Thread * t = new rtos::Thread(osPriorityNormal, OS_STACK_SIZE, nullptr, thread_name[i]); + t->start(bmp388_thread_func); + } +} + +void loop() +{ + +} + +/************************************************************************************** + * FUNCTION DEFINITION + **************************************************************************************/ + +byte bmp388_read_reg(byte const reg_addr) +{ + /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ + byte read_write_buf[] = {static_cast(0x80 | reg_addr), 0, 0}; + + IoRequest req(read_write_buf, sizeof(read_write_buf), nullptr, 0); + IoResponse rsp = bmp388.transfer(req); + + /* Do other stuff */ + + rsp->wait(); + + return read_write_buf[2]; +} + +void bmp388_thread_func() +{ + for(;;) + { + /* Sleep between 5 and 500 ms */ + rtos::ThisThread::sleep_for(rtos::Kernel::Clock::duration_u32(random(5,500))); + /* Try to read some data from the BMP3888. */ + byte const chip_id = bmp388_read_reg(BMP388_CHIP_ID_REG_ADDR); + /* Print thread id and chip id value to serial. */ + char msg[64] = {0}; + snprintf(msg, sizeof(msg), "%s: Chip ID = 0x%X", rtos::ThisThread::get_name(), chip_id); + Serial.println(msg); + } +} diff --git a/examples/Threadsafe_SPI_BusIO/Threadsafe_SPI_BusIO.ino b/examples/Threadsafe_SPI_BusIO/Threadsafe_SPI_BusIO.ino new file mode 100644 index 0000000..6d5f0c5 --- /dev/null +++ b/examples/Threadsafe_SPI_BusIO/Threadsafe_SPI_BusIO.ino @@ -0,0 +1,84 @@ +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +/************************************************************************************** + * CONSTANTS + **************************************************************************************/ + +static int const BMP388_CS_PIN = 2; +static int const BMP388_INT_PIN = 6; +static byte const BMP388_CHIP_ID_REG_ADDR = 0x00; + +static size_t constexpr NUM_THREADS = 20; + +/************************************************************************************** + * FUNCTION DECLARATION + **************************************************************************************/ + +byte bmp388_read_reg(byte const reg_addr); +void bmp388_thread_func(); + +/************************************************************************************** + * GLOBAL VARIABLES + **************************************************************************************/ + +BusDevice bmp388(SPI, BMP388_CS_PIN, 1000000, MSBFIRST, SPI_MODE0); + +static char thread_name[NUM_THREADS][32]; + +/************************************************************************************** + * SETUP/LOOP + **************************************************************************************/ + +void setup() +{ + Serial.begin(9600); + while (!Serial) { } + + pinMode(BMP388_CS_PIN, OUTPUT); + digitalWrite(BMP388_CS_PIN, HIGH); + + for(size_t i = 0; i < NUM_THREADS; i++) + { + snprintf(thread_name[i], sizeof(thread_name[i]), "Thread #%02d", i); + rtos::Thread * t = new rtos::Thread(osPriorityNormal, OS_STACK_SIZE, nullptr, thread_name[i]); + t->start(bmp388_thread_func); + } +} + +void loop() +{ + +} + +/************************************************************************************** + * FUNCTION DEFINITION + **************************************************************************************/ + +byte bmp388_read_reg(byte const reg_addr) +{ + /* REG_ADDR | DUMMY_BYTE | REG_VAL is on SDO */ + byte write_buf[2] = {static_cast(0x80 | reg_addr), 0}; + byte read_buf = 0; + + bmp388.spi().write_then_read(write_buf, sizeof(write_buf), &read_buf, sizeof(read_buf)); + return read_buf; +} + +void bmp388_thread_func() +{ + for(;;) + { + /* Sleep between 5 and 500 ms */ + rtos::ThisThread::sleep_for(rtos::Kernel::Clock::duration_u32(random(5,500))); + /* Try to read some data from the BMP3888. */ + byte const chip_id = bmp388_read_reg(BMP388_CHIP_ID_REG_ADDR); + /* Print thread id and chip id value to serial. */ + char msg[64] = {0}; + snprintf(msg, sizeof(msg), "%s: Chip ID = 0x%X", rtos::ThisThread::get_name(), chip_id); + Serial.println(msg); + } +} diff --git a/examples/Threadsafe_Serial_GlobalPrefixSuffix/Threadsafe_Serial_GlobalPrefixSuffix.ino b/examples/Threadsafe_Serial_GlobalPrefixSuffix/Threadsafe_Serial_GlobalPrefixSuffix.ino new file mode 100644 index 0000000..a22fe04 --- /dev/null +++ b/examples/Threadsafe_Serial_GlobalPrefixSuffix/Threadsafe_Serial_GlobalPrefixSuffix.ino @@ -0,0 +1,87 @@ +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +/************************************************************************************** + * CONSTANTS + **************************************************************************************/ + +static size_t constexpr NUM_THREADS = 5; + +/************************************************************************************** + * FUNCTION DECLARATION + **************************************************************************************/ + +String serial_log_message_prefix(String const & /* msg */); +String serial_log_message_suffix(String const & prefix, String const & msg); +void serial_thread_func(); + +/************************************************************************************** + * GLOBAL VARIABLES + **************************************************************************************/ + +static char thread_name[NUM_THREADS][32]; +#undef Serial +#ifdef ARDUINO_PORTENTA_H7_M4 + SerialDispatcher Serial(Serial1); /* No SerialUSB for Portenta H7 / M4 Core */ +#else + SerialDispatcher Serial(SerialUSB); +#endif + +/************************************************************************************** + * SETUP/LOOP + **************************************************************************************/ + +void setup() +{ + Serial.global_prefix(serial_log_message_prefix); + Serial.global_suffix(serial_log_message_suffix); + + /* Fire up some threads all accessing the LSM6DSOX */ + for(size_t i = 0; i < NUM_THREADS; i++) + { + snprintf(thread_name[i], sizeof(thread_name[i]), "Thread #%02d", i); + rtos::Thread * t = new rtos::Thread(osPriorityNormal, OS_STACK_SIZE, nullptr, thread_name[i]); + t->start(serial_thread_func); + } +} + +void loop() +{ + +} + +/************************************************************************************** + * FUNCTION DEFINITION + **************************************************************************************/ + +String serial_log_message_prefix(String const & /* msg */) +{ + char msg[32] = {0}; + snprintf(msg, sizeof(msg), "[%05lu] ", millis()); + return String(msg); +} + +String serial_log_message_suffix(String const & prefix, String const & msg) +{ + return String("\r\n"); +} + +void serial_thread_func() +{ + Serial.begin(9600); + + for(;;) + { + /* Sleep between 5 and 500 ms */ + rtos::ThisThread::sleep_for(rtos::Kernel::Clock::duration_u32(random(5,500))); + /* Print a unbroken log message including thread name and timestamp as a prefix. */ + Serial.block(); + Serial.print(rtos::ThisThread::get_name()); + Serial.print(": "); + Serial.print("Lorem ipsum ..."); + Serial.unblock(); + } +} diff --git a/examples/Threadsafe_Serial_ProtocolWrapping/Threadsafe_Serial_ProtocolWrapping.ino b/examples/Threadsafe_Serial_ProtocolWrapping/Threadsafe_Serial_ProtocolWrapping.ino new file mode 100644 index 0000000..e6d7549 --- /dev/null +++ b/examples/Threadsafe_Serial_ProtocolWrapping/Threadsafe_Serial_ProtocolWrapping.ino @@ -0,0 +1,110 @@ +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +/************************************************************************************** + * CONSTANTS + **************************************************************************************/ + +static size_t constexpr NUM_THREADS = 5; + +/************************************************************************************** + * FUNCTION DECLARATION + **************************************************************************************/ + +void serial_thread_func(); + +/************************************************************************************** + * GLOBAL VARIABLES + **************************************************************************************/ + +static char thread_name[NUM_THREADS][32]; +#undef Serial +#ifdef ARDUINO_PORTENTA_H7_M4 + SerialDispatcher Serial(Serial1); /* No SerialUSB for Portenta H7 / M4 Core */ +#else + SerialDispatcher Serial(SerialUSB); +#endif + +/************************************************************************************** + * SETUP/LOOP + **************************************************************************************/ + +void setup() +{ + /* Fire up some threads all accessing the LSM6DSOX */ + for(size_t i = 0; i < NUM_THREADS; i++) + { + snprintf(thread_name[i], sizeof(thread_name[i]), "Thread #%02d", i); + rtos::Thread * t = new rtos::Thread(osPriorityNormal, OS_STACK_SIZE, nullptr, thread_name[i]); + t->start(serial_thread_func); + } +} + +void loop() +{ + +} + +/************************************************************************************** + * FUNCTION DEFINITION + **************************************************************************************/ + +String nmea_message_prefix(String const & /* msg */) +{ + return String("$"); +} + +String nmea_message_suffix(String const & prefix, String const & msg) +{ + /* NMEA checksum is calculated over the complete message + * starting with '$' and ending with the end of the message. + */ + byte checksum = 0; + std::for_each(msg.c_str(), + msg.c_str() + msg.length(), + [&checksum](char const c) + { + checksum ^= static_cast(c); + }); + /* Assemble the footer of the NMEA message. */ + char footer[16] = {0}; + snprintf(footer, sizeof(footer), "*%02X\r\n", checksum); + return String(footer); +} + +void serial_thread_func() +{ + Serial.begin(9600); + + Serial.prefix(nmea_message_prefix); + Serial.suffix(nmea_message_suffix); + + for(;;) + { + /* Sleep between 5 and 500 ms */ + rtos::ThisThread::sleep_for(rtos::Kernel::Clock::duration_u32(random(5,500))); + + /* Print a fake NMEA GPRMC message: + * $GPRMC,062101.714,A,5001.869,N,01912.114,E,955535.7,116.2,290520,000.0,W*45\r\n + */ + Serial.block(); + + Serial.print("GPRMC,"); + Serial.print(millis()); + Serial.print(",A,"); + Serial.print("5001.869,"); + Serial.print("N,"); + Serial.print("01912.114,"); + Serial.print("E,"); + Serial.print("955535.7,"); + Serial.print("116.2,"); + Serial.print("290520,"); + Serial.print("000.0,"); + Serial.print("W"); + + Serial.unblock(); + } +} diff --git a/examples/Threadsafe_Serial_Reader/Threadsafe_Serial_Reader.ino b/examples/Threadsafe_Serial_Reader/Threadsafe_Serial_Reader.ino new file mode 100644 index 0000000..643675f --- /dev/null +++ b/examples/Threadsafe_Serial_Reader/Threadsafe_Serial_Reader.ino @@ -0,0 +1,79 @@ +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +/************************************************************************************** + * CONSTANTS + **************************************************************************************/ + +static size_t constexpr NUM_THREADS = 5; + +/************************************************************************************** + * FUNCTION DECLARATION + **************************************************************************************/ + +void serial_thread_func(); + +/************************************************************************************** + * GLOBAL VARIABLES + **************************************************************************************/ + +static char thread_name[NUM_THREADS][32]; +#undef Serial +#ifdef ARDUINO_PORTENTA_H7_M4 + SerialDispatcher Serial(Serial1); /* No SerialUSB for Portenta H7 / M4 Core */ +#else + SerialDispatcher Serial(SerialUSB); +#endif + +/************************************************************************************** + * SETUP/LOOP + **************************************************************************************/ + +void setup() +{ + /* Fire up some threads all accessing the LSM6DSOX */ + for(size_t i = 0; i < NUM_THREADS; i++) + { + snprintf(thread_name[i], sizeof(thread_name[i]), "Thread #%02d", i); + rtos::Thread * t = new rtos::Thread(osPriorityNormal, OS_STACK_SIZE, nullptr, thread_name[i]); + t->start(serial_thread_func); + } +} + +void loop() +{ + +} + +/************************************************************************************** + * FUNCTION DEFINITION + **************************************************************************************/ + +void serial_thread_func() +{ + Serial.begin(9600); + + for(;;) + { + /* Sleep between 5 and 500 ms */ + rtos::ThisThread::sleep_for(rtos::Kernel::Clock::duration_u32(random(5,500))); + + /* Read data from the serial interface into a String. */ + String serial_msg; + while (Serial.available()) + serial_msg += (char)Serial.read(); + + /* Print thread id and chip id value to serial. */ + if (serial_msg.length()) + { + char msg[64] = {0}; + snprintf(msg, sizeof(msg), "[%05lu] %s: %s ...", millis(), rtos::ThisThread::get_name(), serial_msg.c_str()); + Serial.block(); + Serial.println(msg); + Serial.unblock(); + } + } +} diff --git a/examples/Threadsafe_Serial_Writer/Threadsafe_Serial_Writer.ino b/examples/Threadsafe_Serial_Writer/Threadsafe_Serial_Writer.ino new file mode 100644 index 0000000..42ae35d --- /dev/null +++ b/examples/Threadsafe_Serial_Writer/Threadsafe_Serial_Writer.ino @@ -0,0 +1,74 @@ +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +/************************************************************************************** + * CONSTANTS + **************************************************************************************/ + +static size_t constexpr NUM_THREADS = 5; + +/************************************************************************************** + * FUNCTION DECLARATION + **************************************************************************************/ + +void serial_thread_func(); + +/************************************************************************************** + * GLOBAL VARIABLES + **************************************************************************************/ + +static char thread_name[NUM_THREADS][32]; +#undef Serial +#ifdef ARDUINO_PORTENTA_H7_M4 + SerialDispatcher Serial(Serial1); /* No SerialUSB for Portenta H7 / M4 Core */ +#else + SerialDispatcher Serial(SerialUSB); +#endif + +/************************************************************************************** + * SETUP/LOOP + **************************************************************************************/ + +void setup() +{ + /* Fire up some threads all accessing the LSM6DSOX */ + for(size_t i = 0; i < NUM_THREADS; i++) + { + snprintf(thread_name[i], sizeof(thread_name[i]), "Thread #%02d", i); + rtos::Thread * t = new rtos::Thread(osPriorityNormal, OS_STACK_SIZE, nullptr, thread_name[i]); + t->start(serial_thread_func); + } +} + +void loop() +{ + +} + +/************************************************************************************** + * FUNCTION DEFINITION + **************************************************************************************/ + +void serial_thread_func() +{ + Serial.begin(9600); + + for(;;) + { + /* Sleep between 5 and 500 ms */ + rtos::ThisThread::sleep_for(rtos::Kernel::Clock::duration_u32(random(5,500))); + /* Print thread id and chip id value to serial. */ + char msg[64] = {0}; + snprintf(msg, sizeof(msg), "[%05lu] %s: Lorem ipsum ...", millis(), rtos::ThisThread::get_name()); + /* Every Serial.print/println() encapsulated between + * block/unblock statements will only be printed after + * a block statement has occurred. + */ + Serial.block(); + Serial.println(msg); + Serial.unblock(); + } +} diff --git a/examples/Threadsafe_Wire/Threadsafe_Wire.ino b/examples/Threadsafe_Wire/Threadsafe_Wire.ino new file mode 100644 index 0000000..0ebba48 --- /dev/null +++ b/examples/Threadsafe_Wire/Threadsafe_Wire.ino @@ -0,0 +1,90 @@ +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +/************************************************************************************** + * CONSTANTS + **************************************************************************************/ + +static byte constexpr LSM6DSOX_ADDRESS = 0x6A; +static byte constexpr LSM6DSOX_WHO_AM_I_REG = 0x0F; + +static size_t constexpr NUM_THREADS = 20; + +/************************************************************************************** + * FUNCTION DECLARATION + **************************************************************************************/ + +byte lsm6dsox_read_reg(byte const reg_addr); +void lsm6dsox_thread_func(); + +/************************************************************************************** + * GLOBAL VARIABLES + **************************************************************************************/ + +BusDevice lsm6dsox(Wire, LSM6DSOX_ADDRESS); + +static char thread_name[NUM_THREADS][32]; + +/************************************************************************************** + * SETUP/LOOP + **************************************************************************************/ + +void setup() +{ + Serial.begin(9600); + while (!Serial) { } + + /* Fire up some threads all accessing the LSM6DSOX */ + for(size_t i = 0; i < NUM_THREADS; i++) + { + snprintf(thread_name[i], sizeof(thread_name[i]), "Thread #%02d", i); + rtos::Thread * t = new rtos::Thread(osPriorityNormal, OS_STACK_SIZE, nullptr, thread_name[i]); + t->start(lsm6dsox_thread_func); + } +} + +void loop() +{ + +} + +/************************************************************************************** + * FUNCTION DEFINITION + **************************************************************************************/ + +byte lsm6dsox_read_reg(byte const reg_addr) +{ + /* As we need only 1 byte large write/read buffers for this IO transaction + * the buffers are not arrays but rather simple variables. + */ + byte write_buf = reg_addr; + byte read_buf = 0; + + IoRequest req(write_buf, read_buf); + IoResponse rsp = lsm6dsox.transfer(req); + + /* Optionally do other stuff */ + + rsp->wait(); + + return read_buf; +} + + +void lsm6dsox_thread_func() +{ + for(;;) + { + /* Sleep between 5 and 500 ms */ + rtos::ThisThread::sleep_for(rtos::Kernel::Clock::duration_u32(random(5,500))); + /* Try to read some data from the LSM6DSOX. */ + byte const who_am_i = lsm6dsox_read_reg(LSM6DSOX_WHO_AM_I_REG); + /* Print thread id and chip id value to serial. */ + char msg[64] = {0}; + snprintf(msg, sizeof(msg), "%s: LSM6DSOX[WHO_AM_I] = 0x%X", rtos::ThisThread::get_name(), who_am_i); + Serial.println(msg); + } +} diff --git a/examples/Threadsafe_Wire_BusIO/Threadsafe_Wire_BusIO.ino b/examples/Threadsafe_Wire_BusIO/Threadsafe_Wire_BusIO.ino new file mode 100644 index 0000000..ae934a4 --- /dev/null +++ b/examples/Threadsafe_Wire_BusIO/Threadsafe_Wire_BusIO.ino @@ -0,0 +1,78 @@ + /************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +/************************************************************************************** + * CONSTANTS + **************************************************************************************/ + +static byte constexpr LSM6DSOX_ADDRESS = 0x6A; +static byte constexpr LSM6DSOX_WHO_AM_I_REG = 0x0F; + +static size_t constexpr NUM_THREADS = 20; + +/************************************************************************************** + * FUNCTION DECLARATION + **************************************************************************************/ + +byte lsm6dsox_read_reg(byte const reg_addr); +void lsm6dsox_thread_func(); + +/************************************************************************************** + * GLOBAL VARIABLES + **************************************************************************************/ + +BusDevice lsm6dsox(Wire, LSM6DSOX_ADDRESS); + +static char thread_name[NUM_THREADS][32]; + +/************************************************************************************** + * SETUP/LOOP + **************************************************************************************/ + +void setup() +{ + Serial.begin(9600); + while (!Serial) { } + + /* Fire up some threads all accessing the LSM6DSOX */ + for(size_t i = 0; i < NUM_THREADS; i++) + { + snprintf(thread_name[i], sizeof(thread_name[i]), "Thread #%02d", i); + rtos::Thread * t = new rtos::Thread(osPriorityNormal, OS_STACK_SIZE, nullptr, thread_name[i]); + t->start(lsm6dsox_thread_func); + } +} + +void loop() +{ + +} + +/************************************************************************************** + * FUNCTION DEFINITION + **************************************************************************************/ + +byte lsm6dsox_read_reg(byte reg_addr) +{ + byte read_buf = 0; + lsm6dsox.wire().write_then_read(®_addr, 1, &read_buf, 1); + return read_buf; +} + +void lsm6dsox_thread_func() +{ + for(;;) + { + /* Sleep between 5 and 500 ms */ + rtos::ThisThread::sleep_for(rtos::Kernel::Clock::duration_u32(random(5,500))); + /* Try to read some data from the LSM6DSOX. */ + byte const who_am_i = lsm6dsox_read_reg(LSM6DSOX_WHO_AM_I_REG); + /* Print thread id and chip id value to serial. */ + char msg[64] = {0}; + snprintf(msg, sizeof(msg), "%s: LSM6DSOX[WHO_AM_I] = 0x%X", rtos::ThisThread::get_name(), who_am_i); + Serial.println(msg); + } +} diff --git a/keywords.txt b/keywords.txt index 099b24d..f871002 100644 --- a/keywords.txt +++ b/keywords.txt @@ -1,5 +1,5 @@ ####################################### -# Syntax Coloring Map For Arduino_Threads +# Syntax Coloring Map for Arduino_Threads ####################################### ####################################### @@ -8,6 +8,14 @@ Shared KEYWORD1 +IoRequest KEYWORD1 +IoResponse KEYWORD1 +SpiBusDevice KEYWORD1 +SpiBusDeviceConfig KEYWORD1 +WireBusDevice KEYWORD1 +WireBusDeviceConfig KEYWORD1 +BusDevice KEYWORD1 + ####################################### # Methods and Functions (KEYWORD2) ####################################### @@ -18,6 +26,20 @@ broadcastEvent KEYWORD2 sendEvent KEYWORD2 setLoopDelay KEYWORD2 +transfer KEYWORD2 +create KEYWORD2 +block KEYWORD2 +unblock KEYWORD2 +prefix KEYWORD2 +suffix KEYWORD2 +global_prefix KEYWORD2 +global_suffix KEYWORD2 +spi KEYWORD2 +wire KEYWORD2 +read KEYWORD2 +write KEYWORD2 +write_then_read KEYWORD2 + ####################################### # Constants (LITERAL1) ####################################### diff --git a/library.properties b/library.properties index 05de5ff..aca4467 100644 --- a/library.properties +++ b/library.properties @@ -6,5 +6,5 @@ sentence=Easy multi-threading for your Mbed OS-based Arduino. paragraph=This library allows an easy access to the multi-threading capability inherent in all Mbed OS-based Arduino boards. category=Other url=https://github.com/bcmi-labs/Arduino_Threads -architectures=mbed +architectures=mbed,mbed_portenta,mbed_nano includes=Arduino_Threads.h diff --git a/src/Arduino_ThreadsafeIO.h b/src/Arduino_ThreadsafeIO.h new file mode 100644 index 0000000..46bbb72 --- /dev/null +++ b/src/Arduino_ThreadsafeIO.h @@ -0,0 +1,31 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef ARDUINO_THREADSAFE_IO_H_ +#define ARDUINO_THREADSAFE_IO_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "BusDevice.h" +#include "spi/SpiBusDevice.h" +#include "wire/WireBusDevice.h" +#include "serial/SerialDispatcher.h" + +#endif /* ARDUINO_THREADSAFE_IO_H_ */ diff --git a/src/BusDevice.cpp b/src/BusDevice.cpp new file mode 100644 index 0000000..6278aa0 --- /dev/null +++ b/src/BusDevice.cpp @@ -0,0 +1,121 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "BusDevice.h" + +#include "spi/SpiBusDevice.h" +#include "wire/WireBusDevice.h" + +/************************************************************************************** + * BusDeviceBase PUBLIC MEMBER FUNCTIONS + **************************************************************************************/ + +BusDevice BusDeviceBase::create(arduino::HardwareSPI & spi, int const cs_pin, SPISettings const & spi_settings, byte const fill_symbol) +{ + return BusDevice(new SpiBusDevice(SpiBusDeviceConfig{spi, + spi_settings, + cs_pin, + fill_symbol + })); +} + +BusDevice BusDeviceBase::create(arduino::HardwareSPI & spi, int const cs_pin, uint32_t const spi_clock, BitOrder const spi_bit_order, SPIMode const spi_bit_mode, byte const fill_symbol) +{ + return BusDevice(new SpiBusDevice(SpiBusDeviceConfig{spi, + SPISettings(spi_clock, spi_bit_order, spi_bit_mode), + cs_pin, + fill_symbol + })); +} + +BusDevice BusDeviceBase::create(arduino::HardwareSPI & spi, SpiBusDeviceConfig::SpiSelectFunc spi_select, SpiBusDeviceConfig::SpiDeselectFunc spi_deselect, SPISettings const & spi_settings, byte const fill_symbol) +{ + return BusDevice(new SpiBusDevice(SpiBusDeviceConfig{spi, spi_settings, spi_select, spi_deselect, fill_symbol})); +} + +BusDevice BusDeviceBase::create(arduino::HardwareI2C & wire, byte const slave_addr) +{ + return create(wire, slave_addr, true, true); +} + +BusDevice BusDeviceBase::create(arduino::HardwareI2C & wire, byte const slave_addr, bool const restart) +{ + return create(wire, slave_addr, restart, true); +} + +BusDevice BusDeviceBase::create(arduino::HardwareI2C & wire, byte const slave_addr, bool const restart, bool const stop) +{ + return BusDevice(new WireBusDevice(WireBusDeviceConfig{wire, slave_addr, restart, stop})); +} + +/************************************************************************************** + * BusDevice CTOR/DTOR + **************************************************************************************/ + +BusDevice::BusDevice(BusDeviceBase * dev) +: _dev{dev} +{ } + +BusDevice::BusDevice(arduino::HardwareSPI & spi, int const cs_pin, SPISettings const & spi_settings, byte const fill_symbol) +{ + *this = BusDeviceBase::create(spi, cs_pin, spi_settings, fill_symbol); +} + +BusDevice::BusDevice(arduino::HardwareSPI & spi, int const cs_pin, uint32_t const spi_clock, BitOrder const spi_bit_order, SPIMode const spi_bit_mode, byte const fill_symbol) +{ + *this = BusDeviceBase::create(spi, cs_pin, spi_clock, spi_bit_order, spi_bit_mode, fill_symbol); +} + +BusDevice::BusDevice(arduino::HardwareSPI & spi, SpiBusDeviceConfig::SpiSelectFunc spi_select, SpiBusDeviceConfig::SpiDeselectFunc spi_deselect, SPISettings const & spi_settings, byte const fill_symbol) +{ + *this = BusDeviceBase::create(spi, spi_select, spi_deselect, spi_settings, fill_symbol); +} + +BusDevice::BusDevice(arduino::HardwareI2C & wire, byte const slave_addr) +{ + *this = BusDeviceBase::create(wire, slave_addr); +} + +BusDevice::BusDevice(arduino::HardwareI2C & wire, byte const slave_addr, bool const restart) +{ + *this = BusDeviceBase::create(wire, slave_addr, restart); +} + +BusDevice::BusDevice(arduino::HardwareI2C & wire, byte const slave_addr, bool const restart, bool const stop) +{ + *this = BusDeviceBase::create(wire, slave_addr, restart, stop); +} + +IoResponse BusDevice::transfer(IoRequest & req) +{ + return _dev->transfer(req); +} + +SpiBusDevice & BusDevice::spi() +{ + return *reinterpret_cast(_dev.get()); +} + +WireBusDevice & BusDevice::wire() +{ + return *reinterpret_cast(_dev.get()); +} diff --git a/src/BusDevice.h b/src/BusDevice.h new file mode 100644 index 0000000..3767a51 --- /dev/null +++ b/src/BusDevice.h @@ -0,0 +1,94 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef BUS_DEVICE_H_ +#define BUS_DEVICE_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "IoTransaction.h" + +#include "spi/SpiBusDeviceConfig.h" + +/************************************************************************************** + * FORWARD DECLARATION + **************************************************************************************/ + +namespace arduino +{ + class HardwareSPI; + class HardwareI2C; +} + +class BusDevice; +class SpiBusDevice; +class WireBusDevice; + +/************************************************************************************** + * CLASS DECLARATION + **************************************************************************************/ + +class BusDeviceBase +{ +public: + + virtual ~BusDeviceBase() { } + + virtual IoResponse transfer(IoRequest & req) = 0; + + + static BusDevice create(arduino::HardwareSPI & spi, int const cs_pin, SPISettings const & spi_settings, byte const fill_symbol = 0xFF); + static BusDevice create(arduino::HardwareSPI & spi, int const cs_pin, uint32_t const spi_clock, BitOrder const spi_bit_order, SPIMode const spi_bit_mode, byte const fill_symbol = 0xFF); + static BusDevice create(arduino::HardwareSPI & spi, SpiBusDeviceConfig::SpiSelectFunc spi_select, SpiBusDeviceConfig::SpiDeselectFunc spi_deselect, SPISettings const & spi_settings, byte const fill_symbol = 0xFF); + + static BusDevice create(arduino::HardwareI2C & wire, byte const slave_addr); + static BusDevice create(arduino::HardwareI2C & wire, byte const slave_addr, bool const restart); + static BusDevice create(arduino::HardwareI2C & wire, byte const slave_addr, bool const restart, bool const stop); + +}; + +class BusDevice +{ +public: + + BusDevice(BusDeviceBase * dev); + + BusDevice(arduino::HardwareSPI & spi, int const cs_pin, SPISettings const & spi_settings, byte const fill_symbol = 0xFF); + BusDevice(arduino::HardwareSPI & spi, int const cs_pin, uint32_t const spi_clock, BitOrder const spi_bit_order, SPIMode const spi_bit_mode, byte const fill_symbol = 0xFF); + BusDevice(arduino::HardwareSPI & spi, SpiBusDeviceConfig::SpiSelectFunc spi_select, SpiBusDeviceConfig::SpiDeselectFunc spi_deselect, SPISettings const & spi_settings, byte const fill_symbol = 0xFF); + + BusDevice(arduino::HardwareI2C & wire, byte const slave_addr); + BusDevice(arduino::HardwareI2C & wire, byte const slave_addr, bool const restart); + BusDevice(arduino::HardwareI2C & wire, byte const slave_addr, bool const restart, bool const stop); + + IoResponse transfer(IoRequest & req); + + + SpiBusDevice & spi(); + WireBusDevice & wire(); + + +private: + + mbed::SharedPtr _dev; + +}; + +#endif /* BUS_DEVICE_H_ */ diff --git a/src/IoTransaction.h b/src/IoTransaction.h new file mode 100644 index 0000000..812f3f5 --- /dev/null +++ b/src/IoTransaction.h @@ -0,0 +1,125 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef IO_TRANSACTION_H_ +#define IO_TRANSACTION_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +#include + +#include + +/************************************************************************************** + * CLASS DECLARATION + **************************************************************************************/ + +/************************************************************************************** + * IoRequest + **************************************************************************************/ + +class IoRequest +{ +public: + + IoRequest(byte * write_buf_, size_t const bytes_to_write_, byte * read_buf_, size_t const bytes_to_read_) + : write_buf{write_buf_} + , bytes_to_write{bytes_to_write_} + , read_buf{read_buf_} + , bytes_to_read{bytes_to_read_} + { } + + IoRequest(byte & write_buf_, byte & read_buf_) + : IoRequest{&write_buf_, 1, &read_buf_, 1} + { } + + byte * write_buf{nullptr}; + size_t const bytes_to_write{0}; + byte * read_buf{nullptr}; + size_t const bytes_to_read{0}; + +}; + +/************************************************************************************** + * IoResponse + **************************************************************************************/ + +namespace impl +{ + +class IoResponse +{ +public: + + IoResponse() + : bytes_written{0} + , bytes_read{0} + , _cond{_mutex} + , _is_done{false} + { } + + size_t bytes_written{0}; + size_t bytes_read{0}; + + void done() + { + _mutex.lock(); + _is_done = true; + _cond.notify_all(); + _mutex.unlock(); + } + + void wait() + { + _mutex.lock(); + while (!_is_done) { + _cond.wait(); + } + _mutex.unlock(); + } + +private: + + rtos::Mutex _mutex; + rtos::ConditionVariable _cond; + bool _is_done{false}; + +}; + +} /* namespace impl */ + +typedef mbed::SharedPtr IoResponse; + +/************************************************************************************** + * IoTransaction + **************************************************************************************/ + +class IoTransaction +{ +public: + + IoTransaction(IoRequest * q, IoResponse * p) : req{q}, rsp{p} { } + IoRequest * req{nullptr}; + IoResponse * rsp{nullptr}; +}; + +#endif /* IO_TRANSACTION_H_ */ diff --git a/src/serial/SerialDispatcher.cpp b/src/serial/SerialDispatcher.cpp new file mode 100644 index 0000000..7fb6dea --- /dev/null +++ b/src/serial/SerialDispatcher.cpp @@ -0,0 +1,334 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "SerialDispatcher.h" + +/************************************************************************************** + * CTOR/DTOR + **************************************************************************************/ + +SerialDispatcher::SerialDispatcher(arduino::HardwareSerial & serial) +: _is_initialized{false} +, _mutex{} +, _cond{_mutex} +, _serial{serial} +, _thread(osPriorityRealtime, 4096, nullptr, "SerialDispatcher") +, _has_tread_started{false} +, _terminate_thread{false} +, _global_prefix_callback{nullptr} +, _global_suffix_callback{nullptr} +{ + +} + +/************************************************************************************** + * PUBLIC MEMBER FUNCTIONS + **************************************************************************************/ + +void SerialDispatcher::begin(unsigned long baudrate) +{ + begin(baudrate, SERIAL_8N1); +} + +void SerialDispatcher::begin(unsigned long baudrate, uint16_t config) +{ + mbed::ScopedLock lock(_mutex); + + if (!_is_initialized) + { + _serial.begin(baudrate, config); + _is_initialized = true; + _thread.start(mbed::callback(this, &SerialDispatcher::threadFunc)); /* TODO: Check return code */ + while (!_has_tread_started) { } + } + + /* Check if the thread calling begin is already in the list. */ + osThreadId_t const current_thread_id = rtos::ThisThread::get_id(); + if (findThreadCustomerDataById(rtos::ThisThread::get_id()) == std::end(_thread_customer_list)) + { + /* Since the thread is not in the list yet we are + * going to create a new entry to the list. + */ + ThreadCustomerData data; + data.thread_id = current_thread_id; + data.block_tx_buffer = false; + data.prefix_func = nullptr; + data.suffix_func = nullptr; + _thread_customer_list.push_back(data); + } +} + +void SerialDispatcher::end() +{ + mbed::ScopedLock lock(_mutex); + + /* Retrieve the current thread id and remove + * the thread data from the thread data list. + */ + osThreadId_t const current_thread_id = rtos::ThisThread::get_id(); + std::remove_if(std::begin(_thread_customer_list), + std::end (_thread_customer_list), + [current_thread_id](ThreadCustomerData const d) -> bool { return (d.thread_id == current_thread_id); }); + + /* If no thread consumers are left also end + * the serial device altogether. + */ + if (_thread_customer_list.size() == 0) + { + _terminate_thread = true; + _thread.join(); + _serial.end(); + } +} + +int SerialDispatcher::available() +{ + mbed::ScopedLock lock(_mutex); + auto iter = findThreadCustomerDataById(rtos::ThisThread::get_id()); + if (iter == std::end(_thread_customer_list)) return 0; + + prepareSerialReader(iter); + handleSerialReader(); + + return iter->rx_buffer->available(); +} + +int SerialDispatcher::peek() +{ + mbed::ScopedLock lock(_mutex); + auto iter = findThreadCustomerDataById(rtos::ThisThread::get_id()); + if (iter == std::end(_thread_customer_list)) return 0; + + prepareSerialReader(iter); + handleSerialReader(); + + return iter->rx_buffer->peek(); +} + +int SerialDispatcher::read() +{ + mbed::ScopedLock lock(_mutex); + auto iter = findThreadCustomerDataById(rtos::ThisThread::get_id()); + if (iter == std::end(_thread_customer_list)) return 0; + + prepareSerialReader(iter); + handleSerialReader(); + + return iter->rx_buffer->read_char(); +} + +void SerialDispatcher::flush() +{ + mbed::ScopedLock lock(_mutex); + _serial.flush(); +} + +size_t SerialDispatcher::write(uint8_t const b) +{ + return write(&b, 1); +} + +size_t SerialDispatcher::write(const uint8_t * data, size_t len) +{ + mbed::ScopedLock lock(_mutex); + + auto iter = findThreadCustomerDataById(rtos::ThisThread::get_id()); + + /* If this thread hasn't registered yet + * with the SerialDispatcher via 'begin'. + */ + if (iter == std::end(_thread_customer_list)) + return 0; + + size_t bytes_written = 0; + for (; (bytes_written < len) && iter->tx_buffer.availableForStore(); bytes_written++) + iter->tx_buffer.store_char(data[bytes_written]); + + /* Inform the worker thread that new data has + * been written to a Serial transmit buffer. + */ + _cond.notify_one(); + + return bytes_written; +} + +void SerialDispatcher::block() +{ + mbed::ScopedLock lock(_mutex); + auto iter = findThreadCustomerDataById(rtos::ThisThread::get_id()); + if (iter == std::end(_thread_customer_list)) return; + iter->block_tx_buffer = true; +} + +void SerialDispatcher::unblock() +{ + mbed::ScopedLock lock(_mutex); + auto iter = findThreadCustomerDataById(rtos::ThisThread::get_id()); + if (iter == std::end(_thread_customer_list)) return; + iter->block_tx_buffer = false; + _cond.notify_one(); +} + +void SerialDispatcher::prefix(PrefixInjectorCallbackFunc func) +{ + mbed::ScopedLock lock(_mutex); + auto iter = findThreadCustomerDataById(rtos::ThisThread::get_id()); + if (iter == std::end(_thread_customer_list)) return; + iter->prefix_func = func; +} + +void SerialDispatcher::suffix(SuffixInjectorCallbackFunc func) +{ + mbed::ScopedLock lock(_mutex); + auto iter = findThreadCustomerDataById(rtos::ThisThread::get_id()); + if (iter == std::end(_thread_customer_list)) return; + iter->suffix_func = func; +} + +void SerialDispatcher::global_prefix(PrefixInjectorCallbackFunc func) +{ + mbed::ScopedLock lock(_mutex); + _global_prefix_callback = func; +} + +void SerialDispatcher::global_suffix(SuffixInjectorCallbackFunc func) +{ + mbed::ScopedLock lock(_mutex); + _global_suffix_callback = func; +} + +/************************************************************************************** + * PRIVATE MEMBER FUNCTIONS + **************************************************************************************/ + +void SerialDispatcher::threadFunc() +{ + _has_tread_started = true; + + while(!_terminate_thread) + { + /* Prevent race conditions by multi-threaded + * access to shared data. + */ + mbed::ScopedLock lock(_mutex); + /* Wait for new data to be available */ + _cond.wait(); + /* Iterate over all list entries. */ + std::for_each(std::begin(_thread_customer_list), + std::end (_thread_customer_list), + [this](ThreadCustomerData & d) + { + if (d.block_tx_buffer) + return; + + /* Return if there's no data to be written to the + * serial interface. This statement is necessary + * because otherwise the prefix/suffix functions + * will be invoked and will be printing something, + * even though no data is actually to be printed for + * most threads. + */ + if (!d.tx_buffer.available()) + return; + + /* Retrieve all data stored in the transmit ringbuffer + * and store it into a String for usage by both suffix + * prefix callback functions. + */ + String msg; + while(d.tx_buffer.available()) + msg += static_cast(d.tx_buffer.read_char()); + + /* The prefix callback function allows the + * user to insert a custom message before + * a new message is written to the serial + * driver. This is useful e.g. for wrapping + * protocol (e.g. the 'AT' protocol) or providing + * a timestamp, a log level, ... + */ + String prefix; + if (d.prefix_func) + prefix = d.prefix_func(msg); + /* A prefix callback function defined per thread + * takes precedence over a globally defined prefix + * callback function. + */ + else if (_global_prefix_callback) + prefix = _global_prefix_callback(msg); + + /* Similar to the prefix function this callback + * allows the user to specify a specific message + * to be appended to each message, e.g. '\r\n'. + */ + String suffix; + if (d.suffix_func) + suffix = d.suffix_func(prefix, msg); + /* A suffix callback function defined per thread + * takes precedence over a globally defined suffix + * callback function. + */ + else if (_global_suffix_callback) + suffix = _global_suffix_callback(prefix, msg); + + /* Now it's time to actually write the message + * conveyed by the user via Serial.print/println. + */ + _serial.write(prefix.c_str()); + _serial.write(msg.c_str()); + _serial.write(suffix.c_str()); + }); + } +} + +std::list::iterator SerialDispatcher::findThreadCustomerDataById(osThreadId_t const thread_id) +{ + return std::find_if(std::begin(_thread_customer_list), + std::end (_thread_customer_list), + [thread_id](ThreadCustomerData const d) -> bool { return (d.thread_id == thread_id); }); +} + +void SerialDispatcher::prepareSerialReader(std::list::iterator & iter) +{ + if (!iter->rx_buffer) + iter->rx_buffer.reset(new arduino::RingBuffer()); +} + +void SerialDispatcher::handleSerialReader() +{ + while (_serial.available()) + { + int const c = _serial.read(); + + std::for_each(std::begin(_thread_customer_list), + std::end (_thread_customer_list), + [c](ThreadCustomerData & d) + { + if (!d.rx_buffer) + return; + + if (!d.rx_buffer->availableForStore()) + return; + + d.rx_buffer->store_char(c); + }); + } +} diff --git a/src/serial/SerialDispatcher.h b/src/serial/SerialDispatcher.h new file mode 100644 index 0000000..ca17793 --- /dev/null +++ b/src/serial/SerialDispatcher.h @@ -0,0 +1,105 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef SERIAL_DISPATCHER_H_ +#define SERIAL_DISPATCHER_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "api/HardwareSerial.h" + +#include + +#include +#include + +#include + +/************************************************************************************** + * CLASS DECLARATION + **************************************************************************************/ + +class SerialDispatcher : public arduino::HardwareSerial +{ + +public: + + SerialDispatcher(arduino::HardwareSerial & serial); + + + virtual void begin(unsigned long baudrate) override; + virtual void begin(unsigned long baudrate, uint16_t config) override; + virtual void end() override; + virtual int available() override; + virtual int peek() override; + virtual int read() override; + virtual void flush() override; + virtual size_t write(uint8_t const b) override; + virtual size_t write(const uint8_t * data, size_t len) override; + using Print::write; + virtual operator bool() override { return _serial; } + + void block(); + void unblock(); + + typedef std::function PrefixInjectorCallbackFunc; + typedef std::function SuffixInjectorCallbackFunc; + void prefix(PrefixInjectorCallbackFunc func); + void suffix(SuffixInjectorCallbackFunc func); + void global_prefix(PrefixInjectorCallbackFunc func); + void global_suffix(SuffixInjectorCallbackFunc func); + + +private: + + bool _is_initialized; + rtos::Mutex _mutex; + rtos::ConditionVariable _cond; + arduino::HardwareSerial & _serial; + + rtos::Thread _thread; + bool _has_tread_started; + bool _terminate_thread; + + PrefixInjectorCallbackFunc _global_prefix_callback; + SuffixInjectorCallbackFunc _global_suffix_callback; + + static int constexpr THREADSAFE_SERIAL_TRANSMIT_RINGBUFFER_SIZE = 128; + typedef arduino::RingBufferN SerialTransmitRingbuffer; + + typedef struct + { + osThreadId_t thread_id; + SerialTransmitRingbuffer tx_buffer; + bool block_tx_buffer; + mbed::SharedPtr rx_buffer; /* Only when a thread has expressed interested to read from serial a receive ringbuffer is allocated. */ + PrefixInjectorCallbackFunc prefix_func; + SuffixInjectorCallbackFunc suffix_func; + } ThreadCustomerData; + + std::list _thread_customer_list; + + void threadFunc(); + std::list::iterator findThreadCustomerDataById(osThreadId_t const thread_id); + void prepareSerialReader(std::list::iterator & iter); + void handleSerialReader(); +}; + +#endif /* SERIAL_DISPATCHER_H_ */ diff --git a/src/spi/SpiBusDevice.cpp b/src/spi/SpiBusDevice.cpp new file mode 100644 index 0000000..42dd09b --- /dev/null +++ b/src/spi/SpiBusDevice.cpp @@ -0,0 +1,70 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "SpiBusDevice.h" + +#include "SpiDispatcher.h" + +/************************************************************************************** + * CTOR/DTOR + **************************************************************************************/ + +SpiBusDevice::SpiBusDevice(SpiBusDeviceConfig const & config) +: _config{config} +{ + +} + +/************************************************************************************** + * PUBLIC MEMBER FUNCTIONS + **************************************************************************************/ + +IoResponse SpiBusDevice::transfer(IoRequest & req) +{ + return SpiDispatcher::instance().dispatch(&req, &_config); +} + +bool SpiBusDevice::read(uint8_t * buffer, size_t len, uint8_t sendvalue) +{ + SpiBusDeviceConfig config(_config.spi(), _config.settings(), _config.select_func(), _config.deselect_func(), sendvalue); + IoRequest req(nullptr, 0, buffer, len); + IoResponse rsp = SpiDispatcher::instance().dispatch(&req, &config); + rsp->wait(); + return true; +} + +bool SpiBusDevice::write(uint8_t * buffer, size_t len) +{ + IoRequest req(buffer, len, nullptr, 0); + IoResponse rsp = SpiDispatcher::instance().dispatch(&req, &_config); + rsp->wait(); + return true; +} + +bool SpiBusDevice::write_then_read(uint8_t * write_buffer, size_t write_len, uint8_t * read_buffer, size_t read_len, uint8_t sendvalue) +{ + SpiBusDeviceConfig config(_config.spi(), _config.settings(), _config.select_func(), _config.deselect_func(), sendvalue); + IoRequest req(write_buffer, write_len, read_buffer, read_len); + IoResponse rsp = SpiDispatcher::instance().dispatch(&req, &config); + rsp->wait(); + return true; +} diff --git a/src/spi/SpiBusDevice.h b/src/spi/SpiBusDevice.h new file mode 100644 index 0000000..f16347b --- /dev/null +++ b/src/spi/SpiBusDevice.h @@ -0,0 +1,56 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef SPI_BUS_DEVICE_H_ +#define SPI_BUS_DEVICE_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "../BusDevice.h" + +#include "SpiBusDeviceConfig.h" + +/************************************************************************************** + * CLASS DECLARATION + **************************************************************************************/ + +class SpiBusDevice : public BusDeviceBase +{ +public: + + SpiBusDevice(SpiBusDeviceConfig const & config); + virtual ~SpiBusDevice() { } + + + virtual IoResponse transfer(IoRequest & req) override; + + + bool read(uint8_t * buffer, size_t len, uint8_t sendvalue = 0xFF); + bool write(uint8_t * buffer, size_t len); + bool write_then_read(uint8_t * write_buffer, size_t write_len, uint8_t * read_buffer, size_t read_len, uint8_t sendvalue = 0xFF); + + +private: + + SpiBusDeviceConfig _config; + +}; + +#endif /* SPI_BUS_DEVICE_H_ */ diff --git a/src/spi/SpiBusDeviceConfig.h b/src/spi/SpiBusDeviceConfig.h new file mode 100644 index 0000000..196fc43 --- /dev/null +++ b/src/spi/SpiBusDeviceConfig.h @@ -0,0 +1,82 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef SPI_BUS_DEVICE_CONFIG_H_ +#define SPI_BUS_DEVICE_CONFIG_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +#include + +#include + +/************************************************************************************** + * CLASS DECLARATION + **************************************************************************************/ + +class SpiBusDeviceConfig +{ +public: + + typedef std::function SpiSelectFunc; + typedef std::function SpiDeselectFunc; + + + SpiBusDeviceConfig(arduino::HardwareSPI & spi, SPISettings const & spi_settings, SpiSelectFunc spi_select, SpiDeselectFunc spi_deselect, byte const fill_symbol = 0xFF) + : _spi{spi} + , _spi_settings{spi_settings} + , _spi_select{spi_select} + , _spi_deselect{spi_deselect} + , _fill_symbol{fill_symbol} + { } + + SpiBusDeviceConfig(arduino::HardwareSPI & spi, SPISettings const & spi_settings, int const cs_pin, byte const fill_symbol = 0xFF) + : SpiBusDeviceConfig + {spi, + spi_settings, + [cs_pin](){ digitalWrite(cs_pin, LOW); }, + [cs_pin](){ digitalWrite(cs_pin, HIGH); }, + fill_symbol + } + { } + + + arduino::HardwareSPI & spi() { return _spi; } + SPISettings settings () const { return _spi_settings; } + void select () const { if (_spi_select) _spi_select(); } + void deselect () const { if (_spi_deselect) _spi_deselect(); } + byte fill_symbol() const { return _fill_symbol; } + + SpiSelectFunc select_func () const { return _spi_select; } + SpiDeselectFunc deselect_func() const { return _spi_deselect; } + +private: + + arduino::HardwareSPI & _spi; + SPISettings _spi_settings; + SpiSelectFunc _spi_select{nullptr}; + SpiDeselectFunc _spi_deselect{nullptr}; + byte _fill_symbol{0xFF}; + +}; + +#endif /* SPI_BUS_DEVICE_CONFIG_H_ */ diff --git a/src/spi/SpiDispatcher.cpp b/src/spi/SpiDispatcher.cpp new file mode 100644 index 0000000..a5ab024 --- /dev/null +++ b/src/spi/SpiDispatcher.cpp @@ -0,0 +1,175 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "SpiDispatcher.h" + +#include + +/************************************************************************************** + * STATIC MEMBER DEFINITION + **************************************************************************************/ + +SpiDispatcher * SpiDispatcher::_p_instance{nullptr}; +rtos::Mutex SpiDispatcher::_mutex; + +/************************************************************************************** + * CTOR/DTOR + **************************************************************************************/ + +SpiDispatcher::SpiDispatcher() +: _thread(osPriorityRealtime, 4096, nullptr, "SpiDispatcher") +, _has_tread_started{false} +, _terminate_thread{false} +{ + begin(); +} + +SpiDispatcher::~SpiDispatcher() +{ + end(); +} + +/************************************************************************************** + * PUBLIC MEMBER FUNCTIONS + **************************************************************************************/ + +SpiDispatcher & SpiDispatcher::instance() +{ + mbed::ScopedLock lock(_mutex); + if (!_p_instance) { + _p_instance = new SpiDispatcher(); + } + return *_p_instance; +} + +void SpiDispatcher::destroy() +{ + mbed::ScopedLock lock(_mutex); + delete _p_instance; + _p_instance = nullptr; +} + +IoResponse SpiDispatcher::dispatch(IoRequest * req, SpiBusDeviceConfig * config) +{ + mbed::ScopedLock lock(_mutex); + + SpiIoTransaction * spi_io_transaction = _spi_io_transaction_mailbox.try_alloc(); + if (!spi_io_transaction) + return nullptr; + + IoResponse rsp(new impl::IoResponse()); + + spi_io_transaction->req = req; + spi_io_transaction->rsp = rsp; + spi_io_transaction->config = config; + + _spi_io_transaction_mailbox.put(spi_io_transaction); + + return rsp; +} + +/************************************************************************************** + * PRIVATE MEMBER FUNCTIONS + **************************************************************************************/ + +void SpiDispatcher::begin() +{ + SPI.begin(); + _thread.start(mbed::callback(this, &SpiDispatcher::threadFunc)); /* TODO: Check return code */ + /* It is necessary to wait until the SpiDispatcher::threadFunc() + * has started, otherwise other threads might trigger IO requests + * before this thread is actually running. + */ + while (!_has_tread_started) { } +} + +void SpiDispatcher::end() +{ + _terminate_thread = true; + _thread.join(); /* TODO: Check return code */ + SPI.end(); +} + +void SpiDispatcher::threadFunc() +{ + _has_tread_started = true; + + while(!_terminate_thread) + { + /* Wait blocking for the next IO transaction + * request to be posted to the mailbox. + */ + SpiIoTransaction * spi_io_transaction = _spi_io_transaction_mailbox.try_get_for(rtos::Kernel::wait_for_u32_forever); + if (spi_io_transaction) + { + processSpiIoRequest(spi_io_transaction); + /* Free the allocated memory (memory allocated + * during dispatch(...) + */ + _spi_io_transaction_mailbox.free(spi_io_transaction); + } + } +} + +void SpiDispatcher::processSpiIoRequest(SpiIoTransaction * spi_io_transaction) +{ + IoRequest * io_request = spi_io_transaction->req; + IoResponse io_response = spi_io_transaction->rsp; + SpiBusDeviceConfig * config = spi_io_transaction->config; + + config->select(); + + config->spi().beginTransaction(config->settings()); + + /* In a first step transmit the complete write buffer and + * write back the receive data directly into the write buffer + */ + size_t bytes_sent = 0; + for(; bytes_sent < io_request->bytes_to_write; bytes_sent++) + { + uint8_t const tx_byte = io_request->write_buf[bytes_sent]; + uint8_t const rx_byte = config->spi().transfer(tx_byte); + + io_request->write_buf[bytes_sent] = rx_byte; + } + + /* In a second step, transmit the fill symbol and write the + * received data into the read buffer. + */ + size_t bytes_received = 0; + for(; bytes_received < io_request->bytes_to_read; bytes_received++) + { + uint8_t const tx_byte = config->fill_symbol(); + uint8_t const rx_byte = config->spi().transfer(tx_byte); + + io_request->read_buf[bytes_received] = rx_byte; + } + + config->spi().endTransaction(); + + config->deselect(); + + io_response->bytes_written = bytes_sent; + io_response->bytes_read = bytes_received; + + io_response->done(); +} diff --git a/src/spi/SpiDispatcher.h b/src/spi/SpiDispatcher.h new file mode 100644 index 0000000..c42adf2 --- /dev/null +++ b/src/spi/SpiDispatcher.h @@ -0,0 +1,76 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef SPI_DISPATCHER_H_ +#define SPI_DISPATCHER_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +#include "../IoTransaction.h" + +#include "SpiBusDeviceConfig.h" + +/************************************************************************************** + * CLASS DECLARATION + **************************************************************************************/ + +class SpiDispatcher +{ +public: + + SpiDispatcher(SpiDispatcher &) = delete; + void operator = (SpiDispatcher &) = delete; + + static SpiDispatcher & instance(); + static void destroy(); + + IoResponse dispatch(IoRequest * req, SpiBusDeviceConfig * config); + +private: + + static SpiDispatcher * _p_instance; + static rtos::Mutex _mutex; + + rtos::Thread _thread; + bool _has_tread_started; + bool _terminate_thread; + + typedef struct + { + IoRequest * req; + IoResponse rsp; + SpiBusDeviceConfig * config; + } SpiIoTransaction; + + static size_t constexpr REQUEST_QUEUE_SIZE = 32; + rtos::Mail _spi_io_transaction_mailbox; + + SpiDispatcher(); + ~SpiDispatcher(); + + void begin(); + void end(); + void threadFunc(); + void processSpiIoRequest(SpiIoTransaction * spi_io_transaction); +}; + +#endif /* SPI_DISPATCHER_H_ */ diff --git a/src/wire/WireBusDevice.cpp b/src/wire/WireBusDevice.cpp new file mode 100644 index 0000000..19477fb --- /dev/null +++ b/src/wire/WireBusDevice.cpp @@ -0,0 +1,80 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "WireBusDevice.h" + +#include "WireDispatcher.h" + +/************************************************************************************** + * CTOR/DTOR + **************************************************************************************/ + +WireBusDevice::WireBusDevice(WireBusDeviceConfig const & config) +: _config{config} +{ + +} + +/************************************************************************************** + * PUBLIC MEMBER FUNCTIONS + **************************************************************************************/ + +IoResponse WireBusDevice::transfer(IoRequest & req) +{ + return WireDispatcher::instance().dispatch(&req, &_config); +} + +bool WireBusDevice::read(uint8_t * buffer, size_t len, bool stop) +{ + WireBusDeviceConfig config(_config.wire(), _config.slave_addr(), _config.restart(), stop); + IoRequest req(nullptr, 0, buffer, len); + IoResponse rsp = WireDispatcher::instance().dispatch(&req, &config); + rsp->wait(); + return true; +} + +bool WireBusDevice::write(uint8_t * buffer, size_t len, bool stop) +{ + bool const restart = !stop; + WireBusDeviceConfig config(_config.wire(), _config.slave_addr(), restart, _config.stop()); + IoRequest req(buffer, len, nullptr, 0); + IoResponse rsp = WireDispatcher::instance().dispatch(&req, &config); + rsp->wait(); + return true; +} + +bool WireBusDevice::write_then_read(uint8_t * write_buffer, size_t write_len, uint8_t * read_buffer, size_t read_len, bool stop) +{ + /* Copy the Wire parameters from the device and modify only those + * which can be modified via the parameters of this function. + */ + bool const restart = !stop; + WireBusDeviceConfig config(_config.wire(), _config.slave_addr(), restart, _config.stop()); + /* Fire off the IO request and await its response. */ + IoRequest req(write_buffer, write_len, read_buffer, read_len); + IoResponse rsp = WireDispatcher::instance().dispatch(&req, &config); + rsp->wait(); + /* TODO: Introduce error codes within the IoResponse and evaluate + * them here. + */ + return true; +} diff --git a/src/wire/WireBusDevice.h b/src/wire/WireBusDevice.h new file mode 100644 index 0000000..6c9f8bf --- /dev/null +++ b/src/wire/WireBusDevice.h @@ -0,0 +1,56 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef WIRE_BUS_DEVICE_H_ +#define WIRE_BUS_DEVICE_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "../BusDevice.h" + +#include "WireBusDeviceConfig.h" + +/************************************************************************************** + * CLASS DECLARATION + **************************************************************************************/ + +class WireBusDevice : public BusDeviceBase +{ +public: + + WireBusDevice(WireBusDeviceConfig const & config); + virtual ~WireBusDevice() { } + + + virtual IoResponse transfer(IoRequest & req) override; + + + bool read(uint8_t * buffer, size_t len, bool stop = true); + bool write(uint8_t * buffer, size_t len, bool stop = true); + bool write_then_read(uint8_t * write_buffer, size_t write_len, uint8_t * read_buffer, size_t read_len, bool stop = false); + + +private: + + WireBusDeviceConfig _config; + +}; + +#endif /* WIRE_BUS_DEVICE_H_ */ diff --git a/src/wire/WireBusDeviceConfig.h b/src/wire/WireBusDeviceConfig.h new file mode 100644 index 0000000..c58e5b9 --- /dev/null +++ b/src/wire/WireBusDeviceConfig.h @@ -0,0 +1,60 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef WIRE_BUS_DEVICE_CONFIG_H_ +#define WIRE_BUS_DEVICE_CONFIG_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +#include + +/************************************************************************************** + * CLASS DECLARATION + **************************************************************************************/ + +class WireBusDeviceConfig +{ +public: + + WireBusDeviceConfig(arduino::HardwareI2C & wire, byte const slave_addr, bool const restart, bool const stop) + : _wire{wire} + , _slave_addr{slave_addr} + , _restart{restart} + , _stop{stop} + { } + + + inline arduino::HardwareI2C & wire() { return _wire; } + inline byte slave_addr() const { return _slave_addr; } + inline bool restart() const { return _restart; } + inline bool stop() const { return _stop; } + + +private: + + arduino::HardwareI2C & _wire; + byte _slave_addr{0x00}; + bool _restart{true}, _stop{true}; + +}; + +#endif /* WIRE_BUS_DEVICE_CONFIG_H_ */ diff --git a/src/wire/WireDispatcher.cpp b/src/wire/WireDispatcher.cpp new file mode 100644 index 0000000..9a8a664 --- /dev/null +++ b/src/wire/WireDispatcher.cpp @@ -0,0 +1,174 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include "WireDispatcher.h" + +#include + +/************************************************************************************** + * STATIC MEMBER DEFINITION + **************************************************************************************/ + +WireDispatcher * WireDispatcher::_p_instance{nullptr}; +rtos::Mutex WireDispatcher::_mutex; + +/************************************************************************************** + * CTOR/DTOR + **************************************************************************************/ + +WireDispatcher::WireDispatcher() +: _thread(osPriorityRealtime, 4096, nullptr, "WireDispatcher") +, _has_tread_started{false} +, _terminate_thread{false} +{ + begin(); +} + +WireDispatcher::~WireDispatcher() +{ + end(); +} + +/************************************************************************************** + * PUBLIC MEMBER FUNCTIONS + **************************************************************************************/ + +WireDispatcher & WireDispatcher::instance() +{ + mbed::ScopedLock lock(_mutex); + if (!_p_instance) { + _p_instance = new WireDispatcher(); + } + return *_p_instance; +} + +void WireDispatcher::destroy() +{ + mbed::ScopedLock lock(_mutex); + delete _p_instance; + _p_instance = nullptr; +} + +IoResponse WireDispatcher::dispatch(IoRequest * req, WireBusDeviceConfig * config) +{ + mbed::ScopedLock lock(_mutex); + + WireIoTransaction * wire_io_transaction = _wire_io_transaction_mailbox.try_alloc(); + if (!wire_io_transaction) + return nullptr; + + IoResponse rsp(new impl::IoResponse()); + + wire_io_transaction->req = req; + wire_io_transaction->rsp = rsp; + wire_io_transaction->config = config; + + _wire_io_transaction_mailbox.put(wire_io_transaction); + + return rsp; +} + +/************************************************************************************** + * PRIVATE MEMBER FUNCTIONS + **************************************************************************************/ + +void WireDispatcher::begin() +{ + Wire.begin(); + _thread.start(mbed::callback(this, &WireDispatcher::threadFunc)); /* TODO: Check return code */ + /* It is necessary to wait until the WireDispatcher::threadFunc() + * has started, otherwise other threads might trigger IO requests + * before this thread is actually running. + */ + while (!_has_tread_started) { } +} + +void WireDispatcher::end() +{ + _terminate_thread = true; + _thread.join(); /* TODO: Check return code */ + Wire.end(); +} + +void WireDispatcher::threadFunc() +{ + _has_tread_started = true; + + while(!_terminate_thread) + { + /* Wait blocking for the next IO transaction + * request to be posted to the mailbox. + */ + WireIoTransaction * wire_io_transaction = _wire_io_transaction_mailbox.try_get_for(rtos::Kernel::wait_for_u32_forever); + if (wire_io_transaction) + { + processWireIoRequest(wire_io_transaction); + /* Free the allocated memory (memory allocated + * during dispatch(...) + */ + _wire_io_transaction_mailbox.free(wire_io_transaction); + } + } +} + +void WireDispatcher::processWireIoRequest(WireIoTransaction * wire_io_transaction) +{ + IoRequest * io_request = wire_io_transaction->req; + IoResponse io_response = wire_io_transaction->rsp; + WireBusDeviceConfig * config = wire_io_transaction->config; + + if (io_request->bytes_to_write > 0) + { + config->wire().beginTransmission(config->slave_addr()); + + size_t bytes_written = 0; + for (; bytes_written < io_request->bytes_to_write; bytes_written++) + { + config->wire().write(io_request->write_buf[bytes_written]); + } + io_response->bytes_written = bytes_written; + + if (config->restart() && (io_request->bytes_to_read > 0)) + config->wire().endTransmission(false /* stop */); + else + config->wire().endTransmission(true /* stop */); + } + + if (io_request->bytes_to_read > 0) + { + config->wire().requestFrom(config->slave_addr(), io_request->bytes_to_read, config->stop()); + + while(config->wire().available() != static_cast(io_request->bytes_to_read)) + { + /* TODO: Insert a timeout. */ + } + + size_t bytes_read = 0; + for (; bytes_read < io_request->bytes_to_read; bytes_read++) + { + io_request->read_buf[bytes_read] = config->wire().read(); + } + io_response->bytes_read = bytes_read; + } + + io_response->done(); +} diff --git a/src/wire/WireDispatcher.h b/src/wire/WireDispatcher.h new file mode 100644 index 0000000..87fc88f --- /dev/null +++ b/src/wire/WireDispatcher.h @@ -0,0 +1,78 @@ +/* + * This file is part of the Arduino_ThreadsafeIO library. + * Copyright (c) 2021 Arduino SA. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef WIRE_DISPATCHER_H_ +#define WIRE_DISPATCHER_H_ + +/************************************************************************************** + * INCLUDE + **************************************************************************************/ + +#include + +#include "../IoTransaction.h" + +#include "WireBusDeviceConfig.h" + +/************************************************************************************** + * CLASS DECLARATION + **************************************************************************************/ + +class WireDispatcher +{ +public: + + WireDispatcher(WireDispatcher &) = delete; + void operator = (WireDispatcher &) = delete; + + static WireDispatcher & instance(); + static void destroy(); + + + IoResponse dispatch(IoRequest * req, WireBusDeviceConfig * config); + + +private: + + static WireDispatcher * _p_instance; + static rtos::Mutex _mutex; + + rtos::Thread _thread; + bool _has_tread_started; + bool _terminate_thread; + + typedef struct + { + IoRequest * req; + IoResponse rsp; + WireBusDeviceConfig * config; + } WireIoTransaction; + + static size_t constexpr REQUEST_QUEUE_SIZE = 32; + rtos::Mail _wire_io_transaction_mailbox; + + WireDispatcher(); + ~WireDispatcher(); + + void begin(); + void end(); + void threadFunc(); + void processWireIoRequest(WireIoTransaction * wire_io_transaction); +}; + +#endif /* WIRE_DISPATCHER_H_ */