《基于飞腾派的嵌入式系统开发》

《基于飞腾派的嵌入式系统开发》

View project on GitHub

第四章嵌入式系统开发

4.1 概述:嵌入式裸机软件开发流程

本节作为飞腾派嵌入式开发教材的起点,旨在为读者提供进入裸机(Bare-Metal)编程的清晰路径。飞腾派作为一款基于ARMv8的高性能嵌入式处理器,广泛应用于高性能计算、嵌入式系统和服务器领域,其裸机开发具有独特的挑战与价值。本节将详细阐述裸机开发的基本概念,区分裸机程序与基于操作系统的程序,并系统介绍从代码编写到程序在飞腾派硬件上独立运行的完整开发流程。

4.1.1 嵌入式软件系统的分类与裸机定义

嵌入式软件根据其运行环境和功能,可以从宏观上分为两大类:运行在开发平台上的软件(宿主机工具)和运行在目标硬件上的软件(目标机程序)。理解这两类软件的区别是掌握裸机开发的前提。

1. 软件系统的分类

运行在开发平台上的软件(宿主机工具)

宿主机工具是指运行在开发者的电脑(通常为x86架构的Linux或Windows系统)上的软件,用于辅助飞腾派嵌入式系统的开发、编译和调试。这些工具不直接运行在飞腾派目标硬件上,但对开发流程至关重要。具体包括:

  • 文本编辑器与集成开发环境(IDE):开发者使用文本编辑器(如VSCodeVim)或IDE(如Keil MDKIAR Embedded Workbench for ARM)来编写代码。这些工具提供语法高亮、代码补全和项目管理功能,提升开发效率。

PNG

PNG

PNG

  • 交叉编译器:飞腾派采用ARMv8-A架构,因此需要使用针对该架构的交叉编译器,如aarch64-none-elf-gcc(GNU工具链的一部分)。交叉编译器在宿主机上运行,生成可在飞腾派硬件上执行的机器代码。

PNG

  • 汇编器与链接器:汇编器(如GNU的as)将汇编代码转换为目标代码,链接器(如GNU的ld)将多个目标文件链接为可执行文件,并根据链接脚本(.ld文件)指定内存布局。

PNG

  • 调试工具:包括软件调试器(如GDB)和硬件调试器(如J-LinkOpenOCD),用于分析程序运行状态、设置断点和检查寄存器/内存内容。
  • 硬件烧录工具:用于将编译好的二进制文件通过JTAG、串口(UART)或以太网(TFTP)传输到飞腾派硬件的非易失性存储器(如Flash或eMMC)中。

运行在嵌入式系统上的软件(目标机程序)

目标机程序是最终在飞腾派硬件上运行的代码,根据是否依赖操作系统,可进一步分为两类:

  • 带操作系统(OS)的程序:这类程序运行在嵌入式操作系统(如Linux、FreeRTOS等)之上,依赖操作系统的服务(如任务调度、内存管理和设备驱动)来实现功能。典型应用包括运行在Linux上的嵌入式应用程序或RTOS驱动程序。
  • 裸机程序(Bare-Metal):本章的重点,裸机程序直接运行在飞腾派硬件上,不依赖任何操作系统,是嵌入式开发中最底层的编程形式。

2. 裸机程序(Bare-Metal)的定义

裸机程序是指不依赖任何操作系统,直接在飞腾派处理器上运行的软件。它从处理器上电复位后的复位向量地址(通常为0x00xFFFF0000)开始执行,开发者对处理器和片上外设(如GPIO、定时器、串口等)拥有完全的、无限制的控制权。

核心特征

  • 直接硬件控制:裸机程序直接与飞腾派处理器的寄存器、内存和外设交互,无需操作系统的抽象层。例如,开发者可以直接配置通用中断控制器(GIC)或串口寄存器以实现特定功能。
  • 无操作系统开销:由于没有操作系统,裸机程序避免了任务切换、内存分配等开销,运行效率极高,适合对性能要求苛刻的场景。
  • 自定义初始化:开发者需要手动完成所有初始化工作,包括设置堆栈指针(SP)、配置异常向量表、初始化内存管理单元(MMU,若启用)等。
  • 高度定制化:裸机程序与硬件紧密耦合,代码针对飞腾派的具体硬件特性编写,难以直接移植到其他平台。

