ISR设计铁律:短、快、无阻塞。
在RTOS中,中断是硬件事件的实时响应机制。当中断发生时,CPU会立即暂停当前任务,转而执行中断服务例程(ISR)。中断处理的典型流程:
发生中断就得去处理,并且是越快越好,因此在freertos中:
处理阶段 | 执行环境 | 操作内容 | 时间要求 |
---|---|---|---|
ISR | 中断上下文 | 清除中断标志、记录事件、触发任务 | 微秒级(短) |
任务 | 任务上下文 | 复杂数据处理、业务逻辑、通信 | 毫秒级(可长) |
ISR虽然是软件实现的,但是却和硬件密切相关,何时调用、调用哪个ISR都是由硬件决定。
在 FreeRTOS 中,任务(Task)和中断服务例程(ISR)对操作系统的资源(如队列、信号量、定时器等)有不同的使用要求。这种差异源于 任务和中断的本质区别,需要设计两套独立的 API 来满足不同的需求,上面介绍中断的时候也说了,中断要避免阻塞,确保实时性,归根到底还是由于对阻塞行为的不同处理才需要设计两套API。
// 任务中向队列发送数据,若队列满则阻塞等待 100 ticks
xQueueSend(xQueue, &data, 100 / portTICK_PERIOD_MS);
以上面这个函数,对于任务来说, 当任务调用某个 API(如向队列发送数据)时,如果资源不可用(如队列已满),任务可以选择 阻塞等待(Block),直到资源可用或超时。 这能提高 CPU 利用率,任务主动让出 CPU,调度器可运行其他任务,这就适用于非实时要求性较高的场景。
// 错误!在 ISR 中使用任务 API,可能导致不可预知行为
void ISR_Handler() {
xQueueSend(xQueue, &data, 0); // 阻塞参数无意义,但函数内部可能包含阻塞逻辑
}
而如果中断的处理函数中使用了任务版本的API,就像上面的代码,这可能会导致了一些不好的结果,毕竟 ISR 运行在中断上下文中,无法被调度器管理,阻塞或是其他情况会导致系统崩溃,比如看看xQueueSend
函数的内部实现,以阻塞参数为0为前提,只展示部分代码:
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
BaseType_t xQueueSemaphoreTake( QueueHandle_t xQueue,
TickType_t xTicksToWait )
{
//省略
for( ; ; )
{
taskENTER_CRITICAL();
{
/* Semaphores are queues with an item size of 0, and where the
* number of messages in the queue is the semaphore's count value. */
const UBaseType_t uxSemaphoreCount = pxQueue->uxMessagesWaiting;
/* Is there data in the queue now? To be running the calling task
* must be the highest priority task wanting to access the queue. */
if( uxSemaphoreCount > ( UBaseType_t ) 0 )
{
traceQUEUE_RECEIVE( pxQueue );
/* Semaphores are queues with a data size of zero and where the
* messages waiting is the semaphore's count. Reduce the count. */
pxQueue->uxMessagesWaiting = uxSemaphoreCount - ( UBaseType_t ) 1;
#if ( configUSE_MUTEXES == 1 )
{
if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )
{
/* Record the information required to implement
* priority inheritance should it become necessary. */
pxQueue->u.xSemaphore.xMutexHolder = pvTaskIncrementMutexHeldCount();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configUSE_MUTEXES */
/* Check to see if other tasks are blocked waiting to give the
* semaphore, and if so, unblock the highest priority such task. */
if( listLIST_IS_EMPTY( &( pxQueue->xTasksWaitingToSend ) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &( pxQueue->xTasksWaitingToSend ) ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
mtCOVERAGE_TEST_MARKER();
}
taskEXIT_CRITICAL();
return pdPASS;
}
else
{
if( xTicksToWait == ( TickType_t ) 0 )
{
/* For inheritance to have occurred there must have been an
* initial timeout, and an adjusted timeout cannot become 0, as
* if it were 0 the function would have exited. */
#if ( configUSE_MUTEXES == 1 )
{
configASSERT( xInheritanceOccurred == pdFALSE );
}
#endif /* configUSE_MUTEXES */
/* The semaphore count was 0 and no block time is specified
* (or the block time has expired) so exit now. */
taskEXIT_CRITICAL();
traceQUEUE_RECEIVE_FAILED( pxQueue );
return errQUEUE_EMPTY;
}
else if( xEntryTimeSet == pdFALSE )
{
/* The semaphore count was 0 and a block time was specified
* so configure the timeout structure ready to block. */
vTaskInternalSetTimeOutState( &xTimeOut );
xEntryTimeSet = pdTRUE;
}
else
{
/* Entry time was already set. */
mtCOVERAGE_TEST_MARKER();
}
}
}
taskEXIT_CRITICAL();
}
即使xTicksToWait为0,会跳过阻塞逻辑: if( xTicksToWait == ( TickType_t ) 0 ), return errQUEUE_EMPTY;返回一个错误,但是这部分代码调用到了如 taskENTER_CRITICAL()
等任务级代码,这些宏不兼容 ISR 环境,可能导致内核状态混乱。而freertos内核其实也没那么傻,假设我们真在中断中调用了,内核内部也有相应的措施,将taskENTER_CRITICAL()
继续展开可以看到以下定义:
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
/* This is not the interrupt safe version of the enter critical function so
* assert() if it is being called from an interrupt context. Only API
* functions that end in "FromISR" can be used in an interrupt. Only assert if
* the critical nesting count is 1 to protect against recursive calls if the
* assert function also uses a critical section. */
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK == 0
,检查 CPU 的 中断活跃状态寄存器,若值非零,表示当前正在执行中断服务程序(ISR),触发断言,进行报错。官方在里面也给了注释了。
因此为了能在中断中去使用像队列、信号量等这些功能,也就提供了另一套api,也就是相比较于任务的多了以FromISR为结尾的函数。
那为什么不直接使用同一套代码,在代码内部直接添加类似于if else的条件语句判断是任务上下文还是中断上下文不就好了?????
避免运行时上下文判断:
如果统一使用一套 API,函数内部需通过 if (is_in_isr())
判断当前是任务还是 ISR 上下文。这种分支判断会:
// 假设统一 API(伪代码)
BaseType_t xQueueSend(...) {
if (is_in_isr()) {
// ISR 逻辑:无阻塞,直接返回
} else {
// 任务逻辑:可能阻塞
}
}
// FreeRTOS 实际设计:分离为两套 API
BaseType_t xQueueSend(...) // 任务专用,含阻塞逻辑
BaseType_t xQueueSendFromISR(...) // ISR 专用,无阻塞
对于任务是需要有阻塞参数的,但是对于中断则是不需要的,因此设置两套API还可以简化参数, 如果强行合并 API,参数列表会变得臃肿,且某些参数在特定上下文中无效。
并且某些处理器(如低端 MCU)无法通过软件简单判断当前是否在 ISR 中,需依赖特殊指令或寄存器读取。两套 API 的设计避免了这种硬件依赖,简化了 FreeRTOS 的移植。
当使用第三方库时,如果该库内部调用了 FreeRTOS API,但未区分任务和 ISR 上下文,可能导致潜在风险。可以这么解决:
(1) 推迟中断处理(Deferred Interrupt Processing)
void ISR_Handler() {
// 在 ISR 中发送通知到队列
xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);
}
void DeferredTask() {
while (1) {
xQueueReceive(xQueue, &data, portMAX_DELAY);
ThirdParty_Library_SendData(data); // 在任务中调用第三方库
}
}
(2) 强制使用 FromISR API
FromISR
版本(需谨慎验证)。// 第三方库原代码
xSemaphoreGive(xSemaphore);
// 修改为
xSemaphoreGiveFromISR(xSemaphore, NULL); // 假设在任务中调用也允许
类型 | 在任务中 | 在ISR中 |
---|---|---|
队列(queue) | xQueueSendToBack |
xQueueSendToBackFromISR |
xQueueSendToFront |
xQueueSendToFrontFromISR |
|
xQueueReceive |
xQueueReceiveFromISR |
|
xQueueOverwrite |
xQueueOverwriteFromISR |
|
xQueuePeek |
xQueuePeekFromISR |
|
事件组(event group) | xEventGroupSetBits |
xEventGroupSetBitsFromISR |
xEventGroupGetBits |
xEventGroupGetBitsFromISR |
|
任务通知(task notification) | xTaskNotifyGive |
vTaskNotifyGiveFromISR |
xTaskNotify |
xTaskNotifyFromISR |
|
软件定时器(software timer) | xTimerStart |
xTimerStartFromISR |
xTimerStop |
xTimerStopFromISR |
|
xTimerReset |
xTimerResetFromISR |
|
xTimerChangePeriod |
xTimerChangePeriodFromISR |
ISR版本的函数,除了名字不同,关键就在于pxHigherPriorityTaskWoken
参数。先以任务中以队列举个例子,任务A调用 xQueueSendToBack()
写队列,有几种情况发生:
可以看到,在任务中调用API函数可能导致任务阻塞、任务切换,这叫做"context switch",上下文切换。这个函数可能很长时间才返回,在函数的内部实现了任务切换,来看一下xQueueSendToBack
函数内部的实现就知道了:
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
BaseType_t xQueueGenericSend( QueueHandle_t xQueue,
const void * const pvItemToQueue,
TickType_t xTicksToWait,
const BaseType_t xCopyPosition )
{
BaseType_t xEntryTimeSet = pdFALSE, xYieldRequired;
TimeOut_t xTimeOut;
Queue_t * const pxQueue = xQueue;
/*---------------------- 参数合法性检查 ----------------------*/
configASSERT( pxQueue ); // 队列句柄必须有效
configASSERT(!( (pvItemToQueue == NULL) && (pxQueue->uxItemSize != 0) )); // 数据指针非空检查
configASSERT(!( (xCopyPosition == queueOVERWRITE) && (pxQueue->uxLength != 1) )); // 覆盖模式必须为队列长度1
#if ( ( INCLUDE_xTaskGetSchedulerState == 1 ) || ( configUSE_TIMERS == 1 ) )
configASSERT(!( (xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED) && (xTicksToWait != 0) )); // 调度器挂起时不可阻塞
#endif
/*---------------------- 主循环尝试写入队列 ----------------------*/
for( ; ; )
{
taskENTER_CRITICAL(); // 进入临界区(关中断)
{
/* 检查队列是否有空间或允许覆盖写入 */
if( ( pxQueue->uxMessagesWaiting < pxQueue->uxLength ) || ( xCopyPosition == queueOVERWRITE ) )
{
traceQUEUE_SEND( pxQueue ); // 追踪队列发送事件
/*---------------------- 数据写入队列 ----------------------*/
#if ( configUSE_QUEUE_SETS == 1 )
{
// 队列集(Queue Set)相关逻辑
const UBaseType_t uxPreviousMessagesWaiting = pxQueue->uxMessagesWaiting;
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
if( pxQueue->pxQueueSetContainer != NULL )
{
if( (xCopyPosition == queueOVERWRITE) && (uxPreviousMessagesWaiting != 0) )
{
// 覆盖写入且队列非空时,不通知队列集
mtCOVERAGE_TEST_MARKER();
}
else if( prvNotifyQueueSetContainer( pxQueue ) != pdFALSE )
{
// 队列集通知成功,触发任务切换(高优先级任务就绪)
queueYIELD_IF_USING_PREEMPTION(); // 任务切换点1 ⚡
}
}
else
{
// 检查是否有任务等待接收数据
if( listLIST_IS_EMPTY( &(pxQueue->xTasksWaitingToReceive) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &(pxQueue->xTasksWaitingToReceive) ) != pdFALSE )
{
// 唤醒的任务优先级更高,触发任务切换
queueYIELD_IF_USING_PREEMPTION(); // 任务切换点2 ⚡
}
}
else if( xYieldRequired != pdFALSE )
{
// 互斥锁释放顺序异常时的特殊切换
queueYIELD_IF_USING_PREEMPTION(); // 任务切换点3 ⚡
}
}
}
#else /* 非队列集模式 */
{
xYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
if( listLIST_IS_EMPTY( &(pxQueue->xTasksWaitingToReceive) ) == pdFALSE )
{
if( xTaskRemoveFromEventList( &(pxQueue->xTasksWaitingToReceive) ) != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION(); // 任务切换点4 ⚡
}
}
else if( xYieldRequired != pdFALSE )
{
queueYIELD_IF_USING_PREEMPTION(); // 任务切换点5 ⚡
}
}
#endif /* configUSE_QUEUE_SETS */
taskEXIT_CRITICAL(); // 退出临界区(开中断)
return pdPASS; // 发送成功
}
else /* 队列已满 */
{
if( xTicksToWait == 0 )
{
taskEXIT_CRITICAL();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL; // 不阻塞,直接返回队列满错误
}
else if( xEntryTimeSet == pdFALSE )
{
vTaskInternalSetTimeOutState( &xTimeOut ); // 初始化超时计时器
xEntryTimeSet = pdTRUE;
}
}
}
taskEXIT_CRITICAL();
/*---------------------- 队列已满时的阻塞处理 ----------------------*/
vTaskSuspendAll(); // 挂起调度器
prvLockQueue( pxQueue ); // 锁定队列(防止并发操作)
if( xTaskCheckForTimeOut( &xTimeOut, &xTicksToWait ) == pdFALSE )
{
if( prvIsQueueFull( pxQueue ) != pdFALSE )
{
traceBLOCKING_ON_QUEUE_SEND( pxQueue );
vTaskPlaceOnEventList( &(pxQueue->xTasksWaitingToSend), xTicksToWait ); // 将任务挂起到发送等待列表
prvUnlockQueue( pxQueue );
if( xTaskResumeAll() == pdFALSE )
{
portYIELD_WITHIN_API(); // 恢复调度器后立即触发切换 ⚡任务切换点6
}
}
else
{
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll(); // 队列不再满,重试发送
}
}
else /* 超时处理 */
{
prvUnlockQueue( pxQueue );
( void ) xTaskResumeAll();
traceQUEUE_SEND_FAILED( pxQueue );
return errQUEUE_FULL;
}
}
}
进行任务切换的调用:queueYIELD_IF_USING_PREEMPTION
,portYIELD_WITHIN_API
,可以看出是在函数的内部中去直接调用相关代码实现任务的切换。
但是,对于FormISR版本的函数,是不会在内部去实现任务的切换的,回到前面讲的参数:pxHigherPriorityTaskWoken
, 标记是否因操作(如发送队列、释放信号量)导致 更高优先级任务就绪。若为 pdTRUE
,表示需要手动触发任务切换。
上面讲到了xQueueSendToBack
,而xQueueSendToBackFromISR()
函数也可能导致任务切换,但是不会在函数内部进行切换,而是返回一个参数,也就是pxHigherPriorityTaskWoken
,表示传给宏portYIELD_FROM_ISR
是否需要切换,函数原型与用法如下:
BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 必须初始化为 pdFALSE
// 在 ISR 中发送数据到队列
xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);
// 检查是否需要任务切换
if (xHigherPriorityTaskWoken == pdTRUE) {
portYIELD_FROM_ISR(pdTRUE); // 触发任务切换
}
为什么不在 FromISR
函数内部直接切换任务?
FromISR
API(如循环处理多个数据包),每次调用都切换任务会导致性能浪费。毕竟我中断程序正在执行,即使调用了任务切换函数,那个切换的任务也还是运行不了,还是放在了任务就绪链表,资源仍然被CPU占用,一次任务切换还能接受,那要是多次,就很浪费性能了。// 错误设计:每次发送数据都切换任务(低效)
void UART_ISR() {
for (int i=0; i<10; i++) {
xQueueSendFromISR(..., &xWoken); //xWoken就是BaseType_t * const pxHigherPriorityTaskWoken
if (xWoken) portYIELD_FROM_ISR(pdTRUE); // 每次循环都可能切换
}
}
// 正确设计:批量处理完成后切换任务(高效)
void UART_ISR() {
BaseType_t xWoken = pdFALSE;
for (int i=0; i<10; i++) {
xQueueSendFromISR(..., &xWoken);
}
portYIELD_FROM_ISR(xWoken); // 仅切换一次
}
再来看下切换任务的宏portYIELD_FROM_ISR
, 修改 CPU 的 中断返回地址,使 ISR 退出后直接运行调度器,而非返回被中断的任务。
#define portEND_SWITCHING_ISR( xSwitchRequired ) do { if( xSwitchRequired != pdFALSE ) portYIELD(); } while( 0 )
#define portYIELD_FROM_ISR( x ) portEND_SWITCHING_ISR( x )
就是判断传进来的pxHigherPriorityTaskWoken
参数是否不为pdFALSE
,条件成立就调用了portYIELD()
,源程序是这样标注portYIELD()
的:Scheduler utilities.,调度器实用程序
当硬件中断处理耗时较长时,直接全程在 ISR(中断服务例程) 中执行会引发以下问题:
将中断处理拆分为 快速 ISR 和 延迟任务,通过优先级调度平衡实时性与处理效率。
阶段 1:任务 1 运行,中断发生(t1 → t2)
初始状态:
中断触发:硬件事件(如按键按下、数据接收完成)触发中断信号。
CPU 响应:
阶段 2:ISR 快速处理(t2 → t3)
vTaskDelay
)。示例:
void UART_ISR() {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t data = UART_ReadByte(); // 读取数据
xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken); // 发送到队列
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 触发任务切换
}
阶段 3:高优先级任务 2 处理中断(t3)
任务 2 设计:
ISR 退出后,调度器选择 最高优先级就绪任务(即任务 2)。
任务 2 从队列中取出数据,完成后续处理。
示例:
void vDeferredTask(void *pvParameters) {
while (1) {
uint8_t data;
if (xQueueReceive(xQueue, &data, portMAX_DELAY) == pdPASS) {
process_complex_data(data); // 耗时操作
}
}
}
阶段 4:任务 2 完成,任务 1 恢复(t3 → t4)
任务 2 阻塞:处理完成后,主动阻塞以等待下一中断(如调用 xQueueReceive
阻塞)。
任务 1 恢复:调度器重新选择任务 1 继续执行。
系统状态回归:
至于中断和任务之间的通信:队列、信号量、互斥量、事件组、任务通知等等方法,都可使用。
FreeRTOS通过操作ARM Cortex-M处理器的 BASEPRI寄存器 实现中断屏蔽。该寄存器允许设置一个优先级阈值:低于或等于该阈值的中断会被屏蔽,而更高优先级的中断仍可触发,但不可调用FreeRTOS API。
#define configMAX_SYSCALL_INTERRUPT_PRIORITY 5 // 示例值,FreeRTOS中默认的是191
假设该值为5,则优先级数值≤5的中断会被屏蔽(优先级数值越小,实际优先级越高)。
taskENTER_CRITICAL(); // 进入临界区(屏蔽中断)
/* 操作临界资源(如全局变量、硬件寄存器) */
taskEXIT_CRITICAL(); // 退出临界区(恢复中断)
优先级≤configMAX_SYSCALL_INTERRUPT_PRIORITY
的中断被屏蔽。 优先级更高的中断(如硬件故障中断)仍可触发,但不允许调用FreeRTOS API(如队列操作、任务切换函数)。
这里再提一下:优先级数值越小,实际优先级越高(例如优先级 0 > 优先级 1)
支持嵌套调用,内部通过计数器记录嵌套深度,仅当深度归零时恢复中断。
中断延迟:低优先级中断无法及时响应,可能影响系统实时性。 代码简洁性:临界区代码必须简短,避免长时间占用(通常建议控制在几十微秒内)。
来看看函数实际上的内部实现:
taskENTER_CRITICAL()
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define portENTER_CRITICAL() vPortEnterCritical()
portDISABLE_INTERRUPTS();
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
//最后可以看到:
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
* section. */
/* *INDENT-OFF* */
msr basepri, ulNewBASEPRI
dsb
isb
/* *INDENT-ON* */
}
}
/*----
msr basepri, ulNewBASEPRI
就是在basepri寄存器中去屏蔽优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY
的中断
void ISR_Handler(void) {
UBaseType_t uxSavedStatus;
uxSavedStatus = taskENTER_CRITICAL_FROM_ISR(); // 进入临界区,保存中断状态
/* 操作临界资源 */
taskEXIT_CRITICAL_FROM_ISR(uxSavedStatus); // 恢复原始中断状态
}
ISR执行时中断可能已被屏蔽(如嵌套中断),因此需保存当前中断状态uxSavedStatus
,退出时还原。
使用FROM_ISR
后缀的宏,确保在中断上下文中正确处理状态。
ISR内需快速访问共享资源(如读取传感器数据到全局缓冲区)。
低优先级的中断被屏蔽了:优先级低于、等于configMAX_SYSCALL_INTERRUPT_PRIORITY
高优先级的中断可以产生:优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY
任务调度依赖于中断、依赖于API函数,所以:这两段代码之间,不会有任务调度产生
前面提到了屏蔽了中断,它虽然能屏蔽,但是屏蔽的也只能是低优先级中断,高优先级中断是无法屏蔽的,并且也只能在低优先级的中断中去调用ISR版本Freertos的API(FromISR结尾的函数),在高优先级的中断服务程序中是不允许去调用任务的API的
就以xQueueSendToBackFromISR
函数为例子:
#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
--->
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue,
const void * const pvItemToQueue,
BaseType_t * const pxHigherPriorityTaskWoken,
const BaseType_t xCopyPosition )
{
BaseType_t xReturn;
UBaseType_t uxSavedInterruptStatus;
Queue_t * const pxQueue = xQueue;
configASSERT( pxQueue );
configASSERT( !( ( pvItemToQueue == NULL ) && ( pxQueue->uxItemSize != ( UBaseType_t ) 0U ) ) );
configASSERT( !( ( xCopyPosition == queueOVERWRITE ) && ( pxQueue->uxLength != 1 ) ) );
/* RTOS ports that support interrupt nesting have the concept of a maximum
* system call (or maximum API call) interrupt priority. Interrupts that are
* above the maximum system call priority are kept permanently enabled, even
* when the RTOS kernel is in a critical section, but cannot make any calls to
* FreeRTOS API functions. If configASSERT() is defined in FreeRTOSConfig.h
* then portASSERT_IF_INTERRUPT_PRIORITY_INVALID() will result in an assertion
* failure if a FreeRTOS API function is called from an interrupt that has been
* assigned a priority above the configured maximum system call priority.
* Only FreeRTOS functions that end in FromISR can be called from interrupts
* that have been assigned a priority at or (logically) below the maximum
* system call interrupt priority. FreeRTOS maintains a separate interrupt
* safe API to ensure interrupt entry is as fast and as simple as possible.
* More information (albeit Cortex-M specific) is provided on the following
* link: https://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html */
portASSERT_IF_INTERRUPT_PRIORITY_INVALID();
//省略
}
只摘取关键部分:portASSERT_IF_INTERRUPT_PRIORITY_INVALID()
:
#define portASSERT_IF_INTERRUPT_PRIORITY_INVALID() vPortValidateInterruptPriority()
-->
void vPortValidateInterruptPriority( void )
{
uint32_t ulCurrentInterrupt; /* 用于保存当前执行的中断号 */
uint8_t ucCurrentPriority; /* 用于保存当前中断的优先级 */
/* 获取当前正在执行的中断号。
* vPortGetIPSR() 返回 IPSR(中断程序状态寄存器)的值,
* IPSR 中包含当前中断号,若当前不在中断中,该值通常为 0。 */
ulCurrentInterrupt = vPortGetIPSR();
/* 判断当前中断号是否为用户自定义的中断。
* portFIRST_USER_INTERRUPT_NUMBER 定义了第一个用户中断的编号,
* 系统中低于该编号的中断由内核或系统使用。 */
if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER )
{
/* 根据当前中断号查找该中断在中断优先级寄存器数组中的优先级值。
* pcInterruptPriorityRegisters 是一个数组,每个索引对应一个中断的优先级。 */
ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ];
/* 下面的断言用于确保使用 FreeRTOS 安全 API 的中断的优先级不能太高。
*
* 对于 Cortex-M 架构,数值较低的优先级表示逻辑上较高的优先级。
* configMAX_SYSCALL_INTERRUPT_PRIORITY 定义了允许调用 FreeRTOS
* API 的最高中断优先级(数值较高的实际优先级)。
*
* 因此,如果一个中断使用了 ISR 安全的 FreeRTOS API,其优先级
* 必须被设置为大于或等于 ucMaxSysCallPriority(即数值上不低于 configMAX_SYSCALL_INTERRUPT_PRIORITY)。
*
* 注意:默认情况下,某些中断可能处于最高优先级(0),
* 这种情况是非法的,因为它们肯定高于 configMAX_SYSCALL_INTERRUPT_PRIORITY。
*/
configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
}
/* 验证优先级分组设置:
*
* Cortex-M 的中断控制器(NVIC)允许将中断优先级的位分为两部分:
* 1. 抢占优先级(Pre-emption priority):决定中断抢占关系。
* 2. 子优先级(Sub-priority):用于相同抢占优先级中确定响应顺序。
*
* 为了简化 FreeRTOS 的实现,要求所有的优先级位都应被用作抢占优先级,
* 而不应分出子优先级。如果存在子优先级,可能会导致 FreeRTOS API 调用时
* 出现不可预测的行为。
*
* portAIRCR_REG 是应用程序中断和复位控制寄存器,
* portPRIORITY_GROUP_MASK 用于提取其中的优先级分组位。
* ulMaxPRIGROUPValue 是允许的最大优先级分组值(确保全部位都是抢占优先级)。
*
* 如果优先级分组的设置大于 ulMaxPRIGROUPValue,则说明有部分位被当作子优先级,
* 这会导致系统行为异常,因此断言失败。
*/
configASSERT( ( portAIRCR_REG & portPRIORITY_GROUP_MASK ) <= ulMaxPRIGROUPValue );
}
确保所有使用 FreeRTOS API 的中断均设置在允许的优先级范围内,并且系统中断优先级的分组配置符合 FreeRTOS 的要求。这有助于避免优先级反转和不可预知的中断响应问题。
configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
这个便是关键,其实就是要求所处中断服务程序的优先级必须在限定范围内(低优先级部分),也就是优先级数值高于configMAX_SYSCALL_INTERRUPT_PRIORITY
(freertos默认设置的是191)
也就是说如上图,调用该xQueueSendToBackFromISR
函数的中断服务程序的优先级,必须在0xBF(191)往下部分的优先级范围内,191往上的部分,属于高优先级,不允许调用FreeRTOS的API,否则configASSERT
就会触发断言,这个断言可以设置成死循环来表示程序出错或是崩溃
显然,之前我们经常讲的屏蔽中断,屏蔽的其实都是低优先级中断。通过屏蔽低优先级中断可以防止任务的切换,而任务切换依赖的是tick中断去触发调度,因此tick中断就是低优先级中断的其中一种
如果有别的任务来跟你竞争临界资源,你可以把中断关掉:这当然可以禁止别的任务运行,但是这代价太大了。它会影响到中断的处理。
如果只是禁止别的任务来跟你竞争,不需要关中断,暂停调度器就可以了:在这期间,中断还是可以发生、处理。
通过全局变量uxSchedulerSuspended
控制调度器状态:
vTaskSuspendAll(); // 暂停调度器
/* 长时间操作(如处理大数据块) */
if (xTaskResumeAll() == pdTRUE) {
// 若有更高优先级任务就绪,可能立即切换任务
}
支持嵌套调用,恢复时需所有嵌套层级均调用xTaskResumeAll()
。但是耗时操作:如文件系统操作、大块内存拷贝(需避免任务切换但允许中断响应)。
不可中断的API调用:暂停期间无法调用vTaskDelay()
等依赖调度的函数。 实时性影响:长时间暂停可能导致高优先级任务饥饿。
机制 | 中断影响 | 任务切换 | 适用场景 |
---|---|---|---|
互斥量 | 不屏蔽 | 允许 | 公平竞争,通用资源共享 |
屏蔽中断 | 部分屏蔽 | 禁止 | 极短时间操作(<微秒级) |
暂停调度器 | 不屏蔽 | 禁止 | 较长时间操作(允许中断响应) |
守护任务 | 不屏蔽 | 允许 | 集中管理资源(如统一外设访问) |