计组经验分享

经过半个学期的努力,大部分同学已经来到了险象环生的 P5 和 P6。P5 和 P6 已不再像程设、数据结构等课程作业一样只是功能单一、结构简单的设计,相反,我们设计的 CPU 是一个由多个部件组成的有机整体。因此,我们必须对系统进行良好的规划,让它拥有简洁优美的设计,并能稳定工作。

在这里,我为大家分享一些在设计复杂系统时需要遵循的通用准则,并以计组课程的 CPU 为例说明它们的具体应用。这些准则是从我个人数年的阅读、开发经验中总结出来的,希望能够为大家起到一些指引的作用。

为字面量命名

我们在编写代码时,会遇到很多“字面量”(比如指令的 opcode,寄存器号码所在的位置等)。我们来看以下代码片段:

always @(*) begin
    case (funct)
        6'b100100: begin
            regRead1 = instruction[25:21];
            regRead2 = instruction[20:16];
            regWriteSel = 1;
            destinationRegister = instruction[15:11];
            aluCtrl = 1;
        end

        6'b100101: begin
            regRead1 = instruction[25:21];
            regRead2 = instruction[20:16];
            regWriteSel = 1;
            destinationRegister = instruction[15:11];
            aluCtrl = 2;
        end

        6'b100110: begin
            regRead1 = instruction[25:21];
            regRead2 = instruction[20:16];
            regWriteSel = 1;
            destinationRegister = instruction[15:11];
            aluCtrl = 3;
        end

        6'b100111: begin
            regRead1 = instruction[25:21];
            regRead2 = instruction[20:16];
            regWriteSel = 1;
            destinationRegister = instruction[15:11];
            aluCtrl = 4;
        end
    endcase
end

这是一段用于解码 and, or, xor, nor 4 条指令的 case 语句。在这段代码中,我们可以看到许多直接使用的字面量:指令的 funct,rs、rt 和 rd 寄存器的位置,ALU 的功能控制等。这种代码会给编写和阅读带来不小的障碍(特别是当重复使用相同的字面量时)。这时,我认为应该将所有字面量命名,并使用名字来代替字面量。

对于不同类型的字面量,我们使用不同类型的替代方法。例如,对于 opcode 这种在只在当前文件使用的常量,我一般会使用 localparam;对于跨文件使用的常量,我一般会使用 `include;对于像 instruction[25:21] 这样的位操作,我一般会直接将截取结果用 wire 来表示。

改造后的代码如下:

// constants.v
`define aluOr 2
`define aluAnd 3
`define aluNor 6
`define aluXor 7

`define regWriteALU 1
`define regWriteMemRead 2
// ...
// controller.v
`include "constants.v"

localparam _and = 6'b100100;
localparam _or = 6'b100101;
localparam _xor = 6'b100110;
localparam _nor = 6'b100111;

wire [4:0] rti = instruction[20:16];
wire [4:0] rsi = instruction[25:21];
wire [4:0] rdi = instruction[15:11];

// ...

always @(*) begin
    case (funct)
        _and: begin
            regRead1 = rsi;
            regRead2 = rti;
            regWriteSel = `regWriteALU;
            destinationRegister = rdi;
            aluCtrl = `aluAnd;
        end

        _or: begin
            regRead1 = rsi;
            regRead2 = rti;
            regWriteSel = `regWriteALU;
            destinationRegister = rdi;
            aluCtrl = `aluOr;
        end

        _xor: begin
            regRead1 = rsi;
            regRead2 = rti;
            regWriteSel = `regWriteALU;
            destinationRegister = rdi;
            aluCtrl = `aluXor;
        end

        _nor: begin
            regRead1 = rsi;
            regRead2 = rti;
            regWriteSel = `regWriteALU;
            destinationRegister = rdi;
            aluCtrl = `aluNor;
        end
    endcase
end
// alu.v
`include "constants.v"

always @(*) begin
    case (ctrl)
        `aluOr:
            out = A | B;
        `aluAnd:
            out = A & B;
        `aluXor:
            out = A ^ B;
        `aluNor:
            out = ~(A | B);
    endcase
end

不直接使用字面量值,而是将其赋予含义后再使用,将有助于提高代码的可读性,减少混乱。

不要复制粘贴代码 (Don’t repeat yourself)

这里的我们不是在说复制粘贴别人的代码(如果你这样做了,课程组可能会找你麻烦),而是指不要重复自己的代码。当你在对自己的代码按下 Ctrl+V 的时候,你可能需要想一想,是否应该对代码做合理的抽象。

还是以上面那段代码为例。要编写上面的 case 逻辑,我们能想到的直接方式就是将 and 块复制 4 次,然后修改对应的 aluCtrl 逻辑。这样做的问题有:

  • 在粘贴-修改时,容易遗漏或者看错
  • 在修改设计时,需要对每个粘贴的地方都进行同样的编辑

一个更好的做法是,在复制-粘贴代码前,不妨考虑一下自己为什么要复制,然后将相同的内容抽象出来(以 `definemodule 的形式),然后仅修改不同的部分。

