Arduino and the I2C/TWI protocol

I2C/TWI is a low speed serial communication protocol. An I2C network consists of one or more master devices and one or more slave devices. The I2C protocol is not recommended for long-distance data transmission.

The I2C/TWI bus consists of two wires and their associated pull-up resistors. All I2C devices are connected to this pair of wires. The two wires are SDA and SCL.

These may be necessary for learning:

advertising – Amazon.com

The value of the pull-up resistors depends on the voltage of the I2C bus, its length and the number of devices on the bus. The value of the resistor should be between 1K and 10K, as a rule of thumb, use resistors with a value of 4.7k. If the I2C bus is long or more devices are connected to the bus, or the bus voltage is lower, we can reduce the value of the pull-up resistors.

The SDA data line is used to send current data back and forth. The SCL line carries the clock signal used for communication timing. The clock signal is always generated by the current master.

arduino i2c/twi bus

I2C devices can operate at different voltage levels. I2C devices operating at 3.3V can be damaged when connected to the Arduino. In such a case, a level adjustment circuit must be used.

arduino i2c/twi bus with level adjustment circuit

Master devices can send and receive data. The slave devices respond to the master’s request. When sending data on the I2C bus, only one device can send data at a time. Each slave device must have a unique I2C address that identifies it on the I2C bus. An I2C address allows the master to send data on the bus to a specific slave device.

In the table below you can see where the I2C/TWI pins are located on each Arduino board.

BoardsI2C/TWI pins
UNO, NanoA4 (SDA), A5 (SCL)
Mega256020 (SDA), 21 (SCL)
Leonardo 2 (SDA), 3 (SCL)

Excellent tools for practice:

To use the I2C bus, we need the Wire library, this must be added at the beginning of the Arduino sketch as shown below.

#include <Wire.h>

The Wire library contains the following functions. Let’s take them one by one.

The Wire.begin() function introduces the Wire library and starts communication. Devices are connected to the I2C bus as controllers (Master) or peripherals (Slave). Each slave device must have a 7-bit address that is identified by the master on the I2C bus. The optional parameter is the address of the slave device. If this parameter is not specified, the device is connected to the bus as a master. Optionally, the master device can also have an address.

#include "Wire.h"

void setup()
{
  Wire.begin(); // If there is no parameter, the device is connected as a master
}

void loop()
{
  
}
Wire.begin(0x90);   // I2C address

Back to table of contents.

Wire.end() stops I2C communication and disables the Wire library. If we want to use the Wire library again, we need to call the Wire.begin() function again. The Wire.end() function was not available in the original version of the Wire library and may not work in some versions.

Back to table of contents.

The Wire.beginTransmission() function starts the writing process. It has an important parameter, the address of the slave device with which the master wants to talk on the I2C bus. Before the master writes, we send the address of the slave device using Wire.beginTransmission(). If the slave device is present on the I2C bus, writing can begin. Wire.beginTransmission() is followed by Wire.write() and then Wire.endTransmission() .

Wire.beginTransmission(address);

Back to table of contents.

The Wire.write() function writes data on the I2C bus. Writes data from a slave device at the request of the master device, or queues bytes for transmission on the master to send to the slave device. The Wire.write() function can have one or two parameters.

Wire.write(value);
Wire.write(string);
Wire.write(data, length);

The value of the value parameter is a single byte. The string parameter is a character string. The data parameter is the array of bytes to be sent, the length parameter is the number of bytes in the data array.

#include <Wire.h>

byte mybyte = 0;

char mystring[] = "mystring";
char mychar[] = {'a','b','c'};

void setup()
{
  Wire.begin();
}

void loop()
{
  Wire.beginTransmission(6);
  Wire.write(mybyte);

  Wire.write(mystring);
  Wire.write(mychar, sizeof(mychar));  
  Wire.endTransmission();

  delay(1000);
}

Before the master writes to the slave device, we start communication using Wire.beginTransmission() . Wire.write() function stores/queues the data bytes to be sent in a buffer. The size of the buffer is 32 bytes. If the data to be sent does not fit in the buffer, it causes a transmission error.

On the master device, Wire.endTransmission()  sends the contents of the buffer and then stops the write process.

In order for the slave device to be able to send, it must wait for a request from the master. The Wire library includes a function for this purpose, Wire.onRequest() .

Back to table of contents.

To practice:

SparkFun ESP32 WROOM with ESP32-D0WDQ6 chip

SparkFun ESP32 WROOM with ESP32-D0WDQ6 chip

