FPGA教程系列-流水线axis_register解读
流水线的思想有了,那么如何从思想转换到实际,在实际中如何应用?
首先附上ALex大神的代码,这个是axis_register.v。
verilog
`resetall
`timescale 1ns / 1ps
`default_nettype none
/*
* AXI4-Stream register
*/
module axis_register #
(
// Width of AXI stream interfaces in bits
parameter DATA_WIDTH = 8,
// Propagate tkeep signal
parameter KEEP_ENABLE = (DATA_WIDTH>8),
// tkeep signal width (words per cycle)
parameter KEEP_WIDTH = ((DATA_WIDTH+7)/8),
// Propagate tlast signal
parameter LAST_ENABLE = 1,
// Propagate tid signal
parameter ID_ENABLE = 0,
// tid signal width
parameter ID_WIDTH = 8,
// Propagate tdest signal
parameter DEST_ENABLE = 0,
// tdest signal width
parameter DEST_WIDTH = 8,
// Propagate tuser signal
parameter USER_ENABLE = 1,
// tuser signal width
parameter USER_WIDTH = 1,
// Register type
// 0 to bypass, 1 for simple buffer, 2 for skid buffer
parameter REG_TYPE = 2
)
(
input wire clk,
input wire rst,
/*
* AXI Stream input
*/
input wire [DATA_WIDTH-1:0] s_axis_tdata,
input wire [KEEP_WIDTH-1:0] s_axis_tkeep,
input wire s_axis_tvalid,
output wire s_axis_tready,
input wire s_axis_tlast,
input wire [ID_WIDTH-1:0] s_axis_tid,
input wire [DEST_WIDTH-1:0] s_axis_tdest,
input wire [USER_WIDTH-1:0] s_axis_tuser,
/*
* AXI Stream output
*/
output wire [DATA_WIDTH-1:0] m_axis_tdata,
output wire [KEEP_WIDTH-1:0] m_axis_tkeep,
output wire m_axis_tvalid,
input wire m_axis_tready,
output wire m_axis_tlast,
output wire [ID_WIDTH-1:0] m_axis_tid,
output wire [DEST_WIDTH-1:0] m_axis_tdest,
output wire [USER_WIDTH-1:0] m_axis_tuser
);
generate
if (REG_TYPE > 1) begin
// skid buffer, no bubble cycles
// datapath registers
reg s_axis_tready_reg = 1'b0;
reg [DATA_WIDTH-1:0] m_axis_tdata_reg = {DATA_WIDTH{1'b0}};
reg [KEEP_WIDTH-1:0] m_axis_tkeep_reg = {KEEP_WIDTH{1'b0}};
reg m_axis_tvalid_reg = 1'b0, m_axis_tvalid_next;
reg m_axis_tlast_reg = 1'b0;
reg [ID_WIDTH-1:0] m_axis_tid_reg = {ID_WIDTH{1'b0}};
reg [DEST_WIDTH-1:0] m_axis_tdest_reg = {DEST_WIDTH{1'b0}};
reg [USER_WIDTH-1:0] m_axis_tuser_reg = {USER_WIDTH{1'b0}};
reg [DATA_WIDTH-1:0] temp_m_axis_tdata_reg = {DATA_WIDTH{1'b0}};
reg [KEEP_WIDTH-1:0] temp_m_axis_tkeep_reg = {KEEP_WIDTH{1'b0}};
reg temp_m_axis_tvalid_reg = 1'b0, temp_m_axis_tvalid_next;
reg temp_m_axis_tlast_reg = 1'b0;
reg [ID_WIDTH-1:0] temp_m_axis_tid_reg = {ID_WIDTH{1'b0}};
reg [DEST_WIDTH-1:0] temp_m_axis_tdest_reg = {DEST_WIDTH{1'b0}};
reg [USER_WIDTH-1:0] temp_m_axis_tuser_reg = {USER_WIDTH{1'b0}};
// datapath control
reg store_axis_input_to_output;
reg store_axis_input_to_temp;
reg store_axis_temp_to_output;
assign s_axis_tready = s_axis_tready_reg;
assign m_axis_tdata = m_axis_tdata_reg;
assign m_axis_tkeep = KEEP_ENABLE ? m_axis_tkeep_reg : {KEEP_WIDTH{1'b1}};
assign m_axis_tvalid = m_axis_tvalid_reg;
assign m_axis_tlast = LAST_ENABLE ? m_axis_tlast_reg : 1'b1;
assign m_axis_tid = ID_ENABLE ? m_axis_tid_reg : {ID_WIDTH{1'b0}};
assign m_axis_tdest = DEST_ENABLE ? m_axis_tdest_reg : {DEST_WIDTH{1'b0}};
assign m_axis_tuser = USER_ENABLE ? m_axis_tuser_reg : {USER_WIDTH{1'b0}};
// enable ready input next cycle if output is ready or the temp reg will not be filled on the next cycle (output reg empty or no input)
wire s_axis_tready_early = m_axis_tready || (!temp_m_axis_tvalid_reg && (!m_axis_tvalid_reg || !s_axis_tvalid));
always @* begin
// transfer sink ready state to source
m_axis_tvalid_next = m_axis_tvalid_reg;
temp_m_axis_tvalid_next = temp_m_axis_tvalid_reg;
store_axis_input_to_output = 1'b0;
store_axis_input_to_temp = 1'b0;
store_axis_temp_to_output = 1'b0;
if (s_axis_tready_reg) begin
// input is ready
if (m_axis_tready || !m_axis_tvalid_reg) begin
// output is ready or currently not valid, transfer data to output
m_axis_tvalid_next = s_axis_tvalid;
store_axis_input_to_output = 1'b1;
end else begin
// output is not ready, store input in temp
temp_m_axis_tvalid_next = s_axis_tvalid;
store_axis_input_to_temp = 1'b1;
end
end else if (m_axis_tready) begin
// input is not ready, but output is ready
m_axis_tvalid_next = temp_m_axis_tvalid_reg;
temp_m_axis_tvalid_next = 1'b0;
store_axis_temp_to_output = 1'b1;
end
end
always @(posedge clk) begin
s_axis_tready_reg <= s_axis_tready_early;
m_axis_tvalid_reg <= m_axis_tvalid_next;
temp_m_axis_tvalid_reg <= temp_m_axis_tvalid_next;
// datapath
if (store_axis_input_to_output) begin
m_axis_tdata_reg <= s_axis_tdata;
m_axis_tkeep_reg <= s_axis_tkeep;
m_axis_tlast_reg <= s_axis_tlast;
m_axis_tid_reg <= s_axis_tid;
m_axis_tdest_reg <= s_axis_tdest;
m_axis_tuser_reg <= s_axis_tuser;
end else if (store_axis_temp_to_output) begin
m_axis_tdata_reg <= temp_m_axis_tdata_reg;
m_axis_tkeep_reg <= temp_m_axis_tkeep_reg;
m_axis_tlast_reg <= temp_m_axis_tlast_reg;
m_axis_tid_reg <= temp_m_axis_tid_reg;
m_axis_tdest_reg <= temp_m_axis_tdest_reg;
m_axis_tuser_reg <= temp_m_axis_tuser_reg;
end
if (store_axis_input_to_temp) begin
temp_m_axis_tdata_reg <= s_axis_tdata;
temp_m_axis_tkeep_reg <= s_axis_tkeep;
temp_m_axis_tlast_reg <= s_axis_tlast;
temp_m_axis_tid_reg <= s_axis_tid;
temp_m_axis_tdest_reg <= s_axis_tdest;
temp_m_axis_tuser_reg <= s_axis_tuser;
end
if (rst) begin
s_axis_tready_reg <= 1'b0;
m_axis_tvalid_reg <= 1'b0;
temp_m_axis_tvalid_reg <= 1'b0;
end
end
end else if (REG_TYPE == 1) begin
// simple register, inserts bubble cycles
// datapath registers
reg s_axis_tready_reg = 1'b0;
reg [DATA_WIDTH-1:0] m_axis_tdata_reg = {DATA_WIDTH{1'b0}};
reg [KEEP_WIDTH-1:0] m_axis_tkeep_reg = {KEEP_WIDTH{1'b0}};
reg m_axis_tvalid_reg = 1'b0, m_axis_tvalid_next;
reg m_axis_tlast_reg = 1'b0;
reg [ID_WIDTH-1:0] m_axis_tid_reg = {ID_WIDTH{1'b0}};
reg [DEST_WIDTH-1:0] m_axis_tdest_reg = {DEST_WIDTH{1'b0}};
reg [USER_WIDTH-1:0] m_axis_tuser_reg = {USER_WIDTH{1'b0}};
// datapath control
reg store_axis_input_to_output;
assign s_axis_tready = s_axis_tready_reg;
assign m_axis_tdata = m_axis_tdata_reg;
assign m_axis_tkeep = KEEP_ENABLE ? m_axis_tkeep_reg : {KEEP_WIDTH{1'b1}};
assign m_axis_tvalid = m_axis_tvalid_reg;
assign m_axis_tlast = LAST_ENABLE ? m_axis_tlast_reg : 1'b1;
assign m_axis_tid = ID_ENABLE ? m_axis_tid_reg : {ID_WIDTH{1'b0}};
assign m_axis_tdest = DEST_ENABLE ? m_axis_tdest_reg : {DEST_WIDTH{1'b0}};
assign m_axis_tuser = USER_ENABLE ? m_axis_tuser_reg : {USER_WIDTH{1'b0}};
// enable ready input next cycle if output buffer will be empty
wire s_axis_tready_early = !m_axis_tvalid_next;
always @* begin
// transfer sink ready state to source
m_axis_tvalid_next = m_axis_tvalid_reg;
store_axis_input_to_output = 1'b0;
if (s_axis_tready_reg) begin
m_axis_tvalid_next = s_axis_tvalid;
store_axis_input_to_output = 1'b1;
end else if (m_axis_tready) begin
m_axis_tvalid_next = 1'b0;
end
end
always @(posedge clk) begin
s_axis_tready_reg <= s_axis_tready_early;
m_axis_tvalid_reg <= m_axis_tvalid_next;
// datapath
if (store_axis_input_to_output) begin
m_axis_tdata_reg <= s_axis_tdata;
m_axis_tkeep_reg <= s_axis_tkeep;
m_axis_tlast_reg <= s_axis_tlast;
m_axis_tid_reg <= s_axis_tid;
m_axis_tdest_reg <= s_axis_tdest;
m_axis_tuser_reg <= s_axis_tuser;
end
if (rst) begin
s_axis_tready_reg <= 1'b0;
m_axis_tvalid_reg <= 1'b0;
end
end
end else begin
// bypass
assign m_axis_tdata = s_axis_tdata;
assign m_axis_tkeep = KEEP_ENABLE ? s_axis_tkeep : {KEEP_WIDTH{1'b1}};
assign m_axis_tvalid = s_axis_tvalid;
assign m_axis_tlast = LAST_ENABLE ? s_axis_tlast : 1'b1;
assign m_axis_tid = ID_ENABLE ? s_axis_tid : {ID_WIDTH{1'b0}};
assign m_axis_tdest = DEST_ENABLE ? s_axis_tdest : {DEST_WIDTH{1'b0}};
assign m_axis_tuser = USER_ENABLE ? s_axis_tuser : {USER_WIDTH{1'b0}};
assign s_axis_tready = m_axis_tready;
end
endgenerate
endmodule
`resetall
通读与模块划分
这是一个通用的 AXI4-Stream 流水线寄存器(Pipeline Register) ,也被称为 Register Slice 。它的主要作用是在 AXI Stream 接口的 Master 和 Slave 之间插入寄存器(Flip-Flops),以切断长组合逻辑路径,帮助实现 时序收敛(Timing Closure) ,从而让设计能运行在更高的时钟频率下。
1. 核心功能与参数化
该模块设计得非常通用,几乎支持 AXI4-Stream 协议的所有可选信号:
-
参数配置 (
parameter ):-
DATA_WIDTH: 数据位宽。 -
KEEP , LAST , ID , DEST , USER: 这些参数控制是否启用相应的 AXI 侧带信号(Sideband signals)以及它们的位宽。这允许用户根据需要裁剪逻辑资源。 -
REG_TYPE (最关键的参数) : 决定了寄存器的具体架构。默认为2。
-
2. 架构模式详解 (REG_TYPE)
代码使用 generate 语句根据 REG_TYPE 生成三种不同的电路结构:
A. Skid Buffer 模式 (REG_TYPE > 1,默认模式)
问题背景 : 在 AXI Stream 中,握手信号 (VALID 和 READY) 经常形成较长的组合逻辑环路。如果只用简单的寄存器打拍,当下游反压(READY=0)时,上游必须立即停止。为了在打断 READY 信号的时序路径的同时不丢失数据,需要额外的存储空间。
实现原理:
-
模块内部包含两套寄存器:
- 主输出寄存器 (
m_axis_..._reg ) : 直接驱动输出端口。 - 临时缓冲寄存器 (
temp_m_axis_..._reg , 即 Skid Buffer) : 当输出被阻塞(下游不 Ready)但输入端又有新数据到来时,数据会被暂存到这里。
- 主输出寄存器 (
-
无气泡(No Bubbles) : 由于有临时缓冲,即使下游反压解除后,模块也能立即从 buffer 中提供数据,保证了 100% 的吞吐量(每个时钟周期传输一个数据)。(什么是气泡?)
逻辑行为:
- 输入 -> 输出: 下游 Ready,数据直接穿透。
- 输入 -> 临时: 下游不 Ready,数据存入临时 Buffer,且模块对上游拉低 Ready。
- 临时 -> 输出: 下游恢复 Ready,将临时 Buffer 的数据移至输出。
代价: 使用了大约两倍的数据路径触发器资源。
B. 简单寄存器模式 (REG_TYPE == 1)
原理 : 简单的对 VALID 和 DATA 进行打拍(Register)。
优点: 资源占用比 Skid Buffer 少。
缺点 (气泡问题) : 这种结构被称为 "Half-bandwidth" 寄存器。当发生反压(Backpressure)后,为了重新建立握手,通常会引入一个周期的空闲(气泡)。这意味着在频繁启停的传输中,吞吐量可能会下降到 50%。
适用场景: 对吞吐量要求不高,但资源紧张的设计。
C. 直连/旁路模式 (REG_TYPE == 0)
原理 : 使用 assign 语句直接将输入线连接到输出线。
作用: 不消耗逻辑资源,不改善时序。通常用于调试,或者作为占位符,以便在不修改顶层连线的情况下暂时移除流水线级。
3. 代码细节与最佳实践
-
复位策略 (
rst ) :- 代码采用了同步复位。
- 只复位控制信号 :注意代码中只复位了
..._tvalid_reg和s_axis_tready_reg。数据信号(tdata等)没有复位。 - 原因 : 这是 FPGA 设计的最佳实践。数据总线通常很宽,复位它们需要大量的布线资源,且没有必要(因为只要
valid为低,数据就是无效的)。这样做可以显著减少 fan-out 和布线拥塞。
-
Ready 信号的时序优化:
- 在 Skid Buffer 模式中,观察
s_axis_tready_early信号。 - 这是一个 "Look-ahead"(前瞻)逻辑。模块预先计算下一个时钟周期是否能接收数据,并将结果存入触发器
s_axis_tready_reg。 - 这意味着输出给上游的
s_axis_tready是寄存器输出,彻底切断了从下游反馈回来的组合逻辑路径(Critical Path)。
- 在 Skid Buffer 模式中,观察
分段解读
第一部分:参数与端口定义
verilog
module axis_register #
(
parameter DATA_WIDTH = 8, // 数据位宽
parameter KEEP_ENABLE = (DATA_WIDTH>8), // 是否启用 Keep 信号
// ... 其他 ENABLE 参数 (LAST, ID, DEST, USER) ...
parameter REG_TYPE = 2 // 【关键】寄存器类型:0=旁路, 1=简单, 2=Skid Buffer
)
(
input wire clk,
input wire rst,
/* Slave 接口 (输入) - 来自上游 */
input wire [DATA_WIDTH-1:0] s_axis_tdata,
input wire s_axis_tvalid, // 上游说:我有数据
output wire s_axis_tready, // 我对上游说:我可以接收
// ... 其他 s_axis 信号 ...
/* Master 接口 (输出) - 发往下游 */
output wire [DATA_WIDTH-1:0] m_axis_tdata,
output wire m_axis_tvalid, // 我对下游说:我有数据
input wire m_axis_tready, // 下游说:我可以接收
// ... 其他 m_axis 信号 ...
);
标准的 AXI4-Stream 接口转换模块。左手进 (s_axis),右手出 (m_axis)。重点 :REG_TYPE 参数决定了模块内部到底长什么样。
第二部分:Skid Buffer 模式 (REG_TYPE > 1)
1. 定义寄存器
verilog
generate
if (REG_TYPE > 1) begin
// --- 定义两套寄存器 ---
// 第一套:主输出寄存器 (直接连到 m_axis 输出端口)
reg [DATA_WIDTH-1:0] m_axis_tdata_reg = {DATA_WIDTH{1'b0}};
reg m_axis_tvalid_reg = 1'b0;
// ...
// 第二套:临时缓冲寄存器 (Skid Buffer / 备胎)
reg [DATA_WIDTH-1:0] temp_m_axis_tdata_reg = {DATA_WIDTH{1'b0}};
reg temp_m_axis_tvalid_reg = 1'b0;
// ...
准备了两个"箱子":
- 箱子 A (
m_axis ) :放在门口,准备随时送走。 - 箱子 B (
temp ) :放在仓库里,作为备用。
2. 状态控制逻辑 (组合逻辑 always @*)
这部分决定数据该往哪个箱子放。
verilog
// 控制信号定义
reg store_axis_input_to_output; // 输入 -> 输出
reg store_axis_input_to_temp; // 输入 -> 临时
reg store_axis_temp_to_output; // 临时 -> 输出
// 核心逻辑:计算下一拍的状态
always @* begin
// 默认保持状态
m_axis_tvalid_next = m_axis_tvalid_reg;
temp_m_axis_tvalid_next = temp_m_axis_tvalid_reg;
// ... 清零控制信号 ...
if (s_axis_tready_reg) begin
// 【场景1】我之前告诉上游"准备好了",上游发来了数据
if (m_axis_tready || !m_axis_tvalid_reg) begin
// 如果下游也没堵住,或者我的输出箱子本来就是空的:
// ==> 数据直通:输入 -> 输出箱子
m_axis_tvalid_next = s_axis_tvalid;
store_axis_input_to_output = 1'b1;
end else begin
// 如果下游堵住了 (m_axis_tready=0),而且我的输出箱子是满的:
// ==> 刹车不及:输入 -> 临时箱子 (Skid Buffer发挥作用!)
temp_m_axis_tvalid_next = s_axis_tvalid;
store_axis_input_to_temp = 1'b1;
end
end else if (m_axis_tready) begin
// 【场景2】我之前对上游喊了"停"(Ready=0),但下游突然通了
// ==> 把临时箱子里的存货移到输出箱子
m_axis_tvalid_next = temp_m_axis_tvalid_reg;
temp_m_axis_tvalid_next = 1'b0; // 临时箱子空了
store_axis_temp_to_output = 1'b1;
end
end
- 如果路通畅,数据直接进主箱子。
- 如果下游堵车,但上游数据已经过来了,赶紧塞进临时箱子(防止丢包)。
- 如果下游路通了,先把临时箱子里的货发出去。
3. Ready 信号的前瞻计算
verilog
// 计算下一拍我能不能接收数据
// 只要 (输出箱子能发走) 或者 (临时箱子没满 且 (输出箱子没满 或 这次没来数据))
// 简化理解:只要临时箱子是空的,我就能收(因为我有地方存"刹车不及"的数据)
wire s_axis_tready_early = m_axis_tready || (!temp_m_axis_tvalid_reg && (!m_axis_tvalid_reg || !s_axis_tvalid));
s_axis_tready_early 是计算出来的"预告"。它保证了输出给上游的 s_axis_tready 是经过寄存器的,切断了反向时序路径。
4. 数据更新 (时序逻辑 always @(posedge clk))
verilog
always @(posedge clk) begin
// 更新 Ready 信号
s_axis_tready_reg <= s_axis_tready_early;
// 更新 Valid 信号
m_axis_tvalid_reg <= m_axis_tvalid_next;
// ...
// 搬运数据
if (store_axis_input_to_output) begin
m_axis_tdata_reg <= s_axis_tdata;
// ...
end else if (store_axis_temp_to_output) begin
m_axis_tdata_reg <= temp_m_axis_tdata_reg;
// ...
end
if (store_axis_input_to_temp) begin
temp_m_axis_tdata_reg <= s_axis_tdata; // 存入备胎
// ...
end
// ... 复位逻辑 ...
end
真正的"搬运工"。当时钟上升沿到来时,根据指挥中心的指令(store_...),把数据真正地写入寄存器。
第三部分:简单寄存器模式 (REG_TYPE == 1)
如果不需要高性能,只想要个简单的打拍。
verilog
end else if (REG_TYPE == 1) begin
// 只有一套寄存器 (输出箱子)
reg [DATA_WIDTH-1:0] m_axis_tdata_reg;
// ...
// Ready 信号逻辑:
// 只有当下个周期输出箱子是空的 (valid_next=0),我才敢收数据
// 这意味着如果现在箱子满,发走之后,我要花一个周期意识到箱子空了,再拉高 Ready
// 这就是"气泡"的来源。
wire s_axis_tready_early = !m_axis_tvalid_next;
always @* begin
// 简单的逻辑:如果我准备好了,就把输入存到输出
if (s_axis_tready_reg) begin
m_axis_tvalid_next = s_axis_tvalid;
store_axis_input_to_output = 1'b1;
end else if (m_axis_tready) begin
// 如果下游取走了数据,我标记我的箱子为空
m_axis_tvalid_next = 1'b0;
end
end
// ... 下面是常规的 always @(posedge clk) 数据搬运 ...
结构简单,只有一级寄存器。缺点:当发生阻塞后恢复时,无法做到无缝衔接,会有吞吐量损失。
第四部分:旁路模式 (REG_TYPE == 0)
verilog
end else begin
// 旁路:直接连线
assign m_axis_tdata = s_axis_tdata;
assign m_axis_tvalid = s_axis_tvalid;
assign m_axis_tlast = LAST_ENABLE ? s_axis_tlast : 1'b1;
// ...
assign s_axis_tready = m_axis_tready; // 直接透传 Ready
end
这就是一根导线。输入是什么,输出就是什么。没有任何寄存器,不消耗逻辑资源,但也不改善时序。
还是有点雾里看花的感觉,并没有太清楚,还需要再进行一个分解