3、数据类型与运算符:wire与reg类型、向量与标量、常用运算符、运算符优先级与综合结果
说实话,数据类型和运算符这块,是很多新手FPGA工程师容易翻车的地方。我见过不少同事,写了几年Verilog,还在纠结什么时候用wire、什么时候用reg。今天咱们就把这事彻底聊透。
3.1 wire与reg:到底有什么区别?
先记住一句话:wire是连线,reg是变量。就这么简单。
wire类型,说白了就是一根物理导线。它只能被连续赋值语句(assign)驱动。你想想看,一根导线能存储值吗?不能。所以wire没有存储功能,它的值由驱动源实时决定。
reg类型呢?它代表一个存储单元。在always块里被赋值,可以保持状态。但注意——reg不一定会被综合成寄存器。这是个常见的误解。
关键点:reg只是语法上的变量类型,综合工具会根据你的代码逻辑,决定把它实现成寄存器还是连线。
举个例子你就明白了:
// 这个reg会被综合成寄存器
reg [3:0] counter;
always @(posedge clk) begin
counter <= counter + 1;
end
// 这个reg会被综合成连线
reg [3:0] result;
always @(*) begin
result = a + b;
end
我在项目中遇到过有人把所有的信号都定义成reg,觉得这样保险。结果综合报告里一堆warning,说某些reg被优化掉了。嗯,这就是没搞懂本质。
我的个人习惯:组合逻辑用wire,时序逻辑用reg。在always @(*)里赋值的信号,用reg;在assign语句里赋值的信号,用wire。这样代码可读性最好。
3.2 向量与标量:位宽的艺术
标量就是单比特信号,向量就是多比特信号。这个好理解。
wire a; // 标量,1位
wire [7:0] b; // 向量,8位
reg [31:0] c; // 向量,32位
这里有个坑——向量的位序。你写[7:0]还是[0:7],综合出来的结果完全不一样。我个人习惯统一用[MSB:LSB],也就是高位在左、低位在右。这样跟数据手册的标注方式一致,不容易出错。
我曾经接手过一个项目,前工程师混用了两种位序。结果在拼接运算时,数据对不上,查了整整两天。从那以后,我要求团队所有成员必须统一位序规范。
3.3 常用运算符:你每天都在用,但真的用对了吗?
运算符分几大类,我挑重点说:
算术运算符
+、-、*、/、%。注意除法在FPGA里很贵,能不用的地方尽量别用。我一般用移位代替乘除2的幂次。
逻辑运算符
&&、||、!。这些返回的是布尔值(0或1),用在条件判断里。
位运算符
&、|、^、~。这些是按位操作的,跟逻辑运算符要区分开。很多bug就出在这里——把&当成了&&用。
关系运算符
==、!=、>、<、>=、<=。注意==和===的区别。在仿真里,===可以比较x和z状态,但综合时==和===是一样的。
移位运算符
<<、>>。左移补0,右移补0。这是实现乘除法最经济的方式。
拼接运算符
{ }。这个很实用,可以把多个信号拼在一起。比如:
wire [7:0] high, low;
wire [15:0] combined = {high, low};
注意:拼接时所有操作数的位宽必须明确。我曾经见过有人写{a, b},结果a和b都是parameter定义的,综合出来位宽对不上,仿真和综合结果不一致。这种问题最难查。
3.4 运算符优先级:一个括号解决所有问题
说实话,我不建议任何人去背运算符优先级表。为什么?因为代码是给人看的,不是给编译器看的。
我的原则很简单:拿不准就加括号。多一个括号不会让综合结果变差,但少一个括号可能让逻辑出错。
不过,为了让你心里有数,这里还是列一下常见的优先级(从高到低):
| 优先级 | 运算符 | 说明 |
|---|---|---|
| 1(最高) | ! ~ & | ^ ~^ | 单目运算符 |
| 2 | * / % | 乘除取模 |
| 3 | + - | 加减 |
| 4 | << >> | 移位 |
| 5 | < <= > >= | 关系比较 |
| 6 | == != === !== | 相等比较 |
| 7 | & | 按位与 |
| 8 | ^ ~^ | 按位异或 |
| 9 | | | 按位或 |
| 10 | && | 逻辑与 |
| 11 | || | 逻辑或 |
| 12(最低) | ?: | 条件运算符 |
你看这个表,单目运算符优先级最高,条件运算符最低。中间那些,说实话,正常人记不住。所以——加括号。
3.5 运算符的综合结果:代码怎么写,电路就怎么长
综合工具很诚实。你写什么运算符,它就给你生成什么电路。
举个例子:
// 写法1:直接用乘法器
wire [7:0] a, b;
wire [15:0] product = a * b; // 综合出DSP或LUT乘法器
// 写法2:用移位实现乘2
wire [7:0] c;
wire [8:0] double = {c, 1'b0}; // 综合出连线,零资源消耗
你看,同样的功能,不同的写法,综合结果天差地别。这就是为什么我说代码风格直接影响电路质量。
我画了一张图,帮你理清本章的知识脉络:
这张图把本章的核心内容串起来了。从数据类型出发,到向量标量,再到运算符和优先级,最后落到综合结果。你看,所有知识点最终都指向同一个结论——代码怎么写,电路就怎么长。
避坑指南:我曾经在一个项目中,因为运算符优先级搞错,导致一个加法器的输入被截断了。仿真怎么跑都对,上板就是不对。后来用了一整天才定位到问题——就是少了一对括号。从那以后,我写代码的习惯就是:所有表达式都加括号,哪怕是最简单的a + b,我也写成(a + b)。
好了,数据类型和运算符这块,核心内容就这些。记住:wire和reg的本质区别、向量位序要统一、运算符优先级拿不准就加括号、代码风格直接影响综合结果。把这些吃透了,你写出来的代码质量会有质的提升。
公众号:蓝海资料掘金营,微信deep3321