☰
STM32指纹考勤机从零到交付:AS608驱动、数据存储与上位机联调实战
2026/10/21 7:02:37 网站建设 项目流程

简介:这是一份基于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 字节作为填充。虽然浪费了存储,但从根上避免了跨页问题。

写入前还要做一件事:把新记录先写到“待定区”,再移动写指针。也就是双缓冲思想。我先读回刚写入的数据做校验,确认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 的存储代码,都必须做“断电后立刻读回校验”的自动化测试,这个测试被写进我自己的代码模板里,每个新项目都要跑一遍。考勤数据这种一天都不能丢的东西,可靠性验证比功能实现更重要。

希望你按照这个顺序做下来,能少走一点弯路。真正有用的经验,往往不是第一次跑通代码时的兴奋,而是第七天还在稳定的那一点踏实感。希望帮到你。

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

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

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

立即咨询