REXIS Examples

REXIS is both ImageCraft’s RTOS and a component of the JumpStart IoT Connectivity Suite. It’s powerful yet easy to use. This post is from an excerpt of the forthcoming REXIS User Guide, containing examples to demonstrate REXIS features.



As some users may not be familiar with using an RTOS, let’s dive in and look at some examples. The examples are located in <install root>\rexis\examples\, and introduce all the major features of REXIS. (This document will not provide the full listing of the programs, as minor details may change.)

Example Requirements

The examples are written for the ST-Nucleo F411RE board. They should be portable to all boards in the F4 series of ST-Nucleo, and can be ported to any ST-Nucleo easily, especially if JumpStart API (JSAPI) is available for the CPU. Even if JSAPI is not available, the basic requirements are typical for any basic MCU program: setting the system clock, initializing the UART for debug output, and initializing the interrupt vectors.

REXIS Startup

This fragment is needed for the basic REXIS startup:

#define REXIS_MEMORY_POOL_SIZE (1024*8) // 8K bytes
static unsigned char rexis_memory[REXIS_MEMORY_POOL_SIZE];
extern void task1(unsigned);
void main(void)
{

REXIS_SysInit(rexis_memory, REXIS_MEMORY_POOL_SIZE);

REXIS_TaskCreate(“task 1”, task1, 0, 0, 0);

REXIS_SysStart();
// never return
}

There are only three API functions that you must call to get REXIS started:

  1. REXIS_SysInit is called with a memory block. The memory is used by REXIS to allocate task structures, stacks, and other internal usage such as mailboxes, semaphores, etc. This code fragment uses a memory block of 8K bytes. The size of the memory pool should be adjusted to reflect your program’s usage requirements.
  2. REXIS_TaskCreate is called to create a task. Its arguments (from left to right) are: the name of the task, the task function, the task priority, the task stack size, and the task function initial argument. If zero (0) is specified for the task priority and/or the task stack size, default values will be used. At least one task must be created prior to the REXIS_SysStart call. Other tasks can be created either in main() or in other tasks as needed.
  3. REXIS_SysStart is called to start the REXIS kernel. Once started, REXIS never returns to the original calling function.

In the examples below, tasks are assumed to have been created by REXIS_TaskCreate call(s) prior to calling REXIS_SysStart.

NOTE: the supplied example code may look slightly different from the code excerpts below.

Simple Sleep Task / Blinking LED

This is a simple task that blinks an LED:

void task1(unsigned arg0)
{
… // setup LED
while (1)
{
LED_On();
REXIS_TaskSleepMS(700);
LED_Off();
REXIS_TaskSleepMS(300);
}
}

Typically, a REXIS task function runs a forever loop. If a task function ever returns, the task “dies”, and its resources will be reclaimed by REXIS.

In this example, the task turns on an LED, sleeps for 700 milliseconds, turns off the LED, sleeps for 300 milliseconds, and then repeats forever. When run, the LED will blink on and off according to the defined schedule. In this example, as there is only one task, the entirety can also be written without REXIS and the code will look pretty much the same.

The key takeaway is that a task is simply a function running a forever loop. Nothing else other than a call to the REXIS_TaskCreate function is needed to make a function into a task. A task function can do anything that a normal C function can do without limitation. Unlike some scheduling models (e.g. cooperative multitasking), there is no need for a task function to cause scheduling to happen – it just does.

Simple Reader and Writer Tasks Using Mailboxes

Here are two tasks; a writer task which generates ASCII characters one at a time and sends each one to a reader task, and a reader task which displays the received characters via the UART output. This demonstrates the basic of IPC (Inter Process Communication) using a mailbox.

A mailbox is a kernel construct that has a number of mail slots. When a task posts a message (which is just a “void *” pointer) to a mailbox, a mail slot is used to hold the message. When a task fetches a message, the message is copied to the fetcher’s pointer-to argument. Fetching a message removes it from the mailbox.

