☰
RISC-V AXI Verilog源码实战:从仿真到上板的CPU核开发指南
2026/10/19 9:22:33 网站建设 项目流程

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

1. 从一份 RISC-V AXI Verilog 源码说起:这颗 CPU 到底能跑什么

如果你手头正好有一份 RISC-V 核的 Verilog 源码,里面还带着 AXI 总线接口,大概率会经历三个阶段:先兴奋,再迷茫,最后卡在仿真波形里出不来。这份源码解决的就是这个场景——它把一颗基于 RISC-V 指令集的处理器核,连同 AXI4 总线接口,用可综合的 Verilog RTL 完整写了出来。你拿到的不是一份文档,而是一堆.v文件,里面有取指、译码、执行、访存、写回,还有一条能把 CPU 挂到 SoC 上的 AXI 通道。

它适合两类人:一类是想搞清楚 CPU 内部流水线到底怎么用硬件描述语言落地的学生或转行者,另一类是要把 RISC-V 核集成进自己 SoC 里、需要一套能改能仿真的 RTL 原型的工程师。关键词里的 risc-v、axi、verilog、cpu 四个词,基本就是这份源码的全部骨架。接下来我不讲指令集发展史,只讲这份代码怎么读、怎么跑、怎么改。

2. 架构拆解:从取指到 AXI 写回的完整数据通路

2.1 顶层模块与流水线级划分

拿到源码先别急着看叶子模块,直接打开顶层文件。常见做法是顶层只做三件事:例化流水线各级、连接 AXI 主机接口、引出调试信号。一个典型的 RISC-V 核顶层会长这样:

module riscv_core_top #( parameter XLEN = 32, // 数据位宽,32 位 RV32I parameter RESET_ADDR = 32'h8000_0000 // 复位后取指地址 )( input wire clk, input wire rst_n, // AXI4 主机接口(简化信号) output wire [31:0] awaddr, output wire awvalid, input wire awready, output wire [31:0] wdata, output wire wvalid, input wire wready, input wire [31:0] rdata, input wire rvalid, output wire rready ); // 流水线级间信号 wire [31:0] pc_if, instr_if; wire [31:0] pc_id, instr_id; wire [31:0] alu_result_ex; wire [31:0] mem_data_mem, wb_data_wb; // 五级流水:IF / ID / EX / MEM / WB if_stage u_if ( .clk(clk), .rst_n(rst_n), .pc_out(pc_if), .instr_out(instr_if) ); id_stage u_id ( .clk(clk), .rst_n(rst_n), .pc_in(pc_if), .instr_in(instr_if), .pc_out(pc_id), .instr_out(instr_id) ); ex_stage u_ex ( /* 省略端口连接 */ ); mem_stage u_mem ( /* 省略端口连接 */ ); wb_stage u_wb ( /* 省略端口连接 */ ); // AXI 适配层:把 MEM 级的访存请求转成 AXI 事务 axi_adapter u_axi ( .clk(clk), .rst_n(rst_n), .mem_req(mem_req), .mem_addr(mem_addr), .awaddr(awaddr), .awvalid(awvalid), .awready(awready), .wdata(wdata), .wvalid(wvalid), .wready(wready), .rdata(rdata), .rvalid(rvalid), .rready(rready) ); endmodule

这段代码的信息量在于:它把 CPU 和总线彻底解耦了。流水线各级只关心自己的输入输出,AXI 适配层单独拎出来,负责把内部的访存请求翻译成 AXI 的读写通道时序。参数XLEN决定数据位宽,RESET_ADDR决定复位向量,这两个是你改配置时最先动的地方。

2.2 AXI 适配层:读写通道的状态机

AXI 的难点不在信号多,而在握手。valid和ready必须同时为高才算一次传输完成,写地址、写数据、写响应三条通道各自独立握手。源码里通常用一个状态机来串:

localparam S_IDLE = 2'd0, S_AW = 2'd1, S_W = 2'd2, S_B = 2'd3; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; awvalid <= 1'b0; wvalid <= 1'b0; end else begin case (state) S_IDLE: if (mem_wr_req) begin awaddr <= mem_addr; awvalid <= 1'b1; state <= S_AW; end S_AW: if (awready) begin awvalid <= 1'b0; wdata <= mem_wdata; wvalid <= 1'b1; state <= S_W; end S_W: if (wready) begin wvalid <= 1'b0; state <= S_B; // 等写响应 end S_B: if (bvalid) state <= S_IDLE; default: state <= S_IDLE; endcase end end

逻辑说明:S_IDLE等内部写请求,一旦来了就把地址推上awaddr并拉高awvalid;S_AW等从机回awready,握手成功后立刻切到写数据;S_W等wready;S_B等写响应bvalid才算一次写完成。参数上,mem_wr_req来自 MEM 级,mem_addr和mem_wdata是内部信号,你改地址映射时只需要动awaddr的拼接逻辑。

注意:AXI 的valid一旦拉高,在ready到来之前不能撤销,这是协议硬性要求,仿真里如果看到valid中途掉下去,基本就是状态机写错了。

2.3 目录结构与模块职责

一份组织良好的 RTL 源码,目录通常按功能分层,而不是按文件类型堆在一起。常见结构如下:

目录职责典型文件
rtl/core/流水线各级if_stage.v、id_stage.v、ex_stage.v
rtl/axi/AXI 适配与仲裁axi_adapter.v、axi_arbiter.v
rtl/common/通用单元alu.v、regfile.v、decoder.v
tb/测试平台tb_top.v、mem_model.v
sim/仿真脚本run.do、Makefile
syn/综合约束constraints.sdc

这个划分的好处是:你调流水线只进core/,调总线只进axi/,改指令译码只动decoder.v。寄存器堆regfile.v和 ALUalu.v被单独抽出来,是因为它们会被多个流水级复用,放common/里避免循环依赖。

3. 跑起来:仿真环境搭建与第一条指令的波形

3.1 仿真工具选择与脚本

RTL 仿真绕不开工具。常见做法是用 Verilator 做快速功能仿真,或者用 Icarus Verilog 做轻量验证,商业工具则用 VCS、QuestaSim。源码里如果带了Makefile,一般长这样:

