STM32F103C6T6A Bare Metal Programming – I2C Slave Read & Write

In my previous blog posts, we explored how to utilize the STM32 microcontroller as a master device, including performing read and write operations with a slave. Now, let’s advance to the next level, as the microcontroller is also capable of functioning as a slave device. In this post, I will explain how to configure the STM32 as a slave and how to carry out read and write operations. Since this topic builds on the previous discussions, I highly recommend reviewing those posts if you are new to I2C.

We will use most of the functions that were used in the master I2C with some necessary modifications.

I2C Slave initialization

The I2C slave initialization is similar to what we have done for master. Since the master doesn’t have any I2C address but the slave has, we just want to configure the address here. The I2C address is to be configured in the I2C Own Address Register 1 (I2C_OAR1) register. As per the reference manual software should set the 14th bit of this register as 1. So we need to add the following line in the init function

I2C1->OAR1 = 0x4000 | (addr << 1);

We can use the polling method to receive the data, but I am using the interrupt method here because that will be a more efficient way while writing the code.

I2C1->CR2 |= I2C_CR2_ITEVTEN;

uint32_t prioritygroup = NVIC_GetPriorityGrouping();
NVIC_SetPriority(I2C1_EV_IRQn, NVIC_EncodePriority(prioritygroup, 10, 0));
NVIC_EnableIRQ(I2C1_EV_IRQn);

Once the slave address matches with the address sent by master, the slave has to send the ACK for that, so we need to enable the ACK bit in CR1 during initialization

I2C1->CR1 |= I2C_CR1_ACK;

So the complete initialization function will be looks like

void i2c_slave_init(uint8_t addr) {
    uint32_t prioritygroup;

    // Enable clock for Port B, I2C, Alternate function IO
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;

    // Configure the I2C pins as open-drain output
    GPIOB->CRL |= (GPIO_CRL_CNF6 | GPIO_CRL_MODE6_0);
    GPIOB->CRL |= (GPIO_CRL_CNF7 | GPIO_CRL_MODE7_0);

    // Disable the peripheral enable bit
    I2C1->CR1 &= ~(I2C_CR1_PE);

    // Reset I2C
    I2C1->CR1 |= I2C_CR1_SWRST;
    I2C1->CR1 &= ~I2C_CR1_SWRST;

    // Configure internal clock, raise time
    I2C1->CR2 |= 0x08;
    I2C1->TRISE = 0x9;

    // Clock control register for 100KHz I2C frequency
    // at system clock frequency of 8MHz
    I2C1->CCR = 0x28;

    // Configure the slave address; as per the datasheet
    // bit 14 should be set.
    I2C1->OAR1 = 0x4000 | (addr << 1);

    // Enable the peripheral enable bit and ACK
    I2C1->CR1 |= I2C_CR1_PE;
    I2C1->CR1 |= I2C_CR1_ACK;
    I2C1->CR2 |= I2C_CR2_ITEVTEN;

    // Enable NVIC
    prioritygroup = NVIC_GetPriorityGrouping();
    NVIC_SetPriority(I2C1_EV_IRQn, NVIC_EncodePriority(prioritygroup, 10, 0));
    NVIC_EnableIRQ(I2C1_EV_IRQn);
}

I2C Slave Read

The sequence diagram for the I2C slave read is shown in the figure 272 of STM32 reference manual RM0008.

  1. S – Start condition

We don’t need to do anything here, because the master is responsible for starting the condition.

  1. Address – Slave address from master

Unlike what we have seen in the master, the slave will be receiving the address from the master. So the device should be listening to any bus operation during the idle time. We already have enabled the interrupts for I2C events so if the address sent by master matches with the current slave the interrupt will be generated.

  1. A – ACK

The ACK bit already enabled in the initialization, nothing to do here

  1. EV1

Upon the detected address match with the slave address the ADDR bit in the I2C_SR1 will be set. This can be cleared by reading I2C_SR1 & I2C_SR2. If the master is trying to read the data (slave write), the TRA bit in the I2C_SR2 register will be set and TRA will be clear if the slave is reading the data. In our case the TRA bit will be 0.

if((I2C1->SR1 & I2C_SR1_ADDR) != 0) {
     (void)I2C1->SR1;
     (void)I2C1->SR2;
     if(0 == (I2C1->SR2 & I2C_SR2_TRA)) {
        // Receive data
     }
}

   

  1. Data – The data sent from master
  2. A – ACK sent back to the master
  3. EV2

Once the data received in the I2C_DR register, the RxNE flag in the I2C_SR1 will be set. This flag can be cleared by reading the I2C_DR register

while(!(I2C1->SR1 & I2C_SR1_RXNE));
data = I2C1->DR;
  1. P – Stop condition generated by master
  2. EV4

Once the stop condition is detected, the interrupt will be generated by setting the STOPF flag in the I2C_SR1 register. This flag can be cleared by reading the I2C_SR1 register and writing to I2C_CR1 register.

