FreeRTOS中断安全API和推迟中断处理

文章讨论了FreeRTOS为解决在中断服务例程(ISR)中使用API的安全性问题,提供了中断安全的API。使用两套API(带有FromISR后缀的函数)能保持代码高效且简化移植。中断安全API避免了任务和ISR间的冲突,但可能需要在ISR和任务间协调处理。文章还强调了中断应尽可能短和快速,复杂的处理可以推迟到高优先级任务中进行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前面的文章都提到了部分FreeRTOS的API有中断安全的版本,下面详解一下,为什么会这样。

参考资料:

《Mastering the FreeRTOS™ Real Time Kernel》-Chapter 6 Interrupt Management 6.2 Using the FreeRTOS API from an ISR/6.3 Deferred Interrupt Processing

FreeRTOS全解析-7.中断安全API和推迟中断处理

目录

1.使用FreeRTOS中断安全的API

1.1什么是中断安全API

1.2使用两套API的优点

1.3使用两套API的缺点

1.4xHigherPriorityTaskWoken参数

1.5中断中切换任务

2.中断的延迟处理


1.使用FreeRTOS中断安全的API

不了解中断的,可以看一下中断基础知识:嵌入式Linux入门-异常与中断(流程+寄存器全解析)

1.1什么是中断安全API

部分FreeRTOS的API中的部分行为,在中断服务例程(ISR)中执行是非法的,有可能破坏FreeRTOS的调度,或者出现其他不可预测的问题,就比如进入阻塞态,只有任务才能进入阻塞态,ISR又不是任务。

但是在写一个中断服务例程(ISR)时,我们也可能会需要用到一些FreeRTOS的API,这就导致了存在不安全的情况,为了解决这个问题,FreeRTOS提供了两套API,后缀名为FromISR的是中断安全API

这里就引发一个问题了,为什么要使用两套API,而不是使用同一个函数,在函数中判断一下是处于任务中还是处于ISR中,再分别进行不同操作?

1.2使用两套API的优点

归根结底,就是更加高效。

(1)使用同一套函数的话,需要增加额外的判断代码、增加额外的分支,会让函数更长、更复杂、难以测试

(2)在任务、ISR中调用时,需要的参数不一样。就像阻塞,通过参数指定阻塞时间,而ISR中根本就无法阻塞,不需要这个参数。

(3)使用同一套函数的话,移植FreeRTOS时必须提供检测是处于任务还是ISR的方法,有一些芯片架构不好检测,则需要更多的代码来判断。

1.3使用两套API的缺点

使用两套API会引入一个问题,比如你要使用第三方库函数时,即需要在任务中调用它,也需要在ISR中调用它。这个第三方库函数用到了FreeRTOS的API函数。可以用下面的方法解决这个问题:

(1)把中断的处理推迟到任务中进行,在任务中调用库函数

(2)在库函数中使用"FromISR"函数:在任务中、在ISR中都可以调用"FromISR"函数,反过来就不行,非FromISR函数无法在ISR中使用。

(3)第三方库函数也许会提供OS抽象层,自行判断当前环境是在任务还是在ISR中,分别调用不同的函数。

1.4xHigherPriorityTaskWoken参数

xHigherPriorityTaskWoken字面上理解Higher更高的Priority优先级Task任务Woken被唤醒。含义就是:是否有更高优先级的任务被唤醒了。如果为pdTRUE,则意味着后面要进行任务切换,下面详解:

很多API会导致任务切换(或者说上下文切换context switch)

以写队列为例:不了解队列的可以看这篇

FreeRTOS全解析-5.队列(Queue)

任务A调用xQueueSendToBack()写队列,有几种情况发生:

(1)队列满了,任务A阻塞等待,另一个任务B运行

(2)队列没满,任务A成功写入队列,但是有另一个任务B(处于阻塞态,等待队列有数)任务B被唤醒,任务B的优先级更高:任务B先运行

(3)队列没满,任务A成功写入队列,即刻返回

情况1和2都导致了任务切换。并且这个切换是发生在任务A当中的,xQueueSendToBack()还没有返回就切换了。

让我们再来看看中断时调用类似API会发生什么情况

根据上文中的论述,我们在中断中要写队列,应该调用xQueueSendToBackFromISR()。

BaseType_t xQueueSendToBack( QueueHandle_t xQueue,                  const void * pvItemToQueue,                  TickType_t xTicksToWait );BaseType_t xQueueSendToBackFromISR(                  QueueHandle_t xQueue,                  const void *pvItemToQueue,                  BaseType_t *pxHigherPriorityTaskWoken);

可以看到xTicksToWait换成了pxHigherPriorityTaskWoken,因为中断是无法阻塞的。而xQueueSendToBackFromISR函数内部不会切换,只是用pxHigherPriorityTaskWoken参数来保存函数的结果:是否需要切换。

用法:pxHigherPriorityTaskWoken参数使用前需要初始化为pdFALSE。

当它为pdTRUE时表示有任务需要切换。​​​​​​​

BaseType_t xHigherPriorityTaskWoken = pdFALSE;xQueueSendToBackFromISR(xQueue, pvItemToQueue, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken == pdTRUE){    /* 任务切换 */    }

