GD32实战10__I2C

知识点

  1. 掌握I2C总线
  2. 如何看时序图
  3. 如何使用I2C接口的器件,例如AT24C02

原理

​ I2C/IIC(集成电路总线)是philips推出的一种串行总线。

主要特性

  1. 只有两根线,串行数据线SDA,串行时钟线SCL
  2. 总线上的所有器件必须都有唯一的地址
  3. 多主机总线,可同时支持多个slave和多个master,即支持冲突检测和仲裁
  4. 8位双向数据传输,速率标准模式下最高100kbit/s,快速模式下最高400kbit/s,高速模式下最高3.4Mbit/s

硬件电路要求

如图,

  1. 由于设备之间是线与到一起的,所以设备的GPIO必须是开漏输出,不能是推挽输出
  2. 由于GPIO是开漏输出,所以必须接上拉电阻,因此SDA和SCL默认是高电平

GD32实战10__I2C_第1张图片

协议要求

可以这么理解,

  1. I2C上的设备要进行通信,必然需要收发两方,即发送器和接收器,
  2. 总线需要初始化,需要有设备产生时钟信号,总线上的设备总要知道是谁正在访问谁,即主机和从机,主机完成总线初始化,产生时钟信号,并向从机发起寻址访问
  3. 既然各方角色已经明确了,那么
    1. 当主机要访问从机时,总要告诉从机要开始了,即起始调节
    2. 当从机收到后,
      1. 可以告诉主机自己收到了,让主机继续发生后面的数据,即应答信号
      2. 也可以告诉主机自己不想搭理主机了,让主机停止发生,即非应答信号
    3. 当主机判断需要停止该次传输时,应该告诉从机通信结束,即停止调节

具体解释如下:

1. 数据格式

如图,

  1. SDA上的数据每个字节必须是8位,且是按bit传输的,高位先传,低位后传
  2. 前8个bit是数据bit位,最后一个bit是应答或非应答信号

GD32实战10__I2C_第2张图片

2. 有效数据位识别

如图,

1. 当SCL为低电平时,SDA的状态是允许切换的,即发送的数据要在时钟是低电平时输出
2. 当SCL是高电平时,SDA的状态是不能变化的,即需要读取的数据要在时钟是高电平时读走

GD32实战10__I2C_第3张图片

3. 起始条件

​ 如图,当SCL是高电平的时候,SDA由高电平变化到低电平

GD32实战10__I2C_第4张图片

4. 应答信号

如图,

GD32实战10__I2C_第5张图片

5. 非应答信号

如图,

GD32实战10__I2C_第6张图片

6. 停止条件

​ 如图,当SCL是高电平时,SDA由低电平变化到高电平

GD32实战10__I2C_第7张图片

通过I2C总线读写EEPROM

硬件设计

如图,我们通过I2C总线完成对EEPROM AT24C02的读写操作,A0,A1,A2都接地,即该芯片物理地址是0

GD32实战10__I2C_第8张图片

AT24C02C手册导读

要实现对AT24C02C读写操作,我们需要了解AT24C02C的基本功能和要求,这些可以从Datasheet中找到,例如

1. 写保护和地址要求

GD32实战10__I2C_第9张图片

2. 上电复位要求

如下图,芯片复位时长在130到270ms之间,稳妥起见,上电后至少应该在270ms后在对该芯片进行操作

GD32实战10__I2C_第10张图片

3. I2C总线时序要求

如下图,SCL和SDA的每个电平的时间都做了详细的说明,编码时需要严格按照该时间要求编码。

GD32实战10__I2C_第11张图片

GD32实战10__I2C_第12张图片

4. 读操作的基本时序
  1. 下面的图中,都只画了SDA的变化,没画SCL,原因是从I2C协议规定了SDA和SCL的关系,因此只要知道SDA怎么变化的自然就知道SCL应该如何变化,所以只画SDA足够了
  2. 度操作支持下面三种,在手册里都有详细的介绍

GD32实战10__I2C_第13张图片在这里插入图片描述GD32实战10__I2C_第14张图片GD32实战10__I2C_第15张图片

5. 写操作的基本时序

写操作支持两种,手册里也有详细介绍

GD32实战10__I2C_第16张图片

软件设计

  1. 为了更深入的理解I2C总线协议,此例中没用硬件I2C控制器,而是用GPIO口模拟的,原理明白了,硬件方式就更加简单了

  2. 从前面的介绍可以看出,I2C总线是通用的,不仅AT24C02C可以使用,其它的芯片也可以使用,且协议规范是一样的,因此分成三部分,这样三部分的代码在后续的编码中可以做到通用

  3. 下面三部分的编码与上面原理和手册中的时序是完全匹配的,可以对照阅读

