Infinite loop in the `i2c (or Twim) implementation` of nrf52840_hal

I am using an nrF52840 (mdk-board from makediary) to talk to an i2c slave. Everything works upto the point where actual data needs to be transmitted over the i2c bus.


pub fn wake(&self, i2c: &mut I2C) {
        hprintln!("entering wake").unwrap();
        let bytes = [0; 2];
        i2c.write(self.dev_addr, &bytes); 
        hprintln!("exiting wake").unwrap();
    }

#[entry]

fn main() -> ! {

// Get a singleton for all device peripherals and define my own pins. 
    let p = Peripherals::take().unwrap();
    let pins = Pins::new(p0::Parts::new(p.P0), p1::Parts::new(p.P1));
    
// Use pin 26 and 27 as clock and data lines
    let scl = pins.p26.into_floating_input().degrade();
    let sda = pins.p27.into_floating_input().degrade();
    let i2c_pins = twim::Pins {scl, sda};

// Get a Twim and Timer instance. 
    let mut i2c = Twim::new(p.TWIM1, i2c_pins, twim::Frequency::K250);
    let mut timer = Timer::new(p.TIMER0);

// Get a sensor instance and use it to perform a write first and then read the result.
    let mut sensor_instance = sensor::new(&i2c, &timer).unwrap();
    let response = match sensor_instance.atcab_info(&mut i2c, &mut timer) {
        Ok(v) => v,
        Err(_e) => panic!(" error"),
    };

    loop {}

}

My main function ends up calling the wake method after a series of intermediate calls (excluded intermediate calls for the sake of brevity). The wake method is simply suppose to write 2 bytes [0x00, 0x00] onto the i2c bus and return. This doesnt seem to be happening; instead I'm stuck in an infinite loop.

Couple of observations:

  1. Debugging shows that the wake method does get called.
  2. Which in-turn calls the write method of the i2c/Twim instance but then locks up in a loop waiting for the last byte to transmitted i.e. for the EVENTS_LASTTX register to be set to 1 (which never happens).
  3. I've set breakpoints and examined relevant registers in TWIM0 and TWIM1 peripherals. Everything checks out as per the implementation i.e.
    • ADDRESS register has the slave's address
    • TXD block has the right data PTR and 2 byte MAXCNT length
    • PSEL has the right pin selection (see attached screenshot for more).
    • PN_CNF registers for 26 and 27 are set to INPUT and configured as per implementation.

Questions:

  1. I don't understand why the pins are set to INPUT and not OUTPUT. I would have assumed that since we're transmitting i.e. writing data to the bus, both pins (SCL and SDA) should be set to OUTPUT mode. Is my understanding correct?
  2. If no, any ideas on what I might be doing wrong here?

Kind of stuck, any help would be great!

Things I've already been through:

  • Tested this with both TWIM instances TWIM1 and TWIM0 - same result.
  • Also double and triple checked my hardware wiring - seems fine.
  • Lastly I'm able to communicate with the slave using a different board running micropython with the exact same wiring.

Screenshot of a debugging session:

Have you some documentation about Pins, since I can't find anything about it.
If I look to the TWI sample of the hal, it seems that the pins should be p0_p26 and p0_27 instead of p26 and p27 - but I don't know if the Pins::new() makes some abstraction to it.
See: https://github.com/nrf-rs/nrf-hal/blob/master/examples/twi-ssd1306/src/main.rs#L38

Sure, Pins is just another macro defined in Rust-SDK for the nrf52840-mdk development board.

Here's the GitHub repo for it - https://github.com/nrf-rs/nrf52840-mdk-rs

Ok, that seems correct. Have you tried looking at it with a logic analyzer to see if something is sent and that you get a ACK package?

A common mistake is passing a wrong address. Are you sure your address is correct? In 7 bit addressing, you may need to shift the address to left by one bit. (The least significant bit is Read/Write bit.) Perhaps micropython automatically does that for you and the nrf52 hal doesn't? (Or the other way around?)

