3. Verilog基础语法(上):模块结构、数据类型、assign语句、always块、阻塞与非阻塞赋值

各位同学,咱们今天开始啃Verilog语法这块硬骨头。说实话,很多新手一上来就被语法细节搞晕了。我当年也一样,对着书看了三天,写出来的代码还是跑不通。后来我发现,学语法不能死记硬背,得理解它背后的硬件逻辑。

你想想看,Verilog不是软件语言,它描述的是硬件电路。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。带着这个思路去学,很多问题就迎刃而解了。

3.1 模块结构——Verilog的基本单元

模块(module)是Verilog中最基本的描述单位。说白了,一个模块就是一个硬件功能块。你可以把它想象成一个芯片,有输入引脚、输出引脚,内部有逻辑电路。

一个完整的模块结构长这样:

module counter (
    input  wire       clk,    // 时钟信号
    input  wire       rst_n,  // 复位信号,低电平有效
    output reg  [3:0] count   // 4位计数器输出
);

    // 内部逻辑写在这里

endmodule

这里有几个关键点:

  • module/endmodule:模块的起始和结束,成对出现
  • 端口列表:括号里定义模块的输入输出
  • 端口方向:input、output、inout三种
  • 数据类型:wire或reg,后面会细讲
我的习惯:写模块时,我会把端口声明和数据类型写在同一行。这样代码更紧凑,一眼就能看出每个信号的方向和类型。很多公司的代码规范也要求这样写。

模块内部可以包含很多东西:assign语句、always块、实例化其他模块等等。这些我们后面会逐一展开。

3.2 数据类型——wire和reg的区别

这是新手最容易搞混的地方。我刚开始学的时候,也纠结过「什么时候用wire,什么时候用reg」这个问题。

其实很简单,记住一句话:wire是连线,reg是变量

特性 wire reg
本质 物理连线 存储单元
赋值方式 assign连续赋值 always块内赋值
默认值 高阻态z 不定态x
综合结果 组合逻辑 组合逻辑或时序逻辑

举个例子:

wire a, b, c;
assign c = a & b;   // c是a和b相与的结果,用wire

reg [7:0] data_reg;
always @(posedge clk) begin
    data_reg <= data_in;  // data_reg在时钟上升沿更新,用reg
end
注意:reg并不一定综合成寄存器!在always块中描述组合逻辑时,reg综合出来只是连线。我曾经见过一个同事,看到reg就以为是触发器,结果综合出来的电路面积比预期大了好几倍。

3.3 assign语句——连续赋值

assign语句用来描述组合逻辑。它的特点是:只要输入变了,输出立刻跟着变。没有时钟,没有延迟,瞬间完成。

基本语法:

assign 信号名 = 表达式;

实际例子:

module adder (
    input  wire [3:0] a, b,
    output wire [4:0] sum
);
    assign sum = a + b;  // 加法器,组合逻辑
endmodule

assign的右边可以是任何合法的表达式:算术运算、逻辑运算、位运算、条件运算符等等。

我个人特别喜欢用条件运算符(?:)写简单的多路选择器:

assign max = (a > b) ? a : b;  // 取两个数中的较大值

这比写if-else简洁多了,而且综合出来的电路也更高效。

3.4 always块——过程赋值

always块是Verilog中最灵活、也最容易出错的语法结构。它既可以描述组合逻辑,也可以描述时序逻辑。

基本格式:

always @(敏感列表) begin
    // 过程赋值语句
end

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

  • 电平敏感:@(*) 或 @(a or b or c) —— 用于组合逻辑
  • 边沿敏感:@(posedge clk) 或 @(negedge rst_n) —— 用于时序逻辑

看两个例子:

// 组合逻辑:电平敏感
always @(*) begin
    if (sel)
        out = a;
    else
        out = b;
end

// 时序逻辑:边沿敏感
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 0;
    else
        q <= d;
end
关键点:组合逻辑的敏感列表要包含所有输入信号。用@(*)可以自动推导,省心又安全。我建议你养成用@(*)的习惯,避免漏掉信号导致仿真和综合结果不一致。

3.5 阻塞赋值与非阻塞赋值

这是Verilog里最坑的地方,没有之一。我见过太多工程师在这里翻车了,包括我自己。

先看定义:

  • 阻塞赋值(=):顺序执行,前面的赋值会阻塞后面的赋值
  • 非阻塞赋值(<=):并行执行,所有赋值同时完成

听起来抽象?我们看个具体例子:

// 阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;  // c得到的是a更新后的值,即b
end

// 非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;  // c得到的是a更新前的值,即a的旧值
end

看到区别了吗?阻塞赋值是「先算a,再算c」,非阻塞赋值是「同时算a和c,但用的是旧值」。

那实际项目中怎么用?我总结了一个黄金法则:

场景 推荐赋值方式 原因
时序逻辑(always @(posedge clk)) 非阻塞赋值 <= 避免竞争冒险,仿真行为正确
组合逻辑(always @(*)) 阻塞赋值 = 符合组合逻辑的传播特性
assign语句 只能用 = 语法规定,没得选
血的教训:我曾经在一个项目中,把时序逻辑里的非阻塞赋值写成了阻塞赋值。仿真时一切正常,但板子跑起来就是不对。查了三天,最后发现是赋值方式用错了。从那以后,我写always块之前都会先问自己一句:「这是组合逻辑还是时序逻辑?」

3.6 知识体系总览

说了这么多,我们来梳理一下本章的核心脉络。下面这张图可以帮助你建立整体认知:

Verilog基础语法(上)知识体系 模块结构 (module) 数据类型 assign语句 always块 阻塞/非阻塞赋值 wire(连线) vs reg(变量) 组合逻辑:输入变输出即变 电平敏感 vs 边沿敏感 = 顺序执行 vs <= 并行执行 核心:理解硬件电路,而非软件逻辑

这张图把本章的知识点串起来了。模块是骨架,数据类型是血肉,assign和always是两种描述逻辑的方式,而赋值方式则是决定电路行为的关键。

好了,这一章的内容就到这里。记住,学Verilog不是背语法,而是理解硬件。下次你写代码的时候,先想想:「这段代码综合出来是什么电路?」想明白了,语法自然就通了。

我的建议:初学者可以先从简单的组合逻辑开始练手,比如加法器、多路选择器。等熟悉了assign和always的用法,再尝试写时序逻辑。一步一个脚印,别着急。

公众号:蓝海资料掘金营,微信deep3321