开发难度

裸机开发的复杂性主要体现在以下几个方面:

  • 硬件知识要求高:开发者需要深入理解飞腾派处理器的架构、中断机制、外设寄存器和内存映射,依赖飞腾派芯片手册提供的技术细节。
  • 手动资源管理:所有硬件资源(如内存、堆栈、中断)都需开发者手动配置,稍有错误可能导致系统崩溃或不可预期的行为。
  • 调试复杂:由于缺乏操作系统的调试支持,开发者需依赖硬件调试工具(如JTAG调试器)或串口输出来定位问题,调试过程较为复杂。

适用场景

裸机程序在以下场景中具有重要应用价值:

  • 系统启动的第一阶段(Boot Loader Stage 1):裸机代码通常用于初始化硬件并加载后续的引导程序或操作系统。
  • 高实时性系统:如汽车电子控制单元(ECU)或工业控制系统,要求极低的延迟和确定性响应。
  • 资源受限系统:在内存或计算能力有限的设备中,操作系统可能过于臃肿,裸机程序是更优选择。
  • 芯片功能验证:在硬件开发初期,裸机程序用于验证飞腾派芯片的外设功能(如UART通信、定时器触发等)。

4.1.2 裸机程序生成流程及文件格式

裸机程序的生成过程是嵌入式开发中至关重要的环节。由于裸机程序直接运行在硬件上,不依赖任何操作系统,开发者需要精确控制程序在内存中的物理位置加载顺序,以确保程序能够正确加载并执行。本节将详细阐述裸机程序从源代码到最终二进制映像的生成流程,分析每个阶段的技术细节,并介绍裸机开发中涉及的关键文件格式(ELF和BIN)。内容将结合飞腾派平台的实践特点,确保全面、专业且适合学生和嵌入式工程师的学习需求。

1. 裸机程序编译链接过程

裸机程序的生成过程是一个多阶段、严格控制的流程,从源代码到最终可执行映像,涉及编译、汇编、链接和格式转换等步骤。以下是生成流程的完整路径:

\[\text{C/汇编源文件} \xrightarrow{\text{交叉编译/汇编}} \text{目标文件}(_.o) \xrightarrow{\text{链接器 (LD, 配合链接脚本)}} \text{可执行文件}(\text{ELF}) \xrightarrow{\text{格式转换工具}} \text{二进制映像文件}(_.bin)\]

1.1 编译和汇编

任务描述

  • C源代码文件(*.c:C语言用于实现裸机程序的主要逻辑,如外设初始化、数据处理和控制流程。C代码通过交叉编译器(如aarch64-none-elf-gcc)编译为目标文件(*.o)。
  • 汇编源代码文件(*.s:汇编代码通常用于实现低级初始化任务,如设置复位向量、堆栈指针和异常向量表。通过交叉汇编器(如aarch64-none-elf-as)将汇编代码转换为目标文件(*.o)。
  • 交叉性:飞腾派FTC核心基于ARMv8-A架构,因此必须使用支持AArch64指令集的交叉工具链,确保生成的机器码能够在飞腾派硬件上正确执行。工具链需要配置为无操作系统环境(None-ELF),以避免引入操作系统相关的元数据。

1.2 链接与定位

任务描述

  • 链接器(LD):使用aarch64-none-elf-ld将多个目标文件(*.o)和必要的库文件(如启动代码或硬件抽象层)组合为一个可执行文件(*.elf)。
  • 链接脚本(Linker Script):链接脚本是裸机开发的核心文件,负责指定程序各段(如.text.data.bss)在飞腾派内存中的物理地址。它确保启动代码位于处理器的复位向量地址(通常为0x0或芯片手册指定的其他地址),并为数据段分配合适的RAM地址。
  • 输出:生成ELF格式的可执行文件*.elf),包含程序代码、数据、符号表和调试信息。

1.3 格式转换

任务描述

  • 目的:飞腾派硬件(或其BootROM)只能识别和执行原始二进制数据流,无法处理ELF文件中包含的复杂元数据(如段表、符号表和调试信息)。
  • 工具:使用aarch64-none-elf-objcopy(GNU Binutils的一部分)将ELF文件剥离元数据,生成纯粹的二进制映像文件*.bin)。
  • 输出*.bin文件仅包含程序的指令和数据,适合烧录到非易失性存储器(如SPI Flash或eMMC)或加载到RAM执行。

