简介:本资源为基于Verilog的RISC-V处理器与AXI总线接口RTL源代码,面向具备数字逻辑基础、希望深入理解处理器微架构与片上总线协议的硬件工程师及高校学生。包内以SiFive U54MC多核RISC-V处理器IP的AXI4接口实现为核心,涵盖指令集扩展、总线读写事务、突发传输与响应机制、地址解码及中断处理等关键模块,可帮助读者对照源码掌握RTL设计、功能仿真与时序验证的完整流程。资源共1463个文件,以414个SystemVerilog、399个Verilog源文件为主,辅以151个C头文件、111个C源文件及大量Makefile、脚本与文档,压缩包约4.69MB,目录结构清晰便于按模块检索。目前已有1022人学习下载,适合作为RISC-V与AXI总线协议学习及硬件设计能力提升的实践参考。
1. 从一份 RISC-V AXI Verilog 源码包说起:它到底能跑什么
如果你手头正好有一块带 AXI 总线的 FPGA 板子,又不想从零去啃指令集手册,那这份 RISC-V AXI RTL Verilog 源代码包大概率能让你少熬几个通宵。它本质上是一套用 Verilog 写的处理器核 RTL,内部实现了 RISC-V 指令流水线,外部通过 AXI4 接口挂载存储器和外设,风格上参考了业界常见的开源 RISC-V 核设计思路,比如 SiFive 系列核的总线组织方式。换句话说,你拿到的不是一份纸面规范,而是一堆能综合、能仿真、能上板的 .v 文件。
它解决的核心问题是:让你在 FPGA 上快速搭出一个可运行的 RISC-V CPU 系统,而不是花几周时间去调流水线冒险和总线握手。适合谁?一类是做嵌入式 SoC 验证的工程师,想拿一个现成的 AXI 主机模型去测自己的互联矩阵;另一类是高校里做处理器架构实验的同学,需要一份结构清晰、模块划分明确的 RTL 来改流水线、加自定义指令。前提是你得懂 Verilog 基本语法,知道always @(posedge clk)在干什么,否则后面仿真跑起来也是一头雾水。
2. 拆开源码包:模块划分与 AXI 总线握手逻辑
2.1 顶层文件与目录结构怎么读
拿到源码包,第一件事不是急着打开仿真器,而是先把目录树看一遍。常见的组织方式是按功能分目录:core/放流水线主体,axi/放总线接口转换逻辑,periph/放 UART、GPIO 这类外设,sim/放 testbench 和仿真脚本,constraints/放引脚约束。顶层文件一般叫riscv_top.v或soc_top.v,里面做的事就三件:例化 CPU 核、例化 AXI 互联、把外设挂到总线上。
我一般会先看顶层例化关系,画一张模块连接草图。重点确认三件事:CPU 的指令和数据接口是分开的还是统一的、AXI 互联是几主几从、时钟域有没有跨。很多新手翻车就翻在没注意时钟域,仿真波形里信号全是红 X,查半天以为是复位没接对,其实是 AXI 跨时钟域握手没做同步。
# 常见做法是先统计文件数量和模块名,心里有个数 find ./rtl -name "*.v" | wc -l grep -rn "^module" ./rtl --include="*.v" | awk '{print $2}' | sort上面两条命令,第一条数 RTL 文件总数,第二条把所有模块名提取出来排序。逻辑说明:先摸清规模,再确认模块命名是否规范。参数说明:-name "*.v"限定只找 Verilog 文件,--include同理,避免把 testbench 里的 module 混进来。如果模块名里出现_tb后缀,那是仿真用的,综合时不要加进去。
2.2 AXI4 握手通道:valid/ready 到底谁等谁
AXI4 最核心的机制就是五通道独立握手:读地址、读数据、写地址、写数据、写响应。每个通道都用valid和ready两根信号做流控,规则只有一条——发送方拉高valid后必须保持数据稳定,直到接收方拉高ready完成一次传输。听起来简单,但写 RTL 时最容易出的 bug 就是:发送方看到ready还没来就把valid撤了,或者接收方ready依赖valid组合逻辑产生,导致死锁。
// AXI 写地址通道握手片段(简化示意) always @(posedge clk or negedge rst_n) begin if (!rst_n) begin awvalid <= 1'b0; end else if (awvalid && awready) begin awvalid <= 1'b0; // 握手完成,撤销 valid end else if (start_write) begin awvalid <= 1'b1; // 有新写请求,拉高 valid awaddr <= write_addr; // 地址必须与 valid 同拍稳定 end end这段代码的逻辑是:只有在awvalid和awready同时为高的那一拍,才认为握手完成,下一拍撤销awvalid。参数说明:awaddr必须在awvalid拉高的同一拍就赋值,不能等到下一拍,否则违反 AXI 协议。常见错误是把awready直接接到!awvalid,这种组合逻辑在跨模块时极易产生毛刺,仿真能过,上板就挂。
提示:AXI 协议里
valid一旦拉高就不能依赖ready来保持,反过来ready可以等valid。记不住就记一句话——valid 不能等 ready,ready 可以等 valid。
2.3 流水线冒险与 AXI 访存时序的配合
RISC-V 核内部一般有五级流水:取指、译码、执行、访存、写回。访存阶段要发 AXI 读请求,但 AXI 读数据回来有延迟,可能是几拍,也可能几十拍。如果流水线不做暂停,取回来的数据就对不上指令了。常见做法是在访存阶段加一个状态机,发出读请求后把流水线冻结,等rvalid回来再放行。
// 访存暂停逻辑示意 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin stall <= 1'b0; end else if (mem_read_req && !rvalid) begin stall <= 1'b1; // 读请求已发,数据未回,暂停流水 end else if (rvalid) begin stall <= 1'b0; // 数据回来,解除暂停 end end逻辑说明:mem_read_req是访存阶段发出的读请求脉冲,rvalid是 AXI 读数据通道的有效信号。参数说明:stall信号要接到流水线寄存器的使能端,低有效还是高有效取决于你的流水线设计,别接反了。这里有个坑:如果 AXI 互联插入的等待周期不固定,stall拉高的时间也不固定,仿真时要用带延迟的存储器模型,不能用零延迟的理想模型,否则上板必翻车。
3. 从仿真到上板:一套可复现的验证流程
3.1 用 Verilator 或 Icarus 跑通第一个 testbench
源码包里通常自带一个简单的 testbench,往指令存储器里预装几条指令,跑几个周期看波形。我一般先用 Icarus Verilog 做快速语法检查,因为它轻量、启动快;确认没问题再用 Verilator 做周期精确仿真,速度快很多。
# 用 Icarus 编译并运行仿真 iverilog -o sim.out -I ./rtl ./rtl/*.v ./sim/tb_top.v vvp sim.out # 生成波形后用 GTKWave 查看 gtkwave dump.vcd &逻辑说明:第一条命令把所有 RTL 和 testbench 一起编译成sim.out,-I指定头文件搜索路径。第二条命令运行仿真,生成dump.vcd波形文件。参数说明:如果你的 testbench 里用到了$dumpfile和$dumpvars,波形会自动生成;如果没有,需要在 testbench 里手动加。常见问题是编译报错找不到模块,多半是文件顺序不对或漏了某个 .v 文件,用find命令再核对一遍。
3.2 上板前的约束文件与引脚分配
仿真跑通只是第一步,上板才是真正的考验。约束文件一般分两类:时序约束和引脚约束。时序约束告诉综合工具时钟频率是多少,引脚约束告诉工具哪个信号接到哪个物理引脚。很多新手拿到源码包直接综合,结果报一堆时序违例,原因就是约束文件里的时钟频率写得太高,比如写 100MHz,但你的设计实际只能跑 50MHz。
# 常见时序约束片段(XDC 风格) create_clock -period 20.000 -name sys_clk [get_ports clk] set_input_delay -clock sys_clk 2.000 [get_ports rst_n] set_output_delay -clock sys_clk 3.000 [get_ports uart_tx]逻辑说明:create_clock定义时钟周期,20ns 对应 50MHz。set_input_delay和set_output_delay告诉工具外部信号的到达和建立时间。参数说明:周期值要根据你的板子晶振和 PLL 配置来改,别照抄。如果综合后时序违例集中在 AXI 互联部分,优先检查是不是跨时钟域没做同步,而不是盲目降频。
注意:上板前务必确认约束文件里的引脚编号和你的板子原理图一致,接错引脚轻则功能不跑,重则烧外设。
3.3 用 AXI 总线抓包定位访存异常
上板后如果程序跑飞,最有效的排查手段是抓 AXI 总线波形。常见做法是用 FPGA 厂商的集成逻辑分析仪,把 AXI 五通道的 valid/ready 信号都挂上去,触发条件设成awvalid && !awready持续超过 100 个周期,这通常意味着互联死锁。
| 信号名 | 含义 | 异常表现 | 排查方向 |
|---|---|---|---|
| awvalid | 写地址有效 | 长期为高不撤销 | 从设备未响应,检查地址映射 |
| wvalid | 写数据有效 | 与 awvalid 不同步 | 写数据通道独立,需分别握手 |
| bready | 写响应就绪 | 长期为低 | 主机未准备好接收响应 |
| arvalid | 读地址有效 | 与 rready 同时为低 | 双方都在等对方,死锁 |
| rlast | 读数据最后一拍 | 不拉高 | 突发长度配置错误 |
表格里最需要盯的是arvalid和rready同时为低的情况,这是典型的死锁特征。解决方法是检查互联逻辑里有没有组合环路,把rready改成寄存器输出通常能解。
4. 避坑与常见问题排查:那些仿真过但上板挂的瞬间
4.1 现象:仿真波形全红 X,复位后也不消失
原因:复位信号没有正确同步到所有寄存器,或者 testbench 里复位释放时间太早,时钟还没稳定就撤了复位。解决:检查复位树,确保每个模块的复位都来自同一个同步复位源;testbench 里复位至少保持 10 个时钟周期再释放。
4.2 现象:AXI 读数据偶尔错一拍,概率极低
原因:跨时钟域没有做同步处理,或者rvalid和rdata的时序不匹配。解决:在跨时钟域路径上加两级触发器同步,rdata用rvalid打拍后采样。这种问题仿真时因为延迟模型理想,很难复现,上板后温度一变就出来。
4.3 现象:综合报时序违例,集中在 AXI 互联模块
原因:组合逻辑路径太长,valid/ready 信号穿过了多级互联。解决:在互联模块里插入流水寄存器,把长路径打断;或者降低时钟频率先跑通,再逐步优化。
4.4 现象:程序跑飞,但抓波形发现 AXI 握手正常
原因:指令存储器初始化文件路径不对,CPU 取到的全是零或随机值。解决:检查$readmemh的文件路径和格式,确保十六进制文件每行一个指令字,且位宽匹配。
4.5 现象:UART 输出乱码,但 CPU 仿真正常
原因:波特率分频系数算错,或者时钟频率和约束文件不一致。解决:重新计算分频系数,用示波器量一下实际时钟频率,别信综合报告里的标称值。
5. 进阶用法:把 AXI 主机模型换成自定义外设
源码包跑通之后,最有价值的玩法是把 AXI 互联上的某个从设备换成你自己写的外设。比如挂一个自定义的 PWM 控制器,或者一个简单的 DMA 引擎。具体做法是:先复制一份现成的从设备 RTL,改地址译码范围,再改寄存器读写逻辑,最后在顶层例化时把地址映射改掉。
// 自定义从设备地址译码片段 always @(*) begin case (awaddr[15:12]) 4'h1: sel_pwm = 1'b1; // 0x1000 段给 PWM 4'h2: sel_dma = 1'b1; // 0x2000 段给 DMA default: begin sel_pwm = 1'b0; sel_dma = 1'b0; end endcase end逻辑说明:用地址的高 4 位做片选,每个从设备占 4KB 空间。参数说明:awaddr位宽取决于你的地址总线宽度,别照抄。改完之后一定要重新跑一遍仿真,重点看新外设的ready信号会不会拖死总线。
验证方法我一般走三步:第一步,用 testbench 直接往新外设地址写读,看寄存器值对不对;第二步,把 CPU 程序改成访问新外设,跑一个简单循环;第三步,上板用逻辑分析仪抓新外设的片选和读写信号,确认时序余量。从那以后我每次改 AXI 互联,都强制走一遍这三步,再也不敢跳过仿真直接上板。希望帮到你。
本文还有配套的精品资源,点击获取