☰
STM32指纹签到系统:从外设驱动到Flash存储的完整工程解析
2026/10/27 8:44:30 网站建设 项目流程

简介:基于STM32的指纹签到系统优秀毕设项目包,适配毕业设计、课程设计、大作业等场景。面向计算机、电子、自动化等相关专业在校学生与开发者,解决课堂/办公签到场景的嵌入式综合设计需求,兼顾入门学习与二次开发。压缩包共398个文件,约14.75MB,源码以C源文件与头文件为主体,包含Keil工程配置文件、编译生成的hex/axf/map等固件产物,也有png图片和txt文档等说明材料,结构清晰便于查看与移植。目前已有111人学习下载。项目包含完整源码、详细文档和辅助脚本,代码经测试运行通过,可直接编译烧录,也支持在此基础上修改扩展,适合毕业设计答辩演示和课程作业参考。

1. 指纹签到系统是什么:一个把外设驱动、数据库和交互都装进 STM32 的完整工程

实验室考勤、社团活动签到、小型办公室打卡,这些场景背后其实是同一套需求:验证身份、记下时间、事后可查。基于 STM32 的指纹签到系统这份源码工程,就是把这套业务闭环塞进一块 MCU——指纹模块确认“你是谁”,RTC 提供可信时间,内部 Flash 存签到记录,OLED 和按键完成人机交互。它不是用来演示怎么点灯的教程代码,而是一个能跑通完整业务流程、可以直接当课程设计或毕业设计验收的工程,源码结构、硬件接线和文档资料都是配套好的。适合想把 STM32 从驱动做到应用层、想搞懂外设协同和掉电数据管理的嵌入式开发者。

2. 硬件组成与工程结构:先拆清单片机和外设的协作骨架

拿到一份指纹签到系统源码资料,别急着打开 Keil 编译。先把硬件拓扑和工程目录读明白,后面调代码才能对号入座。这个系统的硬件规模不大:主控、指纹模块、显示、按键、存储,最多加一个蜂鸣器,但每部分都承担明确的职责,正好用来理解嵌入式系统的“外设协作”。

2.1 主控与指纹模块:STM32 选型、UART 接线和电平匹配

这套工程最常见的主控是 STM32F103C8T6,也就是大家常说的蓝色 Pill 板,也有部分资料用 STM32F407VET6。C8T6 有 64KB Flash、20KB RAM,对一套签到系统来说资源刚好;如果工程里带了 ILI9341 大屏或者要扩展文件系统,资料包可能就选了引脚更富余的 VET6。指纹模块基本都是 UART 接口的 AS608、R307 或 FPM10A,默认波特率 57600,个别模组出厂是 9600,第一步就要确认。

接线看起来简单,但方向最容易反:

指纹模块引脚STM32 引脚说明
VCC5V 或 3.3V看模组供电规格,AS608 很多支持 3.3~6V
GNDGND必须共地
TXMCU 的 USARTx_RX模块发,MCU 收
RXMCU 的 USARTx_TXMCU 发,模块收

指纹模块上的 TX 接 STM32 的 RX,这是“交叉连接”,接成直连是第一个常见翻车点。第二个坑是电平:如果模组 5V 供电,它的 TX 输出高电平可能到 5V,F103 的 RX 引脚不是 5V tolerant,短时间能顶,长期不可靠。稳妥做法是给 RX 串一个 1k 电阻分压,或者直接用 3.3V 给模组供电。很多资料包原理图里已经画了电平转换电路,照着接就行。

用 HAL 库初始化串口的代码一般是这样的:

/* 指纹模块接 USART1:PA9=TX,PA10=RX */ void Fingerprint_UART_Init(void) { __HAL_RCC_USART1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_9; /* TX 引脚 */ GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_10; /* RX 引脚 */ GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); huart1.Instance = USART1; huart1.Init.BaudRate = 57600; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&huart1); }

这段初始化代码的重点在 BaudRate 必须和模组一致,AS608 出厂常见 57600。GPIO 模式上,TX 脚要配成复用推挽,RX 脚输入模式要带上拉,否则空闲时 RX 电平不确定,容易收到乱码。如果工程用的是标准库,初始化思路完全一样,只是 API 换成 GPIO_InitTypeDef 加 USART_Init,引脚和波特率不要动。

2.2 显示与交互:OLED/LCD 菜单、按键矩阵和状态提示

