
1. 项目概述当IIO驱动遇上Regmap API最近在为一个嵌入式项目调试六轴传感器选型是InvenSense的ICM20608。这玩意儿是个好东西集成了三轴陀螺仪和三轴加速度计在无人机、姿态检测这些领域用得挺多。但真正动手写它的Linux内核驱动时我发现了一个比直接操作SPI寄存器更有意思的路径利用内核的Regmap API来构建一个标准的IIOIndustrial I/O驱动。如果你还在用i2c_transfer或者spi_write_then_read这类底层函数手动拼凑传感器驱动那这篇文章可能会给你打开一扇新窗户。它不仅仅是“读取数据”而是关于如何用现代内核提供的框架更安全、更高效、更维护性地把传感器接入系统。为什么是IIO和RegmapIIO是Linux内核为模拟数字转换器ADC、陀螺仪、加速度计等传感器量身定制的子系统。它统一了用户空间的访问接口通过/sys/bus/iio/devices/iio:deviceX提供了标准化的数据格式和事件处理。而Regmap API则抽象了对硬件寄存器的访问无论是I2C、SPI还是内存映射IO它都提供了一套统一的读写、缓存、同步机制。把这两者结合起来意味着你的驱动代码可以大幅简化把精力从繁琐的通信协议和字节操作中解放出来专注于传感器本身的业务逻辑比如量程配置、数据解析和触发模式。对于ICM20608这种寄存器数量适中、功能清晰的传感器这套组合拳尤其高效。2. 核心思路为什么选择Regmap IIO框架在动手写代码之前得先想清楚为什么不用更“直接”的方法。很多嵌入式工程师的第一个驱动可能就是打开一个SPI设备文件然后配置模式、频率接着用read和write去操作。或者在内核里用spi_sync_transfer。这样做当然能跑起来但它有几个明显的短板一是错误处理很原始二是缺乏寄存器缓存可能频繁读取同一个状态寄存器三是代码和具体的SPI控制器绑定较深换一个通信总线比如I2C就得重写大部分逻辑。Regmap API就是为了解决这些问题而生的。你可以把它想象成一个硬件寄存器的“管家”。你告诉它“这个传感器在SPI总线1上它的寄存器是8位宽的最大时钟频率是10MHz。” 然后你就可以用regmap_read和regmap_write这样的函数去访问寄存器完全不用关心底层是SPI的CS拉低、发送指令字节、接收数据这些细节。Regmap内部会帮你管理传输锁、处理字节序、甚至实现寄存器缓存对于只读或频繁读的寄存器可以避免实际硬件访问。当你的传感器同时支持I2C和SPI时Regmap的优势就更大了核心的寄存器操作逻辑可以完全复用只需要提供不同的配置表regmap_config。IIO框架则是传感器数据的“搬运工”和“翻译官”。它定义了一套完整的模型一个物理传感器是一个iio_dev它包含若干个iio_chan_spec通道比如ICM20608的X/Y/Z轴加速度和角速度。驱动负责向IIO核心注册这个设备并实现一些回调函数比如read_raw从硬件读取原始数据、write_raw配置量程等。然后IIO子系统就会自动在sysfs下创建出对应的文件用户空间程序通过读取这些文件比如in_accel_x_raw就能获取到采样值。IIO还支持硬件触发比如用传感器自己的数据就绪中断来启动读取和缓冲区内核用于高速连续采样功能非常强大。所以整个驱动的架构就清晰了我们用Regmap作为与ICM20608芯片寄存器通信的抽象层用IIO框架作为向Linux系统暴露传感器数据的接口层。驱动代码的主体就是搭建起这两者之间的桥梁。3. 驱动设计拆解从设备树到IIO设备3.1 设备树DTS节点定义一切始于设备树。这是告诉内核硬件连接方式的“地图”。对于挂载在SPI1总线上的ICM20608我们需要在相应的板级DTS文件中添加一个节点。spi1 { status okay; cs-gpios gpio 10 GPIO_ACTIVE_LOW; /* 假设使用GPIO10作为片选 */ icm20608: icm206080 { compatible invensense,icm20608; reg 0; /* SPI片选编号 */ spi-max-frequency 10000000; /* 最大SPI时钟频率10MHz */ interrupt-parent gpio; interrupts 11 IRQ_TYPE_EDGE_RISING; /* 数据就绪中断引脚 */ vdd-supply vdd_3v3; /* 电源 */ vddio-supply vdd_3v3; /* IO口电源 */ }; };这里有几个关键点compatible属性这是驱动匹配的“钥匙”。我们的驱动里需要有一个of_device_id表包含这个字符串。spi-max-frequency必须设置且不能超过芯片手册规定的最大值ICM20608典型为10MHz。interrupts强烈建议使用中断模式。ICM20608的数据就绪DRDY引脚可以连接到主控的一个GPIO上配置为上升沿触发。使用中断可以避免轮询大幅降低CPU占用并在数据新鲜时立即读取。vdd-supply和vddio-supply这是可选的但推荐使用。它利用内核的电源管理框架Regulator来管理传感器供电在系统挂起时可以自动断电。注意SPI的片选CS引脚既可以通过cs-gpios指定一个GPIO来模拟软件片选也可以使用SPI控制器硬件自带的片选线。对于ICM20608其SPI模式支持标准3线或4线带数据就绪中断。如果使用硬件片选通常reg属性就对应了控制器的硬件片选线编号此时可以省略cs-gpios。具体要看主控芯片的SPI控制器支持情况。3.2 驱动探测Probe函数流程当内核启动设备树被解析后compatible字符串匹配成功我们的驱动probe函数就会被调用。这里是驱动初始化的核心。static int icm20608_probe(struct spi_device *spi) { struct iio_dev *indio_dev; struct icm20608_data *data; struct regmap *regmap; int ret; /* 1. 分配IIO设备结构 */ indio_dev devm_iio_device_alloc(spi-dev, sizeof(*data)); if (!indio_dev) return -ENOMEM; data iio_priv(indio_dev); // 获取驱动私有数据指针 >/* 定义IIO通道 */ static const struct iio_chan_spec icm20608_channels[] { /* 加速度计通道 */ { .type IIO_ACCEL, // 类型加速度 .modified 1, // 使用modifier修饰 .channel2 IIO_MOD_X, // 修饰为X轴 .info_mask_separate BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE), // 独立的属性原始值和比例因子 .address ICM20608_REG_ACCEL_XOUT_H, // 寄存器起始地址可选可用于私有读取函数 .scan_index 0, // 在缓冲区内核中的索引 .scan_type { // 扫描类型用于缓冲区内核 .sign s, // 有符号数 .realbits 16, // 有效位数 .storagebits 16, // 存储位数 .shift 0, // 移位 .endianness IIO_BE, // 字节序大端ICM20608数据寄存器是高字节在前 }, }, /* 为Y轴、Z轴加速度和X/Y/Z轴陀螺仪定义类似的通道... */ { .type IIO_ANGL_VEL, // 类型角速度 .modified 1, .channel2 IIO_MOD_X, .info_mask_separate BIT(IIO_CHAN_INFO_RAW) | BIT(IIO_CHAN_INFO_SCALE), .address ICM20608_REG_GYRO_XOUT_H, .scan_index 3, .scan_type { .sign s, .realbits 16, .storagebits 16, .shift 0, .endianness IIO_BE, }, }, }; /* 定义IIO信息结构包含操作回调 */ static const struct iio_info icm20608_info { .read_raw icm20608_read_raw, // 用户读取sysfs时调用 .write_raw icm20608_write_raw, // 用户写入sysfs时调用如设置量程 .debugfs_reg_access icm20608_debugfs_reg_access, // 调试用允许通过debugfs访问寄存器 };icm20608_read_raw是核心回调函数。当用户在用户空间执行cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw时内核最终会调用到这个函数。static int icm20608_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask) { struct icm20608_data *data iio_priv(indio_dev); __be16 raw_data; // 注意字节序ICM20608输出大端数据 int ret; switch (mask) { case IIO_CHAN_INFO_RAW: { // 请求读取原始值 mutex_lock(data-lock); /* 使用Regmap读取两个字节的数据寄存器 */ ret regmap_bulk_read(data-regmap, chan-address, raw_data, 2); mutex_unlock(data-lock); if (ret 0) return ret; /* 将大端16位数据转换为主机字节序并赋值 */ *val (short)be16_to_cpu(raw_data); // 注意转换为有符号short return IIO_VAL_INT; // 返回值类型为整数 } case IIO_CHAN_INFO_SCALE: { // 请求读取比例因子即每个LSB对应的物理量 /* 根据当前配置的量程返回比例因子。 例如加速度计量程为±2g时灵敏度为16384 LSB/g。 那么比例因子就是 (1 / 16384) g/LSB。 为了高精度IIO期望返回两个整数*val 和 *val2。 通常表示为 *val 1, *val2 16384 即 1/16384。 */ *val 0; *val2 icm20608_get_scale(data, chan-type); // 一个根据配置返回比例因子的函数 return IIO_VAL_INT_PLUS_MICRO; // 返回值类型为整数微小数 } default: return -EINVAL; } }可以看到在读取原始数据的核心路径上我们只用了一行regmap_bulk_read就完成了从指定寄存器地址读取2字节数据的操作。Regmap帮我们处理了SPI通信的所有细节包括片选控制、指令字节对于SPI通常是寄存器地址最高位置1表示读操作的拼接。代码简洁且安全。4. 关键实现细节与避坑指南4.1 SPI模式与通信时序ICM20608的SPI模式是Mode 0 (CPOL0, CPHA0)或Mode 3 (CPOL1, CPHA1)。在驱动中我们设置为SPI_MODE_0。这是最常见的SPI模式时钟空闲时为低电平在第一个时钟边沿采样数据。务必与硬件连接确认主控的SPI控制器模式必须与从设备一致。SPI通信的指令格式需要特别注意。对于大多数传感器包括ICM20608读操作时需要先发送一个8位的指令字节其中最高位bit 7为1表示读为0表示写低7位是寄存器地址。紧接着从设备会在后续的时钟周期里输出数据。regmap_bulk_read在SPI后端会自动处理这个格式它会将寄存器地址的最高位置1然后发起一个连续的读传输。对于多字节读取bulk_read它会保持片选有效连续发送时钟读取多个字节这比单字节读取效率高得多。实操心得在调试初期如果读回来的数据全是0xFF或0x00除了检查电源和接线第一要怀疑的就是SPI模式。可以用逻辑分析仪抓一下CLK,MOSI,MISO三条线的波形看时钟极性和相位以及指令字节是否正确。另一个常见错误是字节序ICM20608的数据寄存器是高字节在前大端而我们的主机CPU可能是小端所以必须用be16_to_cpu进行转换。4.2 电源管理与复位序列可靠的驱动必须处理好上电和复位。ICM20608有一个PWR_MGMT_1寄存器驱动在probe函数的icm20608_chip_init阶段需要执行以下操作解除设备复位如果之前处于复位状态。选择时钟源通常选择内部振荡器或PLL。唤醒加速度计和陀螺仪将它们从睡眠模式唤醒。static int icm20608_chip_init(struct icm20608_data *data) { int ret; /* 1. 复位设备 */ ret regmap_write(data-regmap, ICM20608_REG_PWR_MGMT_1, ICM20608_BIT_DEVICE_RESET); if (ret) return ret; msleep(100); // 等待复位完成手册要求至少100ms /* 2. 唤醒并选择时钟源 */ ret regmap_write(data-regmap, ICM20608_REG_PWR_MGMT_1, ICM20608_CLOCK_PLL_XGYRO); if (ret) return ret; usleep_range(50, 100); // 短暂延时 /* 3. 配置加速度计和陀螺仪的量程、带宽等 */ ret regmap_write(data-regmap, ICM20608_REG_ACCEL_CONFIG, ICM20608_ACCEL_FS_SEL_2G); if (ret) return ret; ret regmap_write(data-regmap, ICM20608_REG_GYRO_CONFIG, ICM20608_GYRO_FS_SEL_250DPS); if (ret) return ret; /* 4. 配置中断 */ ret regmap_write(data-regmap, ICM20608_REG_INT_ENABLE, ICM20608_BIT_DATA_RDY_EN); return ret; }注意事项复位后的延时至关重要。数据手册明确要求复位后需要等待一段时间如100ms才能访问寄存器。忽略这个延时会导致后续的配置写入失败。另外在系统进入睡眠Suspend时驱动应该在pm_ops的suspend回调中将传感器置于低功耗模式在恢复Resume时重新初始化。如果使用了Regulatorvdd-supply框架会自动管理电源驱动只需实现runtime PM的回调即可。4.3 中断处理与数据读取优化使用中断而非轮询是生产级驱动的标配。ICM20608的DRDY引脚在每次转换完成时会产生一个脉冲。在驱动中我们使用request_threaded_irq申请中断并分为顶半部icm20608_data_ready_irq_handler和底半部通过工作队列icm20608_data_ready_work_handler实现。顶半部中断处理函数应该尽可能快通常只做两件事确认中断源通过读取中断状态寄存器然后调度一个底半部处理如工作队列、tasklet或软中断。在底半部中再进行可能耗时的操作比如读取所有6轴的数据共12字节进行必要的转换然后通过IIO缓冲区接口或直接推送到用户空间。static irqreturn_t icm20608_data_ready_irq_handler(int irq, void *private) { struct iio_dev *indio_dev private; struct icm20608_data *data iio_priv(indio_dev); /* 可以快速读取中断状态寄存器确认这里简化处理 */ schedule_work(data-data_ready_work); return IRQ_HANDLED; } static void icm20608_data_ready_work_handler(struct work_struct *work) { struct icm20608_data *data container_of(work, struct icm20608_data, data_ready_work); __be16 raw_values[6]; // 存储6个轴的原始数据 int ret; mutex_lock(data-lock); /* 一次性读取所有数据寄存器地址从ACCEL_XOUT_H开始连续12个字节 */ ret regmap_bulk_read(data-regmap, ICM20608_REG_ACCEL_XOUT_H, raw_values, sizeof(raw_values)); mutex_unlock(data-lock); if (ret 0) { dev_err(data-spi-dev, Failed to read data\n); return; } /* 处理数据转换字节序存储到缓冲区或触发IIO事件 */ for (int i 0; i 6; i) { >static int icm20608_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int val, int val2, long mask) { struct icm20608_data *data iio_priv(indio_dev); int reg, fs_sel; if (mask ! IIO_CHAN_INFO_SCALE) return -EINVAL; switch (chan-type) { case IIO_ACCEL: /* 根据传入的val/val2计算并匹配最接近的量程 */ fs_sel icm20608_find_accel_fs(val, val2); reg ICM20608_REG_ACCEL_CONFIG; break; case IIO_ANGL_VEL: fs_sel icm20608_find_gyro_fs(val, val2); reg ICM20608_REG_GYRO_CONFIG; break; default: return -EINVAL; } mutex_lock(data-lock); /* 使用Regmap更新配置寄存器 */ regmap_write(data-regmap, reg, fs_sel); /* 更新驱动内部记录当前量程和灵敏度的变量 */ >