简介:MPU6050六轴传感器集成了三轴陀螺仪与三轴加速度计,是机器人、无人机、智能穿戴及物联网设备中获取运动姿态的核心器件。这份面向嵌入式开发者的源码包,围绕真实运动场景下的位移测算需求,提供从底层寄存器配置到上层应用计算的完整实现,可直接迁移到STM32等主流平台进行二次开发。资源共77个文件,以38个.h头文件与35个.c源文件为主体,覆盖驱动、I2C/USART通信、MotionDriver运动处理库以及时钟和LED辅助模块,另含工程文件与STM32启动文件,便于直接编译烧录。压缩包整体仅328KB,结构精简、模块划分清晰,适合有一定单片机基础的学习者快速上手。目前已有224人学习下载,源码中包含主逻辑与位移测算算法的整合要点,对照代码可重点理解加速度积分、姿态解算在位移计算中的衔接方式,省去自行搭建工程和调试驱动的繁琐过程。
1. 为什么MPU6050算位移比算姿态更折腾
MPU6050几乎是嵌入式惯性传感的入门配置:三轴陀螺仪加三轴加速度计,拉一条I2C总线就能读到原始数据。网上绝大多数教程止步于姿态角,但当你把加速度再做两次积分换成位移时,问题才会真正暴露出来。加速度计噪声经过双重积分会变成随时间的平方发散的漂移,设备静止情况下,几秒钟就能跑出几十厘米的“虚位移”。这套工程把位移测算做成可下载、可直接烧录验证的STM32F10x程序,省去了从零搭驱动和移植姿态解算库的工序,适合需要快速在嵌入式设备上验证“能不能用惯性方式监测移动距离”的开发者。我在拆解这份源码时,重点看了它的标定流程、积分策略和滤波参数,下面按数据链路、算法实现、移植调优的顺序展开。
2. 六轴数据链路与源码工程结构:从寄存器配置到工程模板
2.1 加速度计与陀螺仪在同一个芯片里怎么协作
MPU6050内部集成3轴MEMS加速度计和3轴MEMS陀螺仪,通过内置16位ADC输出数字量。加速度计感知比力(含重力),陀螺仪感知角速度。位移测算的常规路线是:加速度计数据经坐标变换到世界坐标系,减去重力分量,得到线性加速度,再对时间做两次积分。这个流程里任何一个环节的精度不足,最终位移都会失真,所以理解传感器内部寄存器比照搬代码更重要。
芯片的传感器量程是可编程的:加速度计可选±2g、±4g、±8g、±16g,陀螺仪可选±250、±500、±1000、±2000 dps。量程直接影响分辨率。以±2g为例,16位ADC对应满量程32768,则每个LSB约0.061mg;如果工作在±16g,分辨率降到0.488mg/LSB,但能承受更大冲击。位移测算场景运动加速度一般不大,选±2g和±4g更合适。
| 量程 | 满量程值 | 灵敏度 | 每个LSB对应物理量 |
|---|---|---|---|
| ±2g | 2g | 16384 LSB/g | 0.061 mg |
| ±4g | 4g | 8192 LSB/g | 0.122 mg |
| ±8g | 8g | 4096 LSB/g | 0.244 mg |
| ±16g | 16g | 2048 LSB/g | 0.488 mg |
为什么要在意这个数?因为位移是对加速度的二次积分,加速度每增加1个LSB的量化噪声,位移的漂移就会被放大到数量级不同的程度。选取更小的量程,相当于在ADC层面增加有效位数,这是源码里ACCEL_CONFIG设置为0的直接原因。
2.2 工程目录里各文件夹的真实职责
下载解压后,代码结构并不复杂,但第一次接触的人容易在CMSIS和MotionDriver之间迷路。逐个说明:
CMSIS:ARM Cortex-M内核设备头文件和启动文件,属于芯片底层支持。library:STM32标准外设库,包含GPIO、I2C、USART等外设驱动。inc和src:用户代码的头文件和源文件,MPU6050驱动和算法主要在这里。user:主函数、中断服务程序、系统配置文件main.c、stm32f10x_it.c。MotionDriver:InvenSense官方的MPU运动驱动库,负责姿态解算,里面包含DMP的调用接口。LED和clock:指示灯和系统时钟的初始化源码。project/MD移植.uvproj:Keil MDK工程文件,组织起以上所有路径。
这个分层的意义在于:底层寄存器读写交给CMSIS和标准外设库,运动解算算法放在独立的MotionDriver层,应用逻辑集中在user和src。如果后面换到STM32F4或HAL库,只需要改写底层I2C接口,姿态解算和位移算法都能原样保留。很多人在网上找“MPU6050 STM32例程”时拿到的代码都是把驱动和算法揉在一个文件里,看起来简单,改起来很难,而这份工程的结构更适合二次开发。
如果直接打开project/MD移植.uvproj报找不到头文件,先检查C/C++选项卡里的Include Paths是否包含\inc、\library\inc、\CMSIS这几个路径。Keil工程换电脑后经常出现绝对路径失效,这时把路径改为相对路径,也就是..\inc这种写法,重新编译一次就能定位缺了哪些依赖。这份源码的stm32f10x_conf.h也暗示它使用的是标准外设库版本,不要和HAL库头文件混用。
2.3 关键初始化参数:I2C地址、采样率和DLPF滤波
MPU6050默认I2C设备地址是0xD0(7位地址0x68左移一位),SCL速率一般设400kHz,从机地址由AD0引脚决定。下面这段初始化代码在src目录的mpu6050.c中,读一下就能看出整套系统的采样策略:
#define MPU6050_ADDR 0xD0 void MPU6050_Init(void) { I2C_WriteByte(MPU6050_ADDR, PWR_MGMT_1, 0x00); // 解除休眠 I2C_WriteByte(MPU6050_ADDR, SMPLRT_DIV, 0x07); // 采样率=1kHz/(1+7)=125Hz I2C_WriteByte(MPU6050_ADDR, CONFIG, 0x06); // DLPF带宽约5Hz I2C_WriteByte(MPU6050_ADDR, GYRO_CONFIG, 0x08); // 陀螺仪±500dps I2C_WriteByte(MPU6050_ADDR, ACCEL_CONFIG, 0x00); // 加速度计±2g I2C_WriteByte(MPU6050_ADDR, INT_ENABLE, 0x01); // 数据就绪中断使能 }参数说明:
PWR_MGMT_1置0是让传感器从休眠进入正常工作状态,芯片默认上电是睡眠模式。SMPLRT_DIV为7时,内部1kHz时钟分频得到125Hz采样率,对一般位移测算足够。CONFIG设为6,数字低通滤波器(DLPF)截止频率大约5Hz,用于滤除高频振动噪声。位移测算更关注低频运动,可以接受一定的相位延迟。- 陀螺仪选±500dps,比±250dps有更大的角速度范围,同时分辨率也够用。
- 加速度计量程选±2g是为了保留小信号分辨率。
注意这里采样率和DMP输出频率是两回事。MotionDriver在初始化时会通过寄存器重新配置内部FIFO频率,如果后面用DMP读取四元数,SMPLRT_DIV会被DMP库接管。实际工程中,DMP默认输出频率约200Hz,但上层程序通常只按自己的调度周期读取,串口输出和积分时间戳需要另外对齐。
3. 从原始加速度到位移的算法链:标定、姿态解算和二次积分
3.1 标定零偏:静止时读到的不是真正的0
MPU6050出厂有一定校准值,但PCB装配和焊接应力会引入偏差。如果直接用原始数据积分,静止状态下加速度计均值可能是0.05g,陀螺仪零偏可能是每秒几度。经过二次积分,这个偏差会变成按时间平方增长的抛物线漂移,所以标定是所有位移测算的第一步。
常见做法是上电后让设备静止1到2秒,取前N个样本求均值,再在后续采样中减掉。工程里通常把三个轴的偏移量存成全局数组:
int16_t accel_offset[3] = {0}; int16_t gyro_offset[3] = {0}; void MPU6050_Calibrate(uint16_t sample_count) { int32_t acc_sum[3] = {0}, gyro_sum[3] = {0}; for (uint16_t i = 0; i < sample_count; i++) { MPU6050_ReadAccel(accel_raw); MPU6050_ReadGyro(gyro_raw); for (int j = 0; j < 3; j++) { acc_sum[j] += accel_raw[j]; gyro_sum[j] += gyro_raw[j]; } } for (int j = 0; j < 3; j++) { accel_offset[j] = acc_sum[j] / sample_count; gyro_offset[j] = gyro_sum[j] / sample_count; } }这里有一个容易踩的坑:加速度计的零偏不能简单把均值全减掉,因为静止时加速度计测得的是重力1g,而不是0,直接减去均值等于把重力也删掉了。正确做法是陀螺仪做零偏减除,加速度计只用于姿态初始对准,或者交给DMP的校准流程去处理。MotionDriver库内部有一套更完整的校准,会判断设备是否静止,并同时修正陀螺仪零偏和加速度计模长。
3.2 为什么在积分前必须做姿态解算
位移测算中的加速度应该相对世界坐标系描述,而传感器读数是相对于自身坐标系的。设备一旦倾斜,重力就会投影到某个轴,产生虚假的加速度读数。举个例子:一个静止但倾斜30°的设备,在X轴上能读到约0.5g的分量,如果不做姿态解算,积分器会把这段“假加速度”当成持续运动,位移飞速增长。因此必须先用陀螺仪和加速度计做姿态解算,得到载体到世界的旋转关系。
有些精简教程会直接用加速度计反三角函数求俯仰角和横滚角,再手动构造旋转矩阵。这样做的缺陷是动态环境下加速度计噪声会被放大,姿态角抖动严重。DMP的优势在于把陀螺仪和加速度计融合在片内motion引擎中,由芯片直接输出四元数,CPU只负责读取和积分。对于128KB Flash的STM32F103而言,DMP还能减少主控的计算负担。当然,代价是DMP固件中内置的算法不可修改,对特殊运动模型不够灵活;如果你的设备要持续高g运动或快速翻转,还是得回到Mahony或互补滤波自己写。
在这套工程中,MotionDriver承担了姿态解算工作。主要调用如下:
dmp_init(); dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT); dmp_set_fifo_rate(50);DMP以FIFO形式输出四元数,频率可通过dmp_set_fifo_rate设定。拿到四元数后,需要构造旋转矩阵把加速度计数据从载体坐标系投影到世界坐标系:
float accel_n[3]; accel_n[0] = accel_x * (1 - 2*q2*q2 - 2*q3*q3) + accel_y * (2*q1*q2 - 2*q0*q3) + accel_z * (2*q1*q3 + 2*q0*q2); accel_n[1] = accel_x * (2*q1*q2 + 2*q0*q3) + accel_y * (1 - 2*q1*q1 - 2*q3*q3) + accel_z * (2*q2*q3 - 2*q0*q1); accel_n[2] = accel_x * (2*q1*q3 - 2*q0*q2) + accel_y * (2*q2*q3 + 2*q0*q1) + accel_z * (1 - 2*q1*q1 - 2*q2*q2);这里的四元数应由DMP保证归一化。如果后续发现位移呈方向性漂移,先检查四元数模长是否在1附近,偏离超过1%就说明四元数质量问题。更稳妥的做法是显式归一化,成本只是四次浮点乘法和一次平方根。
投影之后,把Z轴减去重力加速度值。若前面加速度用g为单位,减1.0;若换算成m/s²,减9.81。单位不统一是这类算法最常见的低级错误,经常导致位移结果成倍偏大或偏小。
3.3 离散积分实现:梯形积分和泄漏抑制
得到世界坐标系下的线性加速度后,位移计算就是标准的两重积分。工程里通常用梯形积分,比矩形积分误差更小,而且代码很清晰:
float velocity[3] = {0}, displacement[3] = {0}; float prev_velocity[3] = {0}; void integrate_step(float dt) { for (int j = 0; j < 3; j++) { float acc = accel_n[j]; velocity[j] += (prev_accel[j] + acc) * 0.5f * dt; displacement[j] += (prev_velocity[j] + velocity[j]) * 0.5f * dt; prev_accel[j] = acc; prev_velocity[j] = velocity[j]; } }这里有个关键点:dt必须按实际采样间隔来取。如果循环里有耗时操作导致下一帧晚了0.5ms,仍用固定名义dt,位移误差会持续积累。我建议在每次进入积分前用定时器读取计数值,算出差值再转为秒数,而不是HAL_GetTick()两次相减——Tick精度在毫秒级,对积分往往不够。
纯积分无法避免低频漂移,所以工程代码里通常会加入速度衰减系数,也叫做泄漏积分。对每个轴乘上一个接近1但小于1的系数:
velocity[j] = velocity[j] * 0.995f;这个系数意味着速度会随时间缓慢归零,从而抵消一部分缓慢变化的重力分解残留。系数太小会让慢速运动测不准,系数太大又压不住漂移。做位移测算时,0.995是一个可以接受的起点,静止漂移大可以降到0.99,需要测慢速位移则升到0.998。调参时最好能控制设备做一次已知距离的往返运动,观察返回原点后的位移回零情况。
3.4 单位换算和输出量纲
DMP输出的加速度原始值是int16_t,需要按灵敏度换算成g或m/s²。通常会在读取函数里做:
accel_g[0] = (float)accel_raw[0] / 16384.0f; // LSB转g,±2g档位 accel_m_s2[0] = accel_g[0] * 9.80665f; // g转m/s²速度和位移也分别保存为m/s和m。串口输出时如果要省空间,可以用整数型放大1000倍发送,例如位移0.352m输出352。这样既能保留精度,也避免浮点printf带来的Flash占用。
4. 在STM32F10x上移植与调优:串口输出、DMP裁剪和滤波参数
4.1 移植陷阱:裁剪MotionDriver时别动这三处
很多下载了这个工程的人,包括有几年嵌入式经验的人,第一反应是MotionDriver太占内存,想删掉换成自己写的互补滤波。我不太建议这么干,DMP库内部状态机有严格的调用顺序,临时裁剪很容易导致四元数输出恒为零,或者姿态角跟随扰动。
如果确实需要保持原工程结构,至少不能动这三处:
dmp_init()必须在读传感器之前调用,它负责内部状态和DMP固件加载。dmp_set_fifo_rate()设置的频率与FIFO读取loop节奏必须匹配,频率过高容易触发FIFO溢出。dmp_get_packet()在FIFO空时返回非0,循环里要有超时退出,避免死等。
另外,裁剪时不要把inv_mpu_core.c里的寄存器缓存机制删掉。它保存当前传感器配置,后续重新初始化时能快速配置,也避免多次写寄存器带来时序问题。如果你用的是HAL库移植,I2C读写函数要重新实现为HAL_I2C_Mem_Write/Read,但MPU6050寄存器地址没变,核心算法逻辑不受影响。网上搜“mpu6050 hal库”能看到很多示例,本质上和标准外设库是对应的。
4.2 串口输出数据格式与上位机观测
源码中通过USART把加速度、速度和位移发到上位机。调试阶段建议用简单文本帧,按固定周期打印9个float。下面是一个重定向printf后的输出片段:
printf("%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f,%.2f\n", ax, ay, az, vx, vy, vz, dx, dy, dz);如果你在用Keil MDK,浮点printf默认可能不工作。最常见的解决方法是勾选工程选项里的“Use MicroLIB”,把C库换成精简版;更通用的做法是全部转成整型输出,把浮点数乘以1000后按int32_t打印,这样既减小代码体积也避免浮点格式化失败。下表是我调试位移时常用的输出格式取舍:
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 浮点printf文本 | 可读性好,能直接进Excel | 占Flash和CPU | 调试前期 |
| 整型文本(×1000) | 省资源,解析简单 | 小数位固定 | 长时间记录 |
| 二进制帧 | 带宽低,效率高 | 需要上位机解析 | 正式跑数据 |
实际使用时,我一般把输出周期设在20ms到50ms,也就是20Hz到50Hz。输出频率不需要等于积分频率,积分仍按125Hz执行,串口只是按自己的节奏抽样打印。这样既能看到完整波形,又不阻塞主循环。
4.3 滤波参数怎么选:不迷信单一DLPF配置
DLPF带宽越低,加速度噪声越小,但信号延迟越大。位移测量关心的是运动包络,相位延迟影响不大,所以很多人都直接用最低频段,这是可以的。但也要看运动本身的频谱:如果测量对象是机械臂末端,运动频率能到10Hz以上,把DLPF设成5Hz就会把真实加速信号滤平,测出来的位移偏小。
建议用两个实验来定参数:
第一个实验是静态零漂测试。把板子固定在桌面上,供上电等系统稳定,观察打印的dx,dy,dz。10秒内漂移超过1cm,说明噪声抑制不够。可以尝试把CONFIG从0x04(约21Hz)降到0x05(约10Hz)或0x06(约5Hz),同时检查速度衰减系数。
第二个实验是动态往返测试。手持设备沿X轴推出去30cm再拉回来,观察位移是否到30cm且回到0附近。如果动态位移明显偏小,优先提高DLPF截止频率,或者把加速度波形在积分前做一次均值平滑。如果回不到0,多半是速度衰减系数过小或姿态解算有残余误差。
实测中,我用10Hz DLPF和0.995的衰减系数,静止漂移在60秒内约8mm,动态推拉30cm测试误差约2cm。如果把DLPF降到5Hz,静止漂移可以到4mm,但30cm动态测试误差反而增加到4cm,因为真实加速信号被削掉了。所以参数优化的着眼点不是单一指标,而是应用场景中“多长时间”和“多大运动范围”更重要。
这里有一组我调完的参考参数,注意不同硬件布局差异很大:
| 参数 | 参考值 | 说明 |
|---|---|---|
| ACCEL量程 | ±2g | 精度优先 |
| DLPF截止频率 | 10Hz | 对噪声和动态响应折中 |
| 采样率 | 125Hz | 满足100Hz以内运动 |
| 速度衰减系数 | 0.995 | 静态漂移和动态响应平衡 |
| 积分频率 | 与采样率一致 | 防止时间偏差 |
还有一个常见坑:在板子上电但还没水平放置的时候就开始积分,初始化瞬间会有很大的重力分量。源码中的解决方案通常是延迟启动,或者把开始积分的第一个速度强制置零。
5. 可靠性验证:静态零漂测试与动态轨迹复盘
这里给出一个我反复使用的验证流程。先把MPU6050固定到一块铝板上,铝板调平,上电等待DMP初始化完成,然后持续记录位移输出60秒。理想情况下,dx、dy、dz都应该小于1cm。如果超过5cm,先检查加速度计零偏和姿态解算输出是否稳定,再调速度衰减系数。
验证目标可以量化为三组:
- 静止60秒,位移漂移小于0.01m,速度漂移小于0.02m/s。
- 沿X轴平推30cm,输出位移为0.30m±3cm,且Y/Z轴输出不超过2cm。
- 推回原位后,位移累计值恢复到0附近,误差不超过5cm。
如果静态漂移大,先打印未积分的线性加速度。静止时它应该在0.00附近波动,波动幅值超过0.05g就说明DLPF带宽太高或标定不充分。此时可以看MotionDriver输出的原始四元数有无突变,如果四元数突变,多半是FIFO读取中断,而不是算法问题。
动态轨迹复盘建议用二分法:把运动拆成“启动-匀速-停止”三段,分别观察每段速度变化。启动段速度不能突变,停止段速度应快速回零。如果减速后位移还在缓慢增长,说明速度衰减参数作用不够,或者停止瞬间加速度残差没有被滤掉。调节时每次只改一个参数,例如只把衰减系数从0.995改为0.993,再做一轮往返测试。
最后一个实用技巧:在代码里加上上电后丢弃前200个样本的逻辑。上电瞬间I2C不稳定,DMP也需要约100ms固定时间,前若干个位置解算结果可能异常。让程序延迟200ms再开始积分,代码可以这样写:
for (int i = 0; i < 200; i++) { dmp_read_fifo(...); delay_ms(1); }不要把这部分数据计入积分窗口,否则启动时的异常加速度会成为初始位移偏移,最终结果整体偏掉一个常数。这个细节在别人给的源码里通常不会高亮,但很多时候多出来的“起始偏移”正是因为它没有被跳过。把这步做完,再配合串口打印对齐时间戳,位移测算就可以真正拿去做相对距离判断了。
本文还有配套的精品资源,点击获取