ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

树莓派Pico与MPU6050六轴传感器:基于I2C的MicroPython驱动与姿态估算

树莓派Pico与MPU6050六轴传感器:基于I2C的MicroPython驱动与姿态估算 1. 项目整体设计思路与硬件准备树莓派Pico加上MPU6050这个组合是我这个“树莓派100讲”系列里第34个项目。说实话在折腾完前面一堆GPIO点灯、PWM呼吸灯、PWM驱动蜂鸣器这些小玩意儿之后终于到传感器环节了。MPU6050这颗六轴传感器几乎是所有玩嵌入式、做智能硬件的人都绕不开的一颗芯片——三轴加速度计加三轴陀螺仪成本低、资料多、应用广从四轴飞行器的姿态解算到平衡小车的倾角检测再到手环里的步数统计全都有它的身影。在正式开始之前先把这颗传感器掰开揉碎讲明白。MPU6050本质上是两颗MEMS传感器的集成一颗三轴加速度计负责测量物体的加速度单位是g可以用于检测运动状态、倾斜角度一颗三轴陀螺仪负责测量物体绕三个轴的旋转角速度单位是°/s可以用于检测旋转和方向变化。为什么要集成为一颗芯片因为两者放在同一颗芯片里物理距离足够近可以减少数据融合时的误差。当然这是后话。1.1 为什么选择MPU6050而不是其他传感器市面上的姿态传感器很多ICM-20602、BMI160、LSM6DS3都是不错的替代品但我还是首推MPU6050。原因非常简单资料极其丰富踩坑成本低。无论你用Arduino、STM32还是树莓派Pico都有大量现成代码可以用。搜索一下就能找到各种语言的驱动库社区解决方案多到你根本不需要从零造轮子。另一个原因是接口友好。MPU6050支持I2C和SPI两种通信接口默认是I2C模式这也意味着你只需要两根信号线SCL时钟和SDA数据就能读到全部数据连线极其简单非常适合新手入门。相比之下很多新传感器只支持SPI虽然速度更快但接线和调试的复杂度明显上升。还有一个因素这颗芯片虽然发布有些年头了精度比不上一两百块的高端IMU但胜在稳定和便宜。裸片几块钱带开发板的模块也就十几块弄坏了一块也不心疼。对于学习阶段来说这个性价比是无可替代的。1.2 硬件清单与接线方案需要准备的东西非常少树莓派Pico开发板一块无论是普通版还是带WiFi的Pico W底层逻辑完全一样接线和代码也基本通用MPU6050模块一块建议买集成了上拉电阻和稳压电路的那种小板子省去很多麻烦杜邦线若干母对母即可面包板一块可有可无四个引脚直接飞线也能用接线是第一个容易翻车的地方。Pico的I2C引脚不是随便选哪两个都行的虽然MicroPython允许你指定任意GPIO作为软I2C引脚但硬件I2C建议使用官方默认的引脚组合。Pico的I2C0默认在GP4SDA和GP5SCLI2C1默认在GP6SDA和GP7SCL我习惯使用I2C0也就是GP4和GP5。接线关系如下MPU6050引脚Pico引脚说明VCC3V3物理引脚36给传感器供电务必用3.3VGNDGND物理引脚38共地SCLGP5物理引脚7I2C时钟线SDAGP4物理引脚6I2C数据线有一个接线误区很常见有人看到MPU6050模块上的VCC引脚直接接到Pico的5V上。如果模块上带稳压芯片还好但很多廉价模块上没有5V直接进去有可能烧毁芯片。Pico本身也是3.3V逻辑传感器模块用3.3V供电两边电平天然兼容没有任何理由去碰5V。这个细节在调试时一旦出错轻则数据异常重则芯片冒烟。另外AD0这个引脚在多数模块上默认悬空或接地对应的I2C从机地址是0x68。如果AD0接了VCC地址就会变成0x69。这个看起来不起眼的细节后面排查数据读不出来的时候会非常有用。接线完成之后再三确认一遍正负极。我一开始也犯过这种低级错误电源反接导致模块发烫虽然最后运气好没烧但教训是深刻的。接好线之后把USB线插上电脑后面就要进入软件环节了。2. 软件环境搭建与I2C通信原理解析树莓派Pico和传统树莓派最大的区别在于Pico是单片机MCU需要把MicroPython固件烧录到板子里然后通过交互式解释器执行代码。没有Linux系统没有桌面环境它干的事情就是跑你写的那几行代码。但正因为轻量Pico的实时性比跑Linux的树莓派好得多特别适合直接控制传感器和执行底层逻辑。2.1 固件烧录与MicroPython环境准备如果Pico还是全新的第一次连上电脑时电脑会识别出一个名为“RPI-RP2”的U盘。这是Pico的出厂引导加载程序我们需要把MicroPython固件一个UF2格式的文件拖进这个U盘里Pico就会自动烧录并重启。之后再用串口连接板子时看到的就不再是U盘而是一个USB串口设备了。具体步骤在官方文档里写得很清楚但有几个注意点值得提一下下载固件时一定要看清楚是普通Pico版本还是Pico W版本两者固件不通用。Pico W的固件体积更大包含了无线功能刷错型号会导致板子无法启动如果已经烧录过其他固件想重新刷MicroPython可以在按住板载BOOTSEL按钮的同时插入USB线这样会重新进入烧录模式串口驱动在Windows下一般自动安装如果识别不到COM口检查一下是不是用了劣质USB线只有供电没有数据的那种开发环境我推荐用Thonny。这款IDE对MicroPython的支持非常原生打开就能看到Shell交互界面可以直接逐行执行代码调试传感器数据时特别方便。配置上只需要在右下角选择对应的解释器MicroPython (Raspberry Pi Pico)再选择对应的串口号连接成功后Shell里会出现“MicroPython v1.x.x on 2024-xx-xx; Raspberry Pi Pico with RP2040”的版本提示说明环境已经通了。2.2 I2C通信协议到底是怎么回事很多初学者在这一步会卡住不理解为什么传感器能通过两根线传出来几十个数据。其实I2C协议的核心思想没有那么玄乎所有的通信都在SCL时钟线和SDA数据线上进行SCL提供一个统一的时钟节拍SDA在这条时钟线的控制下按位传输数据。这和两个人对话很像——SCL相当于一个节拍器规定什么时候说话、说话节奏多快SDA就是话筒按节拍一个一个比特地把信息传出去。所有接在I2C总线上的设备通过不同的从机地址来区分身份。主机发起通信时先发送一个地址字节总线上只有地址匹配的那个设备才会应答其他设备保持静默这样就实现了多设备共享两条线的通信方式。MPU6050的7位从机地址是1101000二进制换算成十六进制就是0x68。注意在I2C协议里实际发送的地址字节还要左移一位并在最低位加上读/写标志位所以如果直接抓总线看到的地址字节可能是0xD0写或0xD1读。这一点在阅读逻辑分析仪抓取的波形时非常有用但在MicroPython里我们不用操心底层库已经处理好了。Pico为了简化开发内置了硬件I2C外设我们只需要通过machine包调用即可。MicroPython的I2C对象创建方式如下from machine import Pin, I2C i2c I2C(0, sclPin(5), sdaPin(4), freq400000)这里的freq参数是I2C时钟频率400000是400kHz属于快速模式。MPU6050芯片本身支持400kHz但实际测试中如果杜邦线比较长或者模块质量一般400kHz偶尔会出问题表现为数据偶尔丢失或者读到错误的寄存器值。遇到这种情况把频率降到100000问题往往迎刃而解。2.3 用扫描脚本确认通信正常在编写完整的传感器驱动之前先运行一个I2C扫描程序确认Pico能“看到”MPU6050。这一步虽然简单却可以排除掉一多半的硬件问题。from machine import Pin, I2C i2c I2C(0, sclPin(5), sdaPin(4), freq400000) devices i2c.scan() if devices: for device in devices: print(f找到了设备地址: 0x{device:02x}) else: print(没有找到任何I2C设备请检查接线)如果一切正常终端会打印“找到了设备地址: 0x68”。如果什么都没打印按照优先级排查三个地方第一VCC和GND是否接对、接触是否牢固第二SCL和SDA是否接反或者接到了错误的引脚上第三AD0引脚是否被接到了高电平导致地址变成了0x69。我调试的时候还遇到过一种奇怪的情况扫描结果里出现了地址0x68和0x69两个设备。仔细排查后发现是模块质量太差地址引脚的状态不稳定浮动导致同一颗芯片在两个地址间跳变。换一块模块后问题消失。这种玄学故障在廉价传感器模块上并不罕见预算允许的话建议多备一两块。3. MPU6050驱动代码实现与数据换算通信链路打通之后写MPU6050驱动就只是时间问题了。MPU6050内部有上百个寄存器用来控制各种功能和存储数据。我们不需要全部了解只需要关心以下几个关键的寄存器名地址作用PWR_MGMT_10x6B电源管理唤醒芯片、选择时钟源SMPLRT_DIV0x19采样率分频CONFIG0x1A数字低通滤波器配置GYRO_CONFIG0x1B陀螺仪量程配置ACCEL_CONFIG0x1C加速度计量程配置ACCEL_XOUT_H0x3B加速度计X轴数据高字节起始地址GYRO_XOUT_H0x43陀螺仪X轴数据高字节起始地址TEMP_OUT_H0x41温度数据高字节3.1 初始化芯片的关键步骤芯片上电后的默认状态是休眠的如果不“唤醒”读出来的数据全是0。所以初始化时第一件事就是往PWR_MGMT_1寄存器写入0x00让它退出休眠状态。这里有一个细节PWR_MGMT_1寄存器里的DEVICE_RESET位bit 6如果为1芯片会执行复位。我们需要确保这一位是0。直接写入0x00即可满足要求。同时写入0x00还会把时钟源设置为内部振荡器——虽然官方推荐用陀螺仪自带的PLL作为时钟源以减少漂移但对于简单读取数据来说内部振荡器已经足够。接下来配置量程。ACCEL_CONFIG寄存器的低四位是加速度计量程设置默认是0对应±2g。GYRO_CONFIG寄存器的低四位是陀螺仪量程设置默认也是0对应±250°/s。默认量程对大多数应用够用了但如果你做高速旋转的飞控项目就要把量程调到±1000°/s甚至±2000°/s否则数据会溢出。量程越大分辨率越差所以选择的原则是够用就行。采样率方面MPU6050内部的采样率由SMPLRT_DIV寄存器控制计算公式是采样率 陀螺仪输出率 / (1 SMPLRT_DIV)。陀螺仪输出率默认是1kHz所以SMPLRT_DIV设为0就是1kHz设为49就是20Hz。简单的传感器读取项目直接用默认值即可。3.2 MicroPython驱动代码逐段解析完整的驱动代码并不长我按照模块化的方式拆开来讲。from machine import Pin, I2C import math import time class MPU6050: def __init__(self, i2c, addr0x68): self.i2c i2c self.addr addr # 唤醒芯片退出休眠状态 self.i2c.writeto_mem(self.addr, 0x6B, bytes([0x00])) time.sleep_ms(100)构造函数里接受已经创建好的I2C对象和从机地址。writeto_mem是MicroPython中专门用于向I2C设备寄存器写入数据的函数三个参数分别是设备地址、寄存器地址和要写入的字节数据。向0x6BPWR_MGMT_1写入0x00就是唤醒操作。为什么初始化之后要加一个100毫秒的延时因为芯片从休眠状态恢复到正常工作需要一段时间立刻去读数据可能拿到的还是不稳定的初值。延时是一种保险措施类似电脑开机后等系统加载完再操作一样。def read_raw_data(self, reg_addr): # 读取两个字节高字节在前拼成16位有符号整数 data self.i2c.readfrom_mem(self.addr, reg_addr, 2) value (data[0] 8) | data[1] if value 0x8000: value - 0x10000 return valueread_raw_data是读取寄存器数据并拼合成有符号整数的通用函数。MPU6050的每个数据轴占用两个寄存器高字节在前低字节在后需要拼接。拼接的方法是把高字节左移8位然后按位或上低字节。拼完之后得到的16位数据范围是0到65535但物理量是有正负的所以当值大于等于0x8000即32768时需要减去0x10000即65536把它转换成负数。举个例子如果读出来的16位原始值是0xFF00那么它代表的是-256。如果我们不做符号处理直接拿这个数去换算是36864结果就会完全错误。def get_accel_data(self): # 加速度计X、Y、Z轴原始数据 x self.read_raw_data(0x3B) y self.read_raw_data(0x3D) z self.read_raw_data(0x3F) # 换算成g默认量程±2g满量程对应32768 gx x / 16384.0 gy y / 16384.0 gz z / 16384.0 return gx, gy, gz加速度计的默认量程是±2g对应的16位数据范围是-32768到32767。所以换算公式是实际值 原始值 / 16384。这个16384是怎么来的32768除以2g就是16384/g意思是在±2g量程下每1g对应16384个LSB。同理如果配置的加速度计量程是±4g换算系数就是8192±8g则是4096±16g则是2048。这个换算系数并不是随意取的它和ADC分辨率直接相关——16位ADC的最高精度位用来表示满量程所以系数就是32768除以量程值。def get_gyro_data(self): # 陀螺仪X、Y、Z轴原始数据 x self.read_raw_data(0x43) y self.read_raw_data(0x45) z self.read_raw_data(0x47) # 换算成°/s默认量程±250°/s gx x / 131.0 gy y / 131.0 gz z / 131.0 return gx, gy, gz陀螺仪默认量程是±250°/s换算系数是32768除以250约等于131。也就是说原始值单位为LSB除以131之后得到的单位就是每秒转了多少度。和加速度计一样如果陀螺仪量程改成了±500°/s换算系数就要相应改成65.5。def get_temp(self): raw self.read_raw_data(0x41) # 温度换算公式: 实际温度 原始值/340 36.53 temp raw / 340.0 36.53 return tempMPU6050内部还集成了一个温度传感器虽然精度一般但用来监测芯片是否过热、判断传感器工作状态却是完全够用的。温度换算公式在芯片手册中直接给出温度 原始值 / 340 36.53。3.3 主程序循环读取与数据验证驱动类写完之后主程序就非常简单了i2c I2C(0, sclPin(5), sdaPin(4), freq400000) mpu MPU6050(i2c) while True: ax, ay, az mpu.get_accel_data() gx, gy, gz mpu.get_gyro_data() temp mpu.get_temp() print(f加速度(g): ax{ax:.2f}, ay{ay:.2f}, az{az:.2f}) print(f陀螺仪(°/s): gx{gx:.2f}, gy{gy:.2f}, gz{gz:.2f}) print(f温度: {temp:.2f} °C) print(- * 40) time.sleep_ms(500)把这串代码完整执行起来正常情况下应该能看到类似这样的输出加速度(g): ax0.01, ay-0.02, az1.03 陀螺仪(°/s): gx0.05, gy-0.02, gz0.03 温度: 28.50 °C怎么判断数据是否合理把Pico模组平放在桌面上静止不动时加速度Z轴读数应该接近1对应重力加速度X、Y轴接近0陀螺仪三个轴都应该接近0因为静止状态下没有角速度温度一个读数应该略高于室温如果静止时陀螺仪读数一直跳动比如在±5之间来回跳也不用太担心MEMS陀螺仪本身就有零偏噪声这是物理特性。真正需要注意的是如果你拿手转动模块陀螺仪读数有没有跟着明显变化如果怎么动都不变那接线或者寄存器配置可能有问题。3.4 数据偏移现象的解释第一次跑数据的时候你可能注意到一个问题即使传感器平放在桌上加速度Z轴的读数也不是理想的1.0000可能是0.98或者1.05。同样静止状态下陀螺仪读数也有一个小的偏移量比如0.85。这个现象叫“零漂”或“零偏”。MEMS传感器的制造工艺决定了每个芯片的零偏都不同而且会随温度变化。消除零偏的标准做法是校准上电后让传感器完全静止采集几百组数据取平均值把这个平均值作为零偏值存起来之后每次读数都减去这个零偏值。校准的代码并不复杂就是统计平均值的问题# 简单校准静止状态下采样100次得到每个轴的偏移量 offset_ax offset_ay offset_az 0 offset_gx offset_gy offset_gz 0 for _ in range(100): ax, ay, az mpu.get_accel_data() gx, gy, gz mpu.get_gyro_data() offset_ax ax; offset_ay ay; offset_az az offset_gx gx; offset_gy gy; offset_gz gz offset_ax / 100 offset_ay / 100 offset_az / 100 offset_gx / 100 offset_gy / 100 offset_gz / 100 # 之后每帧读数都用原始值减去对应的偏移量注意加速度计的零偏校准要在静止状态下进行而且校准过程中Z轴会包含重力分量大约1g这是正常的不需要额外去除。这个偏移量在后期的姿态解算中会自动起作用。4. 基于原始数据的简单姿态估算与进阶方向能正确读出加速度和陀螺仪数据之后很多人的第一反应是接下来呢这组数据能干什么最有意思的应用自然是姿态解算——算出物体当前倾斜了多少度、朝向哪个方向。这是自平衡车、四轴无人机、机械臂控制的核心基础。4.1 只用加速度计计算倾角在仅有加速度数据的情况下我们可以用三角函数算出物体绕X轴和Y轴的倾斜角度。数学原理是当物体静止时加速度计只受重力作用重力在三个轴上的分量大小就反映了物体的姿态。假设物体绕X轴旋转了一个角度称为roll横滚角那么重力的Y分量是g乘以sin(roll)Z分量是g乘以cos(roll)。Roll角可以通过以下公式计算roll math.atan2(ay, az) * 180 / math.piPitch角俯仰角绕Y轴的公式稍微复杂一点pitch math.atan2(-ax, math.sqrt(ay*ay az*az)) * 180 / math.pi为什么pitch用atan2的第二个参数是开根号而不是直接用az因为要避免当一个轴倾斜接近90度时出现计算奇异点。使用三个轴数据的勾股组合能保证角度在全范围内稳定输出。实测效果是把MPU6050平面与桌面平行时roll和pitch都接近0度把模块立起来角度就会明显变化。这个方案虽然简单粗暴但有一个致命缺点——加速度计无法区分重力加速度和运动加速度。如果你拿着模块快速晃动测出来的倾角就会瞬间乱跳。所以只用加速度计算倾角只适用于静态或缓慢运动的场景。4.2 陀螺仪积分与数据融合的必要性陀螺仪输出的是角速度理论上只要对角速度进行积分即不断累加就能得到角度变化量。比如绕X轴的角速度是10°/s持续了1秒钟那么X轴方向就转过了10度。但问题在实践中变得棘手陀螺仪有零偏误差微小且持续的偏差值在积分过程中不断累积导致角度数据缓慢漂移。我做一个简单的实验验证过静止放置模块陀螺仪积分得到的角度在几十秒内就能漂出好几度这在实际应用中是完全不可接受的。所以虽然加速度计的动态性能差但静态性能好长期稳定不漂移陀螺仪动态性能好响应快但长期漂移严重。两者互补需要通过算法融合起来。最简单的融合算法是互补滤波核心思路是在低频段信任加速度计高频段信任陀螺仪angle 0.98 * (angle gyro_rate * dt) 0.02 * accel_angle这个公式看起来简简单单但效果出乎意料地好。在实际两轮自平衡小车项目里我用这个滤波算法配合PID控制小车能稳稳立住。篇幅所限详细的互补滤波和卡尔曼滤波实现在后面的系列文章中展开。这里想告诉你的是读原始数据是基础把数据处理成有用的姿态信息才是关键。两者之间的鸿沟至少在这个项目里你已经迈过了第一道坎。4.3 我对这个项目的实测记录与建议最后分享一下我实际测试时遇到的几个情况和解决办法这些经验是在文档里查不到的第一刚上电时数据会有一段明显的“预热期”。前几十秒钟陀螺仪数据可能跳得特别厉害然后慢慢趋于稳定。这不是代码的问题是MEMS传感器内部物理结构还在适应温度和环境。建议正式采集数据前先让系统运行十几秒或者把前几十帧数据丢弃不做处理。第二409000Hz的I2C频率在长杜邦线下不稳定。我用20厘米左右的母对母杜邦线连接MPU6050时偶尔会读到明显的跳变数据把频率降到100kHz后数据立刻变得平滑。如果后续项目对采样率有更高要求建议把模块焊接在排针上缩短走线距离或者用屏蔽线。第三Thonny中如果代码更新后数据还是一样的先检查是否真的重新执行了新代码。MicroPython的交互式环境有一个特点手动点击“运行”按钮才会执行当前文件如果之前跑过循环没有停止重新运行时旧的进程还在占用串口报错信息会提示“OSError: [Errno 16] EBUSY”。解决办法是按下CtrlC中断当前程序再重新运行。第四我想强调模块供电的稳定性。MPU6050对电源纹波比较敏感如果和其他大电流外设共用电源数据噪声会明显变大。在这类传感器项目中尽量用独立的3.3V供电支路。这个项目的关键点已经全部落地了从接线到驱动再到数据校验每一步都走得通。后续要扩展的内容也很清晰采集数据后可以试试跌倒检测加速度模值突变判断、手势识别陀螺仪角速度模式匹配、云台稳定控制等。在动手做这些之前把这一套传感器数据读取逻辑练熟后面的路会好走很多。
返回列表