2025 寒假在家一起练项目
一、项目介绍
本次有幸参与由电子森林精心举办的 2025 寒假在家一起练项目,在平台的选择中,我选择了平台5-CrowPanel ESP32 Display 4.3 英寸 HMI 开发板。这款开发板具备丰富的外设、强大的性能、足够多的内存和资源,并且配备了电阻触摸屏和矩阵LED灯,让我更好的完成本次活动的任务1 - 手写识别显示。
二、硬件介绍
项目采用 CrowPanel ESP32 Display 4.3 英寸 HMI 开发板,其主控为 ESP32-S3-WROOM-1-N4R2。该芯片基于先进的 Tensilica LX7 双核处理器,具备强大的运算能力,工作频率高达 240MHz ,支持 2.4GHz Wi-Fi 和蓝牙 5.0 双模通信,可轻松实现数据的无线传输与交互。
在硬件资源方面,这款开发板引出了丰富的外设。它配备了一块 4.3 英寸的 TFT LCD 触摸屏,分辨率为 480×272 像素,支持触摸操作,能够方便用户进行手写数字输入以及人机交互;同时,可以外接一个8*8的矩阵 LED 灯,可用于状态指示或简单的视觉反馈。开发板还引出了多个通用输入输出引脚(GPIO),方便连接各类传感器和执行器,如温度传感器、湿度传感器等,拓展系统功能。此外,它还具备 SPI、I2C 等通信接口,可与其他设备进行高速数据传输和通信。
三、方案框图和项目设计思路
(一)方案框图
整体系统主要由 CrowPanel ESP32 开发板、电阻触摸屏手写输入、数据处理模块(在开发板内完成)以及结果输出模块(在屏幕和矩阵LED上显示识别结果)构成。手写输入模块获取用户手写数字信息,传递给数据处理模块,处理后结果通过结果输出模块展示。

(二)项目设计思路
- 对采集到的图像执行预处理操作,包括二值化处理、去除无效区域,并将图像缩放至 30×30 尺寸。
- 在完成图像预处理后,进行数字识别,通过计算汉明距离实现数据匹配,从而得出最佳匹配结果。
- 显示界面部分,借助定时器持续刷新 LED 矩阵,以实现实时动态展示。
四、软件流程图和关键代码介绍
在软件层面,本项目基于 Arduino 平台进行开发,运用 LVGL、LovyanGFX、Ticker 三个库辅助构建整体工程架构。在 UI 开发过程中,采用 EEZ studio 图形化工具对 LVGL 的 UI 进行配置。
(一)软件流程图

