Skip to content
Antonio Giacomelli edited this page Feb 6, 2026 · 378 revisions

RK0: The 'Real' Real-Time Kernel

The Docbook is the place to find what RK0 is on about.


RK0 is unapologetically real-time...

...and embedded as we know it. image

It acknowledges (or has not forgotten) the processs/thread abstraction, and the software design approach inherited from general purpose systems has 'time' as an afterthought. Conservative policies make Response-Time-Analysis easier. A (comprehensive) set of modular services – some quite distinctive – are tailored for concrete real-time demands.


Application Structure

The example code implements a Synchronisation Barrier - so-called Rendezvous. The Barrier is structured as a Monitor using a Condition Variable model. Note that a Condition Variable is not a primitive (there is no RK_COND_VAR). Alternatively a general (stateless) Sleep Queue is used along with a Mutex Semaphore. There are helpers functions to manipulate any combination of Sleep Queues and Mutexes atomically for signal, wait and broadcast operations - suitable for a Mesa monitor test-loop (characterised by while(!condition) { wait(condition) }) .

Suppose three tasks are cooperating - each one executes part of a job. For every round before making the result available, no task can execute again before all others also are done. We make them 'meet' on a synchronisation point before starting the next round of work.

Configuring the kernel

Knowing that we want to use the application logger facility:

3 solution tasks + 1 logger = 4 tasks.

Each solution task has a different priority (we will use 1, 2, 3). The logger task priority must be lower than any solution task; we set 4 as the lowest priority.

Therefore in kconfig.h we have:

/***[• USER-DEFINED TASKS (NUMBER) ********************************************/
/* !Account for the logger task if using it.                                  */
#define RK_CONF_N_USRTASKS                  (4)

/***[• MINIMAL EFFECTIVE PRIORITY (HIGHEST PRIORITY NUMBER)  ******************/
#define RK_CONF_MIN_PRIO                    (4)

/***[• SYSTEM CORE CLOCK  *****************************************************/
/* If using CMSIS-Core HAL you can set this value to 0, so it will fallback   */
/* to the HAL value set at SystemCoreClock. (Not valid for QEMU buildings).   */
/* Note CMSIS-Core is not bundled in RK0.                                     */
#define RK_CONF_SYSCORECLK                  (2000000UL)

/***[• KERNEL TICK ************************************************************/
/* This will set the tick as 1/RK_SYSTICK_DIV millisec                        */
/* 1000 -> 1 ms Tick, 500 -> 2 ms Tick, 100 -> 10ms Tick, and so forth        */
#define RK_CONF_SYSTICK_DIV                 (100UL)

We set the kernel to have a 10ms system tick, 4 user tasks and lowest priority of these tasks is 4.

The fourth user task is the LoggerTask. In app/inc/logger.h we allocate 128 words for its stack. Each Log message has a maximum length of 64 bytes, and there are 16 log buffers on a Memory Partition pool - every logPost(...) call takes a buffer, that is returned as soon as LoggerTask receives it from a queue, and prints the content on stderr. Now the buffer can be reused.

#define CONF_LOGGER 1 /* Turn logger on/off */

#if (RK_CONF_MESG_QUEUE == OFF)
#error "Need RK_CONF_MESG_QUEUE enabled for logger facility"
#endif
#endif

#if (CONF_LOGGER == 1)
#define LOGLEN 64    /* Max length of a single log message */
#define LOGPOOLSIZ 16 /* Number of log message buffers  */


#define LOG_STACKSIZE 128 /* Size of the stack */

Regarding kernel services, we need:

  • Sleep Queues
  • Mutexes
  • Message Queues (used by the logger facility)
  • (Memory Partitions for Dynamic allocation is always ON)

In kconfig.h:

/******************************************************************************/
/********* 3. INTER-TASK COMMUNICATION ****************************************/
/******************************************************************************/

#define RK_CONF_SLEEP_QUEUE                      (ON)

#define RK_CONF_SEMAPHORE                        (OFF)

#define RK_CONF_MUTEX                            (ON)

#define RK_CONF_MESG_QUEUE                       (ON)
#if (RK_CONF_MESG_QUEUE == ON)
#define RK_CONF_MESG_QUEUE_NOTIFY                (OFF)
#define RK_CONF_PORTS                            (OFF)
#endif

#define RK_CONF_MRM                              (OFF)

Writing the application

