本帖最后由 云天 于 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
【代码实现】- #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);
- }
- }
- }
- }
- }
复制代码
程序是在ESP32-S3开发板上控制64x32 RGB LED点阵屏。程序通过WiFi连接到网络,并使用UDP协议接收来自App Inventor 2制作的APP的指令,以切换显示模式。点阵屏可以显示时钟、小动画,以及滚动显示汉字“点阵时钟”。
程序的主要功能和流程如下:
- 初始化串口、WiFi、UDP服务和NTP时间客户端。
- 连接到指定的WiFi网络,并启动UDP服务以监听特定端口。
- 定义了多个汉字和数字的点阵数据,这些数据用于在点阵屏上显示字符。
- 在loop函数中,程序首先检查是否有UDP数据包到达。如果有,它将读取数据包并根据内容切换显示模式。
- 根据当前的显示模式,程序将执行不同的显示逻辑:
- 模式一(默认):显示时钟和小动画。
- 模式二:滚动显示汉字“点阵时钟”。
- 使用drawDigit、drawChar和drawpic函数在点阵屏上绘制数字、汉字和自定义图案。
代码中还包含了一个extractIPAddress函数,用于将ESP32-S3的IP地址显示在点阵屏上。
【演示视频】
|