一、I2C 基础概念与硬件特性
1.1 I2C 总线核心定义
I2C(Inter-Integrated Circuit)是飞利浦提出的串行半双工通信总线,核心特点是两根信号线实现多设备互联:
- SDA(Serial Data):双向数据线,用于传输数据;
- SCL(Serial Clock):双向时钟线,由主设备产生,同步数据传输;
- 上拉电阻:SDA 和 SCL 需外接(或引脚内部配置)上拉电阻(通常 4.7KΩ),空闲时保持高电平;
- 主从架构:同一总线中仅 1 个主设备(如 I.MX6ULL),可挂载多个从设备(如 AT24C02、LM75),通过设备地址区分从设备。
1.2 I.MX6ULL I2C 硬件特性
- 控制器数量:共 4 路 I2C 控制器(I2C1~I2C4),支持主 / 从模式;
- 传输速率:标准模式(100Kbps)、快速模式(400Kbps);
- 时钟源:默认使用IPG_CLK_ROOT(66MHz),通过分频器得到 I2C 工作时钟;
- FIFO 支持:部分控制器含 TX/RXFIFO(如 ECSPI 关联的 I2C 无 FIFO,需软件模拟时序);
- 中断支持:可配置 FIFO 空、传输完成、仲裁丢失等中断;
- 器件兼容性:支持 I2C 标准从设备(EEPROM、传感器、时钟芯片等),本次重点适配AT24C02(EEPROM) 和LM75(温度传感器)。
二、I2C 核心通信时序
2.1 基础时序单元
- 起始信号(S):SCL 为高电平时,SDA 从高电平拉低(下降沿),标志通信开始;
- 停止信号(P):SCL 为高电平时,SDA 从低电平拉高(上升沿),标志通信结束;
- 数据传输:SCL 高电平时,SDA 电平需稳定(数据有效);SCL 低电平时,SDA 可切换电平(准备下一位数据);
- 应答(ACK):主设备发送 1 字节后,释放 SDA;从设备在 SCL 高电平时拉低 SDA,表示数据接收成功;
- 非应答(NACK):主设备接收最后 1 字节后,SCL 高电平时保持 SDA 高电平,表示无需继续接收。
2.2 核心操作时序
从设备写操作(主→从,如向 AT24C02 写数据)
- 主设备发送起始信号(S);
- 主设备发送从设备地址 + 写标志(最低位为 0),等待从设备应答(ACK);
- 主设备发送从设备内部寄存器地址(如 AT24C02 的存储地址),等待应答;
- 主设备发送数据(1~N 字节),每字节后等待应答;
- 主设备发送停止信号(P),结束写操作。
从设备读操作(从→主,如从 LM75 读温度)
- 主设备发送起始信号(S);
- 主设备发送从设备地址 + 写标志(0),等待应答(此时目的是 “告知读哪个寄存器”);
- 主设备发送目标寄存器地址(如 LM75 的温度寄存器 0x00),等待应答;
- 主设备发送重复起始信号(S)(不发停止信号,避免总线释放);
- 主设备发送从设备地址 + 读标志(1),等待应答;
- 主设备接收数据(1~N 字节):
- 接收前 N-1 字节后,主设备发送 ACK;
- 接收最后 1 字节后,主设备发送 NACK(告知从设备停止发送);
- 主设备发送停止信号(P),结束读操作。
三、I.MX6ULL I2C 寄存器详解
核心寄存器
- I2Cx_IADR:从设备地址寄存器;
- I2Cx_IFDR:分频寄存器(决定 I2C 波特率);
- I2Cx_I2CR:控制寄存器;
- I2Cx_I2SR:状态寄存器;
- I2Cx_I2DR:数据寄存器。
四、I2C 完整实现流程
4.1 I2C 引脚初始化
- 配置复用功能和电气特性,以 I2C1 为例(SDA=UART4_RX,SCL=UART4_TX);
- 初始化 I2C 控制器(先关闭,再配置分频)。
4.2 I2C 通用读写函数
i2c_write:向指定从设备的指定寄存器写入 N 字节数据;i2c_read:从指定从设备的指定寄存器读取 N 字节数据。
I2C 写函数(i2c_write)
功能:向指定从设备的指定寄存器写入 N 字节数据
1// base:I2C控制器基地址(如I2C1) 2// device_address:从设备地址(如LM75=0x48) 3// reg_address:从设备寄存器地址(如LM75温度寄存器=0x00) 4// reg_len:寄存器地址长度(1或2字节) 5// data:待写入数据指针 6// len:数据长度 7void i2c_write(I2C_Type *base, unsigned char device_address, unsigned short reg_address, int reg_len, const unsigned char *data, int len) 8{ 9 // 1. 清除仲裁丢失和中断标志,等待总线空闲 10 base->I2SR &= ~((1 << 4) | (1 << 1)); // 清除IAL(bit4)和IIF(bit1) 11 while((base->I2SR & (1 << 7)) == 0); // 等待ICF(bit7)=1(总线空闲) 12 13 // 2. 配置为主设备发送模式,发送ACK 14 base->I2CR |= (1 << 5) | (1 << 4); // MSTA=1(主模式)、MTX=1(发送) 15 base->I2CR &= ~(1 << 3); // TXAK=0(发送ACK) 16 17 // 3. 发送从设备地址(写模式:最低位=0) 18 base->I2SR &= ~(1 << 1); // 清除IIF(中断标志) 19 base->I2DR = device_address << 1; // 设备地址+写标志 20 while((base->I2SR & (1 << 1)) == 0); // 等待传输完成(IIF=1) 21 22 // 4. 发送寄存器地址(支持1/2字节) 23 for(int i = 0; i < reg_len; ++i) 24 { 25 base->I2SR &= ~(1 << 1); // 清除IIF 26 // 高位在前:若reg_len=2,先发高8位,再发低8位 27 base->I2DR = reg_address >> (reg_len - i - 1) * 8; 28 while((base->I2SR & (1 << 1)) == 0); // 等待传输完成 29 } 30 31 // 5. 发送数据(N字节) 32 while (len--) 33 { 34 base->I2SR &= ~(1 << 1); // 清除IIF 35 base->I2DR = *data++; // 写入1字节数据 36 while((base->I2SR & (1 << 1)) == 0); // 等待传输完成 37 } 38 39 // 6. 发送停止信号(清除主模式) 40 base->I2CR &= ~(1 << 5); // MSTA=0(释放主模式,产生停止信号) 41 while((base->I2SR & (1 << 5)) != 0); // 等待IBB=0(总线空闲) 42 delayus(100); // 短暂延时,确保停止信号稳定 43} 44
I2C 读函数(i2c_read)
功能:从指定从设备的指定寄存器读取 N 字节数据
1void i2c_read(I2C_Type *base, unsigned char device_address, unsigned short reg_address, int reg_len, unsigned char *data, int len) 2{ 3 // 1. 清除标志,等待总线空闲(同写函数) 4 base->I2SR &= ~((1 << 4) | (1 << 1)); 5 while((base->I2SR & (1 << 7)) == 0); 6 7 // 2. 配置为主设备发送模式,先写寄存器地址 8 base->I2CR |= (1 << 5) | (1 << 4); // MSTA=1、MTX=1 9 base->I2CR &= ~(1 << 3); // TXAK=0(发送ACK) 10 11 // 3. 发送从设备地址(写模式) 12 base->I2SR &= ~(1 << 1); 13 base->I2DR = device_address << 1; 14 while((base->I2SR & (1 << 1)) == 0); 15 16 // 4. 发送寄存器地址(同写函数) 17 for(int i = 0; i < reg_len; ++i) 18 { 19 base->I2SR &= ~(1 << 1); 20 base->I2DR = reg_address >> (reg_len - i - 1) * 8; 21 while((base->I2SR & (1 << 1)) == 0); 22 } 23 24 // 5. 发送重复起始信号,切换为读模式 25 base->I2CR |= (1 << 2); // RSTA=1(产生重复起始) 26 base->I2SR &= ~(1 << 1); 27 base->I2DR = device_address << 1 | 1; // 设备地址+读标志(最低位=1) 28 while((base->I2SR & (1 << 1)) == 0); 29 30 // 6. 切换为接收模式 31 base->I2CR &= ~(1 << 4); // MTX=0(接收) 32 base->I2SR &= ~(1 << 1); 33 34 // 7. 若仅读1字节,提前发送NACK 35 if(len == 1) 36 { 37 base->I2CR |= (1 << 3); // TXAK=1(发送NACK) 38 } 39 *data = base->I2DR; // 虚假读:触发接收(I2C全双工,发送时已接收无效数据) 40 41 // 8. 接收N字节数据 42 while(len-- != 0) 43 { 44 while ((base->I2SR & (1 << 1)) == 0); // 等待接收完成 45 base->I2SR &= ~(1 << 1); 46 47 // 处理最后1字节:发送停止信号 48 if(len == 0) 49 { 50 base->I2CR &= ~((1 << 5) | (1 << 3)); // MSTA=0(停止)、TXAK=0 51 while((base->I2SR & (1 << 5)) != 0); // 等待总线空闲 52 } 53 // 处理倒数第2字节:提前发送NACK 54 else if (len == 1) 55 { 56 base->I2CR |= (1 << 3); // TXAK=1(下一字节发NACK) 57 } 58 59 *data++ = base->I2DR; // 读取接收数据 60 } 61} 62
4.3 封装传输函数(xfer)
封装I2C_MSG结构体,统一管理传输参数,提高代码复用性:
1// i2c.h 中定义结构体和枚举 2enum I2C_Direction 3{ 4 I2C_Write = 0, // 写方向 5 I2C_Read = 1 // 读方向 6}; 7 8struct I2C_MSG 9{ 10 unsigned char dev_address; // 从设备地址 11 unsigned short reg_address; // 寄存器地址 12 int reg_len; // 寄存器地址长度(1/2) 13 unsigned char *data; // 数据指针 14 int len; // 数据长度 15 enum I2C_Direction direction;// 传输方向 16}; 17 18// i2c.c 中实现传输函数 19void xfer(I2C_Type *base, struct I2C_MSG *msg) 20{ 21 if(msg->direction == I2C_Write) 22 { 23 i2c_write(base, msg->dev_address, msg->reg_address, msg->reg_len, msg->data, msg->len); 24 } 25 else 26 { 27 i2c_read(base, msg->dev_address, msg->reg_address, msg->reg_len, msg->data, msg->len); 28 } 29} 30
五、LM75 温度传感器驱动(基于 I2C)
LM75 是 I2C 接口的温度传感器,设备地址为0x48,温度寄存器(0x00)存储 16 位数据(高 9 位为温度值,单位 0.5℃)。
5.1 温度读取函数(lm75.c)
1#include "lm75.h" 2#include "i2c.h" 3#include "MCIMX6Y2.h" 4 5// 读取LM75温度(返回值:℃,如25.5℃返回25.5) 6float lm75_get_temperature(void) 7{ 8 unsigned char buffer[2] = {0}; // 存储16位温度数据 9 short temp_raw; // 原始温度值(16位) 10 11 // 1. 构造I2C传输参数 12 struct I2C_MSG msg = 13 { 14 .direction = I2C_Read, // 读方向 15 .dev_address = 0x48, // LM75设备地址 16 .reg_address = 0x00, // 温度寄存器地址(1字节) 17 .reg_len = 1, // 寄存器地址长度=1 18 .data = buffer, // 数据缓冲区 19 .len = 2 // 读取2字节 20 }; 21 22 // 2. 调用I2C传输函数 23 xfer(I2C1, &msg); 24 25 // 3. 解析温度数据(LM75数据格式:高8位+低8位,高9位有效) 26 temp_raw = (buffer[0] << 8) | buffer[1]; // 组合16位原始数据 27 temp_raw >>= 7; // 右移7位,保留高9位(符号位+8位数值) 28 return temp_raw * 0.5f; // 0.5℃/LSB,转换为实际温度 29} 30
5.2 头文件声明(lm75.h)
1#ifndef __LM75_H__ 2#define __LM75_H__ 3 4// 读取LM75温度,返回值单位:℃ 5extern float lm75_get_temperature(void); 6 7#endif 8
5.3 主函数测试(main.c)
初始化 I2C、UART 和 LM75,通过 UART 打印温度数据:
1#include "led.h" 2#include "uart.h" 3#include "i2c.h" 4#include "lm75.h" 5#include "delay.h" 6#include "stdio.h" 7 8int main(void) 9{ 10 // 1. 初始化系统时钟、UART(用于打印)、I2C1 11 init_clock(); // 初始化系统时钟(IPG_CLK=66MHz) 12 init_uart1(); // 初始化UART1(115200bps,用于打印温度) 13 init_i2c1(); // 初始化I2C1(100Kbps) 14 15 while(1) 16 { 17 // 2. 读取LM75温度 18 float temp = lm75_get_temperature(); 19 20 // 3. 格式化温度数据(避免浮点数打印误差) 21 int temp_int = (int)temp; // 整数部分(如25.5→25) 22 int temp_dec = (int)((temp - temp_int) * 10); // 小数部分(如25.5→5) 23 24 // 4. 通过UART打印温度 25 printf("LM75 Temperature: %d.%d℃\n", temp_int, temp_dec); 26 27 delayms(1000); // 1秒刷新一次 28 } 29 return 0; 30} 31
六、关键注意事项
- 上拉电阻配置:I2C 总线必需上拉,可通过引脚电气属性配置内部上拉(如
IOMUXC_SetPinConfig的PUS位),或外接 4.7KΩ 电阻; - 仲裁丢失处理:若多主设备竞争总线,
I2SR->IAL会置 1,需清除该位后重新初始化 I2C; - 应答判断:传输过程中需检查
I2SR->RXAK,若为 1(接收 NACK),需重新发送或终止通信; - 寄存器地址长度:不同器件的寄存器地址长度不同(如 AT24C02 为 1 字节,某些传感器为 2 字节),需在
I2C_MSG中正确设置reg_len; - SION 位使能:部分 I2C 引脚需使能
SION(软件输入路径),否则无法读取 SDA 电平(如IOMUXC_SetPinMux的第 2 个参数设为 1); - 时钟分频计算:I2C 波特率 = IPG_CLK / 分频系数,标准模式(100Kbps)推荐分频系数 = 640(66MHz/640≈103Kbps),快速模式(400Kbps)推荐分频系数 = 160(66MHz/160≈412.5Kbps)。
七、总结
I.MX6ULL 的 I2C 开发的核心是严格遵循时序规范和熟练操作寄存器,关键流程可概括为:引脚复用配置 → I2C控制器初始化(分频、使能) → 封装通用读写函数 → 适配具体I2C器件(LM75/AT24C02) → 测试验证。通过结构体封装传输参数(如I2C_MSG)可显著提高代码复用性,这一思想也与 Linux 内核 I2C 子系统的设计一致,为后续驱动开发打下基础。
《嵌入式硬件——基于IMX6ULL的I2C实现》 是转载文章,点击查看原文。