回想这一周,我几乎每天都要和AI生成的RTL代码斗智斗勇。不是因为我闲着没事,而是项目里确实需要一块新的数据通路模块,我想试试现在这些大模型到底能不能帮我把Verilog写出来。结果呢?代码是写出来了,但综合、仿真、调试加起来的时间,比我手写整整多了一倍。这大概就是标题说的“第一批用AI写RTL的人,快被折腾疯了”的真实写照。
但也别急着唱衰。折腾归折腾,用了几轮之后,我反倒摸出了一些门道——AI不是不能用,而是用法和你平时刷短视频看到的“一键生成代码”完全是两码事。如果你也想拿AI写RTL,这篇文章里的坑你大概率会踩到,我把它们全部拆开讲清楚,顺便整理一套能让AI少给你挖坑的工作方法和排查思路。
1. 为什么有这么多工程师开始把AI当RTL搭档
1.1 从复制粘贴到代码生成:我的第一次尝试
事情起源于一周前,新项目里需要一个数据宽度可配置的同步FIFO。这种事放在以前,我直接在旧项目里挑一个功能相近的模块,改改参数就能用。但那天正好刚看完大模型写代码的演示视频,我心里痒痒,就把键盘交给了AI。
我的提示词其实只给了个大概:“写一个同步FIFO,宽度8位,深度16,带full和empty信号。”AI很快就吐出了一段很工整的Verilog代码,端口声明、寄存器声明、assign语句、always块,一样不缺。第一眼看过去,感觉比很多初级工程师写的还规范,我当时还挺兴奋。
结果问题在仿真阶段暴露了。我给写逻辑和数据逻辑各写了几个方向性的测试,当写指针走满一圈回到0时,empty信号被拉高,和满信号同时为1。理论上一个FIFO不应该既能读又能写时同时报满空,但它的代码里empty的生成逻辑根本没考虑指针回卷的情况。
我盯着波形看了半天,又看了看AI生成的代码,终于在地址计数器的处理上发现它把计数满和存储索引混在了一起。这段代码如果直接进综合,功能上也不会全错,但边界场景一跑就会出协作问题。我把错误信息丢回给AI,它倒是很快改了,可改完以后又冒出新的flag错误。来来回回改了三四轮,最后还是我把那一段计数器逻辑重写了一遍,才算收场。
那次体验最大的感受是:AI确实能生成代码,但它只负责“生成”,并不负责“正确”。你让它在字节层面写一个计数器和读写逻辑,它可以做到;但如果你期望它像资深工程师一样把边界条件和模块之间的握手关系一次想清楚,那真的是想多了。
1.2 AI到底能替RTL工程师做什么
虽然第一次就摔了跤,但我不觉得AI在RTL设计里是废物。相反,经过这一周的折腾,我划清了它的能力边界——哪些环节交给AI是提升效率的,哪些环节交给AI纯粹是给自己找罪受。
目前我觉得AI能干得不错的事情有这么几类:
- 代码骨架生成。端口列表、参数定义、模块例化结构、常用寄存器的定义这些,属于纯粹的文本生成,AI做得很熟练。
- 简单模块的自动补齐。比如基础的计数器、移位寄存器、简单的状态机、多路选择器逻辑,这些几乎全行业都有大量相似代码,AI见过的远比我们多。
- testbench的脚手架。生成时钟、复位、任务task、简单的数据激励,这些本来就很模板化,AI写得又快又省心。
- 语法检查和代码风格整理。把没对齐的端口头对齐,把复杂的if-else改成case,把明显重复的always块合并,这些操作AI表现得完全可以信赖。
而AI干不了也不能干的事是:
- 跨时钟域同步策略的设计,它不知道你的两级同步器应该放在哪条路径上;
- 复位释放的时序逻辑,它不知道你是异步复位还是同步复位,更不知道复位释放信号是否已经同步到目标时钟域;
- 状态编码选择,独热码、格雷码还是binary码,AI默认只会用最简单的写法;
- 面积和功耗优化,它从不看综合报告,它的目标只有“符合你给的自然语言描述”。
所以我不建议把AI当成一个全知的RTL生成器。更准确的比喻是:它像一台执行力很强的打字机,你告诉它每个信号何时拉高何时拉低,它帮你把代码文本敲出来。它没有电路直觉,但可以帮你省掉大量敲代码的时间。
1.3 第一批尝鲜的人为什么会被折腾疯
网上到处是“AI已经会写代码了”“AI要取代程序员了”的说法,但真正拿着AI去写RTL的人,几乎都经历过从惊喜到崩溃的心理过程。我觉得根源有三个。
期望错位是最大的问题。大多数工程师看AI演示时,看到的都是拿Python写排序、拿JavaScript调接口,这类任务本质上是文本生成和数据结构处理,AI确实擅长。但RTL是描述硬件电路,代码的每一个if、always块、赋值语句,最终都要变成一个实实在在的电路。AI在生成代码时,根本没有“这个always块会产生一组寄存器,那个assign语句会综合成一个多路选择器”这种概念。它只是在语言层面模仿人类的代码写法,怎么可能替你保证电路正确。
反馈链条太长也是个问题。写Python代码,跑一下就知道结果对不对;写RTL,你得经过仿真、综合、时序分析几个环节才能发现错误,而且有些错误是潜在的,比如错误的状态编码可能会导致综合后面积大了一倍,但仿真它很完美地通过了。AI不像人类工程师那样能从综合报告里吸取教训,你让它“把面积缩小一点”,它只能无所适从地给你改几个参数,甚至越改越糟。
再加上调试成本高。AI生成的代码里没有注释,没有设计意图说明,出了问题你得像读别人留下来的祖传代码一样去猜它当时为什么这么写。发现问题后把它丢给AI改,AI改完给你一个新的错误,你又得继续把新错误丢回去。几次循环下来,你会发现时间全花在无效沟通上,还不如自己动手。
所以第一批尝鲜的人被折腾疯,本质上不是AI技术本身不行,而是使用方式完全错了。把它当成一个实习生,而不是一个资深专家,心态就会平和很多。
2. 拆解AI写RTL的底层逻辑:它能看懂时序吗
2.1 大模型是怎么“理解”Verilog的
很多人搞不清楚,为什么AI生成的Verilog能过语法检查,甚至会通过部分仿真,却总在综合或边界测试中露出破绽。要理解这一点,得先想明白大模型生成的原理。
大模型本质上是在学习人类语言的概率分布。它用海量的代码语料训练出来,知道什么样的代码文本在统计上更常见。你给它一句“写一个带异步复位的D触发器”,它生成的代码,是它在历史语料中见过的类似代码的某种“平均模式”,而不是从电路原理出发推导出的设计。
这就是为什么AI生成的东西往往看起来有模有样。因为真实世界里的Verilog代码长得都一样:端口声明、always块、阻塞或非阻塞赋值、module和endmodule。AI把这些高频文本模式烂熟于心,组合起来非常流畅。
但真实电路里,一个信号从一个时钟域到另一个时钟域需要同步器,一个计数器溢出会影响状态跳转,一个状态寄存器在IDLE状态下如果复位无效会导致系统崩溃——这些都是电路世界的“隐含规则”。这些规则不会直接显式地写在你给AI的prompt里,AI也没法从prompt里推断出来,它只能凭语料中的统计相关性来做猜测。
我做过一个很形象的类比:让AI写RTL,就像让一个熟读了很多菜谱却从没摸过锅铲的人给你做一道红烧肉。他写的配料表、切菜方法甚至火候建议都有板有眼,放在纸面上谁也挑不出错。但真到灶台前,糖色什么时候放、火开多大、肥肉什么时候煸透,这些“肌肉记忆”他根本给不了你。RTL也一样,时序、复位、握手、流水线,这些东西都是物理世界的约束,不是文本概率能覆盖的。
2.2 综合与仿真为什么总跟AI生成的代码过不去
如果你和AI写RTL的代码打过几次交道,可能都会遇到以下的场景:仿真通过了,波形看着也对,但综合器一跑就是一堆warning。有些是latch,有些是多种驱动,有些是组合逻辑环。
问题出在AI习惯于“文本上怎么像样就怎么写”,而不是“电路上怎么合理就怎么写”。举个例子,对于时序逻辑,规范的做法是always块中统一使用非阻塞赋值,并把所有要触发的信号列在敏感列表里。AI很容易生成两个或多个always块都对同一个寄存器赋值的情况。仿真时你把所有语句都按顺序执行一遍,可能看起来没毛病,但综合工具会认为这相当于多个驱动源,直接报错或者综合出你完全意想不到的电路。
再比如可综合性。Verilog里有不少语句是仿真专用的,像initial、fork/join、延时控制等。AI在做testbench模板的时候很擅长生成这类语句,但它有时会把同一个模块里的仿真语句和可综合语句混在一起。仿真能过,综合却会把这些语句悄悄忽略,最终流片后行为和仿真完全不一致。这种坑,你如果不仔细看综合报告,根本发现不了。
还有一个特别常见的问题是latch。AI在描述组合逻辑时,经常只写if分支不写else,或者case的default分支缺失。在仿真中这倒不会报错,可综合器就会推断出一个锁存器。如果是某个寄存器类型的变量还没赋值,还会有不定态的问题。我的经验是,只要AI生成的是带if-else或case的纯组合逻辑块,三次里至少有一次会在综合阶段蹦出个latch warning。这不是AI笨,而是因为它没有把“电路一定要完整映射,不能留有读不到的分支”当成必须条件。
2.3 时钟、复位、状态机:AI最容易翻车的位置
如果给AI生成RTL的翻车点排个名,时钟、复位、状态机这三个词一定占据前三位。
先说时钟。AI完全不理解时钟域划分。你给它描述一个模块,它往往会假设所有信号都用同一个时钟采样,不会考虑一组信号其实来自两个不同频率的时钟源。跨时钟域的同步它不知道要做,即使你告诉它“这是异步FIFO”,它也只是意识上知道这个词,实际操作时还是会把读写指针直接放进同一个always块,或者把跨时钟域的握手信号不做同步就跑到下一个模块。
复位就更直接了。很多AI模型对复位逻辑的默认写法都是“异步复位,高电平有效”,比如always @(posedge clk or posedge rst)。但在实际项目里,复位极性取决于芯片设计手册和标准单元库,很多库的低电平异步复位根本不能这样写。更麻烦的是异步复位同步释放,这需要先对复位信号做两级同步再送到各个寄存器,AI基本不会想到这一层。我试过一次,AI给出的代码清清楚楚只写了两个端口:clk和rst_n。功能仿真看着对,但做DFT的时候,复位树根本没法正常工作,这就是后话了。
状态机是另一个盲区。AI生成的FSM,状态变量往往是一串二进制编码,但没有完美的默认分支;或者在case中写了default,但default只赋值给状态变量,忘了给一组输出赋值。结果综合出来一堆latch,或者状态机进入未定义状态时直接卡死。如果你有要求它用独热码编码,它就老老实实把每个状态赋一个8'b00000001这样的值,可后续的时序优化它完全无能为力。状态机是一个包含了状态跳转、输出逻辑、复位初值、编码选择等多个维度的设计问题,AI只把最表层的那条线串了起来,一旦需要做出工程取舍,它就不行了。
3. 实操现场:我用AI写了三段RTL,踩了不少坑
3.1 案例一:让AI生成一个异步FIFO,差点被它绕晕
异步FIFO是RTL设计里公认的具有一定难度的模块,因为要处理读写时钟域不一致、指针跨越时钟域同步、空满判断的保守策略等。我想测试一下AI到底能不能碰这个硬骨头。
我的prompt是:“实现一个异步FIFO,深度16,宽度8,写时钟100MHz,读时钟75MHz,提供full和empty信号以及写指针和读指针输出。”
几秒钟的时间,AI给我吐出一份结构还算完整的代码。端口、内存数组、二进制指针、空满标记,该有的都有。我当时还开心了一下,但认真看就发现逻辑上的大问题:它的读写指针都用二进制计数,而且没有做任何跨时钟域的格雷码转换。在异步FIFO里,读写指针要分别同步到对端时钟域,为了减少亚稳态传播风险,通常需要用格雷码。AI把这一步彻底跳过了。
再往下看,它的full信号生成语句几乎是把空判断逻辑复制了一遍,读指针等于写指针就报空,写指针等于读指针就报满,完全没考虑加上“绕回一圈”的判断。结果就是,读写指针都停在同一个位置时,empty和full同时为1。这在功能上完全不能用。
我把问题描述给AI,让它修复,它确实加了一堆格雷码转换和同步器,但加完后我依然发现读指针在跨时钟域后没有做两级同步就进了读判断逻辑。我再反馈一次,它自己可能也被绕晕了,又改成了另一种并不标准的跨时钟方案。最后我直接把异步FIFO的指针同步部分全部手写,AI只保留了我修改好的内存阵列和读写数据路径。整个来回折腾下来,比我直接从零开始写还慢。
这个案例很典型。AI能在外部结构上模仿一个异步FIFO的样子,像端口、寄存器、数组,但在跨时钟域这种深层设计细节上,它的表现就像没有见过这门课的学生。它不是不努力,是真的不知道格雷码和两级同步器是必需的一环。
3.2 案例二:AI处理DFT复位插入时,暴露了逻辑盲区
这个坑是跟DFT相关的。如果你做的是可测试性设计,复位逻辑的写法往往不是最直观的“时钟沿触发复位”那一种。工程上经常要求功能复位和扫描测试复位分开处理,以便在scan_mode下还能正常地控制内部所有寄存器的复位操作。
我让AI写一个带异步复位的模块,它给我的是标准的异步复位代码。仿真没问题,但丢到DFT工具里,复位树检查直接报警。原因是复位逻辑没有考虑scan_mode信号,也没有在复位路径上插入DFT可控条件。工具希望复位信号在测试模式下可以被扫入,而AI的代码把复位做成纯粹的功能引脚,在扫描模式下根本无法置位或清零所有寄存器。
发现这个问题后,我没指望AI能自己改对,而是把DFT要求直接写进注释里再让它重写。它倒是添加了一个scan_mode判断,但判断条件被整合到了内部逻辑而不是复位树上。综合工具最后还是认为复位路径不符合DFT规则。这次我不再和它纠缠了,去EDA工具手册里查了对应规则后,亲手在复位逻辑前面加了一个扫描使能和逻辑门,问题才算解决。
这个案例给我提了个醒:RTL设计不是只有功能仿真这一道关卡,DFT、综合、STA、功耗估算,这些后端流程里的约束,AI完全不具备背景知识。你如果想用AI写RTL,就得提前把这些约束写进prompt里,否则它只会按照“教科书上最标准的样子”生成代码,而这些教科书代码往往缺少实际项目里的工程修饰。
3.3 案例三:状态机代码能跑,但综合后面积翻了一番
第三个案例是我自己手痒,想写一个五级流水线的控制状态机。状态总数大概有12个,我让AI生成FSM。
AI很快给了一套二进制编码的状态寄存器,case语句写得也规整。仿真通过,功能正确,我心里还挺得意。结果跑综合的时候,我发现资源占用比预想高了很多。定位后发现状态机的组合逻辑复杂度爆炸了。因为状态数是12个,二进制编码下,状态转移条件需要比较6位状态寄存器,而且状态转移的组合逻辑会变成一个很宽的case树。在资源紧张的高层设计中,这种宽case树会占用大量查找表。
我手动把状态变量改成独热码,并告诉AI“以后状态机用独热码编码,且每个状态要有明确的default分支”,生成代码后综合资源恢复正常。这一步完全靠经验判断,AI自己在生成的时候根本没有面积意识,它甚至不知道我最终要跑在什么样的目标器件上。
这个案例说明AI对综合后物理层面的影响完全没有感知。它会写出逻辑上正确的代码,但你对面积、延迟、功耗的优化需求,如果你不在prompt里明确写出来,它就一点也帮不上忙。所以想在项目里少受罪,就得提前把这类经验信息补给AI当弹药,而不是指望它自己悟出来。
3.4 案例四:AI也有发光发热的时候
吐槽了这么多,也不是全无收获。我后来试着把AI用在一些更机械化的日常工作里,效果出乎意料地好。
比如维护一个旧模块时,有一段三层嵌套的if-else代码,运行时间长了很难读。我让AI把它翻译成case结构,同时保留原有逻辑,它做得干净利落。还有一次,我需要给一个接口生成接口例化的模板,所有信号名和位宽都已经在接口列表里了,AI直接生成了完整的实例化代码,我只需要替换一下例化的名称,完全没出问题。最省心的一次是生成testbench的初始化任务:复位释放、时钟生成、常见变量初值,AI直接写了十几行,语法规范,还顺手补了注释。
这些任务的共同特点是:接口定义清楚,行为模式固定,与后端物理特性的耦合度低。一旦把这种工作交给AI,我就可以把注意力放在真正有价值的模块架构设计上。所以我对AI的态度是:不要因为它在高难度任务上的翻车就全盘否定。RTL设计里有一大堆“重复劳动”,这些活AI完全接得住,只是你得学会怎么把高难度任务拆成它能力范围内的子任务。
4. 新一代“人机协作”工作流:怎么用AI才不折腾
4.1 把AI当作白板手,而不是最终决策者
被AI折腾了好几天之后,我开始重新思考工作流的定位。在我看来,AI在RTL设计里最适合的角色不是“设计者”,而是“记录者”和“打字员”。
一个更准确的工作方式是:你先在脑子里把模块的接口定义、寄存器功能、状态跳转条件、关键路径约束全部想清楚,然后把这些需求翻译成文字版的“设计文档”,丢给AI让它生成Verilog代码。AI的职责只是把你的自然语言描述转换成代码文本。你不该让它替你做任何设计决策,包括要不要加两级同步器、复位信号怎么接、状态编码用什么格式,这些都是硬件工程师的活。
这个思路有点像口头写作文和语音输入的关系。AI就是你的语音输入法,它能帮你把脑子里的句子变成文字,但它不会替你构思文章论点。如果你连论点都没想好就开始命令AI“写篇文章”,大概率会得到一篇看似通顺但不知所云的废话。
所以当你决定用AI写RTL模块时,先花半小时把模块规格书写清楚,反而比直接把任务扔给AI要省时间。规格书里写清端口列表、时钟频率、复位极性、模块行为、特殊约束(比如需要跨时钟域同步、需要复位树可控),AI生成代码的质量会明显上一个大台阶。
4.2 写好RTL提示词的四步法
如果你希望AI能一次生成接近可用的代码,提示词的质量几乎决定了结果的上限。我自己总结出一个四步提示词模板,基本可以适配绝大多数模块生成场景。
第一步,说明模块背景。比如“这是高速接口模块中的数据缓冲部分”“这是CPU总线桥的写数据通路”。你不用写得特别专业,只要告诉AI模块在整个系统中的定位,它就能大致判断功能范围。
第二步,给全接口信息。这一步最关键,你要逐个列出模块的输入输出信号、方向、位宽、功能。例如:
模块名称:data_buf 端口: clk:输入,1位,主时钟; rst_n:输入,1位,低电平异步复位; wr_en:输入,1位,写使能; rd_en:输入,1位,读使能; data_in:输入,8位; data_out:输出,8位; full:输出,1位,高有效; empty:输出,1位,高有效。第三步,描述具体行为。把你希望的时序行为用文字写清楚,越精确越好。比如“当wr_en为高且full不为高时,在时钟上升沿采样data_in并写入存储;full信号在存储中有效数据数量等于16时拉高;读使能时在rd_en为高且empty为低的下一个时钟上升沿输出当前读指针指向的数据”。这些细节你写得越细,AI生成的逻辑才越接近你真实需要的电路。
第四步,指定代码风格和约束。例如“使用可综合的Verilog代码,不要在模块内部使用initial”“状态机使用独热码编码”“所有时序逻辑采用非阻塞赋值”“对复位信号做两级同步后再使用”。这些约束虽然AI不一定完全理解,但它至少会按照你的格式去写,能帮你少踩不少典型坑。
这四步写下来可能要花十几分钟,但换来的是AI生成的代码基本能进仿真。相比你拿着错误提示疯狂对话一两个小时,效率高得多。
4.3 验证闭环:从仿真到综合的快速反馈
AI生成代码后,还有一个重要环节不能省:快速验证。不要看到代码写得整齐就直接提交到版本库,也不要让AI跟你无休止地对话修改。正确做法是建立一个自己熟悉的验证闭环。
我通常会让AI生成代码后,马上跑一个最小化的定向仿真,覆盖关键握手路径、首尾数据、边界条件。比如FIFO就测写满、读空、绕回一圈后的满空状态;状态机就测从复位释放到第一个状态、状态跳转的每条分支、非法状态恢复。这一步能在半小时内筛掉绝大多数AI的隐藏逻辑错误。
仿真过了,再跑一次lint检查,专门排查latch推断、多驱动、阻塞赋值与非阻塞赋值混用这类可综合性问题。lint没过就直接把报错信息丢给AI修改,此时它也能相对准确地定位问题。
最后,对关键模块做一次综合review,关注面积和时序是否在预期范围内。如果综合报告出现异常大的LUT用量或者关键路径过深,再回溯到RTL做优化。这套闭环跑完,AI生成代码的风险基本能被控制在可控范围。记住,AI最大的价值是提高你写代码的速度,但验证和评审的环节永远不能交给AI一手包办。
5. 常见问题速查表:AI写RTL的避坑清单
5.1 典型问题与排查思路
这一周我几乎把自己踩过的坑都记了下来,再加上和几个也在尝试AI写RTL的同行交流,整理出一张常见问题速查表,基本可以覆盖大部分AI生成RTL后的“翻车场景”。
| 常见症状 | 可能原因 | 排查方向 |
|---|---|---|
| 仿真时数据频繁错拍 | 指针/计数器逻辑与实际握手时序不一致 | 检查同步器、格雷码、FIFO指针产生逻辑 |
| 综合后出现大量latch | 组合逻辑分支缺少default赋值 | 检查case分支、if-else完整性 |
| 复位行为不符合要求 | 复位极性搞反或没有做同步释放 | 确认工艺库复位类型,补两级同步器 |
| 面积/功耗指标超限 | 状态编码不合理、存在冗余逻辑 | 调整状态编码,审查代码中的重复结构 |
| 时序不收敛 | 组合逻辑过长、缺少流水线寄存器 | 在关键路径插入寄存器,重新约束时序路径 |
| DFT测试时复位不受控 | 复位路径未处理scan_mode控制 | 人工插入可测性复位逻辑,按DFT规则修改RTL |
| 仿真通过但综合报错 | 代码中混入了仿真语句 | 排查initial、fork/join、延时控制等语句 |
| AI反复修改还是出错 | 提示词信息不足,AI无法理解完整需求 | 补全接口、行为、约束描述,重新生成 |
这张表的价值在于,你可以根据症状快速判断是自己该动手检查代码,还是继续把问题抛给AI修改。如果问题涉及跨时钟域、复位树、可综合性和后端优化,请直接放弃AI,亲自动手;如果是简单的逻辑分支扩展或代码风格问题,让AI改往往效果不错。
5.2 我给新手的建议
如果你是第一次尝试用AI写RTL,我的第一建议是:不要一口吃成一个胖子。先从一个小模块开始,比如一个简单的8位计数器,或者一个带有效信号的数据寄存器,跑通完整的“生成-仿真-综合”闭环。等你对上AI哪些部分容易出错建立直觉后,再让它生成相对复杂的模块。
第二,永远不要跳过代码评审。不管AI生成的代码看起来多完美,都要拉上另一个懂硬件的人一起过一遍。很多AI代码是“逻辑正确但工程不适用”,比如该打拍的地方不打拍,该同步的地方不同步,这种只有有经验的工程师才能一眼发现。
第三,学习最基本的综合和时序知识。你不需要成为后端专家,但至少要能看懂综合报告里的LUT/FF占用、关键路径延迟、时序违规提示。否则你就只能被动接受AI给出的“我改好了”的结果,完全无法判断改得对错。
第四,持续积累自己的提示词模板。每个项目、每个团队对代码风格和约束要求都不一样,AI生成的代码不会天生适配你的标准。你把常见约束写进一个自己的提示词库里,以后遇到同类任务直接复用,效率会越来越高。
说实话,这一周被AI折腾得不轻,但冷静下来想想,AI在RTL设计里的价值不是“取代你”,而是“放大你”。它把写代码的时间压缩了,把重复劳动吸收了,同时把真正需要设计判断的部分留给了你。你只需要盯紧那些不可妥协的硬件约束,就能从一堆繁琐的机械工作中解脱出来。
最后再分享一个小经验:我现在让AI写RTL,必在提示词里写一句“所有时序逻辑必须在时钟沿下工作,不要在同一模块中使用initial语句”。这句话帮我躲过了至少两次综合事故。你也可以根据自己的历史翻车点,总结出自己的“提示词护身符”。用AI这件事,本质上和打仗一样,工具越顺手,纪律越严格,你才不会在第一波冲锋时就崩溃。