为什么FPGA调试中双口RAM的读写冲突总是隐藏的很深很深?

双口RAM的读写冲突问题在FPGA调试中经常遇到,并且,往往是那种费了好大劲追信号追到吐血后才确认到的问题。在初学FPGA调试中,常常为了所谓的省事,在写代码设计仿真阶段就忽略了双口RAM的读写冲突问题,导致在FPGA上板调试中浪费大量的时间。本文就针对以往出现的双口RAM读写冲突问题展开讨论,希望能够给大家提个醒,内容虽然简单,但的确是不容忽视的一个隐藏很深的大问题。

FPGA调试本身就是挺辛苦的一件事情,尤其是在刚开始调试FPGA的时候,无论培训的时候如何强调一些注意事项,如跨时钟域问题,如接口问题,以及RAM读写冲突问题,但一旦做起项目来,每每还是有同学必须要亲自往这些坑里面跳一次才真正懂得这些BUG的含义。

这么多年了,每届学生都是如此。

双口RAM对同一个地址同时读写、对同一个地址写的冲突,每年都要跟这个问题斗争一两次,每次少则几天,多则几周时间。

下面是一个经过了数天定位到的一个问题,双口RAM两侧同时向同一个地址写入数据导致出错的Vivado调试波形截图。(ZL)


案例1(TK)

1、问题现象:

以队列信息中的队列中分组个数的值为例,由于更新错误导致当队列中实际为空(即分组个数为0),而队列信息显示队列分组个数不为0,导致输出错误数据帧。

2、问题分析

起初是固定的单次发帧,由于没有出入队的连续更新的情况未出现问题。后来在连续发随机帧的时候出现问题。排查到后来看到发现队列帧数计数有问题。

队列信息,如帧数目记录在双口RAM中,每次出队和入队都要更新这个值。此处考虑以下两个问题 :

(1) 会不会同时进入 updating,即两口不能同时对同一地址进行写操作。

(2) 会不会接续写 :由于更新是以当前值为基础,入队时分组数更新数值为 当前值+1,出队时-1,一个端口写入后下一个时钟才能读出,故不能两口在连续的两个时钟进行更新。

3、解决方法

RAM包括如下三种操作模式:

  • no-change :写过程中 输出不变
  • read:写入的先放入存储器内,输出数据线上数据不变。(上个周期写入的,这个周期先读出之前的值)
  • write:写入的,立马放在输出的数据线上

我们可以知道,在写优先的情况下,可以最快得到更新值。因此,双端口RAM的队列信息更新要保证至少要间隔一个钟(且为写优先的情况)。

以下是在三种操作模式下的双端口RAM更新举例,以分组个数为例,出队更新值为输出值-1,入队更新值为输出值+1。



最终解决发现例化IP核时,没有设置成写优先,其默认为no-change,另外在设计过程中,由于流水线设计对时钟数要求没那么严格的情况下,对于RAM的输出值可以延迟一拍输出,得到更稳当的输出值。

