3、Verilog基础回顾:模块化设计、组合逻辑与时序逻辑、状态机编写规范
好,咱们进入第三讲。说实话,每次讲这个章节我都挺感慨的。为什么?因为很多新手工程师,甚至一些工作一两年的同事,最后出问题都出在这些最基础的地方。今天我就把我在项目中踩过的坑、积累的经验,一次性倒给你们。
3.1 模块化设计——别让你的代码变成一锅粥
模块化设计,说白了就是「分而治之」。一个复杂的基站加速器,你不可能在一个文件里写完所有逻辑。我见过有人一个模块写了三千行,那调试起来简直是噩梦。
我个人习惯:每个模块只干一件事。比如一个 FIR 滤波器模块,它就只负责滤波;一个 CRC 校验模块,它就只算校验值。这样后期维护、复用都方便。
模块划分的核心原则:
- 高内聚:模块内部的功能要紧密相关
- 低耦合:模块之间的接口要尽量简单
- 接口清晰:输入输出信号命名规范,一看就懂
举个例子,一个简单的计数器模块:
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
你看,这个模块的接口就三个:时钟、复位、使能。谁都能看懂,谁都能用。我在项目中遇到过有人把使能信号和复位信号混在一起处理,结果时序分析的时候一堆问题。嗯,这里要注意:接口信号要正交,不要搞出依赖关系。
3.2 组合逻辑与时序逻辑——搞清楚谁是谁
这个问题,我每次面试必问。你想想看,如果连这个都分不清,后面的时序收敛根本无从谈起。
3.2.1 组合逻辑
组合逻辑的输出只取决于当前输入。说白了,输入一变,输出立马跟着变,没有记忆功能。
常见的组合逻辑写法:
assign连续赋值语句always @(*)中的阻塞赋值=
举个例子:
// 组合逻辑:多路选择器
assign out = sel ? a : b;
// 或者用 always 块
always @(*) begin
if (sel)
out = a;
else
out = b;
end
我曾经踩过的坑:组合逻辑中产生锁存器(latch)。比如在 always 块中,if 条件没有写全 else,或者 case 没有写 default。综合工具会给你生成一个 latch,这玩意儿在 FPGA 里很坑,容易导致时序问题。
3.2.2 时序逻辑
时序逻辑就不一样了,它依赖时钟沿来更新状态。输出不仅和当前输入有关,还和之前的状态有关。
时序逻辑的标准写法:
always @(posedge clk)中的非阻塞赋值<=
// 时序逻辑:带使能的寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 0;
else if (en)
q <= d;
end
这里有个关键点:非阻塞赋值 <= 和阻塞赋值 = 千万别混用。我刚开始做设计时,在一个 always 块里又用 = 又用 <=,结果仿真结果怎么都不对。后来查了半天,才发现是赋值方式搞混了。
我的个人经验:写时序逻辑就用 <=,写组合逻辑就用 =。永远不要在一个 always 块里混用。这样代码可读性强,也不容易出错。
3.3 状态机编写规范——别让状态机变成「状态机」
状态机是 FPGA 设计的核心。基站加速器里的协议解析、数据调度,说白了都是状态机。我见过太多人把状态机写得乱七八糟,最后 debug 到崩溃。
3.3.1 状态机的三种写法
| 写法 | 特点 | 适用场景 |
|---|---|---|
| 一段式 | 所有逻辑写在一个 always 块里 | 简单状态机(不推荐用于复杂设计) |
| 二段式 | 时序逻辑和组合逻辑分开 | 中等复杂度,我比较常用 |
| 三段式 | 时序、组合、输出逻辑全部分开 | 复杂状态机,可读性最好 |
我个人强烈推荐三段式写法。为什么?因为调试方便。你想想看,如果状态跳转错了,你只需要看第一个 always 块;如果输出错了,你只需要看第三个 always 块。问题定位快得很。
3.3.2 三段式状态机模板
// 状态编码
localparam IDLE = 2'b00;
localparam START = 2'b01;
localparam DATA = 2'b10;
localparam DONE = 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
next_state = state; // 默认保持
case (state)
IDLE: if (start_sig) next_state = START;
START: next_state = DATA;
DATA: if (data_done) next_state = DONE;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(*) begin
// 默认输出
data_valid = 0;
done_sig = 0;
case (state)
DATA: data_valid = 1;
DONE: done_sig = 1;
endcase
end
状态机编写规范总结:
- 使用
localparam定义状态,不要用魔法数字 - 状态编码推荐使用独热码(one-hot)或格雷码(gray),具体看资源与时序要求
- 组合逻辑部分一定要有
default,防止产生 latch - 输出逻辑中,先给所有输出赋默认值,再根据状态修改
嗯,这里要特别强调一下:状态机一定要有默认状态。我曾经在项目中遇到一个情况,状态机跑到一个未定义的状态,结果整个系统卡死了。后来加了 default 和看门狗复位才解决。你想想看,如果是在基站设备里,这种问题会导致掉话,后果很严重。
3.4 避坑指南——我踩过的那些坑
最后,分享几个我实际项目中遇到的坑,你们以后遇到了心里有数:
- 敏感列表不全:写组合逻辑时,always 块的敏感列表没写全,导致仿真和综合结果不一致。解决办法:用
always @(*)代替手动列敏感信号。 - 复位信号处理不当:异步复位虽然简单,但容易导致 metastability。我建议能同步复位就同步复位,实在要用异步复位,一定要做复位同步器。
- 状态机输出未寄存:组合逻辑输出直接驱动外部模块,容易产生毛刺。我的习惯是:所有输出都打一拍寄存器,虽然多一个时钟周期延迟,但信号质量好很多。
一个小技巧:写代码之前,先在纸上画出状态转移图。把每个状态、每个跳转条件、每个输出都画清楚。代码写起来就快多了。我每次做复杂状态机,都是先画图再写代码,很少出问题。
好了,这一讲的内容就到这里。模块化设计、组合逻辑与时序逻辑、状态机编写规范,这三个东西是 FPGA 设计的基石。你们回去之后,把今天讲的三段式状态机模板背下来,以后写代码就用这个模板。相信我,你会感谢我的。