2. Verilog基础语法(上):模块结构、数据类型、运算符、assign语句、always块

各位同学,欢迎来到第二讲。今天咱们要啃的,是Verilog最核心的几块骨头——模块结构、数据类型、运算符,还有两个最重要的语句块:assign和always。

说实话,我刚开始学Verilog那会儿,总觉得这些东西太琐碎。后来做项目多了才发现,基础不牢,地动山摇。很多bug追到最后,其实就是wire和reg用混了,或者always块里漏了敏感列表。嗯,咱们今天就把这些坑一个个填平。

本章核心脉络:理解Verilog的模块化设计思想,掌握wire和reg的本质区别,熟练使用assign和always两种建模方式。

Verilog基础语法(上) 模块结构 module/endmodule 数据类型 wire / reg 运算符 算术/逻辑/位/关系 语句块 assign / always 端口定义 参数化 组合逻辑 时序逻辑 拼接/缩减 移位/条件

2.1 模块结构——Verilog的"黑盒子"

每个Verilog设计,说白了就是一个模块(module)。你可以把它想象成一个黑盒子——外面只看到管脚(端口),里面藏着逻辑电路。

module counter #(
    parameter WIDTH = 8
)(
    input  wire        clk,
    input  wire        rst_n,
    input  wire        en,
    output reg  [WIDTH-1:0] count
);
    // 内部逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 0;
        else if (en)
            count <= count + 1;
    end
endmodule

我个人习惯,写模块时先把端口声明写完,再写内部逻辑。这样结构清晰,别人看你的代码也舒服。

小技巧:端口方向用 input/output/inout 声明。我建议所有端口都显式标明 wire 或 reg 类型,别偷懒。有些工具默认类型会让你抓狂。

2.2 数据类型——wire 和 reg 到底怎么选?

这是新手最容易懵的地方。我当年也纠结过:什么时候用 wire?什么时候用 reg?

说白了就一句话:

  • wire:物理连线。它不保存值,只是把两个点连起来。assign 语句左边必须是 wire。
  • reg:寄存器/变量。它可以在 always 块里被赋值,能保存状态。

但注意!reg 不一定是真正的硬件寄存器。在 always 的组合逻辑块里,reg 综合出来只是连线。嗯,这里容易混淆,我单独列个表:

类型 默认值 赋值方式 综合结果
wire 高阻态 z assign 连续赋值 连线
reg(时序逻辑) 不定态 x always @(posedge clk) 触发器
reg(组合逻辑) 不定态 x always @(*) 连线(无寄存器)
避坑指南:我曾经在项目中把一个多bit的wire信号直接赋值给另一个reg,结果仿真怎么都不对。后来发现是位宽不匹配——wire是[7:0],reg是[3:0],高位被截断了。所以声明时一定要对齐位宽!

2.3 运算符——不只是加减乘除

Verilog的运算符跟C语言很像,但有几个特殊的你得注意:

  • 算术运算符:+ - * / %。注意除法综合出来资源很大,我一般不用。
  • 逻辑运算符:&& || !。结果是1bit的布尔值。
  • 位运算符:& | ~ ^。按位操作,这个在图像处理里用得特别多。
  • 关系运算符:> < >= <= == !=。比较大小。
  • 移位运算符:<< >>。左移1位相当于乘2,右移1位相当于除2。
  • 拼接运算符:{a, b}。把多个信号拼在一起,做图像处理时经常用来组合RGB数据。
  • 条件运算符:? :。相当于if-else的简写。
// 举个例子:图像像素的RGB分离
wire [23:0] pixel;  // {R, G, B} 各8bit
wire [7:0] red   = pixel[23:16];
wire [7:0] green = pixel[15:8];
wire [7:0] blue  = pixel[7:0];

// 拼接回去
wire [23:0] new_pixel = {red, green, blue};

重点:拼接运算符在图像处理里简直是神器。你想把多个通道的数据打包成一个总线,或者从总线里拆出各个通道,用 {} 一行搞定。

2.4 assign语句——连续赋值

assign 是组合逻辑的"直通车"。它描述的是:只要右边信号变化,左边立刻更新。没有延迟,没有时钟。

// 简单的加法器
assign sum = a + b;

// 带条件的赋值
assign max = (a > b) ? a : b;

// 多bit选择
assign data_out = sel ? data1 : data2;

我个人习惯,简单的组合逻辑用 assign,复杂的用 always 块。为什么?因为 assign 写多了,代码可读性会下降。你想想看,一个 assign 里塞了五六个条件运算符,别人看代码得多痛苦。

注意:assign 的左边必须是 wire 类型。如果你写成 assign reg_a = ...,编译器会报错。我刚开始就犯过这个错,找了半天才发现是类型问题。

2.5 always块——时序与组合的"万能工具箱"

always 块是 Verilog 最灵活的语句块。它有两种主要用法:

2.5.1 组合逻辑的 always 块

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

这里用阻塞赋值 =。注意,组合逻辑的 always 块里,所有输入信号都要出现在敏感列表里。用 @(*) 可以自动包含所有输入,省心。

2.5.2 时序逻辑的 always 块

// 时序逻辑:时钟沿触发
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 0;
    else
        q <= d;
end

这里用非阻塞赋值 <=。为什么?因为非阻塞赋值能模拟触发器的行为——所有赋值在时钟沿同时发生,不会互相干扰。

血的教训:我曾经在 always 块里混用了阻塞和非阻塞赋值,结果仿真和综合结果完全不一样。仿真时看起来没问题,上板子就乱跳。从那以后,我给自己定了个规矩:
组合逻辑用 =,时序逻辑用 <=,绝不混用。

2.6 实战小例子:边沿检测

咱们来写一个实用的电路——检测信号的上升沿。这在图像处理里经常用到,比如检测帧同步信号。

module edge_detect (
    input  wire clk,
    input  wire rst_n,
    input  wire sig_in,
    output wire pos_edge,
    output wire neg_edge
);
    reg sig_dly;
    
    // 延迟一拍
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            sig_dly <= 0;
        else
            sig_dly <= sig_in;
    end
    
    // 检测边沿
    assign pos_edge = sig_in & ~sig_dly;  // 上升沿
    assign neg_edge = ~sig_in & sig_dly;  // 下降沿
endmodule

这个电路的核心思想很简单:把信号延迟一个时钟周期,然后比较当前值和历史值。如果当前是1、历史是0,那就是上升沿。反过来就是下降沿。

嗯,到这里,Verilog基础语法的上半部分就讲完了。这些内容看起来简单,但每个点都值得你亲手敲一遍代码、跑一遍仿真。我在做图像滤波项目时,这些基础语法几乎每天都在用——模块化设计让代码可复用,wire/reg的正确使用避免了很多仿真bug,assign和always块则是构建所有逻辑的基石。

记住:基础越扎实,后面写复杂算法时就越从容。

课后练习建议:

  1. 写一个8位加法器模块,用assign实现
  2. 写一个带同步复位的D触发器,用always块实现
  3. 把上面两个模块例化到顶层,跑一遍仿真
可可精品资料库