STM32 I2C tutorial with HAL code example

In this tutorial, we will explain the basic principles of Inter-Integrated Circuit (I2C) communication and a closer look at the I2C hardware of the STM32 microcontroller. To get you started, we will show you how to interface the MPU-6050 accelerometer and gyroscope sensor module with the STM32 Nucleo Board using I2C.

Our other STM32 related tutorials are:

What is I2C?

I2C is a serial data communication protocol used to communicate between Integrated Circuits(ICs). This protocol was created by Philips Semiconductors (now NXP) back in 1982. It is a synchronous, half-duplex, multi-master, multi-slave, packet-switched, single-ended, serial data communication protocol.

I2C signals and Modes of operation

I2C is a 2-wire protocol and they are called SDA and SCL. Both SDA and SCL are bidirectional lines connected to a positive voltage supply via pull-up resistors. Both the SDA and SCL lines are held high when the bus is free.

The output stages of devices must have an open-drain or open-collector configuration. The bus capacitance limits the number of interfaces connected to the bus.

I2C Bus
Figure: I2C Bus

There are four different I2C modes and they are Standard-mode, Fast-mode, Fast-mode Plus, and High-speed mode. All the modes are downward-compatible means any device may be operated at a lower bus speed but Ultra Fast-mode devices are not compatible with previous versions because the bus is unidirectional.

Standard Mode (SM): Data rate up to 100 Kbits/sec,

Fast Mode (FM): Data rate up to 400 Kbits/sec

Fast mode plus (FM+): Data rate up to 1 Mbits/sec

High-Speed Mode (HS-Mode): Data rate up to 3.4 Mbits/sec

Understanding I2C protocol

In I2C, data transfer is always initiated by the master on the SDA line. Master first produces a start condition and after the start condition, the address phase follows. The address phase contains 8 bits, First 7 bits are the address of the slave, and the remaining bit decides the read or write operation.

If the Read/Write bit is 0 that indicates the master will write the data to the slave and if the bit is 1 that indicates the master will read data from the slave.

After sending the addressed slave will compare the data with its own address. If the address matches slave will send an acknowledgment bit to the master and the master receive the acknowledgment bit.

Once the master receives the Acknowledge bit, the master will start the read or write operation to the slave according to the read/write bit sent by the master earlier in the address phase. If the slave receives data it will send the Acknowledgement again or if the master receives the data then the master will send the Acknowledgement.

If the master wants to send or receive more data, it can send or receive more data. If the master wants to end communication, it will generate a stop bit to close the communication.

I2C protocol example
Figure: I2C protocol example

Please note that Every byte put on the SDA line must be eight bits long and each byte must be followed by an Acknowledge bit. Data is transferred with the most significant bit (MSB) first.

I2C peripheral in STM32 Microcontroller

Hardware Overview of I2C in STM32

I 2C (inter-integrated circuit) bus Interface serves as an interface between the microcontroller and the serial I2C bus. It provides multi-master capability and controls all I2C bus-specific sequencing, protocol, arbitration, and timing. It supports the standard mode (Sm, up to 100 kHz) and Fm mode (Fm, up to 400 kHz).

It may be used for a variety of purposes, including CRC generation and verification, SMBus (system management bus), and PMBus (power management bus). 

Depending on specific device implementation DMA capability can be available for reduced CPU overload.

I2C main features in STM32

  • Parallel-bus/I2C protocol converter
  • Multimaster capability: the same interface can act as Master or Slave
  • I2C Master features: Clock generation, Start and Stop generation
  • I2C Slave features:  Programmable I2C Address detection, Dual Addressing Capability to acknowledge 2 slave addresses,  Stop bit detection
  • Generation and detection of 7-bit/10-bit addressing and General Call
  • Analog noise filter
  • Programmable digital noise filter

Communication flow

In Master mode, the I2C interface initiates a data transfer and generates the clock signal. A serial data transfer always begins with a start condition and ends with a stop condition. Both start and stop conditions are generated in master mode by the software.

In Slave mode, the interface is capable of recognizing its own addresses (7 or 10-bit), and the General Call address. The General Call address detection may be enabled or disabled by software.

Data and addresses are transferred as 8-bit bytes, MSB first. The first byte(s) following the start condition contains the address (one in 7-bit mode, two in 10-bit mode). The address is always transmitted in Master mode.

