freertos学习笔记13--个人自用-第17章 中断管理(Interrupt Management)

目录

一:概念

二:两套API函数

2.1 为什么要分两套?

2.2 怎么区分?看后缀

2.3 参数的重大区别

(1) 任务使用的函数:带“闹钟”

 (2) 中断使用的函数:带“通知单”

2.4常用函数对照表 

2.5 代码实战:如何正确使用 ISR 函数

三:portYIELD_FROM_ISR(xHigherPriorityTaskWoken);

1.xHigherPriorityTaskWoken参数

1.通俗理解:餐厅服务员与 VIP 客户

2. 技术原理:它是如何工作的?

3. 代码实战:标准写法

4. 常见疑问 (FAQ)

总结

2. 怎么切换任务 portYIELD_FROM_ISR

四:17.2 中断的延迟处理

1. 为什么要“延迟处理”?

2. 它是如何工作的?

3. 代码模式:如何实现?

4. 总结

五:优化实时性

1. 场景背景

2. 优化前的代码(“懒惰”模式)

3. 优化后的代码(“实时”模式)


一:概念

作为初学者,你可以把中断管理理解为“如何让突发事件(中断)和日常工作(任务)和平共处”。

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 (有超时参数)
中断中后缀带 FromISRxQueueSendFromISR不能 (无超时参数)

2.3 参数的重大区别

这是代码层面上最大的不同,请仔细对比这两个看似功能相同的函数:

(1) 任务使用的函数:带“闹钟”

在任务中,你可以告诉系统:“如果发不进去,我愿意等多久”。

  • 参数xTicksToWait (超时时间)

  • 逻辑:如果队列满了,任务进入阻塞状态,让出 CPU 给别人用,直到超时或队列有空位 3。

 (2) 中断使用的函数:带“通知单”

在中断中,不能等,所以没有超时参数。但它多了一个非常关键的参数:

  • 参数pxHigherPriorityTaskWoken (是否有更高优先级任务被唤醒)

  • 逻辑:中断往队列发数据,可能会唤醒一个正在等待数据的高优先级任务。中断结束后,系统需要知道是不是应该立马切换到那个高优先级任务去 4。

2.4常用函数对照表 

两套API函数列表

类型在任务中在ISR中
队列(queue)xQueueSendToBackxQueueSendToBackFromISR
xQueueSendToFrontxQueueSendToFrontFromISR
xQueueReceivexQueueReceiveFromISR
xQueueOverwritexQueueOverwriteFromISR
xQueuePeekxQueuePeekFromISR
信号量(semaphore)xSemaphoreGivexSemaphoreGiveFromISR
xSemaphoreTakexSemaphoreTakeFromISR
事件组(event group)xEventGroupSetBitsxEventGroupSetBitsFromISR
xEventGroupGetBitsxEventGroupGetBitsFromISR
任务通知(task notification)xTaskNotifyGivevTaskNotifyGiveFromISR
xTaskNotifyxTaskNotifyFromISR
软件定时器(software timer)xTimerStartxTimerStartFromISR
xTimerStopxTimerStopFromISR
xTimerResetxTimerResetFromISR
xTimerChangePeriodxTimerChangePeriodFromISR

这是你最常用的几组对照,建议保存:

功能任务中调用 (Task)中断中调用 (ISR)
写队列xQueueSendxQueueSendFromISR
读队列xQueueReceivexQueueReceiveFromISR
释放信号量xSemaphoreGivexSemaphoreGiveFromISR
获取信号量xSemaphoreTakexSemaphoreTakeFromISR
任务通知xTaskNotifyxTaskNotifyFromISR
软件定时器xTimerStartxTimerStartFromISR

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)点菜。

  1. 突然,门口来了个外卖员送餐(中断发生)。

  2. 你必须暂时停下点菜,去门口接外卖(执行 ISR)。

  3. 你发现这份外卖是属于一位超级 VIP 客户(高优先级任务 Task B)的,这位 VIP 之前因为没饭吃正在睡觉(阻塞状态)。

  4. 你接收外卖的动作(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 不结束,所有任务(包括看门狗喂狗任务)都无法运行,系统看起来像“卡死”了。

解决方案: 将中断处理过程一分为二:

  1. 上半部 (ISR):只做最紧急、最简单的操作(如清除标志位、记录数据、发送信号)。

  2. 下半部 (任务):处理耗时、复杂的逻辑(如数据解析、算法运算)。

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 从中断退出的那一瞬间,直接跳转到“小车控制任务”执行,没有任何延迟

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值