I2C1_Wr / I2C1_Rd functions hang with MikroC for PIC – Need Timeout

In some cases both the MikroC i2c functions I2C1_Wr() and I2C1_Rd() can hang or lockup indefinitely until the PIC is reset. This can happen if the I2C bus isn’t properly terminated, for example if a connection breaks or similar (broken wire or solder joint etc). Having your PIC lockup during operation is generally not very desirable if your are trying to develop a stable software system – if for some reason an i2c device hasn’t been properly connected your system will just lockup for good!

Different posts discuss this problem and its various causes:

https://forum.mikroe.com/viewtopic.php?f=13&t=21270&sid=f45f0199695794c83516f89919183cbb&start=0

and

https://forum.mikroe.com/viewtopic.php?f=88&t=60224

MikroC provided the code to their functions so that software developers (us) could work around the problem, this library implements versions of I2C1_Wr() and I2C1_Rd() that timeout rather than hang, a timeout values is passed as a parameter:

https://libstock.mikroe.com/projects/view/1052/i2c-non-blocking

The library doesn’t implement a C version, so inspired by it (thanks Danny!) I have ported the functions into C, and added some comments – use at your own risk, it works well for me on PIC18F family chips but hasn’t been exhaustively tested!

I have declared the timeouts as constants rather than allowing the timeout to be passed in as a parameter – this is so that the functions can be used as drop-in replacements without having to modify client code.

#define I2C_WRITE_TIMEOUT_US 200
unsigned short tI2C1_Wr(unsigned short d) {
    const unsigned int delay = 2; // us
    unsigned int max_retry = I2C_WRITE_TIMEOUT_US / delay;
    unsigned int retry;
    
    if (max_retry == 0)
        max_retry = 1;
        
    // Interrupt Flag bit - Waiting to transmit/receive
    PIR1.SSP1IF = 0;
    
    // Set data for transmission
    SSP1BUF = (unsigned char)d;
    // Wait for transmission to complete
    // 1 = Transmit is in progress
    // 0 = Transmit is not in progress
    //
    retry = max_retry;
    while (SSP1STAT.r_not_w == 1 && --retry > 0)
        delay_us(delay);
    // Timed-out stop transfer and return error
    if (SSP1STAT.r_not_w == 1) {
        // Enable Stop Condition
        SSP1CON2.PEN = 1;
        return 1;
    }
    
    // Wait for completion
    // 1 = The transmission/reception is complete (must be cleared by software)
    // 0 = Waiting to transmit/receive
    //
    retry = max_retry;
    while (PIR1.SSP1IF == 0 && --retry > 0)
        delay_us(delay);
    // Timed-out stop transfer and return error
    if (PIR1.SSP1IF == 0) {
        // Enable Stop Condition
        SSP1CON2.PEN = 1;
        return 1;
    }
    // Check that we got an ACK
    // 1 = Acknowledge was not received
    // 0 = Acknowledge was received
    //
    if (SSP1CON2.ACKSTAT != 0) {
        // No ACK, abort
        // Enable Stop Condition
        SSP1CON2.PEN = 1;
        return 1;
    }
    // All good...
    return 0;
}

And to Read:

#define I2C_READ_TIMEOUT_US 200
unsigned short tI2C1_Rd(unsigned short ack) {
    unsigned short d = 0;
    const unsigned int delay = 2; // us
    unsigned int max_retry = I2C_READ_TIMEOUT_US / delay;
    unsigned int retry;
    if (max_retry == 0)
        max_retry = 1;
        
    // Interrupt Flag bit - Waiting to transmit/receive
    // 1 = The transmission/reception is complete (must be cleared by software)
    // 0 = Waiting to transmit/receive
    //
    PIR1.SSP1IF = 0;
    // Set receive mode
    // 1 = Enables Receive mode for I2C
    // 0 = Receive idle
    //
    SSP1CON2.RCEN = 1;
    // Wait for read completion
    // 1 = The transmission/reception is complete (must be cleared by software)
    // 0 = Waiting to transmit/receive
    //
    retry = max_retry;
    while (PIR1.SSP1IF == 0 && --retry > 0)
        delay_us(delay);
    // Still not complete, get out...
    if (PIR1.SSP1IF == 0)
        return 0;
    // grab the data
    d = (unsigned short)SSPBUF;
    // ACK required?
    if (ack == 0) {
        // No
        // 1 = Not Acknowledge
        // 0 = Acknowledge
        //
        SSP1CON2.ACKDT = 1;
    } else {
        // Yes
        SSP1CON2.ACKDT = 0;
    }
    // Interrupt Flag bit - Waiting to transmit/receive
    // 1 = The transmission/reception is complete (must be cleared by software)
    // 0 = Waiting to transmit/receive
    //
    PIR1.SSP1IF = 0;
    // Start Ack sequence
    // 1 = Initiate Acknowledge sequence on SDAx and SCLx pins, and transmit ACKDT data bit. Automatically cleared by hardware.
    // 0 = Acknowledge sequence idle
    //
    SSP1CON2.ACKEN = 1;
    // Wait for completion of ack sequence
    retry = max_retry;
    while (PIR1.SSP1IF == 0 && --retry > 0)
        delay_us(delay);
    return d;
}

This works for the first i2c device, it is left as an exercise for the reader to convert it for a second i2c device, i.e. I2C2_Wr() and I2C2_Rd()