一、单片机介绍
1.1 单片机简述
- 整篇文章使用的单片机型号:STC89C52(兼容英特尔8051指令系统)
开发软件:keil 5
烧录软件:stc-isp - 单片机(Micro Controller Unit ,即MCU,微控制器),集成了中央处理器(CPU)、存储器(RAM、ROM)、并行I/O、串行I/O、定时器/计数器、中断系统、系统时钟电路以及系统总线,用于测控领域的单片微型计算机。
- STC89XX系列命名规则
例如:STC89C52 35I PDIP 40
表示:
该芯片工作电压:5.5V~3.8V
拥有ROM:8KB,RAM:512B
工作频率:可达到35MHz
工作温度:属于工业级
封装类型:PDIP,拥有40个引脚
1.2 单片机最小系统
51单片机的最小系统由晶振电路、复位电路和电源电路组成
- 晶振电路:由于单片机正常工作需要一个时钟,因此就需要在其晶振引脚上外接晶振 ,一般情况下选择 12M(适合计算延时时间)或者是 11.0592M(适合串口通信)。若直接将此晶振接入单片机晶振引脚,会发现系统 工作不稳定,这是因为晶振起振的一瞬间会产生一些电感,为了消除这个电感所 带来的干扰,可以在此晶振两端分别加上一个电容,电容的选取需要无极性的。只有保证晶振电路稳定,单片机才能继续工作。
- 复位电路:利用 RC 充放电功能,电源已开启,此时电容充电,最开始相当于短路,RST最初为VCC,然后电容开始慢慢充电,直到充电完成,由于电容隔直,相当于断路,此时 RST 被电阻拉低。
- 电源电路:STC89C52单片机的工作电压是 5.5V~3.3V 范围, 通常我们使用 5V 直流。将电源接入到各芯片电源引脚即可。
二、单片机内部的硬件结构
2.1 单片机内的硬件结构
下面对片内各部件做简单介绍:
- 中央处理器(CPU):8位的CPU,包括运算器和控制器
- 数据存储器(RAM):512B
- 程序存储器(ROM):用来存储程序,片内右8KB的Flash存储器
- 中断系统:具有8个中断源,4个优先级
- 定时器/计数器:3个16位的定时器和计数器
- 4个8位的并行口:P0、P1、P2、P3
- 特殊功能寄存器(SFR):用于CPU对片内各种外设部件进行管理、控制和监视。实际上是片内各种外设部件的控制寄存器和状态寄存器,映射在片内RAM区的80H~FFH的地址区间内
- 1个看门狗定时器(Watch Dog Timer,WDT):当单片机受干扰而使程序陷入“死循环”或“跑飞”状态时,看门狗定时器可将单片机复位,从而使程序恢复正常运行
2.2 引脚功能
PDIP封装的引脚分布:
40 引脚我们 按其功能类别可分为四类:
①电源引脚。如 VCC、GND
②时钟引脚。如 XTAL1、XTAL2
③控制引脚。如 RST、PSEN(29号引脚)、ALE/PROG(30号引脚)、EA/Vpp(31号引脚)
④I/O 口引脚。如 P0、P1、P2、P3,4 组 8 位并行 I/O 口
2.2.1 电源引脚
- VCC(40引脚):接+5V电源
- GND(20引脚):接数字地
2.2.2 时钟引脚
- XTAL1(19号引脚):片内振荡器的反相放大器和外部时钟源的输入端。当使用片内振荡器时,引脚处外接石英晶振和振荡电容;当采用外部独立的时钟源时,本引脚接外部时钟源的信号
- XTAL2(18号引脚):片内振荡器的反相放大器的输出端。当使用片内振荡器时,引脚处外接石英晶振和振荡电容;当采用外部独立的时钟源时,本引脚悬空,实际将XTAL1输入的时钟进行输出
2.2.3 控制引脚
- RST(9号引脚)
复位引脚,高电平有效。当输入连续两个机器周期以上高电平时为有效。用来完成单片机的复位初始化操作,即单片机从头开始执行程序 - EA/Vpp(31引脚)
EA为该引脚的第一功能:外部程序存储器访问允许控制端
①EA 接高电平时,单片机读取内部程序存储器。当有扩展外部 ROM 时,读取完内部 ROM 后自动读取外部 ROM
②EA 接低电平时,单片机只接读取外部 ROM中的内容
Vpp为该引脚的第二功能:在对片内Flash进行编程时,该引脚接入编程电压 - ALE/PROG(30引脚)
ALE为该引脚的第一功能:为CPU访问外部程序存储器或外部数据存储器提供低8位地址的锁存控制信号,ALE 信号负跳变(即由正变负),将P0口发出的低8位地址锁存在片外的地址锁存器中
当没有扩展外部 RAM 时,ALE 会以 1/6 振荡周期的固定频率输出正脉冲信号,因此可以作为外部时钟,或作为外部定时脉冲使用。
当外部扩展有RAM时,每访问外部RAM时,要丢失一个ALE脉冲,频率并不是准确的1/6
PROG为该引脚的==第二功能::在对片内Flash进行编程时,该引脚作为编程脉冲的输入端
注:单片机的内部 有程序存储器(ROM),它的作用是用来存放用户需要执行的程序,那么我们怎 样才能将写好的程序存入这个 ROM 中呢?实际上,我们是通过编程脉冲输入才写 进去的,这个脉冲的输入端口就是 PROG。现在绝大多数单片机都已经不需要编 程脉冲引脚往内部写程序了,比如我们使用的 STC 单片机,它可以直接通过串口 往里面写程序
- PSEN(29号引脚)
片外程序存储器的读选通信号(允许输出控制端),低电平有效
2.2.4 I/O口(GPIO,通用输入输出端口)
2.2.4.1 P0口
首先,先简单了解一下各部分
- 输入缓冲器
在 P0 口中,有两个三态的缓冲器,三态门有三个状态(输出端:高电平、低电平、高阻态),上面一个是读锁存器的缓冲器,也就是说,要读取 D 锁存器输出端 Q 的数据,那就得使读锁存器的这个缓冲器的三态控制端(上 图中标号为‘读锁存器’端)有效。下面一个是读引脚的缓冲器,要读取 P0.x 引脚上的数据,也要使标号为‘读引脚’的这个三态缓冲器的控制端有效,引脚 上的数据才会传输到我们单片机的内部数据总线上 - 锁存器
一个触发器可以保存一位的二进制数(即具有保持功能)。D锁存器的D 端是数据输入端,CP(CLK)是控制端(也就是时序控制信号输入端),Q 是输出端,Q 非是反向输出端。 对于 D锁存器来讲,当 D 输入端有一个输入信号,如果CLK没有时序信号,这时输入端 D 的数据是无法传输到输出端 Q 及反向输出端 Q 非的。如果时序控制端 CLK输入时序脉冲,这时 D 端输入的数据就会传输到 Q 及 Q 非端。数据传送过来后,当 CP 时序控制端的时序信号消失了,这时,输出端还会保持着上次输入端 D 的数据(即把上次的数据锁存起来了)。如果下一个时序控制脉冲信号来了,这时 D 端的数据才再次传送到 Q 端,从而改变 Q 端的状态。 - 多路开关
当多路开关与下面接通时, P0 口是作为普通的 I/O 口使用的,当多路开关是与上面接通时,P0 口是作为地址/数据总线使用的 - 场效应管
P0 口的输出是由两个 MOS 管组成的推拉式结构,也就是 说,这两个 MOS 管一次只能导通一个,当 V1 导通时,V2 就截止,当 V2 导通时, V1 截止
P0口的工作原理
(1)作为I/O端口使用
-
P0 口作为 通用I/O输出口使用时,多路开关的控制信号为 0(低电平),与门输出为0,V1 管就截止;
多路开关是与锁存器的 Q 非端相接的(即 P0 口作为 I/O 口线使用)。来自CPU的“写”脉冲家在CLK(CP)端,内部总线上的数据写入D锁存器并由引脚P0.x输出。当D锁存器为1时,Q非为0,V2截止,输出为漏极开路输出,此时必须外接上拉电阻才能有高电平输出;当D锁存器为0时,Q非为1,V2导通,输出为低电平 -
P0口作为通用I/O输入口时,有两种方式:读锁存器和读引脚
当CPU发出“读锁存器”指令时,锁存器的状态由Q端经过上方的输入缓冲器进入内部总线;
当CPU发出“读引脚”指令时,锁存器的输出状态为1(Q非端为0),使V2截止,端口线已处于高阻态,引脚的状态经过下方的输入缓冲器进入内部总线;
(2)作为系统的地址/数据总线
当P0口作为地址/数据总线使用,“控制”信号为1,多路开关接向上面
当“地址/数据”信息为1,非门输出为0,与门输出为1,V1导通,V2截止,P0.x引脚输出1
当“地址/数据”信息为0,非门输出为1,与门输出为0,V2导通,V1截止,P0.x引脚输出0
可见:P0.x引脚的输出状态随着“地址/数据”控制信号的变化而变化
当P0口作为数据输入时,仅从外部存储器读入信息,对应的控制信号为0,多路开关接到Q非端。由于P0口作为“地址/数据”复用方式访问外部存储器时,CPU自动向P0口写入FFH,使V2截止;同时控制信号为0,V1也截止,从而保证数据信息的高阻抗输入,直接由P0.x引脚通过下方的输入缓冲器进入内部总线
总结:
- 当作为地址/数据总线口使用,他是一个真正的双向口(具有3种状态的端口)用作与外部扩展的存储器连接,输出低8位地址和输出/输入8位数据
- 当P0口用作通用I/O口使用时,需要在引脚外接上拉电阻,没有高阻态,因此它是一个准双向口
- P0口“读引脚”输入时,P0端口由于输出有三态功能,输入前,端口线已处于高阻态,无需向锁存器内写入
2.2.4.2 P1口
P1口的工作原理:
只能作为I/O端口使用
-
P0 口作为 通用I/O输出口使用时,当D锁存器为1时,Q非为0,V2截止,P1.x输出高电平;当D锁存器为0时,Q非为1,V2导通,P1.x输出为低电平
-
P0口作为通用I/O输入口时,有两种方式:读锁存器和读引脚
当CPU发出“读锁存器”指令时,锁存器的状态由Q端经过上方的输入缓冲器进入内部总线;
当CPU发出“读引脚”指令时,先向锁存器写入1(Q非端为0),使V2截止,引脚的状态经过下方的输入缓冲器进入内部总线;
总结:
- P1口内部有上拉电阻,没有高阻抗输入状态,因此它是一个准双向口。作为输出口时,不需要接上拉电阻
- P1口“读引脚”输入时,必须向锁存器内写入1
2.2.4.3 P2口
P2口的工作原理
(1)作为I/O端口使用
-
P2 口作为 通用I/O输出口使用时,多路开关的控制信号为 0(低电平),多路开关是与锁存器的 Q 端相接的。当D锁存器为1时,Q为1,V2截止,输出高电平;当D锁存器为0时,Q为0,V2导通,输出为低电平
-
P2口作为通用I/O输入口时,有两种方式:读锁存器和读引脚
当CPU发出“读锁存器”指令时,锁存器的状态由Q端经过上方的输入缓冲器进入内部总线;
当CPU发出“读引脚”指令时,先向锁存器写入1(Q端为1),使V2截止,引脚的状态经过下方的输入缓冲器进入内部总线;
(2)作为系统的地址总线
当P0口作为地址/数据总线使用,“控制”信号为1,多路开关接向上面
当“地址”信息为0,V2导通,P2.x引脚输出0
当“地址”信息为1,V2截止,P0.x引脚输出1
总结:
- 当作为地址总线口使用,用作与外部扩展的存储器连接,输出高8位地址,输出锁存器的内容保持不变
- P2口内部有上拉电阻,没有高阻抗输入状态,因此它是一个准双向口。作为输出口时,不需要接上拉电阻
- P2口“读引脚”输入时,必须向锁存器内写入1
2.2.4.4 P3口
P3口的工作原理:
(1)第一功能:通用I/O口
- 通用I/O口输出时,“第二功能输出”端应保持高电平。当CPU输出1时,Q=1,V2截止,P3.x输出1;当CPU输出0时,Q=0,V2导通,P3.x输出0
- 通用I/O输入时:
①“读引脚”:锁存器和“第二功能输出”端均置1,V2截止,引脚信息通过下方两个输入缓冲器进入内部总线
②“读锁存器”:Q端的信息经过上方的输入缓冲器进入到内部总线
()第二输入/输出功能 - 第二输出功能,锁存器置1,当“第二功能输出”端输出为1,V2截止,引脚输出为1;当“第二功能输出”端输出为0,V2导通,引脚输出为0
- 第二输入功能,锁存器和“第二功能输出”端均置1,V2截止,引脚信息通过下方最右边的输入缓冲器获得
总结:
- P3口内部有上拉电阻,不存在高阻抗输入状态,故为准双向口
- P3口“读引脚”输入时,必须向锁存器内写入1
- 第一功能和第二功能由单片机执行的指令控制自动切换,用户不需要进行任何设置
2.3 CPU
- CPU由运算器和控制器组成
(1)运算器:
(2)控制器:
2.4 存储器
STC89C52片内拥有8K程序存储器和512B数据存储器
- 程序存储器
程序存储器,又称为只读存储器(Read Only Memory,ROM),掉电不丢失,用于存放用户程序、数据和表格等信息.STC89C52的片内程序存储器为8KB的Flash存储器,地址范围为:0000H~1FFFH
当若EA非引脚接高电平,单片机首先从片内程序存储器的0000H单元开始执行程序,当PC的内容超过1FFFH时系统自动转到片外程序存储器中取指令
中断服务程序的入口地址,又称中断向量,也位于程序存储器单元。在程序存储器中,每个中断都有一个固定的入口地址,当中断发生并得到响应后,单片机就会自动跳转到相应的中断入口地址去执行程序。
由于相邻中断入口地址的间隔区间(8个字节)有限,一般情况下无法保存完整的中断服务程序,因此,一般在中断响应的地址区域存放一条无条件转移指令,指向真正存放中断服务程序的空间去执行。
- 数据存储器:
STC89C52单片机的RAM分两部分,一部分是内部RAM(256字节),一部分是内部扩展RAM(256字节)
256字节的RAM又分低128字节内部RAM(0x00–0x7F)和高128字节内部RAM(0x80–0xFF);
数据存储器,又称为随机访问存储器(Random Access Memory,RAM),掉电丢失,通常存储程序中的变量
2.5 特殊功能寄存器(SFR)
特殊功能寄存器(SFR)的单元地址映射在片内RAM区的80H~FFH区域中
- 特殊功能寄存器名称及地址映像如下表所示:
凡是可以进行位寻址的SFR,其字节地址的末位只能是0H或8H
- 堆栈指针:
堆栈指针是一个8位专用寄存器,它指示出堆栈顶部在内部RAM块中的位置。系统复位后,SP初始化位07H,使得堆栈事实上由08H单元开始,考虑08H~ 1FH单元分别属于工作寄存器组1~3,若在程序设计中用到这些区,则最好把SP值改变为60H或更大的值为宜
堆栈指针主要是为了子程序调用和中断操作而设立的,具体功能:保护断点和现场保护(暂存数据和地址)
①保护断点:无论是子程序调用或中断服务子程序调用,主程序都会被“打断”。需要把主程序的断点在堆栈中保护起来,为程序的正确返回做准备
②现场保护:在执行子程序或中断服务子程序,可能会用到一些寄存器,这就会破坏主程序运行时这些寄存器的原有内容。所在在执行子程序或中断服务程序之前,先把有关寄存器的内容保存起来,送入堆栈
- 寄存器B:
在执行乘、除操作时使用寄存器B,若不执行,可当作一个普通寄存器使用
乘法:两个乘数分别在A、B中,执行乘法指令后,乘积存放在BA寄存器中。高8位放在B中,低8位放在A中
除法:被除数取自A,除数取自B,商存放在A中,余数存放在B中 - 数据指针DPTR
便于访问数据存储器
数据指针可以作为一个16位的寄存器使用,也可以作为两个8位的寄存器使用 - 看门狗定时器WDT:
2.6 时钟周期、机械周期和指令周期
2.7 复位
- 当给单片机的RST引脚加入大于两个机械周期的高电平,即可复位
- 从上图可以看出,复位后,SP=07H,4个I/O端口均为高电平。在某些场景下要注意引脚高电平的影响
2.8 低功耗节电模式
两种低功耗节电模式:空闲模式和掉电保持模式
- 空闲模式
- 进入:IDL置1
CPU的时钟信号关断,虽然振荡器仍然运行,但CPU进入空闲状态。此时,片内所有外围电路(中断系统、串行口和定时器)仍继续工作,内部RAM和SFR中的内容均保持进入空闲模式前的状态 - 退出:响应中断和硬件复位
响应中断:当任何一个允许的中断请求被响应时,IDL位被片内硬件自动清零
硬件复位:通过复位,改变IDL的值,从而达到退出空闲模式的目的
- 掉电模式
- 进入:PD置1
振荡器停止运行,没有时钟源。此时,片内所有部件均停止工作,外部中断继续工作,但内部RAM和SFR中的内容均保持进入掉电模式前的状态 - 退出:响应中断和硬件复位
响应中断:当任何一个允许的中断请求被响应时,IPD位被片内硬件自动清零
硬件复位:通过复位,重新初始化SFR,但不改变片内RAM的内容
三、发光二极管 LED
3.1 LED介绍
- 发光二极管,简称为LED,是一种常用的发光器件,通过电子与空穴复合释放能量发光,它在照明领域应用广泛。
发光二极管可高效地将电能转化为光能,在现代社会具有广泛的用途,如照明、平板显示、医疗器件等。 - 二极管具有:单向导电性
所以说,驱动LED点亮,必须要区分正负极,上述示意图中最右边可以很好区分 - 发光二极管的工作电流为:1~5mA,发光二极管的工作电流越大,显示亮度也就越高。为保证二极管正常工作,通常会串联限流电阻
3.2 点亮第一个LED
- 从上述原理图可以看到,LED模块连接在单片机的P2口上,且连接方式为共阳极,所以只需要把某一LED灯对应的引脚置为低电平,即可驱动LED发光
- 同时LED串联着1K的电阻,其作用:限流,防止LED因电流过大被烧毁,而电阻又是以排阻(4个1组)的形式存在,但是在开发板上标注的可能不是1K,而是102(10*102)(前面是有效数字,最后一位是倍率,两着相乘即可)
- 单片机的I/O端口是单片机与外设进行信息交换的桥梁,因而可通过读取I/O端口的状态来了解外设状态,也可向I/O端口发送命令或数据来控制外设
#include <REGX52.H>
//P2寄存器的字节地址0xA0,可以进行位寻址
//将P2.0引脚定义为LED1
sbit LED1 = P2^0;
void main()
{
while(1)
{
//P2 = 0xFE; //1111 1110
LED1 = 0; //P2_0=0
}
}
为什么要在主函数中加入死循环呢?
答:其任务是不间断地执行特定的功能或任务,例如控制外部设备 、采集数据、处理输入等。 通过将主 程序设计为死循环,可以确保程序在完成一轮任务后立即开始下一轮,不会自行终止。目的是为了让程序一直保持在需要的运行的情况下
不加while(1)的情况下,当main()函数执行完毕之后,会继续跳转到main()函数的首行继续执行。如果程序指针PC在程序结束到跳转到main()函数之前,会读取不确定的指令,从而出现意想不到的结果
其次,在嵌入式C程序中,main()函数是不能返回的
我们在单片机中使用while(1),大部分还是为了防止程序跑飞,因为很多时候执行完某段程序后单片机的程序指针PC(就是程序指针)并不会停止,仍然会继续从ROM中读取指令并执行,这样一来可能会出现程序跑飞的情况,进而出现不确定的结果,我们加个while(1)就能让程序在执行完后在原地循环,相当于停在原地,防止跑飞。
while(1);
意义:这是一个死循环,代码不再向下执行。
3.3 LED闪烁
- 现在,点亮一个LED已经学会,那么实现LED闪烁相信大家也有头绪了!
- 基本思想:先点亮LED,让其延时一段时间;之后熄灭LED,延时一段时间,重复:点亮——延时——熄灭——延时
- 延时函数
这里所展示的延时函数,并不是很精确,基本都是采用循环实现,等到后面学习定时器,就可以实现精准延时
(1)STC-ISP中软件延时器生成的延时函数(修改版)
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
注:此函数是在STC-ISP中生成的延时1ms的函数,然后对其增加一个参数,进行修改而来
(2)利用循环语句
void Delay(uint i)
{
uchar t;
while(i--)
{
for(t = 0;t < 120;t++);
}
}
注:上述两种延时函数,均不精确
- 完整代码
#include <REGX52.H>
#include "intrins.h"
typedef unsigned int uint;
typedef unsigned char uchar;
sbit LED1 = P2^0;
/*
void Delay(uint i)
{
uchar t;
while(i--)
{
for(t = 0;t < 120;t++);
}
}
*/
/*
函数功能:延时
参数类型:无符号的整型
参数说明:xms:延时的毫秒
*/
void Delay(uint xms) //@12.000MHz
{
uchar i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
void main()
{
while(1)
{
//P2 = 0xFE; //1111 1110
LED1 = 0; //P2_0=0
Delay(500);
LED1 = 1; //P2_0=1
Delay(500);
}
}
3.4 流水灯
根据前两个知识点的学习,所谓流水灯,即需要8个数码管在同一时间只亮一个,之后延时;以固定方向移动,使相邻的LED点亮,之后延时
- 暴力解法
将8位数码管一次,每一个LED点亮是,对应的引脚电平,使用十六进制表示出来
#include <REGX52.H>
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 12;
j = 169;
do
{
while (--j);
} while (--i);
}
}
void main()
{
while(1)
{
P2=0xFE;
Delay(100);
P2=0xFD;
Delay(100);
P2=0xFB;
Delay(10);
P2=0xF7;
Delay(100);
P2=0xEF;
Delay(100);
P2=0xDF;
Delay(100);
P2=0xBF;
Delay(100);
P2=0x7F;
Delay(100);
}
}
- 利用库函数
#include <REGX52.H>
#include "intrins.h"
#define LED_PORT P2
typedef unsigned int uint;
typedef unsigned char uchar;
/*
函数功能:延时
参数类型:无符号的整型
参数说明:xms:延时的毫秒
*/
void Delay(uint xms) //@12.000MHz
{
uchar i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
LED_PORT = 0xFE; //1111 1110
while(1)
{
Delay(500);
LED_PORT = _crol_(LED_PORT,1);
}
}
- 引用循环左移_crol_()、循环右移_cror_(),需要添加头文件#include “intrins.h”
- _crol_(P2,1),(unsigned char _crol_ (unsigned char, unsigned char)):第一个参数是循环左移的对象,第二个参数是循环左移的位数
- 循环左移函数:移出的高位补到低位
循环右移函数:移出的低位补到高位
- 利用移位运算符
#include <REGX52.H>
#define LED_PORT P2 //将P2定义为LED_PORT
typedef unsigned int uint;
typedef unsigned char uchar;
/*
函数功能:延时
参数类型:无符号的整型
参数说明:xms:延时的毫秒
*/
void Delay(uint xms) //@12.000MHz
{
uchar i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
uchar i,temp;
while(1)
{
temp = 0x01; //0000 0001
for(i = 0;i < 8;i++)
{
LED_PORT = ~temp;
Delay(500);
temp = temp<<1;
}
}
}
/*
void main()
{
uchar i;
while(1)
{
for(i = 0;i < 8;i++)
{
LED_PORT = ~(0x01<<i);
Delay(500);
}
}
}
*/
- 移位运算符:<<、>>
<<:左移运算符,左移n位,高位丢弃,低位补0
>>:右移运算符:右移n位,低位丢弃,高位补0(正数和无符号数)或高位补1(负数)
该程序中为什么要使用temp中间变量呢?
(1)假如不使用临时变量,由于移位运算符的特性,回出现同一时间多个LED点亮,达不到流水灯的效果
(2)假如temp = 0xFE;会发现和第一种情况如出一辙
利用移位运算符的特性,高位或低位丢失,另一端补0
将temp = 0x01;之后进行取反,送入P2口中,延时一段时间后,对临时变量进行左移1位(让1移位,达到移位的目的,补的0对1毫无影响,取反之后,不会出现多个0的情况)
四、按键
- 按键的任务
(1)判别是否有键按下?若有,进入下一步。
(2)识别哪一个键被按下,并得到相应的键值。
(3)根据键值,跳转到相应的处理程序入口。 - 非编码键盘:指按下按键,键号信息不能直接获取,要通过软件来获取。例如,独立按键和矩阵按键
编码键盘:指当按键按下后1,能直接得到按键键号。例如,装用的键盘接口芯片 - 独立按键的扫描方式:
①查询扫描:编程扫描方式是利用CPU完成其它工作的空余时间,调用键盘扫描子程序来响应键盘输入的请求。在执行键功能程序时,CPU不再响应键输入要求,直到CPU重新扫描键盘为止
② 定时扫描:定时扫描方式就是每隔一段时间对键盘扫描一次,它利用单片机内部的定时器产生一定时间(例如10ms)的定时,当定时时间到就产生定时中断。CPU响应中断后对键盘进行扫描,并在有按键按下时识别出该键,再执行该键的功能程序
③中断扫描:上述两种键盘扫描方式,无论是否按键,CPU都要定时扫描键盘,而单片机应用系统工作时,并非经常需要键盘输入,因此,CPU经常处于空扫描状态。
为提高CPU工作效率,可采用中断扫描工作方式。其工作过程如下:当无按键按下时,CPU处理自己的工作,当有按键按下时,产生中断请求,CPU响应中断,执行中断服务子程序,识别键号,并跳转该键的功能程序
4.1 独立按键
- 按键是一种电子开关,使用时轻轻按开关按钮就可使开关接通,当松开手时, 开关断开。实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开
通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,电压信号 如下图所示:
按键抖动会引起按键被误读多次,为了确保 CPU 对按键的一次闭合仅作一次处理,必须进行消抖。 按键消抖有两种方式,一种是硬件消抖,另一种是软件消抖。为了使电路更 加简单,通常采用软件消抖。
- 按键消抖
①先读取连接的I/O口电平(按下为低电平)
②如果为低电平,则进行延时20ms
③继续判断按键是否松手,如果对应的I/O口仍未低电平,则进行循环
④当按键松手时,即循环跳出,进行延时20ms
⑤之后进行相应的操作
#include <REGX52.H>
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
void main()
{
while(1)
{
if(0 == P3_1)
{
Delay(20);
while(0 == P3_1);
Delay(20);
}
}
}
当然,软件消抖也有一定的弊端,通过延时来达到目的,但在延时过程中,CPU不会进行其他工作,非常占耗CPU资源
- 硬件原理图
从上图中可以看出,4 个独立按键的控制管脚连接到 51 单片机的 P3.0-P3.3 脚上。其中 K1 连接在 P3.1 上,K2 连接在 P3.0 上,K3 连接在 P3.2 上,K4 连接 在 P3.3 上。4 个按键另一端全部连接在 GND,当按键按下后,对应 IO 口即为低电平。
4.1.1 独立按键控制LED亮灭
软件思想:先判断按键是否按下,如果按下,进行按键消抖;继续判断按键是否松开,如果松开,进行按键消抖;之后进行相应的操作
#include <REGX52.H>
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
void main()
{
while(1)
{
if(0 == P3_1)
{
Delay(20);
while(0 == P3_1);
Delay(20);
P2_0 = ~P2_0; //LED1进行点亮、熄灭交替进行
}
}
}
4.1.2 独立按键控制LED表示二进制
- 实现功能:以LED点亮的状态表示二进制
#include <REGX52.H>
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
void main()
{
unsigned char LED_Num = 0;
while(1)
{
if(0 == P3_1)
{
Delay(20); //消抖
while(0 == P3_1);
Delay(20); //消抖
//P2++; //LED熄灭的状态表示的二进制
//P2++˙ //0000 0000
//P2 = ~P2; //1111 1111
LED_Num++;
P2 = ~LED_Num;
}
}
}
分析:
(1)如果按键按下之后,操作语句仅仅为P2++,上电之后P2端口默认为高电平,P2++导致该端口的8个引脚全部为0,即LED全亮,每次加1,则对应二进制的LED为熄灭状态,恰恰与其相反
(2)如果按键按下之后,操作语句为P2++;P2=~P2,则会发现无论按键按下多少次,LED均处于熄灭状态。其原因为:P2端口上电后为高电平,加1之后为低电平,取反之后又回到高电平,一直在循环
(3)为想要达到既定功能,需要增加一个临时变量,对临时变量进行操作,将操作后的临时变量送入P2端口中,即可
当多个引脚作为输入端时,大多数情况下不直接操作作为输入端的引脚,而是增加临时变量,对其进行操作之后,再送入引脚中
4.1.3 独立按键控制LED移位
- 功能:初始LED1点亮,独立按键K1实现左移,独立按键K2实现右移
- 实现:利用一个中间变量,并控制该变量的大小,每次按下按键,变量值会改变,对0x01进行移位,实现LED的移位
#include <REGX52.H>
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
void main()
{
unsigned char LED_Num = 0;
P2 = ~0x01;
while(1)
{
if(0 == P3_1) //K1控制LED左移
{
Delay(20); //消抖
while(0 == P3_1);
Delay(20); //消抖
LED_Num++;
/*
0000 0001 0x01<<0
0000 0010 0x01<<1
0000 0100 0x01<<2
0000 1000 0x01<<3
0001 0000 0x01<<4
0010 0000 0x01<<5
0100 0000 0x01<<6
1000 0000 0x01<<7
*/
if(LED_Num > 7)
{
LED_Num = 0;
}
P2 = ~(0x01<<LED_Num);
}
if(0 == P3_0) //K2控制LED右移
{
Delay(20); //消抖
while(0 == P3_0);
Delay(20); //消抖
LED_Num--; //该变量为无符号变量,当0-1后,变为最大值
if(LED_Num > 7)
{
LED_Num = 7;
}
/*
if(0 == LED_Num) //LED1处于点亮状态,当再次按下K2,需要使LED8点亮
{
LED_Num = 7;
}
else //LED1处于熄灭状态,变量就正常递减
{
LED_Num--;
}
*/
P2 = ~(0x01<<LED_Num);
}
}
}
4.1.4 同一端口的高四位独立按键控制低四位LED
- 功能描述:假如,单片机的P1.4~P1.7接4个按键,P1.0~P1.3接4个LED(低电平点亮),将4个按键的状态反映在4个LED(顺序均是从小到大),开关闭合,对应的LED点亮
#include <REGX52.H>
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
void main()
{
unsigned char temp = 0;
while(1)
{
P1 = 0xFF; //P1口置为高电平
temp = P1 & 0xF0; //屏蔽低四位,获取高四位
P1 = (temp>>4); //将temp的数据右移4位,之后送入P1口中
}
}
- 将输入端口的引脚全部置1,利用与运算,屏蔽未用引脚口,之后进行其他操作,送入输入端
4.2 矩阵键盘
- 在键盘中按键数量较多的情况下,为了节省I/O口资源,通常将按键排列成矩阵的形式
- 硬件原理图
从上图中可以看出,4*4 矩阵按键引出的 8 根控制线直接连接到 51 单片机的 P1 口上。判断按键是否被按下:检测与该键对应的 I/O 口是否为低电平。由于矩阵键盘两端都与单片机 I/O 口相连,因此在检测时需编程通过单片机 I/O 口送出低电平。检测方法有多种,最常用的是行扫描和列扫描 - 扫描输入(放在循环体中):以列扫描为例,先给第一列为低电平,其余列为高电平,检测每一行的的电平值,若出现低电平,则确定按键按下;反之,仅将第二列置为低电平,检测每一行的的电平值……如此循环往复,直至整个键盘扫描完毕
4.2.1 利用LCD1602获取按键键码
- 利用列扫描法,将行线置为高电平,某一列线为低电平,依次检测各行线的电平状态,如检测到低电平,则行与列交叉点即为按键位置;若没有检测到低电平,将下一列线置为低电平,继续进行电平检测
main.c
#include <REGX52.H>
#include "MatrixKey.h"
#include "LCD1602.h"
void main()
{
unsigned char Key_Num;
LCD_Init();
LCD_ShowString(1,1,"KeyNum:");
while(1)
{
Key_Num = MatrixKey_Num();
if(Key_Num)
{
LCD_ShowNum(1,8,Key_Num,2);
}
}
}
MatrixKey.c
#include <REGX52.H>
#include "Delay.h"
/**
* @brief 列扫描,获取矩阵键码
* @param 无
* @retval Key_Num:按下的矩阵键码,范围:1~16
*/
unsigned char MatrixKey_Num()
{
unsigned char Key_Num = 0;
P1 = 0xFF; //P1端口全置为高电平
P1_3 = 0; //将第一列置为低电平
if(0 == P1_7)
{
Delay(20);
while(0 == P1_7);
Delay(20);
Key_Num = 1;
}
if(0 == P1_6)
{
Delay(20);
while(0 == P1_6);
Delay(20);
Key_Num = 5;
}
if(0 == P1_5)
{
Delay(20);
while(0 == P1_5);
Delay(20);
Key_Num = 9;
}
if(0 == P1_4)
{
Delay(20);
while(0 == P1_4);
Delay(20);
Key_Num = 13;
}
P1 = 0xFF; //P1端口全置为高电平
P1_2 = 0; //将第二列置为低电平
if(0 == P1_7)
{
Delay(20);
while(0 == P1_7);
Delay(20);
Key_Num = 2;
}
if(0 == P1_6)
{
Delay(20);
while(0 == P1_6);
Delay(20);
Key_Num = 6;
}
if(0 == P1_5)
{
Delay(20);
while(0 == P1_5);
Delay(20);
Key_Num = 10;
}
if(0 == P1_4)
{
Delay(20);
while(0 == P1_4);
Delay(20);
Key_Num = 14;
}
P1 = 0xFF; //P1端口全置为高电平
P1_1 = 0; //将第三列置为低电平
if(0 == P1_7)
{
Delay(20);
while(0 == P1_7);
Delay(20);
Key_Num = 3;
}
if(0 == P1_6)
{
Delay(20);
while(0 == P1_6);
Delay(20);
Key_Num = 7;
}
if(0 == P1_5)
{
Delay(20);
while(0 == P1_5);
Delay(20);
Key_Num = 11;
}
if(0 == P1_4)
{
Delay(20);
while(0 == P1_4);
Delay(20);
Key_Num = 15;
}
P1 = 0xFF; //P1端口全置为高电平
P1_0 = 0; //将第四列置为低电平
if(0 == P1_7)
{
Delay(20);
while(0 == P1_7);
Delay(20);
Key_Num = 4;
}
if(0 == P1_6)
{
Delay(20);
while(0 == P1_6);
Delay(20);
Key_Num = 8;
}
if(0 == P1_5)
{
Delay(20);
while(0 == P1_5);
Delay(20);
Key_Num = 12;
}
if(0 == P1_4)
{
Delay(20);
while(0 == P1_4);
Delay(20);
Key_Num = 16;
}
return Key_Num;
}
Delay.c
void Delay(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
LCD1602.c
#include <REGX52.H>
//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0
//函数定义:
/**
* @brief LCD1602延时函数,12MHz调用可延时1ms
* @param 无
* @retval 无
*/
void LCD_Delay()
{
unsigned char i, j;
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
/**
* @brief LCD1602写命令
* @param Command 要写入的命令
* @retval 无
*/
void LCD_WriteCommand(unsigned char Command)
{
LCD_RS=0;
LCD_RW=0;
LCD_DataPort=Command;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602写数据
* @param Data 要写入的数据
* @retval 无
*/
void LCD_WriteData(unsigned char Data)
{
LCD_RS=1;
LCD_RW=0;
LCD_DataPort=Data;
LCD_EN=1;
LCD_Delay();
LCD_EN=0;
LCD_Delay();
}
/**
* @brief LCD1602设置光标位置
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @retval 无
*/
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
if(Line==1)
{
LCD_WriteCommand(0x80|(Column-1));
}
else if(Line==2)
{
LCD_WriteCommand(0x80|(Column-1+0x40));
}
}
/**
* @brief LCD1602初始化函数
* @param 无
* @retval 无
*/
void LCD_Init()
{
LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
LCD_WriteCommand(0x01);//光标复位,清屏
}
/**
* @brief 在LCD1602指定位置上显示一个字符
* @param Line 行位置,范围:1~2
* @param Column 列位置,范围:1~16
* @param Char 要显示的字符
* @retval 无
*/
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
LCD_SetCursor(Line,Column);
LCD_WriteData(Char);
}
/**
* @brief 在LCD1602指定位置开始显示所给字符串
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串
* @retval 无
*/
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=0;String[i]!='\0';i++)
{
LCD_WriteData(String[i]);
}
}
/**
* @brief 返回值=X的Y次方
*/
int LCD_Pow(int X,int Y)
{
unsigned char i;
int Result=1;
for(i=0;i<Y;i++)
{
Result*=X;
}
return Result;
}
/**
* @brief 在LCD1602指定位置开始显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~65535
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以有符号十进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-32768~32767
* @param Length 要显示数字的长度,范围:1~5
* @retval 无
*/
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
unsigned char i;
unsigned int Number1;
LCD_SetCursor(Line,Column);
if(Number>=0)
{
LCD_WriteData('+');
Number1=Number;
}
else
{
LCD_WriteData('-');
Number1=-Number;
}
for(i=Length;i>0;i--)
{
LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
}
}
/**
* @brief 在LCD1602指定位置开始以十六进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFF
* @param Length 要显示数字的长度,范围:1~4
* @retval 无
*/
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i,SingleNumber;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
SingleNumber=Number/LCD_Pow(16,i-1)%16;
if(SingleNumber<10)
{
LCD_WriteData(SingleNumber+'0');
}
else
{
LCD_WriteData(SingleNumber-10+'A');
}
}
}
/**
* @brief 在LCD1602指定位置开始以二进制显示所给数字
* @param Line 起始行位置,范围:1~2
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
unsigned char i;
LCD_SetCursor(Line,Column);
for(i=Length;i>0;i--)
{
LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
}
}
4.2.2 密码锁
- 将S1~S10定义为1~9和0,按键12定义为确认键,按键11作为取消键;输入正确则显示OK,错误则显示ERROR
main.c
#include <REGX52.H>
#include "MatrixKey.h"
#include "LCD1602.h"
#define PASSWORD 1123 //初始密码
void main()
{
unsigned char key_num = 0,count = 0;
unsigned int password = 0;
LCD_Init();
LCD_ShowString(1,1,"PassWord:");
while(1)
{
key_num = MatrixKey_Num();
if(key_num)
{
if(key_num <= 10) //按键S1~S10,密码输入,期中S10定义为0
{
if(count < 4)
{
password *= 10; //左移一位
password += (key_num % 10); //个位的数字
count++; //每按下一次,计次加1
}
LCD_ShowNum(1,10,password,4);
LCD_ShowString(2,1," ");
}
}
if(12 == key_num) //按键S12作为确认键
{
if(password == PASSWORD) //密码正确,显示OK,并且清零,为下次输入做准备
{
LCD_ShowString(2,1,"OK");
password = 0;
count = 0;
}
else //密码错误,需要按下取消键,重新输入
{
LCD_ShowString(2,1,"ERROR");
}
}
if(11 == key_num) //按键S11作为撤销键
{
password = 0;
count = 0;
LCD_ShowNum(1,10,password,4);
LCD_ShowString(2,1," ");
}
}
}
五、LED数码管
5.1 数码管的显示原理
- LED数码管为“8”字型,共计8段(包括小数点)。每一段对应一个发光二极管,有共阳极和共阴极两种
- 本单片机的数码管为共阴极。要想使某一数码管点亮,则公共端接低电平,即选中该数码管,也称为位选;之后,根据显示的字符把某些段置为高电平,即点亮某些段,也称为段选
- 把==“a”段对应的字型码字节的最低位==
数码管的电路原理图:
- 74HC245:双线数据缓冲器。当DIR接高电平,则将P0端口的数据送到LED段上;当DIR接低电平,则将LED段上的数据送到P0端口上
- 由于单片机的高电平驱动能力比较弱,该芯片主要是为了提高它的驱动能力
- 138译码器:G1、G2A、G2B为使能端,G1高电平、G2A、G2B低电平,译码器使能;A、B、C为输入端,Y0~Y7为输出端
当输入端对应的二进制转化为十进制,使对应的输出端有效,即输出端对应的为低电平,其余端口输出高电平
C | B | C | Y |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 0 | 1 | 1 |
0 | 1 | 0 | 2 |
0 | 1 | 1 | 3 |
1 | 0 | 0 | 4 |
1 | 0 | 1 | 5 |
1 | 1 | 0 | 6 |
1 | 1 | 1 | 7 |
5.2 静态显示
- 对于该单片机而言,所谓静态显示,在同一时间内,只有某一位数码管被选中,当送入一次字型码后,可以保持不变,直到送入新字型码为止
#include <REGX52.H>
typedef unsigned int uint;
typedef unsigned char uchar;
//共阴极段码表
uchar LED_Num[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
/*
功能:选中某一位数码管,显示字符
参数类型:无符号字符型
参数:
Location:从右至左,依次为1~8
Number:显示相应的数字字符
*/
void Nixie_Tube(uchar Location,Number)
{
switch(Location)
{
case 1:P2_4 = 0;P2_3 = 0;P2_2 = 0;break;
case 2:P2_4 = 0;P2_3 = 0;P2_2 = 1;break;
case 3:P2_4 = 0;P2_3 = 1;P2_2 = 0;break;
case 4:P2_4 = 0;P2_3 = 1;P2_2 = 1;break;
case 5:P2_4 = 1;P2_3 = 0;P2_2 = 0;break;
case 6:P2_4 = 1;P2_3 = 0;P2_2 = 1;break;
case 7:P2_4 = 1;P2_3 = 1;P2_2 = 0;break;
case 8:P2_4 = 1;P2_3 = 1;P2_2 = 1;break;
}
P0 = LED_Num[Number];
}
void main()
{
while(1)
{
Nixie_Tube(6,6);
}
}
5.3 动态显示
- 每一时刻,只有一位位选线有效,即选中某一位显示。每隔一定时间逐位地轮流点亮各数码管(扫描),由于数码管的余辉和人眼的“视觉暂留”作用,只要控制好每位数码管点亮显示的时间和间隔,则可造成“多位同时亮”的假象
- 动态显示的实质:用执行程序的时间来换取I/O口数目的减少
- 执行过程:段选 位选——段选 位选——段选 位选
在选中下一位时,上一位的数据会跟随过来,因为下一位的数据还没有送到,会出现显示数据篡位的情况。所以,要在下一位位选前,将数码管清零,这样就不会影响下位数据的显示
#include <REGX52.H>
typedef unsigned int uint;
typedef unsigned char uchar;
uchar LED_Num[] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
void Delay(uint xms) //@12.000MHz
{
unsigned char i, j;
while(xms--)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
void Nixie_Tube(uchar Location,Number)
{
switch(Location)
{
case 1:P2_4 = 0;P2_3 = 0;P2_2 = 0;break;
case 2:P2_4 = 0;P2_3 = 0;P2_2 = 1;break;
case 3:P2_4 = 0;P2_3 = 1;P2_2 = 0;break;
case 4:P2_4 = 0;P2_3 = 1;P2_2 = 1;break;
case 5:P2_4 = 1;P2_3 = 0;P2_2 = 0;break;
case 6:P2_4 = 1;P2_3 = 0;P2_2 = 1;break;
case 7:P2_4 = 1;P2_3 = 1;P2_2 = 0;break;
case 8:P2_4 = 1;P2_3 = 1;P2_2 = 1;break;
}
P0 = LED_Num[Number];
Delay(1);
P0 = 0x00; //消影
}
void main()
{
while(1)
{
Nixie_Tube(8,1);
Nixie_Tube(7,2);
Nixie_Tube(6,3);
}
}
- 该扫描方式为单片机的直接扫描,不断对数码管点亮刷新,其电路较为简单,但消耗CPU资源,占用大量时间
六、中断系统
6.1 中断
- 中断是为使单片机具有对外部随机发生的事件实时处理而设置的, 中断功能的存在,很大程度上提高了单片机处理外部事件的能力。
- 中断:当中央处理机CPU 正在处理某件事的时候外界发生了紧急事件请求,要求 CPU 暂停当前的工作,转而去处理这个紧急事件,处理完以后,再回到原来被中断的地方,继续原来的工作,这样的过程称为中断。
- 中断系统一般允许多个 中断源,当几个中断源同时向 CPU 请求中断,要求为它服务的时候,这就存在 CPU 优先响应哪一个中断源请求的问题。通常根据中断源的轻重缓急排队,优先 处理最紧急事件的中断请求源,即规定每一个中断源有一个优先级别。CPU 总是 先响应优先级别最高的中断请求。
- 当 CPU 正在处理一个中断源请求的时候(执行相应的中断服务程序),发生 了另外一个优先级比它还高的中断源请求。如果 CPU 能够暂停对原来中断源的服务程序,转而去处理优先级更高的中断请求源,处理完以后,再回到原低级中断服务程序,这样的过程称为中断嵌套。这样的中断系统称为多级中断系统,没有中断嵌套功能的中断系统称为单级中断系统。
- 引起 CPU 中断的根源称为中断源。中断源向 CPU 提出中断请求,CPU 暂时 中断原来的事务 A,转去处理事件 B,对事件 B 处理完毕后,再回到原来被中断 的地方(即断点),称为中断返回。实现上述中断功能的部件称为中断系统(中断机构)
- 优点
①分时操作。CPU 可以分时为多个 I/O 设备服务,提高了计算机的利用率;
②实时响应。CPU 能够及时处理应用系统的随机事件,系统的实时性大大增 强;
③可靠性高。CPU 具有处理设备故障及掉电等突发性事件能力,从而使系统 可靠性提高。
6.2 中断系统结构
6.2.1 中断请求标志寄存器
- TCON寄存器
TCON为定时器/计数器的控制寄存器,字节地址为88H,可位寻址
(1)TF1:片内定时器/计数器T1的溢出中断请求标志位
当启动T1计数后,定时器/计数器T1从初值开始加1计数,当计数溢出时,由硬件自动置1,向CPU申请中断。CPU响应TF1中断时,TF1标志位由硬件自动清零,TF1也可由软件清零
(2)TF0:片内定时器/计数器T0的溢出中断请求标志位
(3)IE1:外部中断请求1的中断请求标志位
(4)IE0:外部中断请求0的中断请求标志位
(5)IT1:选择外部中断请求1位负跳变触发方式还是电平触发方式
IT1 = 0,低电平触发方式;IT1 = 1,负跳变触发方式。当CPU检测到 P3.3引脚上出现有效的中断信号时,中断请求标志IE1置 1,向 CPU 申请中断。
(6) IT0:选择外部中断请求0位负跳变触发方式还是电平触发方式
- 当单片机复位后,TCON会被清零,5个中断源的中断请求标志位均为0
- 对于外部中断而言,当中断响应时,
负跳变的触发方式,会由硬件自动清零,由于负跳变信号过后也就消失了,不会有影响
低电平的触发方式会由硬件清零,但中断信号的低电平可能继续存在,在以后的机械周期采样时,标志位可能重新置1。除了标志位清零外,还需要在中断响应后把中断请求信号输入引脚从低电平强制为高电平
- SCON寄存器
SCON为串口控制寄存器,字节地址为98H,可位寻址
(1)T1:串行口发送中断请求标志位。
每发送完一帧串行数据后,硬件把T1中断请求标志位自动置1.CPU响应串行口发送中断时,并不能清除T1标志位,必须在中断程序中用软件清零
(2)RI:串行口接收中断请求标志位。
6.2.2 中断允许寄存器 IE
IE的字节地址为A8H,可进行位寻址
中断允许寄存器IE对中断的允许对中断的允许和禁止实现两级控制:一个中断允许总开关EA和各中断源的中断允许控制位
1——中断请求被允许,0——中断请求被屏蔽
(1)EA:中断允许总开关控制位
(2)ES:串行口中断允许控制位
(3)ET1:定时器/计数器1的溢出中断允许控制位
(4)EX1:外部中断1中断允许控制位
(5)ET0:定时器/计数器0的溢出中断允许控制位
(6)EX0:外部中断0中断允许控制位
单片机复位后,IE被清零,所有中断请求都被禁止
6.2.3 中断优先级寄存器 IP
中断优先级寄存器IP,其字节地址为B8H,可位寻址
1——高优先级,0——低优先级
(1)PS:串行口中断优先级控制位
(3)PT1:定时器/计数器1的溢出中断优先级控制位
(4)PX1:外部中断1中断优先级控制位
(5)PT0:定时器/计数器0的溢出中断优先级控制位
(6)PX0:外部中断0中断优先级控制位
单片机复位后,IP被清零,各个中断源均为低优先级中断
当同时收到多个同一优先级的中断请求时,中断请求的响应取决于内部的查询顺序:
中断响应还遵循以下规则:
- 低优先级可被高优先级中断,高优先级不能被低优先级中断
- 任何一种中断(低级或高级)一旦得到响应,不会再被它的同级中断源所中断
6.3 响应中断请求的条件
一个中断源的中断请求被响应,必须满足以下条件
- 总中断允许开关接通:EA = 1
- 中断源发出中断请求,即中断源对应的中断请求标志位为1
- 中断源的中断允许位为1
- 无同级或更高级的中断正在被服务
中断响应的过程:首先将程序计数器PC的内容压入堆栈中,以保护断点,再将中断入口地址装入PC计数器,使程序转向响应中断请求的中断入口地址
可以看出两个中断入口只相隔8字节,无法存放一个完整的中断服务程序。因此,通常在中断入口地址放置了一条无条件转移指令,使程序执行转向在其他地址存放的中断服务程序入口
CPU定期在每个机械周期都要查询各中断源的中断请求标志,并不是查询到的所有中断都能立即被响应,当遇到以下情况,中断被封锁
(1)CPU正在处理同级或更高优先级的中断
(2)查询出中断请求的机械周期不是当前正在执行指令的最后一个机械周期。只有指令执行完成后,才能响应中断
6.4 中断函数
由于C51编译器在编译时对中断服务函数自动添加了现场保护、阻断其他中断、返回时自动恢复现场等处理的程序段,编写中断服务程序不必考虑这些问题
一般形式:
void 函数名() interrupt n using n
(1)关键字interrupt后面的n是中断号,n取值0~4,编译器从8Xn+3处产生中断向量
(2)using后面的n用来选择工作寄存区。
在中断函数的入口处将当前的工作寄存器区的内容保护到堆栈中,函数返回之前将被保护的寄存器区的内容从堆栈中恢复。如果不用该关键字,中断函数中的所有工作寄存器的内容将被保护到堆栈中
- 中断函数没有返回值
- 中断函数不能进行参数传递
- 中断函数不能直接被调用
- 如果中断函数中调用其他函数,则被调用的的函数所使用的寄存器区必须与中断函数使用的寄存器区不同
七、定时器/计数器
7.1 结构框图
51单片机的定时器属于单片机的内部资源,其电路的连接和运转均在单片机的内部完成
定时器/计数器和单片机的 CPU 是相互独立的。定时器/计数器工作的过程 是自动完成的,不需要 CPU 的参与
51 单片机中的定时器/计数器是根据机器内部的时钟或者是外部的脉冲信 号对寄存器中的数据加 1
- T0、T1都具有定时器和计数器两种工作模式,其实质都是对脉冲信号进行计数
计数器是对加在P3.4和P3.5引脚上的外部脉冲进行计数;
定时器是对片内时钟信号12分频后的脉冲信号计数 - 计数器从初值开始计数,单片机复位后初值为0,也可以用指令给计数器装一个新的初值
7.2 工作方式寄存器 TMOD
- TMOD用于选择工作模式和工作方式,不可位寻址
- GATE:门控位
GATE=0:是否计数仅由TRx来控制
GATE=1:是否计数,由T外部中断引脚(INT0/INT1)与Rx来控制 - C/T:选择工作模式
C/T=0:定时器,对系统时钟12分频后的内部脉冲计数
C/T=1:计数器,对P3.4和P3.5引脚上的外部脉冲进行计数 - M1、M0:选择工作方式
M1 M0 | 工作方式 |
---|---|
0 0 | 方式0,13位(TLx的低五位和THx的整个8位)定时器/计数器 |
0 1 | 方式1,16位定时器/计数器 |
1 0 | 方式2,8位自动重新装载的定时器/计数器,低8位存放的值自动存放在高8位 |
1 1 | 方式3,T0分成2个8位计数器/定时器,T1停止计数 |
7.3 控制寄存器 TCON
- TCON用于控制T0、T1的启动和停止计数,可位寻址
- TF1、TF0:计数溢出标志位
- TR1、TR0:计数启动控制位
TR1、TR0=1,启动
TR1、TR0=0,停止
7.4 工作方式
7.4.1 方式0:13位定时器/计数器
7.4.2 方式1:16位定时器/计数器
方式1除了使用整个THx和TLx之外,其他的全都和方式0相同
7.4.3 方式2:8位自动重新装载的定时器/计数器
TL0的溢出不仅置位TF0,而且将TH0内容重新装入TL0,TH0内容由软件预置,重装时TH1内容不变
7.4.4 方式3:T0分成2个8位计数器,T1弃用
八、串行口
STC89C52列单片机内部集成有一个全双工通用异步收发串行通信口
- 常见的通信接口比较
8.1 串行通信基础
8.1.1 并行通信和串行通信
- 并行通信
通常使用多条数据线将数据字节的各个位同时传送,每位数据都需要一条传输线,此外,还需要一条或多条控制信号线 - 优点:传输速度快
- 缺点:由于传输线较多,长距离传送时成本高
并行通信适合短距离的数据传输
- 串行通信
将数据字节分成一位一位的形式,在一条传输线上逐个传送 - 串行通信时,要把并行数据变成串行数据发送到线路上去,接收时要把串行数据再变成并行数据
- 优点:传输线少,长距离传送时成本低
8.1.2 同步通信和异步通信
- 串行通信:同步串行通信和异步串行通信
- 同步串行通信采用一个同步时钟,通过一条同步时钟线,加到收发双方,使其达到完全同步。此时,许多字符组成的信息组,一个接一个的传输。在每组信息(通常称为信息帧)的开始要加上同步字符,在没有信息要传输时,要填上空字符,因为同步传输不允许有间隙
2. 异步串行通信
- 异步串行通信是指收、发双方使用各自的时钟控制数据的发送和传输
- 异步通信以数据帧为单位进行传输,各数据帧之间的间隔是任意的,但每个数据帧中的各位是以固定时间传送的,但每个数据帧要附加起始位、停止位,有时还要加上校验位
8.1.3 串行通信的传输模式
- 单工
数据传输仅能按一个固定的方向传输,不能反向传输
- 半双工
数据传输可以双向传输,但不能同时进行传输
- 全双工
数据传输可以同时进行双向传输
8.2 串行口的结构
串口有两个物理上独立的接收、发送缓冲器SBUF(特殊功能寄存器),可以同时发送、接收数据。发送缓冲器只能写入不能读出,接收缓冲器只能读出不能写入,两者公用同一个字节地址99H
8.2.1 串行口控制寄存器 SCON
字节地址98H,可位寻址
波特率:每秒传输二进制位数,单位是bps
定时器溢出率:定时器溢出的频率,即1/溢出一次所用的时间
对于12分频而言:
定时器频率:f / 12,计数一次所需要的时间:12 / f
溢出一次所需要的时间:(最大值 - 初值)x (12 / f)
8.2.2 电源控制寄存器 TCON
字节地址:87H,不可位寻址
九、LED点阵屏
9.1 LED点阵屏介绍
LED点阵屏由若干个独立的LED组成,以矩阵的形式排列,以灯珠亮灭来显示文字、图片、视频等
分类:
(1)按颜色:单色、双色、全彩(红绿蓝)
(2)按色素:88、1616等(大规模的LED点阵通常由很多个小点阵拼接而成)
9.2 硬件电路
LED点阵电路图
- 其中,P0口控制列,D0~D7控制行
74HC595电路
- OE:输出使能端,低电平有效
RCLK:寄存器时钟引脚
SRCLR:串行清零
SRCLK:串行时钟
SER:串行数据
开发板上引脚对应关系
74HC595工作原理
串行数据引脚SER将数据放在左边的移位寄存器中,串行时钟引脚SECLK每来一个上升沿,数据往下移一位。当寄存器时钟引脚RCLK来一个上升沿,会将移位寄存器的数据同时移动到输出端,进行并行输出
- 操作要领:这三个引脚是通过P3的3个引脚控制的。当刚上电时,引脚初始化为高电平,需要对串行时钟引脚SECLK和寄存器时钟引脚RCLK初始化为低电平。当串行时钟引脚SECLK置为高电平(出现上升沿),串行数据向下移动一位,之后将串行时钟引脚SECLK置为低电平,当移位寄存器存储8位数据后,寄存器时钟引脚RCLK置为高电平(出现上升沿),将数据同时移动到输出端,进行并行输出,之后需要将寄存器时钟引脚RCLK置为低电平
十、DS1302实时时钟
10.1 DS1302
DS1302 是 DALLAS 公司推出的涓流充电时钟芯片,内含有一个实时时钟/日历和 31字节静态 RAM,通过简单的串行接口与单片机进行通信。实时时钟/日 历电路提供秒、分、时、日、周、月、年的信息,每月的天数和闰年的天数可自动调整。
当单片机掉电后,实时时钟芯片会自动切换到备用电池,实现掉电也会继续工作
- 时序图
(1)单字节读时序:先对寄存器写命令,从最低位开始写,写数据是在 SCLK 的上升沿实现,而读数据在 SCLK 的下降沿实现。所以, 在单字节读时序中,写命令的第八个上升沿结束后紧接着的第八个下降沿就将要 读寄存器的第一位数据读到数据线上
(2)单字节写时序:两个字节的数据配合 16 个上升沿将数据写入即可 - RTC寄存器
下面对部分寄存器进行说明:
- 秒寄存器:低四位为秒的个位,高的次三位为秒的十位。最高位 CH 为 DS1302 的运行标志,当 CH=0 时,DS1302 内部时钟运行,反之 CH=1 时停止
- 时寄存器:最高位为 12/24 小时的格式选择位,该位为 1 时 表示 12 小时格式。当设置为 12 小时显示格式时,第 5 位的高电平表示下午 (PM);而当设置为 24 小时格式时,第 5 位位具体的时间数据。
- 写保护寄存器:当该寄存器最高位 WP 为 1 时,DS1302 只读不写,所以要在往 DS1302 写数据之前确保 WP 为 0
注:实时时钟的寄存器起始地址以0开始,即秒寄存器地址为0,分寄存器地址1为1
- 命令字
命令字启动每一次数据传输。
MSB (位 7):必须是逻辑 1。如果是 0,则禁止对 DS1302写入
位 6 :逻辑 0时规定为时钟/日历数据,逻辑 1时为 RAM数据
位 1 至 位 5 :表示RAM/寄存器的地址
LSB (位 0) :在逻辑0时为写操作(输出),逻辑1时为读操作(输入)。
通过命令字,可实现对寄存器实现读写操作
上述RTC寄存器图中,前两列已经给出命令字
- BCD码
在日历/时钟寄存器中都是以 BCD 码存放数据
BCD 码是通过 4 位二进制码来表示 1 位十进制中的 0~9 这 10 个数码。
BCD转化十进制:BCD/16*10 + BCD%16(两位)
十进制转化BCD:BCD/10*16 + BCD%10(两位)
十一、蜂鸣器
十二、AT24C02
12.1 AT24C02介绍
12.2 IIC通信
I2C(Inter-Integrated Circuit)总线是由 PHILIPS 公司开发的两线式 串行总线,用于连接微控制器及其外围设备。它是同步通信的一种特殊形式,具有接口线少,控制方式简单, 器件封装形式小,通信速率较高等优点。I2C 总线只有两根双向信号线。一根是数据线 SDA,另一根是时钟线 SCL。由于其管脚少,硬件实现简单,可扩展性强 等特点,因此被广泛的使用在各大集成芯片内。
特点:同步、半双工、带数据应答
IIC电路规范:
时序结构:
-
起始与终止条件:
-
发送一字节数据:
-
接受一字节数据:
当接收一个字节时,主机要释放对SDA线的控制权,将控制权交给从机,将SDA置为1即可
-
数据应答:
数据帧格式:
1.发送数据帧
- 接收数据帧:
- 复合格式:
- AT24C02数据帧