SVA APB 断言:PSEL/PENABLE 两相关系、PREADY 等待态与 PSLVERR 语义的并发断言套件
更新时间:2026-09-28。在 SVA 断言实践 里我们把并发断言的骨架(蕴含、时序算子、disable iff)过了一遍,这一篇把它落到一个具体协议上:APB(Advanced Peripheral Bus,AMBA 家族里的低速外设总线)。APB 的传输只有两个相位,规则看起来比 AXI 简单得多,但正因为简单,工程师容易拍脑袋写三条断言就交差,结果漏掉等待态稳定性和 PSLVERR 窗口这两类最常出事的场景。本文给出一套覆盖 APB 等待态与 PENABLE 全部关键语义的 SVA APB 断言清单,每一条都解释「防的是什么 bug」。
本文要回答的问题
- APB 一笔传输的两个相位分别是什么,PSEL 和 PENABLE 的先后关系怎么用断言钉死?
- PREADY 拉低进入等待态时,哪些信号必须保持稳定,稳定性断言怎么写?
- PSLVERR 只在哪个时钟沿有效,为什么「全程检查 PSLVERR」是错误写法?
- 未知态(X)在协议信号上如何传播,X 检查断言放在哪一层?
- 这组断言该用 bind 挂在 DUT 里,还是放在独立 protocol checker 模块里?
一、APB 两相传输:先回忆协议骨架
APB 的一笔传输固定占两个相位。Setup 相(建立相):master 把 PSEL 拉高,同时给出 PADDR、PWRITE、PWDATA 等控制与数据信号,此时 PENABLE 必须为低。Access 相(访问相):下一拍 PENABLE 拉高,slave 在 PENABLE 为高且 PREADY 为高那一拍完成传输;PREADY 为低则进入等待态(wait state),Access 相顺延,直到 PREADY 拉高。