签到系统的业务流程不复杂,但状态必须让使用者一眼看懂:待机时显示当前时间和今天已签到人数,按下菜单键进入录入、删除、查询,按下确认键执行当前项,签到成功给一声蜂鸣器提示。显示模块的选择直接影响代码体积——SSD1306 的 OLED 驱动轻量,12864 分辨率显示两行菜单够用;ILI9341 的 LCD 能显示一屏历史记录,但驱动初始化序列更长,对 C8T6 的 Flash 占用更明显。

按键部分,很多资料包的做法是三个独立按键:菜单、确认、返回/清除,通过轮询或者外部中断扫描。源码里通常会有一个按键状态机,把短按、长按和连按区分开。菜单切换写成这样:

uint8_t menu_index = 0; /* 0-录入,1-查询,2-清空 */ void Menu_Tick(KeyEvent ev) { if (ev == KEY_SHORT_PRESS) { menu_index = (menu_index + 1) % 3; /* 短按循环切换菜单项 */ OLED_Clear(); Show_Menu(menu_index); } else if (ev == KEY_LONG_PRESS) { Enter_Function(menu_index); /* 长按确认进入当前功能 */ } }

菜单索引的循环取模看着简单,但它决定了按键响应的一致性——短按一次只跳一项,长按才确认,避免用户“按一下进两层”的误操作。实际工程里,主循环和菜单循环不能互相阻塞,常见的做法是一个 tick 函数放到 main 的 while(1) 里反复调用,显示刷新放到另一个定时器中断里。OLED 显示本身有耗时,别在按键中断里直接刷屏,否则按下瞬间会卡顿。

2.3 工程目录与源码分层:看懂资料包里的 USER、HARDWARE 和 DOC

资料包打开后,源码工程的目录结构比代码本身更能说明问题。一套合格的 STM32 工程,至少会把主流程、外设驱动和业务逻辑拆开,而不是全部堆在 main.c 里。我一般会按下面这张表快速定位关键文件:

路径或文件主要内容打开后先看什么
USER/main.c主流程、初始化顺序、状态机main 函数里外设初始化顺序
HARDWARE/finger.c指纹模块指令封装用的串口号、波特率、指令封装
HARDWARE/oled.c显示驱动是 SSD1306 还是 ILI9341
HARDWARE/flash.c签到记录存取Flash 地址定义、写入函数
HARDWARE/rtc.c时间管理是否用 LSE 外部晶振、VBAT 电池
DOC / 根目录原理图、接线说明、使用说明把文档里的引脚分配和实际接线核对

这里最容易忽略的一步:把文档里的引脚分配表和你的实际板子跳线逐一核对。十个工程有九个是程序没问题但引脚对不上,尤其是指纹模块的串口被初始化程序写死成 USART1,底板却把模组接到了 USART3,代码再对也跑不起来。

3. 指纹录入与比对:从指令包到特征存储的完整数据流

指纹模块是整个系统里最“黑匣子”的部分,但它的交互其实很规矩:MCU 发一条指令,模块执行后回一个应答包。把这条指令链路拆开,指纹录入和比对的逻辑就透明了。

3.1 指纹模块的指令包结构:包头、地址、长度和校验和

AS608 和 FPM10A 这类 UART 指纹模块,通信协议基本一致,数据包由 6 字节包头加地址、包标识、长度、指令、参数和校验和组成。包头固定是 0xEF 0x01,接着是 4 字节模块地址,出厂默认 0xFF 0xFF 0xFF 0xFF;然后是包标识,0x01 表示指令包,0x07 表示应答包;后面是长度、指令码、参数和数据,最后跟 2 字节校验和。

写指令封装时,校验和是初学者最容易算错的地方。注意校验和不是只算指令码,而是从包长度开始,对长度、指令、参数和数据的所有字节累加:

