1、项目介绍
(1)使用LVGL编程,设计屏幕的背景图片,添加“识别”、“清除”按钮;
(2)在LCD屏幕上设定一个正方形的写字区域,用于0-9数字的手写输入;
(3)esp32中运行神经网络算法,算法的输入为28*28的手写数字点阵数据,输出识别出的数字;
(4)esp32驱动led点阵的级联74HC595芯片,显示神经网络算法输出的数字。
2、硬件介绍
2.1 CrowPanel ESP32 Display 4.3
屏幕:TFT,4.3",分辨率:480*272;触摸类型:Resistive Touch;屏幕驱动:NV3047
芯片:ESP32-S3-WROOM-1-N4R2,主频:240 MHz;Flash:4MB;SRAM:512KB;ROM:384KB;PSRAM:2MB
开发板对外接口:1*UART0, 2*UART1,2*GPIO, 1*Battery
2.2 点阵LED的74HC595驱动电路
74HC595(串行输入/并行输出移位寄存器)引脚定义:
(1)SER (引脚 14):串行数据输入引脚。通过该引脚输入数据位,数据依次进入HC595的内部寄存器。当输入的数据超过8bit时,数据将从芯片U1的QH_2引脚输出到芯片U2的SER引脚上。
(2)SRCLK (引脚 12):移位时钟引脚。每当此引脚接收到一个脉冲时,寄存器将通过 SER 引脚接受下一个输入位,并向下移动现有位。
(3)RCLK (引脚 13):锁存时钟引脚。通过在该引脚上施加脉冲,可以将移位寄存器中的数据锁存到输出引脚(Q0-Q7)上,更新输出状态。
(4)OE (引脚 11):输出使能引脚。当此引脚低电平时(通常连接到 GND),输出是使能的,能够输出寄存器的数据。当此引脚为高电平时,所有输出都被禁用,输出处于高阻状态。
(5)SRCLR (引脚 10):移位寄存器清除引脚。当该引脚被拉低时,寄存器的所有输出(Q0-Q7)将被清除为低电平0。
芯片U1的QH_2引脚连接到芯片U2的SER引脚上,需要控制的引脚:DIN、SRCLK、RCLK。当芯片U1的输出为1时,对应LED阳极为高电平,当芯片U2的输出为1时,对应LED阴极为低电平。
2.3 TFT驱动电路
2.4 触摸驱动电路XPT2046
触摸芯片与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、方案框图和项目设计思路介绍
(1)本项目通过电阻触摸屏输入0-9的手写数字,获得200*200的二值化笔画矩阵(有笔画区域用1表示,无笔画区域用0表示),然后对笔画数据进行压缩获得28*28的二值化笔画矩阵,将压缩后的图像数据输入神经网络,从而获得推衍的数字;
(2)神经网络推演得到的数字通过led点阵显示;
(3)通过触摸屏幕上的“识别”按钮,用于启动一次神经网络计算;
(4)通过触摸屏幕上的“清除”按钮,用于清除LCD屏幕上的笔画数据。
4、软件流程图和关键代码介绍
4.1软件流程图
4.2 74HC595驱动
使用软件生成数字0~9对应的模值,采用按行扫描的方式逐行显示数字,行刷新率设置为1kHz。程序中先输出阳极74HC595的控制数据,然后输出阴极74HC595的控制数据。
点阵的驱动函数在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中神经网络工具箱对权值,偏移值进行训练。
训练数据的获取:在esp32屏幕上随机手写数字,将28*28像素点输出打印到串口调试助手中,进而得到800组手写数字的原始数据(每个数字由784个像素点组成)。
训练结果:R=0.78
以下代码为具有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:
识别数字1:
识别数字2:
识别数字3:
识别数字4:
识别数字5:
识别数字6:
识别数字7:
识别数字8:
识别数字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上的部署。