- 嵌入式
- 物联网
- 异步编程
【免费下载链接】embassy
Modern embedded framework, using Rust and async.
导读
本文以 embassy-nxp/CHANGELOG.md 为骨架,逐条解读 embassy-nxp 首个带 changelog 的版本(0.1.0)中针对 NXP LPC55 系列芯片落地的一系列 HAL 能力:阻塞版 SPI 驱动、基于 DMA 的异步 USART、简单 PWM 输出、以及底层从lpc55-pac迁移到统一的nxp-pac元数据代码生成机制。通过结合 embassy-nxp/src 下的真实实现,你将掌握 LPC55S69 等芯片上 Flexcomm 外设(USART/SPI)的驱动结构、DMA 异步收发的底层调用链,以及 embassy 系 HAL 如何借助nxp-pac元数据在构建期自动生成外设与引脚代码。
一、版本与变更背景
embassy-nxp 是 Embassy 异步嵌入式框架中面向 NXP 芯片的 HAL crate,当前版本为0.1.0(见 embassy-nxp/Cargo.toml),许可证为 MIT OR Apache-2.0,publish = false,属于仓库内的工作区 crate。它通过package.metadata.embassy定义了针对thumbv8m.main-none-eabihf(LPC55 系列)与thumbv7em-none-eabihf(i.MX RT 系列)的构建矩阵:
[package.metadata.embassy] build = [ {target = "thumbv8m.main-none-eabihf", features = ["defmt", "lpc55-core0"]}, {target = "thumbv8m.main-none-eabihf", features = ["defmt", "lpc55s16"]}, {target = "thumbv7em-none-eabihf", features = ["defmt", "mimxrt1011", "rt", "time-driver-pit"]}, {target = "thumbv7em-none-eabihf", features = ["defmt", "mimxrt1062", "rt", "time-driver-pit"]}, ]从 changelog 的条目来看,这一版本的核心工作集中在 LPC55 平台:补齐阻塞 SPI、引入 DMA 与异步 USART、落地简单 PWM,并把整个 PAC 依赖切换为统一的nxp-pac。以下逐条展开。
二、LPC55:阻塞版 SPI 驱动
变更条目:LPC55: blocking version of SPI
在 embassy-nxp/src/spi.rs 中,SPI 模块通过#[cfg_attr(lpc55, path = "./spi/lpc55.rs")]按芯片选择实现文件,LPC55 的实际实现位于 embassy-nxp/src/spi/lpc55.rs。
2.1 配置结构
驱动提供Config结构体,包含频率、相位、极性、数据位序四项:
pub struct Config { pub frequency: u32, // 时钟频率,默认 1_000_000(1 MHz) pub phase: Phase, // 默认 CaptureOnFirstTransition pub polarity: Polarity, // 默认 IdleLow pub data_format: DataFormat, // MsbFirst / LsbFirst,默认 MsbFirst }phase与polarity直接复用embedded_hal_02::spi的Phase、Polarity枚举,DataFormat则由 HAL 自行定义,最终映射到 SPI 寄存器的LSBF位(spi::vals::Lsbf::Reverse/Standard)。
2.2 阻塞 API 集合
阻塞模式通过Spi<'d, Blocking>提供四组核心方法:
blocking_write:只写,逐字节等待txnotfull,写入fifowr寄存器后flush;blocking_read:只读,写入哑数据(txdata = 0)同时读取fiford;blocking_transfer:半双工读写,以read.len().max(write.len())为循环上界;blocking_transfer_in_place:原地读写。
所有方法都会在写入 FIFO 前轮询txnotfull,并在返回前检查txerr/rxerr,出错时返回Error::Overrun。此外还实现了embedded_hal_02::blocking::spi::Transfer<u8>与Write<u8>trait,保证与既有 embedded-hal 生态驱动兼容。
构造函数方面提供三种模式:
new_blocking:全双工(SCK + MOSI + MISO);new_blocking_txonly:仅发送(SCK + MOSI);new_blocking_rxonly:仅接收(SCK + MISO)。
若 MOSI 与 MISO 均未提供,构造函数会返回ConstructorError::NoTransferringPinsError;频率超出可配置范围时返回IncompatibleFrequencyError。
2.3 底层时钟链:整数分频 + 分数分频两级"雕琢"
SPI 的时钟配置是这段代码中比较有代表性的实现。LPC55 的 Flexcomm 接口函数时钟公式为:
FCLK = (FCCLKSEL 选择的时钟) / (1 + MULT / DIV) 最终频率 = FCLK / (DIVVAL + 1)源码(embassy-nxp/src/spi/lpc55.rs 的configure_clock)固定选择 96 MHz 作为源时钟(FcclkselSel::Enum0x3),先计算整数分频div_val = (96_000_000 / frequency).min(0xFFFF),再计算分数分频mult_val = ((raw_clock * 256 / frequency) - 256).min(255),其中DIV固定写0xFF(即分母 256)。注释明确给出了理论可达范围:最低约 732 Hz(96 MHz / 131_072)。这种"先整数分频、再分数微调"的两级设计,是为了让任意目标频率都能被较高精度的MULT/DIV分数发生器"雕琢"出来。
2.4 Flexcomm 外设选择与引脚绑定
LPC55 的 Flexcomm 外设是"多面手",同一 IP 可通过PERSEL位选择工作为 USART、SPI 或 I2C。configure_flexcomm完成三步:
- 使能
SYSCON中 IOCON 与对应 Flexcomm 实例的 AHB 时钟; - 通过
presetctrl1对外设做一次"断言复位再释放"; - 将
pselid.persel置为Spi并set_lock(true)——锁定位一旦置位,直到整板复位前都无法再更改外设模式。
引脚绑定采用"实例 trait + 引脚 trait"的双重约束:Spi<'d, Blocking>的构造函数要求sck: impl SckPin<T>、mosi: impl MosiPin<T>、miso: impl MisoPin<T>。这些 trait 由impl_spi_sck_pin!/impl_spi_mosi_pin!/impl_spi_miso_pin!三个宏在芯片级代码中批量实现,每个引脚会带出对应的PioFunc(IOCON 复用功能编号)。
三、Codegen:基于 nxp-pac 元数据的外设代码生成
变更条目:Codegen using 'nxp-pac' metadata
这是本次版本最底层的变化:不再为每个芯片手写外设枚举,而是由构建脚本从nxp-pac的METADATA元数据中自动生成。
3.1 构建脚本整体流程
embassy-nxp/build.rs 的main函数执行以下步骤:
- 通过环境变量
CARGO_FEATURE_*检测启用了哪个芯片 feature(mimxrt1011/mimxrt1062/lpc55s16/lpc55-core0),未启用或启用多个都会 panic; - 定义两组 cfg 别名:
cfg_aliases! { rt1xxx: { any(feature = "mimxrt1011", feature = "mimxrt1062") }, } cfg_aliases! { lpc55: { any(feature = "lpc55s16", feature = "lpc55-core0") }, }- 调用
generate_code生成外设单例(peripheral singleton)与引脚实现,输出到OUT_DIR/_generated.rs。
3.2 单例生成策略
singletons函数遍历metadata::METADATA.peripherals,按名字前缀分流:
- 跳过
GPIO*与DMA*(它们需要在第二轮处理); GPIO{n}:为每个引脚生成PIO{n}_{pin}单例,并根据引脚号是否大于 15 启用gpio{n}_hicfg;DMA{n}:为每个DMA{n}_CH{channel}生成通道单例;SCT{n}:只取名字以OUT开头的信号,生成SCT{n}_OUT{m}输出单例。
最终这些单例在 embassy-nxp/src/chips/lpc55.rs 通过include!(concat!(env!("OUT_DIR"), "/_generated.rs"))汇入,并由 embassy-nxp/src/lib.rs 的pub use chip::{Peripherals, interrupt, peripherals}对外暴露。
对于 i.MX RT 系列(_rt1xxx),generate_iomuxc还会从元数据的pins中筛选带有iomuxc.mux定义的引脚,生成iomuxc_pad/iomuxc_mux查询函数,供 embassy-nxp/src/iomuxc.rs 使用。这意味着引脚复用配置也全部由元数据驱动,消除了手写 match 表。
3.3 对开发者的影响
- 新增芯片时只需要在
nxp-pac中补充元数据,并给Cargo.toml增加一个芯片 feature,无需手写大量重复的引脚实现; - 外设单例命名(如
peripherals::USART0、peripherals::PIO1_5)全部与 PAC 元数据保持一致,降低了出错概率; - 代价是构建依赖
nxp-pac(git 依赖,固定 rev 为98b09d2eae1f073804d6ded639c8dab583f614b0)与proc-macro2、quote,构建期更长。
四、LPC55:简单 PWM
变更条目:LPC55: PWM simple
PWM 实现位于 embassy-nxp/src/pwm/lpc55.rs,底层使用 LPC55 的 SCTimer/PWM(SCT0)统一计数器。
4.1 配置模型与周期公式
pub struct Config { pub invert: bool, // 是否反相输出 pub phase_correct: bool, // 相位校正模式,开启后输出频率减半 pub enable: bool, // 是否启动输出 pub divider: u8, // SYSCON 时钟分频,实际除以 divider + 1 pub prescale_factor: u8, // SCT 预分频,实际除以 prescale_factor + 1 pub compare: u32, // 比较值,输出电平翻转点 pub top: u32, // 计数上限,决定周期 }文档注释给出了输出周期计算公式:
周期(时钟周期数)= (top + 1) * (phase_correct ? 1 : 2) * divider * prescale_factor默认 SCT 时钟为 96 MHz。Config::new(compare, top)提供便捷构造函数,默认divider = 255、prescale_factor = 255。
4.2 实现要点
- 共享计数器:SCT0 是统一计数器,
TOP_VALUE以AtomicU32静态保存。源码通过assert!(config.compare <= config.top)强制比较值不超过计数上限,否则计数器永远达不到匹配事件;TOP_VALUE一旦在第一个实例初始化时写入,之后再次修改会直接panic!("The top value cannot be changed after the initialization.")。这与注释中"counter is shared"的设计一致——周期由第一个通道决定,后续通道只能改占空比。 - 事件与输出映射:
match_(0)保存top值,match_(output_number + 1)保存compare值;ev(0)与ev(output_number + 1)两个事件配合out_set/out_clr决定输出拉高/拉低的时机。invert = true时二者互换。 - 生命周期:
Pwm实现Drop,用REF_COUNT(AtomicU8)跟踪活跃实例,最后一个实例销毁时重置TOP_VALUE,允许后续重新配置周期。 - 初始化钩子:
Pwm::reset()在init阶段(见 embassy-nxp/src/lib.rs 的pwm::Pwm::reset())通过presetctrl1.sct_rst断言/释放复位,保证计数器从确定状态开始。
4.3 使用方式
let mut pwm = Pwm::new_output( p.SCT0_OUT0, // SCT 输出通道单例 p.PIO1_5, // 输出引脚 Config::new(compare, top), // 直接指定 compare 与 top ); // 运行时改占空比 pwm.set_config(&Config { compare: new_val, ..config }); let counter = pwm.counter(); // 读取当前计数值五、LPC55:USART 的 ALT 定义迁移与内部宏清理
变更条目:
LPC55: Move ALT definitions for USART to TX/RX pin impls.LPC55: Remove internal match_iocon macro
这两条属于内部重构,但影响了公共 API 的形态。在 embassy-nxp/src/usart/lpc55.rs 中可以看到,每个impl_usart_txd_pin!/impl_usart_rxd_pin!宏展开即为"引脚 + USART 实例 + IOCON 复用功能号"的三元绑定:
impl_usart_txd_pin!(PIO0_29, USART0, Func4); impl_usart_rxd_pin!(PIO0_0, USART0, Func3);即把"该引脚作为某 USART 的 TX/RX 时应该配置成哪个 ALT 功能"直接收进引脚实现里,取代了原先集中式match_iocon宏根据 (pin, peripheral) 查表的分发逻辑。对用户而言,编译器在Usart::new(usart, tx_pin, rx_pin, ...)时通过impl TxPin<T>/impl RxPin<T>trait 约束直接校验引脚与 USART 实例是否匹配,错误在编译期暴露,而不是运行期查表失败。
六、LPC55:DMA 控制器与异步 USART
变更条目:LPC55: DMA Controller and asynchronous version of USART
这是本版本功能量最大的条目:DMA 驱动落地,并为 USART 提供异步收发能力。DMA 模块入口在 embassy-nxp/src/dma.rs,实际实现位于 embassy-nxp/src/dma/lpc55.rs;异步 USART 与 DMA 的配合逻辑集中在 embassy-nxp/src/usart/lpc55.rs。
6.1 模式系统与中断绑定
USART 驱动使用Mode泛型区分阻塞/异步两种模式(embassy-nxp/src/lib.rs):
pub trait Mode: SealedMode {} pub struct Blocking; // 阻塞模式 pub struct Async; // 异步模式Usart<'d, M: Mode>内部持有UsartTx<'d, M>与UsartRx<'d, M>两个半部,可调用split()/split_ref()拆分给不同任务使用。
异步模式依赖中断绑定。lib.rs导出的bind_interrupts!宏将 IRQ 与InterruptHandler关联:
bind_interrupts!( struct Irqs { FLEXCOMM0 => usart::InterruptHandler<peripherals::USART0>; } );InterruptHandler::on_interrupt的实现值得注意:当收到 RX 错误中断时,它不清中断标志,而是置位dma_state.rx_err(AtomicBool)并唤醒rx_err_waker(AtomicWaker)。注释解释了原因:"清标志会让 DMA 传输继续,可能在我们检查传输期间发生的错误之前就发出完成信号"——因此必须让 DMA 先停下来,再统一判定错误类型。
6.2 异步 TX:DMA 写
pub async fn write(&mut self, buffer: &[u8]) -> Result<(), Error> { let ch = self.tx_dma.as_mut().unwrap().reborrow(); let transfer = unsafe { self.info.usart_reg.fifocfg().modify(|w| w.set_dmatx(true)); crate::dma::write(ch, buffer, self.info.usart_reg.fifowr().as_ptr() as *mut _) }; transfer.await; Ok(()) }流程是:开启dmatx位让 DMA 按 FIFO 水位节拍搬运数据 → 构造 DMA 传输 future →await等待完成。源码特意将 future 绑定到变量transfer上再 await,注释指出若不这样做,"数据寄存器指针会跨 await 被持有,使 future 变为非 Send"。
6.3 异步 RX:FIFO 预读 + DMA + 错误竞争
read的实现比 TX 复杂得多,核心思想是"错误字节也会进 FIFO":
- 先清错误标志,并同步读取至多 16 字节(FIFO 深度)——
drain_fifo逐个检查rxerr(Overrun)、parityerr、framerr、rxnoise、deltarxbrk; - 若 FIFO 预读已满足请求长度,直接返回;
- 否则使能错误中断(
framerren/parityerren/rxnoiseen/rxerr)与dmarx,发起 DMA 读; - 用
embassy_futures::select::select同时等待"传输完成"与"错误唤醒"两个 future; - 若 DMA 先完成,仍要通过
rx_err.swap(false)检查最后一字节是否携带错误(Either::First分支的注释:错误可能发生在最后一个字节上); - 判定具体错误类型:按
framerrint→parityerrint→rxnoiseint→ FIFOrxerr的顺序检查中断状态寄存器。
6.4 阻塞与异步的 API 对照
| 能力 | 阻塞(Blocking) | 异步(Async) |
|---|---|---|
| 发送 | blocking_write/blocking_flush | write().await(DMA) |
| 接收 | blocking_read(轮询 FIFO) | read().await(FIFO 预读 + DMA) |
| 状态 | tx_busy() | 同上 |
| 构造 | new_blocking(无需 DMA 通道) | new(需要 IRQ 绑定 + TX/RX DMA 通道) |
UsartRx::new_inner中有一处debug_assert_eq!(has_irq, rx_dma.is_some()),即"有中断处理则必须有 DMA 通道",保证错误中断路径与 DMA 路径配对出现。异步构造的UsartRx在new_inner中会先unpend()再enable()对应 NVIC 中断。
七、从 lpc55-pac 迁移到 nxp-pac
变更条目:Moved NXP LPC55S69 from 'lpc55-pac' to 'nxp-pac'
这是版本中最具方向性的架构决策:LPC55S69 的 PAC 依赖从独立的lpc55-pac统一迁移到nxp-pac。从 embassy-nxp/Cargo.toml 可以看到:
nxp-pac同时是普通依赖(可选,rev = "98b09d2eae1f073804d6ded639c8dab583f614b0")与构建依赖(default-features = false, features = ["metadata"]);- 芯片选择 feature 直接透传 PAC 的芯片 feature:
lpc55-core0 = ["nxp-pac/lpc55s69_cm33_core0", "_lpc55"] lpc55s16 = ["nxp-pac/lpc55s16", "_lpc55"] mimxrt1011 = ["nxp-pac/mimxrt1011", "_rt1xxx", "dep:imxrt-rt"] mimxrt1062 = ["nxp-pac/mimxrt1062", "_rt1xxx", "dep:imxrt-rt"]- PAC 通过
unstable-pacfeature 在embassy_nxp::pac重导出;不启用时仅pub(crate)可见。Cargo.toml注释解释了为何这个重导出永远不稳定:embassy-nxp 的 semver-minor(非破坏性)发布可能对 PAC 做 major-bump(破坏性)升级,官方建议需要固定 PAC 版本的用户直接依赖固定版本的 PAC,且"没有计划让这一 feature 稳定"。
统一到nxp-pac后,SPI/USART 的寄存器访问都变成crate::pac::spi::Spi、crate::pac::usart::Usart、crate::pac::flexcomm::Flexcomm、crate::pac::sct0等统一命名空间;配合第三节的元数据 codegen,驱动代码不再感知具体芯片的 PAC crate 差异,同一套驱动代码可同时覆盖 LPC55 与 i.MX RT 两条产品线。
八、版本演进脉络小结
将各条目按依赖关系串联,可以还原 0.1.0 的开发顺序与设计意图:
- 基础设施先行:从
lpc55-pac迁移到nxp-pac,并引入基于METADATA的构建期 codegen,为多芯片支持打底; - 外设驱动按"阻塞 → 异步"梯度补齐:先有阻塞版 SPI(
Spi<'d, Blocking>+ embedded-hal 0.2 trait 实现),随后 DMA 控制器就绪,USART 才获得异步收发能力; - 重构改善可维护性:把 USART 的 ALT 定义下沉到 TX/RX 引脚 impl,移除内部
match_iocon宏,让引脚-外设匹配关系在类型层面表达; - PWM 提供基础输出能力:基于 SCT0 统一计数器,先做"简单"版本(单一周期、多通道占空比)。
对于想深入代码的读者,推荐按以下路径阅读:
- 入口与初始化:embassy-nxp/src/lib.rs(
init()、bind_interrupts!、Mode类型系统); - 阻塞 SPI 完整实现:embassy-nxp/src/spi/lpc55.rs;
- 异步 USART + DMA 竞争逻辑:embassy-nxp/src/usart/lpc55.rs;
- 构建期 codegen:embassy-nxp/build.rs 与 embassy-nxp/src/chips/lpc55.rs;
- PWM 实现:embassy-nxp/src/pwm/lpc55.rs。
需要注意的是,本 crate 仍处于 0.1.0 早期阶段,unstable-pac明确不稳定,SPI 的 DMA 版本在configure_spi中仍以注释"// DMA is going to be disabled until the async version is implemented"标记为未完成,LPC55 USART 的 CTS/RTS/SCK 引脚 trait 也带有TODO(wt): This needs to be wired up的未接线注释——这些都可以视为下一步版本的功能预告,引用该版本能力时应以这些实际代码状态为准。
- 嵌入式
- 物联网
- 异步编程
【免费下载链接】embassy
Modern embedded framework, using Rust and async.
相关推荐
MaterialStyledDialogs Builder API详解:轻松定制专属对话框
MaterialStyledDialogs Builder API详解:轻松定制专属对话框 MaterialStyledDialogs是一款专为Android开
Apache OpenDAL™ Operator 完全指南:异步与阻塞操作深度解析
Apache OpenDAL™ Operator 完全指南:异步与阻塞操作深度解析 Apache OpenDAL™ 是一个革命性的数据访问层,旨在为开发者提供统
数据存储后端screenshot-to-code 的提交历史与非阻塞多变体生成机制深度解析
screenshot to code 的提交历史与非阻塞多变体生成机制深度解析 screenshot to code 将一次代码生成结果组织为可回溯的“提交(C
人工智能大模型AI 应用代码生成
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考