作为一名对嵌入式开发感兴趣的开发者,你是否也想尝试用现代、安全的 Rust 语言来控制硬件?本文将以一块具体的 ESP32-S3 开发板和一个 WS2812 RGB LED 为例,带你从零开始,实现一个简单的闪烁灯效,并对比传统的 C 语言实现方式。希望通过这篇在 云栈社区 分享的实战记录,能为你开启 Rust 嵌入式开发的大门。
硬件准备
我使用的硬件是一块 ESP32-S3-WROOM-1 开发板。
外设方面,接入了一个 WS2812 LED 灯珠,其信号线连接到了开发板的 GPIO 48 引脚。
开发板的实物图如下:

其与 WS2812 连接的简易电路示意图如下,清晰地标明了控制引脚:

我们的目标很简单:用 Rust 编程,让这个 LED 灯亮起来。
传统实现:C语言版本
如果使用经典的 C 语言来实现,过程会相对直接,因为 ESP-IDF 生态已经相当成熟和完善。核心代码如下:
#include <stdio.h>
#include “freertos/FreeRTOS.h”
#include “freertos/task.h”
#include “driver/gpio.h”
#include “sdkconfig.h”
#include “esp_log.h”
#include “led_strip.h”
static const char *TAG = “main”;
#define GPIO_NUM_48 48
led_strip_handle_t led_flash_init(void) {
// Initialize LED strip
led_strip_config_t led_cfg = {
.strip_gpio_num = GPIO_NUM_48,
.max_leds = 1,
.flags.invert_out = false,
.led_model = LED_MODEL_WS2812,
};
// led strip backend config rmt
led_strip_rmt_config_t rmt_cfg = {
.clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = (10*1000*1000),
.flags.with_dma = false,
};
led_strip_handle_t led_strip = NULL;
led_strip_new_rmt_device(&led_cfg, &rmt_cfg, &led_strip);
ESP_LOGI(TAG, “LED strip initialized”);
return led_strip;
}
void app_main(void)
{
led_flash_init();
ESP_LOGI(TAG, “Starting”);
led_strip_handle_t led_strip = led_flash_init();
bool led_on_of = false;
// 设置亮度
uint8_t brightness = 50;
uint8_t red = (0 * brightness) / 100;
uint8_t green = (0 * brightness) / 100;
uint8_t blue = (139 * brightness) / 100;
while (1)
{
if (led_on_of)
{
led_strip_set_pixel(led_strip, 0, red, green, blue);
led_strip_refresh(led_strip);
ESP_LOGI(TAG, “LED on”);
}else {
led_strip_clear(led_strip);
ESP_LOGI(TAG, “LED off”);
}
led_on_of = !led_on_of;
vTaskDelay(pdMS_TO_TICKS(500));
}
}
C 语言版本直接使用了乐鑫官方提供的 led_strip 组件库。当然,整个项目需要基于官方的 ESP-IDF 项目模板来构建。
现代实践:Rust 实现
现在,让我们看看如何用 Rust 来完成同样的任务。Rust 嵌入式开发的工具链配置稍显复杂,以下是部分必需的准备工作(某些命令可能有重叠,以实际为准):
# Espressif 工具链
cargo install cargo-espflash espflash # 新的调试现在推荐用 probe-rs 了
# debian/ubuntu
sudo apt install llvm-dev libclang-dev clang
不过,更推荐的方法是使用 esp-generate 工具来一键创建项目。这个命令会自动检测并下载对应芯片的工具链,并生成一个配置好的项目骨架。
esp-generate --chip esp32s3 -o alloc -o vscode -o wokwi -o esp-backtrace -o log led
# or use probe-rs
esp-generate --chip esp32s3 -o alloc -o vscode -o wokwi -o probe-rs led
目前更推荐使用 probe-rs,它是一个功能强大的嵌入式调试和交互工具包。需要注意的是,esp-generate 命令在国内网络环境下可能会在下载工具链(espup)时卡住。如果遇到问题,可以尝试手动使用 espup install 来安装 ESP 相关的 Rust 工具链。
重要提示:ESP32 不同系列的芯片,编译目标(target)是不同的。例如,C3 系列是 RISC-V 架构,而 S3 系列是 xtensa-esp32s3-none-elf。安装完工具链后,记得像安装 Rust 本身后需要 source $HOME/.cargo/env 一样,也需要 source 由 espup 导出的环境变量脚本(例如 source /home/kkch/export-esp.sh),否则 Rust 工具链会使用默认的系统环境。
no_std 与 std 环境的选择
在 Rust 生态中,针对 ESP32 有两套主要的开发环境:std 和 no_std。
no_std:官方 esp-rs 团队主要维护和支持的环境。它不依赖操作系统的标准库,更适合资源受限的嵌入式平台。
std:由社区维护,它提供了类似桌面开发的标准库体验,但可能会引入更多资源开销。
官方流行文档中很多例子基于 std,但本着更贴近硬件、更节省资源的原则,本文的实现将基于 no_std 环境。
| Repository |
Description |
Support status |
| esp-rs/esp-hal |
no_std |
官方维护 |
| esp-rs/esp-idf-hal |
std |
社区维护 |
依赖配置
在 no_std 环境下控制 WS2812,我们需要在 Cargo.toml 中添加以下依赖。这些 crates 大部分可由 esp-generate 自动生成。
[dependencies]
esp-hal = { version = “~1.0”, features = [“esp32s3”] }
anyhow = {version = “=1.0.100”, default-features = false}
esp-bootloader-esp-idf = { version = “0.4.0”, features = [“esp32s3”] }
critical-section = “1.2.0”
esp-alloc = “0.9.0”
rtt-target = “0.6.2”
blinksy-esp = {version = “0.11.0”,features = [“esp32s3”]}
blinksy = “0.11.0”
其中,用于控制 WS2812 的关键 crate 是 blinksy(及其平台适配 blinksy-esp)。blinksy 是一个用于控制 LED 阵列的库,它恰好支持 WS2812 灯珠和 ESP 平台,能够处理 1D、2D、3D 的 LED 布局。
如果你选择 std 环境,也可以考虑 ws2812-esp32-rmt-driver 这个 crate。
代码实现
由于我们只有一个 LED 灯,所以定义为一维布局,且数量为 1:
layout1d!(Layout, 1); //只有一个灯
//如果有多个灯组成灯条,那么可以这样写:
//layout1d!(Layout,60); //60个灯
1. 初始化 ESP32S3 的 RMT 驱动
RMT(Remote Control)是 ESP32 上用于生成精确时序脉冲的外设,非常适合驱动 WS2812。
let ws2812_driver = {
let data_pin = p.GPIO48;
let rmt_clk_freq = hal::time::Rate::from_mhz(80);
let rmt = hal::rmt::Rmt::new(p.RMT, rmt_clk_freq).unwrap();
let rmt_channel = rmt.channel0;
ClocklessDriver::default().with_led::<Ws2812>().with_writer(
ClocklessRmtBuilder::default()
.with_rmt_buffer_size::<{ Layout::PIXEL_COUNT * 3 * 8 + 1 }>()
.with_led::<Ws2812>()
.with_channel(rmt_channel)
.with_pin(data_pin)
.build(),
)
};
2. 构建 LED 控制器
let mut control = ControlBuilder::new_1d()
.with_layout::<Layout, { Layout::PIXEL_COUNT }>()
.with_pattern::<Rainbow>(RainbowParams {
..Default::default()
})
.with_driver(ws2812_driver)
.with_frame_buffer_size::<{ Ws2812::frame_buffer_size(Layout::PIXEL_COUNT) }>()
.build();
3. 设置颜色与亮度
这里 RGB 值和亮度的取值范围都是 0.0 到 1.0。
control.set_color_correction(blinksy::color::ColorCorrection { red: 0.5, green: 0.5, blue: 0.5 });
control.set_brightness(0.5); //取值区间是 0-1
4. 在主循环中刷新
loop {
let elapsed_in_ms = elapsed().as_millis();
control.tick(elapsed_in_ms).unwrap();
}
完整代码示例
要将上面 C 语言的效果(蓝灯交替亮灭)用 Rust 实现,完整的 main.rs 代码如下:
#![no_std]
#![no_main]
#![deny(
clippy::mem_forget,
reason = “mem::forget is generally not safe to do with esp_hal types, especially those \
holding buffers for the duration of a data transfer.”
)]
#![deny(clippy::large_stack_frames)]
use esp_alloc as _;
use esp_hal::{self as hal, delay::Delay};
use esp_hal::main;
use blinksy::{
ControlBuilder,
driver::ClocklessDriver,
layout::Layout1d,
layout1d,
leds::Ws2812,
patterns::rainbow::{Rainbow, RainbowParams},
};
use blinksy_esp::{rmt::ClocklessRmtBuilder, time::elapsed};
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
extern crate alloc;
esp_bootloader_esp_idf::esp_app_desc!();
#[allow(
clippy::large_stack_frames,
reason = “it's not unusual to allocate larger buffers etc. in main”
)]
#[main]
fn main() -> ! {
let cpu_clock = hal::clock::CpuClock::max();
let config = hal::Config::default().with_cpu_clock(cpu_clock);
let p = hal::init(config);
// layout1d!(Layout, 60 * 5);
layout1d!(Layout, 1);
let ws2812_driver = {
let data_pin = p.GPIO48;
let rmt_clk_freq = hal::time::Rate::from_mhz(80);
let rmt = hal::rmt::Rmt::new(p.RMT, rmt_clk_freq).unwrap();
let rmt_channel = rmt.channel0;
ClocklessDriver::default().with_led::<Ws2812>().with_writer(
ClocklessRmtBuilder::default()
.with_rmt_buffer_size::<{ Layout::PIXEL_COUNT * 3 * 8 + 1 }>()
.with_led::<Ws2812>()
.with_channel(rmt_channel)
.with_pin(data_pin)
.build(),
)
};
let mut control = ControlBuilder::new_1d()
.with_layout::<Layout, { Layout::PIXEL_COUNT }>()
.with_pattern::<Rainbow>(RainbowParams {
..Default::default()
})
.with_driver(ws2812_driver)
.with_frame_buffer_size::<{ Ws2812::frame_buffer_size(Layout::PIXEL_COUNT) }>()
.build();
control.set_color_correction(blinksy::color::ColorCorrection {
red: 0.0,
green: 0.0,
blue: 1.0,
});
control.set_brightness(0.5); // Set initial brightness (0.0 to 1.0)
let mut led_on_off = false;
let delay = Delay::new();
loop {
if led_on_off {
control.set_color_correction(blinksy::color::ColorCorrection {
red: 0.0,
green: 0.0,
blue: 1.0,
});
} else {
control.set_color_correction(blinksy::color::ColorCorrection {
red: 0.0,
green: 0.0,
blue: 0.0,
});
}
led_on_off = !led_on_off;
let elapsed_in_ms = elapsed().as_micros();
control.tick(elapsed_in_ms).unwrap();
delay.delay_millis(500);
}
}
最终效果
无论是 C 语言版本还是 Rust 版本,最终实现的效果是相同的:连接在 GPIO 48 上的 WS2812 LED 灯会以约 0.5 秒的间隔交替亮起(蓝色)和熄灭。通过这个简单的项目,你可以直观地感受到 Rust 在嵌入式领域的应用方式,虽然初始工具链配置和概念理解可能比传统的 C/C++ 稍复杂,但其强大的类型安全和丰富的现代语言特性,为构建更可靠、更易维护的嵌入式软件提供了可能。感兴趣的开发者可以在此基础上,探索更多来自 开源实战 领域的复杂项目和驱动库。