Bit-Banging I2C with ResetBus() Functionality

The good thing with the internet is: it allows engineers to collaborate. And here is an example: Marc is a reader of this blog had a problem with the I2C hardware of a Freescale Kinetis ARM microcontroller. In his case, the I2C bus could be stuck, and there seems no way to reset it with the I2C hardware on the microcontroller. So a solution would be to reset it with software instead.

Bit Banging Software I2C Driver

Bit Banging Software I2C Driver

I had created a software ‘bit banging’ I2C driver which can be used if there is no hardware I2C driver (see “Bit Banging I2C“), but it was not available for Kinetis and with the “Generic I2C Driver” component. But to overcome I2C issues, Marc wanted that software I2C driver for Kinetis, plus a function to reset the bus by software. He used my work as a starting point, fixed some issues and greatly extended it, and he contributed it back so everyone else can benefit :-).

Bit Banging

“Bit Banging” means that instead using dedicated hardware, normal general purpose I/O pins are used to implement a protocol like I2C or SPI. I had to develop such a bit banging I2C driver because Processor Expert did not implement a hardware driver for microcontroller like the Freescale ColdFire V2 MCF52259 :-(. Having a big banging driver is not as fast as using the hardware I2C, but for slow devices this is not much of a problem.

The code below shows how to receive a byte using bit banging:

byte I2C2_RecvChar(byte *Chr)
{
  word Trial;
  bool Acknowledge;
  word timeout;

  Trial = TRIALS;
  do {
    SDA_SetDir((bool)INPUT);     /* SDA HIGH - START SETUP */
    SCL_SetDir((bool)INPUT);     /* CLOCK HIGH PULSE */
    Delay();                     /* CLOCK HIGH PULSE & BUS FREE TIME */
    /* check that we have a valid start condition: SDA needs to be high */
    timeout = 65535U;
    while((SDA_GetVal()==0U)&&(timeout!=0U)) { /* WAIT FOR CLOCK HIGH PULSE */
      timeout--;
      I2C2_OSYIELD();
    }
    Delay();
    if (timeout==0) {
      InternalStop();
      return ERR_BUSY;
    }
    SDA_SetDir((bool)OUTPUT);
    SDA_ClrVal();                /* START CONDITION */
    Delay();                       /* START HOLD TIME */
    SCL_SetDir((bool)OUTPUT);
    SCL_ClrVal();                /* CLOCK LOW PULSE */
    Delay();
    Write((byte)(SlaveAddr + READ));
    Acknowledge = GetAck();
    --Trial;
  } while ((Trial != 0U) && Acknowledge);
  if (Acknowledge) {
    SCL_SetDir((bool)OUTPUT);
    SCL_ClrVal();                /* CLOCK LOW PULSE */
    InternalStop();
    return ERR_BUSY;
  } else {
    SCL_SetDir((bool)OUTPUT);
    SCL_ClrVal();                /* CLOCK LOW PULSE */
    Delay();
  }
  *Chr = Read();
  SetAck((bool)NOACK);
  return ERR_OK;
}

Hardware Bugs and locked up I2C bus

As for myself, I have faced bugs in the hardware too (see “KL25Z and I2C: Missing Repeated Start Condition“). I was to solve my problem with the information from the errata and using special clock configurations. Still there might be other problems with hung up I2C bus. This does not have to be necessarily a bug in the I2C silicon implementation, but could happen with external I2C devices going wild. Then it could be difficult to get that back under control (except you can perform a power-on reset). Or to send 9 clocks followed by a STOP condition on the bus. But this kind of detailed control might not be availble in the I2C hardware, so bit banging is the solution here.

Reset I2C Bus

Marc added a function to the driver to reset the bus in the GenericSWI2C component:

I2C ResetBus

I2C ResetBus

This method tries to reset the bus with 9 clocks followed by a STOP. It returns TRUE if the bus is IDLE afterwards, or FALSE if the bus still hangs:

bool I2C2_ResetBus(void)
{
  char i;

  if(SDA_GetVal() && SCL_GetVal()) {
    return TRUE;
  }
  SCL_SetDir((bool)INPUT);
  SDA_SetDir((bool)INPUT);
  Delay();
  if(!SCL_GetVal()) {
    return FALSE; /* SCL held low externally, nothing we can do */
  }
  for(i = 0; i<9; i++) { /* up to 9 clocks until SDA goes high */
    SCL_SetDir((bool)OUTPUT);
    SCL_ClrVal();
    Delay();
    SCL_SetDir((bool)INPUT);
    Delay();
    if( SDA_GetVal()) {
      break; /* finally SDA high so we can generate a STOP */
    }
  } /* for */
  if(!SDA_GetVal()) {
    return FALSE; /* after 9 clocks still nothing */
  }
  InternalStop();
  return(SDA_GetVal() && SCL_GetVal()); /* both high then we succeeded */
}

Yield while Delay

The other contribution from Marc is to perform a yield if the driver is waiting:

Yield in GenericSWI2C

Yield in GenericSWI2C

The software driver needs to wait in several places. If using an RTOS like FreeRTOS, and if this attribute is set to ‘yes’, it will perform a task yield. For this the macro OSYIELD() is defined in the driver:

#if I2C2_HAS_RTOS && I2C2_YIELD
  #define I2C2_OSYIELD() taskYIELD()
#else
  #define I2C2_OSYIELD() /* do nothing */
#endif

The driver will automatically detect if an RTOS is present and generates the appropriate driver code.

GenericI2C

I’m using the GenericI2C driver as a general wrapper between LDD (Logical Device Drivers) and non-LDD drivers. So far the Processor Expert component only allowed either LDD or non-LDD components. Now with the addition of the GenericSWI2C component, I needed to change the component to allow either LDD or non-LDD components.

To use the bit banging GenericSWI2C component with the GenericI2C component:

  1. In GenericI2C, enable non-LDD I2C and have LDD I2C disabled:

    GenericI2C Settings

    GenericI2C Settings

  2. Specify GenericSWI2C as interface:

    Adding GenericSWI2C Interface

    Adding GenericSWI2C Interface

  3. Continue to configure GenericSWI2C (pin settings, timing).
  4. Done 🙂

Summary

Bit-Banging the I2C protocol is not the fastest way, but because very generic it has a lot of advantages: it gives me complete control over the communication bits, and I can use any general purpose Bit I/O pins and I’m not limited to the I2C hardware. Sure, that software bit banging I2C driver is not catching every special case, but worked for me very well with my hardware. So I hope it is useful for you too. And with the extensions contributed by Marc the driver is now even more complete and useful.

The updated components are available on GitHub, see “Processor Expert Component *.PEupd Files on GitHub” how to load them.

Thanks again to Marc for his contribution!

Happy Bit-Banging 🙂

Links

26 thoughts on “Bit-Banging I2C with ResetBus() Functionality

  1. TRUE or TRUE ? (This method tries to reset the bus with 9 clocks followed by a STOP. It returns TRUE if the bus is IDLE afterwards, or TRUE if the bus still hangs)

    Like

  2. Great writeup! Erich’s software has saved me so much time, it’s only proper to contribute something back!

    I should point out that the timing of the driver is based on Fast Mode. Most devices are these days but if you have a genuine slow-mode I2C device you might have problems. Not sure when I’ll have time to go back thru and change where the timing needs to be stretched (anyway, some of the times would be so long it would make sense to use FreeRTOS’s delay if available rather than software timer loops)

    So I think I found another with the IC2 module, and I think it should be dealt with there…. the issue is with I2Cbus_ReadAddress/I2Cbus_WriteAddress and multibyte addresses. They need to be byte swapped on the ARM architectures. If I pass e.g. address 0xE228 (length 2), what comes out on the bus is 0x28, 0xE2.

    Strictly, you could byte swap it calling into the driver, but it seems more convenient if you could do something like:

    uint16_t address;
    I2Cbus_ReadAddress(devAddress, &address, sizeof(address), pData, length);

    Thoughts?

    Like

    • Hi Marc,
      yes, that thing with the Fast Mode is noted.
      About little/big endian, you are referring to the GenericI2C driver, correct? Yes, I agree that that should be handled in the driver, and I’m doing this already e.g. search for ‘little endian’.
      The thing is that some I2C devices are using little endian, some big endian. So I think this should be device (e.g. MMA8451) specific, and not I2C driver specific?

      Liked by 1 person

  3. Hi Eric,

    Processor: KL16Z256VLH4
    I installed the Generic I2C component using a LDD option for I2c channel 0.
    I need a second instance of the Generic I2C with a non-LDD for I2C channel 1.
    When I installed the non-LDD version, I selected the Generic SWI2C option, and then a new wait component. The install never completes.
    Have you seen an issue with a second instance of the Generic I2C component?

    Regards,
    George

    Like

    • Hi George,
      there is a bug in Processor Expert: it can hang while creating a shared component :-(. I have reported that to Freescale.
      For some components it helps if you press ‘Cancel’ when it asks you to add/create the shared component.
      As a workaround, I have disabled the inherited components in the settings. I will send you that component by email.
      What you could try otherwise is to copy-paste the component (from another project or from the one you have): then it should not ask you for the inherited component during creation of the component.

      Like

  4. Hi Eric,
    I am having some troubles caused by a timeout in the genericI2C driver bean.
    What happens is that if the remote sensor I am talking to gets it’s SCL line grounded, then the read or write transaction currently underway slows down by a huge amount (like 1000 times slower).
    I believe while this might be correct behavior (the I2C slave is pulling the clock low to tell the host it is not ready, and the host is happy to wait for it), in my case my slave devices would never behave that way and I want that read or write to fail.
    There is a bunch (10?) of places in the driver where a timeout value is loaded with 65535U. When I change this to a reasonable number (like 1000U) I am able to sense that the sensor or it’s wiring has failed, and quickly take appropriate action.

    It would be great if there was a box in the PE bean where I could set this timeout number instead of having to resort to editting the generated code.

    Thanks!
    Brynn

    Like

  5. Also I now get an error compiling the generated code:
    ../Generated_Code/GI2C2.c:242: undefined reference to `GI2C2_CONFIG_SEND_BLOCKContinue’

    I Can’t find any other references to that, it almost looks like the Continue part is a typo.

    (I have both the genericI2C component and the genericSWI2C components in the code)
    Brynn

    Like

  6. Hi Erich, I’m encountering a bus lockup issue while using the InternalI2C component (SDA being held low by MAX7311). I’m not sure about implementing an equivalent to ‘*_ResetBus()’ for this component. Looks pretty well wrapped up, can’t easily get access to pins. Any thoughts on this?
    Not even sure why InternalI2C component was chosen tbh, doesn’t seem to be as commonly used on these forums. Do you know what’s going on there? Should I be using the GenericSWI2C component instead?
    Many thanks in advance.

    Like

  7. Hi Erich.

    I started using GenericSWI2C to use the ResetBus command and noticed that I2C slowed down. I did not find an option to set the frequency. Is there any way to make it faster?

    Like

    • Yes, there are a few settings (you need to check with your device):
      do not generate error events:
      #define McuGenericI2C_CONFIG_USE_ON_ERROR_EVENT 0
      No Mutex if you are the only one using the bus:
      #define McuGenericI2C_CONFIG_USE_MUTEX 0
      Disable yield():
      #define McuGenericSWI2C_CONFIG_DO_YIELD 0
      Set the delay to zero:
      #define McuGenericSWI2C_CONFIG_DELAY_NS (0)

      I hope this helps,
      Erich

      Like

      • With these changes it got faster, but not enough for my application. If I go back to using the internalI2C component, is there any command similar to resetBus?

        Like

        • You could turn on -O3 which improves things too.

          The InternalI2C does not have the reset bus functionality. But you could do the ResetBus with the GenericSWI2C (say after power-up) and then switch over to the InternalI2C later on.

          Like

  8. Hello Erich,
    I would like to use this I2C bit-banging driver on another hardware using another IDE not supporting PE. Can you tel me where I can find the c source code so I can port this out of PE to my own dev tools ?

    Many thanks in advance

    Liked by 1 person

What do you think?

This site uses Akismet to reduce spam. Learn how your comment data is processed.