第4章 Verilog数据类型与运算符
各位同学,今天我们来聊聊Verilog里最基础、也最绕不开的东西——数据类型和运算符。说实话,我刚学Verilog那会儿,觉得这些东西太简单了,不就是几个关键字嘛。结果呢?写出来的代码要么综合不过,要么仿真结果跟预期差十万八千里。后来我才明白,这些基础概念没吃透,后面写复杂逻辑就是给自己挖坑。
4.1 wire与reg:两个最常用的数据类型
先说说wire和reg。这两个东西,我见过太多新手搞混了。其实没那么复杂,你记住一句话:wire是连线,reg是存储。
wire,说白了就是一根物理导线。它不保存值,只是把驱动端的信号传递到接收端。在always块外面赋值,或者用assign语句,都得用wire。我有个习惯——只要是不需要保存状态的信号,一律用wire。
wire使用场景:
- 模块端口连接
- 组合逻辑输出(assign语句)
- 连续赋值
reg就不一样了。它能在时钟边沿保存值,说白了就是个寄存器。但注意啊,reg不一定非得综合成寄存器——如果你在always块里写组合逻辑,reg综合出来可能只是一堆门电路。嗯,这里要注意:reg只代表行为描述中的变量,不代表硬件一定是寄存器。
// wire示例
wire a, b;
wire sum;
assign sum = a + b;
// 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
我的小技巧:在模块端口声明时,input/output默认是wire类型。如果你想让输出是reg,得显式声明。我一般习惯把组合逻辑的输出用wire,时序逻辑的输出用reg,这样代码一看就明白。
4.2 integer与parameter:常数与参数化设计
integer,这东西在仿真里用得比较多。它是32位有符号整数,说白了就是个通用变量。我在写testbench时经常用它做循环计数器。但注意,integer不能直接用于综合——至少我建议你别这么干,综合工具对它的支持不太好。
integer i;
always @(posedge clk) begin
for (i = 0; i < 16; i = i + 1) begin
// 做一些操作
end
end
parameter,这个才是我们做工业控制FPGA设计的核心。参数化设计,说白了就是让代码能复用。我做过一个项目,需要支持不同位宽的SPI接口,要是每个位宽都写一套代码,那不得累死?用parameter,改一个值就行。
// 参数化设计示例
module spi_master #(
parameter DATA_WIDTH = 8,
parameter CLK_DIV = 4
) (
input wire clk,
input wire rst_n,
input wire [DATA_WIDTH-1:0] data_in,
output reg [DATA_WIDTH-1:0] data_out
);
// 内部逻辑
endmodule
我曾经踩过的坑:parameter的位宽一定要算清楚。有一次我定义了一个参数WIDTH=8,结果后面用的时候忘了加范围,直接写了个reg [WIDTH-1:0] data,综合出来位宽不对,查了半天才发现是参数没展开。所以我现在写参数化代码,都会在注释里标明默认值和取值范围。
4.3 算术运算符:加减乘除与取模
算术运算符,大家应该都熟悉。但FPGA里用这些运算符,跟软件里完全不是一回事。你想想看,软件里写个a*b,CPU帮你算好了。FPGA里写个a*b,综合工具得给你生成一个乘法器——这玩意儿占资源啊!
| 运算符 | 含义 | 综合资源 |
|---|---|---|
| + | 加法 | 加法器(LUT+进位链) |
| - | 减法 | 加法器(补码实现) |
| * | 乘法 | DSP或LUT(看位宽) |
| / | 除法 | 非常耗资源,慎用 |
| % | 取模 | 同除法,慎用 |
我在工业控制项目里,加减法用得最多。乘除法嘛,能不用就不用。实在要用除法,我一般会换成移位或者查表。比如除以2,直接右移一位就行。除以10?我可能会用查找表或者CORDIC算法。
// 加减法示例
reg [7:0] a, b, sum;
always @(*) begin
sum = a + b; // 组合逻辑加法
end
// 乘法示例(用DSP实现)
reg [15:0] product;
always @(posedge clk) begin
product <= a * b; // 时序逻辑乘法,综合工具会自动调用DSP
end
我的建议:如果你需要做乘除法,尽量让操作数位宽小一点。8位乘8位,一个DSP就够了。16位乘16位,可能得两个DSP拼起来。另外,除法器最好用IP核,别自己写,容易出问题。
4.4 逻辑运算符:与或非
逻辑运算符,处理的是布尔值。说白了,就是true和false。在Verilog里,0是假,非0是真。逻辑运算符返回的结果也是布尔值——1表示真,0表示假。
| 运算符 | 含义 | 示例 |
|---|---|---|
| && | 逻辑与 | a && b(a和b都为真时结果为真) |
| || | 逻辑或 | a || b(a或b有一个为真即为真) |
| ! | 逻辑非 | !a(a为假时结果为真) |
逻辑运算符最常用的地方就是条件判断。比如if语句里,或者三目运算符里。我写状态机的时候,经常用逻辑运算符组合多个条件。
// 逻辑运算符示例
reg [3:0] state;
wire start, done, error;
always @(posedge clk) begin
if (start && !done) begin
// 开始且未完成
state <= state + 1'b1;
end
if (error || done) begin
// 出错或完成
state <= 4'd0;
end
end
注意:逻辑运算符跟位运算符不一样。逻辑运算符把操作数当成整体判断真假,位运算符是对每一位分别操作。这个区别很重要,我见过有人把&&写成&,结果仿真结果完全不对。
4.5 位运算符:按位操作
位运算符,这才是FPGA的强项。你想想看,CPU处理位操作还得一条条指令,FPGA直接硬件并行,一个时钟周期搞定所有位。
| 运算符 | 含义 | 示例 |
|---|---|---|
| & | 按位与 | 4'b1010 & 4'b1100 = 4'b1000 |
| | | 按位或 | 4'b1010 | 4'b1100 = 4'b1110 |
| ^ | 按位异或 | 4'b1010 ^ 4'b1100 = 4'b0110 |
| ~ | 按位取反 | ~4'b1010 = 4'b0101 |
| >> | 右移 | 4'b1010 >> 1 = 4'b0101 |
| << | 左移 | 4'b1010 << 1 = 4'b0100 |
位运算符在工业控制里太常用了。比如解析一个控制字,每个bit代表不同的使能信号。或者做CRC校验,异或运算是核心。我记得有个项目,需要从32位数据总线里提取出8个控制信号,用位运算符一行代码搞定。
// 位运算符示例
reg [7:0] data;
wire [3:0] high_nibble, low_nibble;
assign high_nibble = data[7:4]; // 取高4位
assign low_nibble = data[3:0]; // 取低4位
// 设置特定位
reg [7:0] control_reg;
always @(posedge clk) begin
control_reg[0] <= enable_a; // bit0:使能A
control_reg[1] <= enable_b; // bit1:使能B
control_reg[7:2] <= 6'd0; // 其他位清零
end
// 移位实现乘除法
reg [7:0] value;
wire [7:0] value_x2 = value << 1; // 乘以2
wire [7:0] value_div2 = value >> 1; // 除以2
我曾经犯过的错:移位操作要注意符号位。有符号数的右移是算术右移,会补符号位。无符号数的右移是逻辑右移,补0。有一次我用有符号数做右移,结果高位一直补1,导致计算结果完全错误。所以我现在做移位,都会先确认操作数是有符号还是无符号。
4.6 运算符优先级与实战建议
运算符优先级,这东西我建议你别死记硬背。我的做法很简单——多用括号。你想想看,代码是给人看的,加个括号一目了然,何必去纠结优先级?
// 不推荐:依赖优先级
assign result = a + b * c >> 2;
// 推荐:加括号明确顺序
assign result = (a + (b * c)) >> 2;
我的实战经验:写Verilog代码,可读性比炫技重要。我见过有人写一行代码用了五六个运算符,看着像天书。这种代码,过两个月自己都看不懂。所以我的原则是:复杂运算拆成多行,每行只做一件事。这样仿真调试也方便,综合结果也更可控。
好了,这一章的内容就这些。数据类型和运算符是Verilog的基石,看似简单,但用好了能写出高效、可读性强的代码。下一章我们聊聊组合逻辑和时序逻辑,到时候会用到今天学的这些知识。有什么问题,欢迎随时交流。