[编者按]为使广大嵌入式系统应用技术人员系统地了解和掌握一些先进应用、开发技术,本刊从创刊号起开辟《学习园地》栏目。上半年将集中介绍嵌入式C编程技术(一)~(六),内容包括单片机C语言应用程序设计中的变量定义和变量空间、C语言编程技巧、函数有效使用及混合编程技术。
嵌入式C是指在嵌入式应用中使用的C语言。在嵌入式应用中非常注重的是代码的时空效率,即产生的代码的运行时间要尽可能少,占用的存储空间(包括程序存储器和数据存储器)要尽可能小。单片机在国内的嵌入式应用领域使用最多,8051是单片机教学的首选机型。现以8051为例讲解产生代码的时空效率,C编译器使用Franklin C51。用C语言进行嵌入式应用的软件开发是必然趋势,程序设计应该以C语言为主,汇编语言为辅。对汇编语言掌握到只要可以读懂程序,在时间要求比较严格的模块中进行程序的优化即可。尽管懂汇编语言不是目的,但懂得一些汇编语言可帮助了解影响C语言效率的8051特殊限定。例如,懂得汇编语言指令就可知道使用片内RAM做变量存储的优势,因为片外变量需要几条指令才能设置累加器和数据指针来存取。最好的嵌入式应用的编程者应是由汇编语言转用C语言,而不是原来用过微机标准C语言的人员。下面将从变量定义和变量空间、C语言技巧、函数的有效使用、混合编程几部分讲述嵌入式C编程技术。本文不涉及C语言基础,可作为北京航空航天大学出版社出版的《单片机的C语言应用程序设计》(修订版)的提高篇。
一、 变量定义和变量空间
C语言是一种类型化语言。它要求对程序中使用的变量,在使用之前必须加以定义,说明其存储属性、存储类型和数据类型,以便编译系统给它分配存储位置。
采用C语言编程,编译器可以自动完成变量的存储空间的分配。寄存器分配、不同存储器的寻址及数据类型等细节问题由C编译器管理,因而变量的定义至关重要,它关系到数据存储器单元的使用和分配,也关系到产生代码的长度。对8位单片机来讲,直接支持的变量类型只有无符号字符和位。编程时要注意两个原则,其一,总是使用可能的最小数据类型。8051系列单片机是8位机,因此,显然对具有“char”类型的对象的操作比“int”或“long”类型的对象要方便得多。C51编译器直接支持所有的字节操作,因而如果不是运算符要求,就不做“int”类型的转换。这可用一个乘积运算来清楚说明,两个“char”类型对象的乘积与8051指令“MUL AB”刚好相符。如果用整型量完成同样的运算,则需要调用库函数。其二,只要有可能,使用“unsigned”数据类型。8051系列单片机并不直接支持有符号数的运算,因而C51编译器必须产生与之相关的更多的代码以解决这个问题。如果使用无符号数据类型,产生的代码要少得多。
无论何时,应尽可能地使用无符号字符变量,因为它能直接被8051所接受。如果使用有符号和无符号两种数据类型,那么就得使用两种格式类型的库函数,占用的存储空间成倍增长。须要进行额外的代码操作来测试符号位,这无疑会降低代码效率。
使用缩写形式定义数据类型:
#define uchar unsigned char
#define uint unsigned int
这样,在以后的编程中就可以用uchar代替unsigned char,用uint代替unsigned int来定义变量。头文件reg51.h中有所有8051的SFR及可位寻址位的定义,只要:
#include
这样,在以后的编程中就可以使用:
P1=0x10;
TMOD=0x01;
TR0=1;
EA=1;
可以养成习惯,在每个程序的开头都加上以下三行:
#include
#define uchar unsigned char
#define uint unsigned int
本文下面将直接使用uchar和 uint,不再进行说明。
这部分讲述C语言编程中的变量定义和变量空间。首先介绍单片机数据处理及其存储器结构特点,然后讨论变量的存储类型和变量存储区域的关系,变量存储属性和变量有效范围的关系,接着是C语言有效编程的几个相关主题。
·只读变量和变量空间
讨论在程序执行过程中只读的常数限定变量。可通过使用“#define”定义减少变量占用空间,提高对象的存取效率。
·使用自动变量减少变量占用空间
讨论如何使用“auto”类型变量减少变量空间的大小,这样只有定义它们的函数执行时,变量的存储单元才保留。
·使用位域减少变量占用空间
讨论如何使用位域减少变量占用空间。
·使用联合减少变量占用空间
讨论如何使用联合减少变量占用空间。
1. 存储空间中的对象定位
8051系列单片机的存储器是哈佛结构的,其特点是将程序存储器(ROM)和数据存储器(RAM)分开,并有各自的寻址机构和寻址方式。8051系列单片机在物理上有四个存储空间:
·片内(内部)程序存储器;
·片外(外部)程序存储器;
·片内(内部)数据存储器;
·片外(外部)数据存储器。
在定位变量时,经常访问的数据对象应放在内部数据RAM中。访问内部数据存储器要比访问外部数据存储器快得多。内部数据RAM由寄存器组、位寻址区域和其他由用户定义的变量共享。由于片内RAM容量的限制(128~256字节,由使用的单片机决定),必须权衡利弊以解决访问效率和这些对象数量之间的矛盾。
在讨论Franklin C51定义变量的数据类型的时候,必须同时提及它的存储类型与8051单片机存储器结构的关系。因为Franklin C51是面向8051系列单片机及其硬件控制系统的开发工具。它定义的任何数据类型必须以一定的存储类型的方式定位在8051的某一存储区中,否则便没有任何的实际意义。
在8051系列单片机中,程序存储器与数据存储器严格分开,特殊功能寄存器与片内数据存储器统一编址,这是单片机与一般微机的存储器结构有所不同的两个特点。C51编译器完全支持8051单片机的硬件结构,可完全访问8051硬件系统的所有部分,支持直接寻址和间接寻址方式。该编译器通过将变量、常量定义成不同的存储类型(data,bdata,idata,pdata,xdata,code)的方法,将它们定位在不同的存储区中。
存储类型与8051单片机实际存储空间的对应关系如表1所列。
表1
当使用code存储类型定义常量数据时,C51编译器会将其定位在程序代码空间(ROM或EPROM)。访问内部数据存储区(data,bdata,idata)比访问外部数据存储区(xdata,pdata)相对要快一些,因此,可将经常使用的变量置于内部数据存储区。
例:
bit flag; 布尔值
code uchar table[]={1,2,3,”help”,oxff};
idata uint temp;
data char var; / char data var; 等价,尽量用后一种
static unsigned long xdata array[100]; 静态变量
extern float idata x,y,z; 外部变量
uint pdata dimension;
uchar xdata vector[10][4];
char bdata flags;
sbit flag0=flags^0;
sbit P11=P1^1;
如果省略存储类型,存储模式将自动决定变量的默认存储类型。存储模式作为编译选项如表2所列。
表2
在C语言中每个变量和函数都有其存储属性。存储属性关系到变量和函数的可见性与生存期,即变量和函数所占存储位置的作用域和时限。
变量的可见性:指变量在其作用域内是可以访问的,或者说是变量的可见区域。变量的生存期:指在一定范围内变量值保存的时限。
由于变量的存储属性不同,有的变量可在程序的各个函数中使用,称全局变量或全程变量;有的只能在特定函数中使用,称局部变量;有些可为所属文件中的函数使用,称局部于文件函数的局部变量。
C语言的存储属性分为三类:自动的、外部的和静态的,其相应的存储类别说明符分别是:
auto自动的
extern外部的
static静态的
Franklin C51支持“基于存储器的指针”和“一般指针”。基于存储器的指针由C源程序中的存储类型决定其存储单元数。用这种指针可高效访问对象,且只需1至2字节。而一般指针需要3字节:2字节偏移和1字节存储类型。指针的定义如表3所列。
例:
char xdata*px;指针自身在默认存储区(取决于存储模式)
char xdata*data pdx;指针明确位于内部数据存储区(data)
data char xdata*pdx;与上同
表3
2. 动态和静态分配的变量
在C程序中,在一个函数中定义的自动变量是动态分配的变量。
自动变量是:
-局部变量的一种;
-在一个函数中定义的变量;
-只能在其定义的函数内部访问。
当调用一个函数时,函数中的自动变量在RAM或寄存器中分配存储。当一个函数执行完时,分配的RAM单元或寄存器释放。因此,自动变量仅在定义它们的函数内部可以访问。
在C程序中,外部变量(在函数外用“extern”说明定义)和静态变量(用“static”说明定义)是静态分配的变量。
·外部变量(extern)
-在函数外部定义;
-在模块中任何位置可以访问;
-在存储器中分配固定的存储单元。
·静态变量(static)
-只能在定义的有效范围内使用;
-在存储器中分配固定的存储单元。
变量的存储空间是动态分配还是静态分配取决于它在程序中定义的位置。外部变量在函数外定义,在RAM中分配变量的存储空间。由于变量的RAM单元已分配,因而,它可以在程序模块中的任何地方访问。
“static”定义的静态变量虽然也在RAM中分配单元,但“静态变量有效范围”的存在,使得静态变量的可见性取决于它在程序中定义的位置。在函数外定义的静态变量称作静态全局变量,而在函数内部定义的静态变量称作静态局部变量。
静态局部变量又称内部静态变量。它在函数内定义,但不像自动变量那样,当函数调用它时就存在,退出函数它就消失。静态变量始终存在着,但函数外部不能存取它,只能在函数内部访问,退出函数后,虽然数据仍保存,但不能访问。
静态全局变量又称外部静态变量。它在函数外面被定义,它的作用域从被定义的地方开始,一直到源文件结束。但一个程序由若干个文件组成时,静态全局变量始终存在着,但它只能在被定义的文件内访问,可供该文件内的所有函数共享此数据,退出文件后,虽然数据仍保存,但不能被其他文件访问。
文件1:
init();
extern
int numproc;
int currid;
int semno;
int nextsem=0;
static int nextproc=100;
null()
{
int userpid=10;
init();
currid=userpid;
nextsem++;
semno=100-nextsem;
return(semno); }
init()
{
static int num=50;
int i=0;
int j;
return(num--);
}
文件2:
extern null();
extern int numproc;
extern int currid;
extern int semno;
extern int nextsem;
int numproc=100;
static int nextproc=50;
start()
{
int pid=0;
if(null()>0) pid++;
}
“num”是静态局部变量,它分配的RAM单元仅有定义它的init()函数可以使用。“nextproc”是静态全局变量,“nextproc”变量仅在定义它的源程序文件中可见。另一个程序文件不能访问到它分配的单元,因而,两个“nextproc”变量分配不同的存储单元。
综上可知,静态变量在定义它的函数或文件之外是不可访问的、未知的,但它们在函数调用之间可以保存其值,这一特征使静态变量很有用处,如用它编写通用函数和库函数时,这些函数可被其他编程者使用。
(1) 自动变量
自动的存储属性是C语言中使用最广泛的一类。按照缺省规则,在C语言程序中,每个函数的函数体所说明的变量,凡不加存储属性说明的,都表示为自动存储类别。当然,对这些变量说明时,也可以在前面缀以说明符auto,但习惯上通常都采用缺省形式,例如:
char c;
等价于
auto char c;
自动变量的作用域,仅局限于定义该变量的函数,因而是局部变量。其可见性与生命期和作用域相同。
自动变量只在定义它的函数调用时,系统才对它分配存储空间,开始它的生命期;在从函数返回时,它所占的存储位置就自动被抹除,从而结束它的生存期。因此,C语言将这种局部于函数的变量称为自动变量。
由于自动变量随函数的返回而消失,其存储位置被抹除,故其值不能被保留。若程序下次再调用该函数,虽然又对此自动变量分配存储空间,但它不会保留前次运算得到的结果,故在函数体中必须给它进行赋值。
因为自动变量的可见性局限于定义它的函数,对其他函数来说,它是不可见的,故其名可以被其他函数重复使用,它们各有自己的作用域,不会产生误解,例如:
main()
{ int i,j,k;
…
}
fun()
{int i,j,k;
…
}
其中两个函数main()和fun()都定义了变量i、j、k,虽然它们都是自动变量,各自局限于定义它们的函数,两者之间没有相关的联系。
局部变量的概念可以扩充到分程序,即对于分程序中的自动变量,其作用域只局限于该程序内。在分程序嵌套使用时,若同名变量,其内层变量的作用域也是局限于定义它的那个层次。
C编译器总是尝试在寄存器里保持局部变量。这样,将索引变量(如for和while循环中计数变量)定义为局部变量是最好的。只要有可能,在函数中使用自动变量。
(2) 外部变量
外部变量的存储属性说明符是extern。按照缺省规则,凡是在所有函数之前,在函数外面定义的变量都是外部变量,定义时可以不写说明符extern,但在函数体内定义外部变量时,必须要用extern说明。
一个外部变量定义后,系统就给它分配固定的存储空间,所有的函数和分程序都可以使用它。当退出一个分程序,或从某一函数返回后,外部变量的空间并不回收,其值也仍然保留,可供其他函数所公用。故此,外部变量是全局变量。其作用域、可见性和生存期是整个程序。
C语言允许将大型程序分解为若干个独立文件,分别进行编译,然后连接在一起,从而提高编译速度,且便于管理。此时,当某变量需要在所有文件中通用时,则只要在一个文件中说明其为全局变量,而在其他文件中用extern说明该变量就可以了。
例如,现有两个文件File1和File2,它们都要用到3个变量i、j和ch,则在分别编译的程序模块中,对全局变量的说明如下:
File1
int i,j;
char ch;
main()
{
…
}
func11()
{
i=120;
…
}
File2
extern int i,j;
extern char ch;
func21()
{
…
i=j/10;
…
}
func22()
{
…
j=10;
…
}
其中,File2的extern通知编译程序,它所定义的变量已在别的文件中说明,使编译程序知其类型和名称,而不用再为它们分配存储空间。
(3) 静态全局变量
在全局变量说明前加上static说明符就构成静态全局变量。它是一种作用域受限制的外部变量。其作用域是从说明处开始直至该文件的末尾,即它只在被定义的这个文件中是可见的。虽然它也是全局变量,但其他文件无法改变其内容,从而可以避免其他文件可能引起的错误。
源文件:
int timeint();
int time;
static int count=0;
int list()
{
int flag=0;
if(++count>=60)/* line 8 */
flag=timeint();
return(flag);
}
int timeint()
{
int temp;
if(!--temp){
time=10;
count-=60;/* line 18 */
}
temp=time*count;/* line 20 */
return(temp);
}
“count”变量在函数的外部定义是静态全局变量。“count"变量在RAM中分配存储单元保存它的值,可以在它编译连接后生成的映像文件(.M51)中看到它分配的内部RAM单元的地址,在列表文件(.LST)中可以看到其存储单元的访问和存储器的使用情况。
映像文件:
D:000AH SYMBOL count
D:000CH PUBLIC time
D:0008H SYMBOL flag
D:0002H SYMBOL temp
“count”变量分配的内部RAM单元的地址是0AH。
列表文件:
; SOURCE LINE # 8
0005 0500 R INC count+01H
0007 E500 R MOV A,count+01H
0009 7002 JNZ ?C0005
000B 0500 R INC count
000D ?C0005:
000D C3 CLR C
000E 943C SUBB A,#03CH
0010 E500 R MOV A,count
0012 6480 XRL A,#080H
0014 9480 SUBB A,#080H
0016 4007 JC ?C0001
; SOURCE LINE # 18
000E 74C4 MOV A,#0C4H
0010 2500 R ADD A,count+01H
0012 F500 R MOV count+01H,A
0014 74FF MOV A,#0FFH
0016 3500 R ADDC A,count
0018 F500 R MOV count,A
; SOURCE LINE # 20
001A AC00 R MOV R4,count
001C AD00 R MOV R5,count+01H
001E AE00 R MOV R6,time
0020 AF00 R MOV R7,time+01H
0022 120000E LCALL ?CIMUL
0025 AA06 MOV R2,AR6
0027 AB07 MOV R3,AR7
MODULE INFORMATION:STATIC OVERLAYABLE
CODE SIZE = 78 ---
CONSTANT SIZE = --- ---
XDATA SIZE = --- ---
PDATA SIZE = --- ---
DATA SIZE = 4 2
IDATA SIZE = --- ---
BIT SIZE = --- ---
END OF MODULE INFORMATION.
上面是和“count”变量有关源程序行对应的汇编程序以及程序存储器和数据存储器的使用情况。(待续)