3. 总线协议基础:握手协议、请求-响应模型、事务与突发传输

各位同学,今天我们聊聊总线协议里最基础、也最核心的几个概念。说实话,很多工程师做了好几年设计,对 Valid-Ready 握手还是模棱两可。我当年刚入行时也踩过坑——有一次仿真死活对不上,查了三天才发现是握手时序理解错了。好,咱们一个一个来。

3.1 Valid-Ready 握手协议

这是总线通信的“最小单元”。说白了,就是两个信号:Valid(发送方说“我有数据了”)和 Ready(接收方说“我能收”)。数据在 Valid 和 Ready 同时为高的时钟沿被采样。

我个人习惯把握手分成三种情况:

  • Valid 先到,Ready 后到:发送方准备好,但接收方忙。数据要等 Ready 拉高才能传。
  • Ready 先到,Valid 后到:接收方空着,但发送方还没准备好。数据等 Valid 拉高。
  • 同时有效:一拍搞定,最理想的情况。

核心要点:Valid 不能依赖 Ready,但 Ready 可以依赖 Valid。为什么?因为 Valid 一旦拉高,必须保持到握手成功,否则数据会丢。Ready 可以随时变,不影响数据完整性。

我在项目中遇到过一个问题:某次 AXI 总线死锁,查到最后发现是 Valid 信号在握手前被拉低了。嗯,这种 bug 极难复现,因为时序窗口很窄。所以我的建议是——Valid 一旦拉高,必须等到握手完成才能变,这是铁律。

// 典型的 Valid-Ready 握手代码
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        data_valid <= 1'b0;
    else if (ready & valid)
        data_valid <= 1'b0;  // 握手成功,拉低
    else
        data_valid <= data_valid;  // 保持
end

小技巧:调试时可以在波形里同时拉 Valid 和 Ready,看它们“与”的结果。如果 Valid 和 Ready 同时为高但数据没变化,那八成是时序问题。

3.2 请求-响应模型

有了握手,我们就能做更复杂的通信了。请求-响应模型就是:主设备发请求,从设备给响应。这听起来简单,但实际设计时有很多门道。

举个例子,CPU 要读内存:

  1. CPU 发读请求(地址 + 读命令)
  2. 内存控制器收到后,去 DRAM 里取数据
  3. 数据准备好后,内存控制器返回数据 + 响应状态

你想想看,这里有个关键问题:请求和响应之间可能有延迟。这个延迟叫“延迟”(Latency)。我见过有些新手设计时,假设响应必须在一个周期内回来——这在真实芯片里几乎不可能。

避坑指南:我曾经设计过一个 DMA 控制器,假设从设备总是立即响应。结果接上一个慢速外设后,整个总线都卡死了。后来我加了一个超时计数器,如果响应超过 N 个周期没回来,就报错并重试。这个机制救了我好几次。

请求-响应模型里,还有个重要概念叫“流水线化”。就是说,主设备可以连续发多个请求,不用等前一个响应回来。这能大幅提高总线利用率。但要注意——从设备必须能处理乱序响应,或者保证响应顺序与请求一致。

3.3 事务(Transaction)

事务,是总线通信的“原子操作”。一个事务包含:请求阶段 + 响应阶段。比如一次读事务,就是从发地址到收到数据的完整过程。

我个人习惯把事务分成几类:

事务类型 说明 典型场景
读事务 主设备读从设备的数据 CPU 取指令、DMA 读内存
写事务 主设备写数据到从设备 CPU 写寄存器、DMA 写内存
原子事务 读-改-写,不可打断 信号量操作、锁操作
广播事务 一个主设备写,多个从设备同时接收 中断分发、同步操作

这里要注意:事务是有边界的。一个事务开始后,必须完整结束,不能被另一个事务打断(除非支持事务取消,但那是高级话题)。我在设计 AXI 总线时,就遇到过事务 ID 冲突的问题——两个事务用了同一个 ID,结果响应乱序了。嗯,从那以后我特别强调事务 ID 的唯一性。

3.4 突发传输(Burst)

突发传输,说白了就是一次请求,多次传输。比如你要读 16 个连续地址的数据,不用发 16 次请求,只发一次请求,然后连续传 16 拍数据。

为什么需要突发?因为每次请求都有开销(地址传输、握手、仲裁等)。如果数据量大,突发能大幅提高效率。我做过一个测试:在 AXI 总线上,用突发传输比单次传输效率高了 4-5 倍。

突发传输有几个关键参数:

  • 突发长度:一次突发传多少拍数据。比如 AXI 支持 1-256 拍。
  • 突发大小:每拍传多少字节。常见的有 1、2、4、8 字节。
  • 突发类型:固定地址(FIXED)、递增地址(INCR)、回绕地址(WRAP)。

实战经验:我建议在设计总线时,优先支持 INCR 类型。FIXED 用得少(除非是 FIFO 操作),WRAP 主要用于 Cache Line 填充。INCR 最通用,几乎所有场景都能用。

突发传输还有个容易忽略的问题:地址对齐。比如你突发大小是 4 字节,那起始地址必须是 4 的倍数。如果不对齐,很多从设备会报错。我曾经在调试 PCIe 时,就因为地址没对齐,导致突发被拆成了多个单次传输,性能直接腰斩。

// 突发传输示例:AXI 读突发
// 地址 0x1000,突发长度 4,突发大小 4 字节
// 实际读取地址:0x1000, 0x1004, 0x1008, 0x100C

assign arvalid = 1'b1;
assign araddr  = 32'h1000;
assign arlen   = 4'd3;  // 突发长度 = 4(实际值 = arlen + 1)
assign arsize  = 3'd2;  // 4 字节(2^2 = 4)
assign arburst = 2'd1;  // INCR 类型

小提示:设计从设备时,一定要检查突发长度是否在支持范围内。有些从设备只支持 1-16 拍突发,如果主设备发了 32 拍,从设备要么截断,要么报错。我建议统一在总线层面做限制,避免每个从设备都做检查。

好了,这一章的内容就这些。Valid-Ready 握手是基础中的基础,请求-响应模型是通信的骨架,事务是原子操作,突发传输是性能利器。下一章我们会讲总线拓扑和仲裁,到时候这些概念都会用上。

记住:协议是死的,设计是活的。理解这些基础后,你就能灵活应对各种总线协议了。