3、Verilog基础(上):模块与端口、数据类型(wire/reg)、组合逻辑(assign/always)、运算符
各位同学,今天咱们聊聊Verilog的基础。说实话,这部分内容看起来简单,但恰恰是很多新手翻车的重灾区。我见过不少项目,代码写得花里胡哨,结果综合出来一堆问题,追根溯源,都是基础没打牢。
3.1 模块与端口:芯片的“黑盒子”思维
Verilog里,模块(module)是基本单元。你可以把它想象成一个黑盒子——有输入、有输出,内部怎么实现,外面不用管。
我个人习惯,写模块前先画个框图。把输入输出引脚标清楚,再动手写代码。这样思路清晰,不容易漏信号。
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire en, // 使能
output reg [3:0] count // 计数值
);
// 内部逻辑
endmodule
注意看端口声明:input、output、inout。其中inout是双向端口,新手慎用。我在项目中吃过亏——双向端口处理不好,容易产生驱动冲突,调试起来很头疼。
端口类型速查表
| 方向 | 关键字 | 说明 |
|---|---|---|
| 输入 | input | 信号从外部流入模块 |
| 输出 | output | 信号从模块流出到外部 |
| 双向 | inout | 既可输入也可输出,需配合三态门 |
3.2 数据类型:wire 与 reg 的“爱恨情仇”
很多初学者搞不清wire和reg的区别。说白了:
- wire:连线,表示物理连接。它本身不存储值,值由驱动源决定。
- reg:寄存器,表示存储单元。它在过程赋值(always块)中保持值。
但注意!reg不一定会综合成寄存器。如果你在always块里写组合逻辑,reg综合出来只是连线。嗯,这里要特别提醒:综合工具看的是你的写法,不是数据类型名字。
我的经验:在组合逻辑的always块里,我习惯用reg类型;在assign语句里,我用wire。这样代码可读性更好,别人一看就知道你的意图。
// wire 用法:assign 连续赋值
wire [7:0] sum;
assign sum = a + b;
// reg 用法:always 过程赋值
reg [7:0] sum_reg;
always @(*) begin
sum_reg = a + b;
end
你想想看,上面两段代码功能完全一样,但写法不同。综合出来的电路也基本一致。所以别纠结名字,关键是理解背后的硬件行为。
3.3 组合逻辑:assign 与 always 的“双雄对决”
组合逻辑,就是输出只取决于当前输入,没有记忆功能。实现方式有两种:
3.3.1 assign 语句
assign是连续赋值,左边必须是wire类型。它描述的是“一直连接”的关系。
assign y = (sel) ? a : b; // 二选一多路器
这种写法简洁明了,适合简单的组合逻辑。我曾经在项目中用assign写了一个复杂的加法树,综合出来面积和时序都很好。
3.3.2 always 语句
always块更灵活,可以写if-else、case等结构。但要注意:
- 组合逻辑的敏感列表用
@(*),表示所有输入信号变化都触发。 - 块内赋值用阻塞赋值
=,非阻塞赋值<=用于时序逻辑。
always @(*) begin
if (sel)
y = a;
else
y = b;
end
避坑指南:我曾经在组合逻辑的always块里用了非阻塞赋值<=,结果仿真和综合结果不一致。查了两天才发现——组合逻辑必须用阻塞赋值!记住:组合逻辑用=,时序逻辑用<=。
3.4 运算符:从算术到位操作
Verilog的运算符很丰富,我挑几个重点说说。
| 类别 | 运算符 | 示例 | 说明 |
|---|---|---|---|
| 算术 | + - * / % | a + b | 加减乘除取模 |
| 位运算 | & | ~ ^ | a & b | 与、或、非、异或 |
| 逻辑 | && || ! | a && b | 逻辑与、或、非 |
| 关系 | > < >= <= == != | a > b | 比较运算 |
| 移位 | << >> | a << 2 | 左移、右移 |
| 拼接 | { } | {a, b} | 位拼接 |
| 条件 | ? : | sel ? a : b | 三目运算符 |
这里有个容易混淆的点:位运算 vs 逻辑运算。位运算是对每个bit操作,逻辑运算结果是布尔值(0或1)。
wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b1100;
assign bit_and = a & b; // 4'b1000
assign log_and = a && b; // 1'b1(因为a和b都不为0)
我个人习惯,在条件判断里用逻辑运算,在数据通路里用位运算。这样代码意图清晰,不容易出错。
运算符优先级提醒:
优先级从高到低:! ~ > 算术 > 移位 > 关系 > 位运算(& | ^) > 逻辑(&& ||) > 条件(?:)
记不住?那就加括号!我写代码时,只要不确定优先级,一律加括号。这样既保险,又方便别人阅读。
3.5 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作学习地图。
这张图展示了本章的四个核心模块:模块与端口是骨架,数据类型是血肉,组合逻辑是行为,运算符是工具。四者结合,就能写出基本的Verilog代码。
学习建议:别急着写复杂代码。先把模块声明、wire/reg区别、assign/always用法练熟。我当年就是基础没打牢,后面写状态机时各种踩坑。基础扎实了,后面学起来飞快。
好了,这一章的内容就到这里。记住:Verilog是硬件描述语言,不是软件编程语言。写代码时,脑子里要想着电路,而不是流程。下一章我们继续聊时序逻辑和状态机,到时候会有更多实战技巧分享。