【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】
前面我们说过,数字电路里面流水线的引入,主要是为了提高数据的处理效率。那么,鉴于此,为什么又要对流水线进行暂停处理呢?直接在某一个阶段,把所有的工作都完成不行吗?举个例子来说,如果指令在exe阶段的时候,同时需要处理乘法和加法(madd指令),那样不是也可以吗。其实,这么做,确实是可以的。但是,真的这么做的话,会大幅度降低时钟运算频率,那样会反而得不偿失了。
1、从verilog到电路
大家习惯于编写verilog代码。但是本质上来说,这其实也是在设计电路。只不过,我们设计的是数字电路。现在假设有这样一段代码,
- reg d;
- always @(posedge clk)
- if(a)
- d <= b;
- else
- d <= c;
这段代码的功能比较简单。就是说,在每次时钟上升沿发生的时候,如果a为真,将b传递给d;反之,将c传递给d。大家可能在看这段代码的时候,认为只是一个简单的时序电路。其实,这里面既包含了组合逻辑,也包含了时序逻辑,
- wire temp;
- assign temp = a ? b:c;
-
- reg d;
- always @(posedge clk)
- d <= temp;
上面的代码换一种写法,这样就比较容易看清楚了。所有的信号都会汇集成temp,也就是一个wire。最终在clk上升沿发生的时候,把temp传递给d。到了这一步,其实继续扩展一下思路,想象下翻译成的最终电路是什么样子的,

上面这个电路其实就能说明数字电路翻译的基本原理了。如果信号a为真,那么下面c的输入其实无关紧要了。因为此时信号的输出只和b相关;同样而言,如果信号a为假,那么上面b的信号就无关紧要了,输出信号就只和c有关。通过上图可以看出,两个信号都会通过或门直接传递给触发器。这样在时钟沿来的时候,把这个信号传递给d寄存器了。
可以看到,如果门电路不是很多的时候,那么组合逻辑的延时不是很明显。但是如果时序逻辑之间的组合逻辑过于复杂,那么整个电路的时钟频率是很难拉起来的。这个时候,与其降低时钟频率,不如暂停一下流水线,等待几个时钟反而要划算的多。
2、添加更多的运算指令
前面一章引入了HI、LO寄存器,这就说明可以添加更多的数学运算指令了,而不仅仅是之前的逻辑运算指令、移位运算指令。常见的简单算术操作都可以添加进来了,比如说add、addi、addiu、addu、sub、subu、clo、clz、slt、slti、sltu、mul、mult、multu。关于乘法,这里有三个指令,分别是mul、mult、multu。注意,mul是把结果保存在通用寄存器里面的,而mult和multu是把运算结果保存在HI、LO寄存器里面的。
准备必要的汇编代码,
- .org 0x0
- .set noat
- .global _start
- _start:
-
- ######### add\addi\addiu\addu\sub\subu ##########
-
- ori $1,$0,0x8000 # $1 = 0x8000
- sll $1,$1,16 # $1 = 0x80000000
- ori $1,$1,0x0010 # $1 = 0x80000010
-
- ori $2,$0,0x8000 # $2 = 0x8000
- sll $2,$2,16 # $2 = 0x80000000
- ori $2,$2,0x0001 # $2 = 0x80000001
-
- ori $3,$0,0x0000 # $3 = 0x00000000
- addu $3,$2,$1 # $3 = 0x00000011
- ori $3,$0,0x0000 # $3 = 0x00000000
- add $3,$2,$1 # overflow,$3 keep 0x00000000
-
- sub $3,$1,$3 # $3 = 0x80000010
- subu $3,$3,$2 # $3 = 0xF
-
- addi $3,$3,2 # $3 = 0x11
- ori $3,$0,0x0000 # $3 = 0x00000000
- addiu $3,$3,0x8000 # $3 = 0xffff8000
-
- ######### slt\sltu\slti\sltiu ##########
-
- or $1,$0,0xffff # $1 = 0xffff
- sll $1,$1,16 # $1 = 0xffff0000
- slt $2,$1,$0 # $2 = 1
- sltu $2,$1,$0 # $2 = 0
- slti $2,$1,0x8000 # $2 = 1
- sltiu $2,$1,0x8000 # $2 = 1
-
- ######### clo\clz ##########
-
- lui $1,0x0000 # $1 = 0x00000000
- clo $2,$1 # $2 = 0x00000000
- clz $2,$1 # $2 = 0x00000020
-
- lui $1,0xffff # $1 = 0xffff0000
- ori $1,$1,0xffff # $1 = 0xffffffff
- clz $2,$1 # $2 = 0x00000000
- clo $2,$1 # $2 = 0x00000020
-
- lui $1,0xa100 # $1 = 0xa1000000
- clz $2,$1 # $2 = 0x00000000
- clo $2,$1 # $2 = 0x00000001
-
- lui $1,0x1100 # $1 = 0x11000000
- clz $2,$1 # $2 = 0x00000003
- clo $2,$1 # $2 = 0x00000000
-
- ori $1,$0,0xffff
- sll $1,$1,16
- ori $1,$1,0xfffb # $1 = -5
- ori $2,$0,6 # $2 = 6
- mul $3,$1,$2 # $3 = -30 = 0xffffffe2
-
- mult $1,$2 # hi = 0xffffffff
- # lo = 0xffffffe2
-
- multu $1,$2 # hi = 0x5
- # lo = 0xffffffe2
- nop
- nop
-
翻译成inst_rom.data文件,
- 34018000
- 00010c00
- 34210010
- 34028000
- 00021400
- 34420001
- 34030000
- 00411821
- 34030000
- 00411820
- 00231822
- 00621823
- 20630002
- 34030000
- 24638000
- 3401ffff
- 00010c00
- 0020102a
- 0020102b
- 28228000
- 2c228000
- 3c010000
- 70221021
- 70221020
- 3c01ffff
- 3421ffff
- 70221020
- 70221021
- 3c01a100
- 70221020
- 70221021
- 3c011100
- 70221020
- 70221021
- 3401ffff
- 00010c00
- 3421fffb
- 34020006
- 70221802
- 00220018
- 00220019
- 00000000
- 00000000
总共汇编代码有43行,所有生成的汇编指令也有43条。

