記事一覧へ
计算机组成日本語

北航计算机组成原理 P6

扩展流水线 CPU 的指令与模块,涵盖字节访存、乘除单元、跳转控制和冒险处理。

封面:Photo by Nguyen Minh · Unsplash

目次

一. 设计草稿

模块功能的扩展和新模块的添加

1. ALU的功能扩展

P6课下新增加了and,slt,sltu等指令,这需要我们扩展ALU,使其支持新的运算类型,好在我们提前把ALUop的位宽设为了四位,因此直接对新运算类型进行编码添加即可

实现如下

module ALU (
    input [31:0] A,
    input [31:0] B,
    input [3:0] ALUop,
    output reg [31:0] result
);

    always @(*)begin

        case(ALUop)
        4'b0000: result = A;
        4'b0001: result = B;
        4'b0010: result = A + B;
        4'b0011: result = A - B;
        4'b0100: result = A & B;
        4'b0101: result = A | B;
        4'b0110: result = ($signed(A) < $signed(B))? 1:0;
        4'b0111: result = ($unsigned(A) < $unsigned(B))? 1:0;
        endcase
        
    end
endmodule

说明:这里要注意有符号运算和无符号运算的区别

2. BEXT和DEXT的设置

为了实现P6的新指令lb,lh,sb,sh,我们需要对内存的写入和读取操作进行一定的修改 以sb指令(save byte)为例:

Save byte 的内存写入示意

我们需要取出GRF[rt]的低八位,然后根据存储地址的低两位将其存到不同的字节位置

为了实现这一操作,我们设置了BEXT来处理储存数据并设置使能信号

实现如下

module BEXT (
    input [31:0] WDin,
    input [1:0] SaveType,
    input [1:0] A,
    output reg [3:0] Bytten,
    output reg [31:0] WDout
);
    always @(*) begin
        // 每次先初始再特判
        WDout = WDin;
        Bytten = 4'b0;
        // 判断使能信号以及要填入的位置
        if (SaveType == 3) begin
            // sw
            Bytten = 4'b1111;
        end else if (SaveType == 2) begin
            // sh
            if (A[1] == 0) begin
                Bytten = 4'b0011;
            end else if (A[1] == 1) begin
                Bytten = 4'b1100;
                WDout[31:16] = WDin[15:0];
            end
        end else if (SaveType == 1) begin
            // sb
            Bytten[A] = 1;
            case (A)
                2'b00: WDout[7:0] = WDin[7:0];
                2'b01: WDout[15:8] = WDin[7:0];
                2'b10: WDout[23:16] = WDin[7:0];
                2'b11: WDout[31:24] = WDin[7:0];
            endcase
        end else begin
            // 非save
            Bytten = 4'b0;
        end
    end
endmodule

不妨结合官方的testbench来理解这一行为

always @(*) begin
    fixed_wdata = data[m_data_addr >> 2];
    fixed_addr = m_data_addr & 32'hfffffffc;
    if (m_data_byteen[3]) fixed_wdata[31:24] = m_data_wdata[31:24];
    if (m_data_byteen[2]) fixed_wdata[23:16] = m_data_wdata[23:16];
    if (m_data_byteen[1]) fixed_wdata[15: 8] = m_data_wdata[15: 8];
    if (m_data_byteen[0]) fixed_wdata[7 : 0] = m_data_wdata[7 : 0];
end

always @(posedge clk) begin
    if (reset) for (i = 0; i < 4096; i = i + 1) data[i] <= 0;
    else if (|m_data_byteen) begin
        data[fixed_addr >> 2] <= fixed_wdata;
        $display("%d@%h: *%h <= %h", $time, m_inst_addr, fixed_addr, fixed_wdata);
    end
end

可以看到,我们实际上的做法是把地址对应内存的整字数据取出来,然后根据字节使能信号m_data_byteen对其进行修改,以达到存入新数据的目的


