关于STC89C51/C52:
- 内部不带振荡源, 必须外接晶振
- 采用11.0592MHz,或22.1184MHz,可方便得到串口通讯的标准时钟.
- 如果使用12MHz, 对应定时器为12分频, 一个机器周期为1 微秒,便于做精确定时.
中断
中断允许控制寄存器 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 |
用11.0592MHz晶振的C52产生较精确的1秒定时中断, 下面的代码是基于SDCC的8052.h. 下面说明一下定时器初始值的计算
- 由晶振11.0592 MHz, 得到定时器时钟为 11.0592 / 12 = 0.9216 MHz,
- 因此1ms对应 921.6 个时钟周期,
- 因此50ms对应 46080 个时钟周期,
- 将其设为一次中断后, 20次中断就对应1s
代码
#include <8052.h>
const unsigned char th = (65536 - 46080) / 256;
const unsigned char tl = (65536 - 46080) % 256;
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:启动T2 -
C/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;
}
参考
- C51 汇编写的延时函数说明及时钟频率 http://www.51hei.com/mcuteach/247.html
- Very helpful SDCC C51 code examples https://github.com/hungtcs-lab/8051-examples