今天我们来玩儿I2C。

I2C概述

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

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

esp32-s入门教程(老宇哥带你玩转)(1)

SCL -- 时钟信号

SDA -- 数据信号

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

esp32-s入门教程(老宇哥带你玩转)(2)

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

启动与停止信号:

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

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

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

esp32-s入门教程(老宇哥带你玩转)(3)

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

esp32-s入门教程(老宇哥带你玩转)(4)

地址字节

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

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

ACK与NACK

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

esp32-s入门教程(老宇哥带你玩转)(5)

数据信号

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

esp32-s入门教程(老宇哥带你玩转)(6)

命令字节

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

写入设备

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

esp32-s入门教程(老宇哥带你玩转)(7)

读取数据

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

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

esp32-s入门教程(老宇哥带你玩转)(8)

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

硬件

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

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

esp32-s入门教程(老宇哥带你玩转)(9)

软件

启动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()有以下几个返回结果:

写数据

向从设备写入数据,在调用 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,代表这个地址通信成功,我们就认为总线上存在这个地址的设备。

esp32-s入门教程(老宇哥带你玩转)(10)

I2C OLED

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

esp32-s入门教程(老宇哥带你玩转)(11)

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 pixels Adafruit_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示例代码演示:

esp32-s入门教程(老宇哥带你玩转)(12)

Adafruit_SSD示例代码效果:

感谢大家,关于ESP32的学习,希望大家Enjoy!

欢迎点分享、收藏、点赞、在看。

,