第三章:数据类型与运算符——wire与reg的区别,常量与参数,位宽的概念,算术、逻辑、移位运算符
各位同学,欢迎来到第三章。这一章的内容,是Verilog的“语法地基”。你想想看,盖房子如果地基没打好,后面肯定要出问题。同样,如果对数据类型和运算符理解不透彻,写出来的代码要么综合不过,要么仿真结果跟你想的完全不一样。
我个人习惯,在讲任何新知识点之前,先问自己一个问题:“这个东西在硬件里到底长什么样?” 搞清楚了物理意义,代码就不会写错。咱们今天就从最基础的两个数据类型开始:wire 和 reg。
3.1 wire与reg:硬件世界的“导线”与“寄存器”
很多初学者最困惑的就是这两个东西。我刚开始学的时候也绕了很久。其实说白了,它们就是硬件里两种最基本的“容器”。
3.1.1 wire——物理连线
wire 代表一根物理导线。它本身不存储任何值,只是把某个地方的信号“传递”到另一个地方。比如你把一个模块的输出,直接连到另一个模块的输入,中间用的就是 wire。
在代码里,wire 只能被 assign 语句驱动。你不能在 always 块里给 wire 赋值,因为 always 块是描述时序逻辑或组合逻辑的“行为”,而 wire 只是一个被动的连接。
// 正确用法:assign 驱动 wire
wire a, b, sum;
assign sum = a + b;
// 错误用法:不能在 always 块里给 wire 赋值
// always @(*) begin
// sum = a + b; // 编译报错!
// end
我的小经验: 在模块的端口声明里,input 和 output 默认就是 wire 类型。除非你明确声明为 reg,否则它们都是导线。
3.1.2 reg——数据存储单元
reg 代表一个寄存器,或者更准确地说,是一个“数据存储单元”。它可以在时钟沿到来时保持住一个值,也可以用于描述组合逻辑中的“临时变量”。
注意,reg 并不一定综合成触发器(Flip-Flop)。如果你在 always @(*) 块里给 reg 赋值,它综合出来就是组合逻辑;如果你在 always @(posedge clk) 块里赋值,它综合出来就是时序逻辑(触发器)。
// 组合逻辑:reg 综合成导线或查找表
reg [3:0] result;
always @(*) begin
result = a + b;
end
// 时序逻辑:reg 综合成触发器
reg [7:0] counter;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 8'd0;
else
counter <= counter + 1'b1;
end
我曾经踩过的坑: 在 always @(posedge clk) 块里,如果你漏写了 else 分支,综合工具会推断出一个“锁存器”(Latch)。锁存器在FPGA里通常是不推荐的,因为它对时序不友好,而且容易产生毛刺。所以,写时序逻辑时,一定要把每个条件分支都写完整。
3.1.3 核心区别总结
| 特性 | wire | reg |
|---|---|---|
| 物理含义 | 物理连线 | 存储单元(寄存器或组合逻辑变量) |
| 驱动方式 | 只能被 assign 驱动 |
只能在 always 或 initial 块中赋值 |
| 默认值 | 高阻态 Z |
不定态 X |
| 综合结果 | 导线 | 触发器或组合逻辑(取决于写法) |
3.2 常量与参数:让代码“活”起来
写代码最忌讳“硬编码”。比如你写了一个计数器,计数值是100。如果下次需求改成200,你是不是要满世界找那个100然后改掉?太累了。这时候就需要 parameter 出场了。
3.2.1 常量:数字的表示法
Verilog里的常量,说白了就是数字。但它的写法有点讲究:位宽'进制数值。
- 位宽:用十进制数表示,比如
8表示8位宽。 - 进制:
b二进制,o八进制,d十进制,h十六进制。 - 数值:对应进制的数字。
// 各种常量写法
8'b1010_1010 // 8位二进制,下划线只是为了可读性
16'hFF00 // 16位十六进制
32'd100 // 32位十进制
4'o7 // 4位八进制
// 如果不指定位宽,默认是32位
'hFF // 等价于 32'h000000FF
注意: 常量里的 x 表示不定态,z 表示高阻态。比如 4'b10xz 表示第3位是1,第2位是0,第1位是不定,第0位是高阻。这在仿真时很有用,但综合时通常不会出现。
3.2.2 parameter:可配置的“常量”
parameter 是Verilog里实现“参数化设计”的关键。你可以在模块内部定义一个参数,然后在实例化时修改它。这样,同一个模块就能适应不同的需求。
// 定义一个带参数的计数器模块
module counter #(
parameter WIDTH = 8, // 位宽,默认8位
parameter MAX_VAL = 255 // 最大值,默认255
) (
input clk,
input rst_n,
output reg [WIDTH-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= {WIDTH{1'b0}};
else if (count == MAX_VAL)
count <= {WIDTH{1'b0}};
else
count <= count + 1'b1;
end
endmodule
// 实例化时修改参数
counter #(
.WIDTH(16),
.MAX_VAL(65535)
) u_counter (
.clk(clk),
.rst_n(rst_n),
.count(count_16bit)
);
我个人习惯,在项目一开始就把所有可能变化的数值都定义成 parameter。比如时钟分频系数、FIFO深度、数据位宽等等。这样后期改需求时,只需要改一个地方,省心又省力。
3.3 位宽的概念:别让你的数据“溢出”
位宽,就是二进制数的位数。比如一个 wire [7:0] data,表示 data 是一个8位宽的信号,取值范围是0到255(无符号数)。
为什么位宽这么重要?因为FPGA里的资源是有限的。你每多用一位,就多消耗一份逻辑资源。而且,位宽不匹配会导致数据截断或符号扩展,仿真时可能看不出问题,但上板子就出错了。
// 位宽不匹配的例子
wire [7:0] a = 8'd200;
wire [3:0] b;
assign b = a; // b 只能得到 a 的低4位,即 8'b1100_1000 的低4位 8'b1000 = 8
// 正确的做法:显式截断或扩展
assign b = a[3:0]; // 只取低4位
我曾经犯过的错: 有一次做图像处理,把两个8位像素相加,结果忘了结果需要9位才能存下。仿真时一切正常,但上板子后图像出现了奇怪的条纹。查了半天才发现是溢出导致的高位丢失。从那以后,我每次做运算前都会先算一下结果的最大位宽。
3.4 运算符:算术、逻辑、移位
运算符是Verilog的“动词”,告诉硬件要做什么操作。咱们重点讲三类:算术、逻辑、移位。
3.4.1 算术运算符
加减乘除,跟C语言差不多。但要注意,除法 / 和取模 % 在综合时会消耗大量资源,能不用尽量不用。如果非要除,建议用移位代替(比如除以2就是右移1位)。
wire [7:0] a = 8'd10, b = 8'd3;
wire [7:0] sum, diff, prod, quot, rem;
assign sum = a + b; // 13
assign diff = a - b; // 7
assign prod = a * b; // 30
assign quot = a / b; // 3(整数除法)
assign rem = a % b; // 1(取余)
3.4.2 逻辑运算符
逻辑运算符返回的是 1 或 0,用于条件判断。注意区分 &&(逻辑与)和 &(按位与),前者返回布尔值,后者返回按位运算结果。
wire [3:0] x = 4'b1010, y = 4'b1100;
wire logic_and, logic_or, logic_not;
assign logic_and = x && y; // 1(因为 x 和 y 都不为0)
assign logic_or = x || y; // 1
assign logic_not = !x; // 0(因为 x 不为0)
// 按位运算
wire [3:0] bit_and, bit_or, bit_xor;
assign bit_and = x & y; // 4'b1000
assign bit_or = x | y; // 4'b1110
assign bit_xor = x ^ y; // 4'b0110
我的习惯: 在 if 条件里,我通常用 if (signal) 而不是 if (signal == 1'b1)。前者更简洁,而且综合结果一样。但要注意,如果 signal 是多位宽,if (signal) 等价于 if (signal != 0)。
3.4.3 移位运算符
移位运算符 << 和 >> 在硬件里就是“布线”,不消耗任何逻辑资源。左移一位相当于乘以2,右移一位相当于除以2(无符号数)。
wire [7:0] data = 8'b0000_1011; // 十进制11
wire [7:0] left_shift, right_shift;
assign left_shift = data << 2; // 8'b0010_1100 = 44(11*4)
assign right_shift = data >> 1; // 8'b0000_0101 = 5(11/2,整数除法)
嗯,这里要注意:对于有符号数,右移是算术右移还是逻辑右移?Verilog里,如果你把信号声明为 signed,右移会进行符号扩展;否则就是补0。我建议,除非你明确需要符号运算,否则一律用无符号数,省得搞混。
3.5 本章小结
这一章的内容,说白了就是Verilog的“语法基础”。wire 和 reg 是两种最基本的类型,parameter 让代码可配置,位宽决定了数据的范围,而运算符则是你操作数据的工具。
我个人觉得,学Verilog就像学一门新语言。刚开始可能觉得语法繁琐,但只要你多写、多仿真、多上板子调试,慢慢就会形成“硬件思维”。下一章我们会讲“组合逻辑与时序逻辑”,这是FPGA设计的核心,敬请期待。