Bluespec与CPU(MIT6.375)

 计组  bluespec  MIPS  流水线 󰈭 8788字

BUAA 6系高等机组实验作业, 使用bluespec工具链编程, 实现一个{,非}流水的右移器以及一个精简MIPS指令集CPU的一微小的部分.

课程疑似参考 6.375 Complex Digital Systems - Spring 2006; 因为 PPT的风格是一模一样的, 但是课程年份与内容应该可能有所出入, 不进行一个几乎是20年前课程的考据了.

环境搭建

实验默认提供的环境是ssh + X11转发, 不过在我的机器好像有问题:

bash
1X Error of failed request:  BadAccess (attempt to access private resource denied)
2  Major opcode of failed request:  18 (X_ChangeProperty)
3  Serial number of failed request:  12
4  Current serial number in output stream:  15

因而直接在本机使用bluespec开源套件.

安装bluespec-git即可, 其提供两个可执行文件:

bluespec-git /usr/bin/bluetcl bluespec-git /usr/bin/bsc

因而该包其实是bluespec语言的编译器bsc, 图形环境bdw等则不默认包含, 尽管arch也有相关的包, 不过暂时感觉不太需要

可以参考一些: 高级祭祖 Bluespec 现代工具链简单食用指南, 也是BUAA的6系前人写的

Hello, World!

从这儿入手! tutorial-helloworld.pdf

可以掌握到的是:

  • 使用bsc命令行工具编译与运行一个bluespec模块

  • rule相关; rule内部是同时进行的, 与顺序无关; rule之间是顺序执行的, 按什么顺序好像不确定

其他参考资料:

Lab1

从基本的门单元构造一个右移器. 首先将构建一个简单的1位多路复用器. 接下来, 将使用for循环编写一个简单的多态复用器. 使用门级多路复用器函数, 就可以构造一个组合右移器. 最后, 将为右移器添加一个简单的门级修改, 以支持算术右移操作.

实验提供了一些模板文件: RightShifter.bsv, RightShifterTypes.bsv, Gates.bsv, RightShifterTest.bsv.

有关复用器的在RightShifter.bsv中.

Text
1function Bit#(1) multiplexer1(Bit#(1) sel, Bit#(1) a, Bit#(1) b);
2	// Part 1: Re-implement this function using the gates found in the Gates.bsv file
3	return (sel == 0)?a:b; 
4endfunction

容易看到这是一个C风格的函数定义, 其接受3个参数, 每个1bit, 返回1ibit.

看到这里的实现, 其使用了三元选择符. 一些简单的代码可以在高层这样实现而不会有任何的硬件实现损失, 但是事实上硬件编译是十分困难且多维度的问题, 因而如果对一些复杂代码仍然在高层实现则可能导致硬件的低效.

那么如何实现更多Bit的选择器呢? 手动处理是麻烦且愚蠢的, Bluespec为我们提供了便利:

Text
1function Bit#(8) multiplexer8(Bit#(1) sel, Bit#(8) a, Bit#(8) b);
2  Bit#(8) aggregate = 0;
3  for (Integer i = 0; i < 8; i = i+1)
4    begin
5      aggregate[i] = multiplexer1(sel, a[i], b[i]);
6    end
7   return aggregate;
8endfunction

使用中括号即可对某一Bit进行索引, 而编译器会自动帮我们变成这样的形状, 这使得其没有什么性能损失

Text
1aggregate[0] = multiplexer1(sel, a[0], b[0]);
2aggregate[1] = multiplexer1(sel, a[1], b[1]);
3aggregate[2] = multiplexer1(sel, a[2], b[2]);
4...
5aggregate[7] = multiplexer1(sel, a[7], b[7]);

至此, 我们已经可以获得一个多位的Mux, 那么如果希望动态的n位mux应该怎么办?

修改上述的代码, 将8换为N即可轻松解决:

Text
1typedef 8 N;
2function Bit#(N) multiplexerN(Bit#(1) sel, Bit#(N) a, Bit#(N) b);
3  Bit#(N) aggregate = 0;
4  for (Integer i = 0; i < valueof(N); i = i+1)
5    begin
6      aggregate[i] = multiplexer1(sel, a[i], b[i]);
7    end
8  return aggregate;
9endfunction

需要注意, N是一个数值类型, , 需要使用valueof()转换为一个整形才可以使用.

Bluespec还支持多态, 多态模块要么使用类型变量,要么将函数或值作为编译时的参数, 用于在编译静态分析时构建正确的模块.

最简单的多态是mkReg()函数, 其可以返回不同的类型. 为了将Mux函数修改为多态的, 只需要将typedef行删去即可.

最后要求使用Log数量的Mux实现32bit的移位器. 其被声明为一个module, 而不是function; 这是由于function默认由编译器内联, 而模块需要显式地通过<-符号实例化. 如果我们将右移操作定义为一个函数, 并在算术和逻辑移位中使用它, 则会实例化两个独立的右移模块. 因此为了尽可能共享移位算法中的电路, 所以将右移操作定义为了一个模块.


Solution:

Text
 1import RightShifterTypes::*;
 2import Gates::*;
 3
 4// sel b + ^sel a
 5function Bit#(1) multiplexer1(Bit#(1) sel, Bit#(1) a, Bit#(1) b);
 6	return orGate(
 7				andGate(sel, b),
 8				andGate(notGate(sel), a)
 9	);
