FireBeetle 2 ESP32-S3驱动64×32 LED点阵屏:时钟与汉字显示
本帖最后由 云天 于 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
[*]确保电源线(VCC和GND)连接正确。
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;// 接收缓冲区
// 数字和冒号的点阵数据(32x32分辨率)
const uint8_t Dian = {{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 = {{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 = {{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 = {
{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 = {{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0x40},{0x00},{0x00},/*"0",0*/};
const uint8_t num1 = { {0x00},{0x00},{0x00},{0x00},{0x60},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x70},{0x00},{0x00},/*"1",1*/
};
const uint8_t num2 = {
{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0xA0},{0x20},{0x40},{0x40},{0x40},{0x80},{0xA0},{0xE0},{0x00},{0x00},/*"2",2*/
};
const uint8_t num3 = {
{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0x20},{0x40},{0x20},{0x20},{0x20},{0xA0},{0xA0},{0xC0},{0x00},{0x00},/*"3",3*/
};
const uint8_t num4 = {{0x00},{0x00},{0x00},{0x20},{0x20},{0x20},{0x60},{0xA0},{0xA0},{0xA0},{0xF0},{0x20},{0x20},{0x30},{0x00},{0x00},/*"4",4*/
};
const uint8_t num5 = {{0x00},{0x00},{0x00},{0xE0},{0x80},{0x80},{0x80},{0xE0},{0x20},{0x20},{0x20},{0xA0},{0xA0},{0x40},{0x00},{0x00},/*"5",5*/
};
const uint8_t num6 = {
{0x00},{0x00},{0x00},{0x60},{0xA0},{0x80},{0x80},{0xA0},{0xD0},{0x90},{0x90},{0x90},{0x90},{0x60},{0x00},{0x00},/*"6",6*/
};
const uint8_t num7 = {
{0x00},{0x00},{0x00},{0x70},{0x50},{0x10},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x20},{0x00},{0x00},/*"7",7*/
};
const uint8_t num8 = {
{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0xA0},{0xE0},{0x40},{0xA0},{0xA0},{0xA0},{0xA0},{0x40},{0x00},{0x00},/*"8",8*/
};
const uint8_t num9 = {
{0x00},{0x00},{0x00},{0x40},{0xA0},{0xA0},{0xA0},{0xA0},{0xA0},{0xE0},{0x20},{0x20},{0x60},{0x40},{0x00},{0x00},/*"9",9*/
};
const uint8_t colon = {
{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x20},{0x20},{0x00},{0x00},{0x00},{0x00},{0x20},{0x20},{0x00},{0x00},/*":",10*/
};
const uint8_t dian = {{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x00},{0x60},{0x60},{0x60},{0x00},{0x00},};
const uint8_t tu1={
{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={
{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) = {&num0, &num1, &num2, &num3, &num4, &num5, &num6, &num7, &num8, &num9};
const uint8_t (*characters) = {&Dian, &Zhen, &Shi, &Zhong,&Dian, &Zhen}; // 字符数组
const uint8_t (*colon_ptr) = :
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, dma_display.color565(0, 0, 255));
drawDigit(2+char_width, 0, *digits, 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, dma_display.color565(0, 0, 255));
drawDigit(2+4 * char_width, 0, *digits, 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, dma_display.color565(0, 0, 255));
drawDigit(2+7 * char_width, 0, *digits, 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, 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 = 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, dma_display.color565(0, 0, 255));
py2=py2+1;
}
}
Serial.println();
}
// 绘制字符的函数
void drawDigit(int x, int y, const uint8_t charData, 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 & (1 << i)) {
// 在点阵屏上绘制像素
dma_display.drawPixel(x + col * 8 + 7 - i, y + row, color);
}
}
}
}
}
void drawpic(int x, int y, const uint8_t charData, 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 & (1 << i)) {
// 在点阵屏上绘制像素
dma_display.drawPixel(x + col * 8 + 7 - i, y + row, color);
}
}
}
}
}
// 绘制字符的函数
void drawChar(int x, int y, const uint8_t charData, 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 & (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地址显示在点阵屏上。
【演示视频】
https://www.bilibili.com/video/BV1jz81zEEL4/?share_source=copy_web
https://www.bilibili.com/video/BV1nvhHzoEcP/?share_source=copy_web
页:
[1]