I²C Communication with PIC Microcontroller – MPLAB XC8

I²C Bus Logo

I²C Communication with PIC Microcontroller – MPLAB XC8

I2C or IIC or I2C stands for Inter-Integrated Circuit. It is a very popular multi-master, multi-slave serial communication interface developed by Philips. I2C uses two bidirectional open drain data lines, Serial Data (SDA) and Serial Clock (SCL) with pull up resistors as shown below. Unlike UART, you can connect and communicate to multiple devices using the same I2C bus.

I2C Communication

I2C Communication

I2C is a master slave protocol. It means that devices connected to I2C bus will be either master or slave. The master is the device which initiate communication and it drives clock (SCL) line. Slaves are the devices which responds to master and it cannot initiate a communication. An I2C bus can have multiple masters and multiple slaves. But commonly we are using single master and multiple slaves. Each slaves are identified or addressed by a unique address. The master will send address of slave + R/W bit first, then followed by other data. So the slave with that particular address will be activated at that moment. R/W bit indicates whether the master wants to read data from or write data to the slave. For example, we can have a microcontroller or host device which is connected to different slave devices like I/O Port Expanders, LED/LCD Drivers, ADCs, DACs, EEPROMs, Real Time Clock (RTC) etc.

Most of the PIC microcontrollers have built in Master Synchronous Serial Port (MSSP) module which can be configured to operate in following modes.

  • Serial Peripheral Interface (SPI)
  • Inter-Integrated Circuit (I2C) – Slave, Master & Multi-master modes

In this tutorial we will learn how to operate MSSP module of PIC Microcontroller as I2C master or slave. For demonstration we are using PIC 16F877A microcontroller. You can easily convert it for other microcontrollers if you understand it clearly.


MSSP Module in I2C Mode

Let’s see in detail about working of MSSP module of PIC Microcontroller in I²C mode. MSSP module can be configured to operate in both 10 bit and 7 bit address mode. In this example we are demonstrating 7 bit mode only as it is the commonly used one. You can easily modify the program to work in 10 bit mode.

I2C Master

We need to write to SSPCON1 and SSPADD registers to configure MSSP module as I²C Master and to set the clock frequency of I²C communication respectively.

I2C Master Block Diagram - PIC Microcontroller

I2C Master Block Diagram – PIC Microcontroller

I2C Slave

Similar to above, we need to write to SSPCON1 and SSPADD registers to configure MSSP module in I²C slave mode and to set the slave device address respectively.

I2C Slave Block Diagram - PIC Microcontroller

I2C Slave Block Diagram – PIC Microcontroller

Clock generated by the master on SCL line will cause the data to shift in and shift out of the SSPSR register which makes the I2C communication happen.



I2C Registers

SSPSTAT – MSSSP Status Register

SSPSTAT Register MSSP Module PIC 16F877A

SSPSTAT Register MSSP Module PIC 16F877A

  • Bit 0 BF : This is the Buffer Full status bit. In the transmit mode this bit will set when we write data to SSPBUF register and it is cleared when the data is shifted out. In the receive mode this bit will set when the data or address is received in the SSPBUF register and it is cleared when we reads the SSPBUF register.
  • Bit 1 UA : This is the Update Address bit and is used only in 10 bit address mode. It indicates that user needs to update the address in the SSPADD register.
  • Bit 2 R/W : This is the Read/Write bit information. In the slave mode it indicates the status of R/W bit during the last address match. In the master mode, 1 indicates that transmit is in progress and vice versa.
  • Bit 3 S : This bit indicates that a Start bit is detected last and it will automatically cleared during Reset.
  • Bit 4 P : As above, this bit indicates that a Stop bit is detected last and it will automatically cleared during Reset.
  • Bit 5 D/A : This is the data or address indicator bit and it is used only in slave mode. If it is set, the last byte received was data otherwise it will be address.
  • Bit 6 CKE : Setting this bit enables SMBus specific inputs. SMBus is an another bus similar to I2C, which are compatible each other.
  • Bit 7 SMP : Setting this bit disables slew rate control and vice versa.

SSPCON1 – MSSP Control Register 1

SSPCON1 Register MSSP Module PIC 16F877A

SSPCON1 Register MSSP Module PIC 16F877A

  • Bit 0 ~ 3 SSPM0 ~ SSPM3 : These are synchronous serial port mode select bits.
