1.9 Getting Started with I2C

1.9.1 Introduction

Author: Filip Manole, Microchip Technology Inc.

The approach in implementing the I2C communication protocol is different among the PIC18F device family of microcontrollers. While the PIC18-K40 and PIC18-Q10 product families have a Master Synchronous Serial Port (MSSP) peripheral, the PIC18-K42, PIC18-K83, PIC18-Q41, PIC18-Q43 and PIC18-Q84 product families have a dedicated I2C peripheral.

The MSSP and I2C peripherals are serial interfaces useful for communicating with external hardware, such as sensors or microcontroller devices, but there are also differences between them. The MSSP peripheral can operate in one of two modes: Serial Peripheral Interface (SPI) and Inter-Integrated Circuit (I2C), having the advantage of implementing both communication protocols with the same hardware. For a detailed comparison between the MSSP and dedicated I2C peripherals, refer to: Master Synchronous Serial Port (MSSP) to the Stand-Alone I2C Module Migration.

This technical brief provides information about the MSSP peripheral of the PIC18-K40 and PIC18-Q10 product families and intends to familiarize the user with the PIC® microcontrollers. The document covers the following use cases:

  • Host Read/Write Data:

    This example shows how the microcontroller configured in I2C Host mode interacts with different I2C Client devices on the PICkit Serial I2C Demo Board.

  • Host Read/Write Data Using Interrupts:

    This example shows how the microcontroller configured in I2C Host mode writes to and reads data from an MCP23008 8-bit I2C I/O expander (client device), addressed in 7-bit mode, using interrupts.

For each use case, there are three different implementations, which have the same functionalities: one code generated with MPLAB® Code Configurator (MCC), one code generated using Foundation Services Library, and one bare metal code. The MCC generated code offers hardware abstraction layers that ease the use of the code across different devices from the same family. The Foundation Services generated code offers a driver-independent Application Programming Interface (API), and facilitates the portability of code across different platforms. The bare metal code is easier to follow, allowing a fast ramp-up on the use case associated code.

Note: The examples in this technical brief have been developed using PIC18F47Q10 Curiosity Nano development board. The PIC18F47Q10 pin package present on the board is QFN.

1.9.2 Peripheral Overview

The I2C bus is a multi-master serial data communication bus. Microcontrollers communicate in a master/slave environment where the master devices initiate the communication and the devices are selected through addressing.

I2C operates with one or more master devices and one or more slave devices. A given device can operate in four modes:
  • Master Transmit mode – master is transmitting data to a slave
  • Master Receive mode – master is receiving data from a slave
  • Slave Transmit mode – slave is transmitting data to a master
  • Slave Receive mode – slave is receiving data from a master
Figure 1-67. I2C Master/Slave Connection

To begin communication, the master device sends out a Start bit followed by the address byte of the slave it intends to communicate with. This is followed by a bit which determines if the master intends to write to or read from the slave.

If there is a slave on the bus with the indicated address, it will respond with an Acknowledge bit. After the master receives the Acknowledge bit, it can continue in either Writing or Reading mode.

  • If the master intends to write to the slave, then it will send a byte and wait for an Acknowledge bit for each sent byte.
  • If the master intends to read from the slave, then it will receive a byte and respond with an Acknowledge bit for each received byte.
Figure 1-68. I2C Transmission

I2C protocol:

  • The Start bit is indicated by a high-to-low transition of the SDA line while the SCL line is held high
  • The Acknowledge bit is an active-low signal, which holds the SDA line low to indicate to the transmitter that the slave device has received the transmitted data and is ready to receive more
  • A transition of a data bit is always performed while the SCL line is held low. Transitions that occur while the SCL line is held high are used to indicate Start and Stop bits.

The MSSP registers used to configure the device in I2C Master mode:

  • MSSP Control register 1 (SSPxCON1) used to enable the MSSP peripheral and set the device in I2C Master mode
  • MSSP Control register 2 (SSPxCON2) used to send the Start and Stop conditions, set the Receive mode and handle the Acknowledge bits
  • MSSP Data Buffer (SSPxBUF) register used to send the bytes to and receive the bytes from the slave
  • In addition, this is the address the I2C slave responds to
I2C Clock = F_OSC / (4 * (SSP1ADD + 1)

1.9.3 Master Write Data

In this use case, the microcontroller is configured in I2C Master mode using the MSSP1 instance of the MSSP peripheral, and communicates with the slave MCP23008, an 8-bit I/O expander that can be controlled through the I2C interface.

The extended pins are set as digital output with an I2C write operation in the slave’s I/O Direction (IODIR) register.

After the pins are set, the program will repeatedly:
  • set pins to digital low, with an I2C write operation in the GPIO register;
  • set pins to digital high, with an I2C write operation in the GPIO register.

To transmit data as master, the following sequence must be implemented:

  1. Generate the Start condition by setting the SEN bit in the SSPxCON2 register.
  2. The SSPxIF flag in the PIR3 register is set by hardware on completion of the Start condition, and must be cleared by software.
  3. Load the slave address in the SSPxBUF register.
  4. The SSPxIF flag in the PIR3 register is set by hardware and must be cleared by software.
  5. Check the ACKSTAT bit in the SSPxCON2 register.
  6. Load the register address in the SSPxBUF register.
  7. The SSPxIF flag in the PIR3 register is set by hardware and must be cleared by software.
  8. Check the ACKSTAT bit in the SSPxCON2 register.
  9. Load the data in the SSPxBUF register.
  10. The SSPxIF flag in the PIR3 register is set by hardware and must be cleared by software.
  11. Check the ACKSTAT bit in the SSPxCON2 register.
  12. To end the transmission, generate the Stop condition by setting the PEN bit in the SSPxCON2 register.
  13. The SSPxIF flag in the PIR3 register is set by hardware and must be cleared by software.
Note: For a reliable I2C operation, external pull-up resistors must be added. Refer to TB3191 - I2C Master Mode for more details.

1.9.3.1 MCC Generated

To generate this project using MPLAB® Code Configurator (MCC), follow the next steps:

  1. Create a new MPLAB X IDE project for PIC18F47Q10.
  2. Open MCC from the toolbar (more information on how to install the MCC plug-in can be found here).
  3. Go to Project Resources → System → System Module and do the following configuration:
    • Oscillator Select: HFINTOSC
    • HF Internal Clock: 64 MHz
    • Clock Divider: 1
    • In the Watchdog Timer Enable field in the WWDT tab, WDT Disabled has to be selected.
    • In the Programming tab, Low-Voltage Programming Enable has to be checked.
  4. From the Device Resources window, add MSSP1, then do the following configuration:
    • Serial Protocol: I2C
    • Mode: Master
    • I2C Clock Frequency: 100000
  5. Open the Pin Manager → Grid View window, select UQFN40 in the MCU package field, and do the following pin configurations:
    Figure 1-69 1-70 1-71 1-72. Pin Mapping
  6. Go to Project Resources → Pin Module and set both pins, RB1 and RB2, to use the internal pull-up by checking the box in the WPU column. Ensure that for MSSP1, SCL is assigned to pin RB1 and SDA is assigned to RB2 in the pin manager grid view.
  7. In the Project Resources window, click the Generate button so that MCC will generate all the specified drivers and configurations.
  8. Edit the main.c file, as following:
#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/examples/i2c1_master_example.h"

#define I2C_SLAVE_ADDR              0x20
#define MCP23008_REG_ADDR_IODIR     0x00
#define MCP23008_REG_ADDR_GPIO      0x09
#define PINS_DIGITAL_OUTPUT         0x00
#define PINS_DIGITAL_LOW            0x00
#define PINS_DIGITAL_HIGH           0xFF

void main(void)
{
    // Initialize the device
    SYSTEM_Initialize();
    
    /* Set the extended pins as digital output */
    I2C1_Write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_IODIR, PINS_DIGITAL_OUTPUT);
    
    while (1)
    {
        /* Set the extended pins to digital low */
        I2C1_Write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO, PINS_DIGITAL_LOW);
        __delay_ms(500);
        /* Set the extended pins to digital high */
        I2C1_Write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO, PINS_DIGITAL_HIGH);
        __delay_ms(500);
	}
}

1.9.3.2 Foundation Services

To generate this project using Foundation Services (FS), follow the next steps:

  1. Create a new MPLAB X IDE project for PIC18F47Q10.
  2. Open MCC from the toolbar (more information on how to install the MCC plug-in can be found here).
  3. Go to Project Resources → System → System Module and do the following configuration:
    • Oscillator Select: HFINTOSC
    • HF Internal Clock: 64 MHz
    • Clock Divider: 1
    • In the Watchdog Timer Enable field in the WWDT tab, WDT Disabled has to be selected.
    • In the Programming tab, Low-Voltage Programming Enable has to be checked.
  4. From the Device Resources window, add I2CSIMPLE, then do the following configuration:
    • Select I2C Master: MSSP1
  5. Open the Pin Manager → Grid View window, select UQFN40 in the MCU package field, and do the following pin configurations:
    Figure 1-69 1-70 1-71 1-72. Pin Mapping
  6. Go to Project Resources → Pin Module and set both pins, RB1 and RB2, to use the internal pull-up by checking the box in the WPU column.
  7. In the Project Resources window, click the Generate button so that MCC will generate all the specified drivers and configurations.
  8. Edit the main.c file, as following:
#include "mcc_generated_files/mcc.h"

#define I2C_SLAVE_ADDR              0x20
#define MCP23008_REG_ADDR_IODIR     0x00
#define MCP23008_REG_ADDR_GPIO      0x09
#define PINS_DIGITAL_OUTPUT         0x00
#define PINS_DIGITAL_LOW            0x00
#define PINS_DIGITAL_HIGH           0xFF

void main(void)
{
    // Initialize the device
    SYSTEM_Initialize();

    /* Set the extended pins as digital output */
    i2c_write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_IODIR, PINS_DIGITAL_OUTPUT);
    
    while (1)
    {
        /* Set the extended pins to digital low */
        i2c_write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO, PINS_DIGITAL_LOW);
        __delay_ms(500);
        /* Set the extended pins to digital high */
        i2c_write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO, PINS_DIGITAL_HIGH);
        __delay_ms(500);
	}
}

1.9.3.3 Bare Metal Code

The necessary code and functions to implement the presented example are analyzed in this section.

The first step will be to configure the microcontroller to disable the Watchdog Timer and to enable the Low-Voltage Programming (LVP).

#pragma config WDTE = OFF     /* WDT operating mode → WDT Disabled */ 
#pragma config LVP = ON       /* Low-voltage programming enabled, RE3 pin is MCLR */ 

Define the _XTAL_FREQ to the clock frequency and include the used libraries.

#define _XTAL_FREQ                  64000000UL
#include <pic18.h>
#include <xc.h>
#include <stdint.h>

The CLK_Initialize function selects the oscillator and the clock divider, and sets the nominal frequency:

static void CLK_Initialize(void)
{
    /* Set Oscillator Source: HFINTOSC and Set Clock Divider: 1 */
    OSCCON1bits.NOSC = 0x6;

    /* Set Nominal Freq: 64 MHz */
    OSCFRQbits.FRQ3 = 1;
}

The PPS_Initialize function routes the SCL to pin RB1 and SDA to pin RB2:

static void PPS_Initialize(void)
{
    /* PPS setting for using RB1 as SCL */
    SSP1CLKPPS = 0x09;
    RB1PPS = 0x0F;

    /* PPS setting for using RB2 as SDA */
    SSP1DATPPS = 0x0A;
    RB2PPS = 0x10;
}

The PORT_Initialize function sets pins, RB1 and RB2, as digital pins with internal pull-up resistors:

static void PORT_Initialize(void)
{
    /* Set pins RB1 and RB2 as Digital */
    ANSELBbits.ANSELB1 = 0;
    ANSELBbits.ANSELB2 = 0;
    
    /* Set pull-up resistors for RB1 and RB2 */
    WPUBbits.WPUB1 = 1;
    WPUBbits.WPUB2 = 1;
}

The I2C1_Initialize function selects the I2C Master mode and sets the I2C clock frequency to 100 kHz:

static void I2C1_Initialize(void)
{
    /* I2C Master Mode: Clock = F_OSC / (4 * (SSP1ADD + 1)) */
    SSP1CON1bits.SSPM3 = 1;
    
    /* Set the baud rate divider to obtain the I2C clock at 100000 Hz*/
    SSP1ADD  = 0x9F;
}

The I2C1_interruptFlagPolling function waits for the SSP1IF flag to be triggered by the hardware and clears it:

static void I2C1_interruptFlagPolling(void)
{
    /* Polling Interrupt Flag */
    
    while (!PIR3bits.SSP1IF)
    {
        ;
    }

    /* Clear the Interrupt Flag */
    PIR3bits.SSP1IF = 0;
}

The I2C1_open function prepares an I2C operation: Resets the SSP1IF flag and enables the SSP1 module:

static void I2C1_open(void)
{
    /* Clear IRQ */
    PIR3bits.SSP1IF = 0;

    /* I2C Master Open */
    SSP1CON1bits.SSPEN = 1;
}

The I2C1_close function disables the SSP1 module:

static void I2C1_close(void)
{
    /* Disable I2C1 */
    SSP1CON1bits.SSPEN = 0;
}

The I2C1_start function sends the Start bit by setting the SEN bit and waits for the SSP1IF flag to be triggered:

static void I2C1_startCondition(void)
{
    /* Start Condition*/
    SSP1CON2bits.SEN = 1;
    I2C1_interruptFlagPolling();
}

The I2C1_stop function sends the Stop bit and waits for the SSP1IF flag to be triggered:

static void I2C1_stopCondition(void)
{
    /* Stop Condition */
    SSP1CON2bits.PEN = 1;
    I2C1_interruptFlagPolling();
}

The I2C1_sendData function loads in SSP1BUF the argument value and waits for the SSP1IF flag to be triggered:

static void I2C1_sendData(uint8_t byte)
{
    SSP1BUF  = byte;
    I2C1_interruptFlagPolling();
}

The I2C1_getAckstatBit function returns the ACKSTAT bit from the SSP1CON2 register:

static uint8_t I2C1_getAckstatBit(void)
{
    /* Return ACKSTAT bit */
    return SSP1CON2bits.ACKSTAT;
}

The I2C1_write1ByteRegister function executes all the steps to write one byte to the slave:

static void I2C1_write1ByteRegister(uint8_t address, uint8_t reg, uint8_t data)
{
    /* Shift the 7-bit address and add a 0 bit to indicate a write operation */
    uint8_t writeAddress = (address << 1) & ~I2C_RW_BIT;
    
    I2C1_open();
    I2C1_start();
    
    I2C1_sendData(writeAddress);
    if (I2C1_getAckstatBit())
    {
        return ;
    }
    
    I2C1_sendData(reg);
    if (I2C1_getAckstatBit())
    {
        return ;
    }
    
    I2C1_sendData(data);
    if (I2C1_getAckstatBit())
    {
        return ;
    }
    
    I2C1_stop();
    I2C1_close();
}

The main function has multiple responsibilities:

  • Initializes the clock frequency, peripheral pin select, ports and I2C peripheral.
  • Sets the slave’s I/O Direction (IODIR) register to ‘0’, the value for digital output pins.
  • Continuously sets the slave’s General Purpose I/O PORT register to digital low and digital high using I2C write operations.
#define I2C_SLAVE_ADDR                  0x20
#define MCP23008_REG_ADDR_IODIR         0x00
#define MCP23008_REG_ADDR_GPIO          0x09
#define PINS_DIGITAL_OUTPUT             0x00
#define PINS_DIGITAL_LOW                0x00
#define PINS_DIGITAL_HIGH               0xFF

void main(void)
{
    CLK_Initialize();
    PPS_Initialize();
    PORT_Initialize();
    I2C1_Initialize();
    
    /* Set the extended pins as digital output */
    I2C1_write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_IODIR, PINS_DIGITAL_OUTPUT);
    
    while (1)
    {
        /* Set the extended pins to digital low */
        I2C1_write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO, PINS_DIGITAL_LOW);
        __delay_ms(500);
        /* Set the extended pins to digital high */
        I2C1_write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO, PINS_DIGITAL_HIGH);
        __delay_ms(500);
	}
}

1.9.4 Master Read/Write Data Using Interrupts

In this use case, the microcontroller is configured in I2C Master mode using the MSSP1 instance of the MSSP peripheral, and communicates with the slave MCP23008, an 8-bit I/O expander that can be controlled through the I2C interface.

The extended pins are set as digital outputs with an I2C write operation in the slave’s I/O Direction (IODIR) register.

