单片机控制技术应用项目化教程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.3 相关知识

1.3.1 单片机简介

计算机系统正在向巨型化、单片化、网络化方向发展,其中单片化是为了提高系统的可靠性、实现微型化,把计算机系统集成在一块半导体芯片上。这种单片计算机简称单片机。单片机的内部硬件结构和指令系统是针对自动控制应用而设计的,所以单片机又称为微控制器(Micro Controller Unit,MCU)。单片机自从20世纪70年代问世以来,已经形成了多品种、多系列。

1.MCS-51单片机及兼容产品

尽管各类单片机很多,但无论是从世界范围或是从国内范围来看,使用最为广泛的应属MCS-51单片机,所以,本书以MCS-51系列八位单片机(8031、8051、8751等)为学习对象,介绍单片机的应用技术。

MCS单片机系列共有十几种芯片,如表1-1所示。

表1-1 MCS系列单片机分类  

MCS单片机分为51和52两个子系列,并以芯片型号的最末位数字作为标志。其中51子系列是基本型,而52子系列则属增强型。

Intel公司推出了8位的MCS-51系列单片机后,在工业控制方面得到了极大的应用。之后,Intel开放了51系列单片机核心技术,PHilips、Atmel、ADI等公司相继推出了基于51内核的单片机。

2.其他类型的单片机产品

在一些公司生产基于51内核单片机的同时,其他一些大公司也开发了自己的单片机,比如Motorola、TI、MicrocHip、ST、Epson、MPS430等。这些单片机的指令系统和内部结构和MCS-51单片机结构不同、功能也各有特点。

3.单片机处理器的应用范围

单片机各个方面性能正在不断提高,它不仅用于通信、网络、金融、交通、医疗、消费电子、仪器仪表、制造业控制等领域,而且还应用在航天、航空、军事装备等领域。

1.3.2 数制与编码

1.数制

1)十进制

十进制以10为基数,共有0~9十个数码,计数规律为低位向高位逢十进一。各数码在不同位的权不一样,故值也不同。例如444,三个数码虽然都是4,但百位的4表示400,即4×102;十位的4表示40,即4×101;个位的4表示4,即4×100;其中102、101、100称为十进制数各位的权。如一个十进制数586.5,按每一位数展开可表示为:

(586.5)10=5×102+8×101+5×100+5×10-1

2)二进制

计算机中经常采用二进制。二进制的基数为2,共有0和1两个数码,计数规律为低位向高位逢二进一。各数码在不同位的权不一样,故值也不同。二进制数用下标B或“2”表示,如一个二进制数101.101,按每一位数展开可表示为:

(101.101)2=1×22+0×21+1×20+l×2-1+0×2-2+1×2-3

3)八进制数

在八进制数中,基数为8。因此,在八进制数中出现的数字字符有8个:0,1,2,3,4,5,6,7。每一位计数的原则为“逢八进一”,用下标O或“8”表示。

4)十六进制数

在十六进制数中,基数为16。因此,在十六进制数中出现的数字字符有16个:0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,其中A、B、C、D、E、F分别表示值10,11,12,13,14,15。十六进制数中每一位计数原则为“逢十六进一”,用下标H表示

2.各数制之间的转换

1)R(R表示任何数制的基数)进制数转换为十进制数

二进制、八进制和十六进制数转换为等值的十进制数,采用按权相加法。用多项式表示,并在十进制下进行计算,所得的结果就是十进制数。

例如,将二进制数1011101转换为十进制数。

(1011101)2=(1×26+0×25+1×24+1×23+1×22+0×21+1×2010

=(64+0+16+8+4+0+1)10

=(93)10

2)十进制数转换为R进制数

十进制数转换为等值的二进制、八进制和十六进制数,需要对整数部分和小数部分分别进行转换。其中,整数部分用连续除以基数R取余数倒排法来完成,小数部分用连续乘以基数R取整顺排法来实现。

例如,将十进制数48.375转换成二进制数(取小数点后三位)。

根据转换规则,整数部分44用除2取余倒排法:

        (44)10 = (101100)2

小数部分0.375采用乘2取整顺排法:

(0.375)10 = (0.011)2

所以:(48.375)10=(101100.011)2

3)二进制数与八进制数、十六进制数的转换

二进制数与八进制数的转换,应以“3位二进制数对应1位八进制数”的原则进行;二进制数与十六进制数的转换,应以“4位二进制数对应1位十六进制数”的原则进行。

例如,(101100)2转换成十六进制数:

            (101100)2=(2C)H

4)二进制数的运算原则

加法:逢二进一;减法:借一当二;乘法:与算术乘法形式相同;除法:与算术除法形式相同。

3.数据类型及数据单位

1)数据的两种类型

计算机中的数据可概括分为两大类:数值型数据和字符型数据,所有的非数值型数据都要经过数字化后,才能在计算机中存储和处理。

2)数据单位

在计算机中通常使用三个数据单位:位、字节和字。位的概念是:最小的存储单位,英文名称是bit,常用小写b或bit表示。用8位二进制数作为表示字符和数字的基本单元。

英文名称是Byte,称为一字节。通常用大写“B”表示。

1B(字节)=8bit(位)

1KB(千字节)=1024B(字节)

1MB(兆字节)=1024KB(千字节)

字长:字长也称为字或计算机字,它是计算机能并行处理的二进制数的位数。

4.编码

1)8421BCD码

用4位二进制数码表示1位十进制数,简称二-十进制码,又叫BCD码。其中8421 BCD码是最常用的BCD码,它和四位自然二进制码相似,各位的权值为8、4、2、1。和四位自然二进制码不同的是:它只选用了四位二进制码中前10组代码,即用0000~1001分别代表它所对应的十进制数0~9,余下的六组代码不用。如表1-2所示。

表1-2 8421BCD码表  

2)ASCⅡ 码

ASCⅡ码使用指定的7位或8位二进制数组合,来表示128或256种可能的字符。标准ASCII码也叫基础ASCⅡ码,使用7位二进制数来表示所有的大写和小写字母,数字0到9、标点符号,以及在美式英语中使用的特殊控制字符。

(1)0~31及127(共33个)是控制字符或通信专用字符(其余为可显示字符),如控制符:LF(换行)、CR(回车)、FF(换页)、DEL(删除)、BS(退格)、BEL(响铃)等;通信专用字符:SOH(文头)、EOT(文尾)、ACK(确认)等;ASCⅡ值为8、9、10和13分别转换为退格、制表、换行和回车字符。它们并没有特定的图形显示,但会依不同的应用程序而对文本显示有不同的影响。

(2)32~126(共95个)是字符(32sp是空格),其中48~57为0到9十个阿拉伯数字。

(3)65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等。

在标准ASCII中,其最高位(b7)用作奇偶校验位。后128个称为扩展ASCII码,目前许多基于x86的系统都支持使用扩展(或“高”)ASCII。扩展ASCII码允许将每个字符的第8位,用于确定附加的128个特殊符号字符、外来语字母和图形符号。

1.3.3 MCS-51单片机

1.MCS-51单片机的内部结构

1)MCS-51单片机基本组成

MCS-51单片机有很多类型,但它们基本相同。下面以AT89C51为例,介绍单片机的内部结构。AT89C51是Atmel公司推出的带有ISP(在线编程)功能的8位单片机,是目前许多领域应用的首选机型。该单片机的主要功能如下:

•完全兼容51系列;

•工作频率0~22MHz;

•4KB Flash ROM,并且可在线编程;

•128B RAM; 

•32 个I/O;

•5个中断向量源;

•2个16位定时/计数器;

•1个全多工串行通信端口;

•看门狗定时器;

•双数据指针;

•片内时钟振荡器;

•具有多种封装方式。

AT89C51内部结构框图如图1-1所示。

图1-1 AT89C51内部结构框图

2)中央处理器

中央处理器是单片机内部的核心部件,是一个8位的中央处理单元,主要由运算器、控制器和若干寄存器组成,通过内部总线与其他功能部件连接。

(1)运算器。运算器用来完成算术运算和逻辑运算功能,它是AT89C51内部处理各种信息的主要部件。运算器主要由算术逻辑运算单元ALU、累加器ACC、暂存器、寄存器B和程序状态字(标志寄存器PSW)组成。

①算术逻辑单元ALU是一个8位的运算器,它可以完成算术运算与逻辑运算,具有数据传送、移位、判断与程序转移等功能。它还有一个位运算器,可以对1位二进制数进行值位、清零、求反、判断、移位等逻辑运算。

②累加器ACC简称为A,是一个8位的寄存器,用来存放操作数或运算的结果。在MCS-51指令系统中,绝大多数指令都要求累加器A参与处理。

③暂存器存放参与运算的操作数,不对外开放。

④寄存器B是专为乘法和除法设置的寄存器,也是8位寄存器。乘法运算时,B是存放乘数。乘法操作后,乘积的高8位存于B中;除法运算时,B是存放除数,除法操作后,余数存于B中。此外,B寄存器也可作为一般数据寄存器使用。

⑤程序状态字(PSW——Program Status Word),程序状态字是一个8位标志寄存器,用于保存程序运行中的各种状态信息。其中有些位状态是根据程序执行结果,由硬件自动设置的,有些位状态则使用软件方法设定。PSW的位状态可以用专门指令进行测试,也可以用指令读出。一些条件转移指令将根据PSW有些位的状态,进行程序转移。PSW的PSW0.0-PSW0.7的位地址为D0H-D7H,各位定义如表1-3所示。

表1-3 PSW的PSW.0-PSW.7的含义  

注:PSW.1位保留未用。

CY(PSW.7)——进位标志位。表示累加器A在加减运算过程中,其最高位A7有无进位或借位。如果操作结果的最高位产生进位或借位,CY由硬件置“1”,否则清零。另外,也可以由位运算指令置位或清零。

AC(PSW.6)——辅助进位标志位。在进行加减运算中,当有低4位向高4位进位(或借位)时,AC由硬件置“1”,否则AC位被清“0”。

F0(PSW.5)——用户标志位。这是一个供用户定义的标志位,根据需要可以用软件来使它置位或清除。

RS1和RS0(PSW.4,PSW.3)——寄存器组选择位。AT89C51片内共有四组工作寄存器,每组八个,分别命名为R0~R7。编程时用于存放数据或地址,但每组工作寄存器在内部RAM中的物理地址不同。RS1和RS0的四种状态组合,就是用于选择CPU当前使用的工作寄存器组,从而确定R0~R7的实际物理地址。RS1、RS0状态与工作寄存器R0~R7的物理地址关系如表1-4所示。

表1-4 RS1、RS0与R0~R7的物理地址关系  

这两个选择位由软件设置,被选中的寄存器组即为当前通用寄存器组。单片机通电或复位后,RS1 RS0=00。

