3. Verilog基础(一):模块与端口、assign语句、常用数据类型、always块基础

各位同学,欢迎来到Verilog基础的第一讲。说实话,每次讲到这一章,我都挺感慨的。记得我刚入行那会儿,对着Verilog代码一头雾水,总觉得这玩意儿跟C语言长得差不多,但用起来完全不是那么回事。今天咱们就把这些最基础、最核心的概念掰开揉碎了讲清楚。

3.1 模块与端口——Verilog的“黑盒子”

Verilog里最基本的设计单元就是模块(module)。你可以把它想象成一个黑盒子。这个黑盒子有输入、有输出,内部实现了特定的逻辑功能。

模块的定义格式很简单,我一般这样写:

module 模块名 (
    端口列表
);
    // 内部逻辑
endmodule

端口就是黑盒子的引脚。端口有三种方向:

  • input:输入端口,数据从外部流入模块内部。
  • output:输出端口,数据从模块内部流出到外部。
  • inout:双向端口,既能输入也能输出。这个用得少,初学者先了解就行。

举个最简单的例子,一个与门模块:

module and_gate (
    input  wire a,      // 输入a
    input  wire b,      // 输入b
    output wire y       // 输出y
);
    assign y = a & b;   // 逻辑与
endmodule

嗯,这里要注意,端口声明时我习惯把wire也写出来。虽然不写默认也是wire类型,但写清楚能让代码更易读。我在项目中吃过这个亏——有一次代码里端口类型没写全,综合时报了一堆莫名其妙的警告,排查了半天才发现是端口类型隐式声明的问题。

3.2 assign语句——连续赋值

assign是Verilog里最常用的语句之一。它的作用是把一个表达式的结果连续地赋值给一个信号。

什么叫“连续赋值”?说白了,就是只要等号右边的表达式发生变化,左边的信号就会立刻更新。这跟软件里的赋值完全不一样——软件是顺序执行的,而硬件是并行的。

看个例子:

module adder (
    input  wire [3:0] a,
    input  wire [3:0] b,
    output wire [4:0] sum
);
    assign sum = a + b;
endmodule

这个加法器,只要a或b变了,sum就会跟着变。没有任何延迟,没有时钟控制,就是纯组合逻辑。

小技巧:assign语句只能对wire类型赋值,不能对reg类型赋值。这个我刚开始也搞混过,后来养成了习惯——组合逻辑用wire,时序逻辑用reg。

3.3 常用数据类型:wire和reg

Verilog里最常用的两种数据类型就是wire和reg。很多初学者会纠结它们到底有什么区别,我直接说结论:

特性 wire reg
默认值 高阻态(z) 未知态(x)
赋值方式 assign语句 always块内赋值
物理含义 连线 寄存器或连线
存储能力 无 有(在always块中)

你想想看,wire就像一根导线,信号从一端传到另一端,没有存储能力。而reg呢,在always块里可以保持上一次的值,所以有存储能力。

但是!这里有个坑——reg不一定是寄存器。在组合逻辑的always块里,reg只是语法上的要求,综合出来还是连线。我曾经在面试时问过这个问题,十个有八个答错。

// 这个reg综合出来是连线
reg y;
always @(*) begin
    y = a & b;
end

// 这个reg综合出来是寄存器
reg q;
always @(posedge clk) begin
    q <= d;
end
避坑指南:我曾经在代码里把组合逻辑的always块漏写了敏感列表,结果仿真和综合结果不一致。后来我养成了习惯——组合逻辑always块用@(*),时序逻辑always块用@(posedge clk)。

3.4 always块基础——时序逻辑的起点

always块是Verilog里最强大的结构之一。它描述了一组语句在特定条件下反复执行的行为。

always块的基本格式:

always @(敏感列表) begin
    // 顺序执行的语句
end

敏感列表决定了always块什么时候被触发。常见的触发方式有:

  • 电平触发:always @(a or b),a或b变化时触发
  • 边沿触发:always @(posedge clk),时钟上升沿触发
  • 组合逻辑:always @(*),所有输入变化时触发

来看一个D触发器的例子:

module dff (
    input  wire clk,
    input  wire rst_n,
    input  wire d,
    output reg  q
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            q <= 1'b0;      // 异步复位
        end else begin
            q <= d;         // 时钟上升沿采样
        end
    end
endmodule

这里要注意几个关键点:

  1. 非阻塞赋值(<=):时序逻辑里一定要用非阻塞赋值。这是硬件设计的铁律,没得商量。
  2. 异步复位:复位信号rst_n在敏感列表里,所以是异步复位。复位有效时,q立刻变0。
  3. 敏感列表顺序:敏感列表里信号的顺序不影响综合结果,但会影响仿真行为。我一般把时钟写在前面,复位写在后面。
核心要点:always块里的语句是顺序执行的,但综合出来的硬件是并行的。这个“顺序执行”只是仿真时的行为,实际硬件里所有赋值是同时发生的。理解这一点,你就算入门了。

3.5 知识体系总览

为了让大家更直观地理解本章的知识结构,我画了一张图:

Verilog基础(一)知识体系 模块与端口 module/endmodule input/output/inout assign语句 连续赋值 组合逻辑 数据类型 wire reg always块 电平触发 @(*) 边沿触发 @(posedge) 组合逻辑 时序逻辑 非阻塞赋值 <= 敏感列表 核心:模块是骨架,assign和always是血肉,wire和reg是细胞

这张图把本章的知识点串起来了。模块是顶层框架,assign和always块是内部逻辑的实现方式,wire和reg是承载数据的载体。三者缺一不可。

好了,这一章的内容就到这里。记住,Verilog不是软件语言,它是硬件描述语言。写代码的时候,脑子里要想的是电路,而不是程序。这个思维转变过来了,后面的学习就会顺畅很多。

可可精品资料库