一、前言
- valid和ready信号,尤其是ready信号的时序一般很差,因为它通常是接收端通过组合逻辑输出的。当流水线的级数较多时,ready反压信号一级一级往前传递,时序将会变得更差。
- 为了优化时序,通常需要对valid和ready信号进行打拍处理。但是由于握手信号的特点,使得这两个信号直接打拍时会发生协议错误,此时就需要使用一些技巧来解决这个问题。
通常对于握手信号的打拍处理有以下三种方式:
- Forward Register Slice:仅处理valid和data信号的打拍
- Backward Register Slice:仅处理ready信号的打拍
- Full Register Slice:同时处理valid信号与ready信号的打拍
二、Forward Register Slice
module valid_flop
(
CLK,
RESET,
VALID_UP,
READY_UP,
DATA_UP,
VALID_DOWN,
READY_DOWN,
DATA_DOWN
);
//-----------------------------------------------------------------------------
parameter WIDTH = 32 ;
//-----------------------------------------------------------------------------
input CLK;
input RESET;
input VALID_UP;
output READY_UP;
input [WIDTH-1:0] DATA_UP;
output VALID_DOWN;
input READY_DOWN;
output [WIDTH-1:0] DATA_DOWN ;
//-----------------------------------------------------------------------------
wire CLK;
wire RESET;
wire VALID_UP;
wire READY_UP;
wire [WIDTH-1:0] DATA_UP;
//Down Stream
reg VALID_DOWN;
wire READY_DOWN;
reg [WIDTH-1:0] DATA_DOWN;
//-----------------------------------------------------------------------------
//Valid
always @(posedge CLK)
if (RESET) VALID_DOWN <= 1'b0;
else VALID_DOWN <= READY_UP ? VALID_UP : VALID_DOWN;
//Data
always @(posedge CLK)
if (RESET) DATA_DOWN <= {WIDTH{1'b0}};
else DATA_DOWN <= (READY_UP && VALID_UP) ? DATA_UP : DATA_DOWN;
//READY with buble collapsing.
assign READY_UP = READY_DOWN || ~VALID_DOWN;
//READY with no buble collapsing.
//assign READY_UP = READY_DOWN;
endmodule
中心思想:在接收端握手成功之前,发送端要将valid和data一直保持住!为了保证接收端在ready_i(READY_DOWN)信号有效时能够采集到data,data必须提前打拍。data在打拍时,使用的是接收端传递给发送端的ready_o(READY_UP)信号,而为了实现提前打拍,ready_o(READY_UP)的值可以等于
ready_i(READY_DOWN)|| ~ (valid_o)VALID_DOWN
。
三、Backward Register Slice
芯片设计-skid buffer(ready打断) 这篇文章中详细介绍的方案一就是典型的skid buffer处理方式。
// 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 <= 1; //有个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;
中心思想:在情况1时刻,如果输入端有数据进入,则使用buf对该数据进行暂存。在情况2时刻,如果buf中存有数据,则优先输出buf中的数据。
四、Full Register Slice
https://zhuanlan.zhihu.com/p/212356622 这篇文章中的FIFO处理模式就是Full Register Slice。