Depending on how you structure your application this can vary a little. Here, the main() function is already within the application.c file. If not and both compilation units need to be exposed to the same dependencies you might want to append them in application.h.

In application.c

#include <kapi.h> /* Kernel API */
/* Configure the application logger faciclity here */
#include <logger.h> 
#include <bsp.h>
int main(void)
{
     
    BSP_Init(); /* this might setup the lower layer, configure PLLs, etc. depends on the platform. for QEMU it is not needed. */

    /* < any other middleware initialisation might be placed here > */

    kCoreInit(); /* Configure and initialise armv6/7M core interrupts needed. This a RK0 function that works for its target plaftorms, with or without CMSIS-Core HAL */
    
    kInit(); /* initialise internal kernel data structures and start the scheduler */

    while(1)
    {  
        kErrHandler(RK_FAULT_APP_CRASH);   
    }

    return (0); /* keep it tight */
 
}

/* Declare objects needed for each task: its Handle name, its entry function, stack buffer name and stack size */


#define STACKSIZE 256 /* 1024 Bytes for each stack. Note logPost have are expensive. */

RK_DECLARE_TASK(task1Handle, Task1, stack1, STACKSIZE)
RK_DECLARE_TASK(task2Handle, Task2, stack2, STACKSIZE)
RK_DECLARE_TASK(task3Handle, Task3, stack3, STACKSIZE)


/* Synchronisation Barrier Pattern code */

typedef struct
{
    RK_MUTEX            lock;
    RK_SLEEP_QUEUE      cond;
    UINT count;        /* number of tasks in the barrier */
    UINT required;     /* number of required tasks */
    UINT round;        /* increased every time all tasks synch     */
} Barrier_t;

VOID BarrierInit(Barrier_t *const barPtr, UINT requiredTasks)
{
    kMutexInit(&barPtr->lock, RK_INHERIT);
    kSleepQueueInit(&barPtr->cond);
    barPtr->count = 0;
    barPtr->round = 0;
    barPtr->required = requiredTasks;
}

VOID BarrierWait(Barrier_t *const barPtr)
{
    UINT myRound = 0;
    kMutexLock(&barPtr->lock, RK_WAIT_FOREVER);

    /* save round number */
    myRound = barPtr->round;
    /* increase count on this round */
    barPtr->count++;
    logPost("[BARRIER: %u/%u]: %s ENTERED ",  barPtr->count, barPtr->required, RK_RUNNING_NAME);

    if (barPtr->count == barPtr->required)
    {
         logPost("[BARRIER: %u/%u]: %s WAKING ALL TASKS",  barPtr->count, barPtr->required, RK_RUNNING_NAME);


        /* reset counter, inc round, broadcast to sleeping tasks */
        barPtr->round++;
        barPtr->count = 0;
        kCondVarBroadcast(&barPtr->cond);
    }
    else
    {
         logPost("[BARRIER: %u/%u]: %s BLOCKED ",  barPtr->count, barPtr->required, RK_RUNNING_NAME);
        /* a proper wake signal might happen after inc round */
        while ((UINT)(barPtr->round - myRound) == 0U)
        {
            kCondVarWait(&barPtr->cond, &barPtr->lock, RK_WAIT_FOREVER);
        }
    }

    kMutexUnlock(&barPtr->lock);

}


/* declare barrier object */
Barrier_t syncBarrier;
#define REQUIRED_TASKS 3

#define LOG_PRIORITY 4 /* the lowest priority */ 
/* MANDATORY Function - initialise declared kernel objects. kCreateTasks will assemble a Task Control Block to each task
using the declared objects for each task and initialise it */
VOID kApplicationInit(VOID)
{

    RK_ERR err = kCreateTask(&task1Handle, Task1, RK_NO_ARGS, "Task1", stack1, STACKSIZE, 1, RK_PREEMPT);
    K_ASSERT(err==RK_ERR_SUCCESS);

    err = kCreateTask(&task2Handle, Task2, RK_NO_ARGS, "Task2", stack2, STACKSIZE, 2, RK_PREEMPT);
    K_ASSERT(err==RK_ERR_SUCCESS);

    err = kCreateTask(&task3Handle, Task3, RK_NO_ARGS, "Task3", stack3, STACKSIZE, 3, RK_PREEMPT);
    K_ASSERT(err==RK_ERR_SUCCESS);
 
    BarrierInit(&syncBarrier, REQUIRED_TASKS); /* initialise barrier (sleep queues and mutexes) */
   
    logInit(LOG_PRIORITY); /* initialise application logger */

}

