欢迎光临
我们一直在努力

STM32实战案例:基于MPU6050的六轴姿态解算与OLED显示

文章目录

    • 一、项目概述
      • 1.1 硬件清单
      • 1.2 项目整体流程
    • 二、硬件接线说明
      • 2.1 STM32与MPU6050接线
      • 2.2 STM32与OLED模块接线
      • 2.3 STM32与USB转TTL接线(下载程序)
    • 三、STM32CubeMX工程配置
      • 3.1 新建工程
      • 3.2 配置系统时钟
      • 3.3 配置I2C接口
      • 3.4 配置串口(可选,用于调试)
      • 3.5 配置工程参数并生成代码
    • 四、代码编写(详细版)
      • 4.1 创建代码文件清单
      • 4.2 编写MPU6050驱动(mpu6050.h / mpu6050.c)
        • 4.2.1 mpu6050.h
        • 4.2.2 mpu6050.c
      • 4.3 编写OLED驱动(oled.h / oled.c)
        • 4.3.1 oled.h
        • 4.3.2 oled.c
      • 4.4 编写卡尔曼滤波算法(kalman.h / kalman.c)
        • 4.4.1 kalman.h
        • 4.4.2 kalman.c
      • 4.5 编写主函数(main.c)
    • 五、代码编译与下载
      • 5.1 工程配置
      • 5.2 编译代码
      • 5.3 下载程序
    • 六、硬件测试与调试
      • 6.1 硬件检查
      • 6.2 功能测试
    • 七、进阶优化(可选)
      • 总结

一、项目概述

本项目以STM32F103C8T6最小系统板为核心,驱动MPU6050六轴传感器采集加速度和角速度数据,通过卡尔曼滤波算法完成姿态解算(得到偏航角、俯仰角、横滚角),并将解算后的姿态数据实时显示在0.96寸I2C接口的OLED屏幕上。整个教程面向零基础小白,从硬件接线到代码编写、编译下载,每一步都详细说明,确保能够实际落地。

1.1 硬件清单

  • 主控板:STM32F103C8T6最小系统板
  • 传感器:MPU6050模块(I2C接口)
  • 显示模块:0.96寸OLED模块(I2C接口)
  • 辅助配件:杜邦线若干、USB转TTL模块(用于下载程序)、5V电源(或USB供电)
  • 软件工具:Keil5 MDK(版本V5.36及以上)、STM32CubeMX(版本6.0及以上)、串口助手(可选)

1.2 项目整体流程

#mermaid-svg-lHpRwkBDYFuKk6Ko{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lHpRwkBDYFuKk6Ko .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lHpRwkBDYFuKk6Ko .error-icon{fill:#552222;}#mermaid-svg-lHpRwkBDYFuKk6Ko .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lHpRwkBDYFuKk6Ko .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lHpRwkBDYFuKk6Ko .marker.cross{stroke:#333333;}#mermaid-svg-lHpRwkBDYFuKk6Ko svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lHpRwkBDYFuKk6Ko p{margin:0;}#mermaid-svg-lHpRwkBDYFuKk6Ko .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-lHpRwkBDYFuKk6Ko .cluster-label text{fill:#333;}#mermaid-svg-lHpRwkBDYFuKk6Ko .cluster-label span{color:#333;}#mermaid-svg-lHpRwkBDYFuKk6Ko .cluster-label span p{background-color:transparent;}#mermaid-svg-lHpRwkBDYFuKk6Ko .label text,#mermaid-svg-lHpRwkBDYFuKk6Ko span{fill:#333;color:#333;}#mermaid-svg-lHpRwkBDYFuKk6Ko .node rect,#mermaid-svg-lHpRwkBDYFuKk6Ko .node circle,#mermaid-svg-lHpRwkBDYFuKk6Ko .node ellipse,#mermaid-svg-lHpRwkBDYFuKk6Ko .node polygon,#mermaid-svg-lHpRwkBDYFuKk6Ko .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lHpRwkBDYFuKk6Ko .rough-node .label text,#mermaid-svg-lHpRwkBDYFuKk6Ko .node .label text,#mermaid-svg-lHpRwkBDYFuKk6Ko .image-shape .label,#mermaid-svg-lHpRwkBDYFuKk6Ko .icon-shape .label{text-anchor:middle;}#mermaid-svg-lHpRwkBDYFuKk6Ko .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lHpRwkBDYFuKk6Ko .rough-node .label,#mermaid-svg-lHpRwkBDYFuKk6Ko .node .label,#mermaid-svg-lHpRwkBDYFuKk6Ko .image-shape .label,#mermaid-svg-lHpRwkBDYFuKk6Ko .icon-shape .label{text-align:center;}#mermaid-svg-lHpRwkBDYFuKk6Ko .node.clickable{cursor:pointer;}#mermaid-svg-lHpRwkBDYFuKk6Ko .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lHpRwkBDYFuKk6Ko .arrowheadPath{fill:#333333;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lHpRwkBDYFuKk6Ko .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lHpRwkBDYFuKk6Ko .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lHpRwkBDYFuKk6Ko .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lHpRwkBDYFuKk6Ko .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lHpRwkBDYFuKk6Ko .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lHpRwkBDYFuKk6Ko .cluster text{fill:#333;}#mermaid-svg-lHpRwkBDYFuKk6Ko .cluster span{color:#333;}#mermaid-svg-lHpRwkBDYFuKk6Ko div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lHpRwkBDYFuKk6Ko .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lHpRwkBDYFuKk6Ko rect.text{fill:none;stroke-width:0;}#mermaid-svg-lHpRwkBDYFuKk6Ko .icon-shape,#mermaid-svg-lHpRwkBDYFuKk6Ko .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lHpRwkBDYFuKk6Ko .icon-shape p,#mermaid-svg-lHpRwkBDYFuKk6Ko .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lHpRwkBDYFuKk6Ko .icon-shape rect,#mermaid-svg-lHpRwkBDYFuKk6Ko .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lHpRwkBDYFuKk6Ko .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lHpRwkBDYFuKk6Ko .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lHpRwkBDYFuKk6Ko :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-lHpRwkBDYFuKk6Ko .nodeStyle>*{fill:#2c3e50!important;color:#ffffff!important;font-size:14px!important;font-family:Microsoft YaHei!important;padding:8px!important;stroke:#3498db!important;stroke-width:2px!important;}#mermaid-svg-lHpRwkBDYFuKk6Ko .nodeStyle span{fill:#2c3e50!important;color:#ffffff!important;font-size:14px!important;font-family:Microsoft YaHei!important;padding:8px!important;stroke:#3498db!important;stroke-width:2px!important;}#mermaid-svg-lHpRwkBDYFuKk6Ko .nodeStyle tspan{fill:#ffffff!important;}

