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