我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

STM32指纹考勤机从零到交付:AS608驱动、数据存储与上位机联调实战

STM32指纹考勤机从零到交付:AS608驱动、数据存储与上位机联调实战 简介这是一份基于STM32F103RCT6与AS608指纹模块的指纹考勤机项目设计文档面向嵌入式系统开发者、电子专业学生及企业信息化管理人员完整阐述从硬件选型到上位机软件实现的考勤系统方案。资源包共1个文件为PDF格式大小19.17MB内容涵盖STM32开发板与AS608模块接线说明、Keil工程创建、通信协议约定、Qt(C)上位机程序设计逻辑以及考勤数据管理、账号配置、日志显示等功能模块介绍。文档还结合项目需求梳理了高效精准指纹识别、可靠串口通信、便捷数据管理三大设计目标并给出预期效果与总结。目前已有277人学习下载适合需要借鉴完整嵌入式考勤项目架构、了解STM32与指纹模块联调及上位机交互设计的读者。通过阅读可掌握指纹采集、数据传输与上位机管理的全流程实现思路为自主开发或课程设计提供直接参考。1. 指纹考勤机为什么看着简单做起来却总有“最后一个坑”如果你搜过“STM32 指纹考勤机”大概率已经看过一堆 AS608 例程几乎每一篇都号称能录入、能搜索、能亮灯。可真按那些例程把板子焊出来绝大多数人会在两个地方卡住一是指纹头长时间跑着跑着就“不认手指”二是好不容易识别成功了上位机收到的考勤记录却对不上日期。这题目最容易被低估的从来不是“驱动指纹模块”而是“考勤机”这三个字背后的数据处理逻辑。本篇文章不讲那种只亮一盏灯的玩具 Demo而是把一台能从线下录指纹、存记录、再到 PC 端生成报表的完整考勤机方案拆开讲清楚。文章适合正在做毕设、公司门禁改造、或者想把手头开发板变成真实产品的嵌入式开发者。你不需要自己有指纹识别算法STM32 只负责跑业务逻辑指纹识别交给模块内部的 DSP 完成。真正要下功夫的是协议封装、存储时序、上位机接口这三段。下面按我做这套东西的顺序展开先选型再驱动再存储再上位机最后把最容易让人翻车的地方挑出来说。2. 硬件选型与接线STM32 和指纹头之间先解决“电”再谈“码”2.1 主控与指纹头选型先想清楚你要的是“能识别”还是“能交付”STM32 这个平台上做指纹考勤最常见的组合是 STM32F103C8T6 加 AS608 光学指纹头。F103C8T6 有 64KB Flash、20KB RAM、3 个 USART跑一个考勤机的业务逻辑完全够用而且价格透明、开发资料多。选它还有一个实际原因指纹头的 UART 通信速度很低哪怕你以后想加 OLED 菜单、蜂鸣器、按键扫描、RTC同时跑起来也不会有性能压力。AS608 这类模块内部已经集成了指纹采集、特征提取和 1:1/1:N 匹配算法对外只暴露一套串口指令。这意味着你不需要在 STM32 里移植任何指纹算法只需要把模块返回的“识别分数”当作一个可信结果。听起来很轻松但你要注意它的边界AS608 常见版本出厂指纹库容量在 300 到 1000 枚之间录入一枚指纹需要按两次手指搜索速度在百枚量级约一秒上下。如果你要做的考勤机是五十人以内的小团队这个量级刚好如果目标是上千人的园区选型就得换成半导体指纹头比如带 FPC 电容阵列的模块。我一般会选择 AS608 的“蓝色开发板”版本原因不是外观而是它把触发电平、LED 指示、串口引脚都引出来了方便初期在面包板上验证协议。量产时再换模块底下的裸板引脚定义基本一致。唯一要改的是供电方案这个放到后面讲。2.2 AS608 串口包格式不要一上来就抄例程先把校验和讲明白AS608 的串口默认参数是 19200 bps、8 位数据、1 位停止位、无校验。很多老课程设计喜欢用 9600原因是 51 单片机的波特率误差小但 STM32 的 USART 用 19200 一样可以做到很低误差没必要主动降速。因为我后面要接上位机总线上还可能挂显示模块统一用 19200 会更顺。它的数据帧结构固定为四段包头、设备地址、包标识、包内容加校验。包头永远是EF 01 FF FF前两个字节EF01是握手码后两个字节FFFF表示模块地址。如果你只接一个指纹头地址不用改如果一条总线上并联多个指纹模块就需要通过指令改模块地址否则所有模块都会响应同一条指令。包标识也就是 PID常用值是01表示命令包、07表示应答包。这里最容易写错的是包长度字段和校验和的累计范围。包长度只统计“PID 之后的指令码加参数”一般是一条命令去掉包头后的字节数减一。校验和则是从 PID 开始往后累加加到最后一个参数为止取低 16 位。网上很多例程把包头一起算进校验和结果就是模块要么不回包要么回一个“校验错误”的应答。下面这个表格是几个常用命令的完整帧你可以对着串口抓包验证命令完整帧内容校验和低字节采图 GenImgEF 01 FF FF 01 00 03 01 00 0005生成特征 Img2TzEF 01 FF FF 01 00 03 02 00 0107搜索 SearchEF 01 FF FF 01 00 07 04 00 01 00 00 00 FF0C看完这个表你会明白所谓“指纹识别”对 STM32 来说只是发几个固定字节再读回一个固定长度的应答。真正的复杂度在于你如何把“采图、生成特征、合成模板、入库”这几个步骤串成一个不会超时的状态机。2.3 接线与供电指纹头一上电就重启多半是电的问题硬件上最容易踩的第一个坑就是电源。AS608 内部有一个微型电机或者光学采集部分手指按上去的瞬间电流会明显上升。如果你让指纹头和一个 2.4G 无线模块共用一路 3.3V 的 LDO那么每次采集图像的瞬间LDO 输出电压都会塌陷一下表现就是屏幕闪一下、指纹头响起“咔”一声重新初始化。所以我的做法是给指纹头单独一路供电可以用一个 500mA 以上的 LDO并在指纹头 VCC 引脚旁边放一个 100μF 电解电容或两个并联的 47μF 钽电容。STM32 本身用另一路供电两路共地即可。串口电平也要注意。AS608 的官方手册写的是 TTL 电平很多模块板载了电平转换可以直接接 3.3V 单片机。如果是从不明渠道买的裸模块先量一下模块 VCC 和 TXD 引脚的空载电压不要直接往 5V 单片机的 USART 上接。另一个容易忽略的是模块上的RST引脚有些模块的这个引脚是漏极开路需要外部上拉到 VCC悬空时指纹头偶尔会自己复位。我习惯把 RST 接一个 GPIO上电初始化时先拉低 10ms 再拉高这样保证指纹头从已知状态启动。WAK 引脚则用来检测有没有手指按住实际项目中比轮询“采图指令”更省事串口上少一堆无效交互。3. 固件侧核心实现AS608 指令封装与“录入、搜索”状态机3.1 封装一个能自己算校验和的指令发送函数先不要急着写业务逻辑所有指纹操作都要经过同一个指令发送函数这是整个固件最值得做对的一层。我的做法是把“包头 PID 长度 指令参数 校验和”用数组拼好然后从串口 2 发出。注意两点长度是指令加参数的总字节数校验和从 PID 字节开始累计不包含包头。static uint8_t as608_send_pkt(uint8_t pid, uint8_t *payload, uint8_t payload_len) { uint8_t frame[64]; uint16_t sum 0; uint8_t i, idx 0; frame[idx] 0xEF; frame[idx] 0x01; frame[idx] 0xFF; frame[idx] 0xFF; frame[idx] pid; frame[idx] 0x00; frame[idx] payload_len; for (i 0; i payload_len; i) { frame[idx] payload[i]; } // 校验和只从 PID 开始累加三个包头字节不参与 for (i 5; i idx; i) { sum frame[i]; } frame[idx] (sum 8) 0xFF; frame[idx] sum 0xFF; for (i 0; i idx; i) { while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, frame[i]); } return wait_as608_ack(1000); }这段代码里pid参数固定传0x01表示命令包但也有特殊情况需要自己传应答包 PID后面删除指纹时会用。wait_as608_ack(1000)是等待模块回复的阻塞函数超时设为 1000ms。为什么不用中断队列因为 AS608 的应答非常短通常 9 到 12 个字节串口中断回调里做状态判断还不如这个简单的阻塞等待清晰。当然如果你在同一颗 MCU 上还要跑 LCD 刷新建议把超时判断改成非阻塞状态机但初版调通功能时没必要。wait_as608_ack内部做的就是按字节收收满 9 个字节后检查帧尾校验和确认无误再返回应答内容。这里有一个很值得提的细节AS608 的应答帧 PID 是07但有的山寨模块会返回08或者把校验和清零批量采购时一定要先用串口助手抓包确认再写进代码。3.2 录入指纹两次采集 一次合成别漏了“移开手指”的提示录入一枚指纹的完整流程是采图、生成特征、要求用户再次按手指、再次生成特征、合成模板、入库。很多人把第三步写成了连续两次采图结果第二次采到的还是同一根手指的同一角度导致合成出来的模板质量差。正确做法是第一次采图后提示用户“移开手指”等手指完全离开再提示“再次按下”。指纹头去抖时间我给 300 毫秒太快会误判。// 录入指纹store_id 是用户要存到的指纹库编号 uint8_t fp_enroll(uint16_t store_id) { uint8_t p[8]; uint8_t resp; // 第一次按手指 p[0] 0x01; p[1] 0x00; p[2] 0x00; // GenImg resp as608_send_pkt(0x01, p, 3); if (resp ! 0x00) return ENROLL_NO_FINGER; // 应答高字节非0表示没人按 p[0] 0x02; p[1] 0x00; p[2] 0x01; // Img2Tz写入缓冲区1 resp as608_send_pkt(0x01, p, 3); if (resp ! 0x00) return ENROLL_IMG_BAD; beep_times(2); // 提示用户手指质量合格请移开 delay_ms(300); // 第二次按手指 p[0] 0x01; p[1] 0x00; p[2] 0x00; resp as608_send_pkt(0x01, p, 3); if (resp ! 0x00) return ENROLL_NO_FINGER; p[0] 0x02; p[1] 0x00; p[2] 0x02; // 存到缓冲区2 resp as608_send_pkt(0x01, p, 3); if (resp ! 0x00) return ENROLL_IMG_BAD; // 合成模板 p[0] 0x05; p[1] 0x00; p[2] 0x00; resp as608_send_pkt(0x01, p, 3); if (resp ! 0x00) return ENROLL_MERGE_FAIL; // 入库 p[0] 0x06; p[1] 0x00; p[2] 0x01; p[3] (store_id 8) 0xFF; p[4] store_id 0xFF; resp as608_send_pkt(0x01, p, 5); if (resp ! 0x00) return ENROLL_STORE_FULL; return ENROLL_OK; }这段代码看着很短但每次调用都包含了两次完整的手指采集过程所以执行时间大约在 2 到 4 秒取决于用户反应速度。其中p[2]这个参数决定了特征放到缓冲区 1 还是缓冲区 2第一次采集必须放 1第二次放 2顺序反了合成出来的模板会报“数据不匹配”。还要注意store_id的范围。AS608 的库地址是 16 位但实际容量只有几百如果你给员工编号从 1000 开始而模块容量只有 300那么store_id必须重映射到 0 到 299 的区间。否则会出现一个诡异现象录入时提示成功搜索时却永远搜不到因为搜索时用的页数和库容量都对不上。3.3 1:N 搜索与超时复位让识别动作变成可重复的稳定事件验证指纹时最常用的指令是 Search它一次性地在当前库中搜索所有指纹。我给它的参数是缓冲区 ID 为 1、起始页为 0、页数设为0xFF也就是说让模块从库的首地址一路搜到尾。uint16_t fp_search(uint16_t *matched_id) { uint8_t p[7]; uint8_t resp[16]; uint8_t len; // 先采图 p[0] 0x01; p[1] 0x00; p[2] 0x00; if (as608_send_pkt(0x01, p, 3) ! 0x00) return SEARCH_NO_FINGER; // 生成特征到缓冲区1 p[0] 0x02; p[1] 0x00; p[2] 0x01; if (as608_send_pkt(0x01, p, 3) ! 0x00) return SEARCH_IMG_BAD; // Search 指令参数缓冲区1起始页0页数255 p[0] 0x04; p[1] 0x00; p[2] 0x01; p[3] 0x00; p[4] 0x00; p[5] 0x00; p[6] 0xFF; if (as608_send_pkt(0x01, p, 7) ! 0x00) return SEARCH_NOT_FOUND; // 读应答包userID在应答数据偏移3和4 if (read_as608_reply(resp, len) ! 0x00) return SEARCH_NOT_FOUND; *matched_id (resp[3] 8) | resp[4]; return SEARCH_OK; }这段代码里read_as608_reply是我封装好的读取函数它会先收 9 字节头再根据头里的包长度收剩余字节。搜索成功时AS608 返回的应答数据里包含匹配到的用户编号和匹配分数。我建议业务上不要只看编号还要把匹配分数也带上低于 80 分的结果在考勤场景里应当视为无效因为手指有汗渍或者传感器上有灰时分数会显著下降直接认成同一个人会影响考勤准确性。这里要提一个“玄学”级别的问题指纹头用久了以后搜索指令偶尔会不返回任何应答直到超时。原因是 AS608 内部 DSP 正在处理上一张图像串口被占死。解决办法是在初始化时把 RST 引脚拉低 50ms 再拉高并在每次搜索前检查WAK引脚如果手指一直在传感器上没有离开就不要发新的采图指令。很多“用着用着指纹头死了”的报告其实都是这个原因。4. 考勤数据链路RTC、离线存储与防重复打卡4.1 记录结构设计一条考勤记录到底要存哪几个字段指纹验证成功只是第一步接下来要把“员工编号 时间 上下班标志”写成一条记录。很多人用字符串2025-03-30 08:32:30直接存进 SD 卡看起来直观实际在 STM32 上很笨重一条记录占 20 多个字节写 SD 卡还要处理 FATFS 的扇区缓冲断电时极易丢最后几条记录。我的做法是把记录压缩成固定 8 字节结构存进外部 EEPROM比如 AT24C64。8KB 空间能存 1024 条记录按一个 30 人的团队每天产生 60 条上下班记录算大约能覆盖两周不掉电数据。桌面考勤机每天同步一次这个容量足够了。typedef struct { uint16_t emp_id; // 员工指纹编号重映射到0~299 uint16_t minutes; // 当天从00:00开始计算的分钟数 uint8_t day; // 日1~31 uint8_t month; // 月1~12 uint8_t duty_type; // 0上班1下班 uint8_t reserved; // 预留字段可用于校验 } __attribute__((packed)) attend_record_t;用整数保存时间有三个好处一是无论 RTC 怎么初始化只要日期部分正确记录就能按月份排序二是上位机拿到minutes后自己换算成时:分不需要在单片机里处理%02d这种格式化输出三是定长结构可以用“循环覆盖”策略写入时只维护一个写指针不用频繁擦除整片 EEPROM。reserved字段我用来存一个固定值0xA5。上位机读取记录时检查这个字节如果不对就说明存储区发生了错位直接丢弃并报警而不是把乱数据混进报表。这种冗余字段看起来浪费时间但在真实项目里能救你很多次。4.2 防重复打卡时间窗判断为什么不可靠考勤逻辑最常见的需求是“一个人一个班次只能打一次卡”。很多设计直接比较上次打卡时间如果间隔小于 5 分钟就忽略。这样写有个明显漏洞员工在 08:29 打了上班卡机器判断间隔不足把 08:31 那次也忽略了但上午那次实际上没有正确记录到“上班”状态。更稳妥的是把考勤时间分成两个区间工作开始前后各 4 小时算“上班区间”午休后 4 小时算“下班区间”。判断重复时不看“距离上次几分钟”而是看“今天这个区间是否已经有一条记录”。这样做的逻辑很直白你一天只能有一个上班记录和一个下班记录重复按压只是刷新当前记录的时间而不是新增记录。uint8_t duty_type_of(uint16_t minutes) { // 上午 07:00~11:59 记为上班 if (minutes 7 * 60 minutes 12 * 60) return DUTY_IN; // 下午 13:00~18:59 记为下班 if (minutes 13 * 60 minutes 19 * 60) return DUTY_OUT; return DUTY_NONE; }这个判断函数定死了时段实际部署时应该允许用户在管理端配置。比如有些公司上班时间是 09:00上午区间就应该调整成 08:00 到 12:00。我把这些区间参数也存进 EEPROM并在上位机下发配置时直接覆盖。这样做的好处是换季调整上班时间时不需要重新给每台考勤机烧录固件。还有一种情况要特别注意跨天打卡。如果员工晚上 20:00 加班到第二天凌晨 02:00他凌晨打卡会被判定为“下班区间无记录”导致漏记。我的做法是允许配置一个“凌晨缓冲区间”例如 00:00 到 04:00 的打卡时间归入昨天的下班记录。上位机处理时发现记录发生在凌晨就自动把日期减一天累加到昨天。4.3 掉电不丢记录的写入策略与上位机拉取逻辑EEPROM 的写入不是简单I2C_WriteByte就能放心的。AT24C64 按页写入一页 32 字节一次跨页写入会回卷到页首把前面数据覆盖掉。所以写一条 8 字节记录时要保证写地址不要落在页边界附近。我的做法是把整个存储区划分成 256 个记录槽每个槽 32 字节只使用前 8 字节剩下的 24 字节作为填充。虽然浪费了存储但从根上避免了跨页问题。写入前还要做一件事把新记录先写到“待定区”再移动写指针。也就是双缓冲思想。我先读回刚写入的数据做校验确认reserved0xA5才更新写指针如果校验失败写指针不动下次写入从原位置重来。这样每次掉电最多丢一条记录而不是把整个记录区写穿。上位机拉取记录时我采用“读后清”的策略。STM32 收到上位机命令后把 EEPROM 里的记录逐条通过串口发送发送完成后把写指针和所有记录槽清零。上位机则需要在这段时间内持续接收不能中途断开否则清空后数据就真的没了。为了降低风险我通常让上位机先发一个“备份完成”命令STM32 收到后才清空管理端异常掉电时可以重新拉取旧数据。5. “考勤机 上位机”联调避坑串口、中文字符与报表的四条亲身记录5.1 串口连接反复掉线驱动、握手和缓冲区顺序做的过程中最让人头疼的问题是上位机软件明明打开了串口但 STM32 发过来的数据偶尔会少掉一截尤其集中在上电后的前 100 毫秒。后来抓包发现指纹模块初始化时会在串口线上产生一段乱码这段乱码被上位机解析成了错误的命令帧导致后续数据错位。现象上位机收到的报表里第一条总是不完整甚至出现一个乱码的编号。原因STM32 与指纹头共用一个串口调试接口上电瞬间 AS608 输出了一串不可预期的自检信息。解决上位机连接后先发一个同步命令比如0xA5 0x5A 0x01STM32 收到后清空串口接收 FIFO再回一个确认字节。只要确认字节正确才认为链路可用。实测这样处理以后串口误码率降到了零。5.2 上位机显示的时间全部是 1970 年RTC 掉电与备份电池我第一次用标准库的 RTC 例程结果每次重新上电时间都回到默认的 1970 年。查资料才发现问题出在备份寄存器上。现象考勤机断电半天后再开机打卡记录里的时间全部变成 1970-01-01。原因STM32 的 RTC 需要外部 32.768kHz 晶振同时依靠 VBAT 引脚保持供电。很多最小系统板根本没有接 VBAT 电池断电后 RTC 直接丢失计时。解决给板子加一个 CR1220 纽扣电池座VBAT 接电池正极同时在固件里判断备份寄存器的一个标志位。如果标志位不是预设值就认为 RTC 从未初始化上位机应当在每次连接时下发当前时间。千万不要依赖“第一次烧录时设定一次”的流程因为纽扣电池电量耗尽后一切回到原点。5.3 Excel 报表里的中文姓名全部乱码编码问题我在上位机管理端用的编程语言是 Python生成报表时用openpyxl库直接写 xlsx 文件开始很顺畅直到用户要求导出 CSV 格式给财务软件用乱码就出现了。现象用 Excel 打开导出的 CSV中文全部变成“鏉庢槑”这样的怪字。原因Excel 读取 CSV 时默认使用 GBK 编码而我写入的是 UTF-8 无 BOM。解决写 CSV 时在文件开头写入\ufeff这个 BOM 头Excel 就会自动以 UTF-8 解析。更省事的办法是干脆生成 xlsx 而不是 CSVxlsx 内部编码本来就是 XML 的 UTF-8没有这个问题。但如果财务系统只认 CSVBOM 头是唯一正解。5.4 上位机指令与模块地址冲突多条 USB 转串口线导致“串口被占用”现场调试最尴尬的一次是考勤机连上 PC 后指纹头自己也能被上位机识别成串口设备。原来系统里有两条 USB 转串口线一条连 STM32 的调试口一条连指纹头单独引出的调试口。两条线同时工作时COM 口号会随机变化上位机选错 COM 口后发送的配置命令直接进了指纹头导致指纹头误以为上位机在给它换地址之后怎么发指令都不回包。现象指纹头突然不听命令上位机提示“写入超时”重新上电后恢复。原因上位机误把命令发给了指纹头的调试串口指纹模块的地址被改成非默认值。解决量产机上只保留 STM32 与上位机通信的一路串口指纹头调试口通过物理跳线断开上位机扫描可用串口时通过设备描述里的 VID/PID 做过滤不能只按 COM 口号选择。如果已经改乱了地址用 AS608 的“恢复出厂”指令把所有参数重置。6. 交付前最后一个验证技巧七天连续压力测试比任何代码审查都有效设备在实验室跑通所有功能后不要急着交付先做一个为期七天的连续压力测试。我的测试矩阵固定如下测试项每天次数通过标准断电重启后打卡10 次断电前后时间正确记录不丢失同一手指连续按 50 次2 轮误拒率不超过 2 次两根不同手指交替按50 次不会出现串号上位机同步后拔线5 次再次连接可重新拉取记录不重复手指带汗、带灰30 次匹配分数低于阈值被拒绝不误录这个测试我一般让单片机自己跑脚本模拟按键上位机这边则写一个自动化脚本每天定点拉取记录并核对日期。第七天结束时把 EEPROM 里的记录和上位机本地缓存的记录做一次异或比对任何一条不一致都要查到底绝不能认为“数据反正能导出来就行”。我早年在这类项目上交过一笔学费某次因为省掉了断电测试设备在场地上用了三天某天下午停电员工下班打卡全部丢失最后靠人工补签才解决。从那以后我的习惯是任何基于 EEPROM 的存储代码都必须做“断电后立刻读回校验”的自动化测试这个测试被写进我自己的代码模板里每个新项目都要跑一遍。考勤数据这种一天都不能丢的东西可靠性验证比功能实现更重要。希望你按照这个顺序做下来能少走一点弯路。真正有用的经验往往不是第一次跑通代码时的兴奋而是第七天还在稳定的那一点踏实感。希望帮到你。本文还有配套的精品资源点击获取
返回列表