龙芯平台MPU6050驱动移植实战:从设备树到IIO数据读取
2026/9/8 5:12:23 网站建设 项目流程

1. 先说结论:这次“MPU驱动移植”到底移植了什么

接到“走马观碑组”这个项目时,我第一反应是:又要从零写一个MPU6050的字符设备驱动了?说实话,团队成员一开始也都这么以为,甚至有人已经在查MPU6050的寄存器手册,准备把FIFO、DMP、中断这些全自己撸一遍。但把需求往细里一拆,发现根本不是那么回事。

我们手里的硬件是一块基于龙芯k平台的嵌入式开发板,要接一颗InvenSense MPU6050六轴惯性传感器,用在姿态采集和运动监测试验上。系统是Linux,内核版本基于龙芯SDK提供的内核源码。所谓的“移植”,真正的边界不是“写一个驱动”,而是:让Linux内核里已有的MPU6050驱动,在龙芯k这个具体平台上被正确识别、编译、加载并稳定出数。

这个判断很关键。因为一旦把目标定义为“移植”而不是“从零开发”,整个技术路线的复杂度就完全不一样了。Linux内核从很早开始就已经把MPU6050驱动纳入主线,代码在drivers/iio/imu/inv_mpu6050/目录下,维护得很完整,支持I2C和SPI两种总线接口。所以我们的任务其实可以拆成四件事:

  1. 在内核源码里确认当前SDK版本有没有包含或可以配置出该驱动;
  2. 在设备树里把MPU6050的I2C地址、中断引脚正确描述出来;
  3. 把驱动编译成内核模块或编入内核镜像,并在目标板上加载成功;
  4. 通过IIO子系统的sysfs接口读取六轴原始数据,验证数据是否正常。

这篇文章就是把这四步完整记下来,顺带把我们踩过的坑、做的取舍、以及最后沉淀下来的排查方法一并说清楚。如果你手头是龙芯、飞腾或其他国产嵌入式平台,要做传感器驱动适配的,这篇东西应该能帮你少走一大段弯路。

2. 移植路线选择:IIO框架、i2c-dev直读、自写驱动的对比与取舍

2.1 为什么不是自己写一个独立驱动?

先说结论:自己从寄存器层面写一个MPU6050驱动,在商业项目里是万不得已才做的事,除非你用的是RTOS裸机环境,或者内核版本老到根本没有现成驱动可用。

MPU6050本身并不复杂,I2C地址默认0x68或0x69,初始化无非是配置电源管理寄存器、设置量程、配置采样率、读FIFO或直接读寄存器。这些工作大半天就能写完。但问题在于,Linux下驱动不是“能不能读到数据”这么简单,你还要考虑中断处理、睡眠唤醒管理、与用户空间的接口规范、热插拔总线的设备模型对接等。这些基础设施如果自己造,成本远高于驱动本身。

我们当时评估过自写方案,估算工作量至少需要一到两周,还要写用户态的接口库,而且后续维护全落到自己头上。相比之下,主线内核的inv_mpu6050驱动已经支持:

  • 加速度计、陀螺仪原始数据读取
  • 温度传感器读取
  • 采样率、量程、低通滤波配置
  • FIFO读取
  • I2C辅助总线(可用于扩展磁力计)
  • 中断支持

这些能力通过标准IIO框架暴露给用户空间,意味着我们之后要写姿态融合算法时,可以直接读sysfs节点或者用libiio,不用再跟寄存器打交道。

2.2 那能不能直接打开i2c-dev用ioctl直读?

很多做快速验证的朋友第一反应是:我在用户态开/dev/i2c-0,用ioctl发I2C读写命令读寄存器不就行了?确实行,而且我们前期验证硬件连线时就是这么干的。但我强烈不建议把它作为“移植”交付方案,原因有三:

一是不符合Linux设备模型,应用层代码得死死绑定I2C设备路径,换一颗挂在其他总线上的传感器就得改代码;二是中断没法优雅地处理,MPU6050的数据就绪中断、DMP中断这类功能在用户态裸读下很难利用起来;三是多个进程并发访问同一个I2C从设备时会打架,你还要自己在应用层做锁。

