ES1 PRJ2, PRJV & PRJD: STM32F05x microcontroller

Lesson 6: I2C

By Hugo Arends

© 2018 HAN University of Applied Sciences, version 1.0

Serial communication interfaces

Communication with external devices like sensors, actuators or other (embedded) computers is often done through dedicated communication peripherals. One of them we have seen in earlier lessons: the U(S)ART. This lesson introduces a new multi-master and multi-slave serial interface and it's communication protocol: Inter-Integrated Circuit (I2C).

I2C interface

The I2C interface is invented by Philips in the 80’s. It is used to connect several low speed peripheral devices to a microcontroller/computer with a two wire interface. With the I2C interface each device can be master and/or slave because both lines are bidirectional. In this lesson we will use the STM32F05x as a master and the LCD module BTHQ21605V as a slave.

The two wire interface consists of two signals called Serial Data (SDA) and Serial Clock (SCL). These lines use open-drain I/O's, so external pullup resistors must be added to make sure that a logic 1 equals Vdd. In our setup the internal pullup resistors of the STM32F051R8 are sufficient. So when enabled, there is no need to add external resistors. The hardware setup will be:

Let’s have a look at the I2C hardware implementation in the STM32F05x by taking a look at the block diagram:

Copyright STMicroelectronics (2012). Retrieved from RM0091.

A ‘Shift register’ is used to transmit and receive data via the SDA line.

In master mode a ‘Master clock generation’-block generates the SCL signal.

In order to use an I2C interface, the GPIO pins must be initialized to an Alternate Function.

When a line is used as an I2C input an analog and/or digital noise filter can be used to minimize the influence of external signals.

In this lesson the I2C of the STM32F05x will be connected to a BTHQ21605V. This is a monochrome LCD display with 16x2 characters and a I2C interface. The LCD can be purchased here:

and here:

These sites also have a copy of the datasheet of the BTHQ21605V. In the datasheet we can see that besides the I2C interface it uses a Power On Reset (POR) signal. This is an active high signal, so when making this line logic one, the LCD will be reset.

In the datasheet we also find which LCD controller is used. This is the PCF2119x by NXP. The datasheet of this controller tells us which instructions we must send to initialize the display, turn it on, display a character, read the displayed characters and read the status. The datasheet can be found here:

I2C Protocol

The I2C protocol states that a master initiates a data transfer with a so-called ‘START condition’ and then immediately transmits the slave address. This means multiple I2C slaves can be present on the same bus and only the addressed slave will respond.

The address is coded in 7-bits (or 10-bits) and an additional bit (LSB) is added to let the slave know what the master wants:

So the following will happen when a master initiates an I2C transfer:

The timing diagram presented in the figure above is made with a Logic Analyzer. This is an instrument for measuring and analyzing digital data. A Logic Analyzer is a must have for every embedded systems engineer. It makes visible what data is available on the bus and helps you analyzing the data by decoding the protocols. This saves significant development time. A very easy to use and affordable Logic Analyzer can be purchased here:

To demonstrate how the BTHQ21605V LCD module can be used, we will have a look at three communication scenarios and how these are implemented with the standard peripheral driver library. These scenarios are:

Initialize STM32F05x I2C hardware

Before the I2C peripheral of the STM32F05x can be used it needs to be initialized. The STM32F051R8 has two I2C peripherals: I2C1 and I2C2. The function BTHQ21605V_Setup() initializes I2C1 as follows:

void BTHQ21605V_Setup(void)


  GPIO_InitTypeDef GPIO_InitStructure;

  I2C_InitTypeDef  I2C_InitStructure;


  // Set I2C1 clock to SYSCLK (see system_stm32f0.c)


  //(#) Enable peripheral clock using

  // RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2Cx, ENABLE)

  //    function for I2C1 or I2C2.

  RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);


  //(#) Enable SDA, SCL  and SMBA (when used) GPIO clocks using

  //    RCC_AHBPeriphClockCmd() function.



  // Enable PB8


  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;

  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;

  GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;

  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL;

  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

  GPIO_Init(GPIOB, &GPIO_InitStructure);


  //(#) Peripherals alternate function:

  //    (++) Connect the pin to the desired peripherals' Alternate

  //         Function (AF) using GPIO_PinAFConfig() function.

  GPIO_PinAFConfig(GPIOB, GPIO_PinSource6, GPIO_AF_1);

  GPIO_PinAFConfig(GPIOB, GPIO_PinSource7, GPIO_AF_1);

  //    (++) Configure the desired pin in alternate function by:

  //         GPIO_InitStruct->GPIO_Mode = GPIO_Mode_AF


  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;

  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;

  //    (++) Select the type, OpenDrain and speed via  

  //         GPIO_PuPd, GPIO_OType and GPIO_Speed members

  GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;

  GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;

  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;


  //    (++) Call GPIO_Init() function.

  GPIO_Init(GPIOB, &GPIO_InitStructure);


  //(#) Program the Mode, Timing , Own address, Ack and Acknowledged

  //    Address using the I2C_Init() function.


  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;

  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

  I2C_InitStructure.I2C_AnalogFilter = I2C_AnalogFilter_Enable;

  I2C_InitStructure.I2C_DigitalFilter = 0;

  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;

  I2C_InitStructure.I2C_OwnAddress1 = 0;

