Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MMIO and Volatile Register Access
MMIO 与 volatile 寄存器访问

What you’ll learn: Type-safe hardware register access in embedded Rust — volatile MMIO patterns, register abstraction crates, and how Rust’s type system can encode register permissions that C’s volatile keyword cannot.
本章将学到什么: 在嵌入式 Rust 里怎样以类型安全的方式访问硬件寄存器,包括 volatile MMIO 的基本模式、寄存器抽象 crate 的用法,以及 Rust 类型系统怎样表达 C 里单靠 volatile 根本表达不清的寄存器权限。

In C firmware, hardware registers are usually accessed through volatile pointers aimed at fixed memory addresses. Rust has equivalent mechanisms, but it can wrap them in much stronger type guarantees.
在 C 固件里,硬件寄存器通常就是靠指向固定内存地址的 volatile 指针访问。Rust 也有对应手段,但它能把这件事包进更强的类型约束里,而不是全靠人肉小心。

C volatile vs Rust volatile
C 的 volatile 和 Rust 的 volatile

// C — typical MMIO register access
#define GPIO_BASE     0x40020000
#define GPIO_MODER    (*(volatile uint32_t*)(GPIO_BASE + 0x00))
#define GPIO_ODR      (*(volatile uint32_t*)(GPIO_BASE + 0x14))

void toggle_led(void) {
    GPIO_ODR ^= (1 << 5);  // Toggle pin 5
}
#![allow(unused)]
fn main() {
// Rust — raw volatile (low-level, rarely used directly)
use core::ptr;

const GPIO_BASE: usize = 0x4002_0000;
const GPIO_ODR: *mut u32 = (GPIO_BASE + 0x14) as *mut u32;

/// # Safety
/// Caller must ensure GPIO_BASE is a valid mapped peripheral address.
unsafe fn toggle_led() {
    // SAFETY: GPIO_ODR is a valid memory-mapped register address.
    let current = unsafe { ptr::read_volatile(GPIO_ODR) };
    unsafe { ptr::write_volatile(GPIO_ODR, current ^ (1 << 5)) };
}
}

svd2rust — Type-Safe Register Access
svd2rust:类型安全的寄存器访问方式

In practice, raw volatile pointers are rarely written by hand. The normal Rust way is to let svd2rust generate a Peripheral Access Crate from the chip’s SVD file.
真到实际项目里,几乎没人愿意手写这种原始 volatile 指针。更正常的 Rust 路子,是让 svd2rust 根据芯片的 SVD 文件生成一个外设访问 crate。

#![allow(unused)]
fn main() {
// Generated PAC code (you don't write this — svd2rust does)
// The PAC makes invalid register access a compile error

// Usage with PAC:
use stm32f4::stm32f401;  // PAC crate for your chip

fn configure_gpio(dp: stm32f401::Peripherals) {
    // Enable GPIOA clock — type-safe, no magic numbers
    dp.RCC.ahb1enr.modify(|_, w| w.gpioaen().enabled());

    // Set pin 5 to output — can't accidentally write to a read-only field
    dp.GPIOA.moder.modify(|_, w| w.moder5().output());

    // Toggle pin 5 — type-checked field access
    dp.GPIOA.odr.modify(|r, w| {
        // SAFETY: toggling a single bit in a valid register field.
        unsafe { w.bits(r.bits() ^ (1 << 5)) }
    });
}
}
C register accessRust PAC equivalent
#define REG (*(volatile uint32_t*)ADDR)PAC crate generated by svd2rust
svd2rust 生成的 PAC crate
`REG= BITMASK;`
value = REG;let val = periph.reg.read().field().bits()
读寄存器后再取字段
Wrong register field → silent UBCompile error — field does not exist
字段写错直接编译不过
Wrong register width → silent UBType-checked width like u8 / u16 / u32
位宽也由类型系统校验

Interrupt Handling and Critical Sections
中断处理与临界区

C 固件里通常会写 __disable_irq() / __enable_irq() 以及特定命名的 ISR。Rust 也有对应能力,但会把不少约束直接拉到类型系统层面。
这样一来,很多以前靠文档和命名约定维持的东西,会变成编译器帮忙盯着的规则。

C vs Rust Interrupt Patterns
C 与 Rust 的中断模式对比

// C — traditional interrupt handler
volatile uint32_t tick_count = 0;

