第3章 C51编程语言基础

第3章 C51编程语言基础

【内容概要】本章介绍有关C51语言编程的基础知识,对C51语言与8051汇编语言编程进行比较,了解C51语言与标准C语言的差别,并对C51语言的数据类型与存储类型,C51语言的基本运算,分支与循环结构,数组、指针、函数等也做以介绍。

随着单片机应用系统的日趋复杂,对程序的可读性、升级与维护以及模块化的要求越来越高,对软件编程的要求也越来越高,这就要求编程人员在短时间内编写出执行效率高、运行可靠的程序代码。同时,也要方便多个编程人员来进行协同开发。

C51语言是目前的8051单片机应用开发中,普遍使用的程序设计语言。C51语言能直接对8051单片机硬件进行操作,它既有高级语言的特点,又有汇编语言的特点,因此在8051单片机程序设计中,C51语言得到非常广泛的使用。

3.1 C51编程语言简介

C51语言是在标准C语言的基础上针对8051单片机的硬件特点进行了扩展,并向8051单片机上移植,经过多年努力,C51语言已成为公认的高效、简洁的8051单片机的实用高级编程语言。与8051汇编语言相比,C51语言在功能上、结构性、可读性、可维护性上有明显优势,且易学易用。

3.1.1 C51语言与8051汇编语言的比较

与8051单片机汇编语言相比, C51语言具有如下优点。

(1)可读性好。C51语言程序比汇编语言程序的可读性好,编程效率高,程序便于修改、维护以及升级。

(2)模块化开发与资源共享。用C51语言开发的程序模块可以不经修改,直接被其他工程所用,使得开发者能够很好地利用已有的大量标准C程序资源与丰富的库函数,从而减少重复劳动,同时也有利于多个程序设计者协同开发。

(3)可移植性好。为某种型号单片机开发的C语言程序,只需将与硬件相关的头文件和编译链接的参数进行适当修改,就可方便地移植到其他型号的单片机上。例如,为8051单片机编写的程序通过改写头文件以及少量的程序行,就可方便地移植到PIC单片机上。

(4)生成的代码效率高。当前较好的C51语言编译系统编译出来的代码,效率只比直接使用汇编语言低20%左右,如果使用优化编译选项,最高效率可达到90%。

3.1.2 C51语言与标准C语言的比较

C51语言与标准C语言有许多相同之处,但也有其自身的一些特点。不同的嵌入式C语言编译系统之所以与标准C语言有不同的地方,主要是由于它们所针对的硬件系统不同。对于8051单片机,目前广泛使用的是C51语言。

C51语言的基本语法与标准C语言相同,只是在标准C语言的基础上进行了适合于8051内核单片机硬件的扩展。深入理解C51语言对标准C语言的扩展部分以及它们的不同之处,是掌握C51语言的关键之一。

C51语言与标准C语言的一些差别如下。

(1)库函数的不同。标准C语言中的,不适合于嵌入式控制器系统的库函数,被排除在C51语言之外,如字符屏幕和图形函数,而有些库函数必须针对8051单片机的硬件特点来做出相应的开发。例如,库函数printf和scanf,在标准C语言中,这两个函数通常用于屏幕打印和接收字符,而在C51语言中,主要用于串行口数据的收发。

(2)数据类型有一定区别。在C51语言中增加了几种针对8051单片机特有的数据类型,在标准C语言的基础上又扩展了4种类型。例如,8051单片机包含位操作空间和丰富的位操作指令,因此,C51语言与标准C语言相比增加了位类型。

(3)C51语言的变量存储模式与标准C语言中的变量存储模式数据不一样。标准C语言最初是为通用计算机设计的,在通用计算机中只有一个程序和数据统一寻址的内存空间,而C51语言中变量的存储模式与8051单片机的各种存储器区紧密相关。

(4)数据存储类型的不同。8051单片机存储区可分为内部数据存储区、外部数据存储区以及程序存储区。内部数据存储区可分为3个不同的C51存储类型:data、idata和bdata。外部数据存储区分为2个不同的C51存储类型:xdata和pdata。程序存储区只能读不能写,可能在8051单片机片内或在片外,C51语言提供的code存储类型用来访问程序存储区。

(5)标准C语言没有处理单片机中断的定义,而C51语言中有专门的中断函数。

(6)头文件不同。C51语言与标准C语言头文件的差异是C51语言头文件必须把8051单片机内部的外设硬件资源(如定时器、中断、I/O等)相应的特殊功能寄存器写入到头文件内。

(7)程序结构的差异。由于8051单片机的硬件资源有限,它的编译系统不允许太多的程序嵌套。其次,标准C语言所具备的递归特性不被C51语言支持。

但是从数据运算操作、程序控制语句以及函数的使用上来说,C51语言与标准C语言几乎没有什么明显的差别。如果程序设计者具备了标准C语言的编程基础,只要注意C51语言与标准C语言的不同之处,并熟悉8051单片机的硬件结构,就能较快地掌握C51语言的编程。

3.2 C51语言程序设计基础

本节在标准C语言的基础上,了解掌握C51语言的数据类型和存储类型、C51语言的基本运算与流程控制语句、C51语言构造数据类型、C51函数以及C51程序设计的其他一些问题,为C51的程序开发打下基础。

3.2.1 C51语言中的数据类型与存储类型

数据是单片机操作的对象,是具有一定格式的数字或数值,数据的不同格式就称为数据类型。

1.数据类型

Keil C51支持的基本数据类型如表3-1所示。针对8051单片机的硬件特点,C51语言在标准C语言的基础上,扩展了4种数据类型(见表3-1中最后4行)。注意,扩展的4种数据类型,不能使用指针来对它们存取。

表3-1 Keil C51支持的数据类型

数据类型

位数

字节数

值  域

signed char

8

1

−128~+127,有符号字符变量

unsigned char

8

1

0~255,无符号字符变量

signed int

16

2

−32 768~+32 767,有符号整型数

unsigned int

16

2

0~65 535,无符号整型数

signed long

32

4

−2 147 483 648~+2 147 483 647,有符号长整型数

unsigned long

32

4

0~+4 294 967 295,无符号长整型数

float

32

4

±1.175494E-38~±3.402823E+38

double

32

4

±1.175494E-38~±3.402823E+38

*

8~24

1~3

对象指针

bit

1

 

0或1

sfr

8

1

0~255

sfr16

16

2

0~65 535

sbit

1

 

可进行位寻址的特殊功能寄存器的某位的绝对地址

2.C51的扩展数据类型

下面对扩展的4种数据类型进行说明。

(1)位变量bit。bit的值可以是1(true),也可以是0(false)。

(2)特殊功能寄存器sfr。8051单片机的特殊功能寄存器分布在片内数据存储区的地址单元80H~FFH之间,“sfr”数据类型占用一个内存单元。利用它可以访问8051单片机内部的所有特殊功能寄存器。例如,sfr P1=0x90这一语句定义了P1端口在片内的寄存器,在程序后续的语句中可以用“P1=0xff”,使P1的所有引脚输出为高电平的语句来操作特殊功能寄存器。

(3)特殊功能寄存器sfr16。“sfr16”数据类型占用两个内存单元。sfr16和sfr一样用于操作特殊功能寄存器,不同的是,sfr16用于操作占两个字节的特殊功能寄存器。例如,“sfr16 DPTR=0x82”语句定义了片内16位数据指针寄存器DPTR,其低8位字节地址为82H,高8位字节地址为83H,在程序的后续语句中就可对DPTR进行操作。

(4)特殊功能位sbit。sbit是指AT89S51片内特殊功能寄存器的可寻址位。例如,

sfr  PSW=0xd0;     //定义PSW寄存器地址为0xd0
sbit  OV=PSW^2;    //定义OV位为PSW.2

符号“^”前面是特殊功能寄存器的名字,“^”后面的数字定义特殊功能寄存器可寻址位在寄存器中的位置,取值必须是0~7。