void FPM_SendPacket(uint8_t cmd, uint16_t param1, uint16_t param2, uint8_t *payload, uint8_t payload_len) { uint8_t buf[32]; uint8_t idx = 6; buf[0] = 0xEF; buf[1] = 0x01; /* 包头 */ buf[2] = 0xFF; buf[3] = 0xFF; /* 默认地址 */ buf[4] = 0xFF; buf[5] = 0xFF; buf[idx++] = 0x01; /* 指令包标识 */ uint16_t len = 1 + 4 + payload_len; /* 指令 1 字节 + 参数 4 字节 + 数据 */ buf[idx++] = len >> 8; buf[idx++] = len & 0xFF; buf[idx++] = cmd; /* 指令码,如 0x01 采集指纹 */ buf[idx++] = param1 >> 8; buf[idx++] = param1 & 0xFF; buf[idx++] = param2 >> 8; buf[idx++] = param2 & 0xFF; if (payload_len) { memcpy(&buf[idx], payload, payload_len); idx += payload_len; } /* 校验和 = 长度 + 指令 + 参数 + 数据,取低 16 位 */ uint16_t sum = (len >> 8) + (len & 0xFF) + cmd + (param1 >> 8) + (param1 & 0xFF) + (param2 >> 8) + (param2 & 0xFF); for (uint8_t i = 0; i < payload_len; i++) { sum += payload[i]; } buf[idx++] = sum >> 8; buf[idx++] = sum & 0xFF; HAL_UART_Transmit(&huart1, buf, idx, 500); }

这段封装函数是所有指纹操作的地基。调用例:采集图像就是 FPM_SendPacket(0x01, 0, 0, NULL, 0),搜索就是 FPM_SendPacket(0x04, 0, 100, NULL, 0)。参数 1 和高低字节的顺序按模块手册来,不同固件对参数含义有差异,改指令之前先把手册对应页看明白。应答包同样固定格式,其中确认码才是判断成败的关键:

确认码含义处理建议
0x00指令执行成功继续下一步
0x01收包错误 / 指令错误重发指令
0x02传感器无手指提示用户重新按压
0x0A指纹特征提取失败检查手指是否干净、按压力度
0x0B特征合成失败让用户换一个手指角度再来

3.2 录入流程:三次按压、模板合成与存储

指纹录入的标准流程是三次按压取特征,再合成模板,最后存进指纹库。为什么必须三次?因为一次按压只能拿到一组指纹特征点,单次特征受手指位置、力度影响太大;三次采集后模块用 RegModel 指令合成一个更稳定的模板。合成失败的原因往往是三次按压时手指位置差异过大,或者模块返回 0x0B。

录入流程代码一般长这样:

#define FINGER_ID 1 /* 录入到 ID=1 */ void FPM_Enroll(uint16_t id) { uint8_t ack = 0xFF; for (uint8_t i = 0; i < 3; i++) { OLED_ShowStr(0, 0, "Press finger", BLUE); /* 提示第一次按压 */ while (Finger_IsTouched() == 0); /* 等待手指按下 */ FPM_SendPacket(0x01, 0, 0, NULL, 0); /* GenImg 采集图像 */ FPM_WaitAck(&ack); /* 读取确认码 */ if (ack != 0x00) { /* 0x00 成功,0x02 无手指 */ OLED_ShowStr(0, 1, "No finger", RED); i--; /* 本次按压不算 */ continue; } OLED_ShowStr(0, 1, "Move finger", BLUE); /* 提示抬起,换位置 */ HAL_Delay(500); } FPM_SendPacket(0x05, 0, 0, NULL, 0); /* RegModel 合成模板 */ FPM_WaitAck(&ack); if (ack != 0x00) { OLED_ShowStr(0, 1, "Enroll fail", RED); return; } FPM_SendPacket(0x06, id, 2, NULL, 0); /* Store 存到 ID=id 页 2 */ FPM_WaitAck(&ack); if (ack == 0x00) { OLED_ShowStr(0, 1, "Enroll OK", GREEN); } }

这里有个细节值得注意:第三次按压后我没有再单独等待“手指抬起”,但在第二次和第三次之间留了 500ms,目的是让模块内部把上一次的采集状态清掉。如果按键按得太快,三次特征差异过小,RegModel 合成的模板质量会打折扣。另外在录入前,通常先做一次 Search 检查库里有没有相同指纹,防止同一个手指反复录入占满 ID 空间。

3.3 比对与搜索:匹配分数阈值、返回 ID 与业务联动

签到时,模块采集一次图像,然后执行 Search 指令,在指纹库里搜索匹配模板,返回匹配到的 ID 和分数。分数表示相似度,不同模组固件的分数范围不一样,AS608 常见在 0~255 之间,阈值取多少直接影响误识率和拒真率。

