龙芯K平台ST传感器驱动移植实战:I2C与Linux驱动全解析

龙芯K平台ST传感器驱动移植实战:I2C与Linux驱动全解析 组里把“龙芯K平台ST驱动移植”这个任务分给我的时候我一开始以为就是把ST官网某个驱动包交叉编译一遍扔到板子上就完事。真上手之后才发现这事儿远比“下载-编译-烧写”复杂得多架构不一样内核接口版本不一样连寄存器操作的习惯都不一样。折腾了两周中间踩的坑凑一凑都能开个小型展览馆。这篇就记录一下整个移植过程从平台梳理、驱动框架选择到最终在龙芯K上把ST传感器数据稳定读出来给后面接这个活儿的兄弟省点弯路。这篇内容的适用对象是手里有龙芯K系列开发板、Linux内核已经跑起来想把ST的IMU、气压计、温湿度这类传感器驱动从官方代码或STM32裸机代码迁移过来的读者。就算你用的不是龙芯只要平台是LinuxI2C总线这套逻辑基本是通用的重点看思路别死抠板子型号。1. 项目背景与移植思路拆解1.1 “走马观碑组”的活儿到底是个啥“走马观碑”这个组名按我们内部说法是“看寄存器手册速度要快扫一眼就得知道这块芯片能不能用”。这个任务挂在嵌入式系统整机验证下面核心需求很朴素龙芯K平台作为一个主控要能通过I2C总线控制ST的传感器芯片把加速度、角速度这类数据读回来供上层应用使用。听起来就是驱动开发的常规操作但“移植”两个字才是关键。ST官方给的驱动源码一般分两类一类是Linux内核自带的st_sensors驱动这类基本不需要怎么改另一类是STM32裸机工程里的库函数代码一大堆寄存器地址宏、读写回调函数直接拿到龙芯K上肯定编不过因为依赖了STM32的HAL库。我们这次要处理的主要是第二类顺带把第一类里内核版本不匹配的问题也收拾干净。整个任务的难点不在“写驱动”而在“看懂别人家的驱动并在新平台复现它的时序逻辑”。传感器芯片本身不会因为主控从小核换成龙芯K而改变寄存器地址改变的是你访问寄存器的途径以及中断、时钟、电源管理等外围配套。搞清楚这个边界问题就解决了一大半。1.2 移植对象与平台边界我先列一下这次移植的平台参数避免后面讲设备树和编译时大家找不着北主控龙芯K系列嵌入式处理器LoongArch架构板子属于2K系列开发平台操作系统Linux版本基于5.10左右的内核目标总线I2C0挂载一颗ST六轴IMU传感器工具链loongarch64-linux-gnu- 交叉编译工具链移植目标将ST官方Linux驱动/STM32参考代码中的传感器控制逻辑整理成可在龙芯K Linux内核下稳定运行的驱动模块这次的传感器选型是ST的LSM6DS3或者同系列六轴传感器逻辑一致它通过I2C接口挂在龙芯K的I2C0上设备地址为0x6B。如果你手里是MPU6050这类InvenSense器件也不要紧I2C驱动的框架完全一样改改寄存器地址和数据解析部分就行。2. 移植前的技术勘察搞清楚驱动在哪一层2.1 龙芯K平台的硬件资源盘点拿到板子第一件事不是翻ST的驱动文档而是先把龙芯K这边的家底盘清楚。Linux下最直接的办法就是看设备树源文件确认I2C控制器已经启用并且引脚没有和别的外设打架。我习惯先执行这么几步ls /dev/i2c-* # 查看系统有几个I2C控制器 i2cdetect -l # 列出I2C总线 cat /sys/kernel/debug/gpio # 查看GPIO占用情况如果 /dev/i2c-0 存在说明I2C0控制器驱动已经加载。没出来就先去查内核配置CONFIG_I2C、CONFIG_I2C_CHARDEV、CONFIG_I2C_GPIO这几个选项必须打开。龙芯K平台的I2C控制器一般挂在内核自带的i2c-ls2x或者类似的平台驱动下但这块不同的BSP版本差异较大稳妥的做法是确认板级设备树里的i2c0节点状态为“okay”。这一步容易被忽略但恰恰是移植能否继续的前提。你自己写的驱动再对底层的I2C适配器没注册好i2cdetect扫出来全是空后面一切都是白搭。2.2 Linux I2C驱动的三层结构在动代码之前先把模型捋清楚。Linux里的I2C驱动其实分三层适配器Adapter驱动负责和硬件I2C控制器打交道把你的读写请求转换成总线时序龙芯K这边由BSP提供。客户端Client驱动负责具体外设比如我们写的传感器驱动。设备节点层用户态通过 /dev/i2c-x 直接发控制指令或者在 /dev/ 下创建字符设备由应用层read。ST官方驱动移植的目标就是第2层。有些BSP供应商会在内核中直接给一段杂七杂八的传感器代码看起来能用但依赖了厂家私有的读写接口这种驱动移植到主线内核基本就是重写。我的建议是别迷信BSP提供的驱动就按内核标准的i2c_driver框架走反而省事。2.3 选字符设备还是选IIO子系统Linux内核里传感器驱动有两个常见归宿一个是IIO子系统Industrial I/O一个是直接写字符设备驱动。IIO的好处是内核帮你管理了sysfs接口、trigger buffer、硬件FIFO这些复杂机制用户态配合iio_info工具就能读数据省得自己写应用层协议。缺点是对新手来说IIO框架自身的学习成本不低尤其是触发缓冲区和采样频率配置那一套。字符设备驱动则更直白open、read、ioctl把寄存器读写封装好用户态想怎么折腾都行。缺点是自己要处理并发访问和资源释放数据量大时性能也不好做。如果你只是验证传感器能不能用我建议先走IIO。我们这次为了快速验证选择先写一个简单的字符设备驱动把数据读通后续再决定是否合并到IIO子系统。这个决策不丢人很多项目都是这么从“能跑”到“跑得专业”的。3. 拆解ST官方驱动别急着移植3.1 从ST官网获取驱动源码的注意事项ST传感器的官方Linux驱动一般挂在GitHub的STMicroelectronics仓库下对应你所用芯片的开源驱动包。找的时候注意几个坑官方的仓库更新频繁分支很多优先找和你内核版本匹配的标签不要一上来就 pulling 最新的 master很可能新代码用了你内核还没有的API。驱动包通常是个zip里面分为include和src两部分不要只拿src头文件里的寄存器定义才是移植的命根子。有些驱动包会依赖ST的st_sensors_common代码如果只拷某一个芯片的文件夹编译的时候会发现缺一堆符号。另外ST官方的Linux驱动普遍基于设备树匹配你在st_lsm6ds3.c里看到的 compatible 字符串就是后面设备树节点里要填的东西。3.2 看懂ST驱动里的关键文件打开ST官方驱动包一般会看到这些关键内容lsm6ds3_reg.c / .h寄存器读写底层一般实现了lsm6ds3_read_reg/lsm6ds3_write_reg。lsm6ds3.h设备地址、寄存器地址、位域定义。lsm6ds3.c核心逻辑包含初始化、使能、读加速度/角速度等函数。部分版本还有lsm6ds3_fifo.c处理硬件FIFO的这个在移植初期可以先不碰。我的建议是按“寄存器定义部分 → 读写函数 → 初始化序列 → 数据读取”这个顺序逐层拆解。寄存器定义是纯数据复制过来只要改改宏名防止冲突就行读写函数必须改因为它们内部调用了HAL库或者平台抽象层初始化序列是从寄存器写入的具体数值和平台无关可以完整复用。3.3 STM32式代码和Linux驱动的关键差异这次项目里还有一部分代码来源于STM32的参考例程写了一大堆HAL_I2C_Mem_Read这样的调用。移植到Linux时最核心的转变是把“阻塞式HAL调用”改成“Linux I2C message传送”。STM32例程中的I2C读写大多是同步阻塞而且一个函数只操作一个寄存器。Linux驱动里则更推荐用struct i2c_msg数组组合读写事务这样一条总线事务就能完成“先发寄存器地址、再读连续数据”的操作省去多次地址建立时间效率也要高不少。还有一点非常关键STM32例程里经常会操作GPIO来模拟片选或复位时序在Linux下这些操作设备树里先声明好GPIO资源然后通过devm_gpiod_get和gpiod_set_value来控制不能直接在驱动里写硬件寄存器号。4. 移植实操从寄存器到用户态数据4.1 设备树配置先让系统知道传感器在哪设备树就是给内核看的“硬件接线图”。在龙芯K平台的板级dts里找到I2C0节点增加一个子节点描述传感器i2c0 { status okay; clock-frequency 100000; lsm6ds36b { compatible st,lsm6ds3; reg 0x6b; interrupt-parent gpio0; interrupts 10 IRQ_TYPE_LEVEL_LOW; supply-io vdd_io; }; };注意几个容易翻车的细节reg 0x6b必须和芯片的硬件接法一致。LSM6DS3的SDO引脚决定了I2C地址是0x6A还是0x6B接错地址的话 i2cdetect 能看到设备但驱动匹配不上。compatible字符串必须和驱动里of_match_table定义的一模一样一个字节都不能差。中断引脚先不接也行但如果你要开数据就绪中断这行必须配上而且GPIO号按板卡实际接线填写。改完设备树以后编译并替换设备树重启板子在/sys/bus/i2c/devices/下就应该能看到0-006b这样的目录这说明内核已经发现设备了。4.2 搭建字符设备驱动基本框架如果不想直接啃IIO框架可以先写一个精简的I2C字符设备驱动。基本结构如下static const struct of_device_id lsm6ds3_of_match[] { { .compatible st,lsm6ds3 }, { } }; MODULE_DEVICE_TABLE(of, lsm6ds3_of_match); static const struct i2c_device_id lsm6ds3_i2c_id[] { { lsm6ds3, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, lsm6ds3_i2c_id); static struct i2c_driver lsm6ds3_driver { .driver { .name lsm6ds3, .of_match_table lsm6ds3_of_match, }, .probe lsm6ds3_probe, .remove lsm6ds3_remove, .id_table lsm6ds3_i2c_id, }; module_i2c_driver(lsm6ds3_driver);probe函数在设备树匹配成功后调用这时候client结构体已经带了设备地址和适配器信息。在probe里要做三件事验证芯片ID、初始化寄存器、注册字符设备。验证芯片ID是移植中最容易出彩也最容易翻车的一步。LSM6DS3的WHO_AM_I寄存器地址是0x0F复位值一般是0x69。如果读出来对不上先别急着改寄存器地址八成是I2C读写函数有问题而不是芯片型号搞错了。先用i2cdetect -y 0看设备地址能不能扫到再用读写寄存器的方式验证ID两下对照就能定位问题。4.3 I2C读写函数整个移植的命脉Linux内核里I2C最底层的传输函数是i2c_transfer它接受struct i2c_msg数组。要读取一个传感器的多字节寄存器需要两个msg第一个写寄存器地址第二个读数据。static int sensor_read_regs(struct i2c_client *client, u8 reg, u8 *buf, int len) { struct i2c_msg msgs[2]; int ret; msgs[0].addr client-addr; msgs[0].flags 0; msgs[0].buf reg; msgs[0].len 1; msgs[1].addr client-addr; msgs[1].flags I2C_M_RD; msgs[1].buf buf; msgs[1].len len; ret i2c_transfer(client-adapter, msgs, 2); if (ret ! 2) { dev_err(client-dev, I2C read failed: reg0x%02x, ret%d\n, reg, ret); return ret 0 ? ret : -EIO; } return 0; }类似地写单个寄存器则是一个msgstatic int sensor_write_reg(struct i2c_client *client, u8 reg, u8 val) { u8 buf[2] { reg, val }; struct i2c_msg msg { .addr client-addr, .flags 0, .buf buf, .len sizeof(buf), }; int ret i2c_transfer(client-adapter, msg, 1); if (ret ! 1) { dev_err(client-dev, I2C write failed: reg0x%02x, val0x%02x, ret%d\n, reg, val, ret); return ret 0 ? ret : -EIO; } return 0; }这里有个细节值得多说一句有些传感器芯片的寄存器写入需要先设置“写使能”位或者要求连续读操作在高位地址有特定的auto-increment行为。这些信息在数据手册的I2C章节都有移植时最好把对应页截图下来对照着写别想当然。我以前因为没注意auto-increment位导致一次读六个字节时只有第一个字节是对的后面全是垃圾数据排查了一整个下午。4.4 初始化序列与数据读取拿到ST官方驱动或参考例程后把初始化序列整理成一张表例如配置块1CTRL1_XL设置加速度量程为±4g、采样率416Hz写入寄存器值0x58配置块2CTRL2_G设置角速度量程为±2000dps、采样率416Hz写入0x58。每个寄存器写什么值、为什么这个值对应数据手册里的位定义逐位换算这个过程不要省略。读取数据的时候加速度和陀螺仪的原始值都在各自的输出寄存器里一般是以小端格式存储的16位有符号数。读取后需要拼接static int sensor_read_raw(struct i2c_client *client, u8 reg_h, u8 reg_l, s16 *val) { u8 data[2]; int ret sensor_read_regs(client, reg_l, data, 2); if (ret) return ret; *val (s16)((data[1] 8) | data[0]); return 0; }这里的小端拼接方式在不同ST芯片上是一致的但个别芯片输出数据的位宽可能有差异比如三轴磁力计有的只有左对齐的14位有效位这种就需要额外的右移操作。千万别所有芯片都套同一个公式。4.5 把数据从内核送到用户态字符设备驱动的 read 接口可以这样实现用户在应用层调用read(fd, buf, 6)驱动从传感器读6个字节的原始数据两个轴各3字节太少了至少6字节才能覆盖两个轴一般六轴需要12字节拷贝到用户缓冲区static ssize_t lsm6ds3_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { struct lsm6ds3_data *data file-private_data; u8 out[12]; int ret; ret sensor_read_regs(data-client, LSM6DS3_OUTX_L_A, out, 6); if (ret) return ret; ret sensor_read_regs(data-client, LSM6DS3_OUTX_L_G, out 6, 6); if (ret) return ret; if (copy_to_user(buf, out, 12)) return -EFAULT; return 12; }用户态拿到这12个字节前6字节是加速度X/Y/Z的小端原始值后6字节是角速度X/Y/Z原始值乘以对应的灵敏度系数就得到物理量。这个接口虽然简陋但验证传感器是否工作完全够用。5. 调试实录五个常见问题与排查技巧5.1 i2cdetect扫描不到设备地址这是最常遇到的问题占了移植初期排查工作的一半。现象是i2cdetect -y 0输出里全是--或者没有0x6b这个地址。按下面顺序逐个排查先查硬件接线SDA、SCL、VDD、GND四根线有没有接反或虚焊示波器/逻辑分析仪看SCL上有没有时钟信号。确认I2C总线编号有的板子I2C0的编号不一定是0通过ls /dev/i2c-*确认然后用i2cdetect -y 总线号去扫。曾经就有兄弟对着I2C1扫了半天其实传感器挂在I2C0上。查设备树i2c0节点是不是status okay控制器时钟有没有开。查上拉电阻I2C总线必须接上拉电阻很多所谓“漂移”问题其实都是上拉电阻没焊、静态电平不稳造成的。如果以上都确认无误但依然扫不到地址用dmesg | grep i2c看内核I2C子系统有没有报错再拿示波器量SDA的波形。硬件问题用软件看代码是看不出来的。5.2 WHO_AM_I读出来是0xFF或者0x00设备能扫到但读WHO_AM_I得到全1或全0说明I2C通信没有真正导通。常见原因有两个第一读写函数里寄存器地址没发对比如把寄存器地址当成I2C从机地址了或者第一个msg的buf设置错误发送了空数据。第二传感器芯片的电平域和龙芯K侧的I2C电平域不匹配需要加电平转换芯片或者漏接了传感器的IOVDD引脚。还有一种情况比较隐蔽某些传感器模块为了兼容3.3V和5V板上自带了电平转换这时模块引出的SDA/SCL可能标反了不要迷信丝印用万用表顺着线路量一下再决定。5.3 数据读出来一直是同一个值这个问题出现时驱动加载正常、读数也不报错但三个轴的数值永远固定不动。排除了传感器没上电这种低级原因后多半是中断/数据就绪标志没处理。很多ST传感器默认在测量完成后把数据更新到输出寄存器但如果你配置了FIFO或者数据就绪中断而驱动又没有处理中断读数就会停止更新。最简单的临时验证方法是在每次read之前读STATUS寄存器LSM6DS3是0x1E确认数据就绪位为1再读输出寄存器。如果就绪位永远为0查初始化序列里的传感器工作模式和采样率配置大概率是CTRL寄存器的值不对。5.4 官方代码里的宏定义冲突ST的驱动喜欢用大量宏定义有些宏名很容易和内核头文件里的宏撞车。我在实验时把ST官方代码整段复制到驱动文件里结果编译报了一堆redefinition错误其中最典型的是#define shell_cmd(n, d, h)这种调试命令宏和RT-Thread里的同名宏冲突。解决办法不复杂把所有ST驱动头文件纳入一个独立的目录并在代码里用#ifndef做好保护冲突的宏统一加ST_前缀涉及shell/调试命令的宏如果目标平台用不上直接注释掉比硬刚要好。内核模块里不是所有宏都有必要存在驱动移植讲究“取其核心业务逻辑”不要为了保留原样而保留。5.5 编译工具链和调试器问题移植过程中交叉编译工具链如果版本太老可能不支持LoongArch的某些新特性反过来工具链太新又可能和BSP头文件有冲突。建议使用龙芯平台官方SDK里配套的交叉工具链不要自己去拉最新的GCC来挑战自我。至于ST-Link、J-Link这些调试器在龙芯板子上并不通用。板级调试用串口和示波器就够了串口看内核日志示波器看I2C时序。如果实在需要代码级调试可以用龙芯自带的调试器但纯驱动开发其实不依赖它printk加示波器组合能解决九成问题。5.6 调试心得给日志留好“后门”我在驱动里习惯留一个debug参数可以在加载模块时通过insmod lsm6ds3.ko debug1打开调试输出。凡是对单次I2C读写的函数强制打一条记录寄存器地址、数据和返回值的日志定位问题的时候就不用反复加printk重新编译了。一个简单示例static int sensor_read_retry(struct i2c_client *client, u8 reg, u8 *buf, int len) { int ret; ret sensor_read_regs(client, reg, buf, len); if (debug) dev_info(client-dev, read reg 0x%02x len %d - %*ph (ret %d)\n, reg, len, len, buf, ret); return ret; }开发期这个日志是救命稻草评测完再把它关掉就行。6. 后续扩展方向与一点个人经验驱动数据能读通之后我建议不要就此收手往后还有几件事值得做一是把字符设备驱动升级成IIO驱动。IIO的好处是用户态可以直接用sysfs读取还能对接libiio这类库省得每个项目都造一套数据读取轮子。移植IIO的工作量主要在iio_dev的初始化和trigger的设置上核心读写函数可以复用。二是加入中断和FIFO支持。像LSM6DS3这类传感器硬件FIFO功能很强数据多的时候可以攒在FIFO里统一批处理大幅减少I2C事务次数。这部分实现起来比轮询模式复杂推荐有精力再搞。三是把量程和采样率的配置做成可动态调节的接口。现在初始化序列是写死的应用层只能接受固定量程。通过ioctl或者sysfs把CTRL1_XL这几类寄存器开放出来让上层应用按需配置驱动才算真正产品化。最后说点个人体会。驱动移植说白了就是“把点灯的思路换成读寄存器的思路”关键拼的不是代码功底是排查链路的能力。我在这次项目里最大的收获是养成了一个习惯任何一项I2C操作先做一个最小可验证的实验比如先读WHO_AM_I确认这个通了再去做后面的初始化。一次只想验证一步出了问题能立刻缩小范围。不要学我一开始那样一口气写完几百行驱动然后烧进去发现啥都不对那感觉和拆盲盒差不多只是拆出来的永远不是惊喜。你在做龙芯K或者类似平台驱动移植的时候把这条记在心里至少能让你的调试时间省掉一半。