异步FIFO常用于实现多bit数据的跨时钟域传输。
4.4 Verilog FIFO 设计
1. 功能
可实现快到慢、慢到快的跨时钟域多bit数据传输,具体功能介绍如下:、
● 可对FIFO深度、宽度进行参数化自定义
● 能够实现数据的异步读写功能,且读出的数据是先入先出的顺序
● 能够指示FIFO空、满状态。同时FIFO内部数据量达到配置的参数数目时,拉高一个可配置的满状态信号
● 写数据宽度和读数据宽度可以不一致,但要实现位数拼接和分割
例如写是8bit读是32bit,那么每1次读的数据应当是每4次写的数据。
同理写是32bit读是8bit,那么每1次读的数据应当是每1次写的数据中8bit的那部分。
2. 架构
首先是异步FIFO的架构,基于自顶向下的设计思路,将功能划分为多个模块,再相互连接
2.1. 顶层模块 async_fifo
由以下几个模块构成:wr_logc、rd_logic和simple dual-port RAM,其中
● Simle dual-port RAM完成数据的存储和读写功能
● wr_logic完成写逻辑的实现,同时产生waddr与双口RAM通信。
rd_logic完成读逻辑的实现,同时产生raddr与双口RAM通信。
注意写逻辑还需要输出full信号,读逻辑需要输出empty信号,这两个信号均与读写指针有关,所以需要wptr和rptr的跨时钟域通信。
参数描述
先是端口信号描述
Signal | Direction | Width(bits) | Description |
---|
rstn | input | 1 | FIFO异步复位 |
---|
wclk | input | 1 | 写时钟 |
---|
wr_en | input | 1 | 写使能 |
---|
wdata | input | `WDATA_WIDTH | 写数据 |
---|
full | output | 1 | 写满标志 |
---|
pfull | output | 1 | 可编程满标志。当FIFO内数据数目达到`PROG_DEPTH时,该信号拉高 |
---|
rclk | input | 1 | 读时钟 |
---|
rd_en | input | 1 | 读使能 |
---|
rdata | output | `RDATA_WIDTH | 读数据 |
---|
empty | output | 1 | 读空标志 |
---|
之后是对FIFO配置参数描述
Parameter | Units | Description |
---|
`WDATA_WIDTH | bit | FIFO写数据宽度,即伪双口RAM的写数据宽度 |
---|
`RDATA_WIDTH | bit | FIFO读数据宽度,即伪双口RAM的读数据宽度 |
---|
`ASYNC_FIFO_DEPTH | bit | FIFO深度 |
---|
`PROG_DEPTH | bit | 可编程数据深度。当FIFO内数据数目达到该值时,pfull拉高 |
---|
2.2. 伪双口RAM dual_port_ram
异步FIFO由读写两个逻辑组成,需要对写数据输入输出,所以使用伪双口RAM就行了。
但是注意我们要实现读写数据宽度不同的FIFO,所以此处的RAM读写宽度也不同,相应的读写地址宽度也不同。
注意这几个RAM的区别 单口RAM(Single-port RAM):只有一端一个时钟,可读、写 伪双口RAM(Simple Dual-port RAM):一端只读,另一端只写,且读写时钟不同 真双口RAM(True Dual-port RAM):两个端口都可读、写,且两端口的时钟不同 单端口和双端口RAM的区别
参数描述
先是端口信号描述
Signal | Direction | Width(bits) | Description |
---|
rstn | input | 1 | RAM异步复位 |
---|
wclk | input | 1 | 写时钟 |
---|
wr_en | input | 1 | 写使能 |
---|
wdata | input | `WDATA_WIDTH | 写数据 |
---|
waddr | input | `WADDR_WIDTH | 写地址 |
---|
rclk | input | 1 | 读时钟 |
---|
rd_en | input | 1 | 读使能 |
---|
rdata | output | `RDATA_WIDTH | 读数据 |
---|
raddr | input | `RADDR_WIDTH | 读地址 |
---|
之后是对伪双口RAM配置参数描述
Parameter | Units | Description |
---|
`WDATA_WIDTH | bit | 伪双口RAM写数据宽度 |
---|
`RDATA_WIDTH | bit | 伪双口RAM读数据宽度 |
---|
`WADDR_WIDTH | bit | 伪双口RAM写地址宽度,需要根据FIFO深度和写数据宽度计算得出 |
---|
`RADDR_WIDTH | bit | 伪双口RAM读地址宽度,需要根据FIFO深度和读数据宽度计算得出 |
---|
例如设置FIFO写数宽16bit,FIFO读数宽8bit,FIFO深度为64bit。 那么RAM写地址应该有64/16=4个,读地址有64/8=8个,所以任意一个写地址可分为两个读地址,如下图
2.3. 写逻辑 wr_logic
写逻辑这边主要实现对伪双口RAM的写控制,同时基于读逻辑模块的rptr信号产生FIFO的full和pfull信号
参数描述
端口信号描述
Signal | Direction | Width(bits) | Description |
---|
rstn | input | 1 | 异步复位 |
---|
wclk | input | 1 | 写时钟 |
---|
wr_en | input | 1 | 写使能 |
---|
wdata | input | `WDATA_WIDTH | 写数据 |
---|
rptr_g | input | `RADDR_WIDTH+1 | 带扩展位的Grey码读指针,注意是rclk域下的信号 |
---|
Signal | Direction | Width(bits) | Description |
---|
wr_en_ram | output | 1 | 双口RAM的写使能 |
---|
waddr_ram | output | `WADDR_WIDTH | 双口RAM的写地址 |
---|
wdata_ram | output | `WDATA_WIDTH | 双口RAM的写数据 |
---|
full | output | 1 | FIFO写满标志 |
---|
pfull | output | 1 | FIFO可编程写满标志 |
---|
wptr_g | output | `WADDR_WIDTH+1 | 带扩展位的Grey码写指针 |
---|
之后是需要配置的参数描述
Parameter | Units | Description |
---|
`WDATA_WIDTH | bit | 伪双口RAM写数据宽度 |
---|
`WADDR_WIDTH | bit | 伪双口RAM写地址宽度,需要根据FIFO深度和写数据宽度计算得出 |
---|
`RADDR_WIDTH | bit | 伪双口RAM读地址宽度,需要根据FIFO深度和读数据宽度计算得出 |
---|
2.4. 读逻辑 rd_logic
读逻辑这边主要实现对伪双口RAM的读控制,同时基于写逻辑模块的wptr信号产生FIFO的empty信号
参数描述
端口信号描述
Signal | Direction | Width(bits) | Description |
---|
rstn | input | 1 | 异步复位 |
---|
rclk | input | 1 | 读时钟 |
---|
rd_en | input | 1 | 读使能 |
---|
rdata_ram | input | `RDATA_WIDTH | 读数据,来自于RAM |
---|
wptr_g | input | `WADDR_WIDTH+1 | 带扩展位的Grey码写指针,注意是wclk域下的信号 |
---|
Signal | Direction | Width(bits) | Description |
---|
rd_en_ram | output | 1 | 送入RAM的读使能 |
---|
raddr_ram | output | `RADDR_WIDTH | 双口RAM的读地址 |
---|
rdata | output | `RDATA_WIDTH | FIFO读数据 |
---|
empty | output | 1 | FIFO读空标志 |
---|
rptr_g | output | `RADDR_WIDTH | 带扩展位的Grey码读指针 |
---|
之后是需要配置的参数描述
Parameter | Units | Description |
---|
`RDATA_WIDTH | bit | 伪双口RAM读数据宽度 |
---|
`RADDR_WIDTH | bit | 伪双口RAM读地址宽度,需要根据FIFO深度和读数据宽度计算得出 |
---|
`WADDR_WIDTH | bit | 伪双口RAM写地址宽度,需要根据FIFO深度和写数据宽度计算得出 |
---|
3. 时序与代码
3.1. dual_port_ram
伪双口RAM的读写时序比较好说,采样到写就写入,采样到读就读出。
但是要考虑读写地址位宽的不同,此处采用大小地址的方案,即每个少位宽的大地址由多个多位宽的小地址组成,
如下图所示例子
注意此处RAM地址并不是Grey码形式的,而是二进制形式。异步FIFO中的Grey码是为了写指针和读指针跨时钟域传输时,避免采样到中间值而设定的。
代码
module dual_port_ram(
input rstn,
input wclk,
input wr_en,
input [`WDATA_WIDTH-1:0] wdata,
input [`WADDR_WIDTH-1:0] waddr,
input rclk,
input rd_en,
input [`RADDR_WIDTH-1:0] raddr,
output [`RDATA_WIDTH-1:0] rdata,
);
generate if(`WADDR_WIDTH > `RADDR_WIDTH)
localparam SHRINK = 1<<(`WADDR_WIDTH -`RADDR_WIDTH);
localparam HIGH_ADDR_WIDTH = `WADDR_WIDTH;
localparam HIGH_DATA_WIDTH = `WDATA_WIDTH;
localparam LOW_ADDR_WIDTH = `RADDR_WIDTH;
localparam LOW_DATA_WIDTH = `RDATA_WIDTH;
reg [HIGH_DATA_WIDTH-1:0] mem [(1<<HIGH_ADDR_WIDTH)-1:0];
reg [HIGH_ADDR_WIDTH-1:0] low_addrs [SHRINK-1:0];
integer i,j,k;
always@(posedge wclk) begin
if(wr_en)
mem[waddr] <= wdata;
end
always@(posedge rclk) begin
if(rd_en) begin
for(j = 0;j <= SHRINK - 1;j = j + 1) begin
rdata[(`RDATA_WIDTH/SHRINK)*(j+1)-1 -:(`RDATA_WIDTH/SHRINK)] <= mem[low_addrs[j]];
mem[low_addrs[j]] <= 0;
end
end
always@(*) begin
for(k = 0;k <= SHRINK - 1;k = k + 1)
low_addrs[k] = raddr * SHRINK + k;
end
else
localparam SHRINK = 1<<(`RADDR_WIDTH -`WADDR_WIDTH);
localparam HIGH_ADDR_WIDTH = `RADDR_WIDTH;
localparam HIGH_DATA_WIDTH = `RDATA_WIDTH;
localparam LOW_ADDR_WIDTH = `WADDR_WIDTH;
localparam LOW_DATA_WIDTH = `WDATA_WIDTH;
reg [HIGH_DATA_WIDTH-1:0] mem [(1<<HIGH_ADDR_WIDTH)-1:0];
reg [HIGH_ADDR_WIDTH-1:0] low_addrs [SHRINK-1:0];
integer i,j,k;
always@(posedge wclk) begin
if(wr_en) begin
for(j = 0;j <= SHRINK - 1;j = j + 1)
mem[low_addrs[j]] <= wdata[(`WDATA_WIDTH/SHRINK)*(j+1)-1 -:(`WDATA_WIDTH/SHRINK)];
end
end
always@(posedge rclk) begin
if(rd_en) begin
rdata <= mem[raddr];
mem[raddr] <= 0;
end
end
always@(*) begin
for(k = 0;k <= SHRINK - 1;k = k + 1)
low_addrs[k] = waddr * SHRINK + k;
end
endgenerate
always@(negedge rstn) begin
if(!rstn) begin
for(i = 0;i < (1<<HIGH_ADDR_WIDTH);i = i + 1)
mem[i] <= 0;
end
end
endmodule
3.2. wr_logic 与 rd_logic
三个功能,生成对RAM的读写地址和使能,同时根据写指针和读指针判断FIFO是为full状态还是empty状态
● RAM 读使能与写使能
这个好说,联合FIFO读写使能和空满标志即可
assign wr_en_ram = wr_en & (!full);
assign rd_en_ram = rd_en & (!empty);
● full与empty判定:读写指针的扩展位
首先我们需要两个指针,分别表示下次写入的RAM地址,即写指针和下次读出的RAM地址,即读指针
无论读指针还是写指针,当一个指针指向的小地址恰好为另一个指针指向的大地址的第一个小地址时,此时RAM要么写满要么读空。
那么如何分辨呢?方法是,将读写指针的位数扩展一位,每当走过一个FIFO深度时,该bit翻转。
扩展位相同,则读空empty有效,扩展位不同,则写满full有效。
注意full和empty都是组合逻辑。如果FIFO满了还写就会覆盖,FIFO空了还读就是空数
● RAM 读指针 与 写指针
读写指针要与RAM的地址端连接。
复位时,俩指针指向各自的第一个地址。
之后每当wr_en有效时,检查full信号是否有效,若无效就将wdata的数据写入RAM,同时写指针移动一个地址。否则不能向RAM写入,可通过控制RAM的wr_en端口阻止写入
之后每当rd_en有效时,检查empty信号是否有效,若无效就从RAM读出rdata的数据,同时读指针移动一个地址。否则不能从RAM读出,可通过控制RAM的rd_en端口阻止读出
之后根据上述full和empty的逻辑判断空满即可。但是注意读写指针均会涉及跨时钟域传输,该如何处理?
读写指针 跨时钟域传输:带扩展位的Grey码
● 电平同步与Grey码
电平同步是一定的,但由于读写指针是多bit,所以采样异步信号时,很可能采样到跳变过程的中间态
例如读指针正从01跳变10时被wclk采样了,就可能采样到11,这会导致full判断出错以及其它问题。
所以读写指针按照Grey码变化、传输(wptr_g和rptr_g),并在连接RAM地址端或判断full或empty时再转化为二进制码(wptr_b和rptr_b),如下图
还有一个思路就是读写指针按照二进制码变化、连接RAM和判断,只不过是在传输的过程时转化为Grey码 但是这种思路行不通,因为码值转换是组合逻辑实时变化,依然会出现中间态。 例如二进制从01跳变到10,会出中间态11,等价于Grey码从01跳变到11,会出中间态10。故没有本质改变,必须是变化也要按照Grey码变化。 但是反过来就不会。例如Grey码从01跳变到11,无中间态,等价于二进制从01跳变到10,也不会出现中间态。
其实最根本的解决办法是完全抛弃二进制码,全局使用Grey码。但是在已知大地址(Grey码)的情况下,我算不出来出其对应的那个几个小地址(Grey码)
● 带扩展位的Grey码
纯用Grey码有个问题就是,指针跑完一圈后,扩展位翻转,但其他位也会翻转呀。
例如0000(第一位是扩展位)走到最后一个地址是0100,然后回到第一个地址,按照之前我们的逻辑,此时指针是1000。1000相比0100翻转了两次!
这样我们还是会异步采样到第三态!还得改进改进
改进的思路是,指针走动方向不变,扩展位为0时其余位递增、扩展位为1时其余位递减,如下
RAM内存 | 第一圈 | 第二圈 | 第三圈 | 第四圈 | ... |
---|
指针顺序为地址一到八 | 带扩展位Grey码 | 带扩展位二进制码 | 带扩展位Grey码 | 带扩展位二进制码 | 带扩展位Grey码 | 带扩展位二进制码 | 带扩展位Grey码 | 带扩展位二进制码 | 带扩展位Grey码 | 带扩展位二进制码 |
---|
地址一 | 0 000 | 0 000 | 1 100 | 1 000 | 0 000 | 0 000 | 1 100 | 1 000 | ... | ... |
---|
地址二 | 0 001 | 0 001 | 1 101 | 1 001 | 0 001 | 0 001 | 1 101 | 1 001 | ... | ... | 地址三 | 0 011 | 0 010 | 1 111 | 1 010 | 0 011 | 0 010 | 1 111 | 1 010 | ... | ... | 地址四 | 0 010 | 0 011 | 1 110 | 1 011 | 0 010 | 0 011 | 1 110 | 1 011 | ... | ... | 地址五 | 0 110 | 0 100 | 1 010 | 1 100 | 0 110 | 0 100 | 1 010 | 1 100 | ... | ... | 地址六 | 0 111 | 0 101 | 1 011 | 1 101 | 0 111 | 0 101 | 1 011 | 1 101 | ... | ... | 地址七 | 0 101 | 0 110 | 1 001 | 1 110 | 0 101 | 0 110 | 1 001 | 1 110 | ... | ... | 地址八 | 0 100 | 0 111 | 1 000 | 1 111 | 0 100 | 0 111 | 1 000 | 1 111 | ... | ... |
---|
读写指针 跨时钟域传输:电平同步 带来的延迟
OK方案就此敲定,现在来分析一下最后一个问题,电平同步产生的延迟会不会导致full和empty不是实时响应,导致FIFO满写or空读?
● empty有效,但FIFO有数
● empty无效,但FIFO为空
● full有效,但FIFO非满
● full无效,但FIFO已满
4. 综合
|