1.__initial_sp 变量
2.Reset_Handler
🧬 第一步:硬件规定的“黄金八字节”
Cortex-M 处理器上电或复位后,其硬件逻辑会执行一个固定的操作:
从地址
0x00000000读取第一个 32 位数据,将其自动加载到主堆栈指针 (MSP)寄存器中。从地址
0x00000004读取第二个 32 位数据,将其自动加载到程序计数器 (PC)寄存器中。
因此,0x00000000和0x00000004这两个地址的内容必须分别是__initial_sp和Reset_Handler的地址。
🏗️ 第二步:启动文件中的“向量表”定义
启动文件(通常是以.s为后缀的汇编文件,如startup_stm32f4xx.s)是确保这一切的第一步。它会定义一个名为__Vectors的数据段,并按照处理器规定的顺序,依次列出所有异常和中断处理函数的入口地址。
其核心片段如下:
assembly
__Vectors DCD __initial_sp ; 向量表第1项: 栈顶地址[reference:12][reference:13] DCD Reset_Handler ; 向量表第2项: 复位向量[reference:14][reference:15] DCD NMI_Handler ; 第3项: NMI DCD HardFault_Handler ; 第4项: 硬错误 ; ... 后续其他中断向量
DCD指令用于分配一个 32 位的存储器单元,并将后面符号的地址放入其中。这样,__initial_sp和Reset_Handler的地址就被按顺序放在了向量表的开头。
第三步:链接器脚本中的“地址”决议
启动文件只是定义了符号,而链接器脚本(Linker Script)则负责最终把这些符号和代码段安排到正确的内存地址。
确定
__initial_sp的值:这个符号通常在链接器脚本中定义,代表栈的起始地址(栈顶)。c
_estack = ORIGIN(RAM) + LENGTH(RAM); /* 栈顶设在 RAM 的最高地址 */ /*[reference:21]*/
然后,在启动文件中通过
IMPORT __initial_sp或EXTERN声明,将其引入并使用。链接器会在链接阶段把这个符号替换为_estack的实际数值。确定
Reset_Handler的地址:Reset_Handler作为函数,其地址由链接器根据代码在 Flash 中的位置自动分配。链接器脚本会通过ENTRY(Reset_Handler)命令,将其指定为程序的入口点。
👑 第四步:FreeRTOS 的介入与“接管”
当芯片复位后,会按照上述顺序执行:
硬件自动加载
__initial_sp到 MSP。硬件自动跳转到
Reset_Handler执行。Reset_Handler完成 C 运行时环境初始化后,调用main()函数。在
main()中,我们调用vTaskStartScheduler()来启动 FreeRTOS 调度器。FreeRTOS 的
vPortStartFirstTask()函数会再次从向量表(NVIC 表)中读取__initial_sp的值,并将其重新写入 MSP 寄存器。这一步是为了确保任务调度开始时,栈指针被正确地重置到向量表定义的位置。
这个过程可以总结为下表:
| 阶段 | 执行主体 | 关键动作 | 涉及文件/组件 |
|---|---|---|---|
| 定义 | 开发人员 | 在启动文件中按顺序列出向量表 | 启动文件 (.s) |
| 决议 | 链接器 | 计算__initial_sp和Reset_Handler的最终地址 | 链接器脚本 (.ld) |
| 加载 | CPU 硬件 | 自动从0x00和0x04加载 MSP 和 PC | Cortex-M 处理器 |
| 执行 | 复位处理函数 | 初始化系统,最终跳转到main() | 启动文件、C 运行时库 |
| 接管 | FreeRTOS | 启动调度器时,再次从向量表加载 MSP | tasks.c,port.c |
所以,FreeRTOS 能确保向量表内容正确,关键在于它依赖并遵循由启动文件和链接器脚本共同建立的、符合硬件规范的工程结构。