知识门户

Back

Rust 开发 RP2350 双核:资源全景与 embassy-rp 核间通信实战#

RP2350 并不是 RP2040 的简单“超频版”。它真正的关键词有两个:双核双架构更强的外设与安全能力。对 Rust 开发者来说,RP2350 的双核既有诱人的算力,也带来一个现实问题:两个核上的异步任务怎么可靠地通信?

本文会先客观地梳理 RP2350 的资源,再说明它和 RP2040 在双核模型上的关键差异,最后以 embassy-rp 0.10.0 为基准,讲清楚 embassy-rp 中启动第二个核、运行第二套 executor,以及用 ChannelSignal、SIO FIFO 完成核间通信的方法。

时效性说明:本文写作时间为 2026-08-13。文中代码与 API 以 embassy-rp 0.10.0、embassy-executor 0.10.0、embassy-sync 0.8.0、embassy-time 0.5.1 为准。Embassy 仍处于 0.x 阶段,API 可能继续演进,建议最终以 docs.embassy.dev 中对应版本为准。

一、RP2350 的资源全景#

1.1 双核、双架构,但不是四核同时跑#

RP2350 在芯片上有两套“处理器槽位”:core 0 和 core 1。每个槽位都同时连接了一个 Arm Cortex-M33 和一个 Hazard3 RISC-V 处理器。同一时刻每个槽位只能激活其中一个架构,所以实际可用的逻辑 CPU 始终是两个,而不是四个。

更准确地说,RP2350 的双架构能力是:

  • 默认可以由引导 ROM 自动识别镜像架构,也可以由 OTP 固定启动架构;
  • core 0 与 core 1 的架构可以在启动阶段分别选择;
  • 常见的量产策略是通过 OTP 永久固定为双 Cortex-M33,或固定为双 Hazard3;
  • 在嵌入式 Rust 生态里,embassy-rp 当前主要针对 RP2350 的 Cortex-M33 路径;RISC-V 路径通常需要从 rp235x-hal 或直接使用 rp235x-pac 入手,不适合假设 embassy-rp 开箱即用地覆盖全部 RISC-V 能力。

所以,对本文的 embassy-rp 示例,默认场景是:两个 Cortex-M33 核心,目标三元组为 thumbv8m.main-none-eabihf

1.2 资源速览#

下表是 RP2350 的主要资源。它和 RP2040 相比,最明显的变化不只是主频,还包括 FPU、TrustZone、更大的 SRAM、HSTX、真随机数发生器、更细的低功耗域,以及对外部 PSRAM 的支持。

类别RP2350 关键能力
CPU双 Cortex-M33 @ 150 MHz,带 FPU/DSP;或双 Hazard3 RISC-V
安全Cortex-M33 TrustZone、Secure Boot、可选镜像签名与加密、8 KB OTP
SRAM520 KB,分布在 10 个可并行访问的存储体
外部存储无内置 Flash,通过 QSPI XIP 运行代码,支持 PSRAM
GPIORP2350A:30 个;RP2350B:48 个
模拟12-bit ADC:RP2350A 4 路,RP2350B 8 路
PWM24 通道
PIO3 个 PIO 块,共 12 个状态机
串行接口2 × UART、2 × SPI、2 × I2C
高速接口1 × HSTX、USB 1.1 Host/Device
DMA12 通道
定时器两个硬件定时器块、AON Timer、Watchdog
安全随机源硬件 TRNG

这里有一个容易被忽略的点:RP2350 的 520 KB SRAM 并不是单一连续块,而是多个 bank。对于双核同时访问不同 bank 的场景,这种设计有利于降低总线竞争;但对单核大数组场景,是否连续通常还要结合链接脚本和实际地址映射看,不应直接假设所有 520 KB 都能当一个线性大堆用。

1.3 与双核直接相关的硬件#

除了 CPU 本身,还有几组硬件资源专门服务于核间通信与同步:

  • SIO 核间 FIFO:两个单向 32 位 FIFO,一个由 core 0 写、core 1 读,另一个相反。RP2040 每方向深度为 8 项;RP2350 每方向深度为 4 项
  • 硬件自旋锁:32 个,用于跨核互斥。Embassy 的 critical-section-implSpinlockRawMutex 都会用到它们。
  • Doorbell:RP2350 新增 8 个门铃中断,可用于“只唤醒、不传数据”的轻量信号。RP2040 没有这套门铃机制。
  • FIFO 中断:RP2350 与 RP2040 的中断号分配不同,RP2350 的 FIFO 中断在两个核上使用同一个中断号,编写底层中断处理时要按当前核区分。

了解这些硬件是有意义的:embassy_rp::multicore 本身就会使用 SIO FIFO 完成 core 1 启动、暂停/恢复和中断唤醒,因此上层应用不要随意直接读写 SIO FIFO,否则可能干扰 Embassy 的控制协议。

二、embassy-rp 的双核启动模型#

2.1 核心思路:一个核一个 executor#

Embassy 的推荐模型并不是“把任务自动分发到两个核”,而是手动启动 core 1,并在 core 1 上运行第二个 Executor

