综合器说好,那才是真好
模块综合
写出的HDL代码需要送入综合器中,生成实际的门电路。综合器会在这个过程中推测你写的代码,并帮你进行一定的优化,最终生成文件。
这里选择开源的Yosys进行综合。官方没有发布适用于Windows的二进制可执行文件,但是Python的WASM库有,虽然是给WebAssembly环境准备的,需要电脑上有wasmtime才行。编写一个综合脚本:
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
| # 读取所有模块(不需要 -formal) read_slang ./user/src/include/defines.svh
read_slang ./user/src/PC.sv read_slang ./user/src/IROM.sv
read_slang ./user/src/PR_IF_ID.sv
read_slang ./user/src/Decoder.sv read_slang ./user/src/imm_extender.sv read_slang ./user/src/RegisterF.sv
read_slang ./user/src/PR_ID_EX.sv
read_slang ./user/src/ALU.sv read_slang ./user/src/NextPC_Generator.sv
read_slang ./user/src/PR_EX_MEM.sv read_slang ./user/src/DRAM.sv read_slang ./user/src/LoadStoreUnit.sv
read_slang ./user/src/PR_MEM_WB.sv
read_slang ./user/src/HazardUnit.sv
read_slang ./user/src/CPU_TOP.sv
# read_slang ./user/sim/tb_CPU_TOP.sv
# 指定顶层模块,并做基础处理 prep -top CPU_TOP # prep -top CPU_TOP -rdff # 此处不能将触发器合并到内存读端口 # prep -top tb_CPU_TOP -flatten
# 写出 JSON write_json ./prj/netlist/CPU_TOP.json
|
之后运行wasmtime --dir . .\yosys.wasm --.\prj\netlist\CPU_TOP.ys即可开始综合。所有的模块(不含IROM)综合结果如下:
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
| === design hierarchy ===
+----------Count including submodules. | 236 CPU_TOP 29 ALU 4 DRAM 49 Decoder 44 HazardUnit 15 NextPC_Generator 4 PC 9 PR_EX_MEM 36 PR_ID_EX 12 PR_IF_ID 5 PR_MEM_WB 9 RegisterF 11 imm_extender
+----------Count including submodules. | 416 wires 4302 wire bits 262 public wires 3205 public wire bits 170 ports 2027 port bits - memories - memory bits - processes 236 cells 3 $add 37 $aldff 1 $and 55 $eq 1 $ge 22 $logic_and 6 $logic_not 12 $logic_or 2 $lt 2 $mem_v2 59 $mux 4 $not 1 $or 11 $pmux 5 $reduce_bool 10 $reduce_or 1 $shl 1 $shr 1 $sshr 1 $sub 1 $xor 12 submodules 1 ALU 1 DRAM 1 Decoder 1 HazardUnit 1 NextPC_Generator 1 PC 1 PR_EX_MEM 1 PR_ID_EX 1 PR_IF_ID 1 PR_MEM_WB 1 RegisterF 1 imm_extender
|
同步读、异步读
对于前面的DRAM,我们也综合一下。这里将异步与同步DRAM的综合结果放在一起,左边为异步,右边为同步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| === DRAM ===
+----------Local Count, excluding submodules. | Default SDRAM | --------- --------- 8 wires 8 wires 147 wire bits 147 wire bits 5 public wires 5 public wires 82 public wire bits 82 public wire bits 5 ports 5 ports 82 port bits 82 port bits 4 cells 4 cells 1 $dff 1 $mem_v2 1 $mem_v2 3 $mux 2 $mux
|
可以看到,区别是一个MUX和一个DFF。
Yosys很聪明,但是Xilinx很傻逼,就不这么想了。对于前面的“同步写异步读”DRAM,Xilinx Vivado会把它综合为六万个DFF组成的阵列:

综合后的估计功耗直接拉到15w了,很难绷得住。因此,我们必须修改为“同步写同步读”的DRAM——这在工业生产上也是规范。不论是Xilinx还是其他厂商,其FPGA需要使用的BlockRAM IP核都是同步读写的,有的甚至需要两拍。
DRAM魔改
我们必须立即马上使用SDRAM。
时序问题
我们只需要改动一行即可:
1 2 3 4 5 6 7
| always_ff @(posedge clk) begin if (we) begin ram_data[a] <= din; end spo <= ram_data[a]; end
|
然后测试一下简单的S-Type和L-Type。不出意外的话,就会出意外:

问题出在wd_sel_MEM。可以看到,在其值为1(选择DRAM输出数据)时,SDRAM压根就没准备好数据。这是因为同步写时数据在count=5时被写入,异步读可以在数值变化的那一刻就开始跟随输出,但同步读只有在时钟沿到来时(count=6)才能读出数据,而这时候wd_sel_MEM已经变为0,选择rf_wd的数据了。
怎么办?我们需要让wd_sel_MEM打一拍。最简单的方法,就是让它穿过最后一级流水线寄存器,在MEM/WB级的寄存器内被打一拍。同样的,我们的rf_wd数据也需要被打一拍。
这里有个问题:我们从SDRAM取的数据要不要送进MEM/WB级流水线寄存器?
答案是不需要。别忘了,我们的问题就是SDRAM的数据在上升沿才被写入读取,同步读取的数据相对原来异步读取的时候已经被打了一拍了。
EX/MEM级流水线寄存器修改
1 2 3 4 5 6 7 8 9 10 11 12
| input logic [ 1:0] wd_sel_mem_i, output logic [ 1:0] wd_sel_wb_o,
always_ff @(posedge clk or negedge rst_n) begin if (!rst_n) begin wd_sel_wb_o <= 2'b0; end else begin wd_sel_wb_o <= wd_sel_mem_i; end end
|
DRAM数据选择MUX
1 2 3 4
|
assign rf_wd_WB = (wd_sel_WB == `WD_SEL_FROM_DRAM) ? DRAM_output_data : rf_wd_WB_from_PR;
|
再次测试,一切正常。
再次综合
在Vivado中进行综合。可以看到,结果很完美:
