明武电子技术资料库 LOGO 明武电子技术资料库电子模块 · 技术支持

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颗独立可寻址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电源 测试,乱色依旧存在——最终定位到真正原因跟供电无关(见下方"踩坑实录"),供电只是本来就该 遵守的安全底线,不是这次乱色的病根。

屏幕UI与13个模式

点屏幕右上角"EFFECT PATTERN"按钮循环切换模式,0~12共13个:

模式名称内容
0DRAW MODE手绘模式:点左边16x16网格用当前选中颜色点亮对应LED,配CLEAR/DRAW按钮
1Rainbow Wave彩虹波浪,色相随时间平移
2Fire Simulation火焰模拟,红橙色随机跳动
3Cyberpunk Pulse品红色呼吸脉冲
4Plasma Flow正弦+余弦叠加的等离子流动
5Neon Rain霓虹雨,绿色光点从顶部往下掉
6Strobe Party用当前选中颜色做频闪
7Color Swirl Center彩色漩涡
8Random Sparkles随机星光闪烁
9Larson Scanner红色扫描光(霹雳游侠车灯风格)
10Ocean Waves蓝绿色海浪效果
11COLOR TEST排查用:全屏纯红/绿/蓝循环,验证颜色通道顺序
12ORDER TEST排查用:单颗白点按0~255顺序走一遍,验证灯珠排布方向

其它固定UI:亮度滑条(BRIGHTNESS,1~20范围内可调)、8色快选色板(MANUAL COLOR SWATCH, 影响手绘和第6号频闪模式的颜色)、当前选色文字读数(SEL: 颜色名)。

踩坑实录:实测挖出的5个真实bug

这块代码是现成拿到的草图,不是从零写的,但实机跑起来之后暴露了好几个只有真机测试才能 发现的问题:

#现象真正原因怎么修好的
①手绘图案跟LED颜色完全对不上,一片乱色手绘模式下主循环不管画面有没有变化,每次空转都把256颗灯的数据重发一遍,高频轰炸式发送把时序线打断了加标志位,只在画面真的改动时才刷新一次
②点第1个色块,实际选中了第2个触摸坐标换算固定偏移了一整格坐标换算里补一个修正量
③修完①之后,拖亮度条LED没反应加节流后,单纯调亮度不会触发刷新亮度变化时额外强制刷新一次
④屏幕上画的颜色跟预期完全不对(比如选红显示青/黄)用错了屏幕驱动型号——这块CYD板常被当ILI9341卖,实际控制芯片是ST7789换成正确的驱动型号,颜色错乱从"毫无规律"变成了"红蓝干净对调"
⑤驱动型号对了,红蓝还是对调这块廉价面板没有真正实现"调色序"这个硬件开关,软件设置改不了不跟硬件较劲,直接在代码里把红蓝两个颜色的数值对调

关键代码:两个真实修复片段

下面这两段是从 CYD_16x16_256LED.ino 真实源码里摘出来的,对应上表①和⑤两条, 方便直接照着抄或者对照自己的项目排查同类问题。

CYD_16x16_256LED.ino · loop() · 对应①手绘模式节流
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;
  }
}
CYD_16x16_256LED.ino · 文件顶部 · 对应⑤屏幕红蓝对调
// 这块面板的红蓝通道在硬件层面对调,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 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() 的调用方式一致。

购买信息

SKU 01780

256位 2020 RGB LED灯板(16x16 方形4孔耳)

SKU01780 256位2020 RGB LED灯板商品图
SKU 01564

ESP32开发板 WiFi蓝牙 2.8寸触摸屏 配线

SKU01564 ESP32 2.8寸黄屏配线商品图

主控板商品图来自明武电子自家店铺,含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电源供电