Embassy 资源管理探秘:从手动拆分到 assign-resources 宏#
引言#
在 Embassy 嵌入式框架中,我们常常看到这样的代码模式:先通过 embassy_rp::init() 获取一个包含所有外设的 Peripherals 结构体,然后将其拆分,把不同的资源子集传递给不同的异步任务。
这种“拆分”操作,是 Rust 所有权机制在嵌入式领域的自然延伸——一个硬件资源(如 GPIO 引脚)同时只能被一个所有者持有,因此不同任务之间必须明确划分各自使用的资源边界。
本文将通过一个具体的 Embassy RP2350 示例,介绍两种资源拆分方法:手动传递与使用 assign-resources 宏,并深入解析后者如何在编译期自动化完成这项工作。
场景设定#
假设我们有一个简单的需求:用两个任务分别控制两组 LED,每组两个引脚,实现双闪效果。
- 任务 A:使用
PIN_20和PIN_21 - 任务 B:使用
PIN_10和PIN_11
对应到实际的 RP2350 开发板上,这可能是一块板载 LED 与扩展引脚 LED 的组合。
方法一:手动分配#
这是最直观的方式:在生成任务(spawn)时,直接从 Peripherals 结构体中取出对应的引脚,作为参数传递进去。
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// 手动取出 PIN_20 和 PIN_21,传递给任务
spawner.spawn(double_blinky_manually_assigned(spawner, p.PIN_20, p.PIN_21)).unwrap();
}
#[embassy_executor::task]
async fn double_blinky_manually_assigned(
_spawner: Spawner,
pin_20: Peri<'static, PIN_20>,
pin_21: Peri<'static, PIN_21>,
) {
let mut led_20 = Output::new(pin_20, Level::Low);
let mut led_21 = Output::new(pin_21, Level::High);
loop {
led_20.toggle();
led_21.toggle();
Timer::after_secs(1).await;
}
}rust优点:直接、简单,无需引入额外依赖,适合资源较少的小型项目。
缺点:当任务需要的资源增多时(比如需要 5 个引脚、2 个 UART、1 个 SPI),函数参数列表会变得非常冗长,且资源分配逻辑分散在 main 和各任务定义之间,不利于后期维护。
方法二:使用 assign-resources 宏#
这是 Embassy 社区推荐的、更为优雅和可维护的方式。它通过外部 crate assign-resources,将资源拆分工作集中化和自动化。
第一步:声明资源分组#
使用 assign_resources! 宏声明一个“资源清单”,明确告诉编译器:我需要把 Peripherals 中的哪些字段提取出来,打包成什么结构体。
use assign_resources::assign_resources;
// 必须引入 peripherals 模块,使宏能识别 PIN_10、PIN_11 等符号
use embassy_rp::peripherals::{self, PIN_10, PIN_11};
assign_resources! {
leds: Leds {
led_10: PIN_10,
led_11: PIN_11,
}
// 可继续为其他任务定义更多资源组
// usb: UsbResources { ... }
}rust语法解析:
leds:组名,后续会作为AssignedResources结构体的一个字段名。Leds:我们要生成的新结构体名称。led_10: PIN_10:映射关系——将Peripherals中的PIN_10字段赋给新结构体的led_10字段。
第二步:执行拆分#
声明完成后,assign_resources! 宏会在编译期自动生成一个名为 split_resources! 的宏。我们只需调用它即可完成拆分:
let r = split_resources!(p);rust这行代码展开后的效果,等价于我们手动编写:
// 这是对 split_resources!(p) 展开后效果的模拟
let r = AssignedResources {
leds: Leds {
led_10: p.PIN_10,
led_11: p.PIN_11,
},
};rust第三步:在任务中使用#
任务函数现在可以接收一个语义清晰的结构体作为参数,而非一长串独立引脚:
#[embassy_executor::task]
async fn double_blinky_macro_assigned(_spawner: Spawner, r: Leds) {
let mut led_10 = Output::new(r.led_10, Level::Low);
let mut led_11 = Output::new(r.led_11, Level::High);
loop {
led_10.toggle();
led_11.toggle();
Timer::after_secs(1).await;
}
}rust深入解析:assign-resources 的工作原理#
这个宏的“神奇”之处在于,它将运行时的资源分配问题,转化为了编译期的代码生成问题。其工作流程分为两个阶段:
阶段一:声明处理(assign_resources!)#
当 Rust 编译器遇到 assign_resources! 宏调用时,会执行以下操作:
- 解析输入:读取你提供的“资源清单”,提取组名、结构体名以及字段映射关系。
- 生成结构体定义:
ruststruct Leds { led_10: PIN_10, led_11: PIN_11, } struct AssignedResources { pub leds: Leds, } - 生成拆分宏:创建一个名为
split_resources!的新宏,其内部包含了从Peripherals中提取指定字段的代码逻辑。
阶段二:拆分执行(split_resources!)#
当你在代码中调用 let r = split_resources!(p); 时:
- 宏展开:
split_resources!宏被展开,生成类似前述模拟代码的 Rust 代码。 - 所有权转移:
p.PIN_10和p.PIN_11的所有权被移动到新生成的Leds结构体中。 - 返回结果:最终得到一个类型为
AssignedResources的结构体实例r。
整个过程在编译期完成,零运行时开销。生成的代码与手写代码在性能上完全一致。
为什么要使用宏?#
| 方面 | 手动分配 | assign-resources 宏 |
|---|---|---|
| 代码集中性 | 资源分配分散在 main 和各任务函数中 | 资源分配集中在一处声明,一目了然 |
| 可维护性 | 修改引脚映射需改动多处 | 修改引脚映射只需改动一处声明 |
| 类型安全 | 依赖手动匹配,容易传错顺序或类型 | 宏生成的结构体类型明确,编译器自动检查 |
| 可扩展性 | 增加资源需同步修改多处参数 | 只需在声明中添加一行映射即可 |
| 函数签名 | 参数列表随资源数量线性增长 | 始终只接收一个结构体参数 |
| 零成本抽象 | 是 | 是(编译期展开,无运行时开销) |
进阶用法与注意事项#
跨文件使用#
assign_resources! 宏可以在 lib.rs 中定义,然后在多个 binary 中共享使用。这使得多固件项目(如不同型号开发板)可以复用同一套资源配置。
// lib.rs
assign_resources! {
leds: LedResources { ... }
usb: UsbResources { ... }
}rust// my_bin.rs
use my_library::{AssignedResources, split_resources};
let r = split_resources!(p);rust类型别名#
可以为外设字段指定类型别名,便于在函数签名中引用:
assign_resources! {
timer: TimerResources {
tim2: TIM2 = PWMTimer, // 将 TIM2 别名为 PWMTimer
}
}rust⚠️ 注意事项#
- 所有权移动:一旦执行
split_resources!(p),p中被拆分走的字段(如PIN_10)就不再生效。未被引用的字段(如PIN_25)仍然可用。 - 作用域:确保
peripherals模块在调用assign_resources!时处于作用域内(通常通过use embassy_rp::peripherals;引入)。 - 单次声明:在一个作用域内,
split_resources!只能由最后一个assign_resources!调用生成,因此通常只声明一次。
总结#
assign-resources 宏的价值在于:把“如何拆分”的重复劳动交给编译器,让开发者专注于“需要什么”的业务逻辑。
它体现了 Rust 宏系统的设计哲学:提供零成本的抽象,在不牺牲运行时性能的前提下,大幅提升代码的可读性和可维护性。在 Embassy 这类嵌入式框架中,这种模式已成为管理硬件资源的标准实践。