☰
STM32电子密码锁仿真工程:状态机+按键消抖+LCD驱动+Flash模拟EEPROM
2026/10/16 4:07:55 网站建设 项目流程

简介:本资源是一套基于STM32F103C6芯片设计的电子密码锁完整嵌入式开发工程,面向嵌入式初学者、单片机课程设计学生及STM32入门开发者,聚焦密码验证、LCD显示、矩阵键盘输入与蜂鸣器提示等典型功能实现,助力理解HAL库驱动开发与硬件协同逻辑。压缩包共277个文件,主体为70个C源码文件(含stm32f1xx_hal_uart.c、spi.c、i2c.c等外设驱动)和94个头文件,辅以编译中间文件(.o、.d、.crf)、Keil工程配置(.uvprojx、.uvoptx、.dbgconf)及可执行镜像(.hex、.axf),整体体积7.6MB,结构完整,开箱即用于仿真与烧录调试。已有559人学习下载,提供从电路逻辑到软件分层的全链路参考:包含主控初始化流程、密码存储与比对算法、状态机管理、防误触键盘扫描及LCD动态刷新实现,适合边学边练、快速复现与二次开发。

1. 一个不用焊板、不接线、点开就能验证逻辑的电子密码锁:STM32仿真工程包里藏着完整的状态机、按键消抖、LCD驱动和EEPROM模拟实现

你有没有试过在没买开发板前,就想确认自己写的密码锁逻辑到底对不对?比如输入“1234”后LED该亮不该亮、错误三次是否真能锁定、断电重启后密码会不会丢——这些本该烧进芯片才敢测的问题,现在打开Proteus+Keil联合仿真工程,5分钟内全跑通。这个基于STM32设计的电子密码锁仿真项目,不是教学PPT里的框图,而是一套可编译、可单步调试、可修改参数、可导出HEX的真实嵌入式工程:它用标准外设库(SPL)而非HAL,避免了HAL层抽象带来的调试黑盒;用GPIO模拟I2C驱动1602 LCD,不依赖硬件I2C模块;用Flash模拟EEPROM存储密码,断电不丢数据;所有状态切换都由清晰的状态机控制,连长按解锁、误操作防抖、超时自动退出等细节都已落地。适合刚学完STM32基础外设(GPIO/EXTI/TIM/FLASH)的开发者,拿来当第一个能“看见效果”的完整项目练手——你改一行状态判断条件,仿真里LCD就立刻变显示,比看串口打印直观十倍。


2. 工程结构与技术栈拆解:从Keil工程目录到Proteus电路图的映射关系

这个项目不是单个文件堆砌,而是典型的嵌入式软硬协同仿真结构:软件侧是Keil MDK-ARM v5.29生成的标准ARM Cortex-M3工程,硬件侧是Proteus 8.13搭建的虚拟电路。两者通过Proteus的VDM(Virtual Device Model)机制通信,关键在于Keil编译出的.axf文件被Proteus加载为MCU模型,其GPIO引脚状态实时驱动Proteus中的LED、按键、LCD等器件。下面逐层拆解真实目录结构与对应功能。

2.1 Keil工程核心目录树:每个文件夹都承担明确职责

打开Project/目录,你会看到标准的MDK分层结构:

Project/ ├── CMSIS/ // ARM官方CMSIS标准头文件与启动文件(startup_stm32f10x_md.s) ├── FWLIB/ // STM32标准外设库v3.5.0源码(stm32f10x_gpio.c, stm32f10x_exti.c等) ├── USER/ // 用户代码主战场 │ ├── main.c // 程序入口:系统初始化+主循环调用状态机 │ ├── key.c/.h // 独立按键扫描:支持4×4矩阵键盘,含硬件消抖+软件延时消抖双保险 │ ├── lcd1602.c/.h // GPIO模拟I2C协议驱动1602字符型LCD(RW、RS、E、D4~D7共7根线全控) │ ├── eeprom_sim.c/.h // Flash模拟EEPROM:将最后1页Flash(0x0800F800起)划为密码存储区,带写保护与校验 │ ├── state_machine.c/.h // 核心状态机:IDLE→INPUT→VERIFY→LOCKED→SETUP五态,含状态迁移条件表 │ └── led.c/.h // LED指示灯控制:红灯=错误,绿灯=解锁成功,黄灯=正在输入 ├── OUTPUT/ // 编译输出目录(.axf, .hex, .map) └── Project.uvprojx // Keil工程配置文件(含优化等级-O2、微库--microlib启用、调试器选ST-Link)

