理清存储器和程序的关系

  先抛出两个问题:对于嵌入式设备,没有RAM能运行吗?没有ROM能运行吗?

  首先看一下什么是ROM。ROM全称Read Only Memory,即只读存储器,其中内容在生产阶段一经烧录就无法更改。显然,现代语境下的ROM已经不是字面意思了,更准确的解读是:用来存放程序的非易失性存储器。即它最重要的特性是非易失性,当年因成本而导致的显著特性“只读”已经随着技术发展而不存在了,但是“只读”这个名字却沿用下来了。所以,下次再看到ROM的时候,脑海中要自动把它翻译成:存放程序、非易失。

  接着再来看一下什么是RAM。RAM全称Random Access Memory,即随机访问存储器。所谓“随机访问”,指的是读取或写入数据时,所需要的时间与数据所在的位置无关。当然RAM诞生以来一直也都有其他特点:与CPU直接交互、存取速度快、掉电不保存。但为什么偏偏把“随机访问”这个特性作为它的名字呢?难道当时除了RAM其他存储器都不支持随机访问?事实也正是如此,早期的存储设备,如纸带、磁鼓、磁盘都是顺序存储器,因此把这个与众不同的特性直接作为名字了。同样的,随着技术的发展,随机访问也不再是RAM特有的特性了,比如带有地址线和数据线的Nor Flash,就支持随机读取。因此下次再看到RAM的时候,脑海中要自动把它翻译成:速度快、易失。

  接下来开始分析上面提出的两个问题。

问题1:可以没有RAM吗?

  众所周知,存储在存储器中的程序可以分成两部分:代码和数据。代码肯定是少不了的,那么数据呢?数据又可以分成两类:不变数据和变化数据。不变数据(接下来以常量称之)比如一个固定的字符串,我们可以像对待代码一样对待它,只需要把它放到一个固定的位置,然后需要用的时候通过地址找到它就可以了。而对于变化数据(接下来以常量称之),在程序运行过程中值会发生变化。你可能会说:那我们也可以把它放到一个固定的位置,通过地址找到他,需要改的时候直接改它不就行了吗?直接改当然也可以,但是这里存在几个问题:

  1. 一般非易失性存储器的写都比较费劲儿又费时,比如Flash,由于它在写之前要先擦除,所以要先备份,然后修改,最后写入,虽然功能没问题,但是速度非常慢,严重影响程序运行效率;
  2. 这种变量我们一般只是作为中间值来使用,并不要求它掉电之后保存,并且它的修改频率通常非常高,如果频繁写的话会很快消耗存储设备的寿命。因此最好把变量放到另一个契合它的特点的存储器里面,它具有以下特点:存取速度快、掉电不用保存数据。

  聪明的你肯定已经想到了,假如我的程序不需要用到变量,那就不需要RAM了。存在这样的程序吗?当然存在,现在我们就来写一个这样的程序,下面直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
                PRESERVE8
THUMB

; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY

__Vectors DCD 0 ;__initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
__Vectors_End

AREA |.text|, CODE, READONLY

; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
LDR R0, =my_main
BX R0
ENDP

my_main PROC
MOV R0, #0
loop ADD R0, R0, #1
B loop
ENDP

; Dummy Exception Handlers (infinite loops which can be modified)

NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
ALIGN
END

  这段代码是启动文件的精简版,也是整个工程里面唯一一个文件,没有引用任何外部库和文件。中断向量表只保留了3个必要的中断,由于没有用到栈,因此向量表的第一条栈地址直接写了0。在复位异常处理程序中跳转到my_main函数,my_main函数的功能非常简单,R0初始化为0,然后不停的自增。看起来是没什么用,但是如果把R0的bit0赋值给某个连着LED的IO就能实现跑马灯了。

  系统默认的分散加载文件需要修改一下,先在设置把使用系统生成的分散加载文件的勾去掉,然后把.\Objects\scatter.sct拷贝到代码目录去,免得和一堆临时文件混在一起,最后把scatter.sct文件修改成如下(注意不能把sct文件加到工程中,编译会报错):

1
2
3
4
5
6
7
8
9
LR_IROM1 0x08000000 0x00080000  {    ; load region size_region
ER_IROM1 0x08000000 0x00080000 { ; load address = execution address
*.o (RESET, +First)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00010000 { ; RW data
.ANY (+RW +ZI)
}
}

  程序运行情况如下,通过单步调试可以看到寄存器窗口中的R0从0开始不断自增。


  整个程序只用到一个R0寄存器,没有使用到内存变量,又没有用到堆和栈,因此完全不需要RAM。我们来看一下它的map文件中的各段大小:

1
2
3
Total RO  Size (Code + RO Data)                   40 (   0.04kB)
Total RW Size (RW Data + ZI Data) 0 ( 0.00kB)
Total ROM Size (Code + RO Data + RW Data) 40 ( 0.04kB)

  可以看到RW段和ZI段的大小都是0

问题2:可以没有ROM吗?

  通常CPU执行程序的过程就是从存储设备上读取一条条命令(机器码),然后一条条译码执行的过程,既然能从ROM里面读命令,那当然也能从读写速度更快更方便的RAM里读。因此我们只要在上电之后把程序下载到RAM里面(IDE可以实现这种功能)就能跑起来了,因此,可以没有ROM。
  但是因为掉电之后RAM程序就没了,所以每次上电之后都要下载一遍,这种设备当然是没有实用价值的。我们这里只是为了理清概念,包括上面的没有RAM的例子也是,这样通常实现不了复杂的功能,没有现实意义。

梳理存储器和程序的关系

  现在我们把程序中包含的内容严谨的分一下类:

  1. RO-CODE,代码(代码必定是只读的,所以可以简写为CODE)
  2. RO-DATA,常量
  3. RW-DATA,初始化且不为0的变量
  4. ZI-DATA,未初始化和初始化为0的变量

  在IDE工具如Keil中,一般把RO-CODE和RO-DATA合成为RO,RW-DATA简称为RW,ZI-DATA简称为ZI。于是就有RO、RW、ZI三类数据。
  按照上面的问题的分析,显然RW和ZI在运行的时候必须处于RAM中,而RO在运行时既可以处于ROM中,也可以处于RAM中,或者两者都有,只要能正常取指令即可。既然RAM的存取速度比ROM快很多,理想情况下当然是RO全部放在RAM中执行。显示情况是RAM往往比ROM贵很多,因此容量不足以容纳下全部RO段内容,此时可以选择把一部分段或者函数放到RAM中,剩余的在ROM中执行。

总结

  分散加载文件的作用就是把程序中的各种段、函数、变量等合理的分配到ROM和RAM中的具体位置中。