G:\注意.tif

不要把bit与sbit相混淆。bit是用来定义普通的位变量,它的值只能是二进制的0或1。而sbit定义的是特殊功能寄存器的可寻址位,它的值是可位寻址的特殊功能寄存器某位的绝对地址,例如PSW寄存器OV位的绝对地址0xd2。

上面的例子还涉及C51注释的写法问题,C51的注释写法有两种:

(1)//……,两个斜杠后面跟着的为注释语句,本写法只能注释一行,当换行时,必须在新行上重新写“//”。

(2)/*……*/,一个斜杠与星号结合使用,本写法可注释任一行,即斜杠星号与星号斜杠之间的所有文字都作为注释,即注释有多行时,只需在注释的开始处,加“/*”,在注释的结尾处,加上“*/”即可。

加注释的目的是为了便于读懂程序,所有注释都不参与程序编译,编译器在编译过程中会自动删去注释。

3.数据存储类型

在讨论C51的数据类型时,必须同时提及它的存储类型,以及它与8051单片机存储器结构的关系,因为C51定义的任何数据类型必须以一定的方式,定位在8051单片机的某一存储区中,否则就没有任何实际意义。

8051单片机有片内、片外数据存储区,还有程序存储区。片内的数据存储区是可读写的,8051单片机的衍生系列最多可有256字节的内部数据存储区(例如AT89S52单片机),其中低128字节可直接寻址,高128字节(80H~FFH)只能间接寻址,从地址20H开始的16字节可位寻址。内部数据存储区可分为3个不同的数据存储类型:data、idata和bdata。

访问片外数据存储区比访问片内数据存储区慢,因为访问片外数据存储区需要通过数据指针加载地址来间接寻址访问。C51提供两种不同的数据存储类型——xdata和pdata,来访问片外数据存储区。

程序存储区只能读不能写。程序存储区可能在8051单片机内部或外部,或者外部和内部都有,这由8051单片机的硬件决定,C51提供了code存储类型来访问程序存储区。

上述的C51的数据存储类型与8051单片机实际存储空间的对应关系见表3-2。

表3-2 C51语言存储类型与8051存储空间的对应关系

存储区

存储类型

与存储空间的对应关系

DATA

data

片内RAM直接寻址区,位于片内RAM的低128字节

BDATA

bdata

片内RAM位寻址区,位于20H~2FH空间

IDATA

idata

片内RAM的256字节,必须间接寻址的存储区

XDATA

xdata

片外64KB的RAM空间,使用@DPTR间接寻址

PDATA

pdata

片外RAM的256字节,使用@Ri 间接寻址

CODE

code

程序存储区,使用DPTR寻址

下面对表3-2中的各种存储区作以说明。

(1)DATA区。DATA区的寻址是最快的,应把经常使用的变量放在DATA区,但是DATA区的存储空间是有限的,DATA区除了包含程序变量外,还包含了堆栈和寄存器组。DATA区声明中的存储类型标识符为data,通常指片内RAM的128字节的内部数据存储的变量,可直接寻址。

声明举例如下:

unsigned char data system_status=0;
unsigned int data unit_id[8];
char data inp_string[20];

标准变量和用户自声明变量都可存储在DATA区中,只要不超出DATA区的范围即可,由于C51使用默认的寄存器组来传递参数,这样DATA区至少失去了8字节的空间。另外,当内部堆栈溢出的时候,程序会莫名其妙地复位。这是因为8051单片机没有报错的机制,堆栈的溢出只能以这种方式表示,因此要留有较大的堆栈空间来防止堆栈溢出。

(2)BDATA区。BDATA区实质上是DATA中的位寻址区,在这个区中声明变量就可进行位寻址。BDATA区声明中的存储类型标识符为bdata,指的是片内RAM可位寻址的16字节存储区(字节地址为20H~2FH)中的128个位。

下面是在BDATA区中声明的位变量和使用位变量的例子:

unsigned char bdata status_byte;
unsigned int bdata status_word;
sbit stat_flag=status_byte^4;
if(status_word^15)
{ …… }
stat_flag=1;

C51编译器不允许在BDATA区中声明float和double型的变量。

(3)IDATA区。IDATA区使用寄存器作为指针来进行间接寻址,常用来存放使用比较频繁的变量。与外部存储器寻址相比,它的指令执行周期和代码长度相对较短。IDATA区声明中的存储类型标识符为idata,指的是片内RAM的256字节的存储区,它只能间接寻址,速度比直接寻址慢。

声明举例如下:

unsigned char idata system_status=0;
unsigned int idata unit_id[8];
char idata inp_string[16];
float idata out_value;

(4)PDATA区和XDATA区。PDATA区和XDATA区位于片外存储区,PDATA区和XDATA区声明中的存储类型标识符分别为pdata和xdata。PDATA区只有256字节,仅指定256字节的外部数据存储区。但XDATA区最多可达64KB,它对应的xdata存储类型标识符可以指定外部数据区64KB内的任何地址。

对PDATA区寻址要比对XDATA区寻址快,因为对PDATA区寻址,只需装入8位地址,而对XDATA区寻址要装入16位地址,所以要尽量把外部数据存储在PDATA区中。

对PDATA区和XDATA区的声明举例如下:

unsigned char xdata system_status=0;
unsigned int pdata unit_id[8];
char xdata inp_string[16];
float pdata out_value;

由于外部数据存储器与外部I/O口是统一编址的,因此外部数据存储器地址段中除了包含数据存储器地址外,还包含外部I/O口的地址。对外部数据存储器及外部I/O口的寻址将在本章的绝对地址寻址中详细介绍。

(5)程序存储区CODE。程序存储区CODE声明的标识符为code,储存的数据是不可改变的。在C51编译器中可以用存储区类型标识符code来访问程序存储区。

声明举例如下:

unsigned char code a[ ]={0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};

上面介绍了C51的数据存储类型,C51数据存储类型及其大小和值域如表3-3所示。

单片机读写片内RAM比读写片外RAM的速度相对快一些,所以应当尽量把频繁使用的变量置于片内RAM,即采用data、bdata或idata存储类型,而将容量较大的或使用不太频繁的那些变量置于片外RAM,即采用pdata或xdata存储类型。常量只能采用code存储类型。

表3-3 C51存储类型及其大小和值域

存储类型

长度/bit

长度/byte

值域

data

8

1

0~255

idata

8

1

0~255

bdata

1

 

0或1

pdata

8

1

0~255

xdata

16

2

0~65 535

code

16

2

0~65 535

变量存储类型定义举例:

(1)char data a1;     //字符变量a1被定义为data型,分配在片内RAM低128字节中。
(2)float idata x,y;    //浮点变量x和y被定义为idata型,定位在片内RAM中,只能用
                     //间接寻址方式寻址。
(3)bit bdata p;     //位变量p被定义为bdata型,定位在片内RAM中的位寻址区。
(4)unsigned int pdata var1; //无符号整型变量var1被定义为pdata型,定位在片外RAM中,
                       //相当于使用@Ri间接寻址。
(5)unsigned char xdata a[2] [4]; //无符号字符型二维数组变量a[2][4]被定义为xdata存储类型,  
                        //定位在片外RAM中,占据2*4=8字节,相当于使用@DPTR间接寻址。

4.数据存储模式

如果在变量定义时略去存储类型标识符,编译器会自动默认存储类型。默认的存储类型进一步由SMALL、COMPACT和LARGE存储模式指令限制。例如,若声明char var1,则在使用SMALL存储模式下,var1被定位在data存储区,在使用COMPACT模式下,var1被定位在idata存储区;在LARGE模式下,var1被定位在xdata存储区中。

下面对存储模式作进一步的说明。