OV(PSW.2)——溢出标志位。当执行算术指令时,由硬件自动置位或清零,表示累加器A的溢出状态。在带符号数运算结果超过范围(-128~+127),无符号数运算结果超过范围(255),乘法运算中积超过255,除法运算中除数为0,在这4种情况下该位为“1”。

判断该位时,通常用运算中次高位向最高位的进(借)位C6和最高位向前的进(借)位C7(也就是CY)的异或来表示OV,即OV=C6⊕C7。

P(PSW.0)——奇偶标志位。表明累加器A内容的奇偶性,如果A中有奇数个“1”,则P置“1”;若1的个数为偶数,则P为“0”。凡是改变累加器A中内容的指令均会影响P标志位。

例如,执行下列两条指令:

            MOV A,#67H ;将立即数送入累加器A中,

            ADD A,#58H        ;将A的值与立即数58H相加,结果存入A中。

            实现67H与58H相加。

67H=01100111B、58H=01011000B,加法过程为:

执行后,A=0BFH,硬件标志位自动设置为:CY=0、AC=0、OV=C6⊕C7,P=1,如无关位为0,则PSW=05H。

(2)控制器。控制器是单片机内部按一定时序协调工作的控制核心,是分析和执行指令的部件。控制器主要由程序计数器PC、指令寄存器、指令译码器和定时控制逻辑电路等构成。

①程序计数器PC是专门用于存放将要执行的下一条指令的16位地址,可寻址64KB范围的ROM。CPU根据PC中的地址到ROM中去读取程序指令码和数据,并送给指令寄存器进行分析。PC的内容具有自动加1的功能,用户无法对其进行读写,只能用指令改变PC的值,可实现程序的跳转等特点。

②指令寄存器用于存放CPU从ROM读出的指令操作码。

③指令译码器是用于分析指令操作的部件,指令操作码经译码后产生相应的信号。

④定时控制逻辑电路用来产生脉冲序列和多节拍脉冲。

(3)寄存器。寄存器是单片机内部的临时存放单元或固定用途单元,包括通用寄存器组和专用寄存器组。用寄存器组用于存放运算过程中的地址和数据,专用寄存器用于存放特定的操作数,指示当前指令的存放地址和指令运行的状态等,51单片机共有4组32个通用寄存器、21个专用寄存器。对于特殊功能寄存器,前面介绍了累加器A、寄存器B和标志寄存器PSW,下面介绍数据指针(DPTR)和堆栈指针(SP:Stack Pointer),其余的在后面项目中介绍。

数据指针(地址)寄存器DPTR为16位寄存器,寻址范围达64KB。它既可以按16位寄存器使用,也可以按寄存器DPH(高8位)DPL(低8位)作为两个寄存器使用。DPTR专门用作寄存片外RAM及扩展I/O口进行数据存取时的地址。

堆栈是一个特殊的存储区,用来暂存数据和地址,它只有一个数据进/出端口,按“先进后出”的原则存取数据。堆栈的底部叫栈底,数据的进出口叫栈顶,栈顶的地址叫堆栈指针,用8位寄存器SP来存放,系统复位后SP的内容为07H,但是一般把堆栈开辟在内部RAM的30H~7FH单元中,空栈时栈底的地址等于栈顶的地址。

数据进入堆栈的操作叫进栈,首先SP的内容加1送入SP,然后再向堆栈存储器写入数据。

数据读出堆栈的操作叫出栈,堆栈存储器读出数据,然后SP的内容减1送入SP。

3)存储器结构

MCS-51单片机的芯片内部有RAM和ROM存储器,外部可以扩展RAM和ROM,在物理上分为4个空间。逻辑上分为程序存储器(内、外统一编址,使用MOVC指令访问)、内部数据存储器(使用MOV指令访问)和外部数据存储器(使用MOVX指令访问)。

(1)内部数据存储器RAM。对于普通8051单片机,内部RAM有256B,用于存放程序执行过程的各种变量及临时数据。低128B可用直接寻址或间接寻址方式进行访问,分为工作寄存器区、位寻址区、用户区和堆栈区4个区域,高128B为特殊功能寄存器区,其片内RAM的配置如图1-2所示。

图1-2 片内RAM的配置图

①工作寄存器区。00H~1FH地址单元,共有四组寄存器,每组8个寄存单元(均为8位),都以R0~R7作为寄存单元编号。寄存器常用于存放操作数及中间结果等,在任一时刻,CPU只能使用其中的一组寄存器,由程序状态字寄存器PSW中RS1、RS0位的状态组合来选择,正在使用的那组寄存器称之为当前寄存器组。

②位寻址区。20H~2FH单元为位寻址区,既可作为一般RAM单元进行字节操作,也可以对单元中每一位进行位操作。位寻址区共有16个RAM单元,计128位,位地址为00H~7FH,如表1-5所示。

表1-5 位寻址区  

③用户区堆栈区。在内部RAM低128单元中,剩下80个单元,地址从30H~7FH,为供用户使用的RAM区,对用户RAM区的使用没有任何规定或限制,在一般应用中常把堆栈开辟在此区中。

④特殊功能寄存器区。80H~FFH(高128B)集合了表1-6所示的一些特殊用途的寄存器,专门用于控制、管理片内算术逻辑部件、并行I/O口、串行I/O口、定时计数器、中断系统等功能模块的工作。

表1-6 特殊用途的寄存器  

对专用寄存器只能使用直接寻址方式,书写时既可使用寄存器符号,也可使用寄存器单元地址。表1-6中字节地址不带括号的寄存器为可以位寻址的寄存器,带括号的是不可以位寻址的寄存器。

尽管还余有许多空闲地址,但用户不能使用。程序计数器PC不占据RAM单元,它在物理上是独立的,因此,是不可寻址的寄存器。

(2)外部数据存储器。MCS-51单片机片内有128字节或256字节的数据存储器,当这些数据存储器容量不够时,可进行外部扩展,外部数据存储器最多可扩展到64KB,地址范围为0000H~0FFFFH,通过DPTR用作数据指针间接寻址方式访问;对于低地址端的256B,地址范围为00H~0FFH,可通过R0或R1间接寻址方式访问。

(3)程序存储器。MCS-51的程序存储器用于存放编好的程序和表格、常数。8051片内有4KB的ROM,8751片内有4KB的EPROM,8031片内无程序存储器。MCS-51能扩展64KB程序存储器,片内外的ROM是统一编址的。如图1-3所示,/EA端接Vcc(+5V),8051的程序计数器PC在0000H~0FFFH地址范围内(即前4KB地址)是执行片内ROM中的程序,当PC在1000H~FFFFH地址范围时,自动执行片外程序存储器中的程序;如果/EA接地,则CPU直接从外部存储器取指令,这时,从/PSEN引脚输出负脉冲,用作外部程序存储器的读选通信号。

图1-3 程序存储器选择图

MCS-51的程序存储器中有些单元具有特殊功能,如:0000H~0002H,系统复位后,(PC)=0000H,单片机从0000H单元开始取指令执行程序。如果程序不从0000H单元开始,应在这三个单元中存放一条无条件转移指令,以便直接转去执行指定的程序;0003H~002AH,共40个单元,这40个单元分为五段,即五个中断源的中断地址区:

0003H~000AH 外部中断0中断地址区;

000BH~0012H 定时器/计数器0中断地址区;

0013H~001AH 外部中断1中断地址区;

001BH~0022H 定时器/计数器1中断地址区;

0023H~002AH 串行中断地址区。

中断响应后,按中断源自动转到各中断区的首地址(即中断入口地址)去执行程序。在中断地址区中存放中断服务程序。一般情况下,8个单元难以存放一个完整的中断服务程序,可以在中断地址区的首地址存放一条无条件转移指令,中断响应后,通过中断地址区的入口地址,转到中断服务程序的实际入口地址。

4)并行输入输出接口

MCS-51系列单片机有4个8位的并行I/O接口:P0、P1、P2和P3口。无外部扩展时,4个并行接口用作通用I/O口,既可以作为输入,也可以作为输出;既可按字节处理,也可按位方式使用,输出时具有锁存能力,输入时具有缓冲功能。有外部扩展时,并行I/O接口用作系统总线。

(1)P0口。P0口有八条端口线,从低位到高位分别为P0.0~P0.7。P0口每一条线由一个数据输出锁存器、两个三态数据输入缓冲器、输出驱动电路和控制电路组成。位结构如图1-4所示,P0口的输出驱动电路由T1和T2形成推挽式结构,带负载能力将大大提高,并且具有驱动8个LSTTL负载的能力,输出电流不大于800μA。输出级是漏极开路,必须外接8.7kΩ~10kΩ的上拉电阻到电源。P0口作为通用I/O口时,属于准双向口。

图1-4 P0口的位结构图

当系统进行存储器扩展时,控制信号C为高电平“1”,P0口用作地址(低8位)/数据分时复用总线。此时P0口是一个真正的双向口。

(2)P1口。P1口有八条端口线,从低位到高位分别为P1.0~P1.7,每条线由一个输出锁存器、两个三态输入缓冲器和输出驱动电路组成,位结构如图1-5所示。P1口是准双向口,只能用作通用I/O接口。输出高电平时,能向外提供拉电流负载,不需外接上拉电阻。当P1口用作输入时,须先向端口锁存器写入1。P1口具有驱动4个LSTTL负载的能力。

图1-5 P1口的一位结构图

(3)P2口。P2口有八条端口线,从低位到高位分别为P2.0~P2.7。P2口也是准双向口,它有两种用途:通用I/O接口和高8位地址线。它不需外接上拉电阻,位结构如图1-6所示。

图1-6 P2口的位结构图

当外扩展存储器时,P2口用作地址线的高8位;不扩展存储器时,P2口用作通用I/O口,带负载能力与P1口相同。

(4)P3口。P3口有八条端口线,从低位到高位分别为P3.0~P3.7。其位结构如图1-7所示,是一个多用途的准双向口。第一功能是作通用I/O接口使用,负载能力与P1,P2相同;第二功能是作控制和特殊功能口使用,这时八条端口线所定义的功能各不相同,如表1-7所示。

图1-7 P3口的位结构图

表1-7 P3口第二功能表  

2.MCS-51单片机的引脚

MCS-51单片机为40引脚的集成芯片,其双列直插封装(DIP)形式引脚排列如图1-8所示。

图1-8 AT89C51单片机引脚图

1)I/O口引脚

AT89C51有4个8位并行I/O接口,共32条I/O线:

①P0口8条I/O线:P0.0~P0.7(39~32脚);

②P1口8条I/O线:P1.0~P1.7(1~8脚);

③P2口8条I/O线:P2.0~P2.7(21~28脚);

④P3口的8条I/O线:P3.0~P3.7(10~17脚)。

P1、P2、P3内置上拉电阻,P0口需外接10kΩ左右的上拉电阻。P0~P3口作输入口时,必须先写入“1”。

2)控制信号引脚

