ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Verilog语言基础:从硬件思维到工程实践

Verilog语言基础:从硬件思维到工程实践 简介一份面向数字电路与FPGA初学者的Verilog HDL基础PDF用于系统学习硬件描述语言的核心概念与多种建模方式。内容从Verilog HDL的简介与发展历史切入详细讲解模块语法、端口声明、连续赋值语句、行为描述与结构化描述并覆盖基本逻辑门、用户定义原语、开关级结构模型、端口到端口时延与路径时延及时序检查等主要能力同时介绍线网和寄存器两类数据类型以及PLI编程语言接口、文件读写、并发与定时建模、条件/情况/循环语句等高级语言结构并辅以半加器模块示例说明模块实例化过程。文档结构清晰从语言背景到语法再到模块实例层层递进适合按章节顺序阅读也可作为备查手册快速定位知识点。资源为单个PDF文档大小约188KB轻量便于在电脑或手机上随时阅读。站内已有1336人学习适合作为课程复习、FPGA入门或数字IC设计前的基础速览资料。 作为常年跟FPGA和数字IC打交道的人我经常会收到类似“Verilog怎么学”“基础要看什么”的提问。市面上的Verilog教程多如牛毛但大部分要么是语法手册式的罗列让人看完记不住重点要么是直接扔出一大段复杂代码新手瞬间劝退。我自己的体会是Verilog语言基础绝不只是记住几个关键字和运算符而是建立一套“硬件思维”——用代码描述电路而不是用代码执行任务。这篇文章我就结合这些年写代码、调仿真、带新人的实际经验把Verilog基础里真正重要的东西拆开讲清楚也把很多热搜里反复出现的工程场景计数器、状态机、UART、SPI、I2C、testbench等背后共通的语法和设计思路一并串起来。1. 为什么“Verilog语言基础”不只是语法背诵很多初学者容易陷入一个误区拼命背语法把module、always、assign这些关键字倒背如流却依然写不出能综合的电路。这里面的关键问题在于Verilog是一门硬件描述语言它的核心价值是“描述硬件结构”而不是像C语言那样描述“计算过程”。1.1 硬件描述语言和软件语言的本质差异写C语言程序时你关注的是算法逻辑、函数调用、变量赋值代码是顺序执行的。而写Verilog时你描述的是一个个物理上真实存在的电路单元寄存器、组合逻辑门、连线、状态机。代码里的一句assign a b c;在综合后就是一颗真实的与门一个always (posedge clk)块在综合后就是一排真实的触发器。这个差异决定了学习Verilog基础的第一原则看得见电路才写得好代码。我见过很多工程师能把语法背得滚瓜烂熟写出来的代码综合出来却是一堆莫名其妙的锁存器原因就是他在用“软件思维”写硬件。1.2 从“功能描述”到“可综合电路”的思维转换写可综合的Verilog代码本质上是在做三件事实例化把现成的门电路、触发器、IP核连接起来。行为描述用always块和assign语句描述电路的行为让综合工具推断出对应的硬件。层次化组织用module把复杂电路拆分成多个可复用的子模块。理解这一点之后再去看那些基础的语法点思路会清晰很多。比如wire和reg的区别本质上对应的是“物理连线”和“存储单元”的区别assign语句对应组合逻辑赋值always块既可以是组合逻辑也可以是时序逻辑。这些不是语法规定而是硬件结构的抽象。2. 模块化设计与数据流思维从第一个module说起任何Verilog设计的基本单元都是module。你可以把module理解成一块“芯片”的封装它有对外的引脚端口有内部的电路实现。学好module的组织方式是写大型项目的基础也是“Verilog语言基础.pdf”这类教程往往放在第一章的原因。2.1 端口定义与连接规则一个标准模块的端口定义方式有两种Verilog-1995风格的module name(a, b, c);和Verilog-2001风格的module name #(参数) (input a, output reg b);。我强烈建议新项目一律采用Verilog-2001风格ANSI风格因为端口方向和数据类型写在一起直观且不容易出错。// 推荐ANSI风格端口定义 module counter #( parameter WIDTH 8 )( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] count ); always (posedge clk or negedge rst_n) begin if (!rst_n) count {WIDTH{1b0}}; else if (en) count count 1b1; end endmodule这段代码是“热搜词”里反复出现的计数器模块的标准写法。注意几个基础细节#(parameter WIDTH 8)是参数化设计调用时可以通过#(.WIDTH(16))覆盖默认值这是模块复用能力的关键。input wire和output reg显式声明了端口类型。wire对应组合逻辑连线reg对应在always块中被赋值的变量。复位采用异步复位、同步释放是常见的工程做法这里先用标准的posedge clk or negedge rst_n实现异步复位。2.2 wire与reg两个最容易混淆的数据类型很多新手对wire和reg的区分感到头疼。我从电路角度给一个直观的理解wire线网是被驱动的它本身不存储值。assign语句驱动的信号、模块端口连接的信号都应该是wire。综合出来就是一根导线或一个门的输出端。reg寄存器是在always块中被赋值的变量。综合工具会根据always块的写法决定实现为触发器还是组合逻辑。名字叫“register”但实际不一定是寄存器也可能综合成组合逻辑。一个常见反例是在always块里给端口直接赋值时端口类型没写对导致综合失败。用ANSI风格端口声明配合“输入端用wire需要在always里赋值的输出端用reg”这个经验法则能避开九成以上的端口类型错误。2.3 模块实例化与层次化设计一个模块写好之后可以通过实例化在另一个模块中复用。实例化语法是很多入门教程一笔带过的部分但在实际工程里几乎天天用counter #( .WIDTH(16) ) u_counter ( .clk (clk), .rst_n (rst_n), .en (en), .count (count_bus) );这里的u_counter是实例名#(.WIDTH(16))是参数覆盖括号里是端口连接。推荐使用按名字连接.端口名(信号名)而不是按位置连接这样代码可读性高也不容易因为端口顺序调整而出错。在Xilinx Vivado或Altera Quartus的工程里层次化例化是组织大型设计的基本手段“Verilog module”这个热搜词的高热度也从侧面说明了模块化是每个初学者绕不开的基本功。3. 过程块、阻塞与非阻塞组合逻辑和时序逻辑的分野always块是Verilog行为描述的核心也是无数仿真与综合不一致问题的来源。掌握always块的关键是明白阻塞赋值和非阻塞赋值背后的硬件时序含义。3.1 阻塞赋值与非阻塞赋值的硬件含义先看一段经典的移位寄存器例子// 正确非阻塞赋值综合为真正的移位寄存器 always (posedge clk) begin q1 d; q2 q1; q3 q2; end // 错误阻塞赋值综合后只有一个触发器d直接到q3 always (posedge clk) begin q1 d; q2 q1; q3 q2; end为什么结果不同因为非阻塞赋值的语义是“在一个时间步结束时统一更新”三个赋值语句同时采样旧值、同时更新新值对应三个串联的触发器而阻塞赋值的语义是“立即更新”执行到q2 q1时q1已经是新值d于是综合下来前面两级被优化掉只剩一个触发器。工程经验法则非常明确时序逻辑posedge clk触发的always块里一律使用非阻塞赋值。组合逻辑电平敏感的always块或assign语句里使用阻塞赋值或直接assign。同一个always块中不要混用和。这个规则我不是第一次强调但每次评审代码还是会看到有人踩坑。它的本质原因是硬件里所有触发器共享同一个时钟边沿采样值和更新值必须严格分开否则就会出现“数据穿透”的硬件错误。3.2 always块中的组合逻辑与锁存器陷阱组合逻辑也可以写在always块中此时使用阻塞赋值且所有分支必须完备否则综合工具会推断出锁存器Latch。锁存器在FPGA里一般是不受欢迎的因为它时序分析困难、容易产生毛刺而且占用资源方式特殊。// 容易产生锁存器的写法 always (*) begin if (sel) y a; // 缺少elsesel为0时y保持旧值综合出锁存器 end // 正确的写法给出完整分支 always (*) begin if (sel) y a; else y b; end初学者写组合逻辑时最好先给输出赋默认值再用if或case覆盖特定条件比如always (*) begin y 1b0; // 默认值避免锁存器 if (sel) y a; end这个“赋默认值”的小技巧配合case语句的default分支能从根上杜绝大多数无意识Latch的生成。4. 参数化设计计数器、分频器与常用基础模块的工程写法Verilog基础里最常被考核的知识点就是计数器、分频器、按键消抖这类基础模块。它们看似简单却是几乎所有实用项目的积木。这个章节我专门把它们的工程写法和常见问题梳理一遍。4.1 计数器与时钟分频二分频到任意分频计数器是数字电路的基本单元也是理解时序逻辑的最佳入门例子。除了前面那段标准计数器实际工程里更常用的是周期可配置计数器// 通用周期计数器每COUNT_MAX个时钟周期产生一个脉冲 module pulse_gen #( parameter COUNT_MAX 1000 )( input wire clk, input wire rst_n, output reg pulse ); reg [15:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 16d0; pulse 1b0; end else if (cnt COUNT_MAX - 1) begin cnt 16d0; pulse 1b1; end else begin cnt cnt 1b1; pulse 1b0; end end endmodule“二分频”是热搜词里的高频概念。用触发器翻转实现二分频是经典写法always (posedge clk) clk_div ~clk_div;。但需要特别提醒在FPGA设计中不建议用寄存器输出直接作为全局时钟使用。更好的做法是使用FPGA厂商提供的PLL/MMCM IP核进行时钟分频管理或使用时序约束明确约束分频时钟域。寄存器分频后的信号适合作为使能脉冲而不是时钟。这个观念从学习基础阶段就建立起来能省去日后大量的时序调试时间。4.2 按键消抖状态机与计数器结合的小样板按键消抖是热搜词中非常经典的话题它非常适合作入门小项目因为麻雀虽小五脏俱全有输入、有输出、有内部状态、有定时逻辑。常见的机械按键抖动时间在5ms~20ms消抖原理是“信号稳定一段时间才认为有效”。用Verilog实现最实用的方法是“计数器消抖”检测到按键电平变化后启动一个计数器计数到稳定时间比如20ms才输出有效电平。// 按键消抖抖动窗口内持续采样稳定后才翻转输出 module debounce #( parameter CNT_MAX 1_000_000 // 20ms 50MHz )( input wire clk, input wire rst_n, input wire key_in, output reg key_out ); reg [19:0] cnt; reg key_dly; always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_dly 1b1; cnt 20d0; key_out 1b1; end else begin key_dly key_in; if (key_dly ! key_in) begin cnt 20d0; // 检测到变化重置计数 end else if (cnt CNT_MAX - 1) begin key_out key_in; // 稳定足够久输出最终电平 end else begin cnt cnt 1b1; end end end endmodule这段代码里值得学习的点有打拍采样key_dly、边沿检测思维、计数稳定窗口。有了这个基础后面理解更复杂的协议时序SPI、I2C时你会发现它们本质上都是在做类似的事情——对信号进行采样、判断、定时。4.3 状态机的三段式写法与完整范例状态机是数字设计中最核心的通用结构UART接收、SPI传输、I2C读写、CAN协议解析全部离不开状态机。Verilog状态机的基础写法主要有三种一段式、二段式、三段式。我推荐使用三段式因为它结构清晰、时序优化容易是业界主流风格。三段式的核心是三个always块第一段时序逻辑描述状态寄存器的跳变state next_state。第二段组合逻辑根据当前状态和输入计算下一个状态next_state。第三段时序逻辑根据当前状态描述输出。// 三段式状态机示例UART接收一个起始位流程简化示例 localparam IDLE 3d0; localparam START 3d1; localparam DATA 3d2; localparam STOP 3d3; reg [2:0] state, next_state; // 第一段状态跳变 always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end // 第二段组合逻辑计算下一状态 always (*) begin next_state state; case (state) IDLE: if (rx_start) next_state START; START: if (mid_bit) next_state DATA; DATA: if (bit_done) next_state STOP; STOP: if (bit_done) next_state IDLE; default: next_state IDLE; endcase end // 第三段输出逻辑时序输出避免毛刺 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_done 1b0; end else begin rx_done (state STOP bit_done) ? 1b1 : 1b0; end end写状态机时要注意两点组合逻辑段中next_state一定在case前先赋值为当前状态避免锁存器状态编码优先使用localparam而不是define避免全局宏污染。状态机看起来复杂但一旦掌握套路写起来很快调试起来也很有条理——看波形时先看state信号跳到哪了再对照输入条件检查为什么跳转问题定位效率极高。5. testbench与仿真验证Modelsim/VCS/开源工具链实践学Verilog基础仿真验证是绝对不能跳过的一环。很多初学者写完代码不知道如何验证正确性也有人只会在FPGA开发板上点灯。掌握testbench编写和常用仿真工具的使用才能形成“编写-仿真-调试-修改”的闭环。5.1 testbench的基本结构与激励生成Testbench本质上也是一个Verilog模块只不过它不综合成硬件而是用来产生激励信号并观察被测模块DUT的输出。基础结构如下timescale 1ns / 1ps module tb_counter(); reg clk; reg rst_n; reg en; wire [7:0] count; // 例化被测模块 counter #( .WIDTH(8) ) u_counter ( .clk (clk), .rst_n (rst_n), .en (en), .count (count) ); // 生成50MHz时钟 initial clk 1b0; always #10 clk ~clk; // 激励过程 initial begin rst_n 1b0; en 1b0; #100; rst_n 1b1; #20; en 1b1; #500; en 1b0; #200; $finish; end endmodule这段代码里演示了几个testbench常用技巧timescale时间刻度声明、initial块生成复位和激励、always #10生成周期性时钟、$finish结束仿真。对于更复杂的设计你还需要掌握$display打印信息、$monitor监控信号变化、以及文件读写任务$readmemh/$writememh很多人在“verilog仿真数据存成文件”这个需求里就会用到。5.2 Modelsim仿真的流程与注意事项“modelsim如何仿真verilog文件”在热搜里常年出现这里把流程简化成五个步骤创建工程添加所有.v源文件。编译Compile看是否有语法错误。在Library面板选中testbench模块右键Simulate。在Wave窗口添加需要观察的信号设置仿真时间。Run运行观察波形并调试。常见的坑有忘了在testbench中加initial $finish导致仿真一直跑不停timescale设置不当导致波形时间单位异常没有添加被测模块的全部输入信号导致输出为高阻态X。这些坑都很基础但几乎每个新手都会遇到至少一次。5.3 开源工具链VSCode Icarus Verilog GTKWave如果你不想用庞大的商业EDA工具或者只是做语法验证和简单仿真开源工具链完全够用。我的个人推荐组合是VSCode安装verilog插件和verilog-format插件实现语法高亮、自动格式化和部分语法检查。Icarus Verilogiverilog命令行编译仿真工具支持大部分Verilog-2001语法适合教学和中小规模设计验证。GTKWave看完波形必备支持VCD/FST格式波形查看。运行方式非常简单iverilog -o tb_counter.vvp tb_counter.v counter.v vvp tb_counter.vvp gtkwave dump.vcd“vscode写verilog”这个热搜词背后实际上就是这套轻量级工具链的应用。对于学习语言基础、写写练习模块、验证状态机逻辑这套组合比动辄几个GB的IDE要清爽得多也非常适合学生党或想快速验证想法的工程师。6. 接口协议基础模块UART、SPI、I2C的Verilog实现要点在热搜词中UART、SPI、I2C三兄弟的热度一直居高不下。它们都是数字系统里最常用的通信协议也是检验Verilog基础功力的试金石。理解它们是掌握“用Verilog实现接口逻辑”的关键。6.1 从仿真文件到协议模块数据怎么“跑起来”很多新手在学完语法后遇到的最大困惑是我自己写的testbench只是给了几个信号但真实的UART数据、SPI时序到底怎么产生我的建议是先学用Verilog写这些协议的master端或收发器再回到testbench中做闭环验证。以UART为例你需要实现的本质是发送端把并行的8位数据变成逐位串行输出按照起始位低电平、数据位LSB first、停止位高电平的帧格式发送。接收端检测起始位下降沿按波特率采样数据线把每一位移位拼装成并行数据。这个过程中核心用到的Verilog知识点其实就是计数器波特率分频计数、移位寄存器串并转换、状态机帧状态管理。我在基础教学里经常强调“协议”听起来高深落到代码上无非是计数器加状态机。只要能熟练使用这两个基础技能UART、SPI、I2C都可以理解成不同参数的数据搬运过程。6.2 SPI主从模块与I2C读写EEPROM的共通道理SPI的Verilog实现重点在于产生正确的时钟极性CPOL和相位CPHA以及移位寄存器在合适边沿收发数据。它的代码结构通常包含分频/使能逻辑产生SCLK。移位寄存器模块负责MOSI/MISO的数据移位。状态机控制片选信号CS的拉低拉高和传输长度。I2C则要稍微复杂一些因为它有应答位机制ACK/NACK和更细的时序要求起始条件、停止条件。如果要实现一个I2C控制器去读写EEPROM你需要关注起始条件和停止条件的产生与检测SCL高电平期间SDA的下降沿/上升沿。器件寻址字节的组装7位从地址加读写位。每个字节传输完后的ACK等待状态。随机读、顺序读、页写等操作命令的流程控制。代码层面我把I2C控制器抽象为两个层次一个是字节传输层产生SCL、采样SDA、处理ACK另一个是命令流程层状态机控制读写的完整时序。这两个层次分别对应“计数器/移位寄存器基础”和“状态机高级应用”正好把Verilog基础里的两大核心技能串起来了。因为I2C和SPI代码较长这里不展开完整实现但我强烈建议初学者自己动手写一遍这两个模块这是从基础语法走向真实项目的最佳练习。7. 数字信号处理的Verilog初探滑动窗口滤波与FIR“滑动窗口滤波verilog”和“基于fpga的fir数字滤波器 verilog”这两个热搜词代表了许多学习者从接口协议走向信号处理方向的延展需求。所谓基础不仅是语法基础也包括“用Verilog实现算法”的思维基础。7.1 滑动窗口滤波均值滤波的寄存器实现滑动窗口均值滤波的硬件思路非常简单用一个移位寄存器保存最近的N个采样值每次新数据进来替换最旧的数据然后计算平均值。// 4点滑动平均滤波器简化示例 module sliding_avg #( parameter DATA_WIDTH 12 )( input wire clk, input wire rst_n, input wire valid_in, input wire [DATA_WIDTH-1:0] data_in, output reg [DATA_WIDTH1:0] data_out ); reg [DATA_WIDTH-1:0] fifo [0:3]; reg [1:0] wr_ptr; reg [DATA_WIDTH1:0] sum; always (posedge clk or negedge rst_n) begin if (!rst_n) begin // 复位清空 end else if (valid_in) begin sum sum - fifo[wr_ptr] data_in; // 加新减旧 fifo[wr_ptr] data_in; data_out sum 2; // 右移两位等价于除以4 end end endmodule这个设计的关键优化在于不重新求和而是“加新减旧”用一个累计和寄存器维持总和每次只需要一次加法和一次减法硬件代价远小于重新求和。这种优化思路在Verilog算法实现中非常常见——用硬件资源额外的寄存器换取计算开销本质上是空间换时间。7.2 分布式算法与FIR滤波器的基础概念“分布式算法 fir fpga”是另一组高频搜索词。分布式算法DA算法是FPGA实现FIR滤波器时最常用的一种优化技术它的核心思想是预先计算所有可能的乘法-加法组合查表再通过移位累加完成滤波运算从而把乘累加运算转化为查找表和加法。虽然完整实现一个分布式FIR比较复杂但它的Verilog基础要点值得掌握将输入信号的每一位分别处理通过查找表LUT预先算出部分积。移位累加把查找表结果按照位的权重进行移位累加。最终累加结果就是滤波输出。写这个模块时你需要的Verilog基础能力包括参数化模块设计、二维寄存器数组RAM、generate语句批量生成逻辑、有符号数运算。这些正是“verilog语言基础”从入门到进阶的延伸方向。有精力的读者可以尝试先写一个3阶或5阶的DA-FIR滤波器仿真通过后再扩展阶数这个过程对Verilog能力的提升非常明显。7.3 有符号数与位宽截断算法实现中躲不开的坑滤波器、信号处理类的Verilog实现最常见的错误出在有符号数和位宽处理上。基础阶段就应该养成好习惯有符号运算使用integer、signed reg、$signed()转换。乘法结果的位宽是两操作数位宽之和加法结果位宽要留出进位空间。截断数据时考虑四舍五入或饱和处理而不是简单丢弃低位。除法操作避免使用/运算符在可综合代码中直接使用可以用移位近似或使用除法IP核。这些细节看似零散但恰恰是工程代码质量的分水岭。很多从C语言转过来的工程师一开始很难意识到“一个加法器溢出后数据就错了”在硬件里意味着什么而这些问题在设计基础阶段多留意后面能省很多调试时间。8. 常见仿真与调试问题排查从语法到功能的完整链路最后一个基础章节我想集中讲一讲调试方法论因为学习Verilog基础的过程中调试能力甚至比写代码能力更重要。很多初学者遇到仿真波形不对就不知道从哪里下手。这里分享一套我常用的排查链路。8.1 编译报错与仿真结果异常的排查顺序当你的代码编译或者仿真出现问题我建议按以下顺序排查语法层看编译报错行号和提示。最常见的“隔壁行报错”情况通常是上一行缺少分号、end没配对、begin块没闭合。我自己的习惯是写完代码先做一次快速的人工检查再交给工具编译。编译层检查是否有信号未声明、位宽不匹配、模块例化端口连接错误。这些错误在波形仿真前就会暴露修正成本最低。仿真层波形中出现高阻Z或未定义X多半是输入没接好、复位没释放、或者信号没有被正确驱动。先在testbench中把每个输入信号都初始化好再看输出波形。逻辑层波形能产生但功能不对。这时候要分两个方向查一看状态机跳转是否符合预期观察state信号实际值二看计数器时序是否与设定参数吻合。8.2 波形观察中的高阻、不定态与竞争冒险高阻态Z经常出现在三态门或未连接的端口上不定态X则可能来自未复位寄存器或未初始化的信号。排查时先确认所有寄存器是否接了复位信号并在复位后被赋初值。testbench中所有输入端口是否都被驱动尤其enable、sel这类控制信号。是否存在阻塞赋值在时序逻辑中引起仿真-综合不一致前面第3节提到的经典坑。8.3 从“能编译”到“可综合”编码风格与lint检查写好Verilog基础代码不只是“仿真通过”还要“可综合、易维护”。几个我长期坚持的编码习惯一个always块只描述一个功能避免巨型块难以阅读。永远是“先声明后使用”信号命名要体现语义cnt、flag、rx_data、state。时序逻辑一律非阻塞赋值组合逻辑一律阻塞赋值不混用。用localparam定义状态和常量不使用魔法数字。写完后用综合工具跑一次Lint检查看看有没有产生锁存器、是否有未使用信号警告。这些习惯不需要一开始就全部做到完美但从基础阶段开始逐步养成会让后面的项目开发效率明显提升。9. 我的一些学习路径建议与工具推荐这部分算是经验之谈。如果你正处在“Verilog语言基础”的学习阶段我建议的路径是这样的先快速过一遍语法基础但不要在语法书里停留太久紧接着拿一个经典小模块计数器、状态机、UART边写边仿真在实战中理解语法之后再去看协议、算法相关的高级实现一步步扩大自己的模块库。我个人在实际教学和带新人时非常看重“三个一百”训练法写一百行基础代码、写一百行testbench、看一百张波形图。听起来枯燥但上手速度奇快。很多看起来复杂的模块写完之后回头看会发现核心还是那些最基础的计数器、移位寄存器和状态机。工具方面再补充一个建议初学者先用简洁的iverilog GTKWave或ModelSim把仿真跑顺别一上来就折腾复杂IDE和时序约束。仿真和综合是不同的概念先用仿真理解“功能正确”再逐步接触综合与实现层面节奏会舒服很多。等需要做完整的FPGA开发时再选择Vivado或Quartus配合对应的开发板逐步上手。“Verilog语言基础”这个话题看似简单实际展开之后覆盖了从语法、模块化设计、时序逻辑、状态机、testbench到接口协议和算法实现的整个入门到进阶链路。这篇文章的各个章节对应的都是这条链路上的关键节点。希望我整理出的这些思路和踩坑经验能帮助你少走一些弯路尽快建立起属于自己的硬件设计思维。本文还有配套的精品资源点击获取
返回列表