第3章51单片机C51语言编程基础 视频 早期单片机程序设计采用汇编语言,程序设计难度大。20世纪末,单片机的C语言编译器逐渐兴起,与汇编语言程序相比,C语言程序在功能上、结构上、可读性、可移植性、可维护性等方面有明显优势,且易学易用,现已成主流的单片机编程语言。Keil C51是美国Keil Software公司专为51系列兼容单片机设计的高级语言C编译器,是使用最广泛的51单片机C语言编译器,C51保留了ANSI C的所有规范,并针对8051单片机的特点作了一些特殊的拓展。 3.1C51程序与编程规范 3.1.1C51的程序结构 C51源程序文件扩展名为.c,其结构与ANSI C语言相同,包括预处理命令、全局声明和函数定义3部分构成,各部分程序均有注释,程序一般结构如下: /*程序说明、功能、设计者、设计时间、修改时间、版本描述等*/ 预处理命令//用于包含头文件等 全局变量声明;//全局变量可被本程序所有函数引用 函数1(形参说明表)声明; …; 函数n(形参说明表)声明; /*主函数*/ main() { 局部变量声明; 执行语句; 函数i调用(实参表); } /*其他函数定义*/ 函数1(形参说明表)//函数1定义 { 局部变量声明;//局部变量只能在函数内部引用 执行语句; 函数j调用(实参表); } … 函数n(形参说明表) //函数n定义 {…; } 1. 预处理命令 预处理命令包括文件包含(#include)、宏定义及撤销(#define,#undef)、条件编译(#if、#else、#endif)。 2. 函数定义 函数是C51程序的基本单位,程序由一个或多个函数构成,其中有且仅有一个名为main()的主函数,程序总是从main()函数开始执行,主函数所有语句执行完毕,则程序结束。主函数中可以调用其他功能函数,调用结束后又返回主函数,功能函数不能调用主函数,但功能函数之间可相互调用和嵌套。功能函数可以用户自定义,也可以是C51编译器提供的库函数。 3. 全局声明 全局声明包括变量声明和函数声明,在函数之外定义的变量为全局变量,在函数之内定义的变量则为局部变量。当一个函数调用另一个函数时,被调用的函数必须是已定义的,还未定义的必须先声明,被调用函数往往和全局变量一起声明。 4. 程序注释 注释语句只起对程序代码功能进行描述的作用,增加程序的可读性,不参与程序的编译链接,不产生可执行的机器码。与ANSI C的规则相同,C51源程序的注释有单行注释和块式注释两种。 1) 单行注释 以“//”符号开始,其后面至行末的所有符号构成单行注释。一般放在执行语句的后面,用于对其所在行的语句功能进行注释。 2) 块式注释 以“/*”符号开始、以“*/”符号结束,其间的所有符号构成块式注释。一般放在某程序模块的前面,用于对其后的程序模块功能进行注释。 3.1.2C51的标志符与关键字 1. 标志符 在编程语言中,用来对变量、符号常量、函数、数组、结构体等对象命名的有效字符串称为标志符(或标识符),C51语言支持自定义的标志符。与ANSI C标志符的命名规则完全相同,C51的标志符可以由字母、下画线“_”及数字0~9组成,首符号必须是字母或下画线,最多32个字符,区分大小写。 2. 关键字 C51语言继承了ANSI C的32个关键字,并根据51系列单片机的硬件特点,增加了一些关键字,C51语言的关键字如表3.1所示。 表3.1C51语言的关键字 类别关键字类型作用 ANSI C 标 准 关 键 字 void基本数据类型声明声明无(或空)类型数据 char基本数据类型声明声明单字节整型数据或字符型数据 int基本数据类型声明声明整型数据 float基本数据类型声明声明单精度浮点型数据 double基本数据类型声明声明双精度浮点型数据 short数据类型修饰说明修饰整型数据,短整型数据 long数据类型修饰说明修饰整型数据,长整型数据 signed数据类型修饰说明修饰整型数据,有符号整数(二进制补码表示) unsigned数据类型修饰说明修饰整型数据,无符号整数 enum复杂数据类型声明声明枚举型数据 struct复杂数据类型声明声明结构类型数据 union复杂数据类型声明声明联合(共用)类型数据 typedef复杂数据类型声明声明重新定义数据类型 sizeof数据类型大小运算符计算特定类型的表达式或变量的大小(即字节数) auto数据存储类别说明指定变量为自动变量,其空间在动态存储区分配(默认) static数据存储类别说明指定变量为静态变量,其空间在静态存储区分配 register数据存储类别说明指定局部变量为寄存器变量,其空间优先在CPU内部寄存器分配 extern数据存储类别说明指定变量为外部变量,由其他程序文件声明的全局变量 const数据存储类别说明指定变量的值在程序执行过程中不可变更 volatile数据存储类别说明指定变量的值在程序执行过程中可被隐含地变更 return流程控制(跳转)用于函数体中,返回特定值 continue流程控制(跳转)中止当前循环,开始下一轮循环 break流程控制(跳转)退出当前循环或switch结构 goto流程控制(跳转)无条件转移语句 if流程控制(分支)用于构成if…else条件语句,条件及其肯定分支 else流程控制(分支)用于构成if…else条件语句,否定分支 switch流程控制(分支)用于构成switch…case开关语句,分支条件 case流程控制(分支)用于构成switch…case开关语句中的第n个分支 default流程控制(分支)用于构成switch…case开关语句中的其他分支 for流程控制(循环)用于构成for循环结构语句 do流程控制(循环)用于构成do…while循环结构 while流程控制(循环)用于构成do…while或while循环结构 续表 类别关键字类型作用 C51 扩 展 关 键 字 bit位变量声明声明一个位变量或位类型的函数 sbit位变量声明声明一个可位寻址变量(指定位地址) sfr特殊功能寄存器声明声明一个8位特殊功能寄存器(指定字节地址) sfr16特殊功能寄存器声明声明一个16位的特殊功能寄存器(指定低位字节地址) data存储器类型说明指定直接寻址的单片机片内数据存储器 bdata存储器类型说明指定可位寻址的单片机片内数据存储器 idata存储器类型说明指定间接寻址的单片机片内数据存储器 pdata存储器类型说明指定分页寻址的单片机扩展数据存储器 xdata存储器类型说明指定单片机扩展数据存储器 code存储器类型说明指定单片机程序存储器 interrupt中断函数说明说明一个中断服务函数 reentrant再入函数说明说明一个再入函数 using寄存器组说明指定单片机的工作寄存器组 _at_存储绝对地址说明声明变量时指定其所在地址 small编译模式说明— compact编译模式说明— large编译模式说明— 3.1.3C51编程规范 养成良好的编程习惯很重要,按一定规范编写程序,有助于设计者理清思路,方便程序纠错、修改、整理、阅读,是程序可维护和可移植的前提。进行C51程序设计时,应注意以下几方面的编程规范。 (1) 一个好的源程序应该添加必要的注释,以增加程序的可读性。 (2) 标志符应能直观反映其含义,避免过于冗长,方便源程序的编写、阅读与理解。 (3) 编译系统专用标志符以下画线开头,建议自定义标志符不使用下画线为首字符。 (4) 自定义的标志符避免与关键字或C51库函数同名。 (5) 虽然C51语言没有限制主函数main()放置的位置,但为阅读方便,最好将其放在所有自定义函数的前面,程序语句的书写顺序依次为: 头文件声明、全局变量和自定义函数声明、主函数main()、自定义函数。 (6) 每条语句单独写一行,或将配合完成某一功能的几条短语句写在同一行,并注释。 (7) 源程序文件中不同结构部分之间要留有空行,来明显区分不同的结构。 (8) 对if、for、while等块结构语句中的“{”和“}”要配对对齐,以突出该块结构的起始和结束位置,使程序阅读更直观。 (9) 源程序书写时,可以通过适当的Tab键操作来实现代码对齐。 3.2C51语言的数据 使用数据时,与ANSI C基本相同,C51也要说明其数据类型、常量或变量、变量的作用范围; 与ANSI C略有不同,C51还要说明其存放在存储器的什么区域。 3.2.1数据类型 ANSI C语言数据类型包括基本类型、构造类型、指针类型以及空类型等。基本类型有字符、整型、短整型、长整型、浮点型、双精度浮点型等; 构造类型包括数组、结构体、共用体以及枚举类型等。基本数据类型中,C51语言增加了位(bit)类型。整型和短整型相同,均为16位; 浮点型和双精度浮点型相同,均为32位。因此,在C51中使用short和double标志符是没有实际意义的。C51支持的基本数据类型、长度及值域如表3.2所示。 表3.2C51支持的基本数据类型、长度及值域 数 据 类 型标志符长度/bit(Byte)值域 位型bit10,1 无符号字符型unsigned char8(1)0~255 有符号字符型signed char8(1)-128~127 无符号整型unsigned int16(2)0~65 535 有符号整型signed int16(2)-32 768~32 767 无符号长整型unsigned long32(4)0~4 294 967 295 有符号长整型signed long32(4)-2 147 483 648~2 147 483 647 浮点型float32(4)±1.175494E-38~±3.402823E+38 指针型*8~24(1~3)对象地址0~65 535 数据的存储有大端与小端模式之别。所谓大端模式(BigEndian),即高位字节存储在低地址,而小端模式(LittleEndian)则低位字节存储在低地址。C51的数据存储采用大端模式。 3.2.2常量与变量及其存储模式 1. 常量 常量是不接受程序修改的固定值,C51的常量包括位常量、整型常量、实型常量、字符型常量、字符串常量等。 1) 位常量 位常量即位常数,只有两个值: 0或1。 2) 整型常量 整型常量即整常数,可带负号。通常情况下,C51程序设计时常采用十进制和十六进制。十进制整常数以非0开始的整数表示,例如,-6、965等。十六进制整常数以0x开始的数表示,例如,-0x2a、0xc1f4等。 整型常量后加字母“L”或“l”表示该数为长整型,例如965为整型,在内存中占2字节,而965L为长整型,在内存中占4字节。 3) 实型常量 实型常量又称浮点常量,是用十进制表示的实常数,可带负号,值包括整数部分、尾数部分和指数部分,指数以10为基数,幂为有符号整数,指数部分以E或e开头,字母E或e之前必须有数字,例如,-3.14、126E-3表示126×10-3、1.76E6表示1.76×106。 C51语言的浮点数使用24位精度(单精度),占4字节的存储单元,浮点数的二进制编码如表3.3所示。 表3.3浮点数的二进制编码 位31~2423~1615~87~0 内容SEEEEEEEEMMMMMMMMMMMMMMMMMMMMMMM 其中S(1位)为符号位,0表示“正”,1表示“负”; E(8位)为指数,偏移为127,E取值范围1~254; 24位的数值中规定整数部分1位和尾数部分23位,整数始终为1不保存,仅保存23位的尾数M。例如,浮点数-13.75,其二进制表示为-1101.11,即-1.10111×2130-127,因此符号位S=1,指数E=130 (即10000010),尾数M=1011100 00000000 00000000,所以-13.75的十六进制编码为0xc15c0000,采用不同的端模式,该浮点数在内存中的信息和占用的存储单元亦不同,如表3.4所示。 表3.4浮点数-13.75在内存中的存储模式 存储地址(首地址Addr)Addr+0Addr+1Addr+2Addr+3 大端模式(C51的模式)0xc10x5c0x000x00 小端模式0x000x000x5c0xc1 4) 字符型常量 字符型常量是指用一对单引号括起来的单个字符,并按其对应的ASCII码值来存储,如'a'、'9'、'!'等,ASCII码参见附录A。 5) 字符串常量 字符串常量是指用一对双引号括起来的一串字符,字符串在内存中以ASCII码值存储,系统自动在字符串的末尾加的一个结束标志NUL,其ASCII码值为00H。因此在程序中,长度为n个字符的字符串常量,在存储器中占n+1字节的存储空间。例如,程序中的常量9、'9'、"9"在存储器中的信息和占用空间是不同的。 6) 符号常量 C51允许将程序中的常量定义为一个标志符,称为符号常量。符号常量一般使用大写字母表示,符号常量使用前必须用宏定义(#define)命令定义。例如: #define PI 3.1416//定义实型符号常量PI为圆周率 2. 变量及其存储模式 变量是程序执行过程中其值可以改变的量。变量使用前必须先定义,用一个标志符作为变量名。在ANSI C中,变量定义的格式如下: [存储类别] 数据类型 变量名表; 存储类别(或存储类型)表示变量的存储方式,存储方式有静态和动态两大类,静态存储方式是指程序运行期间由系统分配固定的存储空间的方式; 动态存储方式是指程序运行期间根据需要动态地分配存储空间的方式。存储类别共有4种: auto(自动的)、static(静态的)、register(寄存器的)、extern(外部的),变量定义时未指定存储类别时,默认存储类别为auto。局部或全局特性表征变量的作用域各异,动态或静态存储特性表征变量的生存期不同,各种类型变量的作用域和生存期如表3.5所示。 表3.5各种类型变量的作用域和生存期 变量存储类别 在函数内定义在函数外定义 作用域生存期作用域生存期 register局部动态—— auto(或无修饰)局部动态可全局静态 static局部静态限本文件静态 extern全局静态全局静态 针对51单片机的数据存储器被分为片内RAM、片外RAM和程序存储区的特点,在C51中,定义变量时还可以指定给变量分配的存储器类型,变量定义的格式如下: [存储类别] 数据类型 [存储器类型] 变量名表; 存储器类型选项用于指定给变量分配的存储器类型,进行准确的地址范围定位。C51编译器能够识别存储器类型如表3.6所示。 表3.6C51存储器类型与数据存储空间的对应关系 存储器类型说明 data片内RAM的低128字节,DATA区(00H~7FH地址空间),直接寻址 bdata片内RAM的位寻址区,BDATA区(20H~2FH地址空间),可位寻址 idata片内RAM的256字节,IDATA区(00H~FFH地址空间),间接寻址 pdata 外部RAM存储器分页寻址(256字节),PDATA区(00H~FFH地址空间) 用“MOVX A,@Ri”和“MOVX @Ri,A”指令访问(详见第8章) xdata 外部RAM存储器,XDATA区(0000H~FFFFH地址空间) 用“MOVX A,@DPTR”和 “MOVX @DPTR,A”指令访问(详见第8章) code 程序存储器ROM,CODE区(0000H~FFFFH地址空间) 用“MOVC A,@A+DPTR”和“MOVC A,@A+PC”指令读(只读,详见第8章) 变量定义实例如下: unsigned char data var;//在DATA区定义无符号字符型变量var char bdata flags;//在BDATA区定义字符型变量flags extern float idata x,y,z;//在IDATA区定义浮点型外部变量x,y,z float pdata a,b;//在PDATA区定义浮点型变量a,b char xdata text[]="OK!";//在XDATA区定义字符串数组变量text[] unsigned int code V_to_T[200];//在CODE区定义无符号整型数组变量V_to_T[] 变量定义时如果省略存储器类型,Keil C51编译器按选择的编译模式Small、Compact或Large确定默认的存储器类型,各编译模式对应的默认存储器类型如表3.7所示。 表3.7编译模式对应的默认存储器类型 编 译 模 式默认的存储器类型 Small(小模式)data(访问速度最快) Compact(紧凑模式)pdata(访问速度较快) Large(大模式)xdata(访问速度最慢) 3.3用C51语言描述单片机资源 C51语言对51单片机资源的描述主要有特殊功能寄存器和位变量的定义,以及以绝对地址方式访问片内RAM、片外RAM和I/O端口。 3.3.1特殊功能寄存器定义 51单片机通过特殊功能寄存器SFR实现对CPU及其片内各种I/O功能模块的管理和控制,表2.2列出了STC15W4K32S4系列单片机特殊功能寄存器名称及地址映像。特殊功能寄存器的地址范围为80H~FFH,只能用直接寻址方式访问,其中地址能被8整除的特殊功能寄存器还是可以位寻址的。 为了能用直接寻址方式访问特殊功能寄存器,C51语言引入扩展的关键字sfr,专用于特殊功能寄存器定义,其语法如下: sfr 特殊功能寄存器名字 = 特殊功能寄存器地址; 例如: sfr PSW = 0xd0;//程序状态字寄存器PSW地址D0H(见表2.2) sfr DPL = 0x82;//16位数据指针DPTR低8位地址82H sfr DPH = 0x83;//16位数据指针DPTR高8位地址83H 特殊功能寄存器名称(标志符)通常采用大写字母,用sfr定义特殊功能寄存器时,“=”后面的地址必须是常数,其值为0x80~0xff,不允许使用带运算符的表达式。 在51单片机中,有部分特殊功能寄存器可组合成16位的寄存器,其特征是16位寄存器的高位字节地址直接位于低位字节地址之后,C51可用关键字sfr16将其定义为16位特殊功能寄存器,可直接访问其16位值。sfr16的语法与sfr类似,用低位字节的地址作为16位特殊功能寄存器的地址。 例如,51单片机有16位数据指针DPTR,如表2.2所示,DPTR由高位字节DPH和低位字节DPL两个8位寄存器组合而成,DPH的地址(83H)紧随DPL的地址(82H)之后,因此DPTR可如下定义: sfr16 DPTR = 0x82;//定义16位特殊功能寄存器,数据指针DPTR低位字节DPL地址82H 51单片机还有一些16位的特殊功能寄存器,如表2.2所示的定时/计数器T0(详见第6章),它也由高位字节TH0和低位字节TL0组合而成,但前者地址不紧随后者,这类寄存器不能使用sfr16关键字将其定义为16位特殊功能寄存器,其16位值不能直接访问。 3.3.2位变量定义 51单片机片内RAM有可位寻址区BDATA,其字节地址为20H~2FH,位地址为00H~7FH,此外地址可被8整除的特殊功能寄存器SFR也是可位寻址的,位地址为80H~FFH。单片机的CPU都可以处理位数据,对此C51增加了bit(位)数据类型,位变量的定义有两种方式,一是一般位变量定义,位地址由编译系统在BDATA区自动分配; 二是可位寻址整型变量和SFR的位变量定义,位地址是确定的。 1. 一般位变量定义 一般位变量定义与其他基本数据类型变量的定义相似,如: bit k0;//在BDATA区定义位变量k0 2. 可位寻址位变量定义 可位寻址位变量定义使用sbit关键字,其语法格式如下: sbit 位变量名 = 可寻址位的位地址; 可位寻址整型变量的位变量定义需先在BDATA区定义整型变量,可寻址位的位地址以“整型变量名^位序”的形式定义。如: unsigned char bdata flag;//在BDATA区定义可位寻址8位字符型变量flag sbit bx = flag^3;//8位整型flag的第3位定义为bx unsigned int bdata key;//在BDATA区定义可位寻址16位整型变量key sbit k0 = key^0;//16位整型key的高位字节第0位为k0 sbit k7 = key^7;//16位整型key的高位字节第7位为k7 sbit k8 = key^8;//16位整型key的低位字节第0位定义为k8 sbit k15 = key^15;//16位整型key的低位字节第7位定义为k15 对可位寻址的特殊功能寄存器,可寻址位的位地址有3种表示方法,其一先用sfr定义SFR名称,然后用“SFR名称^位序”; 其二用“SFR地址^位序”; 其三用绝对位地址。例如,程序状态字PSW的地址是D0H(见表2.2),该地址能被8整除,所以PSW是可位寻址的SFR,其各位含义详见PSW寄存器描述,相应位变量可如下定义: sfr PSW = 0xd0;//定义特殊功能寄存器PSW sbit CY = PSW^7;//用"SFR名称^位序"表示位地址 sbit AC = 0xd0^6;//用"SFR地址^位序"表示位地址 sbit OV = 0xd2;//用绝对位地址表示位地址 又例如,51单片机8位并行I/O口P1的地址(90H)可被8整除,可位寻址,该并口各位可如下定义: sfr P1 = 0x90;//定义特殊功能寄存器P1 sbit P10 = P1^0;//定义P1.0端口 sbit P11 = P1^1;//定义P1.1端口 sbit P17 = P1^7;//定义P1.7端口 3. 通过头文件访问特殊功能寄存器及其可位寻址位 Keil C51编译器把标准51系列单片机的所有特殊功能寄存器及其可位寻址位进行了定义,存放在头文件Keil\C51\INC\REG51.H和REG52.H中,分别对应80C51和80C52单片机。宏晶科技有限公司也为各系列STC单片机的特殊功能寄存器及其可位寻址位作了定义,并存放在头文件中,STC15W4K32S4系列单片机的头文件为Keil\C51\INC\STC\STC15.H。开发用户程序时,只要在程序开始时用预处理命令#include将这个头文件包含到程序中,就可直接使用特殊功能寄存器及其可位寻址位的名称。 【例3.1】头文件引用举例。 #include<stc15.h>//使用STC15系列单片机,引用片内资源定义头文件 void main(void) { ACC=0x0f;//给累加器ACC赋值0x0f P1=0x5a;//从8位并口P1输出0x5a … } 3.3.3绝对地址访问 通常单片机系统的数据存储器和I/O接口是统一编址,绝对地址访问可以涵盖片内RAM、片外RAM、程序ROM(或Flash)及I/O接口,通过变量定义可以对单片机的硬件资源进行描述。如前所述,通常情况下C51在定义变量时只需说明其数据类型、存储类别(可缺省,默认为auto)、存储器类型(可缺省,small编译模式下默认为data),变量的地址由编译器自动分配,但描述一些特定硬件资源时,必须指定其绝对地址。例如,单片机系统扩展了某个I/O模块,该模块的绝对地址由其与单片机的硬件连接决定,对该模块的访问须指定访问单元的绝对地址。又例如,单片机系统的配置参数一般保存在非易失存储器的指定单元中,要读取这些重要参数须指定访问单元的绝对地址。C51访问绝对地址单元常使用“绝对宏”和“_at_关键字”,由于指定了变量的绝对地址,所定义的变量只能是全局变量,无须说明其存储类别。 1. 绝对宏 在Keil C51编译器的安装目录下有一个头文件Keil\C51\INC\ABSACC.H,该文件中定义了一组访问绝对地址的宏,可对code、data、pdata、xdata 4种类型的存储器进行绝对寻址,相关定义如下: #define CBYTE ((unsigned char volatile code *) 0) #define DBYTE ((unsigned char volatile data *) 0) #define PBYTE ((unsigned char volatile pdata *) 0) #define XBYTE ((unsigned char volatile xdata *) 0) #define CWORD ((unsigned int volatile code *) 0) #define DWORD ((unsigned int volatile data *) 0) #define PWORD ((unsigned int volatile pdata *) 0) #define XWORD ((unsigned int volatile xdata *) 0) CBYTE、DBYTE、PBYTE、XBYTE是以字节(8位)形式分别对code区、data区、pdata区、xdata区绝对寻址的宏,CWORD、DWORD、PWORD、XWORD是以字(16位)形式分别对code区、data区、pdata区、xdata区绝对寻址的宏。可以用绝对宏对RAM、ROM或I/O口进行定义和操作,例如: #include<absacc.h> #define PORT XBYTE[0xffc0]//PORT定义为外部扩展I/O口,地址0xffc0,8位 void main(void) {unsigned char data x;//在片内data区定义局部变量x PORT=0xa5;//将数据0xa5写入PORT口 x=CBYTE[0x1000];//将程序ROM区0x1000地址单元内容传给x } 2. _at_关键字 使用_at_关键字指定变量在存储空间的绝对地址,一般格式如下: 数据类型 [存储器类型] 变量名 _at_ 地址常数; 所定义的变量为未初始化变量(不能既定义又初始化)。如果使用_at_关键字声明访问xdata外设的变量,则需要volatile关键字,以确保C编译器不会优化必要的内存访问。例如: #define uchar unsigned char//定义宏名uchar,表示8位无符号整型 uchar code p1 _at_ 0x1000;//在CODE区定义8位整型变量,地址0x1000 uchar volatile xdata PA _at_ 0x8000; //在XDATA区定义8位I/O端口,地址0x8000 void main(void) {uchar data y;//在片内data区定义局部变量y y=p1;//将CODE区0x1000地址单元内容传给x PA=(y&0xf0)|0x05;//P1高4位保留、低4位改为5,之后从PA口输出 } 3.4C51语言的基本语句 3.4.1基本运算 C51语言的基本运算与ANSI C语言完全相同,有赋值运算、算术运算、自加减运算、位运算、复合赋值运算、关系运算、逻辑运算、逗号运算等。 1. 赋值运算 赋值运算符“=”将一个数据赋给一个变量,例如: P1=0x5a;//将十六进制数据0x5a从并行口P1输出 2. 基本算术运算 基本算术运算符都是双目运算符,共有“+”(加)、“-”(减)、“*”(乘)、“/”(除)和“%”(模)5个运算符。 3. 自加与自减运算 “++”(自加)与“--”(自减)运算符是单目运算符,是变量自加或自减1的操作。有“先自加自减”还是“后自加自减”之分,例如: i=(j++)+k;//先将j+k的值赋给i,然后j再自加1 i=(++j)+k;//j先自加1,然后将j+k的值赋给i 采用自加自减运算编程,可提高代码效率和运算速度。 4. 位运算 对char和int型数据,可以按二进制位进行操作运算,位运算有“~”(按位求反)、“&”(按位求与)、“|”(按位求或)、“^”(按位求异或)、“<<”(指定次数的按位左移)和“>>”(指定次数的按位右移)。执行按位左移(或右移)操作时,从低位(或高位)补0。在C51语言中,最后移出的位进入了PSW中的进位标志位CY。 为适应控制领域的应用,单片机重要特点之一是可进行位处理。在C51语言控制类程序设计中,普遍使用位运算,并占相当比例的代码量。 5. 复合赋值运算 赋值运算可以和算术运算、位运算结合构成复合的赋值运算,有10种复合赋值运算: +=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=。例如: unsigned char x=0xe5; x <<= 2;//对x的值(11100101)左移2位,结果x=0x94,CY=1 “与”运算的特点是: 和0与,结果为0; 和1与,结果不变。C51语言中常用这个特点将char或int型变量的某位或某几位清0,例如: unsigned char x; x &= 0xf0;//x高半字节(或高4位)保持,低半字节清0 P1 &= (1<<7)|(1<<6)|(1<<3);//P1口第7、6和3位保持不变,其他位清0 P1 &= ~((1<<7)|(1<<6)|(1<<3));//P1口第7、6和3位清0,其他位保持不变 “或”运算的特点是: 和0或,结果不变; 和1或,结果为1。C51语言中常用这个特点将char或int型变量的某位或某几位置1,例如: P2 |= (1<<6)|(1<<4)|(1<<2);//P2口第6、4和2位置1,其他位不变 “异或”运算的特点是: 和0异或,结果不变; 和1异或,结果为求反。C51语言中常用这个特点将char或int型变量的某位或某几位置求反,例如: P3 ^= (1<<5)|(1<<2)|(1<<0);//P3口第5、2和0位求反,其他位不变 6. 关系运算 关系运算即比较运算,用于比较操作数的大小关系。C51语言有6种关系运算符: <(小于)、<=(小于或等于)、>(大于)、>=(大于或等于)、==(等于)、!=(不等于),其中前4种运算符<、<=、>、>=优先级相同,为高优先级; 后2种运算符==、!=优先级相同,为低优先级。关系运算符的优先级低于算术运算符的优先级,高于赋值运算符的优先级。关系表达式的值为逻辑值,只有真(逻辑1或非0值)和假(逻辑0)两种取值。 7. 逻辑运算 逻辑运算是对逻辑变量进行逻辑与、逻辑或、逻辑非3种运算,其C51语言运算符为&&(逻辑与)、||(逻辑或)、!(逻辑非),其中逻辑非运算的优先级最高,高于算术运算符; 或逻辑运算的优先级最低,低于关系运算符,但高于赋值运算符。逻辑表达式的值也是逻辑量,只有真和假两种取值。 3.4.2分支判断语句 C51语言的分支判断语句与ANSI C的分支判断语句完全相同,有条件语句和开关语句。 1. 条件语句 条件语句使用关键字if,C51提供了3种形式的条件语句,如图3.1所示。 第一种形式如图3.1(a)所示,若逻辑表达式结果为真(逻辑1或非0值),则执行后面的语句; 若逻辑表达式为假(逻辑0),则不执行后面的语句,其语法格式为: if (逻辑表达式){语句;} 第二种形式如图3.1(b)所示,若逻辑表达式结果为真(逻辑1或非0值),则执行语句1; 若逻辑表达式为假(逻辑0),则执行语句2,其语法格式为: if (逻辑表达式){语句1;} else {语句2;} 第三种形式如图3.1(c)所示,用于实现多条件分支,其语法格式为: if (逻辑表达式1){语句1;} else if (逻辑表达式2){语句2;} ︙ else if (逻辑表达式n){语句n;} else {语句m;} 图3.1if语句的3种形式 此处所说的语句可以是组合语句。例如: unsigned char x,y,max,min; if (x>=y){max=x;min=y;} else {max=y;min=x;} 2. 开关语句 开关语句使用关键字switch,实现多分支选择,其语法格式为: switch (表达式){ case 常量表达式1: {语句1}; break; case 常量表达式2: {语句2}; break; ︙ case 常量表达式n: {语句n}; break; default: {语句m}; } 其中,switch()内的表达式可以是整型或字符型表达式,也可以是枚举型数据,每个case和default后面的语句的花括号{}可以省略。开关语句将switch()中的表达式的值与case后面的各个常量表达式的值逐一进行比较,若与某个case后面的常量表达式的值匹配,则执行其后的语句,遇到break语句时,中止执行,跳出switch语句; 如果该case中没有break语句,那么将会顺序执行下一个case后的语句; 若无匹配情况,则执行default后的语句。 3.4.3循环控制语句 C51语言的循环控制语句与ANSI C的循环控制语句完全相同,有while语句、do…while语句和for语句。 1. while语句 如图3.2(a)所示,while语句用来实现“当型”循环,其语法格式为: while (逻辑表达式){语句}; 其中while()后的语句称为循环体语句,当while()中的逻辑表达式的结果为真(逻辑1或非0值)时,则执行循环体语句;反之则终止while循环,向后执行其余程序。如果逻辑表达式的结果一开始就为假,那么循环体语句一次也不会执行。 2. do…while语句 如图3.2(b)所示,do…while语句用来实现“直到型”循环,其语法格式为: do{语句;}while (逻辑表达式); 其中do之后的语句称为循环体语句。do…while结构先执行循环体语句,然后检查逻辑表达式的结果,为真(逻辑1或非0值)则重复执行循环体语句,直到逻辑表达式的结果变为假(逻辑0)时为止,循环体语句至少执行一次。 3. for语句 如图3.2(c)所示,for语句是最为灵活、复杂的循环控制语句,其语法格式为: for (表达式1; 逻辑表达式2; 表达式3){语句;} 其中for()之后的语句称为循环体语句。for语句执行过程如下: 先求解表达式1,初始化循环; 然后检查逻辑表达式2的结果,为真(逻辑1或非0值)则执行循环体语句并求解表达式3,为假则退出循环。 图3.2循环控制语句结构 单片机用户程序开发时,经常使用循环语句构成查询等待、延时、无限循环等程序模块,分别举例说明。 图3.3例3.2图 【例3.2】如图3.3(a)所示,51单片机的I/O引脚P3.2外接按键开关K1,试编程一程序段实现等待键抬起,如图3.3(b)所示。 由图3.3(a)可知,键K1按下时,I/O引脚P3.2为低电平,抬起时为高电平,因此等待键抬起的程序段如下: #include<reg51.h>//引用单片机资源定义头文件 sbit K1=P3^2;//定义K1键位变量 … void main(void) {…; while(!K1);//等待K1抬起 …; } 【例3.3】延时程序设计。 单片机用户程序常用的延时函数可以使用while语句,也可使用for语句。 (1) 使用while语句的延时函数。 void delay(unsigned int td)//td为控制延时时长的形参 {while(td--);//判断td,不为0则自减1直到为0 } (2) 使用for语句的延时函数。 void delay(unsigned int td)//td为控制延时时长的形参 {unsigned char i,j; for (i=td;i>0;i--)//判断td,不为0则自减1直到为0 {for (j=0xff;j>0;j--); } } 【例3.4】无限循环程序设计。 单片机用户程序的整个结构一般是个无限循环程序,使用while和for均可实现无限循环。 (1) 使用while语句的无限循环。 while(1)//逻辑表达式的值恒为"非0" {…;//循环体 } (2) 使用for语句的无限循环。 for ( ; ; )//逻辑表达式2 {…;//循环体 } 3.4.4goto等语句 1. goto语句 goto语句是无条件转移语句,其一般形式为: goto 标号; … 标号:语句; 其中标号是一个标志符,程序中存在以该标号带冒号“:”开头的某语句,执行goto语句时,程序将无条件转移到给定的标号处,执行其后的语句。C51程序常用goto语句跳出多重循环,且只能从内层循环跳到外层循环,不允许从外层循环跳到内层循环。在结构化的程序设计中使用goto语句容易导致程序混乱,在C51语言程序设计中应尽量避免使用该语句。 2. break语句 break语句也是一个无条件转移语句,只能用于开关语句和循环语句之中,其一般形式为: break; 执行break语句程序将退出开关语句或循环语句,执行其后的语句。对于多重循环,break语句只能跳出其所在的那一层循环,而goto语句可以直接从最内层循环跳出来。 3. continue语句 continue语句也是一个无条件转移语句,只用于循环语句之中,其一般形式为: continue; 执行continue语句程序将中止本轮循环,程序从下一轮循环开始处继续执行,直到循环条件不满足(逻辑表达式的值为假)为止,结束循环。 4. return语句 return语句用于终止函数的执行,控制程序返回到调用该函数处,其一般形式为: return [表达式]; 如果return语句后边有表达式,则返回时要计算表达式的值,并将其作为函数的返回值; 如果不带表达式,则函数返回时,函数值不确定。一个函数内部可以有多个return语句,但程序仅执行其中的一个return语句返回调用处。函数也可以不含return语句,程序执行到最后一个界限符“}”时,返回调用处。 3.5C51语言的数组、指针、函数 3.5.1数组 数组是一组数目一定、类型相同的数据的有序集合,用一个名字标记,称为数组名,其中的数据称为数组元素,数组元素的数据类型为该数组的基本类型。例如,字符型(char)变量的有序集合称为字符型(char)数组,整型(int)变量的有序集合称为整型(int)数组。C51语言不能定义bit类型数组。 数组中各元素的顺序用下标表示,下标为n的元素表示为“数组名[n]”。只有一个下标的数组称为一维数组,有两个(或多个)下标的数组称为二维(或多维)数组。C51语言中常用一维数组、二维数组,除了说明数组元素的存储器类型外,定义数组的方法与ANSI C相同。 1. 一维数组 定义一维数组的一般形式如下: 数据类型 [存储器类型] 数组名[元素个数]; 其中数组名是用户标志符,元素个数是一个整型常量表达式。例如: unsigned char data DispBuf[8]; 该语句在DATA区定义名为DispBuf的数组,含8个无符号字符型元素。定义数组时可同时对数组进行整体初始化,若定义后需给数组赋值,只能逐个对元素赋值。例如: char b[4]={1,2,3,4}; //全部初始化,b[0]=1,b[1]=2,b[2]=3,b[3]=4 int a[6]={1,2,3};//部分初始化,a[0]=1,a[1]=2,a[2]=3,未初始化的元素为0 char s[]={"this is string"}; //定义一维字符数组,共15个元素,最后一个是"\0" 2. 二维(或多维)数组 二维数组定义的一般形式如下: 数据类型 [存储器类型] 数组名[行数] [列数]; 其中行数和列数都是整型常量表达式,例如: char c[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};//全部初始化 int d[3][4]={{1,2,3,4},{5,6,7,8}};//部分初始化,未初始化的元素为0 char s[3][6]={"length","width","height"};//定义二维字符数组 3. 用数组实现查表 在基于单片机的嵌入式控制系统中,经常需要按某已知规律实现特定的变换,例如,控制DA转换器(数字量到模拟量的转换)生成正弦波信号,或将热敏电阻(非线性元件)的电阻值变换为温度值。由于单片机运算能力有限,所以人们通常用查表法取代复杂数学计算,以实现特定的变换,查表法代码量少而且运行速度快。 【例3.5】用8位DA转换器生成32点的正弦信号,该正弦信号可如下表示: x(n)=INT[128+127sin(2πn/N)](3.1) 其中N=32,n=0,1,…,31。 按式(3.1)计算出所有32点的正统信号值,如表3.8所示。在CODE区定义无符号字符型数组xsin[32]保存数字正弦信号。 表3.832点8位数字正弦信号表 n0123456789101112131415 x(n)128152176198217233245252255252245233217198176152 n16171819202122232425262728293031 x(n)12810379573822103131022385779103 unsigned char code xsin[32]=//在CODE区定义xsin[32] {128,152,176,198,217,233,245,252,255,252,245,233,217,198,176,152, 128,103,79,57,38,22,10,3,1,3,10,22,38,57,79,103 }; 3.5.2指针 指针是存放变量(或存储器)地址的变量,与ANSI C不同,C51语言定义指针需要说明指针所指变量的存储器类型和指针自身的存储器类型,其声明格式如下: 数据类型 [存储器类型1] * [存储器类型2] 指针名; 其中数据类型和存储器类型1说明指针所指变量的数据类型和存储器类型,存储器类型2说明指针自身的存储器类型。定义指针时,如果未给出指针所指变量的存储器类型(存储器类型1缺省),则称为一般指针; 若给出指针所指变量的存储器类型,则称为基于存储器的指针; 若未给出指针自身的存储器类型(存储器类型2缺省),则由编译模式选择默认存储器类型(如表3.7所示)。C51语言不能定义bit类型的指针。 1. 基于存储器的指针 基于存储器的指针定义时,指针所指对象具有明确的存储器空间。如表3.6所示,若指针所指对象的存储器空间为data型、bdata型、idata型或pdata型,存储单元地址均为1字节,则这种指针占1字节; 若指针所指对象的存储器空间为xdata型或code型,存储单元地址均为2个字节,则这种指针占2字节。例如: char xdata * px;//定义指向XDATA区的char型数据指针px,其在默认存储区占2字节 int data * data pd;//定义指向DATA区的int型数据指针pd,其在DATA区占1字节 px=0x1000;//px存入XDATA区存储单元地址0x1000 *px=0xa5;//px指向的单元(XDATA区地址0x1000)存入0xa5(char型数据) pd=0x30; //pd存入DATA区存储单元首地址0x30 *pd=0x5ac3; //pd指向的单元(DATA区首地址0x30)存入0x5ac3(int型数据) 2. 一般指针 一般定义指针时,若没有说明指针所指对象的存储器空间,则这种指针占3字节,第1字节保存所指对象的存储器类型编码(如表3.9所示),第2字节和第3字节分别保存所指对象的高位和低位地址。例如: char * spt;//定义指向char型数据的一般指针spt,其在默认存储区占3字节 char * data dpt; //定义指向char型数据的一般指针dpt,其在DATA存储区占3字节 spt=(char code *)0x1000; //spt指向CODE区0x1000单元,其3字节存放0xff1000 dpt=(char data *)0x1f;//dpt指向DATA区0x1f单元,其3字节存放0x00001f *dpt=*spt;//将CODE区0x1000单元内容读出并存入DATA区0x1f单元 表3.9一般指针的存储器类型编码 存储器类型data/bdata/idataxdatapdatacode 编码值0x000x010xfe0xff 3.3节中使用绝对宏进行存储器绝对地址访问,所用的宏即为指向0基址的基于存储器的常数指针,例如: #define CBYTE ((unsigned char volatile code *) 0) 此即指向CODE区基址为0的字符型常数指针。 一般指针长度长,系统需根据指针首字节的存储器类型编码选择不同的寻址方式访问所指对象,程序运行速度慢,使用一般指针可以访问任意对象,兼容性高,许多C51的库函数都采用一般指针。基于存储器的指针长度短,既节省存储器空间运行速度又快,使用该指针只能指向特定的存储空间,兼容性较低。采用基于存储器的指针作为自定义函数的参数,应在程序的开始处直接给出函数的原型声明,或用预处理命令“#include”将函数原型说明文件包含进来,否则编译系统会自动将基于存储器的指针转换为一般指针,从而导致错误。 3.5.3函数 1. 函数的定义 函数是C51语言的重要内容,C51语言编译器继承了ANSI C的函数定义方法,并在选择函数的编译模式、选择函数所用工作寄存器组、定义中断服务函数、指定再入方式等方面进行了扩展。C51语言定义函数的一般格式为: 函数类型 函数名([形式参数表])[编译模式][reentrant][interrupt n][using m] {局部变量定义; 函数体语句; } 其中函数类型、函数名、形式参数表、局部变量定义、函数体语句5部分继承ANSI C函数定义相关语法,编译模式、reentrant、interrupt n、using m这4个可选项是C51语言的扩展。 (1) 函数类型说明函数返回值的类型,函数无返回值时其类型为void(空),与ANSI C不同,C51语言中函数类型可以为bit型。 (2) 函数名是用户自定义的函数标志符。 (3) 形式参数表列出主调用函数与被调用函数之间传递数据的形式参数,形式参数必须有数据类型说明,定义无形式参数函数时圆括号不能省略。 (4) 局部变量定义是对函数内部使用的临时变量进行定义。 (5) 函数体语句为实现自定义函数特定功能而设置的各种语句。 (6) 编译模式有Small、Compact、Large共3种选择,用于说明函数内部局部变量和参数的默认存储器类型,各编译模式的默认存储器类型如表3.7所示。 (7) reentrant选项用于定义可再入函数(或可重入函数)。 (8) interrupt n选项用于定义中断服务函数,其中n为中断号,可取值0~31,详见第6章。 (9) using m选项用于确定函数所用的工作寄存器组,其中m为所选工作寄存器组编号,可取值0~3。 调用有“using m”选项的函数时,将完成以下操作: 进入函数时将当前PSW(程序状态字)的值压入堆栈保护,根据m值,更改PSW中的工作寄存器组选择位RS1和RS0,执行函数体语句,退出函数时从堆栈中弹出数据恢复PSW。使用“using m”选项声明的函数时,函数既不能通过寄存器返回其值,也不能返回bit值。非中断服务函数应慎用“using m”选项,以免发生错误。 2. 可再入函数 可再入函数(或称可重入函数)可以由多个进程共享。所谓共享,即当一个进程正在执行一个可再入函数时,另一个进程可以中断该进程,然后执行同一个可再入函数,而不会影响函数的运行结果。 调用函数时,ANSI C会将调用参数和函数所用局部变量压入堆栈保护,递归调用仅使用局部变量的函数时,ANSI C函数总是可再入的。与ANSI C不同,C51堆栈使用片内RAM,堆栈很浅,调用函数时,使用固定的存储空间(称为局部数据区)传递参数和保护局部变量,递归调用将导致局部数据区被覆盖,因此C51函数一般是不可再入的。 C51语言必须用reentrant关键字声明函数为可再入的,C51编译器在默认的存储空间(由编译模式确定)中为可再入函数创建一个模拟堆栈,实现函数调用时的参数传递与局部变量入栈保护,解决数据覆盖问题。可再入函数一般占用较大内存空间,不允许传递bit型参数,不能使用bit型局部变量,运行速度也较慢。 中断服务函数和非中断服务函数共同调用的函数必须声明为可再入函数,否则将导致不可预测的结果。 3.6C51语言的预处理命令 与ANSI C类似,一个C文件经过编译后才能形成可执行的目标文件,编译过程有预处理、编译、汇编和连接共4个阶段。C51语言也提供了预处理命令,在C文件中以“#”开头的命令被称为预处理命令,有宏定义#define、包含#include,以及条件编译#if、#ifdef等。对C51源程序进行编译时,首先进行预处理,将要包含的文件插入源程序中、将宏定义展开、根据条件编译命令选择要使用的代码,然后将预处理的结果和源代码一并进行编译,最后生成目标代码。 3.6.1宏定义 宏定义指令即#define命令,以指定的标志符作为宏名来替代一个字符串的预处理命令。使用宏定义指令,可以减少程序中字符串输入的工作量,而且可以提高程序的可移植性。宏定义分为简单的宏定义和带参数的宏定义,其格式分别为: #define 宏名 宏替换体 #define 宏名(形参) 带形参的宏替换体 其中#define是宏定义指令的关键词,宏名即指定的标志符,一般使用大写字母表示,宏替换体可以是数值常量、自述表达式、字符和字符串等。使用宏定义命令时,应注意以下几点: (1) 宏定义可以出现在程序的任何地方,通常“#define”命令写在文件开头,函数之前,作为文件的一部分,在此文件内有效,或直到用“#undef”命令终止该宏定义处有效。 (2) 在编译过程的预处理阶段,编译器将宏定义有效作用域内的所有宏名用宏替换体替换,带参数时,形参用实参替换。进行宏定义时可以引用已定义的宏名,预处理时实现逐层替换。 (3) 带参数的宏定义时,宏替换体内的形参一定要带括号,因为实参可能是任意表达式,形参若不加括号可能导致其用实参替换后所得表达式与设计者的原意不符。 (4) 宏名引用只是字符串替换,带参数的宏名展开时不分配内存单元,既不进行数值的传递,也没有“返回值”的概念,因此宏定义不存在类型问题,宏名、形参和实参都没有类型。 3.6.2文件包含 文件包含指令即#include命令。所谓包含,是一个程序文件将另一个指定文件的全部内容包含进来。文件包含的一般格式为: #include<文件名>或#include "文件名" 例如,“#include<stdio.h>”就是将C51编译器提供的输入/输出库函数的说明文件stdio.h包含到用户程序中。进行大规模程序设计时,往往将程序的各功能模块函数分散到多个程序文件中,各文件由团队成员分工完成,最后再由包含命令嵌入到一个总的程序文件中。使用包含命令时,应注意以下几点: (1) “#include”命令出现在程序中的位置,被包含文件就从该位置插入。一般情况下,被包含的文件要放在程序文件的前面,否则可能会出现内容尚未定义的错误。 (2) 需包含多个文件时,要用多个包含命令,每个#include命令只指定一个文件。 (3) 采用<文件名>格式时,在头文件目录(即Keil\C51\INC目录)中查找指定文件; 采用“文件名”格式时,在当前目录中查找指定文件。 3.6.3条件编译 一般情况下,源程序中所有代码行都参与编译,但有时希望程序中某些功能模块只在满足一定条件时才参与编译,这就是条件编译。与ANSI C类似,C51语言编译器提供#if、#ifdef和#ifndef共3种条件编译预处理指令。 1. #if型的基本格式 #if 常量表达式 代码段1; #else 代码段2; #endif 如果常量表达式为非0,则编译代码段1,否则编译代码段2。 2. #ifdef型的基本格式 #ifdef 标志符 代码段1; #else 代码段2; #endif 如果本组条件编译命令之前指定标志符已用宏定义#define指令定义过(不论定义为何字符串),则编译代码段1,否则编译代码段2。 3. #ifndef型的基本格式 #ifndef 标志符 代码段1; #else 代码段2; #endif 与#ifdef型相反,如果本组条件编译命令之前指定标志符没有用宏定义#define指令定义过,则编译代码段1,否则编译代码段2。 这里的代码段1或2既可以是C51语言语句(以“;”结束),也可以是预处理指令语句(不以“;”结束)。在以上3种类型的条件编译指令中,#else分支又可以再带自己的编译选项,形成多分支的条件编译; 当然#else分支也可以没有。 条件编译在单片机用户程序设计中非常有用,单片机应用系统常有多种硬件配置,使用条件编译可使一套软件适应不同硬件配置,且不增加编译后的形成的可执行目标代码的代码量和运行速度。 3.7C51语言的库函数 C51语言提供了丰富的可直接调用的库函数,使用库函数可使程序代码简单、结构清晰、易于调试和维护,体现C51功能强大、高效等优点。每个库函数都在相应的头文件中给出了函数原型声明,调用库函数之前,必须在源程序开始处使用预处理命令#include将相应的头文件包含进来,C51语言主要函数库及其相应的头文件如表3.10所示。 表3.10C51主要函数库及其相应的头函数 函数库名称函数原型声明头文件函数库名称函数原型声明头文件 本征函数库intrins.h数学函数库math.h 输入/输出函数库stdio.h绝对地址访问函数库absacc.h 字符函数库ctype.h变量参数表函数库stdarg.h 字符串函数库string.h全跳转函数库setjmp.h 标准函数库stdlib.h偏移量函数库stddef.h C51程序设计时经常使用本征函数库、输入/输出函数库和数学函数库,分别介绍如下。 3.7.1本征函数库 本征函数是指编译时直接将固定的代码插入到当前行,而不是采用子程序调用方式实现函数功能,避免了堆栈操作,大大提高了函数的访问效率。本征函数库提供了循环移位和延时操作等函数,该函数库中的函数如表3.11所示。 表3.11本征函数库的函数及其功能(intrins.h) 循环左移函数 _crol_ _irol_ _lrol_原型 unsigned char _crol_(unsigned char val, unsigned char n); unsigned int _irol_(unsigned int val, unsigned char n); unsigned long _lrol_(unsigned long val,uchar n); 功能将字符型、整型、长整型数据val循环左移n位,相当于RL指令 循环右移函数 _cror_ _iror_ _lror_ 原型unsigned char _cror_(unsigned char val, unsigned char n); unsigned int _iror_(unsigned int val, unsigned char n); unsigned long _lror_(unsigned long val, unsigned char n); 功能将字符型、整型、长整型数据val循环右移n位,相当于RR指令 位测试函数 _testbit_ 原型bit _testbit_(bit x); 功能相当于JBC bit指令 浮点数检查函数 _chkfloat_ 原型uchar _chkfloat_(float ual); 功能测试并返回浮点数状态 延时函数 _nop_ 原型void _nop_(void); 功能使单片机产生延时,相当于插入NOP指令 3.7.2输入/输出函数库 输入/输出函数主要用于通过串口的数据传输操作,由于这些函数使用51单片机的标准串口,因此调用之前应对串口初始化,确保串口通信正常。串口初始化需设置串口模式、波特率和中断允许,8051单片机串口初始化具体代码详见第7章。该函数库提供的库函数及其功能如表3.12所示。 表3.12输入/输出函数库的函数及其功能(stdio.h) 字符读入输出函数 getchar 原型char getchar(void); 功能从串口读入一个字符,并输出该字符 字符读入函数 _getkey 原型char _getkey(void); 功能从串口读入一个字符(改变输入串口唯一需修改的函数) 字符串读入函数 gets 原型char *gets(char *s,int n); 功能从串口读入一个长度为n的字符串并存入由s指向的数组 格式化输出函数 printf 原型int printf(const char *fmstr [,…]); 功能按一定格式从串口输出数据或字符串 字符输出函数 putchar 原型char putchar(char c); 功能从串口输出一个字符(改变输出串口唯一需修改的函数) 字符串输出函数 puts 原型int puts(const char *s); 功能将字符串和换行符写入串口 格式化输入函数 scanf 原型int scanf(const char *fmstr [,…]); 功能将字符串和数据按照一定格式从串口读入 格式化内存缓冲区输出函数尾sprintf 原型int sprintf(char *s, const char *fmstr [,…]); 功能按一定格式将数据或字符串输出到内存缓冲区 格式化内存缓冲区输入函数sscanf 原型int sscanf(char *s,const char *fmstr [,…]); 功能将格式化的字符串和数据送入数据缓冲区 字符回送函数 ungetchar 原型char ungetchar(char c); 功能将读入的字符回送到输入缓冲区 字符串内存输出函数 vprintf 原型void vprintf(const char *fmstr, char *argptr); 功能将格式化字符串输出到内存数据缓冲区 指向缓冲区的输出函 数vsprintf 原型void vsprintf(char *s, const char *fmstr, char *argptr); 功能将格式化字符串和数字输出到内存数据缓冲区 3.7.3数学函数库 数学计算库函数有绝对值、指数、对数、平方根、三角、反三角、双曲函数等常用函数的,数学函数库提供的库函数及其功能如表3.13所示。 表3.13数学函数库的函数及其功能(math.h) 绝对值函数 abs、cabs fabs、labs 原型 int abs(int val);float fabs(float val); char cabs(char val);long labs(long val); 功能用于计算并返回整型、字符型、浮点型、长整型数据的绝对值 指数以及对数函数 exp、log log10、sqrt 原型 float exp(float x);float log10(float x); float log(float x);float sqrt(float x); 功能用于计算并返回指数、对数、以10为底的对数、平方根 续表 三角函数 cos、sin、tan acos、asin、atan atan2 cosh、sinh、tanh 原型 float cos(float x);float atan(float x); float sin(float x);float atan2(float y, float x); float tan(float x);float cosh(float x); float acos(float x);float sinh(float x); float asin(float x);float tanh(float x); 功能用于计算并返回余弦、正弦、正切、反余弦、反正弦、反正切、(y/x)反正切、双曲余弦、双曲正弦、双曲正切 取整函数 ceil、floor 原型float ceil(float x);float floor(float x); 功能计算并返回浮点数的整数部分。ceil不小于最小; floor不大于最大 浮点型分离函数 modf 原型float modf(float x, float *ip); 功能将浮点数x分成整数和小数两部分,整数部分放入*ip,返回小数部分 幂函数 pow 原型float pow(float x, float y); 功能计算并返回xy 3.7.4其他函数库 除了前述的本征函数库intrins.h、输入/输出函数库stdio.h、数学函数库math.h外,C51还有以下函数库: 字符函数库ctype.h、字符串函数库string.h、标准函数库stdlib.h、绝对地址访问函数库absacc.h、变量参数表函数库stdarg.h、全跳转函数库setjmp.h、偏移量函数库stddef.h,这些库函数请参见附录C和Keil Software公司的相关资料。 本章小结 单片机的C语言编程与单片机的硬件紧密联系。以第2章51单片机硬件基本知识为基础,本章主要介绍51单片机C语言编程涉及C51基本知识,包括C51语言的数据、对单片机主要资源的描述、基本运算、程序流程控制、数组、指针、函数、预处理等。读者必须掌握这些单片机C语言编程的基本知识,为编写简单的单片机C语言程序打下基础。 本章的叙述是以读者已初通ANSI C语言为前提的,如读者此前没有学习过ANSI C语言,请另补充阅读相关内容。 习题 3.1C51语言支持哪些变量类型?其中什么类型是ANSI C没有的? 3.2C51语言中整型int、浮点型float数据各是几字节、几位? 3.3简述C51语言的数据存储器类型。 3.4简述C51语言对51单片机特殊功能寄存器的定义方法。 3.5简述C51语言对51单片机片内I/O口和外部扩展I/O口的定义方法。 3.6简述C51语言对51单片机位变量的定义方法。 3.7简述C51语言定义指针的方法,指出其与ANSI C的不同点。基于存储器的指针和一般指针有什么不同? 3.8简述C51语言的函数定义的方法,指出其与ANSI C的不同点。 3.9按照给定的数据类型和存储器类型,写出下列变量的说明形式。 (1) 在data区定义字符变量val1。 (2) 在idata区定义整型变量val2。 (3) 在code区定义无符号字符型数组val3[4]。 (4) 在data区定义一个指向xdata区无符号整型的指针pi。 (5) 在bdata区定义无符号字符型变量key,定义其第0位为k0。 (6) 定义特殊功能寄存器变量P3口,定义其第2位端口P3.2。