STM32 I2C Communication with MPU6050 Sensor: Software and Hardware Implementations

I2C Communication Fundamentals

The primary objective of I2C communication is to enable a microcontroller to read from and write to external module registers. This requires implementing at least two core functions: writing to a specific register address and reading from a specific register address.

Asynchronous timing advantages: Saves one clock line, conserving resources; disadvantages: Strict event requirements, heavy hardware circuit dependency

Synchronous timing offers the opposite characteristics.

1. I2C Communication Overview

I2C (Inter-Integrated Circuit) is a general-purpose data bus developed by Philips.

Two communication lines: SCL (Serial Clock) and SDA (Serial Data)

Synchronous, half-duplex communication with data acknowledgment

Supports multiple devices on the bus (one master with multiple slaves, or multiple masters with multiple slaves)

One master with multiple slaves: A single microcontroller acts as the master, with one or more modules as slaves.

Multiple masters with multiple slaves: Multiple masters and slaves (but only one master can control the bus at any given time)

1.1 Hardware Circuit

All I2C devices connect their SCL lines together and their SDA lines together.

Device SCL and SDA must be configured in open-drain output mode

Add pull-up resistors to both SCL and SDA lines, typically around 4.7KΩ

The left-side CPU represents the microcontroller acting as the bus master. The master has significant control, including complete control over the SCL line at all times. In idle state, the master can initiate control of SDA, but only transfers SDA control to the slave during data transmission from the slave or when the slave sends an acknowledgment. The controlled ICs on the right are slaves connected to the I2C bus, which can be attitude sensors, OLEDs, memory, clock modules, etc. Slaves have limited rights: they can only passively read the SCL clock line and never control it. For the SDA data line, slaves cannot initiate control but can briefly take control only after the master sends a read command or during acknowledgment.

The master's SCL is output, which is fine. Both the master's and slave's SDA alternate between input and output.

The left diagram shows the SCL structure, and the right shows the SDA structure.

First, input signals pass through a data buffer or Schmitt trigger for input, as this has no effect on the circuit. For output, open-drain configuration is used (output low: transistor conducts, pin directly grounded - strong pull-down; output high: transistor off, pin floating - all devices can only output low, not high. To prevent floating pins at high voltage, external pull-up resistors are added to SCL and SDA outside the bus).

Benefits:

  1. Completely eliminates power short circuits, ensuring circuit safety
  2. Avoids frequent pin mode switching; open-drain with weak pull-up simultaneously provides input and output functionality. In open-drain mode, outputting high is equivalent to disconnecting the pin, so it can directly output high before switching to input mode
  3. This mode exhibits "wired-AND" behavior: if any device outputs low, the bus is low. This property is utilized for clock synchronization and bus arbitration in multi-master mode

1.2 Basic I2C Timing Units

(1) Start Condition: SDA transitions from high to low while SCL is high

(2) Stop Condition: SDA transitions from low to high while SCL is high

In idle I2C state, both SCL and SDA are high. During SCL high, SDA transitions from high to low, after which the master pulls SCL low - both to occupy the bus and to assemble these basic units (starts and ends with low).

Stop condition: SCL first returns high, then SDA returns high, with this rising edge triggering the stop condition.

Start and stop conditions are generated by the master. In idle bus state, slaves must release control and not主动 touch the bus (unless in multi-master mode).

(3) Sending one byte: During SCL low, the master places data bits sequentially on SDA (MSB first), then releases SCL (SCL becomes high), and the slave reads the data bits during SCL high. Therefore, SDA must not change during SCL high. Repeating this process 8 times sends one byte.

After starting, the first byte must be sent by the master. Initially SCL is low. To send 0, the master pulls SDA low; to send 1, it releases SDA (which returns high). During SCL low, SDA level can change. After placing the bit, the master releases SCL, which returns high - this is when the slave reads. The slave should read SDA as quickly as possible, typically at the rising edge. After the master releases SCL for some time, it can pull SCL low again to transmit the next bit. The master should place data on SDA as quickly as posible after the SCL falling edge. After data is placed, the master releases SCL again, SCL reaches high, and the slave reads. This process repeats: master pulls SCL low, places data on SDA, releases SCL, slave reads SDA, synchronized by SCL. MSB first, so the first bit is the highest bit B7 of the first byte. SCL and SDA are always controlled by the master; the slave can only passively read.

(4) Receiving one byte: During SCL low, the slave places data bits sequentially on SDA (MSB first), then releases SCL, and the master reads the data bits during SCL high. Therefore, SDA must not change during SCL high. Repeating this process 8 times receives one byte (the master must release SDA before receiving).

Releasing SDA is equivalent to switching to input mode, with all devices including the master in input mode. When the master needs to send, it can actively pull SDA low, but when passively receiving, it must first release SDA (due to the wired-AND bus characteristic).

Receiving one byte is very similar to sending one byte.

Difference: Sending one byte: master places data during low, slave reads during high; Receiving one byte: slave places data during low, master reads during high

