ARM嵌入式学习(十四)--- IMX6ULL的I2C通信实现

目录

一、I2C原理

1.I2C相关概念

2.推挽输出与开漏输出

3.I2C通信时序

4.I2C通信和UART通信的区别:

二、IMX6UUL的IIC外设

[1. I2Cx_IADR寄存器](#1. I2Cx_IADR寄存器)

2.I2Cx_IFDR寄存器:

注意:

[3.I2Cx_I2CR寄存器 :](#3.I2Cx_I2CR寄存器 :)

[4. I2Cx_I2SR 寄存器](#4. I2Cx_I2SR 寄存器)

5.I2Cx_I2DR

三、I2C初始化和发数据函数

1.作为主设备传输模式的I2C协议流程图(初始化+发数据):

注意:

2.I2C初始化:

注意:

3.发数据(难)

总结规律,并简化函数:

第一个改进点:

第二个改进点:

第三个改进点:

整体代码:

三、I2C读数据函数

1.重发起始信号并切换到读取数据模式:

​2.读取数据代码

​注意:

整体代码:

四、统一接口

五、主函数main.c

注意:

六、总结

遇到了以下问题:

1.I2C协议的写数据函数时:

2.I2C协议读数据函数:

3.不了解SION位的作用

4.I2DR的模式和MTX的模式搞混了:


一、I2C原理

在使用I2C之前,我们先学习什么是I2C?

1.I2C相关概念

IIC(Inter-Integrated Circuit)又称I2C,是IICBus简称,又叫集成电路总线。是飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展而来的一种同步串行半双工通信总线方式。该总线允许同时连接多个设备(芯片)。每块芯片在总线上拥有特定的地址。

上图是I2C的总线的使用场景,所有挂载在IIC总线上的设备都有两根信号线,一根是数据线SDA,另一根是时钟线SCL。这两个信号线都是双向的。

由于I2C总线是一种总线式连接方式,意味着在同样的两根线上可能连接有多个不同的设备,这种总线式连接方案通常会被设计为主从应答通信方式。也就是说,对于总线上连接的所有设备来说,有一个设备是主设备,而其他设备都是从设备。设备之间的通信都是由主设备发起的,从设备被动应答主设备。这里不难看出:

(1)从设备之间是无法直接完成通信的;

(2)总线上的从设备必须拥有一个独一无二的标识,这个标识被称为设备地址。设备地址类似一个人的名字,因为在I2C总线通信过程中,无论哪个设备发送数据,其他所有设备都能够监听得到,所以每次通信时,主设备先发送要访问的子设备地址,每个子设备都把监听到的这个地址拿来跟自己进行匹配,只有匹配成功的子设备才是真正需要操作的设备。

作为一种通信方式,IIC总线在某一时刻,总线只允许有一个设备处于发送状态,所发生的数据被总线上所有的设备所接收。IIC通信协议包含有设备地址,只有发送方携带的地址与某个接收方的地址相同时,接收方才真正执行相关的指令。

2.推挽输出与开漏输出

I2C使用的是开漏输出

IIC总线规定,设备在空闲时,两根总线都处于高电平状态 。为保证这种状态,数据线SDA和时钟线SCL都要外接上拉电阻。对于I.MX来说,这个上拉电阻也可以在引脚电器配置中设置。

这里简单介绍一下开漏输出和推挽输出的区别:

特性 推挽输出 开漏输出
内部结构 上拉管 + 下拉管 仅有下拉管
高电平驱动 主动驱动到 VDDVDD​ 需外部上拉电阻
低电平驱动 主动驱动到 GND 主动驱动到 GND
线与能力 不能并联(会短路) 支持并联(线与)
电平转换能力 无(输出电平固定为 VDDVDD​) 有(外部上拉可接不同电压)
速度 快(上升沿陡峭) 上升沿受上拉电阻限制,较慢
功耗 静态时无额外功耗(除非输出短路) 上拉电阻在低电平时会持续消耗电流
典型应用 SPI、UART、LED 驱动 I²C、SMBus、多设备共享总线、电平转换
  • 需要 强驱动、高速、波形干净 → 选 推挽输出

  • 需要 多设备共享总线、电平转换、线与逻辑 → 选 开漏输出 + 外部上拉电阻

如果实在不懂,可以理解为:

  • 推挽 像一个双掷开关,可以主动将引脚拉到高或低

  • 开漏 像一个单刀开关,只能拉低,高电平靠外部上拉实现。

3.I2C通信时序

这是完整的IIC通信时序图:

分步讲解:

(1)IIC总线上连接的若干设备中,每次通信前,发送方首先发送一个"起始"信号,其实信号就是在SCL为高电平时,SDA发送一个低电平。当其它设备接收到这个起始信号后,将进行一次"总线仲裁"。意思就是设备(除发送其实信号的那个设备以外的)都将处于聆听状态。

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

注意:

按照I2C标准,主设备发起通信时先发送一个起始信号,之后发送的第一个字节就是子设备的设备地址 。需要注意的是。设备地址本身只占7个比特,最后一个比特所代表的是之后的数据流向。0代表主发从收,通常描述为写,1表示从发主收,通常描述为读。

(3)发送器每发送一个字节(8个bit),就在时钟脉冲 9 期间释放数据线,由接收器反馈一个应答信号主机SCL拉高,读取从机SDA的电平。对于反馈有效应答位ACK的要求是:接收器在第9个时钟脉冲之前的低电平期间将数据线SDA拉低,并且确保在该时钟的高电平期间为稳定的低电平。

数据线(SDA)为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节。
数据线(SDA)为高电平时,规定为非应答位(NACK),表示接收器没有成功接收该字节。

(4)当发送方发送完最后一个bit后,需要发送一个结束标志来终止整个通信过程。当时钟线SCL 为高电平时,数据线SDA 由低电平向高电平跳变。

4.I2C通信和UART通信的区别:

特性 I²C UART
信号线数量 2 条:SDA(数据)、SCL(时钟) 通常 3 条:TX、RX、GND(地线)
电气特性 开漏输出,需外部上拉电阻 推挽输出(TTL 或 RS-232 电平)
共地要求 隐含(通过系统电源地) 必须单独连接地线
拓扑结构 多主多从总线,可挂多个设备(地址寻址) 点对点(一主一从)

二、IMX6UUL的IIC外设

I.MX6U 提供了 4 个 I2C 外设,通过这四个 I2C 外设即可完成与 I2C 从器件进行通信I.MX6U 的 I2C 支持两种模式:标准模式和快速模式,标准模式下 I2C 数据传输速率最高是 100Kbits/s,在快速模式下数据传输速率最高为 400Kbits/s(这里我们使用标准模式100k)。

介绍一下跟I2C相关的寄存器:

1. I2Cx_IADR寄存器

这是I2C 的地址寄存器, ADR(bit7:1)位有效,用来保存 I2C 从设备地址数据。作为从设备时需要将本机地址写入 IADR,并设置 I2CRIAAS(作为从机响应)等位。

本篇驱动没有这些需求,因此无需操作 IADR

2.I2Cx_IFDR寄存器:

只有 IC(bit5:0)这个位,用来设置 I2C 的波特率,I2C 的时钟源可以选择IPG_CLK_ROOT=66MHz,通过设置 IC 位既可以得到想要的 I2C 波特率。需要注意的是不像其他外设的分频设置一样可以随意设置。

手册第1464页给出了I2Cx_IFDR[bit5:0]为不同的值时的分频值:

比如现在 I2C 的时钟源为 66MHz,我们要设置I2C 的波特率为 100KHz,那么IC 就可以设置为 0X16,也就是 768 分频。

注意:

为什么不设置0x15也就是640分频呢,如果是这个分频,那么就超过标准模式下的最高频率100k了(66000000/640=103.125KHz),所以这里使用768分频

3.I2Cx_I2CR寄存器 :

(1)IEN(bit7):I2C 使能位,为 1 的时候使能 I2C,为 0 的时候关闭 I2C;

(2)IIEN(bit6):I2C 中断使能位,为 1 的时候使能 I2C 中断,为 0 的时候关闭 I2C 中断;

(3)MSTA(bit5):主从模式选择位,设置 IIC 工作在主模式还是从模式,为 1 的时候工作在主模式,为 0 的时候工作在从模式;作为主机时,启动通信前将此位置1,之后会发送一个起始位。在发送过程中向此位清零,会立即产生一个停止位。

(4)MTX(bit4):传输方向选择位,用来设置是进行发送还是接收,为 0 的时候是接收,为 1 的时候是发送;

(5)TXAK(bit3):传输应答位使能,为 0 的话发送 ACK 信号,为 1 的话发送 NO ACK 信号;

(6)RSTA(bit2):重复开始信号,为 1 的话产生一个重新开始信号。

4. I2Cx_I2SR 寄存器

(1)ICF(bit7):数据传输状态位,为 0 的时候表示数据正在传输,为 1 的时候表示数据传输完成;

(2)IAAS(bit6):当为 1 的时候表示 I2Cx_IADR 寄存器中的地址是从设备地址。为0表示I2Cx_IADR是自己的地址;

IBB(bit5):I2C 总线忙标志位,当为 0 的时候表示 I2C 总线空闲,为 1 的时候表示 I2C 总线忙;

(3)IAL(bit4):仲裁丢失位,为 1 的时候表示发生仲裁丢失。仲裁丢失在手册第1455页有说明,如果多个设备同时尝试连接总线,则其中一个成为主设备。硬件会立即将仲裁失败的设备切换到Slave Receive模式 。那么仲裁失败的设备就会产生仲裁丢失,导致此位置位。需要理解的是这种情况一定发生在发送起始位的时候,也就是说在发送完起始位之后应该判断此位是否为1;

(4)SRW(bit2):从机读写状态位,当 I2C 作为从机的时候使用,此位用来表明主机发送给从机的是读还是写命令。为 0 的时候表示主机要向从机写数据,为 1 的时候表示主机要从从机读取数据;

(5)IIF(bit1):I2C 中断挂起标志位,当为 1 的时候表示有中断挂起,此位需要软件清零;I2C 控制器在 每个字节传输结束时 自动置位 IIF(无论发送还是接收)。

(5)RXAK(bit0):应答信号标志位,无论作为主机还是从机,为 0 的时候表示接收到 ACK 应答信号,为 1 的话表示检测到 NO ACK 信号。

5.I2Cx_I2DR

这是 I2C 的数据寄存器,此寄存器只有低 8 位有效,当要发送数据的时候将要发送的数据写入到此寄存器 ,注意此时LSB代表的是数据流向,需要按照实际情况设置为1(发)或者0(读);如果要接收数据的话直接读取此寄存器即可得到接收到的数据。

发送时无论发送设备地址还是数据,都将要发生的内容写到这个寄存器中当要发送的数据写入后,I2C控制器就开始传输数据**,传输完毕后(收到ACK或NACK)就会使IIF(I2Cx_I2SR[bit1])置位**。对于接收数据来说,当从该寄存器读走一个字节后,I2C控制器就开始传输数据,直到IIF置位。因此在读取过程中,第一个读到的字节是无效的

三、I2C初始化和发数据函数

我们查看手册写的示例I2C协议,不考虑总线仲裁(即只有一个从设备)

1.作为主设备传输模式的I2C协议流程图(初始化+发数据):

注意:

框出来的不写是因为:这是判断IAL(总线仲裁)位的,本篇只用到了一个从设备,所以这里不用判断总线仲裁

我们根据这个流程图来写我们的I2C初始化和发数据代码

2.I2C初始化:

设置I2C的引脚和配置引脚电气属性,并且这里我们把I2C分频设置和I2C使能打开(依旧使用传参的方法来方便我们后续使能任意一个I2Cx):

复制代码
void i2c_init(I2C_Type *base)
{
  if (I2C1 == base)
  {
    IOMUXC_SetPinMux(IOMUXC_UART4_RX_DATA_I2C1_SDA, 1);//SION位要置为1
    IOMUXC_SetPinMux(IOMUXC_UART4_TX_DATA_I2C1_SCL, 1);
    IOMUXC_SetPinConfig(IOMUXC_UART4_RX_DATA_I2C1_SDA, 0x98b0);
    IOMUXC_SetPinConfig(IOMUXC_UART4_TX_DATA_I2C1_SCL, 0x98b0);
  }
  else if (I2C2 == base)
  {
    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, 0x98b0);
    IOMUXC_SetPinConfig(IOMUXC_UART5_TX_DATA_I2C2_SCL, 0x98b0);
  }

  base->I2CR &= ~(1 << 7);  //关闭I2C再配置分频
  base->IFDR = 0x16;   //768分频
  base->I2CR |= (1 << 7);  //使能I2C
}

注意:

(1)设置引脚功能的SION要置1,为什么 I2C 需要 SION = 1? 因为I2C 总线是双向开漏 协议,要求每个设备既能驱动低电平 (输出),又能释放总线并检测总线电平(输入),而要检测总线的电平就需要SION置为1。

如果实在不懂,可以理解为:推挽输出SION置为0,开漏输出SION要置为1

(2)引脚电气属性的PUS要置为10(调节上拉电阻的阻值来调节速率)

PUS 值 配置 典型用途
00 100kΩ 下拉 I2C 一般不使用下拉
01 47kΩ 上拉 较快速度(如 400kHz)可选
10 100kΩ 上拉 低速(100kHz)常用
11 22kΩ 上拉 较强上拉,高速(1MHz)适用

(3) 分频要在I2C使能之前

3.发数据(难)

根据总体的流程图一步一步来写发数据的函数,因为我们在初始化中已经配置好了IDFR和使能I2C,以及IADR我们用不着,所以就跳过这几步。

看着流程图一步一步代码的写法为:

总结规律,并简化函数:

写出来整个写函数后,我们发现这个函数很长而且不美观,这里我们观察整个函数的结构,把可以改进的地方列出来:

1.我们函数中的所有while循环等待的都没有超时保护,如果一直等不到就会持续循环无法跳出,所以这里把IBB等待和IIF等待封装成一个函数。

2.不难看出,每次我们发地址或者发数据之后,都会进入到我用红框框起来的地方,这一部分代码在流程图中为A(橙色部分)。我们可以把这些封装成各个函数。

3. 在判断RXAK之后都会发送STOP信号并重新开始整个函数,这里我们不如把这个发送STOP信号放到函数的最后,所有需要发送STOP信号的,直接使用goto关键字。

第一个改进点:

所有的循环等待,我们都加超时保护,并封装成函数:

复制代码
#define TIME_OUT 1
static inline int i2c_wait_bus_busy(I2C_Type *base)  // IBB idle
{
  int times = 50;
  while ((base->I2SR & (1 << 5)) && times--)
  {
    delay_us(TIME_OUT);
  };  // wait i2c irq pending

  return (times <= 0) ? -1 : 0;
}

static inline int i2c_wait_bus_idle(I2C_Type *base)  // IBB busy
{
  int times = 50;
  while ((!(base->I2SR & (1 << 5))) && times--)
  {
    delay_us(TIME_OUT);
  };  // wait i2c irq pending

  return (times <= 0) ? -1 : 0;
}

static inline int i2c_wait_irq(I2C_Type *base)  // IIF
{
  int times = 50;
  while ((!(base->I2SR & (1 << 1))) && times--)
  {
    delay_us(TIME_OUT);
  };  // wait i2c irq pending

  return (times <= 0) ? -1 : 0;
}

第二个改进点:

封装流程图橙色A部分(封装MSTA判断):

复制代码
static inline int i2c_assert_msta(I2C_Type *base)  // 判断MSTA
{
  if (!(base->I2CR & (1 << 5)))  // MSTA != 1  ?
  {
    base->I2SR &= ~(1 << 4);  // clear  IAL
    return -1;                // MSTA == 0
  }

  return 0;
}


//下面的代码即为橙色A部分
//由于要使用goto关键字,就不封装成函数了,进入橙色a后把下面这几条代码贴进去就行
  ret = i2c_assert_msta(base);
  if (-1 == ret)
    return -1;
  if ((base->I2SR & (1 << 0)))
    goto i2c_stop;

第三个改进点:

判断RXAK后如果收到NACK,就goto到我们的发送STOP代码那一块,即代码结尾(这里循环等待已经替换为写好的,带有超时保护的循环等待IBB函数):

复制代码
i2c_stop:
  base->I2CR &= ~(1 << 5);  // stop

  i2c_wait_bus_idle(I2C1);
  // while (base->I2SR & (1 << 5));  // wait IBB == 0  bus  idle

整体代码:

三个改进点写完后,我们的I2C发数据函数就比较美观了:

复制代码
int i2c_write(I2C_Type *base, uint8_t slave_addr, uint16_t reg_addr, unsigned char reg_len, const uint8_t *data,
              uint32_t len)
{
  int ret = 0;
  base->I2SR &= ~((1 << 4) | (1 << 1));  // clear  IAL IIF

  i2c_wait_bus_idle(I2C1);
  // while (base->I2SR & (1 << 5));  // wait IBB == 0  bus  idle

  base->I2CR |= (1 << 5);  // start
  base->I2CR |= (1 << 4);  // set transmit

  i2c_wait_bus_busy(I2C1);
  // while (!(base->I2SR & (1 << 5)));  // wait  buf busy IBB == 1    // do timeout

  base->I2DR = (slave_addr << 1) | (0 << 0);  // write 0

  i2c_wait_irq(I2C1);
  // while (!(base->I2SR & (1 << 1))); // wait iif

  base->I2SR &= ~(1 << 1);  // clear  iif
  
  ret = i2c_assert_msta(base);
  if (-1 == ret)
    return -1;
  if ((base->I2SR & (1 << 0)))
    goto i2c_stop;

  int i = reg_len;
  for (i = reg_len - 1; i >= 0; i--)  // write  reg_addr  to bus
  {                                   // (reg_addr >> 8) & 0xff     (reg_addr & 0xff)
    base->I2DR = (reg_addr >> (8 * i)) & 0xff;

    i2c_wait_irq(I2C1);
    // while (!(base->I2SR & (1 << 1)));

    base->I2SR &= ~(1 << 1);
    ret = i2c_assert_msta(base);
    if (-1 == ret)
      return -1;
    if ((base->I2SR & (1 << 0)))
      goto i2c_stop;
  }

  for (i = 0; i < len; i++)
  {
    base->I2DR = data[i];  // write data

    i2c_wait_irq(I2C1);
    // while (!(base->I2SR & (1 << 1)));  // wait i2c irq pending

    base->I2SR &= ~(1 << 1);
    ret = i2c_assert_msta(base);
    if (-1 == ret)
      return -1;
    if ((base->I2SR & (1 << 0)))
      goto i2c_stop;
  }

i2c_stop:
  base->I2CR &= ~(1 << 5);  // stop

  i2c_wait_bus_idle(I2C1);
  // while (base->I2SR & (1 << 5));  // wait IBB == 0  bus  idle

  return 0;
}

注意:

原先的循环等待(没加超时保护)函数我注释了,方便对比。

三、I2C读数据函数

在读数据之前,我们依然要先发(但不发数据,只发地址),然后重发起始信号后切换到读取数据模式。

查看手册的I2C读数据函数流程图示例:

1.重发起始信号并切换到读取数据模式:

2.读取数据代码

发送完设备地址后,我们便转换成接收方了, 此时需要读取I2DR寄存器一次让它产生一次数据传输 ,因此第一个字节读出的数据实际上是无效的(用于产生数据传输的),需要丢弃

由这个示例的流程图,可以写出的读数据的代码,依旧循环读取(一次循环只能读一个字节):

注意:

(1)要虚读一次I2DR寄存器让它产生数据传输,这个读出来的数据无用需要丢弃

(2)TXAK和RXAK的区别:

读数据的TXAK是主机发给从机的ACK应答,为0则默认发送完一个字节后发送应答ACK,为1则发送NACK表示结束读取。

发数据的RXAK是用于主机检测是否有应答ACK:从机发给主机的应答,为0则表示接受到ACK应答,为1则为未检测到ACK,即为NACK

(3)要先发应答或者STOP信号再读取数据(根据流程图)

整体代码:

复制代码
int i2c_read(I2C_Type *base, uint8_t slave_addr, uint16_t reg_addr, unsigned char reg_len, uint8_t *data, uint32_t len)
{
  int ret = 0;
  base->I2SR &= ~((1 << 4) | (1 << 1));  // clear  IAL IIF

  i2c_wait_bus_idle(I2C1);
  // while (base->I2SR & (1 << 5));  // wait IBB == 0  bus  idle
  // return 1;

  base->I2CR |= (1 << 4);  // set transmit
  base->I2CR |= (1 << 5);  // start

  i2c_wait_irq(I2C1);
  // while (!(base->I2SR & (1 << 5)));

  base->I2DR = (slave_addr << 1) | (0 << 0);  // write 0

  i2c_wait_irq(I2C1);
  // while (!(base->I2SR & (1 << 1)));

  base->I2SR &= ~(1 << 1);
  ret = i2c_assert_msta(base);
  if (-1 == ret)
  {
    return -2;
  }

  if ((base->I2SR & (1 << 0)))
    goto i2c_stop;

  int i = reg_len;
  for (i = reg_len - 1; i >= 0; i--)
  {  // (reg_addr >> 8) & 0xff     (reg_addr & 0xff)
    base->I2DR = (reg_addr >> (8 * i)) & 0xff;

    i2c_wait_irq(I2C1);
    // while (!(base->I2SR & (1 << 1)));

    base->I2SR &= ~(1 << 1);
    ret = i2c_assert_msta(base);
    if (-1 == ret)
      return -3;
    if ((base->I2SR & (1 << 0)))
      goto i2c_stop;
  }

  base->I2CR |= (1 << 2);  // repeat start

  base->I2DR = (slave_addr << 1) | (1 << 0);  // read 1

  i2c_wait_irq(I2C1);
  // while (!(base->I2SR & (1 << 1)));

  base->I2SR &= ~(1 << 1);
  ret = i2c_assert_msta(base);
  if (-1 == ret)
    return -4;
  if ((base->I2SR & (1 << 0)))
    goto i2c_stop;

  base->I2CR &= ~(1 << 4);  // set receive
  data[0] = base->I2DR;     // dummy read

  base->I2CR &= ~(1 << 3);  // set recv  ack
  for (i = 0; i < len; i++)
  {
    i2c_wait_irq(I2C1);
    // while (!(base->I2SR & (1 << 1)));

    base->I2SR &= ~(1 << 1);

    if (i == len - 2)
      base->I2CR |= (1 << 3);  // set  recv nck
    else if (i == len - 1)
      base->I2CR &= ~(1 << 5);  // stop

    data[i] = base->I2DR;
  }

i2c_stop:

  base->I2CR &= ~(1 << 5);  // stop

  i2c_wait_bus_idle(I2C1);
  // while ((base->I2SR & (1 << 5)));

  return 0;
}

这下收发数据都没问题了,但我们的收发函数参数非常多,我们需要统一接口

四、统一接口

写好收发函数后,我们的函数参数太多,可以定义一个结构体 ,而且可以把收发函数封装起来,直接传一个flag的参数来判断使用收还是使用发函数。这样做出来的统一接口方便我们后续使用I2C:

复制代码
typedef struct i2c_msg
{
  uint8_t slave_addr;
  uint16_t reg_addr;
  uint8_t reg_len;
  uint8_t *data;
  uint32_t len;
  uint8_t flag;  //  0  read      1   write
} i2c_msg_t;


int i2c_master_xfer(I2C_Type *base, i2c_msg_t *msg)
{
  int ret = 0;
  if (I2C_RD == msg->flag)
    ret = i2c_read(base, msg->slave_addr, msg->reg_addr, msg->reg_len, msg->data, msg->len);
  else if (I2C_WR == msg->flag)
    ret = i2c_write(base, msg->slave_addr, msg->reg_addr, msg->reg_len, msg->data, msg->len);
  else
    ret = -1;

  return ret;
}

五、主函数main.c

这里我们使用的传感器是LM75,查看手册可以看到它的各个寄存器地址:

这里我们把温度寄存器的数值读出来,并尝试写数据到T-hyst和T-os寄存器中,把值读取出来一下看看收发函数是否正常运行:

复制代码
int main(void)
{
    unsigned char data = 0;
    unsigned char lm75_addr = 0x48;
    unsigned char lm75_reg_addr = 0;
    unsigned char lm75_data[2];
    system_irq_init();
    clk_init();
    led_init();
    beep_init();
    key_irq_init(gpio1_io18_handler);
    gpt1_init();
    uart_init(UART1);

    i2c_init(I2C1);

    while(1)
    {
        printf("i2c 1  start   read\r\n");
        int ret = i2c_read(I2C1, lm75_addr, lm75_reg_addr, 1, lm75_data, 2);
        if(ret < 0)
        {
            printf("i2c_read failed ret = %d\n", ret);
            delay_ms(3000);
            continue;
        }
        printf("i2c 1  read   end  ret = %d\r\n", ret);

        int temp = ((short)((lm75_data[0] << 8) | lm75_data[1]) >> 7);
        printf("temp = %d.%d\r\n", temp / 2, ((temp % 1) == 1) ? 5 : 0);
     
        delay_ms(3000);

        i2c_msg_t msg = 
        {
            .slave_addr = lm75_addr,
            .reg_addr = 0x02,
            .reg_len = 1,
            .data = lm75_data,
            .len = sizeof(lm75_data),
            .flag = I2C_WR
        };        
        lm75_data[0] = 0x33;
        lm75_data[1] = 0x20;
        i2c_master_xfer(I2C1, &msg);
        //i2c_write(I2C1, lm75_addr, 0x02, 1, lm75_data, 2);

        delay_ms(1000);
        lm75_data[0] = 0;
        lm75_data[1] = 0;
        msg.flag = I2C_RD;
        i2c_master_xfer(I2C1, &msg);
        //i2c_read(I2C1, lm75_addr, 0x02, 1, lm75_data, 2);
        printf("T-hyst   0x%x  0x%x\r\n", lm75_data[0], lm75_data[1]);
    }

    return 0;
}

烧录到板子上成功通信并返回温度数值和我们写入的数据

注意:

(1)因为我们格式化的printf函数无法输出浮点数,所以这里的温度数值需要做一些处理

(2)代码中结构体使用用 .reg_len =1这种形式来赋值的方法只能是Linux编程

(3)注意写数据之后把之前装数据的数组清零再读进去,不然读出来无法确定是否读成功了

六、总结

只要跟着流程图示例写一遍读函数,就能稍微理解一点这些代码,不过代码看不懂没事,I2C原理一定要弄懂。

遇到了以下问题:

1.I2C协议的写数据函数时:

(1)发数据之前,要先把从机设备地址写进I2DR中,并设置模式(我没有写地址)

(2)发送STOP信号后,要循环等待IBB置为0(我没有写循环等待IBB)

2.I2C协议读数据函数:

(1)在读数据之前,要虚读一下I2DR寄存器产生数据传输(我没有虚读)

(2)TXAK和RXAK混淆了,具体看读函数代码的注意一栏,有讲各自的作用

3.不了解SION位的作用

4.I2DR的模式和MTX的模式搞混了:

I2DR是读写模式(对寄存器进行读或写),MTX是收发模式(数据流向)

相关推荐
_李小白4 小时前
【OSG学习笔记】Day 31: 渲染到纹理(RTT)
笔记·数码相机·学习
嵌入式小企鹅4 小时前
蓝牙学习系列(七):BLE GATT 数据模型详解
学习·蓝牙·ble·蓝牙协议栈·蓝牙开发·gatt
arvin_xiaoting5 小时前
OpenClaw学习总结_III_自动化系统_3:CronJobs详解
数据库·学习·自动化
少许极端5 小时前
算法奇妙屋(四十一)-贪心算法学习之路 8
学习·算法·贪心算法
Freak嵌入式6 小时前
ESP32 实现在线动态安装库和自动依赖安装-使用uPyPI包管理平台
arm开发·ide·嵌入式·micropython·电子·upypi
.普通人6 小时前
Arm_Cortex-M3权威指南
arm开发
arvin_xiaoting6 小时前
OpenClaw学习总结_III_自动化系统_2:Webhooks详解
运维·学习·自动化
不早睡不改名@7 小时前
Netty源码分析---Reactor线程模型深度解析(一)
java·笔记·学习·netty
祁白_7 小时前
Bugku:备份是一个好习惯
笔记·学习·web安全·ctf