简介:本资源是一套面向计算机体系结构课程学习者、FPGA开发初学者及RISC-V爱好者的设计实践包,聚焦于从零实现一个功能完备的五级流水线RISC-V处理器。它完整覆盖RV32I指令集全部42条用户级指令,集成数据前推、三类冒险(RAW/WAW/structural)检测与响应、静态分支预测(基于分支方向恒定假设)等核心微架构机制,并采用单端口存储器模型以兼顾教学可理解性与硬件约束真实性。压缩包共38个文件(4.08MB),含16个Verilog源码(.v)构成处理器主体与测试平台,8个文本说明(.txt)记录设计日志、测试用例与运行指引,5张架构图/波形图(.jpg/.png)、2份PDF文档(含设计规范与验证报告)、1个Word附赠资料(.docx)以及C++编写的测试用例自动生成工具(.exe/.cpp),目录结构按里程碑(Milestone 2/3)与模块(Sim/Constraint/Test Cases)组织,便于分阶段复现与调试。已有125人学习下载,是深入理解流水线原理、动手验证CPU设计细节的优质工程化学习材料。
1. 项目概述与核心目标
做CPU设计的队伍里,十个有九个选RISC-V不是没有道理。这个项目做的就是基于RISC-V RV32I基础整数指令集的5级流水线处理器,完整支持42条用户级指令,把数据前推、冒险检测、静态分支预测这些课本上写了又写、但一动手就翻车的机制全部用Verilog落地,存储器侧采用单端口模型,最后用自定义C测试用例做了完整验证。
先说说这东西是什么。RISC-V是一个开放、免费的指令集架构,RV32I是它的32位基础整数指令子集,不包含乘除法扩展M、原子操作A、浮点F、压缩指令C这些模块。5级流水线就是经典的IF取指、ID译码、EX执行、MEM访存、WB写回五段结构,跟教材里画的MIPS流水线几乎是同构的,但指令编码和寄存器约定完全不同。数据前推和冒险检测用于解决流水线中的数据相关性,静态分支预测用来缓解控制冒险带来的性能损失。单端口存储器模型的意思是:指令存储和数据存储共用一个访存端口,同一个时钟周期内只能进行一次读或写操作,这对流水线的访存调度提出了额外约束。
这个项目非常适合三类人:第一类是正在学计算机组成原理、想用一门硬件描述语言把流水线真正跑起来的学生;第二类是准备应聘芯片前端、数字IC岗位、需要拿一个拿得出手的项目的求职者;第三类是想系统理解RISC-V生态、为以后做SoC或跑操作系统打基础的人。做完这个项目,你对流水线冲突的理解会从一个"考试会背"的水平,提升到"改一行代码就知道会影响哪条路径"的水平,这两者之间的差距,只有亲手写过才知道。
2. RV32I指令集体系梳理
2.1 42条指令的分类逻辑
RV32I标准基础指令集如果算上FENCE、ECALL、EBREAK,总数大约是47条。这个项目设计的"42条用户级指令",删掉的是哪些?主要是三类:FENCE内存屏障指令、ECALL环境调用、EBREAK环境断点。原因是这些指令在单核、无缓存一致性的场景下没有实际功能,ECALL和EBREAK更多是操作系统和调试器用的,属于特权/系统级语义。保留的42条刚好覆盖了C语言编译后最常见的那批指令,包括R型算术逻辑指令、I型立即数指令、Load/Store访存指令、Branch分支指令、JAL/JALR跳转指令、LUI/AUIPC高立即数指令。
在这42条指令里,有个容易被忽略的点:SLLI/SRLI/SRAI这三条移位立即数指令,在RV32I下使用了SHAMT字段,但立即数只有5位。此外ADDI、SLTI、SLTIU、ANDI、ORI、XORI这些I型指令共享完全相同的立即数解码逻辑,唯一要注意的是SRAI和SRLI的区别在于ALU要判断是算术右移还是逻辑右移,这个判断信号来自func7字段里的第10位。所以指令译码模块不能简单按opcode分类,还要把funct3、funct7、rs1、rd这些字段全部切出来,交给统一的控制信号生成逻辑。
2.2 指令在流水线中的行为差异
不同指令在流水线中的行为差异,是设计控制信号时最关键的输入。按行为划分,42条指令可以分成六组:
- ALU型指令:ADD、SUB、AND、OR、XOR、SLL、SRL、SRA、SLT、SLTU,以及对应的立即数版本,这类指令经历IF/ID/EX/WB四级就结束,MEM级空闲。
- Load型指令:LB、LH、LW、LBU、LHU,需要走完IF/ID/EX/MEM/WB全部五级,MEM级是读数据存储器,WB级把读出数据写回寄存器。
- Store型指令:SB、SH、SW,也是五级,但MEM级是写数据存储器,且WB级没有动作,需要关掉寄存器写使能。
- Branch型指令:BEQ、BNE、BLT、BGE、BLTU、BGEU,在EX级完成两个操作数比较,比较结果决定PC是否跳转,不访存,不写回。
- Jump型指令:JAL、JALR,需要无条件修改PC,JAL的目标地址由PC+立即数得到,JALR的目标地址由rs1+立即数得到,低1位清零。
- U型指令:LUI、AUIPC,立即数在高20位,LUI直接生成立即数,AUIPC是PC+立即数,这两条走四级流水,不涉及数据存储和控制转移。
理解了这个分组后,再去写控制信号就会非常清晰。比如寄存器写使能RegWrite:ALU型、Load型、JAL/JALR、LUI/AUIPC为1,Store型和Branch型为0。又比如存储器读使能MemRead:只有Load型为1。这个真值表在课程设计报告里几乎是必备内容,实际编码时也是从这张表出发,一个信号一个信号地推。
3. 五级流水线整体架构设计
3.1 流水线各级职责与寄存器边界
流水线的本质是用寄存器把组合逻辑切段,让每一段都在一个时钟周期内完成。第一级IF(取指)负责根据PC从存储器取指令,同时计算PC+4作为顺序下一条地址。第二级ID(译码)负责把指令字段拆解,读出寄存器文件的两个操作数,生成所有控制信号,同时处理JAL/JALR这类可以提前确定目标的跳转。第三级EX(执行)负责ALU运算、分支比较、地址计算。第四级MEM(访存)负责读写数据存储器。第五级WB(写回)负责把结果写回寄存器文件。
流水线寄存器组是整条流水线的骨架:IF/ID寄存器存放取到的指令和PC值;ID/EX寄存器存放译码结果、两个读出的操作数、立即数、控制信号;EX/MEM寄存器存放ALU结果、分支结果、要写入存储器的数据、PC;MEM/WB寄存器存放访存结果或ALU结果、目标寄存器号、控制信号。这里有个新手很容易踩的坑:PC值要跟着指令一起往下传,因为分支指令计算目标地址时需要EX级的PC,而不是重新去取IF级的当前PC,否则不同级之间会出现错位。
3.2 控制信号的逐级传递机制
控制信号是流水线设计中最容易被搞错的部分。很多同学第一次写的时候,会直接在EX级根据当前指令生成控制信号,结果发现后面的指令全是乱的。正确做法是:所有控制信号在ID级一次性生成,然后放进ID/EX寄存器,随指令逐级往下传,每一级只使用自己这一级相关的信号。例如MemRead在ID级生成后,要跟着指令走到EX/MEM寄存器,在MEM级才真正生效;RegWrite信号则要一直传到最后一级WB,因为它决定的是WB级的寄存器文件写入。
我实际设计时给每个流水级定义了独立的控制信号结构体:EX_Control包含ALUOp、ALUSrc、Branch;MEM_Control包含MemRead、MemWrite;WB_Control包含RegWrite、MemToReg。在ID/EX寄存器的位宽规划里,把这三个结构体按顺序打包,传下去的时候一组一组拆开,每条指令经过哪一级就用哪一组的信号,其余组保持默认值或直接置零。这样做的好处是,后续如果要扩展乘除法指令,只需要在控制信号结构体里加字段,不需要改整条数据通路。
这里还有个需要说明的设计取舍:为什么不把部分控制信号的生成放到EX级?因为流水线的每一级都必须在一个时钟周期内完成工作,如果EX级既要算ALU又要生成控制信号,关键路径会变长。集中译码让ID级成为组合逻辑最重的级,但整个设计会更规整,时序收敛也更容易。
4. 数据前推与冒险检测:核心难点攻坚
4.1 数据冒险的产生场景
数据冒险的根源是:后一条指令在EX级需要使用前一条指令的结果,但前一条指令的结果要到WB级才能写回寄存器文件。看一个最经典的例子:
add x1, x2, x3 sub x4, x1, x5第二条指令的ID级读x1时,第一条指令还在EX/MEM级,寄存器文件里的x1还是旧值。如果不做任何处理,sub得到的结果就是错的。数据冒险细分有三种类型:RAW(读后写)、WAR(写后读)、WAW(写后写),但在顺序执行的5级流水线里,只有RAW冒险需要处理,WAR和WAW在指令顺序发射、顺序写回的前提下天然不会发生。这一点在考试里经常成为判断题,实际设计时则要确保寄存器的写发生在时钟边沿、读发生在边沿之后,才能让这个前提成立。
RAW冒险根据距离又分成三种具体场景。第一种是相邻指令,load-use距离:add在EX级刚算出结果,紧接着的下一周期,后面的load指令就要用这个结果作为访存地址,中间隔了两级寄存器文件写入周期,光靠前推都来不及,必须停顿一拍,这类叫load-use冒险。第二种是隔一条指令的距离,add的EX/MEM结果可以直接前推到下一条指令的EX级。第三种是隔两条指令的距离,需要使用MEM/WB级的结果前推。处理这三种情况,就是数据前推单元和冒险检测单元的全部工作。
4.2 前推路径的接线与优先级
数据前推的基本思想是:结果不等到WB级写回寄存器,而是在EX/MEM级和MEM/WB级中间直接把数据拉出来,送给EX级的ALU输入端。我的设计里实现了两条前推路径:
- EX/MEM级到EX级:当EX/MEM寄存器里的指令是ALU型或LUI/AUIPC,且它的目标寄存器号等于ID/EX寄存器中当前指令的源寄存器号时,把EX/MEM的ALU结果接到ALU输入MUX上。
- MEM/WB级到EX级:当MEM/WB寄存器中的指令是要写回寄存器的类型,且目标寄存器号等于当前指令源寄存器号时,把MEM/WB的结果接到ALU输入MUX上。
这里有个细节很容易漏:两条前推路径同时满足时,必须优先选择更近的那条,也就是EX/MEM级前推的优先级要高于MEM/WB级,否则会把旧值前推过去。判断逻辑里还有一个必要条件,就是前推源指令的RegWrite必须为1,如果它本身不写寄存器,即使寄存器号匹配也不能前推。Store指令的数据通路也要接前推:如果前一条指令是ALU型,紧跟的SB/SH/SW要写它的结果到内存,那Store的写数据入口需要支持从EX/MEM或MEM/WB前推,否则会在store数据上出现RAW冒险。
4.3 Load-Use Stall的判定与处理
Load-use冒险是唯一需要插入流水线气泡的数据冒险。典型的指令序列是:
lw x1, 0(x2) add x3, x1, x4lw在MEM级读到数据后,结果要到WB级才写回,但下一条add在EX级就需要x1。无论怎么前推,都差一个周期。处理办法是让流水线停顿一拍:add停住不动,硬件同时在IF/ID级和ID/EX级之间插入一条空指令(NOP),保证add的EX级能在两个周期后拿到从MEM/WB前推来的load结果。
冒险检测单元的判定条件是:ID/EX级当前指令是Load且MemRead为1,同时ID/EX级的目标寄存器号等于IF/ID级指令的rs1或rs2(且目标寄存器号不等于x0)。条件成立时,执行以下动作:禁止PC更新,让IF级停住;禁止IF/ID寄存器更新,让已取到的指令留在原处;把ID/EX的控制信号全部清零,相当于向流水线注入了一个NOP。两拍之后,load结果进入MEM/WB级,此时add进入EX级,数据前推单元会正确处理转发。
你可能会问:为什么不用前推而一定要停?因为停顿本质上是在时间维度上挪出了数据就绪的空档。load的结果在MEM级才出来,add的EX级需要数据时,load刚刚才进EX/MEM,路径上的时延不满足时序要求,所以只能加气泡。每次load-use冒险会损失一个周期,这个损失在统计CPI时通常会被体现出来。
5. 静态分支预测与控制冒险处理
5.1 静态"预测不跳转"策略的实现
控制冒险来自改变PC的指令:branch和jump。如果等到EX级比较完才知道是否跳转,那流水线里已经取出的后续指令全部白取。这个项目采用静态分支预测中实现最简单的"预测不跳转"(Predict Not Taken)策略:硬件默认分支不发生跳转,流水线继续顺序取指,直到确认分支真的发生时再冲刷掉错误取入的指令。
为什么选静态不跳转而不是动态预测?两个原因:第一,动态预测需要分支历史表BHT和额外的状态机逻辑,对这个规模的项目来说负担较重;第二,在大多数非循环密集的测试程序里,预测不跳转的准确率其实够用,尤其跳转指令占比不高的情况下,性能损失有限。RISC-V不像MIPS那样有分支延迟槽,MIPS的延迟槽能在跳转后继续执行一条有用的指令,RISC-V选择用预测机制来处理,因此在RISC-V上实现分支预测时,错误预测后的流水线冲刷是必须的。
分支判断放在EX级,意味着分支指令经过IF、ID两级到达EX时,后面跟着的两条指令已经进流水了。如果判断结果是跳转,就要把这两条错误指令全部清除,同时把PC更新为分支目标地址。实际执行下来,错误预测损失3个周期:被冲刷的2拍加上重新取出目标指令的1拍。
5.2 JAL/JALR提前处理与分支冲刷逻辑
对于无条件跳转JAL和JALR,我在ID级就做了提前处理。因为这两条指令的目标地址在ID级就能完整计算出来:JAL的目标是PC+立即数,JALR是寄存器值+立即数。寄存器值从寄存器文件读出来,组合逻辑完成加法,然后直接送到PC选择MUX。这样JAL/JALR只损失2个周期(冲刷IF/ID已取的一条指令,下一拍立即从目标地址取指),比枝策开销更小。
有条件的beq/bne/blt/bge等仍然在EX级比较。比较逻辑复用了ALU中的减法器,因为BEQ/BNE的本质是判断两个数相减是否为0,BLT/BGE的本质是判断减法结果的符号位和有符号溢出标志。ALU在EX级算出结果后,产生一个BranchTaken信号,如果为真,则PC从分支目标地址(ID级已经算好的那个值)重新开始取指,同时将IF/ID寄存器和ID/EX寄存器的内容置为NOP。
实际调试中我发现,冲刷逻辑必须和流水线寄存器的使能信号配合好。错误预测发生时,IF/ID的在reg信号要置0,把当前已取指令丢弃;ID/EX的寄存器也要清空,防止刚解码的错误指令产生写操作或访存操作。这一步漏掉任何一个,都会出现"幽灵指令"执行造成寄存器文件被莫名篡改的诡异问题。
6. 单端口存储器模型与结构冒险
6.1 单端口存储器的约束与选型理由
单端口存储器模型是指令存储和数据存储共享同一个物理端口的方案。传统的哈佛结构通过分离的指令存储器和数据存储器来避免IF和MEM同时访存的冲突,但实际FPGA上的Block RAM大多是单端口或简单的双端口,真双端口RAM资源有限且布线和时序更复杂。采用单端口模型后,流水线里IF级每个周期都需要读指令,MEM级在有load/store指令时也需要访问存储,两者的访存请求会在同一个周期内冲突,这就是结构冒险。
处理结构冒险的原则很简单:访存请求冲突时,MEM级优先级高于IF级,因为MEM级的load/store无法延迟执行,而IF级可以停一拍。仲裁逻辑检测到当前EX/MEM寄存器的MemRead或MemWrite为1时,就冻结IF级。因为具体访存行为发生在MEM级,但MEM级正在访问的指令在EX/MEM寄存器里已经确定,所以仲裁信号要在EX级产生,传到MEM级生效。
还有一点值得说明:这里的数据存储器与指令存储器在物理实现上,我仍然建议用两个单端口RAM实例来模拟,而不是字面上真的共用一个RAM。项目标题里的"单端口存储器模型"是指每个RAM都是单端口的,而不是指I-cache和D-cache在物理上合并。这样做既符合FPGA的BRAM资源特性,又保留了指令和数据的独立地址空间。
6.2 访存仲裁与BRAM读取时序的坑
在仲裁的具体时序上,我踩过一个印象很深的坑。Xilinx的Block RAM是同步读的,也就是说,地址在时钟上升沿送入RAM,数据要等到下一个时钟上升沿才出现在输出端口。如果IF级在第一个周期把PC作为地址给RAM,第二个周期地址锁存、第三周期数据输出,那IF级一个周期根本取不到指令。很多人第一次跑仿真时看到指令数据永远晚一拍,就是这个原因。
解决办法是在IF级的设计上,不按"一个周期取一条指令"的思路,而是让取指贯穿PC更新和RAM读出的全过程:IF/ID寄存器在RAM输出数据的同一拍锁存指令。具体来说,PC在时钟沿更新后,组合逻辑把PC送进RAM地址端口,RAM在这一拍内部进行同步读,下一拍时钟沿到来时RAM输出数据,IF/ID寄存器也在同一拍边沿采样到有效指令。这样IF级虽然从时间上看需要两个周期,但因为PC是每个周期都在更新,RAM的数据输出也每隔一个周期送出一条有效指令,等价于每周期取一条指令,只是有效指令实际落后PC一拍。要保证这个对齐关系正确,关键是让IF/ID寄存器的时钟沿与RAM输出沿严格同步,并且PC的下一次更新发生在IF/ID采到上一次指令之后。
这个细节我推荐在写代码前就先画一个"周期-信号波形图",把PC、RAM地址、RAM数据、IF/ID锁存这四条波形的对齐关系画清楚后再动手。否则等到仿真报错再回头查,会浪费大量时间。
关于RAM写时序,数据存储器采用同步写:写使能和地址、数据在时钟上升沿同时送入,上升沿后写入完成。这也意味着MEM级的写操作能在一个周期内完成,和ID级的读寄存器文件互不干扰。寄存器文件本身我用的是同步写、异步(组合)读的方式,这样ID级在同拍就能读到最新数据,减少一部分前推压力。
7. 测试与验证体系
7.1 从C测试程序到存储器镜像的完整流程
很多课程设计做完硬件就卡在验证环节:光调波形太痛苦,C程序又不知道如何跑进去。这个项目打通了一条从C语言测试程序到硬件仿真的完整链路,工具链用的是RISC-V GNU工具链。流程分四步。
第一步,写C测试代码。比如写一个简单的累加程序,计算1到100的累加和,放到最后的特定地址;第二步,用riscv64-unknown-elf-gcc编译成ELF文件,注意要用-march=rv32i -mabi=ilp32指定32位基础整数指令集,避免编译器生成RV32IM或带扩展指令的代码,否则硬件不支持会直接跑飞;第三步,用riscv64-unknown-elf-objcopy把ELF转成二进制镜像;第四步,把二进制镜像按字节拆成存储器的初始化文件,用Verilog的$readmemh或$readmemb加载到指令存储器模型中。
这个流程里有个重点坑:RV32I的Load指令要求地址对齐,lw必须4字节对齐,lh必须2字节对齐,如果编译器生成的数据布局里有未对齐的访问,单周期仿真的硬件里可能不会报错,但行为不符合规范,结果就不稳定。所以链接脚本要设置好段的对齐属性,把.text段的起始地址设置在0x00000000,把.data段对齐到4字节边界。
还要注意一个细节:RISC-V的栈指针x2/sp在启动代码里必须初始化。C程序会用到栈,如果不初始化sp,所有函数调用的压栈和弹栈都会写到地址0附近的区域,把指令和数据全冲掉。我在测试方案里加了一个startup.s,里面把sp初始化为存储器最高地址,然后跳转到main。这个启动汇编很小,但缺少它的话,斐波那契这类带递归的测试用例会直接炸掉。
7.2 测试用例的分层设计与"通过"判定
测试用例设计要和流水线的风险点一一对应。我按五层组织:
- 基础指令层:每条加法、减法、逻辑运算指令单独执行一次,结果写入不同的寄存器,最终比对寄存器窗口。
- 访存指令层:测试lw/sw/lb/lh/lbu/lhu/sb/sh的读写回环,覆盖不同偏移地址的正负边界,主要验证地址计算和存储器的字节使能逻辑。
- 冒险触发层:专门构造RAW紧邻序列、load-use序列、相隔两条指令的前推序列,验证前推和stall功能。
- 控制流层:写一个带分支的循环,比如冒泡排序,要求分支方向不单一,同时覆盖forward和backward分支;还要测试JALR的函数调用返回路径。
- 综合压力层:跑一个稍大的算法,比如快速排序或矩阵乘法,跑完后把结果写入特定的magic地址。
"通过"的判定不能靠肉眼看波形。我在Testbench里实现了一个自动化比较:硬件执行完毕后,Testbench自动读取寄存器文件和内存区域,与预先导出的黄金模型结果做逐项对比,全部一致则打印PASS,否则打印第一个不一致的位置。这个黄金模型就是riscv-gcc编译出来的程序在参考模拟器上运行的结果,可以用QEMU模拟器配合GDB获取,也可以用SPIKE模拟器直接dump寄存器。这样验证就不是"看起来好像对",而是有一份可判定的对照标准。
8. 常见问题排查实录
8.1 典型Bug速查表
做这个项目过程中,我前后踩了大小无数个坑,这里整理成一张速查表,每一条都对应一个真实发生过的故障:
| 症状 | 可能原因 | 修复方向 |
|---|---|---|
| 第一条指令执行后寄存器全是X态 | 寄存器文件写入时序和复位时序冲突 | 检查写端口时钟沿,确保WB级写回与时钟边沿对齐,写使能要严格限制 |
| 程序总是在某条load指令后错乱 | 未处理load-use冒险,或stall时没有正确注入NOP | 检查冒险检测条件,stall时ID/EX控制信号全部清零 |
| store指令写入的数据一直是旧值 | store的写数据入口没有做前推连接 | 增加MEM/WB到MEM级的store数据前推,并且要与EX/MEM直接结果区分优先级 |
| 分支判断总是晚一拍 | 分支结果在EX级算出后没有立即冻结IF/ID,导致多取一条错误指令 | 检查BranchTaken信号的时序,它在EX级边沿后要立即控制PC选择 |
| JALR跳转后回不到正确地址 | JALR目标地址最低位没有按规范清零 | RV32I规范要求JALR的目标地址bit0强制为0,检查地址计算逻辑 |
| 仿真波形里指令数据总晚一拍 | BRAM同步读特性没处理好 | 按6.2节的波形对齐方法,调整IF/ID锁存时机 |
| 程序里用了除法乘法指令直接跑飞 | 编译器默认生成了M扩展指令 | 编译时指定-march=rv32i,或检查反汇编文件确认所有指令都在42条以内 |
8.2 修复经验和一个值得记录的时序问题
在上述问题中,最值得展开讲的是"stall时注入NOP"这个坑。我在第一版里只冻结了PC和IF/ID寄存器,没有把ID/EX寄存器清零,结果load-use后,原本应该被气泡替代的那条add指令提前进入了EX级,用了旧值做了加法,并且把结果写进了寄存器。这个bug极其隐蔽,因为波形上看,寄存器写入使能信号仍然是1,写入的数值看起来也有模有样,但就是和黄金模型对不上。后来在Testbench里加了"寄存器写入时刻对应指令类型"的观测逻辑,才发现写回使能信号的来源指令不对。修复方式很简单:stall信号有效时,把ID/EX的WB_Control和MEM_Control全部拉低,RegWrite置0,MemWrite置0,这样就切断了后续一切副作用。
另一个值得记录的时序问题是在分支冲刷和load-use stall同时发生时。如果一条load指令后面紧跟着一个分支指令,而分支又恰好需要前推load的结果,那三者的交互就会很复杂。我一开始把stall和flush当成互斥事件处理,结果仿真发现极少数情况下流水线会丢失PC更新,导致取指卡死。最终方案是让stall优先级高于flush:检测到load-use时先停顿,下一拍load结果进入MEM/WB后,再对分支的结果进行判断,必要时冲刷。这个处理方式保证了不会出现"边停顿边冲刷"的竞争态。
如果你要把这个项目继续往下做,我之后实际体验中最建议的方向有两个,都很有价值:一是把分支判断从EX级提前到ID级,可以用一个额外的比较器,把错误预测代价从3拍降低到2拍;二是给处理器加一个简单的中断控制器和CSR寄存器组,这会让它从裸机循环跑测试程序,进化到能跑RTOS内核的程度。但这两个方向都是后话,前提是你把当前这版流水线的每一个波形都吃透,理解清楚每一条前推路径为什么存在、什么时候生效,再去扩展。毕竟流水线这玩意,万变不离其宗,真正懂了RAW和stall,后面所有架构都是它们的变体。
本文还有配套的精品资源,点击获取