Unlocking Embedded RTOS: A Deep Dive into Multi‑Task Scheduling, IPC, and Chip Porting
This comprehensive guide explores the fundamentals of embedded real‑time operating systems, covering instruction set architecture, hardware‑software ecosystems, multi‑task scheduling algorithms, inter‑process communication primitives, software timers, and concrete RISC‑V and Cortex‑M porting implementations with Rust and assembly examples.
Introduction
As a software engineer who finally got to touch hardware, I combined basic computer knowledge with FreeRTOS source code to create a document that explains how to understand software execution through hardware, offering deep insights for embedded engineering.
The document is intentionally written with plain language, analogies, and many illustrations to help readers follow along, experiment with reference implementations, and eventually master the scheduling mechanisms of multi‑core systems.
Glossary
ISA – Instruction Set Architecture, the part of computer architecture that defines the set of opcodes, registers, addressing modes, and I/O.
ISR – Interrupt Service Routine, the software that handles hardware interrupt requests.
Compiler – Translates source code into target code, performing preprocessing, optimization, assembly, and linking.
Exception/Interrupt – Exceptions are critical hardware events; interrupts are the mechanism that temporarily pauses normal execution.
IP – Intellectual Property, a legal concept for licensed designs such as ARM cores.
SOC – System‑on‑Chip, an integrated circuit that contains a complete system with embedded software.
MCU – Microcontroller Unit, a single‑chip computer that integrates CPU, memory, timers, UART, DMA, etc.
DSP – Digital Signal Processor, a specialized microprocessor for audio, telecom, and image processing.
PLC – Programmable Logic Controller, an industrial digital computer for controlling machinery.
RTOS – Real‑Time Operating System, provides deterministic task execution.
Toolchain – A set of tools (compiler, assembler, linker, debugger) used to build software.
Linker Script – Describes how input sections are mapped to output sections and memory layout.
ELF/PE – Executable and Linkable Format (Unix) and Portable Executable (Windows) file formats.
Timer – Generates periodic interrupts to drive the scheduler.
Context Switch – Saves the current CPU state and restores the state of the next task.
Hardware‑Software Ecosystem
The ISA sits at the top of the ecosystem, followed by CPU cores, toolchains, and chips. A robust software stack (OS, middleware, applications) builds on this foundation, enabling modern devices.
Computer Architecture
Based on the von Neumann model, a computer consists of input devices, memory, control unit, arithmetic logic unit, and output devices. Modern MCUs integrate all these components on a single chip.
Memory
Different memory types (SRAM, Flash, DRAM) are chosen based on cost, area, and power requirements.
Peripheral Controllers
GPIO, I2C, SPI, USB, etc., provide standardized bus protocols for external devices.
CPU
Registers (general‑purpose, special‑purpose) hold temporary data and control flow. The CPU executes instructions fetched from memory.
How Software Runs
Code is placed in memory sections (.text, .data, .bss, .rodata) as described by the linker script. The loader copies these sections to their final locations before execution.
Multi‑Task Scheduling
In simple systems, a timer interrupt drives a round‑robin scheduler. For more complex workloads, priority‑based preemptive scheduling is used.
Timer Interrupt Service Routine
fn do_systick(&self) -> bool {
// Update delayed tasks and move ready tasks to the ready queue
// Return true if a higher‑priority task is ready
}Soft‑Interrupt Service Routine
fn _irq_handler() {
// Save current task context
// Switch stack to interrupt stack
// Restore next task context and execute mret
}Task Definition (Rust)
pub struct Task {
pub sp: usize,
pub stack: *mut usize,
pub entry: Func,
pub remaining_ticks: usize,
pub id: u16,
pub priority: u8,
pub state: State,
}Task Initialization
fn save_context(task: &mut Task) {
// Allocate stack space and push initial registers, PC, and status
// Store the stack pointer in task.sp
}Context Switch Assembly (RISC‑V)
.macro SAVE_CONTEXT_SOFT_IRQ
addi sp, sp, -36*4
sw x1, 1*4(sp)
// ... save all registers ...
csrr t0, mstatus
sw t0, 32*4(sp)
csrr t0, mepc
sw t0, 33*4(sp)
.endm
.macro REsw_CONTEXT_SOFT_IRQ
lw x1, 1*4(sp)
// ... restore all registers ...
lw t0, 32*4(sp)
csrw mstatus, t0
lw t0, 33*4(sp)
csrw mepc, t0
addi sp, sp, 36*4
.endmInter‑Process Communication & Synchronization
Binary Semaphore / Mutex
pub struct Notifier {
blocker: Rc<usize>,
signal: Rc<AtomicBool>,
}
impl Notifier {
pub unsafe fn block(&self) { /* block current task */ }
pub unsafe fn wakeup(&self) { /* wake blocked task */ }
pub fn notify_isr(&self) -> nb::Result<(), Error> { /* set signal and wake */ }
pub fn notify(&self) { /* set signal or block */ }
pub fn wait(&self) { /* wait for signal or block */ }
}Counting Semaphore
pub struct Semaphore {
waiters: Rc<RefCell<TaskQueue>>,
notifiers: Rc<RefCell<TaskQueue>>,
signal: Rc<AtomicUsize>,
max_value: usize,
}
impl Semaphore {
pub fn post_isr(&self) -> nb::Result<(), Error> { /* increment signal and wake */ }
pub fn post(&self) { /* increment or block if full */ }
pub fn wait(&self) { /* decrement or block if zero */ }
}Queue (MPMC)
pub struct Queue<T> {
list: Rc<RefCell<VecDeque<T>>>,
sem: Semaphore,
}
impl<T> Queue<T> {
pub fn push_back(&self, item: T) { self.list.borrow_mut().push_back(item); self.sem.post(); }
pub fn pop_front(&self) -> Option<T> { self.sem.wait(); self.list.borrow_mut().pop_front() }
// ISR‑safe variants return nb::Result
}Software Timers
pub struct TimerInner {
entry: Func,
args: *mut c_void,
period: usize,
next_tick: u64,
}
impl TimerInner {
pub fn after<F: FnOnce() + Send + 'static>(ms: usize, f: F) { /* schedule one‑shot */ }
pub fn period<F: Fn() + Send + 'static>(period_ms: usize, f: F) -> usize { /* schedule periodic */ }
}Chip Porting Layer
The Portable trait defines the abstraction required for any target MCU.
pub trait Portable {
fn barrier();
fn free<F, R>(f: F) -> R where F: FnOnce(&CriticalSection) -> R;
fn enable_interrupt();
fn disable_interrupt();
fn start_scheduler() -> !;
fn irq();
fn disable_irq();
fn systick() -> u64;
fn delay_us(us: u64);
fn save_context(task: &mut Task);
}RISC‑V Implementation (GD32VF103)
pub struct Gd32vf103Porting;
impl Portable for Gd32vf103Porting {
fn barrier() { unsafe { riscv::asm::sfence_vma_all() } }
fn free<F, R>(f: F) -> R where F: FnOnce(&CriticalSection) -> R { riscv::interrupt::free(f) }
fn enable_interrupt() { unsafe { riscv::interrupt::enable() } }
fn disable_interrupt() { unsafe { riscv::interrupt::disable() } }
fn start_scheduler() -> ! {
reset_systick();
setup_intrrupt();
unsafe { asm!(include_str!("restore_ctx.S")) };
panic!("unreachable");
}
fn irq() { let ptr = (TIMER_CTRL_ADDR + TIMER_MSIP) as *mut u8; unsafe { ptr.write_volatile(*ptr | 0x01) } }
fn disable_irq() { let ptr = (TIMER_CTRL_ADDR + TIMER_MSIP) as *mut u8; unsafe { ptr.write_volatile(*ptr & !0x01) } }
fn systick() -> u64 { /* read mtime register */ }
fn delay_us(us: u64) { /* busy‑wait using mcycle */ }
fn save_context(task: &mut Task) { /* push registers onto task stack */ }
}Cortex‑M3/4 Implementation
pub struct STM32F1Porting;
impl Portable for STM32F1Porting {
fn barrier() { cortex_m::asm::dsb(); }
fn free<F, R>(f: F) -> R where F: FnOnce(&CriticalSection) -> R { unsafe { cortex_m::interrupt::free(|_| f(&CriticalSection::new())) } }
fn enable_interrupt() { unsafe { cortex_m::interrupt::enable() } }
fn disable_interrupt() { cortex_m::interrupt::disable(); }
fn start_scheduler() -> ! {
log::info!("Start scheduler");
unsafe { asm!("mov r0, #0; msr control, r0; cpsie i; cpsie f; svc 0xff") };
panic!("unreachable");
}
fn irq() { cortex_m::peripheral::SCB::set_pendsv(); }
fn disable_irq() { cortex_m::peripheral::SCB::clear_pendsv(); }
fn systick() -> u64 { unsafe { core::ptr::read_volatile(&port::SYSTICKS) } }
fn delay_us(us: u64) { let cycles = (us * (CPU_CLOCK_HZ as u64)) / 1_000_000; cortex_m::asm::delay(cycles as u32); }
fn save_context(task: &mut Task) { /* push registers onto task stack */ }
}Reference Implementations
The Xtask project (GitHub) provides a portable RTOS that runs on both RISC‑V GD32VF103 and Cortex‑M3/4, with example applications demonstrating task switching, timers, and UART output.
The Xpilot project builds on Xtask to run an IMU sampling loop, sending orientation data over USART for real‑time monitoring.
References
[1] Wikipedia – General reference.
[2] John von Neumann – Computer architecture.
[3] Integrated circuit – Wikipedia.
[4] Digital circuit – Wikipedia.
[5] Hello World – Wikipedia.
[6] GNU ld – Linker documentation.
[7] Xtask – https://github.com/gqf2008/xtask
[8] Xpilot – https://github.com/gqf2008/xpilot
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
