
今年我在内部工具链上反复试一件事让大模型真正参与到 RTL 设计里而不是只用来写注释。挑来挑去最后选了一个规模不大但功能覆盖很全的对象来练手用 AI 从头开始设计一款 GPIO IP。先说清楚这里的 IP 是半导体行业说的 IP core知识产权核不是网络里那个 IP 地址。GPIO 是几乎所有 MCU 和 SoC 都会有的基础外设8 种工作模式把输入、输出、上拉、下拉、模拟、复用、中断这几类 IO 行为全占了。整个流程走下来我的最大感受是AI 在这件事里的角色不是替你写代码而是帮你把需求翻译成设计语言前提是你自己得清楚每一步的边界在哪。文章把整个设计过程拆开讲包括寄存器规划、RTL 生成、验证环境搭建和上板测试适合 FPGA 工程师、IC 前端设计和验证工程师以及所有想评估“AI 辅助硬件开发工具链”真实上限的人。1. 为什么拿 GPIO 当试金石项目定位与设计目标1.1 从一句模糊需求到一张规格表最开始的需求其实很含糊就一句话想做一个通用 GPIO 控制器支持 8 种工作模式能挂到总线上去读按键、控制 LED最好带中断。这种话扔给任何一个工程师大家脑中的细节都不一样。芯片架构师会问总线是什么验证工程师会问复位值前端设计的人会问 IO pad 的方向控制怎么给。恰好是这种模糊需求最适合拿来试 AI让大模型把一句话结构性展开再拿人去过滤。我直接把原始需求发过去要求它回答三件事寄存器列表、总线接口选择、中断方案。AI 给出的建议是挂 APBAdvanced Peripheral Bus低速外设总线寄存器采用 STM32 风格MODER、OTYPER、PUPDR、IDR、ODR、BSRR、AFR中断再额外配一组使能、状态和清除寄存器。这个方向与主流 MCU 的 GPIO 设计思路一致我采纳了一部分但有几处还是要自己定。第一BSRR 这种位操作寄存器对 RTL 做位寻址比较绕但我们还是保留因为对软件非常友好。第二中断触发方式我要求覆盖上升沿、下降沿、高电平、低电平四种而不是只做边沿。第三IO 方向控制统一用 pad_oe 高电平有效的约定避免和 pad 库默认低电平使能搞混。借助 AI 做早期规格梳理真正节省的时间在于它会把地址位宽、复位值、写保护这些零零散散的点全部列出来你只需要按项目实际情况逐条打勾或砍掉。1.2 为什么选 GPIO 而不是 UART/SPI/TimerUART 那种带波特率生成、FIFO、协议状态机的模块第一次就交给 AI 来做排查成本相当高。SPI 同样有主从模式、时钟极性、相位这些纠缠的细节。我不建议第一次用 AI 辅助设计 IP 就拿它们练手因为一旦仿真不过你会分不清是验证环境的问题、协议理解的问题还是代码生成的问题整个过程非常劝退。GPIO 的优势在于功能引脚多但行为分类很干净输入、输出、开漏、上拉、下拉、模拟、复用、中断无非是几种标准行为的组合。RTL 规模估下来也就几百到一千行模块边界极其清晰总线侧面向 APBIO 侧面向 pad中间就是寄存器堆和模式选择逻辑。跑通这样一个“麻雀虽小、五脏俱全”的模块AI 在规格展开、RTL 生成、测试用例编写各环节表现如何基本就能有一个客观判断。GPIO 如果试不出来其他 IP 更不用谈。2. GPIO 的 8 种工作模式是怎么落到寄存器和 IO PAD 上的2.1 8 种模式与寄存器映射关系STM32 那套 8 种工作模式本质是把“方向、输出结构、上下拉”几个维度压缩成了枚举。我在自己的 IP 里也沿用这套框架因为软件工程师最熟悉用库函数时配置过这些寄存器的人非常多文档解释成本低。寄存器规划如下表所示。模式MODEROTYPERPUPDR实际行为典型用途输入浮空00不关心00高阻输入电平全由外部决定按键输入输入上拉00不关心01内部上拉电阻接入外部无驱动时读 1读取开漏输出、机械开关输入下拉00不关心10内部下拉电阻接入外部无驱动时读 0防止输入悬空导致乱跳模拟输入11不关心00内部数字通路关闭信号直通模拟前端ADC 采集通道推挽输出010不关心强驱动能拉高也能拉低LED、普通数字信号开漏输出011不关心只能拉低高电平靠上拉I2C 等线与逻辑复用推挽100不关心由内部外设驱动强输出UART TX、PWM复用开漏101不关心外设控制低电平配合上拉使用I2C SDA/SCLMODER 每个引脚占 2 bit00 输入、01 输出、10 复用、11 模拟。OTYPER 每引脚 1 bit0 推挽、1 开漏。PUPDR 每引脚 2 bit00 无、01 上拉、10 下拉、11 保留。值得注意的是现实芯片里上拉和下拉绝不允许同时打开。RTL 里做互斥译码可以保证即使软件非法写入保留值 11硬件也不会把 pad_pu 和 pad_pd 同时拉高。有了这张表所有引脚行为就能抽象成几个 pad 控制信号pad_oe 控制输出使能pad_out 控制输出电平pad_pu 控制上拉pad_pd 控制下拉。pad 库只认这几个信号GPIO 内部做模式解码即可。2.2 从 ODR 到 PAD 再到 IDR数据通路拆解下面这段简化 Verilog 是我最终实现里截出来的核心片段负责模式解码到 pad 控制的转换。// mode 取自 MODER 寄存器每 pin 2bit // ot 取自 OTYPER 寄存器每 pin 1bit // odr 取自 ODR 输出数据寄存器 wire [1:0] mode moder[pin*2 : 2]; wire ot otyper[pin]; wire dout_sel; assign is_out (mode 2b01); // 普通输出 assign is_af (mode 2b10); // 复用功能 assign dout_sel is_af ? af_out[pin] : odr[pin]; // 输出数据源 assign pad_oe (is_out || is_af) (!ot || !dout_sel); assign pad_out dout_sel; assign pad_pu (mode 2b00) (pupdr[pin*2 : 2] 2b01); assign pad_pd (mode 2b00) (pupdr[pin*2 : 2] 2b10);一开始我把 pad_oe 简单写成(is_out || is_af)结果开漏模式完全不能工作。手动推一遍开漏输出写 1 时输出高电平此时 pad 管脚应该释放由外部上拉拉高写 0 时输出低电平pad 管脚要把它往下拉。所以开漏输出高电平时 pad_oe0、pad_out1开漏输出低电平时 pad_oe1、pad_out0。上面那个!ot || !dout_sel就是在表达这件事。输入路径同样有讲究。外部信号进入寄存器回读和中断检测之前要先打两级同步把跨时钟域带来的亚稳态风险降下来。always (posedge clk or negedge rst_n) begin if (!rst_n) begin in_sync1 {WIDTH{1b0}}; in_sync2 {WIDTH{1b0}}; end else begin in_sync1 pad_in; in_sync2 in_sync1; end end wire [WIDTH-1:0] idr_val in_sync2;如果只打一级综合后报告里全是跨时钟域警告仿真时偶尔出现 IDR 读到中间值的诡异现象。两级是最基本操作。如果你对可靠性要求更高可以用三级同步外加握手但 GPIO 场景通常两级足够。3. 让 AI 写 RTL 的正确姿势我用的 Prompt 模板3.1 Prompt 结构接口、寄存器、时序、风格限定我试过几种 Prompt 风格最失败的就是“帮我写一个 GPIO 模块”。大模型确实会给你一份看起来像模像样的 Verilog但里面没有接口定义、没有复位值、没有地址译码完全不可用。后来我把提示词固定成四个块模块接口、寄存器规划、时序要求、代码风格。模板如下。请设计一个 APB 接口的 GPIO 控制器参数化位宽 GPIO_WIDTH默认 8。 模块名 gpio_top。 接口信号 - input wire pclk, presetn - input wire psel, penable, pwrite - input wire [11:0] paddr - input wire [31:0] pwdata - output reg [31:0] prdata - input wire [GPIO_WIDTH-1:0] pad_in - output reg [GPIO_WIDTH-1:0] pad_out - output reg [GPIO_WIDTH-1:0] pad_oe - output reg [GPIO_WIDTH-1:0] pad_pu, pad_pd - input wire [GPIO_WIDTH-1:0] af_in - output reg [GPIO_WIDTH-1:0] af_out 寄存器规划 MODER、OTYPER、PUPDR、IDR、ODR、BSRR、AFR、IER、ISR、INT_TYPE。 INT_TYPE 每引脚 2bit00 上升沿触发、01 下降沿触发、10 高电平触发、11 低电平触发。 设计要求 1. 读写时序遵循 APB4 协议paddr 为字节地址最低两位恒为 0寄存器译码从 addr[2] 开始。 2. BSRR 低 16 位写 1 置位 ODR高 16 位写 1 清零 ODR。 3. IO 输入信号先经过两级同步再用于 IDR 回读和中断检测。 4. 中断支持上升沿、下降沿、高电平、低电平四种触发方式按引脚独立配置。 5. ISR 是中断状态寄存器写 1 清除对应事件位。 6. 复位时所有寄存器清零方向默认输入IO 输出为高阻。 7. 使用 Verilog-2001 语法不使用 SystemVerilog不生成 X 态赋值。 8. 请直接输出完整可综合代码。这个 Prompt 写清楚了两件关键事寄存器和握手时序。这样生成出来的代码已经具备可仿真基础剩下的问题集中在逻辑细节比如开漏使能的配合。我后续迭代时会再贴上 lint 报错或仿真失败 log让大模型直接给我修正建议效果比重新生成一份代码稳定得多。3.2 第一次生成我把哪些地方判了“不安全”AI 第一次丢出来的代码风格上是通顺的但细看能挑出一堆问题。我印象比较深的几个第一输入同步器写得对却没有把同步后的信号用对地方。中断检测用的竟是原始 pad_in等于把异步信号直接送进边沿检测触发器复位后第一次中断几乎必定出现亚稳态。第二写寄存器时漏了psel penable的访问阶段判断直接把pwrite paddr当成写使能仿真里还好综合之后配合真实总线的读写相位会出现数据写在半拍上的问题。第三开漏输出使能逻辑错了上文已经提过。第四BSRR 高 16 位清零和低 16 位置位用了两条 if 而不是在同一个 cycle 操作软件如果同时写置 1 和清零行为就会变成先清后置或者先置后清和文档描述不符。这些问题如果没有 RTL 基础直接拿去综合八成会在板级验证阶段花掉几乎所有时间。我心里默认一条规则AI 生成的代码必须经过逐信号核对尤其是 pad_oe、prdata、中断清除这类行为。3.3 人到最后的把关RTL review 清单我不会去逐行念代码而是做三张表格。第一张是接口方向表把模块所有端口、位宽、方向、复位后的默认值列出来和 AI 的代码逐项比对。第二张是寄存器行为表每个寄存器写什么、读什么、哪些位只读、哪些写 1 清除对照规格文档检查。第三张是 pad 控制真值表把 8 种模式对应的 pad_oe、pad_out、pad_pu、pad_pd 全部枚举出来再对着代码画组合逻辑。三张表如果都能对上这版 RTL 我才敢说可以进验证。4. 验证场景拆解功能测试点和覆盖率的组织4.1 Testbench 环境搭建验证环节我的习惯是同时上两套工具链一套商业工具跑回归另一套开源仿真器快速调波形。RTL 短期迭代时先用开源仿真器跑最小测试集保留波形每次改完代码立刻跑能省下大量排队时间。Testbench 的核心结构其实不复杂一个 APB master 负责读写序列一个 DUT 是 gpio_top再写一个简单的 pad 行为模型模拟外部上拉、下拉、按键拉低。测试用例不是简单的一两个读写要覆盖下表的场景。测试场景验证点判定条件复位检查所有寄存器清零、IO 高阻pad_oe0IDR0推挽输出写 0/写 1ODR 影响 pad_out 与 pad_oepad_outODRpad_oe1开漏输出写 0/写 1开漏释放行为写 1 时 pad_oe0写 0 时 pad_oe1输入浮空外部拉高/拉低IDR 回读IDR 与 pad_in 一致输入上拉外部悬空时 IDR 读 1pad_pu1pad_pd0输入下拉外部悬空时 IDR 读 0pad_pd1pad_pu0模拟输入数字通路关闭IDR 恒 0pad_oe0复用推挽/复用开漏外设信号输出到 padaf_out 生效ODR 不影响 pad_out边沿中断上升沿、下降沿分别触发对应 ISR 位置位并输出 irqW1C 清除写 1 清除对应位其他位不被误清随机配置回归模式任意组合切换无 X 态传播读写一致一开始测试用例只覆盖前 6 行结果是开漏、中断这类边缘行为果然遗漏。后来把表格里的 11 行全部变成脚本化的 tracer才敢让 AI 去生成回归的随机激励。4.2 边沿检测、去抖和中断的验证细节边沿检测最容易踩的坑是同步打拍方向搞反。用in_sync1 pad_in; in_sync2 in_sync1;之后上升沿是in_sync2 !in_sync1下降沿是!in_sync2 in_sync1。如果哪一个拍子取反写反仿真里手动模拟一个 10ns 的上升沿打印出来会非常清晰这类短时错误一眼就能定位。机械按键场景里有一个真实存在的痛点按键按下和释放的瞬间pad 会有一段 5ms 到 20ms 的不稳定抖动。如果 GPIO 中断只做边沿检测一次按键可能触发七八次中断。我在设计中加了一个可选的去抖窗口计数器检测到边沿后计数器开始计时窗口内信号保持不变才真正提交中断。这个细节在芯片级 GPIO 里非常常见。验证去抖逻辑的关键是构造一个脉宽刚好跨越窗口边界的激励确认计数器和重新触发行为都符合预期。这个用例完全可以让 AI 写只要告诉它“去抖窗口 10us输入信号在窗口内有毛刺期望只有一个中断事件”。4.3 用 AI 生成断言与随机约束的经验SystemVerilog Assertions 对 AI 来说并不难写难的是判断断言本身的正确性。我让 AI 写了一条中断断言它写出来是 IRQ 高电平和 pad_in 电平条件同时成立。这条断言在电平触发场景下没问题但在边沿触发场景下就会误报。正确的做法是把断言精细化例如上升沿触发时IRQ 应在事件发生后的固定时钟周期内置位且保持到软件清除。property p_rise_irq; (posedge clk) disable iff (!rst_n) $rose(in_sync2[0]) |- ##[1:3] irq 1b1; endproperty随机约束的关键是只生成合法模式组合。MODER、OTYPER、PUPDR 之间是有依赖关系的比如模拟模式下 PUPDR 没有意义开漏模式下推挽和开漏只能选一个。用 SystemVerilog 的 constraint 把这些依赖写清楚但 AI 生成的约束大概率会漏掉上拉和下拉不能同时为 1 这条而且它不会按“保留位”约束为 0。这些我在 debug 时都吃过亏所以现在生成完 constraint 不看一眼是不敢跑的。5. 集成与板级验证从仿真到真正亮灯5.1 FPGA 上跑 IP 的注意事项RTL 在仿真里全绿不意味着板子上一定正常。第一个坑就是 pad 库差异仿真的 pad 模型允许你直接从 RTL 控制上下拉FPGA 上的上下拉通常要靠综合工具的 IO 约束实现。我最初在 RTL 里精心设计了 pad_pu/pad_pd上板后发现这些信号并没有像预期那样改变 FPGA 管脚状态原因是 FPGA 原语的配置优先级更高RTL 里输出给 pad 的 PU/PD 信号根本没接到管脚上。所以做原型验证时要么接受 FPGA 约束里的 PULLUP/PULLDOWN要么只能在仿真环境里看内部上下拉行为。第二个坑是开漏模式没有外部上拉电阻时LED 只会默默熄灭不会因为你写了 ODR1 就自动亮。开漏就是只负责拉低高电平稳态必须靠外部电路。调试时把逻辑分析仪接上去观测 pad 波形如果看到只有低电平跳变多半就是上拉缺失。5.2 挂到一个小 MCU 上跑 demo我最后把这个 IP 集成进了一个基于小型 RISC-V 核的测试 SoC在板子上做了三件事点亮 LED、读回按键状态、按键触发中断。第一版 demo 起不来打印寄存器发现 ODR 写进去再读出来是 0。排查链路是先用软件读 MODER 确认总线通了再读 ODR 确认是写不进还是写进去了但被其他逻辑清了。最后定位到 APB 地址对齐问题——仿真里用的 paddr 全是 4 字节对齐的但 SoC 互联的地址线接法没有按 2bit 偏移对齐MODER 寄存器实际落在错误地址上所以看起来像“写不进”。真正上板跑通之后那种满足感比仿真全绿强得多。事实证明只要接口定义清楚、RTL review 做到位IP 从仿真到板级基本不会有结构性问题剩下的都是地址映射和 pad 配置这类环境细节。6. 避坑手册AI 辅助硬件设计最容易踩的典型坑6.1 典型问题速查表现象可能原因排查方向所有输出引脚都是高阻pad_oe 恒为 0复位默认方向为输入开漏模式使能逻辑错误单步检查方向寄存器和 pad_oe 真值表IDR 一直读到 0 或 1输入模式没配置对模拟模式未关闭上下拉配置与外部驱动冲突检查 MODER/PUPDR外部用信号源强驱动IRQ 持续为高触发事件一直存在ISR 没有正确 W1C 清除写 1 清 ISR观察触发条件和清除语义APB 写入无效paddr 未对齐写使能少判了访问阶段地址译码没覆盖该寄存器用仿真波形看 psel/penable/pwrite 相位仿真随机回归出现不定态未初始化的寄存器被读保留位被非法写入检查默认值、约束合法性、X 态来源跟踪AI 代码综合面积异常大没有复用同步器状态机冗余case 分支覆盖过多用综合报告对比预估检查是否存在大量重复逻辑6.2 AI 的边界能帮你提高效率不能替你审代码跑完这个项目最大的体会是AI 真正提升效率的地方在规格展开、模板生成、日志分析和测试用例起草这些都是“重复性较高、逻辑确定性较强”的部分。而一旦出现跨时钟域、位宽对齐、总线握手时序这类需要上下文全局判断的问题AI 的答案就会变得不稳定这时候你需要的是一份经过人脑验证的 review 清单而不是继续和 AI 反复拉扯。另外说一个容易被忽略的点AI 生成代码的出处和授权问题。如果项目将来要商用RTL 是否来自开源仓库的训练数据你的 license 检查工具不一定能识别出来。稳妥的做法是确认大模型输出条款对生成内容的归属约定在正式发布 IP 前做一次代码合规检查。GPIO 这种通用模块的高层风格通常没问题但这不是可以赌的点。我倾向把 AI 看作一个“带资深意见的实习生”它能给你参考设计、能写测试框架、能帮你把问题描述翻译成搜索关键词但设计负责人必须是你自己。它会犯错而且错得非常像真话。这个项目跑完之后我手里这套 GPIO RTL 并不是最漂亮的作品但它是我第一次真正理解从 MODER 到 pad_oe 每一条信号意义的项目。以前用库函数配置 GPIO只是调用现在再做同样的配置脑子里会自动浮现出开漏输出那行 pad_oe 的表达式。AI 让我少写了很多无关代码但也让我多了很多要看的代码。下一步我打算用同一套 Prompt 结构去尝试 SPI Master IP到时候再回来分享对比结果。