增添指令

注意事项:

  • 注意必须在每一级的WriteBackRegAddr检查回写的内容和地址是否符合新增指令的期望,防止在上游产生的正确回写数据在下游被覆盖!
  • 注意务必为新增指令添加阻塞板块Tuse Tnew的取值。
  • 由于特判信号是随着指令在流水线中流动的,所以其后也要加_DEin这样的后缀。
  • 注意默认拓展是0拓展,要进行符号拓展需要将新增指令的特判信号传入EXTOp

运算类

(1) 运算 + 判断 + 回写(地址确定)

P4T1添加指令为 eam,R型指令。具体操作为,取出 GPR[rs]低16位,作为一个有符号数对17取模,结果必须为0~16的某一个非负数,结果为一个16位的数。之后,若 GPR[rt] 的最高位为1,则将该运算结果零扩展至32位,否则将该运算结果一扩展至32位。将结果存至 GPR[rd] 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* 整体分析:
该题对GRF[rs]取值计算,根据GRF[rt]的最高位决定回写内容,存于固定地址
根据尽早产生数据的原则,我们希望利用D级内的组合逻辑得到正确的要回写的结果。*/

controller
wire eam = (opcode_DEin == 6'b0 && func_DEin == 6'b111110) ? 1 : 0;
/* 修改控制信号 */
RegWrite_DEin = (eam || ...) ? 1 : 0;
RegDst_DEin = (eam || ...) ? 2'b01 : ... // 写入rd寄存器
/* 生成特判信号 */
wire isEam_DEin = (eam) ? 1 : 0;

hazardStall
wire eam = (opcode_DEin == 6'b0 && func_DEin == 6'b111110) ? 1 : 0;
Tuse_RS = (eam || ...) ? 0 : ...
Tuse_RT = (eam || ...) ? 0 : ... // 在D级即需要使用
Tnew_DEin = (eam || ...) ? 1 : ... // 在D级产生结果

