I love the I2C protocol. When I need a sensor, every time I try to find one with this protocol, I have also written some libraries for various sensors that use I2C. So I want to write some articles explaining (Arduino, Arduino SAMD MKR, esp8266, and esp32) some interesting features, and I will try to explain how to solve the problems you can have when working with multiple I2C devices.
I use esp8266 in a lot of situations, and it’s one of the cheapest devices in commerce. In this text, we will delve into the specifics of the I2C protocol on the ESP8266, including the methods of communication between devices operating on different voltage levels, such as 5V and 3.3V. We will also learn how to manipulate the speed of communication and use custom pins to suit the requirements of our projects.
Introduction to the I2C protocol
I2C (Inter-Integrated Circuit, eye-squared-C) and is alternatively known as I2C or IIC. It is a synchronous, multi-master, multi-slave, packet switched, single-ended, serial communication bus. Invented in 1982 by Philips Semiconductors. It is widely used for attaching lower-speed peripheral ICs to processors and microcontrollers in short-distance, intra-board communication. (cit. WiKi)
Speed
I2C supports 100 kbps, 400 kbps, and 3.4 Mbps. Some variants also support 10 Kbps and 1 Mbps.
Mode | Maximum speed | Maximum capacitance | Drive | Direction |
---|---|---|---|---|
Standard-mode (Sm) | 100 kbit/s | 400 pF | Open drain | Bidirectional |
Fast-mode (Fm) | 400 kbit/s | 400 pF | Open drain | Bidirectional |
Fast-mode Plus (Fm+) | 1 Mbit/s | 550 pF | Open drain | Bidirectional |
High-speed mode (Hs) | 1.7 Mbit/s | 400 pF | Open drain | Bidirectional |
High-speed mode (Hs) | 3.4 Mbit/s | 100 pF | Open drain | Bidirectional |
Ultra Fast-mode (UFm) | 5 Mbit/s | Push-pull | Unidirectional |
Interface
Like UART communication, I2C only uses two wires to transmit data between devices:
- SDA (Serial Data) – The line for the master and slave to send and receive data.
- SCL (Serial Clock) – The line that carries the clock signal (common clock signal between multiple masters and multiple slaves).
I2C is a serial communication protocol, so data is transferred bit by bit along a single wire (the SDA line).
Like SPI, I2C is synchronous, so the output of bits is synchronized to the sampling of bits by a clock signal shared between the master and the slave. The clock signal is always controlled by the master.
There will be multiple slaves and multiple masters and all masters can communicate with all the slaves.
- Start: The SDA line switches from a high voltage level to a low voltage level before the SCL line switches from high to low.
- Stop: The SDA line switches from a low voltage level to a high voltage level after the SCL line switches from low to high.
- Address Frame: A 7 or 10 bit sequence unique to each slave that identifies the slave when the master wants to talk to it.
- Read/Write Bit: A single bit specifying whether the master is sending data to the slave (low voltage level) or requesting data from it (high voltage level).
- ACK/NACK Bit: Each frame in a message is followed by an acknowledge/no-acknowledge bit. If an address frame or data frame was successfully received, an ACK bit is returned to the sender from the receiving device.
Devices connections
Because I2C uses addressing, multiple slaves can be controlled by a single master. With a 7 bit address, 128 (27) unique addresses are available. Using 10 bit addresses is uncommon, but provides 1,024 (210) unique addresses. Up to 27 slave devices can be connected/addressed in the I2C interface circuit.
Multiple masters can be connected to a single slave or multiple slaves. The problem with multiple masters in the same system comes when two masters try to send or receive data at the same time over the SDA line. To solve this problem, each master needs to detect if the SDA line is low or high before transmitting a message. If the SDA line is low, this means that another master has control of the bus, and the master should wait to send the message. If the SDA line is high, then it’s safe to transmit the message. To connect multiple masters to multiple slaves
esp8266 how to
First, I think It’s better if you read “WeMos D1 mini (esp8266), pinout, specs and IDE configuration“.
Here the esp8266 on Aliexpress esp-12 - Aliexpress esp-07
The ESP8266 is a low-cost Wi-Fi microchip with a full TCP/IP stack and microcontroller capability, produced by Espressif Systems in Shanghai, China.
It has a good set of interfaces:
- SPI
- I²C (software implementation)
- I²S interfaces with DMA (sharing pins with GPIO)
- UART on dedicated pins, plus a transmit-only UART, can be enabled on GPIO2
- 10-bit ADC (successive approximation ADC)
This device has the same limitations, for first, It can’t work like a slave, but you can choice all pins you want for I2C.
As you can see in the image D1 and D2 are the default SCL and SDA pin.
Slave issue with not esp device
From 2.5.0 esp8266 core there is a support for SLAVE mode, but don’t work properly like explained here.
You can try if your chip works as a slave by adding these settings to your device.
Wire.pins(D2, D1); // Default pins you can remove It
Wire.begin(0x12); // join i2c bus with address 0x12
Wire.setClockStretchLimit(1500);
Wire.setClock(10000L); // Max 50kHz
And It’s very important you can compile at 160Mhz CPU.
I’m doing some tests, and with WeMos D1 mini (China clone), when an Arduino UNO via logic converter, I obtain this result in i2c scan address.
Scanning...
Unknow error at address 0x12
done
It finds the device but does not work properly. With Arduino MKR 1010 without a logic converter, I have this result.
Scanning...
I2C device found at address 0x25 !
I2C device found at address 0x60 !
I2C device found at address 0x6B !
done
It finds a device with an address 0x25.
Network master-slave with clock management
Here the WeMos I use for these examples WeMos D1 mini - NodeMCU V2 V2.1 V3 - esp01 - esp01 programmer
You can get some basic concepts from the Arduino article “i2c Arduino: how to create network, parameters and address scanner”.
Here some esp8266 mic on WeMos D1 mini - NodeMCU V2 V2.1 V3 - esp01 - esp01 programmer
For slave configuration, you must limit the I2C speed to MASTER and to SLAVE, the limit on direct connection with WeMos D1 mini is 40kHz, so you must add this command after beginning.
Wire.begin(); // join i2c bus (address optional for master)
Wire.setClock(40000L); // Set speed at 40kHz
To send a parameter to the slave device, you must establish a connection, then send data and alert that the communication is ended.
Wire.beginTransmission(0x12); // Start channel with slave 0x12
Wire.write(GET_NAME); // send data to the slave
Wire.endTransmission(); // End transmission
The slave must register an event to manage this operation.
Wire.onReceive(receiveEvent); // register an event handler for received data
In the function set as a parameter, you must manage the data reception, I send one-byte length instruction, so the numBytes was 1, then you must only retrieve the one-byte information and cast in the format you need and save to the main scope of the sketch.
// function that executes whenever data is received by master
// this function is registered as an event, see setup()
void receiveEvent(int numBytes) {
if (numBytes==1){
int requestVal = Wire.read();
Serial.print(F("Received request -> "));
Serial.println(requestVal);
request = static_cast<REQUEST_TYPE>(requestVal);
}else{
Serial.print(F("No parameter received!"));
}
}
Now the Master is ready to request the data It needs.
Wire.requestFrom(0x08, 14); // request 14 bytes from slave device 0x08
The slave, to the request made from master, return the relative information.
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
switch (request) {
case NONE:
Serial.println(F("Not good, no request type!"));
break;
case GET_NAME:
Wire.write("esp8266 "); // send 14 bytes to master
request = NONE;
break;
case GET_AGE:
Wire.write((byte)45); // send 1 bytes to master
request = NONE;
break;
default:
break;
}
}
Now the complete Master sketch:
/**
* i2c network: send parameter to client and receive response
* with data relative to the request
*
* by Renzo Mischianti <www.mischianti.org>
*
* https://mischianti.org
*
* esp8266 <------> esp8266
* GND GND
* D1 D1
* D2 D2
*
*/
#include <Wire.h>
enum REQUEST_TYPE {
NONE = -1,
GET_NAME = 0,
GET_AGE
};
void setup() {
Wire.begin(); // join i2c bus (address optional for master)
Wire.setClock(40000L); // Set speed at 40kHz
Serial.begin(9600); // start serial for output
while (!Serial){}
Serial.println();
Serial.println(F("Starting request!"));
Wire.beginTransmission(0x12); // Start channel with slave 0x12
Wire.write(GET_NAME); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x12, 14); // request 14 bytes from slave device 0x12
while (Wire.available()) { // slave may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}
Serial.println();
delay(1000); // added to get better Serial print
Wire.beginTransmission(0x12); // Start channel with slave 0x12
Wire.write(GET_AGE); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x12, 1); // request 1 bytes from slave device 0x12
while (Wire.available()) { // slave may send less than requested
int c = (int)Wire.read(); // receive a byte as character
Serial.println(c); // print the character
}
delay(1000); // added to get better Serial print
Serial.println();
}
void loop() {
}
And now the complete Slave sketch:
/**
* i2c network: send parameter to client and receive response
* with data relative to the request. SLAVE SKETCH
*
* by Renzo Mischianti <www.mischianti.org>
*
* https://mischianti.org
*
* esp8266 <------> esp8266
* GND GND
* D1 D1
* D2 D2
*
*/
#include <Wire.h>
enum REQUEST_TYPE {
NONE = -1,
GET_NAME = 0,
GET_AGE
};
void requestEvent();
void receiveEvent(int numBytes);
REQUEST_TYPE request = NONE;
void setup() {
// Wire.pins(D2, D1);
Wire.begin(0x12); // join i2c bus with address 0x12
// Wire.setClockStretchLimit(1500);
// Wire.setClock(50000L);
Serial.begin(9600); // start serial for output
while (!Serial){}
Serial.println(F("Starting!!"));
// event handler initializations
Wire.onReceive(receiveEvent); // register an event handler for received data
Wire.onRequest(requestEvent); // register an event handler for data requests
}
void loop() {
// delay(100);
}
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
switch (request) {
case NONE:
Serial.println(F("Not good, no request type!"));
break;
case GET_NAME:
Wire.write("esp8266 "); // send 14 bytes to master
request = NONE;
break;
case GET_AGE:
Wire.write((byte)45); // send 1 bytes to master
request = NONE;
break;
default:
break;
}
}
// function that executes whenever data is received by master
// this function is registered as an event, see setup()
void receiveEvent(int numBytes) {
if (numBytes==1){
int requestVal = Wire.read();
Serial.print(F("Received request -> "));
Serial.println(requestVal);
request = static_cast<REQUEST_TYPE>(requestVal);
}else{
Serial.print(F("No parameter received!"));
}
}
And now the Master serial output:
17:10:06: Starting request!
17:10:07: esp8266
17:10:09: 45
and the Slave serial output:
17:10:06: Received request -> 0
17:10:08: Received request -> 1
I2C address scanner
One of the useful sketches when you use i2c is the Address scanner. This simple program tries to find all devices connected to the I2C bus.
#include <Wire.h>
void setup()
{
Wire.begin();
Serial.begin(9600);
Serial.println("\nI2C Scanner");
}
void loop()
{
byte error, address;
int nDevices;
Serial.println("Scanning...");
nDevices = 0;
for(address = 1; address < 127; address++ )
{
// The i2c_scanner uses the return value of
// the Write.endTransmisstion to see if
// a device did acknowledge to the address.
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0)
{
Serial.print("I2C device found at address 0x");
if (address<16)
Serial.print("0");
Serial.print(address,HEX);
Serial.println(" !");
nDevices++;
}
else if (error==4)
{
Serial.print("Unknow error at address 0x");
if (address<16)
Serial.print("0");
Serial.println(address,HEX);
}
}
if (nDevices == 0)
Serial.println("No I2C devices found\n");
else
Serial.println("done\n");
delay(5000); // wait 5 seconds for next scan
}
Network: one master and multiple slaves (3.3v and 5v logic)
When you try to connect heterogeneous devices like an Arduino UNO you probably must modify your code.
Here the logic level converter I use for this example Aliexpress
Here the Arduino UNO Arduino UNO - Arduino MEGA 2560 R3 - Arduino Nano - Arduino Pro Mini
First, you can set the clock stretch limit and set the I2C clock to 40kHz, and very important, you are going to compile the sketch with 160MHz of processor speed.
Wire.begin(0x12); // join i2c bus with address 0x12
// ------ If you have throuble uncomment these lines --------
// ------ and compile with processor speed at 160Mhz --------
Wire.setClockStretchLimit(1500);
Wire.setClock(40000L);
// ----------------------------------------------------------
Clock Stretching
While control of the SCL line is the domain of the I2C master, an optional feature of the protocol allows slaves to temporarily control it to slow down transmission before it is ready to accept more data.
To stretch the clock, the slave device simply holds the SCL line down. In that state, the master device must wait for the clock rises back up to high before resuming transmission.
This feature is sometimes a source of trouble: not every device supports it, and a master device with no support for clock stretching will hit errors if a slave device attempts to hold the clock down.
Then the sketch becomes like the other, here the master.
/**
* i2c network: send parameter to client and receive response
* with data relative to the request
*
* by Renzo Mischianti <www.mischianti.org>
*
* https://mischianti.org
*
*
* Arduino UNO <------> Logic converter <------> esp8266 Master <------> esp8266 Slave
* GND GND GND GND GND
* 5v HV LV 3.3v
* A4 HV1 LV1 D1 D1
* A5 HV2 LV2 D2 D2
*
*/
#include <Wire.h>
enum REQUEST_TYPE {
NONE = -1,
GET_NAME = 0,
GET_AGE
};
void setup() {
Wire.begin(); // join i2c bus (address optional for master)
Wire.setClock(40000L); // Set speed at 40kHz
Serial.begin(9600); // start serial for output
while (!Serial){}
Serial.println();
Serial.println(F("Starting request!"));
Wire.beginTransmission(0x08); // Start channel with slave 0x08
Wire.write(GET_NAME); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x08, 14); // request 14 bytes from slave device 0x08
while (Wire.available()) { // slave may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}
Serial.println();
delay(1000); // added to get better Serial print
Wire.beginTransmission(0x12); // Start channel with slave 0x12
Wire.write(GET_NAME); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x12, 14); // request 14 bytes from slave device 0x12
while (Wire.available()) { // slave may send less than requested
char c = Wire.read(); // receive a byte as character
Serial.print(c); // print the character
}
Serial.println();
delay(1000); // added to get better Serial print
Wire.beginTransmission(0x08); // Start channel with slave 0x08
Wire.write(GET_AGE); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x08, 1); // request 1 bytes from slave device 0x08
while (Wire.available()) { // slave may send less than requested
int c = (int)Wire.read(); // receive a byte as character
Serial.println(c); // print the character
}
delay(1000); // added to get better Serial print
Serial.println();
delay(1000); // added to get better Serial print
Wire.beginTransmission(0x12); // Start channel with slave 0x12
Wire.write(GET_AGE); // send data to the slave
Wire.endTransmission(); // End transmission
delay(1000); // added to get better Serial print
Wire.requestFrom(0x12, 1); // request 1 bytes from slave device 0x12
while (Wire.available()) { // slave may send less than requested
int c = (int)Wire.read(); // receive a byte as character
Serial.println(c); // print the character
}
delay(1000); // added to get better Serial print
Serial.println();
}
void loop() {
}
Here the slave Arduino UNO
/**
* i2c network: send parameter to client and receive response
* with data relative to the request. SLAVE SKETCH
*
* by Renzo Mischianti <www.mischianti.org>
*
* https://mischianti.org
*
* Arduino UNO <------> Logic converter <------> esp8266 Master <------> esp8266 Slave
* GND GND GND GND GND
* 5v HV LV 3.3v
* A4 HV1 LV1 D1 D1
* A5 HV2 LV2 D2 D2
*
*/
#include <Wire.h>
enum REQUEST_TYPE {
NONE = -1,
GET_NAME = 0,
GET_AGE
};
void requestEvent();
void receiveEvent(int numBytes);
REQUEST_TYPE request = NONE;
void setup() {
Wire.begin(0x08); // join i2c bus with address #8
Serial.begin(9600); // start serial for output
while (!Serial){}
// event handler initializations
Wire.onReceive(receiveEvent); // register an event handler for received data
Wire.onRequest(requestEvent); // register an event handler for data requests
}
void loop() {
// delay(100);
}
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
switch (request) {
case NONE:
Serial.println(F("Not good, no request type!"));
break;
case GET_NAME:
Wire.write("ArduinoUNO "); // send 14 bytes to master
request = NONE;
break;
case GET_AGE:
Wire.write((byte)43); // send 1 bytes to master
request = NONE;
break;
default:
break;
}
}
// function that executes whenever data is received by master
// this function is registered as an event, see setup()
void receiveEvent(int numBytes) {
if (numBytes==1){
int requestVal = Wire.read();
Serial.print(F("Received request -> "));
Serial.println(requestVal);
request = static_cast<REQUEST_TYPE>(requestVal);
}else{
Serial.print(F("No parameter received!"));
}
}
And you can retrieve the previous WeMos D1 mini slave Sketch.
If you try to launch the I2C scanner from the master (after the slave configuration), you obtain this result.
22:33:55: Scanning...
22:33:55: I2C device found at address 0x08 !
22:33:55: I2C device found at address 0x12 !
22:33:55: done
With the Master parameter sketch you obtain this serial output.
22:51:28: Starting request!
22:51:29: ArduinoUNO
22:51:31: esp8266
22:51:33: 43
22:51:36: 45
Simple network with non-standard I2C pin
With esp8266, you can’t add a new I2C interface, but you can use almost all available pins to the standard channel, so if you cant use D6 as SDA and D5 as SCL you must add this command before beginning.
Wire.pins(D6, D5);
The sketch becomes like this.
/**
* i2c network: send parameter to client and receive response
* with data relative to the request. SLAVE SKETCH
*
* by Renzo Mischianti <www.mischianti.org>
*
* https://mischianti.org
*
* esp8266 <------> esp8266
* GND GND
* D1 D5
* D2 D6
*
*/
#include <Wire.h>
enum REQUEST_TYPE {
NONE = -1,
GET_NAME = 0,
GET_AGE
};
void requestEvent();
void receiveEvent(int numBytes);
REQUEST_TYPE request = NONE;
void setup() {
Wire.pins(D6, D5);
Wire.begin(0x12); // join i2c bus with address 0x12
// ------ If you have throuble uncomment these lines --------
// ------ and compile with processor speed at 160Mhz --------
Wire.setClockStretchLimit(1500);
Wire.setClock(40000L);
// ----------------------------------------------------------
Serial.begin(9600); // start serial for output
while (!Serial){}
Serial.println(F("Starting!!"));
// event handler initializations
Wire.onReceive(receiveEvent); // register an event handler for received data
Wire.onRequest(requestEvent); // register an event handler for data requests
}
void loop() {
// delay(100);
}
// function that executes whenever data is requested by master
// this function is registered as an event, see setup()
void requestEvent() {
switch (request) {
case NONE:
Serial.println(F("Not good, no request type!"));
break;
case GET_NAME:
Wire.write("esp8266 "); // send 14 bytes to master
request = NONE;
break;
case GET_AGE:
Wire.write((byte)45); // send 1 bytes to master
request = NONE;
break;
default:
break;
}
}
// function that executes whenever data is received by master
// this function is registered as an event, see setup()
void receiveEvent(int numBytes) {
if (numBytes==1){
int requestVal = Wire.read();
Serial.print(F("Received request -> "));
Serial.println(requestVal);
request = static_cast<REQUEST_TYPE>(requestVal);
}else{
Serial.print(F("No parameter received!"));
}
}
Thanks
- WeMos D1 mini (esp8266), specs and IDE configuration
- WeMos D1 mini (esp8266), integrated SPIFFS Filesystem
- WeMos D1 mini (esp8266), debug on secondary UART
- WeMos D1 mini (esp8266), the three type of sleep mode to manage energy savings
- WeMos D1 mini (esp8266), integrated LittleFS Filesystem
- esp12 esp07 (esp8266): flash, pinout, specs and IDE configuration
- Firmware and OTA update management
- Firmware management
- OTA update with Arduino IDE
- OTA update with Web Browser
- Self OTA uptate from HTTP server
- Non standard Firmware update
- esp32 and esp8266: FAT filesystem on external SPI flash memory
- i2c esp8266: how to, network 5v, 3.3v, speed, and custom pins
- […]
- i2c Arduino: how to create a network, parameters, and address scanner
- i2c Arduino SAMD MKR: additional interface SERCOM, network, and address scanner
- i2c esp8266: how to, network 5v, 3.3v, speed, and address scanner