做好了所有的准备工作之后,就可以开始分析波形图。依次把pc寄存器、wb寄存器、regfile寄存器、wb_hi&wb_lo寄存器、hi_o&lo_o寄存器拉进来。从第一条指令,向1号寄存器写入0x00008000,接着第二条指令继续向1号寄存器写入0x80000000,这样不断通过阅读寄存器的数值,结合阅读汇编代码,来验证cpu的实现是否正确。如果错误,还要回头看一下中间的时序逻辑和组合逻辑对不对。
如果希望查看HI和LO寄存器,可以直接选中w_whilo,查看当w_whilo为1的时候,写入的数值是什么,对应的汇编指令是什么,和我们之前的期待是不是一样的。

3、暂停流水线
之前我们谈到了暂停流水线这个功能,可是怎么实现这个功能呢?一般来说,对于cpu各个执行模块,还会衍生出来一个control模块,这个control模块会控制流水线是暂停,还是flush掉。今天我们讨论的情况仅仅是暂停。而且就是算暂停,也有可能出现一部分暂停、一部分不暂停的情况。
- `include "defines.v"
-
- module ctrl(
-
- input wire rst,
-
- input wire stallreq_from_id,
-
-
- input wire stallreq_from_ex,
- output reg[5:0] stall
-
- );
-
-
- always @ (*) begin
- if(rst == `RstEnable) begin
- stall <= 6'b000000;
- end else if(stallreq_from_ex == `Stop) begin
- stall <= 6'b001111;
- end else if(stallreq_from_id == `Stop) begin
- stall <= 6'b000111;
- end else begin
- stall <= 6'b000000;
- end
- end
-
-
- endmodule
这就是负责对流水线进行仲裁的ctrl模块,它搜集各个模块的请求,又马上给出一个综合的结果进行输出处理。以pc_reg.v为例,
-
- `include "defines.v"
-
- module pc_reg(
-
- input wire clk,
- input wire rst,
-
- //来自控制模块的信息
- input wire[5:0] stall,
-
- output reg[`InstAddrBus] pc,
- output reg ce
-
- );
-
- always @ (posedge clk) begin
- if (ce == `ChipDisable) begin
- pc <= 32'h00000000;
- end else if(stall[0] == `NoStop) begin
- pc <= pc + 4'h4;
- end
- end
-
- always @ (posedge clk) begin
- if (rst == `RstEnable) begin
- ce <= `ChipDisable;
- end else begin
- ce <= `ChipEnable;
- end
- end
-
- endmodule
没有递增之前,可能pc不停增加就可以了。现在的话,pc需要接收stall的结果,且只有在stall不存在的时候,才能不停递增pc寄存器。
之前我们提到过madd指令,现在就看看,如果有了这个指令,ex阶段应该怎么处理,
- case (aluop_i)
- `EXE_MADD_OP, `EXE_MADDU_OP: begin
- if(cnt_i == 2'b00) begin
- hilo_temp_o <= mulres;
- cnt_o <= 2'b01;
- stallreq_for_madd_msub <= `Stop;
- hilo_temp1 <= {`ZeroWord,`ZeroWord};
- end else if(cnt_i == 2'b01) begin
- hilo_temp_o <= {`ZeroWord,`ZeroWord};
- cnt_o <= 2'b10;
- hilo_temp1 <= hilo_temp_i + {HI,LO};
- stallreq_for_madd_msub <= `NoStop;
- end
- end
简单分析下,初次计算madd的时候,cnt_i肯定是0,那么这里肯定显示算一个临时结果hilo_temp_o,同时stallreq_for_madd_msub申请流水线暂停,cnt_o修改为1;等到下一个周期的时候,将之前的hilo_temp_o通过hilo_temp_i重新获取进来,和HI、LO重新做计算,生成hilo_temp1,stallreq_for_madd_msub也恢复。注意,这里cnt_o没有直接设置为2’b00,主要是为了防止其他情形导致流水线暂停的时候,不会出现重复计算的情况发生。
另外注意,这里的cnt_i、cnt_o都被ex_mem模块保存了。
一切都准备好了,就可以开始测试了。先准备好汇编文件,
-
- .org 0x0
- .set noat
- .global _start
- _start:
- ori $1,$0,0xffff
- sll $1,$1,16
- ori $1,$1,0xfffb # $1 = -5 为寄存器$1赋初值
- ori $2,$0,6 # $2 = 6 为寄存器$2赋初值
-
- mult $1,$2 # hi = 0xffffffff
- # lo = 0xffffffe2
-
- madd $1,$2 # hi = 0xffffffff
- # lo = 0xffffffc4
-
- maddu $1,$2 # hi = 0x5
- # lo = 0xffffffa6
-
- msub $1,$2 # hi = 0x5
- # lo = 0xffffffc4
-
- msubu $1,$2 # hi = 0xffffffff
- # lo = 0xffffffe2
接着就是翻译成二进制文件数据,
- 3401ffff
- 00010c00
- 3421fffb
- 34020006
- 00220018
- 70220000
- 70220001
- 70220004
- 70220005
有了这一切就可以开始进行波形图的分析了,

因为涉及到流水线的查看,所以这个时候可以重点观察下stall信号和pc地址的变更。观察发现,210ns的时候ce生效。290ns的时候数据准备写入,写入的地址是寄存器1,写入的数据是0x0000ffff,这和我们观察的汇编指令代码是一致的。

在350ns的时候,我们发现pc地址出现了暂停,stall信号出现了,这个时候表明处于ex阶段的应该是madd指令。注意,不是mult指令,mult是在不需要流水线暂停的。为了判断我们的分析是否正确,可以把mem_whilo、mem_hi、mem_lo信号加进来。

可以看到除了第一次mem_whilo为1,是因为乘法运算之后,后面的每一次计算,都需要等待两个周期,mem_hi和mem_lo才会又获得正确的数据。 以此类推,后面的几条指令,都会发生流水线暂停的操作,每次都是暂停1个时钟周期后,又立马回复正常。整个流程就是这样一个结果。
插曲:
关于除法的部分,文章并没有涉及,但是Chapter7_3中给出了完整的代码,大家可以去看看。只不过,作者给出的目录没有把名字写对,写成了Chpater7_3。