RTOS内功修炼记(三)—— 内核到底是如何管理中断的?
内容导读:
第一篇文章讲述了任务的三大元素:任务控制块、任务栈、任务入口函数,并讲述了编写RTOS任务入口函数时三个重要的注意点。
- RTOS内功修炼记(一)—— 任务到底应该怎么写?
第二篇文章从任务如何切换开始讲起,引出RTOS内核中的就绪列表、优先级表,一层一层为你揭开RTOS内核优先级抢占式调度方法的神秘面纱。
- RTOS内功修炼记(二)—— 优先级抢占调度到底是怎么回事?
「建议先阅读上文,对RTOS内核的抢占式调度机制理解之后,再阅读本文也不迟。」
这篇文章将讲述RTOS内核到底是如何管理中断的?用户该如何编写中断处理函数?以及用户如何设置临界段?
1.知识点回顾 — 中断
1.1. 中断机制
中断机制是嵌入式系统实现「异步事件处理」的一个重要机制,概括的说可以分为三步:
- ① 外设产生中断请求(比如GPIO外部中断、串口中断、定时器中断等)
- ② CPU判断是否响应中断请求,如果响应,CPU停止执行当前程序,转而去执行中断处理程序(ISR);
- ③ 中断处理程序执行完毕,返回断点处,继续执行被中断前的程序;
在执行低优先级的中断处理程序时,如果CPU允许中断嵌套,则转而去执行更高优先级的中断处理程序,执行完毕依次返回,流程如下:
1.2. Cortex-M内核的中断管理
ARM Cortex-M 内核中有一个专门用于管理中断的外设——NVIC,全称Nested vectored interrupt controller,即嵌套向量中断控制器,用来决定中断的优先级。
NVIC在 ARM Conrtex-M 内核中,用一个 8 位的寄存器来配置,总共可以配置
级中断,但是 ST 公司在生产 STM32 的时候,发现一个小小的单片机根本用不了这么多,纯属浪费,所以将该寄存器的低 4 位 全部置0,只使用高 4 位来配置,这样一来 STM32 就只有
级中断。
简化为16级中断后,ST发现 STM32 内部这么丰富的外设,还是不方便配置,干脆人工给这4位来个分组,划分出了5个分组:
这5种中断分组规则是人为的,用哪种规则随意,之后按照设置的规则分配具体的优先级就行,STM32默认使用的规则是 NVIC_PriorityGroup_0 。
在程序执行过程中,STM32 判断中断优先级的规则为:
- 先判断抢占优先级,数字越小,优先级越高;
- 若抢占优先级相同,判断子优先级,同样,数字越小,优先级越高;
1.3. STM32 HAL库的中断处理
STM32提供的HAL库中提供了默认的中断处理函数,在stm32l4xx_it.c
中,以外部中断为例:
通过HAL库的实现机制,最终会调用到弱定义的回调函数:
__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
UNUSED(GPIO_Pin);
}
这就使用户不用编写额外的函数,「重新定义此回调函数」即可作为中断处理函数:
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { /* 判断哪个引脚触发了中断 */ switch(GPIO_Pin) { case GPIO_PIN_2: /* 处理GPIO2发生的中断 点亮LED*/ HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET); break; default: break; } }
2. RTOS内核对中断的管理
2.1. 中断设置有何变化?
中断使能设置和中断优先级设置,归根到底就是设置寄存器的值,你想它能有什么变化,裸机怎么玩,RTOS内核里面就怎么玩~
2.2. 中断处理有何变化?
在RTOS内核中,不再单纯的只有一个main函数在跑,而是系统中「同时存在多个任务」,由内核根据不同的任务优先级进行抢占式调度执行。
在第二篇文章中提到,RTOS内核中PendSV异常(任务切换/调度)的优先级被设为最低,有3个优点:
① 使得在「任何任务」的执行过程中,都可以被外设产生的中断请求所中断(假设中断都已经使能);
② 避免在中断处理程序中产生任务切换;
③ 使得中断处理程序可以按照中断优先级正常嵌套,不会受任务的影响;
这样一来,即使程序中有了RTOS内核,「从中断产生到执行中断处理程序的整个过程都和裸机程序没有什么不同」。
2.3. 中断返回有什么变化
在裸机程序中,中断处理程序(包括嵌套执行的)最后执行完都会返回main函数被中断处。
然而在RTOS中,因为同时存在多个任务,所以当中断处理程序执行完返回的时候,一脸懵逼,我该返回哪个任务???
其实也简单,且听我慢慢道来~
因为目前的RTOS内核都是「抢占式调度机制」,如果中断处理程序执行完毕后返回了原来的任务,而在就绪列表中存在更高优先级的任务,则违背了抢占式调度的规则。
所以需要用户在中断服务程序执行完毕即将退出的时候,调用tos_knl_irq_leave
函数,在此函数中「找出当前内核就绪列表中优先级最高的任务,直接切换过去执行,强行改变中断程序的正常返回路径」,以遵循“抢占式调度”的规则。
这种方法有个缺陷,当中断发生嵌套的时候,执行完最高优先级的中断处理程序就会跳出去(不去执行中断),漏掉所有的低优先级中断处理程序,这是非常危险的,如图:
为了解决这一问题,RTOS内核中也想了一个非常奇妙的方法,「设置一个全局变量来记录当前中断嵌套数量,当且仅当为0的时候,才跳出去执行最高优先级的任务,否则正常返回。」
接下来,上源码!
① 变量的定义
TencentOS-tiny中用来记录这一数值的全局变量在tos_global.c
中定义:
k_nesting_t k_irq_nest_cnt = (k_nesting_t)0;
其中k_nesting_t类型的定义如下:
typedef uint8_t k_nesting_t;
此变量的最大值为 K_NESTING_LIMIT_IRQ,表示TencentOS-tiny中允许的最大中断嵌套数量:
#define K_NESTING_LIMIT_IRQ (k_nesting_t)250u
② 变量的使用
当进入中断服务函数时,需要用户调用下面的API,将此变量的值+1:
__API__ void tos_knl_irq_enter(void)
{
if (!tos_knl_is_running()) {
return;
}
if (unlikely(k_irq_nest_cnt >= K_NESTING_LIMIT_IRQ)) {
return;
}
++k_irq_nest_cnt;
}
当退出中断服务函数的时候,调用下面的API,将此变量的值-1,如果变量值为0,则表示当前是最后一层中断,开始执行调度到系统中最高优先级任务的操作,否则直接返回:
__API__ void tos_knl_irq_leave(void)
{
TOS_CPU_CPSR_ALLOC();
if (!tos_knl_is_running()) {
return;
}
TOS_CPU_INT_DISABLE();
if (!knl_is_inirq()) {
TOS_CPU_INT_ENABLE();
return;
}
--k_irq_nest_cnt;
if (knl_is_inirq()) {
TOS_CPU_INT_ENABLE();
return;
}
if (knl_is_sched_locked()) {
TOS_CPU_INT_ENABLE();
return;
}
k_next_task = readyqueue_highest_ready_task_get();
if (knl_is_self(k_next_task)) {
TOS_CPU_INT_ENABLE();
return;
}
cpu_irq_context_switch();
TOS_CPU_INT_ENABLE();
}
细心的读者可能会发现,此函数中最后调用任务切换时,不是普通的cpu_context_switch
,而是cpu_irq_context_switch
。
这是因为在CPU调用中断处理程序的时候,原来被中断任务的上文保护已经自动完成了,不再需要保存上文的操作,所以这里只需要切换下文的操作即可。
总结一下就是:
「RTOS的中断处理程序不会返回原来被中断的任务,而是返回到系统中存在的最高优先级任务,只有中断处理程序的编写,与裸机程序有两点不同,其余一模一样」:
- ① 在进入之后调用一次
tos_knl_irq_enter
; - ② 在即将退出之前调用一次
tos_knl_irq_leave
;
比如在知识回顾中我编写的按键中断处理程序,如果改到TencentOS-tiny中,如下:
void EXTI2_IRQHandler(void)
{
tos_knl_irq_enter();
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2);
tos_knl_irq_leave();
}
3. RTOS中的临界段
临界段听着好像甚之牛逼,好高级,其实就是一个纸老虎~
在RTOS中有一些代码「在运行过程中不希望被中断程序所中断」,那么这段代码就叫做临界段。
编写临界段代码也非常简单粗暴:「在临界段代码前直接将中断关闭,在临界段代码之后将中断使能」。
比如在TencentOS-tiny中编写临界段代码的方法如下:
void task1_entry(void)
{
//……
/* 临界段开始,关中断 */
TOS_CPU_INT_DISABLE();
/*
....之间的代码称为临界段,不会被任何中断所打断
*/
/* 临界段结束,开中断 */
TOS_CPU_INT_ENABLE();
//……
}
4. 总结
按照以往的惯例,最后再来总结一下通过本文的学习,得到可以在编写程序中用到的点:
① 「RTOS中编写中断处理函数需要在进入后调用tos_knl_irq_enter
,在退出前调用tos_knl_irq_leave
」。
② 「RTOS中,较复杂的中断处理程序应该设计为一个任务,在中断处理程序中去激活/唤醒该任务执行」。
③ 禁止在临界段代码中调用各种API,会影响系统实时性,还可能会引起系统崩溃。