Skip to content

User Manual

Antonio Giacomelli edited this page Aug 22, 2026 · 14 revisions

RK0 User Manual

Table of Contents

Kernel Version: V0.73.0-dev

Given updates, some information found here can be contradictory. In such cases:

  1. Sleep Queues/Condition Queues: sleep queues might be referred as condition queues. The important thing here is that they are not condition variables. They are simple waiting queues that do not record any signal token.

  2. Mutex Semaphores and Mutex Locks: Mutex Semaphores are Binary Semaphores being used for mutual exclusion. Semaphores do not have ownership of a property and do not handle priority inversion. The purpose of Mutex Locks is specifically to acquire a critical region (you cannot use a Mutex Lock to signal a task) and handle fully transitive priority inheritance. Binary Semaphores are to be an option in very controlled scenarios — when priority inversion is guaranteed to not be an issue.

  3. Priority Ceiling: Priority ceiling is exclusively used on asynchronous direct message passing, not on Mutex Locks. While a task owns at least one message from that pool, its effective priority is raised to at least the configured ceiling until ownership moves to another task or the message is freed. Choose the ceiling as the highest-priority task that may wait for that pool. Pass RK_MESG_PRIO_CEILING_NONE to initialise a pool with no ceiling protocol.

  4. There is no difference between Task ID/TID/PID.

  5. Any type followed by _HANDLE is an address. Dynamic creation/destruction should use these types.


1. About this manual

This manual explains how to use RK0 services and what an application observes. It is deliberately shorter and more operational than the RK0 DocBook.

For each service, the manual asks:

  • What relation does it express?

  • What does success mean to the caller?

  • What state or history does the service retain?

  • When should it be selected instead of a nearby alternative?

  • Which priority dependency appears when a task waits?

1.1. Notation

Priority 0 is the highest user priority. A smaller number means a more urgent task.

State Operational meaning

INITIALISED

The task object exists but is not eligible to execute

READY

The task may be selected by the scheduler

RUNNING

The task owns the processor

WAITING

The task cannot execute until a time or coordination condition is satisfied

TERMINATED

A runtime-spawned task has ended and awaits or has completed reclamation

Return Values are successful (0), unsuccessful but valid (positive), or invalid (negative). Only invalid returns are faulty returns.

Value class Meaning Examples

0

Operation completed

RK_ERR_SUCCESS

positive

Valid operation; requested condition was not met

timeout, queue full, no waiter

negative

Invalid object, parameter, state, or execution context

null object, wrong owner, ISR misuse

Do not treat every non-zero result as a kernel fault. A non-blocking receive from an empty queue is a normal observation. Invalid ownership or execution context is not.

1.2. Core and Optional Services

Core Services are always enabled. Optional Services are turned ON/OFF in kconfig.h.

1.2.1. Core Services are:

  • Task Delays (Busy/Sleep Until/Sleep Release)

  • Task Event Register

  • Partition Memory

1.2.2. Optional Services

Although highly modular, two dependencies exist:

  • Asynchronous Task-to-task Message uses Message Queue infrastructure

  • Condition Variables/Monitors (Mesa Semantics) are treated as a usage pattern (not a service) that composes Sleep Queues and Mutex Locks, providing helper functions. So it needs both Sleep Queues and Mutex Locks enabled.


2. Run RK0 on QEMU

RK0 builds with the GNU Arm Embedded toolchain. The repository demonstration runs on QEMU.

2.1. Build and run

git clone https://github.com/antoniogiacomelli/RK0.git
cd RK0
make ARCH=armv7m qemu

Use ARCH=armv6m for the Cortex-M0 QEMU target. The Makefile also accepts the lower-case compatibility form arch=....

The default application is app/src/application.c. Its selectable examples act as executable documentation.

The principal build outputs are placed under build/<architecture>/:

  • rk0_demo.elf - executable and debugger image;

  • rk0_demo.bin - raw binary;

  • rk0_demo.hex - Intel HEX image;

  • rk0_demo.map - linker map.

2.2. Start a debug server

make ARCH=armv7m qemu-debug

QEMU waits for GDB on TCP port 1234.


3. Build-time configuration (kconfig.h)

core/inc/kconfig.h defines the kernel instance built into the application. It is included by the common kernel headers, so its values affect kernel sources, public declarations, and application code at compile time.

Configuration is global: every translation unit must be built against the same file. Use ON and OFF for Boolean options. After changing kconfig.h, perform a clean rebuild so no object compiled with the previous feature set remains.

Configuration is not runtime admission control. Pool sizes, task counts, stack sizes, and the chosen tick still require application memory and timing analysis.

3.1. Clock and kernel tick (RK_CONF_SYSTICK_DIV)

RK_CONF_SYSCORECLK is the processor clock in hertz. A non-zero value is used directly.

Note

⚠️ On supported hardware builds, 0UL requests the CMSIS-Core SystemCoreClock value. CMSIS-Core is not bundled with RK0, 0UL is not valid for the QEMU build.

RK_CONF_SYSTICK_DIV is the number of kernel ticks generated per second. Despite its name, it is a frequency divisor applied to the core clock, not the tick duration in milliseconds:

systick_counts   = RK_CONF_SYSCORECLK / RK_CONF_SYSTICK_DIV;
tick_interval_ms = 1000UL / RK_CONF_SYSTICK_DIV;

The Cortex-M port loads systick_counts - 1 into the 24-bit SysTick reload register.

RK_CONF_SYSTICK_DIV Tick frequency Tick interval

1000UL

1000 Hz

1 ms

500UL

500 Hz

2 ms

250UL

250 Hz

4 ms

100UL

100 Hz

10 ms

The default is 100UL, giving a 10 ms tick. A shorter tick improves timeout and release granularity but increases periodic interrupt and timeout-list processing. A longer tick reduces that overhead but quantises more application durations to the same tick.

Choose a configuration satisfying all of these constraints:

  • RK_CONF_SYSTICK_DIV is greater than zero and no greater than 1000 for the integer-millisecond model.

  • 1000UL should divide evenly by RK_CONF_SYSTICK_DIV; otherwise the interval used by millisecond helpers is truncated.

  • The core clock should divide evenly by RK_CONF_SYSTICK_DIV when an exact hardware frequency is required.

  • RK_CONF_SYSCORECLK / RK_CONF_SYSTICK_DIV is at least 1 and at most 0x1000000 because Cortex-M SysTick has a 24-bit reload field.

An invalid configuration can leave the computed tick interval at zero and prevent the scheduler from starting. RK_QEMU_UNIT_TEST deliberately overrides the task capacity and tick used by repository tests. Do not use it as an application configuration switch.

3.1.1. Consequences for time APIs

kTickGet() returns raw ticks.

/* RK_CONF_SYSTICK_DIV == 100UL: one tick is 10 ms. */
RK_TICK a = RK_MS_TO_TICKS(25U); /* 2 ticks: 20 ms */

/**
/* @note
/* If RK_CONF_ROUND_UP_MS_TO_TICKS is `ON`
*/

RK_TICK b = RK_MS_TO_TICKS(5U) /* b = 1 tick -> 10 milliseconds   */
/* if OFF */
RK_TICK b = RK_MS_TO_TICKS(5U) /* b = 0  -> as 5milliseconds cannot be represented */

Finite time arguments are limited to RK_MAX_PERIOD, or 0x7fffffff ticks. That is approximately 24.9 days at 1 ms per tick and 248.6 days at 10 ms per tick. RK_WAIT_FOREVER is a separate sentinel — not a timeout that hopefully will never expires.

3.2. Scheduler capacity and system stacks

Symbol Unit Effect

RK_CONF_N_USRTASKS_MAX

tasks

Sizes the shared TCB pool for startup and runtime-spawned user tasks. Optional facilities created with kTaskInit(), including Trace or an application Logger, also consume a slot.

RK_CONF_IDLE_STACKSIZE

32-bit words

Sizes the Idle task stack, including the application Idle hook.

RK_CONF_POSTPROC_STACKSIZE

32-bit words

Sizes the PostProc stack used by timer callbacks and deferred kernel work.

RK_CONF_DYNAMIC_TASK

ON / OFF

Includes runtime task spawning and termination support.

System stack sizes are expressed in words, not bytes. Keep them at or above RK_MIN_STACKSIZE, even, and compatible with the port’s 8-byte alignment. Measure stack use with the actual hooks and callbacks enabled.

RK_CONF_ARMV6M is normally inferred from compiler architecture macros. It selects smaller default task, trace, and dynamic-pool capacities for Cortex-M0-class targets. Override it only when the toolchain does not identify the target correctly.

