2. Verilog HDL基础回顾:模块结构、数据类型(wire/reg)、运算符、always块与assign语句
好,咱们正式开始动手之前,得先把地基打牢。Verilog 这门语言,说白了就是用来描述数字电路的。你写出来的每一行代码,最终都会映射成实际的逻辑门和连线。我见过不少新手,上来就写代码,结果综合出来的电路跟自己想的完全不一样——嗯,这就是基础没打牢的后果。
2.1 模块结构:一切设计的起点
Verilog 的基本单元叫 模块(module)。你可以把它想象成一个黑盒子,有输入、有输出,里面装着具体的逻辑。我个人习惯,写任何模块之前,先画个端口图,再动手写代码,这样思路清晰很多。
一个完整的模块结构长这样:
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire en, // 使能
output reg [3:0] count // 4位计数器输出
);
// 内部逻辑写在这里
endmodule
注意看,module 和 endmodule 是成对出现的。端口列表里,我习惯把输入写在前面,输出写在后面,这样别人看你的代码一目了然。我在项目中遇到过,有人把几十个端口乱序排列,结果自己回头改代码时都找半天——千万别学这个。
2.2 数据类型:wire 与 reg 的区别
这是新手最容易搞混的地方。我刚开始学的时候也迷糊过一阵子。
wire 代表连线,它本身不存储值。说白了,它只是把某个信号的值传递到另一个地方。比如:
wire a, b, c;
assign c = a & b; // c 是 a 和 b 相与的结果
reg 代表寄存器,它能存储值。注意,这里的“寄存器”是逻辑上的概念,不一定真的映射成硬件寄存器。比如在 always 块里赋值的变量,必须声明为 reg:
reg [7:0] data_reg;
always @(posedge clk) begin
data_reg <= 8'hFF;
end
我给大家一个简单的判断方法:在 assign 语句左边出现的,用 wire;在 always 块里被赋值的,用 reg。记住这个,90% 的情况不会错。
2.3 运算符:从算术到位运算
Verilog 的运算符跟 C 语言很像,但有些细节不一样。我整理了一个常用运算符表:
| 类别 | 运算符 | 说明 |
|---|---|---|
| 算术 | + - * / % | 加减乘除取模 |
| 位运算 | & | ~ ^ | 与、或、非、异或 |
| 逻辑 | && || ! | 逻辑与、或、非 |
| 关系 | > < >= <= == != | 比较运算 |
| 移位 | << >> | 左移、右移 |
| 拼接 | { } | 将多个信号拼接成一个 |
这里有个容易踩的坑:位运算和逻辑运算的区别。比如 a & b 是对每一位做与操作,结果还是多位的;而 a && b 是把 a 和 b 当成整体做逻辑判断,结果只有 0 或 1。你想想看,如果 a=4'b1010, b=4'b0101,那 a & b 的结果是 4'b0000,而 a && b 的结果是 1(因为两者都不为 0)。
2.4 assign 语句:组合逻辑的利器
assign 用来描述组合逻辑。它的特点就是:只要右边信号变化,左边立刻更新。这跟硬件里的连线一模一样。
wire [3:0] sum;
assign sum = a + b; // 组合加法器
我个人习惯,简单的组合逻辑用 assign,复杂的用 always 块。比如一个多路选择器:
wire [7:0] mux_out;
assign mux_out = (sel == 2'b00) ? data0 :
(sel == 2'b01) ? data1 :
(sel == 2'b10) ? data2 : data3;
这样写,是不是比一堆 if-else 清晰多了?
2.5 always 块:时序逻辑的核心
always 块是 Verilog 里最灵活、也最容易出问题的地方。它的基本格式是:
always @(敏感列表) begin
// 逻辑代码
end
敏感列表决定了 always 块什么时候执行。常用的写法有:
- 组合逻辑:
always @(*)或always @(a or b or c) - 时序逻辑:
always @(posedge clk or negedge rst_n)
举个例子,一个带异步复位的 D 触发器:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
<=(非阻塞赋值),组合逻辑里用 =(阻塞赋值)。这个区别很重要,搞反了仿真结果会完全不一样。我曾经调试一个计数器,死活数不对,最后发现就是在 always 块里用了阻塞赋值——嗯,从那以后我再也不敢混用了。
2.6 知识体系结构图
为了让你更直观地理解本章的知识脉络,我画了一张图:
好了,这一章的内容就到这里。记住:模块是骨架,wire/reg 是血肉,运算符是工具,assign 和 always 是灵魂。把这几个东西搞明白了,后面的解析器开发才能顺利进行。
公众号:蓝海资料掘金营,微信deep3321