STM32F103C6T6A Bare Metal Programming – I2C Master Write

In my previous post, I explained how I2C functions and demonstrated a hands-on example using the ATTiny85 through the bit-banging method, as it lacks a built-in I2C peripheral. In this post, we will explore the implementation of I2C on a microcontroller that includes a built-in I2C peripheral, specifically the STM32F103C6T6A. We will focus on the I2C master mode in this discussion, with the slave mode covered in a future post. Detailed information about the STM32 I2C peripheral can be found in chapter 26 of the STM32 reference manual (RM0008). Our microcontroller features a single I2C peripheral, with pins PB6 designated for SCL and PB7 for SDA. These pins can also be remapped to PB8 and PB9 if needed.

Let’s split this post into two parts, the first part will explain how the master writes or sends the data to the slave (This blog post) and in the second part (Next blog post) how the master reads the data from the slave.

Before write/read operation, the I2C peripheral has to be initialized.

Initialize I2C peripheral

  1. Enable the Clock for required peripherals:  As we usually do for STM32 microcontrollers, first we need to enable the clock for required peripherals. Since the I2C pins are multiplexed with PORTB pins, we have to enable the clock for GPIOB, AFIO and I2C. The GPIOB and AFIO are connected with APB2 and I2C is connected to the APB1 bus.
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
  1. Configure the SCL and SDA pins: With reference to the Table 27-I2C configure SCL(PB6) and SDA(PB7) as alternate function open-drain.
GPIOB->CRL |= (GPIO_CRL_CNF6 | GPIO_CRL_MODE6_0);
GPIOB->CRL |= (GPIO_CRL_CNF7 | GPIO_CRL_MODE7_0);
  1. Reset the I2C peripheral: This is recommended to avoid any previous error conditions
I2C1->CR1 |= I2C_CR1_SWRST;
I2C1->CR1 &= ~I2C_CR1_SWRST;
while((I2C1->SR2 & I2C_SR2_BUSY));
  1. Configure the parameter to generate the I2C clock: We need to tell I2C what clock we are using for the STM32 microcontroller. This is done through the last 6-bits of the I2C_CR2 register. This is done to reduce the hardware complexity and hence the cost. Since we are using the external 8MHz crystal oscillator (HSE), we are configuring this value in the I2C_CR2 register.
I2C1->CR2 |= 0x08;

STM32-I2C peripheral supports two modes of operations, first one is standard mode(Sm) which supports up to 2MHz SCL frequency. The second one is Fast mode (Fm) which supports up to 4MHz SCL frequency. But as per the standard Sm uses 100KHz and Fm uses 400KHz clock frequency. Here we are using Sm with 100KHz SCL frequency. By default the STM32-I2C will be in Sm and we can change it to Fm using the I2C_CCR register. 

I2C specification says that the maximum raising time of SCL clock should not be more than 1000ns. We have to configure this value in the TRISE field of the I2C_TRISE enable register.

The equation to find TRISE value is 

TRISE = Maximum Raise time1/System Clock + 1 = 1000 ns125 ns + 1 = 9

I2C1->TRISE |= 0x09;

Now we have to configure the Clock Control Register which is used to generate the SCL. The value of CCR register can be find out as follows

CCR = System Clock Frequency/2 x Target Frequency = 8000000/2 x 100000 = 40 = 0x28

I2C1->CCR |= 0x28;
  1. Enable the I2C: 
I2C1->CR1 |= I2C_CR1_PE;

Master Write Operation

Figure 273 in the RM0008 document explains how the master transmitter works. Here is the snapshot.

  1. S – Start Condition

The start condition can be generated by setting the START bit in the I2C_CR1 register.

I2C1->CR1 |= I2C_CR1_START; 
  1. EV5 – Start Bit Set 

Once the start condition is generated the SB bit of I2C_SR1 will be set. This flag can be cleared by reading the I2C_SR1 register and followed by writing an address in the I2C_DR.

while((I2C1->SR1 & I2C_SR1_SB) == 0);
(void)I2C1->SR1;
  1. Address – Write Address to I2C_DR along with Write bit (is_read = 0)
