4. Verilog进阶:任务与函数、generate语句、参数化设计、状态机编写
好,咱们继续往前走。前面几章我们把Verilog的基础语法和组合逻辑、时序逻辑都过了一遍。说实话,那些是基本功,就像练武要扎马步一样。但到了实际项目里,光会这些还不够。
这一章,我们来聊聊几个真正能让你的代码“活起来”的东西——任务与函数、generate语句、参数化设计,还有状态机。嗯,这些都是我每天写验证环境时几乎必用的技巧。
4.1 任务与函数:把重复劳动交给机器
先说说任务和函数。你想想看,写代码最烦什么?最烦的就是重复。同一个操作,比如给总线发个写命令,你每次都要写一堆assign和always,累不累?
任务(task)和函数(function)就是用来解决这个问题的。它们就像是你定义好的“小工具”,用的时候直接调用就行。
4.1.1 函数(function)
函数的特点是:不能包含时序控制,也就是不能有#delay、@(posedge clk)这些东西。它必须在同一个时钟周期内完成计算,然后返回一个值。
我个人习惯用函数来做一些简单的数据转换或计算。比如把二进制转成格雷码:
function [7:0] bin2gray;
input [7:0] bin;
begin
bin2gray = bin ^ (bin >> 1);
end
endfunction
调用的时候特别清爽:
wire [7:0] gray_out;
assign gray_out = bin2gray(bin_in);
你看,一行代码就搞定了。如果不用函数,你得写一个always块,还要定义中间变量,麻烦得很。
4.1.2 任务(task)
任务比函数灵活得多。它可以包含时序控制,可以输出多个值,甚至可以不返回值。说白了,任务就是一段可以随时调用的“子程序”。
我在项目中经常用任务来封装总线操作。比如写一个AXI4-Lite的写操作:
task automatic axi_write;
input [31:0] addr;
input [31:0] data;
begin
@(posedge clk);
awvalid <= 1'b1;
awaddr <= addr;
wvalid <= 1'b1;
wdata <= data;
@(posedge clk);
while (!awready) @(posedge clk);
while (!wready) @(posedge clk);
awvalid <= 1'b0;
wvalid <= 1'b0;
@(posedge clk);
bready <= 1'b1;
@(posedge clk);
while (!bvalid) @(posedge clk);
bready <= 1'b0;
end
endtask
调用的时候,一句 axi_write(32'h1000, 32'hDEAD_BEEF); 就完成了整个握手过程。是不是很爽?
automatic关键字。我曾经因为这个bug调了整整一个下午,后来发现是任务里的循环变量冲突了。
4.2 generate语句:批量生产,省时省力
接下来是generate语句。这东西说白了就是“代码生成器”。当你需要重复例化同一个模块很多次,或者根据参数条件决定是否生成某段代码时,generate就是你的救星。
generate有三种形式:
- generate for:循环生成,适合重复例化
- generate if:条件生成,适合根据参数选择
- generate case:多分支条件生成
4.2.1 generate for 实战
我记得有一次做8x8的Crossbar交换机,需要例化64个交叉节点。如果手写64遍例化,不仅累,还容易写错行号。用generate for就优雅多了:
genvar i, j;
generate
for (i = 0; i < 8; i = i + 1) begin : row
for (j = 0; j < 8; j = j + 1) begin : col
cross_node #(
.WIDTH(DATA_WIDTH)
) u_node (
.clk (clk),
.rst_n (rst_n),
.data_in (data_in[i]),
.sel (sel[i][j]),
.data_out(data_out[j])
);
end
end
endgenerate
这里有个细节要注意:begin : row 和 begin : col 后面的名字是必须的。这是generate块的标签,用来生成唯一的层次化路径名。没有它,仿真器会报错。
4.2.2 generate if 实战
条件生成也很常用。比如你的设计支持两种模式,根据参数选择不同的实现:
parameter MODE = "FAST";
generate
if (MODE == "FAST") begin : fast_mode
// 高性能实现
adder_fast u_adder (...);
end
else if (MODE == "LOW_POWER") begin : low_power_mode
// 低功耗实现
adder_low_power u_adder (...);
end
else begin : default_mode
// 默认实现
adder_std u_adder (...);
end
endgenerate
这样做的好处是,你只需要改一个参数,就能切换整个设计的实现方式。我在做多版本IP时特别喜欢用这招。
4.3 参数化设计:一次编写,多次复用
参数化设计,说白了就是让你的模块“可配置”。通过parameter和localparam,你可以把模块的位宽、深度、阈值等做成可调参数。
举个例子,一个参数化的FIFO:
module fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 16,
parameter ALMOST_FULL_THRESHOLD = DEPTH - 2
) (
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [DATA_WIDTH-1:0] wr_data,
input wire rd_en,
output reg [DATA_WIDTH-1:0] rd_data,
output wire full,
output wire empty,
output wire almost_full
);
// 内部实现...
endmodule
实例化的时候,你可以根据需要调整参数:
fifo #(
.DATA_WIDTH(32),
.DEPTH(64),
.ALMOST_FULL_THRESHOLD(60)
) u_fifo (
.clk (clk),
.rst_n (rst_n),
// ... 其他端口
);
4.4 状态机编写:控制逻辑的灵魂
状态机,这是数字设计的核心中的核心。我敢说,90%以上的控制逻辑都是用状态机实现的。状态机写得好不好,直接决定了你的设计稳不稳。
4.4.1 三段式状态机
我个人强烈推荐使用三段式状态机。为什么?因为它把状态跳转、状态判断、输出逻辑分得清清楚楚,调试起来特别方便。
三段式状态机的结构:
- 第一段:时序逻辑,描述状态跳转
- 第二段:组合逻辑,描述下一状态
- 第三段:时序逻辑,描述输出
来看一个经典的UART接收状态机:
// 状态定义
localparam IDLE = 3'b001;
localparam START = 3'b010;
localparam DATA = 3'b011;
localparam STOP = 3'b100;
reg [2:0] state, next_state;
reg [3:0] bit_cnt;
reg [7:0] shift_reg;
// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:下一状态逻辑
always @(*) begin
next_state = state;
case (state)
IDLE: begin
if (rxd == 1'b0)
next_state = START;
end
START: begin
next_state = DATA;
end
DATA: begin
if (bit_cnt == 4'd7)
next_state = STOP;
else
next_state = DATA;
end
STOP: begin
next_state = IDLE;
end
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
bit_cnt <= 4'd0;
shift_reg <= 8'd0;
rx_done <= 1'b0;
end
else begin
case (state)
IDLE: begin
bit_cnt <= 4'd0;
rx_done <= 1'b0;
end
START: begin
// 什么都不做,等待半个位周期
end
DATA: begin
shift_reg <= {rxd, shift_reg[7:1]};
bit_cnt <= bit_cnt + 1'b1;
end
STOP: begin
rx_done <= 1'b1;
end
endcase
end
end
4.4.2 状态机避坑指南
我曾经在一个项目中,因为状态机漏写了default分支,导致综合后出现了锁存器。仿真时一切正常,但上板子就死机。查了三天才发现是这个问题。
所以,写状态机时一定要记住:
- 每个case都要有default,哪怕只是回到IDLE
- 输出信号要赋初值,避免产生锁存器
- 状态编码要留余量,防止非法状态死锁
另外,我建议在状态机里加一个“看门狗”计数器。如果某个状态停留时间超过预期,就强制跳回IDLE。这在处理外部干扰时特别有用。
4.5 本章小结
这一章的内容,说实话,是Verilog从入门到进阶的分水岭。任务和函数让你的代码更简洁,generate语句让你告别重复劳动,参数化设计让你的模块更灵活,而状态机则是控制逻辑的基石。
嗯,我建议你把这些技巧都练熟。下一章我们会聊到仿真验证中的高级技巧,到时候这些知识都会用上。记住,写代码不是为了炫技,而是为了解决问题。工具用得顺手,问题自然就解决得快。