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):
- START, wait SB
- Send
(0x68<<1)|0, wait ADDR (confirmed ACK) - Clear ADDR (SR1 then SR2 read)
- 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) - Repeated START, wait SB
- Send
(0x68<<1)|1, wait ADDR (confirmed ACK again) - Clear ACK bit (CR1 bit 10) before clearing ADDR — required ordering for a single-byte read
- Clear ADDR, set STOP
- 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;
}