尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

手写RISC-V处理器核:AXI接口与五级流水线设计详解

手写RISC-V处理器核:AXI接口与五级流水线设计详解 简介包含完整RTL源码的RISC-V与AXI4总线接口工程源自SiFive U54MC多核处理器设计。它面向希望深入处理器微架构和片上系统互连的硬件开发者、FPGA工程师以及计算机体系结构方向学生尤其适合作为从零搭建处理器数据通路与访存接口的设计参考。压缩包共1463个文件约4.69MB其中核心硬件代码包括414个SystemVerilog文件和399个Verilog文件另配备C/C测试用例、Makefile与configure构建脚本、设备树配置、许可证说明等辅助材料便于在Linux环境中直接编译、仿真与调试。已有1017人学习此代码包。通过研读这份代码可系统性理解RISC-V基础指令与I、M、A、C、F/D各扩展学习AXI4协议的读写事务、突发模式、ID管理及响应机制在RTL设计层面能掌握Verilog模块划分、组合与时序逻辑描述、多核缓存一致性、中断控制和Testbench验证等工程化方法同时还涵盖地址解码、命令生成、数据宽度与地址宽度设置等总线接口细节对深入掌握处理器与总线互连、提升硬件调试能力具有直接参考价值并有助于梳理SoC设计中的常见问题与调试思路。 整理这套RISC-V核的源码时我给自己定了一个规矩所有对外接口只保留AXI内部一条五级流水线底层全部用Verilog手写。之所以定这个规矩是因为市面上标着“RISC-V CPU Verilog源码”的开源项目我几乎翻过一遍要么是教学性质的迷你核只支持二三十条指令走的是自定义简单总线要么是完整度很高的工业级core模块复杂到不适合拿来当入门教材。我想要的是一份中间形态的代码指令集实现够完整、AXI协议握手规范、代码量又能让人在一个月内读完。这份代码主要面向三类人正在做毕业设计或课程项目的学生准备数字IC/FPGA方向面试的求职者以及想在自己的FPGA工程里快速塞一个软核做SoC原型验证的工程师。如果你能看懂简单的Verilog模块知道什么是时钟、复位、状态机那么跟着后面几节内容把这个工程跑起来不会有太大障碍。1. 三个关键词背后的分工指令集、总线协议与硬件描述语言1.1 一套RISC-V Core在SoC里到底扮演什么角色一个SoC内部通常有CPU核、内存控制器、外设控制器、中断控制器它们通过总线互联。RISC-V核负责取指、译码、执行指令是控制中枢AXI作为主流的总线协议负责把CPU对存储和外设的读写请求高效地传出去Verilog则把这些行为描述成可综合的硬件逻辑。这三者缺一不可没有AXI核只能孤立地跑内部寄存器没有RISC-VAXI上的事务缺少指令语义没有Verilog风格的RTL描述前面两者都只是文档和概念。在这套源代码里核与外部世界只有两类交互一类是面向指令的取指请求一类是面向数据读写的Load/Store请求全部通过AXI主接口发出。这种设计最大的好处是CPU核本身不关心外部接的是Block RAM、DDR控制器还是自定义外设只要对方也是AXI从设备就能直接对接。很多初学者会把CPU核和SoC混为一谈实际上核只是SoC里的一个主设备地址译码、仲裁、外设接入都属于核之外的System Bus部分。把这条边界划清楚后面读代码会顺畅很多。1.2 手写Verilog和Chisel生成RTL的差异我为什么选前者经常有人问我Chisel生成RTL这么流行Rocket和BOOM都是这么来的为什么还要手写Verilog我的看法是两者解决的痛点完全不同。Chisel用Scala描述硬件生成出的Verilog代码量通常很大可读性差跑仿真可以做后端综合时一旦出现时序违例定位逻辑路径要翻几十万行生成代码非常痛苦。对中小规模的CPU核来说手写Verilog能把每一级寄存器的边界、每一根握手信号的控制都把握在自己手里综合和PR阶段的优化空间也更大。Chisel的优势在于大规模复杂项目的参数化生成通过重构和高度抽象能减少重复编码但代价是调试时需要同时理解Scala层和生成后的Verilog层。如果你是第一次做RISC-V CPU我强烈建议先用Verilog把流水线和AXI协议吃透。Verilog是IEEE标准所有EDA工具的支持都最成熟任何仿真器、综合工具、第三方IP都能无缝对接这个兼容性优势在团队协作和流片项目中尤其重要。2. RTL整体框架设计五级流水线如何长出一对AXI主接口2.1 模块划分与流水线各级的职责边界这套CPU采用经典五级结构模块名基本都是常规命名ifu指令取指、idu指令译码、exu执行、lsu访存、wbu写回。设计时有一个原则每一级之间只通过valid/ready风格的握手信号交互不允许跨级引用内部信号。很多初学者写CPU到最后代码调不通十有八九是流水线寄存器之间又拉了一根绕来绕去的信号看起来解决了眼前问题实际上把整个流水线的组合逻辑时序完全搞乱。各路模块的职责划分如下。IFU维护PC和指令侧AXI请求送出的指令放进入口寄存器IDU负责解码、立即数扩展、寄存器堆读口和冲突检测EXU完成算术逻辑运算和分支判断LSU执行Load/Store把地址和写数据打包成数据侧AXI事务WB把来自LSU的读回数据或ALU结果写回寄存器堆。寄存器堆是标准的双读一写端口没有做任何旁路数据前递在EXU和LSU之间显式处理这样代码的逻辑更清晰也方便你逐级理解数据的生命周期。2.2 指令侧AXI和数据侧AXI为什么必须分开指令存取和数据存取在流水线里是两张完全不同的访问模式指令是顺序连续读每次读4字节数据则可能任意地址读单个字或者连续写。把两者合并到一个总线上虽然省端口但会造成严重的总线竞争——取指miss时整个流水线空闲Load miss时又取不了指令。这套代码里采用了哈佛结构IFU引出独立的读通道AXI主接口LSU引出读写都支持的数据侧AXI主接口。即便外部最终用总线矩阵把两边接向同一个内存控制器接口层保持独立也能保证后续扩展ICache和DCache时不用改动流水线逻辑。这里有个容易被忽略的细节指令侧如果只用了AR通道从协议上看没有任何问题但不代表可以直接无视AW。以后如果要做调试模块比如通过外部总线改写指令内存指令侧没有写通道就会卡死。所以代码里即便指令侧只需要读我也把写通道的握手信号留了接口并置为不激活状态这是AXI协议使用中比较老到的一点。2.3 地址映射与仲裁器CPU之外的第一段代码AXI主接口解决的是点对点传输。当外部接了多个从设备比如一块RAM、一个串口、一组GPIO就需要地址译码和仲裁。地址译码把CPU发出的地址区间映射到对应从设备仲裁器在多个主设备请求同一个从设备时决定访问顺序。最常见的仲裁方式是轮询仲裁也就是多个主设备轮流获得总线使用权。写仲裁器最容易出问题的地方是不能只看当前一拍有没有请求还要考虑已经发送且尚未返回的事务不能被新一轮仲裁打断否则响应会错乱。简单做法是在仲裁状态机里增加一个“总线忙”条件只有所有未完成事务结束后才允许切换主设备。这样确实牺牲掉一些并发度但换来的是仲裁模块可以做到无脑可靠。对于初学者来说先把“完全串行化”的仲裁跑通再考虑乱序和并发是我强烈建议的顺序。3. AXI接口RTL实现中那些仿真测不出来的隐藏约束3.1 AXI-Lite与AXI-Full的选择直接影响代码复杂度AXI协议有AXI-Lite和AXI-Full两个子集。AXI-Lite是简化版单拍读写、所有burst长度固定为1、不要求ID适合做寄存器配置类总线AXI-Full支持burst、outstanding、乱序返回适合做内存访问。CPU的数据侧如果用AXI-Lite意味着每次访存最多传一笔访问DDR时效率很低做Cache miss回填更是灾难。特性AXI-LiteAXI-Fullburst支持不支持单拍传输支持可配置burst长度数据宽度固定可配置每事务ID不需要需要用于乱序返回标识outstanding支持不支持支持多个未完成事务适用场景配置寄存器、控制接口内存读写、高速数据通路所以这套代码里数据侧用了AXI-Full指令侧也实现了AR通道的固定长度burst读取。我见过很多项目图省事把所有接口都做成AXI-Lite跑仿真完全正常一放到有真实DRAM的系统里读一行数据要发几十个单拍事务性能和协议规范都不达标。选择协议宽度时标准很简单只要预期会出现连续地址的批量传输就直接用AXI-Full不要留到后面再改。3.2 五通道握手READY和VALID的依赖关系与死锁AXI协议有五个通道读地址AR、读数据R、写地址AW、写数据W、写响应B每个通道都是VALID/READY双信号握手。最容易掉进去的坑是握手信号之间的依赖关系。协议规范明确说VALID不能依赖READY意思是发送方在发起事务时只要自己的状态允许就必须拉高VALID等待READY返回READY则可以等待VALID。典型的死锁写法是发送方为了减少无效传输让VALID等到READY为高才拉高而接收方又设计成没有VALID就不拉READY两方互相等整个总线卡死。// 反例awvalid等待awreadyawready同时等待awvalid直接死锁 always (posedge clk) begin if (issue_req awready) awvalid 1b1; end正确的套路是主设备发出的VALID尽量由状态机控制一旦状态机进入发送态即拉高不需要等READY从设备侧的READY可以在空闲时始终拉高或者用“非满且信道可接收”的逻辑生成。对自己的核而言CPU是主设备控制权在自己手里但从设备模型的配合方式也要在testbench里覆盖到“两者同时有效”和“先READY后VALID”的情况。更关键的是READY建议寄存一拍输出时序更好但代价是增加一个周期的握手延迟对CPU来说这个延迟会直接叠在访存延迟里设计时需要接受它而不是用组合逻辑去穿越。3.3 outstanding事务与ID管理简单可靠的策略AXI-Full允许主设备连续发送多个outstanding事务期间不需要等前一个返回用ID来标识这些事务的返回顺序。对CPU核来说支持多大深度的outstanding直接决定了总线和缓存模型的复杂度。最原始的思路是“一次只允许一个未完成事务”也就是发出AR后必须等R通道收到最后一拍数据才能发下一个AR。这个策略在协议上完全合法实现简单代价是访存延迟完全串行化。如果处理器是单发射、顺序提交的多数场景下这个限制完全可以接受这也是这套代码里数据侧实际的实现方式。如果要做ICache预取或非阻塞Cache就需要支持多个outstanding事务。此时ID字段不能全部填0而要用一个小计数器区分请求。AXI允许乱序返回也可以通过ID把乱序恢复成顺序最简单的方式是只支持顺序返回在从设备端把多路ID响应攒齐后按ID顺序提交。这套代码里预留了ID宽度方便扩展但默认实现不做乱序处理。提这个是想说不要一上来就追求更深的outstanding特性先把单事务、顺序返回这种基础路径验证扎实再谈性能优化。3.4 写响应BRESP与读响应RRESP别把错误直接吞掉读数据通道的RRESP和写响应通道的BRESP里都有一个2位的resp字段0表示OKAY2表示SLVERR3表示DECERR。很多CPU设计demo直接把resp丢到测试端就算完事但一个正经核必须处理响应错误当访问一个不存在的外设地址或从设备报错时最稳妥的做法是把这次访存标记为异常触发类似Load Access Fault的异常入口。实现起来不复杂在LSU里增加一个“访存完成且resp非OKAY”的条件把这个脉冲和访存完成的valid一起送进异常处理逻辑同时禁止写回阶段把脏数据写入寄存器。最容易漏的是写响应通道的时机。写请求在AW和W通道发出后要等B通道返回才能释放该事务的跟踪条目如果B通道迟迟不来后续写请求就没有资源可用。这时候需要设置一个超时计数器超时后按SLVERR处理并记录错误。虽然协议上从设备不应该无限期不返回但作为主设备留一手总比死等强。4. 指令集实现的取舍清单从RV32I到CSR与异常入口4.1 RV32I基础指令的完成标准判断一套RISC-V核有没有真正实现RV32I不是数指令条数而是看整数通用寄存器、控制状态寄存器、异常入口和内存访问是否完整。RV32I包含约40条指令分算术逻辑、Load/Store、分支跳转、CSR访问和内存屏障/环境调用。实际代码里常见的漏项有fence.i在单核CPU里可以空转实现但不能缺了指令解码csrrw/csrrs/csrrc的写后读行为和三操作数关系要仔细看手册SHIFT指令的立即数只使用最低5位load指令的符号扩展和无符号扩展要区分清楚。这些细节在仿真回归中都能抓到但在写代码阶段就要形成自查习惯否则翻到后面改起来成本很高。4.2 M扩展、CSR与异常入口的取舍如果只是在自己设计的SoC里做控制用途RV32I已经够用但要做RISC-V指令集相关的求职项目或想跑更多软件M扩展建议尽早加。实现乘法除法时需要在面积和延迟之间做取舍组合乘法器一拍出结果但布线压力大时钟频率提不上去迭代乘法器用状态机分多拍算完面积小但流水线要支持阻塞周期。这套代码采用更稳妥的做法默认RV32I乘除法作为可选宏开启用迭代方案代价是乘除法期间暂停取指用性能换代码简洁。CSR这一块是很多人做CPU时最后才补又最容易出错的部分。至少要实现机器模式下这几个寄存器mtvec异常入口地址、mepc异常返回地址、mcause异常原因、mstatus全局中断使能位。这套代码里有一个独立的CSR模块译码阶段识别CSR地址执行阶段根据指令类型做对应读写异常发生时由控制逻辑自动写入mepc和mcause并跳转mtvec。这里有个非常实际的注意点异常入口跳转会推翻原本的PC更新逻辑所以这套代码里把异常请求放到IDU/EXU边界统一判断避免分支和异常同时发生时入口被覆盖。4.3 那些“看起来对但一综合就出latch”的写法RTL里最经典的隐患是产生latch。常见写法是在always块里写了多个if分支但没有写else或者case里漏了default又或者某个信号同时被两个always块赋值。仿真是测不出latch的因为功能上它往往“恰好能工作”直到综合报告里出现“inferred latch”或者上板后状态随机跳变才暴露。在这套CPU里最容易出现latch的地方是译码器和总线状态机。译码器里每个输出在当前指令没有意义时也要赋默认值状态机里状态编码不完整时要补充复位态和default态。经验法则是看一个组合逻辑always的输出是否在每一个执行路径上都覆盖了赋值没有覆盖的地方不是保持就是latch。5. 让源码可复现的验证链路testbench、riscv-tests与波形排障5.1 testbench最简结构从hex加载到指令跑完没有任何验证环境的源代码是不完整的。这套代码的testbench思路很直接例化CPU核作为主设备接一个用Verilog行为模型实现的AXI从设备模拟一块足够大的内存仿真开始后读取外部编译好的hex文件按节写入内存模型复位释放后CPU从0地址开始取指。为了确认程序跑完在指令内存里放一个特定指令序列testbench一旦抓到死循环跳转指令就打印寄存器堆快照并结束仿真。initial begin $readmemh(../hex/riscv_test.hex, mem); reset_cpu(); wait(cpu_done); dump_regfile(); $finish; end写testbench时一直强调不要用initial块里的force把信号钉死要让CPU通过真实的AXI事务发起访存这样验证的才是协议层面正确性而不是功能点层面的凑巧。5.2 用riscv-tests做指令集回归能跑hello world只说明程序装载和基本通路没大问题要证明指令集实现正确最直接的方法是回归riscv-tests里的指令级测试。流程是用交叉编译工具链生成RV32I的测试用例把测试的hex加载到仿真内存逐个运行比对输出或者由测试程序自检。第一次跑回归时一定会有不少失败项不要慌先把失败用例里具体是哪种指令分离出来多数问题集中在符号扩展、分支偏移计算或load-store的对齐处理上。这里有个非常实用的技巧在仿真波形里把“取指地址、译码指令、写回数据”三组信号单独拉一组总线手动跟踪一条失败指令从取指到写回的完整生命周期通常几十拍就能定位到是哪个阶段算错了。如果手头有Verilator配合--trace选项生成vcd波形再在GTKWave里按信号名过滤定位速度会快很多。5.3 一次典型的访存时序排障记录举个实际的排障例子也是很多初学者会遇到的。原版代码里LSU发出AR请求后为了偷一点性能把AW请求同时发出结果testbench里因为数据侧和指令侧接的是同一个内存模型两个通道的transaction在内存模型里交错执行导致store操作完成后紧跟着的load读到了旧值。排查链路是这样的先看指令序列和访存地址确认前后两条指令确实访问同一地址再检查内存模型的读写优先级发现模型的写通道和读通道分属独立逻辑没有建立“先处理完写再处理读”的顺序最后定位到LSU缺少一个资源分配约束。修复不复杂在LSU里把写事务和紧跟的读事务用FIFO串行化保证同地址的写后读不被乱序。这个问题纯看功能波形不一定看得出来只有把通道级transaction和指令级时序放一起对照才能暴露。没有回归测试环境靠眼睛盯波形能盯到怀疑人生。6. 拿到这套代码后怎么落地目录结构、工具链与下一步改造6.1 源码目录与命名方便自己也方便别人代码交付时目录结构是否清晰直接决定别人愿不愿意继续用。这套源码按下面的结构组织几乎不加改动就能跑仿真和综合rtl/ ifu.v idu.v exu.v lsu.v wbu.v csr.v regfile.v rv32_axi_top.v sim/ tb_top.v mem_model.sv run_verilator.sh run_vivado_sim.tcl hex/模块命名尽量用功能缩写每个文件顶部写清楚端口来源和目标这是开源硬件项目的基本规矩。还有一个细节AXI信号的命名不要贪图省事只写aw/p等缩写端口名里明确带出通道和工作方向比如s_axi_awvalid。这种命名看起来啰嗦但在后续AHB转AXI、AXI转PCIe这类桥接场景里能少翻无数遍手册。6.2 仿真工具链Verilator还是ModelSim仿真环境我提供两套入口一套基于Verilator加GTKWave适合命令行跑大量回归一套是Vivado自带仿真器的TCL脚本适合点开图形界面看波形ModelSim用户把TCL脚本稍作修改也能用。Verilator跑回归的速度比事件驱动仿真器快一到两个数量级所以riscv-tests的全量回归我建议放在Verilator里执行。不过Verilator对SystemVerilog的支持在部分语法上有差异测试平台的写法要尽量保守避免使用复杂OOP特性否则排语法错误的时间比排设计问题还长。这也是把testbench命名为mem_model.sv但内部只用简单过程块的原因之一。6.3 后续改造方向Cache、AXI-Stream桥与中断控制器拿到源码后最常见的需求是继续往下扩展。几个方向按推荐顺序排第一是实现一个简单的ICache用指令侧AXI的burst能力一次回填多拍这个改造能最直观地提升性能第二是做一个AXI-Full到AXI-Stream的桥接模块用来对接高速数据通路或DMA第三是加入中断控制器把定时器和外部中断接入CSR里的mstatus/mie/mip让核从裸机代码进化成能跑更复杂软件的状态。四个方向里Cache和总线桥的Verilog实现细节我后续会分别专门写文章记录这里先不展开。有一点要再提醒每扩展一个特性就回去跑一遍riscv-tests和手写的访存回归用例保住已有的正确性。最后分享一个我在整理这套AXI接口时反复吃亏得出的经验设计RISC-V AXI核时先把AXI的ID位宽、数据位宽和每通道的outstanding深度定义成宏或参数放在一个专门的配置头文件里后边所有模块都引用这几个参数而不是散落在各文件里。我早期版本把ID位宽写死在几个模块里后来要加Cache不得不一次改十几个文件那种痛苦经历过一次就再也不想经历。如果你正在规划自己的RISC-V核多花半天定义好全局参数后面省下的时间远超这个数。本文还有配套的精品资源点击获取
返回列表