玩转-ESP32-S3-WS2812-RGB-彩灯

8 阅读11分钟

玩转 WS2812 RGB 彩灯

让你板子上的小灯从"亮/灭"进化到"七彩变色" 学会后可以控制任意数量的 WS2812 灯带/灯环


目录

  1. 什么是 WS2812?
  2. 判断你的灯是普通 LED 还是 WS2812
  3. 安装 LED 驱动组件
  4. 控制单颗 WS2812 彩灯
  5. 编写颜色轮播程序
  6. 颜色原理与调色技巧
  7. 常见问题

1. 什么是 WS2812?

WS2812 是一颗智能控制 LED,俗称"彩灯"、"Neopixel"。

1.1 它和普通 LED 有什么不同

普通 LED:          WS2812:
┌──────┐           ┌──────────┐
  🌕               🔴🟢🔵  
 亮/灭             1670万色 
 一个色             可单独控制 
 2根线             只需1根数据线 
└──────┘           └──────────┘
特性普通 LEDWS2812 RGB
颜色只有一种(红/绿/黄等)1670 万种颜色
控制一根 GPIO 线控制亮灭一根 GPIO 线发送颜色数据
电压1.8V~3.3V 随颜色5V(信号线 3.3V 兼容)
价格几分钱几毛钱到几块钱
级联不能可以首尾相连,一根线控几百颗

1.2 常见 WS2812 形态

单颗灯珠(板载)        灯带(5V)         灯环
┌────┐             ┌●●●●●●●●┐        ╭─────╮
│ WS │             │        │        │ ● ● │
│ 28 │             │ ●●●●●  │        │●   ●│
│ 12 │             └────────┘        │ ● ● │
└────┘                                ╰─────╯
你的板子上就是这种       DIY 神器        圆形灯板

1.3 你板子上的 WS2812

ESP32-S3-N8R2 的板载彩灯就是 WS2812,位于板子中间左侧,由 GPIO 48 控制。

              ESP32-S3-N8R2 板上的灯(正面)
          ┌────────────────────────────────────┐
          │  ┌──────────────────────────┐      │
          │  │   ESP32-S3-N8R2 芯片     │      │
          │  └──────────────────────────┘      │
          │  ┌─────────┐  ┌───┐ ┌───┐ ┌───┐  │
          │  │ WS2812  │  │🔴 │ │🟢 │ │🟡 │  │
          │  │ RGB彩灯  │  │PWR│ │TX │ │RX │  │  ← 中间右侧三个指示灯
          │  │ GPIO 48 │  └───┘ └───┘ └───┘  │
          │  └─────────┘                       │
          │  ┌──────────┐  ┌──────────┐       │
          │  │ USB-C ①  │  │ USB-C ②  │       │
          │  │ (标USB)  │  │ (标COM)  │       │
          │  │ 烧录用    │  │ 串口     │       │
          │  └──────────┘  └──────────┘       │
          └────────────────────────────────────┘
          │  └──────────┘  └──────────┘       │
          └────────────────────────────────────┘
                     👆 WS2812 在这里

⚠️ 中间右侧的 PWR(红灯) 是电源指示灯,通电就常亮,软件不可控。 ⚠️ 右下角的 TX/RX 灯 是串口活动指示灯,烧录和数据收发时会闪烁,也不可控。 ✅ 只有中间左侧的 WS2812 彩灯 才是可编程的。


2. 判断你的灯是普通 LED 还是 WS2812

这是新人最容易搞混的问题。用最简单的方法判断:

2.1 通电观察法

情况 A:上电后灯是暗的(几乎不亮) → 大概率是 WS2812。WS2812 上电默认不亮,需要发送数据才亮。

情况 B:上电后灯就亮了(红/黄/绿) → 大概率是普通 LED,硬件直连了电源。

2.2 代码测试法(最准确)

写一段代码:把 GPIO 设为高低电平,看灯的反应。

#include "driver/gp.h"

gpio_set_level(GPIO_NUM_48, 1);  // GPIO 48 高电平
vTaskDelay(2000 / portTICK_PERIOD_MS);
gpio_set_level(GPIO_NUM_48, 0);  // GPIO 48 低电平
现象结论
灯亮了(常亮)✅ 普通 LED,高电平亮
灯灭了✅ 普通 LED,低电平灭/亮
灯一闪一闪不稳定WS2812!GPIO 高低电平对它无效
灯完全没反应可能是 WS2812,也可能是 GPIO 不对

一个很容易搞混的地方: 用 GPIO 高低电平控制 WS2812 时,它可能

  • 发出奇怪的微光
  • 或者某个颜色常亮不灭
  • 或者不受控地闪烁

