2025寒假练-基于CrowPanel ESP32 HMI 4.3英寸显示屏开发板的手写识别显示
该项目使用了CrowPanel ESP32 HMI 4.3英寸显示屏开发板,实现了手写识别显示的设计,它的主要功能为:手写识别显示。
标签
嵌入式系统
显示
lvgl
2025寒假练
CrowPanel ESP32
Tide
更新2025-03-17
11

一、项目需求

  • 使用LVGL编程
  • 在LCD屏幕上设定一个正方形的写字区域
  • 在写字区域里书写0-9的数字
  • 对书写的数字进行识别,并将识别的数字传递给灯板,在灯板上进行显示


二、需求分析

  1. 用户界面设计:
    • 在 LCD 屏幕上设置一个方形的写字区域,用户可以在该区域内手写数字(0-9)。
    • 写字区域需要有清晰的边界,方便用户识别书写范围。
    • 写字区域的大小和位置需要根据屏幕分辨率进行适配。
  2. 手写识别功能:
    • 用户在写字区域书写数字后,系统需要能够识别手写的数字。
    • 识别功能可以通过用户完成书写后点击“识别”按钮触发。
    • 识别算法可以基于预训练的机器学习模型,并通过 PC 进行识别。
  3. 结果显示:
    • 识别后的数字需要显示在屏幕上(例如在写字区域右侧的面板中)。
    • 同时,识别的数字需要通过某种方式传递给灯板 LED 矩阵,并在灯板上显示对应的数字。
  4. 清除功能:
    • 提供一个“清除”按钮,用于清除写字区域的内容,以便重新书写。
  5. 通信功能:
    • 识别功能依赖于 PC,需要通过串口通信将写字区域的图像数据发送到 PC,并接收PC返回的识别结果。


三、功能模块划分

根据分析的需求,整个系统可以划分为以下几个功能模块:

1. 用户界面模块

  • 功能:
    • 初始化 LCD 屏幕。
    • 使用LVGL创建一个方形的写字区域(画布)。
    • 在屏幕右侧创建一个结果显示区域,用于显示识别的数字。
    • 提供“识别”和“清除”按钮。
  • 实现方式:
    • 使用 LVGL 库创建界面元素(画布、按钮、标签等)。
    • 设置合适的布局参数(如间距、边框等)以适配屏幕分辨率。

2. 触摸模块

  • 功能:
    • 捕获用户的触摸输入,并在写字区域绘制笔迹。
    • 支持连续书写和防抖处理。
  • 实现方式:
    • 使用 LVGL 的触摸事件回调函数(canvas_event_cb)来处理触摸输入。
    • 在画布上绘制圆形笔迹,模拟手写效果。

3. 与pc通信模块

  • 功能:
    • 将图像数据发送到 PC 进行识别,通过串口通信实现数据传输。
  • 实现方式:
    • 初始化串口通信。
    • 发送图像数据,并接收识别结果。

4. 手写识别模块

  • 功能:
    • 将书写区域的图像数据提取出来,并预处理(如灰度化、归一化)后输入到模型中
    • 使用机器学习模型(如MNIST手写数字识别模型)对书写内容进行数字识别。
    • 将识别结果返回给用户界面模块。
  • 实现方式:
    • 使用 PC 识别,通过串口发送图像数据,并接收识别结果。

5. LED矩阵显示模块

  • 功能:
    • 将识别的数字显示在灯板LED 矩阵上。
  • 实现方式:
    • 使用CrowPanel ESP32 HMI 4.3英寸显示屏开发板硬件接口控制灯板。
    • 在灯板端编写驱动程序,接收数字并控制硬件点亮对应的 LED 灯。


四、功能框图

五、主要功能代码及说明

1. 用户界面模块

  • 使用LVGL库创建界面元素,包括画布、按钮和标签。
  • 设置布局参数(如间距、边框等)以适配屏幕分辨率。
  • 画布用于手写输入,按钮用于触发识别和清除操作。
  • 结果显示区域用于显示识别的数字。
// 初始化用户界面
void ui_init() {
DEBUG_TIMESTAMP("Starting UI initialization");
g_canvas_width = CANVAS_WIDTH;
g_canvas_height = CANVAS_HEIGHT;
create_handwriting_area(); // 创建写字区域
DEBUG_TIMESTAMP("UI initialization completed");
}

