11. I2C
11.1 什么是I2C
IIC(Inter-Integrated Circuit)协议也称为I2C总线,是一种串行通信协议,通常用于连接低速外设。它由Philips(现在的NXP Semiconductors)公司于1980年代初开发,现在已经成为一个标准。IIC总线只需要两条数据线,分别是串行数据线(SDA)和串行时钟线(SCL),这使得它成为一种非常简单的接口。它适用基于芯片的通信,例如连接传感器、存储器或数字信号处理器等。 在IIC协议中,总线上有一个主设备和多个从设备。主设备掌控着总线上的通信过程,负责发起、控制、停止通信。而从设备则需要等待主设备的请求,接收或发送数据。主设备和从设备之间的数据交换采用帧格式,每个帧通常包含地址、数据和控制信息。主设备根据从设备的地址来选中要通信的设备,从设备则根据控制信息进行相应的操作。IIC协议可以支持多个从设备连接到同一个主设备,为系统设计提供了更大的灵活性。
11.2 I2C的硬件实现
I2C总线通常使用两种电压电平,即高电平(VH)和低电平(VL)。高电平为2.5V至5.5V,低电平为0V至0.3V;这些电压电平范围是根据I2C规范确定的。 I2C总线有不同的传输速率可选,包括标准模式(100 kbps)、快速模式(400 kbps)以及高速模式。传输速率的选择取决于应用的需求和设备的支持能力。
为避免信号冲突,微处理器(MCU)必须只能驱动SDA和SCL在低电平,即开漏输出。设置为开漏模式主要是为了保护器件和防止干扰。
- 防止干扰:多个器件共享同一条数据线(SDA)和同一条时钟线(SCL),如果采用推挽输出模式,多个器件的输出将会叠加在数据线上,造成信号干扰,严重时会损坏器件或导致通信错误。而采用开漏输出模式,则各个器件的输出只有拉低数据线的部分,不会干扰彼此,从而提高了总线的可靠性和抗干扰能力。
- 防止短路:在开漏输出模式下,由于器件的输出只有拉低数据线的部分,如果两个或多个器件同时输出,也不会造成短路。而如果采用推挽输出模式,两个或多个器件同时输出时,可能会形成短路。比如主设备输出高电平,从设备输出低电平。
因设置为开漏模式,需要连接一个外部的上拉电阻(例如:10k)将信号提拉至高电平。故I2C总线中的SDA(数据线)和SCL(时钟线)通常都连接了上拉电阻,以确保逻辑高电平的稳定性。上拉电阻的阻值通常在2.2kΩ至10kΩ之间,具体取决于总线的电容负载和通信距离。
I2C总线的最大线缆长度和传输容量受到一定限制。在标准模式下,最大线缆长度大约在1米左右,而在快速模式下,最大线缆长度约为0.3米。此外,线缆上的总线容量也会对传输速率产生影响。
11.3 I2C数据传输
I2C协议使用总线抢占制进行数据传输。它只有两根通信线,因此它数据传输是基于时钟信号的。时钟由主设备产生,并控制数据的传输速率。数据由主设备发送并接收,但其交换是通过从设备的应答来实现的。
下面是IIC总线的几个重要的时序:
起始信号:SCL在高电平的状态下,SDA的电平由高转低,表示开始一次通信。
停止信号:SCL在高电平的状态下,SDA的电平由低转高,表示结束这次通信。主设备在发送停止信号后不能再向从设备发送任何数据,除非再次发送起始信号。
数据传输:主设备和从设备进行数据的传输,可以是一个或多个字节的数据,发送和接收都是基于地址选择的。
//发送一个字节
void IIC_Send_Byte(unsigned char dat)
{
int i = 0;
SDA_OUT();
SCL(0);
for( i = 0; i < 8; i++ )
{
SDA( (dat & 0x80) >> 7 );
delay_us(1);
SCL(1);
delay_us(5);
SCL(0);
delay_us(5);
dat<<=1;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
I2C还提供了一种称为“ACK/NACK”(应答/非应答)的确认机制。如果一个设备接收到数据,它将通过在SDA线上拉低电平来发送一个应答信号以通知发送方数据已被接收。相反,如果数据被损坏或未接收,接收设备将发送非应答信号。(在SDA上保持高电平)。
在IIC总线中,时钟线由主设备控制,每个数据位在时钟边沿更新,传输的最高速率取决于总线上最慢的设备。一般来讲,IIC总线的通信速率比较慢,通常在几百kbps的范围内。如果需要更高的传输速率,可以采用其他通信协议,如SPI协议、CAN协议等。
11.4 I2C通信流程
I2C通信流程按照以下步骤进行:
- 主控向总线发送开始信号。
- 主控将要通信的设备地址和读写位(R/W)发送到总线上。
- 设备接收到地址后发送应答信号,主控接收到应答信号后发送数据或继续发送地址。
- 设备接收到数据后发送应答信号,主控接收到应答信号后可以继续发送数据或者停止通信。
- 主控向总线发送停止信号。
11.5 IIC基本参数
速率:I2C总线有标准模式(100 kbit/s)和快速模式(400 kbit/s)两种传输模式,还有更快的扩展模式和高速模式可供选择。
器件地址:每个设备都有唯一的7位或10位地址,可以通过地址选择来确定与谁进行通信。
总线状态:I2C总线有五种状态,分别是空闲状态、起始信号、结束信号、响应信号、数据传输。
数据格式:I2C总线有两种数据格式,标准格式和快速格式。标准格式是8位数据字节加上1位ack/nack(应答/非应答)位,快速格式允许两个字节同时传输。
由于SCL和SDA线是双向的,它们也可能会由于外部原因(比如线路中的电容等)出现电平误差,而从而导致通信出错。因此,在IIC总线中,通常使用上拉电阻来保证信号线在空闲状态下的电平为高电平。
11.6 软件I2C与硬件I2C
I2C协议可以通过软件实现或者硬件实现。这两种方式的区别在于实现的方法和所需的硬件资源。
11.6.1 软件I2C
软件I2C是指通过在程序中编写代码来实现I2C通信协议。它利用通用输入输出(GPIO)引脚来模拟I2C的数据线(SDA)和时钟线(SCL),通过软件控制引脚的电平变化来传输数据和生成时序信号。与硬件I2C相比,软件I2C的优势在于不需要特定的硬件支持,可以在任何支持GPIO功能的微控制器上实现。它利用了微控制器的通用IO引脚来实现I2C通信协议。
软件I2C的实现通过编程方式来模拟I2C的主机和从机设备。通过逐位地读取和写入GPIO引脚的状态,并根据I2C协议的时序要求进行相应的操作,实现数据的传输和通信。软件I2C的灵活性较高,可以根据应用需求进行定制和扩展。它可以处理多个从机设备,并支持多主机环境。因此,软件I2C广泛应用于资源受限的MCU系统,特别是那些需要与多个外部设备进行通信的应用。
尽管软件I2C的性能相对于硬件I2C较低,但在一些低速通信和简单通信需求的场景下,软件I2C是一种经济实用的解决方案。
11.6.2 硬件I2C
硬件I2C是指通过专门的硬件模块来处理I2C通信协议。大多数现代微控制器和一些外部设备已经集成了硬件I2C模块,这些硬件模块负责处理I2C通信的细节,包括生成正确的时序信号、自动处理信号冲突、数据传输和错误检测等。可以直接使用硬件引脚连接,无需编写时序的代码。
使用硬件I2C通常相对简单,开发者无需编写复杂的代码来处理通信协议的细节。硬件模块可以直接与外部设备连接,通过专用的引脚进行数据和时钟传输,从而实现高效且可靠的通信。
在选择软件I2C还是硬件I2C时,需要考虑应用需求和硬件资源。软件I2C适用于资源受限的系统,可以在任何支持GPIO的微控制器上实现,但相对性能较低。硬件I2C通常性能更好,但需要硬件支持,并且可能占据一些特定的引脚资源。
11.6.3 IIC优缺点
11.6.3.1 优点
双向传输:I2C总线支持双向传输,可以通过SDA线同时传输主设备和从设备之间的数据,节约了总线的资源。
系统集成:I2C总线可以快速集成到芯片中,减少系统实现的逻辑复杂性,提高了设计效率。
多设备共享:I2C总线可以通过地址传输实现多个设备与主控器的通信,使得多个设备可以共享总线,并直接交互。
高可靠性:I2C总线使用逻辑层次的代替电气信号来表示数据传输,具有更高的传输可靠性。
11.6.3.2 缺点
带宽不高:I2C总线的传输速度限制在400 kbps,相比较于SPI总线和CAN总线,带宽相对较低。
时序要求严格:I2C总线传输数据需要严格遵循时序要求,特别是在高速传输过程,时序容易受到干扰,造成通信失败。
最长电缆长度有限:虽然I2C总线可以通过中继器扩展总线长度,但是由于信号线受到干扰,信号衰减和时序要求等问题,电缆最长长度一般限制在1~2米之间。
总之,I2C总线具有双向传输、系统集成、多设备共享等优点,但传输速度相对较低,时序要求严格且最长电缆长度有限等缺点。
11.7 ESP32S3的I2C介绍
ESP32S3有两个硬件I2C控制器(也称为端口),负责处理两条I2C总线上的通信。每个I2C控制器都可以作为主机或从机运行。ESP32的I2C接口可以配置为主模式或从模式,可以通过简单的API来控制I2C总线上的设备。
11.8 I2C相关函数
在arduino环境中,使用I2C需要引入自带库 Wire.h Wire继承steam类,steam类有的他都有。
#include "Wire.h"
初始化I2C I2C的初始化方式有很多种。
使用默认配置初始化I2C;引脚21默认的SDA, 引脚22是默认的SCL。但是在ESP32S3R8N8开发板上不能使用。
cWire.begin();
1可以设置通信引脚的初始化I2C;
cwire.begin(int sda_pin, int scl_pin);
1可以设置通信引脚、器件地址与I2C速率的初始化I2C;
cwire.begin(uint8_t slaveAddrint sda_pin, int scl_pin);
1还有很多方式,具体请查看arduino中wrie.c和wrie.h的源码。
- 从机请求数据
void requestFrom(uint16_t address, uint8_t size, bool sendStop)
参数:
address : 从机地址
size: 请求字节数
sendStop : 是否发送停止 , 如果为true, 释放IIC总线. 如果为false, 发送一个重新开始的信息, 并继续保持IIC总线的连接.
请求完成后 主机可以用 Wire.available()和Wire.read()等函数等待并获取从机的回答
示例:
Wire.requestFrom(adress,10,true);
- 开始传输
void beginTransmission(int address)
参数:
address : 从机地址
随后, 主机可以使用Wire.write();写数据并使用Wire.endTransmission();结束传输
示例:
Wire.beginTransmission(120);
- 结束传输
Wire.endTransmission();
结束数据传输但不释放IIC占用
endTransmission(false)
返回值: uint8_t 类型
返回值对于数值含义:
值 | 含义 |
---|---|
0 | 成功 |
1 | 数据过长,超出发送缓冲区 |
2 | 在地址发送时接收到NACK信号 |
3 | 在数据发送时接收到NACK信号 |
4 | 其他错误 |
示例:
Wire.endTransmission(false);
- 写数据
Wire.write();
当作为主机时: 主机将要发送的数据加入发送队列;
当作为从机时: 从机发送的数据给主机;
参数:
- Wire.write(value); //单字节发送
- Wire.write(string); //以一系列字节发送
- Wire.write(data,length); //以字节形式发送,指定长度
返回值: byte类型,输入的字节数
- 接收数据寄存器有值
Wire.available();
功能:返回接收到的字节数
返回值: byte类型,可读字节数
- 读取1字节数据
Wire.read()
当作为主机时: 主机使用requestFrom()后 要使用此函数获取数据;
当作为从机时: 从机读取主机给的数据; 返回值: 读到的字节数据 byte
- 读取多字节数据
size_t readBytes(char *buffer, size_t length)
参数:
buffer: 接收缓冲区, 一个char型指针
length: 数据长度
返回值: 数据长度
- 读取直到遇到某字符
size_t readBytesUntil(char terminator, char *buffer, size_t length)
参数:
terminator : 终结字符 char类型
buffer: 接收缓冲区, 一个char型指针
length: 数据长度
返回值: 数据长度
- 读取当前IIC是否忙
Wire.busy();
返回布尔值
11.9 I2C验证
11.9.1 案例一:0.96寸IIC单色屏显示
在arduino的库管理中,下载 ESP32的OLED库:
ESP8266 and ESP32 OLED driver for SSD1306 displays
/* 安装 ESP32 OLED库. 在库管理中搜索 -> ESP8266 and ESP32 OLED driver for SSD1306 displays */
#include <Wire.h>
#include "SSD1306Wire.h"//导入0.96寸屏幕显示库
// 使用Wire库初始化OLED显示器
SSD1306Wire display(0x3c, 9, 10); // 三个参数分别是 器件地址, SDA引脚, SCL引脚
//画线
void drawLines()
{
for (int16_t i = 0; i < display.getWidth(); i += 4)
{
display.drawLine(0, 0, i, display.getHeight() - 1);
display.display();
delay(10);
}
for (int16_t i = 0; i < display.getHeight(); i += 4)
{
display.drawLine(0, 0, display.getWidth() - 1, i);
display.display();
delay(10);
}
delay(250);
display.clear();
for (int16_t i = 0; i < display.getWidth(); i += 4)
{
display.drawLine(0, display.getHeight() - 1, i, 0);
display.display();
delay(10);
}
for (int16_t i = display.getHeight() - 1; i >= 0; i -= 4)
{
display.drawLine(0, display.getHeight() - 1, display.getWidth() - 1, i);
display.display();
delay(10);
}
delay(250);
display.clear();
for (int16_t i = display.getWidth() - 1; i >= 0; i -= 4)
{
display.drawLine(display.getWidth() - 1, display.getHeight() - 1, i, 0);
display.display();
delay(10);
}
for (int16_t i = display.getHeight() - 1; i >= 0; i -= 4)
{
display.drawLine(display.getWidth() - 1, display.getHeight() - 1, 0, i);
display.display();
delay(10);
}
delay(250);
display.clear();
for (int16_t i = 0; i < display.getHeight(); i += 4)
{
display.drawLine(display.getWidth() - 1, 0, 0, i);
display.display();
delay(10);
}
for (int16_t i = 0; i < display.getWidth(); i += 4)
{
display.drawLine(display.getWidth() - 1, 0, i, display.getHeight() - 1);
display.display();
delay(10);
}
delay(250);
}
//画矩形
void drawRect(void)
{
for (int16_t i = 0; i < display.getHeight() / 2; i += 2)
{
display.drawRect(i, i, display.getWidth() - 2 * i, display.getHeight() - 2 * i);
display.display();
delay(10);
}
}
//画填充矩形
void fillRect(void)
{
uint8_t color = 1;
for (int16_t i = 0; i < display.getHeight() / 2; i += 3)
{
display.setColor((color % 2 == 0) ? BLACK : WHITE); // alternate colors
display.fillRect(i, i, display.getWidth() - i * 2, display.getHeight() - i * 2);
display.display();
delay(10);
color++;
}
// Reset back to WHITE
display.setColor(WHITE);
}
// 画圆
void drawCircle(void)
{
for (int16_t i = 0; i < display.getHeight(); i += 2)
{
display.drawCircle(display.getWidth() / 2, display.getHeight() / 2, i);
display.display();
delay(10);
}
delay(1000);
display.clear();
display.drawCircleQuads(display.getWidth() / 2, display.getHeight() / 2, display.getHeight() / 4, 0b00000001);
display.display();
delay(200);
display.drawCircleQuads(display.getWidth() / 2, display.getHeight() / 2, display.getHeight() / 4, 0b00000011);
display.display();
delay(200);
display.drawCircleQuads(display.getWidth() / 2, display.getHeight() / 2, display.getHeight() / 4, 0b00000111);
display.display();
delay(200);
display.drawCircleQuads(display.getWidth() / 2, display.getHeight() / 2, display.getHeight() / 4, 0b00001111);
display.display();
}
void printBuffer(void)
{
// 初始化日志缓冲区
// 分配内存来存储8行文本,每行30个字符。
display.setLogBuffer(5, 30);
// 测试内容
const char* test[] =
{
"Hello",
"World" ,
"----",
"Show off",
"how",
"the log buffer",
"is",
"working.",
"Even",
"scrolling is",
"working"
};
for (uint8_t i = 0; i < 11; i++)
{
//清屏
display.clear();
//打印到屏幕并且换行
display.println(test[i]);
// 将其绘制到内部屏幕缓冲区
display.drawLogBuffer(0, 0);
// 把它显示在屏幕上
display.display();
delay(500);
}
}
void setup()
{
//初始化屏幕
display.init();
//设置对比度
display.setContrast(255);
//画线测试
drawLines();
delay(1000);
//清屏
display.clear();
//画矩形测试
drawRect();
delay(1000);
//清屏
display.clear();
//画填充矩形测试
fillRect();
delay(1000);
//清屏
display.clear();
//画圆测试
drawCircle();
delay(1000);
display.clear();
//显示文本测试
printBuffer();
delay(1000);
display.clear();
}
void loop() { }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
11.9.2 案例二:读取MLX90615温度的数据
#include <Wire.h>
void setup()
{
//串口0初始化,波特率为115200
Serial.begin(115200);
//设置I2C通信线,SDA=5,SCL=6
Wire.begin(5,6);
}
void loop()
{
unsigned char buff[3]={0}; //保存温度高低位数据与校验码
uint16_t temp = 0; //高低位整合数据保存变量
float T=0.0; //换算出的实际温度保存变量
Wire.beginTransmission(0x5A); //发送I2C协议的起始信号,器件地址为0X5A
Wire.write(0X07); //写入要操作的寄存器地址
Wire.endTransmission(false); //停止Stop,并发送一个重新起始信号
//从0X5A的器件地址,读取3个字节数据,读取完成后发送停止信号
Wire.requestFrom((uint16_t)0x5A, (uint8_t)3, (bool)true);
buff[0] = Wire.read();//读取第一个数据,保存温度数据的低8位
buff[1] = Wire.read();//读取第二个数据,保存温度数据的高8位
buff[2] = Wire.read();//读取第三个数据,保存校验码
//数据高低位整合
temp = (uint16_t)(buff[1]<<8) | buff[0];
//换算实际温度
T = (temp*0.02)-273.15;
//延时1秒
delay(1000);
//输出采集到的温度
Serial.printf("temp = %.2f C\r\n",T);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36