• 正文
  • 相关推荐
申请入驻 产业图谱

STM32驱动HLK-ZW101指纹识别模块完整教程(手把手教学从0开发)

09/07 13:55
743
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是DS小龙哥。

早上出门,你按一下指纹,门锁"咔哒"一声开了;到了公司,手指往打卡机上一放,考勤完成;打开保险柜,也是轻轻一按——指纹识别,早就不是什么黑科技,它就在我们每天的生活里。

很多同学跟我说:"龙哥,指纹模块听起来很高大上,是不是很难驱动?"其实真不难。指纹模块本身就是一个"黑盒",里面的采集、算法、存储都由模块自己完成,我们STM32要做的,就是通过串口给它发指令、收应答,跟我们平时驱动一个串口传感器没有本质区别。

今天这篇文章,我就带大家从零开始,用 STM32F103C8T6 驱动 海凌科 HLK-ZW101 半导体指纹识别模块,实现:指纹录入、指纹删除、指纹搜索开锁,外加 OLED 菜单显示、按键操作、蜂鸣器提示,做成一个完整可用的"指纹门禁锁"。

这篇文章也是一篇移植教程:很多同学手里有 AS608/AS606 光学指纹模块的旧代码,想换到 ZW101 上。ZW101 的通信协议和 AS608 是兼容的(都是 0xEF01 帧头那一套),但有几个坑不一样。我会把移植过程中踩到的两个"大坑"(手指检测不稳、搜索一直失败)作为本文精华,详细讲出来,保证大家看完能一次复刻成功。

老规矩,面向0基础,硬件连线全部表格化,代码注释写到每行,跟着做就行。


一、先看效果:我们最终要做成什么样

先给大家看成品,心里有个目标。整个系统由一块 STM32F103C8T6 最小系统板 + HLK-ZW101 指纹模块 + 0.96寸OLED屏 + 4个按键 + 蜂鸣器 + 指示灯组成。

上电后,系统会自动和指纹模块握手,握手成功 OLED 显示 ZW101 Init Success...,然后读取指纹库里已存指纹的个数和模块参数,通过串口打印到电脑。

图1 指纹门禁锁成品实物图

系统有两大功能模式:

1、待机识别模式(刷指纹开锁)
平时屏幕显示待机界面,系统每隔约500ms自动采一次图检测手指。你把已录入的手指按上去,模块自动完成"采图→生成特征→搜索指纹库",搜到了就蜂鸣器提示、OLED显示"开锁成功",同时可以驱动继电器/电磁锁开门;搜不到就提示失败。

2、管理员模式(录指纹/删指纹)
按一下 K1 键进入管理员验证,输入管理员密码(工程里默认是 1212),密码正确后进入管理菜单:

• 指纹录入:用 K3/K4 选择要存入的指纹ID(0~49),按 K2 确认,然后按屏幕提示"请按下指纹→请抬起手指→再按一次",两次采集合并成功后指纹就存进模块了;

• 指纹删除:同样选ID,按 K2 删除指定指纹,也支持一键清空整个指纹库。

图2 OLED菜单界面实拍

整个过程串口1(115200)会同步打印详细日志,方便调试:握手状态、每一步的确认码、匹配到的ID和分数、剩余指纹个数,全都能在串口助手里看到。

图3 串口调试日志截图


二、认识主角:HLK-ZW101指纹模块介绍

2.1 它是个什么东西

HLK-ZW101 是深圳海凌科电子(Hi-Link)推出的一款一体化半导体指纹处理模组。注意两个关键词:•

一体化:它把"主动式半导体指纹采集Sensor"和"指纹识别处理芯片"做在了一起,采集、算法、存储全在模块内部完成,对外只留一个串口;•

半导体(电容式):和老款 AS608 那种"光学拍照式"不同,ZW101 用的是电容式半导体传感器,靠手指脊线/谷线与传感器之间的电容差异来成像。它不需要光源、黑暗中也能工作、防伪能力更强、功耗更低,是现在智能门锁的主流方案。

模块内部用的是一颗高性能低功耗 RISC-V 内核算法芯片,跑360度自适应自学习算法——说白了就是你每次按指纹,它都会悄悄学习、越用越准。

2.2 主要特性参数

我把官方规格书和手册里的关键参数整理成表,大家心里有数:

参数项 指标
传感器类型 主动式半导体(电容式/射频采集),面阵式指纹传感器,表面有保护涂层
主控内核 高性能低功耗 RISC-V 内核,360°自适应自学习算法
通信接口 UART(TTL 3.3V电平),默认波特率 57600,8数据位/1停止位/无校验
波特率范围 可用指令设置,9600 ~ 115200(默认系数6,即 6×9600=57600)
供电电压 3.3V(模组支持3.3V供电,务必稳定)
指纹容量 默认版本存储 50枚 指纹特征(ZW111等大容量版本为100枚)
认假率 FAR < 1/1,000,000(别人的手指误开你的锁,概率百万分之一以下)
拒真率 FRR < 3%(自己的手指偶尔识别不出的概率,多按几次即可)
特征提取时间 < 0.06 秒
单枚匹配时间 < 0.8 秒(实际体验从按压到出结果基本1秒内)
待机/休眠功耗 支持低功耗手指检测,休眠电流低至 8μA 级别
ESD防护 > 15KV(防静电,冬天摸它不容易坏)
指示灯 RGB 三色LED灯圈,可自定义颜色和闪烁方式
封装尺寸 芯片封装 Ø11.6mm×0.6mm,模组封装 Ø21mm×5.0mm,体积很小
上电信号 初始化成功后模组会主动通过串口发送1字节 0x55 作为握手信号

安全等级(分数等级)出厂默认为 Level2,共分 Level1~Level5 五级,等级越高要求越严格(越安全但越难识别),可以通过指令读写。

2.3 引脚定义

ZW101 模组用的是 1.0mm 连接器,引出的线在实际模块上一般带颜色,和我们工程里的接线一一对应。结合工程实际接线,引脚定义如下:

模块引脚/线色 名称 功能说明
红线 VCC 模块主电源,接 3.3V
绿线 VCC(触摸感应电源) 触摸检测部分供电,也接 3.3V(本工程直接并到3.3V)
黑线 GND 地,接 GND(必须和STM32共地)
白线 RX(UART_RD) 模块串口接收,接 STM32 的 TX(PB10)
黄线 TX(UART_TD) 模块串口发送,接 STM32 的 RX(PB11)
蓝线 TOUCH 触摸感应输出 手指触摸时输出高电平,接 PB9(本工程可不依赖它,详见后面讲解)

⚠️ 串口接线记住"交叉接":STM32的TX接模块的RX,STM32的RX接模块的TX,千万别TX接TX,那样是收不到数据的。


三、ZW101 和 AS608/AS606 到底啥关系?能不能直接移植?

很多同学入门指纹识别,最早接触的是 AS608(也叫FPM10A) 这种光学指纹模块,网上资料、Arduino库一大堆。那 ZW101 和它是什么关系?代码能不能直接用?

结论先说:通信协议高度兼容,但有4个差异点要注意。

3.1 相同点:协议帧格式完全一样

ZW101 和 AS608 用的是同一套通信协议(业内常叫"EF01协议"):

• 帧头都是 0xEF01;• 设备地址默认都是 0xFFFFFFFF

• 都是"命令包(标识01)/应答包(标识07)/结束包(标识08)"这套结构;

• 校验和算法完全一样(包标识开始所有字节累加);

• 核心业务指令码完全一样:采图0x01、生成特征0x02、比对0x03、搜索0x04、合并建模0x05、存储0x06、删除0x0C、清空0x0D、读参数0x0F、读指纹数0x1D。

**所以AS608的驱动代码,绝大部分可以直接搬过来用。**这也是为什么这套协议生态这么好——GitHub上有开源的 HLK-ZW 指纹库,明确写着"同时支持 AS608、R307 以及所有 EF-01 协议兼容的指纹模块"。

3.2 不同点:这4个地方移植时要改

我在 hlk_zw101.h 头文件里,专门把 ZW101 和 AS608 的区别注释出来了,一共4条:

/* 与 AS608 的主要区别:
 *   1. 握手指令码为 0x35(AS608 无标准握手指令)
 *   2. 读系统参数应答的字段排列顺序不同
 *   3. 不支持 0x1B 高速搜索, 统一使用 0x04 搜索指令
 *   4. 上电初始化成功后模组会主动发送 1 字节 0x55 握手信号
 */

逐条解释:

