Rust in Everything
基于 embassy-stm32 0.6.0

外设被吃掉之后:E0382 / E0499 / E0597;init called more than once!

主线位置:中断主线第 1 步之前,驱动拿到外设的那一刻。Peripherals、Peri<'a, T>、reborrow 和 steal 的设计见外设单例与 Peri。

flowchart TB
  init["init 返回 Peripherals"] --> how{"怎么交给驱动"}
  how -->|按值| mv["Peri 'static 被移走"]
  how -->|reborrow| br["Peri 短生命周期"]
  mv --> e382["再用:E0382"]
  br --> e499["同时两个:E0499"]
  br --> e597["交给任务:E0597"]
  again["再调 init"] --> panic["运行期 panic"]

实验 crate exp-embassy/pitb-qemu(release)。编译错误在 pitb-qemu/errs/;运行的两个 bin(p21_init、p21_fix)是 QEMU 11.1.2 b-l475e-iot01a,embassy-stm32 用本目录里 hal-qemu-patched 的副本(0.6.0 加 rcc/bd.rs 一行、USART 的 32 位访问补丁,本页用到的 GPIO 与 init 的单例检查没有改动)。

现象

同一个引脚用两次。 LED 用完了,想把同一个引脚读回来(errs/e21_moved.txt):

let led = Output::new(p.PB14, Level::High, Speed::Low);
drop(led); // done with the LED, read the pin back as an input
let _sense = Input::new(p.PB14, Pull::Down);
error[E0382]: use of moved value: `p.PB14`
  --> src/bin/e21_moved.rs:14:27
   |
12 |   let led = Output::new(p.PB14, Level::High, Speed::Low);
   |                         ------ value moved here
13 |   drop(led); // done with the LED, read the pin back as an input
14 |   let _sense = Input::new(p.PB14, Pull::Down);
   |                           ^^^^^^ value used here after move
   |
   = note: move occurs because `p.PB14` has type `Peri<'_, PB14>`, which does not implement the `Copy` trait

drop(led) 不会把引脚还回来,p.PB14 已经移进 Output 了。

两个 reborrow 同时活着(errs/e21_twice.txt):

error[E0499]: cannot borrow `p.PB14` as mutable more than once at a time
  --> src/bin/e21_twice.rs:13:26
   |
12 |   let led = Output::new(p.PB14.reborrow(), Level::High, Speed::Low);
   |                         ------ first mutable borrow occurs here
13 |   let sense = Input::new(p.PB14.reborrow(), Pull::Down);
   |                          ^^^^^^ second mutable borrow occurs here
14 |   let _ = sense.is_high();
15 |   drop(led);
   |        --- first borrow later used here

把借来的驱动交给任务(errs/e21_static.txt):

