NRF9160 I2C/TWI recovery and microsecond resolution timer

Hi,

Something locks-up my I2C sometimes and I need to make I2C recovery. It looks like Nordic has not implemented the zephyr i2c_recover_bus function, so I guess I need to make it manually?

I've already drafted a somehow working code:

static inline void wait() {
  for (int i = 0; i < 60; i++) {
    __asm__("nop");
  }
}

void recover_i2c() {
  const struct device* gpio_dev = NULL;
  gpio_dev = device_get_binding("GPIO_0");
  if (gpio_dev == NULL) {
    k_panic();
  }
  int err = gpio_pin_configure(gpio_dev, I2C_SCL, GPIO_OUTPUT);
  if (err < 0) {
    LOG_ERR("I2C recover, GPIO configure failed: %d", err);
  }
  for (int i = 0; i < 10; i++) {
    err = gpio_pin_set(gpio_dev, I2C_SCL, 0);
    if (err < 0) {
      LOG_ERR("I2C recover, GPIO set failed: %d", err);
    }
    wait();
    err = gpio_pin_set(gpio_dev, I2C_SCL, 1);
    if (err < 0) {
      LOG_ERR("I2C recover, GPIO set failed: %d", err);
    }
    wait();
  }
  err = gpio_pin_configure(gpio_dev, I2C_SCL, GPIO_DISCONNECTED);
  if (err < 0) {
    LOG_ERR("I2C recover, GPIO configure failed: %d", err);
  }
  k_sleep(K_MSEC(1));
}

But I am not very comfortable with it. It looks like k_usleep does not work(sleeping 5us can take 30us), so I just pulled the wait loop out from thin air. Internet and my brains tell me that it is very bad idea. So the actual questions:

- There is no built in I2C recovery in NRD91 SDK, right?

- What is the preferred way to do it? Capture the I2C pin to GPIO like that? Or maybe convert I2C clock to PWM on the fly? Can you do that?

- If the preferred way needs a microsecond resolution timing, what is the correct way to do that?

  • - If the preferred way needs a microsecond resolution timing, what is the correct way to do that?

     This note may be useful:: https://github.com/nrfconnect/sdk-zephyr/blob/v2.4.99-ncs2/include/kernel.h#L457-L462 

    - There is no built in I2C recovery in NRD91 SDK, right?

     No, it does seem like the i2c_recover_bus() is supported by Nordic at the moment. However, you may use the NRFX API, where you can use the function nrfx_twi_twim_bus_recover()

    https://github.com/nrfconnect/sdk-hal_nordic/blob/v1.5.1/nrfx/drivers/include/nrfx_twim.h#L366-L369 

    ttps://github.com/nrfconnect/sdk-hal_nordic/blob/v1.5.1/nrfx/drivers/src/nrfx_twi_twim.c#L45 

    The function above may also be helpful if you want to implement it yourself

    - What is the preferred way to do it? Capture the I2C pin to GPIO like that? Or maybe convert I2C clock to PWM on the fly? Can you do that?

     To be honest, I don't have complete control of this at the moment. I will do some more investigation and get back to.

    Best regards,

    Simon

  • Thanks Simon,

    I'll give this a try next week. I suppose the minimum hassle will be to implement that by my self and use k_busy_wait() for sleeping. I already tried that you can just hijack I2C pins as GPIOs like that.

    I will tell how it went.

  • fastfox said:
    I will tell how it went.

     Sounds good. If you don't get it to work I will look into this myself and try to get it working.

    I have answered a similar question earlier, that may be useful: https://devzone.nordicsemi.com/f/nordic-q-a/71454/i2c-twi-driver-bus-recovery 

  • Ok, so it works and doesn't.

    Works part:

    static inline void print_error(int err) {
      if (err != 0) {
        LOG_DBG("Fail: %d", err);
      }
    }
    
    void recover_i2c() {
      const struct device* gpio_dev = NULL;
      gpio_dev = device_get_binding("GPIO_0");
      if (gpio_dev == NULL) {
        k_panic();
      }
      // Capture I2C pins to GPIOs
      print_error(gpio_pin_configure(gpio_dev, I2C_SCL, GPIO_OUTPUT_HIGH));
      print_error(gpio_pin_configure(gpio_dev, I2C_SDA, GPIO_INPUT));
    
      for (int i = 0; i < 9; i++) {
        // If SDA is high, it means that it is now/already released and there is no need to
        // try to send recovery clocks anymore
        if (gpio_pin_get(gpio_dev, I2C_SDA) > 0) {
          break;
        }
        // Make clock pulses, 5us(10us) should give us 100kHz, but GPIO set also takes time
        // so in practice this is ~6us or ~80kHz. The one time I could did this for device that was stuck,
        // frequency was around 60-70kHz.
        print_error(gpio_pin_set(gpio_dev, I2C_SCL, 0));
        k_busy_wait(5);
        print_error(gpio_pin_set(gpio_dev, I2C_SCL, 1));
        k_busy_wait(5);
      }
      // Stop condition
      print_error(gpio_pin_configure(gpio_dev, I2C_SDA, GPIO_OUTPUT_HIGH));
      print_error(gpio_pin_set(gpio_dev, I2C_SDA, 0));
      k_busy_wait(5);
      print_error(gpio_pin_set(gpio_dev, I2C_SDA, 1));
      k_busy_wait(5);
    
      print_error(gpio_pin_configure(gpio_dev, I2C_SCL, GPIO_DISCONNECTED));
      print_error(gpio_pin_configure(gpio_dev, I2C_SDA, GPIO_DISCONNECTED));
    }

    This code seems to generate the correct clock and data pulses for the recovery.

    Does not work part:

    After running that code I2C is broken. Voltage levels are somewhere in between and some devices on the bus stop working :(

    But as I will make this recovery just before a reboot, it might just save my butt here. But obviously, this is not the right way to do it as the bus goes haywire after the operation.

  • fastfox said:
    But as I will make this recovery just before a reboot, it might just save my butt here. But obviously, this is not the right way to do it as the bus goes haywire after the operation.

    I think just rebooting might be the best option, if possible. As mentioned in the thread TWI Stuck Bus Recovery it was recommended to reinitialize the I2C driver (not run the recovery/clear function). When you reboot the chip, the I2C init function will run again and get reinitialized.

    Best regards,

    Simon

Related