2、嵌入式系统基础:MCU启动流程、向量表与中断、Flash与RAM分区、链接脚本(.lds)解析
各位同学,咱们今天聊点硬核的。做BootLoader开发,说白了就是跟MCU的“起床气”打交道。你想想看,芯片一上电,它怎么知道自己该干嘛?谁告诉它代码在哪?中断来了它找谁?这些搞不清楚,后面写BootLoader就是空中楼阁。
我个人习惯,每接手一个新平台,第一件事就是把这四个东西摸透:启动流程、向量表、存储分区、链接脚本。今天咱们一个一个来啃。
2.1 MCU启动流程:芯片的“第一口奶”
MCU上电后,它做的第一件事不是跑你的main函数,而是执行一段固化在ROM里的启动代码。这段代码通常叫“复位向量”或“启动引导程序”。
我遇到过不少新手,以为程序从main开始。其实不是的。真正的流程是这样的:
- 硬件复位:电源稳定后,复位引脚释放,PC指针指向复位向量地址。
- 取复位向量:从向量表(通常位于Flash起始地址)取出复位中断服务函数的地址。
- 跳转执行:跳转到该地址,执行启动代码(Startup_xxx.s或SystemInit等)。
- 初始化C环境:清零BSS段、拷贝数据段、初始化堆栈指针。
- 调用main:最后才跳到你写的main函数。
核心要点:BootLoader的本质,就是在这个启动流程中“插一脚”。在跳转到应用程序之前,先执行你的升级逻辑。
嗯,这里要注意:不同架构的MCU,启动细节有差异。比如ARM Cortex-M系列,复位后从0x00000000取栈顶指针,从0x00000004取复位向量。而有些老架构的MCU,直接固定从某个地址开始执行。
2.2 向量表与中断:MCU的“通讯录”
向量表是什么?说白了就是一张“中断服务函数地址列表”。每个中断源对应一个表项,里面存着该中断的处理函数地址。
我刚开始做BootLoader时,犯过一个低级错误:升级完应用程序后,发现所有中断都不响应了。查了半天,原来是向量表偏移没设置对。
避坑指南:我曾经在STM32F4上做IAP升级,忘记设置SCB->VTOR寄存器。结果应用程序的中断向量表还在BootLoader的地址范围,一触发中断就跑飞了。记住:应用程序的向量表必须偏移到它自己的Flash地址。
典型的向量表结构(以ARM Cortex-M为例):
// 向量表定义(汇编或C语言)
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
&_estack, // 0x0000: 栈顶指针
Reset_Handler, // 0x0004: 复位向量
NMI_Handler, // 0x0008: NMI中断
HardFault_Handler, // 0x000C: 硬错误
// ... 其他中断
};
为什么BootLoader要关心这个?因为升级过程中,你可能会修改向量表。比如:
- BootLoader本身占用一段Flash,有自己的向量表。
- 应用程序在另一段Flash,也有自己的向量表。
- 跳转前,必须把中断向量表基地址切换到应用程序的地址。
个人经验:我习惯在BootLoader中保留一个“软中断”入口。这样应用程序可以通过触发软中断,主动请求BootLoader执行升级。相当于给两个程序之间留了个“门铃”。
2.3 Flash与RAM分区:你的地盘我做主
做BootLoader,说白了就是“分地盘”。Flash和RAM就那么大,BootLoader占多少、应用程序占多少、升级缓冲区放哪,都得提前规划好。
我见过最惨的案例:有人把BootLoader和应用程序的Flash分区重叠了,一升级就把自己给覆盖了,直接变砖。
典型的Flash分区方案:
| 区域 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| BootLoader区 | 0x08000000 | 32KB | 启动代码、升级逻辑 |
| 参数存储区 | 0x08008000 | 4KB | 升级标志、版本号 |
| 应用程序区 | 0x08009000 | 448KB | 用户代码 |
| 备份区(可选) | 0x08080000 | 256KB | 升级固件暂存 |
RAM分区同样重要:
- 栈区:存放局部变量、函数调用信息。BootLoader和应用程序共用时要注意栈大小。
- 堆区:动态内存分配。我建议BootLoader里尽量不用malloc,容易出问题。
- 全局变量区:.data和.bss段。启动时由启动代码初始化。
核心原则:BootLoader和应用程序的RAM区域不能重叠。否则应用程序跑起来,会把BootLoader的全局变量踩掉。我一般会在链接脚本里明确划分。
2.4 链接脚本(.lds)解析:代码的“施工图纸”
链接脚本,很多人觉得它神秘。其实它就是一张“施工图纸”,告诉链接器:代码放哪、数据放哪、堆栈设多大。
我刚开始学的时候,看着那些SECTIONS、MEMORY命令一头雾水。后来自己手写了一个BootLoader的链接脚本,才真正搞明白。
一个典型的链接脚本结构:
/* 内存布局定义 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
/* 段定义 */
SECTIONS
{
/* 中断向量表,必须放在Flash起始位置 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* 代码段 */
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
. = ALIGN(4);
} > FLASH
/* 只读数据段 */
.rodata :
{
. = ALIGN(4);
*(.rodata)
*(.rodata*)
. = ALIGN(4);
} > FLASH
/* 初始化数据段(运行时在RAM,初始值在Flash) */
.data : AT (ADDR(.rodata) + SIZEOF(.rodata))
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
_edata = .;
. = ALIGN(4);
} > RAM
/* BSS段(未初始化数据) */
.bss :
{
. = ALIGN(4);
_sbss = .;
*(.bss)
*(.bss*)
*(COMMON)
_ebss = .;
. = ALIGN(4);
} > RAM
/* 堆栈定义 */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE ( _end = . );
PROVIDE ( _heap_end = . + 0x4000 );
. = ALIGN(8);
PROVIDE ( _sstack = . );
. = . + 0x2000;
. = ALIGN(8);
PROVIDE ( _estack = . );
} > RAM
}
这里有几个关键点,我当年踩过坑:
- KEEP关键字:告诉链接器,这个段不能被优化掉。向量表必须加KEEP,否则编译器觉得没人引用它,直接给你删了。
- AT关键字:指定.data段的加载地址(在Flash)和运行地址(在RAM)。启动代码需要把数据从Flash拷贝到RAM。
- PROVIDE关键字:定义符号,供启动代码使用。比如_estack就是栈顶指针。
避坑指南:我曾经在某个项目里,因为链接脚本里忘了给应用程序的向量表做偏移,结果程序死活跑不起来。后来在链接脚本里加了:
/* 应用程序链接脚本 */
FLASH (rx) : ORIGIN = 0x08009000, LENGTH = 448K
然后把向量表基地址也改成0x08009000,问题解决。
嗯,说到这,我想起一个细节。链接脚本里的“.”符号,代表当前地址计数器。你写“. = ALIGN(4)”就是让当前地址对齐到4字节。这个在嵌入式里特别重要,因为很多MCU要求中断向量表必须4字节对齐。
最后,我建议你养成一个习惯:每次修改链接脚本后,都生成.map文件看看。里面会详细列出每个段的起始地址、大小、对齐方式。一眼就能看出有没有问题。
好了,这一章的内容就到这。启动流程、向量表、分区、链接脚本,这四个东西是BootLoader的基石。下一章咱们开始动手写真正的BootLoader代码。