FreeRtos的基础与应用
1.多任务与并发
2.调度原理
3.上下文切换
4.滴答计数器
5.堆内存管理
一.多任务与并发
简单的来说就是多个任务在人的感觉下是同时进行的但是在客观下是不连续进行的。

多任务系统的任务是具有优先级的,高优先级的任务可以抢占低优先级任务的 CPU 使用权。优先级相同的任务则各自轮流运行一段极短的时间(时间片) ,从而产生“同时”运行的错觉,这也是抢占式调度和时间片调度的基本原理。在具备优先级的多任务系统中,用户就可以将紧急的事务放在优先级高的任务中进行处理(注意占用的时间,需要主动释放CPU 使用权,防止低优先级任务“饿死” ) ,那么整个系统的实时性就会大大地提高。
二.调度原理
调度器是内核中负责在任何时间点应该执行哪些任务的部分。内核可以在任务生命周期内多次拉起并且稍后恢复一个任务。
调度策略是调度器用来决定在任何时间点执行哪个任务的算法。
FreeRTOS 共支持三种任务调度方式,分别为抢占式调度、时间片调度和协程式调度(略)。
抢占式调度
抢占式调度主要时针对优先级不同的任务,每个任务都有一个优先级,优先级高的任务可以抢占优
先级低的任务,只有当优先级高的任务发生阻塞或者被挂起,低优先级的任务才可以运行。
时间片调度
时间片调度主要针对优先级相同的任务,当多个任务的优先级相同时, 任务调度器会在每一次系统时钟节拍到的时候切换任务,也就是说 CPU 轮流运行优先级相同的任务,每个任务运行的时间就是一个系统时钟节拍。
在 FreeRTOS 中的任务存在四种状态,分别为运行态、就绪态、阻塞态和挂起态。在 FreeRTOS 运行时,任务的状态一定是这四种状态中的一种,下面是四种任务状态的介绍。。
①运行态
如果一个任务得到 CPU 的使用权,即任务被实际执行时,那么这个任务处于运行态。如果运行
FreeRTOS 的 MCU 只有一个处理器核心,那么在任务时刻,都只能有一个任务处理运行态。
② 就绪态
如果一个任务已经能够被执行(不处于阻塞态后挂起态),但当前还未被执行(具有相同优先级或更高优先级的任务正持有 CPU 使用权),那么这个任务就处于就绪态。
③ 阻塞态
如果一个任务因延时一段时间或等待外部事件发生,那么这个任务就处理阻塞态。例如任务调用了
函数 vTaskDelay(),进行一段时间的延时,那么在延时超时之前,这个任务就处理阻塞态。任务也可以处于阻塞态以等待队列、信号量、事件组、通知或信号量等外部事件。通常情况下,处于阻塞态的任务都有一个阻塞的超时时间,在任务阻塞达到或超过这个超时时间后,即使任务等待的外部事件还没有发生,任务的阻塞态也会被解除。要注意的是,处于阻塞态的任务是无法被运行的。
④ 挂起态
任务一般通过函数 vTaskSuspend()和函数 vTaskResums()进入和退出挂起态与阻塞态一样,处于挂起态的任务也无法被运行。
四种任务状态之间的转换图如下图所示:

三.上下文切换
1.存储—加载体系结构:
只有取数型和存数型指令可以访问存储器,运算型指令的原操作数来自寄存器,运算结果也可以写到寄存器的一种计算机体系结构。Cortex-M处理器(微处理器)基于存储—加载体系结构。

