把 GY-521 的 SDA、SCL 接到 Arduino Uno 的 A4、A5,先唤醒沉睡的 MPU-6050,然后把原始的 16 位读数除以 datasheet 给的 scale factor——默认量程下每 g 是 16384、每 °/s 是 131,最后用 complementary filter 把 accelerometer 和 gyro 融合成一个稳定的倾角,显示在 OLED 上。
GY-521 板上有什么?八个引脚各做什么?
GY-521 是一块蓝色小板,载着 InvenSense 的 MPU-6050:一颗黑色芯片里同时装了三轴 gyroscope 和三轴 accelerometer。gyro 量程 ±250 到 ±2000°/s,accelerometer ±2 到 ±16 g;板上的稳压器可以接 3–5V——MPU-6050 datasheet 写明芯片本身只吃 2.375–3.46V,所以 Uno 的 5V 可以放心接:它进的是板上的稳压器,芯片始终工作在低压。丝印上的箭头标出 X 轴和 Y 轴的方向。
八个金色焊盘里,有两个几乎人人都搞混。AD0 决定 I2C 地址:板上有下拉电阻把它拉低,所以悬空不接时模块应答在 0x68;把它接到 3.3V(不是 5V),地址就变成 0x69,同一条总线就能挂第二个模块。XDA 和 XCL 是 MPU-6050 芯片自己的 auxiliary I2C master,用来在芯片后面再串一个 sensor(比如磁力计)——绝对不要把 Uno 接到这两个脚上。
排针出厂是两条散装的,一条直的、一条弯角的——按你的装法选一条焊上,可以参考我们的排针焊接教程。千万不要只插不焊:靠摩擦的接触,板子一倾斜就断——而倾斜正是这个项目要做的事。
材料清单
Parts list
把两个模块插在 breadboard 上,再用公对公 Dupont 线接到 Uno——比拎着一把散线去倾斜板子稳得多。
GY-521 和 OLED 怎么接到 Uno?
Arduino Uno 的 I2C 在两个模拟脚上:A4 是 SDA,A5 是 SCL。两个模块共用这两条线,再加上电源——总共八条跳线。
| 模块引脚 | Uno 引脚 | 说明 |
|---|---|---|
| GY-521 VCC | 5V | 板上稳压器会降压供给芯片 |
| GY-521 GND | GND | — |
| GY-521 SCL | A5 | 共用的 I2C 时钟线 |
| GY-521 SDA | A4 | 共用的 I2C 数据线 |
| GY-521 AD0 | 不接 | 地址保持默认的 0x68 |
| GY-521 XDA、XCL | 不接 | Auxiliary I2C master——不是给 Uno 用的 |
| GY-521 INT | 不接 | 中断输出,本项目用不到 |
| OLED GND | GND | 这块屏的第一脚——认准丝印 |
| OLED VCC | 5V | 第二脚,不是第一脚 |
| OLED SCL | A5 | 和 GY-521 共用时钟线 |
| OLED SDA | A4 | 和 GY-521 共用数据线 |
留意 OLED 的排针顺序:从左到右是 GND、VCC、SCL、SDA,把前两脚接反是这类屏最经典的死法。共用一条总线完全没问题,因为 I2C 给每颗芯片都编了地址:MPU-6050 应答在 0x68,这块 OLED 应答在 0x3C,Uno 点名叫谁谁答——和我们的 DHT11 OLED 温湿度监测是同一套玩法。

