十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103嵌入式手写识别实战:从触摸采到字库加载与状态机设计

STM32F103嵌入式手写识别实战:从触摸采到字库加载与状态机设计 简介一套面向嵌入式初学者的STM32F103手写识别课程设计资源适合单片机课程设计、毕业设计或嵌入式底层学习参考。该项目将触摸屏输入与识别算法结合覆盖LCD显示、触摸屏校准、串口打印、按键切换模式等功能可直接用于理解手写输入与字符识别的整体实现流程。资源共168个文件以C源码72个和头文件63个为主另含hex固件、lib库及LCD相关图片素材并附带工程配置与清理脚本整体约1.85MB结构紧凑、便于直接查看代码或烧录验证。已有2358人学习包内完整实现了开机初始化、字库检测、手写区等待输入、自动识别并输出结果等流程可帮助开发者快速掌握STM32外设驱动与识别逻辑的配合写法也便于在此基础上扩展更多实验功能。1. 在 stm32f103 最小系统上做手写识别先忘掉神经网络拆这套 STM32f103 手写识别课程设计时我最强烈的感受是真正卡住大家的不是识别算法本身而是「触摸轨迹怎么进来、识别结果怎么出去」。Cortex-M3 只有 72MHz指望在 MCU 上跑出一套 MNIST 卷积明显不现实。工程给出的方案很务实LCD 做交互触摸屏采集笔迹SD 卡里放字库外部识别器或内置识别核负责匹配STM32 只做调度。课件里同时出现 stm32f10x_tim.c、lcd.c、sdio_sdcard.c、stm32f10x_flash.c说明这是一条完整链路不是单独跑个 demo。这套实验适合正在做嵌入式课设、蓝桥杯备赛、想从外设驱动跨到应用层的人也适合把标准库重新捡起来的老工程师。2. FatFs 字库加载层为什么工程文件里躺着 cc936.c 和 sdio_sdcard.c2.1 识别前为什么要先检测字库手写识别器初始化之后会先检测字库这一步不是走过场。识别器内部完成笔画匹配后返回的通常不是可以直接在 LCD 上绘制的点阵而是一个字符编码或字库索引。STM32 拿到这个编码还需要到字库里取对应点阵才能在屏幕的显示区画出结果。如果字库缺失或路径不对识别功能本身正常但输出端会变成空白或乱码现象很像「识别器坏了」。常见做法是字库文件以二进制文件形式放到 SD 卡例如 HZK16 存放 GBK 编码的 16×16 点阵ASC16 存放 ASCII 字符。工程里用 FatFs 的ff.c来读写文件系统SD 卡则通过sdio_sdcard.c驱动。手写识别器初始化时挂在 FatFs 之后就是因为它要依赖文件系统读取字库配置如果把初始化顺序反过来识别器启动时会因为找不到字库而回退到出厂默认状态后续识别结果全部不可靠。2.2 挂载 SD 卡与字库检测的 C 代码在初始化阶段字库检测通常这样写#include ff.h #include diskio.h FATFS fs; FIL fp; FRESULT res; FILINFO fno; res f_mount(fs, 0:, 1); if (res ! FR_OK) { printf(mount fail: %d\n, res); return res; } res f_open(fp, 0:/HZK16, FA_READ); if (res FR_OK) { f_close(fp); printf(font library OK\n); } else { printf(font missing, error: %d\n, res); }这段代码里f_mount第一参数传入全局FATFS对象第二参数0:是卷标绑定了底层 SD 卡的驱动号第三个参数1表示立即挂载如果传0则会在第一次读写文件时才挂载排查问题时容易混淆。f_open返回FR_OK说明字库文件存在返回FR_NO_FILE说明路径不对返回FR_NOT_READY则多半是 SD 卡初始化失败要往回查SD_Init()的状态返回值。检测完立刻f_close避免长期占用文件句柄否则后面识别器读字库时可能出现FR_TOO_MANY_OPEN_FILES。2.3 SDIO 时钟分频与接线别一上来就追求高速sdio_sdcard.c走的是 SDIO 外设。很多课程设计里 SDIO 初始化没问题卡也插了可f_mount就是报FR_NOT_READY。此时优先检查SDIO_ClockDiv。SDIO_InitStructure.SDIO_ClockDiv 0x04; SDIO_InitStructure.SDIO_ClockEdge SDIO_ClockEdge_Rising; SDIO_InitStructure.SDIO_BusWide SDIO_BusWide_1b;在 72MHz 主频下SDIO 输出时钟约等于SDIOCLK / (2 SDIO_ClockDiv)。SDIO_ClockDiv设为 0 时接近 36MHz但很多 SD 卡初始化阶段不支持这么高导致SD_Init()卡在SD_CMD8_SEND。常见做法是初始化阶段先用较大的分频系数比如 0x04确认卡 ID 和容量后再切到 1 位或 4 位高速模式。另外SDIO 的 D0-D3、CLK、CMD 要比 SPI 模式多出四根数据线这六条线在 PCB 或杜邦线方案里都要加 10k 左右上拉电阻否则在高阻态时数据线悬空容易偶发读写错误。2.4 cc936.c 的中文编码角色以及 keilkilll.bat 的意义工程里同时出现cc936.c、cc949.c、cc950.c、cc932.c四个文件看到它们第一反应是这事与 FatFs 的_CODE_PAGE配置有关。完整列表如下文件代码页适用区域cc936.cCP936/GBK简体中文cc932.cCP932日文 Shift-JIScc949.cCP949韩文cc950.cCP950繁体中文 Big5FatFs 在ffconf.h中通过_CODE_PAGE指定编码页。如果使用中文文件名或读中文路径_CODE_PAGE要设成 936并保证 cc936.c 参与编译。若是把_CODE_PAGE设成 950 而工程里只留了 cc936.c链接时会出现ff_convert或ff_wtoupper未定义这类错误和 CPU 主频无关纯粹是文件系统配置不匹配。组件里其他三个编码文件可以删掉保留它们不会报错但会造成维护误解。keilkilll.bat是 clean 脚本删除工程编译产生的中间 obj、lst 文件对最终发布包非常有用唯一要注意的是脚本默认删除范围别把字库 bin 文件也列入删除名单。3. 触摸轨迹采集与等距重采样识别器的输入不能是原始点3.1 坐标来源与按下判定手写区在 LCD 上既负责显示轨迹也负责接收触摸坐标。触摸驱动会提供一个结构体典型成员是tp_dev.sta、tp_dev.x、tp_dev.y。在主循环里轮询扫描检测到按下后记录坐标#define TRACE_MAX 256 int16_t trace_buf[2][TRACE_MAX]; uint16_t trace_cnt 0; tp_dev.scan(); if (tp_dev.sta TP_PRES_DOWN) { if (trace_cnt TRACE_MAX) { trace_buf[0][trace_cnt] tp_dev.x; trace_buf[1][trace_cnt] tp_dev.y; trace_cnt; } LCD_DrawPoint(tp_dev.x, tp_dev.y, WHITE); } else { if (trace_cnt 0) { HandWrite_Recognize(trace_buf, trace_cnt); trace_cnt 0; } }TP_PRES_DOWN位被置 1表示当前有笔尖按下清零表示抬起。每次扫描间隔 5ms 左右按下期间持续记录坐标。有个容易忽略的问题用户写一个字时手会短暂停顿但触摸仍处于按下状态此时坐标保持不变如果直接把坐标全量记录下来后续识别会把一个长时间停顿的点当成连续压感。常见做法是先判TP_PRES_DOWN再结合本次坐标与上次坐标的距离差若距离小于一个像素阈值则跳过该点避免同一位置塞进大量重复点。3.2 滑点滤波与抬笔抖动电阻触摸屏在按下瞬间容易出现一段飞点表现为轨迹开头突然跳到屏幕边缘又跳回原处。最简单的方法是滑窗滤波取相邻三点平均值int16_t sx, sy; for (i 1; i trace_cnt - 1; i) { sx (trace_buf[0][i-1] trace_buf[0][i] trace_buf[0][i1]) / 3; sy (trace_buf[1][i-1] trace_buf[1][i] trace_buf[1][i1]) / 3; filter_buf[0][i] sx; filter_buf[1][i] sy; }注意这段代码没有对端点做处理所以filter_buf首尾两个点需要直接复制原始值。3 点均值对单点飞刺足够对连续三点偏移则无能为力。更严格的判断是检查相邻点欧氏距离如果某一点与前后两点的距离都超过整段轨迹平均距离的 3 倍就把它当作坏点剔除。抬笔抖动是另一个坑笔尖离屏时触摸屏会残留一次低压力接触导致轨迹末尾多出一根短尾。我的习惯是丢弃最后 5 个点再检查最后一段向量长度若小于总长度的 2% 则直接截断。3.3 等距重采样与归一化网格原始点受书写速度影响同一个字用慢写和快写产生的时间间隔不同导致采样点密度不同。识别器需要的是一组空间上均匀分布的点列因此要做等距重采样。先累加相邻点距离然后按目标点数等分弧长#define RESAMPLE_N 128 double dist_sum[TRACE_MAX]; double total_len 0; dist_sum[0] 0; for (i 1; i trace_cnt; i) { total_len hypot( filter_buf[0][i] - filter_buf[0][i-1], filter_buf[1][i] - filter_buf[1][i-1]); dist_sum[i] total_len; } double step total_len / (RESAMPLE_N - 1); uint16_t ptr 0; int16_t out[2][RESAMPLE_N]; for (i 0; i RESAMPLE_N; i) { double target step * i; while (ptr trace_cnt - 2 dist_sum[ptr1] target) ptr; double alpha (target - dist_sum[ptr]) / (dist_sum[ptr1] - dist_sum[ptr]); out[0][i] (int16_t)(filter_buf[0][ptr] alpha * (filter_buf[0][ptr1] - filter_buf[0][ptr])); out[1][i] (int16_t)(filter_buf[1][ptr] alpha * (filter_buf[1][ptr1] - filter_buf[1][ptr])); }step是目标弧长ptr在原始累积长度表中前进alpha是线性插值权重。这样做的好处是慢写和快写会得到相同点数的轨迹识别器不会因为笔画速度不同而给出不同结果。重采样后还要归一化把轨迹坐标映射到固定区域。一般先计算包围盒宽高再保持宽高比缩放到 200×200 网格内double scale 200.0 / max(width, height); px (int16_t)((x - min_x) * scale margin); py (int16_t)((y - min_y) * scale margin);不推荐直接把宽高分别拉伸到 200×200否则一个扁平的「一」会被拉成一个方框降低识别可信度。3.4 识别器输入包的组织方式识别器拿到的不应该是二维数组而是一段带包头和点数的二进制流。常见格式是固定包头后面跟坐标数组uint8_t recv_buf[3 RESAMPLE_N * 2]; recv_buf[0] 0xAA; recv_buf[1] (uint8_t)mode; // 模式数字/大写/小写/混合 recv_buf[2] RESAMPLE_N; for (i 0; i RESAMPLE_N; i) { recv_buf[3 i * 2] out[0][i] 0xFF; recv_buf[3 i * 2 1] out[0][i] 8; recv_buf[3 RESAMPLE_N * 2] ...; // 实际以模块手册为准 }这里坐标采用小端排列。如果是整数坐标两个字节刚好如果触摸分辨率超过 16 位就要扩到 4 字节。发送后等待识别器返回类别编码。返回编码再经过模式映射数字模式直接拼成 ASCII 字符字符模式查表转换成对应的字母。4. 识别状态机与四种模式切换KEY0、串口回显和主循环设计4.1 上电初始化顺序识别器必须排在字库之后初始化顺序直接决定实验成败。我这里用的顺序是SystemInit(); LCD_Init(); KEY_Init(); USART1_Init(115200); TP_Init(); SD_Init(); f_mount(fs, 0:, 1); FontLib_Check(); HandWrite_Init(); LCD_Clear(WHITE);LCD_Init要在TP_Init之前因为触摸芯片的采样触发引脚需要正确引脚映射。SD_Init放在f_mount之前FatFs 只是文件系统层底层设备没有起来挂载必然失败。HandWrite_Init放在字库检测后是为了让识别器启动时能直接读取字库或识别配置文件。若把HandWrite_Init提前主控侧无法判断它是否拿到字库调试时会出现「识别结果乱跳」的假象。4.2 主循环状态机等待笔迹、画线、识别直接在主循环里判断TP_PRES_DOWN也能工作但用户按住屏幕时循环会反复执行识别逻辑频繁刷新 LCD。更稳的做法是维护一个状态机typedef enum { ST_WAIT_INPUT, ST_TRACE, ST_RECOGNIZE } op_state_t; op_state_t state ST_WAIT_INPUT; while (1) { tp_dev.scan(); if (state ST_WAIT_INPUT) { if (tp_dev.sta TP_PRES_DOWN) state ST_TRACE; } else if (state ST_TRACE) { if (tp_dev.sta TP_PRES_DOWN) { RecordTracePoint(); } else { state ST_RECOGNIZE; } } else if (state ST_RECOGNIZE) { uint16_t code HandWrite_Recognize(mode); USART1_Printf(result: 0x%04X\n, code); LCD_ShowRecognitionResult(code); state ST_WAIT_INPUT; } DS0_Toggle(); delay_ms(5); }ST_TRACE状态下只有检测到从按下到抬起才进入识别。抬起后即使手指再次接触屏幕状态已经切到ST_RECOGNIZE不会重复识别。DS0_Toggle()放在主循环末尾正常情况下灯会闪烁如果识别卡死灯停在高电平或低电平判断非常直观。5ms 延时是给整个循环一个固定节奏避免触摸扫描频率过快导致采样点重复。4.3 KEY0 模式切换与串口打印模式切换是 KEY0 的一个低频分支不需要占用额外中断static rec_mode_t mode MODE_DIGIT; if (KEY_Scan(0) KEY0_PRES) { mode (rec_mode_t)((mode 1) % 4); printf(mode change %d\n, mode); LCD_ShowMode(mode); }四种模式对应数字、大写字母、小写字母、混合符号。数字模式下识别器只返回 0-9大写模式返回 A-Z混合模式会返回数字和大小写字母。模式设置会在轨迹送入识别器前写入recv_buf[1]让识别器缩小候选范围提高正确率。串口打印先要重定向fputcint fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }检查USART_FLAG_TXE表示发送数据寄存器为空如果写USART_FLAG_TC程序会等到数据完全发送完毕才返回差别不大但 TXE 更适合逐个字节打印。串口助手的编码要设成 GBK因为 MCU 端打印中文提示时字符串常量默认是按本地编码存放的。识别结果本身是十六进制编码不需要转码直接看串口即可。4.4 标准库 v3.5 的编译注意点这套工程大概率基于标准外设库 v3.5在 Keil 里经常见到如下几类错误编译错误常见原因L6218E: Undefined symbol FLASH_Unlock没有把 stm32f10x_flash.c 加入工程L6218E: Undefined symbol SD_InitSDIO 相关库文件缺失或宏定义不对L6218E: Undefined symbol DiskioInitializediskio.c 与平台驱动未绑定遇到undefined symbol优先检查工程文件里是否包含对应的_it.c或_dev.c文件。标准库 v3.5 的启动文件选择也要和芯片容量对应HD对应 512KB 以上 Flash 的型号定义STM32F10X_HD和USE_STDPERIPH_DRIVER后外设库的stm32f10x.h才能正确打开外设寄存器映射。stm32f10x_flash.c在工程里还有一个实际用途是保存触摸校准参数校准结果写入 Flash 后下次开机无需重新校准如果直接烧库文件而不写应用逻辑校准参数仍然只是内存变量断电即失。5. KEY_UP 触摸校准和调试验证别让识别率死在坐标偏移上5.1 校准参数怎么落到 Flash触摸屏不准时按 KEY_UP 进入校准。LCD 会依次在四个角和中心显示十字标记用户点到十字位置后控制器记录该位置触摸芯片的原始 ADC 值再计算 LCD 坐标到触摸坐标的线性映射。简化的缩放参数如下scale_x (LCD_X2 - LCD_X1) / (adc_x2 - adc_x1); offset_x LCD_X2 - scale_x * adc_x2;LCD_X1和LCD_X2是校准点的屏幕坐标adc_x1和adc_x2是触摸芯片上报的原始值。得到scale_x和offset_x后对 Y 轴同理。这套线性映射适合标准四点校准如果屏幕旋转 90 度还需要交换 X/Y 轴和正负号否则手写区会出现镜像。校准参数建议通过FLASH_Unlock、FLASH_ErasePage、FLASH_ProgramHalfWord写入内部 Flash这样下次上电直接读取不必每次校准。注意擦除时不要按页覆盖启动代码选 Flash 末尾的扇区更安全。5.2 验证识别率的小技巧校准完成后不要直接写长句子先做单字统计。循环写 0 到 9 各 5 次每次识别后用printf把期望值和实际值一起打出最后统计正确率。如果某个数字频繁错成另一个先看轨迹回显确认有没有断笔。比如数字 5 容易识别成 6通常因为抬笔过早末尾少了一段弧线。把超时阈值从 200ms 调到 120ms让用户写快点现象会有明显改善。串口输出的单字识别时间也能做参考如果识别耗时超过 300ms大概率是重采样点数太多或识别器串口波特率太低可以先把RESAMPLE_N从 128 降到 64 尝试。5.3 DS0 状态灯与主循环的调试关系DS0 在主循环里翻转不要把它放到定时器中断里。如果识别逻辑阻塞在HandWrite_Recognize主循环停住DS0 就不闪这能直接区分是触摸采集异常还是识别器通信异常。若 DS0 闪但 LCD 不更新问题在显示刷新部分若 DS0 不闪但串口持续打印识别结果说明主循环被某个while等待拖死。在校准模式下我把 DS0 改成校准坐标更新时点亮十字点采集完成就熄灭可以快速确认触摸扫描是否在按预期工作。本文还有配套的精品资源点击获取
返回列表