RT Synchronous Messages

Synchronous Messages are a unique feature of ChibiOS/RT that allows to create client/server architectures into an embedded system. RT implements in its inner scheduler layer a message-passing mechanism, when a context switch is performed a message is always passed between the thread being switched out and the one being switched in. The message is exchanged with almost no overhead. This inner mechanism is used to implement the Synchronous Messages high level functionality.

Global Settings

CH_CFG_USE_MESSAGES This switch enables the synchronous messages API in the kernel.
CH_CFG_USE_MESSAGES_PRIORITY If enabled then threads are queued by priority rather than in FIFO order on the server queue. The default is disabled.

Description

When using synchronous messages there are two kind of threads: clients and servers.

  • Clients are threads that start a transaction by sending a message to a server thread then wait for a response message.
  • Servers are threads that wait for a transaction start from a client, once a message is received the server processes it and finally send a response message to the client. Servers are able to handle one message at time but can also handle multiple messages from different clients and to returns answers in an arbitrary order.

The following diagram shows the sequence of operations among clients and servers.

Transactions

Note that there can be threads that are clients and servers at the same time.

Messages

Messages are always signed scalars with type msg_t. This type is guaranteed to be cast-able to/from data pointers. Clients can send simple encoded messages but also pointers to structures representing complex transactions.

There are three predefined messages used internally to the RT kernel:

  • MSG_OK. Defined as 0 is a generic acknowledge message.
  • MSG_TIMEOUT. Defined as -1 is the message sent in case of timeouts.
  • MSG_RESET. Defined as -2 is the message sent to inform of an object reset condition.

It is assumed that pointers with values 0, -1 and -2 to not be valid pointers. Also note that there are no dedicated objects involved, the exchange is done directly between threads, each thread has its own queue of incoming messages.

API

chMsgSend() Sends a message to the specified thread.
chMsgWait() Waits for a message, returns the sender pointer as a thread_t *.
chMsgGet() Retrieves the message after exiting from chMsgWait().
chMsgRelease() Returns an answer to the specified sender.
chMsgIsPendingI() Evaluates to true if there is a message waiting in queue.

Message Passing

In this scenario there are multiple threads in the system that never share data, everything is done by exchanging messages. Each thread represents a service, the other threads can request the service by sending a message.

The advantage of this approach is to not have to deal with mutual exclusion, each functionality is encapsulated into a server thread that sequentially serves all the requests.
For example, you can have the following scenario:

  • A buffers allocator server.
  • A disk driver server.
  • A file system server.
  • One or more client threads.

Client-Server

Note that the threads do not exchange complex messages but just pointers to data structures in order to optimize the performance. Also note that a thread can be both client and server at the same time, the FS service in the previous scenario for example.

One extra advantage of this approach derives from a common problem when working with RTOSes, often it needed to integrate a library not designed to work with an RTOS that could likely be non-reentrant or use a lot of stack space, for example a graphical stack.
If this is the case then the external code can be encapsulated into a server thread, other threads would request access to code protected by the server by sending messages encoding the various possible operations.
The advantages would be:

  • The non-reentrant code is now usable from multiple threads, there are no mutual exclusion concerns.
  • The large stack must be allocated only for the server thread and not for all threads requesting access to the protected code.
  • The code always runs at a fixed priority level, the priority of its thread.

Examples

Server Thread

Server threads should follow this template.

static THD_FUNCTION(ServerThread, arg) {
 
  while (true) {
    /* Waiting for a queued message then retrieving it.*/
    thread_t *tp = chMsgWait();
    msg_t msg = chMsgGet(tp);
 
    /* Processing the message.*/
    ...;
 
    /* Sending back an acknowledge.*/
    chMsgRelease(tp, MSG_OK);
  }
}

Console Server

A console server thread handles the system-generated messages.

static thread_t *console;
 
/*
 * Console server thread, the argument is the stream where message
 * must be printed.
 */
static THD_WORKING_AREA(waConsoleServerThread, 512);
static THD_FUNCTION(ConsoleServerThread, arg) {
  BaseSequentialStream *stream = (BaseSequentialStream *)arg;
 
  while (true) {
    /* Waiting for a queued message then retrieving it.*/
    thread_t *tp = chMsgWait();
    const char *msg = (const char *msg)chMsgGet(tp);
 
    /* Printing message the message prefixing it with a time stamp.*/
    chprintf(stream, "%010d: %s\r\n", chVTGetSystemTime(), msg);
 
    /* Sending back an acknowledge.*/
    chMsgRelease(tp, MSG_OK);
  }
}
 
/*
 * Initialization.
 */
void main(void) {
 
  /*
   * System initializations.
   * - The Hardware Abstraction Layer is initialized.
   * - Kernel initialization, the main() function becomes a thread and the
   *   RTOS is active.
   */
  halInit();
  chSysInit();
 
  /* Opening the serial port 1.*/
  sdStart(&SD1, NULL);
 
  /* Starting the console thread making it print over the serial
     port.*/
  console = chThdCreateStatic(waConsoleServerThread, sizeof(waConsoleServerThread),
                              NORMALPRIO + 1, ConsoleServerThread, (void *)&SD1);
 
  (void)chMsgSend(console, (msg_t)"System started.");
 
  /* Continuing.*/
  ...;
}