4、MCP协议地址与仲裁:7位地址与10位地址模式、多主机仲裁机制、时钟拉伸

好,咱们今天聊点硬核的。MCP协议里,地址和仲裁这块,说白了就是「谁跟谁说话」和「抢着说话怎么办」的问题。我在做嵌入式调试工具的时候,被这两个问题坑过不止一次,尤其是多主机抢总线那会儿,波形乱得跟一锅粥似的。

4.1 7位地址模式:经典中的经典

MCP协议最常用的就是7位地址模式。你想想看,一条总线上挂几十个设备,每个设备得有个唯一ID吧?7位地址,理论上支持128个设备,但实际中保留了一些特殊地址,比如广播地址0x00,所以可用的大概是112个左右。

地址的传输格式是这样的:主机先发一个起始条件,然后紧跟着7位地址,再加一个读写位(0表示写,1表示读)。这8位数据,从机收到后,如果地址匹配,就会在第9个时钟周期拉低SDA线,发送一个ACK应答。

关键点:地址是高位在前(MSB first),别搞反了。我见过有人把地址0x50写成0x0A,结果从机死活不应答,查了半天才发现是字节序搞错了。

// 7位地址发送示例(伪代码)
void mcp_send_7bit_addr(uint8_t addr, bool is_read) {
    uint8_t byte = (addr << 1) | (is_read ? 1 : 0);
    // 发送8位数据(7位地址 + 1位读写)
    for (int i = 7; i >= 0; i--) {
        set_sda((byte >> i) & 0x01);
        clock_pulse();
    }
    // 等待从机ACK
    release_sda();
    clock_pulse();
    if (read_sda() != 0) {
        // 无应答,处理错误
    }
}

4.2 10位地址模式:扩展的玩法

7位地址不够用怎么办?MCP协议设计了10位地址模式。说实话,我在消费类电子产品里很少见到,但在一些工业传感器阵列里确实用过。

10位地址的发送分两步走:

  1. 第一帧:发送11110xx + 读写位,其中xx是10位地址的高2位。这其实是个保留地址段,告诉从机「我要用10位地址了」。
  2. 第二帧:发送剩下的8位地址(低8位)。

从机在每帧之后都要回复ACK。嗯,这里要注意:10位地址模式下,读写操作的处理方式跟7位地址不太一样。如果是写操作,两步走完直接发数据;如果是读操作,需要先发一个伪写操作来告诉从机你要读,然后重新发起始条件,再发读命令。

我的习惯:能用7位地址就别用10位。10位地址的时序更复杂,而且很多廉价从机芯片根本不支持。我在一个项目里被迫用10位地址,结果发现某款国产传感器手册写支持,实际波形死活不对,最后换了7位地址的型号才搞定。

特性 7位地址 10位地址
最大设备数 112(可用) 1024
传输帧数 1帧 2帧
兼容性 几乎所有设备 部分高端设备
时序复杂度

4.3 多主机仲裁机制:谁抢到算谁的

MCP协议允许总线上有多个主机。你想想看,两个主机同时想发数据,SDA线不就打架了吗?仲裁机制就是解决这个问题的。

仲裁的原理其实很简单:谁先输出低电平,谁就赢。因为MCP的SDA线是开漏输出,低电平是「强拉」,高电平是「释放」。如果主机A想发1(释放),主机B想发0(拉低),那SDA线实际就是低电平。主机A检测到SDA跟自己想发的不一致,就知道自己仲裁失败了,乖乖释放总线。

我曾经踩过的坑:仲裁失败的主机必须释放SDA和SCL线,并且不能立即重试。正确的做法是等待总线空闲(检测到停止条件)后再重新发起传输。我见过有人仲裁失败后立刻重试,结果总线一直乱,最后加了个随机延时才解决。

仲裁发生在两个阶段:

  • 地址阶段:多个主机同时发地址,谁的地址小(二进制值小)谁赢。因为地址小的主机更早拉低SDA。
  • 数据阶段:如果地址相同,那就继续仲裁数据位。数据小的主机赢。

这里有个有意思的点:仲裁过程中不会丢失数据。赢的主机继续正常传输,输的主机自动退出,从机完全感知不到仲裁发生过。这就是MCP协议优雅的地方。

4.4 时钟拉伸:从机的「喘口气」时间

时钟拉伸,英文叫Clock Stretching。说白了就是从机告诉主机:「你慢点,我还没准备好。」

正常MCP通信中,时钟由主机控制。但从机可以在某些时刻(通常是ACK之后)拉低SCL线,迫使主机等待。主机检测到SCL被拉低,就会暂停时钟,直到从机释放SCL。

什么时候会用到时钟拉伸?

  • 从机处理数据慢:比如EEPROM在写数据时,需要几毫秒才能完成。这时候从机拉低SCL,主机等着,直到写完成。
  • 从机内部缓冲区满:一些传感器采集数据需要时间,拉伸时钟让主机别催。

我建议:写主机驱动时,一定要给时钟拉伸设置超时。我遇到过一款温湿度传感器,拉伸时间长达50ms,如果主机没有超时处理,整个系统就卡死了。一般超时设为100ms比较安全。

// 时钟拉伸处理示例
bool wait_for_clock_release(uint32_t timeout_ms) {
    uint32_t start = get_tick_ms();
    while (read_scl() == 0) {  // SCL被从机拉低
        if (get_tick_ms() - start > timeout_ms) {
            return false;  // 超时,处理错误
        }
        // 等待,不做其他操作
    }
    return true;
}

4.5 知识体系总览

下面这张图是我自己整理的MCP地址与仲裁的核心逻辑,你看一眼就能把整个流程串起来:

MCP地址与仲裁核心流程 起始条件 (S) 7位/10位? 发送7位地址+读写位 发送11110xx+读写位 发送低8位地址 仲裁发生? 仲裁失败,释放总线 仲裁成功,继续传输 时钟拉伸处理

这张图把地址发送、仲裁判断、时钟拉伸串在了一起。你从起始条件开始,顺着箭头走一遍,基本就明白整个流程了。

总结一下:7位地址是主流,10位地址是扩展;仲裁靠的是「谁先拉低谁赢」的硬件机制;时钟拉伸是从机控制节奏的手段。这三个知识点,你在写驱动或者用逻辑分析仪抓波形时,几乎天天都会碰到。

好了,这一章就到这里。记住,MCP协议的精髓不在于它有多复杂,而在于它用最简单的硬件设计,解决了多设备通信的核心问题。你把这些机制吃透了,后面分析协议栈或者写Ghidra插件时,会顺手很多。

专注资料整理