免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

FreeRTOS学习(十一)- 核心重点汇总

FreeRTOS学习(十一)- 核心重点汇总 基于 FreeRTOS V10.x 源码与官方手册提炼内核最核心的设计与实现。 目标深入浅出看完即理解 RTOS 的心脏如何跳动。目录总体架构任务管理列表——调度器的数据骨架调度器三大支柱PendSV 上下文切换时间管理与 Tick 中断任务间通信优先级继承软件定时器内存管理启动流程速查表1. 总体架构FreeRTOS 是一款抢占式优先级调度的实时内核核心代码极少约 9000 行 C 少量汇编设计哲学是只做调度不管外设。┌─────────────────────────────────────────────┐ │ 应用程序用户任务 │ ├─────────────────────────────────────────────┤ │ FreeRTOS API 层 │ │ task.h queue.h timers.h semphr.h │ ├───────┬───────┬───────┬───────┬───────────────┤ │ tasks │ queue │timers │ list │ portable │ │ .c │ .c │ .c │ .c │ port.c(heap) │ ├───────┴───────┴───────┴───────┴───────────────┤ │ 硬件抽象层port.c │ │ PendSV / SVC / SysTick │ ├─────────────────────────────────────────────┤ │ Cortex-M3/M4/M7 硬件 │ └─────────────────────────────────────────────┘三大核心机制理解这三个就理解了 FreeRTOS 80%机制作用实现优先级就绪表O(1) 找到最高优先级任务pxReadyTasksLists[]数组 uxTopReadyPriorityTick 中断状态机推进时间、唤醒延时任务xTaskIncrementTick()PendSV 上下文切换保存旧任务 / 恢复新任务xPortPendSVHandler()汇编2. 任务管理2.1 TCB任务控制块每个任务拥有一个 TCB记录任务运行所需的全部信息typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; // 栈顶指针必须是第一个字段 ​ ListItem_t xStateListItem; // 状态列表项内嵌非指针 ListItem_t xEventListItem; // 事件列表项内嵌非指针 ​ UBaseType_t uxPriority; // 当前优先级 StackType_t *pxStack; // 栈底地址用于释放 char pcTaskName[configMAX_TASK_NAME_LEN]; ​ UBaseType_t uxBasePriority; // 优先级继承前的基优先级 TaskHandle_t xMutexHolder; // 持有的互斥量 UBaseType_t uxMutexesHeld; // 持有互斥量计数 ​ // ... 省略调试、MPU、通知等字段 } tskTCB;关键设计pxTopOfStack必须是结构体第一个字段。因为 PendSV 汇编通过pxCurrentTCB指向 TCB直接偏移 0 来读写栈顶要求零偏移。2.2 任务状态机xTaskCreate │ ▼ ┌────────┐ 调度器选中 ┌────────┐ │ Ready │─────────────→│Running │ │ (就绪) │←─────────────│ (运行) │ └────────┘ 时间片/阻塞 └────────┘ ↑ │ │ │ │ vTaskDelay │ vTaskDelay │ │ /队列阻塞 │ /队列阻塞 │ ▼ ▼ │ ┌────────┐ ┌────────┐ │ │Blocked │──────────│Blocked │ │ │(延时/ │ 时间到 │(等待 │ 事件就绪 │ │ 等待) │ │ 事件) │ │ └────────┘ └────────┘ │ │ │ vTaskSuspend │ └──────┐ ┌─────────────┘ ▼ ▼ ┌────────────┐ │ Suspended │ │ (挂起) │ └────────────┘ │ vTaskDelete ▼ ┌────────────┐ │ Deleted │ │ (已删除) │ └────────────┘状态所在列表xItemValue 含义ReadypxReadyTasksLists[prio]优先级不用于排序Blocked(延时)pxDelayedTaskList唤醒时刻绝对 tickBlocked(等待事件)延时表 事件表的xEventListItem唤醒时刻 优先级SuspendedxSuspendedTaskList不使用Running不在任何列表从就绪表取出—2.3 任务创建流程BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, ...) { // 1. 分配 TCB 栈 pxNewTCB pvPortMalloc(sizeof(TCB_t) usStackDepth); // 2. 初始化栈伪造一次异常返回帧 pxNewTCB-pxTopOfStack pxPortInitialiseStack( pxStack, pxTaskCode, pvParameters); ​ // 3. 初始化两个 ListItem vListInitialiseItem(pxNewTCB-xStateListItem); vListInitialiseItem(pxNewTCB-xEventListItem); listSET_LIST_ITEM_OWNER(pxNewTCB-xStateListItem, pxNewTCB); listSET_LIST_ITEM_OWNER(pxNewTCB-xEventListItem, pxNewTCB); // 4. 设优先级加入就绪表 pxNewTCB-uxPriority uxPriority; prvAddTaskToReadyList(pxNewTCB); // 5. 如果调度器已启动触发切换 if (xSchedulerRunning) portYIELD_WITHIN_API(); }栈初始化的精妙之处——pxPortInitialiseStack在栈上伪造一个异常返回帧高地址 │ xPSR 0x01000000 ← Thumb 模式 │ PC task_func ← 任务入口 │ LR prvTaskExit ← 任务返回时调用 vTaskDelete │ R12 0 │ R3, R2, R1 ← 参数通过 R0 传递 │ R0 pvParameters ← 任务参数 │ R11 ~ R4 ← 软件帧初始为 0异常返回后弹出 低地址 ← pxTopOfStack 指向这里这样第一次调度时PendSV 的ldmia r0!, {r4-r11}弹出 R4-R11bx r14触发硬件弹出 R0-R3/PC/xPSRCPU 就从task_func开始执行参数通过 R0 传入。3. 列表——调度器的数据骨架3.1 核心数据结构FreeRTOS 的列表是带哨兵节点的双向循环链表// 精简版哨兵用省内存 typedef struct xMINI_LIST_ITEM { TickType_t xItemValue; // 排序键值 struct xLIST_ITEM *pxNext; // 后继 struct xLIST_ITEM *pxPrevious; // 前驱 } MiniListItem_t; ​ // 完整版任务用 typedef struct xLIST_ITEM { TickType_t xItemValue; struct xLIST_ITEM *pxNext; struct xLIST_ITEM *pxPrevious; void *pvOwner; // 指向宿主通常是 TCB struct xLIST *pxContainer; // 回指所属列表O(1) 删除关键 } ListItem_t; ​ // 列表头 typedef struct xLIST { UBaseType_t uxNumberOfItems; // 节点数 ListItem_t *pxIndex; // 遍历游标 MiniListItem_t xListEnd; // 哨兵节点 } List_t;3.2 哨兵节点 xListEnd这是 FreeRTOS 列表最精妙的设计xListEnd.xItemValue portMAX_DELAY永远最大空列表时xListEnd.pxNext xListEnd.pxPrevious xListEnd自引用成环遍历插入时无需判 NULL循环条件pxNext-xItemValue xValue必然在哨兵处终止3.3 四种核心操作操作函数排序用途有序插入vListInsert()按 xItemValue 升序延时表末尾插入vListInsertEnd()不排序插到 pxIndex 前就绪表删除uxListRemove()O(1)通过 pxContainer 定位所有表轮询取节点listGET_OWNER_OF_NEXT_ENTRY()pxIndex 后移时间片轮转vListInsert有序插入延时表用for (pxIterator pxList-xListEnd; pxIterator-pxNext-xItemValue xValueOfInsertion; pxIterator pxIterator-pxNext) { /* 空循环体 */ } // 插入到 pxIterator 之后哨兵的 MAX 值保证循环必然终止。相同值插在已有节点之后保证同优先级任务公平轮转。vListInsertEnd末尾插入就绪表用// 插入到 pxIndex 之前不是真正的末尾 pxNewListItem-pxNext pxIndex; pxNewListItem-pxPrevious pxIndex-pxPrevious; pxIndex-pxPrevious-pxNext pxNewListItem; pxIndex-pxPrevious pxNewListItem;新任务排在pxIndex前意味着它要等所有已有任务都执行过才轮到——配合listGET_OWNER_OF_NEXT_ENTRY实现时间片轮转。uxListRemoveO(1) 删除List_t *pxList pxItemToRemove-pxContainer; // 反向定位列表 pxItem-pxNext-pxPrevious pxItem-pxPrevious; pxItem-pxPrevious-pxNext pxItem-pxNext; pxItem-pxContainer NULL;通过pxContainer反向指针无需遍历列表即可定位实现 O(1) 删除。3.4 TCB 与列表的关系TCB内嵌非指针两个 ListItem┌─────────────────────────────┐ │ TCB_t │ │ pxTopOfStack │ │ ┌─────────────────────┐ │ 挂在状态表就绪/延时/挂起 │ │ xStateListItem │────┼────→ pxReadyTasksLists[prio] │ │ pvOwner → 自身TCB │ │ 或 pxDelayedTaskList │ │ pxContainer → 所属表 │ │ 或 xSuspendedTaskList │ └─────────────────────┘ │ │ ┌─────────────────────┐ │ 挂在事件表队列/信号量等待 │ │ xEventListItem │────┼────→ xTasksWaitingToSend │ │ pvOwner → 自身TCB │ │ 或 xTasksWaitingToReceive │ │ pxContainer → 所属表 │ │ │ └─────────────────────┘ │ │ uxPriority │ └─────────────────────────────┘xStateListItem表示任务状态同时只能挂在一个状态表xEventListItem表示任务在等待事件可同时挂在一个事件表pvOwner指回 TCB实现双向反查列表节点→TCBO(1)TCB→列表节点直接访问内嵌字段4. 调度器三大支柱4.1 支柱一优先级就绪表// 就绪表优先级数组每个优先级一条链表 List_t pxReadyTasksLists[configMAX_PRIORITIES]; ​ // 位图/变量记录最高非空优先级 volatile UBaseType_t uxTopReadyPriority;选最高优先级任务taskSELECT_HIGHEST_PRIORITY_TASK宏// 从 uxTopReadyPriority 向下找第一个非空链表 while (listLIST_IS_EMPTY(pxReadyTasksLists[uxTopPriority])) --uxTopPriority; ​ // 从该链表轮询取一个任务 listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, pxReadyTasksLists[uxTopPriority]);为什么用数组而不是优先级队列数组下标即优先级O(1) 定位链表uxTopReadyPriority记录最高非空优先级避免从 0 扫描优先级数量有限通常 ≤32数组开销可接受4.2 支柱二Tick 中断状态机每次 SysTick 中断调用xTaskIncrementTick()BaseType_t xTaskIncrementTick(void) { // 1. 检查是否有任务延时到期 if (listLIST_IS_EMPTY(pxDelayedTaskList) pdFALSE) { // 延时表按唤醒时间排序只看队头 pxTCB listGET_ITEM_VALUE_OF_HEAD_ENTRY(pxDelayedTaskList); if (xConstTickCount pxTCB) { // 队头到期移到就绪表 listREMOVE_ITEM(...); prvAddTaskToReadyList(pxTCB); // 继续检查新队头可能多个同时到期 } } ​ // 2. 递增 tick xConstTickCount; ​ // 3. tick 溢出则交换延时表双缓冲 if (xConstTickCount 0) taskSWITCH_DELAYED_LISTS(); ​ // 4. 时间片轮转同优先级多任务时请求切换 if (当前运行任务优先级 最高就绪优先级) xSwitchRequired pdTRUE; ​ return xSwitchRequired; }延时表双缓冲处理 tick 溢出正常情况 pxDelayedTaskList → ListA pxOverflowDelayedTaskList → ListB 溢出后 taskSWITCH_DELAYED_LISTS() 交换两个指针 pxDelayedTaskList → ListB pxOverflowDelayedTaskList → ListA当xTimeToWake xConstTickCount唤醒时刻跨过溢出点任务放入溢出表。tick 归零后交换指针O(1) 完成切换。4.3 支柱三PendSV 上下文切换详见第 5 节。5. PendSV 上下文切换5.1 为什么用 PendSVPendSV 优先级设为最低确保所有中断处理完再切换避免在中断嵌套中进行复杂上下文切换Cortex-M 硬件设计支持 PendSV 异常延迟触发5.2 栈帧布局64 字节每次切换栈上保存完整 16 个寄存器高地址 │ xPSR ─┐ │ PC │ 硬件帧32B │ LR │ CPU 异常进入/返回时自动压栈/出栈 │ R12 │ │ R3 │ │ R2 │ │ R1 │ │ R0 ─┘ ← PendSV 入口时 PSP 指向这里 │ R11 ─┐ │ R10 │ 软件帧32B │ R9 │ stmdb/ldmia 手动压栈/出栈 │ R8 │ │ R7 │ │ R6 │ │ R5 │ │ R4 ─┘ ← 手动压栈后 PSP 指向这里 低地址为什么硬件只保存 R0-R3、R12、LR、PC、xPSRAAPCS 规定 R0-R3、R12 是调用者保存caller-saved函数调用后可能被破坏R4-R11 是被调用者保存callee-saved编译器不会主动保存。RTOS 必须手动保存 R4-R11。5.3 汇编实现三阶段// xPortPendSVHandler (naked 函数编译器不生成 prologue/epilogue) __asm void xPortPendSVHandler(void) { mrs r0, psp // 1. 读当前 PSP 到 R0 isb // 2. 指令同步屏障 ​ ldr r3, pxCurrentTCB // 3. R3 pxCurrentTCB ldr r2, [r3] // 4. R2 pxCurrentTCB ( TCB_A) ​ // 阶段一保存旧任务 stmdb r0!, {r4-r11} // 5. R4-R11 压入 PSP 栈R0 递减 32 str r0, [r2] // 6. TCB_A.top R0新栈顶 ​ // 阶段二切换上下文 stmdb sp!, {r3, r14} // 7. R3 和 LR 压入 MSP保护 mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 // 8-9. 屏蔽低优先级中断非全局关中断 bl vTaskSwitchContext // 10. C 函数选下一个最高优先级任务 mov r0, #0 msr basepri, r0 // 11-12. 解除中断屏蔽 ldmia sp!, {r3, r14} // 13. 从 MSP 恢复 R3 和 LR ​ // 阶段三恢复新任务 ldr r1, [r3] // 14. R1 pxCurrentTCB ( TCB_B) ldr r0, [r1] // 15. R0 TCB_B.top新栈顶 ldmia r0!, {r4-r11} // 16. 从新栈弹出 R4-R11R0 递增 32 msr psp, r0 // 17. PSP R0新栈顶 isb // 18. 指令同步屏障 bx r14 // 19. 异常返回硬件弹出 R0-R3/R12/LR/PC/xPSR }5.4 关键设计细节1. 为什么用 naked 函数编译器不生成 prologue/epilogue因为上下文切换需要完全手动控制寄存器。2. 阶段二为什么把 R3、R14 压入 MSPvTaskSwitchContext是 C 函数会自由使用 R3-R14。PSP 栈此时存储旧任务上下文不能污染。所以把 R3保存pxCurrentTCB和 R14LR/EXC_RETURN压入 MSP主栈调用后恢复。3. 为什么用 BASEPRI 而非 CPSID Imsr basepri, r0只屏蔽优先级 ≥configMAX_SYSCALL_INTERRUPT_PRIORITY的中断高优先级中断仍可响应减少中断延迟。4.bx r14的魔法R14LR在异常进入时被硬件设为EXC_RETURN如0xFFFFFFFD指明返回后使用 PSP 且进入线程模式。bx r14触发异常返回硬件自动弹出 R0-R3/R12/LR/PC/xPSR跳转到新任务。5. STMDB / LDMIA 的对称性stmdb r0!, {r4-r11}Store Multiple Decrement Before先减后存R0 递减 32ldmia r0!, {r4-r11}Load Multiple Increment After先取后增R0 递增 32两者完全对称保证栈帧布局一致。6. 时间管理与 Tick 中断6.1 延时机制// 相对延时从调用时刻起延时 xTicksToDelay 个 tick void vTaskDelay(const TickType_t xTicksToDelay) { xTimeToWake xTickCount xTicksToDelay; listSET_LIST_ITEM_VALUE(pxCurrentTCB-xStateListItem, xTimeToWake); if (xTimeToWake xTickCount) // 溢出 vListInsert(pxOverflowDelayedTaskList, ...); else vListInsert(pxDelayedTaskList, ...); // 按唤醒时间有序 taskYIELD(); // 立即切换走 } ​ // 绝对延时延时到某个绝对 tick 时刻用于周期任务 void vTaskDelayUntil(TickType_t *pxPreviousWakeTime, TickType_t xTimeIncrement);相对延时 vs 绝对延时的区别相对延时 vTaskDelay(100) 执行(20ms) → delay(100) → 执行(20ms) → delay(100) → ... 周期 100 20 120 tick受执行时间影响 ​ 绝对延时 vTaskDelayUntil(last, 100) 执行(20ms) → delay(80) → 执行(20ms) → delay(80) → ... 周期 100 tick固定周期不受执行时间影响6.2 唤醒检查xTaskIncrementTick只检查延时表队头O(1)延时表按唤醒时间升序 xListEnd(MAX) ↔ T1(wake1048) ↔ T2(wake1055) ↔ T3(wake1060) ↔ xListEnd ↑ 队头 只检查这一个 ​ xTickCount1048 1048 → T1 到期移到就绪表 xTickCount1048 1055 → T2 未到期停止检查7. 任务间通信7.1 队列Queue队列是 FreeRTOS 最核心的 IPC 机制信号量、互斥量都是基于队列实现的。typedef struct QueueDefinition { int8_t *pcHead; // 缓冲区头 int8_t *pcTail; // 缓冲区尾 int8_t *pcWriteTo; // 下一个写入位置 union { int8_t *pcReadFrom; // 下一个读取位置 } u; ​ List_t xTasksWaitingToSend; // 等待发送的任务队列满时阻塞 List_t xTasksWaitingToReceive;// 等待接收的任务队列空时阻塞 ​ volatile UBaseType_t uxMessagesWaiting; // 当前消息数 UBaseType_t uxLength; // 队列容量 UBaseType_t uxItemSize; // 每条消息大小 ​ // 互斥量专用 uint8_t ucQueueType; int8_t cRxLock, cTxLock; // 锁定计数临界区内缓存操作 } Queue_t;环形缓冲区工作原理容量5, 已有3条消息 ​ pcHead │ ▼ ┌──┬──┬──┬──┬──┐ │A │B │C │ │ │ └──┴──┴──┬┘┴──┴──┘ │ ↑ pcReadFrom pcWriteTo (下次读这里) (下次写这里) ​ 写入DpcWriteTo 位置放DpcWriteTo 前移 读取 pcReadFrom 位置取ApcReadFrom 前移7.2 队列发送/接收流程xQueueSend(queue, item, timeout): ┌─ 队列未满 │ 是 → prvCopyDataToQueue写入环形缓冲区 │ → 检查 xTasksWaitingToReceive 是否有人等 │ 有 → 唤醒最高优先级等待者可能立即切换 │ 否 → 加入 xTasksWaitingToSend阻塞等待 │ xStateListItem → 延时表xEventListItem → 发送等待表 └─ 返回 ​ xQueueReceive(queue, item, timeout): ┌─ 队列非空 │ 是 → prvCopyDataFromQueue从环形缓冲区读取 │ → 检查 xTasksWaitingToSend 是否有人等 │ 有 → 唤醒最高优先级发送者拷贝其数据直接入队 │ 否 → 加入 xTasksWaitingToReceive阻塞等待 └─ 返回精妙设计唤醒等待者时直接数据搬运。当接收者唤醒发送者时如果队列恰好有空间FreeRTOS 会把发送者要发的数据直接拷贝到队列而不需要发送者醒来再写一次。这减少了一次上下文切换。7.3 信号量与互斥量信号量和互斥量都是长度为 0 的特殊队列类型uxLengthuxItemSizeuxMessagesWaiting特殊功能二值信号量100 或 1—计数信号量N00~N—互斥量101初始可用优先级继承递归互斥量101 持有计数优先级继承 可重入为什么用队列实现信号量复用队列的阻塞/唤醒机制等待列表uxMessagesWaiting即信号量计数值give 向队列发一条空消息uxMessagesWaitingtake 从队列收一条空消息uxMessagesWaiting--8. 优先级继承8.1 优先级反转问题时间 → ​ T1(H) ──────────┐ ┌──────── (被阻塞等互斥量) │ │ T2(M) ┌─────┘ └────── (就绪抢占T3) │ T3(L) ─────┴─── 持有互斥量 ──── 释放 ──→ ​ 问题T3 持有互斥量T1 等待。T2 抢占 T3 执行 导致 T1最高优先级被 T2中优先级间接阻塞。 优先级反转8.2 优先级继承机制当高优先级任务等待互斥量时持有者临时继承等待者的优先级T1(H) ──────────┐ ┌──── (获得互斥量) │ T3 继承 H 优先级 │ T3(L→H) 持有 ──┴────────────────────┘ 释放恢复 L 优先级 ↑ T3 以 H 优先级运行 ↑ T2(M) 无法抢占实现代码简化// 互斥量 take 时如果已被占用 void prvAddCurrentTaskToDelayedList(...) { // 当前任务高优先级H阻塞等待互斥量 // 找到互斥量持有者 pxMutexHolder pxQueue-pxMutexHolder; if (pxMutexHolder-uxPriority pxCurrentTCB-uxPriority) { // 继承持有者优先级提升到 H vTaskPriorityInherit(pxMutexHolder, pxCurrentTCB-uxPriority); // 持有者从原就绪表移出加入 H 的就绪表 } } ​ // 互斥量 give 时 BaseType_t xQueueSemaphoreGive(Queue_t *pxQueue) { // 唤醒等待者 // 恢复持有者原始优先级 pxMutexHolder-uxPriority pxMutexHolder-uxBasePriority; // 从当前优先级就绪表移出加入原始优先级就绪表 }8.3 优先级继承的限制只解决一层反转A→B→C 链式持有互斥量时只继承一层不适用于信号量信号量没有明确的持有者概念递归互斥量记录持有计数可被同一任务多次 take每次 give 减计数9. 软件定时器9.1 架构FreeRTOS 软件定时器不是硬件中断而是在定时器守护任务的上下文中执行回调┌─────────────┐ 命令队列 ┌──────────────┐ │ 用户任务 │───TimerCmd──→│ 定时器守护任务 │ │ xTimerStart │ │ prvTimerTask │ │ xTimerStop │ │ │ └─────────────┘ └──────┬───────┘ │ 回调执行 ▼ ┌─────────────┐ │ 定时器回调 │ │ (守护任务上下文)│ └─────────────┘9.2 定时器控制块typedef struct tmrTimerControl { const char *pcTimerName; // 名字 ListItem_t xTimerListItem; // 挂在活动列表按到期时间排序 TickType_t xTimerPeriodInTicks;// 周期 void *pvTimerID; // 用户标识 TimerCallbackFunction_t pxCallbackFunction; // 回调 UBaseType_t uxAutoReload; // 1周期, 0单次 TaskHandle_t xTimerTaskHandle; // 守护任务句柄 } Timer_t;9.3 工作流程用户调用xTimerStart()→ 构造命令消息 → 发送到命令队列守护任务prvTimerTask从命令队列取出消息计算到期时间将定时器按到期时间有序插入活动列表守护任务阻塞到最早到期时间xTaskDelayUntil到期后执行回调如果是周期定时器则重新计算下次到期时间注意回调在守护任务上下文执行不能阻塞回调执行会阻塞其他定时器回调回调中不能调用vTaskDelay、xQueueReceive等阻塞 API应使用FromISR版本的 API 或xTimerPendFunctionCall10. 内存管理FreeRTOS 提供 5 种 heap 实现不依赖标准 malloc/free10.1 五种 heap 对比实现支持 free碎片合并非连续内存适用场景heap_1否—否只创建不删除极简heap_2是否否创建删除大小固定heap_3是依赖编译器否包装标准 malloc/freeheap_4是是否最常用heap_5是是是多段非连续内存10.2 heap_4 详解最常用核心数据结构——空闲块链表typedef struct A_BLOCK_LINK { struct A_BLOCK_LINK *pxNextFreeBlock; // 下一个空闲块 size_t xBlockSize; // 本块大小含头 } BlockLink_t; ​ // 两个特殊块 static BlockLink_t xStart, xEnd; // xStart.pxNextFreeBlock → 第一个空闲块 // ... → 最后一个空闲块 → xEndxBlockSize portMAX_DELAY哨兵空闲链表按地址排序这是 heap_4 的关键使合并相邻块成为可能xStart → [BlockA: 0x2000-0x202F] → [BlockB: 0x2040-0x206F] → xEnd 48字节 48字节malloc 流程从 xStart 遍历空闲链表找到第一个足够大的块首次适配如果块远大于需求分裂为两块使用块 剩余空闲块从链表移除使用块返回指针跳过块头free 流程根据释放块地址按地址顺序插入空闲链表检查前一个空闲块是否相邻相邻则合并检查后一个空闲块是否相邻相邻则合并free 前... → [Free: 0x2000-0x200F] → [Used: 0x2010-0x201F] → [Free: 0x2020-0x202F] → ... free 后... → [Free: 0x2000-0x202F (合并后48字节)] → ...10.3 heap_5 的非连续内存heap_5 支持多段不连续内存区域HeapRegion_t xHeapRegions[] { { (uint8_t *)0x20000000, 0x10000 }, // 内部SRAM 64KB { (uint8_t *)0x68000000, 0x800000 }, // 外部SDRAM 8MB { NULL, 0 } // 结束标记 }; ​ void vApplicationDaemonTaskStartupHook(void) { vPortDefineHeapRegions(xHeapRegions); // 必须在使用前调用 }初始化时将每段内存初始化为空闲块通过pxNextFreeBlock串成链表。合并只在同一区域内进行跨区域地址不连续无法合并。11. 启动流程main() │ ├─ HAL_Init() // 硬件初始化 ├─ xTaskCreate(Task1, ...) // 创建任务加入就绪表 ├─ xTaskCreate(Task2, ...) │ └─ vTaskStartScheduler() // 启动调度器 │ ├─ 初始化空闲任务、定时器任务 ├─ portDISABLE_INTERRUPTS() ├─ xPortStartScheduler() │ │ │ ├─ 配置 SysTick产生 tick 中断 │ ├─ 设置 PendSV 优先级为最低 │ ├─ 配置 NVIC │ │ │ └─ prvPortStartFirstTask() │ │ │ ├─ ldr r0, pxCurrentTCB // 取第一个任务 │ ├─ ldr r0, [r0] │ ├─ ldr r0, [r0] // 取栈顶 │ ├─ msr msp, r0 // 恢复主栈 │ │ │ └─ SVC 0 // 触发 SVC 异常 │ │ │ └─ vPortSVCHandler() │ ├─ 从 TCB 取栈顶到 PSP │ ├─ msr control, r0 // 切换到 PSP │ └─ bx r14 // 异常返回 │ │ │ └→ 硬件弹出 R0-R3/PC/xPSR │ → 从任务函数开始执行 │ └─ (永远不会返回)SVC 启动第一个任务的精妙之处prvPortStartFirstTask不能直接bx r14因为还没有异常上下文通过SVC 0主动触发异常进入 handler 模式vPortSVCHandler设置好 PSP 后bx r14硬件异常返回机制弹出栈帧CPU 以为是从某个中断返回实际开始执行第一个任务12. 速查表12.1 关键文件文件作用tasks.c任务管理、调度器、状态机queue.c队列、信号量、互斥量timers.c软件定时器list.c双向循环链表portable/GCC/ARM_CM3/port.cCortex-M3 移植层PendSV/SVC/SysTickportable/MemMang/heap_4.c内存管理最常用12.2 关键函数函数作用xTaskCreate()创建任务vTaskDelay()相对延时vTaskDelayUntil()绝对延时周期任务vTaskSuspend()/vTaskResume()挂起/恢复任务vTaskDelete()删除任务vTaskStartScheduler()启动调度器xTaskIncrementTick()tick 中断处理vTaskSwitchContext()选择下一个任务xQueueSend()/xQueueReceive()队列收发xSemaphoreTake()/xSemaphoreGive()信号量/互斥量xTimerStart()/xTimerStop()定时器控制pvPortMalloc()/vPortFree()内存分配/释放12.3 关键宏宏作用taskSELECT_HIGHEST_PRIORITY_TASK选最高优先级任务prvAddTaskToReadyList加入就绪表listGET_OWNER_OF_NEXT_ENTRY轮询取下一个任务taskSWITCH_DELAYED_LISTS交换延时表双缓冲portYIELD()触发 PendSV 请求切换portENTER_CRITICAL()/portEXIT_CRITICAL()临界区基于 BASEPRI12.4 配置参数参数默认说明configUSE_PREEMPTION1抢占式调度configUSE_TIME_SLICING1时间片轮转configMAX_PRIORITIES5最大优先级数configTICK_RATE_HZ1000tick 频率configMAX_SYSCALL_INTERRUPT_PRIORITY—可调用 API 的最低中断优先级configUSE_MUTEXES0启用互斥量含优先级继承configUSE_RECURSIVE_MUTEXES0启用递归互斥量configUSE_COUNTING_SEMAPHORES0启用计数信号量configUSE_TIMERS0启用软件定时器configUSE_IDLE_HOOK0空闲任务钩子configUSE_TICK_HOOK0tick 钩子configCHECK_FOR_STACK_OVERFLOW0栈溢出检测推荐阅读路径入门《FreeRTOS实时内核使用指南》freertos-ug.pdf—— 理解概念和 API进阶《Mastering the FreeRTOS Real Time Kernel》—— 英文权威配合源码深入tasks.c→list.c→port.c→queue.c—— 源码逐行阅读硬件《Cortex-M3权威指南》—— 理解异常模型、PSP/MSP、BASEPRI实践《FreeRTOS源码详解与应用开发 基于STM32》—— 结合硬件实践阅读源码的顺序建议list.c数据结构基础 → tasks.c 的 TCB 和初始化部分 → port.c 的 PendSV/SVC 汇编 → tasks.c 的调度器核心xTaskIncrementTick / vTaskSwitchContext → queue.c队列/信号量/互斥量 → timers.c定时器 → heap_4.c内存管理本文档基于 FreeRTOS V10.x 源码整理覆盖内核核心机制。如需深入特定主题建议结合源码逐行阅读。
返回列表