嵌入式内存管理

1 单片机内存

1.1 程序为什么需要内存

程序需要内存是因为计算机在执行程序时需要存储指令和数据。内存是计算机用于临时存储和操作数据的地方,它可以被快速访问和修改。

具体来说,程序需要内存有以下几个原因:

  1. 存储指令:程序代码被编译成机器指令,并存储在内存中。当计算机执行程序时,CPU会从内存中读取指令并按照顺序执行。这些指令包含了计算、跳转、条件判断等操作,使得程序能够实现各种功能。
  2. 存储数据:程序执行过程中需要操作数据,例如变量、数组、结构体等。这些数据被存储在内存中,使得程序能够读取、修改和保存数据。
  3. 调用函数:程序通常会调用函数来实现特定功能。函数调用涉及到将函数参数传递给函数、保存当前执行状态(例如返回地址和寄存器值),以及在函数执行完毕后回到原来的执行位置。这些过程都需要使用内存来保存和管理数据。
  4. 运行时堆栈:程序在执行过程中会使用堆栈(Stack)来管理函数调用和返回。堆栈用于存储函数的局部变量、函数参数、返回地址等信息。堆栈的使用使得函数调用可以嵌套,并且能够正确地回到调用者的执行点。
  5. 动态内存分配:一些程序需要在运行时动态分配内存,这时候需要使用堆(Heap)来存储动态分配的数据。例如,在C/C++中使用malloc或new函数来动态分配内存。

总的来说,内存在程序执行过程中扮演着临时存储指令和数据的角色,使得程序能够正常运行并实现各种功能。不同的程序会使用不同大小的内存,这取决于程序的复杂性和需求。计算机的内存大小决定了它能够运行多大规模的程序和处理复杂的任务。

1.2 STM32内存划分

1.2.1 整体内存视图

在这里插入图片描述

从上图可以看出整体内存划分为如下描述:

  • SRAM:CPU和主内存之间构建高速缓存,提供更快的数据访问速度,从而提升计算机系统的性能。

    比如当我们执行a ++的时候,第一步在内存(sram)取出a的值,放到主内存中(arm寄存器),第二步pc指针 取出加法指令、主内存的数据加1,第3步将主内存的数据搬移到sram中。STM32F10xxx具有高 达96K字节的静态SRAM功能。它可以作为字节、半字(16位)或全字(32位)进行访问。SRAM的起始地址为0x2000 0000。

  • peripherals:片上外设,比如我们要控制的GPIO,就是配置各种寄存器。在外设地址上划分了一块区域表示GPIO,如下图所示。

在这里插入图片描述

在这里插入图片描述

  • Flash memory:用来保存程序指令和一些掉电不会丢失的代码。上面有512K的flash
  • System memory:厂家出厂时候烧写进去的一些固件,比如支持串口升级的固件。
  • Option Bytes:保存一些关键字
  • 内核地址:ST格式使用的是arm内核,arm内核有自己的寄存器,后面专门有arm架构专题

1.2.2 程序如何选择内存区运行

在这里插入图片描述

我们知道,通过配置单片机boot引脚可以选择程序从哪启动运行。X表示不关心的意思。

  1. 当boot0接地的时候,我们的程序是从flash里面运行,也就是0x80000000地址开始运行,我们下载程序的时候也是下载到这个区域

  2. 当boot0接VCC,BOOT接地;我们是从厂商里面的代码启动,也就是上电会运行boot程序。例如下面设计的串口启动电路。

在这里插入图片描述

首先,下载软件控制DTR输出低电平,则DTR#为高,然后设置RTS为高,则RTS#为低,这样Q2导通了,BOOT被拉高,即实现设置BOOT为VCC,同时Q1也会导通,RESET被拉低,STM32进入复位状态。然后,延时100ms后,下载软件再控制DTR为高电平,则DTR#为低电平,RTS维持高电平,则RTS#继续为低电平,此时STM32的复位引脚,由于Q1不再导通,而变为高电平,STM32结束复位,但是BOOT还是维持为VCC,从而进入bootloader模式,接着下载软件就可以开始连接STM32,下载程序了,从而实现一键下载。