10endfunction
11
12function Bit#(32) multiplexer32(Bit#(1) sel, Bit#(32) a, Bit#(32) b);
13	Bit#(32) res = 0;
14	for(Integer i = 0; i < 32; i = i + 1) begin
15		res[i] = multiplexer1(sel, a[i], b[i]);
16	end
17	return res;
18endfunction
19
20function Bit#(n) multiplexerN(Bit#(1) sel, Bit#(n) a, Bit#(n) b);
21	Bit#(n) res = 0;
22	for(Integer i = 0; i < valueof(n); i = i + 1) begin
23		res[i] = multiplexer1(sel, a[i], b[i]);
24	end
25	return res;
26endfunction
27
28
29
30module mkRightShifter (RightShifter);
31	method Bit#(32) shift(ShiftMode mode, Bit#(32) operand, Bit#(5) shamt);
32		Bit#(32) res = operand, tmp = 0;
33		Integer base[5] = {1, 2, 4, 8, 16};
34		Bit#(1) signal = 0;
35
36		if(mode == ArithmeticRightShift) begin
37			signal = operand[31];
38		end
39
40		for(Integer i = 0; i < 5; i = i + 1) begin
41			// tmp 记录了当前移位后的结果
42			tmp = res[31: base[i]];
43			for(Integer j = 1; j <= base[i]; j = j + 1) begin
44				tmp[32 - j] = signal;
45			end
46			res = multiplexerN(shamt[i], res, tmp);
47		end
48		return res;   
49	endmethod
50endmodule

修改后完整的代码如上, 其中:

  • 三个移位器的实现比较简单, 主要就是利用Bluespec语法支持的for循环以及[]的寻址

  • 右移器的实现比较纠结. 一直在考虑的问题是, 是否强制需要$2^n$路的Mux, 感觉正常来说不太会有31路, 30路, 25路的移位器.. 不过想了半天也不知道怎么能够优雅地用2的指数路的Mux实现, 最终还是就按位处理, 移动5次数据, 并将高位补为对应的符号为/0.

Lab2

本Lab将实现一个时序逻辑电路下的流水右移器.

其不强制要求我们使用我们设计的组合逻辑电路, 允许我们直接在bsv中使用>>运算符; 最大化吞吐量, 即一个周期可以完成一次完整的移位.

参考阅读:


solution:

Text
 1import RightShifterTypes::*;
 2import Gates::*;
 3import FIFO::*;
 4
 5function Bit#(1) multiplexer1(Bit#(1) sel, Bit#(1) a, Bit#(1) b);
 6	return orGate(andGate(a, sel),andGate(b, notGate(sel))); 
 7endfunction
 8
 9function Bit#(32) multiplexer32(Bit#(1) sel, Bit#(32) a, Bit#(32) b);
