从零开始学51单片机C语言
上QQ阅读APP看书,第一时间看更新

3.1 C51基本知识

3.1.1 标识符和关键字

1. 标识符

标识符是用来标识源程序中某个对象的名字,这些对象可以是语句、数据类型、函数、变量、数组等。

标识符的命名应符合以下规则。

(1)有效字符:只能由字母、数字和下划线组成,且以字母或下划线开头。

(2)有效长度:在C51编译器中,支持32个字符,如果超长,则超长部分被舍弃。

(3)C51的关键字不能用作变量名。

标识符在命名时,应当简单,含义清晰,尽量为每个标识符取一个有意义的名字,这样有助于阅读理解程序。

C51区分大小写,如Delay与DELAY是两个不同的标识符。

2. 关键字

关键字则是C51编译器已定义保留的特殊标识符,它们具有固定名称和含义,在程序编写中不允许标识符与关键字相同。C51采用ANSI C标准规定了32个关键字,如表3-1所示。

表3-1 ANSI C标准的关键字

另外,C51还根据51单片机的特点扩展了相关的关键字,如表3-2所示。

表3-2 C51编译器扩展的关键字

3.1.2 数据类型

C51数据类型可分为基本类型、构造类型、指针类型和空类型四类,具体分类情况如图3-1所示。

图3-1 数据类型分类

C51编译器所支持的数据类型如表3-3所示。

表3-3 C51编译器所支持的数据类型

1. char字符类型

char类型的长度是一个字节,通常用于定义处理字符数据的变量或常量。分无符号字符类型unsigned char和有符号字符类型signed char,默认值为signed char类型。unsigned char类型用字节中所有的位来表示数值,可以表达的数值范围是0~255。signed char类型用字节中最高位表示数据的符号,“0”表示正数,“1”表示负数,负数用补码表示。所能表示的数值范围是-128~+127。unsigned char常用于处理ASCII字符或用于处理小于或等于255的整型数。

2. int整型

int整型长度为两个字节,用于存放一个双字节数据。分有符号整型数signed int和无符号整型数unsigned int,默认值为signed int类型。signed int表示的数值范围是-32768~+32767,字节中最高位表示数据的符号,“0”表示正数,“1”表示负数。unsigned int表示的数值范围是0~65535。

3. long长整型

long长整型长度为四个字节,用于存放一个四字节数据。分有符号长整型signed long和无符号长整型unsigned long,默认值为signed long类型。

4. float浮点型

float浮点型用于表示包含小数点的数据类型,占用四个字节。51单片机是8位机,编程时,尽量不要用浮点型数据,这样会降低程序的运行速度并增加程序的长度。

5. 指针型

指针型本身就是一个变量,在这个变量中存放的指向另一个数据的地址。这个指针变量要占据一定的内存单元,在C51中,它的长度一般为1~3个字节。

6. bit位类型

bit位类型是C51编译器的一种扩充数据类型,利用它可定义一个位变量,但不能定义位指针,也不能定义位数组。它的值是一个二进制位,不是0就是1。例如:

bit  flag     //定义位变量flag

7. sfr特殊功能寄存器

sfr也是C51扩充的数据类型,占用一个内存单元,取值范围为0~255。利用它可以访问51单片机内部的所有特殊功能寄存器。

定义方法如下。

sfr 特殊功能寄存器名=地址常数

例如,sfr P1=0x90;这一句定义P1为P1端口在片内的寄存器,在后面的语句中我们可以用P1 = 0xff(对P1端口的所有引脚置高电平)之类的语句来操作特殊功能寄存器。

8. sfr16 16位特殊功能寄存器

在新一代的51单片机中,特殊功能寄存器经常组合成16位来使用。采用关键字sfrl6可以定义这种16位的特殊功能寄存器。sfr16也是C51扩充的数据类型,占用两个内存单元,取值范围为0~65535。

例如,对于89S52单片机的定时器T2,可采用如下的方法来定义。

sfrl6 T2=0xcc;  //定义TIMER2,其地址为T2L=0xcc,T2H=0xcd

这里T2为特殊功能寄存器名,等号后面是它的低字节地址,其高字节地址必须在物理上直接位于低字节之后。

9. sbit可寻址位

