文章目录
-
- 一、项目概述
-
- 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接线
| 3.3V | VCC | 供电(禁止接5V,会烧毁模块) |
| GND | GND | 共地 |
| PB6 | SCL | I2C时钟线 |
| PB7 | SDA | I2C数据线 |
2.2 STM32与OLED模块接线
| 3.3V | VCC | 供电 |
| GND | GND | 共地 |
| PB6 | SCL | 与MPU6050共用I2C时钟线 |
| PB7 | SDA | 与MPU6050共用I2C数据线 |
2.3 STM32与USB转TTL接线(下载程序)
| PA9 | TXD | 串口发送 |
| PA10 | RXD | 串口接收 |
| GND | GND | 共地 |
| 5V | 5V | 可选,给STM32供电 |
三、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
#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, len–i–1)) % 10;
if(enshow == 0 && i < len–1)
{
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 工程配置
- 右键工程名 -> Add Existing Files to Group -> 选择对应的.c文件
- 将.h文件放在工程的Inc目录下,确保头文件路径被包含(魔术棒 -> C/C++ -> Include Paths 添加Inc目录)
5.2 编译代码
5.3 下载程序
- 选择对应的COM口(设备管理器中查看)
- 波特率设置为115200
- 选择生成的HEX文件
- 勾选“DTR低电平复位,RTS高电平进BootLoader”
- 点击“开始编程”,然后给STM32上电(或按复位键)
六、硬件测试与调试
6.1 硬件检查
6.2 功能测试
- 俯仰角(Pitch):前后转动时数值变化
- 横滚角(Roll):左右转动时数值变化
- 偏航角(Yaw):旋转开发板时数值变化(会漂移,属于正常现象)
- 检查I2C接线是否正确(PB6/SCL、PB7/SDA)
- 确认MPU6050和OLED的I2C地址是否正确(代码中分别为0x68和0x78)
- 检查电源是否稳定(必须3.3V供电,禁止5V)


