Skip to main content
Visitor
July 29, 2026
Question

Bare-metal I2C1 on STM32F411 — WHO_AM_I returns 0x98, not 0x68 (GY-521/MPU6050)

  • July 29, 2026
  • 3 replies
  • 78 views

Hi all,

I'm working through a self-directed embedded systems curriculum, writing everything at the register level (no HAL, no CubeMX-generated I2C code) to actually understand what's happening under the hood. Posting this in case it saves someone else the same debugging loop, and to sanity-check my conclusion.

Setup:

  • Board: Nucleo-F411RE
  • Sensor: GY-521 breakout, sold as "MPU6050"
  • I2C1 on PB8 (SCL) / PB9 (SDA), AF4, open-drain, onboard 4.7k pull-ups
  • Standard mode, 100 kHz (CCR = 80, TRISE = 17, APB1 = 16 MHz)
  • Pure register access via a self-built I2C_TypeDef-style struct — no ST HAL calls anywhere

Transaction sequence (reading WHO_AM_I, register 0x75):

  1. START, wait SB
  2. Send (0x68<<1)|0, wait ADDR (confirmed ACK)
  3. Clear ADDR (SR1 then SR2 read)
  4. Send 0x75, wait TXE and BTF (important — TXE alone isn't enough before issuing a repeated START; BTF confirms the byte and its ACK are fully clocked out)
  5. Repeated START, wait SB
  6. Send (0x68<<1)|1, wait ADDR (confirmed ACK again)
  7. Clear ACK bit (CR1 bit 10) before clearing ADDR — required ordering for a single-byte read
  8. Clear ADDR, set STOP
  9. Wait RXNE, read DR

What I've verified with the debugger:

  • Both address phases (write and read) get ACKed — no NACK, no bus hang, ever.
  • Breakpointing right before the register-select write confirms the CPU register holding the value is exactly 0x75 — the correct byte is being placed in DR.
  • The transaction completes cleanly every single run.

The result: DR consistently returns 0x98, not 0x68. Every run, same value — not noise, not an intermittent timing artifact.

What I've found so far: it looks like this specific GY-521 board carries a die that isn't the original InvenSense MPU-6050 — likely an ICM-20689 (or a clone claiming to be one), which is pin- and largely protocol-compatible but reports a different WHO_AM_I value at the same 0x75 offset. Several other people report the exact same 0x98 value on GY-521 boards bought recently, which lines up.

My question for the group: for anyone who's hit this — does the rest of the standard MPU6050 register map (PWR_MGMT_1 default 0x40, ACCEL_CONFIG/GYRO_CONFIG, and the ACCEL/GYRO output registers starting at 0x3B) behave identically on these ICM-20689-based clones, or are there other offsets I should watch for before I build the rest of my accel/gyro read pipeline on top of this? Trying to figure out if it's safe to treat this as a drop-in MPU6050 for everything except the WHO_AM_I check, or if there are other landmines waiting.

Thanks in advance — happy to share the full register-level driver if useful to anyone else doing this without HAL.

#include <stdint.h>

typedef struct {
volatile uint32_t CR1; // (Control Register 1 - on/off, start, stop)
volatile uint32_t CR2; // Tells the peripheral what frequency its input clock is running at (in MHz)
volatile uint32_t OAR1; // (Own Address Register 1 - slave address)
volatile uint32_t OAR2; // (Own Address Register 2 - general call address)
volatile uint32_t DR; // The mailbox. Write a byte here → it gets sent on the bus. Read from it → you get the byte the sensor sent back.
volatile uint32_t SR1; // The flags register — your window into what's happening. You poll bits here to know when each step of the conversation is done
volatile uint32_t SR2; // (Status Register 2)
volatile uint32_t CCR; // Sets the bus speed — how fast SCL ticks. You put a divisor here that determines whether you run at 100 kHz (standard) or 400 kHz (fast). Similar idea to UART's BRR.
volatile uint32_t TRISE; // A fine-tuning register for signal timing (the maximum "rise time" of the bus signals).
} I2C_Msg;

typedef struct {
volatile uint32_t MODER; // (GPIO port mode register)
volatile uint32_t OTYPER; // (GPIO port output type register)
volatile uint32_t OSPEEDR; // (GPIO port output speed register)
volatile uint32_t PUPDR; // (GPIO port pull-up/pull-down register)
volatile uint32_t IDR; // (GPIO port input data register)
volatile uint32_t ODR; // (GPIO port output data register)
volatile uint32_t BSRR; // (GPIO port bit set/reset register)
volatile uint32_t LCKR; // (GPIO port configuration lock register)
volatile uint32_t AFRL; // (GPIO alternate function low register)
volatile uint32_t AFRH; // (GPIO alternate function high register)

} GPIO_Msg;
typedef struct {
volatile uint32_t CR; // (Clock control register)
volatile uint32_t PLLCFGR; // (PLL configuration register)
volatile uint32_t CFGR; // (Clock configuration register)
volatile uint32_t CIR; // (Clock interrupt register)
volatile uint32_t AHB1RSTR; // (AHB1 peripheral reset register)
volatile uint32_t AHB2RSTR; // (AHB2 peripheral reset register)
volatile uint32_t AHB3RSTR; // (AHB3 peripheral reset register)
volatile uint32_t RESERVED0; // Reserved
volatile uint32_t APB1RSTR; // (APB1 peripheral reset register)
volatile uint32_t APB2RSTR; // (APB2 peripheral reset register)
volatile uint32_t RESERVED1[2]; // Reserved
volatile uint32_t AHB1ENR; // (AHB1 peripheral clock enable register)
volatile uint32_t AHB2ENR; // (AHB2 peripheral clock enable register)
volatile uint32_t AHB3ENR; // (AHB3 peripheral clock enable register)
volatile uint32_t RESERVED2; // Reserved
volatile uint32_t APB1ENR; // (APB1 peripheral clock enable register)
volatile uint32_t APB2ENR; // (APB2 peripheral clock enable register)
} RCC_Msg;

#define I2C1 ((volatile I2C_Msg *) 0x40005400) // Base address of I2C peripheral)
#define GPIOB ((volatile GPIO_Msg *) 0x40020400) // Base address of GPIOB peripheral)
#define RCC ((volatile RCC_Msg *) 0x40023800) // Base address of RCC peripheral)
uint8_t i2c_read_register(uint8_t device_addr, uint8_t reg);
int main(void){
RCC->AHB1ENR |= (1<<1); // Enable GPIOB clock
RCC->APB1ENR |= (1<<21); // Enable I2C1 clock
GPIOB->MODER &= ~((3<<16) | (3<<18)); // Clear mode bits for PB8&PB9
GPIOB->MODER |= (2<<16) | (2<<18); // Set mode bits for PB8&PB9 as alternate function
GPIOB->AFRH &= ~((0xF<<0) | (0xF<<4)); // Clear alternate function bits for PB8&PB9
GPIOB->AFRH |= (4<<0) | (4<<4); // Set alternate function bits for PB8&PB9 as AF4 (I2C1)
GPIOB->OTYPER |= (1<<8) | (1<<9); // shared bus, pins can only pull low so devices don't fight
I2C1->CR2 = 16; // Set clock freq. to the clock of the MCU (16 MHz)
// Formula for CCR: CCR = APB1_clock / (2 × desired_SCL_speed)
I2C1->CCR = 80; // Set clock control register for 100 kHz SCL speed (STandard mode)
// Formula for TRISE: TRISE = (APB1_clock_in_MHz) + 1
I2C1->TRISE = 17; // Set maximum rise time for standard mode
/* Because I²C pins are open-drain,
the line doesn't snap HIGH instantly
— the pull-up resistor drags it up,
which takes a little time (the "rise time").
The peripheral needs to know the maximum rise time so its timing stays within the I²C spec.
TRISE tells it that limit.*/
I2C1->CR1 |= (1<<0); // Enable I2C
i2c_read_register(0x68, 0x75);
}
uint8_t i2c_read_register(uint8_t device_addr, uint8_t reg) {
/*1. START
2. Send device address + WRITE bit → (tell it "I want to talk, and I'm going to write")
3. Send the register number we want (0x75)
4. Repeated START → (turn the bus around)
5. Send device address + READ bit → ("now I want to read")
6. Read the byte the sensor sends
7. STOP*/
I2C1->CR1 |= (1<<8); // Generate START condition
while(!(I2C1->SR1 & (1<<0))); // Wait for SB (Start Bit) flag
I2C1->DR = (0x68<<1) | 0; // 1st byte: the device ADDRESS ("talk to chip 0x68, writing")
while(!(I2C1->SR1 & (1<<1))); // Wait for ADDR (Address acknowledged) flag
(void)I2C1->SR1; // Read SR1 to clear ADDR flag
(void)I2C1->SR2; // Read SR2 to clear ADDR flag
I2C1->DR = 0x75; // 2nd byte: the REGISTER number ("I want register 0x75")
while (!(I2C1->SR1 & (1<<7))); // Wait for TXE (Data register empty) flag
while (!(I2C1->SR1 & (1<<2))); // Basierend auf Fausregel
I2C1->CR1 |= (1<<8); // Regenerate the Start
while (!(I2C1->SR1 & (1<<0)));
I2C1->DR = (0x68<<1) | 1; // choosing the same slave again (IMU) + Intent of Reading (Read bit 1)
while(!(I2C1->SR1 & (1<<1)));
I2C1->CR1 &= ~(1<<10);
(void)I2C1->SR1;
(void)I2C1->SR2;
I2C1->CR1 |= (1<<9);
/*SR1 isn't one status — it's a register full of separate flags, each reporting a different event.*/
while(!(I2C1->SR1 & (1<<6)));
uint8_t value = I2C1->DR;
return value;

}

 

3 replies

TDK
July 29, 2026

does the rest of the standard MPU6050 register map (PWR_MGMT_1 default 0x40, ACCEL_CONFIG/GYRO_CONFIG, and the ACCEL/GYRO output registers starting at 0x3B) behave identically on these ICM-20689-based clones

This isn’t really an STM32 question. You should ask the manufacturer you got the sensor from. Better to stick with trusted suppliers if you want reliable products.

"If you feel a post has answered your question, please click ""Accept as Solution""."
David Littell
Senior II
July 29, 2026

Have you tried actually reading the Data Sheets and comparing them, or is that not AI-enough?

Andrew Neil
Super User
July 29, 2026

it looks like this specific GY-521 board carries a die that isn't the original InvenSense MPU-6050 — likely an ICM-20689 (or a clone claiming to be one),

So have you taken this up with the supplier ?

If you’ve already concluded that the product is mis-described, is there really any point in proceeding with it?

 

As ​@David Littell said, have you checked if the behaviour does fully match the ICM-20689 datasheet ?

 

And, as ​@TDK says, this really doesn’t have anything to do with the STM32

A complex system that works is invariably found to have evolved from a simple system that worked.A complex system designed from scratch never works and cannot be patched up to make it work.