飞腾派实践

  • 转换命令

    aarch64-none-elf-objcopy -O binary program.elf program.bin
    
  • 验证:使用hexdumpxxd检查*.bin文件内容,确保其包含正确的机器指令和数据。
  • 用途:生成的*.bin文件可通过JTAG、串口或烧录工具写入飞腾派的存储器,用于调试或独立运行。

注意事项

  • 确保转换后的*.bin文件大小不超过目标存储器的容量。
  • 保留*.elf文件以支持后续调试,避免仅依赖*.bin文件导致调试信息丢失。

2. 裸机开发中的文件格式

在裸机开发中,涉及两种关键文件格式:ELF(可执行与可链接格式)和BIN(二进制映像文件)。这两种格式在开发流程中用途不同,但相辅相成,共同支持裸机程序的生成、调试和运行。

文件格式 英文全称 主要用途 核心特点
ELF Executable and Linkable Format 用于调试和定位信息 包含程序代码、数据、符号表(用于函数和变量名映射)、段头表(描述各段位置)等元数据。支持源码级调试,是JTAG调试器的主要输入文件。
BIN Binary Image File 用于固化和目标机运行 纯粹的二进制数据流,仅包含程序的指令和数据,无元数据。可直接由飞腾派BootROM加载或通过烧录工具写入非易失性存储器。

ELF文件详解

  • 结构:ELF文件包含多个部分:
    • 头部(Header):描述文件格式、目标架构(如AArch64)和入口点。
    • 段表(Section Table):定义代码(.text)、数据(.data.bss)等段的属性和地址。
    • 符号表(Symbol Table):记录函数和变量的名称及地址,便于调试。
    • 调试信息:支持源码级调试(如行号和变量映射)。
  • 用途
    • 调试:ELF文件是aarch64-none-elf-gdb和JTAG调试器的核心输入,用于设置断点、检查寄存器和跟踪源码。
    • 定位:ELF文件记录了程序各段的内存地址,确保链接器正确分配物理地址。
  • 飞腾派实践
    • 使用aarch64-none-elf-readelf分析ELF文件结构:

      aarch64-none-elf-readelf -h program.elf  # 查看头部信息
      aarch64-none-elf-readelf -S program.elf  # 查看段表
      
    • 示例:调试时加载ELF文件以支持源码级跟踪:

      aarch64-none-elf-gdb program.elf
      (gdb) break main
      (gdb) target remote :1234
      

BIN文件详解

  • 结构:BIN文件是程序的内存镜像,仅包含连续的指令和数据流,无任何元数据。文件内容直接对应飞腾派内存中的布局。
  • 用途
    • 烧录:BIN文件可通过JTAG、OpenOCD或厂商烧录工具写入飞腾派的SPI Flash或eMMC。
    • 加载:飞腾派BootROM可直接从非易失性存储器加载BIN文件到RAM执行。
  • 飞腾派实践
    • 烧录BIN文件到SPI Flash:

      openocd -f interface/jlink.cfg -f target/phytium.cfg -c "program program.bin verify reset"
      
    • 验证BIN文件内容与链接脚本一致,确保代码和数据位于正确的内存地址。