#[embassy_executor::task]
async fn blink(mut led: Output<'static>) { /* ... */ }

let led = Output::new(p.PB14.reborrow(), Level::High, Speed::Low);
spawner.spawn(blink(led).unwrap());
error[E0597]: `p.PB14` does not live long enough
  --> src/bin/e21_static.rs:21:25
   |
18 | #[embassy_executor::main]
   |                         - `p.PB14` dropped here while still borrowed
19 | async fn main(spawner: Spawner) {
20 |   let mut p = embassy_stm32::init(Default::default());
   |       ----- binding `p` declared here
21 |   let led = Output::new(p.PB14.reborrow(), Level::High, Speed::Low);
   |                         ^^^^^^ borrowed value does not live long enough
22 |   spawner.spawn(blink(led).unwrap());
   |                 ---------- argument requires that `p.PB14` is borrowed for `'static`

绕开借用检查:再 init 一次。 一个「板级支持」函数自己调 init 拿 LED 引脚,能编译,运行时(p21_init,QEMU):

fn status_led() -> Output<'static> {
  let p = embassy_stm32::init(Default::default());
  Output::new(p.PB14, Level::Low, Speed::Low)
}
first init ok
panicked at ~/exp-embassy/target-pitb-qemu/thumbv7em-none-eabihf/release/build/embassy-stm32-ced6d4165388e73f/out/_generated.rs:172:1:
init called more than once!

出处

Peri<'a, T> 是一个零大小的令牌,不是 Copy。init 返回的 Peripherals 里每个字段都是 Peri<'static, X>;驱动构造函数按值收 Peri<'d, X>,驱动的类型带着同一个 'd。三种编译错误都从这里来:

  • 按值传进去,'static 的令牌被移走(E0382)。驱动 drop 时把引脚设回断开状态(Flex 的 Drop 调 set_as_disconnected),但令牌没有回来。
  • reborrow 是 &mut self 方法,返回的 Peri<'_, T> 带着这次可变借用的生命周期;同一时刻只能有一个(E0499)。
  • #[embassy_executor::task] 的参数必须是 'static(任务存在静态任务池里,会比 main 的局部变量活得久,见任务存储),借来的 Output<'_> 不满足(E0597)。

运行期那道关在 peripherals! 宏生成的 take_with_cs 里:一个 no_mangle 的 static mut _EMBASSY_DEVICE_PERIPHERALS: bool,第一次 take 置为 true,第二次看到 true 就 panic!("init called more than once!")。init 直接调 init_hw,后者在临界区里第一句就是 Peripherals::take_with_cs(cs),所以第二次 init 在碰任何时钟寄存器之前就 panic(位置显示在生成的 _generated.rs 里,那里是 peripherals! 宏展开的地方)。单例只能有一份,这是 Peri 能在编译期保证「一个引脚只有一个驱动」的前提。

修法

按用途选三种写法之一(p21_fix):

// take the pin, not the whole Peripherals; 'static because a task keeps it
fn status_led(pin: Peri<'static, PB13>) -> Output<'static> {
  Output::new(pin, Level::Low, Speed::Low)
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
  let mut p = embassy_stm32::init(Default::default());
  // 1. lend PB14 to a driver for a while, then use it again
  {
    let led = Output::new(p.PB14.reborrow(), Level::High, Speed::Low);
    hprintln!("PB14 as output: {:?}", led.get_output_level());
  } // led dropped here: the borrow ends, the pin goes back to its reset state
  let sense = Input::new(p.PB14.reborrow(), Pull::Down);
  hprintln!("PB14 as input: {:?}", sense.get_level());
  drop(sense);
  // 2. a task needs Peri<'static>: move the pin itself
  spawner.spawn(blink(status_led(p.PB13)).unwrap());
  // ...
}
PB14 as output: High
PB14 as input: Low
blink done, level High
  • 先后两个用途:reborrow(),把第一个驱动限制在一个块里,块结束借用结束,再 reborrow() 一次。
  • 交给任务:不要 reborrow,把 p.PB13 本身(Peri<'static, PB13>)移进驱动,驱动就是 Output<'static>。
  • 辅助函数要外设:参数写 Peri<'static, PB13>(或 Peri<'d, impl Pin>),由 main 里唯一的那次 init 分发,不要让辅助函数自己 init。

steal() 能凭空造出令牌,但它是 unsafe,等于告诉编译器「这个引脚现在没有别人在用」,见外设单例与 Peri。

怎么确认没中招

  • 编译错误里的类型是 Peri<'_, X>:E0382 找第一次把它按值传走的地方;E0499 找两个同时活着的驱动;E0597 看是否把借来的驱动交给了 spawn。
  • grep -rn 'embassy_stm32::init' src/:整个程序只应有一处。
  • 运行期看到 init called more than once!,用 RUST_BACKTRACE 或调试器看调用栈里第二次 init 的调用者。

小结

  • 驱动按值吃掉 Peri;reborrow 借出一个短生命周期的令牌,同时只能有一个;任务参数必须 'static。
  • init 只能调一次,第二次 panic「init called more than once!」。
  • 修法:顺序复用用 reborrow 加作用域;交给任务就移交 'static 的 Peri;辅助函数收 Peri 参数而不是自己 init。

💬 关联讨论

发起讨论
加载中...