2. 什么是上下文(Context)
在操作系统中,上下文(Context)指的是任务(或进程/线程)执行时所依赖的环境状态信息。当任务被切换时,系统需要保存当前任务的上下文,并恢复下一个任务的上下文,以保证任务能够从上次中断的位置继续正确执行。
上下文主要包括以下内容:
- 寄存器状态:包括通用寄存器(R0-R12)、程序计数器(PC)、链接寄存器(LR)、堆栈指针(SP)、程序状态寄存器(PSR)等。
- 堆栈内容:任务私有的堆栈数据,包括局部变量、函数调用返回地址等。
- 浮点寄存器状态:(如果支持浮点运算单元)。
- 内存映射信息:(如页表基址寄存器,在MMU系统中)。
- 任务控制块(TCB)中的状态字段:如任务优先级、状态标志、等待事件等。
在 FreeRTOS 等实时操作系统中,上下文切换(Context Switching)就是保存当前运行任务的上下文到其任务控制块(TCB)栈也要保存,并从下一个要运行的任务的TCB中恢复其上下文的过程。这一过程通常由调度器触发,发生在:
- 任务主动让出CPU(如调用
taskYIELD())。 - 系统时钟节拍中断(tick interrupt)触发时间片轮转。
- 高优先级任务就绪,抢占当前低优先级任务。
- 任务进入阻塞态(如等待信号量、队列、延时等)。
3.Cortex-M状态/操作模式

“Thread Mode”和“Handler Mode”是ARM Cortex-M处理器运行的两种“权限等级”,用来区分普通程序和紧急中断/异常的处理环境。
· Thread Mode(线程模式):跑普通用户代码的模式(比如main函数里的死循环、业务逻辑)。它分为有特权和无特权两种级别(图中靠软件切换)。
· Handler Mode(处理者模式):跑异常/中断处理函数的模式(比如定时器中断、串口接收中断)。永远是有特权的,进入靠硬件触发异常,退出靠“异常返回”指令。
为了让你秒懂,我打个公司管理的比方:
· Thread Mode 特权级 = 老板(普通工作随便做,也能批预算、改核心配置)。
· Thread Mode 无特权级 = 普通员工(只能做分内工作,不能动核心配置,防止搞垮系统)。
· Handler Mode = 消防队长(一旦着火/中断发生,立刻中断所有人,拥有最高权限去处理紧急事务,处理完就回去)。
这张图里还有几个关键点,对你实际写代码特别重要:
1. “特权”到底能多干什么?
能访问所有内存地址,能操作核心控制寄存器(比如关总中断、改系统时钟)。无特权模式如果试图碰这些,会立即触发“硬错误”死机。
2. 怎么从“有特权”切到“无特权”?
软件主动写一个叫 CONTROL 的寄存器(图中“Switch by software”),常用于RTOS(实时操作系统)中把任务线程设为无特权,防止用户任务破坏系统内核。
3. 怎么从“无特权”切回“有特权”?
只能通过异常(中断)!进入Handler Mode后就自动恢复特权了,所以普通任务想申请更高权限,必须通过系统调用(SVC指令)触发软件中断。
4. 你的程序上电默认在哪?
复位后默认是 Thread Mode + 特权级(老板状态),如果你用FreeRTOS,创建任务时系统通常会主动把任务切到无特权级。
Debug State(调试状态) 就是CPU被调试器(比如ST-Link/J-Link)强行暂停时的状态。
打个比方:Thread/Handler Mode 是运动员在场上正常打球,Debug State 就是裁判(调试器)吹哨喊“暂停!所有人别动!”,然后教练(你)进场看战术板(查看内存/寄存器)。
---
进入Debug State的三种方式
1. 硬件断点:程序运行到你在Keil里打红点的那一行,CPU自动暂停。
2. 单步执行:你点“Step Over”或“Step Into”,CPU执行完一条指令后立刻暂停。
3. 数据观察点:你设置监控某个变量,当它的值被修改时,CPU自动暂停(类似于“值变了就报警”)。
---
Debug State下,芯片内部在干什么?
· 指令流水线冻结:CPU停止取指和执行。
· 外设继续运行(注意!):定时器、PWM输出、串口接收等硬件外设不会停(除非你特意配置调试模式下的外设行为)。比如你在断点停住时,串口依然可能收到数据,PWM依然在输出波形。
· 内核寄存器完整保留:R0-R15、状态寄存器(xPSR)、SP指针等全部“冻住”,方便你查看当前现场。

