目录
逻辑和代码设计部分是异步FIFO的关键部分。
按照从外到内的顺序设计逻辑(外部模块的端口是确定的,内部模块的要求可动态调整)
所以先读写逻辑,最后是SDRAM
1. wr_logic 与 rd_logic
三个功能,生成对RAM的读写地址和使能,同时根据写指针和读指针判断FIFO是为full状态还是empty状态
先上一张思维导读,以理解整个设计思路,不是拍脑袋
1.1. RAM 读使能与写使能
这个好说,联合FIFO读写使能和空满标志即可
assign wr_en_ram = wr_en & (!full);
assign rd_en_ram = rd_en & (!empty);
1.2. full与empty判定:读写指针的扩展位
位宽相同的 读写指针
首先我们需要两个指针,分别表示下次写入的RAM地址,即写指针和下次读出的RAM地址,即读指针,且这两种指针的位宽相同
相同的地址位宽是为了便于full和empty判定,后文的SDRAM访问也方便,所以暂使位宽相同。
注意在这种情况下,读指针走的路程永远不会超过写指针。
● 那么如何判断空满呢?认为当再执行一次访问时,出现数据覆盖or空读时,此时RAM要么写满要么读空
注意地址位宽相同但数据位宽是不同的!
例如设置FIFO写数宽16bit,FIFO读数宽8bit,FIFO深度为64bit。
若RAM每个地址的数据位宽选择8bit,此时每次读直接读出对应地址的数据,而每次写则需要写入两个地址的数据。
如下图所示由于每次写要写两个地址的数据,所以如果发现这次写会出现数据覆盖时,就full有效。
而读是每次读一个地址,所以如果发现这次读会出现空读时,就empty有效。
读写指针的 扩展位
此处SDRAM数据位宽选择读、写数据位宽中较小的那个
这必然导致一种访问可以一个地址一个地址得访问,另一种访问必须每次访问多个地址。
这就可以根据指针之间的相隔的地址个数判断空满。
但有一个问题,如果两个指针指向相同的地址,此时是空还是满呢?
解决方法是,将读写指针的位数扩展一位,每当走过一个FIFO深度时,该bit翻转。
扩展位相同,则读空empty有效,扩展位不同,则写满full有效。
注意full和empty都是组合逻辑。如果FIFO满了还写就会覆盖,FIFO空了还读就是空数
1.3. RAM 读指针 与 写指针
复位时,俩指针指向SDRAM的第一个地址。
● 之后每当wr_en有效时,检查full信号是否有效,若无效就将wdata的数据写入RAM,同时写指针移动。否则不能向RAM写入,可通过控制RAM的wr_en端口阻止写入
● 之后每当rd_en有效时,检查empty信号是否有效,若无效就从RAM读出rdata的数据,同时读指针移动。否则不能从RAM读出,可通过控制RAM的rd_en端口阻止读出
之后根据上述full和empty的逻辑判断空满即可。但是注意读写指针均会涉及跨时钟域传输,该如何处理?
跨时钟域 中间态问题:带扩展位的Grey码
● 多bit跨时钟域传输
由于读写指针是多bit二进制变化,所以很可能由于时钟偏斜,时钟沿不同时到达各触发器,导致那几个bit位的变化不是同时的
所以异步时钟很可能采样到跳变过程的中间态
例如读指针从01跳变10,由于时钟偏斜第一位posedge rclk先到、第二位posedge rclk后到,所以wclk处第一个触发器D端变成1早、第二个触发器D端变成0晚,故wclk就可能采样到11,这会导致full判断出错以及其它问题。
而该问题的本质是每个时钟周期都涉及多个触发器的电平变化,那么能不能减少每个时钟周期电平变化的触发器个数?
● Grey码计数器
有,Grey码计数器,每个时钟沿只有1bit发生变化,最多采样到一种错值,即旧值。
那问题来了,怎么实现Grey码计数器?直接实现似乎比较困难。
我的实现方式是将二进制计数器的组合逻辑部分和触发器之间加一个Binary2Grey的码制转换。
SDRAM那边不出意外的话应该是用二进制计数器,直接使用Grey码计数器的二进制组合逻辑部分的输出,然后打一拍再连地址端。
即Grey码计数器的组成是:二进制组合逻辑 + Binary2Grey的码制转换 + 触发器
这里打一拍是因为这里的触发器的初始值为0,是指触发器Q端初始是0,而D端初始应该是1。所以二进制组合逻辑部分的输出初始值也是1,所以要先打一拍。
或者是Grey码计数器出来再接一个Grey2Binary转换模块,再连到SDRAM的地址端。但这样耗费资源多一些
● 带扩展位的Grey码
别忘了full和empty要求计数器要扩展一位的。
纯用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 | ... | ... |
以wr_logic为例模块组成如下
跨时钟域 亚稳态问题:电平同步 带来的延迟
OK方案就此敲定,现在来分析一下最后一个问题,电平同步产生的延迟会不会导致full和empty不是实时响应,导致FIFO满写or空读?
基于打拍导致的延迟,一点点推导证明就可以
● empty有效,但FIFO非空
以读时钟域为准,假如说t时刻empty拉高,表示此时rptr_g和电平同步之后wptr_g_d2满足指针对应关系。
rptr_g表示格雷码版本的读指针
wptr_g_d2表示格雷码版本的写指针,且经过读时钟电平同步
由于wptr_g_d2是写逻辑中wptr_g电平同步的结果,所以wptr_g_d2是wptr延迟一段时间的结果。
因此t时刻wptr_g要么与wptr_g_d2相同,要么已经发生移动,即已经写了几个数据入FIFO,即“虚空”
虚空的情况确实会出现,但不会有任何影响。
● empty无效,但FIFO已空
以读时钟域为准,假如说t时刻empty无效,表示此时rptr_g和电平同步之后wptr_g_d2不满足指针对应关系,那么wptr_g_d2一定要比rptr_g走的路程长。
即在读时钟域看来,此时写的数据一定比读的数据多
而wptr_g_d2还是wptr延迟一段时间的结果,那么t时刻写逻辑中的wptr一定比wptr_g_d2走的路程更长,那么此时FIFO一定有数据,且比读逻辑中看到的的数据要多。
● full有效,但FIFO非满
分析思路与“empty有效,但FIFO非空”相同
以写时钟域为准,假如说t时刻full拉高,表示此时wptr_g和电平同步之后rptr_g_d2满足指针对应关系。
但rptr_g_d2是rptr延迟一段时间的结果,故此时FIFO可能不是满的,这不会有什么影响。
● full无效,但FIFO已满
以写时钟域为准,假如说t时刻full无效,表示此时wptr_g和rptr_g_d2不满足指针对应关系,即还没满。
而rptr_g_d2还是rptr延迟一段时间的结果,那么t时刻读逻辑中的rptr一定比rptr_g_d2走的路程更长,那么此时FIFO一定比写逻辑中看到的的数据要少。
跨时钟域 快采慢:无影响
快采慢带来的问题无非就是采样好几个周期嘛,因为读写逻辑中不涉及对读写指针的计数,所以拉高多次没关系。
主要关注的是当前这一拍异步指针到底在哪。
跨时钟域 慢采快:无影响
慢采快的问题就是遗漏嘛,但是读写逻辑并不关心你异步指针是怎么走的,所以漏就漏了。
还是那句话,主要关注的是,在当前这一拍,你这个异步指针是多少,我不管你怎么走的。
2. sdpram
前文已经定好了SDRAM的读写地址位宽相同且为二进制码、读写数据位宽不同、每个地址的数据位宽为读写数据位宽中较小的那个。
那么如何实现位宽不同的数据访问呢?
该模块下会有一个寄存器变量作为内存reg [DATA_WIDTH-1:0] mem [(1<<ADDR_WIDTH)-1:0];
,这里的ADDR_WIDTH
已知了,那么DATA_WIDTH
是多少呢?
无非有两个选项,'WDATA_WIDTH和’RDATA_WIDTH,此处选择min('WDATA_WIDTH,'RDATA_WIDTH)。所以低数据位宽访问,直接访问。高数据位宽访问,多地址拼接
例如设置FIFO写数宽16bit,FIFO读数宽8bit,FIFO深度为64bit。
那么RAM写地址应该有64/16=4个,读地址有64/8=8个。此处SDRAM地址位宽选择 读地址个数对应的位宽,为3。
所以FIFO读直接读出对应地址的数据,FIFO写则需要写入两个地址的数据。