单片机C语言语法规范

二、单片机C语言语法规范

C语言是一门得到广泛应用的高级程序设计语言,对于C语言的基本用法,本书不做过多介绍,请读者参考其他教材。51系列单片机的C语言(简称C51)在标准C语言的基础上,针对51系列单片机进行了扩展,其特色主要体现在以下几个方面:

(1)C51在继承了标准C语言绝大部分特性且基本语法相同的基础上,针对特定的硬件结构有所扩展,如关键字sbit、data、idata、pdata、code等。

(2)由于单片机的系统资源相对于计算机来说较贫乏,需充分利用RAM和ROM以及外扩的存储器中的资源,因此应用C51编程时更要注意对系统资源的理解。

(3)由于51单片机的数据宽度只有8位,在编程时尽量少用浮点运算和多字节的乘除运算,多使用移位运算,可以使用无符号型数据完成就不要使用有符号型数据,尽可能地降低系统负担。

C51主要从以下几个方面针对51系列单片机CPU硬件对标准C语言的扩展。

1.特殊数据类型

C51具有标准C语言的所有标准数据类型,除此以外,为了更加有效地利用8051单片机,还加入了以下的特殊数据类型:

(1)bit位标量。bit位标量是C51编译器的一种扩充数据类型,利用它可定义一个位标量,但不能定义位指针,也不能定义位数组。它的值是一个二进制位,不是0就是1,类似一些高级语言中的Boolean类型中的True和False。

(2)sfr特殊功能寄存器。sfr也是一种扩充数据类型,占用一个内存单元,值域为0~255。利用它可以访问和操作51单片机内部的所有特殊功能寄存器。如用

sfr P1=0x90;

定义字符“P1”为端口P1在片内的特殊功能寄存器,在后续程序中可用

P1=255;

将P1端口的所有引脚置高电平。

(3)sbit可寻址位。sbit同样是C51中的一种扩充数据类型,利用它可以访问芯片内部RAM中的可寻址位或特殊功能寄存器中的可寻址位。如上述定义的

sfr P1=0x90;

由于P1端口的寄存器是可位寻址的,所以可以定义

sbit P1_1=P1^1;

P1_1为端口P1中的P1.1引脚。同样可以用P1.1的地址去写,如:

sbit P1_1=0x91;

这样在以后的程序语句中就可以用P1_1来对P1.1引脚进行读写操作了。通常位寻址定义可以直接使用系统提供的预处理文件,里面已定义好各特殊功能寄存器的简单名字。

(4)sfr16,16位特殊功能寄存器。sfr16占用两个内存单元,值域为0~65535。sfr16和sfr一样用于操作特殊功能寄存器,所不同的是它用于操作占两个字节的寄存器,如定时器T0和T1。

其余数据类型如char、enum、short、int、float、long等与标准C语言相同,当结果为不同的数据类型时,C51编译器自动完成数据类型的隐式转换。隐式转换按以下优先级别自动进行:

bit char int long float signed unsigned

转换时由低向高进行,而不是数据转换时的顺序。一般情况,如果有几个不同类型的数据同时参加运算,先将低级别类型的数据转换成高级别类型,再作运算处理,并且运算结果为高级别类型数据。

完整的数据类型见表9-1。

另外,由于8051单片机不包括捕获浮点运算错误的中断向量,因此必须由用户自己根据可能出现的错误条件用软件来进行适当的处理。

2.存储类型及存储区

C51编译器支持8051及其扩展系列,并提供对8051单片机所有存储区的访问。每个变量可以被明确地分配到指定的存储空间。由于51单片机对内部数据存储器的访问比对外部数据存储器的访问快得多,因此应当将频繁使用的变量存放在内部数据存储器中,而把较少使用的变量存放在外部数据存储器中。各存储区的简单描述见表9-2。

表9-1 C51数据类型

表9-2 存储区描述

(1)程序存储区CODE。程序存储区CODE生命中的标识符为code,在C51编译器中可用code存储区类型标识符来访问程序存储区。

下面是程序存储区声明的例子:

unsigned char code a[]={0x00,0x01,0x02,‘a’,0x03,};

(2)DATA区。内部数据RAM,只需要较少的指令时间就可存取,使用直接寻址法,因此存取速度较快,但data类型的内存只有0~0x7f,通常将需要高速存取的变量定义成data类型,而data类型内存也包含了位寻址的区域。声明举例如下:

unsigned char data system_status=0;

unsigned int data unit_id[2];

float data outp_value;

mytype data new_var;

(3)IDATA区。使用间接寻址法的内部数据RAM,地址范围为0~0xff,访问速度比data类型稍慢一些,声明举例如下:

unsigned char idata system_status=0;

char idata inp_string[16];

(4)BDATA区。BDATA区是DATA区中的位寻址区,在这个区声明变量就可以进行位寻址。BDATA区的地址范围是0x20~0x2f,共16个字节。在BDATA区中声明位变量和使用位变量的例子如下:

unsigned char bdata status_byte;

unsigned int bdata status_word;

sbit stat_flag=status_byte^4;

if(status_word^15){stat_flag=1;}

编译器不允许在BDATA区中声明float和double型变量。如果需要对浮点数的每一位进行寻址,可以通过包含float和long的联合体来实现,如:

typedef union{

unsigned long lvalue;

float fvalue;

}bit_float;

bit float bdata myfloat;

sbit float_ld=myfloat^31;

(5)PDATA区和XDATA区。PDATA区和IDATA区属于外部存储区,51单片机外部数据区最多有64KB。由于访问外部数据存储区是通过数据指针加载地址来间接访问,因此访问外部数据存储区比访问内部数据存储区慢。

对PDATA和XDATA的操作是相似的,但是PDATA只有256个字节,而XDATA区可达65536字节;对PDATA区的寻址比XDATA区快,因为对PDATA区寻址只需要装入8位地址,而对XDATA区寻址需装入16位地址。对PDATA和XDATA区变量声明如下:

unsigned char xdata system_status=0;

unsigned int pdata unit_id[2];

char xdata inp_string[16];

float pdata outp_value;

3.存储器模式

如果省略存储器类型,系统则会按编译模式SMALL、COMPACT或LARGE所规定的默认存储器类型去指定变量的存储区域。无论哪种存储器模式都可以在任何的51单片机存储区范围内声明变量。然而,把最常用的命令如循环计数器和队列索引放在内部数据区可以显著的提高系统性能。此外,变量的存储种类与存储器类型是完全无关的。指定存储器模式需要在命令行中使用SMALL、COMPACT和LARGE等3个控制命令中的一个,例如:

void fun1(void)small{};

SMALL存储模式把所有函数变量和局部数据段放在51单片机系统的内部数据存储区,这使访问数据非常快,但SMALL存储模式的地址空间受限。在写小型的应用程序时,变量和数据放在data内部数据存储器中是很好的,因为访问速度快,但在较大的应用程序中data区最好只存放小的变量、数据或常用的变量(如循环计数、数据索引),而大的数据则放置在别的存储区域。

COMPACT存储模式变量被定义在分页外部数据存储器中,外部数据段的长度可达256字节。这时对变量的访问是通过寄存器间接寻址(MOVX@Ri)进行的,堆栈位于8051单片机内部数据存储器中。采用这种编译模式时,变量的高8位地址由P2口确定。因此,在采用这种模式的同时,必须适当改变启动程序STARTUP.A51中的参数:PDATASTART和PDATALEN;用L51进行连接时还必须采用连接控制命令PDATA来对P2口地址进行定位,这样才能确保P2口为所需要的高8位地址。

LARGE存储模式所有函数和过程的变量和局部数据段都定位在8051系统的外部数据区,外部数据区最多可有64KB,这要求用DPTR数据指针访问数据。这种访问数据的方法效率是不高的,尤其是对于2个或多个字节的变量,用这种数据访问方法相当影响程序的代码长度。另外,其缺点是这种数据指针不能对称操作。

4.特殊功能寄存器(SFR)

51单片机提供128字节的SFR寻址区,地址为80H~FFH。51单片机中,除了程序计数器PC和4组通用寄存器外,其他所有的寄存器均为SFR,并位于片内的特殊功能寄存器区。该区域可位寻址、字节寻址或字寻址。特殊功能寄存器可由以下几种关键字说明:

(1)sfr声明字节寻址的特殊功能寄存器,例如sfr P0=0x80;表示P0口地址为80H。“sfr”后面必须跟一个特殊功能寄存器名;“=”后面的地址必须是常数,不允许带有运算符的表达式,且范围必须在特殊功能寄存器地址范围内。

(2)sfr16许多新的8051派生系列单片机使用两个连续地址的SFR来指定16位值,例如8052用地址0xCC和0xCD表示定时器/计数器2的低和高字节,如:

sfr16 T2=0x CC;

表示T2地址的低地址T2L=0xCC,高地址T2L=0xCD。

(3)sbit声明可位寻址的特殊功能寄存器和别的可位寻址目标。“=”后面将绝对地址赋给变量名,3种变量声明方式如下:

1)sfr_name^int_constant。

