🌀 ESP32 + MPU6050 惯性导航:从获取数据到自平衡机器人
📅 2026-06-05 · 🏷️ ESP32-S3 · MPU6050 · 传感器 · 自平衡
MPU6050 是目前最流行的六轴惯性传感器,集成了三轴加速度计和三轴陀螺仪,配合 I2C 接口可以方便地与 ESP32 相连。本教程从读取原始数据开始,逐步引入卡尔曼滤波,最终实现一个能保持自平衡的小车底盘。
📦 硬件准备
| 元器件 | 数量 | 备注 |
|---|
| ESP32-S3-DevKitC-1 | 1块 | 主控,也可以用 ESP32 通用版 |
| MPU6050 模块 | 1个 | GY-521 板,带针座 |
| 舵机 MG996R × 2 | 2个 | 用于自平衡车左右轮 |
| 车轮 + 底板套件 | 1套 | 淘宝搜索"自平衡小车底盘" |
| 面包板 + 跳线 | 若干 | 杜邦线公对母 |
| 4.8V 锂电池 | 1块 | 给舵机供电,不要只用 USB 供电舵机 |
🔌 接线
MPU6050 → ESP32-S3
VCC → 3.3V(或 5V 也可以)
GND → GND
SCL → GPIO 1(I2C0_SCL)
SDA → GPIO 0(I2C0_SDA)
⚠️ ESP32-S3 默认 I2C 引脚:
GPIO 0 = SDA
GPIO 1 = SCL
📐 MPU6050 数据手册要点
MPU6050 内置三轴加速度计(±2/4/8/16g 可选)和三轴陀螺仪(±250/500/1000/2000°/s 可选),内部还集成了温度传感器。通过 I2C 地址 0x68(或 0x69 备选)访问内部寄存器。
① 读取原始数据
先直接读取 MPU6050 的加速度和角速度原始值,感受数据质量。
// Wire.h 由 ESP32 Arduino 核心提供,I2C 通信
#include <Wire.h>const int MPU_ADDR = 0x68; // AD0 接地 => 0x68;接 VCC => 0x69void setup() {
Serial.begin(115200);
Wire.begin(0, 1); // ESP32-S3: SDA=GPIO0, SCL=GPIO1
Wire.setClock(400000); // 400kHz I2C 高速模式// 唤醒 MPU6050(默认睡眠模式)
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x6B); // PWR_MGMT_1 寄存器
Wire.write(0x00); // 唤醒,解除睡眠
Wire.endTransmission();
}
void loop() {
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B); // ACCEL_XOUT_H — 加速度数据起始寄存器
Wire.endTransmission();
Wire.requestFrom(MPU_ADDR, 14); // 一次读 14 字节if (Wire.available() == 14) {
int ax = Wire.read() << 8 | Wire.read();
int ay = Wire.read() << 8 | Wire.read();
int az = Wire.read() << 8 | Wire.read();
int tmp = Wire.read() << 8 | Wire.read();
int gx = Wire.read() << 8 | Wire.read();
int gy = Wire.read() << 8 | Wire.read();
int gz = Wire.read() << 8 | Wire.read();
// 除以 16384 得到 g(加速度计默认 ±2g 量程)// 除以 131 得到 °/s(陀螺仪默认 ±250°/s 量程)
Serial.printf("Acc: %7.2f %7.2f %7.2f | Gyr: %7.1f %7.1f %7.1f\n",
ax/16384.0, ay/16384.0, az/16384.0,
gx/131.0, gy/131.0, gz/131.0);
}
delay(20);
}
💡 为什么得到的是原始 ADC 值?
MPU6050 内置 16 位 ADC,加速度原始值 0~65535 对应 ±2g,即零点(平放)在 16384 左右。用手轻轻转动传感器,观察陀螺仪数值变化。
② 卡尔曼滤波:让角度估算稳定
加速度计能直接测量静止时的俯仰角(通过 atan2),但噪声大、响应慢。陀螺仪响应快但会漂移。卡尔曼滤波将两者融合,兼顾实时性和准确性。
// 卡尔曼滤波器 — 单轴(俯仰角)
class KalmanFilter {
public:
KalmanFilter(float q, float r) : Q(q), R(r), angle(0), bias(0), P(1) {}
float update(float accAngle, float gyroRate, float dt) {
// 预测步骤
angle += (gyroRate - bias) * dt;
P += Q * dt;
// 更新步骤float S = P + R;
float K = P / S;
float y = accAngle - angle;
angle += K * y;
bias += K * bias;
P = (1 - K) * P;
return angle;
}
float getAngle() { return angle; }
private:
float Q, R, angle, bias, P;
};
③ 完整自平衡控制代码
用 PID 控制轮子转速,让车体保持直立。当角度倾斜时,轮子向倾斜方向运动来纠正。
#include <Wire.h>
#include <servo.h>#define SERVO_L_PIN 2
#define SERVO_R_PIN 3
#define MPU_ADDR 0x68
#define SERVO_MIN 40
#define SERVO_MAX 115
#define BALANCE_ANG 0
Servo servoL, servoR;
KalmanFilter kf(0.001, 0.1);
// PID 参数(需根据实际重心机械调参)
float Kp = 50, Ki = 0.1, Kd = 10;
float integral = 0, prevAngle = 0;
void readMPU6050(float& ax, float& ay, float& az,
float& gx, float& gy, float& gz) {
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x3B);
Wire.endTransmission();
Wire.requestFrom(MPU_ADDR, 14);
if (Wire.available() == 14) {
ax = (Wire.read() << 8 | Wire.read()) / 16384.0;
ay = (Wire.read() << 8 | Wire.read()) / 16384.0;
az = (Wire.read() << 8 | Wire.read()) / 16384.0;
Wire.read(); Wire.read();
gx = (Wire.read() << 8 | Wire.read()) / 131.0;
gy = (Wire.read() << 8 | Wire.read()) / 131.0;
gz = (Wire.read() << 8 | Wire.read()) / 131.0;
}
}
float calcAngle(float ax, float az) {
return atan2(ax, az) * 180.0 / 3.1415926;
}
void setup() {
Serial.begin(115200);
Wire.begin(0, 1);
Wire.setClock(400000);
Wire.beginTransmission(MPU_ADDR);
Wire.write(0x6B); Wire.write(0x00);
Wire.endTransmission();
servoL.attach(SERVO_L_PIN, SERVO_MIN, SERVO_MAX);
servoR.attach(SERVO_R_PIN, SERVO_MIN, SERVO_MAX);
}
void loop() {
static unsigned long lastT = 0;
float ax, ay, az, gx, gy, gz;
readMPU6050(ax, ay, az, gx, gy, gz);
float accAngle = calcAngle(ax, az);
float dt = (millis() - lastT) / 1000.0;
lastT = millis();
float angle = kf.update(accAngle, gy, dt);
float error = angle - BALANCE_ANG;
integral += error * dt;
integral = constrain(integral, -50, 50);
float output = Kp * error + Ki * integral + Kd * (error - prevAngle) / dt;
prevAngle = error;
int pwm = map(constrain(output, -400, 400), -400, 400, SERVO_MIN, SERVO_MAX);
servoL.writeMicroseconds(pwm);
servoR.writeMicroseconds(pwm);
Serial.printf("%.2f,%.2f,%.1f\n", angle, accAngle, output);
delay(5);
}
⚠️ 调参建议:
如果车体发抖:降低 P
如果车体响应迟钝:提高 P
如果持续漂移:提高 I
如果震荡衰减慢:提高 D
建议从 Kp=30 开始,每次乘以 1.5 倍慢慢找最优值。机械重心越低越容易平衡。
🎬 视频演示
📺 ESP32 + MPU6050 自平衡车完整过程(点击观看)
📊 数据校准技巧
MPU6050 出厂有零偏,买回来第一件事是校准:在平面上静止放置,记录 200 次数据的平均值,在代码里把这个偏移量减去。
void calibrateMPU6050(float& gyroXBias, float& gyroYBias, float& gyroZBias,
float& accXBias, float& accYBias, float& accZBias) {
const int N = 200;
float sumGX = 0, sumGY = 0, sumGZ = 0;
float sumAX = 0, sumAY = 0, sumAZ = 0;
Serial.println("Calibrating MPU6050 — keep sensor still...");
for (int i = 0; i < N; i++) {
float ax, ay, az, gx, gy, gz;
readMPU6050(ax, ay, az, gx, gy, gz);
sumGX += gx; sumGY += gy; sumGZ += gz;
sumAX += ax; sumAY += ay; sumAZ += az;
delay(10);
}
gyroXBias = sumGX / N; gyroYBias = sumGY / N; gyroZBias = sumGZ / N;
accXBias = sumAX / N; accYBias = sumAY / N;
accZBias = (sumAZ / N) - 1.0;
Serial.printf("Calibration done: gyro(%.2f, %.2f, %.2f) acc(%.4f, %.4f, %.4f)\n",
gyroXBias, gyroYBias, gyroZBias, accXBias, accYBias, accZBias);
}
📋 寄存器速查
| 寄存器 | 地址 | 功能 |
|---|
| PWR_MGMT_1 | 0x6B | 睡眠/唤醒/时钟源 |
| ACCEL_XOUT_H | 0x3B | 加速度 X 高字节 |
| TEMP_OUT_H | 0x41 | 温度高字节 |
| GYRO_XOUT_H | 0x43 | 角速度 X 高字节 |
| CONFIG | 0x1A | 数字低通滤波器配置 |
| ACCEL_CONFIG | 0x1C | 加速度量程设置 |
| GYRO_CONFIG | 0x1B | 陀螺仪量程设置 |
🔗 扩展方向
- 📡 加蓝牙模块(HC-05),无线调参和监控
- 📶 加 Wi-Fi,用手机 APP 实时查看角度曲线
- 🧭 融合磁力计(HMC5883L),实现航向角(偏航角)估计
- 🛰️ 加 GPS 模块,实现室内/室外组合导航
- 🤖 用 ESP32-S3 的 NN 引擎做端侧姿态识别(跌倒检测)
💡 快速验证 MPU6050 是否正常:用 Arduino 的 I2C Scanner(搜索"Wire I2C Scanner"),确认能扫描到地址 0x68。如果扫不到:检查接线、SCL/SDA 是否接反、供电是否正常(部分模块需要 5V)。