Using Arduino on the RP2040: I2C

7 minute read

Where I explore how to use the Pico’s abundance of I2C interfaces to create 2 separate I2C instances.

Sources

Introduction

I have been using the Arduino framework and legacy IDE (1.8.19, not the 2.0 version) to develop code on the Raspberry Pi Pico. The Pico is an incredible board, well worth using due to its outstanding price/performance ratio. For only $4, you can buy a very powerful, very well documented and easily available microcontroller. With the addition of the Arduino framework, it makes using the Pico a simple decision for a lot of Uno projects..

That said, with the Pico’s power, comes a bit of complexity. One of the first issues, I needed to explore was to understand how to use the Pico’s 10 I2C interfaces, given the following issues with Arduino Wire:

  1. There can only be two Wire interfaces, Wire and Wire1
  2. All devices on a Wire/Wire1 interface needs to have its own address

The solution to issue 2 is to be able to set the address on each one of your I2C devices. However, some devices appear to be more difficult than others as to setting the address. The Adafruit IR Temperature Sensor is a good example. While I have found some conflicting advice…Adafruit states “I2C interface, 0x5A is the fixed 7-bit address.”

I would rather spend my time trying to understand how to program the Pico, than try to 1) correct Adafruit and 2) try to understand how to program a temperature sensor, so I went about exploring the I2C documentation and the Pico itself.

Shout out to Kevin Gong, who did the initial research and planted the seeds of the two ideas which I developed in this entry. His comments were:

  • “It appears with the Arduino, you can only have one Wire interface at a time”
  • “What if you closed one then created the next one?”

It turns out that the Uno can only have one Wire interface, the Pico may have two. I continued to have an issue, as I wanted to be able to have upto 4 temperature sensor/display pairs running at one time. While the display address could be mitigated (the SparkFun Alphanumeric Display can be daisy-chained), I still had the issue with the temperature sensors. Two I2C interfaces only solved half of my problem.

First I needed to understand how I2C works.

I2C Basics

I won’t go into detail as to how I2C works, as Adafruit, Sparkfun and Arduino all do a fine job! I will call out what is important to know:

  1. An I2C interface has 4 pins:

    • Vin or VDD - power, typically 5V or 3.3V depending on the device
    • GND - ground
    • SDA - the data line
    • SCL - the clock line

    As the power and ground lines come from the rails, only SDA and SCL are notated on the Pico.

  2. If you look at the Pico Pinout, you’ll notice there are SDA/SCL pairs of pins starting at GP0/1 sequentially through GP20/21 and GP26/27 for either I2C0 (Wire) or I2C1 (Wire1). These pins provide the ability to have 10 I2C interfaces.

  3. Think of the two I2C interfaces, Wire and Wire1 as having the ability to multiplex, similar to how the ADC typically works. You might have 10 ADC channels, however, there is only one ADC channel conversion at a time. In the instance of I2C and the Pico, it has 10 I2C pairs, 5 for Wire and 5 for Wire1, however, only one Wire pair and one Wire1 pair can be active at a time.

  4. The easiest way to understand what is connected and the address of the device is to scan your I2C interfaces. Most programs posted, simply scanned Wire, I quickly hacked together a program which scans all 10 pairs on the Pico and shows which Wire interface, SDA/SCL pins and the addresses found. Here is the sample output:

I2C Scan
Wire0 SDA: 0 SCL: 1
No I2C devices found

Wire1 SDA: 2 SCL: 3
No I2C devices found

Wire0 SDA: 4 SCL: 5
No I2C devices found

Wire1 SDA: 6 SCL: 7
No I2C devices found

Wire0 SDA: 8 SCL: 9
No I2C devices found

Wire1 SDA: 10 SCL: 11
No I2C devices found

Wire0 SDA: 12 SCL: 13
No I2C devices found

Wire1 SDA: 14 SCL: 15
I2C device found at address 0x70
Scan Complete

Wire0 SDA: 16 SCL: 17
I2C device found at address 0x5A
Scan Complete

Wire1 SDA: 18 SCL: 19
I2C device found at address 0x70
Scan Complete

Wire0 SDA: 20 SCL: 21
I2C device found at address 0x5A
Scan Complete

Wire1 SDA: 26 SCL: 27
No I2C devices found

All Scans Successful!

Note: I indicate Wire0 as the interface, however, remember, programmatically it is called Wire, while Wire1 is referenced as Wire1.

Using I2C

The following steps are in the program Pico/TempDisplay, I recommend you examine all of the code in context to gain the best understanding. I wanted to use this entry to explicitly document the steps required for multiplexing I2C interfaces.