该方式用一个已声明的SFR sfr_name作为sbit的基地址(SFR的地址必须能被8整除)。“^”后面的表达式指定了位的位置,必须是0~7之间的一个数字,例如:

sfr PSW=0x D0;

sbit OV=PSW^2;

sbit CY=PSW^7;

2)int_constant^int_constant。

该方式用一个整常数作为sbit的基地址,基地址值必须能被8整除。“^”后面的表达式指定位的位置,必须在0~7之间。例如:

sbit OV=0x D0^2;

sbit CY=0x D0^7;

3)int_constant

该方式是一个sbit的绝对位地址,例如:

sbit OV=0x D2;

sbit CY=0x D7;

注意,不是所有SFR都可以位寻址,只有地址可被8整除的SFR可位寻址。

5.C51指针

(1)通用指针。C51提供一个3字节的通用指针,通用指针的声明和使用均与标准C语言相同,但它同时还可以说明指针的存储类型。通用指针用3字节保存,指针的第一字节表明指针所指的存储区空间地址,另外两个字节存储16位偏移量,但对DATA、IDATA和PDATA区,使用8位偏移量就可以了,例如:

1)long*state;为一个指向long型整数的指针,而state本身则根据存储模式存放在不同的RAM区。

2)char*xdata ptr;为一个指向char数据的指针,ptr本身存放在外部RAM区。

以上的long、char等指针指向的数据可存放在任何存储器中。通用指针产生的代码比指定存储区指针代码的执行速度要慢,因为存储区在运行前是未知的,编译器不能优化存储区访问,必须产生可以访问任何存储区的通用代码。

(2)存储器指针。C51允许使用者规定指针指向的存储段,这种指针叫指定存储区指针。例如:

char data*str; //str指向data区中char型数据

int xdata*pow;//pow指向外部RAM的int型整数

存储类型在编译时是确定的,通用指针所需的存储类型字节在指定存储器的指针中是不需要的,指定存储区指针只需用一字节(idata、data、bdata和pdata指针)或两字节(code和xdata指针)。使用指定存储区指针的好处是节省了存储空间,编译器不用为存储器选择和决定正确的存储器操作指令产生代码,使代码更加简短,但必须保证指针不指向所声明的存储区以外的地方,否则会产生错误。

(3)绝对指针。绝对指针类型可访问任何存储区的任何地址,也可用绝对指针调用定位在绝对或相对固定地址的函数。举例说明如下:

char xdata*px; //指向xdata区的指针

char idata*pi; //指向idata区的指针

char code*pc; //指向code区的指针

(4)指针转换。C51编译器可以在指定存储区指针和通用指针之间转换,指针转换可以用类型转换的直接程序代码来强制转换,或在编译器内部强制完成。

当把指定存储区指针作为参数传递给要求使用通用指针的函数时,C51编译器就把指定存储区指针转换为通用指针,如下例中用printf、sprintf和gets等通用指针作为参数的函数。

在调用函数printf中,参数fmt代表2字节code指针,自动转换或强制转换为3字节通用指针,这是因为printf的原型要求用通用指针作为第一参数。

指定存储区的指针作为函数的参数时,如果没有函数原型,就经常被转换成通用指针。如果调用的函数用短指针作为参数,会引起错误。要在程序中避免这种错误,可用#include文件和所有外部函数原型。

6.函数

(1)函数声明。Keil C51编译器对标准C函数声明的扩展包括以下内容:

1)指定一个函数作为一个中断函数。

2)选择所用的寄存器组。

3)选择存储模式。

4)指定重入。

5)指定ALIEN PL/M51函数。

在函数声明中可以包含这些扩展属性,声明C51函数的标准格式如下:

[return_type][funcname([args])[{small|compact|large}][reentrant][interrupt n][using n]

return_type:函数返回值的类型,如果不指定缺省是int。

funcname:函数名。

args:函数的参数列表。

small、compact、large:函数的存储模式。

reentrant:表示函数是递归的或可重入的。

interrupt:表示是一个中断函数。

using:指定函数所用的寄存器组。

(2)函数参数和堆栈。传统的8051中堆栈指针只能访问内部数据存储区,Keil C51编译器把堆栈定位在内部数据区的所有变量的后面,堆栈指针间接访问内部存储区,可以使用0x FF前的所有内部数据区。传统8051的堆栈空间是有限的,最多只有256字节。除了用堆栈传递函数参数外,Keil C51编译器还为每个函数参数分配一个特定地址,当函数被调用时,调用者在传递控制权前必须把参数拷贝到分配好的存储区,函数就可以从固定的存储区提取参数。在这个过程中,只有返回地址保存在堆栈中。中断函数要求更多的堆栈空间,因为必须切换寄存器组,在堆栈中保存寄存器值。

一些派生的51系列单片机的堆栈空间达到几千字节,C51编译器在缺省情况下最多可以用寄存器传递3个参数以提高运行速度。另外一些派生的C51系列单片机只提供64字节的片内数据区,在决定存储模式时应考虑这个因素,因为片内data和idata直接影响堆栈空间的大小。

(3)用寄存器传递参数。C51编译器允许用CPU寄存器传递3个参数,这样可以明显提高系统性能。参数传递可以用PEGPARMS或NOREGPARMS控制命令来控制,表9-3列出了不同参数位置和数据类型所用的寄存器。

表9-3 参数位置及数据类型所用的寄存器

如果没有寄存器可以用来传递参数,则使用固定存储区。以下是几个参数传递的例子:

func1(int a):“a”是第一个参数,在R6,R7中传递。

func2(int a,int b,int*c):“a”在R6、R7中传递,“b”在R4、R5中传递,“c”在R1、R2和R3中传递。

func3(long a,long b):“a”在R4~R7中传递,“b”不能在寄存器中传递,只能在参数传递段中传递。

(4)函数返回值。函数返回值一律放于寄存器中,规律见表9-4。注意,如果函数的第一个参数是bit类型,则别的参数不能用寄存器传递,因此bit参数应该在最后声明。

表9-4 函数返回指与寄存器对照表

(5)函数的存储模式。函数的参数和局部变量保存在有存储模式指定的缺省存储空间中,但是单个函数可以在函数声明中用small、compact和large来声明指定存储模式。如:

函数使用SMALL模式时,局部变量和参数保存在8051内部RAM,数据访问效率高。但是内部存储区是有限的,很多情况下SMALL模式不能满足程序的要求,就必须使用其他存储模式。

(6)函数的寄存器组。51系列单片机的最低32个字节划分为4个寄存器组,每个寄存器组的寄存器为R0~R7,寄存器组有PSW的两位选择。using函数属性用来指定函数所用的寄存器组,例如:

void rb_function(void)using 3

{…}

using属性为0~3的常整数,不允许带操作数的表达式,在函数原型和用寄存器返回值的函数中不允许使用using属性。必须确保寄存器组切换在可控范围内,否则可能产生错误。即使使用相同的寄存器组,用using声明函数也不能返回bit值。

7.重入函数

一般函数中的每个变量都存放在一个固定的位置,当递归调用这个函数时会导致变量被覆盖,所以在实时应用中应尽量少用一般函数。因为函数调用时可能会被中断程序中断,而在中断中可能再次调用这个函数,所以C51允许将函数声明为重入函数。重入函数,又称为再进入函数,是一种可以在函数体内直接调用其自身的函数。重入函数可被递归调用和多重调用而不必担心变量被覆盖,这是因为每次函数调用时的局部变量都会被单独保存起来。由于这些堆栈是模拟的,重入函数一般都比较大,运行起来也比较慢。模拟栈不允许传递bit类型的变量,也不能声明局部位标量。

声明重入函数关键字:reentrant。

声明格式为:函数说明 函数名(形式参数表)reentrant

例如:

8.中断函数

51单片机的中断系统使用十分普遍,C51编译器支持声明中断和编写中断服务程序。中断过程通过使用interrupt关键字和中断编号0~4来实现。使用该扩展属性的方法如下:

返回值 函数名 interrupt n

n对应中断源的编号。

有关中断的详细介绍请参看本章的第二节。

9.绝对地址访问

(1)绝对宏。在程序中,用“#include〈absacc.h〉”可直接使用其中声明的宏来访问绝对地址,包括CBYTE、XBYTE、PWORD、DBYTE、CWORD、XWORD、PBYTE、DWORD等,例如:

#include〈absacc.h〉 //包含absacc.h头文件

#define DAC0832 XBYTE[0x7fff]/*定义DAC0832端口地址*/

DAC0832=0x00;  直接使用宏定义控制端口

rval=CBYTE[0x0002]; 指向程序存储器的0002h地址

rval=XWORD[0x0002]; 指向片外RAM的0004h地址,外部数据存储器的地址*2

(2)_at_关键字。使用时直接在数据声明后加上_at_constant即可,但是需要注意:

1)绝对变量不能被初始化。

2)bit型函数及变量不能用_at_指定。

例如:

idata struct link list_at_0x40;  //指定list结构从40 H开始

xdata char text[256]_at_0x E000; //指定text数组从0E0000H开始

如果用_at_关键字声明变量来访问一个XDATA外围设备,应使用volatile关键字确保C51编译器不进行优化,以便能访问到要访问的存储区。