This personal learning project is an attempt to build a simple/naive RTOS for ARM Cortex-M microcontrollers in Rust.
Targeted microcontrollers are the mps2_an385
(Cortex M3) platform and the stm32f429zi
MCU (Cortex M4).
I've already worked on an RTOS for AVR 8 bits microcontrollers, written in C: https://github.com/lucasdietrich/AVRTOS
-
Inline assembly:
- Procedure Call Standard for the ARM® Architecture
- Cortex-M3 Technical Reference Manual
- Deprecated Features in ARMv7-M
-
Architecture: (
thumbv7em-none-eabihf
), devices:- mps2_an385 (ARM Cortex-M3 )
- stm32f4xx (ARM Cortex-M4)
-
Cortex M3/M4 initialization
- RAM initialization
- Vector table
- Reset handler
- PendSV
- Configure lowest priority (0b111)
- SVCall
- Configure lowest priority (0b111)
- Systick
- Configure highest priority (0b000)
- Other interrupts
-
Peripherals:
- UART
- mps2_an385
- stm32f4xx
- UART
-
RTOS features:
- stacks
- system stack
- user stack
- irq stack
- MSP/PSP
- thread switch (without FPU support)
- cooperative scheduling
- preemptive scheduling
- sleep
- mutex
- semaphore
- minimal drivers support for UART and GPIO
- syscalls:
- printf
- sleep
- fork
- mutex
- semaphore
- memory allocation
- std library (allocator, collections, etc.)
- rust
- C
- stacks
-
Minimal process: load an application from an elf file and run it
- parse elf file
- toolchain for build the application (C/Rust + linker script + relocation? + syscalls)
TODO:
understand why .data MYVAR is already initialized in QEMU-> QEMU loads the .data section from the ELF file to RAMunderstand why .data .bss appears in the ELF file-> QEMU loads the .bss section from the ELF file to RAM- Add the noinit section to the linker script
- If symbol gets wiped out of the elf, gdb won't find it, we need to force the symbol to be kept in the elf file -> how to ? (e.g. _thread_switch)
- Proper Systick/FCPU and FREQ_SYS_TICK handling
Tested with the following toolchains:
nightly-2024-10-01
nightly-2024-07-08
To install a new toolchain:
rustup toolchain add nightly-2024-10-01 --profile minimal
const FOO: u32 = 42; // Const is a compile-time constant
static BAR: u32 = 42; // Static is a runtime constant
static mut BAZ: u32 = 42; // Static mutable
Disable name mangling for a function:
#[export_name = "switch_to_user"]
fn switch_to_user() {
// ...
}
#[export_name = "my_symbol"]
extern "C" fn my_function() {
// ...
}
#[link_section = ".kvars"]
static mut BAZ: u32 = 42;
In order to export the symbol of a static variable, it must be declared with #[used]
:
The no_mangle
attribute make sure the symbol name is not mangled by the compiler (e.g. demo::entry::z_current -> z_current)
#[used]
#[no_mangle]
pub static mut z_current: *mut Thread = core::ptr::null_mut();
!!! warning "TODO" What is bellow is probably wrong
link_name
must only be used on statics and functions that are in an extern
block.
extern "C" {
#[link_name = "z_current"]
static mut z_current: *mut Thread;
}
Following inline assembly code is equivalent to the rust code:
use use core::arch::global_asm;
global_asm!(
"
.section .text, \"ax\"
.global _pendsv
.thumb_func
_pendsv:
push {{r7, lr}}
mov r7, sp
nop
pop {{r7, pc}}
"
);
extern "C" {
pub fn _pendsv();
}
Pure rust:
use use core::arch::asm;
#[no_mangle]
pub unsafe extern "C" fn _pendsv() {
asm!("nop");
}
It's currently impossible to write naked functions in Rust, see rust-lang/rust#90957 for support for #[naked]
functions.
A static variable can be initialized using a const
function:
pub struct Kernel;
impl Kernel {
pub const fn init() -> Kernel {
Kernel {}
}
}
fn main() {
static mut KERNEL: Kernel = Kernel::init();
}
If you want to watch a static rust variable, you need to use its full name, for example:
The full names can be found in the output of nm
: e.g. 2000000c 00000014 d demo::KERNEL
What is the purpose of PhantomData
in the following code ?
pub struct SCB {
_marker: PhantomData<*const ()>,
}
Feel free to help the compiler to inline a function by using the #[inline(always)]
attribute:
impl<D: CsDomain> Cs<D> {
#[inline(always)]
/* This is the only method to obtain a critical session object */
pub unsafe fn new() -> Self {
Cs {
domain: PhantomData,
}
}
}
Set variable in the .noinit
section:
#[link_section = ".noinit"]
static mut THREAD_STACK1: u32 = 0;
It would be great to have:
pub trait KernelSpecs {
const FREQ_SYS_TICK: u32 = 100;
const KOBJS: usize = 32;
}
// CPU: CPU variant
pub struct Kernel<'a, CPU: CpuVariant, Specs: KernelSpecs>
where
[(); Specs::KOBJS]:,
[(); Specs::FREQ_SYS_TICK as usize]:,
{
tasks: sl::List<'a, Thread<'a, CPU>, Runqueue>,
// systick
systick: SysTick<{ Specs::FREQ_SYS_TICK }>,
// Ticks counter: period: P (ms)
ticks: u64,
idle: Thread<'a, CPU>,
// Idle thread
// Kernel objects (Sync) for synchronization
kobj: [Option<Box<dyn KernelObjectTrait<'a, CPU> + 'a>>; Specs::KOBJS],
}
However the feature is not well supported today, it needs #![feature(generic_const_exprs)]
This is discussed here: rust-lang/rust#76560