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 本章知识体系
下面这张图总结了本章的核心内容,你可以对照着梳理一下思路:
好了,这一讲的内容就到这里。参数化设计让代码灵活,generate语句批量生成硬件,任务和函数帮我们模块化代码,状态机则是控制逻辑的核心。这些知识点,你在实际项目中都会反复用到。
记住一点:写Verilog不是写软件,你是在描述硬件。每一行代码,都要想想它综合出来是什么电路。养成这个习惯,你的设计水平会提升很快。
公众号:蓝海资料掘金营,微信deep3321