| 本帖最后由 云天 于 2025-8-1 20:53 编辑 
 【项目概述】
 
 在本项目中,我将介绍如何使用FireBeetle 2 ESP32-S3开发板驱动64×32 RGB LED点阵屏,实现时钟显示和汉字滚动显示功能。通过模式切换,可以使用手机APP发送指令来控制显示内容。这个项目不仅展示了FireBeetle 2 ESP32-S3的强大功能,还结合了硬件驱动、网络通信和图形显示技术,适合有一定Arduino编程基础和硬件连接经验的创客。
 
  【硬件准备】
 FireBeetle 2 ESP32-S3开发板:一款功能强大的物联网开发板,支持WiFi和蓝牙5.0双模通信,具备丰富的外设接口。64×32 RGB LED点阵屏:一款高亮度、全彩的LED显示屏,适合制作小型广告牌或信息显示设备。杜邦线若干:用于连接开发板和点阵屏。电源适配器:为点阵屏提供稳定的5V电源。
  【软件准备】
 【硬件连接】Arduino IDE 2.3.6:用于编写和上传代码到FireBeetle 2 ESP32-S3。ESP32库:通过Arduino IDE的库管理器安装。ESP32-HUB75-MatrixPanel-I2S-DMA库:用于驱动RGB LED点阵屏。Mit App Inventor 2:用于创建手机APP,实现通过UDP发送指令的功能。pctolcd2002:生成点阵数据。
 将点阵屏的16P排线接口与FireBeetle 2 ESP32-S3的对应引脚连接。
 
 复制代码#define R1_PIN 0
#define G1_PIN 9
#define B1_PIN 18
#define R2_PIN 7
#define G2_PIN 38
#define B2_PIN 3
#define A_PIN 4
#define B_PIN 5
#define C_PIN 6
#define D_PIN 8
#define E_PIN -1 // 对于1/32扫描面板,如64x64,需要连接到ESP32的任意可用引脚,例如GPIO 32
#define LAT_PIN 13
#define OE_PIN 14
#define CLK_PIN 12 
  
 2.连接电源适配器:
 
 为点阵屏提供5V电源,确保电源适配器的电流足够(建议使用5V/4A或更高规格)。
 【点阵数据】Pctolcd2002 是一款在中文创客社区中广泛使用的免费软件,用于生成字符点阵数据,这些数据可以被微控制器读取并在LED点阵屏或LCD显示屏上显示。该软件支持多种点阵大小和字体,可以生成汉字、ASCII字符等的点阵数据,非常适合嵌入式系统开发者使用。
 
  
 
 
 【APP开发】
 
 使用Mit App Inventor 2创建一个简单的APP,实现以下功能:
 
 输入FireBeetle 2 ESP32-S3的IP地址。发送UDP指令切换显示模式。显示当前模式对应的图片。
 
   注:udp扩展下载地址:http://ullisroboterseite.de/android-AI2-UDP/UrsAI2UDP.zip
 
 【代码实现】
 程序是在ESP32-S3开发板上控制64x32 RGB LED点阵屏。程序通过WiFi连接到网络,并使用UDP协议接收来自App Inventor 2制作的APP的指令,以切换显示模式。点阵屏可以显示时钟、小动画,以及滚动显示汉字“点阵时钟”。复制代码#include <WiFi.h>
