ST传感器驱动移植到龙芯平台:Linux内核IIO与设备树实战
2026/9/9 13:28:55 网站建设 项目流程

1. 项目背景与整体思路拆解

先交代一下背景。我们组叫“走马观碑”,平时主要做嵌入式底层的活儿,手头有一块龙芯平台的开发板,处理器架构是LoongArch,跑的是Linux内核。这次的任务说起来一句话:把一套基于ST(意法半导体)芯片的驱动方案,整体移植到龙芯平台上。

很多人一听“ST驱动移植”,第一反应是“STM32的HAL库搬过去”。其实完全不是一回事。STM32是裸机MCU,跑的是HAL库或者标准外设库,压根没有Linux内核这层概念。而我们这次说的ST驱动,指的是ST出的一系列传感器芯片的Linux内核驱动,比如LIS3DH加速度计、LSM6DSO惯性测量单元、HTS221温湿度传感器这类,都是I2C或SPI接口的Motion MEMS传感器。这些芯片在消费电子和工业设备里出镜率极高,ST官方在Linux内核里也维护了一批对应的驱动代码,放在drivers/iio/imu、drivers/iio/accel这些目录下。

那为什么还要“移植”?直接编进内核不就行了?问题就出在“平台”两个字上。内核里现成的ST驱动虽然存在,但它们是挂在I2C/SPI子系统之上的IIO驱动,依赖具体的总线控制器驱动、依赖设备树描述、依赖中断控制器,还依赖内核里一堆Kconfig依赖项——而龙芯平台默认的发行版内核,往往没有打开IIO子系统,也没有对应的I2C控制器驱动适配,更别说设备树里根本没有描述这些传感器。所以这个“移植”,实际上要做四件事:内核配置项的裁剪与开启、设备树节点编写、驱动依赖项的补齐、以及部分头文件与平台宏的适配。

我们把方案定成了这样:以一颗ST的LSM6DSO(六轴IMU)为切入点,先打通I2C通路,再把IIO子系统整个拉起来,最后接入一个实际的读取程序做验证。因为IMU是ST传感器家族里最典型的代表,跑通了它,HTS221、LIS3MDL这些兄弟芯片基本都是同一套路,换个compatible字符串和初始化参数就能起来。下面把每一步的细节摊开讲。

2. 移植前准备:环境、工具链与内核源码

2.1 龙芯平台的交叉编译环境

先说环境。龙芯的LoongArch架构目前有三套比较主流的编译工具链:一套是龙芯官方维护的Loongnix工具链,另一套是社区基于GCC上游做的LoongArch分支,还有一套是直接用发行版自带的交叉编译器。我们这次用的是Loongnix的交叉工具链,版本是gcc 8.3.0,配套的sysroot里带了glibc和内核头文件。

安装没什么特殊的地方,解压到/opt目录,配置一下环境变量就能用。关键是确认两件事:第一,工具链的前缀;第二,编译出来的内核能直接在目标板上跑。

export PATH=/opt/loongson-gnu-toolchain-8.3.0-2020.12/bin:$PATH export ARCH=loongarch export CROSS_COMPILE=loongarch64-linux-gnu-

这里有个坑:ARCH必须写成loongarch而不是loongson或者mips。早期龙芯是MIPS架构,内核里叫ARCH=mips,后来LoongArch独立出来了,有些参考资料还是旧的MIPS写法,照抄会直接编译出一堆晦涩的报错。我第一次就踩了这坑,编出来的内核在目标板上压根不启动,串口一点反应没有,后来才反应过来是架构选错了。

2.2 内核源码选型与配置

源码选的是龙芯开源社区的linux内核分支,基于内核5.10版本。选5.10的原因很现实:稳定、维护时间长、社区支持好,而且IIO子系统和ST传感器驱动在这个版本里已经非常成熟了。太新或太旧都有问题——太旧的没有LSM6DSO的支持(这个芯片的驱动是在4.19之后才合入上游的),太新的又可能跟龙芯平台的板级补丁冲突。

拿到源码后先不着急配置,先确认默认配置里有没有打开I2C控制器的驱动。龙芯平台的I2C控制器有两种:一种是芯片内部的I2C控制器,直接挂在SoC上;另一种是通过GPIO模拟的I2C,也就是i2c-gpio。我们这块板子引出的是SoC内部的I2C0控制器,对应内核配置项是I2C_LOONSON。

