运行 C 语言程序
本文将通过实例使用 Bergamot 运行使用 C 语言编写的实例.
准备
下载 Bergamot
首先, 我们需要下载并准备 Bergamot 环境, 您可以参考 教程导入 的安装过程.
当您能够成功编译 Verilator 测试程序 VVerilatorTestCore 说明 Bergamot 已经准备完毕.
安装 RISC-V GCC 交叉编译器
如果您使用了我们准备的 bergamot-build Docker 镜像, 则可以跳过此步骤.
要想编译您的 C 的程序到 RISC-V 二进制可执行程序,您需要安装 RISC-V 官方为您提供的 RISC-V GCC 交叉编译器. 该项目的地址为 riscv-gnu-toolchain .
关于架构模型, 由于 Bergamot 目前使用 RV32 并且浮点 FPU 并不完善, 我们推荐您使用:
./configure --prefix=/opt/riscv-toolchain --with-arch=rv32gc --with-abi=ilp32
参数 --prefix=/opt/riscv-toolchain 指明安装地址, --with-arch=rv32gc 指明我们使用 rv32gc 扩展, --with-abi=ilp32 适用于 32 位软浮点.
编译 Bootloader 启动程序
对于测试核心 VerilatorTestCore 的启动地址为 hffff0000, 该地址正好处于 ROM , 测试核心将在这里首先启动 Bootloader 程序, 这在以下代码中配置:
private class VerilatorTestCore extends Module {
private val config = CoreConfig.default.copy(pcInit = "hffff0000")
// ...
}
对于测试核心的地址映射, 在下面的代码文件中进行配置:
private val interconnect = Module(
new AXIInterconnect(
Seq(
"h00000000", // hole
"h2000000", // mtime
"h10000000", // uart
"h80000000", // ram
"hffff0000" // rom
)
)
)
VerilatorTestCore 启动时, 将加载工作目录下的 boot.hex 文件到 ROM, 该文件符合 verilog 所定义的hex文件格式. 一个 Bootloader 启动程序的例子为:
.text
.globl _start
/* . = 0xffff0000; */
_start:
li a0, 0x80000000 /* Entry point */
jalr ra, 0(a0) /* Go */
上述代码中, 将启动地址 0x80000000 加载到寄存器 a0 中, 然后通过 jalr 指令开始执行地址 0x80000000 处的代码, 此地址正好是虚拟 DRAM 的地址.
通过编译器得到的二进制机器码为:
80000537
000500e7
00000000
00000000
00000000
00000000
将上述内容以 文本 的形式直接保存在 simulator 文件夹下的 boot.hex 文件即可.
编译测试 C 语言测试程序
现在, 编写您的测试 C 语言测试程序, 下面是一个简单的例子:
void main()
{
int *out = (int *)0x80010000;
int a = 0;
for (int i = 1; i <= 10; i++)
{
a += i;
}
*out = a;
while (1)
;
}
该程序将计算 1 到 10 的累加和, 并存到内存 0x80010000 处以供我们检查结果.
因为我们编写的是裸机程序, 编译器无法定位 main 方法, 我们需要手动编写入口程序:
.text
.globl _start
_start:
li sp, 0x80011000
j main
该代码设置栈的地址为 0x80011000 , 设置栈是必须的, 否则我们将无法调用 C 语言函数. 最后我们需要编写链接器脚本, 告诉编译器如何编排内存布局:
OUTPUT_ARCH("riscv")
OUTPUT_FORMAT("elf32-littleriscv")
ENTRY(_start)
SECTIONS {
. = 0x80000000;
.text : { *(.text) }
_end = .;
}
这将告诉编译器我们将代码加载到内存 0x80000000 处开始执行.
最后使用 gcc 命令编译:
riscv32-unknown-linux-gnu-gcc -march=rv32gc -mabi=ilp32 -static -mcmodel=medany -fvisibility=hidden -nostdlib -nostartfiles -Tmain.lds main.S main.c -o main
其中 main.lds 为链接器脚本, main.S 是启 动入口程序, main.c 是我们编写的 C 语言源文件, 最后您将得到二进制 main 文件说明一切进展顺利.
我们的实例程序没有全局变量, 若您想使用全局变量则必须手动编写 bss 段和 data 段的清空和初始化程序, 并且需要在链接器脚本中指明如何编排这两个段, 最后可能需要设置 gp 寄存器的指向.
最后, 我们得到的 main 文件的二进制格式为 elf, 但这并不是我们想要的, VerilatorTestCore 需要的是原始二进制格式, 其将会把该文件的内容按照 二进制 原封不动的加载进内存 0x80000000 处. 在那之前, 您可以通过反编译查看我们的 main 文件结果是否正确:
riscv32-unknown-linux-gnu-objdump -d main > main.txt
这是生成的部分代码, 其中每一行指明了指令在内存中的地址, 以及二进制指令码, 最后是反编译的结果:
main: file format elf32-littleriscv
Disassembly of section .text:
80000000 <_start>:
80000000: 80011137 lui sp,0x80011
80000004: 0040006f j 80000008 <main>
80000008 <main>:
80000008: 1101 add sp,sp,-32 # 80010fe0 <_end+0x10f90>
8000000a: ce22 sw s0,28(sp)
8000000c: 1000 add s0,sp,32
8000000e: 800107b7 lui a5,0x80010
......
现在通过下面的命令, 导出 elf 文件中的 text 段到二进制文件中:
riscv32-unknown-linux-gnu-objcopy -O binary -j .text main main.bin
得到了 main.bin 文件, 可以通过二进制工具打开, 该文件的第一个 4 字节应该是 80011137 对应我们第一条指令. 将该文件复制到 simulator 文件夹中, 现在 simulator 的内容应该有:
boot.hex -- ROM Bootloader 程序
main.bin -- RISC-V 测试程序
obj_dir/VVerilatorTestCore -- VerilatorTestCore 测试程序
运行 VerilatorTestCore
在 simulator 文件夹下, 运行下面的命令:
./obj_dir/VVerilatorTestCore +trace +Bmain.bin +T10000
可以看到控制台会飞速打印我们的运行日志, 运行到 10000 周期后程序退出. 程序退出后会将最后的内存导出到 mem.bin 文件中, 使用二进制编辑器打开或者执行下面的命令, 检查内存地址 0x80010000 的值 (文件偏移 0x10000), 若是 55 (十六进制 37)说明我们的程序运行成功.
xxd -s 0x10000 -l 1 mem.bin
另外在文件 logs/vlt_dump.vcd 会以 VCD 格式导出波形图方便您调试.