date: 2026-09-05

  • 二合一(6轴)
    • 加速度计
      • 各轴向的加速度
    • 陀螺仪
      • 绕轴旋转的角速度
  • 初始化 - > 读取原始数据 - > 解算 - > 得到欧拉角
  • I2C
  • 内含寄存器

一.初始化I2C1

![[Pasted image 20260905144418.png]]

PB7 – SDA
PB6 – SCL

二、keli5中配置模板文件

三、寄存器读写

  • 写寄存器
    • 同时发送寄存器的地址和值
i2c_send_bytes(0xd0,{0x1b,0x18})

mpu6050 从机地址 0xd0
后面跟着的是寄存器的地址和值

  • 读寄存器
    • 发送寄存器地址
    • 读取寄存器的值
    • 例如读取寄存器0x3b的值
i2c_send_bytes(0xd0,0x3b)
regval = i2c_receive_bytes (0xd0)

四、封装两个函数,读写寄存器。

  • 两个函数:
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c,   // &hi2c1
                                    uint16_t DevAddress,       // 从机地址(要左移1位!)
                                    uint16_t MemAddress,       // 寄存器地址
                                    uint16_t MemAddressSize,   // 寄存器地址宽度
                                    uint8_t *pData,            // 数据缓冲区
                                    uint16_t Size,             // 字节数
                                    uint32_t Timeout);         // 超时(ms)

HAL_StatusTypeDef HAL_I2C_Mem_Read(参数完全相同);
  • 其中,寄存器地址宽度 MemAddressSize,MPU6050 用 I2C_MEMADD_SIZE_8BIT(寄存器地址是 8 位;EEPROM 才用 16BIT)

  • 返回值HAL_OK 表示成功——务必用 if 检查它,I2C 没接好时它会返回 HAL_TIMEOUT,这是你排查硬件问题的第一手段。

  • 封装写入数据的函数:

/**
 * @brief 写单个寄存器
 * @param reg: 寄存器地址
 * @param value: 写入值
 * @return HAL 状态
 */
static HAL_StatusTypeDef MPU6050_WriteReg(uint8_t reg, uint8_t value)
{
    return HAL_I2C_Mem_Write(&MPU6050_I2C, MPU6050_ADDR, reg,
                             I2C_MEMADD_SIZE_8BIT, &value, 1, HAL_MAX_DELAY);
}
  • 其中:
// I2C 句柄配置宏(根据实际接线修改)
// 换 I2C 时只需改 MPU6050_I2C 一行:用句柄名(如 hi2c1),不是寄存器基址(如 I2C1)
# define MPU6050_I2C           hi2c1
// I2C 句柄声明(宏展开后为 extern I2C_HandleTypeDef hi2c1;,CubeMX 在 main.c 中定义)
extern I2C_HandleTypeDef MPU6050_I2C;

// 器件地址(AD0接地时7位地址0x68,HAL 使用8位地址需左移 → 0xD0)
# define MPU6050_ADDR          0xD0
  • 封装读数据的函数:
/**
 * @brief 读单个寄存器
 * @param reg: 寄存器地址
 * @return 读取值(失败返回0)
 */
static uint8_t MPU6050_ReadReg(uint8_t reg)
{
    uint8_t value = 0;
    HAL_I2C_Mem_Read(&MPU6050_I2C, MPU6050_ADDR, reg,
                     I2C_MEMADD_SIZE_8BIT, &value, 1, 100);
    return value;
}

五、MPU6050初始化函数封装

  • 初始化步骤:
    • 第 1 步:读 WHO_AM_I
    • 第 2 步:唤醒
      • MPU6050 上电默认是睡眠状态,寄存器能读写,但是传感器不采集,数据全是0
    • 第三步:采样率分频(含低通滤波 DLPF)
    • 第四步:配置量程
  • 详细步骤说明:
1.第一步,读WHO_AM_I
  • 思路:调用读取寄存器值的函数,读取0x75这个寄存器的值,如果返回0x68,则下一步,所以代码这样写:
if (MPU6050_ReadReg(0x75) != 0x68)
{
	return 0
}

这一步是确保和MPU6050通信正常,出现异常直接中断。
0x75这个寄存器的内容是0x68

第 2 步:唤醒芯片(写 PWR_MGMT_1)
  • 思路:向0x6B 这个寄存器写入0x00,让MPU6050芯片开始工作
  • 状态,8 个位各管一摊,最重要的两位:
bit:     7            6           5  4  3      2  1  0
       [RESET]    [SLEEP]       [ ][ ][TEMP]  [CLKSEL]
        软复位    睡眠开关                     时钟源选择

关键就是 bit6 = SLEEP
SLEEP = 1 → 睡眠(上电默认)
SLEEP = 0 → 正常工作
代码也就很简单了:

MPU6050_WriteReg(0x6B,0x00)
第三步:定采样率:
  • 采样率 = MPU6050 每秒更新多少次传感器数据
  • 后续算角度的时候会用到。
  • MPU6050 有一个寄存器:
SMPLRT_DIV
地址:0x19

它通过一个分频值来设置最终采样率。

实际采样率 = 1000Hz ÷ (1 + DIV)
  • 代码上只需要向0x19上写值就能调整采样率
  • 思路,传入期望采样率,计算出div的值,将DIV传入对应寄存器中即可,注意判断一下采样率。
		if(sample_rate_hz >1000 ) sample_rate_hz = 1000;
		if(sample_rate_hz <4) sample_rate_hz = 4;
		uint8_t div = (uint8_t)(1000/sample_rate_hz) - 1;
	    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, div);
  • 关于DLPF(数字低通滤波器)
    • MPU6050 的 CONFIG 寄存器地址是:
MPU6050_CONFIG = 0x1A

它主要用来设置 DLPF(数字低通滤波器)。寄存器中的bit2~bit0,不同的值对应不同的低通滤波带宽,数值越大,带宽越低,滤波越强,数据越平滑,但是响应速度更慢

  • DLPF 开启后基准采样率变成 1 kHz:
    所以这一步前面应该加一步开启DLPF,代码也很好写
	// 低通滤波 44Hz
    MPU6050_WriteReg(MPU6050_CONFIG, 0x03);
第四步 :量程选择:

MPU6050 有两个独立的量程:

加速度计:±2g / ±4g / ±8g / ±16g
陀螺仪:±250°/s / ±500°/s / ±1000°/s / ±2000°/s

对于加速度计:
ACCEL_CONFIG 寄存器:

地址:0x1C

其中 Bit4、Bit3 用来选择量程:

FS_SEL
Bit4 Bit3

00 → ±2g
01 → ±4g
10 → ±8g
11 → ±16g

<< 3 就是二进制整体向左移动 3 位
例如:
1 << 3
结果就是:

00000001
      ↓↓↓
00001000

本质上就是:左移几位,相当于乘以 2 的几次方。
可以通过这个方法来控制bit4和bit3 的值
代码如下:

MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, MPU6050_ACCEL_RANGE << 3);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, MPU6050_GYRO_RANGE << 3);

其中:

#define MPU6050_GYRO_CONFIG   0x1B    // 陀螺仪量程
#define MPU6050_ACCEL_CONFIG  0x1C    // 加速度计量程
// 量程配置(0/1/2/3)
//   加速度: 0=±2g   1=±4g    2=±8g     3=±16g
//   陀螺仪: 0=±250°/s  1=±500°/s  2=±1000°/s  3=±2000°/s
#define MPU6050_ACCEL_RANGE   0
#define MPU6050_GYRO_RANGE    3

更改量程时只需改宏定义这里即可。

六、MPU6050读取原始数据封装函数:

MPU6050 采好的数据不是放一个寄存器里,而是每个轴占 2 个连续寄存器,从 0x3B 开始一路排下去:

地址内容
0x3B / 0x3C加速度 X 高字节 / 低字节
0x3D / 0x3E加速度 Y 高 / 低
0x3F / 0x40加速度 Z 高 / 低
0x41 / 0x42温度 高 / 低
0x43 / 0x44陀螺仪 X 高 / 低
0x45 / 0x46陀螺仪 Y 高 / 低
0x47 / 0x48陀螺仪 Z 高 / 低