#include <ctype.h>
extern REXIS_MAILBOX *mbox1;
void writer_task(unsigned arg0)
{
while (1)
{
for (int x = 0; x < 128; x++)
{
if (isprint(x))
REXIS_MailboxPost(mbox1, &x);
REXIS_TaskSleepMS(rand() % 1000);
}
}
}
void reader_task(unsigned arg0)
{
while (1)
{
int *pi;

REXIS_MailboxFetch(mbox1, (void **)&pi, 0);
putchar(pi[0]);
}
}

Before these two tasks are run, mailbox mbox1 must be created. This can be done in main(), prior to calling REXIS_SysStart. It could also be created in one of the other tasks, as long as it is not referenced before it is created:

mbox1 = REXIS_MailboxCreate(0);

The argument to REXIS_Mailbox is the number of “mail slots”. If zero (0) is specified, a default value (which is 10 as of this writing) will be used instead. In this example, the writer_task loops through the values from 0 to 127, and if the value is a printable character, it drops it at the mailbox and then goes to sleep for a random number of milliseconds. The reader_task uses REXIS_MailboxFetch to fetch the message (i.e. the character), and then writes it out using putchar.

Here we begin to see some of the true power of an RTOS. When the reader task calls REXIS_MailboxFetch, if there is a pending message the call returns immediately with the message, and the call behaves like a function call with just some kernel transition overhead. If there is no message pending, then the reader task will block, and not use any CPU time. When the next message arrives, the REXIS kernel switches execution back to reader task at the earliest appropriate time. Using an RTOS, no explicit polling or busy waiting is needed, allowing the system to utilize the CPU time more efficiently.

If multiple tasks are waiting for a message to arrive at a mailbox, they are ordered by task priority, so a high priority task will receive the message before a lower priority one.

Peripheral Interrupt Handler and Processing Task

In an embedded system, the most efficient method of detecting sensor and other data input is through peripheral interrupts. An interrupt handler, or Interrupt Service Routine (ISR), is called when a hardware device receives data (e.g.: a character arrives at a UART receive register). The ISR reads the data from the hardware device, and the data can then be processed by the firmware. Because interrupts preempt other code from running, in order to minimize performance impact on the rest of the system. the ISR should do its job as quickly as possible and leave the more complex data processing to code that is not running in an interrupt context.

The following demonstrates this setup. It’s similar to the previous reader and writer example. In this example, a UART peripheral interrupt handler generates the data for the writer task. The reader task remains exactly the same as in the previous example. This program then implements a simple echo terminal program. While this use is trivial, as the reader task does not perform much processing, the basic idea is very powerful and applicable to all sort of programming tasks. For example, in a TCP/IP stack, a low level ethernet interrupt driver can fetch data whenever it is presented in the Ethernet hardware, and then pass it onto a high level TCP task to form a TCP packet, before sending it off to the TCP/IP client.

void UART2_RX_ISR(void)
{
static int ch;
ch = getchar(); REXIS_MailboxTryPostFromISR(mbox1, &ch); NVIC_ClearPendingIRQ(USART2_IRQn); }

The getchar() call reads the data off the UART hardware register. As this handler is only called when there is data available, the getchar() call will return immediately with the data. The ISR posts the character as a message to the mailbox similar to the previous example. The reader task is exactly the same as before, and will print out all characters received.

Note: Only a small subset of the REXIS API functions are callable from within an ISR, and they all end with “…FromISR” in their names.

MUTEX Example

A MUTEX (Mutually Exclusive access to shared resources) provides for controlled access to a shared resource. For example, if you have two tasks writing to the UART:

void task1(unsigned arg0)
{
while (1)
{
int x = 0;
printf(“Hello World %d\n”, x);
}
}

void task2(unsigned arg0)
{
while (1)
{
int x = 0;
printf(“I am alive! %d\n”, x);
}
}

