这个项目名看起来挺有意思——“走马观碑组MPU驱动移植”,缩写叫“龙芯k”,实际干的事就是在龙芯平台上把一个惯性传感器(很典型的就是MPU6050这类六轴芯片)的Linux内核驱动跑起来。标题里的“儇”多半是组名或者笔误,不影响理解。这类工作我这些年做过不少,也踩过不少坑,趁这次把整个移植流程、原理、踩坑点全部摊开聊一遍,希望能帮到正在做国产平台驱动适配的朋友。
先给没接触过这块的读者一个定位:这件事的本质,是把原本在x86/ARM平台上写好的内核外设驱动,通过修改总线适配层、设备树描述、配置选型等方式,让它在龙芯的CPU平台上重新编译、加载并正常工作。听起来不复杂,真正上手之后才会发现,硬件时序差异、内核版本差异、甚至是大小端字节序、DTS节点写法、I2C地址探测这些细节,任何一环都可能让你折腾好几天。
1. 项目整体设计与思路拆解
1.1 核心需求解析:这个项目到底在解决什么问题
先说清楚这个项目的基本盘。龙芯平台这几年在党政办公、工控、嵌入式领域铺量很大,尤其是龙芯K系列(面向嵌入式和工控场景),大量设备需要接入各种外围传感器。MPU6050是个很典型的六轴传感器,自带三轴陀螺仪和三轴加速度计,还带一个DMP运动处理器,在姿态解算、跌倒检测、平衡车、工业振动监测这些场景里到处都是。
驱动移植要解决的事情,说白了就是四层:
- 第一层,硬件层面:I2C总线的电气特性和时序是否符合MPU6050的规格;
- 第二层,内核层面:龙芯内核里有没有对应的I2C控制器驱动,有没有I2C设备驱动框架;
- 第三层,设备描述层面:设备树(DTS)或者板级文件里有没有正确描述这个传感器的挂载位置和参数;
- 第四层,应用层面:驱动加载起来之后,用户态能不能正确读到数据,数据单位、量程是否合理。
我在实际做这个项目的时候,发现很多人一上来就急着编译驱动、写测试程序,结果卡在第三层——设备树没配对,内核根本没把设备枚举出来。所以这篇文章我会花比较多篇幅讲设备树和内核配置,这是最容易出问题也最容易被忽略的部分。
1.2 方案选型:为什么选择“从内核主线移植”路线而不是“写裸驱动”
当时我们面临两条路:一条是参考Mainline内核里已经存在的mpu6050驱动直接移植,另一条是抛开内核框架,自己写一个简单的字符设备驱动直接操作I2C寄存器。
我们最终选了前者,原因有几点,这几点的权衡过程很值得参考:
第一,Mainline的mpu6050驱动已经非常成熟。它基于IIO(Industrial I/O)子系统实现,包含了完整的寄存器初始化序列、自检逻辑、中断处理、FIFO数据读取支持,代码审过一次、社区维护多年,出错的概率远比我们自己写的低。自己写驱动的代价,不光是要重新熟悉MPU6050那一百多个寄存器的含义,还要自己处理设备休眠唤醒、FIFO溢出、陀螺仪和加速度计量程切换这些边角逻辑,调试成本不可控。
第二,IIO子系统提供了一个统一的上层接口。驱动挂接到IIO之后,用户态可以直接通过sysfs节点读数据,也可以通过iio_utils库拿数据,未来如果要从MPU6050换成ICM20602这类同生态传感器,只需要小改驱动或者换设备树,应用层基本不用动。
第三,龙芯内核本身已经包含大量IIO框架代码。因为龙芯也在往工业控制、机器人这些领域靠,内核里I2C、GPIO中断、IIO框架这些基础模块都是全的,我们移植的时候不需要从零去补框架,只需要补芯片驱动本身。
结论就是:能站在内核框架的肩膀上解决问题,就不要自己从地板开始砌。这不是偷懒,是工程效率的合理选择。
2. 开发环境搭建与龙芯平台准备
2.1 交叉编译工具链与环境变量
龙芯K系列用的是LoongArch指令集,跟常见的x86、ARM都不一样,所以不能用gcc直接用,必须用龙芯的交叉编译工具链。一般龙芯官方提供的工具链命名格式是:
loongarch64-linux-gnu-gcc安装完工具链之后,环境变量建议这样配,实测比较稳:
export CROSS_COMPILE=loongarch64-linux-gnu- export ARCH=loongarch这两个变量export之后,以后编译内核、编译驱动模块、编译设备树都会自动带上交叉编译参数,不需要每个Makefile里手动改。
需要注意一个比较隐蔽的坑:龙芯的交叉编译工具链有glibc版本和musl libc版本之分。如果目标系统是BusyBox+musl这种精简rootfs,工具链必须也用musl版本,否则编出来的驱动模块虽然能insmod,但一旦调用到某些依赖libc的函数就会出错,符号找不到,很难排查。我当时就是犯了这个错误,刚开始用的glibc工具链,加载模块时报了一堆undefined symbol,一度以为是内核配置问题。
2.2 模拟环境先行:没有硬件也能验证流程
这个项目其实有很现实的问题——龙芯的开发板不一定随时在手边,尤其是驱动开发初期,频繁烧写固件很费时间。我们在做的时候用了一个很有效的方式:先在一个模拟环境里把整个流程跑通,包括内核编译、驱动编入、设备探测、数据读取,然后再到真机上去复现。
这里说的模拟环境主要是QEMU模拟LoongArch虚拟机,以及龙芯官方提供的模拟器。QEMU下跑LoongArch虚拟机,流程大致是:
- 下载QEMU的loongarch版本,目前主线QEMU已经支持LoongArch的virt机型;
- 准备一个LoongArch的虚拟机镜像,通常用Loongnix或者Buildroot自己构建;
- 把带MPU6050驱动编进内核,启动虚拟机后,用i2c-stub或者模拟I2C控制器的方式验证驱动加载路径。
可能有人会问:模拟环境里没有真实的MPU6050芯片,怎么验证驱动?这里有两个思路:
一是用i2c-stub。它是内核自带的一个I2C设备模拟工具,可以在没有硬件的情况下虚拟出一组I2C适配器,然后注册虚拟设备。虽然它没法模拟MPU6050的寄存器行为,但可以验证驱动是否被正确加载、probe是否被调用、I2C读写流程是否走通。
二是用QEMU的I2C模型配合virtsi设备,直接把MPU6050的寄存器模型用软件实现出来,挂到虚拟I2C总线上。这个工作量比较大,但是对驱动验证来说非常彻底。
模拟环境的最大价值在于:它能把“内核编译错误”“设备树语法错误”“驱动与内核API不匹配”这类问题在几分钟内暴露出来,而不用每次都在真机上烧卡、重启、看串口。我个人的建议是:驱动移植类的项目,一定先去做“编译+加载+SysFS节点检查”这三步的模拟验证,再去碰真机。
2.3 固件与BIOS更新:这一步别跳过
龙芯K系列的板卡在启动过程中会先执行一段固件(类似BIOS/bootloader),初始化内存、总线、外设资源。很多时候我们发现I2C控制器工作不正常,排查到最后发现是固件版本太老,I2C控制器时钟配置有问题,或者设备树里描述的中断控制器和固件实际初始化出来的不匹配。
所以,项目开始之前,建议先把龙芯板卡的BIOS更新到官方发布的最新版本。更新固件本身不算复杂,一般官方会提供烧写工具和固件包。操作步骤大致如下:
- 从官方渠道下载对应板卡的最新固件,注意区分主板型号和CPU型号,固件刷错了大概率变砖;
- 将固件文件放到FAT32格式的U盘里;
- 板卡上电时进入固件更新模式(不同板卡按键不同,有的是按Delete,有的是按F2,看官方手册);
- 在固件更新界面选择U盘里的固件文件,开始升级;
- 升级完成后断电重启,进固件设置菜单确认I2C、GPIO等外设的中断号分配是否正确。
这一步花的时间不长,但能省掉后面一大半“硬件层面莫名其妙”的排查时间。
3. MPU6050驱动移植的完整实操
3.1 内核配置准备:从IIO到I2C的依赖链
MPU6050驱动在Linux内核里属于IIO子系统下的运动传感器类。从内核主线来看,需要打开的核心配置项如下:
CONFIG_IIO=y CONFIG_IIO_BUFFER=y CONFIG_IIO_TRIGGERED_BUFFER=y CONFIG_MPU6050_I2C=y CONFIG_INPUT_MPU6050=y (看内核版本,有的版本用这个)在龙芯平台上,还需要确保I2C控制器驱动是开启的,例如:
CONFIG_I2C=y CONFIG_I2C_LOONGSON=y # 龙芯I2C控制器的驱动,具体名称以内核源码为准如果打算把驱动编成模块动态加载,就用=m而不是=y。我个人在调试阶段更喜欢编成模块,这样方便反复insmod/rmmod,不用频繁重启。
配置方法:
make loongson3_defconfig # 具体defconfig名称看官方内核 make menuconfig在menuconfig中依次进入Device Drivers -> Industrial I/O support -> Inertial measurement units,找到Invensense MPU6050 devices。
一个实操心得:龙芯官方内核可能和Mainline内核有差异,有的官方内核里MPU6050驱动可能没有默认编译,需要手动勾选;有的老内核里驱动路径在Device Drivers -> Staging或sensors目录下,要多翻几下。
3.2 设备树编写的关键细节
设备树是这次移植的重中之重。MPU6050挂载在I2C总线上,所以设备树里要做两件事:一是确认I2C控制器的状态是okay,二是把MPU6050作为一个子节点挂到正确的I2C控制节点下。
一个典型的MPU6050设备树节点长这样:
&i2c0 { status = "okay"; clock-frequency = <400000>; /* 尽量用400kHz,读数据更快 */ mpu6050@68 { compatible = "invensense,mpu6050"; reg = <0x68>; /* MPU6050的I2C地址,默认是0x68 */ interrupt-parent = <&gpio>; /* 具体用哪个中断控制器,看板级设计 */ interrupts = <20 2>; /* GPIO 20,上升沿触发 */ mount-matrix = "0", "1", "0", "-1", "0", "0", "0", "0", "1"; /* 安装矩阵,根据芯片实际摆放方向设置 */ }; };这些属性很多,但真正影响驱动能不能跑起来的关键字段其实就几个:
- reg字段。MPU6050的I2C地址由芯片AD0引脚的接地/接高决定,接地是0x68,接高是0x69。这个必须和实际硬件对上,不然驱动probe阶段就会失败。
- interrupt-parent和interrupts。如果这两个字段不对,中断方式读数据就起不来。如果只是想先读数据,可以在驱动里绕过中断,用轮询方式,但终归不如中断方式高效,后续还是要修。
- mount-matrix。这个很多人会忽略,如果装芯片的时候方向跟设备树里的默认矩阵不一致,读出来的加速度和角速度方向就会是反的或者轴序错乱,姿态解算结果完全不可用。
我之前遇到过一个问题:I2C地址写的是0x68,设备也确实是这个地址,但modprobe之后dmesg里始终报“Failed to read WHO_AM_I”。后来用i2cdetect扫描了一下,发现设备其实在0x69上,硬件工程师看错了AD0引脚的接线。这种问题不看实际硬件,光盯代码永远找不到原因。
设备树写完之后,需要通过编译生成dtb文件:
make dtbs然后有两种方式生效:一种是把dtb替换到启动分区里,另一种是在u-boot或GRUB启动参数里指定新的dtb。龙芯平台通常是后者更灵活,启动时在引导菜单里按提示替换dtb文件。
3.3 驱动编译与模块安装
如果内核已经配置好了,驱动编译非常直接:
make modules make modules_install INSTALL_MOD_PATH=/path/to/rootfs编译出来的ko文件路径一般类似:
drivers/iio/imu/inv_mpu6050/inv-mpu6050-i2c.ko如果只用模块方式加载,手动拷贝也行:
cp drivers/iio/imu/inv_mpu6050/*.ko /path/to/rootfs/lib/modules/$(kernel_version)/kernel/drivers/iio/imu/inv_mpu6050/ depmod -b /path/to/rootfs # 重新生成modules.dep实际加载过程我建议这么做,方便看清问题:
modprobe inv-mpu6050-i2c # 或者 insmod inv-mpu6050-i2c.ko dmesg | tail -50加载成功的标志是dmesg里出现类似这样的信息:
inv-mpu6050 i2c-0:0x68: mounting matrix not found: using identity... inv-mpu6050 i2c-0:0x68: whoam i 0x68 ok如果看到“whoam i”校验失败,基本可以断定I2C通信或者地址不对,按前面说的排查。
3.4 用sysfs和工具验证驱动工作状态
驱动加载成功只是第一步,数据对不对才是关键。IIO框架下的驱动,加载之后可以在/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_anglvel_x_raw, in_anglvel_y_raw, in_anglvel_z_raw:陀螺仪三轴原始值
- in_temp_raw:温度原始值
- in_accel_scale, in_anglvel_scale:原始值换算成物理值的比例系数
- name:设备名称,应该是mpu6050
- lable:设备标签,如果设备树里配了lable字段就会显示
原始值读出来是带符号整数,换算方式很简单:
实际加速度(m/s^2) = in_accel_x_raw * in_accel_scale 实际角速度(度/s) = in_anglvel_x_raw * in_anglvel_scale举个例子,如果in_accel_scale是0.000598,raw值是4096,那么实际加速度是4096 * 0.000598 ≈ 2.45 m/s²。芯片静止平放时,理论上Z轴应该接近9.8 m/s²,X/Y轴接近0,如果偏差很大,就要考虑量程配置或者mount-matrix的问题了。
3.5 静态编译进内核的处理
如果在生产环境,一般不希望依赖模块加载,而是直接把驱动编进内核镜像。此时需要把.config里的CONFIG_MPU6050_I2C改为=y,同时确保IIO相关配置也是=y,然后重新编译内核:
make -j$(nproc) make dtbs编完后把新内核镜像和dtb一起烧写或拷贝到启动分区。此时设备启动后不需要任何modprobe操作,/sys/bus/iio/devices/下直接就会出现设备节点。
这也是我比较推荐的生产环境做法——少一个模块加载步骤,就少一个出错环节,也少一个被恶意替换ko文件的安全风险。
4. 常见问题与调试技巧实录
4.1 I2C探测不到设备:先分清是总线问题还是设备问题
这是整个移植过程中最高频的问题。建议的排查顺序是:
第一步,用i2cdetect扫描I2C总线:
i2cdetect -y 0 # 0是I2C总线编号,龙芯平台可能从1或2开始,看实际情况扫描结果会以矩阵形式显示总线上所有有响应的I2C地址。如果0x68或0x69位置有“UU”或者“68”“69”字样,说明设备在线且内核驱动已经绑定;如果是“--”,说明地址上没探测到设备。
第二步,如果i2cdetect都扫不到设备,大概率是硬件问题:I2C上拉电阻没焊、AD0引脚接法不对、电源没供上、SCL/SDA接反了。此时用示波器量一下SCL/SDA波形最直接。
第三步,如果i2cdetect能看到设备但驱动probe报错,则重点看dmesg里的错误信息。此处需要区分“设备发现失败”和“设备初始化失败”,两者的排查路径完全不同。
4.2 数据全是0或者数据跳变剧烈
如果驱动加载成功、节点也有,但读出来的数据要么全0,要么跳变到非常夸张的数值,通常是两类原因:
一类是量程配置不对。MPU6050加速度计的量程默认是±2g,如果应用里设置成了±16g,而驱动初始化时序没有正确写入量程寄存器,raw读数就会超过正常范围。排查方法是通过IIO node里的in_accel_scale值判断,如果scale值跟预期量程不匹配,说明量程寄存器没写对,需要检查驱动初始化序列。
另一类是接线干扰。I2C线如果太长或者没有上拉电阻,高速传输时数据容易出错,表现就是读出数值随机跳变。这个时候降低I2C时钟频率,比如从400kHz降到100kHz,往往能缓解问题。设备树里clock-frequency改一下重新编dtb就行。
4.3 中断方式不生效,只能轮询
MPU6050的INT引脚可以配置成多种触发方式。设备树里interrupts字段如果配置的是上升沿,但芯片实际输出的是低电平有效,驱动就会一直触发不了中断。
排查方法是在设备树里先删掉中断配置,让驱动回退到轮询模式。如果轮询模式下数据正常,就基本可以断定是中断触发方式不匹配。然后查阅MPU6050的INT_PIN_CFG寄存器(地址0x37)的配置,确认ACTL位设置,修改设备树中断类型或者驱动初始化代码里的INT配置。
我在实际项目中遇到过非常隐蔽的一个问题:MPU6050的中断输出是开漏结构,需要外部上拉才能正常工作。龙芯的GPIO内部上拉如果没使能,INT引脚永远是低电平,驱动和中断自然全部失效。查了一下午,最后发现就是少配了一个GPIO上拉属性。
4.4 内核API版本不匹配,编译报错
龙芯官方内核可能不会跟Mainline最新版完全同步。MPU6050驱动如果从最新内核拷贝到老内核上,很可能遇到一些API变更导致编译不过。常见的坑有:
- i2c_driver结构体里的probe函数签名变化:新版用
.probe_new,老版用.probe; - device_property_read_u32系列函数的引入版本;
- devm_iio_device_register和iio_device_register的差异。
解决办法很简单:优先使用龙芯官方内核源码树里自带的MPU6050驱动版本,不要盲目从最新Mainline拷贝过来用。如果必须使用新驱动,就需要对照官方内核的API差异做适配。这块没有捷径,多读内核代码、多查内核版本log。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查/解决办法 |
|---|---|---|
| i2cdetect扫不到设备 | 硬件接线、AD0地址错、供电 | 示波器量波形、检查AD0电平 |
| i2cdetect有设备,probe报WHO_AM_I失败 | I2C地址与设备树reg不一致 | 用i2cdetect确认实际地址,修改设备树 |
| 驱动加载成功,数据全为0 | 量程写错、传感器处于睡眠模式 | 检查PWR_MGMT_1寄存器,重新初始化 |
| 数据跳变剧烈 | 接线干扰、时钟过快 | 降低I2C时钟到100kHz,检查上拉 |
| 中断触发不了 | 触发方式不对、开漏无上拉 | 查INT_PIN_CFG,配置GPIO上拉 |
| 编译报probe签名错误 | 内核API版本不匹配 | 用官方内核自带驱动或适配API |
5. 写在最后的一点经验
这个项目做完之后,我最大的体会是:驱动移植这件事,看似是写代码、改配置,实际上拼的是“硬件认知+内核框架理解+耐心排查”三样东西。做好一个底层驱动移植,三分靠写代码,七分靠排查,而排查最忌讳的就是瞎试。
给准备入手的读者两个最实在的建议:第一,模拟环境一定要用起来,先让编译、设备树、加载路径都走通再上真机,能帮你省掉大量测试时间;第二,遇到问题时先把“设备是否在线、地址是否正确、中断配置是否匹配”这三个基本盘查清楚,再去怀疑驱动本身。
最后再分享一个细节:MPU6050芯片有个自检功能(self-test),驱动加载成功并读到数据之后,建议把传感器静止放置,读一组静态数据,确认加速度计Z轴接近重力加速度、陀螺仪三轴接近0,再开始做动态姿态解算。这个小小的验证习惯,能帮你把“驱动通了但数据方向反了”这类后期问题提前暴露出来。