之前我们用 GPIO 输出点亮过单色 LED,用也通过多个灯的轮流亮灭实现了流水灯。但一颗灯只能亮一种颜色,想做更加丰富的效果就不行了。这篇文章我们来实现进阶的------WS2812 全彩灯带,一颗灯珠就能调出 1600 万种颜色,一整串灯各自亮各自的颜色,做跑马灯、彩虹渐变都不在话下,这种灯我们可以在路边的树上,玩具上等地方都有应用。
这里我们用 ESP-IDF 的 led_strip 组件 + RMT 驱动实现,同时也搞清楚 RGB 顺序、供电、共地这些容易出现问题的地方。
1. 开始前准备
1.1 硬件准备
| 名称 | 数量 | 说明 |
|---|---|---|
| ESP32-P4 开发板 | 1 块 | 用 GPIO2 输出数据信号 |
| WS2812 灯带 / 灯环 | 1 条 | 建议使用RGB模块,我们直接接线就行 |
| 5V 独立电源 | 1 个 | 灯带多了必须外接,别全靠开发板供电 |
| 杜邦线(母对母) | 若干 | 信号线 + 电源线 + 地线 |
1.2 软件准备
不需要新装任何东西,直接复用第 1 课的环境
如何还没有搭建环境可以参照第一课【ESP32入门】Windows 下 ESP-IDF V6.1 环境搭建(上):EIM 离线安装 + VS Code 配置 + 驱动安装_eim-gui-windows-x64-CSDN博客)
1.3 基础概念
1.3.1 WS2812 是什么:一颗灯珠就是一个小工位
普通 LED 就是个发光二极管,通电就亮。WS2812 不一样------它把红、绿、蓝三颗灯珠加一个控制芯片,封在一个小方块里。
最妙的是"可寻址":一整串灯,信号从第一颗进,传完了自动传给下一颗。不用每颗灯都拉一根线出来,三根线(VCC、GND、S)就能控制一整条灯带。
1.3.2 RGB 顺序:不是 RGB,是 GRB
在WS2812 的数据顺序是 GRB 不是 RGB。也就是说你发 24 位颜色数据:前 8 位绿色,中间 8 位红色,最后 8 位蓝色。顺序搞反了的话,想要红色结果亮成绿色。但是用 led_strip 组件的时候,它会帮忙处理这个顺序,你只管给 R、G、B 三个值就行。在WS2812的数据手册中,我们可以看到GRB的顺序,如下:

1.3.3 单线协议:靠高电平长短区分 0 和 1
WS2812 的通信方式很特别------它没有时钟线,只有一根数据线,对应的就是模块的S引脚。它是靠高电平持续的时间长短 来区分发的是 0 还是 1:

- 发 0:高电平约 0.4us,然后低电平
- 发 1:高电平约 0.8us,然后低电平
- 发完一整串,拉低超过 50us,告诉灯珠"这一帧结束了,亮吧"
这个时序精度要求很高,差 100ns 都可能认错。所以不能用普通 GPIO 软件翻转来做------CPU 调度有抖动,时间不准。ESP32 的 RMT 外设就是干这个的:你给它一个时间清单,硬件自动按时翻转引脚,不占 CPU。
1.3.4 供电
WS2812 满亮的时候,一颗灯珠需要 60mA 左右电流。
- 8 颗灯全白 ≈ 480mA
USB 口才供得起 500mA,多接几颗灯就不够了。所以灯带长了,必须外接 5V 独立电源。
还有个关键点:开发板和灯带必须共地。信号线的参考电平是 GND,两个 GND 不连在一起,信号就是"对空气"发的,灯珠根本认不出来。
1.3.5 判断3.3V 单片机能否直接驱动
虽然现在的开发板通常同时带有 5V 和 3.3V 两种供电引脚 ,但供电电压不等于 IO 口的逻辑电平 。即使我们将 WS2812 的电源(VCC)接在开发板的 5V 引脚上,目前主流单片机(如 ESP32等)引脚输出的逻辑高电平依然只有 3.3V 。用 3.3V 的信号去控制 5V 供电的灯珠,就存在无法正常驱动的风险。在WS2812的规格书中我们可以看到如下图电气参数:

从图中可以看到,高电平输入最小值为 0.55VDD ,只要计算出的阈值小于 3.3V 即可正常驱动。已知表中 VDD = 5V ,计算可得 0.55 × 5V = 2.75V < 3.3V,因此证明该灯珠可以直接使用 3.3V 逻辑电平驱动。在其他产品中,如果计算出的阈值大于 3.3V(无法直接驱动),则需要使用电平转换芯片或模块(如 74HCT245 等)对 S 引脚进行电平转换。
2. 案例一:点亮红绿蓝单色
先从最简单的开始:让第一颗灯分别亮红、绿、蓝三种颜色,循环切换。
2.1 接线
断电状态下接线:
| 线 | ESP32P4 | 灯带 / 电源 | 作用 |
|---|---|---|---|
| 信号线 | GPIO22 | DIN / S | RMT 数据信号 |
| 地线 | GND | GND(电源地) | 共地,必须接 |
| 电源线 | 接开发板5V | 5V (电源正) | 灯带供电 |
这里我们使用的模块只有6颗灯珠,所以没有使用独立电源模块供电了。

2.2 打开工程
使用课程提供的 WS2812_RGB 工程压缩包,解压到纯英文路径 下,用 VS Code 直接打开工程文件夹也可以。(WS2812_RGB项目下载)
2.2 代码解释
完整代码先放出来,然后我们拆开讲:
c
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "led_strip.h"
#define LED_NUM 6 // 灯的数量
#define LED_GPIO 22 // 数据引脚
void app_main(void)
{
// 配置灯带参数
led_strip_config_t strip_config = {
.strip_gpio_num = LED_GPIO, // 数据输出引脚
.max_leds = LED_NUM, // 灯带总灯数
};
led_strip_rmt_config_t rmt_config = {
.resolution_hz = 10 * 1000 * 1000, // RMT 时钟分辨率 10MHz
};
// 创建灯带实例
led_strip_handle_t strip;
led_strip_new_rmt_device(&strip_config, &rmt_config, &strip);
// 主循环:红、绿、蓝轮流亮
while (1) {
for (int i = 0; i < LED_NUM; i++) {
led_strip_set_pixel(strip, i, 255, 0, 0); // 亮红色
}
led_strip_refresh(strip); // 把数据发出去
vTaskDelay(pdMS_TO_TICKS(1000)); // 保持 1 秒
for (int i = 0; i < LED_NUM; i++) {
led_strip_set_pixel(strip, i, 0, 255, 0); // 亮绿色
}
led_strip_refresh(strip);
vTaskDelay(pdMS_TO_TICKS(1000));
for (int i = 0; i < LED_NUM; i++) {
led_strip_set_pixel(strip, i, 0, 0, 255); // 亮蓝色
}
led_strip_refresh(strip);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
第一段:配置灯带参数
c
led_strip_config_t strip_config = {
.strip_gpio_num = LED_GPIO, // 数据输出引脚
.max_leds = LED_NUM, // 灯带总灯数
};
led_strip_rmt_config_t rmt_config = {
.resolution_hz = 10 * 1000 * 1000, // RMT 时钟分辨率 10MHz
};
这里配了两个结构体:
strip_config是灯带基本参数:数据从哪个引脚输出、一共有多少颗灯rmt_config是 RMT 驱动参数:.resolution_hz = 10MHz意思是 RMT 的时间精度是每格 100ns。WS2812 的时序要求是 0.4us 和 0.8us,刚好是 4 格和 8 格,这个分辨率刚好够精度
第二段:创建灯带实例
c
led_strip_new_rmt_device(&strip_config, &rmt_config, &strip);
这一行就把 RMT 驱动、时序编码全封装好了。你不用自己算 0.4us 还是 0.8us,组件内部都帮你算好了。
第三段:设置颜色 + 刷新
c
led_strip_set_pixel(strip, i, 255, 0, 0); // 第 i 颗灯,R=255, G=0, B=0
led_strip_refresh(strip); // 把数据发出去
注意是两步操作:
set_pixel只是把颜色写到内存缓冲区里,灯还不会变refresh才真正把数据通过 RMT 发出去,灯才会亮
和之前 PWM 的 set_duty + update_duty 是一个思路------先准备好,再一次性生效。这样做的好处是万一你设到一半被打断了,旧的输出还在,不会出现跳变。

3. 案例二:颜色渐变
最后来个好看点的------整排灯做彩虹渐变,颜色慢慢流动。
3.1 打开工程
使用课程提供的 WS2812_Rainbow 工程压缩包,解压到纯英文路径 下,用 VS Code 直接打开工程文件夹也可以。(WS2812_Rainbow项目下载)
3.2 代码解释
c
#define BRIGHTNESS 50 // 亮度百分比:0~100,这里是 50% 亮度
uint8_t hue = 0; // 色相值,0~255 循环
while (1) {
for (int i = 0; i < LED_NUM; i++) {
// 每颗灯的色相错开一点,形成渐变
uint8_t h = hue + i * (256 / LED_NUM);
uint8_t r, g, b;
// 简单的 HSV 转 RGB(近似版)
h = h % 256;
if (h < 85) {
r = 255 - h * 3; g = h * 3; b = 0;
} else if (h < 170) {
h -= 85;
r = 0; g = 255 - h * 3; b = h * 3;
} else {
h -= 170;
r = h * 3; g = 0; b = 255 - h * 3;
}
// 亮度调节:乘以亮度系数,范围 0~100%
r = r * BRIGHTNESS / 100;
g = g * BRIGHTNESS / 100;
b = b * BRIGHTNESS / 100;
led_strip_set_pixel(strip, i, r, g, b);
}
led_strip_refresh(strip);
hue += 2; // 色相慢慢增加,颜色就流动起来了
vTaskDelay(pdMS_TO_TICKS(30));
}
逐段解释:
1. 色相错开形成彩虹条
c
uint8_t h = hue + i * (256 / LED_NUM);
整排灯不是同一个颜色,而是每颗灯的色相都错开一段距离。6 颗灯的话,每颗错开 256/6 ≈ 42,这样就形成了一条从红到紫的彩虹条。
2. HSV 转 RGB(简化版)
c
if (h < 85) {
r = 255 - h * 3; g = h * 3; b = 0;
} else if (h < 170) {
h -= 85;
r = 0; g = 255 - h * 3; b = h * 3;
} else {
h -= 170;
r = h * 3; g = 0; b = 255 - h * 3;
}
这一段是把色相值转成 RGB 三个通道的亮度。原理是把色相环分成三段:
- 0~85:红 → 黄
- 85~170:黄 → 绿 → 青
- 170~255:青 → 蓝 → 紫 → 红
这个是简化版,精度一般,但做个彩虹流动效果完全够用。想要更精确的颜色,可以用标准的 HSV 转 RGB 算法。
3. 亮度调节
c
#define BRIGHTNESS 50 // 亮度百分比:0~100
r = r * BRIGHTNESS / 100;
g = g * BRIGHTNESS / 100;
b = b * BRIGHTNESS / 100;
WS2812 本身不支持直接调亮度,我们用软件方式:算出 RGB 值之后,整体乘以一个亮度系数。BRIGHTNESS 设成 50,就是所有颜色都减半------相当于亮度砍半。
这样做的好处是:
- 调亮度不用改颜色逻辑,改一个数字就行
- 亮度低一点,整体电流也小,灯带不会太烫
- 晚上用的时候不会晃眼
想调亮度就改 BRIGHTNESS 这个宏:100 是最亮,0 是全灭。
4. 颜色流动起来
c
hue += 2;
vTaskDelay(pdMS_TO_TICKS(30));
每 30ms 整体色相往前推 2,整条彩虹就像在缓缓流动。想调速度的话:
hue += 2里的数越大,流得越快vTaskDelay的延时越小,流得越快
下面这张流程图就是这段代码的执行顺序,方便你整体理解:


4. 注意事项
- WS2812相比较与之前的LED灯珠,功耗会大很多,如果灯带比较长或者灯珠数量比较多的情况下必须外接电源,外接电源的情况下注意GND需要共地;
- 由于灯珠功耗比较大的原因,在使用中模块的发热一般比其他模块高,实际应用中需要注意对其他温度敏感模块的影响:
- WS2812灯珠的亮度比较高,在使用中建议将亮度调到50%以下。