模块设计很巧妙,从原理分析的角度来讲挺完美的。但是,百密一疏,这个模块在设计时,对于两侧时钟域复位信号的处理,作者并未有妥善的考虑。
现象分析
先来看下面这个example和其测试代码:
我么来看下其对应的仿真波形:
可以看到,对于dataIn接口,其valid只拉高了一次,只传输了一个任务。我们在途中时刻1,时刻2处分别对clkA,clkB时钟域做复位,然而dataOut在时刻4、时刻5收到了两个任务,凭空多出来一个任务!!!
对于不同时钟域的复位信号而言,很难做到两侧时钟域复位信号同时拉起,同时释放。而这里问题的导致就是复位信号引起的。
关于StreamCCByToggle的原理,可参照前文所提到的文章,这里不再做过多赘述,小伙伴们也可以自行去阅读SpinalHDL的源代码。我们看下面的波形:
在刚开始dataIn发送了一个任务,导致pushArea_target、popArea_hit信号均变为高电平。随后clkA时钟复位先被拉起,此时pushArea_target在途中时刻1处变为低电平,经时钟同步后popArea_target在时刻2处也变为低电平。而此时popArea_hit仍为高电平。这也就导致了问题出现的原因。在StreamCCByToggle的实现原理里,clkB处的stream接口valid拉高的处理条件为:
而此时由于复位的不同步导致了valid被多拉了一拍,从而dataOut出现了一个不该出现的任务。
归结起来,StreamCCByToggle的实现完全依赖于target、hit两个信号的电平状态,每到来一个任务,两个信号电平都会反转一次。而当两个信号都为高电平时,若此时两侧时钟域出现复位拉起不同步现象,便会产生导致该现象的发生。而这种问题在上板测试时一旦出现想要找到根因还是要颇费功夫的。
回归跨时钟域握手处理
发现问题而不解决无异于耍流氓,StreamCCByToggle的设计机制对于上面的场景无能为力,那么还是采用经典的处理形式进行封装还是比较靠谱。先给出代码:
熟悉跨时钟域处理握手机制的小伙伴对上面的代码并不陌生。当pushArea检测到有任务请求时会拉高taskPending,在popArea侧时钟域同步taskPending为req。检测到req上升沿时popArea发起任务,待任务接收后拉起ack。pushArea侧同步popArea侧ack。检测到ack上升沿后释放taskPending。而在popArea侧检测到req变为低电平后也拉低ack。pushArea侧检测到ack为低电平即可处理新的任务。
写在最后
关于跨时钟域处理在处理上相对来讲还是一个易错点,其处理也是新学者需要好好把握的。SpinalHDL中的源代码还是很值得一读的。一方面,SpinalHDL中的代码质量还是相当之高的,阅读他的源代码,对于自己平时代码设计能够带来一些新的思路。很多人都觉得SpinalHDL难学。一开始我使用SpinalHDL也是按照之前写Verilog的思路来写的,随着对Scala语法的了解渐多(虽然现在和专业玩儿Scala的人员相差十万八千里,但对于我目前的开发工作来讲已经足够使用了),以及对SpinalHDL
lib源代码的阅读理解,从其中逐渐掌握了很多之前写Verilog时不曾想过的威廉希尔官方网站
抽象规则。哪怕有一天老板不让我用SpinalHDL了,而这些设计思想却是一直受用的。
原作者:玉骐
|