CPU的流水线设计
一、CPU的流水线通常被划分为五个部分,它们是:
*
取指令(Instruction Fetch):从内存中获取指令并将其放入指令寄存器中。
*
指令译码(Instruction Decode):将指令从指令寄存器中读取并解码成相应的操作。
*
执行指令(Execution):根据操作码执行指令,可能需要读取寄存器或内存中的数据,并对其进行运算。
*
访问存储器(Memory Access):如果执行的指令需要访问存储器,则在这个阶段进行。
*
写回结果(Write Back):将指令执行的结果写回到寄存器中。
这些部分在流水线中依次执行,每个部分都可以同时处理不同的指令。这种流水线的设计可以提高CPU的吞吐量,从而提高计算机的性能。
取指
在RISC-V架构中,取指令的过程可以简单地概括为从内存中读取指令并将其放入指令寄存器中。下面是RISC-V取指令的详细过程和主要内容:
*
取指令地址(PC值):在RISC-V中,指令地址存储在一个特殊的寄存器中,称为程序计数器(PC)寄存器。当CPU执行一条指令时,PC寄存器中的值会被更新为下一条指令的地址。
*
计算下一条指令地址:在RISC-V中,指令的长度是固定的,通常为4字节(32位),因此下一条指令的地址可以通过将PC值加上4来计算得到。
*
从内存中读取指令:使用计算出的指令地址从内存中读取指令。在RISC-V中,指令的地址通常必须是4的倍数,因此指令的读取可以按照4字节的边界进行。
*
将指令存储到指令寄存器中:一旦指令被读取,它将被存储到一个专用的寄存器中,称为指令寄存器(IR)。指令寄存器是一个高速缓存,可以在CPU的不同阶段中快速访问指令。
在RISC-V架构中,指令的主要内容包括操作码(Opcode)和操作数(Operand)。操作码指定指令要执行的操作类型,如加法、乘法、移位等。操作数则包含了指令要操作的数据或地址,它们可能是立即数(Immediate)、寄存器(Register)或内存地址(Memory
Address)等。
此外,在RISC-V中,还有一些特殊的指令格式,如Jump指令、Branch指令和Load/Store指令。这些指令可以用于实现程序的跳转、条件执行和数据的读写等操作。在取指令阶段,CPU会根据指令的类型和格式进行解码,并将其传递给执行阶段以完成实际的操作。
*
Jump指令(J型指令):Jump指令用于实现无条件跳转,它的操作码为"J"。Jump指令的操作数包含一个立即数,用于指定跳转的目标地址。由于指令地址是4字节对齐的,因此Jump指令的目标地址通常使用相对于当前指令的偏移量表示,而不是绝对地址。
*
Branch指令(B型指令):Branch指令用于实现条件分支,它的操作码为"B"。Branch指令的操作数包含两个寄存器和一个立即数,用于比较两个寄存器的值并根据比较结果选择是否跳转。Branch指令的目标地址也通常使用相对于当前指令的偏移量表示。
*
Load/Store指令(I型和S型指令):Load/Store指令用于实现数据的读写,它们的操作码分别为"LOAD"和"STORE"。Load/Store指令的操作数包含一个基址寄存器和一个偏移量,用于计算出实际的内存地址。I型指令使用立即数作为偏移量,而S型指令使用第二个寄存器作为偏移量。
*
系统调用指令(ECALL指令):ECALL指令用于实现系统调用,它的操作码为"ECALL"。ECALL指令没有操作数,当CPU执行ECALL指令时,会触发操作系统的相关功能,如进程调度、文件操作等。
以上是RISC-V中常见的几种特殊指令格式,它们可以用于实现复杂的程序控制和数据处理操作。需要注意的是,不同的指令格式在解码和执行时可能需要不同的硬件支持,因此在设计RISC-V处理器时需要考虑到这些因素。
取指(Fetch)阶段
RISC-V处理器的取指(Fetch)阶段,其中包含了几个重要的步骤和概念:
*
分支预测:分支指令是在执行时根据条件选择跳转到不同的地址,但是由于分支目标通常无法提前确定,因此在取指阶段需要对分支指令进行预测。分支预测的目的是尽可能减少分支带来的流水线停顿和延迟。
*
程序计数器 PC:程序计数器(Program
Counter,PC)是一个寄存器,它存储当前指令的地址。在取指阶段,PC的值会被更新,以便获取下一条指令的地址。如果分支预测成功,那么PC的值将被修改为分支目标地址;否则PC的值将按照顺序递增。
*
指令缓存 ICache:指令缓存是用于存储指令的一种高速缓存,它可以提高指令的访问速度。在取指阶段,处理器会向ICache发出读取指令的请求。
*
Cache Line:Cache Line是指令缓存中存储的数据块,通常大小为64字节。在取指阶段,处理器可以一次性从ICache中读取一个Cache
Line的数据,这相当于读取了多条指令。
*
指令对齐缓冲
IBF:指令对齐缓冲是一个用于识别指令边界的缓冲区。由于指令的长度可能不同,因此在从ICache中读取指令时,需要将读取的数据交给IBF进行对齐操作,以便正确地分割出每一条指令。
*
译码逻辑:译码逻辑是用于解析指令的一组硬件电路。在取指阶段完成后,指令对齐缓冲会将对齐后的指令交给译码逻辑进行解析,以便获取指令的操作码和操作数等信息。
综上所述,在RISC-V处理器的取指阶段,处理器会进行分支预测,根据预测结果更新PC的值,向ICache发出读取指令的请求,并一次性读取一个Cache
Line的数据。读取的数据会被交给指令对齐缓冲进行对齐操作,对齐后的指令会交给译码逻辑进行解析。通过这些步骤,处理器可以高效地获取指令并进行后续的执行操作。
分支指令类型
(1)无条件跳转/分支( nc nditional Jump/B
)指令,是指(无需判断条件)一定会发生跳转的指令,而按照跳转的目标地址计算方式,还分为以下两种情况。
*
无条直接跳转/分支( onditional Direct Jump/B ranch
)指令,此处的“直接”是指跳转的目标地址从指令编码中的立即数可以直接计算而得。以RISC 架构中的 jal (jump and lin
)指令为例,便属于无条件直接跳转指令。jal指令的汇编示例如“ jal x5, offset ”, jal使用编码在指令字中的位立即数(有符号数〉作为偏移
offset 该偏移量乘以 ,然后与当前指令所在的地址相加,生成得到最终跳转目标地址。
*
无条件间接跳转/分支(UnconditionalIndirect
Jump/Branch)指令,此处的“间接是指跳转的目标地址需要从寄存器索引的操作数中计算出来。以RISC-V架构中的jalr (jumpand
link-register)指令为例,便属于无条件间接跳转指令。jalr 指令的汇编示例如“jalr xl,x6,offset”,jalr
与jal的不同之处在于jalr 使用编码在指令字中的12位立即数(有符号数)作为偏移量
(offset),与jalr的另外一个寄存器索引的操作数(基地址寄存器)相加得到最终的跳转目标地址。
(2)带条件跳转/分支(Conditional Jump/
Branch),是指需要判断条件而决定是否发生跳转的指令,同样按照跳转的目标地址计算方式,还分为以下两种情况。
*
带条件直接跳转/分支(Conditional
DirectJump/Branch)指令,此处的“直接”是指跳转的目标地址从指令编码中的立即数可以直接计算而得。以RISC-V架构为例,其有6条带条件分支指令(Conditional
Branch),这种带条件的分支指令跟普通的运算指令一样直接使用两个整数操作数,然后对其进行比较如果比较的条件满足,则进行跳转。
*
带条件间接跳转/分支(ConditionalIndirectJump/Branch)指令,此处的“间接”是指跳转的目标地址需要从寄存器索引的操作数中计算出来。与上述无条件间接跳转/分支指令的示例同理,但是
RISC-V 架构中没有此类型指令对于带条件跳转/分支指令而言,流水线在取指令阶段无法得知该指令的条件是否成立,
与上述无条件间接跳转/分支指令的示例同理,但是 RISC 架构中没有此类型指令。对于带条件跳转/分支指令流水线在取指令段无法得知该指令是否成立
预测方向
*
对于“方向”的预测,可以分为静态预测和动态预测两种
*
//简单的 RISC-V 取指过程中静态预测的 Verilog 代码。 //在这个代码中,使用了一个 2-bit 的饱和计数器来进行预测, //当计数器为
0 或 1 时预测分支不会发生,当计数器为 2 或 3 时预测分支会发生。 //使用了 RISC-V
指令的格式来区分分支指令。具体来说,我们检查了当前指令的前四位(即 pc[5:2]), //如果是 0000 或
0100,则这个指令可能是分支指令,我们根据具体的指令码(instr)来决定是否预测分支会发生。
//注意,这个代码只是一个简单的实现,实际的静态预测算法可能更加复杂。
//此外,这个代码还缺少一些必要的信号和模块(如时钟和复位信号),需要根据具体的使用环境进行添加。 module
static_branch_prediction( input wire [31:0] pc, input wire [31:0] instr, output
reg predict_taken ); reg [1:0] counter = 2; // 初始化饱和计数器为 2 always @(*) begin
case (pc[5:2]) 4'b0000: // 分支目标为下一个指令 counter <= (instr == 32'h6f) ? 2 : 0; //
BEQ 4'b0100: // 分支目标为当前 PC 加 4 counter <= (instr == 32'h63) ? 2 : 0; // BEQ
default: counter <= counter; // 其他情况计数器不变 endcase end always @(posedge clk)
begin if (counter == 2 || counter == 3) begin predict_taken <= 1; // 预测分支会发生
end else begin predict_taken <= 0; // 预测分支不会发生 end end endmodule //always @(*)
begin 是一种组合逻辑块的写法,它表示这个块会在任何一个输入信号发生变化时重新计算输出。 //具体来说,@(*)
表示这个组合逻辑块会对所有的输入信号(包括 input 和 inout 类型)进行敏感, //只要有任何一个输入信号发生变化,就会触发这个块重新计算。
//因此,always @(*) begin 通常用于计算纯组合逻辑,例如使用逻辑运算符、位移运算符等进行计算,
//而不涉及时序逻辑(如寄存器等)。在这个块内部,我们可以通过 case、if 等语句来实现逻辑运算,得到一个组合逻辑输出。
未完待续