嵌入式硬件——基于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 字节数据

c 复制代码
// 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. 配置为主设备发送模式,发送ACK
    base->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);       // 清除IIF
        base->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 字节数据

c 复制代码
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=1
    base->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字节,提前发送NACK
    if(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=0
            while((base->I2SR & (1 << 5)) != 0);    // 等待总线空闲
        } 
        // 处理倒数第2字节:提前发送NACK
        else if (len == 1) 
        {
            base->I2CR |= (1 << 3);  // TXAK=1(下一字节发NACK)
        }
        
        *data++ = base->I2DR;  // 读取接收数据
    }
}

4.3 封装传输函数(xfer)

封装I2C_MSG结构体,统一管理传输参数,提高代码复用性:

c 复制代码
// 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

cs 复制代码
#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

cs 复制代码
#ifndef __LM75_H__
#define __LM75_H__

// 读取LM75温度,返回值单位:℃
extern float lm75_get_temperature(void);

#endif

5.3 主函数测试(main.c

初始化 I2C、UART 和 LM75,通过 UART 打印温度数据:

cs 复制代码
#include "led.h"
#include "uart.h"
#include "i2c.h"
#include "lm75.h"
#include "delay.h"
#include "stdio.h"

int main(void) 
{
    // 1. 初始化系统时钟、UART(用于打印)、I2C1
    init_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;
}

六、关键注意事项

  1. 上拉电阻配置 :I2C 总线必需上拉,可通过引脚电气属性配置内部上拉(如IOMUXC_SetPinConfigPUS位),或外接 4.7KΩ 电阻;
  2. 仲裁丢失处理 :若多主设备竞争总线,I2SR->IAL会置 1,需清除该位后重新初始化 I2C;
  3. 应答判断 :传输过程中需检查I2SR->RXAK,若为 1(接收 NACK),需重新发送或终止通信;
  4. 寄存器地址长度 :不同器件的寄存器地址长度不同(如 AT24C02 为 1 字节,某些传感器为 2 字节),需在I2C_MSG中正确设置reg_len
  5. SION 位使能 :部分 I2C 引脚需使能SION(软件输入路径),否则无法读取 SDA 电平(如IOMUXC_SetPinMux的第 2 个参数设为 1);
  6. 时钟分频计算: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 子系统的设计一致,为后续驱动开发打下基础。

相关推荐
于小猿Sup10 小时前
VMware在Ubuntu22.04驱动Livox Mid360s
linux·c++·嵌入式硬件·自动驾驶
chao18984412 小时前
STM32 HAL库驱动AT24C02 EEPROM例程
stm32·单片机·嵌入式硬件
不会武功的火柴13 小时前
SystemVerilog语法(8)-有限状态机(FSM)
嵌入式硬件·fpga开发·自动化·ic验证·rtl·uvm方法学
嵌入式小站15 小时前
STM32 零基础可移植教程 05:按键消抖,为什么按一次会触发好几次
chrome·stm32·嵌入式硬件
czhaii16 小时前
跟我动手学FX系列PLC GX2环境
嵌入式硬件
2zcode17 小时前
基于STM32的智能扫地机器人设计与实现
stm32·嵌入式硬件·机器人
jllllyuz18 小时前
单相并网逆变器控制代码实现(STM32版)
stm32·单片机·嵌入式硬件
冉卓电子19 小时前
GD32C103RBT6 misc 内核驱动库极简解析
单片机·嵌入式硬件
yongui4783419 小时前
MAX6675 K型热电偶温度采集程序(Keil环境)
单片机·嵌入式硬件
豆包公子19 小时前
AUTOSAR CP XCP 移植到裸机 MCU-实践篇
单片机·嵌入式硬件