🔥作者简介: 一个平凡而乐于分享的小比特,中南民族大学通信工程专业研究生,研究方向无线联邦学习 🎬擅长领域:驱动开发,嵌入式软件开发,BSP开发 ❄️作者主页:一个平凡而乐于分享的小比特的个人主页 ✨收录专栏:通信协议,本专栏为记录项目中用到的知识点,以及一些硬件常识总结 欢迎大家点赞 👍 收藏 ⭐ 加关注哦!💖💖

时钟拉伸是I²C协议中允许从设备临时控制SCL时钟线的一种机制。当从设备需要更多时间处理数据时,它可以主动将SCL线拉低,强制主设备等待,直到从设备释放SCL线。
想象一个老师和学生对话:
正常情况:
SCL ───┬───┬───┬───┬───┬───┬───┐
│ │ │ │ │ │ │
└───┘ └───┘ └───┘ └───
时钟周期1 时钟周期2
有时钟拉伸:
SCL ───┬─────────────┬───┬─────────────┐
│ │ │ │
└─────────────┘ └─────────────┘
↑ ↑
从设备拉低SCL 从设备释放SCL
强制等待时间 恢复正常时钟完整的数据字节传输(带时钟拉伸):
字节传输开始 字节传输结束
↓ ↓
SDA ──────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬──────
│ D7 │ D6 │ D5 │ D4 │ D3 │ D2 │ D1 │ D0 │ ACK │
SCL ──────┴─┬───┴─────┴─┬───┴─────┴─┬───┴─────┴─┬───┴─────┴──────
│ │ │ │
└───┐ └───┐ └───┐ └───┐
│ │ │ │
等待 等待 等待 等待
(从设备 (正常) (从设备 (正常)
处理数据) 处理数据)主设备(MCU) 从设备(EEPROM)
发送写命令 ───────────►
从设备需要时间写入数据
↓
SCL被从设备拉低
等待10ms...
↓
SCL被释放
继续通信 ◄───────────主设备请求读取传感器数据:
1. 主设备发送读命令
2. 从设备(传感器)需要时间采集数据
3. 从设备拉低SCL进行时钟拉伸
4. 传感器完成数据采集(可能需要100ms)
5. 从设备释放SCL
6. 主设备继续读取数据void i2c_master_send_byte(uint8_t data) {
for (int i = 7; i >= 0; i--) {
// 设置数据位
set_sda((data >> i) & 0x01);
// 拉高SCL
set_scl(HIGH);
// 关键:检查时钟拉伸
while(read_scl() == LOW) {
// SCL被从设备拉低,等待
delay_us(1);
}
// 确保SCL高电平时间足够
delay_us(I2C_HALF_CLOCK);
// 拉低SCL,准备下一位
set_scl(LOW);
delay_us(I2C_HALF_CLOCK);
}
}void i2c_slave_handle_read_request() {
// 收到读请求,需要准备数据
if (!data_ready) {
// 拉低SCL,开始时钟拉伸
set_scl_as_output();
set_scl(LOW);
// 准备数据(可能需要较长时间)
prepare_sensor_data(); // 可能耗时100ms
// 释放SCL
set_scl_as_input(); // 恢复为输入模式(由上拉电阻拉高)
data_ready = 1;
}
}总线仲裁是I²C协议中解决多个主设备同时访问总线冲突的机制。它允许总线在没有中央控制器的情况下,自动决定哪个主设备获得总线控制权。
想象一个会议讨论:
"线与"逻辑:只要有一个设备输出0(拉低),总线就是0。所有设备必须输出1时,总线才是1。
设备1输出:1 1 0 1 0 1 1 0 ← 想要发送的数据
设备2输出:1 1 0 1 0 0 1 1 ← 想要发送的数据
总线实际:1 1 0 1 0 0 1 0 ← 实际总线状态
│ │ │ │ │ ↑
前5位相同 第6位不同,设备1检测到总线为0但自己输出1
设备1输掉仲裁,停止驱动总线
设备2赢得仲裁,继续通信两个主设备同时发起通信的时序:
时间轴 →
主设备A:S 1 0 1 1 0 0 1 0 ... 继续通信
│ │ │ │ │ │ ↑
│ │ │ │ │ │ 检测到冲突,A输出1但总线为0
│ │ │ │ │ │ A立即切换到接收模式
主设备B:S 1 0 1 1 0 0 1 1 ... 赢得仲裁,继续
总线实际:S 1 0 1 1 0 0 1 1 ... B的数据
└──────────────┘
仲裁阶段:A和B输出相同,无冲突
第7位不同,A输掉仲裁
SCL ───┬───┬───┬───┬───┬───┬───┬───┬───┬───
│ │ │ │ │ │ │ │ │
└───┘ └───┘ └───┘ └───┘ └───
SDA(A)─1──1──0──1──1──0──0──1──0──×──高阻抗
SDA(B)─1──1──0──1──1──0──0──1──1──1──继续驱动
总线SDA─1──1──0──1──1──0──0──1──1──1──B控制 ┌─────────┐ ┌─────────┐
│ 主设备A │ │ 主设备B │
│ (MCU1) │ │ (MCU2) │
└────┬────┘ └────┬────┘
│ │
SDA ─┼──────┬───────┼─────►
SCL ─┼──────┼───────┼─────►
│ │ │
┌────┴──────┴───────┴────┐
│ I²C总线 │
└─────────────────────────┘
│ │
┌────┴────┐ ┌────┴────┐
│ 从设备1 │ │ 从设备2 │
└─────────┘ └─────────┘可能发生的仲裁情况:

两个主设备几乎同时发出START:
主设备A:检测到总线空闲,准备发送START
主设备B:也检测到总线空闲,准备发送START
结果:START条件本身不产生仲裁
真正的仲裁从第一个数据位开始主设备A:S 地址 A 数据 A Sr 地址 ...
└─────────┐ └─────┐
│ │
主设备B: S 地址 ... 赢得仲裁
↑
B在A的通信间隙发起START
A的重复START输给B的START特性 | 时钟拉伸 | 总线仲裁 |
|---|---|---|
发起者 | 从设备 | 主设备之间 |
目的 | 从设备需要更多处理时间 | 解决多主冲突 |
影响的线 | 只影响SCL线 | 影响SDA线 |
检测方式 | 主设备检测SCL被拉低 | 主设备比较输出与总线状态 |
结果 | 主设备等待 | 一个主设备赢,其他输 |
数据完整性 | 保持完整 | 保持完整,无数据损坏 |
典型场景 | 慢速EEPROM写入、传感器采集 | 多MCU系统、热插拔设备 |
是否可选 | 可选(设备可能不支持) | 多主系统必需 |
┌─────────────┐ ┌─────────────┐
│ 主MCU1 │ │ 主MCU2 │
│ (环境控制) │ │ (安全监控) │
└──────┬──────┘ └──────┬──────┘
│ │
SDA ─┼──────────────────┼─────►
SCL ─┼──────────────────┼─────►
│ │
┌─────────┼──────────────────┼─────────┐
│ 温度传感器 │ 湿度传感器 │ 烟雾传感器 │ 门磁传感器 │
│ 0x48 │ 0x5C │ 0x2A │ 0x3B │
└─────────┴──────────┴──────────┴─────────┘场景1:时钟拉伸示例
MCU1读取温度传感器(0x48):
1. MCU1发送:S + 0x90(0x48<<1 | 0) + A
2. 发送寄存器地址:0x00 + A
3. 重复START:Sr + 0x91(0x48<<1 | 1)
4. 传感器需要时间转换温度(100ms)
5. 传感器拉低SCL(时钟拉伸)
6. MCU1检测到SCL低,等待
7. 100ms后传感器释放SCL
8. MCU1读取温度数据:0x15 + A,0x80 + N + P场景2:总线仲裁示例
同时发生:
MCU1想读取温度传感器(地址0x48,二进制1001000)
MCU2想读取烟雾传感器(地址0x2A,二进制0101010)
时序:
时间 MCU1发送 MCU2发送 总线实际 结果
t0 S S S 同时开始
t1 1 0 0 MCU2输出0,总线0
t2 0 1 0 MCU1输出0,总线0
t3 0 1 0 MCU1输出0,总线0
t4 1 0 0 MCU2输出0,总线0
t5 0 1 0 MCU1输出0,总线0
t6 0 1 0 MCU1输出0,总线0
t7 0 0 0 地址最后一位,相同
t8 0(写) 0(写) 0 R/W位,相同
t9 ACK ACK ACK 等待从设备响应
结果:两个地址不同,但MCU1在第2位就输掉仲裁
因为MCU1想发1,但总线是0(MCU2发的0)
MCU2赢得总线,继续与烟雾传感器通信
MCU1等待总线空闲后重试问题 | 现象 | 解决方案 |
|---|---|---|
主设备不支持时钟拉伸 | 通信超时、数据错误 | 1. 选择支持时钟拉伸的主设备 2. 在软件中增加等待时间 3. 使用不支持时钟拉伸的从设备 |
时钟拉伸时间过长 | 系统响应慢、超时错误 | 1. 检查从设备处理时间 2. 增加主设备超时时间 3. 优化从设备固件 |
多从设备同时拉伸 | 总线死锁 | 1. 确保从设备不会无限拉伸 2. 设计超时释放机制 |
问题 | 现象 | 解决方案 |
|---|---|---|
频繁仲裁失败 | 某些主设备永远无法通信 | 1. 检查地址分配是否合理 2. 添加随机延迟重试 3. 使用优先级机制 |
仲裁期间数据损坏 | 通信错误 | 1. 确保硬件正确实现"线与" 2. 检查上拉电阻值 3. 验证从设备在仲裁期间不会响应 |
START条件仲裁 | 异常通信中断 | 1. 确保START检测电路正确 2. 实现完整的仲裁状态机 |
观测时钟拉伸:
设置触发条件:SCL下降沿后保持低电平>1ms
捕获到的信号:
[正常时钟][SCL被拉低.........][恢复正常]
↑ ↑
从设备开始拉伸 从设备结束拉伸
持续时间:15.6ms
观测总线仲裁:
设置触发条件:SDA在SCL高时发生变化
捕获到的信号:
主设备A发送:1 0 1 ... 0 1 (输掉仲裁)
主设备B发送:1 0 1 ... 0 0 (赢得仲裁)
总线实际: 1 0 1 ... 0 0
冲突点: ↑
第N位:A发1,B发0,总线0,A检测到冲突// 调试时钟拉伸
void debug_clock_stretching() {
uint32_t stretch_start = get_timer();
while(read_scl() == LOW) {
if (get_timer() - stretch_start > MAX_STRETCH_TIME) {
printf("时钟拉伸超时!持续时间:%lu ms\n",
get_timer() - stretch_start);
break;
}
}
}
// 调试总线仲裁
void i2c_master_send_with_arbitration(uint8_t data) {
for (int i = 7; i >= 0; i--) {
set_sda_bit((data >> i) & 0x01);
set_scl(HIGH);
// 检查仲裁
if (read_sda() != ((data >> i) & 0x01)) {
printf("仲裁失败在第%d位!\n", i);
printf("我发送了%d,但总线是%d\n",
(data >> i) & 0x01, read_sda());
// 切换到接收模式
set_sda_as_input();
return ARBITRATION_LOST;
}
set_scl(LOW);
}
return SUCCESS;
}时钟拉伸:
总线仲裁:
时钟拉伸:
总线仲裁:
I²C的这些高级特性使其能够灵活适应各种复杂场景,从简单的传感器读取到复杂的多主控制系统。理解并正确实现时钟拉伸和总线仲裁,是构建可靠I²C系统的关键。