【转】http://blog.sina.com.cn/s/blog_68f9692f0100jwr4.html
今天同事遇到一个内存溢出的问题,在帮忙解决过程中发现自己对这些东西还没有彻底弄清楚,就搜集了一些资料整理一下,受益匪浅。以前也记过笔记,但是时间一长又忘了,还是放在这里好了。
一.51的存储器结构
笼统来说单片机片内存储器分为CODE区和data区,cpu从code区读取指令,对data区的数据进行运算处理。前者在程序运行中为只读,一般为FLASH,用来放置程序代码和一些只读的数据(如字模表之类),后者可以随机读写,用来存放程序运行中的临时数据,如局部变量或全局变量,全局变量一直占用着RAM内存,而局部变量在使用完后会自动清除RAM空间。当然在片外,可以外扩FLASH和RAM(此时称为XRAM,因为寻址方式不一样),外扩的大小与单片机寻址能力有关。有的单片机将外扩RAM封装在片内,如AT89C51,所以有了所谓的片内XRAM。
二.变量的存储模式
变量是一种在程序执行过程中能不断变化的量。它有数据类型、存储类型、存储器模式和有效范围四种属性。C语言规定每个变量必须有一个标识符作为变量名,在使用一个变量前,必须先对变量进行定义,指出其数据类型和存储模式。以便编译系统为其分配存储单元。在C51中对变量进行定义的格式如下:
[存储类型] 数据类型 [存储器类型] 变量名表,如auto char data i;
存储类型指明变量的存储区域,而变量的存储类型和变量在程序中说明的位置决定了它的作用范围。存储类型含义与C语言相同。存储类型有四种:auto型、extern型、static型、register型,缺省为auto型(看来我一直都是凹凸型啊)。其区别如下:
auto:自动变量。存储在内存的堆栈区,属于临时性存储变量,并不长期占用内存,可以被多次覆盖。
register:寄存器变量。register与auto一样属于自动类别。区别在于register的值保存在CPU的寄存器中。计算机中只有寄存器中的数据才能直接参与运算,而一般变量是放在内存中的,变量参加运算是,需要先把变量从内存中取到寄存器中,然后计算。所以一般把使用最频繁的变量定义成register变量。register变量只能在函数中定义,并只能是int和char型。
static:静态变量。声明静态变量的,也就是C语言中的私有成员.如果在一个函数中声明一个静态变量,静态变量的空间不在栈里面,而是存储在静态空间里,这个函数结束后,静态变量的值依旧存在,内存不会收会此变量占用的内存空间,而是等整个程序都结果后才收回静态变量空间。
extern:外部类型。extern用来声明外部变量,可以用于此程序外的程序中(可在两个C文件间交叉使用),类型要一致。变量在数据运行时被分配了一定的内存空间,该空间在整个运行程序中,只要程序存在,自始自终都被该变量使用,即其值始终不变。
数据类型就不用多说了,bit,byte,char什么的。
存储器类型与单片机的寻址方式有关,影响程序的执行效率。下表是传统C51的存储器类型,不同单片机类型有所差别。
空间名称 |
地址范围 |
说明 |
DATA | D:00H~7FH | 片内RAM直接寻址区 |
BDATA | D:20H~2FH | 片内RAM位寻址区 |
IDATA | I:00H~FFH | 片内RAM间接寻址区 |
XDATA | X:0000H~FFFFH | 64K片外RAM数据区 |
CODE | C:0000H~FFFFH | 64K片内外ROM代码区 |
BANK0~BANK31 |
B0:0000H~FFFFH : : B31:0000H~FFFFH |
分组代码区,最大可扩展32X64KB ROM,应该只能以BANK为单位读写。 |
简单解释一下:
data: 低128字节,可直接寻址,可以用acc直接读写的,速度最快,生成的代码也最小。
bdata:16字节位寻址区(当然也可以按字节寻址),一般很小。
idata: 固定指前面0x00-0xff的256个RAM,其中前128和data的128完全相同,只是因为访问的方式不同。idata是用类似C中的指针方式访问的。汇编中的语句为:mox ACC,@Rx。
xdata: 外部扩展RAM,一般指外部0x0000-0xffff空间,有的集成于片内,用DPTR访问。
pdata: 外部扩展RAM的低256个字节,用movx ACC,@Rx读写。这个比较特殊,而且C51好象有对此BUG,建议少用。
定义变量时如果省略了“存储器类型”,则按编译模式SAMLL, COMPACT,LARGE所规定的默认存储器类型确定变量的存储区域,(#pragma 预编译命令,可以指定函数的默认存储器模式。)C51编译器的三种存储器模式(默认的存储器类型)对变量的影响如下:
1. SMALL:变量被定义在8051单片机的内部数据存储器中,因此对这种变量的访问速度最快。另外,所有的对象,包括堆栈,都必须嵌入内部数据存储器,而堆栈的长度是很重要的,实际栈长取决于不同函数的嵌套深度。
2. COMPACT:变量被定义在分页外部数据存储器中,外部数据段的长度可达256字节。这时对变量的访问是通过寄存器间接寻址(MOVX@Ri)进行的,堆栈位于8051单片机内部数据存储器中。采用这种编译模式时,变量的高8位地址由P2口确定。
3. LARGE:变量被定义在外部数据存储器中(最大可达64K字节),使用数据指针DPTR来间接访问变量,访问数据速度慢,增加程序代码的长度。
存储器模式决定了缺省变量的存储空间,而访问各空间变量的汇编代码的繁简程度决定了代码率的高低。
例如:一个整形变量i,如放于内存18H、19H空间,则++ i的操作编译成四条语句:
INC 0x19
MOV A,0x19
JNZ 0x272D
INC 0x18
0x272D:
而如果放于外存空间0000H、0001H则++i的操作编译成九条语句:
MOV DPTR,0001
MOVX A,@ DPTR
INC A
MOVX @ DPTR,A
JNz #5
MOV OPTR,#0000
MOVX A,@DPTR
INC A
MOVX @ DPTR,A
对于函数而言,一个函数的存储器模式确定了函数的参数和局部变量在内存中的地址空间,规则与变量定义一致。在定义一个函数时可以明确制定该函数的存储器模式,一般的形式如下:
函数类型 函数名(形式参数表) [存储器模式]
其中的存储器模式是选项,未说明时则按该函数编译时的默认存储器模式处理。
例如:
#pragma large
int func1(int a1, int a2) small
{
int c;
……
……
}
int func2(int b1, int b2)
{
int c;
……
……
}
#pragma 预编译命令,可以指定函数的默认存储器模式。C51允许采用存储器的混合模式编程,充分利用单片机中有限的存储器空间,同时还可以加快程序运行的速度。
三、网友的一点经验
1.data区空间小,所以只有频繁用到或对运算速度要求很高的变量才放到data区内,比如for循环中的计数值。
2.data区内最好放局部变量。
因为局部变量的空间是可以覆盖的(某个函数的局部变量空间在退出该函数是就释放,由别的函数的局部变量覆盖),可以提高内存利用率。当然静态局部变量除外,其内存使用方式与全局变量相同;
3.确保你的程序中没有未调用的函数。
在Keil C里遇到未调用函数,编译器就将其认为可能是中断函数。函数里用的局部变量的空间是不释放,也就是同全局变量一样处理。这一点Keil C做得很愚蠢,但也没办法。
4.程序中遇到的逻辑标志变量可以定义到bdata中,可以大大降低内存占用空间。
在51系列芯片中有16个字节位寻址区bdata,其中可以定义8*16=128个逻辑变量。定义方法是:bdata bit LedState;但位类型不能用在数组和结构体中。
5.其他不频繁用到和对运算速度要求不高的变量都放到xdata区。
6.如果想节省data空间就必须用large模式,将未定义内存位置的变量全放到xdata区。当然最好对所有变量都要指定内存类型。
7.当使用到指针时,要指定指针指向的内存类型。
在C51中未定义指向内存类型的通用指针占用3个字节;而指定指向data区的指针只占1个字节;指定指向xdata区的指针占2个字节。如指针p是指向data区,则应定义为: char data *p;。还可指定指针本身的存放内存类型,如:char data * xdata p;。其含义是指针p指向data区变量,而其本身存放在xdata区。
四.关于STARTUP.A51
用C语言编程时,开机时执行的代码并非是从main()函数的第一句语句开始的,在main()函数的第一句语句执行前要先执行一段起始代码,这段起始代码的源程序名为STARTUP.A51。其作用可以看看源代码,简单来说就是进行变量的初始化,设置SP指针、堆栈空间等。如果考虑冷复位和热复位时的数据保存问题可以通过修改它实现(貌似不推荐)。
BTW,冷复位用英文来表示是Restart,热复位用英文来表示是Reset。我们把单片机从没加电到加上电源,而自动产生的复位称为冷复位;单片机在已经通电的情况下,给它一个复位信号,称为热复位。冷复位会使单片机的特殊功能寄存器和数据存储器的内容都改变;而热复位只是特殊功能寄存器的内容改变而单片机的内部数据存储器的内容不变。
六.关于中断
我都困了,坚持做完吧,难得这么整理一回。
中断时很好用的,与查询方式分半边天。C51编译器支持在C语言程序中直接编写51单片机的中断服务程序,C51编译对函数定义进行了扩展,增加了一个关键字interrupt,interrupt是函数定义时的一个选项,加上它函数将函数定义成中断服务函数。
函数类型 函数名(形式参数表) [interrupt n][using n]
interrupt 后面的n为中断号,n的取值范围为0~31,编译器从8n+3处产生中断向量。C51编译器还扩展了一个关键字using,专门用来选择单片机的寄存器组,缺省时由编译器选择一个寄存器组作为绝对寄存器组访问。
对于这个using,多说几句,普通函数也可以用。任何时候,单片机只能用到四组寄存器中的一组,一般情况下keil来自动分配,用USING来选择其中的一组,目的是提高效率减少出入栈次数。
编写8051单片机中断函数时应遵循以下规则:
(l)中断函数不能进行参数传递,如果中断函数中包含任何参数声明都将导致编译出错。
(2)中断函数没有返回值,如果企图定义一个返回值将得到不正确的结果。因此建议在定义中断函数时将其定义为void类型,以明确说明没有返回值。
(3)在任何情况下都不能直接调用中断函数,否则会产生编译错误。因为中断函数的返回是由8051单片机指令RETI完成的,RETI指令影响8051单片机的硬件中断系统。如果在没有实际中断调求的情况下直接调用中断函数,RETI指令的操作结果会产生一个致命的错误。
(4)如果中断函数中用到浮点运算,必须保存浮点寄存器的状态,当没有其它程序执行浮点运算时可以不保存。
(5)如果在中断函数中调用了其它函数,则被调用函数所使用的寄存器组必须与中断函数相同。用户必须保证按要求使用相同的寄存器组。否则会产生不正确的结果,这一点必须引起足够的注意。如果定义中断函数时没有使用using选项,则由编译器选择一个寄存器组作绝对寄存器组访问。
七、关于volatile
顺眼看到,就贴过来看看。
volatile是类型修饰符,影响编译器编译的结果,一般这个修饰符用来告知编译器,被修饰的变量是个“易变的”变量(volatile的本意是“易变的”),与volatile变量有关的运算,不要进行编译优化,以免出错。(加volatile关键字的变量有关的运算,将不进行编译优化。)
例如:volatile int i=10;
int j = i;
int k = i;
(1)volatile 告诉编译器i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取,因而编译器生成的可执行码会重新从i的地址读取数据放在k中。
(2)而优化做法是,由于编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在k中。而不是重新从i里面读。这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问,不会出错。
常用情况:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义。