【ARM 裸机开发 (IMX6ULL-mini)】I2C协议详解

文章目录

前言

在上篇文章【ARM 裸机开发 (IMX6ULL-mini)】EPIT/GPT 定时器、 UART 串口驱动中,介绍了定时器与串口,在最后编写了一个串口发送字符的函数。但是要实现字符串、数字等各种数据格式的发送,还是有一定工作量的。这时候肯定会想到C语言中的printf与scanf函数,这两个函数不就可以发送各种数据格式的数据吗?因此,在这篇文章中,我将介绍如何在ARM芯片代码中移植串口格式化函数 ,除此之外,这篇文章还将介绍i2c协议 的概念与IMX6ULL开发板 和EEPROM(电可擦除只读存储器)、LM75A温度传感器 之间的I2C通信。

一、串口格式化函数移植

移植的基本原理很简单,就是把printf和scanf函数映射到UART1上。stdio 里面的文件其实是从 uboot 里面移植过来的。uboot在后面的文章中也会讲到。

串口格式化函数移植主要有以下步骤:

  1. 由于增加了新的源码和头文件,所以Makefile也需要修改。

    • 增加源程序路径和头文件路径
      因为我们将串口格式化函数放在了一个新的文件夹中,所以还得添加路径,为编译器提供方向。
    • 添加链接目录
      因为串口格式化函数的编译需要用到GNU编译工具中的一些库,所以需要将其链接起来。
  2. 需要在uart.c中添加一个函数:void raise(int n);函数体为空即可,否则会报错

    因为printf函数使用出现错误时需要raise这个函数返回错误信息,但是我移植的库中并没有,因此需要在uart.c中写一个空raise来骗一下编译器。

完成上述操作后,我们就可以通过串口格式化函数来进行串口数据发送与接收了。

二、i2c协议的概念与特点

IIC(Inter-Integrated Circuit)又称I2C,是是IICBus简称,所以中文应该叫集成电路总线。

上面是IIC的使用场景,在IIC总线上的设备都有两根信号线,一根是数据线SDA,另一根是时钟线SCL。这两个信号线都是双向的。IIC总线在某一时刻,总线只允许有一个设备处于发送状态,所发生的数据被总线上所有的设备所接收。

所以IIC协议是一个通信方式为半双工、串行的通信协议。

因为 IIC总线规定,设备在空闲时,两根总线都处于高电平状态。所以数据线SDA和时钟线SCL都要外接上拉电阻。也就是使其具有线与特性,将引脚电平的控制交给对方。

除此之外,IIC通信还遵循高位先行的特点。

三、IIC的时序

IIC总线进行数据传送时,时钟线(SCL)上的信号为高电平期间,数据线(SDA)上的数据必须保持稳定。只有在时钟线(SCL)上的信号为低电平期间,数据线(SCL)上的高电平或低电平状态才允许变化。同时,SCL信号由数据启动发送的设备提供。输出到数据线(SDA)上的每个字节必须是8位。数据传送时,先传送最高位(MSB),后传送最低位(LSB)。以传输0xAC为例,时序图如下所示:

起始信号 :SCL时钟信号线为高电平时,SDA数据信号线由高变低出现下降沿。

数据传输 :

  1. SCL时钟信号线为高电平时,SDA数据信号线电平保持稳定,此时接收方采样数据

  2. SCL时钟信号线为低电平时,SDA数据信号线电平可以改变,此时发送方发送数据

应答信号 :第9个时钟周期,接收方需要向发送方回复应答信号(ACK低电平NACK高电平,至于应答信号是ACK还是NACK在后面解释IIC相关寄存器会提到)。

结束信号:SCL时钟信号线为高电平时,SDA数据信号线由低变高出现上升沿。

I2C协议规定,在起始信号后的第一个字节 是发送设备地址(7位)+数据流向位(1位) ,数据流向位即是主机与从机之间数据流向的标志。

数据流向位0:主机发送给从机1;从机发送给主机。

