☰
AI辅助RTL仿真调试:从日志清洗到根因定位的实践
2026/10/26 4:03:26 网站建设 项目流程

我先说这个工具是怎么来的。做过数字前端验证的朋友应该都有这种体会:每次 RTL 仿真挂了,先打开日志看Error/Fatal关键字,再拉波形都快要拉冒烟了,然后顺着信号一级一级地往回追,运气好的话很快能定位,运气不好那就是一上午光盯着那几条信号翻来覆去,最后发现根因在 TB 侧的一个 makefile 传参错误。这个“打开日志→分析波形→追踪连接→定位根因”的链路,每一步都是高度重复性的体力活,而且特别容易被时间线和信号层级误导。

我当时想的是把“解读日志、看波形、查信号连接”这三件事全部丢给 AI 去干,用一套流程化脚本把 RTL 仿真的现场数据整理成 AI 能直接理解的结构化描述,然后让 AI 给出 root cause 推断和验证建议。这个工具做出来之后,在我当前的验证环境下实测了大半个月,确实能把一次仿真失败的分析时间压缩到一个非常可用的程度,而且最直接的价值不是替代人去思考,而是把人从“翻日志、查波形”的低效检索里释放出来。

它适合谁呢?主要是还在用传统方式天天手点波形、眼睛扫日志的验证工程师,也适合刚接触 RTL 仿真、希望快速建立问题定位方法论的初级开发者,另外如果你正在做 IP 验证、SoC 集成验证,遇到跨模块信号交互出问题的情况,这类工具的思路也可以直接借鉴。下面我把整个工具从设计到落地的过程拆开讲,包含完整的结构设计、核心实现逻辑、我在实际使用中踩过的坑,以及那些你光看文档绝对学不到的排查心得。

1. 整体设计思路与方案选型

1.1 为什么选择“AI 对话式查询”而不是传统自动化脚本

刚开始我也考虑过两种非常常规的路线。第一种是写一套 Tcl 脚本直接对接仿真器的波形接口,自动定位到失败时刻,再往上游追踪驱动关系,把这个过程固化下来。这条路最大的问题是:不同仿真器的波形接口有差异,而且“根因”这个词本身是语义化的,它不等于“某个信号的某个时刻”,可能是一个初始化顺序问题,可能是跨时钟域握手没对齐,也可能是两个模块之间的接口协议理解不一致。纯脚本只能做“信号的逻辑追踪”,没法做“问题的语义归因”。

第二条路线是套用现成的 EDA 平台分析功能。说实话,商业工具里带的一些分析套件在“可视化”和“错误定位提示”上确实很强,但它们对使用环境的要求也高,而且很多时候它们分析的是标准化的断言失败或者统一日志格式,碰到我们项目里大量手写的display乱打、各个模块各自定义的错误码,这些套件就基本帮不上忙。

最后我选了“AI 对话式查询”的路线。核心思路是:环境的脏活累活(日志格式整理、波形数据降维、信号连接关系提取)全部靠脚本做干净,但最终判断完全交给自然语言交互的 AI 来做。这样做的好处是脱离了“每个项目写一套脚本”的困境。AI 能理解“为什么 A 模块的 ready 一直拉不高”这种含糊描述,也能同时结合时序波形和逻辑连接关系做交叉推断。它的上限取决于我对上下文信息的组织能力,而不是脚本里写了多少条 if-else。

1.2 功能模块拆解和它们之间的协作关系

工具的整体架构并不复杂,核心是四个模块:

模块分别是日志清洗模块、波形降维模块、连接关系提取模块和上下文编排模块。日志清洗模块负责把仿真运行产生的原始日志变成结构化的关键事件序列,这里有一个十分关键的取舍:不是所有日志都有保留价值,只有带时间戳、模块路径、严重级别和关键变量值的内容才会被提取,而且要用正则去匹配不同模块不同的打印风格。波形降维模块负责从 VCD / FSDB 这类波形文件里抽取目标信号在关键时间窗口内的跳变行为和数值变化,而不是把完整波形塞给 AI——那种做法既不现实也没有必要。连接关系提取模块读取设计源文件里的端口例化关系,生成一张模块级和信号级的连接图,交给 AI 时是一份邻接表式的描述。上下文编排模块是核心粘合剂,负责把前面三个模块的输出,按照 prompt 模板组合成一份“可被 AI 深度解析的现场报告”。

