知识门户

Back

Embassy 资源管理探秘:从手动拆分到 assign-resources#

引言#

在 Embassy 嵌入式框架中,我们常常看到这样的代码模式:先通过 embassy_rp::init() 获取一个包含所有外设的 Peripherals 结构体,然后将其拆分,把不同的资源子集传递给不同的异步任务。

这种“拆分”操作,是 Rust 所有权机制在嵌入式领域的自然延伸——一个硬件资源(如 GPIO 引脚)同时只能被一个所有者持有,因此不同任务之间必须明确划分各自使用的资源边界。

本文将通过一个具体的 Embassy RP2350 示例,介绍两种资源拆分方法:手动传递使用 assign-resources,并深入解析后者如何在编译期自动化完成这项工作。

场景设定#

假设我们有一个简单的需求:用两个任务分别控制两组 LED,每组两个引脚,实现双闪效果。

  • 任务 A:使用 PIN_20PIN_21
  • 任务 B:使用 PIN_10PIN_11

对应到实际的 RP2350 开发板上,这可能是一块板载 LED 与扩展引脚 LED 的组合。

方法一:手动分配#

这是最直观的方式:在生成任务(spawn)时,直接从 Peripherals 结构体中取出对应的引脚,作为参数传递进去。

优点:直接、简单,无需引入额外依赖,适合资源较少的小型项目。

缺点:当任务需要的资源增多时(比如需要 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! 宏调用时,会执行以下操作:

  1. 解析输入:读取你提供的“资源清单”,提取组名、结构体名以及字段映射关系。
  2. 生成结构体定义
    struct Leds {
        led_10: PIN_10,
        led_11: PIN_11,
    }
    struct AssignedResources {
        pub leds: Leds,
    }
    rust
  3. 生成拆分宏:创建一个名为 split_resources! 的新宏,其内部包含了从 Peripherals 中提取指定字段的代码逻辑。

阶段二:拆分执行(split_resources!#

当你在代码中调用 let r = split_resources!(p); 时:

  1. 宏展开split_resources! 宏被展开,生成类似前述模拟代码的 Rust 代码。
  2. 所有权转移p.PIN_10p.PIN_11 的所有权被移动到新生成的 Leds 结构体中。
  3. 返回结果:最终得到一个类型为 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

⚠️ 注意事项#

  1. 所有权移动:一旦执行 split_resources!(p)p 中被拆分走的字段(如 PIN_10)就不再生效。未被引用的字段(如 PIN_25)仍然可用。
  2. 作用域:确保 peripherals 模块在调用 assign_resources! 时处于作用域内(通常通过 use embassy_rp::peripherals; 引入)。
  3. 单次声明:在一个作用域内,split_resources! 只能由最后一个 assign_resources! 调用生成,因此通常只声明一次。

总结#

assign-resources 宏的价值在于:把“如何拆分”的重复劳动交给编译器,让开发者专注于“需要什么”的业务逻辑

它体现了 Rust 宏系统的设计哲学:提供零成本的抽象,在不牺牲运行时性能的前提下,大幅提升代码的可读性和可维护性。在 Embassy 这类嵌入式框架中,这种模式已成为管理硬件资源的标准实践。

Embassy 资源管理探秘:从手动拆分到 assign-resources 宏
https://glinfei.space/blog/nostd-rust/assign
Author 甘霖飞
Published at 2026年8月13日
Comment seems to stuck. Try to refresh?✨