
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-14L3.5×3.5×1 mm³本质上跟LSM6DSO是同一个封装家族。这带来一个直接好处——很多用于LSM6DSO的转接板、封装库、参考设计可以直接复用到ASM330LHH上。我在画测试板时直接用了一片带LSM6DSO封装的转接板省了不少事。2.2 寄存器地图别背但要知道“那几个关键寄存器”在哪里驱动开发的本质就是跟寄存器对话。ASM330LHH的寄存器布局跟ST惯导系列基本一致我实际调试过程中用到的核心寄存器就这几个列出来供大家有个整体印象地址名称功能说明0x0FWHO_AM_I设备ID固定值0x6B0x10CTRL1_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/Z0x28OUTX_L_A加速度计X轴低字节后面连续是Y/Z0x3A~0x3DFIFO_STATUS1~4FIFO当前已存数据条目数、FIFO水印状态等0x78FIFO_DATA_OUT_TAG读出FIFO数据时附带的数据标签0x79~0x7EFIFO_DATA_OUT_X_L等连续存放从FIFO弹出的原始数据手册里有两百多个寄存器位但实际一个基础驱动用到的不超过三十个。我的建议是先把WHO_AM_I、两个主控制寄存器、状态寄存器、六个输出寄存器搞清楚其他用到再翻。这一条对任何传感器驱动都适用。2.3 两种通信接口选SPI还是I2CASM330LHH同时支持I2C和SPI。I2C地址由SA0引脚电平决定SA0接地时7位地址是0x6A接高电平时是0x6B。SPI最高可以跑10 MHzI2C标准模式下最高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: asm330lhh0 { 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 ARCHarm64 CROSS_COMPILEaarch64-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轴应该接近0Z轴应该接近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,>