这四个模块之间是流水线关系,但做到了完全解耦,任意一个环节的输出格式有变动,其他模块通过配置项同步适配即可。我特意没有做重型的服务化工程,全程用 Python 脚本驱动,配置文件用 JSON,让整个工具集保持轻量。在验证环境里,你可以直接命令行调用,也可以在集成脚本里嵌为函数,灵活度高,不用为公司级流程做很重的改造。

1.3 方案选型的三个关键决策点

第一个决策是整个工具完全离线可用。在我们的工作环境中,仿真服务器往往没有外网访问能力,所以绝对不能依赖任何需要联网的 LLM 服务。我最终采用的方式是:用一套自托管的代码与文本模型作为底座,通过请求/响应方式调用,全部内网部署。这样既保证了数据安全,也保证了在断网条件下依然可以运行。如果你所在的环境允许联网,也可以替换成商用大模型 API,接口层是统一的,不影响上层的 prompt 编排逻辑。

第二个决策是尽量做到仿真器无关。业界常见的波形格式有 VCD / FSDB / FST 等,日志格式更是五花八门。我在工具里做了一层适配层,针对 FSDB 采用通用解析库,针对 VCD 直接用基础文本解析,针对不同仿真器的日志则靠正则模板去命中。用这种方式换仿真器时,只需要改配置,不需要改核心代码。

第三个决策是抽取过程必须做到可回溯。AI 给的结论并不可怕,可怕的是你无从判断它依据了什么。所以我把每一步抽取出来的中间文件全部落盘:日志关键事件表、波形采样段、信号连接列表,全都有单独文件保存,且带有原始行号或时间戳索引。这样 AI 给出的任何解读都能反查源头,确保分析过程真实可验证,而不是靠大模型在玩文字猜谜。

2. 核心实现与关键参数配置

2.1 日志模块:从 “人肉 grep” 到结构化事件流

日志清洗是整个工具最先跑的一步,也是最容易出“你觉得清洗干净了其实没有”的问题。RTL 仿真日志里的信息密级差别很大:有的模块打印很规范,每行都有[时间戳][模块][级别] 消息,这是最理想的格式;有的模块则是什么都没有,就是裸的display("%d", xxx),甚至一个模块不同代码作者打出来的风格完全不一样。如果直接拿这些原始文本来做 AI 的上下文,模型会被大量无效行干扰,尤其是一些周期性的轮询打印,会把真正有效的错误信息淹没掉。

我的解决办法是准备了一份“日志标注模板”,用正则表达式去匹配不同模式。匹配成功的内容按照[时间][源模块][事件类型][摘要信息]的格式整理进一个 JSONL 文件。比如针对 AXI 总线的仿真,就重点抓AWVALID、WVALID、BVALID等通道握手信息里的数据;针对中断控制的验证,就抓中断请求和中断号。对于匹配不上的行,不会直接丢弃,而是用不同的优先级标签保留下来,只是降低它们在上下文里的排序权重。

操作上你会需要定义一份custom_patterns.json配置文件,里面包含你要正则匹配的条目和对应的语义标签,我自己用的模板结构大致这样:

{ "patterns": [ { "name": "axi_handshake_ok", "regex": ".*(AWVALID|WVALID|BVALID).*(1|HIGH).*", "tag": "axi_handshake", "priority": 3 }, { "name": "error_fatal", "regex": ".*\\[(ERROR|FATAL)\\].*", "tag": "error", "priority": 10 } ] }

对应这个配置的日志清洗逻辑,实际上就是把原始文本逐行过一遍,命中error_fatal就按高优先级事件入列,命中axi_handshake_ok就整理成时序事件,最后输出成为按时间排序的事件列表。同时我会设计一个“上下文窗口”,默认数值是失败时刻前 200 行、失败时刻后 50 行,这个窗口范围可配置,因为有些 bug 是突然暴毙,有些 bug 是早期潜伏后期爆发,窗口太小抓不到源头。

