资讯动态

STM32F103C8T6+MPU6050完整工程:从I2C驱动到DMP姿态解算

发布时间:2026/10/17 15:41:09 来源:尧图企业网站定制
简介面向STM32F103C8T6与MPU6050开发者的完整工程包支持直接编译下载适用于姿态解算、飞行器/机器人控制、智能穿戴及健康监测等嵌入式场景。文件总数188个压缩包仅6.16MB包括35个H头文件、32个C源文件、标准外设库中间文件O/CRF/D、Keil工程文件uvprojx以及可直接烧录的HEX文件目录结构清晰便于快速导入MDK编译运行。已有2616人浏览学习资源经过实际验证稳定性与可靠性获得认可。除源码本身还附带示例代码和文档可帮助开发者尽快熟悉MPU6050的I2C通信时序、原始数据读取与姿态解算算法如四元数互补滤波等减少环境配置和排错时间。对初学者是友好易懂的入门模板对专业开发者则是可复用的高效开发起点显著加速无人机、平衡车、体感设备等项目的落地。1. 为什么这份工程值得直接下载省掉三件折腾事调 IMU 的人都知道一个真相读裸数据是两步把裸数据变成可用的姿态角是另外十步。这套 STM32F103C8T6MPU6050 完整工程把最折腾的驱动移植和 DMP 姿态解算都封装好了拿到的 MDK 工程打开就能编译串口直接输出欧拉角不需要自己拼 I2C 时序也不需要对着寄存器手册查地址。它解决的不只是“传感器怎么读”而是“数据怎么变成能用的角度”——从 I2C 初始化、量程配置、DMP 固件加载到串口协议整条链路已经跑通。适合正在做自平衡车、二轴云台或 IMU 手势识别、又不想从寄存器开始啃的人也适合刚上手 STM32、需要一个能复现的传感器工程模板的初学者。下面我按文件结构、硬件接线、姿态解算和常见坑依次拆开讲。2. 工程结构与初始化链路先看懂 HSE→PLL→I2C 再编译整套工程绕不开三个关键环节系统时钟怎么从 8MHz 晶振倍频到 72MHzI2C1 外设怎么配置以及 MPU6050 驱动文件在整个工程里的位置。这三件事在 main.c 里是顺序执行的任何一个环节不对后面读出来的数据都是错的。2.1 文件结构从 main.c 到 Motion Driver 的阅读顺序拿到工程不要急着点编译按钮。先用文本编辑器把文件列表过一遍确认下面这几类文件都齐了路径作用阅读优先级Core/Src/main.c系统时钟、GPIO、串口初始化主循环调用姿态任务第一优先Core/Startup/startup_stm32f103xb.s启动文件对应 C8T6 的 64KB Flash 容量第一优先User/mpu6050.c/.h封装好的 MPU6050 初始化和角度读取接口第二优先Middlewares/inv_mpu.c传感器原始读写与基础配置第二优先Middlewares/inv_mpu_dmp_motion_driver.cDMP 固件加载与 FIFO 读取第三优先Drivers/stm32f1xx_hal_i2c.cHAL 库 I2C 驱动默认不改第四优先阅读顺序我习惯是 main.c → mpu6050.c → inv_mpu_dmp_motion_driver.c → inv_mpu.c。main.c 告诉你整个数据流从哪启动mpu6050.c 是这层封装的核心后面两个 Motion Driver 文件正常情况不用动只需要知道它们提供了哪些 API。需要注意启动文件。F103C8T6 只有 64KB Flash 和 20KB RAM如果工程是从大容量芯片模板改过来的启动文件必须是 startup_stm32f103xb.s 而不是 xd 或 xe 版本。启动文件不匹配时编译能过甚至下载能成功但芯片复位后中断向量表错位表现就是程序跑飞、死在启动阶段这种问题查起来很隐蔽。2.2 系统时钟72MHz 是 I2C 时序的前提MPU6050 的 I2C 时序参数并不依赖主控频率但串口波特率和 I2C 的 SCL 频率计算依赖。如果系统时钟停留在默认的 8MHz 内部 RC串口 115200 算出来的波特率误差会大到一个字符都收不对。先看这段时钟初始化代码void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct {0}; /* 开启外部8MHz晶振HSE并作为PLL输入源 */ RCC_OscInitStruct.OscillatorType RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL RCC_PLL_MUL9; /* 8MHz * 9 72MHz */ if (HAL_RCC_OscConfig(RCC_OscInitStruct) ! HAL_OK) { Error_Handler(); } /* 总线分频HCLK72MHzPCLK136MHzPCLK272MHz */ 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; /* I2C1挂在这条总线上 */ RCC_ClkInitStruct.APB2CLKDivider RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(RCC_ClkInitStruct, FLASH_LATENCY_2) ! HAL_OK) { Error_Handler(); } }这里三个参数最容易出错PLLMUL 固定用 9 倍对应 8MHz 外部晶振FLASH_LATENCY_2 是 72MHz 主频下 Flash 读等待周期写成 0 会随机死机APB1 分频必须写 2I2C1 挂载在 APB1 总线上它的输入时钟是 36MHz后面 HAL_I2C_Init 里的时序计算全部基于这个 36MHz 值。我见过有人把 HSE 那一段整个注释掉结果程序跑在 8MHz 内部 RC 上串口完全乱码I2C 倒是有波形但时序余量很小。排查串口乱码时先查这里不要动波特率。2.3 I2C1 与引脚复用PB6/PB7 的开漏配置F103C8T6 的 I2C1 固定映射到 PB6SCL和 PB7SDA复用功能要配置成开漏输出并开启内部上拉。代码是这样void MX_GPIO_I2C1_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_AF_OD; /* 开漏复用模式 */ GPIO_InitStruct.Pull GPIO_PULLUP; /* 内部上拉作为兜底 */ GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); /* I2C1外设时钟使能 */ __HAL_RCC_I2C1_CLK_ENABLE(); } void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; /* 快速模式400kHz */ hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; /* SCL高低电平比2:1 */ hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); } }开漏输出是 I2C 协议的硬件要求不能改成推挽否则多个设备同时驱动 SDA 时会发生短路式电平对抗。内部上拉一般能保证空闲电平但如果线上有长杜邦线或模块距离远建议在 PCB 或模块侧再接一个 4.7kΩ 上拉到 3.3V。ClockSpeed 我一般先用 100000标准模式打通链路确认读数正常后再提到 400000。MPU6050 数据手册标称支持 400kHz但面包板上飞线较长时 400kHz 容易受干扰表现是偶尔读回 0xFF 或超时。400kHz 下 DutyCycle 参数写成 I2C_DUTYCYCLE_2100kHz 下这个参数不生效。3. 硬件接线与裸数据读取先打通 WHO_AM_I 这一关姿态解算的前提是裸数据能稳定读出来。把模块接到开发板这一步看起来是插几根杜邦线的事实际上地址引脚、电源电平和上拉电阻三个细节直接决定后面能不能通信上。3.1 接线表与地址选择常见 MPU6050 模块引脚有八个这套工程只需要其中五个。接线如下MPU6050 模块引脚接到 STM32F103C8T6说明VCC3.3V不要接 5V电平不匹配GNDGND必须共地SCLPB6I2C1_SCLSDAPB7I2C1_SDAAD0GND 或悬空悬空等同于接 GNDINT不接轮询模式下不需要中断模式再接XDA / XCL不接留给外部磁力计的扩展接口AD0 决定芯片的 I2C 地址接 GND 时 7 位地址是 0x68接 VCC 时是 0x69。这套工程默认 0x68如果你手里的模块 AD0 被拉高了需要把驱动里地址宏改成 0x69否则通信永远超时。还遇到过一种情况模块板载了稳压芯片丝印上写 VCC 可以接 5V但 SDA/SCL 的上拉电阻可能也被拉到了 5V。STM32 的 PB6/PB7 引脚本身容忍 5V但高电平噪声会变大偶尔超时。建议一律接 3.3V让整个 I2C 域保持同一个电平。3.2 读 WHO_AM_I判断模块是否在线HAL 库的 I2C 通信接口是阻塞式的传参里最后一个 100 是超时时间单位毫秒。一个最基本的单字节读函数长这样#define MPU6050_ADDR (0x68 1) /* HAL库需要8位地址左移一位 */ uint8_t MPU_Read_Byte(uint8_t reg) { uint8_t val 0; HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, reg, I2C_MEMADD_SIZE_8BIT, val, 1, 100); return val; }注意地址宏后面的 1。HAL 库的 I2C 地址参数要求 8 位地址即在 7 位地址基础上左移一位并补 R/W 位。如果直接用 0x68总线上发出的器件地址就是 0x34 而不是 0xD0从机不会应答。模块在线与否读 WHO_AM_I 寄存器最直接寄存器地址 0x75读出来应该是 0x68uint8_t id MPU_Read_Byte(0x75); if (id ! 0x68) { /* 模块没接对或地址选错或供电异常 */ }WHO_AM_I 是芯片出厂固化值AD0 高低不影响它的返回值。第一次上电建议只跑这段代码并打印返回值确认输出 0x68 之后再往下走。如果这一步都不通过后面所有姿态数据都是空的。3.3 配置量程与读取六轴原始值MPU6050 上电默认处于睡眠模式先把电源管理寄存器 10x6B的 SLEEP 位清掉然后配置加速度计和陀螺仪量程void MPU_Reset_And_Wakeup(void) { MPU_Write_Byte(0x6B, 0x00); /* 清SLEEP位使用内部8MHz */ MPU_Write_Byte(0x6C, 0x00); /* 禁用温度计减小干扰 */ MPU_Write_Byte(0x1C, 0x18); /* 加速度计 ±16g */ MPU_Write_Byte(0x1B, 0x18); /* 陀螺仪 ±2000dps */ }量程寄存器的低四位是配置位0x18 的二进制是 11000对应 ±16g 和 ±2000dps。这个配置下加速度计灵敏度是 2048 LSB/g陀螺仪是 16.4 LSB/dps。换算时用原始值除灵敏度系数即可比如加速度 ax 的实际值是ax_raw / 2048.0f。读取六轴数据时不推荐分别读取六个寄存器因为两次读取之间芯片可能已经更新数据导致高低字节不匹配。标准做法是从加速度计 X 轴高字节寄存器 0x3B 开始一次连续读 14 字节typedef struct { int16_t ax, ay, az; int16_t gx, gy, gz; } MPU_Data; void MPU_Read_Raw(MPU_Data *d) { uint8_t buf[14]; HAL_I2C_Mem_Read(hi2c1, MPU6050_ADDR, 0x3B, I2C_MEMADD_SIZE_8BIT, buf, 14, 100); d-ax (int16_t)((buf[0] 8) | buf[1]); d-ay (int16_t)((buf[2] 8) | buf[3]); d-az (int16_t)((buf[4] 8) | buf[5]); /* buf[6] 是温度高字节buf[7] 是温度低字节这里用不到 */ d-gx (int16_t)((buf[8] 8) | buf[9]); d-gy (int16_t)((buf[10] 8) | buf[11]); d-gz (int16_t)((buf[12] 8) | buf[13]); }批量读 14 字节比单独读六次省去五次 I2C 事务重要的是避免了“读到一半数据被刷新”的撕裂问题。把这段代码跑通后用手转动模块串口打印原始值应该有明显变化这时姿态解算才有意义。4. 姿态解算DMP 欧拉角接入与三个关键参数裸数据到手只是第一步。直接把陀螺仪积分求角度十分钟后角度就飘到不知哪去了。这套工程用的是芯片内部的 DMP 硬件姿态解算主控只负责从 FIFO 里读结果省心且稳定。4.1 为什么优先用 DMP 而不是自己跑互补滤波F103C8T6 主频 72MHz跑一个 Mahony 互补滤波完全不吃力但工程里选 DMP 的原因不只是省 CPU方案代码量CPU 占用姿态漂移调参成本芯片 DMP小低较小低互补滤波中低中等中Mahony 四元数中中中等中DMP 把四元数解算放在传感器内部的可编程单元主控在 50Hz 频率下读一次 FIFO 就能拿到四元数或欧拉角。它的另一个优势是输出节奏稳定FIFO 缓冲区可以吸收主控偶发的调度卡顿。自己写滤波算法虽然灵活但陀螺仪零偏、加速度计噪声、滤波系数这些参数每块板子都要重新调一轮对快速做原型验证不划算。4.2 DMP 初始化先静止再加载固件Motion Driver 库的初始化顺序有讲究第一步和第二步颠倒会导致陀螺仪偏置记录在错误状态。通常的做法是上电后等待 1 秒让传感器稳定再执行初始化。uint8_t MPU_DMP_Init(void) { uint8_t ret 0; /* 第一步基础配置唤醒芯片设置时钟源 */ ret mpu_init(); if (ret ! 0) { return ret; } /* 第二步使能六轴传感器并写入FIFO */ mpu_set_sensors(INV_XYZ_GYRO | INV_XYZ_ACCEL); mpu_configure_fifo(INV_XYZ_GYRO | INV_XYZ_ACCEL); /* 第三步加载DMP固件设置输出频率 */ ret dmp_load_motion_driver_firmware(); if (ret ! 0) { return ret; } /* 方向参数芯片平放X轴朝前 */ dmp_set_orientation(0, 0, 0, 0, 1, 0); dmp_set_fifo_rate(50); /* 姿态输出频率50Hz */ mpu_set_dmp_state(1); /* 启动DMP */ return 0; }dmp_set_orientation的六个数字是坐标轴映射配置不同版本的 Motion Driver 库传参形式不一样最稳妥的做法是打开工程里的inv_mpu_dmp_motion_driver.h看声明。这里的关键是模块安装方向变了比如竖着安装在云台上需要同步改这组参数否则 pitch 和 roll 会互换甚至反号。dmp_set_fifo_rate(50)这个参数值得多说一句。50Hz 是姿态输出频率对自平衡车和云台都够用。不建议调到 200HzFIFO 总共 1024 字节一帧姿态数据在几十字节上下200Hz 时不到 0.3 秒就会溢出一次主循环稍微卡顿就会读丢。50Hz 下缓冲余量大得多。4.3 从 FIFO 读四元数到欧拉角再走串口DMP 输出的四元数是定点数缩放系数 16384也就是 2 的 14 次方。换算成浮点四元数后再转欧拉角uint8_t MPU_DMP_Get_Angle(float *yaw, float *pitch, float *roll) { short gyro[3] {0}, accel[3] {0}; short quat[4] {0}; long count 0; if (dmp_read_fifo(gyro, accel, quat, count) ! 0) { return 1; } if (count 0) { return 1; } float q0 quat[0] / 16384.0f; float q1 quat[1] / 16384.0f; float q2 quat[2] / 16384.0f; float q3 quat[3] / 16384.0f; *pitch asin(2.0f * (q0 * q2 - q1 * q3)) * 57.29578f; *roll atan2f(2.0f * (q0 * q1 q2 * q3), 1.0f - 2.0f * (q1 * q1 q2 * q2)) * 57.29578f; *yaw atan2f(2.0f * (q0 * q3 q1 * q2), 1.0f - 2.0f * (q2 * q2 q3 * q3)) * 57.29578f; return 0; }四元数转欧拉角的公式是固定的但有几个边界要知道。pitch 用 asin 计算输入范围被限制在正负 90 度当 pitch 接近 90 度时 roll 和 yaw 会出现数值抖动这是欧拉角的万向锁问题不是芯片坏了。如果你做的是云台或穿戴设备尽量避免 pitch 跨越正负 90 度或者后续改用四元数做控制。主循环每 20ms 调用一次读取函数然后通过串口 1 以 115200 波特率输出 CSV 格式printf(%.1f,%.1f,%.1f\r\n, yaw, pitch, roll);串口波形工具可以直接画出三轴角度曲线用来观察静态漂移和动态响应。yaw 角度在静止时会缓慢漂移是正常现象因为 DMP 的航向参考来自陀螺仪积分没有磁力计做绝对参照。对自平衡车和云台来说pitch 和 roll 有加速度计修正漂移很小如果对 yaw 的绝对精度有要求得外接磁力计并开启陀螺仪与磁力计的融合。5. 避坑清单五个必踩的坑与解决记录这套工程我前后在三种不同来源的模块上跑过踩过的坑集中在编译配置、I2C 通信和 DMP 初始化三个层面。下面五条按频率排序每一条都是实际遇到过的。5.1 编译报错编译器提示 HAL_I2C_xxx 未定义现象打开工程直接编译报错说HAL_I2C_Mem_Read未声明或者hi2c1不存在。原因HAL 库的外设模块默认是“按需编译”的stm32f1xx_hal_conf.h里有一组HAL_xxx_MODULE_ENABLED宏I2C 对应的宏没有打开I2C 的所有 API 就不会被编译进去。另外工程树里漏掉stm32f1xx_hal_i2c.c也会出现同样的错误。解决打开stm32f1xx_hal_conf.h找到下面这行并把注释去掉#define HAL_I2C_MODULE_ENABLED然后在 MDK 工程左侧的 Drivers 分组里确认stm32f1xx_hal_i2c.c存在。如果是从别的芯片型号改过来的工程还要在 Options for Target 里把 Device 选成 STM32F103C8Tx并检查 Target 页的 ROM/RAM 范围IROM 起始 0x08000000 大小 0x10000IRAM 起始 0x20000000 大小 0x5000。容量不匹配时编译能过下载后大概率跑飞。5.2 上电就卡死Error_Handler 的黑匣子现象程序烧进去后板子没反应串口无输出调试器停在Error_Handler的死循环里。原因多数情况下是因为 MPU6050 没有接好或没上电初始化函数里的HAL_I2C_Mem_Read超时返回错误而工程对错误的处理是直接进Error_Handler。HAL 库标准生成的Error_Handler里只有一个空的while(1)看起来就是死机。也有的工程写法更激进while(MPU_Init() ! 0);这一行会让程序永远卡在重试里。解决先把模块断开烧一个不初始化 MPU6050 的版本确认串口通。接着接上模块用万用表量 VCC 是否为 3.3V、GND 是否连通。建议把Error_Handler里临时加一行串口打印错误码方便定位是哪一步失败不要一直让它当黑匣子。我一般会让初始化失败时打印MPU_Init failed: %d然后让主循环继续跑串口而不是死等。5.3 读出的数据全是 0xFF先怀疑地址再怀疑接线现象HAL_I2C_Mem_Read返回成功但读出来的 buffer 每个字节都是 0xFF或者偶尔返回超时。原因大多数情况是器件地址没有左移。地址宏直接写成0x68而不是0x68 1时HAL 发出的从机地址不对总线上没有设备 ACK数据线空闲时被上拉到高读到的就是 0xFF。另一种常见原因是 SCL/SDA 杜邦线接反模块上电了但时钟和数据没对上。解决先把地址宏统一改成(0x68 1)如果模块 AD0 接高电平就改成(0x69 1)。再看接线SCL 对 SCL、SDA 对 SDA不要按颜色记——我见过三根线颜色全对、唯独 SDA 插错位置的。最后用逻辑分析仪抓一下 SCL/SDA能直观看到是否有 ACK 位这是排查 I2C 通信问题最快的手段。5.4 I2C 总线锁死SDA 被拉低后怎么自救现象程序运行一段时间后HAL_I2C_Mem_Read一直返回HAL_BUSY用万用表量 SDA 引脚始终是低电平。原因I2C 协议中如果主设备在传输中途被打断比如被高优先级中断抢占且中断里又操作了 I2C从机可能处于半接收状态并一直拉低 SDA。最常见的是在中断服务函数里直接调用 HAL 库的 I2C 读函数导致事务交错。解决不要在中断里做 I2C 长事务中断只置标志位主循环再处理。已经锁死时用 GPIO 模拟的方式给 SCL 引脚连续翻转 9 个时钟脉冲让从机释放 SDA然后重新初始化 I2C。参考代码void I2C_Bus_Reset(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; HAL_GPIO_DeInit(GPIOB, GPIO_PIN_6 | GPIO_PIN_7); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6 | GPIO_PIN_7, GPIO_PIN_SET); for (int i 0; i 9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); delay_us(10); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); delay_us(10); } MX_GPIO_I2C1_Init(); MX_I2C1_Init(); }如果总线频繁锁死先把 I2C 频率降到 100kHz长线环境下 400kHz 的抗干扰能力要弱不少。5.5 DMP 角度乱跳版本匹配、FIFO 溢出与偏置校准现象裸数据读出来正常但 DMP 输出的四元数换算成角度后明显不对pitch 和 roll 互换或者一震动角度跳变几十度。原因归纳起来有三种。一是inv_mpu.c和inv_mpu_dmp_motion_driver.c不是同一套 Motion Driver 版本导致 FIFO 帧格式解析错位四元数缩放系数对不上。二是 FIFO 溢出后dmp_read_fifo返回错误码但它返回的不是致命错误程序继续用旧数据或半帧数据算角度。三是上电后立刻初始化 DMP陀螺仪零偏还没有稳定DMP 把这个错误的偏置当成了基准。解决Motion Driver 文件要整组替换不要只换其中一个替换后静止平放观察四元数应该收敛在[1, 0, 0, 0]附近。读取时检查dmp_read_fifo返回值返回非零时丢弃这一帧不要送进角度计算。初始化前强制延时 1 秒让陀螺仪充分稳定。模块每次重新上电后静止状态的初始角度会有差异这是正常的DMP 会把当前姿态作为原点。6. 再往前走一步中断读 FIFO 与数据平滑6.1 把轮询改成 INT 中断一帧角度准时到位轮询模式下主循环每 20ms 调一次dmp_read_fifo但主循环里如果插入了其他耗时操作读取节奏会被打乱。MPU6050 的 INT 引脚在 FIFO 数据准备好时会产生一个脉冲把它接上并用外部中断接收角度数据就能精确同步到 DMP 的 50Hz 输出节奏。/* INT引脚接PA4上升沿触发 */ void MX_EXTI4_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_4; GPIO_InitStruct.Mode GPIO_MODE_IT_RISING; GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_NVIC_SetPriority(EXTI4_IRQn, 2, 0); HAL_NVIC_EnableIRQ(EXTI4_IRQn); }在stm32f1xx_it.c里把中断向量导向 HAL 库void EXTI4_IRQHandler(void) { HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4); }中断回调里只置一个标志位不要在中断函数里面调用dmp_read_fifoI2C 事务放在主循环处理volatile uint8_t dmp_ready 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_4) { dmp_ready 1; } } /* 主循环里 */ while (1) { if (dmp_ready) { dmp_ready 0; if (MPU_DMP_Get_Angle(yaw, pitch, roll) 0) { /* 使用这一帧角度 */ } } }这样角度采样间隔严格保持在 20ms不受主循环其他代码影响。6.2 一阶低通比滑动平均更适合控制回路拿到稳定的欧拉角后很多人会随手做一个滑动平均来平滑波形但对控制回路来说滑动平均会引入整帧延迟。同样是 50Hz 采样率两种方式的差异很明显处理方式等效延迟噪声抑制适合场景滑动平均 N5约 40ms好调试显示、波形观察一阶低通 alpha0.2约 30ms中等自平衡、云台控制一阶低通的实现只有一行angle_smooth 0.2f * (angle_raw - angle_smooth);alpha 取 0.2 是经验值在 50Hz 输出下能明显滤掉手部高频抖动同时相位滞后比滑动平均小。做自平衡车时如果觉得响应慢把 alpha 往上调如果角度噪声大往下调。这个参数每块板子都要现场试。关于 INT 和轮询的选择我的结论是只是显示波形轮询够用要接控制回路必须走中断。从那以后我每次拿到一块带 IMU 的新板子都强制走一遍先读 WHO_AM_I、再初始化 DMP、最后调频率和低通系数的流程顺序一步不乱。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