Real-Time Programming
Deterministic execution and heapless programming for real-time systems.
Real-Time Requirements
Real-time systems must respond within guaranteed time bounds:
| Type | Requirement | Example |
|---|---|---|
| Hard real-time | Miss = failure | Airbag deployment |
| Firm real-time | Miss = degraded | Video streaming |
| Soft real-time | Miss = lower quality | User interface |
Challenges for Rust
| Challenge | Solution |
|---|---|
| Heap allocation | Use heapless collections |
| Garbage collection | Rust has none! |
| Unbounded operations | Use fixed-size buffers |
| Priority inversion | Proper mutex design |
Heapless Collections
Stack-allocated, fixed-capacity data structures:
use heapless::{Vec, String, FnvIndexMap};
fn example() {
// Vector with capacity 16
let mut vec: Vec<u32, 16> = Vec::new();
vec.push(1).unwrap();
vec.push(2).unwrap();
// String with capacity 64 bytes
let mut s: String<64> = String::new();
s.push_str("Hello").unwrap();
// HashMap with capacity 8
let mut map: FnvIndexMap<&str, u32, 8> = FnvIndexMap::new();
map.insert("key", 42).unwrap();
}
Add to Cargo.toml:
[dependencies]
heapless = "0.8"
Heapless Queues
For inter-task communication:
use heapless::spsc::Queue;
// Single-producer, single-consumer queue
static mut QUEUE: Queue<u32, 16> = Queue::new();
fn producer() {
unsafe {
let mut producer = QUEUE.split().0;
producer.enqueue(42).ok();
}
}
fn consumer() {
unsafe {
let mut consumer = QUEUE.split().1;
if let Some(value) = consumer.dequeue() {
// Process value
}
}
}
Memory Pools
Pre-allocate fixed-size blocks:
use heapless::pool::Pool;
use heapless::pool::singleton::Pool as SingletonPool;
// Define a memory pool
static POOL: SingletonPool<[u8; 128]> = SingletonPool::new();
fn init_pool() {
// Grow pool with static memory
static mut MEMORY: [u8; 128 * 10] = [0; 128 * 10];
unsafe {
POOL.grow(&mut MEMORY);
}
}
fn allocate() -> Option<Box<[u8; 128]>> {
POOL.alloc()
}
Bounded Execution Time
Avoid unbounded operations:
// BAD: Unbounded iteration
fn bad_search(data: &[u32], target: u32) -> Option<usize> {
data.iter().position(|&x| x == target)
}
// GOOD: Bounded iteration with maximum
fn bounded_search(data: &[u32], target: u32, max_iters: usize) -> Option<usize> {
for (i, &val) in data.iter().take(max_iters).enumerate() {
if val == target {
return Some(i);
}
}
None
}
// GOOD: Fixed-size lookup table
const LOOKUP: [u32; 256] = [/* ... */];
fn constant_time_lookup(index: u8) -> u32 {
LOOKUP[index as usize]
}
Atomic Operations
Lock-free synchronization for real-time:
use core::sync::atomic::{AtomicU32, AtomicBool, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
static FLAG: AtomicBool = AtomicBool::new(false);
fn increment() {
COUNTER.fetch_add(1, Ordering::SeqCst);
}
fn signal() {
FLAG.store(true, Ordering::Release);
}
fn wait_for_signal() {
while !FLAG.load(Ordering::Acquire) {
core::hint::spin_loop();
}
}
Critical Sections
Disable interrupts for atomic access:
use cortex_m::interrupt;
static mut SHARED: u32 = 0;
fn critical_update(value: u32) {
interrupt::free(|_| {
unsafe {
SHARED = value;
}
});
}
// With RTIC (Real-Time Interrupt-driven Concurrency)
#[rtic::app(device = stm32f4xx_hal::pac)]
mod app {
#[shared]
struct Shared {
counter: u32,
}
#[task(shared = [counter])]
fn task1(mut ctx: task1::Context) {
ctx.shared.counter.lock(|counter| {
*counter += 1;
});
}
}
Timing and Deadlines
use core::time::Duration;
struct Deadline {
start: u64,
timeout_us: u64,
}
impl Deadline {
fn new(timeout: Duration) -> Self {
Deadline {
start: get_time_us(),
timeout_us: timeout.as_micros() as u64,
}
}
fn is_expired(&self) -> bool {
get_time_us() - self.start >= self.timeout_us
}
fn remaining(&self) -> Option<Duration> {
let elapsed = get_time_us() - self.start;
if elapsed >= self.timeout_us {
None
} else {
Some(Duration::from_micros(self.timeout_us - elapsed))
}
}
}
fn task_with_deadline() -> Result<(), TimeoutError> {
let deadline = Deadline::new(Duration::from_millis(10));
while !work_complete() {
if deadline.is_expired() {
return Err(TimeoutError);
}
do_work_chunk();
}
Ok(())
}
Watchdog Timer
Detect and recover from hangs:
struct Watchdog {
// Hardware watchdog registers
}
impl Watchdog {
fn feed(&mut self) {
// Reset watchdog counter
unsafe {
write_volatile(WATCHDOG_RELOAD, WATCHDOG_KEY);
}
}
fn enable(&mut self, timeout_ms: u32) {
// Configure and enable watchdog
}
}
fn main_loop(mut watchdog: Watchdog) -> ! {
loop {
// Do work
process_inputs();
update_state();
generate_outputs();
// Feed watchdog to prevent reset
watchdog.feed();
}
}
Stack Usage Analysis
Monitor stack usage to prevent overflow:
const STACK_CANARY: u32 = 0xDEAD_BEEF;
#[link_section = ".stack_guard"]
static STACK_GUARD: u32 = STACK_CANARY;
fn check_stack_overflow() -> bool {
unsafe {
core::ptr::read_volatile(&STACK_GUARD) != STACK_CANARY
}
}
// Paint stack for high-water mark analysis
fn paint_stack(stack: &mut [u32]) {
for word in stack.iter_mut() {
*word = STACK_CANARY;
}
}
fn measure_stack_usage(stack: &[u32]) -> usize {
let mut used = 0;
for word in stack.iter().rev() {
if *word != STACK_CANARY {
used += 4;
} else {
break;
}
}
used
}
Best Practices
- No heap allocation in real-time paths
- Bound all loops with maximum iterations
- Use atomic operations for lock-free code
- Minimize critical sections
- Use watchdogs for fault recovery
- Analyze stack usage
- Test worst-case timing
Summary
| Technique | Purpose |
|---|---|
| Heapless collections | Fixed-size data structures |
| Atomics | Lock-free synchronization |
| Critical sections | Mutual exclusion |
| Deadlines | Time-bounded operations |
| Watchdog | Fault recovery |
See Also
Next Steps
Learn about RTOS integration with FreeRTOS and Embassy.