4、Verilog基础语法(二):参数化设计、条件语句、循环与函数任务
各位同学,咱们接着聊Verilog。上一章我们把基础语法过了一遍,今天要讲的这几个东西,说实话,才是真正让你从「会写代码」变成「会设计代码」的分水岭。
我个人习惯把这一章叫做「代码的骨架与血肉」。参数化设计是骨架,让代码能灵活适应不同需求;条件语句是血肉,让逻辑有了判断能力;循环和函数任务,则是让你少写重复代码的利器。
好,咱们一个一个来。
4.1 参数化设计(parameter)—— 让代码学会「变通」
先问个问题:你写过一个计数器模块吗?肯定写过。那如果下次需求变了,计数上限从100变成1000,你怎么做?
最笨的办法:打开代码,把 100 改成 1000。再下次改成 5000,再改一次。改个三五次你还能忍,改到第十次,你肯定会想:「有没有一种办法,让我只改一个数字,整个模块就自动适配?」
有的。这就是 parameter。
核心思想:把设计中可能变化的常量提取出来,用参数代替。实例化时传入不同参数,就能得到不同行为的模块。
看个例子:
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 <= 0;
else if (count == MAX_VAL)
count <= 0;
else
count <= count + 1;
end
endmodule
实例化的时候,你可以这样用:
// 8位计数器,计数到255
counter #(.WIDTH(8), .MAX_VAL(255)) u_counter_8bit (...);
// 16位计数器,计数到65535
counter #(.WIDTH(16), .MAX_VAL(65535)) u_counter_16bit (...);
// 自定义:10位,计数到999
counter #(.WIDTH(10), .MAX_VAL(999)) u_counter_10bit (...);
你看,同一个模块,三种用法。这就是参数化的魅力。
我的经验:我在项目中遇到过一种情况——同一个电源管理芯片,不同客户对过压保护阈值要求不同。以前的做法是给每个客户单独维护一个版本,后来我用parameter把阈值做成参数,一个模块搞定所有客户。维护成本直线下降。
这里有个细节要注意:参数一旦在实例化时被覆盖,就是硬编码了。综合工具会把参数值直接代入,不会生成额外的逻辑。所以不用担心面积问题。
4.2 if-else 与 case 语句 —— 逻辑判断的两种武器
这两个东西,说白了就是让电路「做选择」的。但怎么选,有讲究。
4.2.1 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
但这里有个坑——优先级。if-else 天生带优先级,第一个条件优先级最高,最后一个最低。综合出来的电路是级联的,像一串多米诺骨牌。
避坑指南:我曾经在一个项目中用了一长串 if-else 做译码器,结果时序分析报告里那条路径的延迟特别大。后来换成 case 语句,延迟直接降了一半。为什么?因为 case 是并行的,没有优先级。
4.2.2 case 语句
case 语句长这样:
always @(*) begin
case (sel)
2'b00: out = a;
2'b01: out = b;
2'b10: out = c;
2'b11: out = d;
default: out = 'bx; // 建议加上default
endcase
end
case 语句综合出来是一个多路选择器(MUX),所有分支并行,没有优先级。所以延迟更小,更可控。
那什么时候用 if-else,什么时候用 case?
| 场景 | 推荐用 | 原因 |
|---|---|---|
| 条件有优先级(如中断优先级) | if-else | 天然支持优先级 |
| 条件互斥(如状态机、译码器) | case | 并行、延迟小 |
| 条件较少(2~3个) | if-else | 代码更直观 |
| 条件较多(4个以上) | case | 可读性好,综合结果优 |
小技巧:写 case 语句时,一定要写 default。哪怕你觉得所有情况都覆盖了,也要写。为什么?因为综合工具会认为你没覆盖的情况是「don't care」,可能会生成锁存器。default 可以避免这个问题。
4.3 for 循环与 generate 语句 —— 批量操作的两种姿势
写代码最烦什么?重复。比如你要例化 8 个相同的模块,或者对 32 位数据逐位做同样的操作。手写 8 遍?32 遍?不存在的。
4.3.1 for 循环
for 循环在 Verilog 里是 可综合 的,但有个前提:循环次数必须是常数。
// 对32位数据逐位取反
always @(*) begin
for (i = 0; i < 32; i = i + 1) begin
out[i] = ~in[i];
end
end
综合工具会把 for 循环展开,变成 32 个非门并联。所以不用担心「循环会不会占用很多时间」——它是在编译时展开的,不是运行时。
注意:for 循环里不能写 #delay,不能写 wait,不能写任何时序控制语句。否则综合工具会报错。
4.3.2 generate 语句
generate 是另一种批量操作的方式,但它更强大——它可以用来批量例化模块、批量生成 always 块、批量生成 assign 语句。
看个例子:
// 批量例化8个相同的模块
genvar i;
generate
for (i = 0; i < 8; i = i + 1) begin : gen_adder
adder #(.WIDTH(8)) u_adder (
.a(data_a[i*8 +: 8]),
.b(data_b[i*8 +: 8]),
.sum(data_sum[i*8 +: 8])
);
end
endgenerate
这里 genvar 是 generate 专用的变量类型。注意看 begin : gen_adder,这个标签很重要——它给每个生成的实例起了个名字,方便调试时定位。
for 循环和 generate 的区别?
- for 循环:用在 always 块内部,处理组合逻辑或时序逻辑
- generate for:用在模块级别,处理实例化、wire/reg 声明等
说白了,for 循环是「代码内部的重复」,generate 是「模块级别的重复」。
4.4 函数与任务 —— 把重复代码「封装」起来
写代码久了你会发现,有些逻辑片段反复出现。比如计算 CRC、做数据格式转换、判断某个条件是否满足。每次都重写一遍?太累了。
Verilog 提供了两种封装手段:函数(function) 和 任务(task)。
4.4.1 函数(function)
函数的特点:
- 只能返回一个值
- 不能包含时序控制(#delay、@、wait)
- 默认是组合逻辑
- 可综合
// 计算两个数的最大值
function [7:0] max;
input [7:0] a, b;
begin
if (a > b)
max = a;
else
max = b;
end
endfunction
// 使用
wire [7:0] result;
assign result = max(data1, data2);
函数可以嵌套调用,也可以递归(但综合工具不一定支持递归)。
4.4.2 任务(task)
任务的特点:
- 可以返回多个值(通过 output 端口)
- 可以包含时序控制
- 可以用于仿真,也可以综合(但有限制)
// 将8位数据拆成两个4位
task split_data;
input [7:0] data_in;
output [3:0] high, low;
begin
high = data_in[7:4];
low = data_in[3:0];
end
endtask
// 使用
reg [3:0] h, l;
always @(*) begin
split_data(data_in, h, l);
end
函数 vs 任务,怎么选?
- 只需要返回一个值 → 用函数
- 需要返回多个值 → 用任务
- 需要时序控制 → 用任务(但注意可综合性)
- 需要可综合 → 优先用函数,任务要谨慎
我的习惯:在电源管理的逻辑开发中,我经常用函数来做「阈值比较」和「状态编码转换」。比如把 ADC 采样值转换成电压值,写一个函数,到处调用。任务我用的少,因为大部分场景函数就够了。
本章小结
好,咱们捋一下今天的内容:
- parameter:让模块可配置,一个模块应对多种需求
- if-else vs case:if-else 有优先级,case 并行。根据场景选
- for 循环:always 块内的批量操作,可综合
- generate:模块级别的批量操作,适合例化
- 函数与任务:封装重复逻辑,函数更常用
这些东西,说白了就是让你写更少、更清晰的代码。但光看没用,得练。我建议你找个简单的项目,比如一个可配置的 PWM 发生器,把今天学的都用上。写一遍,比看十遍都管用。
嗯,今天就到这儿。下节课我们聊状态机——那才是 CPLD 逻辑设计的灵魂。
公众号:蓝海资料掘金营,微信deep3321