硬件接线

STM32CubeMX配置工程

Keil工程搭建与代码编写

MPU6050驱动开发

OLED驱动开发

卡尔曼滤波姿态解算

传感器数据采集

数据格式转换

OLED数据显示

程序编译下载

硬件测试与调试

二、硬件接线说明

2.1 STM32与MPU6050接线

STM32F103C8T6MPU6050模块说明
3.3V VCC 供电(禁止接5V,会烧毁模块)
GND GND 共地
PB6 SCL I2C时钟线
PB7 SDA I2C数据线

2.2 STM32与OLED模块接线

STM32F103C8T6OLED模块说明
3.3V VCC 供电
GND GND 共地
PB6 SCL 与MPU6050共用I2C时钟线
PB7 SDA 与MPU6050共用I2C数据线

2.3 STM32与USB转TTL接线(下载程序)

STM32F103C8T6USB转TTL说明
PA9 TXD 串口发送
PA10 RXD 串口接收
GND GND 共地
5V 5V 可选,给STM32供电

三、STM32CubeMX工程配置

3.1 新建工程

  • 打开STM32CubeMX,点击“Access to MCU Selector”
  • 在搜索框输入“STM32F103C8T6”,选择对应型号,点击“Start Project”
  • 弹出“Project Manager”弹窗,点击“Cancel”暂不配置
  • 3.2 配置系统时钟

  • 点击左侧“System Core” -> “RCC”
  • 右侧“High Speed Clock (HSE)”选择“Crystal/Ceramic Resonator”(外部晶振)
  • 点击顶部“Clock Configuration”
  • 将“HCLK”设置为72MHz(通过配置PLL倍频:PLLSRC选择HSE,PLLMUL选择x9)
  • 确认APB1时钟为36MHz,APB2时钟为72MHz
  • 3.3 配置I2C接口

  • 点击左侧“Connectivity” -> “I2C1”
  • 右侧“Mode”选择“I2C Master”(I2C主机模式)
  • “I2C Speed Mode”选择“Fast Mode”(快速模式,400kHz)
  • “GPIO Settings”中确认PB6(SCL)、PB7(SDA)为“I2C1_SCL”、“I2C1_SDA”,上下拉模式选择“Pull-up”(上拉)
  • 3.4 配置串口(可选,用于调试)

  • 点击左侧“Connectivity” -> “USART1”
  • 右侧“Mode”选择“Asynchronous”(异步模式)
  • 确认PA9(TX)、PA10(RX)为对应的串口引脚
  • 3.5 配置工程参数并生成代码

  • 点击顶部“Project Manager”
  • “Project Name”输入“MPU6050_OLED_Attitude”
  • “Project Path”选择保存路径(不要有中文和空格)
  • “Toolchain/IDE”选择“MDK-ARM V5”
  • 点击“Code Generator”,勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”
  • 点击“Generate Code”,等待生成完成后点击“Open Project”打开Keil工程
  • 四、代码编写(详细版)

    4.1 创建代码文件清单

  • mpu6050.h / mpu6050.c(MPU6050驱动)
  • oled.h / oled.c(OLED驱动)
  • kalman.h / kalman.c(卡尔曼滤波算法)
  • main.c(主函数,整合所有功能)
  • 4.2 编写MPU6050驱动(mpu6050.h / mpu6050.c)

    4.2.1 mpu6050.h

    #ifndef __MPU6050_H
    #define __MPU6050_H

    #include "stm32f1xx_hal.h"

    // MPU6050设备地址(AD0接GND时为0x68,接VCC时为0x69)
    #define MPU6050_ADDR 0x68 << 1 // I2C地址需要左移一位

    // MPU6050寄存器地址
    #define MPU6050_PWR_MGMT_1 0x6B // 电源管理寄存器1
    #define MPU6050_SMPLRT_DIV 0x19 // 采样率分频寄存器
    #define MPU6050_CONFIG 0x1A // 配置寄存器
    #define MPU6050_GYRO_CONFIG 0x1B // 陀螺仪配置寄存器
    #define MPU6050_ACCEL_CONFIG 0x1C // 加速度计配置寄存器
    #define MPU6050_ACCEL_XOUT_H 0x3B // 加速度计X轴高8位
    #define MPU6050_ACCEL_YOUT_H 0x3D // 加速度计Y轴高8位
    #define MPU6050_ACCEL_ZOUT_H 0x3F // 加速度计Z轴高8位
    #define MPU6050_TEMP_OUT_H 0x41 // 温度高8位
    #define MPU6050_GYRO_XOUT_H 0x43 // 陀螺仪X轴高8位
    #define MPU6050_GYRO_YOUT_H 0x45 // 陀螺仪Y轴高8位
    #define MPU6050_GYRO_ZOUT_H 0x47 // 陀螺仪Z轴高8位

    // 数据结构体
    typedef struct {
    int16_t Accel_X; // 加速度计X轴原始值
    int16_t Accel_Y; // 加速度计Y轴原始值
    int16_t Accel_Z; // 加速度计Z轴原始值
    int16_t Gyro_X; // 陀螺仪X轴原始值
    int16_t Gyro_Y; // 陀螺仪Y轴原始值
    int16_t Gyro_Z; // 陀螺仪Z轴原始值
    float Accel_X_mg; // 加速度计X轴实际值(mg)
    float Accel_Y_mg; // 加速度计Y轴实际值(mg)
    float Accel_Z_mg; // 加速度计Z轴实际值(mg)
    float Gyro_X_dps; // 陀螺仪X轴实际值(°/s)
    float Gyro_Y_dps; // 陀螺仪Y轴实际值(°/s)
    float Gyro_Z_dps; // 陀螺仪Z轴实际值(°/s)
    float Temperature; // 温度值(℃)
    } MPU6050_DataDef;

    // 函数声明
    HAL_StatusTypeDef MPU6050_Init(I2C_HandleTypeDef *hi2c); // MPU6050初始化
    HAL_StatusTypeDef MPU6050_Write_Reg(uint8_t reg_addr, uint8_t data); // 写寄存器
    HAL_StatusTypeDef MPU6050_Read_Reg(uint8_t reg_addr, uint8_t *data); // 读寄存器
    void MPU6050_Read_All_Data(MPU6050_DataDef *mpu_data); // 读取所有数据
    void MPU6050_Convert_Data(MPU6050_DataDef *mpu_data); // 原始数据转换为实际值

    #endif

    4.2.2 mpu6050.c

    #include "mpu6050.h"

    extern I2C_HandleTypeDef hi2c1; // 声明I2C1句柄(由CubeMX生成)

    /**
    * @brief MPU6050初始化
    * @param hi2c: I2C句柄指针
    * @retval HAL状态
    */

    HAL_StatusTypeDef MPU6050_Init(I2C_HandleTypeDef *hi2c)
    {
    uint8_t temp_data = 0;

    // 1. 唤醒MPU6050(默认休眠,需要解除)
    temp_data = 0x00; // 将PWR_MGMT_1寄存器的BIT7置0,解除休眠
    if(HAL_I2C_Mem_Write(hi2c, MPU6050_ADDR, MPU6050_PWR_MGMT_1, I2C_MEMADD_SIZE_8BIT, &temp_data, 1, 100) != HAL_OK)
    {
    return HAL_ERROR;
    }

    // 2. 设置采样率分频器(采样率=8kHz/(1+SMPLRT_DIV))
    temp_data = 0x07; // 采样率=8kHz/(1+7)=1kHz
    if(HAL_I2C_Mem_Write(hi2c, MPU6050_ADDR, MPU6050_SMPLRT_DIV, I2C_MEMADD_SIZE_8BIT, &temp_data, 1, 100) != HAL_OK)
    {
    return HAL_ERROR;
    }

    // 3. 设置低通滤波器(CONFIG寄存器)
    temp_data = 0x06; // 低通滤波频率=5Hz
    if(HAL_I2C_Mem_Write(hi2c, MPU6050_ADDR, MPU6050_CONFIG, I2C_MEMADD_SIZE_8BIT, &temp_data, 1, 100) != HAL_OK)
    {
    return HAL_ERROR;
    }

    // 4. 设置陀螺仪量程(GYRO_CONFIG寄存器)
    temp_data = 0x00; // ±250°/s(量程最小,精度最高)
    if(HAL_I2C_Mem_Write(hi2c, MPU6050_ADDR, MPU6050_GYRO_CONFIG, I2C_MEMADD_SIZE_8BIT, &temp_data, 1, 100) != HAL_OK)
    {
    return HAL_ERROR;
    }

    // 5. 设置加速度计量程(ACCEL_CONFIG寄存器)
    temp_data = 0x00; // ±2g(量程最小,精度最高)
    if(HAL_I2C_Mem_Write(hi2c, MPU6050_ADDR, MPU6050_ACCEL_CONFIG, I2C_MEMADD_SIZE_8BIT, &temp_data, 1, 100) != HAL_OK)
    {
    return HAL_ERROR;
    }

    return HAL_OK;
    }

    /**
    * @brief 向MPU6050寄存器写数据
    * @param reg_addr: 寄存器地址
    * @param data: 要写入的数据
    * @retval HAL状态
    */

    HAL_StatusTypeDef MPU6050_Write_Reg(uint8_t reg_addr, uint8_t data)
    {
    return HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, reg_addr, I2C_MEMADD_SIZE_8BIT, &data, 1, 100);
    }

    /**
    * @brief 从MPU6050寄存器读数据
    * @param reg_addr: 寄存器地址
    * @param data: 存储读取数据的指针
    * @retval HAL状态
    */

    HAL_StatusTypeDef MPU6050_Read_Reg(uint8_t reg_addr, uint8_t *data)
    {
    return HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, reg_addr, I2C_MEMADD_SIZE_8BIT, data, 1, 100);
    }

    /**
    * @brief 读取MPU6050所有原始数据
    * @param mpu_data: 存储数据的结构体指针
    * @retval 无
    */

    void MPU6050_Read_All_Data(MPU6050_DataDef *mpu_data)
    {
    uint8_t temp_buf[14] = {0}; // 存储14个字节的原始数据

    // 从ACCEL_XOUT_H开始读取14个字节数据
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, MPU6050_ACCEL_XOUT_H, I2C_MEMADD_SIZE_8BIT, temp_buf, 14, 100);

    // 加速度计数据(高8位+低8位)
    mpu_data->Accel_X = (int16_t)(temp_buf[0] << 8 | temp_buf[1]);
    mpu_data->Accel_Y = (int16_t)(temp_buf[2] << 8 | temp_buf[3]);
    mpu_data->Accel_Z = (int16_t)(temp_buf[4] << 8 | temp_buf[5]);

    // 温度数据
    mpu_data->Temperature = (int16_t)(temp_buf[6] << 8 | temp_buf[7]);
    mpu_data->Temperature = (mpu_data->Temperature / 340.0) + 36.53; // 温度转换公式

    // 陀螺仪数据
    mpu_data->Gyro_X = (int16_t)(temp_buf[8] << 8 | temp_buf[9]);
    mpu_data->Gyro_Y = (int16_t)(temp_buf[10] << 8 | temp_buf[11]);
    mpu_data->Gyro_Z = (int16_t)(temp_buf[12] << 8 | temp_buf[13]);

    // 转换为实际物理值
    MPU6050_Convert_Data(mpu_data);
    }

    /**
    * @brief 将原始数据转换为实际物理值
    * @param mpu_data: 存储数据的结构体指针
    * @retval 无
    */

    void MPU6050_Convert_Data(MPU6050_DataDef *mpu_data)
    {
    // 加速度计:±2g量程下,16384 LSB/g → 1 LSB = 1/16384 g = 0.061 mg
    mpu_data->Accel_X_mg = (float)mpu_data->Accel_X / 16384.0 * 1000;
    mpu_data->Accel_Y_mg = (float)mpu_data->Accel_Y / 16384.0 * 1000;
    mpu_data->Accel_Z_mg = (float)mpu_data->Accel_Z / 16384.0 * 1000;

    // 陀螺仪:±250°/s量程下,131 LSB/(°/s) → 1 LSB = 1/131 °/s
    mpu_data->Gyro_X_dps = (float)mpu_data->Gyro_X / 131.0;
    mpu_data->Gyro_Y_dps = (float)mpu_data->Gyro_Y / 131.0;
    mpu_data->Gyro_Z_dps = (float)mpu_data->Gyro_Z / 131.0;
    }

    4.3 编写OLED驱动(oled.h / oled.c)

    4.3.1 oled.h

    #ifndef __OLED_H
    #define __OLED_H

    #include "stm32f1xx_hal.h"
    #include "stdlib.h"

    // OLED设备地址(0x78或0x7A,根据模块硬件配置)
    #define OLED_ADDR 0x78 << 1

    // OLED命令/数据定义
    #define OLED_CMD 0x00 // 写命令
    #define OLED_DATA 0x40 // 写数据

    // 函数声明
    void OLED_Init(void); // OLED初始化
    void OLED_Clear(void); // 清屏
    void OLED_Write_Byte(uint8_t data, uint8_t cmd); // 写字节(命令/数据)
    void OLED_Set_Pos(uint8_t x, uint8_t y); // 设置光标位置
    void OLED_Show_Char(uint8_t x, uint8_t y, uint8_t chr); // 显示单个字符
    void OLED_Show_String(uint8_t x, uint8_t y, uint8_t *str); // 显示字符串
    void OLED_Show_Num(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size); // 显示数字
    void OLED_Show_Float(uint8_t x, uint8_t y, float num, uint8_t len, uint8_t decimals); // 显示浮点数

    #endif

    4.3.2 oled.c

    #include "oled.h"

    extern I2C_HandleTypeDef hi2c1; // I2C1句柄

    /**
    * @brief I2C写数据到OLED
    * @param addr: 设备地址
    * @param data: 数据缓冲区
    * @param len: 数据长度
    * @retval 无
    */

    static void I2C_Write(uint8_t addr, uint8_t *data, uint8_t len)
    {
    HAL_I2C_Master_Transmit(&hi2c1, addr, data, len, 100);
    }

    /**
    * @brief OLED写字节(命令/数据)
    * @param data: 要写入的字节
    * @param cmd: 0-命令,1-数据
    * @retval 无
    */

    void OLED_Write_Byte(uint8_t data, uint8_t cmd)
    {
    uint8_t buf[2];
    buf[0] = cmd ? OLED_DATA : OLED_CMD; // 控制字节
    buf[1] = data; // 数据字节
    I2C_Write(OLED_ADDR, buf, 2);
    }

    /**
    * @brief OLED初始化
    * @param 无
    * @retval 无
    */

    void OLED_Init(void)
    {
    HAL_Delay(100); // 上电延时

    // OLED初始化命令
    OLED_Write_Byte(0xAE, OLED_CMD); // 关闭显示
    OLED_Write_Byte(0x00, OLED_CMD); // 设置低列地址
    OLED_Write_Byte(0x10, OLED_CMD); // 设置高列地址
    OLED_Write_Byte(0x40, OLED_CMD); // 设置起始行地址
    OLED_Write_Byte(0xB0, OLED_CMD); // 设置页地址
    OLED_Write_Byte(0x81, OLED_CMD); // 对比度设置
    OLED_Write_Byte(0xFF, OLED_CMD); // 对比度值(0-255)
    OLED_Write_Byte(0xA1, OLED_CMD); // 段重定义设置,正常显示
    OLED_Write_Byte(0xA6, OLED_CMD); // 正常显示(0xA7为反显)
    OLED_Write_Byte(0xA8, OLED_CMD); // 设置多路复用率
    OLED_Write_Byte(0x3F, OLED_CMD); // 1/64 Duty
    OLED_Write_Byte(0xC8, OLED_CMD); // 扫描方向,COM0~COM63
    OLED_Write_Byte(0xD3, OLED_CMD); // 设置显示偏移
    OLED_Write_Byte(0x00, OLED_CMD); // 偏移0
    OLED_Write_Byte(0xD5, OLED_CMD); // 设置震荡频率
    OLED_Write_Byte(0x80, OLED_CMD); // 分频因子
    OLED_Write_Byte(0xD9, OLED_CMD); // 设置预充电周期
    OLED_Write_Byte(0xF1, OLED_CMD); // 预充电时间
    OLED_Write_Byte(0xDA, OLED_CMD); // 设置COM硬件引脚配置
    OLED_Write_Byte(0x12, OLED_CMD);
    OLED_Write_Byte(0xDB, OLED_CMD); // 设置VCOMH
    OLED_Write_Byte(0x40, OLED_CMD);
    OLED_Write_Byte(0x8D, OLED_CMD); // 电荷泵设置
    OLED_Write_Byte(0x14, OLED_CMD); // 开启电荷泵
    OLED_Write_Byte(0xAF, OLED_CMD); // 开启显示

    OLED_Clear(); // 清屏
    }

    /**
    * @brief OLED清屏
    * @param 无
    * @retval 无
    */

    void OLED_Clear(void)
    {
    uint8_t i, j;
    for(i=0; i<8; i++) // 8页
    {
    OLED_Write_Byte(0xB0+i, OLED_CMD); // 设置页地址
    OLED_Write_Byte(0x00, OLED_CMD); // 设置列低地址
    OLED_Write_Byte(0x10, OLED_CMD); // 设置列高地址
    for(j=0; j<128; j++)
    {
    OLED_Write_Byte(0x00, OLED_DATA); // 写入0,清屏
    }
    }
    }

    /**
    * @brief 设置OLED光标位置
    * @param x: 列坐标(0-127)
    * @param y: 行坐标(0-7)
    * @retval 无
    */

    void OLED_Set_Pos(uint8_t x, uint8_t y)
    {
    OLED_Write_Byte(0xB0+y, OLED_CMD); // 设置页地址
    OLED_Write_Byte(((x&0xF0)>>4)|0x10, OLED_CMD); // 设置列高地址
    OLED_Write_Byte(x&0x0F, OLED_CMD); // 设置列低地址
    }

    // ASCII字符集点阵(8*16)
    static const uint8_t asc2_8x16[] = {
    0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,// 空格
    0x00,0x00,0x7C,0x12,0x11,0x12,0x7C,0x00,0x00,0x00,0x7C,0x12,0x11,0x12,0x7C,0x00,// !
    // 省略中间字符点阵(完整ASCII点阵可自行补充,此处仅保留关键部分)
    0x00,0x7C,0x12,0x11,0x12,0x7C,0x00,0x00,0x00,0x7C,0x12,0x11,0x12,0x7C,0x00,0x00,// 0
    0x00,0x10,0x10,0x10,0x10,0x10,0x00,0x00,0x00,0x10,0x10,0x10,0x10,0x10,0x00,0x00,// 1
    // 其余字符点阵可从网上下载标准8*16 ASCII点阵表补充
    };

    /**
    * @brief 显示单个字符
    * @param x: 列坐标(0-127)
    * @param y: 行坐标(0-7)
    * @param chr: 要显示的字符
    * @retval 无
    */

    void OLED_Show_Char(uint8_t x, uint8_t y, uint8_t chr)
    {
    uint8_t i;
    chr -= ' '; // 偏移量,空格为第一个字符
    OLED_Set_Pos(x, y);
    for(i=0; i<8; i++) // 上半部分8个像素
    {
    OLED_Write_Byte(asc2_8x16[chr*16+i], OLED_DATA);
    }
    OLED_Set_Pos(x, y+1);
    for(i=8; i<16; i++) // 下半部分8个像素
    {
    OLED_Write_Byte(asc2_8x16[chr*16+i], OLED_DATA);
    }
    }

    /**
    * @brief 显示字符串
    * @param x: 列坐标
    * @param y: 行坐标
    * @param str: 字符串指针
    * @retval 无
    */

    void OLED_Show_String(uint8_t x, uint8_t y, uint8_t *str)
    {
    while(*str != '\\0')
    {
    OLED_Show_Char(x, y, *str);
    x += 8; // 每个字符占8列
    if(x > 120) // 超出屏幕宽度换行
    {
    x = 0;
    y += 2;
    }
    str++;
    }
    }

    /**
    * @brief 显示整数
    * @param x: 列坐标
    * @param y: 行坐标
    * @param num: 要显示的数字
    * @param len: 显示位数
    * @param size: 字符大小(此处固定8*16,size=16)
    * @retval 无
    */

    void OLED_Show_Num(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
    {
    uint8_t i, temp;
    uint8_t enshow = 0;
    for(i=0; i<len; i++)
    {
    temp = (num / (uint32_t)pow(10, leni1)) % 10;
    if(enshow == 0 && i < len1)
    {
    if(temp == 0)
    {
    OLED_Show_Char(x+i*8, y, ' ');
    continue;
    }
    else
    {
    enshow = 1;
    }
    }
    OLED_Show_Char(x+i*8, y, temp+'0');
    }
    }

    /**
    * @brief 显示浮点数
    * @param x: 列坐标
    * @param y: 行坐标
    * @param num: 浮点数
    * @param len: 总位数(含小数点)
    * @param decimals: 小数位数
    * @retval 无
    */

    void OLED_Show_Float(uint8_t x, uint8_t y, float num, uint8_t len, uint8_t decimals)
    {
    uint32_t integer_part = (uint32_t)num; // 整数部分
    uint32_t decimal_part = (uint32_t)((num integer_part) * pow(10, decimals)); // 小数部分

    // 显示整数部分
    OLED_Show_Num(x, y, integer_part, len decimals 1, 16);
    // 显示小数点
    OLED_Show_Char(x + (len decimals 1)*8, y, '.');
    // 显示小数部分
    OLED_Show_Num(x + (len decimals)*8, y, decimal_part, decimals, 16);
    }

    4.4 编写卡尔曼滤波算法(kalman.h / kalman.c)

    4.4.1 kalman.h

    #ifndef __KALMAN_H
    #define __KALMAN_H

    #include "stm32f1xx_hal.h"

    // 卡尔曼滤波结构体
    typedef struct {
    float Angle; // 滤波后的角度
    float Q_angle; // 角度过程噪声协方差
    float Q_gyro; // 陀螺仪过程噪声协方差
    float R_angle; // 角度测量噪声协方差
    float P[2][2]; // 误差协方差矩阵
    float K[2]; // 卡尔曼增益
    float Angle_err;// 角度误差
    float Gyro_x; // 陀螺仪X轴数据
    float Gyro_y; // 陀螺仪Y轴数据
    float Gyro_z; // 陀螺仪Z轴数据
    float dt; // 采样时间(s)
    uint32_t last_time; // 上一次采样时间(ms)
    } Kalman_FilterDef;

    // 函数声明
    void Kalman_Init(Kalman_FilterDef *kalman, float Q_angle, float Q_gyro, float R_angle); // 卡尔曼初始化
    float Kalman_Filter_X(Kalman_FilterDef *kalman, float Accel_angle, float Gyro); // X轴卡尔曼滤波
    float Kalman_Filter_Y(Kalman_FilterDef *kalman, float Accel_angle, float Gyro); // Y轴卡尔曼滤波

    #endif

    4.4.2 kalman.c

    #include "kalman.h"

    /**
    * @brief 卡尔曼滤波初始化
    * @param kalman: 卡尔曼结构体指针
    * @param Q_angle: 角度过程噪声协方差
    * @param Q_gyro: 陀螺仪过程噪声协方差
    * @param R_angle: 角度测量噪声协方差
    * @retval 无
    */

    void Kalman_Init(Kalman_FilterDef *kalman, float Q_angle, float Q_gyro, float R_angle)
    {
    kalman->Q_angle = Q_angle;
    kalman->Q_gyro = Q_gyro;
    kalman->R_angle = R_angle;

    // 初始化误差协方差矩阵
    kalman->P[0][0] = 1.0f;
    kalman->P[0][1] = 0.0f;
    kalman->P[1][0] = 0.0f;
    kalman->P[1][1] = 1.0f;

    kalman->Angle = 0.0f;
    kalman->last_time = HAL_GetTick(); // 获取初始时间
    }

    /**
    * @brief X轴卡尔曼滤波(俯仰角)
    * @param kalman: 卡尔曼结构体指针
    * @param Accel_angle: 加速度计计算的角度
    * @param Gyro: 陀螺仪角速度
    * @retval 滤波后的角度
    */

    float Kalman_Filter_X(Kalman_FilterDef *kalman, float Accel_angle, float Gyro)
    {
    uint32_t now_time = HAL_GetTick();
    kalman->dt = (now_time kalman->last_time) / 1000.0f; // 计算采样时间(s)
    kalman->last_time = now_time;

    // 1. 预测步骤
    // 角度预测:上一时刻角度 + 角速度 * 采样时间
    kalman->Angle += (Gyro kalman->Angle_err) * kalman->dt;
    // 误差协方差矩阵预测
    kalman->P[0][0] += kalman->dt * (kalman->dt * kalman->P[1][1] kalman->P[0][1] kalman->P[1][0] + kalman->Q_angle);
    kalman->P[0][1] -= kalman->dt * kalman->P[1][1];
    kalman->P[1][0] -= kalman->dt * kalman->P[1][1];
    kalman->P[1][1] += kalman->Q_gyro * kalman->dt;

    // 2. 更新步骤
    // 计算卡尔曼增益
    float deno = kalman->P[0][0] + kalman->R_angle; // 分母
    kalman->K[0] = kalman->P[0][0] / deno;
    kalman->K[1] = kalman->P[1][0] / deno;
    // 角度误差(测量值 – 预测值)
    kalman->Angle_err = Accel_angle kalman->Angle;
    // 更新角度
    kalman->Angle += kalman->K[0] * kalman->Angle_err;
    // 更新误差协方差矩阵
    float P00_temp = kalman->P[0][0];
    float P01_temp = kalman->P[0][1];
    kalman->P[0][0] -= kalman->K[0] * P00_temp;
    kalman->P[0][1] -= kalman->K[0] * P01_temp;
    kalman->P[1][0] -= kalman->K[1] * P00_temp;
    kalman->P[1][1] -= kalman->K[1] * P01_temp;

    return kalman->Angle;
    }

    /**
    * @brief Y轴卡尔曼滤波(横滚角)
    * @param kalman: 卡尔曼结构体指针
    * @param Accel_angle: 加速度计计算的角度
    * @param Gyro: 陀螺仪角速度
    * @retval 滤波后的角度
    */

    float Kalman_Filter_Y(Kalman_FilterDef *kalman, float Accel_angle, float Gyro)
    {
    // 与X轴滤波逻辑完全一致,复用代码
    return Kalman_Filter_X(kalman, Accel_angle, Gyro);
    }

    4.5 编写主函数(main.c)

    /* USER CODE BEGIN Header */
    /**
    ******************************************************************************
    * @file : main.c
    * @brief : Main program body
    ******************************************************************************
    * @attention
    *
    * Copyright (c) 2024 STMicroelectronics.
    * All rights reserved.
    *
    * This software is licensed under terms that can be found in the LICENSE file
    * in the root directory of this software component.
    * If no LICENSE file comes with this software, it is provided AS-IS.
    *
    ******************************************************************************
    */

    /* USER CODE END Header */
    /* Includes ——————————————————————*/
    #include "main.h"
    #include "i2c.h"
    #include "usart.h"
    #include "gpio.h"

    /* Private includes ———————————————————-*/
    /* USER CODE BEGIN Includes */
    #include "mpu6050.h"
    #include "oled.h"
    #include "kalman.h"
    #include "math.h"
    /* USER CODE END Includes */

    /* Private typedef ———————————————————–*/
    /* USER CODE BEGIN PTD */
    MPU6050_DataDef mpu_data; // MPU6050数据结构体
    Kalman_FilterDef kalman_x; // X轴卡尔曼滤波结构体
    Kalman_FilterDef kalman_y; // Y轴卡尔曼滤波结构体
    float pitch_angle = 0.0f; // 俯仰角(X轴)
    float roll_angle = 0.0f; // 横滚角(Y轴)
    float yaw_angle = 0.0f; // 偏航角(Z轴,仅积分,无校准)
    /* USER CODE END PTD */

    /* Private define ————————————————————*/
    /* USER CODE BEGIN PD */
    /* USER CODE END PD */

    /* Private macro ————————————————————-*/
    /* USER CODE BEGIN PM */
    /* USER CODE END PM */

    /* Private variables ———————————————————*/

    /* USER CODE BEGIN PV */
    /* USER CODE END PV */

    /* Private function prototypes ———————————————–*/
    void SystemClock_Config(void);
    /* USER CODE BEGIN PFP */
    void Calculate_Attitude(void); // 姿态解算函数
    /* USER CODE END PFP */

    /* Private user code ———————————————————*/
    /* USER CODE BEGIN 0 */

    /**
    * @brief 姿态解算函数(加速度计+陀螺仪)
    * @param 无
    * @retval 无
    */

    void Calculate_Attitude(void)
    {
    // 1. 读取MPU6050数据
    MPU6050_Read_All_Data(&mpu_data);

    // 2. 加速度计计算角度(反正切函数)
    // 俯仰角(Pitch):X轴与水平面的夹角
    float pitch_accel = atan2(mpu_data.Accel_Y_mg, sqrt(mpu_data.Accel_X_mg*mpu_data.Accel_X_mg + mpu_data.Accel_Z_mg*mpu_data.Accel_Z_mg)) * 180.0f / M_PI;
    // 横滚角(Roll):Y轴与水平面的夹角
    float roll_accel = atan2(mpu_data.Accel_X_mg, mpu_data.Accel_Z_mg) * 180.0f / M_PI;

    // 3. 卡尔曼滤波融合加速度计和陀螺仪数据
    pitch_angle = Kalman_Filter_X(&kalman_x, pitch_accel, mpu_data.Gyro_X_dps);
    roll_angle = Kalman_Filter_Y(&kalman_y, roll_accel, mpu_data.Gyro_Y_dps);

    // 4. 偏航角(Yaw):陀螺仪Z轴积分(无绝对参考,会漂移)
    yaw_angle += mpu_data.Gyro_Z_dps * 0.01f; // 采样周期约10ms,dt=0.01s
    if(yaw_angle > 360.0f) yaw_angle -= 360.0f;
    if(yaw_angle < 0.0f) yaw_angle += 360.0f;
    }

    /* USER CODE END 0 */

    /**
    * @brief The application entry point.
    * @retval int
    */

    int main(void)
    {
    /* USER CODE BEGIN 1 */

    /* USER CODE END 1 */

    /* MCU Configuration——————————————————–*/

    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();

    /* USER CODE BEGIN Init */

    /* USER CODE END Init */

    /* Configure the system clock */
    SystemClock_Config();

    /* USER CODE BEGIN SysInit */

    /* USER CODE END SysInit */

    /* Initialize all configured peripherals */
    MX_GPIO_Init();
    MX_I2C1_Init();
    MX_USART1_UART_Init();
    /* USER CODE BEGIN 2 */

    // 1. 初始化MPU6050
    while(MPU6050_Init(&hi2c1) != HAL_OK)
    {
    HAL_Delay(500); // 初始化失败,延时重试
    }

    // 2. 初始化OLED
    OLED_Init();

    // 3. 初始化卡尔曼滤波
    Kalman_Init(&kalman_x, 0.001f, 0.003f, 0.5f); // X轴卡尔曼参数
    Kalman_Init(&kalman_y, 0.001f, 0.003f, 0.5f); // Y轴卡尔曼参数

    // 4. OLED显示初始信息
    OLED_Show_String(0, 0, (uint8_t*)"MPU6050 Attitude");
    OLED_Show_String(0, 2, (uint8_t*)"Pitch: ");
    OLED_Show_String(0, 4, (uint8_t*)"Roll : ");
    OLED_Show_String(0, 6, (uint8_t*)"Yaw : ");

    /* USER CODE END 2 */

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
    // 姿态解算
    Calculate_Attitude();

    // OLED显示姿态数据
    OLED_Show_Float(48, 2, pitch_angle, 5, 1); // 俯仰角,5位(含小数点),1位小数
    OLED_Show_Float(48, 4, roll_angle, 5, 1); // 横滚角
    OLED_Show_Float(48, 6, yaw_angle, 5, 1); // 偏航角

    HAL_Delay(10); // 10ms刷新一次,100Hz刷新率
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
    }

    /**
    * @brief System Clock Configuration
    * @retval None
    */

    void SystemClock_Config(void)
    {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    /** Initializes the RCC Oscillators according to the specified parameters
    * in the RCC_OscInitTypeDef structure.
    */

    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
    RCC_OscInitStruct.HSIState = RCC_HSI_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
    {
    Error_Handler();
    }

    /** Initializes the CPU, AHB and APB buses clocks
    */

    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
    |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
    {
    Error_Handler();
    }
    }

    /* USER CODE BEGIN 4 */
    /* USER CODE END 4 */

    /**
    * @brief This function is executed in case of error occurrence.
    * @retval None
    */

    void Error_Handler(void)
    {
    /* USER CODE BEGIN Error_Handler_Debug */
    /* User can add his own implementation to report the HAL error return state */
    __disable_irq();
    while (1)
    {
    }
    /* USER CODE END Error_Handler_Debug */
    }

    #ifdef USE_FULL_ASSERT
    /**
    * @brief Reports the name of the source file and the source line number
    * where the assert_param error has occurred.
    * @param file: pointer to the source file name
    * @param line: assert_param error line source number
    * @retval None
    */

    void assert_failed(uint8_t *file, uint32_t line)
    {
    /* USER CODE BEGIN 6 */
    /* User can add his own implementation to report the file name and line number,
    ex: printf("Wrong parameters value: file %s on line %d\\r\\n", file, line) */

    /* USER CODE END 6 */
    }
    #endif /* USE_FULL_ASSERT */

    五、代码编译与下载

    5.1 工程配置

  • 将上述6个文件(mpu6050.h/.c、oled.h/.c、kalman.h/.c)添加到Keil工程中:
    • 右键工程名 -> Add Existing Files to Group -> 选择对应的.c文件
    • 将.h文件放在工程的Inc目录下,确保头文件路径被包含(魔术棒 -> C/C++ -> Include Paths 添加Inc目录)
  • 魔术棒 -> Target -> 设置晶振为8MHz(外部晶振)
  • 魔术棒 -> Output -> 勾选“Create HEX File”(生成HEX文件)
  • 5.2 编译代码

  • 点击Keil中的“Build”或“Rebuild”按钮,确保无错误、无警告
  • 编译完成后,在工程的Output目录下会生成.hex文件
  • 5.3 下载程序

  • 连接USB转TTL到电脑,安装对应的驱动
  • 打开STM32下载工具(如FlyMcu):
    • 选择对应的COM口(设备管理器中查看)
    • 波特率设置为115200
    • 选择生成的HEX文件
    • 勾选“DTR低电平复位,RTS高电平进BootLoader”
    • 点击“开始编程”,然后给STM32上电(或按复位键)
  • 六、硬件测试与调试

    6.1 硬件检查

  • 确认所有接线正确,无接反、短路情况
  • 给STM32供电(5V),观察OLED屏幕是否亮起并显示初始文字
  • 6.2 功能测试

  • 手持STM32开发板,缓慢转动:
    • 俯仰角(Pitch):前后转动时数值变化
    • 横滚角(Roll):左右转动时数值变化
    • 偏航角(Yaw):旋转开发板时数值变化(会漂移,属于正常现象)
  • 若数据无变化或显示乱码:
    • 检查I2C接线是否正确(PB6/SCL、PB7/SDA)
    • 确认MPU6050和OLED的I2C地址是否正确(代码中分别为0x68和0x78)
    • 检查电源是否稳定(必须3.3V供电,禁止5V)
  • 七、进阶优化(可选)

  • 偏航角漂移优化:增加磁力计(如HMC5883L),实现AHRS九轴姿态解算(互补滤波/卡尔曼滤波)
  • 传感器校准:添加MPU6050陀螺仪和加速度计校准代码,消除零漂
  • 显示优化:增加数值单位(°),优化显示排版,添加实时刷新率显示
  • 串口输出:将姿态数据通过USART1输出到串口助手,便于调试

  • 总结

  • 本项目核心是通过STM32的I2C接口驱动MPU6050采集六轴数据,利用卡尔曼滤波融合加速度计(静态稳定)和陀螺仪(动态快速)的优势完成姿态解算,最终在OLED上显示俯仰角、横滚角、偏航角。
  • 代码编写遵循模块化设计,分为MPU6050驱动、OLED驱动、卡尔曼滤波、主函数四个部分,零基础小白可按文件逐个编写、测试,降低学习难度。
  • 关键注意事项:MPU6050必须3.3V供电、I2C地址需匹配硬件配置、卡尔曼滤波参数可根据实际场景微调以获得更稳定的姿态数据。
  • 赞(0)
    未经允许不得转载:171主机测评 » STM32实战案例:基于MPU6050的六轴姿态解算与OLED显示
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址