3. Verilog基础语法(下):参数化设计、条件语句、循环语句、函数与任务
各位同学,咱们接着聊Verilog。上一节我们把基础语法扫了一遍,这一节要讲的东西,说白了就是让你写的代码更灵活、更聪明。我个人觉得,这部分内容才是真正拉开新手和老手差距的地方。
3.1 参数化设计:让模块学会“变通”
先问大家一个问题:你写了一个8位计数器,明天需求改成16位,怎么办?重新写一个?太累了。参数化设计就是干这个用的。
3.1.1 parameter vs localparam
这两个东西长得像,但用途不一样:
- parameter:模块对外暴露的参数,例化时可以修改。比如数据位宽、FIFO深度。
- localparam:模块内部用的常量,外部改不了。比如状态机的编码、查找表的值。
我个人习惯:能写成localparam的,绝不写成parameter。为什么?因为外部能改的参数越少,模块越不容易被用错。
核心原则:parameter是接口,localparam是内部实现。接口要精简,实现要封装。
3.1.2 实战:参数化计数器
module counter #(
parameter WIDTH = 8, // 位宽,默认8位
parameter MAX = 255 // 计数最大值
)(
input clk,
input rst_n,
output reg [WIDTH-1:0] cnt
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 0;
else if (cnt == MAX)
cnt <= 0;
else
cnt <= cnt + 1'b1;
end
endmodule
你看,例化的时候可以这样用:
counter #(
.WIDTH(16),
.MAX (50000)
) u_counter (
.clk (clk),
.rst_n(rst_n),
.cnt (cnt_16b)
);
我在项目中遇到过最典型的场景:一个I2C控制器,时钟分频系数、从机地址、数据位宽全部用parameter定义。后来从100kHz改到400kHz,改一行代码就搞定。
小技巧:参数名用大写,和信号名区分开。这是行业惯例,一眼就能看出哪些是参数。
3.2 条件语句:if-else 与 case
条件语句是数字电路里的“选择题”。你写if-else,综合器就给你生成比较器和多路选择器。
3.2.1 if-else 的优先级问题
if-else是有优先级的——第一个if优先级最高,最后一个else优先级最低。这会导致什么?综合出来的电路有优先级编码器,面积大、路径长。
// 不好的写法:有优先级
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
换成case,综合出来就是并行选择器:
// 好的写法:并行选择
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
避坑指南:我曾经在一个项目里用if-else写了8级优先级判断,结果时序跑不过200MHz。改成case后,直接干到350MHz。记住:并行逻辑用case,优先级逻辑才用if-else。
3.2.2 casex 与 casez:慎用!
这两个东西支持“无关位”匹配。比如casez可以把某些位忽略掉。听起来很爽对吧?但我在项目中吃过亏——仿真和综合结果不一致。因为casex/casez的综合行为,不同工具处理方式不一样。
我的建议:除非你非常清楚自己在干什么,否则只用case。需要无关位匹配?老老实实写多个case分支。
3.3 循环语句:for 与 generate
循环语句在Verilog里有点特殊——它不是“运行时循环”,而是“编译时展开”。说白了,就是帮你少写重复代码。
3.3.1 for 循环:用在always块里
// 用for循环实现8位数据反转
always @(*) begin
for (i = 0; i < 8; i = i + 1) begin
data_rev[i] = data_in[7-i];
end
end
综合出来是什么?8条赋值语句,没有循环器。你想想看,如果不用for,你得写8行assign。用for,3行搞定。
注意:for循环的边界必须是常数。你不能写 for (i=0; i<N; i=i+1) 然后N是个变量——综合器会报错。
3.3.2 generate:模块级别的“循环”
generate比for更强大,它可以生成模块例化、always块、assign语句。我常用它来做参数化的总线矩阵。
// 生成N个D触发器
genvar i;
generate
for (i = 0; i < N; i = i + 1) begin : dff_gen
dff u_dff (
.clk (clk),
.d (data_in[i]),
.q (data_out[i])
);
end
endgenerate
generate生成的每个实例都有一个名字,比如 dff_gen[0].u_dff、dff_gen[1].u_dff。这在调试时特别有用——你可以直接在波形里找到它们。
个人经验:generate和parameter配合使用,可以写出“自适应”的模块。比如一个N×N的交叉开关,N是参数,内部用generate生成所有交叉点。改一个参数,整个矩阵自动重配。
3.4 函数与任务:把重复代码“包”起来
写代码最忌讳什么?重复。同样的计算逻辑,你复制粘贴了5遍,然后发现有个bug要改——得改5个地方。函数和任务就是解决这个问题的。
3.4.1 function:纯组合逻辑,返回一个值
// 计算奇偶校验位
function parity;
input [7:0] data;
integer i;
begin
parity = 0;
for (i = 0; i < 8; i = i + 1) begin
parity = parity ^ data[i];
end
end
endfunction
// 使用
assign parity_bit = parity(data_in);
函数的特点:
- 不能包含时序控制(#delay、@、wait)
- 至少有一个输入,可以没有输出(函数名本身就是输出)
- 综合成组合逻辑
3.4.2 task:可以有时序,可以有多个输出
// 任务:产生一个SPI写操作
task spi_write;
input [7:0] addr;
input [7:0] data;
output done;
begin
cs_n = 0;
// 发送地址
for (i = 7; i >= 0; i = i - 1) begin
mosi = addr[i];
@(posedge sclk);
end
// 发送数据
for (i = 7; i >= 0; i = i - 1) begin
mosi = data[i];
@(posedge sclk);
end
cs_n = 1;
done = 1;
end
endtask
任务和函数的区别:
| 特性 | function | task |
|---|---|---|
| 时序控制 | 不允许 | 允许 |
| 返回值 | 一个(函数名) | 多个(output端口) |
| 综合 | 组合逻辑 | 组合或时序 |
| 调用位置 | 表达式 | 语句 |
避坑指南:我曾经在任务里写了@(posedge clk),然后在多个always块里同时调用这个任务。结果仿真时出现了竞争——两个always块在同一时钟沿触发了同一个任务。记住:任务里的时序控制要小心,别在多个地方同时调用。
3.5 本章知识体系
下面这张图,把这一节的内容串起来了。你仔细看看,参数化是“骨架”,条件语句是“判断”,循环语句是“批量”,函数任务是“封装”。四者配合,才能写出高质量的Verilog代码。
好了,这一节的内容就到这里。参数化让你的模块更通用,条件语句帮你做选择,循环语句省去重复劳动,函数任务让代码更整洁。这些都是我每天写代码都在用的东西,你练熟了,写Verilog就像搭积木一样简单。