Connecting 3rd-Party I2C Devices#

This guide is for those who wish to interface third-party I2C peripherals. Example Arduino code for these devices will work with EVN Alpha, but requires some code modifications.

For those who want to get straight to the point, skip here.

How I2C Works#

I2C uses a shared bus, where multiple peripherals are connected to the same 2 pins on the host: SDA (Serial Data) and SCL (Serial Clock).

It’s a serial bus since bits are sent one after another, similar to USB (Universal Serial Bus). In that sense, the Arduino terminology of Serial and what we call “Serial Ports” is a bit reductive, as it actually refers to Serial UART (Universal Asynchronous Receive/Transmit).

The I2C host (EVN Alpha in this case) identifies each peripheral on the bus by its I2C address.

Think of the I2C address as a name. The host calls for a specific peripheral’s address to start a transmission. That peripheral responds and either transmits or receives data. Meanwhile, the other peripherals ignore everything until that transmission ends, as their “names” have not been called.

Why Modifications Are Needed#

The trouble arises when multiple devices with the same address are connected to the same bus. If 2 (or more) devices try to transmit data at the same time, nothing works.

There are some solutions for this:

  1. Some peripherals expose a pin you can use to enable/disable them. To communicate with a given peripheral, we can disable all other peripherals with the same address using IO pins.

  2. Some peripherals have pins or pads that you can solder together to change their I2C address, so there are no address clashes.

But these workarounds require peripherals to support those functions (not all do), along with additional IO pins or soldering irons.

Hence, we decided on this:

  1. Use an I2C multiplexer. I2C peripherals are connected to the multiplexer (instead of directly to the bus), and the multiplexer is connected to the I2C bus. The multiplexer acts like a switch, able to control which I2C peripheral is visible on the shared bus.

It’s similar to solution #1, except that we use the multiplexer and I2C commands to make other peripherals invisible instead of digital pins.

EVN Alpha uses an 8-channel TCA9548A multiplexer on both I2C buses, which represent I2C ports 1-16.

Our I2C communications process has now changed from:

  • Call for peripheral’s I2C address to start transmission

  • Transmit / Request for data from peripheral

  • End transmission

To:

  • Call for multiplexer’s I2C address to start transmission

  • Transmit command to multiplexer to make peripheral on desired port (e.g. port 1) visible on I2C bus (peripherals on the multiplexer’s other ports are made invisible)

  • End transmission

  • Call for peripheral’s I2C address to start transmission

  • Transmit / request for data from peripheral

  • End transmission

Long story short, there is an additional I2C transmission to make before interacting with the peripheral normally. This is the modification we must make.

Making Changes to Code#

For this guide, we will use example code for DFRobot’s HuskyLens. This is lifted from their wiki.

#include "HUSKYLENS.h"
#include "SoftwareSerial.h"

HUSKYLENS huskylens;
//HUSKYLENS green line >> SDA; blue line >> SCL
void printResult(HUSKYLENSResult result);

void setup() {
    Serial.begin(115200);
    Wire.begin();
    while (!huskylens.begin(Wire))
    {
        Serial.println(F("Begin failed!"));
        Serial.println(F("1.Please recheck the \"Protocol Type\" in HUSKYLENS (General Settings>>Protocol Type>>I2C)"));
        Serial.println(F("2.Please recheck the connection."));
        delay(100);
    }
}

void loop() {
    if (!huskylens.request()) Serial.println(F("Fail to request data from HUSKYLENS, recheck the connection!"));
    else if(!huskylens.isLearned()) Serial.println(F("Nothing learned, press learn button on HUSKYLENS to learn one!"));
    else if(!huskylens.available()) Serial.println(F("No block or arrow appears on the screen!"));
    else
    {
        Serial.println(F("###########"));
        while (huskylens.available())
        {
            HUSKYLENSResult result = huskylens.read();
            printResult(result);
        }
    }
}