这是 WS2812 的典型特征。 因为它需要特殊的时序协议,不是简单的 0/1 能控制的。

2.3 看电路板

WS2812 灯珠外观:

  • 一般是 4 个引脚(VCC、GND、DIN、DOUT)
  • 灯珠上标着 "WS2812""SK6812"
  • 靠近时会看到内部有黑色的小芯片

普通 LED 外观:

  • 2 个引脚
  • 颜色就是它发光的颜色(红/黄/绿)

3. 安装 LED 驱动组件

控制 WS2812 需要专门的驱动库。在 ESP-IDF 里,这个库叫 led_strip

3.1 添加依赖

在项目 main 目录下创建 idf_component.yml

dependencies:
  espressif/led_strip: "^2.0"

⚠️ 踩坑提醒: 有时候 idf.py add-dependency 命令会因命令行太长失败, 手动创建这个 .yml 文件更稳。

3.2 更新 CMakeLists.txt

修改 main/CMakeLists.txt,加上 REQUIRES led_strip

idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       REQUIRES led_strip)

3.3 编译并自动下载依赖

idf.py build

第一次编译时,ESP-IDF 会自动从组件仓库下载 led_strip 库到项目目录下的 managed_components 文件夹。

如果下载失败(网络问题),多试几次,或更换 WiFi 环境。

3.4 新版 API 注意事项

led_strip v2.0+ 的 API 和旧版有区别:

函数旧版(v1.x)新版(v2.0+)
清空led_strip_clear(strip, 100)led_strip_clear(strip)
刷新led_strip_refresh(strip, 100)led_strip_refresh(strip)

新版的 clear() 和 refresh() 不再需要 timeout 参数。 如果你抄到的是旧版教程,编译会报 "too many arguments"。


4. 控制单颗 WS2812 彩灯

4.1 头文件和配置

#include "led_strip.h"

#define LED_GPIO        GPIO_NUM_48   // 数据线接的 GPIO

static led_strip_handle_t led_strip;   // LED 句柄(全局)

4.2 初始化 WS2812

void app_main(void)
{
    // 1. 配置结构体
    led_strip_config_t strip_config = {
        .strip_gpio_num = LED_GPIO,     // 数据线 GPIO
        .max_leds = 1,                  // 灯的数量(1 = 单颗)
    };
    
    // 2. RMT 配置(WS2812 需要精确时序,RMT 负责干这个)
    led_strip_rmt_config_t rmt_config = {
        .resolution_hz = 10 * 1000 * 1000,  // 10MHz 分辨率
        .flags.with_dma = false,             // 不用 DMA
    };
    
    // 3. 创建 LED 设备
    ESP_ERROR_CHECK(
        led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip)
    );
    
    // 4. 清空(灭灯)
    led_strip_clear(led_strip);
}

ESP_ERROR_CHECK 是什么? 宏:如果函数返回值不是 ESP_OK(=0),就打印错误并崩溃。 开发阶段这样最省心,出错了直接告诉你。

4.3 设置颜色

// 参数:led_strip, LED序号, R值, G值, B值
led_strip_set_pixel(led_strip, 0, 255, 0, 0);  // 第0颗灯,红色
led_strip_refresh(led_strip);                   // 刷新让颜色生效

颜色表:

// RGB 取值范围都是 0~255
led_strip_set_pixel(strip, 0, 255, 0, 0);      // 🔴 红
led_strip_set_pixel(strip, 0, 0, 255, 0);      // 🟢 绿
led_strip_set_pixel(strip, 0, 0, 0, 255);      // 🔵 蓝
led_strip_set_pixel(strip, 0, 255, 255, 0);    // 🟡 黄
led_strip_set_pixel(strip, 0, 255, 0, 255);    // 🟣 紫
led_strip_set_pixel(strip, 0, 0, 255, 255);    // 🩵 青
led_strip_set_pixel(strip, 0, 255, 255, 255);  // ⚪ 白
led_strip_set_pixel(strip, 0, 0, 0, 0);        // ⬛ 关灯

4.4 完整的点灯测试代码

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "led_strip.h"
#include "esp_heap_caps.h"   // 看内存用

#define LED_GPIO  GPIO_NUM_48

static led_strip_handle_t led_strip;