A 9th clock pulse follows the 8 clock cycles of a byte transfer, during which the receiver must send an acknowledge bit to the transmitter.

I2C communication flow
Figure: I2C Communication Flow

I2C Functional block diagram

STM32 I2C block diagram
Figure: I2C functional block diagram

You could find more information in detail in any STM32 microcontroller Reference manual. For example, If you want to know more about the STM32 I2C peripherals please click the link to download the Reference manual of the STM32F44xx microcontroller.

How to handle I2C transactions in STM32

Polling Mode

The first way is just to poll for the hardware resource until it’s ready to move on to the next step in the program instructions. However, it’s not an efficient way to handle I2C and the CPU will end up wasting so much time in a busy or waiting state.

This will happen for both transmission and reception. You have to wait until the current byte of data is transmitted so you can start the next transition and so on.

Interrupts Mode

You could use the I2C interrupts mode. In that mode, you don’t have to wait to finish the data transmission. You have a signal when I2C  is done with the transmission and ready for service by CPU. This is true  for both the data sent and received modes. This saves a lot of time and CPU overload. Interrupts mode is always the best way to handle events like that.

In some Critical applications like medical devices, we need everything to be as perfect and on time. The main problem with interrupts is that we can’t expect when data arrive or during which task. That can harmful to time-critical firmware.

DMA Mode

Direct memory access (DMA) is a method that allows an input or output (I/O) device to send or receive data directly to or from the main memory of the processor without the help of the CPU to speed up memory operations. To facilitate the data transfers, the I2C protocol features a DMA capability implementing a simple request/response protocol.

DMA requests are mainly generated by the Data Register of the processor. Register becoming empty in transmission and full in reception. Data transfer mainly happens between the peripheral to memory, so the CPU can perform the other tasks. This will end up saving a lot of time and is considered to be the most efficient way to handle this peripheral to memory data transfer and vice versa.

MPU6050 Accelerometer and Gyroscope Sensor

The MPU-6050 is the world’s first integrated 6-axis motion tracking device that combines a 3-axis gyroscope, 3-axis accelerometer, and a Digital Motion Processor™ (DMP) all in a small 4x4x0.9mm package. With its dedicated I2C sensor bus, it directly accepts inputs from an external 3-axis compass to provide a complete 9-axis MotionFusion™ output. The MPU-6050 Motion Tracking device, with its 6-axis integration, onboard MotionFusion™, and run-time calibration firmware, enables manufacturers to eliminate the costly and complex selection, qualification, and system-level integration of discrete devices, guaranteeing optimal motion performance for consumers.

MPU6050 Accelerometer and Gyroscope Sensor
Figure: MPU6050

To know more about MPU5060, you can download its datasheet from here.

Interfacing MPU6050 with STM32

Circuit Connection

As we are using STM32 Nucleo board, I connect MPU6050 VCC to STM32 Nucleo 3.3V, Ground to Ground, SCL to SCL(PB8) and SDA to SDA (PB9)

Interfacing MPU6050 with STM32 circuit diagram
Figure: Stm32 MPU6050 circuit connection

Component List

Component Name

Quantity

Purchase Link

STM32 Nucleo Dev. Board

1

MPU5060

1

Breadboard

1

Jumper Wire Pack

3

For troubleshooting, some extremely useful test equipment

Equipment Name

Purchase Link

Best Oscilloscope for Professionals

Best Oscilloscope for Beginners and Students

Logic Analyzer

Best Budget Multimeter

Adjustable Bench Power Supply

Affiliate Disclosure: When you click on links to make a purchase, this can result in this website earning a commission. Affiliate programs and affiliations include, but are not limited to Amazon.com

Preparing STM32CubeIDE

After the circuit connection, Our next step is to prepare the STM32Cube IDE. Open the Cube IDE and go to File > New > Stm32 project

Preparing STM32CubeIDE for I2C1

After that select the board from Board Selector tab. As I am using STM32 Nucleo board thats why I select it.

Preparing STM32CubeIDE for I2C1

Then write the Project Name and choose the Location then click Finish.

Preparing STM32CubeIDE for I2C1

Select the Device Configuration Tool and Enable I2C1. Then select the pin PB8 as SCL and PB9 as SDA.

Preparing STM32CubeIDE for I2C1

