中断是大多数CPU最精彩的部分之一,下面我们通过讲解和编程练习学习中断和定时器相关概念
目录
1.1.什么是中断
1.2.中断的种类
1.3中断的相关概念
1.4. 51单片机可用中断及相关引脚
1.4.寄存器
1.5.中断优先级
在未进行任何关于优先级的设置情况下,51 单片机(52 单片机)中断优先级如图所示。
2.1定时器与定时器中断
2.1.1单片机的两个周期
2.1.2定时器原理
2.2相关寄存器
2.3.定时器的应用
2.3.1精准延时
2.3.2.定时器时钟
2.3.3呼吸灯
2.3.4电机调速
中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。
简单来说,中断的概念非常好理解。当 CPU 在主函数 main 里执行任务 A 时,突然传感器甲发来信号说有件更紧急的事 B 需要马上处理一下。无奈,CPU 只能暂时停下手头的工作A,先去执行任务 B,想着等 B 处理完后再回来处理 A——这已经是一次中断了。
51 单片机的中断主要有三类:
◎ 外部中断(External Interrupt): :在学习 IO 口“I”的功能,即检测功能时,
我们通过在 main 函数的 while(1)死循环里反反复复判断相应 IO 口电平状态来实
现。但是你有没有想过,如果我有 100 个传感器,是不是就要 100 个 if?由于
CPU 是串行,即一条指令一条指令往下执行的,如果 CPU 处理速度慢,在判断
第 3 个传感器时,第 100 个起反应了,可是在当执行到第 100 个 if 时,第 100
个传感器的信号已经消失了——说白了,CPU 脑子反应慢,处理不过来。另外,
100 个 if 放在主函数里,啥时候才能执行到除判断以外的更为重要任务?所以,
我们就想:能不能让传感器接一个特殊的引脚,只要传感器的输出引脚电平一改
变,CPU 立马触发中断,暂停主函数中更为重要的任务,去处理传感器起反应
后本应该执行的任务(原来 if 里面的语句),从而将软件判断改为硬件强制触
发呢?这就是外部中断!
◎ 定时器中断(Timer Interrupt): :定时器中断就非常好理解了:每隔一段
时间让 CPU 停下主函数的任务,去执行我们希望它定时执行的任务。举个例子,
我是个机器人爱好者,有时候需要测量机器人的轮速,通过一种名为编码器的传
感器,我可以得到车轮转过的圈数,如果我再加一个定时器,每隔一个固定的时
间段获取一次圈数,用圈数除以这个固定的时间段,不就得到了机器人车轮的转
速了吗?
◎ 串口中断(Serial Interrupt): :串口是芯片与芯片之间交流的通道。所谓“交
流”,无非指芯片与芯片之间相互发送数据或者指令。当发送端发送后,可以触
发中断,执行我们希望它发送后执行的任务,比如输出个“发送成功”;同理,
当接收端接收到数据或指令后,我们也可以让它触发中断执行某些任务,比如判
断一下接收到的是啥,然后执行相应操作。
◎ 中断源:很好理解,就是引起中断的东西。对于外部中断来说,中断源
就是那些五花八门的传感器;对于定时器中断,中断源是内部的程序;对于串口
中断,中断源当然是其它芯片传来的数据或指令了。
◎ 中断服务子程序: :前面我们一直说 CPU 发生中断后去执行更为紧急的任
务,你可能很疑惑:在哪里执行?就是在中断服务子程序里执行的。后面我们会
单独写一个与你之前接触过的函数不太一样的函数专门作为中断触发后执行的
任务。
◎ 中断嵌套:在解释什么是中断时,我其实已经提到了中断嵌套这个问题
——暂停任务 A 转而执行任务 B 时,收到更为紧急的任务 C,这里第二级中断
就嵌套在第一级里。也许你会问:那是不是在执行一个中断服务子程序时又一个
中断被触发了,就一定要停下这个中断去执行新的中断服务子程序?当然不是
啦,凡是还有个等级优先嘛——这不,我之前不是一直强调“更为紧急”?这说
明中断之间还有个“优先级”呢!
我们用的这款 51 单片机,有 2 个外部中断(INT0、INT1),2 个定时器中
断(Timer0 与 Timer1)与 1 个串口中断(uart)。
把视线集中在 P0 组 IO 口。你会发现,这组 IO 口不仅可以做为正常的 IO口使用,似乎还可以用于别的功能。你瞧,P3.0 与 P3.1 是不是还附带着 RxD 与TxD?这些是与串口通信相关的引脚。同样,P3.2、P3.3 是与外部中断相关的引脚。P3.4、P3.5 是与定时器相关的引脚。
寄存器的英文名是“register”,百度一查,发现它的中文意思是“注册,登记”。通俗地理解,我们要使用 51 单片机中某个功能,是不是得告诉单片机:我需要用你某某功能,具体怎么怎么用,某某参数是某值……那好,在哪里告诉51 单片机这些信息呢?当然是的相关的寄存器“登记”或“注册”了。喜欢打破砂锅问到底的你也许会问:为什么翻译成“寄存器”?我想,可能是在设置时候,某些参数暂时“寄存”在寄存器里吧。
在未进行任何关于优先级的设置情况下,51 单片机(52 单
片机)中断优先级如图所示。
图中的“中断向量地址”不用理会,因为这与汇编语言相关由图可见,优先级最高的是中断是外部中断 0,最低的是外部中断 3(如果有,取决于你用的单片机)。
说到定时器,就得提一下他的总指挥——晶振。
晶振过自己的振动为整个单片机提供时钟,如图下图所示。时钟都有了,芯片内部各
个电路可按时钟提供的节拍运作,那么还会紊乱吗?
◎ 时钟周期:这个好理解嘛,就是晶振产生的时钟的周期。你们手头常见的晶振有两种,一种上面写着“11.0592”,一种写着“12”或者什么也没有写,默认单位为“MHz”。这些参数是晶振的频率。如何通过频率得到周期呢?可以想到:1/f=T,不同频率的晶振的用途是不一样的,12MHz 的晶振一般用于定时器,而 11.0592MHz 的晶振一般用于串口通信。标准的51单片机晶振是1.2M-12M,一般由于一个机器周期是12个时钟周期,所以先12M时,一个机器周期是1US,好计算,而且速度相对是最高的(当然现在也有更高频率的单片机)。11.0592M是因为在进行通信时,12M频率进行串行通信不容易实现标准的波特率,比如9600,4800,而11.0592M计算时正好可以得到,因此在有通信接口的单片机中,一般选11.0592M。
◎ 机器周期 :执行一条基本指令所需要的时间。注意:我说的是基本指令,因为一次加法与一次乘法(少数乘法运算除外)所需要的时间当然得不一样吧?乘法运算的底层实现需要好多条基本指令,而加法一般就一条,这也就是为什么我们写程序要尽可能少用乘法运算的原因。对于 CPU 而言,机器周期由若干时钟周期组成。以你在用的 51 单片机为例,它的机器周期默认为 12 个时钟周期。当然了,既然说了默认情况下 12 个时钟周期构成一个机器周期,那么肯定可以更改了——事实上,你可以通过设置烧录软件将你的 51 单片机设置为6T 模式:6 个时钟周期构成一个机器周期,具体设置下图所示
6T显然相比于12T执行指令更快了。
51 单片机里有一种寄存器专门用于数机器周期,它一共 16 位,由高 8 位TH(Timer High)与低 8 位 TL(Timer Low)组成,如下图所示。
如上图的演示,这个寄存器一共有 16 位,所以可记 2^16=65536 个机器周期。当第 65535 个机器周期过后,寄存器的值全为“1”,再来一个机器周期(第 65536 个)后,便溢出清为 0,同时触发中断!
下面介绍一些常用的寄存器
IE(Interrupt Enable)IE 寄存器负责各种中断的使能,下面逐个介绍 IE 寄存器每一位的作用:
TCON 寄存器主要用于控制定时器,也有部分与外部中断有关。在此,我只
介绍控制定时器的相关位:
顾名思义,此寄存器用于配置定时器的工作模式,一共 8 位,高 4 位(B7-B4)用于配置定时器 1 的工作模式,低 4 位(B3-B0)用于配置定时器 0 的工作模式。
这里,我仅以高 4 位为例:
M1 | M0 | 模式特点 |
---|---|---|
0 | 0 | 13 位定时器(计数器),即由 TH 与 TL 构成的 16 位“计分器”中只用 13 位。这里用了 TL 的低 5 位与整个 TH。此模式不常用。 |
0 | 1 | 16 位定时器(计数器),就像我在介绍定时器原 理讲的那样,TH 与 TL 全用上。当“计分器”计 满后自动置 0,需要人为重新赋初值,否则从 0 开始计起。此模式最常用。 |
1 | 0 | 8 位自动重载定时器,仅将 TL 当成“计分器”, 当 TL 溢出后 TH 的值自动赋给 TL 而无须人为再 给初值。此模式在串口通信时常用。 |
1 | 1 | 此模式略复杂,不常用,也不介绍。 |
这个寄存器就没啥好说的了,前面反反复复提到它。这里要提醒的是:每一个定时器(计数器)都有各自的 TH 与 TL,而并非共用一个。
实现小灯闪烁时,我们用到了延时。当时,我们是通过让 CPU 原地数数来实现的。这种延时方式称为“软件延时”,它是不准确的,因为还有一些冗余的时间我们没有考虑到。因此,本小节我们讨论如何通过定时器来实现精准延时。
先给出代码,同样以小灯闪烁为例,闪烁周期为 1s(500ms 亮,500ms灭),电路接法和你前面做的一样。
#include
sbit LED = P1^0;
unsigned char cnt = 0; //记录发生中断的次数
void main(void)
{
EA= 1; // 打开全局中断
ET0 = 1; // 打开定时器 0 中断功能
TMOD = 0x01; // 将定时器 0 的工作模式设置为工作模式 1
TH0 = 0x3c; // 给“计数器”高 8 位赋初值
TL0 = 0xb0; // 给“计数器”低 8 位赋初值
TR0 = 1; // 开启定时器 0
while(1)
{
// 这里面可以执行你想让 CPU 执行的主要任务
}
}
/*
功能:定时器 0 中断服务子程序,每当“计分器”溢出就来执行此函数。
注意:中断服务子程序的函数定义与普通函数不一样,interrupt 是关键
词,必须得有;后面的序号是定时器 0 的优先级序号,在第六章
我有提到,如果你改变了优先级,则此序号也应该相应改变。
*/
void timer0() interrupt 1
{
cnt++;
// 每隔 50ms 发生一次中断(见代码讲解),故 10 次为 500ms
if(cnt == 10)
{
cnt = 0; // 必变量必须清 0
LED = ~LED; // 每 500ms 改变一次 LED 状态
}
TH0 = 0x3c; // 溢出后“计分器”自动清 0,故需要重新设定初值
TL0 = 0xb0;
}
这里采用模块化编程
1.main.c
#include
#include "Delay.h"
#include "LCD1602.h"
#include "Timer0.h"
unsigned char Sec,Min,Hour;
void main()
{
LCD_Init();
Timer0Init();
LCD_ShowString(1,1,"Clock:");
LCD_ShowString(2,1," : :");
while(1)
{
LCD_ShowNum(2,1,Hour,2);
LCD_ShowNum(2,4,Min,2);
LCD_ShowNum(2,7,Sec,2);
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x66; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
Sec++;
if(Sec>=60)
{
Sec=0;
Min++;
if(Min>=60)
{
Min=0;
Hour++;
if(Hour>=24)
{
Hour=0;
}
}
}
}
}
2.1LCD1602.c
#include
//引脚配置:
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;i0;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');
}
}
2.2LCD1602.h
#ifndef __LCD1602_H__
#define __LCD1602_H__
//用户调用函数:
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
#endif
3.1Timer0.c
#include
/**
* @brief 定时器0初始化
* @param无
* @retval 无
*/
void Timer0Init()//1毫秒@11.0592MHz
{
TMOD &= 0xF0; //设置定时器模式
TMOD |= 0x01; //设置定时器模式
TL0 = 0x66; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0=1; //打开定时器0中断
EA=1; //开总中断
PT0=0;
}
/*定时器终端模板函数
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x66; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
T0Count++;
if(T0Count>=1000)
{
T0Count=0;
P2_0=~P2_0;
}
}
*/
3.2.Timer0.h
#ifndef __TIMER0_H__//防重复定义
#define __TIMER0_H__
void Timer0Init();
#endif
4.1Delay.c
#include
void Delay(unsigned int xms)
{
unsigned char i, j;
while(xms--){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
}
}
4.2Delay.h
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay(unsigned int xms);
#endif
1)原理
所谓呼吸灯,是指 LED 灯逐渐从灭到亮或从亮到灭。不同的占空比对应不同的模拟电压值,也就对应不同的 LED 亮度。当 PWM 波的占空比变化得很快时,从宏观上来看,LED 灯就是“连续”变化的——本质上,它还是离散的,只是变化得太快,欺骗了我们的眼睛罢了。
2)代码实现
#include
void delay_100us(unsigned int time);
void setPWM(unsigned char IO, unsigned char level);
void main(void)
{
unsigned char level = 0; // 占空比等级
unsigned char step = 1; // 步长
unsigned char LED = 0;
while(1)
{
// 逐亮
for(level = 0; level <= 100; level += step)
setPWM(LED, level);
// 逐灭
for(level = 100; level > 0; level -= step)
setPWM(LED, level);
}
}
/*
由于占空比分为 100 级,周期为 10ms,所以 1 级应该对应 100us。
*/
void delay_100us(unsigned int time)
{
TMOD = 0x01;
TR0 = 1;
for(; time > 0;time--)
{
TH0 = 0xff;
TL0 = 0x9c;
while(TF0 == 0);
TF0 = 0;
}
TR0 = 0;
}
void setPWM(unsigned char IO, unsigned char level)
{
unsigned char group_num = IO/10;
unsigned char IO_num = IO % 10;
switch(group_num)
{
case 0:
P0 |= 0x01 << IO_num;
delay_100us(level);
P0 &= ~(0x01 << IO_num);
delay_100us(100-level);
break;
case 1:
P1 |= (0x01 << IO_num);
delay_100us(level);
P1 &= ~(0x01 << IO_num);
delay_100us(100-level);
break;
case 2:
P2 |= 0x01 << IO_num;
delay_100us(level);
P2 &= ~(0x01 << IO_num);
delay_100us(100-level);
break;
case 3:
P3 |= 0x01 << IO_num;
delay_100us(level);
P3 &= ~(0x01 << IO_num);
delay_100us(100-level);
break;
}
}
1) 电机调速原理
你手头用的较多的电机,是如图 7-6 所示的直流电机(就是你小时候玩的四驱车马达)。这种电机不分正负极,只要加上电池(当然你电池电压不能太大)就能够转动。当然,加不同方面和电压可实现不同方向的转动。
然而,等你慢慢地接触更高级的电机时,你会发现你用的电机通常有多个引脚,如下图 所示。
这种电机是可以调速的。它的三个引脚的作用,想必你都可以猜得出来:Gnd 与Vcc 肯定是少不了,用于供电;另外一个引脚当然得用于接收调速的信号。而调速信号则是由 pwm 波产生的模拟值。
2)代码实现
这里,我们实现一个可调速的风扇,风扇由 4 个按键控制,其中 3 个用于调整风扇转速,另一个用于使风扇停止转动。
#include
sbit IN1 = P0^6; // L298N 信号输入 1
sbit IN2 = P0^7; // L298N 信号输入 2
sbit stop = P0^0; // 停止按键
sbit vel1 = P0^1; // 1 档按键
sbit vel2 = P0^2; // 2 档按键
sbit vel3 = P0^3; // 3 档按键
void delay_100us(unsigned int time);
void setPWM(unsigned char IO, unsigned char level);
void main(void)
{
// 起始时让风扇不转
unsigned char velocity = 0;
IN1 = 0;
IN2 = 0;
while(1)
{
// 检测按键,每个按键对应一个速度
if(stop == 0)
velocity = 0;
else if(vel1 == 0)
velocity = 30;
else if(vel2 == 0)
velocity = 60;
else if(vel3 == 0)
velocity = 90;
// 为电机设定 PWM 值
setPWM(6, velocity);
}
}
void setPWM(unsigned char IO, unsigned char level)
{
unsigned char group_num = IO/10;
unsigned char IO_num = IO % 10;
switch(group_num)
{
case 0:
P0 |= 0x01 << IO_num;
delay_100us(level);
P0 &= ~(0x01 << IO_num);
delay_100us(100-level);
break;
case 1:
P1 |= (0x01 << IO_num);
delay_100us(level);
P1 &= ~(0x01 << IO_num);
delay_100us(100-level);
break;
case 2:
P2 |= 0x01 << IO_num;
delay_100us(level);
P2 &= ~(0x01 << IO_num);
delay_100us(100-level);
break;
case 3:
P3 |= 0x01 << IO_num;
delay_100us(level);
P3 &= ~(0x01 << IO_num);
delay_100us(100-level);
break;
}
}
void delay_100us(unsigned int time)
{
TMOD = 0x01;
TR0 = 1;
for(; time > 0;time--)
{
TH0 = 0xff;
TL0 = 0x9c;
while(TF0 == 0);
TF0 = 0;
}
TR0 = 0;
}
以上就是关于中断和定时器的全部内容。