4. Verilog基础语法(下):参数化设计、generate语句、任务与函数、状态机基础
好,咱们接着聊。上一章我们把Verilog的基本语法过了一遍,那些是写代码的“砖块”。这一章,我们来聊聊怎么把这些砖块搭得更灵活、更聪明。说白了,就是让你的代码能“举一反三”,而不是写一堆重复的体力活。
4.1 参数化设计:让模块学会“变通”
先问个问题:你写过一个8位计数器,下次要16位的,怎么办?复制粘贴改数字?嗯,我以前也这么干过。直到有一次,一个项目里需要3个不同位宽的计数器,我复制了三次,结果改漏了一个地方,仿真跑出来全是错的……
从那以后,我就爱上了参数化设计。核心就是两个关键字:parameter 和 localparam。
4.1.1 parameter vs localparam
| 关键字 | 作用 | 能否被外部修改 |
|---|---|---|
parameter |
模块的“可调旋钮” | 可以(实例化时用 #() 传参) |
localparam |
模块内部的“固定常数” | 不可以 |
举个例子,一个参数化的计数器模块:
module counter #(
parameter WIDTH = 8 // 默认8位
)(
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
count <= count + 1;
end
endmodule
实例化的时候,你可以这样用:
counter #(.WIDTH(16)) u_counter_16bit (...); // 变成16位
counter #(.WIDTH(32)) u_counter_32bit (...); // 变成32位
你看,一个模块,搞定所有位宽。这就是参数化的魅力。
parameter 放在模块端口声明之前,用 #() 括起来。这样别人一看就知道这个模块有哪些“旋钮”可以调。
4.2 generate语句:批量生产,省时省力
参数化解决的是“一个模块变多个版本”的问题。那如果我想在模块内部批量生成一堆相同的电路呢?比如,一个8位的加法器,难道要手写8个全加器?
这时候 generate 就派上用场了。它有三种用法:generate for、generate if 和 generate case。
4.2.1 generate for:循环生成结构
我最常用的是 generate for。比如,我要生成一个参数化的加法树:
module adder_tree #(
parameter NUM_INPUTS = 4,
parameter WIDTH = 8
)(
input [WIDTH-1:0] data_in [0:NUM_INPUTS-1],
output [WIDTH-1:0] sum
);
genvar i;
wire [WIDTH-1:0] temp [0:NUM_INPUTS/2-1];
generate
for (i = 0; i < NUM_INPUTS/2; i = i + 1) begin : gen_add
assign temp[i] = data_in[2*i] + data_in[2*i+1];
end
endgenerate
// 这里可以继续用 generate 做下一级加法...
// 实际项目中会用递归或流水线,这里只是演示
endmodule
注意那个 begin : gen_add,我给这个生成块起了个名字。为什么?因为仿真时方便看波形,也方便调试。我曾经有一次没起名字,结果仿真器里报错说找不到信号,找了半天才发现是generate块没命名。
4.2.2 generate if/case:条件生成
有时候,同一个模块要根据参数不同,生成不同的电路。比如:
generate
if (WIDTH <= 8) begin : impl_small
// 用简单的加法器实现
end else begin : impl_large
// 用流水线加法器实现
end
endgenerate
这就像C语言里的 #ifdef,但它是硬件描述,综合器会根据条件“裁剪”电路。
generate 块里的循环变量必须用 genvar 声明,不能用 integer。这是Verilog的硬性规定,别问我为什么,问就是语法规定。
4.3 任务与函数:把重复代码“封装”起来
写代码最怕什么?重复。同样的逻辑写好几遍,改一个地方得改所有地方。在Verilog里,task 和 function 就是用来干这个的。
4.3.1 function:纯组合逻辑的“计算器”
function 只能用于组合逻辑,不能包含时序控制(比如 @(posedge clk))。它返回一个值。比如,一个计算奇偶校验的函数:
function parity;
input [7:0] data;
integer i;
begin
parity = 0;
for (i = 0; i < 8; i = i + 1) begin
parity = parity ^ data[i];
end
end
endfunction
调用的时候:
assign parity_bit = parity(data_in);
4.3.2 task:可以包含时序的“小模块”
task 比 function 灵活,它可以包含延时、等待等时序控制。但它不返回值,而是通过输出参数传递结果。比如,一个产生特定脉冲的task:
task pulse_gen;
input [7:0] pulse_width;
output reg pulse_out;
begin
pulse_out = 1;
#pulse_width;
pulse_out = 0;
end
endtask
调用:
pulse_gen(10, my_pulse); // 产生一个宽度为10个时间单位的脉冲
task,因为它容易写出不可综合的代码。但仿真验证时,task 是神器,尤其是写testbench的时候。比如,模拟一个I2C的读写操作,用task封装起来,调用起来特别爽。
4.4 状态机基础:数字系统的“大脑”
终于到了状态机。说实话,我觉得状态机是FPGA设计的灵魂。你想想看,一个复杂的控制逻辑,比如UART的收发、SPI的时序、甚至一个CPU的指令执行,本质上都是状态机。
4.4.1 三段式状态机:最推荐的写法
状态机的写法有很多种,但我个人强烈推荐“三段式”。为什么?因为它把“状态跳转”、“状态输出”、“次态逻辑”分得清清楚楚,代码可读性高,也容易维护。
看一个经典的例子:一个简单的交通灯控制器。
module traffic_light (
input clk,
input rst_n,
output reg [2:0] light // {red, yellow, green}
);
// 状态编码
localparam IDLE = 2'b00;
localparam GREEN = 2'b01;
localparam YELLOW = 2'b10;
localparam RED = 2'b11;
reg [1:0] state, next_state;
// 第一段:状态寄存器(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:次态逻辑(组合逻辑)
always @(*) begin
case (state)
IDLE: next_state = GREEN;
GREEN: next_state = YELLOW;
YELLOW: next_state = RED;
RED: next_state = GREEN;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合逻辑或时序逻辑)
always @(*) begin
case (state)
GREEN: light = 3'b001; // 绿灯
YELLOW: light = 3'b010; // 黄灯
RED: light = 3'b100; // 红灯
default: light = 3'b000;
endcase
end
endmodule
你看,三段各司其职:
- 第一段:只负责把
next_state锁存到state,纯时序。 - 第二段:根据当前状态和输入,决定下一个状态,纯组合。
- 第三段:根据当前状态,产生输出。
4.4.2 状态编码的选择
状态编码不是随便选的。常用的有三种:
| 编码方式 | 特点 | 适用场景 |
|---|---|---|
| 二进制编码 | 状态数少,节省寄存器 | 状态数很多(比如几十个) |
| 格雷码 | 相邻状态只变1位,抗毛刺 | 跨时钟域传递状态 |
| 独热码 | 每个状态一个寄存器,译码简单 | 状态数少(一般<10),追求速度 |
我个人习惯:如果状态少于8个,用独热码;多于8个,用二进制编码。格雷码嘛,只有在跨时钟域时才用。
4.5 本章知识体系
说了这么多,咱们用一张图来总结一下这一章的核心内容:
嗯,这一章的内容确实不少。参数化设计让你的模块更灵活,generate语句帮你批量“造电路”,任务和函数让你告别重复代码,而状态机则是你设计控制逻辑的利器。这些东西,我在实际项目中几乎天天用。你想想看,一个复杂的FPGA设计,如果不用参数化,不用generate,那代码量得翻多少倍?
好了,这一章就到这里。记住,写Verilog不是写C语言,你脑子里要时刻想着“电路”。参数化生成的是电路,generate生成的是电路,状态机更是电路。把这一点想通了,后面的路就好走了。