刘火良 FreeRTOS内核实现与应用之13——内存管理、中断管理
采取和消息队列一样的学习方法,看书<==>记录问题<==>代码<==>看书<==>笔记的步骤。
内容来源于:
1. 野火:12. 内存管理 — [野火]FreeRTOS 内核实现与应用开发实战—基于RT1052 文档;
2. Mastering the FreeRTOS™ Real Time Kernel
内存管理
基本概念
FreeRTOS的V9.0.0版本为我们提供了5种内存管理算法,分别是heap_1.c、heap_2.c、heap_3.c、heap_4.c、heap_5.c,源文件存放于FreeRTOSSourceportableMemMang路径下,在使用的时候选择 其中一个添加到我们的工程中去即可。
内存管理方案
FreeRTOS规定了内存管理的函数接口,具体见,但是不管其内部的内存管理方案是怎么实现的,所以,FreeRTOS可以提供多个内存管理方案,下面,就一起看看各个内存管理方案的区别。
void *pvPortMalloc( size_t xSize ); //内存申请函数void vPortFree( void *pv ); //内存释放函数void vPortInitialiseBlocks( void ); //初始化内存堆函数size_t xPortGetFreeHeapSize( void ); //获取当前未分配的内存堆大小size_t xPortGetMinimumEverFreeHeapSize( void ); //获取未分配的内存堆历史最小值
1)对于heap_1.c、heap_2.c和heap_4.c这三种内存管理方案,内存堆实际上是一个很大的数组,定义为static uint8_t ucHeap[ configTOTAL_HEAP_SIZE],而宏定义configTOTAL_HEAP_SIZE则表示系统管理内存大小,单位为字,在FreeRTOSConfig.h中由用户设定。
2)对于heap_3.c这种内存管理方案,它封装了C标准库中的malloc()和free()函数,封装后的malloc()和free()函数具备保护,可以安全在嵌入式系统中执行。因此,用户需要通过编译器或者启动文件设置堆空间。
3)heap_5.c方案允许用户使用多个非连续内存堆空间,每个内存堆的起始地址和大小由用户定义。这种应用其实还是很大的,比如做图形显示、GUI等,可能芯片内部的RAM是不够用户使用的,需要外部SDRAM,那这种内存管理方案则比较合适。
中断管理
区分中断优先级和任务优先级:
- 任务是软件功能,与硬件无关;任务的优先级由编程人员在软件中分配,软件算法(调度器)决定哪个任务将处于运行状态;
- 虽然是软件写的,但中断服务程序是硬件功能,硬件控制哪个中断服务程序运行,以及何时运行;
- 最低优先级的中断会中断最高优先级的任务,任务却没有办法抢占ISR(中断服务程序);
基本概念
推迟中断处理
ISR尽量短小精悍(记录中断产生的原因,并清除中断),中断所需要的其他处理工作通常可以在任务中执行,从而使中断服务程序尽快退出,这就是所谓的“推迟中断处理”,因为将中断所需要处理的工作从ISR“推迟”到了任务。
将中断推迟给任务,允许编程人员相对于应用程序中的其他任务优先对待这些中断处理,而且能够使用全部的FreeRTOS API函数。
中断管理方案
重要的参数:xHigherPriorityTaskWoken
BaseType_t xHigherPriorityTaskWoken;
它是一个 变量类型为 BaseType_t
(通常是 int
类型的别名) 的参数,用于在中断服务程序(ISR)或某些特殊 API 中,通知系统是否有更高优先级的任务被唤醒,从而可能需要触发一次上下文切换(即任务切换)。
一、它主要用在什么地方?
1. 中断服务程序(ISR,Interrupt Service Routine)中;
2. 与某些 FreeRTOS FromISR**API 配合使用**
-
比如:
-
xQueueSendFromISR()
-
xQueueReceiveFromISR()
-
xSemaphoreGiveFromISR()
-
xTaskNotifyFromISR()
-
等等(很多带 FromISR 后缀的 API)
-
二、为什么要用 xHigherPriorityTaskWoken
?
背景知识:
在 FreeRTOS 中,任务之间经常通过队列(Queue)、信号量(Semaphore)、事件组(Event Group)等通信机制来同步或传递数据。
但有时候,这些操作是在 中断(ISR)中发起的,比如:
-
一个外部中断发生了,传感器数据来了,你想 通过队列把数据发给任务去处理;
-
或者一个定时器中断触发,你要 释放一个信号量,唤醒等待的任务;
问题是:
🤔 如果中断里唤醒了一个任务,而这个任务的优先级比当前正在运行的任务还要高,那是不是应该立刻切换过去,让高优先级任务先运行?
👉 答案是:当然应该!但 FreeRTOS 不能自己随便切换任务,它需要你(开发者)告诉它:“嘿,我刚刚可能唤醒了一个高优先级任务,你看着办,可能需要任务切换!”
这就是 xHigherPriorityTaskWoken
的作用!
三、xHigherPriorityTaskWoken
的作用详解
📌 定义:
xHigherPriorityTaskWoken
是一个标志变量,用于告诉 FreeRTOS:在当前 ISR 中,是否有一个比当前运行任务优先级更高的任务被唤醒了。如果是,FreeRTOS 可能需要在 ISR 结束时进行一次上下文切换(即任务切换)。
📌 通常的使用模式如下:
步骤 1:定义一个变量
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
初始化为
pdFALSE
,表示默认没有高优先级任务被唤醒。
步骤 2:在中断中调用带有 _FromISR 后缀的 API,并传入该变量
例如,通过队列向某个任务发送数据:
xQueueSendFromISR(xQueue, &data, &xHigherPriorityTaskWoken);
这个函数会在 ISR 中尝试向队列发送数据,如果此时有任务在等待这个队列的数据,那么该任务会被唤醒。
-
如果 被唤醒的任务的优先级 > 当前正在运行的任务优先级,那么:
xHigherPriorityTaskWoken
会被 API 内部设置为pdTRUE
步骤 3:判断是否需要触发上下文切换
在 ISR 的最后,你通常会调用:
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
或者:
portYIELD_FROM_ISR(xHigherPriorityTaskWoken == pdTRUE);
这个宏会根据
xHigherPriorityTaskWoken
的值决定是否立即进行一次 任务切换(上下文切换),让更高优先级的任务得以运行。
四、通俗理解(类比)
你可以把整个过程想象成:
🏢 你是一家公司的老板(当前运行的任务),突然来了一个电话(中断),电话那头说:“有紧急文件需要马上交给经理(更高优先级任务)处理!”
你(ISR)就把文件交给了经理,但经理的优先级比你高,他拿到文件后应该立刻去处理,而不是等你这个老板忙完。
所以你(ISR)就要告诉公司的调度员(FreeRTOS 内核):“嘿,我刚刚叫经理来干活了,他比我还急,你最好现在就让他上场!”
👉 这个“提醒调度员”的动作,就是通过
xHigherPriorityTaskWoken
+portYIELD_FROM_ISR()
实现的。
五 、关键点总结
项目 | 说明 |
---|---|
变量名 |
|
作用 | 用于在中断(ISR)中,标识是否有更高优先级的任务被唤醒,从而可能需要任务切换 |
常用场景 | 在调用 _FromISR 类 API(如 |
初始值 | 通常初始化为 |
API 是否会修改它? | ✅ 是的,如果本次操作导致高优先级任务被唤醒,相关 FromISR API 会将其设为 |
如何使用它? | 最后通过 |
为什么重要? | 保证高优先级任务能够及时响应,提升系统的实时性与响应能力 |
如果不使用会怎样? | 可能导致高优先级任务无法及时运行,降低系统实时性,甚至出现逻辑错误 |
六、补充:portYIELD_FROM_ISR()
是什么?
它是一个 宏,定义在 FreeRTOS 的移植层(portmacro.h)中,其作用是:
根据传入的值决定是否触发一次上下文切换(即任务切换)
它最终可能会调用类似
portYIELD()
的底层函数,具体行为依赖于硬件平台(如 Cortex-M 的 PendSV 机制)
你一般不需要关心它的内部实现,只需要:
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
就可以了。
七、一句话记忆 🧠
xHigherPriorityTaskWoken
就是中断里用来告诉 FreeRTOS:“我可能叫了个大佬(高优先级任务)来干活,你看着办要不要赶紧切换过去!” 的小旗子。
重要方法:用于同步的二进制信号量
可以把二进制信号量认为长度为1的队列一样,队列在任何时候只能最多包含一个数据项,要么空,要么满;
任务通过xSemaphoreTake()函数,接收推迟中断处理的任务,尝试从队列中读取数据项,该读取操作有阻塞时间,如果队列为空,则导致任务进入阻塞状态;
中断发生时,ISR调用xSemaphoreGiveFromISR() API函数将令牌(信号量)放入队列中,填满队列,这将导致任务退出阻塞状态并移除令牌,再次清空队列。
当任务完成处理后,再次尝试从队列中读取,结果发现队列为空,便重新进入阻塞状态,等待下一次操作:
重要方法:用于同步的计数信号量
可以把计数信号量认为是长度大于1的队列,任务对存储在队列中的数据不感兴趣——只对队列数据项的数量感兴趣;
每次“释放”计数信号量时,都会使用其队列的另一个空间;队列数据项的数量就是信号量的“计数”值;
1)计数事件
事件处理程序将在每次事件发生时“释放”信号量——导致信号量的计数值在每次“释放”时递增;任务将在每次处理事件时“获取”信号量——导致信号量的计数值在每次“获取”时递减;
2)资源管理
计数值可以表示可用资源的数量;为了获得对资源的控制权,任务必须先“获取”信号量——减少信号量的计数值,当计数值到0时,就没有可用资源了;
当使用完资源后,就会“释放”信号量——递增信号量的计数值;
重要方法:用于同步的队列
二进制和计数信号量用于事件通信,队列用于事件通信和数据传输;
推迟工作到RTOS守护任务
方法有两种:
1)可以为每个使用推迟处理技术的中断创建一个任务;
2)也可以采用“集中式推迟中断处理”方法,使用xTimerPendFunctionCallFromISR() API函数将中断处理推迟到RTOS守护任务——消除了为每一个中断创建单独任务的需求。
中断不同对时间限制的要求也不同,所以在同一个应用程序中,通常使用两种推迟中断处理方法。
xTimerPendFunctionCallFromISR()
xTimerPendFunctionCallFromISR()
允许你在中断(ISR)中,“提交一个函数调用请求”,这个函数调用不会立即执行,而是被 FreeRTOS 挂起(pend),稍后由 一个后台的 Timer Service Task(定时器服务任务,也叫 Timer Daemon Task) 在 任务上下文中安全地执行它。
关键点总结
项目 | 说明 |
---|---|
函数名 |
|
作用 | 允许你 在中断(ISR)中安全地提交一个函数调用请求,该调用会被 FreeRTOS 后台的 Timer Service Task(定时器守护任务)在任务上下文中执行 |
为什么有用? | 为了 不在中断中执行复杂逻辑,而是把任务“推迟”到后台任务中安全地执行,同时还能传递参数 |
被调用的函数格式 | 必须是: |
参数传递 | 支持传递 1 个指针参数 + 1 个 uint32_t 参数 |
与哪个任务相关? | 由 FreeRTOS 的 Timer Service Task(Timer Daemon Task) 执行,该任务在后台运行,负责调用你提交的函数 |
是否需要配置 Timer Task? | ✅ 是的!你必须开启 |
xTimerPendFunctionCall()
vs xTimerPendFunctionCallFromISR()
函数 | 调用场景 | 说明 |
---|---|---|
| 在任务中调用(非中断) | 功能一样,但是用于任务上下文 |
| 在中断中调用(ISR) | 中断安全版本,用于从中断中延迟调用函数 |
什么时候使用它?
推荐在以下场景使用:
场景 | 是否推荐使用 |
---|---|
在中断中接收到数据,想让任务去处理 | ✅ 推荐 ✅ |
在中断中想调用某个任务函数,但不想直接调用 | ✅ 推荐 ✅ |
想在中断里延迟执行某个逻辑,但不想阻塞或耗时 | ✅ 推荐 ✅ |
想通过函数指针机制,实现中断与任务间的解耦通信 | ✅ 推荐 ✅ |
你不想用队列,但又想传递函数+参数到任务 | ✅ 推荐(一种轻量级替代方案) |