2. Verilog语法精讲(上):模块结构、数据类型、运算符、连续赋值

各位同学,咱们今天聊聊Verilog最基础的东西。说实话,很多新手一上来就急着写代码,结果连模块长什么样都没搞明白。我当年带过一个实习生,写了三天代码,最后发现连端口方向都写反了——这种坑,咱们今天一次性填平。

2.1 模块结构:芯片设计的“乐高积木”

Verilog里,模块(module)就是最基本的构建单元。你可以把它想象成一块乐高积木——有接口(端口),有内部逻辑(功能),还能跟其他积木拼在一起。

一个标准的模块长这样:

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

    // 内部逻辑
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 4'b0;
        else
            count <= count + 1'b1;
    end

endmodule

注意几个关键点:

  • 端口声明:input、output、inout,方向不能搞错。我见过有人把input写成output,仿真能过,综合直接报错。
  • 数据类型:wire和reg,后面细讲。
  • endmodule:模块结束标志,千万别漏了。漏了这个,综合工具会给你一堆莫名其妙的错误。
个人习惯:我写模块时,喜欢把端口声明和内部信号分开写。端口用一行一个,内部信号按功能分组。这样代码看起来清爽,调试时也容易定位问题。

2.2 数据类型:wire vs reg,到底怎么选?

这个问题,几乎每个初学者都会问。我当年也纠结过很久。说白了:

  • wire:连线型,用于组合逻辑。比如assign语句的输出、模块端口之间的连接。
  • reg:寄存器型,用于时序逻辑。比如always块里赋值的变量。

但有个坑:reg不一定真的综合成寄存器。如果你在always块里写组合逻辑(比如always @(*)),reg也可以综合成纯组合电路。嗯,这里要注意——综合工具看的是你怎么用,不是你怎么声明。

举个例子:

// 这个reg综合出来是组合逻辑
reg [3:0] sum;
always @(*) begin
    sum = a + b;
end

// 这个reg综合出来是触发器
reg [3:0] count;
always @(posedge clk) begin
    count <= count + 1'b1;
end
我曾经踩过的坑:在always块里用阻塞赋值(=)写时序逻辑,结果综合出来一堆奇怪的锁存器。后来养成习惯——时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。这个规矩,建议你从一开始就记住。

2.3 运算符:别小看这些符号

Verilog的运算符跟C语言很像,但有几个地方特别容易出错。

类别 运算符 说明
算术 + - * / % 注意:乘法综合出来面积大,除法尽量少用
逻辑 && || ! 结果是1位(true/false)
按位 & | ~ ^ 逐位操作,结果宽度跟操作数相同
归约 & | ~& ~| ^ ~^ 单目运算符,把多位宽向量归约为1位
移位 << >> 左移右移,空位补0
拼接 { } 把多个信号拼在一起,非常实用

你想想看,归约运算符是不是很眼熟?其实就是把多位宽信号的所有位做与、或、异或。我调试时经常用归约或(|)来检查一个总线里有没有任何一位为1——比写循环快多了。

重点提醒:拼接运算符{}是Verilog的独门绝技。比如把两个4位信号拼成8位:{a, b}。还可以重复:{4{1'b1}}生成4个1。这个在构造测试向量时特别好用。

2.4 连续赋值(assign):组合逻辑的“直通车”

连续赋值,说白了就是用assign语句把组合逻辑“连”到wire上。它的特点是:只要右边表达式变了,左边立刻更新。

基本语法:

assign  wire_name = expression;

举个例子:

module adder (
    input  wire [3:0] a, b,
    output wire [4:0] sum
);
    assign sum = a + b;
endmodule

这里sum是wire类型,a和b变了,sum马上跟着变。这就是组合逻辑的本质——没有时钟,没有存储,纯靠连线。

连续赋值有几个要点:

  • 左边必须是wire(或者wire类型的端口)。不能是reg。
  • 右边可以是任何表达式:运算符、函数调用、条件表达式(? :)都可以。
  • 多条assign语句并行执行,跟顺序无关。这个跟软件编程完全不同。
我的经验:写组合逻辑时,能用assign就用assign。只有在需要if-else或case这种复杂条件时,才考虑用always @(*) + reg。为什么?因为assign更直观,综合出来的结果也更可控。

对了,条件运算符(? :)在assign里特别常用。比如:

assign max = (a > b) ? a : b;

这比写always块简洁多了。你想想看,是不是这个理?

2.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了。建议你多看几遍,理解各个概念之间的关系。

Verilog语法基础(上) 模块结构 数据类型 运算符 连续赋值 端口声明(input/output) 内部逻辑(always块) 模块实例化 wire(连线) reg(寄存器) integer/parameter 算术/逻辑/按位 归约/移位/拼接 条件运算符(? :) assign语句 组合逻辑实现 并行执行特性 核心:模块是骨架,wire/reg是血肉,运算符是工具,assign是组合逻辑的直通车

好了,这一章的内容就这些。模块结构、数据类型、运算符、连续赋值——这四个东西是Verilog的基石。你把这些搞明白了,后面学时序逻辑、状态机什么的,就会轻松很多。

可可精品资料库