-
Notifications
You must be signed in to change notification settings - Fork 2
User Manual
- 1. About this manual
- 2. Run RK0 on QEMU
-
3. Build-time configuration (
kconfig.h) - 4. A minimal RK0 application
- 5. Tasks and execution progress
- 6. Waiting, timeouts, and execution context
- 7. Time services
- 8. Signals and waiting
- 9. Shared-state coordination
- 10. Buffered message passing
- 11. Dynamic kernel objects
- 12. Task-to-task message passing
- 13. Latest-state publication
- 14. Deterministic memory
- 15. Dynamic tasks
- 16. Trace and fault handling
- 17. Choosing a communication service
- 18. Application helper macros
- 19. Compact API index
Kernel Version: V0.73.0-dev
Given updates, some information found here can be contradictory. In such cases:
-
The macro
RK_MS_TO_TICKS(time)never returns 0. Iftimehappens to be less than 1 tick (e.g TICK is 10ms,RK_MS_TO_TICKS(5) = 1 tick). -
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.
-
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.
-
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_NONEto initialise a pool with no ceiling protocol. -
There is no difference between Task ID/TID/PID.
-
Any type followed by
_HANDLEis an address. Dynamic creation/destruction should use these types.
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?
Priority 0 is the highest user priority. A smaller number means a more urgent task.
| State | Operational meaning |
|---|---|
|
The task object exists but is not eligible to execute |
|
The task may be selected by the scheduler |
|
The task owns the processor |
|
The task cannot execute until a time or coordination condition is satisfied |
|
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 |
|---|---|---|
|
Operation completed |
|
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.
Core Services are always enabled. Optional Services are turned ON/OFF in kconfig.h.
-
Task Delays (Busy/Sleep Until/Sleep Release)
-
Task Event Register
-
Partition Memory
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 QueuesandMutex Locks, providing helper functions. So it needs both Sleep Queues and Mutex Locks enabled.
RK0 builds with the GNU Arm Embedded toolchain. The repository demonstration runs on QEMU.
git clone https://github.com/antoniogiacomelli/RK0.git
cd RK0
make ARCH=armv7m qemuUse 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.
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.
RK_CONF_SYSCORECLK is the processor clock in hertz. A non-zero value is used
directly.
|
Note
|
|
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 |
|---|---|---|
|
1000 Hz |
1 ms |
|
500 Hz |
2 ms |
|
250 Hz |
4 ms |
|
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_DIVis greater than zero and no greater than 1000 for the integer-millisecond model. -
1000ULshould divide evenly byRK_CONF_SYSTICK_DIV; otherwise the interval used by millisecond helpers is truncated. -
The core clock should divide evenly by
RK_CONF_SYSTICK_DIVwhen an exact hardware frequency is required. -
RK_CONF_SYSCORECLK / RK_CONF_SYSTICK_DIVis at least 1 and at most0x1000000because 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.
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 */
RK_TICK b = RK_MS_TO_TICKS(5U); /* 0 ticks -> 1 tick = 10ms */For timeout-bearing calls, a positive millisecond value shorter than one tick becomes 1 tick.
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.
| Symbol | Unit | Effect |
|---|---|---|
|
tasks |
Sizes the shared TCB pool for startup and runtime-spawned user tasks. Optional facilities created with |
|
32-bit words |
Sizes the Idle task stack, including the application Idle hook. |
|
32-bit words |
Sizes the PostProc stack used by timer callbacks and deferred kernel work. |
|
|
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.
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 |
|
Mutex |
|
Sleep Queue |
|
Message Queue / Mailbox |
|
Application Timer |
|
Most-Recent Message |
|
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.
| Area | Configuration symbols |
|---|---|
Time |
|
Shared state |
|
Buffered messages |
|
Direct task messages |
|
Latest-state publication |
|
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.
RK_CONF_TRACE includes the interactive Trace task and its records.
| Symbol | Controls |
|---|---|
|
Trace task stack in 32-bit words |
|
Trace task priority |
|
Number of registered objects |
|
Interactive command-line buffer |
|
Per-object history depth |
|
Records retained after normal history overflow |
|
Buffered machine-readable frames |
|
Immediate frame output instead of buffering |
|
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.
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.
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.
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
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.
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
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.
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.
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.
Services that may wait accept one of three timeout forms.
| Timeout | Caller behaviour |
|---|---|
|
Inspect or attempt the operation and return immediately |
finite ticks |
Wait up to the requested kernel time |
|
Wait until the service condition is satisfied |
Finite values must not exceed RK_MAX_PERIOD. Because
RK_MS_TO_TICKS(time), time is a duration shorter than 1 tick it truncated to 1 tick.
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.
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.
RK0 keeps different meanings of time in separate calls.
| Service | Meaning | Best use |
|---|---|---|
|
Consume |
Simulated or deliberately active workload |
|
Sleep relative to this call |
One-off delay or back-off |
|
Advance a task-local deadline by |
Periodic work that must account for every activation |
|
Target the next scheduler-start phase slot |
Phase-aligned periodic work that may skip missed releases |
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.
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.
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.
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.
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
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.
Signals express occurrence or availability. They do not carry an application payload.
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.
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.
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
-
kSleepQueueSleep()waits for a future signal. -
kSleepQueueSignal()wakes the highest-priority waiter. -
kSleepQueueWake(q, n, left)wakes up tonwaiters; 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.
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.
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
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.
| 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.
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.
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"]
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.
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.
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.
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.
| 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.
For variable or larger payloads, combine a Memory Partition with a one-word Message Queue of pointers:
-
the sender allocates a block;
-
the sender fills it and enqueues its pointer;
-
the receiver dequeues and processes the block;
-
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.
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
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 |
|
Invocation |
server copied the request and replied |
|
Asynchronous Direct Message |
receiver endpoint owns the |
|
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.
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
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.
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
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_WAITis invalid forkSynchMesgCall(). Use bounded ticks orRK_WAIT_FOREVER.
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
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.
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.
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
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_WAITreturnsRK_ERR_BUFFER_EMPTYif no block is free; -
a finite timeout returns
RK_ERR_TIMEOUTif no block becomes available; -
RK_WAIT_FOREVERwaits 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.
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.
| 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 |
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.
-
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 kipcoutput.
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"]
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.
MRM uses two equal-length application-owned pools:
-
RK_MRM_BUFcontrol buffers, which retain reader counts and payload addresses; -
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.
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.
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.
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
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.
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 1UEach 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.
| Property | Static object | Dynamic object |
|---|---|---|
Control-block storage |
declared by application |
allocated from typed kernel pool |
Start of lifetime |
matching |
matching |
Reference |
address of object |
typed handle such as |
End of lifetime |
normally application lifetime |
matching |
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 |
Timer |
callback argument and anything it references |
MRM |
|
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.
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.
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 ONEach 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
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().
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 |
|---|---|
|
Task state, effective and nominal priority, dispatches, CPU accounting, and stack watermark |
|
Registered objects and last operation |
|
Queue depth and waiting senders or receivers |
|
Task-backed Synchronous/Invocation and Asynchronous Direct Message state |
|
Semaphore and Mutex state |
|
Partition block size and free count |
|
Application Timer state |
|
Recent operations for one object |
|
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.
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.
The services differ mainly in what they retain, where completion occurs, and which priority dependency the kernel can observe.
| 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 |
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) |
Ask these questions in order:
-
Is this an occurrence, shared-state ownership relation, or message?
-
If it is a message, should history be preserved, replaced, or never created?
-
Does the sender complete on queue admission, receiver copy, ownership transfer, or server reply?
-
Is there one owner, one receiver, one server, or many readers?
-
Which task or object owns the data at every step?
-
Which priority dependency appears when a more urgent task waits?
-
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.
-
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.
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.
| Macro | Result | Principal use |
|---|---|---|
|
bytes in one |
common sizing unit |
|
|
Memory Partition blocks |
|
1, 2, 4, 8, or 16 words |
lower-level slot sizing |
|
Queue slot width in words |
Queue or Mailbox init |
|
total Queue backing words |
explicit Queue arrays |
|
|
direct-message block |
|
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.
| Macro | Declares or performs | Follow with |
|---|---|---|
|
entry prototype, aligned static stack, handle |
|
|
entry prototype and handle |
|
|
stack Memory Partition and blocks |
|
|
Queue storage; optionally control block |
|
|
one-slot Queue storage; optionally Mailbox |
|
|
direct-message blocks; optionally partition |
|
|
word-aligned fixed blocks |
|
|
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.
| Macro | Meaning | Important constraint |
|---|---|---|
|
milliseconds divided by tick interval |
sub-tick becomes 1 |
|
ticks multiplied by tick interval |
does not saturate on overflow |
|
ID of running task |
use after scheduling starts |
|
current effective priority |
may include inheritance or ceiling |
|
assigned nominal priority |
excludes temporary change |
|
running task handle |
task context after scheduler start |
|
pointer to running task name |
task owns the string |
|
ID of selected task |
handle must be valid |
|
effective priority of selected task |
may reflect inheritance or boost |
|
pointer to selected task name |
directly dereferences handle |
This index is for orientation. See
core/inc/kapi.h
for exact parameters, conditional compilation, and return values.
| Family | Principal operations |
|---|---|
Kernel and tasks |
|
Dynamic objects and tasks |
|
Scheduler and time |
|
Application Timer |
|
Task Event Register |
|
Semaphore |
|
Sleep Queue |
|
Mutex |
|
Condition Variable |
|
Message Queue |
|
Mailbox |
|
Synchronous Message |
|
Invocation |
|
Asynchronous Direct Message |
|
MRM |
|
Memory Partition |
|
Trace |
|
Copyright © 2026 Antonio Giacomelli. RK0 source code is distributed under the licence stated by the repository.
Copyright (C) 2025 Antonio Giacomelli | www.kernel0.org