SIM ?= verilator TOP := tb_top SRC := $(wildcard rtl/core/*.v) $(wildcard rtl/axi/*.v) $(wildcard rtl/common/*.v) TB := tb/tb_top.v verilator: verilator --cc --exe --build -Wall $(SRC) $(TB) --top-module $(TOP) icarus: iverilog -g2012 -o sim.out $(SRC) $(TB) vvp sim.out

参数说明:SIM是工具选择开关,SRC用通配符把三个目录的 RTL 全收进来,TB指定测试平台。-g2012是 Icarus 支持 SystemVerilog 子集的开关,不加会报语法错。跑make icarus就能出波形文件,用 GTKWave 打开。

3.2 测试平台与内存模型

CPU 要跑起来,必须有个地方放指令。测试平台里通常挂一个行为级内存模型,响应 AXI 读请求:

// 简化内存模型:收到 AR 就返回预置指令 always @(posedge clk) begin if (arvalid && arready) begin rdata <= mem[araddr[15:2]]; // 按字寻址 rvalid <= 1'b1; end else if (rvalid && rready) begin rvalid <= 1'b0; end end // 预置一段 RV32I 指令:addi x1, x0, 10 initial begin mem[0] = 32'h00A00093; // addi x1, x0, 10 mem[1] = 32'h00100093; // addi x1, x1, 1 mem[2] = 32'h0000006F; // jal x0, 0(死循环) end

逻辑说明:内存模型在地址读通道握手时把预置指令推上rdata,CPU 取指级拿到后开始译码执行。mem[0]那条addi的机器码00A00093是 RV32I 标准编码,你可以用在线汇编器换任意指令。参数araddr[15:2]是因为按字对齐,低两位忽略。

3.3 看波形的正确姿势

仿真跑起来后,波形里信号几百条,新手容易看花眼。我一般先盯四个信号:pc_if、instr_if、state(AXI 状态机)、awvalid/awready。如果pc_if一直停在RESET_ADDR不动,说明取指握手没成功,回去查arvalid有没有拉高;如果pc_if在跳但结果不对,重点看译码输出的alu_op和寄存器写回地址。

提示:第一次跑建议把仿真时间设短一点,比如 1000 个时钟周期,先确认复位释放后 PC 能正常递增,再逐步加长看完整程序流。

4. 避坑与排查:仿真能过但上板就挂的五个原因

4.1 复位释放时机不对

现象:仿真里 PC 正常跳,综合上板后 CPU 不取指。原因:复位信号是异步的,但释放时没有和时钟同步,导致流水线各级复位状态不一致。解决:在顶层加一个复位同步器,用两级触发器把rst_n同步到clk域再分发。

4.2 AXI valid 中途撤销

现象:仿真波形里awvalid拉高几个周期后自己掉下去了,从机没来得及回ready。原因:状态机在S_AW里加了超时跳转,或者组合逻辑里awvalid依赖了一个会变的信号。解决:valid必须寄存输出,且只在握手完成后清零。

4.3 寄存器堆写回冲突

现象:连续两条指令写同一个寄存器,结果第一条的值被覆盖。原因:WB 级和 MEM 级同时写寄存器堆,没有做写端口仲裁。解决:要么加旁路,要么在 regfile 里做优先级编码,WB 级优先。

4.4 指令存储器地址非对齐

现象:取指地址是奇数,内存模型返回错误数据。原因:RISC-V 指令必须 4 字节对齐,PC 低两位应该恒为 0。解决:在 IF 级把pc_if[1:0]强制置零,或者复位地址就设成 4 的倍数。

4.5 综合工具把调试信号优化掉

现象:上板后想抓内部信号,发现波形里全是 0。原因:综合工具把没有负载的信号优化了。解决:给关键调试信号加(* keep = "true" *)属性,或者在综合脚本里设-preserve选项。

5. 二次开发:改指令、扩总线、加外设的实操路径

5.1 增加一条自定义指令

RISC-V 的好处是编码空间留了自定义指令段。假设你要加一条crc32指令,走 OP-CUSTOM-0 编码空间。第一步改译码器:

// decoder.v 里增加分支 localparam OP_CUSTOM0 = 7'b0001011; // 自定义指令 opcode always @(*) begin case (opcode) OP_CUSTOM0: begin alu_op = ALU_CRC32; reg_wr = 1'b1; alu_src = 1'b0; // 用 rs1 和 rs2 end // ... 其他指令 endcase end

第二步在 ALU 里实现ALU_CRC32的组合逻辑,第三步在测试平台里加一条自定义指令的机器码验证。参数上,OP_CUSTOM0的 opcode 是 RISC-V 规范预留的,不会和标准指令冲突。

5.2 把 AXI 从 32 位扩到 64 位

数据位宽扩展主要动三个地方:XLEN参数、AXI 数据信号位宽、以及访存级的拼接逻辑。常见做法是把wdata和rdata改成[63:0],然后在 MEM 级根据地址低位决定用高 32 位还是低 32 位。注意 AXI 的wstrb也要同步扩到 8 位,否则字节选通会错。

5.3 挂一个 UART 外设验证总线

最直接的验证方式是在 AXI 总线上挂一个 UART 从机,地址映射到0x1000_0000。CPU 往这个地址写数据,UART 打印到终端。这样你就能用一条sw指令验证整条写通道:取指、译码、执行、AXI 写地址、写数据、写响应,全链路跑通。

5.4 验证方法:从定向测试到随机指令

定向测试只能覆盖你想到的场景。进阶做法是写一个随机指令生成器,往内存里灌随机 RV32I 指令,跑几千个周期后比对寄存器堆快照和参考模型。参考模型可以用 Spike 或者自己写一个行为级 ISS。这一步做完,你才算真正信任这颗核。

从那以后我每次改完 RTL,都强制走一遍「定向测试 → 随机回归 → 波形抽查」三步,不再靠肉眼盯波形下结论。希望这份源码在你手里,能从一堆.v文件变成一颗真正跑起来的核。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询