下载完成以后,下载软件再次控制DTR输出低电平,则DTR#为高电平,此时由于RTS#还是低电平,所以,

Q1导通,STM32再次复位,延时100ms,最后,下载软件控制RTS输出低电平,则RTS#为高电平,BOOT由于R5下拉,变成低电平,同时STM32结束复位,开始运行用户下载的程序。

在这里插入图片描述

  1. 当boot都是VCC的时候,现在SRAM运行代码,用于sram掉电不保存,只是用来调试的时候去选择该模式

在这里插入图片描述

我们程序是烧录在Flash里的,Flash的起始地址为0x08000000。CPU怎么从0x00000000开始运行呢?

这涉及到STM32F1的启动模式,它有三种启动模式,分别是从片上闪存启动、从片上系统闪存启动运行bootloader 和 从片上SRAM启动。我们通过控制BOOT引脚来决定怎么启动。
Flash就是我们常用烧录代码的地方
System memory处启动:厂商会烧一些固件,支持通过串口烧录程序到板子
SRAM处启动:SRAM掉电数据会丢失,当我们调试程序的时候需要大量的改动代码的时候,可以把程序烧入SRAM进行调试,调试好了以后在烧入Flash中。
在这里插入图片描述

上图就是具体怎么控制引脚来决定怎么启动。系统会把对应的启动地址赋值给0x00000004,我们开机取值的之后就指向了我们要求的启动位置。

1.3 内存寻址

1.3.1 内存条实物

在这里插入图片描述

每个内存颗粒有512M的空间,每个内存颗粒由是由8个bank构成。每个bank是512MB。

1.3.2 内存芯片工作原理

在这里插入图片描述

bank就好比工厂里面的仓库、只不过这里的的bank是保存数据的。一颗内存芯片有8个bank.

在这里插入图片描述

每个bank里面由内存单元构成,也是保存数据的最小内存单元。我们知道每一个芯片512MB,512MB = 51210241024 = 536,870,912Byte内存单元,1个芯片有8个bank ,每个bank有512MB/8 = 67,108,864Byte内存单元。

在这里插入图片描述

每个内存单元又是由8个电路构成,由晶体管和电容构成,绿色的线叫做bit线、蓝色的叫字节线,导通控制级连接到字节线上。

在这里插入图片描述

当我们给bit线和字节线通电,晶体管就会导通,电荷就会存在电容中。就表示了二进制1

在这里插入图片描述

当我们给字节线通电,bit线不通电,电荷就会从bit线溜走,电容没有电荷。就表示二进制0.

1.3.3 CPU内存寻址硬件

在这里插入图片描述

看图我们知道了,内存寻址需要CPU内存控制器、地址总线、数据总线。

1.3.4 CPU内存寻址方式1

CPU通过数据总线和地址总线和内存单元绑定在一起,现在有16进制数据0x123456789ABCDEF0.CPU是如何将这个数据保存到内存中,又是怎么取出来的呢?

  • 先将数据转化为二进制数据

在这里插入图片描述

橘红色的表示8*8 = 64为数据。

  • CPU通过内存控制器生成逻辑地址、后面转化为物理地址,分为行地址、和列地址

在这里插入图片描述

  • 通过行地址和列地址定位唯一1个内存单元地址。将数据传送到内存单元中,这就是内存寻址。

在这里插入图片描述
在这里插入图片描述

我们传输一个8字节的数据,需要传送8次,浪费了CPU的时间。

  • 取数据的过程正好相反,首先内存寻址、生成行地址和列地址、通过数据总线将数据读取出来。

在这里插入图片描述

在这里插入图片描述

1.3.5 CPU内存寻址方式2

在读取和写入的时候我们只对1个bank去操作,剩下的bank都没有操作,64位的数据总线我们只用了8位。造成资源浪费。

解决办法如下:只需1次寻址就写入了数据。大大提高了CPU访问内存的效率。

在这里插入图片描述

1.3.6 内存不对齐的处理

1.3.6.1 多次寻址

在这里插入图片描述

上面内存中已经有2个数据,这个时候我们如何保存8个数据。

方式一:

在这里插入图片描述

  • 寻址定位

在这里插入图片描述

  • 由于上面的2个内存有数据,只有先存前面6个数据,

