握手机制Valid-Ready handshake reg
由于该部分网上内容十分全面,所以大部分都是复制的网上当作自己复习的笔记,结尾贴出参考链接
问题
There’s a “valid-ready” protocol-based data transferring interface between module TX
and module RX, with connection and interface timing diagram showed below. However,
for timing issue in real physical design, you need to insert a pipeline slice betweer
TX and Rx for timing closure.
1)Please use Verilog or SystemVerilog to implement such 1-depth pipeline slice.
(15 points)
2)Please use Verilog orto implement a generic parameterized N-depthpipeline slice.
(15 points)
| TX | | RX |
| vld |--------------->|vld |
| data|--------------->|data |
| rdy |<---------------|rdy |
| TX | | S | | RX |
| vld |---->| |----->|vld |
| data|---->| |----->|data |
| rdy |<----| |<-----|rdy |
由该问题引发的一些思考
handshake reg的设计
首先为什么会出现这样一个handshake呢,我们知道,在我们修改setup违例时,经常采用增加reg的方式,而对于这样一个使用上下游使用握手机制产生了时序违例,我们就需要这样一个reg来改善时序问题了。
我们以AXI的握手机制为例,AXI master 到AXI slave 这一簇信号出现了setup违例有以下三种情况:
1、从AXI master 到AXI slave 出现setup违例;
2、从AXI slave 到AXI master出现setup违例;
3、两者都出现setup时序违例。
所以AXI master 和 AXI slave 之间的打牌有四种模式
Forward Registered : 对valid和payload路打拍
Backward Registered : 对ready路打拍
Fully Registered : 同时对valid/payload路和ready路打拍
Pass Through Mode: Bypass,均不打拍
实际上前三种模式已经形成了独立的IP
Forward Registered
// 将进入的valid 和 payload 打拍,ready使用组合逻辑输出
always @(posedge clk or negedge rst_n)begin
if (rst_n == 1'd0)
valid_dst <= 1'd0;
else if (valid_src == 1'd1)
valid_dst <= #`DLY 1'd1;
else if (ready_dst == 1'd1)
valid_dst <= #`DLY 1'd0;
end
always @(posedge clk or negedge rst_n)begin
if (rst_n == 1'd0)
payload_dst <= 'd0;
else if (valid_src == 1'd1 && ready_src == 1'd1)
payload_dst <= #`DLY payload_src;
end
ready_src = (~valid_dst) | ready_dst
/*valid_dst: 在master发请求(拉高valid_src)时拉高valid_dst,直到当前master没有valid请求并且slave可以接收请求(拉高ready_dst)时拉低valid_dst,表示一次传输完成。
payload_dst: 在master发请求(拉高valid_src),并且前面没有请求、请求已经被接收或者正在被接收时将payload_src打拍赋给payload_dst。
其实master本身也会遵循valid-ready协议,payload_src和valid_src做同样处理就行,即也可以在(valid_src == 1'd1 && ready_src == 1'd0)时进行赋值,因为此时payload_src输入应该约束保持原始数据。
ready_src: register slice或者slave可以接收数据时拉高ready_src.
*/
Backward Registered
//Backward Registered 打拍相比较 Forward Registered 会复杂点,因为存在 slave 没有 ready 时 master 发来请求,需要暂存 payload 的场景。
always @(posedge clk or negedge rst_n)begin
if (rst_n == 1'd0)
valid_tmp0 <= 1'd0;
else if (valid_src == 1'd1 && ready_dst == 1'd0 &&valid_tmp0 == 1'd0)
valid_tmp0 <= #`DLY 1'd1;
else if (ready_dst == 1'd1)
valid_tmp0 <= #`DLY 1'd0;
end
always @(posedge clk or negedge rst_n)begin
if (rst_n == 1'd0)
payload_tmp0 <= 'd0;
else if (valid_src == 1'd1 && ready_dst == 1'd0 &&valid_tmp0 == 1'd0)
payload_tmp0 <= #`DLY payload_src;
end
assign payload_dst = (valid_tmp0 == 1'd1) ?payload_tmp0 : payload_src;
always @(posedge clk or negedge rst_n)begin
if (rst_n == 1'd0)
ready_src <= 1'd0;
else
ready_src <= #`DLY ready_dst;
end
/*
ready_src: 对ready通路直接进行打拍。
valid_dst: 当slave没有ready,master发来请求时拉高标志位valid_tmp0,表示下一次slave准备好之后应该从register slice内暂存的payload拿数据
payload_dst: 当slave没有ready,master发来请求时暂存payload到payload_tmp。最终的payload_dst根据标志位valid_tmp0从payload_tmp和payload_src之间选择
*/
Fully Registered
类似于,简单理解就是个乒乓BUFFER,使用非空信号做valid_dst;payload的非满信号做ready_src
Pass Through Mode 该方式不作考虑 实习上并未做任何处理
实际上我们在考虑打拍时,重要的点在于ready的打拍是如何实现的
在思考打拍时 主要有这么两点需要考虑
情况1 : 当后级的ready拉低后,而自身ready没有拉低,此时会出现丢数的情况----数据2在输出端口无法握手,而丢失(因为前级还在收数,对数据进行了覆盖)
情况2:当后级的ready拉高,而自身ready未拉高时,这时会出现重复
解决方法为引入 skid buffer,所谓skid, 即在data_o_ready拉低后,data_i接口是"滑入"STOP状态的,而不是立即停止的。
// ports
output reg data_i_ready;
input data_i_valid;
input [DW-1:0] data_i;
input data_o_ready;
output data_o_valid;
output [DW-1:0] data_o;
// signals
wire buf_valid;
reg [DW-1:0] buf_data;
// data_i_ready由组合逻辑改为时序逻辑
// assign data_i_ready = ~data_o_valid || data_o_ready;
always @ (posedge clk or negedge rst_n) begin
if(!rst_n)
data_i_ready <= 0; //有个buf放在那,因此复位后至少可以收一个数据。
else if(data_o_ready)
data_i_ready <= 1;
else if(data_i_valid) //在data_o_ready拉低后的那一个周期,如果没有数据过来,表明buf为空,可以进数据,data_i_ready保持为1
data_i_ready <= 0; //如果有数据过来,buf被占,则不允许再进数据了,data_i_ready拉低。
end
// ************关于buf_valid和buf_data有2种方案
/*
方案1:所有输入端口的数据都会进入buf,但其实只有(!data_o_ready && data_i_valid && data_i_ready)时刻进去才会发挥作用
优点:控制逻辑简单;缺点:功耗有所增加
*/
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) //可能有些控制信号通过本模块的data端口进行传输,因此有必要进行复位。
buf_data <= 0;
else if(data_i_ready) //不挑时刻,所有数据都进,但是真正有效的还是(!data_o_ready && data_i_valid && data_i_ready)时刻的数据
buf_data <= data_i;
end
assign buf_valid = ~data_i_ready;
/*
方案2:只有(!data_o_ready && data_i_valid && data_i_ready)时刻数据才会进入buf
优点:功耗略低;缺点:控制逻辑稍微复杂点
*/
always @ (posedge clk or negedge rst_n) begin
if(!rst_n)
buf_data <= 0;
else if(!data_o_ready && data_i_valid && data_i_ready) //该条件下,将向buf中存入数据
buf_data <= data_i;
end
// 这段其实等效于assign buf_valid = ~data_i_ready; 写成下面这样更好理解
always @ (posedge clk or negedge rst_n) begin
if(!rst_n)
buf_valid <= 0;
else if(data_o_ready)
buf_valid <= 0; //当data_o_ready拉高,优先读走buf中的数据。
else if(!data_o_ready && data_i_valid && data_i_ready) //该条件下,将向buf中存入数据
buf_valid <= 1;
end
// out,如果buf中有数据,优先读走buf中的数据
assign data_o_valid = data_i_ready? data_i_valid : buf_valid;
assign data_o = data_i_ready? data_i : buf_data;
此外还有一个更简单的方式,就是直接使用fifo进行数据的交互。
反压
应用场景:入口流量大于出口流量时,就需要反压
常用的反压方法:
(1)不带存储器的反压
后级反压信号对本级模块中所有流水reg都进行控制
优点:节省面积资源
缺点:寄存器端口控制复杂
适用情况:流水线深度较大时
(2)带存储体的逐级反压
流水级数不深,可以在每一个需要握手交互
类似该图,只需要对源头进行反压,但是流水线本身不受影响,此时后级需要fifo、ram等存储元件
优点:各级流水寄存器端口控制简单
缺点:需要额外存储体
适用情况:流水线深度较小,每一模块都包含存储体时
(3)带存储体的跨级反压
很多时候在具体设计过程中,颗粒度划分不精细,反压这时候是对模块而言,而不是说模块内部有多少级流水。此外,并不是每一模块都带有存储体。比如,其中可能a模块没有存储体,b模块没有存储体,但ab模块内部还有多级流水,如果c模块有存储体,并且需要反压前级模块,这时候可以选择反压a模块的源头输入数据,然后将ab的流水都存储到带有存储体的c模块,但是如果ab不是都没有存储体的话,就不应该跨级反压,而应该逐级反压
优点:控制简单方便
缺点:需要额外存储体,模块间耦合度高
适用情况:某些模块带存储体,某些模块不带存储体时
问题答案
module slice1 #(
parameter WIDTH = 8
)(
input clk,
input rst_n,
input tx_vld,
input [WIDTH-1:0] tx_data,
output reg tx_rdy,
output reg rx_vld,
output reg [WIDTH-1:0] rx_data,
input rx_rdy
);
reg [WIDTH-1:0] data_buffer;
reg data_buffer_vld;
always @(posedge clk or negedge rst_n) begin
if(~rst_n)
tx_rdy <= 1'b0;
else
tx_rdy <= rx_rdy || ~(rx_vld && tx_vld) || data_buffer_vld;
end
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
data_buffer <= 'd0;
data_buffer_vld <= 1'b0;
end else if(tx_rdy && rx_vld && (~rx_rdy) && tx_vld)begin
data_buffer <= tx_data;
data_buffer_vld<= 1'b1;
end else if(rx_rdy) begin
data_buffer <= data_buffer;
data_buffer_vld<= 1'b0;
end
end
always @(posedge clk or negedge rst_n) begin
if(~rst_n) begin
rx_vld <= 1'b0;
rx_data<= 'd0;
end else if(rx_rdy & data_buffer_vld)begin
rx_vld <= 1'b1;
rx_data<= data_buffer;
end else if(tx_rdy & tx_vld & rx_rdy)begin
rx_vld <= 1'b1;
rx_data<= tx_data;
end else begin
rx_vld <= 1'b0;
rx_data<= 'd0;
end
end
endmodule
// ues fifo
module slice2 #(
parameter WIDTH = 8,
parameter DEPTH = 8,
parameter POINTER_WIDTH = $clog2(DEPTH)
)(
input clk,
input rst_n,
input tx_vld,
input [WIDTH-1:0] tx_data,
output tx_rdy,
output reg rx_vld,
output reg [WIDTH-1:0] rx_data,
input rx_rdy
);
reg [WIDTH-1:0] RAM[0:DEPTH];
reg [POINTER_WIDTH:0] wr_cnt,rd_cnt;
wire wr_en;
wire rd_en;
reg full;
reg empty;
assign wr_en = tx_vld & tx_rdy;
assign rd_en = rx_rdy & rx_vld;
always @(*) begin
if(~rst_n)
full <= 1'b0;
else if( (wr_cnt[POINTER_WIDTH-1:0] == rd_cnt[POINTER_WIDTH-1:0]) && (wr_cnt[POINTER_WIDTH] != rd_cnt[POINTER_WIDTH]))
full <= 1'b1;
else
full <= 1'b0;
end
always @(*) begin
if(~rst_n)
empty <= 1'b1;
else if(wr_cnt == rd_cnt)
empty <= 1'b1;
else
empty <= 1'b0;
end
always @(posedge clk) begin
if(!rst_n)
wr_cnt <= 0;
else if(wr_en & (!full) )
wr_cnt <= wr_cnt + 1;
else
wr_cnt <= wr_cnt;
end
always @(posedge clk) begin
if(!rst_n)
rd_cnt <= 0;
else if( rd_en & (!empty) )
rd_cnt <= rd_cnt + 1;
else
rd_cnt <= rd_cnt;
end
always @(posedge clk) begin
if(wr_en && (!full) )
RAM[wr_cnt[POINTER_WIDTH-1:0]] <= tx_data;
end
always @(posedge clk) begin
if(rd_en & (!empty) )
rx_data <= ram[rd_cnt[POINTER_WIDTH-1:0]];
end
assign tx_rdy = ~full; //只要非满就能够写入
assign rx_vld = ~empty; //只要非空就能够读出
endmodule
参考链接
1.流水线中的valid-ready握手控制打拍 - 知乎 (zhihu.com) 这里面给出的链接从头开始看即可,下面列举不在其中的链接