【√】以后复位芯片,数据再也不会丢了(实战篇)
1、聊一聊
2、分散加载
分散加载-"scatter-loading",字面上意思就是分开运行的意思,让程序中各个不同的部分在合适的物理存储器上运行。当我们各个文件编译生成了各种目标文件以后其中包含了程序的各种段,如代码段、数据段、.bss段等等,我们把这些段一般叫做输入段,连接器把属性相同的输入段组织成相应的输出段和域就形成了映像文件,如下图所示:
上图是作者从arm编译器用户手册中截取的编译器如何通过各个编译工具最终生成对应的平台二进制文件等格式的过程,那么究竟分散加载能够带来哪些具体的好处:
首先要实现目标地址内存的精确定位,这种是最基础的功能。
由于我们的单板一般会有多种物理存储,比如RAM,FLASH,快速SRAM等等,不同的代码可以放到不同的物理存储上运行从而实现内存最大的利用率。
用户可以灵活的确定或者规划相应代码或者数据在内存中运行的位置,从而也就实现了代码的分割,一方面可以简化烧录的代码(比如省去ZI区域内存),另一方面可以更加容易修改代码块,比如进行快速在线升级过程。
那么为了满足如此复杂的地址映射过程,ARM编译器链接过程一般会根据分散文件scatter(.scf文件)进行对应的载入地址和运行地址的映射。
3、一个映像实例
为了帮助大家理解,我们首先看看stm32默认的scatter_File文件:
分析一下:
其默认scatter_File中简单的就分为了IROM和IRAM区域,从地址来看IROM区域对应着Flash地址,而IRAM区域对应着SRAM地址,这里的SRAM进行了分割。
其中IROM区域包括这RO和XO;IRAM区域包括了RW和ZI,并且大家可以看到第15行UNINIT表示对该域不进行初始化的意思,这对后面我们设计冷启动的时候会使用到。
对于该文件的修改一方面可以通过在对应IDE中进行,同时也可以根据编译器手册进行对应的语法修改,从而映射到不同的地址中,该部分篇幅有点长,后续作者再安排上。
RW、ZI、RO、XO
通过上表大家应该发现跟我们之前介绍C语言里面的变量存储方式应该是比较接近了,既然讲到这里就顺便说一下大家用KEIL每次编译输出的这句话:
分析一下:
Code表示的是代码段的大小;RO-data表示只读数据段大小;RW-data表示已经初始化的可读可写数据段;ZI-data表示未初始化或者初始化为0的数据。
那么我们之前的文章提到过其已初始化的数据区在程序运行时会把数据搬运到对应的SRAM中执行,(具体过程也可以参考下图),并且对对应的ZI区域进行清零处理,那么程序最终占用的内存大小=RW+ ZI。
由于ZI是运行过程中进行清零处理的,所以没有必要存储在固件中,最终的固件大小,也就是写入到FLASH上的Bin文件大小=code + RW+ RO,并且此区域会映射到对应的scatter文件中。
4、热启动数据恢复过程实验
前面简单的讲了很多知识点都是为了热启动实例进行铺垫的,其实我们每个简单的现象背后都隐藏着非常多牵连的知识点,只是说大家该怎么去挖掘的问题,所以大家以后看到<最后一个bug>发布的标题都很简单也可以进来看一下,或许会有新的收获,下面我就以stm32为大家讲解一下该过程。
1)设计总体概图
2)去掉自动清除ZI
根据上一篇文章中我们非掉电复位需要获取RAM中的数据信息,那么在运行到main函数之前不能够清除掉内存,而stm32默认把对应的ZI区域清零处理,所以我们需要让scatter_File文件不对ZI初始化,在Keil中的Target中把右侧的NoInit划上√。
3)定位识别变量
从前面我们知道使用分散加载可以实现对应变量或者函数的地址固定化,那么我们通过编译器支持的语法把对应的变量定位到相应的地址,便可以在下一次上电直接定位该变量,如果其值变化了便为掉电复位-冷启动否则为热启动.
参考代码:
1#include "led.h"
2#include "delay.h"
3#include "sys.h"
4
5#define COLD_MODE (0) //冷启动
6#define HOT_MODE (1) //热启动
7
8uint32_t variable __attribute__((at(0x2000B000))); // Place at 0x2000B000
9 int main(void)
10 {
11 uint8_t StartMode = 0;
12 delay_init();
13 LED_Init();
14 uart_init(115200);
15 printf("--->欢迎关注公众号:最后一个bug\r\n");
16
17 if(variable != 0x12345678)
18 {
19 variable = 0x12345678;
20 StartMode = 0;
21 printf("--->启动方式:冷启动\r\n");
22 }
23 else
24 {
25 StartMode = 1;
26 printf("--->启动方式:热启动\r\n");
27 }
28
29 while(1)
30 {
31 if(StartMode == HOT_MODE)
32 {
33 //红灯闪烁
34 LED0 = 0;
35 delay_ms(300);//延时300ms
36 LED0 = 1;
37 delay_ms(300);//延时300ms
38 }
39 else
40 {
41 //绿灯闪烁
42 LED1 = 1;
43 delay_ms(300);//延时300ms
44 LED1 = 0;
45 delay_ms(300);//延时300ms
46 }
47
48 }
49 }
输出结果:
首先上电启动芯片输出1、2行,然后按下复位按钮以后获得3、4两行,这样是符合预期的想法的。
4)优化
上一篇文章中我们说过,如果引起复位的原因导致了RAM里面的数据被破坏,那么将获得不正确数据,那么我们如何在判断数据是否被破坏了呢?处理该问题相对比较困难,毕竟内存并不能像平时存储EEPROM数据那样把校验数据也进行保存,读出来的时候进行检测,如果这样做的话实在是太浪费CPU了。
那么我们会选用一种折中的办法,在我们内存的各个部分均匀的分布定位值,在热启动以后根据多个定位置进行判断,然后大体判断是否部分区域被破坏。
5、最后小结
这里仅仅是为大家展示了一个非常小的应用,后续容错处理和用户体验还有更加复杂的设计,大家可以多去了解一下程序的编译链接过程和分散加载技术,比如说我们现在想把程序加载到对于的RAM里面运行,那我们又该如何编译和设计程序呢?不太熟悉的小伙伴先研究研究。
好了,这里是公众号:“最后一个bug”,一个为大家打造的技术知识提升基地。同时非常感谢各位小伙伴的支持,我们下期精彩见!
推荐好文 点击蓝色字体即可跳转