51单片机MPU6050 DMP驱动实现

一、DMP到底解决了什么问题?

MPU6050自带了加速度计 + 陀螺仪,如果只用原始数据,你需要自己在51单片机上跑卡尔曼滤波或互补滤波来做姿态解算------这对8位单片机来说是算力负担。

DMP(Digital Motion Processor) 是InvenSense内置的数字运动处理器,它能直接在芯片内部完成姿态融合,吐给你的就是现成的四元数,你只需要做一次四元数→欧拉角的数学换算就行。这意味着:

方案 MCU负载 代码复杂度 精度
原始数据+自研滤波 极高(需浮点运算) 复杂 依赖算法调参
DMP四元数输出 极低(仅解析42字节) 简单 官方算法,精度高

二、硬件连接与I2C驱动

2.1 引脚定义与接线

MPU6050引脚 51单片机引脚 说明
VCC 5V/3.3V 模块自带LDO,接5V即可
GND GND 共地
SCL P3.4 I2C时钟线
SDA P3.5 I2C数据线
INT P3.2(可选) DMP数据就绪中断

2.2 软件I2C驱动(Software_I2C.c)

这是整个DMP驱动的地基------所有寄存器读写都靠它。

c 复制代码
#include <reg52.h>
#include <intrins.h>

sbit SCL = P3^4;
sbit SDA = P3^5;

#define SlaveAddress 0xD0    // MPU6050写地址(0xD0 = 0x68 << 1)

// ========== I2C底层时序函数 ==========

void I2C_Delay(void) {
    unsigned char i;
    for(i = 0; i < 10; i++);  // 根据晶振调整延时
}

void I2C_Start(void) {
    SDA = 1; SCL = 1; I2C_Delay();
    SDA = 0; I2C_Delay();
    SCL = 0; I2C_Delay();
}

void I2C_Stop(void) {
    SDA = 0; SCL = 1; I2C_Delay();
    SDA = 1; I2C_Delay();
}

void I2C_SendACK(bit ack) {
    SDA = ack; SCL = 1; I2C_Delay();
    SCL = 0; I2C_Delay();
    SDA = 1;
}

bit I2C_WaitAck(void) {
    bit ack;
    SDA = 1; SCL = 1; I2C_Delay();
    ack = SDA; SCL = 0; I2C_Delay();
    return ack;
}

void I2C_SendByte(unsigned char dat) {
    unsigned char i;
    for(i = 0; i < 8; i++) {
        SDA = (dat & 0x80) >> 7;
        dat <<= 1; SCL = 1; I2C_Delay(); SCL = 0;
    }
    I2C_WaitAck();
}

unsigned char I2C_RecvByte(void) {
    unsigned char dat = 0, i;
    SDA = 1;
    for(i = 0; i < 8; i++) {
        SCL = 1; I2C_Delay();
        dat <<= 1; dat |= SDA; SCL = 0; I2C_Delay();
    }
    return dat;
}

// ========== MPU6050专用读写接口 ==========

void Single_WriteI2C(unsigned char reg, unsigned char dat) {
    I2C_Start();
    I2C_SendByte(SlaveAddress);
    I2C_SendByte(reg);
    I2C_SendByte(dat);
    I2C_Stop();
}

unsigned char Single_ReadI2C(unsigned char reg) {
    unsigned char dat;
    I2C_Start();
    I2C_SendByte(SlaveAddress);
    I2C_SendByte(reg);
    I2C_Start();
    I2C_SendByte(SlaveAddress + 1);
    dat = I2C_RecvByte();
    I2C_SendACK(1);
    I2C_Stop();
    return dat;
}

// 连续读取多个寄存器
void I2C_ReadBytes(unsigned char reg, unsigned char *buf, unsigned char len) {
    unsigned char i;
    I2C_Start();
    I2C_SendByte(SlaveAddress);
    I2C_SendByte(reg);
    I2C_Start();
    I2C_SendByte(SlaveAddress + 1);
    for(i = 0; i < len; i++) {
        buf[i] = I2C_RecvByte();
        I2C_SendACK(i == len - 1);
    }
    I2C_Stop();
}

