DEV Community

Hedy
Hedy

Posted on

How to code MPU-6050 on STM32CubeIDE?

Here’s a clean, CubeMX + HAL way to bring up an MPU-6050 on STM32. You’ll wire I²C, generate a project in STM32CubeIDE, drop in the code below, and get raw accel/gyro + basic pitch/roll with a complementary filter.

1) Wiring (3.3 V logic)

  • Use pins that match your MCU. Adjust in CubeMX.

2) CubeMX setup

  1. Enable I²C (e.g., I2C1), Fast Mode 400 kHz.
  2. Assign SCL/SDA pins; leave internal pulls Disabled (use the module’s pull-ups).
  3. Optional: set PC13 (or any GPIO) as GPIO External Interrupt on Rising Edge for MPU INT. Enable NVIC line.
  4. Project Manager → Generate code → open in STM32CubeIDE.

3) Minimal HAL driver (drop-in)

Create mpu6050.h and mpu6050.c. Update extern I2C_HandleTypeDef hi2c1; if you used another I²C.

mpu6050.h

#pragma once
#include "stm32f1xx_hal.h" // <- change to your STM32 family header
#include <stdbool.h>
#include <stdint.h>

#define MPU6050_I2C_ADDR_68   (0x68 << 1)   // HAL expects 8-bit addr
#define MPU6050_I2C_ADDR_69   (0x69 << 1)

#define MPU6050_REG_WHO_AM_I  0x75
#define MPU6050_REG_PWR_MGMT1 0x6B
#define MPU6050_REG_SMPLRTDIV 0x19
#define MPU6050_REG_CONFIG    0x1A
#define MPU6050_REG_GYROCFG   0x1B
#define MPU6050_REG_ACCCFG    0x1C
#define MPU6050_REG_INT_EN    0x38
#define MPU6050_REG_INT_CFG   0x37
#define MPU6050_REG_ACCEL_XOUT_H 0x3B  // 14 bytes burst: AccX..GyroZ

typedef struct {
  int16_t ax, ay, az;
  int16_t temp;
  int16_t gx, gy, gz;
} mpu6050_raw_t;

typedef struct {
  float ax_g, ay_g, az_g;
  float gx_dps, gy_dps, gz_dps;
  float temp_c;
} mpu6050_si_t;

bool  mpu6050_init(I2C_HandleTypeDef *hi2c, uint16_t addr);
bool  mpu6050_read_raw(I2C_HandleTypeDef *hi2c, uint16_t addr, mpu6050_raw_t *r);
void  mpu6050_convert(const mpu6050_raw_t *r, mpu6050_si_t *o,
                      float acc_lsb_per_g, float gyro_lsb_per_dps);

// Simple complementary filter for pitch/roll (°)
void  mpu6050_complementary(const mpu6050_si_t *m, float dt_s,
                            float *pitch_deg, float *roll_deg, float alpha);
Enter fullscreen mode Exit fullscreen mode

mpu6050.c

#include "mpu6050.h"
#include <math.h>

static HAL_StatusTypeDef wr(I2C_HandleTypeDef *h, uint16_t addr, uint8_t reg, uint8_t val){
  return HAL_I2C_Mem_Write(h, addr, reg, 1, &val, 1, 100);
}
static HAL_StatusTypeDef rd(I2C_HandleTypeDef *h, uint16_t addr, uint8_t reg, uint8_t *buf, uint16_t len){
  return HAL_I2C_Mem_Read(h, addr, reg, 1, buf, len, 100);
}

bool mpu6050_init(I2C_HandleTypeDef *hi2c, uint16_t addr){
  // Check WHO_AM_I = 0x68 (bits 6:1)
  uint8_t who=0;
  if(rd(hi2c, addr, MPU6050_REG_WHO_AM_I, &who, 1) != HAL_OK) return false;
  if((who & 0x7E) != 0x68) return false;

  // Wake up & select PLL with X-gyro as clock
  if(wr(hi2c, addr, MPU6050_REG_PWR_MGMT1, 0x01) != HAL_OK) return false;

  // DLPF = 3 (~44 Hz accel, ~42 Hz gyro), good starting point
  if(wr(hi2c, addr, MPU6050_REG_CONFIG, 0x03) != HAL_OK) return false;

  // Sample rate divider: Fsample = 1kHz / (1 + div). div=4 → 200 Hz
  if(wr(hi2c, addr, MPU6050_REG_SMPLRTDIV, 0x04) != HAL_OK) return false;

  // Gyro full-scale = ±250 dps (FS_SEL=0)
  if(wr(hi2c, addr, MPU6050_REG_GYROCFG, 0x00) != HAL_OK) return false;

  // Accel full-scale = ±2 g (AFS_SEL=0)
  if(wr(hi2c, addr, MPU6050_REG_ACCCFG, 0x00) != HAL_OK) return false;

  // INT: data-ready enable (bit0); active-high, push-pull, latch until read (optional)
  if(wr(hi2c, addr, MPU6050_REG_INT_CFG, 0x10) != HAL_OK) return false; // clear-on-any-read
  if(wr(hi2c, addr, MPU6050_REG_INT_EN,  0x01) != HAL_OK) return false;

  return true;
}

bool mpu6050_read_raw(I2C_HandleTypeDef *hi2c, uint16_t addr, mpu6050_raw_t *r){
  uint8_t b[14];
  if(rd(hi2c, addr, MPU6050_REG_ACCEL_XOUT_H, b, 14) != HAL_OK) return false;
  r->ax = (int16_t)((b[0]<<8)|b[1]);
  r->ay = (int16_t)((b[2]<<8)|b[3]);
  r->az = (int16_t)((b[4]<<8)|b[5]);
  r->temp= (int16_t)((b[6]<<8)|b[7]);
  r->gx = (int16_t)((b[8]<<8)|b[9]);
  r->gy = (int16_t)((b[10]<<8)|b[11]);
  r->gz = (int16_t)((b[12]<<8)|b[13]);
  return true;
}

void mpu6050_convert(const mpu6050_raw_t *r, mpu6050_si_t *o,
                     float acc_lsb_per_g, float gyro_lsb_per_dps){
  o->ax_g = (float)r->ax / acc_lsb_per_g;   // ±2 g → 16384 LSB/g
  o->ay_g = (float)r->ay / acc_lsb_per_g;
  o->az_g = (float)r->az / acc_lsb_per_g;
  o->gx_dps = (float)r->gx / gyro_lsb_per_dps; // ±250 dps → 131 LSB/(°/s)
  o->gy_dps = (float)r->gy / gyro_lsb_per_dps;
  o->gz_dps = (float)r->gz / gyro_lsb_per_dps;
  o->temp_c = (float)r->temp / 340.0f + 36.53f;
}

void mpu6050_complementary(const mpu6050_si_t *m, float dt_s,
                           float *pitch_deg, float *roll_deg, float alpha){
  // Acc angles from gravity vector
  float roll_acc  = atan2f(m->ay_g, m->az_g) * 57.29578f;
  float pitch_acc = atan2f(-m->ax_g, sqrtf(m->ay_g*m->ay_g + m->az_g*m->az_g)) * 57.29578f;

  // Integrate gyro
  *roll_deg  = alpha * (*roll_deg  + m->gx_dps * dt_s) + (1.0f - alpha) * roll_acc;
  *pitch_deg = alpha * (*pitch_deg + m->gy_dps * dt_s) + (1.0f - alpha) * pitch_acc;
}
Enter fullscreen mode Exit fullscreen mode

4) Use it from main.c

#include "mpu6050.h"
extern I2C_HandleTypeDef hi2c1;

static uint16_t mpu_addr = MPU6050_I2C_ADDR_68; // AD0 low

int main(void){
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_I2C1_Init(); // generated by CubeMX

  // small power-up delay
  HAL_Delay(100);

  if(!mpu6050_init(&hi2c1, mpu_addr)){
    // Blink LED or debug here: WHO_AM_I not 0x68? wiring?
    while(1);
  }

  float pitch=0, roll=0;
  uint32_t t0 = HAL_GetTick();

  while(1){
    // If using INT pin + EXTI, wait for a flag instead of polling
    mpu6050_raw_t raw;
    if(mpu6050_read_raw(&hi2c1, mpu_addr, &raw)){
      mpu6050_si_t si;
      // For ±2 g and ±250 dps:
      mpu6050_convert(&raw, &si, 16384.0f, 131.0f);

      uint32_t t1 = HAL_GetTick();
      float dt = (t1 - t0) / 1000.0f; t0 = t1;

      // Complementary filter (alpha ~ 0.98 @ 100–200 Hz)
      mpu6050_complementary(&si, dt, &pitch, &roll, 0.98f);

      // TODO: use si.ax_g, si.gx_dps, si.temp_c, pitch, roll
    }

    // Run near your sample rate (MPU configured ~200 Hz)
    // If polling, a tiny delay helps bus sharing:
    // HAL_Delay(1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Optional (interrupt): In your EXTI callback:

volatile uint8_t mpu_data_ready = 0;
void HAL_GPIO_EXTI_Callback(uint16_t pin){
  if(pin == GPIO_PIN_13) mpu_data_ready = 1;
}
// Main loop:
// if(mpu_data_ready){ mpu_data_ready = 0; mpu6050_read_raw(...); }

Enter fullscreen mode Exit fullscreen mode

5) Calibration & scaling (do this!)

  • Gyro offset: with the board still, average gx/gy/gz over ~2 s and subtract as offsets.
  • Accel bias: at rest, adjust so sqrt(ax^2+ay^2+az^2) ≈ 1 g.
  • If you change full-scales, update scale factors:

    • Accel: ±2/±4/±8/±16 g → 16384/8192/4096/2048 LSB/g
    • Gyro: ±250/500/1000/2000 dps → 131/65.5/32.8/16.4 LSB/(°/s)

6) Common gotchas

  • HAL I²C address: pass 7-bit address left-shifted by 1 (0x68 << 1).
  • Pull-ups: if your breakout lacks them, add 4.7 kΩ to 3.3 V on SDA/SCL.
  • Bus speed/wires: keep short; 400 kHz is fine.
  • INT pin: MPU INT is active-high, push-pull by default; you enabled DATA_RDY_EN already.
  • DMP (quaternions): possible but requires extra firmware; start with the above, then move up if needed.

Top comments (0)