Skip to content

Microcontroller Programming Hardware

MMIO, Registers, và Portable Drivers

1. Memory-Mapped I/O (MMIO)

Concept

                    MEMORY-MAPPED I/O
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   CPU Address Space                                             │
│   ─────────────────                                             │
│                                                                 │
│   0x0000_0000 ┌───────────────────────┐                         │
│               │        Flash          │  ← Code, constants      │
│   0x0800_0000 ├───────────────────────┤                         │
│               │         RAM           │  ← Variables, stack     │
│   0x2000_0000 ├───────────────────────┤                         │
│               │                       │                         │
│               │    Peripheral Regs    │  ← GPIO, UART, SPI...   │
│               │                       │                         │
│   0x4000_0000 │   ┌─────────────────┐ │                         │
│               │   │ GPIO Port A    │ │  0x4002_0000            │
│               │   │ GPIO Port B    │ │  0x4002_0400            │
│               │   │ UART1          │ │  0x4001_1000            │
│               │   │ SPI1           │ │  0x4001_3000            │
│               │   └─────────────────┘ │                         │
│   0x5000_0000 └───────────────────────┘                         │
│                                                                 │
│   Reading/Writing to peripheral address = Hardware operation    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Raw Pointer Access

rust
use core::ptr::{read_volatile, write_volatile};

// STM32 GPIO Port A base address
const GPIOA_BASE: usize = 0x4002_0000;

// Register offsets
const GPIO_MODER: usize = 0x00;   // Mode register
const GPIO_ODR: usize = 0x14;     // Output data register
const GPIO_IDR: usize = 0x10;     // Input data register

/// Set pin as output
fn set_pin_output(pin: u8) {
    let moder = (GPIOA_BASE + GPIO_MODER) as *mut u32;
    
    unsafe {
        let mut value = read_volatile(moder);
        // Clear bits (2 bits per pin)
        value &= !(0b11 << (pin * 2));
        // Set as output (01)
        value |= 0b01 << (pin * 2);
        write_volatile(moder, value);
    }
}

/// Set pin high
fn set_pin_high(pin: u8) {
    let odr = (GPIOA_BASE + GPIO_ODR) as *mut u32;
    
    unsafe {
        let mut value = read_volatile(odr);
        value |= 1 << pin;
        write_volatile(odr, value);
    }
}

/// Read pin state
fn read_pin(pin: u8) -> bool {
    let idr = (GPIOA_BASE + GPIO_IDR) as *const u32;
    
    unsafe {
        let value = read_volatile(idr);
        (value & (1 << pin)) != 0
    }
}

Type-Safe Register Access

rust
use core::ptr::{read_volatile, write_volatile};

/// GPIO Mode values
#[repr(u8)]
pub enum GpioMode {
    Input = 0b00,
    Output = 0b01,
    Alternate = 0b10,
    Analog = 0b11,
}

/// Type-safe GPIO register block
#[repr(C)]
pub struct GpioRegisters {
    pub moder: u32,    // 0x00
    pub otyper: u32,   // 0x04
    pub ospeedr: u32,  // 0x08
    pub pupdr: u32,    // 0x0C
    pub idr: u32,      // 0x10
    pub odr: u32,      // 0x14
    pub bsrr: u32,     // 0x18
    pub lckr: u32,     // 0x1C
    pub afrl: u32,     // 0x20
    pub afrh: u32,     // 0x24
}

impl GpioRegisters {
    /// Get GPIO port by base address
    pub fn get(base: usize) -> &'static mut Self {
        unsafe { &mut *(base as *mut Self) }
    }
    
    /// Configure pin mode
    pub fn set_mode(&mut self, pin: u8, mode: GpioMode) {
        unsafe {
            let mut value = read_volatile(&self.moder);
            value &= !(0b11 << (pin * 2));
            value |= (mode as u32) << (pin * 2);
            write_volatile(&mut self.moder, value);
        }
    }
    
    /// Set pin high (atomic, no read-modify-write)
    pub fn set_high(&mut self, pin: u8) {
        unsafe {
            write_volatile(&mut self.bsrr, 1 << pin);
        }
    }
    
    /// Set pin low (atomic)
    pub fn set_low(&mut self, pin: u8) {
        unsafe {
            write_volatile(&mut self.bsrr, 1 << (pin + 16));
        }
    }
}

