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

各位同学好,今天我们正式开始啃Verilog语法这块硬骨头。说实话,很多初学者觉得语法枯燥,但我要告诉你——在CPLD/FPGA的世界里,语法就是你的螺丝刀和焊枪。搞懂了今天这五个核心概念,你就能看懂八成以上的电机控制代码。

3.1 模块结构——你的第一个“黑盒子”

Verilog里最基本的单元叫模块(module)。你可以把它想象成一个黑盒子:外面能看到的是引脚(端口),里面装的是逻辑电路。

一个标准的模块长这样:

module motor_controller (
    input  wire clk,        // 时钟
    input  wire rst_n,      // 复位,低有效
    input  wire [7:0] speed,// 速度给定
    output reg  [7:0] pwm   // PWM输出
);

// 这里写逻辑代码

endmodule

我个人习惯把端口声明和变量声明分开写,这样代码结构更清晰。注意看,inputoutput后面跟的是数据类型——这就是我们接下来要说的wire和reg。

小技巧:模块名最好用下划线分隔单词,比如pwm_generator而不是PWMGenerator。这在大型项目中能避免很多大小写敏感的问题。

3.2 wire与reg——连线 vs 寄存器

这两个数据类型是新手最容易搞混的。我简单总结一下:

  • wire:相当于一根导线。它不存储值,只是把某处的信号“连”到另一处。
  • reg:相当于一个存储单元。它会在时钟沿到来时“记住”当前值。

但这里有个坑——很多教材说“reg就是寄存器”,其实不完全对。在always块里赋值的变量必须声明为reg,哪怕它综合出来只是一根线。反过来,assign语句赋值的变量必须是wire。

我曾经在项目里犯过这个错:把一个计数器输出声明成wire,结果综合报错报了半天。后来养成习惯——always块里用reg,assign语句用wire,再也没出过问题。

数据类型 赋值方式 综合结果 典型用途
wire assign 连续赋值 连线或组合逻辑 模块间连接、组合逻辑输出
reg always 过程赋值 寄存器或组合逻辑 时序逻辑、状态机、计数器

3.3 always块——时序逻辑的“心脏”

always块是Verilog里最常用的过程块。它有两种典型写法:

组合逻辑写法:

always @(*) begin
    // 敏感列表用 * 表示所有输入信号
    // 这里写组合逻辑
end

时序逻辑写法:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        // 复位操作
    end else begin
        // 时钟上升沿触发的操作
    end
end

嗯,这里要注意:敏感列表一定要写全。如果你写组合逻辑时漏了某个输入信号,综合出来的电路和你想的会完全不一样。我见过一个同事因为敏感列表少写了a,结果仿真和实测对不上,查了两天才找到原因。

避坑指南:我曾经在电机控制项目中,把always块的敏感列表写成@(posedge clk),但里面却用了组合逻辑赋值。结果综合工具给我生成了一个巨大的锁存器,直接把芯片资源吃光了。记住:时序逻辑用posedge/negedge,组合逻辑用@(*)

3.4 assign语句——连续赋值的“直通车”

assign语句用来描述组合逻辑。它的特点是:只要右边表达式有任何变化,左边立即更新

assign pwm_out = (counter < duty) ? 1'b1 : 1'b0;

这行代码的意思是:当计数器值小于占空比时,pwm_out输出高电平,否则输出低电平。你看,这就是一个典型的PWM生成逻辑,用assign一句话就搞定了。

我个人习惯把简单的组合逻辑用assign写,复杂的才放到always块里。这样代码读起来更清爽,也更容易定位问题。

3.5 阻塞与非阻塞赋值——时序的“分水岭”

这是Verilog里最容易被误解的概念,也是面试必考题。简单说:

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

为什么会有这种区别?因为硬件电路里,寄存器是在时钟沿同时更新的,不是一条一条顺序执行的。

看个例子你就明白了:

// 阻塞赋值——错误用法
always @(posedge clk) begin
    a = b;
    c = a;  // 这里c得到的是a更新后的值
end

// 非阻塞赋值——正确用法
always @(posedge clk) begin
    a <= b;
    c <= a;  // 这里c得到的是a更新前的值
end

我曾经在电机速度环里用阻塞赋值写了一个移位寄存器,结果仿真波形完全不对。查了半天才发现——阻塞赋值让数据在一个时钟周期内“穿透”了多个寄存器,相当于把流水线变成了直通线。从那以后,我给自己定了个规矩:时序逻辑一律用非阻塞赋值,组合逻辑才用阻塞赋值

黄金法则:

  • 写时序逻辑(always @(posedge clk))→ 用 <=(非阻塞)
  • 写组合逻辑(always @(*) 或 assign)→ 用 =(阻塞)
  • 同一个always块里不要混用两种赋值方式

3.6 知识体系总览

为了让你更直观地理解今天讲的内容,我画了一张图:

Verilog基础语法知识体系 module / endmodule wire 与 reg always 块 assign 语句 阻塞赋值 (=) vs 非阻塞赋值 (<=) 关键规则 • 时序逻辑 always @(posedge clk) → 非阻塞赋值 <= • 组合逻辑 always @(*) 或 assign → 阻塞赋值 = • 同一个 always 块内不要混用两种赋值方式 CPLD/FPGA逻辑设计基础 · 电机控制应用

这张图把今天讲的五个知识点串起来了。你看,模块结构是外壳,wire/reg是数据类型,always和assign是两种赋值方式,阻塞/非阻塞是时序控制的核心。搞懂这些,你就掌握了Verilog的“骨架”。

好了,今天的内容就到这里。记住:理论要结合实践。我建议你打开开发环境,把今天讲的每个例子都敲一遍,看看仿真波形。只有亲手试过,才能真正理解这些概念。

课后练习:

  1. 写一个模块,用assign实现一个4位加法器
  2. 用always块写一个8位计数器,带异步复位
  3. 对比阻塞和非阻塞赋值在移位寄存器中的区别

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