Verilog 简明教程

Verilog 简明教程


0. 心智模型(先建立,再学语法)

Verilog 代码分两类,物理上必须分开:

  • 设计代码(可综合):描述电路结构,进 FPGA;
  • 测试代码(不可综合):产生输入、观察输出,只活在仿真器里。

判断标准:这句话能不能翻译成真实硬件(门、触发器、连线)?

电路 = 一张依赖图 + 两类边:

  • 组合边(assign / always @(*)):持续传导,输入一变输出立刻跟着变,零延迟;
  • 寄存器节点(always @(posedge clk)):只在时钟沿把输入快照进输出,两个沿之间与输入脱钩——它引入"状态/历史",允许反馈环(环必须被寄存器打破)。

形式化:组合电路 \(y = f(x)\);时序电路 \(y_t = f(x_t, s_{t-1})\),寄存器是状态 \(s\) 的载体。

并行语义:块与块之间并发(一屋子同时运转的电路),块内语句顺序执行。没有主循环、没有调用——实例化是复制一块电路并连线,不是函数调用。

仿真的定位:验证"逻辑对不对"。时序正确性不靠仿真,靠静态时序分析(STA)和上板。仿真里的时间、启动、信号都是理想化抽象;真实电路有延迟、毛刺、亚稳态和混沌上电。


1. 程序骨架

module 模块名(端口1, 端口2, ...);   // 模块 = 带接口的电路块/图纸
    input  wire a;        // 输入(只能是 wire)
    output reg  y;        // 输出(wire 或 reg,见 §2)
    // ... 电路描述 ...
endmodule
  • 端口不写类型默认 wire;建议在文件头加 `default_nettype none 防止隐式 wire 掩盖拼写错误;
  • 实例化:DFF u0 (.d(d[0]), .clk(clk), .q(q[0]));(名称映射,推荐)或按位置 DFF u1 (q[1], clk, d[1]);
  • 实例是物理拷贝:u0、u1 是两份独立硬件,各自占资源,并行工作,无运行时关系。

类比:module ≈ 带端口的元件图纸。区别于 C++ class:实例化不是创建对象,是"拿图纸造零件再焊起来";模块间通信只有连线,没有方法调用;无继承多态,用组合代替。

2. 数据类型:只记两种

类型 是什么 什么时候用
wire 导线,被持续驱动 assign 左边、模块间连线、实例输出
reg 过程块里的变量 always/initial 块内赋值的信号

铁律:assign 配 wire,always 配 reg。

reg 是历史命名错误,不表示触发器:综合出什么看 always 写法。端口分三类:input(只能 wire)、output(wire 或 reg)、inout(只能 wire)。

四值逻辑:0、1、x(未知)、z(高阻/悬空)。仿真初始所有信号为 x。

字面量:4'b1010、8'hFF、8'd255;下划线只助读;位宽不够截高位;负数赋给无符号 reg → 补码全 1。

parameter:例化时替换的常量参数,≈ C++ template<int W> 的值参数化:

module counter #(parameter W = 8)(...);
counter #(.W(32)) u1 (...);   // 例化点覆盖,defparam 禁止

只有值、几乎无类型检查;结构级参数化靠 generate/genvar。

3. 电路的三种写法

3.1 连续赋值(组合逻辑)

assign y = a & b | c;
assign led = &sw;                    // 归约与:全 1 则 1
assign {cout, sum} = a + b + cin;    // 级联赋值

3.2 always 块——两种模式,别混

// 时序逻辑(触发器)
always @(posedge clk)          // 可加 or negedge rst_n
    q <= d;                    // 非阻塞赋值 <=

// 组合逻辑
always @(*) begin              // * = 块内读到的所有信号
    if (sel) y = a;            // 阻塞赋值 =
    else     y = b;
end
时序 组合
敏感表 posedge clk (*)
赋值符 <= =
综合为 触发器 门电路
要求 — if/case 穷尽,否则推断 latch

触发条件 vs 动作:@(...) 里是"什么时候醒"(posedge clk = 名为 clk 的信号 0→1 跳变那一瞬),后面的语句是"醒来干什么"。clk/q/d 全是用户自定义标识符,不是系统内置。

阻塞 vs 非阻塞:= 立即生效(C 语义);<= 块结束时统一更新(模拟同一沿采样的真实触发器)。时序块用 = 或跨块驱动同一信号 = 隐蔽 bug。

异步复位模板:

always @(posedge clk or negedge rst_n)
    if (!rst_n) q <= 0;
    else        q <= d;

3.3 门级原语(了解)

and g1 (w, a, b);        // 输出在前
bufif1 (bus, data, en);  // 三态门:en=1 导通,否则 z

4. 常用语法

// 条件与多路
if (...) ...; else ...;
case (opcode)
    3'b000: y = a + b;
    3'b001, 3'b010: y = a - b;
    default: y = 0;              // 组合逻辑必须写 default
endcase
assign y = sel ? a : b;          // 三目 = 数据选择器

// 位操作
{ a[3:2], b[1:0] }   // 拼接
{4{bit}}             // 复制
^data                // 归约异或(奇偶校验)

// 存储器(大数组慎综合)
reg [7:0] mem [0:255];

// 函数(组合计算复用,0 延迟,纯函数)与任务(可带时序,testbench 用)
function [7:0] f(input [7:0] a, b); f = a + b; endfunction

for 可综合(静态展开);while/forever/repeat 仅 testbench。

5. Testbench 模板(仿真专用,随便写)

`timescale 1ns/1ps
module tb();
    reg  [2:0] sw = 0;       // 激励 → reg;初始化避免 x
    wire led;                // 观察 → wire
    integer i;

    led uut(.sw(sw), .led(led));

    always #50 clk = ~clk;   // 时钟发生器(clk 需 reg)

    initial begin
        for (i = 0; i < 8; i = i + 1) begin
            #100 sw = i;
        end
        $display("done"); $finish;
    end