1. 握手指令码是 0x35。ZW101 有专门的握手指令 PS_HandShake,指令码 0x35(手册3.4.4节可查),发过去模块回确认码0x00就说明在线、正常工作。AS608没有这个标准握手指令(有些AS608例程用0x17或干脆不握手)。

2. 读系统参数(0x0F)应答字段排列顺序不同。AS608老例程里字段顺序和ZW101不一样,如果你直接套用AS608的解析下标,读出来的容量、等级会是乱的。ZW101的字段排列我在驱动里重新标注了(后面代码会讲)。

3. 不支持 0x1B 高速搜索。AS608有个高速搜索指令0x1B,ZW101不支持,统一用 0x04 普通搜索指令搜整个指纹库即可,速度也很快(<0.8s)。

4. 上电主动发 0x55。ZW101上电初始化成功后会主动发一个字节0x55,主机可以用它判断模块启动完成;如果没收到,手册建议M系列延时80ms、S系列延时50ms再操作。

3.3 为什么我选 ZW101

半导体电容式,比光学的更安全(假指纹难骗)、更省电(适合电池供电的门锁)、体积更小;•

价格便宜,十几块钱一个,性价比极高;•

自带RGB灯圈,交互体验好;•

协议通用,AS608的代码改改就能用,学习资料能直接复用。


四、硬件连接:一张接线表搞定

硬件部分我全部整理成表格,0基础同学照着接就行。整个工程用到的外设和引脚如下(全部以工程 main.c 里的实际接线为准)。

4.1 总接线表

外设 模块/外设引脚 STM32F103引脚 说明
ZW101指纹模块 白线 RX PB10(USART3_TX) STM32发→模块收
黄线 TX PB11(USART3_RX) 模块发→STM32收
红线 VCC 3.3V(独立供电) 模块主电源
绿线 VCC(触摸电源) 3.3V 触摸检测供电,和红线并一起接3.3V
黑线 GND GND 必须共地
蓝线 TOUCH(触摸输出) PB9 触摸高电平有效;本工程待机不靠它,可接可不接
0.96寸OLED(IIC软件模拟) VCC 3.3V 供电
GND GND
SCL PB0 IIC时钟
SDA PB1 IIC数据
4颗按键 K1 PB12 翻页/返回(管理员入口)
K2 PB13 确定
K3 PB14 数字1 / ID++
K4 PB15 数字2 / ID--
公共端 GND 按键另一端接地
蜂鸣器 OUT PA15 提示音(有源蜂鸣器,高电平响)
VCC 3.3V
GND GND
指示灯 LED1 PB3 系统运行状态灯
LED2 PB4 系统运行状态灯
LED3 PC13 板载灯
调试串口 USB转TTL USART1(PA9/PA10) 115200,打印调试日志到电脑

注意:PA15、PB3、PB4 默认是 JTAG 调试脚。工程里调用了 JTAG_Set(SWD_ENABLE) 关闭JTAG、只保留SWD下载,这样就能把这几个脚当普通IO用了。下载器用 ST-Link 的 SWD 模式(只接 SWCLK、SWDIO、GND、3.3V)即可,不要用JTAG模式。

4.2 ⚠️ 头号硬件注意事项:必须独立、稳定的3.3V供电

这是无数人栽跟头的地方,我提前重点讲:

ZW101 必须用 3.3V 供电,而且供电电流要够,强烈建议独立供电,不要图省事直接插在STM32最小系统板的3.3V引脚上。

原因:指纹模块在采集图像、点亮RGB灯、跑匹配算法的瞬间,电流会突然增大。STM32板子上那个3.3V(一般是AMS1117-3.3或USB转出来的)带载能力有限,一旦供电跟不上,模块电压被拉低,就会出现各种"玄学问题":

    • 明明手指按好了,却采集失败、反复提示无手指;• 录到一半模块重启、通信中断、串口乱码;• 搜索时好时坏,匹配分数异常低。

推荐做法:用一路独立的3.3V电源给指纹模块供电(比如外接一个3.3V稳压模块、或者用带独立LDO的电源),GND和STM32共地即可。万用表量一下模块VCC引脚,稳定在 3.3V ± 0.1V 才算合格。海凌科官方技术支持也明确建议"使用外部稳压电源或带LDO的开发板供电,避免供电能力不足"。

另外两个硬件小细节:

1. **新模块镜头上有一层保护膜,一定要撕掉!**不撕掉会严重影响成像,导致特征点太少、生不成特征(返回0x07)。

2. 手指要按在采集窗正中间、按实,别只搭个边;手指太干可以哈口气或搓一搓。

图5 硬件接线示意图


五、通信协议详解:帧格式 + 校验和手把手算

驱动指纹模块,本质就是"按格式组帧发出去→等模块应答→解析应答里的确认码"。所以协议一定要看懂。别担心,这套协议很规整。

5.1 数据包的三种类型

模块和主机之间收发的每一包数据,都必须加包头 0xEF01。按"包标识"字节区分类型:

包标识 类型 谁发给谁
0x01 命令包 主机(STM32)→ 模块,用来下指令
0x02 数据包(有后续包) 双向,传输大块数据(如图像)时用
0x07 应答包 模块 → 主机,回应指令执行结果(我们最关心)
0x08 结束包(最后一个数据包) 双向,数据包的最后一包

我们做录指纹、搜指纹这些功能,收发的主要是 0x01命令包 和 0x07应答包。0x02/0x08数据包主要在上传指纹图像、模板时才用,本文工程用不到,了解即可。

5.2 命令包帧格式(我们要发送的)

名称 字节数 内容 说明
包头 2 0xEF 0x01 固定帧头,高字节在前
设备地址 4 FF FF FF FF 默认地址0xFFFFFFFF,高字节在前
包标识 1 0x01 命令包
包长度 2 如 0x00 0x03 长度=从"包长度"后一字节到校验和的总字节数(含指令、参数、校验和),高字节在前
指令码 1 如 0x01 具体指令(采图/搜索等)
参数 N —— 有些指令带参数(如缓冲区号、ID号)
校验和 2 如 0x00 0x05 从"包标识"开始所有字节累加,取低16位,高字节在前

5.3 应答包帧格式(模块回给我们的)

名称 字节数 内容 说明
包头 2 0xEF 0x01
设备地址 4 FF FF FF FF
包标识 1 0x07 应答包
包长度 2 如 0x00 0x03
确认码 1 data[9] 0x00=成功,其它=各种错误,这是我们判断成败的关键
返回参数 N —— 如搜索应答里紧跟"页码(2字节)+得分(2字节)"
校验和 2 sum

注意应答包里确认码的位置:包头2 + 地址4 + 标识1 + 长度2 = 9个字节,所以确认码正好是 data[9](从0开始数)。这个下标在代码里反复用到,记住它。

5.4 校验和怎么算?手把手带你算一遍

校验和规则(手册原文):从"包标识"字节开始,到校验和之前的所有字节,全部累加,超出2字节(65535)的进位直接忽略,取低16位。

也就是:校验和 = 包标识 + 包长度高 + 包长度低 + 指令码 + 所有参数

我们拿最简单的"采集图像 PS_GetImage(指令码0x01)"来算。它的完整命令包是:

EF 01  FF FF FF FF  01  00 03  01  00 05
    • 包标识 = 0x01• 包长度 = 0x0003,即高字节0x00、低字节0x03• 指令码 = 0x01• 无参数

累加:0x01 + 0x00 + 0x03 + 0x01 = 0x05,所以校验和 = 0x00 0x05。✅ 和手册给的算例完全一致。

再算一个"握手 PS_HandShake(指令码0x35)",命令包:

EF 01  FF FF FF FF  01  00 03  35  00 39

累加:0x01 + 0x00 + 0x03 + 0x35 = 0x39,校验和 = 0x00 0x39。✅ 同样和手册一致。

有参数的也一样,比如"读系统参数(0x0F)":0x01+0x00+0x03+0x0F = 0x13 → 校验和 0x0013;"读有效模板个数(0x1D)":0x01+0x00+0x03+0x1D = 0x21 → 0x0021。大家找到规律了吧?就是把包标识之后、校验和之前的字节,一股脑加起来。

5.5 确认码(返回码)速查表

模块应答里的确认码,是我们调试时的"诊断书"。工程里的 EnsureMessage() 函数把它们都翻译成了中文,这里列一份常用的:

确认码 含义 工程里常见场景
0x00 操作成功 / OK 一切正常
0x01 数据包接收错误 帧格式/校验和错、波特率不对、接线虚
0x02 传感器上没有手指 采图时手指还没按上(这是正常状态,不是错误)
0x03 录入指纹图像失败 成像失败
0x04 图像太干、太淡,生不成特征 手指太干
0x05 图像太湿、太糊,生不成特征 手指有水/汗
0x06 图像太乱,生不成特征 按歪了
0x07 图像正常但特征点太少(面积太小) 没按实/保护膜没撕
0x08 指纹不匹配 Match精确比对两枚特征不一致
0x09 没搜索到指纹 Search搜库没搜到(这根手指没录过)
0x0A 特征合并失败 RegModel合并失败:两次不是同一根手指,或图像质量差
0x0B 访问指纹库地址序号超出范围 ID超过容量
0x10 删除模板失败
0x11 清空指纹库失败
0x15 缓冲区无有效原始图 没采图就想生成特征
0x18 读写FLASH出错 存储异常
0x1F 指纹库满 存满了(ZW101默认50枚)
0xFF(工程自定义) 通信超时,模块无应答 没接线/没供电/波特率错

这里先把 0x08、0x09、0x0A 三个码记住,后面"踩坑实录"会反复提到它们,它们是理解录入/识别逻辑的钥匙。

5.6 常用指令码速查表

指令码 函数名 功能
0x01 PS_GetImage 采集指纹图像(探测手指)
0x02 PS_GenChar 由图像生成特征,存入Buffer1/Buffer2
0x03 PS_Match 精确比对Buffer1和Buffer2两枚特征
0x04 PS_Search 用缓冲区特征搜索指纹库,返回ID和分数
0x05 PS_RegModel 合并Buffer1、Buffer2特征,生成模板
0x06 PS_StoreChar 把模板存到指纹库指定ID位置
0x0C PS_DeletChar 删除指定ID开始的N个模板
0x0D PS_Empty 清空整个指纹库
0x0F PS_ReadSysPara 读模块基本参数
0x1D PS_ValidTempleteNum 读有效模板(已录指纹)个数
0x35 PS_HandShake 握手,检测模块是否在线

六、编程思路:串口怎么收发、帧怎么切、指令怎么封装

在贴代码之前,先把整体思路理清楚。看懂了框架,代码就是照着装。

6.1 整体框架

一句话概括:STM32通过USART3发指令帧 → 模块应答 → 串口中断把应答字节收进缓冲区 → 定时器判断一帧收完 → 在缓冲区里找应答包 → 读出确认码判断成败。

┌─────────────┐   命令帧(0x01)    ┌──────────────┐
│   STM32     │ ───────────────► │  HLK-ZW101   │
│  USART3     │                   │  指纹模块     │
│  PB10/PB11  │ ◄─────────────── │  (采集/算法/  │
└─────────────┘   应答帧(0x07)    │   存储)      │
      │                          └──────────────┘
      │ 串口中断逐字节接收
      ▼
 USART3_RX_BUFFER[]  ──► 定时器3空闲20ms判定一帧结束
      │
      ▼
 strstr查找 "EF01+地址+07" 应答包 ──► data[9]确认码 ──► 成功/失败

6.2 为什么用"串口中断接收 + 定时器空闲成帧"

串口收发数据有两种写法:查询方式中断方式

• 查询方式(死等 while 等待收完)会把CPU卡死,而且应答包长度不固定,你不知道该等几个字节;

• 更好的办法是:串口每收到1个字节就进一次中断,把字节扔进缓冲区;再用定时器做"空闲检测"——如果连续20ms没有新字节到来,就认为这一帧收完了,置一个接收完成标志 USART3_RX_FLAG=1

这是串口接收"不定长数据"的经典套路。指纹模块的应答包长度随指令变化(比如搜索应答比握手应答长),用空闲中断/超时成帧,不用提前知道长度,收到多少算多少,超时即整帧。

工程里用 USART3 中断 + 定时器3(TIMER3_Init(72, 20000),约20ms超时) 配合实现:

• USART3初始化波特率57600(USART3_Init(57600));• 每收到1字节,中断里往 USART3_RX_BUFFER 存,并喂一下定时器(重新计时);• 20ms没新字节,定时器判定一帧结束,置 USART3_RX_FLAG=1

6.3 指令发送—应答—确认码 的封装套路

每个指令函数都是同一个"三板斧"模板,理解了一个就理解了全部:

第1步:组帧发送
   SendHead()  → 发 EF01
   SendAddr()  → 发 4字节地址
   SendFlag(0x01) → 发命令包标识
   SendLength()   → 发包长度
   发指令码 + 参数
   SendCheck()    → 发校验和(边发边累加算出来)

第2步:等待并定位应答包
   data = JudgeStr(超时次数);   // 在缓冲区里找 EF01+地址+07

第3步:解析确认码
   ensure = data[9];           // data[9] 就是确认码
   return ensure;              // 0x00成功,其它失败

上层业务(录入、识别)只需要调这些函数,看返回的确认码就行,完全不用关心底层帧细节。这就是封装的好处。

6.4 两个关键的业务设计

后面踩坑会细讲,这里先点题:

1. 等待手指按下/松开:不能用固定延时去猜手指状态,要主动循环调 PS_GetImage(),靠返回码判断——返回 0x00 表示有手指且图像有效,返回 0x02 表示没手指。于是封装出 WaitFingerDown()(等按下)和 WaitFingerUp()(等抬起)两个函数。

2. 录入的"判官"是 RegModel,识别的"判官"是 Search:录入时不要拿 Match(0x03) 当卡点,真正判断两次采集是否合格的是 RegModel(0x05);识别时不要用 Compare/Match,要用 Search(0x04) 搜库返回ID。这是本文最核心的经验,第九章详讲。


七、核心代码逐段讲解

代码来自真实工程,注释都保留了中文。我按"头文件→底层收发→指令封装→录入/识别业务→主流程"的顺序讲。

7.1 驱动头文件 hlk_zw101.h:先看对外提供了哪些函数

头文件相当于"菜单",先看看这个驱动能干嘛:

#ifndef __HLK_ZW101_H
#define __HLK_ZW101_H

#include <stdio.h>
#include "stm32f10x.h"

/*============================================================
 * HLK-ZW101 指纹模组驱动
 * 通信方式 : UART 串口(工程中使用 USART3, PB10=TX -> 模块RX, PB11=RX <- 模块TX)
 * 波特率   : 默认 57600, 8 数据位, 1 停止位, 无校验
 * 帧格式   : 与 AS608 兼容
 *            包头 0xEF01 + 设备地址(4字节) + 包标识(01命令/07应答/08数据包)
 *            + 包长度(2字节) + 指令/数据 + 校验和(2字节)
 * 默认地址 : 0xFFFFFFFF
 * 与 AS608 的主要区别:
 *   1. 握手指令码为 0x35(AS608 无标准握手指令)
 *   2. 读系统参数应答的字段排列顺序不同
 *   3. 不支持 0x1B 高速搜索, 统一使用 0x04 搜索指令
 *   4. 上电初始化成功后模组会主动发送 1 字节 0x55 握手信号
 *============================================================*/

// 指纹模块触摸感应状态引脚: PB9, 触摸时输出高电平
#define PS_Sta   PBin(9)

// 特征缓冲区编号(ZW101 支持多个, 注册/比对常用 1、2 号)
#define CharBuffer1 0x01
#define CharBuffer2 0x02

// 指纹库默认搜索范围
#define FP_LIB_MAX   300     // 搜索/录入的最大页码范围
#define FP_PAGE_MAX  49      // 本工程按键选号允许的最大 ID(0~49)

extern u32 ZW101Addr;        // 模组设备地址, 默认 0xFFFFFFFF

// 搜索结果
typedef struct
{
    u16 pageID;      // 匹配到的指纹 ID(页码)
    u16 mathscore;   // 匹配分数
} SearchResult;

// 模组基本参数(对应 PS_ReadSysPara 读出的内容)
typedef struct
{
    u16 EnrollTimes;  // 注册次数
    u16 TempSize;     // 指纹模板大小
    u16 PS_max;       // 指纹库容量(可存指纹最大数量)
    u16 PS_level;     // 安全等级(分数等级)
    u32 PS_addr;      // 设备地址
    u8  PS_size;      // 通信数据包大小(0->32,1->64,2->128,3->256)
    u16 PS_baud;      // 波特率系数(实际波特率 = 系数 * 9600)
} SysPara;

// 初始化 PB9 为下拉输入, 读取手指触摸状态(触摸输出高电平)
void PS_StaGPIO_Init(void);