①ALE(/PROG)(30脚):地址锁存允许输出信号。在系统存储器扩展时,ALE用于控制锁存器锁存P0口输出的低8位地址;ALE高电平期间,P0输出地址信息;ALE下降沿到来时,P0口的地址信息被外接锁存器锁存,接着出现指令和地址信息,以实现低8位地址和数据的隔离。CPU不执行访问外部存储器时,ALE以时钟频率六分之一为固定频率输出的正脉冲,可作为外部时钟或外部定时脉冲使用。此引脚的第二功能是对单片机内部EPPROM编程时的编程脉冲输入线。

②/PSEN(29脚):外部程序存储器读选通信号输出。在读外部ROM时,/PSEN有效(低电平),以实现外部ROM单元的读操作。

③/EA(Vpp)(31脚):访问程序存储控制信号。当/EA信号为低电平时,对ROM的读操作限定在外部程序存储器;而当/EA信号为高电平时,则对ROM的读操作是从内部程序存储器开始,并可延至外部程序存储器。其第二功能还可作为编程电源线。

④RST(9脚):复位信号输入端,用以完成单片机的复位操作。当单片机振荡器工作时,连续输入2个机器周期以上的高电平,单片机将恢复到初始状态。

3)外接晶振引脚

XTAL1(18脚)和XTAL2(19脚):外接晶振引线端。当使用芯片内部时钟时,用于外接石英晶体和微调电容;当使用外部时钟时,用于接外部时钟脉冲信号。

4)电源引脚

①GND:电源接地线。

②VCC:电源+5V。

③(RST/VPD)(9脚):备用电源

各种型号的芯片,其引脚的第一功能信号是相同的,所不同的只在引脚的第二功能信号。

3.片外总线结构

当MCS-51单片机在进行外部扩展时,单片机的引脚线构成了地址总线(AB)、数据总线(DB)、控制总线(CB)的三总线结构。典型应用如图1-9所示。P0、P2构成地址总线,对外部存储器寻址;P0时分复用作为数据总线;P3口的/PSEN、/WR、/RD、ALE等作为控制总线。

图1-9 片外扩充时单片机的总线结构

4.MCS-51的CPU时钟系统

1)时钟电路

为保证单片机内部各部件之间协调工作,其控制信号必须在统一的时钟信号下按一定时间顺序发出,这些控制信号在时间上的关系就是CPU的时序。产生统一的时钟信号的电路就是时钟电路。AT89C51单片机在内部反相放大器的输入端XTAL1(18脚)和输出端XTAL2(19脚)外接石英晶体(频率在1~24MHz)和微调电容(20pF左右),构成内部振荡器作为时钟电路,如图1-10所示。也使用外部振荡器向内部时钟电路输入固定频率的时钟信号,如图1-11所示,图中上拉电阻为5.1kΩ。

图1-10 内部时钟电路

图1-11 外部时钟电路

2)振荡周期

振荡周期是时钟电路(片内或片外振荡器)所产生的振荡脉冲的周期,即单片机提供的时钟信号的周期。设时钟源信号的频率为fosc,则振荡周期为1/fosc。通常在分析单片机时序时,也定义为节拍(用P表示)。

3)时钟周期

振荡脉冲经过二分频后,就是单片机的时钟信号的周期,称为时钟周期,又称为状态周期。一个状态周期包含2(P1、P2)个节拍来完成不同的逻辑操作。

4)机器周期

机器周期是单片机的基本操作(如取指令等)周期,通常记作TCY。一个机器周期由六个(S1~S6)状态周期组成,因此,一个机器周期共有12个节拍即12个振荡周期。可用下面关系式表示:

TCY=12个振荡周期=12/fosc

5)指令周期

执行一条指令所需要的时间称之为指令周期。MCS-51单片机通常可以分为单周期指令、双周期指令和四周期指令三种。机器周期数越少,指令执行速度越快。

如时钟频率为12MHz时,振荡周期为1/12μs、时钟周期为1/6μs、机器周期为1μs、指令周期为1~4μs。

5.工作方式

1)复位方式

单片机的复位是使CPU和系统中的其他功能部件都处在初始状态,并从初始状态开始工作。MCS-51单片机的复位是外部电路来执行的,在RST引脚(9脚)加上持续2个机器周期(即24个振荡周期)

以上的高电平就执行状态复位。常见的复位方式由通电复位和按键复位两种,复位电路如图1-12(a)、(b)所示。

图1-12 单片机常见的复位电路

单片机复位期间不产ALE和/PSEN信号,即ALE和/PSEN为高电平,单片机复位期间没有取指操作。复位后,内部各专用寄存器状态如表1-8所示。P0~P3口的值为FFH,为输入口做好准备。程序从0000H开始执行,堆栈底部在07H,一般需重新设置SP值。

表1-8 专用寄存器复位后状态  

注:*表示无关位

2)程序执行方式

程序执行方式是单片机的基本工作方式,系统复位后,PC的内容为0000H,程序总是从ROM的0000H地址单元开始取指令,然后根据指令的操作要求执行下去。

3)低功耗操作方式

CMOS型单片机有两种低功耗操作方式:节电方式和掉电方式。节电方式下,CPU停止工作,振荡器保持工作,输出时钟信号到定时器、串行口、中断系统、使它们继续工作,RAM内部保持原值。断电方式下,备用电源仅给RAM供电。只有外部中断继续工作,芯片中程序未涉及的数据存储器和特殊功能寄存器中的数据都将保持原值,其他电路停止工作。节电方式和断电方式可以通过软件设置,由电源控制寄存器PCON的有关位控制。PCON的字节地址为87H,各位含义如表1-9所示。

表1-9 PCON各位的含义  

其中主要与节电、断电方式控制相关的位如下。

GF1、GF0:用户通用标记。

PD:断电方式控制位,PD=1时进入断电模式。

IDL:空闲方式控制位,IDL=1时进入空闲方式。

POF:在AT89C51中是电源断电标记位,通电时为1。

节电方式可由任一个中断或硬件复位唤醒,断电方式只能由硬件复位唤醒。

6.最小系统

利用单片机本身的资源,外加时钟电路,复位电路及电源电路便可以构成单片机的最小配置系统。在最小系统的基础上,外接需要控制的电路和下载相应的程序到芯片存储器中就能正常工作。如图1-13所示,在以AT89C51为核心的最小系统上,再在P1口外接一只发光二极管,固化相应程序到AT89C51程序存储器中,将实现对发光二极管的控制功能。

图1-13 单片机最小系统图

7.指令系统

计算机能够按照人们的意愿工作,是因为人们给了它相应命令。这些命令是由计算机所能识别的指令组成的,指令是CPU用于控制功能部件,完成某一指定动作的指示或命令。

一种微处理器所具有的所有指令的集合,就构成了指令系统。指令系统越丰富,说明CPU的功能越强。一条指令对应着一种基本操作。由于计算机只能识别二进制数,所以指令也必须用二进制形式来表示,称为指令的机器码或机器指令。指令书写时采用助记符来表示。

MCS-51单片机指令系统共有33种功能,42种助记符,111条指令。

MCS-51单片机指令系统包括111条指令,按功能可以划分为五类:数据传送指令(28条)、算术运算指令(24条)、逻辑运算指令(25条)、控制转移指令(17条)、位操作指令(17条)。

1.3.4 MCS-51单片机常用开发工具及使用

单片机芯片只有烧录了程序机器码,构建了工作系统,才能按程序实现功能进行工作,需要有源程序编辑、编译、调试、仿真软件、烧录软件及硬件开发系统等开发平台。

1.案例:应用Keil μVision2开发软件,编辑、编译、调试LED闪烁程序

1)启动

用鼠标左键双击图标,进入图1-14所示界面。

图1-14 Keil μVision2工作界面

2)建立项目

(1)建立新项目。在Keil μVision2 IDE中,按项目方式组织文件,C51源程序、头文件等都放在项目文件(又称工程文件)中统一管理。

①单击“Project”(项目)菜单,在弹出的下拉菜单中,选择“New Project”(新建项目)选项,弹出图1-15所示的“Creat New Project”(创建新项目)对话框。

图1-15 “Creat New Project”(创建新项目)对话框

②在新建项目对话框中,选择保存文件位置(如C盘two-led文件夹)和命名文件名称(如led),文件类型默认为*.uv2,单击“保存”按钮。

③保存项目文件后,在弹出的如图1-16所示对话框左侧date base栏,选择Atmel公司的AT89C51单片机型号,单击“确定”按钮。

图1-16 单片机内核选择对话框

(2)设置项目。建立项目文件后,通常要对项目文件进行设置,才能够对源程序进行编译等操作。

①如图1-17所示,在项目工作界面上点击菜单“Project”,选“Options for Target ‘Target 1’”,或选择工具条上图标,弹出图1-18所示项目设置界面。

图1-17 项目设置操作图

图1-18 项目设置界面

②项目设置界面上部有多个选项卡,大多保留默认设置即可,一般只要设置“Target”、“Output”选项卡。“Target”选项卡的Xtal项设置与系统相符的参数(如12MHZ)。“Output”选项卡,在“Creat HEX file”前的复选框内打“√”;在“HEX”后的文本框中选择“HEX-80”;在“Browse Information”前的复选框内打“√”。

3)编辑源程序文件

①在项目工作界面,点击“File”(文件)菜单,选中“new”(新建)选项,打开新建源程序文件编辑窗口。

②点击“File”菜单,选中“Save as”(保存)选项,弹出保存文件对话框,在文件名栏输入自定义的文件名(如led.c)。注意:必须输入正确的扩展名,如果用C语言编写程序,则扩展名必须为.c;如果用汇编语言编写程序,则扩展名必须为.asm。选择与项目文件一致的文件夹(如C盘two-led文件夹),单击“保存”按钮保存程序文件。

③回到编辑界面后,如图1-19所示。在项目窗口单击“Target1”前面“+”号,然后在“Source Group 1”上右击,在弹出菜单项单击“Add files to Group ‘Source Group 1’”。最后在弹出的对话框文件类型栏选.c,在前面保存源程序的文件夹(C盘two-led文件夹)找到要添加的源程序文件(如led.c),单击“add”(添加)按钮,将源程序文件添加到项目,添加后的效果如图1-20所示。

④在源程序编辑窗口输入如下的C语言源程序,输入完后再保存一次文件。程序输入后效果如图1-21所示。

图1-19 添加源程序到项目

图1-20 添加源程序后的效果图

图1-21 程序输入后的效果图

  #include<reg51.h>

void delay(unsigned int time)

        {

        unsigned int i;

        unsigned char j;

        for(i=0;i<time;i++)

        for(j=0;j<120;j++)

        ;

        }

main( )

        {

        while(1)

        {

                 P1=0xf0;

        delay(2000);

        P1=0x0f;

        delay(2000);

        }

}

4)编译、调试和运行