// Usage
fn blink_led() {
    let gpioa = GpioRegisters::get(0x4002_0000);
    gpioa.set_mode(5, GpioMode::Output); // PA5 = LED
    
    loop {
        gpioa.set_high(5);
        delay(1_000_000);
        gpioa.set_low(5);
        delay(1_000_000);
    }
}

2. Peripheral Access Crates (PAC)

Auto-generated từ SVD

rust
// Sử dụng stm32f4 PAC (generated từ SVD file)
use stm32f4::stm32f401;

fn configure_gpio() {
    // Get peripheral singleton
    let dp = stm32f401::Peripherals::take().unwrap();
    
    // Enable GPIOA clock
    dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());
    
    // Configure PA5 as output
    dp.GPIOA.moder.modify(|_, w| w.moder5().output());
    
    // Set high
    dp.GPIOA.odr.modify(|_, w| w.odr5().high());
}

PAC với Type-State

rust
// PAC registers có type-safe API
dp.GPIOA.moder.modify(|_, w| {
    w.moder0().input()      // Pin 0: input
     .moder1().output()     // Pin 1: output
     .moder2().alternate()  // Pin 2: alternate function
     .moder3().analog()     // Pin 3: analog
});

// Bit manipulation helpers
dp.GPIOA.bsrr.write(|w| {
    w.bs5().set_bit()       // Set pin 5 high
     .br6().set_bit()       // Set pin 6 low
});

3. embedded-hal Traits

Tại sao cần HAL Traits?

                    PORTABLE DRIVER ARCHITECTURE
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   Application Layer                                             │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │              LED Driver, Sensor Driver                  │   │
│   │          (Uses embedded-hal traits only)                │   │
│   └─────────────────────────────────────────────────────────┘   │
│                              │                                  │
│                              ▼                                  │
│   HAL Traits (embedded-hal)                                     │
│   ┌─────────────────────────────────────────────────────────┐   │
│   │   OutputPin    InputPin    SpiDevice    I2c    Delay    │   │
│   └─────────────────────────────────────────────────────────┘   │
│           │             │            │         │                │
│           ▼             ▼            ▼         ▼                │
│   HAL Implementations                                           │
│   ┌─────────┐     ┌─────────┐     ┌─────────┐                   │
│   │ stm32-  │     │  esp-   │     │  rp2040 │                   │
│   │  hal    │     │   hal   │     │  -hal   │                   │
│   └─────────┘     └─────────┘     └─────────┘                   │
│                                                                 │
│   Driver code = portable across all platforms!                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Core HAL Traits

rust
// embedded-hal 1.0 traits

/// Digital output pin
pub trait OutputPin {
    type Error;
    fn set_low(&mut self) -> Result<(), Self::Error>;
    fn set_high(&mut self) -> Result<(), Self::Error>;
}

/// Digital input pin
pub trait InputPin {
    type Error;
    fn is_high(&mut self) -> Result<bool, Self::Error>;
    fn is_low(&mut self) -> Result<bool, Self::Error>;
}

/// Blocking delay
pub trait DelayNs {
    fn delay_ns(&mut self, ns: u32);
    fn delay_us(&mut self, us: u32) {
        self.delay_ns(us * 1000);
    }
    fn delay_ms(&mut self, ms: u32) {
        self.delay_ns(ms * 1_000_000);
    }
}

/// SPI device
pub trait SpiDevice {
    type Error;
    fn transaction(&mut self, operations: &mut [Operation]) 
        -> Result<(), Self::Error>;
}

Writing Portable Drivers

rust
use embedded_hal::digital::OutputPin;
use embedded_hal::delay::DelayNs;

/// LED driver - works on ANY platform implementing embedded-hal
pub struct Led<P: OutputPin> {
    pin: P,
}