I2C1->DR = (addr << 1) | is_read;
  1. A – Once the address is matched with any slave address the slave will pull the SDA line down as ACK.
  2. EV6 – Once the slave sends the ACK, the ADDR bit of I2C_SR1 will be set and this can be cleared by reading the I2C_SR1 and I2C_SR2.
while(!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR1;
(void)I2C1->SR2;
  1. EV8_1 – Shift register and Data register are empty

Wait until the TXE bit of I2C_SR1 is set. Which means the shift register and data register of I2C are empty. So write the first byte of data to I2C_DR and wait until the byte transfer is finished. This can be notified by BTF of I2C_SR1.

while(!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->DR = byte;
while(!(I2C1->SR1 & I2C_SR1_BTF));
  1. P – Stop condition

Generate the stop condition by setting the STOP bit of I2C_CR1 register.

I2C1->CR1 |= I2C_CR1_STOP;

Hands-on

Now let’s implement the code to write some data into AT24C04 EEPROM. Here is the code to write a paragraph to the EEPROM chip.

AT24C04 write procedure, first we need to send the address to which the data to be written. On sending each byte the lower 4 bits of address will incremented automatically but won’t increment the higher 4 bits. This will lead to overwriting the data if we write more than 16 bytes in a single shot. Also once the stop condition is sent to the EEPROM, there should be a minimum 5ms. These are taken care of in the following program.

#include "main.h"

static void delay(uint32_t tick) {
    for (volatile uint32_t i = 0; i < tick; ++i) {
        __NOP();  // No operation (compiler barrier)
    }
}

void i2c_init() {
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;

    GPIOB->CRL |= (GPIO_CRL_CNF6 | GPIO_CRL_MODE6_0);
    GPIOB->CRL |= (GPIO_CRL_CNF7 | GPIO_CRL_MODE7_0);

    I2C1->CR1 |= I2C_CR1_SWRST;
    I2C1->CR1 &= ~I2C_CR1_SWRST;
    while((I2C1->SR2 & I2C_SR2_BUSY));

    I2C1->CR2 |= 0x08;
    I2C1->CCR |= 0x28;
    I2C1->TRISE |= 0x09;

    I2C1->CR1 |= I2C_CR1_PE;
}

void i2c_start() {
    I2C1->CR1 |= I2C_CR1_START;
    while((I2C1->SR1 & I2C_SR1_SB) == 0);
    (void)I2C1->SR1;
}

void i2c_addr(uint8_t addr, uint8_t is_read) {
    I2C1->DR = (addr << 1) | is_read;
    while(!(I2C1->SR1 & I2C_SR1_ADDR));
    (void)I2C1->SR1;
    (void)I2C1->SR2;
}

void i2c_send_byte(uint8_t byte) {
    while(!(I2C1->SR1 & I2C_SR1_TXE));
    I2C1->DR = byte;
    while(!(I2C1->SR1 & I2C_SR1_BTF));
}

void i2c_stop() {
    I2C1->CR1 |= I2C_CR1_STOP;
}

void write_eeprom_data(uint8_t chip, uint8_t addr, uint8_t* data, uint16_t len) {
    uint16_t i = 0;
    uint16_t end_addr = addr | 0x0F;
    uint16_t start = 0;
    uint16_t end = end_addr - addr + 1;

    while(len) {
        i2c_start();
        i2c_addr(chip, 0);
        i2c_send_byte(addr);

        for(i = start; i < end; i++) {
            i2c_send_byte(data[i]);
            len--;
        }

        i2c_stop();
        addr += (end - start);
        start += (end - start);
        if(len > 16) {
            end += 16;
        } else {
            end += len;
        }

        if(addr == 0 && chip == 0x50) {
	    // If first 512 bytes is completed, go to the next page
            // whose I2C address is current address + 1
            chip++;
        }

        // 5ms delay
        delay(10000);
    }
}

Here is the link to the complete source code

STM32-Bare-Metal/I2C/I2C_MASTER/I2C_MASTER_WRITE at master · mshafeeqkn/STM32-Bare-Metal · GitHub

Leave a comment