Ads

Get STM32 tutorial using HAL at $10 for a limited time!

Thursday, March 17, 2016

ARM Cortex-M3 (STM32F103) Tutorial - I2C Master

In this tutorial, I will explain about I2C protocol and how to use it on STM32F103 microcontroller. I2C (Inter-Integrated Circuit) or sometimes called TWI (Two Wire Interface) is a synchronous serial protocol that only need 2 wire for communication. These wire are SCL for clock line and SDA for data line. Using I2C bus, you can connect devices like temperature sensor, EEPROM, RTC, etc (up to 112 device) just using two wire (plus GND wire). For accessing these device, I2C use 7-bit address. Theoretically 7-bit address space allows 128 addresses, however some addresses are reserved for special purpose. Thus, only 112 address can be used. There is also special method to use I2C with 10-bit address.


I2C is multi-master and multi-slave bus. I2C bus can consists of one or more master device, but only one master device can have an access to I2C bus each time. To communicate with a slave device, master is responsible for sending clock to the slave device. Clock signal is always generated by master. I2C drivers are open-drain, means that they can pull the signal low, but can't drive it high. On each signal line (SCL and SDA) must have a pull-up resistor to restore the signal high when no device is asserting it low.


The image above is explain how the I2C bus works. START and STOP condition are defined as rising or falling edges on the SDA line while the SCL line is kept high. After master send a START condition, then it will followed by sending slave address and R/W bit. Then slave device is responsible to generate ACK. ACK is defined as low logic on SDA line.

After that, if master wants to write data to slave (indicate by R/W bit = 0), then master send the data byte to slave. The MSB of data byte will be sent first to slave. Every data byte received, slave is responsible to generate ACK.

If master wants to read data from slave (indicate by R/W bit = 0), then after the first ACK of slave address and R/W bit, master will read data byte from slave. The MSB of data byte will be read first from slave. If the data size is only one byte or that byte is the last data byte, then master will send NACK to slave, otherwise master will send ACK. NACK is defined as high logic on SDA line.

From the image below, you can see the detail of timing diagram for I2C bus.


In this tutorial, I will share how to use I2C on STM32F103 microcontroller as a master and for the slave I will use Arduino. The slave is very simple, when master write data byte 0x01 to it, then the LED on Arduino board will blinking every 250ms. To turn off LED blinking, master must write data byte 0x00. Master also can read the LED blinking status (on/off) from the slave which returns 1 or 0.

This is the code for Arduino as an I2C slave:
#include <Wire.h>

#define OWN_ADDRESS  0x08
#define LED_PIN      13

int ledBlink = 0;

void receiveEvent(int bytes) 
{
    // Read received data
    ledBlink = Wire.read();
}

void requestEvent()
{
    // Send LED blinking status
    Wire.write(ledBlink);
}

void setup() 
{
    pinMode (LED_PIN, OUTPUT);
  
    // Start the I2C bus as slave
    Wire.begin(OWN_ADDRESS); 
    // Attach a function to trigger when something is received
    Wire.onReceive(receiveEvent);
    // Attach a function to trigger when something is requested
    Wire.onRequest(requestEvent);
}

void loop() 
{
    // If received data is 1, then blink LED for 250 ms
    if (ledBlink == 1) 
    {
        digitalWrite(LED_PIN, HIGH);
        delay(250);
        digitalWrite(LED_PIN, LOW);
        delay(250);
    }
    // If received data is 0, then turn off LED
    else if (ledBlink == 0) 
    {
        digitalWrite(LED_PIN, LOW);
    }
}
For the I2C master on STM32F103, I have wrote the code, that consist of several functions:
void i2c_init()
{
    // Initialization struct
    I2C_InitTypeDef I2C_InitStruct;
    GPIO_InitTypeDef GPIO_InitStruct;

    // Step 1: Initialize I2C
    RCC_APB1PeriphClockCmd(I2Cx_RCC, ENABLE);
    I2C_InitStruct.I2C_ClockSpeed = 100000;
    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_Disable;
    I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_Init(I2Cx, &I2C_InitStruct);
    I2C_Cmd(I2Cx, ENABLE);
 
    // Step 2: Initialize GPIO as open drain alternate function
    RCC_APB2PeriphClockCmd(I2C_GPIO_RCC, ENABLE);
    GPIO_InitStruct.GPIO_Pin = I2C_PIN_SCL | I2C_PIN_SDA;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(I2C_GPIO, &GPIO_InitStruct);
}

void i2c_start()
{
    // Wait until I2Cx is not busy anymore
    while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY));

    // Generate start condition
    I2C_GenerateSTART(I2Cx, ENABLE);

    // Wait for I2C EV5. 
    // It means that the start condition has been correctly released 
    // on the I2C bus (the bus is free, no other devices is communicating))
    while (!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT));
}