共 7 项 × 2 字节 = 14 个字节,地址连续。
I2C 有个机制:设定起始地址后连续读,芯片内部的“地址指针”会自动 +1
I2C 每次只能搬 1 个字节,所以芯片把它拆成“高 8 位在前、低 8 位在后”(大端)存放所以读来的8位数据,要手动拼成16位。
*注意:*加速度是有方向的(往左压是正、往右压是负),角速度也有正负(顺/逆时针)。所以拼出来的 16 位数必须按有符号数解释:

  • 同样的 0xFFFF:当 uint16_t 看是 65535,当 int16_t 看是 -1(补码)
  • 不写 (int16_t) 强转或变量声明成 uint16_t,往右转会得到 65535 这种荒谬数字
  • 所以要用int16_t
  • 最后使用指针将数据传输出去
void MPU6050_GetData(int16_t *ax, int16_t *ay, int16_t *az,
                     int16_t *gx, int16_t *gy, int16_t *gz)
{
    uint8_t buf[14];

    // 从 0x3B 连续读 14 字节:加速度6 + 温度2 + 陀螺仪6
    if (HAL_I2C_Mem_Read(&MPU6050_I2C, MPU6050_ADDR, MPU6050_ACCEL_XOUT_H,
                         I2C_MEMADD_SIZE_8BIT, buf, 14, 100) != HAL_OK)
        return;

    // 大端合并(高位在前),buf[6]/buf[7] 为温度,跳过
    if (ax) *ax = (int16_t)((buf[0] << 8) | buf[1]);
    if (ay) *ay = (int16_t)((buf[2] << 8) | buf[3]);
    if (az) *az = (int16_t)((buf[4] << 8) | buf[5]);
    if (gx) *gx = (int16_t)((buf[8] << 8) | buf[9]);
    if (gy) *gy = (int16_t)((buf[10] << 8) | buf[11]);
    if (gz) *gz = (int16_t)((buf[12] << 8) | buf[13]);
}
  • 对于读出来的数据就可以对其进行处理了。
  • 接下来就是解算的过程了

首先验证以上程序:

  • 连接好电路图,烧录程序。
/* USER CODE BEGIN 2 */
//初始化显示器和MPU6050
  OLED_Init();
  
    if (MPU6050_Init(125) != 1)
    {
      while (1)
      {
        OLED_ShowString(0,0,"MPU6050 Init Failed",OLED_6X8);
        if (HAL_I2C_IsDeviceReady(&hi2c1, 0xD0, 3, 100) == HAL_OK)
        
          {
            OLED_ShowString(0, 10, "MPU READY", OLED_6X8);
            uint8_t id = 0;
            
            HAL_I2C_Mem_Read(&hi2c1, 0xD0, MPU6050_WHO_AM_I,I2C_MEMADD_SIZE_8BIT, &id, 1, 100);
            OLED_ShowString(0, 20, "WHO_AM_I:", OLED_6X8);
            OLED_ShowHexNum(54, 20, id, 2, OLED_6X8);
            
          }
          else
          {
            OLED_ShowString(0, 10, "MPU NOT FOUND", OLED_6X8);
          }
            OLED_Update();
            HAL_Delay(100);
        // 如果初始化失败,可以在这里添加错误处理代码
      }
    }

  /* USER CODE END 2 */
  
  /* Infinite loop */
  
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    //函数读取原始数据
    int16_t ax,ay,az,gx,gy,gz;
    MPU6050_GetData ( &ax,&ay,&az,&gx,&gy,&gz);
    OLED_ShowString(0,0,name[0],OLED_6X8);
    OLED_ShowSignedNum (16,0,ax,6,OLED_6X8);
    OLED_ShowString(0,9,name[1],OLED_6X8);
    OLED_ShowSignedNum (16,9,ay,6,OLED_6X8);
    OLED_ShowString(0,18,name[2],OLED_6X8);
    OLED_ShowSignedNum (16,18,az,6,OLED_6X8);
    OLED_ShowString(0,27,name[3],OLED_6X8);
    OLED_ShowSignedNum (16,27,gx,6,OLED_6X8);
    OLED_ShowString(0,36,name[4],OLED_6X8);
    OLED_ShowSignedNum (16,36,gy,6,OLED_6X8);
    OLED_ShowString(0,45,name[5],OLED_6X8);
    OLED_ShowSignedNum (16,45,gz,6,OLED_6X8);
    OLED_Update();
    HAL_Delay(100);
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }

  /* USER CODE END 3 */
  • 此次实验最重要的是,发现了问题并排查了问题
  • 我用的芯片,MPU6050_WHO_AM_I这个寄存器中存放的数据是0x74,所以一直没反应刚开始,加入了错误报告的算法,下次如果有这个问题就能直接排查出来是寄存器问题还是接线问题,这是个很好的编程习惯,后续应该多保持,多思考可能的问题,在程序中提前预防。

