资讯动态

SoC验证环境设计:Stimulus与Checker职责分离的架构实践

发布时间:2026/10/21 10:17:18 来源:尧图企业网站定制
1. 为什么要把 Stimulus 和 Checker 拆开做 SoC 验证的人应该都有这种感觉验证环境写到后期最难受的不是功能点没覆盖到而是环境本身改不动了。一个新特性加进来牵一发而动全身激励生成和结果检查纠缠在一起修一个 bug 带出三个新问题。我早期做某个图像处理模块的验证时犯过一个特别典型的错误。当时为了图省事把激励发送和结果比对写在了同一个 driver 里激励里面携带的配置信息直接被 scoreboard 拿来做参考值两边共享同一份数据结构。项目前期跑得很爽到了覆盖率收敛阶段就开始痛苦了——每加一个异常场景我得同时改 driver、sequence、scoreboard 三个地方还经常因为参考值被激励逻辑“污染”而误报。后来整个环境重写才彻底想明白一个道理Stimulus 只负责“怎么把 DUT 置于某个状态”Checker 只负责“DUT 在这个状态下是否表现正确”两边中间只通过一条清晰的信息渠道通信。这个原则说起来一句话真正落地到架构里涉及的细节非常多。我写这篇东西的契机是某 SoC 项目的验证环境做了一次大重构核心主题就是“Stimulus / Checker 职责分离”。重构之后环境松绑了很多新场景的加入速度明显变快。这篇博文就把整套拆解思路、实现方案和踩过的坑完整整理出来给同样在跟验证环境搏斗的兄弟们做个参考。先说清楚这套思路适合什么人。如果你做的是 IP 级验证一个 agent 包打天下还能撑得住但一旦涉及到多个 IP 集成、软硬件协同、多个时钟域交互的场景Stimulus 和 Checker 缠在一起的环境必然成为瓶颈。如果你正准备从零搭建一个新的 SoC 验证环境那这套分离思路可以直接作为架构设计的起点。这里有一个容易混淆的点要先理顺。职责分离不是简单地把 driver 里的代码拆成两个文件或者把 scoreboard 单独拎出来类化这些都只是形式。真正的分离发生在数据流层面激励端产生的信息和检查端消费的信息必须是两套独立的、语义清晰的、接口稳定的数据契约。如果只是物理上拆了类、逻辑上还共享着一份全局配置那等于换汤不换药。另外一个常见的误解是有些人认为职责分离会增加代码量。实际上分离后的环境在初期确实会多一些封装和适配层的代码但收益在后半程。验证项目的生命周期里环境搭建只占三成工作量剩下七成都在加场景、跑回归、定位失败。分离架构把后七成里最痛苦的“改一处动全身”问题解决掉了这笔账怎么算都划算。2. 职责分离的边界到底画在哪2.1 先给 Stimulus 和 Checker 一个清晰的定义模糊的讨论没有意义先把概念钉死。我一般这样定义这两块Stimulus是环境中一切“主动改变 DUT 状态”的逻辑集合。它包含 sequence 的生成策略、driver 的时序驱动、配置寄存器的写入、中断的触发、以及各类总线上发起的传输事务。Stimulus 的核心目标是让 DUT 进入一个预期的状态至于这个状态之后会不会出问题它不关心。Checker是环境中一切“被动观察并判断正确性”的逻辑集合。它包含 reference model、scoreboard、assertion、protocol checker、覆盖率收集中的某些复查逻辑。Checker 的核心目标是判断 DUT 在某个状态下的表现是否符合预期至于这个状态是怎么来的它不关心。一句话总结协作关系Stimulus 负责提问Checker 负责批改。问题的内容和答案的标准必须分开产生否则就是自己出题自己判卷没有意义。2.2 三种信息流与一组控制流从数据流的角度来看环境里跑着三类信息职责分离的本质就是给这三类信息各开一条独立通道第一类是配置信息描述“这次测试要做什么”。它从 test 层向下流向 sequence 层再流向 driver最后作用到 DUT 上。配置信息同时也应该有一个副本流向 checker 侧让检查端知道“这次测试的预期是什么”。第二类是激励事务transaction描述“具体往总线上发了什么”。它从 sequence 生成经 driver 转成引脚时序在总线上传输。激励事务本身不是 checker 要检查的对象但它会间接决定 DUT 的输出。第三类是观测信息描述“DUT 实际做了什么”。它从 DUT 的接口上被 monitor 采集下来流向 checker。观测信息是唯一允许直接源于 DUT 的信息源。控制流是另一回事主要包括复位、时钟开关、仿真结束机制和覆盖率统计开关。控制流不应该被当成数据在环境主体里传递否则又会变成隐式耦合。我在重构某个数模混合项目时发现原有的环境完全没有这条通道概念。sequence 里直接 new 了一个 scoreboard 对象把激励内容塞进去当参考数据用scoreboard 反过来又会让 sequence 停住等待比较结果。这种双向依赖在调试一个问题的时候特别折磨人经常一个失败点要顺着调用链翻五六个文件。2.3 分离的判定标准单向依赖重构完成之后我总结出一个很简单的判定标准把环境图里 Stimulus 相关模块和 Checker 相关模块之间的所有依赖箭头画出来如果存在从 Checker 指向 Stimulus 的箭头说明分离没做干净。检查端可以知道“激励是什么”——通过配置信息通道这是合法的。检查端不能要求激励端“再发一遍”或者“等一下再跑”这会引入控制流反向依赖。反过来激励端也不应该读取检查端的比较结果来决定下一步行为。仿真的进程控制应该由 test 级的调度统一管理。这个判定标准有一个看起来很绕但实际很实用的操作方式在一个测试用例跑完之前试着把整个代码库里所有 checker 相关的文件删除重新编译。如果编译能通过说明分离合格。挑一个用例试一次就知道咬合有多紧我当时重构完做这个试验改了十几个只读引用才算通过。2.4 分离到什么粒度才算合适边界的粒度直接决定重构的工作量。我的经验是分三层来拆最粗的一层面向测试场景的分离。典型场景是低功耗测试激励端负责唤醒事件序列的生成checker 需要验证唤醒后的寄存器状态和时钟行为。场景级的分离意味着一个测试用例可以被两个环境复用激励环境跑唤醒检查环境跑状态确认。中间一层面向协议模型的分离。以总线协议为例driver 负责按协议时序把 transaction 发出去protocol monitor 负责采集总线行为并检查时序违规这两者的边界在协议标准上是天然清晰的。把协议解析逻辑单独做成 monitor而不是塞进 driver后面接新协议扩展时特别有用。最细一层面向数据语义的分离。激励端的 transaction 里带了 CRC 字段driver 在发送前计算并填充checker 端从总线上解析出报文 CRC 后独立计算核对。这种检查本质上是对数据语义的验证必须在 checker 侧有独立的实现绝不能用激励端的计算结果来比对。粒度的选择要和团队实际情况匹配。我见过一个项目把分离做得过于极端每一个字段都独立一个类结果环境里类的数量超过用例数量管理成本反而高过耦合时的状态。合理的做法是先保证数据通道独立再考虑类的粒度通道干净了类怎么拆都是后话。3. 数据面上的分离实现激励端配置生成3.1 用一张配置表统一描述“这次测试要做什么”实现职责分离的第一步是给激励端设计一张测试配置表scenario config。这张表不关心 DUT 内部结构它只是描述测试意图的标准格式。我用 SystemVerilog 的 class 来承载放在 env 包的一个独立头文件里Stimulus 侧和 Checker 侧各自 include但只读不改。关键做法是config 对象在 test 层创建并“冻结”之后只允许被读取不允许被修改。这个约定靠注释和 code review 来维护虽然没有编译器强制但一旦约定被破坏checker 侧拿到的预期值就可能和实际激励不一致这是最隐蔽的一类 bug。// scenario_config.svh class scenario_config extends uvm_object; rand int unsigned num_bursts; rand bit [7:0] payload_seed; rand burst_kind_e burst_kind; bit [31:0] base_address; bit check_payload_crc; bit check_response_timeout; uvm_object_utils_begin(scenario_config) uvm_field_int(num_bursts, UVM_ALL_ON) uvm_field_int(payload_seed, UVM_ALL_ON) uvm_field_enum(burst_kind_e, burst_kind, UVM_ALL_ON) uvm_field_int(base_address, UVM_ALL_ON) uvm_field_int(check_payload_crc, UVM_ALL_ON) uvm_field_int(check_response_timeout, UVM_ALL_ON) uvm_object_utils_end endclass这里的 payload_seed 很有意思。激励端用它来生成随机数据Checker 端只需要拿到这个 seed 值配合相同的随机算法就能独立生成一份“期望数据”而不需要激励端把每条 burst 的全部 payload 都发给 checker。这在数据量大的场景下能省掉大量存储和参数传递。3.2 激励端如何消费这张配置表激励端拿到 scenario_config 之后只负责把它“翻译”成具体的 sequence 和 transaction。这里有个核心技巧sequence 从配置表里取值而不是从 sequence 基类的约束里硬编码。早期我看到很多 sequence 直接把测试参数写在约束块里比如constraint c { num_bursts 32; }一旦要改成 64就得新建一个 sequence 子类。配置表方案的逻辑是反过来的——sequence 保持通用让 config 来决定参数。测试用例只需要 new 一个 config、把参数随机化两次、然后固定下来序列层自然就生成了对应的场景。class my_test extends uvm_test; virtual function void build_phase(uvm_phase phase); scenario_config cfg scenario_config::type_id::create(cfg); cfg.num_bursts 32; cfg.payload_seed 100; cfg.burst_kind BURST_CRC_ERROR; cfg.base_address 32h8000_0000; // 关键一步创建激励端的 sequence 时把 config 的句柄传进去 // 但 sequence 只能读不能改 my_sequence seq my_sequence::type_id::create(seq); seq.cfg cfg; // 同时把同一份 config 挂在顶层 env 里供所有 checker 访问 uvm_config_db#(scenario_config)::set(this, env.*, scenario_cfg, cfg); endfunction endclass激励端的 driver 拿到 transaction 之后就只做一件事按照协议时序把数据放到总线上。它不知道也不关心这些数据到了 DUT 之后会怎么样。这样 driver 就变成了一个可复用的协议引擎换个 DUT 项目driver 基本不用动。3.3 配置缓冲队列与后门写寄存器激励端还有一类容易被忽略的职责——配置寄存器的写入。在 SoC 验证里很多场景需要先配置 DUT 的寄存器再发起数据传输。寄存器配置写错了后面的所有行为都是空中楼阁。这个环节容易跟 checker 纠缠。寄存器值本身既是激励被写入 DUT又是预期checker 需要知道寄存器里现在是什么值才能判断 DUT 行为。我的做法是维护一个独立的配置跟踪队列写在 env 层激励端的 reg write sequence 每写一次就往队列里 push 一条记录checker 端通过队列查询当前寄存器值。// config_tracker.svh class config_tracker extends uvm_component; // 队列里存的是 (地址, 数据, 写入时刻) protected config_item queue[$]; function void push_back(bit [31:0] addr, bit [31:0] data, time t); config_item item new(); item.addr addr; item.data data; item.write_time t; queue.push_back(item); endfunction function bit [31:0] read_reg(bit [31:0] addr, time t); // 返回在 t 时刻该寄存器的最新值 foreach (queue[i]) begin if (queue[i].addr addr queue[i].write_time t) begin rdata queue[i].data; end end endfunction endclass这个队列有个很大的好处它天然支持了时序回溯。比如我们在验证中要检查“某个信号 D 在寄存器配置之后 10 个时钟内变高”checker 可以直接查 t-10ns 时刻的配置值不用依赖激励端为每个检查点单独传参。4. 检查端的模块化拆分与对比策略4.1 检查端的三件套期望生成、行为采集、对比判定分离架构下检查端本身也需要内部再拆三个角色期望生成reference model接收 scenario_config配合从配置跟踪队列里读到的寄存器值生成“DUT 在这种状态下应该输出什么”的期望数据。行为采集monitor监听 DUT 的真实输出格式化后放在一个标准接口上。对比判定checker拉取两侧数据进行逐字段核对。这里有一个原则必须守住期望生成不允许读取激励端中间变量的值。比如激励端计算了一个 CRC 放在 transaction 的某个字段里期望生成必须自己从头算一遍哪怕算出来的结果一模一样也不能直接拿激励端的字段。一旦形成“期望值直接抄激励值”的习惯事务级别的错误就会被系统性掩盖这正是职责分离最忌讳的情况。我见过的最极端的抄近路写法是激励端构造一个带错误 CRC 的报文期望生成直接用这个错误字段当期望值结果 DUT 的纠错逻辑没有触发任何报警整个用例形同虚设。正确做法是激励端故意填错 CRC期望生成算的是“正确 CRC”checker 对比 DUT 的纠错结果到底是正确值还是错误值。4.2 按接口切片而不是按功能切片SoC 的 checker 不能只做一个巨型 scoreboard把所有接口的数据全倒进去再比。这个做法在接口少、数据量小的 IP 验证里还能用到 SoC 级就会因为时序域差异和调度不确定性导致海量误报。我重构时的做法是按接口切片每一个标准接口AXI、APB、中断线、流接口配一个独立的 monitor checker 对各自检查各自的协议和数据。跨接口的交互行为单独设计一个顶层的 cross-checker 来负责。比如基带模块的验证环境我当时同时接了 AXI 配置口和流数据口。AXI 侧的 checker 只管配置写入是否符合协议、寄存器值是否正确流数据口的 checker 管数据流的格式、长度和 CRC。两者之间唯一共享的信息是 scenario_config 里的数据长度字段不存在直接的数据耦合。后来项目加了一个新的流接口工作量比预想少了一半就是切片的收益。4.3 集合比较和有序比较的取舍数据对比里最容易忽略的细节是顺序是否需要严格一致。有些 DUT 处理完数据之后会把结果缓存起来在某个时刻集中输出输出顺序可能和输入顺序不完全一致。这种情况下 checker 如果坚持有序比较会产生大量假失败。我的标准做法是在期望生成阶段就明确比较模式并把模式写进 scenario_config严格有序模式用于协议顺序有强约束的场景无序集合模式用于数据并行处理、输出顺序无关的场景窗口滑动模式用于有时延但整体有序的场景。无序集合比对在 SystemVerilog 里用关联数组加计数就能高效实现不需要遍历整个队列。我把待匹配的期望数据建成一个 hash bagmonitor 每采到一个结果就从 bag 里扣除一个全部扣完且无剩余即通过。这个思路在实际实现里跑得很快也解决了重复数据导致的配对问题。// checker_core.svh无序比对核心 class unordered_checker extends uvm_component; protected int unsigned expect_bag[string]; function void push_expect(string key); if (expect_bag.exists(key)) expect_bag[key]; else expect_bag[key] 1; endfunction function bit try_match(string key); if (!expect_bag.exists(key) || expect_bag[key] 0) return 0; expect_bag[key]--; return 1; endfunction endclass这个按 key 计数的做法对大量数据的处理效果很好。数据流接口上来一个报文提取关键字段拼成 key直接 try_match耗时是 O(1) 量级对比传统的队列里逐条 find 性能差距巨大。5. 检查端的关键校验点协议、数据、配置、时序5.1 协议级检查协议检查的对象是总线信号层面的时序规范跟 DUT 的功能正确性没有直接关系。地址是否在相位上稳定、读写信号不能同时拉高、burst 长度是否符合协议限制这些都归协议 monitor 管。一个常见的错误是把协议检查放在 driver 里做。driver 在激励发送阶段自己已经保证了时序的合法性如果它自己也做检查等于用同一个逻辑既当运动员又当裁判抓不到自己写时序时埋下的错误。所以在我的环境里driver 只管“怎么发”协议 monitor 独立采集总线波形、独立做检查两边各看各的。协议 checker 用 SystemVerilog assertion 来写是最顺手的。SVA 天然适合描述周期相关的时序属性配合$rose、$fell、##运算符能比较简洁地表达时序要求。这个部分一定要独立成文件不要混在 scoreboard 里因为 assertion 的调试方式、触发时机跟数据比较完全不同混在一起时一个时序断言失败会干扰数据比较的排查。5.2 数据级检查数据级检查是 scoreboard 的主战场主要验证 DUT 的输出数据和期望数据是否一致。数据级检查最常见的坑是参考模型和 DUT 使用的舍入规则不同。我做过一个图像缩放模块DUT 内部用的是定点运算参考模型如果用浮点算完再截断两者的边界值偶尔会差 1。这种“差 1”不是 bug是参考模型不够精确。解决方法是把参考模型的数据通路精度和位宽对齐到 DUT甚至在算法层面用完全相同的定点格式重新实现一遍。这个工作量不小但如果不做所有边界点都会变成“可接受差异到不可接受差异”之间的模糊地带非常烦人。我的经验是参考模型宁可实现得慢也要严格对齐 DUT 的算法精度为的是拿到干净的比较结果这比把参考模型优化到跟 DUT 一样快更有意义。数据级检查还要特别注意不定态。功能仿真的早期阶段时序没完全收敛时DUT 的输出出现 X 态是常事。一个负责任的 scoreboard 会在数据比较前先检查数据是否有效而不是直接拿 X 值和期望值比。否则一次未复位状态的仿真会刷出几百个重复的误报把真正有价值的失败淹没在噪声里。5.3 配置级检查配置级检查检查的是寄存器写入后 DUT 行为是否符合配置意图。这个层面最容易验证不到位因为它要求 checker 理解“配置和硬件行为之间的因果关系”。实现上我用一个配置回调机制config_tracker 里可以注册一组回调函数当特定寄存器被写入特定值时触发对应的事件检查。比如低功耗模式的开关寄存器一旦写入使能checker 就应该在接下来的若干个时钟周期内观察到时钟关断信号。这种关联不可能靠通用 scoreboard 来做得靠场景专用的配置检查器。// power_checker.svh配置触发式检查 class power_checker extends uvm_component; function void init_tracker(config_tracker tracker); tracker.register_callback(32h4A, 1b1, this.write_enable_cb()); endfunction task write_enable_cb(); // 写入使能后 100us 内必须看到 clock_en 拉低 (posedge vif.clock_en iff vif.clock_en 0); // 断言或显式计数均可 endtask endclass配置级检查还有一个优先级规则一位配置字段可能同时影响多个功能点checker 在组织检查项时应该按“配置事件”维度组织而不是按“功能点”维度组织。否则同一个配置写入会导致多个检查器收到通知但各自的判断逻辑又无法确定“这个功能点到底该不该在这个配置下被检查”最终只能被迫接受所有配置为检查的充分条件造成假失败。5.4 时序检查时序检查在 SoC 层面是最容易“查漏”也最容易“误杀”的部分。查漏是因为 SoC 的时序关系往往跨越多个时钟域单看一个接口的断言无法发现问题误杀是因为仿真环境里时钟树的建模不完整实际芯片能工作的时序在仿真里可能略有偏差。我对时序检查的经验是先定尺度只有协议规范里明确的、可量化的时序约束才写入断言比如“读数据必须在读响应后 1-4 拍内有效”。那些模糊的“尽量在若干拍内完成”的描述不要写成硬断言改成覆盖率属性来观察避免误杀。时序断言尽量放在接口边界上不要深入 DUT 内部去检查内部节点时序内部时序检查应该留给 gate-level 仿真RTL 级做功能验证时插得太多只会拖慢仿真速度。跨时钟域的检查则必须显式地统一时间基准。我的做法是给 checker 里所有和纳秒时间相关的参数统一使用绝对时间变量在 test 层传入时钟频率checker 计算时用频率参数来换算而不是硬编码一个周期数。这样同一个 checker 既能跑在 200MHz 的配置下也能跑在 400MHz 的配置下。6. 实操中的三个关键决策点6.1 决策点一激励端的数据类型与 checker 端的数据类型是否要共用基类这个问题的答案不是绝对的它取决于你的项目是否需要激励数据和期望数据在字段级别上共享定义。如果共用基类可以少写很多字段复制逻辑但容易形成隐式耦合激励端改了字段名checker 端跟着编译报错虽然能通过编译器提醒来定位可语义上就失去了解耦的意义。我的实际建议是共用公共字段定义但不共用完整 transaction 类型。拿总线写请求举例地址、数据、突发长度的定义可以放在一个公共包 reducer_pkg 里激励端的 write_req 和 checker 端的 expected_write_req 各自继承或者包含这些公共字段但各自增加私有信息。这样字段定义可以统一更新而两边的行为建模是隔离的。这个折中在大型 SoC 环境里操作性最强。6.2 决策点二参考模型应该放在事务级还是行为级参考模型放在事务级还是行为级取决于 checker 需要验证的是 DUT 的接口行为还是数据算法。事务级参考模型输入的是“一帧数据”输出的是“处理之后的帧”比较的对象是接口语义行为级参考模型则要求模拟 DUT 内部的周期级行为输出的是“在哪个周期哪个信号变成了什么值”。SoC 验证和 IP 验证的需求是不一样的。SoC 验证一般不需要周期级模型因为整个芯片的数据通路行为本来就是乱序、多流水、跨时钟域的参考模型做一个事务级的功能镜像就足够检查时放宽到有序比对或窗口模式。真正需要在周期级较劲的是那些单一 IP、时序严格、输出窗口很短的类型这时候才需要行为级参考模型。我的底线是事务级模型优先除非有明确的周期级检查需求才升级为行为级模型。行为级模型写起来难度大、调试难、对仿真性能影响也大为了一两个边界序贯行为引入一整条周期级链路通常不值得。6.3 决策点三比较的触发是由 monitor 采集驱动还是由 checker 轮询发起比较逻辑的调度方式对误报率影响巨大。我有过一个项目scoreboard 里写了一个 forever 循环每 10 个仿真纳秒扫描一次输出队列。当时它既然能过就没在意后来系统时钟改成 50MHz扫描周期没改结果大量事件漏采误报一堆。稳妥的做法是事件驱动monitor 在总线上采到一个完整事务后通过 TLM 端口把事务推给 checkerchecker 收到一个事务就开始一次比较。这种 push 模型不依赖时间粒度天然与事务边界对齐漏采问题基本绝迹。只有那些“在某个时间窗口内应该没有活动”的检查比如看门狗超时才在 check 阶段用显式的延时等待来实现。7. 验证环境重构的效果与踩坑实录7.1 重构结果三个指标的真实变化重构完成后我记录了三个数据对比重构前和重构后的表现第一个指标是新增用例的平均耗时。之前加一个需要新配置加新数据格式的用例平均开发时间是 3 到 4 天重构后同样的工作压缩到 1.5 天左右。原因很直观新增用例时只需要在 test 层构造 scenario_config 并挂到 env 上激励端和检查端都已经按 config 驱动了不需要动环境主体代码。第二个指标是失败定位的平均耗时。之前一个随机种子失败定位时间动辄五六个小时因为需要同时看激励日志、scoreboard 日志、断言日志而这三种日志并没有时间对齐。重构后checker 在比较失败时会在 log 里打出“期望值来源”“采集时刻”“关联的 config 指纹”经常两个小时甚至几十分钟就定位到问题模块。第三个指标是回归稳定性。之前每个版本的回归都会有几个偶发的“flaky fail”复跑一次就过但查不出原因。重构后这类问题下降了很多主要原因是 checker 不再依赖激励端的中间变量采集到的信号是干净的 DUT 行为数据不存在跨模块的时间竞争。7.2 踩坑实录一剪不断理还乱的 reference 依赖第一次做分离时我把 motivation 做得太激进把所有 checker 依赖的公共包全部拆成独立文件结果 include 顺序一团糟编译报了几百个 unresolved reference花了一整天整理头文件依赖。当时我才反应过来职责分离的核心是数据流分离不是代码文件分离。适当保留公共包是合理的只要在数据层面每个模块的读和写是清楚的就行。之后我归纳了一个简单的编译依赖规则公共底层配置类、枚举、公共字段放最底层激励端和检查端分别 include 公共底层但彼此之间禁止 include。这个规则通过编译器和 CI 检查来约束保证单向依赖基本杜绝了类文件层面的隐式耦合。7.3 踩坑实录二checker 的期望数据被主动修改重构后有一段时间发现某些用例的误报率反复波动排查了很久发现是一段历史遗留代码在 checker 对象初始化时从某个 sequence 里拉取了一个句柄然后直接修改了期望队列里的数据。虽然代码写得很隐蔽但它本质上是让检查端“听从”激励端破坏了双方独立的判断。这种问题在单人维护环境时不容易出现但多人协作时风险很大。我的建议是checker 内部队列的数据结构声明为 protected对外只提供查询和匹配操作不提供修改接口。编译期就能挡住大部分越权操作。7.4 踩坑实录三覆盖率收集与 checker 的边界覆盖率收集虽然不属于 Stimulus 也不属于 Checker但它和两侧都有关系。覆盖率的“触发”来自激励端产生的场景“判定”依赖 checker 是否发生关键事件。重构时一度把覆盖率组挂在了 scoreboard 里面后来发现覆盖率数据会随比较窗口的变化而丢失不好回溯。最终我把覆盖率独立成第三类模块挂在整个 env 的顶层。它只通过 config 和 monitor 的 TLM 端口拿数据和两侧都不产生控制依赖。这样覆盖率回归时的可重复性明显提升调试覆盖率收集问题时也不会干扰 Stimulus / Checker 的正常运行。7.5 踩坑实录四跨时钟域的检查调度SoC 级别的 checker 最棘手的实际问题是跨时钟域的检查。一个接口的事件永远无法精确预测另一个时钟域中哪个周期会观察到该事件。强行用单一周期对齐比较必然导致大量时序差异引发的误报。处理这种场景我最后选择了绝对时间比较策略检查端先记录事件发生时刻$realtime比较时左右各留一个小的允许偏差窗口。偏差窗口的宽度要根据系统和接口规范来确定过优太紧会误杀。我一般先从规范约定的 ±5% 开始如果回归中确认了这个窗口漏掉了真实的违规再逐步收紧。另外跨时钟域的 comparison 必须显式地把两个事件的时间基准统一到同一个仿真绝对时间上很多误报的根源就是拿一个时钟域的相对相位去跟另一个时钟域比比错了基准也查不出来。写在最后的一点体会这套 Stimulus / Checker 职责分离的架构我前前后后在两代模拟与数字混合的 SoC 项目里应用和修正过。最大的感受是验证环境的核心不是代码写得多花哨而是信息流向清晰、依赖关系单向、修改局部化。一旦做到这三点环境就像一台组装良好的机器加一个新零件不再需要拆掉半个机箱。很多刚入行的同学以为验证的活就是把用例跑通、把覆盖率收敛到指标实际上真正的硬功夫在环境架构的未雨绸缪。你在项目中期感受到的每一次“这里怎么改不动”都大概率是早期职责边界没有划清楚的代价。最后再分享一个小技巧。如果你正准备重构旧环境别急着动手先把现有环境的所有跨模块数据访问画成一个清单然后问自己一个问题“这里的数据传递语义上是配置、是激励、还是观测” 把这三种信息流分开标识之后你会发现绝大多数重构的切分点会自然浮出来。这个准备工作花两个小时能省掉后面两周的返工。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