新闻  |   论坛  |   博客  |   在线研讨会
老宇哥带你玩转 ESP32:07 I2C协议,看这一篇就够了
芯片之大家 | 2023-07-04 19:20:13    阅读:6046   发布文章

今天我们来玩儿I2C

I2C概述

I2C全称是Inter-Integrated Circuit,是飞利浦半导体公司(06年迁移到NXP了)在1982年发明的,是使用非常广泛的一种通信协议,很多传感器、存储芯片、OLED等,都是在使用I2C。标准输出模式下能达到100kbps的传输速率,快速模式下能达到400kbps的传输速率,高速模式下能达到3.4Mbps,超高速下最快能达到5Mbps

与UART一样,IIC仅用两条线在设备间通信:

image.png


SCL -- 时钟信号

SDA -- 数据信号

I2C主机与从机之间共享时钟信号,时钟始终由主机控制,总线下面可以挂多个设备,是一种同步,多主,多从,半双工的通信协议,下面我们简单介绍一下通信原理:

image.png


默认情况下,两条线都被上拉,SCL=1,SDA=1。

启动与停止信号:

通信开始,要先发开启动信号,结束的时候,要发送结束信号。

开始信号由主设备发出启动,具体为在SCL高电平期间,SDA从高电平切换到低电平

停止信号由主设备发出结束,具体为在SCL高电平期间,SDA从低电平切换到高电平

image.png


当然,在传输过程中,有时候需要更改数据方向,重新传输等,我们没必要发停止信号,直接重新发启动信号启动即可。

image.png


地址字节

我们的总线上可能挂很多从设备,在我们主设备发送了启动信号之后,总线上的从设备就都被“唤醒”了,等着主设备发送地址宠幸。所以这里有一个从机地址的概念,从机地址以8位字节发送的,MSB在前,最后一位表示接下来读或写,所以高7位构成了从机地址,也可以看出,同一个总线上,可以寻址128个从设备。

一旦从设备的地址匹配,就继续读取最后一位,低电平代表写入,高电平代表读取。其它从设备就忽略后面的数据。

ACK与NACK

在每个字节传输之后,接收设备发送一个应答信号,确认或者不确认,接收设备通过在SCL高电平期间,将SDA拉低生成一个确认信号ACK拉高生成一个不确认信号NACK,这里ACK主要用于表示字节正确传输了,NACK表示数据传输有错误,需要从新发送。应答信号主设备,从设备都可以产生,比如,主设备从从设备读取最后一个字节的数据后,就要发送NACK结束传输。

image.png


数据信号

数据以8位字节格式传输,高字节在前,传输的字节数量没有限制,但是每个字节后面必须要有一个数据接收方产生的应答信号。传输过程中,SCL为低的时候,SDA数据可以改变,SCL为高的时候,SDA的数据必须稳定

image.png


命令字节

当写入或读取从设备中特定寄存器时,主机首先要向已寻址的从机写入寄存器地址,其实也是一个数据字节,我们这里称之为命令字节。

写入设备

主设备在发出启动信号之后,紧着着发送要操作从设备的地址,最后一位为低电平表示接下来写入数据,然后在时钟信号下一位一位的写入数据,在从设备发出ACK应答之后,发送结束信号结束通信。

image.png


读取数据

主设备在发出启动信号之后,紧着着发送要操作从设备的地址,最后一位为高电平表示接下来读取数据,然后接管SDA数据线并在时钟的控制下向主设备发送数据,主设备同样要在每个字节接收完毕的时候发送ACK响应,当主设备不想接收的时候,就在最后一个字节接收后发送NACK响应,然后恢复对总线的控制并发送结束信号。

SCL的控制权始终在主机这里。

image.png


当然,实际还要很多组合传输协议,这里由于篇幅问题就不展开说了,基本上大同小异,我们根据不同设备的数据手册来传输就可以啦。I2C还有很多特性,快速命令,仲裁,多主控等等,普通的应用接触不到,感兴趣的小伙伴自行研究下。

硬件

ESP32有2个硬件I2C总线接口,接口可以配置为主机或从机模式,支持如下特性:

  • 标准模式 (100 Kbit/s)

  • 快速模式 (400 Kbit/s)

  • 高达 5 MHz,但受 SDA 上拉强度的限制

  • 7位/10位寻址模式

  • 双寻址模式,用户可以通过编程命令寄存器来控制 I²C 接口,让他们有更大的灵活性

SDA与SCL是低电平有效的,所以我们应该在两根数据线上用电阻上拉,IO内部也是开漏输出的,一般5V系统接4.7K上拉,3.3V系统接2.4K上拉即可。ESP32上,SDA默认连接GPIO21,SCL默认连接GPIO22,当然,我们可以在代码中配置到任何引脚

image.png


软件

启动I2C