make loongson_defconfig make menuconfig

在menuconfig里需要确认几个关键配置项:

  • Device Drivers -> I2C support -> I2C device interface 打开
  • Device Drivers -> I2C support -> Autoselect pertinent helper modules 打开
  • Device Drivers -> Industrial I/O support -> I2C/SPI sensor drivers 打开

尤其要注意IIO这个一级菜单,它在menuconfig里的位置在“Industrial I/O support”,默认是关闭的。如果不开这个,后面ST驱动就算代码在源码树里,也根本不会被编译——你连menuconfig里都搜不到相关的选项。

2.3 目标板上的确认手段

环境准备得再充分,最后还得落到目标板上验证。我们这块龙芯板子带了串口调试口和网口,系统跑起来之后,第一件事就是确认I2C总线是否注册成功。

ls /dev/i2c-*

如果有/dev/i2c-0,说明I2C控制器驱动已经加载了。再用i2cdetect扫一下总线:

i2cdetect -y 0

这块板子的I2C0总线上挂了一颗LSM6DSO,地址是0x6a(SA0引脚拉高)。i2cdetect扫出来的结果应该在第69列(16进制的0x6a)位置显示器件编号。如果显示“UU”,说明地址被某个驱动占用了;如果显示“--”,说明该地址上没有设备响应。这两个状态都值得注意:UU说明驱动在设备树里已经被枚举到了,但是可能没正确匹配;--说明要么地址不对,要么I2C时序有问题,要么设备压根没上电。

提示:如果i2cdetect工具没装,可以用busybox的i2cdetect命令代替。但busybox版本功能比较弱,只能扫描不能读写寄存器,调试起来很不方便,建议直接装i2c-tools。

3. 驱动移植的核心:设备树与Kconfig

3.1 设备树节点的编写

Linux内核驱动的枚举完全依赖设备树。ST传感器在设备树里的描述方式很统一,以LSM6DSO为例,标准写法是这样的:

&i2c0 { status = "okay"; clock-frequency = <400000>; lsm6dso@6a { compatible = "st,lsm6dso"; reg = <0x6a>; interrupt-parent = <&gpio>; interrupts = <4 IRQ_TYPE_LEVEL_HIGH>; vdd-supply = <&vdd_io>; vddio-supply = <&vdd_io>; }; };

这里有几个关键点要展开说明。

第一,compatible字段必须跟驱动源码里的of_match_table完全一致。ST官方驱动里定义的匹配名是"st,lsm6dso",如果你在设备树里写成了"lsm6dso"或者"st_lsm6dso",驱动根本认不出来。这个字段是字符串精确匹配,差一个字符都不行。查内核源码drivers/iio/imu/st_lsm6dsx/st_lsm6dsx_core.c文件,你会看到这样一个数组:

static const struct of_device_id st_lsm6dsx_of_match[] = { { .compatible = "st,lsm6dso", .data = &st_lsm6dsx_sensor_settings[ST_LSM6DSO_ID], }, ... };

第二,reg字段跟芯片的I2C地址对应。LSM6DSO的I2C地址由SA0引脚决定:SA0接高电平时地址是0x6a,接低电平时是0x6b。如果你的设备树地址和实际硬件不匹配,驱动probe阶段会报ENXIO错误,查起来非常费劲。有一种排查技巧:直接把reg改成一个肯定错误的地址,比如0x00,看报错信息是否变化,以此确认是不是地址问题。

第三,interrupt字段不是必需的,但不配的话驱动会降级为轮询模式。IMU这种传感器,中断模式比轮询模式省电得多,而且数据更新延迟更低。LSM6DSO的INT1引脚可以配置为数据准备好中断,设备树里通过interrupts属性指定GPIO中断源。这个中断号一定要跟硬件实际连接的GPIO对上,否则驱动虽然能注册成功,但永远收不到中断,读取数据时会一直阻塞在等待队列上——表现就是应用层读数据超时。

