2. MULTI工程结构与构建系统

在 Green Hills MULTI 中,一个可编译、可调试、可交付的嵌入式工程并不是“把 .c 文件堆在一起”那么简单。MULTI 通过工程文件(.gpj)把源文件、头文件搜索路径、编译选项、链接脚本、库依赖和输出产物组织成一个可复现的构建单元。理解这套组织方式,是后续调试、优化和量产固件生成的基础。

工程文件与目录组织

MULTI 的工程文件通常以 .gpj 为扩展名,本质是一个 XML 描述文件,记录了工程包含哪些文件、使用哪套编译/链接工具、目标处理器型号以及各类选项。一个典型的工程目录建议按职责分层:

  • src/:存放 .c 源文件,按模块划分子目录,如 drivers/、app/、bsp/。
  • inc/:存放对外公开的头文件,供其他模块 include。
  • ld/:存放链接脚本(.ld)与内存布局描述。
  • out/:构建输出目录,存放 .o、.elf、.map、.hex 等产物。
  • lib/:第三方或预编译库文件(.a)。

在 MULTI 的 Project 视图中,这些目录会以树形结构呈现。工程文件本身不复制源文件,只保存路径引用,因此团队协作时应保证相对路径一致,避免使用绝对路径导致换机后无法构建。

源文件与头文件管理

头文件管理的核心是“搜索路径”与“包含关系”。MULTI 在编译时按以下顺序查找 #include 的头文件:先查当前源文件所在目录,再按工程中配置的 Include Search Path 顺序查找。因此,应把公共头文件目录(如 inc/)加入搜索路径,而不是在源码里写 #include "../../inc/xxx.h" 这类脆弱路径。

头文件自身应使用包含卫士(include guard)防止重复包含:

#ifndef UART_H
#define UART_H

void uart_init(unsigned int baud);
void uart_putc(char c);

#endif /* UART_H */

对于跨模块共享的接口,建议一个模块对应一个头文件,头文件中只放声明,不放定义(除 inline 或宏外),避免多个 .c 文件重复定义符号导致链接冲突。

编译选项配置

MULTI 的编译选项分为“全局选项”和“单文件选项”。全局选项在工程属性中设置,作用于所有源文件;单文件选项可对某个 .c 单独覆盖。常用选项包括:

选项类别典型设置作用
处理器-cpu=arm7tdmi指定目标 CPU 架构
优化-O2 / -Osize速度优先或体积优先
调试信息-g生成 DWARF 调试信息
宏定义-DDEBUG=1条件编译开关
包含路径-I../inc追加头文件搜索目录
警告-Wall开启常用警告

需要特别注意的是,-g 与优化选项并不冲突,但高优化等级(如 -O3)可能重排代码、内联函数,导致单步调试时行号跳转不直观。调试阶段建议使用 -O0 或 -O1 配合 -g,发布阶段再切换到 -O2 或 -Osize。

链接脚本基础

链接脚本决定代码和数据在目标内存中的放置位置。对于没有操作系统或使用简单 RTOS 的嵌入式工程,链接脚本通常要描述 ROM(Flash)和 RAM 的起始地址与长度,并把 .text、.data、.bss、.stack 等段分配到合适区域。一个简化的示例:

MEMORY
{
  ROM (rx) : ORIGIN = 0x00000000, LENGTH = 256K
  RAM (rwx): ORIGIN = 0x20000000, LENGTH = 64K
}

SECTIONS
{
  .text : { *(.text*) } > ROM
  .data : { *(.data*) } > RAM AT> ROM
  .bss  : { *(.bss*)  } > RAM
}

其中 AT> ROM 表示 .data 段的初始化值存放在 ROM 中,运行时由启动代码搬运到 RAM。若链接脚本写错,最常见的现象是链接报错 “section .text will not fit in region ROM” 或程序跑飞。

生成可执行文件与映射文件

MULTI 构建的最终产物通常是 ELF 格式的可执行文件(.elf),它包含代码、数据、符号表和调试信息,可直接用于 MULTI 的调试器加载。若需要烧录到 Flash,还需通过 elftohex 或 IDE 内置转换生成 Intel HEX 或二进制 .bin。

映射文件(.map)是链接器生成的重要文本报告,记录了每个段的大小、起始地址、各目标文件贡献的符号及其地址。排查“RAM 不够用”“某个函数被放到错误地址”等问题时,.map 文件是第一手依据。在 MULTI 中可通过链接选项 -map 指定输出文件名,例如:

gbuild -map out/firmware.map -o out/firmware.elf ...

构建完成后,应检查 .map 中 ROM/RAM 的实际占用是否留有余量,并确认入口符号(如 _start 或 Reset_Handler)地址与链接脚本一致。

MULTI 工程构建数据流与产物关系 工程文件 .gpj 文件列表 / 选项 / 目标 源文件与头文件 src/*.c inc/*.h(搜索路径) 编译选项 -cpu / -O / -g / -D / -I 全局 + 单文件覆盖 编译器 ccarm / gcc 前端 目标文件 .o 含符号表 / 重定位信息 链接脚本 .ld MEMORY / SECTIONS 链接器 地址分配 / 符号解析 可执行文件 .elf 调试 / 加载 映射文件 .map 段大小 / 符号地址 .hex .bin .gpj 引用源文件与选项 .ld 决定段在 ROM/RAM 的落点
建议在工程中固定一套“调试配置”和一套“发布配置”,分别保存不同的优化等级与宏定义,避免手工反复修改选项造成构建结果不一致。

综上,MULTI 工程结构的关键在于:用 .gpj 统一管理文件与选项,用搜索路径和包含卫士管理头文件,用编译选项控制代码生成质量,用链接脚本决定内存布局,最终通过 .elf 和 .map 验证构建结果是否符合预期。掌握这条链路,后续的调试与优化才有稳定的起点。