仓库源文

芯片验证软件的4+1方法


新版本说明

这个版本是在培训完成后,增加了根据现场提问补充内容的版本。原来的描述链基本保持 不变,但通过增加类似如下的引用文本描述对相关主题的深入解释:

| 这是一个对当前描述概念的深入解释

介绍

前段时间给内部团队做了这个培训::doc:架构设计入门知识 。后来看了芯片验证团队 做的一个设计,感觉就没听懂(当然也许是没有参加,因为我们是个线上演讲,只知道人 数有千把人,不知道谁在线上),相关的同事想我单独对验证遇到的情况进行一些具体的 指导,所以我准备了一下专门针对芯片验证的提纲,描述在这里。

首先,我们需要理解,架构是为了解决非常具体的问题,是因为你本身有问题,所以你需 要做架构设计,而不是因为某些专家给你灌输一些你理解不了的概念,然后你需要按他们 的原则来办事。不是要你符合某些“架构”专家的要求,而是你自己有问题需要解决,需要 通过架构问题解决这些问题。

这是架构设计一个非常基本的要求。架构不是一个需求,而是一个内在逻辑驱动的设计行 为。如果它变成一个外在的需求,你会失去目标驱动力,因为你并没有实在要解决的问题 ,而只是做一些动作获取“表扬”,这个事情就怎么都做不成。

那么芯片验证需要解决什么样的问题呢?很简单,你建立一个框架,后面一堆的人在你这 个框架上写测试用例,你的程序也会被移去给其他的验证平台用,你的代码可以一转眼就 会从数千行变成数十万行,那个时候,如果你随便加点代码都要调整很多地方,或者要加 一个功能根本加不动,你就会很被动。

如果你很自信,你没有这种问题,那我们就不用说什么了。你就随手写你的代码就行了, 你不用听任何人唧唧歪歪。但你也别拿个模棱两可的框框来说你的“架构”,那玩意也没什 么用,因为实际上什么东西都在你的脑子里。

一个软件的架构主要控制的是软件什么地方是不变的,什么地方是变化的。所以,其实验 证用的软件和普通的软件是很不一样的。验证软件更关心的是如何可以重置芯片的状态, 然后构造一个或者多个行为,判断芯片是否可以正常反应。而芯片的使用软件更关心的是 如何选定一个使用这个芯片的方法,让其他不在选定范围中的状态根本进都进不去。

比如说,一个网卡(注1)可以把多个通讯队列(Q)分给不同的虚拟网卡(VF),比如你有 100个Q,你可以分配两个VF,每个50个Q。你也可以分成三个VF,两个30个Q,还有一个40 个Q,甚至可以两个50个,还有一个没有VF(比如用于自环测试等等)诸如此类的。如果要 进行验证,我们需要制造多种初始化手段,在网卡上创建不同的VF,每个VF上用不同数量 的队列,而且尽量测试各种极端的分配情况看看会不会出错。而如果要用于实现功能,我 们可能一开始就设置一种VF队列分配算法,那些分配0个队列给VF的情况,我们甚至在很多 地方都加了控制逻辑,让这种情况发生不了。

这样,两个软件的代码行为和模块分解就是不一样的。验证代码可能会根据不同的队列分 配关系,测试不同VF的带宽是否得到扩充。而功能软件可能会更关注比如按一定的策略分 配了VF后,把两个VF组合起来形成Bonding的功能是否可以正常工作,或者其中一条链路断 了,另一条链路能否独立保持两个通讯节点之间的数据是可靠的。

验证是针对芯片或者IP的功能的,而功能软件是基于芯片或者IP的功能实现高层的逻辑, 大部分设计是针对它自己的功能的。

所以,你不能指望直接拿一个实际的功能去完整“验证”你的芯片,你的芯片能让Linux正常 工作,不表示它能让QNX正常工作。能让一个前端服务器正常工作,不见得可以让一个RSA Proxy正常工作。验证是要遍历接口所有可能性的,而功能软件是要实现功能,根本不是一 个东西(功能就不同)。

下面我们用验证软件这个特定的场景,具象化地看看4+1视图怎么用。

Use Case视图

一个验证软件说起来很简单,只要初始化芯片,然后根据芯片的功能输入数据,看看输出 是否符合预期就可以了。但如果考虑我们将需要在几十种芯片(包括FPGA或者EMS这些模型 )上使用,要运行数万个用例的时候,你就没有那么简单了。

