Verilog硬件描述语言入门指南从语法基础到数字电路设计很多刚开始接触FPGA或者数字电路设计的朋友一看到Verilog代码可能就有点懵——这玩意儿看着像C语言但写起来又不太一样。我刚开始学的时候也这样总觉得是在写软件结果仿真出来波形完全不对。后来才明白Verilog描述的是硬件电路你得用“硬件思维”去写代码。这篇教程我就用最直白的方式带你从零开始理解Verilog。咱们不搞那些虚的理论堆砌就说说实际写代码时那些最常用、最关键的知识点。学完这篇你就能看懂大部分Verilog模块并且能自己动手写一些简单的数字电路了。1. Verilog是什么和C语言、VHDL有啥不同在深入语法之前咱们得先搞清楚Verilog到底是干嘛的。这能帮你建立正确的“硬件思维”避免用写软件的习惯去写硬件代码。Verilog简单说就是一种用来描述数字电路的“图纸”语言。你写的代码最终会被综合工具比如Vivado、Quartus转换成实实在在的门电路、触发器等硬件结构烧录到FPGA或者做成ASIC芯片。它诞生于1980年代语法设计上借鉴了C语言所以看起来比较亲切。那么它和咱们熟悉的C语言以及另一个常见的硬件描述语言VHDL到底有啥区别呢我做了个简单的对比特性VerilogC语言VHDL本质硬件描述语言(HDL)描述电路结构/行为通用编程语言描述算法和流程硬件描述语言(HDL)描述电路结构/行为执行模型并行执行。多个always块、assign语句同时工作模拟真实硬件。顺序执行。代码一行接一行地执行。并行执行。进程Process之间并发工作。核心思维你在设计一个电路代码描述的是连线和寄存器。你在编写一个解决问题的步骤序列。你在进行一个严格、严谨的硬件建模类型检查非常严格。语法风格类似C相对简洁、灵活。C语言风格。类似Ada非常冗长、严谨强类型。主要用途FPGA/ASIC设计、数字电路仿真。软件开发、嵌入式软件。复杂系统、军工航天等对可靠性要求极高的领域。打个比方用C语言写程序就像指挥一个厨师CPU按菜谱一步步做菜。而用Verilog设计电路就像你直接设计了一个自动化厨房洗菜机、切菜机、炒菜锅各个模块同时启动并行工作。所以学Verilog第一课忘掉“顺序执行”时刻想着“并行”和“电路”。2. 认识Verilog的“积木块”模块、端口与数据类型一个完整的数字电路比如一个计数器、一个UART串口在Verilog里就是一个module模块。模块之间通过输入输出端口连接构成更大的系统。2.1 模块的基本结构每个Verilog文件通常就是一个模块。它的骨架长这样module 模块名 ( // 端口声明 input wire clk, // 输入端口时钟信号 input wire rst_n, // 输入端口低电平有效的复位信号 input wire [7:0] data_in, // 8位宽的数据输入端口 output reg [7:0] data_out // 8位宽的数据输出端口用reg类型 ); // 在这里写你的逻辑代码 // 比如组合逻辑 assign 时序逻辑 always endmodulemodule和endmodule是必须的关键字像一对括号把整个模块包起来。端口声明定义了模块与外界通信的接口就像芯片的引脚。主要有input输入、output输出、inout双向初学者少用。端口后面可以跟wire或reg类型但通常input只能是wireoutput可以是wire或reg。2.2 两种核心数据类型wire和reg这是Verilog里最常用也最容易混淆的两个类型。你可以这样理解wire线网代表电路中的一根物理连线。它本身不能存储信息只能传递信息。它的值由驱动它的信号源决定。在assign语句中左边必须是wire型变量。wire a, b, c; assign c a b; // c是一根线它的值永远是a和b相与的结果reg寄存器代表一个存储单元能保存一个值直到下次被赋值。注意reg不一定综合成触发器Flip-Flop它只是表示一种“保持”的能力。在always块中被赋值的变量必须声明为reg型。reg q; // 一个存储单元 always (posedge clk) begin q d; // 在时钟上升沿把d的值存到q里 end注意reg和实际硬件中的“寄存器”或“触发器”不是严格对应的。综合工具会根据always块的触发条件是电平敏感还是边沿敏感来决定把它实现为组合逻辑一堆门电路还是时序逻辑触发器。2.3 常量与参数parameterparameter用来定义模块内的常量提高代码的可重用性和可读性。比如定义一个数据宽度module my_module ( input wire [7:0] data_in, output reg [7:0] data_out ); parameter DATA_WIDTH 8; // 定义一个参数数据宽度为8 parameter INIT_VALUE 8‘hFF; // 初始值为十六进制FF // 使用参数 reg [DATA_WIDTH-1:0] counter; // 声明一个位宽为8的计数器 initial begin data_out INIT_VALUE; // 初始化输出值 end endmoduleparameter的值在编译时确定不能在仿真或运行时改变。如果某个常量只在本模块内部使用可以用localparam它更安全防止外部修改。3. 描述硬件行为的两种核心语句硬件行为无非两种组合逻辑输出只取决于当前输入和时序逻辑输出还取决于时钟和过去的状态。Verilog用assign和always这两条语句来描述它们。3.1 组合逻辑assign连续赋值assign语句用于描述组合逻辑它像一根导线右边表达式一变化左边立刻跟着变。wire a, b, c, d, sel; wire out; // 描述一个2选1选择器 (MUX) assign out (sel 1‘b0) ? a : b; // 描述一个与门 assign c a b; // 描述一个复杂的逻辑表达式 assign d (a | b) ~c;特点左边必须是wire型变量。是“连续”的只要右边有变化赋值就发生。不能带有时钟或边沿触发概念。3.2 时序逻辑与复杂组合逻辑always过程块always块功能强大既能描述时序逻辑带时钟也能描述复杂的组合逻辑。描述时序逻辑触发器reg [7:0] count; // 计数器需要存储所以用reg // 一个简单的时钟上升沿触发的计数器 always (posedge clk or negedge rst_n) begin if (!rst_n) begin // 如果复位信号有效低电平 count 8‘d0; // 计数器清零 end else begin // 正常时钟上升沿 count count 8‘d1; // 计数器加1 end end(posedge clk)表示在时钟clk的上升沿触发这个块。是非阻塞赋值这是时序逻辑的标配。它意味着块内所有赋值是同时计算的在时钟沿到来后统一更新。这模拟了寄存器同时动作的硬件特性。描述组合逻辑reg out_reg; // 在always块里赋值必须声明为reg wire a, b, sel; // 描述一个2选1选择器用always块实现 always (*) begin // (*) 或 (a or b or sel) 表示敏感列表任何输入变化就执行 if (sel 1‘b0) begin out_reg a; // 注意这里用阻塞赋值“” end else begin out_reg b; end end(*)是自动敏感列表编译器会自动找出块内所有输入信号任何信号变化都会触发块执行。写组合逻辑时强烈推荐用这个避免遗漏信号导致仿真错误。在描述组合逻辑的always块中使用阻塞赋值。这保证了赋值顺序执行符合组合逻辑的求值顺序。关键区别总结assign 专用于简单的组合逻辑驱动wire。always (*) 用于复杂的组合逻辑内部用驱动reg。always (posedge clk) 用于时序逻辑寄存器内部用驱动reg。4. 必须掌握的Verilog语法细节4.1 数字的表示方法Verilog里数字的写法很特别格式是位宽进制数值。8‘b1100_0011 // 8位二进制数下划线为了易读编译忽略 16‘hDEAD // 16位十六进制数 32‘d100 // 32位十进制数 1‘b1 // 1位二进制表示逻辑‘1’ 1‘b0 // 1位二进制表示逻辑‘0’两个特殊值x 不定值。仿真时可能为0或1常见于未初始化的寄存器。z 高阻态。像引脚悬空常用于三态门tri-state总线。4‘b10xz // 第二位为不定值第一位为高阻态4.2 运算符大部分运算符和C语言一样但有几个硬件描述中特别常用的位拼接运算符{} 把多个信号拼成一个。wire [3:0] a, b; wire [7:0] c; assign c {a, b}; // c[7:4] a, c[3:0] b wire [7:0] d; assign d {4{2‘b01}}; // 重复拼接d 8‘b01010101缩减运算符 对一个向量的所有位进行位操作得到一个1位结果。wire [3:0] vec 4‘b1111; wire and_all, or_all; assign and_all vec; // 与缩减1 1 1 1 1 assign or_all |vec; // 或缩减1 | 1 | 1 | 1 1 assign xor_all ^vec; // 异或缩减1 ^ 1 ^ 1 ^ 1 0条件运算符?: 非常简洁地实现选择器。assign out (sel) ? a : b; // 等同于 if(sel) outa; else outb;4.3 阻塞()与非阻塞()赋值详解这是新手最容易出错的地方我再强调一下阻塞赋值 像C语言赋值立即执行执行完才进行下一条语句。用于组合逻辑。always (*) begin a b; // 执行完这条a立刻变成b的值 c a; // 这里用的a已经是新的值b的值 end非阻塞赋值同时计算统一更新。块结束时才把所有右边的值赋给左边。用于时序逻辑。always (posedge clk) begin a b; // 记住b的值先不更新a c a; // 记住a的“旧”值不是上一步的b end // 时钟沿过后a更新为bc更新为原来的a。黄金法则在描述组合逻辑的always (*)块中用。在描述时序逻辑的always (posedge clk)块中用。不要在同一个always块中混用两种赋值。5. 实战用Verilog设计一个简单的流水灯理论说再多不如动手写一个。咱们来设计一个经典的流水灯假设有4个LED灯。module flow_led ( input wire clk, // 系统时钟比如50MHz input wire rst_n, // 低电平复位 output reg [3:0] led // 4位输出每位驱动一个LED ); // 参数定义让代码更容易修改 parameter CLK_FREQ 50_000_000; // 时钟频率50MHz parameter BLINK_MS 500; // 流水间隔500ms // 计算需要计数的时钟周期数 parameter CNT_MAX CLK_FREQ / 1000 * BLINK_MS - 1; // 内部寄存器 reg [31:0] counter; // 计时计数器位宽要足够大 reg [3:0] led_state; // 当前LED状态 // 时序逻辑部分计时器 always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 32‘d0; // 复位时计数器清零 end else if (counter CNT_MAX) begin counter 32‘d0; // 计数到最大值归零 end else begin counter counter 32‘d1; // 否则计数器加1 end end // 时序逻辑部分LED状态机 always (posedge clk or negedge rst_n) begin if (!rst_n) begin led_state 4‘b0001; // 复位时第一个LED亮 end else if (counter CNT_MAX) begin // 每500msLED状态循环左移一次 led_state {led_state[2:0], led_state[3]}; end // 其他情况led_state保持不变 end // 组合逻辑部分将状态输出到LED假设LED低电平点亮 always (*) begin led ~led_state; // 取反因为LED是低电平点亮 end endmodule代码解读模块定义 声明了时钟clk、复位rst_n和4位LED输出led。参数化设计 使用parameter定义时钟频率和闪烁间隔。如果想改变流水速度只需修改BLINK_MS非常方便。计时器 第一个always块实现了一个计数器从0数到CNT_MAX对应500ms然后归零。这是一个典型的时序逻辑。状态控制 第二个always块也是一个时序逻辑。当计数器计满时counter CNT_MAX改变led_state的状态实现循环左移。{led_state[2:0], led_state[3]}这个拼接操作实现了将最高位移到最低位的循环效果。输出驱动 第三个always块是组合逻辑将led_state取反后赋值给led。假设我们的开发板是LED低电平点亮所以需要取反。这个例子虽然简单但包含了参数化、计数器、状态机、组合逻辑输出等核心概念。你可以把它放到你的FPGA开发板上试试理解每个信号是如何在硬件中流动的。学习Verilog最好的方法就是多写、多仿真、多看综合出来的电路图RTL图。一开始可能会觉得别扭但当你看到自己写的代码变成实实在在闪烁的LED时那种感觉是非常棒的。记住你是在设计电路不是在写程序从硬件角度思考一切都会变得清晰。