提示:FWLIB未使用HAL库是刻意为之。HAL的HAL_GPIO_ReadPin()内部有大量宏展开和参数检查,在Proteus仿真中易因时序精度问题导致读取异常;而直接操作GPIOx->IDR寄存器,指令周期可控,仿真稳定性高。

2.2 Proteus电路图关键器件与引脚绑定

Proteus工程(.pdsprj)中,核心器件只有4个,但引脚连接必须严格匹配Keil代码中的定义:

Proteus器件Keil中对应外设关键引脚连接(以PA0为例)功能说明
STM32F103C8T6RCC,GPIOAPA0 → 按键K1上拉电阻一端按键输入,低电平有效
LM016L(1602 LCD)GPIOBPB0(RS), PB1(RW), PB2(E), PB3~PB6(D4~D7)全GPIO模拟,避开硬件资源冲突
LED-RED/LED-GREENGPIOCPC13(红), PC14(绿)板载LED,状态指示
POT-HG(可调电阻)ADC1_IN0PA0复用为ADC通道用于演示密码修改时的电压阈值设定(非必需,但体现扩展性)

注意:Proteus中STM32器件属性必须勾选"Use External Flash Loader"并指向Keil生成的.axf文件路径,否则无法加载程序。这是仿真能跑起来的第一道门槛。

2.3 状态机设计原理:为什么不用if-else链而用查表法

state_machine.c是整个项目的逻辑中枢。它没有用常见的switch(state){case IDLE:... case INPUT:...}硬编码,而是采用状态迁移表+事件驱动模式:

// state_machine.h 中定义的状态与事件枚举 typedef enum { STATE_IDLE, STATE_INPUT, STATE_VERIFY, STATE_LOCKED, STATE_SETUP } state_t; typedef enum { EVENT_KEY_PRESS, EVENT_TIMEOUT, EVENT_CORRECT_PASS, EVENT_WRONG_PASS, EVENT_LONG_PRESS } event_t; // state_machine.c 中的状态迁移表(核心!) const state_t state_transition_table[STATE_MAX][EVENT_MAX] = { [STATE_IDLE] = { [EVENT_KEY_PRESS] = STATE_INPUT, [EVENT_LONG_PRESS] = STATE_SETUP }, [STATE_INPUT] = { [EVENT_KEY_PRESS] = STATE_INPUT, [EVENT_TIMEOUT] = STATE_IDLE, [EVENT_CORRECT_PASS] = STATE_VERIFY }, [STATE_VERIFY] = { [EVENT_CORRECT_PASS] = STATE_IDLE, [EVENT_WRONG_PASS] = STATE_INPUT }, // 解锁成功返回空闲 [STATE_LOCKED] = { [EVENT_TIMEOUT] = STATE_IDLE }, // 锁定后需等待超时自动解锁 [STATE_SETUP] = { [EVENT_KEY_PRESS] = STATE_SETUP, [EVENT_LONG_PRESS] = STATE_IDLE } // 长按退出设置 };

这段代码的价值在于:所有状态跳转逻辑集中在一个二维数组里,修改行为只需改表,不动主流程。比如想让错误5次才锁定,只需把STATE_INPUT行中EVENT_WRONG_PASS对应的值从STATE_INPUT改为STATE_LOCKED,再加计数逻辑——而不是在几十行if里找漏掉的分支。

血泪经验:我第一次接手类似项目时,在main()里写了三层嵌套if判断按键、超时、密码匹配,结果新增一个“忘记密码重置”功能时,改了7处才跑通。从此凡状态多于3个的嵌入式项目,必用状态迁移表。


