3. 裸机开发环境搭建
裸机调试的第一步不是接上 JTAG,而是让代码真正“跑起来”。RISC-V 裸机开发环境由四部分构成:交叉工具链、编译与链接规则、链接脚本对内存布局的描述,以及启动代码到 C 语言入口的交接。任何一环出错,现象往往都是“上电无反应”或“跑飞”,因此必须把这条链路理解成一条可验证的信号与数据通路,而不是零散命令的堆叠。
一、核心原理:从源码到可执行镜像的链路
宿主机通常是 x86_64 Linux,而目标机是 RV32/RV64 架构,因此需要交叉编译器。工具链把 .c、.S 编译为目标文件,链接器再依据链接脚本把各段放置到目标内存地址,最终生成 ELF 与可烧录的二进制镜像。启动代码负责设置栈指针、清零 BSS、搬运 DATA,然后跳转到 main。这条链路可以用下面的信号与数据流图表示。
二、方法流程:工具链安装与交叉编译
推荐使用官方预编译工具链或发行版包管理器安装。以 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 头导致跑飞 |
复盘时可按“工具链—编译参数—链接脚本—启动代码—烧录镜像”的顺序逐段验证。每一段都有可观测的输出:readelf 看段、objdump 看指令、调试器看寄存器和内存。把这条链路固化为脚本,后续每新增一个外设驱动,都能快速判断问题出在环境还是代码。