FreeRTOS (图文整理)

单片机是单核CPU 我们停下低优先级的 做高优先级的 这个就叫做抢占式调度 freertos 的核心功能是管理任务 就是约等于 电脑上的进程管理

进程管理就是 比如 在windows 里面 进程管理 就是我们在 电脑桌面既要听音乐 打开浏览器这样子

FreeRTOS 调度器负责在多个任务之间进行切换的概念。图中有四个竖直的任务条(任务1~任务4),顶部标注调度器负责任务切换,任务之间用箭头表示切换关系。

  • FreeRTOS调度器
  • 负责切换(红字批注)
  • 任务1
  • 任务2
  • 任务3
  • 任务4

底部字幕:像记流水账一样编写每个任务的代码,剩下的交给 FreeRTOS

图 2(image2.png)

1. 任务函数的一般写法:

// 任务函数的一般写法
// 任务函数使用void类型的返回值,但是有一个void*类型的参数
void vTask1(void *pvParameters)
{
    // 编程方式类似于main()方法,一般写一个无限循环
    for(;;)
    {
        // 使用vTaskDelay()而不使用HAL_Delay()进行延迟
    }
    // 坚决不能写return!
    vTaskDelete(NULL); //一旦退出需删除自身
}

手写批注(红色):

  • 参数void *(指向 void *pvParameters)
  • 像写main()方法一样(指向 void vTask1(void *pvParameters))
  • 一般包含一个永不退出的循环(指向 for(;;))
  • 坚决不要出现return(指向 // 坚决不能写return!)
  • 一旦退出要调用vTaskDelete()删除自身(指向 vTaskDelete(NULL);)

图 3(image3.png)

什么情况下vTaskStartScheduler()函数会退出,导致

这个是调度器

图 4(image4.png)

This is a FreeRTOS kernel architecture diagram showing the relationships between interrupts, the scheduler, tasks, and various kernel objects.

Top — Interrupts (with priorities):

  • SysTick中断 (用于时间片轮询调度) — 优先级最低
  • SVC中断 (用于启动首个任务) — 优先级最高
  • PendSV中断 (用于后续任务切换) — 优先级最低

Center — Scheduler (调度器):
With three scheduling strategies:

  • 带时间片的抢占式调度
  • 不带时间片的抢占式调度
  • 协作式调度

(Hand-drawn red annotation: “图” with an arrow pointing to the scheduler block)

Below scheduler — Tasks:

  • 空闲任务
  • 定时器任务
  • 用户任务1
  • 用户任务2
  • ……
  • 用户任务n

Timer command queue (定时器命令队列) — connects between 软件定时器 and 定时器任务.

Heap memory management (堆内存管理):

  • heap1
  • heap2
  • heap3
  • heap4
  • heap5

Right column — Kernel Objects (内核对象):

  • 队列(邮箱)
  • 队列集合
  • 事件组
  • 软件定时器
  • 信号量 (group, dashed box):
    • 二进制信号量
    • 计数信号量
    • 互斥锁
    • 递归互斥锁
  • 临界区
  • 事件组
  • 任务通知
  • 消息缓冲区
  • 流缓冲区

(Hand-drawn red annotation: red arrow on the right side pointing down toward the 临界区 area, and a red circle/mark above 二进制信号量)

Legend (图例):

  • ⬡ 内核对象(任务也属于内核对象)

图 5(image5.png)

A hand-drawn illustration comparing two FreeRTOS tasks as cartoon characters with speech bubbles.

Left character — 定时器任务 / Timer Service Task

  • Stick figure wearing a red headband, smiling and giving a thumbs-up
  • Red handwritten label on body: T2M
  • Speech bubble: 我是软件定时器的小助理
  • Caption below: 定时器任务 / Timer Service Task

Right character — 空闲任务 / Idle Task

  • Stick figure crying with blue tears, looking sad
  • Red handwritten label on body: IDLE
  • Speech bubble: 啊啊啊,我是所有人的备胎
  • Caption below: 空闲任务 / Idle Task

A small orange dot/circle is drawn in the middle-bottom area of the image between the two characters.

空闲任务 就是下面这个图片

图 6(image6.png)

任务调度时序图,显示 LED1 任务与空闲任务之间的切换关系。

坐标轴标签:

  • 纵轴:上方为 “LED1任务”,下方为 “空闲任务”
  • 横轴:时间(向右)

状态标签(从左到右依次出现):

  • “亮灯”(红色手绘椭圆圈出,箭头指向空闲任务运行段)
  • “灭灯”(橙色手绘椭圆圈出)
  • “亮灯”
  • “灭灯”

红色手写注释(顶部):

一旦LED1任务调用vTaskDelay()进入延迟
调度器将会面临无任务可切换的状态
这个时候就会选择空闲任务来执行

图示说明:
LED1 任务以短脉冲形式运行(亮灯 / 灭灯 切换),每次短暂执行后进入阻塞态,调度器切到空闲任务运行较长时间;下次 LED1 任务就绪时再次抢占空闲任务,如此往复,形成"短脉冲 — 长平台"的交替波形。

其实就是一个备胎 备份

图 7(image7.png)

A photograph of a microcontroller development board (appears to be an STM32 “Blue Pill”-style board) with hand-drawn red annotations pointing to four different interrupt sources on the board.

Annotations (in red, handwritten):

  • EXTI中断 (EXTI Interrupt) — arrow pointing to the upper-left area of the board
  • 定时器中断 (Timer Interrupt) — arrow pointing to the upper-right area (near the USB/BOOT jumper)
  • UART中断 (UART Interrupt) — arrow pointing to the lower-left header pins
  • ADC中断 (ADC Interrupt) — arrow pointing to the lower-right header pins

Uart txe 等等中断

Exti 中断 通过上升沿 下降沿 去触发中断

堆就是一段能动态 分配的内存

图 8(image8.png)

volatile int *p3 = (int *)malloc(128);
// 在堆上分配128字节,指针p4指向该内存的首地址
volatile int *p4 = (int *)malloc(128);

free(p1); // 释放掉p1指向的内存块
free(p3); // 释放掉p3指向的内存块

(最后两行 free 语句被手写荧光笔高亮标黄)


下方为内存示意图说明:

从左到右依次为:

  • .bss 段(左侧空白区域,标有 “.bss”)
  • 第一块灰色内存块(内部有红色波浪划线,表示已释放的内存)
  • 蓝色标记 → p2(红色箭头指向)
  • 白色内存块,标注 128
  • 第二块灰色内存块(内部有红色波浪划线,表示已释放的内存)
  • 蓝色标记 → p4(红色箭头指向)
  • 白色内存块,标注 128
  • 右侧又一块灰色内存块

示意图展示了堆内存布局:.bss 段右侧紧接两块已被释放的内存(带红色划线),分别对应已被 free 的 p1 和 p3 指向的区域;p2 和 p4 为仍指向 128 字节有效内存块的指针。

FREE 是释放内存 malloc 是分配内存

图 9(image9.png)

// 在堆上分配100字节,指针p5指向该内存的首地址
volatile int *p5 = malloc(100);

手写红色批注:8 + 100 = 108

下方标签(部分可见):p2  p4

那8个字节 其实是 信息头

图 10(image10.png)

铁头山羊 FreeRTOS

A diagram showing three stick figures pulling a rope that lifts a 10000-ton “FreeRTOS内核” (FreeRTOS kernel) weight via a pulley. Each figure represents a different interrupt handler with a distinct role in the RTOS context switching mechanism.

Labels and speech bubbles:

  • FreeRTOS内核 10000吨 (FreeRTOS kernel 10000 tons) — the heavy weight being pulled
  • PendSV — speech bubble: “我是主力” (I am the main force)
  • Systick — speech bubble: “我负责时间片调度 也产生系统节拍” (I am responsible for time slice scheduling, and also generate the system tick) — with “系统节拍” underlined in red/pen
  • SVC — speech bubble: “我只负责启动第一个任务, 干完我就收工啦” (I am only responsible for starting the first task; once done, I’m off work)

图 11(image11.png)

A hand-annotated diagram explaining the problems with using standard C library malloc()/free() in an RTOS context, and introducing FreeRTOS’s heap_1–heap_5 memory management schemes.

Hand-drawn annotations (red ink):

  • Red arrow pointing down (top-left) toward the drawbacks list
  • Two red labels: “不可重入” (Not reentrant) and “内存碎片化” (Memory fragmentation)
  • A small red circle drawn below the two labels
  • Two black X marks crossed beneath malloc() and free()
  • Red text below the crosses: “速度慢且执行时间不确定” (Slow speed and uncertain execution time)
  • A large black hand-drawn arrow pointing downward

Text labels:

  • malloc()
  • free()
  • heap_1 ~ heap_5

不可重入 就是 我们两个任务同时进行 同时调用malloc 这样就会出错 就有了下面的方法

图 12(image12.png)

Diagram listing FreeRTOS inter-task communication and synchronization primitives (Chinese labels), shown as hexagonal tags with a dashed bracket grouping the semaphore family, and red hand-drawn arrows/circles annotating several items.

列表项(自上而下):

  1. 队列(邮箱)
  2. 队列集合
  3. 事件组
  4. 软件定时器(外有手绘红/橙色圆圈标记)
  5. 二进制信号量 ┐
  6. 计数信号量 ├─ 由虚线方框括为一组,右侧标注"信号量"
  7. 互斥锁 │
  8. 递归互斥锁 ┘
  9. 临界区
  10. 事件组
  11. 任务通知
  12. 消息缓冲区
  13. 流缓冲区

标注说明:

  • 第 4 项"软件定时器"旁有手绘圆形圈出
  • 右侧有手绘红色箭头/括号分别指向:队列(邮箱)、队列集合、事件组(上部)、信号量组(多个箭头汇入虚线方框)、事件组(下部)、任务通知、消息缓冲区、流缓冲区
  • 左下角底部字幕片段:“时器”(疑似"软件定时器"标题的一部分,被裁切)

图中分组(信号量):二进制信号量、计数信号量、互斥锁、递归互斥锁。

内核对象

图 13(image13.png)

FreeRTOS 内核架构示意图,显示了调度器、任务、堆内存管理以及各类内核对象之间的关系。

手写红字批注(左上角):
HAL_GetTick()

调度器(调度器)三种调度方式:

  • 带时间片的抢占式调度
  • 不带时间片的抢占式调度
  • 协作式调度

任务(用户任务)行:

  • 空闲任务
  • 定时器任务
  • 用户任务1
  • 用户任务2
  • ……
  • 用户任务n

中间:
定时器命令队列

堆内存管理:

  • heap1
  • heap2
  • heap3
  • heap4
  • heap5

右侧内核对象(信号量分组):

  • 事件组
  • 软件定时器
  • 二进制信号量
  • 计数信号量
  • 互斥锁
  • 递归互斥锁
  • 临界区
  • 事件组
  • 任务通知
  • 流缓冲区(部分被遮挡)

图例:
〈 〉 内核对象(任务也属于内核对象)

底部字幕:
使用这个hal get tick去获取系统当前的时间

时间片可以理解成:CPU 分给一个任务连续运行的一小段时间。

系统节拍就是用来看时间的一个东西

系统节拍 = 系统用来计算时间、驱动延时和任务调度的节奏

HAL_GetTick() = 获取当前已经累计了多少毫秒

图 14(image14.png)

两种特点:

时间片调度:同优先级任务轮流跑
抢占式调度:高优先级任务先跑

在 FreeRTOS 里,时间片调度靠 SysTick 系统节拍推动;真正切换任务时,一般由 PendSV 去完成。

图 15(image15.png)

  1. 以下哪个中断不是FreeRTOS内核运行所依赖的中断?()

A. SVC Interrupt B. PendSV C. NMI D. Systick Interrupt

(手写红笔批注,箭头指向 C 选项 NMI):
不可屏蔽中断
发生了致命错误

图 16(image16.png)

  1. 以下哪个选项不是freertos里不使用标准c语言的malloc()和free()的原因?( )

A. malloc()和free()执行速度慢,会影响系统的实时性

B. malloc()和free()不具有可重入性,多个任务同时调用时会产生不可预料的效果

C. malloc()和free()会导致严重的碎片化问题

D. 只要一引入FreeRTOS,标准C库的malloc()和free()将变得不可用

D选项错误

图 17(image17.png)

  1. 以下哪个是Systick中断在FreeRTOS里的作用?()

A. 负责任务切换
B. 负责产生时间片
C. 负责产生系统节拍
D. 负责启动第一个任务

(红色手写批注:A 选项下方写 “PendSV”;D 选项下方画线,并标注 “SVC” 及一个圆圈)

C选项

图 18(image18.png)

单片机位数推荐的 TickType 数据类型
8 位单片机uint16_t
16 位单片机uint16_t
32 位单片机uint32_t
64 位单片机uint64_t

(红笔圈注:8 位单片机、16 位单片机两行被红圈标出)

图 19(image19.png)

例4:请结合当前单片机的情况给configKERNEL_INTERRUPT_PRIORITY赋值

图示说明(8位中断优先级位图):

位7位6位5位4位3位2位1位0
(未使用)(未使用)(未使用)(未使用)(使用)(使用)(使用)(使用)

红色手写注释:

  • 只使用高4位
  • 数字越小优先级越高
  • Cortex-M3内核使用8位二进制数表示中断优先级
  • STM32F1单片机只使用了高4位
#define configKERNEL_INTERRUPT_PRIORITY      15 << 4

左移就是往更大的位权方向移动,所以叫“从低位到高位

图 20(image20.png)

Top card (partially shown):

Cortex-M3内核使用8位二进制数表示中断优先级
STM32F1单片机只使用了高4位

#define configKERNEL_INTERRUPT_PRIORITY          15 << 4

Annotation box (highlighted explanation):

意思是 cortex M3 是使用8位 2进制数表示 而f1单片机只用了4位


Chat response:

已思考 4s >

对,你理解得基本正确。

更准确地说:

Cortex-M3 的 NVIC 中断优先级寄存器,每个中断优先级用 8 位来存。

也就是寄存器里是这样的:

图 21(image21.png)

设置pendSV和Systick中断的优先级

我们需要为PSV和city tick这两个中断设置最低的优先级

图 22(image22.png)

这里的**“实际情况”**指的是:

不是所有 Cortex-M3 单片机的中断优先级位数都一样,要看你当前用的具体芯片到底实现了几位优先级。

比如 Cortex-M3 规定:中断优先级寄存器有 8 位:

bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0

但是具体到 STM32F1,它没有把 8 位都用完,只用了高 4 位:

有效位         无效位
bit7 bit6 bit5 bit4    bit3 bit2 bit1 bit0

所以这个"实际情况"就是指:

STM32F1 实际只使用了高 4 位优先级

因此配置 PendSV 和 SysTick 的优先级时,不能只看 FreeRTOS 的数字,还要看芯片的优先级寄存器格式。

比如 FreeRTOS 想把 PendSV 和 SysTick 设置成最低优先级,也就是 15:

图 23(image23.png)

优先级位数就是:

这个单片机到底用几个二进制位来表示"中断优先级数字"。

比如 STM32F1 只用了 4 位来表示中断优先级:

0000 ~ 1111

4 位能表示的数字范围是:

0 ~ 15

所以 STM32F1 有 16 个优先级编号:

0, 1, 2, 3, ... 15

其中:

0   优先级最高
15  优先级最低

所以 STM32F1 有 16 个优先级编号

0 优先级最高

15 优先级最低

图 24(image24.png)

因为二进制每一格就是一位:

1111

这里有 4 个二进制位,所以叫 4 位优先级。

如果一个芯片用了 3 位优先级:

000 ~ 111

那它只能表示:

0 ~ 7

如果用了 5 位优先级:

00000 ~ 11111

那它可以表示:

图 25(image25.png)

0 ~ 31

回到 STM32F1:

Cortex-M3 的优先级寄存器位置是 8 位:

bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0

但 STM32F1 实际只用了高 4 位:

   有效位            无效位
bit7 bit6 bit5 bit4  bit3 bit2 bit1 bit0

所以 STM32F1 的 优先级位数 = 4 位.

因此最低优先级就是:

2^4 - 1 = 15

图 26(image26.png)

所以 STM32F1 的优先级位数 = 4 位。

因此最低优先级就是:

2^4 - 1 = 15

这就是为什么 FreeRTOS 里会有:

#define configKERNEL_INTERRUPT_PRIORITY	(15 << 4)

一句话记住:

优先级位数 = 芯片实际拿几个二进制位来表示中断优先级,STM32F1 是 4 位,所以优先级范围是 0 到 15。

Api 就是编程接口的意思

t

图 27(image27.png)

例5:本例用来展示configMAX_SYSCALL_INTERRUPT_PRIORITY的作用。

优先级堆栈图(数字越小,优先级越高):

优先级号颜色区域说明
0黄色高优先级,要求高实时性
1黄色高优先级,要求高实时性
2黄色高优先级,要求高实时性
3黄色高优先级,要求高实时性
4黄色高优先级,要求高实时性(configMAX_SYSCALL_INTERRUPT_PRIORITY = 5 << 4 的边界)
5绿色低优先级,实时性要求没那么高
6绿色低优先级,实时性要求没那么高
⋮绿色低优先级,实时性要求没那么高
13绿色低优先级,实时性要求没那么高
14绿色低优先级,实时性要求没那么高
15绿色低优先级,实时性要求没那么高(configKERNEL_INTERRUPT_PRIORITY = 15 << 4)

红色注释(手写):

  • 优先级 0-4 区域:不能调用FreeRTOS的任何API,以防止高优先级的中断被FreeRTOS干扰
  • 边界标注:configMAX_SYSCALL_INTERRUPT_PRIORITY = 5 << 4
  • 优先级 5-15 区域(被红色椭圆圈出):可以调用FreeRTOS的API,被干扰一下也没什么事

左侧箭头标注: 数字越小,优先级越高

底部字幕: 而在这些低优先级里面呢会放一些不那么紧急的代码

5左移4 就是说 我们只能在优先级 比5更低的调用freertos 接口 相反 0到4 他是不允许调用的

图 28(image28.png)

一句话记住:

可以调用 FreeRTOS API = 这个中断可以使用 FreeRTOS 提供的函数,去和任务交互。

例如:

中断收到数据
    ↓
调用 xQueueSendFromISR( )
    ↓
把数据发给 FreeRTOS 任务
    ↓
任务被唤醒,开始处理数据

图 29(image29.png)

然后 FreeRTOS 内核负责决定:

现在让哪个任务运行?
运行多久?
什么时候切换到另一个任务?
任务在等待时要不要让出 CPU?

比如你有 3 个任务:

任务1:LED闪烁
任务2:串口接收
任务3:传感器采集

FreeRTOS 内核会根据任务优先级和状态来调度它们。

例如:

串口任务优先级高
LED任务优先级低

图 30(image30.png)

意思是 内核一般在最低优先级 使用

已思考 1 秒 》

对,可以这样理解:FreeRTOS 内核相关中断一般设置在最低优先级运行。

、

图 31(image31.png)

配置项5:configUSE_TIME_SLICING
是否使用时间片

设置 为0就是不带时间片 设置为1 就是带时间片

我们 一般设置为带时间片的

内存和硬盘 内存是断电以后丢失数据 硬盘是永久

单片机 corte M3 相当于电脑的CPU

图 32(image32.png)

Hand-drawn diagram showing two stacked memory blocks with Chinese annotations in red.

  • Top box labeled Flash with red arrow annotation: 相当于硬盘,程序和常量存储在这里 (Equivalent to hard disk, programs and constants are stored here)
  • Bottom box labeled SRAM with red arrow annotation: 相当于内存,变量存储在这里 (Equivalent to memory, variables are stored here)

Both boxes have small triangular/arrow markers on their left edges (pointing into the boxes), suggesting data flow or bus connections.

图 33(image33.png)

FLASH里:程序当中需要长期存储的、不可变的部分

SRAM里:临时存储的、经常变化的部分

中断向量表 就是一个目录 中断发生的时候 他就会查找中断服务程序

栈

图 34(image34.png)

这是一张用仓库中堆放箱子来比喻"栈"数据结构的示意图,左侧是一栋房屋(含房间),右侧延伸出一块灰色的仓库地面,仓库最里端已存放两个蓝色立方体箱子(上面标注 “A”,下面标注 “B”),最外侧是下一个空位。

图中红色手写笔标注与文字说明如下:

  • 栈底(红字标题)

    • 仓库最里边的位置
    • (红色箭头指向仓库最里端已放好的箱子 A 处)
  • 栈顶(红字标题)

    • 下一个空位置
    • (红色箭头指向箱子 B 下方、空仓库地面边缘)
  • 栈深度(红字标题,前面有一个空心圆圈 ○ 作为编号/序号标记)

    • 能够放多少箱子
    • (红色曲线从仓库地面一直延伸、绕到房屋一侧,用以示意整个仓库可用的容量范围)

图中的元素文本还包括:

  • 仓库内两个箱子上的字母 A 和 B
  • 房屋内部有家具(床、桌椅等)作背景装饰

图 35(image35.png)

房屋平面图作为栈(stack)的形象比喻:右侧有三个蓝色箱子从上到下依次标记为 A、B、C,一个红色向下箭头指向最下方的箱子,说明取出箱子的顺序。图中用红色文字标注该动作。

标签/文字:

  • A
  • B
  • C
  • 箱子(箭头处小字)
  • 弹栈 - 取一个箱子出来(红色标注)

弹栈 是最靠近门口的拿出来

压栈 就是放一个箱子进去

后进先出 是最后放进去的 最先拿出来

图 36(image36.png)

例1: 提供以下3个编程接口,其中stack_init()用来创建一个新的栈,相当于新建一个仓库;
stack_push()用来压栈,相当于把箱子推入到仓库当中;stack_pop()用来弹栈,相当于从仓库里取出最靠
近门口的那个箱子。

//
// @作用: 初始化一个新的栈
// @参数: depth - 栈深度
//
void stack_init(int depth);
//
// @作用: 压栈
// @参数: i - 要压栈的整数
//
void stack_push(int i);
//
// @作用: 弹栈
// @返回: 被弹出栈的整数
//
int stack_pop(void);

示意图说明:

  • 第一个图:一个空的长方形(栈)旁边标注 depth(栈深度),表示初始化时创建的栈空间。
  • 第二个图:栈中已有一个元素(带阴影的小方块),底部箭头向上指向栈顶,下方有阴影方块等待压入,表示压栈操作。
  • 第三个图:栈中有三个元素(带阴影的小方块),底部箭头向下伸出,表示弹栈操作。

Flash 主要用来“存程序”,SRAM 主要用来“运行时存变量”。

Data 和bass 存储 全局变量 和静态变量 heap 存储堆 stack 存储调用栈

“调用栈”就是由系统自动维护的栈,它的作用是在函数调用发生的时候保存上下文。 (也就是看不见 摸不着的)

Flash:像硬盘,断电不丢,存程序。SRAM:像内存,断电丢失,程序运行时用。

1 个 word 等于 4 个 byte。

128个字(word)= 512个字节(byte)

任务栈 就是存储函数调用的上下文

图 37(image37.png)

例3: 忽略Cortex-M3寄存器、忽略字节对齐、忽略编译器优化,在这种情况下请估算以下任务的任务栈深度设置的是否足够。

void vTask1(void *pvParameters)
{
    int a[64]; // 局部变量,整型数组a,长度64
    
    f(); // 调用子函数f()
    
    ....
}

static void f(void)
{
    float b[64]; // 局部变量,浮点型数组b,长度64
    ....
}

int main(void)
{
    ....
    xTaskCreate(vTask1, "task1", 128, NULL, 1, NULL);
    vTaskStartScheduler();
    
    while (1)
    {
    }
}
vTask1()f()
参数void *pvParameters(4个字节)无
局部变量int a[64](256个字节)float b[64](256个字节)
返回地址4个字节无

就是看 int float 的字节

因为调用栈 stack 主要保存函数调用时临时需要的信息,比如:

内容

是否在调用栈里

局部变量

是

参数

是

返回地址

是

静态局部变量

不是

图 38(image38.png)

因为 静态局部变量虽然写在函数里面,但它不是"临时变量"。

调用栈 .stack 的特点是:

函数进入时申请,函数退出时释放。

比如普通局部变量:

void test(void)
{
    int a = 10;
}

a 是普通局部变量,函数 test() 执行时才出现,函数结束后就没了,所以它放在 调用栈 stack 里。

但是静态局部变量:

void test(void)
{
    static int c = 20;
}

c 虽然写在 test() 函数里面,但是它有 static,意思是:

它只初始化一次,并且整个程序运行期间都一直存在。

所以它不能放在 stack 里。因为 stack 里的东西函数结束就会被释放。

图 39(image39.png)

BaseType_t

基本数据类型,成功/失败、是/否

当前处理器架构下最有效率的
数据类型 (int32_t)

Ba… (右侧被截断)

(手写批注)

  • 圈注:32 ↑
  • MSP430
  • ⊙16
  • AVR

(底部字幕)你使用它写FATTOS程序的时候

根据不同位数的单片机来写

图 40(image40.png)

TickType_t

用来记录系统节拍的数据类型

具体数据类型取决于

configTICK_TYPE_WIDTH_IN_BITS

BaseType_t

基本数据类型,成功/失败、是/否

当前处理器架构下最有效率的

数据类型(int32_t)

UBaseType_t

BaseType_t的无符号版本,非负数

对于STM32F1单片机

该数据类型为uint32_t

Ticktype_t 的第一个用法

图 41(image41.png)

// 用于记录系统节拍
TickType_t  xTickCount = 0;

手写标注 (Hand-written annotation): Systick (红色标注)

阶梯图 (Staircase diagram) — tick numbers on each step: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 …

红色标注 (Red annotation):

  • 假设 Systick 溢出的时间间隔为 t0
  • (图示:在第 7 与第 8 阶之间有双向箭头,表示一个溢出周期 t0)

底部公式 (Bottom formula):

当前时间 = xTickCount * t0

决定的类型

图 42(image42.png)

TickType_t的实际数据类型由configTICK_TYPE_WIDTH_IN_BITS决定

取值实际的数据类型适用的单片机架构
TICK_TYPE_WIDTH_16_BITSTickType_t = uint16_t8位或16位单片机
TICK_TYPE_WIDTH_32_BITSTickType_t = uint32_t32位单片机
TICK_TYPE_WIDTH_64_BITSTickType_t = uint64_t32位或64位单片机

任务划分为 运行状态 和非运行状态

任务的四种状态

图 43(image43.png)

运行状态 (Running State) | 被调度器选中,正在由单片机的CPU执行它的任务代码

就绪状态 (Ready State) | 等待被执行

阻塞状态 (Blocked State) | 因等待先决条件(等待超时或者等待事件)而主动放弃执行权

暂停状态 (Suspended State) | 调用vTaskSuspend()使其不能被调度器选中

图 44(image44.png)

手绘漫画,用"接饮水机的水"的排队场景比喻 FreeRTOS 的任务状态:饮水机代表 CPU,正在接水的人 A 是 VIP(运行态),排队等待的 B、C 是普通用户(就绪态),D 被拉黑退出排队(暂停态),E 因等待超时、F 因等待某个事件发生而暂时离开(阻塞态)。

标签与文字:

  • 饮水机(左侧):CPU
  • A(VIP):运行 (Running)
  • 顶部标注:VIP、普通用户
  • B、C:就绪 (Ready)
  • D 对话气泡:「哥们儿,你们为啥不去接水?」;「我进了黑名单」 —— 暂停 (Suspended)
  • E 对话气泡:「刚喝了,10分钟之后再喝」;下方标注:在等待超时
  • F 对话气泡:「我买的杯子还没到,你呢?」;下方标注:在等待某个事件发生
  • E、F 共同状态:阻塞 (Blocked)
  • 红色手写英文注释(覆盖在 E 附近):vTaskDelay( )

空闲任务是其他任务的一个备胎

图 45(image45.png)

vTaskDelay(pdMS_TO_TICKS(1000));

vTaskSuspend(xLED1TaskHandle); // 挂起任务

暂停状态就是不会被调度器选中

图 46(image46.png)

This is a timing diagram illustrating FreeRTOS task state transitions, showing how a task switches between running (priority 1) and being suspended, with the idle task (priority 0) running during gaps.

Y-axis (task priority labels, top to bottom):

  • LED1任务(1)
  • Task1任务(1)
  • 空闲任务(0)

X-axis: Time progression (arrow pointing right)

Event annotations (top, with arrows pointing down to specific moments on the timeline):

  • HAL_GPIO_TogglePin(…)
  • vTaskSuspend(…) — marks where Task1 drops down to the idle level
  • vTaskResume(…) — marked with a red hand-drawn arrow indicating Task1 resumes running

Event annotation (bottom, with arrow pointing up to the timeline):

  • vTaskDelay(…) — marks a gap between running periods

State label (below the timeline, in the suspended interval):

  • 暂停状态

Continuation markers:

  • “……” appearing on both left and right sides of the timeline to indicate the pattern continues

Resume 是恢复状态

]