3.3. Dynamic-object pools

RK_CONF_DYNAMIC_OBJECTS defaults to ON independently of RK_CONF_DYNAMIC_TASK. It enables bounded runtime creation for supported non-task control objects.

Object family Maximum-count symbol

Semaphore

RK_CONF_DYNAMIC_SEMAPHORES_MAX

Mutex

RK_CONF_DYNAMIC_MUTEXES_MAX

Sleep Queue

RK_CONF_DYNAMIC_SLEEP_QUEUES_MAX

Message Queue / Mailbox

RK_CONF_DYNAMIC_MESG_QUEUES_MAX

Application Timer

RK_CONF_DYNAMIC_TIMERS_MAX

Most-Recent Message

RK_CONF_DYNAMIC_MRMS_MAX

A maximum reserves capacity; it does not instantiate every object. Queue storage, MRM arrays, and Asynchronous Direct Message pools remain application-owned.

Initialise the dynamic control-block pools once with RK_INIT_OBJ_PARTITIONS before the first matching Create call.

3.4. Service feature gates

Area Configuration symbols

Time

RK_CONF_CALLOUT_TIMER

Shared state

RK_CONF_SEMAPHORE, RK_CONF_MUTEX, RK_CONF_SLEEP_QUEUE, RK_CONF_CONDVAR

Buffered messages

RK_CONF_MESG_QUEUE, RK_CONF_MESG_QUEUE_SEND_CALLBACK

Direct task messages

RK_CONF_SYNCH_MESG, RK_CONF_ASYNCH_MESG

Latest-state publication

RK_CONF_MRM

Condition-variable helpers require Mutex and Sleep Queue support. Mailboxes use RK_CONF_MESG_QUEUE. RK_CONF_ASYNCH_MESG also requires RK_CONF_MESG_QUEUE because direct-message endpoints use queue infrastructure.

Disabling a service is an architectural decision. Rebuild every translation unit against the same kconfig.h.

3.5. Trace configuration

RK_CONF_TRACE includes the interactive Trace task and its records.

Symbol Controls

RK_CONF_TRACE_STACKSIZE

Trace task stack in 32-bit words

RK_CONF_TRACE_PRIO

Trace task priority

RK_CONF_TRACE_MAX_OBJECTS

Number of registered objects

RK_CONF_TRACE_LINE_LEN

Interactive command-line buffer

RK_CONF_TRACE_RECORD_DEPTH

Per-object history depth

RK_CONF_TRACE_OVERFLOW_BACKLOG

Records retained after normal history overflow

RK_CONF_TRACE_FRAME_BUFFER_DEPTH

Buffered machine-readable frames

RK_CONF_TRACE_FRAME_STDOUT

Immediate frame output instead of buffering

RK_CONF_TRACE_TASK_PRIO_HISTORY

Recording of task effective-priority changes

Trace storage grows with object and history capacities. Trace timing depends on its priority and output backend. Treat it as part of the target configuration; do not assume a development Trace build has zero interference.

3.6. Debug and release error checking

Without NDEBUG, kconfig.h enables RK_CONF_ERR_CHECK, RK_CONF_FAULT, and RK_CONF_FAULT_PRINT_STDERR. These checks detect invalid objects, parameters, contexts, and protocol use. They do not turn a well-defined unsuccessful result such as timeout or queue-full into a fault.

Defining NDEBUG removes that default checking configuration. API preconditions then become application obligations. Invalid use may no longer return the diagnostic code or halt as observed in a checked build.

Validate the release configuration independently rather than treating NDEBUG as only an assertion switch.


4. A minimal RK0 application

An RK0 application provides ordinary platform initialisation, static kernel storage, and the mandatory kApplicationInit() function.

#include <kapi.h>

#define WORKER_STACK_WORDS 160U
#define WORKER_PRIORITY    2U

RK_DECLARE_TASK(workerHandle, WorkerTask,
                workerStack, WORKER_STACK_WORDS)

int main(void)
{
    kCoreInit();
    kInit();

    /* kInit() does not return during normal operation. */
    while (1)
    {
        kErrHandler(RK_FAULT_APP_CRASH);
    }
}

VOID kApplicationInit(VOID)
{
    /* Required before any dynamic-object Create operation. */
    RK_INIT_OBJ_PARTITIONS

    RK_ERR err = kCreateTask(&workerHandle,
                             WorkerTask,
                             RK_NO_ARGS,
                             "Worker",
                             workerStack,
                             WORKER_STACK_WORDS,
                             WORKER_PRIORITY,
                             RK_PREEMPT);
    K_ASSERT(err == RK_ERR_SUCCESS);
}

VOID WorkerTask(VOID *args)
{
    RK_UNUSEARGS

    while (1)
    {
        DoOneCycle();
        kSleepPeriodic(RK_MS_TO_TICKS(20U));
    }
}

RK_DECLARE_TASK declares the entry prototype, an 8-byte-aligned stack, and a task handle. Stack size is expressed in 32-bit words. The handle and stack must remain valid for the application lifetime; file-scope storage is normal.

kCreateTask is an alias of kTaskInit.

kInit() calls kApplicationInit(), completes kernel initialisation, makes created tasks ready, enables interrupts, and starts the highest-priority task. Create startup tasks and initialise their dependencies inside kApplicationInit().

RK_INIT_OBJ_PARTITIONS initialises fixed pools used by runtime-created kernel objects. It is a no-op when RK_CONF_DYNAMIC_OBJECTS is disabled, so it may remain in common startup code. Call it once before any dynamic-object Create operation. It does not create an object.


5. Tasks and execution progress

5.1. Task states

The scheduler is the only component that changes a task from READY to RUNNING.

stateDiagram-v2
    [*] --> INITIALISED
    INITIALISED --> READY: scheduling starts
    READY --> RUNNING: dispatch
    RUNNING --> READY: yield or preemption
    RUNNING --> WAITING: blocking service
    WAITING --> READY: condition satisfied
    RUNNING --> TERMINATED: dynamic termination
Loading

A static task normally moves among INITIALISED, READY, RUNNING, and a family of WAITING pseudo-states. A dynamic task may additionally become TERMINATED.

  • Creation makes a task ready when scheduler start permits it.

  • The scheduler changes one ready task to running.

  • A higher-priority ready task may preempt a preemptible running task.

  • kYield() voluntarily returns the caller to ready.

  • A blocking service changes the caller to a service-specific waiting state.

  • Satisfaction, cancellation, or timeout removes the wait dependency and makes the task ready.

Returning from a task entry function is not a task-termination operation. Task functions should normally contain a loop. Runtime-spawned tasks terminate through the dynamic-task API.

5.2. Fixed-priority FIFO scheduling

RK0 maintains one FIFO ready queue for each normal priority from 0 through 31. It performs no automatic time slicing. A 32-bit bitmap records which queues are non-empty.

flowchart TB
    B["Ready bitmap"] --> P0["Priority 0 FIFO"]
    B --> P1["Priority 1 FIFO"]
    B --> PN["... Priority 31 FIFO"]
    P0 --> S["Scheduler selects least-numbered non-empty queue"]
    P1 --> S
    PN --> S
Loading

When the bitmap is non-zero, its least-significant set bit identifies the highest-priority non-empty queue. Within one priority:

  • initial order follows task creation order;

  • a preempted task returns to the head, so it resumes before equal-priority peers;

  • a yielded task goes to the tail;

  • a task made ready after waiting goes to the tail.

The Idle task is outside the normal ready queues and bitmap. It is dispatched only when the bitmap is zero. It is not the priority-31 task and does not compete with application work.

Equal-priority tasks cooperate by yielding or waiting. If the running task does neither, the application has expressed no reason for an equal-priority task to run.

RK_NO_PREEMPT creates an exceptional non-preemptible task. Once dispatched, it continues until it blocks, yields, or otherwise leaves RUNNING. Use it only for short, bounded routines: it can delay every user task, including more urgent ones.

5.3. Yielding is not sleeping

kYield() says the caller has completed its present turn among ready peers. It does not express a time condition and does not put the task into WAITING.

If no equal- or higher-priority task is ready, yielding does not manufacture work for a lower-priority task. The caller remains the correct task to run.

5.4. Scheduler lock

kSchLock() and kSchUnlock(), also exposed as kPreemptDisable() and kPreemptEnable(), defer user-task preemption. Locks are nested and belong to the task.

The scheduler lock does not disable interrupts and is not a Mutex. It establishes neither shared-resource ownership nor priority inheritance. Use it only where delaying dispatch is itself the required bounded operation.


6. Waiting, timeouts, and execution context

