正点原子 I.MX6ULL MINI 开发板学习笔记:I2C 总线协议与控制器

分享
I2C 时序

这篇笔记继续正点原子 I.MX6ULL MINI 开发板裸机学习,主题是I2C 总线协议与控制器。I2C 是传感器、触摸屏、EEPROM 等芯片最常用的总线之一。代码来自实际工程 /home/zhao/code/2026/touch_screen。

1. 完成什么实验?

实现 I.MX6ULL I2C1/I2C2 控制器的初始化,以及基于 I2C 协议的字节写和字节读函数,为后续 LM75 温度传感器、GT9147 触摸屏驱动做准备。

2. 什么是 I2C?

I2C(Inter-Integrated Circuit)是飞利浦提出的半双工串行总线,只需要两根线:SCL(时钟)和 SDA(数据)。它采用主从架构,主设备通过 7 位或 10 位地址选择从设备。

I2C 读事务

3. I2C 通信的基本时序

  • 起始位 S:SCL 高电平时,SDA 从高变低。
  • 停止位 P:SCL 高电平时,SDA 从低变高。
  • 数据位:SCL 低电平时 SDA 改变,SCL 高电平时 SDA 保持稳定被采样。
  • ACK:每发送 8 位后,接收方在第 9 个时钟把 SDA 拉低表示应答。
  • 7 位地址:地址占高 7 位,最低位 R/W:0 表示写,1 表示读。

4. I2C 读寄存器为什么要两次起始?

读寄存器时,主机先发送从机地址(写方向)和寄存器地址,告诉从机要读哪个寄存器;然后发送重复起始 Sr,再发从机地址(读方向),从机才会把数据返回给主机。这就是“先写后读”的过程。

5. 真实可用的 I2C 代码

下面是工程中验证过的 i2c.c 核心代码,只保留 I2C1 初始化和通用读写。

#include "i2c.h"
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "gpt.h"

void init_i2c1(void)
{
    /* 引脚复用为 I2C1_SDA / I2C1_SCL */
    IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA, 1);
    IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL, 1);
    IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA, 0x70B0);
    IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL, 0x70B0);

    I2C1->I2CR &= ~(1 << 7);    /* 先关闭 I2C */
    I2C1->IFDR = 0x15;          /* 分频,约 100kHz */
    I2C1->I2CR |= (1 << 7);     /* 使能 I2C */
}

static void i2c_start(I2C_Type *base, unsigned char dev_addr, int dir)
{
    base->I2SR &= ~((1 << 4) | (1 << 1));   /* 清除仲裁丢失和中断标志 */
    while ((base->I2SR & (1 << 7)) == 0);   /* 等待上一字节传输完成(ICF,bit7) */

    base->I2CR |= (1 << 5) | (1 << 4);      /* 主机发送模式 */
    base->I2CR &= ~(1 << 3);                /* 发送 ACK */

    base->I2SR &= ~(1 << 1);
    base->I2DR = (dev_addr << 1) | dir;     /* 发送设备地址 + 方向 */
    while ((base->I2SR & (1 << 1)) == 0);   /* 等待传输完成 */
}
static void i2c_write(I2C_Type *base, unsigned char dev_addr,
                      unsigned short reg_addr, unsigned int reg_len,
                      const unsigned char *data, unsigned int len)
{
    base->I2SR &= ~((1 << 4) | (1 << 1));
    while ((base->I2SR & (1 << 7)) == 0);

    base->I2CR |= (1 << 5) | (1 << 4);      /* 主机 + 发送 */
    base->I2CR &= ~(1 << 3);

    base->I2SR &= ~(1 << 1);
    base->I2DR = dev_addr << 1;             /* 地址 + 写 */
    while ((base->I2SR & (1 << 1)) == 0);

    for (unsigned int i = 0; i < reg_len; i++)
    {
        base->I2SR &= ~(1 << 1);
        base->I2DR = reg_addr >> (8 * (reg_len - i - 1));
        while ((base->I2SR & (1 << 1)) == 0);
    }

    while (len--)
    {
        base->I2SR &= ~(1 << 1);
        base->I2DR = *data++;
        while ((base->I2SR & (1 << 1)) == 0);
    }

    base->I2CR &= ~(1 << 5);                /* 发送停止位 */
    int t = 0;
    while ((base->I2SR & (1 << 5)) != 0 && t < 10)
    {
        delay_us(100);
        ++t;
    }
}

