3. 并发与实时系统概念
好,咱们接着往下走。上一节我们把Rust裸机项目的骨架搭好了,也聊了内存布局和启动流程。这一节,咱们要干点实实在在的事——点亮一颗LED。
你可能会想,不就是点个灯吗?在嵌入式世界里,点亮LED就是嵌入式世界的"Hello World"。但背后涉及的东西,其实挺多的:怎么访问寄存器、怎么用抽象层、怎么保证类型安全。我当年从C转Rust做裸机开发时,光是搞明白svd2rust生成的代码怎么用,就折腾了两天。
3.1 从SVD到Rust代码
每个芯片厂商都会提供SVD(System View Description)文件,这是一个XML格式的文档,描述了芯片上所有寄存器的地址、位域、权限等信息。svd2rust这个工具,就是把这些描述翻译成Rust代码。
基本流程是这样的:
- 拿到芯片的SVD文件(比如STM32F103.svd)
- 运行svd2rust生成Rust外设访问库(PAC)
- 在项目中引用生成的代码
我习惯在项目根目录放一个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操作的知识体系串起来了。你可以把它当作一个思维导图来用。
3.6 写在后面
点亮一个LED,看起来简单,但背后是完整的嵌入式软件栈在支撑。从SVD文件到PAC层,再到HAL抽象,最后到你的应用代码——每一层都有它存在的理由。
我个人觉得,理解这个分层结构,比记住某个寄存器地址重要得多。因为芯片会换,项目会变,但这个分层思想,在嵌入式开发里是通用的。
嗯,这一节就到这儿。代码你可以直接拿去烧录试试,如果点不亮,先检查时钟配置和引脚号——我当年在这两个坑里各摔过一次。