Getting Started with I2C: Code Example

In I2C communication, one device acts as the master and initiates the communication, while the other devices act as slaves and respond to the master's commands. The master generates the clock signal and controls the data transfer, while the slaves respond to the commands sent by the master.

Each device on the bus is identified by a unique 7-bit or 10-bit address, which is used by the master to address the slave device for data transfer. It also supports different data transfer formats such as byte mode, page mode, and sequential mode. The I2C protocol also includes error detection and correction mechanisms such as ACK/NACK signals and CRC checks to ensure reliable data transfer. In this article, master-slave code implementation is covered in detail.

Image shared below is explaining the flow chart of the code.

Flowchart I2C

Each module detail for I2C Master is as below:

Main Subroutine Code

  • I2C Initialization:
    • Enable the I2C Peripheral Clock
    • Enable to GPIO Peripheral Clock
    • Configures the GPIO peripheral with the following settings:
      • GPIO Mode
      • GPIO Speed
      • GPIO Output Type
    • Configures the I2C1 peripheral with the following settings:
      • I2C clock speed
      • I2C mode
      • I2C duty cycle
      • I2C own address
      • I2C acknowledgement
      • I2C acknowledged address: 7-bit

I2C Intialisation Code

  • Main Subroutine:
    • Initiate the transmission by:
      • Sending the start condition on SDA line from high to low while keeping SCL line high.
      • Sending the slave address.
      • 7-bit or 10-bit slave address along with the additional bit indicating whether master wants to read from or write to the slave.
      • Data Transfer if wants to Write
      • Else Read the Data
      • Send the Stop Condition by pulling the SDA line from Low to High while keeping the SCL line High.

Start Transmission Code

  • Start Transmission:
    • It waits until the I2C1 is not busy, sends a start condition, waits for the start condition to be sent successfully, sends the slave address with the write bit, waits for the slave address to be sent successfully, sends the data byte by byte, and waits for each data byte to be sent successfully.
    • If the stop bit is set, it sends a stop condition.

Read Data Code

  • Reads data:
    • It waits until the I2C1 is not busy, sends a start condition, waits for the start condition to be sent successfully, sends the slave address with the read bit, waits for the slave address to be sent successfully, receives the data byte by byte, and waits for each data byte to be received successfully.
    • If the stop bit is set, it sends a stop condition.

Each modules details for I2C Slave is as below:

Main Subroutine Code

I2C Initialization

    • Same as I2C Master Initialization

