3. 裸机开发环境搭建

裸机调试的第一步不是接上 JTAG,而是让代码真正“跑起来”。RISC-V 裸机开发环境由四部分构成:交叉工具链、编译与链接规则、链接脚本对内存布局的描述,以及启动代码到 C 语言入口的交接。任何一环出错,现象往往都是“上电无反应”或“跑飞”,因此必须把这条链路理解成一条可验证的信号与数据通路,而不是零散命令的堆叠。

一、核心原理:从源码到可执行镜像的链路

宿主机通常是 x86_64 Linux,而目标机是 RV32/RV64 架构,因此需要交叉编译器。工具链把 .c、.S 编译为目标文件,链接器再依据链接脚本把各段放置到目标内存地址,最终生成 ELF 与可烧录的二进制镜像。启动代码负责设置栈指针、清零 BSS、搬运 DATA,然后跳转到 main。这条链路可以用下面的信号与数据流图表示。

RISC-V 裸机镜像生成与启动数据流 源码 start.S / main.c 交叉编译 riscv64-unknown-elf-gcc 链接 link.lds 决定段地址 ELF / BIN objcopy 生成镜像 烧录 Flash / JTAG 上电后的启动流程(目标机内存视角) 复位向量 0x8000_0000 设置 sp 栈顶地址 清 BSS 拷贝 DATA 跳转 main C 运行环境就绪 关键检查点:入口地址、栈对齐、BSS 清零范围、DATA 搬运源/目的地址

二、方法流程:工具链安装与交叉编译

推荐使用官方预编译工具链或发行版包管理器安装。以 Ubuntu 为例,可直接安装 gcc-riscv64-unknown-elf,也可从源码构建。安装后必须验证目标架构与 ABI,避免误用本机 gcc。

# 安装(Ubuntu/Debian)
sudo apt install gcc-riscv64-unknown-elf binutils-riscv64-unknown-elf

# 验证
riscv64-unknown-elf-gcc -v
riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -c main.c -o main.o

# 查看段布局
riscv64-unknown-elf-readelf -S main.elf
riscv64-unknown-elf-objdump -d main.elf | head -40

交叉编译的关键是 -march 与 -mabi 必须与目标核一致。RV32IMAC 对应 rv32imac 与 ilp32;若目标核不支持原子指令却使用 rv32imac,链接可能通过但运行会触发非法指令异常。此外,裸机环境应加 -ffreestanding -nostdlib -nostartfiles,避免引入宿主 libc 的启动逻辑。

三、链接脚本:把段放到正确的地址

链接脚本决定代码和数据的最终地址。典型的最小链接脚本如下,它把入口设为 _start,并把 .text、.rodata、.data、.bss 依次排布,同时提供 BSS 起止符号供启动代码使用。

ENTRY(_start)

MEMORY {
  RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 64M
}

SECTIONS {
  .text : { *(.text.init) *(.text*) } > RAM
  .rodata : { *(.rodata*) } > RAM
  .data : { *(.data*) } > RAM
  .bss : {
    __bss_start = .;
    *(.bss*) *(COMMON)
    __bss_end = .;
  } > RAM
  . = ALIGN(16);
  _end = .;
}

常见误区是只写 .text 而遗漏 .bss,导致全局未初始化变量落在镜像之外,运行时读到随机值。另一个误区是忘记 ENTRY(_start),使复位向量指向错误位置。

四、启动流程与最小可运行系统

启动代码通常用汇编编写,负责建立 C 运行环境。下面是一个 RV32 的最小 start.S 片段,它设置栈指针、清零 BSS,然后调用 main。

.section .text.init
.globl _start
_start:
    la sp, _stack_top      # 设置栈顶
    la t0, __bss_start
    la t1, __bss_end
1:  bgeu t0, t1, 2f
    sw zero, 0(t0)
    addi t0, t0, 4
    j 1b
2:  call main
    j .                    # main 返回后原地循环

对应的 main.c 可以只做一件事:向 UART 发送字符,证明系统已运行。若没有串口,可用 GPIO 翻转或写内存标志位,再通过调试器观察。

检查项正确表现常见错误现象
工具链架构readelf 显示 RISC-V误用 x86 gcc,链接报错
入口地址_start 位于 0x80000000复位后取指异常
栈指针sp 16 字节对齐压栈越界或异常
BSS 清零__bss_start 到 __bss_end 全 0全局变量初值随机
镜像格式objcopy 生成纯二进制烧录 ELF 头导致跑飞
建议先构建“最小可运行系统”:只包含启动代码、链接脚本和一个空 main,确认能停在 main 后再逐步加入串口、定时器和中断。这样能把环境问题与业务问题分离。
若上电后无任何反应,优先用调试器读取 PC 和 sp。PC 停在 0 或非法地址,通常是入口或链接脚本错误;sp 未初始化,通常是启动代码未执行或栈顶符号未定义。

复盘时可按“工具链—编译参数—链接脚本—启动代码—烧录镜像”的顺序逐段验证。每一段都有可观测的输出:readelf 看段、objdump 看指令、调试器看寄存器和内存。把这条链路固化为脚本,后续每新增一个外设驱动,都能快速判断问题出在环境还是代码。