1. Verilog语言基础回顾:模块结构、数据类型、运算符、赋值语句

各位同学好,我是你们的EDA讲师。今天咱们聊聊Verilog最基础的东西——模块结构、数据类型、运算符和赋值语句。别小看这些,我见过太多人在这上面栽跟头了。

1.1 模块结构——Verilog的骨架

Verilog里,模块(module)是最基本的设计单元。说白了,一个模块就是一个黑盒子,有输入、有输出,里面装着逻辑。

模块的基本结构:

module 模块名 (端口列表);
  // 端口声明
  input  [位宽-1:0] 输入端口;
  output [位宽-1:0] 输出端口;
  inout              双向端口;

  // 内部信号声明
  wire   [位宽-1:0] 内部连线;
  reg    [位宽-1:0] 内部寄存器;

  // 功能描述
  // 可以用assign、always块、实例化等

endmodule

我个人习惯把端口声明和内部信号声明分开写,这样代码结构清晰。嗯,这里要注意:端口列表里的名字要和声明里的名字一致,不然编译会报错。我曾经有一次就是因为端口名写错,查了整整一个下午。

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

Verilog的数据类型,核心就两类:wirereg。你想想看,这就像现实中的导线和存储单元。

类型 默认值 驱动方式 典型用途
wire 高阻态 (z) 连续赋值 (assign) 模块间连线、组合逻辑输出
reg 不定态 (x) 过程赋值 (always块内) 寄存器、状态机、时序逻辑

小技巧: 我建议初学者记住一个原则——在always块里被赋值的变量,必须声明为reg类型。但别误会,reg不一定会被综合成寄存器,它只是语法上的要求。

除了wire和reg,还有integer(整数)、real(实数)、time(时间)等类型。不过说实话,在RTL设计里,wire和reg占了95%以上的使用场景。

1.3 运算符——数字电路的数学语言

Verilog的运算符,说白了就是数字电路的数学表达。我把它分成几类:

1.3.1 算术运算符

  • + 加法、- 减法、* 乘法、/ 除法、% 取模
  • ** 幂运算(Verilog-2001支持)

注意:除法和取模在综合时很耗资源。我在项目中遇到过,用除法器直接导致面积超标。后来改成移位和查表,才把问题解决。

1.3.2 逻辑运算符

  • && 逻辑与、|| 逻辑或、! 逻辑非
  • 结果只有0或1

1.3.3 位运算符

  • & 按位与、| 按位或、^ 按位异或、~ 按位取反
  • ~^^~ 同或

避坑指南: 我曾经把 &&& 搞混过。前者是逻辑与,只返回0或1;后者是按位与,返回逐位运算的结果。比如 4'b1010 && 4'b1100 结果是1,而 4'b1010 & 4'b1100 结果是 4'b1000。完全不一样!

1.3.4 关系运算符

  • ><>=<===!=
  • ===!==(全等比较,会比较x和z状态)

全等比较 === 在仿真调试时特别有用。比如你想检查某个信号是不是高阻态,用 == 会返回x,用 === 才能准确判断。

1.3.5 移位运算符

  • << 左移、>> 右移
  • <<< 算术左移、>>> 算术右移(保留符号位)

1.3.6 拼接与复制运算符

  • {a, b} 拼接:把多个信号拼在一起
  • {n{a}} 复制:把信号a重复n次

举个例子:{4{1'b1}} 等于 4'b1111。这个在初始化寄存器时特别好用。

1.4 赋值语句——连续赋值与过程赋值

赋值语句是Verilog的核心,分两大类:连续赋值过程赋值

1.4.1 连续赋值 (assign)

assign 关键字,用于组合逻辑。等号左边必须是wire类型。

assign sum = a + b;
assign sel = (cnt > 10) ? 1'b1 : 1'b0;

连续赋值是持续驱动的。只要右边表达式变化,左边立即更新。这就像一根导线,输入变了,输出马上跟着变。

1.4.2 过程赋值 (always块内)

过程赋值用在 always 块里,等号左边必须是reg类型。

过程赋值又分两种:

类型 符号 特点 典型场景
阻塞赋值 = 顺序执行,立即更新 组合逻辑的always块
非阻塞赋值 <= 并行执行,块结束时更新 时序逻辑的always块

黄金法则:

  • 描述组合逻辑时,用 always @(*) + 阻塞赋值 =
  • 描述时序逻辑时,用 always @(posedge clk) + 非阻塞赋值 <=

为什么会这样?因为非阻塞赋值能避免竞争冒险。我记得刚入行时,在一个计数器里用了阻塞赋值,结果仿真波形一塌糊涂。后来老工程师告诉我:「时序逻辑用非阻塞,这是铁律。」从那以后,我再也没犯过这个错。

1.4.3 赋值语句示例对比

// 正确的时序逻辑写法
always @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    cnt <= 8'd0;
  else if (en)
    cnt <= cnt + 1'b1;
end

// 错误的写法(阻塞赋值用于时序逻辑)
always @(posedge clk or negedge rst_n) begin
  if (!rst_n)
    cnt = 8'd0;  // 不推荐!
  else if (en)
    cnt = cnt + 1'b1;  // 可能出问题!
end

我的经验: 写always块时,先想清楚这是组合逻辑还是时序逻辑。如果是时序逻辑,闭着眼睛用 <=。如果是组合逻辑,用 =。别混着用,不然仿真和综合结果可能不一致。

1.5 本章知识体系

下面这张图,是我梳理的本章知识结构。你把它记在脑子里,后面学解析器时就能快速定位知识点。

Verilog语言基础 模块结构 module/endmodule 端口声明 (input/output/inout) 内部信号声明 功能描述 数据类型 wire (线网类型) reg (寄存器类型) integer/real/time 向量 [msb:lsb] 参数 parameter 运算符 算术: + - * / % ** 逻辑: && || ! 位运算: & | ^ ~ 关系: > < == != 移位: << >> 拼接/复制: { } {n{ }} 赋值语句 连续赋值 (assign) 过程赋值 (always) 阻塞赋值 (=) 非阻塞赋值 (<=) 组合 vs 时序逻辑 核心:模块是骨架,wire/reg是血肉,运算符是逻辑,赋值是行为

好了,这一章的内容就这些。模块结构、数据类型、运算符、赋值语句,这四个东西是Verilog的基石。后面我们讲解析器时,会反复用到这些概念。你先把基础打牢,后面学起来就轻松了。


专注资料整理