在51单片机应用系统中,经常需要访问特殊功能寄存器中的某些位,C51编译器为此提供了一个扩充关键字sbit,利用它定义可位寻址对象。定义方法有如下3种。

(1)sbit位变量名=位地址

这种方法将位的绝对地址赋给位变量,位地址必须位于0x80~0xff。例如:

sbit  OV=0xd2;
sbit  CY=0xd7;

(2)sbit位变量名=特殊功能寄存器名^位位置

当可寻址位位于特殊功能寄存器中时可采用这种方法,“位位置”是一个0~7的常数。例如:

sfr  PSW=0xd0;
sbit  OV=PSW^2;
sbit  CY=PSW^7;

(3)sbit位变量名=字节地址^位位置

这种方法以一个常数(字节地址)作为基地址,该常数必须在0x80~0xff。“位位置”是一个0~7的常数。例如:

sbit  OV=0xd0^2;
sbit  CY=0xd0^7;

sbit是一个独立的关键字,不要将它与关键字bit相混淆。关键字bit用来定义一个普通位变量,它的值是二进制数的0或1。

重点提示:C51中,用户可以根据需要对数据类型重新进行定义,定义方法如下。

typedef 已有的数据类型  新的数据类型名;

例如:

typedef int integer;

integer a,b;

这两句在编译时,先把integer定义为int,在以后的语句中遇到integer就用int置换,integer就等于int,所以a、b也就被定义为int。typedef只是对已有的数据类型作一个名字上的置换,并不是产生一个新的数据类型。

3.1.3 常量

1. 常量的数据类型

常量是在程序运行过程中不能改变值的量。常量的数据类型主要有整型、浮点型、字符型、字符串型。

(1)整型常量

整型常量可以用十进制、八进制和十六进制表示。至于二进制形式,虽然它是计算机中最终的表示方法,但它太长,所以C语言不提供用二进制表达常数的方法。

用十进制表示,是最常用也是最直观的,如7356、-90等。

八进制用数字0开头(注意不是字母o),如010、016等。

十六进制以数字0+小写字母x或大写字母X开头),如0x10、0Xf等。注意,十六进制数只能用合法的十六进制数字表示,字母a、b、c、d、e、f既可以大写,也可以小写。

(2)浮点型常量

浮点型常量可分为十进制和指数表示形式。十进制由数字和小数点组成,如0.888、3345.345、0.0等。指数表示形式为:[±]数字[。数字]e[±]数字,[]中的内容为可选项,其中内容根据具体情况可有可无,但其余部分必须有,如125e3、7e9、-3.0e-3。

(3)字符型常量

字符型常量是单引号内的字符,如‘a’‘d’等,不可以显示的控制字符,可以在该字符前面加一个反斜杠“\”组成专用转义字符。常用转义字符如表3-4所示。

表3-4 常用转义字符表

(4)字符串型常量

字符串型常量由双引号内的字符组成,如“test”“OK”等。当引号内没有字符时,为空字符串。在C语言中,系统在每个字符串的最后自动加入一个字符‘/0’作为字符串的结束标志。请注意字符常量和字符串常量的区别,如'Z'是字符常量,在内存中占一个字节,而“Z”是字符串常量,占两个字节的存储空间,其中一个字节用来存放‘\0’。

2. 用宏表示常数

假如我们要写一个有关圆的计算程序,那么π(3.14159)会被频繁用到。我们显然没有理由去改π的值,所以应该将它当成一个常量对待,那么,我们是否就不得不一遍一遍地写3.14159这一长串的数呢?

必须有个偷懒的方法,并且要提倡这个偷懒,因为多次写3.14159,难免哪次就写错了。这就用到了宏。宏不仅可以用来代替常数值,还可以用来代替表达式,甚至是代码段。下面只谈宏代替常数值的功能。

宏的语法为:

#define 宏名称 宏值

比如要代替前面说到的π值,应为:

#define  PAI  3.14159

注意,宏定义不是C51严格意义上的语句,所以其行末不用加分号结束。

有了上面的语句,我们在程序中凡是要用到3.14159的地方都可以使用PAI这个宏来取代。作为一种建议和一种广大程序员共同的习惯,宏名称经常使用全部大写的字母。

3. 用const定义常量

常量还可以用const来进行定义,格式为:

const 数据类型 常量名 = 常量值;

例如,const float PAI = 3.14159;

const的作用就是指明这个量(PAI)是常量,而非变量。

常量必须一开始就指定一个值,在以后的代码中,不允许改变PAI的值。

重点提示:用宏定义#define表示的常量和用const定义的常量有没有区别呢?有的。用#define进行宏定义的时候,只是单纯的替换,不会进行任何检查,比如类型、语句结构等,即宏定义常量只是纯粹的替换关系,如#define null 0;编译器在遇到null时总是用0代替null;而const 定义的常量具有数据类型,定义数据类型的常量便于编译器进行数据检查,使程序对可能出现的错误进行排查,所以,用const定义的常量比较安全。另外,用#define定义的常量,不会分配内存空间,每用到一次,都要替换一次,如果这个常量比较大,而且又多次使用,就会占用很大的内存空间。而const定义的常量是放在一个固定地址上的,每次使用时只调用其地址即可。

3.1.4 变量

在程序运行过程中,其值可以被改变的量称为变量。变量有两个要素,一是变量名,变量命名遵循标识符命名规则;二是变量值,在程序运行过程中,变量值存储在内存中。

在C51中,要求对所有用到的变量,必须先定义、后使用,定义一个变量的格式如下。

[存储种类]  数据类型  [存储器类型]  变量名表

在定义格式中,除了数据类型和变量名表是必要的,其他都是可选项。

1. 变量的初始化

unsigned int a;

声明了一个整型变量a。但这变量的值的大小是随机的,我们无法确定。无法确定一个变量值是常有的事。但有些时候,出于某种需要,需要我们事先给一个变量赋初值。为变量赋初值一般用“=”进行赋值,如下所示。

unsigned int a = 0;

其作用是将0赋予a,让a的值初始化为0。

定义多个变量时也一样,例如:

unsigned int a = 0,b= 1;

注意事项:定义一个变量时,如果一个变量的值小于255,一般将其定义为unsigned char类型,最好不要定义为unsigned int类型,因为unsigned char类型只占一个字节,而unsigned int类型则占用两个字节。当然,如果这个变量的值大于255,则不能将其定义为unsigned char类型,只能将其定义为unsigned int类型或其他合适的类型。

2. 变量的存储器类型

51单片机的存储器类型较多,有片内程序存储器、片外程序存储器、片内数据存储器、片外数据存储器。其中,片内数据存储器又分为低128字节和高128字节。为充分支持51单片机的这些特性,C51引入了一些关键字,用来说明存储器类型。表3-5是C51支持的存储器类型。

表3-5 C51存储器类型

例如:

unsigned char code  a=10;  //变量a的值10被存储在程序存储器中,这个值不能被改变

需要注意,当变量被定义成code存储器类型时,其值不能被改变,即变量在程序中不能再重新赋值,否则编译器会报错。

例如:

unsigned char data a ;  //在内部RAM的128字节内定义变量a
unsigned char bdata b ;  //在内部RAM的位寻址区定义变量b
unsigned char idata c ;  //在内部RAM的256字节内定义变量c

如果省略存储器类型,系统则会按编译模式SMALL、COMPACT或LARGE所规定的默认存储器类型(默认为SMALL)去指定变量的存储区域。

SMALL存储模式把所有函数变量和局部数据段放在data数据存储区,对这种变量的访问速度最快。COMPACT存储模式中,变量被定位在外部pdata数据存储器中。LARGE存储模式中,变量被定位在外部xdata数据区。

3. 变量的存储种类

此部分内容将在后面介绍函数时进行说明。

3.1.5 运算符和表达式

运算符就是完成某种特定运算的符号,分为单目运算符、双目运算符和三目运算符。单目就是指需要有一个运算对象,双目就要求有两个运算对象,三目则要有三个运算对象。表达式则是由运算及运算对象所组成的具有特定含义的式子,后面加上分号便构成了表达式语句。

1. 赋值运算符及其表达式

赋值运算符是“=”,它的功能是给变量赋值,例如:

a=0xff;  //这是一个赋值表达式语句,其功能是将十六进制数0xff赋予变量a。

2. 算术运算符及其表达式

C51有以下几种算术运算符,如表3-6所示。