为什么不在API内部切换?

(1)避免没必要的切换

比如UART中断:在UART的ISR中读取字符,我们处理任务往往处理的是一个字符串,而不是单个字符,收到一个字符串后(发现收到回车符),才有必要切换到字符串处理任务。假如在API内部切换,则会变成,中断,处理,中断,处理....效率低下。

(2)让ISR更可控

中断产生的时机是不可预测的,在API中进行任务切换的话,会导致问题更复杂

(3)可移植性更强

(4)高效。部分芯片架构只允许在ISR最后进行切换,如果要修改这部分限制,需要更多的代码。

(5)FreeRTOS Tick中断中可以开启钩子函数,也就是可以调用自己的代码。这时候就需要上述的优点了。

如果你不需要切换,用不到这个参数,传入NULL就可以了。

1.5中断中切换任务

在任务中主动切换,调用taskYIELD(),在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);
}

2.中断的延迟处理

ISR要尽可能得简短快速。原因包括:

(1)即使任务被分配了非常高的优先级,它们也只会在硬件没有服务中断的情况下运行。

(2)ISR会扰乱一个任务的开始时间和执行时间

(3)部分CPU架构在ISR时无法处理新的中断。

(4)写程序时需要考虑变量、外设和内存缓冲区等资源同时被任务和ISR访问的后果并加以防范。

(5)一些CPU架构允许中断嵌套,但是中断嵌套会增加复杂性并降低可预测性。中断越短,嵌套的可能性就越小。

在ISR中做一些必要的工作,比如记录和清除中断,然后把其他的工作放到一个任务中去运行,这就叫延迟中断处理。

只要把这个任务优先级设置得比其他任务高,那么发生中断后,这个任务就会立即运行,就像在中断中运行的效果一样。

如图Task2是中断处理任务,一开始是处于阻塞态。

t1时Task1运行,t2时发生中断,ISR执行,清除中断,解除Task2的阻塞态。

t3时ISR运行结束,Task2的优先级比Task1高,所以ISR结束后直接跳到Task2运行。

t4时Task2运行结束进入阻塞态,等待下一次中断,Task1运行。

什么时候在ISR中处理,什么时候推迟到任务中,没有一个必然的规定,下面几种情况最好在任务中处理:

(1)处理比较复杂。例如,如果中断只是存储模拟到数字转换的结果,那么这最好在ISR中执行,但如果转换的结果也必须通过软件过滤器,那么最好在任务中执行过滤器。

(2)存在ISR内部无法执行的操作,例如写入控制台或分配内存。

(3)处理时间是不确定的——不知道处理将花费多长时间。

当然还有别的办法推迟中断处理,将在后面的文章中提到。

### FreeRTOS 中断使用方法 #### 中断服务例程的基本结构 在 FreeRTOS 中,中断服务例程(ISR)用于响应硬件事件并执行必要的处理逻辑。为了确保 ISR 能够高效运行而不影响系统的实时性能,FreeRTOS 提供了一套机制来简化优化中断处理过程[^2]。 #### 定义中断服务例程 定义一个标准的 FreeRTOS 中断服务程序通常涉及以下几个方面: - **保存上下文**:进入 ISR 后立即保存当前处理器状态。 - **清除标志位**:如果适用的话,应尽快清除触发此中断的状态或条件。 - **调用 API 函数**:对于某些类型的中断,可能需要通知其他任务发生了什么情况;此时可利用 `portYIELD_FROM_ISR()` 或者信号量/消息队列等同步原语来进行通信。 - **恢复现场**:完成所有工作之后再返回到被中断的任务之前要先恢复先前保存好的环境变量。 下面给出一段简单的代码片段展示如何编写这样的 ISR: ```c void vAnExampleInterruptHandler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 清除引起本次中断的原因... // 假设有一个名为 xSemaphore 的二值型信号量用来做任务间通讯, // 这里尝试发送它给等待着它的那个线程. if (xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken) != pdPASS){ // 发送失败后的错误处理... } portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } ``` 这段 C 语言编写的伪码展示了怎样在一个典型的 STM32 设备上创建基于 FreeRTOS中断处理函数[^3]。 #### 中断优先级配置 为了让 FreeRTOS 正确管理多个不同级别的中断源,开发者应当合理设置各个外设所对应的中断向量表项以及它们各自的抢占级别与子优先级。具体来说就是调整 NVIC 控制寄存器内的相应字段以满足项目需求。值得注意的是,在任何情况下都不要让那些打算调用 RTOS 接口的服务具有过高的权限等级——即其数值应该大于等于由宏`configMAX_SYSCALL_INTERRUPT_PRIORITY` 所设定的最大阈值[^5]。 #### 底半部与顶半部的概念 考虑到效率问题,有时并不希望所有的动作都在真正的硬件中断期间被执行完毕。因此引入了所谓的“底半部”概念,指的是将一些耗时较长的工作推迟至稍后某个时刻去完成。“顶半部”的职责仅限于快速识别出发生了什么事情并将相关信息传递出去即可。这种方式有助于提高整个应用程序的整体吞吐率同时也减少了对 CPU 时间片的竞争压力[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闪耀大叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值