1. 项目缘由:龙芯k和“走马观碑组”的MPU到底指什么
1.1 先说清这是一次什么样的移植
这周我们组干了个说大不大、说小不小的活:把MPU6050六轴传感器的驱动,从我们之前折腾过的平台迁到龙芯k这块主控板上。组名“走马观碑组”听起来有点飘,实际上干的活儿特别具体——龙芯k板子上要用姿态数据做后续控制,板子自带的存储和外设不算丰富,得外挂一颗IMU来获取三轴加速度和三轴角速度,所以驱动移植就成了绕不开的一关。
很多人看到“龙芯k - 走马观碑组MPU驱动移植”这个标题,第一反应是:龙芯平台上还有Memory Protection Unit?是不是要做内存保护?这里必须先踩一脚刹车。项目里说的MPU,指的是InvenSense的MPU6050惯性传感器,不是ARM Cortex-M处理器里的那个MPU内存保护单元。这点如果不提前讲清楚,后面读代码读设备树时很容易整岔。顺便说一句,网上常有人搜“h750 mpu怎么设置”,那个多半是在问STM32H750里Memory Protection Unit怎么配置,和我们这颗传感器完全是两码事。
真正要做的事情,是把MPU6050的Linux驱动跑在龙芯k环境里,让用户态程序能通过I2C总线拿到加速度和角速度原始值。刚接到需求时我觉得这不算难,毕竟MPU6050是一颗烂大街的传感器,开源驱动一大把,Linux内核里也有官方维护的inv_mpu6050驱动。可真上手后发现,“驱动移植”这四个字的难点不在MPU6050本身,而在新平台的I2C总线上有没有被正确枚举、中断引脚能不能申请、内核配置有没有把相关子系统打开,以及驱动依赖的设备树节点对不对路。
1.2 为什么叫移植而不是叫开发
关于为什么要用“移植”这个词,我也提一下。MPU6050的寄存器手册是固定的,I2C通信时序也是公开的,从原理上讲,任何平台读写它都差不多。所以多数情况下不需要从零写一个传感器驱动,而是把已有代码搬到新板子上,再根据新平台的差异做适配。这个过程就是典型的“驱动移植”。
移植的工作量通常集中在三块:硬件连接、设备树描述、驱动代码与当前内核API的兼容性。我们这次主要参考了Linux内核自带的inv_mpu6050驱动,它服务于IIO子系统,同时也会替换成一份自定义的简易I2C字符设备驱动做对比验证。后续整篇记录都会围绕这三块展开,顺便把过程中踩过的坑和排查思路都写清楚,希望对同样在龙芯或其它嵌入式Linux平台上接MPU6050的朋友有点帮助。
1.3 这个板子能干什么、适合谁参考
龙芯k这块板子本质是一个嵌入式Linux平台,适合跑应用、做控制。我们的最终目标是在上面跑通一个IMU数据读取链路:MPU6050 采集原始数据,CPU 通过 I2C 读取,再换算成物理单位,供后续姿态解算或避障算法调用。在整个项目里,我们走马观碑组负责的“驱动移植”,是链路最底层的那一环。
这篇内容适合几类人看:手里刚好有龙芯或类似国产嵌入式Linux板子,想外接MPU6050的;在别的平台已经调通过传感器,却不知道怎么把驱动迁移到新内核版本上的;还有一种就是刚接触Linux驱动、想通过一个具体传感器把I2C子系统、设备树、中断机制串起来理解的人。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 移植前的关键准备:硬件接线、内核环境与MPU6050寄存器
2.1 先把硬件和I2C地址确认清楚
很多驱动调不通的问题,根源在硬件连接就没对。MPU6050标准接口一共四根线:VCC、GND、SCL、SDA。有些模块还带XDA、XCL、AD0和INT,但我们只用到基础四线,外加一个INT引脚用来上报数据就绪中断。
龙芯k板子上有两路I2C控制器,我用万用表直接量了板载排针,确定要用的那一路是I2C0,SCL和SDA上都已经有10k左右的上拉电阻。这一点很重要:如果传感器模块本身没有上拉,而板子的I2C总线又没接上拉,那SDA和SCL就一直是浮空状态,Linux的i2cdetect会报Bus error或者什么设备都扫描不到。
MPU6050的7位I2C地址默认是0x68。如果AD0引脚被拉高,地址会变成0x69。我们这块模块的AD0默认接地,所以设备地址就是0x68。在裸机环境下写代码时,经常需要把7位地址左移一位变成0xD0/0xD1再配合读写位使用,但Linux内核I2C子系统里统一使用7位地址,直接填0x68就行,不需要再左移。这也是我见过不少人从STM32裸机习惯迁过来后翻车的地方。
2.2 龙芯k平台的开发环境与内核准备
这次龙芯k板子跑的是Linux 5.10内核,开发机上装好了交叉编译工具链。我习惯把内核源码单独放到工作目录,编译内核模块时直接引用内核源码路径。如果只是加载一个外部模块,并不需要完整编译整个内核,但一定要有已经编译好的内核树,也就是源码和生成过Module.symvers的目录。否则模块编译时会报找不到内核头文件或版本不匹配的错误。
我们这次有两种驱动选择方案。第一种是直接用内核自带的IIO驱动:把CONFIG_IIO、CONFIG_INV_MPU6050_I2C、CONFIG_INV_MPU6050_IIO选上,重新编译内核或相关模块;第二种是编译一个自己的外部模块,注册一个misc设备,把MPU6050的原始寄存器数据直接暴露给应用程序。两种方案各有利弊,IIO驱动的好处是功能完整,连DMP、FIFO、扫描元素这些高级特性都覆盖了;坏处是如果你想看原始寄存器意义,会被框架包装掉不少。简单字符驱动的好处是直观,适合教学和快速验证,坏处是扩展性和并发处理都得自己写。
考虑到我们是做驱动移植记录,最终决定两手都试。先用内核自带驱动把设备树和I2C枚举验证清楚,再用一个精简外部模块把整个寄存器读写流程重新走一遍。这样既能验证平台,又能把MPU6050的工作原理搞清楚。
2.3 MPU6050寄存器里最核心的几个地址
在写任何代码之前,要先把MPU6050的寄存器结构理顺。这里我列一下驱动初始化时一定会碰到的几个关键寄存器:
- 0x6B PWR_MGMT_1:电源管理寄存器。上电默认值很多手册写的是0x40,代表设备处于睡眠模式。所以第一步通常先写0x80触发复位,延时50ms,再写0x01,让传感器用PLL时钟源唤醒,也可以写0x00但推荐0x01。
- 0x19 SMPLRT_DIV:采样率分频器。陀螺仪输出速率默认1kHz,采样频率 = 1kHz / (1 + SMPLRT_DIV)。写入7就是125Hz。
- 0x1A CONFIG:配置数字低通滤波器和内部时钟。一般写0x03或0x06,取决于你要的带宽。
- 0x1B GYRO_CONFIG:陀螺仪量程配置。写入0x18代表满量程±2000°/s,对应灵敏度16.4 LSB/°/s。
- 0x1C ACCEL_CONFIG:加速度计量程配置。写入0x10代表满量程±8g,对应灵敏度4096 LSB/g。
- 0x75 WHO_AM_I:只读寄存器。正常情况读出来是0x68,这个值可以用来确认I2C通信是否已经建立。
还有一组数据寄存器,加速度计是0x3B到0x40,陀螺仪是0x43到0x48,每个轴各占两个字节,高字节在前。读取时可以直接从0x3B一次性读14个字节,包括加速度计三轴、温度、陀螺仪三轴。每次读之前最好先确认0x3A里的FIFO设置没有干扰到普通读取模式。
2.4 读MPU6050时要理解的I2C通信流程
MPU6050的I2C读操作比较典型,是先写寄存器地址,再发起读事务。比如要读WHO_AM_I,主控发送起始条件,发送0x68写位,发送要读的寄存器地址0x75,然后重新发送起始条件,发送0x68读位,接收一个字节,最后发送停止条件。
在Linux里,这个流程可以拆成两条i2c_msg组成的数组:第一条是写寄存器地址,第二条是读数据。如果直接在驱动里调用i2c_transfer,需要自己构造i2c_msg结构体,不能像I2C裸机代码那样直接用库函数自动完成。很多第一次写Linux I2C驱动的人在这里会纠结:为什么读6个字节需要两个message,因为他们不熟悉Linux i2c_transfer的“完整事务”概念。
3. 龙芯k上移植MPU6050驱动的实操全过程
3.1 第一步:用i2cdetect确认设备是否总线上可见
先把内核的I2C设备驱动模块准备好,龙芯k板子上电进入系统后,用i2cdetect -l查看有几条I2C总线。我这边看到的输出里,I2C0对应的总线号是0。如果总线上已经有内核驱动绑定设备,直接扫描可能会失败,需要用i2cdetect -y -r 0强制读扫描。
实际执行i2cdetect -y -r 0后,地址0x68位置会显示编号,说明MPU6050已经挂在总线上。这里有一个细节:如果系统里已经加载了MPU6050驱动,扫描地址可能会返回UU,这是正常的,表示该地址被内核驱动占用。没看到0x68就需要往下排查硬件连接或设备树了。
有些总线扫描模式下,读取某些地址会触发设备异常,因此有人推荐用-r模式让SMBus以READ方式扫描。MPU6050对这种扫描是支持的。如果扫描完全不显示,大概率不是代码问题,而是上拉电阻、供电或者SDA/SCL接反的问题。
3.2 第二步:添加设备树节点
龙芯k平台使用设备树机制描述板载外设。要告诉内核“这颗MPU6050接在I2C0上,地址是0x68,中断脚是GPIO,属性是什么”,就需要在设备树源文件里添加一个子节点。
下面是我们这次实际使用的设备树片段,总线母节点是I2C0:
code复制&i2c0 {
status = "okay";
clock-frequency = <400000>;
mpu6050@68 {
compatible = "invensense,mpu6050";
reg = <0x68>;
interrupt-parent = <&gpio0>;
interrupts = <19 IRQ_TYPE_EDGE_RISING>;
vdd-supply = <®_3p3v>;
vddio-supply = <®_3p3v>;
};
};
compatible字符串必须和驱动里的of_match_table匹配。内核自带inv_mpu6050驱动匹配的compatible是“invensense,mpu6050”。如果你用的是第三方驱动,这个字段要改成驱动自己声明的值,否则设备树扫描不到设备,系统log里也不会出现驱动探测函数被调用。
register属性是0x68,我之前见过有人写0xD0,这是把7位地址和8位地址搞混了。设备树里要求写7位地址,可以理解成i2c_transfer时用到的那个地址值。interrupt-parent和interrupts也要和原理图对应。我们板子上INT连到了GPIO0模块的19号脚,上升沿触发。
写设备树时最容易忽略的是父节点I2C0有没有被设置为okay。很多平台默认状态下外设控制器处于disabled状态,如果不把status改成okay,后面所有操作都是白搭。
3.3 第三步:配置内核和自带IIO驱动
内核自带的MPU6050驱动位于drivers/iio/imu/inv_mpu6050目录。它支持I2C、SPI两种接口。我们用的是I2C,所以要开启以下配置:
code复制CONFIG_IIO=y
CONFIG_INV_MPU6050_I2C=y
CONFIG_INV_MPU6050_IIO=y
如果你只是加载模块而不是重新编译整个内核,也可以先确认内核源码树已经编译过。执行make menuconfig,在Device Drivers -> Industrial I/O support -> Inertial measurement units下面选中MPU6050。保存配置后,编译相关模块:
code复制make ARCH=arm CROSS_COMPILE=你的交叉编译器前缀 -j8 modules
如果模块编译正常,会生成inv_mpu6050_i2c.ko和inv_mpu6050.ko。把这两个模块复制到板子上,加载顺序是先加载inv_mpu6050.ko,再加载inv_mpu6050_i2c.ko,也可以依赖modprobe自动处理。加载完成后,dmesg应该能看到驱动绑定了mpu6050设备。
设备树配好之后,设备会出现在/sys/bus/iio/devices/下。一般路径是iio:device0,进去后可以看到in_accel_x_raw、in_gyro_y_raw、in_accel_scale等属性文件。执行下面命令即可读到原始数据:
code复制cat /sys/bus/iio/devices/iio:device0/in_accel_x_raw
我在实际操作中遇到的第一个状况是:i2cdetect能看到0x68,但模块加载后dmesg完全没有驱动探测记录。这说明设备树节点没写好或驱动没有正确启用。通过检查/sys/firmware/devicetree/base/i2c0/目录,确认设备树里确实生成了mpu6050@68节点,才意识到问题出在内核配置没有打开IIO。
3.4 第四步:如果不用IIO,写一个最小字符驱动验证
为了把MPU6050的工作流程讲得更底層,我又写了一个极简外部模块,没有走完整的IIO框架,只注册一个misc设备,通过file_operations开放读写。这样做的价值在于,能看到一次完整的I2C寄存器读写在Linux驱动里是怎么组织的。
注册入口的核心代码如下:
code复制static int mpu6050_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct mpu6050_dev *dev;
int ret;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
dev->client = client;
dev_set_drvdata(&client->dev, dev);
ret = mpu6050_init_regs(dev);
if (ret)
goto err_free;
ret = misc_register(&dev->miscdev);
if (ret)
goto err_free;
return 0;
err_free:
kfree(dev);
return ret;
}
这里有一个要点:misc_register并不是完整工业级驱动该用的方式,它只适合简单的教学或轮询读取。因为后续如果要开FIFO、DMP、异步通知,还是要回到IIO框架。但在“快速验证一个外设能不能通”这个阶段,misc设备足够省事。
在read函数里,我直接通过i2c_transfer读取0x3B开始的14个字节。读取流程如下:
code复制static int mpu6050_read_raw(struct i2c_client *client,
u8 *buf, size_t len)
{
struct i2c_msg msgs[2] = {
{
.addr = client->addr,
.flags = 0,
.len = 1,
.buf = (u8[]){0x3B},
},
{
.addr = client->addr,
.flags = I2C_M_RD,
.len = len,
.buf = buf,
},
};
return i2c_transfer(client->adapter, msgs, 2) == 2 ? 0 : -EIO;
}
新手经常在这里问:为什么读数据前要先写一个寄存器地址?答案很简单,MPU6050内部有寄存器地址指针,主控不发地址它不知道你要读哪块内存。两段式I2C事务在Linux中用两个msg表示,这是驱动层最经典的写法之一。
初始化序列我这里再贴一个参考版本:
code复制static void mpu6050_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
u8 buf[2] = { reg, val };
struct i2c_msg msg = {
.addr = client->addr,
.flags = 0,
.len = 2,
.buf = buf,
};
i2c_transfer(client->adapter, &msg, 1);
}
static void mpu6050_init_regs(struct i2c_client *client)
{
mpu6050_write_reg(client, 0x6B, 0x80); /* reset */
msleep(50);
mpu6050_write_reg(client, 0x6B, 0x01); /* wake up */
mpu6050_write_reg(client, 0x19, 0x07); /* 125Hz */
mpu6050_write_reg(client, 0x1A, 0x03); /* low pass filter */
mpu6050_write_reg(client, 0x1B, 0x18); /* gyro +-2000dps */
mpu6050_write_reg(client, 0x1C, 0x10); /* accel +-8g */
}
每次写寄存器时最好保持足够延时。尤其是触发复位后,如果立刻往下写PWR_MGMT_1,传感器内部的复位逻辑可能还没完成,写进去的值会被覆盖或丢失。我一开始就碰到读WHO_AM_I正常,但设置量程后读数据仍然是默认量程的问题,最后排查下来是复位后延时不够。
4. 常见问题与排查技巧实录
4.1 i2cdetect扫不到0x68,先别怀疑代码
我在龙芯k上第一次上电时,i2cdetect根本看不到任何设备。这种问题九成是硬件连线问题。先量VCC是否为3.3V,GND是否和板子共地,SCL和SDA有没有接反。MPU6050模块如果不带电平转换,接到3.3V逻辑IO上问题不大;如果接到5V电平上,传感器可能工作不稳定甚至永久损坏。
再看板子I2C控制器是否被其它外设占用。龙芯k平台有可能把同一路I2C同时接到其它传感器或EEPROM上,虽然多设备可以共总线,但如果有地址冲突或某个设备把SDA线拉死,扫描也会失败。最快的方法是只留MPU6050在总线上,断开其它I2C设备,再试一次。
软件方面还有一个坑:如果设备树里I2C0状态被写成disabled,内核不会真正初始化这个控制器。先查/sys/bus/i2c/devices/i2c-0/目录下的name或删除设备树里其它影响,再回来看是不是控制器根本没起来。
4.2 WHO_AM_I能读出来,但加速度和陀螺仪都是0
驱动探测成功,WHO_AM_I也读到了0x68,按理说通信链路已经通了。纯原始数据类问题就比较隐蔽,常见原因是PWR_MGMT_1里sleep位没清掉。上电后传感器的默认状态可能是睡眠,如果你没有在初始化里执行写0x80复位再写0x01唤醒,寄存器虽然可以读,但传感器内部并没有开始刷新。
第二次再读数据时不要只读一个字节看变化。最好一次性读6个轴,加打印看数值是否在波动。我遇到过一种情况:加速度计Z轴一直接近满量程,X、Y轴都是0,这是传感器放在桌面上但没有正确配置量程和低通滤波导致的。这种时候要看读回来的原始值有没有符号扩展,有没有按照大端组合成有符号short。
另外还有一个容易被忽略的是I2C读取的地址增量问题。MPU6050的寄存器支持自动递增,如果你用的是不带自动递增的I2C命令,每次手动读一个地址,那就必须重新发送寄存器地址。按标准两段式读法,是支持从0x3B开始连续读14字节的,只要buf大小足够就行。
4.3 H750上能用的经验,到Linux上为什么不好使
网上有不少“h750 mpu怎么设置”的问题,很多是从STM32H750裸机或RT-Thread场景学来的。在H750里,你会自己初始化GPIO和I2C外设,然后写一个MPU6050_Init函数,通过HAL库函数发寄存器命令,通过Delay控制时序。这些代码换到龙芯k的Linux环境后,最明显的变化是你不应该再手动初始化I2C控制器了。
在Linux中,I2C控制器的初始化由内核对应驱动完成。你在驱动里只需要通过i2c_adapter拿到总线的控制能力。设备地址也不是裸机里的0xD0,而是0x68。延时函数不能用HAL_Delay,而要用msleep或usleep_range。这类问题其实不是MPU6050本身变了,而是运行环境从裸机变成了操作系统,资源管理方式完全不同。
你以前在H750上调试MPU6050的经验里,真正能迁移过来的,是对寄存器理解、量程设置、数据拼接方式,以及中断脚怎么接的认识。底层API和总线模型需要按照Linux的规矩重写。这也正是“移植驱动”和“重写驱动”的区别:逻辑复制,接口重做。
4.4 中断引脚申请失败和处理方法
如果想把数据就绪中断用起来,设备树里的interrupts属性最关键。龙芯k的GPIO中断控制器可能比较特殊,不能盲目照搬STM32的GPIO模式配置。需要查明自己的INT引脚是哪个GPIO控制器、第几个bank、是否支持外部中断。
我遇到过的情况是中断号和另一个设备冲突,导致驱动probe时request_threaded_irq返回EBUSY。排查时先看dmesg里的错误信息,再用cat /sys/kernel/debug/gpio查看gpio状态,最后确认设备树里中断号和实际物理引脚是不是对得上。很多国产平台设备树对GPIO的编号规则和ARM平台不完全一致,不能想当然把GPIO编号当物理引脚号。
中断触发方式建议先用上升沿。MPU6050中断引脚默认是50us左右的高脉冲,如果你设成边沿触发,IRQ handler需要在很短时间内拾取。如果handler里要读数据,最好在中断上半部只置标志位,用threaded irq去读I2C,避免在原子上下文里访问慢速总线导致内核报警。
5. 驱动能跑之后的扩展与最后提醒
5.1 从原始数据到物理单位:scale该填多少
mpu6050驱动跑通后,应用层可能直接拿到的是驱动里的原始LSB值。比如读取in_accel_z_raw返回12000,这个数字不换算成g没有意义。换算的缩放系数其实在初始化时就已经由量程决定了,不像某些传感器支持运行时动态查询。
如果使用IIO,标准做法是同时读scale属性。比如加速度量程设为±8g时,原始值除以4096就是重力加速度倍数。陀螺仪量程设为±2000°/s时,原始值除以16.4就是°/s。这个换算系数和量产校准没有关系,只是量程对应的灵敏度,不少初学者容易搞混。
一个项目里如果只用IIO自带驱动,应用层要同时订阅raw和scale两个节点;如果直接用misc设备读取原始寄存器,就必须在应用层写死灵敏度常量。我个人的建议是:正式项目尽量用IIO框架,至少在规范性和可维护性上有保证。
5.2 后续驱动移植还能在哪些方向扩展
这次移植相当于把MPU6050最基本的I2C链路跑通。再往后,我们走马观碑组至少还有三件事可以做。
第一件事是加DMP支持,利用MPU6050内部的数字运动处理器直接输出四元数,把解算压力从CPU转移出去。Linux内核里的inv_mpu6050驱动对DMP支持比较谨慎,需要额外固件加载和配置流程,这个移植起来比普通读原始数据要复杂。第二件事是启用FIFO,在高频率采样场景下,FIFO能避免CPU频繁被打断,但要做好溢出管理和中断读取逻辑。第三件事是把数据用netlink或input子系统上传给上层应用,让更高层的控制程序不用直接面对sysfs。
从个人经验看,MPU6050驱动移植这件事,真正敲开门的不是“写一个i2c_read函数”,而是理解一个新平台如何描述设备、如何管理总线资源和中断。如果能在龙芯k上把这套机制弄明白,换成其它I2C传感器或者其它Linux平台,流程都差不多。最后提醒一句,如果你也被“h750 mpu怎么设置”这类检索带偏过,冷静下来把设备树、I2C总线、传感器电源引脚三个点重新检查一遍,大概率问题已经解决一半了。
