
任务2 8路LED流水灯设计
一、教学目标
【能力目标】
(1)学会简单硬件电路设计。学会依据任务要求编制程序流程图。
(2)会运用所学知识,完成C51程序初步设计。
【知识目标】
(1)掌握单片机存储器结构。掌握单片机堆栈操作过程。
(2)熟悉单片机数据存储类型及SFR、并行接口C51定义。掌握C51运算符应用。(3)基本掌握if,if-else语句的具体应用。
二、工作任务
(1)P1口控制8只LED(LED1~LED8),使8个指示灯每隔一段时间循环左移1位,依次一个一个从上到下循环闪动。设晶振频率为12MHz。
(2)P1口控制8只LED(LED1~LED8),使8个指示灯实现“点亮—熄灭”闪烁两次、接着8个指示灯从下到上依次点亮两次的周期循环。
(3)P1口控制8只LED(LED1~LED8),P2.0接一个单掷开关控制流水灯的循环方向。开关闭合,从当前显示LED处实现从下到上依次循环;开关打开,同样从当前处从上到下依次循环。初始值定为从两头开始。设晶振频率为12MHz。
三、任务实施
1.任务分解1
1)硬件电路及工作原理
硬件电路设计参考如图2-1-3所示。在项目二任务1中,完成8路LED全亮和全灭闪烁显示,而本任务是流水灯循环显示,每次只有一只LED发光,即点亮步骤:P1.0→延时→P1.1→延时→P1.2→延时→P1.3→延时→P1.4→延时→P1.5→延时→P1.6→延时→P1.7→延时→P1.0,依次点亮即可完成循环闪烁。参考流程图如图2-2-1所示。

图2-2-1 左移循环显示流程图
2)源程序输入与调试
#include <reg52.h> //预处理命令,装载C51单片机头文件 delay(unsigned int t) //定义延时函数 { unsigned int i, j; //定义无符号整形变量i和j for(i=0; i<t; i++) for(j=0; j<t; j++); } main( ) //主函数 { unsigned char LED, i; while(1) //无限循环 { LED=0xfe; //初始状态从点亮LED1(P1.0)开始循环 for(i=0; i<8; i++) //8次循环 { P1=LED; delay(1000); //调用延时函数 LED=(LED<<1)|1; //变量循环左移1位 } } }
3)程序阅读
●“led=0xfe;”语句,对应硬件电路,初始状态从点亮LED1(P1.0)开始循环,送入不同的数值,可以设置上电时的起始LED点亮数目及位置。如led=0xf0;高4位不亮,低4位全亮。
●编写了延时函数delay(unsigned int t)。delay带入口参数,即延时时间可调,通过不同的入口参数,可以调节LED点亮的时间,改变循环的延时频率。入口参数范围由(unsigned int t)语句确定为:0~65 535。本例选择1000,符合要求。
●完成移位任务方法很多,如采用以下循环也能完成:P1=0xfe→延时→P1=0xfd→延时→P1=0xfb→延时→P1=0xf7→延时→P1=0xef→延时→P1=0xdf→延时→ P1=0xbf→延时→P1=0x7f→延时→P1=0xfe。
可以发现,该种方法思路清晰,简单易编,但程序重复较多,代码较长,编写不严谨,程序编写不符合精简要求。大家可以编写两个程序进行比较。
●本例中,采用左移循环语句来完成。选用8只LED,所以for语句循环次数为8次,
led=(led<<1)|1;完成循环左移1位功能。
在后面我们还介绍其他方法:如项目二任务3中的库函数移位法和任务4中的数组法等。
2.任务分解2
1)硬件电路及工作原理
硬件电路设计如图2-1-3所示。本任务是将前面的工作任务进行综合。先完成8路指示灯实现“点亮—熄灭”闪烁两次,然后从下往上依次点亮两次即可完成任务。即点亮步骤:全亮→全灭→全亮→全灭→P1.7→延时→……→P1.0→延时→P1.7→延时→……→P1.0→延时→全亮→全灭→……,依次循环。参考流程图如图2-2-2所示。