void SysTick_Handler(void) {   // Naming convention is critical — get it wrong → HardFault
    tick_count++;
}

uint32_t get_ticks(void) {
    __disable_irq();
    uint32_t t = tick_count;   // Read inside critical section
    __enable_irq();
    return t;
}
#![allow(unused)]
fn main() {
// Rust — using cortex-m and critical sections
use core::cell::Cell;
use cortex_m::interrupt::{self, Mutex};

// Shared state protected by a critical-section Mutex
static TICK_COUNT: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));

#[cortex_m_rt::exception]     // Attribute ensures correct vector table placement
fn SysTick() {                // Compile error if name doesn't match a valid exception
    interrupt::free(|cs| {    // cs = critical section token (proof IRQs disabled)
        let count = TICK_COUNT.borrow(cs).get();
        TICK_COUNT.borrow(cs).set(count + 1);
    });
}

fn get_ticks() -> u32 {
    interrupt::free(|cs| TICK_COUNT.borrow(cs).get())
}
}

RTIC — Real-Time Interrupt-driven Concurrency
RTIC:实时中断驱动并发

For more complex firmware with multiple interrupt priorities, RTIC provides compile-time scheduling and resource locking with zero runtime overhead.
如果固件里有多级中断优先级、共享资源和更复杂的调度关系,RTIC 就很有价值。它把调度和资源访问规则尽量前移到编译期,而且基本没有额外运行时成本。

#![allow(unused)]
fn main() {
#[rtic::app(device = stm32f4xx_hal::pac, dispatchers = [USART1])]
mod app {
    use stm32f4xx_hal::prelude::*;

    #[shared]
    struct Shared {
        temperature: f32,   // Shared between tasks — RTIC manages locking
    }

    #[local]
    struct Local {
        led: stm32f4xx_hal::gpio::Pin<'A', 5, stm32f4xx_hal::gpio::Output>,
    }

    #[init]
    fn init(cx: init::Context) -> (Shared, Local) {
        let dp = cx.device;
        let gpioa = dp.GPIOA.split();
        let led = gpioa.pa5.into_push_pull_output();
        (Shared { temperature: 25.0 }, Local { led })
    }

    // Hardware task: runs on SysTick interrupt
    #[task(binds = SysTick, shared = [temperature], local = [led])]
    fn tick(mut cx: tick::Context) {
        cx.local.led.toggle();
        cx.shared.temperature.lock(|temp| {
            // RTIC guarantees exclusive access here — no manual locking needed
            *temp += 0.1;
        });
    }
}
}

Why RTIC matters for C firmware developers:
为什么 RTIC 对 C 固件开发者很重要:

  • The #[shared] annotation replaces a lot of manual mutex bookkeeping.
    #[shared] 这类标注,能替掉很多手写锁管理样板。
  • Priority-based preemption is planned at compile time instead of by ad-hoc runtime discipline.
    基于优先级的抢占关系在编译期就确定下来,不用在运行时靠人硬维持。
  • Deadlock freedom is one of the big selling points: the framework can prove a lot of locking properties statically.
    它的一大卖点就是很多锁相关性质能静态证明,死锁空间被压得很小。
  • ISR naming mistakes become compile errors rather than mysterious HardFaults.
    中断函数名写错这种事,也更容易在编译阶段暴露,而不是等到板子上硬炸。

Panic Handler Strategies
panic handler 策略

In C firmware, fatal failures often end in reset loops or blinking LEDs. Rust gives panic handling a structured hook so projects can choose a deliberate failure strategy.
C 固件里,出大问题时通常就是复位、死循环或者闪灯报警。Rust 则把这件事做成了明确的 panic handler 入口,让项目能选更清晰的故障策略。

#![allow(unused)]
fn main() {
// Strategy 1: Halt (for debugging — attach debugger, inspect state)
use panic_halt as _;  // Infinite loop on panic

// Strategy 2: Reset the MCU
use panic_reset as _;  // Triggers system reset

// Strategy 3: Log via probe (development)
use panic_probe as _;  // Sends panic info over debug probe (with defmt)

// Strategy 4: Log over defmt then halt
use defmt_panic as _;  // Rich panic messages over ITM/RTT

// Strategy 5: Custom handler (production firmware)
use core::panic::PanicInfo;

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // 1. Disable interrupts to prevent further damage
    cortex_m::interrupt::disable();

    // 2. Write panic info to a reserved RAM region (survives reset)
    // SAFETY: PANIC_LOG is a reserved memory region defined in linker script.
    unsafe {
        let log = 0x2000_0000 as *mut [u8; 256];
        // Write truncated panic message
        use core::fmt::Write;
        let mut writer = FixedWriter::new(&mut *log);
        let _ = write!(writer, "{}", info);
    }

    // 3. Trigger watchdog reset (or blink error LED)
    loop {
        cortex_m::asm::wfi();  // Wait for interrupt (low power while halted)
    }
}
}

Linker Scripts and Memory Layout
linker script 与内存布局

Embedded Rust still uses the same basic memory layout concepts that C firmware does. The usual Rust-facing entry point is a memory.x file.
嵌入式 Rust 在内存布局这件事上,并没有脱离 C 固件世界。该写 FLASH、RAM 起始地址和大小,还是得写,只是入口通常换成了 memory.x

/* memory.x — placed at crate root, consumed by cortex-m-rt */
MEMORY
{
  /* Adjust for your MCU — these are STM32F401 values */
  FLASH : ORIGIN = 0x08000000, LENGTH = 512K
  RAM   : ORIGIN = 0x20000000, LENGTH = 96K
}

/* Optional: reserve space for panic log (see panic handler above) */
_panic_log_start = ORIGIN(RAM);
_panic_log_size  = 256;
# .cargo/config.toml — set the target and linker flags
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32F401RE"  # flash and run via debug probe
rustflags = [
    "-C", "link-arg=-Tlink.x",              # cortex-m-rt linker script
]

[build]
target = "thumbv7em-none-eabihf"            # Cortex-M4F with hardware FPU
C linker scriptRust equivalent
MEMORY { FLASH ..., RAM ... }memory.x at crate root
根目录下的 memory.x
__attribute__((section(".data")))#[link_section = ".data"]
-T linker.ld in Makefile-C link-arg=-Tlink.x in .cargo/config.toml
__bss_start__, __bss_end__Usually handled by cortex-m-rt
很多基础启动细节由 cortex-m-rt 处理
Startup assembly file#[entry] and runtime support from cortex-m-rt
入口由运行时 crate 接管

Writing embedded-hal Drivers
编写 embedded-hal 驱动

The embedded-hal crate defines standard traits for SPI, I2C, GPIO, UART, and more. A driver written against those traits can often run on many different microcontrollers unchanged.
embedded-hal 定义了一套 SPI、I2C、GPIO、UART 等外设的标准 trait。只要驱动写在这套 trait 之上,它通常就能跨很多 MCU 复用,这就是 Rust 嵌入式生态最值钱的地方之一。

C vs Rust: A Temperature Sensor Driver
C 与 Rust 对比:温度传感器驱动

// C — driver tightly coupled to STM32 HAL
#include "stm32f4xx_hal.h"

float read_temperature(I2C_HandleTypeDef* hi2c, uint8_t addr) {
    uint8_t buf[2];
    HAL_I2C_Mem_Read(hi2c, addr << 1, 0x00, I2C_MEMADD_SIZE_8BIT,
                     buf, 2, HAL_MAX_DELAY);
    int16_t raw = ((int16_t)buf[0] << 4) | (buf[1] >> 4);
    return raw * 0.0625;
}
// Problem: This driver ONLY works with STM32 HAL. Porting to Nordic = rewrite.
#![allow(unused)]
fn main() {
// Rust — driver works on ANY MCU that implements embedded-hal
use embedded_hal::i2c::I2c;

pub struct Tmp102<I2C> {
    i2c: I2C,
    address: u8,
}

impl<I2C: I2c> Tmp102<I2C> {
    pub fn new(i2c: I2C, address: u8) -> Self {
        Self { i2c, address }
    }

    pub fn read_temperature(&mut self) -> Result<f32, I2C::Error> {
        let mut buf = [0u8; 2];
        self.i2c.write_read(self.address, &[0x00], &mut buf)?;
        let raw = ((buf[0] as i16) << 4) | ((buf[1] as i16) >> 4);
        Ok(raw as f32 * 0.0625)
    }
}

// Works on STM32, Nordic nRF, ESP32, RP2040 — any chip with an embedded-hal I2C impl
}
graph TD
    subgraph "C Driver Architecture<br/>C 驱动结构"
        CD["Temperature Driver<br/>温度驱动"]
        CD --> STM["STM32 HAL"]
        CD -.->|"Port = REWRITE<br/>移植基本重写"| NRF["Nordic HAL"]
        CD -.->|"Port = REWRITE<br/>移植基本重写"| ESP["ESP-IDF"]
    end
    
    subgraph "Rust embedded-hal Architecture<br/>Rust embedded-hal 结构"
        RD["Temperature Driver<br/>impl&lt;I2C: I2c&gt;"]
        RD --> EHAL["embedded-hal::I2c trait"]
        EHAL --> STM2["stm32f4xx-hal"]
        EHAL --> NRF2["nrf52-hal"]
        EHAL --> ESP2["esp-hal"]
        EHAL --> RP2["rp2040-hal"]
        NOTE["Write driver ONCE,<br/>runs on ALL chips<br/>驱动写一次,多平台复用"]
    end
    
    style CD fill:#ffa07a,color:#000
    style RD fill:#91e5a3,color:#000
    style EHAL fill:#91e5a3,color:#000
    style NOTE fill:#91e5a3,color:#000