第四,vdd-supply和vddio-supply是两个电源域的描述。ST的传感器往往有模拟电源和数字IO电源两个供电引脚,在设备树里通过regulator框架描述。如果目标板上这两个电源是直连的常开电源,可以不用写这两个属性,但写了更规范,将来做电源管理时能省不少事。

3.2 内核配置项:IIO子系统与ST驱动

设备树写好了,接下来要确保内核配置里把对应的驱动选项打开。LSM6DSO对应的配置项是CONFIG_ST_LSM6DSX,它在menuconfig里的路径是:

  • Device Drivers -> Industrial I/O support -> Pressure sensors -> ST LSM6DSX driver

这里的菜单分类有点迷惑性,LSM6DSO虽然是IMU,但它被归在Pressure sensors而不是IMU下面。原因很历史:ST_LSM6DSX驱动的代码文件在drivers/iio/imu/st_lsm6dsx/下面,但menuconfig里它的parent是IIO的传感器分类目录,最早维护者把它放在了Pressure里,后来也没人挪。我一开始在“IMU”分类里死活找不到这个选项,后来才搞清楚。

如果不想在menuconfig里翻来翻去,可以直接改.config文件:

CONFIG_IIO=y CONFIG_ST_LSM6DSX=y CONFIG_ST_LSM6DSX_I2C=y

这里要注意:CONFIG_ST_LSM6DSX是核心驱动,CONFIG_ST_LSM6DSX_I2C是I2C传输层的封装。两个都要打开,而且CONFIG_ST_LSM6DSX_I2C依赖CONFIG_ST_LSM6DSX。如果你只开了核心驱动,编译时会发现st_lsm6dsx_core.o是编出来的,但st_lsm6dsx_i2c.o没有编,设备树里无论如何匹配都不会probe成功。

3.3 registe平台差异:LoongArch的I2C控制器适配

设备树和设备驱动都准备好了,还有一层“隐形”的适配工作:龙芯SoC的I2C控制器驱动可能跟标准内核有差异。LoongArch的I2C控制器驱动代码一般在drivers/iio/i2c/busses/i2c-loongson.c,这个文件在外围设备支持里属于比较“小众”的部分,合并到上游的时间晚,而且不同龙芯型号的寄存器布局还不完全一样。

我们这块板子在I2C probe的时候遇到过一个老熟人:时钟频率不匹配。龙芯I2C控制器的时钟源是SoC内部的APB时钟,频率是100MHz,但驱动里默认按50MHz计算分频系数,导致实际I2C时钟跑到了800kHz。而LSM6DSO的I2C时序要求最高支持400kHz(FM模式),超出规格后设备响应不稳定,表现就是“有时候能读到WHO_AM_I,有时候读不到”。

解决方法是直接改i2c-loongson.c里的时钟源频率定义:

#define LS2K_I2C_CLK 100000000

改完重新编译内核,再用示波器量一下SCL引脚的频率,确认在400kHz以内。有示波器的话这一步一定要实测,不要只看代码里的计算值。分频器的整数舍入可能会导致实际频率跟理论值有偏差,宁可配慢一点也不要超越芯片规格。

4. 从零开始移植:实操流程与关键技术点

4.1 第一步:验证I2C底层的读写通道

在碰驱动之前,先用i2c-tools手工读写一下传感器寄存器,确认物理链路是通的。LSM6DSO的WHO_AM_I寄存器地址是0x0F,复位之后读出来的值应该是0x6C。

i2cget -y 0 0x6a 0x0f

如果返回0x6c,说明I2C通路没问题、芯片正常工作、设备树里的reg地址也写对了。如果返回别的值或者报错,就得返回去查硬件连接和I2C控制器配置。这一步虽然简单,但能帮你把问题范围缩小一大半——物理层的问题绝不要带进驱动调试阶段。

WHO_AM_I验证通过之后,还要把一个control寄存器复位一下,确保芯片处于默认状态。LSM6DSO的CTRL3_C寄存器地址是0x12,bit0是软件复位位,写1然后等待芯片自动清零:

i2cset -y 0 0x6a 0x12 0x01 sleep 0.1 i2cget -y 0 0x6a 0x12

读回来如果bit0已经变成0,说明复位完成。这个操作的意义在于把芯片内部状态清干净——板子在上电过程中I2C总线可能会产生毛刺,导致芯片进入非预期的配置状态。

