描述
开 本: 16开纸 张: 胶版纸包 装: 平装-胶订是否套装: 否国际标准书号ISBN: 9787302504085丛书名: 高等学校电子信息类专业系列教材
![](https://static.easterneast.com/file/easternspree/img/5e0978d55f9849305ddc596e_466874.jpg)
助拥有C语言基础的学生快速入门,加深对理论知识的理解;后3章以单片机综合应用设计、嵌入式系统开发为拓展实践内容。
目录
第1章绪论
1.1单片机简介
1.1.1单片机含义
1.1.2单片机的发展历史
1.1.3单片机的特点与应用
1.1.4单片机的发展趋势
1.2数字电路逻辑基础
1.2.1数制
1.2.2码制
本章小结
思考题
第2章MCS51单片机体系结构
2.1MCS51单片机的内部结构
2.2MCS51单片机的外部引脚及功能
2.2.1电源及时钟引脚
2.2.2控制引脚
2.2.3并行I/O引脚
2.2.4三总线结构
2.3MCS51单片机的中央处理器
2.3.1运算器
2.3.2控制器
2.4MCS51单片机存储器的结构
2.4.1MCS51单片机程序存储器
2.4.2MCS51单片机数据存储器
2.4.3MCS51单片机特殊功能寄存器
2.5MCS51单片机的时钟与时序
2.5.1MCS51单片机的时钟电路
2.5.2MCS51单片机的时序
2.6MCS51单片机的复位
2.6.1MCS51单片机的复位电路
2.6.2MCS51单片机的复位状态
2.7MCS51单片机的低功耗节电模式
本章小结
思考题
第3章C51程序设计基础
3.1C51程序设计基础
3.1.1C51的数据类型与存储类型
3.1.2C51的特殊功能寄存器及位变量定义
3.1.3C51的绝对地址访问
3.1.4C51的基本运算
3.1.5C51的分支与循环程序结构
3.1.6C51的数组
3.1.7C51的指针
3.2C51的函数
3.2.1函数的分类
3.2.2函数的参数与返回值
3.2.3函数的调用
3.2.4中断服务函数
3.2.5变量及存储方式
3.2.6宏定义与文件包含
3.2.7库函数
3.3C51的开发工具
3.3.1集成开发环境Keil μVision4简介
3.3.2Keil μVision4软件的安装、启动和应用程序设计
3.4软件仿真开发工具Proteus
3.4.1Proteus简介
3.4.2Proteus与Keil μVision4的联合仿真
3.4.3Proteus与Keil μVision4的联合调试
本章小结
思考题
第4章MCS51单片机接口技术
4.1MCS51单片机的中断系统
4.1.1中断系统概述
4.1.2中断系统结构
4.1.3中断处理过程
4.1.4中断程序的设计
4.2MCS51单片机的定时/计数器
4.2.1定时/计数器的组成
4.2.2定时/计数器的4种工作模式
4.2.3定时/计数器的编程和应用
4.3MCS51单片机的串行通信
4.3.1串行通信概述
4.3.2MCS51系列单片机的串行口
4.3.3串行口的4种工作方式
4.3.4串行口波特率的计算
4.3.5串行通信的编程与应用
本章小结
思考题
第5章MCS51单片机综合应用设计
5.1LED数码管显示
5.1.1LED数码管的工作原理
5.1.2LED数码管显示设计举例
5.2单片机键盘接口技术
5.2.1独立键盘和矩阵键盘
5.2.2键盘接口设计举例
5.3D/A转换接口技术
5.3.1D/A转换器简介
5.3.2单片机与8位D/A转换器DAC0832的接口设计举例
5.4A/D转换接口技术
5.4.1A/D转换器简介
5.4.2单片机与并行8位A/D转换器ADC0809的接口设计举例
5.5单片机与液晶显示器的接口
5.5.1液晶显示器介绍
5.5.2单片机与液晶显示器的设计举例
5.6温度传感器DS18B20
5.6.1DS18B20简介
5.6.2DS18B20温度测量程序设计举例
5.7温湿度传感器DHT11
5.7.1DHT11简介
5.7.2DHT11室内温湿度测量程序设计举例
5.8步进电机的控制
5.8.1步进电机的基本概念及工作原理
5.8.2用单片机实现四相步进电机的控制程序设计举例
本章小结
思考题
第6章MODBUS协议与应用
6.1MODBUS协议简介
6.1.1MODBUS OSI网络体系结构
6.1.2MODBUS协议描述
6.1.3服务器设备数据块
6.1.4功能码分类
6.2MODBUS RTU/ASCII协议
6.2.1主站节点状态图
6.2.2从站节点状态图
6.2.3主站/从站通信时序图
6.2.4MODBUS RTU协议
6.2.5MODBUS ASCII协议
6.3MODBUS应用
6.3.1MODBUS相关功能码描述
6.3.2MODBUS通信调试
本章小结
思考题
第7章基于Arduino的系统开发
7.1Arduino介绍
7.1.1简介
7.1.2硬件资源
7.2Arduino开发环境
7.2.1Arduino IDE下载及安装
7.2.2Arduino IDE操作基础
7.3Arduino程序基础知识
7.3.1Arduino程序的基本架构
7.3.2Arduino程序的基本函数
7.4应用实例
7.4.1LED闪烁实验
7.4.2模拟量读取实验
本章小结
思考题
参考文献
前言
随着计算机技术以及物联网的广泛应用,单片机在各领域的应用也随之扩大,基于51设计理念的单片机仍然占据着很大的市场,并且不断在翻新。如今单片机的应用已渗透到工业自动化、测控、家用电器、航空航天、卫星遥感等各个领域,因而高等院校工科类各专业普遍开设了单片机原理及应用课程。
2016年宁夏回族自治区“十三五”重点专业电气信息类重点建设专业群子项目——电气信息类工程应用型特色系列教材建设已正式启动,本书作为教材建设项目中的重点教材之一,以双一流建设为目标,加快追赶全国高等教育发展步伐,开展一流科研创新,传承和创新一流文化,转化一流成果,为加快开放、富裕、和谐、美丽宁夏建设做出贡献。
编者摒弃了以往同类单片机教材对MCS51单片机理论知识的烦琐描述,对难以理解的知识点,从典型性、实用性的设计实例出发进行讲解,注重原理和应用相结合,有助于学生自学和迅速提高,激发学生对单片机这一领域的学习兴趣。
本书共7章。前4章以单片机基本原理、体系结构、C51语言程序设计、单片机接口技术等内容为主,依托教学大纲,跳过传统的汇编语言,配合典型性、实用性的设计实例,帮助具有C语言基础的学生快速入门,加深对理论知识的理解。后3章以单片机综合应用设计、嵌入式系统开发为实践拓展。其中,第5章紧密结合日常实验教学和单片机课程设计,内容涉及LED数码管、键盘接口、A/D和D/A转换器、LCD液晶显示器接口、温度传感器、温湿度传感器、步进电机等典型的单片机外设,能够极大地激发学生的学习兴趣,帮助学生进一步提高单片机应用设计的能力; 第6、7章涉及的嵌入式系统开发实践内容主要来自研究生课程、本科毕业设计、宁夏大学大学生创新项目以及一线教师的项目成果,内容涉及MODBUS协议与应用、基于Arduino的系统开发,着眼于学生对理论知识的应用能力和对基本工程问题的解决能力,致力于培养学生良好的工程素养。
本书第1~4章由白娜编写,第5章5.1~5.4节由蔺金元编写、5.1、5.7节由车进编写,5.6、5.8节由陈潮红编写,第6章由刘大铭编写,第7章由孟一飞编写。全书由刘大铭统稿。
本书是宁夏回族自治区“十三五”电气信息类重点专业群建设的研究成果之一,并得到了该项目的资助; 同时也是宁夏大学西部一流专业计划“电子信息工程(卓越工程师方向)”建设的成果之一,并得到了该项目的资助。
在本书的编写过程中,编者参考了大量的教材和参考文献,在此谨向有关作者致以衷心的谢意。
由于编者水平有限,书中的疏漏之处在所难免,敬请读者指正。诚挚地希望得到读者使用本书的宝贵意见与建议。编者的Email: [email protected]。
编者2018年8月
CHAPTER 3
C51程序设计基础
在单片机应用系统的开发中,软件编程占有非常重要的地位。目前用于MCS51系列单片机编程的C语言主要采用Keil C51,简称C51。C51语言是在美国国家标准协会(ANSI)制定的C语言基础上针对51系列单片机的硬件特点进行的扩展。相比于传统的汇编语言,C51语言在功能、结构、可读性、可维护性上有明显的优势,易学易用,大大提高了工作效率,缩短了项目开发周期。现在,C51语言已经成为公认的高效、简洁的51单片机使用的高级编程语言。
3.1C51程序设计基础
C51语言在标准C语言的基础上,根据单片机存储器的硬件结构及内部资源,扩展了相应的数据类型和变量,而C51在语法规定、程序结构与设计方法上都与标准C语言相同。本节在标准C语言基础上介绍C51数据类型和存储类型、C51的基本运算与流程控制语句、C51语言数据类型、C51函数以及C51程序设计的其他一些问题,为C51的程序设计打下基础。
3.1.1C51的数据类型与存储类型
1. 数据类型
数据是单片机操作的对象,是具有一定格式的数字或数值,数据的不同格式就称为数据类型。C51支持的基本数据类型如表31所示。
表31C51支持的基本数据类型
数 据 类 型位数字节数说明
signed char81-128~ 127,有符号字符变量
unsigned char810~255,无符号字符变量
signed int162-32768~ 32767,有符号整型数
unsigned int1620~65535,无符号整型数
signed long324-2417483648~ 2417483647,有符号长整型数
unsigned long3240~4294967295,无符号长整型数
float324-3.4e-38~ 3.4e38
续表
数 据 类 型位数字节数说明
double324在C51中等同于float
*241~3对象指针
bit10或1
sfr810~255
sfr161620~65535
sbit1可进行位寻址的特殊功能寄存器的某位的绝对地址
2. C51的扩展数据类型
下面对扩展的数据类型进行说明。
1) 位变量bit
bit数据类型的值可以是1(true),也可以是0(false)。
2) 特殊功能寄存器sfr
51单片机的特殊功能寄存器分布在片内数据存储区的地址单元80H~FFH,sfr数据类型占用一个内存单元。利用它可以访问单片机内部所有的特殊功能寄存器。例如,sfr P1 = 0x90这一语句定义了P1端口在片内的寄存器,在程序后续语句中可以用P1=0xff使P1的所有引脚输出为高电平之类的语句来操作特殊功能寄存器。
3) 特殊功能寄存器sfr16
sfr16数据类型占用两个内存单元。sfr16和sfr一样用于操作特殊功能寄存器,不同的是它用于操作占两个字节的特殊功能寄存器。例如,sfr16 DPTR = 0x82语句定义了片内16位数据指针寄存器DPTR,其低8位的字节地址为82H,高8位的字节地址为83H。在程序的后续语句中可以对DPRT进行操作。
4) 特殊功能位sbit
sbit是指单片机内部特殊功能寄存器的可寻址位。例如:
sfr PSW =0xd0; /*定义PSW寄存器地址为0xd0*/
Sbit OV=PSW^2/*定义OV位为PSW.2*/
符号^前面是特殊功能寄存器的名字,^后面的数字定义特殊功能寄存器可寻址位在寄存器中的位置,取值必须是0~7。
注意,不要把bit和sbit混淆。bit用来定义普通的位变量,它的值只能是二进制的0或1; 而sbit定义的是特殊功能寄存器的可寻址位,它的值是可以进行位寻址的特殊功能寄存器的某位的绝对地址,如PSW寄存器OV位的绝对地址是0xd2。
3. 数据存储类型
在讨论C51的数据类型时,必须同时提及它的存储类型以及它与51单片机存储器结构的关系。C51完全支持51单片机硬件系统的所有部分。在51单片机中,程序存储器与数据存储器是完全分开的,且分为片内和片外两个独立的寻址空间,特殊功能存储器与片内RAM统一编址,数据存储器与I/O端口统一编址。C51编译器通过把变量和常量定义成不同存储类型的方法将它们定义在不同的存储器中。C51语言的存储类型与8051实际存储空间的对应关系如表32所示。
表32C51语言的存储类型与8051实际存储空间的对应关系
存储区存 储 类 型与存储空间的对应关系
DATAdata片内RAM直接寻址区,位于片内RAM的低128B
BDATAbdata片内RAM位寻址区,位于20H~2FH空间,允许位访问与字节访问
IDATAidata片内RAM的256B,必须是间接寻址的存储器
XDATAxdata片外RAM的全部空间,大小为64KB,由MOVX @DPTR访问
PDATApdata片外RAM的256B,由MOV @Ri访问
CODEcode程序存储区的64KB空间,使用DPTR寻址
1) 片内数据存储器
片内RAM可分为3个区域。
(1) DATA区。该区寻址是最快的,应该把经常使用的变量放在该区,但是DATA区的存储空间是有限的,除了包含程序变量外,还包含了堆栈和寄存器组。DATA区声明中的存储类型标识符为data,通常指片内RAM的128B的内部数据存储的变量,可直接寻址。DATA区变量声明举例如下:
unsigned char data system_status=0;
unsigned int data unit_id[8];
char data inp_string[16];
标准变量和用户自定义变量都可以存储在DATA区中,只要不超过DATA区的范围即可。由于C51使用寄存器组来传递参数,这样DATA区至少失去了8B的空间。另外,当内部堆栈溢出的时候,程序会莫名其妙地复位。这是因为51单片机没有报错机制,堆栈的溢出只能以这种方式表现,因此要留有较大的堆栈空间来防止堆栈溢出。
(2) BDATA区。该区是片内RAM中的位寻址区,在这个区中声明变量就可以进行位寻址。BDATA区声明中的存储类型标识符为bdata,指的是内部RAM可位寻址的16B存储区(字节地址为20H~2FH)中的128位。C51编译器不允许在BDATA区中声明float和double型变量。下面是在BDATA区声明位变量和使用位变量的例子:
unsigned char bdata status_byte;
unsigned int bdata status_word;
sbit stat_flag = status_byte^4;
if(status_word^15)
{…}
stat_flag = 1;
(3) IDATA区。该区使用寄存器作为指针来进行间接寻址,常用来存放使用得比较频繁的变量。与外部存储器寻址相比,IDATA区寻址的指令执行周期和代码长度相对较短。IDATA区声明中的存储类型标识符为idata,指的是片内RAM的256B存储区,只能间接寻址,其速度比直接寻址慢。IDATA区变量声明举例如下:
unsigned char idata system_status=0;
unsigned int idata unit_id[8];
char idata inp_string[16];
float idata out_value;
2) 片外数据存储器
PDATA区和XDATA区位于片外数据存储区,这两个区声明中的存储类型标识符分别为pdata和xdata。PDATA区只有256B,仅指定256B的外部数据存储区。但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;
3) 片外程序存储器
程序存储器CODE声明的标识符为code,存储的数据是不可改变的。在C51编译器中可以用存储器类型标识符code来访问程序存储区。
声明举例如下:
unsigned char code LED[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0Xf8,0x80, 0x90, 0x88, 0x83, 0xc6, 0xa1, 0x86, 0x8e}
对单片机编程时,正确地定义数据类型以及存储类型,是所有编程者在编程前首先要考虑的问题。在资源有限的情况下,如何节省存储单元并保证运行效率是对开发者的一个考验。上面的例子是八段共阳极数码管的真值表定义,因为这些数值固定不变,在数据存储空间有限的情况下,放到充裕的外部程序存储空间是个不错的选择。
对于定义变量的类型应考虑如下问题: 程序运行时该变量可能的取值范围,是否有负值,绝对值多大,以及相应需要多少存储空间。在够用的情况下,尽量选择8位即一个字节char型,特别是unsigned char型。对于51系列单片机而言,浮点类型变量将明显增加运算时间和程序长度。在可能的情况下,尽量使用灵活巧妙的算法来避免浮点变量的引入。
定义数据的存储类型通常遵循如下原则: 只要条件满足,尽量选择内部直接寻址的存储类型data,然后选择idata(即内部间接寻址)。对于那些经常使用的变量,要使用内部寻址,在内部数据存储器数量有限或不能满足要求的情况下才使用外部数据存储器。选择外部数据存储器可先选择pdata型,最后选用xdata型。
需要指出的是,扩展片外存储器在原理上虽很简单,但在实际开发中会带来不必要的麻烦,例如可能降低系统稳定性,增加成本,拉长开发和调试周期等,建议充分利用片内存储空间。另外,通常的单片机应用都是小型的控制应用,代码比较短,对于程序存储器的大小要求很低,常常是片内RAM很紧张而片内Flash ROM很富余,因此如果实时性要求不高可以考虑使用宏,并将一些子函数的常量数据做成数据表,放置在程序存储区,当程序运行时,进入子函数动态调用下载至RAM即可,退出子函数后立即释放该内存空间。
常量只能用code存储类型。变量存储类型定义举例如下:
char data a1;/*字符变量a1被定义为data型,分配在片内RAM低128B内*/
float idata x,y;/*浮点型变量x和y被定义为idata型,定位在片内RAM中,只能用间接寻址方式寻址*/
bit bdata p;/*位变量P被定义为bdata型,定位在片内RAM中的位寻址区*/
unsigned int pdata var1;/*无符号整型变量var1被定义为pdata型,定位在片外RAM中,相当于使用@Ri间接寻址*/
unsigned char xdata a[2][4];/*无符号字符型二维数组变量a[2][4]被定义为xdata存储类型,定位在片外RAM中,占据2×4=8B,相当于使用@DPTR间接寻址*/
4. 数据存储模式
如果在变量定义时略去存储类型标识符,编译器会自动默认存储类型,即存储类型进一步由SMALL、COMPACT和LARGE存储模式指令限制。例如,若声明char var1,则在SMALL存储模式下,varl被定义在DATA存储区; 在COMPACT模式下,var1被定位在IDATA存储区; 在LARGE模式下,var1被定义在XDATA存储区。
在固定的存储器地址上进行变量的传递,是C51标准特征之一。在SMALL模式下,参数传递是在片内数据存储区中完成的。LARGE和COMPACT模式允许参数在外部存储器中传递。C51也支持混合模式。例如,在LARGE模式下,生成的程序可以将一些函数放入SMALL模式下,从而加快执行速度。
1) SMALL模式
在本模式下所有变量都默认位于51单片机内部的数据存储器,这与使用data指定存储器类型的方式一样。本模式下变量访问的效率高,但所有的数据对象和堆栈必须使用内部RAM。
2) COMPACT模式
本模式下所有变量都默认在外部数据存储器的一页内,这与使用pdata指定存储器类型是一样的。该存储模式适用于变量不超过256B的情况,此限制由寻址方式决定,相当于用数据指针@Ri进行寻址。与SMALL模式相比,该存储模式的效率比较低,对变量访问的速度慢一些,但比LARGE模式快。
3) LARGE模式
在LARGE模式下,所有变量都默认位于外部数据存储器,相当于使用数据指针@DPTR进行寻址。通过数据指针访问外部数据存储器的效率低,特别是变量为2B或更多字节时,该模式要比SMALL和COMPACT产生更多的代码。
3.1.2C51的特殊功能寄存器及位变量定义
下面介绍C51如何对特殊功能寄存器以及位变量进行定义并访问。
1. 特殊功能寄存器的C51定义
C51语言允许使用关键字sfr、sbit或直接引用编译器提供的头文件来对特殊功能寄存器进行访问,特殊功能寄存器在片内RAM的高128B,只能采用直接寻址方式。
1) 使用关键字sfr定义特殊功能寄存器
为了能直接访问特殊功能寄存器,C51语言提供了一种定义方法,即引入关键字sfr,语法如下:
sfr 特殊功能寄存器名字 = 特殊功能寄存器地址;
例如:
sfr IE = 0xA8;/*中断允许寄存器地址A8H*/
sfr TCON = 0x88; /*定时/计数器控制寄存器地址88H*/
sfr SCON = 0x98; /*串行口控制寄存器98H*/
例如,要访问16位SFR,可使用关键字sfr16。16位SFR的低字节地址必须作为sfr16的定义地址,例如:
sfr16 DPTR=0x82; /*数据指针DPTR的低8位地址为82H,高8位地址为0x83*/
2) 通过头文件访问SFR
各种衍生型的51单片机的特殊功能寄存器的数量和类型有时是不相同的,对单片机特殊功能寄存器的访问可以通过头文件来进行。
为了用户处理方便,C51语言把51单片机(或52单片机)常用的特殊功能寄存器和其中的可寻址位进行了定义,放在一个reg51.h(或reg52.h)的头文件中。当用户要使用时,只需在使用之前用一条预处理命令#include把这个头文件包含到程序中,就可以使用特殊功能寄存器和其中的可寻址名称。用户可以通过文本编辑器对头文件进行增减。
头文件引用举例如下。
#include/*51型单片机的头文件*/
void main(void)
{TL0=0xF0; /*给定时器T0低字节TL0设置时间常数,已在reg51.h中定义*/
TH0=0x3F;
TR0=1;/*启动定时器0*/
…
}
3) 特殊功能寄存器中的位定义
对SFR中的可寻址位进行访问时,要使用关键字来定义可寻址位,共有3种方法。
(1) 定义格式为
sbit 位名 = 特殊功能寄存器^位置;
例如:
sfr PSW = 0xD0;/*定义PSW寄存器的字节地址0xD0H*/
sfr CY = PSW^7;/*定义CY为PSW.7,地址为0xD7H*/
sfr OV = PSW^2;/*定义OV为PSW.2,地址为0xD2H*/
(2) 定义格式为
sbit 位名 = 字节地址^位置;
例如:
sbit CY = 0xD0^7;/*CY地址为0xD7*/
sbit OV = 0xD0^2;/*OV地址为0xD2*/
(3) 定义格式为
sbit 位名 = 位地址;
这种方法将位的绝对地址赋给变量,位地址必须为0x80~0xFF,例如:
sbit CY= 0xD7; /*CY地址为0xD7*/
sbit OV= 0XD2; /*OV地址为0xD2*/
【例31】片内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) 位变量的定义
由于51单片机能进行位操作,C51扩展的bit数据类型用来定义位变量,这是C51与标准C语言的不同之处。C51采用关键字bit来定义位变量,一般格式为
bit 位变量名;
例如:
bit ov_flag; /*将ov_flag定义为位变量*/
bit lock_pointer; /*将lock_pointer定义为位变量*/
2) 包含类型为bit的参数的函数的定义
C51程序函数可以包含类型为bit的参数,也可以将其作为返回值。例如:
bit func(bit b0,bit b1); /*将位变量b0和b1作为函数func的参数*/
{…
return(b0); /*位变量b0作为函数的返回值*/
…
}
3) 位变量定义的限制
位变量不能用来定义指针和数组,例如:
bit *ptr; /*错误,不能用位变量来定义指针*/
bit array[]; /*错误,不能用位变量来定义数组array[]*/
在定义位变量时,允许定义存储类型,位变量都被放入一个位段,此段总是位于51单片机的片内RAM中,因此其存储类型限制为data或idata,如果将位变量定义成其他类型,则会导致编译错误。
3.1.3C51的绝对地址访问
如何对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区进行寻址。
例如:
#include
#define PORTA XBYTE[0xFFC0]/*将PORTA定义为外部I/O地址,地址为0xFFC0,长度为8位*/
#define NRAM DBYTE[0x50]/*将NRAM定义为片内RAM,地址为0x50,长度为8位*/
【例32】片内RAM、片外RAM及I/O定义的程序如下:
#include
#define PORTA XBYTE[0xFFC0]
#define NRAM DBYTE[0x50]
main()
{PORTA=0x3D;/*将数据3DH写入地址0xFFC0的外部I/O端口PORTA*/
NRAM=0x01; /*将数据01H写入片内RAM的0x50地址单元*/
}
2. _at_关键字
使用关键字_at_可对指定的存储空间的绝对地址进行访问,格式如下:
[存储器类型] 数据类型说明符 变量名_at_地址常数
其中,存储器类型为C51语言能识别的存储类型; 数据类型为C51支持的数据类型; 地址常数用于指定变量的绝对地址,必须位于有效的存储空间之内; 使用_at_定义的变量必须为全局变量。
【例33】使用关键字_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);
}
【例34】将片外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个单元内容清零,则程序如下:
data unsigned char buffer[8]_at_0x40;
void main(void)
{unsigned char i;
for(i=0;i<8;i )
{buffer[i]=0;
}
}
3.1.4C51的基本运算
C51语言的基本运算主要包括算术运算、逻辑运算、关系运算、位运算和赋值运算。
1. 算术运算
算术运算符及其说明如表33所示。
表33算术运算符及其说明
符号说明符号说明
加法运算%取模运算
-减法运算 自增
*乘法运算–自减
/除法运算
读者对表33中的运算符 、-、*,运算比较熟悉,但是对于/和%往往会有疑问。这两个符号都涉及除法运算,但/是取商,而%是取余数。例如,5/3的结果是1,而5%3的结果是2。
表33中的自增和自减运算符是使变量自动加1或自动减1,自增和自减运算符放在变量前和变量之后是不同的:
i,–i: 在使用i前,先使i值加1或减1。
i ,i–: 在使用i后,再使i值加1或减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. 逻辑运算
逻辑运算符及其说明如表34所示。
表34逻辑运算符及其说明
符号说明符号说明
&&逻辑与!逻辑非
||逻辑或
3. 关系运算
关系运算符用于判断两个数之间的关系。关系运算符及其说明如表35所示。
表35关系运算符及其说明
符号说明符号说明
>大于<=小于或等于
>=大于或等于!=不等于
4. 位运算
位运算符及其说明如表36所示。
表36位运算符及其说明
符号作用符号作用
&位与~位取反
|位或<^位异或>>位右移
在实际的控制应用中,常常需要改变I/O口中某一位的值,而不影响其他位,如果I/O口是可以位寻址的,这个问题就很简单。但有时外扩的I/O口只能进行字节操作,因此想在这种场合下实现单独的位控制,就要采用位操作。
【例35】编写程序将扩展的某I/O口PORTA(只能字节操作)的PORTA.5置0,PORTA.1置1,程序如下:
#include
#define PORTA XBYTE[0xFFC0]
void main()
{…
PORTA = (PORTA&0xDF)|0x02; /*和0相与得0,和1相或得1*/
…
}
上面的程序段中,第1行定义了一个片外I/O变量PORTA,其地址为片外数据存储区的0xFFC0。在main函数中,PORTA=(PORTA&0xDF)|0x02;的作用是: 先用运算符&将PORTA.5置0,然后利用运算符将PORTA.1置1。
5. 赋值、指针和取地址运算符
指针是C语言中一个十分重要的概念,将在后面介绍。在这里,先了解C语言中提供的赋值、指针和取地址运算符,如表37所示。
表37赋值、指针和取地址运算符
及其说明
符号作用
=赋值
*指针
&取地址
取内容和取地址的一般形式为
变量 = * 指针变量
指针变量 = &目标变量
赋值运算是将指针变量所指向的目标变量的值赋给左边的变量,取地址运算是将目标变量的地址赋给左边的变量。注意,指针变量只能存放地址,也就是指针型数据,一般情况下,不要把指针型数据赋值给一个指针变量。
3.1.5C51的分支与循环程序结构
在C51中有3种程序结构,即顺序、分支和循环结构。顺序结构是程序自上而下,从main函数开始一直到程序结束,程序只有一条路可走,没有其他的路径可以选择。顺序结构比较简单和容易理解,这里仅介绍分支结构和循环结构。
1. 分支控制语句
实现分支控制的语句有if和switch语句。
1) if语句
if语句用来判定所给定条件是否满足,根据判定结果决定执行两种操作之一。
if语句的基本结构如下:
if(表达式) {语句}
小括号中的表达式成立时,程序执行大括号中的语句,否则程序跳过大括号中的语句部分,而直接执行下面的其他语句。
C51提供了3种形式的if语句:
(1) 形式1。
if(表达式) {语句;}
例如:
if(x>y) {max=x;min=y;}
即如果x>y,则x赋值给max,y赋值给min。如果x>y不成立,则不执行大括号中的赋值运算。
(2) 形式2。
if(表达式) {语句1;} else {语句2;}
例如:
if(x>y){max=x;}
else {max=y;}
本形式相当于双分支选择结构。
(3) 形式3。
if(表达式) {语句1;}
else if(表达式) {语句2;}
else if(表达式) {语句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(表达式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语句。在执行一个case分支后,要使流程跳出switch结构,即中止switch语句的执行,可以用一条break语句完成。switch语句的最后一个分支不添加break语句,结束后直接退出switch结构。
【例36】在单片机程序设计中,常用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。如果键值为1,则执行键值处理函数key1()后返回; 如果键值为4,则执行键值处理函数key4()后返回; 执行完一个键值处理函数后,则跳出switch语句,从而达到按下不同的按键来进行不同的键值处理的目的。
2. 循环控制语句
许多实用程序都包含循环结构,熟练掌握和运用循环结构的程序设计是C51语言程序设计的基本要求。
实现循环结构的语句有以下3种: while语句、dowhile语句和for语句。
1) while语句
while语句的语法形式为
while(表达式)
{循环体语句;
}
表达式是while循环能否继续的条件,如果表达式为真,就重复执行循环体语句; 反之,终止循环体语句。
while循环结构的特点在于,循环条件的测试在循环体的开头,要想执行重复操作,首先必须进行循环条件的测试,如条件不成立,则循环体的语句一次都不能执行。
例如:
while(P1&0x80==0)
{ … }
while中的条件语句对单片机的P1口进行测试,如果P1.7为低电平(0),由于循环体无实际操作语句,故继续测试下去(等待),一旦P1.7的电平变高(1),则循环终止。
2) dowhile语句
dowhile语句的语法形式为
do
{循环体语句;
}
while(表达式);
dowhile语句的特点是: 先执行内嵌的循环语句,再计算表达式,如果表达式的值为非0,则继续执行循环体语句,直到表达式为0时结束循环。
由dowhile构成的循环与while循环十分相似,它们之间的重要区别是: while循环的控制出现在循环体之前,只有当while后面的表达式的值为非0时,才能执行循环体; 在dowhile构成的循环中,总是先执行一遍循环体,然后再求表达式的值,因此无论表达式的值是0还是非0,循环体至少要被执行一次。
和while循环一样,在dowhile循环体中,要有能使while后表达式的值为0的操作,否则循环会无限制地进行下去。根据经验,dowhile循环用得并不多,大多数的循环用while来实现。
【例37】实型数组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()之后的循环体在语法上要求是一条语句; 若在循环体内需要多条语句,应该用大括号括起来组成复合语句。
for的执行过程如下:
(1) 计算表达式1,表达式1通常为初值设定表达式。
(2) 计算表达式2,表达式2通常为终值表达式,若满足条件,转下一步,若不满足条件,则转步骤(5)。
(3) 执行一次for循环体。
(4) 计算表达式3,表达式3通常称为更新表达式,转向步骤(2)。
(5) 结束循环,执行for循环之后的语句。
下面对for语句的几个特例进行说明。
(1) for语句中的小括号的3个表达式全为空。
for(;;)
{循环体语句;}
它的作用相当于一个while(1),这将导致一个无限循环。在编程时,如果需要无限循环,则可采用这种形式的for循环语句。
(2) for语句的3个表达式中,表达式1为空。
例如:
for(;i <=100;i ) sum = sum i;
即不对i设初值。
(3) for语句的3个表达式中,表达式2为空。
例如:
for(i=1;;i ) sum = sum i;
即不判断循环条件,认为表达式始终为真,循环将无休止地进行下去。
(4) for语句的3个表达式中,表达式1、表达式3为空。
例如:
for(;i<100;)
{sum = sum i;
i ;
}
(5) 没有循环体的for语句。
例如:
int a = 1000;
for(t=0;t
{;}
本例的一个典型应用就是软件延时。
在程序设计中,经常用到时间延迟,可用循环机构来实现,即循环执行指令,消磨一段已知的时间。51单片机指令的执行时间就是靠一定数量的时钟周期来计时的,如果使用12MHz晶振,则12个时钟周期花费的时间为1μs。
【例38】编写一个延时1ms程序。
void delay_ms(unsigned char int j)
{unsigned char i;
while(j–)
{ for(i=0;i<125;i )
{;}
}
}
如果把上述程序编译成汇编语言代码进行分析,用for进行的内部循环大约延时8μs,但不是特别精确。不同编译器会产生不同的延时,因此i的上限值125应根据实际情况进行补偿调整。
【例39】无限循环的结构实现。
编写无限循环程序段,可使用以下3种结构。
(1) 使用while(1)结构:
while(1)
{循环体;
}
(2) 使用for(;;)结构。
for(;;)
{循环体;
}
(3) 使用dowhile结构。
do
{循环体;
} while(1);
3. break语句、continue语句和goto语句
在循环体语句执行过程中,如果在满足循环判定条件的情况下跳出代码段,可以使用break语句或continue语句; 如果要从任意地方跳转到代码的某个地方,可以使用goto语句。
1) break语句
前面已介绍过break语句可以跳出switch循环体。在循环结构中,可应用break语句跳出循环体,从而马上结束循环。
2) continue语句
continue语句的作用及用法与break语句类似,区别在于: 当前循环遇到break时,是直接结束循环; 若遇到continue,则是停止本次循环,然后直接尝试下一次循环。
可见,continue并不结束整个循环,而仅仅是中断本次循环,然后跳到循环条件处,继续下一次循环。当然,如果跳到循环条件处,发现条件已不成立,那么循环也会结束。
3) goto语句
goto语句是一个无条件转移语句,当执行goto语句时,将程序指针跳转到goto给出的代码行。基本格式如下:
goto 标号
【例310】计算正数1~100的累加值,存放在sum中。
void main(void)
{unsigned char i;
int sum;
sumadd:
sum = sum i;
if(i<101)
{goto sumadd;
}
}
goto语句在C51中经常用于无条件跳转到某条必须执行的语句以及用于在死循环程序中退出循环。为了方便阅读,也为了避免跳转时引发错误,在程序设计中要谨慎使用goto语句。
3.1.6C51的数组
在单片机的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[3][4]/*array2数组,有3行4列共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]={‘N’, ‘I’, ‘N’, ‘G’, ‘ ‘, ‘X’, ‘I’, ‘A’, ‘\0’ };/*字符串数组*/
定义了一个字符型数组a[],有10个数组元素,并且将9个字符(其中包含一个字符串结束标志’\0′)分别赋给了a[0]~a[8],剩余的a[9]被系统自动赋予空格字符。
C51还允许用字符串直接给字符数组置初值。例如:
char a[10] = {“NING XIA”};
用双引号括起来的一串字符称为字符串常量,C51编译器会自动地在字符串末位加上结束符’\0’。
用单引号括起来的字符表示该字符的ASCII码值,而不是字符本身。例如,’a’表示a的ASCII码值61H。而”a”表示一个字符串,由两个字符a和’\0’组成。
一个字符串可以用一维数组存储,但数组的元素数目一定比字符数多一个,以便C51编译器自动在其后面加入结束符’\0’。
2. 数组的应用
在C51程序设计中,数组一个非常有用的功能就是查表。对于数学运算,编程者更愿意采用查表计算而不是公式计算。例如,对于传感器的非线性转换需要进行补偿,使用查表法就有效得多。再如,LED显示程序中根据要显示的数值,找到相应的显示段码送到LED显示器显示。表可以事先计算好后装入程序存储器中。
【例311】使用查表法,计算0~9的平方。
#define uchar unsigned char
uchar code square[] = [0,1,4,9,16,25,36,49,64,81];
/*0~9的平方表,在程序存储器中*/
uchar function(uchar number)/*function函数定义*/
{ return square[number];}/*返回平方值*/
main()
{result = function(7); /*函数function实参为7,其平方49存入result单元*/
}
在程序的开始处,uchar code square[] = [0,1,4,9,16,25,36,49,64,81];定义了一个无符号字符型的数组square[],并对其进行了初始化,将数0~9的平方值赋予数组square[],类型代码code指定编译器将平方表存储在程序存储器中。主函数调用函数function(),此时function(7)的结果就是返回square这个数组下标为7的第8个元素49。赋值给result后,result的结果就是49。
3. 数组与存储空间
当程序中设定了一个数组时,C51编译器会在系统的存储空间中开辟一个区域,用于存放数组中的内容。数组就包含在这个由连续存储单元组成的模块的存储体内。字符数组占据了存储空间中一连串的字节位置,整型(int)数组在存储空间占据一连串连续的字节对的位置,长整型(long)数组或浮点型(float)数组的一个成员占有4B的存储空间。
当一维数组被创建时,C51编译器就会根据数组的类型在内存中开辟一块大小等于数组长度乘以数据类型长度(即类型占有的字节数)的区域。
对于二维数组a[m][n]而言,其存储顺序是按行存储,即,先存第0行元素的第0列、第1列、第2列,直至第n-1列,然后存第1行元素的第0列、第1列、第2列,直至第n-1列,以此类推,直到第m-1行的第n-1列。
当数组特别是多维数组中大多数元素没有被有效利用时,就会浪费大量的存储空间。对于51单片机,没有大量的存储区,其存储资源极为有限,因此在进行C51程序开发时,要根据需要仔细选择数组的大小。
3.1.7C51的指针
C51支持基于存储器的指针和一般指针两种类型的指针。当定义一个指针变量时,若未给出它所指向的对象的存储类型,则指针变量被认为是一般指针; 反之,若给出了它所指向对象的存储类型,则该指针被认为是基于存储器的指针。
基于存储器的指针类型由C51语言源代码中的存储类型决定,用这种指针可以高效地访问对象,且只需1~2字节。
一般指针占用3个字节: 1个字节为存储类型,2个字节为偏移量。存储类型决定了对象所用的8051的存储空间,偏移量指向实际地址。一个一般指针可以访问任何变量而不管它在8051存储器中的位置。
1. 基于存储器的指针
在定义一个指针时,若给出了它所指对象的存储类型,则该指针是基于存储器的指针。基于存储器的指针以存储类型为变量,在编译时才被确定。因此,地址选择存储器的方法可以省略,这些指针的长度可为1个字节(idata*,data*,pdata*)或2个字节(code*,xadta*)。在编译时,这类操作一般被“内嵌”编码,无须进行库调用。
基于存储器的指针定义举例:
char xdata px*;
在XDATA存储器中定义一个指向字符类型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个字节存储类型,如表38所示。
其中,第一个字节代表指针的存储器类型,存储器类型的编码如表39所示。
表38一般指针
地址存 储 内 容
0存储器类型
1偏移量高位
2偏移量低位
表39存储器类型编码
存储器类型编码值
idata/data/bdata0x00
xdata0x01
pdata0xFE
Code0xFF
例如,以xdata类型的0x1234地址作为指针可表示成如表310所示。
表3100×1234的表示
地址存 储 内 容
00×01
10×12
20×34
当常数作为指针时,须注意正确定义存储器类型和偏移量。
例如,将常数值0x41写入地址0x8000的外部数据存储器:
#define XBYTE((char*)0x10000L)
XBYTE[0x8000] = 0x41;
其中,XBYTE被定义为(char*)0x10000L,0x10000L为一般指针,其存储类型为0x01,偏移量为0000。这样,XBYTE成为指向xdata零地址的指针,而XBYTE[0x8000]则是外部存储器0x8000的绝对地址。
C51编译器不检查指针常数,用户必须选择有实际意义的值。利用指针变量可以对内存地址直接操作。
3.2C51的函数
3.2.1函数的分类
一个C51源程序是由一个个模块化的函数所构成的,函数是指程序中的一个模块,main函数为程序的主函数,其他若干个函数可以理解为一些子程序。一个C51源程序无论包含了多少函数,都是从main函数开始执行的,不论main函数位于程序的什么位置。程序设计者就是编写一系列的函数模块,并在需要的时候调用这个函数,实现程序所要求的功能。
从结构上分,C51语言函数可分为主函数main和普通函数两种。而普通函数又划分为两种: 标准库函数和用户自定义函数。
1. 标准库函数
标准库函数是由C51编译器提供的。编程者在进行程序设计时,应该善于充分利用这些功能强大、资源丰富的标准库资源,以提高编程效率。
用户可以直接调用C51库函数而不需为该函数写任何代码,只需要包含具有该函数说明的头文件即可。例如调用输出函数printf时,要求程序在调用输出函数前包含以下的include命令:
# include
2. 用户自定义函数
用户自定义函数是用户根据需要编写的函数。从函数定义的形式分为无参函数、有参函数和空函数。
1) 无参函数
无参函数在被调用时,既无参数输入,也不返回结果给调用函数,只是为完成某种操作而编写的函数。
无参函数定义形式为
返回值类型标识符 函数名()
{函数体;
}
无参函数一般不带返回值,因此函数的返回值类型的标识符可省略。
例如,main函数为无参函数,返回值类型的标识符可以省略,默认是int类型。
2) 有参函数
调用有参函数时,必须提供实际的输入参数。有参函数的定义形式为
返回值类型标识符 函数名(形式参数列表)
形式参数说明
{函数体;
}
【例312】定义一个函数max,用于求两个数中的大数。
int a,b;
int max(a,b)
{if(a>b)return a;
else return b;
}
上面的程序中,a、b为形式参数,return为返回语句。
3) 空函数
空函数体内是空白的,调用空函数时,什么工作也不做,不起任何作用。定义空函数的目的并不是为了执行某种操作,而是为了以后程序功能的扩充。先将一些基本模块的功能函数定义成空函数,占好位置,并写好注释,以后再用一个编好的函数代替它。这样整个程序结构清晰,可读性好,以后扩充新功能方便。空函数的定义形式为
返回值类型标识符 函数名()
{}
例如:
float min()
{ } /*空函数,占好位置*/
3.2.2函数的参数与返回值
1. 函数的参数
C语言可以在函数之间传递参数,使一个函数能对不同的变量进行功能相同的处理,从而大大提高了函数的通用性与灵活性。
函数之间的参数传递由主函数调用时主调函数的实际参数与被调函数的形式参数之间进行参数传递来实现。被调函数的最后结果由被调函数的return语句返回给主调函数。
函数的参数包括形式参数和实际参数。
1) 形式参数
函数的函数名后面括号的变量名称为形式参数,简称形参。
2) 实际参数
在函数调用时,主调函数后面括号里的表达式是实际参数,简称实参。
在C语言的函数调用中,实际参数与形式参数之间的数据传递是单向的,只能由实际参数传递给形式参数,而不能由形式参数传递给实际参数。
实参与形参必须类型一致,否则会发生类型不匹配的错误。被调函数的形参在函数未调用之前并不占用实际内存单元。只有当函数调用发生时,被调函数的形式参数才分配给内存单元,此时内存单元中主调函数的实际参数和被调函数的形式参数位于不同的单元。在调用结束后,形式参数所占用的内存被系统释放,而实际参数所占用的内存单元保留并维持原值。
2. 函数的返回值
函数的返回值是通过函数中的return语句获得的。一个函数可以有一个以上的return语句,但两个或两个以上的return语句必须在选择结构(if或switch)中使用,因为被调函数只能返回一个变量。
函数返回值的类型一般在定义函数时由返回值的标识符来指定。例如在函数名之前的int指定函数的返回值的类型为整型(int)。若没有指定函数的返回值类型。默认返回值为整形。当函数没有返回值时,则使用标识符void进行说明。
3.2.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) 如果程序中使用了自定义函数,且该函数与调用它的函数在同一个文件中,则应根据主调函数与被调函数在文件中的位置,决定是否对被调函数作出说明。如果被调函数在主调函数之后,一般应在主调函数中,在被调函数调用之前,对被调函数的返回值类型作出说明。如果被调函数出现在主调函数之前,不用对被调函数作说明。如果在所有函数定义之前,在文件的开头处,在函数的外部已经说明了函数的类型,则主调函数中不必对被调函数返回值类型进行说明。
3.2.4中断服务函数
由于标准C语言没有处理单片机中断的定义,为直接编写中断服务程序,C51编译器对函数的定义进行了扩展,增加了一个扩展关键字interrupt,使用该关键字可以将一个函数定义成中断服务函数。由于C51编译器在编译时对声明为中断服务程序的函数自动添加了相应的现场保护、阻断其他中断、返回时自动恢复现场等处理的程序段,因而在编写中断服务函数时可不考虑这些问题,减轻了用汇编语言编写中断服务函数的烦琐程度,而把精力放在如何处理引发中断请求的事件上。
中断服务函数的一般形式为
函数类型 函数名(形式参数表)interrupt n using n
关键字interrupt后面的n是中断号,对于MCS51系列单片机来说,取值为0~4,编译器从8n 3处产生中断向量。MCS51系列单片机中断源对应的中断号和中断向量见表311。
表311中断源对应的中断号和中断向量
中断源中断号中断向量8n 3
外部中断000003H
定时/计数器01000BH
外部中断120013H
定时/计数器1300B3H
串行口40023H
关键字using后面的n是所选择的寄存器组号。在定义一个函数时,using是一个选项,可以省略。如果不选用该项,则由编译器选择一个寄存器区作为绝对寄存器区访问。
例如,外部中断1的中断服务函数书写如下:
void int1() interrupt 2 using 0 /*中断号n=2,选择2号寄存器组*/
3.2.5变量及存储方式
1. 变量
变量根据其有效范围分为局部变量和全局变量。
1) 局部变量
局部变量是在某一个函数中存在的变量,它只在该函数内部有效。
2) 全局变量
全局变量是在整个源文件中都存在的变量。有效区间是从定义点开始到源文件结束,其中所有的函数都可以直接访问该变量。如果定义前的函数需要访问该变量,则需要使用extern关键字对其进行说明; 如果全局变量声明文件之外的源文件需要访问该变量,也需要使用extern关键字对其进行说明。
由于全局变量一直存在,占用了大量的内存单元,且加大了程序的耦合性,不利于程序的移植或复用。全局变量可以使用static关键字定义,该变量只能在变量定义的源文件引用,这种全局变量称为静态全局变量。如果一个文件的非静态全局变量需要被另一个文件引用,则需要在该文件调用前使用extern关键字对该变量进行说明。
2. 变量的存储方式
单片机的存储区可以分为程序存储区、静态存储区和动态存储区3个部分。数据存放在静态存储区或动态存储区。其中全局变量在静态存储区,在程序开始运行时,给全局变量分配存储空间; 局部变量存放在动态存储区,在进入拥有该变量的函数时,给这些变量分配存储空间。
3.2.6宏定义与文件包含
在C51程序设计中经常要用到宏定义和文件包含。
1. 宏定义
宏定义语句属于C51语言的预处理指令,使用宏可以使变量书写简化,增加程序可读性、可维护性和可移植性。宏定义分为简单的宏定义和带参数的宏定义。
1) 简单的宏定义
格式如下:
#define 宏替换名 宏替换体
#define是宏定义指令的关键字,宏替换名一般用大写字母来表示,而宏替换体可以是数值常数、算术表达式,字符和字符串等。宏定义可以出现在程序的任何地方,例如:
#define uchar unsigned char
在编译时可由C51编译器把unsigned char用uchar来替换。
例如,在某程序开头处进行了3个宏定义:
#define uchar unsigned char/*宏定义无符号字符型,方便书写*/
#define uint unsigned int/*宏定义无符号整型,方便书写*/
#define gain 4 /*宏定义增益值*/
由上可见,宏定义不仅可以方便无符号字符型和无符号整型变量的书写,而且当增益值需要变化时,只需要修改gain的宏替换体4即可,而不需要在程序每处修改,大大增强了程序的可读性和可维护性。
2) 单参数的宏定义
格式如下:
#define 宏替换名(形参) 带形参宏替换体
带参数的宏定义可以出现在程序的任何地方,在编译时可由编译器替换为定义的宏替换体,其中的形参用实际参数替换,由于可以带参数,这就增强了宏定义的应用。
2. 文件包含
文件包含是一个程序文件将另一个指定文件的内容包含进去。文件包含的一般格式为
#include
或
#include “文件名”
上述两种格式的差别是: 采用格式时,在头文件目录中指定文件; 采用”文件名”格式时,应当在当前的目录中查找指定文件。例如:
#include/*将51单片机的特殊功能寄存器头文件reg51.h包含到程序中*/
#include/*将标准的输入输出头文件stdio.h包含到程序中*/
#include /*将函数库中专用数学库的函数包含到程序中*/
当程序中需要调用C51语言编译器提供的各种库函数时,必须在文件的开头使用#include命令将相应函数的说明文件包含进来。
3.2.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中,其中包括复制、移动、比较等函数。
3.3C51的开发工具
Keil C51语言是德国Keil Software公司开发的用于51系列单片机的语言软件。Keil C51在兼容标准C语言的基础上,又增加了很多与51单片机硬件相关的编译特性,使得在51系列单片机上开发应用程序更为方便和快捷,生成的程序代码运行速度快,所需要的存储器空间小,完全可以和汇编语言相媲美。它支持众多的8051架构的芯片,同时集编辑、编译、仿真等功能于一体,具有强大的软件调试功能,是众多单片机应用开发软件中最优秀的软件之一。
3.3.1集成开发环境Keil μVision4简介
目前,Keil C51已被完全集成到一个功能强大的全新集成开发环境(Integrated Development Environment,IDE)Keil μVision4中,Keil Software公司推出的Keil μVision4是一款用于51单片机的Windows下的集成开发环境,提供了对基于8051内核的各种型号单片机的支持,为51单片机软件开发提供了全新的C语言开发环境。该开发环境下集成了文件编辑处理、编译链接、项目(project)管理、窗口、工具引用和仿真软件模拟器等多种功能,所有这些功能均可在Keil μVision4的开发环境中极为简便地进行操作。
注意: 本书中Keil C51一般简称C51,指的是51系列单片机编程所用的编程语言; Keil μVision4简称μVision4,指的是用于51系列单片机的C51程序编写、调试的集成开发环境。
C51程序的开发是在Keil μVision4开发环境下进行的,开发者可以购买Keil μVision4软件,也可以在Keil Software公司的主页免费下载评估版本。Keil μVision4在Keil μVision3 IDE的基础上增加了更多大众化的功能。例如,多显示器和灵活的窗口管理系统,系统浏览器窗口的显示设备外设寄存器信息,调试还原视图创建并保存多个调试窗口布局,多项目工作区简化与众多的项目。Keil μVision4旨在提高开发人员的生产力,实现更快、更有效的程序开发。
3.3.2Keil μVision4软件的安装、启动和应用程序设计
1. 软件安装
Keil μVision4集成开发环境的安装同大多数软件一样,根据提示一步一步进行。Keil μVision4安装完毕后,可在桌面上看到Keil μVision4软件的快捷图标。
2. 软件启动
单击桌面上的Keil μVision4软件的快捷图标,即可启动该软件,出现启动界面,如图31所示。
图31Keil μVision4启动界面
3. 应用程序设计
Keil μVision4把用户的每一个应用程序设计当作一个项目,用项目管理的方法把一个应用程序设计需要使用的程序关联到同一个项目中。这样,打开一个项目时,关联程序也都跟着进入了调试窗口,方便用户对项目中各个程序进行编写、调试和存储。因此,在编写一个新的应用程序前,一定要先建立一个项目。下面以一个简单程序实例演示一个工程项目开发的过程。
1) 建立工程项目文件
在菜单栏中选择Project→New μVision Project命令,建立名为LED的工程文件,保存工程到指定目录以便于管理。在此过程中,首先要选择工程所使用的芯片,如图32所示,51内核是由Intel公司制造的,这里选定Intel公司的80/87C52来代替。
图32芯片选择窗口
单击OK按钮后,如图33所示,会弹出一个对话框询问是否添加启动代码,每个工程都需要一段启动代码,选择“是”,启动代码会自动添加到工程中。文件STARTUP.A51是8051系列CPU的启动代码,主要用来对CPU数据存储器进行清零,并初始化硬件和重入函数堆栈指针等。
2) 建立源程序并添加到工程中
在菜单栏选择File→New命令,打开文件编辑器,这就是编写源程序的工作区,由键盘输入源程序,实现单片机P1.0口点亮LED小灯,如图34所示; 接着,选择File→Save命令,将文件命名为LED.c,保存到刚才创建的工程目录下便于管理; 最后将该文件添加到工程中,右击Source Group 1,选择Add Files to Group ‘Source Group 1’命令,即可完成添加。
图33询问是否添加启动代码的对话框
图34源程序编辑界面
3) 工程设置
在菜单栏选择Project→Option for Target ‘Target 1’命令,弹出如图35所示的工程设置对话框。
图35工程设置的Output选项卡
在工程设置对话框中,须特别注意以下两个选项:
(1) 默认选项卡Target下的Xtal(MHz)选项: 用来设置单片机的工作频率,默认值是所选单片机的最高可用频率值。如果使用的是其他频率的晶振,直接在该选项后输入频率值即可,本例输入12MHz。
(2) 选项卡Output下的Create HEX File选项: 选中该项,编译后即可生成名为LED.hex的HEX文件,这个文件用于下载到单片机硬件中,或者在Proteus仿真环境中下载到单片机仿真电路中。这里选中该项,方便联合仿真时使用。
4) 编译源程序,生成目标代码HEX文件
图36编译输出信息窗口
在菜单栏选择Project→Rebuild all target files命令,对源程序进行编译,生成需要下载到单片机里的HEX文件。如图36所示,如果有错误,开发环境下方的Build Output窗口会有相关提示,可以据此返回检查程序错误或进入调试状态; 如果没有错误,Build Output窗口会提示“0 Error(s),0 Warning(s)”表示程序没有错误和警告,同时出现“creating hex file from “LED”…”,表示当前工程目录下已经生成HEX文件,为下一步与Proteus的联合调试做好了准备。
5) 调试运行并查看结果
程序编译没有错误后,就可以进行调试与仿真。在菜单栏选择Debug→Start/Stop Debug Session命令,进入程序调试状态。
在菜单栏选择Peripherals→I/O Ports→Port 1命令,即可弹出如图37所示的窗口,其中显示了P1口各位的状态。
图37示例程序运行效果
在程序调试时,允许程序全速运行和单步运行。
全速运行: 选择Debug→Run命令,直接看到程序运行的总结果。
单步运行: 选择Debug→Step命令,每次只执行一条程序; 选择Debug→Step Over命令,以过程单步形式执行程序,即将函数或子程序作为整体一次执行。
此外,还可以选择Debug→Run to Cursor Line命令,全速运行至光标所在行; 选择Debug→Stop Running命令,停止运行程序。
3.4软件仿真开发工具Proteus
在单片机应用系统设计中,除了3.3节介绍的Keil μVision4集成开发环境,还有一个单片机虚拟仿真软件Proteus被广泛使用。熟练地掌握Proteus和Keil μVision4工具软件的使用以及它们的联合仿真调试,会使单片机应用系统设计及编程的效率大大提高。
3.4.1Proteus简介
Proteus软件是英国Labcenter Electronics公司开发的EDA工具软件,它可以仿真各种模拟器件和集成电路,包括ISIS.EXE、ARES.EXE两个主要程序,ISIS的主要功能是电路原理图设计和与电路原理图的交互仿真,ARES主要用于印制电路板的设计。
ISIS提供的Proteus VSM (Virtual System Modeling)实现了混合式的SPICE电路仿真,它将虚拟仪器、高级图表应用、单片机仿真、第三方程序开发与调试环境有机结合,在搭配硬件模型之前即可在PC上完成原理图设计、电路分析与仿真及单片机程序实时仿真、测试及验证。Proteus具有如下特点:
(1) 具有强大的原理图绘制功能,实现了单片机仿真和SPICE电路仿真相结合。具有模拟电路、数字电路、单片机及其外围电路组成的系统、RS232串行通信、I2C调试器、SPI调试器仿真的功能; 有各种虚拟仪器,如示波器、逻辑分析仪、信号发生器等。
(2) 支持主流单片机系统的仿真。目前支持的单片机类型有51系列、AVR系列、PIC系列等多种系列单片机以及各种外围芯片。
(3) 提供软件调试功能。在硬件仿真系统中具有全速、单步、设置断点等调试功能,同时可以观察各个变量、寄存器等当前状态,并支持第三方的软件编译和调试环境,如Keil μVision4等软件。
总之,Proteus软件是一款功能极其强大的单片机系统的软件仿真开发工具。在系统开发中,一般是先用Proteus设计出系统的硬件电路,编写程序,然后在Proteus环境下仿真调试通过,接下来,按照仿真的结果,完成实际的硬件设计。最后,将仿真通过的程序烧录到单片机中,然后安装到用户样机硬件板上观察运行结果,如有问题,再连接仿真器进行分析、调试。如没问题,调试完毕的软件可将机器代码固化在程序存储器中,一般就能直接投入运行了。
3.4.2Proteus与Keil μVision4的联合仿真
本节主要利用Proteus ISIS进行单片机系统的原理图设计,并实现与Keil μVision4的联合仿真。限于篇幅,本节将直接介绍如何用Proteus仿真单片机电路图以及与Keil μVision4的联合仿真,有关Proteus安装及详细功能请读者参阅专门书籍。
下面紧接着3.3节中的实例来演示仿真软件Proteus的基本操作。
1. 启动Proteus ISIS
启动Proteus ISIS 7.8,启动界面如图38所示。
2. 从元器件库中拾取相关器件
从元器件库中拾取相关器件,为搭建由单片机点亮P1.0口所连接LED灯的电路做好准备。所需器件为单片机、数码管。单击主界面的P图标(Pick Device),打开如图39所示的界面,在Keywords文本框中输入要查询的器件的关键字,这里输入80C51作为关键字,找到后双击器件即可添加到左侧DEVICES列表中,供仿真电路使用。
选取LED灯的操作类似,这里选择LEDGREEN。
图38Proteus ISIS 7.8的启动界面
图39Proteus选取器件界面
3. 搭建单片机仿真电路
搭建单片机仿真电路,如图310所示。用鼠标将拾取的器件拖至空白图纸处,适当布局,并以一定的方式连线即可。在本例中,对LED灯进行了Rotate AntiClockwise操作,阳极接电源,阴极接P1.0口。其中,5V的电源来自左侧图标Terminals Mode里的Power。最后将电路保存为LED.DSN。
图310Proteus单片机仿真电路效果图
注意: 限于篇幅,在不影响仿真结果的前提下,这里其实省去了单片机的最小系统电路,即晶振电路、复位电路及电源电路,这在Proteus中是允许的。读者可以自行添加这些电路,进一步完善单片机仿真电路。
4. 实现Proteus与Keil μVision4的联合仿真
在Proteus环境下建立了单片机仿真电路后,没有加载程序是不可能运行的,此时需要将Keil环境下生成的HEX文件加载到单片机模型中。
图311加载HEX文件窗口
双击Proteus环境中的单片机芯片,如图311所示,在弹出的Edit Component对话框的Program File框中选择之前生成的LED.hex文件,并单击OK按钮确认,这样就相当于把源程序的HEX文件烧写进单片机芯片中了。
5. 运行仿真电路
在仿真电路和程序都没有问题时,直接单击Proteus主窗口左下角的运行按钮,即可仿真运行单片机系统,结果如图312所示,绿色LED小灯被点亮。在运行过程中,可以像在硬件环境中一样与单片机交互。
图312Proteus与Keil μVision4的联合仿真效果图
3.4.3Proteus与Keil μVision4的联合调试
对于较为复杂的程序,如果运行没有达到预期效果,这时可能需要对Proteus与Keil μVision4进行联合调试,联合调试前需要安装vdmagdi.exe(可到Proteus的官方网站下载),然后在Proteus与Keil μVision4中进行联合设置。
联合调试时,先打开Proteus案例(不要运行案例),选中Debug菜单中的Use Remote Debug Monitor命令,这使得Proteus能与Keil μVision4进行通信。
完成上述设置后,再到Keil μVision4中打开工程,选择菜单Project→Option for Target ‘Target 1’命令,打开如图313所示的项目选项对话框。在Debug选项卡中选中右边的Use单选按钮及其右侧下拉列表中的Proteus VSM Simulator。如果Proteus与Keil μVision4安装在同一台PC中,右边Setting中的Host与Port可保持默认值127.0.0.1与8000不变,在跨计算机调试时则需要进行相关修改。
图313Keil μVision4项目选项对话框
完成上述设置后,在Keil μVision4中全速运行程序时,Proteus中的单片机系统也会自动运行。如果希望观察运行过程中某些变量值或设备状态,需要在Keil μVision4中恰当使用Step In、Step Over、Step Out、run to Cursor Line及Breakpoint命令进行跟踪,要注意的是,并非在任何时候都可以使用它们。例如,键盘矩阵扫描时就不能用单步跟踪,因为程序运行到某一步骤时,如果按键后再到Keil中继续单步跟踪,这时按键早就释放了。
本章小结
目前用于MCS51系列单片机编程的C语言都采用Keil C51,简称C51。本章首先介绍了C51语言的数据结构、基本运算、程序结构、C51函数等内容,最后介绍了Keil C51集成开发环境Keil μVision4软件的安装、启动和应用程序设计,以及软件仿真开发工具Proteus与Keil μVision4的联合仿真和联合调试。
思考题
31C51语言有哪些新增的数据类型?
32C51有哪些语句类型?使用每种类型的语句编写一个简单的程序。
33C51程序有哪些常用的头文件?怎样在程序中使用它们?
34使用C51编程语言实现将片内数据存储器中地址30H和40H的单元内容交换。
35举例说明利用Keil μVision4与Proteus软件仿真一个单片机实验的全过程。
36Proteus与Keil μVision4的联合仿真和联合调试的区别是什么?
![插图](https://static.easterneast.com/file/easternspree/img/5e0978e35f9849305ddc596f_466880.jpg)
![插图](https://static.easterneast.com/file/easternspree/img/5e0978ef5f9849305ddc5970_466881.jpg)
![插图](https://static.easterneast.com/file/easternspree/img/5e0978fa5f9849305ddc5971_466882.jpg)
![插图](https://static.easterneast.com/file/easternspree/img/5e0978ff5f9849305ddc5972_466883.jpg)
![插图](https://static.easterneast.com/file/easternspree/img/5e09790f5f9849305ddc5973_466884.jpg)
![插图](https://static.easterneast.com/file/easternspree/img/5e0979145f9849305ddc5974_466885.jpg)
评论
还没有评论。