图 47(image47.png)

A hand-drawn hierarchical bracket diagram classifying scheduling algorithms in FreeRTOS. The top category “FreeRTOS里的调度算法” branches into “抢占式调度” (preemptive) and “协作式调度” (cooperative); the preemptive branch further splits into “带时间片的抢占式调度” and “不带时间片的抢占式调度”.

  • FreeRTOS里的调度算法
    • 抢占式调度
      • 带时间片的抢占式调度
      • 不带时间片的抢占式调度
    • 协作式调度

图 48(image48.png)

抢占式调度(Preemptive Scheduling)

调度器总是选择更高优先级的任务来执行,当高优先级的任务就绪的时候当前任务会立刻被抢占。

(橙色手绘下划线标注:“当高优先级的任务就绪的时候当前任务会立刻被抢占”,末尾有一个小圆圈标记)

协作式调度(Cooperative Scheduling)

调度器同样优先选择高优先级的任务执行来执行,但只有当前任务主动让出控制权的时候才会发生切换。

图 49(image49.png)

这是一张手绘的示意图,展示了 FreeRTOS 中的"抢占式调度"机制。图中用一个饮水机代表 CPU,四个不同优先级的人物 A/B/C/D 分别处于运行、就绪、阻塞三种状态,以说明高优先级任务可以抢占低优先级任务。

标题(左侧竖排):抢占式调度

角色与优先级:

  • A:优先级2,手持杯子正在饮水机前接水(运行状态,运行)
  • B:优先级1,手持杯子排队等待(就绪状态,就绪)
  • C:优先级1,手持杯子排队等待(就绪状态,就绪)
  • D:优先级4,正在喝水(阻塞状态,阻塞)

状态标签(底部):运行 / 就绪 / 阻塞

红色手写注释(指向 D):

VIP客户,水喝完了可以直接接水,不需要等待游客A把水接完

底部红色弧线:从 D 经过 B、C 一直指向 A,表示 D(优先级4,最高)一旦从阻塞转为就绪,会立刻抢占正在运行的 A(优先级2)。

图 50(image50.png)

标题:协作式调度

图示说明:用手绘漫画形式以"接水"场景类比 FreeRTOS 协作式调度。CPU 比喻为饮水机,多个任务(人物 A/B/C/D)按优先级排队获取 CPU 资源。

图示元素与标注:

  • 左侧竖排标题:协作式调度
  • 饮水机图标,标注:CPU
  • 人物 A,头顶标注 优先级2,正在接水,状态:运行
  • 人物 B,头顶标注 优先级1,端着杯子等待,状态:就绪
  • 人物 C,头顶标注 优先级1,端着杯子排在 B 后面
  • 人物 D,头顶标注 优先级4,正在喝水,状态:阻塞
  • 一条红色弧线箭头从 D 绕过指向 B 身后,象征阻塞任务完成后回到就绪队列

