I.MX6U I2C 驱动学习笔记
一、GPIO 输出模式基础回顾
推挽输出电路原理
GPIO_VCC
|
[上MOS]
|
GPIO ─────┤────→ 负载(开/关)
|
[下MOS]
|
GND
开关逻辑:
- GPIO 输出高电平 → 上方 MOS 管导通 → 电流从 GPIO_VCC → 上MOS → 负载 → 下MOS → GND → 形成回路 → 设备工作(开)
- GPIO 输出低电平 → MOS 管截止 → 回路断开 → 设备停止(关)
为什么用两个 MOS(推挽)?
- 提高驱动能力,可主动驱动高低电平
- 实现双向控制(正反向电流)
- 增强稳定性
三种输出模式对比
| 模式 | 描述 | 电路表现 | 常见场景 |
|---|---|---|---|
| 高阻态 | 输出既不驱动高也不驱动低,相当于断开连接 | 引脚呈现高阻抗,对外相当于开路 | 三态门、总线挂载、I2C数据线 |
| 悬空 | 引脚没有连接任何电平,电平不确定,容易受干扰 | 电平随机浮动,可能感应到噪声 | 未连接的输入引脚(应避免!) |
| 推挽 | 用一对MOS管(上拉/下拉)主动驱动高低电平 | 可输出强0或强1,驱动能力强 | GPIO输出、驱动LED、数字信号 |
二、I2C 总线基础概念
什么是"线与"?
"线与" = 任何一方输出低电平,总线就是低电平;只有全部输出高电平,总线才是高电平。
I2C 的 SDA 和 SCL 线都采用"线与",因此:
- 主从设备都可以拉低总线
- 实现多主仲裁:谁先拉低谁赢
为什么 I2C 必须用开漏输出(OD)?
I2C 的 SDA/SCL 引脚必须配置为开漏输出(OD),而不是推挽输出。
原因:
- 允许多个设备共享一根线
- 靠外部上拉电阻实现"线与"
- 避免短路(若两设备同时一个输出高一个输出低,推挽会短路)
- 支持多主多从通信
IIC 推挽 vs 开漏示意
推挽输出: H(高) L(低)
开漏输出: X(高阻) L(低) ← 高电平靠上拉电阻提供
- 推挽:可以主动输出高或低
- 开漏 :只能主动拉低,高电平由外部上拉电阻(接VCC)提供
- 总线空闲时:所有设备释放总线 → 上拉电阻将总线拉高
- 任意设备要发低:拉低总线即可
I2C 总线拓扑
VCC
|
[上拉电阻]
|
├── SCL ── master ── slave1 ── slave2 ── slave3
|
[上拉电阻]
|
└── SDA ── master ── slave1 ── slave2 ── slave3
- 主从应答(轮询):主设备按顺序逐个访问从设备
- 因为 I2C 是同步半双工,主设备必须"轮着来"
三、I2C 通信规则
基本规则(重要!)
- 通信只能由主机发起
- 时钟线 SCL 永远由主机控制
- 数据线传输数据时由发送方控制,应答信号由接收方控制
- 主机写:主机控制数据位,从机控制应答
- 主机读:从机控制数据位,主机控制应答
- 主机第一次发送的数据永远是 7位从机地址 + 1位读写标志
I2C 时序图说明
SCL: ‾‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_‾‾
SDA: ‾‾| 数 据 位 传 输 |ack/nck‾
↑起始 ↑停止
空闲: 总线保持高电平
起始信号:SCL为高时,SDA由低到高 [起始条件]
← 注意:SCL高时SDA变化 = 特殊信号(起始/停止)
数据传输:SCL为低时,SDA可以跳变(写入数据)
SCL为高时,SDA保持不变(接收方采样)
每次传输以8bit为基本单位,先发高位
数据发完:由接收方发出应答信号(1bit的低电平 = ACK)
如果没有应答,通信强制停止
停止信号:SCL为高时,SDA由低到高
ACK/NACK 握手机制
- 主设备发完一个字节(地址或数据)后,从设备 在第9个时钟周期拉低 SDA 表示"收到"(ACK)
- 如果从设备没准备好或不想收,就释放 SDA(高电平),表示"没收到"(NACK)
- 这是 I2C 的"握手机制",确保每一步都有反馈
四、I2C 寄存器详解(I.MX6U)
I.MX6U 有 4 个 I2C 控制器(I2C1~I2C4),对应寄存器为 I2Cx_xxx。
4.1 I2Cx_IADR --- 地址寄存器
| 位 | 名称 | 说明 |
|---|---|---|
| bit7:1 | ADR | 从设备地址(有效位) |
| bit0 | 保留 | 始终为0 |
访问某个 I2C 从设备时,将其设备地址写入 ADR 字段。
4.2 I2Cx_IFDR --- 频率分频寄存器(波特率)
| 位 | 名称 | 说明 |
|---|---|---|
| bit5:0 | IC | I2C 时钟分频值(查手册第1464页的分频表) |
时钟来源:
PLL2 = 528 MHz
PLL2_PFD2 = 528 × 18/24 = 396 MHz
IPG_CLK_ROOT = PLL2_PFD2 / ahb_podf / ipg_podf = 396/4/2 = 49.5 MHz
PER_CLK_ROOT = IPG_CLK_ROOT / perclk_podf = 49.5/1 = 49.5 MHz(默认)
← 也可以选择 IPG_CLK_ROOT = 66MHz(另一种配置)
波特率计算示例:
- 时钟源 = 66MHz,目标波特率 = 100KHz
- IC 设置为
0x15→ 640分频 66,000,000 / 640 = 103.125 KHz ≈ 100KHz✓
⚠️ IC 的值不能随意设置,必须查手册给出的固定分频表选择合适的值。
4.3 I2Cx_I2CR --- 控制寄存器
| 位 | 名称 | 说明 |
|---|---|---|
| bit7 | IEN | I2C 使能位:1=使能,0=关闭 |
| bit6 | IIEN | I2C 中断使能位:1=使能中断,0=关闭 |
| bit5 | MSTA | 主从模式选择:1=主模式,0=从模式 |
| bit4 | MTX | 传输方向:0=接收,1=发送 |
| bit3 | TXAK | 传输应答使能:0=发送ACK,1=发送NACK |
| bit2 | RSTA | 重复开始信号:写1产生Repeated Start |
4.4 I2Cx_I2SR --- 状态寄存器
| 位 | 名称 | 说明 |
|---|---|---|
| bit7 | ICF | 数据传输状态:0=正在传输,1=传输完成 |
| bit6 | IAAS | 1=I2Cx_IADR中的地址是从设备地址 |
| bit5 | IBB | I2C 总线忙标志:0=总线空闲,1=总线忙 |
| bit4 | IAL | 仲裁丢失位:1=发生仲裁丢失(多主竞争失败) |
| bit2 | SRW | 从机读写状态:0=主机要写,1=主机要读 |
| bit1 | IIF | I2C 中断挂起标志:1=有中断挂起(需软件清零) |
| bit0 | RXAK | 应答信号标志:0=收到ACK,1=收到NACK |
仲裁丢失说明:多个设备同时抢总线时,硬件自动让仲裁失败的设备切换到Slave Receive模式。因此发完起始位后应检查 IAL 是否为1。
4.5 I2Cx_I2DR --- 数据寄存器
| 位 | 名称 | 说明 |
|---|---|---|
| bit7:0 | DATA | 发送/接收数据(只有低8位有效) |
使用规则:
- 发送:将要发送的数据写入此寄存器,I2C控制器开始传输数据,传输完毕(收到ACK或NACK)后会使 IIF(I2SR[bit1]) 置位
- 接收:直接读取此寄存器即可得到接收到的数据
- ⚠️ 注意 :对于接收数据来说,当从该寄存器读走一个字节后,I2C控制器就开始传输数据,直到IIF置位。因此在读取过程中,第一个读到的字节实际上是无效的!
五、I2C 初始化代码
第一步:初始化 I2C 引脚(init_i2c_io)
初始化包括两方面:配置引脚复用功能和电器特性;初始化I2C控制器。
关键点:需要将 SION 置位!
关于SION,官方手册说明不清楚,但至少对于SDA来说,需要将其作为输出有作为输入。因此必须使能SION。
c
void init_i2c_io(I2C_Type *base)
{
if(base == I2C1)
{
/* 配置I2C1的SDA引脚 */
IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA, 1); // SION=1
/* 配置I2C1的SCL引脚 */
IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL, 1); // SION=1
/* 配置SDA引脚电气属性:开漏输出,速度100KHz,驱动能力R0/6,上拉/下拉使能,上拉 */
IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA, 0x70B0);
/* 配置SCL引脚电气属性:开漏输出,速度100KHz,驱动能力R0/6,上拉/下拉使能,上拉 */
IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL, 0x70B0);
}
else if(base == I2C2)
{
IOMUXC_SetPinMux(IOMUXC_UART5_RX_DATA_I2C2_SDA, 1);
IOMUXC_SetPinMux(IOMUXC_UART5_TX_DATA_I2C2_SCL, 1);
IOMUXC_SetPinConfig(IOMUXC_UART5_RX_DATA_I2C2_SDA, 0x70B0);
IOMUXC_SetPinConfig(IOMUXC_UART5_TX_DATA_I2C2_SCL, 0x70B0);
}
}
第二步:初始化 I2C 控制器(init_i2c)
c
/**
* @brief 初始化I2C控制器
* @param base - I2C基地址指针(I2C1, I2C2等)
*/
void init_i2c(I2C_Type *base)
{
/* 初始化I2C引脚 */
init_i2c_io(base);
/* 关闭I2C模块 */
base->I2CR &= ~(1 << 7);
/* 设置I2C时钟分频为0x15,生成100KHz的SCL时钟 */
base->IFDR = 0x15;
/* 使能I2C模块 */
base->I2CR |= (1 << 7);
}
六、I2C 读写函数
主发送流程(I2C Master Transmit)
Reset
↓
Program IFDR(设置波特率)
↓
Enable I2C(IEN=1, IIEN=1)
↓
Program IADR(设置地址)
↓
IBB=0? → N → 等待
↓ Y
Program MSTA and MTX(主机模式+发送)
↓
IBB=1? → N → 等待(若IAL=1 → 仲裁失败,重试)
↓ Y
Write Slave Address to I2DR
↓
Data Transmission(循环发数据,等IIF)
↓
清IIF → 检查RXAK → 写下一字节 → 直到完成
↓
Generate STOP Signal(或 Repeated START)
6.1 向 24C02 写入数据(i2c_write)
寄存器地址可能不止一个字节,所以需要分别考虑。
c
void i2c_write(I2C_Type *base,
unsigned char dev_addr, // 从设备地址
unsigned char reg_addr, // 寄存器地址
unsigned char reg_addr_len, // 寄存器地址字节数
const unsigned char *data, // 要写入的数据
unsigned int len) // 数据长度
{
/* 清除中断标志位和仲裁丢失标志位 */
base->I2SR &= ~((1 << 4) | (1 << 1));
/* 等待总线空闲 */
while((base->I2SR & (1 << 7)) != 0);
/* 设置为主机发送模式 */
base->I2CR |= (1 << 4) | (1 << 5); /* 主机模式和发送模式 */
base->I2CR &= ~(1 << 3); /* 发送ACK */
/* 发送设备地址(写模式) */
base->I2SR &= ~(1 << 1); /* 清除中断标志位 */
base->I2DR = (dev_addr << 1); /* 写入设备地址和写标志(0) */
while((base->I2SR & (1 << 1)) == 0); /* 等待传输完成 */
/* 发送寄存器地址 */
if(1 == reg_addr_len)
{
base->I2SR &= ~(1 << 1); /* 清除中断标志位 */
base->I2DR = reg_addr; /* 写入寄存器地址 */
while((base->I2SR & (1 << 1)) == 0); /* 等待传输完成 */
}
else
{
base->I2SR &= ~(1 << 1);
base->I2DR = reg_addr >> 8; /* 写入寄存器地址高字节 */
while((base->I2SR & (1 << 1)) == 0);
base->I2SR &= ~(1 << 1);
base->I2DR = reg_addr & 0xFF; /* 写入寄存器地址低字节 */
while((base->I2SR & (1 << 1)) == 0);
}
/* 循环发送数据 */
while(len--)
{
base->I2SR &= ~(1 << 1); /* 清除中断标志位 */
base->I2DR = *data++; /* 写入数据 */
while((base->I2SR & (1 << 1)) == 0); /* 等待传输完成 */
}
/* 发送停止信号 */
int t = 0;
base->I2CR &= ~(1 << 5); /* 清除主机模式,产生停止信号 */
/* 等待STOP信号完成 */
while((base->I2SR | (1 << 5)) && t < 10)
{
++t;
delay_us(100);
}
}
6.2 从 24C02 读取数据(i2c_read)
读取时序相对复杂:发送完写入地址之后需要重新发送 Repeated Start,修改数据流向为读。
c
void i2c_read(I2C_Type *base,
unsigned char dev_addr,
unsigned short reg_addr,
unsigned char reg_addr_len,
unsigned char *data,
unsigned int len)
{
/* 清除中断标志位和仲裁丢失标志位 */
base->I2SR &= ~((1 << 4) | (1 << 1));
/* 等待总线空闲 */
while((base->I2SR & (1 << 7)) != 0);
/* 设置为主机发送模式(先发地址) */
base->I2CR |= (1 << 4) | (1 << 5); /* 主机模式和发送模式 */
base->I2CR &= ~(1 << 3); /* 发送ACK */
/* 发送设备地址(写模式,用于先指定寄存器地址) */
base->I2SR &= ~(1 << 1);
base->I2DR = dev_addr << 1; /* 写入设备地址和写标志(0) */
while((base->I2SR & (1 << 1)) == 0);
/* 发送寄存器地址 */
if(reg_addr_len == 1)
{
base->I2SR &= ~(1 << 1);
base->I2DR = reg_addr;
while((base->I2SR & (1 << 1)) == 0);
}
else
{
base->I2SR &= ~(1 << 1);
base->I2DR = reg_addr >> 8; /* 写入寄存器地址高字节 */
while((base->I2SR & (1 << 1)) == 0);
base->I2SR &= ~(1 << 1);
base->I2DR = reg_addr & 0xFF; /* 写入寄存器地址低字节 */
while((base->I2SR & (1 << 1)) == 0);
}
/* ★ 产生重复起始信号,切换为读模式 ★ */
base->I2CR |= (1 << 2); /* 产生重复起始信号 */
base->I2SR &= ~(1 << 1);
base->I2DR = dev_addr << 1 | 1; /* 写入设备地址和读标志(1) */
while((base->I2SR & (1 << 1)) == 0);
/* 切换为接收模式 */
base->I2CR &= ~(1 << 4); /* 切换为接收模式 */
base->I2SR &= ~(1 << 1); /* 清除中断标志位 */
/* 如果只读取一个字节,需要提前设置置NACK */
if(1 == len)
{
base->I2CR |= (1 << 3); /* 发送NACK */
}
/* 虚假读,触发数据接收 */
*data = base->I2DR;
/* 第二次发送完设备地址后,便转换成接收方了,
此时读取I2DR寄存器就会让I2C产生一次数据传输,
因此第一个字节读出来的数据实际上是无效的! */
/* 开始连续读取若干字节,注意一定要在读到最后一个字节时回复NACK */
while(len--)
{
while((base->I2SR & (1 << 1)) == 0); /* 等待传输完成 */
base->I2SR &= ~(1 << 1);
/* 最后一个字节处理 */
if(0 == len)
{
int t = 0;
base->I2CR &= ~((1 << 5) | (1 << 3)); /* 清除主机模式和NACK */
/* 等待STOP信号完成 */
while((base->I2SR | (1 << 5)) && t < 10)
{
++t;
delay_us(100);
}
}
/* 倒数第二个字节时,需要发送NACK */
if(1 == len)
{
base->I2CR |= (1 << 3); /* 发送NACK */
}
/* 读取数据 */
*data++ = base->I2DR;
}
}
七、封装通用传输接口(屏蔽底层)
为了让上层代码不用关心底层读写细节,定义一个结构体和传输函数:
7.1 I2C_Msg 结构体
c
struct I2C_Msg
{
unsigned char dev_addr; // 从设备地址
unsigned short reg_addr; // 寄存器地址
unsigned char reg_addr_len; // 寄存器地址字节数
unsigned char *data; // 数据缓冲区指针
unsigned int len; // 数据长度
enum I2C_Direction direction; // 方向:I2C_READ 或 I2C_WRITE
};
7.2 通用传输函数(i2c_transfer)
c
void i2c_transfer(I2C_Type *base, struct I2C_Msg *msg)
{
/* 参数检查 */
if(0 == base)
{
return;
}
/* 根据传输方向选择读或写操作 */
if(msg->direction == I2C_READ)
{
i2c_read(base, msg->dev_addr, msg->reg_addr,
msg->reg_addr_len, msg->data, msg->len);
}
else
{
i2c_write(base, msg->dev_addr, msg->reg_addr,
msg->reg_addr_len, msg->data, msg->len);
}
}
八、实战应用:读取 LM75 温度传感器
LM75 基本信息:
- 设备地址:
0x48 - 寄存器地址:
0(温度寄存器) - 寄存器地址长度:1字节
- 每次读取:2字节(16位温度数据)
- 温度计算:
t = (data[0] << 8 | data[1]); return (t >> 7) * 0.5;
注意:LM75 内部没有什么读数数据存放的位置,转而代之的是寄存器地址(和24C02的存储位置是完全一样的用法)。这里的寄存器指的是LM75内部的寄存器,但用法和24C02的存储位置是完全一样的。
c
float lm75_get_temperature(void)
{
unsigned char data[2];
struct I2C_Msg msg =
{
.dev_addr = 0x48,
.reg_addr = 0,
.reg_addr_len = 1,
.data = data,
.len = 2,
.direction = I2C_READ
};
i2c_transfer(I2C1, &msg);
short t = (data[0] << 8) | data[1];
return (t >> 7) * 0.5;
}
如此一来,LM75的温度采集程序就变得十分简单,完全不需要关心底层 I2C 时序!
九、关键寄存器速查表
| 寄存器 | 位 | 名称 | 功能 |
|---|---|---|---|
| I2Cx_IADR | bit7:1 | ADR | 从设备地址 |
| I2Cx_IFDR | bit5:0 | IC | I2C 波特率分频值 |
| I2Cx_I2CR | bit7 | IEN | I2C 使能 |
| I2Cx_I2CR | bit6 | IIEN | I2C 中断使能 |
| I2Cx_I2CR | bit5 | MSTA | 主/从模式选择 |
| I2Cx_I2CR | bit4 | MTX | 传输方向(0=收,1=发) |
| I2Cx_I2CR | bit3 | TXAK | 应答使能(0=ACK,1=NACK) |
| I2Cx_I2CR | bit2 | RSTA | 重复起始信号 |
| I2Cx_I2SR | bit7 | ICF | 传输状态(1=完成) |
| I2Cx_I2SR | bit5 | IBB | 总线忙标志(1=忙) |
| I2Cx_I2SR | bit4 | IAL | 仲裁丢失(1=丢失) |
| I2Cx_I2SR | bit1 | IIF | 中断挂起(软件清零) |
| I2Cx_I2SR | bit0 | RXAK | 应答标志(0=ACK,1=NACK) |
| I2Cx_I2DR | bit7:0 | DATA | 发送/接收数据寄存器 |
十、注意事项总结
- I2C 引脚必须配置为开漏输出(OD),外部需要上拉电阻
- SION 位必须置1,使 SDA/SCL 引脚同时作为输入(用于采样)
- IIF 标志位需要软件清零,每次传输前后都要处理
- 读操作第一个字节无效,触发读后要丢弃第一次虚读的结果
- 最后一个字节必须回复 NACK,否则从机不知道何时停止
- 发完起始位后检查 IAL,判断是否发生仲裁丢失
- 波特率不能随意设置,必须查手册提供的分频表(IFDR 寄存器)
- 封装思想:底层写好 i2c_write/i2c_read,上层只需填 I2C_Msg 结构体,代码复用性强,移植方便