ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

龙芯平台MPU6050六轴传感器驱动移植实战指南

2026/9/9 17:47:48 拓冰建站 浏览量
龙芯平台MPU6050六轴传感器驱动移植实战指南 先说结论龙芯平台移植MPU6050驱动这件事本质上没有被“国产平台”四个字吓住的必要。我这次代表走马观碑组在龙芯3A5000加LS7A桥片的板子上把一个MPU6050六轴传感器完整跑通从用户态裸读、i2c-dev验证到最后走内核IIO驱动加设备树接管整个过程比预想中顺利但也踩了几个网上教程根本不会写的坑。这篇文章就是把整个MPU驱动移植过程摊开讲清楚包括龙芯I2C控制器怎么定位、MPU6050有哪些寄存器必须处理、三条移植路线怎么选、设备树怎么改、以及最后数据到底怎么验证。如果你手上也有一块龙芯板子或者后续要在LoongArch平台上做传感器适配这篇文章应该能帮你省下不少排查时间。1. 动手之前先把龙芯平台的几个“底子”摸清楚很多人在龙芯上做驱动移植卡住不是卡在MPU6050本身而是卡在平台认知上。龙芯和树莓派、STM32那套玩法有不少差异我先把这次项目里摸清楚的平台背景交代一下。1.1 龙芯I2C控制器长在哪里设备树又是怎么回事龙芯3A5000这类桌面/工控处理器CPU内核本身不带外设控制器大量I/O功能放在桥片LS7A上I2C控制器也在这里。2K系列则不同属于SoC集成I2C控制器直接做在片内。这两个平台在设备树里的描述方式不完全一样搜资料时要先确认自己手上的芯片型号别拿3A5000的dts照搬到2K1000上。以我用的3A5000加LS7A为例设备树源码一般在内核树的arch/loongarch/boot/dts/loongson/目录下桥片的外设节点写在类似ls7a.dtsi的文件里。I2C控制器节点名字通常是i2c1fe00000这种风格但不同内核版本、不同板级文件命名有差异。内核源码里对应的驱动在drivers/i2c/busses/i2c-ls2x.c这个文件是龙芯I2C控制器的原生驱动改设备树前最好先看一眼这个驱动支持的 compatible 字符串和寄存器布局。注意龙芯的固件PMON或UEFI会把设备树传给内核有些版本固件也内置了一份DTB。改完dts后最稳妥的做法是连同内核一起重新编译打包而不是单独指望加载外部dtb。不同板的固件行为不一样我这边踩过的教训是“改了dts但没生效”最后发现是固件优先用了内置DTB折腾了我们一下午。1.2 硬件连接先量电平再谈软件MPU6050模块大部分是3.3V供电SCL/SDA引脚电平也是3.3V。龙芯板子的I2C引脚通常通过排针引出也可能是GPIO复用。连线并不复杂但有一个原则必须遵守先确认电平再上电。MPU6050不是5V耐受器件把SDA/SCL接到5V上基本就报废了。我这次的实际接线方式如下MPU6050引脚龙芯板对应位置备注VCC3.3V电源不能用5VGND电源地和板子共地SCLI2C0_SCL或指定GPIO查板卡原理图确认SDAI2C0_SDA或指定GPIO查板卡原理图确认AD0GND接地后I2C地址为0x68INT暂不接第一版调试不建议接中断AD0这个引脚值得多说一句。AD0接地时传感器地址是0x68接高电平时是0x69。如果总线上只想挂一个MPU6050建议直接接地。上拉电阻的问题也要注意很多现成模块已经自带上拉电阻裸芯片的话SCL/SDA需要各接一个4.7k电阻到VDD不然I2C波形立不起来表现就是i2cdetect偶尔能扫到、偶尔扫不到。1.3 交叉编译环境和目标板通信驱动移植最终要编内核交叉工具链是起步条件。我用的发行版仓库里直接有gcc-loongarch64-linux-gnu如果没有也可以从Loongnix软件源或龙芯开源社区下载交叉工具链。内核源码建议直接使用龙芯厂商维护的内核分支或者主线新内核。老内核的问题在于可能根本没有LoongArch架构支持更不用说LS7A的I2C驱动。基本编译命令如下前面是配置后面是编译make ARCHloongarch CROSS_COMPILEloongarch64-linux-gnu- loongson3_defconfig make ARCHloongarch CROSS_COMPILEloongarch64-linux-gnu- menuconfig make ARCHloongarch CROSS_COMPILEloongarch64-linux-gnu- -j$(nproc) Image新一点的内核可能在defconfig命名上有所不同有的叫loongarch_defconfig有的叫loongson3_defconfig具体看内核目录下的arch/loongarch/configs/。目标板通信方面我一般串口和SSH双通道备着串口看早期启动日志SSH用来传内核包、跑i2c-tools。内核编译完成后替换目标板/boot下的内核镜像重启后通过uname -a确认有没有真的跑上新内核。2. 先弄懂MPU6050这颗传感器到底要“喂”什么MPU6050是一个I2C从设备通信本身不复杂但它有几个寄存器不处理好的话驱动写了也是白写。我建议在写任何代码之前先把下面这些细节吃透尤其是WHO_AM_I和电源管理寄存器。2.1 必须记住的寄存器地图很多移植教程上来就贴代码不解释寄存器。但实际情况是一旦数据不对你还是要回来翻手册。这里列几个这次项目里离不开的寄存器寄存器地址名称作用0x75WHO_AM_I设备ID复位值0x68用来验证I2C链路是否打通0x6BPWR_MGMT_1电源管理bit6是SLEEPbit7是DEVICE_RESET0x19SMPLRT_DIV采样率分频0x1ACONFIG数字低通滤波DLPF配置0x1BGYRO_CONFIG陀螺仪量程配置0x1CACCEL_CONFIG加速度计量程配置0x3BACCEL_XOUT_H加速度数据X/Y/Z各2字节高字节在前0x43GYRO_XOUT_H陀螺仪数据X/Y/Z各2字节高字节在前0x41TEMP_OUT_H温度数据其中0x75这个寄存器是排障的第一抓手。在用户态用i2cget直接读0x75如果能读到0x68说明I2C物理链路、设备地址、电平配置都没有问题如果读不到那后面谈数据都是空谈。另外提醒一句网上不少人把MPU6050写错成“mp6050”搜资料的时候注意关键词不然会漏掉一些本来很有用的帖子。2.2 读出来的原始值怎么换算成物理量MPU6050输出的是16位有符号补码范围大约在-32768到32767。默认量程下加速度计默认±2g灵敏度16384 LSB/g换算公式加速度(g) 原始值 / 16384。陀螺仪默认±250°/s灵敏度131 LSB/(°/s)换算公式角速度(°/s) 原始值 / 131。量程不同换算系数完全不同这个新手很容易翻车。完整对应关系如下量程配置加速度量程加速度灵敏度陀螺仪量程陀螺仪灵敏度默认±2g16384 LSB/g±250°/s131 LSB/(°/s)可选±4g8192 LSB/g±500°/s65.5 LSB/(°/s)可选±8g4096 LSB/g±1000°/s32.8 LSB/(°/s)可选±16g2048 LSB/g±2000°/s16.4 LSB/(°/s)比如你把陀螺仪量程设成了±2000还套用131的系数去换算出来的角速度会夸张到完全没法用。我习惯在驱动初始化时明确配置量程而不是依赖复位默认值这样代码可读性更好后续调参也方便。2.3 内核里其实有现成的MPU6050驱动别重复造轮子这是本次移植最重要的一条认知Linux内核自带的drivers/iio/imu/inv_mpu6050/目录下已经实现了MPU6050的完整驱动包括I2C接口、寄存器初始化、IIO框架接入。它是IIOIndustrial I/O子系统的一部分不是传统的杂项设备驱动。也就是说在龙芯上移植MPU6050最正规、最省力的路线不是“从零写一个驱动”而是“配置内核选项加修改设备树让现成驱动匹配上我们的硬件”。IIO框架的好处在于驱动注册后会在用户态暴露一组标准化的in_accel_x_raw、in_gyro_y_raw之类的属性节点上层应用不需要知道底层寄存器地址只要读sysfs节点就能拿到传感器数据。这对比传统misc驱动要规范得多将来接上层算法、接libiio、接iio-hwmon都方便。3. 三条移植路线选哪条取决于你要干什么同样的硬件在龙芯上有三种玩法用户态直读、用i2c-dev在用户态操作、内核IIO驱动。三条路线我都实际跑过下面把各自情况说清楚。3.1 路线一用户态直读最快验证硬件好坏不需要编译任何内核模块也不需要设备树只要内核开启了/dev/i2c-N设备支持CONFIG_I2C_CHARDEV就可以用C或Python直接访问I2C总线和MPU6050。C语言的简化版本大概长这样用来验证硬件链路#include stdio.h #include stdint.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/i2c-dev.h int main(void) { int fd open(/dev/i2c-0, O_RDWR); if (fd 0) { perror(open); return 1; } // AD0接地设备地址0x68 if (ioctl(fd, I2C_SLAVE_FORCE, 0x68) 0) { perror(ioctl); return 1; } // 唤醒MPU6050PWR_MGMT_1写0清掉SLEEP位 uint8_t wake[] {0x6B, 0x00}; if (write(fd, wake, 2) ! 2) { perror(write wake); return 1; } usleep(100000); // 读WHO_AM_I正常应返回0x68 uint8_t reg 0x75; uint8_t who 0; write(fd, reg, 1); read(fd, who, 1); printf(WHO_AM_I 0x%02X\n, who); // 读加速度X轴高字节和低字节拼成16位补码 reg 0x3B; uint8_t data[6]; write(fd, reg, 1); read(fd, data, 6); int16_t ax (data[0] 8) | data[1]; int16_t ay (data[2] 8) | data[3]; int16_t az (data[4] 8) | data[5]; printf(accel raw: %d %d %d\n, ax, ay, az); close(fd); return 0; }这段代码基本就是MPU6050驱动的最核心部分。它适合在第一步验证“传感器本身是不是好的”“I2C链路是不是通的”。但它的缺点也很明显没有中断、没有缓冲、没有量程配置的管理姿态算法在上面无法稳定跑起来。所以我只把它当“探针”用不做正式方案。3.2 路线二i2c-dev框架不编译内核也能先跑起来路线二本质上和路线一共享同一个/dev/i2c-N接口但操作方式更接近“在用户态写一个半完整驱动”。Linux内核只要开了CONFIG_I2C_CHARDEVy用户态就可以打开/dev/i2c-0、/dev/i2c-1等文件。用Python的smbus库会更高效import smbus import time bus smbus.SMBus(0) # 总线号要看板子实际注册为几号 addr 0x68 # 唤醒 bus.write_byte_data(addr, 0x6B, 0x00) time.sleep(0.1) # 读WHO_AM_I who bus.read_byte_data(addr, 0x75) print(WHO_AM_I , hex(who)) # 读加速度X/Y/Z原始值 data bus.read_i2c_block_data(addr, 0x3B, 6) ax (data[0] 8) | data[1] ay (data[2] 8) | data[3] az (data[4] 8) | data[5] print(accel raw:, ax, ay, az)这个路线的价值在于快速原型验证。你想确认数据手册里的某个寄存器行为或者想临时调整量程、DLPF设置直接写几行脚本就能看效果不用每改一次就编一次内核。但它同样没有系统级的设备管理适合调试和测试不适合作为产品驱动的最终形态。3.3 路线三正规军内核IIO驱动加设备树真正要长期使用还是得走内核IIO驱动。这条路线是“配置驱动框架加设备树描述”开发量反而最小。三条路线的对比如下方案开发量适用场景缺点用户态C直读最小验证硬件、快速测试无中断、无缓冲、不规范i2c-devPython小原型验证、寄存器调试性能一般、无设备管理内核IIO驱动设备树中等正式产品、算法对接需要改设备树和编译内核我最终选择路线三。接下来就把这部分完整展开。4. 龙芯上把MPU6050驱动真正挂进内核的完整过程这部分是实操核心。从设备树修改到内核配置再到驱动加载确认一步步来。4.1 设备树修改把MPU6050描述为I2C0的子节点在龙芯的设备树中I2C控制器节点本身已经存在但未必使能也未必把MPU6050挂上去。我们要做的事情就是给对应的I2C控制器加一个子节点。以下是精简后的设备树片段以I2C0为例i2c0 { status okay; clock-frequency 100000; #address-cells 1; #size-cells 0; mpu605068 { compatible invensense,mpu6050; reg 0x68; interrupt-parent gpio; interrupts 某个GPIO中断号 IRQ_TYPE_EDGE_RISING; mount-matrix 0, 1, 0, -1, 0, 0, 0, 0, 1; }; };我强烈建议第一版先把interrupt-parent和interrupts这两行注释掉不要配置中断。原因后面会详细讲先记住结论没有中断配置驱动也能正常工作做轮询读取配置错了反而可能probe失败。mount-matrix不是必须的但如果你板子上传感器的安装方向和芯片主轴不一致这个属性可以帮你做旋转矩阵换算避免上层算法里做额外翻转。compatible invensense,mpu6050是驱动匹配的关键。内核的inv_mpu6050_i2c.c驱动里i2c_device_id表里就有这个字符串字符串对不上设备树写了也白写。还有一个容易忽略的点要确认你挂在哪个I2C控制器上。龙芯板子可能引出多个I2C总线比如I2C0、I2C1设备树里对应多个节点把子节点写错总线i2cdetect会在错误的/dev/i2c-N上扫不到设备。我这次就经历过“明明接了排针标着I2C0结果设备树里I2C0节点是disabled、I2C1才是默认打开的”这种反直觉情况。4.2 内核配置把inv_mpu6050驱动编进来设备树改好后需要在内核配置里打开IIO子系统和MPU6050驱动。相关选项如下CONFIG_IIOy CONFIG_IIO_BUFFERy CONFIG_IIO_KFIFO_BUFy CONFIG_INV_MPU6050_IIOy CONFIG_INV_MPU6050_I2Cy如果你用menuconfig可以直接搜索MPU6050关键字找到对应项。CONFIG_INV_MPU6050_IIO是驱动核心部分CONFIG_INV_MPU6050_I2C是I2C传输层。两者都打开后重新编译内核、替换目标板内核并重启驱动就应该被加载了。编译过程中有个经验确定自己的内核版本老内核里drivers/iio/imu/inv_mpu6050/的Kconfig菜单路径可能有细微差别。先用make menuconfig搜索关键字确认选项真实存在再保存配置不要凭记忆写.config。4.3 确认驱动加载dmesg和sysfs双验证重启后第一件事看日志dmesg | grep -i mpu正常情况下能看到类似“inv-mpu6050 i2c-0: 0x68: who_am_i 0x68”或者“i2c inv-mpu6050: probe success”之类的日志。如果出现“failed to read who_am_i”那就要回头查硬连接和I2C总线。接着检查IIO设备节点是否注册成功ls /sys/bus/iio/devices/ cat /sys/bus/iio/devices/iio:device0/name cat /sys/bus/iio/devices/iio:device0/in_accel_scale cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw如果name返回类似“mpu6050”的内容说明驱动已经成功接管传感器。in_accel_scale返回的是把原始值换算成标准单位加速度单位m/s²的比例系数。默认±2g量程下这个值大约0.000598也就是9.80665 / 16384。in_accel_x_raw是16位原始值两者相乘就是物理加速度。4.4 中断和DMP扩展等基础通路稳定了再考虑很多人一上来就想把INT引脚中断和DMP数字运动处理器都搞起来。我的建议是优先级往后放。DMP本身是InvenSense的闭源固件内核自带的inv_mpu6050驱动并不直接包含DMP固件加载功能部分厂商分支会带上要完整跑DMP输出四元数还需要移植MPL库或者使用带DMP支持的第三方方案工作量不小。中断则属于“可以加但不需要一开始就加”的功能。IIO驱动在没有中断的情况下会用轮询模式工作10Hz采样完全够用。先把数据通路跑通再考虑用中断和buffer模式做更高帧率的姿态算法这才是正确的推进节奏。5. 数据验证从i2cdetect到姿态数据判读驱动加载成功不代表数据正确。我这次在验证阶段用了三层手段从底层往上层层确认。5.1 i2c-tools三板斧i2cdetect、i2cget、i2cdump在目标板上安装i2c-tools然后依次执行# 列出所有I2C总线 i2cdetect -l # 扫描总线0上的设备如果看到68或69说明设备在该总线上 i2cdetect -y 0扫到0x68后再确认WHO_AM_I寄存器i2cget -y 0 0x68 0x75返回0x68I2C链路就没问题。如果想看完整寄存器状态用i2cdump -y 0 0x68可以一页一页翻寄存器值对排查量程配置、是否处于sleep状态都很有用。5.2 从IIO节点读取加速度和角速度驱动加载成功后直接读sysfs节点就行cat /sys/bus/iio/devices/iio:device0/in_accel_scale cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw cat /sys/bus/iio/devices/iio:device0/in_accel_y_raw cat /sys/bus/iio/devices/iio:device0/in_accel_z_raw cat /sys/bus/iio/devices/iio:device0/in_gyro_x_raw cat /sys/bus/iio/devices/iio:device0/in_gyro_y_raw cat /sys/bus/iio/devices/iio:device0/in_gyro_z_raw加速度原始值乘以in_accel_scale得到m/s²。板子平放时z轴方向应该接近9.8x/y轴接近0。陀螺原始值乘以in_gyro_scale得到rad/s静止时三个轴都在0附近小幅波动。如果静止状态下加速度三轴合成模长远远偏离9.8比如只有2那大概率数据没对常见原因是传感器还在sleep或者读错了寄存器。5.3 用buffer模式做连续采样如果要给姿态算法供数不能手动一个个cat节点要用IIO的buffer模式。先把要采样的通道enable再enable bufferecho 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_x_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_y_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_z_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_gyro_x_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_gyro_y_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_gyro_z_en echo 1 /sys/bus/iio/devices/iio:device0/buffer/enable cat /dev/iio:device0 | xxdbuffer里每个采样点的数据顺序和scan_elements里enable的顺序一致每个通道是16位有符号小端整数。如果插入了timestamp还要按8字节时间戳来解析。想省事的话可以编译内核源码包里的tools/iio/iio_generic_buffer输出更直观。5.4 数据合理性检查别等算法跑起来才发现数据是反的最后一个验证步骤是“物理合理性普查”。把板子分别平放、竖放、倒放记录三轴加速度变化。平放时z轴约±9.8竖放时对应轴约±9.8其余轴接近0。陀螺仪用手快速转动板子数值应立刻响应停稳后回归0附近。这个验证不能省我有一次就是装反了方向上层算法输出的姿态角整个镜像翻转排查了很久才发现是安装方向没有用mount-matrix处理。6. 这次移植最容易翻车的几个点逐个说清楚最后把这次踩过的坑按概率从高到低排一遍。每个坑我都给了一个明确的处理思路。6.1 i2cdetect扫不到设备按顺序排查三个最常见因素最常遇到的情况就是i2cdetect -y 0什么也扫不到。我的排查顺序是供电和地先量一遍确保VCC真的是3.3VGND和板子共地然后量SCL/SDA对地电压正常情况下两个引脚被上拉到3.3V左右如果量到0V可能是管脚复用配置不对也可能是引脚接反了最后确认总线号i2cdetect扫的是1号总线但你设备接在0号上当然什么都扫不到。还有一种情况是模块自带上拉但龙芯板子的引脚被重复配置成其他功能导致I2C控制器没有真正把引脚接管过去。这时候要么去pinctrl配置里把复用关系改对要么干脆走i2c-gpio用GPIO模拟I2C这部分后面会单独说。6.2 WH0_AM_I能读到0x68但加速度数据全是0问题出在唤醒这个坑很有迷惑性。I2C通信正常WHO_AM_I也对但读加速度和陀螺的数据寄存器全是0。原因很简单MPU6050默认处于sleep状态PWR_MGMT_1的SLEEP位是1传感器内部大部分功能没有启动数据寄存器不会更新。处理方式就是初始化时向PWR_MGMT_10x6B写0清掉SLEEP位然后至少要等100ms让内部时钟和传感电路稳定再开始读数据。这个唤醒操作放在所有初始化的最前面否则后续配置都可能无效。6.3 设备树里配了中断反而probe失败龙芯中断配置的坑我一开始在设备树里给MPU6050加了interrupt-parent和interrupts结果dmesg里报中断申请失败驱动probe直接失败。问题出在我把中断号理解成了线性编号实际上龙芯LS7A的GPIO中断要按GPIO bank来组织中断号和GPIO号之间存在映射关系不同板号、不同bank映射还不同。这也是我为什么在前面强调“第一版别配中断”。龙芯的中断控制器行为和x86/ARM不完全一样查资料的成本很高。先把传感器数据通过轮询跑通再根据板卡的原理图和固件文档去梳理GPIO中断映射风险会小很多。如果你确实要用中断建议直接用GPIO的中断属性不要自己去猜中断号。6.4 硬件I2C控制器不给力时i2c-gpio是最后的逃生通道最后一个备用方案也是排查问题时很好用的隔离手段不用芯片自带的I2C控制器改用两个GPIO模拟I2C时序。内核的i2c-gpio驱动是通用的设备树里描述两个GPIO后系统会注册一个新的I2C总线传感器挂在这条总线上同样能被inv_mpu6050驱动识别。设备树示意如下i2c-gpio0 { compatible i2c-gpio; gpios gpio 4 GPIO_ACTIVE_HIGH, /* SDA */ gpio 5 GPIO_ACTIVE_HIGH; /* SCL */ i2c-gpio,sda-open-drain; i2c-gpio,scl-open-drain; #address-cells 1; #size-cells 0; mpu605068 { compatible invensense,mpu6050; reg 0x68; }; };i2c-gpio方式速度通常不超过100kHz对MPU6050这种传感器完全够用。它的价值在于当你不确定是“芯片原生I2C控制器的问题”还是“传感器的问题”时用GPIO模拟I2C可以干净地隔离故障源。我上次遇到一个板子的原生I2C控制器死活不产生波形换用i2c-gpio后传感器立刻就能读问题明显出在控制器或管脚复用配置上而不是传感器本身。最后说点大实话。这次龙芯MPU驱动移植最大的教训不是MPU6050有多难而是平台差异比想象中更容易让人绕远路。先用户态把硬件验证透再上内核驱动先不配中断再逐步加功能优先复用内核现成的inv_mpu6050驱动而不是自己从寄存器开始写。这套顺序让我省了很多无谓的排障时间。另外手头最好留一个逻辑分析仪或者示波器看I2C波形实在没有i2cdetect回显的粘性也可以帮你判断波形质量。后续这个项目我打算在把DMP固件加载跑通然后从IIO buffer里直接拿传感器数据喂给姿态解算到时候再写一篇续篇。