例如,对于上一节的控制器,我们可以发现,对于简单逻辑运算(甚至包括加、减等运算),大部分内容(例如要读取、写入的寄存器)都是相同的,只有少数内容(ALU 的控制信号)不同。因此,我们可以将相同的内容抽象进一个 `define块中。

修改后的代码如下:

// 在 define 中实现多行定义,只需在行尾增加反斜杠
`define simpleALU               \
    regRead1 = rsi;             \
    regRead2 = rti;             \
    regWriteSel = `regWriteALU; \
    destinationRegister = rdi;

always @(*) begin
    case (funct)
        _and: begin
            `simpleALU
            aluCtrl = `aluAnd;
        end
        _or: begin
            `simpleALU
            aluCtrl = `aluOr;
        end
        _nor: begin
            `simpleALU
            aluCtrl = `aluNor;
        end
        _xor: begin
            `simpleALU
            aluCtrl = `aluXor;
        end
    endcase
end

可以看到,合理的抽象能够极大地减少代码长度,使维护变得容易。我的 P6 项目仅有 1263 行代码,甚至比一些同学的 P5 项目更短。

想要了解 DRY 原则的更多内容,可以参考这里

保持简单设计 (Keep It Simple and Stupid)

在设计 CPU 时,我们有时需要决定,是否要将某个功能抽象为一个单独的模块。在遇到这种问题时,我们应该首先思考这样做有什么实际意义。请看下面的代码:

module top;
// ...
ctrl ctrl(...);
extend extendimm(.instr(ctrl.IRO),.siExt(ctrl.siExt),.shift2(ctrl.shift2));
extend16forpc extend16(.instr(ctrl.IRO),.pc(ctrl.PCO));
extend26forpc extend26(.instr(ctrl.IRO),.pc(ctrl.PCO));
mux32 mux16or26(.src1(extend16.imm),.src2(extend26.imm),.op(ctrl.j));
// ...
endmodule

module extend(
    input [31:0] instr,
    input siExt,
    input shift2,
    output [31:0] imm
    );
// ...
endmodule

module extend16forpc(
	 input [31:0] instr,
	 input [31:0] pc,
    output [31:0] imm
    );
// ...
ndmodule
	 
module extend26forpc(
	 input [31:0] instr,
	 input [31:0] pc,
    output [31:0] imm
    );
// ...
endmodule 

这位同学使用控制器模块操作扩展器模块,扩展器模块从指令中提取立即数,然后使用 mux 来选择。这样的写法虽然能够工作,但是稍显复杂。extend 模块功能非常简单,而且不被任何其他地方使用,显然是不必要的抽象。

既然立即数只由指令和 PC 决定,那么我们能不能直接在控制器中决定立即数,从而避免扩展器模块呢?

module Controller (
           input [31:0] instruction,
           output reg [31:0] immediate
);

wire [25:0] bigImm = instruction[25:0];
wire [15:0] imm = instruction[15:0];
wire [31:0] zeroExtendedImmediate = imm;
wire [31:0] shiftedImmediate = {imm, 16'b0};
wire [31:0] signExtendedImmediate = $signed(imm);

wire [5:0] opcode = instruction[31:26];

always @(*) begin
    case (opcode)
        addi: begin
            // ...
            immediate = signExtendedImmediate;
        end

        ori: begin
            // ...
            immediate = zeroExtendedImmediate;
        end

        lui: begin
            // ...
            immediate = shiftedImmediate;
        end
    endcase
end

这样,外部可以直接使用控制器输出的立即数信号,而无需经过多个模块的连接。这会使调试更加简便直接。

避免过度设计,同时又要有合理的抽象,我们在设计时需要认真取舍。

更多内容

由于物理作业太多个人能力有限,今天只能写这么多了。我后续还会更新其他内容(如果大家有想听的内容,欢迎直接找我),请大家随时来看看。

拒绝卖菜

这个就不多说了。

CC BY-SA 4.0 本作品使用基于以下许可授权:Creative Commons Attribution-ShareAlike 4.0 International License.

WordPress Appliance - Powered by TurnKey Linux