2025寒假练 - 用 CrowPanel ESP32 Display 4.3英寸HMI开发板 实现手写识别显示
该项目使用了CrowPanel ESP32 Display 4.3英寸HMI开发板,实现了手写识别显示的设计,它的主要功能为:基于LVGL显示手写数字、基于神经网络识别数字、点阵屏显示。
标签
嵌入式系统
ESP32
lvgl
神经网络
74HC595
Display
Glass
更新2025-03-13
华北电力大学
77

1. 项目介绍

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

2.硬件介绍

2.1 CrowPanel ESP32 Display 4.3英寸HMI开发板

主处理器:ESP32-S3-WROOM-1-N4R2,主频:240MHz, Flash:4MB, PSRAM:2MB

屏幕:TFT-LCD屏幕,分辨率:480*272,触摸类型:电阻式触摸屏,屏幕驱动:NV3047

其他接口:1*TF卡槽,2*GPIO,1*Speak,2*UART1,1*UART0

image.png

2.2 LED点阵灯板

FrEtqxIPfaB6r8uQO5tukmNeGVAF

资源:8*8共64颗单色LED灯,封装大小06038个NPN三极管9013和16个0603封装的电阻,用于驱动LED阵列,每排8个LED提供点亮所需要的电流;两颗级联的74HC595D;2颗0603封装的电源去耦电容;2个5管脚直插的连接器用于与ESP32开发板对接。

ESP32开发板和灯板连接说明:LED灯板VCC和GND连接ESP32开发板的3V3和GND,->D连接IO17,SRCLR、RCLK分别连接IO37、38

74HC595管脚说明:

14脚:SER,串行数据输入引脚。

13脚:OE,输出使能控制脚,低电平有效。

12脚:RCLK,存储寄存器时钟输入引脚。上升沿时,数据从移位寄存器转存到存储寄存器。

11脚:SRCLK,移位寄存器时钟引脚,上升沿时,移位寄存器中的bit 数据整体后移,并接受新的bit(从SER输入)。

10脚:SRCLR,低电平时,清空移位寄存器中已有的bit数据,一般不用,接高电平即可。

9 脚 :串行数据出口引脚。当移位寄存器中的数据多于8bit时,会把已有的bit“挤出去”,就是从这里出去的。用于595的级联。

QA~QH:并行输出引脚。

3.方案框图和项目设计思路介绍

image.png

1)通过电阻式触摸笔在屏幕的数字手写区域输入0~9任意一个数字,屏幕对应显示手写数字,并将200*200的手写区域内图像压缩保存为28*28的二值化图像数据;

(2)点击识别按钮,将28*28的二值化图像数据按照相应格式输入已经训练好的神经网络中,经过计算后,输出识别的数字,并通过LED点阵显示;

(3)点击清除按钮,清除屏幕上的手写数据。

4.软件流程图和关键代码介绍

4.1 软件流程图

image.png

4.2 触摸回调函数

该函数作为LVGL输入设备的核心回调函数,负责处理触摸屏输入事件并实现数字书写区域的实时交互功能。

当检测到有效触摸信号时,它会持续跟踪用户的书写动作:首先通过判断坐标变动阈值(≥20像素)识别新笔画的开始,随后将触摸坐标映射到200×200的数字书写区域内显示,并转换为对应的28×28像素矩阵索引。

该函数利用LVGL画布的双端点连线功能实现流畅的轨迹绘制,并在检测到触摸离开识别区域时重置绘制状态,最终通过全局变量同步更新显示界面和神经网络输入数据,完成从手写输入到数字识别的完整链路处理。同时,在绘制过程中,通过3×3邻域填充算法模拟线条粗细,将用户绘制的轨迹实时转化为MNIST数据集的1维28*28矩阵。

