2025寒假练 - 使用“CrowPanel ESP32 Display 4.3”实现手写数字识别显示
该项目使用了CrowPanel ESP32 Display 4.3,实现了手写数字识别显示的设计,它的主要功能为:在LCD屏幕上设定一个正方形的写字区域,在写字区域里书写0-9的数字,esp32运行神经网络算法对数字进行识别。 该项目使用了74HC595驱动的led点阵模块,实现了数字显示的设计,它的主要功能为:使用esp32驱动74HC595芯片,在灯板上显示数字。。
标签
嵌入式系统
显示
开发板
lihuahua
更新2025-03-13
37

1、项目介绍

(1)使用LVGL编程,设计屏幕的背景图片,添加“识别”、“清除”按钮;

(2)LCD屏幕上设定一个正方形的写字区域,用于0-9数字的手写输入;

(3)esp32中运行神经网络算法,算法的输入为28*28的手写数字点阵数据,输出识别出的数字;

(4)esp32驱动led点阵的级联74HC595芯片,显示神经网络算法输出的数字。

2、硬件介绍

2.1 CrowPanel ESP32 Display 4.3

屏幕TFT4.3"分辨率480*272;触摸类型Resistive Touch;屏幕驱动:NV3047

芯片ESP32-S3-WROOM-1-N4R2主频240 MHzFlash4MBSRAM512KBROM384KBPSRAM2MB

开发板对外接口:1*UART0, 2*UART1,2*GPIO, 1*Battery

image.pngimage.png

2.2 点阵LED74HC595驱动电路

image.png

74HC595(串行输入/并行输出移位寄存器)引脚定义:

(1)SER (引脚 14):串行数据输入引脚。通过该引脚输入数据位,数据依次进入HC595的内部寄存器。当输入的数据超过8bit时,数据将从芯片U1QH_2引脚输出到芯片U2SER引脚上。

(2)SRCLK (引脚 12):移位时钟引脚。每当此引脚接收到一个脉冲时,寄存器将通过 SER 引脚接受下一个输入位,并向下移动现有位。

(3)RCLK (引脚 13):锁存时钟引脚。通过在该引脚上施加脉冲,可以将移位寄存器中的数据锁存到输出引脚(Q0-Q7)上,更新输出状态。

(4)OE (引脚 11):输出使能引脚。当此引脚低电平时(通常连接到 GND),输出是使能的,能够输出寄存器的数据。当此引脚为高电平时,所有输出都被禁用,输出处于高阻状态。

(5)SRCLR (引脚 10):移位寄存器清除引脚。当该引脚被拉低时,寄存器的所有输出(Q0-Q7)将被清除为低电平0

芯片U1QH_2引脚连接到芯片U2SER引脚上,需要控制的引脚:DINSRCLKRCLK。当芯片U1的输出为1时,对应LED阳极为高电平,当芯片U2的输出为1时,对应LED阴极为低电平。

2.3 TFT驱动电路

image.png

2.4 触摸驱动电路XPT2046

image.png

触摸芯片与ESP32的接口定义见文件touch.h

#define TOUCH_XPT2046
#define TOUCH_XPT2046_SCK 12
#define TOUCH_XPT2046_MISO 13
#define TOUCH_XPT2046_MOSI 11
#define TOUCH_XPT2046_CS 0
#define TOUCH_XPT2046_INT 36

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

image.png

(1)本项目通过电阻触摸屏输入0-9的手写数字,获得200*200的二值化笔画矩阵(有笔画区域用1表示,无笔画区域用0表示),然后对笔画数据进行压缩获得28*28的二值化笔画矩阵,将压缩后的图像数据输入神经网络,从而获得推衍的数字;

(2)神经网络推演得到的数字通过led点阵显示;

(3)通过触摸屏幕上的“识别”按钮,用于启动一次神经网络计算;

(4)通过触摸屏幕上的“清除”按钮,用于清除LCD屏幕上的笔画数据。

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

4.1软件流程图

 image.png

4.2 74HC595驱动

使用软件生成数字0~9对应的模值,采用按行扫描的方式逐行显示数字,行刷新率设置为1kHz。程序中先输出阳极74HC595的控制数据,然后输出阴极74HC595的控制数据。

image.png

点阵的驱动函数在dotmatrix.c文件中:

#include "dotmatrix.h"
#include "esp32-hal-gpio.h"
// #include <Arduino_GFX_Library.h>

// 数字 0-9 的显示模式,8x8 矩阵
const uint8_t data1[10] = {     //阳极数据
    0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};

