3. 数据类型与运算符:wire与reg详解、常量定义、位宽概念、算术/逻辑/移位运算符
好,咱们今天聊聊Verilog里最基础、也最容易搞混的两个东西——wire和reg。还有常量怎么定义、位宽是怎么回事,以及那些天天用的运算符。
说实话,我刚开始学的时候,也纠结过“什么时候用wire,什么时候用reg”。后来踩了几个坑,才真正搞明白。今天我把这些经验都抖出来,你一次弄懂,后面写代码就顺了。
3.1 wire与reg:到底有啥区别?
先记住一句话:wire是连线,reg是变量。
听起来简单吧?但实际用起来,很多人就晕了。
3.1.1 wire——物理连线
wire说白了就是一根导线。它不存储值,只是把两个端口连起来。你给它什么,它就传什么。
// 典型用法:模块端口连接
wire a, b;
wire sum;
assign sum = a ^ b; // 用assign赋值,必须用wire
我个人习惯:只要是用assign连续赋值的,左边必须是wire。这是硬性规定,没得商量。
核心规则:
- wire只能在
assign语句或模块端口连接中被赋值 - wire不能存储值,它是组合逻辑
- wire的默认值是高阻态
Z(多驱动时)或X(未驱动时)
3.1.2 reg——寄存器变量
reg不是真正的寄存器,它只是一个变量。它可以在always块里被赋值,并且能保持上一次的值。
// 典型用法:时序逻辑
reg [7:0] counter;
always @(posedge clk) begin
if (rst)
counter <= 8'd0;
else
counter <= counter + 1;
end
嗯,这里要注意:reg不一定综合成寄存器。如果你在always块里写的是组合逻辑(比如always @(*)),那reg综合出来就是连线,不是触发器。
我曾经踩过的坑:
有一次我写了一个always @(posedge clk)块,里面用了reg变量,但忘记给所有分支赋值。结果综合出来一堆锁存器(latch),面积大了30%。从那以后,我养成了习惯——时序逻辑里一定给reg赋默认值。
3.1.3 什么时候用哪个?
| 场景 | 用wire | 用reg |
|---|---|---|
| assign连续赋值 | ✅ 必须 | ❌ 不行 |
| always @(posedge clk) | ❌ 不行 | ✅ 必须 |
| always @(*) | ❌ 不行 | ✅ 必须 |
| 模块输入端口 | ✅ 默认 | ❌ 不行 |
| 模块输出端口 | ✅ 可以 | ✅ 可以 |
你想想看,如果输出端口在always块里赋值,那就得声明成output reg。如果只是assign赋值,那就用output wire。这个区分很重要。
3.2 常量定义:parameter与localparam
写代码最忌讳“魔法数字”。比如counter <= counter + 8'd100,这个100是什么意思?没人知道。
所以我们要用常量定义。Verilog里有两个关键字:parameter和localparam。
3.2.1 parameter——可被外部修改
parameter可以在模块实例化时被覆盖。适合做可配置的参数。
// 定义
module counter #(
parameter WIDTH = 8,
parameter MAX_VAL = 255
) (
input clk, rst,
output reg [WIDTH-1:0] count
);
// 实例化时修改
counter #(
.WIDTH(16),
.MAX_VAL(65535)
) u_counter (...);
我个人习惯:所有可配置的常量都用parameter。比如数据位宽、FIFO深度、计数器最大值。
3.2.2 localparam——模块内部私有
localparam不能被外部修改。它只在本模块内有效。
// 定义状态机的状态
localparam IDLE = 2'b00;
localparam READ = 2'b01;
localparam WRITE = 2'b10;
localparam DONE = 2'b11;
reg [1:0] state;
为什么用localparam?因为状态机的状态值不应该被外部乱改。用localparam就是告诉别人:“这是我的内部秘密,你别碰。”
我的建议:
能用localparam就别用parameter。只有当你确定这个参数需要被外部配置时,才用parameter。这样代码更安全,也更容易维护。
3.3 位宽概念——别小看它
位宽,说白了就是数据有多少根线。比如[7:0]就是8位宽,能表示0~255。
但位宽问题,是新手最容易出bug的地方。我见过太多因为位宽不匹配导致的仿真错误。
3.3.1 位宽声明
reg [7:0] data_8bit; // 8位宽,MSB是7,LSB是0
reg [0:7] data_rev; // 8位宽,MSB是0,LSB是7(不常用)
wire [15:0] bus; // 16位总线
我个人习惯用[高:低]的写法,也就是[7:0]。这样符合直觉——高位在左边,低位在右边。
3.3.2 位宽不匹配的后果
reg [7:0] a = 8'hFF; // 255
reg [3:0] b;
assign b = a; // b只取了低4位,变成4'hF
你看,a是255,b只有4位,结果b变成了15。这种错误在仿真里很难发现,因为工具不会报错,只是默默截断。
我曾经踩过的坑:
有一次做图像处理,一个像素值从12位截断到8位,结果图像颜色全乱了。查了两天才发现是位宽赋值时自动截断。从那以后,我所有跨位宽赋值都手动加&掩码或者用{}拼接。
3.3.3 位宽扩展
小位宽赋值给大位宽时,会自动扩展。无符号数补0,有符号数补符号位。
reg [3:0] small = 4'b1010;
reg [7:0] large;
assign large = small; // large = 8'b0000_1010
3.4 运算符详解
运算符这部分,说白了就是数学和逻辑。但Verilog里有些细节,不注意就会出错。
3.4.1 算术运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
| + | 加法 | a + b |
| - | 减法 | a - b |
| * | 乘法 | a * b |
| / | 除法 | a / b |
| % | 取模 | a % b |
注意:乘法和除法在FPGA里很贵。一个乘法器要消耗很多LUT。除法更夸张,综合出来可能几百个周期才能算完。
我一般这样处理:
- 乘2的幂次:用左移
<< - 除2的幂次:用右移
>> - 非2的幂次:用IP核或者查找表
3.4.2 逻辑运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
| && | 逻辑与 | a && b |
| || | 逻辑或 | a || b |
| ! | 逻辑非 | !a |
逻辑运算符的结果只有0或1。它把操作数当作布尔值处理——非0就是真,0就是假。
reg [3:0] a = 4'b1010;
reg [3:0] b = 4'b0000;
// 逻辑运算
result1 = a && b; // 1 && 0 = 0
result2 = a || b; // 1 || 0 = 1
result3 = !a; // !1 = 0
3.4.3 按位运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
| & | 按位与 | a & b |
| | | 按位或 | a | b |
| ^ | 按位异或 | a ^ b |
| ~ | 按位取反 | ~a |
按位运算符是对每一位独立操作。结果位宽和操作数相同。
reg [3:0] a = 4'b1010;
reg [3:0] b = 4'b1100;
// 按位运算
result1 = a & b; // 4'b1000
result2 = a | b; // 4'b1110
result3 = a ^ b; // 4'b0110
result4 = ~a; // 4'b0101
逻辑 vs 按位,别搞混:
&&和&完全不同。前者是逻辑与,结果只有0或1;后者是按位与,结果和操作数位宽相同。我见过有人用if (a && b)写成了if (a & b),仿真结果完全不对。
3.4.4 移位运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
| << | 左移 | a << 2 |
| >> | 右移 | a >> 2 |
移位运算符在FPGA里是零成本的。因为它只是重新连线,不消耗任何逻辑资源。
reg [7:0] a = 8'b0000_1010; // 10
// 左移1位 = 乘2
result1 = a << 1; // 8'b0001_0100 = 20
// 右移2位 = 除4
result2 = a >> 2; // 8'b0000_0010 = 2
嗯,这里要注意:右移对于有符号数,是算术右移还是逻辑右移? Verilog默认是逻辑右移(补0)。如果你需要算术右移(补符号位),要用>>>运算符。
reg signed [7:0] a = 8'b1000_1010; // -118
// 逻辑右移
result1 = a >> 2; // 8'b0010_0010 = 34
// 算术右移
result2 = a >>> 2; // 8'b1110_0010 = -30
3.5 知识体系结构图
下面这张图,把本章的知识点串起来了。你可以把它当作一个快速索引。
这张图把本章的知识点分成了两大块:数据类型和运算符。数据类型下面又分了wire、reg和常量定义。运算符分了算术、逻辑、按位和移位。位宽概念是贯穿始终的基础,所以放在最下面。
总结一下我的经验:
- wire和reg的选择,记住“assign用wire,always用reg”就够了
- 常量定义优先用localparam,只有需要外部配置时才用parameter
- 位宽问题,多检查、多仿真,别相信自动截断
- 运算符里,移位是最便宜的,乘除是最贵的
好了,这一章的内容就到这儿。你把这些基础打牢了,后面写代码就会顺手很多。
公众号:蓝海资料掘金营,微信deep3321