endmodule

系统任务(全部不可综合):$display、$write、$monitor(信号一变就打印)、$finish、$stop、$time、$random、$readmemh。

6. 可综合 / 不可综合速查

可综合:assign、always @(posedge clk)、always @(*)、if/case/for(静态)、parameter、实例化、原语门。

不可综合(只进 testbench):# 延迟、initial(FPGA 初值部分工具支持但别依赖 ASIC)、$ 系列、fork/join、wait、forever/while/repeat、===/!==(case 等)、force、命名事件、trireg/wor/wand。

灰色危险区:组合块不穷尽(出 latch)、多 always 块驱动同一 reg(竞争)、敏感表漏信号。

7. 完整例子:带复位的参数化计数器

module counter #(parameter W = 4)(
    input  wire         clk,
    input  wire         rst_n,
    output reg  [W-1:0] count
);
    always @(posedge clk)
        if (!rst_n) count <= 0;
        else        count <= count + 1'b1;
endmodule

8. 仿真 vs 现实:时序问题的三层处理

  1. RTL 仿真(功能层):门延迟 = 0,看不到毛刺、亚稳态、jitter。assign #3 y = a & b; 这类行为延迟只影响仿真、不可综合——别在 RTL 里塞 #;
  2. STA(时序层,主战场):实现后基于物理延迟查表,穷举所有路径检查 setup/hold 裕量。XDC 里 create_clock -period 10 [get_ports clk] 是喂给它的;WNS < 0 就改设计(流水、降组合层级)或降频;
  3. 时序仿真(门级 + SDF 反标):实现后把真实延迟反标进仿真,能看到毛刺与竞争。辅助手段,慢,主力是 STA。

启动差异:仿真 t=0 一切为 x、initial 瞬时执行;现实有电源 ramp-up、晶振起振、比特流加载。⇒ 没有复位的设计上板必炸,复位是把仿真隐含假设显式化。

9. 常见错误清单

现象 原因
Illegal assignment always 里给 wire 赋值(或反之)
信号值是 x 拼写错,隐式 wire 生效
组合逻辑出 latch if/case 没写全
仿真对、上板错 #、initial、初值等仿真专用构造混入 RTL
功能时好时坏 时序块用 = 或多块驱动同一信号
例化端口错 位置映射顺序错 → 换名称映射
综合报 let 等 not found 实例化名字笔误,或文件没加入 Design Sources

10. 与 Vivado 流程的对应

设计代码 → Synthesis → Implementation → XDC 约束(PACKAGE_PIN + IOSTANDARD + create_clock)→ Bitstream → 上板。仿真走 testbench,两套代码分目录。注意:烧写前 DRC 强制要求 IOSTANDARD;纯组合逻辑无时钟约束属正常。


附:一句话速记

  • assign 配 wire,always 配 reg;reg 是变量不是寄存器;
  • @ 括号里 = 什么时候醒,后面 = 醒来干什么;
  • 时序用 <= 配 posedge,组合用 = 配 (*) 且写全;
  • module 是图纸,实例化是造零件焊起来;
  • 仿真管逻辑,STA 管时序,上板管物理;
  • parameter ≈ 模板值参数,只有值、无类型检查。
posted @ 2026-09-28 09:35  jeefy  阅读(7)  评论(0)    收藏  举报