void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data)
{
  if (touch_has_signal())
  {
    if (touch_touched())
    {
      data->state = LV_INDEV_STATE_PR;
      /*Set the coordinates*/
      // 【坐标处理】检测是否完成长距离移动(视为新笔画开始)
      if((data->point.x - touch_last_x >=20 || touch_last_x - data->point.x >=20)
      && (data->point.y - touch_last_y >= 20 || touch_last_y - data->point.y >= 20))
      {
        points[1] = {-1,-1};
      }
      data->point.x = touch_last_x;
      data->point.y = touch_last_y;
      // 【区域限制】只在数字识别区域内响应
      if(data->point.x > 140 && data->point.x < 340 && data->point.y > 36 && data->point.y < 236)
      {
        // 绘制逻辑
        points[0] = points[1];
        points[1] = {(short)(data->point.x - 140), (short)(data->point.y - 36)};
        // 200*200压缩至28*28 像素填充:在3x3邻域设置1(模拟线条粗细)
        for(int i=-1; i<=1; i++)
        {
          for(int j=-1; j<=1; j++)
          {
            mnist[(int)((points[1].y*28/200+i)*28 + points[1].x*28/200+j)] = 1;
          }
        }
        // 绘制线段
        if(points[0].x != -1)
        {
          lv_canvas_draw_line(write_area, points, 2, &line_dsc); // 绘制线条,传入点和样式
        }
      }
      else
      {
        // 离开识别区域时重置绘制
        points[1] = {-1,-1};
      }
    }
  }
  else
  {
    data->state = LV_INDEV_STATE_REL;
  }
}

4.3 按钮回调函数

该函数作为LVGL事件驱动的核心回调,负责响应界面按钮点击事件并执行相应操作。

当识别按钮(btn1)被触发时,调用Eloquent TinyML框架对当前手写数字矩阵进行推理,通过遍历预测概率数组确定最高置信度的分类结果,并更新显示标签和预测标志位以触发数码管显示;

当清空按钮(btn2)被点击时,通过重置画布背景、清除像素矩阵数据和双端点坐标来实现书写区域的清空,同步更新界面状态和输入数据。函数通过全局变量(pred_num/pred_flag/mnist)与显示模块、模型推理模块和数码管驱动模块进行状态同步,利用LVGL的异步事件机制确保UI交互的实时性和流畅性,同时要求模型推理耗时需控制在UI刷新周期内以避免卡顿。

void (*p_my_gui)(void);
static void my_event_cb(lv_event_t * e)
{
    lv_obj_t *target  = lv_event_get_target(e);
    if(target == btn1)
    {
      /* 检验压缩的数据是否正确 */
      // for(int i=0; i<28*28; i++)
      // {
      //   if (i%28==0)
      //   {
      //     Serial.printf("\r\n");
      //   }
       
      //   Serial.printf("%d ", mnist[i]);
      // }


      // 【模型推理】使用当前输入数据进行预测
      tf.predict(mnist, predicted);
      // 【结果解析】寻找最大概率的分类结果
      float max = -100;
      for(int i=0; i<10; i++)
        if(predicted[i]>=max)
          pred_num = i, max = predicted[i];
      // 【界面更新】显示识别结果
      lv_label_set_text_fmt(label_num, "NUM: %d", pred_num);
      pred_flag = 1;
    }
    else if(target == btn2)
    {
       // 【界面重置】清除数字书写区域
      lv_canvas_fill_bg(write_area, lv_color_white(), LV_OPA_COVER);
      // 【数据重置】初始化输入矩阵和绘制状态
      points[1] = {-1,-1};
      memset(mnist, 0, sizeof(mnist));
      // 【界面更新】显示清空状态
      lv_label_set_text_fmt(label_num, "NUM:");
      pred_flag = 0;
    }
}

4.4 74HC595驱动点阵屏

该函数通过HC595芯片向LED点阵发送指定数字的显示数据。

它从预定义的段码表hc595_num中读取数字对应的二进制段码,通过两次HC595_Send_Byte配合锁存操作,逐行点亮显示设备上的对应段组,最终实现目标数字的动态显示。

/* 发送数字 */
void HC595_Send_Num(uint8_t num)
{
    uint8_t i;
    uint8_t j;
    uint8_t raw = 0x01;
    uint8_t c = 0;
    for(i = 0; i < 8; i++)
    {
        c = hc595_num[num][i];
        HC595_Send_Byte(raw);
        HC595_Send_Byte(c);
        HC595_Save();
        raw <<= 1;
    }
}

下面是根据8*8的像素矩阵,提前预设好数字0~9以及不显示数字时,控制行显示的HC595芯片8位存储寄存器的输出。

