个人根据韦东山老师的课的一些笔记和个人理解,分享交流

栈:也是一块内存空间 cpu的sp寄存器指向它 可以用在函数调用 局部变量 多任务系统里

栈 (Stack):它像自动传送带。由 CPU 的 SP 寄存器控制,函数一调用,变量自动入栈;函数一结束,变量自动出栈。它不需要你写 free,非常快,但空间通常比较小。

SP 全称是 Stack Pointer,即 栈指针寄存器
它是一个 CPU 内部的专用寄存器,里面存储的是一个内存地址
这个地址指向当前栈空间的“最顶端”
请添加图片描述
在这里插入图片描述
请添加图片描述

a函数去调用b和c函数,其本质就是去调用BL指令

BL指令最终实现两个功能,一个是LR,是返回地址,就是下一条指令的地址
然后PC等于函数a的地址,这里就是去调用函数a,去调用函数b函数c也是同理
在这里插入图片描述

在这里插入图片描述

涉及三个问题,首先就是LR被覆盖了怎么办
第二就是局部变量在栈中如何去分配
为何每个rtos任务都有自己的栈

Q1:首先就是LR被覆盖了怎么办

问题一:以调用函数a为例,在转到函数a的时候,知道需要去修改LR,所以先提前去保存LR,在入口处就去保存LR
划出自己的栈,保存LR进栈里,保存局部变量
请添加图片描述

注意的是这个b和c是a去调用,同时去调用两个,先b搞完后就出栈了,然后就排到c去用
这些R0等就是寄存器

阶段SP 状态栈内存动作
进入 a_funcSP = A - offsetPUSH {LR},保存 main 的返回地址。
调用 b_funcSP 进一步减小划出 b 的栈帧,存 b 的局部变量和 LR
b_func 结束SP 增加(弹回)b 的栈帧被逻辑销毁,SP 回到 a_func
调用 c_funcSP 再次减小复用之前 b 所在的地址空间。
a_func 结束SP 回到 APOP {PC},直接把栈里存的 LR 弹给 PC,跳回 main

Q2:局部变量

请添加图片描述

由这个图可以看出栈是如何给局部变量分配空间的
首先i没有加volatile,它在栈给你分配了空间,然后那些没加volatile的它就优先使用寄存器表示,这就是局部变量在栈的分配

相当于没有优化的这些是有写入内存里的,而且是有给它分配地址的,然后那些没有进内存的,就只在寄存器里

编译器为了追求效率,并不会给所有的局部变量都在栈(内存)里开辟空间:

  • 优先使用寄存器:对于 chuchbuf 这些没有加 volatile 的变量,编译器认为它们是“临时”的。直接用 R5 (ch)R6 (uch)R4 (buf) 这种快速的寄存器来存放,读写速度最快。

  • 被迫进入栈空间:对于加了 volatile 的变量 i,编译器被禁用了优化逻辑。它必须在栈内存中给 i 找个位置(图中 sp = sp - 20 划出的空间之一),每次用到 i 都要通过 STRLDR 真真实实地去读写内存,而不是只在寄存器里转悠。

1. 那些只在寄存器里的变量:没有“固定地址”

正如你观察到的,像 chuch 这种变量,如果编译器觉得寄存器够用,它们可能自始至终都不会进入内存

  • 无地址性:在 C 语言层面你虽然可以定义它们,但在底层,它们没有一个具体的内存地址(比如 0x2000...),它们的名字就是 R5R6

  • 速度极快:CPU 访问寄存器只需要 1 个时钟周期,而访问内存(即便有缓存)通常也要快得多。

  • 风险:如果你尝试对这种变量取地址(比如 &ch),编译器就会“被迫”给它在栈上分配一个空间,因为它必须得有个地址给你。

2. 写入内存的变量:拥有“身份证号”

对于你图中加了 volatilei,或者是因为函数调用需要被 PUSH 的寄存器:

  • 分配地址:它们在栈内存中有了明确的坐标(即 SP + 偏移量)。

  • 可见性:因为有了内存地址,其他的硬件单元(比如 DMA)或者中断函数(通过地址指针)才能找得到它们。

  • 确定性:即便 CPU 现在很忙,这个值也会稳稳地存在内存条/SRAM 里,不会因为寄存器被挪作他用而消失。

. 局部变量与寄存器的“分配”

