51单片机(STC89C52)的中断和定时器
STC89C51/STC89C52 Timer
- 内部不带振荡源, 必须外接晶振
- 采用11.0592MHz,或22.1184MHz,可方便得到串口通讯的标准时钟.
STC89和STC90系列为12T, STC11/STC12系列为1T, 也就是一个指令一个机器周期, 这些都需要外置晶振; STC15系列有内置晶振.
中断
中断允许控制寄存器 IE
字节地址A8H, CPU对中断系统所有中断以及某个中断源的开放和屏蔽是由中断允许寄存器IE控制的
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
---|---|---|---|---|---|---|---|
EA | — | ET2 | ES | ET1 | EX1 | ET0 | EX0 |
EA
(IE.7): 整体中断允许位, 1:允许ET2
(IE.5): T2中断允许位, 1:允许(for C52)ES
(IE.4): 串口中断允许位, 1:允许ET1
(IE.3): T1中断允许位, 1:允许EX1
(IE.2): 外部中断INT1允许位, 1:允许ET0
(IE.1): T0中断允许位, 1:允许EX0
(IE.0): 外部中断INT0允许位, 1:允许
52单片机一共有6个中断源, 它们的符号, 名称以及各产生的条件分别如下
- INT0 - 外部中断0, 由P3.2端口线引入, 低电平或下降沿引起
- INT1 - 外部中断1, 由P3.3端口线引入, 低电平或下降沿引起
- T0 - 定时器/计数器0中断, 由T0计数器计满回零引起
- T1 - 定时器/计数器1中断, 由T1计数器计满回零引起
- T2 - 定时器/计数器2中断, 由T2计数器计满回零引起 <--这个是52特有的
- TI/RI - 串行口中断, 串行端口完成一帧字符发送/接收后引起
定时器中断
51单片机内部共有两个16位可编程的定时器,即定时器T0和定时器T1, 52单片机内部多一个T2定时器. 它们既有定时功能,也有计数功能。可通过设置与它们相关的特殊功能寄存器选择启用定时功能还是计数功能. 这个定时器系统是单片机内部一个独立的硬件部分,它与CPU和晶振通过内部某些控制线连接并相互作用,CPU一旦设置开启定时功能后,定时器便在晶振的作用下自动开始计时,但定时器的计数器计满后,会产生中断。
定时器/计数器的实质是加1计数器(16位), 由高8位和低8位两个寄存器组成.
- TMOD: 定时器的工作方式寄存器,确定工作方式和功能
- TCON: 控制寄存器,控制T0,T1的启动和停止及设置溢出标志
代码例子
#include<reg52.h>
sbit led=P3^0;
/** 中断的设置,首先设置中断的触发方式,再设置开启终端,最后开启总中断。*/
void main() {
IT0=1; //设置外部中断0的触发方式为下降沿
EX0=1; //开启外部中断0
IT1=1; //设置外部中断1的触发方式为下降沿
EX1=1; //开启外部中断1
EA =1; //总中断开关
while(1) {
P0=0xaa;
P0=0xff;
}
}
void EX0_ISR(void) interrupt 0 {
led=~led;
}
void EX1_ISR(void) interrupt 2 { //外部中断1的中断在此为2!
led=~led;
}
代码例子二
#include <reg52.h>
// 定义I/0引脚名称
sbit led1=P1^1;
sbit led2=P1^2;
sbit led3=P1^3;
sbit led4=P1^4;
sbit P32=P3^2;
//全局变量及位标志定义
bit FINT0;
bit FINT1;
bit FT0;
bit FT1;
bit FT2;
unsigned char T0_10ms;
unsigned char T0_50ms;
unsigned char T0_100ms;
//函数声明
void int_0(); //外部中断0
void int_1(); //外部中断1
void timer_0(); //定时器中断1
void timer_1(); //定时器中断2
void serial_1(); //串行中断1
void serial_2(); //串行中断2
//用户函数声明
void initial(); //初始化
void main(){
initial();
while(1){
P32=0; //为了使按'取消'、'确定'键能够产生INT0及INT1中断
//led4=0; //上一句等价于此句
if(FINT0){ //中断0来到要做什么事情
FINT0=0;
led1=0; //INT0中断时点亮
led2=0;
led3=0;
led3=0; //可以在此设一个断点
}
if(FINT1){ //中断1来到要做什么事情
FINT1=0;
led1=1; //INT1中断时熄灭
led2=1;
led3=1;
}
if(FT0){
FT0=0;
if(++T0_10ms > 30){
T0_10ms=0;
//定时多少做什么事,未初始化里定时器尚未设置
}
}
}
}
void initial(){
EA=1; // CPU所有中断开(IE最高位MSB)
EX0=1; // INT0中断开
IT0=0; // INT0 0:低电平触发, 1:下降沿触发
EX1=1; // INT1中断开
IT1=0; // INT1 0:低电平触发, 1:下降沿触发
return;
}
//INT0中断 由P3.2引脚产生
void int_0() interrupt 0 using 0 {
FINT0=1;
}
//INT1中断 由P3.3引脚产生
void int_1() interrupt 2 using 1 {
FINT1=1;
}
//定时器0中断
void timer_0() interrupt 1 using 2 {
FT0=1;
}
//定时器1中断
void timer_1() interrupt 3 using 3 {
FT1=1;
}
//串行中断1
void serial_1() interrupt 4 { }
//定时器2中断
void timer_2() interrupt 5 {
FT2=1;
}
定时器
89C51有两个计数器T0和T1, 89C52还有一个定时器T2
定时器T0和T1
控制寄存器TCON
字节地址88H
, 位寻址8FH - 88H
位地址 | 8F | 8E | 8D | 8C | 8B | 8A | 89 | 88 |
---|---|---|---|---|---|---|---|---|
位符号 | TF1 | TR1 | TF0 | TR0 | IE1 | IT1 | IE0 | IT0 |
TF0
,TF1
: 计数溢出标志位, 当计数溢出时产生中断, 由硬件置1, 当转向中断服务时, 再由硬件自动清0. 计数溢出的标志位的使用有两种情况: 采用中断方式时作为中断请求标志位来使用; 采用查询方式时作为查询状态位来使用.TR0
,TR1
: 定时器运行控制位, 0:停止, 1:启动IE0
,IE1
: 外部中断请求标志位. 当CPU采样到P3.2
和P3.3
出现有效中断请求时, 此位由硬件置1, 在中断响应完成后转向中断服务时, 再由硬件自动清0.IT0
,IT1
: 外部中断请求信号方式控制位. 1:脉冲方式(后沿负跳有效), 0:电平方式(低电平有效), 此位由软件置1或0.
TF0(TF1)——计数溢出标志位
模式控制寄存器 TMOD
逐位定义的8位寄存器, 只能使用字节寻址, 字节地址为89H
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
---|---|---|---|---|---|---|---|
GATE | C/T | M1 | M0 | GATE | C/T | M1 | M0 |
GATE
: 门控位- GATE=0时, 仅用TCON中的
TRO
或TR1
为1, 就可以启动T0, T1 - GATE=1时, 不仅TCON中的
TRO
或TR1
为1, 且需要INT0/INT1也为高电平,才能工作. Enable Timer/Counter only when the INT0/INT1 pin is high and TR0/TR1 is set.
- GATE=0时, 仅用TCON中的
D0
,D1
,D2
,D3
: 为T0的设置D4
,D5
,D6
,D7
: 为T1的设置C/T
: 模式选择, 0:定时模式, 1:计数模式. 计数模式用于外部输入计数M0
,M1
: 工作方式选择, 一般使用都是采用16位的计时计数器
M1 | M0 | 工作方式 | 计数器模式 |
---|---|---|---|
0 | 0 | TMOD=0x00 | 13位计数器 (8192) 13-bit timer/counter, 8-bit of THx & 5-bit of TLx |
0 | 1 | TMOD=0x01 | 16位计数器 (65536) 16-bit timer/counter, THx cascaded with TLx |
1 | 0 | TMOD=0x02 | 自动重载8位计数器 8-bit timer/counter (auto-reload mode), TLx reload with the value held by THx each time TLx overflow |
1 | 1 | TMOD=0x03 | T0分为2个8位计数器,T1为波特率发生器. Split the 16-bit timer into two 8-bit timers i.e. THx and TLx like two 8-bit timer |
可以看到, TMOD=0x01 的情况下, the timer/counter is configured in 16-bit mode. To be specific, it’s counts all the way from 0x0000 to 0xffff. And this mode gives a maximum delay of 71.106 millisecond, again at a osc of 11.0592 MHz.
用11.0592MHz晶振的C52产生较精确的1秒定时中断, 下面的代码是基于SDCC的8052.h. 下面说明一下定时器初始值的计算
- 由晶振11.0592 MHz, 得到定时器时钟为 11.0592 / 12 = 0.9216 MHz,
- 因此1ms对应 921.6 个时钟周期,
- 因此50ms对应 46080 个时钟周期,
- 将其设为一次中断后, 20次中断就对应1s
- 代码中的13, 是用于扣减掉执行时产生的额外机器周期(machine cycles)
代码
#include <8052.h>
#define d_time (65536 - 46080 + 13 + 1)
const unsigned char tl = d_time;
const unsigned char th = d_time >> 8;
volatile unsigned char i = 0;
void main() {
TMOD= 0x01; //工作方式为16位定时器
TH0 = th; //计数寄存器高8位
TL0 = tl; //计数寄存器低8位
EA = 1; //允许中断
ET0 = 0x01; //允许T0中断
TR0 = 1; //启动T0
while(1);
}
void Timer0IRQ(void) __interrupt (1) // 中断处理函数 T0 -> 中断1
{
i++;
if(i > 20) {
P0_7 = (P0_7 == 1)? 0 : 1; //触发P0.7 LED闪烁
i = 1; // 注意这边不能初始化为0, 否则每次会多跑一个中断
}
TH0 = th; //计数寄存器高8 位重新载入
TL0 = tl; //计数寄存器低8 位重新载入
}
定时器T2
控制寄存器TCON2
字节地址0C8H, 可位寻址
CF | CE | CD | CC | CB | CA | C9 | C8 |
---|---|---|---|---|---|---|---|
TF2 | EXF2 | RCLK | TCLK | EXEN2 | TR2 | C/T2 | CP/RT2 |
溢出标志位 | 定时器外部标志 | 接收时钟标志 | 发送时钟标志 | 外部使能 | 启动、停止控制位 | 选择位 | 捕获重装标志 |
TF2
: T2溢出标志, T2溢出时置位并申请中断, 只能用软件清除, 但T2作为波特率发生器使用的时候, (即RCLK=1或TCLK=1), T2溢出时不对TF2置位.EXF2
: 当EXEN2=1时, 且T2EX引脚P1.0
出现负跳变而造成T2的捕获或重装的时候, EXF2置位并申请中断, EXF2也是只能通过软件来清除.RCLK
: 串行接收时钟标志, 只能通过软件的置位或清除. 选择T1或T2作为串行接收的波特率产生器, 0:选择T1
, 1:选择T2
.TCLK
: 串行发送时钟标志, 只能通过软件的置位或清除, 选择T1或T2作为串行发送的波特率产生器, 0:选择T1
, 1:选择T2
.EXEN2
: T2的外部允许标志, 只能通过软件的置位或清除- 0: 禁止外部时钟触发T2
- 1: 当T2未用作串行波特率发生器时, 允许外部时钟触发T2, 当T2EX引脚输入一个负跳变的时候,将引起T2的捕获或重装,并置位EXF2,申请中断.
TR2
: T2的启动控制标志, 0:停止T2, 1:启动T2C/T2
: T2的定时方式或计数方式选择位, 只能通过软件的置位或清除. 0:定时器方式, 1:计数器方式, 下降沿触发.CP/RT2
: 捕获/重装载标志, 只能通过软件的置位或清除.- 0: 重装载方式, 这时若T2溢出(EXEN2=0), 或者T2EX引脚
P1.0
出现负跳变(EXEN2=1), 将会引起T2重装载 - 1: 捕获方式, 这时若T2EX引脚
P1.0
出现负跳变(EXEN2=1), 将会引起T2捕获操作. - RCLK=1或TCLK=1时, CP/RT2控制位不起作用, 被强制工作于定时器溢出自动重装载模式.
- 0: 重装载方式, 这时若T2溢出(EXEN2=0), 或者T2EX引脚
模式控制寄存器T2MOD
字节地址0C9H, 不可位寻址
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 |
---|---|---|---|---|---|---|---|
-- | -- | -- | -- | -- | -- | T2OE | DCEN |
T2OE
: T2输出允许位, 当T2OE=1时, 允许时钟输出到P1.0
(仅对80C54/80C58有效)DCEN
: 向下计数允许位, DCEN=1允许T2向下计数, 否则向上计数.
使用 __nop();
精确定时
假如使用者想要产生精确的延迟时间,建议使用__nop()
函数来组合达成。__nop()
函数能够产生 1 个精确的 CPU 频率周期延迟时间。然而,由于 flash 的速度低于 CPU 的频率速度,在 CPU 内部有缓存优化的技术,编译程序也会自动针对程序做优化,造成__nop() 函数组合出来的时间会与预期的时间不同。因此,建议将程序放置于 SRAM 中执行,以避免优化造成的非预期延迟时间问题. 以产生 2 us 的延迟时间为例:
- CPU 频率= 32MHz => 1 CPU 频率周期花费 1/32000000 sec = 31.25 ns
- 2us 延迟时间 = 2000ns / 31.25 ns = 64 次 CPU 频率周期
由于执行一次 for 循环需要花费 5 个 CPU 频率周期的时间,因此可以使用以下的方式达到 2 us 的时间延迟
- 执行一次 for 循环需要 5 个 CPU 频率周期
- 执行一次
__NOP()
指令需要 1 个 CPU 频率周期 - 64 个 CPU 频率周期 =
8 * ( 5 ( for 循环 ) + 3 * ( __NOP() ) )
void Delay_Test_Function(void) {
for(i = 0; i < 8 ; i++) { /* Delay for 2 us. */
__NOP();
__NOP();
__NOP();
}
}
例子2, 执行一次 PA = 0 需花费 11 CPU 指令周期,这意味着 I/O 会持续 (64+11) * 31.25 ns = 2343.75 ns 的时间才进行转态。
void Delay_Test_Function(void) {
uint32_t i, DelayCNTofCPUClock = 8;
PA0 = 1;
for(i = 0; i < DelayCNTofCPUClock ; i++) { /* Delay for 2 micro seconds. */
__NOP();
__NOP();
__NOP();
}
PA0 = 0;
}
STC-ISP软件提供的示例代码
延时1ms
STC89Cxx, STC90Cxx
C语言
void Delay1ms() //@11.0592MHz
{
unsigned char i, j;
_nop_();
i = 2;
j = 199;
do
{
while (--j);
} while (--i);
}
汇编实现
DELAY1MS: ;@11.0592MHz
NOP ; 1周期
PUSH 30H ; 入栈2周期
PUSH 31H ; 入栈2周期
MOV 30H,#2 ; 3周期
MOV 31H,#194 ; 3周期
NEXT:
DJNZ 31H,NEXT ; 2周期
DJNZ 30H,NEXT ; 2周期
POP 31H ; 2周期
POP 30H ; 2周期
RET ; 2周期
这里说明一下执行逻辑:
- 保存30H和31H的值到栈中
- 分别写入十进制值2和194, 然后进入NEXT标下面的代码
DJNZ 31H,NEXT
这行, 会对31H减一后判断是否为0, 这里会执行194次直到值变为0DJNZ 30H,NEXT
到了这行, 会对30H减一后判断是否为0, 初始值为2, 减一后为1, 继续回到NEXT标DJNZ 31H,NEXT
这行, 31H已经归0了, 减一回到FF, 这里会执行256次直到值变为0DJNZ 30H,NEXT
到了这行, 会对30H减一后判断是否为0, 原值为1, 减一后为0, 往下执行- 30H和31H的值出栈, 要注意顺序, 先入后出
- 返回
- 总共的指令周期是1 + 2 + 2 + 3 + 3 + 2(194+256) + 22 + 2 + 2 + 2 = 921
- 对于11.0592MHz的晶振, 对应12T单片机的指令周期为0.9216MHz, 对应一个毫秒为921.6个周期, 921是很接近的一个值
STC11, STC12
C
void Delay1ms() //@11.0592MHz
{
unsigned char i, j;
_nop_();
i = 11;
j = 190;
do
{
while (--j);
} while (--i);
}
汇编实现
DELAY1MS: ;@11.0592MHz
NOP ; 1周期
NOP
NOP
NOP
PUSH 30H ; 入栈4周期
PUSH 31H ; 入栈4周期
MOV 30H,#9 ; 直接地址MOV, 3周期
MOV 31H,#148 ; 直接地址MOV, 3周期
NEXT:
DJNZ 31H,NEXT ; 5周期
DJNZ 30H,NEXT ; 5周期
POP 31H ; 出栈3周期
POP 30H ; 出栈3周期
RET ; 4周期
分析一下逻辑, 指令周期长度可以查看芯片手册的第五章, 指令系统
- 4个单周期NOP
- 30H和31H入栈
- 给30H赋值十进制9, 给31H赋值十进制148, 进入NEXT代码段
DJNZ 31H,NEXT
, 31H初始值为148, 减一后为147, 不等于0, 因此跳回NEXT, 这里要经过148次DJNZ 30H,NEXT
, 30H初始值为9, 减一后为8, 不等于0, 因此跳回NEXTDJNZ 31H,NEXT
, 31H初始值为00H, 减一后为FFH, 不等于0, 因此跳回NEXT, 这里要经过256次DJNZ 30H,NEXT
, 30H值为8, 减一后为7, 不等于0, 因此跳回NEXT- ...
- 30H和31H的值出栈, 注意顺序, 先入后出
- 返回
周期次数计算
- 4 + 4 + 4 + 3 + 3
- 5 * 148
- 5 * 256 * 8
- 5 * 9
- 3 + 3 + 4
- 总共是 11053, 接近晶振11.0592MHz的千分之一, 即11059.2个周期
参考
- C51 汇编写的延时函数说明及时钟频率 http://www.51hei.com/mcuteach/247.html
- Very helpful SDCC C51 code examples https://github.com/hungtcs-lab/8051-examples
- 8051 DJNZ Instruction https://www.refreshnotes.com/2016/02/8051-djnz-instruction.html
- https://www.keil.com/support/man/docs/is51/is51_djnz.htm