u8 PS_GetImage(void);                                    // 录入指纹图像
u8 PS_GenChar(u8 BufferID);                              // 生成特征
u8 PS_Match(void);                                       // 精确比对两枚特征
u8 PS_Search(u8 BufferID, u16 StartPage, u16 PageNum, SearchResult *p);  // 搜索指纹
u8 PS_RegModel(void);                                    // 合并特征生成模板
u8 PS_StoreChar(u8 BufferID, u16 PageID);                // 存储模板
u8 PS_DeletChar(u16 PageID, u16 N);                      // 删除模板
u8 PS_Empty(void);                                       // 清空指纹库
u8 PS_ReadSysPara(SysPara *p);                           // 读模组基本参数
u8 PS_ValidTempleteNum(u16 *ValidN);                     // 读有效模板个数
u8 PS_HandShake(u32 *PS_Addr);                           // 与 ZW101 模组握手(指令码 0x35)

const char *EnsureMessage(u8 ensure);                    // 确认码错误信息解析

#endif

大家看,函数名非常直观:GetImage采图、GenChar生成特征、Match比对、Search搜索、RegModel合并建模、StoreChar存储、DeletChar删除、Empty清空、ReadSysPara读参数、ValidTempleteNum读个数、HandShake握手。这就是操作指纹模块的"全套动作"。

7.2 底层发送:一个字节一个字节按帧格式发

u32 ZW101Addr = 0xFFFFFFFF;   // 模组默认地址

/*--------------------- 串口3发送一个字节 ---------------------*/
static void MYUSART_SendData(u8 data)
{
    while ((USART3->SR & 0X40) == 0);   // 等待发送数据寄存器空(TC/TXE标志)
    USART3->DR = data;
}

/*--------------------- 组包发送辅助函数 ---------------------*/
// 发送包头 0xEF01
static void SendHead(void)
{
    MYUSART_SendData(0xEF);
    MYUSART_SendData(0x01);
}
// 发送设备地址(4字节, 高字节在前)
static void SendAddr(void)
{
    MYUSART_SendData(ZW101Addr >> 24);
    MYUSART_SendData(ZW101Addr >> 16);
    MYUSART_SendData(ZW101Addr >> 8);
    MYUSART_SendData(ZW101Addr);
}
// 发送包标识(01=命令包)
static void SendFlag(u8 flag)
{
    MYUSART_SendData(flag);
}
// 发送包长度(2字节, 高字节在前)
static void SendLength(u16 length)
{
    MYUSART_SendData(length >> 8);
    MYUSART_SendData(length);
}
// 发送校验和(2字节, 高字节在前)
static void SendCheck(u16 check)
{
    MYUSART_SendData(check >> 8);
    MYUSART_SendData(check);
}

这几个辅助函数把帧的每一段拆开来发,注意所有多字节字段都是"高字节在前"(大端序),比如地址0xFFFFFFFF就发 FF FF FF FF,长度0x0003就发 00 03。

7.3 接收应答:JudgeStr 在缓冲区里定位应答包

/*--------------------- 等待并定位应答包 ---------------------
 * 功能: 发送指令后等待模组应答, 在接收缓冲中查找应答包
 *       应答包包头为 0xEF 0x01 + 地址(4字节) + 0x07
 * 参数: waittime —— 等待循环次数(每次约30ms)
 * 返回: 应答包首地址; 超时返回 0
 *------------------------------------------------------------*/
static u8 *JudgeStr(u16 waittime)
{
    char *data;
    u8 str[8];
    str[0] = 0xef;
    str[1] = 0x01;
    str[2] = ZW101Addr >> 24;
    str[3] = ZW101Addr >> 16;
    str[4] = ZW101Addr >> 8;
    str[5] = ZW101Addr;
    str[6] = 0x07;          // 应答包标识
    str[7] = '';

    USART3_RX_FLAG = 0;     // 清空接收状态
    USART3_RX_CNT  = 0;

    while (--waittime)
    {
        delay_ms(30);
        if (USART3_RX_FLAG)                 // 定时器3空闲超时, 一帧接收完毕
        {
            USART3_RX_BUFFER[USART3_RX_CNT] = '';
            USART3_RX_FLAG = 0;
            USART3_RX_CNT  = 0;
            data = strstr((const char *)USART3_RX_BUFFER, (const char *)str);
            if (data)
                return (u8 *)data;          // 找到应答包
        }
    }
    return 0;
}

这里的技巧是用 C 库函数 strstr() 在接收缓冲区里搜索特征字符串 EF 01 FF FF FF FF 07——这正是应答包的开头7字节。搜到了就返回这一包的首地址,后面 data[9] 取确认码、data[10]往后取参数;搜不到、超时了就返回0,上层把它当成 0xff(通信超时)处理。

为什么要"搜索"而不是直接从头读?因为ZW101上电可能主动发0x55,缓冲区里可能有杂字节,用strstr定位最稳妥,能自动跳过无关字节。


7.4 指令封装:以采图、生成特征、搜索为例

先看最简单的采集图像 PS_GetImage(0x01),这就是"三板斧"的标准模板:

/*--------------------- 录入指纹图像 (指令 0x01) ---------------------*/
u8 PS_GetImage(void)
{
    u16 temp;
    u8  ensure;
    u8 *data;
    SendHead();                 // 包头 EF01
    SendAddr();                 // 地址 FFFFFFFF
    SendFlag(0x01);             // 包标识: 命令包
    SendLength(0x03);           // 包长度 = 3(指令码1 + 校验和2)
    MYUSART_SendData(0x01);     // 指令码 0x01
    temp = 0x01 + 0x03 + 0x01;  // 校验和 = 标识 + 长度 + 指令码
    SendCheck(temp);            // 发送校验和
    data = JudgeStr(2000);      // 等待应答(每次30ms, 2000次约60秒)
    if (data)
        ensure = data[9];       // data[9] = 确认码
    else
        ensure = 0xff;          // 超时无应答
    return ensure;              // 0x00=采到手指, 0x02=无手指
}

生成特征 PS_GenChar(0x02),带1个参数"缓冲区号":

/*--------------------- 生成特征 (指令 0x02) ---------------------
 * BufferID: 特征存放缓冲区编号(ZW101 注册时按次取 1,2,3...; 比对取 1)
 *----------------------------------------------------------------*/
u8 PS_GenChar(u8 BufferID)
{
    u16 temp;
    u8  ensure;
    u8 *data;
    SendHead();
    SendAddr();
    SendFlag(0x01);
    SendLength(0x04);               // 长度=4(指令1+参数1+校验2)
    MYUSART_SendData(0x02);         // 指令码 0x02
    MYUSART_SendData(BufferID);     // 参数: 缓冲区号(1或2)
    temp = 0x01 + 0x04 + 0x02 + BufferID;  // 校验和要把参数也加进去
    SendCheck(temp);
    data = JudgeStr(2000);
    if (data)
        ensure = data[9];
    else
        ensure = 0xff;
    return ensure;
}

搜索指纹 PS_Search(0x04),这是识别开锁的核心,参数最多,应答里要取出ID和分数:

/*--------------------- 搜索指纹 (指令 0x04) ---------------------
 * 以指定缓冲区特征搜索指纹库
 * BufferID : 特征所在缓冲区(一般为 CharBuffer1)
 * StartPage: 搜索起始页
 * PageNum  : 搜索页数
 * 搜索成功返回 pageID 与 mathscore
 * 注: ZW101 不支持 AS608 的 0x1B 高速搜索, 统一用本指令
 *---------------------------------------------------------------*/
u8 PS_Search(u8 BufferID, u16 StartPage, u16 PageNum, SearchResult *p)
{
    u16 temp;
    u8  ensure;
    u8 *data;
    SendHead();
    SendAddr();
    SendFlag(0x01);
    SendLength(0x08);                       // 长度=8(指令1+缓冲1+起始页2+页数2+校验2)
    MYUSART_SendData(0x04);                 // 指令码 0x04
    MYUSART_SendData(BufferID);             // 缓冲区号
    MYUSART_SendData(StartPage >> 8);       // 起始页 高字节
    MYUSART_SendData(StartPage);            // 起始页 低字节
    MYUSART_SendData(PageNum >> 8);         // 搜索页数 高字节
    MYUSART_SendData(PageNum);              // 搜索页数 低字节
    // 校验和 = 标识 + 长度 + 指令码 + 缓冲号 + 起始页(2字节) + 页数(2字节)
    temp = 0x01 + 0x08 + 0x04 + BufferID
           + (StartPage >> 8) + (u8)StartPage
           + (PageNum >> 8) + (u8)PageNum;
    SendCheck(temp);
    data = JudgeStr(2000);
    if (data)
    {
        ensure = data[9];                       // 确认码
        p->pageID    = (data[10] << 8) + data[11];   // 命中页码(ID)
        p->mathscore = (data[12] << 8) + data[13];   // 匹配分数
    }
    else
        ensure = 0xff;
    return ensure;                              // 0x00=搜到, 0x09=没搜到
}

