RK0 with one more boundary made explicit.
When I wrote my first preemptive scheduler, the entire system amounted to context switching under a time slice. Literally the next day, I started trying to split user space from kernel space. After all, it was a kernel, not a library. Which respectable kernel could exist without that distinction?
Plenty, of course. RTEMS says hello.
I was not born knowing things, which remains an irritating discovery.
That first attempt produced a kludge around system calls. Useless complexity. Design target: vanity.
RK01 is not a rebel RK0
RK01 is not RK0 corrected, nor is it simply the next release in the RK0 sequence. They answer different requirements.

PRIVDEFENA (an MPU configuration) leaves the default privileged map available.For unprivileged code, access policy is not inferred from location. It comes from BOOT-approved membership and the MPU entries installed at dispatch.
The linker map only supplies concrete addresses/sizes that must satisfy MPU geometry
RK0 treats the complete firmware as trusted. The application and kernel share one address space; kernel objects can remain transparent, and a service call is an ordinary C call. That is not a deficiency in real-time system software aiming for firm/hard real-time responsiveness. RK0’s low overhead, ease of analysis and excellent real-time response come from this high-effort simplicity applied to the architecture.1
RK01 shifts to meet another feature target, trading analysability and real-time responsiveness for fault confinement. The trusted unit can no longer be the whole application; it is a bounded unprivileged domain. The final image now includes the boundary between kernel space and at least one user domain, which consists of one or more threads. User domains separate unprivileged tasks. They are confined.
The answer required more than adding a memory firewall (MPU). The protection boundary changed: object model and lifecycle, ownership, inter-task communication, fault cleanup, the linker layout, the service call path and context switching. The scheduler is still recognisably RK0: statically bounded, fixed-priority and preemptive.
That is the meaning of RK01: Bounded Responses. Bounded domains.
Objects became a different beast
In RK0, an object handle can naturally be a pointer to the object. Allocating an object may be no more mysterious than declaring its structure. There is little value in hiding the representation when the caller already shares the kernel’s trusted address space.
That model cannot cross a user/kernel boundary. An unprivileged task must not be allowed to provide an arbitrary address and ask the kernel to interpret it as a mutex, queue or task control block.
RK01 therefore separates public identity from kernel representation. A task-facing handle (typedefed as RK_HANDLE) is an encoded token containing an object type, a registry slot, and a generation. It is not a kernel address and cannot be dereferenced by its holder. The kernel resolves it before use. It’s very Windows-like because the concept was borrowed from it.

The lifecycle is deliberately bounded:
- Creation reserves a slot from a kernel-owned pool whose maximum capacity was fixed at build time.
- The kernel assigns the object’s type, scope, owner and current generation.
- A live registry entry is published and an opaque handle is returned.
- Each operation resolves the handle again and checks its type, generation, state and scope.
- Destruction removes registry visibility before the pool slot can be reused.
- Reuse advances the generation, so a stale handle cannot silently name the next object placed in the same slot.
“Dynamic” does not mean an open-ended heap. It means run-time reservation from a statically bounded pool.
Scope is now part of the contract as well. A domain-local object can be used only from its owning domain. A kernel-global object is deliberately visible across domains. Merely possessing the numerical token does not grant authority: identity and scope are checked independently.
This gives task-fault cleanup something concrete to operate on. RK01 first invalidates the faulted task’s public identity, then removes it from ready, wait and timeout structures and cleans up the protocols in which it participated. Mutexes owned by the dead task are poisoned and their waiters receive RK_ERR_MUTEX_OWNER_FAULTED; ownership is not silently transferred across potentially inconsistent state. Message and MRM state also have explicit cleanup paths.
The kernel can retire a task consistently. It cannot guess whether the application state left behind still makes sense.
Memory is still decided before execution
An RK01 domain is a writable-authority region shared by one or more independently scheduled tasks. It is not a process, is not scheduled, and does not have its own executable image. RK01 still produces one firmware image in one final link, and all domains execute code from the shared flash image.
A project may be organised as domain_a.c, domain_a.h, a private header and several task source files. Those files compile into ordinary .o files. The source bundle is a composition convention; it is not a separately loadable module.