所以我们最终选择了第三条路:用内核主线现有的IIO驱动框架。

2.3 确认内核里到底有没有这个驱动

拿到SDK内核源码后,第一步不是急着配置,而是确认代码是否存在。我在内核源码根目录执行:

find drivers/iio/imu -maxdepth 2 -type f | head -50

正常情况下可以看到drivers/iio/imu/inv_mpu6050/目录,里面有inv_mpu6050_core.cinv_mpu6050_i2c.cinv_mpu6050_spi.c等文件。如果这个目录存在,事情就成功了一半。

判断哪个平台总线驱动会被编译,关键是看Kconfig文件。我翻了一下,里面有两个关键配置项:

CONFIG_INV_MPU6050_IIO CONFIG_INV_MPU6050_I2C

INV_MPU6050_IIO是核心驱动,INV_MPU6050_I2C是I2C总线接入层。我们要把这两个配置项都打开,驱动才会被编出来。这里很多人会漏:只开I2C不开IIO核心,编出来的模块文件是空的;反过来也是。

如果你拿到的SDK内核比较老,连inv_mpu6050目录都没有,也没关系,可以从主线内核drivers/iio/imu/inv_mpu6050/整个目录拷贝过来,再补上KconfigMakefile的对应条目就能用。这个操作我们后来在另一个老版本内核上也验证过,风险很低,因为该驱动的依赖项只有I2CIIO几个核心选项。

三种路线的对比,我当时整理过一张表,给项目组评审用:

方案开发量维护成本功能完整性是否推荐
i2c-dev用户态直读极低仅原始寄存器读写仅用于验证硬件
自写字符设备驱动按需定制,但要做大量基础工作不推荐
内核IIO现成驱动完整支持六轴+温度+FIFO+中断推荐

现在回看,这个选择是整个项目最正确的一步。后续所有工作都是在给现有驱动“铺路”,而不是在修驱动本身。

3. 设备树对接与I2C探测:让龙芯k先找到这颗MPU6050

3.1 先确认硬件挂在哪条I2C总线上

设备树这一步是多数移植项目翻车最多的地方,但只要你按顺序做,其实并不难。

在写设备树之前,先做两件事:一是查板级原理图,确认MPU6050的SCL/SDA接在龙芯k的哪个I2C控制器上;二是用示波器或万用表确认传感器供电。我们板卡上MPU6050挂在I2C0,板载3.3V供电,AD0引脚直接接地,所以I2C地址是0x68。如果你板子的AD0接了上拉电阻,地址就是0x69,后面所有设备树和调试命令都要跟着改。

你可以在目标板上用i2cdetect -l看看系统里识别出了几条I2C总线:

i2cdetect -l

正常情况下会输出类似:

i2c-0 i2c DesignWare I2C adapter I2C adapter i2c-1 i2c DesignWare I2C adapter I2C adapter

这个输出能帮你确认控制器驱动已经加载。如果i2cdetect -l本身不存在,说明系统没装i2c-tools,用包管理装一下即可。

3.2 在设备树里添加MPU6050节点

龙芯k平台的内核设备树路径随内核版本和SDK不同会有差异,常见位置在arch/loongarch/boot/dts/loongson/下,老一些的版本在arch/mips/boot/dts/loongson/下。找到板级dts或dtsi文件后,在对应的I2C控制器节点下添加子节点。

我加的节点大致是这样:

&i2c0 { status = "okay"; clock-frequency = <400000>; mpu6050@68 { compatible = "invensense,mpu6050"; reg = <0x68>; interrupt-parent = <&gpio>; interrupts = <12 IRQ_TYPE_LEVEL_HIGH>; }; };

这里有两个点需要特别说明。

第一个是compatible。我看到很多教程喜欢写"invensense,mpu6050",但不同内核版本里驱动匹配的字符串可能会带后缀,比如"invensense,mpu6050"是核心匹配,真实I2C驱动还支持"invensense,mpu6050""invensense,mpu6500""invensense,mpu9250"这类兼容。建议在内核源码里搜一下of_device_id确认:

