一、中断简介
1.1 中断介绍
中断:单片机执行主程序时,由于某个事件的原因,暂停主程序的执行,调用相应的中断处理程序处理该事件(中断服务函数,ISR:Interrupt service routine),处理完毕后再自动继续执行主程序的过程
CPU跳到固定地址去执行代码,这个固定地址通常被称为中断向量,这个跳转时硬件实现的
中断处理过程包含中断请求、中断响应、中断服务及中断返回四个步骤:
中断请求:中断源向CPU发出中断请求
中断响应:CPU允许中断,主程序或中断服务程序暂停,进入中断服务程序(ISR)
中断服务:执行中断服务程序的过程
中断返回:中断服务程序执行完毕后回到主程序或者次级别中断服务程序的过程
1.2 中断延迟处理
中断能打断任何优先级的任务的运行,因此中断一般用于处理比较紧急的事件,ISR执行的要快,否则:
(1)其他低优先级的中断无法被处理:实时性无法保证
(2)用户任务无法被执行:系统显得很卡顿
如果中断非常耗时间,那就需要分为两部分做:
(1)只做简单处理,例如标记该事件,然后触发某个任务
(2)任务:处理复杂且耗时的操作
在使用 FreeRTOS 系统时,ISR和任务之间进行通信一般使用消息队列、信号量、任务通知(只可以发送)或事件标志组等,将标志发给处理任务, 任务做具体处理
具体流程:
t1:任务1运行,任务2阻塞
t2:发生中断,该中断的ISR函数被执行,任务1被打断
ISR函数要尽快能快速地运行,它做一些必要的操作(比如清除中断),然后唤醒任务2
t3:在创建任务时设置任务2的优先级比任务1高(这取决于设计者),所以ISR返回后,运行的是 任务2,它要完成中断的处理。任务2就被称为"deferred processing task",中断的延迟处理任务。
t4:任务2处理完中断后,进入阻塞态以等待下一个中断,任务1重新运行
通过中断机制,在外设不需要 CPU 介入时,CPU 可以执行其他任务,而当外设需要 CPU 时通过产生中断信号使 CPU 立即停止当前任务转而来响应中断请求。这样可以使 CPU 避免把大量时间耗费在等待、查询外设状态的操作上,因此将大大提高系统实时性以及执行效率。
1.3 中断优先级分组
ARM Cortex-M 使用了 8 位宽的寄存器来配置中断的优先等级,但STM32,只用了中断优先级配置寄存器的高4位 [7 : 4],所以STM32提供了最大16级的中断优先等级
其中,STM32的中断分为抢占优先级和响应优先级、
抢占优先级:抢占优先级高的中断可以打断正在执行但抢占优先级低的中断
响应优先级:当同时发生具有相同抢占优先级的两个中断时,子优先级数值小的优先执行
注意:中断优先级数值越小,优先级越高
对于FreeRTOS而言:
(1)低于configMAX_SYSCALL_INTERRUPT_PRIORITY优先级的中断里才允许调用FreeRTOS 的API函数
(2)由于高优先级中断可以抢占低优先级中断,但如果存在子优先级,对于正在运行的中断(抢占优先级相同),子优先级高的中断无法打断正在运行的中断,对于全部设置为抢占式优先级,很容易区分中断优先级。为了方便FreeRTOS管理,分组方式建议
使用分组4:NVIC_PriorityGroupConfig( NVIC_PriorityGroup_4 ),全部设置为抢占优先级
(3)中断优先级数值越小越优先,任务优先级数值越大越优先
二、中断相关寄存器
2.1 中断优先级配置寄存器(配置PendSV:上下文切换、Systick:任务调度,中断优先级)
三个系统中断优先级配置寄存器(32位),分别为 SHPR1、 SHPR2、 SHPR3
SHPRx是一个 32 位的寄存器,只能通过字节访问,每 8 位控制着一个内核外设的中断优先级的配置。位 7:4 这高四位有效,所以可编程为 0 ~ 15
SHPR1寄存器地址:0xE000ED18
SHPR2寄存器地址:0xE000ED1C
SHPR3寄存器地址:0xE000ED20
下图所示,每个寄存器为8位,4个寄存器组成一个优先级寄存器SHPR
在FreeRTOS中,port.c文件中定义
(1)SHPR3寄存器的地址(包括PendSV、Systick)
#define portNVIC_SHPR3_REG ( *( ( volatile uint32_t * ) 0xe000ed20 ) )
#define portNVIC_PENDSV_PRI ( ( ( uint32_t )
configKERNEL_INTERRUPT_PRIORITY ) << 16UL )
#define portNVIC_SYSTICK_PRI ( ( ( uint32_t )
configKERNEL_INTERRUPT_PRIORITY ) << 24UL )
(2)在FreeRTOSConfig.h中定义了配置内核中断优先级:255(0xFF:1111 1111)即最低优先级,Cortex-M3只需关注高四位即可
#define configKERNEL_INTERRUPT_PRIORITY 255
所以(1)中的下面两句是将PendSV、 Systick优先级寄存器(8位)全设为1,除此之外,其余位均为0
(3) 由于SHPR3为32位寄存器,所以要按位或配置PendSV、Systick优先级寄存器,不改变其他寄存器的值,将其设置为最低优先级
portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;
※ 注:每开启任务调度器,FreeRTOS将PendSV和SysTick设置最低优先级,保证系统任务切换不会阻塞系统其他中断的响应(中断可以打断任务,但任务不可以打断中断)
2.2 中断屏蔽寄存器
三个中断屏蔽寄存器,分别为 PRIMASK、 FAULTMASK 和BASEPRI
1:关中断,0开中断
名称 | 描述 |
PRIMASK | 只有1位 1:关闭所有可屏蔽的中断,只剩下NMI和硬fault可以响应 0:不关闭中断(默认值) |
FAULTMASK | 只有1位 1:关闭所有中断,只剩下NMI可以响应 0:不关闭中断(默认值) |
BASEPRI | 最多9位,由表达优先级的位数决定,定义了被屏蔽优先级的阈值 设置某值:所有优先级数值大于等于此值的中断(即优先级低于此值的中断,包括此值的中断)都被屏蔽 0:不屏蔽任何终端(默认值) |
FreeRTOS所使用的中断管理就是利用的BASEPRI寄存器
BASEPRI:屏蔽优先级低于某一个阈值的中断,当设置为0时,则不关闭任何中断
比如: BASEPRI设置为0x50(中断优先级只有高四位有效),代表中断优先级在5~15内的均被屏蔽,0~4的中断优先级正常执行
2.2.1 关闭中断函数:
功能:将优先级值 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY(0x5F相当于0x50,优先级为5)的中断屏蔽
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 95 /* 优先级只有高四位有效 0x5F:0101 1111 */
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; //0x5F
__asm
{
msr basepri, ulNewBASEPRI
dsb
isb
}
}
2.2.2 打开中断函数
将BASEPRI寄存器置0,打开中断
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
}
三、临界区(critical,关键的、极重要的、评判的)
3.1 临界区简介
临界段:临界段代码也叫做临界区,是指那些必须完整运行,不能被打断的代码段
应用场合:
(1)需要严格按照时序初始化的外设:IIC、SPI等
(2)系统以及用户需求
中断和任务调度可以打断当前正在运行的程序,任务调度实质是通过PendSV(最低中断优先级)中断实现的。因此,若想当前程序不被打断,则关闭中断即可
实质:FreeRTOS 在进入临界段代码的时候需要关闭中断,当处理完临界段代码以后再打开中断
3.2 临界区相关API函数
进入临界区,中断、任务均不能打断临界区的代码
函数 | 描述 |
taskENTER_CRITICAL() | 任务级进入临界段 |
taskEXIT_CRITICAL() | 任务级退出临界段 |
taskENTER_CRITICAL_FROM_ISR() | 中断级进入临界段 |
taskEXIT_CRITICAL_FROM_ISR() | 中断级退出临界段 |
3.2.1 进入临界区函数
(1)taskENTER_CRITICAL()
void taskENTER_CRITICAL( void );
头文件:task.h
函数实现原理:
1、关闭 优先级值 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY中断
2、uxCriticalNesting实质就是一个嵌套计数器,为嵌套而用(nesting:v.嵌套;n.巢穴),在开启任务调度器函数中,将其初始化为0,为第一个函数作准备,每进入一次临界区,该嵌套计数器加1
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
(2) taskENTER_CRITICAL_FROM_ISR()
UBaseType_t taskENTER_CRITICAL_FROM_ISR( void );
头文件:task.h
函数实现原理:
1、保存关闭中断前,中断屏蔽寄存器的值
2、关闭 优先级值 ≥ configMAX_SYSCALL_INTERRUPT_PRIORITY中断
3、返回保存中断屏蔽寄存器的值
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
mrs ulReturn, basepri
msr basepri, ulNewBASEPRI
dsb
isb
}
return ulReturn;
}
所以:在使用中断级临界区函数时,需要定义uint32_t的变量,用来保存关闭中断前,中断屏蔽寄存器的值
3.2.2 退出临界区函数
(1)taskEXIT_CRITICAL()
头文件:task.h
函数实现原理:
1、每退出临界区一次,uxCriticalNesting嵌套计数器减1
2、直至uxCriticalNesting嵌套计数器为0,开启所关闭的中断
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
所以:函数必须成对出现
(2)taskEXIT_CRITICAL_FROM_ISR()
头文件:task.h
函数实现原理:
(1)将进入临界区时,屏蔽中断寄存器的值,赋给中断屏蔽寄存器
保持进入临界区以及退出临界区,中断屏蔽寄存器的状态是一样的
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
msr basepri, ulBASEPRI
}
3.3 函数调用格式
(1)任务级临界区调用格式 :
void vTask1( void * pvParameters )
{
for( ;; )
{
taskENTER_CRITICAL();
taskEXIT_CRITICAL();
}
}
(2)中断级临界区调用格式:
void vDemoISR( void )
{
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
}
3.4 临界区特点:
(1)成对使用,支持嵌套
(3)临界区必须保持非常短,否则将影响中断响应时间
(4)不得从临界区调用 FreeRTOS API 函数
3.5 任务调度器的恢复与挂起
函数 | 描述 |
vTaskSuspendAll() | 挂起任务调度器 |
xTaskResumeAll() | 恢复任务调度器 |
1、可以嵌套使用,必须成对出现
2、与临界区不一样的是,挂起任务调度器,不关闭中断
3、它仅仅是防止了任务之间的资源争夺,中断照样可以直接响应
4、挂起调度器的方式,适用于临界区位于任务与任务之间;既不用去延时中断,又可以做到临界区的安全
3.5.1 任务调度器挂起函数
void vTaskSuspendAll( void );
头文件:task.h
void vTaskSuspendAll( void )
{
portSOFTWARE_BARRIER(); //宏,没有实现
++uxSchedulerSuspended;
portMEMORY_BARRIER(); //宏,没有实现
}
本质:uxSchedulerSuspended加1
Q:为什么通过改变一个变量,就可以控制任务的切换呢?
答:uxSchedulerSuspended 初始值为paFALSE,变量值一旦改变,xTaskIncrementTick() 的返回值为pdFALSE,并不会进行任务切换
任务切换时通过PendSV中断实现的,而触发PendSV定义在Systick中断服务函数里,Systick中断服务函数原型如下:
void xPortSysTickHandler( void )
{
vPortRaiseBASEPRI();
{
if( xTaskIncrementTick() != pdFALSE ) //函数返回值不为pdFALSE,就会将PendSV悬起
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
vPortClearBASEPRIFromISR();
}
BaseType_t xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
traceTASK_INCREMENT_TICK( xTickCount );
//变量初始值为paFLASE,一旦调用任务调度器挂起函数,就不会等于pdFLSE
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
//其他操作;
}
else
{
++xPendedTicks;
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
return xSwitchRequired;
//返回xSwitchRequired,一旦调用任务调度器挂起函数,就返回pdFLSE
}
3.5.2 任务调度器恢复函数
BaseType_t xTaskResumeAll( void );
头文件:task.h
返回值:如果恢复调度器导致了上下文切换,则返回 pdTRUE,否则返回 pdFALSE
函数内部实现原理:
BaseType_t xTaskResumeAll( void )
{
TCB_t * pxTCB = NULL;
BaseType_t xAlreadyYielded = pdFALSE;
configASSERT( uxSchedulerSuspended );
//进入临界区
taskENTER_CRITICAL();
{
//变量--(此变量在任务调度器挂起函数中++)
--uxSchedulerSuspended;
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
//判断当前任务个数是否大于0
if( uxCurrentNumberOfTasks > ( UBaseType_t ) 0U )
{
//判断等待就绪列表是否为空
while( listLIST_IS_EMPTY( &xPendingReadyList ) == pdFALSE )
{
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( ( &xPendingReadyList ) );
listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
portMEMORY_BARRIER();
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
prvAddTaskToReadyList( pxTCB ); //添加到就序列表
if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority ) //将恢复的任务优先级与当前任务的优先级比较,若高,则将变量赋值为pdTURE,用于后续上下文切换
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
if( pxTCB != NULL )
{
prvResetNextTaskUnblockTime(); //更新下次阻塞的时间
}
{
TickType_t xPendedCounts = xPendedTicks; //补齐挂起任务调度器丢失的节拍数
if( xPendedCounts > ( TickType_t ) 0U )
{
do
{
if( xTaskIncrementTick() != pdFALSE )
{
xYieldPending = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
--xPendedCounts;
} while( xPendedCounts > ( TickType_t ) 0U );
xPendedTicks = 0;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
if( xYieldPending != pdFALSE ) //进行任务切换
{
#if ( configUSE_PREEMPTION != 0 )
{
xAlreadyYielded = pdTRUE;
}
#endif
taskYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
taskEXIT_CRITICAL();
return xAlreadyYielded;
}
恢复调度器流程图:
四、Systick、SVC、PendSV中断
Cortex-M处理器在设计之初就对操作系统的支持,ARM架构实现了多个特性保证了操作系统(比如FreeRTOS)的设计的方便和高效。
1.双堆栈指针:一个主堆栈指针MSP,一个进程(任务)堆栈指针PSP,MSP用于操作系统的内核,以及中断处理(使用主堆栈),PSP则用于任务栈。
2.SysTick定时器:大多操作系统需要一个硬件定时器来产生操作系统需要的滴答中断,作为整个系统的时基,实现任务调度器,任务时间管理以及其它系统例行维护
3.SVC和PendSV异常,这两种异常对于操作系统实现任务切换有着非常重要的作用。
4.M3有两个执行等级:特权级与用户级,在应用任务中处于用户级限制了任务的访问权限(禁止访问特殊功能寄存器和 NVIC中寄存器的),还可同存储包含单元(MPU)一起使用,进一步提高操作系统的安全性。
4.1 Systick中断
在 Cortex-M 系列中 Systick 是作为 FreeRTOS 的心跳时钟,是调度器的核心。系统是在 Systick 中进行上下文切换。一般默认心跳时钟为 1ms,进入 Systick 中断后,内核会进入处理模式进行处理。
在 Systick 中断处理中,系统会在 ReadList 就绪链表从高优先级到低优先找需要执行的任务,进行调度。如果有任务的状态发生了变化,改变了状态链表,就会产生一个 PendSV 异常,进入 PendSV 异常,通过改变进程栈指针(PSP)切换到不同的任务。
- 对于相同优先级的任务,每隔一个 Systick,运行过的任务被自动排放至该优先级链表的尾部(时间片调度)
- 用户也可以在线程模式下主动触发 PendSV,进行任务切换。
- 在 FreeRTOS 中 SVC 只使用了一次(M0 中没有使用),就是第一次。
- FreeRTOS 进入临界区是通过配置 BASEPRI 寄存器来进行的。
-
4.1.1 源码分析
4.1.1.1 初始化SysTick定时器
vPortSetupTimerInterrupt() —— port.c
功能:设置SysTick计时器,以需要的频率产生滴答中断
#define portNVIC_SYSTICK_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_CURRENT_VALUE_REG ( *( ( volatile uint32_t * ) 0xe000e018 ) )
#define portNVIC_SYSTICK_LOAD_REG ( *( ( volatile uint32_t * ) 0xe000e014 ) )
__weak void vPortSetupTimerInterrupt( void )
{
/* 计算配置滴答中断所需要的常数 */
/* 使能低功耗tickless模式 */
#if ( configUSE_TICKLESS_IDLE == 1 )
{
ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
#endif
/* 关闭滴答定时器,并清除控制状态寄存器以及当前数值寄存器,保证运行的准确性 */
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/**
* #define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL )
* #define portNVIC_SYSTICK_INT_BIT ( 1UL << 1UL )
* #define portNVIC_SYSTICK_ENABLE_BIT ( 1UL << 0UL )
*
* 将重装载数值设置为 72000 -1,即Systick中断频率设置为1000Hz
* 开启定时器,使用内核时钟,当重装载值数到0时,产生滴答中断
*/
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_CONFIG | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
4.1.1.2 流程图
4.1.1.2 SysTick中断服务函数
1、SysTick_Handler() —— stm32f10x_it.c
extern void xPortSysTickHandler(void);
//systick中断服务函数
void SysTick_Handler(void)
{
#if (INCLUDE_xTaskGetSchedulerState == 1 )
/**
* 获取调度器的状态
* 如果状态不是还未开始,则调用xPortSysTickHandler()
*/
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
#endif /* INCLUDE_xTaskGetSchedulerState */
}
2、xPortSysTickHandler() —— port.c
void xPortSysTickHandler( void )
{
/* 关中断,源码参考本章2.2.1 */
vPortRaiseBASEPRI();
{
/**
* 增加时钟计数器xTickCount的值,并获取xTaskIncrementTick() 的返回值
* 返回值为pdTRUE时代表需要进行任务切换
*/
if( xTaskIncrementTick() != pdFALSE )
{
/**
* 往中断控制及状态寄存器 ICSR(地址:0xE000_ED04)的 bit28 写 1
* 挂起一次 PendSV 中断触发 pendSV
*/
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
/* 打开中断 */
vPortClearBASEPRIFromISR();
}
3、xTaskIncrementTick() —— task.c
BaseType_t xTaskIncrementTick( void )
{
/**
* 定义:任务控制块指针变量pxTCB、xItemValue(存放列表项的值)
* xSwitchRequired(判断是否要进行切换)
*/
TCB_t * pxTCB;
TickType_t xItemValue;
BaseType_t xSwitchRequired = pdFALSE;
/**
* 每次发生tick中断时由可移植层调用。
* 增加tick值,然后检查新的tick值是否会导致任何任务被解除阻塞。
*/
// 该宏定义但未实现
traceTASK_INCREMENT_TICK( xTickCount );
/**
* uxSchedulerSuspended:全局变量,表示调度器是否被挂起
* 初始值:pdFALSE
* pdFALSE:没挂起,反之,则表示挂起
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
/**
* 增加一个时基节拍,并赋值给xConstTickCount,之后更新到时基计数器xTickCount中
*
* xTickCount:全局变量,初始值为0
*/
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
xTickCount = xConstTickCount;
/* 当 xConstTickCount = 0,说明发生了溢出 */
if( xConstTickCount == ( TickType_t ) 0U )
{
/**
* 交换延时列表指针pxDelayedTaskList和溢出列表指针pxDelayedTaskList所指向的列表
* xNumOfOverflows:溢出个数加1,初始值为0
* 更新下一任务的阻塞时间:
* (1)如果pxDelayedTaskList所指向延时列表为空,
则将xNextTaskUnblockTime = 最大值
* (2)不为空,将下一任务的阻塞时间更新到xNextTaskUnblockTime中
taskSWITCH_DELAYED_LISTS();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/**
* 如果系统节拍 xConstTickCount的值大于等于xNextTaskUnblockTime 的值,
* 表示有任务需要解除阻塞,因为xNextTaskUnblockTime 里保存的是下一个解除阻塞的时间点值
/
if( xConstTickCount >= xNextTaskUnblockTime )
{
for( ; ; )
{
/* 也就是说,有没有任务在等待调度 */
if( listLIST_IS_EMPTY( pxDelayedTaskList ) != pdFALSE )
{
/**
* 如果pxDelayedTaskList所指向延时列表为空,
* 则将xNextTaskUnblockTime = 最大值
*/
xNextTaskUnblockTime = portMAX_DELAY;
/* 跳出死循环 */
break;
}
else
{
/* 获取延时列表第一个列表项的拥有者,也就是下一个任务的任务控制块 */
pxTCB = listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
/* 将任务的状态列表的值赋值给 xItemValue */
xItemValue = listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) );
/**
* 判断这个值,这个值里面保存的是下一个解除阻塞态的任务对应的解除时间点,
* 判断延时时间是否到了,到了之后就移除延时列表
*/
if( xConstTickCount < xItemValue )
{
/**
* 延时时间还没到,需要更新xNextTaskUnblockTime 的值
* 用xItemValue来更新xNextTaskUnblockTime
*/
xNextTaskUnblockTime = xItemValue;
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 延时时间到了,将任务从延时列表中移除 */
listREMOVE_ITEM( &( pxTCB->xStateListItem ) );
/**
* 任务是否还在等待其他事件,如信号量、列队等,
* 如果是的话就将任务从相应的事件列表中移除,相当于等待事件超时退出
*/
if( listLIST_ITEM_CONTAINER( &( pxTCB->xEventListItem ) ) != NULL )
{
listREMOVE_ITEM( &( pxTCB->xEventListItem ) );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 将任务添加到就绪列表中 */
prvAddTaskToReadyList( pxTCB );
/* 使能抢占调度 */
#if ( configUSE_PREEMPTION == 1 )
{
/* 解除阻塞的任务的优先级 > 当前任务的优先级,进行任务切换 */
if( pxTCB->uxPriority > pxCurrentTCB->uxPriority )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
}
}
}
/* 使能抢占调度与时间片调度 */
#if ( ( configUSE_PREEMPTION == 1 ) && ( configUSE_TIME_SLICING == 1 ) )
{
/* 当前优先级的就绪列表中,列表长度 > 1,则进行任务切换*/
if( listCURRENT_LIST_LENGTH( &( pxReadyTasksLists[ pxCurrentTCB->uxPriority ] ) ) > ( UBaseType_t ) 1 )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
/* 使能钩子函数,执行时间片钩子函数vApplicationTickHook() */
#if ( configUSE_TICK_HOOK == 1 )
{
/* Guard against the tick hook being called when the pended tick
* count is being unwound (when the scheduler is being unlocked). */
if( xPendedTicks == ( TickType_t ) 0 )
{
vApplicationTickHook();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif
#if ( configUSE_PREEMPTION == 1 )
{
if( xYieldPending != pdFALSE )
{
xSwitchRequired = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_PREEMPTION */
}
/* 任务调度器被挂起 */
else
{
/* 全局变量,初始值为0,记录任务调度器挂起过程中的时钟节拍数 */
++xPendedTicks;
#if ( configUSE_TICK_HOOK == 1 )
{
vApplicationTickHook();
}
#endif
}
return xSwitchRequired;
}
4.2 SVC与PendSV中断
CortexM内核有多重模式的,主要分为特权模式和非特权模式。如果处于特权模式,可以修改各种内核寄存器,配置、开关中断。出于运行安全上的考虑,操作系统是希望用户运行其代码的时候是处于非特权模式,仅有操作系统来接管内核寄存器的控制。而中断触发后会将非特权模式切换为特权模式
SVC和PendSV指令的作用就是提供让非特权模式下的用户层代码可以进入特权模式的接口,也就是说:操作系统的入口是中断。
SVC(Supervisor Call,管理调用)和PendSV(Pendable Service Call,可挂起的服务调用),两者多用于操作系统的软件开发中
SVC 用于产生系统函数的调用请求。例如,。操作系统通常不让用户程序直接访问受保护的硬件资源,而是通过提供一些系统服务函数,让用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。
因此,当用户程序想要控制特定的硬件时,它就要产生一个SVC 异常,然后 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,完成用户程序请求的服务
PendSV(可悬起的系统调用),它和 SVC 协同使用。PendSV 则不同,它是可以像普通的中断一样被悬起的。OS 可以利用它“缓期执行”一个异常——直到其它高优先级的中断完成后才执行PendSV异常服务函数。这个中断触发允许被挂起,等没有优先级更高的中断需要运行的时候,才会触发PendSV中断,就是一个允许延时一会再执行的SVC。
利用该特性,若将PendSV设置为最低的异常优先级(任何中断都可以打断),可以让PendSV异常处理在所有其他中断处理任务完成后执行
,这对于上下文切换非常有用,也是各种OS设计中的关键
悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。悬起后,如果优先级不够高,则将缓期等待执行
在portmacro.h
中定义了中断控制状态寄存器:portNVIC_INT_CTRL_REG
= 0xe000ed04。
其Bit 28 为PENDSVSET: portNVIC_PENDSVSET_BIT
:PendSV悬起位
因此,只要在portYIELD()
或者xPortSysTickHandler()
中调用了:
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
![](https://img-blog.csdnimg.cn/direct/34d5717270944c3488c6dbee8bb1cf32.png)
Q1:为什么不使用Systick中断实现上下文切换?
(上面不是已经说SysTick中断已经设置最低优先级,为什么还能打断其他中断?
答:中断优先级分组同样适合内核中断,当内核中断与外部中断优先级设置最低时,内核中断的中断编号更低,即优先级更高,仍然会打断某些外部中断)
在OS设计中,需要在不同任务间切换,这一般被称作上下文切换,其通常在PendSV异常处理中执行,该异常可由SysTick 异常触发。在 FreeRTOS 中,每一次进入 Systick 中断,系统都会检测是否有新的进入就绪态的任务需要运行,如果有,则悬挂 PendSV 异常,来缓期执行上下文切换在上下文切换操作中需要:
注意:寄存器R0~ R3、R12、LR.返回地址(PC)以及xPSR的8个寄存器由硬件保存在栈里,寄存器R4~R11(用来保存局部变量)由我们自己保存在栈中
FreeRTOS启动调度器的时候,会调用void vTaskStartScheduler(void);
void vTaskStartScheduler(void)
{
pxCurrentTCB = &Task1TCB; //手动指定第一个运行的任务
if(xPortStartScheduler() != pdFALSE) //启动调度
{
//调度后不会进入这里
}
}
其中调用了BaseType_t xPortStartScheduler(void);
BaseType_t xPortStartScheduler(void)
{
//配置PendSV和SysTick中断优先级最低
portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI;
protNVIC_SYSPRI2_REG |= portVNIC_SYSTICK_PRI;
prvStartFirstTask(); //启动第一个任务,不再返回
return 0; //不会执行该行
}
其中又调用了_asm void prvStartFirstTask(void);
_asm void prvStartFirstTask(void) //_ASM为C中内嵌汇编的前缀声明
{
PRESERVE8 //8字节对齐
ldr r0, =0xE0000ED08 //加载SCB_VTOR寄存器地址到R0
ldr r0, [r0] //加载SCB_VTOR寄存器的值:0x00000000
ldr r0, [r0] //加载0X00000000地址上的内容,为向量表的起始地址即MSP的值
msr msp, r0 //R0存入MSP
cpsie i //开启中断
cpsie f //开启异常
dsb
isb
svc 0 //调用SVC
nop
nop
}
可以看到在第12行执行了一句:SVC 0,即产生一次SVC系统调用, 服务号0表示SVC中断
随后执行SVC中断服务函数_ASM void vPortSVCHandler(void);
__asm void vPortSVCHandler(void)
{
extern pxCurrentTCB //外部变量
PRESERVE8 //8字节对齐
ldr r3, pxCurrentTCB //加载TCB指针的地址
ldr r1, [r3] //加载TCB指针
ldr r0, [r1] //加载TCB指向的任务块到R0,任务快的第一成员是栈顶指针
ldmia r0!, {r4-r11} //将R0指向的值依次赋给{R4~R11}并自增地址
msr psp, r0 //寄存器R4~R11加载完后,将此时R0的值赋给PSP
isb
mov r0, #0 //R0 = 0
msr basepri, r0 //BASEPRI = 0 不屏蔽任何中断
orr r14, #0xd //R14|=0X0D使得硬件在退出时使用PSP完成出栈操作并返回后进入任务模式Thumb状态,在SVC中断服务里面使用的是MSP,ARM状态
bx r14 //异常返回,由于上一句指令切换到PSP,返回时会自动出栈。因此PSP指针自动将栈中剩下的内容加载到CPU寄存器:xPSR, PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务形参)。出栈操作完成后,PSP指向任务栈的栈顶。
}
在RTOS内核中,任务切换的原理是:手动触发PendSV异常,在PendSV异常服务函数中实现任务切换。
freeRTOS有两种方式触发PendSV异常,一种是通过调用portYIELD(), 另一种是在Systick_Handler()中时基增加出现上下文切换请求。
触发PendSV异常的方法在task.h中的
#define portYIELD() \
{ \
/*触发PendSV,产生上下文切换*/ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
}
以及port.c
中的
void xPortSysTickHandler( void )
{
/* 设置中断掩码,关中断 */
portDISABLE_INTERRUPTS();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE ) /* 检查就绪列表出现更高优先级的任务 */
{
/* 需要任务切换,产生PendSV中断 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}
执行后就会进入PendSV异常,在PendSV中断服务函数中实现上下文切换
stmdb和ldmia指令一般配对使用,stmdb用于将寄存器压栈,ldmia用于将寄存器弹出栈,作用是保存使用到的寄存器
__asm volatile void xPortPendSVHandler( void )
{
/*
1.产生PendSV中断,硬件自动保存栈帧到任务A的栈中
2.读取当前任务A的栈指针PSP,手动把一些寄存器压栈到当前任务栈。
3.把当前任务A栈顶指针保存到任务A的任务控制块中。
4.找到下一个任务B的任务控制块。(查找下一个优先级最高的就绪任务)
5.把任务B控制块的栈顶指针指向的数据弹出到寄存器中
6.更新PSP为任务B的栈顶指针。
7.跳出PendSV中断。
8.硬件自动弹出任务B栈中的栈帧。
*/
extern uxCriticalNesting;
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
mrs r0, psp //R0=PSP
isb
ldr r3, =pxCurrentTCB /* 获取pxCurrentTCB的地址 */
ldr r2, [r3] //将R3的值作为指针取内容,存到R2中,即得到栈顶指针的地址
stmdb r0!, {r4-r11} /* 将R4~R11入栈,保存寄存器 */
str r0, [r2] /* 将这个新的栈顶保存到TCB第一个成员中 */
stmdb sp!, {r0, r3} //入栈保存R3(即&pxCurrentTCB)和 R0
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
msr basepri, r0 //关闭中断
bl vTaskSwitchContext //在临界段切换就绪队列中优先级最高的任务,更新pxCurrentTCB
mov r0, #0 //R0清0
msr basepri, r0 //开启中断
ldmia sp!, {r3, r14} //从主堆栈中恢复寄存器R3和R14的值,此时SP使用的是MSP
ldr r1, [r3] //将R3存放的pxCurrentTCB的地址赋予R1
ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */
ldmia r0!, {r4-r11} /* Pop the registers. */
msr psp, r0 //PSP=R0,更新PSP使异常退出时PSP为基地址进行其他寄存器的自动出栈,如下图1
isb
bx r14 //系统以PSP作为SP指针出栈,把新任务的任务堆栈中剩下的内容加载到CPU寄存器:R0(任务形参)、R1、R2、R3、R12、R14(LR)、R15(PC)和xPSR,切换到新任务,如图2
nop
}
五、FreeRTOS中断相关配置以及总结
5.1 中断相关配置
(1)FreeRTOS可管理的最高中断优先级:
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 95
FreeRTOS可管理的最高中断优先级:中断优先级只用到高四位:bit4~bit7
95(0101 1111):相当于0x50,或者优先级5
191:相当于 0xb0, 或者 优先级 11
(2)最低中断优先级:
#define configLIBRARY_KERNEL_INTERRUPT_PRIORITY 15
由于FreeRTOS使用NVIC分组4方式,即所有位均为抢占优先级使用,范围:0~15
(3)配置系统内核中断优先级(主要为PendSV、Systick中断服务)
#define configKERNEL_INTERRUPT_PRIORITY 255
主要用来配置PendSV、Systick优先级寄存器,将其设置最低,保证系统任务切换不会阻塞系统其他中断的响应(中断可以打断任务,但任务不可以打断中断)
5.2 总结
FreeRTOS管理的中断可以使用函数名带有后缀FromISR的函数,同时可以被屏蔽;不在管理范围内的中断不可调用且不可被屏蔽
在中断服务函数中调度FreeRTOS的API函数需注意:
1、使用中断服务函数的优先级需在FreeRTOS所管理的范围内
2、任一中断都可以打断任何优先级的任务
3、在中断服务函数里边需调用FreeRTOS的API函数,必须使用带“FromISR”后缀的函数
4、NVIC分组方式选择:分组4