1. ARM 架构

1.1 RISC 精简指令集特点

ARM 属于 RISC(精简指令集计算机),核心思想:指令简单、规整、数量少

RISC 关键特征

  1. 对内存只有「读 / 写」指令
    • 只有 Load(读)、Store(写)能访问内存
    • 其他运算不能直接操作内存
  2. 所有数据运算都在 CPU 内部寄存器中完成
  3. 指令格式统一、长度固定,CPU 设计更简单

1.2 RISC 执行乘法 a = a * b 的过程

a = a * b 为例,RISC 必须分 4 步

  1. Load:从内存读取变量 a → 存入寄存器
  2. Load:从内存读取变量 b → 存入寄存器
  3. 运算:在 CPU 内部做乘法运算
  4. Store:把运算结果写回内存 a

对比理解(帮助记忆)

  • CISC(x86 那种):可以一条指令直接 内存a = 内存a * 内存b
  • RISC(ARM):必须 先读寄存器 → 运算 → 再写回

这就是 RISC 著名的:Load / Store 架构

CPU内部用寄存器来存数据

寄存器(Register) —— CPU 内部超高速的临时存储单元,是 ARM 运算的「核心操作载体」。

1.3 ARM CPU 内部寄存器

无论 Cortex-M3/M4(微控制器)还是 Cortex-A7(应用处理器),ARM 内核都标配通用寄存器 + 专用寄存器:

1. 通用寄存器

R0、R1、R2、……、R12:用于临时存放运算数据(如上述 a、b、a*b),可自由读写。

2. 专用寄存器(R13/R14/R15)

有固定功能,不可随意用作普通运算:

寄存器 别名 核心作用
R13 SP (Stack Pointer) 栈指针,指向当前栈顶地址
R14 LR (Link Register) 链接寄存器,保存函数调用后的返回地址
R15 PC (Program Counter) 程序计数器,指向当前执行的指令地址(修改即跳转)

后面还有一个PSR(程序状态寄存器)

1.4 ARM 核心汇编指令(必记)

ARM 汇编指令格式:指令 目标寄存器, 源寄存器1, 源寄存器2/立即数

