第三章:RTL设计进阶——FSM、流水线、CDC与异步FIFO
各位同学,欢迎来到RTL设计的进阶环节。说实话,从组合逻辑和简单时序逻辑走到这里,才算真正摸到了数字芯片设计的门槛。这一章的内容,是我个人在实际项目中花时间最多、踩坑也最多的地方。咱们一个一个来聊。
3.1 有限状态机(FSM)设计
FSM,说白了就是芯片的“大脑”。它决定了一个模块在不同时刻该干什么。我见过很多新手,一上来就写三段式状态机,结果代码又长又乱。其实,FSM的核心不在于代码风格,而在于状态转移图是否清晰。
3.1.1 状态机分类
- Moore型:输出只取决于当前状态。稳定,但可能需要更多状态。
- Mealy型:输出取决于当前状态和输入。状态数少,但组合逻辑容易出毛刺。
我个人习惯,在控制逻辑复杂时优先用Moore型。为什么?因为时序更干净,调试起来省心。我在一个PCIe控制器项目中,就因为用了Mealy型没处理好输入同步,导致状态跳转异常,查了整整两天。
3.1.2 三段式写法(推荐)
嗯,这里要注意。三段式不是花架子,它把时序逻辑和组合逻辑分得清清楚楚,综合工具也喜欢。
// 第一段:状态寄存器
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: next_state = DONE;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合或时序)
always @(posedge clk or negedge rst_n) begin
if (!rst_n) data_valid <= 1'b0;
else case (next_state)
READ: data_valid <= 1'b1;
default: data_valid <= 1'b0;
endcase
end
always @(*) 而不是 always @(current_state or start),避免漏掉敏感信号。我曾经漏掉一个信号,仿真怎么跑都对,综合后一塌糊涂。
3.2 流水线设计
流水线,说白了就是“拆解长路径,提升吞吐率”。你想想看,一个组合逻辑路径延迟10ns,时钟周期就得大于10ns。但如果拆成5级,每级2ns,时钟频率就能跑到500MHz。
3.2.1 流水线插入原则
- 在组合逻辑最长的路径上插入寄存器
- 保持各流水级延迟均衡
- 注意流水线“填满”和“排空”的延迟
我记得有一次做图像处理芯片,一个乘法器链延迟太大。我加了三级流水,频率从100MHz直接飙到300MHz。但代价是什么?输出延迟多了3个时钟周期。这就是典型的“面积换速度”。
3.2.2 经典流水线示例
// 三级流水线加法器
reg [7:0] a_d1, b_d1;
reg [7:0] a_d2, b_d2;
reg [8:0] sum_d2;
reg [8:0] sum_d3;
always @(posedge clk) begin
// 第一级:输入寄存
a_d1 <= a;
b_d1 <= b;
// 第二级:运算
a_d2 <= a_d1;
b_d2 <= b_d1;
sum_d2 <= a_d1 + b_d1;
// 第三级:输出寄存
sum_d3 <= sum_d2;
end
3.3 跨时钟域同步(CDC)
CDC是芯片设计里最容易出bug的地方,没有之一。两个时钟域之间传数据,搞不好就是亚稳态、数据丢失、甚至逻辑错误。我见过一个团队,因为CDC没处理好,芯片回来直接不工作,几百万打水漂。
3.3.1 单比特同步
最经典的方法就是“打两拍”。说白了,就是用两个寄存器把信号同步到目标时钟域。
reg sync_d1, sync_d2;
always @(posedge clk_dst or negedge rst_n) begin
if (!rst_n) begin
sync_d1 <= 1'b0;
sync_d2 <= 1'b0;
end else begin
sync_d1 <= data_src;
sync_d2 <= sync_d1;
end
end
3.3.2 多比特同步——握手协议
多比特数据不能直接打两拍,因为每个比特到达时间可能不同。我建议用“请求-应答”握手。
- 源时钟域:准备好数据,拉高req
- 目标时钟域:同步req,采样数据,拉高ack
- 源时钟域:同步ack,拉低req
- 目标时钟域:拉低ack
你想想看,这样虽然慢,但可靠。我在一个低速控制接口中一直用这种方法,从来没出过问题。
3.4 异步FIFO设计
异步FIFO是CDC的集大成者。它解决了跨时钟域传输批量数据的问题。说白了,就是一个环形缓冲区,读写指针分别在不同时钟域。
3.4.1 核心难点:空满判断
空满判断需要比较读写指针。但指针在不同时钟域,直接比较会出问题。怎么办?用格雷码!格雷码相邻值只有1位变化,同步时最多错一个采样点,不会出现多位同时错误。
// 二进制转格雷码
assign gray = (binary >> 1) ^ binary;
// 格雷码转二进制(组合逻辑)
always @(*) begin
for (int i=0; i<WIDTH; i++) begin
binary[i] = ^(gray >> i);
end
end
3.4.2 空满判断逻辑
| 条件 | 状态 |
|---|---|
| 读指针 == 写指针 | 空 |
| 写指针追上读指针(格雷码下,最高位不同,其余位相同) | 满 |
知识体系总览
下面这张图,是我自己总结的本章知识结构。你可以把它当作一个检查清单,看看自己掌握了哪些。
好了,这一章的内容就到这里。FSM、流水线、CDC、异步FIFO,这四个东西你如果能熟练掌握,RTL设计这块基本就稳了。下一章我们会聊验证方法学,到时候你会发现,今天学的这些恰恰是验证要重点检查的对象。
课后思考:如果你设计的异步FIFO深度是7(不是2的幂次方),格雷码还能用吗?该怎么处理空满判断?