本篇文章由 VeriMake 旧版论坛中备份出的原帖的 Markdown 源码生成
原帖标题为:以 CH549 为例的 51 教程 | 图文 3. 时钟、定时器中断与外部中断
原帖网址为:https://verimake.com/topics/258 (旧版论坛网址,已失效)
原帖作者为:Xy(旧版论坛 id = 73,注册于 2020-10-15 13:03:26)
原帖由作者初次发表于 2021-08-19 15:38:26
📇 点此回到教程主索引
目录
时钟介绍
中断介绍
外部中断具体代码解析
定时器中断具体代码解析
时钟介绍
时钟是单片机的脉搏,是单片机的驱动源,单片机每执行一条指令都需要时钟信号。

分频器倍频器的概念
倍频器可以让原来的时钟信号频率倍增,分频器则是让时钟信号转变为原来的时钟信号频率的整数分之一例如1/2,1/3。
举个例子,24MHz的时钟信号2倍频则是48MHz ,2分频则是12MHz
下图为ch549的时钟系统与结构图

中断介绍
定时器中断介绍
定时器是一个16位寄存器,工作时每接收到一次脉冲信号,它都会在原来的寄存器保存的值加1,直到这个寄存器溢出则会触发中断,又称计数器。

定时器脉冲信号频率为系统时钟fsys的12分之一,板子默认的系统时钟fsys则是12Mhz,所以定时器频率为1Mhz,定时间隔为1us。

举例:若需定时1ms则需要定时器计数1000次,如果要让定时器在计数1000次的时候溢出则需要设定定时器的初值为最大值+1-1000,定时器的最大值为16位寄存器全取1,化为十进制则是2的16次方-1=65535,,而+1 则是因为定时器计数到最大值65535并未溢出,直到下一次计数才算溢出,因此设定的初值应该设为65536-1000 =64536.
中断是指计算机运行过程中,出现某些意外情况需主机干预时,机器能自动停止正在运行的程序并转入处理新情况的程序,处理完毕后又返回原被暂停的程序继续运行。
例如:你正在看电视,突然手机响了,你需要停止看电视,接电话,接完电话再回来看电视,这段过程就叫中断。
定时器中断就是指当定时器溢出的时候,触发中断的这一外设。
外部中断介绍
外部中断指某个端口满足设定的电平条件触发中断的外设。

中断优先级介绍
为了解决同时触发中断的问题,因此每个中断都存在中断优先级。
响应不同优先级中断的原则是:
CPU首先响应高优先级的中断请求;
如果优先级相同,CPU按查询次序响应排在前面的中断;
正在进行的中断过程不能被新的同级或低优先级的中断请求所中断;
正在进行的低优先级中断过程,能被高优先级中断请求所中断。

外部中断具体代码解析
/**
******************************************************************
* @file main.c
* @author xy
* @version V1.0
* @date 2020-9-29
* @brief 外部中断
******************************************************************
* @attention
* verimake 用于ch549例程
* 按下key1按键可以对小灯状态取反
******************************************************************
*/
#include <CH549_sdcc.H> //ch549的头文件,其中定义了单片机的一些特殊功能寄存器
#define led P2_2 //将单片机的P2.2端口定义为led
#define key1 P3_2 //将单片机的P3.2端口定义为key1
/********************************************************************
* 函 数 名 : delay
* 函数功能 : 延时函数
* 输 入 : 时间
* 输 出 : 无
********************************************************************/
void delay(long int i)
{
while(i--);
}
/********************************************************************
* TIPS:
* ch549外部中断包括外部中断0(P3.2)和外部中断1(P3.3)
* 初始化一个外部中断需要三步
* 第一步:设置跳变沿触发方式 例:IT0=0 选择外部中断0为低电平触发;IT0=1 选择外部中断0为下降边沿触发(IT1同理)
* 第二步:打开外部中断0或外部中断1的中断允许 例:EX0=1 打开INT0的中断允许;EX0=0 关闭INT0的中断允许(IT1同理)
* 第三步:打开总中断 例:EA=1 打开总中断;EA=0,关闭总中断
*********************************************************************/
/********************************************************************
* 函 数 名 : Int0Init()
* 函数功能 : 设置外部中断0
* 输 入 : 无
* 输 出 : 无
*********************************************************************/
void Int0Init()
{
//设置INT0
IT0=1; //跳变沿触发方式(下降沿)
EX0=1; //打开INT0的中断允许。
EA=1; //打开总中断
}
/********************************************************************
* 函 数 名 : Int1Init()
* 函数功能 : 设置外部中断1
* 输 入 : 无
* 输 出 : 无
*********************************************************************/
//void Int1Init()
//{
// //设置INT1
// IT1=1; //跳变沿触发方式(下降沿)
// EX1=1; //打开INT1的中断允许。
// EA=1; //打开总中断
//}
/*******************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
********************************************************************/
void main()
{
Int0Init(); //对外部中断0(P3.2)初始化
while(1); //死循环
}
/*******************************************************************
* 函 数 名 : Int0() __interrupt 0
* 函数功能 : 外部中断0的中断函数
* 输 入 : 无
* 输 出 : 无
* TIPS:1.Int0 可以修改成任意函数名
* 2.举例:外部中断1 void Int1() __interrupt 2
* 3.外部中断0 中断号0
* 定时器0中断 中断号1
* 外部中断1 中断号2
* 定时器1中断 中断号3
*******************************************************************/
void Int0() __interrupt 0 //外部中断0的中断函数
{
delay(120000); //延时消抖
if(key1==0)
{
led=!led; //LED状态取反
}
}
按键