启动Wire库并作为主机或者从机加入总线,这个函数调用一次即可,参数为7位从机地址,不带参数就以主机的形式加入总线。

Wire.begin();Wire.begin(address)

主设备从从设备请求字节

由主设备向从设备请求字节,之后用available()和read()函数读取字节,第三个参数位为stop,在请求后会发送停止消息,释放I2C总线,否则总线就不会被释放。

Wire.requestFrom(address, quantity);Wire.requestFrom(address, quantity, stop);

给指定地址的从设备传输数据

给指定地址的从设备传输数据,之后调用write()函数排队传输字节,要通过endTransmission()结束传输。

Wire.beginTransmission(address)

endTransmission()有以下几个返回结果:

  • 0:成功

  • 1:数据太长,无法放入发送缓冲区

  • 2:在发送地址时收到 NACK

  • 3:在发送数据时收到 NACK

  • 4:其他错误

写数据

向从设备写入数据,在调用 beginTransmission() 和 endTransmission() 之间。

Wire.write(value)
Wire.write(string)
Wire.write(data, length)

举个例子

#include <Wire.h>byte val = 0;void setup(){
  Wire.begin(); // join i2c bus}void loop(){
  Wire.beginTransmission(44); // transmit to device #44 (0x2c)
                              // device address is specified in datasheet
  Wire.write(val);             // sends value byte  
  Wire.endTransmission();     // stop transmitting

  val++;        // increment value
  if(val == 64) // if reached 64th position (max)
  {
    val = 0;    // start over from lowest value
  }
  delay(500);
}

读数据

调用requestFrom()后从从设备读取数据。

Wire.read()

举个例子

#include <Wire.h>void setup(){
  Wire.begin();        // join i2c bus (address optional for master)
  Serial.begin(9600);  // start serial for output}void loop(){
  Wire.requestFrom(2, 6);    // request 6 bytes from slave device #2

  while(Wire.available())    // slave may send less than requested
  {    char c = Wire.read();    // receive a byte as character
    Serial.print(c);         // print the character
  }

  delay(500);
}

还有其它一些函数,例如修改时钟频率等等,大家用到的时候自行了解一下。

完整程序

这里我们用一个例子来演示一下,I2C启动之后,我们开始扫描总线上存在的设备,并通过串口打印结果出来,我在I2C下面接了一个OLED的设备。

#include "Wire.h"void setup(){
  Serial.begin(115200); 
  Serial.println();
  Serial.println("Scanning for I2C Devices ...");
  Serial.print("\r\n");  int I2CDevices = 0;  byte address;

  Wire.begin();  
  for (address = 1; address < 127; address++)
  {
    Wire.beginTransmission(address);    if (Wire.endTransmission() == 0)
    {
      Serial.print("Found I2C Device: ");
      Serial.print(" (0x");      if (address < 16)
      {
        Serial.print("0");
      }
      Serial.print(address, HEX);
      Serial.println(")");
      I2CDevices++;
    }
  }  if (I2CDevices == 0)
  {
    Serial.println("没有发现I2C设备!\n");
  }  else
  {   
    Serial.print("发现了");
    Serial.print(I2CDevices);
    Serial.println("个I2C设备!\n");  
  }  
}

void loop(){
}

Wire.endTransmission()返回0,代表这个地址通信成功,我们就认为总线上存在这个地址的设备。

image.png


I2C OLED

I2C只是个通信协议,具体的还是要结合实物来演示,比如一些传感器或者屏幕,这里我们用I2C协议的0.96寸OLED屏幕来演示下:

image.png


OLED使用SSD1306控制芯片,所以我们需要下载一个库SSD1306,另外还需要配合图形库GFX操作,代码中,我们先包含对应头文件,然后创建一个Adafruit_SSD1306对象,第三个参数是用的I2C对象。

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

初始化时候用display.begin(SSD1306_SWITCHCAPVCC, 0x3C)初始化显示对象,传入地址,然后就可以自由简单的显示我们想要显示的数据了。

关于Adafruit_GFX库,非常强大的一个图形库,我们后面单独讲解具体的原理,这里先了解一下即可。

完整程序

#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>#define SCREEN_WIDTH 128 // OLED display width, in pixels#define SCREEN_HEIGHT 64 // OLED display height, in pixelsAdafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);void setup() {
  Serial.begin(115200);  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("SSD1306 allocation failed"));    for(;;);
  }  
  delay(1000);
  display.display();
  
  display.clearDisplay();
  display.setTextColor(WHITE);
  display.setTextSize(1);
  display.setCursor(0,0);
  display.print("CHIPHOME");
  display.display();
  display.setCursor(0,8);
  display.print("12345678");
  display.display();
  delay(1000);
}void loop() {

}

SSD1306示例代码演示:

image.png


*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
推荐文章
最近访客