图2-2-2 8路循环显示流程图
2)源程序输入与调试
#include <reg52.h> //预处理命令,装载C51单片机头文件 delay(unsigned int t) //定义延时函数 { unsigned int data i, j; //定义无符号整形变量i和j位于片内RAM for(i=0; i<t; i++) for(j=0; j<t; j++); } void main(void ) //主函数 { while(1) //无限循环 { unsigned char data i, m, LED; //定义无符号字符变量且位于片内RAM for(m=0; m<2; m++) //LED全灭、全亮两次 { P1=0x00; //点亮LED1~LED8 delay(200); //调用延时函数 P1=0xff; //熄灭LED1~LED8 delay(200); //调用延时函数 } for(m=0; m<2; m++) { LED=0x7f; //初始状态从点亮LED8(P1.7)开始循环 for(i=0; i<8; i++) //8次循环 { P1=LED; delay(200); //调用延时函数 LED=(LED>>1)|0x80; //变量循环右移1位 } } } }
3)程序阅读
●本任务分解2是对前面所有任务进行的综合。因此,将两个程序进行合并,采用顺序结构,先执行闪烁,后执行移位显示。用两个for语句完成。
●显示移位任务是从P1.7开始依次从下往上,选初始值LED=0x7f。
●选用“>>”右移1位。由于右移后填空位补0,为了保证显示正确,采用或0x80后再传送。
3.任务分解3
1)硬件电路及工作原理
硬件电路设计如图2-2-3所示。LED接在P1口上,实现8路循环显示,开关通过上拉电阻接到+5V。开关打开,P2.0=1,高电平;开关闭合,P2.0=0,低电平。通过检测P2.0引脚电平,即可实现开关状态判断。参考流程图如图2-2-4所示。

图2-2-3 开关控灯方向硬件图