Services that may wait accept one of three timeout forms.

Timeout Caller behaviour

RK_NO_WAIT

Inspect or attempt the operation and return immediately

finite ticks

Wait up to the requested kernel time

RK_WAIT_FOREVER

Wait until the service condition is satisfied

Finite values must not exceed RK_MAX_PERIOD.

6.1. What a timeout guarantees

A timeout removes the task’s dependency and makes it ready. Actual return still depends on fixed-priority scheduling. The return value tells the application which completion occurred.

For Synchronous Invocation:

  • a timeout before server acceptance removes the pending call;

  • a timeout after acceptance abandons the reply from the caller’s perspective;

  • the server must still close the accepted rendezvous with kSynchMesgReply(), but no reply is copied to the timed-out caller.

For Asynchronous Direct Message, kMesgAlloc() may wait for a pool block. kMesgSend() has no timeout because it does not wait for endpoint queue capacity.

6.2. Interrupt service routines

An ISR must never request a blocking operation. Some services expose non-blocking ISR paths, including kEventSet(), kSemaphorePost(), and documented no-wait Message Queue calls. Work that wakes several tasks may be deferred to PostProc.

Synchronous/Invocation and Asynchronous Direct Message endpoints are task-context services. kMesgSend() is non-blocking, but is still rejected from ISR context. Check kapi.h for the exact allowed context instead of inferring ISR safety from the absence of a timeout argument.


7. Time services

RK0 keeps different meanings of time in separate calls.

Service Meaning Best use

kBusyDelay(t)

Consume t ticks while RUNNING

Simulated or deliberately active workload

kSleepDelay(t)

Sleep relative to this call

One-off delay or back-off

kSleepUntil(&anchor, p)

Advance a task-local deadline by p

Periodic work that must account for every activation

kSleepRelease(p)

Target the next scheduler-start phase slot

Phase-aligned periodic work that may skip missed releases

7.1. Busy Delay

kBusyDelay(t) leaves the caller running. Only ticks observed while the caller executes count towards completion. Time spent preempted does not reduce the remaining busy delay.

Use it to represent CPU work in an example or for a genuinely active wait. It does not release the processor to lower-priority work.

7.2. Sleep Delay

kSleepDelay(t), also named kSleep(t), suspends the task for a relative duration measured from the call.

DoWork();
kSleep(RK_MS_TO_TICKS(50U));

In a loop, work time, preemption, and release jitter accumulate. Do not use relative sleep to define a periodic task.

7.3. Sleep Until

kSleepUntil() uses a reference owned by the task.

RK_TICK anchor = kTickGet();

while (1)
{
    AcquireAndProcessSample();

    RK_ERR err = kSleepUntil(&anchor,
                             RK_MS_TO_TICKS(10U));
    if (err == RK_ERR_ELAPSED_PERIOD)
    {
        RecordLateActivation();
    }
}

Each call advances the anchor by one period. If that deadline has already elapsed, the call returns immediately with RK_ERR_ELAPSED_PERIOD. It does not skip forward across multiple releases. This favours accounting for every activation over recovery to an absolute phase.

7.4. Sleep Release

kSleepRelease(), also named kSleepPeriodic(), uses the phase grid established at scheduler start.

A late task sleeps less to reach the next phase. If an entire release has been missed, RK0 records an overrun, skips the missed slot, and targets the next valid slot.

Use it for known startup tasks that should remain aligned to a common system phase. A task spawned later at an arbitrary time should normally use kSleepUntil() with a local anchor because it did not participate in the scheduler-start phase.

7.5. Application timer callouts

An Application Timer defers its callback from tick expiry to the PostProc system task.

sequenceDiagram
    participant Tick as SysTick
    participant Post as PostProc
    participant App as Application task
    Tick->>Post: timer expiry job
    Post->>Post: short callback
    Post->>App: event or semaphore
    App->>App: perform normal work
Loading

kTimerInit() arms a one-shot or reloading timer with an initial phase, a period, a callback, and an argument pointer. kTimerCancel() cancels an active timer. When dynamic objects are enabled, kTimerCreate() allocates and arms a timer, and kTimerDestroy() cancels and releases it.

Callouts execute in high-priority, non-preemptible system-task context. They must be short and non-blocking. A normal pattern is to set a task event or post a semaphore, then let an application task do the work.


8. Signals and waiting

Signals express occurrence or availability. They do not carry an application payload.

8.1. Task Event Register

Every task has a private 32-bit Event Register. Other tasks or an ISR set bits; only the receiving task waits for and consumes them.

kEventSet(task, mask) ORs the mask into the register. Setting the same bit repeatedly before consumption still leaves one bit set. If every occurrence matters, use a counting Semaphore or Message Queue.

The receiver selects RK_EVENT_ANY or RK_EVENT_ALL.

#define EVT_SAMPLE   RK_EVENT_1
#define EVT_SETPOINT RK_EVENT_2

RK_TASK_EVENT got = 0UL;
RK_ERR err = kEventGet(EVT_SAMPLE | EVT_SETPOINT,
                       RK_EVENT_ALL,
                       &got,
                       RK_WAIT_FOREVER);
K_ASSERT(err == RK_ERR_SUCCESS);

On success, RK0 may copy the complete register state to got, then clears the requested mask. Bits outside that mask remain pending.

Use Task Events when:

  • the receiver is known;

  • no payload is required;

  • bitwise ANY or ALL composition is useful;

  • repeated occurrences may coalesce.

kEventQuery() observes the register. kEventClear() explicitly clears bits. Passing NULL as the task handle selects the calling task; an ISR must name a target task explicitly.

8.2. Semaphores

A Semaphore stores an unsigned count of available units or pending occurrences. It has no owner.

static RK_SEMAPHORE samplesReady;

VOID kApplicationInit(VOID)
{
    RK_ERR err = kSemaphoreInit(&samplesReady,
                                0U, /* initially no sample */
                                8U  /* retain up to 8 */);
    K_ASSERT(err == RK_ERR_SUCCESS);
}

kSemaphorePend() consumes a stored count. If the count is zero, it returns immediately for RK_NO_WAIT or places the task in the priority-ordered wait queue.

kSemaphorePost() first satisfies the most urgent waiting task. With no waiter, it increments the count up to the configured maximum. Posting at the maximum leaves the count unchanged and returns RK_ERR_SEMA_FULL.

Convenience initialisers are available:

kSemaBinInit(&doorbell, 0U);  /* maximum 1 */
kSemaCountInit(&credits, 12U); /* maximum sizeof(UINT) */

A binary Semaphore can guard a simple region only by application convention. It is not a Mutex Lock so it has no ownershio: any task may post it, it has no owner, and priority inversion is not handled.

The dynamic form uses RK_SEMAPHORE_HANDLE with kSemaphoreCreate() and kSemaphoreDestroy(). Destruction requires no waiters; a stored count is discarded.

8.3. Sleep Queues

A Sleep Queue is a pure, non-latching waiting relation. It stores waiting tasks, not a condition and not an occurrence token.

flowchart LR
    A["Signal before wait"] --> B["Semaphore retains token"]
    C["Signal before wait"] --> D["Sleep Queue retains nothing"]
    E["Future waiter"] --> B
    E --> D
Loading
  • kSleepQueueSleep() waits for a future signal.

  • kSleepQueueSignal() wakes the highest-priority waiter.

  • kSleepQueueWake(q, n, left) wakes up to n waiters; zero means all.

  • kSleepQueueReady() wakes a named task from that queue.

  • kSleepQueueUnready() moves a non-running ready task into the queue.

A wake sent while nobody is waiting is not remembered.


9. Shared-state coordination

9.1. Mutex Locks

A Mutex expresses exclusive ownership of shared state. Once locked, only its owner may unlock it. RK0 Mutexes are non-recursive: attempting to lock the same Mutex twice is invalid.

static RK_MUTEX stateLock;

VOID kApplicationInit(VOID)
{
    RK_ERR err = kMutexInit(&stateLock,
                            RK_PRIO_INHERITANCE);
    K_ASSERT(err == RK_ERR_SUCCESS);
}

VOID UpdateState(VOID)
{
    RK_ERR err = kMutexLock(&stateLock, RK_WAIT_FOREVER);
    K_ASSERT(err == RK_ERR_SUCCESS);

    sharedState.value++;

    err = kMutexUnlock(&stateLock);
    K_ASSERT(err == RK_ERR_SUCCESS);
}

The protocol is selected at initialisation:

  • RK_PRIO_NONE - mutual exclusion only;

  • RK_PRIO_INHERITANCE - fully transitive priority inheritance.

The dynamic form uses RK_MUTEX_HANDLE, kMutexCreate(), and kMutexDestroy(). Before destruction, the Mutex must be unlocked, unowned, and have no waiters.

9.1.1. Priority inheritance

When a high-priority task waits for a Mutex owned by a lower-priority task, the owner inherits the waiter’s effective priority. This prevents unrelated medium-priority work from extending the inversion.

flowchart TB
    H["High waits for M1"] --> L["Low owns M1"]
    L --> X["Low waits for M2"]
    X --> V["Very low owns M2"]
    H -. "effective priority propagates" .-> L
    L -. "transitive inheritance" .-> V
Loading

RK0 recomputes a task’s effective priority across all PI-enabled Mutexes it owns. If an owner itself waits on another Mutex, inherited priority propagates through the dependency chain.

Priority inheritance bounds interference; it does not make long critical sections inexpensive. Keep protected regions bounded and avoid unrelated blocking operations while owning a Mutex.

9.1.2. Mutex versus binary Semaphore

Property Mutex Binary Semaphore

Stored state

locked / unlocked

count 0 / 1

Owner

yes

no

Who may release

owner only

any task or permitted ISR

Priority inheritance

optional

no

Intended meaning

exclusive shared-state access

occurrence or availability

A binary Semaphore can provide mutual exclusion if the application follows a strict convention. The distinction is that the kernel cannot enforce ownership or derive priority inheritance from that convention.

9.2. Condition-variable helpers

A Condition Variable is a Sleep Queue paired with a PI-enabled Mutex.

static RK_MUTEX stateLock;
static RK_SLEEP_QUEUE condition;

VOID InitStateMonitor(VOID)
{
    RK_ERR err = kCondVarInit(&condition, &stateLock);
    K_ASSERT(err == RK_ERR_SUCCESS);
}

VOID WaitForState(VOID)
{
    RK_ERR err = kMutexLock(&stateLock, RK_WAIT_FOREVER);
    K_ASSERT(err == RK_ERR_SUCCESS);

    while (!PredicateIsTrue())
    {
        err = kCondVarWait(&condition,
                           &stateLock,
                           RK_WAIT_FOREVER);
        K_ASSERT(err == RK_ERR_SUCCESS);
    }

    /* Predicate is true and stateLock is held here. */
    UseProtectedState();

    err = kMutexUnlock(&stateLock);
    K_ASSERT(err == RK_ERR_SUCCESS);
}

The caller tests an application predicate while owning the Mutex. kCondVarWait() releases the Mutex and joins the Sleep Queue atomically. If the Mutex was released, the helper attempts to reacquire it before returning, including after timeout or no-wait. The timeout bounds the complete wait-plus-relock operation.

Always test the predicate in a loop. A wake means the protected condition may have changed; it is not proof that this particular task may proceed.

kCondVarSignal() wakes one waiter. kCondVarBroadcast() wakes all. The application decides which signalling discipline preserves its monitor invariant.


10. Buffered message passing

10.1. Message Queues

A Message Queue transfers fixed-size messages by copy and preserves their order up to the configured capacity. Queues and Mailboxes are plain buffered objects: they have no task owner and perform no receiver-priority adoption.

flowchart LR
    P["Producer"] -->|"send by copy"| Q["Bounded FIFO slots"]
    Q -->|"receive by copy"| C["Consumer"]
Loading

A successful send means the message was copied directly to an already-waiting receiver or admitted to queue storage. It does not mean the receiver processed the message.

10.1.1. Declare and initialise

typedef struct
{
    ULONG timestamp;
    UINT value;
} Sample_t;

#define SAMPLE_DEPTH 4U

RK_DECLARE_MESG_QUEUE(sampleQueue,
                      sampleQueueStorage,
                      Sample_t,
                      SAMPLE_DEPTH)

RK_ERR err = kMesgQueueInit(&sampleQueue,
                            sampleQueueStorage,
                            RK_MESGQ_MESG_SIZE(Sample_t),
                            SAMPLE_DEPTH);
K_ASSERT(err == RK_ERR_SUCCESS);

Queue slots are 1, 2, 4, or 8 ULONG words. For larger or variable payloads, use a fixed-block Memory Partition plus a one-word pointer queue, or use Asynchronous Direct Message when task-addressed ownership transfer is the required contract.

Capacity absorbs bursts and jitter; it does not create throughput. Over time, the producer must wait, the application must drop deliberately, or the consumer must sustain the arrival rate.

10.1.2. Send and receive

Sample_t sample = ReadSample();

RK_ERR err = kMesgQueueSend(&sampleQueue,
                            &sample,
                            RK_NO_WAIT);
if (err == RK_ERR_BUFFER_FULL)
{
    RecordDroppedSample();
}
  • kMesgQueueSend() appends a message.

  • kMesgQueueJam() inserts at the front.

  • A sender may wait on a full queue.

  • A receiver may wait on an empty queue.

  • Waiters are selected by effective priority, FIFO among equals.

  • kMesgQueuePeek() copies the front message without consuming it.

  • kMesgQueueQuery() reports buffered messages and waiters.

  • kMesgQueueReset() discards buffered messages and releases current waiters.

  • kMesgQueueDestroy() requires an empty queue with no waiting or broadcast receivers. There is no owner state.

10.1.3. Notify callback

When enabled, kMesgQueueInstallSendCbk() runs a short, non-blocking callback after a successful normal send or jam. The callback is notification, not delivery; the queue remains the authority for the message.

10.2. Mailboxes

RK_MBOX is an alias for a Message Queue of depth one. Ordinary post and pend retain queue semantics. kMesgQueuePostOvw() replaces the pending value. kMboxBroadcast() hands one copied value to tasks already blocked in kMboxBroadcastRecv().

Operation Admission condition Retained result

ordinary post

slot empty or receiver waiting

one queued value

overwrite

any Mailbox state

newest pending value

broadcast

broadcast receivers already waiting

copy delivered to targeted waiters

Mailbox broadcast is a simultaneous handoff to present waiters. It does not retain publication for future readers; use MRM for latest-state publication to many readers.

10.2.1. One-slot behaviours

Mailbox form Use when

Overwrite

Only the newest not-yet-consumed value matters to one receiver

Broadcast

One value must be copied to every task already blocked as a broadcast receiver

Overwrite is not MRM: normal Mailbox receivers compete for one slot. Broadcast is not history: with no waiting broadcast receiver it deposits nothing and returns RK_ERR_BUFFER_EMPTY.

10.3. Mail queue pattern

For variable or larger payloads, combine a Memory Partition with a one-word Message Queue of pointers:

  1. the sender allocates a block;

  2. the sender fills it and enqueues its pointer;

  3. the receiver dequeues and processes the block;

  4. the receiver returns it to the same partition.

This keeps kernel copy cost bounded to one word while making lifetime and ownership explicit. If a non-blocking queue send fails, the sender still owns the block and must free or retry it.

A pointer queue and an Asynchronous Direct Message are not identical. The pointer queue is an application-composed ownership protocol over a generic buffered object. Asynchronous Direct Message records message state, originating pool, sender identity, endpoint ownership, and optional pool-ceiling contributions as one kernel-visible contract.


11. Dynamic kernel objects

Dynamic creation changes an object’s storage and lifetime. It does not change the service’s completion, waiting, or priority semantics. RK0 uses fixed-capacity kernel-owned pools rather than a general heap.

stateDiagram-v2
    [*] --> PoolAvailable
    PoolAvailable --> Live: matching Create
    Live --> Quiescent: application stops users
    Quiescent --> PoolAvailable: matching Destroy
Loading

12. Task-to-task message passing

Direct messages name a task handle. Endpoint state lives in the receiver TCB rather than in a standalone queue object.

Contract Successful sender/caller means Receiver operation

Synchronous Message

receiver copied the offered payload

kSynchMesgRecv()

Invocation

server copied the request and replied

kSynchMesgAccept() / kSynchMesgReply()

Asynchronous Direct Message

receiver endpoint owns the RK_MESG

kMesgWait()

A task may initialise a Synchronous endpoint, which also supports Invocation, or an Asynchronous Direct Message endpoint, but not both. This gives each task one unambiguous direct-message receive policy.

Task roles no longer exist. Any ordinary task may act as receiver or server when its endpoint and application protocol require it.

12.1. Synchronous Message (rendezvous)

The sender offers one bounded payload and blocks until the named receiver copies it into receiver-owned storage. No application payload is deposited in kernel queue storage.