(1)SMALL模式。在该模式下,所有变量都默认位于8051单片机内部的数据存储器内,这与使用data指定存储器类型的方式一样。在此模式下,变量访问的效率高,但是所有数据对象和堆栈必须使用内部RAM。

(2)COMPACT模式。本模式下的所有变量都默认在外部数据存储器的1页(256字节)内,这与使用pdata指定存储器类型是一样的。该存储器类型适用于变量不超过256字节的情况,此限制是由寻址方式决定的,相当于使用数据指针@Ri进行寻址。与SMALL模式相比,该存储模式的效率比较低,对变量访问的速度也慢一些,但比LARGE模式快。

(3)LARGE模式。在LARGE模式下,所有变量都默认位于外部数据存储器,相当于使用数据指针@DPTR进行寻址。通过数据指针访问外部数据存储器的效率较低,特别是当变量为2字节或更多字节时,该模式要比SMALL和COMPACT产生更多的代码。

在固定的存储器地址上进行变量的传递,是C51的标准特征之一。在SMALL模式下,参数传递是在片内数据存储区中完成的。LARGE和COMPACT模式允许参数在外部存储器中传递。C51也支持混合模式,例如,在LARGE模式下,生成的程序可以将一些函数放入SMALL模式中,从而加快执行速度。

3.2.2 C51语言的特殊功能寄存器及位变量定义

下面介绍C51如何对8051的特殊功能寄存器以及位变量进行定义并访问。

1.特殊功能寄存器的C51定义

C51语言允许通过使用关键字sfr、sbit或直接引用编译器提供的头文件来对特殊功能寄存器(SFR)进行访问,8051单片机的特殊功能寄存器分布在片内RAM的高128字节中,对SFR的访问只能采用直接寻址方式。

(1)使用关键字定义sfr。为了能直接访问特殊功能寄存器SFR,C51提供了一种定义方法,即引入关键字sfr,语法如下:

sfr 特殊功能寄存器名字=特殊功能寄存器地址;

例如:

sfr IE=0xA8;     //中断允许寄存器地址A8H
sfr TCON=0x88;    //定时器/计数器控制寄存器地址88H
sfr SCON=0x98;    //串行口控制寄存器地址98H

在8051单片机中,如要访问16位SFR,可使用关键字sfr16,16位SFR的低字节地址必须作为“sfr16”的定义地址,例如:

sfr16 DPTR=0x82  //数据指针DPTR 的低8位地址为82H,高8位地址为83H

(2)通过头文件访问SFR。各种衍生型的8051单片机的特殊功能寄存器的数量与类型有时是不相同的,对单片机特殊功能寄存器的访问可通过头文件的访问来进行。

为了用户处理方便,C51把8051单片机(或8052单片机)常用的特殊功能寄存器和其中的可寻址位进行了定义,放在一个reg51.h(或reg52.h)的头文件中。当用户要使用时,只需在使用之前用一条预处理命令#include<reg51.h>把头文件“reg51..h”包含到程序中,就可以使用特殊功能寄存器名和其中的可寻址位名称了。用户可在Keil环境下打开该头文件查看其内容,也可通过文本编辑器对头文件进行增减。

G:\注意.tif

在程序中加入头文件有两种书写方法,分别为#include<reg51.h>和#include"reg51.h",包含头文件时不需要在后面加分号。

当使用< >包含头文件时,编译器先进入到软件安装文件夹处开始搜索该头文件,也就是Keil/C51/INC这个文件夹下,如果这个文件夹下没有引用的头文件,编译器将会报错。

当使用" "包含头文件时,编译器先进入到当前工程所在文件夹处开始搜索该头文件,如果当前工程所在文件夹下没有该头文件,编译器将继续回到软件安装文件夹处搜索该头文件,若找不到该头文件,编译器将报错。reg51.h在软件安装文件夹处存在,所以一般写成#include<reg51.h>。

头文件引用举例如下:

#include<reg51.h>   //包含8051单片机的头文件
void main(void)
{ 
   TL0=0xf0;    //给定时器T0低字节TL0设置时间常数,已在reg51.h中定义
   TH0=0x3f;    //给定时器T0高字节TH0设置时间常数,已在reg51.h中定义
   TR0=1;      //启动定时器0
   ……
}

(3)特殊功能寄存器中的位定义。对SFR中的可寻址位的访问,要使用关键字来定义可寻址位,定义方法共有3种。

① sbit 位名=特殊功能寄存器^位置;

例如:

sfr PSW=0xd0;        //定义PSW 寄存器的字节地址0xd0
sbit CY= PSW^7;      //定义CY位为PSW.7,地址为0xd0
sbit OV= PSW^2;      //定义OV位为PSW.2,地址为0xd2

② sbit 位名=字节地址^位置;

例如:

sbit CY= 0xd0^7;    // CY位地址为0xd7
sbit OV= 0xd0^2;    // OV位地址为0xd2

③ sbit 位名=位地址;

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

例如:

sbit CY= 0xd7;     // CY位地址为0xd7
sbit OV= 0xd2;     // OV位地址为0xd2

【例3-1】 AT89S51单片机片内P1口的各寻址位的定义如下:

sfr  P1=0x90;  
sbit P1_7= P1^7;  
sbit P1_6= P1^6;  
sbit P1_5= P1^5;  
sbit P1_4= P1^4;  
sbit P1_3= P1^3;  
sbit P1_2= P1^2;  
sbit P1_1= P1^1;  
sbit P1_0= P1^0;

2.位变量的C51定义

(1)C51的位变量定义。由于8051单片机能够进行位操作,因此C51扩展的“bit”数据类型可用来定义位变量,这是C51与标准C的不同之处。

C51采用关键字“bit”来定义位变量,一般格式为:

bit bit_name;

例如:

bit ov_flag;       //将ov_flag定义为位变量
bit lock_pointer;    //将lock_pointer定义为位变量

(2)C51的函数可包含类型为“bit”的参数,也可将其作为返回值。例如:

bit func(bit b0, bit b1);   //位变量b0与b1作为函数func的参数
{  
   ……
   return(b1);           //位变量b1作为return函数的返回值
}

(3)位变量定义的限制。位变量不能用来定义指针和数组。例如:

bit *ptr;      // 错误,不能用位变量来定义指针
bit array[ ] ;   // 错误,不能用位变量来定义数组array[ ]

在定义位变量时,允许定义存储类型,位变量都被放入一个位段,此段总是位于8051单片机的片内RAM中,因此其存储类型限制为DATA或IDATA,如果将位变量定义成其他类型都会导致编译时出错。

3.2.3 C51语言的绝对地址访问

如何对8051单片机的片内RAM、片外RAM及I/O空间进行访问,C51提供了两种常用的访问绝对地址的方法。

1.绝对宏

C51编译器提供了一组宏定义来对code、data、pdata和xdata空间进行绝对寻址。在程序中,用“#include<absacc.h>” 来对absacc.h中声明的宏来访问绝对地址,包括CBYTE、CWORD、DBYTE、DWORD、XBYTE、XWORD、PBYTE、PWORD,具体使用方法参考absacc.h头文件。其中:

  • CBYTE以字节形式对code区寻址;
  • CWORD以字形式对code区寻址;
  • DBYTE以字节形式对data区寻址;
  • DWORD以字形式对data区寻址;
  • XBYTE以字节形式对xdata区寻址;
  • XWORD以字形式对xdata区寻址;
  • PBYTE以字节形式对pdata区寻址;
  • PWORD以字形式对pdata区寻址。

例如:

#include<absacc.h>
#define PORTA XBYTE[0xffc0]  //将PORTA定义为外部I/O口,地址为0xffc0,长度8位
#define NRAM DBYTE[0x50]    //将NRAM定义为片内RAM,地址为0x50,长度8位

【例3-2】 片内RAM、片外RAM及I/O的定义的程序如下:

#include<absacc.h>
#define PORTA XBYTE[0xffc0]   //将PORTA定义为外部I/O口,地址为0xffc0
#define NRAM DBYTE[0x40]     //将NRAM定义为片内RAM,地址为0x40
main( )
{ 
   PORTA=0x3d;         //将数据3DH写入地址为0xffc0的外部I/O端口PORTA 
   NRAM=0x01;          //将数据01H写入片内RAM的0x40单元
}

2._at_ 关键字

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

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

其中,存储器类型为C51能识别的数据类型;数据类型为C51支持的数据类型;地址常数用于指定变量的绝对地址,必须位于有效的存储器空间之内;使用 _at_ 定义的变量必须为全局变量。

【例3-3】 使用关键字 _at_ 实现绝对地址的访问,程序如下:

void main(void)
{   
   data unsigned char y1 _at_ 0x50;   //在data区定义字节变量y1,它的地址为50H
   xdata unsigned int y2 _at_ 0x4000;  //在xdata区定义字变量y2,地址为4000H
   y1=0xff;
   y2=0x1234;
  ……
  while(1);
}

【例3-4】 将片外RAM 2000H开始的连续20字节单元清零,程序如下:

xdata unsigned char buffer[20] _at_ 0x2000;
void main(void)
{  
   unsigned char i;
   for(i=0; i<20; i++)
   {  
     buffer[i]=0
   }
}

如果把片内RAM 40H单元开始的8个单元内容清零,则程序如下:

xdata unsigned char buffer[8] _at_ 0x40;
void main(void)
{ 
   unsigned char j ;
   for(j=0;j<8;j++)
   {  
     buffer[j]=0
   }
}

3.2.4 C51的基本运算

C51的基本运算与标准C类似,主要包括算术运算、关系运算、逻辑运算、位运算和赋值运算及其表达式等。

1.算术运算符

算术运算的算术运算符及其说明如表3-4所示。

表3-4 算术运算符及其说明

符  号

说  明

举例(设x=10, y=3)

+

加法运算

z=x+y;  //z=13

-

减法运算

z=x-y;  //z=7

*

乘法运算

z=x*y;  //z=30

/

除法运算

z=x/y;  //z=3

%

取余数运算

z=x%y; //z=1

++

自增1

 

--

自减1

 

C51中表示加1和减1时可采用自增运算符和自减运算符,自增和自减运算符是使变量自动加1或减1,自增和自减运算符放在变量前和变量后是不同的,如表3-5所示。

表3-5 自增运算符与自减运算符

运 算 符

说  明

举例(设x初值为4)

x++

先用x的值,再让x加1

y=x++;  // y为4,x为5

++x

先让x加1,再用x的值

y=++x;  // y为5,x为5

x−−

先用x的值,再让x减1

y=x−−;  // y为4,x为3

−−x

先让x减1,再用x的值

y=−−x;  // y为3,x为3

2.逻辑运算符

逻辑运算的结果只有“真”和“假”两种,“1”表示真,“0”表示假。表3-6列出了逻辑运算符及其说明。

例如条件“10>20”为假,“2<6” 为真,则逻辑与运算(10>20)&&(2<6)=0&&1=0。

表3-6 逻辑运算符及其说明

运 算 符

说  明

举例(设a=2,b=3)

&&

逻辑与

a&&b;   //返回值为1

逻辑或

a‖b;   //返回值为1

!

逻辑非(求反)

! a;    //返回值为0

3.关系运算符

关系运算符就是判断两个数之间的关系。关系运算符及其说明如表3-7所示。

表3-7 关系运算符及其说明

符  号

说  明

举例(设a=2,b=3)

>

大于

a> b;//返回值为0

<

小于

a< b;//返回值为1

>=

大于等于

a>=b;//返回值为0

<=

小于等于

a<=b;//返回值为1

==

等于

a==b;//返回值为0

!=

不等于

a!=b;//返回值为1

4.位运算

位运算符及其说明如表3-8所示。

表3-8 位运算及其说明

符  号

说  明

举例

&

按位逻辑与

0x19&0x4d=0x09

按位逻辑或

0x19|0x4d =0x5d

^

按位异或

0x19^0x4d =0x54

按位取反

x=0x0f,则~x=0xf0

<<

按位左移(高位丢弃,低位补0)

y=0x3a,若y<<2,则y=0xe8

>>

按位右移(高位补0,低位丢弃)

w=0x0f,若w>>2,则w=0x03

在实际的控制应用中,人们常常想要改变I/O口中的某一位的值,而不影响其他位,如果I/O口是可位寻址的,那么这个问题就很简单。但有时外扩的I/O口只能进行字节操作,因此要想在这种场合下实现单独的位控,就要采用位操作。

【例3-5】 编写程序将扩展的某I/O 口 PORTA(只能字节操作)的PORTA.5清零,PORTA.1置为1,程序如下:

#define <absacc.h>           //定义片外 I/O 口变量PORTA要用到头文件absacc.h
#define PORTA XBYTE[0xffc0]    //定义了一个片外 I/O 口变量PORTA
void main( )
{ 
   ……
   PORTA=( PORTA&0xdf)│0x02;
   ……
}

上面程序段中,第2行定义了一个片外 I/O 口变量PORTA,其地址为片外数据存储区的0xffc0。在main( )函数中,“PORTA=( PORTA&0xdf)│0x02”的作用是先用运算符“&”将PORTA.5置成0,然后再用“│0x02”运算将PORTA.1置为1。

5.指针和取地址运算符

指针是C51语言中一个十分重要的概念,将在本章后面介绍。C51的指针变量,用于存储某个变量的地址,C51用“*”和“&”运算符来提取变量的内容和变量的地址,如表3-9所示。

表3-9 赋值、指针和取值运算及其说明

符  号

说  明

*

提取变量的内容

&

提取变量的地址

提取变量的内容和变量的地址的一般形式分别为:

目标变量=*指针变量    //将指针变量所指的存储单元内容赋值给目标变量
指针变量=&目标变量    //将目标变量的地址赋值给指针变量

例如:

a=&b;      //取b变量的地址送至变量a
c=*b;      //把以指针变量b为地址的单元内容送至变量c

指针变量中只能存放地址(即指针型数据),不能将非指针类型的数据赋值给指针变量。例如:

int i ;   //定义整型变量i
int *b;   //定义指向整数的指针变量b
b=&i;     //将变量i的地址赋给指针变量b
b=i;      //错误,指针变量b只能存放变量指针(变量的地址),不能存放变量i的值

3.2.5 C51的分支与循环程序结构

C51的程序按结构可分为3类,即顺序、分支和循环结构。顺序结构是程序自上而下,从main( )的函数开始一直到程序运行结束,程序只有一条路可走,无其他路径可选择。顺序结构比较简单和便于理解,这里仅介绍分支结构和循环结构。

1.分支控制语句

实现分支控制的语句有:if语句和switch语句。

(1)if语句是用来判定所给定的条件是否满足,根据判定结果决定执行两种操作之一。

if语句的基本结构如下:

if (表达式) {语句}

括号中的表达式成立时,程序执行大括号内的语句,否则程序将跳过大括号中的语句部分,而直接执行下面的其他语句。

C51提供3种形式的if语句。

形式1:

if (表达式) {语句}

例如:

if (x>y) {max=x; min=y;}

即如果x>y,则x赋给max,y赋给min。如果x>y不成立,则不执行大括号中的赋值运算。

形式2:

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

例如:

if (x>y)
{max=x; }
else {min=y;}

本形式相当于双分支选择结构。

形式3:

if (表达式1) {语句1;}
else if (表达式2) {语句2;}
else if (表达式3) {语句3;}
……
else {语句n;}

例如:

if (x>100) {y=1;}
else if (x>50) {y=2;}
else if (x>30) {y=3;}
else if (x>20) {y=4;}
else {y=5;}

本形式相当于串行多分支选择结构。

在if语句中又含有一个或多个if语句,这称为if语句的嵌套。应当注意if与else的对应关系,else总是与它前面最近的一个if语句相对应。

