本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目为基于正点原子精英版开发板的STM32F103完整程序,支持直接烧录使用,专注于实现MPU6050六轴传感器的数据采集、处理与显示。项目涵盖STM32微控制器编程、I2C通信协议应用、传感器初始化与数据读取、数据解码与融合算法(如卡尔曼/互补滤波),以及LCD等显示驱动开发。通过该实践,开发者可深入掌握嵌入式系统中传感器接口、硬件通信和实时数据处理的核心技术,适用于姿态检测、运动控制等应用场景的学习与拓展。

STM32F103微控制器开发与MPU6050姿态解算实战指南

在嵌入式系统的世界里,一块小小的MCU如何感知世界、理解运动,并实时做出反应?这正是我们今天要深入探讨的问题。想象一下:你手里拿着一个智能手环,它能判断你是抬手看时间还是挥拳锻炼;一架无人机悬停空中,哪怕风吹草动也能稳如泰山——这些看似“智能”的行为背后,其实是传感器数据与控制算法的精密协作。

而这一切的起点,往往就是 STM32F103 + MPU6050 这对黄金组合。


从零开始:认识你的主控芯片

说到STM32F103,老工程师们可能都会心一笑:“啊,那是我的青春。”没错,这款基于ARM Cortex-M3内核的经典MCU,至今仍在工业控制、智能家居和教学实验中占据重要地位。72MHz主频,内置丰富的外设资源(GPIO、USART、I2C、SPI、ADC、定时器……),再加上成熟的HAL库支持,让它成为无数开发者踏入嵌入式世界的“第一站”。

它的核心架构采用三级流水线设计,配合嵌套向量中断控制器(NVIC),使得中断响应极快,非常适合需要高实时性的场景。比如你在按键按下的一瞬间,LED立刻亮起,这种“无延迟”体验就得益于高效的中断机制。

举个简单的例子,我们要让PA5引脚驱动一个LED:

__HAL_RCC_GPIOA_CLK_ENABLE(); // 必须先开启时钟!
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;     // 推挽输出
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;    // 低速即可
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

这段代码虽然只有几行,但它揭示了STM32编程的核心范式: 先使能时钟 → 配置结构体 → 调用初始化函数 。这个模式贯穿所有外设操作,是每个STM32开发者必须掌握的基本功。

🤔 小贴士:很多人初学时常犯的错误就是忘记开时钟——结果程序跑飞或完全没反应。记住,STM32的一切外设都像汽车引擎,不上电(时钟)根本动不了!


开发环境怎么选?Keil vs CubeIDE,谁更适合你?

当你拿到一块正点原子精英版开发板,第一步不是写代码,而是搭建开发环境。这时候问题来了:该用Keil MDK还是STM32CubeIDE?

别急,咱们来一场“擂台赛”:

特性 Keil MDK STM32CubeIDE
许可费用 商业授权(贵 😢) 免费开源(香 💯)
编译器 Arm Compiler(优化强) GCC(够用)
图形化配置 需外接CubeMX 内建CubeMX
调试功能 极其强大 友好直观
社区支持 文档多但更新慢 官方持续维护

所以结论很清晰:
- 如果你是企业项目,追求极致性能和长期稳定, 上Keil
- 如果你是学生、爱好者或者做原型验证, 直接上CubeIDE ,省时省力还省钱!

而且CubeIDE最大的优势是什么? 图形化配置+自动生成代码 。你可以像搭积木一样完成引脚分配、时钟树设置,甚至一键生成初始化代码。

比如你想把PA5设为LED输出,PB1作为按键输入,再配上外部8MHz晶振并倍频到72MHz?打开STM32CubeMX,点几下鼠标就搞定了。

// CubeMX 自动生成的时钟配置片段
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_BYPASS;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz × 9 = 72MHz

是不是比手动查手册方便多了?而且CubeMX还会生成详细的PDF报告,告诉你AHB、APB1、APB2各总线频率是多少,有没有超限,一目了然。

graph TD
    A[选择开发工具] --> B{项目性质}
    B -->|企业/量产| C[Keil MDK]
    B -->|学习/原型| D[STM32CubeIDE]
    C --> E[高性能编译+专业调试]
    D --> F[免费+集成化配置]
    E --> G[适合复杂项目]
    F --> H[适合快速验证]

看到这张图了吗?选择工具的本质是 匹配场景 。别盲目追求“高级”,适合自己才是最好的。


GPIO不只是点亮LED那么简单

