让我访问!
对于L-Type和S-Type,我们只支持了lw/sw,它们的兄弟姐妹lb/lbu/lh/lhu/sb /sh还没支持。
我们需要准备一个模块专门用来处理这些非字节存取。在前面ID级传出的sl_type信号终于能派上用场了。
这里有个小技巧:定义存取类型时,用高位来分辨是读还是写,剩下的读写类型一定程度上可以合并。
存取控制模块 LoadStoreUnit.sv
考虑到数据的存取都在MEM级完成,我们需要将模块放置在MEM级。我们要传入来自EX级的待存数据,对其进行适当移位,好将数据存放到合适位置,并根据dram_we和sl_type来将1位写使能dram_we转换为4位的按位写使能dram_we_strbe。
对于读出的数据,还要判断是有符号还是无符号读取。
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 module LoadStoreUnit ( input logic [ 3 :0 ] sl_type, input logic [31 :0 ] addr, input logic [31 :0 ] load_data_i, output logic [31 :0 ] load_data_o, input logic [31 :0 ] store_data_i, output logic [31 :0 ] store_data_o, input logic dram_we, output logic [ 3 :0 ] wstrb ); logic is_load, is_load_unsigned; always_comb begin is_load = (sl_type[3 ] == 1'b0 ); is_load_unsigned = (sl_type[2 ] == 1'b1 ); end always_comb begin wstrb = 4'b0000 ; load_data_o = 32'b0 ; store_data_o = 32'b0 ; if (is_load) begin logic [31 :0 ] raw; raw = 32'b0 ; case (sl_type[1 :0 ]) 2'b01 : begin raw = (load_data_i >> (addr[1 :0 ] * 8 )) & 32'h000000FF ; end 2'b10 : begin raw = (load_data_i >> (addr[1 ] * 16 )) & 32'h0000FFFF ; end 2'b11 : begin raw = load_data_i; end default : raw = 32'b0 ; endcase if (is_load_unsigned) begin load_data_o = raw; end else begin case (sl_type[1 :0 ]) 2'b01 : load_data_o = {{24 {raw[7 ]}}, raw[7 :0 ]}; 2'b10 : load_data_o = {{16 {raw[15 ]}}, raw[15 :0 ]}; 2'b11 : load_data_o = raw; default : load_data_o = 32'b0 ; endcase end end else if (dram_we) begin logic [15 :0 ] half_byte; logic [ 4 :0 ] shift; shift = 4'b0 ; unique case (sl_type[1 :0 ]) 2'b01 : begin logic [7 :0 ] byte_val; half_byte = store_data_i[7 :0 ]; shift = {addr[1 :0 ], 3'b000 }; unique case (addr[1 :0 ]) 2'b00 : wstrb = 4'b0001 ; 2'b01 : wstrb = 4'b0010 ; 2'b10 : wstrb = 4'b0100 ; 2'b11 : wstrb = 4'b1000 ; default : wstrb = 4'b0000 ; endcase store_data_o = ({24'b0 , half_byte[7 :0 ]} << shift); end 2'b10 : begin logic [15 :0 ] half_val; half_byte = store_data_i[15 :0 ]; shift = {addr[1 ], 4'b0000 }; unique case (addr[1 ]) 1'b0 : wstrb = 4'b0011 ; 1'b1 : wstrb = 4'b1100 ; default : wstrb = 4'b0000 ; endcase store_data_o = ({16'b0 , half_byte} << shift); end 2'b11 : begin wstrb = 4'b1111 ; store_data_o = store_data_i; half_byte = 16'b0 ; end default : begin wstrb = 4'b0000 ; store_data_o = 32'b0 ; half_byte = 16'b0 ; end endcase end else begin end end `ifdef DEBUG logic [31 :0 ] sl_type_ascii; logic [ 1 :0 ] select_bits; assign select_bits = addr[1 :0 ]; always_comb begin case (sl_type) `MEM_LB: sl_type_ascii = "LB " ; `MEM_LH: sl_type_ascii = "LH " ; `MEM_LW: sl_type_ascii = "LW " ; `MEM_LBU: sl_type_ascii = "LBU " ; `MEM_LHU: sl_type_ascii = "LHU " ; `MEM_SB: sl_type_ascii = "SB " ; `MEM_SH: sl_type_ascii = "SH " ; `MEM_SW: sl_type_ascii = "SW " ; default : sl_type_ascii = "UNKN" ; endcase end `endif endmodule
之后更新顶层模块,远方的MUX也不要放过:
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 logic [31 :0 ] DRAM_output_data,DRAM_input_data;logic [3 :0 ] dram_we_MEM_strbe;logic [3 :0 ] wstrb;DRAM u_DRAM ( .clk (clk), .a (alu_result_MEM[17 :2 ]), .spo (DRAM_output_data), .we (wstrb), .din (DRAM_input_data) ); assign rf_wd_MEM = rf_wd_MEM_PR2MUX;logic [31 :0 ] load_data_o; LoadStoreUnit u_LoadStoreUnit( .sl_type (sl_type_MEM), .addr (alu_result_MEM), .load_data_i (DRAM_output_data), .load_data_o (load_data_o), .store_data_i (rf_rd2_MEM), .store_data_o (DRAM_input_data), .dram_we (dram_we_MEM), .wstrb (wstrb) ); assign rf_wd_WB = (wd_sel_WB == `WD_SEL_FROM_DRAM) ? load_data_o : rf_wd_WB_from_PR;
我们的LoadStoreUnit是组合逻辑模块 ,因此,不会打乱同步读写的DRAM时序。让我们测试一下看看:
1 2 3 4 5 6 7 8 9 li x1 ,0x12345678 sw x1 ,0 (x0 )lw x2 ,0 (x0 )lw x3 ,0 (x0 )lh x4 ,4 (x0 )lhu x5 ,4 (x0 )lb x6 ,0 (x0 )lhu x7 ,2 (x0 )
可以看到,本应该执行lw的指令却错误地读出了下一条的lh指令,而最后一条lhu直接消失了!这是为什么?
看上面的波形可以知道,用于控制存入数据的sl_type提早了一拍到达。组合逻辑不会影响时序,因此,应当根据时序来设计组合逻辑。我们是在MEM级读取出数据并进行处理的,但是写回阶段是在WB级——这导致我们写回时使用的仍然是MEM级的控制信号。DRAM_output_data在 WB 级可用时,应当使用和 DRAM 读取请求相对应的sl_type进行处理,但当前代码中LoadStoreUnit使用的是sl_type_MEM,而此时MEM级可能已经是下一条指令了。换句话说, “存”与“取”所在的流水级不同,导致“存”正常,“取”异常 。
我们的LoadStoreUnit模块也成了第二个跨流水级的模块。如何修改呢?最简单的方法,就是直接实例化两个一模一样的模块:
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 logic [31 :0 ] DRAM_input_data; logic [31 :0 ] DRAM_output_data; logic [3 :0 ] dram_we_MEM_strbe;LoadStoreUnit u_LoadStoreUnit_MEM( .sl_type (sl_type_MEM), .addr (alu_result_MEM), .load_data_i (32'b0 ), .load_data_o (), .store_data_i (rf_rd2_MEM), .store_data_o (DRAM_input_data), .dram_we (dram_we_MEM), .wstrb (dram_we_MEM_strbe) ); DRAM #( .ADDR_WIDTH (15 ) ) u_DRAM ( .clk (clk), .a (alu_result_MEM[17 :2 ]), .spo (DRAM_output_data), .we (dram_we_MEM_strbe), .din (DRAM_input_data) ); logic [31 :0 ] load_data_WB;LoadStoreUnit u_LoadStoreUnit_WB( .sl_type (sl_type_WB), .addr (alu_result_WB), .load_data_i (DRAM_output_data), .load_data_o (load_data_WB), .store_data_i (32'b0 ), .store_data_o (), .dram_we (1'b0 ), .wstrb () );
我们再次进行测试:
全自动化测试:解放双手
到这里,我们的RV32I CPU 算是彻底完成了。但即使有前面的测试,我们仍不能确保我们的CPU万无一失 。有什么办法可以证明它一定是符合RV32I标准的CPU呢?最好的办法,自然是使用形式化验证,但那个太烦了,而且我不会 。使用官方的测试样例进行测试算是最容易的方法了。riscv-software-src设置了一个测试库,专门用于测试各种架构的RV32 CPU是否符合标准,在riscv-software-src/riscv-tests 下。使用官方工具链编译时,必须带上_Zicsr扩展,这是因为其测试指令和初始化指令中用到了CSR寄存器,不过即使不支持也没关系。
在测试前,还需要修改一下DRAM。lw测试子集会直接从DRAM中读取值,而我们默认是将DRAM全部初始化为32'h00000000。考虑到之前已在IROM模块中指定了内存读取范围$readmemh(testcase, rom_data, 0, (1 << ADDR_WIDTH) - 1),这里在DRAM中可不指定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 logic [31 :0 ] ram_data[1 << ADDR_WIDTH];initial begin integer i; string testcase; for (i = 0 ; i < 1 << ADDR_WIDTH; i = i + 1 ) begin ram_data[i] = 32'h00000000 ; end if ($value$plusargs ("TESTCASE=%s" , testcase)) begin $readmemh (testcase, ram_data); $display ("DRAM: Loaded memory image from %s" , testcase); end end
写个简单的Makefile进行测试,结果如下:
后记
我们做完了吗?做完了99%。
后面,可能会首先完成Zicsr扩展与M扩展,再之后会往自己的项目上靠,研究一下最新的控制流加密扩展Zicfilp和Zicfiss。A、C和F扩展暂不考虑。