4. Verilog HDL进阶:有限状态机(FSM)设计、参数化设计(parameter)、generate语句
各位同学,咱们今天聊点硬核的。Verilog 基础语法你都会了,但真正到了项目里,你会发现——光会写 always 和 assign 远远不够。今天这三个知识点,是我这些年做芯片设计时用得最频繁、也最容易踩坑的地方。
4.1 有限状态机(FSM)设计
有限状态机,说白了就是让电路“记住”它现在在干什么,然后根据输入决定下一步去哪。我刚开始做设计时,总觉得状态机不就是几个 case 嘛,后来被时序问题坑过几次,才明白这里面门道很深。
4.1.1 状态机的两种写法
实际项目中,我们主要用两种状态机:Moore 型和 Mealy 型。区别很简单——Moore 的输出只跟当前状态有关,Mealy 的输出还跟输入有关。
核心区别:
- Moore 型:输出 = f(当前状态),更稳定,但可能需要更多状态
- Mealy 型:输出 = f(当前状态, 输入),响应更快,但容易出毛刺
我个人习惯,能用 Moore 就用 Moore。为什么?因为 Mealy 的输出直接依赖输入,一旦组合逻辑路径没处理好,仿真和综合结果可能不一致。我曾经在一个通信接口项目里用了 Mealy,结果后仿发现输出毛刺导致误码,改回 Moore 就稳了。
4.1.2 三段式状态机(推荐)
很多教材讲两段式,但我强烈建议你用三段式。什么叫三段?就是三个 always 块各司其职:
// 三段式状态机模板
// 第一段:状态转移(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
// 第二段:次态逻辑(组合逻辑)
always @(*) begin
case (current_state)
IDLE: if (start) next_state = READ;
else next_state = IDLE;
READ: if (done) next_state = DONE;
else next_state = READ;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(时序逻辑,避免毛刺)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_out <= 0;
else begin
case (current_state)
READ: data_out <= data_in;
DONE: data_out <= 1;
default: data_out <= 0;
endcase
end
end
我的经验:第三段用时序逻辑输出,虽然会多一个时钟周期的延迟,但能彻底消除组合逻辑毛刺。在高速设计中,这个延迟通常可以接受,但毛刺是致命的。
4.1.3 状态编码的选择
状态编码这事儿,很多人不重视,觉得随便写就行。其实不然。我整理了一个表格,你一看就明白:
| 编码方式 | 占用寄存器 | 速度 | 适用场景 |
|---|---|---|---|
| 二进制编码 | 最少(log2N) | 一般 | 状态数多,对速度要求不高 |
| 格雷码 | 最少(log2N) | 中等 | 跨时钟域传输,减少跳变 |
| 独热码 | 最多(N个) | 最快 | 高速设计,状态数少(<16) |
嗯,这里要注意:独热码虽然费寄存器,但译码逻辑简单,时序容易收敛。我在做 DDR 控制器时,状态机只有 8 个状态,果断用独热码,时序轻松跑过 1GHz。
避坑指南:我曾经犯过一个低级错误——状态机没有写 default 分支。结果综合工具给我插了锁存器,后仿直接跑飞。记住:case 语句一定要有 default,哪怕只是回到 IDLE 状态。
4.2 参数化设计(parameter)
你想想看,如果每个模块的位宽、深度都写死在代码里,换个项目就得重写一遍,那得多累?parameter 就是干这个的——让模块变得可配置。
4.2.1 参数的定义与传递
参数化设计的核心思路:把常量提取出来,用 parameter 定义,上层例化时再传入具体值。
// 参数化 FIFO 模块
module fifo #(
parameter DATA_WIDTH = 8,
parameter DEPTH = 16
) (
input wire clk,
input wire rst_n,
input wire wr_en,
input wire [DATA_WIDTH-1:0] wr_data,
input wire rd_en,
output wire [DATA_WIDTH-1:0] rd_data,
output wire full,
output wire empty
);
// 内部实现...
endmodule
// 上层例化时传参
fifo #(
.DATA_WIDTH(32),
.DEPTH(64)
) u_fifo (
.clk(clk),
.rst_n(rst_n),
// ...
);
我个人习惯,所有可能变化的常量都用 parameter 定义。比如数据位宽、FIFO 深度、计数器最大值。这样代码复用率极高,一个模块写好了,多个项目都能用。
4.2.2 localparam 与 parameter 的区别
很多新手搞不清这两个。简单说:parameter 可以在例化时被覆盖,localparam 不行。localparam 是模块内部自己用的常量,对外不可见。
使用原则:
- 需要外部配置的 → 用 parameter
- 模块内部自己算出来的 → 用 localparam
举个例子:你定义了 DATA_WIDTH,然后内部算地址位宽 ADDR_WIDTH = clog2(DEPTH)。这个 ADDR_WIDTH 就应该用 localparam,因为它是根据 DEPTH 算出来的,外部不需要也不应该修改它。
4.3 generate 语句
generate 语句,说白了就是让 Verilog 拥有“循环”和“条件”能力。你想想看,如果我要例化 32 个相同的加法器,难道要手写 32 遍?generate 就是来解决这个问题的。
4.3.1 generate for 循环生成
最常用的场景:重复例化相同模块,或者重复生成相同逻辑。
// 生成 8 个并行的加法器
genvar i;
generate
for (i = 0; i < 8; i = i + 1) begin : adder_gen
adder #(
.WIDTH(16)
) u_adder (
.a(data_a[i]),
.b(data_b[i]),
.sum(data_sum[i])
);
end
endgenerate
注意那个 begin : adder_gen,这个标号是必须的。为什么?因为 generate 循环会展开成多个实例,每个实例需要唯一的名字。如果不加标号,综合工具会报错。
4.3.2 generate if 条件生成
有时候,同一个模块需要根据参数不同,生成不同的内部结构。这时候 generate if 就派上用场了。
module alu #(
parameter USE_PIPELINE = 1
) (
input wire [31:0] a, b,
input wire [2:0] op,
output reg [31:0] result
);
generate
if (USE_PIPELINE) begin : pipe_alu
// 流水线版本的 ALU 实现
always @(posedge clk) begin
// 分两级流水
end
end else begin : comb_alu
// 组合逻辑版本的 ALU 实现
always @(*) begin
// 纯组合逻辑
end
end
endgenerate
endmodule
我的经验:generate if 特别适合做“性能 vs 面积”的权衡。同一个模块,参数设为 1 时用高性能流水线实现,设为 0 时用低功耗组合实现。一个代码库,两种选择,非常灵活。
4.3.3 generate case 多分支生成
如果条件分支太多,用 if 嵌套会很难看。这时候可以用 generate case:
generate
case (DATA_WIDTH)
8: begin : w8
// 8 位实现
end
16: begin : w16
// 16 位实现
end
32: begin : w32
// 32 位实现
end
default: begin : w_default
// 默认实现
end
endcase
endgenerate
避坑指南:我曾经在一个项目里,generate for 循环的边界写成了 i <= 7,结果综合出来多了一个实例,功能完全不对。记住:generate 循环的边界是 <,不是 <=。另外,genvar 变量只能在 generate 块内部使用,不能在 always 块里引用。
4.4 本章知识体系
下面这张图,是我梳理的本章核心逻辑。你看一眼,就能把三个知识点串起来:
这三个技能,说白了就是让你从“能写代码”变成“会设计代码”。FSM 让电路有“记忆”,parameter 让模块有“弹性”,generate 让代码有“批量生产能力”。三者结合,你就能写出真正工业级的 RTL 代码。
好了,这一章的内容就到这里。记住我强调的那些坑——default 分支、generate 标号、localparam 的使用——这些都是我拿实际项目教训换来的。你把这些记住了,至少能少走半年弯路。
可可精品资料库