04-FreeRTOS任务本质与设计原理-原理与本质.md 29 KB


tags: [source-summary] type: source source: "FreeRTOS任务本质与设计原理-原理与本质" author: "AI助手" date: 2026-09-19

created: 2026-09-19

FreeRTOS任务本质与设计原理:原理与本质

核心问题

FreeRTOS中"任务"的本质是什么?为什么这样设计?它和操作系统理论中的进程/线程有什么关系?

初学者常有的困惑:

  • 知道要创建任务,但不知道任务在内存中到底长什么样
  • 会用信号量、队列,但不知道它们的底层实现为什么不同
  • 学了操作系统理论中的进程/线程,但搞不清RTOS的"任务"到底对应哪个概念
  • 知道要配置栈大小,但不知道为什么需要独立栈、如何计算大小

读完本文,你能理解

  1. 任务 = 执行流 + 栈 + TCB 的本质结构
  2. 上下文切换的硬件级实现原理(PendSV机制)
  3. 所有IPC机制的统一抽象模型
  4. 为什么FreeRTOS要做这些特定的工程设计选择

原理讲解

第一部分:任务的本质

1.1 任务的三要素

FreeRTOS中,一个任务(Task)由三个核心要素构成:

任务 = 执行流(代码) + 栈(独立内存空间) + TCB(任务控制块)

这三者缺一不可:

  • 执行流:就是你编写的任务函数(一个无限循环),定义了"做什么"
  • :保存局部变量、函数调用链、中断上下文,每个任务必须有自己独立的栈
  • TCB:操作系统管理任务的数据结构,记录状态、优先级、栈指针等元信息

1.2 与进程/线程的对比

对比项 进程(Linux) 线程(Linux) 任务(FreeRTOS)
地址空间 独立虚拟地址空间 共享进程地址空间 共享全局地址空间(无MMU)
调度单位 进程 线程 任务
内核数据结构 task_struct + mm_struct task_struct(轻量) TCB(最轻量)
上下文切换 保存寄存器+TLB刷新 保存寄存器 保存寄存器(R4-R11)
切换开销 几十us 几us 1us以内
内存保护 有(MMU+虚拟内存) 有(同进程)

关键理解

  • 进程强调"资源隔离"(独立地址空间),适合通用操作系统
  • 线程强调"共享资源、独立调度",是进程内的轻量执行单元
  • 任务是最轻量的——在RTOS内核管理下,共享所有地址空间,无内存保护

1.3 为什么嵌入式RTOS用"任务"而不是"进程"

根本原因:MCU通常没有MMU(内存管理单元)

没有MMU意味着:

  • 无法实现虚拟地址到物理地址的映射
  • 无法实现内存保护(一个任务可以访问任意内存)
  • 无法实现按需分页、写时复制等高级特性

因此RTOS的设计哲学是"简单、高效、确定性",而非"安全、隔离、通用"。任务就是这个哲学的直接体现。

1.4 TCB(任务控制块)的设计

TCB是操作系统管理任务的核心数据结构。以FreeRTOS源码中的 TCB_t 为参考:

typedef struct tskTaskControlBlock {
    StackType_t *pxTopOfStack;      // 栈顶指针(上下文切换的关键)
    ListItem_t  xStateListItem;     // 链入就绪/阻塞/挂起列表
    ListItem_t  xEventListItem;     // 链入事件等待列表
    UBaseType_t uxPriority;         // 任务优先级
    StackType_t *pxStack;           // 栈起始地址
    char        pcTaskName[configMAX_TASK_NAME_LEN]; // 任务名称
    // ... 其他字段
} tskTaskControlBlock;

每个字段存在的原因

字段 作用 为什么必须有
pxTopOfStack 指向任务栈的当前栈顶 上下文切换时,硬件需要知道从哪里恢复寄存器
xStateListItem 链入 pxReadyTasksLists[] / xDelayedTaskList[] / xSuspendedTaskList 调度器通过遍历链表找到下一个要运行的任务
xEventListItem 链入 xEventWaitList(队列/信号量的等待列表) 任务阻塞等待事件时,需要从事件的等待列表中移除
uxPriority 任务优先级(0 ~ configMAX_PRIORITIES-1) 调度器据此决定抢占和选择最高优先级任务
pxStack 栈的起始地址(分配时记录) 动态删除任务时,需要知道栈的起始地址来释放内存
pcTaskName 任务名称(调试用) 调试时识别不同任务

TCB的内存布局(典型32位ARM Cortex-M):

