资讯动态

Verilog POC代码实战:从概念验证到仿真调试的完整指南

发布时间:2026/9/10 0:58:32 来源:尧图企业网站定制
简介这是一份基于Quartus环境的Verilog概念验证POC代码包面向FPGA/数字逻辑学习者与入门开发者演示了从RTL编写、波形仿真到综合适配的完整设计流程。包体共77个文件压缩包仅215KB除核心的poc.v与printer.v源码外还包含Quartus工程文件qpf/qsf、仿真波形vwf/cvwf、综合与适配报告rpt/summary以及大量编译中间文件cdb/hdb/qmsg等可清晰还原一次经典CPU验证项目的工程组织方式。目前已有463人学习下载。借助包内源码与仿真波形读者可直观理解双向端口inout的驱动规则、三态门使能逻辑以及利用ModelSim/Vivado类工具开展波形模拟的方法同时多种报告文件也为排查时序与映射问题提供了参考适合用来对照学习硬件描述语言的设计验证流程。 说到“verilog POC代码”我先解释一下POC这个词。POC是Proof of Concept的缩写翻译过来叫“概念验证”放到FPGA和数字IC的语境里就是我拿到一个不确定能不能跑通的想法先花半个下午写一份最小的代码在仿真环境里证明它可行再决定要不要继续投入去做完整方案。软件领域习惯叫“技术验证原型”硬件这边其实干的是同一件事只不过我们验证的对象是时序、状态机、跨时钟域这些硬件特有的东西。这几年我在论坛和社群里看到很多朋友搜“verilog计数器”“testbench怎么写”“modelsim怎么仿真verilog文件”其实大家问的大多数问题都不是“某个功能怎么实现”而是“我想验证一下某个思路行不行但不知道怎么开始”。这篇文章就围绕Verilog POC代码这件事把我自己从踩坑到形成方法论的整套经验整理出来不绕弯子直接讲清楚POC到底该怎么做、仿真怎么跑、遇到问题怎么排查以及现在大家都很关心的AI辅助生成Verilog代码到底能不能用。不管你是刚开始学verilog语言入门教程的初学者还是在做毕业设计、项目预研的从业者都可以照着这套流程落地实践。1. 搞懂Verilog POC代码到底是什么1.1 POC不是Demo也不是能交付的工程很多人容易把POC和另外两个东西搞混一个是Demo一个是正式工程代码。Demo讲究的是演示效果比如点亮LED、在屏幕上显示图像、跑一个漂亮的演示界面它不追求边界情况也不追求可维护性。正式工程代码讲究的是稳健、可维护、符合设计规范任何异常输入都得考虑代码风格得统一仿真覆盖率也要尽量到位。POC正好卡在中间。它追求的是“最快速度证明核心风险可以被解决”。举个例子我接到一个需求要评估DDR3读写控制实现方案我不会先把完整控制器写完再去仿真我会先写一个最小系统一个初始化状态机、一组从机模型或者官方IP、一段简单的读写激励验证基本读写时序能通然后停手。这个过程不需要考虑跨时钟域的极致优化不需要覆盖所有边界更不需要接上完整AXI总线。所以我给POC的定义是能够回答一个核心技术问题的最小可仿真代码集合。1.2 一个典型POC代码由哪几块拼成一份标准Verilog POC代码在工程里往往包含下面几块待验证的顶层模块DUT这是你要验证的核心对象通常是一个module可能是UART、SPI slave、I2C读写EEPROM代码、CRC、按键消抖电路或者一个滑动窗口滤波器。测试平台testbench负责给待验证模块产生时钟、复位、输入激励并且检查输出没有testbench的Verilog POC等于没验证。行为级从机模型比如SPI slave、I2C slave、存储模型因为很多时候被测对象需要对接一个外部设备但这个设备在仿真里是不存在的需要你自己用行为级代码模拟一个。仿真脚本或工程文件比如iverilog的编译命令、modelsim的do脚本、Vivado的仿真工程配置这块很多人忽略但其实它是确保POC可以复现的关键。输出检查手段包括波形文件VCD/FSDB、打印日志、断言assertion、数据比对任务。这套结构不是拍脑袋定的是我反复试验之后发现的最省事的搭配。缺了被测模块肯定没戏缺了testbench你根本不知道怎么驱动它缺了从机模型你会发现SPI这种需要外部应答的协议根本没法验证缺了仿真脚本你就得每次手动敲命令效率低到怀疑人生。1.3 从热搜词看POC的常见切入点我看了一下最近相关的热搜词里面高频出现的内容很有意思。大家搜的是“verilog计数器”“verilog按键消抖”“spi协议verilog代码实现”“i2c读写eeprom代码”“uart rs485”“crc verilog”“fir数字滤波器”“轮询仲裁器”“verilog写testbench”这类关键词。这些词背后其实反映了一个共同逻辑它们都是芯片设计和FPGA开发里典型的“核心技术风险点”。比如按键消抖如果你只做软件延时消抖那在FPGA里就要考虑信号抖动、时钟采样、延迟计数器溢出这些问题。SPI协议看上去简单但CPOL/CPHA的四种模式只要选错一种从机读出来的数据全是乱的。UART更不用说了波特率误差超过一定范围数据必然错位。这些模块单独拿出来都不算大工程但每一个都包含硬件设计特有的坑所以最适合做POC。如果把这些关键词拆解一下可以分成三类。第一类是接口类SPI、UART、I2C、HDLC这类协议收发验证重点是时序匹配和协议细节。第二类是算法类FIR滤波器、CRC、滑动窗口滤波验证重点是计算结果的正确性和延迟拍数。第三类是系统类DDR3读写控制、缓存Cache的Verilog实现、轮询仲裁器、递归二分树验证重点是多模块协同和数据一致性。2. 动手前先把POC目标拆成可验证的指标2.1 选一个最小验证子集别让POC变成大工程很多人写POC失败原因只有一个目标太大。一来就想把一个完整的DDR3控制器从初始化、刷新、读写调度到ECC全部写出来这哪是POC这是直接做产品开发。我自己的经验是一个POC只回答一个问题。比如要验证“UART能不能在3MHz系统时钟下跑到115200波特率”核心风险是分频计数器的参数准确性和采样点位置那我就会只写UART发送部分加一段接收回环而不会去写FIFO、中断、FCS校验这些周边内容。要验证“SPI master能不能正确读取EEPROM的ID页”核心风险是状态机的移位移位时序那目标就锁定在读ID这一个命令上写入操作、多页读取、读保护统统不做。当我把目标收敛到这个程度一个POC往往只需要一二百行Verilog代码加一个一百来行的testbench就能完成验证。如果超过五百行基本说明你正在把POC做成项目这时候要回头重新裁剪范围。2.2 明确接口和时序约束定下通过标准写代码之前必须先定清楚两件事接口长什么样仿真通过的标准是什么。接口定义要具体到信号名、位宽、方向、时钟域。比如我要做一个SPI master的POC目标是从机寄存器地址0x00读出ID。那我先画出接口表系统时钟clk、低有效复位rst_n输入start启动信号、addr地址输出rdata读数据、done完成标志外接信号只有cs_n、sclk、mosi、miso四根。没有miso和cs_n以外多余的信号这个POC就算设计好了。通过标准也要提前定。比如“启动start拉高一个时钟后状态机在64个系统时钟周期后拉高done同时rdata等于从机模型返回的ID数据0x5A”。这个标准必须可被仿真结果直接判断不能是“看起来差不多”这种模糊表述。有了明确的通过标准后面写testbench的时候就懂得如何设置expected data而不是漫无目的地看波形。2.3 工具链选择从VSCode到Iverilog到ModelsimPOC阶段最忌讳的就是在工具上花太多时间。如果你只是验证一个小模块完全没必要开Vivado那种重量级IDE编译一次要半分钟慢得你都不想迭代。我个人最推荐的组合是VSCode写代码iverilog编译仿真gtkwave看波形。VSCode装一个Verilog插件做语法高亮和例化模板然后用iverilog的命令行编译。整个过程响应极快适合POC这种高频迭代的场景。如果是在做公司项目新模块或者接口测试往往需要和现有IP联动这个时候就得上仿真工具了。Modelsim是其中最常用的支持多语言混合仿真和波形调试适合稍微复杂的POC。但我要提醒一点Modelsim的TCL脚本和编译库管理有学习门槛第一次用会有点难受建议先建一个标准的do文件模板编译、仿真、添加波形一气呵成不要每次都手工操作。还有一个选择是直接在FPGA厂商的IDE里跑仿真Vivado自带仿真器Quartus里用Modelsim或Questa适合后面要接工程原语的场景。工具选择的原则很简单谁让你离“验证目标”最近你就用谁。不用追求工具链统一也不存在绝对最优解。3. 实操一个SPI主机读寄存器的POC从0到13.1 RTL主体状态机怎么写才不出错理论讲了半天我直接拿一个实战例子来演示完整流程。这个例子是最近在社区里被问得比较多的用一个SPI Master去读从机寄存器IDPOC目标是验证状态机时序是否正确。先看RTL主体我只写核心部分重点看状态机的转移逻辑和移位逻辑。module spi_master_poc #( parameter DATA_W 8 )( input wire clk, // 系统时钟 input wire rst_n, // 低有效复位 input wire start, // 启动一次读取 input wire [7:0] addr, // 从机寄存器地址 output reg [7:0] rdata, // 读数据 output reg done, // 读取完成标志 output reg cs_n, output reg sclk, output reg mosi, input wire miso ); // 状态定义IDLE等待startCMD发送地址RDATA接收数据 localparam IDLE 2d0; localparam CMD 2d1; localparam RDATA 2d2; reg [1:0] state; reg [3:0] bit_cnt; reg [7:0] tx_buffer; reg [7:0] rx_buffer; // 简单的分频假设系统时钟4MHzSPI时钟1MHz分频系数为4 reg [1:0] div_cnt; wire sclk_en; assign sclk_en (div_cnt 2d1); always (posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt 2d0; end else begin div_cnt div_cnt 1b1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; done 1b0; cs_n 1b1; sclk 1b0; mosi 1b0; rdata 8h00; bit_cnt 4d0; tx_buffer 8h00; rx_buffer 8h00; end else begin case (state) IDLE: begin if (start) begin state CMD; cs_n 1b0; mosi 1b0; tx_buffer addr; bit_cnt 4d0; end end CMD: begin if (sclk_en) begin if (bit_cnt DATA_W) begin mosi tx_buffer[DATA_W - 1b1 - bit_cnt]; sclk ~sclk; bit_cnt bit_cnt 1b1; if (bit_cnt[0]) begin tx_buffer {tx_buffer[6:0], 1b0}; end end else begin state RDATA; bit_cnt 4d0; sclk 1b0; end end end RDATA: begin if (sclk_en) begin if (bit_cnt DATA_W) begin rx_buffer {rx_buffer[6:0], miso}; sclk ~sclk; bit_cnt bit_cnt 1b1; end else begin state IDLE; rdata rx_buffer; cs_n 1b1; done 1b1; end end end default: begin state IDLE; end endcase end end endmodule这里我故意用了一种比较“工程化”的状态机写法状态切换和输出赋值都在同一个时序块里状态定义用localparam代替宏定义。这种写法的好处是结构直观POC阶段不需要再引入复杂的状态机编码方式。如果你愿意也可以改成三段式状态机把状态切换和输出组合逻辑分开但POC阶段我建议优先保证可读性和调试便利性。值得注意的一个细节状态进出的时候要同步处理sclk和cs_n不然波形会出毛刺。POC阶段虽然对时序要求不那么严格但如果波形里有毛刺你会耗费大量时间判断到底是代码问题还是仿真问题所以从一开始就要养成干净时序的习惯。3.2 testbench里怎么产生可检查的激励写testbench的核心不是给模块端上电而是创建一个和真实场景接近的工作环境。对于SPI Master来说你需要一个从机模型能响应主机的时钟和命令。timescale 1ns/1ps module tb_spi_master_poc; reg clk; reg rst_n; reg start; reg [7:0] addr; wire [7:0] rdata; wire done; wire cs_n; wire sclk; wire mosi; wire miso; spi_master_poc u_dut ( .clk (clk), .rst_n (rst_n), .start (start), .addr (addr), .rdata (rdata), .done (done), .cs_n (cs_n), .sclk (sclk), .mosi (mosi), .miso (miso) ); // 这里用一个reg做移位模拟从机在sclk上升沿后输出数据 reg [7:0] slave_id; reg [7:0] shift_out; always (posedge sclk or negedge cs_n) begin if (!cs_n) begin shift_out slave_id; end else begin shift_out {shift_out[6:0], 1b0}; end end assign miso shift_out[7]; initial begin clk 0; rst_n 0; start 0; addr 8h00; slave_id 8h5A; #100 rst_n 1; // 启动读取 (posedge clk) start 1; addr 8h00; (posedge clk) start 0; // 等待done wait (done); $display(POC PASS: rdata 0x%02x, rdata); if (rdata ! 8h5A) begin $error(READ FAIL: expected 0x5A, got 0x%02x, rdata); $finish; end #100 $finish; end always #5 clk ~clk; initial begin $dumpfile(spi_poc.vcd); $dumpvars(0, tb_spi_master_poc); end endmodule这段testbench里最容易写错的就是从机模型。很多人直接用组合逻辑assign miso xxx结果在sclk沿附近出现冒险竞争给排查制造巨大干扰。我自己刚学的时候也这么干过后来习惯在sclk的posedge和cs_n的negedge上打一拍输出就从组合逻辑变成了寄存器输出波形干净和真实从机行为也更接近。testbench的职责是尽可能模拟真实器件而不是让被测模块通过一个过于理想的接口。3.3 仿真数据落盘VCD、日志文件、内存Dump三板斧POC验证不只是看波形还要留下可追溯的记录。常用的有三种方式VCD波形、文本日志、内存数据Dump。VCD波形就是$dumpvars导出的文件主要给人看波形。文本日志用$fopen/$fwrite/$fclose配合$display输出主要给人看打印信息。内存Dump用于验证ROM/RAM/FIFO这类存储器模块可以在仿真结束时把所有存储器内容一次性写入文件例如用$readmemh把期望数据读进一个memory仿真后再把实际内容$writememh导出和期望值逐行比对。integer log_fd; initial begin log_fd $fopen(spi_poc.log, w); // 在关键节点写日志 // $fwrite(log_fd, state%0d rdata0x%02x time%0t\n, state, rdata, $time); // $fclose(log_fd); end这里要提醒一个坑使用$fopen/iverilog和Vivado的时候文件路径默认是仿真运行时的当前工作路径。很多人命令是在工程根目录敲的但仿真工具的工作目录可能变了于是怎么也找不到生成的log文件。稳妥的做法是把路径写成相对当前运行的绝对路径或者先cd到固定目录再启动仿真。4. 仿真调试POC最容易踩的坑4.1 波形看着不对先别急着改代码我见过太多人看到波形不对就立刻改代码改完波形更乱最后陷入死循环。调试POC的正确顺序应该是先看时钟和复位是否按预期工作再看控制信号cs_n、sclk这类最后才看数据信号。具体到上面SPI的例子如果rdata一直是0不要第一反应改状态机。先看波形里cs_n有没有正常拉低sclk有没有翻转MISO有没有电平变化。如果sclk一直在翻转但MISO没数据问题很可能出在从机模型而不是主机。如果sclk都没翻转问题大概率出在分频计数器或者状态机根本没进入CMD段。调试时善用gtkwave的标记功能和缩放功能。把sclk的边沿放大逐周期检查主机在SCLK上升沿采样到的MISO是否和从机移位寄存器的输出一致这种方式定位问题比瞎猜快得多。4.2 常见问题速查表我把自己反复遇到的POC仿真问题整理成了一张表每次遇到类似现象就直接查现象大概率原因处理思路信号全是红色X未复位或复位脉冲不够宽检查initial块rst_n拉低保持至少几十个时钟周期输出数据总是慢一拍always (posedge clk)打拍导致输出延迟不是错误注意时序图判断是否影响下游逻辑SCLK频率比预期低很多分频计数器位宽写错溢出周期过长用$display打印分频计数器和div_cnt核对预期周期$fopen写出的文件找不到工作路径和预期不一致在代码里使用绝对路径或先cd到工程目录状态机卡住不跳转某个条件一直不满足或bit_cnt没有清零在状态机的每个分支加$display打印状态变化从机模型数据竞争组合逻辑assign直接驱动MISO改用posedge sclk或negedge cs_n打一拍再输出仿真无限循环不结束状态机存在不可达状态或死循环加default状态检查状态转移条件加仿真总超时$finish使用wait(done)后一直无响应done信号没拉高或拉高了又立即拉低检查done的置位和复位逻辑必要时用脉冲计数器观察4.3 仿真通过不等于上板能跑这是一个我反复强调的观点POC仿真通过只能说明你的逻辑功能在理想仿真环境下正确不代表烧到FPGA上就能工作。仿真是零延迟的理想世界没有信号完整性问题没有时钟抖动没有跨时钟域的亚稳态也没有上电时序问题。所以在POC阶段给仿真结果加一个限定词“功能仿真通过”而不是“验证通过”。如果后续要上板至少还要再做几件事把分频系数调整到真实时钟频率、检查复位释放是否满足时序要求、跨时钟域信号是否需要同步器、测试引脚和物理约束有没有冲突。POC的价值在于帮你快速排除方案层面的风险而不在于包办所有工程化问题。5. AI Agent辅助写Verilog POC我实测后的判断5.1 AI能做什么生成骨架、写tb、补注释最近用AI Agent写Verilog代码已经不是什么新鲜事了社区里到处能看到“claude code写verilog代码”甚至“用AI Agent架构生成RTL”的讨论。我自己的实测结论是AI在POC阶段确实能提效尤其是生成骨架和testbench但绝对不能不做任何审查就直接拿来用。我最常让AI干的三件事生成module骨架、生成testbench初版、给代码补注释。比如我要验证一个SPI slave我会把接口表直接贴给AI告诉它时钟频率、协议模式、帧格式让它生成一份带状态机的Verilog然后再生成一份能发送激励并检查输出的testbench。实测下来AI生成骨架的准确率大概在七成左右接口声明和基本状态机能写对但对时序细节的把握经常出错比如数据采样沿选错、分频计数初始值没算对、复位操作不完整。所以我的工作流程是让AI做初稿我做审查和验证。审查集中在时序逻辑验证集中在仿真是否满足自己定的通过标准。AI写得再快如果我不理解它生成的状态机逻辑那这份代码对我就是不可维护的黑盒。5.2 AI不该替你做什么复位策略、时序分析、验证计划AI在POC阶段的另一个大坑是它经常写出“看起来正确但实际不可综合”的代码。比如它会生成复杂的循环和动态索引这在软件里没问题在Verilog里可能综合出一堆莫名其妙的LUT和MUX。或者在always块里混用阻塞赋值和非阻塞赋值仿真能过上板就跑飞。我自己总结的几条铁律是AI生成的代码必须逐行看懂才允许进工程复位策略由人决定AI给出的复位写法只作参考时序分析时钟频率、建立保持时间、跨时钟域必须由人来做AI给不了实物波形验证计划里的通过标准必须由人定义AI生成的断言只能作为辅助。POC虽小但它也是硬件代码不能把验证闭环完全交给AI。5.3 chisel生成RTL和原生Verilog开发POC阶段怎么选顺便说一个大家经常问的新老开发方式对比问题chisel方式生成RTL与原生verilog开发有什么区别我的看法是chisel适合你已经确定方案、需要大规模参数化代码生成的项目它的Scala抽象能力让生成器代码复用性很强适合团队化、需要长期维护的设计。原生Verilog更直接适合快速验证和思路探索。在POC阶段我的建议是优先用原生Verilog。因为POC的目标是快速验证而不是追求抽象层次。你用chisel写了一堆类最后生成的RTL还是要仿真而多出来的这一层抽象反而会干扰你对信号级行为的判断。等到方案验证完确认要进入正式开发阶段再评估是否值得引入chisel来重构生成逻辑。工具选型是为目标服务的不要为了炫技破坏POC的效率。我自己做POC有个习惯现在也推荐给周围同事不管多小的验证都保留三样东西一个VCD波形文件、一份仿真日志、一段记录通过标准的README。刚开始会嫌麻烦但攒下几十个POC之后你会发现很多模块的验证思路是可以复用的。比如你验证过一个SPI master再遇到SPI slave、UART、I2C你会自然形成一套“先写激励、再建模型、再定通过标准”的套路。最后再说一个小技巧写testbench的时候多花十分钟加自检逻辑用$display、$error和wait语句把通过标准写进代码里。这样每次跑仿真你不需要盯着波形掐表数周期只要看终端有没有PASS效率完全是另一个量级。这套方法我用了很多年从学生时代的作业项目到后来做模块预研一直稳定可靠。POC这件事做的次数越多越明白一个道理代码写得好不好是次要的验证思路清不清楚才是瓶颈。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价