简介:这份资源面向使用STM32微控制器驱动AS608指纹识别模块的嵌入式开发者,提供基于HAL库的驱动实现,帮助解决串口通信配置、模块初始化与指纹图像处理等实际问题。压缩包共2个文件,包含1个.c源文件与1个.h头文件,整体约6KB,分别承载驱动逻辑实现与接口声明,便于直接集成到STM32工程中。目前已有743人学习下载,适合正在做指纹识别、身份验证或安全控制类项目的初中级开发者参考。资源围绕HAL库的UART与SPI通信展开,涉及波特率、数据位、停止位与校验位等串口参数配置,以及GPIO引脚选择、中断服务程序处理收发事件等关键环节,同时覆盖AS608初始化命令序列、指纹图像采集、模板匹配与数据管理等核心功能。借助这份驱动代码,读者可以快速理解MCU与AS608之间的数据交换流程,对照模块手册完成通信协议对接,减少底层调试时间,为指纹识别功能的稳定集成提供可复用的实现基础。
1. 从寄存器到 HAL:为什么 AS608 指纹模块值得用 HAL 库重写一遍
如果你手头有一块 AS608 指纹模块,又刚好在用 STM32 做项目,大概率经历过这样的场景:网上找的例程全是标准库的,GPIO_InitStructure.GPIO_Pin那一套写得很熟,但项目已经迁到 CubeMX + HAL 上了,直接移植过去要么编译报错,要么跑起来收不到包。AS608 本身不复杂,串口发指令、收应答、解析包,但标准库和 HAL 库在串口收发、延时、中断处理上的写法差异,足够让一个下午搭进去。
这份「AS608-HAL库-西西菜鸟」资源,核心价值就在于把 AS608 的驱动从标准库思路完整搬到 HAL 框架下,用HAL_UART_Receive/HAL_UART_Transmit配合状态机收包,而不是靠死循环等标志位。它适合两类人:一类是刚上手 STM32 和指纹模块、想找个能直接跑的 HAL 例程的新手;另一类是手头有标准库代码、需要快速迁移到 HAL 的熟手。下面按「模块怎么通信 → 代码怎么落地 → 坑在哪 → 怎么验证」的顺序拆开讲。
2. AS608 通信协议与 HAL 串口收发的对接方式
2.1 AS608 的包结构到底长什么样
AS608 走的是 UART,默认波特率 57600,数据位 8、停止位 1、无校验。它所有指令和应答都遵循同一套包格式,理解这个格式是后面写解析代码的前提。
| 字段 | 长度 | 说明 |
|---|---|---|
| 包头 | 2 字节 | 固定 0xEF 0x01 |
| 芯片地址 | 4 字节 | 默认 0xFFFFFFFF |
| 包标识 | 1 字节 | 0x01 命令包,0x02 数据包,0x08 结束包 |
| 包长度 | 2 字节 | 从指令码到校验和的总字节数 |
| 指令码 | 1 字节 | 具体操作,如 0x01 录指纹 |
| 参数 | N 字节 | 随指令变化 |
| 校验和 | 2 字节 | 从包标识到参数末尾的累加和 |
校验和的计算方式是:从包标识开始,逐字节累加,取低 16 位。很多人第一次写解析时容易把包头也算进去,结果校验永远不过——这是最典型的翻车点。
2.2 HAL 串口收发的两种写法与选型
标准库时代常用USART_GetFlagStatus轮询,HAL 库对应的是HAL_UART_Receive阻塞式接收。但 AS608 的应答包长度不固定,你不知道对方会回多少字节,所以阻塞式接收必须配合超时和逐字节判断。
常见做法有两种:
第一种是纯阻塞式,用HAL_UART_Receive(&huart, buf, len, timeout)先收包头,再根据包长度字段收剩余部分。优点是逻辑直白,缺点是超时设不好会卡死主循环。
第二种是中断 + 环形缓冲,在HAL_UART_RxCpltCallback里逐字节存入缓冲区,主循环里跑状态机解析。优点是实时性好,缺点是代码量翻倍,新手容易在中断里做耗时操作导致丢包。
我一般建议先用第一种把功能跑通,确认模块和接线没问题,再决定要不要上中断。下面给的是阻塞式收包的完整思路。
2.3 收包状态机的实现
// as608_hal.c #include "as608_hal.h" // 接收缓冲区,AS608 单包最大约 256 字节 static uint8_t rx_buf[256]; /** * @brief 发送命令包并等待应答 * @param cmd 指令码 * @param param 参数指针,无参数传 NULL * @param param_len 参数长度 * @param out 应答数据输出缓冲区 * @param out_len 期望接收的应答长度 * @retval 0 成功,-1 超时,-2 校验失败 */ int as608_send_cmd(uint8_t cmd, uint8_t *param, uint16_t param_len, uint8_t *out, uint16_t out_len) { uint8_t tx_buf[64]; uint16_t idx = 0; uint16_t checksum = 0; // 包头 tx_buf[idx++] = 0xEF; tx_buf[idx++] = 0x01; // 芯片地址,默认全 F tx_buf[idx++] = 0xFF; tx_buf[idx++] = 0xFF; tx_buf[idx++] = 0xFF; tx_buf[idx++] = 0xFF; // 包标识:命令包 tx_buf[idx++] = 0x01; // 包长度 = 指令码(1) + 参数(param_len) + 校验和(2) uint16_t pkg_len = 1 + param_len + 2; tx_buf[idx++] = (pkg_len >> 8) & 0xFF; tx_buf[idx++] = pkg_len & 0xFF; // 指令码 tx_buf[idx++] = cmd; // 参数 for (uint16_t i = 0; i < param_len; i++) { tx_buf[idx++] = param[i]; } // 校验和:从包标识开始累加 for (uint16_t i = 6; i < idx; i++) { checksum += tx_buf[i]; } tx_buf[idx++] = (checksum >> 8) & 0xFF; tx_buf[idx++] = checksum & 0xFF; // 发送 if (HAL_UART_Transmit(&huart2, tx_buf, idx, 100) != HAL_OK) { return -1; } // 接收应答:先收 9 字节头部,拿到包长度后再收剩余 if (HAL_UART_Receive(&huart2, rx_buf, 9, 200) != HAL_OK) { return -1; } // 检查包头 if (rx_buf[0] != 0xEF || rx_buf[1] != 0x01) { return -2; } uint16_t resp_len = (rx_buf[7] << 8) | rx_buf[8]; // 剩余长度 = 包长度 - 指令码(1) - 校验和(2) uint16_t remain = resp_len - 3; if (remain > 0) { if (HAL_UART_Receive(&huart2, rx_buf + 9, remain, 200) != HAL_OK) { return -1; } } // 校验 checksum = 0; for (uint16_t i = 6; i < 9 + remain; i++) { checksum += rx_buf[i]; } uint16_t recv_sum = (rx_buf[9 + remain - 2] << 8) | rx_buf[9 + remain - 1]; if (checksum != recv_sum) { return -2; } // 拷贝应答数据 if (out && out_len > 0) { uint16_t copy_len = (remain < out_len) ? remain : out_len; for (uint16_t i = 0; i < copy_len; i++) { out[i] = rx_buf[9 + i]; } } return 0; }这段代码的关键点有三个。第一,发送时先拼包头和地址,再算包长度,最后算校验和,顺序不能乱。第二,接收时先固定收 9 字节,因为包头 2 字节 + 地址 4 字节 + 标识 1 字节 + 长度 2 字节正好 9 字节,拿到长度字段后再收剩余部分。第三,校验和的范围是从包标识(索引 6)开始,不是从包头开始,这一点和很多人的直觉相反。
参数说明:cmd是指令码,比如 0x01 是录指纹、0x02 是搜指纹、0x1D 是取模板数量。param和param_len对应指令参数,没有就传 NULL 和 0。out和out_len是调用方提供的缓冲区,用来接应答里的数据部分。返回值 -1 表示超时,-2 表示校验失败,这两个错误码在排查时很有用。
2.4 几个常用指令的封装
// 获取指纹模板数量 int as608_get_template_count(uint16_t *count) { uint8_t resp[4]; int ret = as608_send_cmd(0x1D, NULL, 0, resp, sizeof(resp)); if (ret != 0) return ret; // resp[0] 是确认码,0x00 表示成功 if (resp[0] != 0x00) return -3; *count = (resp[1] << 8) | resp[2]; return 0; } // 搜索指纹,返回匹配到的 ID int as608_search_finger(uint16_t *finger_id, uint16_t *score) { uint8_t resp[6]; int ret = as608_send_cmd(0x04, NULL, 0, resp, sizeof(resp)); if (ret != 0) return ret; if (resp[0] != 0x00) return -3; *finger_id = (resp[1] << 8) | resp[2]; *score = (resp[3] << 8) | resp[4]; return 0; }这两个封装展示了调用层的写法:先发指令,检查确认码,再从应答里按大端序取数据。确认码 0x00 表示成功,其他值对应不同错误,比如 0x02 是没搜到指纹,0x03 是录入失败。实际项目里建议把确认码做成枚举,方便日志输出。
3. 从 CubeMX 配置到指纹录入的完整落地步骤
3.1 CubeMX 里的串口配置
打开 CubeMX,选好芯片型号后,在 Connectivity 里启用一个 USART,模式选 Asynchronous。参数按 AS608 的要求设:Baud Rate 57600,Word Length 8 Bits,Parity None,Stop Bits 1。NVIC 里如果打算用中断接收就勾上全局中断,纯阻塞式可以不勾。
生成代码后,main.c里会自动出现MX_USART2_UART_Init(),确认波特率是 57600 而不是默认的 115200。这个细节很容易被忽略,因为 CubeMX 默认给的是 115200,不改的话模块完全不响应。
3.2 接线与供电注意
AS608 模块通常有 6 个引脚:VCC、GND、TX、RX、Touch、3.3V。TX 接 STM32 的 RX,RX 接 STM32 的 TX,交叉接。供电方面,模块标称 3.3V,但实际工作电流在识别瞬间能到 100mA 以上,如果直接用 STM32 板子上的 3.3V 稳压输出,可能会因为电流不够导致识别失败或复位。常见做法是单独给模块供 3.3V,或者确认板子的 LDO 能扛住 150mA 以上。
Touch 引脚是触摸感应输出,不接也能用,接的话可以在待机时唤醒主控。
3.3 录入指纹的完整流程
录入一枚指纹需要三步:第一次采集、第二次采集、合并存储。对应指令码 0x01、0x02、0x05。
// 录入指纹到指定 ID int as608_enroll(uint16_t id) { uint8_t param[5]; uint8_t resp[4]; int ret; // 第一步:采集第一次图像 param[0] = 0x01; // 缓冲区 1 ret = as608_send_cmd(0x01, param, 1, resp, sizeof(resp)); if (ret != 0 || resp[0] != 0x00) return -1; // 第二步:采集第二次图像 param[0] = 0x02; // 缓冲区 2 ret = as608_send_cmd(0x01, param, 1, resp, sizeof(resp)); if (ret != 0 || resp[0] != 0x00) return -1; // 第三步:合并两个缓冲区并存储到指定 ID param[0] = 0x01; // 缓冲区 1 param[1] = 0x02; // 缓冲区 2 param[2] = (id >> 8) & 0xFF; param[3] = id & 0xFF; ret = as608_send_cmd(0x05, param, 4, resp, sizeof(resp)); if (ret != 0 || resp[0] != 0x00) return -1; return 0; }参数说明:id是存储位置,范围 0 到 0x0FFF(取决于模块容量)。第一步和第二步的param[0]分别指定缓冲区 1 和 2,第三步把两个缓冲区合并后存入指定 ID。每一步都要检查确认码,任何一步返回非 0 都要重新开始,不能跳过。
3.4 主循环里的调用节奏
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART2_UART_Init(); uint16_t count = 0; if (as608_get_template_count(&count) == 0) { printf("当前模板数量: %d\r\n", count); } while (1) { uint16_t id, score; if (as608_search_finger(&id, &score) == 0) { printf("匹配到 ID: %d, 分数: %d\r\n", id, score); } HAL_Delay(500); } }主循环里每 500ms 搜一次指纹,搜到就打印 ID 和匹配分数。分数越高表示匹配度越好,一般 50 以上算可靠。这个节奏适合演示,实际产品里通常用触摸中断触发搜索,而不是一直轮询。
4. 避坑与排查:AS608 接 HAL 库最容易翻车的五个地方
4.1 校验和永远不过
现象:发送指令后模块有反应,但解析时校验失败,返回 -2。
原因:校验和的计算范围搞错了。AS608 的校验和是从包标识(第 7 个字节,索引 6)开始累加,不是从包头 0xEF 开始。很多人习惯性地把整个包都算进去,结果差了两个字节。
解决:确认累加起始索引是 6,结束索引是校验和之前一个字节。发送和接收用同一套逻辑,不要一边算一边不算。
4.2 收到包头但收不到完整包
现象:HAL_UART_Receive收到前 9 字节后,剩余部分超时。
原因:包长度字段解析错了,导致remain算出来偏大或偏小。包长度是从指令码到校验和的字节数,不包括包头、地址和标识。如果误把包头也算进去,remain就会多出 6 字节,接收自然超时。
解决:打印rx_buf[7]和rx_buf[8]看看实际长度值,对照指令手册确认。常见指令的包长度是固定的,比如搜指纹应答包长度是 0x0007。
4.3 波特率不匹配导致乱码
现象:串口助手能看到数据,但全是乱码,或者模块完全无应答。
原因:CubeMX 默认波特率是 115200,AS608 出厂默认是 57600。两者不一致时,收到的字节全是错的。
解决:在 CubeMX 里把波特率改成 57600,或者在代码里用HAL_UART_Init重新配置。如果模块之前被改过波特率,需要发指令恢复默认,具体指令查手册。
4.4 供电不足导致识别率低
现象:录入时前两步正常,第三步合并存储时报错,或者搜索时经常返回没搜到。
原因:AS608 在采集图像和存储时电流会瞬间拉高,如果供电不稳,模块内部可能复位或工作异常。
解决:用万用表量一下模块 VCC 和 GND 之间的电压,识别瞬间如果掉到 3.0V 以下,就要换供电方案。单独供 3.3V 或者加一个 100uF 以上的电容在模块电源脚附近。
4.5 中断接收里做耗时操作
现象:用中断方式接收时,偶尔丢包,或者程序跑飞。
原因:在HAL_UART_RxCpltCallback里调用了HAL_Delay或printf,导致中断处理时间过长,下一个字节来了没及时取走。
解决:中断回调里只做一件事——把字节存入环形缓冲区,然后立刻退出。解析和打印放到主循环里做。如果非要在中断里处理,确保没有阻塞调用。
5. 进阶验证:用状态机替代阻塞收包并做压力测试
阻塞式收包在单任务场景够用,但如果主循环里还有其他事情要做,比如刷屏幕、读传感器,200ms 的超时就会成为瓶颈。这时候需要把收包改成状态机,让串口中断驱动,主循环只负责解析。
思路是这样的:定义一个接收状态枚举,在HAL_UART_RxCpltCallback里逐字节推进状态。收到 0xEF 时进入包头状态,收到 0x01 确认包头,然后依次收地址、标识、长度,根据长度字段决定还要收多少字节,收完后置一个标志位,主循环检测到标志位就解析。
typedef enum { STATE_IDLE, STATE_HEADER1, STATE_HEADER2, STATE_ADDR, STATE_ID, STATE_LEN_H, STATE_LEN_L, STATE_DATA, STATE_DONE } rx_state_t; static rx_state_t rx_state = STATE_IDLE; static uint8_t rx_byte; static uint8_t pkt_buf[256]; static uint16_t pkt_idx = 0; static uint16_t pkt_remain = 0; static volatile uint8_t pkt_ready = 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance != USART2) return; switch (rx_state) { case STATE_IDLE: if (rx_byte == 0xEF) { pkt_buf[0] = rx_byte; pkt_idx = 1; rx_state = STATE_HEADER2; } break; case STATE_HEADER2: if (rx_byte == 0x01) { pkt_buf[1] = rx_byte; pkt_idx = 2; rx_state = STATE_ADDR; } else rx_state = STATE_IDLE; break; case STATE_ADDR: pkt_buf[pkt_idx++] = rx_byte; if (pkt_idx == 6) rx_state = STATE_ID; break; case STATE_ID: pkt_buf[pkt_idx++] = rx_byte; rx_state = STATE_LEN_H; break; case STATE_LEN_H: pkt_buf[pkt_idx++] = rx_byte; rx_state = STATE_LEN_L; break; case STATE_LEN_L: pkt_buf[pkt_idx++] = rx_byte; pkt_remain = ((pkt_buf[7] << 8) | pkt_buf[8]) - 1; // 减去指令码本身 rx_state = STATE_DATA; break; case STATE_DATA: pkt_buf[pkt_idx++] = rx_byte; if (--pkt_remain == 0) { pkt_ready = 1; rx_state = STATE_IDLE; } break; default: rx_state = STATE_IDLE; break; } HAL_UART_Receive_IT(&huart2, &rx_byte, 1); }这段状态机的关键点是:每次进中断只处理一个字节,处理完立刻重新挂上接收中断。pkt_remain在收到长度字段后计算,减去指令码占用的 1 字节,剩下的就是数据和校验和的总长。收满后置pkt_ready,主循环里检查这个标志,做校验和解析。
压力测试的做法:连续发 100 次搜指纹指令,统计成功率和平均耗时。如果成功率低于 95%,检查是不是中断优先级被其他外设抢占,或者缓冲区太小导致覆盖。我一般会把pkt_buf开到 256 字节,AS608 的包不会超过这个长度。
从那以后我每次接串口模块,都先用逻辑分析仪抓一遍波形确认波特率和电平,再写代码。这个习惯帮我省掉了至少三次「以为是代码问题、其实是硬件」的返工。希望帮到你。
本文还有配套的精品资源,点击获取