Synchronous Serial Port Select bits - PIC 16F877A

Synchronous Serial Port Mode Select bits – PIC 16F877A

  • Bit 4  CKP : This is SCL clock release control bit. It is used only in slave mode. Setting this bit releases the clock. If zero, it holds the clock (clock stretch).
  • Bit 5 : SSPEN :  This is the synchronous serial port enable bit. Setting this bit enables the serial port.
  • Bit 6 : SSPOV : This is receive over flow indicator bit. If this bit is set during receive mode, it indicates that a byte is received while SSPBUF is holding the previous value. And it has no application in transmit mode. We must clear this bit in software.
  • Bit 7 WCOL : It is the write collision detect bit. If this bit is set during master transmit mode, it indicates that a write to SSPBUF register was attempted when I2C conditions was not valid for a transmission to be started. And if it is set during a slave transmit mode, it indicates that SSPBUF register is written when it is transmitting the previous word. We must clear this bit in software.

SSPCON2 – MSSP Control Register 2

SSPCON2 MSSP Module PIC 16F877A

SSPCON2 MSSP Module PIC 16F877A

  • Bit 0 SEN : Start Condition or Stretch Enable bit. In master mode, setting this bit initiate start condition on SCL & SDA pins and it will be automatically cleared by the hardware. And in slave mode setting this bit enables clock stretching for both slave receive and slave transmit. If it is cleared in slave mode, clock stretching is enabled only for slave transmit.
  • Bit 1 RSEN : Repeated start condition enable bit. This bit has application only in master mode. Setting this bit will initiate repeated start condition on both SCL & SDA pins and it will automatically cleared in hardware.
  • Bit 2 PEN : Stop condition enable bit. This bit has application only in master mode. Setting this bit will initiate stop condition on both SCL & SDA pins and it will be automatically cleared in hardware.
  • Bit 3 RCEN : Receive enable bit. This bit also has application only in master mode. Setting this bit enables receive mode for I2C.
  • Bit 4 ACKEN : Acknowledge sequence enable bit. Setting this bit initiates acknowledge sequence on SCL & SDA lines and it will send ACKDT (see below) bit. This bit will be automatically cleared in hardware. It has application only in master receive mode.
  • Bit 5 ACKDT : Acknowledge data bit. 1 means not acknowledge and 0 means acknowledge. This value will be transmitted when we set the ACKEN bit (above). This bit has application only in master receive mode.
  • Bit 6 ACKSTAT : Acknowledge status bit. 1 indicates that acknowledge was not received from the slave and vice versa. This bit has application in master transmit mode only.
  • Bit 7 GCEN : General call enable bit. Setting this bit enables interrupt when a general call address is received in the register SSPSR.

I2C Library for MPLAB XC8

Master Functions

Initialize I2C Module as Master

void I2C_Master_Init(const unsigned long c)
{
  SSPCON = 0b00101000;            //SSP Module as Master
  SSPCON2 = 0;
  SSPADD = (_XTAL_FREQ/(4*c))-1; //Setting Clock Speed
  SSPSTAT = 0;
  TRISC3 = 1;                   //Setting as input as given in datasheet
  TRISC4 = 1;                   //Setting as input as given in datasheet
}

For Waiting

void I2C_Master_Wait()
{
  while ((SSPSTAT & 0x04) || (SSPCON2 & 0x1F)); //Transmit is in progress
}

Start Condition

void I2C_Master_Start()
{
  I2C_Master_Wait();    
  SEN = 1;             //Initiate start condition
}

Repeated Start

void I2C_Master_RepeatedStart()
{
  I2C_Master_Wait();
  RSEN = 1;           //Initiate repeated start condition
}

Stop Condition

void I2C_Master_Stop()
{
  I2C_Master_Wait();
  PEN = 1;           //Initiate stop condition
}

Write Data

void I2C_Master_Write(unsigned d)
{
  I2C_Master_Wait();
  SSPBUF = d;         //Write data to SSPBUF
}

Read Data

unsigned short I2C_Master_Read(unsigned short a)
{
  unsigned short temp;
  I2C_Master_Wait();
  RCEN = 1;
  I2C_Master_Wait();
  temp = SSPBUF;      //Read data from SSPBUF
  I2C_Master_Wait();
  ACKDT = (a)?0:1;    //Acknowledge bit
  ACKEN = 1;          //Acknowledge sequence
  return temp;
}

