一、开篇
今天系统学习了 i.MX6ULL 的 I2C 裸机驱动,从最底层的物理层一路走到寄存器操作,最后到用逻辑分析仪抓包验证。期间踩了不少坑,也解决了不少问题。这篇博客把整个学习路径整理出来,方便回顾。
学习内容:
- I2C 物理层:开漏、上拉、START/STOP、ACK/NACK
- i.MX6ULL I2C 寄存器:I2CR、I2SR、I2DR、IFDR
- 代码实现:
i2c_init、wait_i2c_iif、i2c_write、i2c_read - 工程配置:Makefile、工具链、
libgcc、stdio库 - 调试:串口、逻辑分析仪、抓包
- 踩坑:超时、页写、地址、时钟
二、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 高电平时。
START:SCL 高时,SDA 从 1 变 0。
STOP:SCL 高时,SDA 从 0 变 1。
2.4 数据位传输
SCL 低时,SDA 允许改变(准备)。
SCL 高时,SDA 必须保持(采样)。
SDA 的所有变化都发生在 SCL 低电平期间。如果 SCL 高时 SDA 变化,会被误判为 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 ────────────┐ ┌───────┐ ┌───────────────┐
└─────┘ └───────────┘ └───────────────
1 1 0 0 1 1 0 0
三、i.MX6ULL I2C 寄存器
3.1 寄存器总览
| 寄存器 | 偏移 | 作用 |
|---|---|---|
| IADR | 0x00 | 从机地址(主机不用) |
| IFDR | 0x04 | 分频,决定 SCL 频率 |
| I2CR | 0x08 | 控制(命令) |
| I2SR | 0x0C | 状态(结果) |
| I2DR | 0x10 | 数据(写=发,读=收) |
寄存器结构体必须用 volatile 修饰,防止编译器优化掉循环里的读操作。
3.2 I2CR 关键位
| 位 | 名称 | 作用 |
|---|---|---|
| 7 | IEN | 使能 |
| 6 | IIEN | 中断使能 |
| 5 | MSTA | 主从 + START/STOP(边沿触发) |
| 4 | MTX | 发送/接收方向 |
| 3 | TXAK | 接收时回 ACK/NACK |
| 2 | RSTA | Repeat START |
MSTA 边沿触发:
- 0→1:发 START
- 1→0:发 STOP
- 已经是 1 再写 1 不会重复发
MTX:
- 1 = 发送(控制器驱动 SDA)
- 0 = 接收(控制器释放 SDA)
TXAK:
- 0 = 收到数据回 ACK
- 1 = 收到数据回 NACK
RSTA:读操作中间发 Repeat START。
3.3 I2SR 关键位
| 位 | 名称 | 作用 |
|---|---|---|
| 7 | ICF | 传输完成 |
| 6 | IAAS | 被寻址为从机 |
| 5 | IBB | 总线忙 |
| 4 | IAL | 仲裁丢失 |
| 1 | IIF | 中断标志(核心) |
| 0 | RXAK | 收到的应答 |
IIF 是核心:每发/收完一个字节,硬件自动置 1,软件写 0 清。
RXAK:0=ACK,1=NACK,必须在清 IIF 之后读。
IBB:1=总线忙,0=空闲,STOP 后等它清 0。
IAL:仲裁丢失,单主模式用不到,但要清残留。
3.4 I2DR
写 I2DR 就是发送,不需要额外命令。硬件自动把 8 位推到 SDA,SCL 自动翻转,第 9 位收 ACK,置 IIF。
3.5 IFDR
决定 SCL 频率。i.MX6ULL 的 I2C 时钟源一般是 66MHz 或 80MHz。
改 IFDR 前必须先关 IEN:
IEN = 0 → 写 IFDR → IEN = 1
你代码里 IFDR = 0x15,对应约 100kHz。
四、代码逐段解析
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)
{
int timeout = 100000;
while((base->I2SR & I2SR_IIF) == 0){
if (--timeout == 0) return -2;
}
base->I2SR &= ~I2SR_IIF;
if ((base->I2SR & I2SR_RXAK) != 0)
return -1;
return 0;
}
三步:等 IIF → 清 IIF → 查 RXAK。
超时:防止从机不响应时死循环。
4.3 i2c_write
int i2c_write(I2C_Type *base, unsigned char dev_addr, unsigned short reg_addr,
int reg_len, unsigned char *data, int data_len)
{
int stat = 0;
int i;
base->I2SR &= ~(I2SR_IAL | I2SR_IIF);
base->I2CR |= I2CR_MTX;
base->I2CR |= I2CR_MSTA;
base->I2DR = (dev_addr << 1) | 0;
stat = wait_i2c_iif(base);
if (stat != 0) goto stop;
for (i = reg_len - 1; i >= 0; i--) {
base->I2DR = reg_addr >> (i * 8);
stat = wait_i2c_iif(base);
if (stat != 0) goto stop;
}
for (i = 0; i < data_len; i++) {
base->I2DR = data[i];
stat = wait_i2c_iif(base);
if (stat != 0) goto stop;
}
stop:
base->I2CR &= ~I2CR_MSTA;
{
int timeout = 100000;
while((base->I2SR & I2SR_IBB) != 0){
if (--timeout == 0) break;
}
}
return stat;
}
核心规律:写 I2DR → 等 IIF → 查 RXAK → 写下一个字节。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2302_81263197/article/details/166598308



