外观
STM32 密码锁 — 代码阅读文档
面向新接手本项目的开发者。读完本文即可定位任意功能的实现位置,并理解整体设计。 配套需求说明见 项目介绍。
1. 项目概述
基于 STM32F103C8T6 的 6 位密码锁,支持:
- 12 键独立按键输入密码(含
*清除、#确认、长按改密 / 长按录入指纹) - 指纹模块开锁与录入(ZW101,USART)
- 密码断电保存(AT24C02 EEPROM)
- OLED 屏幕显示密码掩码 / 状态提示(
OK/ERR/CHG/FP) - 红 / 绿 LED + 无源蜂鸣器作开锁结果与操作反馈
主控芯片 STM32F103C8T6 @ 72 MHz(HSE 8 MHz × PLL9),裸机 + HAL 库,无 RTOS,全部应用逻辑在一个 1 ms 节拍的状态机里非阻塞推进。
2. 目录结构
project/
├── CMakeLists.txt 顶层构建(应用层源文件在此登记)
├── project.ioc STM32CubeMX 工程配置
├── cmake/stm32cubemx/ CubeMX 生成的构建脚本
├── Drivers/ ST HAL + CMSIS(CubeMX 生成,勿手改)
└── Core/
├── Inc/ 头文件
│ ├── app_config.h ★ 应用层共享配置(引脚/时序常量)
│ ├── app_*.h 应用层各模块接口
│ ├── fingerprint.h 指纹驱动接口
│ └── main.h / *.h CubeMX 生成(引脚宏、外设句柄)
└── Src/
├── app_lock.c ★ 主状态机(项目核心)
├── app_key.c 12 键扫描 + 消抖 + 事件
├── app_buzzer.c 蜂鸣器 PWM 音效
├── app_led.c LED 指示(GPIO 共阳)
├── app_oled.c OLED 软件 I2C + 字库
├── app_eeprom.c AT24C02 密码 / 指纹计数存取
├── fingerprint.c ZW101 指纹驱动(协议层 + 步进状态机)
├── main.c 主程序 + 1 ms 节拍调度
└── gpio.c / tim.c / usart.c / i2c.c / stm32f1xx_it.c CubeMX 生成1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
约定:
app_*.c/.h与fingerprint.c/.h是项目自有代码,可自由修改;main.c / gpio.c / tim.c / usart.c / i2c.c / stm32f1xx_it.c由 CubeMX 生成,自定义代码只能写在/* USER CODE BEGIN/END */标记之间(重生成时不覆盖)。
3. 硬件资源
| 外设 | 引脚 | STM32 资源 | 说明 |
|---|---|---|---|
按键 SW0~SW9 / SW10(*) / SW11(#) | PA0,PA1,PA2,PA3,PA4,PA5,PB4,PB5,PB6,PB7,PB8,PB9 | GPIO 输入上拉 | 一端接地,按下读 0 |
| 蜂鸣器 | PB1 | TIM3_CH4 PWM | 无源,改 ARR 变调 |
| LED_R(红) | PA6 | GPIO 推挽输出 | 共阳:低电平亮 |
| LED_G(绿) | PA15 | GPIO 推挽输出 | 共阳:低电平亮 |
| EEPROM AT24C02 | PB10(SCL)/PB11(SDA)/PA12(WP) | I2C2 | 密码 + 指纹计数 |
| 指纹 ZW101 | PA9(TX)/PA10(RX)/PA11(TOUCH_OUT) | USART1 @57600 | 单字节中断接收 |
| OLED SSD1306 | PB0(SCL)/PA7(SDA) | GPIO 开漏(软件 I2C) | 0.96" 128×64 |
引脚宏定义在 CubeMX 生成的 main.h(SW*_Pin、OLED_SCL_Pin 等),LED 与触摸引脚的额外宏在 app_config.h。
4. 软件架构
┌───────────────────────────────────────────────┐
│ app_lock.c —— 主状态机(Lock_Init/Lock_Tick)│ ← 项目核心,串联所有模块
└──┬──────┬──────┬──────┬──────┬──────┬─────────┘
│ │ │ │ │ │
app_key app_buz app_led app_ol app_ee fingerprint
│ │ │ ed prom │
│ │ │ │ │ │
└──────┴──┬───┴───────┴──────┴──┬────┴────────┐
│ CubeMX 生成的 HAL 层 │ USART1 IRQ │
│ (gpio/tim/usart/i2c) │ → 帧解析 │
└──────────┬───────────┴──────────────┘
│
main.c 主循环(1 ms 节拍)1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
分层职责:
- HAL 层(CubeMX 生成):外设初始化(
MX_GPIO_Init/MX_TIM3_Init/MX_USART1_UART_Init/MX_I2C2_Init)、中断入口(stm32f1xx_it.c)。 - 驱动层:
app_key/buzzer/led/oled/eeprom封装单一外设;fingerprint封装指纹模块协议。 - 应用层:
app_lock.c是唯一的状态机,协调所有驱动实现业务逻辑。
5. 主程序与节拍调度
main.c 的 main() 流程:
c
HAL_Init(); // SysTick 等
SystemClock_Config(); // 72 MHz
MX_GPIO_Init(); // CubeMX 外设初始化
MX_I2C2_Init(); MX_TIM1_Init(); MX_TIM3_Init(); MX_USART1_UART_Init();
Lock_Init(); // ★ 初始化全部驱动 + 加载密码 + 进 IDLE 态
while (1) {
// 1 ms 节拍调度:补齐漏掉的毫秒,上限 20ms 防异常追赶
if (now != tick_last) { dt = now - tick_last; while (dt--) Lock_Tick(); }
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
Lock_Tick() 每 1 ms 调用一次(见 app_lock.c),内部顺序:
Key_Tick()+ 消费按键事件 → 派发到当前状态的处理函数FP_Tick()推进指纹协议 + 处理终态- IDLE 下检测活体触摸 → 触发指纹匹配
- 定时态倒计时
g_timer Buzzer_Tick()/Led_Tick()推进音效与闪烁
所有模块都是非阻塞的——没有 HAL_Delay 阻塞主循环(除 EEPROM 写入 5 ms、OLED_Init 上电 50 ms 这类一次性初始化)。状态机的"等待"用 g_timer 倒计时实现。
6. 应用层模块详解
6.1 app_config.h — 共享配置
集中放置跨模块共享的引脚宏与时间常量,调整时序只需改这里:
| 常量 | 值 | 含义 |
|---|---|---|
PASSWORD_LEN | 6 | 密码位数 |
KEY_DEBOUNCE_MS | 20 | 按键消抖 |
KEY_LONG_PRESS_MS | 1500 | 长按阈值 |
UNLOCK_OK_TIME_MS / UNLOCK_FAIL_TIME_MS | 3000 / 1000 | 解锁成功绿 / 失败红 |
PWD_CHANGED_TIME_MS / PWD_ERR_TIME_MS | 2000 / 1000 | 改密成功 / 错误 |
FP_ADD_OK_TIME_MS / FP_ADD_FAIL_TIME_MS | 2000 / 2000 | 录入成功 / 失败 |
FP_ADD_TIMEOUT_MS | 180000 | 录入总超时 3 min |
LED_BLINK_TOGGLE_MS | 500 | LED 闪烁半周期 |
FP_MAX_TEMPLATES | 30 | 指纹库容量 |
还定义了 FP_TOUCH_PIN(PA11) 与 EEPROM_WP_PIN(PA12) 这两个 CubeMX 未直接给宏的引脚,以及 LED_R_PIN/PORT、LED_G_PIN/PORT。
6.2 app_key — 按键扫描
app_key.c / app_key.h
- 12 键轮询,积分式消抖:连续 20 ms 读到按下才确认,松开递减。
- 每键产生三种事件(
key_event_t):KEY_EVT_PRESS:按下边沿(数字键、*清除用)KEY_EVT_SHORT:释放且未达长按阈值(#确认用)KEY_EVT_LONG:持续按住达 1500 ms(#改密、*录指纹用)
- 事件进 16 深度 FIFO,
Key_Poll()取出。KEY_0..KEY_9的枚举值即对应数字 0..9,便于直接当密码位用。
6.3 app_buzzer — 蜂鸣器音效
app_buzzer.c / app_buzzer.h
- TIM3_CH4(PB1) PWM,PSC=71 → 1 MHz 计数 tick;改 ARR 变调,CCR4=半占空比,CCR4=0 静音。
- 7 种音效(
buzzer_sound_t),各自一张{频率, 时长}节奏表,Buzzer_Play()非阻塞启动,Buzzer_Tick()推进:SND_KEY(短滴)、SND_UNLOCK_OK(上行三音)、SND_UNLOCK_FAIL(低沉双音)、SND_PWD_CHANGED(明亮三音)、SND_PWD_ERR(低短)、SND_FP_OK(上行四音)、SND_FP_FAIL(低沉三音)
6.4 app_led — LED 指示
app_led.c / app_led.h
- 普通 GPIO(PA6 红 / PA15 绿),共阳接法:
GPIO_PIN_RESET=亮、GPIO_PIN_SET=灭。 - 三种模式
LED_OFF / LED_ON / LED_BLINK;闪烁由Led_Tick()每 500 ms 翻转。
6.5 app_oled — OLED 显示
app_oled.c / app_oled.h
- SSD1306 128×64,软件 I2C(SCL=PB0 / SDA=PA7,开漏,器件地址 0x78)。
- 内置 6×8 ASCII 子集字模(列优先取模),绘制时 2×2 放大 → 屏上每字符 12×16,步进
OLED_CHAR_STEP=14(含间距防粘连)。 - 1 KB 显存
s_gram[8][128],按页(page 0..7)整页刷新。 - API:
OLED_Init()/OLED_Clear()/OLED_Puts(col, page, str)。Puts自动刷新page与page+1两页。
6.6 app_eeprom — 断电存储
app_eeprom.c / app_eeprom.h
- AT24C02 经 I2C2(器件地址 0xA0),WP=PA12 控制写保护。
- 存储布局:
[0]=魔数 0xA5(有效性标志)、[1..6]=6 位密码、[7]=已录入指纹个数。 - 接口:
EEPROM_LoadPassword/SavePassword、EEPROM_LoadFpCount/SaveFpCount。写入前拉低 WP 解保护、写后HAL_Delay(5)等页写完成、再恢复保护。
7. 状态机(app_lock.c)— 项目核心
app_lock.c 是唯一的状态机,9 个状态:
IDLE ──输入6位+#确认──▶ UNLOCK_OK(绿3s) / UNLOCK_FAIL(红1s) ──▶ IDLE
├──不足6位按#────▶ UNLOCK_FAIL(红1s + 清零) ─────────────▶ IDLE
├──长按#─────────▶ CHANGE_PWD(绿闪) ──6位+#──▶ CHANGE_PWD_OK(绿2s) ──▶ IDLE
│ ├──位数/重复──▶ CHANGE_PWD_ERR(红1s) ──▶ CHANGE_PWD
├──长按*─────────▶ ADD_FP(绿闪,3min) ──成功/超时──▶ ADD_FP_OK(绿2s)/ADD_FP_FAIL(红2s) ──▶ IDLE
└──活体+指纹匹配──▶ UNLOCK_OK / UNLOCK_FAIL(同密码解锁)1
2
3
4
5
6
2
3
4
5
6
核心规则(对应需求):
- 显示态 / 修改态 / 录入态 忽略按键,且指纹不可解锁(需求 4/6/7)。
- 仅 IDLE 可输入密码,同时允许指纹匹配解锁(需求 7)。
- 解锁成功 清除已输入密码(需求 8)。
- 密码 不允许重复数字(需求 1,改密时校验)。
- OLED 在每次状态切换时刷新:IDLE 显示
------占位,输入时实时变***掩码(需求 10),成功 / 失败显示OK/ERR(需求 11),改密显示CHG、录入显示FP。
关键变量:g_state(当前状态)、g_timer(定时态剩余 ms)、g_password(有效密码)、g_input(解锁缓冲)、g_newpwd(改密缓冲)、g_fp_next(下一个录入槽位,断电保存)、g_fp_armed(活体一次匹配使能)。
enter_state() 是状态入口的统一函数:设置 LED / 蜂鸣器 / 定时器 / OLED 显示,集中管理副作用,便于阅读。
8. 指纹驱动(fingerprint.c)
fingerprint.c / fingerprint.h,分两层:
协议层(沿用涂鸦 SDK 参考命名):
- 帧格式
EF 01 FF FF FF FF <包类型> <长度> <负载> <和校验2B>。 - 常量命令表(采图 0x01/0x29、生成特征 0x02、合并 0x05、搜索 0x04、存储 0x06)。
fp_syno_protocol_parse()是字节级 FSM 解析器,在 USART1 接收中断里逐字节喂入。
驱动层(项目自有,单任务非阻塞):
FP_Tick()步进状态机,s_step标记当前等什么应答。- 匹配流程:采图 → 生成特征 → 1:N 搜索
- 录入流程:采图(2 次,中间要求手指抬起) → 合并 → 存储
- 每步有超时(
s_wait),超时abort_op(),录入失败 →FP_ENROLL_FAIL。 - 串口:
HAL_UART_Transmit发送;HAL_UART_Receive_IT单字节中断接收,在HAL_UART_RxCpltCallback里解析并重新启动下一次接收。
对外接口:FP_Init / FP_TouchActive / FP_RequestMatch / FP_RequestEnroll / FP_Reset / FP_Tick,状态机通过 FP_Tick() 返回的 fp_state_t(FP_MATCH_OK / FP_NO_MATCH / FP_ENROLL_OK / FP_ENROLL_FAIL / FP_ERR)感知终态。
⚠️ ZW101 出厂波特率常为 57600,CubeMX 默认 115200,需在 CubeMX 改 USART1 波特率并勾选 USART1 global interrupt(NVIC),否则接收中断无法触发。
9. 构建与烧录
工具链(arm-none-eabi-gcc / ninja / cmake)不在系统 PATH,用 STM32CubeIDE 自带 bundle:
bash
export PATH="/c/Users/jlc/AppData/Local/stm32cube/bundles/gnu-tools-for-stm32/13.3.1+st.9/bin:/c/Users/jlc/AppData/Local/stm32cube/bundles/ninja/1.13.2+st.1/bin:/c/Users/jlc/AppData/Local/stm32cube/bundles/cmake/4.2.3+st.1/bin:$PATH"
cmake -S . -B build/Debug # 首次或结构变化时
ninja -C build/Debug # 增量编译1
2
3
2
3
产物 build/Debug/project.elf,用 STM32CubeProgrammer 烧录。新增 .c 源文件需登记到 CMakeLists.txt 顶层 target_sources。
STM32CubeIDE for Visual Studio Code集成了代码编译与生成,可直接使用,该插件的安装与使用可以在哔哩哔哩上搜索官方教学视频查看,非常简单;
10. 推荐阅读顺序
- 需求.md — 了解产品行为。
- app_config.h — 全局常量。
- main.c — 主循环与节拍调度。
- app_lock.c — 核心,先看状态图注释与
enter_state(),再看on_idle()/on_change_pwd()两个事件处理函数。 - 各外设驱动(app_key / app_buzzer / app_led / app_oled / app_eeprom)按需查阅。
- fingerprint.c — 指纹协议(较独立,最后看)。
11. 常见维护场景
| 需求变更 | 改哪里 |
|---|---|
| 调整 LED / 蜂鸣器 / 解锁等待时长 | app_config.h 时序常量 |
| 增加新音效 | app_buzzer.c 加节奏表 + 枚举 |
| 显示新字符 | app_oled.c FONT_6X8 字模表(列优先,bit0=顶行) |
| 改状态机逻辑 / 增加状态 | app_lock.c 的 enter_state() + 对应 on_* 处理函数 |
| 改引脚 | CubeMX 改 .ioc → 重新生成;应用层引脚宏改 app_config.h |
| 指纹通信不通 | 检查 USART1 波特率(57600)与 NVIC 全局中断是否使能 |