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

好,咱们直接进入正题。

Verilog 这东西,说白了就是硬件描述语言。它不是软件,是描述电路的语言。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。这一点,我希望你从一开始就刻在脑子里。

3.1 模块结构——Verilog的基本单元

一个 Verilog 设计,本质上就是一堆模块的集合。模块就像芯片里的一个功能块,有输入、有输出,内部干点啥。

模块的基本骨架长这样:

module 模块名 (
    input  端口1,
    output 端口2,
    inout  端口3
);
    // 内部逻辑写在这里
endmodule

我个人习惯,模块名用大写字母开头,比如 TopModuleDataPath。这样在工程里一眼就能认出是模块。

举个例子,一个最简单的与门模块:

module AndGate (
    input  a,
    input  b,
    output y
);
    assign y = a & b;
endmodule

你看,assign 语句把输出 y 和输入 ab 的与运算结果连起来了。这就是组合逻辑。

小提示: 端口方向一定要写清楚。input 是输入,output 是输出,inout 是双向。我见过不少新手把 input 和 output 搞反,仿真能过,综合就报错。

3.2 数据类型——线网与寄存器

Verilog 里最常用的两种数据类型:wirereg

  • wire:线网类型,代表物理连线。它只能被 assign 语句驱动,或者被模块端口连接。
  • reg:寄存器类型,代表存储单元。它只能在 always 块里被赋值。

嗯,这里要注意:reg 不一定真的综合成寄存器。如果你在组合逻辑的 always 块里用 reg,它最终可能只是一根线。这跟软件里的变量完全是两码事。

举个例子:

wire w1;          // 一根线
reg  r1;          // 一个寄存器(也可能是线)

assign w1 = a & b;  // 正确,wire 用 assign 驱动

always @(*) begin
    r1 = a & b;     // 正确,reg 在 always 块里赋值
end

我在项目中遇到过一个问题:有人把组合逻辑里的 reg 当成真正的寄存器,结果综合出来的电路跟他想的不一样。说白了,reg 只是语法上的要求,不代表硬件一定是寄存器。

3.3 运算符——跟C语言很像,但有坑

Verilog 的运算符,大部分跟 C 语言一样。但有几个地方特别容易踩坑。

运算符类型 符号 说明
算术 + - * / % 加减乘除取模
逻辑 && || ! 逻辑与、或、非
按位 & | ~ ^ 按位与、或、非、异或
归约 & | ^ ~& ~| ~^ 对向量所有位操作
移位 << >> 左移、右移
拼接 { } 把多个信号拼在一起
条件 ? : 三目运算符

你想想看,&&& 有什么区别?前者是逻辑与,结果只有 0 或 1;后者是按位与,结果是对应位相与。我刚开始学的时候,经常把这两个搞混,仿真结果对不上,查了半天才发现是这里写错了。

还有一个常用的技巧:用拼接运算符 {} 来组合信号。比如:

wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b0101;
wire [7:0] c = {a, b};  // c = 8'b10100101

这在做数据打包的时候特别方便。

3.4 assign语句——组合逻辑的直通车

assign 语句,说白了就是给 wire 赋值。它描述的是组合逻辑,没有时钟,没有边沿,输入一变,输出立刻跟着变。

基本用法:

assign 线网名 = 表达式;

比如:

wire [7:0] sum;
assign sum = a + b;

这里 sum 就是 ab 相加的结果。注意,assign 的左边必须是 wire 类型,不能是 reg

警告: 同一个 wire 不能被多个 assign 语句驱动。如果你写了两个 assign 给同一个 wire,综合工具会报多驱动错误。我曾经在一个大项目里犯过这个错,查了整整一天才找到问题。

另外,assign 语句是并行执行的。你写 10 条 assign,它们同时生效,没有先后顺序。这一点跟软件完全不一样。

3.5 always块——时序逻辑的核心

always 块是 Verilog 里最强大的结构之一。它既可以描述组合逻辑,也可以描述时序逻辑。

基本语法:

always @(敏感列表) begin
    // 逻辑代码
end

敏感列表决定了 always 块什么时候执行。常用的写法:

  • 组合逻辑always @(*)always @(a or b or c)
  • 时序逻辑always @(posedge clk)always @(negedge clk)

举个例子,一个带异步复位的 D 触发器:

reg q;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

这里用了 <= 非阻塞赋值。为什么用非阻塞?因为在时序逻辑里,非阻塞赋值能保证所有赋值在时钟边沿同时生效,不会出现竞争。我建议你养成习惯:时序逻辑用 <=,组合逻辑用 =

嗯,这里有个坑:如果你在同一个 always 块里混用阻塞和非阻塞赋值,仿真结果会非常诡异。我曾经因为这个,仿真波形怎么看怎么不对,最后发现是赋值方式用错了。

3.6 知识体系总览

下面这张图,把本章的核心知识点串起来了。你一看就明白。

Verilog基础语法知识体系 模块结构 数据类型 运算符 module / endmodule 端口声明:input / output / inout wire:线网类型 reg:寄存器类型 算术 / 逻辑 / 按位 归约 / 移位 / 拼接 核心语句 assign 语句 always 块 组合逻辑:assign / always @(*) | 时序逻辑:always @(posedge clk)

这张图把模块结构、数据类型、运算符、assign 和 always 块的关系理清楚了。你写代码的时候,脑子里要有这张图。

3.7 避坑指南

最后,分享几个我踩过的坑:

  • 多驱动问题:同一个 wire 被多个 assign 驱动,或者被 assign 和 always 同时驱动。综合会报错,仿真结果也不对。
  • 敏感列表不完整:组合逻辑的 always 块,如果敏感列表漏了某个输入,仿真结果跟综合结果可能不一致。用 always @(*) 可以避免这个问题。
  • 阻塞与非阻塞混用:同一个 always 块里,不要同时用 =<=。仿真结果会让人崩溃。
  • 位宽不匹配:赋值时,左右两边位宽不一致,工具会自动截断或扩展。这可能导致意想不到的结果。我建议你显式指定位宽,比如 assign y = {2'b00, a};

好了,这一章的内容就这些。记住,Verilog 是描述硬件的,不是写软件的。多动手写代码,多仿真,多看看综合后的电路图,慢慢就有感觉了。


专注资料整理