指令类型 示例 解释
读内存:Load LDR R0, [R1, #4] 读取地址 R1+4 的 4 字节数据,存入 R0
写内存:Store STR R0, [R1, #4]

把 R0 的 4 字节数据,写入地址 R1+4

指令 格式 解释(以 LDRB 为例)
LDRB LDRB R0, [R1, #1] 读内存地址R1+1处的 1 字节数据,存入 R0(R0 高位 24 位补 0)
STRB STRB R0, [R1, #1] 把 R0 的低 8 位数据,写入内存地址R1+1处(仅操作 1 字节,不影响其他位)
适用场景 char 类型、8 位寄存器、外设 8 位寄存器读写
指令 格式 解释(以 LDRH 为例)
LDRH LDRH R0, [R1, #2] 读内存地址R1+2处的 2 字节数据,存入 R0(R0 高位 16 位补 0)
STRH STRH R0, [R1, #2] 把 R0 的低 16 位数据,写入内存地址R1+2
适用场景 short 类型、16 位外设寄存器(如 ADC、UART)
指令 格式 解释
LDRD LDRD R0, R1, [SP, #4] 从栈地址SP+4处读 8 字节数据:前 4 字节→R0,后 4 字节→R1(批量读两个 int)
STRD STRD R0, R1, [SP, #4] 把 R0(前 4 字节)、R1(后 4 字节)写入栈地址SP+4
适用场景 64 位数据、一次性传两个参数(如函数的 a/b 参数)
加法 ADD R0, R1, R2 R0 = R1 + R2
加法(立即数) ADD R0, R0, #1 R0 = R0 + 1(自增)
减法 SUB R0, R1, R2 R0 = R1 - R2
比较 CMP R0, R1 比较 R0 和 R1,结果存入程序状态寄存器(PSR)
跳转:Branch B main 直接跳转到 main 函数处执行
带返回跳转:Branch and Link BL main 先把当前返回地址存入 LR,再跳转到 main

1.5ARM 数据传输三大要素

源、目的、长度

1. 源(Source):数据从哪来

  • 内存某个地址
  • 寄存器
  • 立即数(常数)

2. 目的(Destination):数据到哪去

  • 寄存器
  • 内存某个地址

ARM 规则:运算只能在寄存器里做,内存不能直接运算。所以数据传输一定是:内存 ↔ 寄存器

3. 长度(Length):传多少字节

ARM 最常见三种长度:

  • B(Byte):1 字节
  • H(Halfword):2 字节
  • 无后缀(Word):4 字节(int、指针)

1.6 add 函数反汇编(看到栈操作)

反汇编看栈操作的核心意义

  1. 验证栈的生长规则:直观看到 SP(栈指针)「减小分配空间、增大释放空间」的向下生长特性;
  2. 理解 volatile 的影响:如果去掉volatile,编译器可能优化掉STR r0,[sp,#0](sum 不写回栈),反汇编能直接看到这个差异;
  3. 掌握 ARM 函数调用规范:看到参数 / 返回地址如何通过栈保护、局部变量如何通过栈分配,这是嵌入式开发的核心基础;
  4. 排查栈相关问题:比如栈溢出、参数传递错误等 bug,反汇编是定位这类问题的关键。
int add(volatile int a, volatile int b)
{
	volatile int sum;
    sum = a + b;
    return sum;
}
  • ARM 函数调用规则:函数参数ab默认通过R0R1传入;返回值通过R0返回。
  • 栈(SP)特性:栈是「向下生长」的(SP 减小 = 分配空间,SP 增大 = 释放空间),PUSH自动减 SP,POP自动加 SP。
  • volatile 作用:禁止编译器优化,变量必须读写内存(而非仅寄存器),所以反汇编会有明确的 Load/Store 操作。
地址 机器码 汇编指令 对应 C 代码 深度解析(内存 / 寄存器变化)
0x08002f34 b503 PUSH {r0,r1,lr} 函数入口 核心作用:保护现场(防止后续操作覆盖关键数据)1. 把R0(a)R1(b)LR(返回地址)依次压入栈;2. SP 自动减 3×4=12(3 个寄存器,每个 4 字节);3. 为什么保护?后续会重写 R0/R1,必须先保存原始参数和返回地址。
0x08002f36 b081 SUB sp,sp,#4 定义 sum 核心作用:为局部变量sum分配栈空间1. SP = SP - 4(栈指针下移 4 字节);2. 这 4 字节就是sum的内存地址(栈地址SP+0);3. 此时栈布局:SP+0=sum,SP+4= 之前 PUSH 的 r0/r1/lr。
0x08002f38 e9dd0101 LDRD r0,r1,[sp,#4] 读取 a、b 核心作用:从栈中恢复参数 a、b 到寄存器(Load 操作)1. LDRD= 双字传输(8 字节),从栈地址SP+4读 8 字节;2. 前 4 字节→R0(恢复参数 a),后 4 字节→R1(恢复参数 b);3. 为什么从SP+4读?因为SP+0是 sum 的空间,SP+4才是之前 PUSH 的 r0/r1。
0x08002f3c 4408 ADD r0,r0,r1 sum = a + b 核心作用:执行加法运算(寄存器内操作)1. R0 = R0 (a) + R1 (b)(运算结果存在 R0);2. 符合 RISC 规则:所有运算仅在寄存器执行,无内存直接运算。
0x08002f3e 9000 STR r0,[sp,#0] sum = a + b 核心作用:把运算结果写入 sum(Store 操作)1. 把 R0 中的加法结果,写入栈地址SP+0(即 sum 的内存地址);2. 因为 sum 是volatile,必须写回内存,不能只存在寄存器。
0x08002f40 bd0e POP {r1-r3,pc} return sum 核心作用:释放栈 + 返回函数1. 从栈顶弹出数据,依次写入 R1、R2、R3、PC;2. 关键:把之前保存在 LR 的返回地址写入 PC(CPU 跳回调用 add 的地方);3. SP 自动加 4×4=16(释放所有栈空间);4. 返回值:加法结果已在 R0,符合 ARM 函数返回规则(R0 传返回值)。

1.6.1实例操作:

添加程序代码Add测试代码(上述)进入工程,修改cnt

编译程序;为例方便复制,制作反汇编的指令如下

fromelf  --text  -a -c  --output=xxx.dis  xxx.axf

配置工程-User-After Bulid/ReBuild-Run#1-User Command-输入反汇编指令

XXX.axf改为目录地址方便后续找到(Linker里面找到地址)

点击编译即可生成test.dis

打开dis文件查找add函数进行分析即可

堆和栈

堆:一块内存空间,可以从中分配出一个小buffer,用完再把它放回去

定义一个数组,进行内存的分配和释放就是堆

小例子:

这段代码实现了一个极简的内存分配器(简易版 malloc),它基于一个预先定义的静态字符数组 heap_buf 作为「内存池」,通过维护一个全局位置指针 pos 来分配内存,本质是模拟堆内存的线性分配(也叫「栈式分配」/「线性分配」)。

// 1. 定义一个1024字节的静态字符数组作为内存池(模拟堆空间)
char heap_buf[1024];
// 2. 全局变量pos,记录内存池的下一个可分配位置,初始为0(表示从数组起始位置开始分配)
int pos=0;

// 3. 自定义内存分配函数,参数size是要分配的字节数,返回分配内存的起始地址
void *my_malloc(int size)
{
    // 4. 保存当前的pos值(分配前的位置,即本次分配的起始地址)
    int old_pos=pos;
    // 5. 将pos向后移动size字节,为下一次分配预留空间
    pos+=size;
    
    // 6. 返回本次分配的内存起始地址(heap_buf数组中old_pos位置的地址)
    return &heap_buf[old_pos];
}

在这个buf来分配一个小块的内存,下标从0-1023;开始时pos=0,想要分配size大小的空间如何分配?pos=pos+size;返回首地址

在空闲的内存上实现malloc函数,这块空闲的内存就被称为堆(malloc需要配合free函数使用,对于简单的malloc函数无法实现free函数)

主函数代码

	char ch=65;
	int i;
	char *buf=my_malloc(100);
	unsigned char uch=200;
	for (i=0;i<26;i++)
	buf[i]='A'+i;
  • char ch=65
    • ch栈内存中的 char 类型变量,65 是 ASCII 码值,等价于 ch='A'
    • 注意:char 类型通常是 1 字节,有符号(范围 - 128~127),但 65 在范围内,无溢出。
    • 栈内存特点:系统自动管理,main 函数执行完后销毁。
  • int i
    • i 是栈内存中的 int 类型变量,仅定义未初始化(值为随机垃圾值),后续循环会赋值。
  • char *buf=my_malloc(100)
    • 核心步骤:调用自定义 my_malloc(100) 分配内存:
    • 调用 my_malloc(100),函数内先保存当前 pos=0old_pos
    • pos += 100pos 从 0 变为 100(无对齐、无边界检查);
    • 返回 &heap_buf[0](内存池起始地址),栈中的指针变量 buf 存储这个地址;
    • 内存关系:buf,指向的 100 字节空间在全局数组 heap_buf(模拟堆) 中。
  • unsigned char uch=200
    • uch 是栈内存中的无符号 char 变量,1 字节,200 在 unsigned char 范围(0~255)内,正常存储。

buf 是指针(仅存地址),size 字节在内存池里;buf 存在栈里,它指向内存池起始地址

实际的堆管理

  • heap_buf 是 1024 字节的内存池(模拟堆)
  • 第一次调用 my_malloc(100) → 分配 0~99 字节,buf 指向起始地址 &heap_buf[0]pos 变为 100
  • 第二次调用 my_malloc(50) → 分配 100~149 字节,buf2 指向 &heap_buf[100]pos 变为 150
  • 第三次调用 my_malloc(100) → 分配 150~249 字节,buf3 指向 &heap_buf[150]pos 变为 250

问题是:这种线性分配无法释放单个块,用完就只能重置整个内存池。要解决「分配 + 释放 + 复用」,就需要用链表来管理空闲块和已分配块。

我们把内存池拆成一个个「内存块」,用双向 / 单向链表把这些块串起来:

  • 每个块包含:块头(元数据)+ 有效数据区
    • 块头里存:当前块大小、是否空闲、下一个块的指针
  • 两种链表:
    1. 空闲链表:只串起所有「空闲可用」的块
    2. 已分配链表:串起所有「正在使用」的块(也可以只在块头里用标志位区分)

分配流程(my_malloc 用链表实现)

  1. 遍历空闲链表,找到第一个足够大的空闲块(首次适配 / 最佳适配)
  2. 如果块大小刚好等于需求:直接标记为「已分配」
  3. 如果块更大:拆分成「已分配块 + 新空闲块」,并更新链表
  4. 返回「数据区起始地址」(块头地址 + 块头大小)

释放流程(my_free 用链表实现)

  1. 根据指针找到对应的块头
  2. 标记块为「空闲」
  3. 合并相邻空闲块(避免内存碎片):
    • 向前合并:如果前一个块是空闲,就和当前块合并
    • 向后合并:如果后一个块是空闲,就和当前块合并

核心是:块头存管理信息,链表串起所有块,分配找空闲,释放后合并碎片

栈:也是一块内存空间,CPU的SP寄存器指向它,它可以用于函数调用、局部变量、多任务系统里保存现场

char heap_buf[1024];
int pos=0;
int g_cnt=0;

void *my_malloc(int size)
{
	int old_pos=pos;
	pos+=size;
	
	return &heap_buf[old_pos];
}
	
int b_func(volatile int a)
{
	a+=2;
	return a;
}
int c_func(volatile int a)
{
	a+=3;
	return a;
}

void a_func(volatile int a)
{
	g_cnt=b_func(a);
	g_cnt=c_func(g_cnt);
}
	
int main(void)
{
 
	char ch=65;
	volatile int i=99;
	char *buf=my_malloc(100);
	unsigned char uch=200;
	for (i=0;i<26;i++)
	buf[i]='A'+i;
	
	a_func(i);
}

步骤 1:定义局部变量(栈内存)

变量 类型 存储区域 初始值 说明
ch char 65('A') 1 字节,ASCII 65 对应字符 'A'
i volatile int 99 4 字节,volatile禁止编译器优化
buf char* 指向 heap_buf [0] 指针变量,存储 my_malloc 返回的地址
uch unsigned char 200 1 字节,无符号 char,值在 0~255 范围内
步骤 1:调用 a_func(i),参数传递
  • main 中的 i=99volatile int),作为参数传入 a_func
  • 因为是值传递a_func 内部会创建一个 int 类型的临时变量 a,并把 99 赋值给这个临时变量;
  • 注意:avolatile 修饰的,所以编译器不会把 a 缓存到寄存器,读写 a 都直接操作内存(但单线程下这个修饰仅为示例,无实质影响)。
步骤 2:执行第一行 g_cnt = b_func(a)
  • 先调用 b_func(a):把 a_func 中的临时变量 a=99 传入 b_func
  • b_func 内部逻辑:a += 299+2=101,然后返回 101
  • b_func 的返回值 101 赋值给全局变量 g_cnt → 此时 g_cnt=101
步骤 3:执行第二行 g_cnt = c_func(g_cnt)
  • 先调用 c_func(g_cnt):把当前的 g_cnt=101 传入 c_func
  • c_func 内部逻辑:a += 3101+3=104,然后返回 104
  • c_func 的返回值 104 重新赋值给 g_cnt → 此时 g_cnt=104(覆盖之前的 101)。
步骤 4:a_func 执行结束
  • a_funcvoid 类型,无返回值;
  • 唯一的副作用是修改了全局变量 g_cnt,使其最终值为 104
  • 函数执行完后,a_func 内部的临时变量 a(值为 99)会被销毁(栈帧回收)

为了看清楚栈的具体操作,采用反汇编指令

   i.a_func
    a_func
        0x08000734:    b501        ..      PUSH     {r0,lr}
        0x08000736:    9800        ..      LDR      r0,[sp,#0]
        0x08000738:    f000f808    ....    BL       b_func ; 0x800074c
        0x0800073c:    4902        .I      LDR      r1,[pc,#8] ; [0x8000748] = 0x20000000
        0x0800073e:    6048        H`      STR      r0,[r1,#4]
        0x08000740:    f000f809    ....    BL       c_func ; 0x8000756
        0x08000744:    6048        H`      STR      r0,[r1,#4]
        0x08000746:    bd08        ..      POP      {r3,pc}
    $d
        0x08000748:    20000000    ...     DCD    536870912
    $t
    i.b_func
    b_func
        0x0800074c:    b501        ..      PUSH     {r0,lr}
        0x0800074e:    9800        ..      LDR      r0,[sp,#0]
        0x08000750:    1c80        ..      ADDS     r0,r0,#2
        0x08000752:    9000        ..      STR      r0,[sp,#0]
        0x08000754:    bd08        ..      POP      {r3,pc}
    i.c_func
    c_func
        0x08000756:    b501        ..      PUSH     {r0,lr}
        0x08000758:    9800        ..      LDR      r0,[sp,#0]
        0x0800075a:    1cc0        ..      ADDS     r0,r0,#3
        0x0800075c:    9000        ..      STR      r0,[sp,#0]
        0x0800075e:    bd08        ..      POP      {r3,pc}
    i.main
    main
        0x08000760:    b508        ..      PUSH     {r3,lr}
        0x08000762:    2063        c       MOVS     r0,#0x63
        0x08000764:    9000        ..      STR      r0,[sp,#0]
        0x08000766:    2064        d       MOVS     r0,#0x64
        0x08000768:    f000f812    ....    BL       my_malloc ; 0x8000790
        0x0800076c:    9800        ..      LDR      r0,[sp,#0]
        0x0800076e:    f7ffffe1    ....    BL       a_func ; 0x8000734   

//  LR=0x08000772  返回地址,执行完a后返回下一条指令(回家地址)

//  PC=a_func     0x08000734(跳转地址)
        0x08000772:    2000        .       MOVS     r0,#0
        0x08000774:    bd08        ..      POP      {r3,pc}

如果函数嵌套调用(比如 a_func 调用 b_func,b_func 又调用其他函数),LR 的值确实会被新的 BL 指令覆盖—— 但会通过「栈」来保护 LR 的值,避免返回地址丢失。

在C入口,保存LR进栈

LR 的值会被新的 BL 指令覆盖,但因为每次调用函数前,都会把 LR(和其他寄存器)压入栈保存,所以不会丢失;函数执行完后,再从栈里恢复 LR 的值,就能正确返回。

代码调用链:main → a_func → b_func,我们跟踪每一步 LR 的变化:

步骤 1:main 调用 a_func(BL a_func)

main: 0x0800076e: f7ffffe1 BL a_func ; 0x8000734

  • 动作 1:计算下一条指令地址 = 0x08000772(BL 的下一句);
  • 动作 2:把 0x08000772 存入 LR;
  • 动作 3:PC 跳转到 a_func 入口 0x08000734
  • 此时:LR = 0x08000772(main 的返回地址)。

步骤 2:a_func 开头先保存 LR 到栈(关键!)

a_func: 0x08000734: b501 PUSH {r0,lr} ; 把r0(参数99)和LR(0x08000772)压入栈

栈里现在的内容(栈是向下生长的,sp 是栈指针)

栈地址(sp + 偏移) 内容 说明
sp+0 r0=99 a_func 的参数
sp+4 LR=0x08000772 main 的返回地址

此时:LR 的值被保存到栈,就算后续被覆盖也没关系。

步骤 3:a_func 调用 b_func(BL b_func)

0x08000738: f000f808 BL b_func ; 0x800074c

  • 动作 1:计算下一条指令地址 = 0x0800073C
  • 动作 2:把 0x0800073C 存入 LR(覆盖了原来的 0x08000772);
  • 动作 3:PC 跳转到 b_func 入口 0x0800074c
  • 此时:LR = 0x0800073C(a_func 的返回地址)。

步骤 4:b_func 开头保存新的 LR 到栈

b_func: 0x0800074c: b501 PUSH {r0,lr} ; 把r0=99、LR=0x0800073C压入栈

栈里新增内容:

栈地址(sp + 偏移) 内容 说明
sp+0 r0=99 b_func 的参数
sp+4 LR=0x0800073C a_func 的返回地址

步骤 5:b_func 执行完,恢复 LR 并返回

0x08000754: bd08 POP {r3,pc}

  • 动作 1:从栈里弹出值到 PC(弹出的是 LR=0x0800073C);
  • 动作 2:PC 跳回 a_func 的 0x0800073C
  • 此时:b_func 执行完毕,回到 a_func 继续执行。

步骤 6:a_func 执行完,恢复最初的 LR

0x08000746: bd08 POP {r3,pc}

  • 动作 1:从栈里弹出最初保存的 LR=0x08000772 到 PC;
  • 动作 2:PC 跳回 main 的 0x08000772
  • 此时:a_func 执行完毕,回到 main 继续执行。
调用阶段 LR 值 栈里保存的 LR 是否被覆盖 结果
main→a_func 前 - -
main→a_func 后 0x08000772 未覆盖 LR 存 main 返回地址
a_func 保存 LR 后 0x08000772 0x08000772 - 栈保护 LR
a_func→b_func 后 0x0800073C 0x08000772(栈里) 已覆盖 新 LR 存 a_func 返回地址
b_func 返回后 0x0800073C 0x08000772(栈里) - 回到 a_func
a_func 返回后 0x08000772 - 回到 main
  • LR 会被覆盖:每次执行 BL 指令,都会把新的返回地址写入 LR,覆盖旧值;
  • 栈是解决方案:编译器会在函数开头自动插入PUSH {..., lr},把 LR 压栈保存;
  • 核心保障:函数执行完通过POP {..., pc}从栈恢复 LR 到 PC,实现正确返回;

局部变量在栈中分配,如何分配

int main(void) {
    char ch=65;          // 1字节
    volatile int i=99;   // 4字节
    char *buf=my_malloc(100);  // 4字节(指针,32位系统)
    unsigned char uch=200;     // 1字节
    // ... 函数逻辑
}

  i.main
    main
        0x08000760:    b508        ..      PUSH     {r3,lr}
        0x08000762:    2063        c       MOVS     r0,#0x63
        0x08000764:    9000        ..      STR      r0,[sp,#0]
        0x08000766:    2064        d       MOVS     r0,#0x64
        0x08000768:    f000f812    ....    BL       my_malloc ; 0x8000790
        0x0800076c:    9800        ..      LDR      r0,[sp,#0]
        0x0800076e:    f7ffffe1    ....    BL       a_func ; 0x8000734   

//  LR=0x08000772  返回地址,执行完a后返回下一条指令(回家地址)

//  PC=a_func     0x08000734(跳转地址)
        0x08000772:    2000        .       MOVS     r0,#0
        0x08000774:    bd08        ..      POP      {r3,pc}

加上volatile的局部变量必须写进栈中,不能只存在寄存器中;当寄存器写满时,也会用栈来分配

为什么每个Rtos任务都有自己的栈

无 RTOS 时,只有 1 个栈(主线程栈)

在裸机程序中,整个程序只有一个栈(通常是 MSP,主栈指针):

  • 所有函数调用、局部变量、寄存器保存都在这一个栈里;
  • 程序执行是 “串行的”:一个函数执行完,栈帧销毁,下一个函数再用这个栈;
  • 不存在 “切换”,所以一个栈足够用。

RTOS 的核心需求:任务切换,必须隔离运行状态

RTOS 的核心是多任务抢占式调度 —— 比如:

  1. 任务 A 正在执行a_func,局部变量i=99存在栈里,寄存器r0=99lr=0x08000772
  2. 调度器触发(比如定时器中断),暂停任务 A,切换到任务 B 执行;
  3. 任务 B 执行自己的函数,局部变量j=100、寄存器r0=100
  4. 稍后调度器切回任务 A,要求任务 A 能 “无缝继续执行”(就像没被暂停过)。

如果所有任务共用一个栈,会发生什么?

  • 任务 B 的局部变量会覆盖任务 A 的栈数据(比如任务 A 的i=99被改成j=100);
  • 任务 B 的函数调用会覆盖lrpc等寄存器值,切回任务 A 时,返回地址丢失,程序直接跑飞。

👉 结论 1:每个任务必须有独立栈,用来存储「专属的局部变量、函数调用栈帧、寄存器状态」,切换时互不干扰。

1. 存储任务的局部变量和函数栈帧

每个任务执行自己的函数时,局部变量(如ibuf)、函数调用的PUSH/POP操作,都在自己的栈里完成:

  • 任务 A 的i=99存在任务 A 的栈,任务 B 的j=100存在任务 B 的栈;
  • 即使两个任务调用同一个函数(比如a_func),各自的栈帧也是独立的,参数 / 返回值互不影响。
2. 保存任务切换时的 “上下文”

RTOS 切换任务的本质是「保存当前任务上下文 → 恢复目标任务上下文」,而上下文(寄存器、PC、LR 等)会被保存到任务自己的栈里;

  • 如果没有独立栈,保存 / 恢复上下文时会覆盖其他任务的数据,切换后任务无法正常执行。
3. 避免栈溢出影响其他任务

每个任务的栈大小是独立配置的(比如 FreeRTOS 中xTaskCreateusStackDepth参数):

  • 即使任务 A 发生栈溢出,也只会破坏自己的栈空间,不会影响任务 B/C 的运行;
  • 如果共用栈,一个任务栈溢出会直接导致整个系统崩溃。

FreeRtos源码概述

FreeRTOS 目录结构

使用STM32CubeMX 创建的FreeRTOS工程中,FreeRTOS相关的源码如下:

主要涉及2个目录:

Core

 Inc目录下的FreeRTOSConfig.h是配置文件

 Src目录下的freertos.c是STM32CubeMX创建的默认任务

Middlewares\Third_Party\FreeRTOS\Source

 根目录下是核心文件,这些文件是通用的

 portable 目录下是移植时需要实现的文件

          目录名为:[compiler]/[architecture]  比如:RVDS/ARM_CM3,这表示cortexM3架构在RVDS工具上的移植文件

核心文件

FreeRTOS的最核心文件只有2个:

 FreeRTOS/Source/tasks.c  FreeRTOS/Source/list.c 其他文件的作用也一起列表如下:

移植时涉及的文件

移植FreeRTOS时涉及的文件放在

FreeRTOS/Source/portable/[compiler]/[architecture]目录下, 比如:RVDS/ARM_CM3,这表示cortexM3架构在RVDS或Keil工具上的移植文件。 里面有2个文件:

 port.c  portmacro.h

头文件相关

头文件目录

FreeRTOS需要3个头文件目录:

 FreeRTOS本身的头文件: Middlewares\Third_Party\FreeRTOS\Source\include

 移植时用到的头文件: Middlewares\Third_Party\FreeRTOS\Source\portable\[compiler]\[architecture]

 含有配置文件FreeRTOSConfig.h的目录:Core\Inc

头文件

内存管理

文件在Middlewares\Third_Party\FreeRTOS\Source\portable\MemMang 下,它也是放在“portable”目录下,表示你可以提供自己的函数。 源码中默认提供了5个文件,对应内存管理的5种方法。 (堆)

如何去使用freertos中的堆

入口函数

在Core\Src\main.c 的 main 函数里,初始化了FreeRTOS环境、创建了任务,然后启动调度器。源码如下:

/* Init scheduler */
osKernelInitialize(); /* 初始化FreeRTOS运行环境 */
MX_FREERTOS_Init();
/* 创建任务 */
/* Start scheduler */
osKernelStart();
/* 启动调度器 */

数据类型和编程规范

数据类型

每个移植的版本都含有自己的portmacro.h头文件,里面定义了2个数据类型:

TickType_t:

 FreeRTOS配置了一个周期性的时钟中断:TickInterrupt

 每发生一次中断,中断次数累加,这被称为tickcount

 tickcount这个变量的类型就是TickType_t

 TickType_t 可以是16位的,也可以是32位的

 FreeRTOSConfig.h 中定义configUSE_16_BIT_TICKS 时,TickType_t 就是 uint16_t  否则TickType_t就是uint32_t

 对于32位架构,建议把TickType_t配置为uint32_t

BaseType_t:

 这是该架构最高效的数据类型

 32位架构中,它就是uint32_t  16位架构中,它就是uint16_t  8位架构中,它就是uint8_t 

BaseType_t 通常用作简单的返回值的类型,还有逻辑值,比如pdTRUE/pdFALSE

变量名

变量名有前缀

函数名

函数名的前缀有2部分:返回值类型、在哪个文件定义。

宏的名

宏的名字是大小,可以添加小写的前缀。前缀是用来表示:宏在哪个文件中定义。

内存管理

  • FreeRTOS 内核对象的管理方式:task、queue、信号量、事件组等内核对象通常采用动态分配(用时分配、不用释放),能简化程序设计,无需提前规划对象、简化 API 设计,还可减少内存占用;
  • 标准 C 内存管理函数(malloc/free)不适用 FreeRTOS 的原因:适配性差(嵌入式资源紧缺、代码体积大)、线程不安全、执行时间不确定、易产生内存碎片、编译器配置复杂且调试困难;
  • 堆与栈的核心区别:堆是需管理函数(malloc/free)的空闲内存块,用于动态分配空间;栈用于存储函数局部变量和程序运行环境,也可从堆中分配空间作为栈使用,二者并非同一概念。

FreeRTOS中内存管理的接口函数为:pvPortMalloc、vPortFree,对应于C库的malloc、 free。

文件在FreeRTOS/Source/portable/MemMang下,它也是放在portable目录下,表示你可以提供自己的函数。 源码中默认提供了5个文件,对应内存管理的5种方法。 

常使用heap_4,多个内存使用heap_5;

heap相关函数

pvPortMalloc/vPortFree

函数原型:

void * pvPortMalloc( size_t xWantedSize );

void vPortFree( void * pv );

作用:分配内存、释放内存。 如果分配内存不成功,则返回值为NULL。

xPortGetFreeHeapSize

函数原型:

size_t xPortGetFreeHeapSize( void );

当前还有多少空闲内存,这函数可以用来优化内存的使用情况。比如当所有内核对象都分配好后,执行此函数返回2000,那么configTOTAL_HEAP_SIZE就可减小2000。 注意:在heap_3中无法使用。

xPortGetMinimumEverFreeHeapSize

函数原型: size_t xPortGetMinimumEverFreeHeapSize( void );

返回:程序运行过程中,空闲内存容量的最小值。

注意:只有heap_4、heap_5支持此函数。

malloc 失败的钩子函数

在pvPortMalloc 函数内部:

void * pvPortMalloc( size_t xWantedSize )vPortDefineHeapRegions

{ ...... #if ( configUSE_MALLOC_FAILED_HOOK == 1 )

         {

                if( pvReturn == NULL )

                 { extern void vApplicationMallocFailedHook( void );

                  vApplicationMallocFailedHook(); }

          } #endif

return pvReturn;

}

所以,如果想使用这个钩子函数:

  1.  在FreeRTOSConfig.h 中,把configUSE_MALLOC_FAILED_HOOK定义为1
  2.  提供vApplicationMallocFailedHook 函数 
  3. pvPortMalloc 失败时,才会调用此函数
Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