void i2c_stop()
{
    // Generate I2C stop condition
    I2C_GenerateSTOP(I2Cx, ENABLE);
    // Wait until I2C stop condition is finished
    while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_STOPF));
}

void i2c_address_direction(uint8_t address, uint8_t direction)
{
    // Send slave address
    I2C_Send7bitAddress(I2Cx, address, direction);

    // Wait for I2C EV6
    // It means that a slave acknowledges his address
    if (direction == I2C_Direction_Transmitter)
    {
        while (!I2C_CheckEvent(I2Cx,
            I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
    }
    else if (direction == I2C_Direction_Receiver)
    { 
        while (!I2C_CheckEvent(I2Cx,
            I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
    }
}

void i2c_transmit(uint8_t byte)
{
    // Send data byte
    I2C_SendData(I2Cx, byte);
    // Wait for I2C EV8_2.
    // It means that the data has been physically shifted out and 
    // output on the bus)
    while (!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}

uint8_t i2c_receive_ack()
{
    // Enable ACK of received data
    I2C_AcknowledgeConfig(I2Cx, ENABLE);
    // Wait for I2C EV7
    // It means that the data has been received in I2C data register
    while (!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED));

    // Read and return data byte from I2C data register
    return I2C_ReceiveData(I2Cx);
}

uint8_t i2c_receive_nack()
{
    // Disable ACK of received data
    I2C_AcknowledgeConfig(I2Cx, DISABLE);
    // Wait for I2C EV7
    // It means that the data has been received in I2C data register
    while (!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED));

    // Read and return data byte from I2C data register
    return I2C_ReceiveData(I2Cx);
}

void i2c_write(uint8_t address, uint8_t data)
{
    i2c_start();
    i2c_address_direction(address << 1, I2C_Direction_Transmitter);
    i2c_transmit(data);
    i2c_stop();
}

void i2c_read(uint8_t address, uint8_t* data)
{
    i2c_start();
    i2c_address_direction(address << 1, I2C_Direction_Receiver);
    *data = i2c_receive_nack();
    i2c_stop();
}
You can use i2c_init() functions to initialize the I2C peripheral. In the main loop, I will write a command to turn on and turn off LED blinking on Arduino board every 2500ms. Every write command is followed by a command for reading the current LED blinking status, then that status is displayed on LCD 16x2.
int main(void)
{
    DelayInit();
    lcd16x2_init(LCD16X2_DISPLAY_ON_CURSOR_OFF_BLINK_OFF);
 
    // Initialize I2C
    i2c_init();
 
    while (1)
    {
        // Write 0x01 to slave (turn on LED blinking)
        i2c_write(SLAVE_ADDRESS, 0x01);
        DelayMs(5);
        // Read LED blinking status (off/on)
        i2c_read(SLAVE_ADDRESS, &receivedByte);
        // Display LED blinking status
        lcd16x2_clrscr();
        if (receivedByte == 0)
        {
            lcd16x2_puts("LED Blinking Off");
        }
        else if (receivedByte == 1)
        {
            lcd16x2_puts("LED Blinking On");
        }
        DelayMs(2500);
  
        // Write 0x00 to slave (turn off LED blinking)
        i2c_write(SLAVE_ADDRESS, 0x00);
        DelayMs(5);
        // Read LED blinking status (off/on)
        i2c_read(SLAVE_ADDRESS, &receivedByte);
        // Display LED blinking status
        lcd16x2_clrscr();
        if (receivedByte == 0)
        {
            lcd16x2_puts("LED Blinking Off");
        }
        else if (receivedByte == 1)
        {
            lcd16x2_puts("LED Blinking On");
        }
        DelayMs(2500);
    }
}
If you want to see the full code, you can go to my repository.

6 comments :

  1. Hei, I have a problem about I2C at stm32f103. I have copied your github repositories about access hmc5883l compass. But the data of hmc5883l not show up at my serial terminal. what should i do??

    thanks..

    ReplyDelete
  2. Okay now i already knew why if i activated the i2c, usart won't send any data to my terminal. i2c interuption running continuously without give any chance usart to do interuption data. I think that is the problem :D :D

    ReplyDelete
  3. What is the value of pull-up resistor?

    ReplyDelete
  4. klo inisialiasi stm32 via register langsung bagaimana gan bwt chip stm32F103 mohon pencerahannya gan

    ReplyDelete
  5. anyone can give some example of initialize i2c through registers all suggestions are greatly appreciated

    ReplyDelete
  6. Hello sir , thanks for your tutorials
    I ask you if you can send the hardware connections for I2C with lcd as image

    ReplyDelete