搜索成功的应答包结构是:...07(标识) 00 07(长度) 00(确认码) + PageID(2字节) + MatchScore(2字节) + 校验(2字节)。所以确认码在 data[9],页码在 data[10..11],分数在 data[12..13],都是高字节在前,用 (data[10]<<8)+data[11] 拼起来。

7.5 录入相关:合并建模、存储、握手

合并特征生成模板 PS_RegModel(0x05)——录入是否合格的"判官":

/*--------------------- 合并特征生成模板 (指令 0x05) ---------------------*/
u8 PS_RegModel(void)
{
    u16 temp;
    u8  ensure;
    u8 *data;
    SendHead();
    SendAddr();
    SendFlag(0x01);
    SendLength(0x03);
    MYUSART_SendData(0x05);             // 指令码 0x05: 把Buffer1、2特征合并
    temp = 0x01 + 0x03 + 0x05;
    SendCheck(temp);
    data = JudgeStr(2000);
    if (data)
        ensure = data[9];               // 0x00=合并成功(两次同源且合格)
    else                                // 0x0A=合并失败(不是同一手指/质量差)
        ensure = 0xff;
    return ensure;
}

存储模板 PS_StoreChar(0x06)——把合并好的模板存进指纹库指定ID:

/*--------------------- 存储模板 (指令 0x06) ---------------------
 * 将 BufferID 缓冲区模板存入 PageID 号位置
 * 官方例程注册成功后存储 Buffer1
 *---------------------------------------------------------------*/
u8 PS_StoreChar(u8 BufferID, u16 PageID)
{
    u16 temp;
    u8  ensure;
    u8 *data;
    SendHead();
    SendAddr();
    SendFlag(0x01);
    SendLength(0x06);                       // 长度=6(指令1+缓冲1+页码2+校验2)
    MYUSART_SendData(0x06);                 // 指令码 0x06
    MYUSART_SendData(BufferID);             // 缓冲区号(注册成功后存Buffer1)
    MYUSART_SendData(PageID >> 8);          // 存储位置ID 高字节
    MYUSART_SendData(PageID);               // 存储位置ID 低字节
    temp = 0x01 + 0x06 + 0x06 + BufferID
           + (PageID >> 8) + (u8)PageID;
    SendCheck(temp);
    data = JudgeStr(2000);
    if (data)
        ensure = data[9];
    else
        ensure = 0xff;
    return ensure;
}

握手 PS_HandShake(0x35)——ZW101特有,上电后先确认模块在线:

/*--------------------- 与 ZW101 握手 (指令 0x35) ---------------------
 * 功能: 检测模组是否正常在线工作
 * 说明: ZW101 握手指令码为 0x35(AS608 无此标准指令);
 *       模组上电初始化成功后还会主动发送 1 字节 0x55。
 *       本函数发送 0x35 握手指令, 收到确认码 0x00 即握手成功。
 * 返回: 0=成功, 1=失败
 *-------------------------------------------------------------------*/
u8 PS_HandShake(u32 *PS_Addr)
{
    u16 temp;
    u8  ensure;
    u8 *data;

    SendHead();
    SendAddr();
    SendFlag(0x01);
    SendLength(0x03);
    MYUSART_SendData(0x35);                 // ZW101 握手指令码
    temp = 0x01 + 0x03 + 0x35;
    SendCheck(temp);

    data = JudgeStr(100);                   // 等待应答(每次30ms, 约3秒)
    if (data)
    {
        ensure = data[9];
        if (ensure == 0x00)
        {
            if (PS_Addr)                    // 可选: 顺便读回模块地址
                *PS_Addr = ((u32)data[2] << 24) + ((u32)data[3] << 16)
                         + ((u32)data[4] << 8) + data[5];
            return 0;                       // 握手成功
        }
    }
    return 1;                               // 握手失败
}

删除、清空、读参数、读个数也是同一个模板,指令码换成 0x0C / 0x0D / 0x0F / 0x1D 而已,这里不再逐个贴。特别说一下读系统参数 PS_ReadSysPara(0x0F),ZW101的字段排列和AS608不同,解析下标要按ZW101的来:

/*--------------------- 读模组基本参数 (指令 0x0F) ---------------------
 * 【ZW101 字段排列(与 AS608 不同)】
 *   data[10..11] 已注册指纹个数
 *   data[12..13] 指纹模板大小
 *   data[14..15] 指纹库容量
 *   data[16..17] 分数等级(安全等级)
 *   data[18..21] 设备地址
 *   data[22..23] 数据包大小(0->32,1->64,2->128,3->256)
 *   data[24..25] 波特率系数(实际波特率 = 系数*9600)
 *-------------------------------------------------------------------*/
u8 PS_ReadSysPara(SysPara *p)
{
    // ...组帧发送 0x0F, 校验和 = 0x01+0x03+0x0F = 0x13...
    data = JudgeStr(2000);
    if (data)
    {
        ensure = data[9];
        if (ensure == 0x00)
        {
            p->EnrollTimes = (data[10] << 8) + data[11];   // 已注册个数
            p->PS_max      = (data[14] << 8) + data[15];   // 指纹库容量
            p->PS_level    = (data[16] << 8) + data[17];   // 安全等级
            p->PS_baud     = (data[24] << 8) + data[25];   // 波特率系数
            printf("rn指纹库容量: %d  安全等级: %d  波特率: %d",
                   p->PS_max, p->PS_level, p->PS_baud * 9600);
        }
    }
    // ...
}

读出来波特率系数是6,6×9600=57600,和我们初始化一致,这也能反过来验证通信正常。

最后是把确认码翻译成中文的 EnsureMessage(),调试时全靠它看错误:

const char *EnsureMessage(u8 ensure)
{
    const char *p;
    switch (ensure)
    {
        case 0x00: p = "操作成功"; break;
        case 0x01: p = "数据包接收错误"; break;
        case 0x02: p = "传感器上没有手指"; break;
        case 0x07: p = "指纹图像正常,但特征点太少(或面积太小)"; break;
        case 0x08: p = "指纹不匹配"; break;
        case 0x09: p = "没搜索到指纹"; break;
        case 0x0a: p = "特征合并失败"; break;
        case 0x0b: p = "访问指纹库时地址序号超出范围"; break;
        case 0x1f: p = "指纹库满"; break;
        case 0xff: p = "通信超时,模块无应答"; break;
        // ...其余码见头文件/工程
        default:   p = "模块返回确认码有误"; break;
    }
    return p;
}

7.6 业务层①:等待手指按下/抬起(解决"检测不稳"的关键)

这是踩坑后改造出来的两个函数,思想很简单:别用固定延时瞎等,主动循环采图,看返回码说话。

//---------------- 等待手指按下 ----------------
// 循环采图直到 PS_GetImage 返回 0x00(传感器检测到手指)
// wait_s: 最长等待秒数; 返回 0=检测到手指, 1=超时
static u8 WaitFingerDown(u16 wait_s)
{
    u16 trycnt = wait_s * 3;        // 每 300ms 采一次图
    u8  ensure;
    while (trycnt--)
    {
        ensure = PS_GetImage();
        if (ensure == 0x00) return 0;            // 手指已按下, 图像有效
        if (ensure != 0x02 && ensure != 0xff)    // 0x02=无手指 0xff=超时, 不刷屏
            printf("采图异常: %srn", EnsureMessage(ensure));
        delay_ms(300);
    }
    return 1;
}

//---------------- 等待手指抬起 ----------------
// 循环采图直到 PS_GetImage 返回 0x02(传感器无手指)
// wait_s: 最长等待秒数; 返回 0=手指已抬起, 1=超时
static u8 WaitFingerUp(u16 wait_s)
{
    u16 trycnt;
    u8  ensure;
    delay_ms(600);                          // 先等 0.6s, 避开刚采完图像的稳定期
    trycnt = wait_s * 3;
    while (trycnt--)
    {
        ensure = PS_GetImage();
        if (ensure == 0x02) return 0;       // 传感器无手指 = 手指已抬起
        delay_ms(300);
    }
    return 1;
}

