ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

试用专门写 Verilog 的 Codex Skill:比“让 AI 直接吐 RTL”靠谱不少

2026/8/21 8:26:59 拓冰建站 浏览量
试用专门写 Verilog 的 Codex Skill:比“让 AI 直接吐 RTL”靠谱不少 试用了一个专门写 Verilog 的 Codex Skill比“让 AI 直接吐 RTL”靠谱不少试用了一个专门写 Verilog 的 Codex Skill比“让 AI 直接吐 RTL”靠谱不少一、本次试用说明二、我用它做了什么三、试下来印象比较深的三点1. 可读性确实比“裸生成 Verilog”好2. 最新版不再把“生成代码”当成第一步因为 FPGA 设计里真正容易出错的往往不是 Verilog 语法而是双方对复位、延迟、握手、完成脉冲和异常场景的理解不一致。3. 它对“验证证据”的边界比较克制四、也有几点需要理性看待五、总体感受六、项目地址与调用方式试用了一个专门写 Verilog 的 Codex Skill比“让 AI 直接吐 RTL”靠谱不少最近试用了一个面向Verilog-2001 RTL 开发的 Codex SkillVerilog Generator。这次我重点阅读了当前最新版v1.3.1的工作流同时拿之前使用v1.0.0生成的一份实际 RTL 结果做了对照。它给我的第一印象是这并不是简单给模型加一句“你是 FPGA 专家”而是在尝试把 AI 生成 Verilog 变成一套更接近工程习惯的流程——先把接口、时钟与复位、逐周期行为、状态转换和边界情况讲清楚再进入代码生成与验证。一句话结论它把“让 AI 直接吐一段 RTL”推进到了“先确认设计行为再生成、审查并说明验证边界”的阶段。GitHub 项目地址https://github.com/Eriemon/verilog-generator一、本次试用说明项目内容当前最新版v1.3.1实验结果来源使用 v1.0.0 生成的 RTL测试模块64×64 Conway Game of Life 二维元胞自动机重点观察接口、状态机、双缓冲、边界处理、代码组织和中文注释本次未包含testbench、仿真波形、综合报告、时序报告和上板结果二、我用它做了什么这次选择的测试任务是生成一个64×64 Conway Game of Life生命游戏RTL 核心。需求主要包括在空闲状态下写入初始种子支持连续运行和单步演化输出忙状态、单代完成脉冲和代数计数支持按行读取当前一代的 64 bit 细胞状态按照 Conway 生命游戏的 B3/S23 规则生成下一代状态。从 v1.0.0 生成的代码来看它没有只给出一个接口空壳而是把主要的数据通路和控制流程都展开了。模块内部使用三个状态组织一代生命演化localparam [1:0] ST_IDLE 2d0; localparam [1:0] ST_SCAN 2d1; localparam [1:0] ST_COMMIT 2d2;三个状态分别负责ST_IDLE等待单步或连续运行请求同时允许写入初始种子ST_SCAN逐点扫描 64×64 网格并计算下一代细胞状态ST_COMMIT完成整代计算后切换双缓冲并决定继续运行还是返回空闲态。此外生成结果还包含A/B 两份 4096 bit 帧数据用于当前代与下一代之间的切换八邻域的独立边界判断与活细胞计数B3/S23 规则判断连续运行、单步运行和完成脉冲控制种子逐点写入与当前代按行读回对接口、状态、计数器、标志信号、索引和帧缓存的分区组织比较完整的中文语义注释。【生成结果中的模块端口】// 64x64 二维生命游戏自演化核心模块 module cellular_automaton_2d ( //-----------------全局信号-----------------// input i_clk, // 模块主时钟,用于推进扫描状态机和代际切换 input i_rstn, // 模块低有效复位,拉低时清空双缓冲和代数计数 //-----------------控制接口-----------------// input i_run_en, // 连续运行使能,为高时每代提交后立刻开始下一代扫描 input i_step, // 单步推进请求,仅在空闲态被采纳为一代启动脉冲 output o_busy, // 正在执行扫描或提交流程时对外拉高的忙标志 output o_generation_done, // 每代提交完成后维持一个时钟周期的完成脉冲 output [31:0]o_generation_count, // 已经提交完成的生命代数统计值 //---------------种子写入接口---------------// //SEED接口 input i_seed_we, // 空闲态逐点写入种子的写使能脉冲 input [5:0]i_seed_row, // 种子写请求命中的目标行编号 input [5:0]i_seed_col, // 种子写请求命中的目标列编号 input i_seed_value, // 种子写请求希望写入的细胞状态值 //---------------状态读取接口---------------// input [5:0]i_read_row, // 当前代按行读回所选择的行编号 output [63:0]o_read_row_data // 当前代指定行的完整 64bit 细胞状态向量 );【状态机】例如ST_IDLE / ST_SCAN / ST_COMMIT状态机以及 A/B 双缓冲切换相关代码//---------------配置参数区域---------------// // 网格尺寸与零值常量组 localparam [5:0]GRID_LAST_INDEX 6d63; // 64x64 网格中最后一行或最后一列对应的索引值 localparam [63:0]ROW_DATA_ZERO 64d0; // 越界行或复位清零时复用的全零行向量 localparam [4095:0]FRAME_DATA_ZERO 4096d0; // 双缓冲整帧复位时复用的全零 4096bit 初始图样 //---------------状态参数区域---------------// // 顶层控制状态编码组 localparam [1:0]ST_IDLE 2d0; // 停机并允许外部装载种子的空闲状态 localparam [1:0]ST_SCAN 2d1; // 逐点扫描当前代并写出下一代结果的运行状态 localparam [1:0]ST_COMMIT 2d2; // 整代扫描结束后切换前后台缓冲的提交状态 //-----------------计数信号-----------------// wire [3:0]cnt_neighbor_sum; // 当前被扫描细胞 8 邻域中活细胞数量的求和结果 //----------------状态机信号----------------// reg [1:0]state_current 0; // 当前正在执行的生命演化控制状态 reg [1:0]state_next 0; // 组合逻辑计算得到的下一拍控制状态 reg [5:0]state_row_index 0; // 扫描流程当前命中的细胞行号 reg [5:0]state_col_index 0; // 扫描流程当前命中的细胞列号 //----------------寄存器信号----------------// reg [4095:0]reg_frame_a 0; // 扁平化帧缓冲 A,按 {行号,列号} 映射 4096 个生命位 reg [4095:0]reg_frame_b 0; // 扁平化帧缓冲 B,作为与 A 轮换的整帧存储向量 //----------------状态机区域----------------// // 根据当前状态与控制输入决定下一拍状态流向 always(*)begin state_next state_current; // 缺省保持当前状态,避免组合分支遗漏导致锁存推断 case(state_current) // 状态分支说明: 空闲态负责吸收启动控制并保持种子可写 // 空闲态等待单步或连续运行请求启动新一代扫描 ST_IDLE:begin if(flag_start_scan 1b1)begin state_next ST_SCAN; // 收到连续运行或单步请求后切换到扫描状态 end else begin state_next ST_IDLE; // 没有启动请求时继续停留在空闲状态 end end // 状态分支说明: 扫描态遍历全网格并生成后继代位图 // 扫描态逐点遍历 64x64 网格并在末单元后进入提交阶段 ST_SCAN:begin if(flag_scan_last_cell 1b1)begin state_next ST_COMMIT; // 整个 64x64 网格最后一个细胞计算完成后进入提交状态 end else begin state_next ST_SCAN; // 尚未扫完整个网格时继续保持扫描状态 end end // 状态分支说明: 提交态负责换帧并判定后续是否继续运行 // 提交态翻转双缓冲并根据运行使能决定是否继续下一代 ST_COMMIT:begin if(i_run_en 1b1)begin state_next ST_SCAN; // 连续运行打开时提交完成后直接开始下一代扫描 end else begin state_next ST_IDLE; // 连续运行关闭时提交完成后回到空闲装载状态 end end // 状态分支说明: 默认分支吸收非法状态编码并回到安全起点 // 兜底分支把异常状态编码拉回安全空闲态 default:begin state_next ST_IDLE; // 检测到未定义状态编码时回退到安全空闲状态 end endcase end // 把组合求得的下一状态注册为当前状态 always(posedge i_clk or negedge i_rstn)begin if(i_rstn 1b0)begin state_current ST_IDLE; // 复位时强制状态机回到空闲态等待重新装载 end else begin state_current state_next; // 每个时钟把下一状态锁存成新的当前状态 end end【八邻域求和代码】八邻域求和与 B3/S23 规则代码。//-------------状态任务处理区域-------------// // 当前帧选择位在提交态完成前后台缓冲互换 always(posedge i_clk or negedge i_rstn)begin if(i_rstn 1b0)begin flag_frame_select 1b0; // 复位后固定由 A 帧作为当前代读出来源 end else if(state_current ST_COMMIT)begin flag_frame_select ~flag_frame_select; // 每次提交新一代时翻转当前帧选择位完成双缓冲切换 end end // 扫描行号在启动、行末和整代切换时进行更新 always(posedge i_clk or negedge i_rstn)begin if(i_rstn 1b0)begin state_row_index 6d0; // 复位时从网格首行重新开始扫描坐标 end else if((state_current ST_IDLE) (state_next ST_SCAN))begin state_row_index 6d0; // 从空闲态启动新一代时把扫描起点重新放回首行 end else if(state_current ST_SCAN)begin if(flag_scan_last_cell 1b1)begin state_row_index 6d0; // 扫描完最后一个细胞后把下一代行起点预置回首行 end else if(flag_scan_last_col 1b1)begin state_row_index state_row_index 6d1; // 当前行末列写完后把扫描行号推进到下一行 end end end // 扫描列号在启动、列推进与行切换时进行更新 always(posedge i_clk or negedge i_rstn)begin if(i_rstn 1b0)begin state_col_index 6d0; // 复位时从网格首列重新开始扫描坐标 end else if((state_current ST_IDLE) (state_next ST_SCAN))begin state_col_index 6d0; // 从空闲态启动新一代时把扫描起点重新对准首列 end else if(state_current ST_SCAN)begin if(flag_scan_last_col 1b1)begin state_col_index 6d0; // 当前行最后一列写完后把列坐标回卷到首列 end else begin state_col_index state_col_index 6d1; // 同一行内部扫描时把列坐标推进到下一个细胞 end end end // A 帧在复位、空闲装载和 B 当前帧扫描时承接下一代写入 always(posedge i_clk or negedge i_rstn)begin if(i_rstn 1b0)begin reg_frame_a FRAME_DATA_ZERO; // 复位时把 A 扁平帧一次性清成 4096bit 全零图样 end else if((state_current ST_IDLE) (i_seed_we 1b1))begin reg_frame_a[index_seed_bit] i_seed_value; // 空闲装载阶段把外部种子值写入 A 帧目标单元位 end else if((state_current ST_SCAN) (flag_frame_select 1b1))begin reg_frame_a[index_scan_bit] flag_cell_next; // 当 B 帧作为当前代时把计算出的下一代位写回 A 帧 end end // B 帧在复位、空闲镜像装载和 A 当前帧扫描时收集候选后继代 always(posedge i_clk or negedge i_rstn)begin if(i_rstn 1b0)begin reg_frame_b FRAME_DATA_ZERO; // 复位时把 B 扁平帧初始化成备用后继代的全零图样 end else if((state_current ST_IDLE) (i_seed_we 1b1))begin reg_frame_b[index_seed_bit] i_seed_value; // 空闲装载阶段把同一颗种子同步镜像写入 B 帧对应单元位 end else if((state_current ST_SCAN) (flag_frame_select 1b0))begin reg_frame_b[index_scan_bit] flag_cell_next; // 当 A 帧作为源帧时把新计算的后继位落入 B 帧待提交结果 end end三、试下来印象比较深的三点1. 可读性确实比“裸生成 Verilog”好最明显的是代码结构比较容易顺着读下来。端口、参数、状态、计数器、寄存器、标志信号、索引和输出都有明确分区。信号名也尽量表达用途而不是大量出现tmp1、data2这类需要反复回头猜语义的名字。例如八邻域分别使用具有明确含义的信号表示flag_neighbor_up_left flag_neighbor_up_center flag_neighbor_up_right flag_neighbor_mid_left flag_neighbor_mid_right flag_neighbor_down_left flag_neighbor_down_center flag_neighbor_down_right这种写法可能不是最短的但对人工审查、课堂讲解、工程交接和后续修改更友好。注释也不是简单重复代码而是在解释某个信号为什么存在、某个状态负责什么、当前帧与下一帧如何切换以及边界条件如何处理。2. 最新版不再把“生成代码”当成第一步最新版更强调先确认行为再生成 RTL。新模块的流程不再是一句自然语言需求 ↓ 直接生成一大段 RTL而更接近需求输入 ↓ 补齐接口、时钟、复位和时序信息 ↓ 形成 codegen plan 与同名模块 Spec ↓ 通过 WaveDrom 预览逐周期行为 ↓ 确认状态转换、握手和边界情况 ↓ 生成 RTL 并执行所需检查这比传统的“一句话需求 → 一大段 RTL”更合理。因为 FPGA 设计里真正容易出错的往往不是 Verilog 语法而是双方对复位、延迟、握手、完成脉冲和异常场景的理解不一致。3. 它对“验证证据”的边界比较克制我比较认可的一点是它会把静态检查、仿真、综合、实现和硬件结果分开。检查层次能说明什么不能直接说明什么静态审查命名、位宽、结构和潜在风险功能一定正确编译或 Lint工具可以解析部分规则检查通过仿真行为正确仿真指定测试场景下行为符合预期综合后时序一定收敛综合设计能够映射到目标器件资源实现时序一定通过实现与时序布局布线后满足指定约束板级系统一定正常上板验证指定硬件环境中的真实运行结果所有输入场景均已覆盖没有实际运行过的检查不应该被写成已经通过。代码看起来合理 ≠ 语法检查通过 ≠ 仿真通过 ≠ 综合通过 ≠ 时序收敛 ≠ 上板可用这听起来像一句常识但在 AI 生成硬件代码时非常重要。四、也有几点需要理性看待首先这次用来对照的是 v1.0.0 生成的一份 RTL 源码没有同时附带 testbench、仿真波形、综合报告和时序报告。因此本次试用主要能说明它在代码组织、接口完整性和可读性方面的表现不能据此直接声称设计已经完成全部工具验证。其次生成结果的注释比较密。对于教学、交接和首次理解很友好但放进已有大型工程后可能还需要根据团队规范适当收敛。另外生命游戏示例直接使用两份 4096 bit 扁平帧数据逻辑关系很直观。但真正部署到具体 FPGA 时仍然需要结合器件资源继续考虑 BRAM/URAM 映射、存储端口、行缓存、吞吐率、流水线、并行化和目标频率。可读的 RTL 初版很有价值但它不等于已经得到最优硬件实现。五、总体感受整体试下来我觉得它已经不太像一个单纯的“Verilog 代码生成 Prompt”而更像一个围绕下面这些环节组织起来的 Codex Skill需求确认行为与时序预览模块 SpecVerilog-2001 RTL 生成现有代码阅读与审查语义注释根据真实诊断进行修复区分静态检查、仿真、综合、实现和硬件证据。它比较适合用在快速搭建结构清楚的 RTL 初版把自然语言需求整理成可审查的模块行为阅读和解释已有 Verilog给旧代码补充语义注释根据真实 Lint、编译或仿真日志定位问题在交付前说明实际完成了哪些检查还有哪些内容尚未验证。当然正式工程里仍然离不开 testbench、仿真、综合、时序分析、上板验证和人工 review。但从这次试用来看它至少把 AI 写 RTL 从生成一段看起来像代码的文本往下面这个方向推进了一步生成一份行为更明确、结构可阅读、能够继续审查和验证的工程结果这也是我认为它目前最有价值的地方。六、项目地址与调用方式GitHubhttps://github.com/Eriemon/verilog-generator也可以直接把下面这句话交给 Codex请从 https://github.com/Eriemon/verilog-generator 安装 Verilog Generator 并使用 $readable-verilog-generator 帮我完成这个 Verilog 任务。#FPGA #Verilog #RTL #Codex #EDA #AgentSkill #AI4EDA