想看模块实际接线、原始数值在真实桌面上跳动的样子,这段实拍把硬件部分讲得很清楚:
为什么 MPU6050 会输出 16384 这种巨大的数字?
MPU-6050 送出来的不是”度”也不是”g”——它送的是 −32768 到 +32767 的有符号 16 位整数,把它们直接当实际单位来读是新手的头号错误。板子平放在桌上,Z 轴 accelerometer 读数大约是 16384:不是坏了,这正是用原始 counts 表示的 1 g。按你选的量程,除以 datasheet 里的灵敏度 scale factor:16384 ÷ 16384 = 1.00 g;gyro 读到 262,就是 262 ÷ 131 = 2°/s。这两个除数都不是随便定的:+2 g 占满 16 位量程的整个正半边——32768 个 counts——所以 1 g = 32768 ÷ 2 = 16384;而 32768 ÷ 250 正是 gyro 那个 131 的来历。
| 量程设置 | 原始值除以 | 结果单位 |
|---|---|---|
| Accel ±2 g(默认) | 16384 | g |
| Accel ±4 g | 8192 | g |
| Accel ±8 g | 4096 | g |
| Accel ±16 g | 2048 | g |
| Gyro ±250°/s(默认) | 131 | °/s |
| Gyro ±500°/s | 65.5 | °/s |
| Gyro ±1000°/s | 32.8 | °/s |
| Gyro ±2000°/s | 16.4 | °/s |
这其实是量程和分辨率之间的交换:同样 16 位摊到 ±16 g 上,每个 count 代表的加速度就大了八倍。本教程沿用上电默认值——±2 g 和 ±250°/s,测倾角正合适;以后如果把量程调宽,除数也要跟着换。
为什么 accelerometer 和 gyroscope 两个都要用?
MPU-6050 的 accelerometer 几乎是白送你一个倾角:板子一倾斜,重力那稳稳的 1 g 就按角度分配到 Y 轴和 Z 轴上,atan2(ay, az) 从这个分配比例就能还原出 roll 角——放几分钟都稳如泰山。但它有个死穴:sensor 量的是所有加速度,不只是重力。手一晃,晃动本身多出一股力,合成向量不再指向正下方,算出来的角度就乱跳——偏偏乱跳的时刻,正是你最想看角度的时刻。
gyroscope 的性格刚好相反:把它的旋转速度乘以时间步长再累加——也就是积分——得到的角度又快又顺,完全无视晃动。但每个模块静止时都带一点小小的 bias,把这个恒定误差一直积分下去,它会无限长大——哪怕只剩 0.5°/s 的残余 bias,积分一分钟就是 30° 的假倾角,而板子其实一动不动。
实用的解法是 complementary filter,一行代码各取所长:angle = 0.96 * (angle + rate * dt) + 0.04 * accelAngle。每一圈 loop,filter 保留 96% 由 gyro 更新出来的角度,再掺入 4% accelerometer 给出的答案。这 4% 小到晃动时 accelerometer 的尖峰撼动不了输出,但它每秒重复几十次,几秒之内就把 gyro 累积的 drift 全部抹平。把 0.96 往 0.98 调,就是更信 gyro 一点;往低调,accelerometer 的抖动就会浮现在输出里。不用 DMP 固件,不用 Kalman 数学,一块 Uno 跑得轻轻松松。

正因为有这个 bias,sketch 才要在启动时校准:板子保持不动,取 500 个 gyro 读数求平均,之后每次读数都减掉这个 offset。
完整的 Arduino sketch 长什么样?
需要安装的只有 Adafruit 的显示库:在 Arduino IDE 的 Library Manager 里装 Adafruit SSD1306,弹出提示时一并接受它的 Adafruit GFX 和 Adafruit BusIO 依赖。MPU-6050 这边完全不用库——内建的 Wire 直接读 register,所以你刚学会的每一次除法在代码里都看得见。
setup 的写法来自两个 register 事实:芯片上电时处于 sleep 模式,所以第一步要清掉 PWR_MGMT_1 (0x6B) 里的 SLEEP 位;而全部 14 个数据 byte 从 register 0x3B 开始连续排列,一次 burst read 就能全部取回。
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define MPU_ADDR 0x68 // AD0 悬空不接的 GY-521
#define OLED_ADDR 0x3C // 这块 0.96" 模块的地址
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
// MPU-6050 datasheet 里的灵敏度 scale factor
// (默认量程:+/-2 g 和 +/-250 deg/s)
const float ACCEL_LSB_PER_G = 16384.0;
const float GYRO_LSB_PER_DPS = 131.0;
const float ALPHA = 0.96; // complementary filter 系数
int16_t ax, ay, az, gx, gy, gz;
float gyroBiasX = 0; // 静止时的 gyro offset,启动时测出
float angle = 0; // 融合后的倾角,单位是度
unsigned long lastMicros;
int16_t read16() { // 从 MPU 读一个 big-endian 16 位值
int16_t v = Wire.read() << 8;
v |= Wire.read();
return v;
}
void readMPU() {
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B); // ACCEL_XOUT_H:数据块的起点
Wire.endTransmission(false);
Wire.requestFrom(MPU_ADDR, 14, true); // accel(6) + temp(2) + gyro(6)
ax = read16(); ay = read16(); az = read16();
read16(); // 跳过温度
gx = read16(); gy = read16(); gz = read16();
}
void setup() {
Wire.begin();
// MPU-6050 上电时在睡觉:清掉 PWR_MGMT_1 里的 SLEEP 位
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x6B);
Wire.write(0x00);
Wire.endTransmission(true);
delay(100);
display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
display.setCursor(0, 0);
display.println(F("Calibrating..."));
display.println(F("Keep the board still"));
display.display();
// 取 500 个静止时的 gyro 读数求平均,得到 bias
long sum = 0;
for (int i = 0; i < 500; i++) {
readMPU();
sum += gx;
delay(2);
}
gyroBiasX = sum / 500.0;
lastMicros = micros();
}
void loop() {
readMPU();
unsigned long now = micros();
float dt = (now - lastMicros) / 1000000.0; // 距上一圈 loop 的秒数
lastMicros = now;
// 原始 counts 换算成实际单位(除以 scale factor)
float ayG = ay / ACCEL_LSB_PER_G; // g
float azG = az / ACCEL_LSB_PER_G; // g
float rateX = (gx - gyroBiasX) / GYRO_LSB_PER_DPS; // deg/s,已减去 bias
// accelerometer 倾角:长期稳定,一动就有噪声
float accelAngle = atan2(ayG, azG) * 180.0 / PI;
// complementary filter:gyro 负责快的变化,
// accelerometer 慢慢把 drift 拉回来
angle = ALPHA * (angle + rateX * dt) + (1.0 - ALPHA) * accelAngle;
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.print(F("MPU6050 tilt"));
display.setTextSize(3);
display.setCursor(0, 28);
display.print(angle, 1);
display.setTextSize(1);
display.print(F(" deg"));
display.display();
}
上传之后,先让板子平放着等校准信息走完,然后绕它的 X 轴——丝印箭头指的那条轴——来回滚动。角度应该即时跟着你的手走,放回桌面就归零,并且稳稳停在零上。