void app_main(void)
{
    printf("\n===== WS2812 RGB LED 测试 =====\n");

    // 配置 WS2812
    led_strip_config_t strip_config = {
        .strip_gpio_num = LED_GPIO,
        .max_leds = 1,
    };
    led_strip_rmt_config_t rmt_config = {
        .resolution_hz = 10 * 1000 * 1000,
        .flags.with_dma = false,
    };
    ESP_ERROR_CHECK(
        led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip)
    );

    led_strip_clear(led_strip);

    // 逐个颜色测试,每 2 秒切换
    printf("🔴 红色  2s\n");
    led_strip_set_pixel(led_strip, 0, 255, 0, 0);
    led_strip_refresh(led_strip);
    vTaskDelay(pdMS_TO_TICKS(2000));

    printf("🟢 绿色  2s\n");
    led_strip_set_pixel(led_strip, 0, 0, 255, 0);
    led_strip_refresh(led_strip);
    vTaskDelay(pdMS_TO_TICKS(2000));

    printf("🔵 蓝色  2s\n");
    led_strip_set_pixel(led_strip, 0, 0, 0, 255);
    led_strip_refresh(led_strip);
    vTaskDelay(pdMS_TO_TICKS(2000));

    printf("🟡 黄色  2s\n");
    led_strip_set_pixel(led_strip, 0, 255, 255, 0);
    led_strip_refresh(led_strip);
    vTaskDelay(pdMS_TO_TICKS(2000));

    printf("🟣 紫色  2s\n");
    led_strip_set_pixel(led_strip, 0, 255, 0, 255);
    led_strip_refresh(led_strip);
    vTaskDelay(pdMS_TO_TICKS(2000));

    printf("🩵 青色  2s\n");
    led_strip_set_pixel(led_strip, 0, 0, 255, 255);
    led_strip_refresh(led_strip);
    vTaskDelay(pdMS_TO_TICKS(2000));

    printf("⚪ 白色  2s\n");
    led_strip_set_pixel(led_strip, 0, 255, 255, 255);
    led_strip_refresh(led_strip);
    vTaskDelay(pdMS_TO_TICKS(2000));

    // 关灯
    led_strip_clear(led_strip);
    printf("\n✅ 测试完成!\n");

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

5. 编写颜色轮播程序

有了基础的控制能力,来写一个好看的颜色循环效果。

5.1 定义颜色表

typedef struct {
    uint8_t r, g, b;
    const char *name;
} color_t;

static const color_t colors[] = {
    {255,   0,   0, "🔴 红"},
    {255, 128,   0, "🟠 橙"},
    {255, 255,   0, "🟡 黄"},
    {  0, 255,   0, "🟢 绿"},
    {  0, 255, 255, "🩵 青"},
    {  0, 128, 255, "🔵 蓝"},
    {138,  43, 226, "🟣 紫"},
    {255, 255, 255, "⚪ 白"},
};

#define COLOR_COUNT  (sizeof(colors) / sizeof(colors[0]))

sizeof(colors) / sizeof(colors[0]) 是 C 语言里求数组元素个数的常用写法。 这样加颜色不需要改其他地方。

5.2 轮播逻辑

uint32_t cnt = 0;
while (1) {
    for (int i = 0; i < COLOR_COUNT; i++) {
        led_strip_set_pixel(led_strip, 0,
                            colors[i].r,
                            colors[i].g,
                            colors[i].b);
        led_strip_refresh(led_strip);
        
        printf("[%lu] %s\n", (unsigned long)++cnt, colors[i].name);
        
        vTaskDelay(pdMS_TO_TICKS(500));  // 每 0.5 秒切一次
    }
}

5.3 完整代码

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "led_strip.h"

#define LED_GPIO  GPIO_NUM_48
static led_strip_handle_t led_strip;

typedef struct { uint8_t r, g, b; const char *name; } color_t;

static const color_t colors[] = {
    {255,   0,   0, "🔴 红"},
    {255, 128,   0, "🟠 橙"},
    {255, 255,   0, "🟡 黄"},
    {  0, 255,   0, "🟢 绿"},
    {  0, 255, 255, "🩵 青"},
    {  0, 128, 255, "🔵 蓝"},
    {138,  43, 226, "🟣 紫"},
    {255, 255, 255, "⚪ 白"},
};
#define COLOR_COUNT (sizeof(colors)/sizeof(colors[0]))

void app_main(void)
{
    printf("\n===== WS2812 颜色轮播 =====\n");

    led_strip_config_t strip_config = {
        .strip_gpio_num = LED_GPIO,
        .max_leds = 1,
    };
    led_strip_rmt_config_t rmt_config = {
        .resolution_hz = 10 * 1000 * 1000,
        .flags.with_dma = false,
    };
    ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip));
    led_strip_clear(led_strip);

    uint32_t cnt = 0;
    while (1) {
        for (int i = 0; i < COLOR_COUNT; i++) {
            led_strip_set_pixel(led_strip, 0, colors[i].r, colors[i].g, colors[i].b);
            led_strip_refresh(led_strip);
            printf("[%lu] %s\n", (unsigned long)++cnt, colors[i].name);
            vTaskDelay(pdMS_TO_TICKS(500));
        }
    }
}