三、DMP固件加载------最关键的一步

3.1 DMP固件数据

DMP固件来自Jeff Rowberg公开的MotionApps v2.0,需要将1929字节的固件分8个bank(每段256字节)写入MPU6050的内存中。这里只展示核心加载逻辑:

c 复制代码
// ========== DMP固件数据(MotionApps v2.0,Bank 0示例) ==========
code unsigned char dmpMemoryData[1929] = {
    // Bank 0 (256 bytes)
    0xFB, 0x00, 0x00, 0x3E, 0x00, 0x0B, 0x00, 0x36,
    0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x00,
    // ... (完整固件数据共1929字节,此处省略中间部分)
    // Bank 7 (最后一段)
    0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};

// ========== DMP配置更新数据 ==========
code unsigned char dmpUpdates[47] = {
    0x01, 0xB2, 0x02, 0xFF, 0xFF, 0x01, 0x90, 0x04,
    0x09, 0x23, 0xA1, 0x35, 0x01, 0x6A, 0x02, 0x06,
    0x00, 0x01, 0x60, 0x08, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x60, 0x04, 0x40, 0x00,
    0x00, 0x00, 0x01, 0x62, 0x02, 0x00, 0x00, 0x00,
    0x60, 0x04, 0x00, 0x40, 0x00, 0x00
};

3.2 DMP固件加载函数

c 复制代码
// 将固件数据写入MPU6050内存
unsigned char loadFirmware(void) {
    unsigned int dataNum = 0;
    unsigned char bank, addr;
    
    for(bank = 0; bank < 8; bank++) {
        // 选择bank
        Single_WriteI2C(0x6D, bank);
        
        // 设置写入起始地址
        addr = 0;
        Single_WriteI2C(0x6E, addr);
        
        // 每次写16字节
        unsigned char chunkSize = (bank == 7) ? 8 : 16;
        unsigned char i;
        for(i = 0; i < chunkSize; i++) {
            Single_WriteI2C(0x6F, dmpMemoryData[dataNum++]);
            addr += 1;
            Single_WriteI2C(0x6E, addr);
        }
    }
    return 1;  // 成功返回1
}

// 应用DMP配置更新
void applyUpdates(void) {
    unsigned char i = 0;
    while(i < sizeof(dmpUpdates)) {
        unsigned char bank = dmpUpdates[i++];
        unsigned char addr = dmpUpdates[i++];
        unsigned char length = dmpUpdates[i++];
        
        Single_WriteI2C(0x6D, bank);
        Single_WriteI2C(0x6E, addr);
        
        unsigned char j;
        for(j = 0; j < length; j++) {
            Single_WriteI2C(0x6F, dmpUpdates[i++]);
        }
    }
}

四、MPU6050+DMP完整初始化

c 复制代码
// ========== MPU6050寄存器地址定义 ==========
#define PWR_MGMT_1   0x6B
#define SMPLRT_DIV   0x19
#define CONFIG       0x1A
#define GYRO_CONFIG  0x1B
#define ACCEL_CONFIG 0x1C
#define USER_CTRL    0x6A
#define INT_ENABLE   0x38
#define FIFO_EN      0x23
#define INT_PIN_CFG  0x37

