根据前面几章的介绍,我们已经大致完成了MCDF
的子模块设计和波形测试,分别是control_regisyer
、slave_FIFO
、arbiter
、formatter
。
当然,由于握手机制等一些信号检查在顶层模块中,更容易进行检查,也容易进行调整各个模块的协作关系。比如,end
信号就是在测试顶层模块时发现,将其增加在slave_FIFO
中进行产生更加方便,因此对slave_FIFO
端口做出了一些调整。
下面先对设计文档进行解读,重点关注
1.模块之间的连接关系
2.模块与外部端口信号的映射关系
第一节 MCDF
文档理解
引用文档中的MCDF顶层设计图
在真正连接之前,我先用testbench
给出了一个波形参考。
文件名seq_ref.v
/********<MCDF重要信号时序参考:sequence_reference>*********/
`timescale 1ns/100ps
module seq_ref;
reg clk_i;
reg rst_n;
reg fmt_grant;
wire a2sx_ack,
f2a_ack;//这三个信号为同一个信号,持续至少一个时钟周期,在开始发送后可以降低
assign f2a_ack=fmt_grant & fmt_req;
assign a2sx_ack=fmt_grant & fmt_req;
reg fmt_id_req;//formatter空闲时为高
reg [1:0] a2f_id;//就绪通道信号
reg [31:0] fmt_data;//送出数据
reg fmt_strat,
fmt_req,
fmt_end;//!!!end的产生可能与i息息相关
initial begin
clk_i<=0;
rst_n<=0;
fmt_grant<=0;
fmt_id_req<=0;//
a2f_id<=2'b11;//
fmt_data<=0;
fmt_strat<=0;
fmt_end<=0;
fmt_req<=0;
$dumpfile("seq_ref.vcd");
$dumpvars;
#15 rst_n<=1;
@(posedge clk_i) fmt_id_req<=1;
#40//等待下级就绪
@(posedge clk_i) begin
a2f_id<=2'b10;
fmt_req<=1'b1;//表示就绪,等待grant
end
#30
@(posedge clk_i) fmt_grant<=1;
@(posedge clk_i) begin
fmt_req<=0;
fmt_data<=$random;
fmt_strat<=1;
fmt_id_req<=0;
end
@(posedge clk_i)begin
fmt_grant<=0;
fmt_data<=$random;
fmt_strat<=0;
end
@(posedge clk_i)begin
fmt_strat<=0;
fmt_data<=$random;
end
@(posedge clk_i)begin
fmt_end<=1;
fmt_data<=$random;
end
@(posedge clk_i)begin
fmt_end<=0;
fmt_data<=$random;
end
#500 $finish;
end
always #10 clk_i=~clk_i;
endmodule
时序参考生成的波形主要是为了给后续验证作为参考,MCDF仿真结果可以参考该波形,在满足设计文档要求的基础上进行适当修改、调整。
根据结构图的连线,在顶层模块中引入前面的子模块,根据使用的工具不同,使用的方法略有区别。如前所示,我是用的是vscode
+iverilog
+gtkwave
,因此我在代码里面加入了`include语句,而如果是其他集成开发工具,会通过用户菜单窗口将代码添加进来,就不需要`include语句。需要注意的,为了代码的方便管理,我将每个子模块都放在一个文件夹内,因此`include需要不能不指明路径(因为这只适用于引用文件和被引用模块在同一文件夹下的路径),因此include如下。
`include "../slave_FIFO/slave_FIFO.v"
`include "../control_register/control_register.v"
`include "../arbiter/arbiter.v"
`include "../formatter/formatter.v"
第二节 MCDF
代码实现
文件名:MCDF.v
主要完成子模块的端口连接,注意不要连接错误即可
/***************<MCDF 顶层文件>*********************/
`timescale 1ns/100ps
`include "../slave_FIFO/slave_FIFO.v"
`include "../control_register/control_register.v"
`include "../arbiter/arbiter.v"
`include "../formatter/formatter.v"
/***************<MCDF 顶层文件端口>*********************/
module MCDF
(
input clk ,
rst_n ,
// >>slave接口(3*3)
input wire [31:0] ch0_data ,
input ch0_valid ,
output ch0_ready ,
input wire [31:0] ch1_data ,
input ch1_valid ,
output ch1_ready ,
input wire [31:0] ch2_data ,
input ch2_valid ,
output ch2_ready ,
// >>formatter接口(7)
input fmt_grant ,
output wire [1:0] fmt_child ,
output wire [5:0] fmt_length ,
output fmt_req ,
output wire [31:0] fmt_data ,
output fmt_start ,
output fmt_end ,
// >>control_register接口(4)
input wire [1:0] cmd ,
input wire [5:0] cmd_addr ,
input wire [31:0] cmd_data_i ,
output wire [31:0] cmd_data_o
);
/***************<中间信号>*********************/
wire [5:0] slv0_margin,//slv->cr
slv1_margin,
slv2_margin;
wire slv0_en, //cr->slv
slv1_en,
slv2_en;
wire [1:0] slv0_prio, //cr->arbiter
slv1_prio,
slv2_prio;
wire [2:0] slv0_pkglen, //cr->slv
slv1_pkglen,
slv2_pkglen;
wire a2s0_ack, //arbiter->slv
a2s1_ack,
a2s2_ack;
wire [31:0] slv0_data, //slv->arbiter
slv1_data,
slv2_data;
wire slv0_val, //slv->arbiter
slv1_val,
slv2_val;
wire slv0_req, //slv->arbiter
slv1_req,
slv2_req;
wire slv0_end, //slv->arbiter
slv1_end,
slv2_end;
wire f2a_id_req ,//formatter->arbiter
f2a_ack ;
wire a2f_val ,//arbiter->formatter-
a2f_end ;
wire [1:0] a2f_id ;
wire [31:0] a2f_data ;
wire [2:0] a2f_pkglen_sel ;
/***************<实例化模块>*********************/
//控制寄存器
control_register cr0(
.clk_i (clk),
.rstn_i (rst_n),
.cmd_i (cmd),
.cmd_addr_i (cmd_addr),
.cmd_data_i (cmd_data_i),
.slv0_margin_i (slv0_margin),
.slv1_margin_i (slv1_margin),
.slv2_margin_i (slv2_margin),
.slv0_en_o (slv0_en),
.slv1_en_o (slv1_en),
.slv2_en_o (slv2_en),
.cmd_data_o (cmd_data_o),
.slv0_prio_o (slv0_prio),
.slv1_prio_o (slv1_prio),
.slv2_prio_o (slv2_prio),
.slv0_pkglen_o (slv0_pkglen),
.slv1_pkglen_o (slv1_pkglen),
.slv2_pkglen_o (slv2_pkglen)
);
//接收器
slave_FIFO slave0(
.clk_i (clk),
.rstn_i (rst_n),
.chx_valid_i (ch0_valid),
.a2sx_ack_i (a2s0_ack),
.slvx_en_i (slv0_en),
.chx_data_i (ch0_data),
.slvx_pkglen_i (slv0_pkglen),
.chx_ready_o (ch0_ready),
.margin_o (slv0_margin),
.slvx_data_o (slv0_data),
.slvx_val_o (slv0_val),
.slvx_req_o (slv0_req),
.slvx_end_o (slv0_end)
);
slave_FIFO slave1(
.clk_i (clk),
.rstn_i (rst_n),
.chx_valid_i (ch1_valid),
.a2sx_ack_i (a2s1_ack),
.slvx_en_i (slv1_en),
.chx_data_i (ch1_data),
.slvx_pkglen_i (slv1_pkglen),
.chx_ready_o (ch1_ready),
.margin_o (slv1_margin),
.slvx_data_o (slv1_data),
.slvx_val_o (slv1_val),
.slvx_req_o (slv1_req),
.slvx_end_o (slv1_end)
);
slave_FIFO slave2(
.clk_i (clk),
.rstn_i (rst_n),
.chx_valid_i (ch2_valid),
.a2sx_ack_i (a2s2_ack),
.slvx_en_i (slv2_en),
.chx_data_i (ch2_data),
.slvx_pkglen_i (slv2_pkglen),
.chx_ready_o (ch2_ready),
.margin_o (slv2_margin),
.slvx_data_o (slv2_data),
.slvx_val_o (slv2_val),
.slvx_req_o (slv2_req),
.slvx_end_o (slv2_end)
);
//仲裁器
arbiter arb0(
.clk_i (clk),
.rstn_i (rst_n),
.slv0_prio_i (slv0_prio),
.slv1_prio_i (slv1_prio),
.slv2_prio_i (slv2_prio),
.slv0_pkglen_i (slv0_pkglen),
.slv1_pkglen_i (slv1_pkglen),
.slv2_pkglen_i (slv2_pkglen),
.slv0_data_i (slv0_data),
.slv1_data_i (slv1_data),
.slv2_data_i (slv2_data),
.slv0_req_i (slv0_req),
.slv1_req_i (slv1_req),
.slv2_req_i (slv2_req),
.slv0_val_i (slv0_val),
.slv1_val_i (slv1_val),
.slv2_val_i (slv2_val),
.slv0_end_i (slv0_end),
.slv1_end_i (slv1_end),
.slv2_end_i (slv2_end),
.f2a_id_req_i (f2a_id_req),
.f2a_ack_i (f2a_ack),
.a2s0_ack_o (a2s0_ack),
.a2s1_ack_o (a2s1_ack),
.a2s2_ack_o (a2s2_ack),
.a2f_val_o (a2f_val),
.a2f_id_o (a2f_id),
.a2f_data_o (a2f_data),
.a2f_pkglen_sel_o (a2f_pkglen_sel),
.a2f_end_o (a2f_end)
);
//整形器
formatter fm0(
.clk_i (clk),
.rstn_i (rst_n),
.a2f_val_i (a2f_val),
.a2f_id_i (a2f_id),
.a2f_data_i (a2f_data),
.a2f_pkglen_sel_i (a2f_pkglen_sel),
.fmt_grant_i (fmt_grant),
.a2f_end_i (a2f_end),
.f2a_ack_o (f2a_ack),
.fmt_id_req_o (f2a_id_req),
.fmt_child_o (fmt_child),
.fmt_length_o (fmt_length),
.fmt_req_o (fmt_req),
.fmt_data_o (fmt_data),
.fmt_start_o (fmt_start),
.fmt_end_o (fmt_end)
);
endmodule
使用terosHDL插件可以查看最后的netlist,如下图所示
第三节 MCDF
testbench实现
文件名:MCDF_tb1.v
/*******************<MCDF测试1>*********************/
`timescale 1ns/100ps
`include "MCDF.v"
/*************************<端口声明>*********************/
module MCDF_tb1;
parameter RD=2'b01;
parameter WR=2'b10;
parameter IDLE=2'b00;
reg clk ,
rst_n ;
// >>slave接口(3*3)
reg [31:0] ch0_data ;
reg ch0_valid ;//先不验证
wire ch0_ready ;
reg [31:0] ch1_data ;
reg ch1_valid ;//先不验证
wire ch1_ready ;
reg [31:0] ch2_data ;
reg ch2_valid ;//先不验证
wire ch2_ready ;
// >>formatter接口(7)
reg fmt_grant ;
wire [1:0] fmt_child ;
wire [5:0] fmt_length ;
wire fmt_req ;
wire [31:0] fmt_data ;
wire fmt_start ;
wire fmt_end ;
// >>control_register接口(4)
reg [1:0] cmd ;
reg [5:0] cmd_addr ;
reg [31:0] cmd_data_i ;
wire [31:0] cmd_data_o ;
/*************************<实例化MCDF>*********************/
MCDF MCDF_inst0
(
clk ,
rst_n ,
ch0_data ,
ch0_valid ,
ch0_ready ,
ch1_data ,
ch1_valid ,
ch1_ready ,
ch2_data ,
ch2_valid ,
ch2_ready ,
fmt_grant ,
fmt_child ,
fmt_length ,
fmt_req ,
fmt_data ,
fmt_start ,
fmt_end ,
cmd ,
cmd_addr ,
cmd_data_i ,
cmd_data_o
);
/*************************<任务>*********************/
//不能用任务因为不支持多条语句
/*************************<初始化>*********************/
initial begin
clk<=0;
rst_n<=0;
ch0_data<=0;
ch1_data<=0;
ch2_data<=0;
ch0_valid<=0;//等待通道配置完毕再发送数据
ch1_valid<=0;
ch2_valid<=0;
fmt_grant<=0;//^^
cmd <=IDLE;
cmd_addr <=6'b00_0000;
cmd_data_i<=32'b001_001;//写入则打开
$dumpfile("MCDF_tb1.vcd");
$dumpvars;
/*************************<驱动信号>*********************/
#15 rst_n<=1;//启动
/**************control_register测试*********************/
//先给寄存器赋值
#40
@(posedge clk)begin//配置通道0
cmd<=WR;
cmd_addr<=6'h0;
cmd_data_i<=32'b010_01_1;
end
@(posedge clk)begin//配置通道1
cmd<=WR;
cmd_addr<=6'h4;
cmd_data_i<=32'b000_00_1;
end
@(posedge clk)begin//配置通道2
cmd<=WR;
cmd_addr<=6'h8;
cmd_data_i<=32'b001_00_1;
end
//查询寄存器配置结果
@(posedge clk)begin
cmd<=RD;
cmd_addr<=6'h0;
end
repeat(5)begin
@(posedge clk)
cmd_addr<=cmd_addr+'d4;
end
#20
cmd_addr<='h10;
#20
/**************数据传输*********************/
@(posedge clk)begin
ch0_valid<=1;
ch1_valid<=1;
ch2_valid<=1;
end
//需要等待握手ack
#80
@(posedge clk)begin
ch1_valid<=0;//关闭
end
#40
@(posedge clk)begin//这里可以给grant加入逻辑,在fmt_req之后降低
fmt_grant<=1;
end
@(posedge clk)begin
fmt_grant<=1;
end
@(posedge clk)begin
fmt_grant<=0;
end
#120
@(posedge clk)begin
fmt_grant<=1;
ch2_valid<=0;//关闭ch2
end
@(posedge clk)begin
fmt_grant<=1;
end
@(posedge clk)begin
fmt_grant<=0;
end
#80
@(posedge clk)begin
ch0_valid<=0;//关闭ch0
end
#120
@(posedge clk)begin//这里可以给grant加入逻辑,在fmt_req之后降低
fmt_grant<=1;
end
@(posedge clk)begin
fmt_grant<=1;
end
@(posedge clk)begin
fmt_grant<=0;
end
#2000 $finish;
end
always #10 clk<=~clk;
always @(posedge clk) begin
ch0_data<=$random;
ch1_data<=$random;
ch2_data<=$random;
end
endmodule
第四节 MCDF
仿真波形
使用gtkwave
打开上一节生成的波形,进行观察。
control_register波形观察
在300s之前,先对控制寄存器control_register
进行配置.
0-15ns时,对系统进行复位。这时候控制寄存器内的各个存储单元复位到默认值:通道使能,优先级为3,包长度码为0,FIFO余量为63.(这里之所以时63不是64是因为受数据位宽限制,经过调整,详述见control_regitser
设计章节)。处于idel(cmd_i=00)模式,地址归零。
15ns之后,系统启动。
70ns时,输入指令变成10(WR
),写入地址时00,即通道0的控制寄存器,写入值是16进制的13,即010_011,代表数据包长度为16,使能该通道,通道的人工优先级是1.在下一个时钟周期上升沿到来时(90ns),将数据写入通道0的控制寄存器。可以观察到通道0的优先级信号和数据包长度信号同时也更新了,这时因为这些信号时通过wire和相应寄存器直连。
在90ns,110ns时,与写入通道0的控制寄存器类似,分别为通道1和通道2的控制寄存器也写入相应设置值。波形图如下图所示。其中黄色箭头表示了数据和指令给出到数据被写入寄存器的时延为一个时钟周期。
根据设置值,各通道设置属性如下表所示
通道号 | 使能 | 人工优先级 | 自然优先级 | 数据包长度码 | 数据包长度 |
---|---|---|---|---|---|
0 | y | 1 | 0 | 010 | 16 |
1 | y | 0 | 1 | 001 | 4 |
2 | y | 0 | 2 | 001 | 8 |
130ns是一个idle空闲周期,之所以有这个周期仅仅是为了保持和设计文档给出的波形参考一致。实际上并不需要这个周期,因为读写都是同步时序,并不需要为写留出额外一个时钟周期写入寄存器。
接下来,就是进行数据读取,遍历6个寄存器,为了验证读功能,同时也可以验证之前数据写入的正确性。波形如下图所示。其中蓝色箭头表示了数据和指令给出到数据被读出寄存器的时延为一个时钟周期。由于还slave_FIFO
还没有开始读入数据,因此FIFO保持空。
接下来就可以进行数据输入来验证其他模块了,开启三通道的chx_valid输入的数据就会通过数据从端进入FIFO了。
数据输入
由于如果一直输入的话,因为数据输入和读取没有跨时钟,所以会一直读不完,不方便波形观察。因此这里选择先输入一段时间,然后停止输入。
在270ns同时开启三个通道的数据使能,但是不同的是,我们在350ns关闭通道1的数据输入。如下图所示
可以看到,当开启数据使能时,在下一个时钟posedge到来时,为FIFO存入一个数据,但是slv_margin
的真实数据从64变成63,由于前述我们用63来代表满(因为这并不影响整个MCDF工作),因此观察到的margin
仍然时63,这正是在预料之内的。继续保持数据输入,就可以观察到数据余量开始下降。我们先关停1通道的数据输入,可以观察到,通道1的余量时60,这代表着通道1内存了4个数据。
数据就绪
结合前述的通道属性表,通道1的数据包长度为4,因此FIFO中正好有了一个包,因此将通道0的slvx_req_o
置为高。slvx_req_o的生成逻辑就是,当该通道中的存入数据量,即
64
−
余
量
m
a
r
g
i
n
>
=
p
k
g
l
e
n
64-余量margin>=pkglen
64−余量margin>=pkglen
时,该通道的slvx_req_o
信号置为高,直到数据发送后FIFO内的数据量不再满足上式时再置为低。
仲裁上下级握手
这里slvx_req_o
就是一个握手信号,代表通道就绪。在arbiter
中,每次刷新选择通道时,选择的是“当前就绪通道中综合优先级最高的通道”,否则就选择虚拟通道11
代表没有通道就绪。
arbiter通道刷新时序描述如下:
上级握手:当fmt_id_req
为1时,表示formatter空闲,可以准备下一次发送。
下级握手:满足fmt_id_req
为1时查看是否有slvx_req_o
为高,选择slvx_req_o
为高且优先级最高的通道,否则选通虚拟通道11代表没用通道就绪。这里a2f_id
就是握手信号,非11表示下级就绪。
从波形图上来看就是,当前时钟周期内slvx_req
和fmt_id_req
同时为1,则在下一周期就会进行选通通道刷新。注意到这个下一周期。所以为了保证数据稳定性,我们产生一个fmt_id_req
延时信号,fmt_id_req_d1
表示延迟一个时钟周期.然后我们的fmt_req_o取延时之前和延时之后信号的交集。再叠加上下级信号握手,即保证a2f_id
不等于11,波形如下图所示。
表达式如下,完成一个波形变换。
assign fmt_req_o=fmt_id_req_o & (a2f_id_i!=2'b11) & fmt_id_req_o_d1;
综上,fmt_req
也是一个握手信号,代表整形器就绪(fmt_id_req
),下级也就绪(a2f_id!=11
)。即仲裁器上下级均就绪。因此现在等待formatter
上级就绪,即等待外部接收端给一个允许发送信号grant
。否则就一直阻塞等待在这里。
等待外部允许
由于设计文档并没有给出grant
信号持续时间,因此grant
的上升沿时需要关注的时刻。根据设计文档的时序。在fmt_req
为低时,就发送第一个数据和start
信号,因此这就严格要求f2a_ack允许FIFO发送信号在fmt_req
下降的前一周期就置为高。观察波形关系,再次进行简单的波形变换,取fmt_req
和fmt_grant
的重叠部分为f2a_ack
,这个信号直连到选通通道的a2s_ack
。而在slave_FIFO
中,a2s_ack
信号用于完成自动发送,即通过该信号驱动生成一个与数据包长度有关的序列i,同时会在数据包最后一个数据出发出end
信号。
因此f2a_ack
也是一个握手信号,该信号的下一个时钟周期,选通通道就会按照配置数据包的长度自动发送一个数据包。
由于f2a_ack
波形是根据grant
产生的,因此它不会自动发送多次,一次grant
只会发送一次数据包,然后继续进入等待。
此外,还有一个遗留问题,就是fmt_id_req_o
的具体生成,鉴于它的含义时代表formatter
空闲,因此在复位时或者end
信号结束后过一个周期置高,在fmt_grant
信号posedge之后置低。因为有了mt_grant
信号代表肯定要开始数据发送。fmt_id_req
信号的选择区间较为灵活,只要保证系统可以可靠完成功能即可。
上述握手信号各个波形的关系如下图所示.
数据发送开始与结束
第一次发送的时序关系如下,可以和设计文档中的参考时序进行对比。
start上升沿和end下降沿之间的数据就是输出的有效连续数据包。我们可以看一下一开始输入通道1FIFO的数据包,如下图所示,
我们发现,通道1存入的数据是359FDD68开头,与上面给出的数据输出数据一致。数据输出正确,并且符合时序。
接下来测试通道2发送8数据长度的数据包,
和通道0发送16长度的数据包,
发送时序和过程与通道1类似,这里就不再赘述。
以上就是MCDF的设计过程,相关代码详见附件或者github
其中,顶层文件在MCDF_ALL
文件夹下,vcd和gtkw波形也在该文件夹下,文件结构如下图所示