GPIO可能是最基础的外设,但也最容易被低估。你以为它只能点灯读键?错!它是整个系统的“感官接口”。

STM32的GPIO有多种工作模式,每一种都有其独特用途:

模式 用途
GPIO_MODE_INPUT 浮空输入,需外部上拉
GPIO_MODE_INPUT_PULLUP 按键检测(默认高,按下接地)✅
GPIO_MODE_OUTPUT_PP 驱动LED、继电器 ✅
GPIO_MODE_OUTPUT_OD I²C总线、电平转换 ✅
GPIO_MODE_AF_PP 复用功能,如UART_TX
GPIO_MODE_ANALOG ADC采集

以正点原子精英版为例,板载LED接在PA5,共阳极设计,所以 低电平点亮

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // ON
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);   // OFF

而按键KEY0接在PB1,默认通过内部上拉电阻保持高电平,按下后接地变为低电平:

if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 切换LED状态
}

但这里有个坑:机械按键会有“抖动”现象,一次按下可能触发多次中断。怎么办?加延时消抖!

uint8_t key_state = 1;

if (KEY0 == 0 && key_state == 1) {
    HAL_Delay(20); // 等待抖动结束
    if (KEY0 == 0) {
        do_something();
    }
    key_state = 0;
} else if (KEY0 == 1) {
    key_state = 1;
}

当然,更优雅的方式是使用 外部中断 + 定时器扫描 ,实现非阻塞式处理,后面我们会讲到。


延时不等于“卡死CPU”

说到延时,很多新手喜欢用 HAL_Delay() ,简单粗暴:

HAL_Delay(1000); // 延时1秒

但它底层依赖SysTick中断递减全局变量 uwTick ,如果中断被关闭或优先级被打断,就会失效。更关键的是,这种方式会让CPU“原地睡觉”,啥也干不了。

那有没有更好的办法?

当然有!对于毫秒级延时,可以用 DWT 周期计数器实现精准延时而不依赖中断:

static inline void delay_us(uint32_t us) {
    uint32_t start = DWT->CYCCNT;
    uint32_t cycles = us * (SystemCoreClock / 1000000);
    while ((DWT->CYCCNT - start) < cycles);
}

// 启用DWT前要打开时钟
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

这样就能做到微秒级精确延时,适用于PWM波形生成、传感器时序控制等对时间敏感的场景。

💡 经验之谈:在RTOS系统中,尽量避免使用 HAL_Delay() ,改用任务延时API(如 vTaskDelay() ),才能真正发挥多任务的优势。


中断:让系统学会“主动响应”

如果说轮询是“每隔一秒问一遍‘有人按我吗?’”,那中断就是“一旦被按下立刻通知我”。显然,后者效率更高。

STM32使用NVIC管理中断,Cortex-M3支持最多68个中断通道。以PB1引脚为例,它可以映射到EXTI1线,从而触发外部中断。

配置流程如下:
1. 使用CubeMX将PB1设为 GPIO_EXIT1
2. 在NVIC中启用EXTI1中断;
3. 实现中断服务函数;
4. 用户逻辑放在回调函数中。

void EXTI1_IRQHandler(void) {
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1); // 转发给HAL库
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == GPIO_PIN_1) {
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 响应按键
    }
}

注意到没有?中断服务函数只是“转发”,真正的业务逻辑在 HAL_GPIO_EXTI_Callback 里。这是因为HAL库大量使用 __weak 关键字声明回调函数,允许我们在不修改库源码的情况下注入自己的代码。

🧠 进阶技巧:合理设置抢占优先级和子优先级,避免高优先级中断长时间占用CPU导致其他任务饿死。例如,串口接收中断可以设为较低优先级,确保关键控制任务不受影响。

stateDiagram-v2
    [*] --> Idle
    Idle --> Debouncing: 按键按下
    Debouncing --> Active: 延时20ms后仍低
    Active --> Idle: 松开
    Debouncing --> Idle: 抬起(抖动)

这个状态机模型非常适合结合中断与定时器实现高效按键管理。


I²C通信:连接世界的双线桥梁

现在我们进入重头戏: MPU6050六轴传感器驱动

MPU6050集成了三轴加速度计和三轴陀螺仪,通过I²C接口与STM32通信。为什么选I²C?因为它只需要两根线(SDA数据线 + SCL时钟线),结构简单,成本低,特别适合板内短距离通信。