编译器会计算你函数里有多少个活跃的局部变量:

  • 变量少时:如果你只有 2 个局部变量(如 chuch),编译器可能只选择 PUSH {r4, r5, lr}

  • 变量多时:如果你定义了 10 个局部变量,寄存器 R4-R11 全被占满了,你会看到 PUSH {r4-r11, lr}

  • 寄存器不够用时:如果寄存器全用完了变量还没存完,编译器就会通过 SUB sp, sp, #offset 在栈上划出更多的空间

3. 栈管理的“本质”总结

你可以把栈想象成一个伸缩尺

  • 向下拨动尺子:就是在内存里占位。尺子划过的这段空间,现在属于当前函数,别人不能用。

  • 向上收回尺子:就是宣布这段内存“我用完了”。虽然内存里的数据还在(并没有被抹除),但因为 SP 已经移走了,下次调用别的函数时,SP 再次向下移动,就会直接覆盖掉旧数据。这就是为什么你理解的“自动释放”是如此高效。

4. 你的理解在 RTOS 中的应用

你提到过“多任务系统”,在 FreeRTOS 中,所谓的“切换任务”,本质上就是:

  1. 把当前的 SP 指针保存起来。

  2. 把 CPU 的 SP 寄存器强制改为另一个任务上次保存的地址。 只要 SP 移动了,CPU 面对的就是一套全新的局部变量和返回地址。

Q3:为何每个RTOS任务都要有自己的栈

假设现在有两个任务
rtos本质就一个任务运行一阵子然后去切换到另一个任务,然后不断去重复这个过程
这个过程它在切换时会保存前面任务,然后去恢复要开始的这个任务
保存和恢复的是现场,就是那些寄存器什么的请添加图片描述

可以根据这个生活中看书的例子去辅助理解
请添加图片描述
恢复A的时候,先找到A的结构体,然后得到A的SP,那么这些保存的现场是存在内存里的,现在地址也找到了,直接把这些寄存器的值写入硬件,然后再返回pc的值,那不就可以接着往下运行了,然后那些寄存器的值也都已经恢复

综上所述,每个任务都要有自己的栈,每个任务都有自己的调用关系,自己的局部变量,即便是调用同一个函数也是保存在自己的栈,现场这些也是保存在自己的栈内

RTOS 任务切换与栈管理笔记总结

1. 核心本质:RTOS 是时间的“分时复用”

  • 运行逻辑:RTOS 并不是让多个任务真正同时运行,而是让每个任务运行一小段时间,通过极快的切换频率(Tick),在宏观上产生“并行”的错觉。

  • 切换核心:切换的本质就是现场的保存与恢复。当任务 A 被挂起时,CPU 必须记住它此刻的所有状态,以便下次无缝接轨。

2. 现场 (Context) 到底是什么?

  • 寄存器集合:现场就是 CPU 内部那一套正在变动的数据,包括通用寄存器(R0-R12)、栈指针(SP)、链接寄存器(LR)和程序计数器(PC)。

  • 生活类比:就像看书看到一半去接电话,你必须记住“读到了哪一页(PC)”以及“刚才想到了什么(寄存器数据)”,而书签就是记录这些信息的载体。

3. 为什么必须“每个任务都有自己的栈”?

  • 独立领地:每个任务都有自己的调用关系(栈帧)和局部变量。如果共用一个栈,任务 A 压入的返回地址会被任务 B 的局部变量踩掉,导致程序崩溃。

  • 私有化存储

    • 局部变量:保存在各任务栈中。

    • 现场状态:在切换瞬间,所有的寄存器值都会 PUSH 进入当前任务的私有栈。

    • 栈顶地址 (SP):每个任务的 SP 最终记录在它的任务控制块 (TCB) 中。

4. 切换过程的底层三部曲

  1. 保存:当前任务执行 PUSH,将 R0-R15 的状态全部存入其私有栈内存。

  2. 转场:从当前任务的 TCB 记录下 SP 地址;然后找到下一个任务的 TCB,取出它上次保存的 SP 值并赋给 CPU 的 SP 寄存器。

  3. 恢复:CPU 根据新的 SP 指针执行 POP,将栈内的旧数据还原回硬件寄存器。当 PC 寄存器被弹出时,程序瞬间“闪现”回另一个任务断点处。

Logo

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

更多推荐