┌─────────────────────── 低地址 ───────────────────────┐
│  pxTopOfStack  (4 bytes)     → 上下文切换的入口       │
│  xStateListItem (嵌入式链表节点) → 状态管理           │
│  xEventListItem (嵌入式链表节点) → 事件等待           │
│  uxPriority     (4 bytes)     → 调度优先级            │
│  pxStack        (4 bytes)     → 栈基地址              │
│  pcTaskName     (16 bytes)    → 任务名                │
├──────────────────────── 高地址 ──────────────────────┤
│                    ...                                │
│              任务栈空间(向下增长)                    │
└──────────────────────────────────────────────────────┘

第二部分:任务栈

2.1 栈的作用

每个任务必须有自己独立的栈,原因有三:

1. 保存局部变量和函数调用链

void task1(void *pvParameters) {
    int a = 10;           // 局部变量 → 存在task1的栈上
    int b = func(a);      // func的返回地址 → 存在task1的栈上
    printf("%d", a + b);  // printf的参数 → 存在task1的栈上
}

2. 中断时保存上下文 当SysTick/PendSV中断发生时,硬件自动将部分寄存器压栈(R0-R3, R12, LR, PC, xPSR),软件再将剩余寄存器(R4-R11)压栈。这一切都在当前任务的栈上完成。

3. 每个任务必须有独立的栈 任务A被切换出去时,它的完整执行状态(所有寄存器+调用链)保存在A的栈中;切换到任务B时,B从自己的栈中恢复状态。如果两个任务共享栈,切换时会互相破坏状态。

2.2 栈的增长方向

ARM Cortex-M采用满递减栈(Full Descending, FD)

  • 满(Full):栈指针指向最后压入的有效数据
  • 递减(Descending):栈从高地址向低地址增长

    高地址 ┌─────────────┐
       │  栈底       │ ← pxStack(分配时记录)
       │             │
       │  ↓ 增长方向  │
       │             │
       │  栈顶       │ ← pxTopOfStack(运行时动态变化)
    低地址 └─────────────┘
    

栈溢出检测

  • configCHECK_FOR_STACK_OVERFLOW = 1:检查栈指针是否越界
  • configCHECK_FOR_STACK_OVERFLOW = 2:额外检查栈尾部的哨兵值是否被覆盖
  • MPU保护:高端Cortex-M系列(如Cortex-M7)可通过MPU设置栈区域的访问权限

2.3 栈大小计算

最小栈深度:8字节(硬件自动压栈的部分)

ARM Cortex-M 硬件自动压栈的寄存器:
┌──────────────┐
│  xPSR        │  ← 程序状态寄存器
│  PC          │  ← 程序计数器(返回地址)
│  LR          │  ← 链接寄存器(调用返回地址)
│  R12         │  ← 临时寄存器
│  R3          │  ← 参数/临时寄存器
│  R2          │  ← 参数/临时寄存器
│  R1          │  ← 参数/临时寄存器
│  R0          │  ← 参数/返回值
└──────────────┘
  共 8 × 4 = 32 字节

实际需要计算的因素

  • 函数调用深度:每层调用约 8-16 字节(保存R4-R11 + LR)
  • 局部变量大小:数组、结构体等会占用栈空间
  • 中断嵌套:最坏情况下需要叠加中断栈帧

经验公式

任务栈大小 ≥ 8 + (函数调用深度 × 2) + (局部变量大小 / 4)

例如:函数调用深度10层,局部变量100字节 → 栈大小 ≥ 8 + 20 + 25 = 53 words(向上取整为64 words = 256字节)


第三部分:任务调度

3.1 调度器的本质

调度器的核心问题:从就绪列表中选择最高优先级的任务

FreeRTOS使用位图 + 优先级数组实现O(1)调度:

位图(32位)    优先级数组
┌─┬─┬─┬─┬─┬─┬─┬─┐   ┌───────────────────────────┐
│1│0│0│1│0│0│0│0│   │[0] → NULL (无任务)        │
└─┴─┴─┴─┴─┴─┴─┴─┘   │[1] → TaskA (最高就绪)    │
  │                   │[2] → NULL                 │
  │ bit3=1, bit0=1   │[3] → TaskB               │
  └→ 最高置位=bit3   │...                        │
    → 查表得优先级3   │[31] → NULL                │
                      └───────────────────────────┘

O(1)调度的实现

  1. __builtin_clz() 或查表找到位图中最高置位的bit
  2. 直接索引到对应优先级的就绪链表
  3. 从链表头取出第一个任务

