KEIL 分散加载 UNINIT 实现软件复位后保留非初始化数据
问题背景
在 C 语言中,static 关键字使变量在每次程序执行时只初始化一次。但软件复位(不断电,例如调用 NVIC_SystemReset())会触发一次新的程序执行,启动代码重新运行,所有全局变量和 static 变量都会被重新初始化(.data 段从 Flash 拷贝、.bss 段清零),之前保存的状态随之丢失。
为什么不用 Flash 保存
最常见的做法是在复位前把关键数据写入 Flash,复位后从 Flash 读回。这在功能上可行,但存在两个问题:
- 耗时:Flash 扇区擦除通常需要毫秒级时间,加上写入操作,在工控或实时控制场景中可能错过控制周期。
- 寿命:频繁擦写会加速 Flash 磨损。
如果 RAM 在软件复位后内容不会丢失(大多数 MCU 的 SRAM 在软件复位后保持原值),那么直接让启动代码跳过对某块 RAM 的初始化,就能以零额外开销保留数据。
原理:UNINIT 关键字
KEIL 的分散加载文件(Scatter File)中,UNINIT 关键字告诉链接器:该执行区域不生成加载镜像(即 .axf/.elf 中不包含该区域的初始化数据),启动代码因此不会对其执行拷贝或清零。只要 RAM 本身在软件复位后内容不变,该区域内的变量就会保留复位前的值。
注意:
UNINIT只影响链接器生成的加载镜像和启动代码的初始化行为,并不改变 RAM 硬件本身。如果发生掉电复位或硬件复位,RAM 内容通常不可预测,此时该区域内的值同样会丢失。
实现步骤
1. 修改分散加载文件
在 .sct 文件中,于已有的 RAM 区域之后(或之前,视地址规划而定)新增一个 UNINIT 区域。以下示例在 0x1000A000 处划出 0x2000(8 KB)的 MYRAM 段,段内变量归属 NO_INIT 选择器:
LR_IROM1 0x00000000 0x00080000 { ; load region size_region
ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x10000000 0x0000A000 { ; RW data
.ANY (+RW +ZI)
}
MYRAM 0x1000A000 UNINIT 0x00002000 {
.ANY (NO_INIT)
}
}几点说明:
0x1000A000紧接在RW_IRAM1(0x10000000,大小0x0000A000,即 40 KB)之后,确保不与已使用的 RAM 重叠。请根据自己项目的实际 RAM 布局调整地址和大小。UNINIT 0x00002000表示该执行区域大小为 8 KB,且链接器不为其生成加载镜像。.ANY (NO_INIT)表示所有被标记为NO_INIT段的变量都会放入此区域。
2. 用 __attribute__ 将变量放入 NO_INIT 段
在 C 代码中,使用 __attribute__((section("NO_INIT"))) 将需要跨复位保留的变量指定到 NO_INIT 段:
<原文中此处代码不完整,实际写法形如
static uint32_t g_counter __attribute__((section("NO_INIT"))) = 0;。注意:由于该区域不生成加载镜像,启动代码不会写入初始值,C 代码中写的= 0在首次上电时不会生效,变量内容是 RAM 中的随机值。
3. 处理首次上电与复位的区分
因为 UNINIT 区域在首次上电时内容是随机的,需要一种机制区分"第一次上电"和"软件复位后"。常见做法是在 NO_INIT 区域中放一个魔数(magic number):
- 首次上电时,检查魔数是否等于预期值;若不等,说明是首次运行,将魔数写入预期值并初始化所有变量。
- 软件复位后,魔数仍然正确,直接沿用变量当前值即可。
风险与边界
- 地址冲突:
UNINIT区域的地址和大小必须与项目中其他 RAM 段(堆、栈、外设寄存器映射等)不重叠,否则会造成难以排查的数据损坏。修改分散加载文件后务必检查链接器输出的.map文件,确认各段地址无交叉。 - 仅适用于软件复位:掉电、看门狗硬件复位、外部复位引脚触发等情况下,RAM 内容不保证保留,
UNINIT区域同样会丢失数据。 - 首次上电值不可控:
UNINIT区域在冷启动时是随机值,必须通过魔数或类似机制做首次初始化,否则程序行为不确定。 - 变量生命周期:放入
NO_INIT段的变量不应被memset、memcpy等批量操作意外覆盖。如果启动代码或库函数对整块 RAM 做清零,需确认其范围不包含该区域。 - 多核 / 多任务场景:如果系统有多个核心或 RTOS 任务,需确保复位后各核心/任务对该区域的访问时序正确,避免读到半更新状态。
- 本文代码未经实际编译和硬件验证,地址、大小和段名需根据具体 MCU 和工程调整。
验证建议
- 在
NO_INIT区域中放一个计数器,每次主循环递增。 - 触发一次软件复位,观察计数器是否从复位前的值继续递增(而非归零)。
- 断电重新上电,确认魔数机制生效(计数器回到初始值)。
- 检查
.map文件,确认NO_INIT段地址与堆、栈、其他数据段无重叠。 - 在边界情况下(如连续多次快速复位)验证数据一致性。
小结
利用 KEIL 分散加载的 UNINIT 关键字,可以在 RAM 中划出一块启动代码不初始化的区域,使软件复位后变量值自然保留,避免了 Flash 读写带来的时间开销和寿命消耗。实现上只需修改 .sct 文件并用 __attribute__((section(...))) 标记变量,改动量很小。但需要注意地址规划、首次上电初始化以及仅适用于软件复位这几个关键边界条件。
本文最初于 2023/2/19 发布在 CSDN。
相关文章
评论
正在读取评论…