Verilog基础回顾:从模块到上板

各位同学,咱们今天聊聊Verilog基础。说实话,很多初学者觉得Verilog就是写代码,跟C语言差不多。嗯,这个想法很危险。我见过太多人把Verilog写成C,结果综合出来的电路完全不是那么回事。

咱们先看一张图,把今天要讲的内容串起来:

Verilog基础回顾 模块结构 module/endmodule 数据类型 wire/reg/parameter 运算符 位/算术/逻辑/拼接 组合逻辑 assign/always@(*) 时序逻辑 always@(posedge clk) Testbench initial/always/仿真 最终目标:综合出正确的硬件电路

一、模块结构——Verilog的基本单元

Verilog里最核心的概念就是模块。说白了,一个模块就是一个硬件电路的黑盒子。你给它输入,它给你输出。

module counter (
    input  wire       clk,
    input  wire       rst_n,
    output reg  [3:0] count
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 4'd0;
        else
            count <= count + 1'b1;
    end
endmodule

我个人习惯把端口声明和内部逻辑分开写。这样代码结构清晰,别人一看就懂。模块名我建议用有意义的英文,别用a、b、c这种。

小技巧:端口方向有input、output、inout三种。inout我建议初学者少用,双向总线处理不好容易出问题。我在项目中吃过这个亏。

二、数据类型——wire和reg的区别

很多新手搞不清wire和reg。我换个说法:

  • wire:相当于一根导线。它不存储值,只是传递信号。assign语句赋值给wire。
  • reg:相当于一个寄存器。它能保存值。always块里赋值给reg。

等等,这里有个坑。reg不一定会被综合成寄存器!比如下面这个例子:

reg [3:0] result;
always @(*) begin
    result = a + b;
end

这个result虽然是reg类型,但综合出来只是组合逻辑,没有寄存器。为什么会这样?因为always @(*)是组合逻辑描述,没有时钟沿触发。

注意:reg类型不代表一定是时序逻辑。综合工具看的是你怎么用,不是看你怎么声明。我曾经见过有人把所有信号都声明成reg,结果综合出来的电路面积大得吓人。

另外还有parameter,这个相当于C语言里的宏定义:

parameter DATA_WIDTH = 8;
reg [DATA_WIDTH-1:0] data_bus;

用parameter的好处是,改一个地方,所有地方都跟着变。我建议把经常用到的常量都定义成parameter。

三、运算符——别搞混了位宽

Verilog的运算符跟C语言很像,但有些地方完全不同。我挑几个容易出问题的说说:

运算符类型 符号 说明 常见错误
算术 + - * / % 加减乘除取模 乘法会消耗大量LUT
位运算 & | ^ ~ 按位与或异或非 位宽不匹配时自动截断
逻辑运算 && || ! 逻辑与或非,结果1bit 跟位运算搞混
拼接 { } 把多个信号拼在一起 拼接顺序搞反
移位 << >> 左移右移 移位后位宽不变

举个例子,你想想看:

wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b1100;
wire [3:0] c = a & b;  // 结果是4'b1000
wire       d = a && b; // 结果是1'b1,因为两个都不为0

嗯,这里要注意。&和&&完全是两码事。我见过有人写if (a & b)想判断两个信号都不为0,结果综合出来的电路完全不对。

四、组合逻辑与时序逻辑

这两个概念是FPGA设计的基石。我简单说说:

组合逻辑:输出只取决于当前输入。没有记忆功能。用assign或者always @(*)描述。

// 组合逻辑:二选一多路器
assign out = sel ? b : a;

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。有记忆功能。用always @(posedge clk)描述。

// 时序逻辑:D触发器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end
核心区别:组合逻辑没有时钟,时序逻辑有时钟。组合逻辑用"="阻塞赋值,时序逻辑用"<="非阻塞赋值。这个规则一定要记住,否则仿真和综合结果会不一致。

我在项目中遇到过一个问题:有人把组合逻辑里的赋值写成了"<=",结果仿真时看起来没问题,但综合出来的电路多了一堆没用的锁存器。排查了两天才找到原因。

五、Testbench编写基础

写Testbench说白了就是模拟输入信号,看输出对不对。我一般这么写:

`timescale 1ns / 1ps

module tb_counter;
    reg        clk;
    reg        rst_n;
    wire [3:0] count;
    
    // 实例化被测试模块
    counter uut (
        .clk  (clk),
        .rst_n(rst_n),
        .count(count)
    );
    
    // 生成时钟
    initial begin
        clk = 0;
        forever #5 clk = ~clk;  // 周期10ns
    end
    
    // 生成复位和激励
    initial begin
        rst_n = 0;
        #20;
        rst_n = 1;
        #200;
        $finish;
    end
    
    // 显示结果
    initial begin
        $monitor("time=%0t, count=%d", $time, count);
    end
endmodule

写Testbench有几个要点:

  • 用`timescale指定时间单位
  • 用initial块生成时钟和复位
  • 用forever生成周期性信号
  • 用$monitor或$display查看结果
  • 用$finish结束仿真
我的习惯:Testbench里尽量用阻塞赋值"=",因为它是仿真用的,不是综合用的。另外,我建议每个模块都写一个Testbench,哪怕很简单。仿真能发现80%以上的问题。

好了,Verilog基础就回顾到这里。这些内容看起来简单,但都是FPGA设计的根基。你想想看,如果连wire和reg都分不清,连阻塞和非阻塞都搞混,那后面的时序约束、状态机、接口设计根本没法做。

下一章咱们开始真正进入Vivado,从创建工程开始。到时候我会手把手带你们走一遍流程。

本章小结:
  • 模块是Verilog的基本单元,用module/endmodule定义
  • wire是连线,reg是变量,但reg不一定是寄存器
  • 运算符注意位宽和类型,别把位运算和逻辑运算搞混
  • 组合逻辑用"=",时序逻辑用"<="
  • Testbench是仿真的基础,每个模块都要写
可可精品资料库