4. Verilog基础语法(下):参数化设计、generate语句、任务与函数、状态机基础

各位工程师朋友,大家好。上一讲我们聊了Verilog的基础语法,今天咱们继续往下挖。说实话,很多初学者觉得语法就是背规则,其实不然。语法背后藏着的是硬件设计的思维模式。今天要讲的这几个知识点——参数化设计、generate语句、任务与函数、状态机——都是实战中天天要用的硬功夫。

4.1 参数化设计:让代码活起来

先说说参数化设计。我刚开始做CPLD项目时,经常遇到这种情况:一个模块写死了位宽,换项目就得重新改代码。后来我学乖了,用parameter来定义常数。

说白了,parameter就是Verilog里的常量定义。但它比C语言的#define灵活得多。你可以把它放在模块内部,也可以从上层模块传进来。

核心要点:参数化设计让代码可复用,一个模块能适应不同位宽、不同深度的需求。

// 一个参数化的计数器模块
module counter #(
    parameter WIDTH = 8,      // 默认8位
    parameter MAX_VAL = 255   // 默认最大值
)(
    input  clk,
    input  rst_n,
    input  en,
    output reg [WIDTH-1:0] count
);

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        count <= 0;
    else if (en) begin
        if (count == MAX_VAL)
            count <= 0;
        else
            count <= count + 1;
    end
end

endmodule

你看,这个计数器模块,位宽和最大值都是可调的。实例化时这样写:

// 实例化一个12位、最大值为4095的计数器
counter #(
    .WIDTH(12),
    .MAX_VAL(4095)
) u_counter (
    .clk(clk),
    .rst_n(rst_n),
    .en(en),
    .count(count_12b)
);

我个人习惯把参数化设计用在两个地方:一是数据位宽,二是状态机的状态编码。这样做的好处是,项目后期改需求时,改一个参数就行,不用满世界找代码改。

小技巧:参数名用大写字母,比如WIDTH、DEPTH,这样一眼就能看出是参数,不是信号。

4.2 generate语句:批量生成硬件

接下来是generate语句。这个语法我特别喜欢,因为它能帮我们批量生成重复的硬件结构。

generate有三种形式:generate for、generate if、generate case。最常用的是generate for,说白了就是硬件版的for循环。

// 用generate生成8个D触发器
module shift_reg #(
    parameter WIDTH = 8
)(
    input  clk,
    input  rst_n,
    input  [WIDTH-1:0] d,
    output [WIDTH-1:0] q
);

genvar i;
generate
    for (i = 0; i < WIDTH; i = i + 1) begin : gen_dff
        always @(posedge clk or negedge rst_n) begin
            if (!rst_n)
                q[i] <= 1'b0;
            else
                q[i] <= d[i];
        end
    end
endgenerate

endmodule

嗯,这里要注意一点:generate for循环里的变量要用genvar类型声明,不能用integer。我刚开始学的时候在这上面栽过跟头,综合器报了一堆错。

避坑指南:我曾经在generate块里用了always @(*)组合逻辑,结果综合出来的面积比预期大了三倍。后来发现,generate for生成的是并行硬件,不是软件循环。每个迭代都是独立的硬件实例。

generate if和generate case用得少一些,主要用在根据参数选择不同实现方式的场景。比如:

generate
    if (USE_PIPE) begin : pipe_impl
        // 流水线实现
    end else begin : non_pipe_impl
        // 非流水线实现
    end
endgenerate

4.3 任务与函数:代码模块化

任务(task)和函数(function)是Verilog里实现代码复用的两种方式。很多人分不清它们的区别,我简单说说。

特性 函数(function) 任务(task)
返回值 必须有返回值 无返回值,通过输出端口传结果
时序控制 不能包含时序控制(#、@、wait) 可以包含时序控制
调用方式 作为表达式的一部分 作为独立的语句
可综合性 可综合(有限制) 部分可综合

函数适合做纯组合逻辑的计算,比如:

function [7:0] parity;
    input [7:0] data;
    integer i;
    begin
        parity = 0;
        for (i = 0; i < 8; i = i + 1)
            parity = parity ^ data[i];
    end
endfunction

任务则适合做需要多个步骤的操作,比如读写外部存储器:

task read_mem;
    input [15:0] addr;
    output [7:0] data;
    begin
        @(posedge clk);
        mem_cs <= 1;
        mem_addr <= addr;
        @(posedge clk);
        data = mem_data;
        mem_cs <= 0;
    end
endtask

个人经验:我在项目中尽量用函数,少用任务。因为任务里容易不小心写出不可综合的代码。如果一定要用任务,我会在注释里标明"可综合"。

4.4 状态机基础:硬件设计的灵魂

状态机是CPLD设计的核心。你想想看,CPLD里大部分控制逻辑,说白了就是状态机。我做过一个项目,整个CPLD就一个状态机,跑了三年没出过问题。

状态机分两种:Moore型和Mealy型。Moore型的输出只取决于当前状态,Mealy型的输出还取决于输入。实战中Moore型用得更多,因为时序好分析。

标准的写法是三段式:

// 三段式状态机示例
module fsm_example (
    input  clk,
    input  rst_n,
    input  start,
    input  done,
    output reg [1:0] state_out
);

// 状态编码
localparam IDLE  = 2'b00;
localparam START = 2'b01;
localparam BUSY  = 2'b10;
localparam DONE  = 2'b11;

reg [1:0] current_state, next_state;

// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_state <= IDLE;
    else
        current_state <= next_state;
end

// 第二段:次态逻辑
always @(*) begin
    next_state = current_state;  // 默认保持
    case (current_state)
        IDLE:   if (start) next_state = START;
        START:  next_state = BUSY;
        BUSY:   if (done)  next_state = DONE;
        DONE:   next_state = IDLE;
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑
always @(*) begin
    state_out = current_state;  // 简单输出当前状态
end

endmodule

为什么推荐三段式?因为它的结构清晰,综合工具容易优化。第一段是时序逻辑,第二段和第三段是组合逻辑。这样写,时序约束也好做。

重要提醒:状态机一定要有default分支,而且default要回到IDLE或一个安全状态。我曾经遇到过状态机跑到非法状态,整个系统卡死的故障。从那以后,我每个状态机都加default处理。

状态编码也有讲究。小状态机用二进制编码,省触发器。大状态机(超过16个状态)用独热码(one-hot),省组合逻辑。我一般8个状态以下用二进制,以上用独热码。

4.5 本章知识体系

下面这张图总结了本章的核心内容,你可以对照着梳理一下思路:

Verilog基础语法(下)知识体系 参数化设计 parameter定义 参数传递 默认值设置 localparam 提高代码复用性 generate语句 generate for generate if generate case genvar变量 批量生成硬件结构 任务与函数 function(函数) task(任务) 可综合性区别 自动/静态 代码模块化 状态机基础 Moore型/Mealy型 三段式写法 状态编码 安全状态 控制逻辑核心 核心目标:写出可复用、可维护、可综合的硬件描述代码 参数化 → 灵活配置 | generate → 批量生成 | 任务函数 → 模块化 | 状态机 → 控制逻辑 实战建议:每个项目至少用一次参数化设计和generate,养成习惯

好了,这一讲的内容就到这里。参数化设计让代码灵活,generate语句批量生成硬件,任务和函数帮我们模块化代码,状态机则是控制逻辑的核心。这些知识点,你在实际项目中都会反复用到。

记住一点:写Verilog不是写软件,你是在描述硬件。每一行代码,都要想想它综合出来是什么电路。养成这个习惯,你的设计水平会提升很快。


公众号:蓝海资料掘金营,微信deep3321