ARM 嵌入式开发的固件内存管理
一、编译后各个数值额含义解释
linking...
Program Size: Code=48404 RO-data=3332 RW-data=212 ZI-data=10940
FromELF: creating hex file...
在嵌入式软件开发中,这些数值是对程序在内存中占用空间的统计信息,作为固件大小控制,内存管理的重要参考信息,对于优化程序性能和资源利用非常重要。
1. Code(代码段大小)
含义: Code 表示程序中可执行代码所占用的空间大小,单位是字节。这部分空间存储了处理器要执行的指令,包括函数、循环、条件判断等逻辑的机器码。例如,你编写的 main 函数、各种中断服务函数以及其他自定义函数经过编译和链接后,其指令代码都会包含在这个 Code 大小中。
作用: 了解 Code 大小有助于评估程序的复杂度和对 Flash 存储器(通常用于存储程序代码)的需求。如果 Code 大小接近或超过 Flash 的容量,可能需要优化代码(如去除冗余代码、优化算法等)或更换更大容量的 Flash。
FLASH 占用:Code 段包含编译后生成的机器指令,是程序执行的核心逻辑。这些指令在运行时需要被 CPU 读取并执行,由于 FLASH 具有非易失性,掉电后数据不会丢失,所以 Code 段通常存储在 FLASH 空间中。在嵌入式系统启动时,CPU 从 FLASH 中读取指令并加载到内存中执行。因此,Code 段必然占用 FLASH 空间。
RAM 占用:严格来说,Code 段本身并不直接占用 RAM 空间用于存储指令内容。然而,在程序运行时,部分指令可能会被加载到 CPU 的高速缓存(Cache)中以提高执行效率,高速缓存是位于 CPU 内部或与 CPU 紧密相连的高速存储区域,从广义内存角度可视为 RAM 的一部分。但这种占用并非 Code 段本身的存储需求,而是为了优化执行过程。所以从存储角度,Code 段主要且明确占用的是 FLASH 空间。
2. RO-data (只读数据段大小)
含义: RO-data 即 Read Only data,代表只读数据所占用的空间。这包括常量(如 const 修饰的变量)、字符串常量等。这些数据在程序运行过程中不会被修改,因此被存储在只读区域。
const int Array[1000] = {1, 2, 3,...};
const static int StaticArray[1000] = {1, 2, 3,...};
当数组被声明为 const 时, 它通常会被视为只读数据,存储在 RO-data 段(Read - Only data ),位于 Flash 中,而不会占用 RAM 空间。
作用: 通过分析 RO-data 大小,可以知道程序中固定不变的数据量,对于合理规划 Flash 空间和优化存储布局有帮助。例如,如果有大量的只读数据,可以考虑将它们存储在外部 Flash 中以节省内部 Flash 空间。
FLASH 占用:RO-Data 段存储在程序运行过程中不会改变的数据,如常量字符串、全局常量等。由于这些数据只读的特性,适合存储在 FLASH 这种非易失性存储介质中。例如,const int globalConstant = 10; 或 const char *message = "Hello, World!"; 中的 globalConstant 和 message 所指向的字符串都会存储在 RO-Data 段,占用 FLASH 空间。
RAM 占用:在运行时,RO-Data 段的数据一般不会直接占用额外的 RAM 空间用于存储自身内容。不过,与 Code 段类似,为了提高访问速度,部分数据可能会被加载到高速缓存中,这是一种临时的、为优化访问而产生的占用,并非其存储需求。所以 RO-Data 主要占用 FLASH 空间。
3. RW-data(读写数据段大小)
含义: RW-data 即 Read-Write data,是在程序初始化时需要从 Flash 复制到 RAM 中的可读写数据的大小。这部分数据在程序运行过程中可能会被修改。例如,全局变量和静态变量在初始化时就会占用 RW-data 空间。假设你定义了一个全局变量 int globalVariable = 5;,这个变量及其初始值就会计入 RW-data 大小。
static int StaticArray[1000] = {1, 2, 3,...};
如果对这个静态数组进行了初始化,初始化的值会存储在 RW-data 段(Read-Write data )。RW-data 段在程序启动时,其内容会从 Flash(程序存储区)复制到 RAM 中,同样会占用 RAM 空间。
void function() {
int localVar = 10; // 函数调用时,在栈上为localVar分配空间,并将值10存储在该空间
}
函数内非静态的局部变量初始化,主要涉及栈空间。上面 localVar 占用栈上的空间,其初始化值 10 也存储在栈上对应的位置,不涉及 RW-data。因此如果堆栈空间足够,尽量将一些数据函数专用的数据定义在函数内部,并且不要使用const和static,这样就会大大节省Flash和RAM空间。
作用: RW-data 的大小反映了程序运行时需要在 RAM 中预留的可读写数据空间。如果 RW-data 过大,可能会导致 RAM 资源紧张,需要考虑优化变量的使用方式,例如减少不必要的全局变量或合理分配静态变量的生命周期。
FLASH 占用:RW-data 段存储已初始化且在运行时可能被修改的全局变量和静态局部变量。其初始化值存储在 FLASH 中,在程序启动时,这些初始化值会被从 FLASH 复制到 RAM 的 RW-data 段。例如,int globalVariable = 5;,数字 5 作为初始化值存储在 FLASH 中,程序启动时会被复制到 RAM 的 RW-data 段对应位置。所以 RW-data 的初始化值占用 FLASH 空间。
RAM 占用:RW-data 段在运行时需要在 RAM 中为变量提供可读写的空间,以满足程序对这些变量进行修改的需求。因此,RW-data 段明确占用 RAM 空间。综上,RW-data 同时占用 FLASH 和 RAM 空间。
4. ZI-data(零初始化数据段大小)
含义: ZI-data 即 Zero - Initialized data,是指那些在程序运行前被初始化为零的全局变量和静态变量所占用的空间。这些变量的生命周期贯穿整个程序运行过程,并且它们的内存空间在程序加载到内存时就被分配。这些变量在目标文件中不占用实际的存储空间,因为它们的值在程序启动时会由运行时库自动清零。
// 定义一个大的静态数组
static int largeStaticArray[1000];
这个静态数组没有显式初始化,它会被初始化为零,如同其他零初始化的全局和静态变量一样,占用 ZI-data 段的空间,而 ZI-data 段在程序启动时会被映射到 RAM 中。所以,从程序运行开始,这部分内存就会在 RAM 中占用空间。
作用: ZI-data 大小直接影响程序启动时需要清零的 RAM 空间大小。了解 ZI-data 大小有助于评估程序启动时对 RAM 初始化的开销,同时也能帮助开发者合理规划 RAM 的使用,确保有足够的空间用于程序运行时的数据存储和堆栈操作。
FLASH 占用:ZI-data 段存储在编译时未显式初始化,但需要在程序运行时初始化为 0 的全局变量和静态局部变量。由于其初始值为 0,在编译后的二进制文件(存储在 FLASH)中不占用空间,因为无需在文件中存储这些零值。所以 ZI-data 不占用 FLASH 空间。
RAM 占用:在程序启动时,启动代码会在 RAM 中为 ZI-data 段的变量分配空间并将其清零。例如,int uninitializedGlobal; 这样未初始化的全局变量会在 RAM 的 ZI-data 段分配空间。因此,ZI-data 只占用 RAM 空间。
特别注意 编译程序有时会将启动程序分配的栈空间和堆空间也判定为 ZI-data, 因此可能会出现 ZI-data 非常大的情况,这时判断程序实际的 ZI-data空间,需要从中减去栈的空间和堆的空间。
二、关于堆和栈的空间分配
1、 栈的分配
函数的局部变量、调用参数以及返回值,都会用到栈空间,在函数调用完毕后,所有在栈上分配的空间都会释放,非常高效并且不会产生内存碎片,因此对栈空间的分配非常重要。
如果内存空间紧张,为了精确计算程序占用的栈空间,可以在程序开始运行时,将从栈底到栈指针的所有空间全部用0xFF填满,在使用一段时间后,从栈底到开始所有0xFF占用的空间,就是堆栈剩余的空间,如果这个剩余很大,就可以适当缩小栈空间,如果剩余很小,就应该适当扩大栈空间。
如果涉及到外部FLASH读写,一般的FLASH页面大小为4K,那么在FLASH写函数内,至少需要4K的空间来读取FLASH信息并且在修改后删除FLASH并重新写回,站的空间一定要留够,否则可能出现栈空间不足的情况,尽量不要将缓存放在全局,会永远占用4K的RAM空间,非常浪费。另外尽量减少递归函数的使用,因为不容易控制递归深度,可能会造成栈空间不足的情况。
调试栈时,需要确认栈的工作方式,在大多数 ARM 架构中,栈是向下生长的,也称为递减栈。这意味着栈顶指针(sp,Stack Pointer)向低地址方向移动来分配新的栈空间。假设系统分配给栈的内存区域起始地址(栈底)为 0x20000000,结束地址(栈顶)为 0x20001000。当程序启动时,栈顶指针 sp 通常指向栈顶地址 0x20001000。当第一个函数被调用时,为该函数的局部变量、参数等分配栈空间,栈顶指针 sp 会向低地址方向移动,例如移动到 0x20000FFC(假设函数相关数据占用 4 字节)。如果该函数又调用另一个函数,栈顶指针会继续向低地址移动,为新函数的相关数据分配空间。