I²C协议要点速览

  • 起始条件 :SCL高时,SDA由高变低;
  • 停止条件 :SCL高时,SDA由低变高;
  • 地址帧 :7位设备地址 + 1位R/W标志;
  • ACK/NACK :接收方在第9个时钟周期拉低表示确认;
  • 重复起始 :读操作时用于切换方向而不释放总线。

MPU6050的默认7位地址是 0x68 (AD0接地)或 0x69 (AD0接VCC)。记得检查硬件连接哦!

sequenceDiagram
    participant Master
    participant Slave
    Master->>Slave: Start
    Master->>Slave: 7-bit Address + W(0)
    Slave-->>Master: ACK
    Master->>Slave: Register Address
    Slave-->>Master: ACK
    Master->>Slave: Data Byte
    Slave-->>Master: ACK
    Master->>Slave: Stop

这是典型的寄存器写操作流程。比如我们要唤醒MPU6050,就得往 PWR_MGMT_1 寄存器(0x6B)写0x00:

uint8_t tx_buf[] = {0x6B, 0x00};
HAL_I2C_Master_Transmit(&hi2c1, 0xD0, tx_buf, 2, 100); // 0xD0 = 0x68 << 1

如何配置STM32的I²C外设?

STM32F103自带I²C1和I²C2控制器,支持标准模式(100kbps)和快速模式(400kbps)。推荐使用400kbps以提升数据吞吐率。

使用LL库配置示例:

LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C1);
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOB);

// PB6(SCL), PB7(SDA) 设为复用开漏输出
LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_6, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetPinOutputType(GPIOB, LL_GPIO_PIN_6, LL_GPIO_OUTPUT_OPENDRAIN);
LL_GPIO_SetPinPull(GPIOB, LL_GPIO_PIN_6, LL_GPIO_PULL_UP);

// I2C初始化
LL_I2C_InitTypeDef i2c_init;
i2c_init.ClockSpeed = 400000;
i2c_init.DutyCycle = LL_I2C_DUTYCYCLE_2;
LL_I2C_Init(I2C1, &i2c_init);
LL_I2C_Enable(I2C1);

⚠️ 注意事项:
- 必须外接上拉电阻!一般400kbps用2.2kΩ,100kbps可用4.7kΩ;
- PCB走线尽量短且平行,减少干扰;
- 若挂多个设备,注意地址冲突。


读取MPU6050原始数据全流程

搞定I²C后,就可以开始跟MPU6050对话了。

第一步:验证设备在线

通过读取 WHO_AM_I 寄存器(地址0x75),应返回0x68:

uint8_t id;
HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR<<1, 0x75, I2C_MEMADD_SIZE_8BIT, &id, 1, 100);
if (id == 0x68) {
    // 通信成功 ✅
}

第二步:配置工作参数

mpu6050_write_reg(0x6B, 0x00); // 唤醒
mpu6050_write_reg(0x1B, 0x08); // 陀螺仪±500°/s
mpu6050_write_reg(0x1C, 0x10); // 加速度计±8g
mpu6050_write_reg(0x19, 9);    // 采样率100Hz (1000/(1+9))

第三步:批量读取14字节传感器数据

uint8_t raw[14];
HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR<<1, 0x3B, I2C_MEMADD_SIZE_8BIT, raw, 14, 100);

int16_t ax = (raw[0] << 8) | raw[1];
int16_t ay = (raw[2] << 8) | raw[3];
int16_t az = (raw[4] << 8) | raw[5];
int16_t gx = (raw[8] << 8) | raw[9];
// ...以此类推

这里要注意:MPU6050是大端格式,高位在前,所以我们需要手动拼接两个字节。


数据校准:让传感器说出真话

刚上电的MPU6050数据往往是“不准”的。为什么?因为存在零偏(bias)和温漂。

解决方法很简单: 静态校准

让设备静止放置,采集100组数据求平均值,作为初始偏移量:

int32_t bias_ax = 0, bias_ay = 0, bias_az = 0;
for (int i = 0; i < 100; i++) {
    read_accel(&ax, &ay, &az);
    bias_ax += ax;
    bias_ay += ay;
    bias_az += az;
    HAL_Delay(10);
}
bias_ax /= 100;
bias_ay /= 100;
bias_az /= 100;

后续每次读取的数据都减去这个偏移量:

ax_valid = ax_raw - bias_ax;

温度补偿也可以做:

float temperature = ((float)temp_raw - 36.53f) / 340.0f + 36.53f;
// 用此温度修正陀螺仪零偏

姿态解算:从原始数据到空间角度

有了干净的数据,下一步就是解算姿态角——也就是设备当前的 俯仰角(Pitch) 横滚角(Roll)

