1. C51程序与函数

Download Report

Transcript 1. C51程序与函数

第14章
单片机C语言
程序设计基础
1
第14章
目录
14.1 编程语言Keil C51简介
14.1.1 Keil C51简介
14.1.2 Keil C51的开发环境
14.1.3 C51与标准C的主要区别
14.2 C51语言程序设计基础
14.2.1 C51语言中的数据
14.2.2 C51的位变量定义
14.2.3 一个简单的C51程序
14.2.4 C51的运算符
14.2.5 C51的分支与循环程序结构
2
14.2.6 AT89S51不同存储区的C51定义
14.2.7 C51中断服务函数的定义
14.3 C51的程序设计举例
14.3.1 中断程序的编写
14.3.2 定时器程序的编写
14.3.3 串行口方式0应用程序的编写
14.3.4 独立式键盘查询方式
14.3.5 行列式键盘查询方式
14.3.6 DAC0832应用程序的编写
3
14.3.7 ADC0809应用程序的编写
14.4 C51的集成开发环境Keil µVision3介绍
14.4.1 集成开发环境Keil µVision3简介
14.4.2 Keil µVision3软件的安装、启动和运行
14.4.3 C51程序的开发流程
14.5 C51与汇编语言的混合编程
14.5.1 C51与MCS-51汇编语言的比较
14.5.2 C51与汇编语言混合编程的方法
4
内容概要
本章在假定读者已掌握标准C语言前提下,初步介绍
如何使用C51来编写AT89C51单片机的应用程序。
C51是在标准C的基础上,根据单片机存储器硬件结
构及内部资源,扩展了相应的数据类型和变量,而C51在
语法规定、程序结构与设计方法上,都与标准C相同。
本章重点介绍C51对标准C所扩展的部分,并通过一
些例程来介绍C51的程序设计思想。最后还对C51的集成
开发环境Keil µVision3以及C51与汇编语言的混合编程作
以介绍。
5
14.1 编程语言Keil C51简介
目前51系列单片机编程的C语言都采用Keil C51(简称
C51),Keil C51是在标准C语言基础上发展起来的。
14.1.1 Keil C51简介
C语言是美国国家标准协会(ANSI)制定的编程语言
标准,1987年ANSI公布87 ANSI C,即标准C语言。
Keil C51语言是在ANSI C的基础上针对51单片机的硬件
特点进行的扩展,并向51单片机上移植,经过多年努力,
C51语言已经成为公认的高效、简洁而又贴近51单片机硬
件的实用高级编程语言。
6
目前大多数的51单片机用户都在使用C51语言来进行
程序设计。
用C51进行单片机软件开发,有如下优点:
(1)可读性好。C51语言程序比汇编语言程序的可读性
好,因而编程效率高,程序便于修改。
(2)模块化开发与资源共享。用C51开发出来的程序模
块可以不经修改,直接被其他项目所用,这使得开发者能
够很好地利用已有的大量的标准C程序资源与丰富的库函
数,减少重复劳动。
7
(3)可移植性好。为某种型号单片机开发的C语言程序,
只需将与硬件相关之处和编译连接的参数进行适当修改,
就可以方便地移植到其他型号的单片机上。例如,为51单
片机编写的程序通过改写头文件以及少量的程序行,就可
以方便地移植到PIC单片机上。
(4)代码效率高。当前较好的C51语言编译系统编译出
来的代码效率只比直接使用汇编语言低20%左右,如果使
用优化编译选项,效果会更好。
8
14.1.2 Keil C51的开发环境
Keil C51是德国Keil software公司开发的用于51系列
单片机的C51语言开发软件。Keil C51在兼容ANSI C的基
础上,又增加很多与51单片机硬件相关的编译特性,使得
开发51系列单片机程序更为方便和快捷,程序代码运行速
度快,所需存储器空间小,完全可以和汇编语言相媲美。
它支持众多的MCS-51架构的芯片,同时集编辑、编译、
仿真等功能于一体,具有强大的软件调试功能,是众多的
单片机应用开发软件中最优秀的软件之一。
9
Keil公司目前已推出V7.0以上版本的C51编译器,为
51单片机软件开发提供了全新的C语言环境,同时保留了
汇编代码高效、快速的特点。
现在,Keil C51已被完全集成到一个功能强大的全新
集成开发环境(IDE)µVision3中,该环境下集成了文件
编辑处理、编译链接、项目(Project)管理、窗口、工具
引用和仿真软件模拟器以及Monitor51硬件目标调试器等
多种功能,这些功能均可在Keil µVision3环境中极为简便
地进行操作。
10
本章经常用到Keil C51和Keil µVision3两个术语。
Keil C51一般简写为C51,指的是51单片机编程所用的C
语言;而Keil µVision3,可简写为µVision3,指的是用于
51单片机的C51程序编写、调试的集成开发环境。
µVision3内部集成了源程序编辑器,并允许用户在编
辑源文件时就可设置程序调试断点,便于在程序调试过程
中快速检查和修改程序。此外,µVision3还支持软件模拟
仿真(Simulator)和用户目标板调试(Monitor51)两种工
作方式。在软件模拟仿真方式下不需任何51单片机及其外
围硬件即可完成用户程序仿真调试。
11
在用户目标板调试方式下,利用硬件目标板中的监控
程序可以直接调试目标硬件系统,使用户节省购买硬件仿
真器的费用。
14.1.3 C51与标准C的主要区别
不同的嵌入式处理器的C编译系统与标准C的不同之
处,主要是它们所针对的嵌入式处理器的硬件系统不同。
Keil C51的基本语法与标准C相同,但对标准C进行了扩展。
深入理解Keil C51对标准C的扩展部分是掌握Keil C51
的关键之一。
12
C51与标准C的主要区别如下:
(1)头文件的差异。51系列单片机厂家有多个,它们的
差异在于内部资源如定时器、中断、I/O等数量以及功能
的不同,而对使用者来说,只需要将相应的功能寄存器的
头文件加载在程序内,就可实现所具有的功能。因此,
Keil C51系列的头文件集中体现了各系列芯片的不同资源
及功能。
(2)数据类型的不同。51系列单片机包含位操作空间和
丰富的位操作指令,因此Keil C51与ANSI C相比又扩展了
4种类型,以便能够灵活地进行操作。
13
(3)数据存储类型的不同。C语言最初是为通用计算机
设计的,在通用计算机中只有一个程序和数据统一寻址的
内存空间,而51系列单片机有片内、外程序存储器,还有
片内、外数据存储器。标准C并没有提供这部分存储器的
地址范围的定义。此外,对于AT89C51单片机中大量的特
殊功能寄存器也没有定义。
(4)标准C语言没有处理单片机中断的定义。
(5)Keil C51与标准C的库函数有较大的不同。
由于标准C的中的部分库函数不适于嵌入式处理器系统,
因此被排除在Keil C51之外,如字符屏幕和图形函数。
14
有一些库函数可以继续使用,但这些库函数都必须针
对51单片机的硬件特点来作出相应的开发,与标准C库函
数的构成与用法有很大的不同。例如库函数printf和scanf,
在标准C中,这两个函数通常用于屏幕打印和接收字符,
而在Keil C51中,它们主要用于串行口数据的收发。
(6)程序结构的差异。由于51单片机的硬件资源有限,
它的编译系统不允许太多的程序嵌套。其次,标准C所具
备的递归特性不被Keil C51支持,在C51中,要使用递归
特性,必须用reentrant进行声明才能使用。
15
但是从数据运算操作、程序控制语句以及函数的使用
上来说,Keil C51与标准C几乎没有什么明显的差别。如
果程序设计者具备了有关标准C的编程基础,只要注意
Keil C51与标准C的不同之处,并熟悉AT89S51单片机的
硬件结构,就能够较快地掌握Keil C51的编程。
16
14.2 C51语言程序设计基础
本节介绍C51语言程序设计的有关基础知识。
14.2.1 C51语言中的数据
1. 数据类型
Keil C51的基本数据类型如表14-1所示。针对
AT89S51单片机的硬件特点,C51在标准C的基础上,扩
展了4种数据类型(见表中最后4行)。
注意:扩展的4种数据类型,不能使用指针对它们存
取。
17
表14-1 Keil C51支持的数据类型
数据类型
signed char
位数
8
字节数
1
取值范围
-128~+127,有符号字符变量
unsigned char
8
1
0~255,无符号字符变量
signed int
16
2
-32768~+32767,有符号整型数
unsigned int
16
2
0~65535,无符号整型数
signed long
32
4
unsigned long
32
4
0~+4294967295,无符号长整型数
float
32
4
±3.402823 E+38,浮点数(精确到7位)
double
64
8
±1.175494E-308,浮点数(精确到15位)
*
bit
sfr
sfr16
sbit
24
1
8
16
1
1~3
对象指针
0或1
0~255
0~65535
可进行位寻址的特殊功能寄存器
的某位的绝对地址
1
2
-2147483648~+2147483647,有符号长
整型数
18
2. C51的扩展数据类型
下面对表14-1中扩展的4种数据类型进行说明。
(1)位变量bit
bit的值可以是1(true), 也可以是0(false)。
(2)特殊功能寄存器sfr
AT89S51特殊功能寄存器在片内RAM区的80H~FFH
之间,“sfr” 数据类型占用一个内存单元。利用它可访
问AT89S51内部的所有特殊功能寄存器。
例如:sfr P1=0x90这一语句定义P1口在片内的寄存
器,在后面语句中可用“P1=0xff”(使P1的所有引脚输出
19
为高电平)之类的语句来操作特殊功能寄存器。
(3)特殊功能寄存器sfr16
“sfr16”数据类型占用两个内存单元。sfr16和sfr一样
用于操作特殊功能寄存器。所不同的是它用于操作占两个字
节的特殊功能寄存器。
例如: sfr16 DPTR=0x82语句定义了片内16位数据指
针寄存器DPTR,其低8位字节地址为82H。在后面的语句中
可以对DPTR进行操作。
20
(4)特殊功能位 sbit
sbit 是指AT89S51片内特殊功能寄存器的可寻址位。
例如:
sfr PSW=0xd0
;/*定义PSW寄存器地址为0xd0*/
sbit PSW ^2 = 0xd2
;/*定义OV位为PSW.2*/
符号“^”前面是特殊功能寄存器的名字,“^”的后
面数字定义特殊功能寄存器可寻址位在寄存器中的位置,
取值必须是0~7。
注意,不要把bit与sbit混淆。bit用来定义普通的位变
量,值只能是二进制的0或1。而sbit定义的是特殊功能
21
寄存器的可寻址位, 其值是可进行位寻址的特殊功能寄
存器的位绝对地址,例如PSW寄存器OV位的绝对地址
0xd2。
3. 数据的存储类型
C51完全支持51单片机硬件系统的所有部分。在51单
片机中,程序存储器与数据存储器是完全分开的,且分为
片内和片外两个独立的寻址空间,特殊功能寄存器与片内
RAM统一编址,数据存储器与I/O端口统一编址。C51编
译器通过将变量、常量定义成不同存储类型的方法将它们
定义在不同的存储区中。
22
C51存储类型与AT89S51的实际存储空间的对应关系
见表14-2。下面对表14-2作以说明。
(1)片内数据存储器
片内RAM可分为3个区域:
data:片内直接寻址区,位于片内RAM的低128字节。
bdata:片内位寻址区,位于片内RAM位寻址区
20H~2FH。
idata:片内间接寻址区,片内RAM所有地址单元
(00H~FFH)。
23
24
(2)片外数据存储器
pdata:片外数据存储器页,一页为256字节。
xdata:片外数据存储器RAM的64KB空间。
(3)片外程序存储器
code:外部程序存储器的64KB空间。
对单片机编程,正确地定义数据类型以及存储类型,是所
有编程者在编程前都需要首先考虑的问题。在资源有限的
条件下,如何节省存储单元并保证运行效率,是对开发者
的一个考验。只有对C51中的各种数据类型以及存储类型
非常熟练的掌握,才能运用自如。
25
定义变量类型应考虑如下问题:程序运行时该变量可
能的取值范围,是否有负值,绝对值有多大,以及相应需
要的存储空间大小。在够用的情况下,尽量选择8位即一
个字节的char型,特别是unsiged char。对于51系列这样
的定点机而言,浮点类型变量将明显增加运算时间和程序
长度,如果可以的话,尽量使用灵活巧妙的算法来避免浮
点变量的引入。
定义数据的存储类型通常遵循如下原则:只要条件满
足,尽量选择内部直接寻址的存储类型data,然后选择
idata即内部间接寻址。对于那些经常使用的变量要使用内
26
部寻址。在内部数据存储器数量有限或不能满足要求的情
况下才使用外部数据存储器。选择外部数据存储器可先选
择pdata类型,最后选用xdata类型。
需指出,扩展片外存储器,原理上虽很简单,但在实
际开发中,很多时候,会带来不必要的麻烦,如可能降低
系统稳定性、增加成本、拉长开发和调试周期等,推荐充
分利用片内存储空间。
另外,通常的单片机应用都是面对小型的控制,代码
比较短,对于程序存储区的大小要求很低,常常是片内
RAM很紧张而片内Flash ROM很富裕,因此如果实时性
27
要求不高,可考虑使用宏,以及将一些子函数的常量数据
做成数据表,放置在程序存储区,当程序运行时,进入子
函数动态调用下载至RAM即可,退出子函数后立即释放该
内存空间。
28
14.2.2 C51的位变量定义
由于AT89C51能够进行位操作,C51扩展了“bit”数
据类型用来定义位变量,这是C51与标准C的不同之处。
C51中位变量bit的具体定义如下:
1. 位变量的C51定义方法
C51通过关键字“bit”来定义位变量,格式为:
bit bit-name
;
例如:bit ov-flag
;/* 将ov-flag定义为位变量*/
29
2. C51程序函数的“bit”参数及返回值
C51程序函数可以包含类型为“bit”的参数,也可将
其作为返回值。例如:
bit func(bit b0, bit b1);
/* 位变量b0,b1作为函数func的参数*/
{
……
return(b1);
/* 位变量b1作为函数的返回值*/
}
30
3. 位变量的限制
位变量不能用来定义指针和数组。例如:
bit *ptr
;/* 错误,不能用位变量来定义指针*/
bit a-array[ ]
;/* 错误,不能用位变量来定义数组*/
在定义位变量时,允许定义存储类型,位变量都被放
入一个位段,此段总是位于AT89S51片内RAM中,因此
其存储器类型限制为bdata,data 或idata,如果将位变量
定义成其他类型都会在编译时出错。
31
14.2.3 一个简单的C51程序
一个C51源程序是由一个个模块化的函数所构成,函
数是指程序中的一个模块,main()函数为程序的主函
数,其他若干个函数可以理解为一些子程序。
一个C51源程序无论包含了多少函数,它总是从main
()函数开始执行,不论main()函数位于程序的什么
位置。程序设计者就是编写一系列的函数模块,并在需要
的时候调用这个函数,实现程序所要求的功能。
32
1. C51程序与函数
下面通过一个简单C51程序,认识C51程序与函数。
【例14-1】 在AT89S51的P1.0脚接有一只发光二极管,
二极管的阴极接P1.0脚,阳极通过限流电阻接+5V,现在
让发光二极管每隔800ms闪灭,占空比为50%。已知单片
机时钟晶振为12MHz,即每个机器周期1μs,采用软件延
时的方法,参考程序如下:
33
/* 包含reg51.h 头文件*/
#include <reg51.h>
sbit P10=P1^0; /* 定义位变量P1.0 ,也可使用sbit P10=0x90*/
void Delay(unsigned int i)
/* 延时函数Delay( ),i是形式参数 */
/* 两个花括号之间为函数Delay( )的函数体*/
{
/* 定义变量j */
unsigned int j;
}
/* 如果i>0,则i减1 */
for(;i>0;i--)
{
for(j=0;j<333;j++) /* 如果j <333,则j加1 */
{
;
/*空函数*/
}
}
}
34
void main(void)
/* 主函数main( )*/
{
while(1)
/* 主程序轮询 */
{
P10=1;
Delay(800) ;
/* P1.0输出高电平,发光二极管灭*/
/* 将实际参数800传递给形式参数i,
延时800ms*/
P10=0;
/* P1.0输出低电平,发光二极管亮*/
Delay(800) /* 将实际参数800传递给形式参数i,延时800ms*/
}
}
35
下面对程序进行简要说明。
程序的第1行是“文件包含”,是将另一个文件
“reg51.h”的内容全部包含进来。文件“reg51.h”包含
了51单片机全部的特殊功能寄存器的字节地址及可寻址位
的位地址定义。
程序包含reg51.h的目的就是为了使用P1这个符号,
即通知程序中所写的P1是指AT89S51的P1端口,而不是
其他变量。
36
打开reg51.h文件可以看到“sfr P1=0x90;”,即定
义符号P1与地址0x90对应,而P1口的地址就是0x90。虽然
这里的“文件包含”只有一行,但C编译器在处理的时候却
要处理几十行或几百行。
程序的第2行用符号P10来表示P1.0引脚。在C51中,
如果直接写“P1.0”编译器并不能识别,而且P1.0也不是一
个合法的C51语言程序变量名,所以必须给它起一个另外的
名字,这里起的名字是P10,可是P10是否就是P1.0呢,所
以必须给它们建立联系,这里使用了C51的关键字“sbit”
来进行定义。
37
第3行~第9行对函数Delay进行了事先定义,只有这
样,才能在主程序中被主函数main ( )调用。自行编写的
函数Delay( )的用途是软件延时,调用时使用的这个“800”
被称为“实际参数”,以延时800ms的时间。
注意,内层循环for(j=0;j<333;j++){;}这条语句在反
汇编时对应的汇编代码如下:
HERE:
CLR
A
/*1个机器周期*/
MOV
R7, A
/*2个机器周期*/
INC
R7
/*1个机器周期*/
CJNE
R7, #333,HERE /*2个机器周期*/
38
其中 {;}在反汇编时不对应任何语句,即不占用机器周
期。因而,该for循环共需1+2+333*(1+2)=1002个机器周
期,约为1ms。
相比之下调用外层循环for(;i>0;i--){ }时的这
1+2+i*(1002+1+2)可以近似为 i*1002,即i个ms。编程者
可在一定范围内对i、j调整(不超过i、j的取值范围),来
控制延时时间的长短。
注意,若Delay( )的定义写在main函数的后面,则需
要先作出声明,否则编译无法通过,因为编译到main函数
中的Delay( )语句时,找不到相应的函数体。
39
main为“主函数”,每一个C语言程序有且只有一个
主函数,主函数后面一定有一对花括号“{}”,在花括号
里面书写该函数的代码行。
2. 用户自定义函数与库函数
从结构上划分,函数分为主函数main ( )和普通函数
两种。对普通函数,从用户使用的角度划分有两种:一种
是标准库函数;另一种是用户自定义函数。
(1)标准库函数
Keil C51具有功能强大、资源丰富的标准库函数,由
C51编译器提供。进行程序设计时,应该善于充分利用
40
这些功能强大、资源丰富的标准库函数,提高编程效率。
用户可以直接调用C51的库函数而不需要为这个函数写任
何代码,只需要包含具有该函数说明的头文件即可。
例如调用输出函数printf时,要求程序在调用输出库函
数前包含以下的include 命令:
#include <stdio.h>
(2)用户自定义函数
用户根据自己需要所编写的函数。如例14-1中的
Delay函数。编写时,需要注意以下几点。
41
函数的首部(函数的第1行),包括函数名、函数类
型、函数属性、函数参数(形式参数)名、参数类型。
例如:void Delay(unsigned int i)
函数体,即函数首部下面的花括号“{ }”内的部分。
如果一个函数体内有多个花括号,则最外层的一对“{ }”
为函数体的范围。
C51区分大小写,例如Delay与delay,编译时是不同
的两个名称。
每个语句最后必须有一个分号,分号是C语句的必要
组成部分。
42
从函数的定义的形式上划分可以有三种形式:无参数
函数、有参数函数和空函数。
(1)无参数函数
此种函数在被调用时,既无参数输入,也不返回结果
给调用函数,只是为完成某种操作而编写的。
(2)有参数函数
调用此种函数时,必须提供实际的输入函数,必须说
明与实际参数一一对应的形式参数,并在函数结束时返回
结果,供调用它的函数使用。例如,例14-1中的第3行的
“i”是形式参数。
43
(3)空函数
函数体内无语句,是空白的。调用空函数时,什么工
作也不做,不起任何作用。定义空函数的目的,是为以后
程序功能的扩充。程序最初设计时,往往只涉及最基本的
功能模块的函数,其他模块的功能函数可以在以后补上。
因此先将非基本模块的功能函数定义成空函数,用一个空
语句“;”占好位置,并写好注释,以后再用一个编好的
函数代替它。例如,例14-1中的第8行的“;”就是一个
空语句。
44
3. 函数调用
程序设计者的任务就是编写一系列的用户自定义函数
模块,并在需要的时候调用这些函数以及库函数,实现程
序所要求的功能。调用分为以下几种。
(1)简单调用
调用格式为:函数名(实际参数1,实际参数2,…)
例如,例14-1中主函数main ( )里的子函数调用语句
“Delay(800);”,其中800为实际参数。
45
(2)嵌套调用
是在被调用的函数中又调用其他函数的语句。
(3)递归调用
函数的递归调用就是一个函数在其函数体内有调用自身。
再入函数是一种可以在函数体内直接或间接调用自身的一种
函数,在Keil C51中递归函数必须是可重入的,可重入的函数
需要加上reentrant。
再入函数有以下几点规定。
46