Depending on various factors such as scheduling frequency, task priorities, etc., the chances are that the resulting output would be a jumble of characters sent from both tasks. The fix for this type of issue is to use a MUTEX:

extern REXIS_MUTEX *mutex1;

void task1(unsigned arg0)
{
while (1)
{
int x = 0;
REXIS_MutexLock(mutex1); printf(“Hello World %d\n”, x); REXIS_MutexUnlock(mutex1); } }

void task2(unsigned arg0)
{
while (1)
{
int x = 0;
REXIS_MutexLock(mutex1); printf(“I am alive! %d\n”, x); REXIS_MutexUnlock(mutex1); } }
… // somewhere in main() or earlier
mutex1 = REXIS_MutexCreate();

The MUTEX controls access to the output stream, and when it is run the output will look much saner, because once a task obtains the lock on a MUTEX, the second task will wait until the first task has finished and unlocked the MUTEX before the second task can lock the MUTEX itself.

Detour: Using the Same Function for Different Tasks

In the previous MUTEX example, task1 and task2 are basically the same except for the strings being used. While the following is a contrived example, often a single function can be used for multiple tasks:

void task(unsigned arg0)
{
char *text[2] = { 
“Hello World”,
“I am alive!”
};
char *s = text[arg0]; while (1) { int x = 0; REXIS_MutexLock(mutex1); printf(“%s %d\n”, s, x); REXIS_MutexUnlock(mutex1); } }

Obviously, in real code, the task should perform argument checking to ensure that the arguments are not out of range. The task creates a function, then invokes the task function with different initial arguments:

REXIS_TaskCreate(“task 1”, task, 0, 0, 0);
REXIS_TaskCreate(“task 2”, task, 0, 0, 1);

“arg0” can be of any type, typecasted to unsigned int if needed. For example, a function may take a pointer to different SPI port. With Cortex-M, as a pointer and an unsigned int has the same number of bits (32), so you can cast a pointer value to an int and back again without losing any information.

Semaphore Example

A semaphore can be used to count events, or to manage a fixed set of resources. In this example, consider the case of an interrupt handler which is triggered whenever an event occurs. In this example, we will again use the UART receive interrupt to act as the event trigger. The interrupt handler increments the semaphore value (i.e. the event count) by one. Then, periodically, an event processing task checks the semaphore value, and if it is non-zero, it will decrement the semaphore value by one.

For this contrived example, we will use a global variable to store the character received, so the event process task can access and display it just like in the previous terminal echo example. As the keyboard interface is significantly slower than the REXIS event handling, the possibility of lost characters occurring is minimal, although theoretically possible.

extern REXIS_SEMAPHORE *sema1;
int ch;
void UART2_RX_ISR(void)
{
ch = getchar();
REXIS_SemaphoreSignalFromISR(sema1);
}
void EventProcessTask(unsigned arg0)
{
while (REXIS_SemaphoreWait(sema1, 0))
{
putchar(ch);
}
}

REXIS_SemaphoreSignalFromISR increases the semaphore value by one within an ISR. A regular task can signal a semaphore via the function REXIS_SemaphoreSignal.

REXIS_SemaphoreWait decreases the semaphore value by one, unless its current value is zero, in which case the task blocks until the value becomes one or more (so again, it does not use any CPU time). The second argument to the function is the timeout value; i.e. how many milliseconds the task should wait for the semaphore value to be one or greater. A zero timeout value means that the task will wait forever.

This example also demonstrates that there are multiple ways to solve a problem. The previous solution of using a mailbox is arguably more elegant than this example using a semaphore and a global variable to pass information. Nevertheless, there are cases where semaphore is the better solution to a problem.

Note: In EventProcessTask, the call to REXIS_SemaphoreWait inside the “while” condition eliminates the need for a separate outer “while (1)” forever loop. The function REXIS_SemaphoreWait always returns a non-zero number, so this optimization can be done without causing problems.

Synchronous Message Passing Example