表3-6 算术运算术的功能

除法运算符和一般的算术运算规则有所不同,如是两浮点数相除,其结果为浮点数,如10.0/20.0所得值为0.5,而两个整数相除时,所得值就是整数,如7/3,值为2。

求余运算符的对象只能是整型,在%运算符左侧的运算数为被除数,右侧的运算数为除数,运算结果是两数相除后所得的余数。

注意事项:自增和自减运算符的作用是使变量自动加1或减1。自增和自减符号放在变量之前和之后是不同的。

++i, - -i:在使用i之前,先使i值加(减)1。

i++,i- -:在使用i之后,再使i值加(减)1。

例如:若i=5,则执行j=++i时,先使i加1,即i=i+1=6,再引用结果,即j=6。运算结果为i=6,j=6。

再如:若i=5,则执行y=i++时,先引用i值,即j=5,再使i加1,即i=i+1=6。运算结果为i=6,j=5。

3. 关系运算符及其表达式

关系运算符用来比较变量的值或常数的值,并将结果返回给变量。C语言有6种关系运算符,如表3-7所示。当两个表达式用关系运算符连接起来时,这时就是关系表达式。

表3-7 关系运算符

4. 逻辑运算符及其表达式

逻辑运算符用于求条件式的逻辑值,C51有3种逻辑运算符,如表3-8所示。用逻辑运算符将关系表达式或逻辑量连接起来就是逻辑表达式。

表3-8 逻辑运算符

5. 位运算符及其表达式

C51中共有6种位运算符,如表3-9所示。

表3-9 位运算符

位运算一般的表达形式如下。

变量1 位运算符 变量2

在以上几种位运算符中,左移位和右移位操作稍复杂。

左移位(<<)运算符是用来将变量1的二进制位值向左移动,由变量2指定移动的位数。例如,a=0x8f(即二进制数10001111),进行左移运算a<<2,就是将a的全部二进制位值一起向左移动2位,其左端移出的位值被丢弃,并在其右端补以相应位数的“0”。因此,移位的结果是a=0x3c(即二进制数00111100)。

右移位(>>)运算符是用来将变量1的二进制位值向右移动,由变量2指定移动的位数。进行右移运算时,如果变量1属于无符号类型数据,则总是在其左端补“0”;如果变量1属于有符号类型数据,则在其左端补入原来数据的符号位(即保持原来的符号不变),其右端的移出位被丢弃。例如,对于a=0x8f,如果a是无符号数,则执行a>>2之后,结果为a=0x23(即二进制数00100011);如果a是有符号数,则执行a>>2之后,结果为a=0xe3(即二进制数11100011)。

例:用移位运算符实现流水灯。

在本书第2章中,曾介绍过两个流水灯的例子,当时采用的是给P2口逐位赋值和使用库函数_crol_来实现的,实际上,流水灯还可以通过移位运算符实现,实现的源程序如下:

#include<reg52.h>
#define uchar unsigned char
#define uint unsigned int
void Delay_ms(uint xms)           //延时程序,xms是形式参数
{
  uint i, j;
  for(i=xms;i>0;i--)              //i=xms,即延时x毫秒,xms由实际参数传入一个值
      for(j=115;j>0;j--);
}
void main()
{
  while(1)
  {
     uchar led_data=0xfe;         //给led_data赋初值0xfe,点亮第一只LED灯
     uchar i;
     for(i=0;i<8;i++)
     {
         P2=led_data;             //将led_data赋值给P0
         Delay_ms(500);
         led_data=(led_data<<1)|0x01;  //左移1位后,再与0x01进行或运算,以保证只有一
                                       //只LED灯被点亮
     }
  }
}

该程序在ch3/ch3_1文件夹中。

6. 复合赋值运算符及其表达式

复合赋值运算符就是在赋值运算符“=”的前面加上其他运算符。表3-10是C51中的复合赋值运算符。

表3-10 复合赋值运算符

复合赋值运算其实是C51中一种简化程序的方法,凡是二目运算都可以用复合赋值运算符去简化表达。例如,a+=1等价于a=a+1;b/=a+2等价于b=b/(a+2)。

7. 其他运算符及其表达式

(1)条件运算符

