ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

实测AI写Verilog:783道题揭示大模型在RTL设计的真实水平

实测AI写Verilog:783道题揭示大模型在RTL设计的真实水平

把同样一份 783 道 Verilog 考题丢给最近大火的大模型,回答会是什么?我趁着周末把 NVIDIA 开源的硬件代码生成基准整个跑了一遍:用 API 逐题调用、统一仿真判分、再抽出一批失败样例做人工复核。结论很有意思:AI 写简单组合逻辑真的能用,可一旦碰状态机、跨时钟域和模块例化,翻车概率高到让人不敢直接上板。这篇文章只讲实测,不吹不黑,把题目构成、评测流程、常见错误和工作流建议一次性说透。

我自己十几年的前端验证和 RTL 设计经历,加上这次 783 道题的全量回归,可以给一个比较明确的判断:AI 硬件编程已经过了“玩具阶段”,但离“可信赖同事”还差得远。下面这张图是我脑内的草图——大模型在组合逻辑上像熟练工,在时序控制上像刚毕业的实习生,在系统集成上基本是蒙着眼睛搭积木。文章后面我会用真实代码片段和统计数据把这些分层讲清楚。

1. 为什么我要拿 783 道 Verilog 题去“烤” AI?

1.1 大模型写代码,为什么芯片圈反而不买账?

现在软件圈对 AI 编程的态度相当热情,GitHub Copilot、Cursor、Claude Code 已经成了不少后端工程师的日常工具。原因很简单:软件代码有编译器、有单测、有 CI,错了马上能红,修了马上能绿,反馈链路很短。硬件不一样,RTL 代码写出来只是第一步,后面还挂着仿真、综合、时序收敛、 FPGA 上板实测这一长串流程。

更麻烦的是,Verilog 这门语言本身很“拧”。它看上去像 C,语义却完全不同。always块不是函数,reg不是变量,<=和=之间差着一整个硬件世界观。让 LLM 写一个 Python 排序函数,它大概率一次过;让它写一个带异步复位的 8 位计数器,它能在敏感列表和赋值风格上反复横跳。这也是为什么我坚持要用统一考题做批量测试——坊间那些“AI 教我写 RTL”的截图,十个里有九个只展示成功案例,失败案例全被剪掉了。

1.2 783 道题从哪来:NVIDIA 开源基准大致长什么样

这次用的基准是 NVIDIA 开源的硬件代码生成评测集,包含 783 道 Verilog 设计题。题目来源综合了常见数字电路教材、公开练习平台和工程场景,难度从“三输入与门”一路排到“带握手的 FIFO 控制器”。它和普通题库最大的区别是带了完整测试流程,每道题都有参考模型和自动 testbench,不会用“看着像”来评分,而是真的跑仿真比对输出。

我拿到数据集后先按内容做了分类。组合逻辑题 240 道,包括优先编码器、格雷码转换、二进制除法、加法器树;有限状态机题 180 道,覆盖自动售货机、序列检测、总线仲裁;时序逻辑题 181 道,主要是计数器、移位寄存器、同步 FIFO、流水线寄存器;模块例化和接口 118 道,重点考核跨模块连接和参数传递;剩下 64 道属于层次化和参数化设计,比如generate语句、多维数组和宏定义。这个结构基本还原了真实芯片前端的工作内容,所以拿来评 AI 是有说服力的。

1.3 评测口径:什么算“写对了”

我采用的是业界评测常用的 pass@1 和 pass@5 两个口径。pass@1 的意思是每道题只给模型一次提交机会,生成的 Verilog 直接进仿真,输出和参考模型完全一致才算通过;pass@5 允许同题生成五次,只要任意一次通过就算通过,反映的是模型在“可以反复试错”条件下的能力。

判分过程也做了严格化工序。首先检查语法,用 Verilator 做 lint 编译,语法不过直接记失败;然后接上官方 testbench 跑功能仿真,比对全部信号;遇到仿真超时的用例直接判负,防止模型生成死循环。最终成绩里,组合逻辑 pass@1 在七成上下,时序逻辑四成左右,状态机只有三成多,模块例化和接口类最惨,压到三成以下。整体加权后 pass@1 不到一半,pass@5 能提高到七成以上——这说明 AI 不是不会写,而是稳定性和人的监督仍然重要。

