打印
[开发工具]

探索APM32嵌入式开发中的RAM数据段与资源分配

[复制链接]
144|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
DKENNY|  楼主 | 2024-9-13 17:53 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 DKENNY 于 2024-9-13 17:52 编辑

#申请原创# @21小跑堂
1. 前言
    在嵌入式 MCU 开发中,由于硬件资源有限,RAM 和 ROM 空间常常比较紧张。即使是一些较大的 MCU,其 RAM 空间也通常只有 512KB,而一些小型 MCU 的 RAM 则更显微薄。本文将以 APM32 控制器为例,通过实验介绍RAM的相关数据段以及资源分配情况。

2. 数据段含义

2.1 Program Size解析
    在使用 Keil 编译程序后,编译成功时会在输出栏看到一行信息,其中包含了 Program Size。那么,这个 Program Size 实际上意味着什么呢?其中的 CodeRO-dataRW-data ZI-data 各代表什么呢?

    例如,输出信息为:  
Program Size: Code=1676 RO-data=512 RW-data=40 ZI-data=1024
       - Code 段这个部分表示我们编写的代码占用了多少 Flash 存储空间。需要注意的是,Code 段的大小是经过编译器优化和汇编处理后的结果,添加注释或无效代码不会影响 Code 段的大小。
        - RO-data 段即只读数据段(Read-Only Data)。在代码中定义的 const 常量以及某些固定的字符(如 printf 打印的内容)都会存储在这个数据段中。
        - RW-data 段可读可写数据段(Read-Write Data)。代码中的变量会被存放在这个数据段内。
        - ZI-data 段即零初始化数据(Zero-Initialized Data)用于存放那些初始化为 0 的可读写变量。

    那么,局部变量、全局变量和常量是如何占用 RAM 空间的呢?如何计算它们实际占用的 RAM 和 ROM 空间呢?
    关于 RAM 和 ROM 空间的占用计算方式如下:
       - ROM 空间大小计算方式:Total ROM = Code + RO-data + RW-data
        - RAM 空间大小计算方式:Total RAM = RW-data + ZI-data


    通过实验,我们可以进一步验证和推理局部变量、全局变量以及常量是如何占用 RAM 的。

3. 局部变量、全局变量、常量如何占用 RAM?

    3.1 栈大小Stack_Size 与 堆大小Heap_Size
    但在此之前,我们需要先了解栈大小(Stack_Size)堆大小(Heap_Size)这两个重要概念。MCU 的 RAM 空间被划分为多个区域,其中包括堆和栈(还有其他区域)。这两个区域的大小可以在对应的启动文件中查看和设置。



    当我们编写程序时,栈(Stack)和堆(Heap)是如何使用的呢?
    - 栈的使用在程序编码过程中,当我们在函数内部定义一个局部变量时,实际上是在栈空间上申请了一块内存来存储数据。在程序运行过程中,当进入某个函数时,需要保存当前函数内的变量状态,也就是进行压栈操作。这意味着将当前的寄存器(如 r0、r1 等)压入栈空间,以便在函数退出时能够恢复现场。压栈操作实际上就是在栈空间上占用一块内存来存储数据。当函数退出时,需要进行出栈操作,即从栈空间中读取之前压栈操作保存的数据,恢复寄存器的值,然后继续执行后续的代码。
    - 堆的使用在程序执行过程中,当我们调用 malloc 函数时,实际上是从堆上申请一块内存来供我们使用。一旦使用完成,我们需要调用 free 函数来释放这块内存区域,以便其他部分可以继续使用。

    此外,对于裸机程序(没有操作系统的程序),整个程序使用的栈都是从启动文件(如 startup_xxxx.s)中设置的栈空间中分配的。而对于使用实时操作系统(RTOS)的程序,每个线程都具有独立的栈空间,这一点需要特别注意。

    接着我们进行如下测试验证。

    3.2 验证栈大小设置,对程序编译的影响
    1. 修改栈大小,观察程序编译大小变化,设置 Stack_Size EQU 0x800,程序编译结果如下。
