下冰雹 · 2023年12月18日

【芯片设计】总线超时模块的一些方案思考

监控总线超时是一个总线交互模块的常见操作。总线超时行为,是对于master端一笔发出去的请求,slave端返回的数据无法在规定的时间内到达。那么我们进行超时检测的本质原因是什么呢?是在总线挂死或者其他模块及存储系统出现致命问题时,能够及时的上报异常,进行后续的处理。或者说,在绝大部分的需求场景下,timeout的本质是外部已经无法返回rsp了,而不是rsp返回的晚了。因此,除了在有严格响应时间要求的场景下,系统的超时水线(timeout_th)设置大了些,系统记录时间有偏差,其实都不会影响最终外部挂死导致无法返回rsp的上报。

image.png

如果我们想尽量精确严格的计算timeout,那么最简单来说可以对每一个发出去的请求都进行检测,如果master的outstanding为32,那么也就是对最多32笔数据进行时间检测。当(time0 > timeout_th) || (time1 > timeout_th) || … || (time31 > timeout_th)时,判定为超时上报异常,这样每一笔请求的异常都被检测到了。这种方式也能避免计数器溢出套圈的情况,毕竟每笔请求的timer都是从0计数到达timeout_th就可以上报异常了。

这样虽然很准确,但是需要32个(outstanding=32)寄存器来实现,且资源消耗与outstanding成正比。

image.png
那么我们尝试沿着不同的方向来简化超时检测机制。

既然32个寄存器资源比较大,那咱们换成一个计数器不就好了。那怎么实现对每个请求的检测呢?给每个发出去的请求记录个时间戳就可以啦,时间戳用什么实现呢?用ram搭个entry呗,以req_id(0~31)作为作为索引,记录每个请求的发送时间。当对应id的rsp返回时,从ram中以id为索引查询发送时间戳与当前timer记录的时间进行比对,超过timeout_th则上报异常。

image.png

沿着这个思路我们可以进一步的优化下,假设timer为16bit。在某系统的实际场景中,需要检测的至少是256拍以上才视为超时,那么记录时间戳时低8bit的数值实际上是没有很大参考意义的。因此ram的位宽可以使用8bit而不需要完整的使用16bit。

甚至有些系统的time_entry设计的可能会更加激进,比如说如果我们预期极少有rsp会在1024拍以上返回,一旦出现这种情况即可认为超时,那么time_entry只需要存储{timer[11], time[10]}即可。如果req_record_time = 2’b00,而rsp_time = 2’b10或者2’b11则可以认定二者至少相差1024拍以上,因为req_record_time极大值为12’b0011_1111_1111(1023),rsp_time极小值为12’b1000_0000_0000(2048)。也就是说time_entry可以始终保持2bit的设计规格,根据timeout的数量级将timer对应的2bit记录在entry内,与rsp返回时的timer值进行比较,可以检查该数量级内的超时问题。

但是除了前面提到的timer溢出套圈的问题需要兼顾之外,这种设计本身还有一个很大缺陷,就是如果rsp真的挂死不返回了,存在不能上报的风险。因为上面的超时上报机制必须在rsp返回时才能触发。因此一般需要其他的机制共同检查挂死不返回rsp的问题,比如在指令和任务层面的超时上报机制。

那么如果我们换个方向,我防的就是挂死,只要总线挂死了能报异常上来就行,不要求第一时间非常的准确,是不是也有优化角度呢?

当然是有的,比如一种最简单的timeout机制:所有的req共享同一个timer,只要有req未回收那么timer就一直递增,但是只要有rsp返回就将timer清零。这种设计会造成很大的统计误差,比如一种极端场景请见下图:

image.png

在我们设置的超时水线下,按理说在timeout点红色的rsp已经超时了,但是因为蓝色rsp回来时将timer刷新为0,因此没有上报异常中断。之后红色rsp、绿色rsp连续在边界时间点刷新timer,直到黄色rsp时才检测出timeout上报异常中断。因此这种方案,对于timeout_th的超时水线,极限场景需要outstanding * timeout_th的时间才会发现超时。时间虽然长了点,但就像前文所说,timeout的本质是外部已经无法返回rsp,那么这种设计确实“又不是不能用”。

不过如果同样的资源(一个timer)或者相近的资源,有没有办法做到更快的上报呢,毕竟在有些场景下上报时间也是有要求的。如果是rsp是顺序返回的系统设计,那么确实还有一些办法。我们进一步的分析一下时序:

image.png

在第一个蓝色rsp0返回前,总线上最长时间未返回的rsp time=time0,而当rsp0返回了之后,总线上最长未返回的rsp time=time0(即rsp time)-delay0,delay0是req1和req0发送的时间差。而在红色的rsp1返回后,总线上最长未返回的rsp time=rsp time-delay1,delay1是req2和req1发送的时间差。以此类推,每返回一个rsp就将对应的delay_n减去,此时timer记录的就是准确的最大总线延迟时间。

如果采用这个方案,那么我们就需要记录每一个delay的值,那么也就意味着仍然有outstanding个额外的寄存器用于记录。进一步的简化方案,我们也不一定需要准确的减去每一个delay,减去一个最大的delay就好了。令:

max_delay = max(delay0, delay1, …)

通过一个寄存器记录max_delay,每次返回一个rsp就从timer中减去max_delay(当然,rsp都返回完了,max_delay和timer都要清零的!),那么就能够比较准确找到超时的时间点。但是这样一定是有误差的,原因在于max_delay >= delay_n,因此timer每次都可能会减多了,累积到timeout_th的时间仍旧会比真实的超时时间点要长。极端情况下检测到超时的时长是:timeout_th+max_delay * (oustanding-1),即某一段的req是连续往外发送但是rsp回来时每次timer应该是减1但是实际都减了max_delay。当然,这种情况已经比上面的方案要精确很多了!

再进一步简化,如果能够预期在一个任务中req之间的最大delay值或取值范围,可以将max_delay作为一个配置项cfg_max_delay,这样两个计数器timer+max_delay就简化为一个了,资源消耗和上面的方案一致了。

还有一个小的知识点,对于总线超时不能只检查发出去的req收到的rsp是不是超时了,还要检测req始终没有办法发出去的场景,比如说握手接口,如果总线挂死了axi_awready始终无法为1接收请求,这种场景也是我们需要考虑的。

作者:尼德兰的喵
文章来源:芯时代青年

推荐阅读

更多IC设计干货请关注IC设计专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
19438
内容数
1300
主要交流IC以及SoC设计流程相关的技术和知识
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息