拿到“龙芯k - 走马观碑组MPU驱动移植”这个项目标题时,我第一反应就是:这活儿听起来不大,但真要跑通,牵扯的链条一点都不短。所谓“MPU驱动移植”,放到实际场景里,基本可以锁定为在龙芯平台上把MPU6050这颗六轴惯性传感器的Linux内核驱动调通,让它能稳定地输出加速度和角速度数据。这事儿对于刚接触龙芯LoongArch架构、或者准备做嵌入式外设适配的同学来说,是一条非常典型的学习路径:从硬件接线、设备树编写,到内核配置、驱动模块编译,再到用户态数据验证,每一步都有坑,但每一步也都能学到东西。
这篇东西我打算按自己实际做过的流程来写,以龙芯LS2K0500开发板为主线,兼顾3A5000这类平台上的差异。目的是让你拿着这片文章,不敢说闭着眼睛能复制,但至少能少走一半弯路。适合谁看?手上刚好有龙芯板子、想把I2C类传感器或外设驱动跑起来的人,或者对Linux内核驱动移植流程感兴趣、想找一个完整案例来练手的学生和工程师。
1. 先看懂这个项目在做什么
1.1 项目目标和交付物拆解
“走马观碑组”这个组名看着像是一个内部项目组的代号,实际干的事情却很具体:把MPU6050的驱动移植到龙芯平台上。MPU6050是InvenSense(现在属于TDK)出的六轴运动处理传感器,内部集成了一颗三轴加速度计和一颗三轴陀螺仪,通过I2C接口与主控通信,地址默认为0x68。这颗芯片在消费电子和嵌入式领域用得非常多,无人机、平衡车、机器人、姿态检测模块里都能看到它,Linux内核里也早就有了成熟的上游驱动支持。
所以这个项目的交付物其实包含三块:第一,内核里能识别并绑定这个设备的驱动,也就是CONFIG_INV_MPU6050_I2C对应的那套代码,源码路径在drivers/iio/imu/inv_mpu6050/;第二,描述硬件连接关系的设备树节点,告诉内核“这个MPU6050挂在哪个I2C控制器上、中断脚接哪里、安装方向是怎样的”;第三,用户态能访问的数据接口,也就是IIO子系统生成的/sys/bus/iio/devices/iio:device0/目录,从里面读原始值、量程和采样频率。
很多刚做驱动移植的人容易把目标理解成“写一个驱动文件”,其实在Linux驱动模型下,尤其是这种已经有上游驱动的芯片,真正的难点不在写代码,而在把“设备树描述”和“内核配置”这两件事做对,让既有驱动能在新架构的板子上被正确加载和枚举出来。
1.2 为什么选MPU6050作为移植对象
我个人的看法是,MPU6050几乎是做驱动移植入门最理想的一颗芯片。原因有三个:
第一,硬件接口简单。它只需要I2C,加上一根可选的INT中断脚,总共四根线,不像一些复杂的PCIe设备或者网卡那样需要处理中断控制器、DMA通道、电源管理等一堆东西。
第二,内核驱动成熟稳定。从内核3.x时代开始,inv_mpu6050驱动就在持续维护,到了5.x、6.x版本已经非常完善,支持IIO框架、支持DMP固件加载、支持trigger缓冲。这意味着我们不需要从零造轮子,只需要让这个驱动在龙芯的LoongArch架构下跑起来就行。
第三,验证成本低。一块板子、一个传感器模块、几根杜邦线就能完成全部验证。我在实际项目中用LS2K0500迷你开发板,飞了几根线,半小时内就把硬件链路搭起来了,剩下的时间全在跟内核和设备树较劲。
还有一点不能忽略:MPU6050的量产资料、官方手册、社区帖子都非常全。一旦驱动加载出问题,排查资料比某些冷门芯片多一个数量级。对于刚接触龙芯平台的人来说,选它做第一个移植对象,心理压力会小很多。
1.3 移植思路:不自己造轮子,复用内核上游驱动
这里要强调一个关键判断:移植驱动和编写驱动是两码事。对于MPU6050,几乎没有理由去从寄存器级别重新写一个字符设备驱动。你去网上搜,确实能找到很多裸机或者RTOS下的MPU6050驱动代码,但在Linux系统里,那些代码基本用不上,硬塞进去反而是灾难。正确的做法是复用内核里的inv_mpu6050驱动,它已经处理好了寄存器初始化、量程设置、FIFO读取、中断处理、IIO接口注册等所有脏活累活。
所以我们的移植工作重心其实是以下几步:确认内核版本里带有inv_mpu6050驱动源码、使能对应的Kconfig选项、确认I2C控制器驱动可用、编写正确的设备树节点、编译内核或模块、部署并验证。这个思路同样适用于其他I2C类传感器,比如BMP280、LSM6DS3等,套路是一致的,换了芯片也只是改一下compatible字符串和属性参数。
2. 环境准备与内核源码初始化
2.1 龙芯平台的交叉编译环境搭建
在龙芯板子上做驱动开发,有两种玩法。一种是直接在板子上装好Loongnix或LoongOS系统,原生编译,优点是不用管交叉编译链,但缺点也很明显:板子性能有限,编内核时间很长。另一种是在PC上搭交叉编译环境,出内核镜像和模块,再拷到板子上,这也是我推荐的方式。
交叉编译工具链方面,龙芯官方提供了loongarch64-linux-gnu-前缀的gcc工具链。如果你是x86的PC,可以从龙芯开源社区或者一些第三方镜像站下载。装好之后设置一下环境变量:
export ARCH=loongarch export CROSS_COMPILE=loongarch64-linux-gnu-注意,从内核5.19开始,LoongArch架构的代码正式合入了上游主线,所以理论上你直接拿一个6.x版本的上游内核,也能编译出可用的LoongArch内核。但实际使用中,开发板的板级支持代码往往只在龙芯维护的内核分支里,比如LS2K0500的板级设备树、I2C控制器的pinctrl配置这些,在上游主线里不一定齐全。所以强烈建议直接用龙芯开发板对应SDK里的内核源码,而不是自己去上游找最新版,省去一堆适配的麻烦。
2.2 拿到带板级补丁的内核源码
以LS2K0500为例,配套的SDK里会提供完整的内核源码包。拿到源码之后,第一件事不是急着改配置,而是先确认两件事:一是驱动源码drivers/iio/imu/inv_mpu6050/是否存在;二是板级设备树文件arch/loongarch/boot/dts/loongson/或者类似路径下有没有对应的dts文件,里面有没有I2C控制器的定义。
在确认驱动源码存在之后,先加载默认配置。不同版本的SDK可能配置项名称不一样,有的是ls2k0500_defconfig,有的是loongson3_defconfig,根据实际SDK文档来。加载完默认配置后,用menuconfig做可视化修改:
make loongson3_defconfig make menuconfig在菜单里依次找到:
Device Drivers ---> <*> Industrial I/O support ---> <*> InvenSense MPU6050 devices ---> <*> InvenSense MPU6050 I2C driver这里需要注意,IIO框架是必选项,不能选成模块。因为MPU6050驱动依赖IIO核心的符号,如果IIO本身是模块而MPU6050驱动也是模块,加载顺序会变成麻烦事。在我的实践中,CONFIG_IIO=y、CONFIG_INV_MPU6050_I2C=m是比较稳的组合,MPU6050驱动用模块方式,方便单独更新和调试,IIO核心编进内核避免依赖顺序问题。
2.3 I2C控制器的配置确认
设备树写得再漂亮,如果I2C控制器驱动没使能,一切都白搭。在menuconfig里还要确认:
Device Drivers ---> <*> I2C support ---> <*> I2C device interface <*> Synopsys DesignWare I2C adapter龙芯很多芯片的I2C控制器用的是DesignWare的IP核,对应内核驱动是i2c-designware。LS2K0500内部集成的I2C控制器也是这个风格,所以CONFIG_I2C_DESIGNWARE这系列选项要开。如果这个没开,dmesg里会完全看不到I2C适配器节点,后面i2cdetect也就无从谈起。
另外建议打开I2C的调试工具支持,也就是CONFIG_I2C_CHARDEV,这样用户态才能用i2c-tools里的i2cdetect、i2cget这些命令直接访问总线,排查阶段几乎离不开它们。
3. 设备树编写与驱动绑定
3.1 找到I2C控制器节点并确认状态
龙芯的设备树管理方式和ARM平台很像,板级dts文件里会定义好几个I2C控制器,但默认不一定都打开。我碰到过一种很典型的情况:dts里I2C0、I2C1、I2C2都在,但状态字段全是disabled,需要手动把用到的那个改成okay。
比如在LS2K0500的dts文件里,可能会看到类似这样的节点:
&i2c0 { status = "okay"; clock-frequency = <400000>; pinctrl-names = "default"; pinctrl-0 = <&i2c0_pins>; };这里有几件事要确认。第一是status必须改成okay,否则内核不会注册这个I2C适配器;第二是clock-frequency,MPU6050最高支持400kHz快速模式,但实际使用中如果线材比较长、或者上拉电阻没选好,400kHz可能会不稳,建议先设在100kHz跑通再说;第三是pinctrl引脚的配置,这个最容易出问题,如果引脚复用不对,就算I2C控制器状态正常,SDA和SCL也出不了波形。
3.2 添加MPU6050子节点,关键字段逐个解释
I2C控制器节点搞定之后,在它下面添加MPU6050的子节点。这是整个移植过程中最核心的一步,我直接给出一个我在项目中实际验证过的写法:
&i2c0 { status = "okay"; clock-frequency = <400000>; mpu6050@68 { compatible = "invensense,mpu6050"; reg = <0x68>; interrupt-parent = <&gpio>; interrupts = <12 IRQ_TYPE_EDGE_RISING>; mount-matrix = "0", "1", "0", "-1", "0", "0", "0", "0", "1"; }; };先看compatible。这个字符串必须和驱动里的of_match_table完全一致,否则驱动不会绑定到这个设备。在inv_mpu6050_i2c.c里,匹配表写的是"invensense,mpu6050",如果你写成别的名字,驱动根本不会probe,而且由于I2C不会自动匹配设备ID(除非驱动里还有i2c_device_id表),dmesg里会只有设备被枚举的记录,看不到驱动加载的记录。
再看reg = <0x68>。这是I2C从机地址。MPU6050的AD0引脚接地时地址是0x68,接高电平时是0x69。这个地址必须和硬件实际接法吻合,不然I2C核心在访问设备时会收到NACK,i2cdetect也扫不到设备。
interrupt-parent和interrupts是中断配置。如果硬件上把MPU6050的INT引脚接到了某个GPIO,这里就写对应的GPIO控制器和GPIO编号。需要注意中断触发类型,MPU6050默认的INT输出是高电平有效,但实际波形取决于寄存器配置和FIFO溢出状态,我在调试中更常用IRQ_TYPE_EDGE_RISING,这是因为驱动初始化后会开中断使能,传感器一旦有数据就产生一个上升沿。如果你在设备树里配置了中断,但硬件上其实没接线,这个中断的probe可能会失败,或者设备工作异常。这种情况下最简单的办法是去掉中断配置,让驱动工作在轮询模式,后面你自己在采样周期上做处理。
mount-matrix是传感器安装方向矩阵。MPU6050装到板子上未必是正方向,可能转了90度或者倒装了,这个矩阵就是告诉内核“传感器的物理方向与逻辑方向之间的映射关系”。如果不写,驱动会使用默认的单位矩阵,也就是认为传感器方向和板子坐标系一致。做姿态解算之前,这个必须校准,否则算出来的欧拉角全是歪的。但对一个以“先把数据读出来”为目标的移植项目来说,用单位矩阵先跑通完全没问题。
3.3 硬件接线与电平确认
设备树是软件侧的“接线图”,但硬件侧接得不对,软件再怎么调都没用。MPU6050模块一般有八个引脚,我们常用的只有四个:VCC、GND、SCL、SDA。
接线方面有几个细节容易踩坑。第一是VCC,很多MPU6050模块是3.3V供电,也有少数5V供电的版本,必须确认清楚,接错电压轻则传感器不工作,重则冒烟。第二是SDA和SCL,它们是开漏输出,必须有上拉电阻。大部分现成模块上已经焊接了上拉电阻,但如果你用的是裸芯片,就需要自己在I2C总线上加上拉电阻,通常4.7kΩ到10kΩ都行。第三是地线必须共地,这个看上去是常识,但我在实验室里经常看到有人只接VCC、GND、SDA,忘了SCL,然后找半天问题。
板子侧的I2C引脚,最好先查一下开发板的原理图,确认用的是哪个I2C控制器、对应到板子上的哪个排针。LS2K0500开发板一般会把I2C引脚引出来,标注为I2C0_SCL、I2C0_SDA之类,照着接就行。千万不要凭感觉猜测,我就遇到过把SDA和SCL接反的情况,设备树怎么改都不对,最后用示波器一看,两根线上都有波形,但就是没有正确的ACK应答。
4. 编译驱动、部署镜像与用户态验证
4.1 把驱动编成模块还是直接编进内核
inv_mpu6050驱动在Kconfig里提供了两种选择:编进内核(=y)或者编成模块(=m)。我建议在开发和调试阶段用=m,原因很实际:改设备树或者改驱动参数时,只需要重新编译模块和dtb,不用每次都重编整个内核、刷整个镜像,验收效率高很多。等确认稳定了,再考虑编进内核,减少启动时加载模块的步骤。
编译命令很简单,在确认menuconfig配置保存之后直接执行:
make -j$(nproc) Image modules这里Image是内核镜像目标,龙芯平台上一般用Image而不是zImage。modules目标会编译所有配置为=m的驱动,包括你要的inv_mpu6050_i2c.ko和相关依赖模块。
编译完成后,可以在源码目录里用find确认一下产物:
find . -name "*mpu6050*"正常情况下会看到inv_mpu6050_core.ko和inv_mpu6050_i2c.ko两个文件,前者是驱动核心逻辑,后者是I2C总线绑定层。如果你只编出来一个,说明Kconfig里有些依赖项没满足,比如IIO可能被设成了模块,导致驱动也变成模块但依赖关系奇怪。
4.2 部署内核、设备树和模块到开发板
部署方式取决于你的开发板启动方式。LS2K0500这类板子一般可以从网络、SD卡或者USB存储启动,我这里说一种通用的做法:把新编译的Image、dtb文件和.ko模块都拷到板子的启动分区,然后在引导参数里指定新的镜像。
模块拷贝的时候要注意,不能只拷.ko,因为驱动模块有依赖关系,inv_mpu6050_i2c.ko依赖inv_mpu6050_core.ko,后者又依赖IIO核心。如果你把模块目录整个同步过去,比如:
make INSTALL_MOD_PATH=/mnt/rootfs modules_install这样会自动处理模块依赖和depmod,是最省心的做法。然后把整个内核模块目录打包拷到板子上。如果只是单模块调试,也可以手动拷贝:
cp drivers/iio/imu/inv_mpu6050/inv_mpu6050_core.ko /lib/modules/$(uname -r)/ cp drivers/iio/imu/inv_mpu6050/inv_mpu6050_i2c.ko /lib/modules/$(uname -r)/ depmod -adepmod这步别省,它会生成模块依赖文件,后面你用modprobe加载的时候才知道去哪个文件找依赖。
4.3 用i2cdetect和sysfs验证设备是否真正工作
部署完成重启之后,第一件事是确认I2C总线上能看到MPU6050。在板子上运行:
i2cdetect -y 0这里的0是I2C总线的编号,具体是哪个取决于设备树注册顺序,可以通过ls /dev/i2c-*查看。正常情况下,在地址0x68的位置会显示68,标志这个地址上有设备应答。
如果这一步能看到设备,说明硬件链路和数据链路都是通的。接下来加载驱动:
modprobe inv_mpu6050_i2c然后查看dmesg:
dmesg | tail -20成功的话会看到类似inv-mpu6050 i2c-0: 0x68: whoami 0x68这样的日志,这是驱动读取WHO_AM_I寄存器后打印的,读到0x68说明传感器正确回复了身份信息。
驱动加载成功之后,IIO子系统会自动创建设备节点。进入目录:
cd /sys/bus/iio/devices/iio:device0/ ls你会看到一堆in_accel_x_raw、in_accel_y_raw、in_accel_z_raw、in_anglvel_x_raw、in_anglvel_y_raw、in_anglvel_z_raw之类的文件。用cat直接读:
cat in_accel_z_raw静止放置时,Z轴的加速度原始值应该在16384附近,因为默认量程是±2g,而1g对应的就是16384 LSB。如果读数接近0或者满偏,那大概率是量程配置、安装方向或者传感器本身状态有问题。
一个很实用的验证技巧是:读一下非轴向的数据,比如把板子水平放置时X轴和Y轴的加速度原始值应该在0附近,Z轴接近16384。如果X轴也很大,说明传感器没有放平,或者mount-matrix配置不对。
5. 移植过程中的坑与排查记录
5.1 常见问题速查表
我在实际移植过程中踩过的坑,加上帮别人排查过的案例,汇总成下面这张表。如果你在做这个项目,遇到问题可以先对着表查一遍。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
i2cdetect扫描不到设备 | 从机地址写错、接线松动、上拉电阻缺失、供电不足 | 用万用表量VCC;确认AD0电平;示波器看SDA/SCL波形 |
扫描到设备但modprobe后无反应 | 设备树compatible不匹配、驱动源码未编译进模块 | 检查dts里的compatible是否等于invensense,mpu6050;确认ko文件存在 |
| 驱动加载但dmesg报probe失败 | 中断配置错误、WHO_AM_I读不到 | 临时去掉interrupt字段;用i2cget -y 0 0x68 0x75看返回是否是0x68 |
| sysfs里没有IIO设备目录 | IIO核心未开启、驱动编译成模块但未加载依赖 | 确认CONFIG_IIO=y;检查dmesg有没有注册IIO设备的日志 |
| 数据读出来全是0或满偏 | 传感器芯片进入休眠、量程配置未生效、mount-matrix错误 | 用i2cset唤醒设备;确认设备树无异常属性;核对原始值范围 |
| 内核编译时报LoongArch架构兼容错误 | 驱动依赖了x86或ARM特有代码 | 检查驱动源码里的#include和条件编译,通常需要补架构相关的头文件或函数 |
5.2 典型问题:i2cdetect扫描不到设备的排查思路
这个现象出现概率最高,但原因也最多样。我第一次在LS2K0500上接MPU6050时,i2cdetect扫了一大圈,一个地址都没出来。当时以为设备树写错了,反复检查配置,后来才发现是模块的SDA和SCL接反了。所以在动软件之前,先用示波器或者逻辑分析仪看波形。
如果确认接线没问题,就检查地址。MPU6050的7位地址是0x68,注意这是7位地址,有些工具显示的是8位地址(左移一位变成0xD0),别被这个搞晕。i2cdetect显示的是7位地址,所以0x68是正常的。
还有一种可能:I2C总线上有其他设备占用了地址,导致MPU6050无法应答。多设备共用总线时,地址冲突是常事。这时候要么改硬件上AD0的电平把地址改成0x69,要么换一条空闲的I2C总线。
5.3 典型问题:驱动已加载但没有生成IIO设备节点
设备在总线上能扫到,驱动模块也能insmod进去,但/sys/bus/iio/devices/下面空荡荡。这个问题我曾经被坑了一整个下午,最后定位到原因是设备树里少了mount-matrix属性,而某个版本的内核驱动在解析该属性失败时会直接返回-EINVAL,导致probe中断。
这个问题的排查套路是看dmesg:
dmesg | grep -i mpu dmesg | grep -i inv通常能看到类似inv-mpu6050 i2c-0: mount matrix not found: -22或者更模糊的probe of i2c-0 failed with error -22。这里-22就是-EINVAL,基本可以断定是某个设备树属性没写或者写错了。解决办法很简单,把我在3.2节里给出的完整设备树片段原样贴进去,编译替换dtb就能过。
需要提醒的是,有些龙芯SDK里的内核版本可能对这个驱动的支持不完整,比如inv_mpu6050的设备树解析代码在旧内核里对mount-matrix的处理不如新内核健壮。遇到这种情况,要么升级SDK内核,要么像我一样写全所有支持的属性,强行绕过去。
5.4 板级固件与模拟环境的补充说明
龙芯开发板的启动固件(有的叫BIOS,有的叫PMON,视平台而定)偶尔也会对驱动移植产生影响。我遇到过一次I2C控制器始终注册不上的情况,设备树、内核配置、引脚复用全查了个遍都没问题,最后是刷新了板级固件才解决。这个概率不高,但如果你所有常规排查手段都无效,可以往这个方向想一想,看看官方有没有针对外设枚举或I2C控制器的新版固件。
另外,很多人会问:手头没有实体板子,能不能先把驱动移植的流程跑通?答案是可以用龙芯模拟环境。QEMU支持LoongArch虚拟化,能启动龙芯内核,配合busybox根文件系统做模块加载验证。不过要注意,模拟环境里没有真实的I2C硬件控制器,所以MPU6050这种真实外设是枚举不出来的。它的价值在于提前验证驱动模块在LoongArch架构下的编译兼容性、加载时机和依赖关系,等真机到位之后再处理设备树和硬件层面的问题。我用这个方式在等板子的空窗期把inv_mpu6050.ko的编译和模块依赖先跑通了,为后续节省了不少时间。
6. 实操心得与后续扩展建议
做完整套MPU6050驱动移植,我最大的感受是:这个项目的价值不在于“把一颗传感器的驱动跑起来”本身,而在于它把嵌入式Linux开发中几个最关键的知识点串成了一条完整的链路。你在这个过程中会接触设备树、接触内核Kconfig体系、接触IIO子系统、接触I2C协议栈、接触模块加载机制,任何一个环节理解不到位,设备就是跑不起来。这种“全链路联调”的经验,比单独看某个子系统的文档要深刻得多。
最后再分享一个我在调试中养成的习惯:每改动一个环节,只验证一个变量。比如先确保i2cdetect能扫到设备,再加载驱动;加载驱动之后先看dmesg有没有probe成功,再去看sysfs节点;sysfs节点出现了,再去读数据。不要一次性同时改设备树、换内核配置、调驱动代码,否则出了错根本不知道是哪一步引入的。这个习惯帮我省掉的排查时间,比我写过的任何一段代码都值钱。
如果你已经把这套流程走通了,后续可以考虑的方向是:给MPU6050接上中断脚,用IIO的trigger模式做中断驱动采样,替代最简单的轮询读取;或者加载DMP固件,用硬件姿态解算替换纯软件算法。这些都是在当前驱动框架基础上自然的延伸,本质上还是我们对同一颗传感器在Linux系统里用法的深化。到了那个阶段,你再回头看“MPU驱动移植”这个项目标题,会发现它只是一个起点,而不是终点。