读数不对或什么都不显示,怎么排查?
GY-521 出问题的方式都很有辨识度,每种症状都指向一个具体的地方。
| 症状 | 可能原因 | 解决方法 |
|---|---|---|
| I2C 总线上找不到任何设备 | 接线问题或排针没焊 | 跑一个 I2C scanner sketch;确认 SDA 在 A4、SCL 在 A5,排针是真的焊上了而不是插着 |
| 所有读数都是零 | 芯片还在 sleep | 确认写 PWR_MGMT_1 (0x6B) 那段代码有跑到,地址是 0x68 |
| 某一轴卡在 32767 或 −32768 | 数值顶到满量程 | 猛敲一下瞬间顶满是正常的;某一轴长期卡死不动,多半是 sensor 已损坏 |
| 静止时角度持续漂移 | gyro bias 没有去掉 | 启动校准时让板子完全静止;按 reset 重新校准 |
| 敲一下板子角度就跳 | accelerometer 对冲击的反应 | 小幅度属正常;把 ALPHA 往 0.98 调,更信 gyro 一些 |
| OLED 一直黑屏 | 电源脚接反或地址不对 | 检查 GND 在第一脚的排针顺序;scanner 若在 0x3D 找到屏,就改用 0x3D |
真实客户最常犯的错误
我们最常遇到的,是把 Uno 接进 XDA 和 XCL——因为名字看起来像 SDA、SCL 的兄弟。那两个焊盘是 sensor 自己的 auxiliary 总线,Uno 要接的永远只有 SDA 和 SCL。
其次是跳过焊接:散装排针直接插上去,平放在桌上一切正常,板子一倾斜、针脚一动就断线。先把排针焊好,再谈其它排查。
第三是用原始输出评判模块——看到 16384 就断定 sensor 坏了,或者拿别的量程的除数来除。还有 OLED 那条 GND 在前的排针:习惯性地”红线先接第一脚”,至今仍是唯一一个硬件不会原谅的错误。
FAQ
想得到好的角度,一定要用 DMP 吗?
不用。MPU-6050 内建的 Digital Motion Processor 可以在芯片上直接算姿态,但需要上传一大段固件、用更重的 library。就测倾角而言,这里的 complementary filter 已经给出平滑稳定的结果,而且每一步算术你都真的看得懂。
MPU6050 能测位置或距离吗?
基本不能。算位置要对加速度积分两次,任何微小的 bias 都会被放大两轮,几秒之内算出来的位置就飘出几米——真正的系统要靠 GPS、轮式编码器或摄像头来补。把 MPU-6050 当姿态 sensor 用,别当定位器。
板子明明不动,为什么角度还在漂?
那是 gyro 的静止 bias 在被积分:每个模块静止时读数都不是正好为零,把这个误差一直累加,它就会无限长大。启动时让板子静止做校准,剩下的一点残余交给 filter 里的 accelerometer 项拉回来。
GY-521 能配 ESP32 用吗?
能——接到 ESP32 的 GPIO21(SDA)和 GPIO22(SCL),VCC 供 3.3V,同一套 register 代码原样能跑。我们的开发板对比指南可以帮你判断什么时候值得用上 ESP32 的速度和 WiFi。
每次上电都要重新校准吗?
要,sketch 在启动时大约两秒就做完了。gyro bias 会随温度变、隔天也会变,存起来的校准值会过期——每次开机重新平均一次,几乎没有成本。
最后更新:2026年8月。有问题?WhatsApp 联系我们。



Arduino Uno Compatible SMD UNO R3 with Type B Cable - ATMEGA328P with CH340G-Microcontroller Project
GY-521 Gyroscope Accelerometer Module MPU6050 MPU-6050 Motion Measurement Drone Robotic Application
0.96‘ OLED Display Module Blue White Screen I2C IIC Serial 128X64 LCD Display Board - 0.96' OLED WHITE
MB102 Breadboard 170 400 830 Holes Breadboard Donut Board Arduino Prototype Multi Color - MINI BREADBOARD 170 HOLES (WHITE)
40pcs Dupont Wire 10cm 20cm 30cm for Breadboard DIY Experiment Jumper Wire Breadboard wire - DUPONT WIRE M-M 30CM