Software I2C Multiplexer

The featured image of this post is by Thomas from Pixabay.

What can you do to connect many I2C devices with the same device address to an MCU? Most often, the advice is to use hardware solutions. Here, we will look at how to solve the problem with software employing the FlexWire library.

Motivation

Most I2C devices have a fixed device address. So, you can connect only one such device to your I2C bus. If you want to connect more than one I2C device of the same type, you have the following options:

  1. use a hardware I2C multiplexer such as TCA9548A or PCA9546;
  2. use different I2C interfaces on your MCU, if this is supported;
  3. use different software I2C communication class instances;
  4. use a software I2C class and switch to different SDA lines.

Hardware solutions

Adding an I2C multiplexer, as described in this Adafruit tutorial, is conceptually the easiest solution. However, you need, of course, additional hardware, which one may try to avoid.

If the MCU supports more than one I2C interface, as the ESP32 does, then one can connect the sensors to different I2C buses, as described in this Random Nerd Tutorial. However, this works only if you have such an MCU and the number of I2C devices is not higher than the number of supported buses (usually 2).

Software solutions

In most cases, when we want to connect I2C devices with identical device addresses, there will be some unused GPIOs. These could be used to implement multiple I2C buses, which can be driven by bit-banging I2C libraries, such as SoftWire, SoftwareWire, SlowSoftWire, or FlexWire. These libraries all support multiple instances of the respective classes with different data pins.

Provided you program the I2C interaction by yourself, or you are willing to modify the library that implements the I2C protocol for the particular device, then you could use any of the above-mentioned libraries. If you want to use the device library unchanged and replace the Wire library with a software I2C library, then you run into the problems described in my previous blog post. The FlexWire library is the only library that allows the replacement of the Wire library on a per-sketch basis. You simply include this library as the first one in your sketch, after which the original Wire library will be ignored.

Let us illustrate this by showing how to connect two HUT21D humidity and temperature sensors to an Arduino UNO. Connecting the sensor breakout boards is straightforward. The only point you should be aware of is that the boards need a 3.3 V supply and that the open drain data lines should also be powered this way.

Using the device library SparkFunHUT21D library, one can write the following sketch, which initializes both devices and then loops, reading out both sensors. The sketch can be easily extended to read out more sensors.

#include <FlexWire.h>
#include <SparkFunHTU21D.h>

#define MAXSENSOR 2

// The pins that we use for the I2C buses
uint8_t sdapin[MAXSENSOR] = { 2, 4 };
uint8_t sclpin[MAXSENSOR] = { 3, 5 };

// Array of Flexwire instances
FlexWire wire[MAXSENSOR] = { {sdapin[0], sclpin[0]}, {sdapin[1], sclpin[1]} }; 

// Create array of instances of the HTU21D class
HTU21D htu[MAXSENSOR];

void setup()
{
  Serial.begin(9600);
  Serial.println(F("Multi-I2C example with HTU21D"));
  for (uint8_t i=0; i < MAXSENSOR; i++) htu[i].begin(wire[i]);
}

void loop()
{
  for (uint8_t i=0; i < MAXSENSOR; i++) {
    Serial.print(F("Sensor "));
    Serial.print(i+1);
    Serial.print(F(": "));
    Serial.print(htu[i].readTemperature(), 1);
    Serial.println("C");
  }
  Serial.println();
  delay(1000);
}

The sdapin and sclpin arrays store the GPIO pins that are used for the different I2C buses. The values are in turn used for initializing the wire array that contains all the FlexWire instances. While we could have inserted the values for initializing the sdapin and sclpin arrays directly into the initializing statement for the wire array, further down, it will become clear, why we chose this more indirect way. Finally, the htu array then contains all the HTU21D instances.

In the setup procedure, all the begin methods for the different HTU21D instances are called. In the loop procedure, we call the readTemperature method for all the HTU21D instances stored in the htu array.

Sharing the SCL line

The above solution works perfectly. However, if we are short on GPIOs, there may be the question of whether one could reduce the number of needed GPIOs. In fact, it seems quite possible to share the SCL line. The reason is that an I2C device only reacts if there is a so-called start condition on the bus, and the device address is sent afterward. When the data line is inactive while there is a clock signal, there is neither a start condition nor will there be a valid address transmitted on the bus, and the I2C device will stay inactive. In other words, we can use a common SCL line for all the I2C buses, provided only one bus is active at each time point. The latter is guaranteed with our setup.

The sharing of the SCL line can be easily accomplished by having only one connection from the UNO board in the circuit above from D3 (white line) to both of the SCL pins of the sensor breakout boards. In the sketch we simply have to change two lines as follows:

...

// The pins are we are going to use for the I2C buses
...
const uint8_t sclpin = 3;

// Array of Flexwire instances
FlexWire wire[MAXSENSOR] = { {sdapin[0], sclpin}, {sdapin[1], sclpin} }; 

...

Switching the SDA line

Finally, one may ask why we have one FlexWire instance per I2C bus. Since only one bus can be active at each time point, and since there is no state information except for the SDA pin which needs to be kept, one FlexWire instance should be enough. Indeed, when looking at how a hardware multiplexer is used, where we simply use one Wire instance and connect to different I2C devices, it appears to be possible to get rid of multiple FlexWire instances. We only need to switch the SDA line. This will save at least 32 bytes of RAM for the input buffer per FlexWire instance. I have added the method setPins to the FlexWire class, which sets the SDA and SCL pins. This is very compatible with the ESP32 implementation of the Wire library.

And we need in this case, of course, only one HTU21D instance. So, the sketch needs to be modified as follows.

...

// Flexwire instance
FlexWire Wire;

// HTUD21 instance
HTUD21 htu;
...

void setup()
{
  ...
  for (uint8_t i=0; i < MAXSENSOR; i++) {
    Wire.setsda(sdapin[i], sclpin);
    htu.begin();
  }
}

void loop()
{
  for (uint8_t i=0; i < MAXSENSOR; i++) {
    ...
    Wire.setPins(sdapin[i], sclpin);
    Serial.print(htu.readTemperature(), 1);
    ...
  }
  ...
}

Summary

If you need to connect several I2C devices with identical device addresses to your MCU, and you do not want to buy additional hardware, but you have a few GPIOs to spare, and communication speed is not of main importance, then you can use the FlexWire library to implement a software I2C multiplexer. Switching between different I2C buses is as easy as it is with a hardware multiplexer. Moreover, you do not need to modify the device library at all, because the FlexWire library is a drop-in replacement for (the master-mode part of) the Wire library.

Views: 212

Categories: Library, Programming, Tutorial

2 Comments

  1. Hi, I am trying to do the same thing using two Adafruit SHT-45 Sensors which have a default I2C address of 0x44. In the HTU21 Example, the only part I am having trouble understanding is line 14, which is creating Instances ‘HTU21D htu’. How do I work out how to do this using the SHT-45?

    • Assuming that you use the Adafruit library, you open up one of the example scripts in the library folder (e.g. SH4test.ino) and find out, what the class name of the sensor class is. In this case, it is Adafruit_SHT4x. This means that the line should look like
      Adafruit_AHT4x sht4[MAXSENSOR];

      Further down, you should replace all occurrences of htu by sht4, of course.

      I hope that helps!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Copyright © 2024 Arduino Craft Corner

Theme by Anders NorenUp ↑