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.寄存器组根据AAPCSARM架构过程调用标准在被调用函数返回之前需要将特定的寄存器集(R4-R8R10R11SP用作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位的地址。AAPCSArm Architecture Procedure Call StandardARM架构过程调用标准是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异常。PendSVPendable 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 堆相关 APIFreeRTOS 堆管理器对外提供以下函数原型在portable.hvoid *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 移植和调优的关键环节理解其原理有助于构建稳定、高效的嵌入式实时系统。