ESP32-S3 控制 16 颗 WS2812B 灯环:按键切换、亮度调节与断电记忆

最近我用 ESP32-S3 做了一个 16 颗 WS2812B 灯珠组成的灯环控制器。

最开始的目标只是把灯环点亮,后面逐步加入了按键切换、亮度调节、11 种灯效和断电记忆。项目规模不大,但包含了 GPIO 输入、单总线灯珠控制、按键状态判断、参数持久化和供电稳定处理,很适合作为一个完整的 ESP32-S3 入门项目。

本文记录硬件连接、程序结构和开发过程中遇到的问题。

一、最终实现的功能

  • ESP32-S3 控制 16 颗 WS2812B 灯环;
  • 短按一个按键切换灯效;
  • 长按同一个按键切换亮度;
  • 亮度分为 30、60、100、160、255 五档;
  • 使用 Preferences 将灯效和亮度保存到 NVS;
  • 断电后重新上电,恢复上一次灯效和亮度;
  • 支持 11 种灯效;
  • ESP32-S3 与灯环共用外部 5V 电源。

二、硬件清单

硬件说明
ESP32-S3 开发板主控制器
WS2812B 16 灯珠灯环5V 可编程 RGB 灯珠
5V 电源本项目使用 5V 5A,给开发板和灯环供电
330Ω 电阻串联在数据线上,抑制边沿振铃和瞬态冲击
22μF/10V 电容并联在灯环 5V 与 GND 之间
独立按键短按切灯效,长按切亮度
杜邦线原型连接

三、接线方式

核心连接如下:

ESP32-S3 GPIO4  -> 330Ω 电阻 -> WS2812B DIN
ESP32-S3 GPIO13 -> 按键一端
ESP32-S3 GND    -> 按键另一端

5V 电源正极 -> WS2812B 5V
5V 电源正极 -> ESP32-S3 5V/VIN
5V 电源负极 -> WS2812B GND
5V 电源负极 -> ESP32-S3 GND

22μF 电容并联在灯环输入端的 5V 和 GND 之间。电解电容有极性,正极接 5V,负极接 GND。

这里有两个必须注意的地方:

  1. ESP32-S3 与 WS2812B 必须共地,否则数据线没有统一的电平参考;
  2. 外部 5V 只能接开发板的 5V/VIN,不能接 3V3 引脚。

四、为什么数据线上串联 330Ω 电阻

ESP32-S3 输出的数字信号边沿很快。数据线、杜邦线和灯板输入端共同构成一个非理想传输网络,边沿处可能产生振铃、反射和瞬态电流。

在 GPIO 与 WS2812B DIN 之间串联 330Ω 电阻,可以适当降低边沿速度并限制瞬态电流。它不会把正常数字信号简单地“分压成固定电压”,因为 WS2812B 的数据输入端是高阻负载。

电阻应尽量靠近第一颗灯珠的 DIN 输入端放置。

五、为什么在灯环输入端增加电容

WS2812B 在亮度和颜色快速变化时,瞬时电流也会变化。如果电源线较长、线径偏细或者接触不稳定,灯环输入端可能出现电压波动,表现为随机闪烁、颜色异常或者灯效抖动。

本项目在灯环电源输入端并联了 22μF/10V 电容,用于缓冲局部电压变化。实际使用中,加入电容后灯效明显稳定。

需要注意:电容不能代替合格的电源和可靠的共地连接。灯珠数量更多、线更长时,仍需要重新评估电源容量、线径和多点供电。

六、使用 Adafruit_NeoPixel 控制灯环

Arduino 环境中可以直接使用 Adafruit_NeoPixel 库。它帮助我们完成 WS2812B 所需的精确时序输出,不需要在业务代码里手动翻转 GPIO。

核心初始化形式如下:

#include <Adafruit_NeoPixel.h>

constexpr uint8_t LED_PIN = 4;
constexpr uint16_t LED_COUNT = 16;

Adafruit_NeoPixel pixels(
    LED_COUNT,
    LED_PIN,
    NEO_GRB + NEO_KHZ800
);

void setup() {
    pixels.begin();
    pixels.clear();
    pixels.setBrightness(100);
    pixels.show();
}

NEO_GRB 表示灯珠按 GRB 顺序解释三个颜色通道,NEO_KHZ800 表示使用常见的 800kHz WS2812B 时序。

七、一个按键实现短按和长按

按键使用 INPUT_PULLUP:未按下时读取 HIGH,按下后与 GND 接通,读取 LOW。

程序需要记录按下时刻和释放时刻:

constexpr uint8_t BUTTON_PIN = 13;
constexpr uint32_t LONG_PRESS_MS = 800;

uint32_t pressStartMs = 0;
bool buttonWasDown = false;

void updateButton() {
    bool buttonDown = digitalRead(BUTTON_PIN) == LOW;

    if (buttonDown && !buttonWasDown) {
        pressStartMs = millis();
    }

    if (!buttonDown && buttonWasDown) {
        uint32_t heldMs = millis() - pressStartMs;

        if (heldMs >= LONG_PRESS_MS) {
            nextBrightness();
        } else {
            nextEffect();
        }
    }

    buttonWasDown = buttonDown;
}

实际工程还需要加入 20~50ms 左右的软件消抖,防止一次机械按键动作被识别成多次点击。

八、五档亮度设计

亮度采用固定档位,而不是连续调节:

const uint8_t brightnessTable[] = {30, 60, 100, 160, 255};

长按按键后,亮度索引循环加一:

brightnessIndex = (brightnessIndex + 1) % 5;
pixels.setBrightness(brightnessTable[brightnessIndex]);
pixels.show();

固定档位的好处是:不需要额外屏幕和电位器,单按键也能快速选择常用亮度。

九、使用 Preferences 保存灯效和亮度

ESP32 Arduino 框架提供了 Preferences 类,可以将少量配置保存到芯片的 NVS 非易失性存储中。

#include <Preferences.h>

Preferences preferences;

void loadSettings() {
    preferences.begin("ws2812", false);

    currentMode = preferences.getUChar("mode", MODE_RAINBOW);
    brightnessIndex = preferences.getUChar("brightness", 2);

    if (currentMode >= MODE_COUNT) {
        currentMode = MODE_RAINBOW;
    }

    if (brightnessIndex >= 5) {
        brightnessIndex = 2;
    }
}

MODE_RAINBOW 是键不存在时使用的默认值。第一次运行或 NVS 中尚未保存 mode 时,程序就从彩虹模式开始。

切换灯效和亮度后保存:

preferences.putUChar("mode", currentMode);
preferences.putUChar("brightness", brightnessIndex);

不要在每一帧动画中写 NVS。Flash 擦写次数有限,只在用户确实改变配置后写入即可。

十、当前 11 种灯效

项目中包含以下模式:

  1. Rainbow:彩虹流动;
  2. Breath:呼吸灯;
  3. Color Wipe:逐颗点亮;
  4. Comet:彗星拖尾;
  5. Theater:剧院追逐;
  6. Sparkle:随机闪烁;
  7. Solid:纯色切换;
  8. Ring Chase:双点环形追逐;
  9. Scanner:往返扫描;
  10. Color Wave:彩色波纹;
  11. Fireworks:烟花闪烁。

灯带和灯环的物理结构不同。线性灯带适合单向流动,灯环没有明显起点和终点,更适合旋转、追逐和周期换色。因此部分效果针对 16 颗环形结构做了调整,例如彗星转若干圈后换色、双点相向追逐等。

十一、开发过程中遇到的问题

1. 开发板型号选错

普通 ESP32 与 ESP32-S3 是不同芯片。Arduino IDE 中,普通 ESP32 一般选择 ESP32 Dev Module,ESP32-S3 则选择 ESP32S3 Dev Module。选错后可能正常编译,但上传时提示芯片型号不匹配。

2. 只关注程序,忽略供电

WS2812B 是 5V 灯珠。灯珠数量增加或亮度升高后,电源能力、线材压降和共地质量都会直接影响稳定性。软件排查之前,应先确认灯环端的实际电压。

3. 灯效速度不适合灯环

同样的延时参数放在不同数量和不同形状的灯珠上,视觉效果会不同。跑马灯过快会只剩闪烁感,彗星拖尾过短又看不出层次。动画参数需要结合真实结构调节。

十二、后续可以怎样扩展

  • 增加 Wi-Fi 网页控制;
  • 增加手机调色页面;
  • 接入麦克风做音乐律动;
  • 增加红外遥控;
  • 增加 OLED 显示当前模式;
  • 支持多路灯带输出;
  • 重新设计 PCB,整合电源、接口和按键。

总结

这个项目真正有价值的地方,不只是点亮 16 颗灯珠,而是把多个常见的嵌入式模块串成了一条完整链路:

按键输入 -> 状态判断 -> 灯效输出 -> 亮度控制 -> 参数保存 -> 断电恢复

项目硬件简单、反馈直观,又能自然引出供电、时序、状态机 等知识,很适合作为 ESP32-S3 的入门实践。


作者:白搞电子

原始项目记录:我用 ESP32-S3 做了一个 16 颗灯珠的 WS2812 灯环控制器

更多推荐