①编译。如图1-22所示,添加源程序到项目后,在项目工作界面点击Project菜单,在下拉菜单中选中Translate,将编译当前文件;选中Build target,将编译当前文件并生成应用;选中ReBuild all target file,将重新编译所有文件并生成应用(也可在工具条上分别选中)。在输出窗口观察有无语法错误(0 Error(s)),编译成机器码(如Greating hex file form“led”),如图1-23所示。

图1-22 编译方式选择

图1-23 编译后输出窗口的效果图

②运行与调试。如图1-24所示,编译成功后,在项目工作界面点击Debug菜单,在下拉菜单中选中Start/Stop Debug session。选择step,将单步运行调试;选择step over,将跳过函数单步运行;选择Go,将运行到一个中断。

图1-24 选择调试运行方式

如图1-25所示,单击View菜单,在弹出的下拉菜单中,选择Watch & Call stack windows,观察堆栈窗口;选择memory,观察内存窗口;在Address栏选择存储空间类型(C、D、I、X)及地址(如:C:00010),观察指定空间(如ROM中10H)的内容。

图1-25 选择调试观察窗口

如图1-26所示,进入调试模式后,单击Peripherals菜单,在弹出的下拉菜单中,选择Interrupt、I/O Port、Time、Serial可以打开中断、I/O口、串行口、定时器的设置观察窗口,进行设置和观察,方便调试。

图1-26 选择外围模拟资源图

例如,编译LED闪烁程序led.c后,在Peripherals菜单中,I/O Port打开P1观察窗口,选择在debug菜单中选择step(单步运行),将在P1端口观察到程序运行时的执行结果。如图1-27所示。

图1-27 I/O口调试图

2.案例:用Proteus软件仿真LED轮流闪烁程序

1)启动Proteus ISIS

双击桌面上的ISIS 6 Professional图标,或者单击屏幕下方的“开始”→“程序”→“Proteus 6 Professional”→“ISIS 6 Professional”,出现启动屏幕,进入如图1-28所示的Proteus ISIS集成环境。

图1-28 Proteus ISIS集成环境

2)文件管理

①建立文件。单击“file”菜单,在下拉菜单中选择“New Design”,弹出设计纸张对话框,选择纸张(例如landscape A4),进入如图1-29所示设计工作环境。

图1-29 Proteus设计工作环境图

②保存文件。单击“file”菜单,在下拉菜单中选择“Save Design As”,弹出保存路径对话框,填写文件名和选择路径,点击保存按钮将保存文件。

③打开文件。单击“file”菜单,在下拉菜单中选择“Load Design”,弹出寻找路径对话框,找到待打开的设计文件,点击打开按钮将打开文件。

3)建立仿真模型

①建立元件库。选择设计工作环境界面工具箱上component(元件选取工具)图标,如图1-30(a)所示,点击对象选择器的p按钮(pick Devices),元件如图1-30(b)所示,在打开的对话框keyword文本框中,输入要找的元件(如AT89C51),在备选对象中选择元件(如AT89C51),点击OK,元件将添加到库文件库,如图1-31所示。

图1-30 建立电路仿真元件库窗口

图1-31 添加库元件图

常用元件keyword是:resististors (电阻)、capacitors(电容)、genleect(电解电容)、crysta(晶振)、led-red(红色发光二极管)。

②放置元件。在元件库中,选择待放置的元件(如AT89C51),点击电路图编辑窗口放置元件,如图1-32所示。

图1-32 放置仿真元件

③元件编辑。在元件上单击右键选中元件,单击并按住左键将拖动元件移动;选择工具将调整元件放置方向;单击左键将弹出元件参数设置对话框,在对话框进行参数和序号设置等。如图1-33所示为CPU参数设置图。其中Program File栏为添加.HEX文件项,通过点击按钮,可以浏览打开.HEX文件(如led.hex),然后进行添加。

图1-33 CPU参数设置

④电路连线。点击图标,把元件连接成仿真电路,如图1-34所示,为简单仿真电路模型。

图1-34 简单仿真电路模型

4)仿真

点击工具图标,然后进行运行、暂停、停止仿真,可以观察仿真效果。

3.案例:用ISP并口方式,固化LED轮流闪烁程序

1)并口方式固化程序

单片机下载工具和软件,目前常用的有周立功编程器、伟福编程器、ISP下载等,通过它们,可实现把编译成的机器码烧录到单片机芯片或程序存储器中。

①硬件连接。P1端口作下载端口,连接下载线到计算机的并口,接通电源。

②下载软件的设置。运行下载软件,选用AT89C51单片机芯片,检测芯片及初始化,界面如图1-35所示。

图1-35 下载软件开始界面

编程器类型选择为EASY isp下载线,如图1-36所示进行设置。

图1-36 EASY isp下载线的设置

③下载固化程序。点击“打开文件”菜单,浏览文件位置,打开LED.Hex文件,如图1-37所示,然后点击“自动完成”,将LED闪烁程序的机器码固化到单片机芯片。

图1-37 打开烧录文件

2)USB转串口方式固化程序

有的51内核单片机,可以用串口方式下载软件,USB转串口线价格低廉,现在广泛应用于单片机程序下载与调试中,下载步骤与并口下载相同。

1.3.5 Keil C51程序设计

Keil C51是美国Keil Software公司出品的与51系列兼容的单片机C语言软件开发系统。Keil C51以软件包的形式向用户提供了丰富的库函数和功能强大的集成开发调试工具,生成的目标代码效率高。与汇编语言相比,C51在功能上、结构性、可读性、可维护性上有明显的优势。

用C语言编写单片机应用程序,与标准的C语言程序在语法规则、程序结构及程序设计方法等方面基本相同,虽然不用像汇编语言那样必须具体组织、分配存储器资源和处理端口数据,但在C51语言编程中,对数据类型与变量的定义,必须要与单片机的存储结构相关联,否则编译器不能正确地映射定位。

1.C51程序结构

C51源程序由一个或多个函数组成,每个函数完成一种指定的操作。

例如:

  #include <reg51.h> ∥头文件包含

 / ***********延时函数*************** /

 void delay(unsigned int time) ∥定义延时函数

        {

        unsigned int i,j; ∥定义变量i,j 

        for(j=0;j<time;j++)

        for(i=0;i<258;i++)

   ;  ∥空语句

        }

/ ***********主函数************ /

 main( )

        { 

        unsigned int i;

        unsigned char led_val;

        while(1)

        {

        led_val=0x01;

        for(i=0;i<8;i++)

        {

         P0=~led_val;

         delay(100);

         led_val<<=1;

           }

      }

      }

该程序是由两个相互独立的函数组成:一个是main函数;另一个是delay(int time)函数。main函数调用延迟函数,实现在P0口每隔一定时间轮流输出低电平。可以看出,该程序实现了控制发光二极管按一定时间轮流显示的功能。

从上述程序可以看出,C51程序的基本结构如下。

①C51程序由函数构成。函数是构成C51程序的基本单位,每个C51程序由一个或多个函数组成,必须有且只有一个名为main的主函数。

②每个函数的基本结构如下:

函数名( )

{

        语句1;

        ……

        语句n;

}

有的函数在定义时,函数名前面有返回值类型,函数名后面( )里有形式参数;{}内由若干语句组成的函数体,每个语句必须以“;”结束。

③各函数相互独立,程序的执行总是从主函数开始。

2.数据类型

1)标识符与关键字

C语言中的标识符用来表示源程序中某个对象的名字,作为变量名、函数名、数组名、类型名或文件名,它由一个字符或多个字符组成。标识符的第一个字符必须是字母或下划线,随后的字符必须是字母、数字或下划线,例如coun_t1。标识符的长度一般不多于32个字符。程序中标识符的命名应当简洁明了、含义清晰,便于阅读理解,同时注意区分字母的大小写。

关键字是一种具有固定名称和特定含义的标识符。关键字又称保留字,因为这些标识符系统已经做了定义,用户不能将关键字用作自己定义的标识符。ANSI C标准一共规定了32个关键字,如表1-10所示。C51根据8051单片机扩展的关键字,如表1-11所示。

表1-10 ANSI C标准关键字  

表1-11 C51编译器的扩展关键字  

2)数据类型

数据是计算机的操作对象与处理的基本单元,我们把数据的不同格式称为数据类型。在C51中扩展了数据类型:位型(bit/sbit)、特殊功能器数据类型(sfr/sfr16)。其余数据类型char、int、long、float、enum、指针型等与标准C相同,如表1-12所示。它还支持构造数据类型。

表1-12 Keil C51的基本数据类型  

bit是C51编译器的一种扩充数据类型,用它可定义一个位变量,但不能定义位指针和位数组。如“bit *p;”、“bit [4];”是错误的。它的取值是一个二进制位,不是0就是1。

sbit也是C51特有的数据类型,用它可从字节中定义一个位寻址对象,来访问片内RAM中的可寻址位或特殊功能寄存器中的可寻址位。

sfr数据类型用来定义单片机内部8位的特殊功能寄存器,占用一个内存单元,值的范围为0~255。

sfr16类型用来定义16位的特殊功能寄存器。占用两个内存单元,取值范围为0~65535。

int类型为整型数据,占2个字节。long int为长整型数据类型,占4个字节。数据在存储单元存放时,高字节存放在低地址,低字节存放在高地址。

unsigned int、unsigned long为无符号整型数据类型和无符号长整型数据类型。在存储单元中,二进制位全表示存放数本身。signed int表示带符号整型数据类型,用msb位作符号标志位,数值用二进制补码表示。

Char为字符型数据类型,占1个字节。signed char为带符号字符型数据类型,高位为符号位,数值用补码表示。unsigned char为无符号字符型数据类型,8位全为数据本身。

Float为浮点型数据类型,长度为32位,占4个字节。

*为指针型数据类型。在C51中,指针变量的长度一般为1~3个字节。它也有类型之分,如“char *point”;表示point是一个字符型指针变量。使用指针型变量,可以方便对物理地址直接进行操作。

3.数据类型转换

C51在进行运算时,不同类型的数据要先转换成同一类型,然后才能运算。数据类型的转换可分为以下两种。

(1)自动转换。当运算对象为不同类型时,按“向高看齐”的一致化规则进行,即类型级别较低以及字长较短的一方,转换为类型级别较高的一方的类型。具体类型转换规则如图1-38所示。

图1-38 数据类型转换规则图

(2)强制转换。利用强制类型转换运算符,将一个表达式转换成所需类型,一般形式为:

(类型标识符)表达式

例如,(int)(x+y) ∥将x+y的值转换成整型

对一个变量进行强制转换后,得到一个新类型的数据,但原来变量的类型不变。

4.常量和变量

1)常量

数据有常量和变量之分。常量是指在程序运行过程中,其值不能改变的量。常量包括整型、浮点型、字符型、字符串型和位常量和符号。常量用于不必改变值的场合,如固定的数据表、字库等。