2. 三类考题拆解:AI 的强项和软肋

2.1 组合逻辑:能及格,但“不稳定”

组合逻辑是 AI 的舒适区。格雷码转换这种“公式型”题目,模型通常一句话就能给对。比如二进制转格雷码,标准写法是:

module bin2gray( input [3:0] bin, output [3:0] gray ); assign gray = bin ^ (bin >> 1); endmodule

这类代码没有状态,没有时序,只要知道公式就能写,LLM 的训练集里又堆了大量类似样本,所以 pass@1 冲到七成我一点不意外。真正让我皱眉的是它在同一个知识点上的“不稳定”。同一道 8 位优先编码器题,模型五次提交能给出两种正确方案和三种错误方案,错误方案里既有漏掉valid输出的,也有把casez优先级写反的。也就是说,它并不真正理解“优先级”的含义,只是在猜模式。

组合逻辑里还有个翻车重灾区是位宽。模型很爱写assign result = a + b;,但完全不关心a、b和result各自多少位。若端口定义是 8 位加 8 位输出 9 位,它经常输出 8 位,仿真时高位截断得悄无声息。这种错误比语法错误更阴险,因为编译不报错、波形也能跑,只有你拿着 spec 逐位核对才看得出来。

2.2 时序与状态机:错误高发区

时序逻辑和状态机这两类,是本次测试里最暴露问题的地方。让我先给个典型翻车代码,题目是“带同步复位的 4 位计数器,复位值为 0,计数到 15 后自动回绕”:

always @(posedge clk) begin if (rst) q = 4'b0000; else q <= q + 1'b1; end

仿真跑起来这个模块可能“能过”,因为 Verilog 仿真器对阻塞赋值和非阻塞赋值在单寄存器场景下区分并不严格。但如果你把这代码丢给综合器,综合器可能推断出奇怪的复位结构,严重时还会产生竞争。正确的时序块写法应该统一用非阻塞赋值:

always @(posedge clk) begin if (rst) q <= 4'b0000; else q <= q + 1'b1; end

模型为什么老写错?因为它把 Verilog 当 C 写,想到“赋值”就用=,完全没有“事件调度”和“硬件并发”的概念。它对状态机的处理更头疼。FSM 题通常包含状态编码、次态逻辑、输出逻辑三块,AI 经常把三块揉成一个巨型always块,少写default,或者漏掉某个转移条件,结果综合出来一堆锁存器。我用脚标统计过,180 道 FSM 题的 pass@1 只有 30% 出头,其中三分之一失败源于“漏状态”而不是“写错状态”。

2.3 模块例化与接口:从“写代码”变成“串工程”,AI 立刻露怯

很多宣传 AI 写硬件的案例都停留在“生成一个独立模块”,可真实工程里,RTL 开发大头是连接。把 UART 的 TX、RX、波特率发生器、FIFO 四个模块连成完整收发链路,这考验的是对端口、位宽、握手信号的全局把握,恰好是 LLM 的弱项。

118 道模块例化题暴露了三个高频问题。第一,端口连接顺序错。模型写例化时经常不写.port_name(signal),而是按位置一列了之,一旦模块端口顺序和样本不完全一致,整个连接全部错位。第二,参数传递遗漏。带#(.WIDTH(8))的例化往往丢一半参数,或把参数名写错。第三,跨模块信号冗余。它会把父模块的中间信号原样引出到顶层,搞出多驱动冲突。

I2C 读 EEPROM 那类题更暴露问题。模型能给出一个看起来很完整的 I2C 控制器框架,但练不练 ACK 应答、总线空闲时 SDA 和 SCL 拉不拉高,几乎全靠运气。这类代码不是“修一修能用”,而是“结构对了,细节全错”,给验证人员带来的工作量比手写还大。

3. 实测过程里的自动化评测与避坑

3.1 提示词怎么构造:只给题目,还是连 I/O 都点明?

