Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 35

I2C Master Read/Write Operations

Recommended resources

Hello! Welcome to the next step in our exploration of synchronous communication protocols.

In the previous lesson, we configured the I2C peripheral on your STM32 microcontroller, diving deep into the register-level setup for clocks, GPIOs, and timing. You now have a peripheral that is enabled and ready for action.

Today, we will build directly on that foundation. Our goal is to implement I2C master write and read transactions, handling start/stop conditions and ACKs. We will move from a configured but silent peripheral to one that can actively communicate on the bus. We'll construct the low-level functions required to send and receive data, which we will then use in the next lesson to control the I2C LCD in your kit.

1. The Anatomy of an I2C Transaction

Before writing any code, we need a crystal-clear picture of the sequence of events that constitutes an I2C transaction. At its core, every transaction is a sequence of states on the SDA and SCL lines, governed by the master.

To get a solid conceptual overview of this sequence, let's watch a short video that explains the structure of I2C data frames.

I2C Frame structure Understanding | I2C data transmission

The video 'I2C Frame structure Understanding' from Foolish Engineer provides an excellent, easy-to-follow breakdown of the bits and bytes that make up write and read operations.

Watch the entire video from 00:43 to 09:58. Pay close attention to the analogy used, as it clearly separates the key phases of a transaction: Start, Slave Address + R/W bit, ACK, Register Address (for register-based devices), Data, and Stop.

As you saw, a transaction is not just a single blast of data. It's a carefully choreographed dance of signals. Let's visualize the two main types of transactions with timing diagrams.

Master Write Transaction: This is the simpler case, where the master sends data to a slave.

I2C Master Write Transaction Timing Diagram
A typical I2C write transaction. The master initiates with a START, sends the 7-bit slave address with the R/W bit set to 0 (write), waits for an ACK from the slave, sends a data byte, waits for another ACK, and finally issues a STOP.

Master Read Transaction: This is more complex because the master must first tell the slave what it wants to read. This involves a write phase followed by a read phase, linked by a special "Repeated Start" condition.

I2C Read Register Sequence Timing Diagram
An I2C read transaction. Note the two phases. First, a write sequence sets the register pointer in the slave. Then, a Repeated Start occurs, followed by a new addressing phase with the R/W bit set to 1 (read). The master then clocks in data from the slave, sending ACK after each byte until the last one, which gets a NACK before the STOP.

2. Implementing a Master Write Transaction

Now, let's translate the write transaction diagram into C code. Our approach will be to create small, reusable functions for each step of the protocol. We'll be polling the I2C status registers (SR1 and SR2) to know when each step is complete.

The article "STM32 I2C Register Configuration Tutorial" from ControllersTech provides a clear guide for creating these functions.

STM32 I2C Register Configuration Tutorial - ControllersTech

This article details the register-level implementation of the core I2C functions. We'll focus on the functions for starting, writing, addressing, and stopping.

Read the sections 'I2C START', 'I2C WRITE', and 'Send Slave Address'. Focus on which bits in the Control Register (I2C_CR1) are set to initiate an action, and which bits in the Status Register (I2C_SR1) are checked to confirm completion.

Let's break down the logic for each function based on the reference manual and the article.

Function 1: I2C_Start()

  1. Action: Set the START bit (bit 8) in the I2C_CR1 register. This commands the peripheral to generate a Start condition on the bus.
  2. Verification: Poll the SB (Start Bit) flag (bit 0) in the I2C_SR1 register. This flag is set by hardware once the Start condition is successfully sent. The program must wait in a loop until this bit is 1.
void I2C_Start(void) {
    I2C1->CR1 |= (1 << 8); // Set the START bit in CR1
    while (!(I2C1->SR1 & (1 << 0))); // Wait for SB bit to be set (start condition generated)
}

Function 2: I2C_Address()

  1. Action: After the Start, the master must send the slave's address. You write the 7-bit slave address, shifted left by one bit, into the Data Register (I2C_DR). The least significant bit (bit 0) is the R/W bit (0 for write, 1 for read).
  2. Verification: Poll the ADDR (Address Sent) flag (bit 1) in I2C_SR1. This is set when the slave acknowledges its address.
  3. Cleanup: The ADDR flag must be cleared by software. This is done by reading I2C_SR1 followed by reading I2C_SR2.
void I2C_Address(uint8_t address) {
    I2C1->DR = address; // Send the slave address
    while (!(I2C1->SR1 & (1 << 1))); // Wait for ADDR bit to be set
    // Clear ADDR bit by reading SR1 and SR2
    uint8_t temp = I2C1->SR1 | I2C1->SR2;
}

Function 3: I2C_Write()

  1. Verification: Before sending data, you must ensure the Data Register is empty. Poll the TXE (Transmit data register Empty) flag (bit 7) in I2C_SR1.
  2. Action: Write the byte of data to the I2C_DR.
  3. Verification (for multi-byte): If you are sending more data, you can simply wait for TXE again. To confirm the byte has been fully transmitted and acknowledged, you can wait for the BTF (Byte Transfer Finished) flag (bit 2) in I2C_SR1.