(1)整型常量。整型常量有十进制整数,如122、-4等;十六进制整数,以0x开头,如0x235等;长整数常量是在数字后面加一个字母L表示,如1345L等。

(2)浮点型常量。浮点型常量有十进制形式和指数形式两种表示形式。十进制表示形式又称定点表示形式,由数字和小数点组成,如0.314等。这种形式,如果整数或小数部分为0,可以省略不写,但小数点必须写,如10.、.11等。指数形式由整数部分、尾数部分和指数部分组成。指数部分用E或e开头,幂指数可以为负,如5e-2表示5×10-2

(3)字符型常量。字符型常量是用一对单引号括起来的单个字节。如‘a’、‘c’等。在C51中字符是按所对应的ASCII码值来存储的,一个字符占1个字节。

(4)字符串型常量。字符串型常量是用一对双引号括起来的一串字符,如“CDEF”、“1234”等。它在内存中存储时,自动在字符串的末尾加一个串结束标志(\0),因此,如果字符数为n,则它在内存中占有n+1个字节。字符串常量首尾的双引号,起定界作用,当需要表示双引号字符时,可用双引号转义字符(“\”)来表示。

字符串常量与字符常量是不同的。它们的表示形式不同,在存储时也不同。例如字符‘A’只占1字节,而字符串常量“A”,占2个字节。

(5)位常量。位常量是一位二进制数0或1。

(6)符号常量。将程序中的常量定义为一个标识符,称为符号常量,一般使用大写英文字母表示。其定义形式为:

#define <符号常量名> <常量>

例如,#define PI 3.14

这条预处理命令定义了一个符号常量PI,它的值为3.14。

2)变量

变量是指在程序运行过程中,其值能改变的量。变量数据类型可以选用C51所有支持的数据类型。但是,只有bit和unsisgned char两种数据类型,可以直接支持机器指令,而其他都要经过复杂的变量类型和数据类型的处理,导致程序编译效率低、运行速度慢。

C51中变量有全局变量和局部变量之分,全局变量是在函数外部定义的变量。它可以被多个函数共同使用,其有效作用范围是从它定义的位置开始到整个程序文件结束。如果全局变量定义在一个程序文件的开始处,则在整个程序文件范围内都可以使用它。如果一个全局变量不是在程序文件的开始处定义的,但又希望在它的定义点之前的函数中引用它,这时应在引用该变量的函数中用关键字extern将其说明为“外部变量”。另外,如果在一个程序模块文件中,引用另一个程序模块文件中定义的变量时,也必须用extern进行说明。局部变量是函数中定义的变量,只能在本函数中使用它。

程序中使用变量必须“先定义后使用”,在C51程序设计中,定义一个变量的格式如下:

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

其中方括号内的内容为可选项,数据类型和变量名不能省略。

①数据类型是指前面介绍的C51编译器所支持的各种数据类型。指定了数据类型后,编译器才能为才能为变量分配合适的内存空间。指定数据类型时要使变量的数值范围与数据类型表示数据范围相对应。在程序中应尽可量使用无符号字符变量和位变量。

②变量名为变量的标识。要按前面的介绍使用合法的标识符。

③存储器数据类型是说明数据在单片机的存储区域情况,为变量选择了存储器类型,就是指定了它在MCS-51单片机中使用的存储区域。如表1-13所示,Keil C51编译器能识别的存储器类型有DATA、BDATA、IDATA、PDATA、XDATA、CODE几种。

表1-13 存储器类型  

如果省略变量或参数的存储类型,系统将按照编译器选择的存储模式指定默认存储器类型。

small模式:存储器类型为data,空间最小,速度快。

compact模式:存储器类型为pdata,空间与速度在中间状态。

large模式:存储器类型为xdata,空间最大,速度最慢。

④存储种类是指变量在程序执行过程中的作用范围。C51变量的存储种类有自动变量(auto)、外部变量(extern)、静态变量(static)、寄存器变量 “register” 4种。

a.auto自动存储类。指定被说明的对象放在内存的堆栈中,在C51中,把函数中说明的内部变量指针,以及函数参数表中的参数都放在堆栈中,它们随着函数的进入而建立,随着函数的退出而自动被放弃。在函数中被说明的局部变量,凡未加其他存储类说明的变量都是auto存储类,每次函数被调用时,都要重新在堆栈中分配,位置一般不同。

b.register寄存器存储类。指将变量放在CPU的寄存器中,以求高速处理,一般不推荐使用。

c.extern外部存储器类。在C51中,定义在所有函数之外的变量都是全局变量,编译时分配存储空间,其作用域为从定义点开始到本文件末尾。不带存储类别的外部变量说明称为变量的定义性说明,此时,相应的变量有对应的存储空间;而带有存储类别的外部变量说明称为变量的引用性说明,它不另外占据内存空间。在多个文件程序中,允许其他文件的函数引用在另一个文件中定义的全局变量,应该在需要引用它的文件中用extern作说明。

d.static静态存储类。在函数内部使用static对变量进行说明后,静态存储变量的存储空间在程序的整个运行期间是固定的;在函数每次被调用的过程中,静态内部变量的值具有继承性。如果在定义内部静态变量时不赋初值,则编译时自动赋值0(对数值型变量)或空字符(对字符变量)。内部静态变量在程序中全程存在,但只在本函数内可取值,这样使变量在定义它的函数外部被保护。

3)特殊功能寄存器变量

在C51中,允许用户对单片机片内的特殊功能寄存器进行访问,访问时必须通过sfr或sfr16数据类型说明符进行定义,定义时指明它们所对应的片内RAM单元的地址,使定义后的特殊功能寄存器变量与51单片机的SFR对应。特殊功能寄存器变量定义格式如下:

sfr 8位特殊功能寄存器名=特殊功能寄存器字节地址常数;

sfr16 :16位特殊功能寄存器名=特殊功能寄存器字节地址常数;

例如,sfr P0=0x80; ∥ P0口的地址是80H。

sfr16        DPTR=0x82; ∥ DPTR的地址是80H。

4)位变量

在C51中,允许用户通过位类型符定义位变量。位类型符有两个:bit和sbit。可以定义两种位变量。

bit位类型符用于定义一般的可位处理位变量。它的定义格式如下:

    bit 位变量名;

位变量的存储器类型只能是bdata、data、idata。即位变量的空间只能是片内RAM的可位寻址区20H~2FH,严格来说只能是bdata。

例如:bit data a1;    / *正确* /

   bit bdata a2;   / *正确* /

   bit pdata a3;   / *错误* /

   bit xdata a4;   / *错误* /

sbit位类型符用于定义可位寻址字节或特殊功能寄存器中的位,定义时必须指明其位地址,可以是位直接地址,可以是可位寻址变量带位号,也可以是特殊功能寄存器名带位号。定义格式如下:

    sbit 位变量名=位地址常数;

例如,sbit CY=0Xd7;

    sfr  P1=0x90;

    sbit        P10=P1^0;

在C51中,为了用户处理方便,C51编译器把MCS-51单片机常用的特殊功能寄存器和特殊位进行了定义,放在reg51.h的头文件中,当用户使用时,用#include<reg51.h>预处理命令,把头文件包含到程序中,然后就可以使用这些特殊功能寄存器和特殊位。

5.运算符与表达式

运算符是一种程序记号,当它作用于操作数时,可以产生某种运算。操作数可以是常量、变量或函数,表达式是由运算符及运算对象所组成的具有特定含义的式子。

按运算符在表达式中所起的作用可分为:算术运算符、关系运算符、增量与减量运算符、赋值运算符、逻辑运算符、位运算符、复合赋值运算符、逗号运算符、条件运算符、指针和地址运算符和sizeof运算符等。

按照表达式中运算符与操作数之间的关系,又可把运算符分为单目运算符、双目运算符和三目运算符。

1)算术运算符和表达式

算术运算符和表达式如表1-14所示。

表1-14 算术运算符和表达式  

用算术运算符将运算对象连接起来的式子称为算术表达式。

算术运算符的优先级如表1-15所示。当运算符的优先级相同时,按照从左向右的顺序进行计算。可以使用圆括号帮助限定运算顺序,不能使用方括号与花括号。可以使用多层圆括号,但左右必须配对,运算时从内到外依次计算表达式的值。

表1-15 算术运算符的优先级  

2)自增、自减运算符

自增、自减运算符如表1-16所示。自增运算符(++)、自减运算符(--)只能用于变量,而不能用于常量或表达式,例如,6++是不合法的。++和--的结合方向是“自右至左”。例如,-i++,相当于-(i++)。

表1-16 自增、自减运算符  

常用于循环语句中,使循环变量自动加1;也用于指针变量,使指针指向下一个地址。

3)关系运算符与关系表达式

关系运算符用于对两个运算量进行比较。C51关系运算符,如表1-17所示。用关系运算符将运算符左边的操作数与右边操作数连接起来,称为关系表达式。在进行关系运算时,运算的结果为“真”(1)或为“假”(0)。关系运算符的优先级如表1-17所示。

表1-17 关系运算符及功能  

4)逻辑运算符和逻辑表达式

C51逻辑运算符,如表1-18所示。“&&”与“‖”是双目运算符,它要求有两个运算量(操作数)。“!”是单目运算符,只要求有一个操作数。

表1-18 逻辑运算符及功能  

使用逻辑运算符将关系表达式或逻辑量连接起来,称为逻辑表达式,表1-19给出了a和b的值为不同组合时的运算结果。

表1-19 a和b的值为不同组合时的运算结果  

逻辑运算符的优先级,如表1-18所示。

5)赋值运算符与赋值表达式

赋值运算符的作用是将右边的表达式赋给左边的变量,用赋值运算符将一个变量与一个表达式连接起来的式子称为赋值表达式。

(1)赋值符号。“=”就是赋值运算符,赋值表达式的一般形式为:

变量名=表达式;

赋值符号“=”不同于数学中使用的等号,它没有相等意义,例如,y=y+1;的含义是取出y的变量中的值加1后,再存入y中去。一个表达式中,可出现多个赋值运算符,其运算顺序是从右到左结合,例如,a=b=2+5;相当于a=(b=2+5);赋值表达式。进行赋值运算时,当赋值运算符两边的数据类型不同时,将由系统自动进行转换成运算符左边的数据类型。

(2)复合赋值运算符。C51可以在赋值运算运算符“=”之前加上其他运算符,构成复合赋值运算,用以简化程序,提高编译的效率。其一般格式为:

  变量 双目运算符=表达式;

相当于:

  变量 =变量 双目运算符 表达式;

运算时,首先对变量进行某种运算,然后将运算的结果再赋给该变量。常用的复合运算符有:

+=:加法赋值,例如a+= b相当于a=a+b;

-=:减法赋值,例如 a-= b相当于a =a-b;

*=:乘法赋值,例如 a * = b相当于a=a*b;

/=:除法赋值,例如 a/=b相当于a=a/b;

