STM32F0 I2C - Tutorial 7 with STM32CubeMX

Hi everyone, I’m back :) Another year has come with lots of opportunities and challenges presented to me as I now become a fresh PhD candidate at Nanyang Technological University (NTU) Singapore. Being occupied by all the courses and my research, I hardly had time to continue with the STM32F0 tutorial series I started almost two years ago. However, we have come a long way to finish almost all the basic aspects of the STM32F0 chip including the GPIO, Interrupts, Timer, Counter, PWM, UART and ADC. Only few peripherals including I2C, SPI, DAC, TSC and WDG are left to be discovered. Hence, I thought that I need to spent some time to continue with this tutorial series to finish all the basic peripherals.

Therefore, in this tutorial, I will be covering the following parts:

  1. Some basic ideas of the I2C and when we need to use it.
  2. Overview of the I²C peripheral of the STM32F051 on the STM32F0 Discovery kit.
  3. An example showing how to connect and read data from a temperature and humidity sensor (HDC1080) through I²C port and then, display the temperature and relative humidity on an OLED screen (SSD1306 controller), also through the same I²C port.
  4. Finally, another example to show how to set up two I²C modules of the same STM32F0 (1 master, 1 slave) to transfer data with each other. Let’s get started.

1. Basic I²C – What should we notice?

Fig. 1: I2C basic hardware connection. Soure: http://www.cypress.com

I found this article from Sparkfun describe in a very intuitive way and very easy to understand about the fundamentals of I2C. If you are not familiar with I2C, I suggest that you take a look at the tutorial on Sparkfun website and come back here with these information in mind:

  • The basic hardware connection for I2C communication (SCL, SDA)
  • Start condition: the master device leaves SCL high and pulls SDA low, which informs all slave devices that a transmission is about to start. Start condition can be issued multiple times (repeat start) in case a master wants to retrieve more data from slaves.
  • Address frame: Each device class has a fixed 7-bit ‘device address’ which is used to identify itself from other devices in the same I²C network followed by a R/W bit indicating whether this is a read (1) or write (0) operation. The 9th bit of the frame is the NACK/ACK bit which is the case for all frames (data or address). Only the slave which has the same address sends the ACK back to the master. Since this is just a basic tutorial on I²C, we only consider 7-bit address case from now on in this tutorial.
  • Data frame(s): after the address frame has been transmitted by the master and acknowledged ‘ACK’ by the corresponding slave, it is followed by data frames. Depending on the value of the R/W bit in address frame, data frame direction will be from slave to master and vice versa.
  • Stop condition: Stop conditions are defined by a low to high transition on SDA after a low to high transition on SCL, with SCL remaining high.

Once you get familiar with these ideas, we can continue with our STM32 applications.

2. STM32F0 I2C functions

  • For STM32F0 Discovery kit equiped with STM32F051R8, we have 2 I2C modules: I²C1 and I²C2 that can run simultaneously. Some of the differences between these two modules are extracted from the datasheet of the F051 chip and presented in the table below.

I²C modules comparison

I²C1 possible pin mapping

I²C2 possible pin mapping

  • As indicated in Table 9 above, STM32F051 supports three speed mode (frequency mode) for the I2C communication: 100kHz, 400kHz and 1MHz. Based on the specifications of the targeted I2C sensor, the frequency is chosen accordingly.
  • I²C mode: Master mode – STM32F0 acts as a master to communicate and acquire data from other slaves (sensors); or Slave mode – provide data to other microcontrollers.
  • Analog and Digital noise filters: to suppress spikes on the SDA and SCL lines. The benefits and drawbacks of each filter are presented in Table 87 below.

Analog and Digital noise filter feature
Analog and Digital noise filter: their benefits and drawbacks

3. STM32F0 I2C Master Mode Example

In HAL library, there are several functions that provide us an easy solution to program I²C communication, particularly for Master mode. In this tutorial, I will only mention the normal communication without using interrupts and DMA. Hence, for master transmitting and receiving, we can classify the functions into 2 groups as following:

Group 1: functions used to communicate with those devices that do not have secondary address (such as the sensor we’ll use in the later part):

HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout); 

HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);  