The master must release SDA before receiving data, transferring SDA control to the slave. The slave pulls SDA low to send 0, or releases SDA (which returns high) to send 1. Similarly, data changes during low, and data is read during high. Solid lines represent master-controlled levels, dashed lines represent slave-controlled levels. SCL is always controlled by the master; the master releases SDA before receiving, allowing the slave to control SDA. The slave's data changes follow the SCL falling edge, while the master can read data at any SCL high moment - this is the timing for receiving one byte.

(5) Sending Acknowledgment (sending one bit): After receiving a byte, the master sends one bit in the next clock cycle: 0 for acknowledgment, 1 for non-acknowledgment.

After receiving a byte, an acknowledgment bit is sent to the slave to indicate whether to continue transmission. If the slave sends data and receives acknowledgment, it continues sending; if no acknowledgment, the slave assumes the master is no longer interested and releases SDA, preventing interference with the master's operations.

(6) Receiving Acknowledgment (receiving one bit): After sending a byte, the master receives one bit in the next clock cycle to determine if the slave acknowledged: 0 for acknowledgment, 1 for non-acknowledgment (the master must release SDA before receiving).

After calling send byte, immediately call receive acknowledgment to check if the slave received the data. If the slave received it, when the master releases SDA, the slave should immediately pull it low, and the master reads during SCL high to confirm. If the acknowledgment is 0, the slave indeed received the data. This scenario is: the master just sent data and asks if anyone received it. Now releasing SDA, if someone received it, they pull SDA down, and the master reads high to confirm someone pulled it down, meaning data was received. If after the master releases SDA, it returns high, no one responded, meaning no one received or received without responding.

1.3 I2C Timing Sequences

Slaves have unique device addresses (7-bit address). Example: MPU6050: 1101 000

(1) Write to Specific Address

For a specific device (Slave Address), write specified data (Data) to a specific address (Reg Address) within the device.

SCL and SDA are high. Start by pulling SDA low to generate the start condition. Immediately after, the sequence must be sending one byte - the content must be the slave address + read/write bit. The 7-bit slave address plus 1-bit read/write bit makes 8 bits (one byte). Sending the slave address identifies the communication target; the read/write bit determines whether to read or write next. Specifically: during low, SDA changes data; during high, the slave reads SDA. The green vertical lines indicate data read by the slave. The master is looking for address 1101 000 (MPU6050 address). The following 0 indicates the master will perform a write operation; 1 indicates a read operation. Currently the master is sending one byte - converting to hexadecimal with MSB first gives 0xD0. According to protocol, this is immediately followed by receiving the slave's acknowledgment (Receive ACK, RA). At this moment, the master must release SDA.

If we look only at the master's waveform, after releasing SDA, the pin level returns high (yellow line). However, according to protocol, the slave should pull SDA low at this time (green line). Combining both waveforms, after the master releases SDA, since the slave also pulls it, SDA does not return high - this process indicates the slave generated an acknowledgment. During the high period, the master reads SDA and finds it 0, meaning the addressing was acknowledged and transmission is fine. If the master reads SDA and finds it 1, no one acknowledged during addressing, and a stop condition is generated directly. The subsequent rising edge is generated when the slave releases SDA after acknowledgment - the slave releases SDA control because it needs to change data during low, so this rising edge almost coincides with the SCL falling edge. Continuing, the read/write bit was 0, so after acknowledgment, another byte is sent with the same timing. The second byte can now be sent to the specific device's internal register. The slave device can define the purpose of the second and subsequent bytes - typically the second byte can be a register address or instruction control word. Here 0x19 means operating the register at address 0x19.

Again, the slave acknowledges, the master releases SDA, the slave pulls SDA low, and the master receives acknowledgment 0, indicating the slave acknowledged. The same process repeats: the master sends enother byte - here indicating to write 0xAA at address 0x19. Finally, receive acknowledgment. If the master no longer needs to transmit, it can generate a stop condition: first pull SDA low to prepare for the subsequent SDA rising edge, then release SCL, then release SDA - generating the SDA rising edge during SCL high.

Summary: This data frame's purpose is to write 0xAA to register 0x19 of the device with slave address 1101 0000.

(2) Current Address Read

For a specific device (Slave Address), read slave data (Data) from the address indicated by the current address pointer.

If the master wants to read slave data, it can execute this sequence. Start with SCL high, pull SDA low to generate the start condition. After starting, the master must first call send one byte to address the slave and specify the read/write flag. The diagram shows the target is device 1101 000 with the last read/write flag as 1 (master reads data). Immediately after sending one byte, receive the slave's acknowledgment. The slave acknowledges 0, indicating it received the first byte. After slave acknowledgment, the data transmission direction reverses.

The master just sent a read command, so it cannot continue sending. It must transfer SDA control to the slave and call receive one byte for the receive operation. Subsequently, the slave, having received the master's permission, can write to SDA during SCL low, and the master reads SDA during SCL high. Finally, during SCL high, the master sequentially reads 8 bits to receive one byte of data from the slave: 0000 1111, or 0x0F. No address specification is needed here - the current address pointer is used. In the slave, all registers are allocated in a linear area with a separate pointer variable indicating one register (typically defaulting to address 0). After each write or read, this pointer auto-increments. If the master doesn't specify an address, the slave returns the value of the register pointed to by the current pointer.

