-
Notifications
You must be signed in to change notification settings - Fork 2
Home
The Docbook is the place to find what RK0 is on about.
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.
This example demonstrates how to configure the kernel, declare kernel objects, and structure an application.
The code demonstrates 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 to manipulate any combination of Sleep Queues and Mutexes as Cond Vars.
Suppose three tasks are cooperating - each one process part of a workload. For every round of work, no task can execute again before all others also are done. They need to 'meet' on a synchronisation point before starting again.
We will have 3 tasks each one has with different priorities. We use an application logger that runs on its own task at 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.
In app/inc/logger.h we define a logger stack of 256 words (remember that the log performs printfs), each log message has a maximum length of 64 bytes and the pool of memory blocks used for each log message has 16 log buffers.
#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)
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);
}
}
/* 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 round; /* increased every time all tasks synch */
} Barrier_t;
VOID BarrierInit(Barrier_t *const barPtr)
{
kMutexInit(&barPtr->lock, RK_INHERIT);
kSleepQueueInit(&barPtr->cond);
barPtr->count = 0;
barPtr->round = 0;
}
VOID BarrierWait(Barrier_t *const barPtr, UINT const nTasks)
{
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, nTasks, RK_RUNNING_NAME);
if (barPtr->count == nTasks)
{
logPost("[BARRIER: %u/%u]: %s WAKING ALL TASKS", barPtr->count, nTasks, 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, nTasks, 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);
}
#define N_BARR_TASKS 3
/* declare barrier object */
Barrier_t syncBarrier;
#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); /* 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, N_BARR_TASKS);
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, N_BARR_TASKS);
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, N_BARR_TASKS);
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...
12020 ms :: Task 1 dispatched. Working...
13020 ms :: [BARRIER: 1/3]: Task1 ENTERED
13020 ms :: [BARRIER: 1/3]: Task1 BLOCKED
13020 ms :: Task 2 dispatched. Working...
15020 ms :: [BARRIER: 2/3]: Task2 ENTERED
15020 ms :: [BARRIER: 2/3]: Task2 BLOCKED
15020 ms :: Task 3 dispatched. Working...
18020 ms :: [BARRIER: 3/3]: Task3 ENTERED
18020 ms :: [BARRIER: 3/3]: Task3 WAKING ALL TASKS
18020 ms :: Task 3 left the barrier!
18030 ms :: Task 1 dispatched. Working...
19030 ms :: [BARRIER: 1/3]: Task1 ENTERED
19030 ms :: [BARRIER: 1/3]: Task1 BLOCKED
19030 ms :: Task 2 dispatched. Working...
21030 ms :: [BARRIER: 2/3]: Task2 ENTERED
21030 ms :: [BARRIER: 2/3]: Task2 BLOCKED
21030 ms :: Task 3 dispatched. Working...
24030 ms :: [BARRIER: 3/3]: Task3 ENTERED
24030 ms :: [BARRIER: 3/3]: Task3 WAKING ALL TASKS
24030 ms :: Task 3 left the barrier!
24030 ms :: Task 1 left the barrier!
24030 ms :: Task 2 left the barrier!
24040 ms :: Task 1 dispatched. Working... 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 is bad, lame, last-week, toy-kernel, DOS-like. 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 a tailored OpenBSD)
Yes, Privilege levels are still allowed with no MPU. But not because it 'makes sense'. It is because tweaking an architectural feature that depends on the existence of an external co-processor is unfeasible.
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.
Going further: Safety/security standards (IEC 61508, ISO 26262, DO-178, etc.) with regard to RK0:
-
As stated, RK0 links altogether into a single image**. It is application-specific, hardware-dependent.
-
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.
Copyright (C) 2025 Antonio Giacomelli | www.kernel0.org