Notice that we have 5 parameters to input into the function:

  • I2C_HandleTypeDef *hi2c: pointer to the I²C module you use to communicate. For example, if you define to use 2 I²C module in CubeMX, there will be 2 available pointers: hi2c1 and hi2c2. In the case you use I²C1, just put '&hi2c1' as the pointer for this parameter.

  • uint16_t DevAddress: address of the I²C slave you want to connect with. Normally, this address will be given in the datasheet of the slave device in the form of 7-bit number, starting from bit 0 to bit 6. However, the real transmitting frame should contain 8-bit of data as can be seen in the following image, where the 7-bit address is actually in place of bit 1 to bit 8 of the frame, reserving bit 0 for Read/Write operation bit. Hence, the device address should be shifted 1-bit to the left (address<<1) before using for this parameter. The example with the HDC1080 sensor on later part will demonstrate more on this point.

  • uint8_t *pData: pointer to the data you want to transmit to the slave or the buffer to receive the data from the slave. It can be an array or a pointer. I personally prefer to use array.

  • uint16_t Size: the number of byte you want to transmit or receive through I²C communication.

  • uint32_t Timeout: timeout configuration for the function to stop and return to the main program if there is anything wrong with the I²C connection.

Group 2: functions used to communicate with those devices that have secondary address (such as memory chip AT24Cxxx):

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);

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);  

Since I don’t have any I²C memory chip with me, we’ll first explore group 1 functions with two following examples.

a. HDC1080 Temperature and Humidity sensor

Normally, a simple I²C memory chip like AT24C02 is used to demonstrate the I2C protocol. However, I don’t have any of these memory chip but another I2C temperature and humidity sensor, which has a fairly simple reading protocol, I decided to first try with it. The sensor name is HDC1080 from Texas Instrument. I found it to be fairly good in humidity and temperature accuracy (±2% and ±0.2°C accordingly).

HDC1080 temperature and humidity sensor

You can take a look at the tutorial video below where I did pretty much all the explanation for this sensor and how to connect and read data from it through I²C connection.

And this is the code I used in the video:

  • Variables declaration:
/* Private variables ---------------------------------------------------------*/ 
unsigned char buffer[5];  
unsigned int rawT, rawH;  
float Temperature; float Humidity;  
/* USER CODE END PV */
  • Configuration:
sprintf(buf,"BRIGHTNESS LEVEL:%i",i++);  
/* USER CODE BEGIN 2 */ 
//Config the HDC1080 to perform acquisition separately 
HAL_Delay(15);  
buffer[0]=0x02; //Pointer buffer  
buffer[1]=0; //MSB byte  
buffer[2]=0; //LSB byte  
HAL_I2C_Master_Transmit(&hi2c1,0x40<<1,buffer,3,100);  
//device address 0x40 in the datasheet is shifted 1-bit to the left. 
//3 bytes are transmitting to the slave: buffer[0], buffer[1] and buffer[2]
/* USER CODE END 2 */
  • Triggering measurement and data reading:
/* USER CODE BEGIN 3 */ 
//Trigger Temperature measurement 
buffer[0]=0x00;  
HAL_I2C_Master_Transmit(&hi2c1,0x40<<1,buffer,1,100);  
HAL_Delay(20);  
HAL_I2C_Master_Receive(&hi2c1,0x40<<1,buffer,2,100);  
//receive 2 bytes, store into buffer[0] and buffer[1]
//buffer[0] : MSB data 
//buffer[1] : LSB data 
rawT = buffer[0]<<8 | buffer[1]; //combine 2 8-bit into 1 16bit  
Temperature = ((float)rawT/65536)*165.0 -40.0;  
//Trigger Humidity measurement buffer[0]=0x01; 
HAL_I2C_Master_Transmit(&hi2c1,0x40<<1,buffer,1,100);  
HAL_Delay(20);  
HAL_I2C_Master_Receive(&hi2c1,0x40<<1,buffer,2,100);  
//buffer[0] : MSB data 
//buffer[1] : LSB data 
rawH = buffer[0]<<8 | buffer[1]; //combine 2 8-bit into 1 16bit  
Humidity = ((float)rawH/65536)*100.0; HAL_Delay(100); }  
/* USER CODE END 3 */

b. SSD1306 OLED screen

