4、链接脚本与内存布局:链接脚本(.ld文件)编写、代码段/数据段/BSS段分配、Bootloader与App分区规划。
说到链接脚本,很多做MCU开发的朋友可能觉得它很神秘。其实说白了,它就是告诉编译器:你的代码该放哪儿,数据该放哪儿,堆栈又该放哪儿。我刚开始做Bootloader那会儿,也在这上面栽过跟头。有一次芯片死活跑不起来,查了两天才发现是分区地址写错了。嗯,从那以后,我对链接脚本就格外上心了。
4.1 链接脚本到底在干什么?
你想想看,我们写C代码的时候,函数、全局变量、局部变量,它们最终都要变成二进制数据存到Flash里。但问题是,这些数据不能乱放啊。代码段(.text)要放在Flash里,初始化了的全局变量(.data)既要放在Flash里(初始值),又要搬到RAM里(运行时),没初始化的全局变量(.bss)直接占RAM空间就行。
链接脚本就是干这个分配活的。它告诉链接器:
- Flash从哪个地址开始?多大?
- RAM从哪个地址开始?多大?
- 代码段放Flash的哪个位置?
- 数据段怎么从Flash搬到RAM?
- 堆栈放在RAM的哪个角落?
我个人习惯,拿到一个新芯片,第一件事就是看它的内存映射手册,然后自己写链接脚本。别看厂家提供的模板能用,但真要搞Bootloader分区,还是自己写的心里有底。
4.2 一个典型的链接脚本长什么样?
以STM32F103为例,我给大家拆解一个实际项目里用的链接脚本。这个芯片Flash是512KB,RAM是64KB。
/* 入口点 */
ENTRY(Reset_Handler)
/* 内存区域定义 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
}
/* 段定义 */
SECTIONS
{
/* 中断向量表,必须放在Flash起始位置 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* 代码段 */
.text :
{
. = ALIGN(4);
*(.text)
*(.text*)
*(.glue_7)
*(.glue_7t)
*(.eh_frame)
. = ALIGN(4);
_etext = .;
} > FLASH
/* 只读数据段 */
.rodata :
{
. = ALIGN(4);
*(.rodata)
*(.rodata*)
. = ALIGN(4);
} > FLASH
/* 初始化数据段(运行时在RAM,初始值在Flash) */
_sidata = .;
.data : AT(_sidata)
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .;
} > RAM
/* BSS段(未初始化数据) */
.bss :
{
. = ALIGN(4);
_sbss = .;
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} > RAM
/* 堆 */
._heap :
{
. = ALIGN(8);
__heap_start = .;
. = . + _Min_Heap_Size;
__heap_end = .;
} > RAM
/* 栈 */
._stack :
{
. = ALIGN(8);
__stack_start = .;
. = . + _Min_Stack_Size;
__stack_end = .;
} > RAM
}
这里有几个关键点我要强调一下:
- 中断向量表必须放在Flash的起始位置,这是芯片复位后第一个去的地方。
- _sidata这个符号标记了.data段初始值在Flash里的位置,启动代码要用它来把数据搬到RAM。
- _sdata和_edata标记了.data段在RAM里的起止位置。
- _sbss和_ebss标记了.bss段在RAM里的起止位置,启动代码要把这段内存清零。
4.3 Bootloader与App的分区规划
这才是咱们课程的重头戏。Bootloader和App要共存,就得把Flash分成两个区。我见过很多新手直接把App放在Bootloader后面,结果Bootloader升级了一次,App地址就乱了。
我的分区原则是这样的:
| 分区 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| Bootloader区 | 0x08000000 | 32KB | Bootloader代码、中断向量表 |
| 参数区 | 0x08008000 | 4KB | 升级标志、版本号、校验值 |
| App区 | 0x08009000 | 476KB | 应用程序代码、中断向量表 |
为什么Bootloader只给32KB?因为Bootloader的功能相对固定,不需要太大。我做过最小的Bootloader只有8KB,就实现了串口升级和CRC校验。参数区4KB用来存一些配置信息,比如升级标志、当前版本号、固件校验值等。
4.4 App的链接脚本怎么写?
App的链接脚本和Bootloader的差不多,但有几个关键地方要改:
/* App的链接脚本片段 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08009000, LENGTH = 476K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS
{
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* 其他段定义和Bootloader类似,但起始地址变了 */
/* ... */
}
看到没?Flash的起始地址改成了0x08009000,长度也变成了476KB。这样链接器就会把App的代码从0x08009000开始放。
但光改链接脚本还不够。在App的启动代码里,还要加上这么一句:
/* 设置中断向量表偏移 */
SCB->VTOR = 0x08009000;
这句话告诉CPU:中断向量表不在0x08000000了,挪到0x08009000去了。如果不设这个,CPU一发生中断,还是会跑到Bootloader的向量表里去。
4.5 避坑指南
我在项目中遇到过几个典型问题,分享给大家:
- 分区大小没对齐: 有些芯片要求分区起始地址必须是某个大小的整数倍(比如1KB或4KB)。我遇到过一个人,他把App区放在0x08008001,结果芯片死活不认。后来查手册才发现,地址必须4字节对齐。
- RAM分区冲突: Bootloader和App如果共用RAM,一定要规划好。我习惯把RAM分成两部分,Bootloader只用低地址部分,App用高地址部分。这样跳转时不用清RAM,省事。
- 忘了更新链接脚本: 有一次我改了Flash分区大小,但忘了更新App的链接脚本。结果App编译出来还是按老地址放的,一运行就覆盖了Bootloader。嗯,从那以后我每次改分区都要检查两个链接脚本。
核心要点:
- 链接脚本就是内存布局的蓝图,必须精确到每个字节
- Bootloader和App的分区要留够余量,别卡得太死
- App的中断向量表偏移是跳转成功的关键,忘了设就等着跑飞吧
- 每次改分区,两个链接脚本都要同步更新
好了,链接脚本和内存布局这块就讲这么多。下一章咱们聊聊启动代码的具体实现,看看Bootloader是怎么把控制权交给App的。说实话,那部分才是真正考验功底的地方。