void printResult(HUSKYLENSResult result){
    if (result.command == COMMAND_RETURN_BLOCK){
        Serial.println(String()+F("Block:xCenter=")+result.xCenter+F(",yCenter=")+result.yCenter+F(",width=")+result.width+F(",height=")+result.height+F(",ID=")+result.ID);
    }
    else if (result.command == COMMAND_RETURN_ARROW){
        Serial.println(String()+F("Arrow:xOrigin=")+result.xOrigin+F(",yOrigin=")+result.yOrigin+F(",xTarget=")+result.xTarget+F(",yTarget=")+result.yTarget+F(",ID=")+result.ID);
    }
    else{
        Serial.println("Object unknown!");
    }
}

Here are the following changes:

  • Include the EVN library header (if not already included)

  • Declare an EVNAlpha object

  • Initialize the EVNAlpha object in void setup()

  • Call board.setPort(the peripheral's I2C port) whenever we are about to begin, read or write to the HuskyLens. For this example, let’s assume the HuskyLens is connected to I2C Port 1.

These 4 changes actually amount to just 5 lines, which have been labelled with //EVN modification.

#include "HUSKYLENS.h"
#include "SoftwareSerial.h"
#include "EVN.h"    //EVN modification

HUSKYLENS huskylens;
//HUSKYLENS green line >> SDA; blue line >> SCL

EVNAlpha board;     //EVN modification

void printResult(HUSKYLENSResult result);

void setup() {
    board.begin();  //EVN modification
    Serial.begin(115200);
    Wire.begin();

    board.setPort(1);      //EVN modification
    while (!huskylens.begin(Wire))
    {
        Serial.println(F("Begin failed!"));
        Serial.println(F("1.Please recheck the \"Protocol Type\" in HUSKYLENS (General Settings>>Protocol Type>>I2C)"));
        Serial.println(F("2.Please recheck the connection."));
        delay(100);
    }
}

void loop() {
    board.setPort(1);   //EVN modification
    if (!huskylens.request()) Serial.println(F("Fail to request data from HUSKYLENS, recheck the connection!"));
    else if(!huskylens.isLearned()) Serial.println(F("Nothing learned, press learn button on HUSKYLENS to learn one!"));
    else if(!huskylens.available()) Serial.println(F("No block or arrow appears on the screen!"));
    else
    {
        Serial.println(F("###########"));
        while (huskylens.available())
        {
            HUSKYLENSResult result = huskylens.read();
            printResult(result);
        }
    }
}

void printResult(HUSKYLENSResult result){
    if (result.command == COMMAND_RETURN_BLOCK){
        Serial.println(String()+F("Block:xCenter=")+result.xCenter+F(",yCenter=")+result.yCenter+F(",width=")+result.width+F(",height=")+result.height+F(",ID=")+result.ID);
    }
    else if (result.command == COMMAND_RETURN_ARROW){
        Serial.println(String()+F("Arrow:xOrigin=")+result.xOrigin+F(",yOrigin=")+result.yOrigin+F(",xTarget=")+result.xTarget+F(",yTarget=")+result.yTarget+F(",ID=")+result.ID);
    }
    else{
        Serial.println("Object unknown!");
    }
}

That’s it! 5 additional lines of code.

As long as you always call board.setPort(port_number) before you communicate with the peripheral, everything should work just fine.

Final Notes and Limitations#

Here are some final things to keep note of when getting your third-party devices to work:

  • I2C ports 9-16 are connected to the I2C1 bus, controlled using the Wire1 class in Arduino. Your third-party library will have to be set to use Wire1 instead of Wire for these ports.

  • The multiplexer allows for 2 peripherals with the same address to be connected at the same time, but no connected peripherals can clash with the multiplexer’s I2C address, which is 0x70.

  • EVN libraries have port selection built-in, so you do not need to call board.setPort(port_number) for Standard Peripherals/battery voltage measurement. However, the ports are not de-selected, so after using an EVN library function you must call board.setPort(port_number) again to use your third-party peripheral.

  • The multiplexers are only rated for an I2C frequency of up to 400kHz. Higher than that, your mileage may vary.

  • If you wish, you may control the multiplexer using Wire functions instead of board.setPort(port_number). However, this may break functionality with the EVN libraries, and board.setPort(port_number) is optimized to avoid sending unnecessary I2C commands if the correct port is already selected, so we strongly recommend using it.