简介:这份PDF是一套基于STM32与AS608的指纹考勤机设计方案,面向嵌入式学习者和企业信息化管理人员,解决传统考勤易出错、效率低的问题。文档从项目需求、预期效果到硬件选型逐项拆解,主控选用STM32F103RCT6,指纹模块采用AS608,并用Qt(C++)开发上位机,实现指纹ID管理、考勤记录、数据导出、系统登录、账号注册、设备配置、账号管理和日志显示等功能。资源为单个PDF文档,约19.17MB,适合作为从单片机到上位机完整通信链路的参考范例。正文覆盖前言、硬件选型、Qt上位机界面设计、Keil工程创建、通信协议约定与main.c代码,包含开发板特点、USB下载线、AS608指纹模块容量及接口等选型细节和连接方式,可直接辅助复现项目。设计总结明确了系统操作流程:单片机负责采集指纹,上位机负责指纹ID管理、考勤记录与打卡。目前已有277人学习。
1. 这套STM32指纹考勤机资料:单片机只报ID,其余全交给Qt上位机
大多数考勤机喜欢把所有逻辑都塞进设备里,录指纹、算迟到、统计报表全部在下位机完成,结果固件越堆越重,改一条规则就要重新烧录。而这份基于STM32F103RCT6和AS608指纹模块的考勤机方案,刻意反着来:单片机只负责采集指纹、比对、上报一个指纹ID,剩下打卡记录、账号管理、日志导出全部放到Qt上位机完成。这个拆分方式的直接收益是,改考勤规则不用动硬件,串口一开就能看到谁在什么时候打了卡。适合准备做嵌入式结合桌面应用、但不想在下位机上堆业务逻辑的开发者,也适合课程设计里想把“上位机+单片机”两端都展示出来的场景。资料里包含了完整的AS608驱动、通信协议约定、Qt源码和硬件接线表,照着接线就能跑通一套能用的考勤系统。
2. 硬件选型与系统分工:STM32F103RCT6、AS608和Qt这套组合够用吗
2.1 STM32F103RCT6的资源配置够不够用
资料里选的是STM32F103RCT6,这颗芯片属于F103系列里的大容量型号,配备了48KB SRAM、256KB Flash,以及多达5个串口、51个通用IO、3个SPI、2个I2C,还带USB、CAN和SDIO接口。对这个考勤项目来说,资源是明显富余的。实际项目里只用到其中两条串口:USART1做调试打印,USART2接AS608指纹模块,其余外设几乎都在空闲状态。富余资源不是浪费,而是给调试阶段留的后路——比如想再加一个蓝牙模块做无线开门、或者用I2C外扩存储,基本不用换主控。
开发板上还自带了1.4寸TFT-LCD彩屏,这一点在调试时很实用。AS608握手成功、指纹库里剩余数量、当前刷指纹命中的ID,都可以直接显示在屏幕上,不用每次都去串口助手里翻日志。LCD显示不是核心功能,但建议一开始就预留驱动,后面联调能省很多事。
2.2 AS608模块:容量、接口与自学习
AS608是资料里选定的指纹模块,接口是SH1.0-6P排线,模块作为UART从设备,由主设备发命令控制。指纹容量有50枚、100枚、300枚三档,价格对应不同,采购前先确认公司实际人数,选小了后期只能靠反复删除旧指纹来腾空间。
模块自带两个值得注意的特性。第一个是自学习功能:识别成功一次,模块会把本次采集到的特征值融合进库里已有的模板,所以同一个手指用得越多,后续识别越稳定;反过来说,如果录入时手指状态和日常使用差别很大,前期确实会出现偶尔识别失败的情况,用一段时间后会慢慢变好。第二个是Touch感应脚:手指按压到采集面时,模块Touch脚会输出高电平,下位机可以用这个脚做“是否有手指按压”的判断,避免误触发取图命令。
模块对干手指和湿手指都有适配,正常程度的手汗问题不大,但指纹浅或者脱皮严重的人群,识别率会明显下降。模块支持安全等级调节,安全等级越高匹配越严格,误识率低但拒识率升高,初期联调建议先用低等级,跑通链路后再调到生产等级。
2.3 系统分工:为什么考勤规则全放上位机
这套设计最关键的一点是把“考勤业务”从单片机里摘了出来。下位机只负责三件事:握手、录指纹、刷指纹后上报ID。上位机承担的是ID对应哪个员工、几点打的卡、是否迟到、账号权限、日志记录,全部业务逻辑放在Qt程序里。
这个分工的好处非常实际。其一,上位机改动只需要重新编译Qt工程,不用对设备端反复烧录;其二,考勤记录可以存到电脑本地文件甚至数据库里,后续要对接工资系统,直接在Qt端导出数据即可,而不是去单片机里按地址读Flash;其三,账号管理和权限区分这类功能,在单片机上实现很痛苦,在Qt里不过是几个对话框和表格模型的事。
代价是:这套系统不适合移动考勤,设备端和上位机必须通过USB串口实线连接才能工作。如果目标是多台考勤机分布式部署,那这套架构就不适用,需要改成网络方案。
表格是硬件接线时最常用到的,整理如下:
| 硬件 | 型号/规格 | 作用 |
|---|---|---|
| 主控板 | STM32F103RCT6开发板 | 主控、LCD显示、外设接口 |
| 下载线 | USB转TTL下载线 | 给STM32烧录程序 |
| 指纹模块 | AS608,SH1.0-6P接口 | 指纹采集、特征比对 |
| 通信线 | 杜邦线或模块排线 | 连接USART2与AS608 |
| 上位机 | Qt 5.12.6,C++ | 考勤管理、数据导出 |
3. 串口指令协议拆解:从$dev_cmd_lu_ru_zhi_wen到$update_find,数据包怎么设计
3.1 为什么用“$开头+冒号+数字”的文本协议
资料里约定了一套很直白的文本通信协议。上位机下发指令,格式是“$指令名:参数”,例如“$dev_cmd_lu_ru_zhi_wen:1”表示录入ID为1的指纹,“$dev_cmd_del_zhi_wen:1”表示删除ID为1的指纹。设备上传则用另外几个标记,“$update_lu_ru_ok”表示录入成功,“$update_lu_ru_error”表示录入失败,“$update_find,1,”表示搜索到ID为1的指纹并签到成功。
这种文本协议相比二进制协议,最大的优势是调试时可读性极高。直接打开串口助手,手动敲一条“$dev_cmd_lu_ru_zhi_wen:5”发给设备,就能看到下位机的行为日志。出问题时不用拿着协议文档逐个字节对比,肉眼就能发现是命令拼写错误还是参数格式不对。
下位机侧匹配使用的是C标准库的strstr函数,对传入的字符串做子串查找,所以只要命令的关键字正确,参数部分就能用strlen偏移加atoi转换得到数字。使用时有几个约定需要遵守:命令区分大小写;参数和命令之间只有一个冒号;不能带空格或其他不可见字符;每条指令后推荐带回车换行方便Qt端拆包。
3.2 三类关键时序:录入、删除、刷指纹
把协议按功能拆成三张时序表更容易理解。
| 方向 | 命令/返回 | 含义 |
|---|---|---|
| 上位机→设备 | $dev_cmd_lu_ru_zhi_wen:1 | 录入ID为1的指纹 |
| 设备→上位机 | $update_lu_ru_ok | 录入成功 |
| 设备→上位机 | $update_lu_ru_error | 录入失败(超时/多次比对失败) |
| 上位机→设备 | $dev_cmd_del_zhi_wen:1 | 删除ID为1的指纹 |
| 设备→上位机 | $update_find,1, | 刷指纹命中ID 1,签到成功 |
录入时序是流程最长的:上位机发送指令后,设备端进入“录指纹”状态机,要求按压同一根手指两次,两次特征比对一致后生成模板并存储,最后返回$update_lu_ru_ok。如果10次内没有完成有效按压,设备返回$update_lu_ru_error。这个过程最短也要十几秒,上位机侧必须做成异步等待,不能用阻塞式请求-应答。
刷指纹时序正好相反,设备端在死循环里不断调用PS_GetImage取图,一旦采集到指纹且比对成功,立即主动向上位机发送$update_find,ID,。这里没有“上位机先问、设备再答”的交互,上位机程序要随时准备接收这条上行报文并解析。
删除时序相对简单,删除单枚指纹发$dev_cmd_del_zhi_wen:ID即可,设备内部调用PS_DeletChar完成。协议里还约定了一个特殊值0xFF00,下位机识别到该值时会调用PS_Empty清空整个指纹库,这个操作在初次调试换人测试时非常有用。
3.3 Qt端串口收发的落地片段
Qt端使用QSerialPort类来对接下位机。工程配置文件里需要加一行QT += serialport,否则编译时找不到串口类。初始化代码一般写在主窗口构造函数里。
// Qt端串口初始化,MainWindow构造函数中调用 #include <QSerialPort> QSerialPort *port = new QSerialPort(this); port->setPortName("COM3"); // 端口名以设备管理器实际枚举为准 port->setBaudRate(57600); // 必须与下位机USART2波特率一致 port->setDataBits(QSerialPort::Data8); port->setParity(QSerialPort::NoParity); port->setStopBits(QSerialPort::OneStop); port->setFlowControl(QSerialPort::NoFlowControl); if (!port->open(QIODevice::ReadWrite)) { ui->log->append("串口打开失败: " + port->errorString()); } else { connect(port, &QSerialPort::readyRead, this, &MainWindow::onDataReady); }setPortName这里不能写死。不同电脑上USB转串口枚举出来的COM号不一样,应该做一个下拉框,在设备配置界面里让用户选择。波特率参数必须和下位机USART2_Init(57600)一致,改任何一端都要同步改另一端。
接收端要处理的是数据粘包问题。下位机返回的$update_find,1,后面跟着\r\n,Qt端在readyRead信号里收到的可能是不完整的一帧,也可能一次收到多帧。常见做法是维护一个接收缓冲区,把readAll()读到的数据追加进去,再按换行符逐条切出完整指令。
// 接收槽函数:按换行拆包,交给业务处理 void MainWindow::onDataReady() { QByteArray chunk = port->readAll(); rxBuffer.append(chunk); while (rxBuffer.contains('\n')) { int pos = rxBuffer.indexOf('\n'); QByteArray line = rxBuffer.left(pos).trimmed(); rxBuffer.remove(0, pos + 1); handleDeviceLine(line); } } void MainWindow::handleDeviceLine(const QByteArray &line) { if (line.startsWith("$update_find,")) { // 提取指纹ID,示例:$update_find,3, int id = line.mid(13, line.size() - 14).toInt(); appendRecord(id); // 插入考勤记录表 } else if (line.startsWith("$update_lu_ru_ok")) { ui->log->append("录入成功"); } else if (line.startsWith("$update_lu_ru_error")) { ui->log->append("录入失败"); } }把trimmed()放在拆包后处理,能把末尾的\r去掉,避免匹配时被不可见字符干扰。handleDeviceLine里匹配的是指令前缀,因为一行内可能有多个字段,比如$update_find,1,这串字符的ID部分在逗号之间,用mid直接截取比split更直观。当然用QString::split(',')处理也可以,个人习惯是在这类固定格式里用索引截取,少一次容器分配。
4. STM32固件实现与AS608驱动:状态机录指纹流程与常见问题排查
4.1 初始化顺序:为什么先释放PA15再初始化串口
main函数开头的初始化顺序是有讲究的。第一行调用JTAG_Set(JTAG_SWD_DISABLE),把PA15从JTAG功能释放为普通IO。很多开发板的PA15默认被JTAG占用,如果不释放,后面用PA15做按键或者接其他外设时会发现引脚电平不受控制。释放完引脚后才是LED、按键、延时、串口、定时器和指纹状态脚的初始化。
// main.c 初始化顺序(参考工程结构,宏定义按板卡调整) JTAG_Set(JTAG_SWD_DISABLE); // 释放PA15为普通IO LED_Init(); // 板载LED,闪烁表示程序在跑 KEY_Init(); // 板载按键,预留手动测试入口 delay_init(72); // 72MHz主频对应的延时初始化 USART1_Init(115200); // 串口1:调试日志,接电脑 TIMER1_Init(72, 20000); // 定时器,提供超时计数 USART2_Init(57600); // 串口2:接AS608指纹模块 TIMER2_Init(72, 20000); PS_StaGPIO_Init(); // AS608 Touch状态脚输入这段代码里有几个参数要留意。delay_init(72)传的是系统主频72MHz,这是F103系列标准配置。USART1_Init(115200)是调试串口,接USB转TTL看日志用;USART2_Init(57600)才是和指纹模块通信的串口,两个波特率不能搞混。TIMER1_Init和TIMER2_Init的第二个参数20000是定时器自动重装载值,配合预分频72得到大概200ms的中断周期,在录指纹超时判断和LED翻转里都会用到。
4.2 AS608底层API与错误码:握手、读模板数、读系统参数
初始化完成后,第一件事是让主控和AS608握手。资料里的实现是while(PS_HandShake(&AS608Addr))死循环,握手失败就一直打印“连接指纹模块失败”,这属于阻塞式等待,仅适用于上电自检。如果AS608没供电、接线接反、或者串口波特率不对,程序会卡在这个循环里。调试时建议在循环里加一个超时退出,比如等待5秒后仍然失败,就把错误原因打印出来并进入待机,而不是无休止重试。
// 握手成功后读取指纹库状态 while (PS_HandShake(&AS608Addr)) { printf("连接指纹模块失败....\r\n"); delay_ms(1000); } printf("指纹模块连接成功.....\r\n"); // 读库中有效模板数 ensure = PS_ValidTempleteNum(&ValidN); if (ensure != 0x00) { printf("ensure:%s\r\n", EnsureMessage(ensure)); } // 读模块系统参数 ensure = PS_ReadSysPara(&AS608Para); if (ensure == 0x00) { printf("RemainNum:%d Level:%d\r\n", AS608Para.PS_max - ValidN, AS608Para.PS_level); }PS_ValidTempleteNum返回的是模块内部指纹库的有效模板个数,存储在ValidN变量里。PS_ReadSysPara返回系统参数结构体,其中PS_max表示模块最大容量,PS_level表示当前安全等级。剩余容量用PS_max减去ValidN就能算出来,上位机界面如果显示“剩余可录枚数”,数据来源就在这里。
AS608的API返回值为0x00表示成功,非0值时需要使用EnsureMessage函数把错误码转成可读字符串。常见的错误码比如0x07表示图像不清晰,0x09表示两次采集的特征不一致,这些在联调阶段直接打印出来定位会非常快。
4.3 录指纹状态机:processnum的0到4
录指纹是整个固件里最复杂的部分,资料里用了一个基于processnum变量的switch状态机,分成五个阶段:取第一张图、取第二张图、两次对比、合成模板、存储模板。核心分支如下。
// 录指纹状态机,processnum从0递增到4 switch (processnum) { case 0: printf("请按下手指.\r\n"); ensure = PS_GetImage(); // 采集第一张指纹图像 if (ensure == 0x00) { ensure = PS_GenChar(CharBuffer1); // 生成特征存入CharBuffer1 if (ensure == 0x00) { processnum = 1; // 成功,跳到第二步 } else ShowErrMessage(ensure); } else ShowErrMessage(ensure); break; case 1: printf("再按一次手指.\r\n"); ensure = PS_GetImage(); // 采集第二张指纹图像 if (ensure == 0x00) { ensure = PS_GenChar(CharBuffer2); // 生成特征存入CharBuffer2 if (ensure == 0x00) { processnum = 2; // 成功,跳到对比步骤 } else ShowErrMessage(ensure); } else ShowErrMessage(ensure); break; case 2: ensure = PS_Match(); // 对比两个特征是否同一手指 if (ensure == 0x00) { printf("两次指纹是一样的.\r\n"); processnum = 3; } else { printf("对比失败,重新按手指.\r\n"); processnum = 0; // 对比失败,回到第一步重新采集 } delay_ms(1000); break; case 3: ensure = PS_RegModel(); // 由两个特征生成合成模板 if (ensure == 0x00) { processnum = 4; } else { processnum = 0; ShowErrMessage(ensure); } delay_ms(1000); break; case 4: ensure = PS_StoreChar(CharBuffer2, ID); // 把模板存储到指定ID if (ensure == 0x00) { printf("$update_lu_ru_ok,\r\n"); PS_ValidTempleteNum(&ValidN); // 刷新库中模板数量 return; } else { processnum = 0; ShowErrMessage(ensure); } break; }这个状态机有两个边界条件值得注意。第一个是i计数器,每次进入case 0或case 1都会自增,一旦i超过10,说明用户连续10次没有成功按下手指,程序直接退出并向上位机返回$update_lu_ru_error。第二个是两次按压的是不是同一根手指,只有PS_Match确认一致后才会继续往合成模板方向走,否则会退回最开始的状态。实测中经常遇到的“录到最后一步提示失败”,大多是因为第二次按压时手指位置和第一次偏差太大。
4.4 刷指纹与删除:HighSpeedSearch和DeletChar的参数
刷指纹的主流程在press_FR函数里。每轮循环调用PS_GetImage判断是否有手指按下,一旦取图成功,立即生成特征,然后调用PS_HighSpeedSearch在指纹库中搜索匹配项。
// 刷指纹:取图-生成特征-高速搜索 SearchResult seach; ensure = PS_GetImage(); // 采集图像 if (ensure == 0x00) { ensure = PS_GenChar(CharBuffer1); // 生成特征 if (ensure == 0x00) { ensure = PS_HighSpeedSearch(CharBuffer1, 0, 300, &seach); if (ensure == 0x00) { printf("$update_find,%d,\r\n", seach.pageID); printf("Match ID:%d score:%d\r\n", seach.pageID, seach.mathscore); } else { ShowErrMessage(ensure); } } } delay_ms(1000);PS_HighSpeedSearch的第二个参数0表示从0号页开始搜索,第三个参数300是搜索的页数范围。这里有个坑:如果AS608是50枚或100枚容量版本,300的搜索范围超出模块实际页数,不会报错但会影响耗时;反过来,如果换成300枚版本而搜索范围留的是100,ID大于100的指纹永远匹配不上。建议把这个搜索范围设置成模块实际容量值,并且配一个宏定义,方便不同容量模块切换。
删除指纹调用PS_DeletChar(num, 1),第一个参数是待删除的ID,第二个参数1表示删除该ID对应的模板。资料代码里约定了一个特殊逻辑:如果num大于等于0xFF00,则调用PS_Empty清空整个指纹库。
4.5 常见问题排查:五个具体踩坑记录
以下五条都是在实际复现这套考勤机时大概率会遇到的问题,按“现象—原因—解决”的顺序记录。
1. 上位机发“录入指纹”指令,下位机没任何反应。现象:串口助手发送$dev_cmd_lu_ru_zhi_wen:1后,单片机日志没有任何打印。 原因:指令末尾被串口助手自动添加了换行符或回车符,strstr仍然能匹配到关键字,但atoi在解析数字时遇到换行符,导致ID解析成0,代码里if(ID > 0)分支被跳过。 解决:下位机解析命令前,先对USART1_RX_BUFFER做一次截断,去掉\r和\n;或者Qt端发送时不加换行。我一般更喜欢后者,发送纯指令更干净。
2. AS608握手失败,程序卡死。现象:串口日志反复打印“连接指纹模块失败....”。 原因:模块VCC接到了5V而不是3.3V;或者TX和RX两根线接反。AS608对供电电压比较敏感,接5V可能导致通信异常。 解决:用万用表确认VCC为3.3V,PA2接模块RX白线、PA3接模块TX黄线,不要靠颜色记位置,必须对照接线表逐一核对。
3. 录入时总提示“超过10次没有按手指”。现象:按下手指后,程序没有进入case 0的流程,一直超时退出。 原因:Touch感应线没接或者接触不良,PS_GetImage内部依赖Touch脚判断是否有手指按压。另外,如果手指按下去时间太短,模块来不及完成图像采集。 解决:确认蓝线Touch感应线接到PA6,并且用手指完全覆盖采集面,保持1秒以上再松手。
4. 使用SWD调试时程序突然无法下载。现象:代码里放了JTAG_Set(JTAG_SWD_DISABLE)之后,第二次烧录失败,编译器报找不到设备。 原因:该语句把调试口也禁用了,还在调试阶段的工程会直接失去调试器连接。 解决:工程调试阶段不要过早执行JTAG_Set(JTAG_SWD_DISABLE),等到所有外设测试完,确认不再需要调试器时再启用;如果已经被禁用,按住板卡复位键,在下载工具开始擦除瞬间松手,通常能救回来。
5. 删除指纹后,上位机列表还显示旧ID。现象:设备端删除成功,但Qt考勤记录界面上ID仍然存在。 原因:上位机只在添加指纹成功后刷新了本地列表,没有处理删除成功后的刷新逻辑。 解决:删除指令发送后,等待设备返回确认,再重新读取本地下发同步指令,或者直接让上位机根据删除结果在本地表格中剔除对应ID。
5. Qt上位机端实现:串口收发、登录与考勤记录导出的编码细节
5.1 开发环境搭建:Qt 5.12.6安装里的两个坑
资料里明确指定了Qt 5.12.6版本,安装时有两个环节处理不当会很浪费时间。第一,安装时必须断网,否则安装向导会强制要求登录Qt账户,没有账户的机器根本无法继续。离线安装包本身已经带了所有需要的组件,断网装反而更顺畅。第二,组件选择界面不需要全选,只勾选一个MinGW 32-bit编译器即可。选MinGW而不是MSVC,是为了避免后续还要安装Visual Studio和Windows SDK,MinGW自带编译链,直接就能编译出exe可执行文件。
装完之后建工程时,编译器路径会自动关联到MinGW。需要注意的一点是,Qt 5.12.6的MinGW通常是32位版本,生成的exe在64位Windows上可以正常运行,但发布时要把Qt安装目录下的mingw32_53文件夹里的dll一起打包。
5.2 串口通信封装细节
在5.12.6环境里,串口通信依赖QtSerialPort模块。除了前面提到的.pro文件加QT += serialport,界面侧还要把波特率、数据位、停止位这些参数做成可配置项,而不是写死在代码里。设备配置界面里至少要有端口号下拉框和波特率输入框,默认值填57600。
串口打开之后有一个容易忽视的时序问题:设备刚上电时,AS608模块和串口的初始化需要时间,如果上位机立刻发送指令,很可能被丢弃。常见做法是在主界面加载完成后,先发送一个空行或者握手命令,等待设备返回“指纹模块连接成功”后再置位界面控件,给设备一个准备窗口。
// 设备配置界面:应用串口参数并打开 void DeviceConfigDialog::applySettings() { QString portName = ui->comPort->currentText(); int baud = ui->baudRate->currentText().toInt(); serial.setPortName(portName); serial.setBaudRate(baud); serial.setDataBits(QSerialPort::Data8); serial.setParity(QSerialPort::NoParity); serial.setStopBits(QSerialPort::OneStop); serial.setFlowControl(QSerialPort::NoFlowControl); if (serial.open(QIODevice::ReadWrite)) { ui->statusLabel->setText("串口已打开"); } else { ui->statusLabel->setText("串口打开失败"); } }串口配置类的QSerialPort对象一般设置为主窗口的成员变量,这样多个对话框之间可以共享同一个串口实例,避免重复打开导致设备被占用。
5.3 考勤记录与CSV导出的实现
打卡记录的数据结构,推荐用Qt的QList存储自定义结构体,每条记录包含指纹ID、打卡时间、打卡状态。每次收到$update_find,ID,,就构造一条新记录追加到列表里,同时刷新表格控件。
时间戳取的是上位机本地时间,这是个有意的设计选择。如果从下位机取时间,还得给单片机加RTC芯片,并且设备断电后时间容易丢失。上位机在电脑上运行,系统时钟本身就可靠,同时考勤记录和PC时间对齐,后续统计工时也更方便。
// 导出考勤记录到CSV,带BOM避免Excel中文乱码 QFile file(QCoreApplication::applicationDirPath() + "/attendance_" + QDate::currentDate().toString("yyyyMMdd") + ".csv"); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, "导出失败", file.errorString()); return; } QTextStream out(&file); out.setCodec("UTF-8"); out << "\xEF\xBB\xBF"; // UTF-8 BOM out << "指纹ID,打卡时间,状态\n"; for (const AttendanceRecord &rec : records) { out << rec.id << "," << rec.time.toString("yyyy-MM-dd hh:mm:ss") << "," << rec.status << "\n"; } file.close();BOM那一行不要省略。Excel打开没有BOM的UTF-8文件时,中文表头会直接乱码成“铟纹ID”这样的内容,加了BOM后Excel能正确识别UTF-8编码。文件名用当天日期生成,每天的考勤记录自动存一个文件,月末只需要把这些CSV直接归档或导入Excel汇总。
5.4 账号与日志:别把权限做在下位机
上位机登录界面、账号注册、账号管理这几个功能,本质上都是Qt的窗口和数据库表操作。账号表可以放在本地SQLite数据库里,Qt自带的驱动不需要额外安装服务端,一个文件就能解决。注册界面往用户表插入一条记录,登录界面按用户名和密码去校验,逻辑都很直接。
日志显示窗口在整个联调阶段价值很高。建议把串口收到的所有原始报文都追加到一个QTextEdit里,同时带上时间戳。这样做的好处是,设备端行为一目了然:哪条指令发了、设备回了什么、有没有粘包,全都能在日志窗口里对照。实测下来,很多通信问题不需要上逻辑分析仪,盯着日志图形容就能定位到是哪一端出了问题。
6. 联调四步与参数自查:先握手、再录入、再打卡的完整验证
整个系统跑通的验证路径可以压缩成四步,每一步都有明确的预期结果。按这个顺序走,出了问题也能立刻知道是哪一层卡的。
第一步:握手验证。打开上位机,选择正确的COM口,波特率设置57600,点击打开串口。预期日志窗口打印“指纹模块连接成功”,并且设备端LCD或串口日志显示“系统正常运行!”。如果这一步没过,优先检查AS608的VCC电压和TX/RX接线。
第二步:录入验证。在上位机设备配置界面选择录入指纹,输入ID号如1。设备端提示“请按下手指”,先按一次,再按一次,两次都成功后等待约2秒,观察是否收到$update_lu_ru_ok。如果报错,查看设备端打印的错误码,0x07是图像质量差,0x09是两次指纹不一致。
第三步:删除与清空验证。发送删除指令,删除刚才录入的ID,确认设备端打印“删除指纹成功”。然后拉到协议里约定的0xFF00清空指令执行一次,确认指纹库归零。这一步在生产环境不要随便做,建议只在初始调试阶段执行。
第四步:刷指纹打卡验证。手按已录入的手指,上位机考勤记录表格里应立即新增一条打卡记录。再按一个未录入的手指,确认不会产生记录。导出CSV,打开后确认中文表头正常、打卡时间准确。
这四步之外还有一张参数自查表,可以在联调过程中随时对照:
| 检查项 | 期望值 | 说明 |
|---|---|---|
| USART1波特率 | 115200 | 调试串口 |
| USART2波特率 | 57600 | AS608通信串口 |
| AS608 VCC | 3.3V | 接5V会通信异常 |
| PA2/PA3接线 | PA2→模块RX,PA3→模块TX | 交叉连接 |
| AS608搜索范围 | 按模块容量配置 | 50/100/300对应不同值 |
| Qt侧串口参数 | 8/无校验/1停止位 | 与下位机保持一致 |
最后说一个我踩过不止一次的教训:资料里的Del_FR函数有一段测试残留,函数体内把num直接写死成了1,导致任何删除请求都只删1号指纹。这类“写死在测试代码里”的值,当时联调多花了一个多小时才定位到。从那以后我每次做完一轮联调,都会强制全工程搜一遍写死的数字参数,凡是带注释TODO的测试值统一清理,再走一遍完整验证流程。这个习惯帮我免掉了很多翻车现场,也希望帮到你。
本文还有配套的精品资源,点击获取