这张时序图是全套断言的对照基准:第 2 拍到第 4 拍之间是 Setup 相(PSEL 高、PENABLE 低),第 4 拍起进入 Access 相;中间 PREADY 拉低了两个周期,形成两个等待拍,直到第 8 拍 PREADY 重新拉高,传输在第 10 拍沿完成。所有 SVA APB 断言描述的都是这张图上的局部关系——谁的上升必须在谁之后、等待区间里谁不许动、完成沿上谁才有意义。和 APB 时序协议设计 对照看,协议条文里的每一句话基本都能翻译成一条并发断言。
写断言之前先把术语对齐:所谓「完成沿」,指 PSEL && PENABLE && PREADY 同时为高的那个采样沿——这是整笔传输唯一真正「成交」的时刻,PSLVERR 和读数据只在这个沿有效。
为什么选 APB 练手而不是直接上 AXI?原因有三。第一,APB 只有两个相位,信号一只手数得过来,断言与协议条文的对应关系一目了然,适合把「条文翻译成断言」这套方法论练熟;第二,APB 挂在几乎所有 SoC 的外设区间,寄存器读写都要经过它,验证投入复用率极高——一个 checker 模块绑十几个实例是常态;第三,APB 的坑很集中:等待态稳定、错误窗口、复位行为,抓住这三类就抓住了 APB 集成问题的大头。把这套思路吃透,再去看 AHB-APB 桥 这类跨时钟域协议转换的验证,只是信号多几组、断言多几条的事。
还要明确断言在验证体系里的分工:断言负责「协议合法性」——每一笔传输的形状对不对;scoreboard 负责「数据正确性」——写进去的值读出来对不对。两者不可互相替代。只写 scoreboard 的工程,遇到 master 在等待态偷换地址这种形状 bug,往往表现为偶发的数据比对失败,定位要翻半天波形;有了协议断言,第一笔畸形传输出现的当拍就会被点名。这也是为什么本文所有断言都聚焦形状与窗口,不去比对具体数据值。
二、相位关系断言:PSEL 与 PENABLE 的先后次序
最核心的一条规则:PENABLE 只能在 PSEL 已经拉高的下一拍拉高,且 PENABLE 拉高期间 PSEL 不许掉。拆开写是两条断言。
// A1: PENABLE 拉高时,PSEL 必须已经为高
a_penable_needs_psel: assert property (
@(posedge PCLK) disable iff (!PRESETn)
PENABLE |-> PSEL
) else $error("PENABLE high without PSEL");
// A2: PSEL 拉高后的下一拍,PENABLE 必须拉高(Setup 相只占一拍)
a_psel_then_penable: assert property (
@(posedge PCLK) disable iff (!PRESETn)
$rose(PSEL) |=> PENABLE
) else $error("SETUP phase longer than one cycle");A1 防的是 slave 侧或桥接逻辑在未被选中时误动 PENABLE——常见于把多个外设的 PENABLE 用或门合并时漏了选通条件。A2 用的是非交叠蕴含 |=>:PSEL 上升沿的下一拍必须看到 PENABLE 为高。这一条钉死了 Setup 相长度恰为一拍;如果 master 实现有 bug 让 PSEL 悬空两拍才起 PENABLE,A2 立刻报警。这两条正好对应「两相」的字面含义:建立一拍、访问若干拍,建立相不许拉长。
蕴含方向是这里的高频失分点。有人会把 A1 写成 PSEL |-> PENABLE,这表达的是「选中就必须立即进入访问相」,把 Setup 相本身也判成了违规—— Setup 相恰恰是 PSEL 高而 PENABLE 低的那一拍。写相位断言时先问自己:这个信号「为高」是充分条件还是必要条件?PSEL 高只是 PENABLE 高的必要前提,所以蕴含箭头从 PENABLE 指向 PSEL,而不是反过来。同理,A2 的先行算子用的是 $rose(PSEL) 而不是 PSEL:如果写 PSEL |=> PENABLE,那么 Access 相里每一个 PREADY 为低的等待拍也满足先行条件,断言语义就变成「PSEL 高蕴含下一拍 PENABLE 高」——这在等待态里碰巧也成立,看似无害,但它掩盖不了 Setup 相被拉长两拍的场景吗?其实能,因为第二拍 PSEL 仍高、PENABLE 却还没起,先行条件成立、后继失败,照样报警。两种写法在这条上殊途同归,但 $rose 版本意图更直白,失败报告里的先行时刻也更容易对应到波形上的 Setup 相起点,工程上推荐后者。
背靠背传输是另一个要确认的边角:前一笔完成沿的下一拍,PSEL 可能保持为高直接进入下一笔的 Setup 相(APB 允许连续传输中 PSEL 不掉)。此时 $rose(PSEL) 不会触发,A2 不要求这一拍后紧跟 PENABLE——这是对的,因为连续传输的第二笔也有自己的 Setup 相,PENABLE 必须先掉回低再拉高。所以套件里还隐含一条规则:PENABLE 在两笔传输之间必须至少低一拍,这条用 PENABLE && !PREADY 之外的表述写就是「PENABLE 掉低的当拍要么传输已完成、要么本来就空闲」,作为 A3 稳定区间的自然推论通常不单列,团队约定严格时可以补一条 $fell(PENABLE) |-> $past(PREADY) || !$past(PSEL)。