当按键的一端连接单片机的一个引脚,另一端连接地,在按键被按下去的时候,引脚处就会出现上图这样的信号。
因此我们选择外部中断为下降沿触发的方式进入中断来达到控制led状态的目的。
代码的重要部分
初始化外部中断
/********************************************************************
* 函 数 名 : Int0Init()
* 函数功能 : 设置外部中断0
* 输 入 : 无
* 输 出 : 无
*********************************************************************/
void Int0Init()
{
//设置INT0
IT0=1; //跳变沿触发方式(下降沿)
EX0=1; //打开INT0的中断允许。
EA=1; //打开总中断
}
初始化分为三个步骤:
一. 设定触发方式,之前说过我们选择下降沿触发的方式因此将IT0设为1如果是低电平触发则设为0。
二. 打开外部中断0的中断允许,将EX0设为1。
三. 打开总中断,将EA 设为1。
中断函数
/*******************************************************************
* 函 数 名 : Int0() __interrupt 0
* 函数功能 : 外部中断0的中断函数
* 输 入 : 无
* 输 出 : 无
* TIPS:1.Int0 可以修改成任意函数名
* 2.举例:外部中断1 void Int1() __interrupt 2
* 3.外部中断0 中断号0
* 定时器0中断 中断号1
* 外部中断1 中断号2
* 定时器1中断 中断号3
*******************************************************************/
void Int0() __interrupt 0 //外部中断0的中断函数
{
delay(120000); //延时消抖
IE0=0;
if(key1==0)
{
led=!led; //LED状态取反
}
}
中断函数中分为两个步骤:
一. 消抖
在按键触发信号图中可以看到在第一个10ms中存在许多的下降沿如果忽视的话就会出现大量误判(一次按下,多次动作)。因此我们通过使用延时的方法只让第一个下降沿触发外部中断从而实现软件消抖。
二. 通过gpio的输入功能再次检测按键是否被成功按下,如果按下即可将板子上的led取反。
电路连接
将器材包中的按键连接P3.2与GND。

