3、总线协议基础:握手协议、地址译码、数据相位、控制信号

各位同学,今天我们来聊聊总线协议的基础。说实话,很多刚入行的工程师觉得协议就是一堆时序图,背下来就行。但我在项目中吃过不少亏,才明白协议的本质其实是「约定」——你和另一个模块之间,怎么说话、什么时候说话、说错了怎么办。

这一节,我会把总线协议拆成四个核心要素来讲:握手、地址译码、数据相位、控制信号。你想想看,任何复杂的总线,比如 AXI、Wishbone,甚至你自己写的简单总线,都离不开这四块。

3.1 握手协议:谁先开口?

握手协议,说白了就是解决「你说我听」的同步问题。我见过很多新手,上来就写一个「发送方直接扔数据」的逻辑,结果一上板子就丢数据。为什么?因为接收方还没准备好。

最常见的握手方式是 valid-ready 机制。发送方拉高 valid,表示「我有数据了」;接收方拉高 ready,表示「我可以收」。当两者同时为高,数据才算真正传输。

核心原则:数据只在 valid 和 ready 同时为高的时钟沿被采样。任何一方没准备好,交易就 stall 住。

我在一个高速 ADC 采集项目中遇到过一个问题:ADC 数据源源不断,但后端的 FIFO 偶尔会满。如果我不做握手,数据就会覆盖。后来我加了一个简单的 valid-ready 握手,问题就解决了。嗯,这里要注意:握手会增加一个时钟周期的延迟,但换来的是可靠性。

下面是一个简单的 Verilog 握手示例:

// 发送方
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        valid <= 1'b0;
    else if (ready)
        valid <= 1'b1;  // 数据准备好,等待接收
end

// 接收方
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        ready <= 1'b0;
    else if (valid && ready)
        ready <= 1'b0;  // 接收完成,暂时不接收新数据
end

// 数据采样
wire data_valid = valid && ready;
always @(posedge clk) begin
    if (data_valid)
        data_out <= data_in;
end

我的习惯:在顶层模块中,我会把 valid 和 ready 信号单独拉出来,用仿真波形先看一眼。如果 valid 和 ready 同时为高的周期数不对,那八成是握手逻辑写错了。

3.2 地址译码:谁在说话?

地址译码,就是决定「这个地址属于哪个从设备」。你想想看,总线上挂了好几个外设,比如 SRAM、UART、GPIO,CPU 发一个地址过来,你怎么知道该让谁响应?

地址译码的核心是 地址区间划分。每个从设备分配一段连续的地址空间,译码器根据地址的高位来判断命中哪个设备。

我曾经在一个 SoC 项目中,因为地址译码的边界条件没处理好,导致两个外设同时响应同一个地址。结果总线冲突,数据全乱了。从那以后,我每次写地址译码都会画一张地址映射表:

从设备 基地址 地址范围 大小
SRAM 0x0000_0000 0x0000_0000 - 0x0000_FFFF 64KB
UART 0x1000_0000 0x1000_0000 - 0x1000_00FF 256B
GPIO 0x2000_0000 0x2000_0000 - 0x2000_000F 16B

译码逻辑其实很简单,就是比较地址的高位:

assign sram_sel = (addr[31:16] == 16'h0000) ? 1'b1 : 1'b0;
assign uart_sel = (addr[31:16] == 16'h1000) ? 1'b1 : 1'b0;
assign gpio_sel = (addr[31:16] == 16'h2000) ? 1'b1 : 1'b0;

注意:地址译码一定要考虑「未命中」的情况。如果地址不属于任何设备,应该返回一个错误响应,或者默认拉高一个「无设备」信号。否则,总线会悬空,导致不确定行为。

3.3 数据相位:数据怎么走?

数据相位,指的是数据在总线上传输的时序。说白了,就是「数据什么时候来,什么时候走」。不同的总线协议,数据相位差别很大。

最简单的数据相位是 同步单周期传输:地址在第一个时钟沿发出,数据在第二个时钟沿返回。这种模式延迟低,但带宽也低。

我常用的是一种 流水线数据相位:地址和数据可以重叠。比如,在第一个周期发地址 A,第二个周期发地址 B 的同时,返回地址 A 的数据。这样带宽翻倍,但控制逻辑会复杂一些。

下面是一个流水线数据相位的时序示意:

时钟周期:  T1    T2    T3    T4
地址线:    A0    A1    A2    A3
数据线:    D0    D1    D2    D3

你看,地址 A0 在 T1 发出,数据 D0 在 T2 返回。同时 T2 又发出了地址 A1。这样每个周期都在传输数据,效率很高。

关键点:数据相位必须和握手信号配合。如果接收方没准备好,流水线就要 stall。我习惯在数据相位中加一个「数据有效」信号,这样即使流水线被打断,也不会丢数据。

3.4 控制信号:谁说了算?

控制信号,就是总线上的「交通警察」。它告诉各个模块:现在是读还是写?数据宽度是多少?传输是否结束?

常见的控制信号包括:

  • 读/写信号:决定数据传输方向。高电平读,低电平写。
  • 字节使能:在 32 位总线上,只传输 8 位或 16 位数据时,用字节使能指明哪些字节有效。
  • 传输结束信号:比如 AXI 的 last 信号,告诉从设备这是最后一个数据。
  • 错误信号:当从设备无法响应时,拉高错误信号。

我记得有一次调试一个 DMA 控制器,发现数据总是少传几个字节。查了半天,原来是字节使能信号没处理好。我默认用了 32 位传输,但外设只支持 8 位。从那以后,我写控制逻辑时一定会先确认外设的数据宽度。

下面是一个简单的控制信号示例:

// 读操作
assign read_en = (cmd == READ) && valid;
// 写操作
assign write_en = (cmd == WRITE) && valid;
// 字节使能:只传输低 8 位
assign byte_en = 4'b0001;

我的建议:控制信号不要太多,够用就行。我见过有人把控制信号设计成 20 多个,结果自己都记不住。一般来说,读/写、字节使能、错误信号,这三样就够了。复杂的总线可以再加一个 burst 类型信号。

3.5 知识体系总览

为了让你更直观地理解这四者的关系,我画了一张图。你可以看到,握手协议是基础,地址译码决定目标,数据相位决定时序,控制信号决定行为。四者缺一不可。

总线协议四大核心要素 总线协议 握手协议 地址译码 数据相位 控制信号 valid/ready 机制 解决同步问题 地址区间划分 确定目标设备 流水线/单周期 决定传输时序 读/写/字节使能 控制传输行为 四者协同工作,构成完整的总线协议

好了,这一节的内容就到这里。握手协议、地址译码、数据相位、控制信号,这四个概念你如果能吃透,后面学 AXI、Wishbone 这些具体协议就会轻松很多。记住,协议不是死记硬背的,而是理解「为什么这么设计」。下次你写自己的总线时,也可以从这四个维度去思考。


公众号:蓝海资料掘金营,微信deep3321