(2)switch语句。if语句只有两个分支可供选择,而switch语句是多分支选择语句。switch语句的一般形式如下:

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

上述switch语句的说明如下。

① 每一个case的常量表达式必须是互不相同的,否则将出现混乱。

② 各个case和default出现的次序,不影响程序执行的结果。

③ switch括号内表达式的值与某case后面的常量表达式的值相同时,就执行它后面的语句,遇到break语句则退出switch语句。若所有的case中的常量表达式的值都没有与switch语句表达式的值相匹配时,就执行default后的语句。

④ 如果在case语句中遗忘了break语句,则程序执行了本行之后,不会按规定退出switch语句,而是将执行后续的case语句。在执行1个case分支后,使流程跳出switch结构,即中止switch语句的执行,可以用1条break语句完成。switch语句的最后一个分支可不加break语句,结束后直接退出switch结构。

【例3-6】 在单片机程序设计中,常用switch语句作为键盘中按键按下的判别,并根据按下键的键号跳向各自的分支处理程序。

input: keynum=keyscan( )
switch(keynum)
{
   case 1: key1( ); break;   //如果按下键的键值为1,则执行函数key1( )
   case 2: key2( ); break;   //如果按下键的键值为2,则执行函数key2( )
   case 3: key3( ); break;   //如果按下键的键值为3,则执行函数key3( )
   case 4: key4( ); break;   //如果按下键的键值为4,则执行函数key4( )
   ……
   default:goto input
}

例子中的keyscan( )为键盘扫描函数,如果有键按下,该函数就会得到按下按键的键值,将键值赋予变量keynum。如果键值为2,则执行键值处理函数key2( )后返回;如果键值为4,则执行key4( )函数后返回。执行完1个键值处理函数后,则跳出switch语句,从而达到根据按下的不同按键,来进行不同键值处理的目的。

2.循环控制语句

许多实用程序都包含循环结构,熟练掌握和运用循环结构的程序设计是C51语言程序设计的基本要求。

实现循环结构的语句有以下3种:while语句、do-while语句和for语句。

(1)while语句。while语句的语法形式为:

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

表达式是while循环能否继续的条件,如果表达式为真,就重复执行循环体语句;反之,则终止循环体内的语句。

while循环结构的特点在于,循环条件的测试在循环体的开头,要想执行重复操作,首先必须对循环条件测试,如果条件不成立,则不执行循环体内的操作。

例如:

while((P1&0x80)==0)
{ }

while中的条件语句对AT89S8051单片机的P1口P1.7进行测试,如果P1.7为低电平(0),则由于循环体无实际操作语句,故继续测试下去(等待),一旦P1.7的电平变高(1),则循环终止。

(2)do-while语句。do while语句的语法形式为:

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

do-while语句的特点是先执行内嵌的循环体语句,再计算表达式,如果表达式的值为非0,则继续执行循环体语句,直到表达式的值为0时结束循环。

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

与while循环一样,在do-while循环体中,要有能使while后表达式的值变为0的操作,否则,循环会无限制地进行下去。根据经验,do-while循环用的并不多,大多数的循环用while来实现会更直观。

【例3-7】 实型数组sample存有10个采样值,编写程序段,要求返回其平均值(平均值滤波)。程序如下:

float avg(float *sample)
{ 
   float sum=0;
   char n=0;
   do 
   { 
     sum+=sample[n];
     n++;
   } while(n<10);
   return(sum/10);
}

(3)基于for语句的循环。在3种循环中,经常使用的是for语句构成的循环,它不仅可用于循环次数已知的情况,也可用于循环次数不确定而只给出循环条件的情况,完全可以替代while语句。

for循环的一般格式为:

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

for是C51的关键字,其后的括号中通常含有3个表达式,各表达式之间用“;”隔开。这3个表达式可以是任意形式的表达式,通常主要用于for循环的控制。紧跟在for()之后的循环体,在语法上要求是 1 条语句;若在循环体内需要多条语句,应该用大括号括起来组成复合语句。

for的执行过程如下:

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

② 计算“表达式2”,表达式2通常称为“终值条件表达式”,若满足条件,转下一步,若不满足条件,则转步骤⑤。

③ 执行1次for循环体。

④ 计算“表达式3”,“表达式3”通常称为“更新表达式”转向步骤②。

⑤ 结束循环,执行for循环之后的语句。

下面对for语句的几个特例进行说明。

① for语句中的小括号内的3个表达式全部为空。

例如:

for(;;)
{ 
   循环体语句;
}

在小括号内只有两个分号,无表达式,这意味着没有设初值,无判断条件,循环变量为增值,它的作用相当于while(1),这将导致一个无限循环。一般在编程时,需要无限循环时,可采用这种形式的for循环语句。

② for语句的3个表达式中,表达式1缺省。

例如:

for(;i<=100;i++)sum=sum+i;

即不对i设初值。

③ for语句的3个表达式中,表达式2缺省。

例如:

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

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

④ for语句的3个表达式中,表达式1、表达式3省略。

例如:

for(;i<=100;) 
{ 
   sum=sum+i;
   i++;
}

⑤ 没有循环体的for语句。

例如:

int a=1000;
for(t=0;t<a;t++) 
{;}

本例的一个典型应用就是软件延时。

在程序设计中,常用到时间延时,此时就可用循环结构来实现,即循环执行指令,消磨一段已知的时间。8051单片机指令的执行时间是靠一定数量的时钟周期来计时的,如果使用12MHz晶体振荡器,则12个时钟周期花费的时间为1µs。

【例3-8】 编写一个延时1ms的程序。

void delayms( unsigned char int j)
{
   unsigned char i;
   while(j--)
   {
     for(i=0;i<125;i++)
     {;}
   }
}

如果把上述程序段编译成汇编语言代码进行分析,用for进行的内部循环大约延时8µs,但不是特别精确。不同的编译器会产生不同的延时,因此i的上限值125应根据实际情况进行补偿调整。

【例3-9】 求1+2+3…+100的累加和。

用for语句编写的程序如下:

#include <reg51.h>
#include <stdio.h>
main( )
{
   int nvar1, nsum;
   for(nvar1=0,nsum=1;nsum<=100;nsum++)
   nvar1+=ncount;       //累加求和
   while(1);
}

【例3-10】 无限循环结构的实现。

编写无限循环程序段,可使用以下3种结构。

① 使用while(1)的结构:

while(1) 
{
   代码段; 
}

② 使用for(;;)的结构:

for(;;)
{
   代码段; 
}

③ 使用do-while(1)的结构:

do  
{
   代码段;
} while(1);

3.break语句、continue语句和goto语句

在循环体语句执行过程中,如果在满足循环判定条件的情况下跳出代码段,可使用 break语句或continue语句;如果要从任意地方跳转到代码的某个地方,可以使用goto语句。

(1)break语句。前面已经介绍过用break语句可以跳出switch循环体。在循环结构中,可使用break语句跳出本层循环体,从而马上结束本层循环。

【例3-11】 执行如下程序段。

void main(void )                //主函数main( )
{ 
   int i, sum;
   sum=0;
   for(i=1;i<=10;i++)
   { 
     sum=sum+i;
     if(sum>5) break;
     printf("sum=%d\n", sum);  //通过串口向计算机屏幕输出显示sum值
   }
}

本例中,如果没有break语句,程序将进行10次循环;当i=3时,sum的值为6,此时,if语句的表达式“sum>5”的值为1,于是执行break语句,跳出for循环,从而提前终止循环。因此在一个循环程序中,既可通过循环语句中的表达式来控制循环是否结束,还可直接通过break语句强行退出循环结构。