Global Allocator Setup
全局分配器配置

The alloc crate gives VecString and Box, but on bare-metal targets the program still has to define where heap memory comes from.
alloc crate 能带来 VecStringBox 这些堆类型,但在裸机环境里,程序仍然要自己说明“堆内存到底从哪来”。

#![no_std]
extern crate alloc;

use alloc::vec::Vec;
use alloc::string::String;
use embedded_alloc::LlffHeap as Heap;

#[global_allocator]
static HEAP: Heap = Heap::empty();

#[cortex_m_rt::entry]
fn main() -> ! {
    // Initialize the allocator with a memory region
    // (typically a portion of RAM not used by stack or static data)
    {
        const HEAP_SIZE: usize = 4096;
        static mut HEAP_MEM: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
        // SAFETY: HEAP_MEM is only accessed here during init, before any allocation.
        unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
    }

    // Now you can use heap types!
    let mut log_buffer: Vec<u8> = Vec::with_capacity(256);
    let name: String = String::from("sensor_01");
    // ...

    loop {}
}
C heap setupRust equivalent
Custom malloc() or _sbrk()#[global_allocator] plus Heap::init()
注册全局分配器并手动初始化
configTOTAL_HEAP_SIZE in FreeRTOSHEAP_SIZE constant
pvPortMalloc()Using Vec::new() and friends
堆类型自动走全局分配器
Heap exhaustion → chaos or custom behavioralloc_error_handler or controlled panic path
可以统一走受控失败策略

Mixed no_std + std Workspaces
混合 no_stdstd 的 workspace

Real embedded projects often split code into several crates, some targeting the MCU directly and others targeting a host environment like Linux.
真实项目里,很常见的一种拆法是:一部分 crate 直接跑在 MCU 上,另一部分 crate 跑在 Linux 这种宿主环境里,两边共享协议和核心逻辑。

workspace_root/
├── Cargo.toml              # [workspace] members = [...]
├── protocol/               # no_std — wire protocol, parsing
│   ├── Cargo.toml          # no default-features, no std
│   └── src/lib.rs          # #![no_std]
├── driver/                 # no_std — hardware abstraction
│   ├── Cargo.toml
│   └── src/lib.rs          # #![no_std], uses embedded-hal traits
├── firmware/               # no_std — MCU binary
│   ├── Cargo.toml          # depends on protocol, driver
│   └── src/main.rs         # #![no_std] #![no_main]
└── host_tool/              # std — Linux CLI tool
    ├── Cargo.toml          # depends on protocol (same crate!)
    └── src/main.rs         # Uses std::fs, std::net, etc.

The key pattern is that shared crates like protocol stay no_std, so the same parsing or packet code can be compiled for both firmware and host tools without duplication.
这里最关键的设计点,是把像 protocol 这种共享逻辑做成 no_std,这样固件和宿主工具都能直接复用同一份代码,不用各写一套。

# protocol/Cargo.toml
[package]
name = "protocol"

[features]
default = []
std = []  # Optional: enable std-specific features when building for host

[dependencies]
serde = { version = "1", default-features = false, features = ["derive"] }
# Note: default-features = false drops serde's std dependency
#![allow(unused)]
fn main() {
// protocol/src/lib.rs
#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(feature = "std")]
extern crate std;

extern crate alloc;
use alloc::vec::Vec;
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct DiagPacket {
    pub sensor_id: u16,
    pub value: i32,
    pub fault_code: u16,
}

// This function works in both no_std and std contexts
pub fn parse_packet(data: &[u8]) -> Result<DiagPacket, &'static str> {
    if data.len() < 8 {
        return Err("packet too short");
    }
    Ok(DiagPacket {
        sensor_id: u16::from_le_bytes([data[0], data[1]]),
        value: i32::from_le_bytes([data[2], data[3], data[4], data[5]]),
        fault_code: u16::from_le_bytes([data[6], data[7]]),
    })
}
}

Exercise: Hardware Abstraction Layer Driver
练习:硬件抽象层驱动

Write a no_std driver for a hypothetical LED controller that communicates over SPI and is generic over any embedded-hal SPI implementation.
写一个 no_std 驱动,目标设备是假想的 SPI LED 控制器,而且这个驱动要对任意实现了 embedded-hal SPI trait 的底层都通用。

Requirements:
要求如下:

  1. Define a LedController<SPI> struct.
    定义一个 LedController<SPI> 结构体。
  2. Implement new()set_brightness(led: u8, brightness: u8) and all_off().
    实现 new()set_brightness(led: u8, brightness: u8)all_off()
  3. The SPI protocol is a 2-byte transaction: [led_index, brightness_value].
    SPI 协议规定每次发两个字节:[led_index, brightness_value]
  4. Write tests using a mock SPI implementation.
    再给它写一套基于 mock SPI 的测试。
#![allow(unused)]
fn main() {
// Starter code
#![no_std]
use embedded_hal::spi::SpiDevice;

pub struct LedController<SPI> {
    spi: SPI,
    num_leds: u8,
}

// TODO: Implement new(), set_brightness(), all_off()
// TODO: Create MockSpi for testing
}
Solution 参考答案
#![allow(unused)]
#![no_std]
fn main() {
use embedded_hal::spi::SpiDevice;

pub struct LedController<SPI> {
    spi: SPI,
    num_leds: u8,
}

impl<SPI: SpiDevice> LedController<SPI> {
    pub fn new(spi: SPI, num_leds: u8) -> Self {
        Self { spi, num_leds }
    }

    pub fn set_brightness(&mut self, led: u8, brightness: u8) -> Result<(), SPI::Error> {
        if led >= self.num_leds {
            return Ok(()); // Silently ignore out-of-range LEDs
        }
        self.spi.write(&[led, brightness])
    }

    pub fn all_off(&mut self) -> Result<(), SPI::Error> {
        for led in 0..self.num_leds {
            self.spi.write(&[led, 0])?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    // Mock SPI that records all transactions
    struct MockSpi {
        transactions: Vec<Vec<u8>>,
    }

    // Minimal error type for mock
    #[derive(Debug)]
    struct MockError;
    impl embedded_hal::spi::Error for MockError {
        fn kind(&self) -> embedded_hal::spi::ErrorKind {
            embedded_hal::spi::ErrorKind::Other
        }
    }

    impl embedded_hal::spi::ErrorType for MockSpi {
        type Error = MockError;
    }

    impl SpiDevice for MockSpi {
        fn write(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
            self.transactions.push(buf.to_vec());
            Ok(())
        }
        fn read(&mut self, _buf: &mut [u8]) -> Result<(), Self::Error> { Ok(()) }
        fn transfer(&mut self, _r: &mut [u8], _w: &[u8]) -> Result<(), Self::Error> { Ok(()) }
        fn transfer_in_place(&mut self, _buf: &mut [u8]) -> Result<(), Self::Error> { Ok(()) }
        fn transaction(&mut self, _ops: &mut [embedded_hal::spi::Operation<'_, u8>]) -> Result<(), Self::Error> { Ok(()) }
    }

    #[test]
    fn test_set_brightness() {
        let mock = MockSpi { transactions: vec![] };
        let mut ctrl = LedController::new(mock, 4);
        ctrl.set_brightness(2, 128).unwrap();
        assert_eq!(ctrl.spi.transactions, vec![vec![2, 128]]);
    }

    #[test]
    fn test_all_off() {
        let mock = MockSpi { transactions: vec![] };
        let mut ctrl = LedController::new(mock, 3);
        ctrl.all_off().unwrap();
        assert_eq!(ctrl.spi.transactions, vec![
            vec![0, 0], vec![1, 0], vec![2, 0],
        ]);
    }

    #[test]
    fn test_out_of_range_led() {
        let mock = MockSpi { transactions: vec![] };
        let mut ctrl = LedController::new(mock, 2);
        ctrl.set_brightness(5, 255).unwrap(); // Out of range — ignored
        assert!(ctrl.spi.transactions.is_empty());
    }
}
}

Debugging Embedded Rust — probe-rs, defmt, and VS Code
调试嵌入式 Rust:probe-rs、defmt 与 VS Code

C firmware developers often use OpenOCD + GDB or vendor IDEs. The Rust embedded ecosystem has increasingly converged around probe-rs as a more unified toolchain front end.
很多 C 固件开发者平时靠 OpenOCD + GDB,或者厂商自己的 IDE。Rust 嵌入式这边这几年越来越统一到 probe-rs 这条线上,整体体验会集中一些。

probe-rs — The All-in-One Debug Probe Tool
probe-rs:一站式调试探针工具

probe-rs effectively replaces the OpenOCD + GDB split setup for many workflows. It supports CMSIS-DAP, ST-Link, J-Link, and other common probes out of the box.
在很多工作流里,probe-rs 基本就是拿来替掉 OpenOCD + GDB 这套组合的。CMSIS-DAP、ST-Link、J-Link 这些常见探针它都能直接支持。

# Install probe-rs (includes cargo-flash and cargo-embed)
cargo install probe-rs-tools

# Flash and run your firmware
cargo flash --chip STM32F401RE --release

# Flash, run, and open RTT (Real-Time Transfer) console
cargo embed --chip STM32F401RE

probe-rs vs OpenOCD + GDB:
probe-rs 和 OpenOCD + GDB 的对比:

AspectOpenOCD + GDBprobe-rs
InstallTwo separate tools plus scripts
通常要装两套工具再拼配置
cargo install probe-rs-tools
Config.cfg files per board/probe
每块板子和探针都得配文件
--chip or Embed.toml
芯片名加项目配置即可
Console outputSemihosting, often slow
半主机输出比较慢
RTT, much faster
RTT 更快
Log frameworkUsually printf or ad-hoc logs
多半还是 printf 风格
defmt integration
defmt 配合更自然
Flash algorithmsOften tied to external packs
常依赖外部包
Built-in support for many chips
GDB supportNativeAvailable through probe-rs gdb

Embed.toml — Project Configuration
Embed.toml:项目级配置

Instead of juggling multiple OpenOCD and GDB config files, probe-rs can centralize the setup in one Embed.toml file.
以前那种 .cfg.gdbinit 到处飞的局面,在 probe-rs 这边通常可以收束到一个 Embed.toml 里。

# Embed.toml — placed in your project root
[default.general]
chip = "STM32F401RETx"

[default.rtt]
enabled = true           # Enable Real-Time Transfer console
channels = [
    { up = 0, mode = "BlockIfFull", name = "Terminal" },
]

[default.flashing]
enabled = true           # Flash before running
restore_unwritten_bytes = false

[default.reset]
halt_afterwards = false  # Start running after flash + reset

[default.gdb]
enabled = false          # Set true to expose GDB server on :1337
gdb_connection_string = "127.0.0.1:1337"
# With Embed.toml, just run:
cargo embed              # Flash + RTT console — zero flags needed
cargo embed --release    # Release build

defmt — Deferred Formatting for Embedded Logging
defmt:嵌入式日志里的延迟格式化

defmt stores format strings in the ELF and sends only compact identifiers plus argument bytes from the target. That makes logging dramatically faster and smaller than naïve printf-style approaches.
defmt 的思路是把格式字符串留在 ELF 里,目标板端只发一个索引和参数字节。这比传统 printf 风格日志快得多,也省得多,特别适合资源紧张的嵌入式环境。

#![no_std]
#![no_main]

use defmt::{info, warn, error, debug, trace};
use defmt_rtt as _; // RTT transport — links the defmt output to probe-rs

#[cortex_m_rt::entry]
fn main() -> ! {
    info!("Boot complete, firmware v{}", env!("CARGO_PKG_VERSION"));

    let sensor_id: u16 = 0x4A;
    let temperature: f32 = 23.5;

    // Format strings stay in ELF, not flash — near-zero overhead
    debug!("Sensor {:#06X}: {:.1}°C", sensor_id, temperature);

    if temperature > 80.0 {
        warn!("Overtemp on sensor {:#06X}: {:.1}°C", sensor_id, temperature);
    }

    loop {
        cortex_m::asm::wfi(); // Wait for interrupt
    }
}

// Custom types — derive defmt::Format instead of Debug
#[derive(defmt::Format)]
struct SensorReading {
    id: u16,
    value: i32,
    status: SensorStatus,
}

#[derive(defmt::Format)]
enum SensorStatus {
    Ok,
    Warning,
    Fault(u8),
}

// Usage:
// info!("Reading: {:?}", reading);  // <-- uses defmt::Format, NOT std Debug

defmt vs printf vs log:
defmtprintf 和常规 log 的对比:

FeatureC printf with semihostingRust log cratedefmt
SpeedVery slow
常常慢得离谱
Depends on backendVery fast for embedded use
对嵌入式非常友好
Flash usageStores full strings on target
格式字符串占空间
Same basic problemKeeps compact indices on target
TransportOften semihosting
可能还会暂停 CPU
Backend-dependentRTT
Structured outputNoMostly textTyped binary-encoded data
no_stdVia special setups onlyFront-end only, backends varyNative support
FilteringManual or ad-hocRUST_LOG styleFeature-gated and tooling-aware

VS Code Debug Configuration
VS Code 调试配置

With the probe-rs VS Code extension, you can use a full GUI debugger experience with breakpoints, variables, registers, and call stacks.
装上 probe-rs 的 VS Code 扩展之后,断点、变量、寄存器、调用栈这些图形化调试体验就都能用上了。

// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "probe-rs-debug",
            "request": "launch",
            "name": "Flash & Debug (probe-rs)",
            "chip": "STM32F401RETx",
            "coreConfigs": [
                {
                    "programBinary": "target/thumbv7em-none-eabihf/debug/${workspaceFolderBasename}",
                    "rttEnabled": true,
                    "rttChannelFormats": [
                        {
                            "channelNumber": 0,
                            "dataFormat": "Defmt",
                            "showTimestamps": true
                        }
                    ]
                }
            ],
            "connectUnderReset": true,
            "speed": 4000
        }
    ]
}

Install the extension:
扩展安装命令如下:

#![allow(unused)]
fn main() {
ext install probe-rs.probe-rs-debugger
}

C Debugger Workflow vs Rust Embedded Debugging
C 调试流程与 Rust 嵌入式调试流程对比

graph LR
    subgraph "C Workflow (Traditional)<br/>传统 C 流程"
        C1["Write code<br/>写代码"] --> C2["make flash"]
        C2 --> C3["openocd -f board.cfg"]
        C3 --> C4["arm-none-eabi-gdb<br/>target remote :3333"]
        C4 --> C5["printf via semihosting<br/>输出慢,还会停 CPU"]
    end
    
    subgraph "Rust Workflow (probe-rs)<br/>Rust 的 probe-rs 流程"
        R1["Write code<br/>写代码"] --> R2["cargo embed"]
        R2 --> R3["Flash + RTT console<br/>一条命令完成"]
        R3 --> R4["defmt logs stream<br/>实时日志"]
        R2 -.->|"Or<br/>或者"| R5["VS Code F5<br/>图形化调试"]
    end
    
    style C5 fill:#ffa07a,color:#000
    style R3 fill:#91e5a3,color:#000
    style R4 fill:#91e5a3,color:#000
    style R5 fill:#91e5a3,color:#000
C Debug ActionRust Equivalent
openocd -f board/st_nucleo_f4.cfgprobe-rs info
arm-none-eabi-gdb -x .gdbinitprobe-rs gdb --chip STM32F401RE
target remote :3333Connect GDB to localhost:1337
monitor reset haltprobe-rs reset --chip ...
load firmware.elfcargo flash --chip ...
printf("debug: %d\n", val)defmt::info!("debug: {}", val)
Keil or IAR GUI debuggerVS Code + probe-rs-debugger extension
Segger SystemViewdefmt + probe-rs RTT viewer

Cross-reference: For advanced unsafe patterns that show up in embedded drivers, such as pin projections or arena/slab allocators, see the companion Rust Patterns material mentioned elsewhere in the course.
交叉参考: 嵌入式驱动里更偏底层的 unsafe 模式,比如 pin projection、arena 或 slab 分配器,可以继续对照课程里配套的 Rust Patterns 材料去看。