Skip to content

51单片机 I2C 通讯

前提条件

了解 I2C

I2C总线全程 Inter IC Bus, 常用于芯片间通信, 比如说从AT24C02存储芯片中读取数据. 会使用到两根通信线 SCL(Serial Clock), SDA(Serial Data).

I2C 接线

  • 所有I2C设备的SCL连在一起, SDA连在一起
  • 所有I2C设备的SCLSDA均要配置成开漏输出模式
  • SCLSDA上各添加一个上拉电阻, 阻值一般为4.7kΩ
  • 开漏输出和上拉电阻主要是为了解决多机通信互相干扰的问题

picture 11

https://www.ti.com/lit/an/sbaa565/sbaa565.pdf?ts=1725775105679

I2C 时序

  • 起始条件S: SCL高电平期间, SDA从高电平切换为低电平.
  • 终止条件P: SCL高电平期间, SDA从低电平切换为高电平. picture 3

    https://www.ti.com/lit/an/sbaa565/sbaa565.pdf?ts=1725775105679

  • 发生数据SD: 主设备取出待发送数据(8bit)的最高位, 将SDA设置为该值. 而后拉高SCL再拉低SCL, 从设备会在SCL的下降沿读取SDA的值. 所以在SCL线高电平期间, SDA的值必须保持稳定, 毕竟此时从设备正在读取数据. 重复这一过程8次, 即可发送 8bit 数据. picture 4

    https://www.ti.com/lit/an/sbaa565/sbaa565.pdf?ts=1725775105679

  • 接收数据RD: 主设备先将SDA设置为1以释放SDA. 然后主设备拉高SCL再拉低SCL创造一个时钟脉冲, 在这个脉冲期间, 从设备会把它要发出的值写在SCL上, 所以在这个脉冲期间主设备还需要读取SDA的值. 创建8个脉冲, 读取8次SDA, 就能拼接出一个完整的 byte(8bit)
  • 接收应答RA: 时序图切片与数据传输相同, 可以理解为数据传输的第9位. 主设备每发送一个 byte 后, 从设备会向主设备发送一个应答位(1bit), 0 表示成功收到数据 ACK(Acknowledge), 1 表示没有收到数据或通信结束 NAK(Not Acknowledge)
  • 发送应答SA: 时序图切片与数据传输相同, 可以理解为数据传输的第9位. 主设备每收到一个 byte 后, 需要先从设备发送一个应答位(1bit), 0 表示成功收到数据 ACK(Acknowledge), 1 表示没有收到数据或通信结束 NAK(Not Acknowledge)

注意! 上述简写字母为作者自定义, 用来代表时序图的切片, 不是标准的简写.

例子1, 发送一帧数据.

  1. 起始
  2. 发送数据. 内容为从设备地址, 数据大小 1byte. 其中前7位为设备地址, 最后一位为发送/接受标志位. 例如, 对于AT24C02芯片来说
    • 设备地址的前4位为固定值 0b1010
    • 后3位由芯片引脚 A0, A1, A2 所接的电平高低决定, 假设它们都接了 GND, 那它们就是 0b000
    • 观察数据手册发现, 时序图中 R/W 的 W 上划了一根横线, 所以代表 1 是 Read, 0 是 Write. 由于是写入, 所以值为 0b0
    • 所以第一个 byte 的具体值为 0b10100000
  3. 接收应答
  4. 发送数据, 接收应答 ... 发送数据, 接收应答
  5. 终止 picture 9

例子2, 接受一帧数据

  1. 起始
  2. 发送数据. 内容为从设备地址, 数据大小 1byte. 其中前7位为设备地址, 最后一位为发送/接受标志位. AT24C02芯片来说, 由于是读操作, 所以最后一位是 1, 因为观察数据手册发现, 时序图中 R/W 的 W 上划了一根横线, 这意味着代表 1 是 Read, 0 是 Write.
  3. 接收应答
  4. 接收数据, 发送应答 ... 接收数据, 发生应答
  5. 终止 picture 6

读写 AT24C02 芯片

效果展示

  1. 在串口监视器中发送'r', 单片机从AT24C02读取数据, 打印出原始数据.
  2. 在串口监视器中发送'w', 单片机向AT24C02写入数据.
  3. 在串口监视器中发送'r', 单片机从AT24C02读取数据, 打印读到的数据, 该数据应与写入的数据相同.
  4. 将单片机断电, 然后再上电.
  5. 在串口监视器中发送'r', 单片机从AT24C02读取数据, 打印读到的数据, 该数据应与上一次读出的数据相同.