无论有多少个任务,调度时间都是固定的。

3.2 优先级抢占

FreeRTOS默认使用固定优先级抢占式调度

时间线:
Task A (优先级3): ████████░░░░░░░░░░░░████████
Task B (优先级1): ░░░░░░░░████████░░░░░░░░░░░░
Task C (优先级2): ░░░░░░░░░░░░░░░░████████░░░░
                        ↑            ↑
                   B就绪,抢占A   C就绪,抢占B

抢占的硬件触发:PendSV中断。当调度器发现应该切换任务时,不是直接切换,而是触发PendSV中断,让硬件自动完成上下文保存/恢复。

3.3 时间片轮转

同优先级任务轮流执行一个tick:

Task A (优先级2): ██░░██░░██░░██░░  (每个tick切换一次)
Task B (优先级2): ░░██░░██░░██░░██  (与A轮流执行)

SysTick中断中检查:当前任务的时间片是否用完 → 如果同优先级还有其他就绪任务 → 触发PendSV切换。


第四部分:上下文切换——最核心的机制

4.1 上下文切换的硬件触发

完整的触发链路:

SysTick中断(1ms一次)
    ↓
检查是否有任务需要解除阻塞(如延时到期)
    ↓
检查是否有更高优先级任务就绪
    ↓
如果有 → 设置PendSV挂起位(写ICSR寄存器bit28)
    ↓
PendSV中断(最低优先级)→ 执行实际的上下文切换

为什么PendSV要设为最低优先级?

这是ARM官方推荐的设计模式。原因:

  • 如果PendSV优先级高,可能在其他ISR执行过程中触发切换
  • 这会导致ISR返回到错误的任务上下文
  • 设为最低优先级确保:所有中断都处理完毕后,才执行上下文切换

4.2 上下文切换的汇编实现(Cortex-M为例)

以FreeRTOS的 xPortPendSVHandler 为例,核心逻辑:

xPortPendSVHandler:
    ; ====== 保存当前任务上下文 ======
    MRS     R0, PSP            ; 读取当前进程栈指针(PSP)
    STMDB   R0!, {R4-R11}     ; 将R4-R11压入当前任务栈

    ; ====== 更新TCB的pxTopOfStack ======
    LDR     R1, =pxCurrentTCB  ; 获取当前TCB指针
    LDR     R1, [R1]           ; 解引用得到TCB地址
    STR     R0, [R1]           ; 将新的栈顶存入TCB->pxTopOfStack

    ; ====== 切换到新任务 ======
    PUSH    {LR}               ; 保存EXC_RETURN
    BL      vTaskSwitchContext ; 调度器选择下一个任务(更新pxCurrentTCB)
    POP     {R0}               ; 恢复EXC_RETURN
    BX      R0                 ; 异常返回,硬件自动恢复栈帧

    ; ====== 恢复新任务上下文(在新任务的栈上执行) ======
    ; (由异常返回机制自动触发,或在调度器返回时处理)
    LDR     R1, =pxCurrentTCB
    LDR     R1, [R1]
    LDR     R0, [R1]           ; 读取新任务的栈顶
    LDMIA   R0!, {R4-R11}     ; 从新任务栈中恢复R4-R11
    MSR     PSP, R0            ; 更新PSP为新任务的栈指针
    BX      LR                 ; 异常返回

关键理解

  1. PUSH/POP 只操作R4-R11(8个寄存器),因为R0-R3等由硬件自动保存
  2. pxTopOfStack 是上下文切换的"接力棒"——保存旧任务的栈顶,加载新任务的栈顶
  3. 异常返回(BX LR,LR值为EXC_RETURN)由硬件完成栈帧恢复

4.3 与Linux上下文切换的对比

对比项 FreeRTOS Linux
保存内容 R4-R11(8个通用寄存器) 完整 thread_struct(所有寄存器+浮点+SIMD)
保存位置 任务栈中 task_struct 内嵌的内核栈
切换开销 < 1us 10-100us
TLB刷新 无(无MMU) 需要(虚拟地址空间切换)
缓存影响 可能导致缓存失效
触发方式 PendSV中断 时钟中断/系统调用

为什么差这么多?

  • Linux需要保存/恢复更多状态(浮点寄存器、SIMD寄存器、调试寄存器等)
  • Linux需要刷新TLB(因为切换了地址空间)
  • Linux的调度器更复杂(CFS调度、负载均衡等)
  • FreeRTOS没有地址空间切换,没有内存保护,所以可以做到极致轻量