(3) Read from Specific Address

For a specific device (Slave Address), read slave data (Data) from a specific address (Reg Address).

The first part is the specific address write sequence. Removing the data writing part and appending the address specification (without data writing) to the current address read sequence yields the specific address read sequence (composite format).

The first part is specific address write - only specifying the address without writing data; the second part is current address read. Combined, they form specific address read.

The first part remains the start condition, then sending one byte for addressing with the read/write flag 0 (write operation). After slave acknowledgment, sending one byte specifies the address, which writes to the slave's address pointer. That is, after the slave receives this data, its register pointer points to address 0x19. Then, instead of sending data to write, a start condition (start repeat) is generated, re-addressing with the read flag 1 (read operation). The master then receives one byte - this byte is the data at address 0x19.

The advanced version supports reading/writing multiple bytes.

1.4 MPU6050 Introduction

MPU6050 is a 6-axis attitude sensor that measures the chip's own X, Y, Z-axis acceleration and angular velocity parameters. Through data fusion, it can further obtain attitude angles, commonly used in scenarios like balance cars and drones that require self-attitude detection.

3-axis accelerometer: Measures X, Y, Z-axis acceleration. Has static stability but no dynamic stability

3-axis gyroscope: Measures X, Y, Z-axis angular velocity. Has dynamic stability but no static stability

6-axis = 3-axis accelerometer + 3-axis gyroscope

9-axis = 3-axis accelerometer + 3-axis gyroscope + 3-axis magnetic field strength

10-axis = 3-axis accelerometer + 3-axis gyroscope + 3-axis magnetic field strength + 1 atmospheric pressure sensor

1.4.1 MPU6050 Parameters

16-bit ADC collects sensor analog signals, quantization range: -32768~32767

Accelerometer full-scale selection: ±2, ±4, ±8, ±16 (g)

Gyroscope full-scale selection: ±250, ±500, ±1000, ±2000 (°/sec)

Configurable digital low-pass filter

Configurable clock source

Configurable sampling divider

I2C slave address: 110 1000 (AD0=0) (0x68 needs left shift to incorporate read/write bit: 0xD0 write address, 0xD1 read address)

110 1001 (AD0=1)

1.4.2 Hardware Circuit

The left is the MPU6050 chip, bottom-left is the 8-pin array; top-left is the LDO low-dropout linear regulator.

XCL and XDA are for expansion;

AD0 connected low means 7-bit slave address is 1101 000; connected high means 7-bit slave address is 1101 001 (weak pull-down)

This MPU6050 chip's VDD supply is 2.375-3.46V; with the regulator, it can operate at 3.3~5V

Pin Function
VCC, GND Power supply
SCL, SDA I2C communication pins
XCL, XDA Host I2C communication pins
AD0 Slave address LSB
INT Interrupt signal output
1.4.3 MPU6050 Block Diagram

Top-left is the clock system, with clock input and output pins (internal clock is typically used). Gray areas are sensors.

Self-test: Self-test response.

Manual

Product specification

Register map

Sampling rate divider, configuration register, gyroscope configuration register, accelerometer configuration register

These are: Accelerometer XYZ axes, temperature sensor, gyroscope XYZ axes. _L indicates low 8 bits; _H indicates high 8 bits

These are: Power management register 1/2, device ID number

Sampling rate divider: Smaller divider means faster internal ADC conversion and faster data register refresh; larger divider means slower.

Configuration register

Gyroscope configuration register: High 3 bits are XYZ self-test enable bits, middle 2 bits are full-scale bits

Self-test response range

Accelerometer configuration register: High 3 bits are XYZ self-test enable bits, middle 2 bits are full-scale bits, last 3 bits configure high-pass filter

Accelerometer data registers can be read directly. Read high 8 bits and low 8 bits, shift high 8 bits left 8 or OR with low 8 bits.

Device ID number

This chip defaults to sleep mode on power-up.

  1. Software I2C Read/Write to MPU6050

Implement I2C protocol timing and control MPU6050 by reading/writing registers based on I2C protocol. The module has built-in pull-up resistors.

2.1 Wiring Diagram

Currently STM32 is the master, MPU6050 is the slave. AD0 modifies the slave address LSB (for debugging in code).

Framework: First establish I2C communication layer .c and .h modules. In the communication layer, implement I2C GPIO initialization and 6 basic timing units (start, stop, send byte, receive byte, send ack, receive ack). Then establish MPU6050 .c and .h modules. Based on the I2C communication module, implement specific address read and write. Then implement writing registers to configure the chip and reading registers to get sensor data. Finally, in main.c, call the MPU6050 module to initialize and obtain data for display.

2.2 Module Encapsulation

2.2.1 CustomI2C

(1) I2C Initialization Function

// I2C Initialization
void CustomI2C_Init(void)
{
    // Software I2C only needs GPIO read/write, no library functions needed
    // 1. Initialize SCL and SDA as open-drain output mode
    // 2. Set SCL and SDA to high level
    /* Enable clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);  // Enable GPIOA clock
    
    /* GPIO Initialization */
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;     // Initialize PA1 and PA2 as open-drain output (output low + floating input)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);              
    
    /* Set default level after GPIO initialization */
    GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);       // Set PA1 and PA2 to high level
}

