Skip to content

Arduino in Target mode

Koepel edited this page Jan 31, 2025 · 5 revisions

Not all Arduino boards support the I2C Target mode. The Arduino boards and compatible boards that support the I2C Target mode might have issues. Not every combination of Controller and Target might work.

The I2C bus is not the best bus to transfer data between processors. Serial/UART communication is easier and often better.

In Target mode the Arduino board can receive data or respond to a request. The Target mode for a Raspberry Pi makes use of the multitasking system and is reliable. The Target mode for the ESP32 gets better with newer variants.

Receive data

If the Controller sends data:

Wire.beginTransmission( i2c_target_address);
Wire.write( data);
Wire.endTransmission();

then the Target can receive that data with the 'onReceive' handler.

void setup()
{
  Wire.begin( i2c_target_address);
  Wire.onReceive( receiveEvent);
}

void receiveEvent(int howMany)
{
  byte data = Wire.read();
}

The receiveEvent() function is called from a interrupt routine, therefor it should be as short and as fast as possible.

Since the I2C bus uses packages of data, the code is easier when the packages have a fixed length, for example a 'struct'. In the receiveEvent() function a check can be done if the right amount of data was received.

// Suppose the Controller has send 4 bytes
void receiveEvent(int howMany)
{
  byte data[4];

  if( howMany == 4)
  {
    for( int i=0; i<4; i++)
    {
      data[i] = Wire.read();
    }
  }
}

To keep the receiveEvent() function as short as possible, the received data is often processed in the loop(). The data can be passed on to the loop() with a global buffer for the data and a 'bool' flag. Those variables should be 'volatile' to tell the compiler that the variables can be changed in a interrupt. The compiler will then adapt the optimizations for the code in the loop().


Respond to a request

If the Controller requests data:

Wire.requestFrom( i2c_target_address, number_of_bytes);

then the Target runs the 'onRequest' handler.

void setup()
{
  Wire.begin( i2c_target_address);
  Wire.onRequest( requestEvent);
}

void requestEvent()
{
  byte data = 0x55;
  Wire.write( data);
}

The requestEvent() function is called from a interrupt routine, therefor it should be as short and as fast as possible.

While the requestEvent() function runs, the Target keeps the SCL line low to put the Controller on hold. That is called: clock pulse stretching.

The Controller calls Wire.requestFrom() to request a certain amount of bytes. The Target can return a number of bytes, but that does not have to be the same amount. It can be more or less than requested. The Controller can not even tell if it did receive the right amount of bytes. That is the result of the I2C protocol. The return value of Wire.requestFrom() is not the number of bytes that the Target has sent. The number is zero when there was a bus error.

I prefer to check if everything was okay, and then read all the data. The Controller still does not know that the Target has sent the right amount of bytes.

int n = Wire.requestFrom( i2c_target_address, number_of_bytes);
if( n == number_of_bytes)
{
  // read the bytes
}

Interfering libraries

The Arduino as a I2C Target should respond as fast as possible. There are however libraries that turn off the interrupts for some time (Neopixel, FastLED, DHT, OneWire, and more) or use interrupts intensively themself (SoftwareSerial, VirtualWire, RadioHead in RH_ASK mode, and more). That will cause a problem if the there is a lot I2C communication.


A Target can be a Controller as well

When a Arduino board is set in Target mode, it can be a Controller as well. All the functions for the Controller can be used. However, two Controllers on the I2C bus is bound to go wrong. It is better to stay out of trouble and have only one Controller on the I2C bus.


Give the Target "time to breathe"

It is demanding when a basic 16MHz Arduino board is set as a I2C Target. Even if the bus speed is only 100kHz and the onRequest and onReceive handlers are short and fast. When the Controller continuously pounds the Target with I2C sessions, then the code in the Target Arduino will not run properly anymore. A delay of a few milliseconds between the I2C sessions is enough for the Target to keep up.


Emulate a device or act as a Person in the Middle

When an Arduino is set in Target mode, it could emulate a sensor or a EEPROM. It would even be possible to insert an Arduino board between a Controller and a Target and be a person-in-the-middle. In almost all cases this will not work. The existing Controller must allow clock pulse stretching and there can be timing issues or many other problems.

To name a few:

  1. The Controller should have the same voltage levels as the Target.
    The voltage level of the Controller can be determined by measuring the voltage of SDA and SCL when the I2C bus is not busy. Sometimes a device is 5V tolerant and sometimes a lower voltage level is no problem, but is also possible to damage a I2C device with wrong voltage levels on the bus.
  2. The Controller should allow clock pulse stretching.
    A Arduino as a I2C Target device uses clock pulse stretching because the I2C interface uses both hardware and software. If the Controller is also a Arduino device and uses the Wire library, then it supports clock pulse stretching.
  3. The Controller should have a open-drain (or open-collector) output for SCL.
    Since the Arduino as a I2C Target pulls the SCL low for clock pulse stretching, the SCL signal of the Controller should allow that. If the Controller has a SCL signal that is a strong signal for both high and low levels, then a short circuit will occur if a Target pulls the SCL low.
  4. The Controller should behave according to the I2C standard.
    Some Controllers use a software implementation for the I2C signals. Those are sometimes not according to the I2C standard. It might work for an existing situation, but causes problems as soon as something else is connected to the I2C bus.