// ========== DMP初始化完整流程 ==========
void MPU6050_DMP_Init(void) {
    // Step 1: 解除休眠,选择时钟源
    Single_WriteI2C(PWR_MGMT_1, 0x80);  // 复位
    delay_ms(100);
    Single_WriteI2C(PWR_MGMT_1, 0x00);  // 唤醒,使用内部8MHz时钟
    delay_ms(200);
    
    // Step 2: 基础配置
    Single_WriteI2C(SMPLRT_DIV, 0x04);  // 采样率 = 1kHz / (1+4) = 200Hz
    Single_WriteI2C(CONFIG, 0x03);      // 低通滤波 44Hz
    
    // Step 3: 量程配置
    Single_WriteI2C(GYRO_CONFIG, 0x18);   // 陀螺仪 ±2000°/s
    Single_WriteI2C(ACCEL_CONFIG, 0x01); // 加速度计 ±4g
    
    // Step 4: 加载DMP固件
    loadFirmware();
    
    // Step 5: 使能DMP
    Single_WriteI2C(USER_CTRL, 0x80);    // 使能DMP
    delay_ms(50);
    
    // Step 6: 应用DMP配置
    applyUpdates();
    
    // Step 7: 配置FIFO
    Single_WriteI2C(FIFO_EN, 0x78);     // 使能陀螺仪+加速度计数据入FIFO
    
    // Step 8: 中断配置
    Single_WriteI2C(INT_PIN_CFG, 0x02); // INT引脚低电平有效
    Single_WriteI2C(INT_ENABLE, 0x01);  // 使能DMP中断
    
    Single_WriteI2C(0x6A, 0xC0);       // 重置DMP
    delay_ms(50);
    Single_WriteI2C(0x6A, 0x80);       // 重新使能DMP
    delay_ms(50);
}

参考代码 51单片机MPU6050DMP驱动 www.youwenfan.com/contentcsu/56113.html

五、四元数读取与欧拉角转换

5.1 DMP数据包结构(42字节)

DMP输出的FIFO数据包结构如下:

字节偏移 内容
0-3 四元数 W(Q0)
4-7 四元数 X(Q1)
8-11 四元数 Y(Q2)
12-15 四元数 Z(Q3)
16-17 陀螺仪 X
18-19 陀螺仪 Y
20-21 陀螺仪 Z
22-23 加速度计 X
24-25 加速度计 Y
26-27 加速度计 Z

5.2 四元数读取

c 复制代码
// DMP数据接收缓冲区
unsigned char dmpData[42];
float Q[4];  // 四元数: Q0, Q1, Q2, Q3

// 读取DMP数据包
void readDMPData(void) {
    I2C_ReadBytes(0x74, dmpData, 42);  // FIFO_R_W = 0x74
    
    // 解析四元数(16位有符号整数 → 浮点数)
    long qw = ((long)dmpData[0] << 8) | dmpData[1];
    long qx = ((long)dmpData[4] << 8) | dmpData[5];
    long qy = ((long)dmpData[8] << 8) | dmpData[9];
    long qz = ((long)dmpData[12] << 8) | dmpData[13];
    
    // 归一化到[-1, 1]范围(2^30是MPU6050的缩放因子)
    Q[0] = (float)qw / 1073741824.0f;
    Q[1] = (float)qx / 1073741824.0f;
    Q[2] = (float)qy / 1073741824.0f;
    Q[3] = (float)qz / 1073741824.0f;
}

5.3 四元数→欧拉角转换(核心公式)

c 复制代码
// 欧拉角结构体
typedef struct {
    float pitch;  // 俯仰角(抬头为正)
    float roll;   // 横滚角(右滚为正)
    float yaw;     // 航向角(顺时针为正)
} EulerAngle;

EulerAngle euler;

void quaternionToEuler(void) {
    // 俯仰角 pitch = arcsin(-2*(q1*q3 - q0*q2))
    euler.pitch = asin(-2.0f * (Q[1]*Q[3] - Q[0]*Q[2])) * 57.2957795f;
    
    // 横滚角 roll = atan2(2*(q1*q2 + q0*q3), q0*q0 + q1*q1 - q2*q2 - q3*q3)
    euler.roll = atan2(2.0f * (Q[1]*Q[2] + Q[0]*Q[3]), 
                     Q[0]*Q[0] + Q[1]*Q[1] - Q[2]*Q[2] - Q[3]*Q[3]) * 57.2957795f;
    
    // 航向角 yaw = atan2(2*(q1*q2 - q0*q3), q0*q0 + q1*q1 + q2*q2 - q3*q3)
    euler.yaw = atan2(2.0f * (Q[1]*Q[2] - Q[0]*Q[3]),
                    Q[0]*Q[0] + Q[1]*Q[1] + Q[2]*Q[2] - Q[3]*Q[3]) * 57.2957795f;
}

// 57.2957795f = 180/π,将弧度转换为角度

六、完整主程序示例