#include <WiFiUdp.h>
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
#include <NTPClient.h>
#define R1_PIN 0
#define G1_PIN 9
#define B1_PIN 18
#define R2_PIN 7
#define G2_PIN 38
#define B2_PIN 3
#define A_PIN 4
#define B_PIN 5
#define C_PIN 6
#define D_PIN 8
#define E_PIN -1 // 对于1/32扫描面板,如64x64,需要连接到ESP32的任意可用引脚,例如GPIO 32
#define LAT_PIN 13
#define OE_PIN 14
#define CLK_PIN 12 
HUB75_I2S_CFG::i2s_pins _pins = {R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
HUB75_I2S_CFG mxconfig(64, 32, 1, _pins);
MatrixPanel_I2S_DMA dma_display(mxconfig);
// WiFi网络配置
const char* ssid = "*****";  // 替换为你的WiFi名称
const char* password = "*********";  // 替换为你的WiFi密码
// UDP配置
WiFiUDP udp,ntpUDP;
NTPClient timeClient(ntpUDP);
unsigned int localPort = 8888;  // 本地UDP端口号
char packetBuffer[255];  // 接收缓冲区
// 数字和冒号的点阵数据(32x32分辨率)
const uint8_t Dian[32][4] = {{0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00},
{0x00,0x01,0x00,0x00},
{0x00,0x01,0x80,0x00},
{0x00,0x01,0x80,0x00},
{0x00,0x01,0x80,0x00},
{0x00,0x01,0x80,0x60},
{0x00,0x01,0xFF,0xF0},
{0x00,0x01,0x80,0x00},
{0x00,0x01,0x80,0x00},
{0x00,0x01,0x80,0x00},
{0x00,0x01,0x80,0x00},
{0x01,0x01,0x80,0x80},
{0x01,0xFF,0xFF,0xC0},
{0x01,0x80,0x01,0x80},
{0x01,0x80,0x01,0x80},
{0x01,0x80,0x01,0x80},
{0x01,0x80,0x01,0x80},
{0x01,0x80,0x01,0x80},
{0x01,0x80,0x01,0x80},
{0x01,0xFF,0xFF,0x80},
{0x01,0x80,0x01,0x80},
{0x01,0x80,0x01,0x00},
{0x00,0x00,0x00,0x00},
{0x00,0x10,0x20,0x40},
{0x02,0x08,0x30,0x60},
{0x02,0x0C,0x18,0x30},
{0x06,0x0C,0x18,0x38},
{0x0C,0x06,0x18,0x18},
{0x1C,0x04,0x08,0x18},
{0x18,0x04,0x00,0x10},
{0x00,0x00,0x00,0x00}};
const uint8_t Zhen[32][4] = {{0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00},
{0x00,0x00,0x20,0x00},
{0x00,0x00,0x38,0x00},
{0x10,0x20,0x30,0x00},
{0x1F,0xF0,0x20,0x00},
{0x18,0x30,0x60,0x30},
{0x18,0x6F,0xFF,0xF8},
{0x18,0x40,0x40,0x00},
{0x18,0x40,0xC0,0x00},
{0x18,0x80,0xC8,0x00},
{0x18,0x80,0x8E,0x00},
{0x19,0x01,0x8C,0x00},
{0x19,0x01,0x8C,0x00},
{0x18,0x81,0x0C,0x00},
{0x18,0x43,0x0C,0x30},
{0x18,0x67,0xFF,0xF0},
{0x18,0x22,0x0C,0x00},
{0x18,0x30,0x0C,0x00},
{0x18,0x30,0x0C,0x00},
{0x18,0x30,0x0C,0x00},
{0x1C,0x30,0x0C,0x18},
{0x1B,0xEF,0xFF,0xFC},
{0x18,0xE0,0x0C,0x00},
{0x18,0x80,0x0C,0x00},
{0x18,0x00,0x0C,0x00},
{0x18,0x00,0x0C,0x00},
{0x18,0x00,0x0C,0x00},
{0x18,0x00,0x0C,0x00},
{0x18,0x00,0x0C,0x00},
{0x10,0x00,0x08,0x00},
{0x00,0x00,0x00,0x00}};
const uint8_t Shi[32][4] = {{0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00},
{0x00,0x00,0x02,0x00},
{0x00,0x00,0x03,0x80},
{0x00,0x00,0x03,0x00},
{0x00,0x20,0x03,0x00},
{0x1F,0xF0,0x03,0x00},
{0x18,0x30,0x03,0x00},
{0x18,0x30,0x03,0x00},
{0x18,0x30,0x03,0x18},
{0x18,0x3F,0xFF,0xFC},
{0x18,0x30,0x03,0x00},
{0x18,0x30,0x03,0x00},
{0x18,0x30,0x03,0x00},
{0x18,0x32,0x03,0x00},
{0x1F,0xF1,0x03,0x00},
{0x18,0x31,0xC3,0x00},
{0x18,0x30,0xC3,0x00},
{0x18,0x30,0xE3,0x00},
{0x18,0x30,0x43,0x00},
{0x18,0x30,0x03,0x00},
{0x18,0x30,0x03,0x00},
{0x18,0x30,0x03,0x00},
{0x1F,0xF0,0x03,0x00},
{0x18,0x30,0x03,0x00},
{0x18,0x30,0x03,0x00},
{0x18,0x00,0x03,0x00},
{0x00,0x00,0x03,0x00},
{0x00,0x00,0x3F,0x00},
{0x00,0x00,0x07,0x00},
{0x00,0x00,0x06,0x00},
{0x00,0x00,0x00,0x00}};
const uint8_t Zhong[32][4] = {
{0x00,0x00,0x00,0x00},
{0x00,0x00,0x00,0x00},
{0x02,0x00,0x04,0x00},
{0x03,0x80,0x06,0x00},
{0x03,0x00,0x06,0x00},
{0x02,0x00,0x06,0x00},
{0x06,0x18,0x06,0x00},
{0x07,0xFC,0x06,0x00},
{0x04,0x00,0x06,0x10},
{0x0C,0x01,0xFF,0xF8},
{0x08,0x01,0x86,0x10},
{0x08,0x31,0x86,0x10},
{0x1F,0xF9,0x86,0x10},
{0x13,0x01,0x86,0x10},
{0x23,0x01,0x86,0x10},
{0x43,0x01,0x86,0x10},
{0x03,0x01,0x86,0x10},
{0x03,0x19,0xFF,0xF0},
{0x3F,0xFD,0x86,0x10},
{0x03,0x01,0x06,0x10},
{0x03,0x00,0x06,0x00},
{0x03,0x00,0x06,0x00},
{0x03,0x04,0x06,0x00},
{0x03,0x08,0x06,0x00},
{0x03,0x10,0x06,0x00},
{0x03,0x60,0x06,0x00},
{0x03,0xC0,0x06,0x00},
{0x03,0x80,0x06,0x00},
{0x01,0x00,0x06,0x00},
{0x00,0x00,0x06,0x00},
{0x00,0x00,0x04,0x00},
{0x00,0x00,0x00,0x00}};
const uint8_t num0[16][1] = {{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0x40},{0x00},{0x00},/*"0",0*/};
const uint8_t num1[16][1] = { {0x00},{0x00},{0x00},{0x00},{0x60},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x70},{0x00},{0x00},/*"1",1*/
};
const uint8_t num2[16][1] = { 
{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0xA0},{0x20},{0x40},{0x40},{0x40},{0x80},{0xA0},{0xE0},{0x00},{0x00},/*"2",2*/
};
const uint8_t num3[16][1] = {
{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0x20},{0x40},{0x20},{0x20},{0x20},{0xA0},{0xA0},{0xC0},{0x00},{0x00},/*"3",3*/
};
const uint8_t num4[16][1] = {{0x00},{0x00},{0x00},{0x20},{0x20},{0x20},{0x60},{0xA0},{0xA0},{0xA0},{0xF0},{0x20},{0x20},{0x30},{0x00},{0x00},/*"4",4*/
};
const uint8_t num5[16][1] = {{0x00},{0x00},{0x00},{0xE0},{0x80},{0x80},{0x80},{0xE0},{0x20},{0x20},{0x20},{0xA0},{0xA0},{0x40},{0x00},{0x00},/*"5",5*/
};
const uint8_t num6[16][1] = { 
{0x00},{0x00},{0x00},{0x60},{0xA0},{0x80},{0x80},{0xA0},{0xD0},{0x90},{0x90},{0x90},{0x90},{0x60},{0x00},{0x00},/*"6",6*/
};
const uint8_t num7[16][1] = {
{0x00},{0x00},{0x00},{0x70},{0x50},{0x10},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x00},{0x00},/*"7",7*/
};
const uint8_t num8[16][1] = {
{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0xA0},{0xE0},{0x40},{0xA0},{0xA0},{0xA0},{0xA0},{0x40},{0x00},{0x00},/*"8",8*/
};
const uint8_t num9[16][1] = {
{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xE0},{0x20},{0x20},{0x60},{0x40},{0x00},{0x00},/*"9",9*/
 };
