news 2026/2/8 19:07:18

从零实现RISC-V最小系统完整示例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现RISC-V最小系统完整示例

从零搭建一个能跑代码的RISC-V最小系统:手把手带你点亮第一行“Hello RISC-V”

你有没有想过,一块FPGA上电之后,是如何从一片寂静跳转到执行第一条指令的?
它怎么知道该从哪里取指、数据存在哪、栈指针设在何处?
如果你正准备入门SoC设计,或者想真正搞懂RISC-V不只是“一套指令集”,而是一整套可运行的硬件逻辑组合——那这篇文章就是为你写的。

我们不讲空泛理论,也不堆砌术语。本文将带你从零开始,在FPGA上构建一个可以打印“Hello RISC-V”的最小系统,涵盖CPU核选取、总线互联、存储映射、时钟复位、外设驱动和工具链配置等全流程。最终成果不是仿真截图,而是一个可烧录、可调试、可扩展的真实工程模板


为什么要做“最小系统”?

很多人学RISC-V,是从读《The RISC-V Reader》或跑QEMU开始的。但这些环境已经帮你封装好了“系统”本身——内存有了,设备有了,启动流程也写好了。

可一旦你要自己做一颗处理器,问题就来了:

  • CPU上电后第一条指令从哪来?
  • 全局变量放在哪里?
  • 如何让程序输出点东西看看是不是真在跑?

这就是“最小系统”的意义:它剥离一切冗余,只保留能让处理器跑起来的最简结构。就像搭积木的第一块底板,稳了,后面才能往上加中断、定时器、操作系统……

更重要的是,这个过程逼你直面那些被忽略的底层细节:

比如:PC初始值是多少?ROM内容怎么固化?UART波特率怎么分频?

只有亲手连过一次地址线、写过一段汇编初始化代码,你才会明白什么叫“软硬协同”。


我们用什么CPU核?PicoRV32还是VexRiscv?

市面上开源RISC-V软核不少,但我们选的是PicoRV32—— Clifford Wolf(Verilog大神)的作品,特点非常鲜明:

  • 极致精简:最小配置仅需约1500 LUTs,适合低端FPGA;
  • 功能完整:支持RV32IMC(整数+乘除+压缩指令),能跑GCC编译的C代码;
  • 文档齐全:自带Testbench、Makefile、示例固件;
  • 可综合性强:纯Verilog实现,无黑盒依赖;

相比之下,VexRiscv功能更强(支持缓存、多级流水),但也更复杂。对于初学者来说,PicoRV32就像一辆没有空调和倒车雷达的手动挡小车——麻雀虽小,五脏俱全,适合练手。

而且它的源码风格极其清晰,比如这段关键逻辑:

always @(posedge clk) begin if (reset) begin pc <= 32'h00000000; end else if (trap) begin pc <= trap_addr; end else begin pc <= next_pc; end end

看到没?上电直接跳0x0000_0000——这正是我们要利用的“启动向量”。只要在这个地址放上正确的引导代码,就能控制整个系统的命运。


总线怎么连?别再手动接信号了!

早期我尝试过把CPU的地址线、数据线一根根接到ROM、RAM、UART上……结果改个外设就得重连线,错一个bit系统就挂。

后来才明白:必须引入统一的片上总线协议

我们选用的是Wishbone总线,理由很简单:

  • 结构简单:主从模式,信号少(ADR、DAT、WE、STB、ACK);
  • 社区成熟:有现成的交叉开关(Crossbar)和地址译码器IP;
  • 易于调试:每个事务都有明确的握手过程;

举个例子,当CPU访问0x2000_0000时,我们的地址译码器会判断:

这个地址属于UART控制器范围 → 拉高其片选信号 → 数据走UART的数据通道 → 完成一次写操作

不需要CPU知道具体物理连接,只需要约定好“地址空间地图”。

下面是我们在FPGA中实际使用的内存映射表:

地址区间设备大小用途说明
0x0000_0000~0x0000_FFFFBoot ROM64KB存放启动代码(.text段)
0x1000_0000~0x1000_7FFFSRAM32KB程序运行时数据(.data/.bss)
0x2000_0000~0x2000_0FFFUART Control4KBMMIO寄存器空间
0x3000_0000~0x3000_0FFFGPIO4KBLED/按键控制

这种划分方式遵循哈佛架构思想:指令与数据分离,避免冲突,提升效率。


ROM和SRAM怎么实现?别忘了对齐和初始化!

FPGA上的存储资源靠Block RAM(BRAM)生成。Xilinx Spartan-7这类芯片通常提供几十到上百个BRAM块,足够支撑最小系统。

Boot ROM:你的第一段代码住哪?

Boot ROM存放的是最开始执行的机器码。我们需要做两件事:

  1. 编写启动代码(start.s):
    ```assembly
    .section .text.startup
    .globl _start

_start:
li sp, 0x10008000 # 设置栈指针指向SRAM顶部
call main # 跳转main函数
```

  1. 编译并转换为coe文件,供Xilinx IP核加载:
    bash riscv-none-embed-gcc -T linker.ld -o firmware.elf start.s main.c riscv-none-embed-objcopy -O verilog firmware.elf boot_rom.v

