If you have read the blog on REXIS Examples, or the REXIS documentation, you may have noticed that REXIS supports synchronous message passing API, in addition to the more common asynchronous messaging with mailboxes. Synchronous message passing is not a new idea. Indeed, the inspiration came from QNX Neutrino, the message passing kernel that powered the BlackBerry smartphone devices in the early 2000s.
Synchronous message passing is a great idea. For example, it makes writing a “full-blown” OS (e.g. Linux) based on microkernel simpler. In such a system, OS services such as the file system support, networking etc. are written as regular tasks or processes instead of being part of the kernel code. Of course having a full OS is less important in a microcontroller system, but nevertheless, the use of synchronous message passing can give firmware a cleaner design. (We will see another example in this post below.) As a footnote, REXIS’ functions can be said to reflect those of a microkernel, perhaps pointing to a possible future path for its development.
Some RTOS producers are loathe to benchmark their products, or allow their users to do so. Here at ImageCraft, we welcome it :-). So let’s dive in and look at some numbers for the REXIS message passing API calls.
To summarize, with synchronous message passing, a task sends a message to another task. When that task receives the message, it processes the data based on the message received, and eventually sends a reply message to the original sender. When a task sends a message, it is blocked from running until the receiver replies to it. The order of message sending and receiving calls are irrelevant. This synchronizes the two tasks automatically without using another mechanism such as a MUTEX or semaphore.
Synchronous message passing is perfect for implementing server and client tasks: for example, a server task, e.g. a task that reads a block of data from a SD card, would sit in a forever loop waiting to receive a new message. A client task, e.g., a task that wishes to obtain a block of data from a SD card, would send a message to the server task. When the server task receives the message, it performs the actions specified in the message (e.g. reads a block of SD data) and then replies to the sender task (e.g. sends the data back to the sender). Note that the message format and meaning are determined by the cooperating tasks, not by REXIS.
Without the need to use a separate mechanism for synchronization, the design is cleaner than the alternative of using mailboxes and MUTEX. Moreover, execution time should be faster than the alternative as well, since fewer kernel API calls are needed, and REXIS implements very fast task switching when events occur. For example, when the receiver replies to the sender, the sender is unblocked and scheduling/context switching happens shortly afterward.
Using the timing mechanism shown in the previous blog, here’s the code excerpt:
cmdbuf[0] = CMD_READ;
cmdbuf[1] = mych;
memset(reqbuf, 0, REQLEN);
printf("Testing for '%c'... ", mych);
SetPortA5();
REXIS_MessageSend(SD_ServerTask_pid, cmdbuf, 2, reqbuf, REQLEN);
ClearPortA5();
The important lines are the last 3 lines. The SetPortA5()/ClearPortA5() respectively set and clear PORTA pin 5 , so that the signal can be monitored by the “Logic” logic analyzer. The message consists of a command code in the first byte and an ASCII character in the second byte. The server code looks like this:
while (REXIS_MessageReceive(&sender_pid, recbuf, 2, 0))
{ unsigned char reqbuf[REQLEN]; memset(reqbuf, 0, REQLEN); if (recbuf[0] == CMD_READ) memset(reqbuf, recbuf[1], REQLEN); REXIS_MessageReply(sender_pid, reqbuf, REQLEN); }
In this contrived example, the server reads the message, and if the first byte is the CMD_READ command code (which it is), it fills a 32-byte buffer with the second byte of the message and sends this buffer back to the sender as a reply. In the sender/client task, once it receives the reply message, it checks to make sure that it is filled with 32 bytes of the character sent, to confirm the correct operation of the message passing API.
This is just a contrived example. More complex processing would be done in “real life”. As the action triggered by this example is trivial, the timing code should give a good indication on just how long the REXIS kernel takes to:
- Make the REXIS_MessageSend() call and block the sender task.
- Unblock the server task.
- Schedule and context switch to resume the server task to receive the message (and the server does the processing).
- Have the server task to reply to the sender task.
- Unblock the sender task.
- Schedule and context switch to resume the sender task.
Note that this particular code path is just one scenario involving synchronous message passing. There can be another scenario where the the server task has not yet made the REXIS_MessageReceive call when the client sends a message. The behavior of the client/sender task is the same, but in this alternate scenario, the server task will receive the message immediately when it makes the REXIS_MessageReceive call.
So, how fast is it? On a 100 MHz ST-Nucleo F411, the timing clocks in at 31.5 to 32 usecs (microseconds). That is, since the clock is 100MHz, it takes approximately 3200 instructions to perform all the above functions, or over 30,000 such operations in a second.
There you have it. If there are more tasks running, or there are more messages being sent, the timing would obviously be slower. Nevertheless, this should give you an insight on how long it takes for REXIS to perform this particular feature.