1. 项目概述
在嵌入式开发领域,I2C总线因其简单的两线制结构和多主从设备支持特性,成为传感器连接的经典方案。MPU6050作为一款集成了3轴陀螺仪和3轴加速度计的6轴运动处理传感器,通过I2C接口与主控通信,是学习Linux驱动开发的理想实验对象。
这个项目将带你从零开始,在Linux环境下为MPU6050编写完整的I2C设备驱动。不同于简单的代码示例,我们会深入I2C子系统的架构设计,解析内核中的i2c-core、i2c-adapter和i2c-dev等核心组件,让你真正理解Linux驱动开发的精髓。
2. 硬件准备与电路连接
2.1 MPU6050模块介绍
MPU6050采用QFN-24封装,核心参数包括:
- 加速度计量程:±2/±4/±8/±16g
- 陀螺仪量程:±250/±500/±1000/±2000°/s
- 工作电压:2.375V-3.46V(通常使用3.3V)
- I2C通信速率:标准模式100kHz,快速模式400kHz
模块上通常已集成必要的上拉电阻(4.7kΩ)和稳压电路,可直接与开发板连接。
2.2 硬件连接示意图
以树莓派为例的接线方式:
| MPU6050引脚 | 树莓派GPIO | 功能说明 |
|---|---|---|
| VCC | 3.3V | 电源正极 |
| GND | GND | 电源地 |
| SCL | GPIO3 (SCL) | 时钟线 |
| SDA | GPIO2 (SDA) | 数据线 |
| AD0 | GND或3.3V | 地址选择 |
注意:AD0引脚电平决定器件I2C地址,接地为0x68,接VCC为0x69。多个MPU6050共用总线时需通过此引脚区分地址。
3. Linux I2C子系统架构解析
3.1 I2C核心层(i2c-core)
内核中的i2c-core提供了以下核心功能:
- 总线注册与管理(i2c_add_adapter)
- 设备匹配机制(of_match_table, id_table)
- 通信协议实现(i2c_transfer)
- 用户空间接口(/dev/i2c-%d)
关键数据结构:
c复制struct i2c_adapter {
struct module *owner;
const struct i2c_algorithm *algo; // 底层通信方法
/* ... */
};
struct i2c_client {
unsigned short flags; // 地址标志
unsigned short addr; // 7位设备地址
char name[I2C_NAME_SIZE]; // 设备名
struct i2c_adapter *adapter; // 所属适配器
/* ... */
};
3.2 驱动开发三部曲
- 设备树描述:在.dts文件中定义硬件连接
dts复制&i2c1 {
mpu6050: imu@68 {
compatible = "invensense,mpu6050";
reg = <0x68>;
interrupt-parent = <&gpio>;
interrupts = <17 IRQ_TYPE_EDGE_RISING>;
};
};
- 驱动注册:实现probe/remove等核心回调
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,
};
- 功能实现:数据读写与处理
c复制static int mpu6050_read_raw(struct i2c_client *client, u8 reg, u8 *buf, int len)
{
struct i2c_msg msgs[2] = {
{ // 写寄存器地址
.addr = client->addr,
.flags = 0,
.len = 1,
.buf = ®,
},
{ // 读数据
.addr = client->addr,
.flags = I2C_M_RD,
.len = len,
.buf = buf,
}
};
return i2c_transfer(client->adapter, msgs, 2);
}
4. 完整驱动实现详解
4.1 初始化配置流程
- 电源管理:唤醒设备并设置时钟源
c复制#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_CLK_PLL_XGYRO 0x01
static int mpu6050_init(struct i2c_client *client)
{
u8 val;
// 退出睡眠模式
val = MPU6050_CLK_PLL_XGYRO;
i2c_smbus_write_byte_data(client, MPU6050_PWR_MGMT_1, val);
// 设置加速度计和陀螺仪量程
i2c_smbus_write_byte_data(client, MPU6050_ACCEL_CONFIG, ACCEL_FS_2G);
i2c_smbus_write_byte_data(client, MPU6050_GYRO_CONFIG, GYRO_FS_250DEG);
// 配置采样率(DLPF)
i2c_smbus_write_byte_data(client, MPU6050_CONFIG, DLPF_BW_42HZ);
// 设置采样频率
i2c_smbus_write_byte_data(client, MPU6050_SMPLRT_DIV, 4); // 200Hz
}
- 中断配置(可选):用于数据就绪通知
c复制static irqreturn_t mpu6050_irq_handler(int irq, void *dev_id)
{
struct mpu6050_data *data = dev_id;
// 读取传感器数据
mpu6050_read_data(data->client);
// 唤醒等待进程
wake_up_interruptible(&data->waitq);
return IRQ_HANDLED;
}
4.2 数据读取与处理
- 原始数据读取:
c复制struct mpu6050_raw_data {
s16 accel_x, accel_y, accel_z;
s16 temp;
s16 gyro_x, gyro_y, gyro_z;
};
static int mpu6050_read_raw_data(struct i2c_client *client,
struct mpu6050_raw_data *raw)
{
u8 buf[14];
int ret;
// 从0x3B开始连续读取14个寄存器
ret = mpu6050_read_raw(client, MPU6050_ACCEL_XOUT_H, buf, 14);
if (ret < 0)
return ret;
// 大端格式转换
raw->accel_x = be16_to_cpup((__be16 *)(buf + 0));
raw->accel_y = be16_to_cpup((__be16 *)(buf + 2));
raw->accel_z = be16_to_cpup((__be16 *)(buf + 4));
raw->temp = be16_to_cpup((__be16 *)(buf + 6));
raw->gyro_x = be16_to_cpup((__be16 *)(buf + 8));
raw->gyro_y = be16_to_cpup((__be16 *)(buf + 10));
raw->gyro_z = be16_to_cpup((__be16 *)(buf + 12));
return 0;
}
- 单位转换:
c复制// 温度转换公式:TEMP_degC = (TEMP_OUT / 340) + 36.53
static inline int mpu6050_convert_temp(s16 raw)
{
return (raw / 340) + 3653; // 返回毫摄氏度
}
// 加速度转换(根据设置的量程)
static inline int mpu6050_convert_accel(s16 raw, int fs_sel)
{
const int scale[] = { 16384, 8192, 4096, 2048 };
return (raw * 1000) / scale[fs_sel]; // 返回mg单位
}
// 陀螺仪转换
static inline int mpu6050_convert_gyro(s16 raw, int fs_sel)
{
const int scale[] = { 131, 65, 32, 16 };
return (raw * 1000) / scale[fs_sel]; // 返回毫度/秒
}
5. 用户空间接口实现
5.1 sysfs属性文件
通过sysfs暴露传感器数据:
c复制static ssize_t accel_x_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct mpu6050_data *data = dev_get_drvdata(dev);
return sprintf(buf, "%d\n", data->accel_x);
}
static DEVICE_ATTR_RO(accel_x);
static struct attribute *mpu6050_attrs[] = {
&dev_attr_accel_x.attr,
&dev_attr_accel_y.attr,
&dev_attr_accel_z.attr,
&dev_attr_gyro_x.attr,
&dev_attr_gyro_y.attr,
&dev_attr_gyro_z.attr,
&dev_attr_temp.attr,
NULL
};
static const struct attribute_group mpu6050_group = {
.attrs = mpu6050_attrs,
};
5.2 IIO子系统集成
更专业的做法是实现IIO接口:
c复制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),
.info_mask_shared_by_type = BIT(IIO_CHAN_INFO_SCALE),
},
/* 其他通道定义... */
};
static int mpu6050_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
switch (mask) {
case IIO_CHAN_INFO_RAW:
// 返回原始数据
break;
case IIO_CHAN_INFO_SCALE:
// 返回量程系数
break;
}
return 0;
}
static const struct iio_info mpu6050_info = {
.read_raw = mpu6050_read_raw,
};
6. 调试技巧与常见问题
6.1 I2C工具集使用
- 检测设备:
bash复制i2cdetect -y 1 # 树莓派I2C-1总线扫描
- 寄存器读写:
bash复制# 读取WHO_AM_I寄存器(0x75)
i2cget -y 1 0x68 0x75
# 设置采样率
i2cset -y 1 0x68 0x19 0x04
6.2 常见错误排查
- 设备无响应:
- 检查电源和接线
- 确认上拉电阻(通常4.7kΩ)
- 测量SCL/SDA波形(应有清晰的方波)
- 数据异常:
- 检查量程配置(ACCEL_CONFIG/GYRO_CONFIG)
- 验证字节序(MPU6050为大端)
- 校准零偏(设备水平静止时读取偏移值)
- 驱动加载失败:
- 检查设备树是否生效
- 确认I2C总线编号正确
- 查看内核日志(dmesg | grep mpu6050)
经验:MPU6050首次上电需要约50ms启动时间,probe函数中应添加适当延时。长时间不读取数据时会自动进入低功耗模式,连续读取时应保持适当速率(≥1Hz)。
7. 完整源码结构
最终驱动代码组织如下:
code复制mpu6050/
├── Kconfig
├── Makefile
├── mpu6050.h // 寄存器定义和数据结构
├── mpu6050_core.c // 核心驱动逻辑
├── mpu6050_iio.c // IIO接口实现
└── mpu6050_sysfs.c // sysfs接口
关键代码片段:
c复制/* 在probe函数中完成初始化 */
static int mpu6050_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct mpu6050_data *data;
struct iio_dev *indio_dev;
int ret;
/* 分配IIO设备 */
indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
data = iio_priv(indio_dev);
/* 初始化数据结构 */
data->client = client;
mutex_init(&data->lock);
/* 硬件初始化 */
ret = mpu6050_init(client);
/* 注册IIO设备 */
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;
return iio_device_register(indio_dev);
}
实际开发中,建议结合芯片手册《MPU-6000/MPU-6050 Register Map and Descriptions》查阅各寄存器详细定义。驱动测试时可以先通过i2c-tools验证基本通信正常,再逐步添加功能模块。