目录
三:portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
一:概念
作为初学者,你可以把中断管理理解为“如何让突发事件(中断)和日常工作(任务)和平共处”。
1. 核心概念:两个世界
在嵌入式系统中,CPU 的时间被分成了两部分:
-
任务 (Task):你的日常工作(比如每秒闪烁 LED、处理屏幕显示)。
-
中断 (ISR):突发紧急事件(比如按键被按下、串口收到了数据)。
由硬件决定的铁律: 中断的优先级永远高于任务。无论任务多么重要,一旦中断发生,CPU 必须立刻放下手中的任务去执行中断服务函数 (ISR)。
二:两套API函数
为了适应“任务”和“中断”这两个完全不同的运行环境,FreeRTOS 专门设计了两套 API 函数。
以下是关于这两套 API 的核心讲解:
2.1 为什么要分两套?
根本原因在于运行环境的“特权”不同 :
任务 (Task):比较“悠闲”。如果请求的资源(比如队列数据)没有准备好,任务可以选择阻塞(睡觉等待),直到资源准备好或者超时。
很多API函数会导致任务计入阻塞状态:
运行这个函数的 任务 进入阻塞状态,
比如写队列时,如果队列已满,可以进入阻塞状态等待一会
中断 (ISR):非常“紧急”。中断必须快进快出,绝对不允许阻塞。如果中断“卡”住了,整个系统(包括所有任务)都会停摆。
ISR调用API函数时,ISR不是"任务",ISR不能进入阻塞状态
因为中断不能阻塞,所以它不能调用那些包含“等待时间”参数的普通 API 函数。为了区分和安全,FreeRTOS 强制规定了专用的 ISR 函数。
2.2 怎么区分?看后缀
区分非常简单,看函数名的屁股后面有没有 FromISR :
| 环境 | 函数特征 | 示例 | 能否阻塞? |
| 任务中 | 标准名称 | xQueueSend | 能 (有超时参数) |
| 中断中 | 后缀带 FromISR | xQueueSendFromISR | 不能 (无超时参数) |
2.3 参数的重大区别
这是代码层面上最大的不同,请仔细对比这两个看似功能相同的函数:
(1) 任务使用的函数:带“闹钟”
在任务中,你可以告诉系统:“如果发不进去,我愿意等多久”。
-
参数:
xTicksToWait(超时时间) -
逻辑:如果队列满了,任务进入阻塞状态,让出 CPU 给别人用,直到超时或队列有空位 3。
(2) 中断使用的函数:带“通知单”
在中断中,不能等,所以没有超时参数。但它多了一个非常关键的参数:
-
参数:
pxHigherPriorityTaskWoken(是否有更高优先级任务被唤醒) -
逻辑:中断往队列发数据,可能会唤醒一个正在等待数据的高优先级任务。中断结束后,系统需要知道是不是应该立马切换到那个高优先级任务去 4。
2.4常用函数对照表
两套API函数列表
| 类型 | 在任务中 | 在ISR中 |
|---|---|---|
| 队列(queue) | xQueueSendToBack | xQueueSendToBackFromISR |
| xQueueSendToFront | xQueueSendToFrontFromISR | |
| xQueueReceive | xQueueReceiveFromISR | |
| xQueueOverwrite | xQueueOverwriteFromISR | |
| xQueuePeek | xQueuePeekFromISR | |
| 信号量(semaphore) | xSemaphoreGive | xSemaphoreGiveFromISR |
| xSemaphoreTake | xSemaphoreTakeFromISR | |
| 事件组(event group) | xEventGroupSetBits | xEventGroupSetBitsFromISR |
| xEventGroupGetBits | xEventGroupGetBitsFromISR | |
| 任务通知(task notification) | xTaskNotifyGive | vTaskNotifyGiveFromISR |
| xTaskNotify | xTaskNotifyFromISR | |
| 软件定时器(software timer) | xTimerStart | xTimerStartFromISR |
| xTimerStop | xTimerStopFromISR | |
| xTimerReset | xTimerResetFromISR | |
| xTimerChangePeriod | xTimerChangePeriodFromISR |
这是你最常用的几组对照,建议保存:
| 功能 | 任务中调用 (Task) | 中断中调用 (ISR) |
| 写队列 | xQueueSend | xQueueSendFromISR |
| 读队列 | xQueueReceive | xQueueReceiveFromISR |
| 释放信号量 | xSemaphoreGive | xSemaphoreGiveFromISR |
| 获取信号量 | xSemaphoreTake | xSemaphoreTakeFromISR |
| 任务通知 | xTaskNotify | xTaskNotifyFromISR |
| 软件定时器 | xTimerStart | xTimerStartFromISR |
2.5 代码实战:如何正确使用 ISR 函数
在中断中使用 API 是有固定套路的,请死记这个模板 :
void TIM3_IRQHandler(void)
{
// 1. 定义一个变量,用于记录是否需要切换任务,初始为 pdFALSE (不需要)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 清除中断标志位...
// 2. 调用 FromISR 函数
// 注意最后那个参数的传址 &xHigherPriorityTaskWoken
// 如果发送信号量导致一个高优先级任务醒来,FreeRTOS 会把这个变量改为 pdTRUE
xSemaphoreGiveFromISR(xSemaphore, &xHigherPriorityTaskWoken);
// 3. 在中断退出的最后,强制进行一次任务切换检查
// 如果 xHigherPriorityTaskWoken 是 pdTRUE,这里会触发上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
总结:
不要混用!在 void xxx_IRQHandler(void) 这种中断服务函数里,只能、必须使用带 FromISR 后缀的函数,否则系统会崩溃或死锁。
三:portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
1.xHigherPriorityTaskWoken参数
简单来说,xHigherPriorityTaskWoken 是一个**“是否需要立即抢占”的标志位**。
1.通俗理解:餐厅服务员与 VIP 客户
想象你是一个餐厅的服务员(CPU),你正在给一桌普通客人(低优先级任务 Task A)点菜。
-
突然,门口来了个外卖员送餐(中断发生)。
-
你必须暂时停下点菜,去门口接外卖(执行 ISR)。
-
你发现这份外卖是属于一位超级 VIP 客户(高优先级任务 Task B)的,这位 VIP 之前因为没饭吃正在睡觉(阻塞状态)。
-
你接收外卖的动作(
xQueueSendFromISR),把 VIP 客户叫醒了(任务 B 进入就绪态)。
此时,关键问题来了: 当你接完外卖(ISR 结束)后,你应该:
-
选项 A:回到普通客人那里继续点菜(回到 Task A)?
-
选项 B:立刻跑去服务刚醒来的 VIP 客户(切换到 Task B)?
xHigherPriorityTaskWoken 就是那张提示纸条:
-
如果纸条是
pdFALSE:刚才接的外卖是普通人的,不用急,你回去继续给 Task A 点菜。 -
如果纸条是
pdTRUE:刚才接的外卖是 VIP 的!别回 Task A 了,马上去服务 Task B!
2. 技术原理:它是如何工作的?
在 FreeRTOS 中,当你调用带 FromISR 的函数(如 xQueueSendFromISR)时,系统不会自动进行任务切换 。
-
如果函数内部唤醒了一个任务,并且这个被唤醒的任务优先级 高于 当前被中断的任务优先级。
- FreeRTOS 就会把
xHigherPriorityTaskWoken指向的变量设置为pdTRUE。
这个参数的作用是告诉中断服务程序(ISR)的最后一步: “嘿,刚才有个大人物醒了,你退出中断的时候,别回老地方了,直接切过去!”
3. 代码实战:标准写法
这是你在中断中必须背下来的“三步走”写法:
void UART_IRQHandler(void)
{
// 【第1步】定义一个变量,初始化为 pdFALSE (表示暂时不需要切换)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t data = UART_Receive_Data();
// 【第2步】调用 FromISR 函数
// 注意:我们要把变量的地址 (&) 传进去,这样函数内部才能修改它
xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);
// 如果发送数据导致一个高优先级任务(比如数据处理任务)解除了阻塞,
// FreeRTOS 内部会将 xHigherPriorityTaskWoken 修改为 pdTRUE。
// ...这里可能还有其他代码...
// 【第3步】在中断的最后,根据标志位决定是否强制切换上下文
// 如果 xHigherPriorityTaskWoken 是 pdTRUE,这里就会触发任务切换
// 如果是 pdFALSE,这就相当于一句废话,什么都不做
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
4. 常见疑问 (FAQ)
Q1: 为什么 FromISR 函数不直接在内部自动切换?为什么要麻烦我自己写?
A: 为了效率 。 一个中断里可能调用多次 FreeRTOS 函数(比如连续发送 5 个字节到队列)。
-
如果自动切换:每发一个字节,函数内部都试图切换一次任务,这会产生巨大的无用开销。
-
现在的设计:你发 5 次,每次只是把标志位设为
pdTRUE。等所有事情做完了,在中断的最后只进行一次切换。
Q2: 如果我在中断里多次调用 FromISR 函数怎么办?
A: 使用同一个变量即可 。 xHigherPriorityTaskWoken 就像一个“粘性”标志。只要其中任意一次调用把它变成了 pdTRUE,它就会保持为 pdTRUE。只要有一次操作唤醒了高级任务,最后就需要切换。
Q3: 如果我不写 portYIELD_FROM_ISR 会怎样? A: 系统不会崩,但实时性会变差。 即使高优先级任务醒了,CPU 也会先回到低优先级任务继续运行。直到下一次系统滴答中断(Tick Interrupt)到来,调度器才会发现:“哎呀,有个高优先级任务在排队”,然后才进行切换。这对于需要极速响应的系统是不可接受的。
总结
-
是什么:一个
BaseType_t类型的变量,作为标志位。 -
什么含义:
pdTRUE= 有更高优先级的任务醒了,需要通过portYIELD_FROM_ISR立即切换。 -
怎么用:定义变量 -> 传址给函数 -> 最后调用 YIELD。
2. 怎么切换任务 portYIELD_FROM_ISR
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
-
动作:
portYIELD_FROM_ISR是执行“切换任务”这个动作的开关。 -
条件:它根据传入的参数(
xHigherPriorityTaskWoken)决定是否真的切换。如果参数是pdFALSE,它就什么都不做。 -
位置:必须放在中断服务函数的最后。
四:17.2 中断的延迟处理
中断延迟处理 (Deferred Interrupt Processing) ,它的核心思想就是:“把重活累活从中断里扔出去,交给任务来做”。
1. 为什么要“延迟处理”?
在实时系统(RTOS)中,中断服务函数 (ISR) 有一个黄金法则:越快越好 。
如果你的 ISR 执行时间太长(比如进行了复杂的数学运算、大量数据拷贝、或者读写慢速外设),会产生严重的后果:
-
低优先级中断被堵死:系统无法响应其他硬件请求,实时性崩塌。
-
任务饿死:ISR 优先级永远高于任务,ISR 不结束,所有任务(包括看门狗喂狗任务)都无法运行,系统看起来像“卡死”了。
解决方案: 将中断处理过程一分为二:
-
上半部 (ISR):只做最紧急、最简单的操作(如清除标志位、记录数据、发送信号)。
-
下半部 (任务):处理耗时、复杂的逻辑(如数据解析、算法运算)。
2. 它是如何工作的?
为了保证处理的及时性,用来处理“下半部”工作的那个任务,通常会被设置为最高优先级 。
文档描述了这样一个典型的时间线流程:
-
t1 (正常运行):普通任务(如 Task 1)正在运行,处理中断的任务(Task 2)处于阻塞状态等待信号。
-
t2 (中断发生):硬件中断触发。CPU 暂停 Task 1,跳转执行 ISR。
-
ISR 执行:ISR 快速运行,它不做具体处理,而是通过信号量、队列或任务通知唤醒 Task 2,然后立即退出。
-
t3 (任务切换):ISR 结束后,因为 Task 2 优先级很高且已就绪,调度器立刻切换到 Task 2。
-
Task 2 执行:Task 2 完成复杂的后续处理工作(这就是“延迟处理”)。
-
t4 (恢复):Task 2 处理完后再次进入阻塞状态,Task 1 继续运行。
3. 代码模式:如何实现?
在代码中,我们通常使用二值信号量 (Binary Semaphore) 或 任务通知 (Task Notification) 来实现这种机制。
(1) 中断部分 (ISR):只发信号,光速退出
void EXTI_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 1. 清除硬件中断标志 (极快)
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_13);
// 2. 发送信号量给处理任务 (极快)
// 告诉任务:"活来了,你醒醒!"
xSemaphoreGiveFromISR(xBinarySemaphore, &xHigherPriorityTaskWoken);
// 3. 强制切换:确保中断一退出,马上跑去执行处理任务,而不是回到原来的低优先级任务
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
(2) 任务部分 (Task):死等信号,干重活 这个任务通常优先级设置得非常高。
void vDeferredProcessingTask(void *pvParameters)
{
while(1)
{
// 1. 死等信号量 (平时睡觉,不占 CPU)
xSemaphoreTake(xBinarySemaphore, portMAX_DELAY);
// 2. 醒来后,处理复杂逻辑 (干重活)
// 比如:解析刚刚收到的 1KB 数据、写入 Flash、或者进行复杂的 PID 运算
ProcessHeavyData();
// 3. 处理完后,再次回到开头,继续睡觉等待下一次中断
}
}
4. 总结
中断延迟处理 是解决“中断必须快”与“业务逻辑很复杂”这对矛盾的最佳手段。
-
ISR 的职责:仅仅是当一个“二传手”,负责通知。
-
Task 的职责:才是真正的“处理者”。
-
关键点:这个 Task 必须是高优先级,这样效果看起来就像中断立刻得到了处理一样,但又不会因为长时间占用中断资源而影响系统的稳定性 。
五:优化实时性
核心目的是解决一个痛点:中断唤醒了高优先级任务,但系统没有立刻切换过去,导致了不必要的延迟。
优化实时性就是关于portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
以下是该示例的详细解析:
1. 场景背景
这个示例基于之前的红外接收程序(driver_ir_receiver.c)。
-
动作:用户按下遥控器。
- ISR:中断服务函数解析出按键值,通过
DispatchKey函数把数据写入队列 。 -
任务:有一个高优先级的“小车控制任务”正在阻塞等待这个队列的数据。
2. 优化前的代码(“懒惰”模式)
在优化前,代码是这样写的:
// 优化前的写法
static void DispatchKey(struct ir_data *pidata)
{
// ...省略部分代码...
for (i = 0; i < g_queue_cnt; i++)
{
// 重点看最后一个参数:NULL
xQueueSendFromISR(g_xQueues[i], pidata, NULL);
}
}
存在的问题:
-
这里将最后一个参数设置为
NULL。 -
后果:当
xQueueSendFromISR成功把数据写入队列后,虽然“小车控制任务”已经从阻塞态变成了就绪态,而且它的优先级很高。但是,系统并不知道需要立刻进行任务切换。 -
延迟:CPU 会退出中断,回到原来的低优先级任务继续运行。直到下一次系统滴答(SysTick)中断到来(可能要等 1ms),调度器才会发现:“咦,有个高优先级任务在排队”,这时才进行切换。
-
结论:这就产生了一个
0 ~ 1ms的随机延迟,对于要求极高的实时系统,这是不可接受的。
3. 优化后的代码(“实时”模式)
为了消除这个延迟,代码进行了如下修改:
// 优化后的写法
static void DispatchKey(struct ir_data *pidata)
{
// 1. 定义一个变量,默认不想切换
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
for (i = 0; i < g_queue_cnt; i++)
{
// 2. 传入变量地址。如果有高优先级任务被唤醒,函数内部会把这个变量置为 pdTRUE
xQueueSendFromISR(g_xQueues[i], pidata, &xHigherPriorityTaskWoken);
}
// 3. 在退出前,根据标志位决定是否强制切换
// 如果 xHigherPriorityTaskWoken 变成了 pdTRUE,这里会立刻切换到高优先级任务
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
优化的效果:
-
立刻响应:一旦
portYIELD_FROM_ISR被执行,CPU 从中断退出的那一瞬间,直接跳转到“小车控制任务”执行,没有任何延迟。

被折叠的 条评论
为什么被折叠?



