~9.8 study 2

Nitroblue 1·2025년 9월 8일

Mini OS with 2 applications Code study

실제 코드는 맨 아랫 부분 참고.

겁나 어렵다. 어렵기도 어려운데, 설계 디테일 자체가 엄청 깊고 세심한 것 같다.
대충 어떤 식으로 설계를 해야 하는 지 좀 감이 온다.

코드 설명

  • App 2개 구현
    • BootShowApp : 처음 OS 동작시 빠르게 3번 blink.
    • HearbeatApp : 부팅 후 4ms마다 깜빡임.
  • main.rs에 들어가면 이 앱을 보는 게 아니라 작은 OS를 바라볼 수 있게, 그래서 메모리 주소에 직접 닿지 않고, 내가 원하는 앱을 call해서 간접적으로 처리할 수 있도록 구현.
    — Hierarchy : OS 코어 ↔ 앱 ↔ 보드(레지스터).

1) “main.rs에서 앱이 아니라 작은 OS를 본다”

  • 코드: mod osstruct 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”임.

2) “메모리 주소에 직접 닿지 않는다 → 간접 호출”

  • 앱 코드: mod appsLedBlinkApp, LedSosApp, HeartbeatApp
    앱들은 오직 Syscalls 인터페이스로만 하드웨어를 만짐:

    sys.gpio_write(GpioPin::Led1, true);
    sys.sleep_ms(200);

    레지스터 주소/비트 연산 없음.

  • 하드웨어 접근은 전부 mod boardimpl Syscalls for BoardSyscalls 안으로 격리:

    match pin {
        GpioPin::Led1 => unsafe { write_volatile(...); } // 레지스터 접근
        GpioPin::Led2 => unsafe { write_volatile(...); }
    }

    → 앱은 “OS가 제공하는 시스템콜”만 부르고, 레지스터는 보드 계층만 다룸. 이게 “간접 처리”의 핵심.

3) “원하는 앱을 call해서 간접적으로 처리”

  • 선택 인터페이스: 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)
}

0개의 댓글