我在正式跑题之前先做了提示词敏感性实验。同一道题分别用三种方式问:只给英文题干、给中文题干外加端口列表、给完整接口和自己的单元测试用例。结果很明显,第三种方式的 pass@1 比第一种高约十五个百分点。原因不复杂,模型猜接口会猜错,你把端口名字和方向给它,它就可以把精力集中在内部逻辑上。

我最后统一采用的提示词模板长这样:

用 Verilog 实现一个同步 FIFO,宽度 8 bit,深度 16。 端口定义: clk:时钟,上升沿有效 rst_n:异步复位,低有效 w_en:写使能,高有效 w_data[7:0]:写数据 r_en:读使能,高有效 r_data[7:0]:读数据 full:满标志,高有效 empty:空标志,高有效 要求:读空时 r_data 输出 0;满时写操作无效。代码风格用非阻塞赋值。

注意我不仅给了端口,还把“边界行为”也写进去了。这在真实的芯片项目里就是一份简化版规格书,规格书越清楚,模型产出越靠谱。反过来说,如果你的需求本身含糊,AI 写出来的东西一定更含糊,这个锅不能甩给 AI。

3.2 仿真判分流程:Verilator 编译、testbench 套件、波形核对

评测脚本我用的是 Verilator 加 Icarus 双保险。Verilator 做 lint 检查,重点抓always块敏感列表不完整、变量位宽不匹配、隐式锁存;Icarus 跑功能仿真,输出 VCD 波形供人工复核。整个流水线用一段简单脚本串起来,每道题经历三步:生成代码、写顶层 testbench、编译并回归。

这里有一个细节值得说:testbench 不能只跑一组激励。比如计数器题,只跑三拍时钟谁都对;我把激励扩展成“复位释放、连续计数、随机使能、边界回绕”四段,大约 500 个时钟周期,能让隐藏 bug 的暴露概率高得多。基准里自带的 testbench 本来质量就不错,我又额外加了一层随机约束,避免模型靠“背答案”过关。

判分时遇到过一个坑:模型喜欢在代码里加initial $display或#10 $finish,导致仿真提前结束,把正常执行时间截断了。后来我在编译阶段直接 Ban 掉$finish和$display,只保留assert和覆盖组相关语句。这提醒我,AI 生成物里经常夹带非预期行为,评测脚本必须把它当“可疑代码”看待。

3.3 生成代码跑不通,常见是哪几类原因

我统计了 783 道题中所有失败样例,把问题归成了六类。做成表你看着更直观:

错误类型典型表现出现频率解决思路
语法错误端口少分号、begin/end 不匹配中交给 Verilator 重编,AI 修得快
敏感列表问题时序块漏posedge clk,组合块乱加时钟高提示词里强写“敏感列表只含时钟”
位宽不匹配8 位加 8 位输出 8 位,溢出被截高review 时逐位核对端口宽度
状态机漏状态case没有default,隐式锁存高lint 开 warning,锁存直接报错
功能语义偏差读空时仍输出旧数据中高增加边界用例做定向验证
不可综合风格循环里用while、always内用fork低加综合环境约束,不可综合即失败

每一类都不是孤立现象。比如“隐式锁存”经常和“漏 default”同时出现,解决其中一个,另一个也会好转。所以实际修复时,我不会让 AI 反复盲试同一道题,而是把 lint 报错原样贴回去让它修改,通常两三次就能修到通过。

4. 高分背后,那些“看起来对、一综合就塌”的现象

4.1 宽度、进位、截断:RTL 最“讲理”,AI 最不讲理

仿真能通过不代表综合没问题,这是我这次最深的体会之一。举个实际例子,一道做亮度均值滤波的题,模型写出了这样的累加逻辑:

reg [7:0] acc; always @(posedge clk) begin if (acc >= 255) acc <= 255; else acc <= acc + 8'h8; end

仿真中看起来没什么问题,可综合工程师一眼就能看出问题:acc + 8'h8的中间结果必须要 9 位才能完整表示进位,但代码里acc只有 8 位,综合器会把进位悄悄丢掉。你必须写成reg [8:0] acc_tmp;或者使用8'd8的补码技巧才能保住进位。模型不会主动替你想这些,它能给出“语义正确”的代码,但宽度问题留给你的时序和面积报表买单。