注意:RISC-V要求所有32位访问四字节对齐。如果你试图从0x0000_0001取指令,会触发指令地址未对齐异常。所以链接脚本里.text段起始地址一定要是4的倍数。

SRAM:变量和堆栈的家

SRAM用于存放全局变量、堆、栈。我们用双端口RAM实现,允许CPU同时读写。

关键点在于.bss段清零——这是C语言运行的前提。我们在进入main()之前,必须手动把.bss区域置零:

extern unsigned _sbss, _ebss; void clear_bss() { unsigned *p = &_sbss; while (p < &_ebss) *p++ = 0; }

否则你会发现:int flag;居然不是0!


时钟和复位:别小看那几行Verilog

很多初学者忽略这点,直接把外部晶振连给CPU——结果上电瞬间乱跑指令,死机。

正确做法是:

1. 使用PLL稳定时钟

FPGA内部使用DCM或MMCM锁相环,将输入25MHz晶振倍频至50MHz或100MHz,供给系统使用。

2. 实现“同步释放”的复位机制

推荐使用双触发器同步法处理异步复位:

reg [1:0] rst_sync = 0; always @(posedge clk) rst_sync <= {rst_sync[0], ~btn_rst}; wire sys_rst_n = rst_sync[1];

再加上延时计数器,确保电源稳定后再释放复位:

reg [15:0] power_on_cnt = 0; wire release_rst = (power_on_cnt == 16'd50000); // 延迟约1ms @50MHz always @(posedge clk) begin if (!sys_rst_n) power_on_cnt <= 0; else if (power_on_cnt != 16'hFFFF) power_on_cnt <= power_on_cnt + 1; end assign cpu_reset = ~release_rst;

这样就能有效防止“电源还没上来,CPU就开始跑了”的经典坑。


最实用的外设:UART调试输出

没有输出的系统等于黑盒。哪怕只是点亮LED,你也无法确认它是卡在初始化还是正常运行。

所以我们必须加上UART串口,用来输出调试信息。

UART控制器怎么做?

核心模块包括:

  • 波特率发生器:系统时钟分频得到16倍采样时钟(如115200bps → 1.8432MHz)
  • 发送FIFO:缓冲待发字符,减少CPU轮询负担
  • 内存映射寄存器
  • TXDATAat offset 0:写入即发送
  • RXDATAat offset 4:读取接收到的字节
  • STATUSat offset 8:包含 TXFULL/RXEMPTY 标志

C语言输出字符串(轮询版)

适用于无中断的最小系统:

#define UART_BASE 0x20000000 #define UART_TXDATA (*(volatile uint32_t*)(UART_BASE)) #define UART_STATUS (*(volatile uint32_t*)(UART_BASE + 8)) void uart_putc(char c) { while (UART_STATUS & (1 << 31)); // 等待发送缓冲区非满 UART_TXDATA = c; } void print_str(const char* s) { while (*s) uart_putc(*s++); } int main() { print_str("Hello RISC-V!\n"); while(1); return 0; }

烧录后用USB-TTL线连接PC,打开串口助手,就能看到输出:

Hello RISC-V!

那一刻的感觉,堪比第一次点亮LED。


工具链怎么配?别被名字吓住

编译RISC-V程序要用专用交叉编译器。推荐安装:

# Ubuntu下安装 sudo apt install gcc-riscv64-unknown-elf # 或使用xPack提供的版本 riscv-none-embed-gcc --version

然后写一个简单的链接脚本linker.ld

ENTRY(_start) MEMORY { rom : ORIGIN = 0x00000000, LENGTH = 64K ram : ORIGIN = 0x10000000, LENGTH = 32K } SECTIONS { .text : { *(.text.startup) *(.text) } > rom .rodata : { *(.rodata*) } > rom .data : { *(.data*) } > ram .bss : { _sbss = .; *(.bss*) _ebss = .; } > ram }

最后一键生成bin文件:

riscv-none-embed-gcc -march=rv32imc -mabi=ilp32 \ -T linker.ld -o firmware.elf start.s main.c riscv-none-embed-objcopy -O binary firmware.elf firmware.bin

把这个firmware.bin通过iMPACT或Vivado烧进FPGA的ROM位置,上电即可运行。


遇到问题怎么办?几个常见“坑”提醒你

❌ 串口没输出?

  • 检查UART基地址是否匹配;
  • 波特率设置是否准确(常用115200);
  • TX引脚有没有接反;
  • 是否忘了调用clear_bss()导致程序崩溃;

❌ 程序跑飞?

  • 查PC初始值是不是0x0000_0000
  • ROM内容有没有正确加载;
  • 复位是否同步释放;
  • 栈指针是否指向合法SRAM区域;

