← EasyTool.me

我的毕业帽跑着 Rust:RP2040 + WS2812B LED 矩阵实战指南

发布日期: 2026-05-13

EasyTool.me12 分钟阅读English

"我的毕业帽跑着 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!" 滚动文字、彩虹渐变循环。

物料清单:

总物料成本:约 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 crateRust 生态中可寻址 LED 的标准库。内置 HSV 色彩空间转换、伽马校正和动画辅助函数。通过 RP2040 HAL 的 ws2812-pio 驱动处理精确的 800 kHz 时序信号。

Rust 在"选哪个语言"这个问题上一赢三:
  1. 安全性:借用检查器和类型级引脚状态防止了可导致硬件变砖的 bug
  2. 生态:embassy-rs + smart-leds + RP2040 HAL 是一个成熟、文档完善的工具链
  3. 异步: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 降压)。

流苏检测:干簧管常开。一颗小钕磁铁粘在流苏上。仪式上拨动流苏时,磁铁远离干簧管,电路断开,固件检测到上升沿后触发灯效。

焊接技巧

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 闪存,还有大量空间添加更多动画。

电池与可穿戴性

整个项目最棘手的部分是电池管理。以下是最终方案:

重量:总增重约 200 g——能感觉到但不难受。充电宝最重,约 100 g。

效果:看起来什么样

最终效果比预期的好。LED 光线打在头上形成柔和的漫反射辉光,10 米外清楚可见但不刺眼。白天效果偏含蓄——帽子下透出温暖的光。在毕业礼堂那种偏暗的室内灯光下,颜色特别漂亮。

我最终定的动画序列:帽子默认熄灭,流苏拨动后依次播放旋转地球(10 秒)、"Hello, World!" 滚动文字(15 秒)、彩虹循环(30 秒),然后淡入慢速脉冲动画维持到典礼结束。慢速脉冲约 200 mA,能撑几小时。

"最有趣的是跟朋友解释:是的,我的毕业帽真的在滚动显示 'Hello, World!' 红色 LED 文字;是的,它在跑嵌入式异步 Rust 加协作调度器——那个帧精确的动画计时不是巧合,是 embassy-rs 在干活。"

给嵌入式 Rust 开发者的关键启示

1. embassy-rs 改变了游戏规则

给微控制器写 async 代码感觉像在作弊。embassy 提供了 TimerSpawner 和无操作系统的执行器。对 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。

准备好开始你自己的 Rust 嵌入式项目了吗? rp-hal 仓库有优秀的入门示例。embassy-rs 教程逐步讲解异步模式。smart-leds 文档涵盖从伽马校正到动画链的一切。

如果重来一次我会改什么

去创造点东西吧

这个项目花了一周末工作和几小时 Rust 编码,成果是一个真正能让别人会心一笑的毕业帽。Rust 嵌入式生态让它变得简单——不是"可以做到",而是"做起来很愉快"。

如果你在学 Rust 又想找个好玩的项目,这真的很棒。你会接触到实时异步编程、硬件接口、I/O 外设和电池优化——全部打包在一个能戴在头上的东西里。

代码在 GitHub 上开源。去 fork 它、改造它、做一个更好的版本。我很期待看到你做的东西。