本篇通过闪烁led和数码管两个实验来分析延时与中断概念,以及实现软件仿真。
Chapter1.中断的概念
中断就是当CPU执行主程序时,突然被打断一下,去干别的事情,干完再回来执行主程序。这个过程类似于你在洗衣服时水开了,你停下洗衣服的动作去关掉火,再回来烧开水。
中断还可以进行嵌套,就是CPU在执行一个中断程序的时候又被打断,去执行另一个中断程序,执行完再回来执行第一个中断程序,执行完再回来执行主程序。类似于你在洗衣服的时候去关烧开水的火之后灌开水,正灌着呢发现电话响了去接电话,接完电话回来继续灌开水,灌完开水回来继续洗衣服。
“中断”这个概念来源于单片机本身是串行执行。也就是说,只有执行完一条语句才能执行下一条语句,在同一时间内只能干一件事情。FPGA等支持并行执行的硬件则可以同时干很多不同的事情。然而FPGA也有中断,这里不是特别了解不再阐述。
“中断”这一概念为实现某一信号的触发功能打下了基础。一般而言,52单片机有6个中断源,其中三个为信号中断源,分别是INT0,INT1(外部中断0/1号口)和TI/RI(串行口)中断,这里相当于执行主程序的时候等信号,等来了就执行相应的信号处理操作;三个为芯片内部定时器/计数器中断,分别为T0/T1/T2,它们记满回零后即可触发中断,这里也相当于在时钟电平变化时,就执行下一项操作。
中断也不是随便进行的,状态比较多,设置很麻烦,每个程序执行中断前必须都不厌其烦地设定与检查中断状态。
1.中断的允许与使能
首先一个问题:到底要不要中断?
假如一个程序运行压根不需要用到中断,那么中断的存在可能会影响整个程序,因此默认状态下,中断一般是关着的。想要打开就必须对它进行操作。
允许中断了,但是有6个中断源,到底允许哪个中断呢?这就要再对分别控制6个中断源的寄存器口进行使能操作。
以上两个问题均由中断允许寄存器IE(interrupt enable)控制。它有八位,分别是:
位序号 | D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 |
说明 | 外部中断0 | 定时/计数0 | 外部中断1 | 定时/计数1 | 串行口中断 | 定时 2 溢出中断( 8052用) | 无效位 | 全局中断位 |
位符号(写程序时直接引用) | EX0 | ET0 | EX1 | ET1 | ES | -- | -- | EA |
以上所有位都是高电平有效。因此想要开中断,先让全局中断EA=1,再让中断源所对应的位等于1就可以了。
2.中断的优先级
假如洗衣服时水开了,同时电话铃声也响了,先干哪件呢?这就涉及到中断的优先级。
中断的优先级由寄存器IP(interrupt priority)控制。
以上的设定仅仅有0,1两个状态,只是“相对优先”,不能保证绝对状态。未设置时几个同级状态同时发生时,采用以下的默认优先级排序:注意:本条只针对同时到达的同级中断有效。
假如外部中断1正在执行,然而外部中断0到了,在默认情况下并不会进行中断嵌套!要想实现中断嵌套,必须人为设置外部中断0的优先级高于外部中断1,也即前者优先级为1,后者优先级为0.假如同时设置两者优先级为1,则依然是按照中断到达的顺序执行。
3.定时器的设置
即便是设定好了中断的使能与优先级,定时器的四种工作方式和不同控制位的设定也需要操纵两个寄存器TMOD和TCON。
高四位控制寄存器T1,低四位控制寄存器T0。
- GATE
0:仅由运行控制位TRx(x = 0,1)来控制定时器/计数器运行。
1:用外中断引脚( INT0或 INT1)上的电平与运行控制位TRx共同来控制定时器/计数器运行。
- C/T’
0:为定时器工作模式,对单片机的晶体振荡器12分频后的脉冲进行计数。
1:为计数器工作模式,计数器对外部输入引脚T0(P3.4)或T1(P3.5)的外部脉冲(负跳变)计数。
M1/M2工作方式选择
M1 | M2 | 工作方式 |
---|---|---|
0 | 0 | 方式0,是13位定时器/计数器 |
0 | 1 | 方式1,是16位定时器/计数器 |
1 | 0 | 方式2,8位常数自动装入定时器/计数器 |
1 | 1 | 方式3,仅适用于T0,这时T0分为两个8位计数器,T1停止计数 |
TCON:
TF为溢出标志,TR为相应的计数器使能控制位。
例如,计数器0在工作状态1(16位计数器) 下,先把低四位依次记满,然后向高四位进一位,这样往复直到所有16位都被记满,置TF0=1,此时向CPU请求中断。这样就实现了隔一定时间触发一次时钟沿。
Chapter2.延时的精确计算
在明白如何实现延时之前,应该先了解一些周期概念。
1.单片机的四种周期
<1>时钟周期
也叫振荡周期,就是单片机外接时钟晶振频率的倒数。这是CPU的最基本工作脉冲。
<2>状态周期
时钟周期的两倍。
<3>机器周期
在一个机器周期内,单片机完成一项基本操作(不一定是执行完一条指令)。机器周期相当于6个状态周期即12个时钟周期。
<4>指令周期
执行一条指令所需的时间,一般包含1~4个机器周期。
以下假设单片机采用的晶振为12Mhz(可以查阅单片机参数手册),即可算出几种延时分别需要的时间。
2.粗糙延时:不涉及芯片内部定时器
有一种方法是非常粗糙的延时:利用机器周期执行指令的时间实现延时。此种方法不能精确算出,只能用调试功能设断点测出,非常原始也非常不推荐。
void delayms(uint xms)//延时约xms毫秒
{
uint i,j;
for(i=xms;i>0;i--)
for(j=110;j>0;j--)
}
注意i不能超出255。调试前注意检查晶振频率是否正确,之后设断点计算sec的间隔即可得出函数执行一次延长的时间。与很多偶然因素有关,很不精确。
3.定时器延时
更精确的办法是使用中断和定时器。中断相关的设置开好之后,我们就面临一个给定时器寄存器赋初值的问题。
假设使用定时器0,将其作为16位定时器,其中TH0默认为高八位,TL0默认为低八位。先计算初值为0时装满一次要多长时间:
时钟周期=1/12M 机器周期=(1/12M)*12=1us
记满需要2^16-1个数,还要一条脉冲计数器溢出才能实现中断,也就是2^16=65536。按每个机器周期执行一条指令算,溢出一次一共需要65536us约等于65.5ms。假如需要延时为1s,可以让计数器记满50ms就溢出,之后重复20次中断再执行下一条指令。则可以每一轮都给定时器赋值,使其记到50ms就溢出,相当于每轮少记15536个数。使得T0总共的初值为15536就可以了。因为TL0有八位,则计算式为:
TH0=15536/256 TL0=15536%256 每轮溢出之后计数器会重置,因此要再给计数器赋一遍这样的初值。
完整的代码模板为:
主函数:设置定时器工作方式->装填初值->开总中断->开相应定时器中断->启动相应定时器->用while语句等待(很重要!必须有等待的环节)
中断函数:声明有特定格式,为void T0_time() interrupt 1//这里的1为序号,不能错
重装初值->溢出次数++->假如溢出次数到了xxx,溢出次数清零,执行到点后的操作