☰
【ARM 裸机开发 (IMX6ULL-mini)】I2C协议详解
2026/9/26 11:10:51 网站建设 项目流程

文章目录

    • 前言
    • 一、串口格式化函数移植
    • 二、i2c协议的概念与特点
    • 三、IIC的时序
    • 四、EEPROM
      • 4.1 EEPROM概念
      • 4.2 AT24C02
      • 4.3 AT24C02的I2C通信
    • 五、IMX6ULL上的I2C
      • 5.1 IMX6ULL的I2C原理框图
      • 5.2 相关寄存器
        • IADR
        • IFDR
        • I2CR
        • I2SR
        • I2DR
    • 六、I2C程序C语言编写
      • 6.1 程序流程图
      • 6.2 I2C初始化
      • 6.3 I2C写时序
      • 6.4 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初始化

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

// I2CR#defineIEN(7)#defineMSTA(5)#defineMTX(4)#defineTXAK(3)#defineRSTA(2)// I2SR#defineICF(7)#defineIBB(5)#defineIAL(4)#defineIIF(1)
voidi2c1_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写时序

voidi2c_write(I2C_Type*base,unsignedchardev_address,unsignedcharreg_address,constunsignedchar*data,intlen){// 通信前 仲裁丢失标志位清零 + 中断标记位清零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// 等待从机ACKwhile((base->I2SR&(1<<IIF))==0);// 中断标记位清零base->I2SR&=~(1<<IIF);// 发送寄存器地址base->I2DR=reg_address;// 等待从机ACKwhile((base->I2SR&(1<<IIF))==0);while(len--){// 中断标记位清零base->I2SR&=~(1<<IIF);// 发送数据base->I2DR=*data++;// 等待从机ACKwhile((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读时序

voidi2c_read(I2C_Type*base,unsignedchardev_address,unsignedcharreg_address,unsignedchar*data,intlen){// 通信前 仲裁丢失标志位清零 + 中断标记位清零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;// 等待从机ACKwhile((base->I2SR&(1<<IIF))==0);// 中断标记位清零base->I2SR&=~(1<<IIF);// 发送寄存器地址base->I2DR=reg_address;// 等待从机ACKwhile((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){// 代表收到一个字节后,后续回复NACKbase->I2CR|=(1<<TXAK);}// 假读*data=base->I2DR;// 读取len个字节数据while(len--!=0){// 代表收到一个字节数据,并且主机回复了ACKwhile((base->I2SR&(1<<IIF))==0);// 中断标记位清零base->I2SR&=~(1<<IIF);// 代表数据全部接收完if(len==0){// 发送停止信号base->I2CR&=~(1<<MSTA);// ACKbase->I2CR&=~(1<<TXAK);// 等待总线空闲while((base->I2SR&(1<<IBB))!=0){delay_us(100);}}elseif(len==1){// NACKbase->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,通信结束。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询