手写红色批注(右侧指向 D):

VIP客户,水喝完了可以插队
但不能打断正在接水的人

协作式调度 是不可以插队

图 51(image51.png)

抢占式调度协作式调度
高优先级任务就绪会自动抢占不会发生抢占,需要主动让出执行权

我们一般用抢占式调度 不用协作式调度

图 52(image52.png)

  1. 抢占式调度和协作式调度的核心区别是什么?

自动抢占 vs 手动让出执行权

图 53(image53.png)

文件:工程根目录\FreeRTOS\Inc\FreeRTOSConfig.h

/* 如果configUSE_PREEMPTION被设置为1,则使用抢占式调度
   如果configUSE_PREEMPTION被设置为0,则使用协作式调度 */
#define configUSE_PREEMPTION                       1

vTaskDelay() 期间运行其他任务;延时结束后,原任务只是重新获得“竞争 CPU 的资格”,是否马上运行由优先级决定,不是随机的。

Hal_delay 的本质就是不断的调用 while 循环直到延时的结束 这个while循环才退出,他和我们抢占式调度不一样 他是如果一个高优先级的任务 始终处于可执行的状态,那么低优先级的 任务 就是始终不可以执行 如下图

图 54(image54.png)

例3: 请写一段代码实现LED1和LED3同时闪烁,这次使用HAL_Delay()来实现延迟,并且两个任务设置不同的优先级。比如LED1任务的优先级设置为1,LED3任务的优先级设置为2。观察实现现象,并分析原因。