C语言中有一个三目运算符,它就是“?:”条件运算符,它要求有三个运算对象。它可以把三个表达式连接构成一个条件表达式。条件表达式的一般形式如下。

逻辑表达式? 表达式1 : 表达式2

其功能是:当逻辑表达式的值为真(非0)时,整个表达式的值为表达式1的值;当逻辑表达式的值为假(0)时,整个表达式的值为表达式2的值。

例如,a=1,b=2,我们要求是取a、b两数中较小的值放入变量min中,可以这样写程序。

if (a<b)
min = a;
else
min = b;

用条件运算符去构成条件表达式就变得十分简单明了。

min = (a<b)?a : b

很明显,它的结果和含意都和上面的一段程序是一样的,但是代码却比上一段程序少很多,编译的效率也相对要高,存在的问题是可读性较差,在实际应用时可以根据自己的习惯使用。

(2)sizeof运算符

sizeof是用来求数据类型、变量或是表达式的字节数的一个运算符,但它并不像“=”之类运算符那样在程序执行后才能计算出结果,它是直接在编译时产生结果的,格式如下:

sizeof (数据类型);

(3)强制类型转换运算符

C51有两种数据类型转换方式,即隐式转换和显式转换。隐式转换是在对程序进行编译时由编译器自动处理的,隐式转换遵循以下规则。

① 所有char型的操作数转换成int型。

② 用运算符连接的两个操作数如果具有不同的数据类型,按以下次序进行转换:如果一个操作数是float类型,则另一个操作数也转换成float类型;如果一个操作数是long类型,则另一个操作数也转换成long类型;如果一个操作数是unsigned类型,则另—个操作数也转换成unsigned类型。

③ 在对变量赋值时发生的隐式转换,将赋值号“=”右边的表达式类型转换成赋值号左边变量的类型。例如,把整型数赋值给字符型变量,则整型数的高8位将丧失;把浮点数赋值给整型变量,则小数部分将丧失。

在C51中,只有基本数据类型(即char、int、long和float)可以进行隐式转换,其余的数据类型不能进行隐式转换。例如,我们不能把一个整型数利用隐式转换赋值给一个指针变量,在这种情况下就必须利用强制类型转换运算符来进行显式转换,强制类型转换格式如下。

(数据类型)表达式;

其中,(数据类型)中的类型必须是C51中的一个数据类型,例如:

int a=7,b=2;
float y;
y=(float)a/b;  //先将a转换成float型,再进行运算; 注意与y=(float)(a/b)不同

C51规定了算术运算符的优先级和结合性。优先级是指当运算对象两侧都有运算符时,执行运算的先后次序,按运算符优先级别高低顺序执行运算。结合性是指当一个运算对象两侧的运算符的优先级别相同时的运算顺序。各种运算符的优先级和结合性如表3-11所示。

表3-11 运算符的优先级和结合性

说明:同一优先级的运算符由结合方向确定,例如,*和/具有相同的优先级,因此,3*5/4的运算次序是先乘后除。取负数运算符-和自加1运算符++具有同一优先级,结合方向为自右向左,因此,表达式-i++相当于-(i++)。

3.1.6 表达式语句和复合语句

C51是一种结构化的程序设计语言,提供了相当丰富的程序控制语句,下面先介绍表达式语句和复合语句。

1. 表达式语句

表达式语句是最基本的一种语句。不同的程序设计语言都会有不一样的表达式语句,如VB语言,就是在表达式后面加入回车构成VB的表达式语句,而在51单片机的C51中则是加入分号“;”构成表达式语句,例如:

a=b*10;
i++;

都是合法的表达式语句。一些初学者往往在编写调试程序时忽略了分号“;”,造成程序无法被正常地编译。另外,在程序中加入了全角符号、运算符输错、漏掉也会造成程序不能被正常编译。

在C51中有一个特殊的表达式语句,称为空语句,它仅仅是由一个分号“;”组成。

2. 复合语句

在C51中,一对花括号“{}”不仅可用作函数体的开头和结尾标志,也可作为复合语句的开头和结尾的标志,复合语句也称为“语句块”,其形式如下。

{
  语句1;
  语句2;
  ……;
  语句n;
}