❌ 编译报错“undefined reference to main”?

  • 确保_start符号被正确链接;
  • 启动文件要放在第一个编译;
  • 检查.text.startup段是否被包含;

建议先在ModelSim中仿真验证基本通信流程,再下载到硬件。


可以继续往哪走?这不是终点

你现在拥有的,不仅仅是一个能打印“Hello”的玩具系统,而是一个可无限扩展的SoC骨架

下一步你可以轻松加入:

  • PLIC中断控制器:支持外部中断;
  • Machine Timer:实现sleep()延时;
  • SPI Flash控制器:加载更大固件;
  • 自定义协处理器:加速特定算法;
  • LiteX框架集成:自动生成SoC结构;
  • 运行FreeRTOS甚至Linux(需要MMU支持);

事实上,像 Sipeed Tang Primer 这样的国产开发板,底层就是基于类似结构运行OpenSBI + Linux。


写在最后:动手才是最好的学习

RISC-V的魅力,不在于它的指令有多简洁,而在于你可以完全掌控每一层逻辑

ARM给你一个PDF手册和一堆.bin文件,你说不清它内部发生了什么;
但PicoRV32是一行行看得见的Verilog代码,每一个触发器都在你掌控之中。

当你亲手把CPU、总线、内存、外设连在一起,并看到屏幕上跳出那句“Hello RISC-V”时,你会突然理解什么叫“计算机系统”。

这不是模拟器里的虚拟机,是你造出来的世界第一条指令。

如果你正在寻找这样一个起点,不妨试试照着本文搭建一遍。我已经把完整的RTL、Testbench、Makefile整理好,放在GitHub仓库中(文末可获取)。

欢迎你在评论区分享你的第一次“上电成功”时刻。


附:项目资源推荐

  • PicoRV32源码:https://github.com/cliffordwolf/picorv32
  • 开源工具链:https://xpack.github.io/riscv-none-embed-gcc/
  • FPGA开发板推荐:Digilent Nexys A7 / Lichee Tang Mini
  • SoC生成框架:https://github.com/enjoy-digital/litex (进阶必学)

真正的理解,始于你按下“Run”的那一刻。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/7 4:04:30

提高效率:Arduino IDE为ESP32定制编译选项的完整示例

如何用platform.local.txt深度定制 ESP32 编译流程&#xff1f;实战指南你有没有遇到过这样的情况&#xff1a;写完一个功能丰富的 Arduino 项目&#xff0c;点击“上传”&#xff0c;结果 IDE 弹出错误&#xff1a;“固件太大&#xff0c;无法烧录&#xff01;”或者你想用std…

作者头像 李华
网站建设 2026/2/5 16:50:35

PaddlePaddle镜像中的标签平滑(Label Smoothing)作用解析

PaddlePaddle中的标签平滑&#xff1a;从原理到工业实践 在现代深度学习训练中&#xff0c;一个看似微小的技巧——将真实类别标签从“1.0”轻轻往下调一点&#xff0c;竟然能显著提升模型在线上环境的真实表现。这听起来有些反直觉&#xff1a;我们教模型识别猫的时候&#xf…

作者头像 李华
网站建设 2026/2/3 12:57:31

Windows桌面美化终极指南:TranslucentTB任务栏透明完全教程

Windows桌面美化终极指南&#xff1a;TranslucentTB任务栏透明完全教程 【免费下载链接】TranslucentTB 项目地址: https://gitcode.com/gh_mirrors/tra/TranslucentTB 你是否厌倦了Windows系统千篇一律的灰色任务栏&#xff1f;想要让桌面焕然一新却不知从何入手&…

作者头像 李华
网站建设 2026/2/3 18:06:32

DDR4基础扫盲

Write Leveling&#xff08;写均衡&#xff09;为了解决高速数据传输时时钟和数据信号不同步的问题&#xff0c;确保数据能被准确采样。它的核心原理是PHY通过动态调整数据选通信号&#xff08;DQS&#xff09;的相位&#xff0c;使其与时钟信号&#xff08;CK&#xff09;的上…

作者头像 李华
网站建设 2026/2/3 14:57:06

专业仿写文章Prompt

专业仿写文章Prompt 【免费下载链接】xnbcli A CLI tool for XNB packing/unpacking purpose built for Stardew Valley. 项目地址: https://gitcode.com/gh_mirrors/xn/xnbcli 任务要求&#xff1a; 基于给定的技术文章&#xff08;关于xnbcli工具&#xff09;创作一篇…

作者头像 李华
网站建设 2026/2/6 9:20:48

Windows平台Poppler终极指南:5分钟掌握PDF高效处理技巧

Windows平台Poppler终极指南&#xff1a;5分钟掌握PDF高效处理技巧 【免费下载链接】poppler-windows Download Poppler binaries packaged for Windows with dependencies 项目地址: https://gitcode.com/gh_mirrors/po/poppler-windows Poppler for Windows是一款专为…

作者头像 李华