任务调度时序图说明:

该图展示了一个三层的任务优先级时序图,纵轴从下到上依次为三个优先级层级,横轴表示时间流逝。

纵轴标签(优先级层级):

  • LED3 (2)
  • LED1 (1)
  • 空闲 (0)

横轴: 表示时间(带向右的箭头)。

关键标注(带红色箭头指向):

  • 在 LED3 (2) 层级上,标注 HAL_GPIO_WritePin(...),对应一个红色圆圈标记点
  • 在 LED3 (2) 层级上,标注 HAL_Delay(...),对应另一个红色圆圈标记点
  • 在 空闲 (0) 层级上,有一个橙色圆圈标记点

手绘红色笔注释: 在 LED1 (1) 附近有一段手绘的红色弧线/箭头,指向 空闲 (0) 层级上的标记点,暗示 LED1 任务未运行,空闲任务在运行。

图示含义: LED3(优先级2)目前正在运行,正在执行 HAL_GPIO_WritePin(…) 和 HAL_Delay(…);LED1(优先级1)虽然存在但未获得 CPU;空闲任务(优先级0)有被调度运行的迹象。由于 LED3 任务的优先级高于 LED1,并且 LED3 内部使用 HAL_Delay() 阻塞,期间没有主动让出 CPU,导致 LED1 任务无法运行,从而出现 LED3 单独闪烁、LED1 不闪烁的现象。

所以 用vtaskdelay 不用hal_delay

图 55(image55.png)

例:我们之前调用的vTaskDelay()实际上是通过让当前任务进入阻塞状态来实现的,基于这一点请分析以下闪灯程序各个任务的状态变化。

时序图说明(闪灯程序任务状态变化图):

纵轴:

  • LED1(1)
  • 空闲(0)

横轴:时间

方波信号显示 LED1 引脚电平在高低之间切换。

标注:

  • LED_TogglePin(...):在方波每次上升沿和下降沿位置(红色圆圈标记,红色箭头指向)
  • vTaskDelay(...):在下降沿后标记(红色圆圈 + 红色箭头延伸到下一个上升沿)
  • LED1 - 阻塞(vTaskDelay 期间)
  • IDLE - 运行(vTaskDelay 期间)
  • LED1 - 运行(短脉冲)
  • IDLE - 就绪(短脉冲)