状态图里的自环就是等待态,也是下一节稳定性断言的作用区间。注意图中没有「PENABLE 高而 PSEL 低」这条边——它在协议里根本不存在,A1 就是用来保证任何实现都不会误入这个非法状态。
三、PREADY 等待态:控制信号的稳定性断言
进入等待态(PSEL && PENABLE && !PREADY)后,APB 协议要求 master 保持所有控制信号与写数据不变,直到传输完成。这是整套 SVA APB 断言里最容易漏、又最能抓 bug 的一类:很多 master 状态机在等待拍里「顺手」更新了地址或写数据,slave 在最后一个等待拍采到的就是污染后的值。
// A3: 等待态中 PADDR / PWRITE / PWDATA 必须保持稳定
a_stable_during_wait: assert property (
@(posedge PCLK) disable iff (!PRESETn)
(PSEL && PENABLE && !PREADY) |=>
$stable(PADDR) && $stable(PWRITE) &&
(!PWRITE || $stable(PWDATA))
) else $error("control/write data changed during wait state");这条用 $stable 而不是逐信号写 == $past(...),语义更紧凑。条件里的 !PWRITE || 是常见写法:读传输时 PWDATA 无意义,不纳入稳定检查。如果项目里 PPROT/PSTRB(APB4 引入的保护与字节选通信号,见 APB5 PPB 与 PPROT)在用,同样并入稳定性表达式。
为什么稳定性违规这么难缠?因为多数 slave 只在完成沿采样总线,前面等待拍里信号翻过几次跟头,只要最后拍值对了,功能仿真就一切正常——bug 被 slave 的采样时刻掩盖了。但同一个 master 换接到另一个「在等待拍内预译码」的 slave(比如地址一到就先启动内部存储器译码的低功耗设计),间歇性写错地址的事故就来了。稳定性断言的价值正在于它不依赖任何具体 slave 的行为,直接对总线形状下判决:等待区间里动了不该动的信号,无论有没有造成功能错误,都算违规。这类「现在没炸、换了对接方才炸」的集成隐患,是协议断言相对 scoreboard 的独特贡献。
实测中还有一类灰色情况:某些 master IP 在等待态更新 PWDATA 的未使用高位(字节选通之外的填充位)。严格按协议这也算违规,但撞上成熟 IP 时去改 RTL 不现实。处理方式是把稳定性表达式收窄到「有效位」,例如 PWRITE |-> $stable(PWDATA & strb_mask),或者在 checker 上加一个宽限参数。这类 waiver 必须写在代码注释里说明理由,而不是悄悄删断言——断言松一比特,以后所有人都要猜这一比特为什么松。
还有一条配套断言约束 slave:PREADY 不允许在 PSEL 未生效时随意拉高——虽然多数实现里这无害,但限制住它能让覆盖率与断言环境更干净,也能逼出 slave 把 PREADY 错接到其他片选上的连线 bug。
// A4: PREADY 只在 Access 相内才有意义
a_pready_only_in_access: assert property (
@(posedge PCLK) disable iff (!PRESETn)
PREADY |-> (PSEL && PENABLE)
) else $error("PREADY high outside access phase");A4 属于「收紧型」断言:协议没有明文禁止,但工程上 PREADY 在 Access 相之外应为低或无关。要不要启用按团队约定,启用前先在现有 RTL 上跑一轮空回归,确认没有历史遗留的违反。

对照上图,A3 的作用区间是 PENABLE 高、PREADY 低的那几拍:PADDR 必须钉在 A0 上。波形里一旦出现 PADDR 在等待拍跳变,断言失败信息会直接给出出错时刻,比肉眼扫波形快得多。
四、PSLVERR 语义:只在完成沿有效
PSLVERR 是 slave 报错信号,APB 规范明确:它只在传输完成的最后一个 Access 拍(即 PSEL && PENABLE && PREADY 同高那一拍)有效,其他时刻可为任意值。这个「只在完成沿有效」是写断言时最容易踩反的地方——新手常写成「PSLVERR 高时必须如何」的全程约束,结果被 slave 在等待拍输出的无关电平误触发。
正确的方向有两个:
// A5: 完成沿上 PSLVERR 不允许为 X
a_slverr_known_at_done: assert property (
@(posedge PCLK) disable iff (!PRESETn)
(PSEL && PENABLE && PREADY) |-> !$isunknown(PSLVERR)
) else $error("PSLVERR unknown at transfer completion");
// A6: 读数据只在无错完成沿有效(供 scoreboard 采样的前提)
a_rdata_known_on_read_ok: assert property (
@(posedge PCLK) disable iff (!PRESETn)
(PSEL && PENABLE && PREADY && !PWRITE && !PSLVERR)
|-> !$isunknown(PRDATA)
) else $error("read data unknown on successful read");A5 是硬约束:完成沿上 PSLVERR 必须是确定的 0 或 1。A6 则把「读成功」的三个条件写全——读传输、完成沿、无错误——只有同时满足,PRDATA 才必须已知;写传输或出错完成时,PRDATA 是无关值。这种条件写全的习惯比断言本身更重要:漏掉 !PSLVERR 这一支,一条正常的错误响应读也会被误报成 X 污染。

