今天我们来玩儿I2C。
I2C概述I2C全称是Inter-Integrated Circuit,是飞利浦半导体公司(06年迁移到NXP了)在1982年发明的,是使用非常广泛的一种通信协议,很多传感器、存储芯片、OLED等,都是在使用I2C。标准输出模式下能达到100kbps的传输速率,快速模式下能达到400kbps的传输速率,高速模式下能达到3.4Mbps,超高速下最快能达到5Mbps。
与UART一样,IIC仅用两条线在设备间通信:
SCL -- 时钟信号
SDA -- 数据信号
I2C主机与从机之间共享时钟信号,时钟始终由主机控制,总线下面可以挂多个设备,是一种同步,多主,多从,半双工的通信协议,下面我们简单介绍一下通信原理:
默认情况下,两条线都被上拉,SCL=1,SDA=1。
启动与停止信号:
通信开始,要先发开启动信号,结束的时候,要发送结束信号。
开始信号由主设备发出启动,具体为在SCL高电平期间,SDA从高电平切换到低电平;
停止信号由主设备发出结束,具体为在SCL高电平期间,SDA从低电平切换到高电平;
当然,在传输过程中,有时候需要更改数据方向,重新传输等,我们没必要发停止信号,直接重新发启动信号启动即可。
地址字节
我们的总线上可能挂很多从设备,在我们主设备发送了启动信号之后,总线上的从设备就都被“唤醒”了,等着主设备发送地址宠幸。所以这里有一个从机地址的概念,从机地址以8位字节发送的,MSB在前,最后一位表示接下来读或写,所以高7位构成了从机地址,也可以看出,同一个总线上,可以寻址128个从设备。
一旦从设备的地址匹配,就继续读取最后一位,低电平代表写入,高电平代表读取。其它从设备就忽略后面的数据。
ACK与NACK
在每个字节传输之后,接收设备发送一个应答信号,确认或者不确认,接收设备通过在SCL高电平期间,将SDA拉低生成一个确认信号ACK,拉高生成一个不确认信号NACK,这里ACK主要用于表示字节正确传输了,NACK表示数据传输有错误,需要从新发送。应答信号主设备,从设备都可以产生,比如,主设备从从设备读取最后一个字节的数据后,就要发送NACK结束传输。
数据信号
数据以8位字节格式传输,高字节在前,传输的字节数量没有限制,但是每个字节后面必须要有一个数据接收方产生的应答信号。传输过程中,SCL为低的时候,SDA数据可以改变,SCL为高的时候,SDA的数据必须稳定。
命令字节
当写入或读取从设备中特定寄存器时,主机首先要向已寻址的从机写入寄存器地址,其实也是一个数据字节,我们这里称之为命令字节。
写入设备
主设备在发出启动信号之后,紧着着发送要操作从设备的地址,最后一位为低电平表示接下来写入数据,然后在时钟信号下一位一位的写入数据,在从设备发出ACK应答之后,发送结束信号结束通信。
读取数据
主设备在发出启动信号之后,紧着着发送要操作从设备的地址,最后一位为高电平表示接下来读取数据,然后接管SDA数据线并在时钟的控制下向主设备发送数据,主设备同样要在每个字节接收完毕的时候发送ACK响应,当主设备不想接收的时候,就在最后一个字节接收后发送NACK响应,然后恢复对总线的控制并发送结束信号。
SCL的控制权始终在主机这里。
当然,实际还要很多组合传输协议,这里由于篇幅问题就不展开说了,基本上大同小异,我们根据不同设备的数据手册来传输就可以啦。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,当然,我们可以在代码中配置到任何引脚。
软件
启动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,代表这个地址通信成功,我们就认为总线上存在这个地址的设备。
I2C OLED
I2C只是个通信协议,具体的还是要结合实物来演示,比如一些传感器或者屏幕,这里我们用I2C协议的0.96寸OLED屏幕来演示下:
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示例代码演示:
Adafruit_SSD示例代码效果:
感谢大家,关于ESP32的学习,希望大家Enjoy!
欢迎点分享、收藏、点赞、在看。
,