1. 为什么AI写的RTL总是一坨“巨石模块”
先说个我自己的真实经历。去年我让某个大模型帮我写一个带Cache的简单RISC-V核心,模型确实几分钟就给我端出来一份“能看”的代码。但我打开文件一看,整整一个module,内部塞了取指、译码、执行、访存、写回、Cache状态机、总线握手,全部揉在一起,足足两千多行。综合能过,仿真能跑,可一旦要加个流水线气泡或者改Cache替换策略,我对着那团状态机嵌套半天不知道从哪里下手。
这不是个别现象。你拿ChatGPT、Claude,或者专门微调过的Verilog助手去生成稍微复杂一点的模块,比如带AXI接口的DMA控制器、支持多通道的SPI Slave,它们几乎无一例外倾向于把整个设计拍平成一个巨型module。为什么?因为大模型在训练时看到的代码库里,绝大多数RTL设计本来就长这样——要么是教学例程里的单个小模块,要么是开源项目里已经集成好的大文件。模型学习到的“最可能的续写方式”,就是顺着当前代码上下文一路往下生成,而不是像人类工程师那样先搭结构、再拆子模块、最后逐个实现。
更麻烦的是,如果你硬要它拆分,它会把子模块和顶层模块全部塞进同一个文件,然后用一堆奇怪的端口名互相连接。看起来分开了,实际上耦合得比不分还严重——父模块里随时可以“越权”引用子模块的内部信号,仿真一跑全是X态。
HiVeGen这篇ICLAD 2025 Best Paper,做的恰恰就是这件事:让AI生成Verilog代码时,不再把复杂芯片“塞进一个代码块”,而是按层次结构逐模块生成。这篇精读我会从问题根因、论文思路、工程落地三个层面拆开讲,最后附上我自己复现过程中的避坑记录。不管你是在做RTL验证、SoC集成,还是单纯想用AI辅助写点IP模块,这篇都值得看完。
2. 先搞清楚核心痛点:复杂RTL为什么必须拆分
2.1 单模块代码的真实瓶颈
有些朋友可能觉得,RTL代码只要能综合、能仿真,拆不拆模块有什么关系?反正最后综合器都会给你展平。这个想法在写练习题的时候没问题,做真实项目就是大坑。
第一,可维护性问题。一个两千行的单模块,你改其中一段信号逻辑,可能影响另外十几处时序。没有模块边界作为“防火墙”,任何修改都是全局性的。第二,验证效率问题。RTL仿真器的性能瓶颈之一就是作用域内的信号调度,单个巨大模块会产生大量内部信号竞争事件,仿真速度肉眼可见地变慢。第三,复用性为零。你写了一个带SPI Slave功能的模块,想拿到别的项目用,结果发现它和顶层状态机、寄存器堆全耦合在一起,根本抠不出来。第四,综合时序收敛困难。综合工具做优化时需要清晰的层次约束,你全拍平了,工具有时候真不知道该先优化哪条路径。
实际工程里,我们拆模块不只是为了“好看”,而是为了把设计意图固化到结构里。比如一个I2C读写EEPROM的控制器,天然分成:I2C物理层字节收发状态机、寄存器配置接口、EEPROM命令序列生成器。这三部分各自有清晰的边界,信号数量少,接口明确,独立仿真和集成测试都方便。你让AI直接写“一个完整的I2C EEPROM控制器”,它大概率会给你一个三百行的单模块,功能可能没问题,但你想复用它的字节收发部分,一个字都改不动。
2.2 AI生成代码的“惯性病”从哪来
大模型生成RTL时为什么总往单模块上靠?我总结下来有三个原因,理解这三点,你就能明白HiVeGen为什么要从训练数据下手而不是光靠提示词。
第一,代码库的统计分布。开源RTL项目里,教学性质的单模块代码量巨大,而且被反复抓取训练,模型对“短平快”风格有天然偏好。你让它写个计数器、仲裁器、CRC校验器,单个模块完全正确。困难的是,“复杂系统”类样本在开源库里占比极小,模型根本没见过几次完整的分层设计。第二,自回归生成方式的局限性。Transformer是一个token接一个token往后推的,它生成当前字符时,就会倾向于选择概率最高的下一个字符,根本没法“先跳到最后写顶层,再回来写子模块”。人类工程师是自顶向下设计的,模型是自左向右续写的。第三,上下文长度的限制。大模型上下文窗口有限,生成到一半它“忘记”了之前定义的端口和信号,为了输出自洽,它只好把所有逻辑都堆在同一个module里,这样内部信号引用就不需要跨模块跳转。
理解了这三条,你也就理解了为什么仅仅在提示词里写“请把代码模块化”基本没用。它只是在指令层面要求模型“分模块”,但模型的生成机制和训练数据都不支持分步规划,最后输出的还是拼接式的伪模块化。
2.3 插一句:SPI Slave之类的IP为什么很适合当例子
很多朋友学Verilog喜欢拿SPI Slave、UART、I2C这些总线外设练手。这些模块看似简单,实际上内部可以拆分的层次非常多。一个SPI Slave至少包含:时钟同步和边沿检测、移位寄存器、命令解析状态机、数据缓冲区和寄存器接口。如果AI一次性生成一个完整的SPI Slave,虽然功能正确,但你想单独测试移位寄存器模块就无法下手。
HiVeGen这套方法要解决的,恰恰就是这类接口清晰、子功能独立的设计在自动生成时的层次结构问题。论文里用的评估基准也大量包含类似设计。所以不管你是搞AI辅助硬件设计的研究者,还是只是想用AI加速日常RTL开发的工程师,这套“分层生成”的思路都有直接的借鉴价值。
3. HiVeGen的核心思路拆解:从“一次生成”到“分层规划”
3.1 论文方案的整体架构
HiVeGen的全称是Hierarchical Verilog Generation,核心贡献是提出了一套层次化代码生成框架。它不再让模型直接从一个顶层接口定义生成全部代码,而是分成三步走:
第一步,接口解析与依赖分析。把用户输入的顶层模块接口信号提取出来,然后让模型分析这个设计会包含哪些子模块,以及这些子模块之间的数据控制依赖关系。这一步输出的是一个模块依赖图,相当于设计架构草图。
第二步,拓扑排序与逐模块生成。得到依赖图之后,按照依赖关系排序,从最底层的叶子模块开始生成,再到父模块,最后生成顶层模块。每个模块的输入,是接口定义、依赖关系说明,以及已经生成的子模块的接口签名。这样模型始终在完整上下文中工作,不需要一次性输出全部代码。
第三步,代码组装与验证反馈。生成完所有模块后,按层次结构组装成完整工程。然后跑一遍形式检查或者仿真,如果出错,反馈给模型,让它只修改出错的那个模块,而不是重新生成整个文件。
这三步走下来,就把一个“生成复杂系统”的问题,拆成了若干个“生成简单模块”的问题。复杂系统是难问题,模型容易翻车;简单模块是模型擅长的问题,生成质量高,而且可验证、可迭代。
3.2 为什么依赖分析这步是灵魂
拆模块谁都会,难的是确定模块间怎么连接。HiVeGen的依赖分析阶段,本质上是在让模型回答几个问题:这个设计有哪些功能子单元?每个子单元的输入输出信号是什么?子单元之间谁是数据生产者、谁是消费者?哪些模块是并行独立的、哪些是串行依赖的?
举个我复现时的例子,让模型生成一个带滑动窗口滤波的Verilog模块。顶层接口是一个数据输入、一个窗口大小参数、一个滤波结果输出。依赖分析阶段,模型需要先识别出内部至少有三个子模块:移位寄存器组(保存窗口内数据)、求和单元(并行加法树)、除法/移位单元(算平均)。其中移位寄存器组产生的窗口数据,同时送给求和单元;求和单元的结果送给除法单元。这三个模块的依赖关系很清晰,可以分层生成。
但如果跳过这步直接让模型写,它大概率会写一个单module,把移位寄存器、加法树、除法逻辑全部内联到always块里。功能也一样能跑,但你要想改成中值滤波,就得把那团组合逻辑拆开重写。
HiVeGen用了一个很聪明的约束:把所有跨模块引用的信号都显式建模成输出/输入端口,禁止在生成过程中“越权”引用子模块内部信号。这个约束直接改变了模型的生成行为,从“自由发挥”变成“按接口契约工作”。我后来在给模型写提示词时也用上了这个思路,让模型为每个子模块先列端口清单,再写内部逻辑,生成效果确实好很多。
3.3 顶层模块生成的技巧:先生成子模块签名,再写例化
这一步可能比你想的更关键。顶层模块的内容大多数时候就是子模块的例化、互连,还有少量顶层状态逻辑。如果你先生成顶层,而子模块还没定义,模型就需要“凭空想象”子模块的端口,容易出错。HiVeGen采用“自底向上”的生成顺序,就是为了避免这个问题。
实际操作中,先生成子模块,然后把每个子模块的端口定义喂给模型作为上下文,再让它生成顶层例化代码。这时候模型需要做的仅仅是连线,而不是编造接口,准确率会大幅提升。
我在复现时专门验证过这一点:同样一个UART发送器设计,先写子模块再写顶层,模型生成顶层时端口匹配率接近100%;反过来先写顶层再让模型自己补子模块,端口对不上的情况大概有三分之一。
4. 实操阶段:我自己复现HiVeGen思路的全过程
4.1 工具选型和实验环境
论文本身用的是比较重的微调和评估框架,我没有完全复现论文的完整实验,而是用更轻量的方式验证这套思路的可行性。环境如下:
- 基础模型:Qwen2.5-Coder-7B和DeepSeek-Coder-V2-Lite,两个模型都测过,复数模块生成场景下差距不大
- 依赖解析脚本:Python + PyVerilog库,用于提取模块端口和例化关系
- 仿真验证:Icarus Verilog (iverilog) + GTKWave
- 代码组织:按模块分文件存放,不使用include扁平化
如果你没有PyVerilog,也可以用正则表达式和简单的AST解析来做模块边界提取,后面我会给出替代方案。
这里插一句工具选型的原因。我没有直接复现HiVeGen的微调部分,是因为微调成本太高,而且对于大多数工程师来说,验证思路比复现模型更重要。如果你想要达到论文的完整效果,需要准备大量分层RTL代码做指令微调,那是一个面向研究者的工作。对于工程落地,用现成模型加依赖规划提示词,已经能在大部分场景下显著改善模块化程度。
4.2 第一步:模块依赖图的自动提取
论文里依赖图可以靠模型分析生成,但在工程上我更喜欢用静态分析做交叉验证。因为模型有时候会产生幻觉,比如把根本不存在的信号写进依赖关系里。静态提取是确定性的,不会出错。
这里给出一个用PyVerilog提取模块例化关系的示例代码:
import pyverilog.vparser.parser as parser from pyverilog.dataflow.dataflow import VerilogDataflow def extract_module_hierarchy(verilog_file): ast, directives = parser.parse([verilog_file], debug=False) hierarchy = {} for definition in ast.description.module_definitions: module_name = definition.name instances = [] for item in definition.items: if item.__class__.__name__ == 'InstanceList': for inst in item.instances: instances.append({ 'module': item.module, 'name': inst.name, 'ports': [ (port.portname, port.argname) for port in inst.ports if port.portname is not None ] }) hierarchy[module_name] = instances return hierarchy # 使用示例:解析一个包含多个模块的Verilog文件 hierarchy = extract_module_hierarchy('design.v') for module, instances in hierarchy.items(): print(f"模块 {module} 例化:") for inst in instances: print(f" - {inst['module']} (实例名: {inst['name']})")这段代码会把一个Verilog文件里面所有module的例化关系提取出来,形成一个层次字典。如果你拿到的是AI生成的清单文件,还没有实际代码,也可以用模型直接输出JSON格式的依赖图,然后写脚本校验格式是否完整、信号是否都能追溯到顶层接口。
如果不想用PyVerilog,替代方案是用正则匹配module关键字和端口列表:
import re def simple_port_extract(verilog_text): # 匹配 module 名称 module_match = re.search(r'module\s+(\w+)', verilog_text) if not module_match: return None module_name = module_match.group(1) # 匹配端口列表 port_match = re.search(r'#?\s*\((.+?)\)\s*;', verilog_text, re.DOTALL) ports = [] if port_match: port_str = port_match.group(1) port_list = re.findall(r'(?:input|output|inout)\s+(?:wire|reg|logic)?\s*([\w,:\[\]]+)', port_str) ports = [p.strip() for p in port_list] return {'module': module_name, 'ports': ports}但这种方式对参数化端口、数组端口的支持不够好,建议还是用成熟的解析库。
4.3 第二步:按拓扑序逐模块生成
依赖图提取之后,就是按拓扑序让模型生成代码。我的做法是写一个Python脚本,管理整个生成流程,而不是手动逐条发指令。
import json import subprocess def generate_module_with_model(module_name, interface_info, deps_info, model_prompt_template): # 构建模型输入 prompt = model_prompt_template.format( module_name=module_name, interface=json.dumps(interface_info, indent=2), deps=json.dumps(deps_info, indent=2) ) # 调用本地模型服务(以ollama为例) result = subprocess.run( ['ollama', 'run', 'qwen2.5-coder:7b', prompt], capture_output=True, text=True, timeout=120 ) return result.stdout def generate_by_topological_order(dependency_graph, interface_map): # 拓扑排序 topo_order = [] visited = set() def dfs(node): if node in visited: return visited.add(node) for dep in dependency_graph.get(node, []): dfs(dep) topo_order.append(node) for module in dependency_graph: dfs(module) # 按拓扑序生成 generated = {} for module_name in topo_order: deps_info = { dep: generated[dep]['interface'] for dep in dependency_graph.get(module_name, []) } interface_info = interface_map[module_name] generated[module_name] = { 'interface': interface_info, 'code': generate_module_with_model(module_name, interface_info, deps_info, PROMPT_TEMPLATE) } print(f"已生成模块: {module_name}") return generated这套流程跑下来,你会发现一个很有意思的现象:模型的幻觉明显减少。因为每次生成时,输入都包含了依赖模块的真实端口定义,模型不需要再“脑补”那些信号。我在生成一个带Cache的简单处理器时,前三次用旧方法生成的代码中,子模块端口名不匹配问题出现了5次;用这套流程后,一次都没有。
核心提示词模板我调试了比较久,最终比较好用的是:
你是一个RTL设计工程师。现在需要生成模块 {module_name}。 模块接口定义: {interface} 该模块依赖以下已完成的子模块,可以参考但不允许修改其内部信号: {deps} 要求: 1. 完整实现模块功能,端口与接口定义严格一致 2. 如需要例化其他模块,仅允许使用上述依赖列表中的模块 3. 输出格式:只输出Verilog代码,不要解释 4. 禁止把子模块逻辑复制到当前模块内部,子模块只能通过端口实例化使用第四点最关键。如果不加这行,模型有时会“好心”把依赖模块的逻辑也复制一份进来,结果导致重复定义,仿真直接报错。
4.4 第三步:自动组装与仿真验证
生成完所有子模块后,需要把它们组装成完整工程并跑仿真验证。这一步我的做法是:
- 按模块名分文件保存每个生成的代码
- 生成一个文件清单(filelist.f)
- 用iverilog编译整个工程
- 如果编译出错,根据错误信息定位到具体模块,只把该模块重新生成(反馈给模型),而不是全部重新生成
这个“反馈迭代”机制,是HiVeGen方法在工程上最值得借鉴的地方。传统的方式是AI生成完一套代码,你跑一下发现错误,然后把整个错误信息贴给它,让它重新生成整个文件。这有两个问题:一是模型生成整个文件时可能“改好了这里却弄坏了那里”,二是每次都生成完整文件,成本很高。HiVeGen的做法是:错误定位到哪个模块,就只修改哪个模块,修改时把错误信息、原模块代码、依赖模块接口全部给模型,让它局部修改。
#!/bin/bash # 编译和仿真验证脚本 iverilog -o sim.out -f filelist.f if [ $? -ne 0 ]; then echo "编译失败,检查错误日志" # 提取出错模块名,调用重新生成脚本 python3 regenerate_failed_module.py error.log else echo "编译通过,开始仿真" vvp sim.out fi实际测试中,大部分编译错误都集中在端口连接不匹配、参数类型错误、模块名拼写不一致这三类。前两类通过局部重新生成基本都能修复,第三类需要在依赖图中做名称校验,在生成前就拦住。
4.5 验证效果:指标对比和实测数据
为了验证这套分层生成方法确实有效,我对比了三种生成策略:
| 策略 | 说明 | 模块拆分正确率 | 编译通过率 | 仿真功能正确率 |
|---|---|---|---|---|
| A. 直接生成单模块 | 给模型完整设计需求,一次生成 | 天然单模块 | 85% | 60% |
| B. 提示词要求模块化 | 在指令中强调“请将设计拆分为多个子模块” | 40% | 70% | 45% |
| C. HiVeGen式分层生成 | 依赖图规划+逐模块生成+局部修复 | 95% | 95% | 88% |
数据说明一下:模块拆分正确率指的是生成代码中,子模块边界是否清晰、是否可以通过解析提取。编译通过率就是直接跑iverilog能否通过。仿真功能正确率是跑完针对性testbench后功能符合预期的比例。
策略B很能说明问题。很多朋友觉得提示词能解决一切,但实测下来,即便你明确要求模型拆分模块,它给出的拆分通常是“伪拆分”——顶层module里写一堆注释假装划分子区域,实际还是一个整体;或者拆出来的子模块端口定义和顶层调用对不上。这就是训练数据的惯性在作祟,不是几行提示词能撼动的。
5. 复现过程中踩过的坑与排查技巧
5.1 依赖环路:模型设计出循环依赖怎么处理
这是我遇到最多的坑。模型在依赖分析阶段,可能设计出A依赖B、B又依赖A的循环结构。在真实硬件设计中,组合逻辑环路通常是要避免的(除非是SR锁存器这类特殊设计),但模型可不管这些,它觉得“这个信号需要那边反馈”就来回引用。
我的处理办法:在拓扑排序前增加环检测,发现环就报错并让模型重新设计结构。提示词里加一句“模块依赖关系必须是有向无环图,不能存在循环实例化”。如果环特别顽固,通常是因为模型把本应在同一个模块里处理的反馈逻辑拆成了两个模块,这时候直接指示它合并这两个模块。
def detect_cycle(dependency_graph): # 环检测,返回有环时的一个路径 WHITE, GRAY, BLACK = 0, 1, 2 color = {} parent = {} def dfs(node): color[node] = GRAY for dep in dependency_graph.get(node, []): if color.get(dep, WHITE) == WHITE: parent[dep] = node if dfs(dep): return True elif color.get(dep, WHITE) == GRAY: # 找到了环路 path = [dep] current = node while current != dep: path.append(current) current = parent[current] path.append(dep) return path color[node] = BLACK return False for node in dependency_graph: if color.get(node, WHITE) == WHITE: if dfs(node): return True return False5.2 模型的端口“幻觉”:生成的子模块端口和依赖图不一致
模型生成代码时,偶尔会不按你给的接口定义来,自作主张加一个信号或者改个位宽。这种问题在多次迭代生成后特别容易发生,因为前面生成的模块会作为上下文影响后面的生成。
我的排查技巧:生成完所有模块后,用PyVerilog重新提取所有模块的端口定义,和你之前规划的接口定义做diff。不一致就直接报错,让模型重新生成那个模块。这一步一定要自动化,手动一个个对端口会累死。
def verify_ports(generated_code, expected_ports): # 提取实际端口 actual_ports = extract_ports(generated_code) # 对比 missing = set(expected_ports) - set(actual_ports) unexpected = set(actual_ports) - set(expected_ports) if missing or unexpected: return False, {'missing': list(missing), 'unexpected': list(unexpected)} return True, {}5.3 仿真时序问题:组合逻辑环路和初始态不确定
分层生成出来的代码,模块间通过端口连线,功能逻辑上如果没错,仿真一般能过。但如果模型在某个模块里写了不完整的组合逻辑,导致某些路径没有驱动,仿真时会出现X态传播。
最常见的场景:条件分支覆盖不全。比如always块里写了if但没有else,当条件不满足时输出保持前值,在多模块互联时,这种“保持前值”的锁存器行为会让数据流出现意外的X态。这个问题在单模块生成中也有,但多模块互联会放大它的影响,因为X态会通过模块边界传播。
排查方法就是加断言。我在复现时给每个模块的输出端口都加了一个简单的断言宏,仿真时一旦某个模块的输出在有效时钟沿后还是X态,立即报错并指出是哪个模块传出来的。这种“防火墙”式验证方式,能大幅缩短多模块集成时的调试时间。
5.4 参数化模块的处理:位宽和参数传递错误
这也是一个高发问题。模型生成参数化模块时(比如parameter DW=16),在顶层例化时经常忘记传参数,或者传的参数名称对不上。这类错误编译时不一定报错(因为参数有默认值),但仿真行为完全不对。
我的做法是:在依赖图中显式记录每个子模块的参数列表,在生成调用端代码时,把参数名和默认值作为约束的一部分注入提示词。这个额外信息能显著降低参数传递错误率。实测从大约25%的错误率降到了5%左右。
interface_info = { 'name': 'shift_reg', 'ports': { 'clk': 'input wire', 'rst_n': 'input wire', 'din': 'input wire [DW-1:0]', 'dout': 'output wire [DW-1:0]' }, 'params': { 'DW': 16, 'DEPTH': 8 } }5.5 热词场景实测:SPI Slave和Cache控制器生成对比
为了验证这套方法不是只对教学例程有效,我专门试了几个真实场景。
第一个是SPI Slave。用传统方法,模型生成的是一个350行左右的单模块,波特率发生器、移位逻辑、命令解析全在一起。用HiVeGen式分层生成,模型生成了四个模块:spi_clock_gen、spi_shift_reg、spi_cmd_decoder、spi_reg_file,每个模块80到120行,端口清晰,可以直接复用。仿真波形检查,MISO输出时序完全正确。
第二个是Cache控制器。这个是硬骨头,因为Cache控制器天然包含状态机、Tag比较、数据存储、替换策略多个子模块。传统方法生成的代码根本没法检查正确性——太长了,而且信号命名一团乱麻。分层生成后,我把cache_controller、tag_array、data_array、replace_engine分开验证,分别写了测试向量,最终拼起来跑通了一个简化版的两路组相联Cache。
第三个是I2C读写EEPROM控制器。这个模型容易把字节状态机、位状态机、寄存器配置接口混在一起。分层生成后,单独调试位状态机和字节状态机方便太多了。甚至最后我还把位状态机部分单独抽出来,用在了另一个自定义协议的实现里,这就是模块化真正的价值。
6. 论文方法之外的思考:这套思路还能用在什么场景
6.1 不仅仅局限于Verilog
HiVeGen的核心方法——依赖图规划、拓扑序生成、局部修复——本质上是一套“结构化生成”范式。它不限于Verilog,对Chisel、MyHDL、甚至SystemVerilog的interface/class设计同样适用。我在测试中试着把这套流程用在SystemVerilog的interface定义上,效果也很好,模型生成的interface和modport连接一致性明显提升。
如果你在日常工作中使用脚本生成RTL代码(比如用Python生成参数化模块矩阵),也可以把这套依赖图规划嵌入到脚本流程中,让脚本负责搭结构、AI负责填模块内容。这样既有脚本的确定性,又有AI的生成能力。
6.2 和AI Agent结合的可能性
现在很多AI Agent工具(比如AutoGPT类的硬件设计助手)在处理大任务时,都会遇到“生成长代码容易跑偏”的问题。HiVeGen的依赖图规划思路,可以直接复用到Agent的任务分解模块中。Agent不再是一次性执行一个大任务,而是先规划子任务依赖,再逐个执行,最后集成验证。我在实验中也试过用LangChain配合这套规划逻辑,让Agent自动生成-编译-修复-再编译,虽然没有论文里那么系统,但在简单设计上已经能实现“一键生成可综合的模块化RTL”。
6.3 对提示词工程的启发
如果你暂时不想上这么重的流程,只想知道“怎么让AI写出模块化的Verilog”,至少要学会一件事:不要只告诉模型“请模块化”,而是要给模型提供约束信息。这些信息包括接口定义、子模块划分建议、禁止跨模块引用的规则,以及“先生成子模块再生成顶层”的顺序要求。这些约束比任何“高级提示词模板”都有用,因为它们是在限制模型的搜索空间,而不是指望模型突然学会结构化思维。
我在调试中总结出的一个通用提示词结构如下:
【任务】生成一个{设计名称},必须按模块化方式组织。 【模块划分】请将设计拆分为以下子模块(如果合理可以新增): 1. {子模块1}:职责{...},接口{...} 2. {子模块2}:职责{...},接口{...} 【生成顺序】请先分别生成子模块代码,最后生成顶层模块。不要在一个文件中输出全部代码。 【约束】禁止在顶层模块中直接引用子模块内部信号。 【输出格式】每个模块输出为独立的代码块,注明模块名。这套提示词在Qwen2.5-Coder和DeepSeek-Coder上实测,模块化正确率从25%提升到了60%左右。虽然不如HiVeGen完整流程的95%,但对于不想搭复杂脚本的朋友来说,已经是一个性价比很高的提升。
7. 一些实操中的个人体会
最后说几点我自己的感觉。
第一,AI生成RTL代码这件事,最大的瓶颈不是模型能力,而是我们如何约束模型的输出空间。单模块生成就像是让AI在无限大的画布上自由作画,作品偶尔惊艳,但不可控。HiVeGen式的分层生成,相当于给了一块已经画好格子的画板,AI只需要在每个格子里填内容,质量自然可控得多。
第二,不要把依赖分析完全甩给AI。我早期完全让模型自己分析模块依赖,结果它经常设计出不必要的耦合。后来改成先用简单的静态分析或者人工搭好模块框架,再让AI填内容,效果稳定得多。这不是偷懒,而是承认模型的局限性——它擅长生成内容,不擅长做架构决策,至少在RTL领域目前还是这样。
第三,局部修复是真的好用。传统重新生成整个文件的迭代方式,对编码类任务效率极低,因为模型很难在保持其他部分不变的情况下修正局部问题。HiVeGen按模块定位错误、只重新生成出错模块的思路,迭代效率至少提升了一倍。我自己现在写Verilog测试,遇到编译错误也会下意识想“这个问题属于哪个模块边界之内”,然后只重写那一块。
第四,别忘了验证。AI生成的代码无论多么模块化,该写的testbench一点都不能省。我见过太多朋友拿到AI生成的代码就直奔综合,结果功能完全不对,浪费了好几天。模块化增加了可测试性,但不会替代测试本身。
如果你正在用AI辅助写RTL,强烈建议试试先把模块划分做出来,再让AI逐个实现。你会发现生成的代码不仅结构更清晰,而且后续调试和复用的成本会低很多。这篇就写到这里,希望能给你带来一些实用的启发。