第五部分:任务状态与转换

5.1 四种状态

状态 含义 触发条件 退出条件
运行态 任务正在执行 被调度器选中 被抢占/阻塞/挂起
就绪态 可执行但未运行 创建/解除阻塞/解挂 被调度器选中
阻塞态 等待延时或事件 vTaskDelay/等待队列/信号量 延时到/事件发生
挂起态 主动暂停 vTaskSuspend() vTaskResume()

5.2 状态转换的触发条件

          创建
           ↓
        ┌───────┐    被调度器选中    ┌───────┐
        │ 就绪态 │ ←──────────────→ │ 运行态 │
        └───┬───┘                    └───┬───┘
            ↑ 被抢占                    │
            │                            ↓
            │                      阻塞(vTaskDelay/
            │                      等待信号量等)
            │                            ↓
            │                        ┌───────┐
            └── 事件发生/延时到 ←─── │ 阻塞态 │
                                    └───────┘

        运行态 → vTaskSuspend() → ┌───────┐
                                  │ 挂起态 │
        就绪态 ← vTaskResume() ← └───────┘

关键规则:只有就绪态可以转变为运行态。阻塞态/挂起态的任务必须先回到就绪态,才能被调度。

5.3 与操作系统理论的五状态模型对比

操作系统理论中的经典五状态模型:

新建 → 就绪 ←→ 运行 → 终止
          ↑←── 阻塞 ←┘

FreeRTOS的对应:

  • 新建 → 对应 xTaskCreate() 执行期间(创建后直接进入就绪态)
  • 就绪 → 就绪态
  • 运行 → 运行态
  • 阻塞 → 阻塞态
  • 终止 → 对应 vTaskDelete()(删除后由空闲任务回收资源)

FreeRTOS没有显式的"新建"和"终止"状态,因为创建/删除是瞬时操作,任务要么就绪要么不存在。


第六部分:IPC机制的本质

所有IPC(进程间通信)机制在FreeRTOS中的底层实现,都可以归结为两个基本组件的组合:

IPC = 等待列表 + 数据/状态容器

6.1 消息队列 = 环形缓冲区 + 任务等待列表

消息队列内部结构:
┌──────────────────────────────────────┐
│         Queue_t 结构体               │
│  pcHead ─→ [消息1][消息2]...[消息N] │  ← 环形缓冲区
│  pcTail ─→ 缓冲区末尾               │
│  uxMessagesWaiting = 2              │  ← 当前消息数
│  uxLength = 1 (每条消息大小)        │
│  uxItemSize = 4 bytes               │
│                                      │
│  xTasksWaitingToSend → [TaskC]      │  ← 发送阻塞列表
│  xTasksWaitingToReceive → [TaskA]   │  ← 接收阻塞列表
└──────────────────────────────────────┘

本质:环形缓冲区 + 两个等待列表(发送方/接收方)。

6.2 二值信号量 = 长度为1的消息队列

二值信号量 = 创建时uxLength=1, uxItemSize=0的队列
    ↓
只有"满"和"空"两种状态
    ↓
等价于一个事件标志(发生/未发生)

为什么比队列快? 因为 uxItemSize=0,不复制任何数据,只操作状态。

6.3 计数信号量 = 不保存数据的队列

计数信号量 = 创建时uxItemSize=0的队列
    ↓
每Give一次,uxMessagesWaiting++
每Take一次,uxMessagesWaiting--
    ↓
用于事件计数(如:中断发生了几次)

6.4 互斥信号量 = 信号量 + 优先级继承

互斥信号量 = 二值信号量 + 优先级继承机制
    ↓
当高优先级任务等待低优先级任务持有的互斥量时:
    临时提升低优先级任务的优先级 = 高优先级任务的优先级
    ↓
解决优先级翻转问题

优先级翻转的场景

时间线:
Task H (优先级3): ░░░░░░░░░░████████  (等L释放互斥量)
Task M (优先级2): ░░░░████████░░░░░░  (抢占了L,导致H等更久)
Task L (优先级1): ████░░░░░░░░░░░░░░  (持有互斥量,被M抢占)
                       ↑
                 L被M抢占,H间接被M阻塞(优先级翻转)
                 优先级继承:M的优先级临时提升为3,防止抢占

6.5 事件标志组 = 位图 + 等待逻辑