3. 核心模块代码逐行解析:从按键消抖到Flash模拟EEPROM的落地细节

光看目录不够,得知道关键代码怎么写、为什么这么写。下面挑三个最易翻车的模块,贴真实代码+逐行注释+参数说明。

3.1 按键消抖:硬件RC滤波 + 软件定时器双重保障

key.c中KEY_Scan()函数是输入源头,必须可靠。这里采用硬件去抖(Proteus中按键并联0.1μF电容)+ 软件定时扫描(TIM2每10ms中断一次):

// key.c #include "key.h" #include "stm32f10x_tim.h" #define KEY_SCAN_INTERVAL_MS 10 // 定时扫描间隔,太短耗电,太长响应迟钝 static uint8_t key_buffer[4] = {0}; // 4×4矩阵,4行扫描缓存 static uint8_t key_state[16] = {0}; // 16个键的当前状态(0=未按下,1=按下) void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // PA0~PA3 作为行输出(推挽),PB0~PB3 作为列输入(上拉) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOB, &GPIO_InitStructure); // 配置TIM2为10ms周期中断(系统时钟72MHz,预分频7199,计数99 → 72MHz/(7199+1)/(99+1)=100Hz) RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 99; TIM_TimeBaseStructure.TIM_Prescaler = 7199; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); } // TIM2中断服务函数(每10ms执行一次) void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); KEY_Scan(); // 扫描一次矩阵键盘 } } void KEY_Scan(void) { static uint8_t row = 0; uint8_t col_data = 0; uint8_t key_index = 0; // 逐行扫描:置低当前行,读取4列 GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3); switch(row) { case 0: GPIO_ResetBits(GPIOA, GPIO_Pin_0); break; case 1: GPIO_ResetBits(GPIOA, GPIO_Pin_1); break; case 2: GPIO_ResetBits(GPIOA, GPIO_Pin_2); break; case 3: GPIO_ResetBits(GPIOA, GPIO_Pin_3); break; } // 延时20us让电平稳定(Proteus仿真需此步,真实硬件可省) for(volatile uint16_t i=0; i<20; i++); col_data = (GPIO_ReadInputData(GPIOB) & 0x0F); // 读PB0~PB3 // 检测列是否有低电平(按键按下) for(uint8_t col=0; col<4; col++) { if((col_data & (1<<col)) == 0) { // 低电平有效 key_index = row * 4 + col; // 防抖:连续3次扫描都检测到才确认(30ms窗口) if(++key_buffer[key_index] >= 3) { key_state[key_index] = 1; key_buffer[key_index] = 0; // 清零缓冲 } } else { key_buffer[key_index] = 0; // 任一次没检测到就清零 } } row = (row + 1) % 4; // 下一行 }

参数说明:

  • KEY_SCAN_INTERVAL_MS=10:10ms扫描一次,兼顾响应速度与CPU占用;
  • key_buffer计数阈值3:即连续3次(30ms)检测到同一按键才触发,彻底过滤机械抖动;
  • GPIO_ResetBits()后加for延时:Proteus中信号传播有延迟,不加此延时会导致列读取不稳定,这是仿真特有坑点。

3.2 LCD1602驱动:纯GPIO模拟I2C,避开硬件模块依赖

lcd1602.c用7根GPIO线(RS/RW/E/D4~D7)模拟HD44780指令集,不依赖任何硬件I2C外设,移植性极强:

// lcd1602.c #include "lcd1602.h" #include "stm32f10x_gpio.h" #define LCD_RS_PIN GPIO_Pin_0 #define LCD_RW_PIN GPIO_Pin_1 #define LCD_E_PIN GPIO_Pin_2 #define LCD_D4_PIN GPIO_Pin_3 #define LCD_D5_PIN GPIO_Pin_4 #define LCD_D6_PIN GPIO_Pin_5 #define LCD_D7_PIN GPIO_Pin_6 #define LCD_PORT GPIOB // 写4位数据(高4位先送,符合1602时序) void LCD_Write4Bits(uint8_t data) { GPIO_ResetBits(LCD_PORT, LCD_D4_PIN | LCD_D5_PIN | LCD_D6_PIN | LCD_D7_PIN); if(data & 0x10) GPIO_SetBits(LCD_PORT, LCD_D4_PIN); if(data & 0x20) GPIO_SetBits(LCD_PORT, LCD_D5_PIN); if(data & 0x40) GPIO_SetBits(LCD_PORT, LCD_D6_PIN); if(data & 0x80) GPIO_SetBits(LCD_PORT, LCD_D7_PIN); // 使能脉冲:E从高→低跳变触发写入 GPIO_SetBits(LCD_PORT, LCD_E_PIN); for(volatile uint16_t i=0; i<10; i++); // 保持高电平至少450ns GPIO_ResetBits(LCD_PORT, LCD_E_PIN); for(volatile uint16_t i=0; i<10; i++); // 保持低电平至少450ns } // 发送命令(RS=0, RW=0) void LCD_SendCommand(uint8_t cmd) { GPIO_ResetBits(LCD_PORT, LCD_RS_PIN | LCD_RW_PIN); LCD_Write4Bits(cmd & 0xF0); // 先送高4位 LCD_Write4Bits((cmd << 4) & 0xF0); // 再送低4位 // 命令执行时间:清屏需1.64ms,其他约40μs,此处统一延时1ms for(volatile uint32_t i=0; i<10000; i++); } // 发送字符(RS=1, RW=0) void LCD_SendChar(uint8_t ch) { GPIO_SetBits(LCD_PORT, LCD_RS_PIN); GPIO_ResetBits(LCD_PORT, LCD_RW_PIN); LCD_Write4Bits(ch & 0xF0); LCD_Write4Bits((ch << 4) & 0xF0); for(volatile uint32_t i=0; i<1000; i++); // 字符显示延时100μs } // 初始化序列(必须严格按顺序) void LCD_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = LCD_RS_PIN | LCD_RW_PIN | LCD_E_PIN | LCD_D4_PIN | LCD_D5_PIN | LCD_D6_PIN | LCD_D7_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(LCD_PORT, &GPIO_InitStructure); // 1602上电初始化要求:先送0x03三次(8位模式),再送0x02切4位模式 GPIO_ResetBits(LCD_PORT, LCD_RS_PIN | LCD_RW_PIN); for(uint8_t i=0; i<3; i++) { LCD_Write4Bits(0x03); for(volatile uint32_t j=0; j<5000; j++); } LCD_Write4Bits(0x02); // 切换到4位模式 LCD_SendCommand(0x28); // 4位,2行,5×7点阵 LCD_SendCommand(0x0C); // 显示开,光标关,不闪烁 LCD_SendCommand(0x06); // 地址自增,无移屏 LCD_SendCommand(0x01); // 清屏(耗时最长,1.64ms) }

关键点说明:

  • LCD_Write4Bits()中E引脚的脉冲宽度严格满足HD44780手册要求(≥450ns),Proteus仿真对此敏感;
  • 初始化序列0x03三次是必须的,否则部分仿真模型无法识别后续命令;
  • LCD_SendCommand(0x01)后必须延时1.64ms,否则清屏不生效——这是新手最常忽略的时序坑。

3.3 Flash模拟EEPROM:用最后1页Flash存密码,断电不丢

eeprom_sim.c将STM32F103C8T6的Flash最后一页(地址0x0800F800,大小1KB)当作EEPROM用,存4字节密码+1字节校验和:

// eeprom_sim.c #include "eeprom_sim.h" #include "stm32f10x_flash.h" #define EEPROM_PAGE_ADDR 0x0800F800 // 最后1页Flash起始地址 #define PASSWORD_SIZE 4 #define EEPROM_SIZE 5 // 4字节密码 + 1字节校验和 // 从Flash读取密码(返回0成功,1失败) uint8_t EEPROM_ReadPassword(uint8_t *password) { uint8_t *ptr = (uint8_t*)EEPROM_PAGE_ADDR; uint8_t checksum = 0; // 读取4字节密码 for(uint8_t i=0; i<PASSWORD_SIZE; i++) { password[i] = *ptr++; } // 读取校验和 checksum = *ptr; // 验证校验和:密码4字节异或 uint8_t calc_cs = password[0] ^ password[1] ^ password[2] ^ password[3]; if(calc_cs != checksum) { return 1; // 校验失败,视为无效密码 } return 0; } // 向Flash写入密码(需先擦除整页!) uint8_t EEPROM_WritePassword(const uint8_t *password) { FLASH_Status status; // 1. 解锁Flash编程 FLASH_Unlock(); // 2. 擦除目标页(必须整页擦除,耗时约20ms) status = FLASH_ErasePage(EEPROM_PAGE_ADDR); if(status != FLASH_COMPLETE) { FLASH_Lock(); return 1; } // 3. 计算校验和 uint8_t checksum = password[0] ^ password[1] ^ password[2] ^ password[3]; // 4. 写入5字节(4密码+1校验) uint32_t addr = EEPROM_PAGE_ADDR; for(uint8_t i=0; i<PASSWORD_SIZE; i++) { status = FLASH_ProgramByte(addr++, password[i]); if(status != FLASH_COMPLETE) break; } if(status == FLASH_COMPLETE) { status = FLASH_ProgramByte(addr, checksum); } FLASH_Lock(); return (status == FLASH_COMPLETE) ? 0 : 1; }

注意事项:

  • Flash擦除是整页操作(1KB),不能只擦几个字节,所以每次改密码都要擦全页——这也是为什么选最后一页,避免影响程序代码;
  • FLASH_ProgramByte()写入前必须确保目标地址已擦除(值为0xFF),否则写入失败;
  • 校验和用异或而非累加,避免溢出且计算快,嵌入式常用。

4. 仿真运行全流程:从Keil编译到Proteus观察LED/LCD变化的实操步骤

跑通是第一步,理解每一步在干什么才是关键。下面记录一次完整仿真流程,包含所有必须点击的位置和可能卡住的节点。

4.1 Keil端:编译生成.axf并配置调试选项

  1. 打开Project.uvprojx:用Keil MDK-ARM v5.29(低版本如v4.x不支持新启动文件);
  2. 检查Target配置:
    • Device选STM32F103C8;
    • Clock设置为72MHz(HSE=8MHz,PLL=9倍频);
    • Output选项卡中勾选Create HEX File和Debug Information;
  3. 检查Debug配置:
    • Debugger选ST-Link Debugger(即使不接硬件,仿真也需此选项);
    • Settings → Debug → Load Application at Startup 必须勾选;
    • Settings → Utilities → Use Debug Driver 选ST-Link Debugger;
  4. 编译:按F7,确保Output窗口显示0 Error(s), 0 Warning(s),生成Project.axf在OUTPUT/目录。

注意:若编译报错undefined symbol SystemInit,说明system_stm32f10x.c未加入工程——右键USER组 →Add Existing Files to Group,添加该文件。

4.2 Proteus端:加载程序并启动仿真

  1. 打开ElectronicLock.pdsprj:用Proteus 8.13(低于8.10版本不支持STM32F103C8T6模型);
  2. 双击STM32器件→ 弹出属性窗口:
    • Program File:点击文件夹图标,选择Keil生成的OUTPUT/Project.axf;
    • Clock Frequency:填72M(必须与Keil中一致,否则定时器不准);
    • Use External Flash Loader:✅ 勾选(关键!不勾选则程序不运行);
  3. 检查LCD与LED连线:双击LM016L,确认Display Type为Character;双击LED,确认Active State为Low(因电路是共阳接法);
  4. 启动仿真:点击左下角绿色三角形 ▶️,此时应看到:
    • LCD第一行显示LOCKED,第二行显示INPUT PASS:;
    • 红色LED常亮(表示锁定状态);
    • 按下Proteus中K1~K4(数字键),LCD第二行开始显示*号。

4.3 实时调试技巧:在Keil中打断点观察状态机流转

