ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RISC-V AXI Verilog源码实战:从仿真到SoC集成

RISC-V AXI Verilog源码实战:从仿真到SoC集成

简介:本资源为基于Verilog的RISC-V处理器与AXI总线RTL源代码包,面向数字IC设计、FPGA开发及嵌入式系统学习者,尤其适合希望深入理解处理器微架构与片上总线接口的中高级工程师。包内以SiFive U54MC多核RISC-V处理器IP的AXI4接口实现为核心,涵盖RISC-V基础指令集与扩展、AXI4读写事务与突发传输机制、Verilog模块化建模、RTL综合与仿真验证等关键知识点。资源共1463个文件,以414个SystemVerilog、399个Verilog源文件为主体,辅以151个C/C++头文件、111个C源文件及大量Makefile、脚本与文档,压缩包约4.69MB,目录结构完整,便于按模块检索与工程化学习。目前已有1022人学习下载。通过研读可掌握处理器AXI接口的地址解码、命令生成、数据读写控制与错误处理,并借助测试平台理解功能验证与覆盖率评估流程,对提升硬件设计与SoC集成能力具有实际参考价值。

1. 从一份 RISC-V AXI Verilog 源码说起:为什么我建议你亲手跑一遍

如果你正在找一份能直接上手、结构清晰的 RISC-V 处理器 RTL 源码,那这份基于 Verilog 实现的 RISC-V AXI 总线互联项目值得你花一个周末拆一拆。它不是纸上谈兵的架构图,而是一套可综合、可仿真的完整 RTL 代码,核心在于把 RISC-V 核、AXI 总线互联和存储子系统串成了一条能跑通的链路。适合谁?正在学 SoC 集成的 IC 方向学生、想从 ARM 生态切到 RISC-V 的 FPGA 工程师、以及需要一套干净参考设计做二次开发的老手。你拿到手就能用 Verilog 仿真器跑通一个最小系统,看到 CPU 通过 AXI 发起读写、拿到返回数据的完整波形。这比看十篇总线协议文章都管用。

2. 项目定位与技术选型:这套 RTL 到底解决了什么问题

2.1 它不是一个 CPU 核,而是一个 SoC 集成骨架

很多人看到“RISC-V 源码”第一反应是找一个处理器核。但这份代码的重点不在核本身,而在于把核、总线、存储和外设粘合起来的集成层。RISC-V 核负责取指、译码、执行,但指令和数据从哪里来、怎么送到核里、多个主设备怎么共享存储,这些才是 SoC 设计里最容易翻车的地方。这套 RTL 用 AXI4 总线作为主干,把处理器核的指令接口和数据接口分别接到 AXI 互联矩阵上,再通过 AXI 转 SRAM 的桥接模块访问片上存储。整个数据通路是完整的,不是零散的 IP 拼凑。

从技术选型看,用 Verilog 而不是 SystemVerilog 做 RTL 描述,说明它更偏向可综合、可移植的工程实现,而不是验证平台。AXI4 而不是 AXI3 或 AHB,意味着它支持突发传输、乱序完成和独立的读写通道,更贴近现代 SoC 的实际需求。存储侧用 SRAM 模型而不是 DDR 控制器,降低了仿真门槛,你不需要外挂复杂的存储模型就能跑通。这种“够用就好”的选型思路,恰恰是学习总线互联最合适的起点。

2.2 目录结构:从 RTL 到仿真的完整组织

拿到源码包后,我一般先看目录结构,因为它直接反映了设计者的模块划分思路。这套代码的组织方式大致如下:

riscv-axi-rtl/ ├── rtl/ │ ├── core/ # RISC-V 核相关 RTL │ ├── axi/ # AXI 互联矩阵与协议转换 │ ├── mem/ # SRAM 控制器与存储模型 │ └── top/ # 顶层模块与系统集成 ├── sim/ │ ├── tb/ # 测试平台 │ └── run/ # 仿真脚本与波形配置 ├── constraints/ # 时序约束文件 └── docs/ # 接口说明与寄存器映射

rtl/core/下是处理器核的取指、译码、执行和寄存器堆模块,通常以core_top.v作为核的顶层。rtl/axi/是重头戏,包含 AXI 互联矩阵axi_interconnect.v、AXI 转 SRAM 桥axi2sram.v以及协议检查模块。rtl/mem/里是 SRAM 的行为级模型和控制器,仿真时直接替换成真实 SRAM 即可。rtl/top/把核、互联、存储串起来,形成soc_top.v。sim/tb/下的测试平台负责产生时钟复位、加载测试程序、发起激励并检查结果。

提示:不同版本的目录名可能有细微差异,但模块划分逻辑基本一致。先找到soc_top.v,顺着端口连线就能理清整个数据通路。

2.3 关键模块的接口约定

