# I2C通讯

### 原理

SCL,SDA挂载上拉电阻，保持空闲状态，有最大设备数限制，有传输速率分别

![image-20230408171531425](https://s2.loli.net/2023/04/08/KIn8VUsoMRCNQtk.png)

#### 软件层面

空闲状态:两个都处于高电平

应答信号：主机从机都可以发送，告诉发送者我收到数据了

停止信号：使通讯状态保持空闲

有效性：时钟线处于高电平期间，数据才有效

#### 信号分析

起始信号：时钟线处于高电平期间，数据线SDA从高电平往低电平跳变

停止信号：时钟线处于高电平期间，数据线从低电平往高电平跳变

应答信号：在发送8字节数据后，时钟线SCL处于第九个高电平期间，SDA检测到高电平就是NACK（默认上拉电阻下就是高定平），SDA检测到低电平就是ACK（从机若应答，发送低电平，拉低SDA电平）

应答完再停止

#### 代码

起始信号

```
void iic_start(void)
{
    IIC_SDA(1); 	//把sda拉高，后面才能拉低发出起始信号
    IIC_SCL(1);		//先把scl拉高，确保信号有效
    iic_delay();	//延迟时间与设备有关
    IIC_SDA(0);     /* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */
    iic_delay();
    IIC_SCL(0);     /* 钳住I2C总线，准备发送或接收数据，在发送时再拉高 */
    iic_delay();
}
```

停止信号

```c
void iic_stop(void)
{
    IIC_SDA(0);     /* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
    iic_delay();
    IIC_SCL(1);		//SCL拉高确保数据有效性
    iic_delay();
    IIC_SDA(1);     /*SDA从低到高，实现结束信号的发送 发送I2C总线结束信号 */
    iic_delay();
}
```

检测应答信号

```c
uint8_t iic_wait_ack(void)
{
    uint8_t waittime = 0;
    uint8_t rack = 0;

    IIC_SDA(1);     /* 主机释放SDA线(此时外部器件可以拉低SDA线) */
    iic_delay();
    IIC_SCL(1);     /* SCL=1, 此时从机可以返回ACK */
    iic_delay();

    while (IIC_READ_SDA)    /* 高电平状态，等待应答，若变为低电平，完成应答 */
    {
        waittime++;

        if (waittime > 250) //超时
        {
            iic_stop();
            rack = 1;	//标志位置1，表示失败
            break;
        }
    }

    IIC_SCL(0);     /* SCL=0, 结束ACK检查 */
    iic_delay();
    return rack;	//返回标志位，若能直接从while出来，而不是超时break，则为0，代表应答成功
}
```

发送应答信号

```c
void iic_ack(void)
{
    IIC_SDA(0);     /* SCL 0 -> 1  时 SDA = 0,表示应答 */
    iic_delay();
    IIC_SCL(1);     /* 拉高数据有效，  产生一个时钟 */
    iic_delay();
    IIC_SCL(0);
    iic_delay();
    IIC_SDA(1);     /* 主机释放SDA线 */
    iic_delay();
}
```

发送非应答

```c
void iic_nack(void)
{
    IIC_SDA(1);     /* SCL 0 -> 1  时 SDA = 1,表示不应答 */
    iic_delay();
    IIC_SCL(1);     /* 产生一个时钟 */
    iic_delay();
    IIC_SCL(0);
    iic_delay();
}
```

发送一字节数据

```
void iic_send_byte(uint8_t data)
{
    uint8_t t;
    
    for (t = 0; t < 8; t++)
    {
        IIC_SDA((data & 0x80) >> 7);    /* 高位先发送 */
        iic_delay();
        IIC_SCL(1);		//SCL高，数据有效
        iic_delay();
        IIC_SCL(0);
        data <<= 1;     /* 左移1位,把此次发送的最高位丢掉，用于下一次发送 */
    }
    IIC_SDA(1);         /* 发送完成, 主机释放SDA线，恢复默认的 */
}
```

数据接收

```c
uint8_t iic_read_byte(uint8_t ack)
{
    uint8_t i, receive = 0;

    for (i = 0; i < 8; i++ )    /* 接收1个字节数据 */
    {
        receive <<= 1;  /* 高位先输出,所以先收到的数据位要左移 */
        IIC_SCL(1);
        iic_delay();

        if (IIC_READ_SDA) //读取SDA数据，为1就让receive+1
        {
            receive++;
        }
        
        IIC_SCL(0);//释放SCL
        iic_delay();
    }

    if (!ack)
    {
        iic_nack();     /* 发送nACK */
    }
    else
    {
        iic_ack();      /* 发送ACK */
    }

    return receive;
}
```

总结：

> 会发现每次操作完都把SCL拉低

### AT24C02实操

AT24C02是一款2K BIT(256Byte)的eeprom,有32页，每页8Byte，有WP写保护引脚，当该引脚置高电平，只可读不可写

* **地址方面**：前四位固定1010固定，中间三位由三个引脚A2 A1 A0决定，最后一位地址由数据传输方向决定，当读的时候最后一位为0，写的时候最后一位为1
  * 对本精英开发板来说 写地址为0XA0,读地址为0XA1

​ 前7位称设备地址，真正编程的时候操作的八位地址称作通讯地址

* \*\*写操作：\*\*AT24C02支持字节写模式和页写模式。
  * 字节写模式就是一个地址一个数据进行写。
  * 页写模式就是连续写入数据。只需要写一个地址，连续写入数据时地址会自增，但存在页的限制，超出一页时，超出数据**覆盖**原先写入的数据。但读会自动翻页。
* \*\*读操作：\*\*AT24C02支持当前地址读模式，随机地址读模式和顺序读模式

  * 当前读模式是基于上一次读/写操作的最后位置继续读出数据。
  * 随机地址读模式是指定地址读出数据，一个字节一个字节的读
  * 顺序读模式是连续读出数据，自动翻页

  #### IO口配置

  SCL推挽输出 SDA 开漏输出

  > 输出时：开漏输出的特点是能输出低电平，无法输出高电平，但可以被外部上拉至高电平，主机（MCU）输出0，可以拉低信号，来实现低电平发送，主机输出1（实际不起作用），由外部上拉电阻上拉，实现高电平发送。
  >
  > 输入时：当SDA作输入时，设置输出1状态，此时因为MCU无法输出1，相当于释放了SDA脚，此时外部器件可以主动拉低SDA脚/释放SDA脚（同样由上拉电阻提供“输出1的功能”），实现SDA脚的高低电平变化。由于开漏输出模式下，MCU还是可以读取IDR状态寄存器，来获取引脚高低电平，因此MCU读取IDR，即可获得SDA脚的高低电平状态，从而实现输入检测。
