第 8 章 · 共 15 章
从 Spec 到功能清单
一套可重复使用的技术,把一份规格书拆解成功能清单,而不是依赖读完之后想到什么就写什么——直接应用在 axil_regfile 的 AXI4-Lite 基础和寄存器映射上,产出的清单已经明显比这个项目里任何一个测试实际测过的东西更广。
第 1 章把功能清单列为一份验证计划四个组成部分里的第一个:把 spec 做出的每一条承诺,拆解成具体的条目。"拆解"这个词在这句话里承担的分量很重——功能清单不是 spec 的摘要,也不是读完一遍之后随口想到的三四条要点。这一章讲的正是这两者之间的差别,用一套技术直接应用在 axil_regfile 的 spec 上(uvm-advanced 第 1 章)。
陷阱:只测显而易见的东西
快速读一遍 axil_regfile 的 spec,第一反应往往会写出这样的清单:测写、测读、测中断。 这份清单不能说是错的——每个字都对——但它是一份摘要,不是一次拆解,而摘要恰恰藏起了一份真正的功能清单存在的意义所在。"测写"这三个字底下,悄悄替代了至少大半打真正不同的行为:写的是哪个寄存器、这个地址到底有没有映射、这个寄存器到底支不支持写、每种情况分别产生哪种响应码,以及有没有别的状态作为副作用跟着变化。照着这三条摘要去做,一个验证工程师很可能会在真正做完之前很久,就觉得自己已经做完了。
一套可重复使用的技术:每一节问三个问题
一节一节地读规格书——不是读 RTL,RTL 只能告诉你某一份具体实现恰好做了什么——对每一节都问三个问题:
- 这一节断言了什么必须永远成立的东西? 一条不变量、一条规则、一个有保证的行为——这是最直接的一类功能。
- 这一节明确说了什么是不支持的? 一个"不存在"也值得写下来——不是因为它需要一个测试,而是因为把它写在清单上,才能让评审的人知道这件事是被考虑过、刻意排除的,而不是单纯被忘掉了。
- 这一节描述的东西,跟 spec 别的地方描述的东西有没有关联? 功能很少是孤立存在的。最丰富的场景往往就藏在两个功能的交界处,而不是任何一个单独的功能内部——而这些恰恰是一节一节、每次只看一节地读,最容易漏掉的东西。
把它套用到 axil_regfile 上
协议那一节
AXI4-Lite 自己的文本(uvm-advanced 第 1 章)直接给出了不变量:每个通道只在它的 VALID 和 READY 同时为高的那个时钟边沿完成一次传输;拉高 VALID 的一方绝不能等 READY,而拉高 READY 的一方可以合法地等 VALID(问题 1)。它也直接说清楚了缺了什么——没有 AWLEN/ARLEN,完全没有 burst 状态机(问题 2)——这一点之所以重要,正是因为它意味着没有人应该浪费时间去找一个从来就不打算存在的 burst 行为。它还直接陈述了一种关联:AW 和 W 是相互独立的,可以按任意顺序到达,也可以一起到达,BVALID 要等两者都到齐才会被拉高(问题 3)——这条规则只有作为两个通道之间的一种关系才说得通,不是任何一个通道单独的属性。
寄存器映射那一节
同样的三个问题,这次对着 axil_regfile 自己的寄存器表来问。每个寄存器的读写合法性是一条不变量(问题 1)。响应码的规则也是:合法访问给 OKAY,一个已映射但不支持这个方向访问的寄存器给 SLVERR,一个根本没有映射任何东西的地址给 DECERR——而协议那一节顺带提到 AXI4-Lite 完全没有 EXOKAY,正是问题 2 要找的那种明确的"不存在"。问题 3 在这里的回报最大:CTRL 的 ENABLE 位和 DATA 的写行为,分开来看,就像表里两行互不相干的记录——但 spec 说的是,一次 DATA 写只有在写的那一刻 ENABLE 已经置位的前提下才会让 COUNT 自增。这不是 CTRL 的功能,也不是 DATA 的功能——它只存在于两者的接缝处,一份按寄存器逐行建起来、却没有问过问题 3 的功能清单,会径直从它旁边走过去而不自知。
这样产出的清单
23 个条目,按 spec 本身的组织方式分组,而不是按任何一个测试恰好测到了什么来分组:
A — 通道与握手
| # | 功能 |
|---|---|
| A1 | 每个通道只在它的 VALID 和 READY 同时为高的那个时钟边沿完成一次传输 |
| A2 | 这个 slave 的 VALID 信号(BVALID、RVALID)拉高之前绝不能等待各自的 READY |
| A3 | 这个 slave 的 READY 信号(AWREADY、WREADY、ARREADY)可以合法地等待 VALID,也可以提前拉高 |
| A4 | 不存在 burst 信号——每笔事务都恰好是一拍 |
| A5 | AW 和 W 可以按任意顺序到达,也可以一起到达;slave 必须正确锁存先到的那一方 |
| A6 | AW 和 W 都到达之前,BVALID 不能拉高 |
| A7 | 上一次写操作的 B 响应还没被消费之前,不接受新的 AW/W |
| A8 | AR→RVALID 有一个周期的延迟;RVALID 保持拉高直到 RREADY |
B — 响应码
| # | 功能 |
|---|---|
| B1 | 合法、成功的访问给 OKAY |
| B2 | 一个已映射、但不支持这个访问方向的寄存器给 SLVERR |
| B3 | 任何没有映射任何东西的地址给 DECERR |
| B4 | 这个协议里不存在 EXOKAY |
C — 寄存器行为
| # | 功能 |
|---|---|
| C1 | CTRL.ENABLE(第 0 位)可读可写,写入和读取之间会保持不变 |
| C2 | CTRL.IRQ_CLR(第 1 位)是写操作触发的一次性脉冲——从不被存储,之后的任何读操作都不会反映它 |
| C3 | CTRL 的写是整字操作:一次写同时指定两个位的新值 |
| C4 | STATUS 实时镜像 irq 的值;只读 |
| C5 | 一次 DATA 写无条件更新 data_reg,但只有在写的那一刻 ENABLE 已经置位时才会让 COUNT 自增 |
| C6 | 一次 DATA 读返回最后一次写入 data_reg 的值,不是 COUNT |
| C7 | 一次 COUNT 读返回内部计数器的值;COUNT 只读 |
| C8 | COUNT 一到达 IRQ_THRESHOLD,irq 就立刻置位,并且保持置位(是电平,不是脉冲),直到 COUNT 被清零 |
| C9 | IRQ_THRESHOLD 是一个构建期参数——是一条可配置的轴,不是一个固定不变的数字 |
D — 复位
| # | 功能 |
|---|---|
| D1 | aresetn 撤销时,DUT 每一处状态都回到各自定义好的值 |
| D2 | 复位可能在一笔事务进行到一半时触发,不只是在仿真最开始的时候 |
已经比目前测过的任何东西都更广
把这份清单摆在这个项目实际的测试历史旁边看,有几条已经显得很单薄——比如 C5 那条受 ENABLE 门控的自增,因为这个项目里每一次 CTRL 写都是在任何一次 DATA 写之前就先把 ENABLE 置成了 1。把这个结论彻底展开不是这一章的工作——第 3 章会把清单上的每一条都跑一遍系统性的场景检查,再把结果拿去跟 Coverage 模块的工具实际找到的东西对照。这里要做的事情更窄、也更机械:清单本身得先完整、具体地存在,之后才轮到问它的哪些部分真正被测过。
小结
- 功能清单是对规格书的拆解,不是对它的摘要——"测写、测读"这几个字底下藏着至少大半打真正不同的行为。
- 一节一节地读 spec,对每一节都问三个问题——什么必须永远成立、什么被明确排除在外、跟别的地方有没有关联——能找出快速读一遍会漏掉的东西。
- 明确写下一个"不存在"(没有 burst 信号、没有
EXOKAY)不是做无用功;它能让评审的人看出这个缺口是被考虑过、刻意排除的,而不是被忘掉的。 - 最丰富的功能往往就藏在两节分开读的 spec 之间的接缝处——
axil_regfile里受ENABLE门控的DATA自增,不属于任何一个寄存器单独所有。 axil_regfile的 spec 拆解出 23 个具体条目,分在四组里(通道/握手、响应码、寄存器行为、复位)——这份清单是从 spec 里建出来的,跟任何一个测试实际跑过什么无关。
快速读一遍 axil_regfile 的 spec,写出了“测写、测读、测中断”这份清单。这份清单真正的问题在哪?
AXI4-Lite 的 spec 明确说明没有 burst 信号(没有 AWLEN/ARLEN)。既然一个不存在的功能根本不需要测试,为什么它还应该出现在功能清单上?
功能清单上的 C5——一次 DATA 写只有在写的那一刻 ENABLE 已经置位时才会让 COUNT 自增——被描述成既不属于 CTRL,也不属于 DATA。为什么?