所以,你的软件到底聚焦什么范围呢?适配某种特定的芯片代码到底算不算是你的输出的 一部分呢?这个说起来好像不重要的问题,其实很重要,否则你根本就落不了地。

所以,系统的上下文就显得很重要了,比如你的验证软件范围可能是这样的:

    .. figure:: _static/verify_sw_uc1.jpg

这就是Use Case图。这个图极容易画错。正如我前面反复强调的,如果你学习“怎么画才能 符合专家的要求”,你这个图百分百会画错。这个图是一种设计,设计的目的不是“我没错” ,设计的目的“我决定做这件事,获得我要获得的利益,对错我都背上身”。所以,考量这 个图的建模,要从你细化你要做的事情来开始想:你要做一个验证软件,这个软件有什么 用?“验证某款芯片?”还是“验证某类芯片?”,接着我们问:“你写了这个软件这个芯片就 会得到验证吗?”,显然不是。那么你需要谁来跟你配合?什么算是你的功能,什么算是需 要别人配合的?

所以,只会学个形式的人会重视中间那些圈圈,想着把自己受到的需求全都搬上去。其实 这个图更关心的是那个四方的框框。那里定义了这个软件的边界,决定了你的软件包括什 么东西,和谁打交道。上面这幅图中,验证软件要发挥作用,需要先找一个人写一个芯片 的配置代码,适配到验证软件中,然后让测试者把整个软件在芯片上运行,才能保证这个 测试可以被执行起来。

所以这幅图根本和你需求列表中的那些条目半毛钱关系没有,它关心的是你的“地盘”在哪 里。你在整个业务中,负责的是整个业务的哪一环。

我们根据这个模型对设计进行取舍。比如,我们希望测试用例写作和你的验证平台是相对 独立的,你的模型可能可以改成这样:

    .. figure:: _static/verify_sw_uc2.jpg

你可能还会认为:我只负责写平台,用例有其他团队来写,验证软件本身不包括用例。或 者“我只负责基本用例,大部分用例需要别人写”,那么,你这里这个“用例增加”的功能, 就该摘出去。每个架构图,都是为了让你在某个模型下推演这个角度上的可能性和取舍。 这和我们做芯片投片前,要做数字功能验证,信号一致性验证,电源完整性校验一样的。 我们取最终产品的其中一个切面出来进行单独的逻辑推演,提前看看这个角度未来会不会 做不到而已。

你还可能觉得,“‘用例增加’这个事情,就算我不写,反正我也会做的”。但写和不写的区 别在于:你把这个主题突出来,我们就会对这个逻辑进行建模。

所谓的Use Case图,主要是为了让你弄清楚到底你打算做的是个什么东西,包含什么功能 ,哪里准备做一个相对稳定的接口,用于响应变化相对频繁的部分。

    | 现场有人问:能不能理解Use Case中的Use Case,是需求的“总结”?
    | 答:大部分时候不能。我们这样理解:我们一般说的需求,会有很多维度。
    | 比如对于一个验证软件,可能需求方会要求它是个Apache版权协议的软件,
    | 这不是这个软件的功能。要求Apache协议是需求,但不需要总结在Use Case中。
    | 所以,Use Case是说明要做的软件在整个上下文中呈现为一个什么角色的
    | “设计”,而不是需求的总结。它不表达需求的全集,它是根据需求的要求
    | 进行的第一波建模:你让我做这些东西,我做一个某某东西,在你的工作
    | 环境中,以这个某某东西,承载你要求的那些需求。

这个东西你说不定觉得显而易见,但跟你讨论的人不见得就认为它那么显而易见。而其实 对我来说,我已经看过很多很多次了,认为这个事情显而易见的,到最后写出来的软件, 常常确实也不那么显而易见地符合预期。不是丢了这个,就是丢了那个。在作者自己那里 就理不顺逻辑。当初信誓旦旦说自己知道各个地方是怎么组织的,其实自欺欺人而已。没 有这个建模的过程,你脑子里肯定是一陀浆糊。就算你不是一个团队,需要给其他人讲, 对我来说,我自己写一个程序,都需要在纸面上把这个模型画出来。

你决定了某个人是特定的使用者,你就需要定义给这个人的接口,你的设计在这个地方就 会标准化。比如你让人可以标准化地增加测试用例,你就需要给出写用例的人应该包含什 么头文件,可以使用什么API,不可以使用什么API等等。如果用例都是你自己写,不是个 标准的接口,那就另当别论,你的测试用例可以是一团麻,但你也不要指望,后面放十几 个人来给你写代码。因为这些人会天天来问:“这里到底是怎么回事?为什么我包含