1. I2C总线驱动
  1. 我把电平切换和延时封装成一个宏,这样函数中只要把精力都投入到电平变化就可以了
  2. 延时时间固定5us,这样代码会简单很多,原因如“I2C总线时序要求”一节中,时序基本都要求了最小时间间隔5us足以,有两项有最大时间要求,却不影响我们编码。
#define I2C_Set1(i2c) GPIO_SetBits(i2c);I2C_Delay(5);
#define I2C_Set0(i2c) GPIO_ResetBits(i2c);I2C_Delay(5);
#define I2C_Get(i2c) GPIO_ReadInputBit(i2c);

VOID DRV_I2C_Start(VOID)
{
    I2C_SetOutput(I2C_SDA);
    I2C_Set1(I2C_SDA);
    I2C_Set1(I2C_SCL);
    I2C_Set0(I2C_SDA);
    I2C_Set0(I2C_SCL);
}

VOID DRV_I2C_Stop(VOID)
{
    I2C_SetOutput(I2C_SDA);
    I2C_Set0(I2C_SDA);
    I2C_Set1(I2C_SCL);
    I2C_Set1(I2C_SDA);
}


U32 DRV_I2C_WriteByte(IN U8 data)
{
    U8 i = 0;
    U8 byte = data;
    U8 sda = 0;
    
    I2C_SetOutput(I2C_SDA);
        
    for (i = 0; i < 8; i++)
    {
        I2C_Set0(I2C_SCL);
        if (byte & 0x80)
        {
            I2C_Set1(I2C_SDA);
        }
        else
        {
            I2C_Set0(I2C_SDA);
        }
        I2C_Set1(I2C_SCL);
        byte <<= 1;
    }
    
    I2C_Set0(I2C_SCL);
    I2C_SetInput(I2C_SDA);
    I2C_Set1(I2C_SCL);

    sda = I2C_Get(I2C_SDA);
    if (sda)
    {
        I2C_Set0(I2C_SCL);
        I2C_SetOutput(I2C_SDA);
        return OS_ERROR;
    }
    
    I2C_Set0(I2C_SCL);
    I2C_SetOutput(I2C_SDA);
    I2C_Set1(I2C_SDA);
    
    return OS_OK;
}

U32 DRV_I2C_ReadByte(OUT U8 *byte)
{
    U8 i = 0;
    U8 bit = 0;
    U8 sda = 0;

    I2C_SetInput(I2C_SDA);
    
    for (i = 0; i < 8; i++)
    {
        I2C_Set1(I2C_SCL);
        sda = I2C_Get(I2C_SDA);
        if (sda)
        {
            bit |= 0x1;
        }
        I2C_Set0(I2C_SCL);
        if (i != 7)
        {
            bit <<= 1;
        }
    }
    *byte = bit;
    return OS_OK;
}

VOID DRV_I2C_NoAck(VOID)
{
    I2C_Set0(I2C_SCL);
    I2C_SetOutput(I2C_SDA);
    I2C_Set1(I2C_SDA);
    I2C_Set1(I2C_SCL);
    I2C_Set0(I2C_SCL);
}

VOID DRV_I2C_Ack(VOID)
{
    I2C_Set0(I2C_SCL);
    I2C_SetOutput(I2C_SDA);
    I2C_Set0(I2C_SDA);
    I2C_Set1(I2C_SCL);
    I2C_Set0(I2C_SCL);
}

VOID DRV_I2C_Init(VOID)
{
    GPIO_InitPara GPIO_InitStructure;

    RCC_APB2PeriphClock_Enable(RCC_APB2PERIPH_GPIOB,ENABLE);

    GPIO_InitStructure.GPIO_Pin =  GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_MODE_OUT_OD;
    GPIO_InitStructure.GPIO_Speed = GPIO_SPEED_50MHZ;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    GPIO_SetBits(GPIOB, GPIO_PIN_6);
    GPIO_SetBits(GPIOB, GPIO_PIN_7);
}
2. AT24C02C驱动

下面的函数接口与手册中的读写接口一一对应,其中立即地址读没有做,因为不常用。

/* 字节写 */
VOID DRV_AT24C02C_WriteByte(IN U8 slaveAddr, IN U8 byteAddr, IN U8 data)
{
    DRV_I2C_Start();
    DRV_I2C_WriteByte(slaveAddr);
    DRV_I2C_WriteByte(byteAddr);
    DRV_I2C_WriteByte(data);
    DRV_I2C_Stop();
}