事件标志组内部结构:
┌──────────────────────────────────┐
│  EventGroup_t                    │
│  uxBits = 0b0000_0101           │  ← 事件位图
│                                  │
│  xWaitList = [TaskA(WAIT_ANY),  │  ← 等待列表
│               TaskB(WAIT_ALL)]  │
└──────────────────────────────────┘

等待逻辑:
- WAIT_ANY:任一指定位置位即唤醒
- WAIT_ALL:所有指定位都置位才唤醒

6.6 任务通知 = 直接写TCB的通知值(最轻量)

传统IPC:任务A → [创建队列对象] → 放入数据 → 队列 → 任务B取出
任务通知:任务A → 直接修改任务B的TCB字段 → 完成

为什么任务通知最快?

  • 跳过了创建/操作内核对象的所有开销
  • 不需要复制数据(直接修改32位整数)
  • 不需要管理等待列表(通知目标是固定的)
  • 代价:只能一对一通信,不能缓存多条消息

第七部分:为什么这样设计

7.1 为什么用链表组织任务列表

FreeRTOS用侵入式链表(Intrusive List)组织所有任务列表:

  • 就绪列表:pxReadyTasksLists[configMAX_PRIORITIES]
  • 延时列表:xDelayedTaskList1 / xDelayedTaskList2
  • 挂起列表:xSuspendedTaskList
  • 事件等待列表:嵌入在每个队列/信号量中

原因

  • 增删操作O(1):链表插入/删除只需修改指针
  • 嵌入式场景任务数少(通常10-50个),不需要O(log n)的平衡树
  • 侵入式链表不额外分配内存:链表节点直接嵌入TCB/队列结构体中

7.2 为什么PendSV设为最低优先级

确保中断处理的实时性:

  • 如果PendSV优先级高,可能在关键ISR中触发切换
  • ISR返回后可能跳转到错误的任务上下文
  • 设为最低优先级 = 所有中断处理完毕后才执行切换
  • 这是ARM官方推荐的"上下文切换模式"

7.3 为什么任务通知比队列快

对比项 队列 任务通知
需要创建 是(xQueueCreate)
数据复制 是(memcpy消息体) 否(直接写32位值)
等待列表 需要管理发送/接收列表 直接写TCB字段
适用场景 多对多、需要缓存 一对一、简单通知

7.4 为什么互斥信号量需要优先级继承

优先级翻转是实时系统的经典问题。不解决它,高优先级任务可能被低优先级任务间接阻塞,导致实时性失效。

优先级继承的原理:

  • 高优先级任务等待互斥量时,持有者临时继承高优先级
  • 高优先级任务释放后,持有者恢复原优先级
  • 这是临时的、自动的,不需要任务主动配合

7.5 为什么FreeRTOS的内存管理有heap_1~heap_5

不同嵌入式场景对内存管理的需求差异极大:

方案 特点 适用场景
heap_1 只分配不释放 极简系统,任务不删除
heap_2 分配+释放,不合并相邻空闲块 任务数量固定的系统
heap_3 封装标准库malloc/free + 线程安全 通用场景
heap_4 分配+释放+合并相邻空闲块(碎片少) 大多数嵌入式系统
heap_5 支持非连续内存区域(如外部SRAM) 内存不连续的MCU

没有"最好"的方案,只有最适合特定场景的方案。


跨学科对应表

FreeRTOS概念 操作系统理论 硬件机制 Linux对应
任务(Task) 线程 - task_struct + 内核线程
TCB PCB/TCB - task_struct
任务栈 线程栈 PSP/MSP寄存器 内核栈 + 用户栈
上下文切换 上下文切换 PendSV中断 context_switch()
就绪列表 就绪队列 - CFS红黑树
优先级 优先级 - prio / nice值
优先级抢占 抢占式调度 PendSV PREEMPT配置
时间片 时间片轮转 SysTick SCHED_RR
SysTick 时钟中断 SysTick定时器 timer_interrupt()
PendSV 软中断/调度中断 PendSV异常 schedule()
消息队列 消息传递 - pipe / msgqueue
二值信号量 信号量 - struct semaphore
互斥信号量 互斥锁+优先级继承 - mutex + PI
事件标志组 条件变量 - wait_queue + condition
任务通知 - - 无直接对应(类似轻量级futex)
空闲任务 idle进程 WFI/WFE指令 swapper进程