案例2(ZHF)

  • 问题:
  • 队列长度信息RAM a b口读写异常,更新出错。

  • 现象:
  • 端口卡死,某队列长度达到最大门限,但是发送调度显示队列为空,新数据帧入队申请,不满足门限要求而丢弃,输出没有调度结果,也不能出队操作;

    图5.9 双口RAM a b口读写异常

  • 分析定位:
  • 根据现象中停止发送,对队列长度信息更新相关信号进行Debug测试,定位出问题的根源位置如图5.9所示,该队里进行入队操作后,队列长度信息被入队调度通过队列信息RAM a口更新写入长度13,此时出队操作正在执行,在获取队列长度信时,在a口刚写入后的一个clk,读取得到队列长度信息为12,再经过1个clk,数据稳定在13;但是异常数据12被出队操作获取到并用于出队号队列长度的更新,队列长度更新出错,若干次操作后,当最后一帧出队完成后,会将队列长度更新为负数,如图5.10所示,FPGA中不操作负数,即二进制中很大的正数,远大于队列最大门限,后面再进行入队操作时,入队操作不满足门限要求,无法入队操作,同时出队操作认为队列为空,不会调度该队列出队操作,进入卡死状态;队列长度13的二进制表示为1101’b,12的二进制表示为1100’b,说明出现了单bit翻转错误的问题。

    图5.10 队列长度信息更新出现负数

  • 解决方法:
  • 首先考虑对RAM输出加寄存操作,但是这样会整体引入操作时延,即使入队和出队操作不是同一队列,每次在RAM读数据情况下都需要多等一个clk,为了解决该问题,采用的方法是在出队操作需要读队列长度时,如果前一个时刻a口刚更新该队列,此时a口晚一个clk再用数据,保证第二个clk读出的数据是a口写入稳定输出的,其他情况下不引入时延。

    交换机中地址学习表老化时间更新地址冲突预防(LCR)


    更新模块:用于根据同步更新表项内容同时更新多路并行的内外层MAC地址查找模块与内外层MAC地址学习模块的地址表。当同步更新表缓存器不为空时,读取同步更新表缓存器中的同步更新表项,从中获取要更新的表项地址和地址表,并同时查询多路并行的内外层MAC地址查找模块与内外层MAC地址学习模块正在读取的表项地址,判断这两个地址分别与要更新的表项地址是否为同一地址:如果不是同一地址,即没有发生读写冲突,则将地址表同时传入多路并行的内外层MAC地址查找模块与MAC地址学习模块,如果发生读写冲突,更新模块等待冲突结束后更新地址表。

    冲突检测模块设计(FXY)

    冲突检测模块的主要作用是防止地址表发生读写冲突。所谓的读写冲突是指对双口RAM内某一地址同时进行读写操作,出现此种情况会导致RAM内该冲突地址处的数据出现不可预见的变化。虽然现阶段FPGA内生成双口RAM时会选择读优先还是写优先来规避此类问题,但是结合实验室的多次上板测试经验来看,并不能彻底解决读写冲突导致数据异常的问题。因此为了实现在CPU配置地址表的同时仍能进行查表自学习操作,本设计中增加了一个冲突检测模块,通过该模块判断读写操作是否在同一地址上,从而彻底避免因读写冲突导致数据异常的现象。

    传统的冲突检测中采用的方法是:先读取一项配置信息,然后检测是否冲突,如果没有冲突再进行更新,接着读取下一项配置信息。此种方法至少需要3个时钟才能更新一份表项信息,速率相对较慢,但是状态跳转很简单。为了适应以后更高的数据速率,本设计中采用了一种基于预读取方式的冲突检测的方法,实现高速的读写冲突检测。具体的方法是:先读取一项配置信息,然后对该配置信息进行冲突检测,在检测的同时预读取一项新的配置信息,当完成前配置信息的检测更新后,直接对新的配置信息进行检测并更新。采用此种方法的优点是,当没有发生读写冲突时可以达到一个时钟周期更新一个地址表项,理论上能够达到了地址表最大更新速率。

    冲突检测模块的状态转移图如下图所示。


    IDLE:空闲状态,等待CPU发送过来的配置信息,当检测到FIFO为非空时,跳到REN状态,否则保持原状态。

    REN:读使能状态,该状态用来拉高FIFO的读使能,然后跳转到CHECK状态;

    CHECK:冲突检测状态,在该状态中主要有以下操作。

    • 如果FIFO为非空则在该状态中继续拉高FIFO的读使能。
    • 当FIFO为空且没有发生读写冲突,下一状态跳转到DLE状态。
    • 如果要更新的表项地址与查找模块和学习模块提供的读地址相同,代表发生了读写冲突,将上一状态拉高读使能得到的配置信息放置到寄存器中,不进行任何更新操作,然后跳转到COLLISION1状态,如果此时FIFO为空,则跳转到COLLISION2状态。
    • 如果要更新的表项地址与查找模块和学习模块提供的读地址不同,则表示没有发生读写冲突,根据配置信息更新地址表对应的表项信息,并保持原状态。

    COLLISION1:冲突1状态,指的是上一CHECK状态检测到冲突,但是FIFO为非空状态,在此状态中拉低FIFO读使能,由上两节关于查找模块和学习模块的介绍可知:如果在上一时钟对RAM进行了读操作,则在下一周期绝不会再进行读操作。因此在此状态下,可以将上一状态中寄存的配置信息更新到地址表相应的位置,然后跳转到CHECK状态。

    COLLISION2:冲突2状态,指的是上一CHECK状态检测到冲突,但是FIFO为空,与COLLISION1状态类似,在此状态中拉低FIFO读使能,然后用寄存器中的配置信息更新地址表相应位置内的信息,然后跳转到IDLE状态。

    对于状态CHECK和COLLISION1的补充:由于FIFO的输出具有保持的功能,也就是从FIFO读取一组数据,如果不再拉高读使能,则这组数据会一直在FIFO读数据端口保留。当在CHECK状态拉高了FIFO读使能,当检测到读写冲突并跳转到COLLISION1状态后,由于在COLLISION1状态并没有继续拉高FIFO的读使能,因此由CHECK状态拉高读使能而读出的一组新的数据会一直保持在FIFO的读数据端口上,由COLLISION1状态再回到CHECK状态后,可以直接提取FIFO读数据端口的数据进行冲突检测并更新。

    从上面的状态转移描述可以看出,当FIFO内存有足够多配置信息的时候,如果冲突检测模块一直没有检测到读写冲突,则该模块会一直保持在CHECK状态,并且每一时钟从FIFO读取一帧配置信息,并将配置信息更新到地址表中,实现一个时钟更新一条表项信息的最大速率。


    全文完。

    本文转自:网络交换FPGA,转载此文目的在于传递更多信息,版权归原作者所有。
    *本文由网络交换FPGA授权转发,如需转载请联系作者本人

    最新文章