简介:面向单片机与嵌入式初学者的AS608指纹模块双方案源代码工程,基于STC89C52与Keil4开发,实现指纹录入、存储、比对及指纹锁控制逻辑,适合直接用于门禁、考勤或智能锁项目,也可作为课程设计素材。压缩包共108个文件,约241KB,核心内容为C源码与对应头文件,另有uvproj/uvopt工程文件、编译生成的obj/lst/hex中间文件以及多个备份文件,工程结构完整,便于复现、学习与二次修改。目前已有1076人学习使用。方案1和方案2在指纹数据管理或匹配策略上存在差异,源码包含AS608与MCU的UART通信流程、指纹模板处理函数以及1602液晶显示逻辑,读者可通过对比两个工程理解不同实现思路,也可借助hex文件直接烧录验证;对学习51单片机外设驱动和指纹识别原理具有直接参考价值。整体代码注释清晰,适合动手实践。
1. 指纹锁的门锁控制不难,难的是把 AS608 的指令时序跑对
指纹锁的门锁控制其实只有两个动作:按下手指、识别对了就开锁。真正卡人的不是开锁逻辑,而是 AS608 指纹模块和单片机之间的指令往返。AS608 是一颗光学指纹传感器加识别算法的模组,单片机通过 UART 发指令让模块采集指纹、生成特征、建模板、搜索比对,整个过程像问答一样一问一答。网上流传的2.2 AS608指纹模块 方案1和2的源代码(keil4)这类工程包里,方案1 和方案2 最常见的分组是“查询式直调”和“串口中断状态机式解析”,两条路都能驱动 AS608,但代码结构和适用场景差得很远。这篇按这个分组讲清楚两套写法各自的原理、代码骨架和 Keil4 里容易踩的坑,适合正在做指纹锁、考勤机、储物柜项目的单片机开发者,也适合刚把 Keil4 装好、手头有一块 AS608 模块的入门者照着搭一遍。
2. AS608 指纹识别原理与 Keil4 工程准备
2.1 指纹图像、特征码和模板:识别算法在模块里,不在单片机里
很多初学者第一次拿到 AS608 会把它当成一个图像传感器,想把指纹图像读回来再自己写算法比对。这个方向一开始就错了。AS608 模块内部集成了光学传感器、DSP 和指纹算法固件,单片机只需要做三件事:发指令、收应答、根据应答码决定下一步动作。
手指按上去后,模块内部完成图像的采集、增强、特征点提取,生成一个特征文件(通常几百个字节)。模板则是多次录入特征后合并成的登记数据,存放在模块的 Flash 里,每个模板占一个 PageID。比对时先采集当前手指的特征,再拿这个特征去指定 PageID 范围内搜索匹配。整个流程从模块角度看是固定的,单片机端不要试图插手指纹算法,真正要下功夫的是数据帧的组包、应答超时和状态转移。
模块上电后默认地址是0xFFFFFFFF,波特率常见为 9600bps。每次通讯都是单片机先发命令帧,模块处理后返回应答帧。应答帧不一定只包含确认码,比如读系统参数的命令还会带一长串参数数据,所以接收端必须按帧长度字段动态收完一帧,而不能只死等固定字节数。
2.2 AS608 数据帧格式与 8 个常用指令
AS608 的数据帧格式非常死板,发出去的每一帧都按下面的顺序排列:包头(2 字节0xEF 0x01)、模块地址(4 字节)、包标识 PID(1 字节,命令包为0x01)、包长度(2 字节,高字节在前,表示指令码加参数的总字节数)、指令码、参数数据、校验和(1 字节,取包标识、包长度、指令码、参数数据之和的低 8 位)。
这个帧格式决定了写代码的方式:组包时逐字节塞进发送缓冲区,收包时即使只解析确认码,也要把长度字段读出来才能判断这帧有没有收完。指纹锁项目里用不到的指令很多,实际开发中先掌握下面 8 个就够用了。
| 指令名称 | 指令码 | 参数格式 | 包长度 | 作用 |
|---|---|---|---|---|
| GenImg | 0x01 | 无 | 0x03 | 采集手指图像并存入 ImageBuffer |
| Img2Tz | 0x02 | BufferID(2 字节) | 0x04 | 从图像缓冲区生成特征文件 |
| RegModel | 0x05 | 无 | 0x03 | 合并两次特征生成模板 |
| Store | 0x06 | PageID(2 字节) | 0x04 | 把模板存到 Flash 指定页 |
| Search | 0x04 | BufferID、起始页、个数各 2 字节 | 0x07 | 在指定范围内搜索匹配 |
| DeletChar | 0x0C | PageID、个数各 2 字节 | 0x05 | 删除指定范围模板 |
| Empty | 0x0D | 无 | 0x03 | 清空整个指纹库 |
| ReadSysPara | 0x0F | 无 | 0x03 | 读取模块容量、版本等参数 |
注意Store和Search的参数都是两字节大端序,比如 PageID 为 1 时,参数区是0x01 0x00而不是0x00 0x01。校验和计算时别忘了把 PID 和包长度也算进去,漏掉包长度是抄网上代码时最容易翻车的地方。
2.3 Keil4 下建工程和硬件接线的 3 个注意点
AS608 模块的 VCC 允许范围较宽,但串口逻辑电平是 3.3V。如果用的是 5V 供电的 STC89C52、STC12 系列单片机,模块 TXD 输出的 3.3V 高电平接单片机 RXD 通常能正常识别,模块 RXD 却可能无法承受单片机 TXD 的 5V 高电平。规范做法是在单片机 TXD 到模块 RXD 之间串一个 1kΩ 电阻分压,或者用两个电阻做 3.3V 分压,别指望靠模块内部保护电路硬扛。
Keil4 本身分 C51 版和 MDK 版,AS608 工程的芯片是 8051 核,必须用 C51 编译器。电脑上如果先装了 MDK,打开 51 工程时经常出现 keil4 mdk unknown product 的提示,本质是 IDE 没找到对应器件的支持包,不是工程文件坏了。处理办法是重新运行 Keil4 C51 安装包,选择补装 C51 编译器,再在 Project -> Select Device 里把芯片选成实际的 STC 型号。STC 单片机在 Keil 器件库里没有的话,用 STC-ISP 软件里的“添加型号和头文件到 Keil”功能补齐。
工程里文件编码也值得提前处理。Keil4 默认按 ANSI 解析源码,很多从网上下载的源代码注释是 GB2312 编码,直接在 Windows 记事本里另存为 ANSI 再用 Keil4 打开,否则中文注释会乱码。更隐蔽的是预处理指令里的中文字符串,编码不对会导致编译警告甚至数据错位。
3. 方案1:查询式调用 AS608 指纹模块源代码
3.1 串口初始化和通用帧发送函数
查询式的核心是“发一条指令,阻塞等一条应答”。代码结构简单直观,适合原型验证和逻辑排查。先用 11.0592MHz 晶振把串口初始化成 9600bps 模式 1:
void UART1_Init(void) { SCON = 0x50; // 0101_0000B: 方式1, REN=1 允许接收 TMOD = 0x20; // 定时器1工作在方式2, 8位自动重装 TH1 = 0xFD; // 9600bps @ 11.0592MHz TL1 = 0xFD; PCON = 0x00; // SMOD=0, 波特率不加倍 TR1 = 1; // 启动定时器1作为波特率发生器 }SCON = 0x50同时设置了串口工作方式和接收使能,0x20写入TMOD时要把原来用于定时器 0 的低四位一并考虑,所以正式工程里建议先读再改位。TH1和TL1都装0xFD是因为方式 2 下重装值由 TH1 自动加载到 TL1。
然后写一个通用的发帧函数,把所有指令都按“包头 + 地址 + PID + 长度 + 指令 + 参数 + 校验和”组织:
void AS608_SendFrame(unsigned char cmd, unsigned char *params, unsigned char len) { unsigned char i; unsigned int sum = 0; UART1_SendByte(0xEF); // 包头低字节 UART1_SendByte(0x01); // 包头高字节 UART1_SendByte(0xFF); // 模块地址, 默认全FF UART1_SendByte(0xFF); UART1_SendByte(0xFF); UART1_SendByte(0xFF); UART1_SendByte(0x01); // PID命令包 sum += 0x01; UART1_SendByte(((1 + len) >> 8) & 0xFF); // 包长度高字节 sum += ((1 + len) >> 8) & 0xFF; UART1_SendByte((1 + len) & 0xFF); // 包长度低字节 sum += (1 + len) & 0xFF; UART1_SendByte(cmd); // 指令码 sum += cmd; for (i = 0; i < len; i++) { UART1_SendByte(params[i]); sum += params[i]; } UART1_SendByte(sum & 0xFF); // 校验和取低8位 }len指的仅是参数区字节数,包长度字段是1 + len,因为指令码本身要占一个字节。以GenImg为例,cmd = 0x01、len = 0,包长度就是0x0003。如果这里误写成len + 2,模块会一直不回包,这是最常见的通讯失败原因。
3.2 录入一枚指纹的操作序列
指纹录入需要连续按两次手指。模块要求在第一次生成特征存入 Buffer1 后,第二次再生成特征存入 Buffer2,然后合并成模板。查询式代码里这一段是顺序执行的:
unsigned char param[2]; // 第一次按压 AS608_SendFrame(0x01, NULL, 0); // GenImg if (AS608_WaitAck() != 0x00) return 1; param[0] = 0x01; param[1] = 0x00; // BufferID=1 AS608_SendFrame(0x02, param, 2); // Img2Tz 1 if (AS608_WaitAck() != 0x00) return 2; // 提示松开手指, 再次按压 AS608_SendFrame(0x01, NULL, 0); if (AS608_WaitAck() != 0x00) return 3; param[0] = 0x02; param[1] = 0x00; // BufferID=2 AS608_SendFrame(0x02, param, 2); if (AS608_WaitAck() != 0x00) return 4; AS608_SendFrame(0x05, NULL, 0); // RegModel 合并 if (AS608_WaitAck() != 0x00) return 5; param[0] = 0x00; param[1] = 0x01; // PageID=1 AS608_SendFrame(0x06, param, 2); // Store if (AS608_WaitAck() != 0x00) return 6;AS608_WaitAck在查询式方案里就是阻塞读串口,一直等到收到一帧完整应答才返回确认码。要注意第二次按压前必须加入“请松开手指”的提示延时,否则连续按压太快模块可能采到的还是上一张图。返回码非 0 的退出方式在测试时很有用,能直接通过数字定位到哪一步失败,正式产品里再改成状态机或错误提示。
3.3 比对时用 Search 而不是 Match
比对指纹时最直接的思路是 Match(指令码 0x03),它只比较 Buffer1 和 Buffer2 里的两个特征,适合验证两次采集是否同一根手指。真实指纹锁要做的是拿当前手指特征去指纹库里找,所以必须用 Search:
param[0] = 0x01; param[1] = 0x00; // BufferID=1 param[2] = 0x00; param[3] = 0x00; // 从第0页开始 param[4] = 0x03; param[5] = 0xE7; // 搜索999个模板, 具体容量以模块读出为准 AS608_SendFrame(0x04, param, 6);Search 的应答包比普通确认包多了 4 字节:确认码、匹配到的 PageID 高字节、低字节、匹配分数。查询式代码在确认码为 0 后,还要从接收缓冲区里把 PageID 取出来,开锁逻辑要根据这个 PageID 判断是哪个用户按的指纹。常见返回码含义列出来方便查错:
| 返回码 | 含义 | 常见原因 |
|---|---|---|
| 0x00 | 指令正确执行 | 无 |
| 0x01 | 数据包接收错误 | 组包丢字节、校验和不匹配 |
| 0x02 | 传感器上无手指 | 按压太轻或没有按压动作 |
| 0x03 | 录入指纹失败 | 两次按压特征差异过大 |
| 0x04 | 生成特征失败 | 手指太干或图像质量差 |
| 0x09 | 无有效图像 | 采集时手指移动过快 |
| 0x0A | 合并特征失败 | 两次特征文件不匹配 |
查询式的优点是把每个步骤摊平在代码里,用示波器或者串口打印能快速定位模块在哪个环节返回错误。缺点也明显:AS608_WaitAck死等期间单片机干不了别的事,如果这是指纹锁,按键扫描、舵机控制、液晶刷新全部会被拖住。
4. 方案2:用串口中断和状态机解析 AS608 应答
4.1 查询式在指纹锁上的两个问题
查询式代码跑通原型很容易,放进真实指纹锁就有两个问题。第一,WaitAck的阻塞时间取决于模块处理速度,实测中指纹质量差时 GenImg 要一两秒,等待期间按键完全失灵。第二,模块应答帧长度不固定,查询式通常只读确认码就完事,遇到 ReadSysPara 这类带长参数返回的命令,缓冲区残留数据会污染下一帧。
方案2 改成串口中断加状态机,接收完全由中断驱动,主循环只检查全局变量。单片机在等模块回包的同时还能扫描按键、刷新 OLED、驱动舵机,这才是产品级指纹锁的结构。AS608 的数据帧有明确包头0xEF 0x01,非常适合用状态机逐字节解析。
4.2 串口中断状态机接收一帧 AS608 数据
状态机的思路是:每收到一个字节都检查当前状态,当前位置不同,这个字节的含义就不同。核心状态划分为等包头、收地址、收 PID、收长度、收数据区五个阶段:
volatile unsigned char rxState = 0; volatile unsigned char rxAddrCnt = 0; volatile unsigned char rxDataLen = 0; volatile unsigned char rxCnt = 0; volatile unsigned char rxBuf[20]; volatile unsigned char as608Ack = 0xFF; void UART1_ISR(void) interrupt 4 { unsigned char dat; if (!RI) return; RI = 0; dat = SBUF; switch (rxState) { case 0: // 等待0xEF if (dat == 0xEF) rxState = 1; break; case 1: // 等待0x01 if (dat == 0x01) { rxState = 2; rxAddrCnt = 0; } else { rxState = 0; } break; case 2: // 跳过4字节地址 if (++rxAddrCnt >= 4) rxState = 3; break; case 3: // PID字节, 暂存但不处理 rxState = 4; break; case 4: // 包长度高字节 rxDataLen = dat << 8; rxState = 5; break; case 5: // 包长度低字节 rxDataLen |= dat; if (rxDataLen == 0) { rxState = 0; // 长度异常, 重新同步 } else { rxCnt = 0; rxState = 6; } break; case 6: // 数据区: 指令码、确认码、参数 rxBuf[rxCnt++] = dat; if (rxCnt >= rxDataLen) { as608Ack = rxBuf[0]; // 应答帧首字节就是确认码 rxState = 0; // 一帧收完, 回到同步状态 } break; } }中断函数里没有计算校验和,只做了帧结构解析,这是为了压低中断服务程序耗时。更严谨的做法是在rxState = 6收完时把所有字节累加验证,不匹配就丢弃这一帧并把状态归零,防止总线上的干扰数据被当成应答。as608Ack和rxBuf都加了volatile,因为主循环要在非中断上下文读这两个变量。
4.3 把 Ack 交回主循环
主循环里发完指令后不等待,清掉as608Ack后继续做其他事,下次循环再检查as608Ack是否被中断改写。这个模式把原来的阻塞等待变成了轮询:
as608Ack = 0xFF; AS608_SendFrame(0x01, NULL, 0); // 发起GenImg while (1) { KeyScan(); // 扫描按键 OLED_Refresh(); // 刷新液晶 if (as608Ack != 0xFF) // 模块已应答 { if (as608Ack == 0x00) { // 采集成功, 继续下一步Imag2Tz } else { // 采集失败, 根据as608Ack提示用户 } as608Ack = 0xFF; // 消费掉本次应答 } }这种结构在等待指纹的同时把按键和显示都带起来了。注意as608Ack == 0xFF不能单独作为“未应答”的判断条件,因为模块完全可能返回0xFF?不会,AS608 的有效确认码最高到 0x0F 左右,0xFF只用作本地标志是安全的。也可以定义成独立的ackFlag,更直观一点。
4.4 方案1和方案2适用场景对比
| 对比项 | 方案1 查询式 | 方案2 中断状态机 |
|---|---|---|
| 代码结构 | 顺序执行,直来直去 | 状态分散,需要全局变量 |
| 单片机等待时的行为 | 完全阻塞 | 可并行处理按键和显示 |
| 调试难度 | 简单,步骤线性 | 较难,需要理解状态迁移 |
| 多指令组合流程 | 好写 | 需要维护状态机或队列 |
| 适合场景 | 原型验证、串口调试助手复现指令 | 指纹锁、考勤机等完整产品 |
工程里最常见的做法是先用方案1 在 Keil4 里把模块的每一条指令跑通,确认模块本身是好的;再改造成方案2,把确认码的判断挪到中断里。两块代码共用同一个AS608_SendFrame,所以发送层不需要重写,只是接收层的组织方式变了。
5. Keil4 下验证 AS608 模块的好用技巧
5.1 用 ReadSysPara 核对模块参数
AS608 的容量和固件版本因批次而异,网上代码里写的搜索范围不一定适合你的模块。上电后先发 ReadSysPara(指令0x0F),应答帧会带回一串参数,包括模块容量、指纹库已存数量、波特率、固件版本。用方案2 的中断接收把整帧数据缓存下来,在 Keil4 的 Watch 窗口里直接看rxBuf,比反复试错猜容量可靠得多。搜索范围参数就填rxBuf里读出的容量值,而不是照抄别人的 999。
5.2 清空指纹库和现场排错组合
开发过程中指纹库会被测试数据塞满,手动一条条删效率太低。发Empty清库指令前,先确认模块里有几个模板,读系统参数里的已存指纹数量字段,回来后再读一次比对是否清零,这个步骤能验证写操作是否真正落进 Flash。调试时如果遇到 Keil4 提示“当前不会命中断点”,先确认在 Options for Target -> Debug 里选了正确的仿真方式,再把断点打在AS608_SendFrame发送完校验和之后那一行,比打在中断 ISR 里更容易命中。用逻辑分析仪夹在模块 TXD 引脚上,抓不到波形就查模块供电,能抓到波形就查单片机 RXD 引脚,这个二分法能快速定位是哪一侧通讯断了。把这几个检查点合并成一段自检函数,在初始化后主动跑一遍,现场升级固件前先点一次,能省掉大半开箱即坏的口水。
本文还有配套的精品资源,点击获取