什么是中断?
简介:让CPU打断正常运行的程序,转而去处理紧急的事件(程序),就叫中断。
中断优先级分组设置
ARM Cortex-M 使用了 8 位宽的寄存器来配置中断的优先等级,这个寄存器就是中断优先级来配置寄存器。
但STM32,只用了中断优先级配置寄存器的高4位 [7 : 4],所以STM32提供了最大16级的中断优先等级。
STM32 的中断优先级可以分为抢占优先级和子优先级。
抢占优先级: 抢占优先级高的中断可以打断正在执行但抢占优先级低的中断。
子优先级:当同时发生具有相同抢占优先级的两个中断时,子优先级数值小的优先执行。
注意:中断优先级数值越小越优先。
一共有 5 种分配方式,对应着中断优先级分组的 5 个组。
特点:
1、低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断里才允许调用FreeRTOS 的API函数。
2、建议将所有优先级位指定为抢占优先级位,方便FreeRTOS管理。
3、中断优先级数值越小越优先,任务优先级数值越大越优先。
中断相关寄存器
三个系统中断优先级配置寄存器,分别为 SHPR1、 SHPR2、 SHPR3 。
SHPR1寄存器地址:0xE000ED18。
SHPR2寄存器地址:0xE000ED1C。
SHPR3寄存器地址:0xE000ED20。
FreeRTOS如何配置PendSV和Systick中断优先级?
中断相关寄存器
三个中断屏蔽寄存器,分别为 PRIMASK、 FAULTMASK 和BASEPRI 。
FreeRTOS所使用的中断管理就是利用的BASEPRI这个寄存器。
BASEPRI:屏蔽优先级低于某一个阈值的中断。
比如: BASEPRI设置为0x50,代表中断优先级在5~15内的均被屏蔽,0~4的中断优先级正常执行。
BASEPRI:屏蔽优先级低于某一个阈值的中断,当设置为0时,则不关闭任何中断。
关中断程序示例:
中断优先级在5 ~ 15的全部被关闭 。
开中断程序示例:
FreeRTOS中断管理就是利用BASEPRI寄存器实现的 。
中断管理
在 RTOS 中,需要应对各类事件。这些事件很多时候是通过硬件中断产生,怎么处理呢?
假设当前系统正在运行 Task1 时,用户按下了按键,触发了按键中断。这个中断的处理流程如下:
CPU 跳到固定地址去执行代码,这个固定地址通常被称为中断向量,这个跳转时硬件实现。
执行代码做什么?
保存现场:Task1
被打断,需要先保存
Task1
的运行环境,比如各类寄存器的值。
分辨中断、调用处理函数
(
这个函数就被称为
ISR
,
interrupt service routine)
恢复现场:继续运行 Task1
,或者运行其他优先级更高的任务你要注意到,ISR
是在内核中被调用的,
ISR
执行过程中,用户的任务无法执行。
ISR要尽量快,否则:其他低优先级的中断无法被处理:实时性无法保证用户任务无法被执行:系统显得很卡顿。
如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为 2 部分:
ISR:尽快做些清理、记录工作,然后触发某个任务
任务:更复杂的事情放在任务中处理
所以:需要 ISR
和任务之间进行通信要在 FreeRTOS
中熟练使用中断,有几个原则要先说明:
FreeRTOS 把任务认为是硬件无关的,任务的优先级由程序员决定,任务何时运行由调度器决定。
ISR 虽然也是使用软件实现的,但是它被认为是硬件特性的一部分,因为它跟硬件密切相关。
何时执行?由硬件决定。
哪个 ISR
被执行?由硬件决定。
ISR 的优先级高于任务:即使是优先级最低的中断,它的优先级也高于任务。任务只有在没有中断的情况下,才能执行。
两套 API 函数
在任务函数中,我们可以调用各类 API
函数,比如队列操作函数:
xQueueSendToBack。但是在 ISR
中使用这个函数会导致问题,应该使用另一个函数:
xQueueSendToBackFromISR,它的函数名含有后缀"FromISR"
,表示
"
从
ISR
中给队列发送
数据
"
。
FreeRTOS 中很多
API
函数都有两套:一套在任务中使用,另一套在
ISR
中使用。后者的函数名含有"FromISR"
后缀。
为什么要引入两套 API 函数?
很多 API
函数会导致任务计入阻塞状态:
运行这个函数的任务进入阻塞状态。
比如写队列时,如果队列已满,可以进入阻塞状态等待一会。
ISR 调用
API 函数时,ISR 不是"任务",ISR 不能进入阻塞状态。
所以,在任务中、在 ISR
中,这些函数的功能是有差别的。
两套 API 函数列表
xHigherPriorityTaskWoken 参数
xHigherPriorityTaskWoken 的含义是:是否有更高优先级的任务被唤醒了。如果为pdTRUE,则意味着后面要进行任务切换。
还是以写队列为例。
任务 A
调用
xQueueSendToBack()
写队列,有几种情况发生:
队列满了,任务 A
阻塞等待,另一个任务
B
运行
队列没满,任务 A
成功写入队列,但是它导致另一个任务
B
被唤醒,任务
B 的优先级更高:任务
B
先运行
队列没满,任务 A 成功写入队列,即刻返回
可以看到,在任务中调用 API
函数可能导致任务阻塞、任务切换,这叫做
"context switch",上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换。
xQueueSendToBackFromISR()
函数也可能导致任务切换,但是不会在函数内部进行切
换,而是返回一个参数:表示是否需要切换,函数原型与用法如下:
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,BaseType_t *pxHigherPriorityTaskWoken
);
/* 用法示例 */
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
pxHigherPriorityTaskWoken 参数,就是用来保存函数的结果:是否需要切换
*pxHigherPriorityTaskWoken 等于
pdTRUE
:函数的操作导致更高优先级的任务就绪了,ISR
应该进行任务切换。
*pxHigherPriorityTaskWoken 等于
pdFALSE
:没有进行任务切换的必要
为什么不在"FromISR"
函数内部进行任务切换,而只是标记一下而已呢?为了效率!
示例代码如下:
void XXX_ISR()
{
int i;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(...); /* 被多次调用 */
}
}
ISR
中有可能多次调用
"FromISR"
函数,如果在
"FromISR"
内部进行任务切换,会浪费时间。解决方法是:
在"FromISR"
中标记是否需要切换。
在 ISR
返回之前再进行任务切换。
示例代码如下:
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */
}
/* 最后再决定是否进行任务切换 */
if (xHigherPriorityTaskWoken == pdTRUE)
{
/* 任务切换 */
}
}
上述的例子很常见,比如 UART
中断:在
UART
的
ISR
中读取多个字符,发现收到回车符时才进行任务切换。
在 ISR
中调用
API
时不进行任务切换,而只是在
"xHigherPriorityTaskWoken"
中标记一下,除了效率,还有多种好处:
效率高:避免不必要的任务切换。
让 ISR
更可控:中断随机产生,在
API
中进行任务切换的话,可能导致问题更复杂。
可移植性。
在 Tick
中断中,调用
vApplicationTickHook()
:它运行与
ISR
,只能使用 "FromISR"的函数
使用
"FromISR"
函数时,如果不想使用
xHigherPriorityTaskWoken
参数,可以设置为NULL。
怎么切换任务
FreeRTOS 的 ISR 函数中,使用两个宏进行任务切换:
portEND_SWITCHING_ISR
(
xHigherPriorityTaskWoken
);
portYIELD_FROM_ISR
(
xHigherPriorityTaskWoken
);
这两个宏做的事情是完全一样的,在老版本的 FreeRTOS
中,
portEND_SWITCHING_ISR
使用汇编实现
portYIELD_FROM_ISR
使用
C
语言实现
新版本都统一使用
portYIELD_FROM_ISR
。使用示例如下:
void XXX_ISR()
{
int i;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < N; i++)
{
xQueueSendToBackFromISR(..., &xHigherPriorityTaskWoken); /* 被多次调用 */
}
/* 最后再决定是否进行任务切换
* xHigherPriorityTaskWoken 为 pdTRUE 时才切换
*/
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
中断的延迟处理
前面讲过,ISR
要尽量快,否则: 其他低优先级的中断无法被处理:实时性无法保证,用户任务无法被执行:系统显得很卡顿。
如果运行中断嵌套,这会更复杂,ISR
越快执行约有助于中断嵌套。
如果这个硬件中断的处理,就是非常耗费时间呢?对于这类中断的处理就要分为 2
部分:
ISR:尽快做些清理、记录工作,然后触发某个任务。
任务:更复杂的事情放在任务中处理。
这种处理方式叫"
中断的延迟处理
"(Deferring interrupt processing)
,处理流程如下图所示:
t1:任务
1
运行,任务
2
阻塞。
t2:发生中断。
该中断的 ISR
函数被执行,任务
1
被打断。
ISR 函数要尽快能快速地运行,它做一些必要的操作
(
比如清除中断
)
,然后唤醒任务 2。
t3:在创建任务时设置任务
2
的优先级比任务
1
高
(
这取决于设计者
)
,所以ISR
返回后,运行的是任务
2
,它要完成中断的处理。任务
2
就被称为"deferred processing task",中断的延迟处理任务。
t4:任务
2
处理完中断后,进入阻塞态以等待下一个中断,任务
1
重新运行。
中断与任务间的通信
前面讲解过的队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。要注意的是,在 ISR
中使用的函数要有
"FromISR"
后缀。
资源管理(Resource Management)
在前面讲解互斥量时,引入过临界资源的概念。在前面课程里,已经实现了临界资源的互斥访问。
本章节的内容比较少,只是引入两个功能:屏蔽/
使能中断、暂停
/
恢复调度器。要独占式地访问临界资源,有 3
种方法:
公平竞争:比如使用互斥量,谁先获得互斥量谁就访问临界资源,这部分内容前面讲过。
谁要跟我抢,我就灭掉谁:
中断要跟我抢?我屏蔽中断。
其他任务要跟我抢?我禁止调度器,不运行任务切换。
屏蔽中断
屏蔽中断有两套宏:任务中使用、ISR
中使用:
任务中使用:
taskENTER_CRITICA()/taskEXIT_CRITICAL()
ISR 中使用:
taskENTER_CRITICAL_FROM_ISR()/taskEXIT_CRITICAL_FROM_ISR()
在任务中屏蔽中断
在任务中屏蔽中断的示例代码如下:
/* 在任务中,当前时刻中断是使能的
* 执行这句代码后,屏蔽中断
*/
taskENTER_CRITICAL();
/* 访问临界资源 */
/* 重新使能中断 */
taskEXIT_CRITICAL();
taskENTER_CRITICA()/taskEXIT_CRITICAL()
之间:
低优先级的中断被屏蔽了:优先级低于、等于
configMAX_SYSCALL_INTERRUPT_PRIORITY
高优先级的中断可以产生:优先级高于
configMAX_SYSCALL_INTERRUPT_PRIORITY
但是,这些中断
ISR
里,不允许使用
FreeRTOS
的
API
函数
任务调度依赖于中断、依赖于
API
函数,所以:这两段代码之间,不会有任务调度产生
这套
taskENTER_CRITICA()/taskEXIT_CRITICAL()
宏,是可以递归使用的,它的内部会记录嵌套的深度,只有嵌套深度变为 0
时,调用
taskEXIT_CRITICAL()
才会重新使能中断。
使用
taskENTER_CRITICA()/taskEXIT_CRITICAL()
来访问临界资源是很粗鲁的方法:
中断无法正常运行。
任务调度无法进行。
所以,之间的代码要尽可能快速地执行。
在 ISR 中屏蔽中断
要使用含有
"FROM_ISR"
后缀的宏,示例代码如下:
void vAnInterruptServiceRoutine( void )
{
/* 用来记录当前中断是否使能 */
UBaseType_t uxSavedInterruptStatus;
/* 在 ISR 中,当前时刻中断可能是使能的,也可能是禁止的
* 所以要记录当前状态, 后面要恢复为原先的状态
* 执行这句代码后,屏蔽中断
*/
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
/* 访问临界资源 */
/* 恢复中断状态 */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
/* 现在,当前 ISR 可以被更高优先级的中断打断了 */
}
taskENTER_CRITICA_FROM_ISR()/taskEXIT_CRITICAL_FROM_ISR()
之间:
低优先级的中断被屏蔽了:优先级低于、等于
configMAX_SYSCALL_INTERRUPT_PRIORITY。
高优先级的中断可以产生:优先级高于
configMAX_SYSCALL_INTERRUPT_PRIORITY。
但是,这些中断
ISR
里,不允许使用
FreeRTOS
的
API
函数。
任务调度依赖于中断、依赖于
API
函数,所以:这两段代码之间,不会有任务调度产生。
暂停调度器
如果有别的任务来跟你竞争临界资源,你可以把中断关掉:这当然可以禁止别的任务
运行,但是这代价太大了。它会影响到中断的处理。
如果只是禁止别的任务来跟你竞争,不需要关中断,暂停调度器就可以了:在这期间,中断还是可以发生、处理。使用这 2 个函数来暂停、恢复调度器:
/* 暂停调度器 */
void vTaskSuspendAll( void );
/* 恢复调度器
* 返回值: pdTRUE 表示在暂定期间有更高优先级的任务就绪了
* 可以不理会这个返回值
*/
BaseType_t xTaskResumeAll( void );
示例代码如下:
vTaskSuspendScheduler();
/* 访问临界资源 */
xTaskResumeScheduler();
这套
vTaskSuspendScheduler()/xTaskResumeScheduler()
宏,是可以递归使用的,它的内部会记录嵌套的深度,只有嵌套深度变为 0
时,调用
taskEXIT_CRITICAL()
才会重新使能中断。
FreeRTOS临界段代码保护
临界段代码保护简介
什么是临界段:临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段。
适用场合如:
临界段代码保护函数介绍
临界区是直接屏蔽了中断,系统任务调度靠中断,ISR也靠中断。
任务调度
开启任务调度器
vTaskStartScheduler() :
作用:用于启动任务调度器,任务调度器启动后, FreeRTOS 便会开始进行任务调度
该函数内部实现,如下:
1、创建空闲任务。
2、如果使能软件定时器,则创建定时器任务。
3、关闭中断,防止调度器开启之前或过程中,受中断干扰,会在运行第一个任务时打开中断。
4、初始化全局变量,并将任务调度器的运行标志设置为已运行。
5、初始化任务运行时间统计功能的时基定时器。
6、调用函数 xPortStartScheduler() 。
xPortStartScheduler()
作用:该函数用于完成启动任务调度器中与硬件架构相关的配置部分,以及启动第一个任务。
该函数内部实现,如下:
1、检测用户在 FreeRTOSConfig.h 文件中对中断的相关配置是否有误。
2、配置 PendSV 和 SysTick 的中断优先级为最低优先级。
3、调用函数 vPortSetupTimerInterrupt()配置 SysTick。
4、初始化临界区嵌套计数器为 0。
5、调用函数 prvEnableVFP()使能 FPU。
6、调用函数 prvStartFirstTask()启动第一个任务。
启动第一个任务
prvStartFirstTask () /* 开启第一个任务
vPortSVCHandler ()
注意:SVC中断只在启动第一次任务时会调用一次,以后均不调用。
出栈/压栈汇编指令详解
任务切换
注意:任务切换的过程在PendSV中断服务函数里边完成 。
PendSV中断是如何触发的?
1、滴答定时器中断调用。
2、执行FreeRTOS提供的相关API函数:portYIELD() 。
本质:通过向中断控制和状态寄存器 ICSR 的bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。
查找最高优先级任务
vTaskSwitchContext( ) /* 查找最高优先级任务 */
taskSELECT_HIGHEST_PRIORITY_TASK( ) /* 通过这个函数完成 */
前导置零指令
获取最高优先级任务的任务控制块
总结
以上就是FreeRTOS的中断管理、资源管理、任务调度的核心内容。我讲的是关键点,不过也很片面,内部实现过程更为精妙,读者如果水平较高,可以自己去研究一下,我就不去研究了,因为确实有点难,会到这一步就够了。