第 8 章:第一个 Embassy 工程——GPIO、UART、SPI、I2C 与 Timer
Part2 深入embassy框架
第 8 章:第一个 Embassy 工程——GPIO、UART、SPI、I2C 与 Timer#
本章要回答的问题:如何用 Embassy 点亮 LED、读取传感器、驱动显示器?每个外设的异步 API 长什么样?与 C HAL 有何区别?
本章定位:动手实操。从零搭建一个 Embassy 工程,逐个外设讲解 API,每个外设都有 C 对比。目标平台:STM32F407VGT6(Nucleo-F401RE 或正点原子探索者开发板均可)。
8.1 工程搭建:从 cargo new 到 LED 闪烁#
步骤一:创建工程#
cargo new embassy_blinky
cd embassy_blinkybash步骤二:配置 .cargo/config.toml#
# .cargo/config.toml
[target.thumbv7em-none-eabihf]
runner = "probe-rs run --chip STM32F407VGTx"
[build]
target = "thumbv7em-none-eabihf"
[env]
DEFMT_LOG = "info"toml与第 3 章裸机工程的区别:
runner从probe-rs run变为带--chip参数的完整命令。Embassy 工程通常使用probe-rs run而非probe-rs download,因为run会同时启动 RTT 日志输出。
步骤三:Cargo.toml#
[package]
name = "embassy_blinky"
version = "0.1.0"
edition = "2021"
[dependencies]
# Embassy 核心
embassy-executor = { version = "0.7", features = [
"arch-cortex-m",
"executor-thread",
"defmt",
] }
embassy-time = { version = "0.4", features = ["defmt"] }
# 芯片 HAL
embassy-stm32 = { version = "0.2", features = [
"stm32f407vg",
"memory-x",
"time-driver-any",
"defmt",
] }
# Cortex-M 支持
cortex-m = { version = "0.7", features = ["critical-section-single-core"] }
cortex-m-rt = "0.7"
# 日志与 Panic
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }toml步骤四:memory.x#
/* memory.x —— STM32F407VGT6 的内存布局 */
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 1024K
RAM : ORIGIN = 0x20000000, LENGTH = 128K
}plaintext步骤五:build.rs#
// build.rs
fn main() {
println!("cargo:rustc-link-arg-bins=--nmagic");
println!("cargo:rustc-link-arg-bins=-Tlink.x");
println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
}rust步骤六:第一个 Embassy 程序——LED 闪烁#
// src/main.rs
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::{Duration, Timer};
use defmt_rtt as _;
use panic_probe as _;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
// 初始化芯片 HAL(获取所有外设的所有权)
let p = embassy_stm32::init(Default::default());
defmt::info!("Embassy Blinky 启动!");
// 配置 PA5 为推挽输出(Nucleo 板载 LED)
let mut led = Output::new(p.PA5, Level::Low, Speed::Low);
loop {
led.set_high();
defmt::info!("LED ON");
Timer::after(Duration::from_millis(500)).await; // 异步等待 500ms
led.set_low();
defmt::info!("LED OFF");
Timer::after(Duration::from_millis(500)).await; // 异步等待 500ms
}
}rust步骤七:编译、烧录、运行#
# 编译
cargo build --release
# 烧录并运行(同时启动 RTT 日志)
cargo run --release
# 预期输出(RTT 日志):
# INFO Embassy Blinky 启动!
# INFO LED ON
# INFO LED OFF
# INFO LED ON
# ...bash与 C 的逐行对比#
| 行号 | C(STM32 HAL) | Rust(Embassy) | 说明 |
|---|---|---|---|
| 入口 | int main(void) | #[embassy_executor::main] async fn main(_spawner: Spawner) | Embassy 入口是 async fn |
| 初始化 | HAL_Init(); SystemClock_Config(); | let p = embassy_stm32::init(Default::default()); | 一行搞定时钟+外设 |
| GPIO 配置 | GPIO_InitTypeDef + HAL_GPIO_Init() | Output::new(p.PA5, Level::Low, Speed::Low) | 类型状态,无需结构体 |
| 点亮 | HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) | led.set_high() | 方法调用 vs 函数+枚举 |
| 延时 | HAL_Delay(500) | Timer::after(Duration::from_millis(500)).await | 异步等待 vs 阻塞等待 |
| 日志 | printf() + 重定向 | defmt::info!() | 零开销日志(编译期格式化) |
最关键的差异:HAL_Delay(500) 是阻塞的——CPU 在这 500ms 内什么都不做。Timer::after(...).await 是异步的——CPU 在这 500ms 内可以执行其他任务。
8.2 GPIO 输出与输入#
8.2.1 GPIO 输出:控制 LED#
Embassy API#
use embassy_stm32::gpio::{Level, Output, Speed};
// 创建输出引脚
let mut led = Output::new(p.PA5, Level::Low, Speed::Low);
// ↑引脚 ↑初始电平 ↑速度
// 操作
led.set_high(); // 输出高电平
led.set_low(); // 输出低电平
led.toggle(); // 翻转
led.set_state(Level::High); // 设置指定电平
// 查询当前状态
let is_high: bool = led.is_set_high();rustC 对比(STM32 HAL)#
// C:GPIO 输出配置(~15 行)
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 操作
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); // 高电平
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 低电平
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 翻转c关键差异#
| 方面 | C(HAL) | Embassy |
|---|---|---|
| 时钟使能 | 手动 __HAL_RCC_GPIOA_CLK_ENABLE() | 自动(init() 内部处理) |
| 配置方式 | 填充结构体 + 调用 HAL_GPIO_Init() | 构造函数 Output::new() |
| 类型安全 | 无(任何引脚都可以传给任何函数) | 有(p.PA5 只能用于 GPIOA 的操作) |
| 模式切换 | 重新调用 HAL_GPIO_Init() | 消耗旧对象,创建新对象(类型状态) |
| 引脚冲突 | 无保护(两个模块可以配置同一引脚) | 编译错误(所有权转移后不可再用) |
8.2.2 GPIO 输入:读取按键#
Embassy API(轮询式)#
use embassy_stm32::gpio::{Input, Pull};
// 创建输入引脚(上拉)
let button = Input::new(p.PA0, Pull::Up);
// 读取电平
let is_pressed: bool = button.is_low(); // 低电平有效(按下接地)rustEmbassy API(异步等待——重点!)#
use embassy_stm32::gpio::{Input, Pull};
let mut button = Input::new(p.PA0, Pull::Up);
// 异步等待按键按下(低电平)
button.wait_for_falling_edge().await; // 等待下降沿
defmt::info!("按键按下!");
// 异步等待按键释放(高电平)
button.wait_for_rising_edge().await; // 等待上升沿
defmt::info!("按键释放!");
// 其他等待方法:
// button.wait_for_low().await; // 等待变为低电平
// button.wait_for_high().await; // 等待变为高电平
// button.wait_for_any_edge().await; // 等待任意边沿rustC 对比(中断式)#
// C:按键中断(~40 行)
volatile bool button_pressed = false;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_0) {
button_pressed = true;
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿中断
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
while (1) {
if (button_pressed) {
button_pressed = false;
// 处理按键事件
printf("按键按下!\n");
}
}
}c关键差异#
| 方面 | C(中断 + 标志) | Embassy(async wait) |
|---|---|---|
| 中断配置 | 手动配置 NVIC、优先级、EXTI | 自动(wait_for_* 内部配置) |
| 事件通知 | volatile 标志 + 主循环轮询 | .await 自动挂起/恢复 |
| 竞态风险 | 有(标志可能在检查和清除之间被修改) | 无(单线程协作式) |
| 代码量 | ~40 行 | ~5 行 |
| 多按键 | 需要多个标志 + 多个中断 | 多个 .await 或 select |
完整示例:按键控制 LED#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
use defmt_rtt as _;
use panic_probe as _;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
let mut led = Output::new(p.PA5, Level::Low, Speed::Low);
let mut button = Input::new(p.PA0, Pull::Up);
defmt::info!("按键控制 LED 启动");
loop {
// 等待按键按下(异步,不消耗 CPU)
button.wait_for_falling_edge().await;
// 简单消抖:等待 20ms
embassy_time::Timer::after(embassy_time::Duration::from_millis(20)).await;
// 确认按键仍然按下
if button.is_low() {
led.toggle();
defmt::info!("LED 状态:{}", if led.is_set_high() { "ON" } else { "OFF" });
}
// 等待按键释放
button.wait_for_rising_edge().await;
}
}rust注意:在等待按键期间(wait_for_falling_edge().await),CPU 进入 WFI(Wait For Interrupt)低功耗模式。如果有其他任务,它们会正常执行。这就是异步的价值——等待是免费的。
8.2.3 多任务 GPIO:两个 LED 独立闪烁#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_time::{Duration, Timer};
use defmt_rtt as _;
use panic_probe as _;
// 任务 1:PA5 LED 以 500ms 周期闪烁
#[embassy_executor::task]
async fn blink_slow(mut led: Output<'static>) {
loop {
led.toggle();
Timer::after(Duration::from_millis(500)).await;
}
}
// 任务 2:PA6 LED 以 200ms 周期闪烁
#[embassy_executor::task]
async fn blink_fast(mut led: Output<'static>) {
loop {
led.toggle();
Timer::after(Duration::from_millis(200)).await;
}
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
let led1 = Output::new(p.PA5, Level::Low, Speed::Low);
let led2 = Output::new(p.PA6, Level::Low, Speed::Low);
// 启动两个任务——它们"同时"运行
spawner.spawn(blink_slow(led1)).unwrap();
spawner.spawn(blink_fast(led2)).unwrap();
// main 任务也可以做其他事
loop {
Timer::after(Duration::from_secs(5)).await;
defmt::info!("系统运行中...");
}
}rustC 的等价实现需要:
- 两个定时器中断(或一个定时器 + 分频)
- 两个
volatile标志 - 主循环中的两个
if判断 - 或者使用 RTOS 创建两个任务
Embassy 的实现:两个 async fn,两个 spawn,完事。
8.3 UART:异步串口通信#
8.3.1 Embassy UART API#
Embassy 提供两种 UART 模式:
| 模式 | 类型 | 适用场景 |
|---|---|---|
| DMA 模式 | Uart | 大数据量传输、需要精确控制 |
| 缓冲模式 | BufferedUart | 小数据量、类似 std::io::Read/Write |
DMA 模式(推荐)#
use embassy_stm32::usart::{Uart, Config};
// 创建 UART(需要指定 DMA 通道)
let config = Config::default(); // 默认 115200 8N1
let mut uart = Uart::new(
p.USART2, // 外设
p.PA3, // RX 引脚
p.PA2, // TX 引脚
p.DMA1_CH5, // RX DMA 通道
p.DMA1_CH6, // TX DMA 通道
config,
).unwrap();
// 异步发送
uart.write(b"Hello, Embassy!\r\n").await.unwrap();
// 异步接收(读取 1 字节)
let mut buf = [0u8; 1];
uart.read(&mut buf).await.unwrap();
defmt::info!("收到字节:{:#02x}", buf[0]);
// 异步接收(读取多字节)
let mut buf = [0u8; 64];
uart.read(&mut buf).await.unwrap(); // 读满 64 字节后返回rust缓冲模式#
use embassy_stm32::usart::{BufferedUart, Config};
use embedded_io_async::{Read, Write};
// 创建缓冲 UART(需要静态缓冲区)
static mut TX_BUF: [u8; 256] = [0; 256];
static mut RX_BUF: [u8; 256] = [0; 256];
let config = Config::default();
let mut uart = BufferedUart::new(
p.USART2,
p.PA3,
p.PA2,
unsafe { &mut TX_BUF },
unsafe { &mut RX_BUF },
config,
).unwrap();
// 使用 embedded-io-async trait
uart.write_all(b"Hello\r\n").await.unwrap();
uart.flush().await.unwrap();
let mut byte = [0u8; 1];
uart.read(&mut byte).await.unwrap();rust8.3.2 C 对比(STM32 HAL + 中断)#
// C:UART 接收(中断方式,~50 行)
volatile uint8_t rx_byte;
volatile bool rx_ready = false;
void USART2_IRQHandler(void) {
HAL_UART_IRQHandler(&huart2);
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &huart2) {
rx_ready = true;
HAL_UART_Receive_IT(&huart2, (uint8_t *)&rx_byte, 1); // 重新注册接收
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
// UART 初始化(~20 行配置代码省略)
huart2.Instance = USART2;
huart2.Init.BaudRate = 115200;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
HAL_UART_Init(&huart2);
// 启动第一次接收
HAL_UART_Receive_IT(&huart2, (uint8_t *)&rx_byte, 1);
while (1) {
if (rx_ready) {
rx_ready = false;
// 处理接收到的字节
printf("收到:%c\n", rx_byte);
}
}
}c8.3.3 关键差异#
| 方面 | C(HAL + 中断) | Embassy(async DMA) |
|---|---|---|
| 接收方式 | 中断回调 + volatile 标志 | .await(DMA 传输完成后自动恢复) |
| 发送方式 | HAL_UART_Transmit()(阻塞)或 _IT(中断) | uart.write().await(DMA,异步) |
| CPU 占用 | 中断方式:每字节一次中断 | DMA 方式:传输期间 CPU 完全空闲 |
| 缓冲区管理 | 手动(环形缓冲区 + 头尾指针) | HAL 内部管理 |
| 错误处理 | 回调中检查错误标志 | Result 返回值 |
| 代码量 | ~50 行 | ~10 行 |
8.3.4 完整示例:UART 回显 + 命令解析#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::usart::{Uart, Config};
use defmt_rtt as _;
use panic_probe as _;
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
let config = Config::default();
let mut uart = Uart::new(
p.USART2, p.PA3, p.PA2,
p.DMA1_CH5, p.DMA1_CH6,
config,
).unwrap();
// 发送欢迎信息
uart.write(b"\r\n=== Embassy UART Demo ===\r\n").await.unwrap();
uart.write(b"输入 'LED ON' 或 'LED OFF' 控制 LED\r\n").await.unwrap();
let mut led = embassy_stm32::gpio::Output::new(
p.PA5,
embassy_stm32::gpio::Level::Low,
embassy_stm32::gpio::Speed::Low,
);
let mut cmd_buf = [0u8; 32];
let mut cmd_len = 0;
let mut byte = [0u8; 1];
loop {
// 异步等待一个字节(DMA,CPU 空闲)
uart.read(&mut byte).await.unwrap();
match byte[0] {
b'\r' | b'\n' => {
// 收到回车,解析命令
let cmd = &cmd_buf[..cmd_len];
if cmd == b"LED ON" {
led.set_high();
uart.write(b"OK: LED ON\r\n").await.unwrap();
} else if cmd == b"LED OFF" {
led.set_low();
uart.write(b"OK: LED OFF\r\n").await.unwrap();
} else if !cmd.is_empty() {
uart.write(b"ERR: Unknown command\r\n").await.unwrap();
}
cmd_len = 0;
}
b => {
if cmd_len < 32 {
cmd_buf[cmd_len] = b;
cmd_len += 1;
}
}
}
}
}rust注意:这个程序在等待 UART 数据时(uart.read().await),CPU 完全空闲。如果同时有其他任务(如 LED 闪烁),它们会正常运行。在 C 中实现同样的效果,你需要:中断 + DMA + 环形缓冲区 + 主循环解析——至少 100 行代码。
8.4 SPI:驱动 OLED 显示器#
8.4.1 Embassy SPI API#
use embassy_stm32::spi::{Spi, Config as SpiConfig, Mode};
// 创建 SPI(需要 DMA 通道)
let mut spi_config = SpiConfig::default();
spi_config.frequency = embassy_stm32::spi::Hertz(1_000_000); // 1 MHz
spi_config.mode = Mode::Mode0; // CPOL=0, CPHA=0
let mut spi = Spi::new(
p.SPI1, // 外设
p.PA5, // SCK
p.PA7, // MOSI
p.PA6, // MISO
p.DMA2_CH3, // TX DMA
p.DMA2_CH0, // RX DMA
spi_config,
).unwrap();
// 异步写入(DMA 传输,CPU 空闲)
spi.write(&[0x01, 0x02, 0x03]).await.unwrap();
// 异步读写(全双工)
let tx_data = [0x9F, 0x00, 0x00, 0x00]; // 读取 JEDEC ID
let mut rx_data = [0u8; 4];
spi.transfer(&mut rx_data, &tx_data).await.unwrap();
// rx_data = [0xEF, 0x40, 0x18, 0x00] (Winbond W25Q128)rust8.4.2 C 对比(STM32 HAL)#
// C:SPI 初始化 + 传输(~30 行)
SPI_HandleTypeDef hspi1;
void SPI1_Init(void) {
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// GPIO 配置(SCK, MOSI, MISO)
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// SPI 配置
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
HAL_SPI_Init(&hspi1);
}
// 阻塞式传输
void SPI1_Transmit(uint8_t *data, uint16_t len) {
HAL_SPI_Transmit(&hspi1, data, len, 100); // 100ms 超时
}c8.4.3 完整示例:SSD1306 OLED 显示#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::gpio::{Level, Output, Speed};
use embassy_stm32::spi::{Spi, Config as SpiConfig, Mode};
use embassy_time::{Duration, Timer};
use defmt_rtt as _;
use panic_probe as _;
// SSD1306 命令常量
const CMD_DISPLAY_OFF: u8 = 0xAE;
const CMD_DISPLAY_ON: u8 = 0xAF;
const CMD_SET_MUX_RATIO: u8 = 0xA8;
const CMD_SET_DISPLAY_OFFSET: u8 = 0xD3;
const CMD_SET_START_LINE: u8 = 0x40;
const CMD_SET_SEGMENT_REMAP: u8 = 0xA1;
const CMD_SET_COM_SCAN_DIR: u8 = 0xC8;
const CMD_SET_COM_PINS: u8 = 0xDA;
const CMD_SET_CONTRAST: u8 = 0x81;
const CMD_SET_PRECHARGE: u8 = 0xD9;
const CMD_SET_VCOM_DESELECT: u8 = 0xDB;
const CMD_SET_MEMORY_MODE: u8 = 0x20;
const CMD_SET_COLUMN_ADDR: u8 = 0x21;
const CMD_SET_PAGE_ADDR: u8 = 0x22;
struct Ssd1306 {
spi: SPI,
dc: DC, // Data/Command 引脚
}
impl
Ssd1306
{
async fn send_cmd(&mut self, cmd: u8) {
self.dc.set_low().unwrap(); // DC=0 → 命令
self.spi.write(&[cmd]).await.unwrap();
}
async fn send_data(&mut self, data: &[u8]) {
self.dc.set_high().unwrap(); // DC=1 → 数据
self.spi.write(data).await.unwrap();
}
async fn init(&mut self) {
self.send_cmd(CMD_DISPLAY_OFF).await;
self.send_cmd(CMD_SET_MUX_RATIO).await;
self.send_cmd(63).await; // 1/64 占空比
self.send_cmd(CMD_SET_DISPLAY_OFFSET).await;
self.send_cmd(0x00).await;
self.send_cmd(CMD_SET_START_LINE).await;
self.send_cmd(CMD_SET_SEGMENT_REMAP).await;
self.send_cmd(CMD_SET_COM_SCAN_DIR).await;
self.send_cmd(CMD_SET_COM_PINS).await;
self.send_cmd(0x12).await;
self.send_cmd(CMD_SET_CONTRAST).await;
self.send_cmd(0xCF).await;
self.send_cmd(CMD_SET_PRECHARGE).await;
self.send_cmd(0xF1).await;
self.send_cmd(CMD_SET_VCOM_DESELECT).await;
self.send_cmd(0x40).await;
self.send_cmd(CMD_SET_MEMORY_MODE).await;
self.send_cmd(0x00).await; // 水平寻址模式
self.send_cmd(CMD_DISPLAY_ON).await;
}
async fn clear(&mut self) {
self.set_window().await;
self.send_data(&[0u8; 1024]).await; // 128×64/8 = 1024 字节
}
async fn set_window(&mut self) {
self.send_cmd(CMD_SET_COLUMN_ADDR).await;
self.send_cmd(0).await;
self.send_cmd(127).await;
self.send_cmd(CMD_SET_PAGE_ADDR).await;
self.send_cmd(0).await;
self.send_cmd(7).await;
}
async fn draw_pixel(&mut self, x: u8, y: u8, on: bool) {
// 简化:实际实现需要帧缓冲区
self.set_window().await;
// ... 帧缓冲区操作 ...
}
}
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
// SPI 配置
let mut spi_config = SpiConfig::default();
spi_config.frequency = embassy_stm32::spi::Hertz(8_000_000); // 8 MHz
spi_config.mode = Mode::Mode0;
let spi = Spi::new(
p.SPI1, p.PA5, p.PA7, p.PA6,
p.DMA2_CH3, p.DMA2_CH0,
spi_config,
).unwrap();
// DC 引脚
let dc = Output::new(p.PB0, Level::Low, Speed::High);
// 创建 SSD1306 驱动
// 注意:实际使用中,你会用 embassy-stm32 的 ExclusiveDevice 包装 SPI + CS
let mut oled = Ssd1306 { spi, dc };
// 初始化
oled.init().await;
oled.clear().await;
defmt::info!("OLED 初始化完成");
// 显示动画
let mut x = 0u8;
loop {
oled.clear().await;
// 绘制移动的方块(简化)
oled.draw_pixel(x, 32, true).await;
x = x.wrapping_add(1);
Timer::after(Duration::from_millis(50)).await;
}
}rust8.4.4 SPI 的 C vs Embassy 对比总结#
| 方面 | C(HAL) | Embassy |
|---|---|---|
| 初始化 | ~30 行(GPIO + SPI 配置) | ~8 行(Spi::new()) |
| 传输 | HAL_SPI_Transmit()(阻塞) | spi.write().await(DMA 异步) |
| 片选管理 | 手动 HAL_GPIO_WritePin(CS, LOW/HIGH) | ExclusiveDevice 自动管理 |
| 多设备 | 手动切换 CS | 每个设备一个 SpiDevice 实例 |
| CPU 占用 | 阻塞式:CPU 等待传输完成 | DMA 式:传输期间 CPU 执行其他任务 |
8.5 I2C:读取 BME280 传感器#
8.5.1 Embassy I2C API#
use embassy_stm32::i2c::{I2c, Config as I2cConfig};
// 创建 I2C(需要 DMA 通道)
let mut i2c_config = I2cConfig::default();
i2c_config.frequency = embassy_stm32::i2c::Frequency::F400k; // 400 kHz
let mut i2c = I2c::new(
p.I2C1, // 外设
p.PB6, // SCL
p.PB7, // SDA
p.DMA1_CH0, // TX DMA(Event)
p.DMA1_CH1, // RX DMA(Event)
i2c_config,
).unwrap();
// 异步写入
i2c.write(0x76, &[0xF4, 0x27]).await.unwrap(); // 配置 BME280
// 异步读取
let mut buf = [0u8; 3];
i2c.read(0x76, &mut buf).await.unwrap();
// 异步写后读(最常用:发送寄存器地址,读取数据)
let mut data = [0u8; 8];
i2c.write_read(0x76, &[0xF7], &mut data).await.unwrap();
// data[0..3] = 气压数据
// data[3..6] = 温度数据
// data[6..8] = 湿度数据rust8.5.2 C 对比(STM32 HAL)#
// C:I2C 读取 BME280(~25 行)
I2C_HandleTypeDef hi2c1;
void I2C1_Init(void) {
__HAL_RCC_I2C1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 开漏!
GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
HAL_I2C_Init(&hi2c1);
}
// 阻塞式写后读
HAL_StatusTypeDef BME280_Read(uint8_t reg, uint8_t *data, uint16_t len) {
return HAL_I2C_Mem_Read(&hi2c1, 0x76 << 1, reg,
I2C_MEMADD_SIZE_8BIT, data, len, 100);
}c8.5.3 完整示例:BME280 温湿度读取#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::i2c::{I2c, Config as I2cConfig};
use embassy_time::{Duration, Ticker};
use defmt_rtt as _;
use panic_probe as _;
const BME280_ADDR: u8 = 0x76;
// BME280 寄存器地址
const REG_CHIP_ID: u8 = 0xD0;
const REG_CTRL_HUM: u8 = 0xF2;
const REG_CTRL_MEAS: u8 = 0xF4;
const REG_DATA: u8 = 0xF7;
const REG_CALIB_00: u8 = 0x88;
// 校准数据(从 BME280 的 ROM 读取)
struct Calibration {
dig_t1: u16,
dig_t2: i16,
dig_t3: i16,
// ... 气压和湿度校准参数省略
}
impl Calibration {
async fn read(i2c: &mut I2c<'static, embassy_stm32::i2c::I2c1>) -> Self {
let mut buf = [0u8; 6];
i2c.write_read(BME280_ADDR, &[REG_CALIB_00], &mut buf).await.unwrap();
Self {
dig_t1: u16::from_le_bytes([buf[0], buf[1]]),
dig_t2: i16::from_le_bytes([buf[2], buf[3]]),
dig_t3: i16::from_le_bytes([buf[4], buf[5]]),
}
}
fn compensate_temperature(&self, raw: i32) -> i32 {
// BME280 数据手册中的补偿公式
let var1 = ((raw >> 3) - ((self.dig_t1 as i32) << 1)) * (self.dig_t2 as i32) >> 11;
let var2 = (((raw >> 4) - (self.dig_t1 as i32))
* ((raw >> 4) - (self.dig_t1 as i32)) >> 12)
* (self.dig_t3 as i32) >> 14;
let t_fine = var1 + var2;
(t_fine * 5 + 128) >> 8
}
}
#[embassy_executor::main]
async fn main(_spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
// 初始化 I2C
let mut i2c_config = I2cConfig::default();
i2c_config.frequency = embassy_stm32::i2c::Frequency::F400k;
let mut i2c = I2c::new(
p.I2C1, p.PB6, p.PB7,
p.DMA1_CH0, p.DMA1_CH1,
i2c_config,
).unwrap();
// 验证芯片 ID
let mut chip_id = [0u8; 1];
i2c.write_read(BME280_ADDR, &[REG_CHIP_ID], &mut chip_id).await.unwrap();
defmt::info!("BME280 Chip ID: {:#04x}", chip_id[0]);
assert_eq!(chip_id[0], 0x60, "BME280 未找到!");
// 读取校准数据
let calib = Calibration::read(&mut i2c).await;
// 配置 BME280:正常模式,16x 过采样
i2c.write(BME280_ADDR, &[REG_CTRL_HUM, 0x01]).await.unwrap(); // 湿度 1x
i2c.write(BME280_ADDR, &[REG_CTRL_MEAS, 0x27]).await.unwrap(); // 温度 1x + 气压 1x + 正常模式
// 周期性读取
let mut ticker = Ticker::every(Duration::from_secs(2));
loop {
ticker.next().await; // 每 2 秒读取一次
// 读取 8 字节原始数据(气压 + 温度 + 湿度)
let mut raw = [0u8; 8];
match i2c.write_read(BME280_ADDR, &[REG_DATA], &mut raw).await {
Ok(()) => {
// 解析温度(20 位,MSB 在前)
let raw_temp = ((raw[3] as i32) << 12)
| ((raw[4] as i32) << 4)
| ((raw[5] as i32) >> 4);
let temp = calib.compensate_temperature(raw_temp);
defmt::info!(
"温度: {}.{:01} °C",
temp / 100,
(temp % 100) / 10
);
}
Err(e) => {
defmt::error!("I2C 读取失败: {:?}", e);
}
}
}
}rust8.5.4 I2C 的 C vs Embassy 对比总结#
| 方面 | C(HAL) | Embassy |
|---|---|---|
| 初始化 | ~25 行(GPIO 开漏 + I2C 配置) | ~8 行(I2c::new()) |
| 写后读 | HAL_I2C_Mem_Read()(阻塞,100ms 超时) | i2c.write_read().await(DMA 异步) |
| 错误处理 | 返回 HAL_StatusTypeDef,需手动检查 | Result<(), Error>,可用 ? 传播 |
| 总线恢复 | 手动发送 9 个时钟脉冲 | HAL 内部处理(部分) |
| 多设备 | 同一 hi2c1 句柄,手动切换地址 | 同一 i2c 实例,传入不同地址 |
| CPU 占用 | 阻塞式:CPU 等待 ACK | DMA 式:传输期间 CPU 空闲 |
8.6 Timer:异步定时#
8.6.1 Embassy 的三种定时方式#
| 方式 | API | 适用场景 |
|---|---|---|
| 一次性延时 | Timer::after(Duration).await | ”等待 N 毫秒后继续” |
| 周期性触发 | Ticker::every(Duration) + .next().await | ”每 N 毫秒执行一次” |
| 超时控制 | select() + Timer | ”等待事件,最多等 N 毫秒” |
8.6.2 一次性延时#
use embassy_time::{Timer, Duration};
// 等待 1 秒
Timer::after(Duration::from_secs(1)).await;
// 等待 500 微秒
Timer::after(Duration::from_micros(500)).await;
// 等待 100 毫秒
Timer::after(Duration::from_millis(100)).await;rust与 C 的对比:
// C:阻塞延时(CPU 空转)
HAL_Delay(1000); // 1 秒内 CPU 什么都不做
// C:非阻塞延时(需要手动管理)
static uint32_t last_time = 0;
if (HAL_GetTick() - last_time >= 1000) {
last_time = HAL_GetTick();
// 执行操作
}c// Embassy:异步延时(CPU 可以执行其他任务)
Timer::after(Duration::from_secs(1)).await;
// 在这 1 秒内,其他任务正常运行rust8.6.3 周期性触发(Ticker)#
use embassy_time::{Ticker, Duration};
let mut ticker = Ticker::every(Duration::from_millis(100));
loop {
ticker.next().await; // 等待下一个 100ms 周期
// 执行周期性操作
defmt::info!("Tick!");
}rustTicker vs Timer 的区别:
Timer::after() | Ticker::every() | |
|---|---|---|
| 行为 | 从调用时刻开始计时 | 从创建时刻开始,固定周期 |
| 漂移 | 有(执行时间会累积) | 无(自动补偿执行时间) |
| 适用 | 一次性延时 | 精确周期任务 |
// ❌ Timer 的漂移问题
loop {
do_something(); // 假设耗时 5ms
Timer::after(Duration::from_millis(100)).await;
// 实际周期 = 100ms + 5ms = 105ms(漂移!)
}
// ✅ Ticker 无漂移
let mut ticker = Ticker::every(Duration::from_millis(100));
loop {
ticker.next().await;
do_something(); // 即使耗时 5ms,下一个 tick 仍在 100ms 处
// 实际周期 = 精确 100ms
}rust8.6.4 超时控制(select)#
use embassy_time::{Timer, Duration};
use embassy_futures::select::{select, Either};
// 等待 UART 数据,最多等 5 秒
let result = select(
uart.read(&mut buf), // 分支 A:等待数据
Timer::after(Duration::from_secs(5)), // 分支 B:5 秒超时
).await;
match result {
Either::First(Ok(())) => {
defmt::info!("收到数据!");
}
Either::Second(()) => {
defmt::warn!("超时!未收到数据");
}
Either::First(Err(e)) => {
defmt::error!("UART 错误: {:?}", e);
}
}rustC 的等价实现:
// C:超时等待(~15 行)
uint32_t start = HAL_GetTick();
bool received = false;
while (HAL_GetTick() - start < 5000) {
if (uart_data_ready()) {
received = true;
break;
}
// 问题:这个 while 循环在空转!
// 如果有其他任务,它们无法执行
}
if (!received) {
printf("超时!\n");
}cEmbassy 的优势:select 在等待期间不消耗 CPU——两个 Future 都挂起,CPU 可以执行其他任务。C 的 while 循环是纯空转。
8.6.5 C 的 HAL_Delay vs Embassy 的 Timer#
| 方面 | C HAL_Delay() | Embassy Timer::after().await |
|---|---|---|
| CPU 行为 | 空转(while 循环检查 SysTick) | 挂起(CPU 执行其他任务或 WFI) |
| 多任务影响 | 阻塞所有其他代码 | 只阻塞当前任务 |
| 精度 | 1ms(SysTick 周期) | 取决于时间驱动(通常 1ms 或更高) |
| 功耗 | 高(CPU 全速运行) | 低(可进入 WFI) |
| 可组合性 | 无 | 可与 select、join 组合 |
8.7 综合示例:多外设协作#
需求#
- 每 2 秒读取 BME280 温度(I2C)
- 每 500ms 更新 OLED 显示(SPI)
- 按键切换显示模式(GPIO 输入)
- UART 输出日志
完整代码#
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_stm32::{
gpio::{Input, Level, Output, Pull, Speed},
i2c::{I2c, Config as I2cConfig},
spi::{Spi, Config as SpiConfig, Mode},
usart::{Uart, Config as UartConfig},
};
use embassy_sync::channel::Channel;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Ticker, Timer};
use defmt_rtt as _;
use panic_probe as _;
// 任务间通信通道
static TEMP_CH: Channel = Channel::new();
static MODE_CH: Channel = Channel::new();
// ===== 任务 1:传感器读取 =====
#[embassy_executor::task]
async fn sensor_task(mut i2c: I2c<'static, embassy_stm32::i2c::I2c1>) {
// 初始化 BME280(省略,见 8.5 节)
// ...
let mut ticker = Ticker::every(Duration::from_secs(2));
loop {
ticker.next().await;
let mut raw = [0u8; 8];
match i2c.write_read(0x76, &[0xF7], &mut raw).await {
Ok(()) => {
let raw_temp = ((raw[3] as i32) << 12)
| ((raw[4] as i32) << 4)
| ((raw[5] as i32) >> 4);
let temp = compensate_temperature(raw_temp);
TEMP_CH.send(temp).await;
}
Err(e) => {
defmt::error!("传感器读取失败: {:?}", e);
}
}
}
}
// ===== 任务 2:显示更新 =====
#[embassy_executor::task]
async fn display_task(
mut spi: Spi<'static, embassy_stm32::spi::Spi1>,
mut dc: Output<'static>,
) {
// 初始化 SSD1306(省略,见 8.4 节)
// ...
let mut ticker = Ticker::every(Duration::from_millis(500));
let mut mode: u8 = 0;
loop {
ticker.next().await;
// 检查是否有模式切换请求
if let Ok(new_mode) = MODE_CH.try_receive() {
mode = new_mode;
}
// 获取最新温度
let temp = TEMP_CH.try_receive().unwrap_or(0);
// 更新显示(根据模式)
match mode {
0 => {
// 模式 0:显示温度
let msg = format!("Temp: {}.{:01} C", temp / 100, (temp % 100) / 10);
oled_write_string(&mut spi, &mut dc, 0, 0, msg.as_bytes()).await;
}
1 => {
// 模式 1:显示图形
oled_draw_graph(&mut spi, &mut dc, temp).await;
}
_ => {}
}
}
}
// ===== 任务 3:按键处理 =====
#[embassy_executor::task]
async fn button_task(mut button: Input<'static>) {
let mut mode: u8 = 0;
loop {
button.wait_for_falling_edge().await;
Timer::after(Duration::from_millis(20)).await; // 消抖
if button.is_low() {
mode = (mode + 1) % 2;
MODE_CH.send(mode).await;
defmt::info!("显示模式切换为: {}", mode);
}
button.wait_for_rising_edge().await;
}
}
// ===== 主入口 =====
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_stm32::init(Default::default());
defmt::info!("多外设协作示例启动");
// 初始化外设
let mut i2c_config = I2cConfig::default();
i2c_config.frequency = embassy_stm32::i2c::Frequency::F400k;
let i2c = I2c::new(p.I2C1, p.PB6, p.PB7, p.DMA1_CH0, p.DMA1_CH1, i2c_config).unwrap();
let mut spi_config = SpiConfig::default();
spi_config.frequency = embassy_stm32::spi::Hertz(8_000_000);
spi_config.mode = Mode::Mode0;
let spi = Spi::new(p.SPI1, p.PA5, p.PA7, p.PA6, p.DMA2_CH3, p.DMA2_CH0, spi_config).unwrap();
let dc = Output::new(p.PB0, Level::Low, Speed::High);
let button = Input::new(p.PA0, Pull::Up);
// 启动所有任务
spawner.spawn(sensor_task(i2c)).unwrap();
spawner.spawn(display_task(spi, dc)).unwrap();
spawner.spawn(button_task(button)).unwrap();
// main 任务:心跳日志
loop {
Timer::after(Duration::from_secs(10)).await;
defmt::info!("系统运行正常");
}
}rust这段代码的 C 等价实现需要多少行?#
| 模块 | C 代码量(估计) | Embassy 代码量 |
|---|---|---|
| 系统初始化 + 时钟 | ~50 行 | 1 行 |
| I2C 初始化 + BME280 | ~80 行 | ~30 行 |
| SPI 初始化 + SSD1306 | ~100 行 | ~40 行 |
| UART 初始化 + 日志 | ~40 行 | ~5 行 |
| 按键中断 + 消抖 | ~40 行 | ~15 行 |
| 任务调度(超级循环/RTOS) | ~60 行 | ~10 行 |
| 任务间通信 | ~30 行(全局变量+临界区) | ~5 行(Channel) |
| 总计 | ~400 行 | ~106 行 |
代码量减少约 75%,且消除了所有 volatile、临界区、中断优先级配置。
8.8 本章小结#
API 速查表#
| 外设 | 创建 | 核心操作 | 异步方法 |
|---|---|---|---|
| GPIO 输出 | Output::new(pin, level, speed) | set_high() / set_low() / toggle() | 无(即时操作) |
| GPIO 输入 | Input::new(pin, pull) | is_high() / is_low() | wait_for_falling_edge().await |
| UART | Uart::new(usart, rx, tx, dma_rx, dma_tx, cfg) | write() / read() | 全部异步 |
| SPI | Spi::new(spi, sck, mosi, miso, dma_tx, dma_rx, cfg) | write() / transfer() | 全部异步 |
| I2C | I2c::new(i2c, scl, sda, dma_tx, dma_rx, cfg) | write() / read() / write_read() | 全部异步 |
| Timer | Timer::after(duration) | .await | 本身就是异步 |
| Ticker | Ticker::every(duration) | .next().await | 本身就是异步 |
核心认知#
-
Embassy 的 GPIO 是同步的,通信外设是异步的:
set_high()不需要.await(一条寄存器写入,无需等待),但uart.write()需要.await(DMA 传输需要时间)。 -
DMA 是异步的基础:UART/SPI/I2C 的
.await之所以能”释放 CPU”,是因为底层使用 DMA 传输。没有 DMA,就只能阻塞等待。 -
init()一行搞定所有初始化:时钟树、外设使能、GPIO 复用——全部由embassy_stm32::init()自动处理。C 中需要 50+ 行的SystemClock_Config()+ 外设初始化,在 Embassy 中是一行。 -
所有权系统防止引脚冲突:
p.PA5被Output::new()消耗后,不能再被其他代码使用。如果你尝试把同一个引脚传给两个外设,编译器会报错。C 中没有这种保护。 -
任务间通信通过 Channel:不需要
volatile全局变量,不需要临界区,不需要中断回调。Channel::send().await和Channel::receive().await就是全部。
常见错误与排查#
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
use of moved value: p.PA5 | 同一引脚被使用了两次 | 检查引脚分配,确保每个引脚只用一次 |
no DMA channel available | DMA 通道被其他外设占用 | 更换 DMA 通道(参考数据手册的 DMA 请求映射表) |
I2C error: Nack | 设备地址错误或未连接 | 检查接线、上拉电阻、设备地址 |
SPI transfer timeout | 时钟配置错误或接线问题 | 检查 SCK/MOSI/MISO 接线,降低频率 |
UART garbled output | 波特率不匹配 | 确认 TX/RX 交叉连接,检查波特率设置 |
Timer not working | 未启用 time-driver-any feature | 在 Cargo.toml 中添加该 feature |
下一步#
你现在已经掌握了 Embassy 的基本外设操作。但你可能还有疑问:
Timer::after().await底层是怎么实现的?谁在”计时”?spawner.spawn()到底做了什么?任务是怎么被”调度”的?.await挂起后,CPU 到底在做什么?
这些问题的答案在第 9 章:Embassy 运行时——Executor、Future 与 Waker。
下一章:第 9 章——Embassy 运行时:Executor、Future 与 Waker。我们将打开 Embassy 的”引擎盖”,看看 async/await 在裸机上到底是怎么运行的。