第 10 章:任务间通信——Channel、Signal、Mutex 与 PubSub
Part2 深入embassy框架
第 10 章:任务间通信——Channel、Signal、Mutex 与 PubSub#
本章要回答的问题:Embassy 的任务之间如何传递数据?Channel、Signal、Mutex、PubSub 分别适用于什么场景?与 FreeRTOS 的队列/信号量/事件组有何对应关系?
本章定位:掌握 Embassy 的四种同步原语。每种原语都有完整的 API 讲解、C/FreeRTOS 对比、使用场景分析和实战示例。学完本章,你将能够为任何多任务通信需求选择正确的原语。
10.1 问题:任务之间如何交换数据?#
10.1.1 C 世界的”三件套”#
在 C + FreeRTOS 中,任务间通信依赖三个机制:
// 1. 队列(Queue):传递数据
QueueHandle_t sensor_queue = xQueueCreate(4, sizeof(SensorData));
xQueueSend(sensor_queue, &data, portMAX_DELAY);
xQueueReceive(sensor_queue, &data, portMAX_DELAY);
// 2. 信号量(Semaphore):同步/通知
SemaphoreHandle_t data_ready = xSemaphoreCreateBinary();
xSemaphoreGive(data_ready);
xSemaphoreTake(data_ready, portMAX_DELAY);
// 3. 互斥量(Mutex):保护共享资源
SemaphoreHandle_t i2c_mutex = xSemaphoreCreateMutex();
xSemaphoreTake(i2c_mutex, portMAX_DELAY);
// 使用 I2C...
xSemaphoreGive(i2c_mutex);cC 的问题:
| 问题 | 示例 |
|---|---|
| 类型不安全 | xQueueSend(queue, &data, ...) 中 &data 是 void*,传错类型编译器不管 |
| 大小不匹配 | 队列创建时 sizeof(SensorData) 与发送时的结构体大小不一致 → 内存损坏 |
| 死锁风险 | 两个任务以不同顺序获取两个 Mutex → 死锁(运行时才发现) |
| 优先级反转 | 低优先级任务持有 Mutex,高优先级任务被阻塞 → 需要优先级继承 |
| 忘记释放 | xSemaphoreTake 后忘记 xSemaphoreGive → 其他任务永久阻塞 |
| 全局变量 | 不用 RTOS 原语时,volatile 全局变量 + 临界区 → 竞态条件 |
10.1.2 Embassy 的答案#
Embassy 提供四种通信原语,全部在 embassy-sync crate 中:
| Embassy 原语 | 功能 | FreeRTOS 对应 | C 裸机对应 |
|---|---|---|---|
Channel | 多对多数据传递 | Queue | 环形缓冲区 + 标志 |
Signal | 一对一单值通知 | Binary Semaphore | volatile 标志 |
Mutex | 互斥访问共享资源 | Mutex Semaphore | 临界区 + 标志 |
PubSub | 一对多广播 | Event Group + Queue | 回调函数列表 |
Embassy 的核心优势:
- 类型安全:
Channel<Mutex, SensorData, 4>在编译期确定传递的数据类型 - 无死锁:协作式调度 + 单线程 Executor → 不可能死锁
- 无优先级反转:没有优先级 → 没有反转
- 无
volatile:借用检查器保证数据一致性 - 零堆分配:所有原语都是
static变量,编译期确定大小
10.2 Channel:多对多数据传递#
10.2.1 什么是 Channel?#
Channel 是一个固定容量的异步 FIFO 队列:
- 发送方:
send().await——队列满时挂起 - 接收方:
receive().await——队列空时挂起 - 容量:编译期确定(泛型参数)
- 多对多:多个任务可以同时发送和接收
┌──────────┐ send().await ┌─────────────────┐ receive().await ┌──────────┐
│ Task A │ ──────────────────► │ │ ◄──────────────────── │ Task X │
│ (生产者) │ │ Channel │ │ (消费者) │
└──────────┘ │ [T; N] │ └──────────┘
│ │
┌──────────┐ send().await │ ┌───┬───┬───┐ │ receive().await ┌──────────┐
│ Task B │ ──────────────────► │ │ 0 │ 1 │ 2 │ │ ◄──────────────────── │ Task Y │
│ (生产者) │ │ └───┴───┴───┘ │ │ (消费者) │
└──────────┘ │ 容量 = N │ └──────────┘
└─────────────────┘plaintext10.2.2 API 详解#
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
// 声明:Channel<互斥锁类型, 数据类型, 容量>
static SENSOR_CH: Channel = Channel::new();
// ↑ ↑ ↑
// 保护内部状态的锁 传递的数据类型 队列容量(编译期)
#[derive(Clone, Copy)]
struct SensorData {
temperature: i16,
humidity: u16,
timestamp: u32,
}rust发送操作#
// 异步发送(队列满时挂起)
SENSOR_CH.send(data).await;
// 尝试发送(不挂起,立即返回)
match SENSOR_CH.try_send(data) {
Ok(()) => { /* 发送成功 */ }
Err(TrySendError::Full(data)) => { /* 队列满,data 被退回 */ }
}
// 带超时的发送
use embassy_futures::select::{select, Either};
use embassy_time::{Timer, Duration};
match select(
SENSOR_CH.send(data),
Timer::after(Duration::from_millis(100)),
).await {
Either::First(()) => { /* 发送成功 */ }
Either::Second(()) => { /* 超时 */ }
}rust接收操作#
// 异步接收(队列空时挂起)
let data: SensorData = SENSOR_CH.receive().await;
// 尝试接收(不挂起,立即返回)
match SENSOR_CH.try_receive() {
Ok(data) => { /* 收到数据 */ }
Err(TryReceiveError::Empty) => { /* 队列为空 */ }
}rust查询状态#
// 当前队列中的元素数量
let len: usize = SENSOR_CH.len();
// 队列是否为空/满
let is_empty: bool = SENSOR_CH.is_empty();
let is_full: bool = SENSOR_CH.is_full();
// 剩余容量
let capacity: usize = SENSOR_CH.capacity(); // 始终为 4(编译期确定)rust10.2.3 完整示例:传感器 → 处理 → 显示#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::{self as hal, i2c::I2c};
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Ticker};
use defmt_rtt as _;
use panic_probe as _;
#[derive(Clone, Copy, defmt::Format)]
struct SensorData {
temperature: i16, // 0.1°C 单位
humidity: u16, // 0.1% 单位
}
// 两个 Channel:传感器→处理,处理→显示
static RAW_CH: Channel = Channel::new();
static PROCESSED_CH: Channel = Channel::new();
// 任务 1:读取传感器(每 500ms)
#[embassy_executor::task]
async fn sensor_task(mut i2c: I2c<'static, hal::i2c::I2c1>) {
let mut ticker = Ticker::every(Duration::from_millis(500));
loop {
ticker.next().await;
let mut buf = [0u8; 4];
if i2c.write_read(0x76, &[0xFA], &mut buf).await.is_ok() {
let data = SensorData {
temperature: i16::from_be_bytes([buf[0], buf[1]]),
humidity: u16::from_be_bytes([buf[2], buf[3]]),
};
// 发送到处理任务
// 如果队列满(处理任务来不及处理),这里会等待
RAW_CH.send(data).await;
}
}
}
// 任务 2:数据处理(滤波、校准)
#[embassy_executor::task]
async fn process_task() {
let mut filter_temp: i32 = 0;
let mut filter_hum: i32 = 0;
let mut count: u32 = 0;
loop {
// 等待原始数据(队列空时挂起,CPU 空闲)
let raw = RAW_CH.receive().await;
// 简单移动平均滤波
count += 1;
filter_temp += (raw.temperature as i32 - filter_temp) / count.min(10) as i32;
filter_hum += (raw.humidity as i32 - filter_hum) / count.min(10) as i32;
let processed = SensorData {
temperature: filter_temp as i16,
humidity: filter_hum as u16,
};
// 发送到显示任务
PROCESSED_CH.send(processed).await;
}
}
// 任务 3:显示(每 200ms 刷新)
#[embassy_executor::task]
async fn display_task() {
let mut ticker = Ticker::every(Duration::from_millis(200));
let mut latest = SensorData { temperature: 0, humidity: 0 };
loop {
ticker.next().await;
// 非阻塞获取最新数据(如果有)
while let Ok(data) = PROCESSED_CH.try_receive() {
latest = data; // 只保留最新的
}
// 更新显示
defmt::info!("显示: T={}.{:01}°C H={}.{:01}%",
latest.temperature / 10, (latest.temperature % 10).abs(),
latest.humidity / 10, latest.humidity % 10);
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = hal::init(Default::default());
let mut i2c_cfg = hal::i2c::Config::default();
i2c_cfg.frequency = hal::i2c::Frequency::F400k;
let i2c = I2c::new(p.I2C1, p.PB6, p.PB7, p.DMA1_CH0, p.DMA1_CH1, i2c_cfg).unwrap();
spawner.spawn(sensor_task(i2c)).unwrap();
spawner.spawn(process_task()).unwrap();
spawner.spawn(display_task()).unwrap();
}rust10.2.4 与 FreeRTOS Queue 的对比#
| 方面 | FreeRTOS xQueue | Embassy Channel |
|---|---|---|
| 创建 | xQueueCreate(4, sizeof(SensorData)) | static CH: Channel<..., SensorData, 4> = Channel::new() |
| 类型安全 | ❌ void*(传错类型无警告) | ✅ 泛型参数(编译期检查) |
| 内存分配 | 动态(pvPortMalloc) | 静态(.bss 段) |
| 发送 | xQueueSend(q, &data, timeout) | CH.send(data).await |
| 接收 | xQueueReceive(q, &data, timeout) | let data = CH.receive().await |
| 超时 | pdMS_TO_TICKS(100) | select(send, Timer::after(...)) |
| 满/空检查 | uxQueueSpacesAvailable() | CH.is_full() / CH.is_empty() |
| 中断安全 | xQueueSendFromISR() | CH.try_send()(在中断中) |
| 所有权 | 复制(memcpy) | 移动(零拷贝) |
| 数据大小限制 | 无(但大对象复制开销大) | 建议 Copy 类型(小对象) |
关键差异:所有权 vs 复制
// FreeRTOS:数据被复制到队列中
SensorData data = { .temp = 250, .hum = 600 };
xQueueSend(queue, &data, 0); // memcpy 到队列内部缓冲区
// data 仍然有效,可以继续使用
data.temp = 300; // 不影响队列中的副本c// Embassy:数据被移动到 Channel 中
let data = SensorData { temperature: 250, humidity: 600 };
SENSOR_CH.send(data).await;
// data 已经被移动!不能再使用
// data.temperature = 300; // ❌ 编译错误:use of moved valuerust对于 Copy 类型(如 SensorData),移动和复制的机器码完全相同。但对于大对象,Rust 的所有权系统强制你思考”谁拥有这个数据”——这避免了 C 中常见的”谁负责释放”问题。
10.2.5 Channel 容量选择指南#
| 容量 | 适用场景 | 行为 |
|---|---|---|
1 | 只关心最新值(如传感器读数) | 生产者可能被阻塞(队列满) |
2-4 | 允许短暂突发(如 UART 字节流) | 缓冲突发,平滑速率差异 |
8-16 | 速率差异大(如高速 ADC → 慢速处理) | 较大缓冲,但占用更多 RAM |
> 16 | 极少需要(考虑是否设计有问题) | RAM 开销显著 |
RAM 计算:
Channel 占用 = 容量 × sizeof(T) + 内部状态(~16 字节)
示例:Channel<..., SensorData, 4>
= 4 × 8 字节 + 16 字节
= 48 字节
对比 FreeRTOS Queue:
= 4 × 8 字节 + 队列头(~80 字节)+ 动态分配开销
= ~112 字节 + malloc 碎片plaintext10.3 Signal:一对一单值通知#
10.3.1 什么是 Signal?#
Signal 是一个容量为 1 的 Channel——但它有特殊的覆盖语义:
- 如果 Signal 为空:
signal(value)存储值 - 如果 Signal 已有值:
signal(new_value)覆盖旧值 wait().await:取出值(Signal 变空),如果为空则挂起
┌──────────┐ signal(42) ┌─────────────┐ wait().await ┌──────────┐
│ Task A │ ─────────────────► │ Signal │ ◄──────────────── │ Task B │
│ (发送方) │ │ [42] │ │ (接收方) │
└──────────┘ │ 容量 = 1 │ └──────────┘
└─────────────┘
如果 Task A 连续发送:
signal(1) → Signal = [1]
signal(2) → Signal = [2] ← 覆盖了 1!
signal(3) → Signal = [3] ← 覆盖了 2!
Task B 的 wait() 只会收到 3(最新值)plaintext10.3.2 API 详解#
use embassy_sync::signal::Signal;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
static BUTTON_SIGNAL: Signal = Signal::new();
#[derive(Clone, Copy)]
enum ButtonEvent {
Pressed,
Released,
LongPress,
}rust发送(通知)#
// 发送信号(永不阻塞,永不失败)
BUTTON_SIGNAL.signal(ButtonEvent::Pressed);
// 在中断中发送(安全!)
#[interrupt]
fn EXTI0() {
BUTTON_SIGNAL.signal(ButtonEvent::Pressed);
// 清除中断标志...
}rust接收(等待)#
// 异步等待信号(Signal 为空时挂起)
let event: ButtonEvent = BUTTON_SIGNAL.wait().await;
// 非阻塞检查
match BUTTON_SIGNAL.try_take() {
Some(event) => { /* 有信号 */ }
None => { /* 无信号 */ }
}rust10.3.3 完整示例:按键事件通知#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::{self as hal, gpio::{Input, Pull, Level, Output, Speed}};
use embassy_sync::signal::Signal;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Timer};
use defmt_rtt as _;
use panic_probe as _;
static BUTTON_SIG: Signal = Signal::new();
#[derive(Clone, Copy, PartialEq, defmt::Format)]
enum ButtonEvent {
ShortPress,
LongPress,
}
// 任务 1:按键检测(产生事件)
#[embassy_executor::task]
async fn button_task(mut button: Input<'static>) {
loop {
// 等待按下
button.wait_for_falling_edge().await;
Timer::after(Duration::from_millis(20)).await; // 消抖
if button.is_low() {
// 记录按下时间,等待释放或长按
let press_result = embassy_futures::select::select(
button.wait_for_rising_edge(), // 释放
Timer::after(Duration::from_secs(2)), // 2 秒长按
).await;
match press_result {
embassy_futures::select::Either::First(()) => {
// 在 2 秒内释放 → 短按
BUTTON_SIG.signal(ButtonEvent::ShortPress);
}
embassy_futures::select::Either::Second(()) => {
// 超过 2 秒 → 长按
BUTTON_SIG.signal(ButtonEvent::LongPress);
// 等待释放(避免重复触发)
button.wait_for_rising_edge().await;
}
}
}
}
}
// 任务 2:LED 控制(消费事件)
#[embassy_executor::task]
async fn led_task(mut led: Output<'static>) {
let mut blink_mode = false;
loop {
// 等待按键事件(无事件时挂起,CPU 空闲)
let event = BUTTON_SIG.wait().await;
match event {
ButtonEvent::ShortPress => {
// 短按:切换 LED
led.toggle();
defmt::info!("短按:LED {}", if led.is_set_high() { "ON" } else { "OFF" });
}
ButtonEvent::LongPress => {
// 长按:切换闪烁模式
blink_mode = !blink_mode;
defmt::info!("长按:闪烁模式 {}", if blink_mode { "ON" } else { "OFF" });
}
}
}
}
// 任务 3:闪烁执行(独立于按键)
#[embassy_executor::task]
async fn blink_task(mut led: Output<'static>) {
loop {
// 这里需要共享 LED——实际中应使用 Mutex(见 10.4 节)
// 简化示例:假设 led_task 和 blink_task 不同时操作
Timer::after(Duration::from_millis(200)).await;
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = hal::init(Default::default());
let button = Input::new(p.PA0, Pull::Up);
let led = Output::new(p.PA5, Level::Low, Speed::Low);
spawner.spawn(button_task(button)).unwrap();
spawner.spawn(led_task(led)).unwrap();
}rust10.3.4 与 FreeRTOS Binary Semaphore 的对比#
| 方面 | FreeRTOS Binary Semaphore | Embassy Signal |
|---|---|---|
| 创建 | xSemaphoreCreateBinary() | static SIG: Signal<..., T> = Signal::new() |
| 发送 | xSemaphoreGive(sem) | SIG.signal(value) |
| 接收 | xSemaphoreTake(sem, timeout) | let v = SIG.wait().await |
| 携带数据 | ❌ 不能(只是”有/无”) | ✅ 可以(泛型类型 T) |
| 覆盖语义 | 多次 Give = 一次(二值) | 多次 signal = 保留最新值 |
| 中断安全 | xSemaphoreGiveFromISR() | SIG.signal()(直接调用,安全) |
| 内存 | 动态分配 | 静态分配 |
| 类型安全 | ❌ 无数据 | ✅ 编译期类型检查 |
Signal 的最大优势:它可以携带类型化的数据。FreeRTOS 的 Binary Semaphore 只能表达”有/无”,如果要传递数据,必须配合全局变量(volatile + 临界区)。Signal 把”通知”和”数据”合为一体。
10.3.5 Signal vs Channel(容量=1)#
// 这两个看起来很像:
static SIG: Signal = Signal::new();
static CH: Channel = Channel::new();rust| 行为 | Signal | Channel<..., 1> |
|---|---|---|
| 发送时已有值 | 覆盖旧值 | 阻塞等待空间 |
| 语义 | ”最新状态是什么" | "每个事件都要处理” |
| 丢失数据 | ✅ 允许(只保留最新) | ❌ 不允许(FIFO 保证) |
| 适用场景 | 按键状态、模式切换 | 传感器数据流、命令队列 |
选择规则:
- 如果你关心”每一个”事件 → Channel
- 如果你只关心”最新的”状态 → Signal
10.4 Mutex:互斥访问共享资源#
10.4.1 为什么 Embassy 还需要 Mutex?#
你可能会问:Embassy 是单线程协作式调度,为什么还需要 Mutex?
答案:因为有些资源被多个任务使用,而任务在 .await 点让出 CPU。
// ❌ 错误:两个任务共享同一个 I2C 实例
static I2C: I2c<'static> = /* ... */;
#[embassy_executor::task]
async fn task_a() {
I2C.write_read(0x76, &[0xFA], &mut buf).await; // ← await 点!
// 在 await 期间,task_b 可能也在使用 I2C!
}
#[embassy_executor::task]
async fn task_b() {
I2C.write_read(0x68, &[0x75], &mut buf).await; // ← 冲突!
}rust问题:i2c.write_read().await 在 DMA 传输期间让出 CPU。如果此时另一个任务也发起 I2C 传输,两个 DMA 请求会冲突。
解决:用 Mutex 保护共享资源。
10.4.2 Embassy Mutex 的两种形式#
形式一:embassy_sync::mutex::Mutex(异步 Mutex)#
use embassy_sync::mutex::Mutex;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
// 声明共享资源
static I2C_BUS: Mutex>>> =
Mutex::new(RefCell::new(None));
// 初始化(在 main 中)
async fn init(p: hal::Peripherals) {
let i2c = I2c::new(p.I2C1, p.PB6, p.PB7, p.DMA1_CH0, p.DMA1_CH1, cfg).unwrap();
*I2C_BUS.lock().await.borrow_mut() = Some(i2c);
}
// 使用(在任何任务中)
async fn read_sensor() -> Result<[u8; 3], Error> {
// 获取锁(如果其他任务持有,挂起等待)
let guard = I2C_BUS.lock().await;
let mut i2c_ref = guard.borrow_mut();
let i2c = i2c_ref.as_mut().unwrap();
// 在锁保护下使用 I2C
let mut buf = [0u8; 3];
i2c.write_read(0x76, &[0xFA], &mut buf).await?;
Ok(buf)
// guard 在这里 drop → 自动释放锁
}rust形式二:embassy_sync::blocking_mutex::Mutex(同步 Mutex)#
use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use core::cell::RefCell;
// 用于保护不需要 .await 的简单数据
static COUNTER: BlockingMutex> =
BlockingMutex::new(RefCell::new(0));
// 使用(同步,不挂起)
fn increment() {
COUNTER.lock(|c| {
*c.borrow_mut() += 1;
});
// 锁在闭包结束时自动释放
}rust10.4.3 两种 Mutex 的选择#
异步 Mutex | 同步 BlockingMutex | |
|---|---|---|
锁内可以 .await? | ✅ 可以 | ❌ 不可以 |
| 获取锁时可能挂起? | ✅ 可能 | ❌ 不会(临界区内) |
| 适用场景 | 保护 I2C/SPI/UART 等外设 | 保护计数器、标志、配置 |
| 性能 | 略低(需要挂起/恢复) | 极高(只是关/开中断) |
| 死锁风险 | 理论上存在(但协作式下极难触发) | 无(临界区不可嵌套等待) |
10.4.4 完整示例:共享 UART 日志#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::{self as hal, usart::Uart};
use embassy_sync::mutex::Mutex;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Timer};
use core::cell::RefCell;
use defmt_rtt as _;
use panic_probe as _;
// 共享 UART(多个任务都需要输出日志)
static UART: Mutex>>> =
Mutex::new(RefCell::new(None));
// 封装一个异步日志函数
async fn log(msg: &[u8]) {
let guard = UART.lock().await;
let mut uart_ref = guard.borrow_mut();
let uart = uart_ref.as_mut().unwrap();
uart.write(msg).await.unwrap();
uart.write(b"\r\n").await.unwrap();
// guard drop → 释放锁
}
// 任务 1:传感器日志
#[embassy_executor::task]
async fn sensor_logger() {
loop {
Timer::after(Duration::from_secs(1)).await;
log(b"[SENSOR] temp=25.3C hum=60.1%").await;
}
}
// 任务 2:系统日志
#[embassy_executor::task]
async fn system_logger() {
let mut uptime = 0u32;
loop {
Timer::after(Duration::from_secs(5)).await;
uptime += 5;
// 格式化(实际中使用 heapless::String)
log(b"[SYSTEM] uptime=5s OK").await;
}
}
// 任务 3:错误日志
#[embassy_executor::task]
async fn error_monitor() {
loop {
Timer::after(Duration::from_millis(100)).await;
// 检查错误条件...
// if error_detected {
// log(b"[ERROR] I2C timeout!").await;
// }
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = hal::init(Default::default());
// 初始化 UART 并放入 Mutex
let cfg = hal::usart::Config::default();
let uart = Uart::new(p.USART2, p.PA3, p.PA2, p.DMA1_CH5, p.DMA1_CH6, cfg).unwrap();
*UART.lock().await.borrow_mut() = Some(uart);
spawner.spawn(sensor_logger()).unwrap();
spawner.spawn(system_logger()).unwrap();
spawner.spawn(error_monitor()).unwrap();
log(b"[MAIN] All tasks started").await;
}rust10.4.5 与 FreeRTOS Mutex 的对比#
| 方面 | FreeRTOS Mutex | Embassy Mutex |
|---|---|---|
| 创建 | xSemaphoreCreateMutex() | static M: Mutex<..., T> = Mutex::new(...) |
| 获取 | xSemaphoreTake(m, timeout) | let guard = M.lock().await |
| 释放 | xSemaphoreGive(m)(手动!) | 自动(guard drop) |
| 忘记释放 | 死锁(运行时) | 不可能(RAII,编译器保证) |
| 优先级继承 | 内置(防止优先级反转) | 不需要(无优先级) |
| 递归锁 | 需要 xSemaphoreCreateRecursiveMutex() | 不支持(也不需要) |
| 锁内阻塞 | 可以(但危险) | 异步 Mutex 可以 .await |
| 类型安全 | ❌(Mutex 和数据分离) | ✅(Mutex 包裹数据) |
最关键的差异:RAII 自动释放
// C:忘记释放 Mutex → 死锁
void task_a(void) {
xSemaphoreTake(mutex, portMAX_DELAY);
do_something();
if (error) {
return; // ❌ 忘记 xSemaphoreGive!其他任务永久阻塞!
}
xSemaphoreGive(mutex);
}c// Rust:不可能忘记释放
async fn task_a() {
let guard = M.lock().await;
do_something();
if error {
return; // ✅ guard 自动 drop → 锁自动释放!
}
// guard 在这里 drop → 锁自动释放
}rust这就是 RAII(Resource Acquisition Is Initialization)的力量——资源的生命周期绑定到变量的生命周期。编译器保证:只要变量活着,资源就被持有;变量死了,资源就被释放。没有例外,没有”忘记”。
10.4.6 替代方案:所有权转移(无锁)#
在很多情况下,你可以完全避免 Mutex——通过所有权转移:
// ❌ 需要 Mutex:两个任务共享 I2C
static I2C: Mutex<..., I2c> = ...;
// ✅ 无锁:每个任务拥有自己的外设
#[embassy_executor::task]
async fn sensor_task(i2c: I2c<'static, hal::i2c::I2c1>) {
// i2c 被移动到这个任务中,只有这个任务能使用它
loop {
i2c.write_read(0x76, &[0xFA], &mut buf).await.unwrap();
}
}
#[embassy_executor::task]
async fn gyro_task(i2c: I2c<'static, hal::i2c::I2c2>) {
// 使用不同的 I2C 外设,完全无冲突
loop {
i2c.write_read(0x68, &[0x75], &mut buf).await.unwrap();
}
}rust设计原则:
能用所有权解决的,不用 Mutex。必须共享的,才用 Mutex。
| 场景 | 推荐方案 |
|---|---|
| 每个任务用不同的外设 | 所有权转移(spawn 时传入) |
| 多个任务用同一个外设 | Mutex 保护 |
| 多个任务用同一个外设的不同功能 | 考虑 HAL 是否提供分离的句柄 |
| 简单的共享计数器/标志 | BlockingMutex<RefCell<T>> |
10.5 PubSub:一对多广播#
10.5.1 什么是 PubSub?#
PubSub(Publish-Subscribe)是一个一对多广播通道:
- 发布者:
publish(value)——所有订阅者都会收到 - 订阅者:
next_message().await——等待新消息 - 每个订阅者有独立的消息队列
- 新订阅者不会收到订阅之前的消息
┌──────────────┐
┌──► │ Subscriber 1 │ (独立队列)
│ └──────────────┘
┌───────────┐ publish(v) │ ┌──────────────┐
│ Publisher │ ───────────────┼──► │ Subscriber 2 │ (独立队列)
└───────────┘ │ └──────────────┘
│ ┌──────────────┐
└──► │ Subscriber 3 │ (独立队列)
└──────────────┘plaintext10.5.2 API 详解#
use embassy_sync::pubsub::PubSubChannel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
// 声明:PubSubChannel<锁类型, 消息类型, 每订阅者容量, 最大订阅者数, 最大发布者数>
static EVENT_BUS: PubSubChannel =
PubSubChannel::new();
// ↑ ↑ ↑ ↑ ↑
// 锁类型 消息类型 容量 订阅者 发布者
#[derive(Clone, Copy, defmt::Format)]
enum SystemEvent {
ButtonPressed,
SensorUpdated { temp: i16 },
WifiConnected,
WifiDisconnected,
BatteryLow { percent: u8 },
}rust发布者#
// 获取发布者(最多 1 个,由泛型参数限制)
let publisher = EVENT_BUS.publisher().unwrap();
// 发布消息(永不阻塞,永不失败)
publisher.publish(SystemEvent::ButtonPressed);
publisher.publish(SystemEvent::SensorUpdated { temp: 253 });rust订阅者#
// 获取订阅者(最多 4 个)
let mut sub1 = EVENT_BUS.subscriber().unwrap();
let mut sub2 = EVENT_BUS.subscriber().unwrap();
// 异步等待消息
let event = sub1.next_message().await; // 无新消息时挂起
// 非阻塞检查
match sub1.try_next_message() {
Some(event) => { /* 有新消息 */ }
None => { /* 无新消息 */ }
}rust10.5.3 完整示例:系统事件总线#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::{self as hal, gpio::{Input, Pull, Level, Output, Speed}};
use embassy_sync::pubsub::PubSubChannel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Timer, Ticker};
use defmt_rtt as _;
use panic_probe as _;
#[derive(Clone, Copy, defmt::Format)]
enum SystemEvent {
ButtonShort,
ButtonLong,
TempUpdate(i16),
BatteryLow(u8),
}
// 事件总线:最多 4 个订阅者,每个订阅者缓冲 4 条消息
static EVENT_BUS: PubSubChannel =
PubSubChannel::new();
// ===== 发布者 1:按键事件 =====
#[embassy_executor::task]
async fn button_publisher(mut button: Input<'static>) {
let pub1 = EVENT_BUS.publisher().unwrap();
loop {
button.wait_for_falling_edge().await;
Timer::after(Duration::from_millis(20)).await;
if button.is_low() {
let result = embassy_futures::select::select(
button.wait_for_rising_edge(),
Timer::after(Duration::from_secs(2)),
).await;
match result {
embassy_futures::select::Either::First(()) => {
pub1.publish(SystemEvent::ButtonShort);
}
embassy_futures::select::Either::Second(()) => {
pub1.publish(SystemEvent::ButtonLong);
button.wait_for_rising_edge().await;
}
}
}
}
}
// ===== 发布者 2:传感器事件 =====
#[embassy_executor::task]
async fn sensor_publisher() {
let pub2 = EVENT_BUS.publisher().unwrap();
let mut ticker = Ticker::every(Duration::from_secs(2));
loop {
ticker.next().await;
// 读取传感器...
let temp: i16 = 253; // 模拟 25.3°C
pub2.publish(SystemEvent::TempUpdate(temp));
// 检查电池...
let battery: u8 = 15; // 模拟 15%
if battery < 20 {
pub2.publish(SystemEvent::BatteryLow(battery));
}
}
}
// ===== 订阅者 1:LED 控制 =====
#[embassy_executor::task]
async fn led_subscriber(mut led: Output<'static>) {
let mut sub = EVENT_BUS.subscriber().unwrap();
loop {
let event = sub.next_message().await;
match event {
SystemEvent::ButtonShort => led.toggle(),
SystemEvent::BatteryLow(_) => {
// 低电量:快速闪烁
for _ in 0..5 {
led.toggle();
Timer::after(Duration::from_millis(100)).await;
}
}
_ => {} // 忽略其他事件
}
}
}
// ===== 订阅者 2:日志输出 =====
#[embassy_executor::task]
async fn log_subscriber() {
let mut sub = EVENT_BUS.subscriber().unwrap();
loop {
let event = sub.next_message().await;
defmt::info!("[EVENT] {:?}", event);
}
}
// ===== 订阅者 3:显示更新 =====
#[embassy_executor::task]
async fn display_subscriber() {
let mut sub = EVENT_BUS.subscriber().unwrap();
let mut last_temp: i16 = 0;
loop {
let event = sub.next_message().await;
match event {
SystemEvent::TempUpdate(t) => {
last_temp = t;
// 更新 OLED 显示...
defmt::info!("[DISPLAY] Temp: {}.{:01}°C", t / 10, (t % 10).abs());
}
SystemEvent::BatteryLow(pct) => {
defmt::warn!("[DISPLAY] LOW BATTERY: {}%", pct);
}
_ => {}
}
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = hal::init(Default::default());
let button = Input::new(p.PA0, Pull::Up);
let led = Output::new(p.PA5, Level::Low, Speed::Low);
// 启动发布者
spawner.spawn(button_publisher(button)).unwrap();
spawner.spawn(sensor_publisher()).unwrap();
// 启动订阅者
spawner.spawn(led_subscriber(led)).unwrap();
spawner.spawn(log_subscriber()).unwrap();
spawner.spawn(display_subscriber()).unwrap();
defmt::info!("事件总线系统启动");
}rust10.5.4 与 FreeRTOS Event Group + Queue 的对比#
在 FreeRTOS 中实现一对多广播,通常需要组合使用 Event Group 和 Queue:
// FreeRTOS:一对多广播(复杂!)
EventGroupHandle_t event_group;
QueueHandle_t event_queue;
// 发布者
void publish_event(SystemEvent event) {
xQueueSend(event_queue, &event, 0);
xEventGroupSetBits(event_group, BIT_0); // 通知所有等待者
}
// 订阅者(每个订阅者需要独立的逻辑)
void subscriber_task(void *params) {
while (1) {
// 等待事件组
xEventGroupWaitBits(event_group, BIT_0, pdTRUE, pdFALSE, portMAX_DELAY);
// 从队列中取数据(问题:多个订阅者竞争同一个队列!)
SystemEvent event;
if (xQueueReceive(event_queue, &event, 0) == pdTRUE) {
handle_event(event);
}
}
}cFreeRTOS 方案的问题:
- 多个订阅者竞争同一个 Queue → 只有一个能收到消息
- 需要为每个订阅者创建独立的 Queue → 发布者要向所有 Queue 发送
- Event Group 只有 24 位 → 事件类型有限
- 组合使用复杂,容易出错
| 方面 | FreeRTOS(Event Group + Queue) | Embassy PubSub |
|---|---|---|
| 一对多 | 需要手动实现(每订阅者一个 Queue) | 内置(自动分发) |
| 订阅者独立性 | 需要手动管理 | 自动(每订阅者独立队列) |
| 事件携带数据 | 需要额外 Queue | 内置(泛型类型) |
| 新订阅者 | 需要修改发布者代码 | 无需(动态订阅) |
| 代码量 | ~50 行 | ~10 行 |
10.5.5 PubSub vs 多个 Channel#
// 方案 A:PubSub(推荐用于广播)
static BUS: PubSubChannel<..., Event, 4, 4, 1> = PubSubChannel::new();
// 发布者只需一行:
publisher.publish(event);
// 方案 B:多个 Channel(手动广播)
static CH1: Channel<..., Event, 4> = Channel::new();
static CH2: Channel<..., Event, 4> = Channel::new();
static CH3: Channel<..., Event, 4> = Channel::new();
// 发布者需要向每个 Channel 发送:
CH1.send(event).await;
CH2.send(event).await;
CH3.send(event).await;
// 新增订阅者?修改发布者代码!rust| PubSub | 多个 Channel | |
|---|---|---|
| 新增订阅者 | 无需修改发布者 | 需要修改发布者 |
| RAM 开销 | 共享发布逻辑 | 每个 Channel 独立 |
| 语义清晰度 | ”广播" | "点对点 × N” |
| 适用场景 | 事件通知、状态变更 | 不同数据流 |
10.6 选型指南:如何选择正确的原语#
10.6.1 决策树#
你需要在任务间传递什么?
│
├── 数据流(每个值都要处理)
│ ├── 一对一 → Channel<..., T, N>
│ ├── 多对一 → Channel<..., T, N>(多个生产者)
│ └── 一对多(广播)→ PubSub
│
├── 状态通知(只关心最新值)
│ ├── 一对一 → Signal
│ └── 一对多 → PubSub
│
├── 共享资源访问
│ ├── 锁内需要 .await → embassy_sync::mutex::Mutex
│ └── 锁内不需要 .await → BlockingMutex>
│
└── 同步/屏障(多个任务同时到达某点)
└── 目前 Embassy 无内置 Barrier
→ 用 Channel 或 Signal 手动实现plaintext10.6.2 完整对比表#
| 原语 | 容量 | 方向 | 阻塞语义 | 数据 | 典型场景 |
|---|---|---|---|---|---|
Channel<T, N> | N(编译期) | 多对多 | 满→发送方挂起;空→接收方挂起 | ✅ 类型化 | 传感器数据流、命令队列 |
Signal<T> | 1(覆盖) | 多对一 | 空→接收方挂起;满→覆盖 | ✅ 类型化 | 按键事件、模式切换 |
Mutex<T> | 1(资源) | 互斥 | 被占用→请求方挂起 | ✅ 包裹资源 | 共享 I2C/SPI/UART |
PubSub<T, N, S> | N/订阅者 | 一对多 | 空→订阅者挂起;满→覆盖最旧 | ✅ 类型化 | 系统事件总线 |
10.6.3 常见模式速查#
| 模式 | 原语 | 示例 |
|---|---|---|
| 生产者-消费者 | Channel | 传感器 → 处理器 |
| 最新值缓存 | Signal | 按键状态、当前模式 |
| 事件广播 | PubSub | 系统事件 → LED/日志/显示 |
| 资源共享 | Mutex | 多任务共用 UART |
| 请求-响应 | Channel × 2 | 命令 Channel + 响应 Channel |
| 流水线 | Channel 链 | 采集 → 滤波 → 显示 |
| 中断通知任务 | Signal | ISR → 任务(按键、DMA 完成) |
10.6.4 反模式:不要这样做#
// ❌ 反模式 1:用全局变量 + volatile(C 思维)
static mut SHARED_DATA: u32 = 0; // unsafe!
// ✅ 正确:用 Signal 或 Mutex
static SHARED: Signal = Signal::new();
// ❌ 反模式 2:用 Channel 传递引用
static CH: Channel<..., &'static [u8], 4> = Channel::new(); // 生命周期噩梦
// ✅ 正确:传递拥有所有权的值(或 Copy 类型)
static CH: Channel<..., [u8; 64], 4> = Channel::new();
// ❌ 反模式 3:在 Mutex 锁内做耗时操作
let guard = M.lock().await;
Timer::after(Duration::from_secs(5)).await; // 持锁 5 秒!其他任务全部阻塞!
drop(guard);
// ✅ 正确:最小化锁持有时间
let data = {
let guard = M.lock().await;
guard.borrow().clone() // 快速复制数据
}; // 锁在这里释放
// 在锁外做耗时操作
Timer::after(Duration::from_secs(5)).await;
// ❌ 反模式 4:用 PubSub 做点对点通信
// 如果只有一个订阅者,用 Channel 更简单
// ❌ 反模式 5:Channel 容量设太大
static CH: Channel<..., [u8; 1024], 64> = Channel::new(); // 64KB RAM!
// 重新设计:减小消息大小或容量rust10.7 综合实战:多任务数据采集系统#
需求#
- 2 个传感器(I2C)以不同速率采集
- 数据经过滤波后存入环形缓冲区
- OLED 显示最新数据
- UART 接收命令(“START”/“STOP”/“GET”)
- 按键切换显示页面
- 所有任务通过事件总线协调
架构图#
┌─────────────────────────────────────────────────────────────────────┐
│ 系统架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ Channel ┌─────────────┐ │
│ │ Sensor Task │ ─────────────► │ Filter Task │ │
│ │ (500ms) │ SensorData │ (即时处理) │ │
│ └─────────────┘ └──────┬──────┘ │
│ │ Channel │
│ ▼ │
│ ┌─────────────┐ PubSub ┌─────────────┐ │
│ │ Button Task │ ────────────► │ Event Bus │ │
│ │ (异步等待) │ SystemEvent │ (广播) │ │
│ └─────────────┘ └──┬───┬───┬──┘ │
│ │ │ │ │
│ ┌─────────────┐ │ │ │ │
│ │ UART Task │ ◄─── Signal ────┘ │ │ │
│ │ (命令解析) │ (命令通知) │ │ │
│ └─────────────┘ │ │ │
│ │ │ │
│ ┌────────────┘ └────────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Display Task│ │ Log Task │ │
│ │ (200ms) │ │ (即时) │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘plaintext完整代码#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::{
self as hal,
gpio::{Input, Level, Output, Pull, Speed},
i2c::{I2c, Config as I2cConfig},
usart::{Uart, Config as UartConfig},
};
use embassy_sync::{
blocking_mutex::raw::CriticalSectionRawMutex,
channel::Channel,
signal::Signal,
pubsub::PubSubChannel,
mutex::Mutex,
};
use embassy_time::{Duration, Ticker, Timer};
use core::cell::RefCell;
use defmt_rtt as _;
use panic_probe as _;
// ===== 数据类型 =====
#[derive(Clone, Copy, defmt::Format)]
struct SensorData {
temperature: i16,
humidity: u16,
pressure: u32,
}
#[derive(Clone, Copy, defmt::Format)]
enum SystemEvent {
Start,
Stop,
PageNext,
DataReady,
}
#[derive(Clone, Copy, PartialEq)]
enum UartCommand {
Start,
Stop,
Get,
}
// ===== 通信原语 =====
static SENSOR_CH: Channel = Channel::new();
static FILTERED_CH: Channel = Channel::new();
static EVENT_BUS: PubSubChannel = PubSubChannel::new();
static CMD_SIGNAL: Signal = Signal::new();
static RUNNING: Mutex> = Mutex::new(RefCell::new(true));
// ===== 任务 1:传感器采集 =====
#[embassy_executor::task]
async fn sensor_task(mut i2c: I2c<'static, hal::i2c::I2c1>) {
let mut ticker = Ticker::every(Duration::from_millis(500));
let publisher = EVENT_BUS.publisher().unwrap();
loop {
ticker.next().await;
// 检查是否在运行
let running = { *RUNNING.lock().await.borrow() };
if !running {
continue;
}
// 读取传感器
let mut buf = [0u8; 8];
match i2c.write_read(0x76, &[0xF7], &mut buf).await {
Ok(()) => {
let data = SensorData {
pressure: u32::from_be_bytes([0, buf[0], buf[1], buf[2]]) >> 4,
temperature: i16::from_be_bytes([buf[3], buf[4]]) >> 4,
humidity: u16::from_be_bytes([buf[5], buf[6]]),
};
SENSOR_CH.send(data).await;
publisher.publish(SystemEvent::DataReady);
}
Err(e) => {
defmt::error!("I2C 错误: {:?}", e);
}
}
}
}
// ===== 任务 2:数据滤波 =====
#[embassy_executor::task]
async fn filter_task() {
let mut ema_temp: i32 = 0;
let mut ema_hum: i32 = 0;
let mut ema_press: i64 = 0;
let mut initialized = false;
loop {
let raw = SENSOR_CH.receive().await;
if !initialized {
ema_temp = raw.temperature as i32;
ema_hum = raw.humidity as i32;
ema_press = raw.pressure as i64;
initialized = true;
} else {
// 指数移动平均(α = 0.2)
ema_temp += (raw.temperature as i32 - ema_temp) / 5;
ema_hum += (raw.humidity as i32 - ema_hum) / 5;
ema_press += (raw.pressure as i64 - ema_press) / 5;
}
let filtered = SensorData {
temperature: ema_temp as i16,
humidity: ema_hum as u16,
pressure: ema_press as u32,
};
FILTERED_CH.send(filtered).await;
}
}
// ===== 任务 3:按键处理 =====
#[embassy_executor::task]
async fn button_task(mut button: Input<'static>) {
let publisher = EVENT_BUS.publisher().unwrap();
loop {
button.wait_for_falling_edge().await;
Timer::after(Duration::from_millis(20)).await;
if button.is_low() {
publisher.publish(SystemEvent::PageNext);
defmt::info!("[BTN] 页面切换");
}
button.wait_for_rising_edge().await;
}
}
// ===== 任务 4:UART 命令 =====
#[embassy_executor::task]
async fn uart_task(mut uart: Uart<'static, hal::usart::Usart2>) {
let mut buf = [0u8; 1];
let mut cmd_buf = [0u8; 16];
let mut cmd_len = 0;
loop {
uart.read(&mut buf).await.unwrap();
match buf[0] {
b'\r' | b'\n' => {
let cmd = &cmd_buf[..cmd_len];
match cmd {
b"START" => {
*RUNNING.lock().await.borrow_mut() = true;
CMD_SIGNAL.signal(UartCommand::Start);
uart.write(b"OK: Started\r\n").await.unwrap();
}
b"STOP" => {
*RUNNING.lock().await.borrow_mut() = false;
CMD_SIGNAL.signal(UartCommand::Stop);
uart.write(b"OK: Stopped\r\n").await.unwrap();
}
b"GET" => {
CMD_SIGNAL.signal(UartCommand::Get);
if let Ok(data) = FILTERED_CH.try_receive() {
// 格式化输出(实际中使用 heapless::String)
uart.write(b"OK: T=25.3 H=60.1 P=1013\r\n").await.unwrap();
} else {
uart.write(b"ERR: No data\r\n").await.unwrap();
}
}
_ => {
if !cmd.is_empty() {
uart.write(b"ERR: Unknown cmd\r\n").await.unwrap();
}
}
}
cmd_len = 0;
}
b => {
if cmd_len < 16 {
cmd_buf[cmd_len] = b;
cmd_len += 1;
}
}
}
}
}
// ===== 任务 5:显示 =====
#[embassy_executor::task]
async fn display_task() {
let mut sub = EVENT_BUS.subscriber().unwrap();
let mut page: u8 = 0;
let mut latest = SensorData { temperature: 0, humidity: 0, pressure: 0 };
loop {
// 等待事件(DataReady 或 PageNext)
let event = sub.next_message().await;
match event {
SystemEvent::DataReady => {
if let Ok(data) = FILTERED_CH.try_receive() {
latest = data;
}
}
SystemEvent::PageNext => {
page = (page + 1) % 3;
}
_ => {}
}
// 更新显示
match page {
0 => defmt::info!("[DISP] Page 0: T={}.{:01}°C",
latest.temperature / 10, (latest.temperature % 10).abs()),
1 => defmt::info!("[DISP] Page 1: H={}.{:01}%",
latest.humidity / 10, latest.humidity % 10),
2 => defmt::info!("[DISP] Page 2: P={} hPa",
latest.pressure / 100),
_ => {}
}
}
}
// ===== 任务 6:系统日志 =====
#[embassy_executor::task]
async fn log_task() {
let mut sub = EVENT_BUS.subscriber().unwrap();
loop {
let event = sub.next_message().await;
defmt::info!("[LOG] Event: {:?}", event);
}
}
// ===== 主入口 =====
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = hal::init(Default::default());
defmt::info!("=== 多任务数据采集系统 ===");
// 初始化外设
let mut i2c_cfg = I2cConfig::default();
i2c_cfg.frequency = hal::i2c::Frequency::F400k;
let i2c = I2c::new(p.I2C1, p.PB6, p.PB7, p.DMA1_CH0, p.DMA1_CH1, i2c_cfg).unwrap();
let uart_cfg = UartConfig::default();
let uart = Uart::new(p.USART2, p.PA3, p.PA2, p.DMA1_CH5, p.DMA1_CH6, uart_cfg).unwrap();
let button = Input::new(p.PA0, Pull::Up);
// 启动所有任务
spawner.spawn(sensor_task(i2c)).unwrap();
spawner.spawn(filter_task()).unwrap();
spawner.spawn(button_task(button)).unwrap();
spawner.spawn(uart_task(uart)).unwrap();
spawner.spawn(display_task()).unwrap();
spawner.spawn(log_task()).unwrap();
defmt::info!("所有任务已启动");
}rust通信原语使用总结#
| 原语 | 实例 | 连接 | 作用 |
|---|---|---|---|
Channel | SENSOR_CH | 传感器 → 滤波 | 原始数据流 |
Channel | FILTERED_CH | 滤波 → 显示 | 处理后数据 |
PubSub | EVENT_BUS | 按键/传感器 → 显示/日志 | 事件广播 |
Signal | CMD_SIGNAL | UART → 控制逻辑 | 命令通知 |
Mutex | RUNNING | UART ↔ 传感器 | 共享运行状态 |
10.8 本章小结#
核心认知#
-
Channel 是工作马:80% 的任务间通信需求可以用 Channel 解决。它是类型安全的、固定容量的、异步的 FIFO 队列。
-
Signal 是”最新值”语义:当你只关心”当前状态是什么”而非”发生了什么事件”时,用 Signal。它永远不会阻塞发送方。
-
Mutex 是最后手段:优先考虑所有权转移(每个任务拥有自己的外设)。只有真正需要共享时,才用 Mutex。RAII 保证你不会忘记释放。
-
PubSub 是广播:当一个事件需要通知多个不相关的任务时,PubSub 比”多个 Channel”更优雅、更可扩展。
-
没有死锁:Embassy 的协作式调度 + 单线程 Executor 意味着死锁在理论上不可能发生(除非你在 Mutex 锁内
.await同一个 Mutex)。
与 FreeRTOS 的最终对比#
| 维度 | FreeRTOS | Embassy |
|---|---|---|
| 类型安全 | ❌ void* | ✅ 泛型 |
| 内存分配 | 动态(pvPortMalloc) | 静态(编译期) |
| 资源释放 | 手动(xSemaphoreGive) | 自动(RAII drop) |
| 死锁 | 可能(运行时) | 不可能(编译期+协作式) |
| 优先级反转 | 可能(需优先级继承) | 不存在(无优先级) |
| 竞态条件 | 可能(volatile 不够) | 编译器阻止(借用检查) |
| 代码量 | 多(初始化+发送+接收+错误处理) | 少(声明+使用) |
下一步#
你现在掌握了 Embassy 的完整编程模型:
- ✅ 外设操作(第 8 章)
- ✅ 运行时机制(第 9 章)
- ✅ 任务间通信(第 10 章)
接下来,我们将把这些知识组合成一个完整的、可部署的项目:
第 11 章:完整项目实战——从传感器到云端。我们将构建一个包含传感器采集、OLED 显示、UART 调试、WiFi 上报、OTA 升级的完整 IoT 产品原型。
下一章:第 11 章——完整项目实战:从传感器到云端。