复合语句之间用“{}”分隔,而它内部的各条语句还是需要以分号“;”结束。复合语句是允许嵌套的,也是就是在“{}”中的“{}”也是复合语句。复合语句在程序运行时,“{}”中的各行单语句是依次顺序执行的。在C51中,可以将复合语句视为一条单语句,也就是说,在语法上等同于一条单语句。

对于一个函数而言,函数体就是一个复合语句。要注意的是在复合语句中所定义的变量是局部变量,局部变量就是指它的有效范围只在复合语句内部,即函数体内部。

3.1.7 条件选择语句

1. if语句

if条件语句又被称为分支语句。C51提供了3种形式的if条件语句。

(1)if…else语句

if…else语法格式如下。

if (条件表达式)
{
  语句1
}
else
{
  语句2
}

该语句的执行过程是:如果条件为真,执行语句1,否则(条件为假),执行语句2。

(2)if语句

if语句格式如下。

if (条件表达式)
{
  语句
}

该语句的执行过程是:如果条件为真,执行其后的if语句,然后执行if语句的下一条语句,如果条件不成立(条件为假),则跳过if语句,直接执行if语句的下一条语句。

例如:

if (a==b){a++;}
a--;

当a等于b时,a就加1,否则,a就减1。

(3)嵌套的if…else语句

嵌套的if…else语法格式如下。

if(条件表达式1)
{
  语句1
}
else if(条件表达式2)
{
  语句2
}
else if(条件表达式3)
{
  语句3
}
...
else
{
  语句n
}

以上形式的嵌套if语句执行过程可以这样理解:从上向下逐一对if后的条件表达式进行检测,当检测某一表达式的值为真时,就执行与此有关的语句。如果所有表达式的值均为零,则执行最后的else语句,例如:

if(a>=0) {c=0;}
else if(a>=1) {c=1;}
else if(a>=2) {c=2;}
else if(a>=3) {c=3;}
else  {c=4;}

2. switch语句

虽然用多个if语句可以实现多方向条件分支,但是,使用过多的if语句实现多方向分支会使条件语句嵌套过多,程序冗长,这样读起来也很不好理解。这时如果使用开关语句,不但可以达到处理多分支选择的目的,而且可以使程序结构清晰。开关语句的语法如下。

switch (表达式)
{
  case 常量表达式1: 语句1; break;
  case 常量表达式2: 语句2; break;
  case 常量表达式3: 语句3; break;
  case 常量表达式n: 语句n; break;
  default: 语句
}

运行时,switch后面的表达式的值将会作为条件,与case后面的各个常量表达式的值相对比,如果相等,则执行后面的语句,再执行break语句,跳出switch语句。如果case没有和条件相等的值,就执行default后的语句。当要求没有符合的条件时,不做任何处理,则可以不写default语句。

注意事项:如果在case语句中遗忘了break,则程序在执行了本行case选择之后,不会按规定退出switch语句,而是将执行后续的case语句。有经验的程序员可以在switch语句中预设一系列不含break的case语句,这样程序会把这些case语句加在一起执行。这对某些应用可能是很有效的,但对另一些情况则将引起麻烦,因此使用时必须谨慎小心。

3.1.8 循环语句

C51中用来实现循环的语句有以下三种:while、do while和for循环语句。

1. while循环语句

while语句一般形式为:

while(条件表达式)
{
  循环体语句;
}

while语句中,while是C51的关键字。while后一对圆括号中的表达式用来控制循环体是否执行;while循环体可以是一条语句,也可以是多条语句。若是一条语句可以不加大括号;若是多条语句,应该用大括号括起来组成复合语句。

while语句的执行过程如下。

(1)计算while后一对圆括号中条件表达式的值。当值为非0时,执行步骤(2);当值为0时,执行步骤(4)。

(2)执行循环体中语句。

(3)转去执行步骤(1)。

(4)退出while循环。

由以上叙述可知,while后一对圆括号中表达式的值决定了循环体是否执行,因此,进入while循环后,一定要有能使此表达式的值变为0的操作,否则,循环将会无限制地进行下去。

在一些特殊情况下,while循环中的循环体可能是一个空语句,如下所示。

while(条件表达式) { ;}

其中的大括号可以省略,但分号绝不能省略,如下所示。

while(条件表达式) ;

