数字电路valid-ready握手协议浅析(handshake protocol)

  1. 握手协议背景介绍


我第一次接触握手协议,是在一次fpga开发工程师实习面试中。当时面试官通过邮件给我发送了一个题目:实现对握手协议的打拍,要求传输无气泡。作为萌新的我只能查阅互联网。在使用该协议一段时间后,我总结了一些冠以该协议的知识点以及使用该协议时存在的一些问题,在此总结以便于看到此文章的读者使用握手协议设计自己的rtl代码。由于作者经验有限,如果文章有错误或者不理解之处,欢迎在评论中给我留言😊。

握手协议是一种可以实现数据安全传输的协议,其适用于上下游模块之间的数据传输。其广泛应用于AXI总线以及流水线设计中。

对于上下游模块之间的数据传输,最简单的模式就是连接一个数据通道(data信号)。但是它存在两个致命问题:

  1. 下游模块并不知道在哪段时间内,该数据通道内的数据是有效的。

  1. 当下游模块发生阻塞时,无法反压上游,使得上游暂停数据的发送

为了解决上述问题,我们又引入了两个信号 : valid 和 ready 。 

当valid拉高时,代表该时间段内数据有效。

当ready拉低时,下游可以反压上游,使得其不再发送数据。

valid和ready就是两只大手,当这两只手握在一起时,就代表这一拍的数据安全从上游传递到了下游,因此,握手协议最重要的就是保证了数据的有效性和安全性。

数据安全性:在数据传输过程中,保证不会存在存在数据重复或者丢失的情况

module handshake_signal#(
    parameter data_width = 32
)(
    //上游数据输入接口
    input [data_width - 1 : 0]     din,
    input                          din_valid,
    output                         dout_ready,

    //下游数据输出接口
    output [data_width - 1 : 0]    dout,
    output                         dout_valid,
    input                          dout_ready
);

endmodule

  1. 握手协议的死锁情况


试想,当我们和别人握手时,如果双方都不主动伸出手,双方都在等待对方伸出手的话,那么我们和他人的握手就永远不会发生。同理,在我们设计rtl代码时,当我们上游的模块以ready作为数据发送的触发条件,而下游模块以valid作为数据接受的触发条件时,那么这两个模块之间就永远不会发送数据传送。这就是所谓的握手协议的死锁情况。

因此当我们设计rtl代码时,一定要规定好主动发起握手的一端,例如下游主动拉高ready信号或者上游主动拉高valid信号。同时也要避免间接的使用valid或ready进行判断而导致的组合逻辑回环问题。

在要使用fifo作为数据缓冲的总线中,我们也可以设计带有握手协议的fifo,该fifo的上游端口ready信号会主动拉高,而下游端口valid信号,会在当fifo内存在数据时主动拉高。这样设计的好处是我们可以在fifo上游和下游的数据处理模块中直接使用valid和ready作为判断条件,而不必关注总线握手发生死锁的情况。


  1. 握手协议的时序优化


由于握手协议经常使用在总线中,总线往往会有较高的信号扇出以及通过仲裁的情况。而一端总线的上下游数据处理模块也可能有较多的组合逻辑。因此对采用握手协议的总线进行时序优化是必要的。

当我们进行时序优化时,最常想到的就是在组合逻辑路径较长的地方插入寄存器,打断组合逻辑路径。在握手协议中,就是在valid,ready以及data这三个数据路径中插入寄存器。但是在插入寄存器后,valid和ready会由于寄存器而延迟一拍,这样可能会造成数据传输时,不能保证数据的安全性。因此我们分类讨论三种情况:

  1. 只对valid和data插入寄存器

module handshake_signal#(
    parameter data_width = 32
)(
    //时钟和复位信号
    input                          clk,
    input                          rst_n,

    //上游数据输入接口
    input [data_width - 1 : 0]     din,
    input                          din_valid,
    output                         dout_ready,

    //下游数据输出接口
    output [data_width - 1 : 0]    dout,
    output                         dout_valid,
    input                          dout_ready
);

reg [data_width - 1 : 0] data_reg;
reg                      valid_reg;

always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        data_reg  <= 'b0;
        valid_reg <= 1'b0;
    end else begin
        data_reg  <= din;
        valid_reg <= din_valid;
    end
end

assign dout       = data_reg;
assign dout_valid = valid_reg;
assign din_ready  = dout_ready;


endmodule