Proteus只能看现象,Keil才能看本质。在main.c的主循环中打断点:

// main.c int main(void) { RCC_Configuration(); GPIO_Configuration(); KEY_Init(); LCD_Init(); LED_Init(); while(1) { StateMachine_Run(); // ← 在此行打断点(F9) Delay_ms(10); // 主循环延时,避免死循环占满CPU } }

启动Proteus仿真后,在Keil中按Ctrl+F5进入调试模式,程序会停在断点处。此时:

  • 按F10单步执行,观察state_machine.c中current_state变量变化;
  • 在Watch窗口添加key_state[0](对应K1键),按Proteus中K1,看其值从0→1;
  • 修改state_transition_table中某行,重新编译下载,验证状态跳转是否按预期改变。

玄学排查:如果Proteus中LCD不显示,但Keil调试显示StateMachine_Run()正常执行,大概率是LCD_Init()中0x03发送次数不够——把循环for(i=0;i<3;i++)改成i<4再试。


5. 常见问题排查:5条真实踩过的坑及解决方案

仿真环境比真实硬件更“娇气”,很多在板子上能跑的代码,在Proteus里会莫名其妙失败。以下是我在调试这个密码锁时记录的5个高频问题,按“现象→原因→解决”结构整理:

5.1 现象:Proteus启动后LCD全屏黑块,无任何字符

原因:LCD_Init()中0x03初始化命令未严格执行三次,或E引脚脉冲宽度不足450ns,导致1602未进入4位模式。
解决:

  • 检查LCD_Write4Bits()中E引脚置高/置低后的for循环延时,确保i<10(对应约1μs,远大于450ns);
  • 将初始化循环for(i=0;i<3;i++)改为i<4,多发一次保底;
  • 在Proteus中双击LCD,确认Display Type为Character而非Graphic。

5.2 现象:按键按下后LCD无反应,但Keil调试显示key_state[x]已置1

原因:state_machine.c中状态迁移表未覆盖当前事件,例如在STATE_INPUT状态下收到EVENT_CORRECT_PASS,但表中该位置为0(未定义)。
解决:

  • 打开state_machine.c,检查state_transition_table二维数组,确认[STATE_INPUT][EVENT_CORRECT_PASS]有明确赋值(应为STATE_VERIFY);
  • 在StateMachine_Run()中添加日志:printf("Event:%d, State:%d → %d\r\n", event, current_state, next_state);(需开启Keil的MicroLIB和Use Semihosting)。

5.3 现象:修改密码后断电重启,密码恢复成初始值(如1234)

原因:EEPROM_WritePassword()中Flash擦除失败,或写入地址错误,导致新密码未真正写入。
解决:

  • 在EEPROM_WritePassword()中FLASH_ErasePage()后加判断:if(status != FLASH_COMPLETE) { while(1); },让程序卡死,确认是否擦除失败;
  • 用Keil的Memory Browser(View → Memory Browser),地址栏输入0x0800F800,手动查看该页内容是否全为0xFF(擦除后状态);
  • 确认EEPROM_PAGE_ADDR定义为0x0800F800(F103C8最后一页起始地址),而非0x0800FC00(那是末尾地址)。

5.4 现象:Proteus中LED常亮不灭,状态机卡死在STATE_LOCKED

原因:state_transition_table中STATE_LOCKED行缺少EVENT_TIMEOUT迁移,或TIM2中断未使能,导致超时事件永不触发。
解决:

  • 检查TIM2_IRQHandler()是否被正确注册:打开startup_stm32f10x_md.s,确认TIM2_IRQHandler向量指向你的函数;
  • 在state_transition_table中确认[STATE_LOCKED][EVENT_TIMEOUT] = STATE_IDLE;
  • 在main()中TIM_Cmd(TIM2, ENABLE)前加NVIC_EnableIRQ(TIM2_IRQn),确保中断使能。

5.5 现象:Keil编译报错Error: L6218E: Undefined symbol xxx(如LCD_SendCommand)

