资讯动态

STM32F407指纹密码锁:IIC/SPI/USART三总线与状态机实战

发布时间:2026/9/16 16:10:02 来源:尧图企业网站定制
简介基于STM32F407ZET6的指纹密码锁项目是一套面向单片机进阶学习者的综合实战资源围绕IIC、SPI、USART等通信协议把AS608指纹模块、JR6001语音模块、OLED显示屏、矩阵键盘、外部Flash和继电器整合成可运行的完整锁控系统覆盖从底层硬件驱动到应用层菜单交互的完整开发链路。资源包共399个文件压缩后仅12.09MB以C源文件、头文件和Keil工程文件为主包含56个.c、66个.h等核心代码以及83个.crf、88个.d等编译中间文件既可以阅读逻辑也可以对照分析MDK的编译依赖与文件组织。项目实现了OLED菜单、矩阵按键密码解锁、指纹录入/删除/匹配、语音播报结果、外部Flash存储密码、RTC实时时钟显示、自动锁屏熄屏、继电器模拟开锁上锁并支持无操作自动退出和密码修改等细节核心模块独立划分适合课程设计或毕业设计参考。已有4075人学习下载可直接导入Keil工程运行也可按模块拆解学习适合需要提升STM32外设综合运用能力的开发者。1. 基于STM32F407的指纹密码锁为什么选用三总线方案这是一台以STM32F407ZET6为核心的指纹密码锁。准确说它是做嵌入式项目时“多外设调度”与“多协议协同”的浓缩样本OLED走IICW25Q128走SPIAS608指纹模块和JR6001语音模块走两路USART矩阵键盘用普通GPIO扫描RTC负责锁屏时间继电器控制开锁。功能不复杂但把嵌入式里最常用的三总线全部过了一遍。这套设计的价值不在功能本身而在学习路径你写一段软件IIC驱动OLED、一段SPI读写Flash、两路串口中断收发再把它们并入一个状态机前很难体会“外设多了之后逻辑比协议更棘手”。对于准备做毕业设计或想从裸机转向带管理界面产品的开发者这个项目能让你在同一个工程里对比IIC、SPI、USART的时序差异同时练习真实产品的超时、唤醒、安全验证这些“隐性需求”。2. IIC/SPI/USART三总线硬件映射与驱动配置2.1 引脚分配从矩阵键盘到指纹模块的完整连接表先把硬件连接固定下来后面写驱动才有明确目标。我按标准外设库的工程模板来做引脚尽量错开JTAG/SWD复用口避免调试时占用冲突。下面的连接表是实际跑通过的方案。模块接口STM32引脚说明OLED 0.96寸IICPB6 (SCL), PB7 (SDA)3.3V供电SDA开漏上拉W25Q128SPI1PA5 (CLK), PA6 (MISO), PA7 (MOSI), PA4 (CS)模式016MB容量AS608指纹模块USART1PA9 (TX), PA10 (RX)波特率57600RX/TX交叉连接JR6001语音模块USART2PA2 (TX), PA3 (RX)波特率9600指令触发播放4x4矩阵键盘GPIOPC0~PC3 行PC4~PC7 列行列扫描列输出行输入继电器GPIOPG0高电平开锁需加续流二极管RTC内部无LSE外部32.768K晶振AS608和JR6001都占用串口两个模块的通信格式不同不能共用一路UART所以必须分到两个USART。矩阵键盘行和列的方向不是固定的但我习惯把列设为推挽输出、行设为下拉输入这样扫描时读行稳定。OLED和W25Q128虽然都挂在PB6/PB7附近但一个是IIC一个是SPI不存在冲突。2.2 软件模拟IIC驱动OLED时序和移植边界为什么不用STM32F4的硬件I2C我在这类项目里优先选软件模拟原因有两个一是F4的硬件I2C外设对时序要求比较苛刻稍有不慎就卡在BSY标志上二是OLED屏驱动库大多默认用软件IIC移植起来几乎零成本。缺点就是占CPU但0.96寸OLED刷新率不高对性能影响可以忽略。#define OLED_SCL PBout(6) #define OLED_SDA PBout(7) void IIC_Start(void) { OLED_SDA 1; OLED_SCL 1; delay_us(4); OLED_SDA 0; // SDA在SCL高电平期间拉低表示起始 delay_us(4); OLED_SCL 0; } void IIC_Stop(void) { OLED_SDA 0; OLED_SCL 1; delay_us(4); OLED_SDA 1; // SDA在SCL高电平期间拉高表示停止 delay_us(4); } void IIC_WriteByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { OLED_SCL 0; if (dat 0x80) OLED_SDA 1; // 高位先发 else OLED_SDA 0; delay_us(2); OLED_SCL 1; // 上升沿采样数据 delay_us(2); dat 1; } OLED_SCL 0; }这段代码去掉了ACK检测因为OLED从机应答由硬件保证软件模拟下读ACK要把SDA切回输入模式代码量上不划算。如果有屏幕不显示或花屏先检查起始位是否在SCL高电平时跳变再看SDA引脚是否配置为开漏输出。实际项目中SDA必须外接4.7K上拉电阻或者开启STM32内部上拉否则电平上不去。2.3 SPI驱动W25Q128密码存储和指纹模板的持久化指纹模板存在AS608模块内部Flash而密码、管理员标记、开锁记录放在W25Q128。之所以用外部Flash是因为STM32F407内部Flash按扇区擦写不适合频繁修改密码W25Q128支持按页写、按扇区擦管理起来更灵活。SPI初始化代码基于标准外设库把PA5/PA6/PA7复用为SPI1。void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI1); GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, GPIO_InitStructure); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // 模式0CPOL0 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; // 84MHz/810.5MHz SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); } uint8_t W25Q_ReadByte(void) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, 0xFF); // 发空字节读取 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); return SPI_I2S_ReceiveData(SPI1); } void W25Q_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { W25Q_CS_LOW(); SPI_I2S_SendData(SPI1, 0x06); // 写使能 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); W25Q_CS_HIGH(); W25Q_CS_LOW(); // 页编程命令 0x02 SPI_I2S_SendData(SPI1, 0x02); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, (addr 16) 0xFF); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, (addr 8) 0xFF); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, addr 0xFF); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); while (len--) { SPI_I2S_SendData(SPI1, *buf); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); } W25Q_CS_HIGH(); while (W25Q_ReadStatusReg() 0x01); // 等待写完成 }分频器选8是故意把SPI时钟降到10.5MHz。W25Q128本身支持上百MHz但开发板飞线多、接触电阻大高速SPI容易丢字节。写页命令0x02后跟24位地址一次最多256字节跨页必须拆开发送否则地址会回卷覆盖当前页开头。我在工程里对应用层只开放W25Q_Write和W25Q_Read内部处理扇区擦除和页拆分这样密码模块不用关心底层细节。2.4 两路USART复用AS608与JR6001的通信分工AS608默认波特率57600JR6001是9600两者都是TTL电平直接接USART引脚即可。关键是把串口中断优先级错开避免指纹匹配时语音模块的回传数据把AS608指纹包冲掉。串口模块波特率数据格式中断优先级USART1AS608576008N1抢占优先级2USART2JR600196008N1抢占优先级3两个串口都开RXNE中断中断服务函数里把数据写入各自的环形缓冲。主循环只消费缓冲数据不会因为模块回传时机不同而产生粘包。void USART1_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART1); as608_rb_write(ch); // AS608环形缓冲 } } void USART2_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART2); jr6001_cb_write(ch); // JR6001环形缓冲 } }这里有个容易被忽略的坑AS608识别指纹时返回包长度可能超过100字节。如果环形缓冲区只有64字节第二次进中断就会覆盖前面没读完的数据。我建议缓冲区至少设置256字节或者在中断里直接做包头判断只保留关键字段减少主循环解析压力。3. 指纹注册与密码验证的核心逻辑3.1 AS608指纹模块通信协议拆解AS608使用自定义串口包格式固定以EF 01 FF FF FF FF开头然后是包标识、包长度、命令码、数据和校验和。拿搜索指纹指令来看实际发送的是EF 01 FF FF FF FF 01 00 00 0B 1B 01 00 00 00 00 00 00 00 00 00 23逐段拆开看EF 01是包固定头FF FF FF FF是模块地址默认全F01表示命令包00 00 0B是包长度从包标识后到校验和共11字节1B是“搜索指纹”命令01指定缓冲区号00 00是起始页码00 00是搜索数量范围最后的23是校验和前面所有字节累加取低8位。AS608整个指令集不算复杂但录入指纹需要多步配合先发送01获取图像再发02生成特征重复第二次后发05合并模板。删除则一条0C搞定。匹配时核心就是1B搜索。这些命令配合“成功/失败/无手指”三种应答就构成了下面要讲的状态机。3.2 指纹录入、删除、匹配的状态机设计录入指纹最容易在“等待手指抬起”这个环节卡住。所以我在工程里定义了一个枚举状态用switch-case推进整个录入流程而不是写成一长串阻塞函数。typedef enum { FINGER_IDLE 0, FINGER_CAPTURE1, FINGER_GEN1, FINGER_WAIT_FINGER_UP, FINGER_CAPTURE2, FINGER_GEN2, FINGER_MERGE, FINGER_DONE } FingerEnrollState; FingerEnrollState enroll_state FINGER_IDLE; void FingerEnrollProcess(uint16_t id) { switch (enroll_state) { case FINGER_IDLE: AS608_GenImg(); // 获取图像 enroll_state FINGER_CAPTURE1; break; case FINGER_CAPTURE1: if (as608_result AS608_RET_OK) { AS608_Img2Char(1); // 生成特征到Buffer1 enroll_state FINGER_GEN1; } else if (as608_result AS608_RET_NO_FINGER) { // 无人按指纹保持等待 } else { enroll_state FINGER_IDLE; } break; case FINGER_GEN1: if (as608_result AS608_RET_OK) { OLED_ShowString(0, 4, Remove finger); enroll_state FINGER_WAIT_FINGER_UP; } break; // 省略 FINGER_WAIT_FINGER_UP 及后续状态 } }这里把“获取图像”“生成特征”“合并”分割成独立步骤每一包结果都通过中断缓冲里的回包异步更新as608_result。等待手指抬起时我不断发送GenImg收到“无手指”返回就进入下一步。整个流程加一个超时计数超过10秒没有有效操作强制回到IDLE否则用户中途退出后下一次录用会从错误状态开始。匹配和删除相对简单。匹配调用AS608_Search后返回模板ID和得分通常得分大于40即可判定为同一手指。我在代码里把阈值放宽到50并在OLED上显示得分和ID方便调试时调整指纹按压角度。3.3 矩阵键盘扫描与密码校验指纹锁不能只有指纹开锁密码是第二道认证。4x4矩阵键盘扫描逻辑是逐列拉低逐行读取。行配置为下拉输入列配置为推挽输出。uint8_t KeyScan(void) { uint8_t col, row; uint8_t key_val 0; for (col 0; col 4; col) { GPIO_Write(GPIOC, ~(0x10 col)); // 第col列输出低其余列高 for (row 0; row 4; row) { if (GPIO_ReadInputDataBit(GPIOC, (0x01 row)) 0) { delay_ms(10); // 按键消抖 if (GPIO_ReadInputDataBit(GPIOC, (0x01 row)) 0) { key_val row * 4 col; while (GPIO_ReadInputDataBit(GPIOC, (0x01 row)) 0); return key_val 1; } } } } return 0; }密码设置为6位数字每按一位就在OLED上追加一个“*”。校验时把输入值和Flash中存储的密文对比。我没有直接在内存里保存明文密码而是写入W25Q128前先做一次异或混淆读取后再按位还原。这对门锁场景足够但不要把它当成安全方案真正的高安全性需要加密算法。锁屏状态下输入密码成功直接开锁主菜单里修改密码则要求先验证旧密码再输入两次新密码。这三个页面共用同一个键盘扫描函数只是上层状态不同。3.4 语音反馈JR6001的指令触发表JR6001是串口播放模块音频预先烧录到TF卡或内部Flash里播放时发送指令编号。我设置了一张固定映射表音频编号内容触发场景0x01指纹录入成功合并模板返回OK0x02指纹录入失败图像质量差或合并异常0x03密码正确密码匹配成功0x04密码错误密码不匹配0x05开锁成功继电器闭合0x06门锁已锁定连续失败超过3次void VoicePlay(uint8_t index) { uint8_t cmd[3] {0xAA, 0x02, 0x00}; cmd[1] index; USART_SendData(USART2, cmd[0]); while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); USART_SendData(USART2, cmd[1]); while (USART_GetFlagStatus(USART2, USART_FLAG_TXE) RESET); delay_ms(100); }不同供应商的语音模块指令集有差异用之前要确认波特率和在线模式。部分模块在待机状态命令唤醒无效需要先发送一段固定的“唤醒码”这时先发送一个0xFF或模块自定义的字节再发送真正指令。项目里我把VoicePlay集中在同一个文件后续换模块只改这一个函数。4. 界面状态机和低功耗唤醒4.1 OLED多界面布局与刷新策略0.96寸OLED分辨率128x64一屏最多放三到四行文字。我把界面划分为三层锁屏界面显示时间、日期、解锁提示主菜单显示“指纹”“密码”“设置”子功能界面分别对应录入指纹、删除指纹、修改密码。每层在内存中维护一个current_page枚举主循环只渲染当前页。刷新策略上我采用“触发式重绘”只在按键、RTC秒数变化、状态切换时调用OLED_Clear()其他时间不刷新画面。OLED的IIC刷新一帧大约需要几十毫秒如果放在大循环里连续刷新会抢走指纹匹配和键盘扫描的时间片导致指纹识别卡顿。局部刷新时注意OLED_ShowString会覆盖原有背景需要先清区域再显示新内容否则会残留旧字符。4.2 RTC实时显示和超时自动退出机制锁屏界面除了提示文字还要实时显示日期时间。STM32F407内部RTC在备份域工作主电源掉电后由VBAT供电继续走时。初始化RTC要先打开备份域写保护否则PRL和CNT寄存器写不进去。void RTC_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); // 使能备份域访问 RCC_LSEConfig(RCC_LSE_ON); // 启动外部32.768K while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); RTC_SetPrescaler(32767); // 得到1Hz计时 RTC_SetCounter(0); }使用RTC时要注意“上电残留时间”问题。调试期用ST-Link反复烧录不会清备份域日期可能停在旧值。我习惯每次开发阶段在初始化后强制RTC_SetCounter(0)方便对照串口日志。量产时去掉那行让VBAT维持时间。超时退出我用一个软定时器变量在RTC秒中断里递减。主菜单30秒无操作回锁屏锁屏60秒无操作关闭OLED同时把继电器和语音模块电源拉低。这个策略比在while(1)里计数更稳定因为RTC中断不受主循环阻塞影响。4.3 熄屏唤醒与低功耗模式的取舍完整产品应该支持待机模式。AS608模块的触摸输出脚TOUCH可以直接接到STM32的PB2而PB2正好是WKUP引脚。这样手指碰到指纹模块时产生上升沿将MCU从停止模式唤醒。矩阵键盘要密码唤醒我就把键盘的某一列接到EXTI输入上按键任意一个也能唤醒。void EnterSleepMode(void) { OLED_Clear(); OLED_DisplayOff(); AS608_SleepCmd(); // 让指纹模块进入低功耗 GPIO_ResetBits(GPIOG, GPIO_Pin_0); // 继电器断电 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后需要重新初始化时钟 SystemInit(); OLED_DisplayOn(); }这里有两个关键点。第一从停止模式唤醒后系统时钟自动切回16MHz HSI必须调用SystemInit()恢复168MHz主频否则SPI和USART波特率全乱。第二唤醒后会进入中断服务函数建议在唤醒代码开头加一个标志位进入主循环时跳过第一次按键扫描避免触摸唤醒瞬间产生误触发。提示如果指纹模块的TOUCH引脚直接接PB2注意BKP寄存器的RTC闹钟也会占用WKUP功能二者不能冲突。5. 烧录调试中的四个常见问题与解决技巧5.1 Keil芯片包缺失与Device选型新建工程时最常踩的坑是心里想着F407却发现Keil设备列表里找不到。Keil需要先安装STM32F4系列Device Family Pack。安装后重启Keil在Device里选STM32F407ZETx即可。网上搜“stm32芯片包安装”能找到大量资源但注意要下载与Keil版本匹配的DFP包否则Project窗口会显示设备未知。5.2 “No STM32 Target Found”的真因排查点击Download时弹出error: no stm32 target found通常有三种情况ST-Link接线松、调试器没选对、芯片跑飞把SWD复用掉了。先检查SWDIO、SWCLK、GND三根线再打开Options for Target的Debug选项卡确认Port选择的是SW而不是JTAG。最后把Debug Clock降到1MHz避免飞线干扰。如果还是连不上按住目标板复位键点击烧录后1秒内松手让调试器在用户程序启动前接管芯片。5.3 禁用JTAG后如何恢复调试口指纹锁某些外设紧张的情况下开发者会把PA13/PA14/PA15这些复用引脚占掉并在代码开头加上GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)。执行完这行SWD调试口即刻关闭下次无法下载。恢复方法是按住板子复位不放点击下载后马上松开复位芯片在校检阶段被ST-Link拦下。如果还不行用STM32 ST-Link Utility里的Connect under Reset模式连接再擦除整个Flash。这个技巧建议提前写在工程README里避免过两天自己也忘掉。5.4 用ST-Link Utility验证Flash写入怀疑外部W25Q128里密码写错时用ST-Link Utility只能看内部Flash外部SPI Flash它不认。最快的验证方式是在主循环里加一个“测试读”函数把读出的密码直接显示到OLED上。同时用USB转TTL接AS608的TX抓取指纹模块和STM32之间的往来数据包确认每一条命令都有应答。串口助手设备管理器里如果显示“STM32 Virtual COM Port”带感叹号多半是ST-Link的虚拟串口驱动没装重装驱动后再打开设备。调试阶段我通常把VoicePlay里的指令先屏蔽避免JR6001的回传数据切入USART2影响AS608的串口日志分析。等指纹和密码两部分都调通再打开语音逐个验证播放序号。用这个顺序能把指纹锁的调试时间压缩一大半。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价