含义:LED1 任务运行时执行 LED_TogglePin(…) 后调用 vTaskDelay(…),期间 LED1 进入阻塞状态,IDLE 任务运行;vTaskDelay 结束后 LED1 转为就绪并再次运行。

空闲任务 要么处于 运行状态 要么处于就绪状态 (可执行的状态)

我们看第二张图

图 56(image56.png)

例1: 我们之前调用的 vTaskDelay() 实际上是通过让当前任务进入阻塞状态来实现的,以下闪灯程序各个任务的状态变化。

一张时序波形图,展示闪灯程序中 LED1 任务与 IDLE 任务的状态变化。纵轴表示 LED 电平状态,横轴表示时间;方波高低电平对应不同的任务运行/阻塞阶段。

标签/注释:

  • 纵轴上端标注:LED_TogglePin(…)(指向高电平起点)
  • 纵轴刻度:LED1(1)(高电平)、空闲(0)(低电平)
  • vTaskDelay(…)(指向低电平处)
  • 中部文字:LED1 - 阻塞 / IDLE - 运行
  • 下方文字(对应窄脉冲区间):LED1 - 运行 / IDLE - 就绪

我以前以为空闲任务是 执行了阻塞状态 是taskdelay 执行的阻塞状态 其实不是 它就是循环等待、做少量系统清理

所以空闲任务不是在执行 vTaskDelay()。

而是其他任务执行 vTaskDelay() 后,调度器才切换到空闲任务。最普通的情况下,它就是循环等待、做少量系统清理。

阻塞:任务在等待时间或事件,条件满足后自动恢复。暂停:任务被人为停住,必须调用 vTaskResume() 才能恢复

图 57(image57.png)

This image contains two side-by-side timing diagrams illustrating the difference between preemptive scheduling without and with time slices in FreeRTOS, using a phone-charging analogy between two people (甲 and 乙).

Left diagram — “改进前” (Before improvement):
Step-state diagram with Y-axis levels labeled 甲(1), 乙(1), 空闲(0).

Red annotations along the timeline (left to right):

  • 甲找插座
  • 乙发现插座
  • 甲充满电后进入阻塞
  • 乙的手机关机
  • 乙开始充电
  • 乙充满电后进入阻塞

Caption below:
改进前:甲先充完乙再充
相当于不带时间片的抢占式调度

Right diagram — “改进后” (After improvement):
Step-state diagram with Y-axis levels labeled 甲(1), 乙(1), 空闲(0), showing a square-wave pattern alternating between 甲 and 乙.

Red annotation: 时间片 (with a double-arrow indicating one slice width).

Caption below:
改进后:甲和乙每人充10分钟
相当于带时间片的抢占式调度

图 58(image58.png)

注意这里有两个不同的 void:

void vTaskFunction(void *pvParameters)

前面的 void:

void vTaskFunction

表示函数没有返回值。

后面的 void *:

void *pvParameters

表示参数是一个"通用指针"。

所以这句话不是"函数没有参数",而是:

函数接收一个 void * 类型的通用指针参数。


二、为什么参数类型要用 void *

因为 FreeRTOS 不知道你想给任务传递什么类型的数据。

pvParameters 这个通用指针 (const char *) 强行转的类型是括号里面的

图 59(image59.png)

已思考若干秒 ›

这句:

const char *str = (const char *)pvParameters;

意思是:

把 pvParameters 这个通用指针,转换回"字符串指针",然后交给 str 保存。

图 60(image60.png)

已忘写 55°

不是永远都要写成:

(void *)task1Str

但只要你使用标准的 xTaskCreate(),它的第四个参数类型固定是 void *。

所以有时候 我们要进行转换

图 61(image61.png)

  1. 以下哪种说法是错误的?( )

A. 时间片轮转只在相同优先级的任务之间起效果,如果两个任务的优先级不同则遵循抢占式调度的规则

B. 时间片的主要作用是让相同优先级的任务之间获得近似相等的执行时间

C. 可以通过 configTICK_RATE_HZ 来调节时间片的长度,configTICK_RATE_HZ 的值越大,时间片越长

D. 时间片的长度过短会频繁进入 SYSTICK 中断,影响程序的执行效率

A 选项是正确的

C选项 值越大 时间片越短

图 62(image62.png)

  1. 以下哪个配置项用来调节时间片的长度?( )

    A. configUSE_PREEMPTION B. configUSE_TIME_SLICING C. configTICK_RATE_HZ D. configCPU_CLOCK_HZ

  2. 以下哪种说法是错误的?( )

A 是用来看抢占式 还是协作式

B是用来开关时间片的

C 对的

图 63(image63.png)

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
UART_HandleTypeDef huart1;

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);

(Editor tabs shown: main.c, key_task.h, *key_task.c — the key_task.c tab is circled in red/orange by hand.)

就像这样 我们声明是在main.c 声明 可是实际调用是在key.c 那我们就在key.c 里面添加extern 全局声明

图 64(image64.png)

该图片展示了轻触按键(按钮)的三个组成部分:左侧为按钮实物图,中间为按钮内部结构示意图,右侧为锅仔片实物图。中间的结构示意图标注了外壳、锅仔片和金属触点三个关键部件。

按钮实物图 (左侧)

按钮内部结构示意图 (中间):

  • 外壳
  • 锅仔片
  • 金属触点

锅仔片实物图 (右侧)

原来下面那个是锅仔片

图 65(image65.png)

手绘示意图:说明 STM32F103 按键轮询检测的频率特性。

标题文字(中文):

  • STM32F103 最大主频72MHz
  • 每秒检测按钮状态达10万次左右

图示内容:

顶部为一行数字序列(采样读到的电平值):

0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 1 1 1

每个数字下方均有一个向下箭头,表示一次采样时刻。

其中用红色箭头标注了若干采样点,并在红色箭头上方手写红色"飞"字(意为"飞过/漏掉按键"),被标注的位置依次为序列中的 1、1、1、1(即第 13、16、19 位左右),表示这些采样点飞过了按键按下时刻,没有正确读到按下状态。

序列下方为对应的按键电平波形:先保持低电平(空闲态),随后出现一连串带抖动的脉冲波形(按键按下过程,含机械抖动),最后电平逐渐恢复。

整张图用于直观说明 MCU 按固定时间间隔轮询按键时,会因抖动或采样时机的关系出现漏检的情形,因此通常需要结合消抖算法处理。

所以要进行消抖

Freertos的不用进行消抖

图 66(image66.png)

和裸机最大的区别

裸机FreeRTOS
HAL_Delay() 整个程序等待vTaskDelay() 只有当前任务等待
CPU基本空闲CPU去执行其他任务
一个 while(1) 完成所有功能每个任务都有自己的 for(;;)
靠自己安排执行顺序由调度器安排任务运行

记住一句话:

  • 裸机:HAL_Delay() 是 CPU等。
  • FreeRTOS:vTaskDelay() 是 当前任务等,CPU不等,去运行其他任务。

图 67(image67.png)

A waveform diagram illustrating the electrical signal behavior of a mechanical button press, showing two complete press-release cycles with debounce characteristics.