core 0 和 core 1 各自维护自己的任务队列和调度器:

  • core 0 运行 executor 0,负责主流程;
  • core 1 运行 executor 1,负责实时、阻塞或高负载子任务;
  • 两个 executor 之间通过共享内存、embassy-sync 原语或硬件 FIFO 通信。

这一点非常重要:它意味着你可以在 core 1 上启动多个异步任务,而不是只能塞一个裸函数。

2.2 启动第二个核的最小骨架#

下面代码展示如何启动 core 1,并让两个核各自运行一个异步任务。

这个骨架中的关键点如下:

  • Stack<4096> 为 core 1 单独分配栈。栈太小会导致任务空间不足,实际项目应根据 core 1 任务深度调整。
  • spawn_core1 接收 p.CORE1、静态栈和 FnOnce() -> ! 风格的入口闭包。
  • core 1 的入口闭包会创建并运行第二个 Executor,因此它可以拥有自己的任务集合。
  • 两个 executor 都是静态分配,分别由 StaticCell 保管。

2.3 常用辅助 API#

embassy-rp 的 multicore 模块还提供了几个直接可用的辅助函数:

  • current_core():返回当前执行所在核,CoreId::Core0CoreId::Core1
  • pause_core1() / resume_core1():暂停和恢复 core 1;
  • Stack<N>:为 core 1 提供按 32 字节对齐的栈内存。

这些 API 在调试、低功耗和状态观测时很实用。例如,你可以让 core 0 在进入某种安全状态前调用 pause_core1(),避免两个核继续争用同一外设。

三、跨核通信的三种层次#

在 embassy-rp 上,跨核通信通常可以分成三个层次:

  1. 高层异步原语embassy-syncChannelSignalMutex 等,最推荐;
  2. 共享内存 + 互斥:借助 CriticalSectionRawMutex 或硬件自旋锁保护共享状态;
  3. 底层硬件协议:直接操作 SIO FIFO 或 Doorbell,灵活但需要自行处理与 Embassy 的冲突。

3.1 高层异步原语:推荐路线#

embassy-sync 提供了适合嵌入式异步场景的通信结构。跨核使用时,最重要的是选择正确的 mutex 类型:

use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel;
use embassy_sync::signal::Signal;
rust

CriticalSectionRawMutex 依赖 critical-section 实现。跨核安全的前提是在 embassy-rp 中启用 critical-section-impl feature。该 feature 会让 critical-section 使用 RP 的硬件自旋锁,而不是只关闭当前核的中断。

如果关闭当前核中断就能保证互斥,那在单核 Cortex-M 上通常成立;但双核中,core 0 关闭自己的中断并不会阻止 core 1 同时进入临界区。因此缺少 critical-section-impl 时,共享数据的“安全”是脆弱的。

Channel:带背压的有界消息队列#

Channel 适合命令、事件流等“每条消息都需要被处理”的场景。它是 MPMC 有界队列,满了以后发送方会等待,天然形成背压。

use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel;

#[derive(Clone, Copy, defmt::Format)]
enum Command {
    ReadAdc { seq: u32 },
    Shutdown,
}

static CMD: Channel<CriticalSectionRawMutex, Command, 4> = Channel::new();
rust

core 0 发送:

CMD.send(Command::ReadAdc { seq }).await;
rust

core 1 接收:

match CMD.receive().await {
    Command::ReadAdc { seq } => { /* ... */ }
    Command::Shutdown => { /* ... */ }
}
rust

Signal:只关心最新状态#

Signal 是单槽信号,重复写入会覆盖旧值。它适合“最新传感器读数”“当前状态”这类允许丢失中间值的场景。

#[derive(Clone, Copy, defmt::Format)]
struct Reading {
    seq: u32,
    raw: u16,
}

static TELEMETRY: Signal<CriticalSectionRawMutex, Reading> = Signal::new();
rust

core 1 上报:

TELEMETRY.signal(Reading { seq, raw });
rust

core 0 等待:

let reading = TELEMETRY.wait().await;
rust

一个完整通信示例#

把启动骨架与 ChannelSignal 组合起来,就能形成清晰的“命令/遥测”双核结构:

这个示例虽然是“假 ADC”,但结构可以直接替换成真实外设。它的通信关系很清楚:

  • core 0 通过 CMD 发送请求,并携带递增序号;
  • core 1 接收请求、执行工作,再通过 TELEMETRY 返回结果;
  • core 0 不轮询寄存器,而是在 TELEMETRY.wait().await 上等待异步通知。

3.2 共享内存与互斥#

如果需要共享一个较大的状态结构,而不是每次拷贝消息,可以使用异步互斥锁 embassy_sync::mutex::Mutex

如果你只需要保护一小段不跨越 .await 的临界区,也可以使用 embassy_sync::blocking_mutex::Mutex,它的 API 是 SHARED.lock(|data| { ... }) 这种闭包形式,不会产生异步等待。