/* Tasks definition */

VOID Task1(VOID* args)
{
    RK_UNUSEARGS
    while (1)
    {
        logPost("Task 1 dispatched. Working...");
        kBusyDelay(100); /* simulate work */
         BarrierWait(&syncBarrier);
        logPost("Task 1 left the barrier!");
        kSleep(1); /* suspend so other task can run */
    }
}

VOID Task2(VOID* args)
{
    RK_UNUSEARGS
    while (1)
    {
        logPost("Task 2 dispatched. Working...");
        kBusyDelay(200); /* simulate work */
        BarrierWait(&syncBarrier);
        logPost("Task 2 left the barrier!");
        kSleep(1); /* suspend so other task can run */
    }
}

VOID Task3(VOID* args)
{
    RK_UNUSEARGS
    while (1)
    {
        logPost("Task 3 dispatched. Working...");
        kBusyDelay(300); /* simulate work */
        BarrierWait(&syncBarrier);
        logPost("Task 3 left the barrier!");
        kSleep(1); /* suspend so other task can run */
    }
}

The result is as follows:

       0 ms :: Task 1 dispatched. Working... 
    1000 ms :: [BARRIER: 1/3]: Task1 ENTERED  
    1000 ms :: [BARRIER: 1/3]: Task1 BLOCKED  
    1000 ms :: Task 2 dispatched. Working... 
    3000 ms :: [BARRIER: 2/3]: Task2 ENTERED  
    3000 ms :: [BARRIER: 2/3]: Task2 BLOCKED  
    3000 ms :: Task 3 dispatched. Working... 
    6000 ms :: [BARRIER: 3/3]: Task3 ENTERED  
    6000 ms :: [BARRIER: 3/3]: Task3 WAKING ALL TASKS 
    6000 ms :: Task 3 left the barrier! 
    6000 ms :: Task 1 left the barrier! 
    6000 ms :: Task 2 left the barrier! 
    6010 ms :: Task 1 dispatched. Working... 
    7010 ms :: [BARRIER: 1/3]: Task1 ENTERED  
    7010 ms :: [BARRIER: 1/3]: Task1 BLOCKED  
    7010 ms :: Task 2 dispatched. Working... 
    9010 ms :: [BARRIER: 2/3]: Task2 ENTERED  
    9010 ms :: [BARRIER: 2/3]: Task2 BLOCKED  
    9010 ms :: Task 3 dispatched. Working... 
   12010 ms :: [BARRIER: 3/3]: Task3 ENTERED  
   12010 ms :: [BARRIER: 3/3]: Task3 WAKING ALL TASKS 

Questions

Q: Why there is no release yet?

A: A release is serious stuff. It will exist when:

  • The system is characterised consistently.
  • A seamless test harness for eventual contributors can be pushed;
  • and a CI can be pushed.

Note

Expect v0.x.x for a long time, or maybe jump-in to collaborate.

Q: I heard not splitting user-space from kernel-space makes a bad, lame, last-week, toy-kernel, What you would say?

A: First, to get some real-time literacy (Buttazzo, Bertolotti, Kopetz, etc.).

Second, not every chip RK0 supports has an MPU. Why? Because there is a demand for them. And I ensure they are not coffee-machines - they are controlling loops on plant floors. (not running what should be handled by a moderately small, MMU equipped device running a tailored OpenBSD)

Am I saying that you do not ever need that? Not at all! I exposing a design choice rationale that has gains and drawbacks.

An MPU can be extremely useful - it is a Memory Firewall. But it is not free: it has impact on determinism and jitter, coarse memory utilisation, not just on raw performance. Knowing when not to use it is also engineering.

(Yes, Privilege levels are still allowed with no MPU on the target architectures. Reason: MPU is an external device, the vendor (not ARM) chooses to attach or not.)

Going further: Safety/security standards (IEC 61508, ISO 26262, DO-178, etc.) with regard to RK0:

  • As stated on the Docbook, RK0 links altogether into a single image**. It is suits application-specific, domain-dependent, hardware-dependent, real-time cyber-physical systems

  • A firmware update is updating the entire image. There is no 'hosted application'.

A system employing an RT-Executive like RK0: small codebase, static configuration bias, and strictly bounded behaviour, has gains for a certification process: these characteristics make analysis, testing, and traceability simpler. Even formal proofs would be made easier.


Clone this wiki locally