static void i2c_read(I2C_Type *base, unsigned char dev_addr, unsigned short reg_addr, unsigned int reg_len, unsigned char *data, unsigned int len)
{
    base->I2SR &= ~((1 << IAL) | (1 << IIF));
    while((base->I2SR & (1 << ICF)) == 0);

    base->I2CR |= (1 << MSTA) | (1 << MTX);   /* 主机 + 发送 */
    base->I2CR &= ~(1 << TXAK);

    base->I2SR &= ~(1 << IIF);
    base->I2DR = dev_addr << 1;               /* 地址 + 写 */
    while((base->I2SR & (1 << IIF)) == 0);

    unsigned int i;
    for(i = 0;i < reg_len;++i)                /* 写寄存器地址 */
    {
        base->I2SR &= ~(1 << IIF);
        base->I2DR = reg_addr >> (8 * (reg_len - i - 1));
        while((base->I2SR & (1 << IIF)) == 0);
    }

    base->I2CR |= (1 << RSTA);                /* 重复起始 Sr */
    base->I2SR &= ~(1 << IIF);
    base->I2DR = (dev_addr << 1) | 1;         /* 地址 + 读 */
    while((base->I2SR & (1 << IIF)) == 0);

    base->I2CR &= ~(1 << MTX);                /* 切换为接收模式 */
    base->I2SR &= ~(1 << IIF);

    if(1 == len)
        base->I2CR |= (1 << TXAK);            /* 只读 1 字节,先准备 NACK */

    *data = base->I2DR;                       /* 假读一次,启动接收 */

    while(len--)
    {
        while((base->I2SR & (1 << IIF)) == 0);
        base->I2SR &= ~(1 << IIF);
        if(1 == len)
            base->I2CR |= (1 << TXAK);        /* 最后一字节前发 NACK */
        else if(0 == len)
        {
            base->I2CR &= ~((1 << MSTA) | (1 << TXAK));  /* 发送停止位 */
            int t = 0;
            while((base->I2SR & (1 << IBB)) != 0 && t < 10)  /* 等总线释放 */
            {
                delay_us(100);
                ++t;
            }
        }
        *data++ = base->I2DR;
    }
}

6. 关键点说明

  • 开漏输出 + 上拉:配置字 0x70B0 使能了 47K 片上拉,但 ODE 位(bit11)为 0,并未使能开漏;规范 I2C 应使用 0x78B0(ODE=1)。本实验速率低、走线短,靠片上拉也能正常工作。
  • IFDR 分频:I2C 时钟源通常为 66MHz IPG,通过 IFDR 分频得到标准模式 100kHz 或快速模式 400kHz。
  • I2SR 标志位:ICF(bit7)为字节传输完成标志,IBB(bit5)为总线忙标志。代码起始前等的是 ICF,发停止位后等 IBB 清零,两者别混淆。
  • I2SR[IIF]:传输完成中断标志,发送/接收完一个字节需要等待该位置位。
  • 读操作最后要发 NACK:告诉从机不再读更多字节,然后发送停止位。

7. 实验现象

初始化成功后,配合 GT9147 可以读到设备 ID,配合 LM75 可以读到温度数据(注意 LM75 没有 ID 寄存器);如果 SDA/SCL 接线反了或没上拉,通常会卡在等待 IBB 或 IIF 的循环里。

总结

问题答案
I2C 需要几根线?SCL、SDA 两根,半双工
起始位和停止位怎么产生?SCL 高时 SDA 下降沿为起始,上升沿为停止
地址字节的最低位含义?0=写,1=读(R/W 位不属于 7 位地址本身)
读寄存器为什么两次起始?先写寄存器地址,再重复起始读数据
I.MX6ULL I2C 分频寄存器?IFDR

I2C 协议掌握后,下一篇就可以驱动 LM75 温度传感器,读出真实温度值。

继续阅读

嵌入式开发面试知识手册

嵌入式开发工程师面试知识手册 适用对象:零基础起步、目标是通过嵌入式软件/驱动/BSP 方向面试的读者 编写依据:赵哲简历全部知识点 + 智能车 AGV 控制平台完整项目代码(i.MX6ULL Linux 端 + STM32F407 固件)+ Linux 驱动练习代码包 + embedded-agent 嵌入式开发智能体仓库 + 真实面试手撕代码与追问记录 使用方法:每篇按"通俗解释 → 原理细节 → 命令/API → 代码实例 → 项目结合 → 记忆口诀 → 面试问答"组织;口诀用于快速记忆,问答用于自测。第9篇是讲项目的演练场,第10篇是手撕代码实战题库。 总目录 * 第1篇 C语言与数据结构 * 1.1 编译链接全过程 * 1.2 数据类型与内存

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:Linux 驱动架构详解——编译、树内树外与运行时效果

这是 Linux 驱动小系列的总结篇。前三篇把 LED 驱动从内核模块写到 misc 设备,属于“会用”;这篇把背后的架构讲透:驱动到底怎么编译、内核树内和树外两种方式的区别、以及驱动里每个操作在系统跑起来之后到底产生了什么效果。内容整理自我的学习笔记(note.pdf)和实际工程 code/2026/20260803。 1. 驱动代码到底怎么编译的? 不管哪种方式,最终都靠内核的 kbuild 系统。核心就三个文件的关系: * Kconfig:定义 menuconfig 里的选项(如 CONFIG_MYMODULE),三态 y/m/n——y 编进内核,m 编成模块,n 不编; * .config:menuconfig 的选择结果,保存在源码顶层; * Makefile:

作者:azhezhezhe

正点原子 I.MX6ULL MINI 开发板学习笔记:misc 设备——字符设备驱动的快捷方式

上两篇分别用手动 mknod 和 class_create+device_create 注册字符设备,调用链一长串。这篇用 misc(混杂)设备 一步搞定。代码来自实际工程 code/2026/20260803/05_misc_led,已验证可用。 1. 完成什么实验? 用 misc_register 重写 LED 驱动:一步完成设备号分配、字符设备注册、/dev/myled0 节点创建,同时保留上一篇的 sysfs 属性 led_status。 2. 什么是 misc 设备? misc(混杂)设备是 Linux 给“不好归类的小字符设备”

作者:azhezhezhe