参考资料

工作流程(写入)

向 AT24C02 写入. 数据手册中流程图如下 picture 1
转译为我们自定义符号的示意图 picture 10

工作流程(读取)

从 AT24C02 读取. 数据手册中流程图如下 picture 2
转译为我们自定义符号的示意图 picture 8

代码编写

c
#include <stdint.h>
#include <stdio.h>
#include "uart.h"
#include "at24c02.h"
#include "delay.h"

uint8_t count = 0;

// 中断号为4的原因: STC89C52RC数据手册, 第6章 中断系统
void uart_isr(void)  __interrupt (4) {
  if (RI == 1) {          // 如果是接收中断
    RI = 0;               // 清除接收标志

    char received_data = SBUF;  // 读取接收到的数据

    uint8_t storage_address = 0x02;
    count ++;

    if(received_data == 'w') {
      at24c02_write(storage_address, count);
      delay_ms(6); // 写入需要 5ms

      char str[20];
      sprintf(str, "Data saved, %d", count);
      uart_print(str);
      return;
    }
    if(received_data == 'r') {
      uint8_t data = at24c02_read(storage_address);

      char str[20];
      sprintf(str, "Data retrieved, %d", data);
      uart_print(str);
    }
  }
}

void main(void) {

  uart_init();
  while (1) {
  }
}
c
#include "at24c02.h"

uint8_t at24c02_read(uint8_t address) {
  i2c_start();
  i2c_send(AT24C02_WRITE_ADDRESS);
  i2c_receive_ack();
  i2c_send(address);
  i2c_receive_ack();
  i2c_start();
  i2c_send(AT24C02_READ_ADDRESS);
  i2c_receive_ack();
  uint8_t data = i2c_receive();
  i2c_send_ack(I2C_NO_ACK);
  i2c_stop();

  return data;
}

void at24c02_write(uint8_t address, uint8_t data) {
  i2c_start();
  i2c_send(AT24C02_WRITE_ADDRESS);
  i2c_receive_ack();
  i2c_send(address);
  i2c_receive_ack();
  i2c_send(data);
  i2c_receive_ack();
  i2c_stop();
}
c
#include "i2c.h"

void i2c_start(void) {
  I2C_SDA = 1;
  I2C_SCL = 1;
  I2C_SDA = 0;  // Start condition: SDA goes low when SCL is high
  I2C_SCL = 0;
}

void i2c_stop(void) {
  I2C_SDA = 0;
  I2C_SCL = 1;
  I2C_SDA = 1;  // Stop condition: SDA goes high when SCL is high
}

void i2c_send(uint8_t data) {
  for (uint8_t i = 0; i < 8; i++) {
    I2C_SDA = data & (0x80 >> i);  // Send each bit
    // 注意! 从设备读取 SDA 状态需要时间, 所以 SCL 需要保持一段时间的高电平
    // 查看 AT24C02 数据手册得该芯片最大 SCL 时钟频率为 400kHz, 0.4us一个周期.
    // 51 单片机时钟频率为 11059200 Hz, 11us一个周期. 
    // 所以, 单片机将 SCL 置高后马上置低, 少说了过了 11us, 足够从设备读取 SCL 数据了.
    // 但是对于频率更高的单片机, 就可能需要延时增加 SCL 的时长了.
    I2C_SCL = 1;  // Clock high
    I2C_SCL = 0;  // Clock low
  }
}

uint8_t i2c_receive(void) {
  uint8_t data = 0;
  I2C_SDA = 1;  // Release SDA to read data
  for (uint8_t i = 0; i < 8; i++) {
    I2C_SCL = 1;
    data = (data << 1) | I2C_SDA;  // Read each bit from SDA
    I2C_SCL = 0;
  }
  return data;
}

void i2c_send_ack(I2C_ACK_BIT ack_bit){
  I2C_SDA = ack_bit; // Set ack bit in SDA
  I2C_SCL = 1; // Slave device start reading SDA
  I2C_SCL = 0; // Slave device stop reading SDA
}

I2C_ACK_BIT i2c_receive_ack(void){
  I2C_SDA = 1; // Release SDA
  I2C_SCL = 1; // Start reading SDA
  I2C_ACK_BIT ack_bit = I2C_SDA; // Read ack bit from SDA
  I2C_SCL = 0; // Stop reading SDA

  return ack_bit;
}

完整代码 codes/demo207-51-i2c