(2) Encapsulate SCL and SDA read/write functions

// Encapsulated SCL write function
void CustomI2C_WriteSCL(uint8_t bitValue)
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)bitValue);
    Delay_us(10);
}

// Encapsulated SDA write function
void CustomI2C_WriteSDA(uint8_t bitValue)
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)bitValue);
    Delay_us(10);
}

// Encapsulated SDA read function
uint8_t CustomI2C_ReadSDA(void)
{
    uint8_t bitValue;
    bitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
    Delay_us(10);
    return bitValue;
}

(3) Start Condition

// Start condition
void CustomI2C_Start(void)
{
    // Ensure SCL and SDA are released, pull SDA low first, then SCL
    CustomI2C_WriteSDA(1);      // Release SDA (first)
    CustomI2C_WriteSCL(1);      // Release SCL
    CustomI2C_WriteSDA(0);      // Pull SDA low first
    CustomI2C_WriteSCL(0);      // Then pull SCL low
}

(4) Stop Condition

// Stop condition
void CustomI2C_Stop(void)
{
    // Pull SDA low first, then release SCL, then release SDA
    CustomI2C_WriteSDA(0);      // Pull SDA low first
    CustomI2C_WriteSCL(1);      // Release SCL
    CustomI2C_WriteSDA(1);      // Release SDA
}

(5) Send One Byte

// Send one byte
void CustomI2C_SendByte(uint8_t byte)
{
    uint8_t i = 0;
    for (i = 0; i < 8; i++)
    {
        CustomI2C_WriteSDA(byte & (0x80 >> i));  // Depends on i-th high bit of byte
        CustomI2C_WriteSCL(1);                  // Release SCL
        CustomI2C_WriteSCL(0);                  // Pull SCL low
    }
}

(6) Receive One Byte

// Receive one byte
uint8_t CustomI2C_ReceiveByte(void)
{
    uint8_t byte = 0x00, i = 0;
    CustomI2C_WriteSDA(1);                      // Release SDA
    
    for (i = 0; i < 8; i++)
    {
        CustomI2C_WriteSCL(1);                  // Release SCL
        // Read data
        if (CustomI2C_ReadSDA() == 1)
        {
            byte |= (0x80 >> i);                // Set high bit
        }
        CustomI2C_WriteSCL(0);                  // Pull SCL low
    }
    return byte;
}

(7) Send Acknowledgment

// Send acknowledgment (simplified send byte)
void CustomI2C_SendAck(uint8_t ackByte)
{
    CustomI2C_WriteSDA(ackByte);                // Depends on ackByte
    CustomI2C_WriteSCL(1);                      // Release SCL
    CustomI2C_WriteSCL(0);                      // Pull SCL low
}

(8) Receive Acknowledgment

// Receive acknowledgment (simplified receive byte)
uint8_t CustomI2C_ReceiveAck(void)
{
    uint8_t ackByte;
    CustomI2C_WriteSDA(1);                      // Release SDA
    CustomI2C_WriteSCL(1);                      // Release SCL
    ackByte = CustomI2C_ReadSDA();              // Read data
    CustomI2C_WriteSCL(0);                      // Pull SCL low
    return ackByte;
}

Final CustomI2C.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

// Encapsulated SCL write function
void CustomI2C_WriteSCL(uint8_t bitValue)
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)bitValue);
    Delay_us(10);
}

// Encapsulated SDA write function
void CustomI2C_WriteSDA(uint8_t bitValue)
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)bitValue);
    Delay_us(10);
}

// Encapsulated SDA read function
uint8_t CustomI2C_ReadSDA(void)
{
    uint8_t bitValue;
    bitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
    Delay_us(10);
    return bitValue;
}

// I2C Initialization
void CustomI2C_Init(void)
{
    // Software I2C only needs GPIO read/write, no library functions needed
    // 1. Initialize SCL and SDA as open-drain output mode
    // 2. Set SCL and SDA to high level
    /* Enable clock */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);  // Enable GPIOA clock
    
    /* GPIO Initialization */
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;     // Initialize PA1 and PA2 as open-drain output (output low + floating input)
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);              
    
    /* Set default level after GPIO initialization */
    GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);       // Set PA1 and PA2 to high level
}

// Start condition
void CustomI2C_Start(void)
{
    // Ensure SCL and SDA are released, pull SDA low first, then SCL
    CustomI2C_WriteSDA(1);      // Release SDA (first)
    CustomI2C_WriteSCL(1);      // Release SCL
    CustomI2C_WriteSDA(0);      // Pull SDA low first
    CustomI2C_WriteSCL(0);      // Then pull SCL low
}

// Stop condition
void CustomI2C_Stop(void)
{
    // Pull SDA low first, then release SCL, then release SDA
    CustomI2C_WriteSDA(0);      // Pull SDA low first
    CustomI2C_WriteSCL(1);      // Release SCL
    CustomI2C_WriteSDA(1);      // Release SDA
}

