ARTICLE DETAIL

资讯详情

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

IMX6ULL上移植ICM20608六轴传感器内核驱动全流程

IMX6ULL上移植ICM20608六轴传感器内核驱动全流程 在嵌入式开发里移植一个外设驱动这件事说大不大说小也不小。尤其当目标芯片是IMX6ULL这种Cortex-A7内核的工业级处理器外设是ICM20608这种常见的六轴传感器时过程往往横跨硬件原理图、内核Kconfig、设备树、驱动框架和用户空间验证任何一个环节脱节结果就是传感器数据死活读不出来。我最近在100ask_imx6ull上完整跑了一遍ICM20608的内核驱动移植流程从读寄存器到设备树匹配从内核配置到用iio工具出数据踩了不少坑也理清了很多之前模棱两可的细节。这篇调试笔记把整个过程按实际操作顺序记录下来覆盖硬件连接、内核驱动框架选型、设备树节点编写、内核配置、编译烧写、驱动装载验证以及各种报错排查方法给准备在IMX6ULL平台上接六轴传感器的小伙伴一个可以直接参考的落地路径。1. 项目拆解表面上是在调驱动实际上是在打通一整条链路1.1 这个任务真正要解决什么问题ICM20608是InvenSense推出的一款低功耗六轴惯性传感器内部集成了三轴加速度计和三轴陀螺仪支持I2C和SPI两种通信接口常用于无人机飞控、平衡车、机器人姿态检测等场景。它的量程范围、采样率、数字滤波都可以通过寄存器配置内部还有一个1024字节的FIFO可以缓存数据减轻主控负担。和经典的MPU6050相比ICM20608的功耗更低、封装更小寄存器兼容性大体延续了MPU系列的风格所以内核里往往把二者放在同一个驱动框架下维护。在100ask_imx6ull上“移植内核驱动”落点不是把芯片的寄存器操作写一遍就完事而是要让这颗传感器以标准的Linux驱动形态运行起来具体包括通过I2C或SPI总线与芯片通信、在内核里匹配到对应驱动、生成标准的设备节点通常是IIO框架下的 /dev/iio:deviceX 或自写的 /dev/icm20608、最终让用户空间用一套合规的API读到原始六轴数据。这背后牵涉设备树、pinctrl、内核子系统框架、编译烧写任何一个环节缺少数据都到不了用户空间。1.2 为什么优先选择标准内核驱动而不是裸机驱动思路很多从单片机转过来的开发者第一反应是直接写个裸机驱动把所有寄存器操作放在一个文件里read函数里直接i2c_transfer拿到数据就往上抛。这种方式在简单验证场景下确实可行但放在IMX6ULL这种跑Linux的系统里会带来几个问题一是无法利用内核的电源管理、中断框架、延时API二是设备节点不符合Linux规范后续如果要接MPU6050、ICM20948等其他传感器代码复用性差三是中断、DMA、时钟这些资源没法通过设备树协调容易和别的驱动冲突。内核自带驱动虽然需要配置、匹配设备树但一旦跑通传感器逻辑全部由内核维护用户空间只需要去读sysfs节点或者用ioctl。我的建议是如果只是验证芯片好坏可以先写个小的字符设备驱动但如果目标是长期项目、后续要复用代码或者做产品直接走标准IIO驱动框架是更稳的路线。这次调试我也是先确认了内核里有没有现成的驱动确认存在后才决定用标准方式能少写一大段寄存器配置代码。1.3 移植流程的全景图整个移植过程可以拆成六步硬件连接确认、查芯片数据手册中关键寄存器、确认内核里可用驱动框架、编写设备树节点、重新配置内核、编译烧写后验证数据。每一步的产出都应该能对应到一次可检查的结果比如硬件阶段检查供电电压驱动阶段检查WHO_AM_I寄存器值设备树阶段检查/sys/firmware/devicetree里有没有对应节点内核配置阶段检查Kconfig选项是否被编译进内核。这样一旦出错可以快速定位问题出在哪个环节而不是毫无头绪地乱试。2. 硬件准备与通信接口选择先搞清楚芯片接在哪条总线上2.1 ICM20608的关键引脚与寄存器速览ICM20608通常以QFN封装出现核心引脚包括VDD电源一般1.8V或3.3V、GND、SCL/SCLKI2C时钟或SPI时钟、SDA/SDII2C数据或SPI数据、AD0/SDOI2C地址选择或SPI数据输出、CSSPI片选I2C模式下接到VDD、INT中断输出。芯片支持I2C和SPI两种模式通过CS引脚的电平决定CS拉高时工作于I2C模式拉低时工作于SPI模式。I2C模式下I2C地址由AD0引脚决定——AD0接地时地址是0x68接VDD时地址是0x69这个地址会在后面的设备树编写里用到。寄存器方面必须知道的关键地址有几个WHO_AM_I寄存器在0x75用于校验芯片是否存活ICM20608正常读取值应为0xAF电源管理寄存器1在0x6B默认值是0x40意味芯片处于睡眠状态需要写入0x00唤醒陀螺仪配置寄存器在0x1B加速度计配置寄存器在0x1C用于设置量程范围采样率分频寄存器在0x19FIFO使能寄存器在0x6A。这些寄存器地址后面排查问题时几乎天天见建议先抄在笔记本上。2.2 100ask_imx6ull开发板的I2C资源分布100ask_imx6ull开发板基于NXP的i.MX6ULL处理器SoC内部有多组I2C控制器芯片引脚复用后引出到板载排针或特定接口。我不是第一次在这块板子上调I2C设备经验是先查原理图确认目标总线编号而不是盲目打开某个I2C设备节点去盲试。IMX6ULL的I2C控制器从I2C1到I2C4都有板子上实际引出的可能只占其中一部分不同批次的底板也可能有差异。要注意的是IMX6ULL的I2C引脚是复用引脚必须先在设备树里通过pinctrl把引脚配置成I2C功能I2C控制器本身才能工作。如果只配置了I2C控制器的reg和时钟没有配置引脚总线上根本不会有波形这是新手最容易踩的坑。我这次用的板子把ICM20608接到了I2C1上对应引脚是UART4_TX和UART4_RX复用后的I2C1_SCL和I2C1_SDA。不同的底板可能用I2C2或I2C3务必以手中的原理图为准。2.3 接线时容易忽略的细节接线不只是把SDA、SCL、VDD、GND接上就完事。I2C总线需要在SCL和SDA上接上拉电阻一般4.7kΩ到10kΩ都行具体看总线上挂了多少设备。100ask板子的I2C接口通常已经带上拉但如果自己飞线接模块模块上又没有上拉电阻就很容易出现地址扫描不稳定或者偶尔通信失败的情况。电源纹波对传感器影响也很大ICM20608的模拟电源引脚最好就近放一个0.1uF去耦电容如果板子上已经有就省去自己焊的麻烦。中断引脚是我的一个重要观察点ICM20608的INT引脚可以配置为数据就绪中断输出但如果设备树里没写好中断配置驱动注册时中断申请失败并不会影响I2C数据读取只是会少一条事件通知路径。所以调试初期可以先不接中断引脚先把数据通路跑通再说中断功能属于后续优化项。3. 内核驱动框架选型复用inv_mpu6050是该重点关注的选项3.1 内核里的inv_mpu6050驱动族Linux内核的IIO子系统下有一个目录 drivers/iio/imu/inv_mpu6050/这就是InvenSense系列传感器在内核中的官方驱动。这个驱动支持MPU6000、MPU6050、MPU6500、ICM20608等型号核心模块提供寄存器读写、数据解析、FIFO管理、DMP数字运动处理器支持再通过I2C或SPI的接口层和具体总线对接。目录下有inv_mpu_core.c、inv_mpu_iio.c、inv_mpu_ring.c以及inv_mpu_i2c.c和inv_mpu_spi.c两个总线适配文件。对于我们当前的移植任务最关键的一点是驱动通过设备树compatible字段匹配芯片型号。搜索内核源码可以发现匹配表里通常有“invensense,icm20608”这个兼容字符串也就是说只要我们设备树节点写上兼容属性并挂到正确的I2C总线上驱动就能自动发包几乎不用改内核代码。这正是我选择这条路线的底气内核已经把底层配置写好了我们要做的是让设备树把芯片信息告诉内核。3.2 复用标准驱动的优点与代价复用标准驱动最大的优点是省事寄存器初始化、FIFO读取、中断处理、IIO接口都已经被验证过我们不需要重复造轮子。带来的问题是没有代码层面的定制空间如果项目的需求比较特殊比如要特别低的功耗、特定的中断触发模式或者要绕过驱动里的采样策略改起来反而比自己写驱动麻烦。此外标准驱动依赖内核配置选项如果内核裁剪得比较狠没有把IIO子系统编进去那驱动也无从谈起。另一个代价是学习曲线。很多人不熟悉IIO子系统往 /sys/bus/iio/devices/ 里找数据时觉得别扭因为IIO的读取方式不是传统的open/read一个设备节点而是通过trigger、buffer、scan_elements这些抽象概念来理解数据流。但换个角度想这也是内核为你做了大量工作之后自然形成的抽象值得花一点时间适应。3.3 如果内核版本太老或驱动缺失怎么自写驱动有些开发板的内核版本较旧或者BSP裁剪时把inv_mpu6050驱动删掉了那就要考虑自己写驱动。自写驱动的基本结构并不复杂用i2c_driver注册一个驱动探测函数里读取WHO_AM_I确认芯片存在之后通过regmap或直接i2c_transfer操作寄存器最后注册一个misc设备或者IIO设备。核心代码如下所示static const struct i2c_device_id icm20608_id[] { { icm20608, 0 }, { } }; static const struct of_device_id icm20608_of_match[] { { .compatible invensense,icm20608 }, { } }; static int icm20608_probe(struct i2c_client *client) { int ret; u8 val; ret i2c_smbus_read_byte_data(client, 0x75); if (ret 0) return ret; val (u8)ret; dev_info(client-dev, WHO_AM_I 0x%02x\n, val); if (val ! 0xAF) return -ENODEV; // 唤醒芯片电源管理寄存器1写0 i2c_smbus_write_byte_data(client, 0x6B, 0x00); // 配置陀螺仪量程 -2000dps i2c_smbus_write_byte_data(client, 0x1B, 0x18); // 配置加速度计量程 -16g i2c_smbus_write_byte_data(client, 0x1C, 0x18); return misc_register(icm20608_miscdev); }这个框架第一次跑通后后续的优化空间很大加FIFO支持、中断支持、加入IIO框架、增加sysfs属性等。但要注意几点i2c_smbus_read_byte_data在高速读取大量数据时性能不如i2c_transferregmap设备树匹配必须和of_match_table里的compatible一致引用的头文件要包含linux/i2c.h、linux/miscdevice.h和linux/of_device.h。第一次先跑通是最重要的性能优化可以放在后头。4. 设备树编写与内核配置让内核“看见”你的芯片4.1 确认I2C总线编号与芯片地址设备树的核心任务就是告诉内核哪个I2C控制器上挂了什么设备、设备地址是多少、用哪个引脚中断。在写设备树之前首先得确定I2C控制器编号。我通过在系统上操作查看总线列表来确认ls /dev/i2c-*通常输出会有 i2c-0、i2c-1、i2c-2等。然后用i2cdetect扫描总线上挂载的设备i2cdetect -y 1如果看到0x68或0x69说明ICM20608在I2C1总线上地址已经被识别。这里注意如果芯片处于睡眠状态扫描一般还是能看到地址因为地址应答不依赖寄存器唤醒。只有在通信线路异常时扫描才会空白。如果扫描不到地址先检查引脚配置和上拉再检查电源不要急着改设备树。这个顺序几乎可以排查掉一半的问题。4.2 在设备树里新增ICM20608节点确认I2C总线和地址后修改板级设备树文件在对应的I2C控制器节点下增加子节点。以I2C1为例设备树片段大致如下i2c1 { clock-frequency 100000; pinctrl-names default; pinctrl-0 pinctrl_i2c1; status okay; icm20608: icm2060868 { compatible invensense,icm20608; reg 0x68; interrupt-parent gpio4; interrupts 19 IRQ_TYPE_EDGE_RISING; vdd-supply reg_3p3v; vddio-supply reg_3p3v; }; };pinctrl_i2c1的定义放在iomuxc节点下用来把引脚复用成I2C功能例如pinctrl_i2c1: i2c1grp { fsl,pins MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0 MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0 ; };这里有两个细节值得展开。第一是reg 0x68表示设备I2C地址是0x68这是由AD0引脚接地决定的如果原理图上AD0接高电平那就得改成0x69否则驱动probe时读不到WHO_AM_I。第二是中断配置initially可以不写中断节点等数据通路正常后再补上。如果写了中断却又没有在pinctrl里把引脚复用成GPIO功能中断申请就会失败启动时日志里报错反而干扰排查。4.3 内核Kconfig配置项选择设备树写好后还要确认内核编译时把inv_mpu6050驱动编进去。对于IMX6ULL这种BSP内核通常在menuconfig里搜索“MPU6050”选中以下选项CONFIG_IIOy CONFIG_INV_MPU6050_IIOy CONFIG_INV_MPU6050_I2Cy如果IMX6ULL用的是SPI接口那么对应选CONFIG_INV_MPU6050_SPI。这里注意CONFIG_INV_MPU6050_I2C和CONFIG_INV_MPU6050_SPI是两个独立总线适配层可以同时选中。此外CONFIG_IIO_BUFFER和CONFIG_IIO_TRIGGER通常也会被自动选中但最好检查一下。若想读取数据时使用sysfs触发方式还要保留CONFIG_IIO_SYSFS_TRIGGER。我建议把驱动编进内核镜像而不是编成模块尤其是调试初期。模块方式看起来灵活但每次更换内核版本或设备树后需要重新insmod还要考虑模块和内核的版本匹配问题对新手不友好。直接编进内核后只要设备树正确驱动自动加载dmesg里就能看到probe信息调试效率更高。4.4 编译与烧写内核和设备树编译IMX6ULL内核的常规命令export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make imx_v7_defconfig make menuconfig # 选中IIO和inv_mpu6050相关选项 make -j8 zImage make -j8 dtbs烧写时zImage和对应的dtb文件需要替换到SD卡或EMMC的启动分区。100ask板子通常使用SD卡启动把新的zImage覆盖到boot分区dtb也同步覆盖。这里要特别留意内核版本和dtb必须配套如果更换内核后忘记更新dtb设备树解析时可能出错或者某些外设无法工作。曾经有一回我为了省事只更新了zImage结果I2C节点一直没生效后来才发现boot分区里的dtb还是旧的。5. 驱动装载验证与数据读取从dmesg到传感器数值的第二屏5.1 启动日志里的probe信息完成烧写后启动开发板进入系统后第一件事不是急着读数据而是看内核日志。执行dmesg | grep -i icm正常情况下能看到类似这样的日志inv_mpu6050 i2c-1:0x68: whoami 0xaf 或者 icm20608: probe success 之类的输出。如果什么都没有说明驱动根本没有匹配问题大概率出在设备树compatible字符串不匹配或者I2C控制器节点status没有设为okay。再用i2cdetect -y 1确认地址是否能扫到。如果日志显示失败常见的有两种情况一是WHO_AM_I读到的值不等于0xAF报设备不匹配这可能是因为芯片型号不是ICM20608而是其他MPU系列芯片或者I2C地址错误导致读到了别的设备的寄存器二是总线通信超时I2C控制器没有正常工作的占多数这时回到pinctrl和上拉电阻去查。5.2 IIO目录下的设备节点probe成功后查看IIO设备ls /sys/bus/iio/devices/通常会出现 iio:device0 或类似节点。进入该目录查看cat /sys/bus/iio/devices/iio:device0/name输出通常会是icm20608或mpu6050。重要的是查看两个属性文件in_accel_scale和in_anglvel_scale它们代表原始数据转换成物理值时的缩放系数。加速度计原始数据除以这个系数得到g值陀螺仪原始数据除以这个系数得到dps值。这个细节如果不注意直接拿原始数去算角度结果会差得非常离谱。5.3 实时读取六轴数据的几种方式读取IIO设备数据有几种方式最简单的是逐个读取sysfs文件。先看当前使能的数据通道cat /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_x_en等于1表示加速度X通道已使能。然后读取原始值cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw这种方式虽然直观但每次读取只能拿到一个通道的瞬时值多个通道之间有时间差很不适合做动态数据采集。更推荐使用trigger加buffer的方式。先把需要的通道全部使能然后指定一个trigger触发采集再读取/dev/iio:device0。在shell里可以用以下命令模拟echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_x_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_y_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_accel_z_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_anglvel_x_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_anglvel_y_en echo 1 /sys/bus/iio/devices/iio:device0/scan_elements/in_anglvel_z_en echo 1 /sys/bus/iio/devices/iio:device0/buffer/enable cat /dev/iio:device0 | xxddbg输出为二进制数据流每个通道16位按scan_elements里的类型解析。当看到非全零且数值有变化时说明数据流已经通了。如果发现数值全是0先检查芯片是否还在睡眠模式电源管理寄存器是否配置正确。python方式在后续开发里更普遍可以借助python3的struct模块解析/dev/iio:device0的二进制数据或者直接用libiio库。不过初次验证时工具越直接越好sh脚本加xxd足够完成验证。5.4 数据合理性判断静态数据和微小转动顺利读到数据后如何判断数据合理把开发板水平静置加速度计三个轴中Z轴应当接近1g的正向或负向读数取决于安装方向X和Y轴应接近0g换算成原始值后要结合量程和scale值确认。陀螺仪在静止时读数应接近0dps但会有一定的零漂数值在正负几dps以内通常正常。如果静止时数据还跳变到几十甚至上百dps要么是滤波配置没生效要么是电源噪声太大要么是芯片安装得不够稳固。用手缓慢转动板子陀螺仪数值应当立即响应加速度计数值也会因姿态变化而变化。能把数据随姿态改变而改变驱动移植就算成功了。下一步才是姿态解算、卡尔曼滤波这类算法工作。6. 调试中的典型坑与排查实录汇总6.1 WHO_AM_I读到0xFF或超时如果dmesg里报i2c transfer error即i2c_read失败大概率是设备不在预期地址上或者总线压根没通。我用这一顺序排查过多块板子先用万用表量芯片供电再接逻辑分析仪看总线波形。如果扫描工具能看到地址说明通信链路没问题。如果设备地址从0x68变成0x69按AD0电平去改设备树。有个注意点是部分模块上的AD0引脚出厂就接在VDD上不能想当然地认为是接地。6.2 读到数据全部为零WHO_AM_I正确probe成功但六个通道读取都是0。这种场景我遇到过一次是芯片处于睡眠模式导致的。所以移植后首先要做的寄存器操作就是向0x6B写0x00唤醒设备。内核标准驱动理论上probe时会做这一步但如果设备树属性里恰好配置了某些电源管理选项顺序可能被绕过去。自写驱动就更要小心唤醒操作必须在配置量程之前完成。6.3 中断不触发或中断风暴中断引脚配置比较敏感。中断触发方式如果设成电平触发芯片持续拉高数据就绪引脚内核会认为中断一直被断言反复触发导致CPU占用很高。IMX6ULL设备树中IRQ_TYPE_EDGE_RISING是边沿触发比电平触发更常用。另一个要注意的点是中断GPIO的pinctrl必须单独配置为GPIO模式不能和I2C引脚的pinctrl冲突。如果确认芯片的中断引脚输出电平方式和驱动预期不一致可以在设备树里修改中断触发类型来匹配。6.4 与板载其它I2C设备地址冲突同一个I2C总线上如果还挂了其他设备地址也许恰好和0x68/0x69冲突。这种冲突有时难以察觉因为扫描能扫到地址但读取WHO_AM_I时返回的可能不是0xAF而是其他设备的寄存器值。这种问题的解决方式是把传感器挪到独立总线上或者修改片选/地址引脚避开冲突。6.5 常见报错速查表以下是我整理的速查表按“现象 大概率原因 处理手段”三列给出方便后续快速定位# dmesg | grep -i icm 无输出 # 检查设备树compatible是否匹配、i2c节点status是否okay Symptom: WHO_AM_I 0xFF Cause: 通信链路异常/设备不在对应地址 Fix: 检查I2C地址引脚、供电、上拉电阻 Symptom: WHO_AM_I 0x00 Cause: 芯片未上电或SCL/SDA接反 Fix: 检查电源和接线重新对照原理图 Symptom: i2c transfer error Cause: 控制器未初始化/引脚复用错误 Fix: 检查i2c控制器pinctrl配置和clock-frequency值 Symptom: 六个通道全为0 Cause: 芯片处于睡眠模式/量程设置未生效 Fix: 向0x6B写0x00唤醒重新配置采样率和量程 Symptom: 数值跳变剧烈 Cause: 电源纹波大/量程太小/未开滤波 Fix: 加大去耦电容调整量程配置DLPF滤波 Symptom: 中断风暴 Cause: 触发方式不匹配/引脚配置错误 Fix: 改用边沿触发检查中断GPIO的pinctrl配置7. 从原始数据到可用姿态验证完成后的下一步扩展7.1 为什么原始数据不能直接用于姿态解算ICM20608给出的原始六轴数据经过scale缩放后得到的是物理量即加速度计输出的是g值陀螺仪输出的是dps值。但想得到欧拉角或四元数还需要考虑传感器噪声、零漂和安装方向。直接用加速度计归一化解决俯仰和横滚角在静态场景下够用一旦动态运动加速度计受线性加速度干扰结果会严重失真陀螺仪积分能解决动态场景但零漂会随积分时间累积。所以简单工程中常用互补滤波或卡尔曼滤波融合两者数据。7.2 在Linux用户空间做姿态解算的几种选择拿到iio数据后用户空间可以做多级处理。最简单的方案是用python循环读/sys/bus/iio/devices下的raw值再做互补滤波适合快速验证。工程化方案是通过IIO buffer读取数据流把数据交给C或C进行姿态解算。如果需要用ROS可以直接用ros_imu传感器驱动包它的底层通常支持读取IIO设备或串口数据。选择哪种方案取决于产品形态如果本来就是Linux单板直接在用户态解算即可如果后续要把数据给MCU做控制可能需要通过串口转发处理后的姿态信息。7.3 进一步优化驱动的方向驱动移植成功后可以继续在几个方向上做优化把驱动改造成可加载模块便于同步开发增加IIO trigger支持通过外部定时器触发采集避免数据抖动尝试打开FIFO让芯片攒一批数据后一次性读取降低I2C读写频率节省CPU如果芯片支持DMP可以通过内核驱动加载固件把一部分姿态解算放到传感器内部去减轻主控负担。这些都属于在标准驱动框架内扩展能力的范畴比另起炉灶写一套要高效得多。8. 写在最后的几条经验这次把WBZ的ICM20608移植到100ask_imx6ull最深的感受是驱动移植不只是写代码更多时候是在和设备树、内核配置、硬件接线做“谈判”。回头总结几条实际经验供参考——第一动手前先把原理图的I2C总线编号和芯片地址确认到确切值这是最高性价比的准备工作第二善用内核现成的IIO驱动inv_mpu6050框架已经帮你处理了很多细节至少把它的代码通读一遍再决定是否自写驱动第三调试时先看dmesg再i2cdetect再读WHO_AM_I逐层排出问题切忌一上来就改设备树乱试第四注意区分scale值和raw值很多人数据看起来不对其实是没做单位换算。用自写驱动快速验证芯片工作再用标准驱动落地到产品里是我个人比较习惯的路径。如果想把这次调试过程转成更通用的模板这套“硬件确认—设备树—内核配置—驱动加载—数据验证”的流程几乎可以复用到IMX6ULL上任何I2C外设包括触摸屏控制器、气压计、温湿度传感器等。至少我现在再拿到一块新开发板、一个新传感器不会再心里发虚而是知道第一步应该打开原理图查总线第二步看内核有没有现成驱动第三步写设备树跑起来。这大概就是调试经验的价值吧。
返回列表