<<=:左移位赋值,例如 a<<=b相当于a=a<<b;

>>=:右移位赋值,例如a>>=b相当于a=a>>b;

&=:逻辑与赋值,例如a&=b相当于a=a&b;

%=:取模赋值,例如a%=b相当于a=a%b;

<=:逻辑异或赋值,例如a<=b相当于a=a<b;

凡是双目运算符,都可以与赋值符一起组成复合赋值运算符,它的优先级具有右结合性。

6)位运算符

C51中共有6种位运算符,能对运算对象进行位操作,如表1-20所示。

表1-20 位运算符及含义  

7)逗号运算符与表达式

逗号运算符的作用是把几个表达式连接起来,成为逗号表达式,它的一般形式为:

表达式1,表达式2,…,表达式n;

在运算时,按从左到右的顺序,依次计算出各表达式的值,整个逗号表达式的值就是最右边表达式的值。例如,x=(y=4,z=6,y+3);将括号中的逗号表达式的值赋给x,其结果x=7(y赋值4,z赋值6)。使用逗号运算符一次可完成几个赋值语句,由于逗号运算符的优先级最低,所以必须使用括号才能完成对x的赋值。

8)条件运算符与表达式

它是C51语言一个独特的三目运算符。它是对3个操作数进行操作的运算符,用它将三个表达式连接构成一个条件表达式,它的一般形式是:

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

运算符“?”作用是计算逻辑表达式,当值为真(1)时,将表达式1的值作为整个条件表达式的值;如果当逻辑表达式的值为假(0)时,将表达式2的值作为整个条件表达式的值。例如,y=‘a’>‘b’? 3:5;结果是赋给y=5,因为‘a’>‘b’为假。

9)指针和地址运算符

指针运算符“*”为单目运算符。必须在操作数的左侧,运算结果为指针所指地址的内容,指针运算的一般形式为:

变量=* 指针变量

指针变量只能存放地址,不能将一个整型量或任何其他非地址值赋给一个指针变量。取地址运算符“&”为单目运算符,必须在操作数的左侧,作用是求表达式的地址。一般形式为:

指针变量=& 目标变量

表示将目标变量的地址赋给左边的指针变量。

主要运算符的优先级和结合性如表1-21所示。

表1-21 运算符优先级列表  

6.绝对地址的访问

在C51中,可以使用存储单元的绝对地址来访问存储器。访问绝对地址的方法有三种。

1)使用C51中的绝对宏

C51编译器提供了一组宏定义来对51系列单片机的code、data、pdata和xdata空间进行绝对寻址。在头文件absacc.h中定义了8个绝对宏,函数原型如下:

#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以字节形式对code区寻址,DBYTE以字节形式对data区寻址,PBYTE以字节形式对pdata区寻址,XBYTE以字节形式对xdata区寻址,CWORD以字形式对code区寻址,DWORD以字形式对data区寻址,PWORD以字形式对pdata区寻址,XWORD以字形式对xdata区寻址。访问形式如下:

宏名[地址]

宏名为:CBYTE、DBYTE、PBYTE、XBYTE、CWORD、DWORD、PWORD或XWORD。

在程序中,使用预处理命令“#include <absacc.h>”后就可使用其中定义的宏来访问绝对地址。

例如:

#include <absacc.h>  / *将绝对地址头文件包含在文件中* /

void main( )

   {

        unsigned char var1;

        unsigned int var2;

       var1=XBYTE[0x0005];  ∥利用XBYTE[0x0005]访问片外RAM的0005H字节单元,取单元内容赋值给变量var1

        var2=XWORD[0x0002];         ∥利用XWORD[0x0002]访问片外RAM的0002H字单元

        ……

        }

2)使用C51扩展关键字_at_ 

使用_at_对指定的存储器空间的绝对地址进行访问,一般格式如下:

  [存储器类型] 数据类型说明符 变量名 _at_ 地址常数;

例如,

data char x1 _at_ 0x40; ∥在data区中定义字符变量x1,它的地址为40H

xdata int x2 _at_ 0x2000; ∥在xdata区中定义整型变量x2,它的地址为2000H

xdata char x[2] _at_ 0x3000; ∥在xdata区中定义数组x,它的首地址为3000H

使用时应注意:这种绝对地址定义的变量不能被初始化,bit型函数及变量不能用_at_指定,使用_at_定义的变量必须为全局变量。

3)通过指针访问

Keil C51编译器允许使用者规定指针指向存储段,这种指针叫具体指针。采用具体指针的方法,可以实现在C51程序中对任意指定的存储器单元进行访问,而且能节省存储空间。

例如 :

unsigned char data *p1;  ∥定义一个指向data区的指针p1

p1=0x20;  ∥p1指针赋值,使p1指向pdata区的20H单元

*p1=0x30;  ∥将数据0x30送到片外RAM的20H单元

7.C51的控基本语句

语句是构成C51程序的最小单元,按功能分为表达式语句、空语句、复合语句、函数调用语句、控制语句等。

1)表达式语句

表达式后面加一个分号“;”就构成了一个语句。例如,语句“x=8;”,把8赋给变量x。

2)空语句

表达式语句仅由分号“;”组成,它表示什么也不做。

3)复合语句

由“{”和“}”把若干条变量说明或语句组合在一起,称之为复合语句。复合语句的一般形式为:

{

  语句1;

        语句2;

         ︙

        语句n;

}

复合语句在执行时,其中的各条单语句依次顺序执行。复合语句在语法上等价于一条单语句。

4)函数调用语句

由一个函数调用加上一个分号组成的语句,称为函数调用语句。例如:

Delay( );  ∥调用延迟函数的语句

5)控制语句

控制语句主要分为选择语句、循环语句、转向语句三类。

选择语句主要有if语句 、switch语句,循环语句主要有while语句 、do-while( )语句、for语句,转向语句主要有break (中止执行switch或循环)语句、continue (结束本次循环)语句、goto 转向语句、return (从函数返回)语句。

8.C51程序基本结构与控制语句

1)C51程序基本结构

C语言是一种结构化程序设计语言,以函数为基本单位,每个函数的编程都由若干基本结构组成。归纳起来C51与汇编程序设计一样有三种基本结构:顺序结构、选择结构和循环结构,如图1-39所示。

图1-39 C51程序基本结构图

2)选择结构程序控制语句

通过选择结构,能够改变程序的执行路线,在程序的执行过程中,在某个特定的条件下完成相应的操作。能够实现选择流程控制的语句有:if、if-else 、 if-else if语句和switch-case 语句等。

(1)if语句。C51提供三种形式的if语句。

①if(表达式)语句,格式为:

if(表达式)

语句

如果表达式的值为真(非零的数),则执行if后的语句,否则跳过if后的语句。其执行过程如图1-40所示。

图1-40 if语句流程图

例如,下列程序

#include <reg51.h> 

sbit P10=P1^0;

sbit P20=P2^0;

sbit P21=P2^1;

main( )

{

     if(P10==1)

        {

        P20=0;

        P21=1;

        }

     if(P10==0)

        {

         P20=1;

         P21=0;

        }

}

实现从P1.0口读取电平状态,如果P1.0电平状态为高电平,则P2.0输出低电平,P2.1输出高电平;如果P1.0电平状态为低电平,则P2.1输出低电平,P2.0输出高电平。

②if-else形式,格式为:

if(表达式)

语句1

else 

语句2

如果表达式的值为真,则执行语句1,否则执行语句2。其执行过程如图1-41所示。

当条件表达式的结果为真时,执行语句1,反之就执行语句2。

图1-41 if-else 语句流程图

#include <reg51.h> 

main( )

{       

        sbit P10=P1^0;

        sbit P20=P2^0;

        sbit P21=P2^1;

        main( )

    { 

      if(P10==1)

        {

        P20=0;

        P21=1;

        }

else

        {

        P20=1;

        P21=0;

        }

}

该程序同样实现从P1.0口读取电平状态,如果P1.0电平状态为高电平,则P2.0输出低电平,P2.1输出高电平;如果P1.0电平状态为低电平,则P2.1输出低电平,P2.0输出高电平。

③if-else if形式,格式为:

if(表达式1)

       语句1

       else if(表达式2)

           语句2 

        else if(表达式3)

           语句3 

           …….

        else 

           语句n 

依次判断表达式的值,当出现某个值为真时,则执行其对应的语句,然后跳到整个if语句之外(语句n之后)继续执行程序;如果所有的表达式均为假,则执行语句n,然后继续执行后续程序。

例如:

#include <reg51.h>

main( )

{

        unsigned char n;

        P1=0x0ff;

        n=P1;

        if(n==0x00)

          P2=0x3f;

           else if(n==0x01)

          P2=0x06;

           else if(n==0x02)

            P2=0x5b;

           else if(n==0x03)

          P2=0x4f;

         else

        P2=0x00;

 }

该程序功能是从P1口读入数据,如果读入数据是0~3,则P2口输出相应的(共阴数码管显示)段选码,否则输出0x00。

④if-if-…else-else…形式,格式为:

  if(表达式1)

   if(表达式2)

    if(表达式3)

    语句 1 

    else 

    语句2 

else 

    语句3 

else 

    语句4 

这种形式实际上是if-else的嵌套。执行情况如表1-22所示。

表1-22 if-else嵌套执行情况表  

在四种形式的if语句中,在if关键字之后均为表达式。该表达式通常是逻辑表达式或关系表达式, 但可以是其他表达式,如赋值表达式等,也可以是一个变量。例如,if(a=4)语句,if(b)语句,都是允许的。只要表达式的值为非0,即为“真”。如在if(a=4)语句中,表达式的值永远为非0,所以其后的语句总是要执行的,这种情况在程序中不一定会出现,但在语法上是合法的。

在if语句中,条件判断表达式必须用括号括起来,在语句之后必须加分号。

在if语句的四种形式中,不是单个语句时,而是语句组时,则必须把语句用{} 括起来,组成一个复合语句,但要注意的是,在“}”之后不要加分号。

(2)switch-case语句

switch(条件表达式)

case 条件值1:语句1;break;

case 条件值2:语句2;break;

… 

case 条件值n:语句n;break;

default :语句n+1;break;

表达式的值必须为整数或字符,switch以条件表达式的值,逐个与各case的条件值相比较,当条件表达式的值与条件值相等时,即执行其后的语句,然后不再进行判断,不执行后面所有case后的语句。如条件表达式值与所有case后的条件值均不相同时,则执行default后的语句。每个语句必须有break语句,其执行流程图如图1-42所示。

图1-42 switch-case执行流程图

例如:

#include <reg51.h> 

main( )

{       

        unsigned char n;     

        P1=0xff;

        n=P1;

switch(n)

        {

        case 0x01:P0=0x01;break;

        case 0x02:P0=0x02;break;

        case 0x04:P0=0x04;break;

        case 0x08:P0=0x08;break;

        case 0x10:P0=0x10;break;

        case 0x20:P0=0x20;break;

        case 0x40:P0=0x40;break;

        case 0x80:P0=0x80;break;

        default: P0=0x00;break;

         }

  }

