欢迎光临
我们一直在努力

AI在嵌入式端的部署:基于STM32F407的手写数字识别(TensorFlow Lite Micro)

文章目录

    • 一、前言
    • 二、前期准备
      • 2.1 硬件清单
      • 2.2 软件清单
      • 2.3 核心原理
        • 2.3.1 手写数字识别核心(MNIST数据集)
        • 2.3.2 TensorFlow Lite Micro(TFLM)原理
        • 2.3.3 整体流程
    • 三、模型训练与转换(关键前置步骤)
      • 3.1 Python环境配置
      • 3.2 训练MNIST模型并转换为TFLite
    • 四、TFLM框架移植到Keil工程
      • 4.1 TFLM源码下载与整理
      • 4.2 Keil工程配置
    • 五、底层驱动开发(核心基础)
      • 5.1 延时驱动(bsp_delay.c/h)
      • 5.2 串口驱动(bsp_uart.c/h,调试用)
      • 5.3 LCD+触摸屏驱动(ILI9488+XPT2046)
        • 5.3.1 LCD驱动(lcd.c/h)
        • 5.3.2 触摸屏驱动(touch.c/h)
      • 5.4 手写数字采集与预处理
    • 六、TFLM模型推理代码
      • 6.1 推理核心代码(tflm_infer.c/h)
    • 七、完整整合与测试
      • 7.1 主函数代码(main.c)
      • 7.2 整体推理流程Mermaid图
      • 7.3 编译与下载步骤
    • 八、常见问题排查
      • 8.1 触摸屏采集不到轨迹
      • 8.2 TFLM推理失败(串口打印“张量分配失败”)
      • 8.3 识别结果错误/准确率低
      • 8.4 Keil编译报错“找不到tflite/xxx.h”
    • 九、总结
      • 关键点回顾
      • 扩展方向

一、前言

嵌入式AI(端侧AI)是当前物联网领域的核心趋势之一,其核心是将训练好的人工智能模型部署到资源受限的微控制器(MCU)上,实现本地推理,无需依赖云端。STM32F407作为经典的Cortex-M4内核MCU(1MB Flash、192KB RAM),结合Google推出的TensorFlow Lite Micro(TFLM) 轻量化框架,完全可以实现手写数字识别这类轻量级AI任务。

本教程面向零基础嵌入式开发者,从模型训练/转换、TFLM移植、底层驱动开发、手写数据采集、模型推理全流程讲解,所有代码均可直接复制使用,最终实现“STM32F407触摸屏手写数字→本地推理→LCD显示识别结果”的完整功能,让你快速掌握嵌入式AI部署的核心方法。

二、前期准备

2.1 硬件清单

硬件名称规格/型号作用说明
STM32F407核心板 带外部晶振8MHz、1MB Flash、192KB RAM 主控MCU,运行TFLM框架和推理代码
LCD+触摸屏模块 ILI9488(480×320)+XPT2046触摸屏 手写数字输入(触摸屏)、识别结果显示(LCD)
辅助工具 J-Link/ST-Link、USB转TTL、杜邦线 程序烧录、串口调试、硬件接线
电源 5V/2A适配器 为开发板供电,避免USB供电不足导致触摸屏/屏幕异常
可选工具 示波器 排查触摸屏/ADC异常(新手可暂不准备)