4.2 第二步:添加设备树节点并重新编译内核

设备树节点内容参考3.1节的写法,修改目标板对应的dts文件。龙芯平台的dts路径一般在arch/loongarch/boot/dts/loongson/下面,具体文件名跟板型有关。我们这块板子的dts是ls2k1000.dts,对应的,在根节点下找到i2c0节点,按前面的格式添加子节点。

设备树编译有两种方式:独立编译成dtb文件,或者直接编进内核镜像。我们用的是前者,因为调试阶段分开编译更方便替换:

make dtbs

编译生成的dtb文件一般在arch/loongarch/boot/dts/loongson/目录下。把它拷到目标板的/boot目录,同时备份原来的dtb文件——这个习惯很重要,改坏了马上能恢复。

4.3 第三步:编译内核与模块

如果前面把CONFIG_ST_LSM6DSX配置成了y(内建),直接编译内核:

make -j8 vmlinux

如果配置成m(模块),就单独编译IIO子系统和ST驱动模块:

make modules make modules_install INSTALL_MOD_PATH=/tmp/rootfs

我个人建议调试阶段用模块方式,因为在板子上insmod/rmmod比反复重刷内核快得多。开发稳定之后再改成y内建,减少启动时加载模块的依赖顺序问题。

有一点注意:模块编译必须有内核源码树,而且内核源码树必须是.config完整的那一份。如果你之前clean过,或者用git stash把源码状态改了,编译模块时会报“version magic不匹配”的错误。原因就是模块编译需要用到内核源码里的include/generated/autoconf.h,这个文件是在内核配置阶段生成的,跟源码状态强绑定。

4.4 第四步:模块加载与验证

模块编译好了,拷到目标板上,先看依赖关系:

modinfo st_lsm6dsx_i2c.ko

正常会输出description、author、license这些信息,还能看到depends字段,内容是st_lsm6dsx_core。这说明模块之间的依赖关系已经写进了ko文件的.modinfo段。

加载测试:

insmod st_lsm6dsx_core.ko insmod st_lsm6dsx_i2c.ko dmesg | tail

如果一切正常,dmesg里会看到类似下面的输出:

st_lsm6dsx_i2c 0-006a: lsm6dso i2c device probed

注意0-006a是内核I2C设备的标准命名:总线号0,设备地址0x6a。看到这行说明probe成功,设备已经挂在了IIO子系统下面。

用iio工具验证:

cat /sys/bus/iio/devices/iio:device0/name ls /sys/bus/iio/devices/iio:device0/

name文件应该输出lsm6dso。扫描目录下应该有in_accel_x_raw、in_accel_y_raw、in_accel_z_raw、in_anglvel_x_raw、in_anglvel_y_raw、in_anglvel_z_raw这些属性文件。这些就是IIO框架给用户空间暴露的标准接口。

读取原始值:

cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw

静止状态下读出来是一个接近0但不太稳定的值。把这个值乘以scale对应的灵敏度系数,就能得到g为单位的加速度值。scale文件也在同一目录下,比如in_accel_x_scale,单位是m/s^2/LSB。

4.5 第五步:应用层验证与数据解析

数据的最终消费不在内核里,而在用户空间。IIO框架提供两种读取方式:直接读sysfs属性,或者通过/dev/iio:deviceX字符设备用read()系统调用。前一种适合偶尔采几个点,后一种适合连续数据流。

我们的验证程序直接基于字符设备读:

#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdint.h> int main(int argc, char **argv) { int fd = open("/dev/iio:device0", O_RDONLY); if (fd < 0) { perror("open"); return -1; } /* 循环读取原始数据缓冲区 */ char buf[256]; ssize_t n; for (int i = 0; i < 100; i++) { n = read(fd, buf, sizeof(buf)); if (n > 0) { printf("read %zd bytes\n", n); /* 解析IIO buffer里的数据, * 每8字节为一个样本, * 按照注册的顺序排列 */ } usleep(100000); } close(fd); return 0; }

这块程序能不能读到数据,取决于有没有开启buffer模式。需要用sysfs触发一次捕获:

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/buffer/enable

Buffer使能之后,read()才能返回数据。这个动作在调试阶段经常被忽略,导致“驱动明明probe成功了,应用层就是读不到数据”的假故障。