After getting the HDC1080 sensor to work, we can now move on to display the temperature and humidity on an SSD1306 OLED screen which also uses I²C communication. In this part, I'll demonstrate how to connect to, set up and display stuff on an 0.91" 128x32 SSD1306 OLED display with our STM32F051.

The datasheet of the display canbe found here. In case you're interested in knowing the details of each command and setting that you can do with the display, it's on page 28 of the datasheet. Otherwise, if you only want to display something easy on the screen, I do provide the HAL-compatible library at later part. First, let's take a look at this video where I have shown how to implement the display library files into a cubeMX project and display both temperature and humidity on it.

You can download the entire project from here and try it yourself.

Notice that we have added four additional file into the original CubeMX generated project folder, including: fonts.h, fonts.c, ssd1306.c and ssd1306.h. Header file '.h' usually contains the constant definition, function declarations and other definition stuff. It acts as a bridge to tell the compiler all the functions and definition we have and we can use from the library file. Library file '.c' usually contains functions that can be called from the main program main.c or other '.c' files. Here, 'fonts.c' and 'fonts.h' contain the all the font definition, available font sizes that we can use while 'ssd1306.c' and 'ssd1306.h' provide all functions to control and display things on the OLED.

Note: in order to keep those extra files remain in the project folder without loosing them after regenerate the project with CubeMX, you need to restart Keil after adding the files into the project similarly to what I have done at 2:35 in the clip.

Take a look at 'ssd1306.h' header file, we have the following configurations and functions available:

#include "stm32f0xx_hal.h"
#include "fonts.h"

//Originally from Olivier Van den Eede 2016
//Modified by Le Tan Phuc to be suitable for SSD1306 0.91" OLED display
//Work with STM32Cube MX HAL library
//Jul 2017

#ifndef ssd1306
#define ssd1306

// i2c1 is chosen. In case using I2C2, change to hi2c2
#define SSD1306_I2C_PORT            hi2c1
// SSD1306 I2C address 
#define SSD1306_I2C_ADDR        0x78
// SSD1306 width in pixels
#define SSD1306_WIDTH           128
// SSD1306 LCD height in pixels
#define SSD1306_HEIGHT          32



typedef enum {  
    Black = 0x00, /*!< Black color, no pixel */
    White = 0x01  /*!< Pixel is set. Color depends on LCD */
} SSD1306_COLOR;


typedef struct {  
    uint16_t CurrentX;
    uint16_t CurrentY;
    uint8_t Inverted;
    uint8_t Initialized;
} SSD1306_t;


extern I2C_HandleTypeDef SSD1306_I2C_PORT;


uint8_t ssd1306_Init(void);  
void ssd1306_Fill(SSD1306_COLOR color);  
void ssd1306_UpdateScreen(void);  
void ssd1306_DrawPixel(uint8_t x, uint8_t y, SSD1306_COLOR color);  
char ssd1306_WriteChar(char ch, FontDef Font, SSD1306_COLOR color);  
char ssd1306_WriteString(char* str, FontDef Font, SSD1306_COLOR color);  
void ssd1306_SetCursor(uint8_t x, uint8_t y);

void ssd1306_WriteCommand(uint8_t command);

#endif
  • I used I²C1 in this example, hence, 'hi2c1' is used for the SSD1306_I2C_PORT definition.
  • Several functions we have to control and display stuff on the display:
    • ssd1306_Init(void): Initialize the display with predefined settings.
    • ssd1306_Fill(SSD1306_COLOR color): fill the entire display with either Black (0x00) or White (0x01).
    • ssd1306_UpdateScreen(void): this function must be called to display whatever is stored in the display's memory.
    • ssd1306_DrawPixel(uint8_t x, uint8_t y, SSD1306_COLOR color): draw/clear a pixel on the display at location x (0-127) and y (0-31). Color Black will clear the pixel and White will set a pixel. This function only push data into display's memory. To really show it on the display, UpdateScreen function must be called.
    • ssd1306_WriteString(char* str, FontDef Font, SSD1306_COLOR color): display a string of words on the display where Font is one of the font defined in 'font.h'.
    • ssd1306_SetCursor(uint8_t x, uint8_t y): set the location to display the string in the WriteString function.

4. STM32F0 I2C Slave Mode Example

to be continued