这里给你一个实操上的关键词窗口配置思路:对于短时仿真(百万时钟周期以内),事件窗口可以拉大,直接取全部事件;对于长时仿真(千万时钟周期以上),取失败点前后各 200 行比较合适,多余部分会让 AI 出现“幻觉式关联”,把无关事件串成因果链。我一开始就因为行长时仿真时把所有事件都喂给 AI,结果 AI 给出了一个非常工整但完全错误的原因分析。

2.2 波形模块:把“百万级信号变化”压成“关键行为序列”

波形是整个调试过程里信息量最大的资产,但也是给 AI 用起来最难处理的东西。一个跑了几百万时钟周期的仿真,VCD 文件的体积可能到几个 GB,FSDB 稍好一些,但同样不是 AI 上下文能直接装得下的规模。我在这一步的设计里坚持“不直接传波形,传波形语义”。

波形降维模块的核心动作是采样和跳变压缩。首先根据日志模块里记录的失败时间点,锁定一个时间窗口(默认是失败点前 1000 个时钟周期,到失败点后 100 个时钟周期)。这个时间窗口是不是越长越好?并不是,太长的窗口会让关键信号的变化细节被淹没在大量的翻转里。

这个窗口范围内,每个目标信号只需要记录跳变沿时刻和跳变后的值。比如一个信号axi_awvalid,它不会每拍都变化,多数时候是一个恒定值,那它在这个时间窗口内可能就只有两三个关键时间点需要被记录。把这些关键时间点列出来,就构成了一个非常精简的“跳变行为表”。它不仅包含了信号在每个时刻的值,更重要的是天然包含了“这个信号到底是什么时候开始变的”,而这种时序信息对 root cause 分析极其关键。

我额外做了一步跨时钟域的边沿对齐处理。因为芯片验证里很多 bug 来自异步信号交互,原始波形里一个信号沿和另一个信号沿可能只错开了亚稳态窗口。纯文本描述很难体现这一点,所以我会在降维结果里自动添加一个“与目标时钟上升沿的对齐偏移量”标注,AI 在分析时就能区分“两个事件是完全同拍”——也就是同一时钟沿,以及“两个事件只差几皮秒”,从而意识到这里有异步风险。

操作上,我选了用 Python 的vcdvcd库做 VCD 解析,读入后按周期抽取目标信号在时钟上升沿的采样值,如果信号在这拍和上一拍不一样,就记录一次[时间][信号名][新值]。对于 FSDB 格式,解析难度就大不少,我是通过供应商提供的 C 接口做了转存,先转成中间文本格式再进同样的降维流程。工作量集中在接口封装上,核心逻辑是完全通用的。

2.3 连接关系模块:让 AI 知道“谁和谁是一伙的”

第三块连接关系提取看似简单,实际上它的作用是给 AI 建立“全局观”。root cause 分析里非常常见的一种情况是:从日志看错误是模块 B 报出来的,但实际上是因为模块 A 没有正确驱动某条信号,导致模块 B 在采样时得到一个非法值。如果没有连接关系做指引,AI 只能理解到“模块 B 出错了”,而无法知道模块 A 和模块 B 之间有什么样的交互关系。

连接关系提取模块直接读取 RTL 源码做静态分析,因为现在大家做的设计绝大多数都是 Verilog / SystemVerilog,所以用正则表达式匹配端口列表和模块例化是比较成熟的路线。具体来说,先把所有module块按名字建立索引,然后把所有instance块按例化名建立索引,再匹配端口名和信号名,最终生成一个 JSON 结构的关系表。这个关系表里,每一条记录的基本格式是[驱动模块.信号] -> [接收模块.信号],同时记录位宽信息。

如果是总线信号,比如 AXI 的awready、awvalid、awid,这些信号虽然从语法上是多个独立信号,但功能上是同一条总线的完整握手集合,我是把这些信号聚合成“AXI 写地址通道”作为一个语义组,再交给 AI 去理解。这样做了之后,AI 对协议级 bug 的判断明显更准确,它不是在孤立地分析awready拉不高,而是会结合awvalid这段时间一直为高、awid始终匹配等上下文,来判断这是一个“持续等待握手”而不是“突发错误”。