飞腾派裸机实践

  • 开发流程
    1. 生成*.elf文件,用于调试阶段。通过GDB和JTAG调试器(如J-Link或OpenOCD)加载*.elf文件,实现源码级调试。
    2. 转换生成*.bin文件,用于烧录和独立运行。确保BIN文件与飞腾派存储器的地址范围和容量匹配。
  • 示例工作流
    • 编译和链接生成program.elf

      aarch64-none-elf-gcc -c -o main.o main.c
      aarch64-none-elf-as -o start.o start.S
      aarch64-none-elf-ld -T phytium.ld -o program.elf main.o start.o
      
    • 转换为program.bin

      aarch64-none-elf-objcopy -O binary program.elf program.bin
      
    • 调试program.elf

      aarch64-none-elf-gdb program.elf
      (gdb) target remote :1234
      (gdb) load
      (gdb) continue
      
    • 烧录program.bin到SPI Flash:

      openocd -f interface/jlink.cfg -f target/phytium.cfg -c "program program.bin verify reset"
      

注意事项

  • ELF文件在调试阶段不可或缺,需妥善保存。
  • BIN文件生成后需验证其大小和内容,确保与飞腾派存储器兼容。
  • 调试和烧录时,参考飞腾派芯片手册确认存储器地址和启动模式。

3. 裸机开发的关键组件

为支持裸机程序生成流程,以下关键组件需特别注意:

  1. 复位向量与启动代码
    • 飞腾派FTC核心上电后从复位向量地址(如0x0)开始执行,启动代码必须位于该地址。示例已在上一节展示。
    • 确保启动代码初始化堆栈指针、时钟系统和关键外设(如UART用于调试输出)。
  2. 链接脚本
    • 链接脚本必须精确匹配飞腾派的内存布局。例如,ROM地址用于存储代码,RAM地址用于堆栈和数据。
    • 示例中,堆栈顶地址(_stack_top)定义在RAM末尾,确保堆栈向下增长时不覆盖数据。
  3. 硬件初始化
    • 初始化飞腾派的时钟、内存控制器和外设(如UART、GPIO、GIC)。
  4. 异常向量表
    • 配置异常向量表以处理中断和异常,确保程序能够响应硬件事件(如定时器中断)。

4. 裸机开发的挑战与注意事项

  • 内存布局精确性:飞腾派硬件的内存映射复杂,链接脚本错误可能导致程序无法加载或运行。
  • 工具链兼容性:确保交叉工具链支持AArch64架构,且配置为裸机环境(无标准库)。
  • 调试信息保留:调试阶段需使用ELF文件,切勿仅依赖BIN文件。
  • 存储器容量限制:飞腾派的SPI Flash或eMMC容量有限,需优化代码和数据大小。

4.1.3 嵌入式裸机软件开发三步骤

无论裸机程序的代码量大小,其开发流程都遵循一套固定的步骤,确保程序能够正确编译、加载并在飞腾派的FTC核心上执行。这套流程分为三个核心阶段:生成调试固化运行

PNG

1. 生成(Generation):从源码到映像

生成阶段是在性能强大的宿主机(Host PC,通常为x86架构的Linux或Windows系统)上完成的代码生产过程,强调交叉性(针对目标硬件架构的编译)和精确性(内存分配和代码布局的精准控制)。其目标是将开发者编写的C语言和汇编语言代码转化为飞腾派FTC核心(ARMv8-A架构)可识别和执行的二进制映像文件。

任务核心

  • 代码编写
    • 裸机程序通常结合C语言ARM汇编语言编写。C语言用于实现复杂逻辑,代码结构清晰且易于维护;汇编语言用于低级初始化任务,如设置复位向量、堆栈指针(SP)和异常向量表。
  • 交叉编译
    • 使用交叉工具链(如aarch64-none-elf-gcc)将C和汇编代码编译为机器指令。飞腾派的FTC核心基于ARMv8-A架构,因此工具链必须支持AArch64指令集,并针对无操作系统环境(None-ELF)生成代码。
    • 编译过程包括以下步骤:
      1. 预处理:处理宏定义和头文件包含(gcc -E)。
      2. 编译:将C代码转换为汇编代码(gcc -S)。
      3. 汇编:将汇编代码转换为目标代码(as)。
      4. 链接:使用链接器(ld)和链接脚本(.ld文件)将多个目标文件链接为可执行文件。
  • 链接脚本
    • 链接脚本(如phytium.ld)用于精确控制代码和数据的内存分配,指定各段(如.text.data.bss)的起始地址和存储位置,确保与飞腾派硬件的内存映射一致。
  • 生成映像文件
    • 编译和链接后生成ELF文件*.elf),包含符号表和调试信息,适合调试使用。
    • 使用objcopy将ELF文件转换为纯粹的二进制映像文件*.bin),用于烧录和执行:

2. 调试(Debugging):控制与验证

调试阶段是在飞腾派目标机(Target Board)上进行的程序行为验证和错误排查过程。由于裸机环境没有操作系统提供调试支持,开发者需依赖专业的硬件调试工具直接接管CPU控制权,实现非侵入式调试。这是裸机开发中技术含量最高的一环,涉及硬件与软件的深度交互。

PNG

任务核心

  • 程序下载:将二进制映像(通常为*.bin*.elf)下载到飞腾派的目标内存(通常为RAM)中,用于调试运行。
  • 运行控制:通过调试器暂停、恢复或单步执行程序,观察运行状态。
  • 状态检查:实时查看和修改飞腾派FTC核心的内部寄存器(如通用寄存器、CPSR/SPSR状态寄存器)和内存内容。
  • 断点设置:设置硬件断点,程序运行到指定地址时自动暂停。
  • 源码级调试:基于ELF文件中的符号信息,实现C语言或汇编语言级别的跟踪调试。

调试方法

  • 指令集模拟器 指令集模拟器是用来在一台计算机上模拟另一台计算机上目标程序运行过程的软件工具,也成为软仿真器。内部有一个反映目标处理器硬件的数据结构。它以时序状态机的方式工作,可以根据目标机指令集定义执行目标指令。 应用场合:被调试的程序模块不需要在实际开发板上执行,对模块代码先调试,以加快调试速度(不需要开发板) ARMulator指令集模拟器(集成在ADS1.2)。
  • ROM Monitor方式 ROM Monitor配合调试器完成被调试程序的下载、目标机内存和寄存器的读/写、设置断点以及单步执行被调试程序等功能。 优点:简单方便、成本低廉,支持高级的调试功能,可扩展性强,基本不需要专门的调试硬件支持,几乎所有的交叉调试器(ADS、GDB)都支持这种方式。 缺点: ROM Monitor要占用目标机一定的资源;应用程序的最终运行环境和调试环境有一定的差异;不便于调试有时间特性的程序,实时性较差。

PNG

  • 实时在线仿真器ICE方式 ICE(In Circuit Emulator)是一种用于替代目标机上的CPU设备,主要应用在硬件的实时开发调试中。 ICE上的CPU是一种特殊的CPU,它可以执行目标机CPU的指令,能够将内部的信号输出到被控制的目标机。(CPU) ICE上的内存也可以被映射到用户的程序空间,这样即使在目标机不存在的情况下,也可以进行代码的调试。(MMU)

PNG

  • JTAG调试方式 使用JTAG的调试装置对处理器进行底层的调试。JTAG可以实现的功能有:读取/修改地址空间任意值;设置断点位置;通过内置于CPU内的ATP接口实现CPU寄存器读取/修改,控制CPU运行/停止;下载程序写入Flash芯片等。 优点:不占用目标机的资源,调试环境和最终的程序运行环境基本一致,支持硬件断点,跟踪、精确计量程序的执行时间,具有时序分析等。 缺点:调试的实时性不如ICE方式强,不支持非干扰调试查询。

PNG

JTAG调试方法

  • 硬件调试器:使用JTAG(Joint Test Action Group)或SWD(Serial Wire Debug)接口连接处理器。常见工具包括:
    • J-Link:Segger公司的高性能调试器,支持ARMv8-A架构。
    • OpenOCD:开源调试工具,支持多种调试接口和处理器。

PNG

实现方式

  • 物理连接: JTAG接口一端与PC机相连(传统采用并口,目前通常为USB接口),另一端是面向用户的JTAG测试接口,通过本身具有的边界扫描功能便可以对芯片进行测试。