这张图把 PSLVERR 的「窗口语义」画清楚了:窗口外全是无关心区,窗口内必须先已知、再分支。断言和 scoreboard 的采样逻辑应当共享同一个完成沿表达式,两边各写一份容易在项目演进中漂移。
PSLVERR 还有一层系统语义容易被忽视:它是对「这一笔传输」的响应,不是外设的中断请求。一次写寄存器触发了外设内部错误,外设可以等到下一笔传输才报 PSLVERR——只要它报的确实是那笔的结果。验证侧因此不能在断言里写「写非法地址的当笔必须 PSLVERR=1」,那属于功能检查,应当放进 scoreboard 或定向用例里:断言只保证信号在正确窗口内已知、形状合法,「该报没报」是数据通路的事。这条边界划清楚,套件才不会随需求变动反复返工。
对读破坏(destructive read)类外设也一样:读 FIFO 状态寄存器这个动作本身会改变外设内部状态,PSLVERR 反映的是读操作是否被接受。断言环境不关心接受与否的业务含义,只关心完成沿上这一比特是 0 还是 1、不是 X。保持这个克制,checker 模块才能原封不动地绑到项目里每一个 APB 口上,无论对面是定时器、串口还是寄存器堆。
五、X 检查与复位:断言的边界条件
上文的 $isunknown 属于 X 检查断言。协议信号上的 X 通常意味着三态冲突、未初始化寄存器或复位释放时序问题——这些 bug 在功能仿真里经常被 X 传播的乐观/悲观差异掩盖,断言是最便宜的拦截手段。建议在完成沿对所有相关信号统一做一次已知性检查,配合 2-state/4-state 与 X/Z 传播 的机制理解,能定位到具体是哪一级逻辑把 X 放了进来。
X 检查有一个时机陷阱:复位刚释放的头几拍,总线可能还停在 X 上等待第一次传输,此时 PSEL 为低、没有任何相位断言的前件成立,套件自然沉默——这是正确行为,X 在空闲总线上不违规。真正要拦的是「传输进行中混进 X」:PSEL 自己就是 X 怎么办?此时 PSEL && PENABLE && PREADY 求值结果也是 X,蕴含前件不成立,断言照样沉默,X 就从指缝里漏过去了。补救办法是在 checker 里加一条兜底断言:PRESETn 释放后,PSEL、PENABLE 本身必须已知——!$isunknown({PSEL, PENABLE}) 作为常量式检查常驻,先把两个相位定序信号锚定在 0/1 世界里,其余断言的前件才有可靠的判别基础。这种「先断言判别信号已知、再断言协议关系」的分层思路,在任何总线的 checker 里都是第一行该写的东西。
复位方面,所有断言都带 disable iff (!PRESETn),这是 APB 断言的标配:复位期间 PSEL/PENABLE 可能处于初始化过渡,任何相位断言都不该在复位窗口内评估。更深入的细节——断言在复位释放当拍的行为、异步复位同步释放的采样点——见 disable iff 与复位处理。另外补一条复位专用的断言:复位期间 PSEL 与 PENABLE 必须为低,防止 master 在复位中误启动传输。
// A7: 复位期间不允许出现选通
a_idle_during_reset: assert property (
@(posedge PCLK)
!PRESETn |-> (!PSEL && !PENABLE)
) else $error("transfer active during reset");注意 A7 自己不带 disable iff,因为它的检查对象就是复位区间——这是套件里唯一故意反着写的一条,也是最常被照抄错的一条。
六、断言套件的组织与挂载
单条断言说完了,工程上更关心怎么组织。推荐做法:把这七条断言收进一个独立的 apb_protocol_checker 模块,端口就是 APB 信号全集,然后用 bind 挂到每个 APB 接口实例上。bind 方式与断言覆盖率收集的细节见 bind 与断言覆盖率方法;checker 与 monitor 的分工对照见 monitor 与 protocol checker。
module apb_protocol_checker (
input logic PCLK, PRESETn, PSEL, PENABLE, PREADY,
input logic PWRITE, PSLVERR,
input logic [31:0] PADDR, PWDATA, PRDATA
);
// A1..A7 如上
// 配套 cover:至少观察到一次等待态、一次错误响应
c_wait_state: cover property (
@(posedge PCLK) disable iff (!PRESETn)
PSEL && PENABLE && !PREADY);
c_error_resp: cover property (
@(posedge PCLK) disable iff (!PRESETn)
PSEL && PENABLE && PREADY && PSLVERR);
endmodule
bind apb_slave_dut apb_protocol_checker u_apb_chk (.*);末尾两条 cover 不是可选装饰:断言全绿可能只是因为激励根本没走到等待态和错误分支,cover 项让「断言被真正触发过」成为可统计的事实,与 代码覆盖与功能覆盖 的闭环思路一致。回归报告里应当同时看「断言 0 失败」和「关键 cover 100% 命中」,缺一个都不算收口。
bind 的目标选择也有讲究。挂在 DUT 内部每个外设的端口上,优点是 checker 实例随 RTL 例化自动铺开,新增外设零成本;挂在 testbench 的 interface 上,优点是层次干净、和激励侧同层。实践里两种都见过,关键是一处一策、全项目统一,最怕一半 bind 在 DUT、一半 bind 在 bench,覆盖合并时对不上号。如果 testbench 用了 interface 与 clocking block 组织信号,checker 直接消费 interface 里的信号即可,注意断言采样发生在 clocking block 定义的采样沿,两边时钟事件要对齐,否则会出现断言看到的值比 driver 晚一拍的幽灵失败。
最后估算一下这套套件的成本:七条断言加两条 cover,折合每条 APB 实例约九个并发断言线程,仿真开销在绝大多数回归里不可感知。而它挡下的问题——等待态偷换地址、错误窗口漂移、复位中误启动——每一个在硅后都是「无法复现的偶发错误」级别的灾难。这是芯片验证里投入产出比最高的一类代码,值得在每个项目的第一天就写好。
| 编号 | 检查内容 | 防的典型 bug |
|---|---|---|
| A1 | PENABLE 蕴含 PSEL | 片选合并逻辑漏选通 |
| A2 | PSEL 上升后一拍必起 PENABLE | Setup 相被拉长 |
| A3 | 等待态控制/写数据稳定 | master 等待拍污染地址数据 |
| A4 | PREADY 只在 Access 相 | slave 接错片选 |
| A5 | 完成沿 PSLVERR 已知 | 错误信号未初始化 |
| A6 | 无错读完成沿 PRDATA 已知 | 读通路 X 传播 |
| A7 | 复位期间无选通 | 复位中误启动传输 |
这一篇在知识体系里的位置
- 向上承接:SVA 断言实践 提供并发断言语法与方法论,本篇是它在 APB 上的完整落地。
- 协议背景:APB 等待态与 PENABLE 与 APB 时序协议设计 讲协议本身,本篇假设读者已懂两相传输。
- 横向相关:AHB-APB 桥 是这套断言最常挂的 DUT 之一;valid-ready 握手验证 是同类握手协议的断言对照写法。
- 落地延伸:APB 外设子系统 展示多个 APB 外设集成时 checker 的批量 bind 场景。
总结
SVA APB 断言套件的核心是把两相协议翻译成七条并发断言:A1/A2 钉死 PSEL 与 PENABLE 的相位次序,A3/A4 约束 PREADY 等待态里的信号稳定与归属,A5/A6 把 PSLVERR 与 PRDATA 的有效性精确锁定在完成沿,A7 反向守住复位窗口——配两条 cover 证明断言真的被触发过,再用 bind 挂进每个 APB 实例,这套检查器就能在任何项目里即插即用。