실제 코드는 맨 아랫 부분 참고.
겁나 어렵다. 어렵기도 어려운데, 설계 디테일 자체가 엄청 깊고 세심한 것 같다.
대충 어떤 식으로 설계를 해야 하는 지 좀 감이 온다.
코드 설명
- App 2개 구현
- BootShowApp : 처음 OS 동작시 빠르게 3번 blink.
- HearbeatApp : 부팅 후 4ms마다 깜빡임.
- main.rs에 들어가면 이 앱을 보는 게 아니라 작은 OS를 바라볼 수 있게, 그래서 메모리 주소에 직접 닿지 않고, 내가 원하는 앱을 call해서 간접적으로 처리할 수 있도록 구현.
— Hierarchy : OS 코어 ↔ 앱 ↔ 보드(레지스터).
코드: mod os의 struct Os, trait App, enum AppCall
사용: main()에서
let mut kernel = os::Os::new(&mut app_list, &mut syscalls);
kernel.run(os::AppCall::All) // 혹은 ByName("heartbeat"), ByIndex(0)
→ main은 개별 앱 함수를 직접 부르지 않음. OS(커널) 인스턴스에 앱 목록을 넘기고, 실행 정책(AppCall) 만 알려줌. 즉, main의 관점은 “작은 OS”임.
앱 코드: mod apps의 LedBlinkApp, LedSosApp, HeartbeatApp 등
앱들은 오직 Syscalls 인터페이스로만 하드웨어를 만짐:
sys.gpio_write(GpioPin::Led1, true);
sys.sleep_ms(200);
→ 레지스터 주소/비트 연산 없음.
하드웨어 접근은 전부 mod board의 impl Syscalls for BoardSyscalls 안으로 격리:
match pin {
GpioPin::Led1 => unsafe { write_volatile(...); } // 레지스터 접근
GpioPin::Led2 => unsafe { write_volatile(...); }
}
→ 앱은 “OS가 제공하는 시스템콜”만 부르고, 레지스터는 보드 계층만 다룸. 이게 “간접 처리”의 핵심.
선택 인터페이스: os::AppCall
pub enum AppCall<'a> { ByName(&'a str), ByIndex(usize), All }
실행 로직: Os::run()의 match call { ... }
AppCall::ByIndex(i) => loop { self.apps[i].tick(self.sys); }
AppCall::ByName(n) => loop { self.apps[idx].tick(self.sys); }
AppCall::All => loop { for a in self.apps.iter_mut() { a.tick(self.sys); } }
→ OS가 앱 선택·스케줄링을 맡고, 앱은 tick()만 구현. 사용자는 AppCall로 “무엇을 돌릴지”만 지정.
Syscalls를 둬서 간접 호출만 허용(앱은 레지스터 몰라도 됨).Os::run(AppCall::…)로 앱 호출/스케줄 정책을 OS가 담당.main은 오직 OS에 앱 목록과 정책을 넘기기만 하므로, “앱을 보는 게 아니라 작은 OS를 바라보는” 구조가 완성됨.// mini_os_app_framework.rs (LED-visible edition for STM32F446)
// - Board: STM32F446 (Cortex-M4)
// - On Nucleo-F446RE, the only on-board user LED is LD2 on PA5.
// - This edition maps BOTH logical pins (Led1, Led2) to PA5 so everything is visible.
// - Clock: assume HSI 16 MHz; tweak CYCLES_PER_MS_ESTIMATE if timing looks off.
#![no_std]
#![no_main]
#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(unused_variables)]
#![allow(non_snake_case)]
use cortex_m_rt::entry;
use panic_halt as _;
use rtt_target::{rprintln, rtt_init_print};
// ------------------------- OS Core -------------------------
mod os {
#[derive(Copy, Clone, Debug)]
pub enum GpioPin {
Led1, // mapped to PA5
Led2, // mapped to PA5 (same LED)
}
pub trait Syscalls {
fn gpio_write(&mut self, pin: GpioPin, high: bool);
fn gpio_toggle(&mut self, pin: GpioPin);
fn sleep_ms(&mut self, ms: u32);
fn now_ms(&self) -> u64;
}
pub trait App {
fn name(&self) -> &'static str;
fn init(&mut self, _sys: &mut dyn Syscalls) {}
fn tick(&mut self, sys: &mut dyn Syscalls);
}
pub enum AppCall<'a> {
ByName(&'a str),
ByIndex(usize),
All,
}
pub struct Os<'a> {
apps: &'a mut [&'a mut dyn App],
sys: &'a mut dyn Syscalls,
started: bool,
}
impl<'a> Os<'a> {
pub fn new(apps: &'a mut [&'a mut dyn App], sys: &'a mut dyn Syscalls) -> Self {
Self { apps, sys, started: false }
}
pub fn run(&'a mut self, call: AppCall<'a>) -> ! {
if !self.started {
for a in self.apps.iter_mut() { a.init(self.sys); }
self.started = true;
}
match call {
AppCall::ByIndex(mut i) => {
i %= self.apps.len();
loop { self.apps[i].tick(self.sys); }
}
AppCall::ByName(name) => {
let mut idx = 0usize;
for (i, a) in self.apps.iter().enumerate() { if a.name() == name { idx = i; break; } }
loop { self.apps[idx].tick(self.sys); }
}
AppCall::All => {
loop { for a in self.apps.iter_mut() { a.tick(self.sys); } }
}
}
}
}
}
// ------------------------- Apps ----------------------------
mod apps {
use super::os::{App, GpioPin, Syscalls};
/// 1) Boot LED show: three quick blinks on startup, then it becomes idle.
pub struct BootShowApp { done: bool }
impl BootShowApp { pub const fn new() -> Self { Self { done: false } } }
impl App for BootShowApp {
fn name(&self) -> &'static str { "boot_show" }
fn tick(&mut self, sys: &mut dyn Syscalls) {
if self.done { return; }
// 3x quick blinks to prove the board is alive (all on PA5)
for _ in 0..3 {
sys.gpio_write(GpioPin::Led1, true); sys.sleep_ms(1);
sys.gpio_write(GpioPin::Led1, false); sys.sleep_ms(1);
}
self.done = true;
}
}
/// 2) Heartbeat: steady blink forever (easy visual confirmation)
pub struct HeartbeatApp { last: u64, on: bool, period_ms: u32 }
impl HeartbeatApp { pub const fn new(period_ms: u32) -> Self { Self { last: 0, on: false, period_ms } } }
impl App for HeartbeatApp {
fn name(&self) -> &'static str { "heartbeat" }
fn tick(&mut self, sys: &mut dyn Syscalls) {
let now = sys.now_ms();
if now.wrapping_sub(self.last) >= self.period_ms as u64 {
self.on = !self.on;
// Write both Led1 and Led2 (both mapped to PA5) for maximum visibility
sys.gpio_write(GpioPin::Led1, self.on);
sys.gpio_write(GpioPin::Led2, self.on);
self.last = now;
}
sys.sleep_ms(1);
}
}
}
// -------------- Board layer: STM32F446 raw registers ---------
mod board {
use core::ptr::{read_volatile, write_volatile};
use super::os::{GpioPin, Syscalls};
use cortex_m::asm::nop;
// --- RCC base (STM32F4xx) ---
const RCC_BASE: u32 = 0x4002_3800;
const RCC_AHB1ENR: *mut u32 = (RCC_BASE + 0x30) as *mut u32; // GPIOxEN bits
// --- GPIO base ---
const GPIOA_BASE: u32 = 0x4002_0000;
// Offsets (only what we use)
const MODER_OFF: u32 = 0x00;
const OTYPER_OFF: u32 = 0x04;
const PUPDR_OFF: u32 = 0x0C;
const ODR_OFF: u32 = 0x14;
const BSRR_OFF: u32 = 0x18;
#[inline(always)]
const fn reg32(addr: u32) -> *mut u32 { addr as *mut u32 }
unsafe fn gpio_enable_clock_gpioa() {
// AHB1ENR: bit0 = GPIOAEN
let mut v = unsafe { read_volatile(RCC_AHB1ENR) };
v |= 1 << 0;
unsafe { write_volatile(RCC_AHB1ENR, v) };
for _ in 0..128 { nop(); }
}
unsafe fn gpio_set_output(port_base: u32, pin: u8) {
// MODER: 01 = output
let moder = reg32(port_base + MODER_OFF);
let mut v = unsafe { read_volatile(moder) };
let shift = (pin as u32) * 2;
v &= !(0b11 << shift);
v |= 0b01 << shift;
unsafe { write_volatile(moder, v) };
// OTYPER: push-pull
let otyper = reg32(port_base + OTYPER_OFF);
let mut v = unsafe { read_volatile(otyper) };
v &= !(1 << pin);
unsafe { write_volatile(otyper, v) };
// PUPDR: no pull
let pupdr = reg32(port_base + PUPDR_OFF);
let mut v = unsafe { read_volatile(pupdr) };
let shift2 = (pin as u32) * 2;
v &= !(0b11 << shift2);
unsafe { write_volatile(pupdr, v) };
}
unsafe fn gpio_write(port_base: u32, pin: u8, high: bool) {
let bsrr = reg32(port_base + BSRR_OFF);
let val = if high { 1u32 << pin } else { 1u32 << (pin + 16) };
unsafe { write_volatile(bsrr, val) };
}
unsafe fn gpio_toggle(port_base: u32, pin: u8) {
let odr = reg32(port_base + ODR_OFF);
let cur = unsafe { read_volatile(odr) };
let high = ((cur >> pin) & 1) == 0;
unsafe { gpio_write(port_base, pin, high) };
}
pub struct RawPin { pub(crate) port_base: u32, pub(crate) pin: u8 }
impl RawPin { pub const fn new(port_base: u32, pin: u8) -> Self { Self { port_base, pin } } }
pub struct BoardSyscalls {
led1: RawPin, // both PA5
led2: RawPin, // both PA5
time_ms: u64,
cycles_per_ms: u32,
}
impl BoardSyscalls {
pub const fn new(led1: RawPin, led2: RawPin, cycles_per_ms: u32) -> Self {
Self { led1, led2, time_ms: 0, cycles_per_ms }
}
pub unsafe fn init(&mut self) {
unsafe {
gpio_enable_clock_gpioa();
gpio_set_output(self.led1.port_base, self.led1.pin);
gpio_set_output(self.led2.port_base, self.led2.pin);
}
}
fn spin_delay(&mut self, ms: u32) {
for _ in 0..ms {
for _ in 0..self.cycles_per_ms { nop(); }
self.time_ms = self.time_ms.wrapping_add(1);
}
}
}
impl Syscalls for BoardSyscalls {
fn gpio_write(&mut self, pin: GpioPin, high: bool) {
unsafe {
match pin {
GpioPin::Led1 => gpio_write(self.led1.port_base, self.led1.pin, high),
GpioPin::Led2 => gpio_write(self.led2.port_base, self.led2.pin, high),
}
}
}
fn gpio_toggle(&mut self, pin: GpioPin) {
unsafe {
match pin {
GpioPin::Led1 => gpio_toggle(self.led1.port_base, self.led1.pin),
GpioPin::Led2 => gpio_toggle(self.led2.port_base, self.led2.pin),
}
}
}
fn sleep_ms(&mut self, ms: u32) { self.spin_delay(ms); }
fn now_ms(&self) -> u64 { self.time_ms }
}
pub const GPIOA: u32 = GPIOA_BASE;
}
// --------------------------- main ---------------------------
const CYCLES_PER_MS_ESTIMATE: u32 = 16_000; // HSI 16 MHz (tune if needed)
#[entry]
fn main() -> ! {
rtt_init_print!();
rprintln!("[mini-os] booting (LED-visible edition)");
// Map both Led1 and Led2 to PA5 so all effects are visible on LD2.
let mut syscalls = board::BoardSyscalls::new(
board::RawPin::new(board::GPIOA, 5), // Led1 → PA5
board::RawPin::new(board::GPIOA, 5), // Led2 → PA5 (same LED)
CYCLES_PER_MS_ESTIMATE,
);
unsafe { syscalls.init(); }
rprintln!("GPIO ready: PA5 configured as output (LD2)");
// Apps: boot triple blink, then steady heartbeat (250 ms period)
let mut app_boot = apps::BootShowApp::new();
let mut app_beat = apps::HeartbeatApp::new(4);
let mut app_list: [&mut dyn os::App; 2] = [ &mut app_boot, &mut app_beat ];
let mut kernel = os::Os::new(&mut app_list, &mut syscalls);
kernel.run(os::AppCall::All)
}