外设单例与 Peri
主线位置:中断主线的第 0 步。
ExtiInput::new(p.PC13, p.EXTI13, Pull::Down, Irqs)里的p.PC13和p.EXTI13从哪来、为什么交出去之后就不能再用。本页讲p,Irqs见中断绑定,ExtiInput本身见 GPIO 与 EXTI。
flowchart TB init["embassy_stm32::init"] --> take["Peripherals::take"] take --> p["Peripherals 169 个字段"] p --> owned["p.PB14 移走"] p --> rb["p.PB14.reborrow"] owned --> drv1["Output 'static"] rb --> drv2["Output 'a"] drv2 --> back["drop 后还给 p"] steal["PB14::steal"] --> drv3["unsafe 另造一个"]
外设令牌的定义在 embassy-hal-internal 0.5.0 的 peripheral.rs(107 行)和 macros.rs(135 行),具体列哪些外设由 embassy-stm32 的 build.rs 按芯片生成。本页的实验在 exp-embassy/hal-qemu:编译错误是交叉编译(thumbv7em-none-eabihf,release)的原文;运行结果来自 QEMU 11.1.2 的 b-l475e-iot01a,embassy-stm32 打了注释掉 rcc/bd.rs 里 while !cr.read().dbp() {} 的补丁(PWR 没有模型)。宿主是 M2 Max、macOS 27、rustc 1.99.0。
Peripherals:每个引脚一个类型
build.rs 为每款芯片生成一次 peripherals_definition! 和 peripherals_struct! 调用,写进 OUT_DIR/_generated.rs。STM32L475VG 的那一份开头是:
embassy_hal_internal::peripherals_definition!(
PA0,
PA1,
PA2,
// ...
DMA2_CH7
);
embassy_hal_internal::peripherals_struct!(
PA0,
// ...
(生成文件被 build.rs 用 rustfmt 排过版,rustfmt 读到了本机 ~/.rustfmt.toml 的 2 空格缩进,所以这里不是上游的 4 空格。)列表里 169 项:82 个引脚、16 个 EXTI 通道(EXTI0 到 EXTI15)、14 个 DMA 通道(DMA1_CH1 到 DMA2_CH7),其余 57 个是外设(USART1、TIM2、RCC、FLASH……)。没有 GPIOA 这样的端口令牌,引脚各自是一个令牌;EXTI 和 DMA 也拆到通道。粒度就是「两个驱动不能同时占用」的最小单位。
peripherals_definition! 给每个名字生成一个私有字段的空结构体,加一个 const unsafe fn steal():
pub struct $name { _private: () }
$(#[$cfg])?
impl $name {
// ...
#[inline]
pub const unsafe fn steal() -> $crate::Peri<'static, Self> {
$crate::Peri::new_unchecked(Self{ _private: ()})
}
}
$(#[$cfg])?
$crate::impl_peripheral!($name);
_private: () 让用户代码造不出这个类型,只能通过 init 或 steal 得到。impl_peripheral! 给它实现 Copy、Clone 和标记 trait PeripheralType(Copy + Sized)。类型本身可以复制,所以「只能有一个」不靠这个类型,靠下一节的 Peri。
peripherals_struct! 生成 Peripherals,每个字段是 Peri<'static, peripherals::X>,并提供 take_with_cs:
pub(crate) fn take_with_cs(_cs: critical_section::CriticalSection) -> Self {
#[unsafe(no_mangle)]
static mut _EMBASSY_DEVICE_PERIPHERALS: bool = false;
// safety: OK because we're inside a CS.
unsafe {
if _EMBASSY_DEVICE_PERIPHERALS {
panic!("init called more than once!")
}
_EMBASSY_DEVICE_PERIPHERALS = true;
Self::steal()
}
}
单例的保证在运行期只有这一个布尔量:init_hw 进临界区后第一件事就是 Peripherals::take_with_cs(cs),第二次调用 init 会 panic。#[no_mangle] 让这个标志成为全局唯一的链接符号。在 QEMU 上调两次 init(exp-embassy/hal-qemu/src/bin/peri.rs):
calling init again ...
panicked at .../out/_generated.rs:172:1:
init called more than once!
位置指向 _generated.rs 第 172 行,也就是 peripherals_struct!( 那一行宏调用。
Peri<'a, T>:把所有权和生命周期装进一个零大小的值
Peri 的定义只有两个字段:
pub struct Peri<'a, T: PeripheralType> {
inner: T,
_lifetime: PhantomData<&'a mut T>,
}
它没有实现 Copy 和 Clone,所以 Peri<'static, PB14> 只能被移动。文档注释说它「functionally the same as a &'a mut T」,并给了两个不直接用 &mut T 的理由:一是内存,&mut T 在 32 位目标上总是 4 字节,Peri 里存的是 T 本身,零大小的外设就是零字节;二是代码体积,驱动同时被 SPI4 和 &mut SPI4 调用会单态化两次,换成 Peri<'static, SPI4> 和 Peri<'a, SPI4> 只差生命周期,生命周期不产生单态化。
第一点在 QEMU 上量一下(同一个 peri.rs):
size_of Peri<PB14> = 0
size_of Peri<AnyPin> = 1
size_of &mut PB14 = 4
size_of Output = 1
Output<'d> 只有 1 字节:它里面是 Flex<'d>,Flex 里是 Peri<'d, AnyPin>,AnyPin 只有一个 pin_port: u8(端口号乘 16 加引脚号)。
驱动构造函数吃掉 Peri
Output::new 的签名是 pub fn new(pin: Peri<'d, impl Pin>, initial_output: Level, speed: Speed) -> Self。它把参数交给 Flex::new:
pub fn new(pin: Peri<'d, impl Pin>) -> Self {
// Pin will be in disconnected state.
Self { pin: pin.into() }
}
这里的 into 是 Peri::into,把 Peri<'d, PB14> 换成 Peri<'d, AnyPin>,生命周期不变。具体引脚的类型只活在构造函数的入口,进了驱动就被擦掉,所以 Flex 的所有方法只为 AnyPin 编译一份。哪个引脚实现了 Pin 由 build.rs 生成的 foreach_pin! 表决定(gpio.rs 里展开),表里同时写了每个引脚对应的 EXTI 通道,GPIO 与 EXTI 会用到。
驱动拿走 Peri<'d, ..> 之后,'d 也成了驱动类型的一部分:Output<'static> 能放进 'static 的任务参数,Output<'a> 不能活过它借的东西。驱动 drop 时把硬件恢复原状,Flex 的 Drop 调 set_as_disconnected(),在 GPIO v2 上就是模拟模式(MODER = 0b11)。所以「借出去、用完、还回来」在硬件上也是干净的。
reborrow:借给驱动,用完收回
想临时把引脚交给一个驱动、之后再用,就用 reborrow:
pub const fn reborrow(&mut self) -> Peri<'_, T> {
// safety: we're returning the clone inside a new Peripheral that borrows
// self, so user code can't use both at the same time.
unsafe { self.clone_unchecked() }
}
它复制出内部的 T(T: Copy),装进一个生命周期绑在 &mut self 上的新 Peri。复制本身不安全,安全性来自借用:子 Peri 活着,self 就处于可变借用中。peri.rs 里的写法(打印语句换成了注释)和 QEMU 上读到的 GPIOB.MODER 第 14 位:
let mut p = embassy_stm32::init(Default::default());
{
let mut led = Output::new(p.PB14.reborrow(), Level::High, Speed::Low);
led.set_low();
// MODER14 = 1(输出)
}
// MODER14 = 3(模拟):led drop 时 Flex 恢复了引脚
let input = Input::new(p.PB14.reborrow(), Pull::Up);
drop(input);
let _flex = Flex::new(p.PB14); // 最后一次:直接移走
output: MODER14=1
after drop: MODER14=3 (3 = analog)
input: level High
规则全在编译期。下面三段是 rustc 的原文(exp-embassy/hal-qemu/errs/)。同一个引脚移进两个驱动:
error[E0382]: use of moved value: `p.PB14`
--> src/bin/err_peri_moved.rs:15:27
|
14 | let _led = Output::new(p.PB14, Level::Low, Speed::Low);
| ------ value moved here
15 | let _again = Input::new(p.PB14, Pull::None);
| ^^^^^^ value used here after move
|
= note: move occurs because `p.PB14` has type `Peri<'_, PB14>`, which does not implement the `Copy` trait
两次 reborrow 同时活着:
error[E0499]: cannot borrow `p.PB14` as mutable more than once at a time
--> src/bin/err_peri_reborrow.rs:15:27
|
14 | let led = Output::new(p.PB14.reborrow(), Level::Low, Speed::Low);
| ------ first mutable borrow occurs here
15 | let _input = Input::new(p.PB14.reborrow(), Pull::None);
| ^^^^^^ second mutable borrow occurs here
16 | drop(led);
| --- first borrow later used here
把借来的驱动交给要求 Output<'static> 的任务:
error[E0597]: `p.PB14` does not live long enough
--> src/bin/err_peri_static.rs:17:25
|
14 | #[embassy_executor::main]
| - `p.PB14` dropped here while still borrowed
15 | async fn main(spawner: Spawner) {
16 | let mut p = embassy_stm32::init(Default::default());
| ----- binding `p` declared here
17 | let led = Output::new(p.PB14.reborrow(), Level::Low, Speed::Low);
| ^^^^^^ borrowed value does not live long enough
18 | spawner.spawn(blink(led).unwrap());
| ---------- argument requires that `p.PB14` is borrowed for `'static`
任务要 'static,就得把 p.PB14 本身移进去,而不是借。
&mut p.PB14 的写法在 0.3.0 被拿掉了PR #3999(「Remove Peripheral trait, rename PeripheralRef->Peri.」,首次进入 embassy-hal-internal v0.3.0 和 embassy-stm32 v0.3.0)之前,驱动参数写成 pin: impl Peripheral<P = T> + 'd,构造函数里调 .into_ref() 得到 PeripheralRef<'d, T>;Peripheral 对所有 DerefMut 到外设的类型都有 blanket impl,所以 Output::new(&mut p.PB14, ..) 也能编译,得到一个借用版的驱动。这个 PR 删掉了 Peripheral trait,把 PeripheralRef 改名为 Peri,参数统一成 Peri<'d, T>,借用改由显式的 reborrow() 表达。同一个驱动不再因为「拿到的是值还是 &mut」被单态化两次,这正是 Peri 文档注释里的第二个理由。现在照旧写法会得到:
error[E0308]: mismatched types
--> src/bin/err_peri_mutref.rs:14:26
|
14 | let _led = Output::new(&mut p.PB14, Level::Low, Speed::Low); // pre-0.3 style
| ----------- ^^^^^^^^^^^ expected `Peri<'_, _>`, found `&mut Peri<'_, PB14>`
...
help: consider removing the borrow
rustc 建议去掉 &mut,那会把引脚移走;想保留引脚,应该改成 p.PB14.reborrow()。
steal 与 clone_unchecked:后门
三个后门都是 unsafe:
- 每个外设类型的
steal()(上面宏里的那个),0.5.0 起是const fn(hal-internal 的 CHANGELOG:「Make peripheral steal() const」)。 Peri::clone_unchecked,返回一个同生命周期的副本;文档注释提醒「don't create two SPI drivers onSPI1, because they will "fight" each other」,并说应优先用reborrow()。AnyPin::steal(pin_port),按数字造一个类型擦除的引脚。
Peripherals::steal() 也存在,它跳过 _EMBASSY_DEVICE_PERIPHERALS 检查,直接返回全部令牌。这些后门的用途是中断处理函数里或者 HAL 内部(例如 RingBufferedUartRx 构造时对 DMA 通道 clone_unchecked,再 mem::forget 原来的驱动):调用者自己保证同一时刻只有一个使用者。
size_of::<embassy_stm32::Peripherals>() 是多少?STM32L475VG,169 个字段,每个是 Peri<'static, X>。用 peri.rs 的第五行输出回答。
动脑筋答案
size_of Peri<PB14> = 0
size_of Peri<AnyPin> = 1
size_of &mut PB14 = 4
size_of Output = 1
size_of Peripherals = 0
- 0 字节。169 个字段都是零大小的
Peri<'static, X>,X是{ _private: () }。 init返回它不占栈,p.PB14移进驱动也不拷贝任何数据。所有权检查全在编译期,运行期剩下的只有take_with_cs里那一个布尔量。- 驱动一旦擦成
AnyPin,就要 1 字节记住是哪个引脚。
小结
build.rs按芯片生成外设列表,peripherals!一族宏把每项变成一个用户造不出来的零大小类型;L475 上 169 项,引脚、EXTI 通道、DMA 通道各自独立。- 唯一的运行期检查是
_EMBASSY_DEVICE_PERIPHERALS:init第二次调用就 panic。 Peri<'a, T>是不可复制的零大小令牌,作用等同&'a mut T,但不占 4 字节、不让驱动按值和借用各单态化一次。- 驱动构造函数吃掉
Peri<'d, T>,入口处把具体类型擦成AnyPin之类;'d进了驱动类型,drop 时驱动把硬件恢复成断开状态。 - 临时借用用
reborrow(),移走、重复借用、借用逃出作用域都在编译期报错;0.3.0 起不再接受&mut p.X。 steal、clone_unchecked、AnyPin::steal是unsafe后门,正确性由调用者负责。