Electronic – dsPIC33E – I2C master interrupt flag is never set

i2cinterruptsmicrochipmicrocontroller

I'm trying to design a program that polls a real-time clock (DS1307+) every second via I2C. The microcontroller used is dsPIC33EP64GS506. I use a master interrupt flag to wait until a task (start or stop condition, byte send etc.) is finished. However, it seems that the master interrupt flag is never set. Here is a minimum (non)working example (at the end of the post). I went through several application notes, datasheets etc., unfortunately, I didn't manage to solve the problem.

The program is stuck on the very first call of the function I2C1_Wait_While_Busy(), which uses a master interrupt flag. This function is first used after sending a START condition. However, there is another way to check if START condition finished:

while (I2C1CON1bits.SEN==1);

since SEN bit is cleared by hardware after START condition finishes. If I use this, then the program skips to the next line, which indicates that start condition finished regardless to the master interrupt flag. In other words, the program is stuck on the next call of I2C1_Wait_While_Busy() function.

#include <xc.h>

// CPU operates at 120 MHz (60 MIPS)
#define FCY     60000000UL
#include <libpic30.h>

/* INITIALIZATION ROUTINES */

// Primary oscillator configuration
_FOSCSEL(FNOSC_FRC & IESO_OFF);
_FOSC(PLLKEN_ON & FCKSM_CSECMD & OSCIOFNC_OFF & POSCMD_XT);

// ALTI2C1_ON - I2C1 mapped to ASDA1/ASCL1 instead of SDA1/SCL1
// ALTI2C2_ON - I2C2 mapped to ASDA2/ASCL2 instead of SDA2/SCL2
// DBCC (not used)
_FDEVOPT(ALTI2C1_ON & ALTI2C2_ON);

void Init_Oscillator(void) {

    /* Configure primary oscillator */
    // FOSC = FIN/N1*M/N2

    // FPLLI = FIN/N1
    // N1 = PLLPRE+2, N1 in [2,33]
    short N1 = 2;
    CLKDIVbits.PLLPRE = N1-2; // N1=PLLPRE+2=2

    // FVCO = FPLLI*M
    // M = PLLDIV+2, M in [2,513]
    short M = 48;
    PLLFBD = M-2;

    // FPLLO = FVCO/N2 (FPLLO=FOSC, FCY=FOSC/2)
    // N2 = 2*(PLLPOST+1), N2 is {2,4,8}
    short N2 = 2; // 2, 4 or 8
    CLKDIVbits.PLLPOST = N2/2-1;

    /* Initiate Clock Switch */

    // Primary Oscillator with PLL (XTPLL, HSPLL, ECPLL)
    __builtin_write_OSCCONH(0x03); // 0b011 -> NOSC<2:0> (OSCCON<10:8>)

    // Request oscillator switch to selection specified by the NOSC<2:0> bits
    __builtin_write_OSCCONL(OSCCON | 0x01); // 0b1 -> OSWEN<0> (OSCCON<0>)

    // Wait for Clock switch to occur
    // Current Oscillator Selection bits (read-only): COSC<2:0> (OSCCON<15:13>)
    while (OSCCONbits.COSC!=0b011);

    // Wait for PLL to lock
    // PLL Lock Status bit (read-only): LOCK<0> (OSCCON<5>)
    while (OSCCONbits.LOCK!=1);

    /* Configure auxiliary oscillator */
    // 120 MHz for proper PWM and ADC operation
    // 7.37 MHz * 16 = 117.92 MHz

    // Configure source to fast RC with APLL
    ACLKCONbits.ASRCSEL = 0; // Don't use primary oscillator
    ACLKCONbits.FRCSEL = 1; // Use fast RC oscillator as source
    ACLKCONbits.SELACLK = 1; // Don't use primary PLL (FVCO)
    ACLKCONbits.APSTSCLR = 0b111; // Divide-by-1 for PWM
    ACLKCONbits.ENAPLL = 1; // Enable 16x APLL

    // Wait for auxiliary PLL to lock
    while (ACLKCONbits.APLLCK!=1);

}

void Init_Ports(void) {

    // Set all ports to digital
    ANSELA = 0x00;
    ANSELB = 0x00;
    ANSELC = 0x00;
    ANSELD = 0x00;

    // Set all ports to low
    LATA = 0x00;
    LATB = 0x00;
    LATC = 0x00;
    LATD = 0x00;

}

void Init_I2C(void) {

    // Set baud rate (100 kHz)
    I2C1BRG = 291; // FSCL=100kHz, Tdelay=250ns, FP/2=30MHz

//    // Enable master interrupts
//    IPC4bits.MI2C1IP = 1; // interrupt priority
//    IEC1bits.MI2C1IE = 1; // enable interrupt
//    IFS1bits.MI2C1IF = 0; // clear interrupt flag

    // Enable I2C1
    I2C1CON1bits.I2CEN = 1;

}

/* I2C ROUTINES */

void I2C1_Send_Byte(char byte) {
    // Load I2C1 transmit register
    I2C1TRN = byte;
}

void I2C1_Wait_While_Busy() {
    // Check if I2C1 operation is done
    while(IFS1bits.MI2C1IF==0);

    // I2C1 is ready, clear interrupt flag
    IFS1bits.MI2C1IF = 0;
}

void I2C1_Check_Ack_Status() {
    if (I2C1STATbits.ACKSTAT==1) {
        // NACK, do nothing
    } else {
        // ACK, do nothing
    }
}

void I2C_RTC_Get_Date_and_Time() {

    int b;
    char data[7];

    // Reset master interrupt flag
    IFS1bits.MI2C1IF = 0;

    // Wait for idle state
    while(I2C1STATbits.TRSTAT);

    /* MASTER TRANSMITTER / SLAVE RECEIVER */

    // Send START condition
    I2C1CON1bits.SEN = 1;
//    I2C1_Wait_While_Busy();
    while (I2C1CON1bits.SEN==1);

    // Call the RTC in receive mode
    I2C1_Send_Byte(0xD0); // 1101000|0
    I2C1_Wait_While_Busy();
    I2C1_Check_Ack_Status();

    // Send the first RTC register address
    I2C1_Send_Byte(0x00);
    I2C1_Wait_While_Busy();
    I2C1_Check_Ack_Status();

    /* MASTER RECEIVER / SLAVE TRANSMITTER */

    do {
        // Send RESTART condition
        I2C1CON1bits.RSEN = 1;
        I2C1_Wait_While_Busy();

        // Reverse direction (RTC is transmitter)
        I2C1_Send_Byte(0xD1); // 1101000|1
        I2C1_Wait_While_Busy();
    } while (I2C1STATbits.ACKSTAT==1);

    // Set to ACK bit
    I2C1CON1bits.ACKDT = 0;

    // Receive 7 bytes of data from RTC
    for (b=0; b<7; b++) {
        // Enable receive mode
        I2C1CON1bits.RCEN = 1;
        I2C1_Wait_While_Busy();

        // Get byte from buffer
        data[b] = I2C1RCV;

        // Set to NACK bit after last received byte
        if (b==6)
            I2C1CON1bits.ACKDT = 1;

        // Master acknowledge
        I2C1CON1bits.ACKEN = 1;
    }

    // Send STOP condition
    I2C1CON1bits.PEN = 1;
    I2C1_Wait_While_Busy();

}

/* MAIN ROUTINE */

int main(void) {

    Init_Oscillator();

    Init_Ports();

    Init_I2C();

    while(1) {
        // Poll RTC every second
        I2C_RTC_Get_Date_and_Time();
        __delay_ms(1000);
    }

}

Best Answer

I managed to solve this problem.

When you halt the processor in the middle of the I2Cx communication with a slave, the slave might hold the SDAx line low on next start (or restart), in that way blocking any further communication. This is exactly what happened to me with DS1307+ real-time clock.

This can be solved by generating 10 clock periods (in that case SCLx is configured as a digital output) before enabling I2Cx. After that, the slave will release the SDAx line. Something like this:

void Generate_10_Clock_Pulses_I2C1(void) {

    char i;

    // Set initial clock level to HIGH
    I2C1_SCL = 1;
    __delay_us(5);

    // Generate 10 clock periods
    // Frequency: 100 kHz (10 us)
    for (i=0; i<20; i++) {
        I2C1_SCL = ~I2C1_SCL; // Macro for LATCbits.LATC8 (ASCL1)
        __delay_us(5); // 50%, 100 kHz
    }

}