const uint8_t colon[16][1] = {
{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x20},{0x20},{0x00},{0x00},{0x00},{0x00},{0x20},{0x20},{0x00},{0x00},/*":",10*/
};
const uint8_t dian[16][1] = {{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x60},{0x60},{0x60},{0x00},{0x00},};
const uint8_t tu1[16][2]={
{0x01,0xC0},{0x0E,0x38},{0x18,0x0C},{0x30,0x04},{0x60,0x06},{0x62,0x26},{0x20,0x84},{0x20,0x84},{0x38,0x04},{0x18,0x1C},{0x30,0x04},{0x30,0x04},{0xE0,0x0C},{0x70,0x04},{0x1F,0xF8},{0x01,0x20}
};
const uint8_t tu2[16][2]={
{0x01,0xC0},{0x0E,0x38},{0x18,0x0C},{0x30,0x04},{0x60,0x06},{0x62,0x26},{0x20,0x84},{0x21,0xC4},{0x38,0x14},{0x18,0x1C},{0x34,0x04},{0x30,0x04},{0xE0,0x0C},{0x70,0x04},{0x1F,0xF8},{0x04,0x40},};
const uint8_t (*digits[10])[16][1] = {&num0, &num1, &num2, &num3, &num4, &num5, &num6, &num7, &num8, &num9};
const uint8_t (*characters[6])[32][4] = {&Dian, &Zhen, &Shi, &Zhong,&Dian, &Zhen}; // 字符数组
const uint8_t (*colon_ptr)[16][1] = :
int x_offset = -16; // 横向偏移量
int char_width = 8; // 每个字符的宽度
int x_offset2 = 0; // 横向偏移量
int num_chars2 = 6; // 字符数量
int char_width2 = 32; // 每个字符的宽度
int py1=0;
IPAddress myip;
int foot=0;
int bs=0;
void setup() {
  // 初始化串口
  Serial.begin(115200);
  Serial.println("ESP32-S3 UDP接收端启动中...");
  dma_display.begin();
  dma_display.setBrightness(128);
  dma_display.fillScreen(0);
  // 连接WiFi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  // 等待连接WiFi
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi连接成功!");
  Serial.print("IP地址: ");
  Serial.println(WiFi.localIP());
  myip=WiFi.localIP();
  // 启动UDP监听
  udp.begin(localPort);
  Serial.print("UDP服务器在端口 ");
  Serial.print(localPort);
  Serial.println(" 上启动");
  timeClient.begin();
  timeClient.setTimeOffset(28800); // 设置时区为 GMT+8 (8 * 3600 = 28800 秒)
  timeClient.update();
}
String receivedString ="";
void loop() {
  static unsigned long previousMillis = 0; // 上一次更新时间
  static unsigned long previousMillis2 = 0; // 上一次更新时间
  static unsigned long previousMillis3 = 0; // 上一次更新时间
  static unsigned long previousMillis4 = 0; // 上一次更新时间
  unsigned long currentMillis = millis(); // 获取当前时间(毫秒)
  // 检查是否有UDP数据包到达
  if(receivedString ==""){
    if (currentMillis - previousMillis >= 200) {
    previousMillis = currentMillis; // 更新上一次时间
    dma_display.fillScreen(0);
    extractIPAddress(myip);
    py1=py1-1;
    if(py1<-14*8){
      py1=64;
    }
    }
  }
  else if(receivedString =="1"){
    if (currentMillis - previousMillis3 >= 1000) {
        previousMillis3 = currentMillis; // 更新上一次时间
        bs=1-bs;
    }
   if (currentMillis - previousMillis2 >= 200) {
    previousMillis2 = currentMillis; // 更新上一次时间
    // 更新时间
    timeClient.update();
     // 获取当前时间
     int hours = timeClient.getHours();
     int minutes = timeClient.getMinutes();
     int seconds = timeClient.getSeconds();
     // 清屏
     dma_display.fillScreen(0);
     // 绘制时间
     drawDigit(2, 0, *digits[hours / 10], dma_display.color565(0, 0, 255));
     drawDigit(2+char_width, 0, *digits[hours % 10], dma_display.color565(0, 0, 255));
     if(bs==0){
        drawDigit(2+2 * char_width, 0, colon, dma_display.color565(255, 0,0));
     }
     else{
        drawDigit(2+2 * char_width, 0, colon, dma_display.color565(255, 255,0));
     }
     drawDigit(2+3 * char_width, 0, *digits[minutes / 10], dma_display.color565(0, 0, 255));
     drawDigit(2+4 * char_width, 0, *digits[minutes % 10], dma_display.color565(0, 0, 255));
     if(bs==0){
        drawDigit(2+5 * char_width, 0, colon, dma_display.color565(255, 0,0));
     }
     else{
        drawDigit(2+5 * char_width, 0, colon, dma_display.color565(255, 255,0));
     }
     drawDigit(2+6 * char_width, 0, *digits[seconds / 10], dma_display.color565(0, 0, 255));
     drawDigit(2+7 * char_width, 0, *digits[seconds % 10], dma_display.color565(0, 0, 255));
     if(foot==1){
        drawpic(x_offset, 16, tu1, dma_display.color565(0, 255, 0));
     }
     else{
        drawpic(x_offset, 16, tu2, dma_display.color565(0, 255, 0));
     }
     foot=1-foot;
     // 更新偏移量
     x_offset++;
     if (x_offset >80) {
          x_offset = -16; // 重置偏移量,实现循环滚动
     }
   }
  }
  else if(receivedString =="2"){
   if (currentMillis - previousMillis4 >= 500) {
    previousMillis4 = currentMillis; // 更新上一次时间
    dma_display.fillScreen(0);
     // 绘制所有字符
    for (int i = 0; i < num_chars2; i++) {
      int char_x = x_offset2 + i * char_width2; // 计算当前字符的起始x坐标
      drawChar(char_x, 0, *characters[i], dma_display.color565(0, 0, 255)); // 绘制字符
    }
    // 更新偏移量
    x_offset2--;
    // 更新偏移量
    x_offset2--;
    if (x_offset2 < -char_width2*4) {
      x_offset2 =0; // 重置偏移量,实现循环滚动
    }
   }
  }
  int packetSize = udp.parsePacket();
  if (packetSize) {
    Serial.print("收到来自 ");
    IPAddress remoteIp = udp.remoteIP();
    Serial.print(remoteIp);
    Serial.print(":");
    Serial.print(udp.remotePort());
    Serial.print(" 的数据包,大小: ");
    Serial.println(packetSize);
    // 读取数据包内容
    int len = udp.read(packetBuffer, 255);
    if (len > 0) {
      packetBuffer[len] = 0;  // 添加字符串结束符
    }
    Serial.print("内容: ");
    Serial.println(packetBuffer);
    // 将packetBuffer转换为字符串
    receivedString = String(packetBuffer);
    // 清空packetBuffer
    memset(packetBuffer, 0, sizeof(packetBuffer));
    // 可以在这里添加对接收到的数据的处理逻辑
  }
  // 短暂延时,让CPU有机会处理其他任务
  delay(10);
}
void extractIPAddress(IPAddress ip) {
  String ipStr = ip.toString();  // 将IPAddress对象转换为字符串
  Serial.print("IP地址逐字符处理: ");
  int py2=0;
  for (int i = 0; i < ipStr.length(); i++) {
    char c = ipStr.charAt(i);  // 获取当前字符
    if (c == '.') {
      Serial.print(".");
      drawDigit(py1+py2*8, 0, dian, dma_display.color565(0, 0, 255));
      py2=py2+1;
    } else {
      // 将字符转换为数字
      int num = c - '0';  // ASCII码转换
      Serial.print(num);
      drawDigit(py1+py2*8, 0, *digits[num], dma_display.color565(0, 0, 255));
      py2=py2+1;
    }
  }
  Serial.println();
}
// 绘制字符的函数
void drawDigit(int x, int y, const uint8_t charData[16][1], uint16_t color) {
  for (int row = 0; row < 16; row++) {
    for (int col = 0; col < 1; col++) {
      for (int i = 0; i < 8; i++) {
        if (charData[row][col] & (1 << i)) {
          // 在点阵屏上绘制像素
          dma_display.drawPixel(x + col * 8 + 7 - i, y + row, color);
        }
      }
    }
  }
}
void drawpic(int x, int y, const uint8_t charData[16][2], uint16_t color) {
  for (int row = 0; row < 16; row++) {
    for (int col = 0; col < 2; col++) {
      for (int i = 0; i < 8; i++) {
        if (charData[row][col] & (1 << i)) {
          // 在点阵屏上绘制像素
          dma_display.drawPixel(x + col * 8 + 7 - i, y + row, color);
        }
      }
    }
  }
}
// 绘制字符的函数
void drawChar(int x, int y, const uint8_t charData[32][4], uint16_t color) {
  for (int row = 0; row < 32; row++) {
    for (int col = 0; col < 4; col++) {
      // 检查当前列是否需要点亮
      for(int i=0;i<8;i++){
       if (charData[row][col] & (1 <<i)) {
        // 在点阵屏上绘制像素
        dma_display.drawPixel(x + col*8+7-i, y + row, color);
      }
    }
    }
  }
}
 程序的主要功能和流程如下:
 
 代码中还包含了一个extractIPAddress函数,用于将ESP32-S3的IP地址显示在点阵屏上。初始化串口、WiFi、UDP服务和NTP时间客户端。连接到指定的WiFi网络,并启动UDP服务以监听特定端口。定义了多个汉字和数字的点阵数据,这些数据用于在点阵屏上显示字符。在loop函数中,程序首先检查是否有UDP数据包到达。如果有,它将读取数据包并根据内容切换显示模式。根据当前的显示模式,程序将执行不同的显示逻辑:
 模式一(默认):显示时钟和小动画。模式二:滚动显示汉字“点阵时钟”。
使用drawDigit、drawChar和drawpic函数在点阵屏上绘制数字、汉字和自定义图案。
 
 【演示视频】
 
 
 
 
 
 
 |