Diagram description: A horizontal time-axis signal trace showing two button press events. Each press starts at logic level “1”, transitions to “0” with visible bouncing/chattering lasting less than 1ms, stays stable at “0” for more than 10ms, then transitions back to “1” with another bounce period before settling.

Labels and annotations:

  • 1 — initial logic level (before first press)
  • >10ms — duration of the stable low period between the two bounce regions
  • 0 — logic level reached after the first debounce
  • >10ms — duration of the stable low period of the second press
  • 1 — final logic level (after second press released)
  • 按钮的抖动时间一般小于1ms — (The button bounce time is generally less than 1ms) — labeled under each of the two transition regions
  • An orange circle annotation marker (appears mid-diagram, likely a cursor or highlight indicator)

这个是freertos

图 68(image68.png)

因此,大多数程序都会把 vTaskDelay() 放在循环的最后。


总结一句话:

vTaskDelay(pdMS_TO_TICKS(10)); 并不是"先延时",而是每完成一次按键扫描后休眠10ms,再开始下一次扫描。这样既避免任务一直占用CPU,又让按键以约 100 次/秒(每10ms一次) 的频率进行检测,这对人按键来说已经足够了。


↳ 因此,大多数程序都会把 `vTaskDelay()` 放在循环的最后。

和我们裸机不一样的写法

对,这里的写法和裸机的思路确实不一样,这是很多人刚学 FreeRTOS 时最容易困惑的地方。

图 69(image69.png)

有了 CMSIS 中的寄存器文件(比如 stm32g431xx.h):

  • 芯片厂商在里面把 0x40020000 开始的一堆地址打包贴上了标签叫 GPIOA。
  • 把偏移地址 0x14 对应的寄存器命名为了 ODR。
  • 这样你在代码里就可以直接写:
    GPIOA->ODR = 0x01;

所以,CMSIS 文件夹(特别是里面的 Device Support 部分)本质上就是一份"官方寄存器映射表"。不管是你直接写寄存器代码,还是调用标准库、HAL 库,最终编译器在编译时,都是通过这个文件夹里的寄存器定义,把代码准确地翻译成芯片硬件能听懂的物理地址。

Cmsis 文件

堆内存管理

堆是 一段能动态分配的内存

图 70(image70.png)

单片机里的内存

+------+------+------+------+------+
| .data| .bss |      | .heap| .stack|
+------+------+------+------+------+
.data.bss(空).heap.stack
  • 全局变量和静态变量 (对应 .data 和 .bss)
  • 堆 (对应 .heap)
  • 栈 (对应 .stack)

图 71(image71.png)

噢噢 意思是堆是分配内存 栈是储存一些特殊的

图 72(image72.png)

栈里存的 3 种"特殊数据"

1. 临时的局部变量(用完就扔)

只要是在函数内部定义的变量,都存在栈里。函数执行完,这部分内存就自动释放了。

void MyFunction(void) {
    int temp = 5; // 这个 temp 就是"特殊数据",存在栈里
    // 函数结束离开后,temp 占用的栈空间自动消失
}

2. 函数调用的"指路明灯"(返回地址)

当你的任务代码正在执行 A 函数,突然要调用 B 函数,CPU 必须把"当前执行到哪一行代码"记在栈里(压栈)。等 B 函数执行完了,CPU 再看一眼栈里的记录(出栈),就能准确地跳回 A 函数继续往下走。如果没有栈,CPU 就会"迷路",不知道下一步该去哪。

3. 任务切换时的"进度存档"(寄存器快照)

这是 RTOS(实时操作系统)最神奇的地方。假设单片机只有一个 CPU 核心,但你开了 3 个任务:

图 73(image73.png)

malloc() free()

为什么裸机程序里用得好好的,FreeRTOS里不让用了?

  1. 标准C语言库里的malloc()速度慢且不确定,影响实时性

  2. malloc()和free()容易产生内存碎片化问题

3. malloc()和free()不具有可重入性 (黄色高亮)

五种堆内存管理方案

图 74(image74.png)

A diagram showing heap memory management (堆内存管理) consisting of 5 separate heap blocks arranged horizontally:

  • heap1
  • heap2
  • heap3
  • heap4
  • heap5

The title at the top reads “堆内存管理” (Heap Memory Management), illustrating the concept that memory can be divided into multiple distinct heap regions.

图 75(image75.png)

可重入 (Reentrant):一个函数可以被多个任务同时调用而不出错。

HAL_UART_Transmit()

不可重入

xTaskCreate(..., "Task1", ...);
xTaskCreate(..., "Task2", ...);

同时调用HAL_UART_Transmit()

图示说明: FreeRTOS调度器下有两个任务(Task1 和 Task2)的时间轴,两个任务用绿色区块表示各自的执行时间段。两条蓝色虚线之间,Task1 与 Task2 的执行区间重叠,用红色虚线框出,表示此时两个任务同时调用 HAL_UART_Transmit()。

  • FreeRTOS调度器
  • Task1
  • Task2
  • 竞争使用UART接口,产生意想不到的结果

Freertos 里的任务是并发的 这个uart 他们竞争使用

图 76(image76.png)

另起炉灶,创建自己的堆内存管理方案

堆内存管理

heap1heap2heap3heap4heap5

(右侧可见部分内容:递归互、临界、事件、任务、消息、流缓)

栈:像一叠盘子,一层层放,拿也是从上面拿

堆:像一个仓库,哪里有空位就放哪里

图 77(image77.png)

A diagram comparing memory allocation between a bare-metal program and a FreeRTOS program. On the left, a single vertical memory block labeled main() contains one green region pointed to by a red arrow from loc() (local). On the right, three separate vertical memory blocks labeled task1, task2, and task3 each contain a green region, with three red arrows from malloc() pointing to each task’s memory region.

Labels/text:

  • 裸机程序 (Bare-metal program)
  • FreeRTOS程序 (FreeRTOS program)
  • loc() — red arrow pointing to the green block in the main() column
  • malloc() — three red arrows pointing to the green blocks in task1, task2, and task3
  • main()
  • task1
  • task2
  • task3

裸机只是 一个main调用malloc rtos不是 malloc是不是可重入的

图 78(image78.png)

堆内存管理

heap1heap2heap3heap4heap5

为什么创建 这五种呢 因为 freertos 用不了 所以就用这个了

图 79(image79.png)

对比:标准 C 语言库与 FreeRTOS 的内存管理函数。

标准C语言库FreeRTOS
malloc() – 分配内存pvPortMalloc() – 分配内存
free() – 释放内存vPortFree() – 释放内存

Heap1 到heap 5 的区别与联系

Heap1 只能分配 不能释放 pvportmalloc 分配内存 vportfree 释放内存

用在只创建邮箱 不销毁内核对象

Heap2 内存碎片化问题严重 (最优匹配算法)不合并相邻空闲块

图 80(image80.png)

Code (left side):

3. p3 = pvPortMalloc(50);
4. vPortFree(p1);
5. vPortFree(p3);
6. p4 = pvPortMalloc(25);
7. p5 = pvPortMalloc(70);
8. p6 = pvPortMalloc(64);

(Line 8 is highlighted in yellow with a black arrow pointing to it on the left.)

Memory heap diagram (right side):

870822820825817854
block size (8)allocated (70)block size (8)freeblock size (8)freeblock size (8)allocated (25)block size (8)freeblock size (8)free

