两个Arduino之间I2C通信仿真
1、I2C介绍
集成电路间 (I2C) 协议是一种旨在允许多个“外围”数字集成电路(“芯片”)与一个或多个“控制器”芯片进行通信的协议。它只需要两根信号线来交换信息。
- SDA(串行数据):主机和从机发送和接收数据的线路。
- SCL(串行时钟):传输时钟信号的线路。
与 SPI 一样,I2C 是同步的,因此位的输出通过主机和从机之间共享的时钟信号与位的采样同步。 时钟信号始终由主机控制。
2、I2C的数据组成
使用 I2C,数据以消息的形式传输。 消息被分解成数据帧。 每条消息都有一个地址帧,其中包含从站的二进制地址,以及一个或多个包含正在传输的数据的数据帧。 该消息还包括每个数据帧之间的开始和停止条件、读/写位和 ACK/NACK 位:
- 启动条件:在 SCL 线从高电平切换到低电平之前,SDA 线从高电压电平切换到低电压电平。
- 停止条件:SCL 线从低电平切换到高电平后,SDA 线从低电平切换到高电平。
- 地址帧:每个从站唯一的 7 或 10 位序列,用于在主站想要与其通话时识别从站。
- 读/写位:单个位指定主机是向从机发送数据(低电压电平)还是从它请求数据(高电压电平)。
- ACK/NACK 位:消息中的每个帧后跟一个确认/不确认位。 如果成功接收到地址帧或数据帧,则从接收设备向发送方返回一个 ACK 位。
2)I2C寻址
I2C 没有像 SPI 那样的从机选择线,因此它需要另一种方式让从机知道正在向它发送数据,而不是另一个从机。 它通过寻址来做到这一点。 地址帧始终是新消息中起始位之后的第一帧。
主机将它想要与之通信的从机地址发送给与其相连的每个从机。 然后,每个从设备将主设备发送的地址与其自己的地址进行比较。 如果地址匹配,它会向主机发送一个低电压 ACK 位。 如果地址不匹配,从机什么也不做,SDA 线保持高电平。
3)读/写位
地址帧的末尾包含一个位,用于通知从机是要向其写入数据还是要从其接收数据。 如果主机要向从机发送数据,则读/写位为低电压电平。 如果主机向从机请求数据,则该位为高电压电平。
4)数据帧
在主机检测到来自从机的 ACK 位后,第一个数据帧准备好发送。
数据帧总是 8 位长,并以最高有效位在前发送。 每个数据帧后面紧跟着一个 ACK/NACK 位,以验证该帧是否已成功接收。 在发送下一个数据帧之前,主机或从机必须接收到 ACK 位(取决于谁在发送数据)。
发送完所有数据帧后,主设备可以向从设备发送停止条件以停止传输。 停止条件是在 SCL 线上从低到高转换之后 SDA 线上的电压从低到高转换,而 SCL 线保持高电平。
3、I2C通信过程
1)主机通过将 SDA 线从高电平切换到低电平,然后将 SCL 线从高电平切换到低电平,向每个连接的从机发送启动条件。
2)主机向每个从机发送它想要与之通信的从站的 7 位或 10 位地址以及读/写位:
3)每个从机将主机发送的地址与自己的地址进行比较。 如果地址匹配,从机通过将 SDA 线拉低一位来返回 ACK 位。 如果来自主机的地址与从机自己的地址不匹配,则从机将 SDA 线保持为高电平。
4)主机发送或接收数据帧:
5)传输完每个数据帧后,接收设备向发送方返回另一个 ACK 位以确认已成功接收该帧:
6)为了停止数据传输,主机通过在将 SDA 切换为高电平之前将 SCL 切换为高电平来向从机发送停止条件:
4、I2C连接模式
1)单主机多从机模式
由于 I2C 使用寻址,因此可以从单个主机控制多个从机。 对于 7 位地址,可以使用 128 (2^7) 个唯一地址。 使用 10 位地址并不常见,但可提供 1,024 (2^10) 个唯一地址。 要将多个从设备连接到单个主设备,请按如下方式连接它们,使用 4.7K 欧姆上拉电阻将 SDA 和 SCL 线连接到 Vcc:
2)多主机多从机模式
多个主机可以连接到一个从机或多个从机。 当两个主设备尝试通过 SDA 线同时发送或接收数据时,同一系统中的多个主设备就会出现问题。 为了解决这个问题,每个主设备在发送消息之前都需要检测 SDA 线是低还是高。 如果 SDA 线为低电平,则意味着另一个主机控制了总线,主机应等待发送消息。 如果 SDA 线为高电平,则传输消息是安全的。 要将多个主机连接到多个从机,请使用下图,使用 4.7K 欧姆上拉电阻将 SDA 和 SCL 线连接到 Vcc:
5、仿真电路原理图
6、仿真代码实现
1)主机代码
#include <Wire.h>
#define SLAVE_ADDRESS 0x12 // 从机地址
#define DATA_SIZE 8 // 数据缓冲大小
#define CMD_LED_OFF 0 // LED关命令
#define CMD_LED_ON 1 // LED开命令
void setup() {
// 启动为主机模式
Wire.begin();
Serial.begin(9600);
}
void loop() {
// 向从机发送数据
Wire.beginTransmission(SLAVE_ADDRESS);
Wire.write(CMD_LED_OFF);
Wire.endTransmission();
Serial.println("send led off cmd");
// 向从机请求数据
Wire.beginTransmission(SLAVE_ADDRESS);
Wire.requestFrom(SLAVE_ADDRESS, DATA_SIZE);
if (Wire.available()) {
Serial.print("Data returned: ");
while (Wire.available()) Serial.print((char) Wire.read());
Serial.println();
}
Wire.endTransmission();
delay(500);
Wire.beginTransmission(SLAVE_ADDRESS);
Wire.write(CMD_LED_ON);
Wire.endTransmission();
Serial.println("send led on cmd");
// 向从机发送数据
Wire.beginTransmission(SLAVE_ADDRESS);
Wire.requestFrom(SLAVE_ADDRESS, DATA_SIZE);
if (Wire.available()) {
Serial.print("Data returned: ");
while (Wire.available()) Serial.print((char) Wire.read());
Serial.println();
}
Wire.endTransmission();
delay(500);
}
2)从机代码
#include <Wire.h>
int cmd_dat = 0;
int ledPin = 7; // LED引脚
#define SLAVE_ADDRESS 0x12 // 从机地址
#define CMD_LED_OFF 0 // LED关命令
#define CMD_LED_ON 1 // LED开命令
void setup() {
// 设置LED引脚为输出
pinMode (ledPin , OUTPUT);
// 开始I2C总线,设置为从机模式
Wire.begin(SLAVE_ADDRESS);
// 设置数据接收回调函数
Wire.onReceive(receiveEvent);
// 设置数据请求回调函数
Wire.onRequest(requestEvent);
// 初始化串口
Serial.begin(9600);
}
void receiveEvent(int bytes) {
cmd_dat = Wire.read(); // 读取数据
}
void requestEvent()
{
switch (cmd_dat) {
case CMD_LED_OFF:
Serial.println("request led off");
Wire.write("led off",SLAVE_ADDRESS);
break;
case CMD_LED_ON:
Wire.write("led on",SLAVE_ADDRESS);
Serial.println("request led off");
break;
default:
Wire.write("nothing",SLAVE_ADDRESS);
}
}
void loop() {
if (cmd_dat == CMD_LED_OFF) {
Serial.println("led off");
digitalWrite(ledPin,LOW);
cmd_dat = -1;
}
if (cmd_dat == CMD_LED_ON) {
Serial.println("led on");
digitalWrite(ledPin,HIGH);
cmd_dat = -1;
}
}