PNG

JTAG链的组成:每一条JTAG链是由若干个JTAG的扫描单元串连组成的,每一个扫描单元都可以配置成捕获外部信号的输入单元或者对外的输出单元。依靠移位寄存器,通过JTAG的TDO和TDI信号线,可以使数据串行输出到每一个JTAG扫描单元上,或者读出每一个扫描单元的数据。

PNG

基于JTAG的调试系统内部状态跳转机制

PNG

**工作过程**:

将被测芯片内部所有的引脚通过边界扫描单元(BSC)串接起来,从JTAG的数据输入信号TDI引入,在数据输出信号TDO上引出。在进入调试状态,调试指令和数据从TDI进入,沿着测试链通过测试单元送到芯片的各个引脚和测试寄存器中,通过不同的测试指令来完成不同的测试功能。 JTAG调试不占用系统资源,能够调试没有外部总线的芯片。JTAG不能提供处理器实时运行时的信息。它是通过串行方式依次传递数据的,所以传送信息速度比较慢。

  • 程序下载
    • 通过JTAG/SWD接口将程序映像下载到飞腾派的RAM中。
    • 或者通过以太网(TFTP)或串口(UART)传输映像到目标板。
  • 调试流程
    1. 启动调试器,连接到飞腾派硬件:
    2. 设置断点,例如在main函数入口:
    3. 运行程序并观察状态:
  • 硬件信号监控
    • 使用逻辑分析仪或示波器监控飞腾派外设信号(如GPIO电平、UART通信波形),排查硬件相关问题。
    • 示例:验证UART输出是否符合预期波特率和数据格式。

3. 固化运行(Flashing and Running):独立执行

固化运行阶段是将验证通过的程序映像烧录到飞腾派的非易失性存储器中,确保程序在脱离开发环境后能够独立运行。这是裸机开发的最终目标,代表了产品从开发到实际应用的转化。

任务核心

  • 程序烧录:将二进制映像(*.bin)写入飞腾派的非易失性存储器,如SPI FlasheMMCNAND Flash
  • 启动配置:设置飞腾派的启动模式(Boot Mode),通过硬件引脚(Boot Pin)或寄存器选择启动源(如从SPI Flash启动)。
  • 独立运行:飞腾派FTC核心的BootROM会自动加载存储器中的程序到RAM,执行初始化并进入主逻辑,无需外部工具干预。

实现方式

  • 烧录程序
    • 使用烧录工具(如J-Flash或OpenOCD)将映像写入非易失性存储器。示例OpenOCD命令:

      openocd -f interface/jlink.cfg -f target/phytium.cfg -c "program program.bin verify reset"
      
    • 或者通过飞腾派开发板提供的烧录软件(如厂商提供的Flash工具)完成烧录。

  • 启动模式配置
    • 参考飞腾派芯片手册,设置启动引脚或寄存器以选择从SPI Flash、eMMC或其他存储器启动。
    • 示例:将启动引脚配置为从SPI Flash启动,BootROM会加载存储器中的程序到RAM的指定地址(如0x10000)。
  • 独立运行验证
    • 断开调试器连接,重新上电飞腾派开发板,观察程序是否从复位向量自动启动。
    • 通过串口输出(如UART)记录程序运行日志,验证初始化和功能是否正常。

本节详细阐述了飞腾派裸机程序的生成流程和文件格式,涵盖了编译、汇编、链接和格式转换的各个阶段,并深入分析了ELF和BIN文件在调试和运行中的作用。结合飞腾派FTC核心(ARMv8-A架构)的特点,提供了具体的代码示例、工具命令和实践指南,帮助读者掌握裸机程序从源码到可执行映像的开发过程。后续章节将进一步探讨调试技术、Boot Loader设计和复杂外设编程,为读者提供更深入的嵌入式开发知识。

4.2 嵌入式系统的引导程序与启动代码