最大可挂从机数:27-1 = 127

由于0 号地址被协议预留为广播地址,不能给普通从设备,所以最大可挂从机数为127。

四、EEPROM

4.1 EEPROM概念

Electrically Erasable Programmable Read-Only Memory,电可擦除可编程只读存储器。

非易失存储器:断电数据不会丢,I2C 总线最经典的从设备(AT24Cxx 系列)。这篇文章中我们将使用的芯片型号为AT24C02。

I2C通信的常见速率:100KHZ、400KHZ、1.xM、3.xM

4.2 AT24C02

AT24C02的芯片操作手册中写到:

AT24Cxx 系列主要为下面三个型号:

AT24C02的特点 :

AT2402芯片引脚示意图:

4.3 AT24C02的I2C通信

在前面提到了I2C规定,在起始信号后的第一个字节发送的是设备地址与数据流向位,那么我们怎么知道这个地址呢,AT24C02的芯片操作手册

给出这样一个图:

我使用的AT24C02直接是一个模块,A0-A3默认接地,因此,这个AT24C02 EEPROM的设备地址就为0x50。

有了从机地址,我们还需要知道IMX6ULL与AT24C02的具体是怎么通信的。AT24C02的芯片操作手册也给出了时序图:

写时序 :

读时序 :

根据手册给的时序图,我们可以梳理出:

I2C写时序 :

  1. 主机发送起始信号+设备地址+写位, 主机等待从机回复ACK应答

  2. 主机向从机发送寄存器地址,等待ACK应答

  3. 主机向从机发送数据,等待从机ACK应答

  4. 主机发送停止信号

I2C读时序 :

  1. 主机发送起始信号+设备地址+写位,等待从机ACK应答

  2. 主机发送寄存器地址,等待从机ACK应答

  3. 主机重发起始信号+设备地址+读位,等待从机ACK应答

  4. 主机接受从机数据,主机回复ACK应答

  5. 当主机接受完最后一个字节数据,向从机回复NO ACK

  6. 主机发送停止信号

五、IMX6ULL上的I2C

5.1 IMX6ULL的I2C原理框图

5.2 相关寄存器

在IMX6ULL的参考手册中,我们可以得到这五个寄存器:

IADR

对于第一个寄存器IADR,这是IMX6ULL作为从机时才需要配置,但大多数情况下,IMX6ULL都是作为主机使用,因此,我们暂时不关注它了。

IFDR

I2C Frequency Divider Register,分频寄存器。

IC :I2C 时钟预分频编码,用来选择分频系数 ,对IPG_CLK_ROOT(I2C 模块输入根时钟)分频,产生 I2C 的 SCL 串行位时钟。

我们若要向这个寄存器中写入值来分频,就需要使用下面这个I2C_IFDR 分频表 :

I2CR

控制寄存器。

I2SR

状态寄存器。

I2DR

数据寄存器:存放数据。

从IMX6ULL原理图上可以知道,I2C1引出的引脚为42(SDA)、43(SCL)。

六、I2C程序C语言编写

6.1 程序流程图

由于I2C1所用的引脚是复用的,因此我们还需要对其进行引脚的复用和电气配置。结合上面的内容,我们可以得到这样一张流程图:

6.2 I2C初始化

由于标志位众多,我们直接将其定义为宏,提高代码的可读性:

c 复制代码
// I2CR
#define  IEN    (7)
#define  MSTA   (5)
#define  MTX    (4)
#define  TXAK   (3)
#define  RSTA   (2)