这种循环语句的作用是,如果条件表达式为非0,则反复进行判断(即处于等待状态);若条件表达式的值为0,则退出循环。例如,下面这段程序是读取51单片机串行口数据的函数,其中就用了一个空语句while(!RI)来等待单片机串行口接收结束。

read_com()         //函数定义
{
  char a;          //变量定义
  while(!RI);      //若RI=0,即!RI为1,说明没有接收中断,则继续等待串口接收数据
  a=SUBF;          //读串行口内容
  RI=0;            //清除串行口接收标志
  return(a);       //返回
}

2. do while循环语句

do while语句一般形式为:

do
{
  循环体语句;
}
while(条件表达式);

do while循环语句中,do是C51的关键字,必须和while联合使用。do-while循环由do开始,用while结束。必须注意的是:在while(表达式)后的“;”不可丢,它表示do while语句的结束。while后一对圆括号中的表达式用来控制循环是否执行。在do和while之间的循环体内可以是一条语句,也可以是多条语句。若是一条语句可以不加大括号;若是多条语句,应该用大括号括起来组成复合语句。

do while语句的执行过程如下。

(1)执行do后面循环体中的语句。

(2)计算while后一对圆括号中表达式的值。当值为非0时,转去执行步骤(1);当值为0时,执行步骤(3)。

(3)退出do-while循环。

由do while构成的循环与while循环十分相似,它们之间的重要区别是:while循环的控制,出现在循环体之前,只有当while后面表达式的值为非0时,才可能执行循环体。在do while构成的循环中,总是先执行一次循环体,然后再求表达式的值,因此,无论表达式的值是0还是非0,循环体至少要被执行一次。

和while循环一样,在do while循环体中,要有能使while后表达式的值变为0的操作,否则,循环将会无限制地进行下去。

以笔者的经验,do while循环用得并不多,大多数的循环用while来实现会更直观。

请比较以下两段程序,前者使用while循环,后者使用do while循环。

程序1:

int a = 0;
while(a>0) {a--;}

变量a初始值为0,条件a>0显然不成立。所以循环体内的a--,语句未被执行。本段代码执行后,变量a值仍为0。

程序2:

int a = 0;
do{ a--;}
while(a>0);

尽管循环执行前,条件a>0一样不成立,但由于程序在运行到do时,并不先判断条件,而是直接先运行一遍循环体内的语句:a--。于是a的值成为-1,然后,程序才判断a>0,发现条件不成立,循环结束。

3. for循环语句

for循环语句比较常用,其一般形式为:

for(表达式1;表达式2;表达式3)
{
  循环体语句;
}

for是C51的关键字,其后的一对圆括号中通常含有三个表达式,各表达式之间用“;”隔开。紧跟在for(…)之后的循环体,可以是一条语句,也可以是多条语句。若是一条语句可以不加大括号;若是多条语句,应该用大括号括起来组成复合语句。

for循环的执行过程如下。

(1)计算“表达式1”(“表达式1”通常称为“初值设定表达式”)。

(2)计算“表达式2”(“表达式2”通常称为“终值条件表达式”);若其值为非0,转步骤(3);若其值为0,转步骤(5)。

(3)执行一次for循环体。

(4)计算“表达式3”(“表达式3”通常称为“更新表达式”),转向步骤(2)。

(5)结束循环,执行for循环之后的语句。

下面对for循环语句的几种特例进行简要说明。

第一种特例:for语句中的小括号内的三个表达式全部为空,形成for(;;)形式,这意味着没有设初值,无判断条件,循环变量为增值,它的作用相当于while(1),即构成一个无限循环过程。

第二种特例:for语句三个表达式中,表达式1缺省。例如:

Delay_ms(unsigned int  xms)
{
  unsigned int j;
  for(;xms>0;xms--)
      for(j=0;j<115;j++);
}

这是一个延时程序,在第一个for循环中,没有对变量xms赋初值,因为这里的变量xms是Delay_ms函数的形参,程序运行时,xms由实参传入一个数值。

第三种特例:for语句三个表达式中,表达式2缺省。例如:

for(i=1;;i++)
sum=sum+i;

即不判断循环条件,认为表达式始终为真。循环将无休止地进行下去。它相当于:

i=1;
while(1)
{
  sum=sum+i;
  i++;
}

