Part II: Kernel Architecture
Architecture Overview
Execution Model
QuantumRT operates in two execution levels: Privileged and Unprivileged. Privileged code has unrestricted access to all system resources, while unprivileged code is restricted from modifying critical kernel or hardware registers.
When Memory Protection is disabled, all code executes in privileged mode and can access the entire memory map.
When Memory Protection is enabled, QuantumRT runs the kernel, idle thread, and all interrupt service routines in privileged mode. User threads may execute in unprivileged mode, isolating them from kernel memory and preventing direct access to protected peripherals. This separation increases system robustness by ensuring that application threads cannot corrupt kernel or other thread contexts.
Memory Model
Each thread has its own stack memory, isolated from other threads. The kernel and idle thread also have separate stacks. The kernel stack is used for kernel operations, while the idle thread stack is used when no other threads are ready to run. ARM Cortex-M processors implement Process Stack Pointer (PSP) and Main Stack Pointer (MSP) to facilitate stack separation. Threads (including idle thread) operate using PSP, while the kernel and ISRs utilize MSP.
Boot & Kernel Bring-Up
QuantumRT is shipped with default BSP for ARM Cortex-M platforms which handles necessary low-level initialization for the kernel operation. BSP must be initialized with bsp_init().
System Timer must be configured with qrt_systimer_configure() before starting the kernel.
The kernel is initialized and started by calling qrt_kernelstart(), which converts the currently executing code into the main thread.
Main thread is created with default attributes and runs at the highest priority.
After the kernel has started, the idle thread is created and the scheduler begins managing thread execution.
Threads can only be created after the kernel has started.
Warning
Main thread must not be returned from, as this would lead to undefined behavior, but instead should be exit with pthread_exit().
Timers
System Timer
The kernel requires a hardware timer to serve as the primary timekeeping mechanism. The System Timer provides time-based services, ensuring accurate delays, timeouts, and scheduling. The accuracy of timing services is directly dependent on the frequency of the hardware System Timer.
The System Timer interrupt is configured with the highest priority to maintain accurate timing and ensure no ticks are lost during interrupt handling. To satisfy this requirement, the System Timer interrupt must be the only interrupt assigned the highest priority level.
Interrupts
System Timer interrupts occur whenever the timer reaches a scheduled deadline or when the deadline needs to be updated. On System Timer interrupt, the interrupt handler reloads the timer counter and signals the kernel that a deadline has expired. Deadline calculations for subsequent events are postponed until all Interrupt Service Routines (ISRs) have completed execution, minimizing interrupt latency.
Configuration
The kernel requires integration with a hardware timer. The user must configure the hardware timer to trigger interrupts on overflow and call qrt_systimer_configure(), providing:
A function for reading the hardware timer
A function for reloading the hardware timer
The counting direction of the hardware timer
The hardware timer’s interrupt request number (IRQ)
The hardware timer’s interrupt handler must invoke qrt_systimer_handler(). The kernel manages Nested Vectored Interrupt Controller (NVIC) configuration for the hardware timer.
Base Timer
Configuration
Precise Scheduling
The kernel implements Precise Scheduling, dynamically loading the System Timer with the next scheduled event’s deadline instead of relying on fixed system ticks. This approach eliminates unnecessary timer interrupts, improving accuracy, reducing CPU overhead, and enhancing energy efficiency compared to traditional Tick-Based Scheduling.
Scheduler
The scheduler manages thread execution, ensuring fair resource distribution, responsiveness, and prioritized handling of critical threads. The kernel uses preemptive scheduling, allowing it to interrupt a running thread whenever a higher priority thread becomes ready to execute.
Priority-Based Scheduling
The kernel uses preemptive priority-based scheduling, a scheduling algorithm designed to allocate processing time among threads based on their priority level, where a higher value represents higher priority.
To enable Priority-Based scheduling for a thread, pthread attribute must be set with SCHED_FIFO with pthread_attr_setschedpolicy().
Round-Robin Scheduling
The kernel provides optional Round-Robin scheduling for threads of equal priority, ensuring fair distribution of CPU time among threads without strict real-time constraints, such as background tasks. Threads execute in rotation, each receiving a fixed execution time slice. When the time slice expires or a context switch occurs, the thread is returned to the ready queue, and another thread at the same priority level executes. However, the kernel does not guarantee a thread always completes its full time slice.
To enable Round-Robin scheduling for a thread, pthread attribute must be set with SCHED_RR with pthread_attr_setschedpolicy().
Round-Robin scheduling can be enabled with configuration option QRT_CFG_SCHED_RR_ENABLE.
Interrupts and Scheduling
Interrupt Service Routines (ISRs) respond to hardware events by temporarily interrupting thread execution. Efficient interrupt handling is essential for real-time responsiveness and deterministic scheduling.
QuantumRT does not perform scheduling directly within ISR context. Instead, ISRs use deferred kernel calls to notify the kernel and interact with kernel objects once the ISR has completed. Scheduling decisions are made only after ISR execution finishes and deferred kernel calls have been processed, ensuring minimal interrupt latency and predictable execution.
Yielding
Threads do not need to explicitly yield, but the scheduler automatically preempts lower-priority threads and manages execution order based on priority and scheduling policy.
Threads can voluntarily yield the processor using sched_yield(), allowing other threads of equal priority to execute.
Yielding is particularly useful when a thread has completed its immediate work and wants to allow other threads to run.
Priority Inversion
Priority inversion occurs when a lower-priority thread indirectly prevents a higher-priority thread from executing, effectively reversing the intended priority order. Priority inversion can be bounded or unbounded:
Bounded priority inversion occurs when a high-priority thread is waiting for a resource held by a low-priority thread. The duration of inversion is bounded because it ends as soon as the low-priority thread releases the resource.
Unbounded priority inversion occurs if the duration of inversion becomes unpredictable due to intermediate-priority threads preempting the low-priority thread holding a critical resource needed by a high-priority thread.
Idle Thread
The kernel maintains an idle thread that runs when no other threads are in the ready state. The idle thread ensures that the processor always has a runnable thread, even if no application-specific tasks are ready to execute.
Typically, the idle thread executes continuously at the lowest priority level. It runs in a minimal infinite loop, performing power-saving operations or entering a low-power state to conserve energy when the system is idle if configured so.
Power save mode can be enabled with configuration option QRT_CFG_IDLE_DEEP_SLEEP_ENABLE.
Floating-Point Support
Some supported processor cores include a FPU (Floating-Point Unit), introducing additional registers that must be managed during context switching. Stacking floating-point registers increases thread stack size, adds interrupt latency, and extends context switching time because the registers must be saved and restored.
The kernel supports stacking floating-point registers and it can be enabled per thread with call to qrt_fpu_ctxenable() and can be disabled with qrt_fpu_ctxdisable().
FPU support can be enabled with configuration option QRT_CFG_FPU_ENABLE, and floating-point stacking can be enabled with QRT_CFG_FPU_STACKING_ENABLE.