图2-2-4 开关控灯流程图
2)源程序输入与调试
#include <reg52.h> //预处理命令,装载C51单片机头文件 sbit K1_0=P2^0; delay(unsigned int t) //定义延时函数 { unsigned int i, j; //定义无符号整形变量i和j for(i=0; i<t; i++) for(j=0; j<t; j++); } void main(void ) //主函数 { unsigned char LED; //定义无符号字符变量LED位于片内RAM if(K1_0!=0) //初始化判断开关状态赋值 LED=0xfe; else LED=0x7f; while(1) //无限循环 { if(K1_0!=0) //判断开关 { //开关状态=1,打开,从上到下循环 P1=LED; delay(200); //调用延时函数 LED=(LED<<1)|1; //变量循环左移1位 if(LED==0xff) //判断显示界限 LED=0xfe; } else { //=0,闭合,从下到上循环 P1=LED; delay(200); //调用延时函数 LED=(LED>>1)|0x80; //变量循环右移1位 if(LED==0xff) LED=0x7f; } } }
3)程序阅读
●本任务分解3是将前面的LED左移、右移任务通过开关状态进行综合。因此,将两个程序进行合并,采用顺序结构。
●显示移位任务是从P1.7开始依次从下往上,选初始值LED8=0x7f。从上往下,选初始值LED1=0xfe。
四、相关知识
1.单片机存储器结构
C51单片机将程序存储器(ROM)和数据存储器(RAM)分开,并有各自的寻址机构和寻址方式。
1)AT89C52程序存储器
程序存储器(ROM)用来存放编写好的C51目标程序和执行过程中不会变化的表格常数,如数码管显示代码。分为片内和片外两个空间,由引脚设置选择。
,选择片外ROM,
选择片内RAM。使用中尽量选择内部程序存储器,一般不扩展片外ROM,以节省硬件资源。
程序存储器中规定了一些地址为专用地址,用来存放中断向量表,如中断入口地址。在使用中断时,其地址不能被覆盖,即用户不能使用。其地址分配如下:
0000H 复位入口地址(主程序入口)
0003H 外部中断0中断入口地址
000BH 定时器/计数器0中断入口地址
0013H 外部中断1中断入口地址
001BH 定时器/计数器1中断入口地址
0023H 串行口中断入口地址
002BH 定时器/计数器2中断入口地址
由于0000H~002FH ROM地址为中断入口地址,因此用户程序一般从0030H处开始存放,但在0000H处放置一条跳转指令,使系统复位后从0000H跳转到用户程序。
AT89C52单片机片内有8KB的程序存储器,其地址为0000H~1FFFH。
2)片内低128B数据存储器
MCS-51系列单片机分为51子系列和52子系列两大类。其内部均有128B(地址范围00H~7FH)的基本内存,分为三个区域:工作寄存器区,位寻址区和用户RAM区,如表2-2-1所示。
表2-2-1 低128B数据存储器
(1)工作寄存器区。
工作寄存器位于片内RAM单元地址(00H~1FH)的区域内。单片机将这区域划分为4个区域,称为区0~区3,每个区由8个工作寄存器组成,它们拥有同样的名称:R0~R7,用来存放操作数和中间结果等。
任意时刻,CPU只能使用其中一组寄存器,把正在使用的寄存器区称为当前工作寄存器区,选择哪一个作为当前工作寄存器区由程序状态寄存器PSW中的RS1和RS0两位设置。当不使用工作寄存器时,00H~1FH可用做通用RAM,分区选择如表2-2-2所示。
表2-2-2 工作寄存器区的选择
在C51中,一般不会直接使用工作寄存器R0~R7,它隐含在程序中,如中断服务程序。
(2)位寻址区。
位寻址区位于片内RAM的20H~2FH单元地址,共有16个RAM,总计128位。单片机可以对这些单元数据整体读/写,也可以对其中的某一单元进行单独操作。
(3)用户数据缓冲区。
用户RAM区单元地址为30H~7FH。这个区只能使用字节寻址方式,用于存放堆栈数据和用户使用。
3)片内高128B数据存储器(扩展内存)
52子系列中增加了128个用户RAM,称为高128BRAM,其范围为80H~0FFH。这部分区域使用中只能采用间接寻址方式传送数据。
4)片外数据存储器
片外RAM地址编号由16位数字组成,因此存储单元地址的最大范围为(0000H~0FFFFH)。
5)特殊功能寄存器
特殊功能寄存器是指RAM功能固定、用户不得更改(功能由单片机厂家规定)的寄存器,其单元地址为80H~0FFH。使用中采用直接寻址方式传送数据,如表2-2-3所示。
注意:高128B数据存储器采用间接寻址方式传送数据。
特殊功能寄存器采用直接寻址方式传送数据。
表2-2-3 52系列特殊功能寄存器一览表
(1)累加器Acc。
Acc是单片机中工作最繁忙的特殊功能寄存器,它既可存放操作数,也可用来存放运算的中间结果。在C51中,一般不直接使用其名称。
(2)程序状态寄存器PSW。
程序状态标志寄存器PSW是一个8位寄存器,用来存储当前指令执行后的状态,以供程序查询和判别。复位后PSW=0。各位的定义如表2-2-4所示。
表2-2-4 C52系列特殊功能寄存器一览表
●Cy:进位/借位标志位。单片机为8位,其8位运算器只能表示到0~255,如果做加法的话,两数相加可能会超过255,其进位就存放在这里。执行减法相类似。
●Ac:半字节进位标志位。在进行加法或减法运算时,低4位进位/借位标志位。一般作为十进制调整的判别位。
●F0、F1:用户自定义标志位。
●RS1、RS0:4个通用寄存器区的选择位。
●OV:溢出标志位。
●P:奇偶校验标志。累加器A中的“1”的个数为奇数时P=1,否则P=0。
(3)程序计数器PC。
PC是一个16位的计数器,它决定程序的流向,里面存放的是下一条需要运行的指令地址,寻址空间为64KB。单片机复位后,PC=0000H。
2.堆栈操作
1)概念
单片机中除了有固定功能的寄存器外,还需要有可以公共使用的寄存器,如仓库里可以存放不同用户的货物,我们把这类可以公共使用的寄存器称为栈区,把存取物品的规则,称为“堆栈”。数据写入栈区称为入栈,数据从栈区中读出称为出栈。
2)堆栈指针(SP)
栈区的大小不固定,用户可以根据程序的需要来调整。一般将栈区安排在片内低128B的用户区30H~7FH的范围内。为了准确指明栈区的所在位置,安排一个特殊功能寄存器SP存放当前的栈顶位置,把SP称为堆栈指针,里面存放的是栈顶单元地址。单片机复位后,SP的值位07H。由于默认栈区与工作寄存器区1相冲突。因此,通电后一般都修改堆栈指针,让它指向用户区。
3)数据操作规则
堆栈的操作规则,用一句话来概括就是:“先进后出”。
进栈操作:先SP+1,后写入数据。
出栈操作:先读数据,后SP-1。
4)堆栈功能
堆栈主要是为子程序调用和中断操作而设立的。其具体功能有两个:保护断点和保护现场。
5)堆栈使用方式
堆栈使用方式,即在调用子程序或中断时,下一条运行指令地址(断点)自动进栈。程序返回时,断点再自动弹回PC。
3.C51的数据存储类型
采用汇编语言编程时,是按地址去读写指定存储单元的内容,用不同的指令去表示不同的存储空间。如MOV指令访问片内数据存储器,MOVX指令访问片外数据存储器,MOVC指令访问程序存储器。对内部结构必须非常了解。在C51中直接使用变量名去访问存储单元,而无须关心变量的存放地址,程序的可读性大大增加了。但变量放在哪一个存储空间呢?这对最终目标代码的效率影响很大。因此,在编程时除了说明变量的数据类型外,还应说明变量所在的存储空间即存储类型。具体分配如表2-2-5所示。
表2-2-5 C51存储类型与C51单片机存储空间的对应关系
C51中变量定义的格式
数据类型 [存储类型] 变量名1 [,变量名2]……[,变量名n];
例如,
char data temp; //定义一个有符号字符变量temp,定位于片内数存储区(00H~7FH) bit bdata flags; //定义一个位变量flags,位于位寻址区(20H~2FH) uchar bdata speed; //无符号字符变量speed,位于位寻址区(20H~2FH) uchar idata len; //无符号字符变量len,定位在片内(00H~FFH),用间接寻址方式 uchar code seg[ ]={0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d}; //定义无符号字符型数组seg,存放在程序存储器(ROM)中
说明:选择变量的存储类型时,可遵循以下的原则。
●通常将一些固定不变的参数或表格放在ROM中,即存储类型设为code。
●访问片内数据存储器(存储类型为data/bdata/idata)比访问片外数据存储器的速度要快,因此对一些使用频率较高的变量或者对速度要求较高的程序中的变量可选择片内数据存储器,而将一些不常使用的变量存放于片外数据存储器(存储类型为pdata/ xdata)中。当然如果系统中所用的变量较少,片内数据存储器的空间足够应付时,就无须使用片外数据存储器了。
●对片内高128B的数据存储器空间进行读写,可以将变量的存储类型定义为idata。
●通常将位变量或希望位与字节混合访问的变量的存储类型设置为bdata。
●如果变量定义时省去存储类型说明,编译时会自动选择默认的存储类型,而默认的存储类型由存储模式确定。
在C51应用系统中,依据任务要求从节约资源的角度出发,系统设计可大可小。因此,C51有SMALL、COMPACT、LARGE3种存储模式相对应。在KEIL环境中,可以通过目标工具选项设置选择所需的存储模式。存储模式作为编译选项如表2-2-6所示。
表2-2-6 C51存储模式
4.特殊功能寄存器及其C51定义
通过对变量的存储类型的定义,可以通过变量访问C51单片机的各类存储器,但又通过什么方法去访问它的特殊功能寄存器呢?
1)对特殊功能寄存器的访问
C51单片机的特殊功能寄存器(SFR),分散在片内RAM(80H~0FFH)区间,对它们的操作,只能用直接寻址方式。为了能够直接访问SFR,C51提供了一种自主形式的定义方法。特殊功能寄存器用大写表示。格式为
sfr SFR名=SFR地址;
例如:
sfr TMOD=0x89; //定时器方式寄存器TMOD的地址是89H sfr TL0=0x8A; //定时器TL0的地址是8AH
在C51中,对所有特殊功能寄存器的定义已放在一个头文件“REG51.H”中。因此,只要在程序的开始处加上#include <REG51.h>语句,即可在C51中按名称访问所有的特殊功能寄存器,无须用户再用sfr定义,即sfr用于定义没有命名的新特殊功能寄存器。
2)对于SFR的16位数据的访问
16位寄存器的高8位地址位于低8位地址之后,为了有效地访问这类寄存器,可使用如下格式定义
sfr16 16位SFR名 = 低8位SFR地址;
例如,
sfr16 DPTR=0x82;
DPTR由DPH,DPL两个8位寄存器组成,其中DPL的地址为82H,DPH的地址为83H。
3)对SFR中的某位进行访问
C51单片机的特殊功能寄存器中,地址为8的倍数的寄存器具有位寻址能力。C51通过特殊位(sbit)定义,可以实现对这些特殊功能寄存器位直接进行访问。可使用如下格式定义
sbit SFR位名 = SFR名^i ; (i=0~7)
例如:
sfr TCON=0x88; //定义TCON寄存器的地址为0x88 sbit TR0=TCON^4; //定义TR0位为TCON.4,地址为0x8c sbit TR1=TCON^6; //定义TR1位为TCON.6,地址为0x8e
有了以上定义,在随后的程序中就可以像访问位变量一样方便,例如:
TR0=1; //启动定时器0,相当于汇编中的指令SETB TR0 TR1=0; //停止定时器1,相当于汇编中的指令CLR TR0
同样,除了P0,P1,P2和P3口之外的所有特殊位在“REG51.h”都有定义,因此只要在程序的开始处加上#include <REG51.h>语句,即可在C51中按名称访问这些位,无须用户再用sbit定义。
4)可位寻址对象的定义
可位寻址对象指既可以字节寻址,又可位寻址的对象,位于片内RAM的20H~2FH中。一般先定义变量的数据类型,数据类型可以是字符型、整型、长整型等,其存储器类型必须定义为bdata,然后使用sbit定义该变量中可单独寻址访问的位。例如:
unsigned char bdata flag; sbit flag_0=flag^0;
5.C51并行接口及其定义
1)片内并行口的定义
C51单片机带有4个8位并行口,即SFR中的P0,P1,P2和P3口,对它的定义在“REG51.h”已存在,可直接对其引用,例如:
P2=0xfe; //将数据0xfe输出到P2口 key=P1; //从P1口输入数据到变量key
如果要单独对某位进行操作,可在程序的开头加上位寄存器定义,例如:
sbit P1_0=P1^0; //定义P1_0为P1口的第0位 sbit P1_1=P1^1; //同上 sbit P1_2=P1^2;
2)片外并行口的定义
对于C51单片机外扩的I/O接口,需要使用数据总线(P0口)、地址总线(P2口、P0口)和控制总线(,
,ALE)。根据硬件译码地址,将片外并行口视为片外数据存储器的一个单元,使用XBYTE[I/O接口地址]格式来表示。也可以另起一个替代名称,用#define 语句来定义。其格式为
#define I/O接口名称 XBYTE[I/O接口地址]
其中,XBYTE(大写)表示片外并行口绝对存储器访问的宏,方括号中[I/O接口地址]是存储器的绝对地址,XBYTE在文件“absacc.h”中定义。因此,在使用这种格式定义之前,应加上语句:#include <absacc.h>。例如:
#include <absacc.h> #define 8155PORTA XBYTE[0xffc0] /*将8155PORTA 定义为片外并行口,地址为0xffc0,长度为8位*/
注意:区分大小写和“;”。
3)绝对存储器访问宏
在“absacc.h”头文件中,除了XBYTE[I/O接口地址]外,还有以下几种宏定义:
●CBYTE 允许用户访问程序存储器(CODE区)中指定地址单元。例如:
ID=CBYTE[0X200];读取程序存储器地址为0x200单元的内容到变量ID
●XBYTE 允许用户访问外部数据存储器(XDATA区)中指定地址单元。例如:
XBYTE[0X100]=D;将变量D存入外部数据存储器地址为0x100的单元
●DBYTE 允许用户访问片内数据存储器(DATA区)中指定地址单元。例如:
DBYTE[0x20]=0;将片内20H单元的内容清零
【例2-2-1】 将片内RAM 30H单元开始的10个字节传送到片外数据存储器100H开始的区域。
#include<reg52.h> #include <absacc.h> main() { unsigned char n; for(n=0; n<10; n++) XBYTE[0x100+n]=DBYTE[0x30+n]; while(1); }
4)使用C51扩展关键字_at_
单片机中增加了_at_功能。使用_at_对指定的存储器空间的绝对地址进行访问,一般格式为
[存储器类型] 数据类型说明符 变量名 _at_ 地址常数;
其中,存储器类型为data、bdata、idata、pdata等C51能识别的数据类型,如省略则按存储模式规定的默认存储器类型确定变量的存储器区域;数据类型为C51支持的数据类型。地址常数用于指定变量的绝对地址,必须位于有效的存储器空间之内;使用_at_定义的变量必须为全局变量(在项目二任务3中将介绍全局变量的概念)。
【例2-2-2】 将片内RAM 40H单元的内容清零。
#include<reg52.h> #define uchar unsigned char /*定义符号uchar为数据类型符unsigned char*/ uchar data a _at_ 0x40; /*在data区中定义字节变量a,它的地址为40H*/ main() { a=0; while(1); }
6.C51运算符
C语言的运算符在表达式中,各运算量参与运算的先后顺序不仅要遵守运算符优先级别的规定,还要受运算符结合性的制约,以便确定是自左向右进行运算、还是自右向左进行运算。表2-2-7给出了运算符优先级和结合性的列表。
表2-2-7 运算符优先级和结合性
1)算术运算符及算术表达式
(1)基本的算术运算符。
●加法运算符“+”:双目运算符,如“a+b”,“4+8”等,具有左结合性。
●减法运算符“-”:双目运算符,但“-”也可作负值运算符,此时为单目运算,如“-x”,“-5”等,具有左结合性。
●乘法运算符“*”:双目运算符,具有左结合性。
●除法运算符“/”:双目运算符,具有左结合性。参与运算量均为整型时,结果也为整型,舍去小数。如果运算量中有一个是实型,则结果为双精度实型。
●求余运算符(模运算符)“%”:双目运算符,具有左结合性。要求参与运算的量均为整型。求余运算的结果等于两数相除后的余数,如“9%5”的余数结果为4。
(2)算术表达式和运算符的优先级与结合性。
●算术表达式:用算术运算符和括号将运算对象(也称为操作数)连接起来的、符合C语法规则的式子。如a+b,(a×2)/c,(x+r)×8-(a+b)/7,++i,sin(x)+sin(y),(++i)-(j++)+(k--)等,都是合法的算术表达式的例子。
●运算符的优先级:C语言中,运算符的运算优先级共分为15级。1级最高,15级最低。在表达式中,优先级较高的先于优先级较低的进行运算;而在一个运算量两侧的运算符优先级相同时,则按运算符的结合性所规定的结合方向处理。
●运算符的结合性:算术运算符采用左结合性(自左至右)。
(3)自增、自减运算符。
自增1,自减1运算符:自增1运算符记为“++”,其功能是使变量的值自增1。自减1运算符记为“--”,其功能是使变量值自减1。自增1,自减1运算符均为单目运算,都具有右结合性。可有以下几种形式:
●++i i自增1后再参与其他运算。
●--i i自减1后再参与其他运算。
●i++ i参与运算后,i的值再自增1。
●i-- i参与运算后,i的值再自减1。
【例2-2-3】 判断程序运行后的变量值。
main( ) { int i=5, j=5, p, q; p=(i++)+(i++)+(i++); q=(++j)+(++j)+(++j); }
这个程序中,对p=(i++)+(i++)+(i++)应理解为3个i相加,故p值为15;然后i再自增1三次相当于加3故i的最后值为8。而对于q 的值则不然,q=(++j)+(++j)+(++j)应理解为j先自增1,再参与运算,由于j自增1三次后值为8,3个8相加的和q为24,j的最后值仍为8。
2)关系运算符及表达式
在程序中经常需要比较两个量的大小关系,以决定程序下一步的工作。比较两个量的运算符称为关系运算符。
(1)关系运算符及其优先次序。
在C语言中有以下关系运算符:<、<=、>、>=、==、!=。
●关系运算符都是双目运算符,其结合性均为左结合。
●关系运算符的优先级低于算术运算符,高于赋值运算符。在6个关系运算符中,<、<=、>、>=的优先级相同,高于==和!=,==和!=的优先级相同。
●关系表达式的结果是“真”和“假”,分别用“1”和“0”表示。
【例2-2-4】 若a=4,b=3,c=1,判断下列表达式的值。
a>b 4>3为真,表达式值为1 b+c<a 3+1<4为假,表达式的值为0 a>b==c (a>b表达式值为1,与c相等)表达式值为1 d=a>b 表达式值为1 f=a>b>c 表达式值为0 (a=3)>(b=5) 由于3>5不成立,故其值为假,即为0 a==b==c+5 根据运算符的左结合性,先计算a==b,该式不成立,其值为0,再计算0==c+5, 也不成立,故表达式值为0
(2)关系表达式。
关系表达式的一般形式为
表达式 关系运算符 表达式
例如:
a+b>c-d 等同于(a+b)>(c-d) x>3/2 ‘a’+1<c -i-5*j==k+1
都是合法的关系表达式。由于表达式也可以是关系表达式,因此,也允许出现嵌套的情况。
3)逻辑运算符和表达式
(1)逻辑运算符及其优先次序。
逻辑表达式的一般形式为
表达式 逻辑运算符 表达式
C语言中提供了三种逻辑运算符:&&(与运算)、||(或运算)、!(非运算)。
与运算符&&、或运算符||均为双目运算符,具有左结合性。非运算符!为单目运算符,具有右结合性。逻辑运算符和其他运算符优先级的关系如表2-2-7所示。
按照如表2-2-7所示运算符的优先顺序可以得出,例如:
a>b && c>d 等同于 (a>b)&&(c>d) !b==c||d<a 等同于 ((!b)==c)||(d<a) a+b>c&&x+y<b 等同于 ((a+b)>c)&&((x+y)<b) (a&&b)&&c 等同于 a&&b&&c
(2)逻辑运算的值。
逻辑运算的值也为“真”和“假”两种,分别用“1”和“0”来表示。其求值遵循的规则如下:
●与运算&&:参与运算的两个量都为真时,结果才为真,否则为假。例如:
5>0 && 4>2 由于5>0为真,4>2也为真,相与的结果也为真。 5&&3 由于5和3均为非“0”值为“真”,即为1,相与的结果也为真。
●或运算||:参与运算的两个量只要有一个为真,结果就为真。两个量都为假时,结果为假。例如:
5>0||5>8 由于5>0为真,相或的结果也就为真。
●非运算!:参与运算量为真时,结果为假;参与运算量为假时,结果为真。例如:
!(5>0) 结果为假。
●在由多个逻辑运算符构成的逻辑表达式中,并不是所有逻辑运算符都被执行,只是在必须执行下一个逻辑运算符后才能求出表达式的值时,才执行该运算符。例如a=1,b=2,c=3,d=4,m=1,n=1。
则表达式为
(m=a>b)&&(n=c>d)
因为a>b为假,m=0,故不运算(n=c>d),表达式为假(0)。
(m=a>b)||(n=c>d)
因为a>b为假,m=0,需运算(n=c>d)为假(0),故表达式为假(0)。
4)位操作及其表达式
单片机C51语言也能像汇编语言一样对硬件对象进行按位操作。位运算符的作用是按位对变量进行运算,但是并不改变参与运算的变量的值。如果要求按位改变变量的值,则要利用相应的赋值运算。位运算符只能是整型或字符数,不能用来对浮点型数据进行操作的。单片机C51语言中共有6种位运算符。位运算一般的表达式为
变量1 位运算符 变量2
位运算符也有优先级,从高到低依次是“~”(按位取反)→“<<”(左移)→“>>”(右移)→“&”(按位与)→“^”(按位异或)→“|”(按位或)。
(1)位取反运算符“~”(按位取反)。若a=0xF0,则表达式:a=~a=0x0F
(2)位左移“<<”和位右移运算符“>>”。
●位左移运算符“<<”用来将一个数的各二进制位的全部左移或移动若干位;移位后,空白位补0,而移出的位舍弃。
●位右移运算符“>>”用来将一个数的各二进制位的全部右移或移动若干位;移位后,无符号数空白位补0,而移出的位舍弃。有符号位空白位补符号位,而移出的位舍弃。例如,
若a=E2H=1110 0010B,则表达式为
a=a<<2 a值左移2位,其结果为a=1000 1000B=88H a=a>>2 a值右移2位,a若为无符号数,其结果为a=0011 1000B=38H a值右移2位,a若为有符号数,其结果为a=1111 1000B=F8H
(3)按位与运算符“&”。
如若a=F0H=1111 0000B,b=3BH=0011 1011B,则表达式:c=a&b的值为c=30H
(4)按位异或运算符“^”。
如若a=F0H=1111 0000B,b=3BH=0011 1011B,则表达式:c=a^b的值为c=C4H
(5)按位或运算符“|”。
若a=F0H=1111 0000B,b=3BH=0011 1011B,则表达式:c=a|b的值为c=FBH
【 例2-2-5】 参考任务1,完成8路LED右移两位的流水灯程序设计。
#include <reg52.h> //预处理命令,装载C51单片机头文件 delay(unsigned int t) //定义延时函数 { unsigned int i,j; //定义无符号整形变量i和j for(i=0; i<t; i++) for(j=0; j<t; j++); } main( ) //主函数 { unsigned char LED, i; while(1) //无限循环 { LED=0x3f; //初始状态只点亮LED7、LED6 for(i=0; i<4; i++) //4次循环 { P1=LED; delay(200); //调用延时函数 LED=(LED>>2)|0xc0; //变量循环移位合并 } } }
5)复合赋值运算符
复合赋值运算符就是在赋值运算符“=”的前面加上其他运算符。构成复合赋值表达式的一般形式为
变量 双目运算符=表达式 它等同于 变量=变量 运算符 表达式
以下是C语言中的复合赋值运算符:+=、-=、*=、/=、%=、<<=、>>=、&=、^=、|=。
凡是双目运算都能用复合赋值运算符去简化表达。例如:
a+=56 等同于 a=a+56 a-=56 等同于 a=a-56 y*=x 等同于 y=y*x y/=x+9 等同于 y=y/(x+9)
很明显采用复合赋值运算符会降低程序的可读性,但这样却能使程序代码简单化,并能提高编译的效率。对于开始学习C语言的朋友在编程时最好还是根据自己的理解力和习惯去使用程序表达的方式,不要一味追求程序代码的短小。
6)逗号运算符和逗号表达式
在C语言中逗号“,”也是一种运算符,称为逗号运算符。其功能是把两个表达式连接起来组成一个表达式,称为逗号表达式。其一般形式为
表达式1,表达式2
其求值过程是分别求两个表达式的值,并以表达式2(最后一个)的值作为整个逗号表达式的值。
【例2-2-6】 阅读并分析下列程序。
main() { int a=2, b=4, c=6, x, y; y=(x=a+b),(b+c); //x= a+b =6,y=(b+c)=10 }
本例中,y等于整个逗号表达式的值,也就是表达式2的值,x是第一个表达式的值。对于逗号表达式还要说明如下:
●逗号表达式一般形式中的表达式1和表达式2中又是一个逗号表达式。例如:
表达式1,(表达式2,表达式3)
形成了嵌套情形。因此,可以把逗号表达式扩展为以下形式:
表达式1,表达式2,…,表达式n
整个逗号表达式的值等于表达式n的值。
●程序中使用逗号表达式,通常是要分别求逗号表达式内各表达式的值,并不一定要求整个逗号表达式的值。
●并不是在所有出现逗号的地方都组成逗号表达式,如在变量说明中,函数参数表中逗号只是用做各变量之间的间隔符。
7)条件运算符(三目运算符)
上面说过单片机C语言中有一个三目运算符,它就是“? : ”条件运算符,它要求有三个运算对象。它能把3个表达式连接构成一个条件表达式。条件表达式的一般形式为
逻辑表达式? 表达式 1 :表达式 2
当逻辑表达式的值为真时(非0值)时,整个表达式的值为表达式1的值;
当逻辑表达式的值为假(值为0)时,整个表达式的值为表达式2的值。
如有两个变量a和b,要求取a和b两数中的较小的值放入min变量中,语句为
min=a<b?a:b;
8)if条件语句
在C语言中,选择结构程序设计一般用if语句或switch语句来实现。if语句有3种基本形式:if,if-else和if-else-if。
(1)if语句的3种形式。
A.第一种形式为if基本形式。
if(表达式) {语句组;}
其语义是如果表达式的值为真,则执行其后的语句组;表达式的值为假,不执行该语句组。其过程可表示如图2-2-5所示。如果语句是简单语句,可以不使用“{ }”。
【例2-2-7】 取出P1、P2口的大数送P3口。
#include<reg52.h> void main( ) { unsigned char a, b, max; a=P1; b=P2; max=a; //设定a为最大数 if (max<b) max=b; //a<b,大数送max P3=max; while(1); }
B.第二种形式为if-else形式。
if(表达式) { 语句组1;} else { 语句组2;}
其语义是如果表达式的值为真,则执行语句组1,否则执行语句组2。语句使用场合:需要两个并列分支。其执行过程可表示为如图2-2-6所示。

