1. 项目缘起与整体方案设计
1.1 为什么选STM32F103C8T6加AS608这个组合
手头这个指纹识别项目,其实最早是给一个做考勤机的小团队做的原型验证。他们原本用的是某款带指纹功能的专用芯片,但开发自由度太低,想改个UI或者加个联网功能都束手束脚。后来聊下来,大家一致觉得用STM32F103C8T6配AS608指纹模块是最务实的路子——前者是嵌入式圈子里公认的“国民MCU”,资料多、价格低、供货稳;后者是光学指纹模块里性价比极高的一款,串口通信协议简单,识别率也够用。
STM32F103C8T6这颗芯片,72MHz主频、64KB Flash、20KB SRAM,对于指纹识别这种任务来说绰绰有余。它有三个USART,其中一个用来跟AS608通信,另一个可以接串口屏或者调试输出,剩下的还能扩展别的外设。AS608模块内部自带DSP运算单元,指纹录入、比对、搜索这些重活它自己就干了,STM32只需要发命令、收结果就行,CPU占用率极低。这个分工很关键——很多新手容易犯一个错误,试图在MCU上跑指纹算法,那基本是给自己找不痛快。
OLED显示屏我选的是0.96寸I2C接口的SSD1306,原因很简单:接线少。I2C只需要两根线,SCL和SDA,加上电源和地,四根线搞定。相比SPI接口的OLED,省了CS、DC、RES三根线,对于F103C8T6这种引脚不算特别富裕的芯片来说,能省一根是一根。而且I2C速率设到400kHz,刷一屏指纹状态信息完全感觉不到延迟。
整个系统的数据流是这样的:手指按下→AS608采集图像并提取特征→与模块内存储的指纹模板比对→通过串口返回比对结果给STM32→STM32解析结果→在OLED上显示“识别成功”或“识别失败”→如果成功则驱动继电器或其他执行机构。这个链路里,STM32扮演的是“指挥官”角色,AS608是“专业执行者”,OLED是“信息窗口”。
1.2 硬件选型背后的成本与性能权衡
先说说为什么没选ZW101。ZW101确实更小、功耗更低,但它的通信协议跟AS608不兼容,而且开发资料相对少一些。对于第一次做指纹项目的朋友来说,AS608的文档更全,社区里踩坑记录也多,遇到问题更容易搜到答案。另外AS608的光学传感器对干手指、湿手指的适应性比电容式的好一些,虽然体积大点,但放在考勤机、门禁这种场景里完全不是问题。
STM32F103C8T6的国产替代问题也值得提一句。现在市面上有很多Pin-to-Pin兼容的国产芯片,比如GD32F103、CH32F103等。如果你的项目对成本极度敏感,可以考虑这些替代品,但要注意一点:国产替代品的Flash等待周期、ADC精度、USB外设兼容性可能有细微差异。我实测过GD32F103在跑AS608串口通信时完全没问题,但如果你用了USB DFU升级功能,可能就需要额外调试。所以建议第一版还是用原厂STM32F103C8T6把逻辑跑通,后面再考虑换国产降本。
OLED模块这里有个坑要提前说:市面上0.96寸OLED有I2C和SPI两种,I2C版本又分地址0x78和0x7A两种。买的时候一定要问清楚,或者自己用I2C扫描程序确认地址。我遇到过一批模块标称0x78实际是0x7A的,调了半天以为是驱动问题,结果是地址错了。另外OLED的供电电压一般是3.3V,但有些模块标称5V tolerant,实际接5V时间长了会烧,建议统一用3.3V供电。
1.3 软件架构:HAL库还是标准库
这个问题在STM32社区里争论了很多年。我的建议很明确:新项目一律用HAL库。原因有三:第一,ST现在主推HAL,新芯片只提供HAL支持;第二,HAL的抽象层让代码在不同F1、F4、H7系列之间移植变得容易;第三,CubeMX工具可以自动生成初始化代码,省去大量查手册配寄存器的时间。
但HAL库也有它的代价——代码体积大、执行效率略低。对于F103C8T6这种64KB Flash的芯片,如果全用HAL库,加上OLED驱动和AS608协议栈,编译出来大概占30-40KB,还剩一半空间,够用。如果你实在在意那点效率,可以在关键路径上直接用寄存器操作,比如串口中断里的数据搬运。但整体架构还是建议HAL搭台,局部优化。
FreeRTOS要不要上?对于这个项目,我的看法是:如果你只是做指纹录入和识别,裸机跑前后台架构完全够用。主循环里轮询AS608状态、刷新OLED、检测按键,逻辑简单清晰。但如果你后续要加联网、加多用户管理、加日志存储,那FreeRTOS就值得上了。任务划分可以这样:一个任务专门处理AS608通信(阻塞式收发),一个任务刷OLED(定时刷新),一个任务处理按键和业务逻辑。任务间用消息队列传递指纹ID和识别结果。
2. 核心硬件连接与底层驱动要点
2.1 STM32F103C8T6最小系统板引脚分配
先把手上的板子理清楚。F103C8T6最小系统板通常引出所有GPIO,但有些板子会把PB3、PB4、PA15这几个JTAG引脚默认占用,如果你要用它们做普通IO,需要先禁用JTAG功能。我一般习惯这样分配:
- USART1(PA9/PA10):接AS608模块。PA9是TX,接模块的RX;PA10是RX,接模块的TX。注意交叉连接,TX对RX,RX对TX,这是串口通信最基本的规则,但新手经常接错。
- I2C1(PB6/PB7):接OLED。PB6是SCL,PB7是SDA。这两个引脚需要外接4.7kΩ上拉电阻到3.3V。有些OLED模块自带上拉,但为了稳定,我建议还是在MCU端加上。
- PA0:接一个按键,用于触发指纹录入模式。按键另一端接地,PA0配置为上拉输入,按下时读到低电平。
- PC13:板载LED,用于指示系统运行状态。这个引脚在最小系统板上通常已经焊好LED,低电平点亮。
- PA2/PA3:预留作为调试串口,接USB转TTL模块,方便打印日志。
这里有个细节:AS608模块的供电是3.3V,但它的逻辑电平也是3.3V,所以直接跟STM32的串口对接没问题。如果你用的是5V的AS608模块(有些老版本是5V供电),那TX/RX需要电平转换,否则可能烧掉STM32的串口引脚。买模块的时候一定要确认供电电压。
2.2 AS608串口通信协议拆解
AS608的通信协议说简单也简单,说复杂也复杂。简单在于它就是一个标准的串口命令-应答模式;复杂在于它的数据包格式有严格的校验和,而且不同命令的应答长度不一样。
一个完整的命令包格式是这样的:
| 字段 | 长度 | 说明 |
|---|---|---|
| 包头 | 2字节 | 固定为0xEF01 |
| 芯片地址 | 4字节 | 默认0xFFFFFFFF |
| 包标识 | 1字节 | 0x01表示命令,0x02表示数据,0x08表示结束 |
| 包长度 | 2字节 | 从包标识到校验和的总长度 |
| 指令/数据 | N字节 | 具体命令码和参数 |
| 校验和 | 2字节 | 从包标识到数据末尾的累加和 |
举个例子,让AS608模块自动采集指纹并生成特征,命令码是0x01,后面跟一个参数0x01表示采集到缓冲区1。整个包就是:EF01 FFFFFFFF 01 0004 01 01 0005。最后两个字节0005是校验和,计算方法是:01+00+04+01+01 = 07?不对,这里要仔细算。
实际计算:包标识0x01 + 包长度高字节0x00 + 包长度低字节0x04 + 指令0x01 + 参数0x01 = 0x07。但校验和是两个字节,高字节是0x00,低字节是0x07。所以完整包是:EF01 FFFFFFFF 01 0004 01 01 0007。
这个校验和计算是新手最容易出错的地方。我建议写一个专门的函数来处理打包和校验,不要每次手动算。下面是一个参考实现:
void AS608_SendCmd(uint8_t cmd, uint8_t *param, uint16_t paramLen) { uint8_t buf[64]; uint16_t idx = 0; uint16_t sum = 0; buf[idx++] = 0xEF; buf[idx++] = 0x01; buf[idx++] = 0xFF; buf[idx++] = 0xFF; buf[idx++] = 0xFF; buf[idx++] = 0xFF; buf[idx++] = 0x01; // 包标识:命令 uint16_t pkgLen = paramLen + 3; // 包标识+包长度+指令+参数+校验和 buf[idx++] = (pkgLen >> 8) & 0xFF; buf[idx++] = pkgLen & 0xFF; buf[idx++] = cmd; for (uint16_t i = 0; i < paramLen; i++) { buf[idx++] = param[i]; } // 计算校验和:从包标识开始到参数末尾 for (uint16_t i = 6; i < idx; i++) { sum += buf[i]; } buf[idx++] = (sum >> 8) & 0xFF; buf[idx++] = sum & 0xFF; HAL_UART_Transmit(&huart1, buf, idx, 1000); }接收解析稍微麻烦一点,因为AS608的应答包长度不固定。我的做法是:先等包头0xEF01,然后收完剩下的字节,根据包长度字段确定总长度,最后校验。接收可以用中断+环形缓冲区的方式,也可以用DMA+空闲中断。对于F103C8T6,我推荐DMA+空闲中断,效率高且不占CPU。
2.3 OLED I2C驱动移植与汉字显示
OLED驱动这块,网上有很多现成的代码,但质量参差不齐。我建议自己移植一份干净的SSD1306驱动,核心函数就几个:初始化、写命令、写数据、刷屏、清屏。I2C通信可以用HAL库的HAL_I2C_Mem_Write,也可以用软件模拟I2C。硬件I2C在F103上有个历史遗留问题——某些批次的芯片I2C外设有bug,容易卡死。如果你遇到了,别怀疑自己,换软件I2C就行。
软件I2C的代码很简单,就是手动翻转SCL和SDA:
void I2C_Start(void) { SDA_HIGH(); SCL_HIGH(); delay_us(2); SDA_LOW(); delay_us(2); SCL_LOW(); delay_us(2); }OLED显示汉字需要取模。常用的取模软件是PCtoLCD2002,设置方式:阴码、逐列式、顺向、C51格式。取出来的数组直接放到代码里,用OLED_ShowChinese函数显示。注意汉字是16x16像素,一个汉字占32字节。如果你要显示多个汉字,建议做一个汉字索引表,用Unicode码或者自定义编码来查找,不然代码里全是硬编码的数组,维护起来很痛苦。
注意:OLED初始化之前一定要给足延时,特别是上电后的复位时间。我遇到过一批模块,上电后立刻发命令没反应,延时100ms后就正常了。SSD1306数据手册里写的是VCC稳定后至少延时100ms再发命令。
3. 指纹录入与识别完整实现
3.1 指纹录入流程的每一步拆解
指纹录入不是一步完成的,AS608的流程是:采集图像→生成特征→与缓冲区比对→存储到指定ID。每一步都是一个独立的串口命令,需要等上一步成功后才能进行下一步。
第一步,采集图像。发送命令0x01,参数0x01表示采集到缓冲区1。模块返回0x00表示成功,返回0x02表示传感器上没有手指,返回0x03表示图像太干或太湿。这里有个实操技巧:手指按下去之后不要马上松开,保持1-2秒,让模块有足够时间采集到清晰的图像。AS608的采集时间大概在0.5秒左右,但如果你手指抖动,可能需要重试。
第二步,生成特征。发送命令0x02,参数0x01表示从缓冲区1生成特征。模块返回0x00表示成功,返回0x06表示图像太乱无法生成特征,返回0x07表示特征点太少。这一步失败通常是因为手指太干或者按得太轻,让用户重新按一次就行。
第三步,再次采集并生成特征到缓冲区2。为什么要采两次?因为AS608需要比对两次采集的特征是否一致,确保录入的指纹质量可靠。这一步跟第一步、第二步一样,只是缓冲区换成2。
第四步,比对两个缓冲区的特征。发送命令0x03,模块返回0x00表示两个特征匹配,返回0x08表示不匹配。如果不匹配,说明两次按的手指位置差异太大,需要重新录入。
第五步,存储模板。发送命令0x06,参数是缓冲区编号和要存储的ID号。比如存储到ID 1,参数就是0x01 0x00 0x01(缓冲区1,ID高字节0x00,ID低字节0x01)。模块返回0x00表示存储成功,返回0x01表示ID已存在,返回0x0B表示ID超出范围。
整个流程走下来,如果一切顺利,大概需要3-5秒。如果中间某一步失败,需要从失败的那一步重新开始,而不是从头再来。我建议在代码里用一个状态机来管理这个流程,每个状态对应一个命令,收到成功应答就跳到下一个状态,收到失败应答就回到指定状态重试。
3.2 指纹识别与搜索的代码实现
识别流程比录入简单:采集图像→生成特征→在指纹库中搜索。搜索命令是0x04,参数是缓冲区编号和搜索起始ID、搜索数量。模块返回0x00表示搜索到匹配指纹,同时返回匹配的ID号;返回0x09表示没搜索到。
这里有个性能问题:如果指纹库里存了很多指纹,搜索时间会变长。AS608的搜索速度大概是每枚指纹10ms左右,100枚指纹就是1秒。对于考勤机这种场景,1秒的等待是可以接受的,但如果你要做得更流畅,可以考虑把常用指纹放在前面,或者用1:N识别加1:1验证的组合方式。
代码实现上,我建议把指纹操作封装成几个独立函数:
uint8_t AS608_GenChar(uint8_t bufferId); uint8_t AS608_Search(uint8_t bufferId, uint16_t startId, uint16_t count, uint16_t *matchId); uint8_t AS608_Store(uint8_t bufferId, uint16_t id); uint8_t AS608_Delete(uint16_t id);每个函数内部负责发送命令、等待应答、解析结果。返回值统一用0x00表示成功,其他值表示错误码。这样上层业务逻辑就很清晰:如果AS608_Search返回0x00,就从matchId里取匹配的ID,然后执行开锁动作。
实操心得:AS608的串口波特率默认是57600,但你可以通过命令改成115200甚至更高。我实测115200下通信很稳定,搜索响应也更快。改波特率之后记得把STM32的串口配置也改掉,否则就通信不上了。
3.3 OLED界面设计与交互逻辑
OLED界面不需要花哨,但要清晰。我一般设计三个页面:待机页面、录入页面、识别结果页面。
待机页面显示“请按手指”和当前指纹库容量,比如“已录入:5枚”。录入页面显示当前录入步骤,比如“请按手指...”、“请再按一次...”、“录入成功!”。识别结果页面显示“识别成功 ID:1”或“识别失败”。
按键交互方面,PA0按键短按进入录入模式,长按3秒删除所有指纹。删除所有指纹的命令是0x05,参数是0x00 0x00 0x00 0x64(起始ID 0,数量100)。这个操作要加确认机制,不然误触就麻烦了。
OLED刷新频率不需要太高,200ms刷一次就够了。刷太快反而可能导致I2C总线繁忙,影响AS608的串口通信。我试过在中断里刷OLED,结果串口数据丢了,后来改成主循环里定时刷,就稳定了。
4. 调试过程中踩过的坑与排查技巧
4.1 串口通信不稳定的常见原因
第一个坑:串口接收丢数据。AS608返回的应答包有时候比较长,比如搜索成功的应答有十几字节。如果你用HAL_UART_Receive阻塞接收,指定长度不对就会超时或者丢包。我的做法是用DMA接收,配合空闲中断判断一帧结束。具体配置:开启USART1的DMA接收,缓冲区设64字节,在HAL_UARTEx_RxEventCallback里处理完整帧。
第二个坑:校验和计算错误。前面说过,校验和是从包标识开始累加,不包括包头和芯片地址。我见过有人从包头开始算,结果怎么都不对。还有一个细节:校验和是两个字节,高字节在前,低字节在后。如果你只发一个字节的校验和,模块会认为包不完整。
第三个坑:波特率不匹配。AS608出厂默认57600,但有些商家发货前会改成9600或115200。如果你发现发命令没反应,先用示波器或者逻辑分析仪看一下TX线上的波形,确认波特率。没有仪器的话,写个简单的串口回环测试,发什么收什么,确认STM32的串口配置是对的。
4.2 OLED点不亮的排查思路
OLED点不亮的原因太多了,我按概率从高到低列一下:
- 供电问题:3.3V有没有?电流够不够?OLED全亮时大概20mA,有些最小系统板的3.3V LDO带不动,电压会掉到2.8V以下,OLED就不亮了。用万用表量一下OLED的VCC引脚。
- I2C地址错误:0x78还是0x7A?写个I2C扫描程序,把总线上所有应答的地址打印出来。
- 初始化序列不对:不同批次的SSD1306初始化序列可能有差异。如果点不亮,试试通用的初始化序列,或者用卖家提供的例程。
- 接线错误:SCL和SDA接反了?这个错误太常见了,我至少犯过三次。I2C的SCL是时钟线,SDA是数据线,别搞混。
- 上拉电阻缺失:I2C总线需要上拉电阻,一般是4.7kΩ。有些模块自带,有些不带。用万用表量一下SCL和SDA对VCC的电阻,如果是无穷大,说明没上拉。
4.3 指纹识别率低的优化方法
识别率低通常不是模块的问题,而是使用方式的问题。我总结了几条经验:
第一,手指要干净。油污、水渍、灰尘都会影响光学采集。让用户在识别前擦一下手指,识别率能提升不少。
第二,按压力度要适中。太轻了采集不到足够的特征点,太重了手指变形也会影响。一般按到指纹纹路清晰可见就行。
第三,录入时多采几个角度。同一个手指,正面按一次,稍微左偏按一次,稍微右偏按一次,这样生成的模板适应性更强。AS608支持同一ID存储多个模板,你可以把三个角度的特征都存到同一个ID下。
第四,定期清理传感器表面。光学传感器表面容易积灰,用眼镜布擦一下就行。如果表面有划痕,识别率会明显下降,那就只能换模块了。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 发命令无应答 | 波特率不对、接线错误、模块未供电 | 检查TX/RX是否交叉、量模块VCC电压、试不同波特率 |
| 应答校验和错误 | 校验和计算范围不对 | 确认从包标识开始累加,高字节在前 |
| 指纹录入总是失败 | 手指太干/太湿、按压力度不对 | 让用户哈口气再按、调整按压位置 |
| OLED显示乱码 | 取模方式不对、I2C速率太高 | 确认取模设置为阴码逐列顺向、降低I2C速率到100kHz |
| 识别成功但继电器不动作 | GPIO配置错误、继电器驱动电路问题 | 量GPIO输出电平、检查继电器线圈电压 |
| 系统运行一段时间后死机 | 堆栈溢出、看门狗未喂 | 增大栈空间、检查HardFault、加独立看门狗 |
5. 项目扩展与进阶方向
5.1 加联网功能:ESP8266或ESP32做WiFi网关
指纹识别只是入口,数据上传才是价值。加一个ESP8266模块,通过AT指令跟STM32串口通信,把识别记录上传到服务器。STM32这边只需要在识别成功后,往另一个串口发一条JSON格式的记录,ESP8266负责转发。注意ESP8266的AT指令响应时间不确定,建议用状态机+超时重试的方式,不要阻塞主循环。
5.2 加FreeRTOS做多任务管理
如果项目功能变复杂,裸机的前后台架构会越来越难维护。上FreeRTOS之后,可以这样划分任务:
- Task_AS608:优先级最高,负责指纹模块的收发和状态机。
- Task_OLED:优先级中等,每200ms刷新一次显示。
- Task_Key:优先级中等,检测按键,发送事件到队列。
- Task_Logic:优先级最低,处理业务逻辑,比如识别成功后开锁、记录日志。
任务间用消息队列传递指纹ID和事件类型,用信号量保护共享资源(比如OLED显示缓冲区)。FreeRTOS在F103C8T6上跑完全没问题,20KB SRAM够用,但要注意每个任务的栈空间不要设太大,128字(512字节)起步,根据实际使用调整。
5.3 低功耗设计:电池供电场景
如果要做电池供电的指纹锁,低功耗就是刚需。F103C8T6有睡眠、停止、待机三种低功耗模式。指纹模块不工作时可以发命令让它进入休眠,OLED也可以关闭显示。STM32本身在停止模式下功耗大概20uA,唤醒源可以配置为按键中断或串口接收中断。
具体做法:主循环里如果没有任务,就调用__WFI()进入睡眠。AS608模块在空闲时发0x00命令进入休眠,需要识别时再发命令唤醒。OLED用命令0xAE关闭显示,0xAF打开显示。这样整机待机电流可以做到1mA以下,用2000mAh电池能撑好几个月。
5.4 指纹数据加密存储的考虑
如果项目涉及多用户指纹数据,安全就不能忽视。AS608模块内部存储的指纹模板是特征数据,不是原始图像,泄露风险相对较低。但如果你把指纹ID和用户信息存在STM32的Flash里,建议做简单的加密,比如AES-128。STM32F103C8T6没有硬件加密引擎,软件AES大概占2KB Flash,速度也够用。密钥可以存在芯片的选项字节里,或者用外部加密芯片。
注意:指纹数据属于个人生物特征信息,在项目落地时务必遵守相关数据保护规范,做好告知和授权,不要随意上传或共享原始数据。
6. 写在最后的一些个人体会
这个项目我从画原理图到跑通完整流程,大概花了一周时间,其中调试串口通信和OLED显示占了大半。最大的感受是:嵌入式开发里,硬件问题往往比软件问题更难查。软件错了可以单步调试,硬件错了只能靠示波器和万用表一点点量。
另外,AS608这个模块虽然资料多,但不同批次的固件版本有差异,有些命令的返回值可能跟手册不完全一致。遇到这种情况,别死磕手册,用串口助手发命令实测一下,以实际返回为准。我遇到过一批模块,搜索命令返回的ID号字节序跟手册相反,后来在代码里做了兼容处理才搞定。
最后说一句,指纹识别这个方向,技术本身已经很成熟了,难点不在算法,而在工程细节——怎么让用户按得舒服、怎么让识别率稳定、怎么让系统长时间运行不出问题。这些经验,只有真正动手做过才能积累下来。