grep -rn "of_match_table" drivers/iio/imu/inv_mpu6050/

第二个是interrupt-parentregreg是I2C从机地址,必须和AD0引脚电平对应。中断这一行我当时是照抄示例填的,结果成了整个项目里最大的坑,后面单开一节讲。

3.3 不用重编内核也能验证设备树的方法

设备树不是写了就生效的。龙芯k的引导程序会加载DTB,如果你改了dts,要么重新编译内核生成新的dtb,要么把设备树编成内核模块再用device tree overlay加载。对开发阶段来说,重新编译dtb再替换是最直接的路径。

一个很实用的技巧:先不改dts,用i2cdetect扫一遍I2C0总线,确认传感器在硬件层面能被发现。

i2cdetect -y -r 0

如果地址0x68上有设备,输出里会显示68。这一步能提前把硬件连线问题隔离掉,避免你辛辛苦苦编完内核才发现传感器根本没上电。

如果扫不到,按顺序排查:

  1. 用万用表确认传感器VDD和GND;
  2. 确认SDA/SCL是否接反;
  3. 确认I2C总线上拉电阻是否存在,很多核心板为了省成本没放上拉,导致通信不稳定;
  4. 换一个I2C地址再扫一次,可能是AD0电平不对。

我们这个项目在扫描阶段一切正常,0x68稳稳当当出现,所以当时还挺乐观,觉得后面会很顺。结果编译加载阶段给了我们一个下马威。

3.4 重编设备树并确认加载后的节点

确认dts修改无误后,我重新编了内核和设备树文件。这个命令跟龙芯处理器架构紧密相关,我们用的交叉编译前缀是loongarch64-linux-gnu-

make ARCH=loongarch CROSS_COMPILE=loongarch64-linux-gnu- loongson_k_defconfig make ARCH=loongarch CROSS_COMPILE=loongarch64-linux-gnu- dtbs

编出来的dtb文件替换到引导分区后重启,在/sys/firmware/devicetree/base/下应能看到对应节点:

ls /sys/firmware/devicetree/base/ | grep i2c

以及:

ls /sys/firmware/devicetree/base/soc/i2c@xxxxx/

能看到mpu6050@68子目录,说明设备树节点已经生效。这一步过了,驱动匹配才有基础。

4. 内核配置、交叉编译与模块上板:异常报错排查实录

4.1 打开内核配置的完整流程

在龙芯k平台的Linux内核里,打开MPU6050驱动需要调整几个配置项。我用make menuconfig或直接改.config都可以,重点是把下面几个符号打开:

CONFIG_IIO=y CONFIG_IIO_BUFFER=y CONFIG_IIO_TRIGGERED_BUFFER=y CONFIG_INV_MPU6050_IIO=m CONFIG_INV_MPU6050_I2C=m

注意几个细节:

  • CONFIG_IIO最好直接编译进内核,不要设成模块,因为IIO子系统是其他传感器驱动的基础设施,如果它本身是模块,驱动加载顺序会很麻烦。
  • CONFIG_INV_MPU6050_IIOCONFIG_INV_MPU6050_I2C我们设成m,也就是编成.ko内核模块。这样开发阶段方便单独替换驱动,不用每次改驱动都重刷整个内核。
  • 如果内核版本较老,还要确认CONFIG_I2CCONFIG_GPIO_CDEV这类基础选项是开着的。龙芯SDK默认一般都开,但保险起见查一下。

在源码目录执行:

make ARCH=loongarch CROSS_COMPILE=loongarch64-linux-gnu- ARCH=loongarch CROSS_COMPILE=loongarch64-linux-gnu- modules_prepare

然后编译模块:

make ARCH=loongarch CROSS_COMPILE=loongarch64-linux-gnu- M=drivers/iio/imu/inv_mpu6050 modules

编译完成后,在drivers/iio/imu/inv_mpu6050/下会生成inv-mpu6050-core.koinv-mpu6050-i2c.ko两个文件。注意模块名是带连字符的,跟Makefile里的目标名不一定完全一样。

4.2 模块加载时最容易出现的“背调”错误