图2-2-5 if语句执行流程

图2-2-6 if-else语句执行流程
【例2-2-8】 采用第二种方式取出P1、P2口的大数送P3口。
#include<reg52.h> void main( ) { unsigned char a, b; a=P1; b=P2; if (a>b) P3=a; else P3=b; while(1); }
C.第三种形式为if-else-if形式。
前两种形式的if语句一般都用于两个分支的情况。当有多个并列分支选择时,可采用if-else-if语句,其一般形式为
if(表达式1) 语句1; else if(表达式2) 语句2; else if(表达式3) 语句3; … else if(表达式m) 语句m; else 语句n;
其语义是依次判断表达式的值,当出现某个值为真时,则执行其对应的语句,然后跳到整个if语句之外继续执行程序。如果所有的表达式均为假,则执行语句n,然后继续执行后续程序。if-else-if语句的执行过程如图2-2-7所示。

图2-2-7 if-else-if语句执行流程
【例2-2-9】 比较片内40H、41H两个单元数的大小关系。大数送P1口,小数送P2口。相等清P1口和P2口。
#include<reg52.h> #include <absacc.h> main() { unsigned char m, n; m=DBYTE[0x40]; n=DBYTE[0x41]; if(m>n) { P1=m; P2=n; } else if(m==n) P1=P2=0; else { P1=n; P2=m;} while(1); }
注意:在使用if语句中还应注意以下问题:
·在if语句中,条件判断表达式必须用括号括起来,在语句之后必须加分号。
·在if语句的三种形式中,所有的语句应为单个语句,如果要想在满足条件时执行一组(多个)语句,则必须把这一组语句用“{}”括起来组成一个复合语句。但要注意的是在“}”之后不能再加分号。例如:
if(a>b) { a++; b++;} else { a=0; b=10;}
(2)if语句的嵌套。
当if语句中的执行语句又是if语句时,则构成了if 语句嵌套的情形。在嵌套内的if语句可能又是if-else型的,这将会出现多个if和多个else重叠的情况,这时要特别注意if和else的配对问题。
C语言规定:else总是与它前面最近的if配对。
【例2-2-10】 硬件电路如图2-2-8所示,AT89C51单片机的P1.0~P1.3接4只LED(LED1~LED4),P1.4~P1.7接了4个开关K1~K4,编程将开关的状态反映到LED上(开关闭合,对应的灯亮,开关断开,对应的灯灭)。

图2-2-8 例2-2-8硬件电路
#include<reg52.h> void main(void) { unsigned char a; P1=0xf0; while(1) { a=P1; a=a&0xf0; a>>=4; P1=a|0xf0; } }
五、实践练习
(1)P1口控制8只LED(LED1~LED8),使8只指示灯从左右两边逐一向中间轮流点亮,再由中央向两边逐一点亮,设晶振频率为12MHz。
(2)在题(1)基础上,实现间隔一只灯亮并左右摇摆。