由于slave_FIFO调用了子模块同步FIFO SCFIFO.v
,因此首先简单介绍同步FIFO的设计。
第一节 同步FIFOSCFIFO
设计
同步FIFO实体是一组存储单元,因此需要先用数组方式来实现
reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0];
其中在参数中进行如下定义,以满足本题的64深度32宽度FIFO
parameter DATA_WIDTH = 'd32 , //FIFO位宽
parameter DATA_DEPTH = 'd64
同步FIFO有计数器实现
和读写地址实现
这两种实现方式,这里采用的是读写地址实现。
reg [$clog2(DATA_DEPTH) : 0] wr_ptr; //写地址指针,位宽多一位
reg [$clog2(DATA_DEPTH) : 0] rd_ptr; //读地址指针,位宽多一位
//wire define
wire [$clog2(DATA_DEPTH) - 1 : 0] wr_ptr_true; //真实写地址指针
wire [$clog2(DATA_DEPTH) - 1 : 0] rd_ptr_true; //真实读地址指针
wire wr_ptr_msb; //写地址指针地址最高位
wire rd_ptr_msb;
assign {wr_ptr_msb,wr_ptr_true} = wr_ptr; //将最高位与其他位拼接
assign {rd_ptr_msb,rd_ptr_true} = rd_ptr;
总共64个存储32位单元,为其编码0-63,因此需要6为地址线。而这里读写地址指针wr_ptr
,rd_ptr
都是[6:0]的7位,这是因为最高位是用来判断读空和写满的。低6位才是真实地址。因此我们通过assign
把7位读写地址拆分成高1位和低6位
代码主体通过两个always实现,分别是读always和写always,不同于控制寄存器,FIFO是可以同时读写的。首先是读always
//读操作,更新读地址
always @ (posedge clk or negedge rst_n) begin
if (rst_n == 1'b0)begin
rd_ptr <= 'd0;//读地址清零
fifo_buffer[0]<=0;//清空首地址数据
data_out<=0;
end
else if (rd_en && !empty)begin //读使能有效且非空
data_out <= fifo_buffer[rd_ptr_true];
rd_ptr <= rd_ptr + 1'd1;
end
end
当清零信号到来时,读地址归零,并且清空首地址数据。同时让输出也归零。
正常工作时,根据时钟上升沿锁存的读真实地址rd_ptr_true
来读出对应地址的存储内容,并且同时读真实地址rd_ptr
自增,代表这个单元已经读取过了,避免重复读取。
接着是写always部分,与读类似,这里不再详述
//写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;//写地址清零
else if (!full && wr_en)begin //写使能有效且非满
wr_ptr <= wr_ptr + 1'd1;
fifo_buffer[wr_ptr_true] <= data_in;
end
end
注意读写的条件,除了读写有使能之外,还要满足读的时候非空,写的时候非满。
接下来是FIFO中比较重要的一点,就是读空和写满的判断。在同步FIFO中,由于一直是读指针追赶写指针,因此
- 如果读写地址完全相同,说明读地址完全追赶上了写地址,说明读空。
- 如果读写地址除了最高位完全相同,说明读写地址的值已经绕了一圈追上了读地址(写地址比读地址大了整个FIFO实际存储空间的大小,循环队列),说明写满。
根据以上关系,就有读写判断assign语句:
//当所有位相等时,读指针追到到了写指针,FIFO被读空
assign empty = ( wr_ptr == rd_ptr ) ? 1'b1 : 1'b0;
//当最高位不同但是其他位相等时,写指针超过读指针一圈,FIFO被写满
assign full = ( (wr_ptr_msb != rd_ptr_msb ) && ( wr_ptr_true == rd_ptr_true ) )? 1'b1 : 1'b0;
一般的同步FIFO设计到这里就结束了,但是由于上一章中提到,contorl_register
中有显示FIFO余量的位域margin
,因此需要把FIFO余量在FIFO模块进行输出。
很容易可以的值FIFO余量margin就是64-写寄存器比读寄存器超前的值。但是,上一章也提到,由于位宽限制,除了64之外的余量都按照其本身表示,而64表示为63.综合以上逻辑,代码如下所示。
assign FIFO_margin_o=(~empty)?((wr_ptr>=rd_ptr)?('d64-(wr_ptr-rd_ptr)):(rd_ptr-wr_ptr)):'b11_1111;
SCFIFO的整体代码如下
文件名SCFIFO.v
/****************<slave_FIFO的64深度同步FIFO,指针方式实现>*********************/
/****************<验证见SCFIFO2文件夹>*********************/
`timescale 1ns/100ps
module SCFIFO
#(
parameter DATA_WIDTH = 'd32 , //FIFO位宽
parameter DATA_DEPTH = 'd64 //FIFO深度
)
(
input clk , //系统时钟
input rst_n , //低电平有效的复位信号
input [DATA_WIDTH-1:0] data_in , //写入的数据
input rd_en , //读使能信号,高电平有效
input wr_en , //写使能信号,高电平有效
output reg [DATA_WIDTH-1:0] data_out, //输出的数据
output empty , //空标志,高电平表示当前FIFO已被写满
output full, //满标志,高电平表示当前FIFO已被读空
output wire [5:0] FIFO_margin_o //FIFO通道余量64也算作63
);
//reg define
//用二维数组实现RAM
reg [DATA_WIDTH - 1 : 0] fifo_buffer[DATA_DEPTH - 1 : 0];
reg [$clog2(DATA_DEPTH) : 0] wr_ptr; //写地址指针,位宽多一位
reg [$clog2(DATA_DEPTH) : 0] rd_ptr; //读地址指针,位宽多一位
//wire define
wire [$clog2(DATA_DEPTH) - 1 : 0] wr_ptr_true; //真实写地址指针
wire [$clog2(DATA_DEPTH) - 1 : 0] rd_ptr_true; //真实读地址指针
wire wr_ptr_msb; //写地址指针地址最高位
wire rd_ptr_msb; //读地址指针地址最高位
assign {wr_ptr_msb,wr_ptr_true} = wr_ptr; //将最高位与其他位拼接
assign {rd_ptr_msb,rd_ptr_true} = rd_ptr; //将最高位与其他位拼接
assign FIFO_margin_o=(~empty)?((wr_ptr>=rd_ptr)?('d64-(wr_ptr-rd_ptr)):(rd_ptr-wr_ptr)):'b11_1111;
//读操作,更新读地址
always @ (posedge clk or negedge rst_n) begin
if (rst_n == 1'b0)begin
rd_ptr <= 'd0;//读地址清零
fifo_buffer[0]<=0;//清空首地址数据
data_out<=0;
end
else if (rd_en && !empty)begin //读使能有效且非空
data_out <= fifo_buffer[rd_ptr_true];
rd_ptr <= rd_ptr + 1'd1;
end
end
//写操作,更新写地址
always @ (posedge clk or negedge rst_n) begin
if (!rst_n)
wr_ptr <= 0;//写地址清零
else if (!full && wr_en)begin //写使能有效且非满
wr_ptr <= wr_ptr + 1'd1;
fifo_buffer[wr_ptr_true] <= data_in;
end
end
//更新指示信号
//当所有位相等时,读指针追到到了写指针,FIFO被读空
assign empty = ( wr_ptr == rd_ptr ) ? 1'b1 : 1'b0;
//当最高位不同但是其他位相等时,写指针超过读指针一圈,FIFO被写满
assign full = ( (wr_ptr_msb != rd_ptr_msb ) && ( wr_ptr_true == rd_ptr_true ) )? 1'b1 : 1'b0;
endmodule
第二节 通道从端slave_FIFO
理解
通道从端接口时序在文档中描述如下
一个通道从端包括图1中的slaveX及FIFOX(X=0,1,2)。其中FIFO深度为64。
通道从端从外部接口接收数据,当接收到一个完整的数据包(默认32,由通道X的控制寄存器bit[5:3]
决定)后,则向arbiter发出发送请求。
若请求信号得到响应,则开始发送,直至整个数据包全部发送完成后,再根据情况确定是否发出发送数据请求(是否数据多余32,因为这时无法读入)。如图所示。当
chx_valid
信号为高时,表示写入数据有效。此周期chx_data
应给出要写入的数据。若该时钟周期chx_ready
为高,则表示已经将数据写入。若此时钟周期chx_ready
信号为低,则表示数据还未写入,需要等待chx_ready
为高时才将数据写入。
这里输入的大概意思就是,chx_valid
和chx_data
是对应关系的,在当前时钟周期,如果chx_data
对应的chx_valid
是1,则在下个时钟周期被存入FIFO(当然,假设FIFO没满还能存).反之,如果chx_valid
是0,那么该数据就不被存入。
其中chx_ready
是发送请求,如果FIFO比较满,则将chx_ready
置位低,表示没有准备好接收数据。而在当前时钟周期内,若chx_ready
为低,则chx_data
同样在下一周期不被写入FIFO。chx_ready
的作用是跟外界(即发送数据的模块,这个不再我们设计的范围内)通信,告诉它这个数据并没有接收,让其重新发送。
确定完输入时序,接下来就是确定如何跟子模块SCFIFO
进行连接。首先显而易见的是,full
和empty
信号并不需要使用,因为这里使用margin
来判断FIFO内部的存储情况。
FIFO复位信号rst_n
: 由于除了从端的输入复位之外,还有通道使能和复位有关,因此FIFO的复位信号应该是以上两个信号相与
assign rst_n=rstn_i&slvx_en_i;
FIFO使能信号wr_en
: 如前输入时序所述,需要同时满足chx_ready
和chx_valid
才可以正常写入.
assign wr_en=chx_valid_i&chx_ready_o;
FIFO使能信号rd_en
: 相比之下读使能会复杂一些,因为读实质上就是给arbiter
发送数据。发送需要遵守发送机制和握手规则。比方说,必须FIFO内至少有一个包才能发送。因为发送以包为单位发送的。此外,还需要arbiter
的允许发送信号才可以发送。
因此可以引入一个计数信号i,以确保发送过程的连续性。同时i还可以用来产生发送的有效信号slavx_val_o
.i的相关代码如下
always @(posedge clk_i or negedge rst_n) begin//产生输出长度i计数信号
if(~rst_n)
i<=0;
else if(a2sx_ack_i==1)
i<=1;
else if(i>0 && i<pkglen-1) //^^C语言连续不等式verilog不支持
i<=i+1;
else
i<=0;
end
我们期望产生的i的时序如下图所示
其中1,2,3分别标志着第1/2/3个数据,0表示最后一个数据或者已经发送完毕当前没有发送任务。其中i的计数量是根据control_register
中当前通道的包长度来确定的。如上图所示,我们在i的基础上变换出波形val_i
以表征 i 不为0的时刻.此外,根据上级arbiter请求发送信号a2sx_ack_i
和val_i
的关系,我们进行一个波形变换,将其或起来,就得到了rd_en
,观察其波形如上图,发现正好满足需要读出数据的时序:给出数据时刻(start
,end
来表征start
上升沿到end
下降沿)正好延后读使能rd_en
一个时钟周期,符合FIFO的输入输出时序关系。
确定rd_en
后,根据时序关系易知,slavx_val_o
就是rd_en
延迟一个时钟。
一点小的改造:由于后续需要输出end
信号来表征数据包发送结束,由于在slave_FIFO
模块中,我们有变量i来精确追踪输出数据包的时间,因此我们在本模块中产生end
信号然后发送给其他模块即可。
assign val_i=(i==0)?0:1;
assign rd_en=val_i | a2sx_ack_i;
第三节 通道从端slave_FIFO
代码实现
综合上2节,代码如下
文件名:slave_FIFO.v
/*************************<MCDF通道从端>*********************/
`timescale 1ns/100ps
`include "../SCFIFO/SCFIFO.v"
/*************************<端口声明>*********************/
module slave_FIFO
#(
parameter DATA_DEPTH = 'd64 //FIFO深度
)
(
input clk_i, //时钟
rstn_i, //复位
chx_valid_i, //外部信号有效信号
a2sx_ack_i, //仲裁器允许读数据包
slvx_en_i, //通道使能(来自寄存器)
input wire [31:0] chx_data_i,
input wire [2:0] slvx_pkglen_i, //本通道数据包长度
output wire chx_ready_o, //允许接收外部信号(如果FIFO比较满就不接收)
output wire [5:0] margin_o, //本通道FIFO余量
output wire [31:0] slvx_data_o, //发送给仲裁器的数据
output reg slvx_val_o, //发送给仲裁器的数据有效
output wire slvx_req_o,
output reg slvx_end_o //增加的
);
/*************************<中间信号>*********************/
wire [5:0] pkglen;
integer i;
wire wr_en,
rd_en,
val_i,//i不为0时该信号为1,用于生成rd_en的中间信号
rst_n;
assign wr_en=chx_valid_i&chx_ready_o;
assign chx_ready_o=(margin_o>='d32);//这里比较充裕,可以(margin_o<slvx_pkglen_i)
assign rst_n=rstn_i&slvx_en_i;
assign pkglen=2**(slvx_pkglen_i+'d2);//因为slvx_pkglen_i时[2:0]所以+2不会溢出
/*************************<实例化SCFIFO>*********************/
SCFIFO FIFO1(
.clk(clk_i) ,
.rst_n(rst_n) ,
.data_in(chx_data_i) ,
.rd_en(rd_en) ,
.wr_en(wr_en) ,
.data_out(slvx_data_o),
.empty(),//保留
.full(), //保留
.FIFO_margin_o(margin_o)
);
/*************************<时序电路>*********************/
always @(posedge clk_i or negedge rst_n) begin//产生输出长度i计数信号
if(~rst_n)
i<=0;
else if(a2sx_ack_i==1)
i<=1;
else if(i>0 && i<pkglen-1)//^^C语言连续不等式是错误的
i<=i+1;
else
i<=0;
end
always @(posedge clk_i or negedge rst_n) begin
if(~rst_n)
slvx_val_o<=0;
else
slvx_val_o<=rd_en;
end
always @(posedge clk_i or negedge rst_n) begin
if(~rst_n)
slvx_end_o<=0;
else if(i==pkglen-1)
slvx_end_o<=1;
else
slvx_end_o<=0;
end
/*************************<组合电路>*********************/
assign val_i=(i==0)?0:1;
assign rd_en=val_i | a2sx_ack_i;
assign slvx_req_o=(margin_o<=DATA_DEPTH-pkglen)?1:0;//如果FIFO使用量大于一个pkglen,则请求发送
endmodule
第四节 通道从端slave_FIFO
testbench实现
文件名:slave_FIFO_tb.v
`timescale 1ns/100ps
`include "slave_FIFO.v"
/*************************<端口声明>*********************/
module slave_FIFO_tb;
reg clk_i;
reg rstn_i;
reg chx_valid_i;
reg a2sx_ack_i;
reg slvx_en_i;
reg [31:0] chx_data_i;
reg [2:0] slvx_pkglen_i;
wire chx_ready_o;
wire [5:0] margin_o;
wire [31:0] slvx_data_o;
wire slvx_val_o;
wire slvx_req_o;
wire slvx_end_o;
/********************<实例化slave_FIFO>*********************/
slave_FIFO slave1(
clk_i,
rstn_i,
chx_valid_i,
a2sx_ack_i,
slvx_en_i,
chx_data_i,
slvx_pkglen_i,
chx_ready_o,
margin_o,
slvx_data_o,
slvx_val_o,
slvx_req_o,
slvx_end_o
);
initial begin
clk_i<=0;//
rstn_i<=0;//
chx_valid_i<=1;
a2sx_ack_i<=0;
slvx_en_i<=1;//
chx_data_i<=$random;
slvx_pkglen_i<=2'b00;//
$dumpfile("slave_FIFO.vcd");
$dumpvars;
#25 rstn_i<=1;//
repeat(2)begin
@(posedge clk_i)begin
chx_valid_i<=1;
chx_data_i<=$random;
end
end
@(posedge clk_i)begin
chx_valid_i<=0;
chx_data_i<=$random;
end
repeat(5)begin
@(posedge clk_i)begin
chx_valid_i<=1;
chx_data_i<=$random;
end
end
@(posedge clk_i)
a2sx_ack_i<=1;
@(posedge clk_i)
a2sx_ack_i<=0;chx_valid_i<=0;
#100
@(posedge clk_i)
a2sx_ack_i<=1;
@(posedge clk_i)
a2sx_ack_i<=0;
#200
repeat(40)begin
@(posedge clk_i)begin
chx_valid_i<=1;
chx_data_i<=$random;
end
end
#2000 $finish;
end
always #10 clk_i<=~clk_i;
endmodule
用gtkwave
观察波形,
存入FIFO的数据如下图所示
读出数据,以及中间信号时序如下图,可以发现数据被正确读出了
连续读取两次波形如下图所示
以上便是输入从端slave_FIFO
的设计和测试