把两个.ko拷贝到目标板上,执行modprobe前先手动跑一次:

insmod inv-mpu6050-core.ko insmod inv-mpu6050-i2c.ko

我们的第一个报错就是:

inv_mpu6050: version magic '5.10.27 loongarch' should be '5.10.84 loongarch'

原因很直接:模块是用当前源码树编译的,但板子上运行的内核版本和源码树版本不一致。龙芯SDK经常出现这种情况,厂商把内核源码打了自己的补丁,版本号却和实际烧进去的镜像不一致。

排查方法简单粗暴:先看目标板上到底在跑哪个版本:

uname -r

然后回源码目录make kernelrelease对比。不一致时,按源码树的版本重新编译整个内核镜像并烧写,或者反过来,用正在运行的内核对应的config重新编译模块源码。这里要特别提醒:不要用修改版本号取巧,比如手动改EXTRAVERSIONuname输出一致,内核模块的 vermagic 还包含编译器、内核配置和大版本信息,改了也会在加载时报别的错。最规范的做法是让运行镜像和源码树完全对应。

4.3 驱动匹配失败的经典场景:I2C设备树节点没被识别

把模块加载成功后,我们以为dmesg会立刻刷出一长串初始化日志,结果只看到一行:

inv_mpu6050 i2c-0:0: probe failed with error -ENXIO

-ENXIO通常意味着I2C传输失败,驱动发WHO_AM_I读命令时根本没收到应答。这时候如果只盯着驱动代码看,很容易绕进死胡同。

我们的排查顺序是这样的:

  1. 先跑i2cdetect -y -r 0,确认从设备还在;
  2. 再用i2cget手动读寄存器:
i2cget -y 0 0x68 0x75

正常返回0x680x70(MPU6050对WHO_AM_I的应答值为0x68或0x70,取决于寄存器映射版本)。

如果i2cget能读出来,说明硬件和I2C总线完全正常,问题出在设备树与驱动匹配之间。后来发现,原因是设备树节点的compatible和驱动里的of_device_id表没对齐。SDK内核里的驱动被厂商改过,compatible字符串变成了"invensense,mpu6050,vendor"之类带后缀的东西,而我们的dts里写的是标准字符串。匹配不上,驱动框架直接拒绝probe。

最终我把dts里的compatible改成跟驱动源码里完全一致,才恢复正常。这件事启发我:设备树写完后,一定要在目标板上用这个命令看最终生效值:

cat /sys/firmware/devicetree/base/soc/i2c@xxxxx/mpu6050@68/compatible

4.4 模块加载成功后还要确认IIO设备节点出现

驱动probe成功后,dmesg会输出类似:

inv_mpu6050 i2c-0:0: Detected MPU6050

这时检查/sys/bus/iio/devices/,能看到iio:device0目录。进去看一眼:

ls /sys/bus/iio/devices/iio:device0/

里面应该有一堆in_accel_*in_anglvel_*in_temp_*开头的属性文件。到这里,移植工作已经完成了80%。剩下20%是验证数据是不是真能反映芯片运动。

5. 运动数据校验:从原始寄存器读到在sysfs里看六轴变化

5.1 原始读数和实际物理量的换算关系

IIO框架暴露给用户空间的是原始寄存器值,不是标准单位。MPU6050的加速度计默认量程是±2g,陀螺仪默认量程是±250°/s,需要在应用层做换算。

换算系数分别如下:

  • 加速度:灵敏度是16384 LSB/g,所以物理加速度 = 原始值 / 16384 * 9.8 m/s²
  • 陀螺仪:灵敏度是131 LSB/(°/s),所以角速度 = 原始值 / 131 °/s
  • 温度:推荐公式是 T = 原始值 / 340 + 36.53 °C
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_temp_raw

5.2 静态验证:看重力向量和零偏

把开发板平放在桌面上,理论上Z轴加速度应该接近1g,也就是原始值约16384,X和Y轴接近0。我们实测出的值是:

in_accel_x_raw: -80 in_accel_y_raw: 160 in_accel_z_raw: 16310