The build and BOOT sequence is:
- C declarations place writable objects into intentional sections: domain RAM, task stacks, global shared RAM or kernel RAM.
- Each source file is compiled normally. Its filename does not confer memory ownership.
- The final linker script collects those sections, aligns their windows and exports their boundaries.
- Startup copies initialised data, clears the appropriate BSS regions and prepares privileged kernel memory.
- Application BOOT code constructs domains and tasks, assigns every task to a domain and attaches any explicit shared regions.
- Before the first dispatch, RK01 validates MPU geometry, TASK_RAM placement, reservation overlap, stack placement and separation, object relationships and domain membership. It then freezes the memory-authority topology.
- Dispatch uses MPU register values prepared from that validated topology; it does not construct an address space dynamically in PendSV.
The linker is therefore a classifier of writable authority, not a linker of independent applications.
This distinction matters in C. A writable global declared casually inside record_server.c does not become Record-private because of the filename. Unless it is placed in Record’s declared RAM structure, it will land in some other writable section—normally the implicit App domain. The DOMAIN_IMPL_SRCS build audit exists to catch precisely that mistake.
An explicit domain’s typed RAM structure contains its persistent writable state and, where useful, handle cells. Ordinary member-task stacks are declared separately in the task-stack section. Each active task receives its own stack MPU window; sharing a domain does not mean sharing stacks.
A one-task domain is simply a domain containing one task. RK01 also provides the smaller isolated-task form in which that task’s private domain is exactly its declared stack storage. In neither case does the domain become a scheduler entity: the task is still what the scheduler dispatches.
Two kinds of protected context switch
Flash is installed as a common executable region. On a switch to a different task, RK01 replaces the private-stack MPU region. If the incoming task belongs to a different domain, RK01 also replaces the domain and shared-authority regions.
| Dispatch | MPU work |
|---|---|
| Same task selected again | No MPU rewrite. |
| Different task, same domain | Replace only the private-stack region. |
| Different domain | Replace the domain, attached shared-authority regions and private stack. |
This is why domains are more than labels. Tasks that genuinely share state and switch frequently can occupy one domain and avoid reloading the full authority map on every hand-off. Tasks separated for containment pay the inter-domain cost. Scheduling semantics do not change; only the memory view installed for the selected task changes.
Some constraints come directly from the Cortex-M MPU: a small number of regions, alignment rules and—in the ARMv7-M model—power-of-two geometry. Other restrictions are deliberate real-time policy. Domain geometry and shared attachments are frozen before scheduling because arbitrary run-time remapping and loading would add states, validation work and timing variability that RK01 does not need.
A system call is a protocol, not an instruction
RK01 retains a normal public C API, but the route depends on the caller. Privileged BOOT and handler code can enter an implementation directly. An ordinary unprivileged task enters through SVC.
The software interrupt is only the doorway. The actual boundary protocol is:
- The handler verifies the trap context and obtains the saved user exception frame.
- The dispatcher derives the caller from the currently running TCB; a task cannot claim another identity.
- Encoded handles are resolved and checked for type, generation, lifetime and scope.
- Input, output and function-pointer ranges are checked against the caller’s authorised memory view.
- Packed argument records are copied into kernel storage before nested pointers are interpreted.
- Only after those checks may privileged code operate on an object, caller memory or a hardware-facing service.
An immediate service writes its result into the stacked exception frame and returns.
A blocking service is more interesting. RK01 cannot leave an arbitrary privileged C call chain suspended on the main stack while unrelated tasks and system calls execute. Instead, it records the call number, original arguments, continuation phase, caller frame and wake result in the caller’s TCB. The task enters the appropriate wait structure, PendSV dispatches something else, and the public wrapper re-enters SVC when the task becomes runnable. The application still observes one blocking C function; internally, the operation is represented as bounded task state rather than a suspended privileged stack.
Long non-blocking kernel work can use the same restart idea at explicit checkpoints, allowing a pending higher-priority task to run without exposing privileged continuation state.
This is the privilege boundary fee.
Communication follows the memory boundary
RK01 does not force every interaction into message passing. Inside one domain, tasks can share ordinary domain state and coordinate it with mutexes, semaphores or the other RK0-style services. A pointer-bearing protocol is natural there because both tasks possess the same writable authority.
Across domains, the default changes:
- task events provide notifications without transferring a pointer;
- copied messages and synchronous send/receive move payloads across non-shared domains;
- synchronous call/reply adds a reply and priority-aware server execution;
- kernel-global queues or mailboxes can deliberately connect several domains;
- an explicit shared-memory region can be attached to selected domains when shared state is truly the intended contract.
By-reference messages require memory visible to both sides. Services whose contracts expose leases or pointers, such as MRM, therefore remain naturally domain-local unless the backing storage is deliberately shared.
The rule is not “messages good, shared memory bad.” The rule is that the communication mechanism must agree with the memory authority actually installed by the MPU.
The UART service: where client/server structure becomes natural
The UART Driver implementation in the repository is an example:

An unprivileged task cannot write the USART registers directly. It calls kConsoleWrite() with a buffer and length. The front end rejects a null buffer and requests beyond the configured bound—65 bytes in the default configuration—then constructs a small request/reply description on the caller’s stack and enters SVC.
The syscall path forwards the operation through RK01’s synchronous call/reply mechanism. The caller blocks. The privileged Console task accepts a copied request descriptor, validates the referenced buffer against the original caller’s memory authority, writes the bounded payload to the UART and replies. Because the caller remains blocked until the reply, its private stack and continuation remain stable throughout the loan.
A synchronous named call (extendend rendezvous) runs the server (callee) at the client’s priority (caller) until the operation completes, defined as when the client receives the answer.
On receive, the ISR performs only the bounded interrupt-side transfer: it moves available bytes into a fixed ring and signals the Console task. Foreground ownership, parsing and callbacks run later in task context.
The example is intentionally simple—transmit polls the UART-ready condition. A production driver may replace that with interrupt- or DMA-driven transmission. The architectural point remains: hardware ownership is centralised, clients use a checked protocol, interrupt work stays small, and the timing cost is visible.
Containment is not recovery
Suppose a sensor-processing task follows a corrupt pointer into another domain. The MPU raises MemManage. RK01 records the fault, marks that task FAULT_PENDING and dispatches away from it. PostProc later invalidates the task handle, removes outstanding waits and timeouts, cleans message state and poisons any mutexes the task owned. Waiters are released with an explicit owner-fault result rather than being handed state that may already be inconsistent.
That is containment. Recovery is an application concern.
One system may command an actuator to a safe value and switch to a redundant sensor domain. Another may reinitialise the peripheral and rebuild the affected service state. A third may conclude that shared application invariants cannot be reconstructed safely and reset the whole controller.
The MPU cannot choose among those policies. It limits the damage and gives the rest of the system an explicit failure to react to. Whether continued operation is safe remains an application decision.
A fault in privileged kernel or driver code is different: that code is already inside the trusted base and can access privileged memory. RK01 contains defective unprivileged tasks; it does not claim to shield one privileged component from another.
There is a kernel, and there is an operating system
Software around RK01 can be organised in several ways.

