ARM单片机启动流程(二)(详细解析)

文章目录

      • [1.3.4 SRAM空间理解](#1.3.4 SRAM空间理解)


1.3.4 SRAM空间理解

Flash空间是单片机厂商已经固定了,并且Flash是ROM空间,是非易失性数据。

内存空间具有一定的灵活性,

以此单片机为例内存空间是:

范围是0x2000 0000~0x3FFF FFFF 共计512M

地址是从高到低,并且可以自己配置。

最下面是0x2000 000

裸机很少使用malloc段

main函数上面的空间,就是我们前面看到的_main函数,就是分析汇编语言涉及到的一些函数。

SP就是分配栈空间用,(具体在嵌入式C语言课程里面讲解。)

栈空间:这个里面是循环的,

就是我这次是执行LedDrivInit函数,执行玩以后,我的SP就会回到main函数栈空间的后面位置,然后下一次需要执行的是TurnOnLed那么我就会再次给这个函数分配,相当于是循环利用。栈是重复利用的,
这里也需要说明一下,就是不能定义太多的全局变量,因为全局变量太多,就会占用RAM的空间,导致函数的栈空间不够,但是如果使用函数如替代全局变量,那么使用的就是栈空间,相当于是可以循环使用的。减少内存的消耗。这也是栈的特点。

区别的是Flash就不是,因此需要给每一个函数分配一个空间,因此需要的空间往往就很大,这个不难理解的。

栈空间大小是认为设定的,

1、函数内部有没有使用大数组,需要占用栈空间,如果有这种数组,那么就需要较大的栈空间,不然就会出现栈溢出。最好不要定义这种大数组。

此外结构体的参数较多,最好定义成指针类型,开销不是太大。

只要结构体是参数,那么就直接定义成指针,这样内存开销不是很大。 不是太理解?

以下进行详细解释:

c 复制代码
#include <stdio.h>

// 定义大型结构体(包含多个成员)
typedef struct {
    int id;
    char name[50];
    float scores[100]; // 数组成员,结构体体积较大
    int age;
    char address[100];
} Student;

// 值传递:复制整个结构体副本
void updateStudentValue(Student stu) {
    stu.id = 100; // 仅修改副本
    stu.scores[0] = 99.5;
}

// 指针传递:仅传递地址
void updateStudentPointer(Student *stu) {
    stu->id = 100; // 修改原始结构体
    stu->scores[0] = 99.5;
}

int main() {
    Student s1 = {1, "Alice", {0}, 20, "Beijing"};
    
    // 测试值传递(无效修改)
    updateStudentValue(s1);
    printf("Value Pass: ID=%d, Score0=%.1f\n", s1.id, s1.scores[0]); 
    // 输出:ID=1, Score0=0.0 (未修改)

    // 测试指针传递(有效修改)
    updateStudentPointer(&s1);
    printf("Pointer Pass: ID=%d, Score0=%.1f\n", s1.id, s1.scores[0]); 
    // 输出:ID=100, Score0=99.5 (成功修改)
    
    return 0;
}

updateStudentValue(s1);

就是将整个结构体进行复制传递,

传递 updateStudentValue:复制整个 Student 结构体(约 50 + 100 * 4 + 100 = 550+ 字节

updateStudentPointer(&s1);

可以理解为传递是该结构体的首地址,然后又因为结构体体的地址是连续的,因此可以通过类似数组的样子,找到id对应的地址空间,从而进行修改

通过结构体指针修改成员的本质,正是基于结构体内存的连续性和编译器对成员偏移量的计算

结构体在内存中是一块连续的地址空间,成员按声明顺序依次存储(但可能存在内存对齐填充)

c 复制代码
struct Student {
    int id;         // 4字节
    char name[50];  // 50字节
    float score;    // 4字节
};

假设起始地址为 0x1000,内存布局可能如下(简化版,忽略对齐填充)

c 复制代码
0x1000: id(4字节)
0x1004: name[50](50字节)
0x1036: score(4字节)

当调用 updateStudentPointer(&s1) 时:

  • &s1 获取结构体变量 s1首地址 (如 0x1000)。
  • 函数参数 Student *stu 存储的就是这个首地址
    通过箭头运算符 ->(如 stu->id)时,编译器自动完成以下操作:
  1. 计算成员偏移量
    id 是第一个成员,偏移量为 0
    name 偏移量为 sizeof(int) = 4
    score 偏移量为 sizeof(int) + 50 = 54(实际需考虑对齐,此处简化)。
  2. 生成访问指令
    stu->id 会被编译为:访问地址 stu + 0
    stu->name 编译为:访问地址 stu + 4
    关键点:无需手动计算偏移量,编译器自动处理!

数组通过基地址 + 索引 × 元素大小 定位元素(如 arr[i])。结构体成员定位类似:

基地址(结构体首地址) + 成员偏移量

但区别在于:

  • 数组元素 :类型相同,偏移量计算规则统一(i * sizeof(element))。
  • 结构体成员 :类型不同,偏移量由编译器在编译时静态计算(如 id 偏移量恒为0)

为什么不能直接像数组那样通过指针算术访问?​

虽然理论上可通过首地址+偏移量直接访问:

不推荐这样做,因为:

  1. 破坏可读性 :箭头运算符 -> 更清晰。
  2. 忽略内存对齐:编译器可能在成员间插入填充字节(Padding),手动偏移可能错位

传递结构体指针时,函数内通过 stu->id 修改成员的本质是:
首地址 + 编译器预计算的成员偏移量 → 定位目标内存 → 直接修改原始数据
此过程由编译器保证正确性,无需开发者关心偏移量细节

此时你也明白了,为什么结构体访问可以使用->该符号了吧,为什么数组不行,原因就是这里。

妙!!!!!! 妙!!!!!!妙!!!!!!

2、函数调用层级太多,一直会分配栈空间,那么也可能会导致栈溢出,避免嵌套太多。

栈顶地址怎么计算?

首先根据全局变量这些,以及mallcon这种看需要申请多少等等,然后得出一个数据,那么在根据我们需要的栈空间分配大小,最终就得到了栈顶地址。

通过编译后的.map函数就可以看出来,各种变量分配的空间。

根据走自身工程数据,进行一部分分析:

首先我们在函数中自己定义了栈的大小1024个字节。

c 复制代码
Stack_Size      EQU     0x00000400
c 复制代码
.data                  0x20000088   Section        8  ntc_drv.o(.data)
    g_tempData         0x20000088   Data           4  ntc_drv.o(.data)
    s_index            0x2000008c   Data           2  ntc_drv.o(.data)
    s_convertNum       0x2000008e   Data           2  ntc_drv.o(.data)
    .data              0x20000090   Section        6  rh_drv.o(.data)
    g_adcVal           0x20000090   Data           2  rh_drv.o(.data)
    g_humiData         0x20000092   Data           1  rh_drv.o(.data)
    g_timCount         0x20000094   Data           2  rh_drv.o(.data)
    .data              0x20000098   Section        4  stdout.o(.data)
    .bss               0x2000009c   Section       20  ntc_drv.o(.bss)
    g_temp10MplBuf     0x2000009c   Data          20  ntc_drv.o(.bss)
    STACK              0x200000b0   Section     1024  startup_gd32f30x_hd.o(STACK)

首先说说明的是

c 复制代码
.data                  0x20000088   Section        8  ntc_drv.o(.data)

表示 ntc_drv.o(.data) 共有8个字节,并且其实地址是0x20000088

0x20000088

0x20000089

0x2000008A

0x2000008B

0x2000008C

0x2000008D

0x2000008E

0x2000008F

以上这8个地址,刚好分配了

c 复制代码
    g_tempData         0x20000088   Data           4  ntc_drv.o(.data)
    s_index            0x2000008c   Data           2  ntc_drv.o(.data)
    s_convertNum       0x2000008e   Data           2  ntc_drv.o(.data)

紧接着就是下一个数据段

c 复制代码
.data              0x20000090   Section        6  rh_drv.o(.data)

表示 rh_drv.o(.data) 共有6个字节,并且其实地址是0x20000090

以此类推进行分配RAM空间,也就是.data和.bss段。

c 复制代码
    .bss              0x2000009c   Section       20  ntc_drv.o(.bss)
    g_temp10MplBuf    0x2000009c   Data          20  ntc_drv.o(.bss)
    STACK             0x200000b0   Section     1024  startup_gd32f30x_hd.o(STACK)

因此经过这些计算,就可以得出栈顶空间,也就是在下面这个图里面的注释一样。

但是上面的程序跟这个图里面的不是一个工程。

补充一个说明

RO:常量数据,这一部分也在ROM里面,也就是Flash

ZI是:

Flash里面全局变量数据RW区域和.data里面是一一对应的,就是通过_main函数将RW里面的全局变量给复制到RAM的.data里面。

不难想象,我们是通过Flash进行烧写,因此需要有一个步骤,就是将我们的全局变量进行对应的复制才能到RAM段。因此这两个是一一对应的。

需要说明

Program Size: Code=10300 RO-data=1004 RW-data=156 ZI-data=1044

那么可以分析出对应的bin文件一共有

10300 + 1004 + 156 = 11460大小。

SRAM是嵌入式系统中存储运行时数据的核心,栈是其重要组成部分 ,但SRAM还包含堆、全局变量、缓存等其他功能区域。合理规划栈大小和使用习惯(如避免大局部变量)是确保系统稳定的关键。若需进一步优化内存管理,可结合.map文件分析和启动文件配置。

通过上述数据手册可以看出,SRAM的空间是0x2000 0000~0x2001 7FFF,最大的空间是96KB,经过以下计算也是这样:98304/1024=96KB,98307是地址之差,又因为一个地址只能存储一个字节因此除以1024个字节得到的就是空间大小96KB。

c 复制代码
Stack_Size      EQU     0x00000400    //表示的是1KB,400=1024。

通过上面这个定义可以看出我们的栈空间是1KB。一般这个定义就在启动文件里面可以看出来。

以下是栈空间的理解:

为什么会先考虑栈空间,这是因为单片机在复位后,单片机首先会做两件事:

1、从地址 0x0000,0000 处取出 MSP 的初始值(栈顶地址),用于函数分配内存栈空间;

2、从地址 0x0000,0004 处取出 PC的初始值,这个值是Reset Handler复位函数的地址,然后从这个地址开始执行程序。

那么现在我们先只关心第一个步骤,取出栈顶地址,

从芯片手册里面可以看出SRAM空间是SRAM的空间是0x2000 0000~0x2001 7FFF,

内存分配顺序:静态内存优先

Cortex-M 芯片的 SRAM 分配遵循严格顺序:

  • 步骤 1 :从 0x20000000 开始分配全局变量(.data 已初始化数据、.bss 未初始化数据)。
  • 步骤 2 :分配堆空间(Heap),若程序未使用 malloc 则可能被优化。
  • 步骤 3栈空间(Stack)最后分配 ,其起始地址(__initial_sp) = 静态内存结束地址 + 栈大小
c 复制代码
    0x20000000   0x08000750   0x00000024   Data   RW          165    .data               led_drv.o
    0x20000024   0x08000774   0x00000004   PAD
    0x20000028        -       0x00000400   Zero   RW          648    STACK               startup_gd32f30x_hd.o

可以看出前两个步骤分配完成以后SRAM的空间是0x20000028,然后在跟我们上述定义的栈空间大小,我们就可以得出

  • 静态内存结束地址为 0x20000428 - 0x400 = 0x20000028(栈空间 1KB = 0x400 字节)
    → 表明静态内存占用了 0x20000000~0x20000028 的空间(约 40 字节)。
c 复制代码
Stack_Size      EQU     0x00000400    //表示的是1KB,400=1024。

如果我们把栈空间进行修改,

c 复制代码
Stack_Size      EQU     0x00000800

可以看出前面的内存分布并没有发生改变,改变的只是最后栈空间的大小

c 复制代码
    0x20000000   0x08000750   0x00000024   Data   RW          165    .data               led_drv.o
    0x20000024   0x08000774   0x00000004   PAD
    0x20000028        -       0x00000800   Zero   RW          648    STACK               startup_gd32f30x_hd.o

最终得到的是栈顶地址是:0x20000828

c 复制代码
    __initial_sp                             0x20000828   Data           0  startup_gd32f30x_hd.o(STACK)

文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】

本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:

署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。

相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。

相关推荐
竹照煜_ysn2 小时前
蓝桥杯51单片机设计
单片机·嵌入式硬件·51单片机
Electron-er3 小时前
汽车LIN总线通讯:从物理层到协议栈的深度解析
单片机·汽车电子·lin总线·lin总线通讯
Do vis8244 小时前
STM32第十六天蓝牙模块
stm32·单片机·嵌入式硬件
猫猫的小茶馆6 小时前
【STM32】通用定时器基本原理
c语言·stm32·单片机·嵌入式硬件·mcu·51单片机
jingshaoqi_ccc6 小时前
stm32的USART使用DMA配置成循环模式时发送和接收有着本质区别
stm32·单片机·嵌入式硬件
pumpkin845147 小时前
Rust 调用 C 函数的 FFI
c语言·算法·rust
2401_858286118 小时前
125.【C语言】数据结构之归并排序递归解法
c语言·开发语言·数据结构·算法·排序算法·归并排序
双叶8369 小时前
(C++)学生管理系统(正式版)(map数组的应用)(string应用)(引用)(文件储存的应用)(C++教学)(C++项目)
c语言·开发语言·数据结构·c++
XMAIPC_Robot9 小时前
基于ARM+FPGA的光栅尺精密位移加速度测试解决方案
arm开发·人工智能·fpga开发·自动化·边缘计算