Program Size: Code=1676 RO-data=512 RW-data=40 ZI-data=2048



    2. 修改栈大小为 Stack_Size EQU 0x400 ,程序编译结果如下。
Program Size: Code=1676 RO-data=512 RW-data=40 ZI-data=1024



    根据以上结果,我们可以得出结论:调整栈的大小直接会影响程序占用的RAM量。在我们的情况下,我们只是简单地减小了栈的大小,但实际上这部分未被使用的栈空间缩减只导致了ZI-data数据段的变化。

    3.3 验证局部变量RAM分配
    1. 首先,设置启动文件中的栈大小为 Stack_Size EQU 0x400,这意味着我们分配了 0x400 字节的栈空间,也就是 1024 字节。
    2. 接下来,在 main() 函数内定义一个局部数组 a,其大小为 1 字节。然后编译程序,查看编译后程序的大小变化。
    3. 为了防止编译器优化掉这个数组,我们在定义时增加 __attribute__((unused)) 修饰符。



    4. 最后,进行编译以查看程序的大小变化,并运行程序来验证结果。
    5. 经过操作后发现程序可以正常运行。
    6. 接下来,我们将再次修改数组 a 的大小为 400 字节。然后进行编译并查看程序的大小变化,并最后运行程序。



    7. 在此次实验中,经过编译后发现程序可以正常运行。编译结果显示只有 code 段发生了变化,而 RAM 空间并没有发生改变。
    8. 接着,我们将再次修改数组 a 的大小,这次设置为 1040 字节,已超过了启动文件内分配的栈大小。然后进行编译,查看程序的大小变化,并运行程序,以观察这次变化带来的影响。



    9. 经过编译后,发现程序的 code 字段仅发生了变化,而 RAM 空间并没有改变。然而,当我们尝试实际运行程序时,程序却直接崩溃了。在进行仿真运行后,我们发现程序已经卡在了 HardFault_Handler() 中断处。



    从以上分析,我们可以得出几个关键结论
        - 1. 局部变量会占用栈空间,而这个栈的大小(Stack_Size)是在启动文件中进行设置的。
        - 2. 其次,当栈空间不足时,程序就可能发生崩溃。因此,合理配置栈大小对于确保程序的稳定运行至关重要。


    3.4 验证全局变量的RAM分配
    我们已经了解局部变量如何影响系统栈空间了,接下来我们想知道全局变量是否也会占用系统栈空间。

    在使用全局变量进行验证时,为了防止编译器进行优化,我们需要在 main 函数内增加一行代码来使用这个数组。因此,在 main 函数中我们新增一行代码:
printf("%s", a);
   这样做可以确保全局变量的占用情况被准确地观察和验证。接下来让我们进行实验,以验证全局变量在系统栈空间上的影响。
    1. 首先,我们定义一个全局变量数组 a,大小为 1 字节,并使用 __attribute__((unused)) 修饰符来防止编译器对其进行优化。接下来,进行编译并查看程序的编译结果,然后运行程序进行验证。



    2.编译结果如下,且程序正常运行。
Program Size: Code=1952 RO-data=512 RW-data=48 ZI-data=1024
   3. 接下来,我们将数组 a 的大小修改为 400 字节,然后继续进行验证。



    4. 编译结果如下,且程序正常运行。
Program Size: Code=1952 RO-data=512 RW-data=44 ZI-data=1428
   此时我们可以发现Total RAM增大了。
Add Total Ram = (The current RW-data + The current ZI-data ) - (The laster RW-data + The laster ZI-data) = (1428 + 44) - (1024+ 48) = 400Byte(注意此处400Byte != (400 - 1)Byte 与字节对齐有关)
   5. 继续修改数组a大小为1040,使其超过系统栈大小(当前配置的系统栈大小0x400=1024Byte),继续验证。



    6. 编译结果如下。
Program Size: Code=1952 RO-data=512 RW-data=44 ZI-data=2068
   程序正常运行,并没有之前的死机现象,同时我们可以发现Total RAM继续增大。
Add Total Ram = (The current RW-data + The current ZI-data ) - (The laster RW-data + The laster ZI-data) = (2068 + 44) - (1428 + 44) = 640Byte(注意此处640Byte != (1040 - 400)Byte与字节对齐有关)
   通过我们的实验验证可以得出结论
    全局变量的定义不会占用系统栈空间,这是因为全局变量与局部变量不同,它们在RAM中会单独申请空间。这种区别使得全局变量在内存管理方面具有独特的优势和特点

    3.5 验证全局常量和局部常量const的RAM分配
    接下来,我们将在前面两个实验的基础上,对 const 常量进行进一步探讨。
    1.针对全局变量,如果我们使用 const 修饰,将其变为常量,这将导致该数据被分配到只读数据段(RO-data),而不再使用未初始化数据段(ZI-data)。这种修改会对内存分配方式产生影响,从而优化全局变量在程序中的存储方式。



    2.接下来,我们来看局部常量的情况。
    对于局部常量,如果使用 const 修饰与不使用 const 修饰,程序的编译结果基本没有差别。然而,由于数组 a 的大小超出了系统栈的限制,导致程序无法正常运行。值得注意的是,即使采用 const 修饰,局部变量仍然会占用系统栈空间。



    3. 在使用 const 修饰局部常量后,我们期望只读数据段(RO-data)会增加,但实际上却没有增加。这种情况可能是由于优化导致的,具体是编译器优化还是 C 语言本身的优化暂时不得而知。因为我们将数组 a 的值都赋为0,这种情况被优化了。如果我们简单地修改一下,让数组 a 中的值不全为0,就能看到 RO-data 字段的增加。然而,尽管局部 const 常量会占用系统栈空间,由于系统栈大小不够,程序仍然无法正常运行。



    综上,我们得出以下结论
    - 全局常量 const:全局常量使用 const 修饰后,会被放入只读数据段(RO-data segment),不占用系统栈空间。这种特性使得全局常量在程序运行时不能被修改,但可以被读取。
    - 局部常量 const:局部常量使用 const 修饰后,虽然仍然占用系统栈空间,但由于优化的原因,当局部 const 常量的值可以在编译期间确定且未被修改时,可能不会导致只读数据段的增加。然而,如果在局部 const 常量中的值难以在编译期间确定或者被修改,只读数据段可能会增加。

    3.6 关于堆的分配
    关于堆的分配和使用,实际上比栈要简单得多,具体内容如下:

    - 1. 堆的大小设置
        在启动文件中定义堆的总大小,设置代码为:Heap_Size EQU 0x200
    - 2. 申请堆空间
        在代码中可以使用 malloc 来动态申请堆空间。如果堆中有足够的空闲空间,申请会成功,并返回一个指向首地址的指针;如果堆已满,则返回 NULL 表示申请失败。
    - 3. 释放堆空间
        当使用完堆空间后,应该调用 free 函数来释放。如果不小心重复释放同一块空间,很容易导致程序出现错误(bug)。

    通过这样的方式,我们可以高效地管理堆内存空间。

4. 总结
    以上内容分析了编程中涉及的 RAM 空间使用,特别是变量和常量对 RAM 的占用情况。归纳起来,有以下几点要注意:

  
要点
  
内容
堆栈大小配置
系统堆栈的大小是在启动文件中进行设置的。
全局变量分配
全局变量不是从系统栈分配的,而是直接从  RAM 中分配空间。
局部变量分配
局部变量的内存是从系统栈中分配的。
变量大小限制
如果全局变量的分配超过了芯片实际的 RAM 大小,编译器会直接报错;而局部变量如果超出系统栈大小(在裸机系统中),编译器则不会警告,程序可能会意外崩溃。
全局常量的内存占用
全局常量不占用 RAM 资源,它们仅占用 ROM 的只读数据区域。
局部常量的内存占用
局部常量不仅占用 ROM,还会消耗一定的系统栈空间,在裸机系统中尤其需要注意。

    注意事项
        需要强调的是,以上分析主要针对裸机系统。在实时操作系统(RTOS)中,情况会有所不同,因为 RTOS 中存在系统栈和线程栈,但总体概念依然与栈相关。

5. 附件
    测试工程:
Template.zip (787.17 KB)









使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

28

主题

51

帖子

5

粉丝