Xtensa dual-core 32-bit LX6 microprocessor Up to 240MHz clock frequency, Integrated 802.11 BGN WiFi, 21 GPIO 8-electrode capacitive touch support

advertising

Wire.endTransmission() on the master device sends the contents of the buffer stored by the Wire.write() function and then stops the writing process.

Wire.endTransmission();

The Wire.endTransmission() function may have an optional parameter.

Wire.endTransmission(bool stop);

If the parameter is true , Wire.endTransmission() sends a stop message after the transmission, releasing the I2C bus. true is the default value.

If the parameter is false , Wire.endTransmission() sends a restart message after the transmission, so the I2C bus remains busy. This allows the current master device to send additional messages and prevents another master from taking over the I2C bus.

The return value of the Wire.endTransmission() function can be useful, for example, when debugging. these values ​​can be:

If the return value is “ 0 ”, it means the transfer was successful. “ 1 ” indicates that the data is too long to fit in the transfer buffer.

The value “ 2 ” is the received NACK when the slave address is transmitted. An acknowledgment bit is sent after every sent byte. This AKC/NACK bit is used by the slave device to indicate whether it has successfully received the previous byte. After sending the slave address, when the master sends the address of the slave it wants to communicate with, a slave that recognizes its address sends an ACK. This tells the master that the slave you want to reach is actually on the bus. If no slave device recognizes the address, the result is NACK. In this case, the master interrupts communication because there is no one to talk to.

When the return value is “3”, the received NACK indicates a data transmission error. If the slave device successfully received the previous byte, it sends an ACK. A value of “4” means another error. The value “5” is received in case of timeout.

byte error = Wire.endTransmission();
switch (error) 
{
  case 1:
    Serial.println("Success");
    break;
  case 2:
    Serial.println("Data too long");
    break;
  case 3:
    Serial.println("NACK on transmit of address");
    break;
  case 4:
    Serial.println("NACK on transmit of data");
    break;
  case 5:
    Serial.println("Timeout");
    break;

}

Back to table of contents.

The Wire.requestFrom() function is used by the Master device to request bytes from a Slave device. The bytes can then be retrieved using the Wire.available() and Wire.read() functions. The Wire.requestFrom() function has at least two parameters.

Wire.requestFrom(address, quantity);

The first parameter is address , the 7-bit address of the slave device from which the master device requests bytes. The second parameter is quantity, which specifies the number of bytes that can be requested.

Optionally, we can specify a third parameter.

Wire.requestFrom(address, quantity, stop);

The third parameter is stop . Its value can be true or false . The default value is true. If the value of the stop parameter is true, requestFrom() sends a stop message after the request, releasing the I2C bus.

If the stop parameter of requestFrom() is false, a restart message is sent after the request and the I2C bus is not freed, so the connection remains active. This prevents another master device from initiating communication until the current master completes the request.

Back to table of contents.

The Wire.read() function reads the byte requested by the master device from a peripheral device using the Wire.requestFrom() function.

char mychar = Wire.read();

Back to table of contents.

The Wire.available() function returns the number of bytes that can be retrieved by the Wire.read() function. The Wire.available() function on the master device after the Wire.requestFrom() function…

#include <Wire.h>

void setup()
{
  Wire.begin();
  Serial.begin(9600);
}

void loop()
{
  Wire.requestFrom(0x90, 5);
  while (Wire.available())
  {
    char mychar = Wire.read();
    Serial.print(mychar);
  }
  delay(1000);
}

… or it must be called within the event handler function registered by Wire.onReceive() on the slave device.

Back to table of contents.

The Wire.onReceive() function registers an event handler function to be called when a slave device receives a request from a master device. The Wire.onReceive() function has one parameter.

Wire.onReceive(handler);

The handler parameter is the event handler function that must be called when the slave device receives data. The Wire.available() function is called in this event handler .


#include <Wire.h>

void setup()
{
  Wire.begin(0x90);
  Wire.onReceive(myHandler);
  Serial.begin(9600);
}

void loop() 
{
  delay(1000);
}

void myHandler(readbytes)
{
  while (Wire.available())
  {
    char mychar = Wire.read();
    Serial.print(mychar);
  }
}

Back to table of contents.

The Wire.onRequest() function registers an event handler function on the slave device, this event handler is executed when the master device requests data.

#include <Wire.h>

void setup()
{
  Wire.begin(0x90);
  Wire.onRequest(myRequestHandler);
}

