目录
本章首先讨论状态空间及其与设计复杂性的关系,然后讨论黑盒、切点、驱动常数和分解属性,最后介绍一些可以提高FV运行能力的技术。
本章聚焦使用形式属性验证(FPV)的示例,但大多数也适用于其他形式的FV。无论在FPV、FEV或更具体的基于FPV的流程中遇到复杂性问题,都应该考虑将这些方法应用到设计中。
一、设计状态和相关复杂性
设计状态由特定时刻每个输入和状态元素的值组成,状态元素包括触发器、锁存器和内存元素。如果是n态元素,理论上有个可能的配置。这些状态元素与输入(m)及其组合(
)数量将构成可构思的完整设计状态。一个设计中,此类可信状态的总数为
,在一个10个输入和10个触发器的普通设计中,可能的状态数为
或1048576个。
重置后的设计将处于“重置状态”,通常只有一小部分状态元素通过重置进行初始化,其余元素不受约束,并通过FV工具进行处理,它们可以假设任何成任意值,只要不违反假设。有效重置状态的数量通常不止一个,并且在状态空间中形成一个小区域,而不是一个唯一的状态,FV有效地并行分析所有重置状态。
RTL设计的实际操作或模拟,可以被视为从复位开始穿过状态空间的曲折路径。设计运行时,会在随后的每个时钟周期中从重置状态转换到其他状态,将状态空间中的一个点遍历到另一个点,形成一条轨迹。模拟或仿真工具一次检查一条轨迹,而形式工具并行分析所有此类轨迹。
图10.1显示了状态空间图。设计将从{R}状态开始遍历,并遍历下一组状态,直到到达最终状态。典型的模拟测试将一次探索一条轨迹,FV将探索每个时钟周期中所有可能的状态,如图中不断增长的一系列椭圆所示。
如果FV工具达到完整的证明,有效地探索了设计达到的所有可能状态。如果它做了有界证明,与模拟相比仍然覆盖了一个非常大的空间区域;图10.1中的外圆表示最外层椭圆所示界限的证明,该椭圆表示特定的循环数。它保证在该范围内不可能出现故障,但设计可能会在该范围外出现故障。由于逻辑的复杂性不同,图中的所有区域都不相等:根据所检查的属性,深度的小增量会非常重要。例如,一个设计花费256个周期等待启动计数器完成,然后在计数完成后开始一个非常复杂的操作,分析前256个周期中的每个周期很容易,而计数器完成后的周期可能会暴露设计的完整逻辑。
指数增长的状态空间是FV复杂性问题的主要来源。上述分析随时间变化的行为的FV类型有关:FPV或序列FEV。因为组合FEV在状态元素上分解模型,允许通过形式化工具进行更简单的分析:只需要理解描述单个周期中每个锁存器、触发器和输出的下一个状态的布尔逻辑。即使分析单个状态的正确性也是一个NP完全问题(相当于SAT问题),因此在组合FEV中,复杂的RTL逻辑也可能导致复杂性激增。
二、内存控制器
内存控制器接口基本结构如图10.2所示:
这里的块包含两个内存协议引擎(MPE)和两个内存读/写仲裁器(MRA)。协议引擎和仲裁器紧密连接、协同工作,从各种内存模块访问内存位置,并为一系列请求者提供服务。
内存仲裁器在几个竞争的请求者之间进行选择,使用以下优先级方案发送内存地址和请求:
Refresh > Read > Write
MRA的另一个重要功能是检查,并避免管道中的读取可能返回的值早于最近的写入。此检查需要进行地址比较操作,控制器确保内存访问功能没有危险,并保证正确的数据存储和检索。
MPE包含各种决策有限状态机(decision finite state Machine,FSM),实现大量的深度队列来存储飞行中的内容并执行一些即时操作。MPE强制遵守协议并维持许多数据结构以确保一致性。
该控制器难以收敛的一些特性包括:
- 对所有读写请求进行公平仲裁
- 通过控制器进行可靠的数据传输
- 在所有请求程序中保留优先级
- 符合协议要求
- 避免 RAW 危险
- 最终每个请求都准予
三、观察复杂性问题
FV中的复杂性问题表现为:
- 状态激增导致内存膨胀,导致资源不足。有时还会导致系统崩溃。
- 不确定的运行时间——当工具在一组定义的资源内解决问题时,会不断探索更多的状态,而且永远不会在规定的时间内完成。
- 有界证明的下界——如果有界合理,那么有界证明通常可以的,但是不能满足覆盖点要求或不能执行有趣逻辑的下界不可以。
- 工具崩溃——当系统容量因上述任何原因而受到压力时,工具无法清晰地报告会崩溃。
开始FV验证前,先评估设计的复杂性。设计本身太大而无法用典型的FV工具来处理;需要关注较低的级别,或者寻找黑盒处理机会。该设计可能还包含许多宽而深的内存元素、大量计数器,或与工具集不配合的逻辑样式(如浮点算术)。
第一个一般估计值可以从被测模块中的触发器数量中获得,更精确的是评估每个断言逻辑锥中的失败次数,这些信息可以通过FPV工具报告。由于编码的约束,为了证明断言,逻辑锥可能还会添加一些失败。随着状态元素数量的增加,可能需要花费更多的时间来处理复杂性。
典型FPV工具中使用的模型检查引擎正日益智能化,几年前被认为不适合通过FV实现融合的设计很容易通过最新引擎实现。虽然引擎进一步增强了收敛能力,但设计的复杂性也在不断增加。
建议最初将有界证明作为目标,而不是完全证明(full proofs),因为获得这些证明的成本通常要低得多。在处理复杂性之前,因为证明是有界而不是满的,所以一定要考虑有界的证明是否对当前的问题足够好。
四、简单的收敛技巧
1、选择正确的战斗
为形式证明开发选择设计层次结构级别时,需要确定一组正确的逻辑,其中工具可能会收敛,但分析区域足够大,可以包含一组有趣的证明,而不需要在接口上大量开发假设。
再看看上述内存控制器示例。把内存接口块中状态元素的数量作为一个整体来看时,可能会发现逻辑量太大,黑盒嵌入内存之后,大约有100K个状态元素,不太可能在这些属性上完全收敛。这种情况最好分别验证MPE和MRA块,因为每个子模块都有两个实例(除了实例名之外完全相同),实际上只需要在每个FPV运行中验证1/4的逻辑。
还需要考虑属性逻辑的位置问题,大多数FPV工具都有智能引擎,它们试图最小化在每个断言证明中使用的逻辑量,只引入影响该断言的相关逻辑。即使完整模型可能有100K触发器,如果期望每个断言证明几乎都是MPE或MRA子模块之一的局部证明,那么不会为加载完整模型付出太多的复杂性代价。
在较低级模型上运行时需要谨慎:最初可能会低估需要完成的假设编写的级别。在最初的摆动(wiggling)阶段,寻找反例和发展额外的假设,如果发现假设变得太大或笨重,可能要考虑放弃验证子单元的方法。然后将结合所有四个主要子模块的逻辑来验证整个单元,并准备研究其他降低复杂性的技术。
2、引擎调整
在尝试任何更复杂的技术之前,确保已充分利用了FV工具提供的引擎功能。大多数FV工具都包含多个引擎,实现不同的启发式算法来解决FV问题,这些引擎通常会针对特定的设计结构进行优化,选择正确的引擎集来解决需要验证的问题。有些引擎擅长bug搜索,处于早期设计或验证阶段,并且预计会出现许多性能故障时,它们是最佳选择;另一些优化了安全特性,一些优化了聚合活性属性。一些引擎迭代地向分析区域添加逻辑来实现收敛,而另一些引擎则在状态空间中深入搜索以达到坚实的覆盖点。
在许多情况下,个别发动机提供特定的“旋钮”,即更直接地控制其行为的附加可选参数。寻找以下机会来更细粒度地控制引擎行为:
- Start:可以控制引擎在多深的状态空间中开始搜索反例。例如,当64个深度的队列已满时,怀疑有错误则可以告诉引擎在深度64开始搜索。如果在短时间内找不到,会影响性能;还必须仔细检查此选项是否会导致引擎假定没有早期故障,在没有故障的情况下,证明在逻辑上可能不完整。
- Step:对于逐渐深入验证空间的引擎,可以告诉它们每n个周期检查一次故障,而不是在每个可能的深度检查一次。这在预计会出现故障时更为合适,因为根据工具,它可能会导致一些故障被忽略;如果它导致发动机跳过容易出现的故障,则会影响性能。
- Waypoints:一些引擎也被称为“半形式化”引擎,试图从在设计空间深处寻找有趣的个人痕迹开始,然后从那里探索整个空间。在这样的引擎中,指定一个waypoint 或有趣的覆盖点,可以帮助指导搜索。这项技术实际上可以手动实现,一些FPV工具的引擎中内置了轻量级的半形式化功能。工具包含一些非半形式的FPV引擎,可以以巧妙的方式使用waypoint 信息来指导搜索过程。
3、黑盒(blackboxing)
黑盒,或为FV目标标记要忽略的子模块,是降低设计复杂性的主要技术之一。当一个模块是黑盒时,黑盒的输出被视为设计的主要输入:它们可以在任何时候接受任何值,因为它们的逻辑没有建模。在FV工作开始时,应该考虑使用黑盒。
这种技术可以永远不会产生假阳性(除了黑盒本身缺乏验证的情况),因为黑盒模块使其输出更加通用:FV工具可能会产生额外的假阴性(false negatives)、虚假反例,用户必须调试并使用这些反例来提出新的假设;如果黑盒模块的某些操作可能导致其失败,则永远不会报告通过的断言。黑盒大部分逻辑是安全的,如果出现太多不正确的反例,则会取消黑盒。
许多常见的逻辑类型,总是为FV目标而设置为黑盒,因为它们要么包含普通FV工具不适合的逻辑类型,要么在其他地方被验证。例如,缓存和内存由于其巨大的状态空间而很难分析,数据存储和检索的功能通常不是很有趣,有其他方法可以使用专门的形式化或模拟工具来证明内存元素和缓存的功能正确性。
在计划运行FV工具的一个单元中看到以下内容,强烈建议黑盒:
- Memories and caches(内存和高速缓存器)
- 复杂的算术或数据转换块,如乘法器、除法器、复杂函数或浮点逻辑
- 模拟电路(Analog circuitry)
- 由外部提供的(预验证)IP
- 在较低级别进行FVE的已知区块
- 与FV关注点无关的特殊功能(电源、扫描等)
4、参数和尺寸缩减
在模型中尽可能减少大型数据路径或大型结构大小,例如,如果MRA包含一个N位宽的数据路径,其中每个位使用基本相同的逻辑,通常可以将数据路径的全宽简化为几个位、甚至一个位,FV最终证明适用于总线的所有位。
为了更好地启用这种方法,验证者应该与设计团队合作,确保RTL参数化良好。如果MRA的每个子模块都有一个从下一个更高级别继承的DATA_WIDTH参数,那么在MRA FPV环境中进行这种尺寸缩减可能是一种单行更改;如果有十几个子模块,每个子模块都将N (hardcoded)硬编码为某个数字,如果数字比较大这种编辑会比较难受。
一个块可能有多个实例,但只在一个实例处于活跃状态下运行验证
5、案例分解
当有很大一部分逻辑单独运行,并试图验证一个非常通用的属性时,通常会同时有效地验证多个子机(submachine),这可以为影响的锥增加很多逻辑。
一定要检查设计的关注集(care set,验证时关心的输入条件集以进行验证),并确定案例拆分的机会,添加临时约束,将每次验证尝试限制为设计逻辑的一个子集。
表10.1描述了怎样在例子关注集上进行案例分解。将关注集分解为多个切片,由工具进行有效处理。
这些情况可能是对称的,或者一些情况可能包含比其他情况更多的数据:在表10.1中的示例中,验证器怀疑边界操作码值8'h80有特殊问题,因此将这种情况分离出来,而其他情况覆盖的范围更广,还可能需要处理特殊模式;此表中的拆分也有一种扫描模式,即处理所有操作码值(该模式)。对于每种情况,都需要添加假设,将相关输入设置为限制值或常量,这仅适用于所选的情况。
保证已经证明了所有案例的总和,涵盖了设计的所有可能操作。否则可能会在拆分案例时忽略部分设计空间。与黑盒等技术不同,案例分解可能不安全并导致误报,因为引入的假设限制了每个案例的验证空间。如果验证的目标是完全证明FPV或FEV,则需要一个强有力的论据或形式证据,证明案例总数涵盖了设计的所有活动。
需要考虑不同的case(案例)是否独立,或者是否相互影响。例如,在内存控制器中,虽然能够创建独立的证据,证明在没有外部活动的情况下,MRA0和MRA1上的单个内存读取操作是正确的,但我们是否需要担心两个MRA同时或连续快速动作,并且它们的操作可能相互干扰的情况?如果是这样的话,在完成分裂案例的初始证明之后,仍然需要在没有案例限制的完整模型上运行证明。
6、属性简化
以更简单的形式重写断言,或者将复杂的断言拆分为多个较小的断言,可以使断言更简单,便于形式工具进行分析。
1)布尔简化
如果一个属性在蕴涵(断言语法,|->、|=>)的左边有一个OR,右边有一个AND,则这个属性可以被分割成多个较小的属性;也可以基于第2章语法分解。更小、更简单的属性可以很好地被FPV处理,并且易于收敛,更易于调试。
这种转换最简单的例子是:
p1: assert property ((a || b) |-> c);
p2: assert property (d |-> (e && f));
//转换成:
p1_1: assert property (a |-> c);
p1_2: assert property (b |-> c);
p2_1: assert property (d |-> e);
p2_2: assert property (d |-> f);
序列通常表示为一个隐式AND或OR操作。例如:
p3: assert property (A ##1 B |-> C ##4 D);
//转换成:
p3_1: assert property (A ##1 B |-> C);
p4_1: assert property (A ##1 B |-> ##4 D);
2)使活性(liveness)属性有限
另一种类型的属性简化涉及活性属性:描述可能无限延伸到未来的行为的断言。一般形式工具很难完全分析这些问题,因此有时限制活性的无限性有助于收敛。新定义的极限应该有足够大的数值来模拟一个看起来活跃条件的界限。
例如:
will_respond: assert property {req |-> s_eventually(rsp)}
//替换为:
will_respond50: assert property { req |-> ##[1:50] rsp }
假设有一个逻辑深度远远小于50的模型,以及一组很好的覆盖点,表明模型的一组合理操作可能会在该时间限制内发生。一些模型检查引擎在定义的范围内工作得更好,可以更快地收敛。
如果在设计中存在可能阻止响应的背压信号,需要确保信号在50个周期内发生变化:
bp_fair50: assume property (req|-> ##[1:50] !backpressure);
限制潜在无限时间假设的界限有助于验证设计特性,例如,上述bp_fair50假设对于更一般的活性假设,是一个合适的有界替换,如:
bp_fair: assume property (req|-> s_eventually(!backpressure));
使用bp_fair50而不是bp_fair,对假设设定一个有限界限,也有助于在形式分析中达到有趣的状态。然而,这种简化在技术上是一种过度约束,因此可能存在潜在危险。建议尽可能宽松地约束被测设计(DUT)。消除这些延迟约束,使得引擎有更高的自由来探索更深的状态空间;如果您使用这些技术来解决收敛( convergence issues)问题,建议额外审查,完成第一轮FV后考虑释放它们。
7、切点
切点是模型中的一个位置,在这个位置可以将一个节点从其输入逻辑锥中“切掉”,让它显示任何值。当一个节点被切掉时,FV工具可以从形式分析中删除该节点的输入逻辑,这是一个类似于黑盒的概念:把黑盒想象成是子模块的每个输出都成为一个切点的情况。切点是一种更细粒(fine-grained)的技术,针对设计中的单个节点,FEV使用的关键点也可以被认为是一种切点。
应用切点始终是逻辑上安全的转换,可能会引入假阴性(false positive),但决不会冒假阴性的风险。切点增加了合法行为的空间,因为切点的fanin逻辑被视为返回一个通用的任意值,通常有利于降低剩余逻辑的复杂性。如果删除的逻辑与证明无关,那么在添加切点后,会看到正确的结果;如果验证时犯了错误且删除了相关逻辑,那么在调试假阴性时,会影响结果的切点值。可能需要添加相关的假设来模拟缺失的逻辑,或者选择不同的切点。
在内存控制器示例中,MRA仲裁器检查RAW危险,并因此执行某些操作。假设计算出是否有RAW 危险的逻辑(可能是从以前成功的项目中重复使用的),并且不担心验证这个计算。检查属性的目的是验证当有RAW危险时,读取是否适当延迟;就本次检查而言,执行的操作中是否存在真正的RAW危险并不重要。因此可以安全地在RAW计算的输出处放置一个切点,如图10.3所示。然后,RAW位是一个自由变量,可以取任何值进行验证。
此时被检查的逻辑的复杂性大大降低,因为验证时跳过了计算是否存在RAW危险的所有逻辑。由于RAW比特可以自由获取任何值,验证时可能会遇到假阴性,必须在比特上添加一些假设,使其变得现实。例如,在非读(nonread)操作期间标记RAW危险的情况可能会导致奇怪的行为。
如果难以识别难解属性的切点,一种有用的方法是生成变量文件。这个文件列出了fanin 锥中的所有RTL信号,FPV工具可以直接生成或通过脚本获得。有数百个变量,但关键是寻找一组信号名称,这些名称表明逻辑中不相关的部分导致属性fanin看起来很复杂。例如,可能会发现大量名称中带有“scan”的节点,这表明其中一个节点可能是删除扫描相关逻辑的良好切入点。
在组合FEV中,组合方法:将所有状态元素预定义为切点,以优化比较。为组合FEV添加额外的非状态切点,或为序列FEV添加状态或非状态切点,可以提高性能。如果要删除关键逻辑,可能需要通过添加假设进行补偿。
在某些情况下,可以通过穷举(brute-force)法运行脚本来找到FEV切点,两个模型中都具有相似名称或fanin锥难以证明的关键点的逻辑中寻找中间节点,并尝试证明中间节点是等价的。如果证明成功,这个节点可能是一个很好的切入点,但这可能很费时。
1)带孔的黑盒
切点的一个具体应用:创建一个带有“hole”的黑盒,即一个子模块几乎完全是黑盒,但逻辑的一小部分仍然暴露在FV引擎中。当某些全局功能(如时钟或电源)通过一个模块进行路由时,通常需要带有裂缝的黑盒,否则FV会被黑盒屏蔽,如图10.4所示:
在图10.4中,希望在顶层运行FPV时黑盒化左侧的块,由于依赖黑盒内部的一些时钟逻辑,导致证明在右侧的目标块中失败。为解决这个问题,除了想要保留的输出,可以编写一个脚本,给模块的所有输出添加切点,而不是将模块声明为黑盒。在上述示例中,希望保留输出ck的输入逻辑,脚本找到并剪切其他模块输出(点a、b、c);利用这个脚本,有效创建一个带孔的黑盒,以保留所需的一部分逻辑。
8、半形式验证
在上面关于引擎参数的部分中,介绍了“半形式”验证的概念。假设在寻找bug的FPV环境中,工具在试图到达一个深度覆盖点时卡住了。例如,运行工具一周,在证明中绑定1000个,但发现有一个覆盖点仍然没有达到,在内存控制器示例中,假设有一个刷新计数器,它每或
个周期启动一个特殊操作。由于状态空间的指数增长,工具可能永远无法达到这样的深度。在刷新条件达到时检查可能发生的潜在错误。
解决上述问题的一个解决方案是考虑半形式验证,不是从真正的重置状态开始,而是从提供工具的执行跟踪示例开始。大多数FPV工具允许从模拟中导入重置序列,因此可以只获取一个运行9000个周期并接近刷新点的模拟跟踪。或者可以从FPV工具中获取有效的覆盖跟踪(例如500个周期),将“waypoint”作为重置序列输入,并反复重复该过程,直到建立了一系列覆盖9000个周期执行的覆盖跟踪。一旦到达了接近潜在错误源的waypoint,则让形式引擎彻底地探索断言失败,使用该waypoint作为重置点来开始探索。
从一个特定的深度跟踪开始的,而不是真正的通用重置状态,因此此方法仅用于错误搜索,而不是完全证明(full proof)FPV,许多有效的执行路径都未被检查。这是发现由深层设计状态引发的问题的好方法,因为仍可以从一个特定的点上彻底探索设计空间,从而获得FPV的独特优势。该方法如图10.5所示:
在图10.5中,每个星星表示一个中间waypoint,用户反复输入工具的覆盖点,红色路径标记是FPV伪重置生成的执行轨迹。黑色的圈表示FPV工具对断言失败进行彻底搜索的子空间,将找到错误情况ERR(比工具默认能够探索的深度要深得多)。然而另一个错误条件ERR2只能通过与用户关注的执行路径,非常不同的执行路径来访问,因此该方法不会发现ERR2。
虽然半形式验证没有完全证明FPV的完全穷举搜索,但仍然是一种非常强大的验证方法:在状态空间的一个小区域中运行指数数量的随机测试。如果验证时无法用当前的证明界限,来执行的条件触发的深度错误(deep bug),则考虑使用此方法。
现在许多FPV工具为半形式验证提供了一定程度的自动化,工具允许直接指定一系列waypoint,而无需手动提取每条轨迹并将其转换为重置序列。
9、增量FEV
假设已成功在一个复杂的设计上运行了全验证(full proof)FPV,并且一直在其上运行回归,但回归非常耗时。能够使用先进的复杂度技术成功地验证一个单元,但FPV最终需要几周时间才能运行,这是因为它的复杂性很高,并且有大量的案例被拆分。
已经更改了具有难解或复杂的FPV运行的设计时,应该考虑做增量RTL-RTL FEV 运行,而不是重新运行FPV回归。当新功能关闭时,只需验证新设计是否与旧设计匹配。如果旧设计没有问题,则只需要编写并证明新添加特性的断言,这比在整个模型上重新运行full proof 集所花费的时间和精力少。
五、辅助假设
当遇到复杂问题时,应该考虑辅助假设(helper assumptions):排除问题空间中很大一部分的假设,希望减少复杂性并使模型收敛,如图10.6所示:
在图10.6中,辅助假设排除了属性影响锥的黄色部分,减少了必须由形式引擎分析的逻辑。
1、编写自定义助手假设
在前几章中创建的一些初始假设,例如限制ALU的操作码或将扫描信号设置为0,这是辅助假设的简单示例。
在内存控制器示例中,假设在RAW 危险计算中添加了切点,只有在读取操作进行时,RAW 危险在逻辑上是真实的,但一旦达到了临界点,就没有任何条件强制执行该条件,FPV引擎可能在分析非读取操作触发RAW 危险时浪费大量时间。因此,假设RAW 危险只发生在读取操作上是一个有用的辅助假设。
如果正面临一个复杂的问题,应该考虑设计的行为空间,并尝试寻找机会来创建辅助假设,排除大量不相关的状态。
2、利用经验证的断言
如果已经证明了一系列断言,但只有一小部分未能收敛,这可以利用辅助假设完成验证。大多数现代FPV工具都可以将断言转换为假设,使用这些工具功能将所有经过验证的断言转换为假设,然后利用它们作为其余证明的辅助假设。有些工具会自动执行相似的操作(但即使工具声称具有此功能,也不要想当然地认为),验证时一些“硬”证明可能是在所有“简单”证明完成之前开始的,因此许多复杂的证明可能没有充分利用所有经过验证的断言。
因此,在开始新的运行时,将所有经过验证的断言转换为假设,可能会促进验证进程。
3、是否有太多假设
尽管通过排除部分问题空间,假设有助于对抗复杂性,但它们也会增加复杂性,让FV引擎更需要计算,以确定一组特定行为是否合法。图10.7提供了一个概念性说明,为什么添加假设可以增加或消除复杂性,虽然一组好的假设将减少问题空间,但大量假设可能会使确定特定行为是否合法的计算变得复杂。
因此在添加辅助假设时,需要仔细观察效果,以确定它们是否真的有帮助。如果事实证明“辅助”会使性能变差,则将其分组添加,并删除“辅助”。
六、利用自由变量进行广义分析
自由变量与主要输入相似,只是一个进入设计的值,FV引擎可以自由分配任何假设不禁止的值。自由变量的不同之处在于,它是由FPV用户作为额外输入添加的,而不是已经作为模型的实际输入存在的。通常,用户可以创建一个自由变量,将其作为新的顶级输入添加到模型中,或者将其声明为模型中的变量,然后使用FPV工具中的命令使其成为切点。
自由变量提供了一种概括形式分析的方法,同时考虑涉及变量每个可能值。当模型包含一定程度的对称性时,这些变量可以显著提高形式化分析的效率。
1、利用刚性自由变量的对称性
MRA单元的一个变体,维护不同请求者的令牌(tokens)并根据优先级授予它们。如果令牌是根据需求授予的,那么为每个请求者证明一个属性
parameter TOKEN_CONTROL = 64;
generate for (i=0;i>TOKEN_CONTROL; i++) begin
ctrl_ring(clk,grant_vec[i], stuff)
a1: assert property (p1(clk,grant_vec[i],stuff))
end
endgenerate
将断言放在generate循环中,最终会对断言的版本进行64次稍微不同的验证,这可能会使FPV工具在时间和内存方面有巨大成本。可以从生成循环中删除断言a1,而不是用一个断言来证明该属性:
int test_bit; // free variable
test_bit_legal: assume property(test_bit >= 0 && test_bit<64);
test_bit_stable: assume property (##1 $stable(test_bit));
a1: assert property (p1(clk,grant_vec[test_bit],stuff));
使用自由变量test_bit 使断言具有一般性,不是单独分析64个案例中的每一个,而是告诉test_bit工具完成总体分析,一次性为test_bit 的所有64个可能值证明断言。
使用自由变量时需要小心,在上面的例子中添加了两个基本假设:自由变量具有合法的值,并且它是一个稳定的常数。在使用自由变量替换生成循环或类似规则模式时,通常需要这样的假设。
2、自由变量的其它用法
自由变量另一种用法是表示一种广义行为,即在每个执行周期中,决定采取几个不确定的行动中的哪一个。例如,自由变量可用于表示时钟域交叉边界处的不确定抖动;或表示内存控制器缓存中返回的匹配标记的特征;或者寻找由相同或对称逻辑处理多个行为的情况,FPV引擎可以利用这些情况。
在其中一种情况下使用自由变量时,可能不想像上面的刚性示例那样假设它是永久不变的;相反需要添加一些假设来定义何时可能发生更改。在某些情况下,即使在总线宽度上应用了类似的逻辑,自由变量也很难收敛。为了更好地收敛,在不影响穷尽性的情况下,跨自由变量拆分用例可能是必要的。在上述定义的逻辑中,如果添加一个自由变量来替换64位会导致更大的复杂性,或许可以将数据总线拆分为四个16位表示,并允许FV工具在这四个关注集上运行并行会话。
(top_freevar 55 0) |-> (freevar <= 0 && freevar >= 15)
(top_freevar 55 1) |-> (freevar <= 16 && freevar >=31)
(top_freevar 55 2) |-> (freevar <= 32 && freevar >=47)
(top_freevar 55 3) |-> (freevar <= 48 && freevar >=63)
这种拆分案例,以及利用对称性的自由变量技术,可以大大简化复杂逻辑的证明性质。
3、自由变量的缺点
尽管添加自由变量通常可以提高效率,但自由变量是一把双刃剑。当对称性被利用时,这是非常有效方法。但如果有一个例子,概括分析会导致FPV工具试图同时处理多个不同的逻辑片段,那么自由变量可能会导致错误。你可能想选择分格,为设计空间的不同部分运行真正独立的证明。
自由变量会使断言和假设在模拟或仿真中不那么有用,如果打算在这些环境中重用FV附属资料,需要谨慎添加对模拟友好的指令来随机化自由变量,这样也只能从其他属性中获得一小部分动态覆盖率,因为为自由变量选择的随机值很少与模拟驱动的值一致。
七、简化复杂度的抽象模型
抽象(Abstraction)是扩展FPV容量的最重要技术之一。抽象简化了 DUT 或其输入数据,且保留足够的重要逻辑,以便于证明。之前讨论过的黑盒、切点和自由变量,都是轻量级抽象;大多数抽象模型都使用自由变量。现在讨论更复杂的抽象,将更多的逻辑重塑为更简单的版本,以克服复杂性障碍。
抽象设计的方法有很多种,一些最常见的抽象类型包括:
- 局部化(Localization):排除可能与证明无关的逻辑,比如添加了切点或黑匣子
- 计数器抽象:简化其有趣状态的表示,而不是精确地表示计数器
- 序列抽象:通过分析小部分广义数据包的行为,来表示一个无限信息流序列
- 内存抽象:用精确跟踪某些元素的部分模型表示内存
- 阴影模型:选择某个块逻辑的子集进行建模,并允许未建模区域的任意行为
大多数抽象简化了模型,所以用户需要记住假阴性的问题,需要根据实际设计仔细检查潜在的错误(缺陷)。还需要小心误报(假阳性),虽然大多数抽象都是泛化模型,与切点或黑匣子一样安全,但有些抽象会和限制行为、隐藏错误的假设结合在一起。
1、计数器抽象
造成复杂性(度)的一个常见原因:计数器的序列深度较大。当存在深度计数器时,设计的状态空间直径较大。多个计数器的输出可以相互作用时,设计的状态空间直径更大:形式分析必须达到与计数器大小乘积成比例的深度,才能处理所有可能的组合。
带计数器设计的FV很复杂,且很可能包含通过任何可行有界证明都到达不了的覆盖点(因此导致潜在断言失败)。而且FV工具生成的场景或反例通常很长,很难理解。
验证过程中,处理计数器的一种简单方法:忽略计数器如何计数,而是简单地为计数器分配任意值;这可以通过黑盒计数器或使其输出成为一个切点来实现。但多数情况下,计数器的特定值以及多个计数器之间的关系对设计至关重要,所以在验证过程中为计数器分配任意值可能会导致不可靠的验证。在提取计数器值之前,要先查看计数器值对下游逻辑的影响。
用简单的状态机模型替换计数器,将N位计数器的状态图抽象为几个状态,例如:0、1、至少一、至少零,可以证明许多有用的检查。计数器抽象的示例如图10.8所示:
在图10.8中, 处于重置状态时,模型不确定地(基于自由变量0或1)决定每个循环是保持该状态还是转换到下一个状态。最简单的计数器抽象类型可能只表示重置状态,表示计数器的非临界值,以及一个临界状态,表示触发设计操作的计数器值。
在图10.8中,描述了一个计数器抽象模型,其中几个关键状态由下游逻辑确定。图中有两个临界状态n 和 m;使用这种抽象,需要将原始计数器输出设为切点,对抽象状态机和自由变量建模,然后添加原始计数器输出始终与状态机输出匹配的假设。
计数器抽象模型的状态比计数器本身的少,从而简化了验证过程中的计数器;一些商业工具支持内置的计数器抽象作为其产品的一部分,使这种方法更易于使用。
2、序列抽象
数据传输模块的主要责任是准确地传输数据,序列抽象的目的:用一小部分通用代表替换无限多个可能的数据序列。这种传输程序的复杂性源于:编写的属性来检查输入数据包的序列是否在输出端维持。
图10.9显示了一种机制:通过将所有可能的数据包替换为三个不同且未指明的数据包C、B和 A来验证设计。我们使用一组刚性自由变量来定义这些通用的任意数据包,通过假设序列可能仅由这些数据包组成来约束输入,然后检查所有可能的输入序列是否在输出端复制。使用这种方法,可以检查数据包被丢弃、复制、重新排序或损坏的情况。
使用自由变量在每次运行中,允许三个目标数据包的任意内容,涵盖了由三个任意(但恒定)数据包组成的连续信息量可能产生的任何行为。但这种简化会隐藏一些潜在的错误,例如,假设一个由六个不同的数据包值组成的序列,没有两个值相等,导致一些内部状态机进入坏状态,这种情况会被忽略,因为这只允许总共三个不同的数据包值。
这种抽象模型能够代表一大类潜在的行为,且以高置信度验证数据传输模型;如果要求是完全证明FPV,需要考虑到这些限制。
还有一些其他相关形式的序列抽象(见Chris Komar, Bochra Elmeray, and Joerg Mueller, “Overcoming AXI Asynchronous Bridge Verification Challenges with AXI Assertion-Based Verification IP (ABVIP) and Formal Datapath Scoreboards,” Design and Verification Conference (DVCon) 2013),不限制允许到达的数据,允许所有可能的数据,但仔细设置断言,只跟踪与一小部分标记匹配的数据,这样可以进行更一般的分析,有时会以更高的验证复杂性为代价。
序列抽象可以代表宽范围的序列,在数据传输设计中非常有用。一些商业工具将序列抽象机制作为其产品的一部分,从而允许用户加快数据传输设计的收敛速度。
3、内存抽象
实际上内存、缓存或深层序列会给FV工具带来复杂性,因为它们是状态元素的巨大集合。例如,一个小的寄存器文件包含16个64位的条目,每个条目将展开1024个状态元素,每个状态元素都可能在任何时钟周期内具有新的值。内存是非常规则的结构,内存本身是通过专用技术单独验证。出于FV目的使用黑盒内存通常很实用,并且会在其他地方得到验证。
但不能总是安全地黑盒处理内存,仍然获得FV证明。一些属性可能依赖于可见写入内存的值(seeing a value written to memory),然后检索到相同的值;如果完全屏蔽内存,每次读取内存时都可能会看到错误的值。因此需要某种方法在FV环境中从功能上表示内存。
一个简单的解决方案是使用参数来减小内存大小,如前面关于参数和尺寸减小的部分所述。如果内存是只读的,比如在预编程微码存储器中;
另一种解决方案是将其替换成包含大量常量赋值的简单模块,而不是真正的状态元素。有时还必须使用更强大的抽象方法来提供更简单的内存表示,而不会导致形式引擎的复杂性。
抽象内存的一个有用方法是用一个新版本替换它,该版本有一小组“有趣”的位置。例如,尽管模型的内存很大,断言也只与一次使用四个内存元素的操作相关。不关心所有的内存条目,而是定义了一个只关心四个条目的内存,Entry_1 到Entry_4 ,如图10.10所示:
内存的基本接口保持不变,只能准确表示由自由变量表示的四个特定任意位置的内存。
抽象模块可能包含如下代码:
// 自由变量,将定义为FV工具命令中的切点
logic [11:0] fv_active_addr[3:0]; // 活动地址的刚性变量
logic [63:0] fv_garbage_data; // 提供随机数据
logic [63:0] ABSTRACT_MEM[3:0]; // 抽象记忆
logic garbage; // 1 如果输出垃圾
// 在任何给定的运行期间保持active_addrs恒定
active_addr_rigid: assume property (##1 !$changed(fv_active_addr));
always @(posedge clk) begin
for (int i=0; i<4; i++) begin
if ((op==WRITE) && (addr == fv_active_addr[i]))
ABSTRACT_MEM[i] <= din;
if ((op==READ) && (addr == fv_active_addr[i]))
dout <= ABSTRACT_MEM[i];
end
if ((op==READ) && (addr != fv_active_addr[0])
&& (addr != fv_active_addr[1])
&& (addr != fv_active_addr[2])
&& (addr != fv_active_addr[3]))
begin
dout <= fv_garbage_data;
garbage <= 1;
end
else begin
garbage <= 0;
end
end
现在不再跟踪所有条目,而是将访问限制为仅四个条目;抽象与内存大小无关,可以实现巨大的内存缩减。内存访问的复杂性仅限于跟踪四个条,由于这些条目是任意的,可以被具有任何合法值的自由变量处理,因此这不会损害任何只涉及四个内存位置的证明的通用性;对其他位置的任何访问都将返回任意“垃圾(garbage)”值。
一旦对内存进行了建模,可能还需要通过添加新的假设或断言先决条件,确保断言只使用选定的地址。这些假设并不是严格要求的,但为避免调试许多断言失败(由于对返回垃圾值的非模型地址进行内存读取),可能需要这样做。如果已经创建了一个信号来指示何时访问非跟踪内存地址,比如上面代码示例中的垃圾,那么这相当简单:
// Make sure address used matches a modeled address
no_garbage: assume property (garbage == 0);
在某些情况下,可能还希望为此类垃圾值返回一个标准错误值,例如32'hDEAD_BEEF,以便始终可以使用标准签名检查错误情况。试图创建这样一个标准垃圾值时,必须在输入上添加一个约束,以避免输入这样一个值。
使用这种类型的抽象时要小心,如果属性确实依赖于比在抽象中所允许的更活跃的记忆元素,会发现新假设或先决条件过度约束了环境,隐藏了重要的行为,从而产生了误报的风险。始终要注意将这种抽象与一组好的覆盖点结合起来。
一些供应商已经提供了自动内存抽象的工具包,所以在从头开始实现这种抽象之前,一定要研究这种可能性。
4、影子模型
本章未涉及的一大类设计抽象的总括。如果有一个子模块导致了复杂性问题,用影子模型替换它,一个抽象的替换模型,包含部分但不是全部功能,使用自由变量将真实和复杂的行为替换为任意简单的行为。前面讨论的计数器和内存抽象可以被视为特例:计数器模拟抽象状态,而不是保持计数,内存只跟踪其实际信息的子集。
这种类型的抽象非常有用,包括:
- 算术块:识别已请求的操作,并在正确的时间将输出设置为有效,但返回任意结果。
- 数据包处理:确认数据包完成并检查错误,但忽略解码逻辑和其他处理。
- 控制寄存器:忽略寄存器编程逻辑,但预设标准寄存器值,并使该块像正常控制寄存器(CR)块一样执行读取请求。
面临一个复杂的问题时,看看设计的主要子模块,问问自己是否真的需要表达逻辑的完整程度;会发现许多简化的机会,使FV更有可能成功。