12. I2C 协议
📝本节您将了解 I2C 总线通信协议的原理和时序,学习软件 I2C 的实现方法,并通过天巧星板载 I2C 设备进行通信验证。
🏆本章目标
1️⃣ 理解 I2C 总线的通信原理和时序规范
2️⃣ 掌握软件 I2C(GPIO 位模拟)的实现方法
3️⃣ 了解 I2C 设备地址和读写流程
4️⃣ 实现与板载 I2C 设备的数据通信
12.1 I2C 总线介绍
I2C(Inter-Integrated Circuit)是由 Philips(现 NXP)开发的双线串行通信总线,广泛用于 MCU 与低速外设之间的短距离通信。
总线特点
| 特性 | 说明 |
|---|---|
| 信号线 | 仅需 2 根:SDA(数据)+ SCL(时钟) |
| 通信方式 | 半双工,同一时刻只能单向传输 |
| 主从架构 | 主机发起通信,从机响应 |
| 多设备 | 一条总线可挂载多个从设备(通过地址区分) |
| 速率 | 标准模式 100kHz / 快速模式 400kHz |
| 电平 | 开漏输出 + 外部上拉电阻 |
硬件连接
VDD (3.3V)
│ │
[R] [R] ← 上拉电阻(通常 2.2kΩ ~ 10kΩ)
│ │
MCU ─── SDA ──┼─── 设备1 SDA
MCU ─── SCL ──┼─── 设备1 SCL
│
├─── 设备2 SDA/SCL
└─── 设备3 SDA/SCL ...1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
所有设备的 SDA 和 SCL 并联在同一总线上,通过唯一的 7-bit 地址区分。
开漏输出与上拉电阻
I2C 使用开漏输出结构:设备只能将总线拉低,不能主动驱动高电平。总线空闲时由上拉电阻拉至高电平。这种设计允许多个设备安全地共享同一根线——任何一个设备拉低总线,总线就为低电平;所有设备释放时,总线被上拉为高电平。
12.2 I2C 通信时序
起始条件(Start)
SCL 为高电平时,SDA 从高电平跳变到低电平。
停止条件(Stop)
SCL 为高电平时,SDA 从低电平跳变到高电平。
数据传输规则
- 数据在 SCL 低电平期间允许变化
- 数据在 SCL 高电平期间必须稳定(此时采样)
- 每个字节 8 bit,高位先发(MSB first)
- 每字节后接收方回复 1 bit ACK(应答)
应答机制(ACK / NACK)
- ACK:接收方在第 9 个时钟将 SDA 拉低,表示成功接收
- NACK:SDA 保持高电平,表示未接收或通信结束
写操作时序
Start → [7-bit地址 + W(0)] → ACK → [寄存器地址] → ACK → [数据] → ACK → Stop1
读操作时序
Start → [7-bit地址 + W(0)] → ACK → [寄存器地址] → ACK →
Re-Start → [7-bit地址 + R(1)] → ACK → [数据] → NACK → Stop1
2
2
12.3 天巧星 I2C 资源
| 引脚 | 功能 | 说明 |
|---|---|---|
| PA0 | SDA | 数据线,板载 2.2kΩ 上拉 |
| PA1 | SCL | 时钟线,板载 2.2kΩ 上拉 |
板载 I2C 设备:
| 设备 | 地址 | 用途 |
|---|---|---|
| OLED 显示屏 | 0x3C | 128×64 SSD1306 |
| IMU 姿态传感器 | 视型号 | 加速度 + 陀螺仪 |
软件 I2C vs 硬件 I2C
天巧星使用 GPIO 位模拟(软件 I2C):
- 优点:任意 GPIO 引脚可用,灵活性高,不受硬件 I2C 外设限制
- 缺点:CPU 参与每个时钟周期操作,速率略低
对 OLED 和 IMU 这类低速设备,软件 I2C 完全能胜任。
12.4 软件 I2C 实现
12.4.1 GPIO 配置
SysConfig 中配置 PA0 和 PA1:
| 引脚 | 方向 | IO 结构 | 内部电阻 | 初始值 |
|---|---|---|---|---|
| PA0 (SDA) | Output | Open Drain | Pull-Up | High |
| PA1 (SCL) | Output | Open Drain | Pull-Up | High |
12.4.2 底层引脚操作
c
#include "ti_msp_dl_config.h"
#define I2C_SDA_PORT GPIO_I2C_PORT
#define I2C_SDA_PIN GPIO_I2C_PIN_SDA_PIN
#define I2C_SCL_PORT GPIO_I2C_PORT
#define I2C_SCL_PIN GPIO_I2C_PIN_SCL_PIN
static inline void sda_high(void) { DL_GPIO_setPins(I2C_SDA_PORT, I2C_SDA_PIN); }
static inline void sda_low(void) { DL_GPIO_clearPins(I2C_SDA_PORT, I2C_SDA_PIN); }
static inline void scl_high(void) { DL_GPIO_setPins(I2C_SCL_PORT, I2C_SCL_PIN); }
static inline void scl_low(void) { DL_GPIO_clearPins(I2C_SCL_PORT, I2C_SCL_PIN); }
static inline uint8_t sda_read(void)
{
return (DL_GPIO_readPins(I2C_SDA_PORT, I2C_SDA_PIN) != 0) ? 1 : 0;
}
// I2C 延时(控制速率,约 200kHz)
static inline void i2c_delay(void)
{
delay_cycles(80); // 32MHz 下约 2.5μs
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
12.4.3 协议层函数
c
void i2c_start(void)
{
sda_high(); scl_high(); i2c_delay();
sda_low(); i2c_delay(); // SCL高时 SDA下降沿 = Start
scl_low(); i2c_delay();
}
void i2c_stop(void)
{
sda_low(); scl_high(); i2c_delay();
sda_high(); i2c_delay(); // SCL高时 SDA上升沿 = Stop
}
// 发送 1 字节,返回 ACK 状态(0=ACK, 1=NACK)
uint8_t i2c_write_byte(uint8_t data)
{
for (uint8_t i = 0; i < 8; i++) {
(data & 0x80) ? sda_high() : sda_low();
data <<= 1;
scl_high(); i2c_delay();
scl_low(); i2c_delay();
}
// 读取 ACK
sda_high();
scl_high(); i2c_delay();
uint8_t ack = sda_read();
scl_low(); i2c_delay();
return ack;
}
// 接收 1 字节,ack=0 发送ACK,ack=1 发送NACK
uint8_t i2c_read_byte(uint8_t ack)
{
uint8_t data = 0;
sda_high(); // 释放 SDA
for (uint8_t i = 0; i < 8; i++) {
scl_high(); i2c_delay();
data = (data << 1) | sda_read();
scl_low(); i2c_delay();
}
// 发送 ACK/NACK
ack ? sda_high() : sda_low();
scl_high(); i2c_delay();
scl_low(); i2c_delay();
sda_high();
return data;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
12.4.4 设备读写封装
c
// 向设备写入数据
uint8_t i2c_write_reg(uint8_t dev_addr, uint8_t reg, uint8_t *data, uint8_t len)
{
i2c_start();
if (i2c_write_byte((dev_addr << 1) | 0x00)) { i2c_stop(); return 1; }
if (i2c_write_byte(reg)) { i2c_stop(); return 1; }
for (uint8_t i = 0; i < len; i++) {
if (i2c_write_byte(data[i])) { i2c_stop(); return 1; }
}
i2c_stop();
return 0;
}
// 从设备读取数据
uint8_t i2c_read_reg(uint8_t dev_addr, uint8_t reg, uint8_t *data, uint8_t len)
{
i2c_start();
if (i2c_write_byte((dev_addr << 1) | 0x00)) { i2c_stop(); return 1; }
if (i2c_write_byte(reg)) { i2c_stop(); return 1; }
i2c_start(); // Re-Start
if (i2c_write_byte((dev_addr << 1) | 0x01)) { i2c_stop(); return 1; }
for (uint8_t i = 0; i < len; i++) {
data[i] = i2c_read_byte((i == len - 1) ? 1 : 0);
}
i2c_stop();
return 0;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
12.5 实验:I2C 总线扫描
扫描总线上所有地址,验证通信是否正常:
c
#include "ti_msp_dl_config.h"
#include <stdio.h>
void uart_send_string(const char *str)
{
while (*str) {
while (DL_UART_Main_isBusy(UART_0_INST));
DL_UART_Main_transmitData(UART_0_INST, *str++);
}
}
int main(void)
{
SYSCFG_DL_init();
char buf[64];
uart_send_string("I2C Bus Scan:\r\n");
uint8_t found = 0;
for (uint8_t addr = 0x08; addr < 0x78; addr++) {
i2c_start();
uint8_t ack = i2c_write_byte((addr << 1) | 0x00);
i2c_stop();
if (ack == 0) {
sprintf(buf, " Device found at 0x%02X\r\n", addr);
uart_send_string(buf);
found++;
}
}
sprintf(buf, "Scan complete. %u device(s) found.\r\n", found);
uart_send_string(buf);
while (1) { __WFI(); }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
正常情况下应输出 OLED(0x3C)和 IMU 的地址。
扫描无结果排查
- 确认 PA0/PA1 配置正确(Open Drain + Pull-Up)
- 用万用表测 SDA/SCL 空闲电压应为 3.3V
- 检查设备是否供电正常
- 增大
i2c_delay延时排除时序过快问题
12.6 总线恢复
如果通信中途被打断(如调试器复位),从机可能锁死 SDA。发送 9 个 SCL 脉冲可恢复:
c
void i2c_bus_recovery(void)
{
sda_high();
for (uint8_t i = 0; i < 9; i++) {
scl_high(); i2c_delay();
scl_low(); i2c_delay();
}
i2c_stop();
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
12.7 知识总结
| 内容 | 说明 |
|---|---|
| 信号线 | SDA + SCL,2 线制 |
| 设备寻址 | 7-bit 地址,一总线多设备 |
| 电平结构 | 开漏 + 上拉电阻 |
| 天巧星引脚 | PA0(SDA) / PA1(SCL),2.2kΩ 上拉 |
| 实现方式 | 软件 I2C(GPIO 位模拟) |
| 板载设备 | OLED (0x3C) + IMU |
下一章将学习 SPI 协议,驱动天巧星板载的 W25Q64 Flash 存储芯片。