STM32F103C6T6A Bare Metal Programming – I2C Master Read

In the previous post, I explained how to send data from the I2C master to the slave using the STM32F103C6T6 as the master and the AT24C04 as the slave. In this post, we will explore how to read data from the AT24C04 chip via I2C. First, we need to review the data sheet of the AT24C04 EEPROM to understand the sequence of I2C communication with the EEPROM.

In the EEPROM write operation, we saw that only the lower 4 bits are incremented during sequential writes, so multiple write operations are necessary to write data across more than one page (beyond the 16-byte boundary). However, there is no such limitation during the read operation.

To read data from the EEPROM, the master initially needs to send the start condition and then the address in write mode, followed by the desired address, and then again send another start condition followed by the EEPROM address in read mode; at this point, the EEPROM will send the data sequentially by incrementing the address with each ACK sent by the master. The master has to send a NACK when it receives the desired data to terminate the communication.

Figure 274 in the STM32 reference manual RM0008 shows the sequence diagram of read write operation. As I explained before, before starting the read operation first we need to write the desired address (steps 1 to 6 in the write sequence diagram). Then the read sequence has to start as follows

  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 Read bit (is_read = 1). In the read operation, the master has to send the ACK for every byte received from the slave. This functionality can be enabled by ACK flag in I2C_CR1 register.
I2C1->CR1 |= I2C_CR1_ACK;
I2C1->DR = (addr << 1) | is_read;
  1. A – ACK will be set by the slave whose address match
  2. EV6 – When the slave sends the ACK, the ADDR bit will be set and this is cleared by reading I2C_SR1 and I2C_SR2
while(!(I2C1->SR1 & I2C_SR1_ADDR));
(void)I2C1->SR1;
(void)I2C1->SR2;
  1. EV6_1 – This is used only for 1 byte transfer. Here before sending the data we need to disable the ACK and generate the stop condition.
I2C1->CR1 &= ~I2C_CR1_ACK;
I2C1->CR1 |= I2C_CR1_STOP;
  1. Data – Data would have arrived
  2. A – The hardware will send ACK automatically, no code is required
  3. EV7 – The RxNE flag would have been set, the flag can be cleared by reading the I2C_DR register.
while(!(I2C1->SR1 & I2C_SR1_RXNE));
data = I2C1->DR;
  1. EV7_1 – same as EV6_1 (step 12).  Ie, disable the ACK and generate the stop condition. Once this is done, the hardware will send NACK once received the last bit indicating that the read operation has been done.
  2. P – Stop condition is already configured to send automatically after the last byte is received (EV7_1)

Combining altogether, the program would be like

void i2c_start() {
    // Generate start condition, wait until the SB bit set
    // clear the SB flag by reading SR1 register
    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) {
    // Set ACK flag for receive data
    if(is_read) {
        I2C1->CR1 |= I2C_CR1_ACK;
    }

    // Write address into the data register with write/read flag
    // Wait until the ADDR flag set and clear the ADDR flag by
    // reading SR1 and SR2.
    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) {
    // Wait until the transmit buffer empty
    while(!(I2C1->SR1 & I2C_SR1_TXE));
    I2C1->DR = byte;

    // Wait until transmit finished
    while(!(I2C1->SR1 & I2C_SR1_BTF));
}

void i2c_stop() {
    // Generate stop condition
    I2C1->CR1 |= I2C_CR1_STOP;
}

uint8_t i2c_recv_byte() {
    // Receive a byte from remote
    while(!(I2C1->SR1 & I2C_SR1_RXNE));
    return I2C1->DR;
}

void read_eeprom_data(uint8_t chip, uint8_t addr, uint8_t* data, uint16_t len) {
    int i = 0;
    // Send the memory address in write mode
    i2c_start();
    i2c_addr(chip, 0);
    i2c_send_byte(addr);

    // Again start condition without a stop condition
    // but in read mode
    i2c_start();
    i2c_addr(chip, 1);
    while(len--) {
        // Set the flag to send NACK for the last byte.
        // This is to be done just after the second last
        // byte has been sent.
        if(len == 1) {
            I2C1->CR1 &= ~I2C_CR1_ACK;
            I2C1->CR1 |= I2C_CR1_STOP;
        }

        // Store the received data in the buffer
        data[i++] = i2c_recv_byte();
    }

    // Generate stop condition after receiving
    // whole data
    i2c_stop();
}

void i2c_start() {
    // Generate start condition, wait until the SB bit set
    // clear the SB flag by reading SR1 register
    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) {
    // Set ACK flag for receive data
    if(is_read) {
        I2C1->CR1 |= I2C_CR1_ACK;
    }

    // Write address into the data register with write/read flag
    // Wait until the ADDR flag set and clear the ADDR flag by
    // reading SR1 and SR2.
    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) {
    // Wait until the transmit buffer empty
    while(!(I2C1->SR1 & I2C_SR1_TXE));
    I2C1->DR = byte;

    // Wait until transmit finished
    while(!(I2C1->SR1 & I2C_SR1_BTF));
}

void i2c_stop() {
    // Generate stop condition
    I2C1->CR1 |= I2C_CR1_STOP;
}

uint8_t i2c_recv_byte() {
    // Receive a byte from remote
    while(!(I2C1->SR1 & I2C_SR1_RXNE));
    return I2C1->DR;
}

void read_eeprom_data(uint8_t chip, uint8_t addr, uint8_t* data, uint16_t len) {
    int i = 0;

    // Send the memory address in write mode
    i2c_start();
    i2c_addr(chip, 0);
    i2c_send_byte(addr);

    // Again start condition without a stop condition
    // but in read mode
    i2c_start();
    i2c_addr(chip, 1);
    while(len--) {
        // Set the flag to send NACK for the last byte.
        // This is to be done just after the second last
        // byte has been sent.
        if(len == 1) {
            I2C1->CR1 &= ~I2C_CR1_ACK;
            I2C1->CR1 |= I2C_CR1_STOP;
        }

        // Store the received data in the buffer
        data[i++] = i2c_recv_byte();
    }

    // Generate stop condition after receiving
    // whole data
    i2c_stop();
}

Please note that the I2C has to be initialized before calling the I2C start. Here is the link to the complete code in my repository. 

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

Leave a comment