After the pins are set, the program will repeatedly:
  • set the pins to the value of the data variable, with an I2C write operation in the GPIO register;
  • read from the GPIO register, with an I2C read operation and save the value into the data variable;
  • invert the bits in the data variable to write another value into the GPIO register in the next loop.

To read data from the MCP23008 device, the following sequence must be implemented:

  1. Generate the Start condition by setting the SEN bit in the SSPxCON2 register.
  2. The SSPxIF flag in the PIR3 register is set by hardware on completion of the Start condition and must be cleared by software.
  3. Load the slave address in the SSPxBUF register.
  4. The SSPxIF flag in the PIR3 register is set by hardware and must be cleared by software.
  5. Check the ACKSTAT bit in the SSPxCON2 register.
  6. Load the register address in the SSPxBUF register.
  7. The SSPxIF flag in the PIR3 register is set by hardware and must be cleared by software.
  8. Check the ACKSTAT bit in the SSPxCON2 register.
  9. Generate the Start condition by setting the SEN bit.
  10. The SSPxIF flag in the PIR3 register is set by hardware on completion of the Start condition and must be cleared by software.
  11. Load the slave address in the SSPxBUF register.
  12. The SSPxIF flag in the PIR3 register is set by hardware and must be cleared by software.
  13. Check the ACKSTAT bit in the SSPxCON2 register.
  14. Set the RCEN bit to enable the Receive mode.
  15. The SSPxIF flag in the PIR3 register is set by hardware and must be cleared by software.
  16. Read data from the SSPxBUF register.
  17. Send a Not Acknowledge bit to stop receiving bytes.
  18. To end the transmission, generate the Stop condition by setting the PEN bit in the SSPxCON2 register.
  19. The SSPxIF flag in the PIR3 register is set by hardware and must be cleared by software.
Note: For a reliable I2C operation, external pull-up resistors must be added. Refer to TB3191 - I2C Master Mode for more details.

1.9.4.1 MCC Generated

To generate this project using MPLAB Code Configurator (MCC), follow the next steps:

  1. Create a new MPLAB X IDE project for PIC18F47Q10.
  2. Open MCC from the toolbar (more information on how to install the MCC plug-in can be found here).
  3. Go to Project Resources → System → System Module and do the following configuration:
    • Oscillator Select: HFINTOSC
    • HF Internal Clock: 64 MHz
    • Clock Divider: 1
    • In the Watchdog Timer Enable field in the WWDT tab, WDT Disabled has to be selected
    • In the Programming tab, Low-Voltage Programming Enable has to be checked
  4. From the Device Resources window, add MSSP1, then do the following configuration:
    • Interrupt Driven: Checked
    • Serial Protocol: I2C
    • Mode: Master
    • I2C Clock Frequency: 100000
  5. Open the Pin Manager → Grid View window, select UQFN40 in the MCU package field, and do the following pin configurations:
    Figure 1-69 1-70 1-71 1-72. Pin Mapping
  6. Go to Project Resources → Pin Module and set both pins, RB1 and RB2, to use the internal pull-up by checking the box in the WPU column.
  7. In the Project Resources window, click the Generate button so that MCC will generate all the specified drivers and configurations.
  8. Edit the main.c file, as following:
#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/examples/i2c1_master_example.h"


#define I2C_SLAVE_ADDR              0x20
#define MCP23008_REG_ADDR_IODIR     0x00
#define MCP23008_REG_ADDR_GPIO      0x09
#define PINS_DIGITAL_OUTPUT         0x00
#define MCP23008_DATA               0x0F

void main(void)
{
    // Initialize the device
    SYSTEM_Initialize();

    // Enable the Global Interrupts
    INTERRUPT_GlobalInterruptEnable();

    // Enable the Peripheral Interrupts
    INTERRUPT_PeripheralInterruptEnable();
    
    /* Set data to use in the I2C operations */
    uint8_t data = MCP23008_DATA;
    /* Set the extended pins as digital output */
    I2C1_Write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_IODIR, PINS_DIGITAL_OUTPUT);

    while (1)
    {
        /* Write data to the GPIO port */
        I2C1_Write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO, data);
        /* Read data from the GPIO port */
        data = I2C1_Read1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO);
        /* Overwrite data with the inverted data read */
        data = ~data;

        __delay_ms(500);
	}
}

1.9.4.2 Foundation Services

