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_an386
(arm v7 Cortex M4) platform and the stm32f429zi
MCU (arm v7 Cortex M4).
I've already worked on an RTOS for AVR 8 bits microcontrollers, written in C: https://github.com/lucasdietrich/AVRTOS
- Architecture: (
thumbv7em-none-eabihf
), devices:- mps2_an385 (armv6 Cortex-M3 )
- compile flags:
-mfloat-abi=soft -march=armv6m -mfpu=none
- target triplet:
thumbv6m-unknown-none-eabi
- compile flags:
- mps2_an386 (armv7 Cortex-M4 )
- compile flags:
-mfloat-abi=softfp -march=armv7m -mfpu=fpv4-sp-d16
- target triplet:
thumbv7m-unknown-none-eabi
- compile flags:
- stm32f4xx (armv7 Cortex-M4)
- mps2_an385 (armv6 Cortex-M3 )
- 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
- minimal drivers support
- UART
- mps2_an385
- mps2_an386
- 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
- syscalls:
- printf
- sleep
- fork (needs MMU)
- 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
- build toolchain with crosstool-ng for C development
- custom linker script ?
- write a minimal libc for the os (syscalls)
===========================
arm rust RTOS demo starting
===========================
CPUID base: 0x410fc240
interrupts enabled: true
Systick prio: 0x00
PendSV prio: 0x00
SVC prio: 0x00
Systick prio: 0x00
PendSV prio: 0x07
SVC prio: 0x07
control: 0x00000000 priviledged, MSP, no FPU
elf 1 loaded
elf 2 loaded
Kernel initialized, starting kernel loop and user threads ...
===========================
Test syscall: r0=10005f4a, r1=0, r2=0, r3=0
Test syscall: r0=10005f44, r1=0, r2=0, r3=0
.42 65 6C 6C 6F 20 57 6F 72 6C 64 21 0A 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
42 65 6C 6C 6F 20 57 6F 72 6C 64 21 0A 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Bello World!
Bello World!
Test syscall: r0=1, r1=2, r2=3, r3=4
Test syscall: r0=1, r1=2, r2=3, r3=4
Test syscall: r0=0, r1=2000dfc0, r2=20, r3=87654321
Test syscall: r0=0, r1=2000bf08, r2=20, r3=87654321
Test syscall: r0=0, r1=0, r2=0, r3=0
Loadable ELF returned: 2a
Test syscall: r0=0, r1=0, r2=0, r3=0
Loadable ELF returned: 2a
......................................................
-
Inline assembly:
- Procedure Call Standard for the ARM® Architecture
- Cortex-M3 Technical Reference Manual
- Deprecated Features in ARMv7-M
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
- Choose distinct allocators for multiple Box (e.g. one for the kernel, one for the user)
- Implement synchronization primitives cancellation mecanism (trait function + syscall)
- Implement identifier for synchronization primitives (number, str ??)
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