逻辑一目了然:

WaitFingerDown:反复发采图指令,返回0x00说明手指按上了、图像也有效,立刻往下走;返回0x02说明还没手指,继续等;每300ms试一次,等到超时为止。

• WaitFingerUp:反过来,返回0x02说明手指抬起来了。开头先延时600ms,避开刚采完图的稳定期,防止误判。

这样不管用户什么时候按、按多久,程序都能稳稳地捕捉到,再也不靠"猜"。

7.7 业务层②:录指纹 Add_FR(两次采集→合并→存储)

这是录入的完整主流程,也是踩坑②的结论体现——Match只作参考不阻断,真正判合格的是RegModel

// 录指纹主流程
void Add_FR(int ID)
{
    u8 ensure;

    while (1)
    {
        /*---------------- 第一次采集 ----------------*/
        printf("请按下手指.rn");
        OLED_ShowChineseFont(...);   // 屏幕显示"请按下指纹"
        OLED_RefreshGRAM();

        if (WaitFingerDown(20))             // 最长等待 20 秒
        {
            printf("超时未检测到手指---退出录入...rn");
            return;
        }
        ensure = PS_GenChar(CharBuffer1);   // 第一次图像生成特征 -> Buffer1
        if (ensure != 0x00)
        {
            printf("第一次生成特征失败: %s, 请重新按手指rn", EnsureMessage(ensure));
            delay_ms(1500);
            continue;
        }
        printf("CharBuffer1 指纹 生成特征 正确.rn");
        BEEP = 1; delay_ms(100); BEEP = 0;  // 短响提示第一次采集成功

        /*---------------- 等待抬起手指 ----------------*/
        printf("请抬起手指...rn");
        if (WaitFingerUp(10))               // 最长等待 10 秒
        {
            printf("手指一直未抬起, 本次录入作废, 请重新开始.rn");
            delay_ms(1500);
            continue;
        }

        /*---------------- 第二次采集 ----------------*/
        printf("再按一次手指(同一根手指).rn");
        OLED_ShowChineseFont(...);          // 屏幕显示"再次按下指纹"
        OLED_RefreshGRAM();

        if (WaitFingerDown(20))
        {
            printf("超时未检测到手指---退出录入...rn");
            return;
        }
        ensure = PS_GenChar(CharBuffer2);   // 第二次图像生成特征 -> Buffer2
        if (ensure != 0x00)
        {
            printf("第二次生成特征失败: %s, 请重新录入rn", EnsureMessage(ensure));
            delay_ms(1500);
            continue;
        }
        printf("CharBuffer2 指纹 生成特征 正确.rn");
        BEEP = 1; delay_ms(100); BEEP = 0;

        /*---------------- 精确比对两次特征(仅作参考, 不阻断流程) ----------------
         * ZW101 官方注册流程不用 Match 做卡点: 两次按手指的角度、面积稍有差异,
         * Match 精确比对就可能返回 0x08(不匹配), 但两次特征仍可正常融合。
         * 真正的判官是下一步的 RegModel(0x05 合并):
         *   RegModel 返回 0x00 = 两次特征同源且质量合格, 可存储;
         *   RegModel 返回 0x0a = 两次特征差异过大/图像太差, 才需要重录。
         *--------------------------------------------------------------*/
        printf("对比两次指纹(参考).rn");
        ensure = PS_Match();
        if (ensure == 0x00)
            printf("两次指纹精确匹配.rn");
        else
            printf("Match=%02X(不阻断流程), 继续合并, 以 RegModel 结果为准.rn", ensure);

        /*---------------- 合并模板并存储(真正判定) ----------------*/
        ensure = PS_RegModel();             // 0x05: 合并 Buffer1+Buffer2
        if (ensure != 0x00)
        {
            printf("生成指纹模板失败(0x0a=合并失败): %srn", EnsureMessage(ensure));
            printf("请重录: 同一根手指按在采集窗正中间、按实, 两次位置尽量一致;rn");
            printf("      手指别太干, 并确认模组独立稳定供电、镜头保护膜已撕。rn");
            BEEP = 1; delay_ms(500); BEEP = 0;
            delay_ms(1500);
            continue;                       // 合并失败, 重来
        }
        printf("生成指纹模板成功(两次特征融合成功).rn");

        ensure = PS_StoreChar(CharBuffer1, ID);  // 0x06: 存到 ID 号位置
        if (ensure == 0x00)
        {
            printf("添加指纹成功, ID=%d.rn", ID);
            PS_ValidTempleteNum(&ValidN);       // 读一下当前库存
            printf("当前有效指纹个数:%drn", ValidN);
            BEEP = 1; delay_ms(100); BEEP = 0; delay_ms(100);  // 成功"嘀嘀"两声
            BEEP = 1; delay_ms(100); BEEP = 0; delay_ms(100);
            return;
        }
        printf("存储指纹失败: %srn", EnsureMessage(ensure));
        delay_ms(1500);
    }
}

整个录入链条是:等按下→采图→生成特征到Buf1→等抬起→等再按下→采图→生成特征到Buf2→(Match参考)→RegModel合并判定→StoreChar存库。任何一步失败都会打印中文原因并 continue 重来,体验很友好。

7.8 业务层③:刷指纹识别 press_FR(采图→生成特征→搜库)

待机时主循环约每500ms调一次,检测到手指就搜库。用了一个 armed 静态标志,保证一次按压只搜一次,手指抬起后才重新"布防",避免手指一直按着反复搜索刷屏:

// 函数功能: 搜索指纹(刷指纹开锁)
// 返回值: -1表示搜索失败  >=0 表示搜索到的指纹ID
int press_FR(void)
{
    static u8 armed = 1;            // 1=已布防, 可以检测新手指示
    SearchResult seach;
    u8 ensure;

    ensure = PS_GetImage();
    if (ensure == 0x00)             // 检测到手指
    {
        if (!armed) return -1;      // 本次按压已处理过, 等手指抬起
        armed = 0;                  // 撤防

        ensure = PS_GenChar(CharBuffer1);
        if (ensure == 0x00)         // 生成特征成功
        {
            // ZW101 不支持 0x1B 高速搜索, 使用 0x04 普通搜索, 搜索整个指纹库
            ensure = PS_Search(CharBuffer1, 0, FP_LIB_MAX, &seach);
            if (ensure == 0x00)     // 搜索成功
            {
                printf("搜索指纹成功.rn");
                printf("Match ID:%d  Match score:%drn", seach.pageID, seach.mathscore);
                TOUCH_ID = seach.pageID;    // 保存指纹ID
                return seach.pageID;        // 返回ID(>=0), 主程序据此开锁
            }
            else
            {
                ShowErrMessage(ensure);     // 0x09=没搜到等
            }
        }
        else
        {
            ShowErrMessage(ensure);
        }
        delay_ms(1000);           // 失败提示停留
    }
    else
    {
        if (ensure == 0x02)
            armed = 1;            // 传感器无手指=手指已抬起, 重新布防
        else if (ensure != 0xff)  // 通信超时不打印, 避免刷屏
            printf("获取图像失败...%srn", EnsureMessage(ensure));
    }
    return -1;
}

识别链条是:采图(GetImage)→生成特征到Buf1(GenChar)→搜库(Search,从0页搜FP_LIB_MAX页)→命中返回ID和分数。注意这里识别用的是 Search(0x04),不是 Match/Compare——Search 才是"拿当前指纹去整个指纹库里1:N找人",Match 只是"1:1比对两枚特征",这是踩坑②的核心。

7.9 业务层④:删除/清空指纹

// 删除指纹
// num: 要删除的指纹ID; 传入 0xFF00 表示清空整个指纹库
void Del_FR(int num)
{
    u8 ensure;
    printf("显示删除指纹: %d.rn", num);
    delay_ms(50);

    if (num == 0xFF00)
        ensure = PS_Empty();            // 0x0D: 清空指纹库
    else
        ensure = PS_DeletChar(num, 1);  // 0x0C: 从 num 开始删 1 个
    if (ensure == 0)
        printf("删除指纹成功.rn");
    else
        ShowErrMessage(ensure);
    delay_ms(1500);

    PS_ValidTempleteNum(&ValidN);       // 读剩余个数
    printf("显示剩余指纹个数:%drn", ValidN);
}

PS_DeletChar(PageID, N) 支持从某个ID开始连续删N个,删一个就传N=1;想全部清空就发 PS_Empty()(0x0D)。

