简介:本资源是一套基于STM32F103与MPU6500的高精度姿态解算完整工程,面向嵌入式开发者、飞控/机器人方向学习者及IMU算法实践者,解决六轴传感器数据采集、SPI高速通信、四元数姿态解算与CAN总线实时输出等核心问题。压缩包共206个文件,涵盖37个头文件(.h,含驱动与算法接口定义)、36个C源码(.c,含MPU6500初始化、SPI读写、四元数更新及CAN发送逻辑)、39个编译中间文件(.o/.d),以及KEIL工程配置(.uvprojx/.uvoptx)、链接映射(.map/.axf)和可执行镜像(.hex),整体大小为7.36MB。已有409人学习下载。工程已实现从原始加速度/角速度数据出发,通过时间积分与四元数微分方程实时更新姿态,并输出俯仰、翻滚、偏航角;代码结构清晰,模块化程度高,包含完整的STM32标准外设库驱动(如can.c、tim.c、rcc.c等),可直接编译运行或用于算法二次开发与教学演示。 上周一个做飞行器的朋友扔给我一个压缩包,文件名很长,叫“MPU6500研发代码(四元数).zip_STM32F103_mpu6500 spi_mpu6500 stm_四元数_姿态解”。解压出来一看,底层是STM32F103通过SPI接口读MPU6500,上层跑的是四元数姿态解算,没有用I2C,也没有依赖DMP硬件解算。这类代码网上确实不少,但真正能拿来改完直接上板跑的版本并不多,很多是抄来抄去连初始化顺序都是错的。我花了半天时间把整个工程捋了一遍,顺手在最小系统板上做了验证,这篇文章就把代码包里的核心逻辑、SPI底层实现、四元数解算流程,以及调试时最容易踩的坑全部拆开讲清楚。
这份代码适合谁看?刚接触MPU6500和四元数姿态解算的开发新手,或者手头有STM32F103最小系统板、想用SPI而不是I2C读取MPU6500的人。如果你已经会读原始数据但姿态一直在飘,这篇文章也能给你一些排查方向。
1. 代码包本质:一套SPI驱动加四元数解算的组合模板
1.1 先搞清楚代码包里有什么
这个代码包不是完整的产品工程,更像一个“研发用模板”。解压之后通常能看到这几块内容:MPU6500寄存器定义头文件、SPI读写函数、初始化代码、原始数据读取代码,以及一套四元数姿态解算算法文件。有的版本还会附带串口打印和匿名上位机协议,方便你把姿态数据发到PC上看波形。
从文件命名能看出,这套代码的重点落在“四元数”上,而不是“DMP”。DMP方式是把姿态解算交给MPU6500内部的数字运动处理器,MCU只需要读结果;而这套代码是用STM32F103当解算主体,从传感器拿到原始角速度和加速度后,在MCU上做滤波和四元数更新。两种方式各有优劣,后面我会详细说。
1.2 为什么选SPI而不是I2C
MPU6500同时支持I2C和SPI接口,但这套代码选择了SPI,原因很实际:SPI通信速率可以跑得比I2C高不少,I2C标准模式只有100kbps,快速模式400kbps,而SPI在F103上即使分频到几MHz也没有压力。对姿态解算来说,你需要以200Hz到1kHz的频率读取数据,SPI的时序更从容,CPU占用也更低。
另一个原因是硬件架构。MPU6500的I2C地址是0x68或0x69,是靠AD0引脚电平决定的,一个I2C总线上接多个MPU6500会非常麻烦。SPI每个设备有独立的片选脚CS,多传感器挂载更灵活。当然,SPI接线比I2C多两根线,对飞控这种对体积敏感的场景来说,I2C也有它的价值。但如果是做研发验证,SPI显然更适合折腾。
1.3 硬件连接:六根线定生死
MPU6500模块上的SPI接口经常被称作“6针SPI”,这6根线一般是这样:
| 模块引脚 | 对应MPU6500引脚 | 接STM32F103 |
|---|---|---|
| VCC | VDD | 3.3V(不能接5V) |
| GND | GND | GND |
| SCL / SCLK | SCLK | PA5(SPI1_SCK) |
| SDA / SDI | SDI | PA7(SPI1_MOSI) |
| AD0 / SDO | SDO | PA6(SPI1_MISO) |
| CS / NCS | CS | PA4或者任意GPIO |
这里有个新手特别容易搞混的点:模块上丝印可能把SDA和SDI标成同一个名字,因为I2C模式下SDA就是数据线,SPI模式下它又变成了MOSI输入线。你如果照着I2C的习惯,把SDA接到MCU的PA6(MISO)上,那就全乱了。正确接法是SDA/SDI接MOSI,AD0/SDO接MISO,方向一定不能反。
CS引脚在SPI通信时是片选信号,低电平有效。这个引脚不能悬空,也不能直接固定接低。有些模块为了兼容I2C模式,把CS通过电阻拉到了VCC,你如果直接把它接地,I2C模式会被禁用,SPI模式才能工作。反过来,如果你用I2C但CS被拉低,也会出问题。这部分最好用万用表确认一下模块原理图。
1.4 拿到代码包后的第一个检查点:硬件SPI还是软件SPI
代码包命名看不出来底层用的是硬件SPI外设还是GPIO模拟SPI,所以打开源码第一件事就是搜“SPI_I2S_SendData”或者“HAL_SPI_TRANSMIT”,有这些就是硬件SPI方式;如果看到一堆GPIO_SetBits和GPIO_ResetBits反复翻转,那就是软件模拟SPI。
硬件SPI的优势是主频高、不占CPU,但引脚固定,F103的SPI1只能用PA5、PA6、PA7;软件SPI可以随意指定GPIO,灵活性高,但是位翻转靠延时循环实现,帧率高了CPU就吃紧,而且时序容易受中断影响。
我拿到这个代码包后看了一眼,底层用的是标准库的SPI1,说明原始作者是在F103标准库工程上写的。如果你手头是HAL库工程,直接复制SPI底层函数会报一堆错,最好把读写函数翻译成HAL库版本,算法部分不受影响。后面我会给出标准库和HAL库两版读写函数的写法。
2. MPU6500的SPI通信协议:读完这一节就能自己写驱动
2.1 寄存器读写与时序规则
MPU6500的SPI时序很简单,本质上是“先发地址,再发数据”的结构。读操作时,你要发送的寄存器地址最高位要置1,表示这是读请求;写操作直接发送寄存器地址,最高位为0。
比如读WHO_AM_I寄存器(地址0x75),读请求字节就是0x75 | 0x80 = 0xF5。发送完地址字节后,紧接着发送一个空字节(0x00),接收到的第二个字节就是寄存器内容。写操作则是在地址字节后直接发送要写入的值。
CS片选信号控制整个通信过程:CS拉低表示开始一次传输,CS拉高表示结束。每次读写都要单独拉低再拉高,不能一直把CS固定为低电平,否则就算SPI时钟正常,数据也进不了寄存器。这个细节在实际工程里经常出问题。
2.2 SPI模式与时钟频率的正确配置
MPU6500支持SPI Mode 0和Mode 3,也就是CPOL/CPHA的两种组合。主流代码一般用Mode 0,即CPOL=0、CPHA=0,在时钟第一个上升沿采样数据。如果你按照标准SPI配置了Mode 0之后,读WHO_AM_I一直返回垃圾值,把模式改成Mode 3试一下,大概率能通。这就是个硬件兼容性的问题,数据手册写得再清楚,实际模块板上的其他电路也可能影响时序。
时钟频率方面,第一次调试不要上来就跑高速。很多代码把SPI1分频设置成4分频或64分频,也就是2.25MHz或1.125MHz,这样比较稳。等通信验证通过之后,再逐步把分频减小,看数据是否依然稳定。MPU6500的SPI理论上能跑更高,但STM32F103的SPI外设在高速下容易受杜邦线长度影响,尤其是飞控上那种十几厘米的排线,频率上去了就会出现偶发错字。
2.3 标准库和HAL库的SPI读写函数对比
无论是标准库还是HAL库,底层逻辑都是一样的。我给你两版可以直接用的参考写法。
标准库版本:
uint8_t MPU6500_ReadByte(uint8_t reg) { uint8_t value = 0; GPIO_ResetBits(MPU_CS_PORT, MPU_CS_PIN); // CS拉低,选中设备 SPI_I2S_SendData(SPI1, reg | 0x80); // 发送读地址(最高位置1) while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); SPI_I2S_ReceiveData(SPI1); // 读走第一个无效字节 SPI_I2S_SendData(SPI1, 0x00); // 发送空字节,触发时钟 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); value = SPI_I2S_ReceiveData(SPI1); // 第二个字节才是有效数据 GPIO_SetBits(MPU_CS_PORT, MPU_CS_PIN); // CS拉高,结束传输 return value; }HAL库版本:
uint8_t MPU6500_ReadByte(uint8_t reg) { uint8_t tx[2], rx[2]; tx[0] = reg | 0x80; tx[1] = 0x00; HAL_GPIO_WritePin(MPU_CS_PORT, MPU_CS_PIN, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 10); HAL_GPIO_WritePin(MPU_CS_PORT, MPU_CS_PIN, GPIO_PIN_SET); return rx[1]; }写函数类似,只是地址字节最高位为0,数据方向相反。有一点要特别提醒:HAL库的HAL_SPI_TransmitReceive是阻塞式传输,如果你在主循环里以高频调用,要注意超时时间参数别写太小,否则偶发超时会直接把函数卡死。对于这种单次只有12字节的读取,10毫秒超时绰绰有余。
2.4 硬件NSS和软件CS的坑
很多人用F103的SPI1做驱动时会遇到一个奇怪的故障:代码看起来没问题,但读WHO_AM_I永远返回0xFF。排查到最后发现是SPI_NSS配置问题。
标准库里初始化SPI时有个结构体成员是SPI_NSS,如果配置成SPI_NSS_Hard,硬件会自动控制PA4(NSS引脚)的电平,你手动操作PA4就失效了。MPU6500的CS大部分时间应该由软件控制,所以一定要配置成SPI_NSS_Soft,然后手动用GPIO翻转CS。这是一个特别容易被忽略的底层细节,我见过不止一个项目在这里卡了一整天。
HAL库对应的写法是:
hspi1.Init.NSS = SPI_NSS_SOFT;配置成软件NSS之后,CS引脚完全由GPIO控制,SPI外设不再干预这个引脚电平。
3. 初始化流程和数据读取:代码包能不能用的关键在这里
3.1 上电初始化顺序不能乱
MPU6500上电后的初始化顺序,决定了后续数据是否正常。这个代码包的初始化流程大概是这样的:
第一步,上电之后延时100毫秒左右,让电源稳定。 第二步,向PWR_MGMT_1寄存器(0x6B)写入0x80,执行设备复位。这一步会把所有寄存器恢复到默认值。 第三步,再延时100毫秒。 第四步,向PWR_MGMT_1写入0x00,唤醒传感器。很多新手在这一步漏掉,结果陀螺仪和加速度计读出来全是0,因为芯片一直处于睡眠模式。 第五步,设置陀螺仪量程和加速度计量程。 第六步,配置DLPF低通滤波器和采样率分频。 第七步,读取WHO_AM_I做校验,确认SPI通信正常。
WHO_AM_I寄存器在0x75,MPU6500的默认值应该是0x70。注意MPU6050的WHO_AM_I读出来常见值是0x68,而MPU6500是0x70,两者不一样。如果你用的是MPU6500模块,读出来0x68就要怀疑是不是买到了标成6500的6050,或者SPI时序有问题。
3.2 寄存器配置和采样率计算
代码包里通常会配置陀螺仪量程为±2000dps,加速度计为±16g,这是最常用的保守配置。对应关系要记清楚:
| 量程 | 加速度计灵敏度(LSB/g) | 陀螺仪灵敏度(LSB/dps) |
|---|---|---|
| ±2g / ±250dps | 16384 | 131 |
| ±4g / ±500dps | 8192 | 65.5 |
| ±8g / ±1000dps | 4096 | 32.8 |
| ±16g / ±2000dps | 2048 | 16.4 |
读出来的原始值是int16型,要换算成物理量,必须除以对应的灵敏度。比如量程设置为±16g时,加速度原始值32600,换算后大约是32600 / 2048 = 15.9g,这个数字是合理的。如果量程设置和换算系数不匹配,姿态解算出来的角度会有明显的错误。
采样率计算涉及SMPLRT_DIV寄存器。当DLPF开启时,陀螺仪内部采样率固定为8kHz;当DLPF关闭时,采样率为1kHz。SMPLRT_DIV的值等于内部采样率除以期望采样率再减1。比如想要500Hz采样率,DLPF开启的情况下,SMPLRT_DIV = 8000 / 500 - 1 = 15,十六进制就是0x0F。
很多代码包直接把这个寄存器写死成0x07或者0x00,也没解释原因。你在移植到自己工程时,要根据实际控制周期重新算一遍。姿态解算的dt如果和采样率对不上,四元数更新会偏快或偏慢,表现就是静止时姿态还在漂。
3.3 连续读取原始数据的技巧
读取加速度计和陀螺仪数据时,最好一次性连续读出所有寄存器,而不是分多次单字节读。原因是MPU6500内部传感器数据是不断更新的,如果你先读加速度计X轴,然后再读Y轴,中间可能已经混入了另一帧数据,导致一组数据里各个轴的采样时刻不一样。这在静止时看不出问题,一旦运动起来就会出现非常奇怪的抖动。
标准做法是从ACCEL_XOUT_H(0x3B)开始,连续读取14个字节,把加速度计三轴、温度、陀螺仪三轴全部读回来。MPU6500的寄存器地址是连续排列的,完全支持这种突发读模式。SPI突发读和单字节读的区别,在于CS只需要拉低一次,然后连续发送多个地址和数据字节,中间不需要拉高。
连续读取一次14字节,在1MHz SPI时钟下大约需要112个时钟周期,耗时不到0.1毫秒。如果你用软件模拟SPI,这个耗时会长一些,但仍然比I2C快得多。
4. 四元数姿态解算:代码里那段“数学”到底在算什么
4.1 为什么姿态解算首选四元数
欧拉角用三个角度表示姿态,非常直观,但有万向锁问题:当俯仰角接近±90°时,横滚和航向会失去区分度,出现数值跳变。四元数用四个参数表示旋转,不存在万向锁,而且计算旋转只需要做四元数乘法,比三角函数更高效。
代价是四元数不够直观,你很难直接看出当前姿态是多少度。所以实际工程里通常是内部用四元数做姿态更新,输出时再转换成欧拉角。
代码包里那段四元数更新代码,本质上是在做一件很朴素的事:用陀螺仪测到的角速度对四元数做积分,再用加速度计(如果有磁力计还会加上磁力计)修正积分产生的漂移。
4.2 陀螺仪积分四元数更新的公式
四元数更新的一阶近似公式如下,这一步在代码里通常长这样:
q0 += 0.5f * (-q1 * gx - q2 * gy - q3 * gz) * dt; q1 += 0.5f * ( q0 * gx + q2 * gz - q3 * gy) * dt; q2 += 0.5f * ( q0 * gy - q1 * gz + q3 * gx) * dt; q3 += 0.5f * ( q0 * gz + q1 * gy - q2 * gx) * dt;这里gx、gy、gz的单位必须是弧度每秒(rad/s)。MPU6500读出来的是角速度,经过灵敏度换算后是度每秒(dps),大多数代码包会在进入四元数更新前乘上一个系数:
gx *= DEG2RAD; // 0.01745329f gy *= DEG2RAD; gz *= DEG2RAD;这个系数在很多代码包里被隐藏得很深,有时候写在main函数里,有时候写在解算函数内部。如果你发现姿态在剧烈旋转时角度完全对不上,先检查这个单位转换有没有漏掉。
dt是采样周期,用秒为单位。如果你的解算循环实际运行周期是2毫秒,dt就应该填0.002f。这里不能直接用固定常量,最好通过定时器或者系统滴答实时计算。代码包里的固定dt在短时间测试时没问题,运行时间长了,时钟误差会累积成明显的漂移。
4.3 互补滤波和梯度下降:两类主流算法的取舍
代码包里最常见的四元数姿态解算算法有两类。一类是Mahony互补滤波,代码里会出现Kp、Ki两个参数;另一类是Madgwick梯度下降法,代码里会出现beta参数。
Mahony的核心思路是:加速度计测量值归一化后,和四元数推算出的重力分量做叉积,得到姿态误差,再用PI控制器修正陀螺仪零偏,最后做四元数积分。它代码量少,在STM32F103上跑200Hz毫无压力。
关键代码如下:
// 加速度计归一化 norm = invSqrt(ax*ax + ay*ay + az*az); ax *= norm; ay *= norm; az *= norm; // 用四元数推算重力在机体坐标系的分量 vx = 2.0f*(q1*q3 - q0*q2); vy = 2.0f*(q0*q1 + q2*q3); vz = q0*q0 - q1*q1 - q2*q2 + q3*q3; // 叉积求误差 ex = ay*vz - az*vy; ey = az*vx - ax*vz; ez = ax*vy - ay*vx; // 误差积分 exInt += Ki*ex*dt; eyInt += Ki*ey*dt; ezInt += Ki*ez*dt; // 修正陀螺仪 gx += Kp*ex + exInt; gy += Kp*ey + eyInt; gz += Kp*ez + ezInt;Kp和Ki的调整经验:Kp默认2.0、Ki默认0.02是比较常见的起点。Kp越大,加速度计对姿态的纠正越强,动态响应越快,但噪音也越大;Kp太小时姿态会慢慢飘。如果是静止测试,先把Ki设为0,只调Kp,等静止姿态稳定了再逐步加Ki抑制长时间漂移。
Madgwick算法则用梯度下降法去求解“加速度计和磁力计给出的姿态误差”,误差值用beta参数控制。beta值越大越信任加速度计,越小越信任陀螺仪。典型值是beta=0.1,如果你的应用动态运动很多,可以适当调大到0.2左右,但注意不要超过0.3,否则姿态会显得“粘手”,反应迟钝。
4.4 四元数转欧拉角:公式、顺序和范围
四元数最终要转成欧拉角,代码包里这一段通常固定写成ZYX旋转顺序:
roll = atan2f(2.0f*(q0*q1 + q2*q3), 1.0f - 2.0f*(q1*q1 + q2*q2)); pitch = asinf(2.0f*(q0*q2 - q3*q1)); yaw = atan2f(2.0f*(q0*q3 + q1*q2), 1.0f - 2.0f*(q2*q2 + q3*q3));计算出来roll和yaw范围是-180°到180°,pitch范围是-90°到90°,单位是弧度,转成角度要乘57.29578f。
这里务必注意旋转顺序。同样的四元数,如果采用不同的旋转顺序,转欧拉角的公式完全不一样。市面上有些代码用的是YXRZ或者XYZ顺序,直接套用上面的公式会得到错误结果。判断方法很简单:把模块水平放置,上电后roll、pitch、yaw都应该是0附近;然后分别绕每个轴转动,观察对应角度变化是否正常。如果转动X轴时yaw在变,说明旋转顺序和你的坐标系定义不一致。
另外一个常见需求是把yaw从-180~180映射到0~360,很多上位机显示需要这样的范围。转换很简单:
if (yaw < 0) yaw += 360.0f;4.5 初始四元数:别傻乎乎地固守(1,0,0,0)
大多数代码包在启动时把四元数初始化为q0=1, q1=0, q2=0, q3=0,表示“初始姿态为水平”。如果你的设备上电时是水平放置的,没问题;但如果是竖着放、侧着放,四元数从水平姿态开始收敛,运动起来就会有一个很大的初始误差,需要花不少时间才能纠正过来。
更稳妥的做法是上电后先静止采集几十毫秒的加速度计数据,用加速度计估算出初始的roll和pitch:
roll_init = atan2f(ay, az); pitch_init = atanf(-ax / sqrtf(ay*ay + az*az));然后利用欧拉角转四元数的公式,把初始姿态灌入解算器。在缺少磁力计的情况下,yaw初值设0即可。这个改进在实际项目里效果非常明显,尤其是自平衡车这种一上电就需要知道当前大致角度的设备。
5. 从代码包到上板运行:遇到的问题和排查思路
5.1 SPI通信失败的排查顺序
我把这套代码烧进STM32F103最小系统板后,第一次运行就读不到正确的WHO_AM_I,返回却是0xFF。当时按这个顺序排查,很快定位到了问题:
第一,检查CS引脚配置。CS输出模式必须配置成推挽输出,不能是开漏。开漏输出在高电平时需要外部上拉,很多最小系统板上PA4并没有上拉电阻,就会导致CS电平不稳定。
第二,检查SPI引脚复用功能。标准库需要开启AFIO时钟,并且把PA5、PA6、PA7配置成复用推挽输出。如果你只是把它们配置成普通GPIO,SPI外设的信号根本出不来。
第三,检查SPI模式。先固定用Mode 0,如果不行换成Mode 3。很多时候不是硬件坏,而是时序不匹配。
第四,用逻辑分析仪抓SCLK、MOSI、CS三根线的波形,确认CS在每次传输前确实拉低,传输结束后拉高。如果CS一直为低,说明代码里某个地方把它永久锁住了。
5.2 数据异常跳变和零漂的处理
SPI通信正常后,姿态数据还是可能乱跳。大部分原因集中在三点。
第一个是DLPF低通滤波器没配置。MPU6500的原始加速度计和陀螺仪数据带有高频噪声,直接进姿态解算会让输出角度抖得很厉害。把DLPF配置成带宽42Hz左右是一个比较合理的折中,既能滤掉大部分机械振动,又不会让响应迟钝太多。
第二个是陀螺仪零偏没有补偿。每个MPU6500芯片的陀螺仪零偏都不一样,上电静止时你读到的角速度不是0,可能是10dps甚至更高。四元数积分会把这些零偏累积成角度漂移。解决办法是上电后静止采集500到1000个样本,取平均值作为零偏,之后每次读取数据都减去这个值。
代码实现可以参考:
for (i = 0; i < 500; i++) { mpu9250_read_gyro(&gx_raw, &gy_raw, &gz_raw); gx_offset += gx_raw; gy_offset += gy_raw; gz_offset += gz_raw; } gx_offset /= 500; gy_offset /= 500; gz_offset /= 500;第三步是检查dt是否和解算循环的实际周期一致。F103跑四元数解算时,如果循环里还有OLED刷新、串口打印等耗时操作,循环周期会不稳定。这时固定dt会导致姿态更新速度时快时慢。更可靠的做法是用定时器计时,把实际时间间隔算出来作为dt。
5.3 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| WHO_AM_I返回0xFF | SPI模式不对、CS没拉低、引脚配置错误 | 换Mode 0/3,检查CS电平,确认复用功能 |
| 数据全为0 | 芯片处于睡眠模式 | PWR_MGMT_1写入0x00唤醒 |
| 加速度计数值异常偏小 | 量程配置和换算系数不匹配 | 按AFS_SEL正确设置灵敏度 |
| 静止时角度缓慢漂移 | 陀螺仪零偏未补偿 | 上电静止采集均值,做零偏校正 |
| 运动时角度严重失真 | 单位没从dps转成rad/s | 检查DEG2RAD是否应用 |
| 姿态在高频抖动 | DLPF带宽太高 | 把DLPF配置到42Hz附近 |
| 数据偶尔错乱 | SPI时钟过高、杜邦线过长 | 降低SPI时钟,缩短线缆长度 |
5.4 这套代码还能怎么扩展
验证通过之后,这套模板可以往很多方向扩展。我在实际项目中常用的一种改法是:把MPU6500的数据读取和四元数解算从主循环里拆出来,放到定时器中断里固定1ms执行一次,然后用一个全局结构体保存姿态结果,主循环只负责消费数据。这样做的好处是解算周期稳定,不受主循环其他任务影响。
另一种常见做法是把四元数解算结果通过CAN总线上报给其他控制器。F103的CAN外设和SPI不冲突,PA11、PA12做CAN收发,PA5、PA6、PA7做SPI传感器,正好错开。这套MPU6500的SPI驱动可以直接复用,不需要额外改动。
说到底,MPU6500不外乎读传感器、做解算、对外输出姿态这三件事。把这个代码包读明白了,回头再看MPU6050甚至IMU9250之类的芯片,你会发现寄存器兼容性极高,驱动移植只需要改WHO_AM_I校验值和少数寄存器地址。我后来换用另一款传感器,底层驱动差不多是复制的,主要是四元数解算算法没动过。
最后再分享一个小经验:调这类代码时,建议先用串口把所有中间变量打出来,包括原始加速度计、原始陀螺仪、换算后的物理量、q0到q3,以及最终欧拉角。不要只盯着最终角度看,否则一旦出错,你根本分不清是传感器读数的问题、单位换算的问题,还是四元数更新算法的问题。把中间量一层层打出来,对比每一层的数据是否合理,问题就能迅速缩小范围。我靠这个方法省下过很多排查时间,希望你用不上,但真出问题时一定用得上。
本文还有配套的精品资源,点击获取