// 创建写字区域
void create_handwriting_area() {
// 分配画布内存
size_t required_mem = g_canvas_width * g_canvas_height * sizeof(lv_color_t);
DEBUG_PRINTF("Allocating canvas memory: %d bytes\n", required_mem);
if (psramFound()) {
g_canvas_buf = static_cast<lv_color_t*>(heap_caps_malloc(required_mem, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
if (g_canvas_buf) {
DEBUG_PRINTLN("Canvas memory allocated in PSRAM");
}
}

if (g_canvas_buf == nullptr) {
DEBUG_PRINTLN("Failed to allocate canvas memory");
ui_state = UI_STATE_ERROR;
return;
}

// 创建左侧画布容器
lv_obj_t *left_cont = lv_obj_create(lv_scr_act());
lv_obj_set_size(left_cont, LEFT_PANEL_WIDTH, LEFT_PANEL_HEIGHT);
lv_obj_align(left_cont, LV_ALIGN_LEFT_MID, CANVAS_PAD, 0);
lv_obj_set_style_bg_color(left_cont, lv_color_hex(UI_COLOR_WHITE), 0);
lv_obj_set_style_border_width(left_cont, BORDER_WIDTH, 0);
lv_obj_set_style_border_color(left_cont, lv_color_hex(UI_COLOR_BLACK), 0);

// 创建画布
g_canvas = lv_canvas_create(left_cont);
lv_canvas_set_buffer(g_canvas, g_canvas_buf, g_canvas_width, g_canvas_height, LV_IMG_CF_TRUE_COLOR);
lv_obj_align(g_canvas, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(g_canvas, canvas_event_cb, LV_EVENT_ALL, NULL); // 添加触摸事件回调
lv_canvas_fill_bg(g_canvas, lv_color_hex(0xFFFFFF), LV_OPA_COVER); // 设置画布背景为白色
DEBUG_PRINTLN("Canvas initialized with white background");

// 创建右侧面板
create_right_panel();
}

// 创建右侧面板
void create_right_panel() {
lv_obj_t *right_cont = lv_obj_create(lv_scr_act());
lv_obj_set_size(right_cont, RIGHT_PANEL_WIDTH, LEFT_PANEL_HEIGHT);
lv_obj_align(right_cont, LV_ALIGN_RIGHT_MID, -CANVAS_PAD, 0);
lv_obj_set_style_bg_color(right_cont, lv_color_hex(UI_COLOR_WHITE), 0);

// 创建结果显示区域
lv_obj_t *display_area = lv_obj_create(right_cont);
lv_obj_set_size(display_area, BTN_WIDTH, BTN_WIDTH);
lv_obj_align(display_area, LV_ALIGN_TOP_MID, 0, CANVAS_PAD);

// 创建结果标签
g_result_label = lv_label_create(display_area);
lv_obj_set_style_text_font(g_result_label, &lv_font_montserrat_48, 0);
lv_obj_set_style_text_color(g_result_label, lv_color_hex(UI_COLOR_BLACK), 0);
lv_label_set_text(g_result_label, "?");
lv_obj_center(g_result_label);

// 创建按钮
create_buttons(right_cont);
}

// 创建按钮
void create_buttons(lv_obj_t *parent) {
// 创建识别按钮
lv_obj_t *recog_btn = lv_btn_create(parent);
lv_obj_set_size(recog_btn, BTN_WIDTH, BTN_HEIGHT);
lv_obj_align(recog_btn, LV_ALIGN_CENTER, 0, 20);
lv_obj_set_style_bg_color(recog_btn, lv_color_hex(UI_COLOR_BLUE), LV_STATE_DEFAULT);

lv_obj_t *recog_label = lv_label_create(recog_btn);
lv_label_set_text(recog_label, "Recog");
lv_obj_center(recog_label);
lv_obj_add_event_cb(recog_btn, recog_btn_event_cb, LV_EVENT_CLICKED, NULL); // 添加事件回调

// 创建清除按钮
lv_obj_t *clear_btn = lv_btn_create(parent);
lv_obj_set_size(clear_btn, BTN_WIDTH, BTN_HEIGHT);
lv_obj_align(clear_btn, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_obj_set_style_bg_color(clear_btn, lv_color_hex(UI_COLOR_RED), LV_STATE_DEFAULT);

lv_obj_t *clear_label = lv_label_create(clear_btn);
lv_label_set_text(clear_label, "Clear");
lv_obj_center(clear_label);
lv_obj_add_event_cb(clear_btn, clear_btn_event_cb, LV_EVENT_CLICKED, NULL); // 添加事件回调
}

2. 触摸模块

  • 使用LVGL的触摸事件回调函数canvas_event_cb处理触摸输入。
  • 在画布上绘制圆形笔迹,模拟手写效果。
  • 支持连续书写和防抖处理,通过HW_TOUCH_THRESHOLD参数控制。
// 画布事件回调函数
void canvas_event_cb(lv_event_t *e) {
static lv_point_t last_point = {0, 0};
lv_obj_t *canvas = (lv_obj_t *)lv_event_get_target(e);
lv_event_code_t code = lv_event_get_code(e);

if (!canvas) {
DEBUG_PRINTLN("Invalid canvas in event callback");
return;
}

lv_indev_t *indev = lv_indev_get_act();
if (!indev || lv_indev_get_type(indev) != LV_INDEV_TYPE_POINTER) {
DEBUG_PRINTLN("Invalid input device");
return;
}

lv_point_t point;
lv_indev_get_point(indev, &point);

// 防抖处理
static int32_t last_x = 0, last_y = 0;
int32_t dx = point.x - last_x;
int32_t dy = point.y - last_y;
float distance = sqrt(dx * dx + dy * dy);

if (distance < HW_TOUCH_THRESHOLD && last_x != 0) {
point.x = last_x;
point.y = last_y;
} else {
last_x = point.x;
last_y = point.y;
}

// 调整触摸点坐标到画布的相对位置
lv_area_t coords;
lv_obj_get_coords(canvas, &coords);
point.x -= coords.x1;
point.y -= coords.y1;

if (!is_point_in_canvas(canvas, &point)) {
current_state = HANDWRITE_IDLE;
led_matrix_set_touch_active(false);
return;
}

lv_color_t color = lv_color_hex(0x000000); // 设置绘制颜色为黑色

switch (code) {
case LV_EVENT_PRESSED:
led_matrix_set_touch_active(true);
current_state = HANDWRITE_DRAWING;
last_point = point;
draw_point(canvas, &point, color); // 绘制点
DEBUG_TIMESTAMP("Started drawing");
break;

case LV_EVENT_PRESSING:
if (current_state == HANDWRITE_DRAWING) {
draw_line(canvas, &last_point, &point, color); // 绘制线段
last_point = point;
}
break;

case LV_EVENT_RELEASED:
led_matrix_set_touch_active(false);
current_state = HANDWRITE_IDLE;
DEBUG_TIMESTAMP("Finished drawing");
break;

default:
break;
}

lv_obj_invalidate(canvas); // 触发重绘
}

3. 与PC通信模块

  • 初始化串口通信,波特率为PC_COMM_BAUD
  • 将画布图像数据转换为二进制格式,并通过串口发送到PC端。
  • 接收PC端返回的识别结果,并解析数字和置信度。
// 初始化PC通信
int pc_comm_init(void) {
DEBUG_PRINTLN("PC communication initialized");
Serial.println("HELLO_FROM_ESP32");
Serial.flush();
return 0;
}

// 发送图像数据到PC进行识别
int pc_recognize_digit(lv_obj_t *canvas, pc_recog_result_t *result) {
if (!canvas || !result) {
DEBUG_PRINTLN("Invalid parameters for PC recognition");
return -1;
}

DEBUG_PRINTLN("\n=== Starting PC digit recognition ===");

lv_img_dsc_t* img = lv_canvas_get_img(canvas);
if (!img) {
DEBUG_PRINTLN("Failed to get canvas image");
return -1;
}

analyze_canvas(canvas); // 分析画布内容

// 设置通信状态
comm_state = PC_COMM_SENDING;
comm_start_time = millis();

// 清空串口缓冲区
while (Serial.available()) {
Serial.read();
}

// 发送识别请求
Serial.println("START_RECOGNITION");
Serial.printf("SIZE:%d,%d\n", img->header.w, img->header.h);
Serial.flush();

// 发送图像数据
for (int y = 0; y < img->header.h; y++) {
String row = "";
for (int x = 0; x < img->header.w; x++) {
lv_color_t color = lv_canvas_get_px(canvas, x, y);
bool is_background = (color.ch.red == 31 && color.ch.green == 63 && color.ch.blue == 31);
row += is_background ? "0" : "1";
}
Serial.println(row);

if (y % 10 == 0) {
delay(5);
Serial.flush();
}
}

Serial.println("END_RECOGNITION");
Serial.flush();

// 等待响应
DEBUG_PRINTLN("Waiting for response...");
comm_state = PC_COMM_WAITING;

// 清空之前的结果
result->digit = -1;
result->prob = 0.0;

// 等待响应数据
String response = "";
bool responseComplete = false;
unsigned long wait_start = millis();

while (!responseComplete && (millis() - wait_start < PC_COMM_TIMEOUT)) {
if (Serial.available()) {
String line = Serial.readStringUntil('\n');
line.trim();

DEBUG_PRINTF("Received: %s\n", line.c_str());

if (line.startsWith("RESULT:")) {
response = line;
responseComplete = true;

// 发送确认消息
Serial.println("RESULT_RECEIVED");
Serial.flush();

// 解析响应
String data = response.substring(7);
int commaPos = data.indexOf(',');
if (commaPos > 0) {
result->digit = data.substring(0, commaPos).toInt();
result->prob = data.substring(commaPos + 1).toFloat();
DEBUG_PRINTF("识别结果: 数字=%d, 置信度=%.3f\n", result->digit, result->prob);
comm_state = PC_COMM_IDLE;
return 0;
}
}
}
delay(10);
}

if (!responseComplete) {
DEBUG_PRINTLN("Response timeout!");
comm_state = PC_COMM_ERROR;
return -1;
}

DEBUG_PRINTLN("Failed to parse response");
comm_state = PC_COMM_ERROR;
return -1;
}

4. 手写识别模块

  • 使用PC识别,通过串口发送图像数据到PC端。
  • 接收PC端返回的识别结果,并在用户界面模块中显示。
  • 同时在LED矩阵上显示识别的数字。
// 识别按钮事件回调
void recog_btn_event_cb(lv_event_t *e) {
if (ui_state != UI_STATE_NORMAL || !g_canvas || !g_result_label) {
DEBUG_PRINTLN("Cannot recognize in current state or invalid objects");
return;
}

DEBUG_PRINTLN("Starting PC recognition process...");
ui_state = UI_STATE_BUSY;

// 清除之前的结果
lv_label_set_text(g_result_label, "...");
lv_obj_invalidate(g_result_label);

// 清空LED矩阵显示
led_matrix_clear();

// 给LVGL一些时间更新显示
lv_timer_handler();
delay(100);

pc_recog_result_t result;
int ret = pc_recognize_digit(g_canvas, &result);

if (ret == 0 && result.digit >= 0 && result.digit <= 9) {
char buf[8];
snprintf(buf, sizeof(buf), "%d", result.digit);
lv_label_set_text(g_result_label, buf); // 显示识别结果
led_matrix_show_digit(result.digit); // 在LED矩阵上显示数字
} else {
lv_label_set_text(g_result_label, "E");
}

lv_obj_invalidate(g_result_label);
lv_timer_handler();
ui_state = UI_STATE_NORMAL;
}

5. LED矩阵显示模块

  • 使用CrowPanel ESP32 HMI 4.3英寸显示屏开发板的硬件接口控制LED矩阵。
  • 在LED矩阵上显示识别的数字。
  • 支持触摸活动时暂停LED矩阵刷新。
// 初始化LED矩阵
void led_matrix_init() {
pinMode(DIN_PIN, OUTPUT);
pinMode(SRCLK_PIN, OUTPUT);
pinMode(RCLK_PIN, OUTPUT);

digitalWrite(DIN_PIN, LOW);
digitalWrite(SRCLK_PIN, LOW);
digitalWrite(RCLK_PIN, LOW);

led_matrix_clear(); // 清空显示
}

// 显示数字
int led_matrix_show_digit(int digit) {
if (digit < 0 || digit > 9) {
return -1; // 数字无效
}

currentDigit = digit; // 更新当前显示的数字
return 0;
}

// LED矩阵更新函数
void led_matrix_loop() {
if (touchActive) {
return; // 如果触摸活动,暂停LED刷新
}

unsigned long currentTime = millis();
if (currentTime - lastRefreshTime >= refreshInterval) {
if (currentDigit >= 0 && currentDigit <= 9) {
for (int row = 0; row < 8; row++) {
shiftOut16Bits(DIGITS[currentDigit][row]); // 发送当前行的数据
delayMicroseconds(1000); // 短暂延时
shiftOut16Bits(0x0000); // 清空数据
}
} else {
shiftOut16Bits(0x0000); // 清空LED矩阵
}
lastRefreshTime = currentTime;
}
}


六、实物效果图


七、遇到的问题及解决方法

  • 软件兼容性问题:在不同版本的库和工具之间存在一些兼容性问题,例如 LVGL 库更新导致的部分函数调用报错,Python 库版本不匹配导致的识别程序运行异常等。通过查阅官方文档、社区论坛以及进行版本回退或升级等操作,逐步解决了软件兼容性问题,保证了开发的顺利进行。
  • 识别准确率问题:在手写识别初期,发现对于某些书写风格独特的数字,识别准确率较低。通过对模型进行微调、增加训练数据的多样性,增加训练轮数以及优化图像预处理算法等措施,有效提高了识别准确率,使其能够更好地适应不同用户的书写习惯。


八、总结

通过本次项目,我积累了丰富的嵌入式开发和机器学习应用经验。在项目过程中,深刻认识到前期需求分析和详细设计的重要性,合理的模块划分和清晰的接口定义能够大大降低开发难度和后期维护成本。

未来,可以考虑进一步优化手写识别算法,提高对复杂背景和多样化书写风格的适应能力。还可以拓展系统的功能,如增加手写签名识别、简单图形绘制与识别等,以满足更多应用场景的需求。此外,探索将识别算法部分迁移到 ESP32 本地运行,减少对 PC 端的依赖,提升系统的独立性和便携性也是一个值得研究的方向。

附:如何运行程序

1.硬件连接

DIN_PIN(数据输入引脚):连接到显示屏开发板的GPIO 17。

SRCLK_PIN(移位寄存器时钟引脚):连接到显示屏开发板的GPIO 38。

RCLK_PIN(存储寄存器时钟引脚):连接到显示屏开发板的GPIO 37。

2. 配置和上传代码

2.1 下载项目代码

将所有项目文件(如Handwrite.ino、ui.h、handwriting.h等)保存到一个文件夹中。在Arduino IDE中打开Handwrite.ino文件。

2.2 配置开发板

在Arduino IDE中,选择“工具”->“开发板”,选择对应的ESP32 S3开发板,点击“上传”按钮,将代码上传到ESP32开发板如果上传成功,ESP32将重启并运行代码。开发板配置选项如图:


image.png

2.3 安装Python
确保你的计算机上安装了Python(推荐使用Python 3.8及以上版本)。你可以从Python官网下载并安装。

2.4 安装必要的Python库

程序依赖于多个Python库,包括torch(PyTorch)、torchvisionmatplotlibPillow(PIL)、numpyserial等。你可以通过以下命令安装这些库(如果尚未安装):

pip install torch torchvision matplotlib pillow numpy pyserial


2.5 运行程序

在命令行中运行以下命令启动程序:

python fix_mnist_bias.py

程序运行后,会执行以下操作:

  1. 检查预训练模型:如果模型文件存在,则直接加载模型;否则,训练一个新的模型并保存。
  2. 测试模型:程序会使用MNIST测试集随机选择一些样本进行测试,并显示测试结果。
  3. 串口通信:程序会列出可用的串口,并提示你选择一个用于与ESP32通信的串口。
  4. 接收数据并识别:程序会等待ESP32发送手写图像数据,处理这些数据并使用预训练的模型进行识别,最后将识别结果通过串口发送回ESP32。
附件下载
Handwrite.zip
团队介绍
个人
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号