// Send one byte
void CustomI2C_SendByte(uint8_t byte)
{
    uint8_t i = 0;
    for (i = 0; i < 8; i++)
    {
        CustomI2C_WriteSDA(byte & (0x80 >> i));  // Depends on i-th high bit of byte
        CustomI2C_WriteSCL(1);                  // Release SCL
        CustomI2C_WriteSCL(0);                  // Pull SCL low
    }
}

// Receive one byte
uint8_t CustomI2C_ReceiveByte(void)
{
    uint8_t byte = 0x00, i = 0;
    CustomI2C_WriteSDA(1);                      // Release SDA
    
    for (i = 0; i < 8; i++)
    {
        CustomI2C_WriteSCL(1);                  // Release SCL
        // Read data
        if (CustomI2C_ReadSDA() == 1)
        {
            byte |= (0x80 >> i);                // Set high bit
        }
        CustomI2C_WriteSCL(0);                  // Pull SCL low
    }
    return byte;
}

// Send acknowledgment (simplified send byte)
void CustomI2C_SendAck(uint8_t ackByte)
{
    CustomI2C_WriteSDA(ackByte);                // Depends on ackByte
    CustomI2C_WriteSCL(1);                      // Release SCL
    CustomI2C_WriteSCL(0);                      // Pull SCL low
}

// Receive acknowledgment (simplified receive byte)
uint8_t CustomI2C_ReceiveAck(void)
{
    uint8_t ackByte;
    CustomI2C_WriteSDA(1);                      // Release SDA
    CustomI2C_WriteSCL(1);                      // Release SCL
    ackByte = CustomI2C_ReadSDA();              // Read data
    CustomI2C_WriteSCL(0);                      // Pull SCL low
    return ackByte;
}

Test if slave provides acknowledgment

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "CustomI2C.h"

int main()
{
    OLED_Init();                              // Initialize OLED
    // Test if slave provides acknowledgment
    CustomI2C_Init();
    // Specific address write
    CustomI2C_Start();                        // Start condition
    
    // Test if any slave on bus
    CustomI2C_SendByte(0xD0);                 // 1101 000 0  Addressing
    uint8_t ack = CustomI2C_ReceiveAck();      // 000/001
    OLED_ShowHexNum(1, 1, ack, 3);
    
    // Test AD0 pin name change
    // Wire AD0 to VCC (1101 0010), 0xD0 won't acknowledge (001)
    CustomI2C_SendByte(0xD0);
    ack = CustomI2C_ReceiveAck();
    OLED_ShowHexNum(1, 1, ack, 3);
    while (1)
    {
        
    }
}

2.2.2 MPU6050

This module is built on CustomI2C

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "CustomI2C.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS  0xD0

// Write to specific address
// Parameters are 8-bit address and 8-bit data
void MPU6050_WriteReg(uint8_t regAddress, uint8_t data)
{
    CustomI2C_Start();                          // I2C start
    CustomI2C_SendByte(MPU6050_ADDRESS);         // Send MPU6050 address
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_SendByte(regAddress);             // Send specified register address
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_SendByte(data);                   // Specified data to write to register
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_Stop();                           // Stop
}

// Read from specific address
uint8_t MPU6050_ReadReg(uint8_t regAddress)
{
    uint8_t data;
    
    CustomI2C_Start();                          // I2C start
    CustomI2C_SendByte(MPU6050_ADDRESS);         // Send MPU6050 address
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_SendByte(regAddress);             // Send specified register address
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_Start();                          // I2C start
    CustomI2C_SendByte(MPU6050_ADDRESS | 0x01);  // Read data 1101 0000 write/1101 0001 read
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    data = CustomI2C_ReceiveByte();              // Slave takes bus control, starts sending one byte
    CustomI2C_SendAck(0);                       // Send acknowledgment
    
    CustomI2C_Stop();                           // Stop
    
    return data;
}

// Initialize MPU6050
void MPU6050_Init(void)
{
    CustomI2C_Init();                           // Initialize I2C
}

Small test

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"

int main()
{
    OLED_Init();                              // Initialize OLED
    MPU6050_Init();
    
    // Read chip ID
    uint8_t id = MPU6050_ReadReg(0x75);         // Address is 0x75
    OLED_ShowHexNum(1, 1, id, 2);              // ID 0x68
    
    // Write register to exit sleep mode
    MPU6050_WriteReg(0x6B, 0x00);              // Power management register 1
    
    MPU6050_WriteReg(0x19, 0xAA);              // Sample rate divider register, address 0x19, value 0xAA
    id = MPU6050_ReadReg(0x19);
    OLED_ShowHexNum(2, 1, id, 2);              // 0xAA
    
    while (1)
    {
        
    }
}

Macro define register addresses

#ifndef __MPU6050_REG_H__
#define __MPU6050_REG_H__

#define MPU6050_SMPLRT_DIV    0x19
#define MPU6050_CONFIG       0x1A
#define MPU6050_GYRO_CONFIG  0x1B
#define MPU6050_ACCEL_CONFIG 0x1C

