1. MPU6050与I2C子系统概述
MPU6050是一款集成了3轴陀螺仪和3轴加速度计的6轴运动处理传感器,广泛应用于无人机、平衡车、手机等需要运动检测的设备中。这款传感器通过I2C接口与主控芯片通信,是学习Linux I2C子系统开发的理想案例。
I2C(Inter-Integrated Circuit)是一种简单、双向二线制的同步串行总线,由Philips公司开发。它只需要两根线:串行数据线(SDA)和串行时钟线(SCL)。I2C总线支持多主多从架构,每个连接到总线的设备都有唯一的地址。
在Linux系统中,I2C子系统分为三个层次:
- I2C核心层:提供总线驱动和设备驱动的注册、注销方法
- I2C总线驱动层:针对特定适配器的实现
- I2C设备驱动层:针对特定设备的驱动实现
理解这三个层次的关系对于开发I2C设备驱动至关重要。核心层负责管理总线驱动和设备驱动,总线驱动负责特定控制器的硬件操作,设备驱动则负责特定设备的业务逻辑。
2. 硬件准备与连接
2.1 MPU6050硬件特性
MPU6050的主要特性包括:
- 数字输出6轴运动处理数据
- 用户可编程的陀螺仪满量程范围:±250、±500、±1000、±2000°/秒
- 用户可编程的加速度计满量程范围:±2g、±4g、±8g、±16g
- 数字可编程低通滤波器
- 工作电压:2.375V-3.46V
- 低功耗模式
- 400kHz快速模式I2C接口
2.2 硬件连接指南
MPU6050与开发板的连接非常简单,只需要4根线:
| MPU6050引脚 | 开发板引脚 | 说明 |
|---|---|---|
| VCC | 3.3V | 电源 |
| GND | GND | 地线 |
| SCL | I2C3_SCL | 时钟线 |
| SDA | I2C3_SDA | 数据线 |
注意:不同开发板的I2C接口编号可能不同,需要查阅开发板手册确认。在我们的例子中使用的是I2C3接口。
2.3 地址选择
MPU6050的I2C地址由AD0引脚决定:
- AD0接地:地址为0x68
- AD0接VCC:地址为0x69
大多数开发板默认将AD0接地,因此地址通常为0x68。如果系统中需要连接多个MPU6050,可以通过改变AD0引脚的电平来区分它们。
3. I2C工具使用详解
3.1 i2c-tools安装与基本命令
i2c-tools是Linux下调试I2C设备的必备工具集,包含以下几个常用命令:
- i2cdetect:检测I2C总线上的设备
- i2cget:读取I2C设备的寄存器
- i2cset:设置I2C设备的寄存器
- i2cdump:导出I2C设备的所有寄存器值
- i2ctransfer:执行复杂的I2C传输
安装命令:
bash复制sudo apt update
sudo apt install i2c-tools -y
3.2 设备检测与验证
首先使用i2cdetect扫描I2C总线上的设备:
bash复制i2cdetect -y 3
参数说明:
- -y:跳过确认提示
- 3:I2C总线编号
执行结果示例:
code复制 0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
可以看到地址0x68处有设备响应,这就是我们的MPU6050。
3.3 寄存器读写操作
MPU6050有多个重要寄存器需要配置:
3.3.1 电源管理寄存器
地址0x6B(PWR_MGMT_1)控制芯片的电源模式:
bash复制i2cset -y 3 0x68 0x6B 0x00
这条命令将PWR_MGMT_1寄存器设置为0x00,唤醒芯片并使用内部时钟源。
3.3.2 陀螺仪配置寄存器
地址0x1B(GYRO_CONFIG)设置陀螺仪量程:
bash复制i2cset -y 3 0x68 0x1B 0x08
0x08对应±500°/s的量程。
3.3.3 加速度计配置寄存器
地址0x1C(ACCEL_CONFIG)设置加速度计量程:
bash复制i2cset -y 3 0x68 0x1C 0x00
0x00对应±2g的量程。
3.3.4 读取设备ID
地址0x75(WHO_AM_I)存储设备ID:
bash复制i2cget -y 3 0x68 0x75
原装MPU6050返回0x68,兼容芯片可能返回0x70。
3.4 高级I2C操作
i2ctransfer命令可以执行复杂的I2C传输,模拟重复起始条件:
bash复制i2ctransfer -y 3 w1@0x68 0x3B r2
这条命令先写入寄存器地址0x3B(加速度计X轴高位),然后读取2个字节的数据。
4. 用户层程序开发
4.1 I2C设备节点
Linux系统将I2C控制器抽象为设备文件,通常位于/dev/i2c-*。我们的MPU6050连接在I2C3上,对应设备文件是/dev/i2c-3。
4.2 核心数据结构
用户层I2C编程主要使用两个结构体:
4.2.1 i2c_msg
定义单个I2C消息:
c复制struct i2c_msg {
__u16 addr; // 从设备地址
__u16 flags; // 读写标志
__u16 len; // 消息长度
__u8 *buf; // 数据缓冲区
};
flags常用值:
- I2C_M_RD:读操作
- 0:写操作
4.2.2 i2c_rdwr_ioctl_data
封装多个i2c_msg:
c复制struct i2c_rdwr_ioctl_data {
struct i2c_msg *msgs; // 消息数组
__u32 nmsgs; // 消息数量
};
4.3 完整示例程序
下面是一个完整的用户层程序,实时读取加速度计X轴数据:
c复制#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/i2c.h>
#include <linux/i2c-dev.h>
int main()
{
int fd;
// 打开I2C设备
fd = open("/dev/i2c-3", O_RDWR);
if(fd < 0) {
perror("open failed");
return -1;
}
// 唤醒芯片
uint8_t wake_data[] = {0x6B, 0x00};
struct i2c_msg wake_msg = {
.addr = 0x68,
.flags = 0,
.len = 2,
.buf = wake_data,
};
struct i2c_rdwr_ioctl_data wake_packets = {
.msgs = &wake_msg,
.nmsgs = 1,
};
ioctl(fd, I2C_RDWR, &wake_packets);
// 配置陀螺仪和加速度计
uint8_t gyro_config[] = {0x1B, 0x08};
struct i2c_msg gyro_msg = {
.addr = 0x68,
.flags = 0,
.len = 2,
.buf = gyro_config,
};
uint8_t accel_config[] = {0x1C, 0x00};
struct i2c_msg accel_msg = {
.addr = 0x68,
.flags = 0,
.len = 2,
.buf = accel_config,
};
struct i2c_rdwr_ioctl_data config_packets = {
.msgs = (struct i2c_msg[]){gyro_msg, accel_msg},
.nmsgs = 2,
};
ioctl(fd, I2C_RDWR, &config_packets);
// 主循环读取加速度数据
while(1) {
uint8_t reg = 0x3B; // 加速度X轴高位寄存器
uint8_t data[2] = {0};
struct i2c_msg msg[2] = {
{
.addr = 0x68,
.flags = 0,
.len = 1,
.buf = ®,
},
{
.addr = 0x68,
.flags = I2C_M_RD,
.len = 2,
.buf = data,
}
};
struct i2c_rdwr_ioctl_data packets = {
.msgs = msg,
.nmsgs = 2,
};
if(ioctl(fd, I2C_RDWR, &packets) < 0) {
perror("ioctl failed");
break;
}
// 合成16位数据(大端序)
int16_t acc_x = (data[0] << 8) | data[1];
printf("Acceleration X: %d\n", acc_x);
usleep(100000); // 100ms间隔
}
close(fd);
return 0;
}
4.4 程序编译与运行
编译命令:
bash复制gcc mpu6050_user.c -o mpu6050_user
运行程序:
bash复制./mpu6050_user
程序会持续输出加速度计X轴的原始数据,晃动传感器可以看到数值变化。
5. 内核驱动开发
5.1 设备树配置
在设备树中添加MPU6050节点:
dts复制&i2c3 {
mpu6050@68 {
compatible = "invensense,mpu6050";
reg = <0x68>;
status = "okay";
};
};
关键点:
- 节点必须作为I2C控制器的子节点
- compatible属性用于驱动匹配
- reg属性指定I2C地址
- status属性设置为"okay"启用设备
5.2 驱动框架
Linux I2C驱动框架与Platform驱动类似,主要包含以下部分:
5.2.1 驱动结构体
c复制static struct i2c_driver mpu6050_driver = {
.driver = {
.name = "mpu6050",
.of_match_table = mpu6050_of_match,
},
.probe = mpu6050_probe,
.remove = mpu6050_remove,
.id_table = mpu6050_id,
};
5.2.2 匹配表
c复制static const struct of_device_id mpu6050_of_match[] = {
{ .compatible = "invensense,mpu6050", },
{},
};
MODULE_DEVICE_TABLE(of, mpu6050_of_match);
5.3 SMBus API详解
内核提供了多种SMBus API用于I2C通信:
5.3.1 字节操作
c复制// 读取一个字节
s32 i2c_smbus_read_byte_data(const struct i2c_client *client, u8 command);
// 写入一个字节
s32 i2c_smbus_write_byte_data(const struct i2c_client *client, u8 command, u8 value);
5.3.2 字操作
c复制// 读取一个字(16位)
s32 i2c_smbus_read_word_data(const struct i2c_client *client, u8 command);
// 写入一个字(16位)
s32 i2c_smbus_write_word_data(const struct i2c_client *client, u8 command, u16 value);
5.3.3 块操作
c复制// 读取多个字节
s32 i2c_smbus_read_i2c_block_data(const struct i2c_client *client, u8 command, u8 length, u8 *values);
// 写入多个字节
s32 i2c_smbus_write_i2c_block_data(const struct i2c_client *client, u8 command, u8 length, const u8 *values);
5.4 完整驱动示例
c复制#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/slab.h>
#define MPU6050_WHO_AM_I 0x75
#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
#define MPU6050_ACCEL_XOUT_H 0x3B
struct mpu6050_data {
struct i2c_client *client;
};
static int mpu6050_read_data(struct i2c_client *client)
{
int ret;
u8 buf[6];
// 读取加速度计XYZ三轴数据(6字节)
ret = i2c_smbus_read_i2c_block_data(client, MPU6050_ACCEL_XOUT_H, 6, buf);
if (ret < 0) {
dev_err(&client->dev, "Failed to read accel data\n");
return ret;
}
// 合成16位数据(大端序)
int16_t acc_x = (buf[0] << 8) | buf[1];
int16_t acc_y = (buf[2] << 8) | buf[3];
int16_t acc_z = (buf[4] << 8) | buf[5];
dev_info(&client->dev, "Accel: X=%d, Y=%d, Z=%d\n", acc_x, acc_y, acc_z);
return 0;
}
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct mpu6050_data *data;
int ret;
u8 id_val;
// 检查I2C功能
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA)) {
dev_err(&client->dev, "I2C adapter doesn't support SMBUS\n");
return -ENODEV;
}
// 分配设备数据结构
data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
if (!data)
return -ENOMEM;
data->client = client;
i2c_set_clientdata(client, data);
// 唤醒芯片
ret = i2c_smbus_write_byte_data(client, MPU6050_PWR_MGMT_1, 0x00);
if (ret < 0) {
dev_err(&client->dev, "Failed to wake up MPU6050\n");
return ret;
}
// 配置陀螺仪
ret = i2c_smbus_write_byte_data(client, MPU6050_GYRO_CONFIG, 0x08);
if (ret < 0) {
dev_err(&client->dev, "Failed to config gyro\n");
return ret;
}
// 配置加速度计
ret = i2c_smbus_write_byte_data(client, MPU6050_ACCEL_CONFIG, 0x00);
if (ret < 0) {
dev_err(&client->dev, "Failed to config accel\n");
return ret;
}
// 验证设备ID
id_val = i2c_smbus_read_byte_data(client, MPU6050_WHO_AM_I);
if (id_val < 0) {
dev_err(&client->dev, "Failed to read WHO_AM_I\n");
return id_val;
}
if (id_val != 0x68 && id_val != 0x70) {
dev_err(&client->dev, "Invalid device ID: 0x%x\n", id_val);
return -ENODEV;
}
dev_info(&client->dev, "MPU6050 detected, ID: 0x%x\n", id_val);
// 测试读取数据
mpu6050_read_data(client);
return 0;
}
static int mpu6050_remove(struct i2c_client *client)
{
dev_info(&client->dev, "MPU6050 driver removed\n");
return 0;
}
static const struct of_device_id mpu6050_of_match[] = {
{ .compatible = "invensense,mpu6050", },
{},
};
MODULE_DEVICE_TABLE(of, mpu6050_of_match);
static const struct i2c_device_id mpu6050_id[] = {
{ "mpu6050", 0 },
{}
};
MODULE_DEVICE_TABLE(i2c, mpu6050_id);
static struct i2c_driver mpu6050_driver = {
.driver = {
.name = "mpu6050",
.of_match_table = mpu6050_of_match,
},
.probe = mpu6050_probe,
.remove = mpu6050_remove,
.id_table = mpu6050_id,
};
module_i2c_driver(mpu6050_driver);
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("MPU6050 I2C driver");
MODULE_LICENSE("GPL");
5.5 驱动编译与测试
Makefile示例:
makefile复制obj-m := mpu6050.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
编译并加载驱动:
bash复制make
sudo insmod mpu6050.ko
查看内核日志:
bash复制dmesg | tail
应该能看到MPU6050的检测信息和加速度数据。
6. 进阶开发与优化
6.1 添加sysfs接口
为了让用户空间更方便地访问传感器数据,可以添加sysfs接口:
c复制static ssize_t accel_show(struct device *dev, struct device_attribute *attr, char *buf)
{
struct mpu6050_data *data = dev_get_drvdata(dev);
u8 accel_data[6];
int ret;
ret = i2c_smbus_read_i2c_block_data(data->client, MPU6050_ACCEL_XOUT_H, 6, accel_data);
if (ret < 0)
return ret;
return sprintf(buf, "%d %d %d\n",
(int16_t)((accel_data[0] << 8) | accel_data[1]),
(int16_t)((accel_data[2] << 8) | accel_data[3]),
(int16_t)((accel_data[4] << 8) | accel_data[5]));
}
static DEVICE_ATTR_RO(accel);
static struct attribute *mpu6050_attrs[] = {
&dev_attr_accel.attr,
NULL,
};
static const struct attribute_group mpu6050_attr_group = {
.attrs = mpu6050_attrs,
};
// 在probe函数中添加:
ret = sysfs_create_group(&client->dev.kobj, &mpu6050_attr_group);
if (ret) {
dev_err(&client->dev, "Failed to create sysfs group\n");
return ret;
}
加载驱动后,可以通过以下命令读取加速度数据:
bash复制cat /sys/bus/i2c/devices/3-0068/accel
6.2 实现IIO接口
对于传感器设备,Linux提供了Industrial I/O (IIO)子系统,可以更规范地暴露传感器数据:
c复制#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
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);
u8 buf[6];
int ret;
switch (mask) {
case IIO_CHAN_INFO_RAW:
ret = i2c_smbus_read_i2c_block_data(data->client, MPU6050_ACCEL_XOUT_H, 6, buf);
if (ret < 0)
return ret;
switch (chan->channel2) {
case IIO_MOD_X:
*val = (int16_t)((buf[0] << 8) | buf[1]);
return IIO_VAL_INT;
case IIO_MOD_Y:
*val = (int16_t)((buf[2] << 8) | buf[3]);
return IIO_VAL_INT;
case IIO_MOD_Z:
*val = (int16_t)((buf[4] << 8) | buf[5]);
return IIO_VAL_INT;
default:
return -EINVAL;
}
default:
return -EINVAL;
}
}
static const struct iio_chan_spec mpu6050_channels[] = {
{
.type = IIO_ACCEL,
.modified = 1,
.channel2 = IIO_MOD_X,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
.type = IIO_ACCEL,
.modified = 1,
.channel2 = IIO_MOD_Y,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
{
.type = IIO_ACCEL,
.modified = 1,
.channel2 = IIO_MOD_Z,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW),
},
};
static const struct iio_info mpu6050_info = {
.read_raw = mpu6050_read_raw,
};
// 在probe函数中初始化IIO设备
struct iio_dev *indio_dev;
indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
if (!indio_dev)
return -ENOMEM;
data = iio_priv(indio_dev);
data->client = client;
indio_dev->name = "mpu6050";
indio_dev->channels = mpu6050_channels;
indio_dev->num_channels = ARRAY_SIZE(mpu6050_channels);
indio_dev->info = &mpu6050_info;
indio_dev->modes = INDIO_DIRECT_MODE;
ret = devm_iio_device_register(&client->dev, indio_dev);
if (ret)
return ret;
IIO接口提供了更丰富的用户空间工具支持,如iio_generic_buffer、iio_event_monitor等。
6.3 添加中断支持
MPU6050的INT引脚可以配置为在数据准备好时触发中断,减少轮询开销:
- 在设备树中添加中断配置:
dts复制mpu6050@68 {
compatible = "invensense,mpu6050";
reg = <0x68>;
interrupt-parent = <&gpio>;
interrupts = <17 IRQ_TYPE_EDGE_RISING>;
status = "okay";
};
- 在驱动中处理中断:
c复制static irqreturn_t mpu6050_interrupt(int irq, void *private)
{
struct mpu6050_data *data = private;
// 读取传感器数据
mpu6050_read_data(data->client);
return IRQ_HANDLED;
}
// 在probe函数中注册中断
ret = devm_request_irq(&client->dev, client->irq, mpu6050_interrupt,
IRQF_TRIGGER_RISING, "mpu6050", data);
if (ret) {
dev_err(&client->dev, "Failed to request IRQ\n");
return ret;
}
7. 常见问题与调试技巧
7.1 I2C通信失败排查
-
设备未检测到:
- 检查硬件连接是否正确
- 确认I2C总线编号是否正确
- 测量SCL/SDA线是否有信号
-
读取返回错误:
- 检查设备地址是否正确
- 确认寄存器地址是否正确
- 检查电源电压是否稳定
-
数据不正确:
- 确认字节序是否正确(MPU6050使用大端序)
- 检查量程配置是否合理
- 验证设备ID是否正确
7.2 内核驱动调试技巧
- 增加调试输出:
c复制dev_dbg(&client->dev, "Register 0x%02x value: 0x%02x\n", reg, val);
编译时需要定义DEBUG宏:
makefile复制EXTRA_CFLAGS += -DDEBUG
-
使用逻辑分析仪:
捕获I2C总线上的实际通信数据,与预期对比。 -
检查I2C适配器功能:
c复制#include <linux/i2c.h>
// 在probe函数中检查适配器功能
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
dev_err(&client->dev, "Adapter doesn't support required functions\n");
7.3 性能优化建议
-
减少I2C通信次数:
- 使用块读取操作一次读取多个寄存器
- 合并配置写入操作
-
合理设置采样率:
- 根据应用需求配置合适的采样率
- 使用中断代替轮询
-
用户空间优化:
- 使用mmap映射传感器数据
- 实现双缓冲减少数据拷贝
8. 项目扩展思路
8.1 数据滤波与处理
原始传感器数据通常包含噪声,可以添加滤波算法:
- 移动平均滤波
- 卡尔曼滤波
- 互补滤波
8.2 姿态解算
结合加速度计和陀螺仪数据计算设备姿态:
- 欧拉角计算
- 四元数表示
- 姿态融合算法
8.3 多传感器融合
将MPU6050与其他传感器(如磁力计)数据融合,提高姿态估计精度。
8.4 用户空间库开发
封装常用功能为库函数,简化应用开发:
- 提供校准接口
- 实现姿态解算函数
- 添加数据记录功能
9. 实际应用案例
9.1 平衡车控制
使用MPU6050检测车身倾斜角度,通过PID算法控制电机保持平衡。
9.2 手势识别
分析加速度计和陀螺仪数据模式,识别特定手势。
9.3 运动跟踪
记录设备运动轨迹,用于运动分析或导航。
10. 资源与参考
- MPU6050数据手册:了解寄存器详细定义和电气特性
- Linux内核文档:Documentation/i2c/ 和 Documentation/devicetree/
- IIO子系统文档:了解标准传感器接口实现
- i2c-tools源码:学习用户空间I2C操作实现
- 相关开源项目:参考成熟的MPU6050驱动实现
通过本教程,你应该已经掌握了从硬件连接到用户空间应用的全套MPU6050开发流程。实际项目中可以根据需求选择适合的开发层次和功能实现方式。