(二)关键代码
//矩阵LED相关
#include <Ticker.h>
Ticker timer;
const int dataPin = 18; // DIN
const int clockPin = 17; // SRCLK
const int latchPin = 38; // RCLK
void shiftOutData(byte row, byte data) {
digitalWrite(latchPin, LOW);
shiftOut(dataPin, clockPin, MSBFIRST, data);
shiftOut(dataPin, clockPin, MSBFIRST, row);
digitalWrite(latchPin, HIGH);
}
void onTimer(){
if(canvas_dig == -1)
return;
for(int i = 0; i < 8; i++){
shiftOutData((1<<i), dig_num[canvas_dig*8+i]);
}
}
void setup()
{
pinMode(latchPin, OUTPUT);
pinMode(clockPin, OUTPUT);
pinMode(dataPin, OUTPUT);
timer.attach(0.001, onTimer); // 0.1秒 = 100毫秒
}
// 手写板记录图像的三个过程
// 写字板按下
void action_canvas_pressed(lv_event_t * e) {
lv_indev_t * indev = lv_indev_get_act();
lv_point_t p;
lv_indev_get_point(indev, &p);
lastPoint = p; // 记录按下时的点作为起点
lastPoint.x -= canvas_off_x;
lastPoint.y -= canvas_off_y;
}
// 写字板未松手
void action_canvas_pressing(lv_event_t * e) {
lv_indev_t * indev = lv_indev_get_act();
lv_point_t currentPoint;
lv_indev_get_point(indev, ¤tPoint);
currentPoint.x -= canvas_off_x;
currentPoint.y -= canvas_off_y;
//实时更新边界
if(currentPoint.x < min_point.x)
min_point.x = currentPoint.x;
if(currentPoint.y < min_point.y)
min_point.y = currentPoint.y;
if(currentPoint.x > max_point.x)
max_point.x = currentPoint.x;
if(currentPoint.y > max_point.y)
max_point.y = currentPoint.y;
// 创建一个包含两个点的数组
lv_point_t points[2] = {lastPoint, currentPoint};
// Serial.print("Pressing at x: ");
// Serial.print(points[1].x);
// Serial.print(", y: ");
// Serial.println(points[1].y);
// Serial.print("LastPressing at x: ");
// Serial.print(points[0].x);
// Serial.print(", y: ");
// Serial.println(points[0].y);
// Serial.println("");
lv_canvas_draw_line(objects.dig_canvas, points, 2, &line_dsc); //画线
lastPoint = currentPoint; // 更新最后一点的位置
lv_obj_invalidate(objects.dig_canvas); // 刷新
}
// 写字板松手
void action_canvas_released(lv_event_t * e) {
min_point.x -= 5;
min_point.y -= 5;
max_point.x += 5;
max_point.y += 5;
// Serial.print("min_x = ");
// Serial.print(min_point.x);
// Serial.print(" min_y = ");
// Serial.println(min_point.y);
// Serial.print("max_x = ");
// Serial.print(max_point.x);
// Serial.print(" max_y = ");
// Serial.println(max_point.y);
}
// 数字识别和显示的代码
//计算按键按下
void action_btn_cal_press(lv_event_t * e)
{
//Serial.println("action_btn22_pressed");
// 计算总像素数
size_t pixel_count = canvas_width * canvas_height;
//获取画布的图像描述符
const lv_img_dsc_t *img_dsc = (const lv_img_dsc_t *)lv_canvas_get_img(objects.dig_canvas);
if (!img_dsc) {
printf("无法获取画布图像描述符\n");
return;
}
// 获取图像头信息
lv_img_header_t header;
lv_res_t res = lv_img_decoder_get_info(img_dsc, &header);
if (res != LV_RES_OK) {
printf("无法获取图像头信息\n");
return;
}
const lv_color_t *buf = (const lv_color_t *)img_dsc->data;
// 分配内存用于存储二值化后的图像
uint8_t *binary_image = (uint8_t *)malloc(pixel_count * sizeof(uint8_t));
if (!binary_image) {
printf("内存分配失败\n");
return;
}
// 遍历每个像素进行二值化处理
for (size_t i = 0; i < pixel_count; ++i) {
if (buf[i].full == 0xFFFF) { // 如果是纯白(背景),注意这里是16位颜色格式
binary_image[i] = 0;
} else { // 不是白色即认为是前景(数字部分)
binary_image[i] = 1;
}
}
uint8_t writePic_width = max_point.x - min_point.x;
uint8_t writePic_height = max_point.y - min_point.y;
// 获取手写区域像素
uint8_t *vaildDig_image = (uint8_t *)malloc(writePic_width * writePic_height * sizeof(uint8_t));
for (int y = min_point.y; y < max_point.y; ++y) {
for (int x = min_point.x; x < max_point.x; ++x) {
vaildDig_image[(y-min_point.y) * writePic_width + (x-min_point.x)] = binary_image[y * canvas_height + x];
}
}
// for (int y = 0; y < writePic_height; ++y) {
// for (int x = 0; x < writePic_width; ++x) {
// printf("%d ", vaildDig_image[y * writePic_width + x]);
// }
// printf("\n");
// }
//printf("----------------------\n");
// 转化成30*30的大小
uint8_t *resized_image = (uint8_t *)malloc(TARGET_WIDTH * TARGET_HEIGHT * sizeof(uint8_t));
resize_image(vaildDig_image, resized_image, writePic_width, writePic_height, TARGET_WIDTH, TARGET_HEIGHT);
for (int y = 0; y < TARGET_HEIGHT; ++y) {
for (int x = 0; x < TARGET_WIDTH; ++x) {
printf("%d ", resized_image[y * TARGET_WIDTH + x]);
}
printf("\n");
}
// 识别图像
canvas_dig = findBestMatch(dig_templates, resized_image);
printf("dig = %d\n",canvas_dig);
printf("------------------------------\n");
String strCanvasDig = String(canvas_dig);
char text[10]; // 创建一个足够大的字符数组
strCanvasDig.toCharArray(text, sizeof(text));
lv_label_set_text(objects.lable_res, text);
// 清理资源
free(resized_image);
free(vaildDig_image);
free(binary_image);
//Serial.println("action_btn22_pressed end");
}
(三)EEZ studio界面

在左上方可以看到有5个定义的触发事件,在左侧可以看到使用了哪几个控件
五、功能展示图及说明
(一)说明
在 CrowPanel ESP32 开发板的触摸屏指定区域,可通过手指或触摸笔进行手写数字输入。完成输入后,点击识别按键,系统将自动执行识别操作,并在屏幕右侧 “result” 区域显示识别出的数字,同时在矩阵 LED 上同步刷新该数字。例如,当手写数字 “5” 时,系统精准识别并在屏幕特定区域显示 “5”,这一过程表明系统的识别功能运行正常。
(二)功能展示图


六、项目中遇到的问题和解决方法
- 图像预处理效果不佳:写的算法有点问题,在将有效区域提取出来的时候,总是在越界操作,导致数据一直不太正确。操作vaildDig_image 的时候没有将min_point.y 和 min_point.x减去掉
修改后的代码
// 获取手写区域像素
uint8_t *vaildDig_image = (uint8_t *)malloc(writePic_width * writePic_height * sizeof(uint8_t));
for (int y = min_point.y; y < max_point.y; ++y) {
for (int x = min_point.x; x < max_point.x; ++x) {
vaildDig_image[(y-min_point.y) * writePic_width + (x-min_point.x)] = binary_image[y * canvas_height + x];
}
}
- 数字识别:需先将数据导出,随后开展模型训练或模板匹配工作。因为训练数据获取难度较大,并且需要细致的参数调整与优化。所以拿到数据后用哪一种方式都可以。
- 在年前完成了 SquareLine Studio 的注册并搭建了基础开发环境,但该软件试用期限仅为一个月。当准备开展任务时,发现软件已过期无法使用。解决措施如下:1. 寻求特殊授权或解决方案(未找到)。2. 筛选其他替代软件,最终选用 EEZ Studio。
七、对本次活动的心得体会
- 技术能力提升:通过本次项目,深入学习了手写数字识别的原理和技术,掌握了 CrowPanel ESP32 开发板的使用,以及在 Arduino IDE 环境下进行代码编写和调试
- 掌握了几种常用的图像处理方式:二值化、提纯、缩放图片、汉明距离计算、模板匹配
- 问题解决能力:在项目中遇到各种难题,通过查阅资料、尝试不同方法,最终成功解决,锻炼了自己独立解决问题的能力。