1. 项目概述:握手协议在跨时钟域通信中的核心价值
在FPGA和复杂数字系统的设计中,跨时钟域信号处理是一个绕不开的经典难题。当数据或控制信号需要从一个时钟域传递到另一个与之完全异步的时钟域时,如果处理不当,亚稳态、数据丢失或采样错误等问题就会接踵而至,导致系统行为不可预测。我遇到过不少项目,前期功能仿真一切正常,一旦上板实测,间歇性的数据错误就冒出来了,排查起来极其头疼,根源往往就出在跨时钟域处理这块。
针对这个问题,工程师们发展出了多种成熟的解决方案,比如简单的两级触发器同步、异步FIFO、以及这次要深入探讨的握手协议。输入材料中提到的“先异步暂存,后同步写入”的方法,其核心思想就是握手。与单向同步或FIFO相比,握手协议更像是一种“通信礼仪”:发送方和接收方通过专用的请求和应答信号进行“对话”,确保每一次数据传输都得到对方的确认后才算完成。这种方式虽然会引入一定的通信延迟,但其最大的优势在于极高的可靠性和对任意时钟频率比率的天然适应性。无论是快时钟到慢时钟,还是慢时钟到快时钟,甚至是频率关系不固定的时钟域之间,握手协议都能稳健地工作。
这篇文章,我将结合自己多年的实战经验,为你彻底拆解基于握手协议的跨时钟域通信。我会从原理讲起,用状态机的方式带你一步步实现发送端和接收端,并通过仿真和上板测试,验证其在极端时钟比例下的可靠性。无论你是正在学习FPGA的在校学生,还是工作中遇到类似问题的工程师,相信这份从理论到代码、从仿真到实测的完整记录,都能给你提供一份可靠的“避坑指南”和可直接复用的设计模板。
2. 握手协议原理与设计思路拆解
2.1 为什么需要握手?从亚稳态说起
要理解握手协议的必要性,得先明白跨时钟域传输的根本风险——亚稳态。当一个信号在时钟边沿附近发生变化时,其值在寄存器输出端会在一段时间内处于一个非0非1的中间态,这个状态最终会稳定到0或1,但稳定所需的时间是随机的,可能超过一个时钟周期。如果这个亚稳态信号被后续逻辑使用,就会导致系统功能错误。
简单的两级触发器同步可以极大地降低亚稳态传播的概率,但它只适用于单比特、电平变化不频繁的控制信号。对于多比特的数据总线,如果简单地用同步器对每一位进行同步,由于路径延迟的差异,在接收时钟域采样时,可能会抓到数据变化过程中的一个“中间值”,比如从8‘h00变化到8’hFF时,采到了8‘h0F或8’hF0,这被称为数据歪斜。握手协议通过控制信号来“框定”数据稳定的窗口,完美地避开了这个问题。
2.2 握手协议的工作流程与状态定义
握手协议的核心是两个信号:请求和应答。输入材料中给出的流程图非常经典,我们可以将其转化为更清晰的状态机描述。
发送端状态机:
- 空闲:等待发送条件。数据总线和
req信号均无效。 - 发送数据与请求:将待发送数据放到总线上,然后拉高
req信号,告知接收端“数据已准备好,请接收”。 - 等待应答:持续检测来自接收端的
ack信号。一旦检测到ack有效,则拉低req信号,表示“我知道你收到了”。 - 等待通信结束:持续检测
ack信号。一旦ack被接收端拉低,表示一次完整的握手结束,可以返回状态2,开始下一次传输。
接收端状态机:
- 空闲:等待发送端的请求。
- 等待请求:持续检测发送端的
req信号。一旦检测到req有效,立即锁存当前数据总线上的值,并拉高ack信号,表示“数据已接收”。 - 等待请求释放:持续检测
req信号。一旦检测到req被拉低,则拉低ack信号,完成本次握手,并返回状态2,准备接收下一次数据。
这个“一问一答”的流程,确保了发送端只有在确认接收端已经安全取走数据后,才会更新总线上的数据;接收端也只有在确认发送端知道数据已被取走后,才会准备接收新数据。数据总线在整个req有效期间都保持稳定,为接收端提供了充足的采样窗口。
2.3 关键设计考量:同步器的放置与时序收敛
输入材料的代码中有一个至关重要的细节:ack信号在发送端被两级寄存器同步,req信号在接收端也被两级寄存器同步。这是握手协议正确工作的基石,绝对不能省略。
- 为什么需要同步?
req和ack本身就是跨时钟域的信号。如果不经同步直接用于状态机的条件判断,亚稳态就会直接侵入控制逻辑,导致状态机跑飞。 - 同步器放在哪里?原则是:异步信号进入哪个时钟域,就在哪个时钟域用同步器处理。因此,来自接收时钟域的
ack信号,在发送时钟域用两级触发器同步;来自发送时钟域的req信号,在接收时钟域用两级触发器同步。代码中的ack_reg2和req_reg2就是同步后的、稳定的本地版本信号。 - “便于时序收敛”的含义:在FPGA设计中,时序收敛是指设计满足建立时间和保持时间的要求。将异步信号同步到一个时钟域后,该信号相对于这个时钟域的时序就是确定的,后端工具可以进行静态时序分析,确保建立/保持时间得到满足。如果直接使用异步信号,时序路径的起点是不确定的,工具无法分析,也就谈不上“收敛”。
注意:这里使用的是最简单的两级触发器同步器。在可靠性要求极高的场合(如医疗、航空),可能会使用三级甚至更多级同步器来进一步降低亚稳态失效概率,但这也会增加延迟。对于大多数消费类和工业类应用,两级同步已足够可靠。
3. 核心模块代码实现与逐行解析
接下来,我们基于输入材料中的代码框架,进行更详细的实现和解析。我将补充完整的模块接口、信号定义,并解释每一个状态和操作背后的意图。
3.1 发送端模块实现
发送端模块负责产生数据、发起传输请求,并等待接收端的确认。
module handshake_sender #( parameter DATA_WIDTH = 8, // 数据总线宽度 parameter MEM_DEPTH = 256 // 发送内存深度 )( // 系统信号 input wire t_clk, // 发送端时钟 input wire rst_n, // 低电平有效全局复位 // 与接收端的握手接口 output reg [DATA_WIDTH-1:0] data_out, // 输出到接收端的数据总线 output reg req, // 发送请求,高有效 input wire ack, // 接收端应答,高有效 // 测试用内存接口(实际应用可能来自其他逻辑) output reg [$clog2(MEM_DEPTH)-1:0] TR_MEM_Addr, // 发送内存地址 input wire [DATA_WIDTH-1:0] TR_MEM_Data // 发送内存数据 ); // 状态定义 localparam TR_IDLE = 3'b000; localparam SND_DATA_REQ = 3'b001; localparam CHK_ACK_ACTIVE = 3'b010; localparam CHK_COMM_END = 3'b011; reg [2:0] tr_state; // 发送端状态机当前状态 reg [DATA_WIDTH-1:0] data_buf; // 数据缓冲寄存器 reg ack_reg1, ack_reg2; // 两级同步寄存器链 // --- 关键部分:对异步ack信号进行同步 --- always @(posedge t_clk or negedge rst_n) begin if (!rst_n) begin ack_reg1 <= 1'b0; ack_reg2 <= 1'b0; end else begin ack_reg1 <= ack; // 第一级同步,捕捉异步信号 ack_reg2 <= ack_reg1; // 第二级同步,输出稳定信号 end end // 后续状态机只使用同步后的 ack_reg2 // --- 发送端主状态机 --- always @(posedge t_clk or negedge rst_n) begin if (!rst_n) begin data_buf <= {DATA_WIDTH{1'b0}}; req <= 1'b0; TR_MEM_Addr <= 0; tr_state <= TR_IDLE; end else begin case (tr_state) TR_IDLE: begin // 初始化状态。req拉低,地址归零。 // 在实际系统中,这里可以等待一个“发送使能”信号。 req <= 1'b0; TR_MEM_Addr <= 0; tr_state <= SND_DATA_REQ; // 直接进入发送流程,开始第一次传输 end SND_DATA_REQ: begin // 1. 将数据从内存放到总线上(或来自其他数据源) data_buf <= TR_MEM_Data; // 2. 拉高req信号,向接收端发起请求 req <= 1'b1; // 3. 为下一次传输准备地址(如果内存未读完) if (TR_MEM_Addr < MEM_DEPTH - 1) TR_MEM_Addr <= TR_MEM_Addr + 1; else TR_MEM_Addr <= 0; // 循环读取 // 4. 进入下一个状态,等待接收端确认 tr_state <= CHK_ACK_ACTIVE; end CHK_ACK_ACTIVE: begin // 等待接收端的应答信号。 // 注意:这里判断的是同步后的ack_reg2,而不是原始的ack。 if (ack_reg2 == 1'b1) begin // 接收端已确认收到数据,可以撤销请求信号 req <= 1'b0; tr_state <= CHK_COMM_END; end // 如果ack_reg2为0,则保持在本状态等待。 // 这个等待时间取决于接收端时钟频率和响应速度。 end CHK_COMM_END: begin // 等待接收端释放应答信号,以完成整个握手周期。 if (ack_reg2 == 1'b0) begin // 握手周期完全结束,可以开始下一次数据传输 tr_state <= SND_DATA_REQ; end end default: begin tr_state <= TR_IDLE; end endcase end end // 将缓冲的数据连接到输出端口 assign data_out = data_buf; endmodule代码要点解析:
- 数据缓冲:使用
data_buf寄存器暂存从内存读出的数据。这是一个好习惯,它确保了在req有效期间,data_out总线上的数据是恒定不变的,即使TR_MEM_Data在变化(因为地址在SND_DATA_REQ状态就已更新)。 - 状态转移条件:所有状态转移都严格依赖于同步后的信号(
ack_reg2)和本地寄存器,避免了异步条件判断。 req的撤销时机:在CHK_ACK_ACTIVE状态,一看到ack_reg2为高就立刻撤销req。这符合协议:发送端一旦知道数据被接收,就立刻结束请求。
3.2 接收端模块实现
接收端模块负责检测请求、锁存数据,并发出应答。
module handshake_receiver #( parameter DATA_WIDTH = 8, parameter MEM_DEPTH = 256 )( // 系统信号 input wire r_clk, // 接收端时钟 input wire rst_n, // 低电平有效全局复位 // 与发送端的握手接口 input wire [DATA_WIDTH-1:0] data_in, // 来自发送端的数据总线 input wire req, // 发送端请求,高有效 output reg ack, // 接收端应答,高有效 // 接收数据存储接口 output reg [$clog2(MEM_DEPTH)-1:0] RE_MEM_Addr, // 接收内存地址 output reg RE_MEM_We, // 接收内存写使能 output reg [DATA_WIDTH-1:0] RE_MEM_Data // 写入接收内存的数据 ); // 状态定义 localparam RE_IDLE = 2'b00; localparam CHK_REQ_ACTIVE = 2'b01; localparam CHK_REQ_RELEASE = 2'b10; reg [1:0] re_state; // 接收端状态机当前状态 reg req_reg1, req_reg2; // 两级同步寄存器链 // --- 关键部分:对异步req信号进行同步 --- always @(posedge r_clk or negedge rst_n) begin if (!rst_n) begin req_reg1 <= 1'b0; req_reg2 <= 1'b0; end else begin req_reg1 <= req; // 第一级同步 req_reg2 <= req_reg1; // 第二级同步 end end // 后续状态机只使用同步后的 req_reg2 // --- 接收端主状态机 --- always @(posedge r_clk or negedge rst_n) begin if (!rst_n) begin ack <= 1'b0; RE_MEM_Addr <= 0; RE_MEM_We <= 1'b0; RE_MEM_Data <= {DATA_WIDTH{1'b0}}; re_state <= RE_IDLE; end else begin RE_MEM_We <= 1'b0; // 默认写使能关闭,只在锁存数据时拉高一个周期 case (re_state) RE_IDLE: begin // 初始化状态 RE_MEM_Addr <= 0; re_state <= CHK_REQ_ACTIVE; end CHK_REQ_ACTIVE: begin // 持续检测发送端的请求信号(同步后的版本) if (req_reg2 == 1'b1) begin // 检测到有效请求! // 1. 锁存当前数据总线上的值到接收内存 RE_MEM_Data <= data_in; RE_MEM_We <= 1'b1; // 产生一个时钟周期的写脉冲 // 2. 地址递增,为下一次存储做准备 if (RE_MEM_Addr < MEM_DEPTH - 1) RE_MEM_Addr <= RE_MEM_Addr + 1; else RE_MEM_Addr <= 0; // 3. 拉高ack信号,告知发送端“数据已取走” ack <= 1'b1; // 4. 进入下一状态,等待请求撤销 re_state <= CHK_REQ_RELEASE; end // 如果req_reg2为0,则保持在本状态等待。 end CHK_REQ_RELEASE: begin // 等待发送端撤销请求信号 if (req_reg2 == 1'b0) begin // 发送端已撤销请求,本次握手完成,接收端也撤销应答 ack <= 1'b0; // 返回等待状态,准备接收下一个数据 re_state <= CHK_REQ_ACTIVE; end end default: begin re_state <= RE_IDLE; end endcase end end endmodule代码要点解析:
- 数据锁存时机:在
CHK_REQ_ACTIVE状态,一旦检测到同步后的req_reg2为高,立即将data_in锁存到RE_MEM_Data,并产生写使能。此时数据总线是稳定的(因为发送端在拉高req后一直保持数据不变)。 ack的生成与撤销:ack在锁存数据的同时拉高,在检测到req撤销后拉低。ack信号本身是接收时钟域产生的,但它会跨时钟域传回发送端,因此发送端需要对其进行同步。- 状态机简化:接收端状态机比发送端更简单,因为它只需要响应请求,不需要管理数据源。
4. 仿真验证与极端场景测试
设计完成后的仿真验证至关重要。我们需要验证协议在正常情况下的工作流程,更要测试其在极端时钟比例下的健壮性,正如输入材料中提到的120M到1M,以及1M到120M。
4.1 测试平台搭建
我们将编写一个测试平台,实例化发送端和接收端,并为其提供不同频率的时钟。
`timescale 1ns / 1ps module tb_handshake(); parameter DATA_WIDTH = 8; parameter MEM_DEPTH = 8; // 时钟和复位 reg t_clk; // 发送时钟,假设120MHz reg r_clk; // 接收时钟,假设1MHz 或 120MHz reg rst_n; // 握手信号 wire [DATA_WIDTH-1:0] data_bus; wire req; wire ack; // 内存模型 reg [DATA_WIDTH-1:0] TR_MEM [0:MEM_DEPTH-1]; reg [DATA_WIDTH-1:0] RE_MEM [0:MEM_DEPTH-1]; wire [$clog2(MEM_DEPTH)-1:0] tr_addr, re_addr; wire tr_mem_we; // 发送内存读使能(模拟) wire re_mem_we; // 接收内存写使能 wire [DATA_WIDTH-1:0] tr_data_out, re_data_in; // 时钟生成 // 发送时钟 120MHz -> 周期约8.333ns initial t_clk = 0; always #4.167 t_clk = ~t_clk; // 半周期 // 接收时钟 1MHz -> 周期1000ns (用于测试快发慢收) // initial r_clk = 0; // always #500 r_clk = ~r_clk; // 半周期 // 接收时钟 120MHz -> 周期约8.333ns (用于测试同频或慢发快收) initial r_clk = 0; always #4.167 r_clk = ~r_clk; // 半周期 // 复位生成 initial begin rst_n = 0; #100 rst_n = 1; // 100ns后释放复位 end // 初始化发送内存 integer i; initial begin for (i=0; i<MEM_DEPTH; i=i+1) begin TR_MEM[i] = i + 100; // 存入一些测试数据,如100,101,102... end end // 实例化发送端 handshake_sender #( .DATA_WIDTH(DATA_WIDTH), .MEM_DEPTH(MEM_DEPTH) ) u_sender ( .t_clk(t_clk), .rst_n(rst_n), .data_out(data_bus), .req(req), .ack(ack), .TR_MEM_Addr(tr_addr), .TR_MEM_Data(tr_data_out) ); // 连接发送端内存数据线 assign tr_data_out = TR_MEM[tr_addr]; // 实例化接收端 handshake_receiver #( .DATA_WIDTH(DATA_WIDTH), .MEM_DEPTH(MEM_DEPTH) ) u_receiver ( .r_clk(r_clk), .rst_n(rst_n), .data_in(data_bus), .req(req), .ack(ack), .RE_MEM_Addr(re_addr), .RE_MEM_We(re_mem_we), .RE_MEM_Data(re_data_in) ); // 将接收到的数据写入接收内存模型 always @(posedge r_clk) begin if (re_mem_we) begin RE_MEM[re_addr] <= re_data_in; end end // 测试主流程 initial begin // 等待复位完成 wait(rst_n == 1); #1000; // 等待一段时间让数据传输一些 // 检查接收内存中的数据是否与发送内存一致 $display("\n--- 数据一致性检查 ---"); for (i=0; i<MEM_DEPTH; i=i+1) begin if (TR_MEM[i] === RE_MEM[i]) begin $display("地址 %0d: 发送值=%0d, 接收值=%0d [PASS]", i, TR_MEM[i], RE_MEM[i]); end else begin $display("地址 %0d: 发送值=%0d, 接收值=%0d [FAIL]", i, TR_MEM[i], RE_MEM[i]); end end #1000; $finish; end endmodule4.2 仿真波形分析与关键时序
使用仿真工具(如ModelSim、Vivado Simulator)运行上述测试平台,我们可以观察到详细的波形。这里分析几个关键时序点:
正常握手周期(同频或频率相近):
t_clk上升沿:发送端进入SND_DATA_REQ,拉高req,数据data_bus更新。r_clk上升沿:接收端检测到同步后的req_reg2变高,进入CHK_REQ_ACTIVE,锁存data_bus,拉高ack。t_clk上升沿:发送端检测到同步后的ack_reg2变高,进入CHK_ACK_ACTIVE,拉低req。r_clk上升沿:接收端检测到req_reg2变低,进入CHK_REQ_RELEASE,拉低ack。t_clk上升沿:发送端检测到ack_reg2变低,握手结束,准备下一次发送。
快时钟域到慢时钟域(120M -> 1M):
- 这是握手协议优势最明显的场景。发送端的
req脉冲(一个120MHz周期)在接收端看来,是一个持续很多个1MHz周期的稳定高电平。接收端有充足的时间在它的某个时钟上升沿采样到这个稳定的req_reg2,从而安全地锁存数据。数据绝对不会丢失,但传输效率很低,发送端需要等待很久才能收到ack。
- 这是握手协议优势最明显的场景。发送端的
慢时钟域到快时钟域(1M -> 120M):
- 发送端的
req脉冲(一个1MHz周期)在接收端看来,是一个持续约120个120MHz周期的宽脉冲。接收端会很快采样到req_reg2变高,锁存数据并回复ack。由于ack脉冲也较宽,发送端也能可靠采样。传输延迟主要由慢时钟决定。
- 发送端的
实操心得:在仿真中,务必关注
req和ack信号在跨时钟域同步后的延迟。你会看到req_reg2相对于原始的req有1-2个接收时钟周期的延迟,ack_reg2同理。这个延迟是同步器引入的,是正常的,也是握手协议能正确工作的原因。状态机必须使用这些同步后的信号进行判断。
4.3 上板实测与调试技巧
仿真通过后,就可以进行上板实测了。这里分享几个调试此类设计的技巧:
- 使用ILA(集成逻辑分析仪)抓取信号:这是最强大的调试手段。将
t_clk、r_clk、req、ack、data_bus、tr_state、re_state等关键信号添加到ILA核中。设置触发条件,例如在req上升沿触发,然后观察整个握手过程的波形,与仿真波形进行对比。 - 添加调试计数器:在代码中添加一些计数器,例如发送成功计数器、接收成功计数器、超时错误计数器等。通过读取这些计数器的值,可以判断模块是否在持续工作,以及是否有数据丢失。
- 极端时钟测试:在板上实际测试120M到1M的场景。可以使用锁相环生成这两个差异巨大的时钟。测试时,可以让发送端连续发送一个递增的数据序列,接收端将数据通过UART打印到电脑,检查是否连续、正确。
- 关注时序报告:综合和实现后,一定要查看时序报告,确保
t_clk和r_clk两个时钟域内部的时序都已收敛。握手协议本身处理了跨时钟域路径,但每个时钟域内部的路径仍需满足时序。
5. 握手协议的优缺点分析与适用场景
经过完整的实现和测试,我们可以对握手协议方式做一个客观的总结。
5.1 优势
- 高可靠性:通过双向确认机制,从根本上避免了亚稳态导致的数据错误,数据传递是100%可靠的。
- 对时钟频率比无要求:无论发送端时钟快还是慢,甚至频率关系动态变化,握手协议都能工作。这是其相对于异步FIFO(需要预估深度)的一个显著优点。
- 实现简单:核心逻辑就是两个状态机,代码清晰,易于理解和调试。
- 资源消耗少:仅需要几个寄存器和一些组合逻辑,比异步FIFO消耗的Slice、BRAM资源要少得多。
5.2 劣势
- 传输效率低:这是最大的缺点。完成一次数据传输需要
req和ack两个信号来回传递,中间涉及同步等待,延迟很大。尤其是在快时钟到慢时钟的场景,效率极低。平均带宽远低于时钟频率。 - 吞吐量不稳定:传输延迟取决于两个时钟的频率和相位关系,每次传输的时间可能不一样,不适合需要恒定高带宽的数据流。
- 控制信号需同步:
req和ack本身需要同步处理,增加了设计复杂度(虽然不大)。
5.3 适用场景建议
根据其特点,握手协议适用于以下场景:
- 低速控制信号传递:例如将一个使能信号、配置命令从处理器时钟域传递到外设时钟域。对延迟不敏感,但要求绝对可靠。
- 偶发的数据传递:例如系统初始化时加载一段配置数据,或者偶尔上报一些状态信息。数据量小,传递频率低。
- 时钟关系不确定的系统:两个模块的时钟来自不同的、可能动态调整的源,无法预先确定频率比。
- 资源极其受限的设计:没有多余的BRAM资源来实现异步FIFO。
反之,对于需要高速、连续、大数据量传输的场景(如图像数据流、高速AD采样数据流),应优先选择异步FIFO。
6. 常见问题、故障排查与优化技巧
在实际项目中,即使按照标准流程设计,也可能遇到问题。下面是我总结的一些常见坑点和解决思路。
6.1 问题:系统“卡死”,状态机不再跳转
- 现象:仿真或上电后,发送端或接收端状态机停在某个状态(如
CHK_ACK_ACTIVE或CHK_REQ_ACTIVE),不再运行。 - 排查:
- 检查同步链:这是最常见的原因。确认
ack在发送端是否经过了正确的两级同步(生成ack_reg2),req在接收端是否经过了正确的两级同步(生成req_reg2)。状态机判断条件必须使用同步后的信号。 - 检查复位:确保两个时钟域的复位信号
rst_n都有效且同步释放。如果两个域复位不同步,可能导致一端开始发请求时,另一端还在复位状态,无法响应。 - 仿真观察:查看
req和ack信号是否真的在变化?同步后的req_reg2和ack_reg2是否跟随变化?可能存在信号连接错误。
- 检查同步链:这是最常见的原因。确认
6.2 问题:数据错误,但控制信号似乎正常
- 现象:握手过程在波形上看都对了,但接收端锁存的数据值不对。
- 排查:
- 检查数据总线稳定性:在接收端锁存数据(
req_reg2变高)的时刻,data_bus上的值是否稳定且是期望值?确保发送端在拉高req后,在req保持高电平期间,data_bus绝不改变。 - 检查数据路径延迟:如果
data_bus是由发送端寄存器直接驱动,通常没问题。但如果data_bus经过了一些组合逻辑,可能存在毛刺或延迟过大,在接收时钟沿到来时还未稳定。最佳实践是:驱动data_bus的信号最好也用时序逻辑(寄存器)输出,并与req信号在同一个always块中赋值,以保证严格的同步关系。
- 检查数据总线稳定性:在接收端锁存数据(
6.3 问题:在极高频率下时序违例
- 现象:当时钟频率很高(如300MHz以上)时,布局布线后出现建立时间违例。
- 优化:
- 对同步器添加约束:在XDC/SDC文件中,对
ack_reg1和req_reg1寄存器设置set_false_path或set_clock_groups -asynchronous。告诉时序分析工具,这条路径是异步的,不需要分析。工具会专注于同步器后面的ack_reg2/req_reg2到下游逻辑的时序。 - 使用专用同步器原语:一些FPGA厂商提供了经过优化的同步器宏(如Xilinx的
xpm_cdc_single),它们通常被放置在高性能的专用同步寄存器上,可靠性更高。
- 对同步器添加约束:在XDC/SDC文件中,对
6.4 性能优化技巧
如果觉得标准握手协议延迟太大,可以考虑以下变种:
- 流水线化握手:在发送端,可以在等待上一个数据的
ack时,就将下一个数据放到总线上。但这需要接收端能连续处理,且数据总线必须保持稳定直到被读取,实现起来更复杂,本质上提高了带宽利用率但没减少单次延迟。 - “ req” 脉冲检测法:发送端不保持
req为高,而是产生一个与其时钟同步的、宽度为一个周期的脉冲。接收端使用边沿检测电路来捕捉这个脉冲。这可以减少req为高的时间,但需要更精细的同步和边沿检测逻辑,可靠性需要仔细设计。
对于大多数应用,标准的、完整的握手协议已经足够可靠和清晰。我的建议是,首先把标准版本做稳、做透,理解其每一个时序细节。在确实遇到性能瓶颈且资源允许时,再考虑更复杂的优化方案。清晰的逻辑和可靠的运行,在工程中往往比那一点点性能提升更重要。