资讯动态

基于AI Agent的RTL仿真调试工具设计与FIFO空读X态定位

发布时间:2026/10/26 8:13:37 来源:尧图企业网站定制
做验证的朋友多少都有过这种经历RTL仿真跑挂了日志文件几百MB波形几个GB信号层次上千条面对一片报错日志只能靠肉眼和鼠标在波形GUI里一点点翻。我自己被这个流程折磨了很久干脆动手做了一个RTL仿真调试工具核心思路很直接把仿真日志、波形和信号连接关系变成AI可以查询的结构化数据源让大模型帮你查日志、抽波形、追信号驱动链最终给出root cause分析。这篇文章把完整设计和实现过程记录下来包括我做这个工具时的架构选择、核心模块拆解、一个真实的FIFO空读X态问题定位过程以及这些实际踩过的坑。对数字IC验证、FPGA调试和想给内部EDA流程引入AI能力的工程师应该都有参考价值。1. 工具整体设计与思路拆解1.1 验证日常里真正浪费时间的地方先说我做这个工具之前最痛的一件事仿真日志、波形、RTL代码三者之间是割裂的。日志告诉你在时间点T、模块M有个ERROR但要看懂为什么出错你得去波形里找到T附近的信号跳变看完波形又发现某个信号的值不对还得回到RTL代码里查这个信号是被谁驱动的。这三个环节来回切换大量时间其实不是花在“思考”上而是花在“翻数据”上。更头疼的是波形里信号一大片你并不总是知道该看哪几个。很多经验丰富的工程师能在几分钟内锁定时序关系是因为脑子里已经有“信号连接图”了——知道data_out可能由哪个模块驱动、fifo_empty来自哪个时钟域、rd_en和状态机有什么关系。但这些知识一旦遇到不熟悉的模块或者跨时钟域设计就要现查例化关系、端口映射、assign语句非常耗时。1.2 为什么让AI“查数据”而不是让AI“直接给结论”一开始我也想过更激进的方案把整个波形文件、日志文件全部文本化扔给大模型让它直接输出root cause。试过之后发现这个想法在工程上行不通。波形文件本身是二进制压缩的VCD展开成文本动辄几个GB日志就算能塞进上下文窗口里面的重复消息也会把模型注意力淹没。更关键的是大模型对数值和时间线的处理很粗糙你让它直接看几万行信号跳变去推断哪一拍的时序关系出了问题结果大概率是自信满满的胡编。所以我最后选择了“工具调用式Agent”的架构。类比一下这就像你请了一个刚入职的助理不指望他凭经验直接告诉你答案而是给他一台装了查询系统的电脑让他按规则查日志、拉波形、看连接关系每一步都有据可查。大模型负责拆解问题和组织语言但所有事实数据都来自工具返回而不是模型自己生成。这套设计能把幻觉限制在一个很小的范围里AI如果说错了你拿工具原始返回一对照就能发现。1.3 总体架构三块数据底座加一个Agent壳整个工具分成四层前两层是离线/在线建立的数据底座第三层是查询接口第四层才是AI Agent。数据底座这层有个原则所有数据都先转成统一的时间轴和统一信号命名空间避免AI在不同ND格式之间做换算。仿真日志 → 日志解析器 → 事件流索引 VCD/FST → 波形引擎 → 跳变沿时间索引 采样窗口 RTL代码 → 连接图构建器 → 信号/模块实例有向图 三个数据源 → 统一查询接口(query_log / query_wave / query_signal_connection) ↓ LLM Agent(工具调用 约束提示) ↓ root cause 分析报告查询接口只暴露三个函数查询日志、查询波形、查询信号连接关系。AI要回答“这个ERROR最早发生在什么时候”“data_ok在140ns到160ns之间为什么是X”“fifo_rd_en由谁驱动”这类问题就必须调用函数拿到结构化结果。这样做的另一个好处是每个查询都可以单独审计Agent到底根据哪些数据做了判断完全透明。2. 核心模块与关键技术实现2.1 日志结构化不同仿真器格式适配与事件流模型日志解析是整个工具里看起来最简单、实际坑最多的模块。不同仿真器的格式差异很大Questa的日志常见格式是# 10000 ERROR [scope] messageVCS则可能是Warning: [XXX] ...Xcelium又有自己的样式。如果只写一个正则换仿真器就废了。我这里的做法是先做一层预归一化把常见格式统一映射成一个标准事件结构dataclass class LogEvent: time_ns: float # 统一换算到纳秒 level: str # INFO / WARNING / ERROR / FATAL hierarchy: str # 模块路径如 tb.dut.u_fifo msg_id: str # 错误码或消息类型 text: str # 清洗后的完整消息 start_line: int # 在原始日志中的行号范围用于反查 end_line: int预归一化的重点在于把时间单位统一。仿真器打印的时间戳常常不带单位可能默认是ps、ns或者fs要在配置里告诉工具当前仿真的时间精度。换算如果不做AI后面读波形时间戳的时候会差出几个数量级结论全错。另一个容易忽略的点是转义字符和多行日志很多仿真器会把Verilog转义信号名里的反斜杠直接打印出来还会把一行长消息折成多行显示解析时要把这些处理掉否则AI按连续文本理解会断错句子。2.2 波形引擎不是把波形塞给AI而是按需采样一开始我天真地想把一段波形转换成文本给AI比如“t100 clk0 rst1 ...”结果发现完全不可行。一个时钟周期就产生几十个信号的文本一个100ns窗口可能有上千行模型根本抓不住重点。后来我把思路改成“跳变沿抽取”波形本质上是一组随时间变化的信号真正有意义的是信号发生变化的那一拍两个跳变之间信号一直保持不变这个区间不需要重复显示。波形引擎的底层用FST格式做持久化文件的压缩率比VCD高很多加载速度也快。但是即使FST也不能在启动时把所有信号值全展开否则几个GB的文件照样吃内存。我的方案是边加载边建立“信号名 → 跳变事件数组”的索引每个事件只保存时间点和变化后的值然后按需对查询窗口采样def sample_signal(signal_name, t_start_ns, t_end_ns, max_events100): edges timeline[signal_name].range(t_start_ns, t_end_ns) if len(edges) max_events: # 超限时先做模式压缩连续重复模式只保留第一个 # 并标记 sampledTrue让AI意识到可能遗漏跳变 edges compress_patterns(edges, max_events) return [{t_ns: t, value: fmt_value(v)} for t, v in edges]对单bit信号值保留0/1/X/Z对多bit总线我习惯转成十六进制比如data_out0xDEAD。这样模型不用去做二进制到十六进制的手动转换避免算错。采样窗口如果过大返回数据超过模型的输出上限工具侧就要做抽样或分段返回时明确标注“这是抽样结果可能漏掉中间跳变”。这样AI就不会把一次抽样遗漏误判成信号一直没有变化。2.3 信号连接图从RTL代码到可查询的driver/receiver关系这是整个工具里最能提升root cause效率的部分也是实现起来最复杂的部分。它的目标是回答两类问题某个信号由谁驱动某个信号驱动了哪些负载我构建了一个有向图节点可以是信号、寄存器、线网或模块实例边表示驱动关系。具体做法是先用解析器读RTL文件提取assign语句、always块里的驱动信号、实例例化的端口连接关系。比如看到assign fifo_rd_en (state READ) !fifo_empty;就建立fifo_rd_en的driver是表达式里的state和fifo_empty。看到某个模块实例把rd_en端口连到ctrl_fifo_rd_en就记录一条跨层次连接。遇到跨层次信号统一转成全局命名比如tb.dut.u_fifo.fifo_rd_en。这个模块一开始用的是现成的Verilog解析器后来发现很多真实RTL会用到generate、宏定义、参数化例化导致解析不全。我就加了一层兜底从仿真器dump出的层次化信号列表里直接把顶层信号和子模块端口对应关系导入图中。这样即使静态解析漏掉某些连接网表级别的路径仍然存在。对于PLL、DDR PHY这类没有RTL源码的硬核IP则维护一个黑盒模块列表避免连接关系断裂。2.4 AI参与方式工具函数加约束性提示词工具调用的核心是给大模型定义一套Schema让模型能自己决定调用哪个函数、传哪些参数。我的三个函数定义是这样设计的{ function: query_log, arguments: { level: ERROR|WARNING|INFO|FATAL, time_start_ns: 1200, time_end_ns: 1500, scope: tb.dut.u_fifo } }模型根据语义会生成这样的调用。工具层收到参数后去事件流索引里查返回结果限制在几十条以内每条都带时间戳、层级、原始消息。同样地query_wave允许传入信号名列表和时间窗口query_signal_connection允许传入信号名和查询方向upstream/downstream。三个函数合在一起正好对应验证工程师日常的“查日志、拉波形、追连接”三步。提示词部分也下了不少功夫。我明确要求AI在每条分析结论里都要引用工具返回的时间戳和信号名如果工具返回空结果就如实说查不到不许脑补。我还在提示词里加了一条硬规定所有二进制、十六进制和时间的换算必须交给工具完成AI只负责比较和推理。这样既降低了幻觉率也方便后续人工复核。3. 实操过程一个FIFO空读X态问题的root cause复盘3.1 案例背景数据总线上出现了X我最近在调一个异步FIFO的设计写时钟域和读时钟域频率不同。仿真跑到某个阶段日志里开始刷FATAL: fifo read data is X而且一旦出现后续大量断言跟着失败。从现象看像是读侧在FIFO为空的时候仍然发出读请求导致读数据总线被驱动成X。但真正的疑问是读状态机为什么会在一个“空”的FIFO上发起读操作我用这个工具一步步查。先看整体现象和目标下面这张表是当时记录的项目描述现象tb.dut.u_fifo.data_out 出现较长X态后续断言风暴日志关键词FIFO read data X / empty violation嫌疑信号fifo_empty、fifo_rd_en、state初始怀疑跨时钟域同步导致空/满标志滞后3.2 第一轮日志查询缩小时间窗口我没有直接去看波形而是先让Agent执行日志查询目标是找到X态首次出现的时间、涉及的模块和当时的上下文。我给的指令是“查询日志中所有FATAL按时间排序给出最早出现的几条并列出它们之间的时间间隔。”Agent调用query_log得到的结果大致是这样的t110000 ns FATAL tb.dut.u_fifo fifo_read_x t110012 ns FATAL tb.dut.u_fifo fifo_read_x t110018 ns FATAL tb.dut.u_fifo fifo_read_xAI据此判断第一个FATAL发生在110000 ns附近后续FATAL密集出现说明这个错误一旦产生就会连续触发断言。它建议我去看110000 ns到110050 ns这段窗口内的读使能、空标志、状态机信号。这个窗口定位比在几个GB波型里人肉搜索要快得多。3.3 第二轮波形查询确认空读时序接下来我给Agent指令“查看tb.dut.u_fifo模块下fifo_empty、fifo_rd_en、state和data_out在110000到110050 ns窗口内的波形判断是否存在空读。”Agent会做一次query_wave调用返回采样后的跳变事件。关键数据是这样的t110000 fifo_empty1 fifo_rd_en0 stateIDLE data_outZZZZ t110004 fifo_empty1 fifo_rd_en1 stateREAD data_outXXXX t110008 fifo_empty1 fifo_rd_en1 stateREAD data_outXXXX t110012 fifo_empty1 fifo_rd_en0 stateIDLE data_outXXXXAI返回的分析是在t110004 ns时fifo_rd_en被拉高了但此时fifo_empty仍然为1状态机已经进入READ状态。数据总线在rd_en为高期间被驱动成X这说明FIFO内部存储体在空状态下被读输出了不确定态。由此基本确认是一次空读。但这个答案只解释了现象还没解释“为什么状态机会在fifo_empty为1的时候发起读操作”。这就进入第三轮。3.4 第三轮信号连接追踪找到真正的驱动链我让Agent继续查两个信号连接关系fifo_rd_en由谁驱动fifo_empty由谁驱动。query_signal_connection返回的信息让不少人意外。fifo_rd_en的驱动来源是读时钟域的状态机FSM具体到一段条件表达式assign fifo_rd_en (state READ) !fifo_empty;。也就是说状态机的逻辑本身是有判断的_fifo_empty为1时不应该输出读使能。那为什么波形里fifo_empty1但rd_en还是拉高了问题一定出在表达式中用到的fifo_empty并不是FIFO内部实时的空标志。继续查fifo_empty的信号连接发现它来自写时钟域。FIFO内部真正的空标志empty_async先被同步器打了两拍同步到读时钟域之后才变为fifo_empty。问题就出在这两拍同步延迟上。在上百个时钟周期的场景里FIFO从空变为非空、再从非空变为空的瞬间同步后的fifo_empty会滞后几个周期。状态机拿到的“空标志”其实是几个周期前的状态它判断FIFO还有数据于是发出读请求而实际上FIFO内的数据已经被读完了。于是一次空读就发生了。到这里AI给出的root cause已经很清晰读时钟域的状态机使用了未经适当同步的异步空标志确切地说是同步器输出相对真实FIFO状态存在几个周期的延迟导致状态机在FIFO刚变为空的边沿附近做出错误判断。修复方向也很明确要么在FSM进入READ状态前增加额外的握手等待要么把读侧的空标志从FIFO内部寄存器直接生成避免同步延迟影响判断。3.5 结果验证与效率对比按这个思路在RTL里加了保护逻辑重新仿真后FATAL消失X态也没有再出现。整个过程从喂日志到输出结论大概用了十几分钟如果按老办法人工查先定位错误时间、再翻波形、再找信号驱动链估计至少得半天。这个工具真正提升的不是“AI聪明”而是“数据底座让AI能像工程师一样查数据”每步都有原始依据我作为验证工程师只需要做最后的判断和修复。4. 常见问题与排查技巧实录4.1 波形文件几个GB一加载就内存爆掉这是我最早遇到的硬伤。VCD格式文本膨胀太严重几个GB的文件在加载时如果直接解析成内存里的嵌套字典机器直接卡死。后来的方案是能转FST就先转FST加载时只做时间索引不在内存里保留每个时刻的完整信号快照查询时再有针对性地对一个时间窗口做采样。实际项目里如果只有VCD也可以用增量扫描的方式建立跳变事件索引不要一次性把所有值都展开。另外提醒一句如果你用常见EDA工具打开波形看到红线X态那只是表面现象真正原因往往在驱动链的上游。我的工具对这种问题很有用直接让AI查X态信号的driver链往往能看到某个上游模块的输出在什么时候变成X。4.2 日志里有转义字符和多行消息解析器经常断错句仿真器的日志并不都是规整的“时间级别消息”。很多消息里有Verilog转义信号名反斜杠会干扰正则有些长消息被折成多行直接按行解析会把一条错误拆成好几段。我最后在解析链路里加了一个预处理先把所有转义信号名做归一化去掉反斜杠和多余空白然后把连续以缩进开头的多行内容合并成一条消息保留起始行号作为索引。这个细节看着小但对AI理解影响很大。你想想模型明明看到一行FATAL fifo read data is X结果下一行是断掉的Because empty flag ...它可能误以为这是两个独立事件做出完全不同的分析。4.3 信号命名不统一、层次名带转义仿真器里的全局信号名和RTL源码里的局部名字经常对不上。比如RTL里写的fifo_empty在仿真Dump里可能叫tb.dut.u_fifo.fifo_empty数组信号、带下标的总线也可能被转义成奇怪的名字。如果不做归一化AI查fifo_empty时只能搜到零散结果连接关系图也连不起来。我在连接图构建器里维护了一张全局信号映射表把“单元内名称”、“全局层次名”、“转义原始名”三者互相映射。查询接口支持任意一种写法最后都会归一化到统一的全局名字。这个映射表要定期和设计同步特别是RTL重构之后。4.4 模型幻觉和数值计算不可靠这是最不能含糊的问题。AI偶尔会在没有工具数据支持的情况下编造时间戳或信号值明明工具返回t112 ns模型可能写成t121 ns。我的对策有三层第一凡是工具返回的数据在给模型的文本里都带上“原始时间戳信号名值”三元组让模型必须引用第二数值换算全部在工具侧完成波形引擎返回的都是统一单位的纳秒时间十六进制总线值也是提前算好的第三后处理环节会对模型输出做一次“证据核验”——检查它引用的信号名和时间点是否真的存在于索引中如果查不到就标记为“未证实结论”提醒人工复核。我这里强烈建议你不要让AI做任何二进制补码、时间单位换算、上升沿计算之类的数学操作。模型更擅长模式识别和多步推理数值计算本来就不可靠工具能做的一定在工具里做掉。4.5 数据安全与私有化部署对于芯片公司来说RTL代码和仿真波形属于核心敏感资产直接调用外部大模型API不现实。我在实际使用中用的是本地部署的开源模型通过Ollama这类工具跑起来配合工具调用接口工作。如果公司内部有统一的LLM网关也可以把工具接到内部API上关键是数据不出内网。如果必须用外部模型也有一个折中方案把信号名做哈希脱敏、日志文本只保留错误类型ID波形数据只保留时间窗口内的跳变摘要。这样模型分析的是“脱敏后的指纹”得到结论后再映射回真实信号名。不过整体效果会打折扣能本地部署还是尽量本地部署。做这个工具下来我最大的体会是不要把AI当成“一步到位给答案”的机器而是把它当作一个能高效查阅数据、按图索骥的调试副手。日志、波形、信号连接三块数据底座的可靠性决定了AI分析的准确度。最后分享一个小技巧设计查询接口时每个函数都要返回“这次查询范围”的元数据比如时间窗口、抽样方式、命中条数。有了这些元数据AI才能判断自己的查询粒度是否足够是否需要缩小时间范围或者换个信号再查。这一点在复杂的多时钟域问题里尤其好用。

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

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

免费获取报价 →
↑