传感器 · 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。

0x68原装 WHO_AM_I
0x70国产 WHO_AM_I
同代码两块都能读
76.7%Flash 占用

原装 vs 国产:同一份代码都能读

程序用裸 I2C 直接读寄存器,不依赖任何 MPU6050 库,也不校验 WHO_AM_I。所以原装和国产兼容版,代码一个字不改,两块都能正常读六轴数据、驱动 OLED。

对比项原装版国产兼容版
I2C 地址0x680x68
WHO_AM_I0x680x70
能否读取正常正常(同代码)
OLED 显示正常正常
这是本页最想让你记住的一点:如果你用"套库"的做法,很多库会拿 WHO_AM_I 做校验,对不上 0x68 就直接初始化失败,国产片就"趴窝"了。裸读不挑芯片,谁都能读。

静止零偏(mean)和抖动(pp)

mean 是平均值(看偏不偏),pp 是峰峰值最大减最小(看抖不抖)。这是两块样品的现象记录,不代表"所有国产都比原装差"。

轴原装 mean原装 pp国产 mean国产 pp
ax-745~8516625~150
ay-70~75982~120
az18230~110-2900~170
gx-195~16-267~20
gy-27~14228~24
gz-98~13141~20
加速度(ax/ay/az)不能直接逐轴比。原装是直排针(平放,重力落在 Z),国产是弯排针(立放,重力落在 X),物理朝向不一样。所以 accel 的 mean 差别主要来自朝向,不是性能。陀螺仪(gx/gy/gz)是角速度、静止都接近 0,那个才可以比。

编译烧录记录

项目本项目实际使用
主控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 库)
未验证的环境不当成支持。本项目没有在 Arduino IDE、Keil、STM32CubeIDE 下编译或验证过,所以不提供这些环境的支持说明。

What it does

这个模块干什么

MPU6050 就是给单片机装一个"体感",让板子知道自己在往哪边歪、转得多快。你把它前倾、后仰、左右歪,甚至绕竖直方向转一圈,它都能报出对应的角度和角速度。一块芯片里同时装了加速度计和陀螺仪两套传感器,所以叫"六轴"。

它和普通单轴加速度计的区别是:六轴一体,倾斜用加速度算、转动用陀螺仪测,一块芯片同时给全,不用分开接两个传感器。

俯仰角 / 横滚角(pitch / roll):描述"板子往前/后、左/右歪了多少度"。
角速度(gyro):描述"转得多快",单位是度每秒(°/s)。
WHO_AM_I:芯片里一个固定的"身份证号"寄存器,读它能认出这块芯片是哪家的、原装还是兼容版。

Wiring

接线

本项目实测 MPU6050 和 OLED 共用同一组 I2C。下图接法实机跑通(接线图待补)。

信号MPU6050STM32F103C8T6OLED
电源VCC3.3VVCC / VDD
地GNDGNDGND
时钟SCLPB8SCL / SCK
数据SDAPB9SDA
地址脚AD0GND 或悬空—
中断脚INT不接—
AD0 决定地址。AD0 接地或悬空 = 0x68;AD0 接 3.3V = 0x69。本项目用默认 0x68。
要点说明
供电 3.3VGY-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

下载

PlatformIO 工程 ZIP 含 platformio.ini、src/main.cpp、README.md。已去掉本机构建路径,解压后 VS Code 打开即可 Build。
下载 ZIP

Video

视频演示

成片制作中,完成后这里会放实机演示视频。

Reference

实用参数与资料

资料参考 下面来自芯片数据手册和公开资料,不是我们实测的结论,用来帮助理解代码里的"魔法数字"。

代码里那些数字的来历

数字含义来源
0x3B六轴数据起始寄存器,连续 14 字节数据手册
0x75WHO_AM_I 寄存器数据手册
0x6B电源管理寄存器,写 0 唤醒芯片数据手册
16384±2g 量程下 1g 对应的原始值(LSB/g)数据手册
131±250°/s 量程下 1°/s 对应的原始值(LSB/°/s)数据手册
0x68 / 0x69I2C 地址,由 AD0 引脚电平决定数据手册

俯仰/横滚角怎么算

静止时加速度计只感受到重力,用重力在三个轴上的分量反推倾斜角:

pitch = atan2(ax, sqrt(ay² + az²)) × 180/π   // 俯仰角(前后倾)
roll  = atan2(ay, sqrt(ax² + az²)) × 180/π   // 横滚角(左右倾)
这只能算"倾斜",算不出"偏航"。绕竖直方向的偏航角(yaw)要靠陀螺仪积分,而陀螺仪积分会随时间漂移,不借助磁力计很难长期稳定。本项目只做倾斜展示,没做 yaw。

国产兼容版和原装的区别

原装 MPU-6050国产兼容版
WHO_AM_I0x680x70
六轴数据读取正常正常(裸 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

资料来源

本页标注「本项目实测」的内容来自我们自己的实机测试;标注「资料参考」的内容来自数据手册和公开资料,由我们重新整理表述。

BNO086 + CYD ST7789 接线、V1.7源码与在线烧录

返回传感器分类 返回首页 淘宝店铺