上个月接了龙芯平台的一个驱动移植活儿,项目代号叫“龙芯k”,我们走马观碑组负责把ST的传感器驱动从标准内核代码拉到LoongArch上跑通。以前在ARM平台调驱动,感觉就是改改设备树、编一下模块,但到了龙芯上,LoongArch指令集带来的差异还是让我在几个关键点上多花了不少时间。这篇文章就把整个“走马观碑组ST驱动移植”过程写透,包括思路、代码、设备树、编译步骤,还有最后踩进去的坑。
如果你手头有一块龙芯板卡,想把ST的加速度计、陀螺仪、磁力计这类外设驱动跑起来,或者说你正准备从x86/ARM转向国产CPU平台的驱动开发,这篇记录应该能帮你省下几个晚上。我会用一次真实的LIS3DH传感器移植作为主线,逐步拆解驱动移植的每一个环节,从选型到验证,尽量做到能照着复现。
1. 内容整体设计与思路拆解
1.1 为什么要把ST驱动搬到龙芯上
龙芯平台在工控、嵌入式、信创设备里越来越常见,但它的外设生态不像x86那样统一,很多板载芯片都是ST、NXP这类厂商的。驱动移植的本质,不是把代码从一个目录拷到另一个目录,而是让一个为通用Linux内核准备的驱动,在LoongArch架构上能够正确编译、匹配硬件、并通过内核子系统向应用层提供数据。
拿ST的LIS3DH加速度计来说,它虽然是老款,却是ST在Linux内核里最典型的加速度计之一。内核里已经有现成的st_accel系列驱动,代码写得很规范,通过regmap抽象寄存器访问,驱动本身并不直接感知CPU架构。但真正拿到龙芯平台上,内核版本、设备树、I2C控制器、GPIO中断这些事情都需要重新对齐。我们做这个项目时,最开始以为只要把CONFIG开起来就能跑,结果发现事情没那么简单。
所以驱动移植,需要先弄明白“驱动的哪些部分依赖平台,哪些部分跟平台无关”。现代Linux内核已经把外设驱动做成了跨架构,只要把依赖平台的底座(总线、中断、时钟、电源)铺好,ST驱动基本就能在龙芯上工作。
1.2 驱动框架选型:IIO、input还是HAL库
ST官方经常会提供两套代码:一套是STM32的HAL库驱动,另一套是Linux内核驱动。这两套不能混用,我们做Linux驱动,只能走内核这一条路。而在内核里,传感器驱动又分两个主要方向,IIO子系统和input子系统。我的建议是首先考虑IIO,尤其是加速度计、陀螺仪、磁力计这类输出原始数据的传感器。
IIO和input的选择,本质上取决于数据的使用方式。input子系统适合处理中断触发的事件,比如按键、遥控器,它直接上报Linux input event。IIO子系统则更像是一个传感器数据中心,支持多通道缓冲、采样频率配置、事件触发、sysfs读取原始值,更适合做姿态解算、振动监测、数据记录。LIS3DH这类加速度计当然也可以注册成input设备上报方向变化,但如果想拿到各个轴的原始加速度值,IIO是首选。
| 对比项 | IIO子系统 | input子系统 |
|---|---|---|
| 数据粒度 | 原始传感器数据,多通道 | 已语义化的输入事件 |
| 调试方式 | sysfs直接读取in_accel_x_raw | evtest观察事件 |
| 采样控制 | 支持采样频率和缓冲 | 事件驱动为主 |
| 适用传感器 | 加速度/陀螺仪/磁力计/气压计 | 按键/触摸屏/鼠标 |
我们最终选择了内核自带的IIO驱动,没有自己写。因为ST的Linux内核驱动维护得很好,通过regmap和IIO框架已经对底层架构做了一次隔离,移植时只需要处理平台绑定和低层资源,不需要重写寄存器操作。
1.3 前期调研三件事
动手写代码之前,我做了三件事,每一件都直接影响了后面的进度。
第一,确认板卡内核版本。龙芯开源社区和Loongnix仓库提供的内核版本通常比自己从kernel.org下载的要更适合板子,因为里面带了板级设备树和板级补丁。我这次从Loongnix拉取的5.15内核,本身就包含了st_accel驱动,这让我省掉了把驱动文件手动拷进内核的大麻烦。
第二,确认I2C控制器在设备树里是否已经启用。很多龙芯开发板默认只启用了某些外设接口,比如I2C0可能被dts里注释掉了,或者GPIO复用状态不对。如果不先确认这些,驱动加载再也没用,因为设备树里根本没有你的外设节点。
第三,确认传感器硬件连接。LIS3DH的SA0引脚决定I2C地址,INT引脚接的是哪个GPIO,这个必须对照原理图来确定。我见过不少同事不看原理图,照着网上的设备树直接改interrupts属性,结果中断号不是对应GPIO,模块加载后中断注册失败。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 龙芯平台的驱动开发差异点
龙芯的LoongArch架构,在驱动开发流程上和ARM有一定的相似性,都依赖设备树来描述硬件,驱动通过platform_driver或i2c_driver注册,再利用of_match_table匹配设备节点。但底层有几处容易踩的差异,这里重点提三个。
第一个是IO访问方式。LoongArch的MMIO访问需要使用readl/writel这类内核标准接口,不能简单解引用物理地址。这个其实大多数驱动已经处理好了,但如果你自己在设备树里给某个寄存器区域配了reg属性,然后在驱动里直接使用ioremap并强制指针访问,极有可能在龙芯上触发非对齐访问或者字节序问题。
第二个是中断控制器。龙芯的中断系统和ARM GIC不一样,它沿用了一些MIPS风格的多级中断设计。设备树中interrupt-parent可能指向某个GPIO控制器,这个GPIO控制器的interrupt-cells可能是1也可能是2,需要看具体板卡的dts定义。这个差异直接影响interrupts属性怎么填。
第三个是内存屏障。LoongArch在设备DMA场景下对缓存一致性有自己的处理要求。不过对于I2C这类低速外设,一般不会涉及DMA,内核的I2C子系统会处理好,但如果移植的是需要DMA的驱动,比如某些ST的高端图像传感器或音频芯片,就必须检查dma_alloc_coherent和dma_map_single这类接口的使用,必要时加barrier。
2.2 ST传感器驱动的三层结构
理解ST传感器驱动的结构,是移植的基础。以LIS3DH为核心的ST加速度计驱动,在内核源码里通常分布在drivers/iio/accelerometer目录下,主要由三个文件组成:st_accel_core.c、st_accel_i2c.c、st_accel_spi.c。
st_accel_core.c负责硬件无关的逻辑,包括通道定义、采样频率设置、数据读取、scaling转换、以及和IIO核心的交互。st_accel_i2c.c则是I2C总线适配层,负责注册一个i2c_driver,在probe时创建regmap,并调用核心层的st_accel_common_probe继续初始化。st_accel_spi.c是SPI版本,逻辑类似,只是用SPI的regmap配置。
这套结构最大的好处是把“设备逻辑”和“总线访问”解耦了。移植时,大部分情况只需要修改I2C/SPI适配层里的设备ID表,让它能匹配到你的设备树节点。如果你的内核版本里ST驱动补丁比较全,甚至连这个表都不用改,直接配置好设备树就能跑。当然,我们实际项目里还是遇到了一点小问题,比如某个龙芯内核版本的of_match_table没有包含“st,lis3dh”字符串,这种情况下就需要自己添加一行。
2.3 需要改的核心代码点与电源管理
从实际操作来看,ST驱动移植需要改动的核心代码点集中在下面几个地方。
第一是of_device_id和i2c_device_id匹配表。这两个表是设备树识别和设备id识别的入口。如果设备树里写“st,lis3dh”,而of_match_table里只有“st,lis2dh12”,那就匹配不上,设备完全不会probe。
第二是regmap配置。st_accel_i2c.c里通常会用devm_regmap_init_i2c创建regmap,并设置regmap_config,包括寄存器位宽、读写标志位等。这些参数一般是ST传感器通用的,不需要为龙芯修改。但要注意regmap_config里的max_register是否匹配你的芯片型号,如果混用了,读取寄存器会返回范围错误。
第三是电源管理。很多ST驱动会在probe里调用devm_regulator_get获取VDD和VDDIO供电。如果龙芯板卡设备树里没有定义regulator节点,regulator_get会返回EPROBE_DEFER,导致设备被内核反复挂起重试,看起来像是驱动卡死。最简单的做法是在设备树中加一个fixed-regulator节点,或者在驱动里对regulator_get的返回值宽容处理。我这里更建议前者,因为动驱动代码会影响多个平台,能少改就少改。
3. 实操过程与核心环节实现
3.1 环境准备:本机编译比交叉编译省心
我之前在x86上给ARM板卡做交叉编译,习惯了用arm-linux-gnueabihf工具链。但到了龙芯,如果你用错了交叉工具链很容易出现“Unknown architecture”的错误,因为LoongArch的交叉编译工具链需要匹配内核的ABI。这次我直接在龙芯开发板上跑make,反而更省心,因为不用关心工具链版本和内核源码是否匹配。
环境准备很简单,确保板子上有内核源码并安装好依赖工具:
bash复制apt install make gcc flex bison bc libssl-dev libncurses5-dev
然后进入内核源码目录,确认当前架构:
bash复制make ARCH=loongarch loongson64_defconfig
这一步会生成一个基本能用的龙芯板级配置。如果没有loongson64_defconfig这个目标,说明你用的内核源码不是龙芯维护的分支,需要回到Loongnix仓库重新拉取。
3.2 内核配置与设备树节点
生成基础配置后,使用make menuconfig打开配置界面,找到以下位置:
Device Drivers -> Industrial I/O support -> Accelerometers
勾选ST LIS3DH three-axis digital accelerometer,同时把下面的ST sens common相关的I2C支持也选上。如果你打算把驱动编译成模块,记得把CONFIG_IIO_TRIGGERED_BUFFER也打开,因为ST加速计驱动在用到trigger buffer时会依赖它。
设备树节点是这次移植的关键。假设LIS3DH挂在I2C0上,I2C地址为0x18,INT1接到GPIO0的第5个引脚,那设备树节点的写法大致如下:
dts复制&i2c0 {
status = "okay";
clock-frequency = <400000>;
lis3dh@18 {
compatible = "st,lis3dh";
reg = <0x18>;
interrupt-parent = <&gpio0>;
interrupts = <5 IRQ_TYPE_LEVEL_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&lis3dh_irq_pin>;
};
};
这里有一个特别容易错的地方:interrupts属性里的第一个数字未必就是GPIO的物理编号。龙芯的设备树中,GPIO控制器可能带有自己的bit offset,需要参考板级dtsi文件里gpio0的gpio-ranges定义。如果你不确定,可以先不要把interrupts属性加进来,先用polling方式测试I2C读写,等motion数据读取正常后再处理中断。
3.3 驱动匹配表的修改与补全
修改完成设备树后,打开drivers/iio/accelerometer/st_accel_i2c.c确认匹配表。我用的内核里,of_device_id数组类似这样:
c复制static const struct of_device_id st_accel_of_match[] = {
{ .compatible = "st,lis2dh12" },
{ .compatible = "st,lis3dh" },
...
};
如果确实没有“st,lis3dh”这一行,就在末尾加上,然后修改i2c_device_id表:
c复制static const struct i2c_device_id st_accel_id_table[] = {
{ LIS3DH_DEV_NAME },
...
};
这两处都确认好了,就可以重新编译设备树和驱动模块。如果你改动过设备树,需要执行:
bash复制make dtbs
然后把生成的dtb文件复制到引导分区。如果驱动是模块,直接make modules编译,必要时make modules_install安装。一般来说,重新编译dtb后需要重启或更新引导参数才生效,驱动模块则可以用modprobe手动加载。
3.4 编译、加载和验证
这里我推荐一种“先模块后内置”的调试方式。先把驱动编译成模块,手动modprobe,如果出问题,可以在系统运行状态下反复卸载、修改、重编译,调试效率比每次重启高得多。
加载模块:
bash复制modprobe st_accel_i2c
然后用dmesg查看probe日志:
bash复制dmesg | tail -20
成功的话,通常能看到设备注册到IIO子系统的信息。如果没有任何信息,先检查设备树节点有没有被内核认出来:
bash复制ls /sys/bus/i2c/devices/
如果出现类似0-0018的目录,说明I2C设备已经被枚举,但驱动可能没匹配上。如果这个目录都没有,还是先回头查设备树。
设备正常注册后,查看IIO设备:
bash复制ls /sys/bus/iio/devices/
读取加速度原始数值:
bash复制cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw
cat /sys/bus/iio/devices/iio:device0/in_accel_scale
将raw值乘以scale就是标准单位下的加速度。当板卡倾斜或振动时,raw值应该明显变化。如果一直恒定为0,请跳到第4章的“读数异常”排查。
4. 常见问题与排查技巧实录
4.1 编译错误常见原因与解决办法
编译ST驱动遇到最多的报错是“Unknown symbol”或“undefined symbol”,比如st_sensors相关的注册函数找不到。这个问题的根源通常是内核配置里没有打开CONFIG_IIO_ST_SENSORS_I2C或CONFIG_IIO_ST_SENSORS_CORE。解决方法是在menuconfig中把ST Sensors common相关选项全部打开,确保这些底层库和驱动处于同一构建目录。
还有一种情况是编译时找不到regmap API。LIS3DH驱动依赖regmap,但如果内核配置里CONFIG_REGMAP被关了,编译自然过不去。建议先检查:
bash复制grep CONFIG_REGMAP /boot/config-$(uname -r)
如果确认配置齐全仍然报错,检查内核源码目录下的drivers/iio/accelerometer/Makefile,是否把st_accel相关文件加入了OBJ列表。有几次我在复制旧驱动文件时,忘了更新Kconfig和Makefile,导致文件在源码里但根本没被编译。
4.2 设备树匹配不上怎么查
设备树匹配不上是最隐蔽的问题,因为不会报编译错误,只是probe不执行。我常用的排查方法是先看sysfs是否已经枚举出i2c设备:
bash复制ls /sys/bus/i2c/devices/
如果0-0018存在但没有probe,说明I2C核心已经检测到设备地址,只是找不到匹配的驱动。此时检查设备树里compatible属性和驱动中of_match_table里的字符串是不是一模一样,包括大小写和标点。一定不要忽略系统里是否有其他驱动抢先注册了同样的compatible,这会导致设备被另一个驱动占用,而ST驱动反而没有获取到设备。
另外,可以用里面的办法查看设备树是否被正确加载:
bash复制ls /sys/firmware/devicetree/base/
如果能找到i2c0节点下的lis3dh@18目录,说明dts解析没问题。如果找不到,检查dts语法或者是否没有把新的dtb刷进启动分区。龙芯板卡有些需要设置uboot环境变量来加载指定dtb,这一步经常被忽略。
4.3 I2C通信失败排查
I2C通信失败的表现是probe阶段读取设备ID失败,甚至modprobe时直接返回错误。先用i2cdetect确认I2C总线上是否有设备:
bash复制i2cdetect -y 0
如果地址0x18处显示“--”,说明传感器没有正常响应。最常见原因是SA0引脚电平不对或传感器供电没接好。需要注意LIS3DH的I2C地址是7位地址0x18还是0x19,取决于SA0连接低电平还是高电平。如果设备在主控的I2C总线另一侧挂了多个设备,还要检查是否存在地址冲突。
如果i2cdetect能看到设备,但驱动读取ID还是失败,怀疑是I2C时序问题。把设备树里的clock-frequency从400000调低到100000再试一次。龙芯的I2C控制器有时对高速I2C的时序容忍度不高,降低频率能解决大多数不稳定问题。上一次我们排查了很久,最后发现是板子上拉电阻阻值从10K改到4.7K才稳定。
4.4 读数一直为零或恒定值
读数持续为零或长时间不变,通常不是传感器坏了,而是设备没被正确唤醒。ST传感器很多默认是低功耗或省电模式,驱动初始化时要确保能通过配置寄存器唤醒设备。IIO驱动在probe时一般会设置默认的采样频率和电源模式,如果供电不稳定,写入寄存器的值可能丢失。
另一个可能原因是缓冲没有更新。如果使用trigger buffer方式读取,但没有正确触发,读取的原始值可能一直是旧数据。先用sysfs直接读取单次采样:
bash复制cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw
如果单次读取有变化,说明传感器本身正常,问题在缓冲或中断。如果单次读取永远是0,再用i2cdump查看传感器寄存器值,确认WHO_AM_I寄存器是否为0x33,若不对,说明I2C访问的寄存器配置有问题。
4.5 中断申请失败排查
中断是这次龙芯移植里比较头疼的部分。如果驱动里加了irq配置,但申请失败,一般会在dmesg里看到“device tree: interrupt-map”相关提示,或者“could not request IRQ”信息。
首先确认设备树里的interrupt-parent和interrupts属性。龙芯板卡的GPIO控制器有些定义在soc节点下,interrupt-cells可能是2(表示<pin, flags>),也可能是1。如果填错了,中断号解析出来就是错的。查看GPIO控制器节点:
bash复制cat /sys/kernel/debug/gpio
这个文件会列出当前所有GPIO的控制者、基线和占用情况。根据LIS3DH INT1实际连接的GPIO编号,检查驱动申请的中断号是否在这个范围内。如果无法确定,可以先在设备树中去掉interrupts属性,让驱动走轮询模式,这样至少能先验证传感器数据通路是否正常。
4.6 长时间运行的稳定性问题
模块能加载、数据能读,不代表移植完成。长时间运行后出现I2C总线挂死或设备无响应,是驱动工程师必须考虑的问题。一次我们连续跑了两天,LIS3DH突然报出“NACK”错误,后来排查发现是信号完整性问题,板卡上某一根I2C线线缆过长,导致信号反射。这个问题在ARM板上可能不明显,但龙芯平台的I2C控制器对时序参数更敏感。
建议在驱动里增加简单的recovery机制,比如检测到NACK时重置I2C控制器或重新初始化传感器。如果你用的是内核自带的I2C子系统,可以打开CONFIG_I2C_GPIOREBOARD或者I2C的bus recovery功能。实际项目中,加一个看门狗线程定期读取传感器,如果连续多次失败就执行一次i2c controller reset,能很大程度提高稳定性。
5. 移植思路的扩展应用
5.1 其他ST系列器件的快速移植方法
这套“调研内核版本、配置设备树、核对匹配表、sysfs验证”的方法,不只是LIS3DH能用。ST的很多传感器,比如LSM6DSO系列陀螺仪加加速度计、LIS2MDL磁力计、HTS221温湿度传感器,在内核IIO子系统里都有对应驱动,并且结构都是“core + i2c + spi”三层。只要你目标内核里有这些文件,就可以复用同样的流程。
唯一要特别注意的就是电源和中断。ST传感器为了低功耗,经常需要两个供电轨VDD和VDDIO,还有一个数据就绪中断。在龙芯平台上,电源轨如果不好配,就先用固定regulator顶着。中断如果调不出来,就先轮询。先把数据通路打通,再逐步完善高级功能,这是最稳妥的推进方式。
结尾
这次“龙芯k”项目的ST驱动移植,给我最大的感受是,现代Linux内核的抽象层真的把跨平台问题解决了一大半,剩下的工作更多是“读懂平台”而不是“重写代码”。走马观碑组的经验就一句话:先搞定设备和驱动的握手,再谈数据读得好不好。设备树配好,匹配表对齐,I2C地址正确,九成驱动都能跑起来。如果你正在折腾龙芯加ST的外设,不妨按这个顺序排查,能少踩不少坑。最后再分享一个小技巧,遇到传感器驱动在内核里没有时,先搜整个drivers目录里的compatible字符串,比翻遍全网的博客都管用。
