几乎每个嵌入式程序都需要处理一些不变的数据量;也就是在程序运行的时候这些量的值不会发生改变。举个例子,我认为大多数人会觉得相当吃惊,当调用:
printf("Hello");
后输出的不是Hello字符串。很明显,像"Hello"这样的字符串应该是不变的量。除了文字常量,许多程序还需要配置数据,状态转换表,或者常数系数,这些都应该是不变的。我们通常将不变的量称为只读的,与之相反的是可以读写的变量。
在许多桌面应用中,只是逻辑上而不是物理上区分只读和读写。链接器可能将所有的只读数据放在一个数据段中来给促进程序加载到内存中去。
在许多嵌入式系统中,只读和读写不仅仅只是逻辑上的区别了。大多数嵌入式系统程序不像桌面应用程序那样从磁盘中加载数据。嵌入式系统中,只读数据将永久保存在ROM中。很明显,读写数据不能放在ROM中;它们必须放在RAM里面。因此,编译器必须要能够区分读写数据和只读数据,这样它才有办法将只读数据放入ROM中,将读写数据放入RAM里。
事实上,就ROM和RAM而言,在嵌入式系统中都是很缺乏的。例如,一些系统在生产线上让所有的model使用同一个控制程序,并且为每个根据每个model的需求来配置相应的数据。这些系统将二进制代码和只读数据分别放在不同的存储段中,以便将数据放在ROM中和代码区分开来。因此,虽然每个model使用不同的ROM来存储配置数据,但是在生产线上每个model都可以使用相同ROM来存储二进制代码。
典型的C/C++嵌入式系统编译器根据这些需求,它们将代码和数据映射到以下几个不同的逻辑段:
O 代码(Code)段(也叫<i>text</i>段):只读段,包含程序代码
O 文字(Literal)段:只读段,包含初始化数据
O 初始化数据(也叫<i>plain data</i>):读写段,包含用于程序启动的初始化数据
O 未初始化数据(也叫<i>bss</i>):读写段,包含一些未初始化的数据,直到程序使用到。
编译器和链接器也可以提供一种像#pragma一样的开关或者命令使得你可以将一些逻辑段放入一个物理段中。例如,你可以将文字常量和代码固化到ROM中,也可以和初始化的数据一起放入RAM。在这个段模型下,将文字常量放到ROM中是相当容易的。编译器会将所有的文字常量收集到文字常量段中。链接器和其他的后端工具会确保将文字常量段放入ROM中。
将非文字常量放入到ROM中是一个比较复杂点的问题。编译器必需将初始化的只读数据和初始化的读写数据区分开来。很明显,未初始化的数据不能是只读的,如果这样的化那么就是没用的量,所以它必需放在RAM中。然而,区分初始化的数据的方法并不是很明显。
例如,当一个编译器遇到如下声明:
unsigned char two_to_the[]
= { 1, 2, 4, 8, 16, 32, 128, 256 };
编译器是将其放入到文字常量段,还是初始化数据段中呢?
即使two_to_the是放在RAM中的读写数据,初始化数据的复制本可能出现在ROM中。在程序启动的时候将初始化的值从ROM中复制到RAM。
大多数嵌入式开发工具支持将初始化的数据放到ROM中,当编译一个源文件的时候,编译器将所有初始化的数据放到一个段中。默认情况下这个段就是初始化数据段。然而,你可以使用一个编译器开关告诉编译器将初始化的数据复制到文字常量段中。因此,你可以将所有需要设置为只读的数据集中到一个源文件中并使用相应的开关来将他们放入文字常量段。这个方法有一个要注意的就是你需要根据你物理上的需求来整理某些部分,而不是逻辑上的。由此你可能会想将一些只读量和读写数据放在使用它们的同一个源文件中去,但是不能这样干。你必需将只读数据放在一个单独的源文件中,程序需要做出更多的努力来读取它们 [1]。
许多编译器提供pragmas这样的开关。它可以在一个相同文件中将数据放到一个不同的段中。举个例子:
#pragma data(“literal”)
unsigned char two_to_the[]
= { 1, 2, 4, 8, 16, 32, 64, 128 };
#pragma data()
第一个pragma告诉编译器将以下的定义放入到文字常量段中去。第二个pragma告诉编译器返回到之前的段中去。
不幸的是,编译器们在pragma语法上都不太一样,有时显得很戏剧性。C和C++标准都说明了pragma语法的存在性,但是没有强制编译器们要支持某种特定的pragma语法。以此,使用到pragma的代码移植性很差。
以上的方法都有一个问题,就是没有提供一种防止对只读变量写操作的方法。一个编译器应该为以下这种操作报错:
two_to_the[i] = 0;
但是不幸的是却不可以。你可能都不会注意到这个错误直到你运行程序。
接下来进入const修饰符。const修饰符可以使得编译器检测到对只读数据的写操作。例如:
const unsigned char two_to_the[]
= { 1, 2, 4, 8, 16, 32, 64, 128 };
将two_to_the定义为“包含只读数据的字符型数组”。const是two_to_the类型符的一部分,编译器用它来核实以后对two_to_the的操作的合法性。举个例子,在上面的声明后,下面这样一个复制语句:
two_to_the[i] = 0;
是一个编译错误。
仅仅使用const修饰符是不足以将其放入到ROM中去的。你同样需要使用链接器和后端工具将其放入到你想放的位置中去。const修饰符只是提供了一个开始而已。
将数据放到ROM仅仅只是const修饰符几个用法中的一个。还有,尽管使用了后端工具相应的支持,在声明的时候时候const修饰符也不一定能确保将数据放到ROM中去。因为声明必需满足其他的语法限制。在未来的几个月,我将细说这些限制并且讲述const的其他用法。<small>[ESP]</small>
本文是翻译自: Dan Saks 的 Placing Data into ROM