4、事务层故障注入:TLP格式错误、CRC校验错误、标签冲突、重放超时
事务层,说白了就是PCIe协议栈里最靠近软件的那一层。我们平时说的读写请求、完成报文,全是在这一层打包成TLP(事务层包)发出去的。做故障注入时,这一层也是最容易出幺蛾子的地方。
我个人习惯把事务层的故障分成四类:格式错误、CRC校验错误、标签冲突、重放超时。这四类问题,我在项目里都踩过坑,今天一个一个讲清楚。
4.1 TLP格式错误注入
TLP格式错误,说白了就是包的结构不对。比如头部的Fmt字段和Type字段不匹配,或者长度字段算错了。这种错误在正常链路里很少见,但一旦出现,接收端会直接丢弃这个包。
我遇到过最典型的一个案例:某次在做Switch压力测试时,发现偶尔会有TLP被丢弃。查了半天,发现是某个DMA引擎在生成TLP时,把长度字段写成了0。嗯,这种低级错误,在真实芯片里还真会出现。
TLP格式错误注入的常见手法:
- 修改Fmt字段(2'b00 → 2'b01,比如把3DW无数据改成3DW有数据)
- 修改Type字段(比如把Memory Read改成Memory Write)
- 篡改Length字段(比如实际有128字节,但Length写的是0)
- 破坏TD位(如果TD=1但后面没有TAG Digest)
你想想看,如果接收端收到一个格式错误的TLP,它会怎么处理?按照PCIe规范,接收端会直接丢弃这个包,并且不会发送任何Nak。这就是所谓的「静默丢弃」。所以做测试时,你得通过其他手段(比如统计计数器)来确认错误是否被正确检测到了。
我的经验:做TLP格式错误注入时,建议从最简单的错误开始——比如只改一个bit的Fmt字段。这样容易定位问题。我曾经一上来就搞了个复杂的格式错误,结果Switch直接挂死了,调试了三天才发现是格式错误触发了某个未处理的异常状态。
4.2 CRC校验错误注入
CRC校验,这是TLP的最后一层保护。每个TLP尾部都有一个32位的CRC(ECRC或LCRC)。接收端收到包后,会重新计算CRC,跟包里的CRC比对。如果不一致,就说明数据在传输过程中被破坏了。
CRC校验错误注入,说白了就是故意把CRC算错。你可以直接翻转CRC字段里的某几个bit,或者干脆把CRC字段清零。
注意:CRC校验错误和格式错误不同。格式错误是「包的结构不对」,CRC错误是「包的内容被篡改了」。接收端对这两种错误的处理方式也不一样:格式错误直接丢弃,CRC错误会触发重传机制(如果启用了ACK/NAK协议)。
我记得有一次做系统鲁棒性测试,我们往链路上注入了大量的CRC错误。结果发现,Switch的重传缓冲区很快就满了,然后整个链路就卡死了。后来分析发现,是重传超时时间设置得太短,导致重传请求和原始数据包在链路上打架。
做CRC错误注入时,我建议你关注以下几个点:
- 错误率控制:别一上来就100%错误率,先从0.1%开始,慢慢往上加
- 错误位置:可以只改CRC字段,也可以改数据字段(让接收端算出来的CRC不匹配)
- 错误模式:单bit翻转、多bit翻转、burst错误,效果都不一样
4.3 标签冲突注入
标签(Tag)是TLP里的一个关键字段。每个发出的Non-Posted请求(比如Memory Read)都会带一个唯一的Tag。当完成报文(Completion)回来时,通过Tag来匹配是哪个请求的响应。
标签冲突,说白了就是两个不同的请求用了同一个Tag。这会导致完成报文无法正确匹配,系统行为就乱套了。
为什么会发生标签冲突?正常情况下,请求者会维护一个Tag池,每次发请求时从池里取一个空闲的Tag。但如果软件有bug,或者硬件状态机出错了,就可能重复使用同一个Tag。
标签冲突注入的典型场景:
- 让同一个Function连续发出两个相同Tag的Memory Read请求
- 让不同Function使用相同的Tag(如果Switch不做Tag隔离)
- 在重传过程中,修改TLP的Tag字段
我遇到过最头疼的一个问题:某款Switch芯片在处理标签冲突时,竟然会把两个完成报文都转发给请求者。结果请求者收到两个相同Tag的Completion,直接懵了——它不知道该用哪个。后来查规范才发现,PCIe规范对标签冲突的处理是「未定义行为」,也就是说,芯片厂商可以自己决定怎么处理。
避坑指南:做标签冲突测试时,一定要确认Switch的行为是否符合预期。我曾经遇到过一款Switch,在标签冲突时直接丢弃了所有后续的Completion,导致系统完全卡死。这种问题在规格书里是查不到的,只能靠实测。
4.4 重放超时注入
重放超时,这是数据链路层的事情,但跟事务层密切相关。简单说,就是发送端发出一个TLP后,在规定时间内没收到接收端的确认(ACK),就会重发这个TLP。
重放超时注入,就是故意让接收端不发送ACK,或者延迟发送ACK,让发送端误以为包丢了,从而触发重传。
我常用的手法有两种:
- 延迟ACK:把ACK延迟到重放超时时间之后才发送
- 丢弃ACK:直接丢掉接收端发出的ACK,让发送端一直等
你想想看,如果重放超时设置得太短,会发生什么?发送端会频繁重传,链路利用率直线下降。如果设置得太长,一旦真的丢包了,恢复时间又太长。所以重放超时时间的设置,是个典型的trade-off。
重要提醒:重放超时注入可能会触发链路训练。如果连续多次重传失败,PCIe链路会进入恢复状态(Recovery),甚至重新训练。做测试时要注意,别把链路搞崩了。
我记得有一次做压力测试,我们往链路上注入了大量的重放超时。结果发现,Switch的重放缓冲区(Replay Buffer)很快就满了。因为每个重传的TLP都要占用缓冲区,直到收到ACK才能释放。如果重传太多,缓冲区溢出,就会导致新的TLP无法发送。嗯,这就是所谓的「死锁」场景。
知识体系总览
下面这张图,是我自己总结的事务层故障注入的知识体系。你可以把它当作一个checklist,做测试时对照着来。
这张图把四个故障类型和它们的典型表现、处理方式都列出来了。做测试时,我建议你按照这个顺序来:先做格式错误,再做CRC错误,然后是标签冲突,最后是重放超时。因为前两种相对简单,后两种涉及的状态机更复杂。
最后说一句:事务层的故障注入,说白了就是「故意搞破坏」,看系统能不能扛得住。但搞破坏也要有章法,别一上来就搞大动作。我个人的习惯是:先做单点故障,再做组合故障;先做低频率,再做高频率。这样出了问题也好定位。
公众号:蓝海资料掘金营,微信deep3321