(2)continue语句。continue语句的作用及用法与break语句类似,区别在于:当前循环遇到break,是直接结束循环,若遇上continue,则是停止当前这一层循环,然后直接尝试下一层循环。可见,continue并不结束整个循环,而仅仅是中断当前这一层循环,然后跳到循环条件处,继续下一层的循环。当然,如果跳到循环条件处,发现条件已不成立,那么循环也会结束。

【例3-12】 输出整数1~100的累加值,但要求跳过所有个位为3的数。

为完成题目要求,在循环中加一个判断,如果该数个位是3,就跳过该数不加。如何来判断1~100的数中哪些位的个数是3呢?用求余数的运算符“%”,将一个2位以内的正整数,除以10后,余数是3,就说明这个数的个位为3。例如对于数73,除以10后,余数是3。根据以上分析,参考程序如下:

void main(void )
{
   int i, sum=0;
   sum=0;
   for(i=1;i<=100;i++)
   { 
     if(i%10==3)
     continue;
     sum=sum+i; 
   }
     printf("sum=%d\n", sum);    //在计算机屏幕显示sum值
}

(3)goto语句是一个无条件转移语句,当执行goto语句时,将程序指针跳转到goto给出的下一条代码。基本格式如下:

goto  标号

【例3-13】 计算整数1~100的累加值,存放到sum中。

void main(void )    
{ 
   unsigned char i
   int sum;
   sumadd:
   sum=sum+i;
   i++;
   if(i<101)
   { 
     goto sumadd;
   }
}

goto语句在C51中经常用于无条件跳转某条必须执行的语句以及用于在死循环程序中退出循环。为方便阅读,也为了避免跳转时引发错误,在程序设计中要慎重使用goto语句。

3.2.6 C51的数组

在C51程序设计中,数组的使用较为广泛。

1.数组简介

数组是同类数据的一个有序结合,用数组名来标识。整型变量的有序结合称为整型数组,字符型变量的有序结合称为字符型数组。数组中的数据,称为数组元素。

数组中各元素的顺序用下标表示,下标为n的元素可表示为数组名[n]。改变[ ]中的下标就可以访问数组中的所有元素。

数组有一维、二维、三维和多维数组之分,C51中常用的有一维数组、二维数组和字符数组。

(1)一维数组

具有一个下标的数组元素组成的数组称为一维数组,一维数组的形式如下:

类型说明符 数组名[元素个数];

其中,数组名是一个标识符,元素个数是一个常量表达式,不能是含有变量的表达式。

例如:

int array1[8]

定义了一个名为array1的数组,数组包含8个整型元素,在定义数组时,可对数组进行整体初始化,若定义后对数组赋值,则只能对每个元素分别赋值。例如:

int a[3]={2,4,6};    //给全部元素赋值,a[0]=2,a[1]=4,a[2]=6
int b[4]={5,4,3,2};   //给全部元素赋值,b[0]=5,b[1]=4,b[2]=3,b[3]=2

(2)二维数组或多维数组

具有两个或两个以上下标的数组称为二维数组或多维数组。定义二维数组的一般形式如下:

类型说明符 数组名[行数] [列数];

其中,数组名是一个标识符,行数和列数都是常量表达式。例如:

float array2 [4] [3]   //array2数组,有4行3列共12个浮点型元素

二维数组可以在定义时进行整体初始化,也可在定义后单个进行赋值。例如:

int a[3] [4]={1,2,3,4},{5,6,7,8},{9,10,11,12};  //a数组全部初始化
int b[3] [4]={1,3,5,7},{2,4,6,8},{ };         // b数组部分初始化,未初始化的元素为0

(3)字符数组

若一个数组的元素是字符型的,则该数组就是一个字符数组。例如:

char  a[10]= {'B', 'E', 'I', ' ', 'J', 'I','N','G','\0'}; //字符串数组

定义了一个字符型数组a[ ],它有10个数组元素,并且将9个字符(其中包括1个字符串结束标志 '\0' )分别赋给了a[0]~a[8],剩余的a[9]被系统自动赋予空格字符。C51还允许用字符串直接给字符数组置初值,例如:

char a[10]= {"BEI JING"};

用双引号括起来的一串字符称为字符串常量,C51编译器会自动地在字符串末尾加上结束符'\0'。

用单引号括起来的字符为字符的ASCII码值,而不是字符串。例如,‘a’表示a的ASCII码值61H,而“a”表示一个字符串,由两个字符a和\0组成。

一个字符串可用一维数组来装入,但数组的元素数目一定要比字符多一个,以便C51编译器自动在其后面加入结束符‘\0’。

2.数组的应用

在C51编程中,数组的查表功能非常有用,如数学运算,编程者更愿意采用查表计算而不是公式计算。例如,对于传感器的非线性转换需要进行补偿,使用查表法就要有效得多。再如,LED显示程序中根据要显示的数值,找到对应的显示段码送到LED显示器显示。表可以事先计算好后装入程序存储器中。

【例3-14】 使用查表法,计算数0~9的平方。

#define uchar unsigned char
uchar code square[ ]={ 0,1,4,9,16,25,36,49,64,81}; //0~9的平方表,存储在程序存储器中
uchar fuction(uchar number) 
{
   return square[number]   // 返回平方数
};        
main( ) 
{ 
   result=fuction(7);      // 函数fuction( )的实际参数为7,其平方49存入result单元 
}

在程序的开始处,“uchar code square[ ]={ 0,1,4,9,16,25,36,49,64,81};”定义了一个无符号字符型的数组square[ ],并对其进行了初始化,将数0~9的平方值赋予了数组square[ ],类型代码code指定编译器将平方表定位在程序存储器中。

主函数调用函数fuction( ),假设得到实际参数为7;从square数组中查表获得相应的求得其平方的数为49。执行result= fuction(7)后,result的结果为相应的平方数49。

3.数组与存储空间

当程序中设定了一个数组时,C51编译器就会在系统的存储空间中开辟一个区域,用于存放数组的内容。数组就包含在这个由连续存储单元组成的模块的存储体内。对字符数组而言,它占据了内存中一连串的字节位置。对整型(int)数组而言,将在存储区中占据一连串连续的字节对的位置。对长整型(long)数组或浮点型(float)数组,一个成员将占有4字节的存储空间。

当一维数组被创建时,C51编译器就会根据数组的类型在内存中开辟一块大小等于数组长度乘以数据类型长度(即类型占有的字节数)的区域。

对于二维数组a[m] [n]而言,其存储顺序是按行存储,先存第0行元素的第0列、第1列、第2列,直至第n-1列,然后返回到存第1行元素的第0列、第1列、第2列,直至第n-1列……如此顺序存储,直到第m-1行的第n-1列。

当数组特别是多维数组中大多数元素没有被有效地利用时,就会浪费大量的存储空间。对于8051单片机,其存储资源极为有限,因此在进行C51编程开发时,要仔细地根据需要来选择数组的大小。

3.2.7 C51的指针

C51支持两种不同类型的指针:通用指针和存储器指针。

1.通用指针

C51提供一个3字节的通用指针,通用指针声明和使用与标准C语言完全一样。通用指针的形式如下:

数据类型 *指针变量;

例如:

uchar *pz

例中pz就是通用指针,用3字节来存储指针,第一字节表示存储器类型,第二、三字节分别是指针所指向数据地址的高字节和低字节,这种定义很方便但速度较慢,在所指向的目标存储器空间不明确时普遍使用。

2.存储器指针

存储器指针在定义时指明了存储器类型,并且指针总是指向特定的存储器空间(片内数据RAM、片外数据RAM或程序ROM)。例如:

char xdata *str;    // str指向xdata区中的char型数据
int xdata *pd;     // pd指向外部RAM区中的int型整数

由于定义中已经指明了存储器类型,因此,相对于通用指针而言,指针第一个字节省略,对于data、bdata、idata与pdata存储器类型,指针仅需要1B,因为它们的寻址空间都在256B以内,而code和xdata存储器类型则需要2B指针,因为它们的寻址空间最大为64KB。