int16_t FPM_Search(void) { uint8_t rbuf[16]; FPM_SendPacket(0x04, 0, 100, NULL, 0); /* Search:从第 0 页开始搜 100 页 */ HAL_UART_Receive(&huart1, rbuf, 16, 2000); if (rbuf[6] != 0x03 || rbuf[9] != 0x00) { /* 应答包标识和确认码判断 */ return -1; } uint16_t id = (rbuf[10] << 8) | rbuf[11]; /* 匹配到的指纹 ID */ uint16_t score = (rbuf[12] << 8) | rbuf[13]; /* 匹配分数 */ return (score >= 50) ? id : -1; /* 阈值低于 50 不认 */ }

这里的应答包解析偏移需要根据模组手册核对,不同固件可能在确认码后多出保留字节。阈值建议别开始就调到 80 以上,否则干燥季节手指脱皮时拒真率会高到受不了。我一般先用 50 跑一周,看误识率再往上微调。签到系统的重点是正确绑定人,不是追求极限识别速度,所以阈值取中档比取高档实用。

4. 签到记录落地:RTC 时间戳、Flash 存储与掉电恢复

指纹签到系统的“签”只是前半段,后半段是把“谁、什么时间、签没签过”这个事实可靠地记录下来。这部分拼的不是算法,而是嵌入式系统里最容易翻车的两个环节:时间来源和数据持久化。

4.1 RTC 校时与后备电池:为什么签到时间必须走 RTC

签到记录里最重要的一列是时间戳。如果用 HAL_GetTick() 或者毫秒计数器记时间,上电就归零,掉电后记录连“今天”都看不出来。所以这套工程的核心设计之一,就是用 STM32 片上 RTC 配合 32.768kHz 外部低速晶振提供绝对时间,VBAT 引脚再接一颗纽扣电池,这样主电源断电后 RTC 还在走,签到记录的时间戳才有意义。

RTC 配置代码里有两个参数必须调对,异步分频和同步分频:

void RTC_Config(void) { __HAL_RCC_PWR_CLK_ENABLE(); __HAL_RCC_RTC_ENABLE(); HAL_PWR_EnableBkUpAccess(); /* 开放备份域访问权限 */ __HAL_RCC_RTC_CONFIG(RCC_RTCCLKSOURCE_LSE); /* 时钟源选 32.768kHz 外部晶振 */ hrtc.Instance = RTC; hrtc.Init.HourFormat = RTC_HOURFORMAT_24; hrtc.Init.AsynchPrediv = 0x7F; /* 异步分频 128 */ hrtc.Init.SynchPrediv = 0xFF; /* 同步分频 256,得到 1Hz */ HAL_RTC_Init(&hrtc); }

AsynchPrediv 和 SynchPrediv 共同把 LSE 的 32.768kHz 分频到 1Hz,这样 RTC 每一秒计一次数。两个值不是随便配的,改乱了秒计数会不准。RTC 一般只在第一次上电或者需要校时时调用一次 SetDate/SetTime,之后不要每次都重新初始化,否则会覆盖已走的时间。资料包里如果带了上位机校时功能,通常是串口下发一个时间结构体,主控解析后调用 HAL_RTC_SetDate。

4.2 内部 Flash 存储:地址划分、按页擦除与滚动写入

签到记录属于非易失数据,存哪?常见方案有三种:内部 Flash、外部 I2C EEPROM(AT24C02)、外挂 SPI Flash。对于课程设计级别的系统,用内部 Flash 最省事,不需要额外接线,但有两个限制:必须按页擦除、而且不能和代码放在同一页。

F103C8T6 的 Flash 每页 1KB,代码通常从 0x08000000 开始烧录,我可以把最后一页留给签到记录。一个典型的地址划分如下:

区域地址范围用途
代码区0x08000000 ~ 0x0800F7FF程序代码和只读常量
配置信息0x0800F800 ~ 0x0800FBFF校时标志、指纹 ID 占用表
签到记录区0x0800FC00 ~ 0x0800FFFF按条追加签到记录

把记录区放在最后一页有一个好处:即使擦写逻辑写崩了,也不会把代码区抹掉,整板还能通过烧录器恢复。写入 Flash 的代码要遵循“解锁—擦除—写入—加锁”的顺序:

typedef struct { uint16_t fingerID; uint16_t year; /* 直接存 uint16_t,避免 BCD 转换 */ uint8_t month; uint8_t day; uint8_t hour; uint8_t minute; uint8_t second; uint8_t status; /* 0x00 正常,0xFF 空闲记录 */ } SignRecord; #define SIGN_PAGE_ADDR 0x0800FC00 void Flash_WriteRecord(uint32_t addr, SignRecord *rec) { HAL_FLASH_Unlock(); /* 写之前必须擦除扇区,内部 Flash 不能只改单个字节 */ FLASH_EraseInitTypeDef erase = {0}; erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.PageAddress = SIGN_PAGE_ADDR; erase.NbPages = 1; uint32_t error = 0; HAL_FLASHEx_Erase(&erase, &error); /* 把结构体按半字写入 Flash */ uint16_t *p = (uint16_t *)rec; for (uint8_t i = 0; i < sizeof(SignRecord) / 2; i++) { HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, addr + i * 2, p[i]); } HAL_FLASH_Lock(); }

这段代码有一个隐藏问题:每写一条记录就把整页擦掉一次,一页 1KB 存不了几十条,这种方案只能应付演示。工程里如果要用得久,就得做滚动存储——记录按顺序追加到页内的空闲区域,页写满了再换下一页,而不是每次擦整页。我见过不少资料包用最简单的整页擦写法,断电后只有最后一条记录能留下,这种情况在验收时会被当场问住。

4.3 签到状态机:防重复签到、失败回滚与串口导出

签到不是只有“匹配成功”一个动作,还要处理“这个人今天已经签过”和“存储失败”两个分支。状态机一般写成这样:

void SignByFingerID(uint16_t id) { if (IsSignedToday(id)) { /* 查今天记录里有没有这个 ID */ OLED_ShowStr(0, 0, "Repeat", RED); Buzzer_Beep(2); return; } SignRecord rec = {0}; rec.fingerID = id; RTC_GetDateTime(&rec); /* 取当前时间戳 */ if (Flash_AppendRecord(&rec) == OK) { /* 追加写,不擦整页 */ OLED_ShowStr(0, 0, "Sign OK", GREEN); Buzzer_Beep(1); } else { OLED_ShowStr(0, 0, "Save Fail", RED); /* 存储失败不回写状态,允许下次重试 */ } }

这里的防重复签到用的是一个“今日签到 ID 表”,每天零点清空,或者用日期段和 ID 组成复合键去查 Flash。很多工程直接线性扫描整页记录看有没有同日期同 ID,记录多了会变慢,但签到系统一天几十条,线性扫描完全够用。

调试时把记录导出来看是最直观的。通过 USART2 或者虚拟串口,把内存里的 SignRecord 结构体逐个打印成 CSV 行:

printf("%d,%04d-%02d-%02d %02d:%02d:%02d\r\n", rec.fingerID, rec.year, rec.month, rec.day, rec.hour, rec.minute, rec.second);

导出的数据可以直接喂给 Excel 或上位机,用来核对“签到时间是否连续、有没有重复 ID”。工程里如果带了上位机源码,通常就是分析这个 CSV 协议生成考勤表的。

5. 指纹签到系统的 4 个典型问题与排查:现象、原因、解决

这套工程跑不起来的原因,百分之八十集中在通信、显示、存储和按键四个方向。把下面四条排查经验对齐,大部分问题能自己定位,不用求人。

5.1 指纹模块没反应或回包乱码

现象:代码执行到发送采集指令后,串口没有收到任何应答,或者收到的是 0x00 0xFF 之类的乱码。

原因:最常见是 TX、RX 接反了,模块的 TX 接到了 MCU 的 TX;其次是波特率不匹配,模组出厂是 57600,代码初始化成了 9600;再就是模块供电不足,按压瞬间电流大,电压跌落导致模块复位,表现就是“时好时坏”。

解决:第一步,把模组单独接到 USB-TTL 上,用串口助手手动发一条 0xEF 0x01 开头的指令,看模组是否回 ACK,这能排除 MCU 代码干扰;第二步,用万用表量模组 VCC 在按压瞬间的电压,低于 3.0V 基本就是供电问题,换独立 5V 电源;第三步,确认 TXD/RXD 交叉后再查波特率。这个顺序逻辑是从外到内,比上来就改代码快得多。

5.2 用 ILI9341 读回 ID 固定是 a1a1