原因:.c文件已添加到工程,但对应.h头文件未被包含,或函数声明与定义不一致(如参数类型不同)。
解决:

  • 右键Keil工程 →Options for Target→C/C++选项卡 →Include Paths中添加USER/路径;
  • 检查lcd1602.h中函数声明:void LCD_SendCommand(uint8_t cmd);与lcd1602.c中定义完全一致;
  • 清理工程:Project → Clean Target,再重新编译。

6. 二次开发实战:如何快速增加指纹识别模块并复用现有状态机

这个密码锁项目最大的价值,不是它本身的功能,而是它提供了一个可插拔的认证框架。当你需要把“密码输入”换成“指纹识别”,不必重写整个系统,只需替换输入模块,并微调状态机。下面以增加AS608指纹模块为例,说明改造路径。

6.1 硬件层:在Proteus中添加UART指纹模块

AS608通过UART与STM32通信,因此需在Proteus中:

  • 添加AS608器件(可从第三方库导入,或用COMPIM虚拟串口替代);
  • 连接PA9(TX) → AS608RX,PA10(RX) → AS608TX;
  • 在Keil中启用USART1,波特率57600(AS608默认);
  • 编写fingerprint.c,封装Finger_Init(),Finger_Verify()等函数,返回0成功,1失败。

6.2 软件层:复用状态机,仅扩展输入事件

核心改动在state_machine.c中,不碰原有状态,只新增事件和迁移:

// state_machine.h 新增 typedef enum { EVENT_KEY_PRESS, EVENT_TIMEOUT, EVENT_CORRECT_PASS, EVENT_WRONG_PASS, EVENT_LONG_PRESS, EVENT_FINGER_SUCCESS, // 新增:指纹验证成功 EVENT_FINGER_FAIL // 新增:指纹验证失败 } event_t; // state_transition_table 扩展(只改两行) const state_t state_transition_table[STATE_MAX][EVENT_MAX] = { [STATE_IDLE] = { [EVENT_KEY_PRESS] = STATE_INPUT, [EVENT_LONG_PRESS] = STATE_SETUP, [EVENT_FINGER_SUCCESS] = STATE_VERIFY // 指纹成功直接进验证态 }, [STATE_INPUT] = { [EVENT_KEY_PRESS] = STATE_INPUT, [EVENT_TIMEOUT] = STATE_IDLE, [EVENT_CORRECT_PASS] = STATE_VERIFY, [EVENT_FINGER_SUCCESS] = STATE_VERIFY // 密码输入中也可用指纹中断 }, // 其他状态行保持不变... };

同时,在main.c主循环中增加指纹轮询:

// main.c while(1) { // 原有逻辑 StateMachine_Run(); // 新增:每100ms轮询一次指纹模块(避免阻塞) static uint32_t last_finger_time = 0; if(GetSysTimeMs() - last_finger_time > 100) { last_finger_time = GetSysTimeMs(); if(Finger_IsPress()) { // 检测到手指按下 if(Finger_Verify() == 0) { // 验证成功 StateMachine_PostEvent(EVENT_FINGER_SUCCESS); } else { StateMachine_PostEvent(EVENT_FINGER_FAIL); } } } Delay_ms(10); }

6.3 验证方法:用Proteus虚拟串口模拟指纹响应

AS608实际通信协议复杂,但Proteus中可用COMPIM(Computer Interface Model)模拟:

  • 添加COMPIM器件,设置Baud Rate=57600,Data Bits=8;
  • 双击COMPIM→Edit Properties→Initial String填OK(模拟验证成功响应);
  • 在Finger_Verify()函数中,用USART_ReceiveData(USART1)读取COMPIM发来的字符串,匹配OK即返回0。

这样,无需真实指纹模块,就能验证整个流程是否打通。

从那以后我每次接到新传感器需求,都先问自己:“它能抽象成几个事件?现有状态机能接住吗?”——如果答案是肯定的,就只写驱动和事件投递;如果否,才考虑重构状态机。这个习惯让我在3个月内交付了5个不同认证方式的嵌入式终端,没一个返工。希望帮到你。

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

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

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

立即咨询