void I2C_Write(uint8_t data) {
    while (!(I2C1->SR1 & (1 << 7))); // Wait for TXE bit to be set (DR is empty)
    I2C1->DR = data; // Load data into DR
    while (!(I2C1->SR1 & (1 << 2))); // Wait for BTF bit to be set (transfer finished)
}

Function 4: I2C_Stop()

  1. Action: Set the STOP bit (bit 9) in the I2C_CR1 register. The hardware will then generate the Stop condition on the bus, and this bit is automatically cleared.
void I2C_Stop(void) {
    I2C1->CR1 |= (1 << 9); // Set the STOP bit in CR1
}

With these four functions, you can now construct a complete write transaction:

// Example: Write 0x01 to a device at address 0x4E
I2C_Start();
I2C_Address(0x4E); // 0x4E is address with R/W bit = 0
I2C_Write(0x01);
I2C_Stop();

3. Implementing a Master Read Transaction

Reading from a slave is more nuanced. As shown in the timing diagram, it requires a write phase to specify the target register, followed by a read phase initiated by a repeated start.

The most complex part is managing the ACK/NACK signals. When reading multiple bytes, the master must ACK every byte except the last one. Before reading the final byte, the master must signal this to the slave by sending a NACK, followed by a STOP.

Let's turn back to the ControllersTech article for a detailed explanation of the read logic. This is the most challenging part of today's lesson.

STM32 I2C Register Configuration Tutorial - ControllersTech

This section of the article breaks down the complex logic for single-byte and multi-byte read operations. Pay very close attention to how and when the ACK bit is cleared and the STOP condition is generated.

Read the sections 'I2C READ', 'Read Data from Slave Device', and the two comments below that discuss the slave address for reading (Address+0x01) and the specific timing for single-byte reads on the F103. This is critical for getting your code to work on your Blue Pill board.

Based on the article, let's outline the logic for a generic I2C_Read function.

Key Concepts for Reading:

  • Repeated Start: To switch from writing the register address to reading data without another device interrupting, you call I2C_Start() again before a I2C_Stop().
  • Read Address: To signal a read, you send the slave address with the R/W bit set to 1. This is typically done by (slave_address << 1) | 1. If your address variable already includes the shifted value, it's just address | 1.
  • Receiving Data: You wait for the RXNE (Read data register Not Empty) flag (bit 6) in I2C_SR1 to be set, then read the data from I2C_DR.
  • ACK/NACK Control:
    • To ACK a received byte (and ask for more), the ACK bit (bit 10) in I2C_CR1 must be set.
    • To NACK the final byte (to signal the end of the read), the ACK bit must be cleared before the final byte is received.

The logic differs slightly for reading a single byte versus multiple bytes, as described in the reference manual and the article. Here is a high-level function combining the logic for a complete read operation:

// This is a conceptual function combining all steps.
void Read_From_Device(uint8_t device_address, uint8_t reg_address, uint8_t* buffer, uint8_t size) {
    // 1. Start and write the register address we want to read from
    I2C_Start();
    I2C_Address(device_address); // Address with write bit (0)
    I2C_Write(reg_address); // Send the register address, but use a variant of I2C_Write that doesn't wait for BTF
    
    // 2. Send a repeated start and the address with the read bit set
    I2C_Start();
    // The I2C_Read function handles the rest
    I2C_Read((device_address | 1), buffer, size); // Address with read bit (1)
    
    // The I2C_Stop is handled inside the full I2C_Read implementation
}

Implementing a robust I2C_Read function that handles both single and multi-byte cases requires careful state management. The code in the "I2C READ" section of the LINK article you just studied is an excellent, complete reference for this.


Conclusion

Today, we have bridged the gap between theory and practice by implementing the fundamental I2C transaction functions. You now understand the precise sequence of register manipulations and status flag checks required to master the I2C bus.

Key Takeaways:

  • An I2C write transaction involves: START -> ADDRESS+W -> ACK -> DATA -> ACK -> STOP.
  • An I2C read transaction is a two-phase process: a write to set the register pointer, followed by a REPEATED START, and then the read sequence: ADDRESS+R -> ACK -> DATA -> ACK/.../NACK -> STOP.
  • Communication is orchestrated by setting bits in control registers (I2C_CR1) and polling flags in status registers (I2C_SR1). Key flags are SB, ADDR, TXE, RXNE, and BTF.
  • The master controls acknowledgements during a read. It must send a NACK before reading the final byte to signal the end of the transfer.
  • Clearing the ADDR flag requires a specific read sequence of I2C_SR1 and I2C_SR2.

Preview of the Next Lesson:
With our low-level functions ready, we are prepared to talk to a real device. In the next lesson, we will interface with an I2C LCD. We will apply the I2C_Write functions we've built to send initialization commands and display data on the screen. We will also see how a logic analyzer can be an invaluable tool for debugging I2C traffic, which will complete the final learning outcome for this module.