跳转至

第 4 章:嵌入式软件开发 —— 从代码到"烧录"

场景: 你在电脑上写好了智能家居中控的代码。但电脑是 x86 架构,中控是 ARM 架构——电脑上的程序不能直接在 ARM 芯片上运行。你需要"交叉编译"——在 x86 上编译出 ARM 能执行的程序,然后"烧录"到芯片里。


4.1 交叉编译

核心比喻:交叉编译就是"用中文写菜单,翻译成英文给外国厨师"

你(开发者)用中文写菜单(C 代码),但厨师(ARM 芯片)只懂英文(ARM 机器码)。你需要一个翻译(交叉编译器)把中文菜单翻译成英文。

这个翻译过程在你的电脑(x86)上完成,但翻译结果给 ARM 芯片用——这就是"交叉编译"。

交叉编译工具链

源代码(.c/.h)
┌──────────────┐
│   编译器(gcc)  │ → 汇编文件(.s)
└──────┬───────┘
┌──────────────┐
│   汇编器(as)   │ → 目标文件(.o)
└──────┬───────┘
┌──────────────┐
│   链接器(ld)   │ → 可执行文件(.elf/.bin/.hex)
└──────┬───────┘
┌──────────────┐
│  烧录/下载     │ → 写入芯片 Flash
└──────────────┘
工具 作用 输入 输出
编译器(arm-none-eabi-gcc) C/汇编 → 目标代码 .c .s .o
汇编器(arm-none-eabi-as) 汇编 → 机器码 .s .o
链接器(arm-none-eabi-ld) 链接目标文件 + 库 .o .a .elf
格式转换(objcopy) ELF → HEX/BIN .elf .hex .bin
调试器(GDB + JTAG/SWD) 下载和调试 .elf 芯片

4.2 启动代码与 Bootloader

核心比喻:启动代码就是中控主机的"开机自检"

你按下中控主机的电源键后,它不会立刻进入工作状态。它先做"开机自检":

  1. 检查内存是否正常
  2. 初始化时钟系统
  3. 把程序从 Flash 搬到 RAM
  4. 设置堆栈指针
  5. 跳转到 main() 函数

这个过程由启动代码(Startup Code)完成。

启动流程

上电/复位
┌──────────────┐
│ 1. 复位向量    │ ← CPU 从固定地址读取第一条指令
└──────┬───────┘
┌──────────────┐
│ 2. 初始化堆栈  │ ← 设置 MSP(主堆栈指针)
└──────┬───────┘
┌──────────────┐
│ 3. 初始化时钟  │ ← 配置系统时钟(PLL、分频)
└──────┬───────┘
┌──────────────┐
│ 4. 复制数据段  │ ← 把 .data 从 Flash 复制到 RAM
└──────┬───────┘
┌──────────────┐
│ 5. 清零 BSS 段 │ ← 初始化未初始化的全局变量为 0
└──────┬───────┘
┌──────────────┐
│ 6. 调用 main()│ ← 进入用户程序
└──────────────┘

Bootloader

功能 说明
初始化硬件 时钟、内存、串口等基本外设
加载应用程序 从 Flash/SD 卡/网络加载主程序
固件更新 支持通过串口/USB/网络升级固件
恢复模式 主程序损坏时进入恢复模式
引导选择 选择启动哪个应用程序

4.3 内存映射

嵌入式系统的内存布局(链接脚本 .ld 文件定义):

高地址  ┌──────────────┐
        │    栈(Stack)  │ ← 向下增长
        │              │
        ├──────────────┤
        │    堆(Heap)   │ ← 向上增长(动态内存分配)
        ├──────────────┤
        │   .bss 段     │ ← 未初始化全局变量(在 RAM 中)
        ├──────────────┤
        │   .data 段    │ ← 已初始化全局变量(Flash→RAM)
        ├──────────────┤
        │   .rodata 段  │ ← 只读数据(常量字符串等)
        ├──────────────┤
        │   .text 段    │ ← 程序代码(在 Flash 中)
低地址  └──────────────┘
存储位置 内容
.text Flash 程序代码(机器指令)
.rodata Flash 只读数据(const 变量、字符串常量)
.data Flash→RAM 已初始化的全局/静态变量
.bss RAM 未初始化的全局/静态变量(启动时清零)
Heap RAM 动态分配内存(malloc)
Stack RAM 函数调用栈、局部变量

4.4 常见开发工具

工具类型 代表产品 用途
IDE Keil MDK、IAR EWARM、STM32CubeIDE 集成开发环境
编译器 ARM GCC、ARMCC、IAR ICCARM 交叉编译
调试器 J-Link、ST-Link、DAP-Link 下载和调试
调试接口 JTAG、SWD 芯片调试接口
RTOS FreeRTOS、μC/OS、RT-Thread 实时操作系统
HAL 库、LL 库、CMSIS 硬件抽象层

4.5 常见考试题型

例题 1: 交叉编译是指( )。

A. 在 ARM 上编译 x86 程序 B. 在一种平台上编译出另一种平台的可执行程序 C. 同时编译多个源文件 D. 使用多种编译器编译同一程序

查看答案

答案:B

交叉编译是在一种平台(如 x86 PC)上使用交叉编译器生成另一种平台(如 ARM)可执行程序的过程。嵌入式开发几乎都使用交叉编译。

例题 2: 嵌入式系统启动时,.bss 段的处理是( )。

A. 从 Flash 复制到 RAM  B. 清零  C. 保持不变  D. 从 RAM 复制到 Flash

查看答案

答案:B

.bss 段存储未初始化的全局变量和静态变量。C 标准规定这些变量初始值为 0,所以启动代码会将 .bss 段清零。.data 段(已初始化变量)才需要从 Flash 复制到 RAM。

例题 3: Bootloader 的主要功能不包括( )。

A. 初始化硬件  B. 加载应用程序  C. 管理文件系统  D. 固件更新

查看答案

答案:C

Bootloader 主要负责硬件初始化、加载和启动应用程序、支持固件更新。文件系统管理是操作系统的功能,不属于 Bootloader 的核心职责。


要点总结

  • 交叉编译:在 PC(x86)上编译出嵌入式芯片(ARM)可执行的程序
  • 启动流程:复位→初始化堆栈→初始化时钟→复制 .data→清零 .bss→调用 main()
  • 内存布局:.text(Flash)、.data(Flash→RAM)、.bss(RAM)、堆、栈
  • Bootloader:初始化硬件、加载程序、固件更新
  • 常见工具:Keil/IAR IDE、J-Link/ST-Link 调试器、JTAG/SWD 接口

课后练习

  1. 流程描述 :描述嵌入式系统从上电复位到执行 main() 函数的完整启动流程。

  2. 段分析 :以下变量分别存储在哪个段中?

    const int a = 10;        // ?
    int b = 20;              // ?
    int c;                   // ?
    char *p = malloc(100);   // p 在 ? 段,*p 指向 ? 区域
    

  3. 真题演练 :嵌入式系统开发中,将 ELF 格式文件转换为 HEX 文件使用的工具是(  )。


下一章预告: 程序写好了,但芯片怎么和外部世界交互?GPIO 控制 LED、UART 串口通信、SPI/I2C 连接传感器——第 5 章见。

继续第 5 章:接口与通信技术 →