/* 页写 */
VOID DRV_AT24C02C_WritePage(IN U8 slaveAddr, IN U8 byteAddr, IN U8 data[], IN U8 len)
{
    U8 i = 0; 
    
    DRV_I2C_Start();
    DRV_I2C_WriteByte(slaveAddr);
    DRV_I2C_WriteByte(byteAddr);
    for (i = 0; i < len; i++)
    {
        DRV_I2C_WriteByte(data[i]);
    }
    DRV_I2C_Stop();
}

/* 选择地址读 */
VOID DRV_AT24C02C_ReadByte(IN U8 slaveAddr, IN U8 byteAddr, OUT U8 *data)
{
    U8 tmp = 0; 

    DRV_I2C_Start();
    DRV_I2C_WriteByte(slaveAddr);
    DRV_I2C_WriteByte(byteAddr);
    DRV_I2C_Start();
    DRV_I2C_WriteByte(slaveAddr+1);
    DRV_I2C_ReadByte(&tmp);
    DRV_I2C_NoAck();    
    DRV_I2C_Stop(); 
    *data = tmp;
}

/* 连续读 */
VOID DRV_AT24C02C_ReadPage(IN U8 slaveAddr, IN U8 byteAddr, OUT U8 data[], IN U8 len)
{
    U8 tmp = 0; 
    U8 i = 0;

    DRV_I2C_Start();
    DRV_I2C_WriteByte(slaveAddr);
    DRV_I2C_WriteByte(byteAddr);
    DRV_I2C_Start();
    DRV_I2C_WriteByte(slaveAddr+1);
    for (i = 0; i < len-1; i++)
    {
        DRV_I2C_ReadByte(&tmp);
        DRV_I2C_Ack();
        data[i] = tmp;
    }
    
    DRV_I2C_ReadByte(&tmp);
    DRV_I2C_NoAck();    
    data[i] = tmp;
    DRV_I2C_Stop();    
}

VOID DRV_AT24C02C_Init(VOID)
{
    DRV_I2C_Init();
}

3. 功能测试举例

下面的代码,只是为了举例说明如何使用上述接口而已,其中0xA0的含义如下图,其中R/W位接口内部有处理,此处统一填了0。

在这里插入图片描述

#define I2C_AT24C02C_ADDR 0xA0

VOID APP_I2C_Test(VOID)
{
    U8 len = 255;
    U8 databufIn[5] = {0};
    U8 databufOut[255] = {0};
    U8 dataOut = 0;
    U8 byteAddr = 0x00;

    DRV_AT24C02C_Init();
    
    dataOut = 0;
    DRV_AT24C02C_ReadByte(I2C_AT24C02C_ADDR, byteAddr, &dataOut);
    APP_DEBUG("read=0x%x", dataOut);
    DRV_AT24C02C_WriteByte(I2C_AT24C02C_ADDR, byteAddr, 0x11);
    APP_Delay(10);
    DRV_AT24C02C_ReadByte(I2C_AT24C02C_ADDR, byteAddr, &dataOut);
    APP_DEBUG("read=0x%x", dataOut);
    
    DRV_AT24C02C_WriteByte(I2C_AT24C02C_ADDR, byteAddr, 0x2);
    APP_Delay(10);
    DRV_AT24C02C_ReadByte(I2C_AT24C02C_ADDR, byteAddr, &dataOut);
    APP_DEBUG("read=0x%x", dataOut);
    
    DRV_AT24C02C_WriteByte(I2C_AT24C02C_ADDR, byteAddr, 0xff);
    APP_Delay(10);
    DRV_AT24C02C_ReadByte(I2C_AT24C02C_ADDR, byteAddr, &dataOut);
    APP_DEBUG("read=0x%x", dataOut);

    DRV_AT24C02C_ReadPage(I2C_AT24C02C_ADDR, byteAddr, databufOut, len);
    I2C_Dump(databufOut, len);
    DRV_AT24C02C_WritePage(I2C_AT24C02C_ADDR, byteAddr, databufIn, 5);
    APP_Delay(10);
    DRV_AT24C02C_ReadPage(I2C_AT24C02C_ADDR, byteAddr, databufOut, len);
    I2C_Dump(databufOut, len);
    
    while(1);
}

代码路径

https://github.com/YaFood/GD32F103/tree/master/TestI2C

你可能感兴趣的:(ARM)