Slave Functions

Initialize module as slave

void I2C_Slave_Init(short address) 
{
  SSPSTAT = 0x80;    
  SSPADD = address; //Setting address
  SSPCON = 0x36;    //As a slave device
  SSPCON2 = 0x01;
  TRISC3 = 1;       //Setting as input as given in datasheet
  TRISC4 = 1;       //Setting as input as given in datasheet
  GIE = 1;          //Global interrupt enable
  PEIE = 1;         //Peripheral interrupt enable
  SSPIF = 0;        //Clear interrupt flag
  SSPIE = 1;        //Synchronous serial port interrupt enable
}

On Receive interrupt

Note : This is not a general function, so you need to modify it according to your application.

void interrupt I2C_Slave_Read()
{
  if(SSPIF == 1)
  {
    SSPCONbits.CKP = 0;

    if ((SSPCONbits.SSPOV) || (SSPCONbits.WCOL)) //If overflow or collision
    {
      z = SSPBUF; // Read the previous value to clear the buffer
      SSPCONbits.SSPOV = 0; // Clear the overflow flag
      SSPCONbits.WCOL = 0; // Clear the collision bit
      SSPCONbits.CKP = 1;
    }
  }

  if(!SSPSTATbits.D_nA && !SSPSTATbits.R_nW) //If last byte was Address + Write
  {
    z = SSPBUF;
    while(!BF);
    PORTD = SSPBUF;
    SSPCONbits.CKP = 1;
  }
  else if(!SSPSTATbits.D_nA && SSPSTATbits.R_nW) //If last byte was Address + Read
  {
    z = SSPBUF;
    BF = 0;
    SSPBUF = PORTB ;
    SSPCONbits.CKP = 1;
    while(SSPSTATbits.BF);
  }

  SSPIF = 0;
  }
}

PIC to PIC Communication using I2C

In this example we have bidirectional communication between two pic microcontrollers. 8 bit switches and 8 LEDs are connected to each microcontroller. We will be controlling LEDs connected to a PIC with switches connected to other microcontroller.

Circuit Diagram

PIC to PIC Communication using I2C - Circuit Diagram

PIC to PIC Communication using I2C – Circuit Diagram

Hope you can easily understand above circuit diagram. You can make any of above 2 microcontroller as master, it depends only on the program written on it. 8MHz crystals are used for providing required clock for the operation of microcontrollers. An RCR (Resistor-Capacitor-Resistor) made using 10KΩ, 0.1µF, 4.7KΩ is connected to MCLR pin of PIC Microcontroller as recommended by Microchip. It will work fine even if you tie MCLR pin directly to VDD but Microchip doesn’t recommends it. 680Ω resistors are used to limit current through LEDs which are connected to PORTD of PIC microcontroller. 8 bit dip switches are connected to the PORTB of PIC microcontroller. Here we are using Internal Pull Up of PORTB. Hence no external resistors are required for switches.

MPLAB XC8 – Code

Since circuit of both microcontrollers are identical, you can use any of the 2 microcontrollers are master or slave.



Master

// PIC16F877A Configuration Bit Settings
// 'C' source line config statements
#include <xc.h>
#include <pic16f877a.h>

#define _XTAL_FREQ 8000000

// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.

// CONFIG
#pragma config FOSC = XT   // Oscillator Selection bits (XT oscillator)
#pragma config WDTE = OFF  // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled)
#pragma config BOREN = OFF // Brown-out Reset Enable bit (BOR disabled)
#pragma config LVP = OFF   // Low-Voltage In-Circuit Serial Programming Enable bit
#pragma config CPD = OFF   // Data EEPROM Memory Code Protection bit 
#pragma config WRT = OFF   // Flash Program Memory Write Enable bits 
#pragma config CP = OFF    // Flash Program Memory Code Protection bit

void I2C_Master_Init(const unsigned long c)
{
  SSPCON = 0b00101000;
  SSPCON2 = 0;
  SSPADD = (_XTAL_FREQ/(4*c))-1;
  SSPSTAT = 0;
  TRISC3 = 1;        //Setting as input as given in datasheet
  TRISC4 = 1;        //Setting as input as given in datasheet
}