Braces below the heap label three free blocks:

  • 空闲块1 (Free Block 1) — spans the cells showing 22 and 20 (the “22” cell is circled in red with an arrow pointing down to it)
  • 空闲块2 (Free Block 2) — spans the cell showing 17 (the “17” cell is circled in red)
  • 空闲块3 (Free Block 3) — spans the cell showing 54 (the “54” cell is circled in red)

如图 到后面就会出现分配不了的画面

Heap4 就可以 也是使用最优匹配算法Heap_4可以合并相邻空闲块

图 81(image81.png)

Heap_3

对标准C库的malloc()和free()进行改造,让其具有可重入性

问题:

  1. 内存碎片化
  2. 速度慢、不确定

流程图说明: FreeRTOS 内核对象需要动态分配内存,若直接使用标准C语言库的malloc()和free(),结果不可行(不可重入)。

图示流程:

  • FreeRTOS
    • ↓
  • 内核对象
    • ↓ (动态分配内存)
  • 标准C语言库的 malloc()和free()
    • ↑
  • 不可行(不可重入)

Malloc 和free 不太好用 heap3是让malloc 和free 变得可重入

图 82(image82.png)

在malloc()和free()执行期间暂停调度器

void *pvPortMalloc( size_t xWantedSize )
{
    void *pvReturn;

    vTaskSuspendAll(); // 暂停调度器

    pvReturn = malloc( xWantedSize );

    xTaskResumeAll();  // 恢复调度器

    return pvReturn;
}

说明:代码中 vTaskSuspendAll() 和 xTaskResumeAll() 行为以黄色高亮,注释分别为"暂停调度器"和"恢复调度器";malloc 调用所在的一行 pvReturn = malloc( xWantedSize ); 用红色边框突出标注。

重入的意思是同时使用malloc 和free 就会卡住

这个是heap 5(用的比较少)

图 83(image83.png)

Heap_5 同Heap_4(最优匹配算法、合并相邻空闲块),支持不连续内存

Heap_1 Heap_2 Heap_3 Heap_4 Heap_5

左侧图示(Heap_1/2/3/4 适用场景):

  • STM32 芯片内含 SRAM1
  • 我们使用的STM32F103RCT6内部只有一块连续的SRAM
  • 内存布局:
    • 0xFFFF FFFF
    • 0x2000 BFFF
    • 0x2000 0000 — SRAM1
    • 0x0000 0000
  • heap_1、heap_2、heap_4
  • uint8_t ucHeap[]; (位于SRAM1中)
  • 全部位于SRAM1之中

右侧图示(Heap_5 适用场景):

  • STM32 芯片含 SRAM1、SRAM2,通过 FSMC接口 连接外部 SRAM3
  • 另外一些更复杂的嵌入式系统会有多个SRAM存储器
  • 内存布局:
    • 0xFFFF FFFF
    • SRAM3
    • SRAM2
    • SRAM1
    • 0x0000 0000
  • FreeRTOS管理的堆分散在多段不连续的内存之中
uint8_t ucHeap[];

图 84(image84.png)

Heap_5

同 Heap_4(最优匹配算法、合并相邻空闲块),支持不连续内存

需要通过调用 vPortDefineHeapRegions() 提前定义好堆的位置

例4:通过一个例子演示 heap_5 的用法,看看就好无需掌握~

内存布局图

地址范围区域
0xFFFF FFFF
0x0307 FFFFRAM3 512k 字节
0x0300 0000(16k 高亮) 0x0300 3FFF / 0x0300 0000
0x0200 7FFFRAM2 32k 字节
0x0200 0000(16k 高亮) 0x0200 3FFF / 0x0200 0000
0x0100 7FFFRAM1 32k 字节
0x0100 0000(16k 高亮) 0x0100 7FFF / 0x0100 4000
0x0000 0000

代码示例

const HeapRegion_t xHeapRegions[] =
{
    { 0x10004000, 16 * 1024 }, // 在RAM1内部,16k
    { 0x20000000, 16 * 1024 }, // 在RAM2内部,16k
    { 0x30000000, 16 * 1024 }, // 在RAM3内部,16k
}

int main(void)
{
    // 先调用vPortDefineHeapRegions()定义堆的位置
    vPortDefineHeapRegions( xHeapRegions );

    ...
    pvPortMalloc(...);
    ...
    vPortFree(...);
    ...
}

内存使用情况分析

图 85(image85.png)

STM32 SRAM 内存布局图

图示左侧为 STM32 SRAM 芯片图示,右侧展示 STM32 内部 SRAM 的内存分段布局,从左到右依次为 .data段、.bss段、.heap段、中间未分配的内存区域、以及 .stack段,各段下方用箭头和文字标注其用途。

内存分段布局(从左到右):

段名用途说明
.data段有初始值的全局变量/静态变量
.bss段没有初始值的全局变量/静态变量
.heap段堆 — malloc() / free()
(中间空白区)未分配的内存区域
.stack段栈 — 函数调用的返回地址、局部变量、参数

关键标注:

  • .data段 指向:有初始值的全局变量/静态变量
  • .bss段 指向:没有初始值的全局变量/静态变量
  • .heap段 与 .stack段 之间标注:未分配的内存区域(双向箭头)
  • .heap段 标注:堆 malloc()/free()
  • .stack段 标注:栈 函数调用的返回地址、局部变量、参数

图 86(image86.png)

编译结果

arm-none-eabi-size   5.3\ example1.elf
arm-none-eabi-objdump -h -S 5.3\ example1.elf   > "5.3 example1.list"

   text    data     bss     dec     hex filename
  17044      96    6488   23628    5c4c 5.3 example1.elf

Finished building: default.size.stdout

(红色手写圈注圈出以下字段:text、data、bss、dec、hex)

只读数据 (text)

下载到Flash当中的程序和常量

图 87(image87.png)

This diagram shows a terminal output from ARM cross-compilation size tool, with a red hand-drawn circle highlighting the “bss” column and a red arrow pointing to an explanation of the relationship between .bss, .heap, and .stack segments.

Terminal output:

arm-none-eabi-size 5.3\ example1.elf
arm-none-eabi-objdump -h -S 5.3\ example1.elf > "5.3 example1.list"
   text    data     bss     dec     hex filename
  17044      96    6488   23628    5c4c 5.3 example1.elf
Finished building: default.size.stdout

(Red circle around bss, red arrow pointing down)

Green annotation in box: .bss段+.heap段+.stack段

Green annotation calculation:

.bss段 = 6488 - .heap段 - .stack段
      = 6488 - 512 - 1024
      = 4912

图 88(image88.png)

除了Heap_3和Heap_5比较特殊之外

FreeRTOS的堆(ucHeap[])位于.bss段当中

                    ucHeap[]
                       |
                       v
        ┌─────────┬─────────┬─────────┬─────────┬─────────┐
        │ .data段 │ .bss段  │ .heap段 │         │ .stack段│
        └─────────┴─────────┴─────────┴─────────┴─────────┘
            96        4912      512     41.6k字节    1024

(注:.bss段被红色手绘圈圈标注,ucHeap[] 用红色箭头指向 .bss段)

段名大小
.data段96
.bss段4912
.heap段512
(未命名)41.6k字节
.stack段1024
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