当前位置: 首页 > news >正文

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

文章目录

      • 1.3.4 SRAM空间理解



1.3.4 SRAM空间理解

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

内存空间具有一定的灵活性,
![[Pasted image 20250703190449.png]]
以此单片机为例内存空间是:

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

在这里插入图片描述

地址是从高到低,并且可以自己配置。
最下面是0x2000 000

裸机很少使用malloc段

![[Pasted image 20250703190609.png]]

![[Pasted image 20250703190831.png]]

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

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

栈空间:这个里面是循环的,
![[Pasted image 20250703191203.png]]

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

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

栈空间大小是认为设定的,
![[Pasted image 20250703191702.png]]

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)时,编译器自动完成以下操作:
  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这种看需要申请多少等等,然后得出一个数据,那么在根据我们需要的栈空间分配大小,最终就得到了栈顶地址。
![[Pasted image 20250703192713.png]]

通过编译后的.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)

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

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

![[Pasted image 20250703192713.png]]

补充一个说明

![[Pasted image 20250703195058.png]]

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

ZI是:
![[Pasted image 20250703195336.png]]

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大小。
![[Pasted image 20250703195808.png]]

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

![[Pasted image 20250615214452.png]]

通过上述数据手册可以看出,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 协议进行发布。

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

http://www.dtcms.com/a/265793.html

相关文章:

  • UDP服务器主要是指什么意思?
  • 提升自动驾驶导航能力:基于深度学习的场景理解技术
  • Centrifugo 深度解析:构建高性能实时应用的开源引擎
  • RocketMQ-Dashboard页面报Failed to fetch ops home page data错误
  • 车载交换机动态MAC学习和静态MAC绑定如何获取MAC地址表
  • BitsAndBytesConfig量化及注意事项
  • 明远智睿H618:开启多场景智慧生活新时代
  • 代码随想录打卡第五天
  • TinyWebserver学习(8)-定时器
  • 深度解析:venv和conda如何解决依赖冲突难题
  • 使用netstat与grep命令结合批量查找特定内容
  • Class3图像分类数据集代码
  • 数学建模_时间序列
  • CTF Web PHP弱类型与进制绕过(过滤)
  • 【云计算】企业项目 策略授权
  • 网络层:ip协议 与数据链路层
  • C++反射之获取可调用对象的详细信息
  • 《Spring 中上下文传递的那些事儿》Part 2:Web 请求上下文 —— RequestContextHolder 与异步处理
  • 低代码实战训练营教学大纲 (10天)
  • Linux之Socket 编程 UDP
  • 自然光实时渲染~三维场景中的全局光照
  • osg加入实时光照SilverLining 天空和3D 云
  • 租车小程序电动车租赁小程序php方案
  • Flutter 3.29+使用isar构建失败
  • 创客匠人视角:知识变现与创始人 IP 打造的破局之道
  • centos7源码编译安装python3
  • SSM和SpringBoot框架的关系
  • 关于微前端框架micro,子应用设置--el-primary-color失效的问题
  • FPGA从零到一实现FOC(一)之PWM模块设计
  • 火语言 RPA:突破企业自动化瓶颈,释放数字生产力​