Subroutine Code

  • Main Subroutine:
    • An infinite loop waiting for a start condition from the master by checking:
      • 'I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED' event
      • After getting the start condition, reads 2 bytes of data from master by waiting for 'I2C_EVENT_SLAVE_BYTE_RECEIVED' event
      • Call 'I2C_ReceiveData()' to retrieve the received data.
      • Receives the data
      • Finally, the loop waits for a stop condition from the master by checking the I2C_EVENT_SLAVE_STOP_DETECTED event
      • Clears the stop flag using I2C_ClearFlag()`.

Note: The code examples are just a basic example to get started with I2C communication

The following code examples are written in C language and assume that the I2C peripheral is connected to an external I2C device with a 7-bit slave address of 0x50.

Sample Code for I2C Master:

/*Include all the header files as per the selected controller*/
/*In current example STM32F4 Microcontroller is used as a master device*/
#include "stm32f4xx.h"
#include "stm32f4xx_i2c.h"
#include "stm32f4xx_rcc.h"

#define I2C_SPEED              100000 /*100 KHz*/
#define I2C_SLAVE_ADDRESS      0xA0   /*Slave address would be mentioned in the Datasheet of the device working as a Slave*/

I2C_InitTypeDef I2C_InitStruct;

void I2C1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    /*Enable the I2C peripheral clock*/
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

    /*Enable GPIO peripheral clock*/
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);

    // Configure the GPIO pins for I2C1
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_OType = GPIO_OType_OD;
    GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    /*Connect I2C1 pins to the alternate function (AF4)*/
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_I2C1);
    GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_I2C1);

    /*Initialize I2C1 peripheral*/
    I2C_DeInit(I2C1);
    I2C_InitStruct.I2C_ClockSpeed = I2C_SPEED;
    I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStruct.I2C_OwnAddress1 = 0x00;
    I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_Init(I2C1, &I2C_InitStruct);

    /*Enable I2C1 peripheral*/
    I2C_Cmd(I2C1, ENABLE);
}

void I2C1_StartTransmission(uint8_t slaveAddr, uint8_t *buffer, uint8_t length, uint8_t stopBit)
{
    // Wait until I2C1 is not busy
    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

    // Send start condition
    I2C_GenerateSTART(I2C1, ENABLE);

    // Wait for start condition sent successfully
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    // Send slave address
    I2C_Send7bitAddress(I2C1, slaveAddr, I2C_Direction_Transmitter);

    // Wait for slave address sent successfully
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    // Send data
    for (uint8_t i = 0; i < length; i++) 
    {
        I2C_SendData(I2C1, buffer[i]);

        // Wait for data sent successfully
        while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
    }

    if (stopBit) 
    {
        /*Send stop condition*/
        I2C_GenerateSTOP(I2C1, ENABLE);
    }
}

void I2C1_ReadData(uint8_t slaveAddr, uint8_t *buffer, uint8_t length)
{
   /*Wait until I2C1 is not busy */
   while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

   /* Send start condition */
   I2C_GenerateSTART(I2C1, ENABLE);

   /* Wait for start condition sent successfully */
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

  /* Send slave address*/
  I2C_Send7bitAddress(I2C1, slaveAddr, I2C_Direction_Receiver);

  /* Wait for slave address sent successfully */
  while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

  /* Read data */
  for (uint8_t i = 0; i < length; i++) 
  {
    if (i == (length - 1)) 
    {
        /* Disable Acknowledgement */
        I2C_AcknowledgeConfig(I2C1, DISABLE);
    }

    /* Wait for data received successfully */
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));

    buffer[i] = I2C_ReceiveData(I2C1);
  }

/* Enable Acknowledgement */
I2C_AcknowledgeConfig(I2C1, ENABLE);

/* Send stop condition */
I2C_GenerateSTOP(I2C1, ENABLE);
}

int main (void) 
{
  
    uint8_t data[2];
  
    /*Initialize I2C1*/
    I2C1_Init ();
  
    /*Read data from the sensor*/
    I2C1_StartTransmission (I2C_SLAVE_ADDRESS, data, 2, 1);
 
    /*Process the received data*/
    
    /*Write data to the sensor*/
    data[0] = 0x01;
    data[1] = 0x02;
    I2C1_StartTransmission (I2C_SLAVE_ADDRESS, data, 2, 1);
while (1); return 0; }

Sample Code for I2C Slave:

#include "stm32f10x.h"

/*Slave Address, would be different for each slave device*/
#define I2C_SLAVE_ADDRESS 0x50

void I2C1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;
    I2C_InitTypeDef I2C_InitStruct;

    /*Enable the I2C & GPIO peripheral clock*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

    /*GPIO Configuration*/
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    /*I2C Configuration*/
    I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStruct.I2C_OwnAddress1 = I2C_SLAVE_ADDRESS << 1;
    I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_InitStruct.I2C_ClockSpeed = 100000;
    I2C_Init(I2C1, &I2C_InitStruct);

    /*Enable I2C*/
    I2C_Cmd(I2C1, ENABLE);
}

int main(void)
{
    uint8_t data[2];

    /*I2C Initialization*/
    I2C1_Init();

    while (1) 
    {
        /*Wait for the start condition*/
        while (!I2C_CheckEvent(I2C1, I2C_EVENT_SLAVE_RECEIVER_ADDRESS_MATCHED));

        /*Check the data*/
        for (uint8_t i = 0; i < 2; i++) 
        {
            while (!I2C_CheckEvent(I2C1, I2C_EVENT_SLAVE_BYTE_RECEIVED));
            data[i] = I2C_ReceiveData(I2C1);
        }

        // Process data

        /*Wait for stop condition*/
        while (!I2C_CheckEvent(I2C1, I2C_EVENT_SLAVE_STOP_DETECTED));
        I2C_ClearFlag(I2C1, I2C_FLAG_STOPF);
    }

    return 0;
}

In conclusion, the I2C communication protocol is a widely used standard for communication between multiple devices in embedded systems. In master-slave mode, one device acts as the master and initiates the communication, while the other devices act as slaves and respond to the master's requests. With the help of the code example provided, user can get started with I2C communication and build more complex applications that require multiple devices to communicate with each other.