3. Verilog基础语法(一):模块结构、端口定义

各位同学,咱们今天正式开始啃Verilog这块硬骨头。说实话,很多初学者一上来就被各种语法吓住了,其实没那么玄乎。你想想看,Verilog说白了就是描述硬件的一种语言,跟C语言最大的区别是——它是并行执行的。

先看模块结构。一个模块就像一块芯片的“外壳”,里面装着你想要的功能。

module led_controller(
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    output reg  [3:0] led       // 4个LED
);

// 内部逻辑写在这里

endmodule

我个人习惯把端口定义写在模块名后面,这样一眼就能看到输入输出。注意看,inputoutput后面跟的是wirereg。这里有个坑——input端口必须是wire类型,不能是reg。为什么?因为输入信号是从外部进来的,你没法在模块内部给它赋值。output可以是wire也可以是reg,看你怎么用。

端口类型速查表

端口方向允许的类型说明
inputwire只能被外部驱动
outputwire / regwire用于组合逻辑,reg用于时序逻辑
inoutwire双向端口,必须用wire

assign语句、wire与reg类型

好,接下来聊聊assign语句。assign就是连续赋值,说白了就是“只要右边变了,左边立刻跟着变”。

wire a, b, c;
assign c = a & b;   // c是a和b的与,实时更新

这里c必须是wire类型。为什么?因为assign是组合逻辑,没有存储能力。reg就不一样了,它可以在always块里被赋值,能记住状态。

我记得刚入行那会儿,有个同事把reg当wire用,在assign右边放了个reg变量,结果综合出来一堆奇怪的锁存器。嗯,这里要注意——assign的左边只能是wire,always块里赋值的变量必须是reg。这是硬性规定,别搞反了。

我的经验:写组合逻辑用wire+assign,写时序逻辑用reg+always。这样分工明确,代码可读性高。我见过有人把所有信号都定义成reg,然后在always块里用组合逻辑写,虽然也能综合,但看着别扭。

always块与敏感列表

always块是Verilog的灵魂。它的语法长这样:

// 组合逻辑:电平敏感
always @(*) begin
    // 组合逻辑代码
end

// 时序逻辑:边沿敏感
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        // 复位
    else
        // 正常逻辑
end

敏感列表就是always块“关注”的信号列表。组合逻辑用@(*),意思是“所有输入信号变化都触发”。时序逻辑用@(posedge clk),只在时钟上升沿触发。

我曾经在项目里犯过一个低级错误——组合逻辑的敏感列表没写全。比如:

always @(a or b) begin  // 只敏感a和b
    c = a & b & d;       // 但这里用了d
end

结果仿真时d变了c没变,查了半天才发现敏感列表漏了d。后来我学乖了,组合逻辑一律用@(*),省心。

避坑指南:我曾经见过一个设计,敏感列表里写了20多个信号,漏了一个,导致综合前后仿真不一致。从那以后,组合逻辑我全部用@(*),时序逻辑只敏感时钟和异步复位。这是最安全的写法。

阻塞赋值与非阻塞赋值

这个知识点是Verilog初学者最容易翻车的地方。我把它叫做“硬件工程师的驾照考试”——过不了这关,后面寸步难行。

阻塞赋值用=,非阻塞赋值用<=。区别在哪?

  • 阻塞赋值:顺序执行,前面的赋值会阻塞后面的。适合组合逻辑。
  • 非阻塞赋值:并行执行,所有赋值在同一个时间点同时生效。适合时序逻辑。

看个例子就明白了:

// 阻塞赋值 - 组合逻辑
always @(*) begin
    b = a;
    c = b;   // c等于a,因为b先被赋值了
end

// 非阻塞赋值 - 时序逻辑
always @(posedge clk) begin
    b <= a;
    c <= b;   // c等于b的旧值,不是a
end

你想想看,如果时序逻辑里用了阻塞赋值,会发生什么?

// 错误示范!时序逻辑用阻塞赋值
always @(posedge clk) begin
    b = a;
    c = b;   // 综合出来可能变成两个寄存器串联
end

综合工具会把它解释成两个寄存器串联,但仿真时c和b同时更新,导致仿真和综合结果不一致。这就是所谓的“仿真与综合不匹配”。

黄金法则

  • 组合逻辑用阻塞赋值 =
  • 时序逻辑用非阻塞赋值 <=
  • 同一个always块里不要混用两种赋值

我记得有一次评审代码,发现一个同事在always块里同时用了=<=,综合出来一堆奇怪的触发器。我问他为什么这么写,他说“我觉得这样写更直观”。嗯,硬件设计不能靠“觉得”,得靠规则。

我的习惯:写代码前先想清楚——这个always块是组合逻辑还是时序逻辑?想清楚了再动笔。组合逻辑用@(*)+=,时序逻辑用@(posedge clk)+<=。这样写出来的代码,综合结果和仿真结果高度一致,省去很多调试时间。

本章知识体系

下面这张图总结了本章的核心知识点,我画成了流程图,方便你对照复习:

Verilog基础语法(一)知识体系 模块结构 端口定义 数据类型 module/endmodule input/output/inout wire / reg assign语句 always块 赋值方式 连续赋值,左边必须是wire 敏感列表:@(*) 或 @(posedge clk) 阻塞= / 非阻塞<= 组合逻辑用assign 时序逻辑用always 组合= 时序<= 核心原则:组合逻辑用阻塞,时序逻辑用非阻塞

好了,这一章的内容就这些。模块结构、端口定义、assign和always、wire和reg、阻塞和非阻塞——这几个概念是Verilog的基石。你先把它们吃透,后面学起来就顺了。

记住我反复强调的那句话:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值。这是无数硬件工程师用血泪换来的经验,别自己去踩一遍坑才信。