尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

龙芯平台驱动移植实战:ST传感器MPU6050适配Linux IIO子系统

龙芯平台驱动移植实战:ST传感器MPU6050适配Linux IIO子系统 龙芯平台上的驱动移植实战从一个ST传感器驱动说起做嵌入式开发的人大多都遇到过这种情况手里拿了一块很常见的传感器模组一搜资料满屏都是STM32的裸机例程、HAL库代码、寄存器配置写得确实漂亮。但当这块传感器要接到一块跑着Linux的国产SoC上时问题就来了——这些现成的驱动代码没法直接跑底层依赖完全不同。最近我们组就在做类似的事情把一套原本写给ST平台的传感器驱动完整移植到了龙芯K系列平台上。借这个机会把整个过程和一些关键细节整理出来希望对正在做类似跨平台驱动迁移工作的朋友有帮助。先说清楚这次要解决的核心问题。我们组的项目代号叫走马观碑组名字听着有点赶时间的意思实际上也确实是在赶节点。手里的任务是在龙芯K系列嵌入式平台上把一颗传感器从底层驱动开始全部跑通供上层的姿态解算算法使用。传感器的型号是MPU6050这颗六轴传感器在飞控、平衡车、穿戴设备里太常见了网上能找到的驱动几乎全是STM32平台的——要么是标准外设库写的要么是HAL库写的要么是寄存器直操。我们所做的事情就是把这套ST生态下的驱动逻辑移植到Linux内核的IIO子系统框架里跑在龙芯2K1000上。这篇文章会从整体思路、关键细节、实操步骤和踩坑记录四个维度展开内容偏嵌入式Linux中阶无论是刚接触驱动移植的初学者还是已经在做国产平台适配的工程师应该都能找到有价值的信息。1. 移植前必须想清楚的三件事1.1 明确边界你移植的是驱动逻辑而非驱动代码很多人在做这种跨平台移植时第一反应是改代码——把STM32的寄存器地址改一改把HAL库函数换成Linux API看起来就能跑了。这个思路不能说全错但放在Linux内核里往往走不通。STM32上的驱动是跑在裸机或RTOS上的CPU直接操作寄存器地址总线访问是一拍一个准而Linux内核里的驱动面对的是一个多进程、虚拟内存、中断密集的环境还要跟设备模型、电源管理、并发访问这些机制打交道。所以真正的移植是把ST驱动里那颗传感器怎么初始化、怎么读数据、怎么算校验的逻辑抽出来然后把它的IO通路、内存访问方式、时间控制方式换成Linux的机制。以MPU6050为例ST平台的例程核心就三部分I2C初始化、寄存器配置PWR_MGMT_1、SMPLRT_DIV、CONFIG、GYRO_CONFIG、ACCEL_CONFIG这些、以及按需读取传感器数据寄存器。移植到龙芯Linux平台时I2C初始化换成Linux的I2C子系统寄存器配置的逻辑可以原样保留但数据读取要通过I2C adapter的API去操作而不是直接拉GPIO模拟时序。这就是边界传感器内部时序和寄存器语义是ST给了规范的这部分要原封不动搬过来怎么跟芯片打交道则是新平台再实现一遍。1.2 选型对比裸机直操、字符设备还是内核子系统方案选型决定了后面所有工作。当时我们对比了三条路一是在龙芯的用户态程序里直接操作GPIO模拟I2C时序访问传感器简单粗暴但占用CPU、时序不稳更重要的是跟系统里其他I2C设备有冲突风险二是写一个独立的字符设备驱动把MPU6050的读写接口暴露给用户态上层应用直接open/read/ioctl这个方案自由度大但所有逻辑要自己管理后续要接入Linux的标准传感器框架就麻烦了三是用内核标准的IIOIndustrial I/O子系统把MPU6050挂成i2c client设备让IIO框架帮我们处理buffer、trigger、sysfs接口这些事。最终选了IIO方案理由很直接Linux内核的IIO子系统就是为传感器这种设备设计的MPU6050这种六轴传感器在主流内核里甚至已经有现成的IIO驱动可以参照后面如果要支持设备树配置、中断触发采样、DMA搬数据都不用自己从头写。更重要的是龙芯官方内核版本相对主流内核有一定滞后用标准框架可以减少后续内核升级时带来的适配工作量。1.3 资源盘点确认龙芯平台上的I2C控制器怎么用无论驱动方案选得多漂亮底层通路不通就全是空谈。在动手写MPU6050驱动前必须先把龙芯2K1000平台的I2C控制器摸清楚。龙芯的核心里一般至少有两组I2C控制器分别挂在不同基地址上对应的设备树节点需要自己确认。这一步强烈建议先用内核自带的i2c-tools工具做一次探测把总线上有哪些设备、地址对不对先验证了再往下走。我们当时就在这一步发现板卡上I2C2总线的上拉电阻焊错了位置导致SDA一直被拉低总线直接stuck——这个在STM32上很难遇到的问题在Linux下通过i2cdetect非常容易暴露。2. 驱动移植的核心细节ST驱动里到底有哪些不能搬的东西2.1 从寄存器操作到设备树映射ST平台的MPU6050驱动里第一件事往往是写一个初始化函数里面一堆I2C_WriteByte这样的调用。这些函数在Linux下不会存在替代者是i2c_transfer或者更封装好的i2c_smbus_read/write。但这里有个容易忽略的点STM32的I2C时序参数和龙芯平台的I2C频率怎么匹配。MPU6050的手册里写得很清楚I2C最大支持400kHz快速模式。STM32例程里通常会把I2C外设配置为400kHz而龙芯Linux内核的I2C控制器驱动默认频率一般是100kHz你配置成400kHz也没有问题。但如果你的板卡上还有其他老器件只支持100kHz共用一个总线的时候就要小心了最简单是给MPU6050单独挂一条I2C总线或者把频率统一降到100kHz——传感器性能虽会打点折扣但姿态解算的采样率通常几十到几百Hz100kHz完全够用。还有一个必须匹配的地方是中断引脚。MPU6050的INT输出是推挽还是开漏、低有效还是高有效STM32例程里一般在GPIO配置里设置。在Linux下这些信息全部来自设备树。设备树里的interrupt属性要跟硬件实际接法一致包括GPIO控制器、引脚号和触发方式。我们当时在这里踩了一个不小的坑设备树里写了IRQ_TYPE_EDGE_RISING但硬件上传感器的INT是开漏输出上拉电阻后默认高产生中断时被拉低怎么配都是反的最终不看原理图光靠猜浪费了小半天。2.2 I2C事务拆分与错误处理逻辑重构ST平台驱动读传感器数据通常是这样的顺序设置起始地址寄存器然后连续读若干个字节。比如读加速度计原始数据顺序写一次寄存器地址0x3B然后连续读14个字节。这个操作在STM32上直接一个I2C_Master_Recv就搞定了但在Linux I2C子系统里你要用两段式的i2c_transfer先发送一个写命令指定内部寄存器地址然后紧接着发一个读命令读指定长度数据。这两段要放到同一个消息数组里借助I2C的repeated start特性保证总线事务不被打断。static int mpu6050_i2c_read_regs(struct i2c_client *client, u8 reg, u8 *buf, int len) { struct i2c_msg msgs[2] { { .addr client-addr, .flags 0, .len 1, .buf reg, }, { .addr client-addr, .flags I2C_M_RD, .len len, .buf buf, }, }; if (i2c_transfer(client-adapter, msgs, 2) ! 2) { dev_err(client-dev, i2c read regs failed, reg 0x%02x\n, reg); return -EIO; } return 0; }这里就得好好聊一聊错误处理了。STM32裸机驱动里I2C读失败最常见的处理方式是重试几次——因为裸机环境下总线冲突概率低顶多是时序配置有问题。但在Linux内核里I2C传输失败的原因五花八门总线被其他设备占用锁死、从设备时钟拉伸导致超时、电源域未上电、总线频率太高斜率不达标等等。如果驱动里遇到EIO就傻傻地重试很可能导致内核线程卡在死循环里。更重要的是Linux I2C子系统的错误码是有语义的-EIO是总线错误-ENXIO是设备无应答-EAGAIN是总线忙。上层在retry的时候应该区分错误类型-EAGAIN这类才值得重试几次-ENXIO就该直接考虑是不是设备地址没配对、电源没上齐。这些逻辑在ST平台不会遇到移植时如果不补上后面做压力测试很容易被埋。2.3 初始化序列与校验逻辑按平台重新梳理MPU6050的初始化流程是固定的唤醒PWR_MGMT_1写0、配置采样率、配置量程、配置低通滤波。这些寄存器配置的值是ST官方标注好的不需要改动。但初始化完成后通常要读一下WHO_AM_I寄存器确认设备在线这个检查逻辑必须在Linux驱动里保留因为它能避免很多以为在转、其实没在转的问题。比较容易被忽略的是MPU6050的数据输出格式。加速度计和陀螺仪的原始数据都是有符号16位整数大端字节序。ST的例程里会拼一下字节序这在Linux下也要处理。但Linux内核的字节序辅助函数跟STM32裸机里有区别裸机里你可以直接手动移位拼接在Linux内核里建议用get_unaligned_be16或者be16_to_cpu有一类未对齐访问的问题是从STM32移植到ARM64/MIPS平台时最容易被忽略的——STM32F1是ARM Cortex-M3内核支持非对齐访问龙芯是MIPS架构非对齐访问会触发异常搞不好直接内核panic这一点一定要格外小心。还有一个容易忽略的点是MPU6050内置温度传感器的校准。ST库例程里会读温度寄存器换算出温度值换算公式是T 36.53 temp_reg / 340。这个公式里的偏移和灵敏度系数在量程配置不变时可以直接沿用但如果你的系统对温度要求高还是要做一个逐片校准。我们在龙芯平台上做整机测试时就发现传感器焊在板子上之后受CPU发热影响温度读数比环境温度高十几度这个现象在裸机调试时往往被忽略但上了Linux系统CPU负载上来后温度漂移会直接反映到陀螺仪零偏上。3. 实操过程从零到一在龙芯内核里把MPU6050驱动跑起来3.1 环境准备与内核配置这次移植工作的目标平台是龙芯2K1000这是一颗面向工业控制、电力、交通等嵌入式场景的SoC集成了两个LA264处理器核主频1GHz片上带了丰富的外设控制器。它跑Linux的方式跟主流ARM平台几乎一样都是通过设备树描述硬件内核版本我们用的是龙芯官方的Loongnix内核基于Linux 4.19内核演进而来这个版本对2K1000支持得很完整但IIO子系统的相关配置项需要手动开启。内核配置要注意打开这几个选项CONFIG_I2C必须、CONFIG_I2C_DESIGNWARE_PLATFORM之类对应的龙芯I2C控制器驱动、CONFIG_IIO、CONFIG_IIO_BUFFER、CONFIG IIO TRIGGERED BUFFER。其中CONFIG_IIO_TRIGGERED_BUFFER这个最容易漏如果没有它即使驱动里写了iio_triggered_buffer_setup也只是不报错但整个buffer功能是空的读不到数据。# 进入内核源码目录确认相关配置 make loongson2k_defconfig make menuconfig # 确认以下配置项: # Device Drivers - Industrial I/O support - [*] Industrial I/O support # Device Drivers - Industrial I/O support - [*] Industrial I/O buffering # Device Drivers - Industrial I/O support - [*] Triggered buffer support # Device Drivers - I2C support - [*] I2C support # 保存后编译 make -j$(nproc) uImage3.2 设备树节点的编写与验证设备树是Linux下描述硬件信息的关键。在龙芯平台I2C控制器节点通常已经在SoC的dtsi文件里定义好了我们只需要在板级dts文件里追加MPU6050这个从设备的子节点。设备树里要包含设备使用的地址、中断信息以及可能需要的私有属性。MPU6050的7位地址默认是0x68如果AD0引脚接了高电平则变成0x69这个要看实际硬件连接。i2c2 { status okay; clock-frequency 400000; mpu605068 { compatible invensense,mpu6050; reg 0x68; interrupt-parent gpio0; interrupts 11 IRQ_TYPE_EDGE_RISING; mount-matrix 0, 1, 0, -1, 0, 0, 0, 0, 1; }; };这里有一点要特别提一下interrupt-parent和interrupts这两个属性的写法不同平台的GPIO控制器不太一样。龙芯2K1000的GPIO控制器在设备树里的表示方式需要查对应手册别想当然抄ARM板卡的写法。写完设备树后先不要急着进内核用i2cdetect直接验证一下I2C总线上能不能扫到0x68这个地址这是最简单有效的一步。如果设备树和内核I2C控制器驱动都正常i2cdetect -y 2应该能在对应位置看到设备编号。3.3 驱动代码的移植与编译驱动代码的移植放在一个独立的C文件里直接放在drivers/iio/imu/目录下如果这目录已经在Makefile里被编译了话里面参考Linux内核里已有的mpu6050驱动框架大部分功能可以复用但因为我们是从零开始移植也没有直接引用内核里现成的MPU6050代码而是把ST平台的驱动程序逻辑重新用IIO框架实现了一遍这样更贴近我们自己的传输需求也方便后面自己维护。核心代码大概分三块一是I2C通信接口前面已经展示了i2c_read_regs的写法写寄存器方向类似但不需要repeated start。二是IIO通道描述定义设备支持的通道类型、精度、偏移量和位宽这一部分直接决定用户态读到的是什么数据。三是triggered buffer机制当采样触发到来时把加速度的X/Y/Z、陀螺仪的X/Y/Z共6个通道数据一次性读入buffer。#include linux/i2c.h #include linux/module.h #include linux/iio/iio.h #include linux/iio/buffer.h #include linux/iio/trigger_consumer.h #include linux/iio/triggered_buffer.h #define MPU6050_REG_ACCEL_XOUT_H 0x3B #define MPU6050_REG_GYRO_XOUT_H 0x43 #define MPU6050_READ_LEN 14 struct mpu6050_data { struct i2c_client *client; struct mutex lock; s16 buf[8]; /* 6轴数据 时间戳占位 */ }; static int mpu6050_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask) { struct mpu6050_data *data iio_priv(indio_dev); int ret; u8 reg (chan-type IIO_ACCEL) ? MPU6050_REG_ACCEL_XOUT_H chan-scan_index * 2 : MPU6050_REG_GYRO_XOUT_H (chan-scan_index - 3) * 2; mutex_lock(data-lock); ret mpu6050_i2c_read_regs(data-client, reg, (u8 *)data-buf[chan-scan_index], 2); mutex_unlock(data-lock); if (ret 0) return ret; *val be16_to_cpu(data-buf[chan-scan_index]); return IIO_VAL_INT; }编译的时候一定要确保内核头文件路径正确驱动编成模块还是编进内核要考虑清楚。开发阶段强烈建议编成模块M这样改代码不用整个内核重编直接insmod/rmmod循环测试。我们组在开发阶段就是一直用模块方式只有验证稳定后才考虑编进内核这个习惯能省下大量等编译的时间。3.4 用户态验证与数据读取驱动编译安装后用户态可以通过sysfs接口直接读到传感器数据。IIO框架会自动创建/sys/bus/iio/devices/iio:deviceX目录里面会列出available_scan_masks、in_accel_x_raw、in_gyro_y_raw之类的属性节点。直接cat这些节点如果数值在合理范围变化比如轻轻晃动板子时加速度计读数有明显波动说明最基础的寄存器通路的读写已经正常。# 查看设备是否被正确识别 cat /sys/bus/iio/devices/iio:deviceX/name # 应该是输出 mpu6050 或者你自己定义的 compatible 对应的名字 # 读取一个原始加速度值 cat /sys/bus/iio/devices/iio:deviceX/in_accel_x_raw # 如果板子水平静止数值应该在0附近波动但注意直接cat属性节点走的是read_raw回调是逐个通道单独读的。如果要拿到一组同步的六轴数据必须启用buffer模式。IIO的buffer模式需要先设置扫描元素再使能buffer最后通过/dev/iio:deviceX设备节点以指定格式读取。具体操作可以通过内核提供的工具iio_generic_buffer来做也可以通过驱动程序自带的trigger触发。这里要提醒的是别指望用cat命令读/dev/iio:deviceX——它是二进制流不是文本必须用工具读。我们当时用了一个小脚本每100ms从buffer模式里取一帧数据做一次静态校验把重力加速度的模长算一下正常应该在9.8m/s²附近波动不超过0.2。如果算出来的模长偏离非常大多半是量程或者灵敏度系数填错了。这一步校验很值能把配置错误和硬件问题快速分开。MPU6050加速度计的量程选择±2g/±4g/±8g/±16g直接决定了换算系数的分母如果驱动里读到的原始值按±2g去换算但实际配置的是±16g那算出来的重力模长就会明显偏大。量程、灵敏度和姿态算法的系数是强关联的这里错了后面全错。4. 常见问题与排查技巧实录4.1 I2C总线挂在半路stuck bus的处理这次移植遇到的最典型的问题就是I2C总线卡死。现象是设备树和内核都配置正确但i2cdetect什么都扫不到用示波器看SDA线一直低电平这就是所谓的总线stuck。排查步骤很直接先把I2C控制器复位一遍确认从设备供电、上拉电阻无误再看是否是有别的驱动在总线事务中途被打断了。但有一个在Linux下特有、裸机下少见的坑I2C控制器中断号冲突或者DMA通道冲突会导致总线事务异常。龙芯2K1000的I2C控制器支持DMA模式如果在设备树里配置了DMA但DMA通道被其他设备占用了启动的时候不报错但跑着跑着总线事务就会断在半路从而造成stuck。当时我们查了半天都不明所以最后在dmesg里看到i2c_designware的timeout警告顺着线索才定位到DMA通道冲突。解决方法是暂时禁用I2C控制器的DMA功能在设备树里将dmas属性删掉改成纯中断模式。对于MPU6050这种低速传感器DMA完全没必要中断模式足够还少了一层复杂度。4.2 设备树匹配不上为什么compatible对不上另一个高频问题是内核驱动明明编译进去了但/sys/bus/i2c/devices下面就是没有设备节点。排查思路是这样的先看设备树节点有没有被正确加载可以看/proc/device-tree或者debugfs里的of_node信息再看I2C总线上的设备有没有被扫描到i2cdetect -l列出总线号i2cdetect -y 总线号看设备地址如果设备扫到了但对不上驱动大概率是compatible字符串不匹配。这里有个小细节驱动里的of_match_table里的compatible字符串必须跟设备树里的完全一致包括大小写和点号。我们当时把invensense,mpu6050写成了invensense,mpu60x0就差一个字符结果驱动就是挂不上。这个纯属手滑但排查起来又慢又气后来养成了习惯——写完设备树和驱动后先用一个命令验证compatible是否存在对应驱动# 查看i2c总线上已注册的设备和驱动匹配情况 ls /sys/bus/i2c/devices/ cat /sys/bus/i2c/devices/2-0068/name # 如果能读出名字说明匹配成功4.3 数据持续为0或跳动异常寄存器配置与并发访问排查数据全为0这种问题通常不是I2C读失败而是寄存器地址或者读取长度写错了。比如读加速度计X轴起始地址是0x3B读两个字节。如果起始地址误写成了0x3DY轴那X轴读出来的自然就是0。这种错误在裸机调试时一眼就能在调试器里看出来但在Linux下需要加打印或者用逻辑分析仪抓I2C波形才能定位。还有一个隐蔽的问题是并发访问。如果你的驱动里没有加锁而用户态同时有两个线程在读同一个通道I2C总线访问就会交叉读回来的数据有可能是两个通道的混合体。在STM32裸机里这种并发问题一般不会发生因为你的主循环是单线程的但在Linux内核里read回调可能被多个进程同时调用不加mutex就会偶发性拿到错位数据。这一点我们一开始没有注意直到做1000次连续采样稳定性测试时才暴露出来加锁后就消失了。4.4 常见问题速查表现象可能原因排查手法解决办法i2cdetect扫不到设备上拉电阻、供电、总线stuck示波器量SDA/SCL波形检查硬件复位I2C控制器i2cdetect有设备但无驱动节点compatible不匹配cat /sys/bus/i2c/devices/*/name修正设备树或of_match_table驱动注册成功但数据全为0寄存器地址或读取长度错误逻辑分析仪抓I2C时序对照手册核对寄存器映射数据跳动剧烈量程配置与换算系数不一致静态重力模长校验统一量程和灵敏度系统启动后偶发I2C timeoutDMA通道冲突或时钟频率过高dmesg查timeout警告禁用DMA或降频至100kHz频繁读取时有错位数据并发访问未加锁多线程压力测试复现加mutex保护I2C事务中断触发频率异常中断触发方式或GPIO配置错误中断测试脚本来验证按原理图修正设备树中断属性编译报错找不到头文件内核头文件路径不对检查Kbuild/Makefile补全依赖头文件5. 最后再分享一点心得做驱动移植最忌讳的就是一头扎进代码里猛改改着改着忘了最初的边界。每次动手前先把这个平台上哪些是系统给你的、哪些是要自己写的想清楚能省下一大半调试时间。这次的MPU6050移植对我们组来说只算个开始后面还有气压计、磁力计、多路PWM输出要全部从ST平台搬过来。有了这一次的经验后面几个驱动的移植速度明显快了很多——因为架构思路通了搬的就不再是一行行代码而是一套成熟的映射关系哪些按原样保留哪些必须重写哪些要格外小心。希望这篇流水账一样的记录也能让正在做类似事情的朋友少走几次弯路。
返回列表