impl<P: OutputPin> Led<P> {
    pub fn new(pin: P) -> Self {
        Self { pin }
    }
    
    pub fn on(&mut self) -> Result<(), P::Error> {
        self.pin.set_high()
    }
    
    pub fn off(&mut self) -> Result<(), P::Error> {
        self.pin.set_low()
    }
    
    pub fn blink<D: DelayNs>(&mut self, delay: &mut D, ms: u32) 
        -> Result<(), P::Error> 
    {
        self.on()?;
        delay.delay_ms(ms);
        self.off()?;
        delay.delay_ms(ms);
        Ok(())
    }
}

// Usage trên STM32
use stm32f4xx_hal::{gpio::PA5, prelude::*};

fn main() {
    let pin: PA5<Output<PushPull>> = /* configure */;
    let mut led = Led::new(pin);
    led.on().unwrap();
}

// Usage trên ESP32 - CÙNG driver code!
use esp_hal::gpio::GpioPin;

fn main() {
    let pin: GpioPin<Output, 5> = /* configure */;
    let mut led = Led::new(pin);
    led.on().unwrap();
}

4. Interrupt Handling

Interrupt Vector Table

rust
use cortex_m_rt::entry;
use stm32f4xx_hal::{pac, prelude::*, interrupt};

#[entry]
fn main() -> ! {
    let dp = pac::Peripherals::take().unwrap();
    
    // Configure external interrupt on PA0
    dp.SYSCFG.exticr1.modify(|_, w| w.exti0().pa0());
    dp.EXTI.imr.modify(|_, w| w.mr0().unmasked());
    dp.EXTI.rtsr.modify(|_, w| w.tr0().enabled()); // Rising edge
    
    // Enable interrupt in NVIC
    unsafe {
        cortex_m::peripheral::NVIC::unmask(pac::Interrupt::EXTI0);
    }
    
    loop {
        cortex_m::asm::wfi(); // Wait for interrupt
    }
}

/// Interrupt handler
#[interrupt]
fn EXTI0() {
    // Clear interrupt flag
    let dp = unsafe { pac::Peripherals::steal() };
    dp.EXTI.pr.write(|w| w.pr0().clear());
    
    // Handle button press
    toggle_led();
}

Critical Sections

rust
use cortex_m::interrupt;

static mut COUNTER: u32 = 0;

fn increment_counter() {
    // Disable interrupts during access
    interrupt::free(|_cs| {
        unsafe {
            COUNTER += 1;
        }
    });
}

// Với RTIC framework
#[rtic::app(device = stm32f4::stm32f401)]
mod app {
    #[shared]
    struct Shared {
        counter: u32,
    }
    
    #[task(shared = [counter])]
    fn task1(mut cx: task1::Context) {
        cx.shared.counter.lock(|counter| {
            *counter += 1;
        });
    }
}

rust
#![no_std]
#![no_main]

use cortex_m_rt::entry;
use panic_halt as _;
use stm32f4xx_hal::{pac, prelude::*};

#[entry]
fn main() -> ! {
    // Take peripherals
    let dp = pac::Peripherals::take().unwrap();
    let cp = cortex_m::Peripherals::take().unwrap();
    
    // Configure clocks
    let rcc = dp.RCC.constrain();
    let clocks = rcc.cfgr.sysclk(84.MHz()).freeze();
    
    // Configure GPIO
    let gpioa = dp.GPIOA.split();
    let mut led = gpioa.pa5.into_push_pull_output();
    
    // Create delay
    let mut delay = cp.SYST.delay(&clocks);
    
    // Blink forever
    loop {
        led.set_high();
        delay.delay_ms(500u32);
        led.set_low();
        delay.delay_ms(500u32);
    }
}

🎯 Best Practices

PracticeReason
Use PAC/HALType-safe, less bugs than raw pointers
Use embedded-halPortable drivers
Minimize unsafeContain all unsafe in HAL layer
Critical sectionsProtect shared state
Check errataHardware has bugs too!