前言
芯片验证是为了发现芯片中的错误而执行的过程,它是一个破坏性的过程。有效激励灌入待测模块后,需要判断出不符合功能描述的行为。检查器(Checker)就是用于查看待测模块是否按照功能描述文档做出期望的行为,识别出所有的设计缺陷。
不同被检查逻辑的层次,所需要的检查方法不一致,本文主要侧重于模块级验证的检查器设计。该层次需要检查的范围有:
- 待测模块的所有输入输出信号;
- 待测模块的内部设计细节;
- 待测模块在芯片系统级的应用角色;
所需要的功能描述文档至少有:
- 项目产品文档:描述本模块的实际应用场景。
- 项目需求文档:将产品文档转换为技术人员可读懂的技术文档。
- 待测模块设计文档:将需求文档转换为实际RTL规范。
有些模块还需要其它手册,比如Arm和RISC-V的CPU还会有架构参考手册。
对待测模块行为的检查,可以采用不同的检查方法,主要有:
- 监视器(monitor):可以用于信号级别的数值、时序和协议检查;
- 参考模型+比较器(Checker):检查包层级的信息;
- 断言:主要依靠它检查模块的逻辑细节和时序信息;
- 定向测试:用于简单场景的检查,一般自带检查器;
- 形式验证:用数学方式来证明功能是否正确;
根据情况不同,以上这些方法可以采用其中的一种或多种,一般很少只使用一种的。待测模块使用什么样的检查方法以及检查哪些内容,也是芯片验证的一大难点。和激励设计分析类似(如何写出更牛更系统的验证激励),本文也是从完备性、可拓展性和可控性三方面展开阐述如何系统地思考和构建验证检查器。
1. 完备性
检查器的完备性是最基本要求,可以从接口类型、内部结构、结束检查(End of check)和检查器审查这四方面去思考。
1.1 接口类型
接口类型检查主要是检查输入输出信号是否符合文档描述的行为,也就是与待测模块连接的上下游模块的互动信号行为。可以从单一接口类型和多个接口交互两方向去分析。
1.1.1 单一接口类型
单一接口类型分析由信号检查和协议检查组成。
- 信号检查:主要检查每根信号的行为,包括信号值正确性和数据完整性。
- 协议检查:主要检查多根信号之间的行为,包括时序检查、握手检查,hazard检查,order检查,outstanding能力检查和包传输流程检查等。
信号值检查、时序检查和握手检查通常可以用断言检查;outstanding能力检查和包传输流程检查通常可以用监控器检查。数据完整性、hazard检查和order检查等复杂建模需求的通常需要使用检查器检查。
1.1.2 多个接口交互
多个接口交互由数据完整性、保序和同步交互组成。
- 数据完整性:检查信息传输没有丢失。
- 保序:检查有先后关系的传输包是否正确传输。
- 同步交互:检查传输包是否有同步或者约定先后出现等关系。
多个接口交互的检查通常需要检查器检查。
1.2 内部结构
内部结构检查需要沿着数据流(data flow)方向分析,看看有哪些必须检查的转换(transformation)和决策(decision)。主要有:
- 内部功能点:order、hazard、forward、sleep、replay逻辑、register读写等。
- 资源占用:FIFO、Buffer、Interface、流水线等。
- 资源仲裁:优先级、死锁等。
- 数据完整性:数据拆分、数据合并、数据保序等。
- 功能配置:待测模块行为是否符合当前寄存器和参数配置等。
- 异常检查:复位值、内部触发fault、外部返回错误响应、中断等。
- 异常事件是否合理;
- 异常处理是否正确;
- 异常处理完,待测模块是否能恢复;
内部结构的检查经常需要用到RTL内部的辅助信号,最好是在非常有必要或可以大大节省检查器建模工作量的情况下,才使用这些辅助信号。而且检查器用辅助信号之前,必须检查辅助信号行为的合理性。
1.3 结束检查
结束检查主要用于在验证用例结束时,检查RTL和TB的状态,比如RTL是否处于非busy状态、FIFO是否为空、请求是否全部处理完毕等。只要有预期某个功能在用例结束时必须处于某种状态,都可以放到结束检查里。主要有:
- TB/DUT状态检查:Counter、FIFO、Buffer、Queue、Busy等。
- 仿真日志检查:可以通过脚本自动提取。
- 波形检查:通过脚本解析波形内容来检查。
1.4检查器审查
审查(Review)环节对完善检查器起着至关重要的作用,可以提高检查器质量,并减少漏检查特性的概率,对个人成长也有很大帮助。
建议以下这些审查方式都要按顺序进行:
- 个人审查:由自己进行,对照历史错误清单进行,举一反三,避免犯类似错误。
- 同组审查:由组内更有经验的人进行,对检查完备性、检查器设计结构、假设、采用的RTL辅助信号、代码实现进行检查。
- 跨组审查:由待测模块的设计人员进行,对检查器完备性、设计顾虑、易错特性、局限性和各种检查器假设进行检查。
2. 可拓展性
可扩展性也包括可复用性。为了处理验证周期缩短、待测RTL规格频繁变动等各种情况,我们需要在设计检查器时提前构思如何让检查器更容易扩展。基于此,我们可以从模块化和解耦性方面去思考。
2.1 模块化
模块化需要对待测模块的每个功能点检查都分别集中于一处,而不是将一个功能点的检查分散到检查器各个地方,且多个功能点检查互相穿插耦合,这样后期如果某个功能点改动会影响检查器的很多代码,而且容易改漏。
把常见的功能封装成公共库,这样在多个地方都可以直接使用,如果功能需要改动,只需要改动一处代码即可。
不要把所有检查器内容都放到一个class类里解决,这样会造成这个类代码巨多,而且难以维护。可以把检查器根据待测模块特性切割成多个小的建模单位,由这些小的建模单位共同组成整体检查器,这样结构更清晰,而且待测模块特性的改动,只会影响其中一些建模单位,不需要整体检查器都修改。
2.2 解耦性
解耦性要求检查器代码尽量减少依赖于环境其它组件的代码,不管是变量类型、方法还是类,最好使用自身定义的,方便修改、移植和重用。
比如monitor送过来的类内容要转换为检查器自己内部定义的类内容,检查器就可以减少依赖于monitor中类的改动。
3. 可控性
检查器的可控性在这里主要是指是否方便控制检查器的功能检查范围和消息打印内容。
3.1 功能检查控制
最常见的就是控制检查器是否开启,比如在调试激励的时候,通常会先把检查器关闭掉,避免造成不必要的干扰。
如果需要的话,功能检查控制可以做到更细粒度的控制,按照待测模块的规格、接口或某个功能来控制,这样就方便根据实际情况,选择打开或关闭哪些检查。
3.2 消息等级控制
检查器中会打印一些消息,跟踪检查器的内部情况,方便出错时调试。有效的打印消息越多,仿真速度越慢,但对调试来说更友好些。因此,需要控制好不同消息的打印等级,不要每个周期都打印大量信息。默认情况下,只打印一些关键信息。只有需要更多调试消息时,才调低等级,打印更多的消息。
总结
综上所述,检查器设计可以按照“完备性->可拓展性->可控性”方向去分析。在完备性中,可以按照“接口类型->内部结构->结束检查->检查器审查”方向去分析。在可拓展性中,可以按照“模块化->解耦性”方向去分析。在可控性中,可以按照“功能检查控制->消息等级控制”方向去分析。
另外很重要的一点是:要经常对验证结果进行复盘分析,并增强检查器。
总得思维导图如下: