1
\$\begingroup\$

This may be a very borderline Stack Overflow question, but I decided to post here because I'm dealing with the I2C protocol for a PIC MCU in XC8.

I'm learning how to talk with devices over I2C and it's going quite well, but I'm not sure of the best way to deal with a hung state from a not-acknowledge from a slave. The problem comes from the while() loops and the program not progressing until they're met (or not true - you know what I mean). My first method was with a watch dog reset, but I didn't like re-initializing everything any time something goes wrong.

I've consolidated all the writing of the registers to a function:

void I2Cwrite(unsigned char SLAVE, unsigned char REG, unsigned char DATA)
{
    SSPCON2bits.SEN = 1; // RESET
    while(SSPCON2bits.SEN == 1);
    SSPBUF = SLAVE;
    PIR1bits.SSPIF = 0;
    while(PIR1bits.SSPIF == 0);
    while(SSPCON2bits.ACKSTAT == 1);
    SSPBUF = REG;
    PIR1bits.SSPIF = 0;
    while(PIR1bits.SSPIF == 0);
    while(SSPCON2bits.ACKSTAT == 1);
    SSPBUF = DATA;
    PIR1bits.SSPIF = 0;
    while(PIR1bits.SSPIF == 0);
    while(SSPCON2bits.ACKSTAT == 1);
    PIR1bits.SSPIF = 0;
    SSPCON2bits.PEN = 1;
    while(PIR1bits.SSPIF == 0);
}

Then all I have to do is write my data to a function like this:

I2Cwrite(DRVwrite, 0x01, 0x07);
I2Cwrite(DRVwrite, 0x1A, 0b11011110);
I2Cwrite(DRVwrite, 0x16, 0b01010110);
I2Cwrite(DRVwrite, 0x17, 0b01011110);
I2Cwrite(DRVwrite, 0x1E, 0b00110000);
I2Cwrite(DRVwrite, 0x1B, 0b10011100);

If I fall into a hung state mid write, what is a good way to jump to the start of that particular I2C write function and try again?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

The answer involves a Timer 2 to keep track of timing, and loops to give the ability of restarting from the beginning. The main send function is now grouped in a while(1) loop to allow multiple retries or parts. Each section of potential hung state is broken into a PART.

Timer 2 is set to a high priority interrupt, when overflows (because it has been waiting for too long) it sets a variable called TIMEOUT to 1. The ISR looks like:

void __interrupt(high_priority) High_ISR(void)
{
    if(PIR1bits.TMR2IF == 1)
    {
        TIMEOUT = 1;
        PIR1bits.TMR2IF = 0;
    }
}

When the program jumps back after servicing the ISR into waiting in the while loop, the while loops now have a function in them that breaks the wait if TIMEOUT equals 1.

void I2Cwrite(unsigned char SLAVE, unsigned char REG, unsigned char DATA)
{
    while(1)
    {
        TMR2 = 0;
        T2CONbits.TMR2ON = 1;
        
        if(PART == 0)
        {
            SSPCON2bits.SEN = 1; // RESET
            while(SSPCON2bits.SEN == 1);
            SSPBUF = SLAVE;
            PIR1bits.SSPIF = 0;
            while(PIR1bits.SSPIF == 0);
            while(SSPCON2bits.ACKSTAT == 1)
            {
                if(TIMEOUT == 1)
                {
                    break;
                }
            }
            T2CONbits.TMR2ON = 0;
        }
        if(PART == 1)
        {
            SSPBUF = REG;
            PIR1bits.SSPIF = 0;
            while(PIR1bits.SSPIF == 0);
            while(SSPCON2bits.ACKSTAT == 1)
            {
                if(TIMEOUT == 1)
                {
                    break;
                }
            }
            T2CONbits.TMR2ON = 0;
        }
        if(PART == 2)
        {
            SSPBUF = DATA;
            PIR1bits.SSPIF = 0;
            while(PIR1bits.SSPIF == 0);
            while(SSPCON2bits.ACKSTAT == 1)
            {
                if(TIMEOUT == 1)
                {
                    break;
                }
            }
            T2CONbits.TMR2ON = 0;
        }
        if(PART == 3)
        {
            PIR1bits.SSPIF = 0;
            SSPCON2bits.PEN = 1;
            while(PIR1bits.SSPIF == 0)
            {
                if(TIMEOUT == 1)
                {
                    break;
                }
            }
            T2CONbits.TMR2ON = 0;
        }
        if(TIMEOUT == 1) /////// Try again and start from beginning
        {
            PART = 0;
            TIMEOUT = 0;
        }
        else
        {
            if(PART == 3) // If the last part has been sent, break the while(1) loop
            {
                PART = 0;
                break;
            }
            else
            {
                PART++;
            }
        }
    }
}
\$\endgroup\$
3
  • 1
    \$\begingroup\$ I2C writes should never fail with a NAK. Except if it is an intended feature, like an I2C EEPROM being busy during write so it does not respond until ready. So normally a NAK means there is a hardware problem, like chip not present, chip in reset, or not detecting bus transactions properly. Of course it is a nice feature to exit I2C functions if there is an error and report the error in code. But if a chip that is expexted to respond is missing, there is likely other bits communicated unreliably as well, so you can't just assume all other transactions were fine. \$\endgroup\$
    – Justme
    Commented Sep 5, 2022 at 4:28
  • 1
    \$\begingroup\$ That is a nice solution @ezra_vdj, well done for getting there all by yourself. You can now put a timer-checking code into a macro or an inline function to avoid your code being DRY. In the actual production code, you'd usually make the whole I2C transaction interrupt-driven which would avoid blocking code (you'd still use a timer to detect time-outs though) \$\endgroup\$ Commented Sep 5, 2022 at 15:15
  • \$\begingroup\$ Thanks @AndrejsGasilovs ! One of those figure-it-out as you ask it questions I guess. What do you mean by DRY code? \$\endgroup\$
    – ezra_vdj
    Commented Sep 6, 2022 at 3:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.