4. Verilog语法基础(上):模块结构、端口定义、assign语句、always块、阻塞与非阻塞赋值

好,咱们今天开始啃Verilog语法。说实话,很多新手一上来就被各种语法细节吓住了。我当年也是,看着一堆beginendassign,心里直犯嘀咕:这跟C语言长得像,但怎么用起来完全不是一回事?

别急。咱们今天不讲那些花里胡哨的,就讲四个最核心的东西:模块长什么样、端口怎么接、assign怎么用、always块怎么写。最后再聊聊那个让无数人翻车的阻塞与非阻塞赋值。

4.1 模块结构:Verilog的基本单元

Verilog里,所有东西都装在模块(module)里。你可以把模块想象成一个芯片:有引脚(端口),有内部电路(逻辑),有功能描述。

一个完整的模块长这样:

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

    // 内部逻辑写在这里

endmodule

嗯,这里要注意:moduleendmodule是成对出现的,就像C语言里的花括号。我见过有人写了一大堆代码,最后忘了写endmodule,编译报错找了半天——这种低级错误,谁还没犯过呢?

4.2 端口定义:输入、输出、双向

端口就是模块跟外界打交道的通道。有三种类型:

端口类型 关键字 说明
输入 input 只能从外部读,内部不能赋值
输出 output 只能从内部驱动,外部不能赋值
双向 inout 既能读又能写,常用于总线

端口还有数据类型:wire(线网)和reg(寄存器)。简单记:

  • wire:用assign赋值,或者直接连模块端口
  • reg:在always块里赋值

我个人习惯:输入端口一律用wire,输出端口如果是在always里赋值就用reg,否则用wire。这样思路清晰,不容易乱。

小技巧:端口声明时可以同时指定方向和类型,比如 output reg [7:0] data。这样写更简洁,我推荐新手这么用。

4.3 assign语句:组合逻辑的直通车

assign是Verilog里最直观的语句。说白了就是:左边等于右边,随时更新。它描述的是组合逻辑,没有时钟,没有边沿,信号一变就立刻反应。

assign led = button ? 1'b1 : 1'b0;

上面这行代码的意思是:如果button按下(高电平),led就亮;否则灭。硬件上就是一个多路选择器。

我在项目中遇到过一个问题:有人用assignreg类型赋值,结果编译报错。记住,assign的左边必须是wire类型,不能是reg

多个assign可以同时存在,它们之间是并行的,跟书写顺序无关。这一点跟软件完全不同,你想想看:硬件电路本来就是同时工作的,哪来的先后顺序?

4.4 always块:时序逻辑的根据地

always块是Verilog里最灵活、也最容易出问题的地方。它的基本格式:

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

敏感列表决定了这个块什么时候被触发。常见的有两种:

  • 组合逻辑always @(*)always @(a or b or c)
  • 时序逻辑always @(posedge clk or negedge rst_n)

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

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这里要注意:always块里可以写if-elsecase等语句,但不能写assign。而且,always块里赋值的变量必须是reg类型。

警告:千万不要在多个always块里对同一个变量赋值!这是硬件设计的大忌。我曾经调试一个项目,发现某个信号莫名其妙地变成X态,查了两天才发现是两个always块都在驱动它。

4.5 阻塞与非阻塞赋值:新手翻车重灾区

好,终于到了这个让无数人头疼的话题。Verilog里有两种赋值方式:

赋值方式 符号 特点
阻塞赋值 = 立即执行,顺序执行
非阻塞赋值 <= 并行执行,块结束时统一更新

说白了:

  • 阻塞赋值(=):就像C语言,写完这句再写下一句
  • 非阻塞赋值(<=):所有赋值同时发生,块结束时才生效

为什么会有这种区别?因为硬件是并行的。你想想看,一个寄存器在时钟上升沿同时采样多个信号,这些信号必须是同一时刻的值,不能有先后。

我给大家一个铁律:

组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。

千万别混!我见过有人把<=用在组合逻辑里,结果仿真波形怎么看怎么不对。还有人在时序逻辑里用=,综合出来的电路多了一堆没用的锁存器。

看个对比:

// 错误示例:时序逻辑用了阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;  // 这里a已经是新值了,不是我们想要的
end

// 正确示例:时序逻辑用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;  // 这里a还是旧值,符合硬件行为
end

第一个例子,因为阻塞赋值是顺序执行的,c拿到的是a的新值,相当于两级寄存器串在一起。第二个例子,c拿到的是a的旧值,两个寄存器是并行的。硬件上,这两种电路完全不同!

我的经验:always块时,先想清楚这是组合逻辑还是时序逻辑。组合逻辑就用=,时序逻辑就用<=。养成习惯后,基本不会出错。

4.6 小结

今天咱们讲了四个基础知识点:

  • 模块结构:module/endmodule是基本框架
  • 端口定义:input/output/inout,wire/reg要分清
  • assign语句:组合逻辑,左边必须是wire
  • always块:时序逻辑的根据地,敏感列表要写对
  • 阻塞与非阻塞:组合用=,时序用<=,这是铁律

这些是Verilog的骨架,后面的所有内容都建立在这之上。下一节咱们会讲运算符、条件语句和case语句,到时候你会发现,有了今天的基础,那些东西学起来就快多了。

嗯,今天就到这儿。记住,写Verilog不是写C语言,脑子里要时刻想着硬件电路。你写的是电路,不是程序。