4. Verilog基础(二):运算符、条件语句、循环语句、任务与函数
好,我们接着往下聊。上一章我们把Verilog的基本结构、数据类型和模块化设计捋了一遍。这一章,咱们要深入一点,聊聊那些真正让代码“活”起来的元素——运算符、条件判断、循环,还有任务和函数。
这些东西,说白了就是Verilog的“语法肌肉”。你光知道怎么声明一个wire、一个reg,那只是搭了个空架子。真正让电路按照你的意图去工作,靠的就是这些控制结构和运算逻辑。
我个人习惯,在写RTL代码之前,会先在脑子里过一遍:这个功能,用组合逻辑还是时序逻辑?用if-else还是case?需不需要写个function来复用?想清楚了再动键盘,效率会高很多。
4.1 运算符:不只是加减乘除
Verilog的运算符,跟C语言很像,但有些地方你得特别小心。我刚开始学的时候,就吃过“位宽不匹配”的亏。
4.1.1 算术运算符
加减乘除(+、-、*、/)、取模(%)、幂运算(**)。这些都很直观。但有一点要注意:除法器和取模器在硬件里非常昂贵。你写个“a / b”,综合工具会给你生成一个庞大的组合逻辑除法器,面积大、延迟高。
4.1.2 位运算符与归约运算符
位运算符:按位与(&)、按位或(|)、按位异或(^)、按位取反(~)。这些是数字电路的基础,没什么好说的。
归约运算符:这是Verilog的特色。它把一个向量的所有位“归约”成1位。比如:
wire [7:0] data;
wire all_ones;
assign all_ones = &data; // 等价于 data[7] & data[6] & ... & data[0]
我个人特别喜欢用归约异或(^data)来检查奇偶校验。在项目中,我曾经用一行代码就搞定了整个数据包的奇偶校验逻辑,简洁又高效。
4.1.3 逻辑运算符与关系运算符
逻辑运算符(&&、||、!)返回的是布尔值(0或1)。关系运算符(>、<、>=、<=、==、!=)也一样。这里有个坑:全等运算符(===)和不全等运算符(!==)。
普通相等(==)在比较时,如果某一位是x或z,结果就是x(未知)。而全等运算符(===)会把x和z当作具体的值来比较。这在仿真调试时非常有用。
4.1.4 移位运算符
左移(<<)、右移(>>)、算术右移(>>>)。算术右移会保留符号位,这在有符号数运算中很关键。
嗯,这里要注意:移位运算符综合出来的就是连线,不占任何逻辑资源。所以,能用移位实现的乘除,千万别用乘除运算符。
4.1.5 拼接与复制运算符
拼接运算符({})和复制运算符({{}})是Verilog的“瑞士军刀”。
wire [7:0] a, b;
wire [15:0] c;
assign c = {a, b}; // 拼接
assign d = {4{a}}; // 复制4次
你想想看,用一行代码就能把多个信号拼成一个总线,或者把一个信号扩展成多位,是不是很方便?
4.2 条件语句:if-else 与 case
条件语句是RTL设计的灵魂。没有它们,你的电路就是一条直线,不会“思考”。
4.2.1 if-else 语句
if-else的用法跟C语言几乎一样。但在硬件里,它综合出来的是优先级编码器。
always @(*) begin
if (sel == 2'b00)
out = a;
else if (sel == 2'b01)
out = b;
else if (sel == 2'b10)
out = c;
else
out = d;
end
这段代码综合出来,sel[0]的优先级最高,sel[1]次之。为什么?因为if-else是串行判断的。这在某些场景下是好事,但如果你想要一个“并行选择”的效果,就得用case。
4.2.2 case 语句
case语句综合出来的是多路选择器(MUX),所有分支是并行的。
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
default: out = '0;
endcase
end
这里有个非常重要的点:case语句必须写全所有分支,或者加上default。否则,综合工具会给你生成一个“锁存器(latch)”。
我曾经在项目中排查过一个诡异的bug:某个信号在仿真时正常,但芯片回来后就是不对。最后发现,是case语句少写了一个分支,综合工具默默地生成了一个latch。从那以后,我写case必加default。
4.2.3 casex 与 casez
这两个是case的变种,允许在比较时忽略某些位。casez把z当作“无关位”,casex把x和z都当作“无关位”。
说实话,我在实际项目中很少用casex和casez。因为它们容易掩盖设计中的x态传播问题。如果你真的需要“无关位”比较,我建议用casez,并且只在仿真中使用。
4.3 循环语句:for、while、repeat、forever
循环语句在Verilog里,主要用于生成语句(generate)和testbench。在RTL代码里,for循环用得比较多,但要注意它的综合行为。
4.3.1 for 循环
for循环在RTL中是可综合的,但前提是循环次数必须是常数。
always @(*) begin
integer i;
for (i = 0; i < 8; i = i + 1) begin
parity = parity ^ data[i];
end
end
这段代码综合出来,就是8个异或门串联。for循环在综合时会被“展开”成并行逻辑。所以,循环次数不能太大,否则会生成巨大的组合逻辑。
4.3.2 while、repeat、forever
while和repeat在RTL中不可综合,只能在testbench里用。forever也一样,主要用于生成时钟和激励。
initial begin
forever #5 clk = ~clk; // 生成周期为10的时钟
end
你想想看,while循环的条件在硬件里没法“动态判断”,所以综合工具直接拒绝。记住这个原则:RTL代码里,循环次数必须是编译时就能确定的常数。
4.4 任务(task)与函数(function)
任务和函数,是Verilog里实现代码复用的两种方式。它们很像,但有几个关键区别。
4.4.1 函数(function)
函数的特点:
- 必须返回一个值
- 不能包含时序控制(如#delay、@posedge)
- 至少有一个输入参数
- 在RTL中可综合
function [7:0] parity_gen;
input [7:0] data;
integer i;
begin
parity_gen = 0;
for (i = 0; i < 8; i = i + 1)
parity_gen = parity_gen ^ data[i];
end
endfunction
调用时:assign parity = parity_gen(data_in);
我个人习惯,把一些简单的、组合逻辑的、需要复用的计算,写成function。比如奇偶校验、格雷码转换、位宽匹配等。
4.4.2 任务(task)
任务的特点:
- 可以有多个输出,也可以没有返回值
- 可以包含时序控制
- 可以有input、output、inout参数
- 在RTL中不可综合(除非是纯组合逻辑的任务)
task spi_write;
input [7:0] addr;
input [7:0] data;
begin
cs_n = 0;
sclk = 0;
// 发送地址
for (i = 7; i >= 0; i = i - 1) begin
mosi = addr[i];
#10 sclk = 1;
#10 sclk = 0;
end
// 发送数据
for (i = 7; i >= 0; i = i - 1) begin
mosi = data[i];
#10 sclk = 1;
#10 sclk = 0;
end
cs_n = 1;
end
endtask
这个任务只能在testbench里用,因为它有时序控制(#10)。在RTL里,你不能这么写。
4.4.3 自动(automatic)任务与函数
默认情况下,任务和函数里的变量是静态的(static)。如果你在多个地方同时调用同一个任务或函数,静态变量会互相干扰。
解决办法:加上automatic关键字。
function automatic [7:0] parity_gen;
// 内部变量是自动的,每次调用独立
endfunction
嗯,这里要注意:在SystemVerilog里,默认就是automatic的。但如果你还在用老式的Verilog-95/2001,建议养成加automatic的习惯。
4.5 避坑指南与实战建议
聊了这么多,最后总结几条我踩过的坑:
- if-else不写全,综合出latch:这是最常见的RTL bug。记住,组合逻辑里,所有条件分支都必须覆盖。
- case不加default,同样出latch:哪怕你觉得所有情况都覆盖了,也请加上default。因为仿真时可能会有x态。
- 在RTL里用除法器:除非你明确知道自己在做什么,否则别用“/”和“%”。用移位和加法替代。
- 循环次数不是常数:综合工具会直接报错,或者生成一个巨大的组合逻辑。
- 任务里有时序控制,却用在RTL中:综合工具会报错,或者忽略时序控制,导致仿真和综合不一致。
我曾经在一个项目中,因为case语句少写了一个default,导致芯片在特定条件下输出错误。排查了整整两天,最后用形式验证工具才找到问题。从那以后,我写代码时,每写一个case,就立刻补上default,已经成了肌肉记忆。
好了,这一章的内容就到这里。运算符、条件语句、循环、任务与函数,这些是Verilog的“语法肌肉”。多写、多练、多踩坑,你就能慢慢找到感觉。下一章,我们会聊聊更高级的话题——状态机设计与时序控制。