简介:本资源是一套基于STM32F103C6芯片设计的电子密码锁完整嵌入式开发工程,面向嵌入式初学者、单片机课程设计学生及STM32入门开发者,聚焦密码验证、LCD显示、矩阵键盘交互与EEPROM数据存储等典型功能实现。压缩包共277个文件,主体为70个C源文件与94个头文件(.h),构成标准HAL库驱动框架;另含25个编译中间文件(.o、.d、.crf)、4个Keil项目文件(.pdsprj、.uvprojx)及调试配置文件(.dbgconf、.hex、.axf),完整覆盖从代码编写、编译链接到烧录调试的全流程。资源大小7.6MB,结构规范,模块划分清晰,含I2C、SPI、UART、ADC、TIM、SD、IRDA等外设驱动示例,便于理解底层通信逻辑与密码锁状态机设计。目前已有559人学习下载,适合用于课程实践、毕业设计参考或嵌入式系统功能模块拆解学习。
1. 电子密码锁仿真不是玩具:它是一套可落地的嵌入式状态机教学闭环,专为刚焊完第一块STM32最小系统板、却卡在“按键抖动怎么消”“LCD刷新总花屏”“密码输错三次就死机”这类真实问题上的开发者准备
你手头有一块STM32F103C8T6(俗称“蓝 pill”),杜邦线插得满桌都是,Keil里编译通过了,但下载进板子后——按键按一次触发三回,密码输对了LCD不显示“OPEN”,输错两次后系统彻底无响应,连串口打印都停了。这不是你代码写得烂,是缺一个把“理论状态图”真正映射到“GPIO电平跳变+定时器中断+FSM状态迁移”的完整参照系。这个基于STM设计的电子密码锁仿真+源码包,就是干这个的:它用Proteus 8.9搭建了带矩阵键盘、1602 LCD、蜂鸣器、LED指示灯和电磁锁模拟的全硬件仿真环境;配套Keil MDK-ARM工程包含完整CMSIS标准外设库驱动,所有模块都封装成独立.c/.h文件,状态机逻辑全部显式写出(不是靠while(1)硬轮询);更关键的是,它把“密码验证失败后的防暴力破解延时锁定”“长按确认键进入设置模式”“掉电记忆当前密码”这些工业级细节全实现了,且每处都有中文注释标出可修改参数。如果你正被嵌入式开发的“最后一公里”卡住——能看懂寄存器手册却调不通实际外设,能写算法却搞不定状态同步——这份资源就是你的调试底座。
2. 仿真环境与工程结构:Proteus电路图与Keil工程目录的严格对齐,是避免“仿真能跑、实物翻车”的第一道防线
2.1 Proteus仿真电路核心元件与引脚映射表
项目提供的Proteus 8.9设计文件(ElectronicLock_Schematic.pdsprj)中,所有外设连接均按STM32F103C8T6数据手册物理引脚定义严格布线。重点核对以下三组映射关系,这是后续Keil代码配置的物理依据:
| 外设模块 | Proteus中器件型号 | STM32引脚(AFIO重映射前) | Keil代码中对应宏定义 | 物理意义说明 |
|---|---|---|---|---|
| 4×4矩阵键盘 | KEYBOARD_4X4 | PA0~PA3(行)、PB0~PB3(列) | KEY_ROWx_GPIO_PORT/KEY_COLx_GPIO_PIN | 行线接推挽输出,列线接上拉输入,扫描法识别按键 |
| 1602 LCD | LM016L | PB5~PB10(DB0~DB5+RS+RW+E) | LCD_DATA_PORT/LCD_CTRL_PORT | 采用4位数据总线模式,节省IO;E脉冲下降沿锁存 |
| 蜂鸣器+LED指示 | BUZZER & LED1~LED2 | PC13(蜂鸣器)、PC14(开锁LED)、PC15(错误LED) | BUZZER_GPIO_PORT/PORT等 | 蜂鸣器用PWM控制音调,LED低电平点亮(共阳接法) |
提示:Proteus中双击任一芯片,在“Edit Properties”里查看“Package”确认是否为
LQFP48封装,若误选TSSOP20会导致引脚数量不匹配,仿真时直接报“Pin not found”。
2.2 Keil MDK-ARM工程目录结构解析:从启动到应用的分层逻辑
打开Project/MDK-ARM/ElectronicLock.uvprojx,其目录结构体现典型的STM32固件分层思想:
ElectronicLock/ ├── CMSIS/ # 标准CMSIS内核文件(无需修改) ├── FWLIB/ # STM32F10x标准外设库(V3.5.0) │ ├── inc/ # 库头文件(stm32f10x.h等) │ └── src/ # 库源码(启动文件startup_stm32f10x_md.s已适配) ├── USER/ # 用户代码主目录 │ ├── main.c # 系统入口:初始化+主循环(仅调用state_machine_run()) │ ├── stm32f10x_it.c # 中断服务函数:SysTick_Handler()用于10ms节拍,EXTI0_IRQHandler()处理按键中断 │ ├── system_stm32f10x.c # 系统时钟配置(HSE=8MHz,SYSCLK=72MHz) │ ├── key_scan.c/h # 独立键盘扫描模块:含消抖状态机(3次采样间隔10ms) │ ├── lcd1602.c/h # LCD驱动:支持清屏、光标定位、字符串显示(非阻塞式写入) │ ├── buzzer.c/h # 蜂鸣器控制:提供beep_on()/beep_off()及beep_pattern()播放预设音效 │ ├── eeprom_emu.c/h # 模拟EEPROM:利用Flash第102页(0x0800FC00)存储密码,擦写前校验 │ └── fsm_core.c/h # **核心状态机**:定义LOCKED/UNLOCKED/SETTING/ERROR四种状态及迁移条件 ├── OUTPUT/ # 编译输出目录(Keil自动生成) └── LISTING/ # 启动文件汇编列表(调试时查向量表用)关键点在于:fsm_core.c不直接操作硬件寄存器,而是通过key_scan_get_key()、lcd_print_str()等抽象接口与外设模块通信。这种解耦让你在移植到实物板时,只需重写.c文件中的GPIO初始化和读写函数,状态机逻辑完全不动。
2.3 主循环与状态机调度机制:为什么不用while(1)硬轮询?
main.c中的主循环极简,仅保留节拍驱动和状态机更新:
// main.c 主循环片段 int main(void) { SystemInit(); // 系统时钟初始化(72MHz) delay_init(72); // SysTick初始化(1ms基准) uart_init(9600); // 串口调试(可选) // 外设初始化(顺序不能乱!) key_scan_init(); // 键盘扫描(配置GPIO+EXTI) lcd_init(); // LCD初始化(发送Function Set等指令) buzzer_init(); // 蜂鸣器GPIO配置 eeprom_emu_init(); // 模拟EEPROM页擦除校验 while(1) { state_machine_run(); // **唯一业务逻辑调用** delay_ms(10); // 10ms节拍,保证状态机最小响应时间 } }state_machine_run()在fsm_core.c中实现,它是一个纯函数:不依赖全局变量,所有状态数据通过static fsm_state_t current_state维护,每次调用根据当前状态、输入事件(如KEY_ENTER、KEY_ERROR)查表决定下一状态,并执行对应动作(如lcd_print_str("OPEN", 0, 1))。这种设计让逻辑可预测、易测试——你甚至可以把state_machine_run()复制到PC端用C语言单元测试框架验证状态迁移是否正确。
3. 核心状态机实现:从LOCKED到UNLOCKED的四步链路,每一行代码都在解决一个具体硬件问题
3.1 状态定义与迁移条件表:硬件事件如何触发状态切换?
fsm_core.h中明确定义了四个状态及其触发条件,这是整个系统的决策中枢:
// fsm_core.h 状态枚举定义 typedef enum { STATE_LOCKED, // 初始锁定态:显示"LOCKED",等待输入 STATE_UNLOCKED, // 解锁成功态:LED亮+蜂鸣器短鸣,持续5秒后自动锁回 STATE_SETTING, // 设置模式态:输入新密码(需长按KEY_SET 2秒进入) STATE_ERROR // 错误态:密码错误3次,锁定30秒(期间按键无效) } fsm_state_t; // fsm_core.c 中的状态迁移逻辑(简化版) void state_machine_run(void) { static uint8_t error_count = 0; static uint32_t lock_start_time = 0; key_event_t key_evt = key_scan_get_event(); // 获取去抖后按键事件 switch(current_state) { case STATE_LOCKED: if(key_evt == KEY_ENTER) { // 按下确认键 if(check_password(input_buffer)) { // 密码校验函数 current_state = STATE_UNLOCKED; unlock_action(); // 开锁动作:LED亮、蜂鸣器响 } else { error_count++; if(error_count >= 3) { current_state = STATE_ERROR; lock_start_time = get_systick_ms(); // 记录锁定起始时间 } buzzer_error_beep(); // 错误提示音 } } break; case STATE_UNLOCKED: if(get_systick_ms() - unlock_start_time > 5000) { // 5秒超时 current_state = STATE_LOCKED; lock_action(); // 关锁:LED灭、蜂鸣器停 } break; // STATE_SETTING 和 STATE_ERROR 的实现逻辑类似,此处省略 } }这段代码的关键在于:所有状态迁移都由明确的硬件事件(key_evt)驱动,而非轮询GPIO电平。key_scan_get_event()返回的是经过3次10ms采样确认的稳定按键事件(KEY_UP/KEY_DOWN/KEY_LONG),彻底规避了机械按键抖动导致的误触发——这是实物调试中最常踩的坑。
3.2 密码校验与EEPROM存储:如何让密码掉电不丢失?
密码存储在Flash模拟的EEPROM中,eeprom_emu.c实现了安全擦写流程。默认密码为1234,存储在FLASH_SAVE_ADDR(0x0800FC00)地址:
// eeprom_emu.c 密码写入函数(关键安全逻辑) uint8_t eeprom_write_password(const uint8_t *pwd, uint8_t len) { FLASH_Status flash_status; uint32_t *flash_addr = (uint32_t*)FLASH_SAVE_ADDR; // 1. 先解锁Flash编程 FLASH_Unlock(); // 2. 擦除目标页(必须整页擦除!) flash_status = FLASH_ErasePage(FLASH_SAVE_ADDR); if(flash_status != FLASH_COMPLETE) return 1; // 3. 写入密码(4字节密码+1字节长度+1字节校验和) for(uint8_t i=0; i<len && i<4; i++) { uint32_t data = pwd[i] << (i*8); // 低位对齐 flash_status = FLASH_ProgramWord(flash_addr + i, data); if(flash_status != FLASH_COMPLETE) goto write_fail; } // 4. 写入长度和校验和(防数据损坏) uint32_t meta_data = ((uint32_t)len) | (calc_checksum(pwd, len) << 8); flash_status = FLASH_ProgramWord(flash_addr + 4, meta_data); write_fail: FLASH_Lock(); // 锁定Flash,防止误写 return (flash_status == FLASH_COMPLETE) ? 0 : 1; }注意:STM32F103的Flash擦写有寿命限制(约1万次),因此代码中未实现密码修改时的自动擦写,而是要求用户先手动擦除(通过Proteus中右键Flash器件选择“Erase”)。实物开发时建议增加写保护判断,避免频繁擦写损坏Flash。
3.3 LCD显示与按键消抖:为什么1602总显示乱码?根源在这里
lcd1602.c采用4位数据总线模式,关键在于时序控制。lcd_write_cmd()函数中,E(使能)引脚必须满足“高电平保持时间≥450ns,下降沿采样”:
// lcd1602.c 关键时序控制(针对STM32F103 72MHz主频) void lcd_write_cmd(uint8_t cmd) { LCD_RS_CLR; // RS=0,写指令 LCD_RW_CLR; // RW=0,写操作 // 高4位数据(D4~D7) LCD_DATA_PORT = (LCD_DATA_PORT & 0xF0F0) | ((cmd & 0xF0) << 4); delay_us(1); // 保证建立时间 LCD_E_SET; // E=1,启动传输 delay_us(1); LCD_E_CLR; // E=0,下降沿锁存(关键!) delay_us(100); // 等待指令执行(最长1.64ms,此处留足余量) // 低4位数据(D0~D3) LCD_DATA_PORT = (LCD_DATA_PORT & 0xF0F0) | ((cmd & 0x0F) << 4); LCD_E_SET; delay_us(1); LCD_E_CLR; delay_us(100); }实物调试时若LCD显示乱码,90%概率是delay_us(1)不够精确。STM32F103在72MHz下,1条空指令约13.9ns,delay_us(1)需循环约72次。建议用示波器测E引脚波形,确保高电平宽度≥450ns且下降沿陡峭。
4. 常见问题排查:那些让你对着示波器抓狂3小时的“玄学”故障,其实都有固定解法
4.1 现象:Proteus中按键按下无反应,串口打印显示key_evt=0
原因:矩阵键盘扫描时,行线(PA0~PA3)未配置为推挽输出模式,或列线(PB0~PB3)未启用内部上拉。Proteus默认GPIO为高阻态,不主动上拉则列线电平浮动,无法检测到行线拉低。
解决:检查key_scan_init()函数中GPIO初始化代码:
// key_scan.c 必须包含以下配置 GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); 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);4.2 现象:LCD显示“OPEN”后立即变为空白,或字符错位
原因:LCD初始化时未严格遵循HD44780时序。lcd_init()中第一次发送0x30(Function Set)后,必须等待至少4.1ms才能发第二次,否则控制器未完成复位。
解决:在lcd_init()开头强制加入长延时:
// lcd1602.c 初始化函数修正 void lcd_init(void) { delay_ms(15); // 上电等待时间(关键!) lcd_write_cmd(0x30); // 第一次Function Set delay_ms(5); // 必须≥4.1ms lcd_write_cmd(0x30); // 第二次 delay_ms(1); lcd_write_cmd(0x30); // 第三次 delay_ms(1); lcd_write_cmd(0x28); // 4位数据,2行显示,5×7点阵 // 后续指令... }4.3 现象:输入正确密码后LED不亮,但蜂鸣器正常发声
原因:LED电路为共阳接法(LED阳极接VCC,阴极接PC14),而代码中LED_ON()宏定义为GPIO_ResetBits()(即输出低电平导通),但PC14引脚未开启时钟。
解决:检查led_init()中RCC时钟使能:
// led.c 初始化(常见遗漏点) void led_init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 必须使能GPIOC时钟! GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_SetBits(GPIOC, GPIO_Pin_14 | GPIO_Pin_15); // 默认熄灭(共阳需高电平) }4.4 现象:Keil编译报错undefined symbol 'SystemInit'
原因:工程中未添加system_stm32f10x.c文件,或该文件中SystemInit()函数被注释。此函数负责配置HSE/PLL,是SysTick定时器工作的前提。
解决:在Keil中右键USER文件夹 →Add Existing Files to Group 'USER'→ 添加system_stm32f10x.c,并确认其内容未被注释:
// system_stm32f10x.c 必须存在且未注释 void SystemInit(void) { // ... HSE配置代码(约50行) RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2; // APB1预分频2 RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1; // APB2预分频1 // ... PLL配置 }5. 二次开发实战:从“能跑通”到“能改功能”的三个必做改造,附验证方法
5.1 改造一:将4位密码升级为6位,同时兼容旧密码存储格式
原始密码存储为4字节(0x00000001 0x00000002 0x00000003 0x00000004),扩展需调整EEPROM布局。核心步骤:
- 修改密码存储结构:在
eeprom_emu.h中定义新结构体,保留向后兼容字段:
// eeprom_emu.h 新增 #pragma pack(1) typedef struct { uint8_t pwd[6]; // 6字节密码(原4字节+新增2字节) uint8_t len; // 实际密码长度(4或6) uint8_t checksum; // 前6字节校验和 uint8_t reserved[3]; // 填充至12字节,对齐Flash页 } pwd_struct_t; #pragma pack()- 重写密码读写函数:
eeprom_read_password()需判断len字段决定读取字节数:
// eeprom_emu.c 改造后读取逻辑 uint8_t eeprom_read_password(uint8_t *pwd_buf, uint8_t *len_out) { pwd_struct_t pwd_data; uint32_t *flash_ptr = (uint32_t*)FLASH_SAVE_ADDR; // 读取整个结构体(12字节 = 3个word) for(int i=0; i<3; i++) { ((uint32_t*)&pwd_data)[i] = flash_ptr[i]; } *len_out = pwd_data.len; memcpy(pwd_buf, pwd_data.pwd, (*len_out < 6) ? *len_out : 6); return (pwd_data.checksum == calc_checksum(pwd_data.pwd, pwd_data.len)) ? 0 : 1; }- 验证方法:在Proteus中运行,先用旧密码
1234解锁,进入STATE_SETTING后输入123456并确认。用Proteus的“Debug → Memory Browser”查看0x0800FC00地址,应看到01 02 03 04 05 06 06 XX(XX为校验和),证明6字节已写入。
5.2 改造二:增加红外遥控解锁功能(使用VS1838B接收头)
在现有矩阵键盘基础上叠加红外解码,需新增硬件和软件模块:
Proteus电路改造:添加VS1838B器件,OUT引脚接PA4(配置为EXTI4中断源)。注意VS1838B需外接10kΩ上拉电阻至VCC。
Keil新增红外解码模块:创建
ir_decode.c/h,利用SysTick节拍测量脉宽:
// ir_decode.c 核心解码逻辑(NEC协议) #define IR_START_HIGH_US 9000 // 引导码高电平9ms #define IR_START_LOW_US 4500 // 引导码低电平4.5ms #define IR_BIT_HIGH_US 560 // 逻辑1高电平0.56ms #define IR_BIT_LOW0_US 560 // 逻辑0低电平0.56ms #define IR_BIT_LOW1_US 1690 // 逻辑1低电平1.69ms uint8_t ir_decode_buffer[4]; // 存储解码后的32位数据(地址+命令) uint8_t ir_decode_complete = 0; void EXTI4_IRQHandler(void) { static uint32_t last_edge_time = 0; static uint8_t bit_pos = 0; uint32_t now = get_systick_us(); uint32_t pulse_width = now - last_edge_time; if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) == Bit_RESET) { // 下降沿 if(pulse_width > 8000 && pulse_width < 10000) { // 引导码高电平 bit_pos = 0; } else if(pulse_width > 500 && pulse_width < 700) { // 逻辑1高电平 if(bit_pos < 32) { ir_decode_buffer[bit_pos/8] |= (1 << (7 - bit_pos%8)); bit_pos++; } } } last_edge_time = now; EXTI_ClearITPendingBit(EXTI_Line4); }- 集成到状态机:在
state_machine_run()中增加红外事件处理:
// fsm_core.c 新增 if(ir_decode_complete) { if(ir_decode_buffer[2] == 0x12) { // 假设0x12为解锁命令 current_state = STATE_UNLOCKED; unlock_action(); } ir_decode_complete = 0; // 清标志 }5.3 改造三:将蜂鸣器提示音改为语音播报(使用WT588D语音芯片)
实物开发中,蜂鸣器音效单调,而WT588D支持MP3语音。此改造需更换硬件并重写驱动:
Proteus替换器件:移除BUZZER,添加WT588D芯片,BUSY引脚接PB11,DATA引脚接PB12(模拟UART TX)。
新增语音驱动:
wt588d.c实现串口协议发送:
// wt588d.c 语音播放函数(WT588D UART模式) void wt588d_play_voice(uint8_t voice_id) { uint8_t cmd[4] = {0x00, 0x00, 0x00, 0x00}; cmd[0] = 0x01; // 播放指令 cmd[1] = voice_id; // 语音ID(0x01="OPEN",0x02="ERROR") cmd[2] = 0xFF; // 校验和(低8位) cmd[3] = 0xFF; // 校验和(高8位) USART_SendData(USART1, cmd[0]); while(USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); USART_SendData(USART1, cmd[1]); // ... 发送剩余字节 }- 替换原蜂鸣器调用:在
unlock_action()中调用wt588d_play_voice(0x01)替代buzzer_beep(1000)。
从那以后我每次接到新外设需求,都强制走一遍“Proteus建模→信号时序分析→Keil驱动分层→状态机集成”四步流程。哪怕只是加一个LED,也要先画出它的驱动电流路径,再查STM32数据手册确认GPIO最大灌电流是否超标。这套习惯让我在三年内没再因为“硬件连错了”返工过一次——希望帮到你。
本文还有配套的精品资源,点击获取