void I2C_Master_Wait()
{
  while ((SSPSTAT & 0x04) || (SSPCON2 & 0x1F));
}

void I2C_Master_Start()
{
  I2C_Master_Wait();
  SEN = 1;
}

void I2C_Master_RepeatedStart()
{
  I2C_Master_Wait();
  RSEN = 1;
}

void I2C_Master_Stop()
{
  I2C_Master_Wait();
  PEN = 1;
}

void I2C_Master_Write(unsigned d)
{
  I2C_Master_Wait();
  SSPBUF = d;
}

unsigned short I2C_Master_Read(unsigned short a)
{
  unsigned short temp;
  I2C_Master_Wait();
  RCEN = 1;
  I2C_Master_Wait();
  temp = SSPBUF;
  I2C_Master_Wait();
  ACKDT = (a)?0:1;
  ACKEN = 1;
  return temp;
}

void main()
{
  nRBPU = 0;                    //Enable PORTB internal pull up resistor
  TRISB = 0xFF;                 //PORTB as input
  TRISD = 0x00;                 //PORTD as output
  PORTD = 0x00;                 //All LEDs OFF
  I2C_Master_Init(100000);      //Initialize I2C Master with 100KHz clock
  while(1)
  {
    I2C_Master_Start();         //Start condition
    I2C_Master_Write(0x30);     //7 bit address + Write
    I2C_Master_Write(PORTB);    //Write data
    I2C_Master_Stop();          //Stop condition
    __delay_ms(200);
    I2C_Master_Start();         //Start condition
    I2C_Master_Write(0x31);     //7 bit address + Read
    PORTD = I2C_Master_Read(0); //Read + Acknowledge
    I2C_Master_Stop();          //Stop condition
    __delay_ms(200);
  }
}

Slave

// PIC16F877A Configuration Bit Settings
// 'C' source line config statements

#include <xc.h>
#include <pic16f877a.h>
#define _XTAL_FREQ 8000000

// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.

// CONFIG
#pragma config FOSC = XT    // Oscillator Selection bits (XT oscillator)
#pragma config WDTE = OFF   // Watchdog Timer Enable bit (WDT disabled)
#pragma config PWRTE = OFF  // Power-up Timer Enable bit (PWRT disabled)
#pragma config BOREN = OFF  // Brown-out Reset Enable bit (BOR disabled)
#pragma config LVP = OFF    // Low-Voltage In-Circuit Serial Programming Enable bit
#pragma config CPD = OFF    // Data EEPROM Memory Code Protection bit
#pragma config WRT = OFF    // Flash Program Memory Write Enable bits
#pragma config CP = OFF     // Flash Program Memory Code Protection bit
short z;
void interrupt I2C_Slave_Read()
{
  if(SSPIF == 1)
  {
    SSPCONbits.CKP = 0;

    if ((SSPCONbits.SSPOV) || (SSPCONbits.WCOL))
    {
      z = SSPBUF; // Read the previous value to clear the buffer
      SSPCONbits.SSPOV = 0; // Clear the overflow flag
      SSPCONbits.WCOL = 0;  // Clear the collision bit
      SSPCONbits.CKP = 1;
    }

    if(!SSPSTATbits.D_nA && !SSPSTATbits.R_nW)
    {
      z = SSPBUF;
      while(!BF);
      PORTD = SSPBUF;
      SSPCONbits.CKP = 1;
    }
    else if(!SSPSTATbits.D_nA && SSPSTATbits.R_nW)
    {
      z = SSPBUF;
      BF = 0;
      SSPBUF = PORTB ;
      SSPCONbits.CKP = 1;
      while(SSPSTATbits.BF);
    }

    SSPIF = 0;
  }
}

void I2C_Slave_Init(short address)
{
  SSPSTAT = 0x80;
  SSPADD = address;
  SSPCON = 0x36;
  SSPCON2 = 0x01;
  TRISC3 = 1;   //Setting as input as given in datasheet
  TRISC4 = 1;   //Setting as input as given in datasheet
  GIE = 1;
  PEIE = 1;
  SSPIF = 0;
  SSPIE = 1;
}

void main()
{
  nRBPU = 0;            //Enables PORTB internal pull up resistors
  TRISB = 0xFF;         //PORTB as input
  TRISD = 0x00;         //PORTD as output
  PORTD = 0x00;         //All LEDs OFF
  I2C_Slave_Init(0x30); //Initialize as a I2C Slave with address 0x30
  while(1);
}