该程序功能是从P1口读入数据,从P0口输出数据。

使用Switch-case语句时,在case后的各条件值不能相同,否则会出现错误,在case后,允许有多个语句,各case和default语句的先后顺序可以变动,而不会影响程序执行结果;default语句可以省略。

3)循环结构流程控制语句

C51循环语句有while、do-while 、for。

(1)while循环

while(表达式)语句;

其中表达式是循环条件,语句为循环体。当条件表达式的结果为真时,程序就重复执行后面的语句,一直执行到条件表达式的结果为假时终止。这种循环结构首先检查所给条件,再根据检查结果,决定是否执行后面的语句。执行流程如图1-43所示。

图1-43 while 、do-while执行流程图

例如:从1加到1000,并将结果打印出来。

#include <stdio.h>

main( )

{

 long sum=0;  ∥因为sum的值超过int型变量能表示的范围,所以设置成长整型

        int i=1;

        while (i<=1000)

        { 

          sum+=i;

          i++;

        }

}

运行结果:500500

此程序中循环条件是“i<=1000”,循环体是“sum+=i;i++;”。“sum+=i;”语句实现的是随着i的增加,将累加的结果存放在sum中,sum起累加器的作用,共计1000次。

在表达式中使用的变量必须在执行到循环语句之前赋值,即变量初始化。循环体中的语句必须在循环过程中修改表达式中的变量的值。

while语句在使用时,语句中的表达式一般是关系表达式或逻辑表达式,只要表达式的值为真(非0),即可继续循环;循环体如包含有一个以上的语句,则必须用{}括起来,组成复合语句;应注意循环条件以避免永远为真,造成死循环。

(2)do…while语句

do

 {  

  语句; 

 }while(表达式); 

表达式是循环条件,其中语句是循环体,是直到型循环结构。

这种循环结构的执行过程是先执行给定的循环语句,然后再检查条件表达式的结果,当条件表达式的值为真时,则重复执行循环体语句,直到条件表达式的值变为假时为止。因此,do…while循环结构在任何条件下,至少会被执行一次。

例如:求1+2+…+1000的和。

#include <stdio.h>

main( )

        { 

          int i=1;

          long sum = 0;

          do

           {

             sum+=i;

             i++;

          }while (i<=1000);

        }

运行结果:500500

运行结果和while循环一样。while语句是先判断<表达式>是否成立,然后再执行循环体;do…while语句是先执行循环体一次,然后再去判断<表达式>是否成立。

(3)for语句

for([表达式1:循环控制变量赋初值];[表达式2:循环继续条件];[表达式3:循环变量增值])

    { 循环体语句组;}

三个表达式之间必须用分号“;”隔开,其执行流程如图1-44所示。

图1-44 for语句流程图

例如。求1~1000之和。

#include <stdio.h>

        main( )

        { 

         int i,n=1000;

         long sum = 0;

         for(i=1;i<=n;i++)

         sum+=i;

        }

for循环的执行过程:先赋值“i=1”,然后判断“i<=n”是否成立,若为真,执行循环体“sum+=1”,转而执行“i++”,如此反复,直到“i<=n”为假为止。

for语句在使用时,三个控制表达式只是语法上的要求,可以灵活应用,其中任何一个都允许省略,但分号不能省。表达式1和表达式3可以是简单表达式、逗号表达式等。用空循环来延长时间,起到延时作用,例如,for(t=0;t<time;t++);循环体是空语句。

例如:

#include <reg51.h> 

main ( unsigne char time)

{       

        Int i,j;

        for(i=0;i<time;i++)    

        for(j=0;j<120;j++)

        {

         ;

        }

}

该程序在12M时钟的系统中,可以实现1ms× time的延迟。

三个表达式都缺省时,for(;;)<语句>是一个无限循环。当循环次数预先不确定时,可用此方法,但在循环体内必须设置break语句跳出循环,否则将成死循环。

(4)循环的嵌套。一个循环体内又包含另一个完整的循环结构,称为循环的嵌套,在内嵌的循环中还可以嵌套循环,就形成了多层循环。while循环、do…while循环和for循环可以互相嵌套。

例如:

#include <reg51.h> 

main (unsigned char count)

{

        unsigned char j,k;     

        while(count--!=0)

        { 

         for(j=0;j<10;j++)

         for(k=0;k<72;k++) 

          ; 

        }

 }

该程序在12M时钟的系统中,可以实现10ms×count的延迟。

4)转移语句

(1)goto语句。goto语句格式是:goto 语句标号;

语句标号是按标识符规定书写的符号, 放在某一语句行的前面,标号后加冒号(:)。goto语句常与if语句连用,在满足某一条件时,程序就跳到标号处执行,这样可以实现循环。用goto语句可以一次跳出多层循环,但是goto语句的转移范围只能在同一函数内,从内层循环跳到外层循环,而不允许从外层循环跳到内层循环。不加限制地使用goto语句会造成程序结构的混乱,降低程序的可读性。

例如:求1~100之间的整数和。

#include <stdio.h>

main( )

 int i=1,sum=0;

 loop:if(i<=100)

        {

        sum+=i;

        i++;

        goto loop;

        }

   }

本例中,利用if语句和goto语句的配合实现循环,但这不是循环语句。

(2)break语句。break语句格式是:

break;

break语句可以使流程从当前循环或switch结构中跳出,转移到该结构后面的第一个语句处。当有嵌套时,它只能跳出它所处的那一层循环,而不像goto语句可以直接从最内层循环跳出来。break语句不能用在除了循环语句和switch语句之外的任何其他语句中。

(3)continue语句。continue语句格式是:

continue;

它是循环继续语句,只能用在循环结构中,作用是结束本次循环,即跳过当前一轮中continue语句之后的尚未执行的语句,将流程转到下一轮循环入口。它常与if语句一起使用,用来加速循环。

例如:把1~20之间的不能被3整除的数输出。

#include <reg51.h>

main( )

 {

      int n;

      for(n=1;n<=20;n++)

      {

        if(n%3==0)

        continue;

        P0=n;

        }

}

(4)返回语句。返回语句格式是:

return (表达式);或return;

如果return语句后边带有表达式,则要计算表达式的值。使用return语句只能向主调函数回送一个值。如果return后面不跟表达式,则该函数不返回任何值,只能控制流程返回调用处。如果函数体的最后一条语句为不带表达式的return语句,则该语句可以省略。也就是说,在这种情况下,当程序执行到最后一个界限符“}”处时,就自动返回主调用函数。

9.函数

函数是C51程序的基本组成部分,C51的程序是由一个主函数main( )和若干个子函数构成,由主函数main( )开始,根据需要来调用子函数,子函数也可以互相调用。在进行程序设计的过程中,同一个函数可以被一个或多个函数调用任意多次。当被调用函数执行完毕后,就返回原来函数执行。

C51编译器还提供了丰富的运行库函数,用户可根据需要随时调用,在使用时,用户只需在程序中用预处理器伪指令,将有关头文件包含进来即可,可提高编程效率和速度。

1)函数的说明与定义

C51中所有函数与变量一样,在使用之前必须说明。说明是指说明函数是什么类型的函数,一般库函数的说明都包含在相应的头文件<*.h>中。

例如,标准输入输出函数包含在“stdio.h”中,非标准输入输出函数包含在“io.h”中,在使用库函数时必须先知道该函数包含在哪个头文件中,在程序的开头用#include <*.h>或#include"*.h"说明。

(1)函数声明。函数声明的格式如下:

函数类型 函数名(数据类型 形式参数,数据类型 形式参数,……);

函数类型是该函数返回值的数据类型,可以是整型(int)、长整型(long)、字符型(char)、浮点型(float)、无值型 (void)、指针型。无值型表示函数没有返回值。函数名为函数的名称,需符合C51的标识符规则要求,小括号中的内容为该函数的形式参数。

例如:

int putlll(int x,int y,int z,)  ∥说明一个整型函数

char *name(void);  ∥说明一个字符串指针函数

void student(int n, int m);  ∥说明一个不返回值的函数

(2)函数的定义。函数定义就是确定该函数完成什么功能,以及怎么运行。C51对函数的定义的一般形式为:

函数类型 函数名(数据类型 形式参数,数据类型 形式参数…)

  {

   函数体; 

  } 

例如:

  int max(int x, int y)

        {

        int z;  ∥ 函数体中的说明部分,定义整型变量z

        z=x>y?x:y;  ∥ x、y中的最大数赋给z

        return z;  ∥返回z的值

        }

本例中,定义了函数,函数名为max,返回值类型为整型,形式参数为x、y。函数体在{}内,实现求两个数中最大值的功能。

函数定义时,函数类型为函数返回值的类型,为C51的基本数据类型。无返回值为void,为int型可省略。

函数名的命名必须遵循标识符规则,且易读。

形式参数必须用( )括起,每个形参必须有形参声明,数据类型为C51的基本数据类型。多个形参之间必须用“,”分隔,并不是所有函数都有形参。

函数体包括在花括号“{}”中,为C51语句的组合。所用函数体中使用到的除形参之外的变量,在开始部分进行变量的类型声明。

一个程序必须有一个主函数,其他用户定义的子函数可以是任意多个,函数的位置可以在main( )函数前,也可以在其后。

2)函数参数与返回值

在定义函数时,函数名后面括号中的变量称为“形式参数”(简称形参)。而在调用函数时,函数名后面括号中的变量称为“实际参数”(简称实参)。函数参数用于建立函数之间的数据联系。当一个函数被另一个函数调用时,实际参数传递给形式参数,以实现主调函数与被调函数之间的数据通信。

有的函数在被调用执行完后,向主调函数返回一个执行结果,这个结果称为函数的返回值。函数的返回值用返回语句return实现。

例如:调用max(int x,int y)函数,求7、8的大数从P0输出。

#include <reg51.h>

    int max(int x,int y)

     { 

        if(x>y)

        return x;

        else

        return y;

    }

    main( )

    {

         P0=max(7,8);

    }

函数main调用max函数时,实际的参数7、8传给了x、y。用return z返回函数的返回值。

3)函数的调用

(1)函数的一般调用。函数的一般调用有语句调用和函数表达式调用两种。

函数语句调用是把函数调用作为一个语句。这种调用通常用于调用一个不带返回值的函数。一般形式为:

函数名(实参表);

函数表达式调用是用表达式形式调用函数,这种调用通常用于调用一个带有返回值的函数。一般形式为:

变量名=函数表达式;

例如:

#include <reg51.h>  ∥头文件包含

/ ************延时函数************** /

unsigned int add(unsigned char a,unsigned char b)  ∥定义延时函数

     { 

        unsigned int c  ∥定义变量C

        c=a+b;

          return c

        }