After that Save the Project and Generate the code.

Preparing STM32CubeIDE for I2C1
Preparing STM32CubeIDE for I2C1

STM32 I2C code for MPU6050

After Preparing the circuit and Stm32Cube IDE copy the code to your IDE. Then build and upload the code to the stm32 microcontroller.

Or, If you want to create a project in stm32CubeMX for Keil uvision IDE then we have already publish a article in this topic. Please click the link blow to visit the article

How to create a project in stm32CubeMX for Keil uvision Ide

For demonstration purposes, we are using polling mode in the program. You can use Interrupt or DMA mode. We program the sensor for Accelerometer value. You can also get the Gyroscope value by reading the register of the sensor.

For writing commands to the MPU6050 memory, we use a HAL api called HAL_I2C_Mem_Write.

HAL_I2C_Mem_Write (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t * pData, uint16_t Size, uint32_t Timeout);

For reading from the memory address, we use another HAL api called HAL_I2C_Mem_Read.

HAL_I2C_Mem_Read (I2C_HandleTypeDef * hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t * pData, uint16_t Size, uint32_t Timeout);

Here is the code:

/* Includes ------------------------------------------------------------------*/
#include "main.h"

/* Private variables ---------------------------------------------------------*/
I2C_HandleTypeDef hi2c1;
DMA_HandleTypeDef hdma_i2c1_tx;
DMA_HandleTypeDef hdma_i2c1_rx;

UART_HandleTypeDef huart2;

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_USART2_UART_Init(void);
static void MX_I2C1_Init(void);

#define MPU6050_ADDRESS 0x68 << 1 // last bit is for read/write bit

//MPU6050 Register addresses. You can download the register map from internet
#define smplrt_dv_register      0x19
#define gyro_comfig_register    0x1B
#define accel_config_register   0x1C
#define accel_x_out_h_register  0x3B
#define temp_out_h_reg          0x41
#define gyro_x_out_h_register   0x43
#define pwr_mgmt_1_register     0x6B
#define who_i_am                0x75

//this array store the raw data of X,Y and Z axis of accelerometer
uint8_t Receive_Data[6]; 

int16_t Accel_X_RAW_DATA = 0;
int16_t Accel_Y_RAW_DATA = 0;
int16_t Accel_Z_RAW_DATA = 0;

float Ax, Ay, Az;

void PMU6050_Init(void){
    uint8_t check_val, sendData;
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDRESS, who_i_am, 1, &check, 1, 1000);
    if(check_val == 104){
        sendData = 0x00;
	HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS, pwr_mgmt_1_register, 1, &sendData, 1, 1000);
	sendData = 0x07;
	HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS, smplrt_dv_register, 1, &sendData, 1, 1000);
	sendData = 0x00;
	HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS, gyro_comfig_register, 1, &sendData, 1, 1000);
	sendData = 0x00;
        HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDRESS, accel_config_register, 1, &sendData, 1, 1000);
    }
}

int main(void)
{
  HAL_Init();
  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_I2C1_Init();
  /* USER CODE BEGIN 2 */
  PMU6050_Init();

  while (1)
  {
      /* USER CODE END WHILE */
      HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDRESS, accel_x_out_h_register, 1, Receive_Data, 6, 1000);
      Accel_X_RAW_DATA = (int16_t)(Receive_Data[0] << 8 | Receive_Data [1]);
      Accel_Y_RAW_DATA = (int16_t)(Receive_Data[2] << 8 | Receive_Data [3]);
      Accel_Z_RAW_DATA = (int16_t)(Receive_Data[4] << 8 | Receive_Data [5]);

      Ax = Accel_X_RAW_DATA/16384.0;
      Ay = Accel_Y_RAW_DATA/16384.0;
      Az = Accel_Z_RAW_DATA/16384.0;

      HAL_Delay(1000);
  }
}

Please Note: I have not included some parts of the code which are auto-generated by STM32CubeIDE.

Mahamudul Hasan

I hold a B.Sc degree in Electrical & Electronic Engineering from Daffodil International University, Bangladesh. I am a Printed Circuit Board (PCB) designer and Microcontroller programmer with an avid interest in Embedded System Design and IoT. As an Engineer, I love taking challenges and love being part of the solution. My Linkedin Profile Link: https://linkedin.com/in/mheee

Recent Posts