const uint8_t data2[11][8] = {      //阴极数据
    {0x00,0x3C,0x24,0x24,0x24,0x24,0x3C,0x00},  //数字0
    {0x00,0x18,0x18,0x18,0x18,0x18,0x18,0x00},  //数字1
    {0x00,0x3C,0x20,0x20,0x3C,0x04,0x04,0x3C},  //数字2
    {0x00,0x3C,0x20,0x20,0x3C,0x20,0x20,0x3C},  //数字3
    {0x00,0x24,0x24,0x24,0x3C,0x20,0x20,0x20},  //数字4
    {0x00,0x3C,0x04,0x04,0x3C,0x20,0x20,0x3C},  //数字5
    {0x00,0x3C,0x04,0x04,0x3C,0x24,0x24,0x3C},  //数字6
    {0x00,0x3C,0x20,0x20,0x20,0x20,0x20,0x20},  //数字7
    {0x00,0x3C,0x24,0x24,0x3C,0x24,0x24,0x3C},  //数字8
    {0x00,0x3C,0x24,0x24,0x3C,0x20,0x20,0x3C},  //数字9
};

void dotmatrix_gpio_init(void){
    // #define DIN_PIN    37/* DIN 引脚   J1  阴极 */
    // #define CLOCK_PIN  17/* 时钟引脚      SRCLK*/
    // #define LATCH_PIN    18/* 锁存引脚    RCLK*/
    pinMode(DIN_PIN, OUTPUT);
    pinMode(CLOCK_PIN, OUTPUT);
    pinMode(LATCH_PIN, OUTPUT);
}

void sendData(uint8_t data,uint8_t gpio) {
    for (int i = 0; i < 8; i++) {
        // 发送每一位
        if (data & (0x80 >> i)) {
            // 设置 gpio 高
            digitalWrite(gpio, 1);
        } else {
            // 设置 gpio 低
            digitalWrite(gpio, 0);
        }

        // 脉冲时钟引脚
        digitalWrite(CLOCK_PIN, 1);
        digitalWrite(CLOCK_PIN, 0);
    }
}

void latchData() {
    // 锁存数据到输出引脚
    digitalWrite(LATCH_PIN, 1);
    digitalWrite(LATCH_PIN, 0);
}

void displayNumber(uint8_t number,uint8_t frame) {
    if (number > 9) {
        return; // 不支持的数字
    }
    sendData(data1[frame],DIN_PIN); // 发送阳极数据
    sendData(data2[number][frame],DIN_PIN); // 发送阴极数据
    // 锁存数据以更新输出
    latchData();
}
void api_displayNumber(uint8_t number)      //
{
    static uint16_t frame = 0;
    // delay(2);   //延迟2ms
    //控制刷新率
    displayNumber(number,frame);
    if (frame >= 7)     frame = 0;
    else    frame ++;            
}

在main.c文件中创建了点阵扫描任务0,将该任务放在内核0中运行,刷新一幅画面需要8ms,即屏幕的刷新率为125hz。该任务从队列xqueue0中读取待显示的数字。

TaskHandle_t Task0;
TaskHandle_t Task1;
xQueueHandle xqueue0;  //创建的测试队列句柄,我们定义数据为int型
//Task0code: 点阵扫描
void Task0code( void * pvParameters ){
  unsigned char result = 10;
  dotmatrix_gpio_init();  //hc595引脚初始化
  TickType_t xTicksToWait = pdMS_TO_TICKS(0);
  BaseType_t xStatus;  //用于状态返回在下面会用到

  for(;;){
    xStatus = xQueueReceive(xqueue0, &result, xTicksToWait);  //从队列0中取一条数据
    api_displayNumber(result);
    delay(1);
  }
}

4.3 触摸屏扫描以及显示

