标签:
C51 编程
C-51 编译器支持下列数据类型:
数 据 类 型 |
长 度 |
值 域 |
bit |
1 字节 |
0 或 1 |
signed char |
1 字节 |
-128~+127 |
unsigned char |
1 字节 |
0~255 |
signed int |
2 字节 |
-32768~+32867 |
unsigned int |
2 字节 |
0~65535 |
signed long |
4 字节 |
-2147483648~+2147483647 |
unsigned long |
4 字节 |
0~4294967295 |
float |
4 字节 |
±1.176E-38~±3.40E+38 |
指针 |
1~3 字节 |
对象地址 |
sbit |
1 位 |
0 或 1 |
sfr |
1 字节 |
0~255 |
sfr16 |
2 字节 |
0~65535 |
编译的数据类型(如结构)包含上表所列的数据类型。由于8051系列是8位机,因而不存在字节校准问题。这意味着数据结构成员是顺序放置的。
数据类型的转换:当计算结果隐含着另外一种数据类型时,数据类型可以自动进行转换,例如,将一个位变量赋给一个整型变量时,位型值自动转换为整型值,有符号变量的符号也能自动进行处理。这些转换也可以用C语言的标准指令进行人工转换。
2 数据类型的物理结构
1.2.1 bit
“bit”类型只有1位,不允许有位指针和位数组。位对象始终位于8051 CPU的可寻址RAM空间。如果程序控制流允许,L51将位对象交迭。
1.2.2 signed/unsigned char;data/idata/pdata 指针
“char”类型标量和基于存贮器的“data/idata/pdata”指针具有1个字节长度(8 bits)。
1.2.3 signed/unsigned int/short;xdata/code 指针
“int”和“short”类型标量及指向xdata/code区域的指针具有2字节长度(16
bits)。
整型值(或偏移)0x1234以下面方式保存在内存中:
地址: +0 +1
内容: 0x12 0x34
1.2.4 signed/unsigned long
“long”类型标量长为4个字节(32 bits),值0x12345678以下面方式放置:
地址: +0 +1 +2 +3
内容: 0x12 0x34 0x56 0x78
1.2.5 “一般”指针
“一般”指针包括3个字节:2字节偏移和1字节存贮器类型:
地址: +0 +1 +2
内容: 存贮器类型 偏移高位 偏移低位
第一个字节代表了指针的存贮器类型,存贮器类型编码如下:
存贮器类型 IDATA XDATA PDATA DATA CODE
值 1 2 3 4 5
使用其它类型值可能导致不可预测的程序动作。
XDATA类型的0x1234地址作为指针表示如下:
地址: +0 +1 +2
内容: 0x02 0x12 0x34
当用常数作指针时,必须注意正确定义存贮器类型和偏移。下例将值0x41写入绝对地址为0x8000的外部数据存贮器:
#define XBYTE ((char *)0x20000L)
XBYTE[0x8000]=0x41;
上例中用其它常数索引或索引变量也起作用。这样,各种存贮器类型的绝对地址可以一种非常有效的方式访问。但有一个例外,即SFR。
注意:绝对地址定义为“long”型常量,低16位包含偏移,高8位表明了xdata类型。为了表示这种指针,必须用长整数来定义存贮器类型。
C51编译器不检查指针常数,用户必须选择有实际意义的值。
1.2.6 float
“float”类型为4个字节(32位),使用的格式与IEEE-754标准(32位)具有24位精度,尾数的高位始终为“1”,因而不保存,位的分布如下:
l 1位符号
l 8位指数位
l 23位尾数
符号位是最高位,尾数为最低的位,内存中按字节存贮如下:
地址: +0 +1 +2 +3
内容: MMMM MMMM MMMM MMMM E MMM MMMM S EEE EEEE
其中: S:符号位,1=负,0=正
E:指数(在两个字节中),偏移为127
M:23位尾数,最高位“1”
浮点值——12.5的十六进制为0xC1480000,它按下面方式存贮:
地址: +0 +1 +2 +3
内容: 0x00 0x00 0x48 0xc1
8051不包括捕获浮点错误(例外)的中断向量。用户软件因此必须对错误条件作出适当反应。下面推荐一种方法(也可以用其它可靠办法):“union”用来保存浮点值,这个“union”必须包括一个“float”和一个“unsigned long”,以根据IEEE对错误作出响应。除了通常浮点值外,IEEE标准可能出错的条件以下面二进制值表示,为检查可能出现的计算错误,可在计算后进行检查。因为当执行一个运算时考虑了每个运算符的错误状态并且该状态被送到结果中。
NaN 0xFFFFFFF 不是一个数
+INF 0x7F80000 正无穷(正溢出)
-INF 0XFF80000 负无穷(负溢出)
1.3 C-51 的扩充定义
1.3.1 特殊功能寄存器的声明
MSC-51 系列包括多种寄存器,其中一些具有特殊功能,如定时器,端口的控制寄存器等,为了能够直接访问这些寄存器,C51编译器提供了一种定义的自主形式,这是必要的,因为这些定义与标准C语言是不兼容的。
为了支持这些特殊功能寄存器(SFR)的声明,引入了关键词“sfr”,语法如下:
sfr-dcl:sfr sfr_name=int_constant
例:
sfr p0=0x80;
sfr p1=0x90;
必须注意的是“sfr”后不是一个地址而是一个名字。因此上例中名字P0和P1(port0和port1)定义为特殊功能寄存器并被赋予相应的绝对地址,名字可按意愿自由选取,源文件中不应有先定义的sfr名字。
“=”号后的地址必须是常数,不允许带有运算符的表达式,这个常数表达式必须在特殊功能寄存器的地址范围内,位于0X80到0XFF之间。
8051系列寄存器数量和类型是极其不同的,因此建议将所有特别的“sfr”声明放入一个头文件,头文件包括8051一些系列成员中的SFR定义。进一步的定义可由用户用一文件编辑器产生。
1.3.2 对SFR的16位数据访问
在新的8051系列产品中,SFR在功能上经常组合为16位的,为了有效的访问这类SFR,使用定义“sfr16”,当“SFR”的高端直接位于低端后时,对SFR16位的访问是可能的。例如8052的定时器2就是这种情况,16位声明的语法与“sfr”相同,SFR低地址部分必须作为sfr16的地址:
例:sfr16 T2=0xCC /*Timer2:T2L=0CCH,T2H=0CDH */
sfr16 RCAP2=0xCA /*RCAP2L=0CAH,PCAP2H=0CBH */
本例中,T2(由T2L和T2H组成)和RCAP2(由RCAP2L和RCAP2H组成)被定义为16位SFR,即使在这种情况下,声明中的名字后仍不是赋值语句,而是一个SFR地址,高字节必须直接位于低字节之后,这种声明适用于所有新的SFR,但不能用于Timer0和Timer1。
1.3.3 SBIT:特殊功能位声明
在典型的8051应用问题中,经常需要单独访问SFR中的位,C51扩充功能使之成为可能,特殊位,象SFR一样,不与标准C语言兼容,使用保留字“sbit”可访问位寻址对象。与SFR声明一样,用保留字“sbit”声明某些特殊位接受符号名,“=”后语句将绝对值地址赋给变量名,这种地址分配有三种方法:
方法1:sfr_name^int_constant
当字节是特殊功能寄存器的地址可用这个方法。sfr_name必须是已定义的SFR的名字,“^”后的语句定义了基地址上的特殊位的位置,该位置必须是一个0~7的数。
例: sfr PSW="0xD0";
sfr LE="0xA8";
sbit OV="PSW"^2;
sbit CY="PSW"^7;
方法2:int_constant^int_constant
这种方法以一整常数作基地址,该值必须在0x80~0xFF之间,并能被8整除,确定位的位置方法同上。
例: sbit OV="0xD0"^2;
sbit CV="0xD0"^7;
sbit EA="0xA8"^7;
方法3: int_constant
这种方法是将位的绝对地址赋给变量,地址必须位于0x80~0xFF之间。
例: sbit OV="0xD2";
sbit CY="0xD7";
sbit EA="0xAF";
特殊功能位代表了一个独立的声明类,它不能与其它声明和位域互换。
1.3.4 BIT:位标量声明
除了通常的C数据类型外,C51编译器支持“bit”数据类型,对此有下列扩充与限制:
(1) 函数可包含类型为“bit”的参数,也可将其作为返回值。
bit bfunc(bit b0,bit b1){
/*……*/
return(b1);
}
注:使用禁止中断(#pragma disable)或包含明确的寄存器组切换(using n)的函数不能返回位值,在这种情况下,编译器会识别出来并产生一个错误信息。
(2) 位标量声明的语法及C声明的语义
static bit dirction_bit;
extern bit lock_printer_port;
bit display_invers;
(3) 对于位声明的限制
l 位不能声明为一个指针(bit *bit_poiter)
l 不存在位数组(bit b_array[5])
位声明中允许定义存贮器类型,位都被放入一个位段,它总是在8051内部RAM中,因此存贮器类型限制为DATA或IDATA,声明为其它存贮器类型都将导致编译出错。
1.3.5 可位寻址对象
可位寻址对象指可以字节或位寻址的对象,当对象位于MSC-51可寻址RAM中时会有这种情况,C51允许带“bdata”类型的对象放入可位寻址存贮器中。
bdata int ibase; /*位寻址指针 int*/
bdata char bary[4]; /*位寻址数组 arrray*/
使用“sbit”声明可独立访问可位寻址对象的位:
sbit mybit0=ibase^0;
sbit mybit15=ibase^15;
sbit ary07=bary[0]^7;
sbit ary37=bary[3]^7;
对象“ibase”和“bary”也可位寻址:
ary37=0; /*寻址“bary[3]”中的位7*/
ibase=-1; /*寻址字节地址*/
mybit15=0; /*寻址“ibase”的位15*/
sbit声明要求基址对象的存贮器类型为“bdata”,否则只有绝对的位声明方法是合法的。位位置(‘^’操作符号后)的最大值依赖于指定的基类型,这个值于char/uchar而言是0~7,对于int/uint/short/ushort而言是0~15,对于long/ulong而言是0~31。
在编译器内存贮器类型bdata与data一样操作,并且只作与可再定位的sbit的运算。注:可位寻址的的段长最大不能超过16字节,可再定位的sbit声明自动转为公共的(PBULIC)以使它们能被其它C模块使用。
模块1: sbit ary37=bary[3]^7;
模块2: extern bit ary37;
sbit声明也可为结构和函数所用:
union lft {float mf;long ml;} ;
bdata struct bad { char ml; union lft u; }tcp;
sbit tcpf31=tcp.u.ml^31; /*浮点限制*/
sbit tcpml0=tcp.ml^0;
sbit tcmpl7=tcp.ml.^7;
注:位位置的指定不能直接被float类型所用,如果需要这样做,浮点标量必须与一个长整型标量一起放入一个联合中并且位位置必须由长整型标题指定(见上例)。
1.4 存贮器类型
C51编译器完全支持8051微处理器及其系列的结构,可完全访问MCS-51硬件系统所有部分。每个变量可准确地赋予不同的存贮器类型(data,idata,pdata,xdata,code)。访问内部数据存贮器(idata)要比访问外部数据存贮器(xdata)相对要快一些,因此,可将经常使用的变量置于内部数据存贮器中,而将较大及很少使用的数据单元置于外部数据存贮器中。
存贮器类型 |
描 述 |
data |
直接寻址内部数据存贮器,访问变量速度最快(128bytes) |
bdata |
可位寻址内部数据存贮器,允许位与字节混合访问(16 bytes) |
iIdata |
间接寻址内部数据存贮器,可访问全部地址空间(256bytes) |
pPdata |
分页(256bytes)外部数据存贮器,由操作码MOVX @Ri访问 |
xdata |
外部数据存贮器(64K),由MOVX @DPTR访问 |
code |
代码数据存贮器(64K),由MOVC @A+DPTR访问 |
变量说明举例:
data char charvar;
char code msg[]=”ENTER PARAMETER:”;
unsigned long xdata array[100];
float idata x,y,z;
unsigned char xdata vector[10][4][4];
sfr p0=0x80;
sbit RI="0x98";
char bdata flags;
sbit flago="flags"^0;
如果在变量说明时略去存贮器类型标志符,编译器会自动选择默认的存贮器类型。默认的存贮器类型进一步由控制指令SMALL、COMPACT和LARGE限制。例如:如果声明char charvar,则默认的存贮器模式为SMALL,charvar放在data存贮器;如果使用COMPACT模式,则charvar放入idata存贮区;在使用LARGE模式的情况下,charvar被放入外部存贮区或xdata存贮区。
1.5 存贮器模式
存贮器模式决定了自动变量和默认存贮器类型,参数传递区和无明确存贮区类型的说明。在固定的存贮器地址变量参数传递是C51的一个标准特征,在SMALL模式下参数传递是在内部数据存贮区中完成的。LARGRE和COMPACT模式允许参数在外部存贮器中传递。C51同时也支持混合模式,例如在LARGE模式下生成的程序可将一些函数分页放入SMALL模式中从而加快执行速度。
存贮器模式 |
描 述 |
SMALL |
参数及局部变量放入可直接寻址的内部寄存器(最大128bytes,默认存贮器类型是DATA) |
COMAPCT |
参数及局部变量放入分页外内部存贮区(最大256bytes,默认存贮器类型是PDATA) |
LARGE |
参数及局部变量直接放入外部数据存贮器(最大64K,默认存贮器类型是XDATA) |
1.6 指针
Franklin C-51支持“基于存贮器的”和“一般指针”。
1.6.1 基于存贮器的指针
基于存贮器的指针由C源代码中存贮器类型决定并在编译时确定,用这种指针可高效访问对象且只需一个字节(idata*,data*,pdata*)或2个字节(code*,xdata*)。操作较短指针的代码被缩短,一般被“内行”编码;库调用不再必要。
声明举例:
char xdata *pt |
在xdata存贮器中声明一个指向对象类型为“char”的指针。指针默认自身在默认存贮区(决定于编译模式),长度为2字节。(值为0~0xFFFF) |
char xdata *data pdx; |
除了指针明确位于内部数据存贮器(data)中外,与上例相同。它与编译模式无关。 |
data char xdata *pdx; |
本例与上例完全相同。存贮器类型定义既可放在声明的开头也可直接放在声明的对象之前。这种形式是为了与早期C-51编译器版本兼容。 |
上面例子阐明了指针的一般声明及使用。它们与所有的数据类型和存贮器类型相关。所有用于一般指针的操作同样可用于基于存贮器的指针。
1.6.2 一般指针
“一般”指针需3个字节:1个字节为存贮类型,2个字节为偏移量。存贮器类型决定了对象所用的8051存贮器空间,偏移量指向实际地址。一个“一般”指针可访问任何变量而不管它在8051存贮器空间中的位置。这样就允许一般性函数,如memcpy将数据从任意一个地址拷贝到另一个地址空间。
1.6.3 基于存贮器的指针与一般指针的转换
一个已定位指针可转换为一个一般指针(3字节)及副本。这在某些时候是很有用的。例如库函数包含一个一般指针变量,象printf(),sprintf(),gets()等包含一个一般指针变量,如同以前的编译器和库版本一样。这些函数因而广泛适用。
例:extern int printf(void *format,…);
在printf调用中,2字节指针自动转换为一个3字节指针,而printf的原型正需要一个一般指针(3字节)作为其第一参量。
注:如果没有函数原型,函数调用的参量中指针总是转换为一般指针。如果函数确实需要一个短指针作参量,这会产生错误,为了避免在程序中发生这类错误,需要使用头文件,或某些函数声明函数原型。这将保证让编译器转换为所需类型,否则会产生类型不匹配错误。
例:指针定义说明
指 针 说 明 |
长 度 |
指 向 |
float *p; |
3 字节 |
所有8051存贮器空间中的“float”(一般指针) |
char data *dp; |
1字节 |
“data”存贮器中的“char” |
int idata *ip; |
1字节 |
“idata”中的“int” |
long pdata *pp; |
1字节 |
“pdata”中的“long” |
char xdata *xp; |
2字节 |
“xdata”中的“char” |
int code *cp; |
2字节 |
“code”中的“int” |
1.6.4 抽象指针类型
抽象指针类型用来在每个存贮区访问任意绝对地址,或来产生绝对CALLs。在这个过程中,常数类型或字符型、整型都用抽象类型作了原则性修改(类型整理)以允许进行绝对访问或调用。
例:
char xdata *px;
char idata *pi;
char code *pc;
char c;
int i;
pc = (void*)main;
i = ((int(code*)(void))0xFF00(); /*LCALL 0FF00H */
c = *((char code*)0x8000); /*char [code[0x8000]]*/
i = *((int code*)0x1200); /*int from code[0x1200]*/
px = *((char xdata *xdata*)0x4000); /*x ptr from xdata[0x4000]*/
px = ((char xdata *xdata*)0x4000)[0]; /*同上*/
px = ((char xdata *xdata *)0x4000)[1] /*x ptr from xdata[0x4002]*/
1.7 寄存器组定义
8051系列的器件包含4个相同的寄存器组,每个寄存器组包括8个寄存器(R0~R7),C51编译器可使在一函数中决定用哪一寄存器组成为可能。绝对寄存器的访问可用AREGS/NOAREGS和REGISTERBANK来控制。
定义一个带扩展性的函数语法如下:
返回类型 函数名([参数])[模式][再入][中断 n]using n
再入和中断将在后两节讨论。
例:void rb_function(void) using 3;
“using”不允许用于外部函数,它对函数的目标代码影响如下:
l 函数入口处将当前寄存器保存入栈;
l 指它的寄存器还会改变;
l 函数退出前寄存器组被恢复。
“using”定义对于返回一个寄存器内的值的函数是无用的。编程者必须十分小心以保证任何寄存器切换都只在仔细控制的区域发生。如果不做到这一点将会产生不正确的函数结果。即使当编程者使用同一寄存器组时,带“using”属性的函数原则上也不能返回一个位值。
实际产生的代码决定于编译器及不同开关条件,有时用命令产生绝对的寄存器地址,当需要进行这样的地址计算时,使用REGISTERBANK指令的影响只是计算Arn寄存器使用的地址,而必进行实际切换。
1.8 中断服务程序
C51编译器及其对C语言的扩充允许编程者对中断的所有方面进行控制。这种支持能使系统编程者创建高效的中断服务程序,用户只需在普通和高级方式下关心中断及必要的寄存器组切换操作,C51编译器将产生最合适的代码。
1.8.1 中断服务程序的定义
使用中断服务函数的完整语法如下:
返回值 函数名([参数])[模式][再入] interrupt n[using n]
“interrupt”后接一个0~31的常数,不允许使用表达式。
中断不允许用于外部函数,它对函数目标代码的影响如下:
l 当使用函数时,SFR中的ACC、B、DPH、DPL和PSW(当需要时)入栈;
l 如不使用寄存器组切换,甚至中断函数所需的所有工作寄存器(Rn)都入栈;
l 函数退出前,所有的寄存器内容出栈;
l 函数由8051控制命令“RETI”终止。
1.8.2 开发中断过程时的规则
l 不能进行参数传递,如果中断过程包括任何参数声明,编译器将产生一个错误信息;
l 无返回值,如果想定义一个返回值将产生错误,然而,如果返回整型值编译器将不产生错误信息,因为整型值是默认值,因而编译器不能清楚识别。
l 编译器会识别对中断过程的直接调用并拒绝它们,在任何情况下不能直接调用中断过程,因为退出该过程是由操作码RETI完成的。RETI影响8051芯片的硬件中断系统,由于硬件上没有中断请求存在,因而这个操作码的结果是不定的并且通常是致命的。由于疏忽,可能用指针来间接调用它,这是值得注意的。
l 编译器从绝对地址8n+3处产生一个中断向量,其中n为中断号,该向量包括一个到中断过程的跳转,向量的产生可由指令NOINTVECTOR压缩。因而用户有能力从独立的汇编模块中提供中断向量。
l C51编译器允许0~31个中断,究竟允许哪些中断依赖于使用的8051系列芯片,编译器不能检查。
l 如果中断程序中有浮点运算,必须保持浮点寄存器状态,当没有其它程序执行浮点运算时,可能不保存,函数“fsave”和“fprestore”用来保存浮点状态。
l 中断过程调用的函数所使用的寄存器必须与中断过程相同,当没有使用“using”指令时,编译器会选择一个寄存器组作绝对寄存器访问,当子程序使用另一个寄存器组时会发生错误,用户必须保证按要求使用相应寄存器组,C编译器不会对此检查。
例:
unsigned int interruptent;
unsigned char second;
time() interrupt 1 using 2 /*定时器0中断服务程序,工作寄存器使用2区*/
{
if(++interruptcnt==4000) {
second++; /*秒计数加一*/
interruptcnt=0; /*清中断计数*/
}
}
1.9 再入函数
再入函数可被递归调用,调用可发生在任何时候,即使是在中断过程中。在实时处理的应用问题中常常需要再入函数。
使用关键字“reentrant”可有选择地定义函数有再入能力。在存贮器模式的基础上为再入函数在内部或外部存贮器中模拟了一个栈区域。由于MCS-51缺乏合适的寻址方法,使用栈结构是相当必要的。因而应尽量少用再入函数。
定义一再入函数的语法如下:
返回值 函数名([参数])[模式]reetrant[interrupt n][using n]
例: