“走马观碑”这名字听着挺快,但这次干的活真快不起来。上周接了个任务,要把一颗MPU6050六轴传感器在龙芯平台上把驱动移植跑通。名字里带个“k”,说白了就是龙芯内核(kernel)的驱动适配活。乍一看是个老生常谈的驱动问题,实际动手才发现,整个链路涉及硬件接线、设备树描述、内核配置、交叉编译四个层面,任何一个环节没对齐,数据都会以玄学方式消失。这篇文章把这次移植过程完整记录下来,包括方案选型、设备树怎么写、内核怎么配置、编译烧录以后怎么验证,以及在真机上踩过的几个坑。无论是刚接触国产平台开发的新手,还是准备把IMU类传感器接到其他嵌入式Linux平台上的工程师,这份记录应该都能帮你省下不少时间。
1. 项目拆解:先把“移植”这事想明白
1.1 这个任务到底要解决什么问题
接到任务时,“走马观碑组MPU驱动移植”这句话其实挺模糊的。组里有人做过类似项目,但对龙芯平台不熟;有人熟悉龙芯,但没搞过传感器驱动。第一件事就是把需求拆开,确认到底要做什么。
这里提到的MPU,从芯片型号看就是InvenSense(现TDK)的MPU6050,一颗六轴惯性测量单元,内部集成了三轴加速度计和三轴陀螺仪。它通过I2C接口和主控通信,在嵌入式Linux里非常常见。所谓“驱动移植”,本质上是解决三个问题:
- 第一个,Linux内核怎么知道这颗芯片的存在?答案是设备树,设备树里要把MPU6050挂在哪个I2C控制器下、用哪个地址、中断接在哪个引脚上,全部描述清楚。
- 第二个,谁来和芯片对话、把原始传感器数据读出来?答案是内核驱动,MPU6050在Linux主线内核里已经有官方驱动,不需要从头写。
- 第三个,数据读出来之后,上层应用怎么拿?答案是内核的IIO(Industrial 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_I(0x75)是用来识别芯片身份的,读出来的值固定是0x68,这个值可以用来验证I2C通信是否正常。PWR_MGMT_1(0x6B)是电源管理寄存器,上电后默认芯片处于休眠状态,必须把这个寄存器的值清零或者设置为0x01才能唤醒。SMPLRT_DIV(0x19)设置采样率分频,GYRO_CONFIG(0x1B)设置陀螺仪量程,ACCEL_CONFIG(0x1C)设置加速度计量程。这些寄存器在驱动初始化时都会配置,但理解它们的作用,后面排查问题时会很有帮助。
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>; mpu6050@68 { 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"; }; };解释一下每个字段。mpu6050@68是节点名字,@后面是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.3V,GND接GND,SCL接板子的I2C_SCL,SDA接板子的I2C_SDA,AD0接GND(这样地址是0x68),INT接板子的一个空闲GPIO。特别提醒,MPU6050的VCC不能接5V,芯片最大额定电压是3.6V,接5V会直接烧掉。如果模块板载了稳压芯片,那另说,但最好不要赌这个。
接好线之后,先别急着做软件。上电,用万用表量一下模块VCC引脚对地有没有3.3V,SCL和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 ARCH=loongarch export CROSS_COMPILE=loongarch64-linux-gnu-说明一下,ARCH=loongarch告诉内核构建系统你编译的是哪个架构,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_I2C=y CONFIG_IIO=y CONFIG_INV_MPU6050_I2C=y把这三个选项编成=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.3V,GND有没有共地,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 w1@0x68 0x75 r1 # 把PWR_MGMT_1寄存器(0x6B)清零,唤醒芯片 i2ctransfer -y 0 w2@0x68 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控制器节点,看看下面有没有mpu6050@68这个子目录。如果有,说明设备树解析正常,问题大概率出在驱动匹配上;如果没有,说明设备树本身就没生效,得回去检查编译和部署环节。
“走马观碑”这个组名,本来是形容反应快、过目不忘。但做驱动移植这件事,恰恰不能走马观花。一个寄存器偏差、一个中断号写错,结果就是数据乱飘。把每一步都验证到位,反而才是最快到达终点的路径。
另外给第一次做国产平台开发的朋友提个醒:龙芯的生态相比ARM还有一定差距,很多教程和资料要靠自己翻内核源码、读设备树文件去摸索,但反过来想,这个过程能把嵌入式Linux的底层机制理解得更透。MPU6050只是个开始,同样的方法完全可以用在气压计、磁力计、温湿度传感器上——只要明白了设备树、IIO子系统、驱动匹配这三板斧,后面遇到再多的“MPU类”传感器,套路都是一样的。