龙芯平台MPU6050驱动移植实战:设备树与IIO全程解析
“走马观碑”这名字听着挺快但这次干的活真快不起来。上周接了个任务要把一颗MPU6050六轴传感器在龙芯平台上把驱动移植跑通。名字里带个“k”说白了就是龙芯内核kernel的驱动适配活。乍一看是个老生常谈的驱动问题实际动手才发现整个链路涉及硬件接线、设备树描述、内核配置、交叉编译四个层面任何一个环节没对齐数据都会以玄学方式消失。这篇文章把这次移植过程完整记录下来包括方案选型、设备树怎么写、内核怎么配置、编译烧录以后怎么验证以及在真机上踩过的几个坑。无论是刚接触国产平台开发的新手还是准备把IMU类传感器接到其他嵌入式Linux平台上的工程师这份记录应该都能帮你省下不少时间。1. 项目拆解先把“移植”这事想明白1.1 这个任务到底要解决什么问题接到任务时“走马观碑组MPU驱动移植”这句话其实挺模糊的。组里有人做过类似项目但对龙芯平台不熟有人熟悉龙芯但没搞过传感器驱动。第一件事就是把需求拆开确认到底要做什么。这里提到的MPU从芯片型号看就是InvenSense现TDK的MPU6050一颗六轴惯性测量单元内部集成了三轴加速度计和三轴陀螺仪。它通过I2C接口和主控通信在嵌入式Linux里非常常见。所谓“驱动移植”本质上是解决三个问题第一个Linux内核怎么知道这颗芯片的存在答案是设备树设备树里要把MPU6050挂在哪个I2C控制器下、用哪个地址、中断接在哪个引脚上全部描述清楚。第二个谁来和芯片对话、把原始传感器数据读出来答案是内核驱动MPU6050在Linux主线内核里已经有官方驱动不需要从头写。第三个数据读出来之后上层应用怎么拿答案是内核的IIOIndustrial I/O子系统驱动把数据注册成标准接口应用层直接读文件节点就行。所以这个任务真正的核心工作量不在“写代码”而在“对参数”——把硬件连接、设备树配置、内核编译选项这三样东西对齐。理清这一点后面做起来心里就有底了。1.2 移植方案选型用内核自带驱动还是自己写方案选择上摆在面前的有三条路。第一条路是用Linux内核主线自带的inv_mpu6050驱动它在内核源码drivers/iio/imu/inv_mpu6050/目录下支持I2C和SPI两种接口兼容MPU6050、MPU6500、MPU9150等一堆型号功能完整代码维护活跃。这是最推荐的路子因为驱动是现成的只要在内核配置里打开相关选项把设备树节点写对基本上就能跑起来。第二条路是找芯片厂商或第三方提供的板级驱动包通常是自定义的字符设备驱动直接读写寄存器然后在应用层做封装。这种方案在老产品里很常见但问题也明显驱动代码和特定内核版本绑定内核一升级就报错维护成本高而且接口不标准应用层代码没法复用。第三条路是自己写一个字符设备驱动从零开始操作I2C注册misc设备然后把数据用ioctl或read传给用户空间。这种方案适合学习原理但不适合工程项目因为要把寄存器初始化、FIFO管理、中断处理、数据校准这一整套逻辑重新实现一遍工作量不小。最终选了第一条路——内核自带驱动。原因很简单项目要求尽快跑通功能而且后续要扩展到其他传感器用标准IIO框架最省心。这次的实际经验也说明主线驱动在龙芯平台上不需要改一行C代码纯粹靠设备树和内核配置就能适配。1.3 设备树这次移植的真正重头戏如果只用一句话总结这次移植的难点我会说“设备树是灵魂”。很多新手搞Linux驱动移植特别喜欢盯着C代码看但实际上在龙芯这种不太常见的平台上写错C代码的概率极低真正卡住你的往往是设备树里一个不起眼的属性。设备树Device Tree是一种描述硬件资源的数据结构它的作用相当于把原本写在板级文件里的硬件信息抽离出来用文本形式描述 CPU 有哪几个 I2C 控制器、GPIO 控制器每个外设挂在哪个总线上、地址是多少、用到哪根中断线。内核启动时解析设备树然后根据节点里的compatible字段去匹配对应的驱动。因为龙芯平台的板卡没有统一的标准设备树每一块开发板的设备树文件都不一样所以写设备树之前必须先搞清楚板子的实际情况——I2C控制器是几号、中断控制器是哪个节点、引脚有没有复用冲突。这次项目的经验是拿到开发板先别急着动代码先读原理图再读厂家提供的设备树源文件最后再动手改。原理图上标得清清楚楚的东西比任何想当然都可靠。2. 硬件原理与依赖关系先把底层的活搞清楚2.1 MPU6050这颗芯片到底是怎么工作的虽然驱动代码是现成的但不懂芯片的原理出了问题就只能瞎猜。MPU6050的内部结构可以简单分成五块三轴陀螺仪、三轴加速度计、温度传感器、DMP数字运动处理器以及一个I2C从机接口。陀螺仪测量角速度单位是度每秒dps加速度计测量比力通常说加速度单位是g或者m/s²。DMP可以在芯片内部完成姿态解算把四元数直接算好输出省去主控的浮点运算负担——但内核自带驱动默认不用DMP而是把原始数据直接上报姿态解算放在应用层做。芯片和主控通信的接口是I2C这是一个非常经典的两线制协议SCL时钟线、SDA数据线。MPU6050作为I2C从机地址取决于AD0引脚的电平——AD0接地时地址是0x68接高电平是0x69。一个I2C总线上最多可以挂两个MPU6050就是靠这个地址位区分的。寄存器操作方面有几个关键寄存器必须知道。WHO_AM_I0x75是用来识别芯片身份的读出来的值固定是0x68这个值可以用来验证I2C通信是否正常。PWR_MGMT_10x6B是电源管理寄存器上电后默认芯片处于休眠状态必须把这个寄存器的值清零或者设置为0x01才能唤醒。SMPLRT_DIV0x19设置采样率分频GYRO_CONFIG0x1B设置陀螺仪量程ACCEL_CONFIG0x1C设置加速度计量程。这些寄存器在驱动初始化时都会配置但理解它们的作用后面排查问题时会很有帮助。2.2 龙芯平台的I2C控制器和中断资源龙芯平台和常见的ARM开发板在设备树写法上有一些差异但硬件原理是通用的。这次用的龙芯SoC内部集成多个I2C控制器每个控制器有一组独立的SCL/SDA引脚对应设备树里的i2c0、i2c1这样的节点。使用哪个控制器完全取决于硬件原理图上MPU6050的SCL和SDA实际接到了哪组引脚。这里有个细节需要注意I2C是开漏输出总线上必须有上拉电阻才能正常工作。很多市售的MPU6050模块已经自带上拉电阻但如果你是自己画的板子一定要在SCL和SDA上各接一个4.7kΩ左右的上拉电阻到3.3V。否则I2C通信会出现间歇性失败设备一会儿能扫到一会儿扫不到非常折腾。中断资源方面MPU6050有一个INT引脚可以在数据准备好时产生中断通知主控读取。设备树里要把这个引脚对应的GPIO号和中断触发方式描述清楚。如果不用中断驱动也可以用轮询方式读取数据但效率和实时性会差一些。这次项目里把中断接上了设备树里配置成上升沿触发。需要提醒的是不同龙芯板卡对中断控制器的描述方式不一样有的用gpio有的用pic务必以实际板子的设备树源文件为准。2.3 设备树节点到底该怎么写主语驱动匹配设备树节点靠的是compatible字符串。MPU6050在内核驱动里对应的字符串是invensense,mpu6050。如果compatible写错驱动模块加载的时候根本无法和你这个节点配对设备就不会出现在系统里。一个完整的MPU6050设备树节点长这样i2c0 { status okay; clock-frequency 400000; mpu605068 { compatible invensense,mpu6050; reg 0x68; interrupt-parent gpio; interrupts 15 IRQ_TYPE_EDGE_RISING; mount-matrix 0, 1, 0, -1, 0, 0, 0, 0, 1; }; };解释一下每个字段。mpu605068是节点名字后面是I2C地址这只是个标识不影响功能。reg 0x68才是真正告诉驱动芯片在总线上哪个地址。interrupt-parent指定中断控制器interrupts指定引脚编号和触发方式。最后那个mount-matrix是三维旋转矩阵用来描述芯片在板子上的安装方向——如果芯片的X轴和板子的X轴方向不一致通过这个矩阵可以重新映射坐标轴。这次项目里器件是立着装的所以矩阵里有一堆0和1如果你平着装矩阵就是单位矩阵1,0,0、0,1,0、0,0,1。设备树写完之后不要急着编译整个内核。先在板子上用i2cdetect确认硬件连接没问题再进设备树这一步能省掉大量排查时间。3. 实操过程从零到驱动跑通3.1 第一步硬件连接选型和引脚确认这次用的是一块龙芯开发板板子上已经预留了I2C接口的排针。MPU6050模块是市面上很常见的蓝色小板引出VCC、GND、SCL、SDA、AD0、INT六个引脚。接线遵循的原则很简单同电压域互接信号线一一对应。VCC接3.3VGND接GNDSCL接板子的I2C_SCLSDA接板子的I2C_SDAAD0接GND这样地址是0x68INT接板子的一个空闲GPIO。特别提醒MPU6050的VCC不能接5V芯片最大额定电压是3.6V接5V会直接烧掉。如果模块板载了稳压芯片那另说但最好不要赌这个。接好线之后先别急着做软件。上电用万用表量一下模块VCC引脚对地有没有3.3VSCL和SDA有没有被拉高正常应该量到3.3V左右这一步能提前发现接线错误避免后面对着软件瞎调。3.2 第二步准备交叉编译工具链和内核源码龙芯的CPU架构是LoongArch和常见的x86、ARM都不一样所以编译内核需要用龙芯的交叉编译工具链。官方提供的工具链前缀通常是loongarch64-linux-gnu-从龙芯开源社区可以下载到现成的工具链压缩包解压后把bin目录加进PATH环境变量即可。内核源码方面建议直接从龙芯维护的Linux内核仓库拉取因为里面包含了龙芯各款SoC的板级设备树和支持代码。如果用主线内核某些开发板可能存在设备树不完整的情况反而要多花时间补配置。# 以实际安装路径为准把工具链加入PATH export PATH/opt/loongarch-toolchain/bin:$PATH export ARCHloongarch export CROSS_COMPILEloongarch64-linux-gnu-说明一下ARCHloongarch告诉内核构建系统你编译的是哪个架构CROSS_COMPILE指定交叉编译器的前缀。这两个环境变量在后续所有编译步骤里都会用到。3.3 第三步修改设备树文件这一步是整个移植过程中最需要细心的地方。龙芯开发板的内核源码里设备树文件一般在arch/loongarch/boot/dts/loongson/目录下文件名通常是ls2k1000.dts、ls3a5000.dts之类的以实际板型为准。找到对应的.dts文件后先搜索i2c0节点确认该I2C控制器有没有被启用——有些板子的I2C控制器默认是disabled状态需要在设备树里显式改为okay。然后在这个节点下添加MPU6050子节点内容和前面写过的示例一致。注意interrupt-parent不要照抄示例要改成实际板子提供的GPIO控制器节点名。设备树改完后编译它是个独立步骤可以只编译设备树而不重新编译整个内核make dtbs编译好的.dtb文件会在arch/loongarch/boot/dts/loongson/下生成。这个文件需要和内核镜像一起烧到板子上或者放在引导分区里让引导程序加载。3.4 第四步内核配置打开IIO和MPU6050驱动开关设备树描述了“硬件在哪里”内核配置则决定“驱动编不编进去”。MPU6050采用的是IIO子系统所以内核里要打开一系列配置选项。最稳妥的方式是直接用make menuconfig图形界面找到对应路径Device Drivers --- * Industrial I/O support --- -*- Enable buffer support within IIO -*- Industrial I/O buffered device based on triggered buffer *** Accelerometers *** * InvenSense MPU6050 devices --- * MPU6050 i2c transport driver菜单路径可能随着内核版本不同略有变化但搜索功能是最快的——在menuconfig界面按/输入INV_MPU6050就能直接跳到对应选项。另外还需要确保I2C子系统已经打开一般龙芯的默认配置里I2C是开的但保险起见也要检查一下。如果不想用menuconfig也可以直接改.config文件手动确认下面三个配置项CONFIG_I2Cy CONFIG_IIOy CONFIG_INV_MPU6050_I2Cy把这三个选项编成y直接编进内核镜像而不是m编译成模块这样驱动会随内核启动自动加载省去手动modprobe的麻烦。3.5 第五步编译内核、设备树和部署配置完成后开始编译。第一次编译龙芯内核可能比较久取决于机器性能一般十几分钟到半小时都有可能。make -j$(nproc) Image dtbs编译产物里Image是内核镜像.dtb是设备树二进制文件。把它们部署到板子上通常有两种方式一种是通过TFTP从网络启动适合调试阶段反复修改另一种是烧写到板载存储介质适合最终交付。调试阶段我强烈推荐网络启动——每次编译完直接用TFTP拉取新内核和设备树省去反复插拔存储卡的痛苦。板子起来之后第一步是确认I2C总线上能不能扫到设备# 查看系统里的I2C总线列表 i2cdetect -l # 在I2C总线0上扫描设备按实际情况替换总线号 i2cdetect -y 0如果一切正常在0x68的位置会看到一个编号说明芯片在总线上响应了。然后再看IIO设备有没有生成ls /sys/bus/iio/devices/正常情况下会看到类似iio:device0的目录列一下里面的文件ls /sys/bus/iio/devices/iio:device0/能看到in_accel_x_raw、in_accel_y_raw、in_accel_z_raw、in_anglevel_x_raw、in_anglevel_y_raw、in_anglevel_z_raw、in_temp_raw这些文件就说明驱动已经完全跑起来了。3.6 第六步读取数据并验证正确性驱动跑通只是“能工作”还要验证数据对不对。最简单粗暴的方式是直接读文件节点# 读取加速度计X轴原始值 cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw # 读取陀螺仪Z轴原始值 cat /sys/bus/iio/devices/iio:device0/in_anglevel_z_raw原始值不是物理单位需要乘以scale文件里的系数才是实际测量值。比如in_accel_x_scale的值如果是0.000598那X轴的加速度就是原始值乘以0.000598单位是m/s²。陀螺仪同理。更好的验证是让芯片动起来。把板子平放在桌上加速度计Z轴读数应该接近1g也就是原始值乘scale后约等于9.8左右X和Y轴接近0。然后拿起板子快速翻转可以看到读数明显变化。陀螺仪静止时应该接近0快速转动时读数会变大。用一组手动转动数据对比一下基本就能确认方向和量程都没问题。如果要观察动态波形可以用一句简单的shell循环持续读取while true; do accel_z$(cat /sys/bus/iio/devices/iio:device0/in_accel_z_raw) scale$(cat /sys/bus/iio/devices/iio:device0/in_accel_z_scale) echo $(date %H:%M:%S) accel_z: $(echo $accel_z * $scale | bc) sleep 0.1 done这样能看到实时的数据流验证传感器对运动的响应是否及时。4. 常见问题与排查技巧实录4.1 i2cdetect扫不到设备地址这是最常见的坑而且原因往往很基础。如果i2cdetect -y 0扫不出任何地址先按优先级排查三类问题。第一硬件问题。VCC有没有3.3VGND有没有共地SCL和SDA是不是接反了。用万用表量一下SCL和SDA对地电压正常应该在3.3V附近。如果其中一个被拉低很可能是某个引脚和别的信号短路了或者模块本身是坏的。第二I2C总线编号问题。龙芯SoC有多个I2C控制器你接的不一定是i2c0。要先用i2cdetect -l列出所有总线再挨个扫。曾有同事接了三个多小时没扫出来最后发现MPU6050挂在i2c1上一直在扫i2c0。第三I2C控制器本身没工作。在设备树里确认对应的I2C控制器节点状态是okay而且内核日志里没有报I2C控制器注册失败的错误。用dmesg | grep i2c看看有没有异常信息。4.2 能扫到设备但系统里没有IIO设备如果i2cdetect能扫到0x68但/sys/bus/iio/devices/下什么都没有问题基本出在两个地方。第一个是设备树compatible字符串和驱动不匹配。内核驱动是通过of_match_table里的compatible来匹配设备树节点的字符串必须完全一致大小写和标点都不能错。查一下内核源码里INV_MPU6050驱动定义的地方grep -r invensense drivers/iio/imu/inv_mpu6050/看到的结果应该包括invensense,mpu6050。比对一下设备树里的compatible是不是这个。第二个是内核配置里驱动没编进去。用dmesg | grep mpu看看有没有驱动注册相关的日志。如果没有确认CONFIG_INV_MPU6050_I2C是否真的生效了编译完的内核里可以用grep INV_MPU6050 /boot/config-$(uname -r)来验证——当然这只是通用做法嵌入式环境里直接在源码目录看.config就行。4.3 设备节点生成了但读出的数据全是0或恒定值IIO目录里有设备文件但读出来全是一个固定值或者全零这通常是芯片进入了睡眠模式或者I2C通信半通不通。MPU6050上电后电源管理寄存器PWR_MGMT_1的复位值是0x40也就是芯片默认处于休眠状态只有I2C寄存器可以访问但传感器数据不更新。驱动初始化时要把这个寄存器改成0x01。如果驱动代码正常执行这一步会自动完成但如果你用的是自己写的简单测试程序很容易漏掉这一项。另一个可能原因是芯片没被正确唤醒或者采样率配置出了问题。可以用i2ctransfer手动读写寄存器来验证硬件本身是否正常# 读取WHO_AM_I寄存器0x75应该返回0x68 i2ctransfer -y 0 w10x68 0x75 r1 # 把PWR_MGMT_1寄存器0x6B清零唤醒芯片 i2ctransfer -y 0 w20x68 0x6B 0x00如果WHO_AM_I都读不出0x68那说明I2C通信有问题如果能读出0x68但数据还是不变就要怀疑模块本身是不是坏了。4.4 陀螺仪零漂严重和加速度计噪声大这不算“故障”而是所有MEMS传感器的通病。陀螺仪静止时读数不会刚好是0而是有一个固定的零偏一般在±20dps以内都算正常。加速度计在静止时读数也有抖动这是正常噪声。解决零漂的办法是校准。最简单粗暴的校准方法是把设备静止平放采集一分钟数据然后取平均值作为偏置后续使用实时数据时把偏置减掉。IIO框架里也提供了一些校准机制但在嵌入式场景下最实用的还是在应用层做一次偏移标定。另外如果噪声大到离谱大概率是供电不稳定。MPU6050的VCC上最好加一个0.1uF的去耦电容靠近芯片电源引脚放置。I2C总线上的上拉电阻阻值不对也会引入噪声常见值范围是2.2kΩ到4.7kΩ太大或太小都可能导致通信波形变形。4.5 中断引脚配置不对导致轮询了也拿不到数据这个坑比较隐蔽。如果设备树里interrupts配置不对驱动注册中断时会失败但不会导致系统崩溃只是中断方式的数据读取用不了。如果不小心把中断号和别的设备冲突了还可能干扰其他外设正常工作。排查方法是看内核日志dmesg | grep inv如果看到“IRQ”相关的报错比如request_irq failed那就是中断配置的问题。可以先临时把设备树里的interrupt-parent和interrupts删掉改用轮询方式确认数据能读出来再回头研究正确的中断引脚号。这种方式能把“硬件问题”和“配置问题”分开排查至少先确定芯片本身是好用的。5. 踩坑心得与工程师手记这次移植做完我最大的感受是在这类国产平台上做驱动移植真正难的从来不是写代码而是把“硬件怎么接”和“内核怎么认”这两件事对上。回头看看如果一开始就把文档读透走对流程可能半天就能跑通先看原理图确认I2C总线号和中断引脚再查内核源码确认驱动支持的compatible字符串然后修改设备树、配置内核、编译部署最后用i2cdetect和cat /sys/bus/iio/devices/验证。但实际踩坑之后才发现每个细节都有讲究——总线号不对、上拉电阻缺失、设备树节点忘了配对任何一个都会让数据变成一团迷雾。有个小技巧特别值得分享设备树改完之后可以在板子上直接看内核是否识别了你的节点。进入/proc/device-tree/目录找到对应的I2C控制器节点看看下面有没有mpu605068这个子目录。如果有说明设备树解析正常问题大概率出在驱动匹配上如果没有说明设备树本身就没生效得回去检查编译和部署环节。“走马观碑”这个组名本来是形容反应快、过目不忘。但做驱动移植这件事恰恰不能走马观花。一个寄存器偏差、一个中断号写错结果就是数据乱飘。把每一步都验证到位反而才是最快到达终点的路径。另外给第一次做国产平台开发的朋友提个醒龙芯的生态相比ARM还有一定差距很多教程和资料要靠自己翻内核源码、读设备树文件去摸索但反过来想这个过程能把嵌入式Linux的底层机制理解得更透。MPU6050只是个开始同样的方法完全可以用在气压计、磁力计、温湿度传感器上——只要明白了设备树、IIO子系统、驱动匹配这三板斧后面遇到再多的“MPU类”传感器套路都是一样的。