触摸扫描函数由函数lv_timer_handler()周期性调用,触摸扫描函数中首先获取触摸位置的x、y坐标,然后判断触摸位置是否为设定的200*200像素大小的输入方形区域内。若是,则在该坐标位置绘制5*5大小的原点,然后将有笔画的位置坐标进行压缩,然后存储在image数组内。(image二维数组可理解为一张黑白图像,值为1的位置表示有笔画,值为0的位置表示无笔画)

 //触摸扫描
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*/
      data->point.x = touch_last_x;
      data->point.y = touch_last_y;

      // 左上角:        // x:240 - 100      // y:136 - 100
      // 右下角:        // x:240 + 100      // y:136 + 100
      if((data->point.x >= (240 - 100))  && (data->point.x <= (240 + 100))  &&  (data->point.y >= (136 - 100))  && (data->point.y <= (136 + 100))){
        int X_width=0,Y_width=0;

        lv_obj_t * my_dot = lv_obj_create(lv_scr_act()); // 创建一个对象
        lv_obj_set_size(my_dot, 5, 5); // 设置大小为 5x5 像素
        lv_obj_set_style_bg_color(my_dot, lv_color_black(), 0); // 设置背景颜色为黑色
        lv_obj_set_style_radius(my_dot, 3, 0); // 设置圆角半径,使其成为圆形
        lv_obj_set_pos(my_dot,data->point.x,data->point.y); // 设置 x = 50, y = 100

        //绘制点
        for(X_width=-1;X_width<=1;X_width+=1)
        {
          for(Y_width=-1;Y_width<=1;Y_width+=1)
          {           
            image[(data->point.y + Y_width - (136 - 100))/MUTILP][(data->point.x + X_width - (240 - 100))/MUTILP] = 1;  //保存笔画数据
          }
        }
      }
    }
    else if (touch_released())
    {
      data->state = LV_INDEV_STATE_REL;
    }
  }
  else
  {
    data->state = LV_INDEV_STATE_REL;
  }
}

4.4 数字神经网络识别

本项目采用具有sigmoid 隐藏神经元和线性输出神经元的两层前馈网络估计手写数字。输入层为28x28个手写数字像素点;输出层为10个浮点数,数值最大的对应识别的数字。使用matlab中神经网络工具箱对权值,偏移值进行训练。

image.png

训练数据的获取:在esp32屏幕上随机手写数字,将28*28像素点输出打印到串口调试助手中,进而得到800组手写数字的原始数据(每个数字由784个像素点组成)。

image.png

训练结果:R=0.78

image.png

以下代码为具有sigmoid 隐藏神经元的神经网络计算函数myNeuralNetworkFunction(),x1为待识别数字的像素,b_y1为识别结果。

 /* Function Definitions */
/*
 * MYNEURALNETWORKFUNCTION neural network simulation function.
 *
 *  Auto-generated by MATLAB, 11-Jan-2025 14:53:40.
 *
 *  [y1] = myNeuralNetworkFunction(x1) takes these arguments:
 *    x = Qx784 matrix, input #1
 *  and returns:
 *    y = Qx10 matrix, output #1
 *  where Q is the number of samples.
 *
 * Arguments    : const float x1[784]
 *                float b_y1[10]
 * Return Type  : void
 */
void myNeuralNetworkFunction(const float x1[784], float b_y1[10])
{
double b_b[10];
  float xp1[579];
  float b[50];
  float f;
  int i;
  int k;


  /*  ===== NEURAL NETWORK CONSTANTS ===== */
  /*  Input 1 */
  /*  Layer 1 */
  /*  Layer 2 */
  /*  Output 1 */
  /*  ===== SIMULATION ======== */
  /*  Dimensions */
  /*  samples */
  /*  Input 1 */
  /*  Remove Constants Input Processing Function */
  /*  ===== MODULE FUNCTIONS ======== */
  /*  Map Minimum and Maximum Input Processing Function */
  for (k = 0; k < 579; k++) {
    xp1[k] = x1[iv[k]] * 2.0F - 1.0F;
  }


  /*  Layer 1 */
  /*  Sigmoid Symmetric Transfer Function */
  for (k = 0; k < 50; k++) {
    f = 0.0F;
    for (i = 0; i < 579; i++) {
      f += fv[k + 50 * i] * xp1[i];
    }


    b[k] = (float)exp(-2.0F * ((float)b_a[k] + f));
  }


  /*  Layer 2 */
  memcpy(&b_b[0], &a[0], 10U * sizeof(double));
  for (i = 0; i < 50; i++) {
    b[i] = 2.0F / (b[i] + 1.0F) - 1.0F;
  }


  /*  Output 1 */
  /*  Map Minimum and Maximum Output Reverse-Processing Function */
  for (k = 0; k < 10; k++) {
    f = 0.0F;
    for (i = 0; i < 50; i++) {
      f += fv1[k + 10 * i] * b[i];
    }


    b_y1[k] = (((float)b_b[k] + f) - -1.0F) / 2.0F;
  }
}

findMax()函数用于在神经网络输出数组b_y1中查找出最大值,对应索引表示识别出的数字。

float imagex[INPUT_SIZE]={0};           //神经网络输入层
unsigned char image[28][28];        //28*28像素点缓冲区
float output[10] = {0.0f};