//I2C_InitStructure.I2C_Timing = 0x00310309; // ~400 kHz. @ 8 MHz (HSI) see Ref. Man. Table 72

//I2C_InitStructure.I2C_Timing = 0x50330309; // ~400 kHz. @ 48 MHz (SYSCLK) see Ref. Man.Tbl 74

  I2C_InitStructure.I2C_Timing = 0x2033030A; // =400 kHz. @ 48 MHz (SYSCLK) measured with LA

  I2C_Init(I2C1, &I2C_InitStructure);


  //(#) Optionally you can enable/configure the following parameters without

  //    re-initialization (i.e there is no need to call again I2C_Init() function):

  //    (++) Enable the acknowledge feature using I2C_AcknowledgeConfig() function.

  //    (++) Enable the dual addressing mode using I2C_DualAddressCmd() function.

  //    (++) Enable the general call using the I2C_GeneralCallCmd() function.

  //    (++) Enable the clock stretching using I2C_StretchClockCmd() function.

  //    (++) Enable the PEC Calculation using I2C_CalculatePEC() function.

  //    (++) For SMBus Mode:

  //         (+++) Enable the SMBusAlert pin using I2C_SMBusAlertCmd() function.

  //(#) Enable the NVIC and the corresponding interrupt using the function

  //    I2C_ITConfig() if you need to use interrupt mode.


  //(#) When using the DMA mode

  //   (++) Configure the DMA using DMA_Init() function.

  //   (++) Active the needed channel Request using I2C_DMACmd() function.


  //(#) Enable the I2C using the I2C_Cmd() function.

  I2C_Cmd(I2C1, ENABLE);

  //(#) Enable the DMA using the DMA_Cmd() function when using DMA mode in

  // the transfers.


First the I2C1 clock is enabled. SDA and SCL are connected to PB7 and PB6, so GPIOB clock must also be enabled.

PB6 and PB7 are set to their Alternate Function 1 (see Table 15 in the STM32F051R8 datasheet), the open-drain pin type is selected and the pullup resistors are enabled.

Then I2C1 is initiated with the I2C_InitTypeDef structure:

In this example we will not use interrupts or DMA, so I2C1 can finally be enabled with the I2C_Cmd() function.

Power on reset

PB8 will be used to trigger the Power On Reset (POR) signal of the BTHQ21605V. This pin is initialized with a GPIO_InitTypeDef structure in the previously discussed function BTHQ21605V_Setup(). In another function called BTHQ21605V_PowerOn() this pin is toggled to initiate the power on reset sequence of the LCD controller. The logic analyzer shows the following timing diagram:

First the POR signal on channel 2 is made logic 1. When it is made logic 0 some additional time (2 ms) is waited before the I2C communication is started. This gives the LCD controller time for internal reset actions.

The exact I2C communication on SDA and SCL is not visible here, as the window is not zoomed in far enough.

Writing data to the BTHQ21605V

When writing data to an I2C slave we have to consider three things:

The relationship between these three is made visible with a time-sequence diagram. The diagram below shows how 10 bytes of data are written to I2C slave address 0x76:

The five blue arrows show moments in time where the I2C software needs to wait for an I2C hardware event. An ACK from the slave for instance will be detected by the I2C hardware and when detected, the Transmitter ready flag (TXIS) will be set in the Interrupt and Status Register (ISR). The software can simply wait for this event to happen with a while-loop or an interrupt can be generated.

With a Logic Analyzer we can exactly see what SDA and SCL signals are generated between the STM32F05x hardware and the Slave I2C interface:

Notice that sending ten bytes of data requires 11 bytes to be transmitted: address+write command and 10 data bytes. Each transfer is acknowledged by the slave.

The following code, using the Standard Peripheral Driver Library, realizes this sequence:

void BTHQ21605V_PowerOn(void)


  // POR: Power On Reset


  Delay(SystemCoreClock/8/500); // 2ms


  Delay(SystemCoreClock/8/500); // 2ms

  // Wait until BTHQ21605V ready

  while(BTHQ21605V_ReadStatus(BTHQ21605V_STATUS_BF) != RESET){;}

  // Wait while I2C peripheral is not ready


  // Start I2C write transfer for 10 bytes

  I2C_TransferHandling(I2C1, BTHQ21605V_ADDR, 10, I2C_AutoEnd_Mode,




  // 1. Write control byte: select instruction register

  I2C_SendData(I2C1, BTHQ21605V_CONTROL_BYTE);


  // 2. Send instruction: 2 lines x 16, 1/18 duty, extended instruction set

  I2C_SendData(I2C1, BTHQ21605V_FUNCTION_SET |

                     BTHQ21605V_FUNCTION_SET_M |




  // 3. Send instruction: Set display configuration to right to left & top to

  //    bottom

  I2C_SendData(I2C1, BTHQ21605V_DISP_CONF |

                     BTHQ21605V_DISP_CONF_P |




  // 4. Send instruction: Set to character mode, full display, icon blink

  //    disabled

  I2C_SendData(I2C1, BTHQ21605V_ICON_CTL);



  // 5. Send instruction: Set voltage multiplier to 2

  I2C_SendData(I2C1, BTHQ21605V_HV_GEN);


  // 6. Send instruction: Set Vlcd and store in register VA

  I2C_SendData(I2C1, BTHQ21605V_VLCD_SET | 0x28);


  // 7. Send instruction: Change from extended instruction set to basic

  //    instruction set

  I2C_SendData(I2C1, BTHQ21605V_FUNCTION_SET |



  // 8. Send instruction: Display control: set display on, cursor off, no blink

  I2C_SendData(I2C1, BTHQ21605V_DISPLAY_CTL |




  // 9. Send instruction: Entry mode set: increase DDRAM after access, no shift

  I2C_SendData(I2C1, BTHQ21605V_ENTRY_MODE_SET |




  // 10. Send instruction: Return home, set DDRAM address 0 in address counter

  I2C_SendData(I2C1, BTHQ21605V_RETURN_HOME);


  I2C_ClearFlag(I2C1, I2C_ICR_STOPCF);


Before writing to the BTHQ21605V we want to make sure the LCD controller is not busy. The function BTHQ21605_ReadStatus() is used to verify the busy flag in the status register of the LCD controller.

After making sure the STM32F05x I2C hardware is ready, a transfer is started with the function I2C_TransferHandling(). This function needs some additional explanation. It is documented like this:


  * @brief  Handles I2Cx communication when starting transfer or during transfer

  *         (TC or TCR flag are set).

  * @param  I2Cx: where x can be 1 or 2 to select the I2C peripheral.

  * @param  Address: specifies the slave address to be programmed.

  * @param  Number_Bytes: specifies the number of bytes to be programmed.

  *   This parameter must be a value between 0 and 255.

  * @param  ReloadEndMode: new state of the I2C START condition generation.

  *   This parameter can be one of the following values:

  *     @arg I2C_Reload_Mode: Enable Reload mode .

  *     @arg I2C_AutoEnd_Mode: Enable Automatic end mode.

  *     @arg I2C_SoftEnd_Mode: Enable Software end mode.

  * @param  StartStopMode: new state of the I2C START condition generation.

  *   This parameter can be one of the following values:

  *     @arg I2C_No_StartStop: Don't Generate stop and start condition.

  *     @arg I2C_Generate_Stop: Generate stop condition (Number_Bytes should be

  *          set to 0).

  *     @arg I2C_Generate_Start_Read: Generate Restart for read request.

  *     @arg I2C_Generate_Start_Write: Generate Restart for write request.

  * @retval None


After the transfer is handled, the TXIS flag will be set. The function BTHQ21605V_WaitForI2CFlag() is implemented to wait for the flag to be set.

After sending the last byte the TXIS flag will not be set. Instead, we will wait for the hardware to have generated the STOP condition by checking the STOPF-flag.

Erroneous situations

As long as there are no unexpected situations all flags will be set/reset as expected. But, if for instance no I2C slave is connected, the TXIS flag will never be set and the program ends in an endless-loop. Or if for instance the slave device needs more time to execute a certain command it might not respond as expected also resulting in an endless loop.

One technique to solve this is adding timeouts in the while-loops when checking the ISR flags. This has been implemented in the function BTHQ21605V_WaitForI2CFlag():

void BTHQ21605V_WaitForI2CFlag(uint32_t flag)


  uint32_t timeout = BTHQ21605V_TIMEOUT;


  if(flag == I2C_ISR_BUSY)


    while(I2C_GetFlagStatus(I2C1, flag) != RESET)


      if(timeout-- == 0)


        BTHQ21605V_Status = BTHQ21605V_COMM_ERROR;







    while(I2C_GetFlagStatus(I2C1, flag) == RESET)


      if(timeout-- == 0)


        BTHQ21605V_Status = BTHQ21605V_COMM_ERROR;






Before checking the flag in the status register with the standard peripheral driver library function I2C_GetFlagStatus(), the variable ‘timeout’ is set to a value defined by BTHQ21605V_TIMEOUT. This value indicates how many times the flag should be checked. Depending on the clock frequency of the core this takes a certain time. Notice that the value BTHQ21605V_TIMEOUT in the example code is not used to create an exact timing. It is simply a construction to make sure the communication will never get stuck in an endless loop.

If the flag never gets set/reset, the global variable BTHQ21605V_Status will be set to BTHQ21605V_COMM_ERROR and this variable will be checked periodically in the main-loop.

Reading data from the BTHQ21605V

Reading data from an I2C slave is very similar. However, before we can read data, we must first tell the desired slave device what data needs to be read. The BTHQ21605V for example, allows us to read status information or LCD display data. The following time-sequence diagram shows how the STM32F05x is able to read status information from the BTHQ21605V internal status register:

A so called ‘Repeated Start Condition’ is used to switch from writing to reading. The address is sent ones again, but now with the LSB set to logic 1 indicating the master wants to read from the slave. The next data transfer the direction of SDA-line will be swapped, transferring data from the slave to the master. After the byte has been received, the STM32F05x I2C hardware automatically generates a NACK and a STOP condition (due to setting AutoEnd mode in the second transfer handling).

The Logic Analyzer shows the following output:

The green dot on the left shows the START condition, the green dot in the middle the REPEATED START condition and the red square on the right the STOP condition.

The instruction register is selected by transmitting 0x00 to the slave. The slave responds with 0x48 being the status in the status register. The meaning of these bits can be found in the datasheet of the PCF2119x controller.

The following code is used to realize this sequence:

uint8_t BTHQ21605V_ReadStatus(uint8_t bf_ac)


  uint8_t status = RESET;

  // Wait while I2C peripheral is not ready


  // Start I2C write transfer for 1 byte, do not end transfer (SoftEnd_Mode)

  I2C_TransferHandling(I2C1, BTHQ21605V_ADDR, 1, I2C_SoftEnd_Mode,



  // 1. Write control byte: select instruction register

  I2C_SendData(I2C1, BTHQ21605V_CONTROL_BYTE);


  // Repeated start I2C read transfer for 1 byte

  I2C_TransferHandling(I2C1, BTHQ21605V_ADDR, 1, I2C_AutoEnd_Mode,




  // 1. Read status

  status = I2C_ReceiveData(I2C1);


  // Wait for- and clear stop condition




  // Return requested result

  if(bf_ac == BTHQ21605V_STATUS_BF)


    return(status & 0x80);


  else if(bf_ac == BTHQ21605V_STATUS_AC)


    return(status & 0x7F);


  else // BTHQ21605V_STATUS_BF_AC





It is straightforward to see how the function I2C_TransferHandling() is used two times. The first time the transfer should not be ended (I2C_SoftEnd_Mode) the second time it should (I2C_AutoEnd_Mode).

After the Stop condition has been detected, the requested result is returned to the caller.



  * @brief  This function changes the state of the display cursor.

  * @param  NewState: Enable or disable the cursor.

  *                   This parameter can be: ENABLE or DISABLE.

  * @retval None


void BTHQ21605V_Cursor(FunctionalState NewState);


  * @brief  This function changes the state of blinking the current selected

  *         character.

  * @param  NewState: Enable or disable character blinking.

  *                   This parameter can be: ENABLE or DISABLE.

  * @retval None


void BTHQ21605V_CharBlink(FunctionalState NewState);


  * @brief  This function adds the Euro sign (€) to the first free location in

  *         CGRAM

  * @param  None

  * @retval None


void BTHQ21605V_AddEuroSignToCGRAM(void);

Tip: For help on the the last function see the PCF2119x datasheet paragraph 16.14.


The STM32F051R8T6 used on the discovery board utilizes two I2C interfaces: I2C1 and I2C2. Implement the following:

TIP. Implement an interrupt handler to handle slave communication.

I2C Communication Peripheral Application Library (CPAL v2)

In addition to the I2C standard peripheral driver library, STMicroelectronics has also developed a more sophisticated driver called Communication Peripheral Application Library (CPAL). It handles all I2C communication with interrupts and DMA based functions. Version 2 is suitable for STM32F0 and STM32F3 devices. It aims to provide an intuitive, easy to use and sufficient programming interface (Init, Deinit, Read, Write):

A lot of documentation and example code is available here:

CPAL v2 documentation

CPAL v2 firmware library & example code


This assignment is meant for those who like a challenge. Rewrite the BTHQ21605V driver so it uses CPAL v2.

Compare the two implementations in terms of code size and execution time. More lessons? Click here!

[1] Ref.: STMicroelectronics (2012), User Manual UM1566, [Online publication]