ESP32 2.8寸黄屏 · 触屏控制256颗LED矩阵
16x16共256颗WS2812/NeoPixel兼容可寻址RGB LED矩阵,用ESP32黄屏(CYD)做触屏控制台: 左边手绘网格实时同步点亮灯板,右边模式切换、亮度滑条、8色快选。实机排查连续挖出并修好 5个真实bug,全部验证通过。
ESP32-2432S028R (CYD) WS2812/NeoPixel x256 Adafruit_NeoPixel TFT_eSPI 网页烧录已实测 触屏画图16x16共256颗WS2812/NeoPixel兼容可寻址RGB LED矩阵,用ESP32黄屏(CYD)做触屏控制台: 左边手绘网格实时同步点亮灯板,右边模式切换、亮度滑条、8色快选。实机排查连续挖出并修好 5个真实bug,全部验证通过。
ESP32-2432S028R (CYD) WS2812/NeoPixel x256 Adafruit_NeoPixel TFT_eSPI 网页烧录已实测 触屏画图这块灯板是16x16排布、总共256颗独立可寻址RGB LED,每颗灯内置驱动芯片,只用一根数据线
就能串行控制全部256颗灯各自的颜色——协议和时序跟WS2812/WS2812B兼容
(Adafruit_NeoPixel库的NEO_GRB + NEO_KHZ800模式)。
本项目用ESP32 2.8寸黄屏(俗称CYD,ESP32-2432S028)当主控兼触屏控制台:屏幕上画一个 16x16的网格对应灯板的256颗灯,点触屏幕选颜色、画图案,灯板实时同步点亮;另外还内置了 10种自动灯效(彩虹、火焰、脉冲、等离子流等)和2个排查用的诊断模式。
| 参数 | 数值 / 说明 |
|---|---|
| 灯珠封装 | 2020(2.0×2.0mm贴片) |
| 灯珠数量/排布 | 256颗,16x16矩阵,方形4孔耳固定板 |
| 协议兼容 | WS2812/WS2812B兼容(单线归零码,NEO_GRB,800kHz) |
| 灯板尺寸 | 63.5mm × 50.8mm,约10g |
| 数据引脚 | GPIO27(经330Ω限流电阻接DIN) |
| 供电 | 5V独立供电(不与ESP32共用USB口的5V轨),GND共地 |
| 排布方向 | Z字形,逐行从左到右(非蛇形serpentine) |
| 主控板 | ESP32 2.8寸黄屏(CYD,ESP32-2432S028),SKU 01564 |
| 屏幕真实驱动芯片 | ST7789(而非常见文档标注的ILI9341) |
| 信号 | 接法 |
|---|---|
| DIN | 数据输入,接ESP32 GPIO27,中间串一个330Ω/0.25W限流电阻 |
| GND | 地,必须和ESP32共地(即使灯板用独立5V供电,GND也要连一起) |
| 5V | 灯板供电,不要接ESP32板载USB口的5V轨 |
⚠ 供电安全是这块灯板最容易翻车的地方
代码把亮度上限锁死在 MAX_BRIGHTNESS=20(满量程255的约8%),这是故意设的
安全上限,不是bug——256颗灯就算亮度很低,画面里同时点亮大片白色/亮色时瞬时电流仍然可观。
测试/演示时不要为了效果好看把这个上限改高,除非确认改用了足够大的独立5V电源。
曾经的排查弯路:早期怀疑过"USB供电不足→数据时序被冲乱→颜色乱套",特意换了独立5V电源 测试,乱色依旧存在——最终定位到真正原因跟供电无关(见下方"踩坑实录"),供电只是本来就该 遵守的安全底线,不是这次乱色的病根。
点屏幕右上角"EFFECT PATTERN"按钮循环切换模式,0~12共13个:
| 模式 | 名称 | 内容 |
|---|---|---|
0 | DRAW MODE | 手绘模式:点左边16x16网格用当前选中颜色点亮对应LED,配CLEAR/DRAW按钮 |
1 | Rainbow Wave | 彩虹波浪,色相随时间平移 |
2 | Fire Simulation | 火焰模拟,红橙色随机跳动 |
3 | Cyberpunk Pulse | 品红色呼吸脉冲 |
4 | Plasma Flow | 正弦+余弦叠加的等离子流动 |
5 | Neon Rain | 霓虹雨,绿色光点从顶部往下掉 |
6 | Strobe Party | 用当前选中颜色做频闪 |
7 | Color Swirl Center | 彩色漩涡 |
8 | Random Sparkles | 随机星光闪烁 |
9 | Larson Scanner | 红色扫描光(霹雳游侠车灯风格) |
10 | Ocean Waves | 蓝绿色海浪效果 |
11 | COLOR TEST | 排查用:全屏纯红/绿/蓝循环,验证颜色通道顺序 |
12 | ORDER TEST | 排查用:单颗白点按0~255顺序走一遍,验证灯珠排布方向 |
其它固定UI:亮度滑条(BRIGHTNESS,1~20范围内可调)、8色快选色板(MANUAL COLOR SWATCH,
影响手绘和第6号频闪模式的颜色)、当前选色文字读数(SEL: 颜色名)。
这块代码是现成拿到的草图,不是从零写的,但实机跑起来之后暴露了好几个只有真机测试才能 发现的问题:
| # | 现象 | 真正原因 | 怎么修好的 |
|---|---|---|---|
| ① | 手绘图案跟LED颜色完全对不上,一片乱色 | 手绘模式下主循环不管画面有没有变化,每次空转都把256颗灯的数据重发一遍,高频轰炸式发送把时序线打断了 | 加标志位,只在画面真的改动时才刷新一次 |
| ② | 点第1个色块,实际选中了第2个 | 触摸坐标换算固定偏移了一整格 | 坐标换算里补一个修正量 |
| ③ | 修完①之后,拖亮度条LED没反应 | 加节流后,单纯调亮度不会触发刷新 | 亮度变化时额外强制刷新一次 |
| ④ | 屏幕上画的颜色跟预期完全不对(比如选红显示青/黄) | 用错了屏幕驱动型号——这块CYD板常被当ILI9341卖,实际控制芯片是ST7789 | 换成正确的驱动型号,颜色错乱从"毫无规律"变成了"红蓝干净对调" |
| ⑤ | 驱动型号对了,红蓝还是对调 | 这块廉价面板没有真正实现"调色序"这个硬件开关,软件设置改不了 | 不跟硬件较劲,直接在代码里把红蓝两个颜色的数值对调 |
下面这两段是从 CYD_16x16_256LED.ino 真实源码里摘出来的,对应上表①和⑤两条,
方便直接照着抄或者对照自己的项目排查同类问题。
void loop() {
handleTouchInputs();
if (currentPattern > 0) {
if (millis() - lastEffectUpdate > effectDelay) {
runPatternEngine(currentPattern);
lastEffectUpdate = millis();
}
} else if (gridDirty) { // 只有画面真的改动了才刷新
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
int logicalIndex = y * MATRIX_WIDTH + x;
matrix.setPixelColor(XY(x, y), paletteColors[manualGrid[logicalIndex]]);
}
}
matrix.show();
gridDirty = false;
}
}
// 这块面板的红蓝通道在硬件层面对调,MADCTL寄存器的BGR/RGB位不起作用,
// 直接对调这四个常量的数值。GREEN/MAGENTA/WHITE/BLACK/DARKGREY的红蓝
// 分量相等或都是0,对调后数值不变,不用改。
#undef TFT_RED
#define TFT_RED 0x001F
#undef TFT_BLUE
#define TFT_BLUE 0xF800
#undef TFT_YELLOW
#define TFT_YELLOW 0x07FF
#undef TFT_CYAN
#define TFT_CYAN 0xFFE0
最终验证:五个bug全部修完后,实测确认选色、画图、亮度调节、 灯珠排布方向(手绘复杂图案,LED点亮形状跟屏幕完全一致)、屏幕自身颜色显示,全部正常。
正面:16x16灯珠矩阵本体
背面:接线全景(DIN经电阻接GPIO27,5V/GND独立供电)
背面丝印特写:Dot Matrix 16x16 256颗 RGB LED、CN1/DIN接口标注
操作演示、13个模式实机效果和完整排查过程的视频正在制作中,发布后在这里补链接。
不想装 Arduino IDE 的话,直接用乐鑫官方 ESP Launchpad 网页烧录(仅支持电脑端 Chrome / Edge):
🔥 打开 ESP Launchpad 一键烧录(乐鑫官方) 备选:本站自制版
完整的 CYD_16x16_256LED.ino(含13个模式的全部实现、触摸坐标换算、UI绘制)可以
直接下载,用 Arduino IDE 打开就能编译烧录:
⬇ 下载 .ino 源文件(约16KB) ⬇ 下载完整源码包(含2个诊断程序+说明)
依赖库:Adafruit_NeoPixel、TFT_eSPI、XPT2046_Touchscreen。TFT_eSPI 需要按下方接线表
改全局 User_Setup.h(驱动选 ST7789_DRIVER),下载包里的 README 有完整配置。
#include <SPI.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
#include <Adafruit_NeoPixel.h>
// 这块CYD面板的R/B通道硬件级对调,MADCTL的BGR/RGB寄存器位改不动它,
// 直接对调下面四个颜色常量的数值绕开。GREEN/MAGENTA/WHITE/BLACK/DARKGREY
// 红蓝分量相等或都是0,对调后数值不变,不用改。这个覆盖只影响屏幕预览,
// NeoPixel灯珠颜色不受影响(Adafruit_NeoPixel不用这些宏)。
#undef TFT_RED
#define TFT_RED 0x001F
#undef TFT_BLUE
#define TFT_BLUE 0xF800
#undef TFT_YELLOW
#define TFT_YELLOW 0x07FF
#undef TFT_CYAN
#define TFT_CYAN 0xFFE0
// --- 触摸引脚 (CYD固定引脚) ---
#define XPT2046_IRQ 36
#define XPT2046_MOSI 32
#define XPT2046_MISO 39
#define XPT2046_CLK 25
#define XPT2046_CS 33
// --- 16x16 / 256颗LED 硬件配置 ---
#define LED_PIN 27
#define MATRIX_WIDTH 16
#define MATRIX_HEIGHT 16
#define NUM_LEDS (MATRIX_WIDTH * MATRIX_HEIGHT)
#define MATRIX_SERPENTINE false // false=Z字形逐行,true=蛇形
#define MAX_BRIGHTNESS 20 // USB供电测试安全上限,别调高除非换独立5V电源
Adafruit_NeoPixel matrix(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
TFT_eSPI tft = TFT_eSPI();
SPIClass touchSPI = SPIClass(VSPI);
XPT2046_Touchscreen ts(XPT2046_CS, XPT2046_IRQ);
uint8_t currentPattern = 0;
uint8_t globalBrightness = 10;
uint32_t selectedColor;
uint8_t manualGrid[NUM_LEDS];
bool gridDirty = true; // 只在画面真的改动时才重发一次
uint8_t effectHue = 0;
unsigned long lastEffectUpdate = 0;
int effectDelay = 30;
const int gridStartX = 10, gridStartY = 40, cellSize = 11;
uint32_t paletteColors[8];
uint16_t paletteTFT[] = {TFT_RED, TFT_GREEN, TFT_BLUE, TFT_YELLOW, TFT_CYAN, TFT_MAGENTA, TFT_WHITE, TFT_DARKGREY};
const char* paletteNames[] = {"RED", "GREEN", "BLUE", "YELLOW", "CYAN", "MAGENTA", "WHITE", "DARKGREY"};
uint8_t selectedPaletteIdx = 0;
void setup() {
Serial.begin(115200);
pinMode(21, OUTPUT); digitalWrite(21, HIGH);
paletteColors[0] = matrix.Color(255, 0, 0);
paletteColors[1] = matrix.Color(0, 255, 0);
paletteColors[2] = matrix.Color(0, 0, 255);
paletteColors[3] = matrix.Color(255, 255, 0);
paletteColors[4] = matrix.Color(0, 255, 255);
paletteColors[5] = matrix.Color(255, 0, 255);
paletteColors[6] = matrix.Color(255, 255, 255);
paletteColors[7] = matrix.Color(0, 0, 0);
selectedColor = paletteColors[0];
tft.init();
tft.setRotation(1);
tft.fillScreen(0x10A2);
touchSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS);
ts.begin(touchSPI);
ts.setRotation(1);
matrix.begin();
matrix.setBrightness(globalBrightness);
matrix.clear();
matrix.show();
memset(manualGrid, 7, sizeof(manualGrid));
buildInterfaceFrame();
drawInteractiveGrid();
}
void loop() {
handleTouchInputs();
if (currentPattern > 0) {
if (millis() - lastEffectUpdate > effectDelay) {
runPatternEngine(currentPattern);
lastEffectUpdate = millis();
}
} else if (gridDirty) {
for (int y = 0; y < MATRIX_HEIGHT; y++) {
for (int x = 0; x < MATRIX_WIDTH; x++) {
int logicalIndex = y * MATRIX_WIDTH + x;
matrix.setPixelColor(XY(x, y), paletteColors[manualGrid[logicalIndex]]);
}
}
matrix.show();
gridDirty = false;
}
}
// 触摸坐标换算:色板点击处补了(x-200)/14-1的修正,实测确认原始换算固定
// 偏移一整格(点第0格实际选中第1格)
void handleTouchInputs() {
if (!ts.touched()) return;
TS_Point p = ts.getPoint();
int x = map(p.x, 200, 3700, 0, 320);
int y = map(p.y, 240, 3800, 0, 240);
if (x >= gridStartX && x < (gridStartX + (MATRIX_WIDTH * cellSize)) &&
y >= gridStartY && y < (gridStartY + (MATRIX_HEIGHT * cellSize))) {
int cellX = (x - gridStartX) / cellSize;
int cellY = (y - gridStartY) / cellSize;
int idx = cellY * MATRIX_WIDTH + cellX;
if (currentPattern == 0 && idx >= 0 && idx < NUM_LEDS) {
manualGrid[idx] = selectedPaletteIdx;
gridDirty = true;
int px = gridStartX + (cellX * cellSize);
int py = gridStartY + (cellY * cellSize);
tft.fillRect(px, py, cellSize - 1, cellSize - 1, paletteTFT[selectedPaletteIdx]);
tft.drawRect(px, py, cellSize - 1, cellSize - 1, TFT_DARKGREY);
}
delay(30);
}
if (x >= 200 && x <= 310 && y >= 50 && y <= 88) { // 模式切换按钮
currentPattern++;
if (currentPattern > 12) currentPattern = 0;
drawPatternButton();
matrix.clear();
matrix.show();
delay(250);
}
if (x >= 200 && x <= 310 && y >= 110 && y <= 135) { // 亮度滑条
int rawVal = map(x, 200, 310, 1, MAX_BRIGHTNESS);
globalBrightness = constrain(rawVal, 1, MAX_BRIGHTNESS);
matrix.setBrightness(globalBrightness);
gridDirty = true; // 强制刷新一次,不然画图模式下调亮度看不到变化
updateBrightnessSlider();
delay(30);
}
if (x >= 200 && x <= 312 && y >= 155 && y <= 180) { // 色板选择
int clickIdx = (x - 200) / 14 - 1; // 修正实测偏移的一整格
if (clickIdx >= 0 && clickIdx < 8) {
selectedPaletteIdx = clickIdx;
selectedColor = paletteColors[selectedPaletteIdx];
buildInterfaceFrame();
}
delay(150);
}
if (y >= 195 && y <= 230) {
if (x >= 200 && x <= 252) { // CLEAR
memset(manualGrid, 7, sizeof(manualGrid));
drawInteractiveGrid();
matrix.clear();
matrix.show();
gridDirty = false;
delay(200);
} else if (x >= 258 && x <= 310) { // DRAW MODE
currentPattern = 0;
gridDirty = true;
drawPatternButton();
drawInteractiveGrid();
delay(200);
}
}
}
// 屏幕/触屏坐标 -> 物理灯号
uint16_t XY(uint8_t x, uint8_t y) {
if (MATRIX_SERPENTINE && (y & 1)) x = MATRIX_WIDTH - 1 - x;
return (uint16_t)y * MATRIX_WIDTH + x;
}
// 0~255 输入映射彩虹色环,r-g-b循环过渡
uint32_t Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if (WheelPos < 85) return matrix.Color(255 - WheelPos * 3, 0, WheelPos * 3);
if (WheelPos < 170) { WheelPos -= 85; return matrix.Color(0, WheelPos * 3, 255 - WheelPos * 3); }
WheelPos -= 170;
return matrix.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
// 10种自动灯效 + 2种诊断模式的具体实现(Rainbow/Fire/Pulse/Plasma/Rain/
// Strobe/Swirl/Sparkles/Larson/Ocean + COLOR TEST/ORDER TEST)见完整下载包,
// 篇幅太长这里不全贴,逻辑都在 runPatternEngine() 一个函数里,按 patternId
// 分支处理,跟上面 loop() 的调用方式一致。
主控板商品图来自明武电子自家店铺,含USB数据线、触摸笔、4P连接线,PCB板86×50mm,整套约63.5g。
| 现象 | 优先检查 |
|---|---|
| 手绘颜色跟LED对不上,无规律花色 | 先看是不是手绘模式下matrix.show()被高频调用(见上方①),不是供电问题 |
| 点色块选错了颜色 | 看屏幕"SEL: 文字"读数是否跟点击位置对应,若固定偏移一格是触摸坐标换算问题(②) |
| 调亮度LED没反应 | 确认亮度改变有没有强制触发一次matrix.show()(③) |
| 屏幕上颜色跟预期不一致(比如选红显示蓝/青/黄) | 先确认屏幕驱动型号选对没有——很多"ILI9341"标注的CYD板实际是ST7789(④);驱动对了颜色还是错就试试直接对调颜色常量数值(⑤) |
| 改了TFT_eSPI的User_Setup.h感觉没生效 | 警惕arduino-cli/Arduino IDE的编译缓存不识别库头文件改动,必要时手动清空build cache强制全量重编译再测 |
| 256颗全亮会不会烧板子 | 代码把亮度锁在20/255就是为了防这个,不要为了效果调高,除非用独立大电流5V电源供电 |