试用专门写 Verilog 的 Codex Skill:比“让 AI 直接吐 RTL”靠谱不少
试用了一个专门写 Verilog 的 Codex Skill:比“让 AI 直接吐 RTL”靠谱不少
试用了一个专门写 Verilog 的 Codex Skill:比“让 AI 直接吐 RTL”靠谱不少
最近试用了一个面向 Verilog-2001 RTL 开发的 Codex Skill:Verilog 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 = 2'd0;
localparam [1:0] ST_SCAN = 2'd1;
localparam [1:0] ST_COMMIT = 2'd2;
三个状态分别负责:
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 = 6'd63; // 64x64 网格中最后一行或最后一列对应的索引值
localparam [63:0]ROW_DATA_ZERO = 64'd0; // 越界行或复位清零时复用的全零行向量
localparam [4095:0]FRAME_DATA_ZERO = 4096'd0; // 双缓冲整帧复位时复用的全零 4096bit 初始图样
//---------------状态参数区域---------------//
// 顶层控制状态编码组
localparam [1:0]ST_IDLE = 2'd0; // 停机并允许外部装载种子的空闲状态
localparam [1:0]ST_SCAN = 2'd1; // 逐点扫描当前代并写出下一代结果的运行状态
localparam [1:0]ST_COMMIT = 2'd2; // 整代扫描结束后切换前后台缓冲的提交状态
//-----------------计数信号-----------------//
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 == 1'b1)begin
state_next = ST_SCAN; // 收到连续运行或单步请求后切换到扫描状态
end else begin
state_next = ST_IDLE; // 没有启动请求时继续停留在空闲状态
end
end
// 状态分支说明: 扫描态遍历全网格并生成后继代位图
// 扫描态逐点遍历 64x64 网格并在末单元后进入提交阶段
ST_SCAN:begin
if(flag_scan_last_cell == 1'b1)begin
state_next = ST_COMMIT; // 整个 64x64 网格最后一个细胞计算完成后进入提交状态
end else begin
state_next = ST_SCAN; // 尚未扫完整个网格时继续保持扫描状态
end
end
// 状态分支说明: 提交态负责换帧并判定后续是否继续运行
// 提交态翻转双缓冲并根据运行使能决定是否继续下一代
ST_COMMIT:begin
if(i_run_en == 1'b1)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 == 1'b0)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 == 1'b0)begin
flag_frame_select <= 1'b0; // 复位后固定由 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 == 1'b0)begin
state_row_index <= 6'd0; // 复位时从网格首行重新开始扫描坐标
end else if((state_current == ST_IDLE) && (state_next == ST_SCAN))begin
state_row_index <= 6'd0; // 从空闲态启动新一代时把扫描起点重新放回首行
end else if(state_current == ST_SCAN)begin
if(flag_scan_last_cell == 1'b1)begin
state_row_index <= 6'd0; // 扫描完最后一个细胞后把下一代行起点预置回首行
end else if(flag_scan_last_col == 1'b1)begin
state_row_index <= state_row_index + 6'd1; // 当前行末列写完后把扫描行号推进到下一行
end
end
end
// 扫描列号在启动、列推进与行切换时进行更新
always@(posedge i_clk or negedge i_rstn)begin
if(i_rstn == 1'b0)begin
state_col_index <= 6'd0; // 复位时从网格首列重新开始扫描坐标
end else if((state_current == ST_IDLE) && (state_next == ST_SCAN))begin
state_col_index <= 6'd0; // 从空闲态启动新一代时把扫描起点重新对准首列
end else if(state_current == ST_SCAN)begin
if(flag_scan_last_col == 1'b1)begin
state_col_index <= 6'd0; // 当前行最后一列写完后把列坐标回卷到首列
end else begin
state_col_index <= state_col_index + 6'd1; // 同一行内部扫描时把列坐标推进到下一个细胞
end
end
end
// A 帧在复位、空闲装载和 B 当前帧扫描时承接下一代写入
always@(posedge i_clk or negedge i_rstn)begin
if(i_rstn == 1'b0)begin
reg_frame_a <= FRAME_DATA_ZERO; // 复位时把 A 扁平帧一次性清成 4096bit 全零图样
end else if((state_current == ST_IDLE) && (i_seed_we == 1'b1))begin
reg_frame_a[index_seed_bit] <= i_seed_value; // 空闲装载阶段把外部种子值写入 A 帧目标单元位
end else if((state_current == ST_SCAN) && (flag_frame_select == 1'b1))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 == 1'b0)begin
reg_frame_b <= FRAME_DATA_ZERO; // 复位时把 B 扁平帧初始化成备用后继代的全零图样
end else if((state_current == ST_IDLE) && (i_seed_we == 1'b1))begin
reg_frame_b[index_seed_bit] <= i_seed_value; // 空闲装载阶段把同一颗种子同步镜像写入 B 帧对应单元位
end else if((state_current == ST_SCAN) && (flag_frame_select == 1'b0))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:
- 需求确认;
- 行为与时序预览;
- 模块 Spec;
- Verilog-2001 RTL 生成;
- 现有代码阅读与审查;
- 语义注释;
- 根据真实诊断进行修复;
- 区分静态检查、仿真、综合、实现和硬件证据。
它比较适合用在:
- 快速搭建结构清楚的 RTL 初版;
- 把自然语言需求整理成可审查的模块行为;
- 阅读和解释已有 Verilog;
- 给旧代码补充语义注释;
- 根据真实 Lint、编译或仿真日志定位问题;
- 在交付前说明实际完成了哪些检查,还有哪些内容尚未验证。
当然,正式工程里仍然离不开 testbench、仿真、综合、时序分析、上板验证和人工 review。
但从这次试用来看,它至少把 AI 写 RTL 从:
生成一段看起来像代码的文本
往下面这个方向推进了一步:
生成一份行为更明确、结构可阅读、能够继续审查和验证的工程结果
这也是我认为它目前最有价值的地方。
六、项目地址与调用方式
GitHub:
https://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
更多推荐



所有评论(0)