一、开篇
今天系统学习了 i.MX6ULL 的 I2C 裸机驱动,从最底层的物理层一路走到寄存器操作,最后到用逻辑分析仪抓包验证。这篇博客把整个学习路径整理出来,方便回顾。
二、I2C 物理层
2.1 两根线
I2C 只有两根线:
- SDA:数据线,双向
- SCL:时钟线,主机产生
2.2 开漏 + 上拉
I2C 引脚必须是开漏输出,不能是推挽。
| 输出结构 | 能主动拉低 | 能主动拉高 | 能否并联 |
|---|---|---|---|
| 推挽 | ✅ | ✅ | ❌ 会短路 |
| 开漏 | ✅ | ❌(靠上拉) | ✅ 线与 |
开漏只能拉低,高电平靠外部上拉电阻。这样多个设备接在一起不会短路,谁拉低谁赢。
2.3 START / STOP
| 信号 | SCL | SDA |
|---|---|---|
| START | 1 | 1 → 0 |
| STOP | 1 | 0 → 1 |
START 和 STOP 都发生在SCL 高电平时,这是它们和普通数据位最大的区别。
2.4 数据位传输
SCL 低时,SDA 允许改变(准备)。
SCL 高时,SDA 必须保持(采样)。
SDA 的所有变化都发生在 SCL 低电平期间,否则会被误判为 START/STOP。
2.5 ACK / NACK
每发完 8 位,第 9 个时钟是应答位:
| 名称 | SDA | 含义 |
|---|---|---|
| ACK | 拉低 | 收到了,继续 |
| NACK | 不动(高) | 没收到 / 结束 |
发送方在第 9 位释放 SDA,由接收方控制。
2.6 MSB First
I2C 规定高位先发,先发 D7,最后发 D0。
2.7 完整波形示例
发10101100:
D7=1 D6=0 D5=1 D4=0 D3=1 D2=1 D1=0 D0=0 ACK ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ SCL ─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ └── SDA ────────────┐ ┌───────┐ ┌───────────────┐ └─────┘ └───────────┘ └───────────────三、i.MX6ULL I2C 寄存器
3.1 寄存器总览
| 寄存器 | 偏移 | 作用 |
|---|---|---|
| IADR | 0x00 | 从机地址(主机不用) |
| IFDR | 0x04 | 分频,决定 SCL 频率 |
| I2CR | 0x08 | 控制(命令) |
| I2SR | 0x0C | 状态(结果) |
| I2DR | 0x10 | 数据(写=发,读=收) |
3.2 I2CR 关键位
| 位 | 名称 | 作用 |
|---|---|---|
| 7 | IEN | 使能 |
| 5 | MSTA | 主从 + START/STOP(边沿触发) |
| 4 | MTX | 发送/接收方向 |
| 3 | TXAK | 接收时回 ACK/NACK |
| 2 | RSTA | Repeat START |
3.3 I2SR 关键位
| 位 | 名称 | 作用 |
|---|---|---|
| 5 | IBB | 总线忙 |
| 4 | IAL | 仲裁丢失 |
| 1 | IIF | 传输完成标志(核心) |
| 0 | RXAK | 收到的应答 |
IIF 是核心:每发/收完一个字节,硬件自动置 1,软件写 0 清。
3.4 I2DR
写 I2DR 就是发送,不需要额外命令。
四、代码逐段解析
4.1 i2c_init
void i2c_init(I2C_Type *base) { IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA, 1); IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL, 1); IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA, 0x10B0); IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL, 0x10B0); base->I2CR &= ~I2CR_IEN; base->IFDR = 0x15; base->I2CR |= I2CR_IEN; }做两件事:
- 配引脚(复用为 I2C1,开漏 + 上拉)
- 配控制器(关 IEN → 设 IFDR → 开 IEN)
顺序不能乱:IFDR 只在 IEN=0 时可写。
4.2 wait_i2c_iif
int wait_i2c_iif(I2C_Type *base) { while((base->I2SR & I2SR_IIF) == 0); base->I2SR &= ~I2SR_IIF; if ((base->I2SR & I2SR_RXAK) != 0) return -1; return 0; }三步:等 IIF → 清 IIF → 查 RXAK。这是所有字节传输的同步点。
4.3 i2c_write
void i2c_write(I2C_Type *base, unsigned char dev_addr, unsigned char reg_addr, unsigned char *data, int data_len) { base->I2SR &= ~(I2SR_IAL | I2SR_IIF); base->I2CR |= I2CR_MTX; base->I2CR |= I2CR_MSTA; base->I2DR = (dev_addr << 1) | 0; wait_i2c_iif(base); base->I2DR = reg_addr; wait_i2c_iif(base); for (i = 0; i < data_len; i++) { base->I2DR = data[i]; wait_i2c_iif(base); } stop: base->I2CR &= ~I2CR_MSTA; while((base->I2SR & I2SR_IBB) != 0); delay_ms(5); }核心规律:写 I2DR → 等 IIF → 查 RXAK,每个字节走一遍。
五、关键理解点
5.1 为什么(dev_addr << 1) | 0
I2C 第一个字节格式:
bit7~bit1 = 7 位地址 bit0 = R/W(0=写,1=读)左移 1 位把地址移到高 7 位,最低位填 R/W。
5.2 MSTA 是边沿触发
- MSTA 0→1:发 START
- MSTA 1→0:发 STOP
不是电平触发,是变化触发。
5.3 SCL 和 SDA 是独立的
SCL 拉低不代表 SDA 也拉低。两根线各自由不同的管子控制。
5.4 IIF 写 0 清
i.MX I2C 的 IIF 是写 0 清,和大多数寄存器相反:
base->I2SR &= ~I2SR_IIF; // 正确 base->I2SR |= I2SR_IIF; // 错误,写 1 无效六、实验:用逻辑分析仪抓包
6.1 工具
- 逻辑分析仪(不是频谱分析仪)
- 软件:PulseView(廉价克隆版)或 Logic(正版 Saleae)
6.2 接线
| 逻辑分析仪 | 板子 |
|---|---|
| CH0 | SDA(B33 / UART4_RX_DATA) |
| CH1 | SCL(B34 / UART4_TX_DATA) |
| GND | GND |
GND 必须接,否则波形会乱。
6.3 设置
- 采样率:≥ 1MHz(推荐 4MHz)
- 加 I2C 解码器
- SDA 选 CH0,SCL 选 CH1
- 触发条件:START 条件
6.4 期望结果
Start Address: 0x50 (Write) ACK Data: 0x00 ACK Data: 0xAB ACK Data: 0xCD ACK Stop6.5 检查项
| 检查项 | 应该是什么 |
|---|---|
| START | SCL 高时 SDA 下降 |
| 地址 | 0x50 左移 1 位 + 写位 |
| ACK | 第 9 位 SDA 被拉低 |
| 数据 | 和程序里发的一致 |
| STOP | SCL 高时 SDA 上升 |
七、踩过的坑
- 开漏和推挽搞反:以为开漏只会拉高,实际是只能拉低。
- START 的 SDA 方向:START 是 SDA 1→0,MSTA 0→1 是命令,两者不冲突。
- SCL 拉低不代表 SDA 拉低:两根线独立。
- IIF 写 0 清:和大多数寄存器相反。
- IFDR 只在 IEN=0 时可写:顺序不能乱。
- 没有超时机制:从机不响应会死循环。
八、总结
I2C = 串行 + 同步 + 开漏 + 多主多从。
i.MX6ULL 的 I2C 控制器把协议全部硬件化,软件只操作 4 个寄存器。
核心流程:写 I2DR → 等 IIF → 查 RXAK。
START/STOP 靠 MSTA 边沿触发。