这个结果是合理的,偏差来自芯片安装角度和温度漂移。如果Z轴实测值偏离16384太多,比如变成8000甚至负的,大概率是量程寄存器配置没生效,或者芯片安装方向理解错了。

陀螺仪在静止状态下,三轴原始值应该非常接近0。我们这边测出来:

in_anglvel_x_raw: -3 in_anglvel_y_raw: 11 in_anglvel_z_raw: 2

这个零偏水平对消费级IMU来说算正常,拿来做姿态解算前需要做零偏校准,把静止时的均值作为补偿量减掉。

5.3 动态验证:用手翻转板子观察数据联动

静态验证只能说明寄存器能读,不能说明陀螺仪真的在感知运动。我们把板子绕X轴快速翻转90度,在翻转瞬间抓取数据:

watch -n 0.1 cat /sys/bus/iio/devices/iio:device0/in_anglvel_x_raw

能看到X轴瞬时读数跳到几千,翻转结束后归零。同样地,拿起板子左右平移,加速度计的X/Y轴会有明显变化。这一步很重要,能确认IMU的六个轴都活着,排除假焊或引脚接触不良导致某一路无响应的情况。

如果你想在用户态持续读数据,可以用iio_info这个工具,或者直接用Python的pylibiio库。简单验证阶段,我更喜欢直接用shell脚本循环读,毕竟不依赖额外编译环境:

while true; do ax=$(cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw) az=$(cat /sys/bus/iio/devices/iio:device0/in_accel_z_raw) gx=$(cat /sys/bus/iio/devices/iio:device0/in_anglvel_x_raw) echo "$(date +%H:%M:%S) ax=$ax az=$az gx=$gx" sleep 0.1 done

5.4 采样率和量程配置

MPU6050驱动默认配置不一定满足所有应用场景。我们做运动监测时发现默认采样率太低,需要调高。IIO框架下可以直接用sysfs接口修改:

cat /sys/bus/iio/devices/iio:device0/in_accel_sampling_frequency_available echo 1000 > /sys/bus/iio/devices/iio:device0/in_accel_sampling_frequency

量程同理:

cat /sys/bus/iio/devices/iio:device0/in_accel_scale_available echo 0.000598550415 > /sys/bus/iio/devices/iio:device0/in_accel_scale

这里有个容易忽略的点:加速度计量程变化会影响in_accel_scale的值,而用户在应用层读取原始值再手动换算时,必须同步使用新的scale系数。IIO框架的in_accel_scale属性就是为了解决这个一致性问题的,推荐读到的物理值直接用原始值 * scale,而不是自己写死灵敏度。

6. 复盘中的三个硬坑:中断映射、内核版本和总线电气特性

6.1 中断引脚映射:设备树里看似“无害”的一行,卡了我们两天

文章前面我提到,dts里加了这行:

interrupts = <12 IRQ_TYPE_LEVEL_HIGH>;

当时以为照着芯片手册写个GPIO号就行,结果probe日志反复出现:

inv_mpu6050 i2c-0:0: IRQ configuration failed -EINVAL

问题出在龙芯k的GPIO中断控制器跟ARM平台的GPIO编号体系完全不同。ARM平台上,设备树里可以直接写interrupts = <&gpio 12 IRQ_TYPE_LEVEL_HIGH>来表示GPIO控制器下的第12号引脚,但龙芯平台需要先弄清自己的GPIO控制器映射到了哪个中断号,还要确认对应的pinctrl驱动有没有初始化。

我不建议你在不熟悉平台中断路由的情况下照抄ARM设备树的写法。稳妥的排查路径是:

  1. 先去掉dts里的interrupt-parentinterrupts两行,让驱动以轮询方式工作;
  2. 确认六轴数据稳定后,再看数据手册查具体GPIO中断号;
  3. 用内核的GPIO中断调试接口逐步验证引脚能不能产生中断。

对我们这个项目来说,最终不需要中断也能满足需求,因为采样率要求不高,轮询模式就够用了。所以我把中断节点从设备树里移除,驱动反而更稳定。如果你的应用必须用中断,建议单独开一个星期专门搞GPIO路由验证,别把它跟MPU6050驱动移植混在一起并行排查,否则会互相干扰。

