STM32指纹密码锁开发实战:AS608模块对接、GSM报警与矩阵键盘设计
2026/9/16 1:09:06 网站建设 项目流程

简介:这套STM32指纹密码锁程序是一套完整的嵌入式安全系统方案,主要面向智能锁开发者、电子设计竞赛学生以及物联网爱好者,将AS608指纹识别、GSM远程报警、密码管理与蜂鸣器本地告警集于一体,适用于家庭门禁、办公安防等场景。资源包共101个文件,以44个.h头文件和43个.c源文件为主,对应STM32标准外设库驱动、AS608指纹通信、GSM模块AT指令控制等核心代码,另有工程配置与编译脚本,整体仅339KB,轻量便于直接移植。目前已有576人学习下载,适合用于STM32项目实战与课程设计参考。代码基于Keil工程组织,模块划分清晰,完整呈现了从指纹录入、密码校验、异常触发到短信报警的整个链路,用户拿到后既可烧录到开发板验证功能,也可参照其状态机与中断设计扩展自己的智能锁应用,综合了解生物识别与远程告警的嵌入式实现方式。

1. 一套 STM32 指纹密码锁的完整方案长什么样

把 AS608 指纹模块、矩阵键盘、GSM 短信报警和电磁锁控制塞进一块 STM32 里,是这个项目标题背后的真实需求:门禁不只认指纹,还得留密码后路,异常时得能拨号报警。这类系统在实验室门禁、宿舍柜锁、毕业设计展品里是最常见的一套组合,难点不在单个模块,而在把三个外设的时序、电源、错误处理整合进一个主循环里。

AS608 负责指纹录入与比对,矩阵键盘承担密码输入,GSM 模块(常见 SIM800C / SIM900A 系列)在连续验证失败或锁体被撬时发短信给预设号码。STM32 在这里的角色不是算力担当,而是"调度中枢":串口 1 接指纹、串口 2 接 GSM、GPIO 扫描键盘、继电器控制锁舌。程序规模不大,但牵扯到串口分包、Flash 存储、AT 指令时序这三类基本功,恰好是嵌入式岗笔试和面试最爱考的东西。

这篇不照着原工程一行行读,而是把"如果你要从零搭一套"的完整路径讲清楚:AS608 怎么发送指令、密码存哪、GSM 怎样发短信、三个外设怎么在一个循环里和谐共处、现场调试盯哪些点。适合正在调 AS608 但协议不熟的人,也适合想抄一套健壮状态机做门禁项目的工程师。

2. AS608 指纹模块与 STM32 的串口协议对接

2.1 AS608 的指令包结构

AS608 用的是自定义串口协议,不是 Modbus,也不是简单的"发 0x01 返回 0x00"。它要求设备地址 + 包标识 + 长度 + 指令 + 参数 + 校验和,并且校验和是"从包标识异或到最后一个参数"。初次调这个模块的人,十有八九是栽在校验和算错或应答帧长度解析错上。

指令包格式固定为:包头F5 55 AA,命令码占 1 字节,随后是参数,最后 1 字节是校验和(从包标识异或到参数末尾)。应答帧结构相同,但中间夹着确认码00表示成功,01表示收包错误,0B表示指纹库已满这类语义。

下面这段是发送一条完整指令并接收应答的最小 C 实现,按 AS608 手册的通用格式封装:

#define AS608_HEAD1 0xF5 #define AS608_HEAD2 0x55 #define AS608_HEAD3 0xAA #define AS608_ADDR 0xFFFFFFFF uint8_t as608_send_cmd(uint8_t cmd, uint8_t *params, uint8_t len) { uint8_t buf[32]; uint8_t sum = 0, i; uint8_t resp[16]; uint16_t timeout = 500; buf[0] = AS608_HEAD1; buf[1] = AS608_HEAD2; buf[2] = AS608_HEAD3; buf[3] = 0x01; // 包标识,命令包固定 0x01 buf[4] = (len + 3) & 0xFF; // 长度 = 命令码 + 参数 + 校验和 buf[5] = cmd; sum = buf[3] ^ buf[4] ^ buf[5]; // 校验起始:包标识异或到命令码 for (i = 0; i < len; i++) { buf[6 + i] = params[i]; sum ^= params[i]; } buf[6 + len] = sum; uart_dma_send(USART1, buf, 7 + len); // 等待应答帧,这里用超时接收 if (uart_recv_timeout(USART1, resp, 12, &timeout) != 0) { return 0xFF; // 超时,模块没响应 } return resp[9]; // 应答帧第 10 字节是确认码 }