<stdio.h>编译不过?……”。你不回答,他们就会用各种手段,最后真给你包含了stdio.h, 你的平台基础就被绑定了,如果你的系统目录中没有stdio.h,你的系统就跑不起来。如果 他们还用了openssl.h呢?调用了perl和tcl的脚本呢?访问了Linux的sysfs呢?你的依赖 就更多了。最后你的系统就不可控。

概念空间建模

当你有了这方面的考量,你就会需要概念空间的建模。概念空间说明这些使用者每个怎么 用你的系统。为了说明白这些功能,你需要一些基本的定义,说明这些定义之间的关系。 比如这里提到的用例增加的功能,你可能需要说明白:

用例要用什么语言写?可以调用什么基础设施?写好的用例怎么插入到目标系统中?这些 用例会被按什么顺序调用,怎么报错?

为了说明这些里面,你需要说明什么是“用例”,什么是“插入”,什么是目标系统,什么是“ 测试报告”等等。这些概念不一定需要深入到具体的接口或者语言,但你需要说明它们之间 的关系,使用的逻辑。这个建模的作用是保证无论你的功能最终怎么实现,你至少是“说得 通”的。很多人不建这个模型,都会觉得“这个很通啊”,但其实你真说一下看看,你就知道 在概念上说通一个功能,其实很不简单。

比如,用例是什么?你觉得你知道。但定义一下看看?可能我们可以这样定义:“用例是一 个在xx测试环境完成初始化后的一个模块,这些模块之间没有依赖关系,可以被不分先后 地独立调用。模块中包含一组称为test_xxx的函数,这些函数无输入参数,返回pass, fail或者通过uc_fault()抛出异常。……”

恭喜你,你已经给你的系统制造了一组约束了。这组约束就是你写具体这些模块的时候的“ 设计需求”。没有这么一个建模过程,你写具体代码的时候根本不会考虑这些问题吧?

但在我们让它们变成真正的细节设计的约束前,我们这个模型更重要的是用来挑逻辑漏洞 。(所以概念视图又叫逻辑视图)。比如你前面这样定义你的“用例”,那么我就问了:你 初始化完成以后就跑用例,那我需要不同的初始化环境跑相同的用例这种情形应该怎么测 试?

比如RISCV可以配置XLEN来决定字长,我想初始化为32位字长和64位字长分别测试,上面的 逻辑怎么通?

然后你就准备好修改你的“概念空间”吧。

概念空间建模说到底就是用“大白话”说清楚功能是怎么被提供的。因为它是大白话,它就 成为一种高层抽象,因为我们不用关心具体的细节。比如测试用例的函数的函数原型是什 么?我们不关心。反正这个原型是什么,只要它能被调用,怎么都行,用test_做前缀的所 有函数也行;定义一个数组,里面列出这些函数也行。这些可以先不考虑,留给下一层设 计,但里面是一组函数,我们可以一个个调用,调用完以后要重置系统状态,我对每个测 试例的初始状态就会有要求。那这时如果我想到了,部分测试用例我们可能有依赖关系, 我们希望测试完一个再测试下一个,不要重置状态,这就只能把他们全部放到同一个测试 用例中了。因为我们前面的逻辑定义是每个用例都会被复位状态。但这样可能就导致我们 出错的时候不容易定位具体是哪个用例出的问题,那也许我们需要引入一个叫子用例的概 念……

你看,如果你一开始深入想这些问题,你就会发现,一个自恰的,能把问题说明白的概念 空间,不是那么容易建立的。

    | 现场有人问:概念空间建模有没有固定的方法或者最优实践可以仿着做?
    | 答复:这是个危险的思路。“固定的方法”意味着你不准备设计,
    | 而是准备不需要动脑,按某种“已经存在的设计”进行机械的执行。
    | 这恰恰是“构架设计”,甚至“设计”的大忌。所以,请一定放弃这种想法,
    | 构架设计可能有一些模式,但你必须很小心,不要陷入到模式中,
    | 而要用事实去校验这种模式是否适合。这也是在本文最开始的地方强调的:
    | 你是有自己的问题要解决,而不是为了满足“专家”提供的“模式”。

