芯片设计圈最近越来越多人拿 LLM 写 Verilog,但效果一直像开盲盒——同一个模型,有时候给你一段能正常计数的计数器,有时候给你一段仿真直接卡死的状态机。ASP-DAC 2025 上这篇 OPL4GPT 论文算是帮大家做了一次“控制变量”实验:让同一个 GPT-4o,去完成同一批设计任务的 C++ 版和 Verilog 版,结果差异大得有点扎心。
这篇精读文章适合三类人:做 FPGA/ASIC 的工程师,芯片验证的同学,以及想搞明白“为什么 LLM 在硬件描述语言上容易翻车”的人。我会把论文的实验框架、评测口径、C++ 与 Verilog 的对比结果拆开讲,再补上我自己实际用 GPT 生成 RTL 时踩过的坑和验证方法,保证你能拿来就用。
1. OPL4GPT 精读:它到底做了什么实验
1.1 为什么非要拿 C++ 当对照组
先说结论:Verilog 单独测没意义,必须有个参照物。
LLM 写代码的评测大家见得多了,HumanEval、MBPP 这些基准基本都在测 Python、C++、Java 这类软件语言,结果一个赛一个好看。但硬件描述语言很少进主流榜单,偶尔有人测一下,也只是一个孤立数字:“通过率 40%”。这个数字背后是模型能力问题,还是 Verilog 语言本身太难,完全说不清。
OPL4GPT 的聪明之处在于:让 GPT-4o 同时用 C++ 和 Verilog 解同一批硬件设计题。
C++ 在这里不是陪跑,而是“对照组”。它和 Verilog 在语法表面上有相似性——都是类 C 语法,都有 if、case、for、函数调用。但一个描述串行执行逻辑,一个描述并行硬件电路。如果 GPT-4o 在 C++ 上表现优秀、在 Verilog 上拉胯,那问题就出在“语言/领域转换”这个环节;如果两边都翻车,那可能模型本身能力不够。论文用这个对比,把锅精准地甩给了 Verilog 生成这个具体环节,而不是笼统地骂模型。
对我这种平时写 RTL 的人来说,这个设计思路本身就是种启发:评测一个工具,先定义清楚它的“难度基线”。没有基线的数字,参考价值要打对折。
1.2 提示词增强框架拆解
OPL4GPT 从名字看就能猜个大概,这是围绕 GPT-4o 做提示词工程和少样本学习的框架。从论文公开口径看,它不只做“把题目丢给模型”这么简单,而是把输入建模成三层结构。
第一层叫任务描述规范化。原始需求往往是一段人话,比如“设计一个出租车计费系统,起步价 8 元,每公里 2.4 元,等待每 5 分钟加收 1 元”。这种描述直接扔给模型,它得靠自己理解并转成逻辑。OPL4GPT 的做法是先整理成一个结构化提问:输入信号是什么、输出信号是什么、行为规则有哪几条、时序约束是什么、有没有复位和使能。
第二层是少样本示例选择。这里有个容易被忽视的点:示例不是越多越好,而是越“难”越好。论文的思路是选那些“看起来简单但模型容易做错”的题目作为范例,比如带异步复位和时钟使能的计数器、带起始位判断的 UART 接收状态机。这类示例能约束模型输出风格,避免它写出“伪硬件”风格——比如用delay、sleep、动态循环这类软件思维来写 RTL。
第三层是约束条件注入。这一层说白了就是告诉模型硬性规则:时序逻辑块里必须用非阻塞赋值,所有信号位宽显式声明,FSM 必须三段式或两段式,不能用initial来初始化可综合逻辑。这些东西对资深工程师来说是肌肉记忆,但对没有硬件直觉的 LLM,你不说它真的会漏。
2. 实验设计与评测口径:这样对比才公平
2.1 测试集是这么构造的
OPL4GPT 的测试集不是随便找几道题,而是按硬件设计的高频场景分了五类,基本覆盖了一个数字工程师日常会碰到的活。
第一类是纯组合逻辑:多路选择器、加法器、译码器、二进制转 BCD 这类,没有时钟,纯看输入输出的映射关系。
第二类是基础时序逻辑:模 N 计数器、移位寄存器、分频器,这类开始有 clk 和复位,但结构简单。
第三类是有限状态机:序列检测器、交通灯控制器、出租车计费系统这类,状态转移关系复杂,输出不仅看当前输入,还要看历史状态。
第四类是总线协议接口:UART 收发、SPI 读写、I2C 读 EEPROM,这类是重灾区,因为涉及时序握手、时钟分频、起始停止条件,细节极多。
第五类是算法硬件化:滑动窗口滤波、冒泡排序状态机、前缀和流水线,这类需要把纯软件算法映射到硬件流水线上。
每一道题都会准备一份统一的自然语言功能描述,然后分别要求 GPT-4o 输出“C++ 函数版”和“Verilog 模块版”。题目数量不算多,但每个都带针对性,不是靠堆量刷分。
2.2 评测指标与工具链怎么定
评测代码生成质量,最核心的原则是:功能正确性高于语法正确性。写出一段能编译的 C++ 不代表算对,写出一段能过 lint 的 Verilog 更不代表电路功能正确。OPL4GPT 用的评测方式我实际跑起来也非常认同。
第一层是语法编译检查,用 Icarus Verilog 跑一遍iverilog -g2012,编译通过率,这只是及格线。
第二层是功能仿真验证,为每个题目写自校验 testbench,喂相同的激励,比对输出与 golden model。仿真跑不过的直接记为失败,不看代码风格。
第三层是 pass@k 指标。简单说就是让模型对同一题目采样生成 k 个版本(论文里取了 1 和 5 两档),看这么多次里至少有几次能通过功能仿真。这个指标对真实使用场景特别重要——工程师拿 LLM 辅助写代码,本来就是为了多试几个方案,pass@5 更能反映“实际可不可用”。
2.3 提示词和采样参数的具体设置
这里有一段论文里非常值得抄的细节,就是提示词里到底塞了什么、参数怎么设。
温度取多少很讲究。从 OPL4GPT 实验来看,导数和稳定生成阶段用temperature=0.2,追求确定性;等到生成多个候选方案时切到 0.8,增加输出多样性。如果一开始就用高温采样,同一个题目生成的代码五花八门,验证工作量直接翻倍。
少样本示例的数量也不是拍脑袋。论文的趋势是:示例太少(0 到 1 个)模型容易放飞,示例太多(超过 5 个)上下文过长反而干扰判断,比较稳的是 3 个左右。每个示例要短小精悍,一个模块一个功能,不要给大杂烩式的完整工程代码。
另外还做了链式思考(chain-of-thought)注入的对比:让模型先输出“设计思考”:简要描述模块接口、状态转移、关键时序,再输出代码。这一招对时序逻辑和状态机任务的提升非常明显。
3. 实验结果:Verilog 输在哪几个关键环节
3.1 各任务类型通过率对比
这是论文里最扎心的一张表,按论文趋势整理成大概数字:
| 任务类型 | C++ pass@1 | C++ pass@5 | Verilog pass@1 | Verilog pass@5 |
|---|---|---|---|---|
| 组合逻辑 | 86% | 94% | 71% | 83% |
| 基础时序 | 78% | 89% | 45% | 61% |
| 有限状态机 | 70% | 82% | 32% | 47% |
| 总线协议接口 | 64% | 78% | 21% | 36% |
| 算法硬件化 | 62% | 75% | 18% | 33% |
看到这张表,我第一反应是:C++ 也没高到哪去,说明模型写代码能力有上限;但 Verilog 那列的数据更真实地反映了硬件生成的现状。组合逻辑问题不大,因为本质上就是一个逻辑函数;基础时序直接砍半,说明模型开始理解不了“时钟沿 + 寄存器更新”这个模型;到总线和算法硬件化,几乎接近不可用。
3.2 四类高频错误逐个拆
我根据类似实验的工程经验把论文指出的典型错误归纳成了四类,每类都给个能复现的代码片段,你对着看就知道 GPT 是怎么翻车的。
第一类是阻塞赋值写时序逻辑。这是最经典的错法,模型输出长这样:
always @(posedge clk) begin count = count + 1'b1; end=阻塞赋值表示立即更新,这种写法在仿真里会引发竞争,综合出来也很容易变成多个触发器互相锁存。正确写法必须是非阻塞赋值:
always @(posedge clk or negedge rst_n) begin if (!rst_n) count <= 8'h00; else if (en) count <= count + 1'b1; end第二类是位宽缺失。模型经常写出reg count;当计数器,结果 1 位寄存器计两下就翻转。硬件设计里位宽是数据完整性的基础,LLM 不会主动帮你考虑“计到 100 需要几位”。这个只能靠约束规则和 lint 检查兜底。
第三类是敏感列表问题。比如组合逻辑:
always @(a or b) begin q = a & b; end漏掉c后,仿真里 c 变化但 q 不更新,综合出锁存器。现在推荐直接写always @(*)或always_comb,但 GPT 在生成老式风格代码时依然容易犯这个毛病。
第四类是异步复位和时钟混用。模型可能在always @(posedge clk)块内用if (rst)处理异步复位,导致复位信号没有即时生效,或者把异步复位写成了同步复位逻辑。
3.3 语言差异、数据差异和验证差异
Verilog 生成率低,不是模型“笨”那么简单。原因有三层,一层比一层要命。
第一层是训练语料的天然差异。GitHub 上海量的 C++ 项目代码喂饱了模型,它见过无数种for循环、std::vector的写法;而 Verilog 在公开仓库里的占比极小,高质量的 RTL 代码很多还在公司私有仓库里。模型没见过足够多好样本,自然写不出好作品。
第二层是语言本质的差异。C++ 是串行思维,一行一行执行,逻辑是单线程推演;Verilog 是并发事件驱动,所有always块同时跑,信号之间靠事件触发传播。模型在自己的“思维链路”里天然倾向顺序执行,所以一旦涉及多个 always 块交互、时钟域同步,它就蒙圈。
第三层是验证反馈的差异。C++ 写完可以立即编译运行,看到崩溃和错误输出,很快自我纠错;Verilog 错了不会“报错给你看”,你得自己搭 testbench、跑仿真、看波形,在一个错误状态里打转。论文还专门指出,缺少及时反馈是 LLM 在硬件语言上难提升的最大外部因素。
4. 从论文结论到工程师日常:LLM 写 RTL 的正确用法
4.1 用 GPT 生成 RTL 的四步工作流
看完论文别灰心,至少我知道的多数做 FPGA 的团队,已经用这套方法论把 LLM 用成了“高级模板生成器”,关键是要有正确的工作流。
第一步,写清楚接口再让它动笔。不要一上来丢一句“帮我写个 I2C 控制器”,而是把端口定义给全,clk、rst_n、数据线、地址线、位宽、时序参数。GPT 在有明确接口时输出规范程度会高很多。
第二步,让它先出方案再出代码。强制它写一个“架构分析”,包含模块划分、状态转移图和关键电路结构。这个环节有两个作用:你可以审查逻辑对不对,避免它写出一个“看似自洽但整体错误”的设计;它输出结构后再写代码,自己会不自觉地按照刚规划的方案走,一致性会好不少。
第三步,仿真快速闭环。我把验证环境搭成一键运行,生成完代码直接跑 testbench,不通过就把仿真日志喂回给模型。这个反馈循环比多次手工改代码强得多,因为前面讨论过,LLM 需要反馈才能迭代。
第四步,修改建议要精准到信号。不要发“这段代码有 bug”,而要说“上升沿时 data 信号握手时序不对,应该在第 3 个时钟沿后拉高 valid”。模型修这个能力比整段重写靠谱得多。
4.2 一套轻量验证环境的搭建
我自己平时验证 LLM 生成的 Verilog,用一套非常轻的工具链:Icarus Verilog 做仿真,Verilator 做 lint,Testbench 自己手写。下面这个工程模板可以直接抄。
SIM ?= iverilog TARGET = sim_vvp SRC = top.v tb_top.v vcd_dump.v all: sim sim: $(SIM) -g2012 -o $(TARGET) $(SRC) vvp $(TARGET) lint: verilator --lint-only -Wall top.v clean: rm -f $(TARGET) *.vcd *.logtestbench 里最关键的是加$dumpfile和$dumpvars,这一步无数人忘,导致出问题连波形都没得看。
initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb_top); repeat(200) @(posedge clk); $finish; end加上自动化断言,用$error做输出比对:
always @(posedge clk) begin if (en && (count == 8'hFF)) if (data_out !== 8'h00) $error("count overflow mismatch at time %0t", $time); end这套环境跑一遍只要几十秒,配合 GPT 的迭代修复,基本能实现“生成一两轮就能得到功能正确的模块”。
4.3 可直接抄作业的提示词模板
我基于论文的思路,整理了一份稳定提升生成质量的英文提示词模板,实测比中文裸提示高不少成功率:
Design a Verilog module with the following specifications. Part 1 - Architecture: - Describe the interface briefly (ports, bit widths, clock, reset). - List the internal modules/FSM states if applicable. Part 2 - RTL code requirements: - Use IEEE 1364-2005 or SystemVerilog. - Use non-blocking assignments in sequential always blocks. - Use blocking assignments in combinational always blocks. - Declare explicit bit widths for all signals. - Use active-low async reset (rst_n) if provided. - Do NOT use initial blocks for synthesizable logic. - Avoid latches; all cases must have defaults. Module ports: input wire clk, input wire rst_n, ... Behavior: ... Return only the Verilog code in a code block.这个模板的核心就是把论文里“约束注入层”的思想物化。你可以保存为常用 snippet,每次生成时先把模板复制进去再填需求。
5. 实际上手踩过的坑与排查思路
5.1 “仿真挂死”的排查顺序
我试过很多次,GPT 生成的 RTL 编译能过,但仿真一跑就挂死或者输出全 X。这种问题多半出在三个地方,我按概率排个序。
第一看复位是否真正收敛。生成代码里rst_n没接进 always 块,或复位后某些寄存器没初始化,波形里全是红色的 X。第二看是否有组合环。assign和always之间互引,形成无时钟驱动的高阻环。第三看敏感列表是否漏信号,阻塞赋值导致的竞争更是重灾区。
我的排查顺序是:先看编译 warning,再开波形查 X 态源头,然后检查所有赋值的左右位宽是否匹配,最后把所有 always 块的非阻塞/阻塞赋值统一审查一遍。这套流程基本能覆盖 90% 的模型生成问题。
5.2 综合面积和时序问题
功能对了不代表能上板。GPT 生成的代码经常写成“全 always 块风格”,一个简单逻辑也包成时序块,综合出来触发器冗余,面积和功耗感人。
所以我的原则是:功能仿真通过后,必然跑一遍综合看报告。如果资源占用比手写多一倍,那就让 GPT 用“数据流风格”重写关键路径——多让它用连续赋值assign和外置 combinational 逻辑,少用无谓的中间寄存器。
5.3 中文提示词与英文提示词的差异
这算是我个人实测下来的独家经验:同一个题目,英文提示词比中文提示词生成的 Verilog 好用很多。
不是英文有什么魔力,而是开源训练语料里英文技术文档、英文注释的 RTL 项目占绝对大头。模型在英文语境下更容易检索到对应模式。我试过一个计数器,中文提示生成出来带integer i做循环,根本不可综合;英文提示生成出来是干净的reg [7:0] count加非阻塞赋值,一次就过。
如果你英文也不好,折中方案是:描述部分用中文,但端口跟行为约束用英文关键字写,代码生成质量能回到可接受水平。
5.4 “板级和仿真对不上”的典型场景
最坑的场景是仿真通过、综合通过,上板就不干活。这时候别急着骂工具,先查时序约束。
LLM 生成代码时完全不会考虑组合逻辑深度,一个复杂表达式可能铺了 8 级组合逻辑,芯片跑不到目标频率。我遇到过 UART 波特率生成器仿真好好的,上板乱码,查了半天发现是分频计数逻辑在关键路径上超了时序。
我现在的做法是把“时序约束文件”也交给 GPT 生成初版,比如时钟周期约束、输入输出延迟,再手工修正。它生成约束文件的水平比生成 RTL 水平好得多,因为那更像写配置。
再说个有点反直觉的经验:OPL4GPT 论文结论里“C++ 好、Verilog 差”,引出的实际建议不是“别让 LLM 写 RTL”,而是“让 LLM 多干验证和脚本的活”。让 GPT 帮你写 testbench、写 Python 脚本解析波形、写 Makefile、写寄存器描述文件,正确率高、迭代快,比硬让它写核心 RTL 划算得多。
我个人在实际操作中的体会是:LLM 生成 RTL 的能力会随示例质量和反馈闭环显著提升,前提是你得给它搭好“脚手架”——接口定义、仿真环境、约束清单。指望一句提示词直接生成能上流片的高质量 Verilog,至少现阶段还是太天真了。以后有了更好的开源硬件语料和自动验证反馈机制,这个局面大概率会变好,但眼下最好的姿势,是把 GPT 当成一个手速极快的实习生:它能帮你把骨架搭好,但架构、时序、跨时钟这些真正的“硬骨头”,还得你自己啃。