7.10 main.c 主流程:初始化握手→待机循环

主函数做的事:初始化外设→和模块握手(失败就一直重试)→读指纹个数和参数→进入主循环(检测按键进管理菜单 / 周期检测指纹开锁)。

int main()
{
    JTAG_Set(SWD_ENABLE);          // 关闭JTAG保留SWD, 释放PA15/PB3/PB4

    u8 key = 0;
    u32 time_cnt = 0;
    int run_state = 0;

    BEEP_Init();                   // 蜂鸣器
    LED_Init();                    // LED
    USART1_Init(115200);           // 串口1: 打印调试日志
    printf("666串口1初始化成功..rn");

    BEEP = 1; LED1 = 0; LED2 = 0; LED3 = 0; delay_ms(500);
    BEEP = 0; LED1 = 1; LED2 = 1; LED3 = 1; delay_ms(500);

    KEY_Init();                    // 按键
    Oled_Init();  OLED_Clear();    // 0.96寸OLED

    USART3_Init(57600);            // 串口3: 接指纹模块(ZW101 默认57600)
    TIMER3_Init(72, 20000);        // 定时器3: 辅助串口3接收, 20ms空闲超时成帧
    PS_StaGPIO_Init();             // 触摸状态脚 PB9

    // 与 HLK-ZW101 握手(指令码0x35), 失败一直重试
    while (PS_HandShake(&ZW101Addr))
    {
        OLED_Clear();
        OLED_ShowString(0, 16 * 0, 16, "ZW101 Init  Error...");
        OLED_RefreshGRAM();
        printf("连接ZW101指纹模块失败....rn");
        delay_ms(1000);
    }
    printf("ZW101指纹模块连接成功.....rn");
    OLED_Clear();
    OLED_ShowString(0, 16 * 0, 16, "ZW101 Init Success...");
    OLED_RefreshGRAM();
    delay_ms(1000); delay_ms(1000); delay_ms(1000);

    u8 ensure = PS_ValidTempleteNum(&ValidN);   // 读库指纹个数
    if (ensure != 0x00) printf("ensure:%d  %srn", ensure, EnsureMessage(ensure));

    ensure = PS_ReadSysPara(&ZW101Para);        // 读模块参数(容量/等级/波特率)
    if (ensure == 0x00)
        printf("指纹最大容量:%d  安全等级:%drn", ZW101Para.PS_max, ZW101Para.PS_level);
    else
        printf("ensure:%d  %srn", ensure, EnsureMessage(ensure));

    while (1)
    {
        key = KEY_Scan();                       // 按键检测
        if (key) { beep_prompt(); printf("key=%drn", key); }

        // K1: 进入管理员验证(密码1212), 通过后可录指纹/删指纹
        if (key == 1)
        {
            OLED_Clear(); OLED_RefreshGRAM();
            run_state = page5_pass_open(1);     // 输管理员密码
            if (run_state == 0)
            {
                page3_EnterFingerprint();       // 指纹录入页面
                OLED_Clear(); OLED_RefreshGRAM();
                page4_DeleteFingerprint();      // 指纹删除页面
                OLED_Clear(); OLED_RefreshGRAM();
            }
        }

        // 约每 500ms 一次
        if (time_cnt > 50)
        {
            LED1 = !LED1; LED2 = !LED2;         // 系统运行心跳灯
            time_cnt = 0;

            // 待机周期采图检测手指, 命中指纹则开锁
            run_state = press_FR();
            if (run_state >= 0)
            {
                close_lock();                   // 解锁(可在此驱动继电器/电磁锁)
            }
        }
        DelayMs(10);
        time_cnt++;
    }
}

说明:工程里 open_lock()/close_lock() 是预留的锁控函数,你可以在里面拉高/拉低一个GPIO去驱动继电器、电磁锁或舵机close_lock() 在识别成功后被调用,表示"解锁"。蓝线触摸脚PB9工程里做了初始化,但待机采图轮询已经够用,所以主循环不依赖它(ZW101的触摸信号在模组进入休眠流程后才有效,本工程不休眠)。


八、完整录入/识别流程

8.1 录入流程(一枚指纹要采两次)

录入一枚指纹,标准流程是"两次采集、合并成模板、存库",步骤如下:

1. 握手:上电发0x35确认模块在线;

2. 读参数确认:发0x1D读库存、0x0F读参数,确认容量/等级正常;

3. 第一次按手指WaitFingerDown 等到 GetImage 返回0x00 → GenChar(Buffer1) 生成特征存入特征缓冲区1,蜂鸣器"嘀"一声;

4. 松开手指WaitFingerUp 等到 GetImage 返回0x02(无手指);

5. 第二次按同一根手指WaitFingerDown → GenChar(Buffer2)生成特征存入缓冲区2;

6. (可选)Match参考:发0x03精确比对两枚特征,结果只打印不阻断;

7. 合并建模:发 RegModel(0x05),把Buf1、Buf2两枚特征融合成一枚完整模板。返回0x00=融合成功(说明是同一根手指且质量合格);返回0x0A=合并失败(两次不是同一手指或图像太差),需重录

8. 存库:发 StoreChar(0x06),把模板存到指定ID;成功后蜂鸣器"嘀嘀"两声,并读回库存个数。

为什么要采两次?因为单次按手指接触面有限、角度随机,模块把两次(甚至多次)采集的特征融合成一枚更完整的模板,识别率更高、更抗角度变化。这也是为什么两次必须是同一根手指,否则 RegModel 会返回 0x0A。

8.2 识别流程(刷指纹开锁)

识别是"1:N搜库",流程如下:

1. 待机时主循环约每500ms调一次 press_FR()

2. GetImage(0x01) 采图:返回0x02说明没手指,静默返回;返回0x00说明有手指;

3. GenChar(Buffer1) 把当前图像生成特征;

4. Search(0x04) 用这枚特征从第0页开始搜索整个指纹库(FP_LIB_MAX页)

5. 返回0x00=搜到:应答里带命中的 pageID(指纹ID)和 mathscore(匹配分数),打印并返回ID,主程序执行开锁(蜂鸣器+OLED"开锁成功"+驱动锁);

6. 返回0x09=没搜到:说明这根手指没录过(或按得太差),提示失败;

7. 用 armed 标志保证一次按压只搜一次,等手指抬起(再次0x02)后才重新布防。


九、调试踩坑实录(重点!两个真实大坑)

这部分是全文最值钱的地方。从AS608移植到ZW101,我前后卡了两个坑,都是"代码看着没问题、功能就是不对"的那种。把现象、排查、根因、解法完整写出来,大家遇到能直接对号入座。

坑①:手指检测不稳——明明按了手指,模块时灵时不灵

【现象】

录指纹时,手指都按在采集窗上了,屏幕却一直停在"请按下指纹";有时候按一下有反应、有时候要按好几次才响应;偶尔录到一半又突然报无手指。体验非常差,根本没法用。

【最初的错误写法】

一开始图省事,用的是"固定延时轮询":发一次采图指令,然后 delay_ms(1000) 等一秒,再去读结果。问题就在这——

    • 延时太短:模块还没完成采集,你就去读,读到的是旧状态;• 延时太长:用户手指可能已经抬起了,你还在等,错过时机;• 固定延时本质是"猜",猜用户什么时候按、按多久,怎么猜都不稳。

再加上当时指纹模块是直接插在STM32板子的3.3V引脚上取电,供电电流不足,采集瞬间电压跌落,模块应答时有时无,雪上加霜。

【排查过程】

1. 先用串口助手/串口日志盯着 PS_GetImage() 的返回码看,发现没手指时稳定返回 0x02(传感器上没有手指),有手指且图像有效时返回 0x00这两个码就是判断手指状态最可靠的依据,根本不用延时去猜。

2. 万用表量模块VCC电压,采集瞬间电压被拉低,确认供电不足。

3. 检查模块,发现新模块镜头上的保护膜没撕,成像受影响。

【解决办法】

• 软件上:废弃固定延时,改成主动等待的 WaitFingerDown()WaitFingerUp() 两个函数(代码见7.6节)。循环调用 PS_GetImage(),用返回码判断:0x00=有手指(按下)、0x02=无手指(抬起/没按)。轮询间隔从原来的1s缩短到300ms,捕捉手指更灵敏;每次采集成功蜂鸣器短响一声,盲操作也有反馈。

