前言
总线超时检查机制是系统中必要的模块设计,用于在总线无法返回response时能够及时上报中断。从理论上分析,如果总线发生了诸如挂死或者物理损坏等超时行为,无论计时器上报timeout的时间偏大还是偏小,都是一定可以上报中断的。不过呢在一些敏感的系统中,及时发现问题还是很有必要的,因此这篇文章来聊一下如何设计一个相对平衡的超时计数器。
适用场景为支持outstanding的顺序返回response的总线。
严格计时
如果以最严格的超时检查机制,那么理应对每一个发送出去的req都要进行rsp返回计时:
中断超时的条件也就是:
(time0 > timeout) || (time1 > timeout) || (time2 > timeout) || (time3 > timeout) || ...
任何一个计时器超时了都会上报超时中断,显然这样是最及时最准确的方式。但是这样做的缺点也非常的明显,需要为每一个req组织一个计时器,大概有outstanding * timer_width的寄存器资源消耗以及额外的逻辑开销。
简单计时
如果采用简单的超时检查机制呢,那么我们就只需要知道总线是否还“活着”,因此可以把计时器设置为看门狗的方式:当有rsp未返回时,计数器始终累加,每次有一个rsp返回,则把计数器清零:
timer = rsp ? 0 : outstanding ? timer+1 : timer;
这样做的收益是非常明显的,我们只需要一个计时器就可以了资源上非常的节省,并且在总线确实无法返回时一定能做到超时检测。但是缺点也是非常明显的,即此时真正的上报中断时间可能会远远大于设置的timeout值,极端场景如下图:
第三个返回的rsp实际早已经超时了,但是直到很久后才被检测发现到或者没有被发现。简单计时模式下,设置超时时间阈值为timeout时,极限情况(每个rsp与上一个rsp都相隔timeout拍)超时了需要在timeout*outstanding拍后才会触发中断,而实际上第二个rsp就已经超时了。对于超时检测不敏感的系统,这样的设计没有问题,但是如果需要尽快上报超时中断的例如车载系统,我个人觉得这样设计风险比较大。
平衡时间与资源的计时方式
那么我们的目的就是很明确了,就是寻找一种资源占有小且能够尽快发现总线超时的计时器方案。那么我们来进一步观察顺序放回rsp的总线行为:
如果只有一个计时器,我们希望他记录的是什么值呢?显然记录的应该下一个要回来的rsp距离他对应的req的时间。假设我们只发出了req0和req1,那么显然现在我们需要记录的是rsp0是不是要超时了,如果rsp0超时了那么根本不用管rsp1直接上报中断就可以了。如果摸一个时刻(距离req0发送时间点time0后)rsp0收回了,那么此时计时器应该修改为什么值呢:
timer = time0 - delay0;
即此时距离req1发送的时间点已经过去了(time0-delay0)时间,而rsp1还没有回来,显然计时器应该调整为这个记录值。以此类推,只要回来一个rsp_n那么就将req_n~req_n+1之间的delay减去,此时的timer记录的时间就时钟保持准确,如果发生超时可以第一时间上报。然而这样设计还有一个问题,虽然我们的计时器只有一份,但是我们需要记录每两个req之间的delay大小,资源上仍然没有节省。
因此可以对该方案进行优化,即不记录每一个delay的大小而只记录一个max_delay的值:
max_delay = max(delay0, delay1, delay2, ...);
即每次返回一个rsp则对timer执行减max_delay的操作。显然,这回导致timer减的值偏大,因此实际到达超时的时间仍然会比设定时间长,极限的情况是:
timeout + max_delay*(outstanding-1)
也就是req是连续来的,每次rsp返回时timer都应该减1但是实际减了max_delay,计时器的值相比真正的值少了max_delay*(outstanding-1),自然发生超时情况下上报中断的时间要比预定的timeout时间多max_delay*(outstanding-1)。不过相比于简单计时方案的timeout*outstanding拍才能上报中断,这种方案显然已经足够的快速了。
如果希望进一步的简化设计方案,可以考虑将max_delay替换为cfg_delay,即通过软件配置寄存器配置一个delay值,每次timer都减去这个值即可避免了在rtl中刷新max_delay。但是这样做的虽然保证了在超时发生时必然可以上报中断,也会导致cfg_delay配置过小时误报中断,因此cfg_delay的设置需要一定的经验。