if (I2C1->SR1 & I2C_SR1_STOPF) {
    (void)I2C1->SR1;
    I2C1->CR1 |= I2C_CR1_PE;
}

I2C Slave Write

The sequence diagram 271 shown in STM32 reference manual RM0008 explains how the write works. This looks similar to what we have seen in the case of slave read except the points mentioned below.

EV1

When the ADDR flag is set upon matching the address sent by the master, we have to check the TRA bit in the I2C_SR2 register. This flag will be set if the master sent the address for read operation (ie slave write).

if((I2C1->SR1 & I2C_SR1_ADDR) != 0) {
    (void)I2C1->SR1;
    (void)I2C1->SR2;
    if(I2C1->SR2 & I2C_SR2_TRA) {
        // send data here
    }
}

EV3-1

Once the slave detects the address and ADDR flag has been cleared, the slave has to wait until the TxE flag becomes 1. This notifies that the transmit buffer and transmit shift register are empty and software can load the data into the I2C_DR.

EV3

Here also the TxE flag will be set. This notifies the I2C_DR is empty but the shift register is  not empty. But we can write to I2C_DR in this stage, no need to wait until the shift register is free.

while(!(I2C1->SR1 & I2C_SR1_TXE));
I2C1->DR = byte;

EV3-2

After the master has received all the data, it sends a NACK, which causes the AF bit in the SR register to be set. To clear this bit, we need to write a 0 to it.

while(!(I2C1->SR1 & I2C_SR1_AF));
I2C1->SR1 &= ~I2C_SR1_AF;

Combined Slave Read & Write code

The complete read and write code will like

void i2c_slave_init(uint8_t addr) {
    uint32_t prioritygroup;

    // Enable clock for Port B, I2C, Alternate function IO
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;

    // Configure the I2C pins as open-drain output
    GPIOB->CRL |= (GPIO_CRL_CNF6 | GPIO_CRL_MODE6_0);
    GPIOB->CRL |= (GPIO_CRL_CNF7 | GPIO_CRL_MODE7_0);

    // Disable the peripheral enable bit
    I2C1->CR1 &= ~(I2C_CR1_PE);

    // Reset I2C
    I2C1->CR1 |= I2C_CR1_SWRST;
    I2C1->CR1 &= ~I2C_CR1_SWRST;

    // Configure internal clock, raise time
    I2C1->CR2 |= 0x08;
    I2C1->TRISE = 0x9;

    // Clock control register for 100KHz I2C frequency
    // at system clock frequency of 8MHz
    I2C1->CCR = 0x28;

    // Configure the slave address; as per the datasheet
    // bit 14 should be set.
    I2C1->OAR1 = 0x4000 | (addr << 1);

    // Enable the peripheral enable bit and ACK
    I2C1->CR1 |= I2C_CR1_PE;
    I2C1->CR1 |= I2C_CR1_ACK;
    I2C1->CR2 |= I2C_CR2_ITEVTEN;

    // Configure the NVIC
    prioritygroup = NVIC_GetPriorityGrouping();
    NVIC_SetPriority(I2C1_EV_IRQn, NVIC_EncodePriority(prioritygroup, 10, 0));
    NVIC_EnableIRQ(I2C1_EV_IRQn);
}
void i2c_slave_listen() {
    // Wait until any master sent our address
    // through the I2C interface. Clear the flag
    // if the address matched.
    while(!(I2C1->SR1 & I2C_SR1_ADDR));
    (void)I2C1->SR1;
    (void)I2C1->SR2;
}

uint8_t i2c_slave_recv_byte() {
    // Receive 1 byte from the master.
    while(!(I2C1->SR1 & I2C_SR1_RXNE));
    return I2C1->DR;
}

void i2c_slave_send_byte(uint8_t byte) {
    // Send 1 byte to master
    while(!(I2C1->SR1 & I2C_SR1_TXE));
    I2C1->DR = byte;
}

void i2c_slave_send_finish() {
    // Wait until the NACK is received and clear the flag
    while(!(I2C1->SR1 & I2C_SR1_AF));
    I2C1->SR1 &= ~I2C_SR1_AF;
}

static volatile uint8_t off_time, on_time;
void I2C1_EV_IRQHandler() {
    if((I2C1->SR1 & I2C_SR1_ADDR) != 0) {
        (void)I2C1->SR1;
        (void)I2C1->SR2;
        if(I2C1->SR2 & I2C_SR2_TRA) {
            // Slave is in write mode
            i2c_slave_send_byte('A');
            i2c_slave_send_byte('B');
            i2c_slave_send_finish();
        } else {
            // Slave is in read mode
            on_time = i2c_slave_recv_byte();
            off_time = i2c_slave_recv_byte();
        }
    }

    if (I2C1->SR1 & I2C_SR1_STOPF) {
        (void)I2C1->SR1;
        I2C1->CR1 |= I2C_CR1_PE;
    }
}

STM32-Bare-Metal/I2C/I2C_SLAVE at master · mshafeeqkn/STM32-Bare-Metal · GitHub

Leave a comment