51 单片机【外部中断、定时器中断、回调函数】
51 单片机【外部中断、定时器中断、回调函数】
这里的外部中断类似监听器,时时刻刻监视某引脚的电平变化;这里的定时器中断类似于定时任务,可以定时执行某函数;这里将回调函数和中断结合起来,案例里有点设计模式的味道(忘了哪个了,也可能就是感觉,关于高层不能调用低层的解决),也有点函数式编程的味道。
1、中断(包括 外部中断 和 定时器中断,串口todo)
中断可以狭义地看作是监听。它可监听 如 按钮按下、定时器溢出、CPU内部某值的改变等。
以下都是以89C52的某个(大概不是所有)单片机为例,(我也不知道这个是哪个,用哪个就查看哪个的手册) 它共有8个中断,这8个中断包含 4个外部中断、3个计时器中断、1个串口中断。中断有四个优先级可供我们设置。
- 关于这8个中断。
- 串口中断 todo
- 外部中断。 太长了,放下面了,和 “中断” 同级了
- 定时器中断。 太长了,放下面了,和 “中断” 同级了
- 关于优先级。
- 优先级需要我们自己设置,没设置的话每个中断没有优先级,所以看作它们是同一个优先级,这时需要通过查询顺序区分先后。
- 优先级可以管控的情况分为两种,第一种——A中断正在执行,B中断触发了; 第二种——A和B中断同时触发了。这两种情况都会比较A和B的优先级进行区分谁去执行。
- 每一个当下没执行但已经触发了的中断到后来都会被执行的。
- 如果两个中断平级,则按查询顺序决定哪个中断先执行。
- 按 查询顺序(不是优先级)给中断排一下顺序: 外部中断0(INT0')、 定时器中断0(Timer0)、外部中断1(INT1')、定时器1(Timer1)、串口中断(UART)、定时器中断2(Timer2)、外部中断2(INT2')、外部中断3(INT3')。
- 通过查手册设置优先级。
2、外部中断
外部中断有4个,分别监视四个引脚(P32、P33、P42、P43)。
-
我们需要先配置外部中断,进行一系列的参数开启,才可以让它监视起来。 这里只记录一丢丢,具体操作需要查询数据手册。看下图,当我们想使用 INT0' 中断时,我们 要做的是 把 总开关 EA、管控 INT0' 的 EX0打开,然后通过 IT0控制是下降沿触发还是低电平触发即可。
外部中断配置起来简单,但要注意的是,有些寄存器的位在头文件里没有定义,可通过数据手册或下面的第三个图找到它的寄存器然后操作即可。
中断请求标志位写代码时用不到。
/** * 以0号中断为例 */ #include <STC89C5xRC.H> #define LED_0 P00 /** * @brief 初始化0号中断,这个函数需要被调用一次以开启中断 */ void Init_Int0(){ // 打开中断总开关 EA = 1; // 打开外部中断1开关 EX0 = 1; // 配置中断为下降沿触发 IT0 = 1; } /** * 0号中断触发后要执行的代码 */ void Func_Int0() interrupt 0 { // interrupt后面的数是啥看手册 LED_0 = ~LED_0; }
3、定时器中断。
关于配置定时器中断用到的寄存器位看下图 或者 从数据手册找。
定时器的触发条件是计数寄存器溢出时触发。
计数寄存器有两个,可通过模式决定如何使用这两个寄存器。修改M0和M1的值,如下第三张图片,或者查手册。
C/T、GATE、M1、M0 这四个位都在TMOD寄存器里,这个寄存器8bit,另外4bit操控另一个定时器的这四个位,这四个位如何配置看图或手册。
看代码吧
void Timer0_Init() {
// 打开中断开关
EA = 1;
// 打开定时器中断开关
ET0 = 1;
// 开启TR0,允许 第一个定时器 中断计数
TR0 = 1;
// 设置TMOD,TMOD是一个寄存器,它高四位和低四位分别控制两个不同的定时器
// 我们需要操作低四位的那个寄存器,高四位保持原态
TMOD &= 0xF0;
TMOD |= 0x01;
// timer0 设置为 1ms 中断
TL0 = T1MS;
TH0 = T1MS >> 8;
counter = 0;
}
总的来说一下,外部中断的配置需要 总开关EA,单个开关,下降沿/低电平的触发方式; 定时器中断配置需要 总开关EA,单个开关,TR0/1/2、控制电路的TMOD(也就是里面的位)、两个寄存器的值(通过它俩控制时间)
定时器的函数写在驱动层,即Dri层。
关于定时器的触发时间。
我们外部使用的时钟频率是11.0592MHZ,也就是每秒刷新11059200次。外部时钟频率与芯片内部时钟频率可选择12:1的转换或者6:1的转换,一般我们用12的;
当内部时钟走过 11059200/12/1000 个时钟频率时,过去了1ms;
已知定时器中的计数器溢出时中断触发,我们使用的01模式使用16位全都用来计数,所以当它计到65536时定时器触发;
所以如果我们想让定时器1ms触发一次,只需要初始化定时器时或者每次定时器触发后,把定时器中的计数器置为11059200/12/1000 ,从它走到溢出,刚好走过了1ms。
4、回调函数+定时中断
使用回调函数有个有点,就是可以在高一层定义回调,高一层不用调用低一层的代码,低一层直接调用高一层的回调来掉自己这一层的函数。
我们使用回调函数就是把我们想调用的函数套一层壳子,下面总结下这层壳子要怎么封装和之前写的定时器中断整合在一起。
-
在定时器的驱动头文件中定义函数指针的类型;
typedef void(*Timer0_Callback)(void);
-
在定时器驱动C文件中定义函数指针的数组,用来注册函数;
static Timer0_Callback s_timer0_callbacks[MAX_CALLBACK_COUNT];
-
在初始化定时器中将函数指针的数组置空;
// 初始化时函数指针的数组置空 for (i = 0; i < MAX_CALLBACK_COUNT; i++){ s_timer0_callbacks[i] = NULL; }
-
写注册函数的函数,供外部调用
u8 Dri_Timer0_RegisterCallback(Timer0_Callback callback) { u8 i; for (i = 0; i < MAX_CALLBACK_COUNT; i++) { // 注册过,返回 if (s_timer0_callbacks[i] == callback) return 1; } // 没注册过,注册 for (i = 0; i < MAX_CALLBACK_COUNT; i++) { if (s_timer0_callbacks[i] == NULL) { s_timer0_callbacks[i] = callback; return 1; } } return 0; }
-
通过定时器执行注册过的函数
void Dri_Timer0_Func() interrupt 1 { u8 i; // 更新计数器中的数,保证下次还是1ms后触发 TL0 = T1MS; // 这个T1MS是宏定义,(65536 - 11059200 / 12 / 1000) TH0 = T1MS >> 8; // 调用所有的回调函数 for(i = 0; i<MAX_CALLBACK_COUNT; i++){ if (s_timer0_callbacks[i]) s_timer0_callbacks[i](); } }
-
使用时,初始化定时器,调用注册回调函数将我们想实现的逻辑放进去就行了
// 代码略
定时器中断可以用来解决之前的延时问题,使用起来很灵活,可根据实际情况操作,可以代替延时函数实现按键消抖、数码管的计数等,通过回调函数可以对定时器中断作优化,也很灵活,需要多多练习。