#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H   0x41
#define MPU6050_TEMP_OUT_L   0x42
#define MPU6050_GYRO_XOUT_H  0x43
#define MPU6050_GYRO_XOUT_L  0x44
#define MPU6050_GYRO_YOUT_H  0x45
#define MPU6050_GYRO_YOUT_L  0x46
#define MPU6050_GYRO_ZOUT_H  0x47
#define MPU6050_GYRO_ZOUT_L  0x48

#define MPU6050_PWR_MGMT_1   0x6B
#define MPU6050_PWR_MGMT_2   0x6C
#define MPU6050_WHO_AM_I     0x75

#endif

Continue improving MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "CustomI2C.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS  0xD0

// Write to specific address
// Parameters are 8-bit address and 8-bit data
void MPU6050_WriteReg(uint8_t regAddress, uint8_t data)
{
    CustomI2C_Start();                          // I2C start
    CustomI2C_SendByte(MPU6050_ADDRESS);         // Send MPU6050 address
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_SendByte(regAddress);             // Send specified register address
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_SendByte(data);                   // Specified data to write to register
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_Stop();                           // Stop
}

// Read from specific address
uint8_t MPU6050_ReadReg(uint8_t regAddress)
{
    uint8_t data;
    
    CustomI2C_Start();                          // I2C start
    CustomI2C_SendByte(MPU6050_ADDRESS);         // Send MPU6050 address
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_SendByte(regAddress);             // Send specified register address
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    CustomI2C_Start();                          // I2C start
    CustomI2C_SendByte(MPU6050_ADDRESS | 0x01);  // Read data 1101 0000 write/1101 0001 read
    CustomI2C_ReceiveAck();                     // Receive acknowledgment
    
    data = CustomI2C_ReceiveByte();              // Slave takes bus control, starts sending one byte
    CustomI2C_SendAck(1);                       // Send acknowledgment
    // Send non-acknowledgment for last byte, send acknowledgment for previous bytes
    
    CustomI2C_Stop();                           // Stop
    return data;
}

// Initialize MPU6050
// Configuration: Exit sleep mode, select gyroscope clock, no standby for 6 axes, sampling divider 10, maximum filter parameters
// Gyroscope and accelerometer select maximum range
void MPU6050_Init(void)
{
    CustomI2C_Init();                                                   // Initialize I2C
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);                          // Configure power management register 1
    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);                          // Configure power management register 2
    
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);                          // Sample rate divider register, 10 divider
    MPU6050_WriteReg(MPU6050_CONFIG, 0x06);                              // Configuration register
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);                         // Gyroscope configuration register
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);                        // Accelerometer register   
}

// Get data registers, return 6 data, parameter is pointer
void MPU6050_GetData(int16_t* accX, int16_t* accY, int16_t* accZ, 
                    int16_t* GyroX, int16_t* GyroY, int16_t* GyroZ)
{
    uint8_t dataH, dataL;
    dataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);                        // Read accelerometer X-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);                        // Read accelerometer X-axis low 8 bits
    *accX = (dataH << 8) | dataL;                                       // Return via pointer
    dataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);                        // Read accelerometer Y-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);                        // Read accelerometer Y-axis low 8 bits
    *accY = (dataH << 8) | dataL;                                       // Return via pointer
    dataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);                        // Read accelerometer Z-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);                        // Read accelerometer Z-axis low 8 bits
    *accZ = (dataH << 8) | dataL;                                       // Return via pointer
    
    
    dataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);                         // Read gyroscope X-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);                         // Read gyroscope X-axis low 8 bits
    *GyroX = (dataH << 8) | dataL;                                      // Return via pointer
    dataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);                         // Read gyroscope Y-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);                         // Read gyroscope Y-axis low 8 bits
    *GyroY = (dataH << 8) | dataL;                                      // Return via pointer
    dataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);                         // Read gyroscope Z-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);                         // Read gyroscope Z-axis low 8 bits
    *GyroZ = (dataH << 8) | dataL;                                      // Return via pointer
}

