I2C(IIC)的仲裁、时钟同步和时钟扩展

本文深入探讨了I2C通信中的关键概念,包括多主机模式下的仲裁机制、时钟同步原理以及从机如何通过时钟扩展动态调整通信速率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

I2C(IIC)的仲裁、时钟同步和时钟扩展

注意,CSDN以及博客园上有大量抄袭和以讹传讹的情况,注意鉴别。

本文参考了Philip的I2C specification以及wiki pedia,并且通过实践验证。

阅读本文要求你已经对i2c的协议有基本的了解。我们将会着重介绍多主机模式(multi-master)下的仲裁(arbitration),仲裁时不同主机之间的时钟同步(clock synchronization),以及从机(slave)动态调整通信速率的措施时钟扩展(clock stretching)

仲裁

发生仲裁的前提是,一条总线上挂载了多个主机,并且这些主机都支持多主机模式,即每一个主机都可以实时监测SDA以及SCL的情况,从而通过start和stop位来确定总线的情况(被占用还是空闲)。当多个主机检测到当前总线处于空闲状态时(这里以2个主机为例),可能会同时发出启动标志位(发出的时间间隔很短,因此无法检测到其他节点产生的电平变化,误认为总线是空闲的),这时候仲裁机制开始生效。

总线归属权的仲裁是通过SDA线完成的。由于I2C的总线设计是线与(wired-AND),因此低电平是显性电平。只要有一个节点将SDA拉低,那么整条总线都会拉至低电平。在两个主机都认为总线空闲并开始通信之后,当遇到第一个两者发送的不同的位时,会决出总线的归属者。

直接举例。设主机1将向address为0b10100111的从机发送数据,主机2将向addres为0x10101000的从机发送数据。两者都占有总线并同时向SDA写入数据。注意I2C发送的顺序是MSB->LSB(先发送高位)。两者的前四位数据相同,而当发送进行到第五个时钟周期时,master1的第五位为0,而master2的第五位为1,此时根据线与特性,SDA总线的电平会被master1拉低。master2会检测到自己要发送的电平和总线的实际电平不符,从而获知有其他主机正在发送,随之停止后续的发送,让出总线的归属权。

若两主机向相同的地址写入数据,那么仲裁过程将继续,直到在后续的数据位中决出总线的归属权(仍然是通过上述的方法)。

P.S. 仲裁不能发生在start和stop段,否则行为是未定义的。即倘若两个主机向相同的地址写入相同的数据,那么并不能决出总线的归属权。在这时,一个主机发出stop结束通信,而另一个主机则发出restart开始新的通信,这时候的行为就是未定义的。详见I2C-bus specification and user manual的第十二页:

在这里插入图片描述

I2C的仲裁是通过采样SDA完成的,因此不会破坏数据的有效性,通信不会停止,也不会有数据帧丢失。但是I2C不具有主机优先级的设定,因为总线的归属权是根据主机发送的数据确定的,因此无法提前为每个主机设定发送或接收的优先级。

时钟同步

时钟同步只会在仲裁时发生。SCL是由主机产生的时钟信号,用于和从机确定数据发送和采样的时间点。倘若处在仲裁期间,会有多个主机同时发送往SCL上发送时钟信号。两个主机配置的通信速率可能不同,因此时钟频率必然不同;即使配置了相同的通信速率,两者开始发送数据的时间也不同。此时时钟信号具体如何确定就要通过clock synchronization的机制。

在总线空闲的时候,SCL被上拉电阻拉高。开始通信后,主机的时钟信号接入SCL中。如下图所示,有两个主机(时钟信号分别为CLK1和CLK2)都认为主机空闲,因此开始将时钟信号输入SCL。同步分为以下五个阶段:

  1. CLK1率先变为低电平,由于线与特性CLK2也变成低电平。
  2. 但是CLK2的低电平时间更长(时钟周期更长,CLK1的周期更短),即使主机1内部的CLK1已经变成高电平,SCL的实际电平仍然和CLK2保持一致。此时,主机1将会检测到这一情况,并从此时开始计数(计算低电平持续的时间,即下图中的wait state,稍后用)
  3. CLK2迎来高电平之后,主机1内部的wait state结束,因为两者都为高,因此SCL总线进入高电平,两个主机内部都会开始对高电平持续的时间进行计数。
  4. 随后,CLK1会比CLK2先回到低电平(此时CLK1的周期仍然比CLK2更短)。
    在这里插入图片描述
虚线框表示CLK2原本的波形
  1. 现在,CLK1以及获得了需要延长的低电平时间,CLK2也获得了需要减短的高电平时间(都是通过刚才的计数获得的),两个主机会根据之前的计数重新调整自己的时钟周期,从而完成时钟同步。

例如,在时钟同步之前,主机1的速度为400kbit/s(fast mode),主机2的速度为100kbit/s(standard mode),同步之后,速度都会变为100kbit/s。如果两者的通信速率本就相同,但也会因为CLK的开始时间不一样而出现错位,这同样会通过时钟同步而矫正。