广播地址0xFFFFFFFF在代码里没有参与报文拼接,因为 AS608 默认帧格式里地址被省略,实际是"无地址模式",所以包标识后直接是长度。长度字段计算的是从命令码到校验和的字节数,也就是len + 3。确认码的位置需要拿逻辑分析仪抓一帧真实应答确认,不同批次模块的应答帧"确认码在第几个字节"略有差异,这是踩坑点——建议上电后先发0x01读版本号,打印整帧 12 个字节十六进制值,再定位偏移。

2.2 指纹录入:六步指令序列

指纹录入不是发一条指令就完事,而是六步操作:收集图像、生成特征、生成模板、存储模板、再次收集图像、合并模板。AS608 的固件把中间状态保存在模块内部缓冲区,MCU 侧看到的是一串状态机跳转。

指令码含义参数典型应答
0x01读版本号确认码 + 版本字符串
0x02收集指纹图像00 成功 / 01 收包错误
0x05生成特征(存缓冲区)BufferID:1 或 200 成功 / 0B 指纹太干或太湿
0x06生成模板BufferID 1, 200 成功 / 0B 合并失败
0x0A存储模板BufferID, PageID00 成功 / 0B 库满
0x0B删除模板PageID, 数量00 成功 / 0B 地址错误
0x0C清空指纹库00 成功
0x13搜索指纹BufferID, 起始页, 页数00 + 页码 + 得分

录入流程代码示意:

uint8_t enroll_finger(uint16_t page_id) { uint8_t p1[2]; if (as608_send_cmd(0x02, NULL, 0) != 0x00) return 1; // 按第一次手指 if (as608_send_cmd(0x05, (uint8_t[]){0x01}, 1) != 0x00) return 2; // 生成特征到缓冲区1 delay_ms(800); // 提示换手指 if (as608_send_cmd(0x02, NULL, 0) != 0x00) return 3; // 按第二次手指 if (as608_send_cmd(0x05, (uint8_t[]){0x02}, 1) != 0x00) return 4; // 生成特征到缓冲区2 if (as608_send_cmd(0x06, NULL, 0) != 0x00) return 5; // 合成模板 p1[0] = 0x01; // 模板存在缓冲区1 p1[1] = page_id & 0xFF; if (as608_send_cmd(0x0A, p1, 2) != 0x00) return 6; // 存入指纹库 page_id return 0; }

参数数组里BufferID固定 1 或 2,PageID是指纹库的槽位号,0~299 是常用范围。这里有个容易忽略的细节:0x05的第二个参数"重试次数"在很多手册版本里存在,但实际固件不一定解析,发送时只传 BufferID 反而最稳。如果第二次按手指时模块在 5 秒内没等到指纹图像,0x05会返回超时错误码,代码里需要给操作者明确的"请重新按"提示,不能直接当失败结束。

2.3 指纹搜索与识别状态机

识别比录入简单:采集图像 -> 生成特征 -> 搜索。搜索是0x13,参数里带起始页和搜索页数,通常填 0 和 300 表示全库搜索。

uint16_t search_finger(void) { uint8_t params[4]; if (as608_send_cmd(0x02, NULL, 0) != 0x00) return 0xFFFF; // 没按手指 if (as608_send_cmd(0x05, (uint8_t[]){0x01}, 1) != 0x00) return 0xFFFF; params[0] = 0x01; // 用缓冲区1的特征搜索 params[1] = 0x00; // 起始页 params[2] = 0x01; // 页数高字节 params[3] = 0x2C; // 300 页(0x012C) if (as608_send_cmd(0x13, params, 4) == 0x00) { // 确认码后面两字节是找到的 PageID return read_resp_page_id(); } return 0xFFFF; }

建议把识别流程放到定时器中断里做"非阻塞轮询":主循环每 200ms 发一次0x02采集指令,模块收到后会自动检测手指按压。这样手指放上去不用等软件触发,体验接近"即按即开"。搜索耗时取决于库容量,300 枚全库搜大约 500ms,期间不能发其他 AS608 指令,否则模块会回错误包。库超过 100 枚时,识别率会因特征碰撞下降,这时候应该按"分组搜索"来优化,而不是一味加 PageID。

3. 密码管理:矩阵键盘输入、EEPROM 存储与防暴力尝试

3.1 密码存哪:内部 Flash 模拟 EEPROM

指纹库在 AS608 内部,但密码得自己存。常见做法三段选一:外挂 AT24C02、用 STM32 内部 Flash 的最后两页模拟 EEPROM、直接写死在代码里。写死最省事但没法改,AT24C02 要多拉一根 I2C,内部 Flash 方案零成本且断电不丢,中容量型号(STM32F103RCT6)的最后 1MB 空间擦写寿命约 1 万次,对门锁这种不频繁改密的场景绰绰有余。

内部 Flash 写入需要先擦除整页再写,且写入过程中不能有中断,所以要把存密码封装成一个关中断函数:

#define PASSWORD_ADDR 0x080FF800 // 最后一页起始地址 #define PASSWORD_LEN 6 // 6 位密码,BCD 码存储 void save_password(uint8_t *pwd) { uint32_t temp[8]; uint8_t i; for (i = 0; i < 6; i++) { temp[i] = pwd[i]; } __disable_irq(); FLASH_Unlock(); FLASH_ErasePage(PASSWORD_ADDR); for (i = 0; i < 6; i++) { FLASH_ProgramHalfWord(PASSWORD_ADDR + i * 2, temp[i]); } FLASH_Lock(); __enable_irq(); }

Flash 页大小对不同型号不一样,F103 是 1KB 或 2KB 一页,地址要对齐到页边界。这里用半字写入,读出时直接*(uint8_t*)PASSWORD_ADDR按字节取。存的不是 ASC 码而是 BCD 码,键盘10x01,省空间也方便比对。密码校验成功后建议把 Flag 位 "已修改密码" 顺便写进同一页,这样重启后能区分是首次上电还是已初始化,避免每次开机都强制回默认密码123456

Flash 模拟 EEPROM 有个替代方案:直接外挂 AT24C02。如果你不差那两块钱,AT24C02 更省心,读写都是字节级操作,还不用关中断。选型逻辑很简单——批量生产选 Flash 方案省物料,自己调试用 AT24C02 省头发。

3.2 矩阵键盘扫描与输入状态机

4x4 矩阵键盘的扫描用行列反转法:先所有行输出低、读列,有按键时得到列号;再所有列输出低、读行,得到行号。关键在去抖——机械键盘按下瞬间会有 5~20ms 的抖动,扫描间隔 20ms 读两次一致才算有效。

uint8_t key_scan(void) { uint8_t col, row, key = 0xFF; uint16_t t1, t2; // 行输出低,列上拉输入 set_rows_output_low(); col = read_cols(); if (col == 0xFF) return 0xFF; // 无按键 // 第一次读出列,延时去抖 delay_ms(20); t1 = read_cols(); if (t1 != col) return 0xFF; // 反转:列输出低,行上拉输入 set_cols_output_low(); row = read_rows(); delay_ms(20); t2 = read_rows(); if (t2 != row) return 0xFF; key = decode_key(row, col); // 查表映射到 0~15 // 等待按键释放,防止连发 while (read_cols() != 0xFF || read_rows() != 0xFF); return key; }

释放检测一定要做,否则一次按键会被执行好几次。输入密码时用状态机:空闲、输入中、校验、已开锁。输入中每按一位存进缓冲区,按#确认,按*退格。超时 10 秒没按完,自动清空缓冲区回到空闲,这个超时细节是决定体感的关键——很多人直接调阻塞式while等按键,导致指纹验证和密码输入互相卡死。

正确做法是把键盘扫描放进 10ms 定时中断,主循环只消费"按键事件队列"。这样指纹搜索的 500ms 阻塞期间,密码输入也不会丢键。

3.3 防暴力尝试:错误锁定与掉电保持

密码锁最怕穷举。6 位数密码全空间 100 万次,不做限制的话脚本跑一夜就开了。常见策略:连续错 3 次锁 30 秒,错 5 次触发 GSM 报警。锁定状态要存 Flash,否则断电重上电锁定就失效——这等于没锁。

uint8_t check_pwd_lock(void) { uint8_t err_cnt; uint32_t lock_until; err_cnt = read_flash_byte(ERR_CNT_ADDR); lock_until = read_flash_word(LOCK_TIME_ADDR); if (err_cnt >= 5) { if (get_rtc_sec() < lock_until) return 1; // 仍在锁定期 else { clear_lock_flags(); // 到期自动解锁 return 0; } } return 0; }

锁定期按 RTC 秒数算,如果板子上没有 RTC 芯片,用系统定时器的 tick 累计也行,但断电后 tick 归零,锁定时间就得重新从 Flash 读 "锁定时刻 + 锁定秒数" 来推算。更稳妥的做法是掉电前把锁定到期时间算好写进 Flash,而不是只存"锁定标记"。还要注意 Flash 写入次数:错误计数每错一次就写一次 Flash,暴力尝试时可能在几分钟内把这一页 Flash 写穿,所以错误计数只存 RAM,等到状态变化(比如触发了锁定)才落到 Flash。

4. GSM 报警链路:SIM800C 短信触发与 STM32 状态联动

4.1 SIM800C 的接线与串口时序

SIM800C 是 2G 模块,2G 退网后部分区域搜不到网,这是选型时要先确认的点。模块串口电平是 2.8V,直接接 STM32 的 3.3V TX 可能烧模块,稳妥做法是串一个 1K 电阻分压,STM32 的 RX 接模块 TX 则可以直接连,因为模块 2.8V 输出高于 STM32 的低电平阈值。

模块上电后不能立刻发 AT 指令——它要等网络注册,冷启动最慢能到 5~10 秒。初始化序列按下表逐步来,每步都要等到明确应答再走下一步:

指令预期应答说明
ATOK串口通断检测,发 3 次没回应查电平
AT+CPIN?+CPIN: READYSIM 卡识别
AT+CSQ+CSQ: 15,0信号强度,第一个数大于 10 才稳
AT+CMGF=1OK切文本模式
AT+CSCS="GSM"OK字符集,中文短信需要切 UCS2

信号强度+CSQ: 99,0表示无信号,这种情况发短信必失败,要在代码里打印出来而不是闷头重试。GSM 模块在发短信时电流峰值能到 2A,电源必须用独立 LDO 或 DC-DC 供电,和 STM32 共用 3.3V 会直接把单片机拉复位——这是现场最多发的故障。

4.2 AT 指令拼接与短信发送

发送短信是拼字符串的过程,核心是AT+CMGS="13800138000"\r\n后跟短信内容,再发0x1A结束。STM32 侧用 sprintf 拼好号码和内容,经 UART2 发出:

void send_sms(const char *phone, const char *msg) { char cmd[64]; uint16_t len; sprintf(cmd, "AT+CMGS=\"%s\"\r\n", phone); uart_send_string(UART2, cmd); delay_ms(300); // 等模块返回 '>' uart_send_string(UART2, msg); uart_send_string(UART2, "\r\n"); uart_send_byte(UART2, 0x1A); // Ctrl+Z 结束 // 等待 +CMGS: xxx 应答,超时 10s wait_sms_ack(10000); }

delay_ms(300)是等模块返回>提示符,不同固件版本等待时间不同,保守起见串口收到>字符再发内容更稳。短信内容如果含中文,要先AT+CSCS="UCS2"然后内容转 UCS2 十六进制编码,每个汉字占 4 个字符。报警短信一般建议纯英文字母和数字,省去编码转换的麻烦,比如ALARM: DOOR OPEN!。反正门卫看号码就能知道是哪扇门。

主循环里发短信必须加"正在发送"标志位,不能在短信发送过程中再去读 AS608 或键盘,串口 DMA 缓冲会互相覆盖。更规范的做法是开一个发送状态机:IDLE -> SENDING -> WAIT_ACK -> DONE,主循环每个周期检查状态。

4.3 报警触发条件:连续失败、撬锁检测、超时未关门

报警不能只在密码错时响,门锁场景至少三类:连续指纹/密码验证失败达阈值(防尾随)、锁体被撬(干簧管/震动传感器输入)、开门超时未关(门磁检测)。把这些事件统一包成一个alarm_trigger(type)

void alarm_trigger(uint8_t type) { static uint8_t last_report_ts = 0; uint8_t now = get_tick_sec(); // 同一事件 60 秒内只报一次,防短信轰炸 if (now - last_report_ts < 60) return; last_report_ts = now; switch (type) { case ALARM_PWD_FAIL: send_sms(PHONE_OWNER, "ALARM: PWD FAIL 5 TIMES"); break; case ALARM_TAMPER: send_sms(PHONE_OWNER, "ALARM: TAMPER DETECT!"); break; } }

短信报警的最小间隔 60 秒是经验值,运营商对同一号码短时间发大量短信会触发风控停机,报警逻辑宁可漏发一次也不能让人工卡被锁。GSM 发送期间如果主控被打断,模块可能卡在未知状态,下次发 AT 指令前应补一发 0x10 字符(Ctrl+P,取消当前会话)作复位。

事件日志建议在报警的同时通过串口输出[EVENT] tamper at 12:03:22到调试口,现场排查时日志和短信能对上号才敢下手改逻辑。

5. 指纹与密码的权限合并:开锁仲裁、超时回锁与掉电保持

5.1 三种开锁方式的优先级与互斥

一套完整的门锁,开锁入口至少三路:指纹匹配、密码校验、管理员指纹强制开锁。优先级上,管理员指纹 > 普通指纹 > 密码。管理员指纹(比如 PageID 0 和 1)不仅要开锁,还要允许进入"添加指纹"菜单,普通指纹只能开锁。这个通过 PageID 区间划分:

uint8_t evaluate_access(uint8_t page_id) { if (page_id == 1 || page_id == 2) return ACCESS_ADMIN; if (page_id <= 100) return ACCESS_USER; return ACCESS_DENIED; }

校验逻辑统一进一个access_ctrl()函数,不接受外部散乱的 if 嵌套。主循环里定时器设置的标志位驱动:指纹模块返回匹配成功、密码状态机走到确认态、管理员指令下发——都会调用这个函数,但最终结果都是写入同一个全局变量current_access。之后只由lock_control()按这个变量决定继电器动作。

5.2 开锁超时回锁与继电器控制

继电器吸合让电磁锁断电(或通电开锁,取决于锁型),这里必须处理两个问题:继电器不能一直吸着,锁也不能开了不管。开锁动作后延时 5 秒自动回锁——这个 5 秒用定时器计数,不能用delay_ms(5000)阻塞主循环,否则这 5 秒内有人按指纹、输密码全部无效。

void lock_control(void) { static uint16_t unlock_cnt = 0; if (current_access != ACCESS_NONE) { relay_on(); unlock_cnt = 500; // 500 * 10ms = 5 秒 current_access = ACCESS_NONE; } if (unlock_cnt > 0) { unlock_cnt--; if (unlock_cnt == 0) relay_off(); } }

电磁锁的开锁信号是脉冲式的,继电器持续吸合会让线圈发热甚至烧毁,所以开锁后继电器最多吸合 5 秒,之后不管有没有人进来都断电。门磁传感器(干簧管)接一个 GPIO 外部中断,检测到开门状态持续超过 30 秒,触发ALARM_OPEN_TIMEOUT——这是防止门被掩住后忘了关的关键兜底。

5.3 独立看门狗与异常复位恢复

恶意输入、静电、电源毛刺都可能让 STM32 跑飞。指纹锁这类安防设备必须开 IWDG 独立看门狗,喂狗只能放在主循环空闲位置。注意别在中断里喂狗——如果主循环已经卡死,中断还能跑的话看门狗就形同虚设。

void main_loop(void) { while (1) { process_key_event(); // 键盘事件 poll_as608(); // 非阻塞指纹轮询 check_alarm_inputs(); // 撬锁 / 门磁检测 update_rtc_tick(); IWDG_ReloadCounter(); // 只有走到这里才喂狗 } }

掉电保持除了前面说的密码存 Flash,管理员指纹的"允许录入"标志也要掉电复位——重新上电后自动回到安全状态,需要重新验证管理员才能进入配置菜单。否则有人拆开外壳断电重启就能绕过密码锁定,那就闹笑话了。

6. 现场调试:AS608 不识别、GSM 无响应、按键乱码的定位手法

6.1 AS608 不应答:先抓物理层,再抓协议层

模块不回应,九成是接线问题而不是代码问题。先拿万用表量模块 VCC 和 GND,确认供电在 3.6~5V 之间且纹波小于 100mV;再把 STM32 的串口 TX、RX 和模块交叉对接确认没接成同向。物理层没问题后,用 USB-TTL 直接连 AS608,电脑端发一帧F5 55 AA 01 03 01 03(读版本号),看有没有返回值。这一招能把问题精确隔离在模块本身还是单片机侧。

AS608 这部分常见的误诊是把"模块返回的 FF FF FF FF"当成无响应,其实那是模块在忙或帧头解析失败。抓串口数据的正确姿势是逻辑分析仪挂在 TX 上,波特率设 57600,看发送帧是否完整。确认码返回0x01表示收包错误,基本都是校验和算错,用 printf 把每一帧逐字节打印:[TX] F5 55 AA ... sum=0x3D,和手册对一遍马上能发现。

6.2 GSM 收不到短信:信号 vs 供电优先级

GSM 的网络注册和短信收发是两个独立问题。先发AT+CSQ看信号强度,第一个返回数低于 10 说明在室内/地下室,换天线或移到窗边。信号没问题但短信发不出,用串口抓模块返回:+CMS ERROR: 512是 SIM 卡欠费或没开通短信,+CMS ERROR: 513是短信中心号码没配。模块所有指令都返回 ERROR,则大概率是串口电平不对或供电电流不够。

实测很常遇到的一种怪现象:GSM 模块单独测试正常,一接 STM32 就重启。这是共地不良或电源纹波过大。给模块供电用独立的LM2596MP1584降压模块,输入侧和主控板用同一个 12V 电源但压降路径分开,同时把模块的VDD_EXT通过 100uF 电容对地滤波。

6.3 矩阵键盘乱码:按键抖动和行列反转时序

键盘按键每次按下去输出两个不同键值,是先消抖后反转的时序写错了——特别注意行列反转时,必须先停掉 GPIO 的输出模式再切换为输入模式,否则瞬间短路会把按键值读成0x00,软件上多等 1ms 再做第二次读取就稳了。矩阵键盘有鬼键(按下某个键,相邻两个键同时触发),检查行列二极管有没有焊反。如果是买的成品键盘模块,短路时测一下各行列的默认电平状态再改列映射表。

调试建议在工程里开放一个debug.c,把串口打印、AS608 帧日志、GSM AT 指令回显、键盘按键队列全部打上时间戳,DEBUG_LEVEL宏控制打印量。现场问题只要把串口日志拉回来,按时间线把事件对上号,80% 的故障不用拆机就能定位。最后留一根调试串口的排针在主板上,这个习惯能省掉大量来回烧固件的时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询