
简介这是一套面向嵌入式初学者与单片机课程设计者的STM32F407电子密码锁完整实现方案主控采用STM32F407配合3.2寸LCD屏、继电器与电磁铁锁头构成可上电即用的实物系统。上电后需点击屏幕四下完成坐标校准成功后显示Adjust Success随后进入类似手机拨号键盘的界面支持输入密码与修改密码默认开锁密码为123456修改时需先输入管理员密码0000且管理员密码需重新烧写程序才能更改。资源包共271个文件约234.47MB包含50个h头文件、48个c源文件及大量o、d、crf编译中间文件另有4份doc开发文档、3段mp4视频讲解、2个hex烧录文件、2份xlsx表格与硬件连接图等覆盖源代码、开发文档、视频讲解与硬件连接图四类内容。目前已有3473人学习下载适合希望掌握LCD驱动、RTC、定时器、Flash读写与继电器控制等外设编程的读者可据此快速复现密码锁原型并理解工程目录组织与调试流程。1. 从一块 F407 最小系统板到能用的电子密码锁这套源码到底值不值得拆手头有块 STM32F407 最小系统板屏幕、矩阵键盘、电磁锁散在桌上想拼一套能改密码、能锁死、能报警的电子密码锁结果卡在状态机怎么写、掉电密码怎么存、开锁瞬间复位怎么办——这套 STM32f407 电子密码锁源码包解决的就是从「板子能跑」到「锁能用」之间那段没人愿意细讲的工程活。它面向的是已经点过灯、调过串口但没独立做过完整交互设备的嵌入式从业者和电子类专业学生。整套代码围绕 F407 的 GPIO、定时器、USART、Flash 读写和外部中断展开把密码输入、校验、存储、开锁驱动、错误锁定串成一条可复现的主线。下面按「这是什么 → 怎么跑起来 → 坑在哪 → 怎么改」的顺序拆能直接抄的步骤我都落成代码块参数怎么改、失败看哪里一并写清。2. 硬件链路与工程结构先把引脚、时钟和存储映射理清2.1 外设接线与引脚分配拿到源码先别急着编译第一件事是把引脚表和自己的板子对一遍。F407 的 GPIO 分 A~G 组每组 16 个引脚复用功能靠 AFR 寄存器配置。电子密码锁典型外设是 4x4 矩阵键盘、I2C 或并口 LCD、一个继电器或电磁锁驱动、一个蜂鸣器外加可选的外部 Flash 或 EEPROM。矩阵键盘占 8 根线4 行 4 列LCD1602 并口占 6 根RS、RW、EN 加 4 位数据继电器 1 根蜂鸣器 1 根加起来 16 根左右F407 随便一组都够。我一般把键盘行线接 PA0~PA3 设为推挽输出列线接 PA4~PA7 设为上拉输入扫描时逐行拉低读列。这里有个容易忽略的点F407 的 GPIO 速度等级默认是 2MHz键盘扫描这种低速场景够用但继电器驱动引脚建议设成 50MHz 并加三极管别直接拿 IO 灌电流F407 单脚最大 25mA电磁锁线圈启动电流轻松上百毫安直连必烧。// 键盘行线推挽输出初始高电平 GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); gpio.Pin GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3; gpio.Mode GPIO_MODE_OUTPUT_PP; // 推挽输出 gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_LOW; // 键盘扫描低速即可 HAL_GPIO_Init(GPIOA, gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3, GPIO_PIN_SET); // 键盘列线上拉输入读电平判断按键 gpio.Pin GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; // 内部上拉省外部电阻 HAL_GPIO_Init(GPIOA, gpio);这段初始化里Speed选LOW是刻意的键盘扫描对边沿速率没要求低速反而减少 EMI。Pull用内部上拉能省掉 4 个外部电阻但如果你键盘排线超过 20cm内部上拉那点弱电流扛不住分布电容读到的列线会飘这时候老老实实外接 4.7k 上拉。参数上唯一要按自己板子改的是GPIO_PIN_x和对应的GPIOA换成 PB、PC 组同理记得同步改__HAL_RCC_GPIOx_CLK_ENABLE()。2.2 时钟树与定时器节拍F407 默认跑 168MHz靠 HSE 8MHz 经 PLL 倍频。源码里SystemClock_Config()一般配成 HSEPLLM8、N336、P2得到 168MHz 的 SYSCLKAHB 不分频APB1 四分频 42MHzAPB2 二分频 84MHz。这个配置决定了你后面定时器周期算得对不对——很多人抄了代码发现 1ms 定时变成 0.5ms就是 APB 分频和定时器时钟倍频关系没搞清APB 预分频不为 1 时定时器时钟是 APB 时钟的两倍。// TIM3 做 1ms 系统节拍APB142MHz定时器时钟84MHz htim3.Instance TIM3; htim3.Init.Prescaler 8400 - 1; // 84MHz / 8400 10kHz htim3.Init.Period 10 - 1; // 10kHz / 10 1kHz - 1ms htim3.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_Base_Init(htim3); HAL_TIM_Base_Start_IT(htim3); // 开更新中断Prescaler和Period这两个参数是密码锁里最常被改错的地方。预分频值写的是「实际分频数减一」因为计数器从 0 开始周期同理。上面 8400×10 正好把 84MHz 降到 1kHz中断里做键盘扫描和超时计时。如果你把 APB1 改成八分频定时器时钟变 42MHz那Prescaler得改成 4200-1否则节拍全乱。失败时先拿示波器量一个 IO 翻转或者用调试器看TIM3-CNT是否在动比盲猜快。2.3 密码存储映射内部 Flash 扇区怎么选密码掉电不能丢F407 内部 Flash 是最省事的方案不用外挂芯片。F407 的 Flash 按扇区擦除扇区 0~3 各 16KB扇区 4 是 64KB扇区 5~11 各 128KB。代码区一般占前几个扇区密码这种小数据我习惯放最后一个扇区扇区 11起始地址 0x080E0000避免和程序互相踩。#define PWD_SECTOR FLASH_SECTOR_11 #define PWD_ADDR 0x080E0000 #define PWD_MAGIC 0x5A5A1234 // 判断是否首次上电 void save_password(uint32_t pwd) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase {0}; uint32_t err 0; erase.TypeErase FLASH_TYPEERASE_SECTORS; erase.Sector PWD_SECTOR; erase.NbSectors 1; erase.VoltageRange FLASH_VOLTAGE_RANGE_3; // 2.7~3.6V HAL_FLASHEx_Erase(erase, err); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, PWD_ADDR, pwd); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, PWD_ADDR 4, PWD_MAGIC); HAL_FLASH_Lock(); }VoltageRange必须和你板子供电匹配3.3V 系统用RANGE_3写错会返回错误标志。擦除是按扇区整块擦扇区 11 有 128KB为了存 8 字节擦掉 128KB寿命上不划算但功能没问题要频繁改密码就换外部 EEPROM。PWD_MAGIC是后悔药首次上电读到 magic 不对就加载默认密码 123456 并提示用户修改避免空 Flash 读出全 0xFF 导致永远开不了锁。3. 主状态机与开锁逻辑从按键扫描到继电器动作3.1 状态机设计为什么不用一堆 if 堆电子密码锁本质是个状态机待机、输入中、校验中、开锁、错误锁定、改密。新手最容易写成if(按键1){...} else if(按键2){...}一路堆下去加到改密和锁定逻辑后自己都读不懂。源码里用enum定义状态主循环里switch分发每个状态只关心自己的输入和跳转逻辑清晰也好加功能。typedef enum { ST_IDLE, // 待机等待输入 ST_INPUT, // 正在输入密码 ST_VERIFY, // 校验 ST_UNLOCK, // 开锁 ST_LOCKED, // 错误锁定 ST_CHANGE_PWD // 修改密码 } lock_state_t; lock_state_t state ST_IDLE; uint8_t input_buf[8]; uint8_t input_len 0; void lock_fsm(uint8_t key) { switch (state) { case ST_IDLE: if (key ! KEY_NONE) { input_len 0; state ST_INPUT; } break; case ST_INPUT: if (key 0 key 9 input_len 6) { input_buf[input_len] key; } else if (key KEY_OK) { state ST_VERIFY; } else if (key KEY_CANCEL) { input_len 0; state ST_IDLE; } break; case ST_VERIFY: if (check_password(input_buf, input_len)) { state ST_UNLOCK; } else { err_count; state (err_count 3) ? ST_LOCKED : ST_IDLE; } break; // 其余状态略逻辑同上 } }input_len 6是密码长度上限改密码长度就改这个数同时input_buf数组大小要跟着调。err_count 3是错误锁定阈值常见做法是锁 30 秒用定时器计数实现别用HAL_Delay死等否则键盘和显示全卡住。状态机的好处是加「改密」只需加一个ST_CHANGE_PWD分支不用动其他逻辑。3.2 开锁驱动与防抖继电器和电磁锁怎么控开锁动作就是拉高继电器控制脚保持几百毫秒再释放。但这里有两个坑一是机械按键抖动二是开锁瞬间电流冲击导致 MCU 复位。防抖我一般用定时器 10ms 周期扫描连续两次读到同一键值才算有效比HAL_Delay(20)阻塞式防抖靠谱不耽误其他任务。// 10ms 定时器中断里调用非阻塞防抖 uint8_t key_scan_debounce(void) { static uint8_t last KEY_NONE, stable KEY_NONE; static uint8_t cnt 0; uint8_t now key_matrix_read(); // 读原始键值 if (now last) { if (cnt 2) cnt; if (cnt 2) stable now; // 连续两次一致才认 } else { cnt 0; last now; } return stable; } // 开锁拉高继电器 500ms void do_unlock(void) { HAL_GPIO_WritePin(RELAY_GPIO, RELAY_PIN, GPIO_PIN_SET); unlock_tick 500; // 主循环里递减到 0 释放 }cnt 2配合 10ms 周期等于 20ms 消抖窗口对大多数机械键盘够用如果你的键盘手感特别差把阈值提到 3。继电器和 MCU 供电一定要分开走线电磁锁线圈两端并一个续流二极管1N4007 就行否则断电瞬间反电动势会把 IO 打穿。我见过最惨的翻车是电磁锁和板子共用一根细 USB 线开锁瞬间压降把 MCU 拉到复位现象是「一开锁就重启」查了半天以为是代码问题其实是供电。3.3 显示与交互反馈LCD1602 或 OLED 负责提示「输入密码」「密码错误」「已锁定」。并口 LCD1602 用 4 位模式能省 4 根线初始化顺序不能错先发 0x33、0x32 切 4 位再发 0x28 设两行 5x70x0C 开显示关光标0x06 设增量0x01 清屏。顺序错了屏幕要么不亮要么显示乱码这是血泪经验。void lcd1602_init(void) { HAL_Delay(50); // 上电等待 40ms lcd_write_cmd(0x33); HAL_Delay(5); lcd_write_cmd(0x32); HAL_Delay(5); // 切 4 位模式 lcd_write_cmd(0x28); HAL_Delay(5); // 两行显示 lcd_write_cmd(0x0C); HAL_Delay(5); // 开显示关光标 lcd_write_cmd(0x06); HAL_Delay(5); // 光标右移 lcd_write_cmd(0x01); HAL_Delay(5); // 清屏 }每个命令后的HAL_Delay不是可有可无LCD 控制器处理命令需要时间尤其清屏要 1.52ms 以上删掉延时会出现「偶尔花屏」的玄学问题。显示内容刷新别放在定时器中断里I2C 或并口操作耗时放主循环按状态更新即可。4. 避坑与排查密码锁最容易翻车的五个地方4.1 现象烧录后屏幕不亮但程序在跑原因通常是 LCD 背光或对比度引脚没接对或者 4 位模式初始化顺序错。先量背光引脚电压再确认lcd_write_cmd(0x33)和0x32是否都发了。解决对比度引脚V0接 10k 电位器中点调到字隐约可见初始化顺序严格按 3.3 节来别自作主张合并命令。4.2 现象按键偶尔连击或没反应原因是扫描周期和消抖窗口不匹配或者列线上拉太弱。解决确认定时器真是 10ms用示波器量 IO 翻转消抖阈值按键盘质量调 2~3排线长就外接 4.7k 上拉。别用HAL_Delay做消抖会拖垮整个系统响应。4.3 现象改完密码重启后还是旧密码原因是 Flash 擦除后没等操作完成就写或者写地址没对齐。F407 按字32 位编程地址必须 4 字节对齐。解决擦除后查FLASH-SR的 BSY 位确认为 0 再写PWD_ADDR保证 4 的倍数写完回读校验一次不一致就重写。4.4 现象一开锁 MCU 就复位原因是电磁锁电流冲击导致电源跌落或续流二极管缺失。解决锁和 MCU 分开供电共地即可线圈并续流二极管继电器控制脚加 100nF 去耦。这是硬件问题改代码没用。4.5 现象错误锁定后时间不走永远解不开原因是锁定计时用了阻塞延时或者计时变量在中断里没加volatile。解决锁定倒计时放定时器中断递减变量声明加volatile主循环只读不写。编译器优化会把没volatile的变量缓存到寄存器导致主循环读到的永远是旧值这个坑很隐蔽。5. 进阶改造与验证把固定密码升级成可管理方案源码跑通只是起点实际项目里往往要支持多用户、临时密码或上位机改密。我一般先做一件事把密码存储从「单个 uint32」升级成带校验的结构体加一个简单的累加和防止 Flash 位翻转读出乱码。typedef struct { uint32_t magic; // 0x5A5A1234 uint8_t pwd[6]; // 6 位密码ASCII uint8_t len; // 实际长度 uint16_t crc; // 前几字节累加和 } pwd_record_t; uint16_t calc_crc(pwd_record_t *r) { uint16_t sum 0; uint8_t *p (uint8_t *)r; for (int i 0; i offsetof(pwd_record_t, crc); i) sum p[i]; return sum; }offsetof算到crc字段之前的字节数校验时重算比对不一致就回退默认密码。这样即使 Flash 某位翻转也不会拿错误密码去比对导致锁死。验证方法很直接写进去后手动改一个字节再读看是否触发回退或者用调试器直接改内存模拟位翻转。再进一步是加串口改密协议。F407 的 USART1 接 PA9/PA10波特率 115200定义一个简单帧帧头 0xAA、命令字、数据、校验、帧尾 0x55。收到改密命令先校验旧密码通过才写 Flash。这里注意串口接收用中断加环形缓冲别在中断里直接写 FlashFlash 操作耗时毫秒级会丢后续字节。// 串口中断收字节入环形缓冲主循环解析 void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_RXNE)) { uint8_t b (uint8_t)(huart1.Instance-DR 0xFF); ring_push(rx_ring, b); // 只入队不解析 } }主循环里ring_pop逐字节组帧校验通过再调save_password。这样即使 Flash 写入卡住串口也不会丢数据。最后一步验证连续改密 100 次每次重启读回比对确认无一位出错再模拟断电直接拔电在写入过程中看是否能靠 magic 和 crc 回退到安全状态。从那以后我每次做带存储的嵌入式设备都强制走一遍「写入中断电」测试宁可多花十分钟也不想在现场被一把打不开的锁堵住。希望帮到你。本文还有配套的精品资源点击获取