Now that you know what you have plugged into the I2C interfaces, let’s begin to program using them! Here are the steps to setting up I2C:

1. Create the I2C devices

Install the Library required by your device then instantiate instances of the devices you will use:

// 2 Temp Sensors
Adafruit_MLX90614 mlx_1 = Adafruit_MLX90614();
Adafruit_MLX90614 mlx_2 = Adafruit_MLX90614();

// 2 SparkFun Qwiic Alphanumeric Displays - Green COM-18566
// https://learn.sparkfun.com/tutorials/sparkfun-qwiic-alphanumeric-display-hookup-guide
HT16K33 AN_display_1;
HT16K33 AN_display_2;

2. Identify the pins and address used by each device

In this step you would transcribe the data you got from running the I2C_Scanner (above) into something programmatic. I setup a struct array to consolidate the specific pins to be used. I use init_d(index, SDA, SCL, address) to gather the information. The next steps will use init_Wire(index, Wire_instance) to setup the I2C interface.

// Assign names to all I2C devices, adjust max_I2C if required
#define max_I2C 4
enum device {temp_1, temp_2, AN_1, AN_2};
struct I2C_device 
{
  unsigned int SDA; // the GP for SDA
  unsigned int SCL; // the GP for SCL
  byte address;     // device address
} ;
struct I2C_device devices[max_I2C];

...code...

// Use I2C_Scanner to get the information below
// init_d is an easy way to ensure the device SDA/SCL/address are all in one spot
init_d(temp_1, 20, 21, 0x5A);   // IR temp sensor on Wire
init_d(AN_1, 18, 19, 0x70); 	// Alphanumeric display 1 on Wire1
init_d(temp_2, 16, 17, 0x5A); 	// IR temp sensor on Wire
init_d(AN_2, 14, 15, 0x70); 	// Alphanumeric display 2 on Wire1

3. Test interface (optional)

In the TempDisplay program, I test the two displays by initializing the Wire interface, displaying BOOT then closing the interface. If the display isn’t found, I show a status error blink. (I wanted to use this totally separate from a PC, so no Serial link.)

The way to do this is this:

    // init displays and show BOOT message
    init_Wire(AN_1, Wire1);
    if (AN_display_1.begin(devices[AN_1].address,
                            DEFAULT_NOTHING_ATTACHED,
                            DEFAULT_NOTHING_ATTACHED,
                            DEFAULT_NOTHING_ATTACHED,
                             Wire1) == false)
    {
      status(error);
    }
    AN_display_1.print("BOOT");
    Wire1.end();

Note I use the devices struct array for information such as the address. It begins to simplify the structure of the code and much of it can be easily duplicated and I change an index (AN_2)or device instance (AN_display_2)

4. Use the interface

The steps to use an interface are: (WireN can either be Wire or Wire1, confirm its correct!!)

  1. Initialize I2C interface(s)- init_Wire(index, WireN);
  2. Begin device instance - mlx_1.begin();
  3. Use the device - read an temperature, display data etc.
  4. Close the I2C interface(s) - WireN.end();

The exact commands for reading a temperature and displaying the temperature looks like this:

void showTemp_2()
{
    init_Wire(AN_2, Wire1);
    init_Wire(temp_2, Wire);
    mlx_1.begin();
    float IR_temperature = mlx_1.readObjectTempF();
    displayAN(IR_temperature, AN_display_2);
    Wire.end();
    Wire1.end();
}

It is important to only initialize the I2C interface using init_Wire() once, per use despite there might be multiple devices on the interface. The device index used is not important as all devices on that interface will have the same SDA and SCL attributes.

Debugging

I use the status(msg) function to indicate how far I have executed in a program. For example, I have the following message blinks setup:

    init_m(start, 2, 1);    // 2 sec delay w2 slow blinks for boot start
    init_m(error, 2, 16);   // 2 sec delay w32 very fast blinks for I2C error
    init_m(success, 2, 2);  // 2 sec delay w4 medium blinks for I2C success
    init_m(next, 1, 4);     // 1 sec delay w8 fast blink for I2C next

This allows me to track program execution by using the status(next); command. If I see 4 quick blinks, I know I have made it to a specific spot in the program. I also know I need to see 2 slow blinks on boot and if the I2C displays can’t be seen, I’ll have a very fast blinking LED.

Closing

As I stated earlier, another way to setup the TempDisplay program would be to plug the display and temperature sensor into the same set of pins. This would simplify the switching as you would be able to initialize the I2C interface with the both devices, use the devices then close the interface. An example of this implementation is showTemp1().

If you wished you could measure and display two sets of data simultaneously in this manner. Ultimately, you could have 10 pairs of sensor/displays.

Comments powered by Talkyard.