现象:LCD 初始化函数里读控制器 ID,返回 0xA1A1,屏幕不亮或者花屏。这个问题在热搜词“stm32 使用 ili9341 读 id 是 a1a1”里被反复问到,属于显示驱动的高频坑。

原因:0xA1A1 不是正常的 ILI9341 ID,正常应该是 0x9341。这个返回值往往意味着屏的复位引脚时序不对、SPI 模式引脚接错,或者这块屏幕用的根本不是 ILI9341,而是 ST7789、ST7735 等兼容驱动 IC,初始化序列对不上。

解决:先把 RST 引脚拉低再释放,延时至少 120ms,很多初始化失败的根因是复位时序太短;然后核对 IM0/IM1 等模式引脚,确定屏是 4 线 SPI 还是 3 线 SPI;最后如果读 ID 还是 a1a1,大概率驱动 IC 型号不对,换对应驱动 IC 的初始化序列。这个坑在资料包里出现的频率不低,因为不少供货商会用兼容屏替代原型号。

5.3 掉电后签到记录清零

现象:系统运行正常,签到也有提示,但一断电再上电,记录全没了,或者只剩最后一条。

原因:记录只写在 RAM 数组里,没落 Flash;或者写 Flash 的方式是“先擦除整页再写一条”,所以重启后只有最近一次操作的数据;还有可能是 RTC 的 VBAT 没接电池,断电后时间归零,导致按日期查记录失效。

解决:先检查代码里有没有调用 Flash_WriteRecord,如果没有,说明这是一份阉割版资料,需要自己补存储模块;如果有,则要改成“追加写入”,先扫描页内空闲区,找到下一个可写地址再写,而不是每次擦整页。另外确认 VBAT 引脚接上纽扣电池,否则时间戳不可信。验证方法很简单:签到一条,断电,上电,把记录读出来打印,看是否完整。

5.4 按键一次触发却签到了两三次

现象:轻轻按一下菜单键,界面跳了两级,或者在签到界面按一次确认,记录里出现连续两条签到。

原因:按键没有消抖,MCU 扫描速度又很快,按下瞬间的电平抖动被识别成多次有效触发;如果用了外部中断,且中断里没有做释放判断,一次按压会连进两次中断。

解决:在按键扫描函数里加 20~50ms 延时消抖,检测到稳定低电平后再触发;更稳的做法是用“上一次状态”做边沿触发——只有电平从释放变成按下才计一次,消除了长按重复触发。如果工程里按键回调已经跑在中断里,不要在中断里直接调 HAL_Delay,改用状态机或者计时器去抖。

6. 在自己的板子上验证完整流程:三个最值得做的验收测试

6.1 断电重启后模板匹配与记录保持

这是整个工程最核心的验证:录好一个指纹 ID,正常签到一次,然后直接拔电再上电。上电后先别碰指纹头,用串口把 Flash 里的签到记录打出来,确认时间戳和 ID 都在;接着按一下手指,确认还能匹配到同一 ID。这两步同时验证了“模板库持久化”和“记录持久化”,只要有一项没过,说明存储链路有问题,后面所有功能都建立在沙子上。

6.2 连续签到压力测试与重复签到拦截

用同一个手指连续签 10 次,中间不停顿。预期结果应该是第一次提示签到成功,后面 9 次提示“重复签到”且不新增记录。这个测试除了验证防重复逻辑,还能暴露按键连触问题:如果 10 次按压出现了 10 条记录,就要回头检查按键消抖。再换一个角度:签到时故意把手指放歪、放慢,看模块会不会误配到别人的 ID,这能检验阈值设得合不合理。

6.3 用串口导出当天记录,核对时间戳

把打印函数打开,导出一份 CSV 文本,重点核对三件事:时间是不是按顺序递增、有没有同 ID 的重复值、日期在跨零点后是否正确切到新一天。RTC 的年份进位和月份进位最容易出错,比如 23:59:59 签一次,00:00:01 再签一次,两条记录日期要不同,这是用 RTC 结构体时最容易漏测的边界。

我自己的习惯是先把“断电重启 + 记录保持 + 防重复”这三个测试跑成固定流程,再做 UI 优化和上位机联动。有一回我把显示和指纹调得漂漂亮亮,以为完工了,结果一断电全清零,当场被打回原形。后来我把 RTC 校时和 Flash 追加写入当作第一优先级,反而省了大量返工时间。这套源码拿回来后,建议你也先做这个顺序,稳了再谈扩展,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询