传感器 · MPU6050 · 六轴 I2C
MPU6050 六轴姿态传感器 STM32F103 接线与代码
让单片机知道"现在朝向哪边、转得多快"。本页是 STM32F103C8T6 + MPU6050(GY-521) + 0.96 OLED 的完整实机验证记录,裸 I2C 读寄存器,原装和国产兼容版同一份代码都能跑。
Quick Facts
- 芯片:MPU-6050 六轴(3 轴加速度 + 3 轴陀螺仪)
- 地址:0x68(AD0 接地/悬空),0x69(AD0 接高)
- WHO_AM_I:0x68 原装 · 0x70 国产兼容
- 供电:3.3V,GY-521 板载稳压
- 接线:SCL→PB8,SDA→PB9,共 4 根线
- 实测:原装/国产同代码均能读,WHO_AM_I 不同
Verified
实机验证结果
本项目实测 原装和国产兼容版各测了一块,下面数据都是我们跑出来的。更新于 2026-08-16。
原装 vs 国产:同一份代码都能读
程序用裸 I2C 直接读寄存器,不依赖任何 MPU6050 库,也不校验 WHO_AM_I。所以原装和国产兼容版,代码一个字不改,两块都能正常读六轴数据、驱动 OLED。
| 对比项 | 原装版 | 国产兼容版 |
|---|---|---|
| I2C 地址 | 0x68 | 0x68 |
| WHO_AM_I | 0x68 | 0x70 |
| 能否读取 | 正常 | 正常(同代码) |
| OLED 显示 | 正常 | 正常 |
0x68 就直接初始化失败,国产片就"趴窝"了。裸读不挑芯片,谁都能读。
静止零偏(mean)和抖动(pp)
mean 是平均值(看偏不偏),pp 是峰峰值最大减最小(看抖不抖)。这是两块样品的现象记录,不代表"所有国产都比原装差"。
| 轴 | 原装 mean | 原装 pp | 国产 mean | 国产 pp |
|---|---|---|---|---|
| ax | -745 | ~85 | 16625 | ~150 |
| ay | -70 | ~75 | 982 | ~120 |
| az | 18230 | ~110 | -2900 | ~170 |
| gx | -195 | ~16 | -267 | ~20 |
| gy | -27 | ~14 | 228 | ~24 |
| gz | -98 | ~13 | 141 | ~20 |
编译烧录记录
| 项目 | 本项目实际使用 |
|---|---|
| 主控 | STM32F103C8T6(BluePill) |
| 传感器 | MPU6050(GY-521),原装 + 国产兼容版各一块 |
| 显示 | 0.96 寸 OLED,SSD1306,128×64,地址 0x3C |
| 开发工具 | VS Code + PlatformIO |
| 框架 | Arduino / STM32duino |
| 烧录 | ST-Link |
| 串口 | 115200 |
| 依赖库 | Adafruit SSD1306、Adafruit GFX Library(无 MPU6050 库) |
What it does
这个模块干什么
MPU6050 就是给单片机装一个"体感",让板子知道自己在往哪边歪、转得多快。你把它前倾、后仰、左右歪,甚至绕竖直方向转一圈,它都能报出对应的角度和角速度。一块芯片里同时装了加速度计和陀螺仪两套传感器,所以叫"六轴"。
它和普通单轴加速度计的区别是:六轴一体,倾斜用加速度算、转动用陀螺仪测,一块芯片同时给全,不用分开接两个传感器。
角速度(gyro):描述"转得多快",单位是度每秒(°/s)。
WHO_AM_I:芯片里一个固定的"身份证号"寄存器,读它能认出这块芯片是哪家的、原装还是兼容版。
Wiring
接线
本项目实测 MPU6050 和 OLED 共用同一组 I2C。下图接法实机跑通(接线图待补)。
| 信号 | MPU6050 | STM32F103C8T6 | OLED |
|---|---|---|---|
| 电源 | VCC | 3.3V | VCC / VDD |
| 地 | GND | GND | GND |
| 时钟 | SCL | PB8 | SCL / SCK |
| 数据 | SDA | PB9 | SDA |
| 地址脚 | AD0 | GND 或悬空 | — |
| 中断脚 | INT | 不接 | — |
0x68;AD0 接 3.3V = 0x69。本项目用默认 0x68。
| 要点 | 说明 |
|---|---|
| 供电 3.3V | GY-521 板载稳压,可接 3.3V 或 5V,但和 3.3V 的 STM32 一起用时直接 3.3V 最稳 |
| 必须共地 | 所有模块 GND 接在一起,否则 I2C 会莫名失败 |
| 地址不冲突 | MPU6050 是 0x68,OLED 是 0x3C,可以共用一组 I2C |
| PA13 / PA14 | 留给 ST-Link 调试,不要占用 |
Code
完整代码
本项目实测 下面是实际烧录进板子的那一份,一字未改。环境:VS Code + PlatformIO + Arduino(STM32duino)。核心思路:裸 I2C 读寄存器,不套库、不校验 WHO_AM_I。
关键处说明
1. 把 I2C 指到 PB8 / PB9。STM32duino 默认的 I2C 引脚不是 PB8/PB9,不手动指定就读不到任何东西。
2. 扫地址 + 读 WHO_AM_I(只显示,不拦截)。启动扫 0x68/0x69,找到后读 0x75 这个 WHO_AM_I 寄存器,只打印、不做"必须等于 0x68"的判断——这正是国产兼容版能直接跑的原因。
3. 读六轴数据。从 0x3B 连续读 14 字节:前 6 字节加速度,中间 2 字节温度跳过,后 6 字节陀螺仪。
4. 姿态角用 atan2 反推。静止时加速度计只感受重力,用重力在三个轴上的分量反推俯仰/横滚角。量程加速度 ±2g(16384 LSB/g)、陀螺仪 ±250°/s(131 LSB/°/s)。
#include <Arduino.h>
#include <math.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Wire.h>
// 04_MPU6050 : raw-I2C MPU6050 (GY-521) reader, import-vs-domestic test.
// Bare Wire calls (no MPU6050 library) so it works on both genuine parts
// (WHO_AM_I=0x68) and domestic clones (WHO_AM_I=0x70). WHO_AM_I is only
// reported, never enforced, so neither part is rejected at init.
constexpr uint8_t OLED_ADDR = 0x3C;
constexpr int SCREEN_W = 128;
constexpr int SCREEN_H = 64;
constexpr uint8_t MPU_ADDR_68 = 0x68; // AD0 low / floating (GY-521 default)
constexpr uint8_t MPU_ADDR_69 = 0x69; // AD0 high
constexpr uint8_t REG_PWR_MGMT_1 = 0x6B;
constexpr uint8_t REG_SMPLRT_DIV = 0x19;
constexpr uint8_t REG_CONFIG = 0x1A;
constexpr uint8_t REG_GYRO_CONFIG = 0x1B;
constexpr uint8_t REG_ACCEL_CONFIG = 0x1C;
constexpr uint8_t REG_WHO_AM_I = 0x75;
constexpr uint8_t REG_ACCEL_XOUT_H = 0x3B; // accel(6)+temp(2)+gyro(6)=14B
constexpr float ACCEL_LSB_PER_G = 16384.0f; // +-2g
constexpr float GYRO_LSB_PER_DPS = 131.0f; // +-250 deg/s
constexpr float PI_F = 3.14159265f;
constexpr float RAD2DEG = 180.0f / PI_F;
constexpr int WINDOW = 50;
Adafruit_SSD1306 display(SCREEN_W, SCREEN_H, &Wire, -1);
uint8_t g_mpuAddr = 0, g_whoAmI = 0;
bool g_found = false;
int16_t ax=0, ay=0, az=0, gx=0, gy=0, gz=0;
float pitch=0.0f, roll=0.0f;
static bool i2cProbe(uint8_t addr){ Wire.beginTransmission(addr); return Wire.endTransmission()==0; }
static bool mpuWrite(uint8_t addr, uint8_t reg, uint8_t val){ Wire.beginTransmission(addr); Wire.write(reg); Wire.write(val); return Wire.endTransmission()==0; }
static uint8_t mpuReadByte(uint8_t addr, uint8_t reg){ Wire.beginTransmission(addr); Wire.write(reg); if(Wire.endTransmission()!=0) return 0; if(Wire.requestFrom(addr,(uint8_t)1)<1) return 0; return (uint8_t)Wire.read(); }
static bool mpuReadAll(uint8_t addr, int16_t out[6]){
Wire.beginTransmission(addr); Wire.write(REG_ACCEL_XOUT_H);
if(Wire.endTransmission()!=0) return false;
if(Wire.requestFrom(addr,(uint8_t)14)<14) return false;
uint8_t b[14]; for(int i=0;i<14;i++) b[i]=(uint8_t)Wire.read();
out[0]=(int16_t)((b[0]<<8)|b[1]); out[1]=(int16_t)((b[2]<<8)|b[3]); out[2]=(int16_t)((b[4]<<8)|b[5]);
out[3]=(int16_t)((b[8]<<8)|b[9]); out[4]=(int16_t)((b[10]<<8)|b[11]); out[5]=(int16_t)((b[12]<<8)|b[13]);
return true;
}
static void showStatus(const char *l1, const char *l2){
display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1);
display.setCursor(0,0); display.println("04_MPU6050"); display.println(); display.println(l1); display.println(l2); display.display();
}
static void showData(){
char buf[40];
display.clearDisplay(); display.setTextColor(SSD1306_WHITE); display.setTextSize(1);
display.setCursor(0,0); display.print("04_MPU6050");
display.setCursor(0,16); snprintf(buf,sizeof(buf),"@0x%02X WHO=0x%02X",g_mpuAddr,g_whoAmI); display.print(buf);
display.setCursor(0,32); snprintf(buf,sizeof(buf),"P %5.1f R %5.1f",pitch,roll); display.print(buf);
display.setCursor(0,48);
int gxd=(int)lroundf((float)gx/GYRO_LSB_PER_DPS), gyd=(int)lroundf((float)gy/GYRO_LSB_PER_DPS), gzd=(int)lroundf((float)gz/GYRO_LSB_PER_DPS);
snprintf(buf,sizeof(buf),"gx%4d gy%4d gz%4d",gxd,gyd,gzd); display.print(buf);
display.display();
}
static void detectAndInit(){
g_found=false; g_mpuAddr=0; g_whoAmI=0;
if(i2cProbe(MPU_ADDR_68)) g_mpuAddr=MPU_ADDR_68; else if(i2cProbe(MPU_ADDR_69)) g_mpuAddr=MPU_ADDR_69;
if(g_mpuAddr==0){ showStatus("MPU not found","Check wiring"); Serial.println("MPU6050 not found on 0x68 / 0x69"); return; }
g_whoAmI=mpuReadByte(g_mpuAddr,REG_WHO_AM_I);
mpuWrite(g_mpuAddr,REG_PWR_MGMT_1,0x00); delay(50);
mpuWrite(g_mpuAddr,REG_SMPLRT_DIV,0x07); mpuWrite(g_mpuAddr,REG_CONFIG,0x03);
mpuWrite(g_mpuAddr,REG_GYRO_CONFIG,0x00); mpuWrite(g_mpuAddr,REG_ACCEL_CONFIG,0x00);
g_found=true;
Serial.print("MPU addr=0x"); Serial.print(g_mpuAddr,HEX);
Serial.print(" WHO_AM_I=0x"); Serial.println(g_whoAmI,HEX); Serial.println("init OK");
}
void setup(){
Serial.begin(115200); delay(300);
Wire.setSCL(PB8); Wire.setSDA(PB9); Wire.begin();
if(!display.begin(SSD1306_SWITCHCAPVCC,OLED_ADDR)){ Serial.println("OLED init failed"); while(true) delay(1000); }
Serial.println("=== MPU6050 import/domestic test ==="); Serial.println("OLED 0x3C: found");
showStatus("OLED OK","MPU detect..."); detectAndInit();
}
void loop(){
if(!g_found){ detectAndInit(); delay(500); return; }
int16_t v[6]; int32_t sum[6]={0};
int16_t mn[6]={32767,32767,32767,32767,32767,32767}, mx[6]={-32768,-32768,-32768,-32768,-32768,-32768};
int n=0;
for(; n<WINDOW; n++){
if(!mpuReadAll(g_mpuAddr,v)) break;
for(int i=0;i<6;i++){ sum[i]+=v[i]; if(v[i]<mn[i]) mn[i]=v[i]; if(v[i]>mx[i]) mx[i]=v[i]; }
}
if(n==0){ showStatus("Read failed","Check wiring"); Serial.println("read failed"); delay(500); return; }
ax=v[0]; ay=v[1]; az=v[2]; gx=v[3]; gy=v[4]; gz=v[5];
float axg=(float)ax/ACCEL_LSB_PER_G, ayg=(float)ay/ACCEL_LSB_PER_G, azg=(float)az/ACCEL_LSB_PER_G;
pitch=atan2f(axg,sqrtf(ayg*ayg+azg*azg))*RAD2DEG;
roll =atan2f(ayg,sqrtf(axg*axg+azg*azg))*RAD2DEG;
showData();
Serial.print("ax="); Serial.print(ax); Serial.print(" ay="); Serial.print(ay); Serial.print(" az="); Serial.print(az);
Serial.print(" gx="); Serial.print(gx); Serial.print(" gy="); Serial.print(gy); Serial.print(" gz="); Serial.print(gz);
Serial.print(" pitch="); Serial.print(pitch,1); Serial.print(" roll="); Serial.println(roll,1);
const char *names[6]={"ax","ay","az","gx","gy","gz"};
Serial.print("[stats] ");
for(int i=0;i<6;i++){ int mean=(int)(sum[i]/n), pp=(int)(mx[i]-mn[i]); Serial.print(names[i]); Serial.print(" mean="); Serial.print(mean); Serial.print(" pp="); Serial.print(pp); if(i<5) Serial.print(" | "); }
Serial.println();
}
Adafruit SSD1306 和 Adafruit GFX Library。用 PlatformIO 打开工程会按 platformio.ini 自动下载。没有用任何 MPU6050 库,读取全靠裸 I2C。
Download
下载
Video
视频演示
成片制作中,完成后这里会放实机演示视频。
Reference
实用参数与资料
资料参考 下面来自芯片数据手册和公开资料,不是我们实测的结论,用来帮助理解代码里的"魔法数字"。
代码里那些数字的来历
| 数字 | 含义 | 来源 |
|---|---|---|
0x3B | 六轴数据起始寄存器,连续 14 字节 | 数据手册 |
0x75 | WHO_AM_I 寄存器 | 数据手册 |
0x6B | 电源管理寄存器,写 0 唤醒芯片 | 数据手册 |
16384 | ±2g 量程下 1g 对应的原始值(LSB/g) | 数据手册 |
131 | ±250°/s 量程下 1°/s 对应的原始值(LSB/°/s) | 数据手册 |
0x68 / 0x69 | I2C 地址,由 AD0 引脚电平决定 | 数据手册 |
俯仰/横滚角怎么算
静止时加速度计只感受到重力,用重力在三个轴上的分量反推倾斜角:
pitch = atan2(ax, sqrt(ay² + az²)) × 180/π // 俯仰角(前后倾)
roll = atan2(ay, sqrt(ax² + az²)) × 180/π // 横滚角(左右倾)
国产兼容版和原装的区别
| 原装 MPU-6050 | 国产兼容版 | |
|---|---|---|
| WHO_AM_I | 0x68 | 0x70 |
| 六轴数据读取 | 正常 | 正常(裸 I2C) |
| DMP 固件 | 支持 | 不兼容,需软件融合 |
国产版 WHO_AM_I = 0x70、DMP 不兼容,来自旧版资料网页和公开资料;本项目本次用裸 I2C 实测确认了 0x70 和"能直接读"这两点。性能差异不在这里下普遍结论。资料参考
Troubleshooting
常见问题排查
| 现象 | 原因 | 怎么办 |
|---|---|---|
| 扫描不到设备 | AD0 电平不对(地址变了)、没共地、供电不足 | 查 GND 是否接上,AD0 接地或悬空用 0x68 |
| WHO_AM_I 不是 0x68 | 可能是国产兼容版(0x70),不是坏了 | 用裸 I2C 读即可,别用校验 WHO_AM_I 的库 |
| 读数恒为 0 | 没唤醒芯片(PWR_MGMT_1 没写 0) | 初始化写 0x6B=0x00 再读 |
| 用现成库初始化失败 | 国产片 WHO 0x70 过不了库的校验 | 换裸 I2C 读寄存器,不套库 |
| 姿态角跳动大 | 只用加速度算,动起来有噪声 | 加滤波或做姿态融合(互补/卡尔曼) |
Sources
资料来源
- MPU-6050 数据手册:寄存器地址(0x3B/0x75/0x6B 等)、量程换算(16384 LSB/g、131 LSB/°/s)、I2C 地址(AD0)。
- 旧版资料网页(02532-gy521-mpu6050):国产兼容版 WHO_AM_I = 0x70、DMP 不兼容。本项目实测确认了 0x70 和"能直接读"。
- 本项目实测:原装/国产 I2C 地址、WHO_AM_I、六轴数据、静止 mean/pp,见「实机验证结果」一节。
本页标注「本项目实测」的内容来自我们自己的实机测试;标注「资料参考」的内容来自数据手册和公开资料,由我们重新整理表述。