mips
wire [15:0] eam_low16 = A1_DEin[15:0];
wire [15:0] eam_ans16 = (($signed(eam_low16) % $signed(16'd17)) + $signed(16'd17)) % $signed(16'd17);
wire [31:0] eam_ans32 = (A2_DEin[31] == 1) ? {16'b0, eam_ans16} : {16'b1, eam_ans16};
assign WriteBack_DEin = (isEam_DEin == 1) ? eam_ans32 : ...

/* 注意还要修改下游两级的WriteBack */
assign WriteBack_EMin = (isEam_DEout === 1) ? WriteBack_DEout : ...
assign WriteBack_MWin = (MemtoReg_EMout == 1) ? WD : WriteBack_EMout; // 这一句不用修改

/* 由于回写地址是确定的,我们无需额外担心该指令结果转发的正确性 */

(2)计算 + 回写

P5T1的指令为addoi,行为是将16位立即数符号拓展或加上GRF[rs],将这个加和的相反数的原码存入rt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/* 整体分析: 该指令行为类似于addi,注意要将imm16做符号拓展 */

controller
wire addoi = (opcode_DEin == 6'b111110) ? 1 : 0;
/* 修改控制信号 */
RegWrite_DEin = (addoi || ...) ? 1 : 0;
ALUSrc_DEin = (addoi || ...) ? 1 : 0;
/*生成特判信号 */
wire isAddoi = (addoi) ? 1 : 0;

HazardStall
wire addoi = (opcode_DEin == 6'b111110) ? 1 : 0;
Tuse_RS = (addoi || ...) ? 1 : ... // 在E级使用rs
Tuse_RT = (addoi || ...) ? `maxTuse : ... // 用不到rt
Tnew_DEin = (addoi || ...) ? 2 : ... // 在E级产生新数据

module origin(
// 计算A_in的原码
input [31:0] A_in;
output [31:0] A_out;
);
reg [31:0] hold;
reg [31:0] temp;
integer i;

always@(*) begin
if (A_in > 32'h0) begin
hold = A_in;
end
else if (A_in < 32'h0) begin
temp = A_in - 32'h1;
for (i = 0; i < 31; i = i + 1) begin
hold[i] = !temp[i];
end
hold[31] = 1;
end
else begin
hold = 32'h0;
end
end
assign A_out = hold;
endmodule

mips
wire [31:0] addoi_neg = 32'h0 - ALU_ans;
wire [31:0] addoi_ans; // 32位原码

assign WriteBack_EMin = (isAddoi) ? addoi_ans : ...

跳转类

(1)判断 + 回写(地址不确定) + 跳转

P4T2添加指令为 cptlI型指令(注意,这个不是J型指令,跳转方式和beq类似)。具体操作为,若 GPR[rs] 的低18位的二进制表示中存在连续6个1,则将PC+8存于 GPR[rt] 中,否则将PC+8存于 GPR[31] 中。之后,把PC的值无条件修改为PC + 4 + {sign_ext}(offset<<2 || 0^2)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* 整体分析
这条指令类似于beq 区别在于cptl是要回写的,且回写地址不确定
同样根据尽早产生计算结果的原则,我们希望在D级就得到要回写的数据和地址
*/
controller
wire cptl = (Opcode_DEin == 6'b111110) ? 1 : 0;

/* 修改控制信号 */
RegWrtie_DEin = (cptl || ...) ? 1 : 0;
RegDst_DEin = (cptl || ...) ? 2'b10 : ... // 默认写入31号寄存器
Branch_DEin = (cptl || ...) ? 1 : 0;
Zero_DEin = (cptl || ...) ? 1 : 0; // 总是会跳转

/* 生成特判信号 */
wire isCptl_DEin = (cptl) ? 1 : 0;


hazardStall
wire cptl = (Opcode_DEin == 6'b111110) ? 1 : 0;
Tuse_RS = (cptl || ...) ? 0 : ... // 在D级用到rs数据
Tuse_RT = (cptl || ...) ? `maxTuse : ... // 不需要用到rt数据
Tnew_DEin = (cptl || ...) ? 1 : ... // 在D级产生结果


check( // 判断GRF[rs]里是否有连续的6个1,如是,yes = 1
input [31:0] A1_DEin,
output yes
);

mips
/* D级 */
assign WriteBack_DEin = (isCptl_DEin == 1) ? pc_FDout + 32'h8 : ...
assign RegAddr_DEin = (isCptl_DEin == 1 && yes == 1) ? rt_DEin : ... // 否则按照RegDst

/* E级 */
assign WriteBack_EMin = (isCptl_DEin == 1 || ...) ? WriteBack_DEout : ...
assign RegAddr_EMin = (isCptl_DEin == 1 || ...) ? RegAddr_DEout : ...

/* M级 */
assign WriteBack_MWin = (isCptl_DEin || ...) ? WriteBack_EMout : ...
assign RegAddr_MWin = (isCptl_DEin || ...) ? RegAddr_EMout : ...

// 最后,由于我们在D级就确定了要回写入哪个寄存器,即RegAddr_DEout的值是确定正确的,则不需要额外担心转发的正确性

访存类

(1)访存 + 判断 + 回写(地址不确定)

P5T3添加指令为 lwoc,I型指令。具体操作为,从DM中读取起始地址为 GPR{base} + {sign_ext}(offset<<2 || 0^2) 的一整个字,若该结果小于无符号32位整数 0x80000000,则取该结果的低4位进行零扩展后作为写入 GRF 的地址,否则取指令中的 rt 为写入 GRF 的地址,写入数据均为该结果。

(acknowledgement:Lazyfish)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* 整体分析:
该指令tle与lw大致相同,区别在于在写回地址处需要做一个选择。
且务必注意该选择给转发带来的影响。
*/
controller
wire lwoc = (opcode_DEin == 6'b111110) ? 1 : 0;

/* 修改控制信号 */
RegWrite_DEin = (lwoc || ...) ? 1 : 0; // 需要回写
RegAddr_DEin = (lwoc || ...) ? 2'b00 : ... // 首先默认写入rt
ALUOp_DEin = (lwoc || ...) ? 2'b00 : .... // 默认做加法
MemRead_DEin = (lwoc || ...) ? 1 : 0; // 需要访存
MemtoReg_DEin = (lwoc || ...) ? 1 : 0; // 访存后需要回写

/* 生成特判信号 */
wire isLwoc_DEin = (lwoc) ? 1 : 0;


hazardStall
wire lwoc = (opcode_DEin == 6'b111110) ? 1 : 0;
Tuse_RS = (lwoc || ...) ? 1 : 0; // rs用于计算地址
Tuse_RT = (lwoc || ...) ? `maxTuse : ... // 不需要用到rt的数据
Tnew_DEin = (lwoc || ...) ? 3 : ... // 新指令产生时间与lw一致


mips
/* M级 */
wire smaller = (WD < 32'h80000000) ? 1 : 0;
wire [4:0] low4_zeroExt = {1'b0, WD[3:0]};
RegAddr_MWin = (isLwoc_EMout == 1 && smaller == 1) ? low4_zeroExt : RegAddr_EMout;

/* 至此,我们能保证新增lwoc指令回写内容和地址都是正确的,但是,lwoc结果转发回去的正确性如何保证?*/

/*
我们先回顾一下阻塞时如何实现的:
当instruction_FDout对应的阻塞信号在hazardStall中产生高电平时,DEreg被清空,同时instruction_FDout和pc_FDout维持不变。这在事实上就形成了指令停留在D级,传到E级的是空抛的效果。

在这里,当我们的第n条指令是lwoc时,我们希望的是第n+1条指令停留在D级,直到正确的RegAdrr_MWout产生。也就是说,E级检查到lwoc指令就要立刻使Stall维持高电平,M级检查到lwoc指令也同样。

暂停两周期后,我们就能保证第n+1条指令所接受的转发数据的正确性。在时间要求不严格的情况下,我们成功的解决了这道题。但是,如果时间要求比较严格呢?TLE!

在这个题的语境下回写地址RegAddr_MWout有17种可能(GRF[0] ~ GRF[15] + rt。 事实上,上机更常见的情况回写地址一般只有两种选择可能),
也就是说,
我们可以在E级检测到lwoc时判断:rt_DEin和rs_DEin 是否等于 RegAddr_DEout (lwoc要回写的地址) 或 GRF[0] ~ GRF[15]。如果是,暂停。否则不暂停。M级检测是同样的道理。

一般来说,这样的优化足以使我们通过上机测试。

P5被卡时间挂掉的感想:还要特判写0号寄存器的情况进一步加速
*/

回写地址不确定

事实上,我们可以将回写地址不确定分为两类,第一种是必须要使用存储器中数据的,如上面所说的lwoc那样,另一种是在M级之前就能根据计算判断回写地址的。例如:

GRF[rt]GRF[rs]做加法,和是奇数写入rd,否则写入31号寄存器。

像这样的题目,事实上,我们总能通过在D级内实现一个组合逻辑便能得到正确回写位置(这时需要将Tues_RTTuse_RS设为0,并把Tnew_DEin设置为1)。

我们也可以把判断的事情交给E级,这样就需要处理转发正确性的问题。

如果我们在controller中总是默认写入rd寄存器,则我们只需要考虑实际写入31号寄存器且下一条指令正好使用到了31号寄存器的问题。

1
2
wire stall_E = (isxxx_DEout == 1 && (rt_DEin == 31 || rs_DEin== 31)) ? 1 : 0;
wire stall_M = (isxxx_EMout == 1 && RegAddr_MWin == 31 && (rt_DEin == 31 || rs_DEin == 31)) ? 1 : 0;

原码反码与补码

  • 原码:原码是一种简单的机器数表示法,最高位为符号位,用 0 表示正数,用 1 表示负数,其余位表示数值的绝对值。
    • 对于十进制数 +5,若用 8 位二进制原码表示,其原码为 00000101。其中最高位 0 表示正数,后面的 0000101 表示数值 5 的二进制形式。
    • 对于十进制数 -5,8 位二进制原码为 10000101,最高位 1 表示负数,后面同样是 5 的二进制表示形式。
  • 反码:对于正数,其反码与原码相同;对于负数,反码是在原码的基础上,除符号位外,其余各位按位取反(即 0 变 1,1 变 0)得到的。
    • 十进制数 +5 的 8 位二进制反码依然是 00000101,因为正数的反码和原码一致。
    • 十进制数 -5 的原码是 10000101,那么它的 8 位二进制反码则是 11111010。先保留符号位 1,然后将原码中除符号位之外的 0000101 按位取反得到 11111010
  • 补码:对于正数,其补码与原码、反码相同;对于负数,补码是在反码的基础上再加 1 得到的。
    • 十进制数 +5 的 8 位二进制补码还是 00000101
    • 十进制数 -5 的反码是 11111010,那么它的 8 位二进制补码就是 11111011,即反码 11111010 再加 1 得到。

减法溢出

考虑一个大的负数减去一个正数,如:

A - B = 0x80000000 - 0x00000001

减去B就等于加B的相反数的补码(按位取反再加1)-1的补码为0xffffffff。

1_10000000_00000000_00000000_00000000 + 1_11111111_11111111_11111111_11111111 = 11_01111111_11111111_11111111_11111111

temp[32] != temp[31] 溢出

PS:感谢lazyfish提供的题目(传送门:lazyfish-lc.github.io)