-
Notifications
You must be signed in to change notification settings - Fork 2
Home
The Docbook provides design internals, design choice rationale, some theory and rich usage examples.
The User's Guide is a streamlined document, focusing on services description and minimal idiomatic examples.
...and embedded as we know it.

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.
RK0 is a Real-Time Executive for application-specific firmware on constrained MCU-based solutions driving physical processes. Programming these systems is less about 'using APIs' (or fighting them) and more about reasoning on runtime behaviour: which tasks exist, which events wake them, what put them to sleep, and what worst-case bounds follow.
- The concurrency unit is called Task, and implements the 'thread' concurrency model. A Task is a thread.
- From above, the Execution Image is that of a single-process that we arrange for concurrency: multiple threads, each one granted a part of the running stack. Memory maps to physical memory.
- User tasks and system tasks have the same privilege. They run following different stack pointers. Tasks are static.
- Overall memory allocation is static. A deterministic allocator is available for application design. If willing for dynamic kernel objects it can handle. Tasks on the other hand are strictly static.
- The scheduler always selects the highest-priority READY task.
- A task after selected to run, will run until it blocks, yields, or is preempted by a higher-priority READY task.
- This is not only real-time mindset, it enforces rational design of tasks as they need to have a reason to exist, a well-defined job to execute and an urgency.
- Time-slice is not a kernel concern.
- RK0 supports classic shared-memory (events, locks) and message-passing. You can use both or just one.
Time is a first-class concern.
- Periodic Execution rates, phase-locking, time drift compensation, bounded waiting, are explicit service concerns.
Where RK0 fits
RK0 fits systems which quality of service degrades quickly when not meeting time constraints. Tasks can't be unaware of each other and are actually cooperating concurrent units. The solution is domain-dependent. The hardware is domain-dependent.
Good fits include:
- control and automation firmware (motor drives, power, motion, protection logic)
- systems that need bounded blocking and predictable scheduling under load
- applications where you can define worst-case behaviour offline (WCET, bounded critical sections, bounded queues)
Where RK0 does not fit
Any solution that until recently was handled by MMU equipped, moderately powerful devices, runnning tailored full-fledged OSes (historically BSDs, until Linuxes prevailed):
- anything multiprogramming or anything that can't tell upfront what will be running; hosted applications.
- internet-facing devices with a large attack surface where the MCU is treated as a secure network boundary.
- designs that require process isolation, rich filesystems, heavy networking stacks as first goals.
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:
while(condition == false)
{
sleep on event(condition == true);
} 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.
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 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)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 */
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; /* this lock keeps a single active task in the monitor */
RK_SLEEP_QUEUE cond; /* queue tasks sleep for required==count */
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)
{
/* helper: task sleeps and unlock the mutex (so another task can enter)
atomically */
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 */
}
} 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 Copyright (C) 2025 Antonio Giacomelli | www.kernel0.org