4.寄存器组
根据AAPCS(ARM架构过程调用标准),在被调用函数返回之前,需要将特定的寄存器集(R4-R8,R10,R11,SP,用作v6的R9)恢复到调用函数前的原始值,这些寄存器由被调函数保存,称为callee-saved register,也就是说如果被调函数中有使用到这些寄存器,则需要在函数入口处保存,在函数出口处恢复。
对 于 函 数 的 调 用 者 , 即 硬 件 上 在 进 入 异 常 程 序 之 前 , 它 会 自 动 保 存 R 0 -
R3,R12,R14,R15和xPSR寄存器(按顺序从后往前压,即先压xPSR),这些寄存器称为
caller-saved register。
R12(Intra-Procedure-call Scratch Register)寄存器有32位。对于汇编中的跳转指令bl,它并不能跳转到整个程序的地址空间,因为指令中的前几位是对指令进行编码的,所以无法跳转到整个32位的地址。
AAPCS(Arm Architecture Procedure Call Standard,ARM架构过程调用标准)是ARM公司制定的函数调用约定,规定了:
· 参数怎么传(用寄存器还是栈)
· 返回值怎么返回
· 寄存器谁保存(调用者保存还是被调用者保存)
· 栈怎么对齐
简单说:它让不同编译器(GCC、IAR、ARMCC)编译出来的函数能互相调用,好比大家都说普通话。
压栈时保存到栈里的数据被统称为栈帧(stack frame)。
Cortex-M处理器的栈帧总是双字对齐(双字对齐(8字节对齐)在ARM里,就是要求内存地址必须是8的倍数(地址末尾3位为0)。这是AAPCS的硬性规定,不满足直接硬件异常 (UsageFault)。),确保栈的使用遵循AAPCS标准。
压入栈的xPSR寄存器第9位标志,表明是否发生双字栈调整。

在ARM Cortex-M(以及大多数ARM架构)下,SP(栈指针)必须始终指向8的倍数地址,这是硬件和AAPCS规范的双重要求。
但这里有个实战细节:因为栈是向下增长(满递减栈),所以:
· 任务栈顶可以是任意地址
· 但 SP当前指向的地址(栈顶指针的值)必须是 8的倍数(任务栈的起始地址(数组首地址):可以任意(甚至1字节对齐),因为编译器分配数组时只保证数组元素自身对齐。
· 任务栈的栈顶指针(SP):必须8字节对齐,这是运行时的硬性要求。)
· 创建任务时(如xTaskCreate),系统分配栈空间后,会自动把初始栈顶向下调整到8字节对齐,保证第一次运行任务时SP符合要求
5.上下文切换实现
①SysTick和PendSV(可悬起系统调用)
SysTick定期触发产生上下文切换。除此之外,上下文切换也会在任务放弃占用CPU时发生(portYIELD()),两种方式最终都会触发PendSV异常。(PendSV(Pendable Service Call,可挂起服务调用) 是ARM Cortex-M内核中一个优先级可编程的异常,在RTOS(如FreeRTOS)里,它是任务切换的“发动机”。)
②SVC(系统服务调用)

SVC 用于产生系统函数的调用请求。例如,操作系统不让用户程序直接访问硬件,而是通过提供一些系统服务函数,用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。
在FreeRTOS中,使用SVC异常来开启第一个任务的调度。然后将PendSV异常优先级设置为最低,保证中断的实时性,后续的上下文切换全由PendSV来实现。
四.滴答计数器
休眠时,RTOS任务将指定需要唤醒的时间。阻塞时,RTOS任务可以指定希望等待的最长时间。实时内核通过滴答计数变量测量时间。滴答中断以严格的时间精度增加滴答数,允许内核以所选择的定时器中断频率来测量时间。每次滴答数增加时,实时内核必须检查是否现在是解除阻塞或唤醒任务的时间。

五. 堆内存管理
在 FreeRTOS 中,堆(Heap)是内核用于动态分配内存的连续内存区域。任务创建、队列、信号量、事件组等内核对象都需要从堆中分配内存。FreeRTOS 提供了多种堆管理方案,开发者可以根据目标平台的资源约束和性能需求进行选择。
5.1 为什么需要堆管理?
嵌入式系统通常资源受限,没有标准 C 库的 malloc()/free() 或它们效率低下、碎片化严重。FreeRTOS 自带的堆管理器针对实时性、确定性和小内存场景做了优化,主要解决以下问题:
- 确定性:分配/释放时间可预测,适合硬实时任务。
- 碎片控制:提供算法减少内存碎片。
- 多任务安全:分配/释放操作可重入,支持任务和中断服务程序(ISR)调用。
- 空间效率:管理开销小,适合几 KB 到几十 KB 的 RAM。
5.2 FreeRTOS 提供的堆实现
FreeRTOS 源码的 portable/MemMang 目录下提供了 5 种堆实现(heap_1.c ~ heap_5.c),编译时只需选择其中一个链接到工程。
| 堆方案 | 特点 | 适用场景 |
|---|---|---|
| heap_1 | 只分配,不释放。最简单、最快、碎片最少。 | 系统启动后所有内核对象(任务、队列等)一次性创建,之后不再删除。 |
| heap_2 | 支持分配与释放,使用最佳匹配算法。不会合并相邻空闲块。 | 重复创建/删除相同大小对象的场景(碎片可控)。现已被 heap_4 取代。 |
| heap_3 | 封装标准库的 malloc()/free(),增加线程安全(关中断)。 |
已有成熟堆管理器的平台,或需要与外部库共用堆。 |
| heap_4 | 支持分配与释放,使用首次适应算法,会合并相邻空闲块(防碎片)。 | 最常用。适用于重复创建/删除不同大小对象的场景。 |
| heap_5 | 在 heap_4 基础上,支持堆内存分布在多个非连续内存区域。 | RAM 分散在多个物理地址(如内部 SRAM + 外部 SDRAM)。 |
5.3 堆相关 API
FreeRTOS 堆管理器对外提供以下函数(原型在 portable.h):
void *pvPortMalloc( size_t xWantedSize );
void vPortFree( void *pv );
size_t xPortGetFreeHeapSize( void );
size_t xPortGetMinimumEverFreeHeapSize( void );
内核对象创建函数(如 xTaskCreate()、xQueueCreate())内部会调用 pvPortMalloc(),因此通常无需直接调用。但在应用层需要动态内存时,应使用这些接口而非标准库函数。
5.4 配置堆大小
堆大小通过 configTOTAL_HEAP_SIZE 在 FreeRTOSConfig.h 中定义:
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 20 * 1024 ) ) // 20KB
实际可用堆空间略小于此值,因为管理结构会占用少量字节。可通过 xPortGetFreeHeapSize() 监控剩余堆内存。
5.5 堆内存布局示例(heap_4)

上图展示了 heap_4 的典型内存布局:
- 整个堆是一个字节数组(
ucHeap[ configTOTAL_HEAP_SIZE ])。 - 每个分配块包含头部(大小、状态)和用户数据区。
- 空闲块通过链表连接,分配时遍历链表找到第一个足够大的块(首次适应)。
- 释放时会检查前后块是否空闲,若是则合并成大块,减少碎片。
5.6 实战建议
- 选择 heap_4:除非有特殊需求,否则 heap_4 是最平衡、最推荐的选择。
- 合理设置堆大小:通过
xPortGetMinimumEverFreeHeapSize()观察系统运行一段时间后的最小剩余堆,据此调整configTOTAL_HEAP_SIZE。 - 避免在中断中频繁分配:虽然 API 可重入,但分配操作可能关中断,影响实时性。建议在任务中预分配或使用静态分配。
- 注意内存对齐:FreeRTOS 堆管理器保证返回的指针满足架构对齐要求(如 8 字节)。
堆内存管理是 FreeRTOS 移植和调优的关键环节,理解其原理有助于构建稳定、高效的嵌入式实时系统。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)