在飞腾派FTC核心(ARMv8-A架构)的裸机开发中,启动代码(Startup Code)引导程序(Boot Loader) 是系统从上电复位到运行用户程序的关键环节。由于裸机环境缺乏操作系统的支持,启动代码必须以高效的汇编语言完成硬件初始化,为C语言程序的执行奠定基础。本节将详细阐述启动代码的工作原理、汇编实现细节,以及引导程序(如U-Boot)在飞腾派中的裸机本质。

4.2.1 引导程序与启动代码概述

本节深入探讨飞腾派裸机开发中启动代码和引导程序的核心作用,分析其功能、实现方法和与硬件的交互机制。通过理论与实践结合,帮助读者掌握系统启动流程的底层逻辑。


1. 启动代码的定义与作用

定义

启动代码是飞腾派FTC核心上电复位或手动复位时,处理器执行的第一段代码。它通常存储在非易失性存储器(如Flash或ROM)的复位向量地址(Reset Vector),由处理器自动加载并运行。

  • 存储位置:飞腾派FTC核心的复位向量地址通常为0x0(参考芯片手册),启动代码必须精确放置在此地址。
  • 编程语言:由于对速度和底层控制的极高要求,启动代码通常使用汇编语言(ARMv8-A指令集)编写,确保高效且直接访问硬件资源。

作用

启动代码在裸机环境中充当“BIOS”的角色,负责将系统从上电后的“冰冷”状态(Cold Boot)转换为一个可运行C语言程序的稳定环境。其主要功能包括:

  • 初始化处理器状态
    • 配置程序计数器(PC)、堆栈指针(SP)和异常向量表。
    • 设置处理器运行模式(如AArch64的EL3模式,飞腾派支持的最高特权级)。
  • 初始化内存
    • 配置.data段:将初始化数据从存储位置(Flash,LMA)拷贝到运行位置(RAM,VMA)。
    • 清零.bss段:将未初始化数据区域置零。
    • 设置堆栈:分配并初始化堆栈空间,供C语言函数调用使用。
  • 初始化关键外设
    • 配置时钟和电源管理单元,确保处理器和外设运行在正确的频率和电压。
    • 初始化UART(可选),用于调试输出。
  • 跳转到C语言主函数
    • 在完成硬件初始化后,启动代码跳转到C语言的main函数,开始执行用户程序。

飞腾派实践

  • 复位向量:飞腾派FTC核心的复位向量地址通常为0x0或BootROM指定的地址(参考芯片手册)。启动代码的_start标签必须位于此地址。
  • 汇编实现start.S):

    .section .vectors, "ax"
    .global _start
    _start:
        // 初始化堆栈指针
        ldr x0, =_stack_top
        mov sp, x0
    
        // 初始化.data段(从Flash拷贝到RAM)
        bl data_init
    
        // 清零.bss段
        bl bss_init
    
        // 跳转到C语言main函数
        bl main
        b .  // 无限循环,防止程序跑飞
    
    data_init:
        ldr x0, =_data_lma    // 数据段存储地址(Flash)
        ldr x1, =_data_vma    // 数据段运行地址(RAM)
        ldr x2, =_data_size   // 数据段大小
        cbz x2, data_init_end // 如果大小为0,跳过
    data_copy:
        ldr x3, [x0], #8      // 从LMA读取8字节
        str x3, [x1], #8      // 写入VMA
        subs x2, x2, #8       // 减小计数器
        bne data_copy         // 继续循环
    data_init_end:
        ret
    
    bss_init:
        ldr x0, =_bss_start   // BSS段起始地址
        ldr x1, =_bss_end     // BSS段结束地址
        mov x2, #0            // 清零值
    bss_clear:
        cmp x0, x1            // 检查是否到达BSS段末尾
        beq bss_clear_end
        str x2, [x0], #8      // 清零当前地址
        b bss_clear
    bss_clear_end:
        ret
    
  • 说明
    • _start是程序入口,位于复位向量地址。
    • 初始化堆栈指针(SP)到_stack_top(由链接脚本定义,参考3.2.3节)。
    • data_init拷贝.data段从Flash到RAM。
    • bss_init清零.bss段。
    • 跳转到main函数,进入C语言环境。