使用存储器指针好处是节省了存储空间,编译器不用为存储器选择和决定正确的存储器操作指令来产生代码,这使代码更加简短,但必须保证指针不指向所声明的存储区以外的地方,否则会产生错误。通用指针产生的代码执行速度比指定存储区的指针要慢,因为存储区在运行前是未知的,编译器不能优化存储区访问,必须产生可以访问任何存储区的通用代码。

由上所述可知,使用存储器指针比使用通用指针效率高,存储器指针所占空间小,速度更快,在存储器空间明确时,建议使用存储器指针,如果存储器空间不明确,则使用通用指针。

3.3 C51语言的函数

函数是一个完成一定相关功能的执行代码段。在高级语言中,函数与另外两个名词“子程序”和“过程”用来描述同样的事情,在C51中使用的术语是“函数”。

C51程序中函数数目是不受限制的,但是一个C51程序必须至少有1个函数,即主函数,名称为main。主函数是唯一的,整个程序必须从主函数开始执行。

C51还可以建立和使用库函数,可由用户根据需求调用。

3.3.1 函数的分类

从结构上分,C51中函数可分为主函数main( )和普通函数两种。而普通函数从编程者的角度又可以划分为两种:标准库函数和用户编写的自定义函数。

1.标准库函数

标准库函数由C51编译器提供,编程者在进行程序设计时,应该善于充分利用这些功能强大、资源丰富的标准库函数资源,以提高编程效率。

用户可以直接调用C51的库函数而不需要为这个函数写任何代码,只需要包含具有该函数说明的头文件即可。例如调用输出函数printf时,要求程序在调用输出库函数前包含以下的include 命令:

#include <stdio.h>

2.用户自定义函数

用户自定义函数是用户根据自己的需要所编写的函数。从函数定义的形式上来分,可分为:无参函数、有参函数和空函数。

(1)无参函数。

此种函数在被调用时,既无参数输入,也不返回结果给调用函数,只是为完成某种操作而编写的函数。

无参函数的定义形式为:

返回值类型标识符 函数名( )
{
   函数体;
}

无参函数一般不带返回值,因此函数的返回值类型的标识符可以省略。

例如,函数main( ),该函数为无参函数,返回值类型的标识符可以省略,默认值是int类型。

(2)有参函数。

调用此种函数时,必须提供实际的输入函数。有参函数的定义形式为:

返回值类型标识符 函数名(形式参数列表)
形式参数说明
{
   函数体;
}

【例3-15】 定义一个函数max( ),用于求两个数中较大的数。

int a,b
int max(a, b)
{ 
   if(a>b)return(a);
   else return(b);
}

上面的程序段中,a、b为形式参数。return( )为返回语句。

(3)空函数。

函数体内无语句,调用空函数时,什么工作也不做,不起任何作用。定义空函数的目的,并不是为了执行某种操作,而是为了以后程序功能的扩充。例如,先将一些基本模块的功能函数定义成空函数,占好位置,并写好注释,以后再用一个编写好的函数代替它。这样整个程序的结构清晰,可读性好,为以后扩充新功能提供方便。

空函数的定义形式为:

返回值类型标识符 函数名( )
{  }

例如:

float min(  )
{  }      //空函数,占好位置

3.3.2 函数的参数与返回值

1.函数的参数

C语言采用函数之间的参数传递方式,使一个函数能对不同的变量进行功能相同的处理,从而大大提高了函数的通用性与灵活性。

函数之间的参数传递,是由调用函数的实际参数与被调用函数的形式参数之间进行数据传递来实现。被调用函数的最后结果由被调用函数的return语句返回给调用函数。

函数的参数包括形式参数和实际参数。

(1)形式参数:函数的函数名后面括号中的变量名称为形式参数,简称形参。

(2)实际参数:在函数调用时,主调函数名后面括号中的表达式称为实际参数,简称实参。

在C语言的函数调用中,实际参数与形式参数之间的数据传递是单向进行的,只能由实际参数传递给形式参数,而不能由形式参数传递给实际参数。

实际参数与形式参数的类型必须一致,否则会发生类型不匹配的错误。被调用函数的形式参数在函数未调用之前,并不占用实际内存单元。只有当函数调用发生时,被调用函数的形式参数才分配给内存单元,此时内存中调用函数的实际参数和被调用函数的形式参数位于不同的单元。在调用结束后,形式参数所占有的内存被系统释放,而实际参数所占有的内存单元仍保留并维持原值。

2.函数的返回值

函数的返回值是通过函数中的return语句获得的。1个函数可以有1个以上的return语句,但是多于1个的return语句必须在选择结构(if或do/case)中使用(如前面求两个数中的大数函数max( )的例子),因为被调用函数一定只能返回1个变量。

函数返回值的类型一般在定义函数时,由返回值的标识符来指定。如在函数名之前的int指定函数的返回值的类型为整型数(int)。若没有指定函数的返回值类型,默认返回值为整型类型。

当函数没有返回值时,则使用标识符void进行说明。

3.3.3 函数的调用

在一个函数中需要用到某个函数的功能时,就调用该函数。调用者称为主调函数,被调用者称为被调函数。

1.函数调用的一般形式

函数调用的一般形式为:

函数名 {实际参数列表};

若被调函数是有参函数,则主调函数必须把被调函数所需的参数传递给被调函数。传递给被调函数的数据称为实际参数(简称实参),实参必须与形参的数据在数量、类型和顺序上都一致。实参可以是常量、变量和表达式。实参对形参的数据传递是单向的,即只能将实参传递给形参。

2.函数调用的方式

主调函数对被调函数的调用有以下3种方式。

(1)函数调用语句把被调用函数的函数名作为主调函数的一个语句,例如:

print_message( );

此时,并不要求函数返回结果数值,只要求函数完成某种操作。

(2)函数结果作为表达式的一个运算对象,例如:

result=2*gcd(a,b);

被调函数以一个运算对象出现在表达式中。这要求被调函数带有return语句,以便返回一个明确的数值参加表达式的运算。例中,被调函数gcd为表达式的一部分,它的返回值乘2再赋给变量result。

(3)函数参数即被调函数作为另一个函数的实际参数,例如:

m=max(a,gcd(u,v));

其中,gcd(u,v)是一次函数调用,它的值作为另一个函数的max( )的实际参数之一。

3.对调用函数的说明

在一个函数调用另一个函数时,必须具备以下条件。

(1)被调函数必须是已经存在的函数(库函数或用户自定义的函数)。

(2)如果程序中使用了库函数,或使用了不在同一文件中的另外自定义函数,则应该在程序的开头处使用#include包含语句,将所有的函数信息包含到程序中来。

例如“#include<stdio.h>”,将标准的输入、输出头文件stdio.h(在函数库中)包含到程序中来。在程序编译时,系统会自动将函数库中的有关函数调入程序中去,编译出完整的程序代码。

(3)如果程序中使用了自定义函数,且该函数与调用它的函数同在一个文件中,则应根据主调函数与被调函数在文件中的位置,决定是否对被调函数作出说明。

a.如果被调函数在主调函数之后,一般应在主调函数中,在被调函数调用之前,对被调函数的返回值类型作出说明。

b.如果被调函数出现在主调函数之前,不用对被调函数进行说明。

c.如果在所有函数定义之前,在文件的开头处,在函数的外部已经说明了函数的类型,则在主调函数中不必对所调用的函数再做返回值类型说明。

3.3.4 中断服务函数

由于标准C语言没有处理单片机中断的定义,为了能进行8051单片机的中断处理,C51编译器对函数的定义进行了扩展,增加了一个扩展关键字interrupt。使用interrupt可以将一个函数定义成中断服务函数。由于C51编译器在编译时对声明为中断服务程序的函数自动添加了相应的现场保护、阻断其他中断、返回时自动恢复现场等处理的程序段,因而在编写中断服务函数时可不必考虑这些问题,这就为用户编写中断服务程序提供了极大方便。

