admin 管理员组文章数量: 887021
2023年12月23日发(作者:border的中文)
中国科学技术大学
业余无线电协会编
目 录
§1 前言………………………………………………………………………………………… 1
§2 单片机简介………………………………………………………………………………… 2
2.1 数字电路简介…………………………………………………………………………… 2
2.2 MCS-51单片机简介……………………………………………………………………… 2
2.3 Easy 51 Kit Pro简介………………………………………………………………… 5
2.4 Easy 51 Kit Pro电路功能分析……………………………………………………… 5
§3 MCS-51单片机的C语言编程……………………………………………………………… 8
3.1 汇编语言………………………………………………………………………………… 8
3.2 建立你的第一个C项目………………………………………………………………… 8
3.3 生成hex文件…………………………………………………………………………… 12
3.4 Keil C语言……………………………………………………………………………… 14
3.5 单片机I/O……………………………………………………………………………… 18
3.6 中断……………………………………………………………………………………… 25
3.7 定时器/计数器………………………………………………………………………… 27
3.8 定时器的应用举例……………………………………………………………………… 29
3.9 外部中断………………………………………………………………………………… 34
3.10 串行通信……………………………………………………………………………… 38
3.11 定时器2……………………………………………………………………………… 43
3.12 看门狗………………………………………………………………………………… 47
3.13 空闲模式和掉电模式………………………………………………………………… 50
§4 MCS-51单片机C语言编程应用进阶…………………………………………………… 51
4.1 扫描式键盘……………………………………………………………………………… 51
4.2 EEPROM芯片AT93C46的读写…………………………………………………………… 55
4.3 Keil C的高级使用……………………………………………………………………… 63
§5 编写高质量的单片机C程序……………………………………………………………… 64
5.1 文件结构………………………………………………………………………………… 64
5.2 程序的版式……………………………………………………………………………… 66
5.3 单片机程序命名规则与变量选择……………………………………………………… 70
5.4 表达式和基本语句……………………………………………………………………… 73
5.5 函数设计………………………………………………………………………………… 77
5.6 单片机程序框架………………………………………………………………………… 79
附图:Easy 51 Kit Pro电路图(最小系统板)…………………………………………… 80
附图:Easy 51 Kit Pro电路图(学习板)………………………………………………… 81
V2.0.2
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
§1 前言
什么是单片机,目前还没有一个确切的定义。普通认为单片机是将CPU、RAM、ROM、定时器/计数器以及输入输出(I/O)接口电路等计算机主要部件集成在一块芯片上,这样所组成的芯片级微型计算机称为单片微型计算机(Single Chip Microcomputer)。简称为单片微机或单片机。利用单片机程序,可以实现对硬件系统的小型化的智能控制。由于单片机的硬件结构与指令系统都是按工业控制要求设计的,常用于工业的检测、控制装置中,因而也称为微控制器(Micro-Controller)或嵌入式控制器(Embedded-Controller)。
单片机的应用十分广泛,我们将以Easy 51 Kit Pro单片机学习板为基础,学习51单片机的入门知识。
本学习资料面向掌握基本电路知识和基础C语言编程的单片机初学者。为使读者能迅速上手,本资料并不深入介绍单片机的内部体系结构和指令系统,而是从读者较熟悉的高级语言开始使读者掌握单片机的C语言编程。通过本资料的大量程序例子,读者应当可以在较短的时间内熟悉单片机的入门编程以及单片机基本外围电路的连接,从而具备基本的单片机开发能力。然而,本资料的局限性也正在于此,当读者需要更深入地开发单片机或其它控制器时,可能需要进一步了解其内部体系结构和指令系统,这时读者就应查阅其它资料了。
同时,本学习资料还致力于引导读者编写高质量的单片机C语言程序。尽管单片机程序的规模有限,但高质量的单片机程序除了可以优化运行效率外,对程序的开发速度和可维护性也具有重要的影响。
中国科学技术大学业余无线电协会
1
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
§2 单片机简介
2.1 数字电路简介
在一个控制系统中,单片机是电路的一部分,单片机中的程序是针对其所在的电路编写的。因此,要对单片机编程并实现一定的功能,必须了解整个系统的电路图。单片机编程是针对某个特定的电路进行的,这一点和普通的编程不一样。编程时很可能要经常参照电路图。与单片机直接相关的电路大都是数字电路。数字电路各部分的功能十分明确,因此相对比较容易掌握。
简单地说,数字电路就是只有“0”和“1”两种信号的电路。判别信号究竟是“0”还是“1”是通过电压的大小(常称作“电平”)来判断的。不同的数字器件的电平判断标准是不一样的。常用的数字器件以高电平(超过某一阈值的电平)作为逻辑“1”,以低电平(低于某一阈值的电平)作为逻辑“0”。其中高电平阈值大于低电平阈值,处于高电平阈值与低电平阈值之间的电压是无效的。高电平阈值与低电平阈值的具体值与数字器件的供电电压有关,如AT89S51单片机的高电平阈值为(0.2Vcc+0.9)V,低电平阈值为(0.2Vcc-0.1)V,其中Vcc为单片机的供电电压。早年常用的数字器件的额定供电电压为5V,现在3.3V、1.8V等电压的数字器件已经大量使用了。在Easy 51 Kit Pro中,我们仍使用5V供电的单片机。另外,还有一种RS-232电平标准,以-12V~-5V作为逻辑“1”,以5V~12V作为逻辑“0”。电脑上的串口都符合RS-232标准。
还有一个“地”的概念。在电路中“地”并不是通常意义中的地,而是指电路中的一点,这一点的电压被人为地规定为0V。
2.2 MCS-51单片机简介
目前生产单片机产品的公司非常多,当中较有影响力的有intel公司推出的MCS-51系列等。很多公司的产品都是与MCS-51架构兼容(MCS-51 compatible)的。本资料中采用的atmel公司的AT89C51/52或AT89S51/52单片机,就是兼容MCS-51架构的单片机。
Atmel公司的AT89C51(以后简称“C51”)、AT89C52(以后简称“C52”)、AT89C2051(以后简称“C2051”)以及C51、C52的换代产品AT89S51(以后简称“S51”)、AT89S52(以后简称“S52”)容易上手、价格低廉(不超过10元/片)、资料丰富,是初学者入门时广泛采用的单片机。
C51拥有4096字节(1字节=8位)的片内程序存储器、128字节的RAM、32个I/O口、两个定时器、6个中断源、一个串口等。C52、C2051的资源与C51差别不大,其中C52的片内程序存储器为8192字节、RAM为256节、定时器有3个,其它与C51一样; C2051的片内程序存储器为2048字节、I/O口只有15个,另比C51多了一个模拟比较器,工作电压范围比较宽,为2.7V~6V(C51/52、S51/52为4.5~5.5V),其它与C51一样。
I/O、定时器、中断、串口等资源的用法在后面有详细介绍。这里只对程序存储器与RAM作一下说明。单片机程序代码经过编译(C程序)或汇编(汇编程序)后,要把编译或汇编得到的代码文件(一般来说编译得到hex格式文件、汇编得到bin格式文件)烧写到单片机内,存放这个程序的地方就是程序存储器。显而易见,单片机的程序存储器越大,我们就可以把越大、越复杂的程序放进去。如果我们编写的程序太大,那么单片机的程序存储器就有可能会放不下这个程序。这时,解决办法就只有精简代码、外接程序存储器(前提是所用的单片机支持使用外部程序存储器)或采用程序存储器容量更大的单片机。RAM是单片机程序运行时存放变量的地方,常量也可以放在RAM中。C51中的RAM大小为128字节,这就是说单片机程序中最多只能同时存在128个unsigned char型的变量或64个unsigned int型的变量(在Keil环境中,int型变量的长度为16位,这与VC的32位不一样)或者是相应的2中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
各种不同类型变量的组合。可以看出,单片机程序所允许的规模比Windows或其它操作系统环境下的程序要小得多。编写单片机程序时一定要注意不要滥用资源。
S51与S52分别是C51、C52的换代产品。从用户的角度看,S5x单片机比相对应的C5x单片机多了看门狗与在线编程(ISP)功能,另外最高运行速度有所增加(C5x最高支持24MHz的时钟频率,而S5x最高支持33MHz的时钟频率,但市面上比较容易买到的S5x单片机仍只最高支持24MHz的时钟频率)。
看门狗的使用在后面会有详细介绍,我们来看看单片机的在线编程(ISP)功能。要把程序烧写到AT89C系列单片机中,最常用的做法是把单片机插入专用的编程器中,通过编程器把程序烧到单片机里。这样做的麻烦之处是在调试程序时,编程者对程序作出的每次修改,都要把单片机从电路中拔出来,插到编程器,烧好后又要把单片机重新插回电路板。可以想象,这种工作是吃力不讨好的。利用S5x单片机的ISP功能,我们就无须来回插拔单片机,只要在电路中把单片机的ISP编程引脚接出来,并且这几个引脚所接的外围电路对ISP没有影响,就可以用ISP编程器对单片机进行烧写了。另外,支持AT89C系列单片机的编程器成本要比ISP下载线高最少几倍。一根并口ISP下载线的成本仅几元钱。
除了S系列、C系列外,atmel公司的MCS-51兼容产品也有其它系列,它们的主要区别在于供作电压范围,在此就不作介绍了。
值得注意的是,一片单片机可以反复擦写的次数是有限的,atmel的C系列、S系列单片机的声称可重复擦写次数为1000次。
下面以S52为例,对其引脚功能一一作出说明。C51、C52以及S51的引脚功能与S52大致相同,如有遇到不同的地方会用粗斜体字特别说明。
图2.2 AT89C2051引脚功能图
图2.1 AT89S52引脚功能图
1、电源引脚
Vcc 40 电源引脚
GND 20 接地引脚
工作电压为4V~5.5V。
2、外接晶体引脚
3中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
图2.3 外接晶体引脚
XTAL1 19
XTAL2 18
XTAL1是片内振荡器的反相放大器输入端,XTAL2则是输出端,使用外部振荡器时,外部振荡信号应直接加到XTAL1,而XTAL2悬空。内部方式时,时钟发生器对振荡脉冲二分频,如晶振为12MHz,时钟频率就为6MHz。晶振的频率最高可以达到33MHz(C5x为24MHz)。电容取30pF±10pF。
单片机程序指令的执行是以振荡器的振荡来驱动的。在MCS-51架构中,每12个振荡器周期组成一个指令周期(或称机器周期)。单片机执行指令的时间是以指令周期为单位的。不同指令的执行时间可能是不同的,一条指令的执行时间最短为一个指令周期。因此,单片机所接的振荡器频率越高,它执行指令的速度就越快。
型号同样为AT89S52的芯片,在其后面还有频率编号,有24MHz和33MHz等可选。读者在购买和选用时要注意了。如AT89S52 24PC就是最高振荡频率为24MHz,40P6封装的普通商用芯片。单片机芯片后缀的详细含义可见相应单片机数据手册的Ordering Information。
3、复位 RST 9
在振荡器运行时,有两个机器周期(24个振荡周期)以上的高电平出现在此引脚时,将使单片机复位,只要这个引脚保持高电平,单片机便一直处于复位状态。复位后P0~P3口均置1,引脚表现为高电平,程序计数器和特殊功能寄存器SFR全部清零。当复位引脚由高电平变为低电平时,芯片从ROM的00H处开始运行程序。复位操作不会对内部RAM有所影响。常用的复位电路如图2.4所示。当单片机上电时,由于电容的作用,RST引脚会处于短暂的高电平状态,直到电容充电到一定程度时,RST引脚的电平会被8.2K的电阻拉低,单片机开始运行程序。图中的按键为手动复位按键,当按下复位按键后,RST引脚会被1K的电阻上拉至高电平,单片机复位,按键松开后,RST恢复低电平,单片机重新从程序存储器的00H处运行程序。手动复位按键在单片机的最小系统中并不是必须的,但对单片机的复位控制会方便些。
4
中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
图2.4 常用复位电路
4、输入输出引脚
(1)P0端口[P0.0~P0.7] P0是一个8位漏极开路型双向I/O端口,端口置1(对端口写1)时作高阻抗输入端。作为输出口时能驱动8个TTL。P0端口要外接上拉电阻。
(2)P1端口[P1.0~P1.7] P1是一个带有内部上拉电阻的8位双向I/0端口。输出时可驱动4个TTL。端口置1时,内部上拉电阻将端口拉到高电平,作输入用。对内部Flash程序存储器编程时,接收低8位地址信息。除此之外P1端口还用于一些专门功能,具体见表2.1。
P1 引脚
P1.0
P1.1
P1.5
P1.6
P1.7
兼用功能
T2(外部计数器)、时钟输出(C51、S51无此功能)
T2EX(定时器2捕捉和重载触发及方向控制)(C51、S51无此功能)
MOSI(用于在线编程)(C51、C52无此功能)
MISO(用于在线编程)(C51、C52无此功能)
SCK(用于在线编程)(C51、C52无此功能)
表2.1 P1 端口引脚兼用功能表
(3)P2端口[P2.0~P2.7] P2是一个带有内部上拉电阻的8位双向I/0端口。输出时可驱动4个TTL。端口置1时,内部上拉电阻将端口拉到高电平,作输入用。对内部Flash程序存储器编程时,接收高8位地址和控制信息。在访问外部程序和16位外部数据存储器时,P2口
送出高8位地址。而在访问8位地址的外部数据存储器时其引脚上的内容在此期间不会改变。(4)P3端口[P3.0~P3.7] P2是一个带有内部上拉电阻的8位双向I/0端口。输出时可驱动4个TTL。端口置1时,内部上拉电阻将端口拉到高电平,作输入用。对内部Flash程序存储器编程时,接控制信息。除此之外P3端口还用于一些专门功能,具体见表2.2。
P3 引脚
P3.0
P3.1
P3.2
P3.3
P3.4
P3.5
P3.6
P3.7
兼用功能
串行通信输入(RXD)串行通信输出(TXD)外部中断0(INT0)外部中断1(INT1)定时器0输入(T0)
定时器1输入(T1)
外部数据存储器写选通WR
外部数据存储器写选通RD
表2.2 P3 端口引脚兼用功能表
中国科学技术大学业余无线电协会
5
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
5、其它的控制或复用引脚
(1)ALE/-PROG 30 访问外部存储器时,ALE(地址锁存允许)的输出用于锁存地址的低位字节。即使不访问外部存储器,ALE端仍以不变的频率输出脉冲信号(此频率是振荡器频率的1/6)。在访问外部数据存储器时,出现一个ALE脉冲。对Flash存储器编程时,这个引脚用于输入编程脉冲PROG。
(2)PSEN 29 该引脚是外部程序存储器的选通信号输出端。当S52由外部程序存储器取指令或常数时,每个机器周期输出2个脉冲即两次有效。但访问外部数据存储器时,将不会有脉冲输出。
(3)-EA/Vpp 31 外部访问允许端。当该引脚访问外部程序存储器时,应输入低电平。要使S52只访问外部程序存储器(地址为0000H-FFFFH),这时该引脚必须保持低电平。当使用内部的程序存储器时,此引脚应与Vcc相连。对Flash存储器编程时,用于施加Vpp编程电压。
C2051的引脚功能与C51相仿,在此不另外介绍了,仅给出C2051的引脚图,如图2.2所示。
2.3 Easy 51 Kit Pro简介
Easy 51 Kit Pro是中国科大业余无线电协会继Easy 51 Kit之后设计的一块51单片机学习板,供单片机初学者使用。Easy 51 Kit Pro包括了四位的数码LED显示输出、两个按键输入、一个3×4扫描式键盘和外接93C46 EEPROM芯片等。利用这块学习板,初学者可以学习51单片机各模块的编程,熟悉单片机程序的特点,也可以试着写一些功能比较完整的程序(如闹钟、秒表等)。如果初学者能够掌握本资料中给出的各个例子,就可以独立开发单片机程序了。
Easy 51 Kit Pro由两块电路板组成。其中大的电路板为MCS-51最小系统板,小的电路板为学习板(学习板中没有单片机)。采用两块电路板的原因是考虑到当初学者入门后学习板部分的电路可能已经没有很大的用处,而一块设计完善的最小系统板可能在日后仍能派上用场。在设计上,Easy 51 Kit Pro支持USB接口供电,采用一体化设计,学习板可以直接插在最小系统板上面,无须数据线连接,两块电路板可共用同一电源。如果单片机采用AT89S51或AT89S52,那么整个Easy 51 Kit Pro与并口ISP下载线的总成本大约为50元,再加上一根双公头的A型USB线,就可以轻松学习MCS-51单片机了。
2.4 Easy 51 Kit Pro电路功能分析
现在我们来分析一下Easy 51 Kit Pro的电路。Easy 51 Kit Pro的电路见附图。
最小系统板包含了51单片机的最小系统、I/O线外接插座以及可断开的232电平转换芯片及9针串口等。C5x、S5x的最小系统如图2.5。这个最小系统在前面已有详细介绍,在这里就不罗嗦了。标记为“TX”和“RX”的跳线可以用跳线帽把MAX232与单片机的串口断开或接通。接通时可以与电脑的串口直接通信,断开时则MAX232与单片机无任何电气上的信号连接。
6中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
图2.5 MCS-51单片机最小系统
在学习板中的电路我们将会配合后面的例子逐步进行分析。
中国科学技术大学业余无线电协会
7
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
§3 MCS-51单片机的C语言编程
3.1 汇编语言
在学习51单片机的C语言编程之前,我们先来了解一下汇编语言。使用汇编语言可以对单片机进行最直接的控制。每执行一条汇编语句,单片机就会执行一条指令。下面是一些汇编语句的例子:
LD AX,#0CCC2H
ADD AX,CX
利用汇编语言对单片机编程,所编写的代码效率很高,但用汇编语言写程序尤其是较大型的程序十分费时,程序的移植也存在问题。所以现在多用C语言对单片机进行编程,再在必要的地方用汇编语言实现。本资料介绍的是单片机的C语言编程。尽管汇编语言在单片机程序开发中有着其固有的缺点,但对单片机的指令系统以及汇编语言有一定的了解,对编写出高质量的单片机C程序是很有帮助的。
3.2 建立你的第一个C项目
使用C语言肯定要使用到C编译器,以便把写好的C程序编译为机器码,这样单片机才能执行编写好的程序。Keil uVision2是众多单片机应用开发软件中优秀的软件之一,下面我们用Keil uVision2建立一个小程序项目。
首先是运行Keil软件。运行几秒后,出现如图3.1的屏幕。
图3.1 启动时的屏幕
接着按下面的步骤建立第一个项目:
(1)点击Project菜单,选择弹出的下拉式菜单中的“New Project”,如图3.2。接着弹出一个标准Windows文件对话窗口,如图3.3。在“文件名”中输入你的第一个C程序项目名称,这里我们用“test”。“保存”后的文件扩展名为uv2,这是Keil uVision2项目文件扩展名,以后我们可以直接点击此文件以打开先前做的项目。
8中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
图3.2 New Project菜单
图3.3 文件窗口
(2)选择所要使用的单片机,这里我们选择Atmel公司的AT89S52。此时屏幕如图3.4所示。完成上面步骤后,我们就可以编写程序了。
9中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
图3.4 选取芯片
(3)首先我们要在项目中创建新的程序文件或加入旧程序文件。如果你没有现成的程序,那么就要新建一个程序文件。在这里我们以一个C程序为例介绍如何新建一个C程序和如何加到项目中。点击图3.5中1的新建文件的快捷按钮,在2中出现一个新的文字编辑窗口,这个操作也可以通过菜单“File”->“New”或快捷键Ctrl+N来实现。现在可以编写程序了,光标已出现在文本编辑窗口中,等待我们的输入。下面是一段程序:
#include
#include
void main(void)
{
SCON = 0x50; //串口方式1,允许接收
TMOD = 0x20; //定时器1 定时方式2
TCON = 0x40; //设定时器1 开始计数
TH1 = 0xE8; //22.1184MHz 2400 波特率
TL1 = 0xE8;
TI = 1;
TR1 = 1; //启动定时器
while(1)
{
printf("Hello World!n"); //显示Hello World
}
}
值得注意的是,Keil的文本编辑器对中文的支持欠佳,所以读者在写程序时,可以考虑用英文对程序代码进行注释。本书中的代码多用中文注释仅是为了使读者阅读方便。
(4)点击图3.5中的3保存新建的程序,也可以用菜单“File”->“Save”或快捷键Ctrl+S进行保存。因是新文件所以保存时会弹出类似图3.3的文件操作窗口,我们把第一个程序命名为“test1.c”,保存在项目所在的目录中,这时你会发现程序单词有了不同的颜色,说10中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
明Keil的C语法检查生效了。如图3.6鼠标在屏幕左边的“Source Group 1”文件夹图标上右击弹出菜单,在这里可以做在项目中增加减少文件等操作。我们选“Add File to Group
‘Source Group 1’”弹出文件窗口,选择刚刚保存的文件,按“ADD”按钮,关闭文件窗,程序文件已加到项目中了。这时在“Source Group 1”文件夹图标左边出现了一个小“+”号,说明文件组中有了文件,点击它可以展开查看。
图3.5 新建程序文件
图3.6 把文件加入到项目中
(5)C程序文件已被我们加到了项目中了,下面就剩下编译运行了。这个项目我们只是用做学习新建程序项目和编译运行仿真的基本方法,所以使用软件默认的编译设置,它不会生成用于芯片烧写的hex文件。我们先来看图3.7,图中1、2、3都是编译按钮,不同的是1是用于编译单个文件。2是编译当前项目,如果先前编译过一次之后文件没有作过编辑改动,11中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
这时再点击是不会再次重新编译的。3是重新编译,每点击一次均会再次编译链接一次,不管程序是否有改动。在3右边的是停止编译按钮,只有点击了前三个中的任一个,停止按钮才会生效。5是这三个按钮在菜单中相应的命令,效果和按钮是一样的。这个项目只有一个文件,你按1、2、3中的任一个都可以编译。在4中可以看到编译的错误信息和使用的系统资源情况等,以后我们要查错就靠它了。6是有一个小放大镜的按钮,这就是开启/关闭调试模式的按钮,它也在菜单“Debug”->“Start/Stop Debug Session”中,快捷键为Ctrl+F5。
图3.7 编译程序
(6)进入调试模式,软件窗口样式大致如图3.8所示。图中1为运行,当程序处于停止状态时才有效,2为停止,程序处于运行状态时才有效。3是复位,模拟芯片的复位,程序回到最开头处执行。按4我们可以打开5中的串行调试窗口,这个窗口我们可以看到从51芯片的串行口输入输出的字符,这里的第一个项目也正是在这里看运行结果。这些在菜单中也有,这里不再一一介绍。首先按4打开串行调试窗口,再按运行键,这时就可以看到串行调试窗口中不断的打印“Hello World!”。这样就完成了第一个C项目。最后我们要停止程序运行回到文件编辑模式中,就要先按停止按钮再按开启/关闭调试模式按钮。然后我们就可以进行关闭Keil等相关操作了。到此为止,我们初步学习了一些Keil uVision2的项目文件创建、12中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
编译、运行和软件仿真的基本操作方法。
图3.8 调试运行程序
3.3 生成hex文件
在开始学习C语言的主要内容时,我们先来看看如何用Keil uVision2来编译生成用于烧写芯片的hex文件。Hex文件格式是Intel公司提出的用来保存单片机或其他处理器的目标程序代码的文件格式。一般的编程器都支持这种格式。我们先来打开刚做的第一个项目,打开它的所在目录,找到2的文件就可以打开先前的项目了。然后右击图3.9中的1项目文件夹,弹出项目功能菜单,选“Options for Target’Target1’”,弹出项目选项设置窗口,同样先选中项目文件夹图标,这时在Project菜单中也有一样的菜单可选。打开项目选项窗口,转到“Output”选项页,如图3.10所示,图中1是选择编译输出的路径,2是设置编译输出生成的文件名,3则是决定是否要创建hex文件,选中它就可以输出hex文件到指定的路径中。现在我们重新编译一次,很快在编译信息窗口中就显示hex文件创建到指定的路径中了,如图3.11。这样我们就可用自己的编程器所附带的软件去读取并烧到芯片了。
中国科学技术大学业余无线电协会
13
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
图3.9 项目功能菜单
图3.10 项目选项窗口
中国科学技术大学业余无线电协会
14
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
图3.11 编译信息窗口
3.4 Keil C语言
相信读者们对标准C语言(ANSI C)已经十分熟悉。Keil中的C语言与ANSI C语言略有变化,下面对Keil C语言的介绍将着重介绍Keil C与ANSI C不同的地方。
3.4.1 数据类型
表3.1中列出了Keil uVision2 C51编译器所支持的数据类型。在标准C语言中基本的数据类型为char、int、short、long、float和double,而在C51编译器中int和short相同,float和double相同,这里就不列出说明了。下面来看看它们的具体定义:
数据类型
unsigned char
signed char
unsigned int
signed int
unsigned long
signed long
float
*
bit
sfr
sfr16
sbit
长 度
单字节
单字节
双字节
双字节
四字节
四字节
四字节
1~3 字节
位
单字节
双字节
位
值域
0~255
-128~+127
0~65535
-32768~+32767
0~4294967295
-2147483648~+2147483647
±1.175494E-38~±3.402823E+38
对象的地址
0 或1
0~255
0~65535
0 或1
表3.1 KEIL uVision2 C51 编译器所支持的数据类型
1、char 字符类型
char类型的长度是一个字节,通常用于定义处理字符数据的变量或常量。分无符号字符类型unsigned char和有符号字符类型signed char,默认值为signed char类型。unsigned
char类型用字节中所有的位来表示数值,所可以表达的数值范围是0~255。signed char类型用字节中最高位字节表示数据的符号,“0”表示正数,“1”表示负数,负数用补码表示(正数的补码与原码相同,负二进制数的补码等于它的绝对值按位取反后加1)。所能表示的数值范围是-128~+127。unsigned char常用于处理ASCII字符或用于处理小于或等于255
中国科学技术大学业余无线电协会
15
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
的整型数。在51单片机程序中,unsigned char是最常用的数据类型。
2、int 整型
int整型长度为两个字节,用于存放一个双字节数据。分有符号int整型数signed int和无符号整型数unsigned int,默认值为signed int类型。signed int表示的数值范围是-32768~+32767,字节中最高位表示数据的符号,“0”表示正数,“1”表示负数。unsigned
int表示的数值范围是0~65535。
3、long 长整型
long长整型长度为四个字节,用于存放一个四字节数据。分有符号long长整型signed
long和无符号长整型unsigned long,默认值为signed long类型。signed int表示的数值范围是-2147483648~+2147483647,字节中最高位表示数据的符号,“0”表示正数,“1”表示负数。unsigned long表示的数值范围是0~4294967295。
4、float 浮点型
float浮点型在十进制中具有7位有效数字,是符合IEEE-754标准的单精度浮点型数据,占用四个字节。
5、* 指针型
指针型本身就是一个变量,在这个变量中存放的指向另一个数据的地址。这个指针变量要占据一定的内存单元,对不同的处理器长度也不尽相同,在C51中它的长度一般为1~3个字节。
6、bit 位标量
bit位标量是C51编译器的一种扩充数据类型,利用它可定义一个位标量,但不能定义位指针,也不能定义位数组。它的值是一个二进制位,不是0就是1,类似一些高级语言中Boolean类型中的True和False。
7、sfr 特殊功能寄存器
sfr也是一种扩充数据类型,占用一个内存单元,值域为0~255。利用它可以访问51单片机内部的所有特殊功能寄存器。如用sfr P1 = 0x90 这一句定P1为P1端口在片内的寄存器,在后面的语句中我们用P1 = 255(对P1 端口的所有引脚置高电平)之类的语句来操作特殊功能寄存器。
8、sfr16 16 位特殊功能寄存器
sfr16占用两个内存单元,值域为0~65535。sfr16和sfr一样用于操作特殊功能寄存器,所不同的是它用于操作占两个字节的寄存器,如定时器T0和T1。
9、sbit 可寻址位
sbit同样是C51中的一种扩充数据类型,利用它可以访问芯片内部RAM中的可寻址位或特殊功能寄存器中的可寻址位。如先前我们定义了
sfr P1 = 0x90; //因P1 端口的寄存器是可位寻址的,所以我们可以定义
sbit P1_1 = P1^1; //P1_1 为P1 中的P1.1 引脚
同样我们可以用P1.1的地址去写,如sbit P1_1 = 0x91;
16中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
这样我们在以后的程序语句中就可以用P1_1来对P1.1引脚进行读写操作了。通常这些可以直接使用系统提供的预处理文件,里面已定义好各特殊功能寄存器的简单名字,直接引用可以省去一点时间。当然你也可以自己写自己的定义文件,用你认为好记的名字。
3.4.2 常量
常量是在程序运行过程中不能改变值的量。变量的定义可以使用所有C51编译器支持的数据类型,而常量的数据类型只有整型、浮点型、字符型、字符串型和位标量。
常量的数据类型说明是这样的:
1、整型常量可以表示为十进制如123,0,-89等。十六进制则以0x开头如0x34,-0x3B等。长整型就在数字后面加字母L,如104L,034L,0xF340等。
2、浮点型常量可分为十进制和指数表示形式。十进制由数字和小数点组成,如0.888,3345.345,0.0等,整数或小数部分为0,可以省略但必须有小数点。指数表示形式为[±]数字[.数字]e[±]数字,[]中的内容为可选项,其中内容根据具体情况可有可无,但其余部分必须有,如125e3,7e9,-3.0e-3。
3、字符型常量是单引号内的字符,如‘a’,‘d’等,不可以显示的控制字符,可以在该字符前面加一个反斜杠“”组成专用转义字符。常用转义字符表请见表3.2。
4、字符串型常量由双引号内的字符组成,如“test”,“OK”等。当引号内的没有字符时,为空字符串。在使用特殊字符时同样要使用转义字符如双引号。在C中字符串常量是作为字符类型数组来处理的,在存储字符串时系统会在字符串尾部加上o转义字符以作为该字符串的结束符。字符串常量“A”和字符常量‘A’是不同的,前者在存储时多占用一个字节的字间。
5、位标量,它的值是一个二进制。
转义字符
o
n
r
t
b
f
'
"
含义
空字符(NULL)
换行符(LF)
回车符(CR)
水平制表符(HT)
退格符(BS)
换页符(FF)
单引号
双引号
反斜杠
ASCII 码(16/10 进制)
00H/0
0AH/10
0DH/13
09H/9
08H/8
0CH/12
27H/39
22H/34
5CH/92
表3.2 常用转义字符表
常量可用在不必改变值的场合,如固定的数据表,字库等。常量的定义方式有几种,下面来加以说明。
#define FALSE 0x0; //用预定义语句可以定义常量
#define TRUE 0x1; //这里定义False 为0,True 为1
//在程序中用到False 编译时自动用0 替换,同理True 替换为1
unsigned int code a=100; //这一句用code 把a 定义在程序存储器中并赋值
const unsigned int a=100;//这一句用const关键字把a定义在RAM中并赋值
常量的合理使用可以提高程序的可读性、可维护性。因此,一个非小型的高质量的单片机C程序必定会用到常量。上面介绍了定义常量的三种方法:宏定义、用code关键字定义中国科学技术大学业余无线电协会
17
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
以及用const关键字定义。通过宏定义的常量并不占用单片机的任何存储空间,而只是告诉编译器在编译时把标识符替换一下,这在资源受限的单片机程序中显得非常有用。用code关键字定义的常量放在单片机的程序存储器中;用const关键字定义的常量放在单片机的RAM中,要占用单片机的变量存储空间。单片机的程序存储器空间毕竟要比RAM大得多(S51、C51只有128字节的RAM空间,S52、C52只有256字节的RAM空间),所以当要定义比较大的常量数组时,用code关键字定义常量要比用const关键字定义合理一些。
3.5 单片机I/O
输入输出(I/O)是单片机的最基本功能。C51、C52、S51、S52共有4个I/O端口,合共32根I/O引脚。每个引脚都可以分别设置用作输入还是输出。在单片机程序中,只要往某个I/O寄存器写“1”,那么相对应的引脚就会输出高电平;反之,只要往某个I/O寄存器写“0”,那么相对应的引脚就会输出低电平。当单片机的I/O端口用作输入时,要先往相对应的I/O寄存器写“1”。这时在单片机程序中,就可以通过读相对应的I/O寄存器的值得知该引脚处于高电平状态(“1”)还是低电平状态(“0”)。
3.5.1 显示“0123”的程序
下面我们结合Easy 51 Kit Pro电路,详细分析一个简单的输出程序,并对程序进行仿真。在实际的硬件中,我们除了观看程序的效果外还可以用示波器查看单片机各个引脚的输出波形。在本资料中,所有程序中所用的晶体的频率为22.1184MHz。
1、 数码管显示原理
图3.12b 共阳极数码管原理图
图3.12a数码管
图3.12c 共阴极数码管原理图
在单片机系统中,常用LED数码管来显示各种数字或符号。由于它具有显示清晰、亮度高、使用电压低、寿命长的特点,因此使用非常广泛。
LED数码管由8个发光二极管(LED)组成,如图3.12a所示。其中7个长条形的LED排列成“日”字形,另一个圆点形的LED在右下角作为显示小数点用,能显示各种数字及部分英文字母。LED数码管按连接方式分为两种:一种是8个LED的阳极都连在一起的,称之为共阳极数码管,如图3.12b所示;另一种是8个LED的阴极都连在一起的,称之为共阴极数码管,如图3.12c所示。
共阴和共阳结构的数码管各笔划段名和安排位置是相同的。当LED导通时,相应的笔划段发亮,由发亮的笔划段组合显示各种字符。如果把8个笔划段dp、g、f、e、d、c、b、a对应于一个字节(8位)的D7、D6、D5、D4、D3、D2、D1、D0,那么显示字符的字形代码就18
中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
可以用8位二进制码表示。例如,对共阴的数码管,当公共阴极接地(为零电平),而阳极dp、g、f、e、d、c、b、a各段为01110011时,显示“P”字符,即对于共阴极的数码管,“P”字符的字形码是0x73。如果是共阳的数码管,公共阳极接高电平,显示“P”字符的字形码应为10001100(0x8C)。
在Easy 51 Kit Pro学习板中,使用了一个4位的数码管模块,能同时显示4个字母或数字符号。这个模块的4个数码管的笔划引脚是共用的,而公共阴极(或阳极)是各自独立的。因此,模块中的4位显示控制是不能同时进行的,而是通过扫描控制的。现以共阴极的4位数码管模块为例,说明如何显示“1234”这一组数字。我们把4个共阴极引脚分别记为DIG1、DIG2、DIG3和DIG4。首先我们给模块的笔划引脚加上“1”的共阴字形码电平0x06(00000110),然后给DIG1引脚送上低电平,DIG2、DIG3、DIG4引脚送上高电平,这样,模块的第二、三、四位都不亮,而第一位显示“1”的图案。经过一个较短的时间后,我们给模块的笔划引脚加上“2”的共阴字形码电平0x5b,然后给DIG2引脚送上低电平,DIG1、DIG3、DIG4引脚送上高电平,这样,模块的第一、三、四位都不亮,而第二位显示“2”的图案。接着,我们给模块的笔划引脚加上“3”的共阴字形码电平0x4f,然后给DIG3引脚送上低电平,DIG1、DIG2、DIG4引脚送上高电平,这样,模块的第一、二、四位都不亮,而第三位显示“3”的图案。最后,给模块的笔划引脚加上“4”的共阴字形码电平0x66,然后给DIG4引脚送上低电平,DIG1、DIG2、DIG3引脚送上高电平,这样,模块的第一、二、三位都不亮,而第四位显示“4”的图案。这个扫描过程循环进行。由于人眼的视觉暂留和LED的余辉效应,我们就觉得“1”、“2”、“3”、“4”这四个图案时同时连接出现的了。
2、 电路分析
我们把Easy 51 Kit Pro的电路中本程序用到的部分提取出来,重绘于图3.13。
图3.13 显示“0123”的程序的电路图
中国科学技术大学业余无线电协会
19
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
在图3.13中,D3为4位的共阳极数码管模块。DIG1~DIG4分别是4个位的共阳极。单片机的P1.0~P1.3四个端口分别通过74HC244后与4个公共极相连(电路图标号相同的地方在电气上都是连通的)。由于点亮数码管需要的电流较大,单片机的I/O输出高电平时不能提供足够的电流,所以电路中用74HC244来驱动数码管的公共阳极,在效果上,我们可以忽略掉74HC244,认为P1.0~P1.3就是直接与模块的四个公共极相连,控制着这四个公共极。P0.0~P0.7分别与模块的a~g七个笔划引脚以及dp相连。
3、 程序分析
程序中所包含的头文件at89x52.h里包含了C52和S52的寄存器的定义。如果用的单片机是C51或S51,则头文件应为at89x51.h;如果用的单片机是C2051,则头文件应为at89x051.h。
下面我们编写一个程序,在Easy 51 Kit Pro的数码管模块(共阳)上固定显示“0123”4个数字。4位数码管模块的原理以及Easy 51 Kit Pro的电路图都已经分析过了,实现这个程序应该很容易。程序中的P0与P1分别对应单片机的P0和P1端口。因此只要对P0或P1赋一个8位的值,就可以控制P0或P1的8个引脚的电平输出了。比如P1 = 0x01语句可以使P1.0输出高电平,而P1.1~P1.7输出低电平。
程序中SEG_CODE数组分别对应0~9这十个数字的共阳字形码,并且SEG_CODE [i] = i,这样有利于程序的编写。COMM数组控制模块的4个共阳极的电平。值得注意的是,在这个程序中,SEG_CODE数组和COMM数组都是常量,因此在这两个数组的声明中都加入了code关键字。
“for(j=0; j<200; j++);”这一句是用来提供一定的延时的,适当的延时可以改善显示的效果。大家可以试试不同的值以达到最佳效果。必须知道的是,这个循环200次的循环体所执行的指令次数并不是200次,而是与j的数据类型以及编译器有关。因此,这个循环的执行时间是不能直接确定的。想知道它的执行时间,最好用示波器观察一下时间。
另外可以看到,程序的显示控制部分是在主函数的while(1)死循环里进行的,这样数码管的显示就会不断地刷新,直到掉电为止。在以后的例子中读者可以看到,while(1)死循环是单片机程序必不可少的部分。
【程序1】程序清单如下:
/*I/O口分配情况*/
/*P0:P0_7用于控制七段数码管笔段码*/
/*P1:P1_3--扫描四个数码管的共阳极*/
//晶振频率22.1184MHz
#include
void main()
{
unsigned char code COMM[4] = {0x01,0x02,0x04,0x08};
unsigned char i,j;
unsigned char code SEG_CODE[]
= {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};
//0~9的共阳字形码
while(1)
{
20中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
for(i=0; i<4; i++)
{
P1 = COMM[i]; //选通一位数码管
P0 = SEG_CODE[i]; //送笔段码
for(j=0; j<200; j++); //延时
}
}
}
4、 仿真
在把程序烧到单片机之前,我们可以先在Keil软件中进行仿真。程序仿真是单片机编程中的重要手段。通过仿真,我们可以看到单片机内寄存器值的变化、输入输出端口的变化、串口、定时器/计数器的情况以及中断状态等。
单片机程序的软仿真与VC中的Debug Manager十分相似(但愿读者用过)。如果在调试单片机程序时,不使用硬件的仿真器,那么Keil中的软件仿真就是调试程序的最有力手段了。因为如果不对编写好的程序进行仿真而直接烧到单片机里,在电路中运行,就像是在编写普通C语言程序时只用printf语句在关键的地方打印一些关键信息出来。这样做往往只能看得出结果是错误的,而并不能很快地定位出出错的地方。更糟糕的是,在编写普通C语言程序时,即使不使用Debug Manager,你也可以在你所需要的地方加入printf语句,在显示器上打印出足够多的信息供你查错。但在单片机程序中,能够用于显示的资源往往是少之又少的。例如在Easy 51 Kit Pro学习板中,就只有4位的数码管用作显示,读者可以想象它能显示出多少能供你查错的信息。相信用过printf与Debug Manager调试程序的读者都可以体会到后者比前者可以带来多大的效率提升。而在调试单片机程序时,仿真(当然使用仿真器要比纯软件仿真要好,因为仿真器的仿真是在真实的电路中查看程序运行状态)比不仿真只会带来更大的效率提升。另外,在编写单片机程序时,你可能会遇到一些你认为是莫名其妙的错误(发现这些错误后,你可能可以很快地采取一些办法避开它或解决它,但理解这些错误出现的原因可能要花比较长的时间),这些错误往往隐蔽性很强,如果不通过仿真,你可能永远也想象不出这会是个错误。
在仿真中,我们把“for(j=0; j<200; j++);”一句改为:
for(j=0; j<10000; j++)
{
for(k=0; k<100; k++);
}
并把变量j的定义改为unsigned int,同时增加一个unsigned int类型的变量k。这样做的好处时增加延时时间,使我们能在仿真中比较清楚地看到单片机P0和P1端口的变化。
修改后重新编译,运行“Debug”菜单中的“Start/Stop Debug Session”。在“Peripherals”菜单中选中“I/O Ports”中的“Port0”和“Port1”。这时可以看见P0和P1端口了,如图3.14所示。然后点击“run”快捷按钮,仿真就开始了。这时可以看到P0和P1端口的值在不断改变(打上勾的为1,不打勾的为0)。
中国科学技术大学业余无线电协会
21
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
图3.14 P0和P1的仿真窗口
为了更清楚地检查程序运行是否正确,我们可以进入单步运行的模式。运行“Debug”菜单中的“Step”命令,可以一步一步地检查程序的运行。
5、 用示波器观察程序的运行
用示波器观察程序的运行的单片机程序调试中的重要手段。我们用示波器观察单片机的P0.0~P0.7、P1.0~P1.3引脚,应该可以看到各个引脚的电平是周期变化的。
至此,本资料中的第一个示例程序就分析完毕了。Easy 51 Kit Pro中还有4个LED,可以实现走马灯程序。相信看懂显示“0123”的程序后,走马灯程序对读者来说实在是太简单了。下面仅简单地分析一下电路。程序中用到的电路重绘于图3.15。4个LED D2、D3、D4、D5的阴极分别与单片机的P3.7、P3.6、P3.5、P3.4相连。LED的阳极通过限流电阻接到Vcc。当单片机的相应引脚置高电平时,相应的LED不亮;当单片机的相应引脚置低电平时,相应的LED亮。如果把LED的阳极与单片机的I/O口相连,而阴极通过限流电阻接到地,那么当单片机的相应引脚置低电平时,相应的LED不亮;当单片机相应的引脚置高电平时,相应的LED亮。但这种连接方式会由于单片机的I/O端口置高电平时驱动能力不足而使LED点亮时亮度不足。
图3.15 走马灯程序的电路图
22中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
【程序2】程序清单如下:
//晶振频率22.1184MHz
#include
void main()
{
unsigned char i;
unsigned int k;
unsigned char j;
P3 |= 0xf0; //P3.4~P3.7置为高电平,4个LED都灭
while(1)
{
for(i=4; i<8; i++)
{
P3 = ~(1<
for(k=0; k<10000; k++) //延时
{
for(j=0; j<10; j++);
}
}
}
}
读者应该已经注意到,程序2中用到了位运算。位运算是单片机程序中常用到的运算。在这个程序中,由于循环变量k的值要取到10000,而unsigned char型变量的取值只能取到255,所以k的类型必须为整型。循环变量j的取值最大为10,所以用unsigned char型的变量就够了。读者必须注意,不同的变量类型除了占用内存的空间不一样外(比如char型占一个字节,int型占两个字节),程序的执行效率也会受到影响。读者不妨把j由unsigned char型变量改成unsigned int型变量,程序的其它地方都不变。重新编译后的程序走马灯的速度将大大降低。
3.5.2 数按键次数的程序
在这个程序中,我们用一个按键作程序的输入,每按下一次按键,数码管的显示就加1(程序开始时显示“0”)。为简单起见,这次我们只用到数码管的一位。当数码管的数字从0加到9后,下一次按键事件将使数码管的显示归0。当按键按未按下时,单片机相应的I/O口引脚呈高电平,程序从单片机读出“1”;当按键按下时,单片机相应的I/O口接地,电压为0,程序从单片机读出“0”。按键按下时,单片机引脚的电平会发生短暂的抖动,在单片机输入引脚上接上一个0.01μF的接地电容可以减小抖动带来的干扰。
我们把Easy 51 Kit Pro的电路中本程序用到的部分提取出来,重绘于图3.16。电路中按键与单片机的P3.2相连。当单片机发现输入引脚P3.2有从高电平(“1”)到低电平(“0”)的变化时,认为是可能按键事件。再经过一段短时间的延时(约10ms),如果此时P3.2仍为低电平,则认为的确发生了按键事件。延时的目的是为了消除按键时发生的抖动,称为软件去抖动。
在程序中我们第一次用到bit型变量,这种变量在ANSI C中是没有的。在程序中为了23中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
便于区分变量的类型,我们在bit型变量前都加了小写的b,以与unsigned char型变量区别开来。Delay函数中的延时值DELAY_10MS用了宏定义,方便调试时修改。因为用循环的做法进行延时,这个值最好用实验的办法试出来。
图3.16 数按键程序的电路图
【程序3】程序清单如下:
//晶振频率22.1184MHz
#include
#define INPUT_PIN P3_2
//定义输入引脚,这里用pin而不是port,是因为只有一个引脚
#define SEG_PORT P0
#define DISPLAY_DIG1 P1&=0xf0;P1|=0x01
//选通4位数码管中的1位,用宏定义的方法使程序更直观
#define PRESSED 0 //定义按键的两个状态
#define UNPRESSED 1
#define DELAY_10MS 10000 //10ms的延时参数
24中国科学技术大学业余无线电协会
51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料
void Delay(unsigned int n)
{
unsigned int i;
for(i=0; i } void main() { unsigned char currentNumber = 0; //数码管显示的当前数值,初始化为0 bit bNewStatus,bOldStatus; //记录按键的前后两个状态 unsigned char code SEG_CODE[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; INPUT_PIN = 1; //把该引脚设置为输入 DISPLAY_DIG1; SEG_PORT = SEG_CODE[currentNumber]; //显示 bOldStatus = INPUT_PIN; //读输入引脚电平 while(1) { bNewStatus = INPUT_PIN; if(bNewStatus==PRESSED && bOldStatus==UNPRESSED) { //检测到电平负跳变 Delay(DELAY_10MS); bNewStatus = INPUT_PIN; //再读一次输入引脚 if(bNewStatus == PRESSED) { //确认为按键事件 if(currentNumber != 9) { //更新当前显示的数字 currentNumber++; } else { currentNumber = 0; } } } bOldStatus = bNewStatus; SEG_PORT = SEG_CODE[currentNumber]; //显示当前数字 } } 程序3中大量利用宏定义的方法定义常量、端口以及简单语句。这大大增加了程序的可读性。后面的程序示例也会沿用这一风格。 3.6 中断 中断是指在CPU处理某事情时,外部发生了某一事件,请求CPU马上处理,CPU暂25中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 时停止当前的处理工作,转入处理外部事件,处理完再重新返回原来中断的地方,继续执行原来的工作。 我们把请求CPU中断的请求源称为中断源,也就是CPU中一些可以引起中断的事件。C51、S51中一共有5个中断源:INT0中断、INT1中断、两个片内定时器中断(TF0、TF1)、一个串行口中断。而C52和S52则还有定时器2中断,共6个中断源。我们把这些中断分为外部中断和内部中断。外部中断包括INT0、INT1中断,内部中断包括两个或三个片内定时器中断(TF0、TF1)、一个串行口中断。其中INT0、INT1、定时器中断是由控制寄存器TCON控制,而串行口中断由串口控制寄存器SCON控制。 下面介绍几个与中断控制有关的寄存器。 1、 中断屏蔽寄存器IE 中断屏蔽寄存器IE控制对中断源的开放或者屏蔽,每一个中断源是否被允许中断,都由它来设置,其内容如表3.3所示。 名称 D7 D6 D5 D4 D3 D2 D1 D0 IE EA - ET2 ES ET1 EX1 ET0 EX0 表3.3 中断屏蔽寄存器IE EA:CPU的中断开放标志,EA=1,开放所有中断;EA=0,屏蔽所有的中断。 ET2:定时器/计数器T2溢出中断允许位,ET2=1,允许T2中断;ET2=0,禁止T2中断。在C51、S51和C2051中没有T2,所以C51、S51和C2051中的IE寄存器ET2位无定义。 ES:串行口中断允许位。ES=1,允许串行口中断;ES=0,禁止串行口中断。 ET1:定时器/计数器T1溢出中断允许位,ET1=1,允许T1中断;ET1=0,禁止T1中断。 EX1:外部中断1中断允许位。EX1=1,允许外部中断1中断;EX1=0,禁止外部中断1中断。 ET0:定时器/计数器T0溢出中断允许位,ET0=1,允许T0中断;ET0=0,禁止T0中断。 EX0:外部中断0中断允许位。EX0=1,允许外部中断0中断;EX0=0,禁止外部中断0中断。 2、中断优先级寄存器IP 中断优先级寄存器IP可以用来设置中断的优先级,只要用程序修改其内容,就可以实现各中断源级别的设置,其内容如表3.4所示。 名称 D7 D6 D5 D4 D3 D2 D1 D0 IP - - PT2 PS PT1 PX1 PT0 PX0 表3.4 中断优先级寄存器IP IP中的某位如果为1,则该位所对应的中断定义为高优先级中断;反之,IP中的某位如果为0,则该位所对应的中断定义为低优先级中断(PT2、PS、PT1、PX1、PT0、PX0分别对应定时器T2中断、串行口中断、定时器T1中断、外部中断1、定时器T0中断、外部中断0)。其中PT2位在C51、S51和C2051中无定义。 3、定时器/计数器控制寄存器TCON(见下一节) 4、串行口控制寄存器SCON 中国科学技术大学业余无线电协会 26 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 串行口控制寄存器SCON控制串行口的工作,其内容如表3.5所示。 名称 D7 D6 D5 D4 D3 D2 D1 D0 SCON SM0 SM1 SM2 REN TB8 RB8 TI RI 表3.5 串行口控制寄存器SCON SM0:串行口工作方式控制位。 SM1:串行口工作方式控制位,和SM0一起来选择工作方式。 SM2:多机通信控制位。 REN:允许串行接收控制位。由软件来设置,置1时允许接收,清零时禁止接收。 TB8:串行发送数据的第9位,由设置0或者1。 RB8:串行接收数据的第9位。 TI:发送中断标志位,由片内硬件在方式0串行发送第8位结束时置位,或在其它方式串行发送停止位的开始时置位。在中断服务程序中要清零才能发送下一次数据。 RI:接收中断标志位,由片内硬件在方式0串行接收到第8位结束时置位,或在其它方式串行接收到停止位的中间时置位。在中断服务程序中要清零才能接收下一次数据。 3.7 定时器/计数器 MCS-51内含2个16位的定时器/计数器T0和T1。它们分别有4种工作模式,其控制字均在相应的特殊功能寄存器中,通过对控制寄存器的编程,用户可以方便地选择行当的工作模式。对每个定时器/计数器,在特殊功能寄存器TMOD中都有一个控制位,它选择T0/T1为定时器还是计数器。 当T0/T1用于定时器方式时,定时器的输入来自内部时钟发生电路,每过一个机器周期,定时器加1,而一个机器周期包含有12个振荡周期,所以,定时器的计数频率为晶振频率的1/12。当T0/T1用于计数器方式时,计数器对外部事件计数,计数脉冲来自外部输入引脚。当外部输入引脚发生“1”到“0”的负跳变时,计数器的值加1。 MCS-51单片机定时器T0由TH0、TL0构成,T1由TH1、TL1构成。T0、T1的工作控制是由控制寄存器TCON和方式寄存器TMOD实现的。TMOD用于控制各定时器/计数器的功能和工作模式,TCON用于控制定时器/计数器T0、T1的启动和停止,同时包含定时器/计数器的状态。它们属于特殊功能寄存器,需要由软件来设置,在系统复位时,TCON、TMOD寄存器的所有位都被清零。 TCON的内容如表3.6所示。 名称 D7 D6 D5 D4 D3 D2 D1 D0 TCON TF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0 表3.6 TCON寄存器 TF1:定时器1溢出标志。当定时器/计数器1溢出时,由硬件置位;当主机响应中断,转向中断服务程序时,由硬件清零。 TR1:定时器1运行控制位,由软件置位/复位来开启或关闭定时器/计数器1。 TF0:定时器0溢出标志。当定时器/计数器0溢出时,由硬件置位;当主机响应中断,转向中断服务程序时,由硬件清零。 TR0:定时器0运行控制位,由软件置位/复位来开启或关闭定时器/计数器0。 IE1:外部中断1跳变中断请求标志,当检测到INT1发生1到0的跳变时,由硬件置位;当主机响应中断,转向中断服务程序时,由硬件清零。 IT1:外部中断1触发方式控制位,由软件置位或清零来选择外部中断1的跳变/电平触中国科学技术大学业余无线电协会 27 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 发中断请求。IT1=0时,外部中断1为电平触发方式,当INT1输入低电平时,置位IE1。采用电平触发方式时,外部中断源必须保持低电平有效,直到该中断被CPU响应,同时在该中断服务程序执行完之前,外部中断源必须被清除,否则将产生另一次中断。IT1=1时,外部中断1为边沿触发方式,在对INT1的相邻两次采样中,如果一个周期中为高电平,接下来的周期为低电平,则置位IE1,表示外部中断1正在向CPU申请中断。直到该中断被CPU响应时,才被硬件清零。 IE0:外部中断0跳变中断请求标志,当检测到INT1发生1到0的跳变时,由硬件置位;当主机响应中断,转向中断服务程序时,由硬件清零。 IT0:外部中断0触发方式控制位,应用同IT1。 TMOD的内容如表3.7所示。 名称 D7 D6 D5 D4 D3 D2 D1 D0 TMOD GATE M1 M0 GATE M1 M0 C/T C/T 表3.7 TMOD寄存器 GATE:门控制位,当GATEx=1时,控制寄存器TCON的TRx=1(x=0或1)。 C/T:定时器、计数器方式选择位,该位为1时为计数器,为0时为定时器。 M0:定时器/计数器工作模式选择位。 M1:定时器/计数器工作模式选择位。 定时器/计数器的工作模式由M0、M1来选择,共有4种工作模式,下面逐一介绍。 1、工作模式0 M1=0、M0=0时,工作在工作模式0,此时T0、T1的功能都是相同的,用户可以任意选用。定时器工作在模式0时,其计数器为13位,由TLx的低5位和THx的8位所构成,TLx的高3位可以不考虑。定时器/计数器溢出时,置位TCON中的溢出标志位TFx。 2、工作模式1 M1=0、M0=1时,工作在工作模式1,此时T0、T1的功能都是相同的,用户可以任意选用。工作模式1与工作模式0的区别是计数器的位数不同。工作模式0是13位计数器,工作模式1是16位计数器。TLx、THx作为16位寄存器用,计数值从初值开始(初值由软件设定),计数到0xFFFF后,再加1,计数器被溢出复位,并把溢出标志TFx置1。 3、工作模式2 M1=1、M0=0时,工作在工作模式2,此时T0、T1的功能都是相同的,用户可以任意选用。工作模式2下,计数器是8位的,并且具有自动恢复功能。TLx作为计数器用,而THx作为8位寄存器用,存放计数初始值。计数器开始以TLx的数值为初始值做增1计数,当溢出时,除了把溢出标志TFx置1外,还同时把THx的计数初值送入TLx,使TLx又重新从初始值开始计数,THx中的数值保持不变。 4、工作模式3 M1=1、M0=1时,工作在工作模式3,此时T0、T1的功能与前3种工作模式大不相同。在这种工作模式下,T1仅保留了它原来的计数值,并停止了计数,相当于设置了TR1=0,而T0分成了两个独立的8位计数器TH0和TL0。其中TL0使用T0的所有控制位(GATE、中国科学技术大学业余无线电协会 28 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 ,而TH0被固定设置为一个8位的定时器,对机器周期计数,并C/T、TR0、TF0和INT1)借用了T1的控制位TR1和TF1。这种方式适用于要求增加一个额外的定时器/计数器的场合。因为当T0工作在模式3时,T1可以定为模式0、1、2,作为串口的波特率发生器。 3.8 定时器的应用举例 定时器/计数器的应用十分广泛,下面我们举两个例子说明其使用方法。 3.8.1 实现数码管显示的按秒跳变 下面这个程序控制4位数码管模块中的第一位实现从0到9的跳变(跳到9后回0),。Easy 51 Kit Pro采用跳变时间为1秒。计数器的最大值为216-1=0xFFFF(16位计数器时)22.1184MHz的晶振,所以计数器计数增1的时间为12秒。使计数器溢出的最22.1184×10612×216≈35.6(ms)。为实现较准确的计时,我们可以选择计数器的初值,长时间为22.1184×106使计数器的溢出时间为25ms。这个初值计算如下: 初值=216−0.025=0x4C00 1222.1184×106所以TH=0x4C,TL=0。 计数器每溢出一次,就产生一次中断,产生40次中断就是一秒。 程序电路如图3.13。 【程序4】程序清单如下: //晶振频率22.1184MHz #include #define TIMER0H 0x4c #define TIMER0L 0x00 #define TIMER0_RUN TR0=1 #define SECOND_OVERFLOW 40 #define SEG_PORT P0 #define DISPLAY_DIG1 P1&=0xf0;P1|=0x01 unsigned char g_CurrentDigit=0; //当前显示的数字 void timer() interrupt 1 { static unsigned char s_Count = 0; TH0 = TIMER0H; //重置定时器初值 TL0 = TIMER0L; 29中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 //每次进入中断服务程序,TH0和TL0的值都已经变为0,所以要恢复原来的初始值 TIMER0_RUN; //定时器运行,开始下一次计数 if(s_Count != SECOND_OVERFLOW) { //未到整秒,把sCount值加1 s_Count++; } else { //到整秒,s_Count归0,更新把当前显示的数字 s_Count = 0; if(g_CurrentDigit != 9) { g_CurrentDigit++; } else { g_CurrentDigit = 0; } } return; } void Initial(void) //初始化 { IE = 0x82; //仅允许Timer0中断 TMOD = 0x01; //Timer0使用工作方式1(16位),定时器 TH0 = TIMER0H; //设置定时器初值 TL0 = TIMER0L; TIMER0_RUN; //定时器开始运行 DISPLAY_DIG1; } void main() { unsigned char code SEG_CODE[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; Initial(); while(1) { SEG_PORT = SEG_CODE[g_CurrentDigit]; //显示当前的数字 //当timer0溢出时,单片机响应timer0中断,调用timer函数, //每40次调用当前显示的数字加1 } } 程序分析: 30中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 程序中主程序做的事只是在死循环中反复显示当前的数字,每产生一次中断,程序就跳转到中断服务函数timer()中进行相应的更新。 这里中断服务函数timer()有别于普通C函数的地方是在声明中多了“interrupt 1”,说明这个函数是中断号为1的中断服务函数。各个中断对应的中断号如表3.8所示。 这个程序需要初始化的东西比较多,我们把这些初始化语句都放在了初始化函数Initial()中,这也是程序初始化很常见的做法。我们还第一次用到了静态变量和全局变量。全局变量是中断处理函数与外界程序进行参数传递的唯一途径,因此在单片机程序中全局变量的使用频率要比普通的C语言程序高。尽管如此,由于全局变量的使用会影响程序的结构化,所以在可以不使用全局变量的地方,还是要避免使用全局变量。在程序中,为了把全局变量与静态变量跟普通变量区别开来,我们在变量前分别加了小写g_和小写s_以示区别。 IE 寄存器中的使能位和C中的中断号 0 1 2 3 4 5 中断源 外部中断0 定时器0溢出 外部中断1 定时器1溢出 串行口中断 定时器2溢出(仅在S52、C52中有此中断源) 表3.8 Keil C中的中断号 3.8.2 延时函数 在程序中有时可能需要实现准确的延时,这就需要用到定时器。我们可以屏蔽掉相应定时器的中断请求,使CPU不响应中断,通过访问TFx来判断计数器是否溢出。下面这个程序编写了一个Delay(unsigned int)函数,入口参数为以毫秒为单位的延时时间。该程序利用延时函数实现了和上一程序相同的功能。Delay()函数通过反复调用延时10ms或1ms的函数来达到入口参数所要求的延时时间。最长的延时时间支持为约65秒。这里比较画蛇添足地同时编写了延时10ms和1ms两个函数,是考虑到延时1ms时计算得到的定时器初值不是整数,四舍五入后会产生一定的误差。而延时10ms的函数没有四舍五入误差。这样做在实用中未必很有用,也往往没有必要。读者可以注意一下这个程序的currentDigit变量由于无须与中断处理函数交换数据,所以不用全局变量,而是采用了局部变量。 【程序5】程序清单如下: //实现Delay函数,利用Delay函数而不是中断实现上一个程序的功能 //晶振频率22.1184MHz #include #define TIMER10MS_H 0xb8 #define TIMER10MS_L 0x00 #define TIMER1MS_H 0xf8 #define TIMER1MS_L 0xcd #define TIMER0_RUN TR0=1 #define TIMER0_STOP TR0=0 #define SEG_PORT P0 31中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 #define DISPLAY_DIG1 P1&=0xf0;P1|=0x01 void Delay10ms(void) //精确延时10ms { TH0 = TIMER10MS_H; TL0 = TIMER10MS_L; //给timer0写入初始值 TIMER0_RUN; //开始计数 while(!TF0); //等到定时器溢出 TF0 = 0; //清定时器溢出标志 TIMER0_STOP; //停止计数 return; } void Delay1ms(void) //延时1ms,由于晶振频率的关系误差比10ms的函数大 { TH0 = TIMER1MS_H; TL0 = TIMER1MS_L; //给timer0写入初始值 TIMER0_RUN; //开始计数 while(!TF0); //等到定时器溢出 TF0 = 0; //清定时器溢出标志 TIMER0_STOP; //停止计数 return; } void Delay(unsigned int t) //提供以毫秒为单位的延时 { while(t >= 10) //当延时时间大于10ms时,每10ms调用一次Delay10ms函数,比较准确 { Delay10ms(); t -= 10; } while(t) //当延时时间不足10ms时,每1ms调用一次Delay1ms函数,误差会大一点 { Delay1ms(); t--; } return; } void Initial(void) { IE = 0; //屏蔽所有中断 TMOD = 0x01; //Timer0使用工作方式1(16位),定时器 32中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 DISPLAY_DIG1; } void main() { unsigned char code SEG_CODE[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; unsigned char currentDigit; Initial(); while(1) { SEG_PORT = SEG_CODE[currentDigit]; //数码管显示当前数字 Delay(1000); //延时1000ms if(currentDigit != 9) { currentDigit++; } else { currentDigit = 0; } } } 3.8.3 重写数按键的程序 前面的数按键程序也有Delay函数,但那时的Delay函数并不能提供准确的延时。下面的程序采用提供准确延时的Delay函数。 【程序6】程序清单如下: #include #define TIMER10MS_H 0xb8 #define TIMER10MS_L 0x00 #define TIMER1MS_H 0xf8 #define TIMER1MS_L 0xcd #define TIMER0_RUN TR0=1 #define TIMER0_STOP TR0=0 #define SEG_PORT P0 #define DISPLAY_DIG1 P1&=0xf0;P1|=0x01 #define INPUT_PIN P3_2 #define PRESSED 0 #define UNPRESSED 1 void Delay10ms(void) 33中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 { TH0 = TIMER10MS_H; TL0 = TIMER10MS_L; //给timer0写入初始值 TIMER0_RUN; //开始计数 while(!TF0); //等到定时器溢出 TF0 = 0; //清定时器溢出标志 TIMER0_STOP; //停止计数 return; } void Delay1ms(void) { TH0 = TIMER1MS_H; TL0 = TIMER1MS_L; //给timer0写入初始值 TIMER0_RUN; //开始计数 while(!TF0); //等到定时器溢出 TF0 = 0; //清定时器溢出标志 TIMER0_STOP; //停止计数 return; } void Delay(unsigned int t) { while(t >= 10) { Delay10ms(); t -= 10; } while(t) { Delay1ms(); t--; } return; } void Initial(void) { IE = 0; //屏蔽所有中断 TMOD = 0x01; //Timer0使用工作方式1(16位),定时器 DISPLAY_DIG1; INPUT_PIN = 1; } 中国科学技术大学业余无线电协会 34 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 void main() { unsigned char currentNumber = 0; bit bNewStatus,bOldStatus; unsigned char code SEG_CODE[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; Initial(); bOldStatus = INPUT_PIN; while(1) { SEG_PORT = SEG_CODE[currentNumber]; bNewStatus = INPUT_PIN; if(bNewStatus==PRESSED && bOldStatus==UNPRESSED) { Delay(10); bNewStatus = INPUT_PIN; if(bNewStatus == PRESSED) { if(currentNumber != 9) { currentNumber++; } else { currentNumber = 0; } } } bOldStatus = bNewStatus; } } 上面的两个Delay函数各有好处。用循环延时的Delay函数尽管不能提供非常准确的延时,但程序简单,更可以省下单片机一个定时器资源,这在很多时候是很有用的。用定时器的Delay函数要占用一个定时器,在单片机定时器资源够用的情况下用来产生准确延时是个不错选择。 前面单片机I/O一节中的显示“0123”的程序中,延时也是用循环实现的。这个程序也可以用定时器(中断或查询)来实现。有兴趣的读者可以自己重写一下。 3.9 外部中断 外部中断的使用可以见前面TCON寄存器的说明。下面我们再次重写前面的数按键程序。这次我们利用单片机的外部中断0检测按键按下的事件。 【程序7】程序清单如下: #include 35中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 #define TIMER10MS_H 0xb8 #define TIMER10MS_L 0x00 #define TIMER1MS_H 0xf8 #define TIMER1MS_L 0xcd #define TIMER0_RUN TR0=1 #define TIMER0_STOP TR0=0 #define SEG_PORT P0 #define DISPLAY_DIG1 P1&=0xf0;P1|=0x01 #define ENABLE_INT EA=1 #define DISABLE_INT EA=0 #define INPUT_PIN P3_2 #define PRESSED 0 #define UNPRESSED 1 unsigned char g_CurrentNumber = 0; void Delay10ms(void) { TH0 = TIMER10MS_H; TL0 = TIMER10MS_L; //给timer0写入初始值 TIMER0_RUN; //开始计数 while(!TF0); //等到定时器溢出 TF0 = 0; //清定时器溢出标志 TIMER0_STOP; //停止计数 return; } void Delay1ms(void) { TH0 = TIMER1MS_H; TL0 = TIMER1MS_L; //给timer0写入初始值 TIMER0_RUN; //开始计数 while(!TF0); //等到定时器溢出 TF0 = 0; //清定时器溢出标志 TIMER0_STOP; //停止计数 return; } void Delay(unsigned int t) { while(t >= 10) { Delay10ms(); 中国科学技术大学业余无线电协会 36 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 t -= 10; } while(t) { Delay1ms(); t--; } return; } void key() interrupt 0 { bit bStatus; DISABLE_INT; //暂时禁止中断 Delay(10); bStatus = INPUT_PIN; if(bStatus == PRESSED) { if(g_CurrentNumber != 9) { g_CurrentNumber++; } else { g_CurrentNumber = 0; } } ENABLE_INT; //重新开放中断 } void Initial(void) { IE = 0x01; //屏蔽所有中断 TMOD = 0x01; //Timer0使用工作方式1(16位),定时器 DISPLAY_DIG1; INPUT_PIN = 1; IT0 = 1; //外部中断0:边缘触发方式 ENABLE_INT; } void main() { unsigned char code SEG_CODE[] = 中国科学技术大学业余无线电协会 37 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; } Initial(); while(1) { SEG_PORT = SEG_CODE[g_CurrentNumber]; } 3.10 串行通信 3.10.1 串行通信介绍 串行通信是指将构成字符的每个二进制数据位,依据一定的顺序逐位进行传送的通信方法。串行通信不同于并行通信,串行通信的数据是一位一位顺序地发送或接收,而并行通信的各数据位是同时传送的。串行通信比并行通信节省传送口线,传送距离远,但传送速度要慢。串行通信在主机与存储器、主机与打印机、存储器与存储器等方面应用广泛。 单片机的串行通信方式有两种:异步通信和同步通信。 1、异步通信 异步通信规定了字符数据的传送格式,每个数据用起始位表示字符的开始,用停止位表示字符的结束。在一帧数据中,先是一个起始位0,然后是若干个数据位,规定低位在前,高位在后,最后一位是奇偶校验位(可以省略),接下来是停止位1。用这种格式表示的字符,可以一个接一个地传送。 (1)起始位 在通信线上,没有数据传送时处于逻辑“1”状态。当发送设备要发送一个字符数据时,首先发出一个逻辑“0”信号,这个逻辑低电平就是起始位。起始位通过通信线传向接收设备,当接收设备检测到这个逻辑低电平后,就开始准备接收数据位信号。因此,起始位所起的作用就是表示字符传送的开始。 (2)数据位 当接收设备收到起始位后,紧接着就会收到数据位,数据位的个数可以是5、6、7或8位数据。在字符数据传送的过程中,数据位从最低有效位开始传送。 (3)奇偶校验位 数据位发送完之后,就可以发送奇偶校验位(也可以不发送)。奇偶校验位用于有限差错检验,通信双方在通信时约定一致的奇偶校验方式。就数据传送而言,奇偶校验位是冗余位,但它表示数据的一种性质,这种性质用于检错,虽然检错能力有限但很容易实现。 (4)停止位 在奇偶校验位或者数据位(无奇偶校验位时)之后是停止位。它可以是1位、1位半或2位,停止位是一个字符数据的结束标志。 在异步通信中,字符数据一个接一个地传送。在发送间隙,也就是空闲时,通信总路线处于逻辑“1”状态(高电平),每个字符数据的传送均以逻辑“0”(低电平)开始。 在异步通信中,CPU与外设之间必须有两项约定,即字符格式和波特率。字符格式的约定使双方能够在对同一种0和1的串理解成同一种意义。波特率是数据传送的速率,即每秒钟传送的二进制数的位数。例如,数据传送的速率是120字符/秒,而每个字符如上述约定包含10个信息位,则传送波特率为1200位/秒(波特)。“位/秒”常表示为“bps”。 2、同步通信 38中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 在异步通信中,每个字符要用起始位和停止位作为字符开始和结束的标志,发送标志多占用了时间,所以在数据块传送时,为了提高速度,常去掉这些标志,而采用同步传送。同步通信不像异步通信那样,靠起始位在每个字符数据开始发送和接收同步,而是通过同步字符在每个数据块传送开始时使收发双方同步。由于数据块传输开始要用同步字符来指示,同时要求由时钟来实现发送端与接收端之间的同步,所以同步通信的硬件比较复杂。 同步通信的特点如下: (1)以同步字符作为传送的开始,从而使收发双方得到同步。 (2)每位占用的时间都相等。 (3)字符数据之间不留空隙,当线路空闲或没有字符可发送时,发送同步字符。 同步字符可由用户选择一个或者两个特殊的8位二进制码,与异步通信收发双方使用的相同的字符格式一样,同步通信的收发双方必须使用相同的同步字符。 作为应用,异步通信常用于传输信息量不大、传输速率比较低的场合。在信息量很大、传输速率要求较高的场合,常采用同步通信。 3.10.2 MCS-51单片机用于串行通信的寄存器 MCS-51单片机的串行接口是一个可编程的全双工串行通信接口。它可用作异步通信方式,与串行传送信息的外部设备相连接,或用于通信标准异步通信协议进行全双工的MCS-51多机系统,也可以通过同步方式,使用TTL或CMOS移位寄存器来扩充I/O口。 MCS-51单片机通过引脚RXD(P3.0,串行数据接收端)和引脚TXD(P3.1,串行数据发送端)与外界通信。SBUF是串行口缓冲寄存器,包括发送寄存器和接收寄存器。它们有相同名字和地址空间,但不会出现冲突,因为它们两个一个只能被CPU读出数据,一个只能被CPU写入数据。 1、串行口控制寄存器SCON 串行口控制寄存器SCON用于定义串行口的工作方式及实施接收和发送控制。其各位定义如表3.9所示,其中串行口工作方式如表3.10所示。 名称 D7 D6 D5 D4 D3 D2 D1 D0 SCON SM0 SM1 SM2 REN TB8 RB8 TI RI SM0 0 0 1 1 0 1 0 1 表3.9 串行口控制寄存器 SM1 工作方式 0 1 2 3 功能描述 8位移位寄存器 10位UART 11位UART 11位UART 波特率 Fosc/12 可变 Fosc/64或Fosc/32 可变 表3.10 串行口工作方式 其中: Fosc:晶振频率。 SM2:多机通信控制位。在方式0中,SM2一定要等于0。在方式1中,当SM2为1时则只有接收到有效停止位时,RI才置1。在方式2或3中,当SM2为1且接收到的第9位数据RB8为0时,RI才置1。 REN:接收允许控制位。由软件置位以允许接收,又由软件清零来禁止接收。 TB8:要发送数据的第9位。在方式2或3中,要发送的第9位数据,根据需要由软件置1或清零。例如,可约定作为奇偶校验位,或在多机通信中作为区别地址帧或数据帧的标志位。 中国科学技术大学业余无线电协会 39 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 RB8:接收到的数据的第9位。在方式0中不使用RB8。在方式1中,若SM2为0,RB8为接收到的停止位。在方式2或3中,RB8为到的第9位数据。 TI:发送中断标志。在方式0中,第8位发送结束时,由硬件置位。在其它方式的发送停止位前,由硬件置位。TI置位既表示一帧信息发送结束,同时也向CPU申请中断。可根据需要,用软件查询的方法获得数据已发送完毕的信息,或用中断的方式来发送下一个数据。TI必须用软件清零。 RI:接收中断标志位。在方式0中,当接收完第8位数据后,由硬件置位。在其它方式中,在接收到停止位的中间时刻由硬件置位(例外情况见对SM2的说明)。RI表示一帧数据接收完毕,可用查询的方法获知或者用中断的方法获知。RI也必须用软件清零。 2、特殊功能寄存器PCON 特殊功能寄存器PCON是为了在CHMOS的80C51单片机上实现电源控制而附加的。其中最高位是SMOD,用于选定串行口的波特率,详见后面说明。 3.10.3 MCS-51单片机串行口的工作方式 MCS-51单片机的全双工串行口可编程为4种工作方式,以下分别介绍。 1、方式0 方式0为移位寄存器输入/输出方式,也称为同步方式。可外接移位寄存器以扩展I/O口,也可以外接同步输入/输出设备。8位串行数据从RXD输入或输出,TXD用来输出同步脉冲。 数据发送:串行数据从RXD引脚输出,TXD引脚输出移位脉冲。CPU将数据写入发送寄存器时,立即启动发送,将8位数据以Fosc/12的固定波特率从RXD输出,低位在前,高位在后。发送完一帧数据后,发送中断标志TI由硬件置位。 数据接收:当串行口以方式0接收时,首先置位允许接收位REN。此时,RXD为串行数据输入端,TXD仍为同步脉冲移位脉冲输出端。当RI=0和REN=1同时满足时,开始接收。当接收到第8位数据时,将数据移入接收寄存器,并由硬件置位RI。 2、方式1 方式1为波特率可变的10位异步通信接口方式,是标准的异步通信方式。发送或接收一帧信息,包括1个起始位0,8个数据位和1个停止位1。 数据发送:当CPU执行一条指令将数据写入发送缓冲SBUF时,就启动发送。串行数据从TXD引脚输出,发送完一帧数据后,由硬件置位TI。 数据接收:在REN=1时,串行口采样RXD引脚,当采样到有1至0的跳变时,确认是开始位0,就开始接收一帧数据。只有当RI=0且停止位为1或者SM2=0时,停止位在进行RB8,8位数据才能进入接收寄存器,并由硬件置位中断标志RI;否则信息丢失。所以在方式1接收时,应先用软件对RI和SM2标志清零。 3、方式2 方式2为固定波特率的11位UART方式。它比方式1增加了一位可编程为1或0的第9位数据。 数据发送:发送的串行数据由TXD端输出,一帧信息为11位,附加的第9位来自SCON寄存器的TB8位,用软件置位或复位。它可作为多机通信中地址/数据信息的标志位,也可以作为数据的奇偶校验位。当CPU执行一条数据写入SBUF的指令时,就启动发送器发送。发送一帧信息后,置位中断标志TI。 数据接收:在REN=1时,串行口采样RXD引脚,当采样到有1至0的跳变时,确认是开始40中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 位0,就开始接收一帧数据。在接收到附加的第9位后,扫RI=0或者SM2=0时,第9位数据才进入RB8,8位数据才能进入接收寄存器,并由硬件置位中断标志RI;否则信息丢失,且不置位RI。再过一位的时间后,不管上述条件是否满足,接收电路即行复位,并重新检测RXD上从1到0的跳变。 4、方式3 方式3为波特率可变的11位UART方式。除波特率外,其余与方式2相同。 3.10.4 串行口的波特率选择 在串行通信中,收发双方的数据传送速率即波特率要有一定的约定。在MCS-51串行口的4种工作方式中,方式0和方式2的波特率是固定的,而方式1和方式3的波特率是可变的,由定时器T1的溢出率控制。 1、方式0 方式0的波特率固定为晶振频率的1/12。 2、方式2 方式2的波特率由PCON中的选择位PCON.7即SMOD来决定,可由下式表示: 波特率=2SMOD×Fosc/64 也就是当SMOD=1时,波特率为1/32Fosc,当SMOD=0时,波特率为1/64Fosc。 3、方式1和方式3 定时器T1作为波特率发生器,其波特率计算公式如下: 波特率=2SMOD×定时器T1溢出率/32 T1溢出率=T1计数率/产生溢出所需的周期数 式中T1计数率取决于它工作在定时器状态还是计数器状态。当工作于定时器状态时,T1计数率为Fosc/12;当工作于计数器状态时,T1计数率为外部输入频率,此输入频率应小于Fosc/24。产生溢出所需周期与定时器T1的工作方式、T1的预置值有关。 定时器T1工作于方式0:溢出所需周期数=8192-x 定时器T1工作于方式1:溢出所需周期数=65536-x 定时器T1工作于方式2:溢出所需周期数=256-x 因为方式2为自动重装入初值的8位定时器/计数器模式,所以用它来做波特率发生器最恰当。当时钟频率选用22.1184MHz时,很容易获得标准的波特率。表3.11列出了定时器T1工作于方式2时常用的波特率及初值。 常用波特率 38400 19200 9600 4800 2400 Fosc(MHz) 22.1184 22.1184 22.1184 22.1184 22.1184 SMOD TH1的初值 1 0 0 0 0 0xFD 0xFD 0xFA 0xF4 0xE8 表3.11 常用的波特率及T1初值 中国科学技术大学业余无线电协会 41 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 3.10.5 串行通信编程示例 下面是串行通信的一个程序,Easy 51 Kit Pro通过DB9串口和电脑串口相接,电脑串口发送数据,单片机接收后发回电脑串口。运行这个程序前,要把Easy 51 Kit Pro的最小系统板中标注为“RX”和“TX”的两个跳线分别接上跳线帽。程序比较简单,这里不再详细分析了,程序中已附有注释。 【程序8】程序清单如下: //晶振频率22.1184MHz #include #define ENABLE_INT EA=1 #define DISABLE_INT EA=0 void inituart(void) //串口初始化 { SCON = 0x50; //方式1, 8位数据位,允许接收 TMOD |= 0x20; //设置TIMER1: timer 1,方式2 PCON |= 0x80; //SMOD=1 TH1 = 0xfd; // TH1: reload value for 38400 baud @ 22.1184MHz TR1 = 1; // TIMER1开始计数 ES = 1; //允许串行口中断 PS = 1; //设置串行口中断为高优先级中断 ENABLE_INT; } void com_isr() interrupt 4 //串行口中断服务程序 { unsigned char temp; bit bTI_Backup; DISABLE_INT; //屏蔽所有中断 if(RI) //如果是接收到数据引起中断 { RI = 0; // RI清零 bTI_Backup = TI; //备份当前TI的值 TI = 0; //TI清零,以发送数据 temp = SBUF; //读接收到的数据到temp SBUF = temp; //把temp的数据发送出去 while(!TI); //等待至发送成功 TI = bTI_Backup; //恢复原来TI的值 } if(TI) //如果是发送完数据引起的中断,则除了TI清零外什么也不干 { TI = 0; //TI清零,以发送数据 } 中国科学技术大学业余无线电协会 42 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 ENABLE_INT; //开放中断 } void main() { inituart(); while(1) { ; } } 3.11 定时器2 除了定时器0和定时器1外,C52和S52单片机还有另外一个定时器——定时器2。下面我们介绍一下定时器2的用法。 TF2 7 EXF2 6 RCLK 5 TCLK 4 EXEN2 3 TR2 2 C/T2 1 CP/RL20 表3.12 T2CON寄存器 定时器2是一个16位的定时器/计数器。T2CON中的C/T2位决定定时器2是用作定时器还是计数器。定时器2有三种工作模式:捕捉模式、自动重载(增或减计数)模式和波特率发生器模式。定时器2的工作模式由T2CON决定,详见表3.14。定时器2有两个8位的寄存器TH2和TL2。在定时器功能中,每个机器周期TL2都会增1。由于一个机器周期由12个振荡周期组成,所以定时器2的计数速率为振荡频率的1/12。 与定时器2有关的寄存器为T2CON和T2MOD。 T2CON寄存器的组成见表3.12。下面是详细说明。 TF2:定时器2溢出标志位。定时器2溢出时置位。必须由软件清零。当RCLK=1或TCLK=1时,TF2不会置位。 EXF2:定时器2外部标志位。当EXEN2=1,且T2EX引脚发生电平的负跳变引起捕捉或重载时被置位。当允许定时器2中断时,EXF2=1会使CPU跳转到定时器2的中断处理函数中。EXF2必须由软件清零。在增/减计数模式下(DCEN=1),EXF2不会引起中断。 RCLK:接收时钟使能。置位时,在串行通信模式1和模式3中,串口将使用定时器2的溢出脉冲作为接收时钟。RCLK=0将使串口使用定时器1的溢出脉冲作为接收时钟。 TCLK:发送时钟使能。置位时,在串行通信模式1和模式3中,串口将使用定时器2的溢出脉冲作为发送时钟。TCLK=0将使串口使用定时器1的溢出脉冲作为发送时钟。 EXEN2:定时器2外部信号使能。置位时,如果定时器2并没有用作串口的波特率发生器,T2EX引脚的电平负跳变将发生捕捉或重载。 TR2:定时器2启动/停止控制位。TR2=1将启动定时器2。 C/T2:定时器/计数器选择位。C/T2=0时用作定时器,C/T2=1时用作计数器(下降沿触发)。 CP/RL2:捕捉/重载选择位。CP/RL2=1时,如果EXEN2=1,将在T2EX引脚发生中国科学技术大学业余无线电协会 43 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 电平负跳变时产生捕捉事件。CP/RL2=0时,当定时器2溢出或T2EX引脚发生电平负跳变时(如果EXEN2=1)发生重载。当RCLK或TCLK=1时,这一位将被忽略,定时器2溢出时会被强行自动重载。 - 7 - 6 - 5 - 4 - 3 - 2 T2OE 1 DCEN 0 表3.13 T2MOD寄存器 T2MOD寄存器的组成见表3.13。下面是详细说明。 T2OE:定时器2输出使能位。 DCEN:置位时,定时器2可设置成增/减计数器。 RCLK+TCLK 0 0 1 X CP/RL2 0 1 X X TR2 1 1 1 模式 16位自动重载 16位捕捉 波特率发生器 (关闭) 0 表3.14 定时器2的工作模式 3.11.1 捕捉模式 在捕捉模式中,要设置T2CON中的EXEN2位。如果EXEN2=0,那么定时器2是一个16位的定时器或计数器,当它溢出时,T2CON中的TF2置位,并可产生中断。如果EXEN2=1,那么定时器2的操作是一样的,但单片机的T2EX引脚的从1到0的电平变化会把当前TH2和TL2的值分别捕捉到RCAP2H和RCAP2L中。此外,T2EX引脚的电平变化会使T2CON中的EXF2位置位。EXF2和TF2一样,都可以产生中断。 3.11.2 自动重载(增或减计数)模式 在16位自动重载模式中,定时器2可以设置成增计数或减计数。这一功能由T2MOD中的DCEN(减计数使能)位设置(见表3.13)。当单片机复位时,DCEN被置0,定时器为增计数方式。当DCEN被置位时,定时器2可以为增计数或减计数,这要取决于T2EX引脚的值。 当DCEN=0时,定时器2处于增计数方式。在这种模式下,如果EXEN2=0,那么定时器2增计数至0xFFFF,当定时器溢出时,把TF2位置位。定时器溢出会使定时器的寄存器值自动从RCAP2H和RCAP2L自动重载。RCAP2H和RCAP2L的值由软件预设。如果EXEN2=1,那么定时器寄存器的重载除由定时器溢出触发外,还可以被T2EX引脚的从1到0的电平变化触发。这一电平变化还会把EXF2位置位。如果中断被允许,TF2和EXF2的置位都可以产生中断。 把DCEN位置1使定时器2可以增计数或减计数。在这种模式下,T2EX引脚的电平控制计数的方向。当T2EX引脚为高电平时,定时器2增计数。当定时器计数至0xFFFF溢出并把TF2置位,同时RCAP2H和RCAP2L的值会被分别自动重载至定时器的寄存器TH2和TL2。 当T2EX引脚为低电平时,定时器2减计数。当TH2和TL2分别与RCAP2H和RCAP2L相等时,定时器下溢出,TF2置位,0xFFFF被重载至定时器的寄存器中。 在这种模式下,EXF2位在定时器2上溢出或下溢出时翻转,可以被用作定时器的第17位。这时EXF2并不作为中断的标志位。 中国科学技术大学业余无线电协会 44 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 3.11.3 波特率发生器模式 通过设置T2CON中的TCLK和(或)RCLK位,定时器2可用作波特率发生器。注意如果把定时器2仅用作发送或接收而把定时器1用作另一功能,那么发送和接收的波特率可以不一样。 波特率发生器模式与自动重载模式相似,TH2的溢出会引起定时器2的定时器值被由软件预设的RCAP2H和RCAP2L自动重载。 串行通信模式1和模式3中的波特率由定时器2的溢出速率决定: 模式1和模式3的波特率=定时器2的溢出速率 16这时,定时器2可以被设置作定时器或计数器。在大多数应用(波特率发生器应用)中,。当定时器2被设置作波特率发生器时,它的定定时器2都被设置作定时器(CP/T2=0)时器操作是不一样的。通常,作为一个定时器,它每个机器周期增1(以振荡频率的1/12的速率增1)。但作为波特率发生器,定时器每2个振荡周期增1。波特率的计算公式如下: 模式1和模式3的波特率=振荡器频率 32×[65536-(RCAP2H,RCAP2L)]其中(RCAP2H,RCAP2L)是由RCAP2H和RCAP2L组成的16位无符号整数。 注意TH2的溢出并不会使TF2置位,也不会产生中断。另外,如果EXEN2被置位,T2EX引脚处的从1到0的电平变化会把EXF2置位,但不会引起(RCAP2H,RCAP2L)重载到(TH2,TL2)。因此,当定时器2被用作波特率发生器时,T2EX可以用作额外的外部中断。 还要注意的是,当定时器2在波特率发生器模式下运行(TR2=1)时,TH2和TL2不应被读写。RCAP2寄存器可以读,但不应被写入。在访问定时器2的寄存器或RCAP2寄存器前,应先把定时器停掉(把TR2清0)。 3.11.4 可编程时钟输出 P1.0可用作输出50%占空比的方波。这一引脚除了常规的I/O外,还有两个其它功能。它可以用作定时器2的外部时钟输入,也可以用作输出从61Hz到4MHz(振荡频率为16MHz时)的占空比为50%的方波。 要把定时器2设置成时钟发生器,要把C/T2清0和把T2OE(T2MOD.1)置1。TR2位(T2CON.2)用于开始或停止定时器。 时钟的输出频率取决于振荡器频率和定时器2的捕捉寄存器(RCAP2H和RCAP2L): 输出频率=振荡器频率 4×[65536−(RCAP2H,RCAP2L)]在时钟发生器模式下,定时器2的溢出不会产生中断。可以同时把定时器2用作波特率发生器和时钟发生器。但是,波特率和时钟频率不能分别确定,因为它们共用RCAP2H和RCAP2L。 下面的程序和程序4一样,实现数码管显示按秒跳变的功能。不同之处是这个程序用的是定时器2中断而不是定时器0中断实现。读者可以比较一下定时器2和定时器0/定时器1用法的异同。 45中国科学技术大学业余无线电协会 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 【程序9】程序清单如下: /*用T2中断实现数码管的按秒跳变*/ //晶振频率22.1184MHz #include #define TIMER2H 0x4c #define TIMER2L 0x00 #define TIMER2_RUN TR2=1 #define SECOND_OVERFLOW 40 #define SEG_PORT P0 #define DISPLAY_DIG1 P1&=0xf0;P1|=0x01 unsigned char g_CurrentDigit=0; //当前显示的数字 void timer() interrupt 5 { static unsigned char s_Count = 0; if(TF2) { TF2 = 0; } if(EXF2) { EXF2 = 0; } TIMER2_RUN; //定时器2运行,开始下一次计数 if(s_Count != SECOND_OVERFLOW) { //未到整秒,把sCount值加1 s_Count++; } else { //到整秒,s_Count归0,更新把当前显示的数字 s_Count = 0; if(g_CurrentDigit != 9) { g_CurrentDigit++; } else { g_CurrentDigit = 0; } } return; 中国科学技术大学业余无线电协会 46 51单片机C语言编程入门——Easy 51 Kit Pro配套学习资料 } void Initial(void) //初始化 { IE = 0xa0; //仅允许Time20中断 // TMOD = 0x01; //Timer0使用工作方式1(16位),定时器 RCAP2H = TIMER2H; //设置定时器初值 RCAP2L = TIMER2L; TH2 = TIMER2H; TL2 = TIMER2L; TIMER2_RUN; //定时器开始运行 DISPLAY_DIG1; } void main() { unsigned char code SEG_CODE[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; Initial(); while(1) { SEG_PORT = SEG_CODE[g_CurrentDigit]; //显示当前的数字 //当timer0溢出时,单片机响应timer0中断,调用timer函数, //每40次调用当前显示的数字加1 } } 在这个程序中,分别对RCAP2H/RCAP2L和TH2/TL2进行初始化。定时器2第一次运行时,定时器从(TH2,TL2)起进行增计数。当定时器2溢出时,RCAP2H/RCAP2L的值分别自动重新载入TH2/TL2,因此,在定时器2的中断服务函数中无需重新设置TH2/TL2的值。定时器2的中断除了可能由于TF2置位产生,也可能是由于EXF2置位产生。并且,这两个标志位不会因为进入中断服务函数而清除,所以在中断服务函数中添加了标志位清零的语句。 3.12 看门狗 看门狗是S5x系列单片机比C5x系列多出来的功能之一。看门狗可以在CPU死机时重启CPU。看门狗由一个14位的计数器和看门狗寄存器WDTRST组成。单片机复位后,看门狗是处于禁用状态的。要使能看门狗,就要连续向WDTRST寄存器写入0x1e和0xe1。当看门狗使能且振荡器工作时,看门狗计数器每个机器周期增1。使能看门狗后,除了复位(硬件复位或看门狗溢出复位)外没有办法禁用看门狗。当看门狗计数器溢出时,它会在RST引脚产生一个高电平脉冲,迫使单片机复位。 当看门狗使能后,程序必须不断地向WDTRST写0x1e和0xe1以避免看门狗溢出(通常称为“喂狗”)。看门狗的14位计数器在数到16383(0x3FFF)后溢出,这时单片机会复位。这意味着程序必须最多16383机器周期内喂一次狗。 下面我们重写3.8.2节的程序,在程序中加入了看门狗。在程序中,我们不仅在主函数的while(1)循环中加入了喂狗语句,还在Delay10ms()和Delay1ms()函数中加入了喂狗语句。47中国科学技术大学业余无线电协会
版权声明:本文标题:51单片机C语言编程入门(详讲版) 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.freenas.com.cn/jishu/1703278019h445251.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论