跨核场景下,同样要开启 critical-section-impl。此外,Embassy 还提供 embassy_rp::spinlock_mutex::SpinlockRawMutex<N>,让用户显式选择某个硬件自旋锁;如果你的系统还同时使用 PAC 直接访问自旋锁,需要提前规划编号,避免冲突。

3.3 底层 SIO FIFO 与 Doorbell#

在某些延迟敏感的场景,开发者可能想直接使用 SIO FIFO。RP 的核间 FIFO 本质上是一个 32 位单向队列,写端阻塞到可写,读端阻塞到可读:

use cortex_m::asm;
use embassy_rp::pac;

fn fifo_write(value: u32) {
    let sio = pac::SIO;
    while !sio.fifo().st().read().rdy() {}
    sio.fifo().wr().write_value(value);
    asm::sev();
}

fn fifo_read() -> u32 {
    let sio = pac::SIO;
    while !sio.fifo().st().read().vld() {}
    sio.fifo().rd().read()
}
rust

这段代码只适合理解硬件原理,不建议在 embassy-rp 多核项目中随意使用。原因是:

  • spawn_core1pause_core1resume_core1 以及 interrupt executor 都依赖 FIFO 控制协议;
  • 如果应用层也写 FIFO,可能把控制令牌和业务数据混在一起,造成 core 1 卡死或状态错乱;
  • RP2350 的 FIFO 深度只有 4 项,比 RP2040 更小,直接用裸 FIFO 传连续数据流并不宽裕;
  • RP2350 的 SIO 区域还存在已知勘误(如 RP2350-E2 对自旋锁别名的影响),底层寄存器操作应优先依赖经过测试的 HAL/PAC 抽象。

如果确实需要“只唤醒、不传数据”,可以关注 RP2350 的 Doorbell。不过在 embassy-rp 的 multicore 模块中目前没有直接暴露高层 Doorbell API,通常需要借助 PAC 访问;非必要场景下,用 SignalChannel 往往更简单、更可移植。

四、工程配置要点#

要让上面的例子跑起来,除了 Rust 代码,还需要正确配置目标、链接脚本和依赖 feature。

一个 RP2350B/Pico 2 风格的 Cargo.toml 依赖片段如下:

其中 unstable-pac 只是为了前面 SIO FIFO 示例能访问 embassy_rp::pac;如果你的项目完全使用 embassy-sync 高层原语,可以省略它。

memory.x 也需要按 RP2350 地址空间配置。以板载 4 MB Flash 为例:

MEMORY
{
    FLASH : ORIGIN = 0x10000000, LENGTH = 4096K
    RAM   : ORIGIN = 0x20000000, LENGTH = 520K
}
text

.cargo/config.toml 中应指定 Cortex-M33 目标,并链接 defmt 脚本:

[build]
target = "thumbv8m.main-none-eabihf"

[target.thumbv8m.main-none-eabihf]
runner = "probe-rs run --chip RP2350"
rustflags = [
    "-C", "link-arg=-Tlink.x",
    "-C", "link-arg=-Tdefmt.x",
]
toml

最后再强调一遍:如果你的板子是 RP2350A 而不是 RP2350B,把 embassy-rp 的 feature 换成 rp235xa;如果板载 Flash 不是 4 MB,memory.xFLASH 长度要按实际芯片调整。

五、什么时候该用双核?#

双核不是免费的午餐,它同时带来共享外设竞争、临界区、调试和功耗等问题。客观地说,以下场景更适合把 core 1 单独拉出来:

  • 需要长时间运行的实时采集、编解码、电机控制或软 DSP;
  • 不希望慢速外设或复杂协议阻塞 core 0 的 UI/控制流程;
  • 需要把安全敏感流程和普通业务放在不同执行域;
  • 单核已经无法满足延迟预算,且任务之间有清晰边界。

反过来,如果任务本身只是几个低频异步任务,或者外设竞争比并行收益更大,单核 + Embassy 异步通常已经足够。先测量,再决定是否引入第二个核,比一开始就把系统切成双核更稳健。

六、总结#

RP2350 的双核并不是“四核”,而是两个处理器槽位在 Arm 与 RISC-V 之间二选一。对当前 Rust/Embassy 实践,最成熟的是双 Cortex-M33 路线。embassy-rp 的双核模型可以概括为:

  1. spawn_core1 启动第二个核;
  2. 在 core 1 上运行独立的 Executor
  3. embassy-syncChannelSignalMutex 跨核通信;
  4. 开启 critical-section-impl,让 critical-section 在双核下真正安全;
  5. 底层 SIO FIFO 和 Doorbell 可作为性能手段,但需要理解 Embassy 内部协议,避免冲突。
Rust 开发 RP2350 双核:资源全景与 embassy-rp 核间通信实战
https://glinfei.space/blog/nostd-rust/rp2350%E5%8F%8C%E6%A0%B8%E4%B8%8Eembassy-rp%E6%A0%B8%E9%97%B4%E9%80%9A%E4%BF%A1
Author 甘霖飞
Published at 2026年8月13日
Comment seems to stuck. Try to refresh?✨