
1. 先聊聊为什么 SoC 验证这么“磨人”干了十几年的芯片验证带过不少新人也踩过无数坑。经常有刚入行的朋友问我SoC 验证到底难在哪不就是写写 testbench、跑跑仿真、收集一下覆盖率吗说实话如果只是做一个小 IP 的模块级验证确实相对单纯。但一旦到了 SoC 级别光是把整个芯片“跑起来”这件事就够你喝一壶的。SoC 之所以叫 SoC是因为它把 CPU、总线、内存控制器、外设接口、安全模块、电源管理单元等一大堆东西集成在一颗芯片上。这意味着验证工程师面对的不再是一个简单的功能单元而是一个有着多核、多时钟域、多电源域、复杂互连的“微缩系统”。你要验证的不只是某个模块对不对而是这些模块组合在一起之后系统还能不能按预期去启动、运行、响应中断、切换电源状态、处理异常。这也是为什么不管是消费级的主控 SoC还是车规级、工业级的 SoC验证周期往往占整个芯片开发周期的一半以上。这篇手册就是写给在 SoC 验证这条路上摸爬滚打的同行们看的。不管你是刚接手 SoC 级验证的新人还是已经写过几年 UVM 平台、准备往 SoC 方向转的工程师或者是正在做 FPGA 原型验证、需要和验证团队打交道的设计工程师这篇文章里的内容应该都能给你一些参考。我会尽量多讲一些实操层面的东西包括验证策略怎么定、验证环境怎么搭、常见问题怎么排查也会聊一些书里写不到、但实际项目中一定会遇到的“潜规则”。2. SoC 验证的全局观策略永远比代码重要2.1 为什么 SoC 级验证不能照搬模块级那套打法很多人第一次做 SoC 验证的时候会很自然地把模块级验证的那套方法搬过来搭一个基于 UVM 的环境把接口协议跑通写一堆 sequence 去激励 DUT最后收覆盖率。这套东西在 IP 层面确实没问题但放在 SoC 层面你会发现一个很尴尬的问题SoC 的输入激励不应该是“你主动去驱动接口”而应该是“让芯片自己跑起来”。举一个很简单的例子验证一个 SPI 控制器 IP你可以通过 AHB 接口去配置它的寄存器然后往 FIFO 里塞数据观察 MOSI 输出是否符合协议。但在 SoC 层面这一切的主体应该是 CPU。你需要让 CPU 去执行一段程序通过总线配置 SPI 寄存器再由 CPU 发起数据传输。也就是说SoC 验证的激励源从“外部 testbench”变成了“内部软件”验证的重点也从“接口时序”变成了“系统行为”。这是一个思维上的巨大转变。所以做 SoC 验证首先要想清楚的不是怎么写代码而是怎么定策略。这个策略要回答几个问题验证的重点放哪里不同的测试场景用什么验证手段去覆盖跑仿真环境还是跑 FPGA 原型软件和硬件的验证边界在哪里这些问题想不清楚后面的环境搭得再漂亮也很难真正把 SoC 验证透。2.2 分层验证策略从 IP 到系统的三级火箭业内比较成熟的做法是把验证分成三个层级IP 级验证、子系统级验证和 SoC 级验证。这三个层级各有侧重不能互相替代。IP 级验证大家都很熟了重点是把单个 IP 的功能、协议、时序都验透。子系统级验证是近几年越来越受重视的一层。所谓子系统一般是指由多个 IP 通过总线或 NoC 组成的一个功能块比如 GPU 子系统、ISP 子系统、显示子系统、安全子系统等。这一层的价值在于它可以用比 SoC 级更快的仿真速度去验证多个 IP 之间的交互逻辑尤其是总线协议和低功耗接口的正确性。很多 SoC 级才会暴露的毛病其实在这一层就能抓出来。SoC 级则负责最终的系统集成和启动验证。到了这一层仿真速度会很慢所以测试场景必须精挑细选。一般的做法是把启动流程、中断响应、DDR 读写、外设访问这些“大场景”放在 SoC 级去跑而把具体的功能细节下放到 IP 级和子系统级去验证。这个思路很像写软件时的分层测试单元测试、集成测试、系统测试各司其职你总不能让每段代码都跑到系统测试那一层才去发现问题。2.3 覆盖率驱动闭合覆盖率不是终点而是起点覆盖率的提法是值得商榷的地方。很多团队把功能覆盖率、代码覆盖率跑到某个数字比如 90%当作验证的终点好像达到这个数字就可以 sign off 了。但实际上覆盖率只是一个手段告诉你“哪些代码执行到了、哪些功能点被刺激到了”它并不等于“验证通过”。我在实际项目里见过太多这种情况功能覆盖率数字很难看领导很焦虑于是大家开始写“凑覆盖率”的用例。就是在约束里稍微改一改随机种子或者加几个反向用例把那些没打到的仓洞给打过去。数字是好看了但漏测的问题还是漏在那里。覆盖率的意义是帮你发现“没想到的场景”而不是帮你证明“测完了”。正确的做法是每收集一轮覆盖率都去逐条审查那些没有覆盖到的点问一句“为什么没覆盖到是环境激励不够还是这个功能根本不会在这种配置下出现”如果答案是后者那这个 coverage hole 是可以接受的但你要在验证计划里写明原因。3. 核心验证场景拆解从启动到互连再到低功耗3.1 芯片启动流程验证SoC 验证的“第一课”SoC 验证里最基础也最重要的一个场景就是启动流程验证。你可以把芯片启动理解成一台电脑开机上电、复位释放、ROM 里的 BootROM 代码开始执行、初始化时钟和 DDR、从外部存储加载引导程序、跳转到应用程序。任何一个环节出问题整个芯片就“变砖”了所以这是所有 SoC 验证团队都会最先搭起来的一个场景。启动验证的难点不在于代码逻辑本身而在于你必须在仿真中准确地模拟出上电时序。现在的 SoC 通常有很多个电源域有的域先上电有的域后上电期间还要经过多次复位释放和时钟切换。仿真环境里要建模这些时序一般会用固定延时或者基于真实电源管理芯片模型的激励来驱动。我自己踩过一个坑仿真里启动测试总是失败查了整整两天最后发现是复位信号释放的时间点和时钟稳定之间差了那么几个毫秒而仿真里的时钟源没有在复位释放前完成稳定。这种问题如果不把上电时序建模清楚是极难定位的。另外要提醒的是启动验证一定要做到“自动检查”不能用眼睛去盯波形。判断启动是否成功的标准一般有两个一是 PC 指针是否跳转到了预期的地址空间二是某个关键外设比如 UART有没有输出预期的打印信息。比较务实的做法是在 testbench 里写一个 watchdog如果仿真时间超过某个阈值比如 200ms 仿真时间PC 还没有跳转到目标地址就自动报错终止仿真。这样跑回归的时候才能快速发现启动相关的 regressions。3.2 系统级总线与互连验证别让 NoC 成为“黑盒”总线互连是 SoC 里最容易出问题、也最难排查的地方。不管是传统的 AXI/AHB 总线矩阵还是近年来越来越流行的 NoC片上网络甚至是 TileLinkrocket chip/chisel 生态里常见的 SoC 互连协议这类开源生态里常用的互连方案它们在系统里的角色都是“快递公司”数据包从一个主设备出发经过若干路由节点最终到达目标从设备。验证互连本质上就是在验证这套快递系统在并发、拥塞、异常情况下的表现。互连验证最核心的手段有两个协议级断言和定向随机测试。协议级断言就是通过 protocol checker 实时监测总线上的协议违例比如 AXI 的 READY 和 VALID 握手规则、burst 是否跨越了边界、写响应通道有没有丢掉响应等。定向随机测试则是通过随机生成大量不同的传输序列覆盖不同的主从设备组合、不同的地址范围、不同的 burst 长度、不同的对齐方式来触发总线上的竞争和仲裁条件。这里特别想说一下 TileLink。这几年做开源 SoC 的人越来越多了Rocket Chip、Chipyard 生态里经常能看到 TileLink 的身影。TileLink 比起 AXI 多了一些很有意思的特性比如它支持 atomic operation 直接在总线层面广播还支持自定义的 per-agent 权限控制。如果你在用 TileLink断言验证的重要性会更突出因为这个协议对一致性的要求非常严格一旦出现两个主设备同时拿到同一块 cache line 的授权而彼此不知情后果就很严重。3.3 低功耗验证不跑 UPF 的 SoC 验证是不完整的低功耗验证是 SoC 验证里最容易被低估、也最让新人头疼的一块。我见过不少团队功能验证跑得风生水起一提到低功耗验证就说“我们还没有建好 UPF 环境”。但实际上现在功耗管理基本是所有 SoC 的标配电源域开关、DVFS、p-state 切换都是 P0 级需求。低功耗验证如果缺席芯片回来后大概率会碰上莫名其妙的漏电流、唤醒失败、数据丢失这类的问题。UPFUnified Power Format创建电源域和供电状态的过程听起来很简单但实际跑起来坑很多。最常见的坑是 isolation cell 的位置和使能时序不对导致电源关断之后模块的输出处于不确定状态把下游的模块给带崩了。这种问题在普通功能仿真里是看不出来的因为功能仿真默认所有信号都是上电状态。做低功耗验证的时候有一个小技巧很实用在仿真开始之前先跑一个“power-aware 检查”把 UPF 文件里声明的每个电源域都遍历一遍确认开关电源的宏模型都正常工作。另外低功耗验证的用例不能贪多关键是覆盖几个核心 scenario上电顺序、DMSDynamic Memory Switch或 Power Gating 等多种组合下的唤醒序列、以及唤醒过程中的中断响应是否正确。这些场景一旦在仿真里跑通了芯片回来之后睡下去能醒过来心里就踏实一大半。3.4 时钟复位与跨时钟域验证细枝末节里藏着的“大雷”时钟和复位看似是基础中的基础但每次芯片出问题概率很高的就是这块。SoC 里通常有几十个时钟域和几百条复位信号而且很多复位还是异步复位、同步释放的方式。跨时钟域CDC这个问题几乎每个项目都会遇到但又几乎每个项目都容易在紧张的时间表下被轻视。跨时钟域问题的本质是一个信号从一个时钟域传到另一个时钟域时目标域的触发器可能采到信号的中间态导致亚稳态。如果这个亚稳态没有被正确同步就会传播成逻辑错误。验证和检查 CDC 问题的思路是“预防为主检查为辅”。目前业内最有效的做法是用专门的 CDC 工具在 RTL 阶段做结构检查和仿真验证。简单来说就是把所有跨时钟域的路径列出来逐条确认有没有加同步器、同步器的类型是否合适、有没有经过单bit握手电路等等。跨时钟域验证有一个痛苦的地方如果你自己去写测试用例去“逼出”亚稳态效率极低因为亚稳态是一个概率事件。所以我更推荐在实际项目里把重点放在工具的结构检查上再用仿真验证去配合确认重要的数据跨时钟域路径。也就是说CDC 验证的本质是“静态检查 少量定向仿真”不要指望靠随机仿真来覆盖这一块。4. 验证环境与工具选型如何搭建一套跑得稳、调得快的 SoC 验证环境4.1 仿真器选型以团队沉淀和工具成熟度为先SoC 验证到底选哪家仿真器这个问题几乎每个新项目都会讨论一轮。商业仿真器像 Synopsys 的 VCS、Cadence 的 Xcelium、Siemens 的 Questa性能和协议支持通常做得比较好开源阵营里的 Verilator、Icarus Verilog 这几年进步也很大特别是 Verilator 在跑大 SoC 的时候编译出的仿真速度甚至能比部分商业工具快出不少。我的建议是选工具不能只看跑分要看团队已有的验证环境、VIP、脚本流程和员工熟练度。有团队对某个工具的编译报错信息有长期积累的排查经验换一个新工具可能一开始就能把人搞崩溃。另外现在很多开源生态里比如基于 chisel / spinalhdl / tlctl 的 SoC 项目Verilator 几乎是默认标配如果你做的是这一类芯片起步阶段直接用 Verilator 会顺畅很多。关键是把环境提前搭好做个简单的性能测试比如跑一个最小的 CPU 启动程序看看编译时间、仿真速度和 debug 体验是否符合预期。4.2 UVM 之外SoC 级验证的软件驱动环境怎么搭UVM 是模块级、子系统级验证的主流方法论这一点没什么好争论的。但在 SoC 级纯 UVM 环境往往会显得笨重因为你要处理的核心不是“接口激励”而是“软件程序”。所以SoC 级验证环境通常采用一种混合架构用 UVM 管好外部的物理接口比如 DDR、PCIe、USB用“虚拟外设”或“后门访问机制”提供 CPU 运行所需的代码和数据通过软件测试程序去驱动芯片内部逻辑。拿 DDR 验证举个例子。SoC 里的 CPU 要执行程序需要先把指令从 Flash 加载到 DDR或者直接 XIP就地执行。仿真环境里当然可以挂一个 DDR 的模型让 CPU 去初始化 SDRAM 控制器再完成读写。但这样整个仿真时间会很长。更高效的做法是用后门方式把测试程序直接写入到仿真的“伪 DDR”里跳过延时极长的 DDR training 流程直接让 CPU 取指执行。这一步做好能为你每天节省好几个小时的仿真时间。实际上很多 SoC 验证环境里跑的不是硬件 testbench而是一个“裸机测试程序”的集合。这些程序被交叉编译成 hex/bin加载进仿真模型里运行通过 UART 或者共享内存的方式回报执行结果。这本质上是“软件仿真 硬件模型”的 co-simulation 架构也是现在几乎所有 SoC 验证团队都会使用的方案。如果你所在的团队还没有建立起这样的流程建议优先考虑。4.3 回归自动化与持续集成让验证“无人值守”SoC 验证用例规模通常很大一次回归跑几千个测试都是家常便饭。如果没有一套好的回归管理和持续集成CI系统验证团队的日常就会被“手动跑仿真、手动分析失败、再手动重跑”占据效率低到爆炸。我在这次项目里就把回归系统接到了基础的 CI 平台无论是自建的 Jenkins 还是 GitLab CI每次代码提交后自动触发冒烟测试定期跑全量回归然后把结果汇总到 Dashboard 上。做回归自动化的时候有几个细节值得留意。一是失败用例要自动归档波形和日志这样第二天早上来你只需要看 Dashboard 上标红的用例直接点开波形就能定位问题。二是要做失败用例的自动分类区分是环境挂掉、编译失败还是真正的功能失败。区分环境问题和功能问题这个能力非常重要因为 SoC 仿真经常会出现超时、死锁、内存耗尽这类环境问题如果这些不稳定也统计进失败率会严重干扰你对代码质量的判断。三是回归报告的稳定性尽量做到“无白名单机制”不要允许某些用例一直失败还挂在 CI 里不然久而久之大家就对失败产生了“审美疲劳”真正的问题反而被忽略了。5. 问题排查技巧实录那些浪费过我几天几夜的问题5.1 仿真“死锁”与 CPU Hang先查时钟再查中断最后查总线SoC 仿真里最常见的现象不是功能输出错误而是整个仿真卡住不动了或者在某个断点之后 CPU 不再执行新指令。遇到这种问题我的排查顺序一般很固定首先确认时钟还在翻转没有因为某个门控逻辑被意外关闭导致整个模块停摆其次确认中断状态看看是不是有某个 pending 的中断没有被正确处理把 CPU 困在 ISR 里了最后再去看总线状态有没有可能发生了死锁比如某个 master 在等一个数据响应而响应永远发不回来。这里有一个非常实用的技巧仿真环境里一定要在 CPU 的总线上挂一个“指令追踪”模块或者至少在仿真日志里周期性地打印 PC 指针。这样 CPU 卡住的时候你能直接从日志里看到它是停在哪个地址、哪个函数里而不是去慢慢 debug 波形。没有这个工具的 SoC 验证环境遇到 CPU hang 问题就是抓瞎只能一点一点地看信号翻转到怀疑人生。5.2 复位释放的“隐藏依赖”为什么测试用例一拍脑袋就变慢有一次我在验证一个带安全岛的 SoC发现一个很诡异的现象同一个中断测试用例有时 10 秒仿真时间就完成了有时 50 秒都没跑完。查了很久才发现问题出在复位释放的“隐藏依赖”上这个 SoC 的应用程序代码在启动时需要读取安全芯片的哈希值而安全芯片的上电初始化比其他模块慢得多。如果测试用例在中途引入了某个异步事件导致安全芯片的初始化晚了一段时间后续的读操作就会陷入长时间的等待。这种问题在 UVM 环境里很难发现因为你需要同时跟踪“软件执行流程”和“硬件模块状态”两条线。排查下来的经验是遇到仿真时间突然变长的情况不要只盯着功能正确性先去看效率——哪一段代码执行的时间异常增加了。方法就是在软件测试用例里加上时间戳打印记录关键节点的仿真时间。有了这个你就能一眼看出程序卡在了哪个阶段再带着这个信息去查硬件状态命中率高得多。5.3 覆盖率空洞分析别急着“凑”先搞清楚是“不需要”还是“没想到”功能覆盖率收集完之后总要面对一堆覆盖空洞。新人最常见的做法是看到某个功能点没有被覆盖就飞快地写一个 sequence 去覆盖它。但更专业的做法是先想清楚这个功能点在当前架构下“应该不应该”被覆盖到。举个例子如果 SoC 里的一个 DMA 控制器只支持 32 位寻址但地址位宽在总线上扩展到了 40 位那么高 8 位的地址线从来不会置 1这个仓洞就是“本来就不需要”的情况不用花时间去填。反过来如果某个中断源的触发逻辑从来没能被真正触发过那就得小心了很可能是环境缺少一个能够真正发起该中断的外部事件模型。你需要在验证计划里注明原因而不是默默地让覆盖率数字“变好看”。每次覆盖率分析我都推荐做一次“覆盖率评审会”把 RTL 设计师、架构师和验证工程师拉在一起把覆盖率报告逐条过一遍。这个会可能有点费时间但价值非常高。设计师往往能一眼看出哪些仓洞是“不可能的”也能指出哪些仓洞暴露了他们自己心里也没底的功能点。6. 给验证新人的几句掏心窝的话写了这么多技术细节最后想结合自己这些年的经验给刚入行或者准备往 SoC 验证方向转的朋友们几句建议。第一句是不要只做“跑仿真的人”。你要去理解架构、理解设计意图、理解软件的执行流程。很多验证工程师入行头两年都在写 sequence、跑回归、看覆盖率技术能力确实在进步但对芯片整体的理解进步很慢。如果只盯着自己的 testbench不去看 RTL 里那段难懂的状态机到底想干嘛那十年后你还是在写 sequence。第二句是学会“设计验证环境”而不仅仅是“使用验证环境”。你会用 UVM不等于你会搭 UVM 环境。SoC 验证环境的复杂程度远超模块级很多项目里困难的部分不是测试用例本身而是环境里的各种组件怎么协同工作。花时间去理解 scoreboard 怎么实现自动比对、reference model 怎么建模、后门访问怎么保证数据一致性这些能力会在你将来带项目和解决疑难杂症的时候发挥巨大作用。第三句是一定要培养“全局排查”的思维方式。SoC 层面的 bug往往不是你负责的那一小块代码的问题而是多个模块之间的交互问题。当你定位一个问题时不要只看着某个信号在那里较劲要试着顺着数据流去思考这个信号从哪来经过了什么模块被谁消费了有没有可能是在源头就已经错了只是传播到这里才显现出来这种思维习惯是成为一个合格的 SoC 验证老兵最关键也最难跨越的一步。最后SoC 验证这条路确实辛苦项目紧张的时候经常要连轴转地查 bug、跑回归。但说真的当你在仿真里看到一个完整的系统从复位中苏醒、CPU 开始跑代码、外设开始响应中断、Linux 系统最终起来的那一刻那种成就感也是很多其他方向的工作很难带来的。把这个验证老兵的手册整理出来也算是对自己这些年踩坑和积累的一次回顾。如果里面的哪一段内容能让你在项目里少走几步弯路那这功夫就没白费。