/ ***********主函数************ /

main( )

        unsigned int i;

        i= add(3,5);

        P0=i;

}

本程序实现P0口输出3+5的和。其中:i= add(3,5);调用add(unsigned char a,unsigned char b)函数。

注意:C51在调用函数时,被调用的函数必须是已经存在的函数(库函数或用户自定义函数)。

(2)函数的参数传递

①调用函数向被调用函数以形式参数传递。在调用函数时,一般主调函数和被调函数之间存在数据传递,这种数据传递是通过函数的参数实现的,实际参数将传递给形式参数。

例如,在上面主程序调用add(unsigned char a,unsigned char b)函数时,是把实际参数3传给形式参数a,5传给形式参数b。

注意:函数调用时实际参数必须与子函数中形式参数的数据类型、顺序和数量完全相同。

②被调用函数向调用函数返回值。函数调用时,只有执行到被调函数的最后一条语句后,或执行到语句return时才能返回。没有return语句,仅返回给调用函数一个0。若要返回一个值, 就必须用return语句,但return语句只能返回一个参数。例如,i= add(3,5);就是把add(3,5)的返回值赋给i。

③用全局变量实现参数互传。C51中,根据变量的作用范围不同,可将变量分为局部变量和全局变量。

在函数内定义的变量以及形式参数均属局部变量。局部变量在定义它的函数内有效。例如,上述unsigned int add(unsigned char a,unsigned char b)函数中的unsigned int c;语句定义的变量c就是局部变量。它只在unsigned int add(unsigned char a,unsigned char b)函数中有效。

全局变量是指所有函数之外定义的变量,其作用范围从作用点开始,直到程序结束。例如:上述程序开始部分声明语句如下:

unsigned char bai;

unsigned char shi;

unsigned char ge;

定义了三个全局变量ge、shi、bai。

设置全局变量的目的是为了增加数据传递的渠道,将所要传递的参数定义为全局变量,可使变量在整个程序中对所有函数都使用。例如,将ge、shi、bai定义为全局变量,实现了数据才在main( ),change( ),display( )函数中传递。

注意:全局变量如果与局部变量同名,则在局部变量的作用范围内,全局变量不起作用。

(3)函数的嵌套调用与递归调用。函数的嵌套调用是指在调用一个函数的过程中,被调用的函数调用了另一个函数。

C51允许函数自己调用自己,这种方式叫函数的递归调用, 递归调用可以使程序简洁、紧凑。例如:

  Int fact(n);

          {

           int n; 

           int product;

           if(n == 1)

           return(1);

           product=fact(n-1)*n;  ∥函数自身调用

           return(product);

          }

本程序为了实现求n!,用product=fact(n-1)*n;语句调用了函数本身fact(n)。

4)函数作用范围

C51中每个函数都是独立的代码块, 函数代码归该函数所有,除了对函数的调用以外, 其他任何函数中的任何语句都不能访问它。除非使用全局变量,否则一个函数内部定义的程序代码和数据,不会与另一个函数内的程序代码和数据相互影响。

 C51中不能在一个函数内再说明或定义另一个函数,但C51中只要先定义后使用,一个函数不必附加任何说明语句而被另一函数调用。如果一个函数在定义函数时,用static存储类说明符来进行了声明,则为内部函数(也称静态函数),这样可以使函数只局限于所在文件。通常把只由同一文件使用的函数和外部变量放在一个文件中,用static使之局部化,其他文件不能引用。

如果用存储类说明符extern说明,则表明此函数为外部函数。

如果在定义函数时不进行存储类说明,则隐含为外部函数。在需要调用此函数的文件中,要用extern说明所用的函数是外部函数。

10.预编译处理

C51提供了编译预处理的功能,编译预处理是在编译前先对源程序中的预处理命令进行“预处理”,然后将预处理的结果和源程序一起再进行通常的编译处理,以得到目的代码。预处理命令以“#”打头,末尾不加分号,可以出现在程序的任何位置,作用范围是从出现处直到源文件结束。通常有三种预处理指令:宏定义、条件编译、文件包含。

1)宏定义

宏定义是用预处理命令#define指定的预处理,它用一个指定的符号来表示一个字符串,又称符号常量定义。一般形式为:

#define 标识符 常量表达式

“define”是关键字,它表示该命令是宏定义。“标识符”是指定的符号,一般大写,而“常量表达式”就是赋给符号的字符串。宏定义由于不是C51的语句,所以不用在行末加分号。

例如,#define pi 3.14  ∥指定符号pi来代替3.14,在预处理时,把程序在该命令以后的所有的3.14都用pi来代替。

常用#define 定义数据类型

例如,#define unchar unsigned char  ∥定义unsigned unchar就是unsigned char类型

也常用#define定义并行口,P0,P1,P2,P3的定义在头文件reg51.h中,扩展的外部RAM和外部I/O口需要用户自定义。

例如:

#include<abasacc.h.>

#define PA XBYTE [0xffde]

main( )

{

PA=0x3b;  ∥将数据0x3b写入地址为0xffde的存储单元或I/O口

}

本程序用预处理命令#define,将PA定义为外部I/O口,地址为0xfde,XBYTE为一个指针,指向外部RAM的0地址单元,包含在abasacc.h头文件。

2)条件编译

在编译过程中,对程序源代码的各部分可以根据所求条件有选择地进行编译,即条件编译。条件编译可以选择不同的编译范围,从而产生不同的代码。C51编译器的预处理器的常用条件编译命令有:#if、#elif、#else、#endif等。一般形式是:

#if常量表达式1

   程序段1

  #elif常量表达式2

   程序段2

… …

 #elif常量表达式n-1

   程序段n-1

#else

    程序段n

#endif

如果常量表达式1的值为真(非0)时,就编译程序段1,然后将控制传递给#endif命令,结束本次条件编译,继续下面的编译处理;否则,如果常量表达式1的值为假(0),程序段1不编译,而将控制传递给下面的一个#elif命令,对常量表达式2的值进行判断。如果常量表达式2的值为假(0),则将控制再传递给下一个#elif命令,直到遇到#else或#endif命令为止。

3)文件包含

#include指令是让预处理器把源文件,嵌入到当前源文件中的该点处(用指定文件的全部内容替换该预处理行)。格式如下:

#include<文件名> 或 #include“文件名”

include是关键字,文件名是被包含的文件名,应该使用文件全名,包括文件的路径和扩展名。文件包含命令一般习惯写在文件的开头,如果文件名用引号括起来,那么就在源程序所在位置查找该文件;如果用尖括号“< >”括起来,那么就按定义的规则来查找该文件。

例如,#include<abasacc.h.>

  #include<abasacc.h.>

1.3.6 单片机程序设计

1.单片机程序设计的基本步骤

单片机程序设计的基本步骤如下。

①分配存储空间、工作寄存器及有关端口地址。

②画出程序流程图。程序流程图是用符号表示程序执行流程的框图,表示符号有以下几种:

③编制源程序。

④仿真、调试和优化程序。

⑤固化程序。

2.案例:编写限位状态识别程序

1)编程要求

限位状态识别电路如图1-45所示,编写程序实现功能:当机构运行未到位时,SW1处于断开状态,此时只有绿灯D2亮。当机构运行到位后,迫使限位开关SW1闭合,此时只红灯D2亮。

图1-45 限位状态识别电路图

2)编程思路

先读取P1.0端口状态,然后判断状态,如果P1.0状态为高电平,让P2.1输出低电平点亮绿灯,P2.0输出高电平,熄灭红灯。如果状态为低电平,让P2.0输出低电平点亮红灯,P2.1输出高电平,熄灭绿灯,程序流程图如图1-46所示。

图1-46 端口状态检测程序流程图

3)编写程序

按照编程思路与程序流程图,参考程序如下:

#include<reg51.h>

        sbit led1=P2^0;  ∥定义LED引脚

        sbit led2=P2^1;  ∥定义LED引脚

        sbit key=P1^0;  ∥定义按键引脚

/ **********LED1亮*********** / 

void led1_on(void)

        {

        led1=0;

        led2=1;

        }

/ **********LED2亮*********** / 

void led2_on(void)

         {

        led2=0;

        led1=1;

         }

/ **********主函数*********** /

main( )

{

         while(1)

        {

           if(key==1)

        led1_on( );

           else

        led2_on( );

        }

}

3.案例:编写双路声光防盗报警程序

1)编程要求

某双路声光防盗报警电路如图1-47所示。要求结合硬件电路,编写程序实现如下功能:

正常状态(SW1断开,SW2闭合)下,不报警。当异常状态(SW1闭合或SW2断开时)时,启动声光报警:蜂鸣器蜂鸣发声,LED1、LED2交替点亮闪烁,交替时间自定。

图1-47 双路声光防盗报警电路图

2)编程思路

先检测P1.0、P1.1的电平状况,正常状态时,SW1断开,SW2闭合,即P1.1输入为低电平,P1.0输入为高电平,则按要求不报警。异常时SW1闭合或SW2断开,即P1.1输入为高电平或P1.0输入为低电平,则按要求报警。因此,先读取P1.0、P1.1的电平状态,再根据P1.0、P1.1高低电平的组合状态来决定报警还是不报警。主程序流程图如图1-48所示。

图1-48 双路报警主程序流程图

报警时,先使P2.0输出低电平控制LED1点亮、P2.1输出高电平控制LED2不点亮,P2.2输出与原来状态相反电平,使蜂鸣器发声。然后再使P2.1输出低电平控制LED2点亮、P2.0输出高电平控制LED1不点亮,P2.2输出与原来状态相反电平,使蜂鸣器发声。这样来实现LED1、LED2亮灭交替闪烁与蜂鸣报警。不报警时P2.0、 P2.1、P2.2均输出高电平。

交替闪烁的时间,可以通过延迟时间的多少来实现。

3)编写程序

根据编程思路,按流程图设计参考程序如下:

 #include<reg51.h>

 sbit SW1=P1^0;

 sbit SW2=P1^1;

 sbit LED1=P2^0;

 sbit LED2=P2^1;

 sbit FMQ=P2^2;

/ ***********延迟函数********** /

void delay(unsigned int time)

        {

        unsigned int i,j;

        for(i=0;i<time;i++)

         for(j=0;j<120;j++)

             ;

        }

/ ***********报警函数********** /

void Call_police(void)

        {

        LED1=0;

        LED2=1;

        FMQ=!FMQ;

        delay(500);

        LED1=1;

        LED2=0;

        FMQ=!FMQ;

        delay(500);

        }

/ ***********正常状况不报警函数**********

void normal(void)

{

        LED1=1;

        FMQ=1;

        LED2=1;

}

/ ***********主函数**********

main( )

{

         LED1=1;  ∥初始状态(可省)

         LED2=1;

         FMQ=1;

        while(1)

        {

        if(SW1==0|SW2==1)

          Call_police( ); 

        else

          normal( );

        }

}