3. 硬件描述语言基础(Verilog):模块结构、数据类型、运算符、assign语句、always块

好,咱们进入正题。Verilog 这玩意儿,说白了就是用来描述数字电路的。你把它想象成画电路图的“文字版”就行。我刚开始学的时候,总觉得它跟 C 语言很像,结果写出来的东西综合出来一堆莫名其妙的 latch,那叫一个惨。所以,咱们得先把它当硬件来理解,别当软件写。

3.1 模块结构:CPLD 设计的“积木块”

一个 Verilog 设计,最核心的就是 module。你可以把它看作一个黑盒子,有输入、有输出,里面装着你要实现的逻辑。

基本结构长这样:

module 模块名 (
    端口1,
    端口2,
    ...
);
    // 端口声明
    input  端口1;
    output 端口2;
    inout  双向端口;

    // 内部信号声明
    wire  内部连线;
    reg   内部寄存器;

    // 逻辑功能描述
    // assign 语句、always 块、门级实例化等

endmodule

我个人习惯,模块名用下划线分隔的小写字母,比如 spi_controller。端口列表里,每个端口一行,加上注释,这样后期维护起来,你看着也舒服。

小提示: 每个 .v 文件只放一个 module,模块名跟文件名保持一致。这是工程规范,别问为什么,问就是好找。

3.2 数据类型:wire 和 reg,别搞混了

这是新手最容易踩坑的地方。我见过太多人把 wirereg 用错,综合出来一堆意想不到的玩意儿。

  • wire:代表一根物理连线。它只能被 assign 语句或者模块实例的端口驱动。说白了,它就是个“被动”的信号,你不能在 always 块里给它赋值。
  • reg:代表一个寄存器或者一个变量。它可以在 always 块里被赋值。注意,它不一定会被综合成寄存器,也可能是组合逻辑。

举个例子:

wire   a, b;      // 两根连线
wire   sum;       // 一根连线,用来表示 a + b 的结果
reg    clk;       // 一个寄存器,用来模拟时钟
reg [3:0] counter; // 一个 4 位的寄存器,用来计数

// assign 语句驱动 wire
assign sum = a + b;

// always 块驱动 reg
always @(posedge clk) begin
    counter <= counter + 1'b1;
end

避坑指南: 我曾经在做一个 SPI 接口时,把一个 wire 类型的信号放在了 always 块的赋值左边,结果综合器报了一堆错。记住,wire 只能被 assign 驱动,reg 只能在 alwaysinitial 里赋值。

3.3 运算符:跟 C 语言差不多,但有坑

Verilog 的运算符跟 C 语言很像,但有几个地方要特别注意。

类别 运算符 说明
算术 + - * / % 加减乘除取模。注意,除法综合出来很费资源,尽量少用。
位运算 & | ~ ^ 按位与、或、非、异或。这个在 CPLD 里用得非常多。
逻辑运算 && || ! 逻辑与、或、非。结果只有 0 或 1。
关系运算 > < >= <= == != 比较大小、相等、不等。结果也是 0 或 1。
移位 << >> 左移、右移。左移相当于乘 2,右移相当于除 2。
拼接 { } 把多个信号拼成一个宽的信号。比如 {a, b}
条件 ? : 三目运算符,相当于 if-else 的简写。

注意: 位运算和逻辑运算的区别。比如 a & b 是把 a 和 b 的每一位进行与操作,结果是一个多位宽的信号。而 a && b 是把 a 和 b 当成布尔值,结果只有 1 位。我刚开始就经常搞混,写出来的逻辑完全不对。

3.4 assign 语句:组合逻辑的“直通车”

assign 语句用来描述组合逻辑。它相当于把右边的表达式“硬连线”到左边的 wire 上。只要右边表达式的值变了,左边立刻跟着变。

// 一个简单的 2 选 1 多路选择器
module mux2to1 (
    input  a,
    input  b,
    input  sel,
    output y
);
    assign y = sel ? b : a;
endmodule

你看,这个 assign 语句,用三目运算符实现了一个多路选择器。简洁明了。我个人习惯,所有简单的组合逻辑都用 assign 搞定,代码看起来清爽。

警告: assign 语句的左边必须是 wire 类型,不能是 reg。右边可以是 wire 或 reg,但 reg 的值必须是当前时刻的,不能是时序的。

3.5 always 块:时序逻辑的“心脏”

always 块是 Verilog 里最强大的结构,也是最容易出问题的地方。它用来描述时序逻辑(比如触发器、计数器)和复杂的组合逻辑。

基本语法:

always @(敏感列表) begin
    // 逻辑描述
end

敏感列表 决定了 always 块什么时候执行。常见的有:

  • always @(posedge clk):在时钟的上升沿执行,用来描述时序逻辑。
  • always @(negedge clk):在时钟的下降沿执行。
  • always @(a or b or sel):当 a、b、sel 中任何一个变化时执行,用来描述组合逻辑。

举个例子,一个带异步复位的 D 触发器:

module dff (
    input  clk,
    input  rst_n,
    input  d,
    output reg q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 1'b0;
        else
            q <= d;
    end
endmodule

这里有几个关键点:

  • 敏感列表里同时有 posedge clknegedge rst_n,表示时钟上升沿或复位下降沿都会触发。
  • 复位是低电平有效(rst_n),所以用 negedge
  • 赋值用的是 <=(非阻塞赋值),这是时序逻辑的标准写法。

避坑指南: 我曾经在写一个状态机时,把组合逻辑和时序逻辑混在同一个 always 块里,结果综合出来一堆奇怪的 latch。记住,描述时序逻辑用 posedge clk,描述组合逻辑用 @(*) 或列出所有输入信号。别混着来。

3.6 知识体系结构图

下面这张图,把本章的核心知识点串起来了。你一看就明白,Verilog 的模块结构、数据类型、运算符、assign 和 always 块之间是什么关系。

Verilog 模块 (module) 端口声明 数据类型 运算符 逻辑功能描述 input output inout wire reg 算术 位运算 逻辑 关系 移位 拼接 assign always 图:Verilog 模块知识体系结构 每个模块由端口、数据类型、运算符和逻辑描述四部分组成

嗯,这张图把本章的内容都串起来了。你写代码的时候,脑子里要有这个结构图,就知道每个部分该放什么。

核心要点:
  • module 是基本单元,端口是它的“接口”。
  • wire 用于连线,reg 用于寄存器或变量。
  • 运算符跟 C 语言类似,但注意位运算和逻辑运算的区别。
  • assign 描述组合逻辑,always 描述时序逻辑或复杂组合逻辑。
  • 时序逻辑用非阻塞赋值 <=,组合逻辑用阻塞赋值 =

好了,这一章的内容就到这儿。记住,Verilog 是描述硬件的语言,不是写软件的。多写、多仿真、多看看综合出来的 RTL 图,慢慢就有感觉了。