i2c-bus-nanoframework

I2C Bus in nanoFramework: Sensors and Scanner

  • 5 min

The I2C bus is a protocol for connecting multiple devices using two lines.

With UART we would need a separate connection for each sensor. The I2C protocol (Inter-Integrated Circuit) allows sharing the bus.

Since it is a bus, we can connect several devices to the same two lines. The controller selects who it wants to talk to using an address.

In this post we will learn how to use the I2cDevice class to manage this bus, scan for connected devices, and read data from them.

For this tutorial you need to install the NuGet package: nanoFramework.System.Device.I2c

Basic concepts: SDA and SCL

I2C uses only two lines (plus GND, of course):

  • SDA (Serial Data): Where data travels.
  • SCL (Serial Clock): The clock that sets the pace of communication.

Unlike UART, here there IS a clock (it’s synchronous). And very importantly: I2C lines work with “Open Drain” logic, which means they need Pull-Up resistors to function. Fortunately, most sensor modules you buy (breakout boards) already come with these resistors soldered on.

Configuration in nanoFramework

On the ESP32, we can use almost any pair of pins for I2C, but it is recommended to use the default pins to avoid headaches.

  • Standard ESP32 (DevKit V1):
  • SDA: GPIO 21
  • SCL: GPIO 22

The main class is I2cDevice. To instantiate it, we first need to define the connection settings (I2cConnectionSettings), which includes the bus ID (usually 1 on ESP32) and the Slave Address.

using System.Device.I2c;

// Configuration: Bus 1, Device Address (e.g., 0x77 for BMP280)
I2cConnectionSettings settings = new I2cConnectionSettings(1, 0x77);

// Create the device
I2cDevice sensor = I2cDevice.Create(settings);
Copied!

Creating an I2C Scanner

Before trying to read a complex sensor, the first thing to know is: Is my sensor connected and detected? And what is its address? (Sometimes we buy a Chinese chip and the address is not what the datasheet says.)

Let’s write a small program that goes through all possible addresses (0 to 127) and tries to say hello.

using System;
using System.Device.I2c;
using System.Diagnostics;
using System.Threading;

namespace I2cScanner
{
    public class Program
    {
        public static void Main()
        {
            Debug.WriteLine("Starting I2C Scanner...");

            // On ESP32, Bus 1 usually uses pins 21 (SDA) and 22 (SCL)
            // If you use other pins, you need to configure them using 'I2cDevice.Create' with 'I2cConnectionSettings'
            // Note: In recent versions of nanoFramework for ESP32, you may need to configure the pins
            // using the System.Device.I2c (Configuration) library
            
            // Loop through valid addresses (usually 8 to 119)
            for (int address = 0x08; address < 0x78; address++)
            {
                I2cConnectionSettings settings = new I2cConnectionSettings(1, address);
                
                using (I2cDevice device = I2cDevice.Create(settings))
                {
                    try
                    {
                        // Attempt to read one byte. 
                        // If the device exists, it will respond (ACK).
                        // If it doesn't exist, it will throw an exception or return an error.
                        byte[] buffer = new byte[1];
                        var result = device.Read(buffer);

                        // If the read is successful
                        Debug.WriteLine($"Device found at address: 0x{address:X2}!");
                    }
                    catch
                    {
                        // Nobody home, ignore
                    }
                }
            }
            
            Debug.WriteLine("Scan finished.");
            Thread.Sleep(Timeout.Infinite);
        }
    }
}
Copied!

If you see Device found at address: 0x77! in the console, you know who to call.

Reading a sensor using the WriteRead pattern

I2C communication with sensors usually follows a standard pattern:

  1. We Write to the sensor telling it: “I want to read register X”.
  2. We then immediately Read the sensor’s response.

This is done atomically using the WriteRead method.

Example: Reading a chip identifier

Almost all sensors (like the MPU6050 accelerometer or the BMP280 barometer) have a register called WHO_AM_I or CHIP_ID. It is a read-only register that returns a fixed value, ideal for verifying we are talking to the correct chip.

Suppose we have a BMP280 sensor (Address 0x76 or 0x77). Its ID register is usually 0xD0 and should return 0x58.

public static void ReadIdentifier()
{
    // 1. Connection to BMP280 (assuming address 0x76)
    I2cConnectionSettings settings = new I2cConnectionSettings(1, 0x76);
    using I2cDevice bmp280 = I2cDevice.Create(settings);

    // 2. Prepare buffers
    byte[] registerToRead = new byte[] { 0xD0 }; // ID register address
    byte[] receivedData = new byte[1];          // Where we store the response

    // 3. Execute WriteRead (Atomic transaction)
    bmp280.WriteRead(registerToRead, receivedData);

    // 4. Check
    Debug.WriteLine($"Chip ID: 0x{receivedData[0]:X2}");

    if (receivedData[0] == 0x58)
    {
        Debug.WriteLine("It's an authentic BMP280!");
    }
}
Copied!

Using an IoT.Device driver

Here comes the good news. It’s great for learning to send raw bytes, but for actual work… there’s no need to reinvent the wheel.

The .NET IoT and nanoFramework community maintains a huge repository of drivers already written for hundreds of popular sensors (AHT10, BMP280, MPU6050, SSD1306, etc.).

Simply search NuGet for: nanoFramework.IoT.Device.Bmxx80 (for the BMP280).

// Example using the official driver (Much easier)
using Iot.Device.Bmxx80;
using Iot.Device.Bmxx80.FilteringMode;

// ...
Bmp280 bmp = new Bmp280(I2cDevice.Create(new I2cConnectionSettings(1, 0x76)));

bmp.TemperatureSampling = Sampling.HighResolution;

var reading = bmp.Read();
Debug.WriteLine($"Temperature: {reading.Temperature.DegreesCelsius} ºC");
Copied!

Always check if a NuGet driver exists before diving into the datasheet and programming hexadecimal registers by hand. It will save you days of work.