unsigned char findMax(float *fx)        //寻找输出层中的最大值
{
    unsigned char i,k=0;
    float maxNum=0.0;
    maxNum=fx[0];
    for(i=1;i<10;i++)
    {
        if(maxNum<=fx[i])
        {
            maxNum=fx[i];
            k=i;
        }
    }
    return k;
}

//将图像缓冲区矩阵变为输入列向量
void ImageToArray(){            //输入层
    unsigned short i,j,k=0;
    for(i=0;i<28;i++){
        for(j=0;j<28;j++){
            imagex[k]=image[i][j];
            // inputX[k] = imagex[k];
            k++;
        }
    }
}

void ClearImageArray(){         //清除28*28像素点缓冲区
    unsigned short i,j,k;
    for(i=0;i<28;i++){
        for(j=0;j<28;j++){
            image[i][j]=0;  
            imagex[k] = 0;
            k++;
        }  
    }
}

void NN_CAL(unsigned char * res){
    ImageToArray();                     //将图像缓冲区矩阵变为输入列向量
    unsigned char i,j;
    for(i = 0;i<28;i++){                //打印图像缓冲区数据
        for(j = 0;j<28;j++){
            printf("%d ",(int)image[i][j]);
        }
        // printf("\n");
    }
    printf("\n");
    myNeuralNetworkFunction(imagex,output);     //核心代码
    *res = findMax(output);
}

main.c函数中创建了任务1,该任务在内核1中运行。当清除绘图按键按下时,调用ClearImageArray()清除image图像数据,然后重新显示ui。当识别按键按下时,调用NN_CAL()函数进行神经网络数字识别,然后将输出结果写到队列xqueue0中。

//Task1code: lvgl
void Task1code( void * pvParameters ){
  unsigned char result = 10;
  BaseType_t xStatus;  //用于状态返回在下面会用到
  TickType_t xTicksToWait = pdMS_TO_TICKS(0);   // 阻止任务的时间

  for(;;){
    lv_timer_handler();   //在主循环中被调用,以确保 LVGL 的内部机制能够正常运行并按照设定的时间间隔更新界面元素
   
    if(global_flag == 1){   //清除绘图
      // printf("Clean!\n");
      ClearImageArray();
      ui_init();
      global_flag = -1;
    }else if(global_flag == 0){//识别
     
      NN_CAL(&result);
      xStatus = xQueueSendToFront( xqueue0, &result, xTicksToWait );
      // printf("Identify! result = %d\n",result);
      // printf("Identify! sigmoid = %f\n",sigmoid(-2));
      global_flag = -1;
    }
  }
}

5、功能展示图及说明

首先在正方向框中手写数字,按下按键“Identify”,右边led点阵上会显示数字,按下按键“Clean”,lcd屏幕上手写数字清除。

识别数字0:

image.png

识别数字1:

image.png

识别数字2:

image.png

识别数字3:

image.png

识别数字4:

image.png

识别数字5:

image.png

识别数字6:

image.png

识别数字7:

image.png

识别数字8:

image.png

识别数字9:

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

(1)使用板卡官方vscode+IDF环境下的例程,程序可以编译烧录,但是板卡屏幕就是不显示图像。后面改用Platformio环境进行开发。

(2)本人对lvgl不太熟悉,故采用SquareLine Studio软件设计用户UI以及用户按键,生成lvgl的c代码,放置板卡中运行。

(3)使用matlab神经网络工具箱对mnist手写数字数据进行训练,但将训练好的神经网络放在esp32板卡中运行时,始终无法识别出正确的数字。后来自己手写800个数字,将每个手写数字的28*28像素数据通过串口发送到电脑上进行训练,手写数字识别正确率提升至78%。

7、对本次活动的心得体会(包括意见或建议)

(1)通过本次活动学习了ESP32的开发环境以及程序开发方式;

(2)学会使用SquareLine Studio软件配置lcd屏幕ui界面;

(3)学会了手写数字神经网络的训练,以及算法在esp32上的部署。



附件下载
CrowPanel_ESP32_4.3_PIO_Demo_my4.zip.001
需要对文件CrowPanel_ESP32_4.3_PIO_Demo_my4.zip.001和文件CrowPanel_ESP32_4.3_PIO_Demo_my4.zip.002一起进行解压缩
CrowPanel_ESP32_4.3_PIO_Demo_my4.zip.002
需要对文件CrowPanel_ESP32_4.3_PIO_Demo_my4.zip.001和文件CrowPanel_ESP32_4.3_PIO_Demo_my4.zip.002一起进行解压缩
团队介绍
个人
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号