这里我踩过一个很典型的坑:连接关系的抽取结果和实际仿真行为不匹配。原因是设计里有大量的generate块,里面例化次数由宏定义控制,如果只用正则表达式做静态提取,会把实际未例化的路径也当成存在路径。解决方式是在连接关系表里增加一列“是否可能被条件例化”,这列的判断不是靠猜,而是结合编译选项和宏定义表去筛选。

2.4 上下文编排:操作里的“最后一百米”决定结果质量

模块到这里已经拿到了三类数据:结构化事件表、关键信号跳变序列、信号连接关系图。如果直接把这三份 json 丢给 AI,效果通常不好,因为 AI 无法理清楚先看哪一份,也做不到把三类信息在时间维度上对齐。上下文编排模块做的事情就是:我把所有信息整合成一份“案情汇报书”,让 AI 像一个刚接手案件的工程师一样快速进入状态。

这份“汇报书”在 prompt 层面大概是这个结构:先说明项目背景和设计规范,也就是这到底是个什么样的 IP,什么协议,时钟频率多少,复位方式是什么;然后把日志里的关键事件按时间顺序平铺出来,标注哪些是异常点;再给出故障附近时间窗内的波形行为描述,重点描述信号的跳变时间和值变化;最后附上连接关系,指出哪些信号之间有驱动关系。

最后在 prompt 里明确要求 AI 按这样的输出格式返回:先给一个 root cause 的初步结论,再写推断依据,依据要求精确到信号名和时间点,最后给出验证建议,就是设计一个什么样的定向用例或者加什么样的断言去证实这个推断。通过这种约束,AI 的回复基本是结构化且可验证的。如果只是简单说“帮我看看为什么挂”,它给出的结论会非常发散。

3. 实操过程与关键环节详解

3.1 环境准备与配置

你需要准备的软件环境很常规:一台能跑 Python 3.8+ 的服务器或者开发机,安装若干个 Python 库,包括vcdvcd用于解析 VCD 波形、pyyaml用于读取 YAML 格式的工程配置、requests用于向内网部署的模型服务发起 API 请求。整个工具集没有依赖任何商业 EDA 库,所以环境安装非常轻,大概几分钟搞定。

假设你希望在一套现有的 UVM 验证环境中接入这个调试工具,推荐的做法不是直接改造现有环境,而是把调试工具作为一个“事后分析”的独立流程嵌入在编译回归脚本中。仿真正跑挂了,回归脚本里先调用一次“日志事件抽取”,把结构化日志存到一个固定目录下的events.jsonl文件;如果有波形文件生成,就同步调用“波形降维”输出wave_events.json和wave_samples.json;当脚本收集齐了这些文件后,再自动启动“分析流程”,把结果合并并通过 API 发送给 AI。

配置环节中最容易遇到的问题就是“时间基准不一致”。仿真日志里的时间戳和波形文件里的时间戳,如果一个是仿真器全局绝对时间,另一个是从某个时间点开始的相对时间,两者对不上,后面 AI 分析出的时间线就是错乱的。我针对这个问题做了一个很笨但是很有效的处理:日志事件里检索到第一个Error时刻 T_error,波形降维窗口的中心点就用 T_error 来定位,并把波形采样的时间原点也归一到 T_error 上。这样即便存在时间偏移,至少错误点附近的对齐一定是正确的。

3.2 从一次实际仿真失败到 root cause 的完整走查

我拿一个实际发生的案例走一遍流程,方便你对整个过程有一个整体的感知。某次回归仿真,测试场景是一个总线桥接模块在连续读操作中偶发超时。日志上只有一句ERROR: timeout waiting for response,从大型日志文件里直接看,它连 RDATA 相关的信号一个都没有提到。传统分析方式要开波形,找到超时对应的时刻,再顺着读通道一路往回追,非常费时间。

用这个工具后第一步是运行日志抽取模块,出来的事件流显示:在T-1200左右,地址通道握手正常,写通道握手正常,读地址通道持续拉高,读数据通道的RVALID一直为低;在T-100时RVALID拉高了一个周期后立刻拉低,之后再也没有拉高。这个信息直接反映出“从设备给过一个响应,但只响应了一次”,而不是传统的“从设备完全没有响应”,这已经是一个很关键的过滤。