我在实践中发现,人们不愿意做这样的设计包括几种原因。一种是因为缺乏经验——不是缺 乏写构架设计的经验——而是干脆就缺乏编码经验,所以他并没有能力判断,如果写一个库 ,里面都是test_这样的代码调用起来是什么样的,能不能组合得起来一个个轮着调,恢复 芯片的状态是不是可以做到……这些东西他都不知道,他非得写两行代码,编译一把,运行 一下,看看通过了,再补两行,再编译编译……这种逼着他做概念空间建模,也是白搭。这 种就先玩玩吧,也甭指望搞什么架构设计了,老实承认做不了架构设计好了。

另一种情况呢,是怕露怯。对不少人来说,写在文档中的东西就是“承诺”,是他的尊严, 没有写出代码来校验过,都不敢写出架构设计说明来。这种,真就只能看你的思想道德建 设了。我是劝你别这样,但你不听,我也没有什么办法。我只是判断你这么弄软件肯定会 一团糟而已。

对我来说,你不做架构设计,你的软件不可能写得好。这和你出趟远门不查地图不看列车 时刻表,直接出门,走到哪里是哪里一样。你非要出去走一趟,想要到了目的地在给我写 个攻略,你还不如别写。你都到了,我没空理你,赶紧去下个目的地吧。但一个严肃的项 目,在开始阶段,你不肯进行高层建模,你还控制几十人的人力,你最后做出什么狗屎垃 圾我们也只能认了,能用一段时间也只能顶一段时间了。现在软件相对成熟,不少软件都 基于过去成功的软件做的,你再烂,顶一小段时间还是可以的,就是没有未来而已。

最后,可能有人会奇怪,为什么讲4+1视图会不需要讲UML图的。其实UML图根本不是4+1视 图的关键,UML图的用途就那么几个:

    .. figure:: _static/uml_elements.jpg

在概念视图中,它通常只是用来表明我们描述每个架构模型的时候,那些概念之间的关系 而已。比如这里的概念视图,我们要说明白验证软件,测试用例,硬件使能软件,用UML的 类图来表达关系更加容易而已:

    .. figure:: _static/verify_sw_concept_diag.jpg

有这张图你肯定更容易说明白你整个软件的组成,但概念空间的建模的重点就不是那几张 UML图。

开发视图

概念视图不是实际的代码,所有其他视图都一样。所有的“模型”,都不是实际的代码(但 他们可以是代码),因为如果我们有代码了,通常我们不需要建模。

所以,我们应该理解,开发视图不是延续概念视图的逻辑出来的。它是换一个细节关心的 角度,设计另一组约束给细节设计而已。

开发视图关心的是你的写的代码。概念视图中你说两张网卡一张发,一张收。开发视图中 只看到“网卡”这个实现。你用的时候用了很多张网卡,但开发的时候你只是开发了一种网 卡。

所以开发视图建模的角度是你到底要“开发多少东西”,而不是“生产”或者“购买”多少东西 。对于验证,我们关心的是你开发的那个东西是什么,后面我们怎么叫它,它拉多少个分 支。比如你的平台叫TestBench,里面不包含测试用例。好,现在测试用例和它是什么接口 ?一起编译还是二进制接口?最终测试的人,每次都必须重新编译一次,还是TestBench是 固定的二进制版本,然后链接到TestCase中?还是说,你这些都是二进制,只要和对应的 平台使能代码链接就可以用?平台使能代码有多少种可选的情形?

比如我设想一种情况,可能就是这样的:

    .. figure:: _static/verify_sw_dev_view.jpg

这个很简单,我们认为被测试的平台除了初始化,都是一样的代码,所以,除了特定的适 配,我们可以只出一个TestBench的开发库,Testcases直接集成到其中(就是你要加用例 必须加到TestBench的库里面),之后不同平台你适配不同的平台代码就行了。

这个看起来和前面的概念空间视图是不是很像——如果你学它的样子,的确是的。他们的关 键区别在于,概念空间我不管你具体怎么开发,怎么用,我只说我会有一个TestBench,你 找人加上initCode就可以运行,然后你测试就可以了。但开发视图关心的是开发起来的时 候怎么处理具体的开发问题,比如这个TestBench有多少种变体?编译出来有多少个二进制 ?源代码和别人接口还是源代码让别人编译?——这些是“开发”的时候关系的问题,所以它 才不是概念,而是“开发视图”。我们构架模型建完了,就要进入开发了,没有这个模型, 就只是在实验室里面玩玩而已——当然最后上市场的时候你还是要鸡飞蛋打地“攻关”,见一 个客户“落地”一个客户,然后版本洒得到处都是,然后再来个重构,拉通,统一版本……的 。——我还是那句话,没有架构,你什么错到最后就是可以解释的,只是你永远得不到一个 好的,可以长远发展的软件而已。

