3. 并发与实时系统概念

好,咱们接着往下走。上一节我们把Rust裸机项目的骨架搭好了,也聊了内存布局和启动流程。这一节,咱们要干点实实在在的事——点亮一颗LED。

你可能会想,不就是点个灯吗?在嵌入式世界里,点亮LED就是嵌入式世界的"Hello World"。但背后涉及的东西,其实挺多的:怎么访问寄存器、怎么用抽象层、怎么保证类型安全。我当年从C转Rust做裸机开发时,光是搞明白svd2rust生成的代码怎么用,就折腾了两天。

3.1 从SVD到Rust代码

每个芯片厂商都会提供SVD(System View Description)文件,这是一个XML格式的文档,描述了芯片上所有寄存器的地址、位域、权限等信息。svd2rust这个工具,就是把这些描述翻译成Rust代码。

基本流程是这样的:

  1. 拿到芯片的SVD文件(比如STM32F103.svd)
  2. 运行svd2rust生成Rust外设访问库(PAC)
  3. 在项目中引用生成的代码

我习惯在项目根目录放一个build.rs脚本,自动完成这个转换。不过对于学习阶段,我建议你先手动跑一遍,看看生成的代码长什么样。

核心概念:svd2rust生成的PAC层,本质上是把每个外设寄存器映射成Rust结构体,利用volatile读写保证操作不被编译器优化掉。每个寄存器字段都有类型安全的读写方法。

3.2 embedded-hal抽象层

直接操作PAC层当然可以,但代码会变成这样:

// PAC层操作GPIO
unsafe {
    (*GPIOA::ptr()).odr.modify(|r| r | (1 << 5));
}

嗯,这代码能跑,但可读性差,而且不通用。换个芯片,所有代码都得重写。

这就是embedded-hal存在的意义。它定义了一套通用trait,比如OutputPin、InputPin、DelayMs等。芯片厂商或社区会为具体芯片实现这些trait。你的应用代码只需要依赖这些trait,换芯片时只需要换底层实现。

层次 作用 可移植性
PAC(svd2rust) 寄存器级访问,类型安全 芯片专用
HAL(embedded-hal实现) 外设功能抽象,如GPIO、SPI、UART 系列内可移植
应用层 业务逻辑,只依赖trait 跨平台可移植

说白了,embedded-hal就是一套接口契约。你写应用时,对着接口编程,别管底层是谁实现的。

3.3 点亮LED的完整流程

好,理论说够了,咱们动手。下面是一个完整的LED闪烁程序,基于STM32F103和stm32f1xx-hal库。

3.3.1 项目依赖配置

# Cargo.toml
[dependencies]
cortex-m-rt = "0.7"
cortex-m-semihosting = "0.5"
panic-halt = "0.2"

[dependencies.stm32f1xx-hal]
version = "0.10"
features = ["rt", "stm32f103", "medium"]

这里rt特性会启用cortex-m-rt的入口处理,stm32f103指定芯片型号,medium表示中等容量(64KB-128KB Flash)。

3.3.2 代码实现

#![no_std]
#![no_main]

use panic_halt as _;
use stm32f1xx_hal::{
    prelude::*,
    pac,
    gpio::GpioExt,
    timer::TimerExt,
};

#[cortex_m_rt::entry]
fn main() -> ! {
    // 获取外设实例
    let dp = pac::Peripherals::take().unwrap();
    let cp = cortex_m::Peripherals::take().unwrap();

    // 初始化GPIO端口C
    let mut gpioc = dp.GPIOC.split();
    
    // 配置PC13为推挽输出
    let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
    
    // 配置定时器
    let mut timer = dp.TIM2.timer_hz(&mut dp.RCC, 1.Hz());
    
    loop {
        led.set_high().unwrap();
        timer.wait().unwrap();
        led.set_low().unwrap();
        timer.wait().unwrap();
    }
}

这段代码做了几件事:

  • 获取外设和内核的访问权限(take是单例模式,只能调用一次)
  • 用split()把GPIO端口拆成独立引脚
  • 用into_push_pull_output()配置引脚模式
  • 用定时器产生1Hz的延时
  • 在循环里交替拉高拉低电平

个人经验:我第一次用into_push_pull_output时,忘了传crh寄存器的可变引用,编译报错说所有权问题。Rust的所有权系统在这里帮了大忙——它确保你不会同时用两个配置去操作同一个引脚。

3.4 避坑指南

我在项目里遇到过几个典型问题,列出来给你参考:

  • GPIO复用功能没配置:有些芯片的LED引脚可能默认是JTAG/SWD功能,需要先remap。我有一块板子,PC13默认是TAMPER-RTC功能,不配置根本点不亮。
  • 时钟没使能:操作GPIO前必须使能对应端口的时钟。HAL层一般会在split()时自动处理,但如果你直接操作PAC层,很容易漏掉这一步。
  • 引脚号搞错:PCB上丝印写的"LED1"可能连的是PB5,但原理图上标的是PB5还是PB0?我建议你拿到板子后,先拿万用表量一下,别信丝印。

特别注意:STM32的PC13引脚驱动能力较弱,直接驱动LED可能不够亮。如果你发现LED亮度异常,检查一下引脚的最大输出电流(通常20mA),必要时加三极管驱动。

3.5 知识结构总览

下面这张图,把整个GPIO操作的知识体系串起来了。你可以把它当作一个思维导图来用。

GPIO操作知识体系 SVD 芯片描述文件 svd2rust → PAC 寄存器层 HAL 实现(embedded-hal trait) 应用代码(只依赖 embedded-hal trait) 时钟使能 引脚模式配置 set_high() / set_low() LED 点亮! 从SVD到LED点亮,每一层都有明确的职责。应用层只关心trait,不关心寄存器地址。

3.6 写在后面

点亮一个LED,看起来简单,但背后是完整的嵌入式软件栈在支撑。从SVD文件到PAC层,再到HAL抽象,最后到你的应用代码——每一层都有它存在的理由。

我个人觉得,理解这个分层结构,比记住某个寄存器地址重要得多。因为芯片会换,项目会变,但这个分层思想,在嵌入式开发里是通用的。

嗯,这一节就到这儿。代码你可以直接拿去烧录试试,如果点不亮,先检查时钟配置和引脚号——我当年在这两个坑里各摔过一次。