在动手改代码之前,必须搞清楚几个核心模块的接口信号。以 AXI 互联矩阵为例,它通常有多个主设备端口和多个从设备端口,每个端口都遵循 AXI4 的五通道结构:读地址通道、读数据通道、写地址通道、写数据通道和写响应通道。每个通道都有valid和ready握手信号,这是 AXI 协议里最容易出问题的地方。

处理器核的指令接口一般配置为只读主设备,数据接口配置为读写主设备。互联矩阵根据地址译码把请求路由到对应的从设备。SRAM 控制器作为从设备,接收 AXI 读写请求并转换为 SRAM 的片选、写使能和地址数据信号。地址映射通常在顶层模块里以参数形式定义,比如指令存储区从0x8000_0000开始,数据存储区从0x8000_0000开始,外设区从0x1000_0000开始。这些参数在soc_top.v里以localparam或parameter声明,改地址映射就是改这几个值。

3. 跑通仿真:从零到波形里看到 AXI 握手

3.1 仿真环境准备与工具选择

跑通这套 RTL 不需要昂贵的商业工具。常见的开源仿真器如 Icarus Verilog 和 Verilator 都能胜任,前者对 Verilog 支持更完整,后者速度更快但需要 C++ 测试平台。我一般先用 Icarus Verilog 跑功能仿真,因为它对 AXI 这种带时序的协议仿真更直观,波形里能看到每个握手信号的变化。

安装 Icarus Verilog 在 Ubuntu 下就是一行命令:

sudo apt-get install iverilog gtkwave

gtkwave用来查看生成的波形文件。如果你用 Windows,可以装 MSYS2 或 WSL,或者用 EDA Playground 在线跑,但本地跑更自由,改代码后重新编译只要几秒。

仿真脚本通常放在sim/run/下,名字可能是run.sh或Makefile。先看一眼脚本内容,确认它调用了哪些源文件、顶层模块名是什么、仿真时间设了多长。常见做法是用一个filelist.f列出所有 RTL 文件,然后iverilog -o sim.out -f filelist.f编译,再vvp sim.out运行。

3.2 编译与运行:一条命令背后的文件依赖

假设目录结构如上所述,编译命令大致如下:

iverilog -g2012 -o sim.out \ -I rtl/core -I rtl/axi -I rtl/mem -I rtl/top \ rtl/core/*.v rtl/axi/*.v rtl/mem/*.v rtl/top/*.v \ sim/tb/tb_soc_top.v

-g2012启用 SystemVerilog 2012 标准,虽然代码是 Verilog,但测试平台可能用了 SystemVerilog 的断言或logic类型。-I指定头文件搜索路径,如果代码里有include语句会用到。最后列出所有 RTL 文件和测试平台文件。编译成功后生成sim.out,用vvp sim.out运行。

运行时会打印仿真日志,通常包括测试用例开始、读写请求发起、数据比对结果。如果测试平台生成了 VCD 波形文件,用gtkwave dump.vcd打开就能看到信号跳变。

注意:如果编译报错“module not found”,先检查filelist.f里是否漏了某个文件,或者模块名和文件名不一致。Verilog 不强制文件名等于模块名,但仿真器按文件名找模块时容易出问题。

3.3 波形解读:一次 AXI 读事务的完整生命周期

跑通仿真后,在波形窗口里添加以下信号:时钟clk、复位rst_n、AXI 读地址通道的arvalid、arready、araddr,读数据通道的rvalid、rready、rdata。你会看到一次典型的 AXI 读事务:

  1. 主设备在arvalid拉高的同时给出araddr,表示读地址有效。
  2. 从设备在准备好接收时拉高arready,两者同时为高的时钟上升沿完成地址握手。
  3. 若干周期后,从设备拉高rvalid并给出rdata,主设备拉高rready表示可以接收。
  4. 两者同时为高的时钟上升沿完成数据握手,一次读事务结束。

如果arvalid一直高但arready一直低,说明从设备没准备好,可能是地址译码没命中或 SRAM 控制器状态机卡住了。如果rvalid迟迟不来,检查从设备是否收到了读请求,以及 SRAM 读延迟是否设得过大。这些排查经验在仿真阶段就能积累,比上板调试省事得多。

3.4 测试平台怎么改:换一个测试程序

测试平台通常用一个initial块加载测试程序到指令存储器,然后释放复位让 CPU 开始取指。测试程序一般以十六进制文件形式存在,比如test.hex,每行一条指令的机器码。如果你想换一个测试程序,比如让 CPU 执行一段简单的加法循环,可以用 RISC-V 工具链编译 C 代码生成hex文件,替换掉原来的。

常见做法是用riscv64-unknown-elf-gcc编译,然后objcopy -O verilog生成十六进制文件。测试平台里的$readmemh任务负责加载:

initial begin $readmemh("test.hex", u_imem.mem_array); end

u_imem是指令存储器的实例名,mem_array是内部存储数组。改测试程序就是改这个文件路径和内容。如果你想验证数据通路的读写,可以在测试平台里直接发起 AXI 读写请求,不经过 CPU,这样更可控。

4. 核心模块拆解:AXI 互联与 SRAM 桥的 RTL 实现

4.1 AXI 互联矩阵的地址译码逻辑

AXI 互联矩阵的核心是地址译码:根据主设备发出的地址,判断该请求应该路由到哪个从设备。下面是一段简化的地址译码逻辑:

// 地址译码:根据 araddr 高位判断从设备 always @(*) begin // 默认不选中任何从设备 slave_sel = 2'b00; if (araddr[31:28] == 4'h8) begin slave_sel = 2'b01; // 0x8000_0000 区域,选中 SRAM end else if (araddr[31:28] == 4'h1) begin slave_sel = 2'b10; // 0x1000_0000 区域,选中外设 end end

araddr[31:28]取地址的高 4 位做粗粒度译码,实际项目中可能用更长的位宽做更细的划分。slave_sel信号控制多路选择器,把读地址通道的信号转发到对应从设备的端口。写地址通道同理,但要注意读写通道是独立的,译码逻辑要分别处理。

参数说明:araddr是 32 位读地址,slave_sel是 2 位从设备选择信号,支持最多 4 个从设备。如果从设备数量超过 4 个,需要加宽slave_sel位宽。地址映射关系通常写在顶层模块的参数里,改映射就是改这里的比较值。

4.2 AXI 转 SRAM 桥的状态机设计

SRAM 接口比 AXI 简单得多:片选、写使能、地址、写数据、读数据。AXI 转 SRAM 桥需要把 AXI 的突发传输拆解成单次 SRAM 访问,或者用 SRAM 的流水线模式支持突发。下面是一个简化的状态机:

// AXI 写事务转 SRAM 写操作的状态机 localparam IDLE = 2'b00; localparam WRITE = 2'b01; localparam RESP = 2'b10; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; end else begin case (state) IDLE: begin if (awvalid && wvalid) begin sram_cs <= 1'b1; sram_we <= 1'b1; sram_addr <= awaddr[15:2]; // 字对齐 sram_wdata<= wdata; state <= WRITE; end end WRITE: begin sram_we <= 1'b0; state <= RESP; end RESP: begin bvalid <= 1'b1; // 写响应有效 if (bready) begin bvalid <= 1'b0; sram_cs<= 1'b0; state <= IDLE; end end endcase end end

这个状态机假设写地址和写数据同时有效,实际 AXI 协议允许它们不同时到达,所以需要加缓冲或握手逻辑。sram_addr取awaddr[15:2]是因为 SRAM 按字寻址,地址低 2 位忽略。sram_cs是片选,sram_we是写使能,sram_wdata是写数据。写响应通道的bvalid在写完成后拉高,等待主设备bready握手。

提示:如果 SRAM 读延迟大于 1 个周期,读数据通道需要加寄存器缓冲,否则rvalid和rdata对不上。

4.3 处理器核的取指与数据接口对接

处理器核通常有两个 AXI 主接口:一个用于取指,一个用于数据读写。取指接口只读,数据接口读写。在顶层模块里,这两个接口分别接到互联矩阵的不同主端口上。取指接口的地址来自程序计数器,数据接口的地址来自执行单元的访存指令。

对接时要注意位宽匹配。如果核的地址是 32 位,AXI 互联也是 32 位,直接连即可。如果核的数据是 32 位,AXI 数据也是 32 位,也没问题。但如果核的数据是 64 位而 AXI 是 32 位,就需要加数据宽度转换逻辑,把一次 64 位访问拆成两次 32 位访问。这套代码里通常保持一致,避免转换开销。

另一个注意点是时钟域。如果核和 AXI 互联在同一个时钟域,直接连。如果跨时钟域,需要加异步 FIFO 或握手同步器。仿真阶段通常用单时钟,上板时如果核跑 50MHz 而 AXI 跑 100MHz,就必须处理跨时钟域问题。

4.4 存储模型与地址映射的修改方法

SRAM 行为级模型通常是一个二维数组:

reg [31:0] mem_array [0:16383]; // 64KB SRAM always @(posedge clk) begin if (sram_cs && sram_we) begin mem_array[sram_addr] <= sram_wdata; end if (sram_cs && !sram_we) begin sram_rdata <= mem_array[sram_addr]; end end

mem_array大小决定 SRAM 容量,16383是深度减一,对应 64KB。改容量就是改这个数字。地址映射在顶层模块里定义,比如指令存储区从0x8000_0000开始,数据存储区从0x8000_0000开始,两者共用同一块 SRAM 的不同区域。如果想让指令和数据分开,可以例化两个 SRAM 模型,分别映射到不同地址段。

改地址映射时,同步修改互联矩阵的译码逻辑和测试平台的地址常量,否则 CPU 取指会跑到错误的位置。常见做法是把地址映射写成parameter,在顶层统一管理,子模块通过参数传递获取。

5. 避坑与常见问题:仿真跑不通时先查这几条

5.1 编译报错“unknown module type”

现象:iverilog编译时提示某个模块未定义,比如axi_interconnect。原因通常是文件列表不全,或者模块名和文件名不一致导致仿真器找不到。解决:用find . -name "*.v"列出所有 RTL 文件,确认filelist.f里没有遗漏。如果模块名和文件名不同,在filelist.f里显式列出文件名,不要依赖自动搜索。

5.2 仿真卡死,波形里没有任何握手

现象:运行vvp sim.out后卡住不动,波形里arvalid一直低或arready一直低。原因可能是复位没释放,或者时钟没产生。解决:检查测试平台的initial块,确认rst_n在若干周期后拉高,clk用always #5 clk = ~clk产生。如果复位释放了但握手不动,检查地址译码是否命中,可以在译码逻辑里加$display打印地址和选中信号。

5.3 读数据全为 X 或 0

现象:波形里rdata一直是x或0,但rvalid有拉高。原因可能是 SRAM 模型没初始化,或者读地址超出了mem_array范围。解决:在测试平台里用$readmemh初始化 SRAM,或者用for循环给mem_array赋初值。检查读地址是否在 SRAM 映射范围内,如果超出范围,互联矩阵可能返回默认值0。

5.4 写事务完成但读不回数据

现象:写操作看起来完成了,bvalid也拉高过,但后续读操作返回的数据不是写入的值。原因可能是写地址和读地址没有对齐到同一字,或者 SRAM 写使能时序不对。解决:确认读写地址都按字对齐,低 2 位为 0。检查 SRAM 写使能是否在时钟上升沿有效,写数据是否在写使能有效时稳定。可以在 SRAM 模型里加$display打印每次读写地址和数据,对比波形定位。

5.5 上板后时序不满足

现象:仿真通过,但综合实现后时序报告显示建立时间违例。原因通常是组合逻辑路径太长,比如地址译码和互联选择逻辑在一个周期内完成。解决:在关键路径上插入寄存器,把地址译码和通道选择分成两级流水。或者降低时钟频率,给组合逻辑更多时间。常见做法是在 AXI 互联矩阵的输入输出加寄存器切片,牺牲一个周期延迟换时序余量。

6. 二次开发:从跑通到改出自己的一套 SoC

跑通仿真只是第一步,真正有价值的是在这套 RTL 上改出你自己的东西。我一般会从三个方向入手:换核、扩总线、加外设。

换核是最直接的改造。这套代码的处理器核是 RISC-V,但接口是标准的 AXI 主接口。你可以把核换成另一个 RISC-V 实现,只要它提供 AXI 主接口,就能直接接到现有的互联矩阵上。如果新核用的是 AHB 或自定义总线,就需要加一个协议转换桥。常见做法是保留互联矩阵和 SRAM 控制器,只替换rtl/core/下的内容,顶层模块的端口连线基本不用动。

扩总线是更有挑战的改造。现有互联矩阵可能只支持 2 个主设备和 2 个从设备,如果你想接入更多主设备(比如 DMA)或更多从设备(比如 UART、SPI),就需要扩展互联矩阵的端口数量和译码逻辑。AXI 互联矩阵的扩展通常用交叉开关(crossbar)结构,每个主设备到每个从设备都有一条独立通路。你可以用现成的 AXI 交叉开关 IP,也可以自己写一个简化的共享总线版本。自己写的好处是可控,坏处是要处理更多的握手和仲裁逻辑。

加外设是最实用的改造。比如加一个 UART,让 CPU 能打印调试信息。UART 的寄存器接口通常是 AXI4-Lite,比完整 AXI4 简单得多。你可以在互联矩阵上挂一个 AXI4-Lite 从设备端口,把 UART 的寄存器映射到某个地址段。CPU 通过读写这些寄存器来发送和接收数据。测试平台里可以模拟 UART 接收,把收到的字符打印到终端。

验证改造是否成功,我习惯用一个小测试程序:让 CPU 通过 UART 打印 “Hello” 然后进入死循环。如果终端能看到 “Hello”,说明取指、数据访问、总线互联和外设接口都通了。这个测试比复杂的基准程序更直接,出问题时也更容易定位是哪一级出了问题。

从那以后我每次改完 RTL,都强制走一遍“编译→仿真→看波形→上板测 UART”的流程,不跳过任何一步。因为很多问题在仿真阶段就能暴露,上板只是确认。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进