另一个宽度相关的错误是常数比较。模型经常写if (cnt == 15),而不是if (cnt == 4'd15)。前者在 Verilog 里会把 15 默认成 32 位有符号整数,综合出一个 32 位比较器,面积白白翻倍。这种“功能等价、但实现极差”的代码,过了仿真也上不了板。

4.2 组合逻辑“闭卷”与意外锁存器

锁存器是 ASIC 设计里的大忌,而 AI 很擅长制造锁存器。它的典型操作是:把组合逻辑写进always @(*),但某些分支没赋值,系统为了保持输出只能推断出锁存。这次实测里,FSM 类的 output 逻辑几乎每三道题就有一个锁存警告。

比如一个序列检测器,状态机输出应该在MOORE或MEALY中用assign给值,但模型写成了:

always @(*) begin case (state) S0: if (data_in == 1'b1) out = 1'b0; S1: out = 1'b1; default: out = 1'b0; endcase end

没有default的 case,加上S0分支里out只在if内赋值,综合器会认为“某些条件没定义输出”,于是生成锁存。这个问题 lint 能查出来,但只有你把 lint 放进评测流程才会发现,单靠肉眼 review 很容易漏掉。

我后来在提示词里加了一句“所有组合逻辑块必须在所有分支和所有条件下完成赋值,且必须包含 default”,错误率立刻降了三成。这充分说明,模型不是学不会规则,而是你如果不在需求里点名,它就按最省事的模板来了。

4.3 综合概念缺失:时钟频率、面积、功耗统统不管

AI 写代码时完全没有“硬件资源”概念。让它实现一个 32 位整数除法器,它可能直接写一个while循环,仿真时 behavior 正确,可综合器根本不知道该怎么把它拍平到查找表和触发器中。换成状态机实现除法,它又容易把状态变量和迭代变量混在一起,分不清哪些信号该在时钟沿更新。

更典型的是它对“并行”的误解。软件思维认为“代码行按顺序执行”,硬件思维是“每个 always 块天然并行”。模型经常把两个不相关的信号写进同一个 always 块,导致它们被强制串联。有些题明明可以用独立并行赋值完成,它偏要在中间插入一个中间寄存器,白白增加流水级数。这些都是功能过了、性能崩了的典型案例,光靠仿真测试完全看不出来。

5. 大规模“AI + RTL”工作流怎么搭才不翻车

5.1 哪些模块可以放心交给 AI 写草稿

经历完整测试后,我对 AI 能干的活有了相对清晰的边界。第一类是纯组合转换逻辑,像格雷码、CRC、奇偶校验、字节序转换,这类题 pass@1 高,生成结果基本能直接进 review;第二类是简单状态机骨架,只要把状态编码和转移条件给全,它给出的框架比手写快得多;第三类是测试代码和脚本,比如生成 UVM sequence、写覆盖率采集代码、整理接口文档,这些不是核心逻辑,错了也不影响流片。

我用这些模块试过几次真实项目,最大的收益是“起手式”阶段。原来写一个新模块,光搭端口声明和主模块框架就要二十分钟,AI 十秒给完整版,我在上面改业务逻辑反而更快。 AI 是很好的“草稿机”,但草稿和成稿之间必须隔着一道验证海。

5.2 哪些模块建议别用 AI 直接生成

反过来说,有几类模块我是强烈不建议让 AI 直接生成的。跨时钟域相关,比如异步 FIFO、脉冲同步器,模型没有理解亚稳态和格雷码同步的精髓,生成代码看着像,实际用起来大概率出问题;复杂握手协议,像 AXI、PCIe、DDR 控制器,模型生产不了带背压和乱序处理的逻辑;复位架构和时钟门控,这是整个芯片的命脉,交给 AI 相当于把一个城市的交通调度交给只会开车的司机。

还有一类容易被忽略,就是包含大量状态标志位的“半时序半组合”模块。比如浮点运算单元里的舍入状态机,状态之间既有组合判断又有寄存器缓存,模型经常把判断逻辑嵌在两个时序块里,导致功能正确却多了一拍延迟。要想改对,你必须对算法本身足够熟,否则 AI 的“看似合理”会让你花更多时间 debug。

5.3 前端验证工程师的“AI 结对”实操流程

如果你和我一样做前端验证,我建议把 AI 当成验证流程里的辅助角色,而不是主笔。我的日常工作流分五步。第一步,需求拆解:把模块功能写成一条条可验证的 acceptance criteria,比如“当 FIFO 满时写操作无效”、“读空时数据总线为 0”。第二步,提示词生成:把接口和验收标准传给 AI,让它产出初版 RTL。第三步,静态检查:用 Verilator 跑 lint,把 warning 全部清零。第四步,定向验证:手写或让 AI 生成一组定向 testbench,覆盖所有边界条件。第五步,随机验证:接入约束随机验证,跑大量回归,用覆盖率倒逼代码修改。

这五步里,花费时间最多的是第四步和第五步,而不是第一步和第二步。换句话说,AI 把“写代码”这个动作压缩了,但没有压缩“验证”这个真正核心的环节。谁要是觉得 AI 能连验证一起做掉,那他大概率是在拿流片当赌注。

5.4 给 AI 的提示词模板(可以直接抄)

结合实测,我整理了一个可以复用的提示词模板。它可能不华丽,但非常出活:

你是一名资深数字前端工程师。请用 Verilog 实现 [模块名]。 功能描述: [把你要实现的行为用白话写清楚,越具体越好] 接口定义: [列出全部端口名称、方向、位宽、有效电平] 关键约束: 1. 时序逻辑全部使用非阻塞赋值 <= 2. 组合逻辑 always 块需要包含 default 分支且所有路径都要赋值 3. 所有比较操作需写明位宽,例如 cnt == 4'd15 4. 敏感列表只包含规定时钟和复位,不要附加多余信号 5. 不要使用 initial、while、fork 等不可综合结构 边界行为: [描述复位值、满/空/使能无效时的信号输出] 请先给出完整 Verilog 代码,再列出你对该代码的自查清单。

模板里每句话都有对应坑。非阻塞赋值是综合风格基础,default 分支是防锁存,位宽是保面积,敏感列表是防仿真与综合不一致,禁用不可综合结构是保能上板。再加上“自查清单”三个字,模型会在输出前多过一遍脑子,实测能减少 20% 左右的低错。

6. 实测后的几句“内行”体会

6.1 用 AI 写硬件,比我预期的快,但没我预期的稳

783 道题全量跑完,最大的感受是“快”和“稳”之间存在巨大鸿沟。模型生成一段能通过仿真的 RTL,平均只要十几秒;可让它连续十道题稳定保持同一质量,几乎不可能。同一道题,它第一次可能给你最优解,第二次就犯个低级错误。这种不稳定在写软件时还能忍,因为测试能兜底;在写硬件时就很危险,因为测试通过并不等于实现可综合、可收敛。

对我个人而言,AI 已经替代掉了大约三成“体力型代码工作”,但离替代“工程师判断力”还差得很远。它适合做预案,不适合做决策。

6.2 检视代码比写代码更值钱

这次回归测试里最花时间的其实不是跑测试,而是复核那些“仿真过了但风格怪异”的代码。我抽了 50 个通过用例做人工 review,发现其中相当一部分存在位宽隐患、锁存风险和低效逻辑。如果只看仿真结果,这些代码会被当成“行”;只有人眼盯着看,才能看出它们上板后会是什么下场。

所以我劝一线验证同学,千万别把 AI 生成的结果当成可信交付。你最重要的技能是 code review 和验证设计。你越会设 boundary condition,AI 就越老实。

6.3 别指望 AI 代替验证,但可以拿它当“第二双眼睛”

最后一句话送给还在观望的人。AI 写硬件代码这件事,现阶段最合理的定位是“第二双眼睛”:用它快速生成初稿,用它审查自己的代码风格,让它帮你搭一套初始 testbench 和覆盖率框架。真正做版本决策、签核验证、时序收敛的,依然是你自己。如果你把验证设计得足够严密,AI 会成为很趁手的效率工具;如果你放松验证这道关口,AI 会让你加班加得比手写还惨。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进