Skid Buffer RTL 实现:两拍寄存器、反压吸收与 ready 时序优化
更新时间:2026-09-19。本文把 valid/ready 握手 里反复出现的 Skid Buffer 落到代码,也是 AXI 寄存器切片 的基本单元。异步场景对应 异步 FIFO。
本文要回答的问题
- Skid Buffer 为什么能用两个寄存器吸收一拍反压?
- empty/full 状态怎么转移,ready 怎么提前计算?
- 它和「直接连线」「完整 FIFO」差在哪,什么时候够用?
- 为什么它能同时改善 ready 路径时序?
一、要解决的两个问题
- 反压丢拍:下游某拍
ready=0,若上游这拍仍valid=1,没有缓冲就只能要求上游也停且保持——但上游可能收不到及时的 ready(路径太长)。 - ready 长组合路径:下游 ready 常由「内部有没有空位」组合产生,经多级模块反传会形成很长的 ready 组合链,拖低频率。
Skid Buffer 用「输出寄存器 + 一个备用(skid)寄存器」提供 1 拍弹性深度:即使下游立刻反压,它也敢向上游表示「这拍我还能收」,把数据存进备用槽,下拍再吐。
二、标准实现(2 深度、1 拍弹性)
verilog
module skid_buffer #(parameter W=32) (
input logic clk, rst_n,
input logic s_valid, output logic s_ready,
input logic [W-1:0] s_data,
output logic m_valid, input logic m_ready,
output logic [W-1:0] m_data
);
logic [W-1:0] buf; // 备用槽
logic full; // 备用槽是否占用
// 本拍是否在输出侧完成一次转移
wire m_xfer = m_valid & m_ready;
// 本拍是否在输入侧完成一次转移
wire s_xfer = s_valid & s_ready;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
full <= 1'b0; m_valid <= 1'b0;
end else begin
// 备用槽在「输出没走、输入却来了」时装入;在「输出走掉且没新货」时清空
full <= (!m_xfer && s_xfer) | (full && !m_xfer);
// 主输出寄存器:空时优先直通新数据;满时先吐备用槽
if (!m_valid || m_ready) begin
m_data <= full ? buf : s_data;
m_valid <= 1'b1;
end
// 备用槽:当前正在输出且又进来新拍,先把新拍存起来
if (m_valid && !m_ready && s_xfer) buf <= s_data;
end
end
// 只要备用槽没满,就敢向上游承诺接收(关键:ready 只依赖本地 full,不绕到下游)
assign s_ready = !full;
endmodule工作直觉:
- empty(full=0):进来的数据直接进输出寄存器给下游,空载延迟近乎 0/1 拍。
- 下游反压那拍(m_valid=1, m_ready=0)同时上游又来货:新货存进
buf,置 full;对上游而言这拍s_ready=!full=1,货被安全收下。 - 下拍:先把
buf内容输出;备用槽腾空,s_ready 重新为 1。 - 若下游连续反压超过弹性(full=1 后还来货),s_ready=0,正式反压给上游——所以它只能吸收与深度等量的拍(本实现 1 拍),连续长反压仍需更深 FIFO。
三、时序为什么变好
s_ready = !full,只依赖本地寄存器 full,与下游 m_ready 无关——那条「从最下游一路组合反传到最上游」的 ready 长链被切断。valid/data 也都经寄存器输出。代价是 1~2 组数据寄存器(主 + 备用)与一点满空逻辑。这正是 AXI register slice 用它做「浅切片」的原因:在几乎不增加稳态延迟的情况下改善时序。
四、三种缓冲形态对比
| 直接连线 | Skid Buffer(本文) | 完整 FIFO | |
|---|---|---|---|
| 弹性深度 | 0 | 1(可扩 2) | 任意(8/16…) |
| 空载延迟 | 0 | 0~1 拍 | ≥1 拍 |
| 切 ready 长路径 | 否 | 是 | 是 |
| 连续反压 | 立即反压上游 | 吸收 1 拍 | 吸收 N 拍 |
| 面积 | 0 | 2 组寄存器+小控制 | N 组存储器+指针/比较 |
| 适用 | 模块紧邻、时序宽松 | 模块边界、寄存器切片、宽流 | 跨时钟、速率差大、突发削峰 |
经验:模块边界/总线切片默认放一级 Skid Buffer;只有需要吸收多拍(突发、跨时钟域、平均速率差)时才上 FIFO。跨时钟域不能用 Skid Buffer,必须用带格雷码指针的 异步 FIFO。
五、握手铁律(实现时不能违反)
s_valid由上游寄存器驱动,一旦举起必须自保持直到s_xfer,不能等 s_ready 才决定。s_ready可组合产生(如本实现!full),允许在 valid 之前就为高;但不能与上游 valid 形成组合环。- data 与 valid 对齐,valid 保持期间 data 不变。
- 复位后 m_valid=0、full=0,输出空闲。
- 宽总线时备用槽面积大,可只对反压真实存在的通道加(AXI 五通道按需加,见 寄存器切片)。
六、常见的坑
- ready 仍然组合绕到 m_ready:写成
s_ready = m_ready || ...长链,没起到切断时序的作用。 - 满位更新漏情形:只在「装」时置 1、忘了「下游一直不收且已 full」要保持 full,数据被覆盖。
- valid 跟着 ready 抖:破坏自保持,反压时丢拍。
- 当 FIFO 用:只有 1 拍弹性,连续反压多拍仍丢,必须上深 FIFO。
- 跨时钟域误用:Skid Buffer 是同步电路,CDC 必须异步 FIFO。
- 复位不清 m_valid/full:上电可能输出一笔假数据。
- 位宽带 sideband(keep/last/id)却只缓冲 data:last/keep/id 没一起进备用槽,包信息错位;要整束信号一起缓冲。
相关与延伸
协议地基见 valid/ready 握手;总线应用见 AXI 寄存器切片 与 AXI Crossbar;更深缓冲/跨域见 FIFO 深度设计、异步 FIFO CDC;位宽转换见 位宽适配器。
总结
Skid Buffer 用「输出寄存器 + 一个备用槽 + full 标志」提供一拍弹性:下游反压的同拍仍敢用只依赖本地 full 的 s_ready 收下上游数据并存入备用槽,下拍再吐,既不丢拍又把 ready 的长组合路径切断,空载几乎零延迟、面积只有两组寄存器;它适合做模块边界和总线浅切片,连续多拍反压要换深 FIFO、跨时钟域要换异步 FIFO。