Scanning I2C addresses

I2C is a simple open-drain bus that is popular for connecting devices to microcontrollers (MCUs), such as sensors, RTCs, displays etc. Devices on the I2C bus are addressed by 7 bits and the bus data and clock lines are pulled-up by a set of pull-up resistors. A good first test for checking that I2C communication works fine is to scan for the devices' addresses. This article presents two I2C address scanners for ATmega328p AVR (e.g. Arduino Pro Mini 3.3V 8 MHz) and GD32VF103 RISC-V (e.g. Longan Nano) MCUs.

I2C High-Level Overview

On an I2C bus, every communication is initiated by a master (controller) that addresses a slave (target) device. There can be multiple controllers and multiple targets on a bus. With MCUs, it's common to have one controller (the MCU) and one or more target devices connected to a bus.

An address of a target is static, i.e. there is no address autoconfiguration protocol. Usually, the address of a target is specified in its datasheet.

The following sequence diagram describes the structure of I2C communication:

I2C sequence diagram

I2C Address Scanning

The basic idea with I2C address scanning is to simply send START conditions for all possible addresses (there are just up to 2^7 of them!) and detect ACKnowledgement conditions on the bus, in a loop.

Of course, in any case, after sending the address and detecting either an ACK or NACK the controller immediately sends a STOP condition to avoid confusing any target device and being able to continue with the next one.

Scanning on the ATmega328p

There are several I2C scanners around that use the Arduino API. Arguably, that API hides too much of the I2C fundamentals, such that it's not immediately obvious how the scanning really works.

I thus wrote a bare bones I2C scanner that directly interfaces with the I2C unit of an 8 bit ATmega MCU using its registers:

// at 8 Mhz
static void i2c_set_100kHz()
{
    TWBR = 32;
}
static void i2c_set_400kHz()
{
    TWBR = 2;
}

// NB: clearing TWINT starts next transmission
//     TWI == I2C
//     TWCR = TWI Control Register
//     TWDR = TWI Data Register

// start master transmission
static void i2c_start()
{
    // clear TWINT flag, send START, enable TWI unit
    TWCR = _BV(TWINT) | _BV(TWSTA) | _BV(TWEN);
    loop_until_bit_is_set(TWCR, TWINT);
    // NB: TWSTA must be cleared explicitly in the next operation
}
static void i2c_stop()
{
    // clear TWINT flag, send STOP, enable TWI unit
    TWCR = _BV(TWINT) | _BV(TWSTO) | _BV(TWEN);
    loop_until_bit_is_clear(TWCR, TWSTO);
    // NB: TWSTO is cleared automatically
    // NB: TWINT is NOT set after STOP transmission ...
}
// rw: TW_READ (1) or TW_WRITE (0)
static void i2c_set_address(uint8_t addr, uint8_t rw)
{
    TWDR = (addr << 1) | rw;
    // clear TWINT flag, send STOP, enable TWI unit
    TWCR = _BV(TWINT) | _BV(TWEN);
    loop_until_bit_is_set(TWCR, TWINT);
}

static void setup()
{
    setup_uart();
    stdout = &uart_stdout;
    i2c_set_100kHz();

    // uncomment if your I2C modules don't come with pull-ups ...
    // DDRC &= ~ _BV(DDC4); PORTC |= _BV(PORTC4);
    // DDRC &= ~ _BV(DDC5); PORTC |= _BV(PORTC5);
}

static void probe_address(uint8_t i)
{
    i2c_start();
    i2c_set_address(i, TW_WRITE);
    if ((TWSR & TW_STATUS_MASK) == TW_MT_SLA_ACK)
         printf("Found device on address: 0x%" PRIx8 " (%" PRIu8 ")\n", i, i);
    i2c_stop();
}

static void scan()
{
    // skipping reserved addresses
    // cf. https://en.wikipedia.org/wiki/I%C2%B2C#Reserved_addresses_in_7-bit_address_space
    for (uint32_t i = 8; i<128; ++i)
        probe_address(i);
}

