STM32C5 EEPROM Emulation 密码锁示例
本工程基于 STM32C542RCT6,使用片上 EDATA Flash 模拟 EEPROM,并实现通过 USART2 串口交互的 6 位密码锁。密码正确时点亮 PA5 上的 LD1,约 5 秒后熄灭并进入 Standby;密码保存在EDATA区域,断电不丢失。
1. 芯片与系统配置
- MCU:STM32C542RCT6。
- 时钟:HSE 24 MHz,经 PSIS 输出 144 MHz。
- HCLK/PCLK1/PCLK2/PCLK3 均为 1 分频,Flash latency 为 4 WS。
- 使用 STM32C5 HAL/LL、CMSIS、EEPROM Emulation v2.1.0。
- 构建:CMake + Ninja,预设为
debug_GCC_STM32C542RCT6。
mx_system_init() 依次初始化 HAL、NVIC、ICACHE、RCC、PWR、USART2、GPIO、TIM1、FLASH 和 CRC;关键初始化失败时 main() 返回 -1。
2. 外设设置
2.1 外设配置总览
| 外设/模块 |
关键配置 |
引脚/资源 |
主要用途 |
| RCC |
HSE 24 MHz,PSIS/SYSCLK 144 MHz;AHB/APB 均 1 分频 |
系统时钟 |
为 CPU 和外设提供时钟 |
| ICACHE |
启用,HAL_ICACHE_IT_NONE |
芯片内部 |
提高代码访问效率 |
| USART2 |
115200、8N1、无流控、TX/RX |
PA2/PA3,AF7 |
串口提示、密码输入 |
| LPDMA1_CH0 |
USART2 TX,内存到外设,字节宽度 |
USART2 TX 请求 |
DMA 发送支持 |
| LPDMA1_CH1 |
USART2 RX,外设到内存,字节宽度 |
USART2 RX 请求 |
DMA 接收支持 |
| GPIO |
低速推挽输出,无上下拉 |
PA5/LD1 |
开锁状态指示 |
| TIM1 |
预分频 14399,周期 0x1387,更新中断 |
TIM1_UPD,通道 3 |
LED 控制和开锁计时 |
| PWR |
PVD、Flash 低功耗、Wakeup Pin 4 |
Wakeup Pin 4,高电平有效 |
Standby 唤醒 |
| FLASH |
EDATA Flash 接口 |
FLASH_EDATA_BASE |
EEPROM 仿真数据存储 |
| CRC |
16 bit,多项式 0x109B,初值 0xFFFFFFFF |
CRC 外设 |
EEPROM 数据完整性校验 |
| DWT |
开启 CYCCNT |
Cortex-M 内部 |
统计 CPU 周期 |
上表是当前工程的配置速查,下面分别说明各外设的初始化方式及其在应用中的作用。
USART2 与 LPDMA1
USART2 用作密码锁终端:
| 参数 |
配置 |
| TX/RX |
PA2/PA3,AF7 |
| 波特率 |
115200 |
| 数据格式 |
8N1,无硬件流控 |
| 方向 |
TX/RX |
| 过采样 |
16 倍 |
LPDMA1_CH0 用于 USART2 TX(内存到外设),LPDMA1_CH1 用于 USART2 RX(外设到内存),均为字节传输并启用中断。工程另有 1024 字节环形发送队列和 UART_DMA_Send(),但当前 printf() 最终经 __io_putchar() 调用阻塞式 HAL_UART_Transmit();getchar() 也通过 __io_getchar() 阻塞接收。
GPIO、LED 与 TIM1
PA5 配置为低速推挽、无上下拉输出,LD1 高电平有效。由于 LED_PWM_MODE=1,LED 控制还会使用 TIM1 通道 3 输出比较。
TIM1 配置为内部时钟、向上计数,预分频 14399,周期 0x1387(4999),启用更新中断,优先级为抢占 0、子优先级 0。更新回调会:
- 调用
bsp_led_callback() 翻转 LD1;
- 调用
open_door_cunter_callback() 累计开锁计时;
- 计数达到 120 后进入 Standby。
PWR、低功耗与唤醒
mx_pwr.c 开启 PVD(EXTI line 16、上升沿)、Flash low-power mode 和 Wakeup Pin 4。Wakeup Pin 4 为高电平有效、无上下拉;同时保留 JTAG I/O(PA13、PA14、PA15、PB4)。
enter_standby() 会保存密码,清除 Wakeup Pin 4 和上次电源模式标志,然后调用 HAL_PWR_EnterStandbyMode()。主流程为:
mx_system_init -> App_Init -> 检查 Wakeup Pin 4
├─ 已唤醒:App_Run -> 开锁 -> Standby
└─ 未唤醒:直接进入 Standby
FLASH、CRC 与 DWT
FLASH 由 mx_flash.c 初始化并提供给 EEPROM 仿真层。CRC 参数为:16 bit、多项式 0x109B、初始值 0xFFFFFFFF,输入和输出均不反转。CRC 用于仿真数据帧完整性校验。
sys_DWT.c 开启 DWT CYCCNT,App_Init() 用它统计初始化提示输出的 CPU 周期;DWT 不参与密码校验或存储。
3. 工程结构
3.1 工程文件结构图
Project08_EEPROM_emulation/
├─ main.c / main.h 入口、系统初始化和主循环
├─ Application/
│ ├─ app/
│ │ ├─ app.c / app.h 密码锁业务逻辑、低功耗流程
│ │ └─ callbacks.c / callbacks.h UART/TIM 等 HAL 回调分发
│ ├─ bsp/
│ │ ├─ bsp_uart.c / bsp_uart.h USART2 输出和 DMA 发送队列
│ │ ├─ bsp_led.c / bsp_led.h LD1 控制
│ │ └─ bsp_BTN.c / bsp_BTN.h 按键消抖和事件识别
│ └─ tool/
│ ├─ sys_Data_Flash.c / sys_Data_Flash.h EEPROM 读写二次封装
│ └─ sys_DWT.c / sys_DWT.h DWT 周期计数
├─ generated/
│ ├─ hal/ RCC/PWR/GPIO/UART/TIM/FLASH/CRC 初始化
│ └─ utilities/ EEPROM 配置和初始化入口
├─ utilities/
│ ├─ eeprom_emulation/ EEPROM 核心、FLITF 算法、Flash/CRC 接口
│ └─ syscalls/ printf/getchar 底层重定向
├─ stm32c5xx_drivers/ STM32C5 HAL/LL 驱动
├─ arch/cmsis/ CMSIS 内核和设备支持
├─ user_modifiable/ 启动文件、链接脚本、构建上下文
├─ build/ CMake/Ninja 构建输出(本地生成)
├─ CMakeLists.txt / CMakePresets.json 构建配置
└─ Project08_EEPROM_emulation.ioc2 STM32CubeMX2 工程配置
生成代码负责芯片和外设初始化;Application 负责板级封装和业务逻辑;utilities/eeprom_emulation 提供通用 Flash EEPROM 仿真;sys_Data_Flash 提供业务层按虚拟地址读写的简单接口。
4. EEPROM Emulation 配置
配置文件:generated/utilities/eeprom_emul_conf.h。
| 宏 |
当前值 |
含义 |
EE_ALGO_FLITF |
1 |
使用 FLITF 算法 |
EE_FRAME_LINE_SIZE |
8 |
数据线大小 8 字节 |
EE_START_PAGE_ADDRESS |
0x09000000 |
仿真页起始地址 |
EE_FLASH_BASE_ADDRESS |
FLASH_EDATA_BASE |
使用 EDATA Flash |
EE_FLASH_PAGE_SIZE |
FLASH_EDATA_PAGE_SIZE |
EDATA 页大小 |
EE_NB_OF_VARIABLES |
1000 |
虚拟变量总数 |
EE_GUARD_PAGES_NUMBER |
1 |
保护页数量 |
EE_CYCLES_NUMBER |
1 |
仿真周期数 |
当前组合为 FLITF + EDATA + CRC;NVM、ECC 和模板接口虽在工程中保留,但未被当前配置选用。
初始化链路:
App_Init
-> sys_Data_Flash_Init
-> mx_eeprom_emulation_init
-> 获取 mx_flash_gethandle / mx_crc_gethandle
-> EE_Init(&mx_ee_object, EE_CONDITIONAL_ERASE)
EE_Init() 初始化 Flash/CRC 接口并恢复仿真页状态。EE_CONDITIONAL_ERASE 只在需要时擦除页面。FLITF 算法把变量记录追加到活动页,空间不足时搬移有效记录、擦除旧页并继续使用,用于模拟 EEPROM 更新并降低单页磨损。
业务层使用虚拟地址 1~6 保存密码的 6 个 ASCII 字节:
sys_Data_Flash_Write(1, password, 6);
sys_Data_Flash_Read(1, password, 6);
封装层实际逐字节调用 EE_WriteVariable8bits() 和 EE_ReadVariable8bits()。当写入返回 EE_INFO_CLEANUP_REQUIRED 时调用 EE_CleanUp() 回收旧页。
密码生命周期:
- RAM 初始密码为
123456;
App_Init() 初始化仿真后读取虚拟地址 1~6;
- 首次使用没有有效记录时打印读取错误,但默认数组仍可继续使用;
- 修改密码先只更新 RAM;
- 进入 Standby 前由
enter_standby() 写回地址 1~6;
- 下次唤醒或复位时重新读取。
修改变量数量、起始地址、周期数或页配置后,应重新生成工程,并确认链接脚本、EDATA 区域和仿真页范围不冲突;虚拟地址必须位于 1~EE_NB_OF_VARIABLES。
5. 密码锁使用流程
5.1 上电与唤醒
- USART2 连接串口终端:115200、8N1、无流控;
- 下载并复位程序;
- 终端打印功能说明、默认密码和改密提示;
- 初始化后系统进入 Standby;
- 对 Wakeup Pin 4 施加配置的有效高电平,唤醒系统。
5.2 正常开锁
唤醒后执行 open_locked():
- 启动 TIM1 更新中断;
- 输入
1 或其他字符选择显示方式:1 明文回显,其他字符隐藏为*;
- 输入 6 位当前密码并回车;
- 密码正确:打印成功信息、点亮 LD1,保持约 5 秒;
- 密码错误:打印失败信息并关闭 LD1;
- 打印待机提示并进入 Standby。
代码固定读取 6 次 getchar(),再清理换行;建议严格输入 6 个字符后回车,不要加入空格或额外字符。
5.3 修改密码
输入特殊密码 000000 进入改密:
- 输入
000000;
- 输入新的 6 位密码;
- 新密码不能为
000000;
- 再次输入新密码确认;
- 两次一致时打印
Password is updated! 并更新 RAM;
- 随后进入 Standby 时写入 EEPROM 仿真区;
- 两次不一致时打印
Password is not updated!,原密码不变。
修改成功不会立即单独写 Flash,而是在后续 enter_standby() 中保存;若保存前掉电,仍使用 Flash 中的原密码。
6. 构建与下载
cmake --preset debug_GCC_STM32C542RCT6
cmake --build --preset debug_GCC_STM32C542RCT6
已有构建目录时:
cmake --build build/debug_GCC_STM32C542RCT6
ELF 输出为 build/debug_GCC_STM32C542RCT6/Project08_EEPROM_emulation.elf。烧录后打开 USART2 终端即可测试。
7. 关键文件
| 文件 |
作用 |
main.c |
初始化、唤醒判断、主循环 |
Application/app/app.c |
密码校验、改密、开锁、Standby |
Application/tool/sys_Data_Flash.c |
EEPROM 读写封装 |
generated/utilities/eeprom_emul_conf.h |
EEPROM 参数 |
generated/utilities/mx_eeprom_emulation_app.c |
EE_Init() 入口 |
utilities/eeprom_emulation/core/ |
仿真核心和算法 |
utilities/eeprom_emulation/interface_flash/ |
EDATA Flash 接口 |
utilities/eeprom_emulation/interface_crc/ |
CRC 接口 |
generated/hal/mx_usart2.c |
USART2/LPDMA1 |
generated/hal/mx_tim1.c |
TIM1/更新中断 |
generated/hal/mx_pwr.c |
Standby/Wakeup 配置 |
utilities/syscalls/syscalls.c |
printf/getchar 重定向 |
8. 优化建议
8.1 优先修复的功能和可靠性问题
- 完善 EEPROM 错误处理:
- 让
sys_Data_Flash_Read/Write() 根据EE_INVALID_VIRTUALADDRESS、EE_ERROR_CORRUPTION、Flash 错误等返回真实状态;
App_Init() 读取失败时明确区分“首次使用”和“数据损坏”,必要时执行EE_Format() 或进入故障提示;
- 写入密码后校验读回值,避免 Flash 编程失败仍被当作成功。
- 修正改密保存时机:当前改密成功后先修改 RAM,只有进入 Standby 前才写 Flash。建议改密成功后立即写入并读回校验,或者增加掉电保护状态,避免保存前掉电导致新密码丢失。
- 避免在中断中执行复杂业务:
HAL_TIM_UpdateCallback() 只置位计时标志,在主循环中处理 Standby 和业务逻辑,减少中断执行时间。
- 统一返回值和参数校验:检查地址范围、长度是否为 6、空指针和 EEPROM 初始化状态,不要让底层失败最终仍返回
SYS_DATA_FLASH_OK。
8.2 串口和交互优化
- 真正启用 UART DMA 发送:让
_write() 调用UART_DMA_Send(),并处理环形缓冲区满的情况;这样连续printf() 不会逐字节阻塞 CPU。
- 增加接收状态机:目前
getchar() 固定阻塞读取 6 个字符,建议改为中断/DMA 接收 + 行缓冲,支持退格、超时、回车结束和非法字符提示。
- 统一换行格式:串口输出统一使用
\\r\\n,并为菜单、密码输入、结果和故障状态定义明确的提示文本。
- 加入输入超时和错误次数限制:长时间无输入时自动回到待机;连续输错达到阈值后锁定一段时间,降低暴力尝试风险。
8.3 低功耗与定时器优化
- 核对 Wakeup Pin 4 的实际引脚连接:README 和代码只能说明 HAL 的 Wakeup Pin 4 配置,硬件原理图仍需确认具体物理引脚和有效电平。
- 减少无意义的 TIM1 运行:仅在开锁交互或 LED 指示需要时启动 TIM1,进入待机前停止定时器并关闭无关时钟。
- 重新评估 LED 控制方式:若只需要常亮/熄灭,建议关闭
LED_PWM_MODE,直接使用 GPIO,降低代码复杂度和功耗。
- 完善唤醒原因记录:进入待机前记录状态,唤醒后读取并打印唤醒源,便于区分首次上电、Wakeup Pin 唤醒和异常复位。
8.4 EEPROM 寿命与数据设计优化
- 只在数据变化时写入:写入前比较新旧密码,密码未变化时不产生新的 EEPROM 记录。
- 为密码增加版本、长度和校验字段:可将密码记录设计为结构体,例如
magic + version + length + password + crc,便于升级和检测异常数据。
- 合理设置仿真容量:
EE_NB_OF_VARIABLES=1000 远大于当前 6 个字节的需求;应根据实际变量数量、写入频率和寿命目标计算页数,避免占用不必要的 EDATA 空间。
- 明确格式化策略:量产产品不要随意启动时格式化;应提供受控的恢复出厂流程,防止误擦除密码。
8.5 安全性优化
- 不要明文保存密码:使用硬件安全能力、密钥派生后的摘要或加密存储;同时注意仅保存摘要时仍需支持密码验证。
- 删除或保护默认密码提示:当前默认密码和改密口令直接打印在串口上,只适合演示,不适合产品。
- 防止调试口泄露:量产时评估 JTAG/SWD、读保护、调试认证和 EDATA 访问权限。
- 增加安全擦除和恢复机制:密码重置应需要物理授权或受保护命令,不能只依靠公开的
000000 特殊口令。
9. 注意事项
- 密码以 ASCII 明文保存,没有加密;CRC 只提供完整性检查,不是安全存储。
- 当前
sys_Data_Flash_Read/Write() 对底层错误主要打印信息,量产代码应补充严格错误传播。
BTN_Scan() 当前未接入主循环或定时扫描;使用实体按键需要补充 GPIO、扫描周期及到Process_Button_Event() 的连接。
mx_*.c/.h 和eeprom_emul_conf.h 通常由 STM32CubeMX2 生成,修改配置应同步修改.ioc2/组件配置。