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

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.


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;

}

Can you post an annotated schematic showing how everything is connected? That will make it much easier to help identify the problem.

You are correct that 0x68 is one of the two valid I²C addresses:

AD0 = GND (Low): 0x68 (104 decimal, default)
AD0 = VCC (High): 0x69 (105 decimal)

The ICM-20689 uses the same two I²C addresses.

If you're reading the WHO_AM_I register (0x75), the ICM-20689 should return 0x98. That is the device ID, not the I²C address.

I suspect you may be confusing the WHO_AM_I value with the device address, or reading the wrong value.

What does an I²C scanner report for the device address?