At one extreme, there is no separate OS layer. A specialised application’s tasks call RK01 directly. This is the closest protected analogue to the normal RK0 arrangement.
A larger product may build an integrated user-side OS or framework above RK01, collecting protocols, storage policy and application services behind a common interface while still using the kernel mechanisms underneath.
A more decomposed system may place selected services in server tasks. Applications send requests rather than owning devices or service state directly. Hardware-facing endpoints may remain privileged, while parsing, protocols and product policy can live in unprivileged domains.
“Application A”, “Application B” and “Application C” in such a drawing are architectural groupings, not independently loaded programs. Each may contain one or several tasks and domains. RK01 still schedules tasks individually, and the final firmware is still one linked image.
The operating system built around RK01 can therefore opt into a microkernel-like organisation where the boundary pays for itself. RK01 does not require every facility to become a server.
Why it leans towards a microkernel style
Once memory authority is explicit, several design choices become natural:
- client/server communication;
- centralised ownership of hardware and persistent service state;
- copied payloads across protection boundaries;
- independently scheduled services;
- small interrupt handlers;
- priority-aware synchronous calls;
- explicit fault boundaries.
That is microkernel-like structure. It does not automatically make the implementation a strict microkernel, nor does placing a privileged driver in a task remove it from the trusted base. An OS built on RK01 may push substantial policy into unprivileged service domains and keep only a narrow privileged hardware endpoint, but it may also keep a timing-critical facility inside the kernel.
Moving every operation through isolated servers can follow the least privileged principle, but it also adds validation, copying and context switches: client to kernel, kernel to server, server back through the kernel, and finally back to the client. On a small real-time target, those transitions belong in the response-time calculation.
That argument is older than me. And hell, I am old.
The boundary has a price
The ThreadX-style preemption-chain stress test makes part of that price visible. Five same-domain counter tasks form a priority chain: the lowest-priority task drives it, each intermediate task resumes the next higher-priority task and suspends itself, and the highest-priority task increments its counter and suspends. The test is deliberately dominated by event operations, preemption and context switching.
Under the stated STM32F401RE Cortex-M4F/80 MHz, -O2, FPU-disabled and NDEBUG conditions, RK01 completed ~26.62% of RK0’s chain throughput. The reciprocal is ~3.76. When every task is isolated from others (every task belongs to a domain), the reciprocal increases to ~4.90; that is, RK01 completed ~20.40% of RK0’s tasks in the same interval.

That result must be named correctly. It does not prove that an RK01 context switch is universally 4 or 5 times slower. The chain includes SVC entry and exit, handle and pointer validation, event-state changes, blocking and wake-up work, PendSV, and the private-stack MPU update. All participating application tasks are in the implicit App domain, so it does not include full inter-domain authority-map replacement either.
RK0 stopped at exactly equal counters. RK01 sometimes ended a reporting interval with a one-count spread while remaining within the test’s ±1 criterion. That is consistent with the reporter interrupting the chain at a different position. It is not, by itself, a numerical measurement of timing jitter.
The response-time model must distinguish direct calls from syscalls, immediate syscalls from blocking continuations, same-domain dispatch from inter-domain dispatch, and direct kernel service from client/server service. Protection makes the analysis more involved because the machine now has more intentional states.
It is still a real-time kernel
Real-time does not mean that protection is free or that two counters must always stop at the same integer. It means that the mechanisms affecting response time have bounded behaviour that can be included in analysis.
RK01 retains fixed-priority scheduling, bounded pools, finite wait structures, static MPU capacity, bounded syscall argument handling and a memory-authority topology frozen before dispatch. Even run-time-created tasks and objects come from build-time-limited storage. Same-domain and inter-domain switches have different costs, but both paths perform a finite, known amount of work.
The honest claim is therefore not that RK01 preserves RK0’s simplicity. It cannot. The claim is that RK01 exchanges some of that simplicity and throughput for explicit fault-containment boundaries without abandoning bounded real-time mechanisms.
There is no reason to merge the two kernels into one compromise. RK0 will remain flat, transparent and direct. RK01 should expose the cost of its boundaries and make them useful.
RK01 is RK0 with one more boundary made explicit—and with the bill attached.
The current source is available in the RK01 repository.
- Complex control applications that require hard timing constraints on tasks’ execution need to be supported by highly predictable operating systems. Predictability can be achieved only by introducing radical changes in the basic design paradigms found in classical time-sharing systems. For example, in any real-time control system, the code of each task is known a priori and hence can be analyzed to determine its characteristics in terms of computation time, resources, and precedence relations with other tasks. Therefore, there is no need to consider a task as an unknown processing entity; rather, its parameters can be used by the operating system to verify its schedulability within the specified timing requirements. Moreover, all hard tasks should be handled by the scheduler to meet their individual deadlines, not to reduce their average response time. In addition, in any typical real-time application, the various control activities can be seen as members of a team acting together to accomplish one common goal, which can be the control of a nuclear power plant or an aircraft. This means that tasks are not all independent, and it is not strictly necessary to support independent address spaces. (Buttazzo, Hard Real-Time Computing systems) ↩︎

Leave a Reply