Model Train-related Notes Blog -- these are personal notes and musings on the subject of model train control, automation, electronics, or whatever I find interesting. I also have more posts in a blog dedicated to the maintenance of the Randall Museum Model Railroad.
2023-02-28 - SDB: Using FreeRTOS Tasks & Priorities
Category SDB
The Software Defined Blocks project uses an ESP32 with sensors to emulate block activations for a train model railroad.
How can the current modules benefit from the FreeRTOS tasks?
An initial prototype was built using MicroPython. There’s good support for ESP32, especially using the Adafruit libraries. Unfortunately, MicroPython on ESP32 still has quite some limitations. For example there’s no OpenCV support, etc. Since the goal is to make the project as accessible as possible, sticking to the regular Arduino / C++ seemed a better option.
One issue with the MicroPython version is that I realized that “MicroPython Threads” are implemented using FreeRTOS tasks all of the same priority. Thus they execute in round robin fashion, with no control from the application. That means a task doing some IO can be pre-empted in the middle of e.g. an I2C-in-software operation.
With better FreeRTOS control, we can use tasks and we can:
- Use priorities to ensure some tasks are run before others.
- Use critical sections or mutexes to avoid same-priority tasks from interrupting each other.
The design suggestion is as follows for Core and Task Priorities:
- Core 0:
- Priority ? = wifi tasks.
- Priority 0 = idle task.
- Core 1:
- Priority 4 = sensor acquisition.
- Priority 3 = (optional) block logic & IO notifications.
- Priority 2 = (optional) display updates.
- Priority 1 = main loop.
- Priority 0 = idle task.
We’ll still have modules with an init + loop, but these are called from the main loop.
Each module is free to start its own sensor acquisition tasks as desired, with the caveats:
- They all share a common mutex to prevent select operations from being preempted.
- We will provide that via the shared module manager.
- They must yield reasonably using vTaskDelay or xTaskDelayUntil -- because we do not require exact timings, the former one is good enough for our usage.
Not every module needs to have a task. Because the main loop will still run, simple modules (display updates, block logic updates) can simply run in the loop callbacks.
We provide an “IO critical section mutex” via the module manager that can be used any time a specific IO operation must not be preempted. An example of such an operation would be an ADC measure, or reading from the ToF I2C, or updating the OLED buffer. Because sensors should use tasks all at the same priority on the same core, they will run in round robin.
We now have a situation where different tasks read and write from the data store. By construction, they cannot access it at the same time. The access can be preempted, as long as it’s by another task not performing data store operations.
For that, we’ll use a different mutex to protect reads and writes.
To ensure we cannot have deadlocks, both mutexes should not be held at the same time -- IO operations should not perform data store operations.