本指南涵盖为 FPGA 软核开发搭建 RISC-V GCC 工具链,特别针对 /home/user/work/ip_riscv 中的 ip_riscv 和 ip_lcpu 软核项目。工作流包括:将 C 和汇编代码编译为 RISC-V ELF 二进制文件、提取存储器初始化文件(.hex / .mif),以及将编译后的固件与 Verilator 和 ModelSim 仿真集成。
RISC-V 协作组织提供适用于 Ubuntu 的预编译工具链:
# 下载最新的预编译工具链 (rv32imc / rv32imac)
TOOLCHAIN_VER="2024.04.12"
wget https://github.com/riscv-collab/riscv-gnu-toolchain/releases/download/${TOOLCHAIN_VER}/riscv32-elf-ubuntu-22.04-${TOOLCHAIN_VER}.tar.gz
# 安装到 tools 目录
mkdir -p /home/user/tools/riscv
tar -xzf riscv32-elf-ubuntu-22.04-${TOOLCHAIN_VER}.tar.gz -C /home/user/tools/riscv/
# 添加到 PATH(或创建激活脚本)
export PATH="/home/user/tools/riscv/bin:$PATH"
# 验证
riscv32-unknown-elf-gcc --version
优点:
当需要自定义 multilib 配置、最新编译器优化或非标准扩展时,从源码构建。
# 安装构建依赖
sudo apt install autoconf automake autotools-dev curl python3 \
libmpc-dev libmpfr-dev libgmp-dev gawk build-essential \
bison flex texinfo gperf libtool patchutils bc \
zlib1g-dev libexpat-dev
# 克隆工具链仓库
git clone --depth 1 https://github.com/riscv-collab/riscv-gnu-toolchain.git \
/home/user/tools/riscv_src
cd /home/user/tools/riscv_src
# 配置为 rv32imc(无浮点,压缩指令)
# 使用 newlib(裸机嵌入式)
./configure \
--prefix=/home/user/tools/riscv \
--with-arch=rv32imc \
--with-abi=ilp32 \
--with-multilib-generator="rv32i-ilp32--;rv32im-ilp32--;rv32imc-ilp32--"
# 构建(根据 CPU 性能约需 30-60 分钟)
make -j$(nproc)
# 验证
/home/user/tools/riscv/bin/riscv32-unknown-elf-gcc --version
优点:
-march)如果你的软核项目需要同时支持最小 rv32i 配置(Boot ROM)和完整的 rv32imc 配置(应用程序代码):
./configure \
--prefix=/home/user/tools/riscv \
--with-arch=rv32imc \
--with-abi=ilp32 \
--with-multilib-generator="rv32i-ilp32--;rv32im-ilp32--;rv32imc-ilp32--;rv32imac-ilp32--"
然后使用相应的 -march 标志编译:
# 引导加载程序:最小 ISA
riscv32-unknown-elf-gcc -march=rv32i -mabi=ilp32 -c boot.S -o boot.o
# 应用程序:带乘法和压缩指令
riscv32-unknown-elf-gcc -march=rv32imc -mabi=ilp32 -c main.c -o main.o
创建专用的环境脚本:
# /home/user/tools/scripts/riscv_env.sh
# 用法: source riscv_env.sh
export RISCV_TOOLCHAIN="/home/user/tools/riscv"
export PATH="${RISCV_TOOLCHAIN}/bin:$PATH"
# Makefile 中的通用变量
export CROSS_COMPILE=riscv32-unknown-elf-
export RISCV_ARCH=rv32imc
export RISCV_ABI=ilp32
echo "RISC-V 工具链已激活"
echo " GCC: $(which riscv32-unknown-elf-gcc)"
echo " ARCH: ${RISCV_ARCH}"
echo " ABI: ${RISCV_ABI}"
# 汇编启动代码
riscv32-unknown-elf-gcc -march=rv32imc -mabi=ilp32 \
-nostartfiles -nostdlib \
-c startup.S -o build/startup.o
# 编译 C 应用程序
riscv32-unknown-elf-gcc -march=rv32imc -mabi=ilp32 \
-nostartfiles -nostdlib \
-Os -ffunction-sections -fdata-sections \
-Wall -Wextra \
-I src/ -I include/ \
-c src/main.c -o build/main.o
# 使用自定义链接脚本链接
riscv32-unknown-elf-gcc -march=rv32imc -mabi=ilp32 \
-nostartfiles -nostdlib \
-T linker.ld \
-Wl,--gc-sections \
-o build/firmware.elf \
build/startup.o build/main.o
| 标志 | 用途 |
|---|---|
-march=rv32imc |
ISA:I(基础)+ M(乘法)+ C(压缩) |
-mabi=ilp32 |
ABI:32 位 int/long/pointer(软浮点) |
-nostartfiles |
不使用标准 C 启动代码 (crt0) |
-nostdlib |
不链接标准 C 库 |
-Os |
优化体积(对 FPGA BRAM 至关重要) |
-ffunction-sections |
将每个函数放入独立段(使能 --gc-sections) |
-fdata-sections |
将每个数据变量放入独立段 |
-Wl,--gc-sections |
链接器垃圾回收——丢弃未引用的代码 |
-Wall -Wextra |
启用所有警告 |
-T linker.ld |
自定义链接脚本 |
链接脚本是编译代码与软核内存映射之间的桥梁。每个 ip_riscv / ip_lcpu 项目都需要自定义的 linker.ld。
/* /home/user/work/ip_riscv/fw/linker.ld */
/* ip_riscv 软核链接脚本 */
OUTPUT_ARCH(riscv)
ENTRY(_start)
MEMORY
{
/* 指令 RAM:64 KB,起始地址 0x0000_0000 */
IRAM (rx) : ORIGIN = 0x00000000, LENGTH = 64K
/* 数据 RAM:32 KB,起始地址 0x0001_0000 */
DRAM (rw) : ORIGIN = 0x00010000, LENGTH = 32K
/* Boot ROM:8 KB,起始地址 0x0002_0000(启动后只读) */
BOOTROM (r): ORIGIN = 0x00020000, LENGTH = 8K
/* 内存映射外设:0x8000_0000 */
MMIO (rw) : ORIGIN = 0x80000000, LENGTH = 4K
}
SECTIONS
{
/* ── 代码段 位于 IRAM ── */
.text : {
*(.text.init) /* _start 和早期初始化 */
*(.text*) /* 所有函数 */
*(.rodata*) /* 只读数据(字符串、常量) */
. = ALIGN(4);
} > IRAM
/* ── 已初始化数据 ── */
/* LMA(加载地址)在 IRAM,VMA(虚拟地址)在 DRAM */
/* 启动代码将 .data 从 IRAM 复制到 DRAM */
.data : {
_data_start = .;
*(.data*)
*(.sdata*)
. = ALIGN(4);
_data_end = .;
} > DRAM AT > IRAM
_data_lma = LOADADDR(.data); /* .data 在 IRAM 中的存储地址 */
/* ── 零初始化数据 ── */
/* 启动代码将此区域清零 */
.bss : {
_bss_start = .;
*(.bss*)
*(.sbss*)
*(COMMON)
. = ALIGN(4);
_bss_end = .;
} > DRAM
/* ── 栈(从 DRAM 末尾向下增长) ── */
_stack_top = ORIGIN(DRAM) + LENGTH(DRAM);
_stack_size = 4K;
/* ── 堆(可选,位于 BSS 之后、栈之前) ── */
_heap_start = _bss_end;
_heap_size = _stack_top - _stack_size - _bss_end;
/* ── Boot ROM 段 ── */
.boot : {
*(.boot*)
} > BOOTROM
/* ── 丢弃未用段 ── */
/DISCARD/ : {
*(.comment)
*(.note*)
*(.eh_frame*)
}
}
# /home/user/work/ip_riscv/fw/startup.S
# 最小化 RISC-V 裸机软核启动代码
.section .text.init, "ax"
.globl _start
_start:
# 初始化栈指针
la sp, _stack_top
# 将 .data 段从 IRAM 复制到 DRAM
la t0, _data_lma # 源地址 (IRAM)
la t1, _data_start # 目标地址 (DRAM)
la t2, _data_end # .data 在 DRAM 中的结束地址
1:
bge t1, t2, 2f
lw t3, 0(t0)
sw t3, 0(t1)
addi t0, t0, 4
addi t1, t1, 4
j 1b
2:
# 清零 .bss 段
la t0, _bss_start
la t1, _bss_end
1:
bge t0, t1, 2f
sw zero, 0(t0)
addi t0, t0, 4
j 1b
2:
# 跳转到 main
call main
# 如果 main 返回,则无限循环
halt:
wfi
j halt
IRAM (rx) : ORIGIN = 0x00000000, LENGTH = 64K
DRAM (rw) : ORIGIN = 0x00010000, LENGTH = 32K
ip_lcpu(16 KB IRAM,8 KB DRAM,轻量级):
IRAM (rx) : ORIGIN = 0x00000000, LENGTH = 16K
DRAM (rw) : ORIGIN = 0x00004000, LENGTH = 8K
带外部 SDRAM 的自定义配置:
IRAM (rx) : ORIGIN = 0x00000000, LENGTH = 64K
DRAM (rw) : ORIGIN = 0x00010000, LENGTH = 32K
SDRAM (rw) : ORIGIN = 0x10000000, LENGTH = 32M
在链接脚本中添加 .sdram 段,将大型数据缓冲区放置到外部 SDRAM 中。
FPGA 工具需要存储器初始化文件来预加载软核的指令和数据 RAM。使用 objcopy 提取原始二进制文件,并将其转换为 Intel HEX 或 Altera MIF 格式。
# 提取 .text 段(指令 + 只读数据)→ 原始二进制
riscv32-unknown-elf-objcopy -O binary -j .text firmware.elf build/iram.bin
# 提取 .data 段 → 原始二进制
riscv32-unknown-elf-objcopy -O binary -j .data firmware.elf build/dram.bin
# 提取整个固件为平坦二进制(适用于简单核心)
riscv32-unknown-elf-objcopy -O binary firmware.elf build/firmware.bin
# 使用 Python 脚本将二进制转换为 Intel HEX
# (objcopy 仅支持 binary、ihex、srec;ihex 分段效果不佳)
python3 /home/user/work/ip_riscv/scripts/bin2hex.py \
build/iram.bin \
build/iram.hex \
--address 0x00000000 \
--wordsize 4
辅助脚本 bin2hex.py:
#!/usr/bin/env python3
# /home/user/work/ip_riscv/scripts/bin2hex.py
"""将原始二进制文件转换为 Intel HEX 格式,用于 FPGA 存储器初始化。"""
import sys
import struct
def bin2hex(bin_path, hex_path, base_addr=0x00000000, wordsize=4):
with open(bin_path, 'rb') as f:
data = f.read()
# 按字对齐填充
remainder = len(data) % wordsize
if remainder:
data += b'\x00' * (wordsize - remainder)
with open(hex_path, 'w') as f:
addr = base_addr
for i in range(0, len(data), wordsize):
word_bytes = data[i:i+wordsize]
# 小端序字
word = struct.unpack('<I', word_bytes)[0] if wordsize == 4 else \
struct.unpack('<H', word_bytes)[0]
# Intel HEX 记录: @<addr> <hex_word>
f.write(f"@{addr:08X} {word:0{wordsize*2}X}\n")
addr += wordsize
print(f"已转换 {len(data)} 字节 → {hex_path}")
if __name__ == '__main__':
if len(sys.argv) < 3:
print("用法: bin2hex.py <输入.bin> <输出.hex> [--address 0xNN] [--wordsize 4]")
sys.exit(1)
base = 0x00000000
wordsize = 4
args = sys.argv[1:]
for i, a in enumerate(args):
if a == '--address' and i+1 < len(args):
base = int(args[i+1], 16)
if a == '--wordsize' and i+1 < len(args):
wordsize = int(args[i+1])
bin2hex(args[0], args[1], base_addr=base, wordsize=wordsize)
python3 /home/user/work/ip_riscv/scripts/bin2mif.py \
build/iram.bin \
build/iram.mif \
--depth 16384 \
--width 32
#!/usr/bin/env python3
# /home/user/work/ip_riscv/scripts/bin2mif.py
"""将原始二进制文件转换为 Altera MIF 格式。"""
import sys
import struct
def bin2mif(bin_path, mif_path, depth=16384, width=32):
with open(bin_path, 'rb') as f:
data = f.read()
bytes_per_word = width // 8
num_words = min(len(data) // bytes_per_word, depth)
with open(mif_path, 'w') as f:
f.write(f"-- MIF 文件,由 {bin_path} 生成\n")
f.write(f"DEPTH = {depth};\n")
f.write(f"WIDTH = {width};\n")
f.write(f"ADDRESS_RADIX = HEX;\n")
f.write(f"DATA_RADIX = HEX;\n")
f.write(f"CONTENT BEGIN\n")
for i in range(num_words):
start = i * bytes_per_word
word_bytes = data[start:start+bytes_per_word]
if len(word_bytes) < bytes_per_word:
word_bytes += b'\x00' * (bytes_per_word - len(word_bytes))
word = struct.unpack('<I', word_bytes)[0]
f.write(f" {i:04X} : {word:08X};\n")
# 其余地址填充为零
if num_words < depth:
f.write(f" [{num_words:04X}..{depth-1:04X}] : 00000000;\n")
f.write(f"END;\n")
print(f"已转换 {len(data)} 字节 → {mif_path} ({num_words}/{depth} 个字)")
if __name__ == '__main__':
if len(sys.argv) < 3:
print("用法: bin2mif.py <输入.bin> <输出.mif> [--depth 16384] [--width 32]")
sys.exit(1)
depth = 16384
width = 32
args = sys.argv[1:]
for i, a in enumerate(args):
if a == '--depth' and i+1 < len(args):
depth = int(args[i+1])
if a == '--width' and i+1 < len(args):
width = int(args[i+1])
bin2mif(args[0], args[1], depth=depth, width=width)
# 反汇编 ELF,验证代码布局
riscv32-unknown-elf-objdump -d firmware.elf > build/firmware.dis
# 显示段头(检查地址和大小)
riscv32-unknown-elf-objdump -h firmware.elf
# 显示符号表
riscv32-unknown-elf-objdump -t firmware.elf | sort
# 以十六进制显示所有段内容
riscv32-unknown-elf-objdump -s firmware.elf
# 检查 IRAM 的前几个字是否与 ELF 匹配
xxd build/iram.bin | head -5
riscv32-unknown-elf-objdump -d firmware.elf | head -20
# 检查段大小是否在内存限制内
riscv32-unknown-elf-size firmware.elf
# 预期输出:
# text data bss dec hex filename
# 12340 512 2048 14900 3a34 firmware.elf
一个完整的固件构建 Makefile。
# /home/user/work/ip_riscv/fw/Makefile
# 为 ip_riscv 软核构建固件: C/ASM → ELF → .hex / .mif
# ── 工具链 ──
CROSS := riscv32-unknown-elf-
CC := $(CROSS)gcc
OBJCOPY := $(CROSS)objcopy
OBJDUMP := $(CROSS)objdump
SIZE := $(CROSS)size
# ── 架构 ──
ARCH := rv32imc
ABI := ilp32
MARCH := -march=$(ARCH) -mabi=$(ABI)
# ── 目录 ──
SRC_DIR := src
INC_DIR := include
BUILD_DIR := build
# ── 内存映射(必须与 linker.ld 匹配) ──
IRAM_BASE := 0x00000000
DRAM_BASE := 0x00010000
IRAM_SIZE := 65536 # 64 KB
DRAM_SIZE := 32768 # 32 KB
# ── 源文件 ──
C_SRCS := $(wildcard $(SRC_DIR)/*.c)
ASM_SRCS := $(wildcard $(SRC_DIR)/*.S)
C_OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(C_SRCS))
ASM_OBJS := $(patsubst $(SRC_DIR)/%.S, $(BUILD_DIR)/%.o, $(ASM_SRCS))
OBJS := $(ASM_OBJS) $(C_OBJS)
# ── 输出文件 ──
ELF := $(BUILD_DIR)/firmware.elf
IRAM_HEX := $(BUILD_DIR)/iram.hex
IRAM_MIF := $(BUILD_DIR)/iram.mif
DRAM_HEX := $(BUILD_DIR)/dram.hex
DIS := $(BUILD_DIR)/firmware.dis
# ── 编译标志 ──
CFLAGS := $(MARCH) -nostartfiles -nostdlib \
-Os -ffunction-sections -fdata-sections \
-Wall -Wextra \
-I$(INC_DIR)
LDFLAGS := $(MARCH) -nostartfiles -nostdlib \
-T linker.ld -Wl,--gc-sections
# ── 默认目标 ──
.PHONY: all
all: $(ELF) $(IRAM_HEX) $(IRAM_MIF) $(DRAM_HEX) $(DIS)
# ── 创建构建目录 ──
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
# ── 编译 C ──
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
@echo " CC $<"
$(CC) $(CFLAGS) -c $< -o $@
# ── 汇编 ──
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.S | $(BUILD_DIR)
@echo " AS $<"
$(CC) $(MARCH) -c $< -o $@
# ── 链接 ──
$(ELF): $(OBJS) linker.ld
@echo " LD $@"
$(CC) $(LDFLAGS) $(OBJS) -o $@
@echo ""
$(SIZE) $@
@echo ""
# ── 提取 IRAM 二进制 ──
$(BUILD_DIR)/iram.bin: $(ELF)
@echo " OBJCOPY iram.bin"
$(OBJCOPY) -O binary -j .text $< $@
# ── 提取 DRAM 二进制 ──
$(BUILD_DIR)/dram.bin: $(ELF)
@echo " OBJCOPY dram.bin"
$(OBJCOPY) -O binary -j .data $< $@
# ── Intel HEX ──
$(IRAM_HEX): $(BUILD_DIR)/iram.bin
@echo " HEX $@"
python3 ../scripts/bin2hex.py $< $@ --address $(IRAM_BASE) --wordsize 4
$(DRAM_HEX): $(BUILD_DIR)/dram.bin
@echo " HEX $@"
python3 ../scripts/bin2hex.py $< $@ --address $(DRAM_BASE) --wordsize 4
# ── Altera MIF ──
$(IRAM_MIF): $(BUILD_DIR)/iram.bin
@echo " MIF $@"
python3 ../scripts/bin2mif.py $< $@ \
--depth $$(($(IRAM_SIZE) / 4)) --width 32
# ── 反汇编 ──
$(DIS): $(ELF)
@echo " OBJDUMP $@"
$(OBJDUMP) -d $< > $@
# ── 清理 ──
.PHONY: clean
clean:
rm -rf $(BUILD_DIR)
# ── 帮助 ──
.PHONY: help
help:
@echo "构建目标:"
@echo " all — 构建固件: ELF → HEX → MIF → DIS"
@echo " clean — 删除构建产物"
@echo ""
@echo "build/ 目录下的输出文件:"
@echo " firmware.elf — ELF 二进制文件"
@echo " iram.hex — 指令 RAM 的 Intel HEX 文件"
@echo " dram.hex — 数据 RAM 的 Intel HEX 文件"
@echo " iram.mif — 指令 RAM 的 Altera MIF 文件"
@echo " firmware.dis — 反汇编清单"
// 在 Verilator 的仿真包装中
// 在仿真开始时使用 $readmemh 加载 IRAM
reg [31:0] iram [0:16383]; // 64 KB / 4 字节 = 16K 字
initial begin
$readmemh("build/iram.hex", iram);
$display("IRAM 已加载: %0d 字节", $size(iram) * 4);
end
对于 Verilator,建议使用 C++ Testbench 加载二进制文件:
// sim_main.cpp — Verilator C++ Testbench
#include "Vfpga_top.h"
#include "verilated.h"
#include <fstream>
#include <vector>
void load_iram(Vfpga_top* top, const char* hex_file) {
std::ifstream f(hex_file);
std::string line;
while (std::getline(f, line)) {
if (line[0] != '@') continue;
uint32_t addr = std::stoul(line.substr(1, 8), nullptr, 16);
uint32_t data = std::stoul(line.substr(10), nullptr, 16);
// 写入仿真存储器模型
top->iram_mem[addr >> 2] = data;
}
printf("IRAM 已从 %s 加载\n", hex_file);
}
int main(int argc, char** argv) {
Verilated::commandArgs(argc, argv);
Vfpga_top* top = new Vfpga_top;
load_iram(top, "build/iram.hex");
// 运行仿真
top->clk = 0;
top->rst_n = 0;
top->eval();
// ... 仿真循环 ...
delete top;
return 0;
}
# modelsim_sim.tcl — ModelSim 仿真脚本
vlib work
vlog +acc -work work -f verible.filelist
vlog +acc -work work sim/tb_fpga_top.sv
vsim -voptargs=+acc work.tb_fpga_top
# 在仿真开始时加载 IRAM
mem load -infile build/iram.hex -format hex /tb_fpga_top/u_dut/u_riscv_core/iram
# 运行直到超时或暂停
run -all
quit
// sim/tb_fpga_top.v — 带固件加载的软核 Testbench
module tb_fpga_top;
reg clk = 0;
reg rst_n = 0;
// DUT
fpga_top u_dut (
.clk(clk),
.rst_n(rst_n),
.o_uart_tx(uart_tx),
.o_gpio_led(gpio_led)
);
// 将固件加载到 IRAM 中
initial begin
$readmemh("build/iram.hex", u_dut.u_riscv_core.iram);
$readmemh("build/dram.hex", u_dut.u_riscv_core.dram);
$display("固件已加载到 IRAM + DRAM");
end
// 产生时钟
always #5 clk = ~clk; // 100 MHz 仿真时钟
// 复位序列
initial begin
rst_n = 0;
repeat (10) @(posedge clk);
rst_n = 1;
end
// 监控 UART 输出,检查测试通过/失败消息
// 固件应通过 UART 打印 "TEST_PASS" 或 "TEST_FAIL"
integer test_result = 0;
always @(posedge clk) begin
if (uart_rx_ready) begin
$write("%c", uart_rx_data);
if (uart_rx_data == "P") begin
// 从 UART 流中解析 "PASS" 或 "FAIL"
// ...
end
end
end
// 超时看门狗
initial begin
#10000000; // 100 MHz 下 1 ms
$display("超时: 仿真在 1 ms 内未完成");
$finish;
end
endmodule
构建固件并将其加载到仿真环境后:
riscv32-unknown-elf-objdump -h build/firmware.elf
riscv32-unknown-elf-objdump -d build/firmware.elf | head -30
# 地址 0x00000000 的第一条指令应该来自你的 _start 代码
.data 段的 LMA/VMA 正确:
riscv32-unknown-elf-objdump -h build/firmware.elf | grep "\.data"
# VMA 应在 DRAM 中,LMA 应在 IRAM 中
riscv32-unknown-elf-size build/firmware.elf
# text 应 < IRAM_SIZE, data+bss 应 < DRAM_SIZE
head -5 build/iram.hex
riscv32-unknown-elf-objdump -d build/firmware.elf | head -20
# 地址 0 处的 HEX 字应与反汇编指令匹配
| 文件 | 用途 |
|---|---|
linker.ld |
内存映射定义(IRAM、DRAM、BOOTROM、MMIO) |
startup.S |
复位向量、栈初始化、.data 复制、.bss 清零 |
Makefile |
构建自动化:C/ASM → ELF → HEX → MIF |
bin2hex.py |
将二进制转换为 Intel HEX 供 Vivado/ModelSim 使用 |
bin2mif.py |
将二进制转换为 Altera MIF 供 Quartus 使用 |
build/firmware.elf |
链接后的可执行文件 |
build/iram.hex |
指令 RAM 初始化文件 |
build/dram.hex |
数据 RAM 初始化文件 |
build/firmware.dis |
反汇编清单,用于调试 |