// Get ID number
uint8_t MPU6050_GetID(void)
{
    return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

2.3 Main Function

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"

int16_t AX, AY, AZ, GX, GY, GZ;

int main()
{
    OLED_Init();                              // Initialize OLED
    MPU6050_Init();
    OLED_ShowString(1, 1, "ID:");
    OLED_ShowHexNum(1, 4, MPU6050_GetID(), 6);
    
    while (1)
    {
        MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
        OLED_ShowSignedNum(2, 1, AX, 5);
        OLED_ShowSignedNum(3, 1, AY, 5);
        OLED_ShowSignedNum(4, 1, AZ, 5);
        OLED_ShowSignedNum(2, 8, GX, 5);
        OLED_ShowSignedNum(3, 8, GY, 5);
        OLED_ShowSignedNum(4, 8, GZ, 5);
    }
}

Phenomenon:

x / 32768 = x / full scale

x = 1913/32768*16 = 0.93408203125g/m². Gravity acceleration. Other data is calculated similarly.

  1. I2C Communication Peripheral

Simple applications use software I2C; for performance requirements, use hardware I2C

3.1 I2C Peripheral Introduction

STM32 integrates hardware I2C transceiver circuits that can automatically execute clock generation, start/stop condition generation, acknowledgment bit transmission/reception, and data transmission/reception, reducing CPU load

Supports multi-master model (STM32 uses variable multi-master: any device can become master, equal treatment)

Supports 7-bit/10-bit address modes

7-bit address mode: The first byte after start must be 7-bit address + read/write bit. Only 128 addresses, limited quantity.

10-bit address: The first two bytes after start are both addressing. The first byte has 7 empty bits, the second has 8 empty bits, totaling 15 empty bits, with 5 extra bits as flags (11110). If the second byte is also addressing, the first 5 bits of the first byte must be 11110, and the remaining 2 bits of the first byte + 8 bits of the second byte serve as addressing, making 10-bit addressing.

Supports different communication speeds: standard speed (up to 100 kHz), fast (up to 400 kHz)

Supports DMA

Compatible with SMBus protocol (system management bus, mainly used in power management)

STM32F103C8T6 Hardware I2C Resources: I2C1, I2C2

3.1.1 I2C Peripheral Block Diagram

The coordinates are the communication pins SDA and SCL of this peripheral. The SMBALERT below is for SMBus use. These pins typically connect to the outside world through GPIO alternate mode.

Looking at the I2C function block diagram:

The upper part is data control, with data register and data shift register as core. When sending data, a byte can be written to the data register DR. When the shift register has no data to shift, the data register value transfers to the shift register. During shifting, the next data can be placed directly in the data register. Once the previous data finishes shifting, the next data can seamlessly continue transmission. When data transfers from data register to shift register, the status register TXE bit is set to 1, indicating the transmit register is empty - this is the transmission process. For reception, the same path: input data shifts from the pin into the shift register one bit at a time. When a full byte is received, data transfers from shift register to data register, and the RXNE flag is set, indicating the receive register is not empty, allowing data to be read from the data register. (Unlike UART which is full-duplex, this is half-duplex). Write corresponding bits in control register to determine when to read/write.

When STM32 acts as slave: It's summoned by the master and needs its own address.

The lower part is SCL clock control.

3.1.2 Basic I2C Model Diagram

First, the cooperation between shift register and data register is the core of communication. I2C is MSB first, so this shift register shifts left. One SCL clock shifts once, 8 shifts complete one byte. For reception, data shifts in from the right through GPIO, 8 shifts complete one byte reception. GPPIO needs to be configured as alternate open-drain output (alternate means GPIO state is controlled by on-chip peripheral; open-drain is the port configuration mode required by I2C protocol).

3.1.3 Master Transmission

When STM32 wants to perform specific address write, it follows this diagram. There are 7-bit and 10-bit address master transmissions. Difference: 7-bit address has one addressing byte after start; 10-bit has two addressing bytes (where the first byte's first 5 bits are flags 11110 + 2 address bits + 1 read/write bit, and the second byte is pure 8-bit address). Together they form 10-bit addressing. Mainly focus on 7-bit.

7-bit address: Process is start, slave address, acknowledgment, then data1, acknowledgment, data2, acknowledgment, ..., dataN, acknowledgment, stop. (MPU6050 specifies data1 is the specified register address, data2 is the data at that register address).

This is a typical specific address write timing sequence. After initialization, the bus is idle by default, STM32 defaults to slave mode. To generate a start condition, STM32 writes to the control register (check manual);

Then STM32 switches from slave to master mode (in multi-master, STM32 has data to send and jumps out). After controlling the hardware circuit, check flags to see if the hardware reached the desired state. Use EV5 event as the flag;

When detecting the start condition is sent, a byte of slave address can be sent. The slave address needs to be written to data register DR. After writing to DR, the hardware automatically transfers this byte to the shift register and sends it to the I2C bus. The hardware then automatically receives and checks the acknowledgment bit. If no acknowledgment, it sets an acknowledgment failure flag, which can trigger an interrupt. After addressing, EV6 event occurs, EV8_1 event,

3.1.4 Master Reception

Current address read format

3.1.5 Software/Hardware Waveform Comparison

  1. Hardware I2C Read/Write to MPU6050

4.1 Wiring Diagram

Same as software I2C read/write to MPU6050

4.2 Module Encapsulation

1. Configure I2C peripheral, initialize I2C2 peripheral

(1) Enable I2C peripheral and corresponding GPIO clock

(2) Initialize I2C GPIO to alternate open-drain mode

(3) Configure entire I2C using structure

(4) I2C_Cmd, enable I2C

2. Control peripheral circuit, implement specific address write timing

3. Control peripheral circuit, implement specific address read timing

Library functions

void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

// I2C generate start/stop condition
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
// Configure whether to acknowledge slave after receiving a byte
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
// Send data, write to data register DR
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
// Receive data, read data from DR register
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
// Send 7-bit address
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
// Status monitoring
I2C_CheckEvent();

MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS  0xD0

// Encapsulate I2C_CheckEvent function with timeout to prevent deadlock
void MPU6050_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
    uint32_t timeOut = 10000;
    while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
    {
        timeOut--;
        if (timeOut == 0)
        {
            break;
        }
    }
}

// Write to specific address
// Parameters are 8-bit address and 8-bit data
void MPU6050_WriteReg(uint8_t regAddress, uint8_t data)
{
    // 2. Control peripheral circuit, implement specific address write timing
    I2C_GenerateSTART(I2C2, ENABLE);                  // Generate start condition
    // Wait for EV5 event
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
    
    // Send slave address, with automatic acknowledgment reception
    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    // Wait for EV6 event
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
    
    I2C_SendData(I2C2, regAddress);                   // Directly write to DR, send data
    // Wait for EV8 event
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);
    
    I2C_SendData(I2C2, data);                        // Directly write to DR, send data
    // Wait for EV8 during continuous transmission, EV8_2 for last bit
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
    
    I2C_GenerateSTOP(I2C2, ENABLE);                   // Generate stop condition
}