第二步是运行波形降维模块,针对故障窗口内的arvalid、arready、rvalid、rready四个信号做跳变压缩,结果显示rready在RVALID拉高后的下一个周期才拉高,这说明主设备在等到RVALID拉高之前并没有准备好接收数据。到这里,AI 推理的走向就开始明确了:可能不是从设备不响应,而是主设备在握手协议上的就绪信号拉高时机晚了。

第三步,连接关系模块输出了这两条信号上下游驱动关系。看到rready驱动的源头不是总线桥本身,而是用户逻辑中的一个状态机信号。于是 AI 结合事件流和时间窗口给出了结论是:用户逻辑状态机在跳转到接收状态时,没有把rready准备好,而是在检测到rvalid之后才把rready拉高,而总线桥协议要求rready必须提前拉高,这个根因完全符合所有信号的时序特征。

这个分析过程总结下来很像一个资深验证工程师的思路:先看错误时刻附近发生了什么,再锁定协议信号的行为是否正确,然后顺着驱动关系找到真正有问题的设计逻辑。AI 在这里更多是把我的排查方法复现了一遍,而不是真的比人聪明多少,但只要它复现得足够稳定,你就能把精力省下来去设计测试方案。

3.3 如何设计 prompt 去引导 AI 给出正确结论

prompt 设计是整个工具最值得调优的部分。我用了两版迭代才得到一个相对稳定的模板。第一版的做法是把所有资料汇总粘贴,然后问一句“帮我分析一下 root cause”,结果是 AI 输出的结论泛泛而谈,大量使用“可能”、“建议检查”这类不确定词汇,缺乏实际指导价值。

第二版做了大幅调整。开头直接指定角色和上下文——你是资深验证工程师,当前面对一个 RTL 仿真失败现场。接下来写明输入信息的组织方式:事件表按时间顺序给出,信号描述只列出关键信号和它们变化时刻的行为,连接关系只列出与异常信号相关的路径。最后明确输出格式:必须给出根因、依据、验证建议三段式。

同时在 prompt 里加了一个很重要的约束:如果没有足够证据支持某个结论,必须要求自己指出缺失的信息,而不是强行给出可疑的判断。这一步大幅提升了结论的可信度。 AI 有时会自动脑补信号行为,比如你说valid一直为低,它就能推断出“因为ready没有拉高所以valid没有拉高”,但实际情况可能是valid的驱动逻辑本身就有问题。加了“无证据不推断”约束后,AI 会主动问你——ready信号在故障前的行为是什么?连接关系里有没有valid的驱动源?——这样反而能形成一个人机互动的二次侦察流程,比一次性问答要可靠得多。

4. 实际使用中遇到的问题与排查技巧实录

4.1 日志事件和波形时间对不上

这是我最开始遇到的也是最头疼的问题。不同的仿真器打印日志用的时间格式不同,有说t=100ns的,有说#1000的,还有的直接打印周期计数值而不是时间。时间单位不一致会导致波形降维模块选错窗口。解决办法是在日志抽取时先做单位归一化,把所有时间统一换算成“时钟周期序号”而不是“绝对时间”,甚至可以直接通过检索复位完成信号找到仿真启动的零时刻,然后所有 T 都是相对复位完成后的周期数。

4.2 波形文件太大导致降维过程极慢

一个 5 GB 的 VCD 文件,用 Python 传统逐行读取加正则匹配的方式非常慢,可能跑几十分钟。我试过用多进程对文件分片解析,有效果但工程复杂。实测下来最稳且提速效果明显的方式是:先用vcd2fst把 VCD 转换成 FST 格式,再使用 FST 专用的 C 接口做事件采样。FST 格式的特点是只按信号名索引事件,不记录完整文件内容,特别是在定位某个信号在某个时间窗口内变化这件事上,精度和速度都提升了一个量级。如果你所在环境不支持 FST,退而求其次是用vcdvcd的流式迭代方式,不要一次性加载整个文件到内存。

4.3 AI 把周期性轮询信号当成有效事件