方法一:仅用加速度计计算倾斜角

当设备静止时,加速度计主要感受重力。利用三角关系可得:

$$
\text{Pitch} = \arctan\left(\frac{a_x}{\sqrt{a_y^2 + a_z^2}}\right) \times \frac{180}{\pi}
$$
$$
\text{Roll} = \arctan\left(\frac{a_y}{\sqrt{a_x^2 + a_z^2}}\right) \times \frac{180}{\pi}
$$

代码实现:

pitch = atan2f(ax, sqrtf(ay*ay + az*az)) * 57.2958f;
roll = atan2f(ay, sqrtf(ax*ax + az*az)) * 57.2958f;

优点:提供绝对参考(重力方向)
缺点:受动态加速度干扰严重,比如移动手机时角度乱跳

方法二:积分陀螺仪数据

陀螺仪测量角速度,通过对时间积分得到角度变化:

gyro_pitch += gyro_rate_x * dt; // dt = 0.01s for 100Hz

优点:响应快,抗振动
缺点:存在零偏,积分会漂移!哪怕每秒差0.1°,一分钟就偏6°


融合算法登场:互补滤波 or 卡尔曼滤波?

既然单一传感器都不完美,那就融合起来!

互补滤波:简单有效,适合嵌入式

思想很简单: 加速度计看长期趋势,陀螺仪看短期变化

公式如下:

angle = 0.98 * (angle + gyro_rate * dt) + 0.02 * accel_angle

解释:
- 0.98 是陀螺仪权重(高频成分)
- 0.02 是加速度计权重(低频修正)

代码实现:

float dt = 0.01f;
float alpha = 0.98f;

pitch = alpha * (pitch + gx * dt) + (1 - alpha) * pitch_acc;
roll  = alpha * (roll  + gy * dt) + (1 - alpha) * roll_acc;

效果立竿见影:既保留了陀螺仪的灵敏度,又通过加速度计定期“拉回来”,防止漂移。


实时数据显示:让数据看得见

最后一步,把姿态角传回电脑可视化。

可以通过串口发送JSON格式数据:

printf("{\"pitch\":%.2f,\"roll\":%.2f,\"yaw\":%.2f}\n", pitch, roll, yaw);

然后用Python + Matplotlib实时绘图,或者接入Unity做3D姿态模拟,效果非常炫酷!

graph TD
    A[读取MPU6050原始数据] --> B{是否校准?}
    B -- 是 --> C[应用零偏与温度补偿]
    B -- 否 --> D[直接转换]
    C --> E[加速度转Pitch/Roll]
    D --> E
    E --> F[陀螺仪积分计算增量角度]
    F --> G[进入融合滤波模块]
    G --> H[输出稳定姿态角]

这条数据流贯穿整个系统,每一个环节都不能出错。


总结:从硬件到算法,构建完整的感知闭环

回顾整个流程,我们完成了:

✅ 搭建STM32开发环境
✅ 掌握GPIO、中断、SysTick等基础外设编程
✅ 实现I²C通信与MPU6050驱动
✅ 获取并校准原始传感器数据
✅ 使用互补滤波进行姿态解算
✅ 输出实时姿态信息

这套技术栈不仅适用于MPU6050,还可以扩展到BMP280气压计、磁力计HMC5883L、GPS模块等更多传感器,最终构建出完整的IMU惯性导航系统。

🚀 更进一步?
- 引入四元数和Mahony滤波,实现三维全姿态解算(含偏航角Yaw)
- 结合蓝牙/WiFi上传数据,打造无线姿态监测终端
- 移植到FreeRTOS,实现多任务并发处理

嵌入式开发的魅力就在于此: 你亲手赋予一块芯片“感知世界”的能力 。每一次成功的通信、每一帧稳定的姿态数据,都是代码与物理世界的对话。

所以,别犹豫了——插上你的STM32开发板,点亮第一个LED,迈出通往智能世界的坚实一步吧!💪🔥

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目为基于正点原子精英版开发板的STM32F103完整程序,支持直接烧录使用,专注于实现MPU6050六轴传感器的数据采集、处理与显示。项目涵盖STM32微控制器编程、I2C通信协议应用、传感器初始化与数据读取、数据解码与融合算法(如卡尔曼/互补滤波),以及LCD等显示驱动开发。通过该实践,开发者可深入掌握嵌入式系统中传感器接口、硬件通信和实时数据处理的核心技术,适用于姿态检测、运动控制等应用场景的学习与拓展。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