3、Verilog基础:模块与端口、数据类型、连续赋值与过程赋值、阻塞与非阻塞赋值

好,咱们今天聊聊Verilog最核心的几个概念。说实话,很多新手一上来就被这些概念绕晕了。我当年也是,看了三天书,写出来的代码还是跑不通。后来才发现,不是语法难,是没理解背后的设计思想。

这一节,咱们就把这些基础打扎实。你想想看,盖房子得先懂砖头和水泥,写Verilog也一样。

3.1 模块与端口:芯片的“黑盒子”

模块(module)是Verilog的基本单元。说白了,它就是一个黑盒子。你给它输入,它给你输出。至于里面怎么算的,那是你的事。

我习惯把模块想象成一个芯片引脚图。端口就是芯片的引脚,用来和外界通信。

模块的基本结构:

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

    // 内部逻辑写在这里

endmodule

这里要注意几点:

  • 端口方向:input、output、inout。inout双向端口用得少,新手先别碰。
  • 端口类型:wire和reg。后面会细讲。
  • 位宽:[3:0]表示4位宽。我见过有人写成[0:3],也能用,但行业惯例是从高位到低位。

我的习惯:端口声明时,我会把input和output分开写,每个端口一行。这样代码可读性高,review时一眼就能看清接口。

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

Verilog里最常用的两种数据类型就是wire和reg。很多初学者会问:什么时候用wire,什么时候用reg?

我直接给你一个简单粗暴的答案:

  • wire:用连续赋值(assign)驱动,或者连接模块端口。
  • reg:在always块里赋值。

举个例子:

wire a, b, c;
assign c = a & b;   // 连续赋值,c必须是wire

reg [7:0] data;
always @(posedge clk) begin
    data <= data + 1;  // 过程赋值,data必须是reg
end

嗯,这里要注意:reg不一定是寄存器。它只是表示一个变量,可以被过程赋值。在组合逻辑的always块里,reg综合出来就是连线。我刚开始做项目时,以为reg一定是触发器,结果综合出来的面积大得吓人……

避坑指南:我曾经在项目中犯过一个低级错误——把组合逻辑的输出声明成wire,然后在always块里赋值。编译直接报错。记住:always块里赋值的变量,必须是reg类型。

3.3 连续赋值 vs 过程赋值

这两个概念,我当年花了整整一周才彻底搞明白。

连续赋值(assign)

  • 用assign关键字
  • 只能对wire类型赋值
  • 赋值是“持续的”——只要右边变了,左边立刻更新
  • 用于描述组合逻辑

过程赋值(always块内赋值)

  • 在always块里进行
  • 可以对reg类型赋值
  • 赋值是“有条件的”——只在特定事件发生时执行
  • 可以描述组合逻辑,也可以描述时序逻辑

看个对比:

// 连续赋值:组合逻辑
wire sum;
assign sum = a + b;

// 过程赋值:组合逻辑(always @(*))
reg sum_reg;
always @(*) begin
    sum_reg = a + b;
end

// 过程赋值:时序逻辑(always @(posedge clk))
reg sum_ff;
always @(posedge clk) begin
    sum_ff <= a + b;
end

你发现没有?同样的加法,三种写法都能实现。但综合出来的电路完全不同。第一个是纯组合逻辑,第二个也是组合逻辑(但用了reg类型),第三个是带触发器的时序逻辑。

核心区别:连续赋值是“线”的思维,过程赋值是“块”的思维。前者描述数据流,后者描述行为。

3.4 阻塞赋值 vs 非阻塞赋值

这是Verilog里最容易踩坑的地方,没有之一。我见过太多老手在这个问题上翻车。

阻塞赋值(=)

  • “阻塞”的意思是:这条语句执行完,下一条才能执行
  • 用于组合逻辑的always块
  • 赋值是立即生效的

非阻塞赋值(<=)

  • “非阻塞”的意思是:所有赋值同时计算,然后同时更新
  • 用于时序逻辑的always块
  • 赋值是延迟生效的(在块结束时统一更新)

看个经典例子:

// 错误示范:时序逻辑里用阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;  // 这里a已经是新值了!
end

// 正确示范:时序逻辑里用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;  // 这里a还是旧值!
end

第一个例子,a和c会同时变成b的值。第二个例子,a变成b,c变成a的旧值。仿真结果完全不同!

我曾经踩过的坑:有一次写一个移位寄存器,在always块里用了阻塞赋值。仿真波形看起来完全正确,但综合后的网表仿真死活不对。查了两天才发现是赋值方式的问题。从那以后,我给自己定了个规矩:时序逻辑用非阻塞,组合逻辑用阻塞。这个规矩救了我无数次。

总结一下规则:

场景 赋值方式 说明
组合逻辑(always @(*)) 阻塞赋值(=) 描述数据流,立即生效
时序逻辑(always @(posedge clk)) 非阻塞赋值(<=) 描述寄存器行为,并行更新
连续赋值(assign) 不适用 只能用wire,没有赋值方式选择

小技巧:如果你不确定该用哪种,先问自己:这个always块综合出来应该是组合逻辑还是时序逻辑?组合逻辑用=,时序逻辑用<=。记住这个,能避开90%的坑。

好了,这一节的内容就到这儿。模块端口、数据类型、赋值方式,这些都是Verilog的基石。别急着往下赶,先把这些概念吃透。下一节咱们聊运算符和表达式,到时候你会发现,基础打牢了,后面学什么都快。