ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

IMX6ULL上移植ICM20608驱动:基于Linux IIO子系统的完整调试指南

IMX6ULL上移植ICM20608驱动:基于Linux IIO子系统的完整调试指南 板子刚拿到手的时候我其实没打算用现成的裸机例程去调ICM20608。100ask_imx6ull跑的是完整Linux系统芯片又是Cortex-A7内核这种硬件上写裸机驱动属于自己给自己找麻烦。内核里明明躺着成熟的IIO子系统还有一套几乎可以白嫖的MPU6050系列驱动ICM20608的寄存器布局又和MPU6050高度相似合理做法是把这套驱动“移植”到当前设备树和内核配置下让Linux替我们把传感器管起来。这篇文章就是这个过程的完整调试笔记。从IIO驱动选型、设备树节点怎么写到编译模块、加载报错、数据校准每个步骤我都会把原理和踩坑点拆开讲。适合正在玩100ask_imx6ull、正打算接加速度计/陀螺仪的嵌入式Linux爱好者尤其建议那些曾经在“为什么我已经配置了设备树却还是读不到数据”这个问题上卡住的人读。1. 移植前先想清楚ICM20608和IMX6ULL到底各在做什么1.1 ICM20608的数据能力和本质ICM20608是InvenSense出的一颗6轴惯性测量单元内部集成了三轴加速度计和三轴陀螺仪输出是16位数字量自带温度传感器。加速度计量程可以配置为±2g、±4g、±8g、±16g陀螺仪量程可以配置为±250、±500、±1000、±2000 dps。从硬件接口上看它支持I2C和SPI两种方式I2C最高到400kHzSPI可以跑到8MHz以上。默认上电时它处于睡眠模式这一点是驱动调试里面最常见的坑后面我会专门说。这颗芯片在寄存器层面和MPU6050兼容度很高WHO_AM_I寄存器地址是0x75ICM20608读取它应该返回0xAF不同量程对应的灵敏度系数也和MPU6050一致比如±2g下加速度计是16384 LSB/g±250dps下陀螺仪是131 LSB/(dps)。这种兼容性就是“移植”而不是“从零写”的根本原因。1.2 IMX6ULL为什么直接套Linux IIO而非裸机驱动i.MX6ULL是一颗单核ARM Cortex-A7处理器主频最高528MHz带浮点单元板上跑Linux毫无压力。既然跑Linux最合理的做法是复用内核里现成的IIOIndustrial I/O驱动框架。IIO是Linux专门给传感器类设备准备的子系统既管理设备树和驱动匹配又向用户空间暴露统一接口比如/sys/bus/iio/devices/iio:device0/下面的in_accel_x_raw、in_gyro_z_raw这些节点。这对应用层非常友好不管是写姿态解算程序还是采集数据做分析直接读文件就行不用关心底层的I2C读写、寄存器地址等琐碎细节。很多新手一想到传感器第一反应是去查芯片手册然后自己用I2C控制器寄存器写数据读取。这个方向在单片机上没有问题但在IMX6ULL这种能跑完整Linux的平台上效率太低了。我们应该做的是把内核驱动“接”到正确的总线上而不是重新发明轮子。2. 内核准备版本、配置文件与驱动源码定位2.1 我采用的内核版本和交叉编译链100ask_imx6ull官方BSP里一般带的是Linux 4.1.15内核这个版本对IMX6ULL的各个外设支持已经很齐全。我本身没有从官方下载新内核用的就是板卡配套源码里的内核只是在它基础上改配置、改设备树。交叉编译链我用的arm-linux-gnueabihf-gcc7.x版本即可。实际操作时把工具链路径加入PATH并且在内核源码根目录下固定导出两个关键变量export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf-如果使用的是官方SDK自带脚本比如build.sh一般已经把这俩变量写好了。重点提醒内核对编译器版本、glibc版本并不是特别敏感但架构必须对ARCHarm不能写成ARCHarm64也别用x86工具链干ARM的活。2.2 menuconfig里开启IIO和MPU系列驱动4.1.15内核里已经存在drivers/iio/imu/inv_mpu6050/目录里面不但有MPU6050还写了MPU6500和ICM20608的支持。现在我们需要把它编译成模块。在源码根目录执行make distclean make imx6ull_14x14_evk_defconfig make menuconfig然后依次进入以下菜单勾选配置Device Drivers→Industrial I/O support→ 勾选Industrial I/O support在IIO子菜单中勾选Enable buffer support within IIO后面做触发采集时要用继续进入IMU devices→Invensense MPU6050 devices→ 这里应该能看到针对MPU6050/6500/ICM20608的相关驱动选取成M即编译为内核模块如果你手头的内核源码比较老IMU devices菜单下的字符串里没有ICM20608字样也不要慌核心驱动代码里可能有但没有被menuconfig文案全部体现。这时候可以直接修改drivers/iio/imu/inv_mpu6050/Kconfig并在对应Makefile里确认obj-$(CONFIG_INV_MPU6050_I2C) inv-mpu6050-i2c.o这类行存在。最后即使编译出来的模块文件名和菜单名不完全一致也不影响加载。配置完成后保存退出先跑一遍make modules或直接make zImage。如果是只改设备树可以只编make dtbs如果是改了驱动配置建议完整编一遍保险起见我通常一口气编完内核和模块。3. 设备树节点与引脚复用让内核知道传感器挂在哪个I2C上3.1 从imx6ull.dtsi到板级dts的生效链路设备树描述硬件拓扑。IMX6ULL的I2C控制器在arch/arm/boot/dts/imx6ull.dtsi中已经定义好了比如i2c1节点里包含compatible fsl,imx6ul-i2c, fsl,imx21-i2c默认status可能是disabled。板级设备树例如imx6ull-14x14-evk.dts通过i2c1这种引用方式打开并补充引脚配置最终生效的是这两级叠加的结果。我在100ask板子上做移植时就把ICM20608挂在i2c1总线上。在板级设备树中添加这样一个节点i2c1 { clock-frequency 100000; pinctrl-names default; pinctrl-0 pinctrl_i2c1; status okay; icm20608: icm2060868 { compatible invensense,icm20608; reg 0x68; interrupt-parent gpio5; interrupts 3 IRQ_TYPE_LEVEL_LOW; /* 如果你的模块没接INT引脚或者INT接到了其他GPIO需要对应修改 */ }; };补充对应的pinctrl_i2c1引脚配置pinctrl_i2c1: i2c1grp { fsl,pins MX6UL_PAD_CSI_MCLK__I2C1_SCL 0x4001b8b0 MX6UL_PAD_CSI_PIXCLK__I2C1_SDA 0x4001b8b0 ; };这里的0x4001b8b0不是随便抄的。它包含IOMUXC的配置值高比特位是PAD属性的选择比如上下拉、施密特触发、驱动强度、速度等级等。不同板子、不同走线长度、不同上拉电阻的值都可能不同最稳妥的方式是参考你BSP里其他I2C节点的配置值再结合硬件原理图微调。如果设备树里不写clock-frequency驱动会使用默认值。ICM20608最高支持400kHz但刚上手时建议用100kHz降低硬件走线和上拉带来的信号完整性问题。等数据读取稳定后再提高这是一个比较保守但有效的调试顺序。3.2 引脚复用最容易出的问题IMX6ULL的I2C1功能并不只在固定一组引脚上。比如CSI_MCLK可以复用为I2C1_SCLCSI_PIXCLK可以复用为I2C1_SDA也有的板子会把UART4的引脚复用成I2C。如果你在设备树里写的引脚实际上是另一块板子、另一张原理图上的复用关系那I2C总线波形会出现但要么探测不到设备要么频繁出错。我的排查思路是先在原理图上找ICM20608模块的SCL和SDA究竟连到IMX6ULL的哪个PAD然后去arch/arm/boot/dts/imx6ull-pinfunc.h里搜索对应的MX6UL_PAD_xxx__I2C1_xxx宏把它写进pinctrl节点。如果原理图上连的是GPIO1_IO03这种普通GPIO而且硬件上又想让I2C通过GPIO模拟那设备树配置方式就完全不一样了最好不要用这种方式而是改接真正的I2C控制器引脚。另外引脚被复用为I2C后不能再被系统中其他设备用作GPIO。确认方式很简单打开你内核配置前生成的.config或正在生效的设备树搜索是否还有pinctrl_xxx也使用了同一个MX6UL_PAD_CSI_MCLK。如果有冲突I2C控制器可能会在request时直接报错。4. 编译、落盘、加载模块第一次看到设备节点4.1 编译DTB和模块设备树改动后执行make dtbs生成新的.dtb。如果板上U-Boot是从SD卡FAT分区读取dtb那么把生成的imx6ull-14x14-evk.dtb或者是100ask对应的那个dtb文件复制到SD卡第一个分区替换原文件即可。这里要注意编译出的dtb文件名必须和U-Boot环境变量里配置的fdt_file完全一致否则要么加载了旧dtb要么直接启动失败。启动失败时U-Boot会打印“Failed to load”之类的日志这时候回到U-Boot命令行用printenv fdt_file核对就好。驱动模块编译后产物通常是两个.ko文件一个核心库一个I2C接口层。把这两个文件拷到板子的/root目录例如cp drivers/iio/imu/inv_mpu6050/inv-mpu6050.ko /path/to/tftp/root/ cp drivers/iio/imu/inv_mpu6050/inv-mpu6050-i2c.ko /path/to/tftp/root/如果板子连了网络也可以直接scp过去。4.2 加载顺序和依赖在板端加载时inv-mpu6050-i2c.ko依赖inv-mpu6050.ko。不按顺序加载会给insmod报“Unknown symbol in module”之类的错误。比较省事的方式是先insmod inv-mpu6050.ko再insmod inv-mpu6050-i2c.ko。或者直接把两个文件放到/lib/modules/$(uname -r)/下运行depmod -a然后用modprobe inv-mpu6050-i2c自动解决依赖。加载完成后用dmesg | tail看内核日志。正常情况下能看到类似inv-mpu6050-i2c 0-0068: probed i2c i2c-0: new_device: Instantiated device mpu6050 at 0x68这里关键信息是探针成功。如果你在这一步看到了报错不要急着改驱动先去看具体是哪一段链路出的问题。这一步最值得关注的就是probed有没有成功。成功了说明设备树匹配、I2C通信、芯片ID识别都通过了。此时/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_gyro_x_raw这些节点那么驱动侧已经跑通了。从这里开始剩下的工作就从“驱动移植”切换到了“数据验证和调试”。5. 真正费时间的部分调试实录与常见故障定位5.1 故障场景一i2cdetect看不到0x68加载驱动实例会失败会直接导致探测不到设备但从最源头排查我们得先确认I2C总线本身通不通。开发板上执行i2cdetect -y -r 0-y表示跳过交互确认-r表示使用SMBus read方式扫描。IMX6ULL的I2C控制器对普通I2C探测的兼容性有时不太好建议无论如何都加-r。正常应该看到类似0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- 68 -- -- -- -- --0x68就在那里。如果扫描结果全是--通常意味着硬件供电有问题传感器没有工作。用万用表量VCC对GND电压ICM20608一般工作在3.3V或1.8V具体看模块设计。SDA/SCL接反了或者接的是其他I2C控制器引脚。I2C上拉电阻没有总线拉不起来。IMX6ULL的I2C引脚内部可能有弱上拉但外部仍需设计上拉电阻如果通过杜邦线长距离连接尤其容易出这种问题。如果扫描到的地址是0x69而不是0x68说明模块上AD0引脚被拉高了设备树里reg 0x68就要同步改成0x69否则驱动一样找不到设备。这是很隐蔽的坑模块厂商常常把AD0用跳帽控制默认可能是1。5.2 故障场景二i2cdetect能看到设备但内核日志报ID不匹配i2cdetect能看到0x68说明I2C通、设备活着但dmesg里却报类似“invalid WHO_AM_I”的信息或者显示驱动根本没有probe。这时候先手动读取芯片的WHO_AM_I寄存器i2cget -y 0 0x68 0x75ICM20608应该返回0xaf。如果你读到的是别的值比如0x00或者0xff那说明I2C时序下寄存器读取有问题或者芯片处于异常状态。如果读到了0xaf而内核还是拒绝probe问题多半出在内核驱动代码的ID校验环节。有些老版本内核没有把ICM20608的ID加进inv_check_and_setup_chip()的合法性清单只判断了MPU6050的0x68和MPU6500的0x70。解决办法也很直接打开drivers/iio/imu/inv_mpu6050/inv_mpu6050_core.c找到类似下面这段static int inv_check_and_setup_chip(struct inv_mpu6050_state *st) { ... data INV_MPU6050_REG_WHO_AM_I_MASK; if (data ! INV_MPU6050_REG_WHO_AM_I_VALUE data ! INV_MPU6500_REG_WHO_AM_I_VALUE) return -ENODEV; ... }把data ! INV_ICM20608_REG_WHO_AM_I_VALUE这个条件补进去编译对应模块后重新加载。5.3 故障场景三设备节点出来了但数据全为0或恒定值这是很多人在成功probe之后会遇到的第二个大坑。设备节点满头各种_raw节点都在读取却要么全是0要么长时间不变。第一个要查的就是睡眠模式。ICM20608上电默认PWR_MGMT_1寄存器0x6B最高位是睡眠位初始值可能为1。驱动在probe时应该会设置但如果你的板子上设备树匹配到了但驱动初始化流程没走完或者你用的是老版本驱动就可能遇到芯片还睡着的状态。可以用i2cget直接读取判断i2cget -y 0 0x68 0x6b如果是0x40说明进入睡眠模式。用i2cset唤醒i2cset -y 0 0x68 0x6b 0x00再读数据节点应该立刻有响应。第二个要查的是满量程和采样配置寄存器。如果加速度计的量程配置为±2g那么水平静置时Z轴读数应该在16384附近X/Y在0附近陀螺仪在无旋转时应接近0的固定值。如果所有轴都恒定在一个大数比如32767可能是配置寄存器没生效也可能I2C读到的只是寄存器后8位而高字节数据读错了。这时候用i2cdump -y 0 0x68把整个寄存器空间拉出来看重点看0x3B到0x48这段数据输出寄存器以及0x1A、0x1B、0x1C这几个配置寄存器。5.4 中断引脚配置的连锁问题我在设备树里写了interrupt-parent gpio5和interrupts 3 IRQ_TYPE_LEVEL_LOW但实际调试时发现自己的模块INT引脚悬空根本没有接到GPIO上。这时驱动虽然能probe但和触发缓冲区有关的功能可能工作不正常有些内核版本里中断申请失败还会导致probe失败。如果你确认模块没有INT引脚或者不想接中断建议先把设备树节点中的interrupt-parent和interrupts属性去掉。这样驱动无法使用中断触发模式但普通sysfs读取原始数据完全没问题。等后面需要做高速连续读取时再补硬件中断。这个取舍是很多移植笔记里不会写清楚的容易让人误以为“必须有INT才能用”。6. 从原始值到物理量读取、换算和校准实战6.1 利用sysfs节点读取原始数据驱动正常工作后读取加速度数据的命令很直接cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw cat /sys/bus/iio/devices/iio:device0/in_gyro_z_raw每个_raw文件都会返回一个带符号整数。不同量程下同样的物理加速度这个整数大小不同。判断当前使用的量程可以看cat /sys/bus/iio/devices/iio:device0/in_accel_scale cat /sys/bus/iio/devices/iio:device0/in_gyro_scale计算关系为物理值 raw值 × scale值拿加速度计举例如果scale是0.000598约1/16384的g单位变换那么水平静置时Z轴raw大约是16384乘以scale后约9.8单位是m/s²正好是一个g。陀螺仪侧在±250dps量程下原始值131对应1°/s所以scale大约是0.007633以dps为单位时。这段换算逻辑建议写成一个小脚本方便随时校准观察ACCEL_SCALE$(cat /sys/bus/iio/devices/iio:device0/in_accel_scale) GYRO_SCALE$(cat /sys/bus/iio/devices/iio:device0/in_gyro_scale) AX$(cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw) AY$(cat /sys/bus/iio/devices/iio:device0/in_accel_y_raw) AZ$(cat /sys/bus/iio/devices/iio:device0/in_accel_z_raw) GX$(cat /sys/bus/iio/devices/iio:device0/in_gyro_x_raw) echo acc($AX,$AY,$AZ) gyro($GX,$GY)6.2 数据合理性判断静止和翻转测试拿到第一组数据后先让板子水平静置。理想情况下Z轴加速度约等于1g即16384 LSB或9.8 m/s²X和Y接近0陀螺仪三轴接近0允许微小零偏。然后把板子分别沿着X、Y、Z轴翻转90度观察对应轴向的数值变化。假设绕X轴转了90度原来Z轴的1g读数会逐渐转移到Y轴或X轴这个变化过程和公式要对得上。如果翻转后所有轴没有任何变化检查是否又进入睡眠模式或配置寄存器是否被其他驱动重置。陀螺仪响应的测试更简单快速旋转板子的时候观察in_gyro_*_raw旋转停止后它应该很快回到0附近。如果旋转中读数始终为0大多是因为驱动没有正确读取数据输出寄存器或者设备树绑定的I2C控制器不是传感器实际连接的那个。6.3 零偏校准的土办法因为我后面要做姿态解算零偏直接影响积分漂移所以在应用层里做了个很简单的零偏采集板子完全静止连续读取100组陀螺仪原始值取平均作为零偏在姿态更新时先减掉这个零偏再积分。加速度计也可以做类似的offset但硬件本身出厂校准通常还行主要校准的是PCB装配角度带来的轴间偏差这部分看你的精度需求决定做不做。我自己的经验是如果要拿它做云台或飞行器姿态融合陀螺仪零偏是必须处理的否则积分十来秒就飘得没边。计算俯仰角和横滚角时直接使用静止加速度计的三轴值pitch atan2(-Ax, sqrt(Ay*Ay Az*Az))roll atan2(Ay, Az)这里要特别提醒IMX6ULL板子的传感器安装方向可能和设备树采用的I2C编号所对应的物理轴方向不一致。我调试时就把板子横放竖放对比过发现驱动给的in_accel_x_raw正方向和我预期的正方向反了。这种情况下优先改应用层的轴映射不要轻易去改驱动代码改驱动反而容易影响其他项目。7. 触发缓冲模式与SPI备选方案的补充7.1 为什么主动放弃中断触发而用sysfs轮询我最终没有启用中断驱动的buffer模式原因有两方面。一是我的模块INT引脚没接到IMX6ULL GPIO上要启用需要改硬件二是姿态解算的采样率不需要很高100Hz左右的轮询足够利用IIO sysfs节点在应用层定时读取完全满足需求。如果你的项目需要稳定采样频率、不想应用层频繁cat文件那建议把INT引脚接到一个空闲GPIO并在设备树中配置好中断然后启用IIO的trigger和buffer通过异步通知方式采集。这一步的配置并不复杂和常规IIO驱动一致但前提是硬件上先把INT接好。7.2 如果硬件是SPI接口如果传感器通过SPI挂在IMX6ULL上移植逻辑和I2C类似但细节不同。设备树节点里要选fsl,imx6ul-ecspi对应的SPI控制器ICM20608在SPI模式下寄存器地址带有RW标志位驱动会通过spi设备表和驱动匹配。另外SPI接口一般需要自己处理CS片选信号IMX6ULL的ECSPI可以硬件管理CS也可以软件控制。我不建议一开始就用SPI调驱动因为I2C的调试工具链i2cdetect、i2cget、i2cset比SPI的调试工具丰富得多。没有特别高的速率要求I2C的400kHz对六轴IMU已经够用。8. 移植完成后的个人经验与后续扩展方向折腾完这一趟最大的感触是这类移植项目的核心不在于修改源码而在于理解设备树、内核驱动和用户空间三者之间的配合链路。ICM20608的驱动代码在内核里几乎不需要改动真正决定能不能跑的是设备树里的compatible字符串、reg地址、引脚复用配置是否准确。如果你在调试时把dmesg日志、i2cdetect扫描结果、传感器寄存器dump都记录下来排查效率会高非常多。后续如果要把这个传感器接入实际项目我建议的思路是在内核里保持IIO驱动不变把姿态解算、滤波、数据融合放到用户空间实现。用in_accel_/in_gyro_原始值配合scale系数就能得到标准物理单位的数据。再往上做卡尔曼滤波或互补滤波直接在应用层用C或Python解决这样开发最灵活也不会把自己锁死在某一个内核版本里。最后还有个小技巧把这次改动的设备树节点、.config、模块拷贝路径整理成一个patch文件下次重装系统或换板子时一条git apply就能恢复整个移植成果。这比重新看原理图、重新配menuconfig要省太多时间。
返回列表