出现错误的情况:如下图所示,如果ready没有拉高,此时数据上游保持原来数据不变。valid和data寄存器中保持一份上游数据。当下游ready拉高时,会先将打拍寄存器中的值读走。此时打拍寄存器会再保存一份上游数据,而上游数据接收到ready时,原先数据就不再保持了。再过一个时钟周期,下游会读取打拍寄存器中的值,而此刻打拍寄存器传递给下游的值还是上游的旧值。这个旧值被传递了两次。因此会导致下游接收数据的重复,从而无法保证数据传递的安全性。

出现错误的时序图:

总结: ready上升沿出现错误,上游数据重复一拍

  1. 只对ready插入寄存器

module handshake_signal#(
    parameter data_width = 32
)(
    //时钟和复位信号
    input                          clk,
    input                          rst_n,

    //上游数据输入接口
    input [data_width - 1 : 0]     din,
    input                          din_valid,
    output                         dout_ready,

    //下游数据输出接口
    output [data_width - 1 : 0]    dout,
    output                         dout_valid,
    input                          dout_ready
);

reg                      ready_reg;

always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        ready_reg <= 1'b0;
    end else begin
        ready_reg <= dout_ready;
    end
end

assign dout       = din;
assign dout_valid = din_valid;
assign din_ready  = ready_reg;

endmodule

数据丢失的情况:

对ready信号插入寄存器后,由于寄存器延迟1个clock,会导致下游无法及时反压上游数据。上游向下游传递数据时,丢失1个clock的数据。

由下图所示,在clock0时:

数据上游模块行为:上游模块接收到ready0信号,此时上游模块满足握手条件(valid0,ready0),上游模块认为data0已经传递到下游模块,因此上游模块将data0切换为data1

数据下游模块行为:下游模块满足的握手条件(valid0,ready1),下游模块接收到data0。

在clock1时:

数据上游模块行为:上游模块接收到ready1信号,此时上游模块满足握手条件(valid1,ready1),

上游模块认为data1已经安全传输到下游,因此上游模块将传输数据切换为data2

数据下游模块行为:下游模块接收到上游的data1和valid1信号,但是此时下游模块没有ready信号,因此下游模块不满足握手条件,data1未存入下游模块中,因此data1丢失

在clock2时:

数据上游模块行为:上游模块不满足握手条件,因此上游模块保持data2数据不变

数据下游模块行为:下游模块不接收上游数据,也不发起ready

总结 :ready下降沿出现错误,上游数据丢失一拍

出现错误的时序图:

数据重复的情况:

对ready信号插入寄存器后,由于寄存器延迟1个clock,上游数据无法及时收到ready信号,会导致下游数据接收两次上游传递的数据。

由下图所示:

在clock0时:

数据上游模块行为:上游模块主动发起valid0和data0的传输。

数据下游模块行为:未准备好接收上游发送的数据。

在clock1时:

数据上游模块行为:未收到ready信号,因此继续保持data0和valid0

数据下游模块行为:下游开始接收数据,此时由于valid0和ready0有效,下游模块达成握手条件,data0被下游模块处理。

在clock2时:

数据上游模块行为:上游收到ready0信号,因此上游模块将传输数据切换为data1

数据下游模块行为:下游正常接收数据,此时由于valid0和ready1有效,下游模块达成握手条件,data0被下游模块处理,导致data0被重复处理两次。

出现错误的时序图:

总结:ready上升沿出现错误,上游数据重复一拍

  1. 对valid,data以及ready都插入寄存器

module handshake_signal#(
    parameter data_width = 32
)(
    //时钟和复位信号
    input                          clk,
    input                          rst_n,
    
    //上游数据输入接口
    input [data_width - 1 : 0]     din,
    input                          din_valid,
    output                         dout_ready,

    //下游数据输出接口
    output [data_width - 1 : 0]    dout,
    output                         dout_valid,
    input                          dout_ready
);

reg [data_width - 1 : 0] data_reg;
reg                      valid_reg;
reg                      ready_reg;

always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        data_reg  <= 'b0;
        valid_reg <= 1'b0;
        ready_reg <= 1'b0;
    end else begin
        data_reg  <= din;
        valid_reg <= din_valid;
        ready_reg <= dout_ready;
    end
end

assign dout       = data_reg;
assign dout_valid = valid_reg;
assign din_ready  = ready_reg;

endmodule

是对以上两种情况的总和。即:ready的上升沿和下降沿都会出现错误,在ready上升沿时,数据丢失一拍,在ready下降沿时,上游数据丢失一拍

在对以上情况进行分析后,我们明确了数据重复以及丢失的情况。因此对于valid data 以及 ready信号进行简单打拍是无法保证数据传输的安全性的。因此我们需要对握手信号打拍进行特殊处理。

出现错误的时序图:

解决方法:

使用带有握手信号的fifo。原因有两个:

  1. 在使用握手信号的fifo时,由于fifo内部的空慢判断是由指针控制的,而指针的增减会绝对准确的知道上游写入fifo数据的个数和下游读出fifo数据的个数。这样就不会出现数据重复或者丢失的情况

  1. 由于使用两个计数器来作为判断条件,主动发起上游ready或者下游valid,这两个计数器的本质也是寄存器。用计数器作为判断条件,我们就会将上下游的数据用寄存器隔离开。这样会有利于时序优化。

//此设计为深度为1的fifo,可以用作对握手信号的打拍
//可以在此设计的基础上进行适当的更改,加大fifo的深度
module sync_fifo#(
    parameter data_width = 32
)(
    //时钟和复位信号
    input                          clk,
    input                          rst_n,
    
    //上游数据输入接口
    input [data_width - 1 : 0]     din,
    input                          din_valid,
    output                         dout_ready,

    //下游数据输出接口
    output [data_width - 1 : 0]    dout,
    output                         dout_valid,
    input                          dout_ready
);

reg [data_width - 1 : 0] data_reg;
reg                      valid_reg;
reg                      ready_reg;

reg [1:0]                din_counter;
reg [1:0]                dout_counter;

always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        din_counter <= 'b0;
    end else begin
        if(din_valid & din_ready)begin
            din_counter <= din_counter + 1'b1;
        end
    end
end

always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        dout_counter <= 'b0;
    end else begin
        if(dout_valid & dout_ready)begin
            dout_counter <= dout_counter + 1'b1;
        end
    end
end


always @(posedge clk or negedge rst_n)begin
    if(!rst_n)begin
        data_reg     <= 'b0;
        valid_reg    <= 1'b0;
        ready_reg    <= 1'b0;
    end else begin
        if(din_counter[1] == dout_counter[1]) begin
            if(din_counter[0] > dout_counter[0]) begin
                ready_reg   <= 1'b0;
                data_reg    <= data_reg;
                valid_reg   <= valid_reg;
            end else begin
                ready_reg   <= 1'b1;
                data_reg    <= din;
                valid_reg   <= din_valid;
            end
        end else begin
            if(din_counter[0] < dout_counter[0]) begin
                ready_reg   <= 1'b0;
                data_reg    <= data_reg;
                valid_reg   <= valid_reg;
            end else begin
                ready_reg   <= 1'b1;
                data_reg    <= din;
                valid_reg   <= din_valid;
            end
        end
    end
end

assign dout       = data_reg;
assign dout_valid = valid_reg;
assign din_ready  = ready_reg;

endmodule

在使用skid buffer时,其本质也是一个深度为1的fifo,但是由于在设计时,有可能会用到下游的信号来判断上游的valid或ready状态,当出现判断时,其本质也是组合逻辑,会导致上下游信号直接进行组合逻辑连接。因此在使用该方法时,需要注意判断条件导致的组合逻辑路径过长

module skid_buffer #(
    parameter data_width = 64
)(
    //时钟和复位信号
    input                          clk,
    input                          rst_n,
    
    //上游数据输入接口
    input [data_width - 1 : 0]     din,
    input                          din_valid,
    output                         dout_ready,

    //下游数据输出接口
    output [data_width - 1 : 0]    dout,
    output                         dout_valid,
    input                          dout_ready
);

reg                      pre_dout_ready;
reg [data_width - 1 : 0] din_reg_0;
reg                      din_valid_reg_0;
reg                      dout_ready_reg_0;
reg [data_width - 1 : 0] din_reg_1;
reg                      din_valid_reg_1;

//未完待续...
endmodule


  1. 总结


在高速数字电路设计中,使用流水线加速数据的计算是一种很好的方法。而在流水线计算中,几乎一定要使用握手协议,作为流水线上下游模块的数据传输总线。

当我们使用握手协议的总线时,首先要明确该总线上下游的模块对于valid和ready处理的方式,是它主动发起valid或ready还是需要我们自己设计的模块主动发起valid和ready,以避免握手协议死锁情况的出现。同时,对于该类型总线进行时序优化,插入寄存器打拍时,一定要注意数据安全性,即数据是否丢失或者数据是否重复。

对于上述问题,插入一个握手协议的fifo或者使用skid buffer是一个很好的解决方法。


参考资料:

  1. https://zipcpu.com/blog/2019/05/22/skidbuffer.html

  1. https://zipcpu.com/blog/2021/08/28/axi-rules.html


WaveDrom Editor 代码段

  1. 对valid和data打拍的时序图:

{signal: [
  {    name: 'clk',   wave: 'PPPPPP'},
  ['上游数据端',
   {  name: 'din',          wave: '033400', data: 'data0 data0 data1 data1 data1'},
   {  name: 'din_valid',     wave: '011100', data: 'D1'   },
   {  name: 'din_ready',     wave: '001110', data: 'D1'   },
  ],
  {},
  ['中间寄存器',
   {  name: 'data_reg',      wave: '003340', data: 'data0 data0 data1'},
   {  name: 'valid_reg',     wave: '001110', data: 'D1'   },
//   {  name: 'ready_reg',     wave: '000100', data: 'D1'   },
  ],   
   
  {},
  ['下游数据端',
   {  name: 'dout',         wave: '003340', data: 'data0 data0 data1'},
   {  name: 'dout_valid',     wave: '001110', data: 'Q2'},
   {  name: 'dout_ready',     wave: '001110', data: 'Q2'},
  ]
]}
  1. 对ready打拍,ready上升沿时序图:

{signal: [
  {    name: 'clk',   wave: 'PPPPPP'},
  ['上游数据端',
   {  name: 'din',          wave: '033400', data: 'data0 data0 data1 data1 data1'},
   {  name: 'din_valid',     wave: '011100', data: 'D1'   },
   {  name: 'din_ready',     wave: '001110', data: 'D1'   },
  ],
  {},
  ['中间寄存器',
//   {  name: 'data_reg',      wave: '003340', data: 'data0 data0 data1'},
//   {  name: 'valid_reg',     wave: '001110', data: 'D1'   },
   {  name: 'ready_reg',     wave: '001110', data: 'D1'   },
  ],   
   
  {},
  ['下游数据端',
   {  name: 'dout',         wave: '033400', data: 'data0 data0 data1'},
   {  name: 'dout_valid',     wave: '011100', data: 'Q2'},
   {  name: 'dout_ready',     wave: '011100', data: 'Q2'},
  ]
]}
  1. 对ready打拍,ready下降沿时序图:

{signal: [
  {    name: 'clk',   wave: 'PPP'},
  ['上游数据端',
   {  name: 'din',          wave: '340', data: 'data0 data1'},
   {  name: 'din_valid',     wave: '110', data: 'D1'   },
   {  name: 'din_ready',     wave: '110', data: 'D1'   },
  ],
  {},
  ['中间寄存器',
//   {  name: 'data_reg',      wave: '003340', data: 'data0 data0 data1'},
//   {  name: 'valid_reg',     wave: '001110', data: 'D1'   },
   {  name: 'ready_reg',     wave: '110', data: 'D1'   },
  ],   
   
  {},
  ['下游数据端',
   {  name: 'dout',         wave: '340', data: 'data0 data1'},
   {  name: 'dout_valid',     wave: '110', data: 'Q2'},
   {  name: 'dout_ready',     wave: '100', data: 'Q2'},
  ]
]}

  • 12
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Valid-Ready握手协议在Verilog设计中用于数据传输的同步,确保数据的有效性和可靠性。它通常用于两个模块之间的数据交换,并且在发送数据之前,接收方必须准备好接收数据。 下面是一个简单的Verilog代码示例,演示了Valid-Ready握手协议的基本原理: ```verilog module ValidReadyHandshake ( input wire clk, input wire reset, input wire data_valid, output wire data_ready, input wire [DATA_WIDTH-1:0] data_in, output wire [DATA_WIDTH-1:0] data_out ); // 内部状态定义 reg [DATA_WIDTH-1:0] internal_data; reg internal_valid; reg internal_ready; // 同步时钟 always @(posedge clk) begin if (reset) begin internal_ready <= 1'b0; internal_data <= {DATA_WIDTH{1'b0}}; internal_valid <= 1'b0; end else begin internal_ready <= data_ready; internal_data <= data_in; internal_valid <= data_valid; end end // 数据输出逻辑 assign data_out = internal_data; // Ready信号逻辑 assign data_ready = internal_ready && !internal_valid; endmodule ``` 在这个示例中,Valid-Ready握手协议的发送方将数据放入`data_in`端口,并设置`data_valid`信号为高电平。接收方通过`data_ready`信号表示它已经准备好接收数据。当接收方准备好时,它将`data_ready`信号设置为高电平,发送方将通过`data_ready`信号的状态来判断是否可以发送数据。 值得注意的是,`clk`和`reset`信号是必需的,用于同步时钟和复位。 这只是一个基本示例,你可以根据具体的设计需求对握手协议进行扩展和修改。希望这个示例对你有所帮助!如果你有更多问题,请随时提问。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值