微控制器的不同存储器类型中有什么?

电器工程 微控制器 C 嵌入式 记忆
2022-01-06 08:24:52

有不同的内存段,各种类型的数据在编译后从 C 代码中放入。即:.text, .data, .bss, 栈和堆。我只想知道这些段中的每一个将驻留在微控制器内存中的哪个位置。也就是说,考虑到内存类型是 RAM、NVRAM、ROM、EEPROM、FLASH 等,哪些数据会进入什么类型的内存。

我在这里找到了类似问题的答案,但他们未能解释每种不同内存类型的内容是什么。

任何形式的帮助都将受到高度赞赏。提前致谢!

3个回答

。文本

.text 段包含实际代码,并被编程到微控制器的闪存中。当存在多个不连续的闪存块时,可能存在多个文本段;例如,位于存储器顶部的起始向量和中断向量,以及从 0 开始的代码;或单独的部分用于引导程序和主程序。

.bss 和 .data

可以在函数或过程外部分配三种类型的数据;第一个是未初始化的数据(历史上称为.bss,其中也包括0初始化的数据),第二个是已初始化的(非bss),即.data。“bss”这个名字在历史上来自“由符号开始的块”,大约 60 年前在汇编程序中使用。这两个区域都位于 RAM 中。

在编译程序时,变量将被分配到这两个通用区域之一。在链接阶段,所有数据项将被收集在一起。所有需要初始化的变量都会留出一部分程序内存来​​保存初始值,并且就在调用 main() 之前,变量将被初始化,通常由名为 crt0 的模块进行初始化。bss 部分由相同的启动代码初始化为全零。

对于一些微控制器,有更短的指令允许访问 RAM 的第一页(前 256 个位置,有时称为第 0 页)。这些处理器的编译器可能会保留一个关键字 likenear来指定要放置在那里的变量。同样,也有微控制器只能通过指针寄存器引用某些区域(需要额外的指令),这些变量被指定far最后,一些处理器可以逐位寻址一段内存,编译器将有一种方法来指定它(例如关键字bit)。

所以可能会有额外的段,如 .nearbss 和 .neardata 等,这些变量被收集到。

.rodata

函数或过程外部的第三种数据类似于初始化变量,只是它是只读的,不能被程序修改。在 C 语言中,这些变量使用const关键字来表示。它们通常作为程序闪存的一部分存储。有时它们被标识为 .rodata(只读数据)段的一部分。在使用哈佛架构的微控制器上,编译器必须使用特殊指令来访问这些变量。

栈和堆

堆栈和堆都放在 RAM 中。根据处理器的体系结构,堆栈可能会增长,也可能会下降。如果它长大,它将被放置在 RAM 的底部。如果它向下增长,它将被放置在 RAM 的末尾。堆将使用未分配给变量的剩余 RAM,并与堆栈相反的方向增长。堆栈和堆的最大大小通常可以指定为链接器参数。

放置在堆栈上的变量是在函数或过程中定义的没有关键字的任何变量static它们曾经被称为自动变量(auto关键字),但不需要该关键字。从历史上看,auto它之所以存在,是因为它是 C 之前的 B 语言的一部分,因此需要它。函数参数也放在堆栈上。

这是 RAM 的典型布局(假设没有特殊的第 0 页部分):

在此处输入图像描述

EEPROM、ROM 和 NVRAM

在闪存出现之前,EEPROM(电可擦可编程只读存储器)用于存储程序和常量数据(.text 和 .rodata 段)。现在只有少量(例如 2KB 到 8KB 字节)可用的 EEPROM,如果有的话,它通常用于存储配置数据或其他需要在掉电上电时保留的少量数据循环。这些没有在程序中声明为变量,而是使用微控制器中的特殊寄存器写入。EEPROM 也可以在单独的芯片中实现,并通过 SPI 或 I²C 总线访问。

ROM 本质上与 Flash 相同,不同之处在于它在工厂进行了编程(用户无法编程)。它仅用于非常大容量的设备。

NVRAM(非易失性 RAM)是 EEPROM 的替代品,通常作为外部 IC 实现。如果有电池备份,常规 RAM 可能被认为是非易失性的;在这种情况下,不需要特殊的访问方法。

尽管可以将数据保存到闪存中,但闪存的擦除/编程周期数有限(1000 到 10,000),因此它并不是真正为此而设计的。它还需要一次擦除内存块,因此仅更新几个字节很不方便。它适用于代码和只读变量。