在这里插入图片描述

  • 第二次寻址定位到8个内存单元,但是只需前面2个内存单元。

取出数据的时候需要寻址2次,还需要将2次读取的数据进行合并,浪费了很多时间、处理难度大。

优点就是没有浪费存储空间

1.3.6.2 内存对齐(大多数解决方式)

解决办法:写入另外一行。

在这里插入图片描述

优点:提高效率,只需要1次寻址定位,不需要额外处理。

缺点:浪费存储空间。

1.5.6.3 C语言结构体内存对齐

了解了存储器的寻址方式,我们就知道我们在嵌入式C结构体里面的内存单元是怎么划分的了示例如下

结构体内存对齐的规则通常受编译器、编译选项和目标硬件的影响。以下是一些常见的结构体内存对齐规则:

  1. 自然对齐:结构体成员按照自然大小对齐。例如,int类型通常是4字节对齐,char类型是1字节对齐,等等。结构体成员的起始地址通常是该成员自身大小的倍数。
  2. 最大对齐:结构体成员按照所有成员中最大对齐要求对齐。这意味着结构体成员的起始地址通常是最大对齐要求的倍数。
  3. 指定对齐:有时候,你可以通过编译器的特定语法或预处理指令来指定结构体成员的对齐方式。例如,#pragma pack__attribute__((packed))指令可以用于取消对齐或设置指定对齐。

//此代码在32位Linux下编写
typedef struct _st_struct1
{
char a;
short b;
int c;
}st_struct1;

printf(“%ld\n”,sizeof(st_struct1));

打印结果为:8. 让我们分析一下为什么结果为8.

我们知道按照对齐规则要保证最大对齐,char 暂用1个字节、short为2个字节,他们子让对齐下是3个字节,但是又要同时满足最大对齐,所以 char 和short暂用4个字节。int 的自然对齐和最大对齐都同时满足。所以暂用的空间是4+4 = 8字节。

typedef struct _st_struct1
{
char a;
int b;
short c;
}st_struct1;

printf(“%ld\n”,sizeof(st_struct1));

打印结果为:12 让我们分析一下为什么结果为12

我们知道按照对齐规则要保证最大对齐,char 暂用1个字节、int为4个字节,他们子让对齐下是5个字节,但是又要同时满足最大对齐,所以 char需要用到4个字节。int 的自然对齐和最大对齐都同时满足。所以暂用的空间是4+4 = 8字节。short 自然对齐占用2个字节,但是要满足最大对齐4.所以short暂用4个字节,所以4+4+4 = 12个字节。

1.3.7 STM32的数据总线

在STM32F103微控制器中,数据总线主要是指AHB(Advanced High-performance Bus)和APB(Advanced Peripheral Bus)两种总线。这些总线是用于连接CPU和存储器、外设以及其他核心功能模块的主要数据通路。

在这里插入图片描述

1.3.8 STM32的地址总线

  • 在STM32F103微控制器中,地址总线是CPU核心(ARM Cortex-M3内核)的一部分,它是内部硬件,不是外部可见的物理引脚。地址总线负责生成用于访问内存和外设的地址信号。STM32F103内部的地址总线由CPU核心和存储器控制器组成。CPU核心是处理器的计算和控制中心,它执行指令和操作数据。存储器控制器用于协调CPU与内部存储器(如Flash和SRAM)以及外设之间的数据传输

  • 在STM32F103中,CPU核心通过地址总线生成访问内存和外设的地址信号。这些地址信号将发送给存储器控制器,以执行读取或写入操作。存储器控制器根据接收到的地址信号,将数据从相应的内存地址或外设寄存器中读取出来,或者将数据写入到相应的地址中。

  • 虽然地址总线不是外部可见的物理引脚,但是在程序员编写代码时,需要使用内部寄存器和指令来控制地址总线,从而正确访问内存和外设。具体的地址生成和访问过程由CPU核心和存储器控制器自动处理,程序员只需正确配置和使用相应的内存地址和外设地址即可。

1.4 C语言如何访问内存

1.4.1 C语言对内存地址的封装

在这里插入图片描述