const uint8_t hc595_num[11][8] = {
    {0x00, 0x3C, 0x66, 0x42, 0x42, 0x42, 0x66, 0x3C},//0
    {0x10, 0x18, 0x14, 0x10, 0x10, 0x10, 0x10, 0x7E},//1
    {0x00, 0x7E, 0x40, 0x40, 0x7E, 0x02, 0x02, 0x7E},//2
    {0x00, 0x7E, 0x40, 0x40, 0x7E, 0x40, 0x40, 0x7E},//3
    {0x18, 0x14, 0x12, 0x11, 0xFF, 0x10, 0x10, 0x10},//4
    {0x00, 0x7E, 0x02, 0x02, 0x7E, 0x40, 0x40, 0x7E},//5
    {0x00, 0x7E, 0x02, 0x02, 0x7E, 0x42, 0x42, 0x7E},//6
    {0x7E, 0x40, 0x20, 0x20, 0x10, 0x10, 0x08, 0x08},//7
    {0x00, 0x7E, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x7E},//8
    {0x00, 0x7E, 0x42, 0x42, 0x7E, 0x40, 0x40, 0x7E},//9
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}//不显示
};

4.5 神经网络

image.png

通过Python,基于Keras框架构建了一个神经网络模型结构如上图所示,考虑到要在esp32上部署,所以使用了简单的结构,使用MNIST手写数字数据集进行训练,最后可以达到98%的准确率,将训练好的模型保存为.h5文件。通过tinymlgen库的port函数将.h5文件转化为.h文件。

#ifdef __has_attribute
#define HAVE_ATTRIBUTE(x) __has_attribute(x)
#else
#define HAVE_ATTRIBUTE(x) 0
#endif
#if HAVE_ATTRIBUTE(aligned) || (defined(__GNUC__) && !defined(__clang__))
#define DATA_ALIGN_ATTRIBUTE __attribute__((aligned(4)))
#else
#define DATA_ALIGN_ATTRIBUTE
#endif


const unsigned char model_data[] DATA_ALIGN_ATTRIBUTE = {0x1c, 0x00, ...... 0x00, 0x03};//此处省略
const int model_data_len = 6664;

5.功能展示图及说明

6b5f9e22a0fe645dadd9f2362926cb8.jpg

功能说明

4a09e543bd323f5f9ff1445a9e2fbe9.jpg

功能展示图(成功识别“6”)

6. 项目中遇到的难题和解决方法

6.1 触摸区域坐标映射与书写连贯性 ​

问题:电阻式触摸屏存在灵敏度差异,手写时的小幅移动易产生误判,且200×200区域需压缩为28×28像素时可能出现线条断裂或粗细不均。 ​

解决方案:引入移动距离阈值(≥20像素)​判断新笔画开始,避免噪声干扰。 采用3×3邻域填充算法模拟粗细,平衡线条连续性与识别精度。 在LVGL中启用双端点连线功能,通过lv_canvas_draw_line()实现平滑轨迹绘制。

6.2 神经网络模型在嵌入式端的部署

问题:嵌入式设备的Flash和PSRAM容量有限,传统模型占用内存过大。​模型训练框架(如Keras)与嵌入式推理框架(如TensorFlow Lite)之间的格式转换问题

解决方案:对神经网络模型重新设计,使其结构更加简单,权重参数数量更加精简。通过tinymlgen库的port函数将模型文件转化成C语言文件,配合Eloquent TinyML库使用训练好的模型。 ​

7.对本次活动的心得体会

7.1 项目收获​

嵌入式GUI开发:掌握了LVGL的事件驱动机制与硬件抽象层设计,理解了电阻式触摸屏的坐标转换逻辑。

​边缘AI落地:通过Keras-TinyML框架实现了轻量化模型训练与部署,深刻体会到模型压缩对嵌入式端的重要性。

​硬件驱动调试:成功解决了HC595级联控制中的时序问题,提升了LED显示的可靠性。

7.2 改进建议

模型优化: 引入迁移学习​(如基于MNIST预训练的CNN模型),提升复杂手写数字的识别率。增加数据增强​(旋转、缩放)训练,增强模型的泛化能力。 ​

硬件扩展: 优化LED驱动电路,采用MOSFET替代三极管以降低功耗。

软件架构: 将全局变量替换为消息队列​(如FreeRTOS的xQueueSend),提升多任务并发稳定性。 增加错误处理机制​(如SPI通信失败重试、模型加载异常检测)。

​用户体验: 添加书写实时反馈​(如当前笔画粗细调节、数字轮廓显示)。 设计多语言支持​(如中文数字标签),扩大应用场景。

附件下载
CrowPanel_ESP32_4.3_Glass.zip
内有README文件,请认真阅读
团队介绍
华北电力大学 电气与电子信息工程学院 电子信息工程 王亚楠
团队成员
Glass
和自己相比,有进步吗?
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号