I hope that you understand above programs. Please do comment if you have any doubts.

Download Here

You can download the complete MPLAB program and Proteus simulation files here.

Share this post

  • Stephen Madeira

    Not working! The program freezes in Wait function

  • It is 100% working. It may freeze if the slave device is not responding.

  • Leaf Hurricane

    You’re a legend. 😉

  • Manasa Ramprasad

    CAN I GET A CODE FOR INTERFACING PIC16F18855 WITH DS1307(RTC). PLEASE HELP ME

  • Manasa Ramprasad

    I NEED CODE IN MP LAB FOR INTERFACING PIC16F18855 WITH RTC THROUGH I2C PROTOCOL

  • Manasa Ramprasad

    DO WE NEED TO HAVE SEPARATE PROJECT FILES FOR MASTER AND SLAVE?

  • angeljo

    Dear Ligo, pls Help me..
    i will try above codes on hardware but it was not working, simulation was execute perfectly…
    what i can do, for obtain o/p on hardware…..

  • Yes, separate project files are required.

  • Thanks for the feedback. 🙂

  • Check the pullup resistors of I2C lines.

  • Santiago Blandon

    Awesome job my friend!! You outlined, explained, and provided the code for a tough subject to understand on one’s own. Thank you!

  • Maito Guy

    I am not that good at programming so anybody can explain this line found at Master Read function

    ACKDT = (a)?0:1;

  • Thanks for the feedback.

  • It means.

    if(a == 1)
    ACKDT = 0;
    ese
    ACKDT = 1;

  • David Marks

    Thank you very clear for a newbie like myself to understand. I have come from a different mcu and C compiler, but I am finding the xc8 syntax cleaner, and the documentation is easier to understand as well.

  • David Marks

    Thank you now that was a tricky one but it makes sense now 🙂

  • Thanks for the feedback.

  • Phuoc

    ” if(!SSPSTATbits.D_nA && !SSPSTATbits.R_nW)
    {
    z = SSPBUF;
    while(!BF);
    PORTD = SSPBUF;
    SSPCONbits.CKP = 1;
    SSPM3 = 0; ( why you need SSPM3 = 0 here ??)
    }”

  • Christophe Nabil Nicolas El-Kh

    This is a wonderful explanation Thank you for that!
    However, I have a couple of questions:

    I’m trying to connect a Raspberry Pi (Master) to an 18F4550 (Slave) which in turn is connected to an LCD. So I will be sending data from my Pi to the Pic and have them displayed on the LCD. In such a case, would it be enough to simply connect the SDA and SCL pins on both devices? also, in the slave code, you’ve included setting TRIS4 and 5 as inputs. Why is that?
    Thank you.,

  • Alexander Erakhtin

    So it is possible to assign an address to a slave? I have been lead to believe that every device comes with its own address for I²C? Or does that not count for microcontrollers?

  • Vinoth Sms

    Sir pls give me master to slave mplab program for pic24hj256gp210

  • Paul G.

    Thank you so much ! these tutorials are very helpful and well explained. Might it be possible to write a tutorial on the SPI communication ?

  • Alexander Erakhtin

    Hello once again. I’m a little confused, the interrupt I2C_Slave_Read() is never called in the slave example? How come?

  • Alexander Erakhtin

    Why do you set SSPM3 to 0? Making sure only 7-bit addresses are used?

  • Sorry, that was a mistake. I removed it now.

  • You need to take care of voltage levels.
    Those pins are set as inputs as prescribed in the datasheet.
    Kindly use our forums ( https://electrosome.com/forums/ ) for asking doubts outside the scope of above article.

  • Thanks for it.
    Acutually it was unnecessary. I removed it now.

  • Shailav Bharadwaj

    I tried implementing your code for Master(with 4×4 Keypad) and Slave with LCD. I didn’t get any output on Proteus simulation. I carefully followed all your instructions but the LCD does not show any keypad presses. What might I be doing wrong?

  • I can’t help you unless you provide complete details including the code.
    Kindly use our forums (https://electrosome.com/forums/) for that.

  • Alexander Erakhtin

    Okay, thanks, one more thing: you can only read one byte with this code,right?