Mailboxes are anonymous and asynchronous. Any task can post and fetch messages to and from a mailbox, and both posting and retrieving mailbox messages do not cause a task to wait unless the mailbox is full (when posting) or empty (when retrieving) respectively. However, there are times when you may want to pass messages between tasks, and also want to synchronize their actions. For this and other uses, REXIS provides another set of message-passing APIs that are synchronous.

For example, consider the case of a writing a resource server, e.g. a task that returns a block of data from a block device such as an SD card. There are multiple ways to do this, of course, but synchronous message passing provides an elegant solution:

void SD_ServerTask(unsigned arg0)
{
int recbuf[2];
while (REXIS_MessageReceive(&sender_pid, recbuf, sizeof (int) * 2, 0)) { // reads a block (of BUFLEN) and return to sender if (recbuf[0] == CMD_READ) { unsigned char recbuf[BUFLEN]; int block_no = recbuf[1]; … // open the SD device SD_DeviceRead(block_no, buffer, BUFLEN); REXIS_MessageReply(sender_pid, buffer, BUFLEN); } … // other commands } }

void task1(unsigned arg0)
{
int cmdbuf[2];
while (1) { unsigned char recbuf[BUFLEN]; cmdbuf[0] = CMD_READ; cmdbuf[1] = 0; // read from block 0 REXIS_MessageSend(SD_ServerTask_pid, cmdbuf, sizeof (int)*2,
recbuf, BUFLEN, 0); // now recbuf contains the SD data // process the data… }
}

As with the mailbox API, REXIS does not restrict what kind of data may be passed as “messages”. The Send, Receive, and Reply functions all take buffers with the respective lengths as arguments. The details of these APIs are described later.

In this example, the SD_ServerTask performs various functions on the SD Card. One of them is CMD_READ, a command that reads a block of data from the card. The command enumerations, meanings, and formats are all defined by the tasks. The server task calls REXIS_MessageReceive to receive a message. When a message is received, the server task checks to see whether it is the CMD_READ command. If it is, the example code fragment reads the data from the SD card (these details are left as an exercise and are not relevant to the example here) and sends the data back to the sender task as a reply message via the REXIS_MessageReply API.

Meanwhile, the sender task “task1” sends a message to the server task via REXIS_MessageSend. The first argument is the receiver task process ID, which can be obtained in several ways. The second and third arguments are the message buffer and its length. The message in this case contains the CMD_READ command and the block number of the SD card to be read from. The fourth and fifth arguments are the buffer to receive the reply message and its length. The REXIS kernel blocks the sender task when it makes the REXIS_MessageSend call, and only unblocks the sender task when the receiver task replies to the sender, as described in the previous paragraph.

The order in which the tasks invoke REXIS_MessageSend and REXIS_MessageReceive does not matter. A message send will always block the sender task. If the receiver task is not waiting for a message at that moment, the message is queued at the receiver task until the receiver performs a message-receive call. Or if the receiver task is already waiting for a message, the sent message is delivered to the receiver task immediately. Therefore, the receiver task may or may not block when making a REXIS_MessageReceive call, depending on whether or not there is a pending message that was sent to it earlier.

These synchronous message passing functions are incredibly powerful. If an RTOS provides mailboxes as its only IPC mechanism, then the above functionality must be implemented using multiple mailboxes, or with the addition of a MUTEX or semaphores to synchronize data availability.

Watchdog Example

This example only works if you have implemented the watchdog functions for your MCU. See Watchdog Reset.

void task1(void)
{
REXIS_KillTask(0);
while (1)
;
}

On startup, REXIS creates a “null task” that is always runnable and calls the REXIS_UserPatWatchdog function whenever it is run in order to prevent the watchdog from resetting the system. In this example, the task calls REXIS_KillTask to kill the null task. The argument is the task ID to be killed, and the null task always has the task ID of zero. Therefore, the watchdog will not be “petted”, and the watchdog timer will reset the system after 2 to 3 seconds.

#rexis #rtos #interprocesscommunication 

Scroll to Top