原文:https://www.eefocus.com/GorgonMeducer/blog/11-02/204604_66653.html
AVR Mega系列单片机是广大电子爱好者所熟悉和喜爱的。在后51时代,它以易开发——使用以C和BASIC为代表的高级语言;易使用——内部集成了大量常用的外设模块;高性能——同等时钟下是传统C51执行效率的12倍;低功耗——用水果电池就可以驱动等特点占据了相当的市场份额,得到了广泛的应用。
什么是AVR的绝对定位呢?
简单说就是在C语言环境下将工程中某个函数或者数据放在AVR存储器中自己“心仪”的位置。对代码来说,定位的最小单位是函数;对数据来说,定位的最小对象是全局变量、静态局部变量和全局常量。举例来说,在下面的代码片断里,全局常量g_strBuffer和全局变量g_hwDataBuffer以及函数Example中的静态局部变量s_bFlag都是可以进行绝对定位的。
C语言中,变量的空间分配方式有两种:静态分配和动态分配。所谓静态分配就是指在程序编译的时候,变量所占用的空间大小以及他们在存储器中的具体位置就已经确定了——除非我们特殊指定(也就是绝对定位),否则编译器会自动替我们做好这些工作。前面的例子中,全局变量和静态局部变量都是静态分配的。所谓动态分配,是指在程序运行的时候,由程序自己通过某种机制动态管理存储器的分配方式。就局部变量n来说,在函数Example被调用时n获得属于自己的空间,而在函数退出时这些空间会被自动释放。可见,n所使用的存储器是重复使用的,其地址也总是动态变化的,对这样的局部变量进行绝对定位就如同刻舟求剑一般——是不可能做到的。
const char g_strBuffer[] = “Hello world”;
unsigned int g_hwDataBuffer[10] = {0};
void Example(void)
{
unsigned char n = 0;
static unsigned char s_bFlag = 0;
…
}
结论:只有代码和静态分配的变量才能参与绝对定位。
推论:C语言中只有函数、全局变量和静态局部变量才能参与绝对定位。
什么场合下需要使用绝对定位呢?
通常在什么场合下我们会需要使用绝对定位技术呢?
1、 比如我们希望编写一段程序能够通过与外界通讯实现自我升级——也就是不借助编程器而实现Flash的更新。对于这样的应用,AVR要求用户必须把更新Flash相关的某些代码放置到专门的Boot区域中。如何编写Bootloader并不在本文的讨论范围之内。
2、 某些应用中,我们希望把一些变量放置在指定的位置,比如使用外扩SRAM的时候,我们希望把一些体积庞大的数组放置到扩展的SRAM空间中,而保留更多的内部SRAM供系统使用。
3、 对于某些需要大量查表的应用,如果表格本身的数据是常量,在SRAM吃紧的情况下,为了节省空间,我们往往会希望把这些表格从SRAM挪到Flash中,从而留下更多的可用内存空间。
如此等等,不一而足。总结说来我们往往会因为“需要实现某些硬件功能”,“需要获取更大的连续存储空间”,“需要节省空间”或者“满足某些特殊的应用需求”,而试图借助绝对定位技术实现所需的功能。
绝对定位技术的实现
既然有这么多场合牵扯到绝对定位技术,那么我们该如何去做呢?有统一的方法没有?很遗憾,没有。代码和变量的绝对定位并不是C/C++标准的一部分,不仅不同的编译器使用的方法不同,同一个编译器在不同版本中处理的方法也有可能大相径庭。笔者习惯于将绝对定位问题分成三类,分别是:代码的绝对定位、同一存储器内变量的绝对定位和跨存储器的变量绝对定位。在后面的讨论中,我们会按照这样的划分将问题展开,以常用的三种编译环境(ICC、IAR和GCC)为平台,分别介绍它们具体的实现方法。
在具体介绍之前,有一个有趣的问题不得不提:什么是地址空间?可以从0x0000开始计数的地址都属于同一个空间,这个空间就是地址空间。SRAM的地址可以从0x0000开始计数,因此存在SRAM地址空间;FLASH可以从0x0000开始计数,因此存在FLASH地址空间;同样EEPROM可以从0x0000开始,也就存在EEPROM地址空间。同一个地址,比如0x1234,在不同的地址空间中表达的意义当然也不一样:在SRAM里表示一个RAM单元,在FLASH里表示一个FLASH字节;在EEPROM里表示一个ROM单元。如果觉得抽象,你大可以把一个地址空间想象成一张纸,所以SRAM有一张纸,FLASH有一张纸,EEPROM也有一张纸。把这样三张纸叠放在一起,即便你一笔戳下去戳出同样的窟窿,三个窟窿也是分别落在不同平面上的。这可以用来解释了为什么下面的代码运行起来往往得到的是错误的结果:
//假设这里的FLASH是一个宏,作用是告诉编译器其修饰的变量应该存放在Flash中
FLASH unsigned char strHello[] = “Hello world”;
…
extern void LCD_OUT(unsigned char *pchBuffer, unsigned char chLength);
…
LCD_PRINT((unsigned char *) strHello, sizeof(strHello));
首先,很容易看出代码的作者是想在LCD上输出一个“Hello world”,但实际运行的结果往往是LCD上出现了一串与“Hello world”等长的乱码。这是因为,字符串数组strHello是放置在Flash中的,其地址属于Flash地址空间;函数LCD_ PRINT需要的参数pchBuffer是一个指向RAM地址空间的指针,也就是说这个指针指向的数据应该是存放在RAM中的。代码的作者也注意到,如果单纯使用
LCD_ PRINT (strHello, sizeof(strHello));
来调用函数肯定会遇到编译器指针不匹配的错误(strHello的地址是“FLASH unsigned char *”型的,而函数LCD_ PRINT需要的是一个“unsigned char *”的地址),因此,代码作者试图通过强制类型转换让编译器“闭嘴”,这就好比把FLASH地址空间和SRAM地址空间叠在一起,用strHello的地址在两张纸上戳了一个洞——FLASH空间中,对应的位置存储的的确是“Hello world”,但谁也没有规定在SRAM相同的位置存储的也是“Hello world”。所谓张冠李戴,不过如此。
介绍了这么多,今天我们就先从代码的绝对定位技术开始演练。
>> 程序代码的绝对定位
所谓代码的绝对定位,就是将指定的函数放到Flash中指定的地方。对ICC,IAR和GCC而言,实现起来最简单的要数IAR了,比如:
void Example(void) @ 0x1234 //将函数放置在以地址 0x1234 为起始的Flash上
{
…
}
上面的代码通过在函数后增加一个“@”符号和一个具体的地址就完成了对函数的绝对定位,可谓简单干脆。这是IAR编译器的为我们提供的一大便利,称之为“立即绝对定位”。这里的“立即”和汇编语言中“立即数”的“立即”在意义是类似的。在纯粹的C/C++语言环境下,GCC不支持“立即绝对定位”;ICC虽然在版本7.15之前支持这种定位方式,但在随后的版本中却取消了。作为一种被放弃的特性,本文也不作介绍。
与“立即绝对定位”相对,还有一种被称之为“范围绝对定位”的技术在ICC,IAR和GCC中得到更普遍的应用。取字面上的意思,所谓“范围绝对定位”是指将某些函数放置到某一个指定的范围内,在这个范围内,这些函数之间的位置关系是相对的,或者说是它们之间的位置关系我们并不关心——函数的空间分配和定位全部交由编译器来负责。你可以理解为,“范围绝对定位”就好比是定义了一个瓶子:瓶子的容量(也就是范围的大小)决定了能容下多少函数;而把瓶子放哪儿,就是需要我们指定(绝对定位)的了。这里必须要注意:通常情况下一个范围不能跨越两个不同的地址空间。
图1 打开GCC工程配置 |
1 GCC环境
借助AVR Studio4,我们可以利用图形化的界面定义一个范围。假设,我们已经使用AVR Studio4建立了一个GCC工程,此时,依次选择菜单Project->Configuration Options打开工程设置窗口(如图1所示);在窗口左侧的悬浮列表中选择Memory Settings。单击Add按钮,添加一个新的范围,这里也称为段(Segment),如图2所示:
图2 添加一个新的Flash段MyZone |
__attribute__ ((section(“.MyZone”))) + 函数原型
比如,我们希望将函数MyFuncitonA放置到“.MyZone”中:
__attribute__ ((section(“.MyZone”))) void MyFunctionA(void);
或者
__attribute__ ((section(“.MyZone”)))
void MyFunctionA(void);
也可以
void MyFunctionA(void) __attribute__ ((section(“.MyZone”)));
为了让代码看起来更容易懂一些,我们可以事先定一个宏:
#define FLASH_MYZONE __attribute__ ((section(“.MyZone”)));
然后用这个新的宏来修饰函数,比如
FLASH_MYZONE void MyFunctionA(void);
或者:
void MyFunctionA(void) FLASH_MYZONE;
基于以上的定义,我们可以通过下面的代码完成之前的目标:
void MyFunctionA(void) FLASH_MYZONE; //将MyFunctionA放置到MyZone中
void MyFunctionB(void) FLASH_MYZONE; //将MyFunctionB放置到MyZone中
从这个例子中我们容易看出,__attribute__ ((section(“.MyZone”))) 一次只对一个函数原型有效,其放置的位置并不是非常严格。其实,每一个工程都有一个默认的Flash段“.text”用于放置用户的代码;也就是说,如果我们没有使用__attribute__ ((section(“.MyZone”)))来制定代码的存放位置,实际上函数就被自动加入到“.text”段中。反过来想一想,如果我们在AVR Studio4的Memory Settings中,定义一个同名的“.text”段并指定了地址,结果会怎样呢?有兴趣的你可以试验一下。
2 ICC环境
ICC环境下添加新范围要更为直接一些,打开工程编译选项,在Target选项卡中找到“Other Options”文本框,按照如下的语法填入绝对地址:
-b<范围名称>:<绝对地址>
我们依然以MyZone为例,则将代码定位到0x4000地址上的写法为:
-bMyZone:0x8000
细心的你也许已经注意到,明明是定位到0x4000地址上,为什么这里要写作0x8000呢?为什么看起来是一个两倍的关系呢?原因很简单,AVR的Flash实际上是以2个字节的WORD为单位的,所有的AVR指令也都是2个字节(16位)长度的。因此,当我们定位代码的时候,应该使用WORD(也就是2字节长度)作为基本单位,这就能够保证每一条指令在绝对定位以后依然能够被单片机正确的识别(这种方式我们通常称之为对齐到“字”)。ICC编译器绝对定位使用的单位是“字节”,0x4000的“字”地址换算为“字节”也就是0x8000。
前面我们曾提到,在代码中GCC一次只能重新定位一个函数;而ICC编译器通过一个扩展的预编译语法结构#pragma可以批量的将一定范围内的函数都定位到目标范围内。据体语法如下:
#pragma text: <自定义区域名称> // 将随后的函数都放置到自定义范围内
……
#pragma text: text // 将随后的函数都放置到默认的text范围内
……
仍然以MyZone为例,对MyFunctionA和MyFunctionB的定位可以写为:
#pragma text: MyZone // 将随后的函数放置到MyZone里
void MyFunctionA(void);
void MyFunctionB(void);
#pragma text: text // 将随后的函数放置到默认的text段内,也就是恢复普通的定位
是不是很方便?不同版本的ICC都支持这一定位方式,可以放心使用。此外,ICC对范围的定义不光可以指定起始地址,还可以指定范围的大小,其语法为:
-b<范围名称>:<起始地址0>.<中止地址0>:<起始地址1>.<中止地址1>:……
比如:
-bMyZone:0x1000.0x2000:0x3000.0x4000
实际上将MyZone定义为一个不连续的区域,由0x1000到0x2000和0x3000到0x4000两个区块组成。详细的使用方法可以参考ICCAVR用户手册Help->Help Topics->PROGRAMMING THE AVR->Addressing Absolute Memory Locations 章节的描述,这里就不再一一为您展开。
3 IAR环境
与ICC类似,IAR环境下可以在工程选项中加入以下的语法结构实现范围的定义:
-Z(CODE)<范围名称>=<起始地址>-<中止地址>
例如:
-Z(CODE)MyZone=8000-A000
将MyZone定义在了0x4000至0x5000的Flash地址空间上。这里括号里面的CODE表示代码地址空间,也就是片内Flash。同样,我们可以看出IAR的绝对定位也是以“字节”而不是“字”为基本单位的。对于一个打开了的工程,添加范围的具体操作也并不复杂:首先,在Workspace窗口中选中位于最顶端的工程名称,单击右键选择Options;在弹出窗口的Category列表中选择Linker;在右边的设置窗体选择最右边的Extra Options选项卡,选中Use command line options,并在下面的文本框中加入: “-Z(CODE)MyZone=8000-A000”。
IAR和GCC一样,在代码中一次只能给一个函数进行绝对定位,其语法为:
#pragma location=”<范围名称>”
例如:
#pragma location=”MyZone”
void MyFunctionA(void) {…}
#pragma location=”MyZone”
void MyFunctionB(void) {…}
这里#pragma表达式的位置只能位于目标函数的正上方。其实IAR的范围定义非常灵活和细致,除了通过-Z表达式以外,还可以使用-P表达式来定义范围,只不过这个范围可以和已有的范围重叠;对于放置到该范围内的函数,系统会自动从已用范围的牙缝中寻找一个空挡来安置它们,可谓“斤斤计较”。更为详尽的内容可以从IAR Help目录的IAR C/C++ Compiler Reference Guide,Part1 Using Compiler的Placing Code and Data章节获得。