正点原子 I.MX6ULL MINI 开发板学习笔记:I2C 总线协议与控制器

这篇笔记继续正点原子 I.MX6ULL MINI 开发板裸机学习,主题是I2C 总线协议与控制器。I2C 是传感器、触摸屏、EEPROM 等芯片最常用的总线之一。代码来自实际工程 /home/zhao/code/2026/touch_screen。
1. 完成什么实验?
实现 I.MX6ULL I2C1/I2C2 控制器的初始化,以及基于 I2C 协议的字节写和字节读函数,为后续 LM75 温度传感器、GT9147 触摸屏驱动做准备。
2. 什么是 I2C?
I2C(Inter-Integrated Circuit)是飞利浦提出的半双工串行总线,只需要两根线:SCL(时钟)和 SDA(数据)。它采用主从架构,主设备通过 7 位或 10 位地址选择从设备。

3. I2C 通信的基本时序
- 起始位 S:SCL 高电平时,SDA 从高变低。
- 停止位 P:SCL 高电平时,SDA 从低变高。
- 数据位:SCL 低电平时 SDA 改变,SCL 高电平时 SDA 保持稳定被采样。
- ACK:每发送 8 位后,接收方在第 9 个时钟把 SDA 拉低表示应答。
- 7 位地址:地址占高 7 位,最低位 R/W:0 表示写,1 表示读。
4. I2C 读寄存器为什么要两次起始?
读寄存器时,主机先发送从机地址(写方向)和寄存器地址,告诉从机要读哪个寄存器;然后发送重复起始 Sr,再发从机地址(读方向),从机才会把数据返回给主机。这就是“先写后读”的过程。
5. 真实可用的 I2C 代码
下面是工程中验证过的 i2c.c 核心代码,只保留 I2C1 初始化和通用读写。
#include "i2c.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "gpt.h"
void init_i2c1(void)
{
/* 引脚复用为 I2C1_SDA / I2C1_SCL */
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, 0x70B0);
IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL, 0x70B0);
I2C1->I2CR &= ~(1 << 7); /* 先关闭 I2C */
I2C1->IFDR = 0x15; /* 分频,约 100kHz */
I2C1->I2CR |= (1 << 7); /* 使能 I2C */
}
static void i2c_start(I2C_Type *base, unsigned char dev_addr, int dir)
{
base->I2SR &= ~((1 << 4) | (1 << 1)); /* 清除仲裁丢失和中断标志 */
while ((base->I2SR & (1 << 7)) == 0); /* 等待上一字节传输完成(ICF,bit7) */
base->I2CR |= (1 << 5) | (1 << 4); /* 主机发送模式 */
base->I2CR &= ~(1 << 3); /* 发送 ACK */
base->I2SR &= ~(1 << 1);
base->I2DR = (dev_addr << 1) | dir; /* 发送设备地址 + 方向 */
while ((base->I2SR & (1 << 1)) == 0); /* 等待传输完成 */
}
static void i2c_write(I2C_Type *base, unsigned char dev_addr,
unsigned short reg_addr, unsigned int reg_len,
const unsigned char *data, unsigned int len)
{
base->I2SR &= ~((1 << 4) | (1 << 1));
while ((base->I2SR & (1 << 7)) == 0);
base->I2CR |= (1 << 5) | (1 << 4); /* 主机 + 发送 */
base->I2CR &= ~(1 << 3);
base->I2SR &= ~(1 << 1);
base->I2DR = dev_addr << 1; /* 地址 + 写 */
while ((base->I2SR & (1 << 1)) == 0);
for (unsigned int i = 0; i < reg_len; i++)
{
base->I2SR &= ~(1 << 1);
base->I2DR = reg_addr >> (8 * (reg_len - i - 1));
while ((base->I2SR & (1 << 1)) == 0);
}
while (len--)
{
base->I2SR &= ~(1 << 1);
base->I2DR = *data++;
while ((base->I2SR & (1 << 1)) == 0);
}
base->I2CR &= ~(1 << 5); /* 发送停止位 */
int t = 0;
while ((base->I2SR & (1 << 5)) != 0 && t < 10)
{
delay_us(100);
++t;
}
}
static void i2c_read(I2C_Type *base, unsigned char dev_addr, unsigned short reg_addr, unsigned int reg_len, unsigned char *data, unsigned int len)
{
base->I2SR &= ~((1 << IAL) | (1 << IIF));
while((base->I2SR & (1 << ICF)) == 0);
base->I2CR |= (1 << MSTA) | (1 << MTX); /* 主机 + 发送 */
base->I2CR &= ~(1 << TXAK);
base->I2SR &= ~(1 << IIF);
base->I2DR = dev_addr << 1; /* 地址 + 写 */
while((base->I2SR & (1 << IIF)) == 0);
unsigned int i;
for(i = 0;i < reg_len;++i) /* 写寄存器地址 */
{
base->I2SR &= ~(1 << IIF);
base->I2DR = reg_addr >> (8 * (reg_len - i - 1));
while((base->I2SR & (1 << IIF)) == 0);
}
base->I2CR |= (1 << RSTA); /* 重复起始 Sr */
base->I2SR &= ~(1 << IIF);
base->I2DR = (dev_addr << 1) | 1; /* 地址 + 读 */
while((base->I2SR & (1 << IIF)) == 0);
base->I2CR &= ~(1 << MTX); /* 切换为接收模式 */
base->I2SR &= ~(1 << IIF);
if(1 == len)
base->I2CR |= (1 << TXAK); /* 只读 1 字节,先准备 NACK */
*data = base->I2DR; /* 假读一次,启动接收 */
while(len--)
{
while((base->I2SR & (1 << IIF)) == 0);
base->I2SR &= ~(1 << IIF);
if(1 == len)
base->I2CR |= (1 << TXAK); /* 最后一字节前发 NACK */
else if(0 == len)
{
base->I2CR &= ~((1 << MSTA) | (1 << TXAK)); /* 发送停止位 */
int t = 0;
while((base->I2SR & (1 << IBB)) != 0 && t < 10) /* 等总线释放 */
{
delay_us(100);
++t;
}
}
*data++ = base->I2DR;
}
}
6. 关键点说明
- 开漏输出 + 上拉:配置字
0x70B0使能了 47K 片上拉,但 ODE 位(bit11)为 0,并未使能开漏;规范 I2C 应使用0x78B0(ODE=1)。本实验速率低、走线短,靠片上拉也能正常工作。 - IFDR 分频:I2C 时钟源通常为 66MHz IPG,通过 IFDR 分频得到标准模式 100kHz 或快速模式 400kHz。
- I2SR 标志位:ICF(bit7)为字节传输完成标志,IBB(bit5)为总线忙标志。代码起始前等的是 ICF,发停止位后等 IBB 清零,两者别混淆。
- I2SR[IIF]:传输完成中断标志,发送/接收完一个字节需要等待该位置位。
- 读操作最后要发 NACK:告诉从机不再读更多字节,然后发送停止位。
7. 实验现象
初始化成功后,配合 GT9147 可以读到设备 ID,配合 LM75 可以读到温度数据(注意 LM75 没有 ID 寄存器);如果 SDA/SCL 接线反了或没上拉,通常会卡在等待 IBB 或 IIF 的循环里。
总结
| 问题 | 答案 |
|---|---|
| I2C 需要几根线? | SCL、SDA 两根,半双工 |
| 起始位和停止位怎么产生? | SCL 高时 SDA 下降沿为起始,上升沿为停止 |
| 地址字节的最低位含义? | 0=写,1=读(R/W 位不属于 7 位地址本身) |
| 读寄存器为什么两次起始? | 先写寄存器地址,再重复起始读数据 |
| I.MX6ULL I2C 分频寄存器? | IFDR |
I2C 协议掌握后,下一篇就可以驱动 LM75 温度传感器,读出真实温度值。