【Zephyr|ESP32-S3】基础学习:点亮第一颗WS2812
【Zephyr|ESP32-S3】基础学习:点亮第一颗WS2812
哈喽,我是余火,一个普通的牛马打工人,目前正在学如何使用Zephyr RTOS。
前面几篇把 Hello World 跑起来了,也学了 LOG 日志系统。这次来控制点硬件外设——点亮 ESP32-S3-DevKitC-1 板载的 WS2812 RGB LED,让它循环显示不同颜色。
WS2812 看起来只是一颗 LED,但它背后串起了 Zephyr 驱动开发的完整链路:Kconfig 启用驱动 → 设备树描述硬件 → 代码调用 API 操作。搞懂这次,后面驱动 I2C、SPI 外设就是同一个套路。
改了哪些东西
在之前 Hello World 的工程上直接修改,一共需改 2 个文件、新增 2 个文件:
| 文件改动 | 改了什么 |
|---|---|
prj.conf |
新增 CONFIG_LED_STRIP=y |
boards/xxx.overlay |
新增设备树配置(引脚映射、I2S、DMA) |
src/main.c |
核心业务代码 |
CMakeLists.txt |
工程名从 project(hello_world) 改为 project(blinky) |
启用 LED Strip 驱动 API
在 prj.conf 里加一行:
CONFIG_LED_STRIP=y
💡 LED Strip 不是子系统,而是设备驱动层的一套 API。
drivers/led_strip.h定义了统一的 LED 灯带操作接口,底层由具体驱动实现(如 I2S 方式、SPI 方式)。这一行 Kconfig 会自动拉取 I2S、DMA 等底层依赖,不用手动一个个开。
为什么要用 I2S + DMA 驱动 WS2812
WS2812 使用单线 NRZ 协议,每个 bit 的时序窗口严格在 1.25μs 左右,高低电平占比区分 0 和 1:
| 参数 | T0H(传0) | T1H(传1) | T0L/T1L |
|---|---|---|---|
| 时间 | 0.4μs | 0.8μs | 0.45μs |
| 容差 | ±0.15μs | ±0.15μs | ±0.15μs |
一帧完整数据包含 24 bit(每个颜色通道 8 bit),发送一颗 LED 的数据需要 30μs 左右。如果灯带上有 60 颗 LED,一次全量刷新就需要 1.8ms。纳秒级时序如果用 GPIO bitbang(软件翻转电平),中断抖动就可能把 0 变成 1。Zephyr 的做法是把 RGB 数据编码成 I2S 音频帧,通过 DMA 传输来保证时序精确——DMA 由硬件搬运数据,不占用 CPU,时序稳定性远优于软件方案。
💡 这也是嵌入式开发中的常见思路:用硬件外设替代软件时序控制。同样的思路在驱动 SPI 屏幕(SPI+DMA)、驱动舵机(PWM+定时器)中都会遇到。
设备树 Overlay
Zephyr 沿用了 Linux 的设备树机制,在设备树中用文本描述硬件信息。我们通过 Overlay 覆盖层来追加配置,不需要改动板子默认的 .dts 文件。
在工程下新建 boards/esp32s3_devkitc_esp32s3_procpu.overlay 并包含以下内容:
#include <zephyr/dt-bindings/led/led.h>
/* I2S0 引脚映射:数据输出接到 GPIO38(WS2812 的 DIN) */
&i2s0_default {
group1 {
pinmux = <I2S0_O_SD_GPIO38>;
};
};
/* I2S0 外设 + DMA 配置 + WS2812 子节点 */
&i2s0 {
status = "okay";
dmas = <&dma 3>;
dma-names = "tx";
led_strip: ws2812@0 {
compatible = "worldsemi,ws2812-i2s";
reg = <0>;
chain-length = <1>;
color-mapping = <LED_COLOR_ID_GREEN
LED_COLOR_ID_RED
LED_COLOR_ID_BLUE>;
};
};
/* 启用 DMA 控制器 */
&dma {
status = "okay";
};
/* 代码中通过 DT_ALIAS(led_strip) 引用,换板子只改 overlay */
/ {
aliases {
led-strip = &led_strip;
};
};
Overlay 各字段含义:
| 字段 | 含义 | 本例值 |
|---|---|---|
compatible |
驱动匹配标识,Zephyr 据此加载对应驱动 | "worldsemi,ws2812-i2s" |
chain-length |
LED 灯珠串联数量 | <1>(板载 1 颗) |
color-mapping |
通道映射顺序(WS2812 硬件是 GRB) | GREEN→RED→BLUE |
status = "okay" |
启用该外设(默认 "disabled") |
I2S0 和 DMA 都要启用 |
dmas |
绑定 DMA 通道 | <&dma 3> — DMA 控制器的第 3 通道 |
chain-length = <1> 表示板载 1 颗 LED,后续要外接灯带改这个值就行。color-mapping 声明 WS2812 的硬件映射关系是 GRB,代码里可以继续按 RGB 使用——驱动内部会自动做通道重排。
头文件引入
#include <stdio.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/led_strip.h>
#include <zephyr/sys/util.h>
led_strip.h 是 LED Strip 驱动 API 的头文件,定义了 struct led_rgb、led_strip_update_rgb() 等我们用到的所有类型和函数。
宏定义
/* 从设备树获取 WS2812 设备节点,对应 overlay 中的 aliases */
#define STRIP_NODE DT_ALIAS(led_strip)
/* 从设备树读取 LED 数量,对应 overlay 中的 chain-length */
#define STRIP_NUM_PIXELS DT_PROP(STRIP_NODE, chain_length)
/* 颜色切换间隔(毫秒) */
#define SLEEP_TIME_MS 500
/* 便捷宏:初始化 struct led_rgb 的 r/g/b 字段 */
#define RGB(r, g, b) {(r), (g), (b)}
💡 为什么用设备树宏而不是硬编码?
DT_ALIAS(led_strip)让代码与硬件解耦——换一块板子,只需要改 overlay 文件,C 代码一行不动。DT_PROP从设备树读取属性值,保证代码中的 LED 数量与设备树声明一致,避免两边改漏。
变量定义与设备获取
/*
* 预定义颜色表(7 种)
* 亮度值 0x20 约等于 12%,避免过亮刺眼
* 顺序:红、绿、蓝、黄、青、品红、白
*/
static const struct led_rgb colors[] = {
RGB(0x20, 0x00, 0x00), /* red */
RGB(0x00, 0x20, 0x00), /* green */
RGB(0x00, 0x00, 0x20), /* blue */
RGB(0x20, 0x20, 0x00), /* yellow */
RGB(0x00, 0x20, 0x20), /* cyan */
RGB(0x20, 0x00, 0x20), /* magenta*/
RGB(0x20, 0x20, 0x20), /* white */
};
/* 像素缓冲区,长度 = LED 数量,每次更新前填充此数组 */
static struct led_rgb pixels[STRIP_NUM_PIXELS];
/* 编译时获取设备实例,节点不存在则构建直接报错 */
static const struct device *const strip = DEVICE_DT_GET(STRIP_NODE);
DEVICE_DT_GET 在编译期从设备树获取设备实例,如果 overlay 里没有声明这个节点,构建阶段直接报错,不会留到运行时才发现。
主函数逻辑
核心流程就三步:获取设备 → 填充像素 → 发送更新。
int main(void)
{
/* 检查驱动是否就绪(I2S、DMA 等是否正常初始化) */
if (!device_is_ready(strip)) {
printf("LED strip device not ready\n");
return 0;
}
printf("WS2812 strip: %d pixel(s)\n", STRIP_NUM_PIXELS);
size_t color = 0;
while (1) {
/* 清空像素缓冲区 */
memset(pixels, 0, sizeof(pixels));
/* 将当前颜色写入每个像素 */
for (size_t i = 0; i < STRIP_NUM_PIXELS; i++) {
memcpy(&pixels[i], &colors[color],
sizeof(struct led_rgb));
}
/* 发送像素数据:RGB → I2S 编码 → DMA 传输 → WS2812 接收 */
led_strip_update_rgb(strip, pixels, STRIP_NUM_PIXELS);
/* 切换到下一种颜色,循环回绕 */
color = (color + 1) % ARRAY_SIZE(colors);
k_msleep(SLEEP_TIME_MS);
}
return 0;
}
编译烧录后,板载 WS2812 每隔 500ms 切换一种颜色,依次循环红、绿、蓝、黄、青、品红、白。
常见问题
Q1:编译报错 undefined reference to 'led_strip_update_rgb'
prj.conf 漏了 CONFIG_LED_STRIP=y。没有这行,驱动 API 不会被编译进固件,链接器找不到函数符号。
Q2:编译报错 DT_ALIAS undeclared
Overlay 文件名与 Build Target 不匹配。Zephyr 按 <board_name>.overlay 匹配文件名,如果 Build Target 是 esp32s3_devkitc_esp32s3_procpu,overlay 文件名必须是 esp32s3_devkitc_esp32s3_procpu.overlay,一个字都不能差。
Q3:运行时打印 device not ready
检查 overlay 中 I2S 和 DMA 是否都设了 status = "okay"。两者缺一不可——只有 I2S 没有 DMA,驱动初始化会失败。
Q4:编译通过但 LED 不亮
确认 GPIO38 接线正确。V1.0 版本的 ESP32-S3-DevKitC-1 板载 LED 接在 GPIO48 而非 GPIO38,overlay 中的 pinmux 需要改为 I2S0_O_SD_GPIO48。可以在 ESP32-S3-DevKitC-1 的原理图上确认板载 LED 对应的引脚编号。
总结
本篇用 Kconfig 启用 LED Strip 驱动、设备树 Overlay 描述 I2S+DMA 硬件、DEVICE_DT_GET 获取设备实例这三步点亮了 WS2812。同样这个"Kconfig → Overlay → 设备树宏 → API"流程也能直接套用到驱动 I2C 传感器、SPI 显示屏、PWM 舵机等外设。
希望我的笔记能对你有一点点点的帮助!欢迎关注一起学习👇
更多推荐



所有评论(0)