嵌入式硬件——基于IMX6ULL的I2C实现
一、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 字节数据
// base:I2C控制器基地址(如I2C1)
// device_address:从设备地址(如LM75=0x48)
// reg_address:从设备寄存器地址(如LM75温度寄存器=0x00)
// reg_len:寄存器地址长度(1或2字节)
// data:待写入数据指针
// len:数据长度
void i2c_write(I2C_Type *base, unsigned char device_address, unsigned short reg_address, int reg_len, const unsigned char *data, int len)
{// 1. 清除仲裁丢失和中断标志,等待总线空闲base->I2SR &= ~((1 << 4) | (1 << 1)); // 清除IAL(bit4)和IIF(bit1)while((base->I2SR & (1 << 7)) == 0); // 等待ICF(bit7)=1(总线空闲)// 2. 配置为主设备发送模式,发送ACKbase->I2CR |= (1 << 5) | (1 << 4); // MSTA=1(主模式)、MTX=1(发送)base->I2CR &= ~(1 << 3); // TXAK=0(发送ACK)// 3. 发送从设备地址(写模式:最低位=0)base->I2SR &= ~(1 << 1); // 清除IIF(中断标志)base->I2DR = device_address << 1; // 设备地址+写标志while((base->I2SR & (1 << 1)) == 0); // 等待传输完成(IIF=1)// 4. 发送寄存器地址(支持1/2字节)for(int i = 0; i < reg_len; ++i) {base->I2SR &= ~(1 << 1); // 清除IIF// 高位在前:若reg_len=2,先发高8位,再发低8位base->I2DR = reg_address >> (reg_len - i - 1) * 8;while((base->I2SR & (1 << 1)) == 0); // 等待传输完成}// 5. 发送数据(N字节)while (len--) {base->I2SR &= ~(1 << 1); // 清除IIFbase->I2DR = *data++; // 写入1字节数据while((base->I2SR & (1 << 1)) == 0); // 等待传输完成}// 6. 发送停止信号(清除主模式)base->I2CR &= ~(1 << 5); // MSTA=0(释放主模式,产生停止信号)while((base->I2SR & (1 << 5)) != 0); // 等待IBB=0(总线空闲)delayus(100); // 短暂延时,确保停止信号稳定
}
I2C 读函数(i2c_read)
功能:从指定从设备的指定寄存器读取 N 字节数据
void i2c_read(I2C_Type *base, unsigned char device_address, unsigned short reg_address, int reg_len, unsigned char *data, int len)
{// 1. 清除标志,等待总线空闲(同写函数)base->I2SR &= ~((1 << 4) | (1 << 1));while((base->I2SR & (1 << 7)) == 0);// 2. 配置为主设备发送模式,先写寄存器地址base->I2CR |= (1 << 5) | (1 << 4); // MSTA=1、MTX=1base->I2CR &= ~(1 << 3); // TXAK=0(发送ACK)// 3. 发送从设备地址(写模式)base->I2SR &= ~(1 << 1);base->I2DR = device_address << 1;while((base->I2SR & (1 << 1)) == 0);// 4. 发送寄存器地址(同写函数)for(int i = 0; i < reg_len; ++i) {base->I2SR &= ~(1 << 1);base->I2DR = reg_address >> (reg_len - i - 1) * 8;while((base->I2SR & (1 << 1)) == 0);}// 5. 发送重复起始信号,切换为读模式base->I2CR |= (1 << 2); // RSTA=1(产生重复起始)base->I2SR &= ~(1 << 1);base->I2DR = device_address << 1 | 1; // 设备地址+读标志(最低位=1)while((base->I2SR & (1 << 1)) == 0);// 6. 切换为接收模式base->I2CR &= ~(1 << 4); // MTX=0(接收)base->I2SR &= ~(1 << 1);// 7. 若仅读1字节,提前发送NACKif(len == 1) {base->I2CR |= (1 << 3); // TXAK=1(发送NACK)}*data = base->I2DR; // 虚假读:触发接收(I2C全双工,发送时已接收无效数据)// 8. 接收N字节数据while(len-- != 0) {while ((base->I2SR & (1 << 1)) == 0); // 等待接收完成base->I2SR &= ~(1 << 1);// 处理最后1字节:发送停止信号if(len == 0) {base->I2CR &= ~((1 << 5) | (1 << 3)); // MSTA=0(停止)、TXAK=0while((base->I2SR & (1 << 5)) != 0); // 等待总线空闲} // 处理倒数第2字节:提前发送NACKelse if (len == 1) {base->I2CR |= (1 << 3); // TXAK=1(下一字节发NACK)}*data++ = base->I2DR; // 读取接收数据}
}
4.3 封装传输函数(xfer)
封装I2C_MSG结构体,统一管理传输参数,提高代码复用性:
// i2c.h 中定义结构体和枚举
enum I2C_Direction
{I2C_Write = 0, // 写方向I2C_Read = 1 // 读方向
};struct I2C_MSG
{unsigned char dev_address; // 从设备地址unsigned short reg_address; // 寄存器地址int reg_len; // 寄存器地址长度(1/2)unsigned char *data; // 数据指针int len; // 数据长度enum I2C_Direction direction;// 传输方向
};// i2c.c 中实现传输函数
void xfer(I2C_Type *base, struct I2C_MSG *msg)
{if(msg->direction == I2C_Write) {i2c_write(base, msg->dev_address, msg->reg_address, msg->reg_len, msg->data, msg->len);} else {i2c_read(base, msg->dev_address, msg->reg_address, msg->reg_len, msg->data, msg->len);}
}
五、LM75 温度传感器驱动(基于 I2C)
LM75 是 I2C 接口的温度传感器,设备地址为0x48
,温度寄存器(0x00)存储 16 位数据(高 9 位为温度值,单位 0.5℃)。
5.1 温度读取函数(lm75.c
)
#include "lm75.h"
#include "i2c.h"
#include "MCIMX6Y2.h"// 读取LM75温度(返回值:℃,如25.5℃返回25.5)
float lm75_get_temperature(void)
{unsigned char buffer[2] = {0}; // 存储16位温度数据short temp_raw; // 原始温度值(16位)// 1. 构造I2C传输参数struct I2C_MSG msg = {.direction = I2C_Read, // 读方向.dev_address = 0x48, // LM75设备地址.reg_address = 0x00, // 温度寄存器地址(1字节).reg_len = 1, // 寄存器地址长度=1.data = buffer, // 数据缓冲区.len = 2 // 读取2字节};// 2. 调用I2C传输函数xfer(I2C1, &msg);// 3. 解析温度数据(LM75数据格式:高8位+低8位,高9位有效)temp_raw = (buffer[0] << 8) | buffer[1]; // 组合16位原始数据temp_raw >>= 7; // 右移7位,保留高9位(符号位+8位数值)return temp_raw * 0.5f; // 0.5℃/LSB,转换为实际温度
}
5.2 头文件声明(lm75.h
)
#ifndef __LM75_H__
#define __LM75_H__// 读取LM75温度,返回值单位:℃
extern float lm75_get_temperature(void);#endif
5.3 主函数测试(main.c
)
初始化 I2C、UART 和 LM75,通过 UART 打印温度数据:
#include "led.h"
#include "uart.h"
#include "i2c.h"
#include "lm75.h"
#include "delay.h"
#include "stdio.h"int main(void)
{// 1. 初始化系统时钟、UART(用于打印)、I2C1init_clock(); // 初始化系统时钟(IPG_CLK=66MHz)init_uart1(); // 初始化UART1(115200bps,用于打印温度)init_i2c1(); // 初始化I2C1(100Kbps)while(1) {// 2. 读取LM75温度float temp = lm75_get_temperature();// 3. 格式化温度数据(避免浮点数打印误差)int temp_int = (int)temp; // 整数部分(如25.5→25)int temp_dec = (int)((temp - temp_int) * 10); // 小数部分(如25.5→5)// 4. 通过UART打印温度printf("LM75 Temperature: %d.%d℃\n", temp_int, temp_dec);delayms(1000); // 1秒刷新一次}return 0;
}
六、关键注意事项
- 上拉电阻配置: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 子系统的设计一致,为后续驱动开发打下基础。