PICORV32源码精读:从取指到写回的RISC-V软核实现剖析

发布时间:2026/10/31 19:51:41
PICORV32源码精读:从取指到写回的RISC-V软核实现剖析 如果你问我想在今天搞明白一个CPU到底是怎么从取指到写回一步步把程序跑起来的应该先读哪份源代码我会毫不犹豫地说把PICORV32打开逐行读一遍。原因很简单——它小、完整、没有任何工业级处理器常见的花活你看到的每一个寄存器、每一个状态、每一段组合逻辑背后都有一个能讲清楚的道理。更重要的是PICORV32不是一个玩具它在真实项目里被大量用作嵌入式软核、自定义指令集实验平台、以及教学芯片设计的样例。这篇文章是我自己把PICORV32源码从顶层到细节啃完一圈之后的笔记不是单纯翻译代码注释而是把它的架构思路、状态机流转、译码逻辑、参数化裁剪套路以及集成到FPGA时容易踩的坑都串起来讲。适合刚接触RISC-V和软核的人作为第一篇源码精读也适合已经在用PICORV32但还没完全吃透内部逻辑的开发者对照着查漏补缺。1. 为什么PICORV32值得逐行读小核不小五脏俱全1.1 一个“小处理器”到底能干什么PICORV32是Clifford Wolf在2015年前后发布的开源RISC-V软核完整实现了RV32IMC指令集也就是32位基础整数指令加上可选的乘除法扩展再加上可选的压缩指令扩展。它没有Cache、没有TLB、没有乱序执行、没有多级流水线整个核心只有一个文件picorv32.v却能跑C编译器生成的普通RISC-V程序能接中断能和外部存储器通过简单的握手协议通信。我第一次看到它的综合结果时有点意外在主流FPGA上一个带乘除法、带压缩指令、带中断控制器的配置通常只用掉一千个左右的LUT和几百个触发器主频可以跑到几十甚至上百兆赫兹。这意味着它塞进任何一颗低端FPGA都能轻松运行非常适合做SoC里的控制核心、协议转换桥、或者教学用的可修改处理器。它的设计哲学很明确——用重复的简单代替复杂。指令不并行、不前瞻、不猜测每条指令老老实实花多个时钟周期完成换来的是时序简单、面积极小、代码量少到可以用一个周末读完。1.2 和市面上其它软核相比它输在性能赢在理解成本很多人在选型时会把PICORV32和VexRiscv、Rocket、SERV放在一起比较。它们的定位其实是完全不同的物种硬比性能没有意义。软核架构风格代码规模适用场景PICORV32无流水线多周期状态机单文件数千行Verilog教学、小面积嵌入式、自定义指令实验VexRiscv5级流水线用SpinalHDL编写较大工程化程度高对性能有要求的中等嵌入式SoCRocket有序5级流水面向SoC集成非常庞大需要运行Linux等复杂系统的应用SERV位串行处理器逐个bit执行指令面积极致小极低功耗、极低性能的场景我见过不少项目上来就想用VexRiscv结果被流水线冒险、AXI接口、总线矩阵搞得焦头烂额也见过用Rocket跑Linux结果发现大部分资源都在服务操作系统而应用逻辑只占很小比重。如果你的需求是一个“可靠的、能跑裸机C代码的、方便改指令集的32位核”PICORV32往往是成本最低的选项。从学习角度来说PICORV32的价值更高。CPU设计中最难理解的概念——取指、译码、执行、访存、写回、分支延迟、中断响应、乘除法迭代——在PICORV32里全部以最朴素的方式呈现出来。读懂了它再去看那些带流水线、带分支预测的核就很容易把复杂概念映射回已知状态机上。1.3 源码的藏身之处与文件组织PICORV32的官方仓库结构非常干净核心的源代码全部集中在picorv32.v这一个文件里。文件里定义了主模块picorv32还封装了带AXI接口的picorv32_axi模块。除了核心模块之外仓库里通常会带上一个名为picosoc的示例SoC演示了如何用PICORV32搭配SPI Flash、UART、GPIO组成一个能跑完整程序的单片机还有一套testbench和测试程序用来做回归验证。这种“一个文件就是一颗CPU”的组织方式在实际工程里特别讨喜。集成的时候不需要在一堆工程文件里翻找依赖关系直接把picorv32.v拷贝进工程配置好参数就可以开始干活。修改时也只触碰一个文件出问题方便回溯。当然代价是它的代码结构非常密初读时会觉得到处都有define、generate、条件编译不舒服但值得习惯——因为这是很多优秀开源硬件项目的通用表达方式。2. picorv32.v的骨架寄存器、数据通路和“单块式”设计2.1 核心信号的“名册”要读源码先要认识信号。PICORV32里没有复杂的分层总线数据和控制全部通过一组寄存器在时钟边沿处推进。下面是我读代码时整理的核心寄存器清单了解它们各自干什么后面看状态机会顺畅很多。信号名作用reg_pc程序计数器保存当前取指地址reg_op1、reg_op2ALU的两个源操作数reg_opcode当前正在执行的指令的特征码reg_rdata从存储器读回的数据暂存reg_rs1、reg_rs2、reg_rd指令中的寄存器索引reg_imm译码生成的立即数reg_mem_addr访存地址reg_mem_wdata、reg_mem_wstrb写存储器的数据和字节使能reg_shifter移位、乘法、除法迭代中的一个核心工作寄存器reg_count乘除法以及部分多周期操作的递减计数器latched_branch、latched_store、latched_load分支、写存储、读存储的锁存标志这套寄存器几乎构成了CPU的全部内部状态。你会发现这里没有单独的指令寄存器IR也没有流水线寄存器组因为无流水线状态机只需要在正确的时间点把数据锁存下来就够了。2.2 数据通路一条指令如何流过这颗芯PICORV32的执行过程是一条清晰的链路取指、译码、读寄存器、执行、访存、写回。但是因为没有任何流水线重叠整个过程是在同一个大状态机的控制下顺序完成的。以一条简单的add指令为例它大概会经历这样几个阶段CPU先把reg_pc放到存储器接口上请求取指等待mem_ready返回取回指令后译码逻辑识别出opcode、funct3、funct7、寄存器编号、立即数字段从寄存器文件读出源操作数放入reg_op1和reg_op2ALU根据解码结果完成加法结果准备写回reg_rd在写回周期更新寄存器文件和reg_pc指向下一条指令。整个过程中那个“大状态机”并不像教科书里那种STATE_FETCH / STATE_EXECUTE / STATE_WRITE_BACK的显式状态编码而是用若干latched_*信号表达“现在正处于哪个子阶段”。这是PICORV32代码不容易一眼看懂的地方但也是它代码紧凑的原因。2.3 集中式always块可综合风格的最高效选择PICORV32的核心逻辑几乎全部写在一个带时钟和复位的同步always块中。这种写法在学术风格的Verilog代码里不多见因为多数教材会教你拆成always (*)和always (posedge clk)两个部分用显式状态机分开控制和数据。集中式写法的优点很明显所有寄存器在同一时刻统一更新天然避免了多进程写法中常见的信号竞争和赋值顺序问题综合器把它看作一个大FSM加数据通路优化起来反而更主动。代价是代码不直观初读者容易迷失在一个几百行的always块里分不清哪个分支是取指、哪个分支是写回。我的读法建议是先别着急扣细节把latched_*信号当成状态变量来标记进度再看reg_pc在哪些条件下变化理解“当前处在哪个环节”比理解每一行的组合逻辑要重要得多。3. 状态机无流水线的CPU如何稳定走完每条指令3.1 取指循环指令是怎么被“喂”进来的PICORV32的取指逻辑非常直白。CPU把reg_pc送到存储器地址线上同时把mem_valid拉高表示这是一次有效的读请求。外部存储器或总线在准备好数据后把数据放在mem_rdata上并拉高mem_ready。CPU在时钟边沿看到mem_ready1之后才把mem_rdata锁存为当前指令然后进入译码阶段。整个过程中最关键的理解点是取指至少需要两个时钟周期一个周期发出请求一个周期收回数据。如果存储器很慢CPU就一直停在“等待取指返回”的位置不做任何其他事。这也是无流水线处理器的典型特征——时间开销全部串行化简单但慢。3.2 分支与跳转为什么要花掉额外周期分支指令在PICORV32里占据的执行路径相当有代表性。当译码发现一条beq、bne这类分支指令时CPU先正常读取源操作数在ALU里比较两个数是否相等或不等然后在同一个周期里根据比较结果决定“是否锁存分支”。这里的核心机制是latched_branch。如果分支条件成立CPU会把目标地址锁存起来随后的取指阶段直接从锁存的目标地址开始取指如果条件不成立则像普通指令一样继续顺序执行。由于这个决策发生在execute阶段分支指令要额外占用周期来等待操作数、完成比较、再进入下一轮取指所以一条分支指令的总开销大约在三个周期以上。用打比方的方式理解它无流水线CPU处理分支就像你走到岔路口停下来看地图再选择方向而带预测的流水线CPU则像导游提前带着队伍往一个方向走万一走错再折返。PICORV32选择了最稳妥也最慢的那种。3.3 存储和加载mem_valid/mem_ready握手的意义访存指令是PICORV32里交互最丰富的一类。先说读lw指令译码后CPU把计算出的地址放到reg_mem_addr在总线接口上呈现mem_valid1和读取地址同样等待mem_ready。数据返回后先暂存在reg_rdata里再按字/半字/字节的宽度对齐最后写回目标寄存器。写比读稍微复杂一点。sw指令需要同时输出地址、写数据和字节使能。PICORV32的写数据是reg_mem_wdata字节使能reg_mem_wstrb用来指示当前写哪几个字节。之所以在写路径上再加一个锁存状态是为了保证在mem_ready为低的时候写数据和地址一直稳定不会因为后续指令的推进而改变。整个握手协议其实只遵循一个朴素原则主设备发出请求从设备准备好后应答主设备再继续。别小看这个协议PICORV32能轻松挂接SRAM、SPI Flash、片上外设甚至AXI总线靠的就是这个统一的mem_valid / mem_ready接口。3.4 乘除法指令迭代实现的“笨办法”M扩展指令是PICORV32里性能差距最大的一类。如果不启用硬件乘法器参数乘法、除法、取模全部用迭代算法完成一条32位乘法或除法指令需要消耗三十多个时钟周期。这对于控制类程序来说通常可以接受但如果你在循环里做大量乘法运算性能会明显拖后腿。迭代实现的思路其实很经典乘法用移位加除法用移位减每周期处理一个bit。代码中的reg_shifter在这里充当被乘数、商、余数的工作寄存器reg_count充当递减计数器。从阅读角度来说追踪reg_count从高到低递减的过程就等于看到了迭代循环的推进轨迹。PICORV32提供的PICORV32_ENABLE_MUL_HARDWAREMUL参数可以替换乘法实现为组合逻辑乘法器让乘法在几个周期内完成PICORV32_ENABLE_MUL_FASTDIV则优化除法器的移位步进。需要注意的是这些优化以增加组合逻辑深度为代价如果你追求高频时序可能需要评估权衡。4. 译码器与立即数理解RISC-V指令集的一个窗口4.1 RISC-V指令分类在PICORV32中的落地RISC-V指令集在编码上有一个很友好的特性指令类型清晰、opcode字段固定译码器不需要像x86那样逐字节猜边界。PICORV32正是充分榨干了这种友好性。在代码里你会在localparam定义中看到大量CPU_OPCODE_*常量比如CPU_OPCODE_LOAD、CPU_OPCODE_STORE、CPU_OPCODE_BRANCH、CPU_OPCODE_JALR、CPU_OPCODE_OP_IMM、CPU_OPCODE_OP等等。译码器的核心就是一个基于reg_opcode的case语句按照RISC-V的opcode把指令粗分成几大类再根据funct3/funct7细化为具体操作。这种“两层译码”结构的优势在于复用性极高。同一条加法执行通路既服务于add指令也服务于lw、sw、jalr的地址计算因为它们的核心操作都是“加法”。PICORV32把这种通用ALU操作集中在一个模块里整个译码逻辑永远不会因为新增一条需要简单算术支持的指令而大改。4.2 立即数的生成不同格式如何统一成32位RVC压缩指令、标准指令、跳转指令、访存指令里的立即数格式各不相同。RISC-V的优势在于这些立即数本质上都是从固定的bit位置抽取若干位然后做符号扩展所以PICORV32在译码阶段会一次性组合出一个统一的32位立即数reg_imm后续执行单元不再关心指令类型。阅读这部分代码时我建议按这样的顺序理解先看标准R型、I型、S型、B型、U型、J型六种格式的bit布局再对照代码中如何用几个slice和|运算拼出中间值最后看符号扩展怎么把12位、20位立即数补充到32位。一旦理清这个映射你会觉得RISC-V指令集编码是真心为译码器省面积。4.3 自定义指令区PICORV32最有意思的扩展点PICORV32在标准RV32I之外留出了自定义指令空间也就是custom0、custom1、custom2这组opcode区。这一组指令空间让开发者可以在不修改工具链的情况下注入专用指令配合PCO协处理器接口甚至可以加速特定运算。举个例子如果你希望CPU带一条硬件计算min的指令理论上可以在自定义指令区定义一种编码让它在译码阶段被识别为一次ALU操作执行阶段直接输出(op1 op2) ? op1 : op2。这种改法不需要改动RISC-V工具链因为程序里可以用汇编伪指令或者内联汇编发出对应的原始编码。此外PICORV32还有一套PICO专用指令比如SETQ和GETQ。它们的作用是向一个小型队列寄存器写入或读取数据再配合协处理器接口传递到外部硬件。很多基于PICORV32做DSP或通信加速的项目正是用这套机制把CPU和硬件加速器之间的握手开销降到了最低。5. 参数化配置一个核适配多种场景5.1 关键参数的底层影响PICORV32的灵活性在很大程度上来自顶部的一组参数。修改这些参数会直接改变综合出来的硬件规模和行为读代码时一定要顺带理解它们的开关在哪里生效。参数作用使用建议RISCV_ISA_M_EXTENSION启用乘除法指令需要乘法时打开面积略增RISCV_ISA_C_EXTENSION启用压缩指令降低代码体积增加译码复杂度RISCV_ISA_E_BASE嵌入式模式只保留16个寄存器追求极致面积时用PICORV32_CATCH_MISALIGN响应非对齐访存调试时打开生产环境可按需关闭PICORV32_CATCH_ILLINSTR响应非法指令裸机调试强烈建议打开PICORV32_ENABLE_MUL_HARDWAREMUL用硬件乘法器替代迭代乘法性能优先时打开PICORV32_ENABLE_MUL_FASTDIV优化除法速度除法频繁时打开PICORV32_ENABLE_IRQ使能中断控制器需要实时响应时打开PICORV32_ENABLE_IRQ_QREGS中断队列寄存器扩展需要嵌套中断时打开PICORV32_MEM_EXTRA_STATEBITS扩展访存状态位总线延迟较长时使用这些参数在源码中大多通过generate或条件编译来影响最终生成的结构。比如乘除法扩展打开时才会生成reg_count和迭代逻辑关闭时这部分电路整体被优化掉面积差异非常明显。5.2 中断无特权模式下如何实现快速响应PICORV32的中断控制器不走RISC-V标准的机器模式中断机制而是一个轻量级的自定义实现。它没有CSR寄存器组不区分特权等级甚至不需要复杂的mtvec配置而是把传入的一组IRQ请求映射到固定处理入口并用一个简单的中断屏蔽寄存器控制是否响应。中断响应时CPU会记录当前PC值作为返回地址然后跳转到中断服务程序入口。PICORV32_ENABLE_IRQ_QREGS打开后控制器维护一个小型队列可以同时记录多个待处理的返回地址这让它具备了一定的中断嵌套能力。对于小系统来说这套机制已经足够实用。我在集成过时传感器轮询、网络协议栈这类实时任务时都靠它完成快速上下文切换而不用引入整套RTOS。如果你需要标准的中断控制器行为PICORV32的这套轻量方案反而不合适建议外挂标准中断控制器或者换用配置了完整CSR的核。5.3 面积与性能的权衡什么时候该忍住不换核综合之后PICORV32的面积数字相当漂亮。基础配置在主流7系列FPGA上大约只需要几百个LUT和几百个触发器启用压缩指令、乘除法、中断控制器之后也基本维持在一千多个LUT的水平。这直接意味着时序收敛很容易资源别的地方随便用。但它的性能上限就摆在那里。因为每条指令平均要花三到五个周期主频即使能跑到100MHz实际吞吐也比不上一颗简单的三级流水线软核。我的建议是评估项目时先算清楚每条热路径指令的需求如果应用主要在做状态控制、寄存器读写、低速通信协议PICORV32绰绰有余如果涉及大量乘加运算、数据搬运或者打算跑Linux这种重量级应用就果断换VexRiscv甚至Rocket不要在PICORV32上强行优化。6. 跑起来和改起来PICORV32的实际落地经验6.1 在仿真环境里跑通一条程序官方仓库提供了完整的testbench环境。简单来说你需要准备一个RISC-V工具链建议用带-marchrv32imc支持的GCC把C代码或汇编编译成elf或hex格式加载到testbench的存储器模型里然后跑仿真。仿真有个很有用的点testbench会逐条记录指令执行状态任何异常PC、非法指令、总线挂死都会立刻暴露。我调试自定义指令时几乎全靠testbench在波形里观察reg_op1、reg_op2和写回数据的变化不需要在FPGA上反复烧录。6.2 FPGA集成时的四个经典坑把PICORV32接进真实SoC时我踩过一些值得记录的坑。第一是mem_ready时序。如果把PICORV32直接接片上RAMRAM的读延迟和mem_ready的时序必须严格对好否则偶尔会出现取指数据错位。用Block RAM时建议在RAM输出侧打一拍保证mem_rdata稳定后再被锁存。第二是复位信号。PICORV32支持同步复位但复位释放时机如果和时钟边沿太近会偶发PC不定状态。稳妥做法是使用外部复位芯片的干净复位信号尽量不要用逻辑产生的毛刺复位。第三是中断的粘性问题。外部IRQ如果是一个持续时间较长的高电平脉冲而CPU尚未开启中断使能这个脉冲可能在使能打开之后被误判为新中断导致进入错误的中断上下文。处理方式是在中断源侧做边沿检测或者确保中断信号宽度符合预期。第四是启动地址。上电后PC从固定地址进入这个地址通常需要放一条跳转指令到真正的初始化代码。如果从SPI Flash启动还要在Flash里准备好对应的启动头。很多时候仿真能跑、上板不行问题就出在启动引导阶段。6.3 亲手加一条自定义指令的完整路径为了演示PICORV32的扩展能力我拿一个具体例子说明给CPU增加一条自定义指令作用是计算两个寄存器中较小值并写回目标寄存器。实现的路径大致如下在custom0指令区选取一个未占用的编码空间确定funct7和funct3的组合在译码case中新增分支让这条指令像普通R型指令一样读取两个源寄存器并生成写回目标在ALU执行逻辑中增加一个min判断分支输出比较结果在testbench里构造一组测试数据验证结果正确性。整个过程不需要改动工具链因为我们可以直接在汇编中用.word或者内联汇编发出自定义编码。但这里要提醒一句自定义指令会让代码彻底绑定PICORV32换其它RISC-V核就跑不了。所以做之前先想清楚是将其作为底层硬件抽象还是仅仅为了验证某个加速思路。我在一个电机控制项目里就加过类似的自定义指令用来在单周期内完成位置误差钳位省去好几条比较和分支指令。效果很明显但也因此把控制代码锁死在了这颗核上。后来硬软件解耦时花了不少功夫把指令替换回标准函数。这种权衡做之前一定要有数。另一个实际经验是读PICORV32源码不要只盯着picorv32.v单文件把picosoc示例SoC一起看了才能理解它和外部存储、外设、中断源怎么配合。源码里有些信号在纯核仿真里根本用不到只有放在SoC上下文里才明白为什么作者要多留那些状态位。如果你把这个核读透了我建议下一个目标可以去看VexRiscv的流水线实现重点对比“无流水线状态机”和“流水线冒险处理”的差别——这时候你会发现因为PICORV32已经把指令生命周期讲得足够清楚再看流水线就很容易理解哪里需要前递、哪里需要停顿、哪里需要冲刷。我当年就是沿着这条路线从PICORV32起步一步步啃下了更多复杂处理器源码后来设计自己的小核心时脑子里最早浮现的框架仍然是PICORV32这套简单可靠的状态机逻辑。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询