第二章 Verilog HDL核心语法(上):模块结构、数据类型、操作符、assign语句、always块
各位同学,咱们今天聊聊Verilog最基础的东西。说实话,很多新手一上来就急着写代码,结果连模块长什么样都没搞明白。我当年也犯过这毛病,被老工程师骂过好几次才改过来。咱们先把地基打牢,后面才能盖高楼。
2.1 模块结构——芯片设计的“黑盒子”
Verilog里最基本的设计单元就是模块(module)。你可以把它想象成一个黑盒子——外面只看到管脚(端口),里面藏着逻辑。我习惯把模块比作一个芯片的“功能块”,比如一个加法器、一个计数器,甚至一个完整的CPU。
一个标准的模块长这样:
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire en, // 使能
output reg [3:0] count // 4位计数器输出
);
// 内部逻辑写在这里
endmodule
注意看,端口声明有input、output,还有inout(双向口)。我个人建议:能用input就别用inout,双向口处理起来麻烦得很,容易出时序问题。我在项目中见过有人为了省几个管脚用inout,结果调试时把自己坑惨了。
2.2 数据类型——wire和reg的“爱恨情仇”
Verilog里最常用的数据类型就两个:wire和reg。很多初学者搞不清它们的区别,其实说白了:
- wire:连线,用于组合逻辑。它不存储值,只是把信号连起来。
- reg:寄存器,用于时序逻辑。它能记住上一次的值。
但注意!reg不一定真的综合成寄存器。如果你在always块里写组合逻辑,reg只是语法上的要求,综合出来还是组合电路。嗯,这里要特别提醒一下。
举个例子:
wire a, b, c;
assign c = a & b; // 组合逻辑,用wire
reg [7:0] data_reg;
always @(posedge clk) begin
data_reg <= data_in; // 时序逻辑,用reg
end
我曾经遇到过一个同事,把所有信号都定义成reg,结果综合报告里一堆没必要的寄存器,面积大了30%。所以记住:该用wire就用wire,别偷懒。
2.3 操作符——Verilog的“数学工具箱”
Verilog的操作符跟C语言很像,但有些地方不一样。我整理了一张表,方便你查阅:
| 类别 | 操作符 | 说明 |
|---|---|---|
| 算术 | + - * / % | 加减乘除取模。注意除法综合出来面积很大 |
| 位运算 | & | ~ ^ | 与、或、非、异或。组合逻辑常用 |
| 逻辑 | && || ! | 逻辑与、或、非。用于条件判断 |
| 关系 | > < >= <= == != | 比较大小、相等判断 |
| 移位 | << >> | 左移、右移。乘2除2的利器 |
| 拼接 | { } | 把多个信号拼在一起,非常实用 |
这里我要特别说一下拼接操作符{ }。我在做数据通路设计时,经常用它来组合控制信号。比如:
wire [7:0] high_byte, low_byte;
wire [15:0] word = {high_byte, low_byte}; // 拼成一个16位字
你想想看,如果没有拼接操作符,你得写多少assign语句?
2.4 assign语句——组合逻辑的“直通车”
assign语句用来描述组合逻辑。它的语法很简单:
assign 信号名 = 表达式;
比如:
wire [3:0] sum;
assign sum = a + b;
assign的左边必须是wire类型,右边可以是任何表达式。我习惯把简单的组合逻辑用assign写,比如地址译码、数据选择等。但复杂的组合逻辑,我建议用always块,可读性更好。
这里有个坑:多个assign不能对同一个信号赋值。如果你写了:
assign data = a;
assign data = b; // 错误!多驱动
综合工具会报错,因为不知道到底该用哪个值。我在项目中见过有人不小心写了两个assign,结果仿真没问题,综合出来X态满天飞。
2.5 always块——时序逻辑的“心脏”
always块是Verilog里最强大的结构。它既可以描述时序逻辑,也可以描述组合逻辑。基本语法:
always @(敏感列表) begin
// 逻辑代码
end
敏感列表决定了always块什么时候执行。常见的写法:
- 时序逻辑:
always @(posedge clk or negedge rst_n) - 组合逻辑:
always @(*)或always @(a or b or c)
我个人强烈建议:时序逻辑用非阻塞赋值(<=),组合逻辑用阻塞赋值(=)。这是无数前辈用血泪换来的经验。为什么?
非阻塞赋值在时钟沿同时更新,模拟了寄存器的行为。而阻塞赋值是顺序执行的,用在时序逻辑里会出大问题。我曾经调试过一个bug,就是因为有人在always块里混用了阻塞和非阻塞赋值,结果仿真和综合结果完全不一样。
看个正确的例子:
// 时序逻辑:计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0;
else if (en)
count <= count + 1'b1;
end
// 组合逻辑:译码器
always @(*) begin
case (sel)
2'b00: data_out = a;
2'b01: data_out = b;
2'b10: data_out = c;
default: data_out = 4'b0;
endcase
end
注意看,时序逻辑里用<=,组合逻辑里用=。这个习惯一定要养成。
- 模块是Verilog的基本单元,端口要分清楚input/output
- wire用于组合逻辑,reg用于时序逻辑(但reg也可以描述组合逻辑)
- 操作符里注意除法、乘法要慎用
- assign描述简单的组合逻辑,always块描述复杂的组合或时序逻辑
- 时序逻辑用非阻塞赋值,组合逻辑用阻塞赋值
好了,这一章的内容就这些。语法这东西,光看没用,得多写。我建议你打开编辑器,把上面的例子都敲一遍,然后改改参数看看效果。遇到问题别怕,调试本身就是学习的过程。