sequenceDiagram
    participant S as Sender
    participant E as Receiver endpoint
    participant R as Receiver
    S->>E: offer pointer and byte count
    S-->>S: WAITING
    R->>E: receive
    E->>R: copy payload
    E->>S: READY after copy
Loading

Success means the receiver copied the payload. It does not mean the receiver processed it or produced a reply. Multiple senders may wait and are selected by effective priority.

/* Startup: configure the receiver endpoint maximum. */
RK_ERR err = kSynchMesgInit(controllerHandle,
                            sizeof(Sample_t));
K_ASSERT(err == RK_ERR_SUCCESS);

/* Sender: source remains valid until this call returns. */
err = kSynchMesgSend(controllerHandle,
                     &sample,
                     sizeof(sample),
                     RK_WAIT_FOREVER);
K_ASSERT(err == RK_ERR_SUCCESS);

/* Receiver: destination owns the copied value. */
Sample_t received;
ULONG copiedBytes = 0UL;

err = kSynchMesgRecv(&received,
                     &copiedBytes,
                     RK_WAIT_FOREVER);
K_ASSERT(err == RK_ERR_SUCCESS);

Rules:

  • The endpoint maximum is non-zero and a multiple of RK_WORD_SIZE.

  • Each handoff supplies its actual byte count, no larger than the endpoint maximum.

  • If a sender times out, its source pointer is invalidated for the receiver. A later receive cannot consume stale data.

  • A high-priority blocked sender contributes priority to a lower-priority receiver until the waiting relation changes.

  • Send and receive reject a running task that owns a Mutex, preserving one coordination authority.

12.2. Invocation (extended rendezvous)

Invocation uses the same Synchronous endpoint but extends request copy with server acceptance, processing, and a reply copied back to the caller.

sequenceDiagram
    participant C as Caller
    participant S as Server
    C->>S: call with request and reply buffer
    C-->>C: WAITING
    S->>S: accept and copy request
    S->>S: process
    S->>C: reply copied to caller buffer
Loading

kSynchMesgSend() completes on receive copy. kSynchMesgCall(), also named kSynchMesgInvoke(), completes only when the server replies or the caller’s timeout expires.

Request_t req = {1U, value};
Reply_t reply = {RK_ERR_SUCCESS, 0UL};
ULONG replyBytes = 0UL;

RK_SYNCH_ATTR attr = {
    &req, sizeof(req),
    &reply, sizeof(reply),
    &replyBytes
};

RK_ERR err = kSynchMesgCall(serverHandle,
                            &attr,
                            RK_WAIT_FOREVER);
K_ASSERT(err == RK_ERR_SUCCESS);

Server side:

RK_SYNCH_CALL_DATA call = {0};
Request_t accepted;
ULONG reqBytes = 0UL;

RK_ERR err = kSynchMesgAccept(&call,
                              &accepted,
                              &reqBytes,
                              RK_WAIT_FOREVER);
K_ASSERT(err == RK_ERR_SUCCESS);

Reply_t result = {
    RK_ERR_SUCCESS,
    accepted.value + 1UL
};

err = kSynchMesgReply(&call,
                      &result,
                      sizeof(result));
K_ASSERT(err == RK_ERR_SUCCESS);

Rules:

  • The request pointer remains valid until accept.

  • The caller-owned reply buffer remains valid until the call returns.

  • A timeout before accept removes the pending caller.

  • After accept, a caller timeout abandons the reply, but the server must still close the active call.

  • The server receives the active caller’s priority contribution until reply or abandoned-call completion.

  • RK_NO_WAIT is invalid for kSynchMesgCall(). Use bounded ticks or RK_WAIT_FOREVER.

12.3. Asynchronous Direct Message

An RK_MESG is allocated from an application-provided fixed-block pool. kMesgSend() transfers ownership to a named task endpoint without waiting for the receiver to copy or process it.

flowchart LR
    P["Pool"] -->|"allocate"| S["Sender owns"]
    S -->|"send transfers ownership"| E["Endpoint owns"]
    E -->|"wait returns pointer"| R["Receiver owns"]
    R -->|"free"| P
Loading

A successful send means the receiver endpoint owns the message object. The sender must not touch it again. If a receiver is waiting, it becomes ready with the pointer; otherwise the message remains queued at the endpoint.

12.3.1. Declare and initialise a pool

typedef struct
{
    UINT source;
    UINT sequence;
    ULONG value;
} AsyncPayload_t;

#define ASYNC_POOL_DEPTH 4U
#define ASYNC_POOL_CEILING 3U

RK_DECLARE_MESG_POOL(asyncPool,
                     asyncPoolBuf,
                     AsyncPayload_t,
                     ASYNC_POOL_DEPTH)

RK_ERR err = kMesgPoolInit(&asyncPool,
                           asyncPoolBuf,
                           sizeof(AsyncPayload_t),
                           ASYNC_POOL_DEPTH,
                           ASYNC_POOL_CEILING);
K_ASSERT(err == RK_ERR_SUCCESS);

err = kMesgEndpointInit(receiverHandle);
K_ASSERT(err == RK_ERR_SUCCESS);

Each pool block consists of an RK_MESG header followed by the fixed-capacity application payload.

Pass RK_MESG_PRIO_CEILING_NONE as the fifth argument when the pool does not need a priority ceiling.

12.3.2. Pool priority ceiling

The bounded resource is the message pool. If all blocks are owned, an urgent task can wait in kMesgAlloc() while lower-priority tasks hold those blocks.

For a ceiling-enabled pool, any task owning at least one message from that pool runs no lower than ceilingPrio. The boost follows ownership:

  • allocation makes the allocator the owner;

  • a successful send transfers both message ownership and its pool-ceiling contribution to the receiving endpoint task;

  • kMesgWait() transfers queued ownership to the receiver’s active scope without changing the owning task;

  • kMesgFree() removes the ownership contribution and returns the block;

  • a failed send does not transfer ownership.

Choose the ceiling as the highest priority, numerically smallest priority value, of any task that may wait for that pool.

A timeout on kMesgAlloc() bounds how long the caller waits. The ceiling bounds the priority inversion caused by lower-priority owners of the scarce blocks. These solve different parts of the same resource-exhaustion case.

sequenceDiagram
    participant L as Lower-priority owner
    participant P as Message pool
    participant H as Urgent allocator
    L->>P: owns last block
    H->>P: alloc waits
    P->>L: apply pool ceiling
    L->>P: free or transfer block
    P->>H: direct allocation handoff
Loading

12.3.3. Allocate, fill, and send

RK_MESG *mesgPtr = NULL;

RK_ERR err = kMesgAlloc(&asyncPool,
                        &mesgPtr,
                        RK_WAIT_FOREVER);
K_ASSERT((err == RK_ERR_SUCCESS) && (mesgPtr != NULL));

AsyncPayload_t *payloadPtr =
    RK_MESG_PAYLOAD(mesgPtr, AsyncPayload_t);

payloadPtr->source = 1U;
payloadPtr->sequence = ++sequence;
payloadPtr->value = ReadValue();

err = kMesgSend(receiverHandle, mesgPtr);
if (err != RK_ERR_SUCCESS)
{
    /*
     * Ownership did not move. The sender still owns the block
     * and must retry by policy or return it to the pool.
     */
    RK_ERR freeErr = kMesgFree(mesgPtr);
    K_ASSERT(freeErr == RK_ERR_SUCCESS);
}
K_ASSERT(err == RK_ERR_SUCCESS);

/* On successful send, the sender no longer touches mesgPtr. */

kMesgAlloc() is where allocation policy appears:

  • RK_NO_WAIT returns RK_ERR_BUFFER_EMPTY if no block is free;

  • a finite timeout returns RK_ERR_TIMEOUT if no block becomes available;

  • RK_WAIT_FOREVER waits until a block is returned;

  • when a block is freed while tasks are waiting on that pool, RK0 hands it directly to one waiting allocator.

kMesgSend() has no timeout and does not wait for receiver endpoint queue space. It transfers a pointer; it does not copy application payload bytes.

12.3.4. Receive and free

RK_MESG *mesgPtr = NULL;

RK_ERR err = kMesgWait(RK_ANY_TASK,
                       &mesgPtr,
                       RK_WAIT_FOREVER);
K_ASSERT((err == RK_ERR_SUCCESS) && (mesgPtr != NULL));

AsyncPayload_t *payloadPtr =
    RK_MESG_PAYLOAD(mesgPtr, AsyncPayload_t);

Consume(payloadPtr->value);

err = kMesgFree(mesgPtr);
K_ASSERT(err == RK_ERR_SUCCESS);

kMesgWait(RK_ANY_TASK, ...) accepts the first eligible message from any sender. Supplying a specific task handle filters by sender without discarding other queued messages.