To generate this project using Foundation Services (FS), follow the next steps:

  1. Create a new MPLAB X IDE project for PIC18F47Q10.
  2. Open MCC from the toolbar (more information on how to install the MCC plug-in can be found here).
  3. Go to Project Resources → System → System Module and do the following configuration:
    • Oscillator Select: HFINTOSC
    • HF Internal Clock: 64 MHz
    • Clock Divider: 1
    • In the Watchdog Timer Enable field in the WWDT tab, WDT Disabled has to be selected
    • In the Programming tab, Low-Voltage Programming Enable has to be checked
  4. From the Device Resources window, add I2CSIMPLE, then do the following configuration:
    • Select I2C Master: MSSP1
  5. Go to Device Resources → Peripherals → MSSP1, then check the Interrupt Driven box.
  6. Open the Pin Manager → Grid View window, select UQFN40 in the MCU package field and do the following pin configurations:
    Figure 1-69 1-70 1-71 1-72. Pin Mapping
  7. Go to Project Resources → Pin Module, and set both pins, RB1 and RB2, to use the internal pull-up by checking the box in the WPU column.
  8. In the Project Resources window, click the Generate button so that MCC will generate all the specified drivers and configurations.
  9. Edit the main.c file, as following:
#include "mcc_generated_files/mcc.h"

#define I2C_SLAVE_ADDR              0x20
#define MCP23008_REG_ADDR_IODIR     0x00
#define MCP23008_REG_ADDR_GPIO      0x09
#define PINS_DIGITAL_OUTPUT         0x00
#define MCP23008_DATA               0x0F

/*
                         Main application
 */
void main(void)
{
    // Initialize the device
    SYSTEM_Initialize();

    // Enable the Global Interrupts
    INTERRUPT_GlobalInterruptEnable();

    // Enable the Peripheral Interrupts
    INTERRUPT_PeripheralInterruptEnable();

    /* Set data to use in the I2C operations */
    uint8_t data = MCP23008_DATA;
    /* Set the extended pins as digital output */
    i2c_write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_IODIR, PINS_DIGITAL_OUTPUT);
    
    while (1)
    {
        /* Write data to the GPIO port */
        i2c_write1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO, data);
        /* Read data from the GPIO port */
        data = i2c_read1ByteRegister(I2C_SLAVE_ADDR, MCP23008_REG_ADDR_GPIO);
        /* Overwrite data with the inverted data read */
        data = ~data;
        
        __delay_ms(500);
	}
}

1.9.4.3 Bare Metal Code

The necessary code and functions to implement the presented example are analyzed in this section.

The first step will be to configure the microcontroller to disable the Watchdog Timer and to enable the Low-Voltage Programming (LVP).

#pragma config WDTE = OFF     /* WDT operating mode → WDT Disabled */ 
#pragma config LVP = ON       /* Low-voltage programming enabled, RE3 pin is MCLR */ 

The CLK_Initialize function selects the HFINTOSC oscillator and the clock divider, and sets the nominal frequency to 64 MHz:

static void CLK_Initialize(void)
{
    /* Set Oscillator Source: HFINTOSC and Set Clock Divider: 1 */
    OSCCON1bits.NOSC = 0x6;

    /* Set Nominal Freq: 64 MHz */
    OSCFRQbits.FRQ3 = 1;
}

The PPS_Initialize function routes the SCL to pin RB1 and SDA to pin RB2:

static void PPS_Initialize(void)
{
    /* PPS setting for using RB1 as SCL */
    SSP1CLKPPS = 0x09;
    RB1PPS = 0x0F;

    /* PPS setting for using RB2 as SDA */
    SSP1DATPPS = 0x0A;
    RB2PPS = 0x10;
}

The PORT_Initialize function sets pins, RB1 and RB2, as digital pins with internal pull-up resistors:

static void PORT_Initialize(void)
{
    /* Set pins RB1 and RB2 as Digital */
    ANSELBbits.ANSELB1 = 0;
    ANSELBbits.ANSELB2 = 0;
    
    /* Set pull-up resistors for RB1 and RB2 */
    WPUBbits.WPUB1 = 1;
    WPUBbits.WPUB2 = 1;
}

The I2C1_Initialize function selects the I2C Master mode and the baud rate divider:

static void I2C1_Initialize(void)
{
    /* I2C Master Mode: Clock = F_OSC / (4 * (SSP1ADD + 1)) */
    SSP1CON1bits.SSPM3 = 1;
    
    /* Set the baud rate divider to obtain the I2C clock at 100000 Hz*/
    SSP1ADD  = 0x9F;
}

The INTERRUPT_Initialize function enables the global and peripheral interrupts:

static void INTERRUPT_Initialize(void)
{
    /* Enable the Global Interrupts */
    INTCONbits.GIE = 1;
    /* Enable the Peripheral Interrupts */
    INTCONbits.PEIE = 1;
}

The following functions are part of the I2C driver. Their implementation can be found below, at the GitHub link.

static uint8_t I2C1_open(void);
static void I2C1_close(void);
static void I2C1_startCondition(void);
static void I2C1_stopCondition(void);
static uint8_t I2C1_getAckstatBit(void);
static void I2C1_sendNotAcknowledge(void);
static void I2C1_setReceiveMode(void);
static void I2C1_write1ByteRegister(uint8_t address, uint8_t reg, uint8_t data);
static uint8_t I2C1_read1ByteRegister(uint8_t address, uint8_t reg);

The following functions are associated with an I2C transmission state. Their implementation can be found below, at the GitHub link.

static void I2C_stateWriteStartComplete(void);
static void I2C_stateWriteAddressSent(void);
static void I2C_stateWriteRegisterSent(void);
static void I2C_stateWriteDataSent(void);
static void I2C_stateReadStart(void);
static void I2C_stateReadStartComplete(void);
static void I2C_stateReadAddressSent(void);
static void I2C_stateReadReceiveEnable(void);
static void I2C_stateReadDataComplete(void);
static void I2C_stateStopComplete(void);

The MSSP1_interruptHandler function is called every time the SSP1IF flag is triggered. This handler must execute different operations, depending on the current state, which are stored in I2C1_status.state.

The I2C_stateFuncs vector contains all the function pointers associated with all the I2C transmission states.

The MSSP1_interruptHandler function calls the function for the current state, where the state is updated, after which the SSP1IF flag is cleared.

static void MSSP1_interruptHandler(void)
{
    /* Call the function associated with the current state */
    I2C_stateFuncs[I2C1_status.state]();
    
    /* Clear the Interrupt Flag */
    PIR3bits.SSP1IF = 0;
}

void __interrupt() INTERRUPT_InterruptManager (void)
{
    if(INTCONbits.PEIE == 1)
    {
        if(PIE3bits.SSP1IE == 1 && PIR3bits.SSP1IF == 1)
        {
            MSSP1_interruptHandler();
        }
    }
}

The main function has multiple responsibilities:

  • Initializes the clock frequency, Peripheral Pin Select, ports and I2C peripheral
  • Enables the peripheral and global interrupts
  • Sets the slave’s I/O Direction (IODIR) register to ‘0’, the value for digital output pins
  • Continuously sets the extended port to the value data with a write operation in the slave’s General Purpose I/O PORT Register; reads the value from the same register and inverts the read value
#define I2C_SLAVE_ADDRESS           0x20
#define MCP23008_REG_ADDR_IODIR     0x00
#define MCP23008_REG_ADDR_GPIO      0x09
#define MCP23008_DATA               0x0F
#define PINS_DIGITAL_OUTPUT         0x00

void main(void)
{
    CLK_Initialize();
    PPS_Initialize();
    PORT_Initialize();
    I2C1_Initialize();
    INTERRUPT_Initialize();
    
    /* Set the initial state to Idle */
    I2C1_status.state = I2C_IDLE;
    /* Set data to use in the I2C operations */
    uint8_t data = MCP23008_DATA;
    /* Set the extended pins as digital output */
    I2C1_write(I2C_SLAVE_ADDRESS, MCP23008_REG_ADDR_IODIR, PINS_DIGITAL_OUTPUT);
    
    while (1)
    {
        /* Write data to the GPIO port */
        I2C1_write(I2C_SLAVE_ADDRESS, MCP23008_REG_ADDR_GPIO, data);
        /* Read data from the GPIO port */
        data = I2C1_read(I2C_SLAVE_ADDRESS, MCP23008_REG_ADDR_GPIO);
        /* Overwrite data with the inverted data read */
        data = ~data;

        __delay_ms(500);
    }
}