【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_rgbled_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 舵机等外设。


希望我的笔记能对你有一点点点的帮助!欢迎关注一起学习👇

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