The receiver must eventually return every successfully received message to its originating pool with kMesgFree(), unless an application-level protocol transfers it again through a valid path.

Sender identity is available through kMesgGetSenderHandle() and kMesgGetSenderID(). Payload accessors include kMesgPayload(), kMesgPayloadConst(), and RK_MESG_PAYLOAD(message, Type).

RK_CONF_ASYNCH_MESG requires RK_CONF_MESG_QUEUE. Endpoint queues are task-backed; the message pool remains an application-owned Memory Partition.

12.4. Completion and ownership summary

Contract Sender-side lifetime Success means Retained history

Synchronous Message

sender retains source while blocked

receiver copied payload

none

Invocation

caller retains request and reply buffers

server replied

none

Asynchronous Direct Message

ownership moves to endpoint

endpoint owns RK_MESG

queued message objects

Use Synchronous Message when delivery copy is the completion boundary. Use Invocation when a server result is required. Use Asynchronous Direct Message when the sender must continue immediately and explicit message-object ownership is acceptable.

12.5. Endpoint and lifetime rules

  • Synchronous/Invocation and Asynchronous endpoints are mutually exclusive per receiver task.

  • Pool storage remains application-owned for its entire use.

  • An allocated async message remains owned by the allocator until a successful send.

  • A successful async send transfers the message and its pool-ceiling contribution.

  • A failed async send leaves ownership with the sender, which must retry or free.

  • Direct endpoints are task-context services.

  • Direct endpoint state appears in Trace list kipc output.


13. Latest-state publication

13.1. Most-Recent Message

MRM is a non-blocking 1:N protocol for publishing the latest complete state. New readers never walk through a backlog of obsolete samples.

flowchart LR
    W["One writer"] -->|"publish current version"| M["MRM"]
    M --> R1["Reader A"]
    M --> R2["Reader B"]
    M --> R3["Reader C"]
Loading

Use MRM when:

  • one writer publishes state;

  • several readers operate at independent rates;

  • the latest complete value matters more than every intermediate value;

  • readers must never observe a partially updated object.

MRM is publication, not notification. It does not wake readers.

13.1.1. Configure both pools

MRM uses two equal-length application-owned pools:

  1. RK_MRM_BUF control buffers, which retain reader counts and payload addresses;

  2. application payload buffers.

For one writer and R readers, configure R + 2 elements: one per communicating task plus one spare.

#define N_READERS  3U
#define N_MRM_BUFS (N_READERS + 2U)

static RK_MRM stateMrm;
static RK_MRM_BUF stateControlPool[N_MRM_BUFS];
static PlantState statePayloadPool[N_MRM_BUFS] K_ALIGN(4);

RK_ERR err = kMRMInit(&stateMrm,
                      stateControlPool,
                      statePayloadPool,
                      N_MRM_BUFS,
                      RK_TYPE_WORD_COUNT(PlantState));
K_ASSERT(err == RK_ERR_SUCCESS);

Use static zero-initialised storage for the RK_MRM_BUF pool.

kMRMCreate() allocates only the MRM control object. The control-buffer and payload arrays remain application-owned. Before kMRMDestroy(), ensure no reader retains a version and no writer reservation remains outstanding. An unreferenced current publication is reclaimed during destruction.

13.1.2. Writer: reserve and publish

RK_MRM_BUF *bufferPtr = kMRMReserve(&stateMrm);

if (bufferPtr != NULL)
{
    PlantState state = ReadPlantState();

    RK_ERR err = kMRMPublish(&stateMrm,
                             bufferPtr,
                             &state);
    K_ASSERT(err == RK_ERR_SUCCESS);
}

Reservation is private to the writer until publication. Publishing copies the payload and makes that buffer the only version visible to new readers.

kMRMReserve() returns NULL when no buffer is available. The writer must apply an explicit application policy: skip, retry later, or record an overrun.

13.1.3. Reader: get and unget

PlantState state;
RK_MRM_BUF *bufferPtr = kMRMGet(&stateMrm, &state);

if (bufferPtr != NULL)
{
    ProcessState(&state);

    RK_ERR err = kMRMUnget(&stateMrm, bufferPtr);
    K_ASSERT(err == RK_ERR_SUCCESS);
}

kMRMGet() copies the current payload and returns the handle of the version from which it was taken. Pair every successful get with exactly one unget of that same handle. The handle lets RK0 retain an old version while readers that acquired it are still active.

MRM never waits, wakes a task, or donates priority. kMRMGet() returns NULL before the first publication.


14. Deterministic memory

14.1. Memory Partitions

A Memory Partition allocates homogeneous fixed-size blocks from application-supplied storage.

flowchart LR
    P["Fixed block pool"] --> A["allocate one block"]
    A --> U["application owns block"]
    U -->|"free exactly once"| P
Loading

Allocation and free are immediate. They do not wait or affect task priority. There is no variable-size search, splitting, coalescing, or general heap-fragmentation model.

typedef struct
{
    RK_TICK timestamp;
    CHAR text[60];
} LogRecord;

#define LOG_BLOCKS 8U

static RK_MEM_PARTITION logMemory;
RK_DECLARE_MEM_POOL(LogRecord, logPool, LOG_BLOCKS)

VOID InitLogMemory(VOID)
{
    RK_ERR err = kMemPartitionInit(&logMemory,
                                   logPool,
                                   sizeof(LogRecord),
                                   LOG_BLOCKS);
    K_ASSERT(err == RK_ERR_SUCCESS);
}

Allocate and test for exhaustion:

LogRecord *recordPtr = kMemPartitionAlloc(&logMemory);

if (recordPtr != NULL)
{
    FillRecord(recordPtr);
}

Return the block exactly once to its original partition:

RK_ERR err = kMemPartitionFree(&logMemory, recordPtr);
K_ASSERT(err == RK_ERR_SUCCESS);

Block size is rounded up to a word boundary. The pool must be aligned, sized for the rounded block geometry, and contain at least one block. Allocation does not clear a reused block; initialise application fields before use.

14.2. Configure bounded pools

Enable runtime creation of non-task objects and set the maximum count required for each family.

#define RK_CONF_DYNAMIC_OBJECTS ON

#define RK_CONF_DYNAMIC_SEMAPHORES_MAX   4U
#define RK_CONF_DYNAMIC_MUTEXES_MAX      4U
#define RK_CONF_DYNAMIC_SLEEP_QUEUES_MAX 2U
#define RK_CONF_DYNAMIC_MESG_QUEUES_MAX  4U
#define RK_CONF_DYNAMIC_TIMERS_MAX       2U
#define RK_CONF_DYNAMIC_MRMS_MAX         1U

Each maximum reserves a kernel-owned Memory Partition for that control-object type. A maximum of zero leaves the family unavailable even when dynamic objects are enabled.

Exhausting a family returns RK_ERR_BUFFER_EMPTY. It does not fall back to a heap and does not wait for another object to be destroyed.

RK_CONF_DYNAMIC_OBJECTS defaults to ON independently of RK_CONF_DYNAMIC_TASK. The two facilities use separate pools and may be configured independently.

Initialise object partitions once before the first Create operation.

VOID kApplicationInit(VOID)
{
    RK_INIT_OBJ_PARTITIONS

    /* Create startup tasks and initialise application objects. */
}

The statement-style macro is a no-op when the feature is disabled. Startup code that must inspect initialisation failure may call kObjPartitionsInit() directly inside an RK_CONF_DYNAMIC_OBJECTS feature guard.

14.3. Static and dynamic forms

Property Static object Dynamic object

Control-block storage

declared by application

allocated from typed kernel pool

Start of lifetime

matching Init call

matching Create call

Reference

address of object

typed handle such as RK_MUTEX_HANDLE

End of lifetime

normally application lifetime

matching Destroy after quiescence

Service operations

ordinary API

same ordinary API, passing the handle

Create allocates the kernel control block, not every buffer referenced by it.

Dynamic family Storage remaining application-owned

Semaphore, Mutex, Sleep Queue

none

Message Queue or Mailbox

message buffer supplied to Create

Timer

callback argument and anything it references

MRM

RK_MRM_BUF array and payload pool supplied to kMRMCreate()

Application-owned supporting storage must remain valid until the corresponding Destroy succeeds.

Example with a dynamically allocated Mutex:

static RK_MUTEX_HANDLE stateLock = NULL;

RK_ERR err = kMutexCreate(&stateLock,
                          RK_PRIO_INHERITANCE);
K_ASSERT((err == RK_ERR_SUCCESS) && (stateLock != NULL));

