
搞嵌入式Linux这几年我越来越觉得移植这个词被很多人说得很玄乎。实际上所谓的内核驱动移植拆开来看无非是三件事让内核认识你的硬件、把驱动代码放进当前内核能编译的体系里、再把寄存器接口和应用层的数据通路对上。这篇文章就是我在100ask_imx6ull开发板上移植ICM20608六轴传感器内核驱动的完整记录从读原理图确认引脚、改设备树、开内核配置、编译镜像到最后的传感器数据读取和问题排查尽量把每一步的为什么也写出来方便后面有人遇到类似板子时不至于和我一样从头踩坑。先说这块板子。100ask_imx6ull是百问网基于NXP i.MX6ULL做的开发板板载了一颗ICM20608。这颗传感器来自InvenSense现在归TDK内部集成三轴加速度计和三轴陀螺仪还带一个温度通道接口支持SPI和I2C。凡是涉及姿态解算、云台稳定、跌倒检测、小车导航的场合都得先把这种传感器的数据读出来才能继续做算法。所以第一步就是要让Linux内核驱动它并且能稳定读到数值。如果你正准备做类似的事这篇文章适合所有刚接触设备树、IIO子系统、SPI驱动框架的嵌入式Linux学习者。我不会假设你已经很懂内核但每个关键点都会尽量说明原理。建议手上备好两块东西板子的原理图以及ICM20608的数据手册Datasheet后面每一步几乎都要和它俩核对。1. 项目概述与整体思路1.1 任务目标与核心难点我的目标很明确在100ask_imx6ull上让内核识别板载ICM20608通过SPI总线完成寄存器读写最终在应用层拿到三轴加速度和角速度数据。听起来不难但真正做起来有几个坑是绕不开的内核默认配置里IIOIndustrial I/OLinux内核专门为传感器类设备设计的子系统大概率没有打开驱动有两种来源内核自带的inv_mpu6050驱动或者自己写一个字符设备驱动选型本身就有讲究设备树节点必须写对compatible、reg、interrupts、pinctrl一个都不能错错一个probe就起不来100ask默认内核版本是4.9.x这个版本里inv_mpu6050只有I2C路径没有SPI路径直接按网上新版本的教程操作会卡在配置项找不到这一步。所以我决定把两条路线都梳理清楚优先尝试内核自带的IIO驱动因为最省事、代码质量有保障如果内核版本不支持SPI再退回到自定义SPI驱动。实际调试中我两条都走了一遍下面会按实际操作顺序展开。1.2 为什么IIO框架是首选Linux内核里传感器驱动一般归在IIO子系统下它的定位类似传感器界的Input子系统。IIO提供了统一的抽象设备注册到 /sys/bus/iio/每个传感器有下属的xxx_raw、xxx_scale等属性文件应用层可以直接用cat读到数据也可以借助触发器trigger、环形缓冲buffer做高频连续采样。inv_mpu6050这个驱动在内核里的历史很久了它兼容MPU6000、MPU6050、ICM20608等一票InvenSense芯片。内核社区维护过的代码寄存器配置、量程切换、睡眠唤醒这些坑基本都被趟平了直接用比自己从零写要稳得多。但前提是你的内核版本得支持。我查了一下手上的4.9.88内核drivers/iio/imu/inv_mpu6050/目录下只有inv_mpu6050_i2c.c没有spi版本也就是说这个版本的内核只支持I2C接入不支持SPI接入。如果你用的是4.19以后的内核通常已经包含了inv_mpu6050_spi.c事情就简单很多。所以这里有个很关键的判断动作拿到任何板子第一步永远是去内核源码里翻一翻确认有没有现成驱动、缺什么总线路径这个习惯能帮你省掉大量无用功。如果内核确实没有SPI支持也不是只能放弃。两个选择把新版内核的SPI驱动代码回移过来这才是真正的移植或者针对这颗芯片写一个精简的spi_driver。5.4或5.10内核里的inv_mpu6050_spi.c不复杂主要由spi_device_id、of_device_id和一个probe函数组成回移成本并不高但如果只是学习验证写一个只服务本芯片的简单驱动更快后面的章节我会给一个能跑通的最小实现。2. 环境准备与硬件确认2.1 原理图核查是关键的第一步很多人一上来就抄网上的设备树结果板子Probe失败然后开始怀疑驱动代码最后发现是引脚配错。我的习惯是动手前先把原理图翻出来确认三件事——传感器挂在哪个SPI控制器上、中断脚接到哪个GPIO、供电电压多少。在100ask_imx6ull的板子上ICM20608挂在ECSPI1上四根线分别是SCLK、MOSI、MISO、CS中断输出接到GPIO4_IO19附近。不过不同批次、不同版本的核心板引脚可能会有差异一定以你自己手上板子的原理图为准。我在这里强调核查的原因是设备树里compatible、reg、interrupts、pinctrl这些参数全部来自原理图和芯片手册写错任何一个驱动都可能静默失败而且错误日志非常隐蔽。ICM20608的电气特性也值得先记住几个关键的参数数值说明SPI最高时钟8 MHz实际建议先用2~4 MHz过高容易出通信问题SPI模式Mode 0CPOL0CPHA0很多SPI传感器都是这个模式读操作寄存器地址最高位置1这是InvenSense系列SPI协议的规定WHO_AM_I寄存器0x75复位值0xAF这是验通信是否正确的最快方法供电范围2.375V~3.46V板载一般接3.3V把这些参数抄在一张便签上后面写代码和排查问题都用得上。2.2 交叉编译环境搭建我的主机是Ubuntu 20.04 x64交叉工具链用100ask配套的Linaro GCC 7.5版本arm-linux-gnueabihf-内核源码直接拉100ask的官方仓库分支是linux-4.9.88。export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- export PATH/opt/100ask/toolchain/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH如果用的是自己装的通用工具链其实也可以但版本别太老也别太新太老的编译器有时编新内核会报错太新的编译器比如GCC 12编老内核偶尔也会遇到类型检查变严导致的告警。开发环境和板子之间的传输我习惯用NFS挂载根文件系统内核镜像放开发板启动分区这样改完dtb或驱动模块可以直接拷贝测试不用反复烧卡。如果你的板子环境不支持NFS用scp拷到板子也行就是多几步。2.3 内核配置里需要打开的开关IIO相关配置在menuconfig里的路径是Device Drivers - Industrial I/O support需要确认的开关CONFIG_IIOyCONFIG_IIO_BUFFERy如果要高频采样CONFIG_INV_MPU6050_I2Cy或mI2C路径CONFIG_INV_MPU6050_SPIy或mSPI路径看内核版本是否提供需要特别说明的是4.9内核里inv_mpu6050目录根本没有SPI选项所以我第一次按网上的教程执行 make menuconfig搜索INV_MPU6050_SPI结果是空的。这时候不要慌先看内核版本再决定是回移代码还是走自定义驱动路线。如果用的是新版内核开这两个配置后驱动就能编译出来模块名是inv-mpu6050-i2c.ko或inv-mpu6050-spi.ko。3. 设备树修改与内核编译3.1 设备树节点怎么写设备树是内核认识板子硬件的主要手段。以100ask_imx6ull为例板级设备树文件在arch/arm/boot/dts/100ask_imx6ull.dts不同内核版本文件名可能有差异。我需要在ECSPI1节点下增加ICM20608的子节点。首先要确认ECSPI1这个控制器节点本身是okay状态然后把子节点挂进去ecspi1 { pinctrl-names default; pinctrl-0 pinctrl_ecspi1; cs-gpios gpio4 9 GPIO_ACTIVE_LOW; fsl,spi-num-chipselects 1; status okay; icm20608: icm206080 { compatible invensense,icm20608; reg 0; spi-max-frequency 4000000; interrupt-parent gpio4; interrupts 19 IRQ_TYPE_LEVEL_HIGH; }; };对应的引脚复用配置放在iomuxc节点下iomuxc { pinctrl_ecspi1: icm20608grp { fsl,pins MX6UL_PAD_ECSPI1_SCLK__ECSPI1_SCLK 0x10b0 MX6UL_PAD_ECSPI1_MOSI__ECSPI1_MOSI 0x10b0 MX6UL_PAD_ECSPI1_MISO__ECSPI1_MISO 0x10b0 ; }; };这里我要特别提醒以上引脚宏名只是示例不同内核版本、不同板级配置宏定义可能存在差异。一定要去自己内核的arch/arm/boot/dts/路径下查iomuxc相关的头文件确认这些宏在当前内核里真实存在。我踩过最典型的坑就是从网上抄了一段设备树宏名在当前内核里根本没有定义编译dtbs时居然也能通过因为dts里未定义的宏会被当作字符串不实际编译会报错但更隐蔽的是那种宏名恰好存在、引脚功能却对不上的情况SPI时钟死活出不来。所以配置完建议用dtc反编译一下生成的dtb确认引脚复用值真的写进去了。3.2 编译内核、设备树和模块配置、编译的完整命令序列如下make 100ask_imx6ull_defconfig # 或者直接 make menuconfig 手动开 IIO 相关选项 make menuconfig make zImage -j4 make dtbs make modules -j4如果编译内核前改过设备树一定要重新make dtbs并确认生成的dtb文件时间戳更新了。然后把它拷贝到板子的启动分区或者如果你的系统用U-Boot从网络加载就放到tftp目录里。模块安装可以这样make modules_install INSTALL_MOD_PATH/path/to/rootfs我在实际使用中为了调试方便更多是直接把.ko文件单独拷到板子的/lib/modules/目录下然后手动insmod。省事也更容易控制加载顺序。3.3 启动后先确认设备树是否生效在进入驱动调试之前先花一分钟确认设备树节点真的被内核解析了。板子上执行ls /proc/device-tree/ecspi1/或者ls /sys/firmware/devicetree/base/ecspi1/能看到icm206080子节点说明设备树解析正常。如果看不到八成是status没写okay或者整个ecspi1节点被disabled了。这一步的排查成本很低但能过滤掉后面一大半的驱动不工作问题。4. 驱动移植与加载调试4.1 顺着内核IIO驱动的Probe路径走一遍如果内核里能打开INV_MPU6050_SPI装上inv-mpu6050-spi.ko后配合上面写的设备树节点驱动的probe会被自动触发。加载模块后马上看日志modprobe inv-mpu6050-spi dmesg | tail -30正常情况能看到类似inv_mpu6050: probing的日志之后在/sys/bus/iio/devices/下会出现iio:device0。这个设备就是ICM20608的抽象节点。内核IIO驱动里probe的核心逻辑大致是先用regmap初始化SPI通信然后读取WHO_AM_I寄存器对比芯片IDID对了才继续配置寄存器、注册IIO设备。我在5.4内核上跑通过这个流程日志正常、数据干净。所以如果你的内核本身支持SPI路径到这里其实已经成功了。4.2 4.9内核没有SPI路径时的补救思路如果像100ask默认的4.9.88内核那样inv_mpu6050目录下根本没有SPI相关文件有两条路可以走。第一条路是回移SPI支持。把新版内核里的inv_mpu6050_spi.c拷贝到当前内核同名目录下并在Kconfig和Makefile里补上对应的条目。这个文件本身不复杂主要就是一个spi_driver结构体和probe函数probe内部调用inv_mpu6050_probe这是IIO核心驱动提供的通用入口。难点在于新旧内核API差异4.9里inv_mpu6050_probe的参数和5.x不完全一样regmap的初始化方式也有变化需要逐一比对。如果是给自己学习用这个工程挺值得做一遍能让你真切感受到内核API的演进。第二条路更快写一个只服务ICM20608的spi_driver用miscdevice暴露一个字符设备给应用层。模块代码量不大寄存器配置也不需要像IIO驱动那样面面俱到只要能唤醒芯片、设置量程、读加速度和角速度就行了。我这次实际调试就是先用这条路验证硬件后面再回头折腾IIO。4.3 最小可用的自定义SPI驱动骨架下面这段代码是我验证硬件用的核心部分重点是看SPI读写的姿势#include linux/module.h #include linux/spi/spi.h #include linux/of_device.h #define ICM20608_WHO_AM_I 0x75 #define ICM20608_PWR_MGMT_1 0x6B #define ICM20608_ACCEL_XOUT_H 0x3B #define ICM20608_GYRO_XOUT_H 0x43 static int icm20608_read_reg(struct spi_device *spi, u8 reg, u8 *val) { u8 tx[2] {reg | 0x80, 0}; u8 rx[2] {0}; struct spi_transfer tr { .tx_buf tx, .rx_buf rx, .len 2, }; struct spi_message msg; spi_message_init(msg); spi_message_add_tail(tr, msg); spi_sync(spi, msg); *val rx[1]; return 0; } static int icm20608_write_reg(struct spi_device *spi, u8 reg, u8 val) { u8 tx[2] {reg 0x7F, val}; /* 写操作时最高位拉低 */ struct spi_transfer tr { .tx_buf tx, .len 2, }; struct spi_message msg; spi_message_init(msg); spi_message_add_tail(tr, msg); return spi_sync(spi, msg); } static int icm20608_probe(struct spi_device *spi) { u8 id 0, val 0; spi-mode SPI_MODE_0; spi-max_speed_hz 4000000; spi_setup(spi); icm20608_read_reg(spi, ICM20608_WHO_AM_I, id); dev_info(spi-dev, WHO_AM_I 0x%02x\n, id); if (id ! 0xAF) return -ENODEV; /* 复位芯片 */ icm20608_write_reg(spi, ICM20608_PWR_MGMT_1, 0x80); msleep(50); /* 唤醒PWR_MGMT_1 的 SLEEP 位bit6清零 */ icm20608_write_reg(spi, ICM20608_PWR_MGMT_1, 0x00); /* 加速度计量程 ±8gACCEL_CONFIG 寄存器 bit4:3 设为 10 */ icm20608_write_reg(spi, 0x1C, 0x10); /* 陀螺仪量程 ±1000dpsGYRO_CONFIG 寄存器 bit4:3 设为 10 */ icm20608_write_reg(spi, 0x1B, 0x10); dev_info(spi-dev, icm20608 probe ok\n); return 0; } static const struct spi_device_id icm20608_id[] { { icm20608, 0 }, {} }; MODULE_DEVICE_TABLE(spi, icm20608_id); static const struct of_device_id icm20608_of_match[] { { .compatible invensense,icm20608 }, {} }; MODULE_DEVICE_TABLE(of, icm20608_of_match); static struct spi_driver icm20608_driver { .probe icm20608_probe, .id_table icm20608_id, .driver { .name icm20608, .of_match_table icm20608_of_match, }, }; module_spi_driver(icm20608_driver); MODULE_LICENSE(GPL);编译成模块后加载并观察insmod icm20608.ko dmesg | tail -10如果看到WHO_AM_I 0xaf和probe ok说明硬件通信没问题剩下的就是把它扩展成可以读数据的字符设备。我这里故意没有贴完整的read函数因为每个项目对外接口不一样有的是miscdevice、有的是input子系统、有的直接进IIO。核心就一句话SPI时序会了寄存器会了驱动骨架就是你的了。4.4 从probe到数据通路的重点细节如果你要扩展上面的骨架把加速度和陀螺仪数据读出来注意几点加速度数据从0x3B开始连续读6个字节XH、XL、YH、YL、ZH、ZL陀螺仪从0x43开始同样是6个字节每个轴都是16位有符号数内存里是大端序需要自己拼一下PWR_MGMT_1复位后默认0x40即SLEEP位是1必须清零才能正常工作量程改变后读到的原始值换算物理量的系数也要跟着变。换算关系可以记一张小表以默认±2g和±250dps为例量程加速度计灵敏度陀螺仪灵敏度±2g / ±250dps16384 LSB/g131 LSB/(°/s)±4g / ±500dps8192 LSB/g65.5 LSB/(°/s)±8g / ±1000dps4096 LSB/g32.8 LSB/(°/s)±16g / ±2000dps2048 LSB/g16.4 LSB/(°/s)物理量 原始值 / 灵敏度。比如量程±2g下Z轴读到16384就是1g。5. 数据读取与应用层测试5.1 用sysfs直读IIO数据如果走的是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_gyro_x_raw有些版本叫in_anglvel_x_raw是一个东西、in_temp_raw以及对应的scale文件。直接读原始值cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw物理量换算很简单读到的raw值乘以同目录下的scale文件。比如in_accel_scale显示0.000598那么加速度 m/s² raw × 0.000598。这里0.000598其实是2g量程下1个LSB对应的重力加速度换算来的1/16384 × 9.80665 ≈ 0.000598。如果你之前把量程配成了±8gscale文件里的数值会对应变化不用自己去算。5.2 连续打印一个简易测试程序我习惯写一个很小的C程序来持续打印数据方便观察传感器响应#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h int read_iio(const char *path) { char buf[32] {0}; int fd open(path, O_RDONLY); if (fd 0) return 0; read(fd, buf, sizeof(buf) - 1); close(fd); return atoi(buf); } int main(void) { const char *base /sys/bus/iio/devices/iio:device0/; char path[128]; while (1) { snprintf(path, sizeof(path), %sin_accel_x_raw, base); int ax read_iio(path); snprintf(path, sizeof(path), %sin_accel_y_raw, base); int ay read_iio(path); snprintf(path, sizeof(path), %sin_accel_z_raw, base); int az read_iio(path); snprintf(path, sizeof(path), %sin_gyro_x_raw, base); int gx read_iio(path); snprintf(path, sizeof(path), %sin_gyro_y_raw, base); int gy read_iio(path); snprintf(path, sizeof(path), %sin_gyro_z_raw, base); int gz read_iio(path); printf(accel: %4d %4d %4d gyro: %4d %4d %4d\n, ax, ay, az, gx, gy, gz); usleep(100000); } return 0; }编译时用交叉编译器arm-linux-gnueabihf-gcc -o imu_test imu_test.c5.3 怎么看数据才是正常的传感器数据对不对有个很直观的验证方法。把板子平放在桌面上静止不动时Z轴加速度应该接近1g在±2g量程下raw值接近16000~17000X、Y轴加速度接近0三轴陀螺仪数值都接近0会有小幅噪声比如±10以内波动这正常温度通道读到的值换算成摄氏度后大约在20~35之间。然后用手拿起板子缓慢旋转陀螺仪的读数应该跟着变化快速晃动板子加速度计读数应该出现明显波动。如果静止时Z轴raw是0或者接近0那大概率说明芯片还在睡眠状态或者量程寄存器没配好。如果陀螺仪静止时输出巨大漂移先看PWR_MGMT_2里有没有把对应轴关掉再看是不是电源不稳。sysfs路径连续cat的延迟虽然不高但如果你要做姿态解算建议在驱动层开IIO触发器或FIFO连续模式单纯靠应用层循环读sysfs在高频下会有调度抖动。如果只是验证硬件sysfs完全够用。6. 常见问题与排查技巧实录6.1 设备树没生效Probe根本没有被调用这是我遇到最多的情况。症状很典型insmod模块后dmesg里没有任何打印或者modprobe都成功了但/sys/bus/iio/devices/下始终不出现设备。排查顺序我建议这样先确认设备树节点有没有被解析ls /proc/device-tree/ecspi1/看不到节点就是设备树问题看节点status是不是okay很多控制器节点在板级dts里默认被disabled看compatible字符串和驱动里的of_device_id是否完全一致包括大小写在驱动probe入口加printk用dmesg确认驱动有没有被匹配到检查控制器节点的pinctrlSPI时钟脚有没有被其他外设复用。最后这条很隐蔽。i.MX6ULL的引脚复用表里同一个引脚往往有多个功能选项如果其他节点比如某个GPIO按键抢先把它配成了GPIO功能SPI控制器就会哑火。6.2 WHO_AM_I读不到0xAF硬件通信层面的问题基本都出在这里。可能的原因排在前三的是SPI模式不对。ICM20608必须用Mode 0CPOL0CPHA0如果内核把它默认设成Mode 3或别的方式读出来的数据就是乱的。检查spi_setup里的mode设置或者在设备树节点里显式加spi-cpol、spi-cpha属性。时钟频率太高。虽然手册标称8MHz但有些板子线长、布局不好或者供电边缘不够干净高频率下通信不稳定。先降到1MHz试试跑通了再往上提。寄存器地址的读写位没设置对。读操作地址要|0x80写操作是0x7F这个细节写错WHO_AM_I读出来永远是0。另外提醒一个容易忽略的测试手段直接用逻辑分析仪抓SCLK、MOSI、MISO三根线。我调试时发现读数不对用逻辑分析仪一看MOSI上确实发了0xF50x75|0x80但MISO上根本没有回应——瞬间定位到是板子焊接问题和驱动代码无关。如果你是开发新手逻辑分析仪或示波器永远是排查SPI问题的第一利器。6.3 数据全为零或恒定如果WHO_AM_I读出来是对的说明SPI通路正常但加速度和陀螺仪全是0大概率是寄存器配置没生效。我总结三个重点检查项是否真的唤醒了芯片。复位后PWR_MGMT_1是0x40SLEEP位为1。如果忘了清除很多寄存器是能读但数据不更新看起来就是全零。ACCEL_CONFIG、GYRO_CONFIG写没写进去。写完后读回来看一眼确认寄存器值真的变了。SPI写操作有时因为CS片选时序问题不生效。是不是读错了寄存器地址。加速度起始地址0x3B陀螺仪起始地址0x43中间夹着温度0x41、0x42地址多写一个字节全都错位。6.4 数据漂移、噪声特别大传感器不是理想器件静止时陀螺仪输出有小漂移、加速度计输出有小噪声这本身正常。但如果噪声明显超出预期先看看量程配置和scale换算对不对再看供电。另外ICM20608内部有个数字低通滤波器DLPF在0x1A寄存器里配置。需要滤波的时候可以打开但要注意DLPF会影响带宽选型时要和应用需求匹配。做姿态解算时陀螺仪零漂可以在上电静止的阶段采样做offset校准把静止时的平均值存下来之后每次读数减去这个值这是工程上最简单的处理办法也足够应对大部分入门项目。6.5 问题速查表现象可能原因排查动作probe未调用设备树节点没生效检查compatible、status、pinctrlWHO_AM_I不对SPI模式/频率/读写位改Mode 0降频检查地址最高位数据全零芯片睡眠复位后清PWR_MGMT_1的SLEEP位数据全零寄存器没写入写完后回读确认数据乱跳量程/scale不匹配核对配置和换算系数中断不触发GPIO配置错用gpio命令翻转验证引脚功能写在最后的一点经验这次移植真正写驱动代码的时间其实不到一小时剩下的大半个下午全都耗在内核版本缺SPI路径和设备树引脚配错上。我个人现在形成了一套固定的操作习惯凡是拿到新板子移植驱动第一步永远是去内核源码目录里翻有没有同系列芯片的现成驱动没有就找厂商SDK里的驱动实在不行才轮到自己写设备树相关的任何引脚宏都必须从当前内核的dts、头文件里现查现用网上抄来的代码不管看起来多对都要先验证宏存在、引脚复用值正确。最后再分享一个调试技巧在设备树节点里故意写一个错误的compatible加载驱动后如果dmesg里有drvprobe fail之类的提示至少能确认设备树解析和驱动匹配的链路是通的。这个反向验证的办法我在很多诡异故障里都用过比瞎猜快得多。希望这篇调试笔记能让你的ICM20608移植之路少走几个弯路。