ARM-09-I.MX6U-I2C

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 通信规则

基本规则(重要!)

  1. 通信只能由主机发起
  2. 时钟线 SCL 永远由主机控制
  3. 数据线传输数据时由发送方控制,应答信号由接收方控制
    • 主机写:主机控制数据位,从机控制应答
    • 主机读:从机控制数据位,主机控制应答
  4. 主机第一次发送的数据永远是 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 发送/接收数据寄存器

十、注意事项总结

  1. I2C 引脚必须配置为开漏输出(OD),外部需要上拉电阻
  2. SION 位必须置1,使 SDA/SCL 引脚同时作为输入(用于采样)
  3. IIF 标志位需要软件清零,每次传输前后都要处理
  4. 读操作第一个字节无效,触发读后要丢弃第一次虚读的结果
  5. 最后一个字节必须回复 NACK,否则从机不知道何时停止
  6. 发完起始位后检查 IAL,判断是否发生仲裁丢失
  7. 波特率不能随意设置,必须查手册提供的分频表(IFDR 寄存器)
  8. 封装思想:底层写好 i2c_write/i2c_read,上层只需填 I2C_Msg 结构体,代码复用性强,移植方便
相关推荐
senijusene2 小时前
IMX6ULL 时钟系统配置与定时器 (EPIT/GPT)
stm32·单片机·fpga开发
会编程的小孩2 小时前
stm32f103c8t6工程模板 配置成stm32f407zgt6工程模板
stm32·单片机·嵌入式硬件
somi72 小时前
ARM-08-I.MX6U UART 串口
arm开发·单片机·嵌入式硬件·自用
mcupro2 小时前
TQTT_KU5P开发板教程---在Windows下XCKU5P+AD9361测试
嵌入式硬件·fpga开发·模块测试
青桔柠薯片3 小时前
IMX6ULL 时钟、定时器与中断系统:从晶体振荡器到GIC的硬件机制分析
嵌入式硬件·imx6ull
Zevalin爱灰灰3 小时前
零基础入门学用物联网(ESP8266) 第二部分 MQTT基础篇(五)
单片机·物联网·mqtt·嵌入式·esp8266
欢乐熊嵌入式编程3 小时前
做一个智能温湿度监控系统(含显示与数据上传)
单片机·温湿度·嵌入式学习·智能温湿度监控系统
辰哥单片机设计3 小时前
STM32智能家用垃圾桶(升级版)
stm32·单片机·嵌入式硬件
qq_150841993 小时前
浅析光模块固件之PC-MCU-Driver构架下的二级I2C从机的透传编程(再续)
单片机·嵌入式硬件