抢占式调度:
1. 高优先级的可以优先运行,即使以及有低优先级的在运行,会先停止低的再运行高的(优先级按数字大小分大小)
2. 高优先级任务不停止,低优先级的任务无法运行
3. 被抢占的任务会进入就绪态
时间片调度:
1. 同等优先级任务轮流享有相同的CPU占用时间(可设置),叫时间片,在Free RTOS中一个时间片等于SysTick(系统/滴答定时器)中断周期
2. 没有用完的时间直接丢掉,不会补偿,永远都是一个时间片
任务状态: 运行态,就绪态,阻塞态,挂起态(类似暂停,调用vTaskSuspend()进入挂起态,调用vTaskResume()解除挂起态进入就绪态)
仅就绪态可以直接进入运行态
任务创建与删除:****
1. 使用xTaskCreate() 动态方法创建任务
2. 使用xTaskCreateStatic() 多了一个Static(静态)表示静态方法创建任务
3. 使用vTaskDelete() 删除任务
4. 动态静态区别在分配内存的方法不同,静态由用户自己分配,动态由Free RTOS自己分配,在它管理的堆中
任务控制块也就是TCB,可以看作是任务的身份证,用于辨识任务。
创建任务具体流程:****
一:内存准备
- 申请任务堆栈内存(获取首地址)
- 申请任务控制块TCB内存(获取首地址)
- 将堆栈地址绑定到TCB的栈成员
二:TCB初始化
- 调用 prvInitialiseNewTask()
1 初始化堆栈(可选填0xA5)
2 记录栈顶地址 → pxTopOfStack
3 保存任务名称 → pcTaskName[]
4 保存任务优先级 → uxPriority
5 绑定状态列表项/事件列表项与TCB关系
三:加入就绪列表
5. 调用 prvAddNewTaskToReadyList()
1 全局操作:
-
- 任务计数器 uxCurrentNumberOfTasks++
- 若是首个任务 → 初始化任务列表
2 非首任务时:
-
- 若调度器未启动:比较新任务与当前任务优先级,更高则切换当前TCB指向
- 优先级位图操作:将 uxTopReadyPriority 对应优先级位置1(标记就绪)
- 按优先级升序插入就绪列表末尾(值越小优先级越高)
3 栈与句柄初始化:
-
- 调用 pxPortInitialiseStack 初始化寄存器上下文空间
- 任务句柄指向TCB地址
四:动态响应
触发任务切换条件:
若调度器已运行 且 新任务优先级 > 当前任务 → 立即执行任务切换
删除任务具体流程:
一:任务定位 *
- 获取目标TCB
-
- 通过任务句柄判断删除对象:
-
- NULL → 删除当前任务(自身)
- 有效句柄 → 删除指定任务
二:解除任务关联 ****
- 移除任务所有关联列表
-
- 将任务从所属队列中移除,包括:
-
- 就绪列表(Ready List)
- 阻塞列表(Blocked List)
- 挂起列表(Suspended List)
- 事件等待列表(Event List)
三:删除类型分流 *
- 根据删除类型执行操作
- 情况A:删除自身任务
-
- 将当前任务加入 等待删除列表
- 内存释放委托:由空闲任务(Idle Task)后续执行 prvDeleteTCB() 释放TCB及栈内存 情况B:删除其他任务
-
- 立即调用 prvDeleteTCB() 直接释放内存
- 全局更新:
○ 任务计数器减一(uxCurrentNumberOfTasks--)
○ 阻塞超时校准:检查被删任务是否为下一个待唤醒的阻塞任务 → 更新超时计时器
-
四:调度响应
- 强制切换条件:
-
- 若 调度器正在运行 且 删除的是自身任务 → 立即触发一次任务切换(上下文切换)****
R11-R4寄存器实际是内存,就是存放任务的具体内容,任务要运行就是把任务函数地址赋值给PC寄存器,M4,M7的EXC_RETURN的bit4可以修改,1表示不支持浮点数,0表示支持
每个优先级前有一位,用来判断是不是有任务,有就置 1,方便后续任务切换
挂起与恢复
挂起:类似暂停,可以恢复
FromISP后缀:中断中专用,Free RTOS管理的中断优先级为5-15,要注意中断时调用Free RTOS的API函数要保证中断优先级不高于它管理的最高优先级(数字低为高)
建议把所有优先级位指定为抢占优先级位,方便FreeRTOS管理
任务挂起函数流程:
一:任务定位
- 获取目标TCB
-
- 任务句柄为 NULL → 挂起当前任务(自身)
二:解除任务关联
- 移除任务所有状态列表
-
- 从以下列表中移除任务:
-
- 就绪列表(Ready List)
- 阻塞列表(Blocked List)
- 事件列表(Event List)
- 加入挂起列表
-
- 将任务插入 挂起列表(Suspended List) 末尾
三:调度环境判断
- 调度器运行中操作
-
- 更新下一次阻塞时间(防止被挂起任务阻塞后续任务唤醒)
- 若挂起自身 → 立即触发任务切换
- 调度器未运行操作
-
- 检查挂起任务数:
-
- 挂起数 = 总任务数 → 当前控制块置 NULL
- 否则 → 激活下一个最高优先级任务
任务恢复函数流程:
一:恢复条件校验
- 宏 INCLUDE_vTaskSuspend 必须配置为 1
- 禁止恢复运行中的任务
二:恢复操作
- 检查任务状态
-
- 任务在挂起列表中 → 移出挂起列表
- 加入就绪列表
-
- 将任务按优先级插入就绪列表末尾
- 触发切换判断
-
- 恢复任务优先级 > 当前任务 → 触发任务切换
中断恢复函数流程:
一:中断安全操作
- 关闭FreeRTOS管理的中断
-
- 保存 BASEPRI 寄存器原值
- 防止被其他中断打断
二:恢复操作判断
- 调度器状态检查
-
- 调度器未挂起:
-
- 若任务在挂起列表 → 移出并加入就绪列表
- 恢复任务优先级 > 当前任务 → 标记 xYieldRequired = pdTRUE
- 调度器已挂起:
-
- 将任务插入 等待就绪列表(延后处理)
三:环境还原与返回
- 恢复中断状态
-
- 还原 BASEPRI 寄存器值
- 返回切换标记
-
- 输出 xYieldRequired 供中断退出时判断是否切换任务
一:系统中断优先级配置寄存器
寄存器组:SHPR1、SHPR2、SHPR3
- 关键操作:
-
- 通过 SHPR3 配置 PendSV 和 SysTick 中断优先级
- 设定原则:将其设为最低优先级
- 设计目的:
-
- 确保系统任务切换(PendSV)和心跳调度(SysTick)
- 不阻塞其他高优先级中断的实时响应
二:中断屏蔽寄存器组
寄存器 | 功能描述 |
---|---|
PRIMASK | 屏蔽所有可配置优先级的中断 |
FAULTMASK | 屏蔽除NMI外的所有异常和中断 |
BASEPRI | 核心寄存器:屏蔽优先级低于设定阈值的中断 |
BASEPRI 工作机制
- 阈值控制逻辑:
-
- 写入值 0xN0(N为十六进制优先级阈值)
- 示例:设定 BASEPRI = 0x50 时:
-
- 屏蔽范围:优先级值 5~15(值越大优先级越低)
- FreeRTOS 依赖:
-
- 中断管理基石:通过动态调节BASEPRI实现:
-
- 临界区保护
- 中断嵌套控制
三:技术实质说明
- 优先级数值规则:
-
- 数值越小优先级越高(0为最高)
- SHPR3设置最低优先级即赋予最大数值
- 安全隔离设计:
-
- PendSV/SysTick 低优先级 → 避免抢占关键外设中断
- BASEPRI 阈值屏蔽 → 保障高实时性中断响应
- FreeRTOS 应用:
-
- portDISABLE_INTERRUPTS() 本质是设置BASEPRI阈值
- taskENTER_CRITICAL() 依赖此寄存器构建临界区
FreeRTOS中的列表实质上就是一个双向的环状链表,首尾相连,可以随意添加或删除列表项,有一个变量记录列表项数目,用于遍历,每个列表项有编号用于排序,初始化有一个末尾列表项,,它不计入列表数,编号最大。
两个校验值就是存放确定已知的常量,通过检查它是否变化来判断数据是否受到破坏,一般用于调试。
下面为迷你列表项,它也是列表项,但仅用于标记列表的末尾和挂载其他插入列表中的列 表项,用户是用不到迷你列表项的
1. vListInitialise()初始化列表就是把列表末尾项编号赋为最大,首尾指向它自己,再初始化两个校验值,传入参数为要初始化的列表。
2. vListInitialiseItem()初始化列表项就是把它所属列表项赋值为NULL,同样初始化两个校验值,传入参数为要初始化的列表项。
3. vListInsertEnd()列表项末尾插入列表项是一种无序的排列,是把它插入到列表 pxIndex 指向列表项的前面,不管它的编号大小,传入参数为列表和要插入的列表项。
4. vListInsert()列表插入列表项是一种有序排列,会根据编号来寻找合适的位置插入(如果最大就插入到末尾项前面,否则就遍历找位置)传入参数同上。
5. uxListRemove()列表移除列表项就是跟链表删除一样,传入要删除的列表项,返回值为删除后剩余列表项数目
任务切换出栈流程如下:首先通过三重指针(TCBTop->R3->R1->R0)获取TCB栈顶地址,这时R0指向R4也就是手动恢复起始点,就开始进行手动出栈,把栈内R4-R11寄存器储存的数据出栈到CPU的R4-R11寄存器,也就是用ldmia r0!, {r4-r11}把数据弹出到CPU,每弹出一个R0地址增加4字节,完成后R0指向xPSR寄存器,然后把R0的地址加载到PSP任务进程指针,这时PSP指向硬件自动恢复区起始点xPSR,就触发硬件自动恢复,最后再把R0清零,把R0的值赋值给basepri开启中断。
栈位置 | 寄存器 | 类型 | 作用说明 | |
---|---|---|---|---|
栈顶 (低地址) | R4 | 通用寄存器 | 任务内部变量(手动恢复区起始) | |
↑ | R5 | 通用寄存器 | 任务内部变量 | |
↑ | R6 | 通用寄存器 | 任务内部变量 | |
↑ | R7 | 通用寄存器 | 任务内部变量 | |
↑ | R8 | 通用寄存器 | 任务内部变量 | |
↑ | R9 | 通用寄存器 | 任务内部变量 | |
↑ | R10 | 通用寄存器 | 任务内部变量 | |
↑ | R11 | 通用寄存器 | 任务内部变量(手动恢复区结束) | |
-------- | ---------- | --------------------------- | ||
↑ | xPSR | 特殊功能寄存器 | 程序状态寄存器 • 恢复任务运行状态 • 包含Thumb模式标志(bit0=1) • 中断号等关键状态信息 | |
↑ | R15(PC) | 特殊功能寄存器 | 程序计数器 • 核心跳转控制 • 恢复时立即执行任务入口函数 • 实现执行流无缝切换 | |
↑ | R14(LR) | 特殊功能寄存器 | 链接寄存器 • 存储0xFFFFFFFD值 • 指示使用PSP恢复上下文 • 引导硬件自动恢复流程 | |
↑ | R12 | 通用寄存器 | 临时寄存器(自动恢复) | |
↑ | R3 | 通用寄存器 | 通用寄存器(自动恢复) | |
↑ | R2 | 通用寄存器 | 通用寄存器(自动恢复) | |
↑ | R1 | 通用寄存器 | 通用寄存器(自动恢复) | |
栈底 (高地址) | R0 | 通用寄存器 | 任务参数寄存器 • 存储pvParameters任务参数 • 作为任务函数第一个参数 |
关键寄存器协同机制表
寄存器 | 作用 | 恢复方式 | 协同机制说明 |
---|---|---|---|
R13(PSP) | 任务栈指针 | 软件设置 | • 通过MSR PSP, R0设置 • 定位硬件恢复起始位置 • 任务运行时动态维护栈顶 |
R14(LR) | 上下文恢复控制器 | 软件设置 | • 存储0xFFFFFFFD魔数 • 触发时指示硬件使用PSP恢复 • 控制处理器返回线程模式 |
R15(PC) | 执行流切换器 | 硬件自动恢复 | • 恢复时立即跳转至任务代码 • 实现任务无缝切换 • 决定程序执行位置 |
xPSR | 状态同步器 | 硬件自动恢复 | • 恢复中断前的程序状态 • 保证Thumb模式正常运行(bit0=1) • 维持中断/异常状态一致性 |
• 压栈操作:地址递减(栈顶向低地址移动)具体操作与出栈操作类似,自动压栈以后,先获取当前任务栈PSP,复制到r0,加载 pxCurrentTCB 的地址到 R3,获取 pxCurrentTCB 的值(当前任务TCB地址)到 R2(TCB->R3->R2),然后判断是否支持浮点数,支持就要多手动压栈几个寄存器,接着就手动压栈,把r4-r11的值压入PSP指向的栈的r4-r11寄存器,最后r0就指向r4地址。接着把r0的值也就是新的栈顶地址写入到r2指向的内存也就是它的TCB栈顶地址里,然后把r3(pxCurrentTCB指针地址)和r14(LR)手动压栈到当前任务堆栈也就是MSP里面
• 出栈操作:地址递增(栈顶向高地址移动)
• 切换流程:手动恢复(R4-R11)后,指针↑移动到xPSR,触发硬件自动↑恢复关键寄存器
压栈出栈构成的上下文切换都在PendSV中断服务函数里进行,自动压栈出栈使用的都是PSP,中断内其他行为使用的都是MSP。
切换任务:首先进行上述压栈操作,然后关中断,查找下一个要运行的任务的任务控制块,接着从主栈MPS获取r3和r14,r3指向的内存里现在储存的是新任务的地址,因为pxCurrentTCB指针总是指向当前任务优先级最高的,获取 pxCurrentTCB 的新值(新任务的TCB地址)获取新任务的栈顶地址(r3-> r1->r0),然后就是一样的出栈操作,最后使用 LR(EXC_RETURN) 返回中断模式
taskSELECT_HIGHEST_PRIORITY_TASK( )查找最高优先级的任务,就是通过前导置零指令获取32位的就绪列表前标志位的头部有几个零,以此获取最高优先级,然后通过下面这个函数让TCB指向当前任务,每触发一次PendSV中断就进来切换一次,会跳过末尾列表项,其实这个就是时间片调度
vTaskDelay(ticks)内部实现逻辑:
- 挂起调度器: 暂时禁止任务切换,保护内核数据结构。
- 计算唤醒时间: 获取当前系统节拍计数 xTickCount,将其加上请求的 ticks 得到目标唤醒时间 xWakeTime。
- 移出就绪态: 将当前任务的状态列表项从它所在的就绪列表或事件列表中移除。
- 加入延时队列:
-
- 设置任务列表项的值 xItemValue = xWakeTime。
- 根据 xWakeTime 是否小于等于当前 xTickCount(即是否发生了节拍计数器溢出),选择pxDelayedTaskList(当前周期)或pxOverflowDelayedTaskList(溢出周期) ,双列表自动处理溢出。
- 按 xItemValue(唤醒时间)升序插入到选定的延时列表中(确保链表头部总是最先唤醒的任务)。
- 恢复调度器: 允许任务切换,当前任务失去CPU进入阻塞态(Blocked)。
当系统节拍中断(SysTick)发生时:
- 计数递增: 全局节拍计数器 xTickCount 加1。
- 检查溢出(回绕): 如果 xTickCount 回绕到0,交换两个延时列表的角色(确保新周期的时间比较逻辑正确)。
- 检查到期任务: 检查 pxDelayedTaskList 头部任务:
-
- 若头部存在且其 xItemValue (唤醒时间) <= 当前 xTickCount,则该任务到期。
- 立即从延时列表中移除该到期任务,移回其优先级对应的就绪列表,状态变为就绪(Ready)。
- 重复检查和移除链表头部直到头部任务时间未到(链表的排序确保了这一步高效)。
- 标记待调度: 若到期任务中有更高优先级的任务或内核需要抢占当前任务,则设置调度器标记 xYieldPending = pdTRUE。
- 触发实际调度: 在 SysTick 中断退出前/时(通常通过 PendSV 中断),检查 xYieldPending。如果标记为真,执行一次上下文切换,让新就绪的高优先级任务得以运行。
队列****
一、队列基础概念
- 存在意义与作用
- 全局变量弊端:多任务操作时数据无保护(易损坏)
- 队列优势:
-
- 内置访问冲突保护机制
- API调用简单安全
- 不属于特定任务,支持多任务/中断读写
- 核心特性
- 数据结构:
-
- 默认先进先出(FIFO)
- 可配置后进先出(LIFO)
- 数据传递方式:
-
- 值传递(数据拷贝)
- 大数据建议传递指针
- 阻塞机制:
阻塞时间 | 行为描述 |
---|---|
0 | 直接返回(不等待) |
0 ~ port_MAX_DELAY | 等待设定时间(超时返回) |
port_MAX_DELAY | 永久阻塞(直到操作成功) |
- 功能扩展
- 基于队列实现的高级功能:
-
- 队列集(Queue Sets)
- 互斥信号量(Mutex)
- 计数信号量(Counting Semaphore)
- 递归互斥信号量(Recursive Mutex)
二、队列结构体(Queue_t)
- 核心成员
typedef struct QueueDefinition {
int8_t *pcHead; // 存储区起始地址
int8_t *pcWriteTo; // 下一个写入位置
List_t xTasksWaitingToSend; // 发送阻塞任务列表
List_t xTasksWaitingToReceive; // 接收阻塞任务列表
volatile UBaseType_t uxMessagesWaiting; // 当前消息数
UBaseType_t uxLength; // 队列容量
UBaseType_t uxItemSize; // 单消息字节数
} Queue_t;
- 关键成员功能
- pcWriteTo:实现FIFO/LIFO的核心
- uxMessagesWaiting:队列状态判断(0=空,uxLength=满)
- 阻塞列表:任务阻塞时挂入对应列表
三、队列API函数全解
- 队列创建
函数 | 类型 | 特点 |
---|---|---|
xQueueCreate() | 动态创建 | FreeRTOS自动管理堆内存 |
xQueueCreateStatic() | 静态创建 | 需用户预先分配内存空间 |
底层实现(xQueueGenericCreate):
-
计算队列总内存大小(控制块 + 存储区)
-
初始化队列指针和阻塞列表
-
写队列操作
操作类型 | 常规任务环境 | 中断环境(ISR) |
---|---|---|
尾部写入 | xQueueSend() | xQueueSendFromISR() |
xQueueSendToBack() | ||
头部写入 | xQueueSendToFront() | xQueueSendToFrontFromISR() |
覆盖写入 | xQueueOverwrite() | xQueueOverwriteFromISR() |
关键特性:
- 覆盖写入仅适用于单消息队列
- 所有写入最终调用xQueueGenericSend()
- 读队列操作
操作类型 | 常规任务环境 | 中断环境(ISR) |
---|---|---|
读取并删除 | xQueueReceive() | xQueueReceiveFromISR() |
读取不删除 | xQueuePeek() | xQueuePeekFromISR() |
关键区别:
- xQueueReceive会移动读指针
- xQueuePeek保持队列不变
写入队列:prvCopyDataToQueue() 函数实现逻辑
- 数据写入检查:
-
- 函数首先检查队列项目大小 (uxItemSize)。
- 如果 uxItemSize > 0 (表示队列存储实际数据),则执行数据拷贝操作。
- 如果 uxItemSize == 0 (例如信号量使用的队列,无实际数据负载),则跳过数据拷贝。
- 写入位置处理 (FIFO/LIFO):
-
- 尾部写入 (queueSEND_TO_BACK / FIFO 模式):
-
- 使用 memcpy 将用户数据 (pvItemToQueue) 复制到队列的当前写指针 (pcWriteTo) 指向的位置。
- 写指针向前移动一个项目大小的字节 (pcWriteTo += pxQueue->uxItemSize).
- 检查写指针是否达到或超过队列存储区尾部 (pcTail)。
- 如果越界,则将写指针回绕到存储区头部 (pcHead),实现环形缓冲区。
- 头部写入 (queueSEND_TO_FRONT / LIFO 模式):
-
- 使用 memcpy 将用户数据复制到队列的当前读指针 (u.xQueue.pcReadFrom) 指向的位置 减去一个项目大小 的位置 (相当于队列的逻辑“前端”)
- 更新读指针:将其向前移动一个项目大小 (pcReadFrom -= uxItemSize),为下次头部写入或LIFO读取做准备。
- 检查读指针是否小于存储区头部 (pcHead)。
- 如果越界,则将读指针回绕到存储区尾部 减去一个项目大小 的位置 (pcTail - uxItemSize)。
- 消息计数更新与唤醒机制:
-
- 原子性地增加队列的当前消息计数 (uxMessagesWaiting++)。
- 检查增加前的消息计数 (uxPreviousMessagesWaiting) 是否为0。
- 如果之前为0 (表示队列从空变为了非空状态),并且有任务阻塞在接收队列上:
-
- 进入临界区 (taskENTER_CRITICAL()) 保护唤醒操作。
- 检查队列的接收等待列表 (xTasksWaitingToReceive) 是否非空。
- 如果非空,则调用 xTaskRemoveFromEventList() 唤醒该列表中最高优先级的等待任务(将它从阻塞列表移到就绪列表)。
- 退出临界区 (taskEXIT_CRITICAL())。
- (在某些条件下,可能触发调度器切换 queueYIELD_IF_USING_PREEMPTION(),但通常在退出临界区后由其他机制处理)。
信号量:其实就是一种队列,是一种解决同步问题的机制,可以实现对共享资源的有序访问,它的数值代表资源量,每次被接收就减一,释放就加一,如果最大值锁定为1就是二值信号量,否则就是计数型信号量。二值信号量可能会导致优先级翻转,就是优先级低的能运行,高的反而不能运行,因为可能低的获取了信号量一直没释放,高的要切换任务接收不到信号量就阻塞了,让次一级的运行了。为了解决这种情况就有了互斥信号量,唯一区别就是会把低优先级的提到跟高优先级一样的优先级,避免了中间其他优先级的任务插手,让等待阻塞的时间最短,要注意互斥信号量不能用在中断里,因为中断不是任务,没有任务优先级,创建互斥信号量时还会自动释放一次信号量。
队列集:故名思意就是多个队列的集合,可以同时监听多个队列,任意一个队列都可以让它脱离阻塞
事件标志组:事件标志组是一组事件标志的集合, 可以简单的理解事件标志组,就是一个整数,除高8位以外每一位都可以用来表示一个事件,每个事件含义由用户自己定义,可以同时判断多位。它是一个uint16_t或uint32_t,跟信号量,队列的区别就是它可以选择读取后消除或不消除,可以同时被多处读取,类似广播,队列信号量就不行。
任务通知:
有四种更新方式:覆盖/不覆盖接受任务的通知值;更新接受任务通知值的一个或多个bit;增加接受任务的通知值
优势:效率更高,比使用队列,事件标志组或信号量要更快;使用内存更小,因为无需额外创建结构体
劣势:无法发送数据给ISR,因为ISR没有任务结构体,但ISR可以使用任务通知功能发送数据给任务;无法广播给多个任务;无法缓存多个数据,因为任务通知是通过更新任务通知值来发送数据的,而任务结构体中只有一个任务通知值,就只能保持一个数据;发送方不接受阻塞,不能进入阻塞等待
软件定时器
1. 依赖于系统时钟频率,是具有定时功能的软件,可以设置定时周期,当指定事件到达后要用回调函数(也称超时函数),用户在回调函数中处理信息,缺点是精度没有那么高(因为它以系统时钟为基准,系统时钟中断优先级又是最低,容易被打断)。 对于需要高精度要求的场合,不建议使用软件定时器。
-
软件定时器的超时回调函数是由软件定时器服务任务调用的,软件定时器的超时回调函数本身不是任务,因此不能在该回调函数中使用可能会导致任务阻塞的 API 函数。在调用函数 vTaskStartScheduler()开启任务调度器的时候,会创建一个用于管理软件定时器的任务,这个任务就叫做软件定时器服务任务,它的作用: 负责软件定时器超时的逻辑判断;调用超时软件定时器的超时回调函数;处理软件定时器命令队列
-
有单次定时器和周期定时器,休眠态(没有运行,所以其定时超时回调函数不会被执行)和运行态,新创建的软件定时器处于休眠状态 ,也就是未运行的
-
回调函数要尽快实行,不能进入阻塞状态,即不能调用那些会阻塞任务的 API 函数,如:vTaskDelay();访问队列或者信号量的非零阻塞时间的 API 函数也不能调用。
Tickless模式就是让运行空闲任务的时间变为睡眠模式,用的是_WFI进入睡眠,任何中断都可以打断它,因此为了避免滴答定时器打断休眠,就会先关闭滴答定时器,再根据休眠时间补偿回去,确保时钟精准。
内存:
因为标准C库里的malloc()和free()没有保证线程安全,并且会占用大量代码空间,因此不适合直接用在FreeRTOS里,Free RTOS提供了五种动态内存分配的算法
heap_1:就是直接定义一个大数组,需要就分配给你。(不允许内存释放)
heap_2:使用最适应算法管理内存,支持释放内存,但它不能把相邻的内存块合并,多次申请释放不同大小的内存就会导致内存变得碎片,产生内存碎片,难以再有足够大的内存可以申请。(最适应算法:这里的heap空闲内存是按的大小从小到大排列,会自动找比申请内存大最少的内存块来分配,比如说要申请20的内存,然后有25,50大小的两个内存块,它就会优先把25的分配给它,剩下的5内存就仍是空闲状态,可以给别人继续使用,前提是申请的内存要小于或等于5,会产生的碎片也就是这些)
heap_3:就是多加了个关闭开启任务调度器,防止任务切换干扰内存管理,保证线程安全
heap_4: 使用首次适应算法,支持内存申请与释放,并且能够把相邻的空闲内存块进行合并(首次适应算法:这里的内存按地址从低到高排列,先找到哪个内存块大小大于或等于要申请的内存就直接使用它,然后把剩下的留给别人继续使用,相连有空闲的就合并在一起)
heap_5: 就是在heap_4的基础上加上管理多个非连续内存区域的能力,但它没有定义默认内存堆,就需要手动指定内存区域信息,对其进行初始化,使用下面这个结构体来指定
heap_4:
初始化内存堆 prvHeaplint() :
先获取内存堆的大小和起始地址,然后进行八字节对齐,对其过程中就可能舍弃部分内存,对齐后又有新的起始地址和内存堆大小。然后将xStart内存块指向下一个空闲地址,也就是对齐后的地址,并且xStart内存块大小为0,接着把pxEnd内存块初始地址赋值为(末尾地址 – 内存块结构体大小),设置End,然后记录第一个空闲内存块的地址和大小,以及指向的下一个内存块,记录历史堆栈剩余最小值和当前堆栈剩余值,然后把xBlockAllocatedBit的最高位置1,就是限制内存块的大小。
内存堆:
heap_4:
空闲内存链表的插入:prvInsertBlockIntoFreeList( ):
一些普通的链表插入,不过让内存块地址从小到大排了,会获取到要插入位置的前面一个的地址(我把地址两位来表示,比较方便,比如要在20,50之间插入25,就会获取20的地址)插入以后就要判断是不是应该合并,先判断新插入的地址会不会在这个内存块内,会就合并,不会就插入到找到的下一个的前面(也就是50前面),然后判断要插入的这个内存块会不会把下一个的首地址包含了(就是25后续的内存空间里会不会有50的首地址),会就把下一个合并在插入的这个里(就是把50合并到25里),要注意不能合并pxEnd
内存申请:pvPortMalloc( ):
先判断是不是初始化好,没有就进行初始化,接着判断要申请的内存大小有没有超过限制,没有就开始申请,申请的内存大小加上内存结构体的大小还有八字节对齐浪费的一部分内存就是真正要申请的内存,堆中有可以分配的内存块就继续分配,先遍历链表找到合适大小的内存块,然后返回内存块的地址,把这个内存块移出空闲链表,如果有剩余的内存就把剩余的内存移入空闲块链表,然后更新内存堆剩余可分配内存大小以及历史剩余最小堆栈,还有更新申请成功次数
内存释放:vPortFree( ):
先将需要释放的内存块的地址进行偏移,偏移到起始地址,也就是让内存块结构体也包含在要释放的内存里,然后判断内存块是否已经被申请,只有被申请的才能释放,判断通过后判断待释放内存块是否不在空闲块链表里,不在就把它标记为未分配,更新空闲内存块的大小,然后把释放的内存块插入到空闲列表里,并且更新释放成功次数