编译烧录后,你会看到灯以 0.5 秒切换一次颜色,循环 8 种颜色。


6. 颜色原理与调色技巧

6.1 RGB 颜色原理

WS2812 内部有三颗微型 LED:R(红)、G(绿)、B(蓝)

每个颜色用一个 0~255 的数字表示亮度:
0   = 全灭
128 = 半亮
255 = 最亮

三种颜色组合,产生 256×256×256 = 1670 万种颜色

6.2 常见调色

// ==== 单色 ====
(R=255, G=0,   B=0  )  →  最亮红色
(R=128, G=0,   B=0  )  →  暗红色(降低亮度)

// ==== 混色 ====
(R=255, G=255, B=0  )  →  黄色  (红+绿)
(R=255, G=0,   B=255)  →  紫色  (红+蓝)
(R=0,   G=255, B=255)  →  青色  (绿+蓝)
(R=255, G=255, B=255)  →  白色  (红+绿+蓝)

// ==== 暖色系 ====
(R=255, G=100, B=0  )  →  橙色
(R=255, G=60,  B=0  )  →  暖黄
(R=255, G=200, B=100)  →  暖白

// ==== 冷色系 ====
(R=0,   G=100, B=255)  →  天蓝
(R=50,  G=150, B=255)  →  海蓝
(R=200, G=220, B=255)  →  冷白

6.3 亮度控制技巧

方式一:整体降低 RGB 值

// 红色满亮度:255, 0, 0 → 很亮
// 红色半亮度:128, 0, 0 → 柔和
// 红色微光:  10,  0, 0 → 仅仅能看见

方式二:Gamma 校正(看起来更自然)

人眼对亮度的感知不是线性的。如果想让亮度"看起来"均匀:

// 线性值 → Gamma 校正值
// 亮度 25%  → 实际写 64
// 亮度 50%  → 实际写 128
// 亮度 75%  → 实际写 191
// 亮度 100% → 实际写 255

// Gamma 校正函数
uint8_t gamma_correct(uint8_t linear) {
    // 简单的平方Gamma
    float normalized = linear / 255.0f;
    float gamma = normalized * normalized;  // Gamma 2.0
    return (uint8_t)(gamma * 255.0f);
}

6.4 从 0 到 255 渐变

// 红色从暗到亮渐变
for (int i = 0; i <= 255; i++) {
    led_strip_set_pixel(led_strip, 0, i, 0, 0);
    led_strip_refresh(led_strip);
    vTaskDelay(pdMS_TO_TICKS(10));  // 每级 10ms,2.5 秒完成
}

7. 常见问题

7.1 编译报 "led_strip: unknown name"

原因:main/CMakeLists.txt 没加 REQUIRES led_strip

解决:

idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       REQUIRES led_strip)     ← 加上这一行

7.2 编译报 "too many arguments"

原因:你用的 led_strip 是 v2.0+,函数签名变了

解决:

// 旧版(v1.x)
led_strip_clear(strip, 100);
led_strip_refresh(strip, 100);

// 新版(v2.0+)不要 timeout 参数
led_strip_clear(strip);
led_strip_refresh(strip);

7.3 灯完全不亮

排查步骤:

1. 检查 GPIO 对不对 → 有的板子在 GPIO 2/21/48
2. 检查 led_strip 初始化是否成功 → 有没有 ESP_ERROR_CHECK
3. 检查是不是普通 LED(不是 WS2812)→ 用 GPIO 高/低电平试试
4. 检查代码有没有走到设置颜色的地方 → 串口打印确认

7.4 灯的颜色不对

原因:RGB 顺序问题

有些 WS2812 的 RGB 顺序是 GRB(绿红蓝)而不是 RGB(红绿蓝)。

// 如果正常顺序颜色不对,试试交换 R 和 G
led_strip_set_pixel(strip, 0, G, R, B);  // 交换 R 和 G

7.5 接外部 WS2812 灯带不亮

外部 WS2812 灯带需要 5V 供电!
ESP32 的 3.3V 带不动!

正确接线:
ESP32 GPIO ───── WS2812 DIN
ESP32 GND  ───── WS2812 GND
外部 5V    ───── WS2812 VCC(不要用 ESP32 的 3.3V)

进阶方向

学会了单颗 WS2812,接下来可以:

  1. 级联多颗:设置 max_leds = 30,分别控制每颗灯珠
  2. 制作彩虹效果:用 HSL 颜色模型,H 值循环
  3. 音乐可视化:配合麦克风,灯随音乐跳动
  4. 智能灯带:加 WiFi,做手机控制的 RGB 灯带

下一篇:《ESP32 变身 WiFi 智能灯控制器》 → 在浏览器里控制你的彩灯 🌐