5. 移植过程中遇到的典型问题与解决实录

5.1 内核配置引发的“隐身”设备

症状:设备树节点写好了,I2C地址也验证通了,但/sys/bus/i2c/devices/下面找不到设备目录,dmesg里也没有任何probe相关的报错。

排查过程:先看i2c bus有没有注册成功,/dev/i2c-0存在,i2cdetect也能扫到0x6a地址。再看内核配置,结果发现CONFIG_OF_GPIO没有打开,导致GPIO中断相关的设备树解析逻辑没被编译进去。而ST驱动的probe函数里,有一个gpiod_get()的调用,当GPIO子系统没启用时,这个调用返回的是-ENOSYS错误,驱动就直接放弃probe了——但是错误日志级别比较低,dmesg里看不到默认输出。

解决:打开CONFIG_OF_GPIO和CONFIG_GPIOLIB,重新编译内核。

这个问题的本质是:驱动的probe失败而不报错,典型的“静默失败”。排查这类问题要习惯性用dmesg的更高日志级别查看:

dmesg -n 8 echo 8 > /proc/sys/kernel/printk

同时在驱动代码里临时加打印,或者用内核的动态调试机制:

echo 'file st_lsm6dsx_core.c +p' > /sys/kernel/debug/dynamic_debug/control

5.2 I2C通信偶发超时:时钟频率计算错误的坑

症状:频繁读数时,偶尔会报“i2c transfer timeout”错误,设备会暂时从I2C总线上“消失”,过一会又自己恢复。

排查过程:先用示波器抓SCL波形,发现高电平时间明显短于低电平时间,占空比不对。进一步查驱动代码,发现龙芯I2C控制器的clock-frequency属性解析逻辑里,把设备树里配置的400kHz当成了内部滤波后的频率,没有做预分频计算。但实际上这个控制器的预分频值需要按照“总线时钟/目标频率/4”的公式另外配置,代码里直接把400000赋值给了寄存器。

解决:修改i2c-loongson.c里的频率配置逻辑,按照SoC的数据手册重新计算分频值。

这个问题的经验是:芯片手册和驱动代码之间有差异是非常普遍的事情。龙芯的这套I2C代码是从早期产品线带过来的,适配新一代SoC时频率计算公式没跟上,导致bug潜伏了很久。移植驱动的时候,不要盲目相信代码里的注释和计算,用示波器实测才是唯一靠谱的检验手段。

5.3 中断申请失败导致probe卡死

症状:加了interrupt属性之后,驱动probe变得非常慢,甚至卡死。dmesg里反复出现“request_irq failed”的提示。

排查过程:设备树里interrupt-parent写的是&gpio,但龙芯平台的中断控制器层级跟ARM不一样,存在一个“主中断控制器 -> 子中断控制器”的级联结构。GPIO控制器注册的中断号是相对GPIO控制器域的局部编号,需要经过irq_domain的映射才能变成系统全局中断号。设备树里如果直接写了GPIO域的中断号,request_irq的时候会申请到一个无效的中断号,大概率一直返回失败。

解决:查看GPIO控制器的设备树文档,确认interrupt-cells的个数和含义。龙芯平台GPIO节点的中断描述一般是两个cell:第一个是GPIO引脚号,第二个是触发标志。格式跟ARM平台的GIC不太一样,用ARM平台的写法在LoongArch上是不兼容的。

这个问题的根子在于“架构思维惯性”。做嵌入式的人习惯了ARM的设备树写法,到了LoongArch平台很多细节都不一样了。移植驱动的本质,其实是对目标平台特性的深度理解,而不只是“编译-拷贝-运行”的流水线操作。

5.4 IIO buffer采样频率设置异常

症状:启动buffer模式之后,read()读取数据频率明显偏快或偏慢,跟设定的ODR对不上。

排查过程:LSM6DSO的ODR是通过CTRL1_XL寄存器(加速度计)和CTRL6_C寄存器(陀螺仪)配置的,IIO驱动会根据用户写入的采样频率自动计算出芯片寄存器应该设置的值。但ST驱动在计算分频系数时,用的是固定的12.5kHz内部时钟作为基准,LSM6DSO的内部时钟接的是PLL输出,实际频率可能会因为外部晶振的偏差有±2%的浮动。如果板子设计时没有给IMU提供高精度的外部时钟,ODR的偏差就会比较大。

