# Bare-metal I2C1 on STM32F411 — WHO\_AM\_I returns 0x98, not 0x68 (GY-521/MPU6050)

**URL:** <https://forum.arduino.cc/t/bare-metal-i2c1-on-stm32f411-who-am-i-returns-0x98-not-0x68-gy-521-mpu6050/1453784>\
**Category:** Sensors\
**Created:** [July 29, 2026, 12:58pm UTC](https://forum.arduino.cc/t/bare-metal-i2c1-on-stm32f411-who-am-i-returns-0x98-not-0x68-gy-521-mpu6050/1453784 "2026-07-29T12:58:45Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![youssefgebah](https://dub1.discourse-cdn.com/arduino/user_avatar/forum.arduino.cc/youssefgebah/32/1279231_2.png) [@youssefgebah](https://forum.arduino.cc/u/youssefgebah)\
**Post date:** [July 29, 2026, 12:58pm UTC](https://forum.arduino.cc/t/bare-metal-i2c1-on-stm32f411-who-am-i-returns-0x98-not-0x68-gy-521-mpu6050/1453784/1 "2026-07-29T12:58:46Z")

</div>

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.

```arduino

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;

}

```

---

<div class="post-metadata">

**Author:** ![gilshultz](https://avatars.discourse-cdn.com/v4/letter/g/67e7ee/32.png) [@gilshultz](https://forum.arduino.cc/u/gilshultz)\
**Post date:** [July 30, 2026, 3:12pm UTC](https://forum.arduino.cc/t/bare-metal-i2c1-on-stm32f411-who-am-i-returns-0x98-not-0x68-gy-521-mpu6050/1453784/2 "2026-07-30T15:12:41Z")

</div>

> [@youssefgebah](#):
>
> GY-521

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?