err = kMutexLock(stateLock, RK_WAIT_FOREVER);
K_ASSERT(err == RK_ERR_SUCCESS);

UpdateSharedState();

err = kMutexUnlock(stateLock);
K_ASSERT(err == RK_ERR_SUCCESS);

err = kMutexDestroy(&stateLock);
K_ASSERT((err == RK_ERR_SUCCESS) && (stateLock == NULL));

Create and Destroy take the address of the typed handle variable. A failed Create leaves the output handle NULL. A successful Destroy clears the handle variable supplied to it and returns the control block to its family pool.

For a Message Queue, only the control block is dynamic:

#define WORK_DEPTH 4U

typedef struct
{
    ULONG command;
    ULONG argument;
} WorkItem;

static RK_MESG_QUEUE_HANDLE workQueue = NULL;

RK_DECLARE_MESG_QUEUE_BUF(workStorage,
                          WorkItem,
                          WORK_DEPTH)

RK_ERR err = kMesgQueueCreate(&workQueue,
                              workStorage,
                              RK_MESGQ_MESG_SIZE(WorkItem),
                              WORK_DEPTH);
K_ASSERT((err == RK_ERR_SUCCESS) && (workQueue != NULL));

/* Use workQueue through the ordinary Message Queue operations. */

/* First stop users and drain the queue. */
err = kMesgQueueDestroy(&workQueue);
K_ASSERT((err == RK_ERR_SUCCESS) && (workQueue == NULL));

workStorage is not released by Destroy. It is application storage and may be reused only after successful destruction.

14.4. Destruction requires quiescence

Destroy is not a close or broadcast-cancellation operation. The application must stop new users and allow current operations to finish.

Dynamic family Required state before Destroy

Semaphore

no waiting tasks; a stored count may be discarded

Mutex

unlocked, no owner, no waiting tasks

Sleep Queue

no waiting tasks

Message Queue / Mailbox

empty, no waiting sender, receiver, or active broadcast receiver

Timer

may be armed; Destroy cancels it before release

MRM

no outstanding reader reference and no reserved unpublished buffer

Rules common to every dynamic family:

  • Create and Destroy are task-context operations.

  • A Destroy function accepts only an object produced by the matching Create function. Never pass a static object’s address.

  • The kernel clears only the handle variable supplied to Destroy.

  • Copied handles and raw pointer aliases are not tracked; they become stale after successful destruction.

  • Quiescence is an application protocol. Prevent another task from beginning an operation during destruction.

  • Give one component authority over the owning handle and the Create/Destroy operations.

  • Application-owned supporting storage must outlive the control object.

  • Do not destroy a Timer from its own callback. PostProc still uses Timer state after the callback returns.

Trace registers a created object normally and unregisters it after successful destruction.


15. Dynamic tasks

Dynamic tasks support middleware that must create and terminate tasks after scheduling has started. The feature is disabled by default and must be enabled deliberately.

#define RK_CONF_DYNAMIC_TASK ON

Each dynamic task consumes:

  • one TCB slot from the fixed kernel task pool;

  • one stack block from an application-selected Memory Partition.

flowchart LR
    T["TCB pool"] --> S["Spawned task"]
    P["Application stack partition"] --> S
    S -->|"terminate"| C["PostProc cleanup"]
    C --> T
    C --> P
Loading

Declare the task handle, entry, stack storage, and attributes.

#define DYNAMIC_STACK_WORDS 256U
#define DYNAMIC_TASKS       2U

RK_DECLARE_DYNAMIC_TASK(workerHandle, WorkerTask)

RK_DECLARE_DYNAMIC_STACK_POOL(workerStackMemory,
                              workerStackPool,
                              DYNAMIC_TASKS,
                              DYNAMIC_STACK_WORDS)

static RK_DYNAMIC_TASK_ATTR workerAttributes = {
    .taskFunc = WorkerTask,
    .argsPtr = RK_NO_ARGS,
    .taskName = "DynWork",
    .priority = 3U,
    .preempt = RK_PREEMPT,
    .stackMemPtr = &workerStackMemory
};

Initialise the stack partition, then spawn.

RK_ERR err = kMemPartitionInit(&workerStackMemory,
                               workerStackPool,
                               sizeof(workerStackPool[0]),
                               DYNAMIC_TASKS);
K_ASSERT(err == RK_ERR_SUCCESS);

err = kTaskSpawn(&workerAttributes, &workerHandle);
K_ASSERT(err == RK_ERR_SUCCESS);

A successfully spawned task becomes ready immediately and may preempt its creator.

Only runtime-spawned tasks may be terminated. kTaskTerminate(&handle) terminates another eligible dynamic task and clears that handle variable. kTaskTerminateSelf() defers cleanup to PostProc because a running task cannot reclaim its own active stack.

Termination is rejected while the task owns or waits for a resource, remains in an unsupported blocking state, or has dependants through Synchronous/Invocation or Asynchronous Direct Message endpoint state.

The kernel clears the handle variable supplied to termination. Copied aliases remain the application’s responsibility.

There are no dynamic-object APIs for Memory Partitions or Task Event Registers. Synchronous/Invocation and Asynchronous Direct Message endpoints remain task-backed. Direct-message pools remain application-owned Memory Partitions.

Real-time rule: runtime creation changes the active task set. Bound when and how many tasks may exist. A spawned periodic task should normally use kSleepUntil() with a local anchor because it did not participate in the scheduler-start phase used by kSleepRelease().


16. Trace and fault handling

16.1. Trace console

When RK_CONF_TRACE is enabled, kTraceInit() starts an interactive UART-backed Trace task. The board supplies a non-blocking receive hook and signals input from its UART ISR.

Name initialised objects so console output is readable.

RK_ERR err = kTraceNameObject(&sampleQueue, "SampleQ");
K_ASSERT(err == RK_ERR_SUCCESS);

err = kTraceNameObject(&stateLock, "StateLk");
K_ASSERT(err == RK_ERR_SUCCESS);

err = kTraceInit();
K_ASSERT(err == RK_ERR_SUCCESS);

Names are limited by RK_NAME_SIZE. With the default size, use at most seven visible characters plus the terminating NUL.

Command Use

top

Task state, effective and nominal priority, dispatches, CPU accounting, and stack watermark

list kobjects

Registered objects and last operation

list kmesg

Queue depth and waiting senders or receivers

list kipc

Task-backed Synchronous/Invocation and Asynchronous Direct Message state

list ksema

Semaphore and Mutex state

list kmem

Partition block size and free count

list ktimers

Application Timer state

hist <name>

Recent operations for one object

hist task/<name>

Effective-priority changes for one task

Trace CPU percentage is diagnostic accounting, not a cycle-accurate profiler. Use object history together with task and object snapshots to understand causality.

16.2. Error Handling

In checked builds, invalid API use can call kErrHandler() in addition to returning an error. Configure it for the application’s fault policy: halt, capture evidence, reset, or transfer control to a safety mechanism.

Treat these as programming errors rather than routine branch conditions:

  • using an uninitialised or wrong object;

  • using a stale handle after dynamic-object destruction;

  • destroying an object while another task can still use it;

  • blocking from an ISR;

  • recursively locking a Mutex;

  • unlocking a Mutex without ownership;

  • using Synchronous/Invocation operations while owning a Mutex;

  • touching or freeing an Asynchronous Direct Message after successful send;

  • failing to free an async message after a send error or after receiving it;

  • freeing a block to the wrong partition;

  • letting a task return from its entry function.

Exhausted bounded resources are valid system states only when the application has an explicit policy for them. Stack overflow and resource exhaustion should produce evidence useful after reset. Trace stack watermarks, object history, and application-retained fault records are complementary.


17. Choosing a communication service

The services differ mainly in what they retain, where completion occurs, and which priority dependency the kernel can observe.

17.1. Completion and retention matrix

Need Choose Success means Retention

Count occurrences or units

Semaphore

one token was consumed or posted

bounded count

Private bitwise occurrences

Task Event Register

requested bits were present and consumed

coalesced bits

Wait for a future notification

Sleep Queue

the task was signalled after waiting began

no latch

Guard shared state

Mutex

caller owns the lock

ownership until unlock

Wait on shared-state predicate

Condition Variable

wake completed and Mutex was reacquired

predicate remains application state

Preserve bounded message history

Message Queue

message copied to receiver or queue

bounded FIFO

Keep one pending newest value

overwrite Mailbox

one-slot value replaced or delivered

newest slot

Deliver to present waiters

broadcast Mailbox

waiting receivers obtained copies

none for future readers

Transfer a pool-backed message