// I2SR
#define  ICF    (7)
#define  IBB    (5)
#define  IAL    (4)
#define  IIF    (1)
c 复制代码
void i2c1_init(void)
{
    // 配置引脚复用功能
    IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL, 1);
    IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA, 1);

    // 配置引脚电气属性
    IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL, 0xF0B0);
    IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA, 0xF0B0);

    I2C1->I2CR &= ~(1 << 7);    // I2C1失能
    I2C1->IFDR = 0x15;          // 640分频
    I2C1->I2CR |= (1 << 7);     // I2C1使能
}
  1. IMX6ULL 引脚是复用的,UART4_TX 可以复用成 I2C1_SCL;UART4_RX 可以复用成 I2C1_SDA。
  2. 开启软件输入路径(SDA 是双向,必须打开),I2C 的 SDA 需要读取外部电平,这个参数必须置 1。
  3. IPG_CLK_ROOT = 66MHz,分频640,I2C 标准模式(100kHz),工程小幅偏差。

6.3 I2C写时序

c 复制代码
void i2c_write(I2C_Type *base, unsigned char dev_address, unsigned char reg_address, const unsigned char *data, int len)
{
    // 通信前 仲裁丢失标志位清零 + 中断标记位清零
    base->I2SR &= ~((1 << IIF) | (1 << IAL));
    // 通信前,等待数据传输完毕
    while ((base->I2SR & (1 << ICF)) == 0);
    // 发送起始信号
    base->I2CR |= (1 << MSTA);
    // 主机模式:发送模式
    base->I2CR |= (1 << MTX);
    // 后续检测从机ACK信号
    base->I2CR &= ~(1 << TXAK);
    // 中断标记位清零
    base->I2SR &= ~(1 << IIF);
    // 发送设备地址 + 写位
    base->I2DR = dev_address << 1;   // 101 0000 0
    // 等待从机ACK
    while ((base->I2SR & (1 << IIF)) == 0);
    // 中断标记位清零
    base->I2SR &= ~(1 << IIF);
    // 发送寄存器地址
    base->I2DR = reg_address;
    // 等待从机ACK
    while ((base->I2SR & (1 << IIF)) == 0);

    while (len--)
    {
        // 中断标记位清零
        base->I2SR &= ~(1 << IIF);
        // 发送数据
        base->I2DR = *data++;
        // 等待从机ACK
        while ((base->I2SR & (1 << IIF)) == 0);
    }

    // 发送停止信号
    base->I2CR &= ~(1 << MSTA);

    // 等待总线空闲
    while ((base->I2SR & (1 << IBB)) != 0)
    {
        delay_us(100);
    }
}
  1. 初始化标志,等待上一次传输结束;
    • 清除旧中断、仲裁丢失标志;
    • 等待 I2C 模块上次字节传输完成,避免前后两次通信冲突。
  2. 配置为主机发送模式,准备发起起始位;
  3. 发送从机地址(写);
  4. 发送 EEPROM 内部存储地址(reg_address);
  5. 循环写入多字节数据。

6.4 I2C读时序