// Read from specific address
uint8_t MPU6050_ReadReg(uint8_t regAddress)
{
    // 3. Control peripheral circuit, implement specific address read timing
    I2C_GenerateSTART(I2C2, ENABLE);                  // Generate start condition
    // Wait for EV5 event
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
    
    // Send slave address, with automatic acknowledgment reception
    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    // Wait for EV6 event
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
    
    I2C_SendData(I2C2, regAddress);                   // Directly write to DR, send data
    // Wait for EV8 event
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
    
    I2C_GenerateSTART(I2C2, ENABLE);                  // Generate start condition
    // Wait for EV5 event
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
    
    // Receive data also has automatic acknowledgment sending
    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
    // Wait for EV6 event
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
    
    I2C_AcknowledgeConfig(I2C2, DISABLE);              // Configure ACK bit
    I2C_GenerateSTOP(I2C2, ENABLE);                   // Generate stop condition
    
    // Wait for EV7 event
    MPU6050_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);
    uint8_t data = I2C_ReceiveData(I2C2);
    
    I2C_AcknowledgeConfig(I2C2, ENABLE);              // Configure ACK bit
    return data;
}

// Initialize MPU6050
// Configuration: Exit sleep mode, select gyroscope clock, no standby for 6 axes, sampling divider 10, maximum filter parameters
// Gyroscope and accelerometer select maximum range
void MPU6050_Init(void)
{
    // 1. Configure I2C peripheral, initialize I2C2 peripheral
    // (1) Enable I2C peripheral and corresponding GPIO clock
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    
    // (2) Initialize I2C GPIO to alternate open-drain mode
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);    
    
    // (3) Configure entire I2C using structure
    I2C_InitTypeDef I2C_InitStructure;
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;                         // I2C mode
    I2C_InitStructure.I2C_ClockSpeed = 50000;                          // Clock speed
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;                  // Clock duty cycle, 1:1 for speeds below 100KHz
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;                        // ACK bit
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;  // 7-bit address
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;                          // Own address, not used
    I2C_Init(I2C2, &I2C_InitStructure);
    // (4) I2C_Cmd, enable I2C
    I2C_Cmd(I2C2, ENABLE);
    
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);                          // Configure power management register 1
    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);                          // Configure power management register 2
    
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);                         // Sample rate divider register, 10 divider
    MPU6050_WriteReg(MPU6050_CONFIG, 0x06);                             // Configuration register
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);                        // Gyroscope configuration register
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);                       // Accelerometer register   
}

// Get data registers, return 6 data, parameter is pointer
void MPU6050_GetData(int16_t* accX, int16_t* accY, int16_t* accZ, 
                    int16_t* GyroX, int16_t* GyroY, int16_t* GyroZ)
{
    uint8_t dataH, dataL;
    dataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);                        // Read accelerometer X-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);                        // Read accelerometer X-axis low 8 bits
    *accX = (dataH << 8) | dataL;                                       // Return via pointer
    dataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);                        // Read accelerometer Y-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);                        // Read accelerometer Y-axis low 8 bits
    *accY = (dataH << 8) | dataL;                                       // Return via pointer
    dataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);                        // Read accelerometer Z-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);                        // Read accelerometer Z-axis low 8 bits
    *accZ = (dataH << 8) | dataL;                                       // Return via pointer
    
    
    dataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);                         // Read gyroscope X-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);                         // Read gyroscope X-axis low 8 bits
    *GyroX = (dataH << 8) | dataL;                                      // Return via pointer
    dataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);                         // Read gyroscope Y-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);                         // Read gyroscope Y-axis low 8 bits
    *GyroY = (dataH << 8) | dataL;                                      // Return via pointer
    dataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);                         // Read gyroscope Z-axis high 8 bits
    dataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);                         // Read gyroscope Z-axis low 8 bits
    *GyroZ = (dataH << 8) | dataL;                                      // Return via pointer
}

// Get ID number
uint8_t MPU6050_GetID(void)
{
    return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

4.3 Main Function

Same as 2.3, phenomenon also same as 2.3

Tags: STM32 I2C MPU6050 Embedded Systems sensor interfacing

Posted on Thu, 24 Sep 2026 16:52:36 +0000 by Graphi