综合器说好,那才是真好

模块综合

写出的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组成的阵列

I  SYNTHESIS  65,536  DFFs  !!!

综合后的估计功耗直接拉到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。不出意外的话,就会出意外:

SL大法坏!

问题出在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
// 回写数据来源选择MUX
// 对于同步DRAM DRAM的spo已经是寄存器输出 在WB级直接使用以避免多余延迟
// DRAM_output_data在整个WB周期内保持稳定 可以安全地被寄存器堆采样
assign rf_wd_WB = (wd_sel_WB == `WD_SEL_FROM_DRAM) ? DRAM_output_data : rf_wd_WB_from_PR;

再次测试,一切正常。

再次综合

在Vivado中进行综合。可以看到,结果很完美:

被正确识别成RAM的SDRAM