Asynchronous Direct Message

endpoint owns RK_MESG

queued message objects

Deliver directly and wait for copy

Synchronous Message

receiver copied payload

none

Invoke a server and receive result

Invocation

server replied

none

Publish latest state to many readers

MRM

complete value became current

latest safe version(s)

17.2. Selection questions

Ask these questions in order:

  1. Is this an occurrence, shared-state ownership relation, or message?

  2. If it is a message, should history be preserved, replaced, or never created?

  3. Does the sender complete on queue admission, receiver copy, ownership transfer, or server reply?

  4. Is there one owner, one receiver, one server, or many readers?

  5. Which task or object owns the data at every step?

  6. Which priority dependency appears when a more urgent task waits?

  7. What happens on timeout, cancellation, queue saturation, or pool exhaustion?

Do not emulate a stronger contract with a weaker service unless the application explicitly accepts the changed completion, timeout, ownership, and scheduling semantics.

17.3. Common distinctions

  • A Signal is one-way. Signalling never waits for a reply.

  • A successful queue send means admission or direct copy to a waiting receiver; it does not mean processing completed.

  • A successful Synchronous Message means the receiver copied the payload; it does not imply a reply.

  • Invocation includes a reply and therefore has a later completion boundary.

  • A successful Asynchronous Direct Message send transfers ownership; it does not wait for receiver execution.

  • MRM provides safe latest-state access. It does not notify or wake consumers.

  • A Mutex makes ownership and priority inheritance visible to the kernel. A binary Semaphore relies on application convention.

  • A Sleep Queue retains waiting tasks only. A Semaphore retains a token when no task is waiting.


18. Application helper macros

kapi.h exposes compile-time helpers for aligned storage, sizing, task access, and time conversion. Declaration macros reserve application storage. Init, Create, or Spawn establishes the object or task.

18.1. Size calculations

Macro Result Principal use

RK_WORD_SIZE

bytes in one ULONG

common sizing unit

RK_TYPE_WORD_COUNT(Type)

sizeof(Type) rounded to words

Memory Partition blocks

RK_TYPE_SIZE_POW2_WORDS(Type)

1, 2, 4, 8, or 16 words

lower-level slot sizing

RK_MESGQ_MESG_SIZE(Type)

Queue slot width in words

Queue or Mailbox init

RK_MESGQ_BUF_SIZE(Type, N)

total Queue backing words

explicit Queue arrays

RK_MESG_BLOCK_SIZE_BYTES(Type)

RK_MESG header plus word-aligned payload

direct-message block

RK_MESG_POOL_WORDS(Type, N)

total direct-message pool words

direct-message arrays

Message Queue payloads are limited to 1, 2, 4, or 8 words. A larger type may round to 16 words, but queue initialisation rejects it.

Direct-message pool helpers include the RK_MESG header and permit an application-defined payload type.

RK_DECLARE_MEM_POOL(LogRecord, logPool, LOG_BLOCKS)

kMemPartitionInit(&logMemory,
                  logPool,
                  sizeof(LogRecord),
                  LOG_BLOCKS);

RK_DECLARE_MESG_POOL(asyncPool,
                     asyncBuf,
                     AsyncPayload_t,
                     4U)

kMesgPoolInit(&asyncPool,
              asyncBuf,
              sizeof(AsyncPayload_t),
              4U,
              RK_MESG_PRIO_CEILING_NONE);

RK_TYPE_WORD_COUNT() returns words; sizeof() returns bytes. kMesgPoolInit() receives payload bytes and a priority ceiling, while RK_MESG_POOL_WORDS() sizes the complete backing array.

18.2. Declaration and initialisation helpers

Macro Declares or performs Follow with

RK_DECLARE_TASK

entry prototype, aligned static stack, handle

kTaskInit() / kCreateTask()

RK_DECLARE_DYNAMIC_TASK

entry prototype and handle

kTaskSpawn()

RK_DECLARE_DYNAMIC_STACK_POOL

stack Memory Partition and blocks

kMemPartitionInit()

RK_DECLARE_MESG_QUEUE_BUF / RK_DECLARE_MESG_QUEUE

Queue storage; optionally control block

kMesgQueueInit()

RK_DECLARE_MBOX_BUF / RK_DECLARE_MBOX

one-slot Queue storage; optionally Mailbox

kMboxInit()

RK_DECLARE_MESG_POOL_BUF / RK_DECLARE_MESG_POOL

direct-message blocks; optionally partition

kMesgPoolInit()

RK_DECLARE_MEM_POOL

word-aligned fixed blocks

kMemPartitionInit()

RK_INIT_OBJ_PARTITIONS

bounded dynamic control-block pools

once before Create calls

Queue and Mailbox helpers encode payload type and capacity only into the C array size. The kernel does not retain that C type. Pass matching size and capacity to initialisation.

typedef struct
{
    UINT command;
    ULONG value;
} Request_t;

RK_DECLARE_MESG_QUEUE(requestQueue,
                      requestStorage,
                      Request_t,
                      4U)

kMesgQueueInit(&requestQueue,
               requestStorage,
               RK_MESGQ_MESG_SIZE(Request_t),
               4U);

RK_DECLARE_MESG_POOL(requestPool,
                     requestPoolStorage,
                     Request_t,
                     4U)

kMesgPoolInit(&requestPool,
              requestPoolStorage,
              sizeof(Request_t),
              4U,
              2U);

Use a Queue when RK0 should copy values into bounded slots. Use a direct-message pool when a task-addressed endpoint should take ownership of an allocated message object.

All declaration macros reserve application storage. Its lifetime must cover every task and kernel path that can reference it.

18.3. Time and task lookup helpers

Macro Meaning Important constraint

RK_MS_TO_TICKS(ms)

milliseconds divided by tick interval

sub-tick becomes 1

RK_TICKS_TO_MS(ticks)

ticks multiplied by tick interval

does not saturate on overflow

RK_RUNNING_TID

ID of running task

use after scheduling starts

RK_RUNNING_PRIO

current effective priority

may include inheritance or ceiling

RK_RUNNING_NOM_PRIO

assigned nominal priority

excludes temporary change

RK_RUNNING_HANDLE

running task handle

task context after scheduler start

RK_RUNNING_NAME

pointer to running task name

task owns the string

RK_TASK_TID(handle)

ID of selected task

handle must be valid

RK_TASK_PRIO(handle)

effective priority of selected task

may reflect inheritance or boost

RK_TASKNAME_PTR(handle)

pointer to selected task name

directly dereferences handle


19. Compact API index

This index is for orientation. See core/inc/kapi.h for exact parameters, conditional compilation, and return values.

Family Principal operations

Kernel and tasks

kInit, kTaskInit / kCreateTask, kYield, task accessors

Dynamic objects and tasks

RK_INIT_OBJ_PARTITIONS, kObjPartitionsInit, kTaskSpawn, kTaskTerminate

Scheduler and time

kSchLock / kSchUnlock, kSleepDelay, kSleepRelease, kSleepUntil, kTickGet

Application Timer

kTimerInit / kTimerCreate, kTimerCancel, kTimerDestroy

Task Event Register

kEventSet, kEventGet, kEventQuery, kEventClear

Semaphore

kSemaphoreInit / Create, Pend, Post, Query, Destroy

Sleep Queue

kSleepQueueInit / Create, Sleep, Signal, Wake, Ready, Unready, Query, Destroy

Mutex

kMutexInit / Create, Lock, Unlock, Query, Destroy

Condition Variable

kCondVarInit, kCondVarWait, kCondVarSignal, kCondVarBroadcast

Message Queue

kMesgQueueInit / Create, Send, Recv, Jam, Peek, Reset, Query, Destroy

Mailbox

kMboxInit / Create, Post, Pend, overwrite, broadcast, Destroy

Synchronous Message

kSynchMesgInit, kSynchMesgSend / kSynchSendWait, kSynchMesgRecv / kSyncRecv

Invocation

kSynchMesgCall / kSynchMesgInvoke, kSynchMesgAccept, kSynchMesgReply

Asynchronous Direct Message

kMesgEndpointInit, kMesgPoolInit, kMesgAlloc, kMesgSend, kMesgWait, kMesgFree

MRM

kMRMInit / Create, kMRMReserve, kMRMPublish, kMRMGet, kMRMUnget, Destroy

Memory Partition

kMemPartitionInit, kMemPartitionAlloc, kMemPartitionFree

Trace

kTraceInit, kTraceNameObject, snapshot and history APIs


Copyright © 2026 Antonio Giacomelli. RK0 source code is distributed under the licence stated by the repository.

Clone this wiki locally