c 复制代码
void i2c_read(I2C_Type *base, unsigned char dev_address, unsigned char reg_address, unsigned char *data, int len)
{
   // 通信前 仲裁丢失标志位清零 + 中断标记位清零
    base->I2SR &= ~((1 << IIF) | (1 << IAL));
    // 通信前,等待数据传输完毕
    while ((base->I2SR & (1 << ICF)) == 0);
    // 发送起始信号
    base->I2CR |= (1 << MSTA);
    // 主机模式:发送模式
    base->I2CR |= (1 << MTX);
    // 后续检测从机ACK信号
    base->I2CR &= ~(1 << TXAK);
    // 中断标记位清零
    base->I2SR &= ~(1 << IIF);
    // 发送设备地址 + 写位
    base->I2DR = dev_address << 1;
    // 等待从机ACK
    while ((base->I2SR & (1 << IIF)) == 0);
    // 中断标记位清零
    base->I2SR &= ~(1 << IIF);
    // 发送寄存器地址
    base->I2DR = reg_address;
    // 等待从机ACK
    while ((base->I2SR & (1 << IIF)) == 0);

    // 重发起始信号
    base->I2CR |= (1 << RSTA);
    // 中断标记位清零
    base->I2SR &= ~(1 << IIF);
    // 重发设备地址 + 读位
    base->I2DR = dev_address << 1 | 1;
    // 等待从机回复ACK信号
    while ((base->I2SR & (1 << IIF)) == 0);
    // 主机模式:接收模式
    base->I2CR &= ~(1 << MTX);
    // 中断标记位清零
    base->I2SR &= ~(1 << IIF);

    // 代表只需要读取一个字节
    if (len == 1)
    {
        // 代表收到一个字节后,后续回复NACK
        base->I2CR |= (1 << TXAK);
    }

    // 假读
    *data = base->I2DR;

    // 读取len个字节数据
    while (len-- != 0)
    {   
        // 代表收到一个字节数据,并且主机回复了ACK
        while ((base->I2SR & (1 << IIF)) == 0);
        // 中断标记位清零
        base->I2SR &= ~(1 << IIF);

        // 代表数据全部接收完
        if (len == 0)
        {
            // 发送停止信号
            base->I2CR &= ~(1 << MSTA);
            // ACK
            base->I2CR &= ~(1 << TXAK);
            // 等待总线空闲
            while ((base->I2SR & (1 << IBB)) != 0)
            {
                delay_us(100);
             }       
        }
        else if (len == 1)
        {
            // NACK
            base->I2CR |= (1 << TXAK);
        }

        *data++ = base->I2DR;
    }
}
  1. 重复起始位 RSTA(随机读核心)

    I2C 随机读流程:先要发送从机写地址 + 内部寄存器地址 ,告诉 EEPROM 要读取的存储位置;

    不发送停止位,直接发送 RSTA 重复起始,再发送从机读地址,切换方向,从机开始往主机发数据。

    如果不用 RSTA,先发 P 再 S,就是普通读,会丢失寄存器地址信息。

  2. 假读 dummy read

    IMX6ULL 硬件 I2C 接收机制:

    写入读地址之后,硬件就会自动接收第一个字节,存入移位寄存器,必须先读一次 I2DR 把这个预接收的字节 "取走",这一次读是 dummy 假读,不使用这个值,后面循环里才会依次读到有效数据。

    很多人写 I2C 读函数踩坑,忘记 dummy read,读到的数据错位!

  3. ACK / NACK 控制规则

    I2C 读的时候,主机负责发 ACK/NACK:

    • 还剩多个字节:主机回复ACK(TXAK=0),通知从机继续发下一字节
    • 只剩最后 1 个字节:提前设置TXAK=1,收到最后一字节后主机回复NACK,通知从机不再继续传输
    • NACK 之后主机发送停止位 P,通信结束。
相关推荐
番茄灭世神13 小时前
PN学堂GD32教程第11篇——I2C与EEPROM
c语言·stm32·单片机·嵌入式·gd32
haliu15 小时前
【FHE 同态加密】我们如何实现同态加密推理(十六):未完成清单与已知边界(同态加密推理的精度、性能与安全)
人工智能·嵌入式·c·fhe·推理引擎·c11·边缘推理·同态加密推理
blueSatchel1 天前
UVC驱动源码研究
linux·嵌入式
今年下半年2 天前
【运维】windows虚拟机安装arm(aarch64)服务器
运维·windows·arm·虚拟机·aarch64
haliu2 天前
【FHE 同态加密】我们如何实现同态加密推理(十五):实测账本——一跳 1.8 小时,钱花在哪(同态加密推理性能剖析)
人工智能·嵌入式·c·fhe·推理引擎·c11·边缘推理·同态加密推理
额额额对了2 天前
SPI通信
linux·c语言·汇编·单片机·嵌入式硬件·arm
一直C2 天前
【ARM嵌入式开发】LCD液晶屏显示原理、色彩模式与显存地址映射详解
lcd·嵌入式·arm·寄存器·cortex-a7
Hespethorn2 天前
洪泛、路由、网络栈:LoRa 自组网三条路线的设计取舍与量化对比
物联网·嵌入式
用户0510122572963 天前
RGA(二)——RK3588 的核拓扑与能力差异
linux·嵌入式