其他的教程中常出现一句话叫:“同步之后的时钟,低电平的时间取决于低电平最长的CLK,高电平时间取决于高电平时间最短的CLK”,描述的实际上就是I2C总线的线与特性。通过线与的特性以及I2C对SCL的持续检测(采样),我们就可以实现时钟同步了。

关于时钟同步的原始描述,可以参见I2C-bus specification and user manual的第11页。

时钟扩展

和其他常见的通信方式相比,I2C可以动态地调整总线的通信速率,这是通过时钟扩展(clock stretchign)完成的。

同样,时钟扩展需要I2C的硬件支持,其主机和从机都需要相关的硬件才可以实现这一功能。

虽然我们已经设定了I2C的通信速率(如标准模式,快速模式,高速模式等),但支持时钟扩展的I2C硬件可以根据情况动态改变通信速率。时钟扩展是由从机发起的。如从机正在处理其他任务无暇接收数据,或上一次收到的数据还没来得及写入或转移到其他地方时,就可以在收到一个byte的数据之后主动将SCL拉低(此时尚未发送ACK给主机),主机会检测到SCL的变化,进入等待状态(wait state),此时主机便会停止数据的发送,直到检测到SCL被释放(I2C的两条总线都是接了上拉电阻,默认为高电平,因此称回到高电平为“释放”)并收到释放之后的ACK信号。

### IIC协议仲裁机制的工作原理 IIC(Inter-Integrated Circuit)总线是一种广泛使用的串行通信协议,其设计允许在同一总线上连接多个主设备从设备。当存在多主设备的情况下,可能会发生两个或更多主设备尝试同时发起传输的情况。为了防止这种冲突并确保数据的正确传递,IIC协议引入了一种基于硬件的仲裁机制。 #### 1. 总线竞争与仲裁需求 在IIC总线中,如果多个主设备试图同时访问共享的SCL(时钟线)SDA(数据线),则可能发生总线竞争。为了避免这种情况下的混乱,IIC协议规定了严格的信号行为准则:任何设备都不能强制拉高已经处于低电平状态的SCL或SDA线[^1]。这一原则被称为“线与逻辑”,即只有当前线路为高电平时才能将其拉低,反之不允许强行改变已有的低电平状态。 #### 2. 硬件仲裁的具体过程 当多个主控制器几乎同步启动一次新的传送操作时,它们会各自按照计划依次驱动SCL及时序配合下输出自己的目标地址到SDA上。由于采用了开漏结构加上外部上拉电阻构成的实际电路特性,在任一时刻实际观测到的数据位将是所有参与者共同作用的结果——只要有一个节点希望该比特位置于零,则最终表现出来的就会是零;仅当没有任何一方主张置零时才会呈现默认的一级电压代表‘1’的状态[^3]。 因此,在每次发送期间,每一个潜在的竞争者都会持续监测自己所期望发出的内容是否确实反映到了真实的物理层面上去。一旦发现预期之外的变化(比如本来打算发出去的是'0', 结果看到还是保持住了先前就存在的 '1') ,那么这表明有另一个优先级更高的进程正在主导这条路径上的交流活动,此时应该立即停止进一步的动作以免干扰对方正常的通讯流程完成整个事务处理周期直至结束为止。 #### 3. 多主机环境中的协调运作 得益于上述描述的技术手段支持,即使是在复杂环境下拥有众多可能互相争夺使用权的不同实体之间也能够维持良好秩序而不至于造成不可挽回的信息丢失或者错误累积现象出现。通过这种方式实现了真正意义上的资源共享型架构模式构建起来之后便可以更加高效稳定地服务于各种应用场景之中去了。 ```python # 示例代码展示如何模拟简单的I2C仲裁逻辑 def i2c_arbitration(master_list, sda_line): active_masters = master_list[:] while len(active_masters) > 1: current_bit = [] for master in active_masters[:]: bit_to_send = master.send_next_bit() if not all(b == bit_to_send for b in current_bit): # 如果检测到不同的比特值,则放弃那些想要发送更高数值的master if any(b != bit_to_send and b == 1 for b in current_bit): active_masters.remove(master) current_bit.append(bit_to_send) actual_sda_value = min(current_bit) # 使用最小值作为真实SDA值 (遵循线与逻辑) sda_line.set(actual_sda_value) for master in active_masters[:]: expected = master.get_expected_bit() if expected != actual_sda_value: active_masters.remove(master) return active_masters[0] class MockMasterDevice: def __init__(self, data_stream): self.data_stream = iter(data_stream) def send_next_bit(self): try: return next(self.data_stream) except StopIteration: return None def get_expected_bit(self): return self.send_next_bit() class SDA_LineSimulator: def set(self, value): pass # 这里只是模拟设置SDA线的行为 masters = [ MockMasterDevice([0, 0, 1]), MockMasterDevice([0, 1]) ] sda_sim = SDA_LineSimulator() winner = i2c_arbitration(masters, sda_sim) print("Winner:", winner) ```
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值