第4章 链接脚本与内存布局:分散加载文件解析

做嵌入式开发,说白了就是在跟内存打交道。你写的每一行代码,最终都要落到Flash或者RAM里。而链接脚本,就是告诉编译器「你的代码该放哪儿」的那张地图。

我记得刚入行那会儿,接手一个老项目,Bootloader死活跑不起来。折腾了两天,最后发现是分散加载文件里一个地址写错了。从那以后,我养成了一个习惯——写代码之前,先把内存布局图画清楚。

4.1 分散加载文件(.sct)是什么?

分散加载文件,后缀是.sct,也叫链接脚本。ARM编译器用它来定义程序的存储布局。你可以把它理解成一张「房产证」,标明了每块内存的归属和用途。

一个典型的.sct文件长这样:

LR_IROM1 0x08000000 0x00010000 {    ; 加载区,起始0x08000000,大小64KB
  ER_IROM1 0x08000000 0x00010000 {  ; 执行区,起始0x08000000
   *.o (RESET, +First)              ; 中断向量表,必须放在最前面
   *(InRoot$$Sections)              ; 根区代码
   .ANY (+RO)                       ; 所有只读数据
  }
  RW_IRAM1 0x20000000 0x00004000 {  ; RAM区,起始0x20000000,大小16KB
   .ANY (+RW +ZI)                   ; 读写数据和零初始化数据
  }
}

这里面有几个关键概念:

  • 加载区(Load Region):程序在Flash里的存放位置
  • 执行区(Execution Region):程序运行时在内存里的位置
  • 输入段(Input Section):具体的代码或数据段
我的习惯:每次新建工程,我都会先画一张内存布局草图,标清楚Flash和RAM的起始地址、大小,然后再写.sct文件。这样能避免很多低级错误。

4.2 Flash与RAM分区规划

做Bootloader开发,Flash分区是重中之重。为什么?因为Bootloader和App要共用同一颗芯片的Flash,谁占哪儿,必须提前说好。

我一般这样规划:

分区 起始地址 大小 内容
Bootloader区 0x08000000 32KB Bootloader代码、中断向量表
参数存储区 0x08008000 4KB 升级标志、版本号、校验值
App区 0x08009000 128KB 应用程序代码
备份区 0x08029000 128KB 升级时的备份固件

你想想看,如果Bootloader和App的地址重叠了,会发生什么?轻则程序跑飞,重则芯片变砖。我在量产现场就见过一次,烧录时地址配错了,整批板子都得返工。

注意:分区时一定要留够Bootloader的空间。我建议至少32KB,因为Bootloader要处理通信协议、Flash擦写、校验逻辑,代码量其实不小。

4.3 Bootloader与App的地址空间划分

地址空间划分,核心就一句话:各管各的地,别越界。

Bootloader的链接脚本:

LR_IROM1 0x08000000 0x00008000 {
  ER_IROM1 0x08000000 0x00008000 {
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
  }
  RW_IRAM1 0x20000000 0x00004000 {
   .ANY (+RW +ZI)
  }
}

App的链接脚本:

LR_IROM1 0x08009000 0x00020000 {
  ER_IROM1 0x08009000 0x00020000 {
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
  }
  RW_IRAM1 0x20000000 0x00004000 {
   .ANY (+RW +ZI)
  }
}

看到区别了吗?Bootloader的起始地址是0x08000000,App的起始地址是0x08009000。中间那4KB(0x08008000~0x08008FFF)就是参数存储区。

这里有个坑,我曾经踩过:App的中断向量表偏移。CMS32芯片要求,如果App不在0x08000000启动,就必须在代码里设置中断向量表偏移寄存器(VTOR)。否则,中断来了,CPU会跑到Bootloader的向量表里找入口,结果可想而知。

核心要点:

  • Bootloader和App的Flash地址不能重叠
  • App必须设置VTOR偏移
  • RAM区可以共用,但要注意堆栈冲突
  • 参数存储区要放在Bootloader和App都能访问的位置

4.4 内存布局实战图解

光说理论不够,我画了一张图,把整个内存布局串起来:

CMS32 Bootloader与App内存布局 Flash (0x08000000 ~ 0x0803FFFF) Bootloader区 0x08000000 ~ 0x08007FFF (32KB) 参数存储区 0x08008000 ~ 0x08008FFF (4KB) App区 0x08009000 ~ 0x08028FFF (128KB) 备份区 0x08029000 ~ 0x08048FFF (128KB) RAM (0x20000000 ~ 0x20003FFF) 栈区 (向下增长) 0x20003C00 ~ 0x20003FFF (1KB) 堆区 0x20003800 ~ 0x20003BFF (1KB) 全局变量区 (RW+ZI) 0x20000000 ~ 0x200037FF (14KB) 关键说明: 1. Bootloader启动后,检查参数存储区的升级标志 2. 如果有升级请求,从备份区读取固件,写入App区 3. 升级完成后,跳转到App区执行(需设置VTOR偏移) 4. RAM区由Bootloader和App共用,但堆栈在跳转前需重新初始化

4.5 避坑指南

做量产烧录这么多年,我总结了几条血泪教训:

  • 地址对齐:Flash擦除是按扇区来的,分区起始地址必须是扇区对齐的。CMS32的扇区大小一般是2KB或4KB,具体看型号。
  • 中断向量表:App的VTOR偏移必须和链接脚本里的起始地址一致。我见过有人改了.sct文件,忘了改VTOR,结果中断全乱套。
  • RAM冲突:Bootloader和App如果共用RAM,跳转前一定要把Bootloader用过的全局变量清零,否则App可能读到脏数据。
  • 参数存储:升级标志、版本号这些参数,建议用结构体封装,加上CRC校验。我曾经遇到过Flash写入一半掉电,参数区数据损坏,导致系统反复进入升级模式。
我的建议:量产烧录时,把Bootloader和App的.sct文件都打印出来,贴在工位上。烧录前对照检查一遍,能省很多麻烦。