4、单周期握手:最简单的握手实现,Verilog代码与波形分析

各位同学,今天我们来聊聊握手设计里最基础、也最直观的一种——单周期握手。

说实话,我刚入行那会儿,第一次接触握手协议,就被各种valid/ready的时序搞得晕头转向。后来带我的老工程师跟我说了一句话,我到现在还记得:「你先别想复杂的,就从最简单的单周期握手开始。」

嗯,今天我就把这句话送给你。

4.1 什么是单周期握手?

单周期握手,说白了就是一个时钟周期内完成一次数据传输。

它的核心思想很简单:

  • 发送方把数据准备好,同时拉高valid信号
  • 接收方在同一个时钟周期内,如果准备好接收,就拉高ready信号
  • valid和ready同时为高时,数据被成功传输

你想想看,这就像两个人面对面站着,一个人递东西,另一个人伸手接。只要双方同时伸手,东西就交接完成了。整个过程只需要一拍。

4.2 单周期握手的信号定义

我们通常用三个信号来实现单周期握手:

信号名 方向 说明
valid 发送方 → 接收方 数据有效标志,高电平表示发送方已准备好数据
ready 接收方 → 发送方 接收就绪标志,高电平表示接收方可以接收数据
data 发送方 → 接收方 数据总线,宽度根据设计需求定义

这里有个关键点:valid和ready的握手条件。握手成功(即数据传输完成)的条件是:valid & ready 同时为高。

核心公式:

传输完成 = valid & ready

数据被采样 = valid & ready(在时钟上升沿)

4.3 Verilog代码实现

好,我们直接上代码。我个人习惯把发送方和接收方分开写,这样结构更清晰。

4.3.1 发送方模块

module sender (
    input  clk,
    input  rst_n,
    input  ready,          // 来自接收方的就绪信号
    output reg valid,      // 数据有效标志
    output reg [7:0] data  // 8位数据总线
);

// 内部计数器,用于产生测试数据
reg [3:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        cnt   <= 4'd0;
        valid <= 1'b0;
        data  <= 8'd0;
    end
    else begin
        // 当握手成功时,更新数据
        if (valid & ready) begin
            cnt   <= cnt + 1'b1;
            data  <= data + 8'd1;
            // 这里可以控制是否连续发送
            // 如果希望连续发送,valid保持为高
        end
        
        // 简单的控制逻辑:每4个时钟周期发送一次数据
        if (cnt == 4'd3) begin
            valid <= 1'b1;
        end
        else if (valid & ready) begin
            valid <= 1'b0;  // 发送完成后拉低valid
        end
    end
end

endmodule

4.3.2 接收方模块

module receiver (
    input  clk,
    input  rst_n,
    input  valid,          // 来自发送方的有效标志
    output reg ready,      // 接收就绪标志
    input  [7:0] data,     // 数据总线
    output reg [7:0] data_out  // 接收到的数据
);

// 接收方准备好接收的条件
// 这里我们假设接收方总是准备好,但也可以加入反压逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        ready    <= 1'b0;
        data_out <= 8'd0;
    end
    else begin
        // 默认ready为高,表示随时可以接收
        ready <= 1'b1;
        
        // 当握手成功时,采样数据
        if (valid & ready) begin
            data_out <= data;
        end
    end
end

endmodule

小提示:上面的接收方代码中,我把ready默认拉高了。实际项目中,接收方可能因为内部FIFO满等原因需要拉低ready,这就是反压机制。我们后面章节会详细讲。

4.4 波形分析

代码写完了,我们来看看波形。我画了一张时序图,帮你理解单周期握手的过程。

单周期握手时序图 clk T0 T1 T2 T3 T4 T5 T6 valid ready data 0x00 0x01 0x02 0x03 ✓ ✓ ✓ ✓ = 握手成功(valid & ready 同时为高)

从波形图可以看出:

  • T0时刻:valid为低,ready为高,没有数据传输
  • T1时刻:valid变为高,ready保持高,握手条件满足,数据0x00被传输
  • T2时刻:valid拉低,ready拉低,没有传输
  • T3时刻:valid再次拉高,ready也拉高,数据0x01被传输
  • T4-T6时刻:重复上述过程,数据0x02、0x03依次传输

注意:单周期握手要求valid和ready在同一个时钟周期内同时为高。如果ready在valid为高后的下一个周期才拉高,那就不是单周期握手了,而是多周期握手。我曾经在项目中遇到过这种情况,导致数据丢失,排查了好久才发现是ready信号延迟了一拍。

4.5 单周期握手的优缺点

任何设计都有取舍,单周期握手也不例外。

优点

  • 实现简单:逻辑清晰,代码量少,适合初学者上手
  • 延迟低:数据在一个周期内完成传输,没有额外等待
  • 资源占用少:不需要额外的状态机或计数器

缺点

  • 时序约束严格:valid和ready的组合逻辑路径必须在半个周期内完成,高频下容易出问题
  • 灵活性差:接收方必须在一个周期内做出响应,无法处理复杂的反压场景
  • 不适用于跨时钟域:单周期握手要求双方在同一时钟域下工作

我的经验:单周期握手最适合用在模块内部的数据通路上,比如ALU和寄存器堆之间的数据传输。如果是跨模块或者跨时钟域,我建议你用更稳健的握手方式,比如双周期握手或者FIFO。

4.6 避坑指南

最后,我分享几个我在项目中踩过的坑:

  1. valid和ready的时序关系:我曾经以为只要valid和ready都为高,数据就能正确传输。但忽略了setup/hold时间,导致高频下数据采样错误。记住:数据必须在时钟上升沿之前稳定。
  2. ready信号的产生:不要把ready做成纯组合逻辑,尤其是当接收方内部有流水线时。我建议用寄存器打一拍,虽然多了一个周期延迟,但时序更可控。
  3. 数据总线的保持:握手成功后,发送方不能立即改变数据,必须保持到下一个时钟上升沿。否则接收方可能采到错误数据。

好了,单周期握手的内容就讲到这里。它虽然简单,但却是理解更复杂握手协议的基础。你把这个搞懂了,后面学双周期握手、四周期握手就会轻松很多。

可可精品资料库