简介:这是一份基于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 位。网上很多例程把包头一起算进校验和,结果就是模块要么不回包,要么回一个“校验错误”的应答。下面这个表格是几个常用命令的完整帧,你可以对着串口抓包验证:
| 命令 | 完整帧内容 | 校验和低字节 |
|---|---|---|
| 采图 GenImg | EF 01 FF FF 01 00 03 01 00 00 | 05 |
| 生成特征 Img2Tz | EF 01 FF FF 01 00 03 02 00 01 | 07 |
| 搜索 Search | EF 01 FF FF 01 00 07 04 00 01 00 00 00 FF | 0C |
看完这个表你会明白,所谓“指纹识别”对 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 字节作为填充。虽然浪费了存储,但从根上避免了跨页问题。
写入前还要做一件事:把新记录先写到“待定区”,再移动写指针。也就是双缓冲思想。我先读回刚写入的数据做校验,确认reserved==0xA5,才更新写指针;如果校验失败,写指针不动,下次写入从原位置重来。这样每次掉电最多丢一条记录,而不是把整个记录区写穿。
上位机拉取记录时,我采用“读后清”的策略。STM32 收到上位机命令后,把 EEPROM 里的记录逐条通过串口发送,发送完成后把写指针和所有记录槽清零。上位机则需要在这段时间内持续接收,不能中途断开,否则清空后数据就真的没了。为了降低风险,我通常让上位机先发一个“备份完成”命令,STM32 收到后才清空;管理端异常掉电时,可以重新拉取旧数据。
5. “考勤机 + 上位机”联调避坑:串口、中文字符与报表的四条亲身记录
5.1 串口连接反复掉线:驱动、握手和缓冲区顺序
做的过程中最让人头疼的问题是:上位机软件明明打开了串口,但 STM32 发过来的数据偶尔会少掉一截,尤其集中在上电后的前 100 毫秒。后来抓包发现,指纹模块初始化时会在串口线上产生一段乱码,这段乱码被上位机解析成了错误的命令帧,导致后续数据错位。
现象:上位机收到的报表里,第一条总是不完整,甚至出现一个乱码的编号。原因:STM32 与指纹头共用一个串口调试接口,上电瞬间 AS608 输出了一串不可预期的自检信息。解决:上位机连接后先发一个同步命令,比如0xA5 0x5A 0x01,STM32 收到后清空串口接收 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 而不是 CSV,xlsx 内部编码本来就是 XML 的 UTF-8,没有这个问题。但如果财务系统只认 CSV,BOM 头是唯一正解。
5.4 上位机指令与模块地址冲突:多条 USB 转串口线导致“串口被占用”
现场调试最尴尬的一次,是考勤机连上 PC 后,指纹头自己也能被上位机识别成串口设备。原来系统里有两条 USB 转串口线,一条连 STM32 的调试口,一条连指纹头单独引出的调试口。两条线同时工作时,COM 口号会随机变化,上位机选错 COM 口后,发送的配置命令直接进了指纹头,导致指纹头误以为上位机在给它换地址,之后怎么发指令都不回包。
现象:指纹头突然不听命令,上位机提示“写入超时”,重新上电后恢复。原因:上位机误把命令发给了指纹头的调试串口,指纹模块的地址被改成非默认值。解决:量产机上只保留 STM32 与上位机通信的一路串口,指纹头调试口通过物理跳线断开;上位机扫描可用串口时,通过设备描述里的 VID/PID 做过滤,不能只按 COM 口号选择。如果已经改乱了地址,用 AS608 的“恢复出厂”指令把所有参数重置。
6. 交付前最后一个验证技巧:七天连续压力测试比任何代码审查都有效
设备在实验室跑通所有功能后,不要急着交付,先做一个为期七天的连续压力测试。我的测试矩阵固定如下:
| 测试项 | 每天次数 | 通过标准 |
|---|---|---|
| 断电重启后打卡 | 10 次 | 断电前后时间正确,记录不丢失 |
| 同一手指连续按 50 次 | 2 轮 | 误拒率不超过 2 次 |
| 两根不同手指交替按 | 50 次 | 不会出现串号 |
| 上位机同步后拔线 | 5 次 | 再次连接可重新拉取,记录不重复 |
| 手指带汗、带灰 | 30 次 | 匹配分数低于阈值被拒绝,不误录 |
这个测试我一般让单片机自己跑脚本模拟按键,上位机这边则写一个自动化脚本每天定点拉取记录并核对日期。第七天结束时,把 EEPROM 里的记录和上位机本地缓存的记录做一次异或比对,任何一条不一致都要查到底,绝不能认为“数据反正能导出来就行”。
我早年在这类项目上交过一笔学费:某次因为省掉了断电测试,设备在场地上用了三天,某天下午停电,员工下班打卡全部丢失,最后靠人工补签才解决。从那以后,我的习惯是任何基于 EEPROM 的存储代码,都必须做“断电后立刻读回校验”的自动化测试,这个测试被写进我自己的代码模板里,每个新项目都要跑一遍。考勤数据这种一天都不能丢的东西,可靠性验证比功能实现更重要。
希望你按照这个顺序做下来,能少走一点弯路。真正有用的经验,往往不是第一次跑通代码时的兴奋,而是第七天还在稳定的那一点踏实感。希望帮到你。
本文还有配套的精品资源,点击获取