中断服务函数的一般形式为:

函数类型 函数名(形式参数表)interrupt n using n

关键字interrupt后的n是中断号,对于8051单片机,n的取值为0~4。

关键字using后面的 n是所选择的寄存器组,using是一个选项,可省略。如果没有使用using关键字指明寄存器组,中断函数中所有工作寄存器的内容将被保存到堆栈中。

有关中断服务函数的具体使用注意事项,将在中断系统一章中进行介绍。

3.3.5 变量及存储方式

1.变量

(1)局部变量。局部变量是某一个函数中存在的变量,它只在该函数内部有效。

(2)全局变量。在整个源文件中都存在的变量称为全局变量。全局变量的有效区间是从定义点开始到源文件结束,其中的所有函数都可直接访问该变量。如果定义前的函数需要访问该变量,则需要使用extern关键词对该变量进行说明,如果全局变量声明文件之外的源文件需要访问该变量,也需要使用extern关键词进行说明。

由于全局变量一直存在,占用了大量的内存单元,且加大了程序的耦合性,这就不利于程序的移植或复用。

全局变量可使用static关键词进行定义,该变量只能在变量定义的源文件内使用,不能被其他源文件引用,这种全局变量称为静态全局变量。如果一个其他文件的非静态全局变量需要被某文件引用,则需要在该文件调用前使用extern关键词对该变量声明。

2.变量的存储方式

单片机的存储区间可以分为程序存储区、静态存储区和动态存储区3部分,数据存放在静态存储区或动态存储区。其中全局变量存放在静态存储区,在程序开始运行时,给全局变量分配存储空间;局部变量存放在动态存储区,在进入拥有该变量的函数时,给这些变量分配存储空间。

3.3.6 宏定义与文件包含

在C51程序设计中要经常用到宏定义与文件包含。

1.宏定义

宏定义语句属于C51语言的预处理指令,使用宏可使变量书写简化,增加程序的可读性、可维护性和可移植性。宏定义分为简单的宏定义和带参数的宏定义。在C51的程序编写中,经常使用简单的宏定义。简单的宏定义格式如下:

#define 宏替换名 宏替换体

#define是宏定义指令的关键词,宏替换名一般用大写字母来表示,而宏替换体可以是数值常数、算术表达式、字符和字符串等。宏定义可以出现在程序的任何地方,例如宏定义:

#define uchar unsigned char

在编译时可由C51编译器把“unsigned char”用“uchar”来替代。

例如,在某程序的开头处,进行了3个宏定义:

#define uchar unsigned char   //宏定义无符号字符型变量方便书写
#define uint unsigned int    //宏定义无符号整型变量方便书写 
#define gain 4             //宏定义增益 
……

由上述的3个宏定义可见,宏定义不仅可以方便无符号字符型和无符号整型变量的书写(前2个宏定义),而且当增益需要变化时,只需要修改增益gain的宏替换体4即可(第3个宏定义),而不必在程序的每处修改,这便大大增加了程序的可读性和可维护性。

2.文件包含

文件包含是指一个程序文件将另一个指定文件的内容包含进去。文件包含的一般格式如下:

#include <文件名>

#include "文件名"

上述两种格式的差别是:采用<文件名>格式时,在头文件目录中查找指定文件。采用“文件名”格式时,应在当前的目录中查找指定文件。例如:

#nclude<reg51.h>   //将8051单片机的特殊功能寄存器包含文件包含到程序中来
#include<stdio.h>  //将标准的输入、输出头文件stdio.h(在函数库中)包含到程序中来
#include"stdio.h"  //同上,在当前的目录中查找指定文件stdio.h

当程序中需要调用C51语言编译器提供的各种库函数时,必须在文件的开头使用#include命令将相应函数的说明文件包含进来。

3.3.7 库函数

C51提供了丰富的可直接调用的库函数,这些库函数可使程序代码简单、结构清晰、易于调试和维护。

下面介绍几类重要的库函数。

(1)特殊功能寄存器包含文件reg51.h或reg52.h。reg51.h中包含了所有的8051的sfr及其位定义。reg52.h中包含了所有的8052的sfr及其位定义,一般系统都包含reg51.h或reg52.h。

(2)绝对地址包含文件absacc.h。该文件定义了几个宏,以确定各类存储空间的绝对地址。

(3)输入/输出流函数位于stdio.h文件中。流函数默认8051的串口来作为数据的输入/输出。如果要修改为用户定义的I/O口读写数据,如改为LCD显示,可以修改lib目录中的getkey.c及putchar.c源文件,然后在库中替换它们即可。

(4)动态内存分配函数,它位于stdlib.h中。

(5)能够方便地对缓冲区进行处理的缓冲区处理函数位于string.h中,其中包括复制、移动、比较等函数。

思考题及习题

一、填空题

1.与汇编语言相比, C51语言具有                等优点。

2.C51语言头文件包括的内容有8051单片机    ,以及    的说明。

3.C51提供了两种不同的数据存储类型        来访问片外数据存储区。

4.C51提供了code存储类型来访问    

5.对于SMALL存储模式,所有变量都默认位于8051单片机    

6.C51用“*”和“&”运算符来提取指针变量的    和指针变量的    

二、判断对错

1.C51语言处理单片机的中断是由专门的中断函数来处理的。

2.在C51语言中,函数是一个完成一定相关功能的执行代码段,它与另外两个名词“子程序”和“过程”用来描述同样的事情。

3.在C51语言编程中,编写中断服务函数时需要考虑如何进行现场保护、阻断其他中断、返回时自动恢复现场等处理的程序段的编写。

4.全局变量是在某一函数中存在的变量,它只在该函数内部有效。

5.全局变量可使用static关键词进行定义,由于全局变量一直存在,占用了大量的内存单元,且加大了程序的耦合性,不利于程序的移植或复用。

6.绝对地址包含头文件absacc.h定义了几个宏,用来确定各类存储空间的绝对地址。

三、简答题

1.C51在标准C的基础上,扩展了哪几种数据类型?

2.C51有哪几种数据存储类型?其中数据类型“idata,code,xdata,pdata”各对应AT89S51单片机的哪些存储空间

3.bit与 sbit定义的位变量有什么区别?

4.说明3种数据存储模式SMALL模式、COMPACT模式和LARGE模式之间的差别。

5.do-while构成的循环与while循环的区别是什么?

四、编程

1.编写程序,将单片机片外2000H为首地址的连续10个单元的内容,读入到片内RAM的40H~49H单元中。

2.编写将单片机片内一组RAM单元清零的函数,函数内不包括这组RAM单元的起始地址和单元个数,起始地址和单元个数参数应在执行函数前由主函数赋值。

目录

  • 版权
  • 内容提要
  • 第2版前言
  • 第1章 单片机概述
  • 第2章 AT89S51单片机片内硬件结构
  • 第3章 C51编程语言基础
  • 第4章 开发与仿真工具
  • 第5章 单片机与开关、键盘以及显示器件的接口设计
  • 第6章 中断系统的工作原理及应用
  • 第7章 定时器/计数器的工作原理及应用
  • 第8章 串行口的工作原理及应用
  • 第9章 单片机系统的并行扩展
  • 第10章 AT89S51单片机系统的串行扩展
  • 第11章 AT89S51单片机与DAC、ADC的接口
  • 第12章 单片机各种应用设计
  • 第13章 功率接口设计
  • 第14章 单片机应用系统抗干扰与可靠性设计
  • 第15章 单片机应用系统的设计与调试
  • 附录A 基础实验题目
  • 附录B 课程设计题目
  • 附录C 头文件"LCD1602.h"清单
  • 附录D 头文件"DS1302.h"清单
  • 参考文献

同系列书

人邮微信
本地服务
教师服务
教师服务
读者服务
读者服务
返回顶部
返回顶部