cpu设计和实现(流水线暂停)

时间:2022-11-28 10:56:01

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱: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。到了这一步,其实继续扩展一下思路,想象下翻译成的最终电路是什么样子的,

cpu设计和实现(流水线暂停)

         上面这个电路其实就能说明数字电路翻译的基本原理了。如果信号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条。

cpu设计和实现(流水线暂停)

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

        如果希望查看HI和LO寄存器,可以直接选中w_whilo,查看当w_whilo为1的时候,写入的数值是什么,对应的汇编指令是什么,和我们之前的期待是不是一样的。

cpu设计和实现(流水线暂停)

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

        有了这一切就可以开始进行波形图的分析了,

cpu设计和实现(流水线暂停)

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

cpu设计和实现(流水线暂停)

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

cpu设计和实现(流水线暂停)

        可以看到除了第一次mem_whilo为1,是因为乘法运算之后,后面的每一次计算,都需要等待两个周期,mem_hi和mem_lo才会又获得正确的数据。 以此类推,后面的几条指令,都会发生流水线暂停的操作,每次都是暂停1个时钟周期后,又立马回复正常。整个流程就是这样一个结果。