我的毕业帽跑着 Rust:RP2040 + WS2812B LED 矩阵实战指南
发布日期: 2026-05-13
"我的毕业帽跑着 Rust。" 这句话我想了挺久的——好在 Rust 嵌入式生态不只是说说而已。这个项目让我深入体验了 embassy-rs、RP2040 HAL、WS2812B LED 时序控制,以及用安全语言写嵌入式固件的乐趣。
这篇文章完整记录了如何把一个 WS2812B RGB LED 矩阵嵌入到毕业帽上——电子部分、Rust 代码、电池方案,以及为什么我觉得 Rust 是最适合这种创意嵌入式项目的语言。
这个项目的创意来源于 Eric Park 的原版毕业帽项目(HN 40+ 分),我用 RP2040 + embassy-rs 做了异步控制方案的扩展。
项目概览:我们在做什么
概念很简单:把 RGB LED 矩阵嵌在毕业帽内侧,用 Rust 写固件控制,电池供电,可以戴着走。流苏从右向左拨动时触发 LED 动画——旋转地球、"Hello, World!" 滚动文字、彩虹渐变循环。
物料清单:
- 树莓派 Pico(RP2040)——大脑,通过 embassy-rs 框架跑 Rust 固件
- 48 颗 WS2812B RGB LED——排列成 6×8 矩阵贴在帽檐内侧
- USB-C PD 触发板——从充电宝获取稳定的 5V 供电
- 充电宝——标准 USB-C 充电宝,藏在帽子里
- 干簧管 + 小磁铁——一个在帽子上,一个在流苏上,检测流苏拨动
- 跳线和焊锡——手工焊接,没有定制 PCB
总物料成本:约 150 元人民币。总制作时间:一周末焊接 + 几小时 Rust 编码。
为什么嵌入式项目选 Rust?
在深入具体制作之前,值得理解为什么 Rust 是嵌入式项目的优秀选择——尤其像这种要求可靠性、续航和开发体验的项目。
类型级硬件安全:Rust 的类型系统在编译时就捕获引脚配置错误。你不小心把输出引脚配置成输入了?编译器直接报错——不同引脚模式被编码为不同的类型。这在 C 里可能要花几小时用逻辑分析仪调试。
零成本抽象:Rust 的嵌入式生态基于 trait 系统(比如 embedded-hal),编译后和手写 C 的机器码一样高效。embassy-rs 异步任务的运行时开销为零——相比传统 RTOS,没有额外的上下文切换成本。
embassy-rs 异步运行时:这是 RP2040 开发的一个游戏规则改变者。embassy-rs 提供了完全在微控制器上运行的异步运行时——不需要操作系统。你把并发动画写成独立的 async 任务,执行器负责基于硬件定时器的协作调度。在 C 里需要状态机或中断处理器的功能变成简单的 async/await 循环。
smart-leds crate:Rust 生态中可寻址 LED 的标准库。内置 HSV 色彩空间转换、伽马校正和动画辅助函数。通过 RP2040 HAL 的 ws2812-pio 驱动处理精确的 800 kHz 时序信号。
- 安全性:借用检查器和类型级引脚状态防止了可导致硬件变砖的 bug
- 生态:embassy-rs + smart-leds + RP2040 HAL 是一个成熟、文档完善的工具链
- 异步:async/await 协作多任务对 LED 动画来说是完美匹配——每个动画就是一个 yield 给执行器的循环
关于为什么维护成本在嵌入式项目中也很重要,可以参考我们的分析:AI 编码代理的真正考验:降低维护成本而不是增加负担——同样的原理也适用于固件,选择让你以后少调试的语言。
硬件制作:手工焊接 48 颗 LED
这是整个项目最花时间的部分。把 48 颗 WS2812B 焊到租来的毕业帽上——还是不能永久改动的帽子——不但需要耐心,还要求点手艺。
LED 矩阵布局
我排列成 6×8 网格,每颗间距约 1.5 厘米,覆盖帽子内侧大约 9×12 厘米的区域。LED 朝向头部——光线打在毕业生头顶上之后形成漫反射辉光效果,而不是直接刺眼。
每颗 WS2812B 有四根引脚:VCC(5V)、GND、DIN(数据输入)、DOUT(数据输出)。它们串联:一颗的 DOUT 接到下一颗的 DIN。RP2040 的 GPIO 引脚驱动第一颗 LED 的 DIN,数据依次流过全部 48 颗。
接线简图
充电宝 (USB-C)
|
USB-C PD 触发板 (协商 5V)
|
+-- 5V 总线 ---- [48 颗 WS2812B 全部 VCC 脚]
|
+-- GND 总线 --- [48 颗 WS2812B 全部 GND 脚 + RP2040 GND]
|
RP2040 GPIO 28 ---- [LED #1 DIN]
LED #1 DOUT ---- [LED #2 DIN]
LED #2 DOUT ---- [LED #3 DIN]
... (串联全部 48 颗)
RP2040 GPIO 15 ---- [干簧管 + 上拉电阻]
干簧管 ---- [GND] (磁铁靠近时导通)
磁铁 ---- [粘在流苏上]
供电考量:48 颗 WS2812B 全白亮度约消耗 2.4 A(每颗 50 mA)。这超过了 RP2040 的 3.3V 稳压器容量。USB-C PD 触发板从充电宝协商 5V 最高 3A 供电。RP2040 通过 VSYS 引脚从 5V 总线取电(板载稳压器负责 5V→3.3V 降压)。
流苏检测:干簧管常开。一颗小钕磁铁粘在流苏上。仪式上拨动流苏时,磁铁远离干簧管,电路断开,固件检测到上升沿后触发灯效。
焊接技巧
- 先给每个焊盘上锡。WS2812B 的焊盘很小(约 2 mm²)。放 LED 之前先在每个焊盘上点一小坨锡。
- 用细线。我从一根坏掉的 USB-C 线里剥线出来。28 AWG 刚好——够软好布线,也够粗承受电流。
- 分段测试。每焊完 8 颗就上电跑个快速测试,确认链路正常再继续。
- 热熔胶做应力释放。焊点上抹一点热熔胶,防止帽子弯折时线断。
Rust 固件:核心代码
固件基于 embassy-rs 的异步执行器。来看关键部分。
项目配置(Cargo.toml)
[package]
name = "gradcap-rs"
version = "0.1.0"
edition = "2024"
[dependencies]
embassy-rp = "0.3"
embassy-executor = "0.7"
embassy-time = "0.5"
smart-leds = "0.4"
ws2812-pio = "0.5"
panic-halt = "0.2"
embedded-hal = "1.0"
[profile.release]
lto = true
opt-level = "s"
debug = false
主入口
#![no_std]
#![no_main]
use embassy_executor::Spawner;
use embassy_rp::gpio::{Input, Pull, Output, Level};
use embassy_rp::pio::Pio;
use embassy_time::{Timer, Duration};
use smart_leds::{
hsv::{Hsv, hsv2rgb},
RGB8,
};
use ws2812_pio::Ws2812Pio;
use panic_halt as _;
const LED_COUNT: usize = 48;
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// 通过 PIO(可编程 I/O)驱动 WS2812B
let pio = Pio::new(p.PIO0);
let led_pin = Output::new(p.PIN_28, Level::Low);
let mut ws2812 = Ws2812Pio::new(
pio.common,
pio.sm0,
led_pin,
pio.dma,
);
// 干簧管输入(内部上拉)
let reed_switch = Input::new(p.PIN_15, Pull::Up);
spawner.spawn(animation_task(ws2812, reed_switch)).unwrap();
}
动画系统(async 任务)
这是 embassy-rs 最闪耀的地方。每个动画是一个独立的 async 任务。执行器协作调度——一个任务 await(等待定时器)时,另一个任务运行。
#[embassy_executor::task]
async fn animation_task(
mut ws2812: Ws2812Pio,
reed_switch: Input<'static>,
) {
let mut leds = [RGB8::default(); LED_COUNT];
let mut tassel_moved = false;
loop {
// 等待流苏被拨动(干簧管上升沿)
loop {
if reed_switch.is_high() {
tassel_moved = true;
break;
}
Timer::after(Duration::from_millis(10)).await;
}
// 执行动画序列
spinning_globe(&mut ws2812, &mut leds).await;
hello_world(&mut ws2812, &mut leds).await;
rainbow_cycle(&mut ws2812, &mut leds).await;
// 序列结束后熄灭
leds = [RGB8::default(); LED_COUNT];
ws2812.write(&leds).await;
tassel_moved = false;
}
}
动画:旋转地球
地球动画利用 HSV 色彩循环创建海洋/大陆对比,通过偏移量在 LED 矩阵上滚动显示。
async fn spinning_globe(
ws2812: &mut Ws2812Pio,
leds: &mut [RGB8; LED_COUNT],
) {
let mut offset = 0;
for _ in 0..256 {
for (i, led) in leds.iter_mut().enumerate() {
let pos = (i + offset) % LED_COUNT;
let is_land = (pos / 6 + pos % 6) % 3 != 0;
*led = if is_land {
RGB8::new(34, 139, 34) // 森林绿
} else {
RGB8::new(30, 100, 200) // 海洋蓝
};
}
ws2812.write(leds).await;
offset = (offset + 1) % 6;
Timer::after(Duration::from_millis(50)).await;
}
}
动画:"Hello, World!" 滚动文字
6×8 矩阵上显示文字虽然挤但还能看。每个字符编成 5×6 像素点阵,整行滚动显示。
async fn hello_world(
ws2812: &mut Ws2812Pio,
leds: &mut [RGB8; LED_COUNT],
) {
// 简单的位图字体 (5x6 像素每字符)
let font: &[&[u8]] = &[
// H
&[0b10001, 0b10001, 0b11111, 0b10001, 0b10001],
// ...(其他字符)
];
for offset in 0..(font.len() * 6 + 8) {
leds.fill(RGB8::default());
for (col, char_idx) in ((offset / 6)..font.len()).enumerate() {
if col >= 8 { break; }
let char_data = font[char_idx];
let char_offset = offset % 6;
for row in 0..5 {
if char_data[row] >> (6 - char_offset - 1) & 1 == 1 {
let led_idx = row * 8 + col;
if led_idx < LED_COUNT {
leds[led_idx] = RGB8::new(255, 0, 0);
}
}
}
}
ws2812.write(leds).await;
Timer::after(Duration::from_millis(100)).await;
}
}
动画:彩虹渐变循环
经典演示——遍历整个 HSV 色调范围,创建平滑的彩虹扫掠。在 smart-leds crate 帮助下几乎不费吹灰之力。
async fn rainbow_cycle(
ws2812: &mut Ws2812Pio,
leds: &mut [RGB8; LED_COUNT],
) {
for hue in (0..=255).step_by(2) {
for (i, led) in leds.iter_mut().enumerate() {
let hsv = Hsv {
hue: (hue + (255 / LED_COUNT * i) as u8) as u8,
sat: 255,
val: 128,
};
*led = hsv2rgb(hsv);
}
ws2812.write(leds).await;
Timer::after(Duration::from_millis(20)).await;
}
}
编译和刷写
RP2040 的 Rust 编译流程很直接:
# 安装 RP2040 目标
rustup target add thumbv6m-none-eabi
# 编译(release 模式最小二进制体积)
cargo build --release
# 进入 Pico 引导模式(按住 BOOTSEL 插入 USB)
# 用 elf2uf2 刷写(拖放方式)
cargo install elf2uf2-rs
elf2uf2-rs target/thumbv6m-none-eabi/release/gradcap-rs
# 或用 probe-rs(需要调试探头)
cargo install probe-rs --features cli
probe-rs run --chip RP2040 target/thumbv6m-none-eabi/release/gradcap-rs
release 二进制大小约 48 KB——远小于 RP2040 的 2 MB 闪存,还有大量空间添加更多动画。
电池与可穿戴性
整个项目最棘手的部分是电池管理。以下是最终方案:
- 充电宝:一个薄的 5000 mAh USB-C 充电宝,用魔术贴固定在帽子内衬里。
- USB-C PD 触发:不是必须的——很多充电宝直接输出 5V。但触发板确保纯净供电,防止 LED 脉冲时充电宝自动断电。
- 线管理:一根 15 厘米的 USB-C 短线连接充电宝和触发板。所有东西塞进帽冠,留一个小开口给 USB-C 口(用于充电)。
- 续航:全白亮度约消耗 2.4 A。彩虹循环半亮度(val=128)约 1.2 A。5000 mAh 充电宝可持续 3-4 小时持续动画。毕业典礼约 2 小时,我用 30% 亮度(约 700 mA)——绰绰有余。
重量:总增重约 200 g——能感觉到但不难受。充电宝最重,约 100 g。
效果:看起来什么样
最终效果比预期的好。LED 光线打在头上形成柔和的漫反射辉光,10 米外清楚可见但不刺眼。白天效果偏含蓄——帽子下透出温暖的光。在毕业礼堂那种偏暗的室内灯光下,颜色特别漂亮。
我最终定的动画序列:帽子默认熄灭,流苏拨动后依次播放旋转地球(10 秒)、"Hello, World!" 滚动文字(15 秒)、彩虹循环(30 秒),然后淡入慢速脉冲动画维持到典礼结束。慢速脉冲约 200 mA,能撑几小时。
给嵌入式 Rust 开发者的关键启示
1. embassy-rs 改变了游戏规则
给微控制器写 async 代码感觉像在作弊。embassy 提供了 Timer、Spawner 和无操作系统的执行器。对 LED 动画来说简直是完美选择——每个动画写成一个简单的 async 循环,执行器在它们之间切换。
2. RP2040 是很棒的 Rust 目标
PIO(可编程 I/O)模块和 Rust 的类型安全性是绝配。embassy-rp crate 把 PIO 作为类型检查的资源暴露出来——你在编译时指定 PIO 块、状态机和 DMA 通道,编译器捕获不匹配。
3. WS2812B 时序挺麻烦(但 PIO 让它变简单)
WS2812B 使用单线协议,需要精确的 800 kHz 时序。软件方式位敲是可能的但很脆弱。基于 PIO 的驱动(ws2812-pio)完全在硬件中生成信号,CPU 可以自由运行动画,PIO 负责时序。
4. Rust 嵌入式生态已经可以用于生产
2026 年的 Rust 嵌入式生态已经成熟。embassy-rs 有完善的文档。smart-leds 处理所有色彩数学。probe-rs 提供了在 VS Code 中的 GDB 风格断点调试。工具链和 C/C++ 一样流畅——甚至在某些方面更流畅,因为 cargo 处理依赖和版本无需折腾 CMake。
如果重来一次我会改什么
- 先焊接一小部分再继续:我焊完 48 颗才测试。有一颗虚焊,在链中找到那颗坏掉的 LED 花了 45 分钟的万用表排查。少量分批测试。
- 电池电压降:全白亮度时 5V 总线的电压降足够触发 RP2040 的欠压检测。在 5V 总线上加 470 µF 电容彻底修复。
- 干簧管去抖:机械干簧管会产生触点抖动。在固件中用
Timer::after加 10 ms 去抖延迟干净解决。 - 做成可拆卸的:如果是做在租的帽子上,我会用小万用板代替直接焊在帽布上。布上的焊点如果帽子被弯折会脱焊。
去创造点东西吧
这个项目花了一周末工作和几小时 Rust 编码,成果是一个真正能让别人会心一笑的毕业帽。Rust 嵌入式生态让它变得简单——不是"可以做到",而是"做起来很愉快"。
如果你在学 Rust 又想找个好玩的项目,这真的很棒。你会接触到实时异步编程、硬件接口、I/O 外设和电池优化——全部打包在一个能戴在头上的东西里。
代码在 GitHub 上开源。去 fork 它、改造它、做一个更好的版本。我很期待看到你做的东西。