开发视图通常是个很烦的东西,但你要知道烦的不是视图,是方案。在构架阶段都这么烦 的东西,到了具体开发的时候你反而觉得会没有?那只有一种解释:

你没有面对现实。

Sadly,不肯面对现实是不少人的常态。

    | 现场有人问:很多时候,我们只是某个模块或者子系统的设计者,
    | 而且我们的上游常常没有清晰的设计约束告诉我们,这种情况如何处理?
    |
    | 答复:这种情况确实挺常见。但我们这样看:上游没有给你清晰的设计约束,
    | 你还是最终把代码写出来了。你的代码都能写出来,你还是有逻辑链而已,
    | 你宁愿在代码中呈现你的逻辑链,却不在架构设计中呈现你的逻辑。
    | 这最终是你不能面对现实而已。我们的构架设计和设计文档模板中都有假
    | 设和限制一章,请想想你应该在这些写什么。

    | 现在还有人问:如果一个系统本身没有做架构设计,新做的系统应该怎么做设
    | 计设计?是否应该先重构?
    |
    | 答复:没有东西是没有构架的,只要它能用,最多是架构比较烂(关联复杂)
    | 而已。我个人反对任何没有商业利益的重构。一个东西都能用了,你为了个
    | 人审美重构它,这是吃饱了撑的。如果你有新的需求,或者你的原系统的坏
    | 架构已经产生很大的伤害了。你就是一个新的设计,每个新的设计都是建立
    | 在旧的逻辑上的,你写一个新程序,也会选择使用C或者Python之类的程序平
    | 台来写,这些语言也有自己的缺陷,你也不会重构它们。所以,每个设计都
    | 可以是新设计。我们常常可以先对老系统进行构架画像,用很粗的线条呈现
    | 它对我们新功能暴露出来的形态,然后我们可以在这个画像的基础上实施我
    | 们的构架建模了。

处理视图

处理视图建模另一个写代码的时候控制不住的维度,主要是Scalability。也就是说,如果 你一个线程,一台机器搞不定,你怎么把业务展开到多个执行体上。

看什么类型的验证了,我所知的很多验证,因为被验证平台执行速度上的问题,用集群进 行验证并不少见。如果就看前面几个视图,你可能就不觉得你需要分开测。但一考虑到运 行一个验证需要几天,或者几个星期。要对被验证的业务进行切片,然后每个切片验证芯 片的一部分,每个切片要跑一天。你的想法(约束)就又不同了。

处理视图也不是什么结构化的方法,基本上也是大白话(通常可以用UML图来辅助表述而已 ):你的测试怎么分开多种类型,在哪里分开,在那里独立执行,在哪里汇聚,等等。注 意了,这里的“哪里”的概念,在前面都没有提过,它不是“用例”这样的维度,也不是“ TestBench”这样的维度,很可能你要定义针对处理视图的概念,比如“主控线程”,“测试线 程”,“报告收集线程”,然后你有线程组织起来的线程组,节点,计算节点池这样的概念, 这最后还是UML类图,只是用来表述你怎么安排你的程序而已。

我也画一个例子吧:

    .. figure:: _static/verify_sw_process_view.jpg

如果你的验证很简单,就是验证一个测试片是不是符合架构定义,直接在测试片上运行, 那你也不需要处理视图,你可以不做这个建模,而不是假惺惺满足这个4+1的名字,什么都 胡陬几句上去。架构设计每句话都是针对细节设计的约束,而不是给某个专家或者领导检 查看的。

部署视图

最后,部署视图在我的职业生涯中用得很少,它也是个独立的维度,考虑最后运行起来, 怎么安装到目标节点上,如何控制这个安装过程,具体每个节点跑什么这样一个维度。这 个部分我作为答疑来讲吧——如果你们有问题,可以直接问。

    | 现场有人问:设计中是否只有这四种视图?
    |
    | 答复:显然不是。4种视图只是在你没有其他思路的时候给你提示从这里开始
    | 而已,你用什么视图,完全取决于你设计的具体情况:有什么东西是你编码
    | 的时候控制不住的,而不是——如前所述——专家告诉你要满足的某些“原则”。

[1] 没有相关背景的读者可能觉得网卡不是“芯片”,但现在的SoC中其实是内置网卡的。 当然我也可以举CPU核测试的例子,但这样有直观感受的读者会更少,所以我们还 是用网卡来举例子。