void loop()
{
  delay(1000);
}

void myRequestHandler()
{
  Wire.write("from slave");
}

Back to table of contents.

The Wire.setClock() function changes the clock frequency of the I2C communication. The Wire.setClock() function has one parameter, clockFrequency . This parameter is the value of the desired communication clock (in hertz).

Wire.setClock(clockFrequency);

The accepted values ​​are: 100000 Hz (normal mode) and 400000 Hz (fast mode). Some devices also support 10000 Hz (low speed mode), 1000000 Hz (fast mode plus) and 3400000 Hz (high speed mode).

Back to table of contents.

ARDUINO MEGA 2560 REV3
ARDUINO MEGA 2560 REV3 – advertising

The Wire.setWireTimeout() function sets the communication timeout in master mode. The Wire.setWireTimeout() function sets the default timeout without parameters.

Wire.setWireTimeout();

We can give two parameters to the Wire.setWireTimeout() function.

Wire.setWireTimeout(timeout, reset_on_timeout);

The “ timeout ” parameter is the timeout value in microseconds, if it is zero, the timeout check is disabled. If the ” reset_on_timeout ” parameter is true, the I2C hardware automatically resets upon timeout and restarts communication on the I2C bus.

If there is an error on the I2C bus, freezes and other anomalies may occur during I2C communication. This feature can be useful when testing the I2C bus.

Back to table of contents.

If the timeout occurs, a flag is set that can be queried using the Wire.getWireTimeoutFlag() function. The flag can be cleared manually with the Wire.clearWireTimeoutFlag() function.

The flag is also cleared when the Wire.setWireTimeout() function is called.

bool myFlag = Wire.getWireTimeoutFlag();

Back to table of contents.

A call to Wire.clearWireTimeoutFlag() clears the timeout flag and returns the current value of the flag.

bool myFlag = Wire.clearWireTimeoutFlag();

Back to table of contents.

Let’s see a simple example of using the I2C bus. Let’s connect a PCF8591 ADC/DAC module to the Arduino board. The resolution of the ADC and the DAC is 8 bits, the value can be 0-255.

The module in the example below has an LDR light sensor on board, this is ADC0. channel; a thermistor, ADC1. channel; and a potentiometer, ADC3. channel; to test the inputs. The ADC2 channel does not contain a peripheral, it is only output to the header. An LED was connected to the analog output. Inputs and output can be used for other purposes. The module includes two 10K pull-up resistors for the I2C bus.

connect a PCF8591 ADC/DAC module to the Arduino board

The following example code shows testing the DAC channel. We can see the result in the LED on board PCF8591.

#include "Wire.h"


const char PCF8591 (0x48);
 
void setup()
{
  Wire.begin();
}
 
void loop()
{  
  for(int i=0; i<256; i++) 
  { 
    Wire.beginTransmission(PCF8591);
    Wire.write(0x40);             // control byte - Start DAC
    Wire.write(i);                // Value to send to DAC 
    Wire.endTransmission();
    delay(15);
  }
  
  for(int i=255; i>=0; --i) 
  { 
    Wire.beginTransmission(PCF8591);
    Wire.write(0x40);
    Wire.write(i);
    Wire.endTransmission(); 
    delay(15);
  } 
}

With the second example, we can test the inputs. upload it to the Arduino UNO and then open the serial monitor…

#include "Wire.h"


const char PCF8591 (0x48);
byte adcvalue0, adcvalue1, adcvalue2, adcvalue3;
 
void setup()
{
  Wire.begin();
  Serial.begin(9600);
}
 
void loop()
{
  Wire.beginTransmission(PCF8591);
  Wire.write(0x04);
  Wire.endTransmission();
  Wire.requestFrom(PCF8591, 5);   // 5 byte data request from PCF8591
 
  adcvalue0=Wire.read();          // previous measurement, outdated data.
  adcvalue0=Wire.read();          // the second data reading is a fresh measurement
  adcvalue1=Wire.read();
  adcvalue2=Wire.read();
  adcvalue3=Wire.read();
 
  Serial.print(adcvalue0);        // LDR
  Serial.print(", ");
  Serial.print(adcvalue1);        // thermistor
  Serial.print(", ");
  Serial.print(adcvalue2);        // empty
  Serial.print(", ");
  Serial.print(adcvalue3);        // potmeter
  Serial.println();
 
  delay(500);
}
UNO R3 board ATmega 328 with usb cable
UNO R3 board ATmega 328 with usb cable from amazon(Ad)