int main()
{
    setup();

    for (;;) {
        printf("Scanning I2C bus at 100 kHz ...\n");
        scan();

        printf("Scanning I2C bus at 400 kHz ...\n");
        i2c_set_400kHz();
        scan();

        printf("done\n\n");

        i2c_set_100kHz();

        for (uint16_t i = 0; i < 30 * 30; ++i)
            _delay_ms(32);
    }

    return 0;
}

The is also a pretty direct translation of the basic idea. It's clear from the code that the scanner check whether the address is acknowledged or not.

See Also

Scanning on the GD32VF103

Scanning for I2C addresses on the the GD32VF103 32 Bit RISC-V MCU (e.g. the Longan Nano) isn't too different:

static void probe_address(uint32_t i)
{
    while (i2c_flag_get(I2C0, I2C_FLAG_I2CBSY))
        ;
    i2c_start_on_bus(I2C0);
    while (!i2c_flag_get(I2C0, I2C_FLAG_SBSEND))
        ;
    // it's cleared by getting the flag, ie. reading I2C_STAT0
    i2c_master_addressing(I2C0, i << 1, I2C_TRANSMITTER);
    uint32_t k = 0;
    while (!i2c_flag_get(I2C0, I2C_FLAG_ADDSEND)) {
        if (i2c_flag_get(I2C0, I2C_FLAG_AERR)) {
            i2c_flag_clear(I2C0, I2C_FLAG_AERR);
            i2c_stop_on_bus(I2C0);
            return;
        }
        // in case no bus is connected
        if (k++ > 1000 * 1000) {
            i2c_stop_on_bus(I2C0);
            return;
        }
    }
    printf("Found device on address: 0x%" PRIx32 " (%" PRIu32 ")\n", i, i);
    // NB: it's cleared by reading I2C_STAT0 _and_ I2C_STAT1
    i2c_flag_clear(I2C0, I2C_FLAG_ADDSEND);

    i2c_stop_on_bus(I2C0);

    // wait for stop being sent
    while (I2C_CTL0(I2C0) & I2C_CTL0_STOP)
        ;
}

static void scan()
{
    // skipping reserved addresses
    // cf. https://en.wikipedia.org/wiki/I%C2%B2C#Reserved_addresses_in_7-bit_address_space
    for (uint32_t i = 8; i<128; ++i)
        probe_address(i);
}

int main()
{
    rcu_periph_clock_enable(RCU_GPIOB);
    rcu_periph_clock_enable(RCU_I2C0);
    // yes, I2C is open-drain
    gpio_init(GPIOB, GPIO_MODE_AF_OD, GPIO_OSPEED_50MHZ,
            GPIO_PIN_6 | GPIO_PIN_7) ;
    i2c_deinit(I2C0);
    i2c_clock_config(I2C0, 100000, I2C_DTCY_2);
    // NB: we don't call i2c_mode_addr_config()
    //     because we are only in master transmit mode
    i2c_enable(I2C0);

    for (;;) {
        printf("Scanning I2C bus at 100 kHz ...\n");
        scan();

        printf("Scanning I2C bus at 400 kHz ...\n");
        i2c_clock_config(I2C0, 400000, I2C_DTCY_2);
        scan();

        printf("done\n\n");

        i2c_clock_config(I2C0, 100000, I2C_DTCY_2);
        delay_1ms(30 * 1000);
    }

    return 0;
}

Since the Nuclei SDK provides an I2C API that isn't too high-level (and arguably doesn't obfuscate too much) it's used instead of wrapping the register accesses in a similar way.

On that MCU, detecting the ACK of an address is a little bit tricky and it isn't explicitly documented in the manual.

Also, the setup code for the I2C unit is more complex than on the ATmega328p and arguably not immediately obvious after consulting the manual.

See Also

Other MCUs

Looking at the general idea and the above example it shouldn't be hard to implement an analogous I2C address scanner on a completely different MCU, given that it has some sort of acceptable documentation of its I2C unit.