2.2 软件清单

  • 开发环境:Keil MDK-ARM V5(需安装STM32F4芯片包,版本≥2.13);
  • 模型工具:Python 3.8+、TensorFlow 2.10+(用于训练/转换模型);
  • 底层库:STM32F4标准库V3.5(或HAL库,本文以标准库为例);
  • 框架源码:TensorFlow Lite Micro(TFLM)源码(适配Cortex-M4);
  • 辅助工具:串口调试助手、XPT2046手册、ILI9488手册。
  • 2.3 核心原理

    2.3.1 手写数字识别核心(MNIST数据集)

    MNIST是手写数字识别的经典数据集,包含70000张28×28像素的灰度手写数字图片(0-9),其中60000张训练集、10000张测试集。我们的目标是:

  • 用TensorFlow训练一个轻量级CNN模型(适配STM32资源);
  • 将模型转换为TFLite量化模型(int8量化,减小体积/提升速度);
  • 将TFLite模型转换为C数组(嵌入式可直接调用);
  • STM32采集触摸屏手写轨迹,预处理为28×28灰度图(匹配MNIST格式);
  • 调用TFLM框架执行模型推理,输出识别结果(0-9)。
  • 2.3.2 TensorFlow Lite Micro(TFLM)原理

    TFLM是Google专为微控制器设计的轻量化TFLite框架,核心特点:

    • 极小的内存占用:核心代码仅几十KB,支持int8/uint8量化模型;
    • 无操作系统依赖:可直接运行在裸机MCU上;
    • 适配Cortex-M系列:针对ARM Cortex-M内核做了优化(如硬件浮点、内存对齐);
    • 推理速度快:28×28手写数字推理仅需几十ms(STM32F407)。
    2.3.3 整体流程

    #mermaid-svg-y7JJu1cpJRuYhznP{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-y7JJu1cpJRuYhznP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-y7JJu1cpJRuYhznP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-y7JJu1cpJRuYhznP .error-icon{fill:#552222;}#mermaid-svg-y7JJu1cpJRuYhznP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-y7JJu1cpJRuYhznP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-y7JJu1cpJRuYhznP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-y7JJu1cpJRuYhznP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-y7JJu1cpJRuYhznP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-y7JJu1cpJRuYhznP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-y7JJu1cpJRuYhznP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-y7JJu1cpJRuYhznP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-y7JJu1cpJRuYhznP .marker.cross{stroke:#333333;}#mermaid-svg-y7JJu1cpJRuYhznP svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-y7JJu1cpJRuYhznP p{margin:0;}#mermaid-svg-y7JJu1cpJRuYhznP .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-y7JJu1cpJRuYhznP .cluster-label text{fill:#333;}#mermaid-svg-y7JJu1cpJRuYhznP .cluster-label span{color:#333;}#mermaid-svg-y7JJu1cpJRuYhznP .cluster-label span p{background-color:transparent;}#mermaid-svg-y7JJu1cpJRuYhznP .label text,#mermaid-svg-y7JJu1cpJRuYhznP span{fill:#333;color:#333;}#mermaid-svg-y7JJu1cpJRuYhznP .node rect,#mermaid-svg-y7JJu1cpJRuYhznP .node circle,#mermaid-svg-y7JJu1cpJRuYhznP .node ellipse,#mermaid-svg-y7JJu1cpJRuYhznP .node polygon,#mermaid-svg-y7JJu1cpJRuYhznP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-y7JJu1cpJRuYhznP .rough-node .label text,#mermaid-svg-y7JJu1cpJRuYhznP .node .label text,#mermaid-svg-y7JJu1cpJRuYhznP .image-shape .label,#mermaid-svg-y7JJu1cpJRuYhznP .icon-shape .label{text-anchor:middle;}#mermaid-svg-y7JJu1cpJRuYhznP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-y7JJu1cpJRuYhznP .rough-node .label,#mermaid-svg-y7JJu1cpJRuYhznP .node .label,#mermaid-svg-y7JJu1cpJRuYhznP .image-shape .label,#mermaid-svg-y7JJu1cpJRuYhznP .icon-shape .label{text-align:center;}#mermaid-svg-y7JJu1cpJRuYhznP .node.clickable{cursor:pointer;}#mermaid-svg-y7JJu1cpJRuYhznP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-y7JJu1cpJRuYhznP .arrowheadPath{fill:#333333;}#mermaid-svg-y7JJu1cpJRuYhznP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-y7JJu1cpJRuYhznP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-y7JJu1cpJRuYhznP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y7JJu1cpJRuYhznP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-y7JJu1cpJRuYhznP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y7JJu1cpJRuYhznP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-y7JJu1cpJRuYhznP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-y7JJu1cpJRuYhznP .cluster text{fill:#333;}#mermaid-svg-y7JJu1cpJRuYhznP .cluster span{color:#333;}#mermaid-svg-y7JJu1cpJRuYhznP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-y7JJu1cpJRuYhznP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-y7JJu1cpJRuYhznP rect.text{fill:none;stroke-width:0;}#mermaid-svg-y7JJu1cpJRuYhznP .icon-shape,#mermaid-svg-y7JJu1cpJRuYhznP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-y7JJu1cpJRuYhznP .icon-shape p,#mermaid-svg-y7JJu1cpJRuYhznP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-y7JJu1cpJRuYhznP .icon-shape rect,#mermaid-svg-y7JJu1cpJRuYhznP .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-y7JJu1cpJRuYhznP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-y7JJu1cpJRuYhznP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-y7JJu1cpJRuYhznP :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    系统流程循环

    实时识别流程

    嵌入式环境配置

    模型开发阶段

    TensorFlow训练MNIST模型

    TFLite模型转换int8量化,减小体积

    转换为C数组格式适配STM32内存

    Keil工程集成TFLM框架 + 模型C数组

    STM32底层驱动开发LCD驱动 + 触摸屏驱动 + 串口调试

    触摸屏采集手写轨迹实时绘制到LCD

    预处理步骤降采样为28×28灰度图二值化归一化

    TFLM推理引擎加载模型并执行推理

    结果解析与显示解析Softmax输出LCD显示识别数字

    系统复位或清空准备下一次识别

    三、模型训练与转换(关键前置步骤)

    3.1 Python环境配置

    安装Python 3.8+后,执行以下命令安装依赖:

    pip install tensorflow==2.10.0 numpy matplotlib pillow

    3.2 训练MNIST模型并转换为TFLite

    新建mnist_train_convert.py,代码如下(含训练、量化、转换为C数组,零基础可直接运行):

    import tensorflow as tf
    import numpy as np
    import os

    # 1. 加载MNIST数据集并预处理
    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
    # 归一化到[0,1],并扩展维度(适配CNN输入:(样本数, 28, 28, 1))
    x_train = np.expand_dims(x_train.astype(np.float32) / 255.0, axis=1)
    x_test = np.expand_dims(x_test.astype(np.float32) / 255.0, axis=1)
    # 标签独热编码
    y_train = tf.keras.utils.to_categorical(y_train, 10)
    y_test = tf.keras.utils.to_categorical(y_test, 10)

    # 2. 定义轻量级CNN模型(适配STM32F407资源)
    model = tf.keras.Sequential([
    tf.keras.layers.Conv2D(8, (3,3), activation='relu', input_shape=(28,28,1)),
    tf.keras.layers.MaxPooling2D((2,2)),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(16, activation='relu'),
    tf.keras.layers.Dense(10, activation='softmax')
    ])

    # 3. 编译并训练模型
    model.compile(optimizer='adam',
    loss='categorical_crossentropy',
    metrics=['accuracy'])
    model.fit(x_train, y_train, epochs=5, batch_size=32, validation_data=(x_test, y_test))

    # 4. 评估模型
    test_loss, test_acc = model.evaluate(x_test, y_test)
    print(f"测试准确率:{test_acc:.4f}")

    # 5. 转换为TFLite模型(int8量化,减小体积/提升速度)
    # 5.1 生成量化校准数据集(用100个测试样本)
    def representative_data_gen():
    for i in range(100):
    yield [np.expand_dims(x_test[i], axis=0)]

    # 5.2 转换为量化TFLite模型
    converter = tf.lite.TFLiteConverter.from_keras_model(model)
    # 设置量化参数
    converter.optimizations = [tf.lite.Optimize.DEFAULT]
    converter.representative_dataset = representative_data_gen
    # 指定目标平台(ARM Cortex-M)
    converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8]
    converter.inference_input_type = tf.int8 # 输入int8
    converter.inference_output_type = tf.int8 # 输出int8
    tflite_model = converter.convert()

    # 6. 保存TFLite模型
    tflite_model_path = "mnist_model_int8.tflite"
    with open(tflite_model_path, 'wb') as f:
    f.write(tflite_model)
    print(f"TFLite模型已保存:{tflite_model_path}")

    # 7. 将TFLite模型转换为C数组(适配STM32)
    def tflite_to_c_array(tflite_model_path, c_file_path):
    with open(tflite_model_path, 'rb') as f:
    tflite_model = f.read()
    # 转换为C数组
    c_array = "const unsigned char mnist_model[] = {\\n"
    c_array += ", ".join([f"0x{b:02x}" for b in tflite_model])
    c_array += "\\n};\\n"
    c_array += f"const int mnist_model_len = {len(tflite_model)};\\n"
    # 保存为C文件
    with open(c_file_path, 'w') as f:
    f.write(c_array)
    print(f"C数组模型已保存:{c_file_path}")

    # 生成C数组文件
    tflite_to_c_array(tflite_model_path, "mnist_model.h")

    运行上述代码后,会生成两个关键文件:

    • mnist_model_int8.tflite:量化后的TFLite模型(约10KB);
    • mnist_model.h:转换后的C数组模型(可直接加入STM32工程)。

    四、TFLM框架移植到Keil工程

    4.1 TFLM源码下载与整理

  • 从GitHub下载TFLM源码:git clone https://github.com/tensorflow/tensorflow.git;
  • 进入tensorflow/lite/micro目录,复制以下核心文件到Keil工程的TFLM目录:
    • core/:TFLM核心框架(如micro_interpreter.cc、memory_helpers.cc);
    • kernels/:常用算子(如conv2d.cc、max_pool_2d.cc、softmax.cc);
    • memory_planner/:内存规划(greedy_memory_planner.cc);
    • tools/make/downloads/:依赖库(如cmsis/CMSIS/NN);
    • include/:头文件(tensorflow/lite/micro下所有.h)。
  • 4.2 Keil工程配置

  • 新建Keil工程(STM32F407ZET6),创建以下分组:
    • Core:主函数、中断服务函数;
    • Library:STM32F4标准库文件;
    • BSP:板级驱动(LCD、触摸屏、串口、延时);
    • TFLM_Core:TFLM核心文件;
    • TFLM_Kernels:TFLM算子文件;
    • Model:mnist_model.h(模型C数组);
  • 添加头文件路径:Options for Target→C/C++→Include Paths,添加:
    • TFLM所有头文件目录(tflite/micro/include、tflite/micro/core等);
    • STM32标准库头文件目录;
    • BSP驱动目录;
  • 定义全局宏:在C/C++→Define中添加:__FPU_PRESENT=1,ARM_MATH_CM4,TF_LITE_STATIC_MEMORY,TF_LITE_MCU_DEBUG_LOG=0
    • TF_LITE_STATIC_MEMORY:启用静态内存(适配MCU);
    • TF_LITE_MCU_DEBUG_LOG=0:关闭调试日志(节省内存);
  • 配置栈/堆大小:Options for Target→Linker→Stack Size=0x800、Heap Size=0x1000(增大堆内存,避免TFLM内存不足)。
  • 五、底层驱动开发(核心基础)

    5.1 延时驱动(bsp_delay.c/h)

    // bsp_delay.h
    #ifndef __BSP_DELAY_H
    #define __BSP_DELAY_H

    #include "stm32f4xx.h"

    typedef uint8_t u8;
    typedef uint16_t u16;
    typedef uint32_t u32;

    void delay_init(u8 sysclk);
    void delay_us(u32 nus);
    void delay_ms(u16 nms);

    #endif // __BSP_DELAY_H

    // bsp_delay.c
    #include "bsp_delay.h"

    static u8 fac_us=0;
    static u16 fac_ms=0;

    void delay_init(u8 sysclk)
    {
    SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
    fac_us=sysclk/8;
    fac_ms=(u16)fac_us*1000;
    }

    void delay_us(u32 nus)
    {
    u32 temp;
    SysTick->LOAD=nus*fac_us;
    SysTick->VAL=0x00;
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk;
    do{
    temp=SysTick->CTRL;
    }while((temp&0x01)&&!(temp&(1<<16)));
    SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;
    SysTick->VAL=0X00;
    }

    void delay_ms(u16 nms)
    {
    u32 temp;
    SysTick->LOAD=(u32)nms*fac_ms;
    SysTick->VAL=0x00;
    SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk;
    do{
    temp=SysTick->CTRL;
    }while((temp&0x01)&&!(temp&(1<<16)));
    SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;
    SysTick->VAL=0X00;
    }

    5.2 串口驱动(bsp_uart.c/h,调试用)

    // bsp_uart.h
    #ifndef __BSP_UART_H
    #define __BSP_UART_H

    #include "stm32f4xx.h"
    #include "bsp_delay.h"

    void USART1_Init(u32 baud);

    #endif // __BSP_UART_H

    // bsp_uart.c
    #include "bsp_uart.h"
    #include "stm32f4xx_usart.h"
    #include "stm32f4xx_gpio.h"
    #include "stdio.h"

    void USART1_Init(u32 baud)
    {
    GPIO_InitTypeDef GPIO_InitStruct;
    USART_InitTypeDef USART_InitStruct;
    NVIC_InitTypeDef NVIC_InitStruct;

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

    GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
    GPIO_Init(GPIOA, &GPIO_InitStruct);

    USART_InitStruct.USART_BaudRate = baud;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;
    USART_InitStruct.USART_StopBits = USART_StopBits_1;
    USART_InitStruct.USART_Parity = USART_Parity_No;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART1, &USART_InitStruct);

    NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);

    USART_Cmd(USART1, ENABLE);
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
    }

    int fputc(int ch, FILE *f)
    {
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, (u8)ch);
    return ch;
    }

    void USART1_IRQHandler(void)
    {
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
    USART_ReceiveData(USART1);
    USART_ClearITPendingBit(USART1, USART_IT_RXNE);
    }
    }

    5.3 LCD+触摸屏驱动(ILI9488+XPT2046)

    5.3.1 LCD驱动(lcd.c/h)

    // lcd.h
    #ifndef __LCD_H
    #define __LCD_H

    #include "stm32f4xx.h"
    #include "bsp_delay.h"

    #define LCD_W 480
    #define LCD_H 320

    #define WHITE 0xFFFF
    #define BLACK 0x0000
    #define RED 0xF800
    #define GREEN 0x07E0
    #define BLUE 0x001F
    #define YELLOW 0xFFE0
    #define GRAY 0x8410

    #define LCD_CMD_ADDR ((u32)0x60000000)
    #define LCD_DATA_ADDR ((u32)0x60020000)

    #define LCD_Write_Cmd(cmd) {*(volatile u16*)LCD_CMD_ADDR = cmd;}
    #define LCD_Write_Data(data) {*(volatile u16*)LCD_DATA_ADDR = data;}

    void LCD_Init(void);
    void LCD_Clear(u16 color);
    void LCD_Draw_Point(u16 x, u16 y, u16 color);
    void LCD_Draw_Line(u16 x1, u16 y1, u16 x2, u16 y2, u16 color);
    void LCD_Show_Char(u16 x, u16 y, u8 ch, u16 color, u16 bg_color, u8 size);
    void LCD_Show_Num(u16 x, u16 y, u32 num, u8 len, u16 color, u16 bg_color, u8 size);

    #endif // __LCD_H

    // lcd.c
    #include "lcd.h"
    #include "stm32f4xx_fsmc.h"
    #include "stm32f4xx_gpio.h"

    void LCD_GPIO_Config(void)
    {
    GPIO_InitTypeDef GPIO_InitStruct;

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOD | RCC_AHB1Periph_GPIOE | RCC_AHB1Periph_GPIOG, ENABLE);
    RCC_AHB3PeriphClockCmd(RCC_AHB3Periph_FSMC, ENABLE);

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;

    GPIO_PinAFConfig(GPIOD, GPIO_PinSource0, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource1, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource4, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource5, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource8, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource9, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource10, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource11, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource12, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource13, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource14, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOD, GPIO_PinSource15, GPIO_AF_FSMC);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_4 | GPIO_Pin_5 |
    GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 |
    GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_Init(GPIOD, &GPIO_InitStruct);

    GPIO_PinAFConfig(GPIOE, GPIO_PinSource0, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource1, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource2, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource3, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource4, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource5, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource6, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource7, GPIO_AF_FSMC);
    GPIO_PinAFConfig(GPIOE, GPIO_PinSource8, GPIO_AF_FSMC);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 |
    GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7 |
    GPIO_Pin_8;
    GPIO_Init(GPIOE, &GPIO_InitStruct);

    GPIO_PinAFConfig(GPIOG, GPIO_PinSource12, GPIO_AF_FSMC);
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12;
    GPIO_Init(GPIOG, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
    GPIO_Init(GPIOG, &GPIO_InitStruct);
    }

    void LCD_FSMC_Config(void)
    {
    FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStruct;
    FSMC_NORSRAMTimingInitTypeDef FSMC_NORSRAMTimingInitStruct;

    FSMC_NORSRAMTimingInitStruct.FSMC_AddressSetupTime = 1;
    FSMC_NORSRAMTimingInitStruct.FSMC_AddressHoldTime = 0;
    FSMC_NORSRAMTimingInitStruct.FSMC_DataSetupTime = 5;
    FSMC_NORSRAMTimingInitStruct.FSMC_BusTurnAroundDuration = 0;
    FSMC_NORSRAMTimingInitStruct.FSMC_CLKDivision = 0;
    FSMC_NORSRAMTimingInitStruct.FSMC_DataLatency = 0;
    FSMC_NORSRAMTimingInitStruct.FSMC_AccessMode = FSMC_AccessMode_A;

    FSMC_NORSRAMInitStruct.FSMC_Bank = FSMC_Bank1_NORSRAM1;
    FSMC_NORSRAMInitStruct.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable;
    FSMC_NORSRAMInitStruct.FSMC_MemoryType = FSMC_MemoryType_SRAM;
    FSMC_NORSRAMInitStruct.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b;
    FSMC_NORSRAMInitStruct.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable;
    FSMC_NORSRAMInitStruct.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low;
    FSMC_NORSRAMInitStruct.FSMC_WrapMode = FSMC_WrapMode_Disable;
    FSMC_NORSRAMInitStruct.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState;
    FSMC_NORSRAMInitStruct.FSMC_WriteOperation = FSMC_WriteOperation_Enable;
    FSMC_NORSRAMInitStruct.FSMC_WaitSignal = FSMC_WaitSignal_Disable;
    FSMC_NORSRAMInitStruct.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable;
    FSMC_NORSRAMInitStruct.FSMC_WriteBurst = FSMC_WriteBurst_Disable;
    FSMC_NORSRAMInitStruct.FSMC_NORSRAMTimingStruct = &FSMC_NORSRAMTimingInitStruct;
    FSMC_NORSRAMInitStruct.FSMC_ReadWriteTimingStruct = &FSMC_NORSRAMTimingInitStruct;

    FSMC_NORSRAMInit(&FSMC_NORSRAMInitStruct);
    FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM1, ENABLE);
    }

    void LCD_Reset(void)
    {
    GPIO_SetBits(GPIOG, GPIO_Pin_13);
    delay_ms(100);
    GPIO_ResetBits(GPIOG, GPIO_Pin_13);
    delay_ms(100);
    GPIO_SetBits(GPIOG, GPIO_Pin_13);
    delay_ms(100);
    }

    void ILI9488_Init(void)
    {
    LCD_Write_Cmd(0xCF);
    LCD_Write_Data(0x00);
    LCD_Write_Data(0xC1);
    LCD_Write_Data(0x30);

    LCD_Write_Cmd(0xED);
    LCD_Write_Data(0x64);
    LCD_Write_Data(0x03);
    LCD_Write_Data(0x12);
    LCD_Write_Data(0x81);

    LCD_Write_Cmd(0xE8);
    LCD_Write_Data(0x85);
    LCD_Write_Data(0x00);
    LCD_Write_Data(0x78);

    LCD_Write_Cmd(0xCB);
    LCD_Write_Data(0x39);
    LCD_Write_Data(0x2C);
    LCD_Write_Data(0x00);
    LCD_Write_Data(0x34);
    LCD_Write_Data(0x02);

    LCD_Write_Cmd(0xF7);
    LCD_Write_Data(0x20);

    LCD_Write_Cmd(0xEA);
    LCD_Write_Data(0x00);
    LCD_Write_Data(0x00);

    LCD_Write_Cmd(0xC0);
    LCD_Write_Data(0x10);

    LCD_Write_Cmd(0xC1);
    LCD_Write_Data(0x00);

    LCD_Write_Cmd(0xC5);
    LCD_Write_Data(0x30);
    LCD_Write_Data(0x30);

    LCD_Write_Cmd(0xC7);
    LCD_Write_Data(0xB7);

    LCD_Write_Cmd(0x36);
    LCD_Write_Data(0x48);

    LCD_Write_Cmd(0x3A);
    LCD_Write_Data(0x55);

    LCD_Write_Cmd(0xB1);
    LCD_Write_Data(0x00);
    LCD_Write_Data(0x1B);

    LCD_Write_Cmd(0xB6);
    LCD_Write_Data(0x0A);
    LCD_Write_Data(0xA2);

    LCD_Write_Cmd(0xF2);
    LCD_Write_Data(0x00);

    LCD_Write_Cmd(0x26);
    LCD_Write_Data(0x01);

    LCD_Write_Cmd(0xE0);
    LCD_Write_Data(0x0F);
    LCD_Write_Data(0x2A);
    LCD_Write_Data(0x28);
    LCD_Write_Data(0x08);
    LCD_Write_Data(0x0E);
    LCD_Write_Data(0x08);
    LCD_Write_Data(0x54);
    LCD_Write_Data(0xA9);
    LCD_Write_Data(0x43);
    LCD_Write_Data(0x0A);
    LCD_Write_Data(0x0F);
    LCD_Write_Data(0x00);
    LCD_Write_Data(0x00);
    LCD_Write_Data(0x00);
    LCD_Write_Data(0x00);

    LCD_Write_Cmd(0xE1);
    LCD_Write_Data(0x00);
    LCD_Write_Data(0x15);
    LCD_Write_Data(0x17);
    LCD_Write_Data(0x07);
    LCD_Write_Data(0x11);
    LCD_Write_Data(0x06);
    LCD_Write_Data(0x2B);
    LCD_Write_Data(0x56);
    LCD_Write_Data(0x3C);
    LCD_Write_Data(0x05);
    LCD_Write_Data(0x10);
    LCD_Write_Data(0x0F);
    LCD_Write_Data(0x3F);
    LCD_Write_Data(0x3F);
    LCD_Write_Data(0x0F);

    LCD_Write_Cmd(0x29);
    }

    void LCD_Set_Window(u16 x0, u16 y0, u16 x1, u16 y1)
    {
    LCD_Write_Cmd(0x2A);
    LCD_Write_Data(x0 >> 8);
    LCD_Write_Data(x0 & 0xFF);
    LCD_Write_Data(x1 >> 8);
    LCD_Write_Data(x1 & 0xFF);

    LCD_Write_Cmd(0x2B);
    LCD_Write_Data(y0 >> 8);
    LCD_Write_Data(y0 & 0xFF);
    LCD_Write_Data(y1 >> 8);
    LCD_Write_Data(y1 & 0xFF);

    LCD_Write_Cmd(0x2C);
    }

    void LCD_Draw_Point(u16 x, u16 y, u16 color)
    {
    LCD_Set_Window(x, y, x, y);
    LCD_Write_Data(color);
    }

    void LCD_Clear(u16 color)
    {
    u32 i;
    LCD_Set_Window(0, 0, LCD_W1, LCD_H1);
    for(i=0; i<(u32)LCD_W*LCD_H; i++)
    {
    LCD_Write_Data(color);
    }
    }

    void LCD_Draw_Line(u16 x1, u16 y1, u16 x2, u16 y2, u16 color)
    {
    int dx = x2 x1;
    int dy = y2 y1;
    int abs_dx = dx > 0 ? dx : dx;
    int abs_dy = dy > 0 ? dy : dy;
    int step_x = dx > 0 ? 1 : 1;
    int step_y = dy > 0 ? 1 : 1;
    int err = abs_dx abs_dy;

    while(1)
    {
    LCD_Draw_Point(x1, y1, color);
    if(x1 == x2 && y1 == y2) break;
    int err2 = 2 * err;
    if(err2 > abs_dy)
    {
    err -= abs_dy;
    x1 += step_x;
    }
    if(err2 < abs_dx)
    {
    err += abs_dx;
    y1 += step_y;
    }
    }
    }

    // 简易字符显示(适配数字显示)
    const u8 asc2_1608[95][16] = {
    // 省略ASCII字符点阵(可直接复制标准16×8点阵表)
    };

    void LCD_Show_Char(u16 x, u16 y, u8 ch, u16 color, u16 bg_color, u8 size)
    {
    u8 i, j;
    u8 temp;
    u8 pos = ch ' ';
    if(size == 16)
    {
    for(i=0; i<16; i++)
    {
    temp = asc2_1608[pos][i];
    for(j=0; j<8; j++)
    {
    if(temp & (1<<j)) LCD_Draw_Point(x+j, y+i, color);
    else LCD_Draw_Point(x+j, y+i, bg_color);
    }
    }
    }
    }

    void LCD_Show_Num(u16 x, u16 y, u32 num, u8 len, u16 color, u16 bg_color, u8 size)
    {
    u8 i;
    u8 temp;
    u8 enshow = 0;
    for(i=0; i<len; i++)
    {
    temp = (num / (u32)pow(10, leni1)) % 10;
    if(enshow == 0 && i < len1)
    {
    if(temp == 0)
    {
    LCD_Show_Char(x+i*8, y, ' ', color, bg_color, size);
    continue;
    }
    else enshow = 1;
    }
    LCD_Show_Char(x+i*8, y, temp+'0', color, bg_color, size);
    }
    }

    void LCD_Init(void)
    {
    LCD_GPIO_Config();
    LCD_FSMC_Config();
    LCD_Reset();
    ILI9488_Init();
    LCD_Clear(BLACK);
    printf("LCD初始化完成!\\r\\n");
    }

    5.3.2 触摸屏驱动(touch.c/h)

    // touch.h
    #ifndef __TOUCH_H
    #define __TOUCH_H

    #include "stm32f4xx.h"
    #include "bsp_delay.h"

    #define TOUCH_CS_PIN GPIO_Pin_0
    #define TOUCH_CS_PORT GPIOB
    #define TOUCH_CS_CLK RCC_AHB1Periph_GPIOB

    #define TOUCH_IRQ_PIN GPIO_Pin_1
    #define TOUCH_IRQ_PORT GPIOB
    #define TOUCH_IRQ_CLK RCC_AHB1Periph_GPIOB

    typedef struct
    {
    u16 x;
    u16 y;
    u8 press; // 0:未按下,1:按下
    } Touch_Data;

    extern Touch_Data touch_data;

    void TOUCH_Init(void);
    u16 TOUCH_Read_AD(u8 cmd);
    void TOUCH_Read_Pos(Touch_Data *data);
    u8 TOUCH_Calibrate(void); // 触摸屏校准(可选)

    #endif // __TOUCH_H

    // touch.c
    #include "touch.h"
    #include "stm32f4xx_spi.h"
    #include "stm32f4xx_gpio.h"

    Touch_Data touch_data = {0, 0, 0};

    void TOUCH_SPI_Init(void)
    {
    SPI_InitTypeDef SPI_InitStruct;
    GPIO_InitTypeDef GPIO_InitStruct;

    RCC_AHB1PeriphClockCmd(TOUCH_CS_CLK | TOUCH_IRQ_CLK, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);

    GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_SPI2);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource14, GPIO_AF_SPI2);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_SPI2);

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
    GPIO_InitStruct.GPIO_Pin = TOUCH_CS_PIN;
    GPIO_Init(TOUCH_CS_PORT, &GPIO_InitStruct);
    GPIO_SetBits(TOUCH_CS_PORT, TOUCH_CS_PIN);

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN;
    GPIO_InitStruct.GPIO_Pin = TOUCH_IRQ_PIN;
    GPIO_Init(TOUCH_IRQ_PORT, &GPIO_InitStruct);

    SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
    SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
    SPI_InitStruct.SPI_CPOL = SPI_CPOL_High;
    SPI_InitStruct.SPI_CPHA = SPI_CPHA_2Edge;
    SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
    SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;
    SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
    SPI_InitStruct.SPI_CRCPolynomial = 7;
    SPI_Init(SPI2, &SPI_InitStruct);

    SPI_Cmd(SPI2, ENABLE);
    }

    u8 SPI2_Write_Read(u8 data)
    {
    while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI2, data);
    while(SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET);
    return SPI_I2S_ReceiveData(SPI2);
    }

    u16 TOUCH_Read_AD(u8 cmd)
    {
    u16 ad_val = 0;
    u8 i;

    GPIO_ResetBits(TOUCH_CS_PORT, TOUCH_CS_PIN);
    SPI2_Write_Read(cmd);
    delay_us(6);
    ad_val = SPI2_Write_Read(0xFF);
    ad_val <<= 8;
    ad_val |= SPI2_Write_Read(0xFF);
    ad_val >>= 4;
    GPIO_SetBits(TOUCH_CS_PORT, TOUCH_CS_PIN);

    return ad_val;
    }

    void TOUCH_Read_Pos(Touch_Data *data)
    {
    u16 x_ad, y_ad;
    static u16 x_ad_prev = 0, y_ad_prev = 0;

    if(GPIO_ReadInputDataBit(TOUCH_IRQ_PORT, TOUCH_IRQ_PIN) == 0)
    {
    x_ad = TOUCH_Read_AD(0x90);
    y_ad = TOUCH_Read_AD(0xD0);

    // 滤波(去抖动)
    if(abs(x_ad x_ad_prev) < 20 && abs(y_ad y_ad_prev) < 20)
    {
    data->x = x_ad;
    data->y = y_ad;
    data->press = 1;
    }
    x_ad_prev = x_ad;
    y_ad_prev = y_ad;
    }
    else
    {
    data->press = 0;
    }

    // 触摸屏坐标转换(适配480×320 LCD,需根据实际校准调整)
    data->x = (u32)(data->x 300) * LCD_W / 3600;
    data->y = (u32)(3900 data->y) * LCD_H / 3600;

    // 边界限制
    if(data->x < 0) data->x = 0;
    if(data->x >= LCD_W) data->x = LCD_W 1;
    if(data->y < 0) data->y = 0;
    if(data->y >= LCD_H) data->y = LCD_H 1;
    }

    void TOUCH_Init(void)
    {
    TOUCH_SPI_Init();
    printf("触摸屏初始化完成!\\r\\n");
    }

    5.4 手写数字采集与预处理

    编写handwrite.c/h,实现触摸屏手写轨迹采集,并预处理为28×28灰度图(匹配MNIST格式):

    // handwrite.h
    #ifndef __HANDWRITE_H
    #define __HANDWRITE_H

    #include "stm32f4xx.h"
    #include "touch.h"
    #include "lcd.h"

    #define HANDWRITE_AREA_X1 50
    #define HANDWRITE_AREA_Y1 50
    #define HANDWRITE_AREA_X2 350
    #define HANDWRITE_AREA_Y2 350

    #define MNIST_SIZE 28

    extern u8 mnist_data[MNIST_SIZE][MNIST_SIZE]; // 28×28灰度图

    void HANDWRITE_Init(void);
    void HANDWRITE_Capture(Touch_Data *touch_data);
    void HANDWRITE_Preprocess(void);
    void HANDWRITE_Clear(void);

    #endif // __HANDWRITE_H

    // handwrite.c
    #include "handwrite.h"
    #include "bsp_delay.h"

    u8 mnist_data[MNIST_SIZE][MNIST_SIZE] = {0};
    u16 handwrite_buf[LCD_W][LCD_H] = {0}; // 手写轨迹缓冲区

    void HANDWRITE_Init(void)
    {
    // 绘制手写区域边框
    LCD_Draw_Line(HANDWRITE_AREA_X1, HANDWRITE_AREA_Y1, HANDWRITE_AREA_X2, HANDWRITE_AREA_Y1, WHITE);
    LCD_Draw_Line(HANDWRITE_AREA_X2, HANDWRITE_AREA_Y1, HANDWRITE_AREA_X2, HANDWRITE_AREA_Y2, WHITE);
    LCD_Draw_Line(HANDWRITE_AREA_X2, HANDWRITE_AREA_Y2, HANDWRITE_AREA_X1, HANDWRITE_AREA_Y2, WHITE);
    LCD_Draw_Line(HANDWRITE_AREA_X1, HANDWRITE_AREA_Y2, HANDWRITE_AREA_X1, HANDWRITE_AREA_Y1, WHITE);
    printf("手写区域初始化完成!\\r\\n");
    }

    void HANDWRITE_Capture(Touch_Data *touch_data)
    {
    static u16 x_prev = 0, y_prev = 0;

    if(touch_data->press == 1)
    {
    // 限制在手写区域内
    if(touch_data->x >= HANDWRITE_AREA_X1 && touch_data->x <= HANDWRITE_AREA_X2 &&
    touch_data->y >= HANDWRITE_AREA_Y1 && touch_data->y <= HANDWRITE_AREA_Y2)
    {
    // 绘制手写轨迹
    if(x_prev != 0 && y_prev != 0)
    {
    LCD_Draw_Line(x_prev, y_prev, touch_data->x, touch_data->y, WHITE);
    handwrite_buf[touch_data->x][touch_data->y] = 1;
    }
    x_prev = touch_data->x;
    y_prev = touch_data->y;
    }
    }
    else
    {
    x_prev = 0;
    y_prev = 0;
    }
    }

    void HANDWRITE_Preprocess(void)
    {
    u16 i, j;
    u16 dx = (HANDWRITE_AREA_X2 HANDWRITE_AREA_X1) / MNIST_SIZE;
    u16 dy = (HANDWRITE_AREA_Y2 HANDWRITE_AREA_Y1) / MNIST_SIZE;

    // 清空MNIST数据
    memset(mnist_data, 0, sizeof(mnist_data));

    // 降采样为28×28
    for(i=0; i<MNIST_SIZE; i++)
    {
    for(j=0; j<MNIST_SIZE; j++)
    {
    u16 x_start = HANDWRITE_AREA_X1 + i * dx;
    u16 y_start = HANDWRITE_AREA_Y1 + j * dy;
    u16 count = 0;

    // 统计小区域内的手写点
    for(u16 x=x_start; x<x_start+dx; x++)
    {
    for(u16 y=y_start; y<y_start+dy; y++)
    {
    if(handwrite_buf[x][y] == 1) count++;
    }
    }

    // 二值化(匹配MNIST:0=黑,255=白)
    if(count > (dx*dy)/2) mnist_data[i][j] = 255;
    else mnist_data[i][j] = 0;
    }
    }

    printf("手写数据预处理完成,转换为28×28灰度图!\\r\\n");
    }

    void HANDWRITE_Clear(void)
    {
    // 清空手写区域
    for(u16 x=HANDWRITE_AREA_X1; x<=HANDWRITE_AREA_X2; x++)
    {
    for(u16 y=HANDWRITE_AREA_Y1; y<=HANDWRITE_AREA_Y2; y++)
    {
    LCD_Draw_Point(x, y, BLACK);
    handwrite_buf[x][y] = 0;
    }
    }
    memset(mnist_data, 0, sizeof(mnist_data));
    }

    六、TFLM模型推理代码

    6.1 推理核心代码(tflm_infer.c/h)

    // tflm_infer.h
    #ifndef __TFLM_INFER_H
    #define __TFLM_INFER_H

    #include "stm32f4xx.h"
    #include "mnist_model.h" // 模型C数组
    #include "tensorflow/lite/micro/micro_interpreter.h"
    #include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
    #include "tensorflow/lite/schema/schema_generated.h"
    #include "tensorflow/lite/micro/all_ops_resolver.h"

    #define TENSOR_ARENA_SIZE 20*1024 // 张量内存池(20KB,适配STM32F407)

    u8 TFLM_Infer(u8 input[28][28], u8 *result);

    #endif // __TFLM_INFER_H

    // tflm_infer.c
    #include "tflm_infer.h"
    #include "bsp_uart.h"

    // 定义算子解析器(仅包含模型用到的算子)
    tflite::MicroMutableOpResolver<5> op_resolver;

    // 张量内存池
    alignas(16) uint8_t tensor_arena[TENSOR_ARENA_SIZE];

    u8 TFLM_Infer(u8 input[28][28], u8 *result)
    {
    // 1. 初始化算子解析器
    op_resolver.AddConv2D();
    op_resolver.AddMaxPool2D();
    op_resolver.AddFullyConnected();
    op_resolver.AddSoftmax();
    op_resolver.AddReshape();

    // 2. 加载模型
    const tflite::Model* model = tflite::GetModel(mnist_model);
    if(model->version() != TFLITE_SCHEMA_VERSION)
    {
    printf("模型版本不匹配!\\r\\n");
    return 1;
    }

    // 3. 初始化解释器
    static tflite::MicroInterpreter interpreter(model, op_resolver, tensor_arena, TENSOR_ARENA_SIZE);
    TfLiteStatus allocate_status = interpreter.AllocateTensors();
    if(allocate_status != kTfLiteOk)
    {
    printf("张量分配失败!\\r\\n");
    return 1;
    }

    // 4. 获取输入/输出张量
    TfLiteTensor* input_tensor = interpreter.input(0);
    TfLiteTensor* output_tensor = interpreter.output(0);

    // 5. 预处理输入数据(转换为int8,匹配量化模型)
    for(int i=0; i<28; i++)
    {
    for(int j=0; j<28; j++)
    {
    // MNIST量化模型输入范围:0~255 → int8(-128~127)
    input_tensor->data.int8[i*28 + j] = (int8_t)(input[i][j] 128);
    }
    }

    // 6. 执行推理
    TfLiteStatus invoke_status = interpreter.Invoke();
    if(invoke_status != kTfLiteOk)
    {
    printf("推理执行失败!\\r\\n");
    return 1;
    }

    // 7. 解析输出结果(softmax概率,取最大值索引)
    int8_t max_val = output_tensor->data.int8[0];
    u8 max_idx = 0;
    for(int i=1; i<10; i++)
    {
    if(output_tensor->data.int8[i] > max_val)
    {
    max_val = output_tensor->data.int8[i];
    max_idx = i;
    }
    }

    *result = max_idx;
    printf("推理完成,识别结果:%d\\r\\n", max_idx);
    return 0;
    }

    七、完整整合与测试

    7.1 主函数代码(main.c)

    #include "stm32f4xx.h"
    #include "bsp_uart.h"
    #include "bsp_delay.h"
    #include "lcd.h"
    #include "touch.h"
    #include "handwrite.h"
    #include "tflm_infer.h"

    // 系统时钟初始化(168MHz)
    void SystemInit(void)
    {
    RCC_DeInit();
    RCC_HSEConfig(RCC_HSE_ON);
    while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET);
    RCC_PLLConfig(RCC_PLLSource_HSE, 8, 168, 2, 7);
    RCC_PLLCmd(ENABLE);
    while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);
    RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
    while(RCC_GetSYSCLKSource() != 0x08);
    RCC_HCLKConfig(RCC_SYSCLK_Div1);
    RCC_PCLK1Config(RCC_HCLK_Div4);
    RCC_PCLK2Config(RCC_HCLK_Div2);
    delay_init(168);
    }

    int main(void)
    {
    u8 infer_result = 0;
    Touch_Data touch_data = {0, 0, 0};
    u8 infer_flag = 0; // 推理触发标志

    // 1. 初始化底层硬件
    SystemInit();
    USART1_Init(115200);
    delay_ms(100);
    printf("========== STM32F407 TFLM手写数字识别 ==========\\r\\n");

    // 2. 初始化核心模块
    LCD_Init();
    TOUCH_Init();
    HANDWRITE_Init();

    // 3. 显示提示信息
    LCD_Show_Char(10, 10, 'T', WHITE, BLACK, 16);
    LCD_Show_Char(20, 10, 'o', WHITE, BLACK, 16);
    LCD_Show_Char(30, 10, 'u', WHITE, BLACK, 16);
    LCD_Show_Char(40, 10, 'c', WHITE, BLACK, 16);
    LCD_Show_Char(50, 10, 'h', WHITE, BLACK, 16);
    LCD_Show_Char(60, 10, ':', WHITE, BLACK, 16);
    LCD_Show_Char(70, 10, ' ', WHITE, BLACK, 16);
    LCD_Show_Char(80, 10, 'W', WHITE, BLACK, 16);
    LCD_Show_Char(90, 10, 'r', WHITE, BLACK, 16);
    LCD_Show_Char(100, 10, 'i', WHITE, BLACK, 16);
    LCD_Show_Char(110, 10, 't', WHITE, BLACK, 16);
    LCD_Show_Char(120, 10, 'e', WHITE, BLACK, 16);
    LCD_Show_Char(130, 10, ' ', WHITE, BLACK, 16);
    LCD_Show_Char(140, 10, '0', WHITE, BLACK, 16);
    LCD_Show_Char(150, 10, '-', WHITE, BLACK, 16);
    LCD_Show_Char(160, 10, '9', WHITE, BLACK, 16);

    // 4. 主循环
    while(1)
    {
    // 读取触摸屏数据
    TOUCH_Read_Pos(&touch_data);

    // 采集手写轨迹
    HANDWRITE_Capture(&touch_data);

    // 检测手写完成(触摸屏松开)
    if(touch_data.press == 0 && infer_flag == 0 && handwrite_buf[HANDWRITE_AREA_X1+10][HANDWRITE_AREA_Y1+10] != 0)
    {
    // 预处理为28×28灰度图
    HANDWRITE_Preprocess();

    // 执行TFLM推理
    if(TFLM_Infer(mnist_data, &infer_result) == 0)
    {
    // 显示识别结果
    LCD_Show_Char(10, 30, 'R', WHITE, BLACK, 16);
    LCD_Show_Char(20, 30, 'e', WHITE, BLACK, 16);
    LCD_Show_Char(30, 30, 's', WHITE, BLACK, 16);
    LCD_Show_Char(40, 30, 'u', WHITE, BLACK, 16);
    LCD_Show_Char(50, 30, 'l', WHITE, BLACK, 16);
    LCD_Show_Char(60, 30, 't', WHITE, BLACK, 16);
    LCD_Show_Char(70, 30, ':', WHITE, BLACK, 16);
    LCD_Show_Num(80, 30, infer_result, 1, RED, BLACK, 16);
    }

    infer_flag = 1; // 标记推理完成
    }

    // 长按触摸屏清空手写区域(重新输入)
    if(touch_data.press == 1 && infer_flag == 1)
    {
    HANDWRITE_Clear();
    infer_flag = 0;
    // 清空识别结果显示
    for(u16 x=10; x<=100; x++)
    {
    for(u16 y=30; y<=46; y++)
    {
    LCD_Draw_Point(x, y, BLACK);
    }
    }
    }

    delay_ms(10);
    }
    }

    7.2 整体推理流程Mermaid图

    #mermaid-svg-SECUmn5kpkyFk404{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SECUmn5kpkyFk404 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SECUmn5kpkyFk404 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SECUmn5kpkyFk404 .error-icon{fill:#552222;}#mermaid-svg-SECUmn5kpkyFk404 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SECUmn5kpkyFk404 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SECUmn5kpkyFk404 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SECUmn5kpkyFk404 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SECUmn5kpkyFk404 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SECUmn5kpkyFk404 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SECUmn5kpkyFk404 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SECUmn5kpkyFk404 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SECUmn5kpkyFk404 .marker.cross{stroke:#333333;}#mermaid-svg-SECUmn5kpkyFk404 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SECUmn5kpkyFk404 p{margin:0;}#mermaid-svg-SECUmn5kpkyFk404 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-SECUmn5kpkyFk404 .cluster-label text{fill:#333;}#mermaid-svg-SECUmn5kpkyFk404 .cluster-label span{color:#333;}#mermaid-svg-SECUmn5kpkyFk404 .cluster-label span p{background-color:transparent;}#mermaid-svg-SECUmn5kpkyFk404 .label text,#mermaid-svg-SECUmn5kpkyFk404 span{fill:#333;color:#333;}#mermaid-svg-SECUmn5kpkyFk404 .node rect,#mermaid-svg-SECUmn5kpkyFk404 .node circle,#mermaid-svg-SECUmn5kpkyFk404 .node ellipse,#mermaid-svg-SECUmn5kpkyFk404 .node polygon,#mermaid-svg-SECUmn5kpkyFk404 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SECUmn5kpkyFk404 .rough-node .label text,#mermaid-svg-SECUmn5kpkyFk404 .node .label text,#mermaid-svg-SECUmn5kpkyFk404 .image-shape .label,#mermaid-svg-SECUmn5kpkyFk404 .icon-shape .label{text-anchor:middle;}#mermaid-svg-SECUmn5kpkyFk404 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SECUmn5kpkyFk404 .rough-node .label,#mermaid-svg-SECUmn5kpkyFk404 .node .label,#mermaid-svg-SECUmn5kpkyFk404 .image-shape .label,#mermaid-svg-SECUmn5kpkyFk404 .icon-shape .label{text-align:center;}#mermaid-svg-SECUmn5kpkyFk404 .node.clickable{cursor:pointer;}#mermaid-svg-SECUmn5kpkyFk404 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SECUmn5kpkyFk404 .arrowheadPath{fill:#333333;}#mermaid-svg-SECUmn5kpkyFk404 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SECUmn5kpkyFk404 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SECUmn5kpkyFk404 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SECUmn5kpkyFk404 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SECUmn5kpkyFk404 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SECUmn5kpkyFk404 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SECUmn5kpkyFk404 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SECUmn5kpkyFk404 .cluster text{fill:#333;}#mermaid-svg-SECUmn5kpkyFk404 .cluster span{color:#333;}#mermaid-svg-SECUmn5kpkyFk404 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SECUmn5kpkyFk404 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SECUmn5kpkyFk404 rect.text{fill:none;stroke-width:0;}#mermaid-svg-SECUmn5kpkyFk404 .icon-shape,#mermaid-svg-SECUmn5kpkyFk404 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SECUmn5kpkyFk404 .icon-shape p,#mermaid-svg-SECUmn5kpkyFk404 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SECUmn5kpkyFk404 .icon-shape rect,#mermaid-svg-SECUmn5kpkyFk404 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SECUmn5kpkyFk404 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SECUmn5kpkyFk404 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SECUmn5kpkyFk404 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    结果处理

    模型推理

    数据采集与处理

    触摸屏手写轨迹采集实时绘制到LCD

    检测手写完成触摸屏松开事件

    预处理: 降采样为28×28二值化匹配MNIST格式

    初始化TFLM解释器加载量化模型

    输入数据转换uint8→int8量化

    执行推理Conv2D → MaxPool → Softmax

    解析模型输出取Softmax最大值索引

    LCD显示识别结果数字0-9

    长按触摸屏清空准备重新输入

    7.3 编译与下载步骤

  • 将所有代码按工程目录添加到Keil MDK工程;
  • 确认mnist_model.h(模型C数组)已加入Model分组;
  • 检查TFLM头文件路径和全局宏定义是否正确;
  • 点击Build编译工程,确保0 Error 0 Warning;
  • 连接J-Link和STM32F407开发板,点击Download下载HEX文件;
  • 上电后,在LCD手写区域手写数字(0-9),松开触摸屏后自动识别并显示结果。
  • 八、常见问题排查

    8.1 触摸屏采集不到轨迹

  • 硬件接线错误:检查XPT2046的CS/PB0、IRQ/PB1、SPI2(PB13/PB14/PB15)接线;
  • 触摸屏未校准:运行TOUCH_Calibrate()函数校准坐标,调整TOUCH_Read_Pos中的转换公式;
  • SPI速率过快:将SPI_BaudRatePrescaler_256改为SPI_BaudRatePrescaler_128,降低SPI速率。
  • 8.2 TFLM推理失败(串口打印“张量分配失败”)

  • 张量内存池不足:增大TENSOR_ARENA_SIZE(如改为25*1024),确保不超过STM32F407的RAM(192KB);
  • 算子解析器缺失:检查op_resolver.AddXXX()是否包含模型用到的所有算子(Conv2D、MaxPool2D等);
  • 模型格式错误:重新转换TFLite模型,确保为int8量化格式,且模型版本为TFLITE_SCHEMA_VERSION。
  • 8.3 识别结果错误/准确率低

  • 手写预处理问题:检查HANDWRITE_Preprocess()中的降采样和二值化逻辑,确保输出28×28灰度图匹配MNIST;
  • 模型训练不足:增加Python训练代码的epochs(如改为10),提升模型准确率;
  • 量化精度损失:尝试使用float32模型(体积更大,推理稍慢),对比识别效果;
  • 手写区域偏移:调整HANDWRITE_AREA_X1/X2/Y1/Y2,确保手写数字居中。
  • 8.4 Keil编译报错“找不到tflite/xxx.h”

  • 头文件路径未添加:检查Options for Target→C/C++→Include Paths,确保包含TFLM所有头文件目录;
  • TFLM源码缺失:确认已添加TFLM核心文件(micro_interpreter.cc、all_ops_resolver.cc等)。
  • 九、总结

    关键点回顾

  • 模型转换核心:TensorFlow训练的MNIST模型需量化为int8格式,再转换为C数组,适配STM32的Flash/RAM资源;
  • TFLM移植关键:仅添加模型用到的算子,合理配置张量内存池,避免内存不足;
  • 手写预处理重点:触摸屏采集的轨迹需降采样为28×28灰度图,二值化匹配MNIST数据集格式;
  • 嵌入式推理流程:输入预处理→模型加载→解释器初始化→推理执行→结果解析,全程无操作系统依赖。
  • 扩展方向

  • 模型优化:使用模型剪枝/蒸馏技术减小模型体积,提升推理速度;
  • 多分类扩展:将MNIST替换为手写字母/汉字数据集,实现更复杂的识别任务;
  • 硬件加速:使用STM32H7系列(带DSP/Neon)或外接AI加速芯片(如GD32AI);
  • 交互优化:添加语音播报识别结果、手写撤销/重做功能;
  • 多模态融合:结合音频识别,实现“手写+语音”双模态数字识别。
  • 本教程完整覆盖了嵌入式AI部署的核心流程,从模型训练到端侧推理,所有代码均可直接落地。掌握本教程的方法后,你可以快速将其他轻量级AI模型(如语音识别、图像分类)部署到STM32等微控制器上,真正实现“端侧智能”。

    赞(0)
    未经允许不得转载:171主机测评 » AI在嵌入式端的部署:基于STM32F407的手写数字识别(TensorFlow Lite Micro)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址