常见误区

  1. 误区:任务可以并行执行 正解:单核MCU同一时刻只能运行一个任务。多任务是通过快速切换制造"并发"假象,而非真正并行。

  2. 误区:栈大小越大越好 正解:栈过大会浪费宝贵的RAM(MCU通常只有几十KB到几百KB)。应根据函数调用深度和局部变量精确计算,或通过 uxTaskGetStackHighWaterMark() 监测实际使用量。

  3. 误区:高优先级任务一直运行就不会被抢占 正解:高优先级任务调用 vTaskDelay() / 等待信号量等阻塞操作时会让出CPU。如果高优先级任务是死循环(不阻塞),低优先级任务将永远无法运行(饿死)。

  4. 误区:ISR中可以调用任何FreeRTOS API 正解:ISR中只能调用带 FromISR 后缀的API。普通API可能触发上下文切换或阻塞,在ISR中调用会导致未定义行为。

  5. 误区:任务通知可以替代所有IPC 正解:任务通知只能一对一、不能缓存多条消息、ISR不能接收。需要多对多通信或消息缓存时,必须使用队列/信号量。


面试要点

Q1: FreeRTOS中任务的本质是什么?和Linux线程有什么区别?

: 任务 = 执行流(任务函数)+ 独立栈 + TCB(任务控制块)。

与Linux线程的关键区别:

  • 任务共享全局地址空间(无MMU),线程共享进程虚拟地址空间(有MMU保护)
  • 上下文切换只保存R4-R11(8个寄存器),开销<1us;Linux线程需要保存完整寄存器集+可能刷新TLB,开销10-100us
  • 任务没有内存保护,一个任务可以访问任意内存地址

Q2: 上下文切换的过程是怎样的?为什么PendSV要设为最低优先级?

: 过程:PendSV中断触发 → 保存当前任务R4-R11到其栈中 → 更新TCB的pxTopOfStack → 调度器选择下一个任务 → 从新任务栈中恢复R4-R11 → 异常返回。

PendSV设为最低优先级的原因:确保所有其他中断(如SPI、UART、定时器)都处理完毕后,才执行上下文切换。否则可能在ISR执行过程中触发切换,导致ISR返回到错误的任务上下文。

Q3: 消息队列、信号量、任务通知的底层实现有什么共同点?

: 所有IPC的底层都是"等待列表 + 数据/状态容器"的组合:

  • 队列 = 环形缓冲区 + 发送/接收等待列表
  • 二值信号量 = 长度为1、不保存数据的队列
  • 任务通知 = 直接修改目标TCB的通知字段(跳过队列对象)

任务通知最快,因为省去了创建内核对象和管理等待列表的开销。

Q4: 什么是优先级翻转?FreeRTOS如何解决?

: 优先级翻转:高优先级任务等待低优先级任务持有的互斥量,而中等优先级任务抢占了低优先级任务,导致高优先级任务被间接阻塞更长时间。

FreeRTOS通过互斥信号量的优先级继承机制解决:当高优先级任务等待互斥量时,持有者的优先级被临时提升为等待者的优先级,防止中等优先级任务抢占。

Q5: FreeRTOS的栈大小应该怎么配置?

  • 最小栈深度:8 words(32字节),对应R0-R3, R12, LR, PC, xPSR的硬件自动保存
  • 实际需要:根据函数调用深度(每层约2 words)和局部变量大小计算
  • 调试方法:启用 configCHECK_FOR_STACK_OVERFLOW,通过 uxTaskGetStackHighWaterMark() 监测栈水位
  • 经验法则:从256字节开始,根据实际运行情况调整

参考资料

  • raw/Joplin/嵌入式+Linux/FreeRTOS学习笔记/02-任务调度与状态管理.md — 调度器、状态转换、上下文切换基础
  • raw/Joplin/嵌入式+Linux/FreeRTOS学习笔记/05-任务创建与删除.md — 任务三要素、TCB结构、动态/静态创建
  • raw/Joplin/嵌入式+Linux/FreeRTOS学习笔记/10-消息队列.md — 消息队列实现原理
  • raw/Joplin/嵌入式+Linux/FreeRTOS学习笔记/11-信号量机制.md — 二值/计数/互斥信号量
  • raw/Joplin/嵌入式+Linux/FreeRTOS学习笔记/13-事件标志组.md — 事件标志组实现
  • raw/Joplin/嵌入式+Linux/FreeRTOS学习笔记/14-任务通知.md — 任务通知的三种模式与性能对比
  • raw/Joplin/嵌入式+Linux/FreeRTOS学习笔记/16-内存管理.md — heap_1~heap_5的选择
  • raw/Joplin/嵌入式+Linux/FreeRTOS学习笔记/01-实时操作系统基础.md — RTOS基本概念