相应的,在取数据时,我们就应该根据地址的低两位与load类型判断要取得数据属于哪些字节,并对其进行扩展,由此我们设置了DEXT模块

实现如下:

module DEXT (
    input [31:0] RDin,
    input [1:0] LoadType,
    input [1:0] A,
    output reg [31:0] RDout
);
    always @(*) begin
        if (LoadType == 3) begin
            // lw
            RDout = RDin;
        end else if (LoadType == 2) begin
            // lh
            if (A[1] == 1) begin
                // 加载高位
                RDout = {{16{RDin[31]}},RDin[31:16]};
            end else if (A[1] == 0) begin
                // 加载低位
                RDout = {{16{RDin[15]}},RDin[15:0]};
            end 
        end else if (LoadType == 1) begin
            // lb
            case (A)
                2'b00: RDout = {{24{RDin[7]}},RDin[7:0]};
                2'b01: RDout = {{24{RDin[15]}},RDin[15:8]};
                2'b10: RDout = {{24{RDin[23]}},RDin[23:16]};
                2'b11: RDout = {{24{RDin[31]}},RDin[31:24]};
            endcase
        end else begin
            // 非load
            RDout = RDin;
        end
    end
endmodule

注意当前我们的lw,lh,lb都需要符号扩展

3. 乘除模块的设置

实话实说,在简单看完一遍教程之后我并不是很好的理解了乘除模块的行为,比如start信号如何产生,在翻阅了讨论区和往年博客之后理解了乘除模块作为有限状态机的本质,其实就是根据当前的状态start,busy或者空闲,结合MDUop的输入改变我们的状态

实现如下:

module MDU (
    input clk,
    input reset,
    input [31:0] A,
    input [31:0] B,
    input [3:0] MDUop,
    output [31:0] MDUresult,
    output reg busy,
    output reg start
);
    reg [31:0] HI;
    reg [31:0] LO;
    reg [31:0] HItemp;
    reg [31:0] LOtemp;
    reg [3:0] Tmax;
    reg [3:0] cnt;
    // 读取逻辑
    assign MDUresult = (MDUop == 4'b0101) ? HI : 
                       (MDUop == 4'b0110) ? LO : 0;
    initial begin
        HI = 0;
        LO = 0;
        HItemp = 0;
        LOtemp = 0;
        Tmax = 0;
        busy = 0;
        cnt = 0;
        start = 0;
    end

    always @(*) begin
        case (MDUop)
            4'b0001: begin
                {HItemp, LOtemp} <= $signed(A) * $signed(B);
                Tmax <= 4'b0101;
                start <= 1;
            end
            4'b0010: begin
                {HItemp, LOtemp} <= $unsigned(A) * $unsigned(B);
                Tmax <= 4'b0101;
                start <= 1;
            end
            4'b0011: begin
                HItemp <= $signed(A) % $signed(B);
                LOtemp <= $signed(A) / $signed(B);
                Tmax <= 4'b1010;
                start <= 1;
            end
            4'b0100: begin
                HItemp <= $unsigned(A) % $unsigned(B);
                LOtemp <= $unsigned(A) / $unsigned(B);
                Tmax <= 4'b1010;
                start <= 1; 
            end
            4'b0111: begin
                HI <= A;
                start <= 0;
            end 
            4'b1000: begin
                LO <= A;
                start <= 0;
            end 
            default: start <= 0;
        endcase
    end
    // busy控制
    always @(posedge clk) begin
        if (reset) begin
            HI <= 0;
            LO <= 0;
            HItemp <= 0;
            LOtemp <= 0;
            Tmax <= 0;
            busy <= 0;
            cnt <= 0;
            start <= 0;
        end else if (busy) begin
            if(cnt == Tmax - 1)begin
                HI <= HItemp;
                LO <= LOtemp;
                cnt <= 0;
                busy <= 0;
            end else begin
                cnt <= cnt + 1;
                HI <= HI;
                LO <= LO;
            end
        end else if (start) begin
            busy <= 1;
            cnt <= 0;
            start <= 0;
        end
    end
endmodule

说明:

  1. MDU需要我们内置HI和LO两个寄存器存储计算结果
  2. 当新的指令进入E级时,我们先判断该指令的操作类型,对于mfhi,mflo,mthi,mtlo这四种指令,我们不需要将start置1,直接执行相应操作即可,需要注意对MDUop的判断必须是组合逻辑,当指令进入E级后,我们需要立刻给出正确的start信号,不然可能会导致阻塞有误影响输出结果
  3. 为了模拟真实计算乘除法时的延迟,我们这里设置了HItemp和LOtemp两个寄存器,当最后一个周期结束后,再将其写入HI,LO寄存器

4. NPC模块的设置

为了应对本次新加入的bne指令和课上需要添加的跳转类型指令,使得我们的跳转和连接逻辑更加清晰,我将以往的跳转相关的代码单独组合出来设立了独立的NPC模块

实现如下:

module NpcCTRL (
    input [31:0] PCD,
    input [1:0] btype,
    input jal,
    input jr,
    input [31:0] Imm32D,
    input [25:0] imm26,
    input [31:0] R1D,
    input [31:0] R2D,
    input RegWritetemp,
    output [31:0] NPC,
    output PCsrc,
    output link,
    output ClearDelayBar,
    output RegWriteD
);

    wire branch;
    wire [31:0] PCadd4;
    wire [31:0] ImmJump;

    // 比较器
    CMP cmp(
        .R1D(R1D),
        .R2D(R2D),
        .btype(btype),
        .branch(branch)
    );

    // NPC逻辑
    assign PCadd4 = PCD + 32'h0000_0004;
    assign ImmJump = {PCadd4[31:28], imm26, 2'b00};
    assign NPC = branch? ($signed(Imm32D) + $signed(PCadd4)) :
                 jal? ImmJump :
                 jr? R1D : PCadd4;
    assign PCsrc = branch | jal | jr;
    assign link = jal;

    // 预留功能,清除延迟槽
    assign ClearDelayBar = 0;
    
    // 预留功能,根据跳转情况判断写入
    assign RegWriteD = RegWritetemp;
    
endmodule

其中也对课上可能出现的特殊指令类型预留了一些功能,增加了可拓展性


冒险控制

沿用AT模型法,对于我们添加的这些指令只要正确地设置了每条指令的Tnew和Tuse,那么我们就几乎可以做到不修改原来的控制逻辑代码

需要注意的是:对于mult,multu,div,divu,mthi,mtlo这些指令,它们虽然是R型指令,但实际上并没有对寄存器的写入操作,因此在译码时直接将RegWrite置0,也就是不会对这些指令的结果进行转发

此外,当start或者busy信号为1时,表示乘除模块正在工作,我们需要将乘除类指令阻塞在D级,而其它指令可以继续执行(ALU与MDU独立的结果),由此我们只需要在HazardCTRL加几行代码

// 乘除槽阻塞
if ((|MDUopD) && (busy || start)) begin
    pause = 1;
end

二. 测试方案

因为在P5对转发的测试已经很多了,在译码没出现问题的基础上,使用AT模型基本不会出现转发上的Bug,因此我们还是主要把精力放在新机制的实现上

乘除槽的测试

构造多条乘除类指令相连的情况,观察乘除模块的行为是否正常

// 寄存器初始化写入略
mult    $t3, $t2, $t1
mflo    $s2,
mtlo    $s1,
mflo    $s2,

这里我确实发现了一个bug,就是乘除类模块start信号的设置问题,我将start设置为了下一周期置一,导致没能实现对下面指令的阻塞

load/save类指令的测试

构造样例,看写入的数据是否正确以此判断BEXT和DEXT模块是否有误

// 寄存器初始化写入略
sw      $s1, 0($s2)
sh      $s1, -1($s2)
sb      $s1, 1($s2)
lb      $s3, 0($s2)
lw      $s4, 1($s2)
lh      $s5, -1($s2)

跳转类测试

可以在跳转类指令前构造几条与其冲突的数据,同时测试转发和跳转的逻辑

// 寄存器初始化写入略
sh      $s1, 0($s2)
lw      $s2, 0($s2)
and     $s3, $s2, $s1
bne     $s3, $s2, label:
nop
label:
add     $s4, $s2, $s3

大规模测试

可以用教程提供的测例或测评机生成的随机样例进行规模更大的测试,以此来找出可能存在的bug


在构造自己的测试方案时,建议使用这种结合两者的构造方法

三.思考题

  1. 为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器? 通过设置单独的MDU模块,即使在乘除运算延迟的期间,其它指令仍然可以在ALU中进行运算,这大大提高了我们的效率;HI,LO不属于GRF中的通用寄存器,且只用于乘除类指令,因此我们需要把它独立

  2. 真实的流水线 CPU 是如何使用实现乘除法的?请查阅相关资料进行简单说明。

    • 并行乘法器: 使用布斯算法或改进的布斯算法(如 Booth-3),配合部分积压缩器(如 Wallace Tree 或 Dadda Tree),实现多位乘法的快速计算。
    • 流水线乘法器: 将乘法器设计为多阶段流水线,分解为部分积生成、部分积压缩和最终加法三部分。这种方式允许乘法操作和其他指令并行执行,提高吞吐量。
    • 多时钟周期除法器: 基于恢复法(Restoring Division)或非恢复法(Non-Restoring Division),逐步生成商位和余数。适合节省硬件资源。
    • 快速除法器: 使用 SRT 算法(Sweeney, Robertson, and Tocher)或 Goldschmidt 算法,结合流水线设计,加快计算过程。
  3. 请结合自己的实现分析,你是如何处理 Busy 信号带来的周期阻塞的? 我的做法是,当D级进入的指令为乘除类型且busy或start信号为1时,就进行阻塞

    // 乘除槽阻塞
    if ((|MDUopD) && (busy || start)) begin
        pause = 1;
    end
  4. 请问采用字节使能信号的方式处理写指令有什么好处?(提示:从清晰性、统一性等角度考虑) 使用字节使能信号使得我们对save类指令有了一个统一规范的设计方案,而不需要对每个指令进行单独设计,这大大节省了我们的开发成本,更符合高内聚低耦合的原则

  5. 请思考,我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢? 否,无论是从DM中读取或是向DM写入,我们都需要根据操作的类型以及地址的最低两位对原始数据进行相应的处理;当某场景有大量的字节读取和写入需求时,如C语言的字符串,按字节读和写的效率会更高

  6. 为了对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助? 我使用了宏定义来辅助译码,这让我在译码时思路更加清晰,更不容易写错;此外AT模型的使用,对所有指令进行具体建模,让我们能更轻松高效地处理数据冲突

  7. 在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的? 乘除槽的测试

    // 寄存器初始化写入略
    mult    $t3, $t2, $t1
    mflo    $s2,
    mtlo    $s1,
    mflo    $s2,

    load/save类指令的测试

    // 寄存器初始化写入略
    sw      $s1, 0($s2)
    sh      $s1, -1($s2)
    sb      $s1, 1($s2)
    lb      $s3, 0($s2)
    lw      $s4, 1($s2)
    lh      $s5, -1($s2)

    跳转类测试

    // 寄存器初始化写入略
    sh      $s1, 0($s2)
    lw      $s2, 0($s2)
    and     $s3, $s2, $s1
    bne     $s3, $s2, label:
    nop
    label:
    add     $s4, $s2, $s3

    解决方法:转发和暂停

  8. 如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果。

    我采用了半随机生成半手动构造的测试方法,手动构造的样例中,我主要着重于设计多冲突场景,并且尽可能地覆盖多类冲突情况;使用随机生成的样例,起到一个补充和提高覆盖率的作用

DISCUSSION

コメント

元の画像を見る ↗