七,MPU6050数据解算函数封装

介绍几个名词

  • roll ,横滚,意思是左右压的场景下,绕x轴旋转,角度由ay,az来计算
  • pitch,俯仰,前后抬,绕y轴旋转,ax,az计算
  • yaw,偏航,水平转圈,绕z轴旋转,加速度计无法计算,航向变化
  • 这三个角度就是欧拉角
  • MPU6050的芯片某个角有个小圆点,通过这个小圆点可以建立坐标轴,如图:
    ![[Pasted image 20260906134817.png]]
    通过陀螺仪和加速度计都能计算出欧拉角,但是各有优劣,陀螺仪的漂移问题比较严重,随着时间的推移,计算误差会越来越大,加速度计的噪声问题比较大,后续会介绍一种方法来解决这些问题
①加速度计静态倾角

静止时,加速度计读到的就是重力在各轴上的投影。芯片水平放桌上:az=+1g,ax=ay=0。板子一倾斜,其他轴上也就有了重力的分量,通过三个轴的比例关系就能计算出倾角

  • 水平放时,重力全在z轴,所以az≈16384(+1g),倾角 0°
  • 竖起来90°时,重力全在水平轴上,倾角 90°
    公式如下
roll  = atan2(ay, az)                                  ← 横滚(左右压)
pitch = atan2(-ax, sqrt(ay² + az²))                    ← 俯仰(抬头/点头)

row角度推导关系:pitch同理
![[Pasted image 20260906132223.png]]
以上仅针对保持绕轴不动的情况,比如当计算roll角度时,如果x轴同步旋转,那么几何关系就会变化,但是其本质其实是yoz这个平面上的g分量再分解到y轴和z轴,所以只需要改变一项,即:

g_yoz * cos(θ) = gz
g_yoz * sin(θ) = gy
gy / gz = tan(θ)

可以看到推导出来显示,roll角的计算与x轴是否跟着旋转没有关系,公式依旧成立
针对pitch角的推导:

  • 理想状态下,绕y轴旋转时,倾角是xz方向上分量的比例反tan函数的值,即:

[θ=arctan⁡axaz][ \boxed{ \theta=\arctan\frac{a_x}{a_z} } ][θ=arctanazax]

  • 当绕y轴的同时,也绕x轴,此时
② 陀螺仪解算:
  • 假设一个初始偏航角yaw,同时测得gz ,单位°/s,假设gz不变的情况下,有以下公式:
    yaw[k]=yaw[k−1]+gz∗dtyaw[k] = yaw[k-1] + gz * dtyaw[k]=yaw[k1]+gzdt
    很好理解,其实这就是一个对于时间的积分,当gz不再不变的时候,每dt时间,测量gz,再计算和,即可测得该时刻的偏航角yaw,容易知道,dt越短,结果越准确
    同理,有:
    pitch[k]=pitch[k−1]+gx∗dtroll[k]=roll[k−1]+gy∗dt \begin{aligned} pitch[k] = pitch[k-1] + gx * dt \\ \\ roll[k] = roll[k-1] + gy * dt \end{aligned} pitch[k]=pitch[k1]+gxdtroll[k]=roll[k1]+gydt
    其中gx,gy,gz,分别是绕xyz轴旋转的角速度
    dt 不是采样周期,而是两次积分之间的真实间隔(由循环能跑多快决定,必须用 HAL_GetTick 实测)。详见第九节。
  • 有了公式就能开始写代码了:
float MPU6050_DataToAcc(int16_t raw)              
{
    return raw / 16384.0f * (1 << MPU6050_ACCEL_RANGE);    // 加速度
}
/**
    * @brief 通过陀螺仪的原始数据结果计算真实的角速度
    * @param  通过指针传入原始数据
    * @retval 调用之后,原始数据的gx,gy,gz的值会被处理为真实的角速度值。
*/
float MPU6050_DataToGyro(int16_t raw)

{

    return raw / 131.0f * (1 << MPU6050_GYRO_RANGE);    // 角速度

}

/**
    * @brief 通过陀螺仪的测量数据结果计算欧拉角
    * @param  传入陀螺仪的原始数据
    * @retval 调用之后,pitch_gyro,roll_gyro,yaw全局变量会直接有效
*/
void MPU6050_gProc(int16_t gx, int16_t gy, int16_t gz)
{
    // 计算欧拉角(单位:°)
    pitch_gyro += MPU6050_DataToGyro(gx) * dt;
    roll_gyro  += MPU6050_DataToGyro(gy) * dt;
    yaw   += MPU6050_DataToGyro(gz) * dt;
}

/**
    * @brief 通过加速度的测量数据结果计算欧拉角
    * @param  传入加速度的原始数据
    * @retval 调用之后,pitch_acc,roll_acc全局变量会直接有效
*/
void MPU6050_aProc(int16_t ax, int16_t ay, int16_t az)

{
    // 计算欧拉角(单位:°)
    pitch_acc = atan2(-ax, sqrt(ay * ay + az * az)) * 180 / 3.1415926f;
    roll_acc  = atan2(ay, az) * 180 / 3.1415926f;
}

⚠️ 上面 gProc(以及它的公式)里 gx/gy 写反了——这是当时的翻车版本:pitch 应来自 gy、roll 应来自 gx。正确版本见第八节。
写了两个计算欧拉角的函数,分别通过陀螺仪和加速度计来计算。
主函数中这样调用:

while (1)

  {
    //函数读取原始数据
    int16_t ax,ay,az,gx,gy,gz;
    MPU6050_GetData ( &ax,&ay,&az,&gx,&gy,&gz);
    //通过两个函数得到欧拉角
    MPU6050_gProc(gx, gy, gz);  // 积分计算欧拉角(单位:°)
    MPU6050_aProc(ax, ay, az);  // 通过加速度计算
    
    //将欧拉角打印出来
    OLED_ShowString(0, 0, "Pitch_Gyro:", OLED_6X8);
    OLED_ShowFloatNum(80, 0, pitch_gyro, 3, 3, OLED_6X8);
    OLED_ShowString(0, 9, "Roll_Gyro:", OLED_6X8);
    OLED_ShowFloatNum(80, 9, roll_gyro, 3, 3, OLED_6X8);
    OLED_ShowString(0, 18, "Yaw:", OLED_6X8);
    OLED_ShowFloatNum(80, 18, yaw, 3, 3, OLED_6X8);
    OLED_ShowString(0, 27, "Pitch_Acc:", OLED_6X8);
    OLED_ShowFloatNum(80, 27, pitch_acc, 3, 3, OLED_6X8);
    OLED_ShowString(0, 36, "Roll_Acc:", OLED_6X8);
    OLED_ShowFloatNum(80, 36, roll_acc, 3, 3, OLED_6X8);
    OLED_Update();
    HAL_Delay(100);

初始化省略了没写出来。
实验现象是:Pitch_Acc,Roll_Acc基本上没有什么变化,会有跳变但是整体在0,1周围,Pitch_Gyro,roll_gyro,yaw的变化特别明显,一直在增加,即使我什么也不动。

补充:
积分公式里的 dt = 循环周期,OLED 刷屏会把循环周期撑大,而 dt 越大、混叠误差越大。 也就是说,从传感器上一次采样,到这一次采样之间的间隔过大,导致数据丢失,计算误差过大,所以说,需要将传感器与OLED的任务分开进行。分开细节详见:[[mpu6050#九、裸机多任务:]]

八、零偏校准和互补滤波

零偏校准:
- 原理:上电静止时,陀螺仪的读数理应全 0。但实际 gx≈8°/s,这就是零偏。采集几百帧取平均,得到每个轴的零偏值,之后每帧读数减掉它。
- 代码思路加入校准函数:
float gyro_offset_x, gyro_offset_y, gyro_offset_z;

void MPU6050_CalibrateOffset(uint16_t samples)
{
    int32_t sum_x = 0, sum_y = 0, sum_z = 0;
    int16_t gx, gy, gz;
    
    for (uint16_t i = 0; i < samples; i++)
    {
        MPU6050_GetData(NULL, NULL, NULL, &gx, &gy, &gz);
        sum_x += gx;
        sum_y += gy;
        sum_z += gz;
        HAL_Delay(2);  // 等一帧
    }
    
    // 存为 °/s(与 gProc 中 DataToGyro 的单位一致)
    gyro_offset_x = MPU6050_DataToGyro((int16_t)(sum_x / samples));
    gyro_offset_y = MPU6050_DataToGyro((int16_t)(sum_y / samples));
    gyro_offset_z = MPU6050_DataToGyro((int16_t)(sum_z / samples));
}
  • 调用时机:main.c 里 MPU6050_Init 之后、主循环之前。板子必须静止不动,放桌上别碰。
  • 调用完后,在gproc函数中,使用gyro_offset_x, gyro_offset_y, gyro_offset_z,在积分前减去零偏,使得零偏在积分器的输入端就被消除了,角度根本不会累积漂移。
void MPU6050_gProc(int16_t gx, int16_t gy, int16_t gz)

{
    // 计算欧拉角(单位:°)
        pitch_gyro += (MPU6050_DataToGyro(gy) - gyro_offset_y) * dt;   // gy → pitch(绕Y轴)
        roll_gyro  += (MPU6050_DataToGyro(gx) - gyro_offset_x) * dt;   // gx → roll(绕X轴)
        yaw  += (MPU6050_DataToGyro(gz) - gyro_offset_z) * dt;

}
  • 加入零偏校准后,陀螺仪解算出来的数据明显不一样了,误差也没有那么大了,这时就发现另一个问题了:
    • 当我移动MPU6050的时候,加速度计解算出来的roll和pitch变化十分剧烈,变化到七八十的时候,陀螺仪计算出来的roll和pitch却只有二三十。这就引出了加速度计的缺点:
      • 加速度计测算出来的不只有重力加速度,还包括运动时的线性加速度 ,所以当MPU6050运动时,角度变化会特别大。
  • 由此,引出互补滤波。
互补滤波:
  • 核心就一句话:陀螺仪负责短期的瞬时运动,加速度计负责长期的稳定。根据前面二者的计算可以得到陀螺仪能够很好检测出瞬时的运动带来的变化,但是因为零漂的影响下,长期会漂,脱离正轨;而加速度计长期稳定,对于瞬时的运动会计算外力,所以两者频域互补(陀螺补高频、加速度计补低频)。
  • 公式:
    • angle = α·(angle + ω·dt) + (1-α)·angle_acc,α=0.98 → 98% 信任陀螺。
    • 权重可以调整
  • 缺陷:yaw角依旧没法纠正,原因在于加速度计计算不了yaw角。
  • 代码:
void MPU6050_complementaryFilter(void)

{
    // 互补滤波融合陀螺仪和加速度计数据

    float alpha = 0.98f; // 滤波系数,0~1之间,越接近1越依赖陀螺仪

    pitch_gyro = alpha * pitch_gyro + (1 - alpha) * pitch_acc;
    roll_gyro  = alpha * roll_gyro  + (1 - alpha) * roll_acc;

}

九、裸机多任务:

*分频调度 *: 这里只有两个任务,所以用轮询即可,if判断即可:

// 采样/积分每轮都做(高频),OLED 显示每 100ms 做一次(低频)——两者解耦
uint32_t last_oled = 0;              // ★ 必须放在 while 外面

 while (1)

  {
    //函数读取原始数据
    int16_t ax,ay,az,gx,gy,gz;
    MPU6050_GetData ( &ax,&ay,&az,&gx,&gy,&gz);
    //通过两个函数得到欧拉角
    MPU6050_gProc(gx, gy, gz);  // 积分计算欧拉角(单位:°)
    MPU6050_aProc(ax, ay, az);  // 通过加速度计算
    MPU6050_complementaryFilter();  // 互补滤波融合(每轮都调)
    
    if (HAL_GetTick() - last_oled >= 100)    // 每100ms刷一次 OLED

    {
      //将欧拉角打印出来
      last_oled = HAL_GetTick();
      OLED_ShowString(0, 0, "Pitch_Gyro:", OLED_6X8);
      OLED_ShowFloatNum(80, 0, pitch_gyro, 3, 3, OLED_6X8);
      OLED_ShowString(0, 9, "Roll_Gyro:", OLED_6X8);
      OLED_ShowFloatNum(80, 9, roll_gyro, 3, 3, OLED_6X8);
      OLED_ShowString(0, 18, "Yaw:", OLED_6X8);
      OLED_ShowFloatNum(80, 18, yaw, 3, 3, OLED_6X8);
      OLED_ShowString(0, 27, "Pitch_Acc:", OLED_6X8);
      OLED_ShowFloatNum(80, 27, pitch_acc, 3, 3, OLED_6X8);
      OLED_ShowString(0, 36, "Roll_Acc:", OLED_6X8);
      OLED_ShowFloatNum(80, 36, roll_acc, 3, 3, OLED_6X8);
      OLED_Update();
    }

注意:pitch_gyro/roll_gyro 经互补滤波后,装的是融合值(不再是纯陀螺值),标签沿用旧名只是为了省事。

除此之外,还有一点:

欧拉角积分:dt 的正确理解
核心

角度 = Σ (角速度 × dt)
dt = 这一步积分真实覆盖的时间
= 从「上一次积分」到「这一次积分」实际过了多久

要点
  1. dt 不是"采样周期",是"积分间隔"(两次积分之间的真实间隔)。
    两者相等 ⇔ 你"每采样一次就积一次"。(这是特例,不是通例)
  2. “dt 越小越准” 对 —— 但那个"小"必须是真实的小(靠循环跑得快),
    而不是随手填一个小数字。填的 dt 撒了大谎,转得越久漂得越狠。
  3. dt 必须实测(HAL_GetTick),不能写死常量。因为循环有抖动,
    真实间隔每帧都在变(7/8/9ms…),只有实测才跟得上。
黎曼和的定义

∫ω dt ≈ Σ(ω × dt) —— 这个近似只在 dt = 每一步的真实宽度时成立。
dt 填错 = 每步宽度标错 = 总和按比例缩放。

正确写法(实测 dt)
static uint32_t last_tick = 0;

void MPU6050_gProc(int16_t gx, int16_t gy, int16_t gz)
{
    uint32_t now = HAL_GetTick();
    float dt = (last_tick == 0) ? 0.008f : (now - last_tick) / 1000.0f;
    last_tick = now;
    if (dt > 0.5f) dt = 0.008f;   // 只防异常;dt=0 让它加 0 即可

    pitch_gyro += (MPU6050_DataToGyro(gy) - gyro_offset_y) * dt;
    roll_gyro  += (MPU6050_DataToGyro(gx) - gyro_offset_x) * dt;
    yaw        += (MPU6050_DataToGyro(gz) - gyro_offset_z) * dt;
}
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