表现形式
按下按键后,led的状态改变。
定时器中断具体代码解析
/**
******************************************************************
* @file main.c
* @author xy
* @version V1.0
* @date 2020-9-29
* @brief 定时器中断
******************************************************************
* @attention
* verimake 用于ch549例程
* 使用定时器中断定时1s将小灯状态取反
******************************************************************
*/
#include <CH549_sdcc.H> //ch549的头文件,其中定义了单片机的一些特殊功能寄存器
#define led P2_2 //将单片机的P2.2端口定义为led
/********************************************************************
* TIPS:
* ch549定时器一共有四个分别为T0/T1/T2/Watch-Dog
* 此例程主要讨论T0/T1(定时器0/定时器1)
* 初始化一个定时器中断需要五步
* 第一步:设置定时器工作模式 模式0 TMOD=0x00 13 位定时/计数器
* 模式1 TMOD=0x01 16 位定时/计数器
* 模式2 TMOD=0x10 自动重载 8 位定时/计数器
* 第二步:设置定时器的初值,
* 注:1.默认设定下定时器T0/T1/T2/UART0/GPIOiFlashROM/iRAM/SFR使用的Fsys时钟信号为12MHz
* 2.T2MOD寄存器默认设定定时器T0/T1选标准时钟Fsys/12
* 3.因此计数器频率为1MHz,计数间隔为1us
* 举例 1ms的初值 1ms/1us=1000。也就是要计1000个数,初值=65535-1000+1(因为实际上计数器计数到66636才溢出)=64536=FC18H
* 设置 TH0=0XFC;//设置前八位为16进制的FC——11111100
* TL0=0X18;//设置后八位为16进制的18——00011000
* (定时器1同理)
* 第三步:打开定时器0或定时器1的中断允许 例:ET0=1 打开INT0的中断允许;ET0=0 关闭INT0的中断允许(定时器1同理)
* 第四步:打开总中断 例:EA=1 打开总中断;EA=0,关闭总中断
* 第五步:TCON里T0运行控制位
* TR0置1时,T0开始工作;TR0置0时,T0停止工作。TR0由程序置1或清0,控制定时/计数器的启动与停止
*********************************************************************/
/********************************************************************
* 函 数 名 : Timer0Init
* 函数功能 : 定时器0初始化
* 输 入 : 无
* 输 出 : 无
*************************************************** *****************/
void Timer0Init()
{
TMOD|=0x01;//设置定时器工作模式为模式1 TMOD|=0x01等同于TMOD=TMOD|0x01 使用或控制符防止此处的设置对前面的设置造成影响
//设置初值 目标为定时1ms 1ms/1us=1000。也就是要计数1000个数
//初值=65535-1000+1(+1是因为实际上计数器计数到65536才溢出)=64536=FC18H
TH0=0XFC;
//设置前八位为16进制的FC——11111100
TL0=0X18;
//设置后八位为16进制的18——00011000
ET0=1;//打开T0中断
EA=1;//打开总中断
TR0=1;//使得T0开始工作
}
/********************************************************************
* 函 数 名 : Timer1Init
* 函数功能 : 定时器1初始化
* 输 入 : 无
* 输 出 : 无
*************************************************** *****************/
//void Timer1Init()
//{
// TMOD|=0x01;//设置定时器工作模式为模式1 TMOD|=0x01等同于TMOD=TMOD|0x01 使用或运算符防止此处的设置对前面的设置造成影响。
// //设置初值 目标为定时1ms 1ms/1us=1000。也就是要计数1000个数。
// //初值=65535-1000+1(+1是因为实际上计数器计数到65536才溢出)=64536=FC18H
// TH1=0XFC;
//设置前八位为16进制的FC——11111100
// TL1=0X18;
//设置后八位为16进制的18——00011000
// ET1=1;//打开T1中断
// EA=1;//打开总中断
// TR1=1;//使得T1开始工作
//}
/*******************************************************************
* 函 数 名 : main
* 函数功能 : 主函数
* 输 入 : 无
* 输 出 : 无
********************************************************************/
void main()
{
Timer0Init();//定时器0初始化
while(1); //死循环
}
/*******************************************************************
* 函 数 名 : Timer0() __interrupt 1
* 函数功能 : 定时器0的中断函数
* 输 入 : 无
* 输 出 : 无
* TIPS:1.Timer0 可以修改成任意函数名
* 2.举例:定时器1的中断函数 void Timer1() __interrupt 3
* 3.外部中断0 中断号0
* 定时器0中断 中断号1
* 外部中断1 中断号2
* 定时器1中断 中断号3
*******************************************************************/
void Timer0() __interrupt 1
{
TH0=0XFC;
TL0=0X18;
//继续给定时器赋初值,定时1ms,初值为FC18H
static int i;//设置局部变量 i
i++;
if(i==1000)//每进入一次中断则是1ms,1000次则是1s,此时将led状态取反即可达到实验目的。
{
i=0;
led=!led; //led状态取反
}
}
代码的重要部分
初始化定时器中断
/********************************************************************
* 函 数 名 : Timer0Init
* 函数功能 : 定时器0初始化
* 输 入 : 无
* 输 出 : 无
*************************************************** *****************/
void Timer0Init()
{
TMOD|=0x01;//设置定时器工作模式为模式1 TMOD|=0x01等同于TMOD=TMOD|0x01 使用或控制符防止此处的设置对前面的设置造成影响
//设置初值 目标为定时1ms 1ms/1us=1000。也就是要计数1000个数
//初值=65535-1000+1(+1是因为实际上计数器计数到65536才溢出)=64536=FC18H
TH0=0XFC;
//设置前八位为16进制的FC——11111100
TL0=0X18;
//设置后八位为16进制的18——00011000
ET0=1;//打开T0中断
EA=1;//打开总中断
TR0=1;//使得T0开始工作
}
初始化分为五个步骤:
一. 设定定时器的工作模式常用的则是模式1,为16位的定时器。
二. 设定定时器的初值,目标为定时1ms,1ms/1us=1000。也就是要计数1000个数,初值=65535-1000+1(+1是因为实际上计数器计数到65536才溢出)=64536=FC18H。将FC赋值给TH0,18赋值给TL0这两个8位寄存器。
三. 打开定时器0的中断允许,设置ET0=1。
四. 打开总中断,设置EA=1。
五. 打开定时器0中断的门控使能位,设置TR0=1。
中断函数
/*******************************************************************
* 函 数 名 : Timer0() __interrupt 1
* 函数功能 : 定时器0的中断函数
* 输 入 : 无
* 输 出 : 无
* TIPS:1.Timer0 可以修改成任意函数名
* 2.举例:定时器1的中断函数 void Timer1() __interrupt 3
* 3.外部中断0 中断号0
* 定时器0中断 中断号1
* 外部中断1 中断号2
* 定时器1中断 中断号3
*******************************************************************/
void Timer0() __interrupt 1
{
TH0=0XFC;
TL0=0X18;
//继续给定时器赋初值,定时1ms,初值为FC18H
static int i;//设置局部变量 i
i++;
if(i==1000)//每进入一次中断则是1ms,1000次则是1s,此时将led状态取反即可达到实验目的。
{
i=0;
led=!led; //led状态取反
}
}
中断函数分为两个步骤:
一. 重新给定时器赋初值,因为触发定时器中断的时候定时器已经溢出了,因此为了让它可以继续定时1ms 我们需要重新将它的初值设为FC18H。
二. 为了达到每1s取反一次的效果,我们先设置个局部变量 i,并且每进入一次定时器中断的时候就将 i 加1,当他累加到1000的时候就将 led 的状态取反。
LED状态每1s改变一次。