10	Bit#(32) res_vec = 0;
11	for (Integer i = 0; i < 32; i = i+1)
12		begin
13		res_vec[i] = multiplexer1(sel, a[i], b[i]);
14		end
15	return res_vec; 
16endfunction
17
18function Bit#(n) multiplexerN(Bit#(1) sel, Bit#(n) a, Bit#(n) b);
19	Bit#(n) res_vec = 0;
20	for (Integer i = 0; i < valueof(n); i = i+1)
21		begin
22		res_vec[i] = multiplexer1(sel, a[i], b[i]);
23		end
24	return res_vec; 
25endfunction
26
27module mkRightShifterPipelined (RightShifterPipelined);
28	FIFO#(Tuple3#(ShiftMode, Bit#(32), Bit#(5))) c[6];
29
30	// operand右移cnt位, 模式为mode
31	function Bit#(32) shiftOnce(ShiftMode mode, Bit#(32) operand, Integer cnt);
32		bit signal = 0;
33		if(mode == ArithmeticRightShift) begin
34			signal = operand[31];
35		end
36
37		operand = operand[31: cnt];
38		for(Integer i = 1; i <= cnt; i = i + 1) begin
39			operand[32 - i] = signal;
40		end
41
42		return operand;
43	endfunction
44
45
46	for(int i = 0; i < 6; i = i + 1) begin
47		c[i] <- mkFIFO();
48	end
49
50	for(Integer i = 0; i < 5; i = i + 1) begin
51		rule pipeShift;
52			match {.mode, .operand, .shamt} = c[i].first();
53			c[i].deq();
54
55			Integer base[5] = {1, 2, 4, 8, 16};
56			if(shamt[i] == 1) begin
57				operand = shiftOnce(mode, operand, base[i]);
58			end
59
60			c[i + 1].enq(tuple3(mode, operand, shamt));
61		endrule
62	end
63
64	method Action push(ShiftMode mode, Bit#(32) operand, Bit#(5) shamt);
65		c[0].enq(tuple3(mode, operand, shamt));
66	endmethod
67	
68	method ActionValue#(Bit#(32)) pull();
69		match {.mode, .res, .shamt} = c[5].first();
70		c[5].deq();
71		return res;
72	endmethod
73
74endmodule
  • bluespec支持使用for循环构造rule, 这是极好的

    • 需要注意, 如果不初始化c[i]会报错:
    plain
     1Warning: "RightShifter.bsv", line 51, column 22: (G0023)
     2  The body of rule `rsp_pipeShift' has no actions. Removing...
     3Warning: "RightShifter.bsv", line 51, column 22: (G0023)
     4  The body of rule `rsp_pipeShift_1' has no actions. Removing...
     5Warning: "RightShifter.bsv", line 51, column 22: (G0023)
     6  The body of rule `rsp_pipeShift_2' has no actions. Removing...
     7Warning: "RightShifter.bsv", line 51, column 22: (G0023)
     8  The body of rule `rsp_pipeShift_3' has no actions. Removing...
     9Warning: "RightShifter.bsv", line 51, column 22: (G0023)
    10  The body of rule `rsp_pipeShift_4' has no actions. Removing...
  • 类似的, 根据shamt的5个bit位划分流水段; 段内的工作就是取上一阶段的内容, 移位, 塞到寄存器给下一个阶段使用, 没有什么难点

但是测试模块有点怪, 故意不初始化c[6]队列, 运行后结果为:

Text
 1import RightShifterTypes::*;
 2import RightShifter::*;
 3import FIFO::*;
 4
 5(* synthesize *)
 6module mkTests (Empty);
 7   RightShifterPipelined rsp <- mkRightShifterPipelined;
 8   Reg#(Bit#(32)) tbCounter <- mkReg(0);
 9   FIFO#(Bit#(32)) answerFifo <- mkSizedFIFO(6);
10   
11   // there are many ways to write tests.  Here is a very simple
12   // version, just to get you started.
13
14   rule run;
15	rsp.push(LogicalRightShift, tbCounter, 1);
16	answerFifo.enq(tbCounter >> 1);
17	tbCounter <= tbCounter + 1;
18   endrule
19
20   rule test;
21
22      let b <- rsp.pull();
23      let answer = answerFifo.first();
24      answerFifo.deq();
25
26      if (b != answer) $display("result is ", b, " but expected ", answer);
27      else $display("correct! ", b, " ", answer);
28
29      if (tbCounter > 10) $finish(0);
30
31   endrule
32endmodule
33
34# make && ./a.out
35# bsc -sim -u TestShifterPipe.bsv
36# checking package dependencies
37# All packages are up to date.
38# bsc -sim -e mkTests
39# Bluesim object reused: mkTests.{h,o}
40# Bluesim object created: model_mkTests.{h,o}
41# Simulation shared library created: a.out.so
42# Simulation executable created: a.out
43# correct! 2863311530          0
44# correct! 2863311530          0
45# correct! 2863311530          1
46# correct! 2863311530          1
47# correct! 2863311530          2
48# correct! 2863311530          2
49# correct! 2863311530          3
50# correct! 2863311530          3
51# correct! 2863311530          4
52# correct! 2863311530          4
53# correct! 2863311530          5

我不知道为什么两个值不一样但是却通过了if循环.. 我其实也不太知道let, <-以及<=的区别…

Lab3 Part1

实现一个精简的MIPS指令集CPU.

参考文档语焉不详, 同时执行环境不能很好地迁移到本机, 需要python2系列的版本.. 什么古早实验lab..

由于为其配置一套环境过于麻烦了, 因而Lab3与Lab4只能选择在本地编辑, 上传至服务器编译运行与测试.

给出的template使用两阶段实现, 一阶段取指, 另一阶段实现所有剩余的事情.

Text
 1import Types::*;
 2import ProcTypes::*;
 3import MemTypes::*;
 4import RFile::*;
 5import IMemory::*;
 6import DMemory::*;
 7import Decode::*;
 8import Exec::*;
 9import Cop::*;
10
11typedef enum {Fetch, Execute} State deriving (Bits, Eq);
12
13interface Proc;
14   method ActionValue#(Tuple2#(RIndx, Data)) cpuToHost;
15   method Action hostToCpu(Bit#(32) startpc);
16endinterface
17
18(* synthesize *)
19module [Module] mkProc(Proc);
20  Reg#(Addr) pc <- mkRegU;
21  RFile      rf <- mkRFile;
22  IMemory  iMem <- mkIMemory;
23  DMemory  dMem <- mkDMemory;
24  Cop       cop <- mkCop;
25  
26  Reg#(State) state <- mkReg(Fetch);
27  Reg#(Data)     ir <- mkRegU;
28
29  rule doFetch(cop.started && state == Fetch);
30    let inst = iMem.req(pc);
31
32    $display("pc: %h inst: (%h) expanded: ", pc, inst, showInst(inst));
33
34    // store the instruction in a register
35    ir <= inst;
36
37    // switch to execute state
38    state <= Execute;
39  endrule
40
41  rule doExecute(cop.started && state == Execute);
42    let inst = ir;
43
44    let dInst = decode(inst);
45
46    let rVal1 = rf.rd1(validRegValue(dInst.src1));
47    let rVal2 = rf.rd2(validRegValue(dInst.src2));     
48
49    let copVal = cop.rd(validRegValue(dInst.src1));
50
51    let eInst = exec(dInst, rVal1, rVal2, pc, ?, copVal);
52
53    if(eInst.iType == Unsupported)
54    begin
55      $fwrite(stderr, "Executing unsupported instruction at pc: %x. Exiting\n", pc);
56      $finish;
57    end
58
59    if(eInst.iType == Ld)
60    begin
61      let data <- dMem.req(MemReq{op: Ld, addr: eInst.addr, byteEn: ?, data: ?});
62      eInst.data = gatherLoad(eInst.addr, eInst.byteEn, eInst.unsignedLd, data);
63    end
64    else if(eInst.iType == St)
65    begin
66      match {.byteEn, .data} = scatterStore(eInst.addr, eInst.byteEn, eInst.data);
67      let d <- dMem.req(MemReq{op: St, addr: eInst.addr, byteEn: byteEn, data: data});
68    end
69
70    if (isValid(eInst.dst) && validValue(eInst.dst).regType == Normal)
71      rf.wr(validRegValue(eInst.dst), eInst.data);
72
73    pc <= eInst.brTaken ? eInst.addr : pc + 4;
74
75    cop.wr(eInst.dst, eInst.data);
76
77    // switch back to fetch
78    state <= Fetch;
79  endrule
80  
81  method ActionValue#(Tuple2#(RIndx, Data)) cpuToHost;
82    let ret <- cop.cpuToHost;
83    return ret;
84  endmethod
85
86  method Action hostToCpu(Bit#(32) startpc) if (!cop.started);
87    cop.start;
88    // pc <= startpc;
89  endmethod
90endmodule
  • 在doFetch中, 使用IMemrory类型的指令寄存器iMem取指令, 由于要传递给二阶段, 所以用一个中间寄存器ir存储, 同时将CPU状态变为二阶段;

  • 在doExecute中, 实现从ir中读取指令, 随后调用decode函数, 该函数定义在simulator/lib/Decode.bsv. 有关所有支持的指令:

  • 译码返回的类型定义在simulator/lib/ProcTypes.bsv中:

Text
 1typedef struct {
 2  IType            iType;
 3  ByteEn           byteEn;
 4  Bool             unsignedLd;
 5  AluFunc          aluFunc;
 6  BrFunc           brFunc;
 7  Maybe#(FullIndx) dst;
 8  Maybe#(FullIndx) src1;
 9  Maybe#(FullIndx) src2;
10  Maybe#(Data)     imm;
11} DecodedInst deriving(Bits, Eq);
  • 取寄存器内容, validRegValue其实只是一层带有合法性的封装; 而RFile模块其实是一个寄存器组, rd1和rd2也是完全一样的东西.. 都是通过下标索引获得寄存器内容.
Text
 1// ProcTypes.bsv
 2function RIndx validRegValue(Maybe#(FullIndx) idx) = validValue(idx).idx;
 3
 4// RFile.bsv
 5 (* synthesize *)
 6module mkRFile( RFile );
 7    Vector#(32, Reg#(Data)) rfile <- replicateM(mkConfigReg(0));
 8
 9    function Data read(RIndx rindx);
10        return rfile[rindx];
11    endfunction
12   
13    method Action wr( RIndx rindx, Data data );
14        if(rindx!=0) rfile[rindx] <= data;
15    endmethod
16
17    method Data rd1( RIndx rindx ) = read(rindx);
18    method Data rd2( RIndx rindx ) = read(rindx);
19endmodule
  • cop是协处理器相关代码

  • 有关exec函数, 其接受6个参数, 实现广义上的执行操作; 结果将被保存在eInst结构中返回.

verilog
 1(* noinline *)
 2function ExecInst exec(DecodedInst dInst, Data rVal1, Data rVal2, Addr pc, Addr ppc, Data copVal);
 3  ExecInst eInst = ?;
 4  Data aluVal2 = isValid(dInst.imm) ? validValue(dInst.imm) : rVal2;
 5  
 6  let aluRes = alu(rVal1, aluVal2, dInst.aluFunc);
 7  
 8  eInst.iType = dInst.iType;
 9  
10  eInst.data = dInst.iType == Mfc0?
11                 copVal :
12               dInst.iType == Mtc0?
13                 rVal1 :
14               dInst.iType==St?
15                 rVal2 :
16               (dInst.iType==J || dInst.iType==Jr) ?
17                 (pc+4) :
18                 aluRes;
19  eInst.byteEn = dInst.byteEn;
20  eInst.unsignedLd = dInst.unsignedLd;
21  
22  let brTaken = aluBr(rVal1, rVal2, dInst.brFunc);
23  let brAddr = brAddrCalc(pc, rVal1, dInst.iType, validValue(dInst.imm), brTaken);
24  eInst.mispredict = brAddr != ppc;
25
26  eInst.brTaken = brTaken;
27  eInst.addr = (dInst.iType == Ld || dInst.iType == St) ? aluRes : brAddr;
28  
29  eInst.dst = dInst.dst;
30
31  return eInst;
32endfunction
  • 执行完exec函数后, 主函数会根据eInst的类型将lw和sw的再执行一些内存操作. dMem.req其实像是一个业务处理函数, 其接受一个业务请求并执行:

    Text
     1typedef enum{Ld, St} MemOp deriving(Eq,Bits);
     2typedef struct{
     3    MemOp op;
     4    ByteEn byteEn;
     5    Addr  addr;
     6    Data  data;
     7} MemReq deriving(Eq,Bits);
     8
     9
    10(* synthesize *)
    11module mkDMemory(DMemory);
    12  RegFile#(Bit#(26), Data) mem <- mkRegFileFullLoad("memory.vmh");
    13
    14  method ActionValue#(MemResp) req(MemReq r);
    15    Bit#(26) index = truncate(r.addr>>2);
    16    let data = mem.sub(index);
    17    if(r.op==St)
    18    begin
    19      Vector#(NumBytes, Bit#(8)) bytes = unpack(data);
    20      Vector#(NumBytes, Bit#(8)) bytesIn = unpack(r.data);
    21      for(Integer i = 0; i < valueOf(NumBytes); i = i + 1)
    22      begin
    23	if(r.byteEn[i])
    24	  bytes[i] = bytesIn[i];
    25      end
    26      mem.upd(index, pack(bytes));
    27    end
    28    return data;
    29  endmethod
    30endmodule
  • 因此主函数中, ld对应取出data, 执行gatherLoad函数, 该函数的作用是区分lb, lh, lw的取值粒度. 具体可以从:

    • lib/Types.bsv中得知ByteEn其实就是一个4位的布尔数组,

      v
      1typedef 32 DataSz;
      2typedef Bit#(DataSz) Data;
      3
      4typedef TDiv#(DataSz, 8) NumBytes;
      5typedef Vector#(NumBytes, Bool) ByteEn;
    • 其在lib/Decode.bsv中被赋给dInst, 随后被原封不动地拷贝给eInst; 可以看到, 取1B的标志位中只有0号被置位, 取1H的标志中0和1被置位, 取1W的标志则为全1.

      v
       1opLB, opLH, opLW, opLBU, opLHU:
       2begin
       3	dInst.iType = Ld;
       4	dInst.byteEn = replicate(False);
       5	case (opcode)
       6		opLB, opLBU: dInst.byteEn[0] = True;
       7		opLH, opLHU:
       8		begin
       9			dInst.byteEn[0] = True;
      10			dInst.byteEn[1] = True;
      11		end
      12		opLW: dInst.byteEn = replicate(True);
      13	endcase
      14	dInst.unsignedLd = case (opcode)
      15		opLB, opLH, opLW: False;
      16		opLBU, opLHU: True;
      17	endcase;
      18	dInst.aluFunc = Add;
      19	dInst.dst = validReg(rt);
      20	dInst.src1 = validReg(rs);
      21	dInst.src2 = Invalid;
      22	dInst.imm = Valid(signExtend(imm));
      23	dInst.brFunc = NT;
      24end
    • 这样再看到gatherLoad即可得知, 其首先根据拓展标志位决定是0拓展还是符号拓展, 不过有关拓展函数的定义我没有找到在哪里; 随后offset取addr的低2位, 再根据byteEn的标志情况取对应的值出来; 这里可以合法猜测, extend总是返回一个32bit的结果, 因而对于取半字和取字的情况才需要划分为vector再取元素! 而且顺序似乎也是小端序的感觉, 不太清楚这是在什么层面做的, 不过可以确定顺序貌似是反的.

      v
       1function Data gatherLoad(Addr addr, ByteEn byteEn, Bool unsignedLd, Data data);
       2function extend = unsignedLd? zeroExtend : signExtend;
       3Bit#(2) offset = truncate(addr);
       4if(byteEn[3])
       5	return extend(data);
       6else if(byteEn[1])
       7begin
       8	Vector#(2, Bit#(16)) dataVec = unpack(data);
       9	return extend(dataVec[1-offset[1]]);
      10end
      11else
      12begin
      13	Vector#(4, Bit#(8)) dataVec = unpack(data);
      14	return extend(dataVec[3-offset]);
      15end
      16endfunction
  • 主函数中有关sw类型函数也是类似的, 首先要对数据进行大小的变换, 再塞到内存请求中发送. 具体的代码就不看了

  • 下一步是寄存器写回, 判断合法后则直接rf.wr写回.

  • 最后的步骤是修改PC寄存器, 修改协处理器, 修改状态. 至此该阶段工作全部完成.


由于要求只是将写回阶段划分出来, 因而修改其实比较简单.

verilog
 1typedef enum {Fetch, Execute, Writeback} State deriving (Bits, Eq);
 2...
 3Reg#(ExecInst) wbr <- mkRegU;
 4...
 5
 6...
 7	if(eInst.iType == Ld)
 8	begin
 9		let data <- dMem.req(MemReq{op: Ld, addr: eInst.addr, byteEn: ?, data: ?});
10		eInst.data = gatherLoad(eInst.addr, eInst.byteEn, eInst.unsignedLd, data);
11	end
12	else if(eInst.iType == St)
13	begin
14		match {.byteEn, .data} = scatterStore(eInst.addr, eInst.byteEn, eInst.data);
15		let d <- dMem.req(MemReq{op: St, addr: eInst.addr, byteEn: byteEn, data: data});
16	end
17
18	if (isValid(eInst.dst) && validValue(eInst.dst).regType == Normal) begin
19		wbr <= eInst;
20		state <= Writeback;
21	end
22	else begin
23		pc <= eInst.brTaken ? eInst.addr : pc + 4;
24		cop.wr(eInst.dst, eInst.data);
25		state <= Fetch;
26	end
27endrule
28
29rule doWriteBack(cop.started && state == Writeback);
30	let eInst = wbr;
31	rf.wr(validRegValue(eInst.dst), eInst.data);
32
33	pc <= eInst.brTaken ? eInst.addr : pc + 4;
34	cop.wr(eInst.dst, eInst.data);
35	state <= Fetch;
36endrule
37...

不过在服务器编译的运行的时候遇到问题… 第一次编译运行时可以进行测试, 测到一半报错什么空间不足.. 再重新运行编译测试就G了:

看了下目前$HOME目录只占用了390M… 太小气了吧, 清理一些垃圾后就行了.

Lab3 Part2

Lab3的Part2是两阶段流水线相关, Lab4是修改为三阶段流水线. 将写回分开.

提供的模板文件为simulator/procs/ControlHazardOnly/pcMsg_epoch.bsv.

其接口设计类似, 暴露两个方法:

Text
1interface Proc;
2   method ActionValue#(Tuple2#(RIndx, Data)) cpuToHost;
3   method Action hostToCpu(Bit#(32) startpc);
4endinterface

具体到mkProc中:

Text
  1(* synthesize *)
  2module [Module] mkProc(Proc);
  3  Reg#(Addr) pc <- mkRegU;
  4  RFile      rf <- mkRFile;
  5  IMemory  iMem <- mkIMemory;
  6  DMemory  dMem <- mkDMemory;
  7  Cop       cop <- mkCop;
  8  AddrPred pcPred <- mkBtb;
  9
 10  //Fifo#(1, Fetch2Execute) ir <- mkPipelineFifo;
 11  Fifo#(2, Fetch2Execute) ir <- mkCFFifo;
 12  Fifo#(1, Redirect) execRedirect <- mkBypassFifo;
 13  //Fifo#(2, Redirect)   execRedirect <- mkCFFifo;
 14
 15  //This design uses two epoch registers, one for each stage of the pipeline. Execute sets the eEpoch and discards any instruction that doesn't match it. It passes the information about change of epoch to fetch stage indirectly by passing a valid execRedirect using a Fifo. Fetch changes fEpoch everytime it gets a execRedirect and tags every instruction with its epoch
 16
 17  Reg#(Bool) fEpoch <- mkReg(False);
 18  Reg#(Bool) eEpoch <- mkReg(False);
 19
 20  rule doFetch(cop.started);
 21    let inst = iMem.req(pc);
 22
 23    $display("Fetch: pc: %h inst: (%h) expanded: ", pc, inst, showInst(inst));
 24
 25    // dequeue the incoming redirect and update the predictor whether it's a mispredict or not
 26    if(execRedirect.notEmpty)
 27    begin
 28      execRedirect.deq;
 29      pcPred.update(execRedirect.first);
 30    end
 31    // change pc and the fetch's copy of the epoch only on a mispredict
 32    if(execRedirect.notEmpty && execRedirect.first.mispredict)
 33    begin
 34      fEpoch <= !fEpoch;
 35      pc <= execRedirect.first.nextPc;
 36    end
 37    // fetch the new instruction on a non mispredict
 38    else
 39    begin
 40      let ppc = pcPred.predPc(pc);
 41      pc <= ppc;
 42      ir.enq(Fetch2Execute{pc: pc, ppc: ppc, inst: inst, epoch: fEpoch});
 43    end
 44  endrule
 45
 46  rule doExecute;
 47    let inst  = ir.first.inst;
 48    let pc    = ir.first.pc;
 49    let ppc   = ir.first.ppc;
 50    let epoch = ir.first.epoch;
 51
 52    // Proceed only if the epochs match
 53    if(epoch == eEpoch)
 54    begin
 55      $display("Execute: pc: %h inst: (%h) expanded: ", pc, inst, showInst(inst));
 56  
 57      let dInst = decode(inst);
 58  
 59      let rVal1 = rf.rd1(validRegValue(dInst.src1));
 60      let rVal2 = rf.rd2(validRegValue(dInst.src2));     
 61  
 62      let copVal = cop.rd(validRegValue(dInst.src1));
 63  
 64      let eInst = exec(dInst, rVal1, rVal2, pc, ppc, copVal);
 65  
 66      if(eInst.iType == Unsupported)
 67      begin
 68        $fwrite(stderr, "Executing unsupported instruction at pc: %x. Exiting\n", pc);
 69        $finish;
 70      end
 71
 72      if(eInst.iType == Ld)
 73      begin
 74        let data <- dMem.req(MemReq{op: Ld, addr: eInst.addr, byteEn: ?, data: ?});
 75        eInst.data = gatherLoad(eInst.addr, eInst.byteEn, eInst.unsignedLd, data);
 76      end
 77      else if(eInst.iType == St)
 78      begin
 79        match {.byteEn, .data} = scatterStore(eInst.addr, eInst.byteEn, eInst.data);
 80        let d <- dMem.req(MemReq{op: St, addr: eInst.addr, byteEn: byteEn, data: data});
 81      end
 82
 83      if (isValid(eInst.dst) && validValue(eInst.dst).regType == Normal)
 84        rf.wr(validRegValue(eInst.dst), eInst.data);
 85  
 86      // Send the branch resolution to fetch stage, irrespective of whether it's mispredicted or not
 87       // TBD: put code here that does what the comment immediately above says
 88
 89      // On a branch mispredict, change the epoch, to throw away wrong path instructions
 90       // TBD: put code here that does what the comment immediately above says
 91  
 92      cop.wr(eInst.dst, eInst.data);
 93    end
 94
 95    ir.deq;
 96  endrule
 97  
 98  method ActionValue#(Tuple2#(RIndx, Data)) cpuToHost;
 99    let ret <- cop.cpuToHost;
100    return ret;
101  endmethod
102
103  method Action hostToCpu(Bit#(32) startpc) if (!cop.started);
104    cop.start;
105    pc <= startpc;
106  endmethod
107endmodule
  • 首先看到最后, 两个接口的实现, hostToCpu启动协处理器, 同时装载启动地址至PC寄存器; 而在cpuToHost中则加载协处理器中的值作为返回值返回即可.

  • 一旦协处理器启动, 则使能取值阶段.

  • 看到execRedirect, 这是一个旁路FIFO(栈)的实现, 具体定义在simulator/lib/Fifo.bsv中. 这个栈只有2个元素, full是布尔数组, 按位表示该索引对应的栈中是否有元素; 但是具体的实现好奇怪, 可能是和ehr有关..

    Text
     1// A bypass FIFO implementation
     2// notFull < enq < {notEmpty, first} < deq < clear
     3module mkBypassFifo(Fifo#(1, t)) provisos(Bits#(t, tSz));
     4  Ehr#(2, t) data <- mkEhr(?);
     5  Ehr#(3, Bool) full <- mkEhr(False);
     6
     7  method Bool notFull = !full[0];
     8
     9  method Action enq(t x) if(!full[0]);
    10    data[0] <= x;
    11    full[0] <= True;
    12  endmethod
    13
    14  method Bool notEmpty = full[1];
    15
    16  method Action deq if(full[1]);
    17    full[1] <= False;
    18  endmethod
    19
    20  method t first if(full[1]);
    21    return data[1];
    22  endmethod
    23
    24  method Action clear;
    25    full[2] <= False;
    26  endmethod
    27endmodule
    • 可能会疑惑Ehr是什么, 其实貌似就是以寄存器为元素的vector:

      Text
      1typedef  Vector#(n, Reg#(t)) Ehr#(numeric type n, type t);
    • 在mkEhr中貌似使用到了RWire, RWire是一种基本的无状态模块, 其目的是允许在方法和规则之间传输数据, 而不需要寄存器的周期延迟. 也就是说, RWire可以在一个循环中写入, 而该值可以在同一个循环中读出;值不会跨时钟周期存储.

  • 另一个没见过的是AddrPred结构, 其位于simulator/lib/AddrPred.bsv. 其暴露两个接口, 一个会根据pc预测一个地址, 另一个会根据Redirect结构更新一些什么.

    Text
     1interface AddrPred;
     2  method Addr predPc(Addr pc);
     3  method Action update(Redirect rd);
     4endinterface
     5
     6module mkPcPlus4(AddrPred);
     7  method Addr predPc(Addr pc) = pc + 4;
     8
     9  method Action update(Redirect rd);
    10    noAction;
    11  endmethod
    12endmodule
    13
    14typedef 64 BtbEntries;
    15typedef Bit#(TLog#(BtbEntries)) BtbIndex;
    16typedef Bit#(TSub#(TSub#(AddrSz, TLog#(BtbEntries)), 2)) BtbTag;
    17
    18(* synthesize *)
    19module mkBtb(AddrPred);
    20  RegFile#(BtbIndex, Addr) arr <- mkRegFileFull;
    21  RegFile#(BtbIndex, BtbTag) tagArr <- mkRegFileFull;
    22  Vector#(BtbEntries, Reg#(Bool)) validArr <- replicateM(mkReg(False));
    23
    24  function BtbIndex getIndex(Addr pc) = truncate(pc >> 2);
    25  function BtbTag getTag(Addr pc) = truncateLSB(pc); 
    26
    27  method Addr predPc(Addr pc);
    28    BtbIndex index = getIndex(pc);
    29    BtbTag tag = getTag(pc);
    30    if(validArr[index] && tag == tagArr.sub(index))
    31      return arr.sub(index);
    32    else
    33      return (pc + 4);
    34  endmethod
    35
    36  method Action update(Redirect rd);
    37    if(rd.taken)
    38    begin
    39      let index = getIndex(rd.pc);
    40      let tag = getTag(rd.pc);
    41      validArr[index] <= True;
    42      tagArr.upd(index, tag);
    43      arr.upd(index, rd.nextPc);
    44    end
    45  endmethod
    46endmodule
    • 一个模块是PcPlus4, 这是好理解的, 不进行任何预测, 仅仅做PC+4的工作

    • 第二个是Btb模块.

      • 首先看到其做了2个typedef, TLog和TSub是多态的数学函数, 计算两个变量的log和sub. 在本例子中, BtbIndex为Bit#(6), 那么BtbTag为32-6-2=Bit#(24).

      • 随后创建两个6比特的寄存器文件arr和tagArr, 元素分别为Addr和BtbTag; 创建长度64的布尔值数组validArr. arr中为该6bit的index对应的预测地址, 而tagArr为该6bit的index对应的tag值, 只有后续发现tag一致才能读取该预测结果, 而validArr则表示该入口是否有效. 有关BtbEntries和BtbIndex的定义, 这里为什么需要这样子大费周章以两种形式来表示64呢? 一个猜测是如果BtbEntries不为2的幂次的话, 这两个值可能就不一样了, 需要更多的比特位来表示入口值.?

      • trancate是截断高位, truncateLSB是截断低位. 其将32位的PC寄存器截取出tag和index位.

      • 在predPc的实现中, 首先根据6位index位置, 检查validArr以及Tag是否匹配, 均通过后则返回预测的结果; 不然只能简单的+4.

      • 相对应的在update中, 先根据taken标志判断一些什么(TBD), 随后使用rd中的pc和nextPc来更新上述的三个数组即可.

    • 补充一些有关Redirect结构的内容, 定义在simulator/lib/ProcTypes.bsv中:

      Text
      1typedef struct {
      2  Addr pc;
      3  Addr nextPc;
      4  IType brType;
      5  Bool taken;
      6  Bool mispredict;
      7} Redirect deriving (Bits, Eq);
  • 接着看回主文件, 如果重定向栈非空, 则用该结果更新预测器; 这里一个神秘的地方是, 为什么可以先deq再取first, 这可能是因为bluespec与传统的c等高层语言不同, deq的更新只会在下一个时钟周期进行, 因而在当前rule的first仍然可以取到正确的内容.

  • 如果存在重定向, 接着进一步判断是否发生了mispredict.

    • 如果发生错误预测, 则从execRedirect中取值装载进PC, 并将fEopch反转(why?)

    • 如果没有发生错误, 接着使用预测器预测下一个地址, 装载进入pc即可. 这里还把Fetch2Execute塞到了ir中传递给了执行阶段, 该结构包含:

      Text
      1typedef struct {
      2	Addr pc;
      3	Addr ppc;
      4	Data inst;
      5	Bool epoch;
      6} Fetch2Execute deriving (Bits, Eq);
  • 进入执行阶段, 只有当epoch一致时才继续执行. 其主要内容与Lab3 Part1几乎一致, 主要区别在于最后要为分支预测器的结果做一些处理.

    • 回头看到exec函数, 其中包含若干关于分支预测结果的内容:
      Text
      1...
      2let brTaken = aluBr(rVal1, rVal2, dInst.brFunc);
      3let brAddr = brAddrCalc(pc, rVal1, dInst.iType, validValue(dInst.imm), brTaken);
      4eInst.mispredict = brAddr != ppc;
      5
      6eInst.brTaken = brTaken;
      7eInst.addr = (dInst.iType == Ld || dInst.iType == St) ? aluRes : brAddr;
      8...

    根据eInst这几个字段的内容即可根据预测结果做一些相应的处理.


Solution:

其实主要是与取值阶段的内容相对应起来:

  • 只要是分支处理了就应该向取值阶段发送消息, 供其更新预测器信息

  • 如果分支预测预测失败, 将Epoch翻转; 这里的道理可能在于2阶段流水线, 因而实际上分支处理不会超过2个阶段, 因而使用1bit即可表示当前的指令路径是否正确了.

Text
1// Send the branch resolution to fetch stage, irrespective of whether it's mispredicted or not
2// TBD: put code here that does what the comment immediately above says
3if(eInst.brTaken)
4	execRedirect.enq(Redirect{mispredict: eInst.mispredict, nextPc: eInst.addr});
5
6// On a branch mispredict, change the epoch, to throw away wrong path instructions
7// TBD: put code here that does what the comment immediately above says
8if(eInst.brTaken && eInst.mispredict)
9	eEpoch <= !eEpoch;

Lab4

最后Lab4咕了.. 预估错误Lab4的工作量了写不完了 胡写一气就交了

将写回阶段拆为第三个阶段; 可能会产生数据冒险, 因而可以使用计分板等技术处理; 另一个要求是添加bypass功能.

首先看一看计分板, 已经有库文件定义好了simulator/lib/Scoreboard.bsv:

Text
 1import Fifo::*;
 2import ProcTypes::*;
 3
 4interface Scoreboard#(numeric type size);
 5  method Action insert(Maybe#(FullIndx) r);
 6  method Action remove;
 7  method Bool search1(Maybe#(FullIndx) r);
 8  method Bool search2(Maybe#(FullIndx) r);
 9  method Bool search3(Maybe#(FullIndx) r);
10  method Action clear;
11endinterface
12
13function Bool isFound(Maybe#(FullIndx) x, Maybe#(FullIndx) k);
14   if(x matches tagged Valid .xv &&& k matches tagged Valid .kv &&& kv == xv)
15     return True;
16   else
17     return False;
18endfunction
19
20// search CF {enq, deq, first, notEmpty, notFull}
21// deq CF enq
22// search < clear
23module mkCFScoreboard(Scoreboard#(size));
24  SFifo#(size, Maybe#(FullIndx), Maybe#(FullIndx))  f <- mkCFSFifo(isFound);
25
26  method insert = f.enq;
27
28  method remove = f.deq;
29
30  method search1 = f.search;
31  method search2 = f.search;
32  method search3 = f.search;
33
34  method clear = f.clear;
35endmodule
36
37// notEmpty < first < deq < search < notFull < enq < clear
38module mkPipelineScoreboard(Scoreboard#(size));
39  SFifo#(size, Maybe#(FullIndx), Maybe#(FullIndx)) f <- mkPipelineSFifo(isFound);
40
41  method insert = f.enq;
42
43  method remove = f.deq;
44
45  method search1 = f.search;
46  method search2 = f.search;
47  method search3 = f.search;
48
49  method clear = f.clear;
50endmodule
51
52interface CountScoreboard#(numeric type size);
53  method Action insert(Maybe#(FullIndx) r);
54  method Action remove;
55  method Bit#(TLog#(TAdd#(size, 1))) search1(Maybe#(FullIndx) r);
56  method Bit#(TLog#(TAdd#(size, 1))) search2(Maybe#(FullIndx) r);
57  method Bit#(TLog#(TAdd#(size, 1))) search3(Maybe#(FullIndx) r);
58  method Action clear;
59endinterface
60
61// search CF {enq, deq, first, notEmpty, notFull}
62// deq CF enq
63// search < clear
64module mkCFCountScoreboard(CountScoreboard#(size));
65  SCountFifo#(size, Maybe#(FullIndx), Maybe#(FullIndx))  f <- mkCFSCountFifo(isFound);
66
67  method insert = f.enq;
68
69  method remove = f.deq;
70
71  method search1 = f.search;
72  method search2 = f.search;
73  method search3 = f.search;
74
75  method clear = f.clear;
76endmodule
  • 其接口提供了几个函数, FullIndx是其参数:

    Text
    1typedef Bit#(5)	RIndx;
    2typedef enum {Normal, CopReg} RegType deriving (Bits, Eq);
    3typedef struct {
    4	RegType regType;
    5	RIndx idx;
    6} FullIndx deriving (Bits, Eq);
  • isFound函数的作用是判断两个Maybe#(FullIndx)类型的变量x和k是否相等. 函数首先检查x和k是否都是Valid标签, 并且它们的值相等, 如果满足这些条件, 则返回True, 否则返回False. 该函数用于在mkCFSFifo或者mkPipelineSFifo中生成Fifo.. Bluespec中居然可以传递函数作为参数, 感觉不错; 查阅simulator/lib/Fifo.bsv后发现该函数主要是传递给Fifo的search方法使用的, 有点像C++的运算符重载..

Text
 1method Bool search(st s);
 2	Bool ret = False;
 3	for(Bit#(sz1) i = 0; i < nb; i = i + 1)
 4	begin
 5		let ptr = (deqP[0] + i)%nb;
 6		if(isFound(data[ptr], s) && i < cnt0)
 7			ret = True;
 8	end
 9	return ret;
10endmethod
  • mkCFScoreboardmkPipelineScoreboard几乎完全一致, 不同之处仅在于队列的构造是使用mkCFSFifo还是mkPipelineSFifo; 具体的区别我不知道, 因为实现过于长了..

总结

总的来说… 总感觉事实上并不能学到特别多的东西, 四个Lab加起来实际写的代码量可能一共都没有200行.. 只是应付一下还是蛮容易的, 但是深挖进去读代码其实还是能学到一些东西的.

不过理想中的Lab是直接实现整个Lab4的基础框架以及各个库文件, 而不是在Lab4框架上面修修补补.. 不过连Lab4都做不出来的我也没啥好多说的了

嗨! 这里是 rqdmap 的个人博客, 我正关注 GNU/Linux 桌面系统, Linux 内核 以及一切有趣的计算机技术! 希望我的内容能对你有所帮助~
如果你遇到了任何问题, 包括但不限于: 博客内容说明不清楚或错误; 样式版面混乱; 加密博客访问请求等问题, 请通过邮箱 rqdmap@gmail.com 联系我!
修改日志
  • 2023-07-07 22:22:35 Bluespec与CPU