第三章:数据类型与运算符——wire与reg的区别,常量与参数,位宽的概念,算术、逻辑、移位运算符

各位同学,欢迎来到第三章。这一章的内容,是Verilog的“语法地基”。你想想看,盖房子如果地基没打好,后面肯定要出问题。同样,如果对数据类型和运算符理解不透彻,写出来的代码要么综合不过,要么仿真结果跟你想的完全不一样。

我个人习惯,在讲任何新知识点之前,先问自己一个问题:“这个东西在硬件里到底长什么样?” 搞清楚了物理意义,代码就不会写错。咱们今天就从最基础的两个数据类型开始:wirereg

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

我的小经验: 在模块的端口声明里,inputoutput 默认就是 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 驱动 只能在 alwaysinitial 块中赋值
默认值 高阻态 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 逻辑运算符

逻辑运算符返回的是 10,用于条件判断。注意区分 &&(逻辑与)和 &(按位与),前者返回布尔值,后者返回按位运算结果。

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的“语法基础”。wirereg 是两种最基本的类型,parameter 让代码可配置,位宽决定了数据的范围,而运算符则是你操作数据的工具。

我个人觉得,学Verilog就像学一门新语言。刚开始可能觉得语法繁琐,但只要你多写、多仿真、多上板子调试,慢慢就会形成“硬件思维”。下一章我们会讲“组合逻辑与时序逻辑”,这是FPGA设计的核心,敬请期待。