解决:在设备树里通过st,pull-ups属性、或者通过驱动里的st_lsm6dsx_hw_settings结构体预先设定固定的ODR档位,而不是让驱动自动计算。

5.5 寄存器误写导致设备“锁死”

症状:偶尔出现LSM6DSO的I2C地址在总线上完全失联,i2cdetect扫描不到,但断电重启后又恢复正常。

排查过程:查了芯片手册才发现,LSM6DSO有一个I2C地址锁存机制:如果SDO/SA0引脚在上升沿采样时读到异常电平,芯片会自动进入一种低功耗的“等待”状态,此时I2C接口不响应任何命令。这个引脚我们板子上是直接上拉到VDD的,正常应该一直是高电平。但系统启动时这个引脚的IO配置还没有完成,GPIO输出一个短暂的低电平脉冲,正好被芯片采样到了。

解决:在硬件上给SA0引脚加一个10kΩ的下拉电阻或上拉电阻,同时确认系统启动过程中该引脚的电平稳定。纯软件角度可以在I2C控制器驱动里,在probe时先通过GPIO操作把这个引脚拉高,再初始化I2C总线。

这类问题最有意思的地方在于:硬件设计和软件适配总是相互纠缠。驱动移植做到最后,往往要“跨界”解决硬件引脚配置的问题,这也是嵌入式开发的常态。

6. 驱动适配的进阶细节:从LSM6DSO到整个ST传感器家族

6.1 ST传感器驱动的共同架构

ST在Linux内核IIO子系统的驱动,整体架构高度统一。以LSM6DSO的驱动为例,代码分为两个层次:核心层(core)和传输层(i2c/spi)。核心层负责芯片寄存器配置、数据解析、IIO设备注册;传输层只负责把寄存器的读写操作映射到具体的物理总线协议上。

这个设计的好处很明显:如果你想移植LIS3DH(三轴加速度计),工作量不会比LSM6DSO大多少,甚至更小。LIS3DH的核心驱动代码已经在内核里存在很多年了,bug基本被修干净了,你要做的事情只有两步:设备树里把compatible改成"st,lis3dh",地址改成0x18或0x19,然后编译打开CONFIG_ST_LIS3DH。剩下的一切IIO标准化流程——设备注册、sysfs接口暴露、buffer管理——全自动走完。

HTS221温湿度传感器同理。它的配置项是CONFIG_HTS221,设备树写法:

hts221@5f { compatible = "st,hts221"; reg = <0x5f>; vdd-supply = <&vdd_io>; };

HTS221的I2C地址只有0x5f这一个,因为它的ADDR引脚只有一个电平组合。比LSM6DSO简单。

6.2 遇到老版本内核的兼容处理

前面提到的都是理想情况——内核里有现成驱动。但实际工作中经常碰到目标平台的内核版本比较老,驱动的API接口对不上。比如老内核的IIO框架用的是iio_dev_attr,新内核改成了iio_dev_attr_init;老内核的regmap API参数跟新内核也不完全一样。

适配思路是加兼容层:写一个小的头文件,把新API映射到老API上。但这样会引入条件编译的复杂性,不建议核心驱动代码里加一堆#ifdef。更好的做法是直接找对应内核版本的分支代码——ST驱动在Linux内核里是按版本维护的,5.4和5.10的ST_LSM6DSX驱动代码差别不小。用git可以很方便地切换:

git log --oneline -- drivers/iio/imu/st_lsm6dsx/ git checkout <需要的内核版本标签> -- drivers/iio/imu/st_lsm6dsx/

这样拉出来的代码跟目标内核的API是完全匹配的,省去了一堆编译修错的功夫。

6.3 多个ST设备共存时的地址冲突处理

如果一个系统里既要接LSM6DSO,又要接LIS3DH,还要接HTS221,I2C地址怎么排布就成了一个硬件设计问题。ST传感器的I2C地址通常是可配置的,LSM6DSO是0x6a/0x6b,LIS3DH是0x18/0x19,HTS221是0x5f,LIS3MDL(磁力计)是0x1c/0x1e。如果总线资源充足,最好各占一条I2C总线;如果必须共享总线,要仔细核对所有设备地址位,避免硬件上焊错了SA0/SDO引脚导致地址冲突。