• 硬件上:• ① 指纹模块改用独立、稳定的3.3V供电(别用STM32板载3.3V硬带),GND共地,万用表确认VCC稳定在3.3V±0.1V;

• ② 手指按在采集窗正中间、按实,不要只搭边;

• ③ 撕掉镜头保护膜

• ④ 手指太干会返回0x04,哈口气或搓一搓再按。

改完之后,手指一按就有响应,百发百中。这个坑告诉我们:和硬件打交道,"主动查询状态"永远比"被动延时猜测"可靠;软件问题和硬件供电问题要一起查。

坑②:Search/Match 始终失败,指纹就是"匹配不上"

【现象】

指纹明明按流程录进去了(StoreChar也返回成功了),可一到识别环节,刷手指就是不开锁。日志里要么返回 0x08(指纹不匹配),要么搜索返回失败,怎么按都搜不到。一度怀疑模块坏了。

【排查过程与根因】

这个坑的根子在于没分清几个指令到底是干嘛的,想当然地用错了"判官"。我梳理一下三个容易混淆的指令:

指令 作用 用在哪
PS_Match 0x03 1:1精确比对 Buffer1和Buffer2里的两枚特征,只告诉你"这俩像不像" 录入时两次采集之间做参考
PS_RegModel 0x05 把Buffer1、Buffer2两枚特征合并融合成一枚模板 录入时判断是否合格的判官
PS_Search 0x04 拿Buffer里的特征去整个指纹库做1:N搜索,返回命中的ID和分数 识别开锁的唯一正确指令

我先后犯了两个错误:

错误1:识别时用 Compare/Match(0x03)去"匹配指纹"。

Match只是比对缓冲区里那两枚特征,它根本不会去指纹库里找。你录好的指纹是存在库里的,光Match缓冲区里的东西,当然永远"匹配不上"。识别开锁要的是"这根手指是谁",必须用 Search(0x04) 搜库,让模块拿当前特征去和库里所有模板逐一比对,命中就返回ID和分数。

错误2:录入时把 Match(0x03) 当成合格判官,Match一返回0x08就判定录入失败、推倒重来。

两次按手指,角度、接触面积不可能完全一样。Match是"精确比对",稍有差异就可能返回0x08(不匹配),但这不代表两次不是同一根手指、也不代表不能融合。官方注册流程根本不拿Match卡流程。真正判断两次采集能不能合成一枚合格模板的,是 RegModel(0x05)

RegModel 返回 0x00 = 两枚特征融合成功,说明确实是同一根手指、图像质量合格,可以存库;

RegModel 返回 0x0A = 特征合并失败,说明两次差异太大(不是同一根手指)或图像质量太差,这时候才需要重新录。

还有一个前提容易漏:必须两次采集、合并成一枚模板(Buf1+Buf2→RegModel),再 StoreChar 存进库,这枚指纹之后才能被 Search 搜到。 如果只采一次、或者没合并就存,库里的模板是不完整/无效的,搜索自然搜不到。

【解决办法】

• 录入流程固定为:第一次采图→GenChar(Buf1)→等抬起→第二次采图→GenChar(Buf2)→RegModel合并(以它的0x00/0x0A判定成败)→StoreChar存库;Match只打印结果做参考,不阻断流程

• 识别流程固定为:采图→GenChar(Buf1)→Search(0x04)从0页搜整个库,用返回的确认码判断:0x00=搜到(带ID和分数),0x09=没搜到(这根手指没录过);

• 不要用AS608的 0x1B 高速搜索指令,ZW101不支持,统一用0x04;

• 录入时两次用同一根手指、按在正中间、位置尽量一致,能大幅降低 RegModel 返回0x0A的概率。

改完之后,录入的指纹一刷一个准,串口能稳定打印 Match ID:x  Match score:xxx,分数越高越匹配。

一句话总结坑②:录入看 RegModel,识别用 Search,Match 只是参考。 记住这三句,指纹识别的逻辑就通了。

图8 调试踩坑前后串口日志对比


十、常见问题 FAQ

Q1:模块完全没应答,握手一直失败(OLED显示 Init Error)?
A:按这个顺序查:① 供电——是否3.3V、电流够不够、GND有没有共地(供电是头号原因);② 接线——TX/RX是否交叉接(STM32的PB10 TX接模块RX,PB11 RX接模块TX),杜邦线有没有松动虚接;③ 波特率——USART3必须是 57600、8N1;④ 模块是否上电正常(可观察模块灯,ZW101上电初始化成功会主动发0x55)。

Q2:串口打印乱码?
A:波特率不匹配。调试串口USART1是115200,指纹串口USART3是57600,别搞混;确认晶振是8MHz、系统时钟72MHz,延时和波特率才准。

Q3:录入时 RegModel 一直返回 0x0A(特征合并失败)?
A:两次不是同一根手指,或两次图像质量差/位置差太多。用同一根手指、按采集窗正中间、按实,两次位置尽量一致;手指别太干;确认保护膜已撕、供电稳定。

Q4:识别时一直返回 0x09(没搜索到指纹)?
A:这根手指没录成功/没存进库,或按得太差。先重新完整录一次(确认StoreChar返回0x00、ValidTempleteNum数量增加),识别时手指按正按实;也可能是录的时候没走RegModel合并、模板无效。

Q5:返回 0x08(指纹不匹配)要紧吗?
A:在录入环节Match返回0x08是正常的(两次角度有差异),不用管,以RegModel为准;在识别环节你根本不会看到0x08——识别用的是Search,失败返回的是0x09。如果你在识别时去调了Match,那就是用错指令了,见坑②。

Q6:返回 0x07(特征点太少)?
A:手指没按实、接触面积太小,或镜头保护膜没撕、镜头脏。把手指按满采集窗、清洁镜头。

Q7:能存多少枚指纹?存满了怎么办?
A:ZW101默认版本存 50枚(ID 0~49,工程里FP_PAGE_MAX=49)。存满返回0x1F(指纹库满),可以用PS_Empty清空,或删除不用的ID。大容量版本(如ZW111)支持100枚。

Q8:一定要接触摸感应脚(蓝线PB9)吗?
A:本工程不强制。ZW101的TOUCH触摸信号在模组进入休眠流程后才有效,本工程不休眠、待机直接周期采图检测手指,所以蓝线可接可不接。想做低功耗唤醒时再用它。

Q9:搜索分数(mathscore)多少算通过?
A:Search只要返回0x00就是达到安全等级阈值、通过了。安全等级默认Level2(共1~5级),等级越高阈值越严、越安全但越难识别,可通过写寄存器指令调整。

Q10:AS608的老代码能直接烧给ZW101用吗?
A:大部分能(协议兼容),但务必改4点:握手用0x35、读系统参数字段顺序按ZW101解析、不要用0x1B高速搜索(改用0x04)、注意上电0x55。详见第三章。


十一、应用场景与扩展玩法

学会驱动指纹模块后,能做的东西非常多:

指纹门锁/门禁:识别成功驱动继电器、电磁锁或舵机开锁,配OLED显示和蜂鸣器提示,就是本文成品;•

指纹考勤机:Search返回的ID对应用工号,加个RTC时钟,把"ID+时间"存下来就是打卡记录;•

指纹保险柜/储物箱:指纹开锁替代机械钥匙/密码,安全又方便;•

电子密码锁升级:在密码锁基础上加指纹,指纹+密码双重认证;•

电动车/摩托车防盗启动:指纹验证通过才允许点火;•

权限分级:利用指纹ID区分管理员/普通用户,不同ID开放不同功能;•

联网扩展:ZW101只有串口,你可以再加一颗蓝牙/WiFi模块(如HC-05、ESP8266),把开锁记录、报警信息推送到手机,或做远程临时授权;•

低功耗电池供电:利用ZW101的休眠(8μA级)+触摸唤醒脚,做成电池供电的无线门锁。

ZW101本身还支持RGB灯自定义颜色/闪烁、上传指纹图像、自动注册(PS_AutoEnroll一站式注册)等高级功能,手册里都有,入门后可以慢慢挖。


十二、资料与工程获取

本文涉及的完整工程(含 hlk_zw101.h / hlk_zw101.c 驱动、main.c、OLED/按键/蜂鸣器/串口/定时器驱动)、ZW101官方协议手册、录删识别完整源码,都已经打包整理好。

项目开发使用的全部软件工具已经上传到网盘:https://ccnr8sukk85n.feishu.cn/wiki/QjY8weDYHibqRYkFP2qcA9aGnvb?from=from_copylink

相关推荐

嵌入式开发工程师

微信公众号