如图所示:单片机执行int a; a++;代码在内存的访问过程

  1. 定义int a;a是给sram的地址起一个名称,本质上是地址上划分了1个区域,该区域保存了a的值。
  2. 当执行a++,arm控制器从flash里面取出指令执行LDR :从ram的地址取出a的值,放到R0寄存器中。
  3. arm控制器从flash里面取出 ADD指令:ADD R0,#1.
  4. 在ALU运算控制单元中执行R0 = R0+1,从flash取出STR指令:STR R0,【addrA】,将计算的结果放到ram中。

取出指令的过程:是通过R15寄存器,也就是PC寄存器,PC寄存器保存flash里面代码的2进制指令。通过PC取出指令区执行代码。

1.4.2 用指针来间接访问内存

关于类型(不管是普通变量类型int float等,还是指针类型int * float *等),只要记住:
类型只是对后面数字或者符号(代表的是内存地址)所表征的内存的一种长度规定和解析方法规定而已。
C语言中的指针,全名叫指针变量,指针变量其实很普通变量没有任何区别。譬如int a和int *p其实没有任何区别,a和p都代表一个内存地址(譬如是0x20000000),但是这个内存地址(0x20000000)的长度和解析方法不同。a是int型所以a的长度是4字节,解析方法是按照int的规定来的;p是int *类型,所以长度是4字节,解析方法是int *的规定来的(0x20000000开头的连续4字节中存储了1个地址,这个地址所代表的内存单元中存放的是一个int类型的数)。

1.4.3 用数组来管理内存

数组管理内存和变量其实没有本质区别,只是符号的解析方法不同。(普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析方法不一样)。
int a; // 编译器分配4字节长度给a,并且把首地址和符号a绑定起来。
int b[10]; // 编译器分配40个字节长度给b,并且把首元素首地址和符号b绑定起来。

数组中第一个元素(a[0])就称为首元素;每一个元素类型都是int,所以长度都是4,其中第一个字节的地址就称为首地址;首元素a[0]的首地址就称为首元素首地址

1.5 STM32数据在内存的体现

1.5.1 程序复位过程

在这里插入图片描述

在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HHlPchWt-1690952584119)(D:\C语言高级专题\image-20230801165450696.png)]

在板子一上电或者复位的时候,板子做的第一件事情是读取 0x0000 0000 - 0x0000 0003,把读到的值指向MSP(主栈指针),然后读取0x0000 0004 - 0x0000 0007存放的复位向量,把读取到的值指向Reset_Handler。到这一步位置都是处理器自动完成,不需要写代码。

在这里插入图片描述

上图能将跳转关系体现的更加明显。在0x00000000读取到栈顶地址0x20000428。然后读取后四个字节0x00000004的值是0x08000c59,这个地址指向Reset_Handler的地址0x08000c58。我们发现0x0800c59和0x0800c58之间差了一个数,这是因为奇数能证明其在Thumb状态下执行。最后在复位程序中有一步可以跳转到我们的main函数。

  1. 板子做的第一件事情是读取 0x0000 0000 - 0x0000 0003,把读到的值指向MSP(主栈指针)
  2. 然后读取0x0000 0004 - 0x0000 0007存放的复位向量,把读取到的值指向Reset_Handler
  3. 初始化程序计数器指针 PC
  4. 设置中断向量表的入口地址
  5. 调用 SystemIni() 函数配置 STM32 的系统时钟
  6. 设置 C库的分支入口“__main”
  7. 在_main中初始化ZI段,将没有初始化的、初始化为0的变量初始为0,将RW段的数据从flash中拷贝到rw中,
  8. 初始化C库函数。跳转到main函数。

1.5.2 用户程序在flash里面的组织架构

在这里插入图片描述

在这里插入图片描述

  • 当用户下载程序到flash里面,首先将中断向量表烧写到flash的起始地址,如上图红框所示。

  • 接着将各种C文件里面的函数所用到的处理器指令保存到代码区,如下图所示。

在这里插入图片描述

  • 文件中有些const常量和字符串常量需要保存到flash里面,还有一些初始化不为0的变量,如下图所示,

在这里插入图片描述

1.5.3 用户数据在sram的组织架构

在这里插入图片描述

在这里插入图片描述

