-
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.
...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. And, you do mind the hardware.
- The Execution Image is a single-process. Roughly, we enable concurrency over a single process by composing a thread of execution with a unique portion of the running stack. This composition gives us a schedulable unit, we chose to call a Task:
- User Tasks and System Tasks have the same privilege, while running on separate stack pointers.
- Rationale for both decisions are on the Docbook.
-
Memory is mapped to the physical memory - we are talking about CPUs on the range of 8-256 KiB of RAM.
-
Tasks are strictly allocated on compile time. Cannot be created on runtime, fork, or join.
-
Application-specific needs for dynamic allocation and deallocation, as well as some kernel mechanisms, make use of an allocator that suits real-time demands: fixed-size blocks, word-aligned, O(1).
- 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.
-
Much effort was put on the selection and design of classic concurrency mechanisms: from crude stateless Sleep Queues to Mutexes with fully transitive priority inheritance. They are self-contained and composable mechanisms.
-
Oddly for message-passing over shared-memory, RK0 chose to enforce real-time policy by design. What normally would be a convenient way for exchanging data+synchronising over shared-memory was turned into kernel primitives.
-
You can use both paradigms or just one. If you have an use case that does not seem to have a dual implementation let me know.
- Periodicity mechanisms that can be phase-aware, when compensating for time-drifts. Bounded waitings. Application Timers.
It should be clear now that RK0 does not aim to be a fit for every application domain.
Where RK0 fits
- RK0 is suitable 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. The deployed application was tested and is trusted. This is the typical closed-model which most real-time critical applications fit.
Where it doesn't
- Any solution that until recently was handled by MMU equipped, moderately powerful devices, runnning tailored full-fledged OSes (historically BSDs, until Linuxes prevailed); anything that face it on the web, exposing to high attack surface.
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 Task = 4 tasks.
We will use priorities 1 to 4.
//@file core/inc/kconfig.h
/***[• USER-DEFINED TASKS (NUMBER) ********************************************/
/* !Account for the logger task if using it. */
#define RK_CONF_N_USRTASKS (4)
/***[• MINIMAL EFFECTIVE PRIORITY (HIGHEST PRIORITY NUMBER) ******************/
/* NOTE: if not willing to set the lowest priority in use, hardwire it to 31.
you skip the common pitfall of forgeting to update it, for a little memory
overhead */
#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, configured as follows:
//@file app/inc/logger.h
#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 256 /* 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)
//@file core/inc/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.
//@file application.c
#include <kapi.h> /* Kernel API */
#include <logger.h>
#include <bsp.h> /* platform bsp */
int main(void)
{
/* keep interrupts disabled */
kDisableIRQ();
/* this might setup the lower layer, configure PLLs, etc. depends
on the platform. */
BSP_Init();
/* < any other middleware initialisation might be placed here > */
initOtherSutff();
/* Configure and initialise armv6/7M core interrupts needed.
This a RK0 function that works for its target plaftorms,
with or without CMSIS-Core HAL */
kCoreInit();
/* initialise internal kernel
data structures and start the scheduler.
Interrupts will be enabled on due time. */
kInit();
/* we shall never return from kInit unless
things go wrong */
while(1)
{
/* it gets here only if fault is not caught before */
kErrHandler(RK_FAULT_APP_CRASH);
}
return (-1); /* keep it tight, int func must have a return */
}
/*** Synchronisation Barrier ***/
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 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)
#define LOG_PRIORITY 4 /* the logger task priority */
/* declare the monitor */
Barrier_t syncBarrier;
#define REQUIRED_TASKS 3
/*** Initialise Application ***/
/* This function is mandatory */
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 Q: Why there is no release yet?
A: A release is serious stuff. v1.0.0 will come when I can push:
-
a good Trace mechanism (it is ongoing)
-
can provide meaningful and honest system characterisation (it is ongoing)
-
the test harness in a way it works for any potential contributors (it works on my computer)
Copyright (C) 2025 Antonio Giacomelli | www.kernel0.org