软件层面,IIO子系统对多个同型号设备是天然支持的——每个设备会分配独立的iio:deviceX序号,设备树里写多个节点即可。但有一个坑:如果两个同型号设备挂在同一条I2C总线上,且地址也一样,那就只有第一个probe成功,第二个会报address already in use错误。这种问题只能在硬件设计时避免,软件没有好的解法。

7. 性能优化与稳定性沉淀

驱动跑通只是一个开始。作为一个“走马观碑”的嵌入式团队,我们习惯在功能验证通过之后,再做一轮性能测试和稳定性加固。这次针对LSM6DSO重点做了三件事。

第一件事:测量I2C总线实际带宽。用示波器抓SCL频率,确认是400kHz而不是更高或更低。然后写了一个循环读取程序,连续读10000次角速度原始值,统计平均单次读取耗时。预期应该在2ms左右(400kHz总线,每次传输大约8字节,加上寄存器地址和停止位)。实际测出来是2.3ms,基本符合预期。如果明显偏大,就有可能是GPIO模拟I2C而不是硬件I2C控制器——GPIO模拟I2C的带宽上限通常只有100kHz左右,实测性能会有数量级差异。

第二件事:验证中断模式下的数据连续性。在中断模式下,每个数据准备好中断到来时,读一次FIFO。这样就涉及一个新的问题:中断处理函数里不能做耗时操作,否则中断嵌套或者丢失中断会导致数据空洞。标准做法是在中断上半部只设置标志位,实际读取放到中断下半部(线程化中断或workqueue)。ST驱动默认实现了这种模式,实际测下来1000Hz采样率下没有任何数据丢失。

第三件事:掉电和热启动测试。模拟目标设备反复断电上电,检测驱动是否能正确恢复。这个测试发现了一个有意思的问题:断电之后寄存器内容不变,重新上电如果走的是软复位而不是完全掉电复位,芯片会保留掉电前的配置状态。所以驱动在probe的时候一定要发软复位命令,把芯片恢复到已知状态。ST驱动代码里确实有这个动作:

if (data->hw->enable) st_lsm6dsx_shub_set_enable(data, false);

但只针对sensor hub配置,对主传感器配置的复位逻辑在新版本代码里才补齐。如果内核版本偏老,建议手动在probe函数里加上软复位操作。

8. 写在最后:移植工作的核心体会

移植ST驱动到龙芯平台这件事,表面上看起来是“改设备树-开内核配置-编译-跑通”的流程化操作,实际上考验的是对整个Linux内核I2C/IIO子系统、LoongArch平台特性、以及传感器芯片本身工作原理的立体理解。

我在实际做这个项目的过程中最有感触的一点是:绝大多数问题都不是“技术难”造成的,而是“信息不对称”造成的。ST官方把驱动维护得很好,但官方文档和应用笔记默认是跑在ARM平台的参考板上;龙芯平台的内核代码和板级补丁也有自己的生态体系。当这两套体系第一次碰撞时,谁都不完全了解对方的环境,这时候出现的问题往往是最难查的——因为它既不完全是驱动bug,也不完全是平台bug,而是两个系统在边界上没对齐。

后来我们组就定了一个规矩:凡是涉及平台移植的任务,第一周不写代码,只做三件事——读目标平台的芯片手册、读目标板原理图、在设备树上把每一个引脚的硬件连接理清楚。这次ST驱动移植能在一周半时间内跑通,前期准备占了很大功劳。尤其是I2C地址、中断引脚、电源控制这三个点,在动手之前就用表格核对过了,后面调试省了大量时间。

最后再分享一个小技巧:IIO设备调通之后,不要急着写复杂的应用层代码,先用命令行把关键链路验证一遍——重点确认probe成功、sysfs属性文件存在、原始值读出来合理性这三个里程碑。每一段都稳了再进入下一段,比直接憋一个大程序再debug高效得多。这个思路,在龙芯上移植ST驱动如此,在其他任何平台移植任何驱动也一样适用。

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

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

立即咨询