I don't have one on me right now. But I'm looking for one. But from what I can tell, I don't think its transmitting at all and the reason for my suspicion is that both pins are set to INPUT before the write. (i.e. at the time of the write to TASKS_STARTTX register, both pins are in INPUT mode)

Shouldn't they be set to OUTPUT if we are going to set the state of a pin?

I can confirm that the ADDRESS register contains the correct slave address and it matches the one in the micropython case. Here's a screenshot of the micropython test. If you compare it with the earlier screenshot, we see the same addresses - (address 96 in decimal and x60 in hex)

I would still try 0xC0 instead of 0x60.

This doc says micropython's scan returns a 7-bit address and the nrf-hal crate's write doesn't seem to do shifting.

Ok, let me try that and get back to you.

I can confirm that changing the address to 0xC0 doesn't work. Here's the screenshot.

@trembel @lonesometraveler I was wondering if someone could test wiring-up an nrf52840 board and an i2c device (any type) and confirm if the current version (0.10.0) of nrf52840_hal actually works.

Sorry, I've at the moment no access to an nRF52, but I can get one in a week.

Maybe you should try building a minimal example (i.e. initialize only this two pin and make a write to i2c) to see if that works. Maybe you could also try it bare-metal without HAL, I2C is not that complicated on the nRF52 (take a look here: NordicSnippets/main.c at master · andenore/NordicSnippets · GitHub, its in C but easily portable to Rust).

The registers can be found in the datasheet.

This one is a bare-metal binary but yeah, I'll probably try writing my own minimal i2c implementation. Although, I was hoping I didn't have to for my current need (which is just a simple write-wait-read). But thanks!

@nihalpasham
I only have a nRF52832. The code below works for me. I can read WHO_AM_I register of my sensor. I pass a 7 bit address.


fn main() -> ! {
    let p = pac::Peripherals::take().unwrap();
    let port0 = p0::Parts::new(p.P0);

    let scl = port0.p0_26.into_floating_input().degrade();
    let sda = port0.p0_27.into_floating_input().degrade();

    let pins = twim::Pins { scl, sda };

    let mut i2c = Twim::new(p.TWIM0, pins, twim::Frequency::K100);

    let bytes = [0x0F; 1];
    i2c.write(0x6B, &bytes);

    let mut bytes = [0; 1];
    i2c.read(0x6B, &mut bytes);

    loop {}
}

Thanks @lonesometraveler. Now, I think it kind of rules out a bug in the HAL implementation, considering both nRF52840 and nRF52832 use the same shared (i.e. common) HAL. So, that leaves me with 2 other things, I could try.

  1. Use an external pull-up resistor and
  2. Get my hands on a scope.

PS: which scope are you using?

I think the software and the logic analyzer is from saleae. You can also use a cheap saleae clone, if you have one around (~5$)

1 Like

So, it turns out I was dealing with buggy hardware.

I wired up my nRF to a different i2c based sensor (IMU -mpu6050) and it worked flawlessly (results in the screenshot below). I was able to wake the IMU sensor, examine the registers and confirm that the requisite 2 bytes were indeed transferred. So, I think I can junk my extension board and find another way to interface with the actual sensor I'm trying to talk to.

I'm including a link to the extension board just in case someone happens to run into this problem. (The extension board is made by microchip that somehow works with an esp32 but doesnt play well with other boards). Dynamic Dev Tool Page | Microchip Technology

Actually, I guess this is expected as the extension board was designed to specifically work with ATMEL dev boards only (that also magically happens to work with an esp32) :thinking:

I use a logic analyzer from Saleae.

1 Like

Finally managed to get my hands on a logic analyzer. Here are my findings -

  1. nRF52 does not get an ACK back and from what I know about i2c, it using the 7-bit addressing scheme.
  2. ESP32 on the other hand gets an ACK but it looks like its using i2c's 10-bit addressing scheme

Is my understanding correct? If yes, can we use 10-bit addresses on the nRF too.

It seems to me both pictures show a 7-bit address + R/W bit. How did you conclude ESP32 uses 10 bit addressing?

This page explains I2C slave addressing. It may be helpful.