2. 引导程序(Boot Loader)的裸机本质

引导程序(Boot Loader)是飞腾派等高性能应用处理器(MPU)启动过程中的重要组件,负责从上电到加载操作系统内核的全过程。在裸机环境中,引导程序的阶段1(Stage 1) 等同于启动代码,完全由汇编语言实现,完成关键硬件初始化。

Boot Loader 角色

  • 定义:Boot Loader是一段运行在裸机环境中的程序,负责初始化硬件、加载操作系统内核或用户程序,并将其转移到正确的内存位置。
  • 飞腾派实践:飞腾派开发板通常使用U-Boot作为引导程序,广泛应用于嵌入式Linux系统开发。U-Boot分为两个阶段:
    • 阶段1(Stage 1):裸机启动代码,负责最低限度的硬件初始化。
    • 阶段2(Stage 2):运行在C语言环境中,完成复杂初始化和内核加载。

阶段1(Stage 1):裸机启动代码

  • 裸机本质
    • 阶段1运行在裸机环境中,无操作系统支持,完全由汇编语言编写。
    • 目标是将系统从上电状态带入可运行C语言的环境。
  • 主要任务
    • 处理器初始化
      • 配置ARMv8-A的执行级别(EL3/EL2/EL1,飞腾派通常从EL3开始)。
      • 设置异常向量表,处理复位、中断等事件。
    • 内存初始化
      • 初始化DDR内存控制器,确保RAM可用。
      • 拷贝.data段,清零.bss段,设置堆栈。
    • 外设初始化
      • 配置系统时钟(PLL),确保处理器和外设运行在正确频率。
      • 初始化UART(用于调试输出)或GPIO(用于状态指示)。
    • 跳转到阶段2
      • 在RAM中设置C语言运行环境,跳转到U-Boot的C语言代码。

阶段2(Stage 2):C语言环境

  • 特点
    • 运行在RAM中,使用C语言实现,依赖阶段1建立的堆栈和内存环境。
    • 完成复杂初始化,如SD卡、网络接口或文件系统支持。
    • 加载操作系统内核(如Linux)或用户程序到RAM并跳转执行。

3. 裸机启动的挑战与注意事项

  • 复位向量对齐
    • 启动代码必须精确位于复位向量地址(0x0或BootROM指定地址)。
    • 解决方法:检查链接脚本的.text段分配,确保_start在正确位置。
  • 内存初始化复杂性
    • DDR初始化需要配置复杂的控制寄存器,参考飞腾派手册。
    • 解决方法:仔细阅读芯片手册,确保时钟和DDR参数正确。
  • 资源限制
    • 飞腾派Flash和RAM容量有限,需优化启动代码大小。
    • 解决方法:使用-Os编译选项,避免不必要的全局变量。
  • 调试依赖
    • 启动代码错误可能导致系统无法启动,调试依赖JTAG和UART。
    • 解决方法:在启动代码中添加UART日志,配合JTAG检查寄存器和内存。

小结

本章详细阐述了基于飞腾派的嵌入式裸机软件开发流程。裸机开发流程涵盖三个核心阶段:首先是生成阶段,在宿主机上利用 交叉工具链和链接脚本将 C/汇编源代码编译链接为包含调试信息的 ELF 文件,转换为用于烧录的纯粹 BIN 二进制映像文件;其次是调试阶段,利用 JTAG 等硬件调试器配合 ELF 文件实现对目标机寄存器和内存的源码级控制与验证;最后是固化运行阶段,将 BIN 文件 写入 非易失性存储器 以确保程序能够独立启动和运行。同时,本章深入介绍了系统引导的关键组件:启动代码,这段通常由汇编语言编写的代码是处理器上电后执行的第一段程序,其核心作用是初始化处理器状态、配置内存环境以及关键外设,最终跳转至 C 语言的 main 函数,而高性能系统中的 引导程序(如 U-Boot)的阶段1本质上就是这段裸机启动代码。