再入函数不能包括位操作以及51单片机的未寻址区。

在同一个程序中可以定义和使用不同存储模式的再入函数,
任何模式的再入函数不能调用不同模式的再入函数。

在参数的传递上,实际参数可以传递给间接调用的再入函数。
14.2.4 C51的运算符
在程序中实现运算,要熟悉常用的运算符。本节对C51中
用到的标准C运算符进行复习,为C51的程序设计打下基础。
1. 算术运算符
如表14-3所示。
47
表14-3 算术运算符及其说明
符号
说明
+
加法运算
-
减法运算
*
乘法运算
/
除法运算
%
取模运算
++
自增1
--
自减1
48
对于“/”和“%”往往会有疑问。这两个符号都涉及
除法运算,但“/”运算是取商,而“%” 运算为取余
数。例如“5/3”的结果(商)为1,而“5%3”的结果为
2(余数)。
表14-3中的自增和自减运算符是使变量自动加1或减
1,自增和自减运算符放在变量前和变量之后是不同的。
++i,--i:在使用i之前,先使i值加(减)1。
i++,i--:在使用i之后,再使i值加(减)1。
49
例如:若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。
在例14-1中可以看到有关自增和自减运算符的应用。
2. 逻辑运算符
逻辑运算符及其说明如表14-4所示。
50
表14-4 逻辑运算符及其说明
符号
说明
&&
逻辑与
ΙΙ
逻辑或
!
逻辑非
3. 关系运算符
判断两个数之间的关系。关系运算符及其说明如表14-5。
51
表14-5 关系运算符及其说明
符号
说明
>
大于
<
小于
>=
大于或等于
<=
小于或等于
==
等于
!=
不等于
52
4. 位运算
位运算符及其说明如表14-6所示。
表14-6 位运算其说明
符号
说明
&
位逻辑与
Ι
位逻辑或
^
位异或
~
位取反
<<
位左移
>>
位右移
53
5. 赋值、指针和取值运算符
是对变量操作的基本运算。赋值、指针和取值运算符
及其说明如表14-7所示。
表14-7 位运算其说明
符号
说明
=
赋值
*
指向运算符
&
取地址
54
14.2.5 C51的分支与循环程序结构
程序结构上可把程序分为三类,即顺序、分支和循环
结构。顺序结构是程序的基本结构,程序自上而下,从
main()的函数开始一直到程序运行结束,程序只有一条
路可走,没有其他的路径可以选择。顺序结构比较简单和
便于理解,这里重点介绍分支结构和循环结构。
55
1. 分支结构程序
(1) 只有两条分支的时候用
If (条件) {分支1}
else {分支2}
(2) 分支较多时
在分支较多时的情况下使用switch语句。
56
switch ( ) { case( ):语句;
break;
case( ):语句;
break;
…………
default:语句;
break;
}
57
注意:每个switch分支必须有一个break语句,否则
程序并不能跳出switch,就会继续执行case后面的case语
句。如果看一下上述结构的程序对应的汇编语言源程序可
看到,每一条break语句对应了汇编语言中的一条SJMP指
令,而没有SJMP指令程序会继续向下执行,并不能跳出
分支选择语句。
实际上在对应的汇编语言源程序中,case(0),case(1)
……只是确定了分支的地址,真正的判断是在switch语
句开始的。
58
2. 循环结构程序
循环语句有以下三种。
(1)for循环
格式:
for(循环体初始化;循环体执行条件;循环体执行后操作)
花括号{}中为循环体内容。
这里有一个值得注意的现象,能够反映出C51在编译中对
于执行时间和占用的存储单元的权衡。
例如,for (i=0;i<10;i++)对应的汇编语句为:
59
CLR
A
;1个机器周期
MOV
R7,A
;2个机器周期
R7
;1个机器周期
LOOP: INC
CJNE R7,#0AH,LOOP
;2个机器周期
而for (i=2;i<10;i++)对应的汇编语句为:
MOV
LOOP: INC
R7, #02H
;2个机器周期
R7
;一个机器周期
CJNE R7,#0AH,LOOP
;两个机器周期
60
为什么当i=0时,编译器要多花一个机器周期对for循
环初始化?这是因为在使用立即数时,单片机需要在代码
空间(程序存储器)中为该立即数申请一个存储单元,用
来存放该立即数,作为MOV指令的操作数;而累加器A是
单片机中的寄存器,使用A可以节省一个字节的存储空间,
从而实现以时间换取空间。
(2)while循环
格式为:
while(循环体执行条件),花括号{}中为循环体内容。
61
(3)do while循环
格式为:
do { },花括号{ }中为循环体内容
while(循环体执行条件)
前两种循环是先进行循环条件是否满足的判断,才决
定循环体是否执行;而“do while循环”是在执行完循环
体后再判断条件是否满足,再决定循环体是否继续执行。
三种循环中,经常使用的是for语句。下面来说明for语句
的应用。
62
【例14-2】求1到100之间整数的和。
程序如下:
#include <reg51.h>
#include <stdio.h>
main( )
{
int nVar1, nSum;
for(nVar1=0,nSum=1;nSum<=100;nSum++)
nVar1+ =nCount;
/*累加求和*/
while(1);
}
63
关于循环,需说明的是,在无操作系统的控制器和处
理器上运行的程序,主体通常采用轮询方式,即把所有的
操作包含在一个while(1){}中,如例14-1。这样的无限循环
在面向通用计算机的软件设计中是不被允许的,然而嵌入
式系统软件设计中,则由于其硬件构成和使用需求,常常
采用这种无限循环。
64
14.2.6 AT89C51不同存储区的C51定义
AT89C51有不同的存储区。利用绝对地址的头文件
absace.h可对不同的存储区进行访问。该头文件的函数包
括:
CBYTE
(访问code区,字符型)
DBYTE
(访问data区,字符型)
PBYTE
(访问pdata区或I/O口,字符型)
XBYTE
(访问xdata区或I/O口,字符型)
另外还有CWORD、DWORD、PWORD、XWORD四个
函数,它们的访问区域同上,只是访问的数据类型为int型。
65
注意:AT89S51片内的4个并行I/O口(P0~P3),都
是SFR,故对P0~P3采用定义SFR的方法。而AT89S51
在片外扩展的并行I/O口,这些扩展的I/O口与片外扩展的
RAM是统一编址的,即把一个外部I/O端口当作外部RAM
的一个单元来看待。可根据需要来选择为pdata类型或
xdata类型。对于片外扩展的I/O端口,根据硬件译码地址,
将其看作片外RAM的一个单元,使用语句#define进行定
义。例如:
#includ <absace.h>;
/* 不可缺少*/
#define PORTB XBYTE[0xffc2]
;
/* 定义外部I/O口PORTB的地址为xdata区的0xffc2*/
66
也可把片外I/O口的定义放在一个头文件中,然后在程
序中通过#include语句调用。一旦在头文件或程序中通过
使用#define语句对片外I/O口进行了定义,在程序中就可
以自由使用变量名(例如:PORTB)来访问这些片外I/O
端口了。
14.2.7 C51中断服务函数的定义
由于标准C没有处理单片机中断的定义,为直接编写
中断服务程序,C51编译器对函数的定义进行了扩展,增
加了一个扩展关键字interrupt,使用该关键字可以将一个
函数定义成中断服务函数。由于C51编译器在编译时对
67
声明为中断服务程序的函数自动添加了相应的现场保护、
阻断其他中断、返回时恢复现场等处理的程序段,因而在
编写中断服务函数时可不必考虑这些问题,减轻了用汇编
语言编写中断服务程序的繁琐程度,而把精力放在如何处
理引发中断请求的事件上。
中断服务函数的一般形式为:
函数类型 函数名(形式参数表)[interrupt n] [using n]
关键字interrupt后面的 n是中断号,对于AT89S51,
取值为0~4,编译器从8×n+3处产生中断向量。AT89S51
中断源对应的中断号和中断向量见表14-3。
68
表14-3 AT89S51中断号和中断向量
中断号n
中断源
中断向量(8×n+3)
0
外部中断0
0003H
1
定时器0
000BH
2
外部中断1
0013H
3
定时器1
001BH
4
串行口
0023H
其他值
保留
8×n+3
AT89S51在内部RAM中有4个工作寄存器区,每个寄存器区
包含8个工作寄存器(R0-R7)。C51扩展了一个关键字using,
专门用来选择AT89S51的4个不同的工作寄存器区。在定义一个
函数时,using是一个选项,如果不选用该项,则由编译器选择
一个寄存器区作为绝对寄存器区访问。
69
关键字using对函数目标代码的影响如下:
在中断函数的入口处将当前工作寄存器区内容保护到
堆栈中,函数返回前将被保护的寄存器区的内容从堆栈中
恢复。
使用关键字using在函数中确定一个工作寄存器区时
必须小心,要保证工作寄存器区切换都只在指定的控制区
域中发生,否则将产生不正确的函数结果。还要注意,带
using属性的函数原则上不能返回bit类型的值,且关键字
using和关键字interrupt都不允许用于外部函数,另外也
都不允许有一个带运算符的表达式。
70
例如,外中断1( IN T 1 )的中断服务函数书写如下:
void int1( ) interrupt 2 using 0/*中断号n=2,选择0区工作寄存器区*/
编写AT89S51中断程序时,应遵循以下规则:
(1)中断函数没有返回值,如果定义了一个返回值,将
会得到不正确的结果。因此建议在定义中断函数时,将其
定义为void类型,以明确说明没有返回值。
(2)中断函数不能进行参数传递,如果中断函数中包含
任何参数声明都将导致编译出错。
71
(3)在任何情况下都不能直接调用中断函数,否则会
产生编译错误。因为中断函数的返回是由指令RETI完成
的。RETI指令会影响AT89S51中的硬件中断系统内的不
可寻址的中断优先级寄存器的状态。如果在没有实际的中
断请求的情况下,直接调用中断函数,也就不会执行
RETI指令,其操作结果有可能产生一个致命的错误。
(4)如果在中断函数中再调用其他函数,则被调用的
函数所使用的寄存器区必须与中断函数使用的寄存器区不
同。
72
14.3 C51的程序设计举例
本节重点介绍对AT89S51片内各种功能部件及硬件接
口的C51例程,读者应仔细阅读并理解这些例程。
14.3.1 中断程序的编写
为响应中断请求而进行中断处理的程序称为中断程序。
由中断初始化程序和中断服务程序两部分组成。
中断初始化程序的位置位于主程序中,主要包括选择外
部中断的触发方式、开中断、设置中断优先级等。
73
【例14-3】在单片机系统的P1口上接有8只LED,LED
的阳极接+5V,阴极接P1口的引脚。当P1口某一口线输出
为0时,将LED点亮。在外部中断0输入脚P3.2( IN T 0 )引
脚接上拉电阻并接有一只按钮开关K1。用K1按钮来产生外
部中断0的输入信号。接口电路如图14-1所示。
要求将外部中断0设为负跳沿触发。在程序刚启动时,
P1口上的8只LED亮。按一次按钮开关K1,使引脚P3.2接
地,产生一个外中断0的中断请求,在中断服务程序中,让
低4位LED和高4位LED交替闪烁。参考程序:
74
图14-1
控制8只LED交替闪烁的电路
75
#include <reg51.h>
void Delay(unsigned int i)/* 定义延时函数Delay( ),i是形式参数,
不能赋初值*/
{
unsigned int j;
for(;i > 0;i- -)
for(j=0;j<333;j++)
/*晶振为12MHz,j的范围见例
14-1*/
{;}
/*空函数*/
}
76
void main( )
/*主函数*/
{
EA=1;
/*总中断允许*/
EX0=1;
/*外部中断0中断允许*/
IT0=1;/*选择外部中断0为跳沿触发方式*/
While(1)
/*循环*/
{ P1=0;}
/* P1口的8只LED全亮*/
}
77
void int0( ) interrupt 0 using 0
/*外中断0的中断服务函数*/
{
EX0=0;
/*禁止外部中断0中断*/
P1=0x0f;
/*低4位LED灭,高4位LED亮*/
Delay(800) ;
/*延时800ms*/
P1=0xf0;
/*高4位LED灭,低4位LED亮*/
Delay(800);
/*延时800ms */
EX0=1;/*中断返回前,打开外部中断0中断*/
}
78
14.3.2 定时器程序的编写
【例14-4】在AT89S51的P1口上接有8只LED。下面
采用定时器T0的方式1的定时中断方式,使P1口外接的8
只LED每0.5s闪亮一次。
(1)设置TMOD寄存器
定时器T0工作在方式1,应使TMOD寄存器的M1、
M0=01;定时器模式,应设置C/ T =0;对T0的运行控制
仅由TR0来控制,应使GATE0=0。定时器T1不使用,各
相关位均设为0。所以,TMOD寄存器应初始化为0x01。
79
(2)计算定时器T0的计数初值
设定时时间5ms(即5000µs),设定时器T0的计数初
值为X,假设晶振的频率为11.0592MHz,则定时时间为:
定时时间=(216−X)12/晶振频率
则5000=(216 −X)12/11.0592
得:X=60928,转换成十六进制后为:0xee00,其中
0xee装入TH0,0x00装入TL0。
(3)设置IE寄存器
本例由于采用定时器T0中断,因此需将IE寄存器中的
EA、ET0位置1。
80
(4)启动和停止定时器T0
将定时器控制寄存器TCON中的TR0=1,则启动定时
器T0;TR0=0,则停止定时器T0定时。
定时器T0方式1中断定时的参考程序:
#include<reg51.h>
Char i=100;
/*给变量i赋初值*/
void main()
{
TMOD=0x01;
/*设置定时器T0为方式1*/
TH0=0xee;
/*向TH0写入初值的高8位*/
81
TL0=0x00;
/*向TL0写入初值的低8位*/
P1=0x00;
/*P1口8只LED点亮*/
EA=1;
/*总中断允许*/
ET0=1;
/*定时器T0中断允许*/
TR0=1;
/*启动定时器T0*/
while(1) ;
/*循环*/
{
;
}
}
82
Void T0_int(void) interrupt 1
/*定时器T0中断服务程序*/
{
TH0=0xee;/*给T0装入16位初值,计数4608后,T0溢出*/
TL0=0x00;
i--;
/*循环次数减1*/
if(i< =0)
{
P1=~ P1;
/*P1口按位取反*/
i =100;
/*重新设置循环次数*/
}
}
83
14.3.3 串行口方式0应用程序的编写
AT89S51的串口的方式0是同步串行通信接口。方式0
的典型应用是外扩串行输入并行输出的同步移位寄存器
74LS164,实现并行I/O的扩展。
【例14-5】图14-2是利用串行口方式0通过74LS164
外接8个LED发光二极管的接口电路,编写程序使发光二
极管轮流显示。图中CLK端为同步脉冲输入端。STB为控
制端,当STB=0时,则8位并行输出端关闭,但是允许串
行数据从A和B端输入。当STB=1时,A和B输入端关闭,
但允许8位并行数据输出。
84
当8位串行数据发送完毕后,引起中断,在中服务程序
中,串行发出下一个8位数据。参考程序如下。
图14-2
串行口的方式0 外接8个LED发光二极管的接口电路
85
#include <reg51.h>
#include<stdio.h>
sbit P10 = 0x90;
Xdata char nIndex;
Delay();
main()
{
SCON = 0x00;
/* 串行口初始化为方式0*/
ES=1;
EA=1;
/* 全局中断允许 */
nIndex=1;
SBUF=nIndex;
P10=0;
while(1)
86
{;}
}
void Serial_Port() interrupt 4 using 0
{
if(TI==1){
P10=1;
Delay( );
P10=1;
nIndex<<=1;
if(nIndex==0) nIndex=1;
SBUF=nIndex;
}
TI=0;
RI=0;
87
}
Delay( )
{int nCounter;
for(nCounter=0;nCounter<128;nCounter++);
}
88
14.3.4 独立式键盘查询方式
【例14-6】采用查询方式对实现独立式键盘的键值读取。
独立式键盘的接口电路如图14-3所示。
图14-3 独立式键盘的接口电路
89
由图14-3,当P1口某一位为0时,表明该位所接的按
键被按下,然后在计算机屏幕上输出按下的按键的信息。
例如,3号键按下,则在PC机显示屏上显示“key No.
3 down”。程序如下:
#include<reg51.h>
#include<stdio.h>
/*因为用到printf( )函数,所以要包含
stdio.h */
Uart_Init();
sbit P10=0x90;
/*定义P1口各位的位名*/
sbit P11=0x91;
sbit P12=0x92;
90
sbit P13=0x93;
sbit P14=0x94;
sbit P15=0x95;
sbit P16=0x96;
sbit P17=0x97;
main( )
{
Uart_Init( );
while(1); /*无限循环,不断查询按键状态,键值判断和提示信
息的显示*/
{
91
if(P10==0)
printf("key No.0 down\n");/*如0号键按下,串行输出按下键信息*/
if(P11==0)
printf("key No.1 down\n");/*如1号键按下,串行输出按下键信息*/
if(P12==0)
printf("key No.2 down\n");/*如2号键按下,串行输出按下键信息*/
if(P13==0)
printf("key No.3 down\n");/*如3号键按下,串行输出按键信息*/
if(P14==0)
92
printf("key No.4 down\n");/*如4号键按下,串行输出按键信息*/
if(P15==0)
printf("key No. 5 down\n");/*如5号键按下,串行输出按键信息*/
if(P16==0)
printf("key No. 6 down\n");/*如6号键按下,串行输出按键信息*/
if(P17==0)
printf("key No. 7 down\n");/*如7号键按下,串行输出按键信息*/
}
}
93
Uart_Init( ) /* 实验系统初始化函数,因要通过实验系统串行输出显
示*/
{ SCON=0x52;
/* 设置串行口控制寄存器SCON */
TMOD=0x20;
/* 12MHz时钟时波特率为2400 */
TCON=0x69;
/* TCON */
TH1=0xf3;
/* TH1 */
}
94
14.3.5 行列式键盘查询方式
【例14-7】本例为2008年某省电子大赛D题“电能质
量检测装置”某参赛者设计中的一部分。单片机外接行列
式键盘,用于控制单片机向FPGA(现场可编程门阵列)
发出指令启动AD对信号采样分析,并控制单片机向LCD
传输数据以显示波形和参数。
采用查询方式对实现行列式键盘的键值读取。独立式
4×4键盘的接口电路如下页图(图10-10)。
95
图
行列式键盘的接口电路
96
图中,每个按键行数与P1口高半字节对应,列数与
P1口低半字节对应,行列号结合即可确定键位。首先查
询判断是否有键按下:驱动P1口高半字节为全0,低半字
节为全1,当读回P1口低半字节存在某位为0时,表明有
键按下。
延时去抖后,获取键位。先获取键位列号:按照之前
的驱动方式对P1口驱动高半字节为全0,低半字节全1,
读回P1口的低半字节值。然后获取行号:驱动P1口低半
字节为全0,高半字节为全1,读回P1口的低半字节值。
得到键位值后,进入键位服务程序。
97
头文件key.h如下:
#ifndef KEY_H
#include <AT89C51.h>
#include <stdio.h>
#define KEY_H
#define KEY_PORT P1
#define KV_FALSE 0Xff
/*无键按下* /
/*宏定义键位值* /
#define KV_START 0x11
/*“开始采样”键* /
#define KV_END
0x12 /*“结束采样”键* /
#define KV_wave
0x14 /*“显示波形”键*/
#define KV_parameter 0x18 /*“显示基本参数”键*/
98
#define KV_U_harmon 0x21
/*“显示电压谐波分析”键*/
#define KV_I_harmon 0x22
/*“显示电流谐波分析”键* /
#define KV_7
0x24
/*未使用的按键,下同*/
#define KV_8
0x28
#define KV_9
0x41
#define KV_10
0x42
#define KV_11
0x44
#define KV_12
0x48
#define KV_13
0x81
#define KV_14
0x82
#define KV_15
0x84
#define KV_16
0x88
uchar KeyScan(void);
99
/*宏定义几种数据类型*/
typedef unsigned char uchar;
typedef unsigned int uint;
typedef unsigned long ulong;
#endif
用于扫描键盘以得到键值的子程序如下:
#include "key.h"
uchar KeyScan(void)
{
uchar keyValue;
/*检测是否有键按下*/
KEY_PORT = 0x0F;
if(~(KEY_PORT | 0xF0) == 0) return KV_FALSE; /*延时去抖*/
100
DelayMs(5); /*检测是否仍有键按下并获取P1口键位列号*/
KEY_PORT = 0x0F;
keyValue = ~(KEY_PORT | 0xF0);
if(keyValue==0) return KV_FALSE; /*补充P1口键位行号*/
KEY_PORT= 0xF0;
keyValue |= ~(KEY_PORT | 0x0F); /*等待按键释放*/
do {
KEY_PORT = 0x0F;
if(~(KEY_PORT | 0xF0) != 0) continue; /*仍然被按下*/
/*判断是否真的释放*/
DelayMs(5);
if(~(KEY_PORT | 0xF0) != 0) continue; /*key not released*/
101
break;
}while(1);
//TODO:
/*调整延时以确定按键释放*/
return keyValue;
/*返回键值*/
}
用于键位服务的子程序如下:
void KeyServe(uchar keyvalue)
{
switch(keyvalue)
{
case KV_START: /*“开始”键的服务程序*/
{
/*向FPGA发出START指令,开始采样; */
}
102
break;
case KV_END:
/* “结束”键*/
{
/*结束单片机的主体循环*/
/*停止采样*/
/*显示整个测试过程中的最大、最小值即调用lcd.c的
max_min_print子函数*/
}
break;
case KV_wave:
/*“波形显示”键*/
{
103
/*调用lcd.c中的wave_print(void)子函数*/
}
break;
case KV_parameter: /* “参数显示”键*/
{
/*调用lcd.c中的parameter_print()子函数*/
}
break;
case KV_U_harmon:
/*“电压谐波显示”键*/
{
/*调用lcd.c中的hammon_print子函数; */
}
break;
104
case KV_I_harmon:
/* “电流谐波显示”键*/
{
/*调用lcd.c中的hammon_print子函数; */
}
break;
default: break; /*注意,使用case时要考虑所有的可能性,为了穷
举,避免出现意想不到的结果,应使用default作为陷阱*/
}
return;
}
105
14.3.6 DAC0832应用程序的编写
AT89S51与DAC0832单缓冲方式接口电路如图11-3
所示。
【例14-8】根据图11-3的接口电路,编写产生如图
11-4所示锯齿波的程序。参考程序如下:
#include <reg51.h>
#define DAC0832Addr 0xfffe /* 0832端口地址*/
#define uchar unsigned char/*定义uchar代表单字节无符号数*/
#define uint unsigned int
/*定义uint无符号字
sbit P26=0xA6;
/* P2.6位的位地址*/
106
sbit P27=0xA7;
/* P2.7位*/
void TransformData(uchar c0832data);/*转换数据*/
void Delay( ) ;
/*执行延时函数*/
main( )
/*主函数*/
{
uchar cDigital=0;/*待转换的单字节无符号数字量初值为0*/
Delay( );
/*调用延时程序*/
while(1)
/*主体函数轮询*/
{
TransformData(cDigital); /*调用数模转换函数*/
cDigital++;
/*输出锯齿波转换的数字量不断加1*/
107
Delay( );
/*调用延时函数,无参数传递*/
}
}
void TransformData(uchar c0832data)
{
*((uchar xdata*)DAC0832Addr)=c0832data;/*向DAC0832输出
待转换数字量c0832data*/
}
void Delay()
/*延时程序*/
{
uint i;
for(i=0;i<200;i++);
}
108
本例说明如下:
(1)大部分简单、低速的外围设备(例如2MHz采样
率AD、DA、RS232、RS422/485串口等,而非DDR2
SDRAM、千兆以太网口等)都是基于存储器映射的字符
设备,对于它们的操作与对于外部存储器的读、写访问是
一样。
(2)程序的伪代码部分(宏的使用)将DAC0832的
端口地址0xfffe宏定义为DAC0832Addr,是为了定义明确,
方便使用和修改。使用该地址进行DAC0832写访问时要
先进行类型转换。
109
用(uchar xdata*)把DAC0832Addr转换为指向
0xfffe地址的指针型数据,再用指针间接寻址。这种方法
(例14-7程序中阴影标注一行)是较为经典和精简的代码
风格,初学者可以用如下拆分、等价的方式理解这句代码:
首先,由于宏替换,(uchar xdata*)DAC0832Addr相
当于(uchar xdata*) 0xfffe,即将0xfffe强制转换为指向外
部数据空间的unsigned char类型的指针,指针内容为
0xfffe,即指向了DAC0832数据端口。
理解*((uchar xdata*)DAC0832Addr),它相当于*p,p
是指向外部数据空间0xfffe的unsigned char类型指针。
110
最后,*((uchar xdata*)DAC0832Addr)=c0832data意义
显然为:将c0832data的值写入DAC0832的数据端口。
因此,以下两个代码段在功能上是等价的。
代码段一:
#define DAC0832Addr 0xfffe
#define uchar unsigned char
*((uchar xdata*)DAC0832Addr)=c0832data;
代码段二:
unsigned char *p;
p=0xfffe;
*p= c0832data;
111
显然前者比后者有两个优点:
首先,代码段一的意义明确,可读性和可移植性更强。
更重要的是,代码段一节省了数据存储空间,因为它无需
使用指针变量,而宏是不占用数据存储空间的,它只占用
程序存储空间。这一优点的意义在本章14.2.1小节末有详
细说明。
(3)由于是产生锯齿波,因此,输出给DAC0832的
数字量cDigital要不断加1。当cDigital的值为255时再加1
就发生溢出并为0,所以在程序中不用再判断cDigital是否
为255。
112
14.3.7 ADC0809应用程序的编写
介绍单片机控制ADC0809进行A/D转换的程序设计。
【例14-9】单片机与ADC0809的中断方式通信的接口电
路如图14-4所示。
单片机采用中断方式读取A/D转换后的数据。
ADC0809开始转换后EOC(End Of Converge)脚为低电
平,当A/D转换结束时EOC脚变为高电平,经取非后作为
中断请求信号触发单片机中断,在中断服务程序中读取
A/D转换的结果。
参考程序:
113
图14-4
单片机与ADC0809中断方式通信的接口电路
114
#include <reg51.h>
#include <stdio.h>
#define ADC0809CH0 0x7FF8
/* 7FF8H为0809通道0地址*/
#define ADC0809CH1 0x7FF9
/* 7FF9H为0809通道1地址*/
#define ADC0809CH2 0x7FFA
/* 7FFAH为0809通道2地址*/
#define ADC0809CH3 0x7FFB
/* 7FFBH为0809通道3地址*/
#define uchar unsigned char
/* 定义uchar为单字节无符号数*/
#define uint unsigned int
/* 定义uint为双字节无符号数*/
xdata uint cChannelIndex;/* cChannelIndex存储类型为xdata */
xdata uchar cDigitalData[4]=0;/*存放转换结果数组的存储类型为
xdata */
xdata uint nCurrentAddr; /*nCurrentAddr存储类型为xdata */
115
Void Uart_Init();
/*声明初始化函数*/
sbit P25=0xA5;
/*特殊功能寄存器P2.5位*/
sbit P26=0xA6;
/*特殊功能寄存器P2.6位*/
sbit P27=0xA7;
/*特殊功能寄存器P2.7位*/
void SelectChannel(uint c0809addr,uchar c0809data);
/*声明“选择通道并启动转换”函数*/
uchar GetResult( );
/*声明“获取转换结果”函数*/
void Delay( ) ;
/*声明延时函数*/
main( )
{ Uart_Init( );
P0=0xFF;
/* P0端口初始化为0xFF*/
P1=0xFF;
/* P1端口初始化为0xFF*/
116
P2=0xFF;
/* P2端口初始化为0xFF*/
P3=0xFF;
/* P3端口初始化为0xFF*/
EX0=1;
/*开外部中断0中断允许*/
EA=1;
/*总中断允许*/
cChannelIndex=0;
nCurrentAddr=ADC0809CH0; /*给出通道0地址*/
SelectChannel(ADC0809CH0,cChannelIndex); /*选择
ADC0809通道0*/
Delay();
/*调用延时函数*/
cChannelIndex++;
/*地址加1*/
nCurrentAddr=ADC0809CH1; /*切换为通道1地址*/
117
SelectChannel(ADC0809CH1,cChannelIndex);
/*选择ADC0809通道1*/
Delay();
cChannelIndex++;
/*调用延时函数*/
/*地址加1*/
nCurrentAddr=ADC0809CH2;/*切换为通道2地址*/
SelectChannel(ADC0809CH2,cChannelIndex);
/*选择ADC0809通道2*/
Delay( );
/*延时*/
cChannelIndex++;
/*地址加1*/
118
Delay( );
/*延时*/
while(1);
}
Uart_Init()
/* 初始化函数*/
{SCON = 0x52; /* 设置串行口控制寄存器SCON*/
TMOD = 0x21; /* 12MHz时钟时的波特率为2400 */
TCON = 0x69;
TH1= 0xf3;
/* 设置定时器控制寄存器TCON */
/* 写入TH1的计数初值 */
nCurrentAddr=ADC0809CH3;/*切换为通道3地址*/
SelectChannel(ADC0809CH3,cChannelIndex);
/*选择ADC0809通道3*/
119
}
void Delay()
/*延时函数*/
{ uint i;
for(i=0;i<200;i++);
}
void SelectChannel(uint c0809addr,uchar c0809data)
/*“选择通道并启动转换”函数*/
{
*((uchar xdata *)c0809addr)=c0809data;
/*将c0809addr 强制转换为指向AD芯片当前通道地址的无符号整
型指针,并使用该指针访问该通道,将变量c0809data数据写入AD芯
片*/
120
void int0() interrupt 0 using 0 /*外中断0的中断函数*/
{ cDigitalData[cChannelIndex]=GetResult();
printf("Got channel %d Result\n",cChannelIndex);
/*响应 后输出*/
}
uchar GetResult( )
/*得到转换结果*/
{uchar cResult;
cResult=*((uchar xdata *)nCurrentAddr);
/*将nCurrentAddr强制转换为指向当前转换结果的无符号字符型
指针,并使用该指针对ADC的输出端口进行访问,读取转换结果存入
cResult*/
121
return cResult;
/*返回转换后数字量*/
}
上述程序对ADC0809通道0~3这4路通道的模拟量以中断
方式进行采集。具体说明如下:
(1)函数SelectChannel( )用来进行通道的选择,
主程序将地址和数据以参数的方式传递给SelectChannel。
(2)函数SelectChannel( )中的参数c0809data无关
紧要,通道选择只是单片机向外输出地址c0809addr,数
据可以为任意值。为什么?因为只有在单片机向片外数据
存储区写入某个数据时,控制信号ALE以及P2.7才会
122
有效,同时发出地址信号(通道号),并将其锁存,从而
使START端有效,启动A/D开始转换。
(3)在外部中断0的中断服务程序中读取转换的数字
量结果。
(4)GetResult( )函数中的地址nCurrentAddr能选
通ADC0809,此时 以及P2.7都为低电平,从而使
ADC0809的 端有效,就能将转换结果放到数据线上,从
单片机P0口读入。
(5)4路通道(通道0~3)的转换结果放到了数组
cDigitalData[ ]中。
123
14.4 C51的集成开发环境Keil µVision3介绍
C51程序开发是在Keil µVision3 开发环境下进行,首
先介绍该开发环境。
14.4.1 集成开发环境Keil µVision3简介
Keil Software公司推出的Keil µVision3 是一款基于
Windows的软件平台,它是一种用于51单片机的集成开发
环境(IDE—Intergrated Development Eviroment)。
µVision3提供了对基于8051内核的各种型号单片机的支
持,完全兼容先前的Keil µVision2版本。目前当前较新的
版本为Keil C51 V8.08a。
124
开发者可购买Keil µVision3软件,也可到Keil
software公司的主页免费下载Eval(评估)版本。该版本
同正式版本一样,但有一定的限制,最终生成的代码不能
超过2KB,但用于学习已经足够。开发者还可以到Keil公
司网站申请免费的软件试用光盘。
Keil µVision3内包含了功能强大的编辑器和调试器。
编辑器可以像一般的文本编辑器一样对源代码进行编辑,
并允许用户在编辑时设置程序断点(可在源代码未经编译
和汇编之前)。用户启动µVision3的调试器之后,断点即
被激活。
125
断点可被设为条件表达式、变量或存储器访问,断点
被触发后,调试器的命令或调试功能即可执行,因此用户
可以在编辑器内调试程序,使用户快速地检查和修改程序。
用户还可以在编辑器中选中变量和存储器来观察其值。
并可在双层窗口中显示,还可对其进行适当的调整。此外,
µVision3调试器具有符号调试特性以及历史跟踪,代码覆
盖,复杂断点等功能。
Keil C51编译器在遵循ANSI C标准的同时,为51单
片机进行了特别的设计和扩展,能让用户使用在应用中需
要的所有资源。
126
Keil C51的库函数含有100多种功能,其中大多数是
可再入的。函数库支持所有的ANSI C的程序。库函数中
的程序还为硬件提供特殊指令,例如nop、testbit、rol、
ror等,方便了应用程序的开发。
Keil µVision的串口调试器软件comdebug.exe,用于
在电脑端能够看到单片机发出的数据,该软件无需安装,
可直接在当前位置运行这个软件。若读者需最新版,可到
有关搜索网站输入关键词“串口调试器”,找到一个合适
的下载网站,可即下载最新版本。当然,使用Windows自
带的“超级终端”也是不错的选择。
127
14.4.2 Keil µVision3软件的安装、启动和运行
1. 软件安装
Keil µVision3的安装,同大多数软件安装一样,根据提
示进行。安装完毕后,可在桌面上看到Keil µVision3软件
的快捷图标。
2. 软件启动
点击桌面上的Keil µVision3软件的快捷图标,即可启动
该软件,几秒后,出现编辑界面。
3. 软件的运行
128
(1)建立一个新工程
Keil µVision3把用户每一个应用程序设计都当作一个项目,
用项目管理的方法把一个应用程序设计中所需要用到的、互相
关联的程序链接在同一项目中。这样,打开一个项目时,所需
的关联程序也都跟着进入了调试窗口,方便用户对项目中各个
程序的编写、调试和存储。
用户也可能开发多个项目,每个项目用到了相同或不同的
程序文件和库文件,采用项目管理,很容易区分不同项目中所
用到的程序文件和库文件。因此,在编写一个新的应用程序前,
先建立项目的良好习惯。下面首先介绍如何建立一个新项
129
目。
在编辑界面下,首先要建立一个点击“Project”菜单,
选择下拉式菜单中的“New Project”,弹出文件对话窗
口,选择要保存的路径,在“文件名”中输入一个程序项
目名称,保存后的文件扩展名为“.uv2”,这是Keil
µVision3项目文件的扩展名,以后可直接点击此文件就可
打开先前做的项目。
点击“保存”后,这是会弹出一个对话框,要求选择
单片机的型号,用户可根据所使用的单片机来选择。Keil
µVision3支持几乎所有的51内核的单片机。
130
开始编写第一个程序。点击“File”菜单,再在下拉
菜单中单击“New”选项。此时光标在编辑窗口里闪烁,
这时,用户可以输入代码了。
输入完毕,单击菜单上的“File”,在下拉菜单中单
击“Save As”,在“文件名”栏的编辑框中,键入文件
名,同时,必须键入正确的扩展名。
注意,如果用C语言编写程序,则扩展名为“.c”;
如果用汇编语言编写程序,则扩展名必须为“.asm”。然
后,单击“保存”按钮。
上述工作完成后,还有有关项目的设置,程序的编译
131
和链接,程序的调试。这些内容,读者可按Keil µVision3
开发环境的帮助功能,进行反复练习和操作,从而熟练地
掌握该软件的使用。篇幅所限,不再赘述。
14.4.3 C51程序的开发流程
C51程序开发与在Windows中运行的项目工程的开发
有所不同,在Windows中,一般程序的编译结果是后缀名
为“.exe”的可执行文件,该文件在Windows系统中能直
接运行,而单片机C51程序的开发属于嵌入式开发,遵循
主流的交叉编译模式,即在宿主机(运行Keil µVision3的
PC机)上开发编译,在目标机(51系列单片机)上运行。
在
132
宿主机上的编译结果为COFF文件或是满足Intel规范的
文件,文件包含了可执行的机器码,要经过编程器烧写到
单片机的程序区(Flash ROM)才能执行。
一个单片机C51应用程序的开发流程如下:
在Keil µVision3中新建项目工程→建立并编辑源文件
→编译调试项目工程→生成目标文件→下载到程序存储器
中→程序运行。有关Keil µVision3的具体使用,请见Keil
µVision3的使用操作说明。
14.5 C51与汇编语言的混合编程
目前多数开发人员都在用C51开发单片机程序,但
133
在一些速度和时序敏感的场合下,C51略显不足,而有些
特殊的要求必须通过汇编语言程序来实现,但是用汇编语
言编写的程序远不如用C51语言编写的可读性好和效率高。
因此采用C51与汇编语言混合编程是解决这类问题的最好
方案。
134
14.5.1 C51与MCS-51汇编语言的比较
无论是采用C51语言还是汇编语言,源程序都要转换
成机器码,单片机才能执行。对于用C51编制的程序,要
经过编译器,而采用汇编语言编写的源程序要经过汇编器
汇编后产生浮动地址作为的目标程序,然后经过链接定位
器生成十六进制的可执行文件。
用MCS-51汇编语言编程时,需要考虑它的存储器结
构,尤其要考虑其片内数据存储器与特殊功能寄存器的合
理正确使用,及按实际地址处理端口数据。就是说编程者
必须具体地组织、分配存储器资源和正确处理端口数据。
135
C语言能直接对计算机的硬件进行操作,与汇编语言相比
它具有如下优点:
(1)C51要比MCS-51汇编语言的可读性好。
(2)程序由若干函数组成,为模块化结构。
(3)使用C51编写的程序可移植性好。
(4)编程及程序调试的时间短。
(5) C51中的库函数包含了许多标准的子程序,且具有
较强的数据处理能力。大大减少编程工作量。
(6)对单片机中的寄存器分配、不同存储器的寻址以及
数据类型等细节可由编译器来管理。
136
汇编语言的特点如下:
(1)代码执行效率高。
(2)占用存储空间少。
(3)可读性和可移植性差。
使用C51编程,虽不像汇编语言那样要具体地组织、
分配存储器资源和处理端口数据,但是对数据类型和变量
的定义,必须与AT89C51的存储器的存储器结构相关联,
否则编译器就不能正确地映射定位。用C51编写的程序与
标准C程序编写的不同之处必须根据AT89C51的存储器结
构以及内部资源定义相应的数据类型和变量。
137
所以用C51编程时,如何定义与单片机相对应的数据
类型和变量,是使用C51编程的一个重要问题。
混合编程多采用如下的编程思想,程序的框架或主体
部分以及数据处理及运算用C51编写,时序要求严格的部
分用汇编语言编写。这种混合编程的方法将C语言和汇编
语言的优点结合起来,已经成为目前单片机程序开发的最
流行的编程方法。
138
14.5.2 C51与汇编语言混合编程的方法
在把汇编语言程序加入到C语言程序前,须使汇编语
言和C51程序一样具有明确的边界、参数、返回值和局部
变量;必须为汇编语言编写的程序段指定段名并进行定义;
如果要在它们之间传递参数,则必须保证汇编程序用来传
递参数的存储区和C51函数使用的存储区是一样的。
在C51中使用汇编语言有以下三种方法。
139
1. C51代码中嵌入汇编代码
可通过预编译指令“asm”在C51代码中嵌入汇编代
码。方法是用#pragma语句,具体结构为:
#pragma asm
汇编指令行
#pragma endasm
这种方法是通过asm和endasm告诉C51编译器,中间
的行不用编译为汇编行,例如:
#include <reg51.h>
extern unsigned char code newval[256] ;
void func1(unsigned char param)
140
{
unsigned char temp;
temp= newval[param]
temp*=2; temp/=3;
#pragma asm
MOV
; 预编译指令asm
P1,R7 ; 这些汇编语言代码行不用再编译为汇编行
NOP
NOP
NOP
MOV
P1,#0
#pragma endasm
}
141
注意,Keil µVision3的默认设置不支持asm和
endasm,采用本法进行混合编程,需要在.c文件的
option设置中,允许用伪指令asm和endasm选项。
2. 控制命令SRC控制
本方式最为灵活简单,先用C51编写代码,然后用
SRC控制命令将C51文件编译生成汇编文件(. SRC),
在该汇编文件中对要求严格的部分进行修改,保存为汇编
文件.ASM,再用A51进行编译生成机器代码。
3. 模块间接口
本方式,汇编语言程序部分和C51程序部分位于不同
142
的模块,或不同的文件,通常由C51程序模块调用汇编语
言程序模块的变量和函数,例如调用汇编语言编写的中断
服务程序。
C51模块和汇编模块的接口比较简单,分别用C51和
A51对源文件进行编译,然后用L51连接obj文件即可。模
块接口间的关键问题是C51函数与汇编语言函数之间的参
数传递。C51中有两种参数传递方法。
(1)通过寄存器传递
(2)通过固定存储区传递
143