Overview
The RP2040 MCU has two FIFO queues between the processor cores, one from core 0 to core 1, and the other vice versa. They can be used for whatever purpose the programmer sees fit. Module Messages
uses them two transfer messages between threads on the two cores. Messages are one means to synchronise threads on different cores. Threads on the same core can use Semaphores
and Signals
.
There are also spinlocks, but we don’t consider these here.
Requirement
Each thread on one core must be able to send messages to any thread on the other core.
Design Considerations
Each FIFO is 32 bits wide, eight values deep. After some experimentation with different use cases and scenarios, I have decided to make the messaging stateless, that is, each message is self-contained, and not part of a stateful protocol.
The reasons for this decision are mainly simplicity, and the many different possible requirements of various scenarios. The stateless approach is much easier to handle in general, not least in the case of a run-time error, or a timing issue on one of the cores, or buffer overflows, or missing parts of stateful multi-part messages. Think: error handling, and in particular autonomous recovery from errors.
Stateful protocol can be implemented on program level if needed, based on the facilities provided by module Messages
, designed to fit the specific requirements of the use case.
Since each message must be self-contained, it can only hold 32 bits of information. As we want to be able to send messages to any thread on the other core, these 32 bits must contain the target thread’s identification, limiting the usable data payload that can be transferred. In other words, it’s not possible to transfer 32 bit data with one message. However, this can easily be solved on program level, using shared buffers, or splitting the data into two messages.
Each message is simply:
TYPE Message = ARRAY 4 OF BYTE;
With this definition, making use of Astrobe’s corresponding language extension, we can easily send any four byte value across the FIFO, including basic types.
Module Messages
has one requirement, however: element zero of the above array must contain the id of the target thread, or rather, as we’ll see the id of the thread’s sender-receiver.
The other three bytes are usable for whatever purpose for a specific program. See example program Messaging.
Architecture
+--------+ +-------------+
| thread |-subscr->| SndRcv |
| | | +--------+ | +---------+ +---------+ +---------+
| |---put-->| | outBuf | |---->| |---->| FIFO >> |---->| |---->
| | | +--------+ | | FIFO | +---------+ | FIFO |
| | | +--------+ | | thread | +---------+ | thread |
| |<--get---| | inBuf | |<----| |<----| << FIFO |<----| |<----
| | | +--------+ | +---------+ +---------+ +---------+
+--------+ +-------------+
<------------------ core 0 -------------------------------->|<---------- core 1 -------
A thread:
- subscribes to a sender-receiver (
SndRcv
) - puts messages into an output buffer (
outBuf
) - gets messages from an input buffer (
inBuf
) - can await an incoming message
- can await the output buffer getting emptied
A dedicated thread on each core handles the transfers across the FIFOs. Since the FIFO handling thread and any messaging client threads are scheduled cooperatively, and each message sent and received is self-contained without state on this basic level, the out and in buffers can be shared without interference.
Each SndRcv
has an id, which is unique per core. The sender must know the id of the receiver’s SndRcv
. This relationship between thread and its SndRcv
is fixed in the program code. There’s no fancy dynamic lookup or the like. Simplicity and robustness.
Buffer Overruns
Any buffer overruns are handled like the other buffers of the RP2040: when full, new arrivals are lost, but the condition is registered in corresponding flags.
Data Transfers
To transfer larger amounts of data, two threads can synchronise the use of a shared buffer via corresponding messages.
The FIFO Handler
The threads handling the FIFOs on both sides are “normal” application level threads. That is, they are also time-triggered with the current version of the kernel, and they share the execution time window of the scheduler with all other threads on the core. Their period must be selected based on the timing requirements of the communicating threads.
Installation
The messaging framework is installed by
Messages.Install(period, priority)
after installing the kernel. Thereafter, the application threads can subscribe to their SndRcv
. Note that we don’t want the id of these SndRcv
to change in case we make changes to the program, for example the order in which the threads are initialised and started, so the programmer assigns the ids explicitly, unlike the thread ids, which are assigned in the order the threads are allocated.