☰
手写ASM330LHH驱动:从寄存器到IIO子系统的完整实践
2026/10/3 15:59:15 网站建设 项目流程

1. 为什么我放着现成的st_sensors不用,偏要自己写一次ASM330LHH驱动

ASM330LHH这颗芯片,ST官方归类是车规级六轴惯性传感器,加速度计加陀螺仪二合一,同系列的还有LSM6DSO、LSM6DSR这些消费级/工业级型号。如果你在嵌入式Linux环境里做开发,第一反应肯定是:内核里不是已经有st_sensors的驱动了吗?设备树里配一下就能出数据,何必自己写?

这个问题的答案,恰好是这篇学习笔记的核心。我个人的建议是:能用内核现成驱动就直接用,但有一个前提——你必须自己能看懂它,并且能在它“失灵”的时候徒手写出替代方案。真正上手过一遍驱动编写,你对IIO子系统、SPI/I2C总线模型、设备树匹配机制、传感器寄存器级的操作逻辑,会有一个完全不一样的理解。尤其ASM330LHH这种带硬件FIFO、支持多种中断输出、还额外集成温度传感器和机器学习内核的器件,嵌套在st_sensors框架下写一遍,比单纯调用ioctl读十个角度值要值得多。

这篇笔记不是从零开始教你“什么是I2C”,而是记录我完整地走了一遍“读手册—搭框架—写寄存器—配中断—调FIFO—验证数据”的流程,中间踩了若干坑,最后还跟ST官方驱动做了对照。阅读对象建议是已经能跑通嵌入式Linux最小系统、会看原理图、至少写过一次字符设备驱动的朋友。如果是纯Linux小白,建议先把设备树、platform_driver、i2c_client这几块基础补上再回来看。

顺便说一句,我这次的使用场景是给一台AGV小车做姿态反馈,要求不高,主要输出角速度和线加速度,后续打算叠加航向推算。所以我重点关心三个东西:能不能保证1kHz的角速度采样、FIFO的批量读取是否稳定、以及长时间运行后有没有数据丢帧。这些点也会贯穿全文。

2. 读懂ASM330LHH的关键参数和内部结构,比抄代码重要十倍

2.1 硬件层面的第一印象:这是一颗为“动态场合”设计的芯片

先看一组参数:ASM330LHH的加速度计支持±2/±4/±8/±16 g四挡满量程,陀螺仪支持±125/±250/±500/±1000/±2000/±4000 dps六挡。ODR最高能到6.66 kHz,但实际应用里陀螺仪1.66 kHz、加速度计1.66 kHz已经非常够用。噪声密度方面,陀螺仪典型值在4 mdps/√Hz上下,加速度计典型值在60 μg/√Hz左右,比上一代LSM6DSL数据好一些,这就是车规级和消费级拉开差距的地方。

最容易被忽略的一个指标是正交误差,手册里给的是陀螺仪XY/Z轴间0.05°以内,这意味着做姿态融合时,坐标系的转换矩阵偏差较小,融合算法里少一个需要额外标定的量。如果你只是拿它测个振动,这个参数确实无所谓;但你要做的是IMU积分,那初始的轴对准误差会直接变成姿态角漂移,所以选型时我特别留意了这个。

还有一个不能忽视的硬件细节:ASM330LHH的封装是LGA-14L(3.5×3.5×1 mm³),本质上跟LSM6DSO是同一个封装家族。这带来一个直接好处——很多用于LSM6DSO的转接板、封装库、参考设计可以直接复用到ASM330LHH上。我在画测试板时直接用了一片带LSM6DSO封装的转接板,省了不少事。

2.2 寄存器地图:别背,但要知道“那几个关键寄存器”在哪里

驱动开发的本质,就是跟寄存器对话。ASM330LHH的寄存器布局跟ST惯导系列基本一致,我实际调试过程中用到的核心寄存器就这几个,列出来供大家有个整体印象:

地址名称功能说明
0x0FWHO_AM_I设备ID,固定值0x6B
0x10CTRL1_XL加速度计ODR、满量程、数字滤波配置
0x11CTRL2_G陀螺仪ODR、满量程、数字滤波配置
0x12CTRL3_CSPI/I2C接口配置、寄存器地址自增等
0x13CTRL4_C中断引脚功能选择、数据准备中断使能
0x14CTRL5_C陀螺仪低功耗/高精度模式
0x15CTRL6_C加速度计低功耗/高精度模式
0x19CTRL10_CFIFO、传感器数据路由、外部时钟
0x1ESTATUS_REG各通道数据就绪标志
0x20OUT_TEMP_L温度数据低字节
0x22OUTX_L_G陀螺仪X轴低字节,后面连续是Y/Z
0x28OUTX_L_A加速度计X轴低字节,后面连续是Y/Z
0x3A~0x3DFIFO_STATUS1~4FIFO当前已存数据条目数、FIFO水印状态等
0x78FIFO_DATA_OUT_TAG读出FIFO数据时附带的数据标签
0x79~0x7EFIFO_DATA_OUT_X_L等连续存放从FIFO弹出的原始数据

手册里有两百多个寄存器位,但实际一个基础驱动用到的不超过三十个。我的建议是:先把WHO_AM_I、两个主控制寄存器、状态寄存器、六个输出寄存器搞清楚,其他用到再翻。这一条对任何传感器驱动都适用。

2.3 两种通信接口选SPI还是I2C

ASM330LHH同时支持I2C和SPI。I2C地址由SA0引脚电平决定,SA0接地时7位地址是0x6A,接高电平时是0x6B。SPI最高可以跑10 MHz,I2C标准模式下最高400 kHz。

做AGV姿态感知这种实时性要求略高的场合,我推荐SPI,理由有三个:

  • SPI没有I2C那种应答/仲裁机制,连续读取六个寄存器时波形干净,不容易被其他I2C设备干扰;
  • ASM330LHH的SPI支持最高10 MHz,同样数据量下留给CPU的调度余量更多;
  • 设备数量少的话,SPI片选直连GPIO,时序控制起来更明确。

当然I2C也有它的优势,接线少、调试简单,如果用现成的IMU评估板,I2C几乎零成本就能跑起来。这次调试我先用I2C验证了寄存器读写,确认ID和初始数据流正常之后,才切换到SPI跑正式的数据采集,分两步走能明显降低排错难度。

3. 驱动整体框架:IIO子系统、platform_driver和“我是谁”

3.1 字符设备?输入子系统?IIO?到底挂哪里

Linux下驱动一个传感器,有几种挂载方式:

  • 传统的字符设备驱动,自己在/dev/下创建一个节点,用户程序用open/read/ioctl操作;
  • 输入子系统(input subsystem),适合按键、触摸屏、遥控器这类以事件为语义的设备;
  • IIO子系统(Industrial I/O),专门为工业传感器设计,支持加速度计、陀螺仪、磁力计、光传感器、ADC等,提供sysfs接口和trigger缓冲机制。

ASM330LHH这类六轴IMU,正确的归宿就是IIO。IIO不但帮你把数据暴露成了/sys/bus/iio/devices/iio:deviceX这样的标准接口,还自带trigger、buffer、event这几个抽象层次。用IIO框架写驱动,可以少写很多样板代码,同时后续如果用户态要用libiio或者Python的pylibiio,完全无缝衔接。

3.2 搭框架:probe函数里做什么

一个最小的IIO传感器驱动,骨架大概长这样:

static const struct iio_info asm330lhh_info = { .read_raw = asm330lhh_read_raw, .write_raw = asm330lhh_write_raw, }; static int asm330lhh_probe(struct spi_device *spi) { struct iio_dev *indio_dev; struct asm330lhh_data *data; int ret; data = devm_kzalloc(&spi->dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; >static int asm330lhh_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask) { struct asm330lhh_data *data = iio_priv(indio_dev); int ret; switch (mask) { case IIO_CHAN_INFO_RAW: ret = asm330lhh_read_axis(data, chan->address, val); if (ret < 0) return ret; return IIO_VAL_INT; case IIO_CHAN_INFO_SCALE: /* 尺度因子,由满量程寄存器决定 */ *val = 0; *val2 = asm330lhh_get_scale(chan,>&ecspi2 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi2>; cs-gpios = <&gpio5 13 GPIO_ACTIVE_LOW>; status = "okay"; asm330lhh: asm330lhh@0 { compatible = "st,asm330lhh"; reg = <0>; spi-max-frequency = <5000000>; interrupt-parent = <&gpio1>; interrupts = <7 IRQ_TYPE_EDGE_RISING>; // 供电、复位脚等其他属性按实际原理图补充 }; };

有几个细节值得展开说一下。

第一,spi-max-frequency我写的是5 MHz,芯片能力是10 MHz,但考虑到PCB走线还有转接板,先降到5 MHz能显著减少波形边沿的振铃问题。等验证全部通过再往上拉,也是一种“性能回退优先”的调试思路。

第二,如果同时用I2C功能测试,I2C设备树节点和SPI节点二选一,不能同时把同一个物理器件挂两条总线。虽然内核不至于崩溃,但两套驱动会疯狂抢设备,造成各种随机错误。我的办法是先屏蔽SPI节点,设备树改为I2C方式,等I2C通了再把SPI节点放开。

第三,中断类型我用的IRQ_TYPE_EDGE_RISING,对应的是数据准备好时INT引脚从低拉高的电平跳变。如果后面改FIFO水印中断,可能要用IRQ_TYPE_LEVEL_HIGH之类,这取决于你配置的是“数据就绪”还是“FIFO水位”事件。这个看似不起眼的点,会直接影响驱动收到的中断号是否有效。

5.2 编译、加载与第一个有效数据

写完驱动后,编译以模块方式加载:

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules cp asm330lhh.ko /srv/nfs/rootfs/root/ # 目标板上 insmod asm330lhh.ko dmesg | tail -20

如果probe成功,dmesg里应该能看到挂载信息,然后在/sys/bus/iio/devices/下出现新的设备目录:

ls /sys/bus/iio/devices/ # 假设是 iio:device2 cat /sys/bus/iio/devices/iio:device2/in_accel_x_raw cat /sys/bus/iio/devices/iio:device2/in_gyro_z_raw

别急着写c程序,先用命令验证是最快的。手头没有iio_attr/iio_readdev这些工具时,纯用shell也能测试。不过这里要提醒:在第一次读到非零数据后,先检查数据的噪声水平。静止状态下,加速度计X/Y轴应该接近0,Z轴应该接近1g对应的raw值(比如±2g量程下大约是16384),陀螺仪三轴应该在几十个LSB以内波动。如果出现某个轴数据特别大或者不断跳变,先怀疑是不是配置错了量程,而不是算法问题。

5.3 中断驱动的FIFO读取和批量数据流

设备树里配了中断,驱动里就用request_threaded_irq来处理数据就绪中断。每次中断来临,说明FIFO里已经有数据,这时候我选择一次性把FIFO中所有条目读出来。

读取的流程大概是:

static irqreturn_t asm330lhh_irq_handler(int irq, void *private) { struct iio_dev *indio_dev = private; struct asm330lhh_data *data = iio_priv(indio_dev); uint8_t status; int count; /* 先看状态寄存器,确认是不是FIFO水印触发 */ asm330lhh_read_reg(data, ASM330LHH_REG_STATUS, &status); if (!(status & ASM330LHH_STATUS_FIFO_THRESHOLD)) return IRQ_NONE; /* 读取FIFO中已存储的样本数量 */ count = asm330lhh_read_fifo_count(data); /* 从0x78开始连续读,每个样本包含 tag + 6路原始数据 */ asm330lhh_read_fifo(data,>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询