2. Verilog HDL 进阶语法(一):`generate` 语句、`function` 与 `task`、多维数组与结构体
各位同学,咱们今天聊点硬核的。前面基础语法大家应该都摸熟了,但说实话,真正到了项目里,光靠 always 和 assign 是远远不够的。今天讲的这三个东西——generate、function/task、多维数组与结构体——是我个人认为从「小白」到「能干活」必须跨过的三道坎。
嗯,咱们一个一个来。
2.1 generate 语句:让代码学会「复制粘贴」
先问大家一个问题:如果你要例化 8 个一模一样的 D 触发器,你会怎么写?
新手可能会写 8 遍 dff u1(...),对吧?但要是 64 个呢?128 个呢?手写不累死才怪。
generate 就是干这个用的。它本质上是个「编译时的循环」或「编译时的条件判断」。注意,是编译时,不是运行时。这意味着它生成的电路在仿真开始前就已经固定了。
2.1.1 generate for:批量例化
我最常用的就是 generate for。举个例子,一个 8 位的流水线寄存器:
module pipeline_reg #(
parameter WIDTH = 8
)(
input clk,
input rst_n,
input [WIDTH-1:0] din,
output [WIDTH-1:0] dout
);
reg [WIDTH-1:0] pipe [0:2]; // 三级流水
genvar i;
generate
for (i = 0; i < 3; i = i + 1) begin : stage
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pipe[i] <= 'b0;
else if (i == 0)
pipe[i] <= din;
else
pipe[i] <= pipe[i-1];
end
end
endgenerate
assign dout = pipe[2];
endmodule
这里有个细节要注意:begin : stage 这个标签不是随便写的。它会给每个循环体生成一个唯一的层次化名称,比如 stage[0]、stage[1]、stage[2]。这在调试时特别有用——你可以直接在波形里看到每一级寄存器的信号。
generate for,我一定给 begin 加标签。不加标签,综合工具可能会报奇怪的错误,而且仿真时你根本找不到信号在哪。
2.1.2 generate if 与 generate case:条件编译
有时候,同一个模块要根据参数不同生成不同的电路。比如一个加法器,参数 WIDTH 小于等于 8 时用行波进位,大于 8 时用超前进位。这时候 generate if 就派上用场了。
module adder #(
parameter WIDTH = 8
)(
input [WIDTH-1:0] a, b,
input cin,
output [WIDTH-1:0] sum,
output cout
);
generate
if (WIDTH <= 8) begin : ripple
ripple_adder #(.WIDTH(WIDTH)) u_ripple (
.a(a), .b(b), .cin(cin),
.sum(sum), .cout(cout)
);
end else begin : carry_lookahead
cla_adder #(.WIDTH(WIDTH)) u_cla (
.a(a), .b(b), .cin(cin),
.sum(sum), .cout(cout)
);
end
endgenerate
endmodule
我曾经在一个项目中遇到过这样的情况:芯片面积超标,后来发现是某个模块的 generate if 条件写反了,导致所有实例都用了大面积的超前进位加法器。嗯,那一次教训让我养成了每次写完 generate 都要检查综合报告的习惯。
generate if 的条件必须是常量表达式(参数或局部参数),不能是信号。我曾经见过有人写 if (en),结果综合报了一堆错——因为 en 是 wire,不是常量。
2.2 function 与 task:把重复代码「封装」起来
写 RTL 和写软件其实有共通之处:最怕重复代码。Verilog 提供了 function 和 task 两种「子程序」机制。但很多人搞不清它们的区别,我简单总结一下。
| 对比项 | function |
task |
|---|---|---|
| 返回值 | 必须有,且只有一个 | 无返回值,通过 output 端口传结果 |
| 时序控制 | 不能包含 #、@、wait 等 |
可以包含时序控制 |
| 调用方式 | 作为表达式的一部分 | 作为独立语句 |
| 可综合性 | 通常可综合(纯组合逻辑) | 部分可综合(需谨慎) |
| 典型用途 | 组合逻辑计算、位宽转换 | 仿真激励、复杂时序操作 |
2.2.1 function:纯组合逻辑的好帮手
我个人习惯把 function 用在那些需要反复计算的组合逻辑上。比如计算奇偶校验位:
function parity;
input [7:0] data;
integer i;
begin
parity = 1'b0;
for (i = 0; i < 8; i = i + 1) begin
parity = parity ^ data[i];
end
end
endfunction
// 使用
wire odd_parity = parity(tx_data);
注意,function 里不能有 always 块,也不能有 @(posedge clk)。它天生就是组合逻辑。如果你在 function 里写了时序控制,仿真器会报错——我当年第一次写 function 时就踩过这个坑。
2.2.2 task:仿真中的瑞士军刀
task 比 function 灵活得多。它可以在仿真中模拟复杂的总线时序。比如模拟一个 SPI 写操作:
task spi_write;
input [7:0] addr;
input [7:0] data;
output ack;
begin
cs_n = 1'b0;
sclk = 1'b0;
// 发送地址
for (int i = 7; i >= 0; i--) begin
mosi = addr[i];
#10 sclk = 1'b1;
#10 sclk = 1'b0;
end
// 发送数据
for (int i = 7; i >= 0; i--) begin
mosi = data[i];
#10 sclk = 1'b1;
#10 sclk = 1'b0;
end
cs_n = 1'b1;
ack = 1'b1;
end
endtask
你想想看,如果没有 task,每次写 SPI 都要复制粘贴这一大段 #10 的代码,多痛苦。
task 在综合时要格外小心。带有时序控制的 task 通常不可综合。只有纯组合逻辑的 task 才能被综合工具接受。所以我的原则是:设计代码用 function,仿真代码用 task。
2.3 多维数组与结构体:让数据组织更清晰
说实话,Verilog 的数组和结构体支持一直比较弱。但 SystemVerilog 大大改善了这一点。不过咱们今天还是先讲 Verilog 本身能用的东西。
2.3.1 多维数组
Verilog 支持一维数组,比如 reg [7:0] mem [0:255] 就是一个 256 字节的存储器。但二维数组呢?
标准 Verilog 其实不支持真正的二维数组作为端口。但我们可以用「数组的数组」来模拟。比如一个 8×8 的寄存器文件:
reg [7:0] regfile [0:7][0:7]; // 8行,每行8位
// 访问第3行第5列
regfile[3][5] <= 8'hA5;
不过要注意,这种写法在有些综合工具里可能不支持。我建议你查一下目标工具的用户手册。我在一个项目中用过这种写法,结果换了个综合工具就报错了——嗯,从那以后我尽量用一维数组加地址计算来替代。
2.3.2 结构体(SystemVerilog)
如果你用的是 SystemVerilog(现在大多数新项目都是),那结构体是你的好伙伴。比如描述一个 AXI 总线的事务:
typedef struct packed {
logic valid;
logic [31:0] addr;
logic [2:0] burst;
logic [3:0] len;
logic [1:0] size;
} axi_ar_chan_t;
axi_ar_chan_t ar_chan;
// 赋值
ar_chan.valid = 1'b1;
ar_chan.addr = 32'h1000_0000;
ar_chan.burst = 3'b001;
ar_chan.len = 4'd7; // 8次传输
ar_chan.size = 2'b10; // 4字节
结构体的好处是:你把相关的信号打包在一起,代码可读性瞬间提升一个档次。而且 packed 结构体可以直接作为寄存器或线网类型,综合工具也能处理。
2.4 本章知识体系
下面这张图总结了本章的核心知识点和它们之间的关系。你可以把它当作一个「思维导图」来用。
这张图把三个知识点串起来了。你看,generate 解决的是「重复例化」的问题,function/task 解决的是「重复逻辑」的问题,而多维数组和结构体解决的是「数据组织」的问题。三者合在一起,你的代码就能从「能跑」变成「优雅」。
好了,今天的内容就到这里。记住,这些语法不是用来炫技的,而是用来解决实际问题的。下次写代码时,多想想:这段代码能不能用 generate 简化?这个计算能不能写成 function?这些信号能不能打包成结构体?
养成这个习惯,你的 RTL 设计水平会上一个台阶。
generate for 实现一个参数化的桶形移位器(barrel shifter),并用 function 计算移位后的结果。写完后对比两种方式的代码量和可读性。
可可精品资料库