三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

利用 STM32F407 BKPSRAM 实现运行时变量监控 —— 从方案到 Keil 调试实战

利用 STM32F407 BKPSRAM 实现运行时变量监控 —— 从方案到 Keil 调试实战

利用 STM32F407 BKPSRAM 实现运行时变量监控 —— 从方案到 Keil 调试实战

一、什么是 BKPSRAM

1.1 先看一张图

STM32F407 的存储系统里有一个很特别的区域叫备份域(Backup Domain)。备份域里住着几个东西:

┌──────────────────────────────────────┐ │ STM32F407 备份域 │ ├──────────────────────────────────────┤ │ RTC 备份寄存器(80 字节) │ 纽扣电池供电可保持 │ BKPSRAM(4KB @ 0x40024000) │ VDD 不掉就保持 │ RTC 日历寄存器 │ └──────────────────────────────────────┘

BKPSRAM 全称Backup SRAM,4KB 大小,挂在 AHB1 总线上,起始地址0x40024000。它的核心特性是:

只要芯片的 VDD 没断电,数据就一直保持——哪怕是 CPU 复位、看门狗复位、软件复位都不丢。

注意,MCU上电Backup SRAM内的数据是随机的。

1.2 和 Flash / RTC 备份寄存器的对比

方案容量写入速度复位保持断电保持写入寿命适合场景
Flash 末扇区16KB+慢(~50μs/字)~10K 次擦除偶尔记录
RTC 备份寄存器80B极快需电池无限少量关键变量
BKPSRAM4KB极快需电池 / VDD无限实时日志

BKPSRAM 的杀手锏是:写操作不阻塞 CPU,不影响 Modbus 这种实时通信的时序。

1.3 上电初始化三件套

要使用 BKPSRAM,芯片启动后必须按顺序完成三步使能:

// 第一步:开 AHB1 总线上的 BKPSRAM 时钟__HAL_RCC_BKPSRAM_CLK_ENABLE();// 第二步:使能备份域写访问(否则写不进去)HAL_PWR_EnableBkUpAccess();// 第三步:使能备份域内部调压器(F4 特有,漏掉这步 BKPSRAM 不可用!)HAL_PWREx_EnableBkUpReg();

第三步是最容易踩的坑。不加这行,BKPSRAM 读出来全是乱码。


二、方案设计

2.1 问题场景

假设你维护着一个跑 Modbus 协议的嵌入式设备,设备偶尔会异常复位。你想知道:

  • 复位前哪个全局变量被改过?
  • 什么时候改的?
  • 改之前是多少?改之后是多少?

传统的做法是守着 Keil 的 Watch 窗口设断点,但设备可能跑几个小时甚至几天才出一次问题。而且看门狗复位后,SRAM 里的变量全部丢失。

2.2 日志条目设计

每条日志定长 16 字节,四个字段:

typedefstruct{uint32_ttick;// HAL_GetTick() 时间戳 — "什么时候"uint32_taddr;// 被监控变量的地址 — "谁被改了"uint32_told_val;// 修改前的值 — "从多少"uint32_tnew_val;// 修改后的值 — "改成多少"}BfarEntry;

为什么用变量的地址而不是变量名?因为地址在 map 文件里能反查到对应的符号名,比存字符串高效得多。

2.3 BKPSRAM 内存布局

4KB 的空间分成头部(16 字节)和日志区(4080 字节,最多 255 条):

BKPSRAM 基址: 0x40024000 0x000 Magic 0xBFA70001 ← 冷/热启动哨兵 0x004 WriteHead 下一条写入位置 0x008 OverflowCount 写满回绕次数 0x00C Reserved 保留 ────────────────────── 0x010 Entry[0] 16 bytes 0x020 Entry[1] 16 bytes ... 0xFF0 Entry[254] 16 bytes

2.4 冷启动 vs 热复位

关键设计:用0xBFA70001这个 Magic 值区分两种场景:

上电 / 复位 │ ├─ 读 Magic == 0xBFA70001 ? │ │ │ ├─ 否 → 冷启动:写 Magic,Init 头部,日志从零开始 │ │ │ └─ 是 → 热复位:VDD 没断过,旧日志保留!

这样一来,看门狗复位后你还能在 Keil 里回读复位前的历史日志。

2.5 写满回绕

255 条日志肯定不是无限的。写满 4KB 后(WriteHead + 16 > 4096),自动回绕到第一条(0x10),OverflowCount 加一。这是典型的环形缓冲区策略。

2.6 代码结构

只新增了两个文件,侵入性极低:

MDK-ARM/User/ ├── bfar.h ← 结构体 + API 声明 + 便捷宏 └── bfar.c ← Bfar_Init() + Bfar_Log() + Bfar_Erase()

main.c中只加了一行Bfar_Init()调用。不在任何已有模块中自动埋点,日志调用由你手动加在你关心的变量赋值处。

这里先放上所有的代码
bfar.c如下

/* ── BKPSRAM 变量监控日志 实现 ───────────────────────────────────────── * * BKPSRAM 内存布局(4KB @ 0x40024000): * * 偏移 大小 字段 说明 * ────────────────────────────────────────────────────── * 0x000 4B Magic 冷/热启动哨兵值 0xBFA70001 * 0x004 4B WriteHead 下一条目的写入偏移(初始 0x10) * 0x008 4B OverflowCount 写满回绕次数 * 0x00C 4B Reserved 保留 * 0x010 16B Entry[0] 第 1 条日志 * 0x020 16B Entry[1] 第 2 条日志 * ... ... ... ... * 0xFF0 16B Entry[254] 第 255 条(最后一条) * * 写入流程: * 1. 检查 WriteHead + 16 是否超出 4KB → 超出则回绕到 0x10 * 2. 在 bfar + WriteHead 处写入 Entry * 3. WriteHead += 16,写回头部 * * 读取方法(Keil Debug): * Memory 窗口输入 0x40024000 * 每 16 字节一条日志,小端序 4 字节一组反着读 * ────────────────────────────────────────────────────────────────────*/#include"bfar.h"#include"stm32f4xx_hal.h"/* BKPSRAM 基址与容量(STM32F407) */#defineBFAR_BASE((uint32_t)0x40024000)#defineBFAR_SIZE(4096)/* 头部各字段在 BKPSRAM 中的偏移 */#defineBFAR_OFF_MAGIC0x00/* 哨兵值,4 字节 */#defineBFAR_OFF_WRITEHEAD0x04/* 写入位置,4 字节 */#defineBFAR_OFF_OVERFLOW0x08/* 溢出计数,4 字节 */#defineBFAR_OFF_RESERVED0x0C/* 保留,4 字节 */#defineBFAR_OFF_FIRST_ENTRY0x10/* 第一条日志的偏移 */#defineBFAR_ENTRY_SIZE(16)/* 每条日志 16 字节 */#defineBFAR_MAX_ENTRIES255/* (4096 - 16) / 16 *//* 哨兵值:冷启动时写入,复位后校验,确认日志是否来自本次上电 */#defineBFAR_MAGIC0xBFA70001/* 直接指针映射 BKPSRAM 地址空间 */staticuint8_t*constbfar=(uint8_t*)BFAR_BASE;/* ── 上电初始化 ────────────────────────────────────────────────────── * 1. 使能 BKPSRAM 时钟(AHB1 总线) * 2. 使能备份域写访问 * 3. 使能备份域内部调压器(F4 必须,否则 BKPSRAM 不可用) * 4. 读 Magic:!= 0xBFA70001 → 冷启动,初始化头部 * == 0xBFA70001 → 热复位,保留旧日志 * ────────────────────────────────────────────────────────────────────*/voidBfar_Init(void){uint32_tmagic;/* 三步使能 BKPSRAM */__HAL_RCC_BKPSRAM_CLK_ENABLE();/* AHB1 时钟门控 */HAL_PWR_EnableBkUpAccess();/* 备份域写使能 */HAL_PWREx_EnableBkUpReg();/* 备份域调压器(F4) *//* 读哨兵值判断冷/热启动 */magic=*(uint32_t*)(bfar+BFAR_OFF_MAGIC);if(magic!=BFAR_MAGIC){/* 冷启动:Magic 不对,写头部并清空日志区 *//* 任何读取日志的人应该只看 WriteHead 之前的内容,之后的字节不管是随机值还是旧数据都不应被信任 */*(uint32_t*)(bfar+BFAR_OFF_MAGIC)=BFAR_MAGIC;*(uint32_t*)(bfar+BFAR_OFF_WRITEHEAD)=BFAR_OFF_FIRST_ENTRY;*(uint32_t*)(bfar+BFAR_OFF_OVERFLOW)=0;*(uint32_t*)(bfar+BFAR_OFF_RESERVED)=0;}/* 热复位:Magic 匹配,WriteHead 不变,日志保留 */}/* ── 记录一条日志 ──────────────────────────────────────────────────── * 参数: * addr - 被修改变量的地址(方便在 map 文件中反查变量名) * old_val - 修改前的值 * new_val - 修改后的值 * * tick 自动从 HAL_GetTick() 获取。 * 写满 4KB 后自动回绕到第一条,OverflowCount++。 * ────────────────────────────────────────────────────────────────────*/voidBfar_Log(uint32_taddr,uint32_told_val,uint32_tnew_val){uint32_twrite_head;uint32_toverflow;BfarEntry entry;/* 读取当前写入位置和溢出计数 */write_head=*(uint32_t*)(bfar+BFAR_OFF_WRITEHEAD);overflow=*(uint32_t*)(bfar+BFAR_OFF_OVERFLOW);/* 空间不够:回绕到日志区开头 */if(write_head+BFAR_ENTRY_SIZE>BFAR_SIZE){write_head=BFAR_OFF_FIRST_ENTRY;overflow++;*(uint32_t*)(bfar+BFAR_OFF_OVERFLOW)=overflow;}/* 填充条目并写入 BKPSRAM */entry.tick=HAL_GetTick();entry.addr=addr;entry.old_val=old_val;entry.new_val=new_val;*(BfarEntry*)(bfar+write_head)=entry;/* 推进 WriteHead,写回头部 */write_head+=BFAR_ENTRY_SIZE;*(uint32_t*)(bfar+BFAR_OFF_WRITEHEAD)=write_head;}/* ── 擦除全部日志 ──────────────────────────────────────────────────── * 将 Magic 清零后立即调用 Bfar_Init(),效果等同于冷启动重建。 * 不需要复位,调用后日志区立即可用(从头开始写)。 * ────────────────────────────────────────────────────────────────────*/voidBfar_Erase(void){*(uint32_t*)(bfar+BFAR_OFF_MAGIC)=0;Bfar_Init();}

bfar.h如下

#ifndef_BFAR_H#define_BFAR_H#include<stdint.h>/* ── BKPSRAM 变量监控日志 ───────────────────────────────────────────── * * 将全局变量的每次修改记录到备份域 SRAM(STM32F407: 4KB @ 0x40024000)。 * VDD 不掉电则数据不丢失,可抵抗看门狗 / CPU 复位。 * * Memory 窗口查看方法: * Keil Debug → View → Memory → 输入 0x40024000 * 每行 16 字节,前 16 字节是头部,之后每条日志 16 字节。 * 小端序:4 字节一组倒过来读。或用 DD 0x40024010 直接解析。 * ────────────────────────────────────────────────────────────────────*//* 日志条目结构体,定长 16 字节 */typedefstruct{uint32_ttick;/* 时间戳,HAL_GetTick() 返回值,单位 ms */uint32_taddr;/* 被监控变量的地址,告诉你"谁被改了" */uint32_told_val;/* 修改前的值 */uint32_tnew_val;/* 修改后的值 */}BfarEntry;/* 上电初始化:开 BKPSRAM 时钟、使能备份域、校验 Magic 判断冷/热启动 */voidBfar_Init(void);/* 记录一条日志。tick 自动填充,调用者只需提供 addr / old_val / new_val */voidBfar_Log(uint32_taddr,uint32_told_val,uint32_tnew_val);/* 擦除全部日志:清 Magic 后立即重新初始化 */voidBfar_Erase(void);/* 便捷宏:赋值并记录,一行搞定 * * 用法:BFAR_TRACK_WRITE(g_pHoldingRegs->control_mode, 3); * * 展开后等价于: * uint32_t _old = g_pHoldingRegs->control_mode; * g_pHoldingRegs->control_mode = 3; * Bfar_Log(&g_pHoldingRegs->control_mode, _old, 3); */#defineBFAR_TRACK_WRITE(var,new)do{\uint32_t_old=(uint32_t)(var);\(var)=(new);\Bfar_Log((uint32_t)&(var),_old,(uint32_t)(new));\}while(0)#endif/* _BFAR_H */

三、实际演示

3.1 工程背景

  • 平台:STM32F407ZGTx(Cortex-M4,168MHz)
  • 固件:FreeModbus RTU 从机,10 种 Modbus 功能码全开
  • 通信:USART1(PA9/PA10),115200 8N1,无校验
  • 编译:Keil MDK (ARMCC V5.06)
  • 烧录/调试:OpenOCD + ST-Link (SWD)

3.2 写测试日志

main.cwhile(1)之前写两条测试日志:

// 测试条目1:记录一次对地址 0x20000000 的写入Bfar_Log(0x20000000,0x11111111,0x22222222);// 测试条目2:记录 control_mode 变量的真实修改uint32_told=g_pHoldingRegs->control_mode;g_pHoldingRegs->control_mode=3;Bfar_Log((uint32_t)&g_pHoldingRegs->control_mode,old,3);

也可以用便捷宏一行搞定:

BFAR_TRACK_WRITE(g_pHoldingRegs->pid_kp,0x0100);// 展开后自动记录旧值、新值、变量地址

编译烧录,设备正常运行。

3.3 进入 Keil Debug

进入 Keil Debug 模式用 Memory 窗口查看:

Keil → View → Memory Windows → Memory 1 地址栏输入:0x40024000

3.4 解读 Memory 窗口数据

Memory 窗口第一行(每行 16 字节):

0x40024000: 01 00 A7 BF 30 00 00 00 00 00 00 00 00 00 00 00 ───┬─────── ───┬─────── ───┬─────── ───┬─────── Magic WriteHead Overflow Reserved

头部解析(STM32F407 是小端序,4 字节要倒过来读):

偏移原始字节小端序还原说明
0x0001 00 A7 BFBF A7 00 010xBFA70001Magic 正确,已初始化
0x0430 00 00 0000 00 00 300x30 = 48已写入 2 条日志
0x0800 00 00 000无溢出

WriteHead = 0x30,说明当前有两条记录:Entry[0] 在 0x010,Entry[1] 在 0x020。


第二行是第一条日志:

0x40024010: 78 01 00 00 00 00 00 20 11 11 11 11 22 22 22 22 ───┬─────── ───┬─────── ───┬─────── ───┬─────── tick addr old_val new_val
字段原始字节小端序还原解读
tick7801 00 000x0000017A上电后 378ms 记录的
addr00 00 00 200x20000000第一个测试用的假地址
old_val11 11 11 110x11111111旧值
new_val22 22 22 220x22222222新值

和代码中Bfar_Log(0x20000000, 0x11111111, 0x22222222)完全吻合。


第三行是第二条日志:

0x40024020:7A 01 00 00 F0 00 00 20 01 00 00 00 03 00 00 00 ───┬─────── ───┬─────── ───┬─────── ───┬─────── tick addr old_val new_val
字段原始字节小端序还原解读
tick7A 01 00 000x0000017A = 378上电后 378ms 记录的
addr00 00 00 200x20000000第一个测试用的假地址
old_val11 11 11 110x11111111旧值
new_val22 22 22 220x22222222新值

和代码完全吻合。

g_pHoldingRegs->control_mode=1;uint32_told=g_pHoldingRegs->control_mode;g_pHoldingRegs->control_mode=3;Bfar_Log((uint32_t)&g_pHoldingRegs->control_mode,old,3);

3.5 模拟复位验证数据保持

  1. 让设备跑一会儿,跑几条 Modbus 读写操作
  2. 手动触发软件复位(或等待看门狗复位)
  3. 不要断电,重新进入 Keil Debug
  4. 查看0x40024000—— Magic 仍然在,之前的日志仍然在

这就是 BKPSRAM 的核心价值:复位不丢数据

3.6 map 文件反查变量名

从日志中看到addr = 0x200000B4,想知道这到底是哪个变量?

打开freemodbus.map,搜索0x200000b4(注意地址小写):

grep 0x200000b4 freemodbus.map

在 map 文件中找到对应的符号名,就知道是哪个全局变量被动了手脚。


四、API 速查

函数作用
Bfar_Init()上电初始化,自动判断冷/热启动
Bfar_Log(addr, old, new)记录一条日志,tick 自动填充
Bfar_Erase()擦除全部日志,立即重建
BFAR_TRACK_WRITE(var, new)宏,赋值 + 记录一行搞定

五、提醒

  1. HAL_PWREx_EnableBkUpReg()不能省。STM32F4 备份域有个内部调压器,不使能 BKPSRAM 读写是乱的。
  2. BKPSRAM 上电数据是随机的,不能假设上电为全零。Magic 哨兵值就是用来区分随机数据和有效日志的。
  3. 断电数据会丢。如果要防物理断电,需要接 VBAT 纽扣电池,或者换 Flash 方案。
  4. Keil Debug 下载时可能覆盖 BKPSRAM。建议用 OpenOCD 单独烧录,避免 Keil 的 Download 按钮触发全片擦除。
  5. 小端序别反。Memory 窗口显示的字节顺序和数字的真实顺序是反的,4 字节数据要倒过来读。

← 返回列表