第四种特例:没有循环体的for语句。例如:

int sum=2000;
for(t=0;t<sum;t++){;}

此例在程序中起延时作用。

下面举一个例子,说明for循环语句的具体应用。

例:由单片机的P3.7脚(外接蜂鸣器)输出救护车的声音。

通过软件延时,使P3.7脚输出1kHz和2kHz的变频信号,每隔1s交替变化1次,即可模拟救护车的声音,详细源程序如下。

#include <reg52.h>
sbit P37=P3^7;
/ *******以下是250μs延时函数******* /
void delay250(unsigned int x)
{
unsigned int j,i;
for(i=0;i<x;i++)
  for(j=0;j<25;j++);
}
/ ********以下是主函数******** /
void main()
 {
 unsigned int i,j;
 {
 for(;;)                   //大循环
  {
    for(i=0;i<2000;i++)    //循环2000次
     {
        P37=~P37;          //输出声音
        delay250(2);       //延时500μs
     }
     for(j=0;j<4000;j++)   //延时4000次
     {
        P37=~P37;          //输出声音
        delay250(1);       //延时250μs
     }
   }
  }
}

该源程序在ch3/ch3_2文件夹中。

4. break和continue语句在循环体中的作用

(1)break语句

前面已经介绍过,用break语句可以跳出switch语句体。在循环结构中,也可应用break语句跳出本层循环体,从而提前结束本层循环。

例如,如下程序。

#include<reg52.h>
void main(void)
{
  int i,sum;
  sum=0;
  for(i=1;i<=10;i++)
  {
     sum=sum+i;
        if(sum>5)break;
  }
  while(1);
}

该例中,当i=3时,sum的值为6,if(sum>5)语句的值为1,于是执行break语句,跳出for循环,执行“while(1);”语句,程序处于等待状态。若没有break语句,则程序需要等到i<=10时才能退出循环。

(2)continue语句

continue意为继续。它的作用及用法和break类似。重要区别在于:当循环遇到break,则直接结束循环,若遇上continue,则停止当前这一遍循环,然后直接尝试下一遍循环。可见,continue 并不结束整个循环,而仅仅是中断当前这一遍循环,然后跳到循环条件处,继续下一遍的循环。当然,如果跳到循环条件处,发现条件已不成立,那么循环也将结束,所以我们称为尝试下一遍循环。

在while和do while循环中,continue语句使得流程直接跳到循环控制条件的测试部分,然后决定循环是否继续进行。在for循环中,遇到continue后,跳过循环体中余下的语句,而去对for语句中的“表达式3”求值,然后进行“表达式2”的条件测试,最后根据“表达式2”的值来决定for循环是否执行。下面举例说明。

#include<reg52.h>
void main(void)
{
  int i,sum = 0;
  for(i = 1; i<=100;i++)
  {
            if( i % 10 == 3) continue;
            sum =sum+ i;
  }
  P2=sum;
  while(1);
}

该源程序在ch3/ch3_3文件夹中。

为了判断一个1~100的数中哪些数的个位是3,程序中用了求余运算符%,即将一个2位以内的正整数,除以10以后,余数是3,就说明这个数的个位为3。比如23除以10,商数是2,余数是3。

程序执行的最终结果为:sum=0x11da(十进制为4570),并将结果送P2口,然后,程序处于等待状态。

程序的执行结果可通过Keil软件进行观察,方法是:启动Keil软件,输入上面的源程序,进入软件仿真界面,选择菜单view→Watch→locals,打开观察窗口,按工具栏中062-02按钮进行调试,sum不断变化,循环结束后,sum的最终结果为0x11DA(即4570),如图3-2所示。

图3-2 在观察窗口查看变量sum的值

5. goto语句

goto语句是一种无条件转移语句,使用格式为:

goto 标号;

其中,标号是C51中一个有效的标识符,这个标识符加上一个“:”一起出现在函数内某处,执行goto语句后,程序将跳转到该标号处并执行其后的语句。另外,标号必须与goto语句同处于一个函数中,但可以不在一个循环层中。通常,goto语句与if条件语句连用,当满足某一条件时,程序跳到标号处运行。

goto语句通常不用,主要是因为它将使程序层次不清,且不易读,但在多层嵌套退出时,用goto语句则比较合理。