程序组件所属类别
机器代码指令Code
常量RO-data
初值非0的全局变量RW-data
初值为0的全局变量ZI-data
局部变量ZI-data栈空间
使用malloc动态分配的空间ZI-data堆空间

RO-size = code + RO-data:(程序占用flash空间的大小)

RW-size = RW -data +ZI-data :(运行程序占用ram的大小)

ROM-size = RO-size +RW-data (烧写程序占用flash的大小)

1.5.4 堆、栈是什么?我们为什么要有堆栈

C语言的运行必须要堆栈的环境,因此我需要在程序的一开始设置好堆栈,以便运行C函数

  • 栈的作用是用于局部变量,函数调用,函数形参等的开销
  • 堆主要用来动态内存的分配

在这里插入图片描述

上图是C语言的执行环境,我们有只读区、可读写区、堆区、共享库的内存映像、栈区。共享库的内存映像里封装了库函数之类的东西可供调用;可读写区中data是指初值非0的全局变量,bss指初始化为0的全局变量。

1.5.4.1 详细解读栈的作用

1.5.4.2 详细解读堆的作用

1.5.5 加载地址 链接地址 运行地址 存储地址

  • 加载地址:将指令或数据从地址A拷贝到地址B,地址A就是加载地址。比如将要出初始化的全局变量RW从flash地址A拷贝到sarm中的RW区域,那A地址叫加载地址。

  • 链接地址:由链接脚本文件指出,链接的时候确定

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00040000 { ; load region size_region
ER_IROM1 0x08000000 0x00040000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x0000C000 { ; RW data
.ANY (+RW +ZI)
}
}

  • 运行地址:程序在内存中运行时候的地址。

在这里插入图片描述

  • 存储地址:指令或数据在flash中存放的存储地址,就是存储地址

在这里插入图片描述

1.5.6 程序和数据加载的过程

问题1:程序或数据的链接地址要和运行地址一致,但往往程序或数据的链接地址和运行地址不一样,因此需要代码重定向。

  1. 程序入口点:链接地址指定了程序的入口点,告诉微控制器从Flash存储器的哪个地址开始执行程序。这通常是main函数的地址,即程序的起始执行点。
  2. 代码和数据的存放位置:链接器使用链接地址将目标文件中的代码和数据放置在正确的Flash存储器地址上。这确保了代码和数据的正确映射,使得程序能够正确地访问和执行。
  3. 防止地址冲突:链接地址的正确设置可以避免代码和数据之间的地址冲突。如果多个目标文件有重叠的地址,链接器将负责解决这些冲突,并确保每个部分都正确放置在Flash存储器的不同地址上。
1.5.6.1 位置无关代码

位置无关代码(Position-Independent Code,PIC)是一种在内存中加载时不依赖于固定内存地址的机器代码。这种代码的特点是可以被加载到任意内存地址,而不需要对其进行重新定位(Relocation)。

相比之下,位置无关代码不依赖于固定的内存地址,而是使用相对地址或符号来引用数据和函数。这使得它们能够在不进行重新定位的情况下加载到内存中,并且能够在不同的内存地址上正确运行。位置无关代码在动态链接和运行时加载等场景中特别有用,例如在共享库、动态链接库(DLL)等中广泛使用。

1.5.6.2 位置有关代码

传统的编译生成的代码通常是位置相关的,即指令和数据引用了固定的内存地址。

1.5.6.3 代码重定向

果加载地址和编译时的预设地址不一致,就需要进行重新定位,将其中的地址引用进行修正,以确保代码可以正确运行。这种重新定位会增加加载和启动代码的复杂性,特别是在多任务系统和共享库的情况下

1.5.6.4 解决办法
  1. 使用相对寻址:代码中的地址引用是相对于指令的位置的偏移量,而不是绝对地址。这使得代码可以在加载时进行动态调整。
  2. 使用全局偏移表(GOT):在一些架构中,全局偏移表用于存储全局变量和函数的地址。代码引用全局变量和函数时,通过GOT来间接引用,使得代码可以加载到任意地址。
  3. 使用基址寄存器:某些架构和操作系统提供了特殊的寄存器,如x86中的EBP或ARM中的PC(程序计数器),用于实现位置无关代码。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值