1. IMX6ULL平台I2C总线概述
I2C(Inter-Integrated Circuit)总线是飞思卡尔IMX6ULL处理器中一项基础但至关重要的外设接口。作为双线制串行通信协议,它通过SDA(数据线)和SCL(时钟线)两根信号线实现主从设备间的数据交换,最高支持400kHz(快速模式)的通信速率。在嵌入式开发中,I2C常被用于连接各类传感器、EEPROM、触摸控制器等低速外设。
IMX6ULL的I2C控制器具有以下硬件特性:
- 支持主从模式切换
- 可编程时钟频率调节
- 内置FIFO缓冲(深度64字节)
- 多主机仲裁机制
- 支持7位/10位地址模式
实际项目中,我们常需要在不依赖Linux内核驱动的情况下,直接通过寄存器操作实现I2C通信。这种裸机驱动开发方式对理解硬件工作原理、时序控制以及异常处理机制有着更高要求。
2. 硬件原理深度解析
2.1 物理层信号特性
IMX6ULL的I2C接口采用开漏输出设计,必须外接上拉电阻(典型值4.7kΩ)。信号电压与芯片IO电压相关,在3.3V系统下:
- 低电平阈值:VIL ≤ 0.3VDD ≈ 1V
- 高电平阈值:VIH ≥ 0.7VDD ≈ 2.31V
信号时序参数需严格满足I2C协议规范:
- 起始条件保持时间(tHD;STA)≥ 600ns
- 数据保持时间(tHD;DAT)≥ 0ns
- 时钟低电平周期(tLOW)≥ 1.3μs
- 时钟高电平周期(tHIGH)≥ 0.6μs
2.2 寄存器映射详解
IMX6ULL的I2C控制器通过以下关键寄存器实现配置:
| 寄存器名 | 地址偏移 | 功能描述 |
|---|---|---|
| I2CR_IADR | 0x00 | 从设备地址寄存器 |
| I2CR_IFDR | 0x04 | 时钟分频配置(设置SCL频率) |
| I2CR_I2CR | 0x08 | 控制寄存器(使能/中断设置) |
| I2CR_I2SR | 0x0C | 状态寄存器(传输状态指示) |
| I2CR_I2DR | 0x10 | 数据收发寄存器 |
时钟分频计算公式:
code复制SCL频率 = IPG_CLK / (20 × (IFDR[5:0] × 2 + 3))
其中IPG_CLK通常为66MHz,IFDR取值0-63。例如要配置100kHz时钟,IFDR应设为0x15。
3. 裸机驱动实现步骤
3.1 初始化流程
- GPIO复用配置:
c复制// 配置I2C1_SCL(GPIO1_IO20)和I2C1_SDA(GPIO1_IO21)
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO20_I2C1_SCL, 0);
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO21_I2C1_SDA, 0);
// 设置电气特性(开漏、上拉等)
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO20_I2C1_SCL, 0x70B0);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO21_I2C1_SDA, 0x70B0);
- 时钟使能:
c复制CCM->CCGR1 |= CCM_CCGR1_I2C1(CCM_CCGR_ON);
- 控制器配置:
c复制I2C1->I2CR &= ~(1 << 7); // 先禁用I2C控制器
I2C1->IFDR = 0x15; // 设置100kHz时钟
I2C1->I2CR |= (1 << 7); // 使能I2C
3.2 数据传输实现
典型的数据发送函数实现:
c复制int i2c_write(uint8_t addr, uint8_t reg, uint8_t val) {
// 发送起始条件
I2C1->I2CR |= I2CR_I2CR_MSTA_MASK;
I2C1->I2CR |= I2CR_I2CR_MTX_MASK;
// 发送设备地址(写模式)
I2C1->I2DR = (addr << 1) | 0;
while(!(I2C1->I2SR & I2SR_IIF_MASK)); // 等待传输完成
I2C1->I2SR &= ~I2SR_IIF_MASK;
// 发送寄存器地址
I2C1->I2DR = reg;
while(!(I2C1->I2SR & I2SR_IIF_MASK));
I2C1->I2SR &= ~I2SR_IIF_MASK;
// 发送数据
I2C1->I2DR = val;
while(!(I2C1->I2SR & I2SR_IIF_MASK));
I2C1->I2SR &= ~I2SR_IIF_MASK;
// 发送停止条件
I2C1->I2CR &= ~I2CR_I2CR_MSTA_MASK;
return 0;
}
3.3 中断处理机制
IMX6ULL的I2C中断通过以下步骤实现:
- 在I2CR寄存器中使能中断(I2CR_IIEN位)
- 在NVIC中注册中断服务函数
- 在ISR中检查I2SR状态位并处理
示例中断服务函数:
c复制void I2C1_IRQHandler(void) {
uint8_t status = I2C1->I2SR;
if(status & I2SR_IAL_MASK) {
// 仲裁丢失处理
I2C1->I2SR &= ~I2SR_IAL_MASK;
}
if(status & I2SR_IIF_MASK) {
// 正常传输中断
// ...数据处理逻辑...
I2C1->I2SR &= ~I2SR_IIF_MASK;
}
}
4. 关键问题与调试技巧
4.1 常见硬件问题排查
- 信号完整性问题:
- 现象:通信随机失败,示波器显示信号振铃
- 解决方案:
- 缩短走线长度(建议<10cm)
- 增加上拉电阻值(如改为10kΩ)
- 在信号线上串联33Ω电阻
- 从设备无响应:
- 检查清单:
- 确认从设备地址正确(7位地址需左移1位)
- 测量SCL/SDA电压是否达到高电平阈值
- 确认从设备供电正常
- 检查ACK信号是否正常返回
4.2 软件调试方法
- 寄存器级调试:
c复制// 打印关键寄存器状态
void i2c_debug(void) {
printf("I2CR: 0x%X\n", I2C1->I2CR);
printf("I2SR: 0x%X\n", I2C1->I2SR);
printf("I2DR: 0x%X\n", I2C1->I2DR);
}
- 逻辑分析仪抓包:
- 推荐配置:
- 采样率:至少4MHz
- 触发条件:SCL下降沿
- 解码协议:I2C(设置地址格式为7位)
- 时序违规检测:
c复制// 在关键操作间插入延时
#define I2C_DELAY() __asm volatile("nop; nop; nop; nop")
void i2c_start(void) {
I2C1->I2CR |= I2CR_I2CR_MSTA_MASK;
I2C_DELAY(); // 满足tHD;STA要求
}
5. 性能优化实践
5.1 DMA传输配置
对于大数据量传输(如EEPROM读写),可使用DMA减轻CPU负担:
- 配置DMA通道源/目标地址为I2DR寄存器
- 设置传输数据长度
- 使能I2C的DMA请求:
c复制I2C1->I2CR |= I2CR_I2CR_DMAEN_MASK;
5.2 时钟优化技巧
当系统需要动态调整I2C速率时:
c复制void i2c_change_speed(uint32_t freq) {
uint8_t div = (IPG_CLK / (20 * freq) - 3) / 2;
I2C1->I2CR &= ~I2CR_I2CR_IEN_MASK; // 禁用I2C
I2C1->IFDR = div & 0x3F; // 设置新分频值
I2C1->I2CR |= I2CR_I2CR_IEN_MASK; // 重新使能
}
5.3 低功耗模式处理
在系统进入低功耗前:
- 保存I2C寄存器状态
- 关闭I2C时钟
- 配置GPIO为高阻态
唤醒后恢复流程:
c复制void i2c_restore(void) {
// 恢复GPIO配置
IOMUXC_SetPinMux(IOMUXC_GPIO1_IO20_I2C1_SCL, 0);
IOMUXC_SetPinConfig(IOMUXC_GPIO1_IO20_I2C1_SCL, 0x70B0);
// 恢复寄存器状态
I2C1->IFDR = saved_regs.ifdr;
I2C1->I2CR = saved_regs.i2cr;
}
6. 实际应用案例
6.1 连接AT24C02 EEPROM
典型读写操作实现:
c复制#define EEPROM_ADDR 0x50
void eeprom_write(uint8_t addr, uint8_t data) {
i2c_start();
i2c_send_byte((EEPROM_ADDR << 1) | 0);
i2c_send_byte(addr);
i2c_send_byte(data);
i2c_stop();
delay_ms(10); // 等待写入完成
}
uint8_t eeprom_read(uint8_t addr) {
uint8_t data;
i2c_start();
i2c_send_byte((EEPROM_ADDR << 1) | 0);
i2c_send_byte(addr);
i2c_repeated_start();
i2c_send_byte((EEPROM_ADDR << 1) | 1);
data = i2c_receive_byte(0); // 发送NACK结束
i2c_stop();
return data;
}
6.2 驱动MPU6050传感器
初始化配置示例:
c复制void mpu6050_init(void) {
// 唤醒设备
i2c_write_byte(MPU6050_ADDR, MPU6050_PWR_MGMT_1, 0x00);
// 配置陀螺仪量程 ±2000°/s
i2c_write_byte(MPU6050_ADDR, MPU6050_GYRO_CONFIG, 0x18);
// 配置加速度计量程 ±8g
i2c_write_byte(MPU6050_ADDR, MPU6050_ACCEL_CONFIG, 0x10);
// 设置DLPF带宽 42Hz
i2c_write_byte(MPU6050_ADDR, MPU6050_CONFIG, 0x03);
}
数据读取函数:
c复制void mpu6050_read(int16_t *accel, int16_t *gyro) {
uint8_t buf[14];
i2c_read_bytes(MPU6050_ADDR, MPU6050_ACCEL_XOUT_H, buf, 14);
accel[0] = (buf[0] << 8) | buf[1]; // AX
accel[1] = (buf[2] << 8) | buf[3]; // AY
accel[2] = (buf[4] << 8) | buf[5]; // AZ
gyro[0] = (buf[8] << 8) | buf[9]; // GX
gyro[1] = (buf[10] << 8) | buf[11]; // GY
gyro[2] = (buf[12] << 8) | buf[13]; // GZ
}
7. 进阶开发技巧
7.1 多主机仲裁处理
当检测到仲裁丢失(IAL标志置位)时:
- 立即释放总线
- 等待随机延时后重试
- 记录错误次数,超过阈值则报警
实现示例:
c复制int i2c_safe_write(uint8_t addr, uint8_t *data, int len) {
int retry = 0;
while(retry < 3) {
int ret = i2c_write(addr, data, len);
if(ret != -EAGAIN) return ret;
// 随机退避延时
delay_ms(10 + (rand() % 50));
retry++;
}
return -EBUSY;
}
7.2 时序严格性保障
对于时序敏感设备(如某些RTC芯片),需精确控制信号间隔:
c复制void i2c_strict_delay(void) {
uint32_t cycles = SystemCoreClock / 1000000; // 1us对应的周期数
DWT->CYCCNT = 0;
while(DWT->CYCCNT < cycles);
}
void rtc_write(uint8_t reg, uint8_t val) {
i2c_start();
i2c_strict_delay();
i2c_send_byte(RTC_ADDR);
// ...其余操作同样插入精确延时
}
7.3 错误恢复机制
完整的错误恢复流程应包括:
- 软件复位I2C控制器
c复制void i2c_reset(void) {
I2C1->I2CR &= ~I2CR_I2CR_IEN_MASK;
delay_ms(1);
I2C1->I2CR |= I2CR_I2CR_IEN_MASK;
}
- 重新初始化GPIO
- 检查从设备状态
- 记录错误日志
在实际项目中,我发现将SCL时钟频率适当降低(如从400kHz降到100kHz)能显著提高通信稳定性,特别是在长距离布线或连接多个从设备时。另外,对于关键数据通信,建议实现CRC校验机制,可以在应用层添加简单的校验和验证。