【内容概要】目前单片机应用设计的开发人员,大多使用C51语言来进行单片机应用程序编程。C51语言是在标准C语言的基础上,根据单片机存储器的硬件结构及内部资源,扩展了相应的数据类型和变量,而C51在语法规定、程序结构与设计方法上,都与标准C相同。本章主要介绍有关C51语言的基础知识,此外还对C51的集成开发环境Keil µVision3以及单片机软硬件仿真平台Proteus作一介绍。
在单片机应用系统的开发中,软件编程占有非常重要的地位。随着单片机的集成度越来越高以及单片机系统的广泛应用,对软件编程的要求也越来越高,要求编程人员在短时间内编写出执行效率高、运行可靠的程序代码。同时,由于实际系统的日趋复杂,对程序的可读性、升级与维护以及模块化的要求越来越高,以方便多个工程师进行协同开发。
C51语言是近年来在国内外的51单片机开发中,普遍使用的一种程序设计语言。C51语言能直接对单片机硬件进行操作,既有高级语言的特点,又有汇编语言的特点,因此在单片机应用的程序设计中,得到了非常广泛的使用。
3.1 编程语言Keil C51简介
目前用于51系列单片机编程的C语言都采用Keil C51(简称C51),Keil C51是在标准C语言的基础上发展起来的。
3.1.1 Keil C51简介
C语言是美国国家标准协会(ANSI)制定的编程语言标准,1987年ANSI公布了87 ANSI C,即现行的标准C(即ANSI C)语言。
Keil C51语言是在ANSI C的基础上针对51单片机的硬件特点进行的扩展,并向51单片机上移植,经过多年努力,C51语言已经成为公认的高效、简洁的51单片机的实用高级编程语言。
与51单片机的汇编语言相比,用C51语言进行的软件开发,具有如下优点。
(1)可读性好。C51语言程序比汇编语言程序的可读性好,因而编程效率高,程序便于修改、维护以及升级。
(2)模块化开发与资源共享。用C51语言开发出来的程序模块可以不经修改,直接被其他项目所用,这使得开发者能够很好地利用已有的大量的标准C程序资源与丰富的库函数,减少重复劳动,同时也有利于多个工程师进行协同开发。
(3)可移植性好。为某种型号单片机开发的C语言程序,只需将与硬件相关之处和编译链接的参数进行适当修改,就可以方便地移植到其他型号的单片机上。例如,为51单片机编写的程序通过改写头文件以及少量的程序行,就可以方便地移植到PIC单片机上。
(4)生成的代码效率高。当前较好的C51语言编译系统编译出来的代码效率只比直接使用汇编语言低20%左右,如果使用优化编译选项,最高可达到90%左右,效果会更好。
3.1.2 C51与标准C的比较
单片机的C51语言与标准C语言之间有许多相同的地方,但也有其自身的一些特点。不同的嵌入式C语言编译系统之所以与标准C语言有不同的地方,主要是由于它们所针对的硬件系统不同。对于51单片机,目前广泛使用的是Keil C51语言,简称C51语言。
C51语言的基本语法与标准C语言相同,C51语言在标准C语言的基础上进行了适合于51系列单片机硬件的扩展。深入理解Keil C51语言对标准C语言的扩展部分以及它们的不同之处,是掌握C51语言的关键之一。
C51语言与标准C语言的主要区别如下。
(1)库函数的不同。由于标准C语言的库函数中的部分库函数不适合于嵌入式控制器系统,因此它们被排除在Keil C51语言之外,如字符屏幕和图形函数。有一些库函数可以继续使用,但是这些库函数都必须针对51单片机的硬件特点来做出相应的开发。例如,库函数printf和scanf,在标准C语言中,这两个函数通常用于屏幕打印和接收字符,而在Keil C51语言中,主要用于串行口数据的收发。
(2)数据类型有一定的区别。在C51语言中增加了几种针对51单片机特有的数据类型,在标准C语言的基础上又扩展了4种类型。例如,51单片机包含位操作空间和丰富的位操作指令,因此,C51语言与标准C语言相比就要增加位类型。
(3)C51语言的变量存储模式与标准C语言中的变量存储模式数据不一样。标准C语言最初是为通用计算机设计的,在通用计算机中只有一个程序和数据统一寻址的内存空间,而C51语言中变量的存储模式与51单片机的存储器紧密相关。
(4)数据存储类型的不同。51单片机存储区可分为内部数据存储区、外部数据存储区以及程序存储区。内部数据存储区可分为3个不同的C51存储类型:data、idata和bdata。外部数据存储区分为2个不同的C51存储类型:xdata和pdata。程序存储区只能读不能写,可能在51单片机内部或者外部。C51语言提供了code存储类型来访问程序存储区。
(5)标准C语言没有处理单片机中断的定义,而C51语言中有专门的中断函数。
(6)C51语言与标准C语言的输入/输出处理不一样。C51语言中的输入/输出是通过51单片机的串行口来完成的,输入/输出指令执行前必须对串行口进行初始化。
(7)头文件的不同。C51语言与标准C语言头文件的差异是C51语言头文件必须把51单片机内部的外设硬件资源(如定时器、中断、I/O等)相应的功能寄存器的写入到头文件内。
(8)程序结构的差异。由于51单片机的硬件资源有限,它的编译系统不允许太多的程序嵌套。其次,标准C语言所具备的递归特性不被C51语言支持。
但是从数据运算操作、程序控制语句以及函数的使用上来说,Keil C51语言与标准C语言几乎没有什么明显的差别。如果程序设计者具备了有关标准C语言的编程基础,只要注意Keil C51语言与标准C语言的不同之处,并熟悉51单片机的硬件结构,就能够较快地掌握C51语言的编程。
3.2 Keil C51的开发工具
Keil C51语言是德国Keil software公司开发的用于51系列单片机的C51语言开发软件。Keil C51在兼容标准C的基础上,又增加了很多与51单片机硬件相关的编译特性,使得在51系列单片机上开发应用程序更为方便和快捷,生成的程序代码运行速度快,所需要的存储器空间小,完全可以和汇编语言相媲美。它支持众多的8051架构的芯片,同时集编辑、编译、仿真等功能于一体,具有强大的软件调试功能,是众多的单片机应用开发软件中的最优秀软件之一。
3.2.1 集成开发环境Keil µVision3简介
目前,Keil C51已被完全集成到一个功能强大的全新集成开发环境(IDE—Intergrated Development Eviroment)µVision3中,Keil Software公司推出的Keil µVision3 是一款用于51单片机的Windows下的集成开发环境,提供了对基于8051内核的各种型号单片机的支持,为51单片机软件开发提供了全新的C语言开发环境。该开发环境下集成了文件编辑处理、编译链接、项目(Project)管理、窗口、工具引用和仿真软件模拟器以及Monitor51硬件目标调试器等多种功能,所有这些功能均可在Keil µVision3的开发环境中极为简便地进行操作。
Keil µVision3完全兼容先前的Keil µVision2版本。Keil公司目前已经推出了v7.0以上版本的C51编译器,目前较新的版本为Keil C51 v8.08a。
µVision3内部集成了源程序编辑器,并允许用户在编辑源文件时就可设置程序调试断点,便于在程序调试过程中快速检查和修改程序。此外,µVision3还支持软件模拟仿真(Simulator)和用户目标板调试(Monitor51)两种工作方式。在软件模拟仿真方式下不需要任何51单片机及其外围硬件即可完成用户程序仿真调试。在用户目标板调试方式下,利用硬件目标板中的监控程序可以直接调试目标硬件系统,使用户节省购买硬件仿真器的费用。
C51程序的开发是在Keil µVision3开发环境下进行的。开发者可购买Keil µVision3软件,也可到Keil software公司的主页免费下载Eval(评估)版本。该版本同正式版本一样,但有一定的限制,最终生成的代码不能超过2KB,但用于学习已经足够。开发者还可以到Keil公司网站申请免费的软件试用光盘。
Keil µVision3内集成了功能强大的源程序编辑器和调试器。编辑器允许用户在编辑源文件时就可设置程序调试断点,便于在程序调试过程中快速检查和修改程序,可以像一般的文本编辑器一样对源代码进行编辑。用户启动µVision3的调试器之后,断点即被激活。断点可被设置为条件表达式、变量或存储器访问,断点被触发后,调试器的命令或调试功能即可执行,因此用户可以在编辑器内调试程序,使用户快速地检查和修改程序。用户还可以在编辑器中选中变量和存储器来观察其值。并可以在双层窗口中显示,还可对其进行适当的调整。此外,µVision3调试器具有符号调试特性以及历史跟踪,代码覆盖,复杂断点等功能。
Keil µVision环境下还有串口调试器软件comdebug.exe,用于在电脑端能够看到单片机发出的数据,串口调试器软件无需安装,可直接运行这个软件。若读者需要最新版,可到有关搜索网站输入关键词“串口调试器”,找到一个合适的下载网站,即可下载“串口调试器”的最新版本。当然,使用Windows自带的“超级终端”也是不错的选择。
3.2.2 Keil µVision3软件的安装、启动和运行
1.软件安装
Keil µVision3集成开发环境的安装,同大多数软件安装一样,根据提示进行。Keil µVision3安装完毕后,可在桌面上看到Keil µVision3软件的快捷图标。
2.软件启动
单击桌面上的Keil µVision3软件的快捷图标,即可启动该软件,几秒种后,出现编辑界面。
3.软件的运行
Keil µVision3把用户的每一个应用程序设计都当作一个项目,用项目管理的方法把一个应用程序设计中所需要用到的、互相关联的程序链接在同一项目中。这样,打开一个项目时,所需要的关联程序也都跟着进入了调试窗口,方便用户对项目中各个程序的编写、调试和存储。用户也可能开发了多个项目,每个项目用到了相同或不同的程序文件和库文件,采用项目管理,就很容易区分不同项目中所用到的程序文件和库文件,非常容易管理。因此,在编写一个新的应用程序前,一定先要建立项目。下面首先介绍如何建立一个新的项目。
在编辑界面下,首先要建立一个单击“Project”菜单,选择下拉式菜单中的“New Project”,弹出文件对话窗口,选择要保存的路径,在“文件名”中输入一个程序项目名称,保存后的文件扩展名为“.uv2”,这是Keil µVision3项目文件的扩展名,以后可直接单击此文件就可打开先前做的项目。
单击“保存”后,这时会弹出一个对话框,要求选择单片机的型号,用户可根据所使用的单片机来选择。Keil µVision3支持几乎所有的51内核的单片机。然后开始编写第一个程序。单击“File”菜单,再在下拉菜单中单击“New”选项。此时光标在编辑窗口里闪烁,这时,用户可以输入代码了。输入完毕,单击菜单上的“File”,在下拉菜单中单击“Save As”,在“文件名”栏的编辑框中,键入文件名,同时,必须键入正确的扩展名,然后,单击“保存”按钮。
在上述工作完成后,还有有关项目的设置,程序的编译和链接,程序的调试。关于这些内容,读者可按照Keil µVision3开发环境的帮助功能,进行反复练习和操作,从而熟练地掌握该软件的使用。由于篇幅所限,这里不再赘述。
3.3 C51语言程序设计基础
本节在标准C语言的基础了解掌握C51的数据类型和存储类型、C51的基本运算与流程控制语句、C51语言构造数据类型、C51函数以及C51程序设计的其他一些问题,为C51的程序设计打下基础。
3.3.1 C51语言中的数据类型与存储类型
1.数据类型
数据是单片机操作的对象,是具有一定格式的数字或数值,数据的不同格式就称为数据类型。
Keil C51支持的基本数据类型如表3-1所示。针对AT89S51单片机的硬件特点,C51在标准C的基础上,扩展了4种数据类型(见表3-1中最后4行)。注意,扩展的4种数据类型,不能使用指针对它们存取。
2.C51的扩展数据类型
下面对扩展的4种数据类型进行说明。
(1)位变量bit。bit的值可以是1(true),也可以是0(false)。
(2)特殊功能寄存器sfr。AT89S51单片机的特殊功能寄存器分布在片内数据存储区的地址单元80H~FFH之间,“sfr”数据类型占用一个内存单元。利用它可以访问AT89S51单片机内部的所有特殊功能寄存器。例如:sfr P1=0x90这一语句定义了P1端口在片内的寄存器,在程序后续的语句中可以用“P1=0xff”使P1的所有引脚输出为高电平之类的语句来操作特殊功能寄存器。
(3)特殊功能寄存器sfr16。“sfr16”数据类型占用两个内存单元。sfr16和sfr一样用于操作特殊功能寄存器。所不同的是它用于操作占两个字节的特殊功能寄存器。例如:“sfr16 DPTR=0x82”语句定义了片内16位数据指针寄存器DPTR,其低8位字节地址为82H,高8位字节地址为83H。在程序的后续语句中可以对DPTR进行操作。
(4)特殊功能位sbit。sbit是指AT89S51片内特殊功能寄存器的可寻址位。例如:
sfr PSW=0xd0; /定义PSW寄存器地址为0xd0/
sbit PSW ^2 = 0xd2; /定义OV位为PSW.2/
符号“^”前面是特殊功能寄存器的名字,“^”后面的数字定义特殊功能寄存器可寻址位在寄存器中的位置,取值必须是0~7。
注意,不要把bit与sbit相混淆。bit是用来定义普通的位变量,它的值只能是二进制的0或1。而sbit定义的是特殊功能寄存器的可寻址位,它的值是可进行位寻址的特殊功能寄存器的某位的绝对地址,如PSW寄存器OV位的绝对地址0xd2。
3.数据存储类型
在讨论C51的数据类型时,必须同时提及它的存储类型,以及它与51单片机存储器结构的关系,因为C51定义的任何数据类型必须以一定的方式定位在51单片机的某一存储区中,否则没有任何实际意义。
51单片机有片内、片外数据存储区,还有程序存储区。51单片机片内的数据存储区是可读写的,51单片机的衍生系列最多可有256字节的内部数据存储区,其中低128字节可直接寻址,高128字节(80H~FFH)只能间接寻址,从20H开始的16字节可位寻址。内部数据存储区可分为3个不同的数据存储类型:data、idata和bdata。
访问片外数据存储区比访问片内数据存储区慢,因为片外数据存储区是通过数据指针加载地址来间接寻址访问的。C51提供两种不同的数据存储类型xdata和pdata以访问片外数据存储区。
程序存储区只能读不能写。程序存储区可能在51单片机内部或者外部,或者外部和内部都有,由51单片机的硬件来决定,C51提供了code存储类型来访问程序存储区。
C51存储类型与AT89S51单片机实际的存储空间的对应关系如表3-2所示。
下面对表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字节的空间。另外,当内部堆栈溢出的时候,程序会莫名其妙地复位。这是因为51单片机没有报错的机制,堆栈的溢出只能以这种方式表示,因此要留有较大的堆栈空间来防止堆栈溢出。
(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存储类型。
变量存储类型定义举例:
(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中,占据24=8字节,相当于使用@DPTR间接寻址/。
4.数据存储模式
如果在变量定义时略去存储类型标识符,编译器会自动默认存储类型。默认的存储类型进一步由SMALL、COMPACT和LARGE存储模式指令限制。例如,若声明char var1,则在使用SMALL存储模式下,var1被定位在data存储区,在使用COMPACT模式下,var1被定位在idata存储区;在LARGE模式下,var1被定位在xdata存储区中。
在固定的存储器地址上进行变量的传递,是C51的标准特征之一。在SMALL模式下,参数传递是在片内数据存储区中完成的。LARGE和COMPACT模式允许参数在外部存储器中传递。C51也支持混合模式。例如,在LARGE模式下,生成的程序可以将一些函数放入SMALL模式中,从而加快执行速度。下面对存储模式作进一步的说明。
(1)SMALL模式。在该模式下,所有变量都默认位于51单片机内部的数据存储器,这与使用data指定存储器类型的方式一样。在此模式下,变量访问的效率高,但是所有数据对象和堆栈必须使用内部RAM。
(2)COMPACT模式。当使用本模式时,所有变量都默认在外部数据存储器的1页内,这与使用pdata指定存储器类型是一样的。该存储器类型适用于变量不超过256字节的情况,此限制是由寻址方式决定的,相当于使用数据指针@Ri进行寻址。与SMALL模式相比,该存储模式的效率比较低,对变量访问的速度也慢一些,但比LARGE模式快。
(3)LARGE模式。在LARGE模式中,所有变量都默认位于外部数据存储器,相当于使用数据指针@DPTR进行寻址。通过数据指针访问外部数据存储器的效率较低,特别是当变量为2字节或更多字节时,该模式要比SMALL和COMPACT产生更多的代码。
3.3.2 C51语言的特殊功能寄存器及位变量定义
本节介绍C51如何对51单片机的特殊功能寄存器以及位变量进行定义并访问。
1.特殊功能寄存器的C51定义
C51语言允许通过使用关键字sfr、sbit或直接引用编译器提供的头文件来对特殊功能寄存器(SFR)进行访问,51单片机的特殊功能寄存器分布在片内RAM的高128字节中,对SFR的访问只能采用直接寻址方式。
(1)使用关键字定义sfr。为了能直接访问特殊功能寄存器SFR,C51提供了一种定义方法,即引入关键字sfr,语法如下:
sfr特殊功能寄存器名字=特殊功能寄存器地址;
例如:
sfr IE=0xA8; /中断允许寄存器地址A8H/
sfr TCON=0x88; /定时器/计数器控制寄存器地址88H/
sfr SCON=0x98; /串行口控制寄存器地址98H/
在51单片机中,如要访问16位SFR,可使用关键字sfr16。16位SFR的低字节地址必须作为“sfr16”的定义地址,例如:
sfr16 DPTR=0x82 /数据指针DPTR 的低8位地址为82H,高8位地址为83H/
(2)通过头文件访问SFR。各种衍生型的51单片机的特殊功能寄存器的数量与类型有时是不相同的,对单片机特殊功能寄存器的访问可以通过头文件的访问来进行。
为了用户处理方便,C51把51单片机(或52单片机)常用的特殊功能寄存器和其中的可寻址位进行了定义,放在一个reg51.h(或reg52.h)的头文件中。当用户要使用时,只需在使用之前用一条预处理命令#include把这个头文件包含到程序中,就可以使用特殊功能寄存器名和其中的可寻址位名称了。用户可以通过文本编辑器对头文件进行增减。
(3)特殊功能寄存器中的位定义。对SFR中的可寻址位的访问,要使用关键字来定义可寻址位,共有3种方法。
① sbit 位名=特殊功能寄存器^位置;
例如:
sfr PSW=0xD0; /定义PSW 寄存器的字节地址0xD0H
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/
② 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/
【例】 片内I/O口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定义。由于51单片机能够进行位操作,C51扩展的“bit”数据类型用来定义位变量,这是C51与标准C的不同之处。
C51采用关键字“bit”来定义位变量,一般格式为:
bit bit_name;
例如:
bit ov_flag; / 将ov_flag定义为位变量/ bit lock_pointer; / 将lock_pointer定义为位变量/
(2)函数可以包含类型为bit的参数,也可将其作为返回值。C51程序函数可以包含类型为“bit”的参数,也可将其作为返回值。例如:
bit func(bit b0, bit b1); / 位变量b0与b1作为函数func的参数/
{ ……
return(b1); / 位变量b1作为函数的返回值/
}
(3)位变量定义的限制。位变量不能用来定义指针和数组。例如:
bit ptr; / 错误,不能用位变量来定义指针/ bit array[ ] ; / 错误,不能用位变量来定义数组array[ ]*/
在定义位变量时,允许定义存储类型,位变量都被放入一个位段,此段总是位于51单片机的片内RAM中,因此其存储类型限制为DATA或IDATA,如果将位变量定义成其他类型都会导致编译时出错。
3.3.3 C51语言的绝对地址访问
如何对51单片机的片内RAM、片外RAM及I/O进行访问,C51提供了两种比较常用的访问绝对地址的方法。
1.绝对宏
C51编译器提供了一组宏定义来对code、data、pdata和xdata空间进行绝对寻址。在程序中,用“#include”来对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区寻址。
2.at关键字
使用关键字at可对指定的存储器空间的绝对地址进行访问,格式如下:
[存储器类型] 数据类型说明符 变量名at地址常数
其中,存储器类型为C51能识别的数据类型;数据类型为C51支持的数据类型;地址常数用于指定变量的绝对地址,必须位于有效的存储器空间之内;使用at定义的变量必须为全局变量。
【例】 使用关键字at实现绝对地址的访问,程序如下:
void main(void)
{ data unsigned char y1_at_0x50; /在data 区定义字节变量y1,它的地址为50H/
xdata unsigned int y2_at_0x4000; /在xdata区定义字变量y2,地址为4000H/
y1=0xff;
y1=0x1234;
……
while(1);
}
【例】 将片外RAM 2000H开始的连续20字节单元清0。 程序如下:
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个单元内容清0,则程序如下:
xdata unsigned char buffer[8]_at_0x40;
void main(void)
{ unsigned char j ;
for(j=0;j<8; j++)
{ buffer[j]=0
}
}
3.3.4 C51的基本运算
C51的基本运算与标准C类似,主要包括算术运算、关系运算、逻辑运算、位运算和赋值运算及其表达式等。
1.算术运算符
算术运算的算术运算符及其说明如表3-4所示。
读者对表3-4中的运算符“+、−、*”运算比较熟悉,但是对于“/”和“%”往往会有疑问。这两个符号都涉及除法运算,但“/”运算是取商,而“%” 运算为取余数。例如“5/3”的结果(商)为1,而“5%3”的结果为2(余数)。表3-4中的自增和自减运算符是使变量自动加1或减1,自增和自减运算符放在变量前和变量之后是不同的。例如:
++i,- -i:在使用i之前,先使i值加(减)1。
i++,i- -:在使用i之后,再使i值加(减)1。
例如:若i=4,则执行x=++i时,先使i加1,再引用结果,即x=5,运算结果为i=5,x=5。
再如:若i=4,则执行x=i++时,先引用i值,即x=4,再使i加1,运算结果为i=5,x=4。
2.逻辑运算符
逻辑运算符及其说明如表3-5所示。
3.关系运算符
关系运算符就是判断两个数之间的关系。关系运算符及其说明如表3-6所示。
4.位运算
位运算符及其说明如表3-7所示。
在实际的控制应用中,人们常常想要改变I/O口中的某一位的值,而不影响其他位,如果I/O口是可位寻址的,这个问题就很简单。但有时外扩的I/O口只能进行字节操作,因此要想在这种场合下实现单独的位控,就要采用位操作。
5.指针和取地址运算符
指针是C语言中一个十分重要的概念,将在后面介绍。在这里,先来了解C语言中提供的两个专门用于指针和取地址运算符,如表3-8所示。
取内容和取地址的一般形式分别为:
变量=*指针变量
指针变量=&目标变量
取内容运算是将指针变量所指向的目标变量的值赋给左边的变量;取地址运算是将目标变量的地址赋给左边的变量。注意,指针变量中只能存放地址(也就是指针型数据),一般情况下不要将非指针类型的数据赋值给一个指针变量。
3.3.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语句的说明如下。
(1)每一个case的常量表达式必须是互不相同的,否则将出现混乱。
(2)各个case和default出现的次序,不影响程序执行的结果。
(3)switch括号内表达式的值与某case后面的常量表达式的值相同时,就执行它后面的语句,遇到break语句则退出switch语句。若所有的case中的常量表达式的值都没有与switch语句表达式的值相匹配时,就执行default后面的语句。
(4)如果在case语句中遗忘了break语句,则程序执行了本行之后,不会按规定退出switch语句,而是将执行后续的case语句。在执行1个case分支后,使流程跳出switch结构,即中止switch语句的执行,可以用1条break语句完成。switch语句的最后一个分支可以不加break语句,结束后直接退出switch结构。
【例】 在单片机程序设计中,常用switch语句作为键盘中按键按下的判别,并根据按下键的键号跳向各自的分支处理程序。
input: keynum=keyscan( )
switch(keynum);break;
{ case 1: key1( ); /如果按下键的键值为1,则执行函数key1( )/
case 2: key2( ); /如果按下键的键值为2,则执行函数key2( )/
case 3: key3( ); /如果按下键的键值为3,则执行函数key3( )/
case 4: key4( ); /如果按下键的键值为4,则执行函数key4( )/
…
default:goto input
}
例子中的keyscan( )是另行编写的一个键盘扫描函数,如果有键按下,该函数就会得到按下按键的键值,将键值赋予变量keynum。如果键值为1,则执行键值处理函数key1( )后返回;如果键值为4,则执行key4( )函数后返回。执行完1个键值处理函数后,则跳出switch语句,从而达到按下不同的按键来进行不同的键值处理的目的。
2.循环控制语句
许多实用程序都包含循环结构,熟练掌握和运用循环结构的程序设计是C51语言程序设计的基本要求。
实现循环结构的语句有以下3种:while语句、do-while语句和for语句。
(1)while语句。while语句的语法形式为:
while(表达式)
{ 循环体语句;
}
表达式是while循环能否继续的条件,如果表达式为真,就重复执行循环体语句;反之,则终止循环体内的语句。
while循环结构的特点在于,循环条件的测试在循环体的开头,要想执行重复操作,首先必须进行循环条件的测试,如条件不成立,则循环体内的重复操作一次也不能执行。
例如:
while((P1&0x80)==0)
{ }
while中的条件语句对AT89S51单片机的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来实现会直观。
【例】 实型数组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++)
{;}
本例的一个典型应用就是软件延时。
在程序的设计中,经常用到时间延迟,可用循环结构来实现,即循环执行指令,消磨一段已知的时间。AT89S51单片机指令的执行时间是靠一定数量的时钟周期来计时的,如果使用12MHz晶振,则12个时钟周期花费的时间为1µs。
【例】 编写一个延时1ms程序。
void delayms( unsigned char int j)
{ unsigned char i;
while(j--)
{ for(i=0;i<125;i++)
{;}
}
}
如果把上述程序段编译成汇编语言代码进行分析,用for进行的内部循环大约延时8ms,但不是特别精确。不同的编译器会产生不同的延时,因此i的上限值125应根据实际情况进行补偿调整。
【例】 无限循环的结构实现。
编写无限循环程序段,可使用以下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语句跳出本层循环体,从而马上结束本层循环。
【例】 执行如下程序段。
void main(void ) / 主函数main( ) /
{ int i, sum;
sum=0;
for(i=1;i<=10;i++)
{ sum=sum+i;
if(sum>5) break;
print("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并不结束整个循环,而仅仅是中断这一层循环,然后跳到循环条件处,继续下一层的循环。当然,如果跳到循环条件处,发现条件已不成立,那么循环也会结束。
【例】 输出整数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;
}
print("sum=%d\n", sum); /在计算机屏幕显示sum值,了解本语句的功能即可/
}
(3)goto语句是一个无条件转移语句,当执行goto语句时,将程序指针跳转到goto给出的下一条代码。基本格式如下:
goto 标号
【例】 计算整数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.3.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.数组与存储空间
当程序中设定了一个数组时,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列。
当数组特别是多维数组中大多数元素没有被有效地利用时,就会浪费大量的存储空间。对于51单片机,不拥有大量的存储区,其存储资源极为有限,因此在进行C51编程开发时,要仔细地根据需要来选择数组的大小。
3.3.7 C51的指针
C51支持基于存储器的指针和一般指针两种指针类型。当定义一个指针变量时,若未给出它所指向的对象的存储类型,则指针变量被认为是一般指针,反之若给出了它所指向对象的存储类型,则该指针被认为是基于存储器的指针。
基于存储器的指针类型由C51源代码中的存储类型决定,用这种指针可以高效访问对象,且只需1~2字节。
一般指针占用3字节:1字节为存储器类型,2字节为偏移量。存储器类型决定了对象所用的8051的存储空间,偏移量指向实际地址。1个一般指针可以访问任何变量而不管它在8051存储器的位置。
1.基于存储器的指针
在定义一个指针时,若给出了它所指对象的存储类型,则该指针是基于存储器的指针。
基于存储器的指针以存储类型为变量,在编译时才被确定。因此,为地址选择存储器的方法可以省略,以便在这些指针的长度可为1字节(idata,data,pdata)或2字节(code,xdata*)。在编译时,这类操作一般被“内嵌”编码,而无须进行库调用。
基于存储器的指针定义举例:
char xdata px*;
在xdata存储器中定义了1个指向字符类型(char)的指针。指针自身在默认的存储区,长度为2字节,值为0~0xFFFF。再看下面一个例子:
char xdata *data pdx;
除了明确定义指针位于8051内部存储器(data)外,其他与上例相同,它与编译模式无关。再看一个例子:
data char xdata *pdx;
本例与上例完全相同。存储器类型定义既可以放在定义的开头,也可以直接放在定义的对象之前。
C51的所有数据类型都和8051的存储器类型相关。所有用于一般指针的操作同样可用于基于存储器的指针。 基于存储器的指针定义举例如下:
char xdata px; / px 指向一个存在片外RAM的字符变量,px 本身在默认的存储器中,由编译模式决定,占用2字节 /
char xdata data py; / py 指向一个存在片外RAM的字符变量,py本身在RAM中,与编译模式无关,占用2字节 /
2.一般指针
在函数的调用中,函数的指针参数需要用一般指针。一般指针的说明形式如下:
数据类型 *指针变量;
例如:
char *pz
这里没有给出pz所指变量的存储类型,pz处于编译模式的默认的存储区,长度为3字节。
一般指针包括3字节:2字节偏移和1字节存储器类型,如表3-9所示。
其中,第1个字节代表指针的存储器类型,存储器类型的编码如表3-10所示。
例如,以xdata类型的0x1234地址作为指针表示,如表3-11所示。
当常数作指针时,必须注意正确定义存储器的类型和偏移。
C51编译器不检查指针常数,用户必须选择有实际意义的值。利用指针变量可以对内存地址直接操作。
3.4 C51语言的函数
函数是一个完成一定相关功能的执行代码段。在高级语言中,函数与另外两个名词“子程序”和“过程”用来描述同样的事情。在C51中使用的是函数这个术语。
C51中函数的数目是不限制的,但是1个C51程序必须至少有1个函数,以main为名,称为主函数,主函数是唯一的,整个程序从这个主函数开始执行。
C51还可以建立和使用库函数,可由用户根据需求调用。
3.4.1 函数的分类
从结构上分,C51中函数可分为主函数main( )和普通函数两种。而普通函数从编程者的角度又可以划分为两种:标准库函数和用户自定义函数。
1.标准库函数
标准库函数是由C51编译器提供的。编程者在进行程序设计时,应该善于充分利用这些功能强大、资源丰富的标准库函数资源,以提高编程效率。
用户可以直接调用C51的库函数而不需要为这个函数写任何代码,只需要包含具有该函数说明的头文件即可。
2.用户自定义函数
用户自定义函数是用户根据自己的需要所编写的函数。从函数定义的形式上分可以有将其划分为:无参函数、有参函数和空函数。
(1)无参函数:此种函数在被调用时,既无参数输入,也不返回结果给调用函数,只是为完成某种操作而编写的函数。
无参函数的定义形式为:
返回值类型标识符 函数名( )
{ 函数体;
}
无参函数一般不带返回值,因此函数的返回值类型的标识符可以省略。
例如,函数main( ),该函数为无参函数,返回值类型的标识符可以省略,默认值是int类型。
(2)有参函数:调用此种函数时,必须提供实际的输入函数。有参函数的定义形式为:
返回值类型标识符 函数名(形式参数列表)
形式参数说明
{ 函数体;
}
【例】 定义一个函数max( ),用于求两个数中的大数。
int a,b
int max(a, b)
{ if(a>b)return(a);
else return(b);
}
上面的程序段中,a、b为形式参数。return( )为返回语句。
(3)空函数:此种函数体内无语句,是空白的。调用空函数时,什么工作也不做,不起任何作用。定义空函数的目的,并不是为了执行某种操作,而是为了以后程序功能的扩充。先将一些基本模块的功能函数定义成空函数,占好位置,并写好注释,以后再用一个编好的函数代替它。这样整个程序的结构清晰,可读性好,以后扩充新功能方便。
空函数的定义形式为:
返回值类型标识符 函数名( )
{ }
例如:
float min( )
{ } /空函数,占好位置/
3.4.2 函数的参数与返回值
1.函数的参数
C语言采用函数之间的参数传递方式,使一个函数能对不同的变量进行功能相同的处理,从而大大提高了函数的通用性与灵活性。
函数之间的参数传递,由主函数调用时主调函数的实际参数与被调函数的形式参数之间进行数据传递来实现。
被调用函数的最后结果由被调用函数的return语句返回给调用函数。
函数的参数包括形式参数和实际参数。
(1)形式参数:函数的函数名后面括号中的变量名称为形式参数,简称形参。
(2)实际参数:在函数调用时,主调函数名后面括号中的表达式称为实际参数,简称实参。
在C语言的函数调用中,实际参数与形式参数之间的数据传递是单向进行的,只能由实际参数传递给形式参数,而不能由形式参数传递给实际参数。
实际参数与形式参数的类型必须一致,否则会发生类型不匹配的错误。被调用函数的形式参数在函数未调用之前,并不占用实际内存单元。只有当函数调用发生时,被调用函数的形式参数才分配给内存单元,此时内存中调用函数的实际参数和被调用函数的形式参数位于不同的单元。在调用结束后,形式参数所占有的内存被系统释放,而实际参数所占有的内存单元仍保留并维持原值。
2.函数的返回值
函数的返回值是通过函数中的return语句获得的。1个函数可以有1个以上的return语句,但是多于1个的return语句必须在选择结构(if或do/case)中使用(如前面求两个数中的大数函数max( )的例子),因为被调用函数一定只能返回1个变量。
函数返回值的类型一般在定义函数时,由返回值的标识符来指定。如在函数名之前的int指定函数的返回值的类型为整型数(int)。若没有指定函数的返回值类型,默认返回值为整型类型。
当函数没有返回值时,则使用标识符void进行说明。
3.4.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(在函数库中)包含到程序中来。
在程序编译时,系统会自动将函数库中的有关函数调入到程序中去,编译出完整的程序代码。
(3)如果程序中使用了自定义函数,且该函数与调用它的函数同在一个文件中,则应根据主调函数与被调函数在文件中的位置,决定是否对被调函数作出说明。
a.如果被调函数在主调函数之后,一般应在主调函数中,在被调函数调用之前,对被调函数的返回值类型作出说明。
b.如果被调函数出现在主调函数之前,不用对被调函数进行说明。
c.如果在所有函数定义之前,在文件的开头处,在函数的外部已经说明了函数的类型,则在主调函数中不必对所调用的函数再做返回值类型说明。
3.4.4 中断服务函数
由于标准C没有处理单片机中断的定义,为了能进行51单片机的中断处理,C51编译器对函数的定义进行了扩展,增加了一个扩展关键字interrupt。使用interrupt可以将一个函数定义成中断服务函数。由于C51编译器在编译时对声明为中断服务程序的函数自动添加了相应的现场保护、阻断其他中断、返回时自动恢复现场等处理的程序段,因而在编写中断服务函数时可不必考虑这些问题,减小了用户编写中断服务程序的烦琐程度。
中断服务函数的一般形式为:
函数类型 函数名(形式参数表)interrupt n using n
关键字interrupt是中断号,对于51单片机,n的取值为0~4。
关键字using后面的 n是所选择的寄存器组,using是一个选项,可以省略。如果没有使用using关键字指明寄存器组,中断函数中的所有工作寄存器的内容将被保存到堆栈中。
有关中断服务函数的具体使用注意事项,将在第5章进行详细介绍。
3.4.5 变量及存储方式
1.变量
(1)局部变量。局部变量是某一个函数中存在的变量,它只在该函数内部有效。
(2)全局变量。在整个源文件中都存在的变量称为全局变量。全局变量的有效区间是从定义点开始到源文件结束,其中的所有函数都可直接访问该变量。如果定义前的函数需要访问该变量,则需要使用extern关键词对该变量进行说明,如果全局变量声明文件之外的源文件需要访问该变量,也需要使用extern关键词进行说明。
由于全局变量一直存在,占用了大量的内存单元,且加大了程序的耦合性,不利于程序的移植或复用。
全局变量可以使用static关键词进行定义,该变量只能在变量定义的源文件内使用,不能被其他源文件引用,这种全局变量称为静态全局变量。如果一个其他文件的非静态全局变量需要被某文件引用,则需要在该文件调用前使用extern关键词对该变量声明。
2.变量的存储方式
单片机的存储区间可以分为程序存储区、静态存储区和动态存储区3部分。数据存放在静态存储区或动态存储区。其中全局变量存放在静态存储区,在程序开始运行时,给全局变量分配存储空间;局部变量存放在动态存储区,在进入拥有该变量的函数时,给这些变量分配存储间。
3.4.6 宏定义与文件包含
在C51程序设计中要经常用到宏定义、文件包含与条件编译。
1.宏定义
宏定义语句属于C51语言的预处理指令,使用宏可以使变量书写简化,增加程序的可读性、可维护性和可移植性。宏定义分为简单的宏定义和带参数的宏定义。
当程序中需要调用C51语言编译器提供的各种库函数时,必须在文件的开头使用#include命令将相应函数的说明文件包含进来。
3.4.7 库函数
C51的强大功能及其高效率的重要体现之一在于提供了丰富的可直接调用的库函数。使用库函数可以使程序代码简单、结构清晰、易于调试和维护。
下面介绍在程序设计中几类重要的库函数。
(1)特殊功能寄存器包含文件reg51.h或reg52.h。reg51.h中包含了所有的8051的sfr及其位定义。reg52.h中包含了所有的8052的sfr及其位定义,一般系统都包含reg51.h或reg52.h。
(2)绝对地址包含文件absacc.h。该文件定义了几个宏,以确定各类存储空间的绝对地址。
(3)输入/输出流函数位于stadio.h文件中。流函数默认8051的串口来作为数据的输入/输出。如果要修改为用户定义的I/O口读写数据,如改为LCD显示,可以修改lib目录中的getkey.c及putchar.c源文件,然后在库中替换它们即可。
(4)动态内存分配函数,位于stdlib.h中。
(5)能够方便地对缓冲区进行处理的缓冲区处理函数位于string.h中。其中包括复制、移动、比较等函数。
3.5 软件仿真开发工具Proteus与
Keil µVision3的联调
在3.2节中我们已经介绍了Keil C51以及集成开发环境(Intergrated Development Eviroment,IDE)µVision3。目前,在单片机应用系统的设计中,单片机应用系统的虚拟仿真软件Proteus已经得到非常广泛的使用。熟练地掌握Proteus与µVision3工具软件以及它们的联合调试的使用,会使单片机应用系统的设计以及编程的效率大大提高。
3.5.1 软件仿真开发工具Proteus简介
软件仿真开发工具是一种完全用软件手段对单片机应用系统进行仿真开发的。软件仿真开发工具与用户样机在硬件上无任何联系。通常这种系统是由PC上安装仿真开发工具软件构成,可进行单片机应用系统的设计、仿真、开发与调试。
Proteus软件是英国Lab center Electronics开发的EDA工具软件,它为各种实际的单片机系统开发提供了功能强大的虚拟仿真工具,已有近20年的历史。它除了具有和其他EDA工具一样的原理编辑、设计以及模拟电路、数字电路的仿真功能外,最大的特色是对单片机应用系统连同程序运行以及所有的外围接口器件、外部的测试仪器一起仿真。针对单片机的应用,可以直接在基于原理图的虚拟模型上进行编程,并实现源代码级的实时调试。Proteus软件具有如下特点。
(1)能够对模拟电路、数字电路进行仿真。
(2)除了仿真51系列单片机外,Proteus软件还可仿真68000系列、AVR系列、PIC12-18系列、Z80系列、HC11等其他各系列单片机。
(3)具有硬件仿真开发系统中的全速、单步、设置断点等调试功能,同时可以观察各个变量、寄存器等的当前状态。
(4)该软件提供了世界上各种流行的单片机以及丰富的外围接口芯片,可进行RS-232动态仿真、I2C调试器、SPI调试器、键盘和LCD的仿真。
(5)Proteus软件提供了丰富的虚拟仪器,如示波器、逻辑分析仪、信号发生器等。利用虚拟仪器在仿真过程中可以测量系统外围电路的特性,设计者可以充分利用Proteus软件提供的虚拟仪器,来对单片机系统进行仿真测试与调试。
总之,Proteus软件是一款功能极其强大的单片机系统的软件仿真开发工具。
Proteus的软件模拟,不需要用户硬件样机,是直接就可在PC上开发和调试单片机系统的软件。调试完毕的软件可将机器代码固化在单片机的程序存储器中,一般能直接投入运行。
尽管软件仿真开发工具Proteus具有开发效率高,不需要附加的硬件开发装置成本。但是软件模拟器是使用纯软件来对用户系统仿真,是在理想的状况下的仿真,对硬件电路的实时性还不能完全准确地模拟,因此不能进行用户样机硬件部分的诊断与实时在线仿真。所以在系统的开发中,一般是先用Proteus仿真软件设计出系统的硬件电路,编写程序,然后在Proteus环境下仿真调试通过。接下来依照仿真的结果,完成实际的硬件设计。再将仿真通过的程序烧录到单片机中,然后安装到用户样机硬件板上去观察运行结果,如果有问题,再连接硬件仿真器去分析、调试。
3.5.2 Proteus与Keil µVision3的联调
Keil C51 µVision 3软件支持众多的不同公司的单片机产品,集编辑、编译和程序仿真等于一体,同时还支持汇编和C语言的程序设计,界面友好易学,在调试程序、软件仿真方面有很强大的功能。但在进行较为复杂程序的开发中,如果运行在Keil C51 µVision 3环境下编写的程序没有达到预期的效果,这时可使用Proteus与Keil C51 µVision 3软件进行联合调试。假设两个软件均已安装在PC上,联合调试前需先对Proteus与µVision 3进行相应的设置。
联合调试时,先打开Proteus案例(但不要运行),然后选中“调试”菜单中的“使用远程调试设备”选项,这就使得Keil C51能与Proteus进行通信。
完成上述设置后,再到Keil C51 µVision 3中打开相应的用户程序项目,正确地选择菜单与工具栏中的命令项。完成上述设置后,在Keil C51 µVision 3全速运行程序时,Proteus中的单片机系统也会自动运行,可在运行过程中观察某些变量值或者设备状态。
因此,在编程中,熟练地正确使用Proteus 与Keil C51 µVision 3这两个软件开发工具,并在联合调试中,恰当地使用单步、跳出、运行到当前行、设置断点等手段,就会收到事半功倍的效果。限于篇幅,本节仅介绍了软件仿真开发工具Proteus的基本特性以及与Keil C51的联合调试。有关Proteus以及Keil C51 µVision3的详细功能以及联合调试的使用请读者参阅专门书籍。