6.2 内核版本不一致导致的模块加载失败

前面提到的version magic报错,看似是个小问题,实际上暴露了龙芯SDK一个常见现象:源码树和预编译镜像版本不一致。这里我把排查步骤再完整列一遍,方便你直接照着做。

在源码目录执行:

make ARCH=loongarch CROSS_COMPILE=loongarch64-linux-gnu- kernelrelease

在目标板执行:

uname -r

如果两个输出不一致,不要急着改源码版本号,而是先确认板子上的内核config是否跟源码目录里的config一致:

zcat /proc/config.gz | grep CONFIG_LOCALVERSION

如果板子系统里没开CONFIG_IKCONFIG,看不到config,就把源码目录的.config拷到目标板,对比scripts/extract-ikconfig的输出。最理想的解决方案是:用SDK源码目录重新编译完整内核镜像,烧写进板子,保证运行内核和源码树完全对应。这样后面每编一个模块都不会再碰到魔数不匹配问题。

6.3 I2C总线电气特性:400kHz不稳定,降到100kHz立刻稳定

在数据验证阶段,我们遇到过一个诡异现象:刚开机时数据正常,运行十几分钟后偶尔读到-121(-EREMOTEIO)或-6(-ENXIO)。

一开始怀疑是芯片过热或驱动问题,排查半天后发现,问题出在I2C总线的上拉强度和时钟频率组合上。MPU6050硬件上虽然没有外接上拉电阻,但龙芯k核心板内部I2C控制器的上拉电阻太弱,400kHz通信时信号沿不够陡,抗干扰能力差,稍微有点线缆长度或电源波动就开始出错。

我们的修复方法是把设备树里的clock-frequency从400000改成100000:

&i2c0 { status = "okay"; clock-frequency = <100000>; };

改完后连续跑了72小时没再丢过数。对MPU6050这种采样率上限1kHz的传感器来说,I2C 100kHz完全够用,没必要追求高速。

如果你板子上有条件,建议在SDA和SCL线上各加一个2.2kΩ到4.7kΩ的上拉电阻,这是从硬件层面根治I2C不稳定问题的办法。没有条件改硬件时,降低总线时钟是最快最有效的软件规避手段。

6.4 其他辅助排查工具

模块加载后如果怀疑驱动内部有计算问题,可以打开内核动态调试:

echo "file drivers/iio/imu/inv_mpu6050/* +p" > /sys/kernel/debug/dynamic_debug/control

然后再看dmesg,驱动的函数级日志会全部打出来。这个手段在Probe阶段尤其有用,能精确看到驱动卡在哪个函数里。

推荐一套检查顺序,写入你的项目检查单:

  1. i2cdetect -l确认I2C控制器存在;
  2. i2cdetect -y -r 0确认0x68设备在线;
  3. i2cget -y 0 0x68 0x75确认能读到WHO_AM_I;
  4. dmesg | grep inv看驱动日志;
  5. ls /sys/bus/iio/devices/确认IIO设备节点;
  6. 静态读三轴和温度,确认数值在合理范围;
  7. 动态翻转板子,确认数据联动。

只要按这个顺序走,基本能把硬件、设备树、驱动、内核配置四个层面的问题完全隔离。


这次MPU6050驱动移植,从开始到稳定出数,前后用了大约四天。真正改驱动代码的时间几乎为零,绝大部分时间都耗在设备树匹配和平台差异上。我个人最大的体会是:国产嵌入式平台的寄存器手册和GPIO中断模型往往不按主流开发板的套路来,设备树中ARM平台的写法不能直接生搬硬套。拿到一块新板子,先把I2C器件扫描通、把数据读出来,再回头搞中断和高级功能,这个顺序能省掉无数头大的排查时间。

如果你也在做类似的传感器驱动适配,建议保存一下第5节的检查清单,按部就班来,不要把时间浪费在怀疑驱动本身。龙芯k平台加MPU6050这个组合,只要把设备树和内核版本这两关过了,后面就是一片坦途。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询