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
- Enable I²C (e.g., I2C1), Fast Mode 400 kHz.
- Assign SCL/SDA pins; leave internal pulls Disabled (use the module’s pull-ups).
- Optional: set PC13 (or any GPIO) as GPIO External Interrupt on Rising Edge for MPU INT. Enable NVIC line.
- 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);
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;
}
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);
}
}
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(...); }
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:
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)