有些设计里会有一些轮询状态的信号,比如每 1000 拍产生一个中断请求,这本身是正常工作行为,但在事件表里它会频繁出现。AI 很容易把这种周期性跳变和错误关联起来,引导分析朝错误方向走。解决起来也简单:在日志清洗阶段把周期性信号单独标记为“背景信号”,并在 prompt 里明确说明“下面这些信号属于正常周期行为”,同时从连接关系表里删除这些信号相关的异常高亮标注。这么做之后 AI 给出的分析中就不再出现“因为轮询信号跳变所以导致错误”这类错误判断了。

4.4 连接关系提取误伤总线聚合信号

有一条总线信号,位宽 64 位,但业务上只用了其中低 32 位,高 32 位悬空。静态提取时不会感知到这一点,它依然会把这个信号当成 64 位完整连接。当 AI 看到某个 64 位信号的数据异常时,可能会去研究高 32 位的驱动关系,白白浪费上下文和推理时间。处理方案:在连接关系模块里增加一个“实际有效位宽”配置项,你在工程配置里人工声明哪些信号的有效位宽是多少。这个方案虽然是半自动的,但作为工具的主人,你对设计业务的了解恰恰是 AI 无法替代的部分。

4.5 未登录模块路径让 AI 无法定位根因

在设计里从模块 A 的层次化路径能找到信号,模块 B 的路径也是明确的,但中间隔了好几层例化以后,最终的信号名在连接关系表里可能只出现了一次,上下文里如果缺少完整的层级路径,AI 给出的结论就会变成“可能和某信号有关”,而不是“信号 xx 在层级的第几级发生了变化”。我最后采取的措施是在连接关系提取时收集了每个信号所在的完整层次路径,并把它和信号名一并提供给 AI。只要你给它路径,它就能找到真正的位置。这是很细节的优化,但它的作用比你多喂一百个波形成绩还重要。

4.6 模型输出不稳定导致解析困难

内网部署的模型通常体积和参数规模都不如商业大模型,偶尔会出现输出格式混乱的问题,比如说好的 JSON 里突然加了一段自然语言。为了解决这个问题,我在调用模型的时候在 prompt 的最后追加“你只输出 JSON,不要输出任何解释或额外文字”,同时在解析端用正则锁死result字段和confidence字段的取值,如果解析失败就自动要求模型重新回答一次。双保险试下来,成功率基本维持在 95% 以上。如果你遇到的模型更“叛逆”,会拒绝重答,那就退回上一档:人工查看模型输出,手动纠正格式之后再把结果喂给下游工具。这也是无奈之举,但工程化的东西实用才是第一位的。

5. 工具扩展思路与适用边界

做到现在这个阶段,整条链路已经比较顺了。我还在尝试把它往两个方向延伸。第一个方向是把它接入更上游的编译阶段,也就是在编译报错之前就把断言层的信息结构化,这样不仅能定位功能错误,还能对断言失败做出预判。第二个方向是把同一套流程用于覆盖率的收集与分析,收集到的覆盖率数据同样可以被 AI 用来判断当前测试是否真的“刺激”到了怀疑点,从而给出下次回归用例的修改方向。

但是我必须诚实地讲一句:这类工具不是万能的。它最有效的时间点是在定位明确、现象单一的“常规验证问题”上。如果你面对的是一个随机化激励下极其复杂的多模块联调问题,它确实能帮你把信息压缩和呈现出来,但最终的分析方向还是要靠工程师自己的判断。毕竟 RTL 调试里大量隐性的项目背景信息,例如这条路径是不是最近重构过、这个模块的历史 bug 率是不是偏高、这周的代码改动是从哪个分支合进来的,这些上下文都在人的脑子里,工具现阶段还不会自动读 Git log,也不会自己翻修改记录。在把工具当“侦探”用的时候,你自己才是最核心的那位“警探”。

所以我会建议你从最小可行版本开始:先抽出日志模块,让 AI 帮你总结错误日志;再逐步加上波形降维和连接关系提取。不要妄想一步到位,先让工具在某个具体的项目里帮上忙,再一点一点培养你对它的信任感,你会发现在 RTL 调试这个领域,AI 最好的定位从来不是“替代大脑”,而是“扩展注意力的边界”。这样一步步走下来,你能把省下来的时间花在真正需要创造力的验证策略设计上,我觉得这才是这个工具最有价值的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询