1 unstable release
0.1.0 | Feb 21, 2024 |
---|
#20 in #hard
2KB
SCARS
SCARS is a Real-Time Operating System (RTOS) designed for hard real-time applications, inspired by the Ada Ravenscar profile. It is a traditional style RTOS with fixed priority preemptive scheduling, not an async executor. It is still a work-in-progress, and is not recommended for production systems. Currently only RISC-V without FPU, and pthreads based simulator are implemented. ARM and FPU support are planned.
-
It implements the immediate priority ceiling protocol, and provides an unified priority model for tasks and interrupts, which allows developers to use the same APIs in tasks and interrupts. Mutex protected data can be accessed from interrupt handlers.
-
All tasks and interrupt handlers are statically allocated, yet started at runtime, allowing tasks and interrupt handlers to capture variables from the environment that they are started in. No need for global static variables.
-
All scheduling operations that depend on the number or order of tasks, are executed within a preemption lock that allows interrupts, minimizing the maximum interrupt latency. Interrupt latency is independent from scheduling and number of tasks.
-
Any priority based synchronization primitives that are below a priority ceiling, will not prevent higher priority tasks or interrupts from executing.
Tasks
The entry task is defined with entry
attribute for a function. There can be only
one entry task. Tasks can never exit.
#[scars::entry(name = "main", priority = 1, stack_size = 2048)]
fn main() {
loop {}
}
Other tasks are created with the make_task!
macro, which statically allocates the
task state, including the task stack.
const TASK_PRIO: TaskPriority = 2;
const TASK_STACK_SIZE: usize = 1024;
const CHANNEL_CEILING: AnyPriority = Priority::any_task_priority(TASK_PRIO);
const CHANNEL_CAPACITY: usize = 1;
#[scars::entry(name = "main", priority = 1, stack_size = 2048)]
fn main() {
let task = make_task!("task", TASK_PRIO, TASK_STACK_SIZE);
let (sender, receiver) = make_channel!(u32, CHANNEL_CAPACITY, CHANNEL_CEILING);
task.start(move || {
// Task closure captures `sender`
let mut counter: u32 = 0;
loop {
sender.send(counter);
counter += 1;
}
});
loop {
let value = receiver.recv();
println!("Received {:?}", value);
}
}
Idle task
The kernel creates an internal idle task to be run when no other task is ready. The idle task is special in that it does not follow the priority ceiling protocol, and can run even when higher priority tasks hold locks.
To run custom code in the idle task, the user can define the idle task hook with
the idle_task_hook
attribute:
#[scars::idle_task_hook]
fn idle_task_hook() {
// do something when idle
}
The hook-function can use InterruptLock
and PreemptLock
; however, due to being exempt
from the priority ceiling protocol, it may not acquire a CeilingLock
. An attempt to
acquire a CeilingLock
from the idle task will result in a runtime error.
Interrupt Handlers
Interrupt handlers are created similarly to tasks with make_interrupt_handler!
macro.
use scars::pac;
const INTERRUPT_PRIO: InterruptPriority = 2;
const CHANNEL_CEILING: AnyPriority = Priority::any_interrupt_priority(INTERRUPT_PRIO);
const CHANNEL_CAPACITY: usize = 10;
#[scars::entry(name = "main", priority = 1, stack_size = 2048)]
fn main() {
let uart_handler = make_interrupt_handler!(pac::Interrupt::UART1, INTERRUPT_PRIO);
let (sender, receiver) = make_channel!(u32, CHANNEL_CAPACITY, CHANNEL_CEILING);
let mut counter: u32 = 0;
uart_handler.attach(move || {
// Interrupt handler closure captures `sender` and `counter`
counter += 1;
// Must use non-blocking `try_send` in interrupt
let _ = sender.try_send(counter);
});
uart_handler.enable_interrupt();
loop {
let value = receiver.recv();
println!("Handled {:?} interrupts", value);
}
}
Immediate Priority Ceiling Protocol
Tasks and interrupt handlers have fixed base priorities, and all priority based locking primitives have a priority ceiling, which is the maximum priority of any task or an interrupt that can acquire the lock. Acquiring a priority based lock immediately raises the active priority of the task or an interrupt to the ceiling priority of that lock, preventing other tasks or interrupts from acquiring the lock. The priority based locking implements mutual exclusion in the scheduler ready queue.
If the ceiling of the lock is at interrupt priorities, then interrupts are blocked up to that priority. Higher priority interrupts are allowed to execute, but since they are above the ceiling, they are never allowed to acquire the lock. The benefit from this, is that whenever interrupt handler executes, it may access priority lock protected data; if a task or lower priority interrupt handler is holding the lock, the interrupt handler is not allowed to execute in the first place. The downside is that all locking primitives such as mutexes and condition variables, must have the ceiling priority specified by the developer.
Violating the priority ceiling protocol by trying to acquire a lock from a task or an interrupt handler with higher priority than the lock ceiling priority, will lead to a runtime error.
Synchronization primitives
- Locks:
InterruptLock
,PreemptLock
andCeilingLock
Mutex
Condvar
Channel
- others TBD
Simulator
The scars-khal-sim
crate implements a pthreads based simulator on host. Interrupt
simulation has not yet been implemented.
Running tests
khal-e310x
Configuration in .cargo/config.toml is for QEMU.
$ cargo test --release --package=scars --features="khal-e310x" --target=riscv32imac-unknown-none-elf
khal-sim
$ cargo test --release --package=scars --features="khal-sim" --target=x86_64-unknown-linux-gnu