ARM单片机启动流程(二)(详细解析)
文章目录
- 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、函数内部有没有使用大数组,需要占用栈空间,如果有这种数组,那么就需要较大的栈空间,不然就会出现栈溢出。最好不要定义这种大数组。
此外结构体的参数较多,最好定义成指针类型,开销不是太大。
只要结构体是参数,那么就直接定义成指针,这样内存开销不是很大。 不是太理解?
以下进行详细解释:
#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对应的地址空间,从而进行修改
通过结构体指针修改成员的本质,正是基于结构体内存的连续性和编译器对成员偏移量的计算。
结构体在内存中是一块连续的地址空间,成员按声明顺序依次存储(但可能存在内存对齐填充)
struct Student {int id; // 4字节char name[50]; // 50字节float score; // 4字节
};
假设起始地址为 0x1000
,内存布局可能如下(简化版,忽略对齐填充)
0x1000: id(4字节)
0x1004: name[50](50字节)
0x1036: score(4字节)
当调用 updateStudentPointer(&s1)
时:
&s1
获取结构体变量s1
的首地址(如0x1000
)。- 函数参数
Student *stu
存储的就是这个首地址
通过箭头运算符->
(如stu->id
)时,编译器自动完成以下操作:
- 计算成员偏移量:
id
是第一个成员,偏移量为0
;
name
偏移量为sizeof(int) = 4
;
score
偏移量为sizeof(int) + 50 = 54
(实际需考虑对齐,此处简化)。 - 生成访问指令:
stu->id
会被编译为:访问地址stu + 0
;
stu->name
编译为:访问地址stu + 4
。
关键点:无需手动计算偏移量,编译器自动处理!
数组通过基地址 + 索引 × 元素大小定位元素(如 arr[i]
)。结构体成员定位类似:
基地址(结构体首地址) + 成员偏移量。
但区别在于:
- 数组元素:类型相同,偏移量计算规则统一(
i * sizeof(element)
)。 - 结构体成员:类型不同,偏移量由编译器在编译时静态计算(如
id
偏移量恒为0)
为什么不能直接像数组那样通过指针算术访问?
虽然理论上可通过首地址+偏移量直接访问:
但不推荐这样做,因为:
- 破坏可读性:箭头运算符
->
更清晰。 - 忽略内存对齐:编译器可能在成员间插入填充字节(Padding),手动偏移可能错位
传递结构体指针时,函数内通过 stu->id
修改成员的本质是:
首地址 + 编译器预计算的成员偏移量 → 定位目标内存 → 直接修改原始数据。
此过程由编译器保证正确性,无需开发者关心偏移量细节
此时你也明白了,为什么结构体访问可以使用->
该符号了吧,为什么数组不行,原因就是这里。
妙!!!!!! 妙!!!!!!妙!!!!!!
2、函数调用层级太多,一直会分配栈空间,那么也可能会导致栈溢出,避免嵌套太多。
栈顶地址怎么计算?
首先根据全局变量这些,以及mallcon这种看需要申请多少等等,然后得出一个数据,那么在根据我们需要的栈空间分配大小,最终就得到了栈顶地址。
通过编译后的.map函数就可以看出来,各种变量分配的空间。
根据走自身工程数据,进行一部分分析:
首先我们在函数中自己定义了栈的大小1024个字节。
Stack_Size EQU 0x00000400
.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)
首先说说明的是
.data 0x20000088 Section 8 ntc_drv.o(.data)
表示 ntc_drv.o(.data) 共有8个字节,并且其实地址是0x20000088
0x20000088
0x20000089
0x2000008A
0x2000008B
0x2000008C
0x2000008D
0x2000008E
0x2000008F
以上这8个地址,刚好分配了
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)
表示 rh_drv.o(.data) 共有6个字节,并且其实地址是0x20000090
以此类推进行分配RAM空间,也就是.data和.bss段。
.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。
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
) = 静态内存结束地址 + 栈大小。
0x20000000 0x08000750 0x00000024 Data RW 165 .data led_drv.o0x20000024 0x08000774 0x00000004 PAD0x20000028 - 0x00000400 Zero RW 648 STACK startup_gd32f30x_hd.o
可以看出前两个步骤分配完成以后SRAM的空间是0x20000028,然后在跟我们上述定义的栈空间大小,我们就可以得出
- 静态内存结束地址为
0x20000428 - 0x400
=0x20000028
(栈空间 1KB =0x400
字节)
→ 表明静态内存占用了0x20000000~0x20000028
的空间(约 40 字节)。
Stack_Size EQU 0x00000400 //表示的是1KB,400=1024。
如果我们把栈空间进行修改,
Stack_Size EQU 0x00000800
可以看出前面的内存分布并没有发生改变,改变的只是最后栈空间的大小
0x20000000 0x08000750 0x00000024 Data RW 165 .data led_drv.o0x20000024 0x08000774 0x00000004 PAD0x20000028 - 0x00000800 Zero RW 648 STACK startup_gd32f30x_hd.o
最终得到的是栈顶地址是:0x20000828
__initial_sp 0x20000828 Data 0 startup_gd32f30x_hd.o(STACK)
文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。
【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。