2、 堆的分配
如果程序内没有使用malloc和free,则一般不会使用堆空间。也有一种例外,就是程序使用的库函数或外部SDK可能使用到malloc和free,一般使用查询资料来进行确认,或者编写小的测试程序来进行验证。
三、关于中断向量表的偏移
如果只有一套固件,一般不会涉及中断向量表偏移,但是如果涉及到IAP升级,一边都会采用BOOT+APP两套程序的方式,硬件启动时先从起始地址加载BOOT,再由BOOT程序转向APP,由于APP并不存储在起始地址上,这就涉及到中断向量表的偏移处理。有三种方式进行中断向量表的偏移控制:
1. 固件程序中修改中断偏移
启动文件一般是汇编文件,BOOT跳转到APP时都是从reset中断开始运行,因此我们修改reset中断函数即可。定义一个变量记录偏移值,再reset中断函数中,把这个变量值写入中断偏移寄存器。这种方式非常直观,适合大多数情况。
new_vect_table EQU 0x00001000 ; 首先需要定义偏移量
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
;reset NVIC if in rom debug
LDR R0, =0x20000000
LDR R2, =0x0
MOVS R1, #0 ; for warning,
ADD R1, PC,#0 ; for A1609W,
CMP R1, R0
BLS RAMCODE
; ram code base address.
ADD R2, R0,R2
RAMCODE
; reset Vector table address.
LDR R0, =0xE000ED08 ; 这是中断偏移寄存器地址
LDR R2, =new_vect_table ; 将新的偏移量写入中断偏移寄存器地址
STR R2, [R0]
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
如果不想修改汇编文件,我们可以在main函数开始时,修改中断偏移,这也是一种比较容易理解的方式。
uint32_t new_vect_table = 0x00001000; // 首先需要定义偏移量
volatile uint32_t *vtor = (volatile uint32_t *)0xE000ED08; // 这是中断偏移寄存器地址
*vtor = new_vect_table; // 将新的偏移量写入中断偏移寄存器地址
2. BOOT程序中修改中断偏移
这种方式与第一种方式基本一致,但是可以适应多个APP情况,BOOT程序可以在跳转到APP之前,先根据APP的地址,动态修改中断偏移,然后再进行跳转。使用这种方式,APP的reset中断函数或者main函数不能再修改中断偏移寄存器的值,否则改好的偏移置可能又被改错了。
3. 自动获取中断偏移并在固件程序中修改
前两种方式一个是适应单一APP的方式,一个是适应多个APP的方式,但是都是假设BOOT一定存在,假如想让APP在没有BOOT的情况下可以独自运行(中断向量表不做偏移),当有BOOT时,又可以根据加载地址的不同,自动更新中断向量表偏移,可以有一种一劳永逸的方方法,就是利用 __Vectors 标签。
在启动文件中,都有一个标签用来标识中断向量表的起始地址,一般的名称为 __Vectors,这个标签标识的地址,其实也是程序加载的首地址,因为中断向量表总是在程序的起始地址。我们将这个地址作为中断向量表的偏移值,就不需要再单独定义中断偏移值了。
.....
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors
DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset
DCD NMI_Handler ; NMI
DCD HardFault_Handler ; Hard Fault
DCD 0 ; Reserved
.....
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
;reset NVIC if in rom debug
LDR R0, =0x20000000
LDR R2, =0x0
MOVS R1, #0 ; for warning,
ADD R1, PC,#0 ; for A1609W,
CMP R1, R0
BLS RAMCODE
; ram code base address.
ADD R2, R0,R2
RAMCODE
; reset Vector table address.
LDR R0, =0xE000ED08 ; 这是中断偏移寄存器地址
LDR R2, =__Vectors ; 将__Vectors 地址写入中断偏移寄存器地址
STR R2, [R0]
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
或者在main函数开始时,修改中断偏移:
extern uint32_t __Vectors; // 中断向量表起始地址
volatile uint32_t *vtor = (volatile uint32_t *)0xE000ED08; // 这是中断偏移寄存器地址
*vtor = (uint32_t)&__Vectors; // 将__Vectors 地址写入中断偏移寄存器地址
通过这种方式,不管是APP单独写入FLASH中运行,还是放在FLASH中的其他地址,通过BOOT跳转后运行,都可以自动修改正确的中断向量表偏移,使得APP可以正常运行。
通过实际修改程序,这种方式确实可行,不理解MCU开发商的例程为什么不这样设计,是为了让每个嵌入式开发人员都有机会研究并理解一下中断偏移的设置问题吗?
浙公网安备 33010602011771号