EEPROM 对擦除/编程周期有更高的限制(100,000 到 1,000,000),因此它更适合此目的。如果微控制器上有可用的 EEPROM 并且足够大,那么您就可以在其中保存非易失性数据。但是,您还必须在写入之前先擦除块(通常为 4KB)。

如果没有 EEPROM 或太小,则需要外部芯片。一个 32KB 的 EEPROM只有 66¢,可以擦写 1,000,000 次。具有相同数量的擦除/编程操作的 NVRAM 更昂贵 (x10) NVRAM 通常读取速度比 EEPROM 快,但写入速度较慢。它们可以一次写入一个字节,也可以按块写入。

这两者的更好替代品是 FRAM(铁电 RAM),它基本上具有无限的写入周期(100 万亿)并且没有写入延迟。它与 NVRAM 的价格差不多,32KB 大约 5 美元。

普通嵌入式系统:

Segment     Memory   Contents

.data       RAM      Explicitly initialized variables with static storage duration
.bss        RAM      Zero-initialized variables with static storage duration
.stack      RAM      Local variables and function call parameters
.heap       RAM      Dynamically allocated variables (usually not used in embedded systems)
.rodata     ROM      const variables with static storage duration. String literals.
.text       ROM      The program. Integer constants. Initializer lists.

此外,启动代码和中断向量通常有单独的闪存段。


解释:

如果一个变量被声明为static或者如果它位于文件范围内(有时草率地称为“全局”),则它具有静态存储持续时间。C 有一条规则,规定程序员未显式初始化的所有静态存储持续时间变量必须初始化为零。

每个隐式或显式初始化为零的静态存储持续时间变量都以.bss. 而那些显式初始化为非零值的最终以.data.

例子:

static int a;                // .bss
static int b = 0;            // .bss      
int c;                       // .bss
static int d = 1;            // .data
int e = 1;                   // .data

void func (void)
{
  static int x;              // .bss
  static int y = 0;          // .bss
  static int z = 1;          // .data
  static int* ptr = NULL;    // .bss
}

请记住,嵌入式系统的一个非常常见的非标准设置是“最小启动”,这意味着程序将跳过所有具有静态存储持续时间的对象的初始化。因此,永远不要编写依赖于这些变量的初始化值的程序,而是在第一次使用它们之前将它们设置在“运行时”中,这可能是明智之举。

其他部分的示例:

const int a = 0;           // .rodata
const int b;               // .rodata (nonsense code but C allows it, unlike C++)
static const int c = 0;    // .rodata
static const int d = 1;    // .rodata

void func (int param)      // .stack
{
  int e;                   // .stack
  int f=0;                 // .stack
  int g=1;                 // .stack
  const int h=param;       // .stack
  static const int i=1;    // .rodata, static storage duration

  char* ptr;               // ptr goes to .stack
  ptr = malloc(1);         // pointed-at memory goes to .heap
}

在优化过程中,可以进入堆栈的变量通常最终会出现在 CPU 寄存器中。根据经验,任何没有获取地址的变量都可以放在 CPU 寄存器中。

请注意,指针比其他变量更复杂一些,因为它们允许两种不同类型的const,具体取决于指向的数据是否应该是只读的,或者指针本身是否应该是。了解差异非常重要,这样当您希望它们位于闪存中时,您的指针不会意外地进入 RAM。

int* j=0;                  // .bss
const int* k=0;            // .bss, non-const pointer to const data
int* const l=0;            // .rodata, const pointer to non-const data
const int* const m=0;      // .rodata, const pointer to const data

void (*fptr1)(void);       // .bss
void (*const fptr2)(void); // .rodata
void (const* fptr3)(void); // invalid, doesn't make sense since functions can't be modified

在整数常量、初始化列表、字符串文字等的情况下,它们可能最终以 .text 或 .rodata 结尾,具体取决于编译器。很可能,它们最终成为:

#define n 0                // .text
int o = 5;                 // 5 goes to .text (part of the instruction)
int p[] = {1,2,3};         // {1,2,3} goes to .text
char q[] = "hello";        // "hello" goes to .rodata

虽然任何数据都可以进入程序员选择的任何内存,但通常系统在数据的使用配置文件与内存的读/写配置文件匹配的情况下工作得最好(并且打算被使用)。

例如程序代码是WFRM(写少读多),而且有很多。这非常适合 FLASH。ROM OTOH 是 W 一次 RM。

堆栈和堆很小,有很多读写。那最适合RAM。

EEPROM 不能很好地适合这两种用途,但它确实适合在上电期间持续存在的少量数据的配置文件,因此用户特定的初始化数据,可能还有记录结果。