c 复制代码
#include <reg52.h>
#include <math.h>
#include <stdio.h>

// 串口初始化(11.0592MHz晶振,9600波特率)
void UART_Init(void) {
    TMOD |= 0x20;    // 定时器1工作方式2
    TH1 = 0xFD;        // 9600波特率
    TR1 = 1;
    SCON = 0x50;       // 串口工作方式1,允许接收
}

void UART_SendString(char *s) {
    while(*s) {
        SBUF = *s++;
        while(!TI); TI = 0;
    }
}

void delay_ms(unsigned int ms) {
    unsigned int i, j;
    for(i = 0; i < ms; i++)
        for(j = 0; j < 114; j++);
}

void main(void) {
    UART_Init();
    
    UART_SendString("MPU6050 DMP Initializing...\r\n");
    MPU6050_DMP_Init();
    UART_SendString("DMP Init Done!\r\n");
    
    delay_ms(500);  // 等待DMP稳定
    
    while(1) {
        readDMPData();       // 读取42字节DMP数据
        quaternionToEuler();  // 转换为欧拉角
        
        // 通过串口输出角度(简化版整数输出)
        char buf[64];
        sprintf(buf, "Pitch:%.1f Roll:%.1f Yaw:%.1f\r\n", 
                euler.pitch, euler.roll, euler.yaw);
        UART_SendString(buf);
        
        delay_ms(50);  // 约20Hz输出频率
    }
}

七、关键注意事项与排错指南

7.1 初始化顺序不能乱

DMP的初始化必须严格按照以下顺序,否则固件加载会失败:

复制代码
复位 → 唤醒 → 基础配置 → 加载固件 → 使能DMP → 应用配置 → FIFO配置 → 中断配置

7.2 常见问题速查

现象 原因 解决方法
串口无输出 I2C通信失败 检查SCL/SDA引脚连接,确认上拉电阻(4.7kΩ)
四元数全为0 DMP固件未加载成功 检查固件数据是否正确写入,确认0x6D/0x6E/0x6F寄存器操作
角度跳变 四元数归一化错误 确认除以的是1073741824.0f (2^30)而非16384.0f
航向角漂移 DMP本身无磁力计校准 DMP的yaw角会随时间漂移,如需精确航向需外接磁力计
角度数值不对 未乘57.3 四元数转欧拉角后必须×180/π才能转为角度

7.3 性能评估

在STC89C54RD+(11.0592MHz)上实测:

  • DMP初始化耗时:约300ms(含固件加载)
  • 单次角度读取:约2ms(I2C连续读42字节)
  • MCU占用率:< 5%(大部分时间在delay等待)
  • 角度精度:静态下Roll/Pitch ±0.5°,Yaw ±1°(有漂移)

八、总结

这套51单片机MPU6050 DMP驱动方案的核心价值在于把算力密集的姿态融合从51单片机转移到MPU6050芯片内部,让8位单片机也能轻松实现高精度姿态测量。

相关推荐
Deitymoon1 小时前
STM32——继电器
stm32·单片机·嵌入式硬件
hfdz_00421 小时前
无人机无刷电机(BLDC)无感六步换相与过零点检测
嵌入式硬件·无人机·硬件设计
恶魔泡泡糖2 小时前
stm32F103C8T6标准库外部中断的概念
stm32·单片机·嵌入式硬件
VBsemi-专注于MOSFET研发定制2 小时前
高端LED封装自动化产线功率MOSFET选型方案——精密、高效与可靠驱动系统设计指南
运维·单片机·自动化
LCG元3 小时前
STM32项目实战:基于STM32F103的智能台灯控制
stm32·单片机·嵌入式硬件
rjszcb4 小时前
mcu.之armv7 contex-M3/M4系列,时钟树,中断, cpu架构,上电启动过程(二)
单片机
姓刘的哦4 小时前
RK3568之I2C子系统(协议)
单片机·嵌入式硬件
咕噜咕噜啦啦5 小时前
一些单片机学习相关名词
单片机·嵌入式硬件
芋头莎莎5 小时前
ESP32 +VSCode与搭建开发环境教程
vscode·单片机