龙芯K平台MPU6050驱动移植实战:设备树与IIO适配 团队里给项目起名的同事大概也没想到这个“走马观碑组MPU驱动移植”的任务最后会变成我近期花时间最多的一件小事。事情本身的起因很简单手头到了一块龙芯K平台开发板想在上面接一颗MPU6050六轴传感器做姿态数据采集。MPU6050这颗芯片不稀奇Linux内核里也早就有了现成驱动但当真要把它在龙芯K的Linux环境里跑起来中间隔着的不是代码而是一堆平台差异、设备树和固件细节。这里的MPU指的不是微处理器概念里的MPU而是InvenSense现在属于TDK的MPU6050一颗经典的六轴惯性传感器三轴加速度加三轴陀螺仪I2C接口功耗低、价格便宜是很多姿态测量和动作识别项目的入门首选。整个移植工作可以概括成一句话把Linux内核里为MPU6050写的IIO驱动在龙芯K平台上完成编译、适配和验证让传感器数据能稳定、正确地被系统读取。这篇文章适合正在做龙芯平台外设适配的嵌入式工程师、要参加相关比赛的学生团队以及对Linux内核IIO子系统感兴趣的读者。整篇记录是实操流水账加踩坑心得不会只罗列命令关键步骤都会解释为什么这么做。1. 项目到底要做什么走马观碑组的一次MPU驱动移植选型1.1 龙芯K平台和一颗小传感器的组合龙芯K系列是龙芯在桌面和嵌入式方向的一条产品线基于LoongArch指令集架构。和常见的x86、ARM平台相比它的软件生态还在快速完善中很多外设驱动在主线Linux里已经能跑但不同板卡之间的差异仍然存在。我们手头这块龙芯K开发板跑Linux 5.15内核板上引出了多路I2C、SPI、UART等常用接口理论上看接一颗I2C接口的MPU6050是标准操作。但“理论上看”和“实际能用”之间隔着一座山。MPU6050的Linux驱动虽然在内核里但驱动被编译进内核、被设备树匹配、被I2C控制器正确枚举是一连串相互依赖的动作。在x86平台或者成熟的ARM开发板上这些链路大多被厂商调好了换到龙芯平台每一步都可能掉链子。尤其是LoongArch的内核分支、板级设备树、片上I2C控制器的差异会把一个看似简单的“接个传感器”变成一次小型落地项目。这次项目的最终目标很明确让MPU6050的数据出现在龙芯K平台的/sys/bus/iio/devices/目录下可以通过标准IIO接口读到加速度和角速度原始值并且长时间运行不报错。不是从零写一个驱动而是把内核里现成的驱动“移植”到目标平台并跑通。1.2 技术路线为什么选“内核主线驱动加设备树适配”动手之前团队内部其实评估过三条技术路线。第一条是用户态直读I2C就是利用Linux的i2c-dev接口写一个用户态程序通过/dev/i2c-N直接读写MPU6050寄存器。这条路线最简单几行代码就能读WHO_AM_I但缺点也明显占着总线不放、没有中断支持、没有缓存机制数据实时性和通用性都差而且完全绕开了内核的驱动模型属于“能用但不好用”。第二条是把厂商提供的MPU6050驱动源码交叉编译成内核模块。很多传感器厂商会提供linux驱动源码包但这些源码往往只针对某个特定内核版本拿到新平台后需要改不少接口。而且这类驱动质量参差不齐有的还带着私有API后续维护成本很高。第三条就是这次实际采用的方案复用内核主线drivers/iio/imu/inv_mpu6050驱动通过设备树节点把I2C总线和传感器匹配起来针对龙芯平台做配置和必要的小改动。选这条路的原因很直接主线驱动由内核社区长期维护IIO框架也更现代上层应用好对接。对一颗2012年就发布的传感器来说主线驱动的成熟度远超大多数厂商驱动。方案开发量稳定性可维护性适用场景用户态i2c-dev直读最小一般差快速验证硬件链路厂商驱动源码移植中等取决于源码质量中等主线无驱动的传感器主线驱动设备树适配小高好主线已有支持的传感器如果你手里的传感器在主线IIO子系统里本来就有驱动别犹豫直接走第三条路线。很多人在“自己写一个驱动”和“改动现有驱动”之间选择了前者结果把大量时间花在了重复造轮子上。1.3 怎么才算移植成功没有验收标准的移植都是耍流氓。这次项目在动手前就列了一个五条验收清单后面所有工作都对着这个清单推进i2cdetect能扫到0x68地址的设备节点说明I2C链路正常。驱动probe成功dmesg里能看到inv-mpu6050相关的芯片ID信息。/sys/bus/iio/devices/iio:device0下能读到in_accel_*和in_gyro_*属性。采样频率可以配置并能通过FIFO或直接读取方式连续拿到数据。板卡连续运行两小时以上无I2C超时、无内核错误、数据无异常跳变。第五条看上去简单但恰恰是最容易暴露问题的。比如驱动默认配置的采样率偏高时如果I2C控制器时钟配置不对跑一会就会出现FIFO溢出数据开始丢帧。所以验收标准里一定要包含稳定性测试不能只读到一个数据就算完事。2. 动手之前的四项准备龙芯K平台环境与内核源码梳理2.1 先摸清楚板卡硬件拓扑很多移植失败的问题根子不在驱动代码上而在硬件连接没搞清楚。拿到板卡第一步不是插电开整而是找原理图和用户手册确认三个问题传感器接到了哪个I2C控制器中断脚接到了哪个GPIO以及传感器供电电压是多少。我们这块板卡的I2C2控制器把SDA和SCL引到了扩展排针上传感器模块的VDD接3.3VAD0引脚悬空因此I2C地址是0x68。如果你用的模块AD0接了高电平地址就会变成0x69这是排查时特别容易忽略的点。有条件的话拿万用表量一下SDA、SCL上有没有上拉电阻很多便宜的MPU6050模块板上自带上拉焊接在开发板上的版本则要看板子设计。I2C总线没有上拉或者上拉电阻过大都会导致通信不稳定但又不至于完全不通这种“薛定谔的设备”最耽误时间。另外留意中断脚。MPU6050的INT脚是开漏输出如果你要使用数据就绪中断得在设备树里配置对GPIO。但如果第一阶段只做数据轮询可以先不接中断脚等基本数据通路通了再补。我们这次先用了轮询模式后面才加中断逐步增加复杂度排查起来会舒服很多。2.2 交叉编译器与内核源码准备龙芯K平台属于LoongArch架构不能用x86的gcc直接编译内核需要准备loongarch64交叉工具链。龙芯官网提供了预编译的工具链解压后把bin目录加入PATH即可。内核源码版本尽量与板卡厂商提供的一致如果板子跑的是5.15就用主线5.15版本如果厂商在内核里打了一些板级补丁最好直接用厂商发布的内核源码树否则后面容易出现设备树节点对不上的情况。配置内核前先检查一下编译器版本是否满足内核要求。LoongArch架构对gcc版本有一定要求太老的编译器会直接编译失败报一些莫名其妙的错误。建议先执行一次简单的编译验证比如编译kernel/sysctl.o确认工具链没问题再开始配置工作。export ARCHloongarch export CROSS_COMPILEloongarch64-linux-gnu- make loongson3_defconfig如果是厂商提供的BSP内核配置目标名称可能不同比如ls2k_defconfig之类以厂商文档为准。生成.config之后再用menuconfig补齐后面要用的配置项。2.3 先用龙芯模拟环境把内核跑起来在真机上反复刷机调试很费时间尤其是一开始内核配置还可能缺东西。我们这阶段用了龙芯平台常用的QEMU模拟环境用qemu-system-loongarch64配合一个精简根文件系统把刚编译出来的内核先跑一遍确认能够正常启动到shell。这一步的核心价值是前置验证把“内核能不能启动”和“模块能不能加载”这两个问题从每次几分钟的刷机循环里解放出来。模拟环境里虽然不会真的有MPU6050芯片但可以通过观察内核启动日志确认I2C控制器、GPIO控制器、IIO子系统这些关键选项是否已经被编译进内核。qemu-system-loongarch64 -M virt -m 1G -kernel vmlinux -drive filerootfs.ext4,formatraw -append root/dev/vda consolettyS0 -nographic启动后能看到内核日志正常滚动进入shell后手动加载一个测试模块确认模块加载链路没问题说明基础内核配置OK。当然模拟环境和真实硬件的驱动路径仍有差异I2C外设级验证最终还是要回到真机。但先跑模拟环境能帮你把“软件问题”和“硬件问题”这两类问题在前面切分开。2.4 确认I2C总线编号和设备在线状态等到真机就绪后第一件事是确认I2C总线编号。龙芯K平台可能有多个I2C控制器板卡文档里说的I2C2在Linux里可能编号为i2c-1或i2c-2不能想当然。用i2cdetect -l列出所有总线再逐个扫描。i2cdetect -l i2cdetect -y 2如果扫描结果里没有0x68先从硬件查起传感器是否上电、SDA/SCL是否接反、地址是否是0x69、总线上是否有其他设备占用了同一地址。我曾经遇到一次扫描不到设备最后发现是杜邦线接触不良重新插拔后就好了。硬件问题优先排查不要在软件配置上反复折腾。确认总线能扫到设备后可以用i2c-tools里的i2cget直接读WHO_AM_I寄存器地址0x75如果返回0x68说明I2C通信完全正常可以进入驱动移植阶段。i2cget -y 2 0x68 0x75如果这里能读到正确ID后面所有驱动问题都聚焦在“软件匹配”上问题范围瞬间缩小很多。3. 核心实现MPU6050驱动在龙芯Linux内核里的移植步骤3.1 先看懂inv_mpu6050驱动框架Linux主线里MPU6050的驱动位于drivers/iio/imu/inv_mpu6050/目录下核心文件包括inv_mpu6050_core.c、inv_mpu6050_i2c.c、inv_mpu6050_ring.c、inv_mpu6050_buffer.c等。大致分工是core文件处理传感器初始化和IIO设备注册ring和buffer处理数据缓冲与触发采样i2c文件负责与I2C控制器打交道。这个驱动本质上是一个I2C驱动通过i2c_driver结构体向内核注册。它既支持通过i2c_device_id匹配设备树节点也支持通过of_match_table匹配compatible字符串。MPU6050的compatible字符串是invensense,mpu6050设备树节点只要写好这个compatible并挂在正确的I2C总线下驱动就能被调用到probe函数。用IIO框架的好处是驱动注册完成后用户态不需要调用read()去读一个字符设备而是通过/sys/bus/iio/devices/iio:device0/目录下的各种属性文件直接读取数据。每个属性都有标准命名比如in_accel_x_raw代表X轴加速度计的原始值in_gyro_z_raw代表Z轴陀螺仪的原始值。上层应用可以完全不关心寄存器地址只跟属性打交道。3.2 设备树节点的写法与注意点设备树是整个移植过程中最容易出错也最关键的部分。节点写法看起来很简单但隐含细节很多。下面是我们最终使用的设备树片段挂在i2c2控制器下i2c2 { status okay; clock-frequency 400000; mpu605068 { compatible invensense,mpu6050; reg 0x68; interrupt-parent gpio0; interrupts 5 IRQ_TYPE_EDGE_RISING; mount-matrix 0, 1, 0, -1, 0, 0, 0, 0, 1; }; };时钟频率这里有一个常见误区。MPU6050在数据手册里标注的最大I2C时钟是400kHz也就是Fast Mode所以clock-frequency 400000从芯片角度没问题。但实际能不能跑400k取决于龙芯平台I2C控制器的实际时钟分频以及总线上电容、上拉电阻是否满足要求。如果高速模式不稳定日志里出现I2C NACK或超时降到100kHz试试往往能解决。interrupt-parent和interrupts要看板级设备树里GPIO控制器的实际标签。我们板卡上接到了gpio0的第5脚所以interrupt-parent gpio0interrupts 5 IRQ_TYPE_EDGE_RISING。如果你的板卡GPIO控制器标签不同直接照抄会编译报错或者probe时中断申请失败一定要去平台dtsi里确认节点名。mount-matrix是传感器的安装方向矩阵。如果你的传感器水平放置且X轴朝前可以不写或者用单位矩阵。我们板卡安装后坐标系和默认方向不一样所以用了上面的旋转矩阵。可以把mount-matrix理解成“传感器坐标系转换到设备坐标系”的旋转矩阵数字格式是三行三列的字符串数组。一开始不确定方向时先不写等读出实际数据再改。3.3 内核配置项一个都不能少设备树写好后要确认内核配置打开了相关子系统的支持。MPU6050驱动依赖IIO框架和IIO触发缓冲区少了任何一个驱动都能编译但运行时不是找不到设备就是没有buffer节点。Device Drivers - Industrial I/O support - Industrial I/O core Enable buffer support within IIO Enable triggered buffer support within IIO Accelerometers - InvenSense MPU6050 devices InvenSense MPU6050 I2C driver Gyroscopes - InvenSense MPU6050 devices不同内核版本的菜单布局略有差异。5.15版本里MPU6050的加速度和陀螺仪功能放在同一个驱动目录下勾选InvenSense MPU6050 I2C driver即可。除了IIO本身还需要确保CONFIG_I2C、CONFIG_OFOpen Firmware设备树支持、CONFIG_GPIOLIB以及对应GPIO中断控制器配置打开。CONFIG_IIOy CONFIG_IIO_BUFFERy CONFIG_IIO_TRIGGERED_BUFFERy CONFIG_INV_MPU6050_I2Cm CONFIG_INV_MPU6050_IIOy我当时踩过一个坑CONFIG_INV_MPU6050_I2C编成了模块但rootfs里没有/lib/modules/对应目录导致modprobe直接失败。如果只是为了快速验证初期可以直接编进内核y省去模块拷贝这一步。等确认功能正常后再改为模块便于后续迭代。3.4 代码层适配什么时候需要动源码我们这次比较顺利inv_mpu6050驱动在5.15内核里可以直接编译通过没有改一行代码。但如果你用的内核版本比较老或比较新可能会遇到API差异。比如老内核里regmap_read_bypassed这个函数在某个版本后改了签名或者I2C的struct i2c_client字段有变动。这类问题排查思路是编译报错后先别急着猜去对应内核版本的头文件和官方git日志里查这个函数的定义变化。比如报错信息提示inv_mpu6050_i2c.c里某行调用了regmap_read_bypassed但在你的内核头文件里找不到这个函数就去drivers/base/regmap/regmap.c里搜索当前版本对应的函数名。常见替代方案可能是regmap_read和带locking的版本。如果改动不复杂可以直接在驱动源码里做条件编译适配#if LINUX_VERSION_CODE KERNEL_VERSION(5, 10, 0) ret regmap_read_bypassed(regmap, reg, val); #else ret regmap_read(regmap, reg, val); #endif但我的建议是如果驱动源码需要改的地方超过三处先停下来评估一下是不是内核版本差太远。找罪受不如换一个与平台内核版本相近的驱动版本。3.5 编译、部署与验证的完整步骤设备树和内核配置搞定后进入编译部署环节。如果驱动编成模块只需要编译模块部分拷到板子上加载。建议编译模块时同时把设备树编一遍确保dts语法没问题。make ARCHloongarch CROSS_COMPILEloongarch64-linux-gnu- dtbs make ARCHloongarch CROSS_COMPILEloongarch64-linux-gnu- Mdrivers/iio/imu/inv_mpu6050 modules编译产物中会有一个inv-mpu6050-i2c.ko文件这是I2C接口版本的驱动模块。拷到板子后先看内核是否已经识别到设备再加载模块。scp drivers/iio/imu/inv_mpu6050/inv-mpu6050-i2c.ko rootboard_ip:/tmp/ ssh rootboard_ip insmod /tmp/inv-mpu6050-i2c.ko dmesg | tail -20如果一切正常dmesg里会出现类似下面的日志inv-mpu6050 i2c-2:8: Found Invensense MPU6050 chip iio iio:device0: setup the trigger successfully此时再查看IIO设备目录ls /sys/bus/iio/devices/ cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw cat /sys/bus/iio/devices/iio:device0/in_gyro_y_raw连读几十次数值应该在零附近有微小抖动而不是固定值或跳变到离谱的大数。如果数值正常说明整条数据通路已经打通。更专业的验证方法是用内核源码自带的小工具iio_generic_buffer可以配置采样次数、采样频率并把数据存成文件。cd tools/iio make ./iio_generic_buffer -a -n iio:device0 -c 100这个工具会打印设备类型、采样频率、可用通道等信息并连续采样100个点。它能直观看到FIFO有没有溢出、每次采样的时间戳是否正常。我第一次跑的时候打印的采样频率明显偏高数据时间戳跳动后来在设备树里把clock-frequency从400k降到100k才恢复正常非常典型的平台差异问题。4. 实测排障龙芯平台MPU6050移植中遇到的坑与修复方法4.1 一张速查表先解决八成问题整个调试过程中遇到的问题可以整理成一张速查表。如果你在移植中卡住先对照这张表排查大概率能省一晚上的时间。现象可能原因排查手段解决方向i2cdetect扫描不到设备接线问题、地址错误、模块未上电万用表测量、检查AD0电平重新接插线、配置地址为0x69扫描到设备但驱动probe失败dts compatible不匹配dmesg查看驱动匹配过程检查设备树字符串拼写WHO_AM_I读出来不是0x68I2C通信受干扰、上拉弱降低clock-frequency频率从400k降到100k读到加速度全为固定大数传感器供电不稳或初始化失败读scale属性、检查PWR_MGMT_1重新上电、复位传感器FIFO溢出或采样时间戳异常采样频率过高、I2C时序问题降低采样频率检查in_*_sampling_frequency中断不触发或申请失败GPIO复用配置不对检查gpiochip、pinctrl修改pinctrl和interrupts配置编译驱动报错找不到函数内核版本API差异搜索当前内核对应函数修改驱动或换驱动版本这张表里最常见的一类问题就是设备树匹配相关的。dmesg里如果出现“inv-mpu6050: probe of 2-0068 failed with error -22”基本可以确定是设备树参数有问题。error -22对应EINVAL通常是某个属性值不合法比如reg写错格式、interrupts缺少flags。4.2 三个印象最深的难题第一个难题是I2C总线编号对不上。设备树里把传感器挂在i2c2下但实际扫描后发现根本扫不到设备。捣鼓了半小时发现龙芯K平台设备树里的i2c2在Linux运行时枚举出来的总线号不是2而是4。因为dtsi里还有多个I2C控制器有些status是disabled但编号顺序并不连续。后来我改用i2cdetect -l先列出所有总线再逐个扫描最终在i2c-4上找到了设备设备树节点名称不改但代码里要根据实际枚举结果调试。第二个难题是中断引脚的GPIO复用。设备树里配好了interrupts但模块probe时申请中断一直失败。排查后发现板卡上这颗GPIO默认被复用成了其他功能需要在dts里配置pinctrl将引脚切换为GPIO模式。这类问题在带内部复用功能的SoC上极其常见尤其是龙芯K这样外设控制器较多的芯片。解决方案是在传感器节点里添加pinctrl属性在board级dts里定义对应的引脚配置pinctrl-0 gpio0_5_pin; pinctrl-names default;具体引脚标签要以板卡dts的pinctrl头文件为准。如果你不确定先不配interrupt属性改用轮询模式数据照样能读只是不能用数据就绪中断。第三个难题来自一块老批次的板卡。I2C通信在跑采样时总出现随机超时一开始怀疑是驱动或者传感器损坏后来查看板级固件changelog发现旧版固件对I2C控制器的时钟配置描述不完整官方更新固件后这个问题就不存在了。从那以后我每拿到一块新板子第一件事就是记录固件版本号并确认官方是否有已知问题说明。在嵌入式平台调试外设很多莫名其妙的硬件问题和驱动没关系更新板级固件往往比改代码更有效。4.3 数据校准和方向验证的粗浅经验驱动能出数据只是第一步数据准不准是另一回事。由于MPU6050是MEMS传感器芯片出厂时有零偏而且不可能完全理想地安装在电路板上所以读取原始值后必须结合scale属性和零偏校准。加速度计的scale属性在/sys/bus/iio/devices/iio:device0/in_accel_scale里陀螺仪对应in_gyro_scale。物理值等于raw值乘以scale。比如in_accel_x_raw读出来是1024in_accel_scale是0.000598那么实际加速度大约是0.612g。校准通常分两步第一步是零偏校准把设备静止放置在水平面上连续采集1000个样本分别求X、Y、Z三轴的均值。理论上水平静止时加速度计Z轴应该约等于1gX和Y接近0陀螺仪三轴都接近0。实测均值与理论值的偏差就是该轴的零偏。第二步是尺度校准一般以重力加速度1g作为参考把Z轴读数调整为标准值计算出scale修正系数。不过大多数应用直接用内核默认scale就够了真正的关键是方向校准。如果旋转板卡后加速度读数的正负方向和实际运动方向相反就需要mount-matrix或者在上层应用里做坐标变换。方向验证的方法很朴素把板卡分别沿X轴、Y轴、Z轴旋转90度观察对应轴的读数变化。X轴朝上时in_accel_x_raw应该约等于1g朝下则约等于-1g以此类推。我们当时发现X轴和Y轴数据和实际方向交换了于是通过mount-matrix做了一个90度旋转校准。4.4 模拟环境到真机的差异处理模拟环境里验证不了真实的I2C设备这是模拟器天生的局限。但不能因此否定模拟环境的价值。我把模拟环境用来验证三件事内核能否启动、IIO相关的模块能否加载、设备树编译后的二进制能否被正确解析。这三件事在模拟环境里跑通能大幅缩短真机调试周期。在真机联调时我会先用用户态程序直读寄存器确认I2C链路没问题再加载内核驱动。这个顺序很重要它能把问题分成“硬件链路问题”和“驱动匹配问题”两层。如果用户态i2cget都读不到WHO_AM_I就不用去查驱动老老实实回去查接线和上拉电阻。5. 移植完成之后一点心得与可以继续折腾的方向5.1 这次移植沉淀下来几条不好听但很实在的经验移植工作做得越多越觉得“移植”这个动作的本质不是写代码而是做一次对接口差异和硬件行为的梳理。你要搞清楚目标平台的设备树怎么写I2C控制器怎么枚举GPIO中断怎么映射然后把这些信息汇总成内核驱动要求的样子。遇到问题先看dmesg再看数据手册最后查源码这个顺序能少走很多弯路。dmesg会直接告诉你驱动卡在哪一步比如是probe阶段就失败还是读取寄存器时超时指向性完全不同。而数据手册的作用是在你怀疑“内核驱动是不是写错了”的时候能靠寄存器定义来判断真正的问题在哪。大多数情况下内核主线驱动不会错错的是平台配置和硬件连接。给后来者最实在的建议是记录要同步做。每次改动设备树或配置后把dmesg的关键输出、I2C扫描结果、实测数据都存下来。上次排一个GPIO复用问题就是翻出了两小时前的配置记录才意识到自己改过pinctrl省下了重复排查的时间。5.2 驱动跑通之后还有几件值得折腾的事如果你移植的驱动已经稳定读取数据下一步有几个方向很值得尝试。第一个是配置数据就绪中断。当前用的轮询方式虽然简单但CPU占用高、实时性差。把INT引脚接到指定GPIO通过IIO触发系统实现中断驱动采样可以让CPU在无数据时进入休眠数据来了再唤醒。MPU6050驱动本身支持这功能关键是把设备树里的interrupts配置准确。第二个是启用FIFO。MPU6050内部有1024字节FIFO可以缓存多组采样数据适合低功耗场景MCU不需要每次采样都唤醒总线。驱动里对应的就是inv_mpu6050_buffer相关代码通过IIO buffer接口可以读到FIFO里的数据。第三个是玩DMP。MPU6050内置数字运动处理器可以输出四元数把姿态解算的负载从CPU搬到传感器内部。不过主线驱动的DMP支持不算完整想用DMP的话需要结合厂商的相关代码或研究社区补丁。如果你只是偶尔读一下原始数据DMP可有可无但如果要做惯性导航或体感交互它就是真正的加分项。5.3 最后分享一个调试小习惯调试I2C外设时我习惯准备一根杜邦线和一个逻辑分析仪。逻辑分析仪不需要多贵便宜的那种能看到I2C波形就够用。当软件层层配置都查不出问题的时候用逻辑分析仪抓一下SDA和SCL波形能瞬间看出是总线根本没通信、ACK位超时还是地址错误。很多“玄学”问题抓完波形就变成“显然”问题了。这次移植虽然只是龙芯平台上一个小传感器的适配但整个过程把I2C控制器、设备树、IIO驱动、GPIO中断、固件更新这些嵌入式开发的常见内容都串了一遍。对组里的新人来说这比单纯看文档学内核驱动有意思得多也扎实得多。如果你手头正好有龙芯板子和一颗MPU6050按照上面的流程走一遍大概率也能在半天内跑通祝顺利。