STM32F103与AS608指纹模块实战:串口协议、状态机与全流程代码
最近把吃灰的STM32F103C8T6最小系统板和AS608指纹模块翻出来重新调了一遍指纹录入、识别、删除这一整套流程终于跑通也顺手整理成了一套可以复用的代码。这套组合在国内做智能门锁、考勤机、保险柜相关课设和毕设的人里几乎是标配主控便宜、模块也便宜一个项目做下来串口通信、协议解析、状态机设计这些基本功全都能练到。如果你正准备做类似项目或者刚接触STM32想找一个“做完有成就感的实战”这篇文章非常适合你。我会把方案选型、硬件接线、AS608的串口协议、完整代码流程和排障心得都摊开讲清楚照着做基本能一次点亮。1. 项目设计思路为什么这套组合值得做1.1 主控选型不是越贵越好先说说为什么选STM32F103C8T6。这颗芯片是Cortex-M3内核最高主频72MHz片上Flash 64KB、SRAM 20KB价格在量产和零售端都压得极低一块最小系统板十几块钱就能买到。对于指纹锁这类项目来说算力需求其实不高因为指纹图像处理和特征匹配算法全部由指纹模块内部完成主控只需要“发指令、收结果、做交互”所以这种级别的MCU完全够用。还有一个很现实的原因资料多、生态成熟。从寄存器标准库到HAL库从Keil到IAR网上随便一搜就是大把例程。真要遇到问题你几乎不可能搜不到别人踩过的坑。另外如果你在做量产项目STM32F103C8T6的国产替代方案也非常成熟GD32F103C8T6、HK32F103C8T6之类的芯片可以直接替换这颗料串口、GPIO这部分完全兼容工程上基本能做到换芯片不改代码、重新编译一次就过。当然国产替代芯片在USB、ADC等外设上有些细节差异但本项目的USART应用不受影响。1.2 AS608 指纹模块把算法做成“黑盒”AS608是一个光学指纹模块内部集成了光路、传感器和一颗DSP指纹处理芯片。这颗DSP出厂就固化了完整的指纹算法包括图像采集、特征提取、模板合成、1:N搜索比对全部在模块内部完成。对于开发者来说它就是一个“指纹黑盒”你通过串口告诉它“采集指纹”“生成特征”“合成模板”“存储模板”它就把活干完再把结果通过串口回复给你。这也是这套方案最大的价值点你不用懂指纹识别算法却能在一天之内做出一个能实际运行的门锁原型。如果你去啃算法光是特征点提取和比对那一层就足够你研究大半年。AS608帮你把最难的算法层全部封装起来了你要做的核心工作就是会写串口指令、会解析应答帧、会设计一套状态机。目前市面上同类型的模块还有R307、ZW101等不少模块的指令协议和AS608基本同源都是那套0xEF01开头的帧格式。所以你把AS608调通了换其他模块的迁移成本很小顶多就是波特率和地址参数改一改。1.3 系统工作流程怎么设计整个项目的功能拆成三个模式录入模式、识别模式、删除模式用两个按键循环切换。录入模式是让用户按两次手指模块采集两次图像、生成同一枚指纹的模板并存入指定ID识别模式是按压一次手指模块从指纹库中搜索匹配返回匹配到的ID和得分删除模式是按ID删除单枚指纹或者对整个指纹库执行清空。显示交互方面最简单的方案是一块OLED屏幕加一个蜂鸣器OLED显示“请按手指”“录入成功”“验证通过”这些状态蜂鸣器做成功/失败的提示音。如果你手头只有LED也能凑合但OLED会让调试效率高不少串口打印当然也能做不过做成产品原型还是有屏幕更直观。状态机的核心思想是主循环里用一个状态变量记录当前模式按键触发模式切换每个模式下调用对应的AS608指令序列并把模块返回的错误码翻译成人话显示出来。这个结构很简单但拓展性很好后面要加FreeRTOS、加低功耗、加入网模块都不需要推翻重写。2. AS608 串口协议与指令细节2.1 串口参数与数据包格式先立一个前提AS608模块的UART默认参数绝大多数是57600bps、8位数据、1位停止位、无校验。但我必须提醒一句某些批次或者某些店铺定制的模块默认波特率是9600而且厂家一般会把模块背面贴一张小标签写明波特率。我手上这块贴的是“57600”如果你通信不上第一反应就去看标签而不是去怀疑代码。模块与MCU之间通讯采用的是固定帧格式每一帧都长这样2字节包头 6字节地址 1字节包标识 2字节包长度 N字节数据 2字节校验和。包头固定是0xEF 0x01。地址默认是0xFF 0xFF 0xFF 0xFF 0xFF 0xFF这是一级地址绝大多数场景不需要改。包标识有几种0x01是命令包就是MCU发给模块的指令0x02是数据包0x08是结束包用于上传下载指纹特征这种大数据传输的场景0x07是应答包是模块回复给MCU的结果。长度字段是高字节在前它表示的是“包标识之后、校验和之前”那部分数据的字节数也就是指令码加参数的总长度。举个例子你要发送“采集指纹”指令也就是GenImg指令码是0x01没有参数。那么长度字段就是0x00 0x01。整帧就是EF 01 FF FF FF FF FF FF FF 01 00 01 01 校验和高字节 校验和低字节。这个帧结构虽然简单但每个字节都不能错尤其是长度和校验和错了模块直接不回包。2.2 校验和怎么算校验和模块官方定义是从包标识到数据段最后一个字节的所有字节累加后取低16位。注意包头和地址不算也就是从包标识那一个字节开始累加。我直接用C代码写给你uint16_t checksum 0; uint16_t packageLen 1 paramLen; // 指令码 参数长度 checksum 0x01; // 包标识 checksum (packageLen 8) 0xFF; checksum packageLen 0xFF; checksum cmd; // 指令码 for (uint16_t i 0; i paramLen; i) { checksum param[i]; }校验和的两字节也是高字节在前。很多人在这一步翻车不是忘了把包标识算进去就是忘了加长度字段。校验和不正确模块收包后直接判为错误帧要么不回包要么回一个确认码0x01表示“收包出错”。2.3 核心指令与错误码AS608的常用指令其实就那几条建议先把下面这些背下来指令码名称功能参数说明0x01GenImg采集指纹图像无0x02Img2Tz将图像生成特征存到Buffer1字节BufferID取0x01或0x020x03RegModel合并Buffer1和Buffer2生成模板无0x04Store存储模板到Flash1字节BufferID 2字节PageID0x05Search在指纹库中搜索匹配1字节BufferID 2字节起始页 2字节搜索数量0x08DeleteChar删除模板2字节起始页 2字节数量0x09Empty清空整个指纹库8字节确认口令应答帧里确认码通常是2个字节0x00 0x00表示成功。完整的确认码含义如下表排查问题时要学会对着这个表看确认码含义0x00成功0x01数据包接收错误0x02传感器上没有手指0x03录入指纹图像失败0x04指纹太干或太淡0x05指纹太湿或太模糊0x06指纹图像过于杂乱0x07特征点太少0x08指纹不匹配0x09搜索不到指纹0x0A特征合并失败0x0B访问页号超范围0x0D指纹库已满0x0E写入Flash失败有一点我想多说两句不同批次的模块对个别错误码的定义会有细微差异所以我建议你收到模块后先把厂家给的协议手册保存好以手册为准。3. 硬件接线与工程配置3.1 接线表与供电注意接线是所有工作的基础AS608和STM32F103C8T6之间只需要四根线AS608引脚连接目标说明VCC3.3V或5V按模块丝印AS608常见为3.3V供电个别版本自带稳压可接5VGNDGND必须与STM32共地TXDPA10USART1_RX模块发送单片机接收RXDPA9USART1_TX单片机发送模块接收串口连接必须交叉这是个老生常谈但还是很多人犯的错误模块的TX要接单片机的RX模块的RX要接单片机的TX我在调试时亲眼见过有人把两根线接成直连结果收发都不通还以为是波特率错了。供电这一点值得单独强调。AS608工作时电流并不小指纹头内部有LED光源和传感器工作电流大致在60mA左右瞬间峰值可能更高。如果你的STM32最小系统板是用USB口供电再额外从板子的3.3V引脚引电给AS608有时候会因为板上LDO电流不够、纹波太大导致指纹采集异常。比较稳妥的做法是AS608的VCC独立走一组电源比如主板5V经过单独的AMS1117-3.3转出来给它供电或者直接用质量好一点的USB线给开发板供电然后把模块VCC接在板子的5V引脚前提是你的模块支持5V输入。3.2 工程环境与下载方式开发环境用Keil MDK最多IAR也行HAL库和标准库都能完成这个项目。我这次用的是Keil MDK 5 HAL库原因是HAL库的串口收发封装比较完整代码量少适合快速验证。如果你之前跟的是标准库教程也没问题逻辑完全相同只是函数名不一样。下载调试推荐用DAP-Link也就是网上很常见的“ARM DAPLink”下载器SWD四根线就能搞定SWDIO、SWCLK、GND、3V3。ST-Link也同样可以。如果你做的是量产门锁固件还想保护一下Flash里的程序可以在Keil里配置Option Bytes把读保护级别设到Level 1这样别人通过SWD只能整体擦除不能直接把固件读出来。STM32F103C8T6的读保护功能是硬件自带的纯软件层面无法完全防止被读这个小细节适合有产品化需求的场景课设阶段就不用折腾了。3.3 指纹模块的安装与稳定性细节指纹头的排线是个隐蔽的坑。AS608模块的传感器和主控板之间通常有一根软排线出厂时可能没插到底或者运输过程中松了。如果你发现模块上电后串口能通但采集图像总是失败先别急着改代码把排线拔下来重新插一下听到卡扣“咔哒”一声才算到位。指纹识别对电源纹波和静电比较敏感我自己的习惯是在模块VCC和GND之间就近焊一颗100uF电解电容再并联一颗0.1uF陶瓷电容实测下来图像采集成功率会高不少。模块的指纹窗口也要保持干净手上沾了油污再按上去生成的特征明显会差指纹头表面脏了用软布蘸少量酒精轻轻擦不要用硬物刮。4. 核心代码实现录入、识别、删除全流程4.1 串口指令收发封装代码层面的第一步是封装发送命令和接收应答的函数。发送函数的核心就是按帧格式组包然后通过串口一次性发出去。我用HAL库做了这样一个封装void AS608_SendCmd(uint8_t cmd, uint8_t *param, uint16_t paramLen) { uint8_t buf[64]; uint16_t idx 0; uint16_t packageLen 1 paramLen; uint16_t sum 0; buf[idx] 0xEF; // 包头 buf[idx] 0x01; for (uint8_t i 0; i 6; i) buf[idx] 0xFF; // 地址 buf[idx] 0x01; // 命令包标识 buf[idx] (packageLen 8) 0xFF; buf[idx] packageLen 0xFF; buf[idx] cmd; // 指令码 sum 0x01; sum (packageLen 8) 0xFF; sum packageLen 0xFF; sum cmd; for (uint16_t i 0; i paramLen; i) { buf[idx] param[i]; sum param[i]; } buf[idx] (sum 8) 0xFF; buf[idx] sum 0xFF; HAL_UART_Transmit(huart1, buf, idx, 200); }接收应答的时候要注意应答帧的长度不是固定的。比如“搜索指纹”成功时应答帧除了2字节确认码还会带2字节页码、2字节得分总数据长度是6字节而普通指令的应答只有2字节确认码。所以接收函数必须先读固定长度的帧头解析出长度字段再根据长度收剩下的字节uint8_t AS608_WaitResponse(uint8_t *respData, uint16_t timeout) { uint8_t header[11]; uint8_t buf[64]; uint16_t dataLen, sum 0; if (HAL_UART_Receive(huart1, header, 11, timeout) ! HAL_OK) return 0; // 简单校验包头、包标识 if (header[0] ! 0xEF || header[1] ! 0x01) return 0; if (header[8] ! 0x07) return 0; dataLen (header[9] 8) | header[10]; if (dataLen 60) return 0; if (HAL_UART_Receive(huart1, buf, dataLen 2, timeout) ! HAL_OK) return 0; // 累加校验从包标识到数据段 sum 0x07; sum header[9]; sum header[10]; for (uint16_t i 0; i dataLen 2; i) sum buf[i]; // 这里的低16位如果为0表示校验通过 // 把确认码和数据拷贝给上层使用 if (respData ! NULL) { for (uint16_t i 0; i dataLen; i) respData[i] buf[i]; } return 1; }这里有个调试经验HAL库的阻塞接收在模块一直没有响应时会一直卡到timeout结束所以timeout参数要根据场景设置合理值。采集指纹图像那一步用户可以按压的时间本来就是一秒到几秒timeout设个3000ms比较合理而删除模板这种指令模块很快就能回timeout设500ms就够。4.2 指纹录入前端控制逻辑录入指纹的标准流程要按顺序走一遍采集图像、生成特征存入Buffer1、提示用户再次按压、再采集图像、生成特征存入Buffer2、合成模板、存储模板。代码逻辑上建议每一步都检查确认码并用返回值精确定位失败在哪一步。uint8_t AS608_Enroll(uint16_t pageID) { uint8_t param[6]; uint8_t resp[8]; // 第1步采集第一次指纹图像 if (!AS608_SendCmdAndWait(0x01, NULL, 0, resp, 3000)) return 1; if (resp[0] 0x02) return 2; // 没有检测到手指 if (resp[0] ! 0x00) return 3; // 采集失败 // 第2步生成特征存入Buffer1 param[0] 0x01; if (!AS608_SendCmdAndWait(0x02, param, 1, resp, 1000)) return 4; if (resp[0] ! 0x00) return 5; // 提示用户移开再按一次 AS608_ShowMessage(fingeroff); HAL_Delay(300); // 第3步采集第二次指纹图像 if (!AS608_SendCmdAndWait(0x01, NULL, 0, resp, 3000)) return 6; if (resp[0] 0x02) return 7; if (resp[0] ! 0x00) return 8; // 第4步生成特征存入Buffer2 param[0] 0x02; if (!AS608_SendCmdAndWait(0x02, param, 1, resp, 1000)) return 9; if (resp[0] ! 0x00) return 10; // 第5步合并模板 if (!AS608_SendCmdAndWait(0x03, NULL, 0, resp, 1000)) return 11; if (resp[0] ! 0x00) return 12; // 第6步存储到指定ID param[0] 0x01; param[1] (pageID 8) 0xFF; param[2] pageID 0xFF; if (!AS608_SendCmdAndWait(0x04, param, 3, resp, 1000)) return 13; if (resp[0] ! 0x00) return 14; return 0; // 录入成功 }这里面我重点解释一下Buffer1和Buffer2的设计。模块内部有两个特征缓冲区录入时必须两次采集同一个指纹分别生成特征存进两个Buffer然后再用RegModel指令把两份特征点融合成一份更完整的模板。为什么一定要这张做因为单次按压采集到的特征点有限指纹边缘、按压力度、角度都会影响特征点的完整性。两次取特征再合并可以显著提高后续识别时的通过率。这也是为什么有些项目里“按压一次就录入”的做法是不规范的录入模板的质量直接决定后面的识别率。4.3 指纹识别与分数阈值识别流程比录入简单核心就三步采集图像、生成特征到Buffer1、Search搜索整个指纹库。uint8_t AS608_Identify(uint16_t *matchedID, uint16_t *score) { uint8_t param[5]; uint8_t resp[8]; if (!AS608_SendCmdAndWait(0x01, NULL, 0, resp, 3000)) return 1; if (resp[0] 0x02) return 2; // 没手指 if (resp[0] ! 0x00) return 3; param[0] 0x01; if (!AS608_SendCmdAndWait(0x02, param, 1, resp, 1000)) return 4; if (resp[0] ! 0x00) return 5; param[0] 0x01; // 用Buffer1搜索 param[1] 0x00; // 起始页高字节 param[2] 0x00; // 起始页低字节 param[3] 0x03; // 搜索数量高字节 param[4] 0xFF; // 搜索数量低字节 if (!AS608_SendCmdAndWait(0x05, param, 5, resp, 1500)) return 6; if (resp[0] ! 0x00) return 7; // 没匹配到 if (matchedID ! NULL) *matchedID (resp[2] 8) | resp[3]; // 匹配上的页号 if (score ! NULL) *score (resp[4] 8) | resp[5]; // 匹配得分 return 0; }Search指令的搜索数量我传的是0x03FF覆盖范围大于模块常见容量300枚所以能搜到全部指纹。AS608不同容量版本的差异主要在Flash大小和最大存储指纹数你可以在模块手册里确认容量然后把这个值改成实际容量值。还有个细节是匹配分数。模块返回的score值反映匹配程度分值越高说明越像。不同模块固件对分数口径可能不同但通常80以上就比较可靠了。你要是发现识别误判太高可以把阈值调高一些反过来识别太严导致老是拒识就把阈值调低一点。这个值要作为宏定义放在程序开头方便现场调。4.4 删除模板与清空指纹库删除单枚指纹的指令是DeleteChar参数是起始页号和要删除的数量。比如你要删除ID为5的指纹uint8_t AS608_DeleteByID(uint16_t pageID) { uint8_t param[4]; param[0] (pageID 8) 0xFF; param[1] pageID 0xFF; param[2] 0x00; // 删除数量高字节 param[3] 0x01; // 删除1枚 return AS608_SendCmdAndWait(0x08, param, 4, resp, 1000); }清空整个指纹库是Empty指令它需要带一个8字节的确认口令防止软件误操作把所有指纹全删了。这个口令固定是0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01少一个字节或者多一个字节模块都不会执行。我建议在UI交互上加一个“长按3秒确认清空”的逻辑避免误触发。4.5 主状态机与按键交互主程序用两个按键进入不同模式参考下面的伪代码结构while (1) { switch (mode) { case MODE_IDLE: if (KEY1_PRESSED) mode MODE_ENROLL; if (KEY2_PRESSED) mode MODE_IDENTIFY; break; case MODE_ENROLL: // 简单引导流程选择ID - 录两次手指 break; case MODE_IDENTIFY: AS608_Identify(id, score); if (id有效 score 80) OPEN_LOCK(); mode MODE_IDLE; break; case MODE_DELETE: // 选择要删除的ID执行DeleteByID break; } }这是一个最简单的裸机状态机。我再多说一句如果后面按键逻辑变复杂或者想加入超时自动回待机、低功耗休眠建议把状态机抽成函数指针数组甚至直接移植FreeRTOS把每个模式做成独立任务。STM32F103C8T6的20KB SRAM跑一个精简版FreeRTOS完全没问题串口、按键、指纹模块各占一个任务代码会清爽很多。5. 调试验证与常见问题实录5.1 串口通信异常的排查方向遇到指令发出去模块没反应先按这个顺序查第一波特率。看模块标签如果贴的是9600就把初始化改成9600别迷信“AS608默认57600”这句话。第二TX/RX是否交叉。用万用表量一下模块TXD引脚有没有电平变化发指令时用示波器或者逻辑分析仪抓一下波形最直观。第三共地。模块和MCU不共地串口电平没有参考基准通信必然失败这个问题在飞线连接时非常容易犯。第四电源电流。如果模块指示灯一亮一灭、图像采集时模块重启基本就是供电不足换独立供电。还有个常见问题是串口粘包。因为AS608应答帧是连续到达的如果接收时没有按长度字段精确读取或者上一帧数据没清空下一帧就解析错位。我习惯每次发送指令前清空一下串口接收缓冲或者用空闲中断DMA接收效果会稳定很多。5.2 录入和识别失败怎么定位录入失败时首先要会看错误码。返回0x04说明手指太干可以搓一搓手再试返回0x05说明手指太湿把手擦干返回0x06是图像杂乱往往是按压时手指滑动或者指纹头表面脏了返回0x07是特征点太少换一个纹理更清晰的手指或者让手指按下去时稍微转动一下让更多的脊线被采集到。录入多次失败还要确认指纹库是不是满了如果模块容量300枚存了300枚后再录入就会返回0x0D。识别阶段最常见的问题是明明录入成功识别时却匹配不上。这时候先别怀疑模块坏了大概率是录入模板质量太差建议删掉重新录并且录入时让用户分别用“正中按压”“稍微偏左”“稍微偏右”按三次模块会从三次图像中提取更丰富的特征点。对于老人和小孩的手指指纹纹理浅录一次两次往往不够可以多录几次不同的按压角度。5.3 识别稳定性优化心得我实测下来把模块的采集超时设置和用户交互节奏配合好识别体验会明显提升。比如第一次GenImg失败后如果是因为没有手指UI要先提示“请按压手指”然后延时再重试不要一口气发几十条指令把串口刷爆。AS608模块内部处理图像也需要时间指令之间保留至少几十毫秒的间隔响应会更稳定。分数阈值这块我再展开一点在实际门锁项目里安全性和便利性是矛盾的。阈值太高指纹稍微出点汗就被拒阈值太低不同手指之间误识别率上升。项目调试阶段先用默认值跑一周记录实际得分分布再根据需求调整。比如家用场景我一般建议阈值在50到80之间既要防误开也要保证老人小孩能被识别。5.4 关于购买模块和固件版本的建议AS608这个型号被做成了各种“定制版”有的带蓝色背光有的不带有的内置了触摸感应输出引脚有的没有。这些都不影响基本功能但要注意别买到拆机件。拆机指纹头最典型的问题是图像传感器老化录入时总报“图像杂乱”换新模块立马正常。另外一个判断标准是看排线和焊点全新件的排线插座干净焊点光亮拆机件往往有拆焊痕迹。如果你手头的模块是ZW101或者其他同协议兼容模块代码几乎不用改顶多调整一下波特率和搜索数量。有些模块固件版本较老对某些指令的应答慢一点timeout时间要给足不要一超时就判失败。如果你决定把这个项目产品化还有几个点值得加进去指纹库剩余容量显示、多次识别失败后的锁定延时、掉电记忆模块地址、通过Boot引脚做固件升级。这些功能都不难但能把一个“课设级别”的门锁变成“可用级别”的产品。最后再说一个我自己的体会。整套系统调通之后建议你故意把电源线接长一点、用快充充电器给系统供电、在指纹头旁边放一个电机启动的大负载分别观察识别率的变化。你会直观地感受到电源质量对指纹识别的影响有多大。这个项目最值钱的部分不是那几行串口代码而是你通过这些调试过程建立起来的“系统稳定性意识”。后面无论你是去做智能锁产品还是转去做其他传感器采集类项目这套排查思路都能直接迁移过去。这个简单的入门项目可能改变你未来做嵌入式工程的很多习惯。