MPU6050驱动分析(3)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

348.5.4MPU6050驱动分析(3)

// 应用程序打开课程驱动对应的字符设备节点。
fd_mpu = open("/dev/I2C1_mpu6050", O_RDWR);
// 应用程序请求读取 12 字节,即 6 个 short 类型的原始轴数据。
error = read(fd_mpu, resive_data, 12);
// 应用程序关闭文件描述符;最后一个引用释放时会进入驱动的 release 回调。
error = close(fd_mpu);

// 定义本驱动的字符设备对象。
static struct cdev mpu6050_chr_dev;
// 保存创建出的设备类。
struct class *class_mpu6050;
// 保存创建出的 class 设备对象。
struct device *device_mpu6050;
// 保存 probe() 获得的 MPU6050 I²C client。
struct i2c_client *mpu6050_client = NULL;

// 定义采样率分频寄存器地址。
#define SMPLRT_DIV 0x19
// 定义数字低通滤波配置寄存器地址。
#define CONFIG 0x1A
// 定义加速度计配置寄存器地址。
#define ACCEL_CONFIG 0x1C
// 定义 X 轴加速度高字节寄存器地址。
#define ACCEL_XOUT_H 0x3B
// 定义 X 轴加速度低字节寄存器地址。
#define ACCEL_XOUT_L 0x3C
// 定义 Y 轴加速度高字节寄存器地址。
#define ACCEL_YOUT_H 0x3D
// 定义 Y 轴加速度低字节寄存器地址。
#define ACCEL_YOUT_L 0x3E
// 定义 Z 轴加速度高字节寄存器地址。
#define ACCEL_ZOUT_H 0x3F
// 定义 Z 轴加速度低字节寄存器地址。
#define ACCEL_ZOUT_L 0x40
// 定义 X 轴陀螺仪高字节寄存器地址。
#define GYRO_XOUT_H 0x43
// 定义 X 轴陀螺仪低字节寄存器地址。
#define GYRO_XOUT_L 0x44
// 定义 Y 轴陀螺仪高字节寄存器地址。
#define GYRO_YOUT_H 0x45
// 定义 Y 轴陀螺仪低字节寄存器地址。
#define GYRO_YOUT_L 0x46
// 定义 Z 轴陀螺仪高字节寄存器地址。
#define GYRO_ZOUT_H 0x47
// 定义 Z 轴陀螺仪低字节寄存器地址。
#define GYRO_ZOUT_L 0x48
// 定义电源管理寄存器 1 的地址。
#define PWR_MGMT_1 0x6B

// 定义向 MPU6050 的一个寄存器写入一个字节的辅助函数。
static int i2c_write_mpu6050(struct i2c_client *mpu6050_client, u8 address, u8 data)
{ // i2c_write_mpu6050() 函数开始。
    // 保存 I²C 核心的传输结果。
    int error = 0;
    // 准备“寄存器地址 + 寄存器数据”两个待发送字节。
    u8 write_data[2];
    // 定义一个低层 I²C 消息。
    struct i2c_msg send_msg;

    // 第一个字节告诉 MPU6050 接下来要访问哪个寄存器。
    write_data[0] = address;
    // 第二个字节是要写入该寄存器的数据。
    write_data[1] = data;

    // 使用 probe() 传入的 client 中保存的 7 位 I²C 从地址。
    send_msg.addr = mpu6050_client->addr;
    // 不设置 I2C_M_RD,表示这是一条主机向从机写数据的消息。
    send_msg.flags = 0;
    // 让消息缓冲区指向前面准备的两个字节。
    send_msg.buf = write_data;
    // 指定本条消息发送两个字节。
    send_msg.len = 2;

    // 通过 client 所属的 I²C adapter 执行一条消息。
    error = i2c_transfer(mpu6050_client->adapter, &send_msg, 1);
    // 成功时应返回已执行的消息数 1,而不是传输的字节数 2。
    if (error != 1)
    { // I²C 消息没有完整执行时进入错误分支。
        // 向内核日志输出传输失败提示。
        printk(KERN_DEBUG "\n i2c_transfer error \n");
        // 保持课程源码原样:统一返回 -1。
        return -1;
    } // I²C 写消息错误处理结束。
    // 一条消息完整执行,返回成功。
    return 0;
} // i2c_write_mpu6050() 函数结束。

// 定义从 MPU6050 指定寄存器开始读取数据的辅助函数。
static int i2c_read_mpu6050(struct i2c_client *mpu6050_client, u8 address, void *data, u32 length)
{ // i2c_read_mpu6050() 函数开始。
    // 保存 I²C 核心的传输结果。
    int error = 0;
    // 保存本次要读取的起始寄存器地址。
    u8 address_data = address;
    // 定义两条连续执行的 I²C 消息。
    struct i2c_msg mpu6050_msg[2];

    // 第一条消息发往当前 MPU6050 的 7 位从地址。
    mpu6050_msg[0].addr = mpu6050_client->addr;
    // 第一条消息不设置 I2C_M_RD,因此方向为写。
    mpu6050_msg[0].flags = 0;
    // 第一条消息发送目标寄存器地址。
    mpu6050_msg[0].buf = &address_data;
    // 寄存器地址在此协议中占一个字节。
    mpu6050_msg[0].len = 1;

    // 第二条消息仍发往同一个 MPU6050 从地址。
    mpu6050_msg[1].addr = mpu6050_client->addr;
    // 设置 I2C_M_RD,表示第二条消息由从机向主机读数据。
    mpu6050_msg[1].flags = I2C_M_RD;
    // 把接收到的字节写入调用者提供的缓冲区。
    mpu6050_msg[1].buf = data;
    // 指定需要读取的字节数。
    mpu6050_msg[1].len = length;

    // 在一次组合传输中依次执行“写寄存器地址”和“读数据”两条消息。
    error = i2c_transfer(mpu6050_client->adapter, mpu6050_msg, 2);

    // 成功时应返回已执行的消息数 2。
    if (error != 2)
    { // 两条消息没有全部执行时进入错误分支。
        // 向内核日志输出读取失败提示。
        printk(KERN_DEBUG "\n i2c_read_mpu6050 error \n");
        // 保持课程源码原样:统一返回 -1。
        return -1;
    } // I²C 读消息错误处理结束。
    // 两条消息完整执行,返回成功。
    return 0;
} // i2c_read_mpu6050() 函数结束。

// 定义课程驱动在打开设备时调用的 MPU6050 配置函数。
static int mpu6050_init(void)
{ // mpu6050_init() 函数开始。
    // 累加四次寄存器写操作的结果。
    int error = 0;
    // 写 0x00 到电源管理寄存器 1,清除休眠等控制位。
    error += i2c_write_mpu6050(mpu6050_client, PWR_MGMT_1, 0X00);
    // 把采样率分频值设置为 7。
    error += i2c_write_mpu6050(mpu6050_client, SMPLRT_DIV, 0X07);
    // 把数字低通滤波配置字段设置为 6。
    error += i2c_write_mpu6050(mpu6050_client, CONFIG, 0X06);
    // 写入加速度计配置值 1。
    error += i2c_write_mpu6050(mpu6050_client, ACCEL_CONFIG, 0X01);

    // 任意一次辅助函数返回 -1 后,累加结果都会小于 0。
    if (error < 0)
    { // MPU6050 配置失败时进入错误分支。
        // 向内核日志输出初始化失败提示。
        printk(KERN_DEBUG "\n mpu6050_init error \n");
        // 向调用者返回失败。
        return -1;
    } // MPU6050 配置错误处理结束。
    // 四次寄存器写操作均成功。
    return 0;
} // mpu6050_init() 函数结束。

// 实现字符设备的 open 回调。
static int mpu6050_open(struct inode *inode, struct file *filp)
{ // mpu6050_open() 函数开始。
    // 输出一次设备打开日志。
    printk("\n mpu6050_open \n");

    // 配置 MPU6050;课程源码没有检查该函数的返回值。
    mpu6050_init();
    // 保持课程源码原样:无论配置是否失败都向 VFS 返回成功。
    return 0;
} // mpu6050_open() 函数结束。

// 实现字符设备的 read 回调。
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{ // mpu6050_read() 函数开始。
    // 暂存传感器输出的高字节。
    char data_H;
    // 暂存传感器输出的低字节。
    char data_L;
    // 保存 copy_to_user() 未能复制的字节数。
    int error;
    // 依次保存三轴加速度和三轴角速度原始值。
    short mpu6050_result[6];

    // 读取 X 轴加速度高字节。
    i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_H, &data_H, 1);
    // 读取 X 轴加速度低字节。
    i2c_read_mpu6050(mpu6050_client, ACCEL_XOUT_L, &data_L, 1);
    // 把高字节移到 16 位结果的高八位。
    mpu6050_result[0] = data_H << 8;
    // 加上低字节,拼成 X 轴加速度原始值。
    mpu6050_result[0] += data_L;

    // 读取 Y 轴加速度高字节。
    i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_H, &data_H, 1);
    // 读取 Y 轴加速度低字节。
    i2c_read_mpu6050(mpu6050_client, ACCEL_YOUT_L, &data_L, 1);
    // 把高字节移到 16 位结果的高八位。
    mpu6050_result[1] = data_H << 8;
    // 加上低字节,拼成 Y 轴加速度原始值。
    mpu6050_result[1] += data_L;

    // 读取 Z 轴加速度高字节。
    i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_H, &data_H, 1);
    // 读取 Z 轴加速度低字节。
    i2c_read_mpu6050(mpu6050_client, ACCEL_ZOUT_L, &data_L, 1);
    // 把高字节移到 16 位结果的高八位。
    mpu6050_result[2] = data_H << 8;
    // 加上低字节,拼成 Z 轴加速度原始值。
    mpu6050_result[2] += data_L;

    // 读取 X 轴陀螺仪高字节。
    i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_H, &data_H, 1);
    // 读取 X 轴陀螺仪低字节。
    i2c_read_mpu6050(mpu6050_client, GYRO_XOUT_L, &data_L, 1);
    // 把高字节移到 16 位结果的高八位。
    mpu6050_result[3] = data_H << 8;
    // 加上低字节,拼成 X 轴角速度原始值。
    mpu6050_result[3] += data_L;

    // 读取 Y 轴陀螺仪高字节。
    i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_H, &data_H, 1);
    // 读取 Y 轴陀螺仪低字节。
    i2c_read_mpu6050(mpu6050_client, GYRO_YOUT_L, &data_L, 1);
    // 把高字节移到 16 位结果的高八位。
    mpu6050_result[4] = data_H << 8;
    // 加上低字节,拼成 Y 轴角速度原始值。
    mpu6050_result[4] += data_L;

    // 读取 Z 轴陀螺仪高字节。
    i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_H, &data_H, 1);
    // 读取 Z 轴陀螺仪低字节。
    i2c_read_mpu6050(mpu6050_client, GYRO_ZOUT_L, &data_L, 1);
    // 把高字节移到 16 位结果的高八位。
    mpu6050_result[5] = data_H << 8;
    // 加上低字节,拼成 Z 轴角速度原始值。
    mpu6050_result[5] += data_L;

    // 按应用传入的 cnt 把内核缓冲区复制到用户空间。
    error = copy_to_user(buf, mpu6050_result, cnt);

    // copy_to_user() 返回非零表示仍有字节未复制。
    if (error != 0)
    { // 用户空间复制失败时进入错误分支。
        // 向内核日志输出复制失败提示。
        printk("copy_to_user error!");
        // 保持课程源码原样:统一返回 -1。
        return -1;
    } // 用户空间复制错误处理结束。
    // 保持课程源码原样:成功后返回 0,而不是实际读取的字节数。
    return 0;
} // mpu6050_read() 函数结束。

// 实现字符设备的 release 回调。
static int mpu6050_release(struct inode *inode, struct file *filp)
{ // mpu6050_release() 函数开始。
    // 输出一次文件释放日志。
    printk("\n mpu6050_release \n");
    // 本例没有需要在关闭时释放的每次打开资源,直接返回成功。
    return 0;
} // mpu6050_release() 函数结束。

// 定义本字符设备提供给 VFS 的文件操作函数表。
static struct file_operations mpu6050_chr_dev_fops =
{ // file_operations 初始化开始。
    // 标明这些回调由当前内核模块提供。
    .owner = THIS_MODULE,
    // 把 VFS 的打开操作连接到课程驱动的 mpu6050_open()。
    .open = mpu6050_open,
    // 把 VFS 的读取操作连接到课程驱动的 mpu6050_read()。
    .read = mpu6050_read,
    // 把文件对象最终释放操作连接到 mpu6050_release()。
    .release = mpu6050_release,
// file_operations 初始化结束。
};

// 以下是课程 mpu6050_probe() 中本节继续分析的字符设备注册代码。
// 先给 cdev 设置所有者;保留课程源码顺序,后文会说明 cdev_init() 会将它清零。
mpu6050_chr_dev.owner = THIS_MODULE;
// 初始化 cdev,并记住前面定义的文件操作函数表。
cdev_init(&mpu6050_chr_dev, &mpu6050_chr_dev_fops);

// 把 cdev 加入字符设备映射,使申请到的设备号范围可以被 VFS 查找。
ret = cdev_add(&mpu6050_chr_dev, mpu6050_devno, DEV_CNT);
// 检查 cdev 是否添加失败。
if (ret < 0)
{ // cdev_add() 失败时进入错误分支。
    // 向内核日志输出添加失败提示。
    printk("fail to add cdev\n");
    // 保持课程源码原样:返回 -1,且没有回收此前申请的设备号。
    return -1;
} // cdev_add() 错误处理结束。

// 创建名为 DEV_NAME 的设备类。
class_mpu6050 = class_create(THIS_MODULE, DEV_NAME);

// 在该类中创建带有 mpu6050_devno 的设备模型对象。
device_mpu6050 = device_create(class_mpu6050, NULL, mpu6050_devno, NULL, DEV_NAME);
// 保存 I²C 核心传给 probe() 的 client,供后续 open/read 使用。
mpu6050_client = client;
// 向 I²C 核心报告 probe() 成功。
return 0;

源码边界:代码区按本节视频实际分析范围,收录应用层 open/read/close、cdev 与 file_operations、probe() 中的字符设备注册步骤,以及 open/read/release 和两种 I²C 传输辅助函数。代码均来自课程配套文件,只移除旧注释、统一排版并增加逐行解释;没有抄入视频虽用于查原理但不属于课程业务代码的 cdev_init()、cdev_add()、i2c_transfer() 内核实现,也没有提前收录下一节的 OLED 业务代码。

一、mpu6050驱动

本节从上节的“动态申请设备号”继续向下,完成两条链路:

字符设备注册:
设备号 → cdev_init() → cdev_add() → class_create() → device_create()

应用访问:
/dev/I2C1_mpu6050 → 设备号 → cdev → file_operations
    → open/read/release → MPU6050 I²C 传输

这两条链路相互衔接,但不能混为一谈:cdev 负责把设备号连接到文件操作,class/device 负责设备模型和热插拔事件,mdev 负责建立 /dev 节点,而 I²C helper 才真正产生总线传输。

1.1 资源管理器跳转

课程代码调用:

cdev_init(&mpu6050_chr_dev, &mpu6050_chr_dev_fops);

两个参数分别是:

参数本例含义
struct cdev *&mpu6050_chr_dev要初始化的字符设备对象
const struct file_operations *&mpu6050_chr_dev_fops此字符设备提供给 VFS 的操作表

原稿中的“cdv”“cdv_init”都是 cdev、cdev_init 的 OCR 错误。cdev 可理解为 character device,但它不是实际 MPU6050 硬件本体,而是 Linux 字符设备层用于管理访问入口的内核对象。

课程 Linux 4.1.15 的 struct cdev 主要包含:

成员作用
kobj嵌入的 kobject,提供对象生命周期和内核对象模型基础
owner所属模块指针
ops指向 file_operations
list关联当前 cdev 已缓存的 inode
dev此 cdev 覆盖的起始设备号
count覆盖的连续设备号数量

视频根据 list 成员推断“字符设备通过链表注册、每个 cdev 都对应一个链表”,说得过于简单。课程内核的 cdev_add() 实际通过 kobj_map() 把设备号范围加入 cdev_map;cdev.list 主要维护已经与该 cdev 建立联系的 inode。不能只凭一个 list_head 成员推断完整注册机制。

1.2 设备结构体分析

file_operations 是一组函数指针,不是函数本身。课程驱动只实现了其中三个操作:

应用/VFS 操作驱动回调本例行为
open().open = mpu6050_open配置 MPU6050 的四个寄存器
read().read = mpu6050_read读取三轴加速度和三轴角速度原始值并复制到用户空间
文件对象最终释放.release = mpu6050_release只打印日志

.owner = THIS_MODULE 不是原稿所说的“this_mode”。它告诉内核这些回调由当前模块提供,使文件操作引用期间可以保护模块生命周期。它与 struct cdev 自己的 owner 成员是两个位置。

应用调用并不是直接跳到函数名。以打开为例,课程内核大致经历:

open("/dev/I2C1_mpu6050", O_RDWR)
  → VFS 解析设备节点并取得 dev_t
  → 字符设备通用入口 chrdev_open()
  → 按 dev_t 查找 cdev
  → 用 cdev->ops 替换当前 file 的操作表
  → 调用 mpu6050_chr_dev_fops.open
  → mpu6050_open()

这种“结构体成员赋函数地址”的思想与 struct i2c_driver 中给 .probe、.remove 赋值相似,都是回调表;但调用者和触发条件不同:前者由 VFS 文件操作触发,后者由 I²C/驱动核心在绑定或解绑时触发。

1.3 字符设备操作

字符设备注册至少包含三个不同动作:

  1. alloc_chrdev_region():预留设备号范围;
  2. cdev_init():初始化内存中的 cdev 对象并保存操作表;
  3. cdev_add():把“设备号范围 → cdev”映射加入内核,使其立即可被查找。

只执行第一、二步,应用仍不能通过设备号进入该 cdev;cdev_add() 成功后,若系统中已经存在匹配设备号的节点,打开动作便可能立即到达回调。因此,在调用 cdev_add() 前,应把回调可能使用的状态准备好。

1.3.1 字符设备操作之结构体分析

课程内核中的 cdev_init() 会完成三件关键工作:清零整个 struct cdev、初始化其中的 list/kobject、保存传入的 file_operations 指针。它只是“准备好供添加”,并不注册设备号,也不创建 sysfs 或 /dev 对象。Linux 字符设备 API

这里存在一个课程源码顺序问题:

mpu6050_chr_dev.owner = THIS_MODULE;
cdev_init(&mpu6050_chr_dev, ...);  ← 先 memset() 整个 cdev

第二行会把上一行写入的 cdev.owner 清为 NULL。常见顺序应是先 cdev_init(),再设置 cdev.owner。精校稿按要求没有改写课程源码,只指出结果。本例 file_operations.owner 仍是 THIS_MODULE,因此文件操作还有模块引用保护,但不能据此认为错误顺序是正确范例。

1.3.2 字符设备操作之代码示例

cdev_add(&mpu6050_chr_dev, mpu6050_devno, DEV_CNT) 用设备号和数量填写 cdev,并把该连续范围加入字符设备映射。成功返回 0,失败返回负错误码。视频把第三个参数说成“默认 1”不严谨:它应与此前申请的设备号数量一致,本例只是因为 DEV_CNT 定义为 1。

课程源码的失败路径还不完整:

  • cdev_add() 失败后直接 return -1,没有释放已经由 alloc_chrdev_region() 申请的设备号;
  • class_create() 和 device_create() 失败时返回错误指针,代码没有用 IS_ERR() 检查;
  • 如果后续步骤失败,应该按完成顺序逆序回滚已经建立的资源。

class_create(THIS_MODULE, DEV_NAME) 在本课程 4.1.15 内核中创建名为 I2C1_mpu6050 的 class;通常可在下面看到:

ls -l /sys/class/I2C1_mpu6050

class 是设备模型的功能分类视图,不等于 I²C 总线,也不等于 C++ 的类。/sys/class/net 中出现网络接口,表示这些设备按网络功能归类;并不表示它们都是字符设备。

device_create() 会建立并注册一个 struct device,在 sysfs class 下生成设备及包含主、次设备号的 dev 属性,并发出 uevent。它本身不会直接在磁盘中“写出 /dev/I2C1_mpu6050 文件”;本项目由 BusyBox mdev 根据事件或扫描结果创建设备节点。Linux 设备模型基础设施

可分层验证:

ls -l /sys/class/I2C1_mpu6050
cat /sys/class/I2C1_mpu6050/I2C1_mpu6050/dev
ls -l /dev/I2C1_mpu6050

前者观察 class/device,dev 属性显示“主设备号:次设备号”,最后一行才观察 mdev 建立的字符设备节点。

课程代码把 mpu6050_client = client 放在 device_create() 之后。由于 cdev_add() 后字符设备已经可访问,device uevent 又可能触发 mdev 和用户程序,这个顺序存在打开设备时 mpu6050_client 尚未保存的竞态。另一个设备模型问题是 device_create() 的 parent 传了 NULL,因而这个 class 设备没有明确挂到实际 I²C client 的 struct device 下。

“probe() 返回 0”只表示驱动接受了这次绑定并认为初始化成功。课程 probe() 没有读取 WHO_AM_I 或访问传感器,真正的寄存器配置推迟到应用 open() 时,所以成功绑定不能证明 MPU6050 已应答,更不能证明数据正确。

1.4 思维导图总结

完整生命周期应分成以下阶段:

insmod
  → 模块 init
  → i2c_add_driver()
  → 设备与驱动匹配
  → mpu6050_probe()
  → 设备号、cdev、class/device 建立

应用 open/read/close
  → VFS
  → file_operations
  → MPU6050 I²C 读写

rmmod
  → i2c_del_driver()
  → 解除绑定并调用 remove()
  → device/class/cdev/设备号逆序清理
  → 模块 exit 返回

原稿中的“延迟驱动接口(delay drive)”是 i2c_del_driver() 的语音识别错误:del 是删除/注销,不是 delay。卸载也不是只调用一个 exit 就结束;i2c_del_driver() 会促使已绑定 client 解绑并执行课程的 mpu6050_remove()。

应用 close(fd) 与驱动 release 也不是绝对“一次 close 对应一次 release”。文件描述符经 dup()、fork() 等可能共享同一个打开文件对象,通常要到最后一个引用释放时才调用 release;本课程单进程、未复制描述符的简单场景中,看起来才近似一一对应。

1.5 open函数分析

1.5.1 open函数之初始化操作

mpu6050_open() 先打印日志,再调用 mpu6050_init()。四次 I²C 写入的硬件含义如下:

写操作按 MPU-6000/MPU-6050 寄存器手册解释
PWR_MGMT_1(0x6B) = 0x00清除 SLEEP、CYCLE、TEMP_DIS 等位,并选择内部 8 MHz 振荡器。手册建议为更好稳定性选择陀螺仪或外部时钟作参考;课程代码未采用该建议
SMPLRT_DIV(0x19) = 0x07采样率 = 陀螺仪输出率 / (1 + 7)
CONFIG(0x1A) = 0x06DLPF_CFG=6,加速度计/陀螺仪带宽约 5 Hz,陀螺仪输出率为 1 kHz;结合上一行得到约 125 Hz 采样率
ACCEL_CONFIG(0x1C) = 0x01AFS_SEL 两位仍为 00,因此满量程为 ±2g;低位的具体含义应以所用芯片版本的寄存器手册为准

这些结论来自 TDK InvenSense 的 MPU-6000/MPU-6050 Register Map Rev. 4.2。GYRO_CONFIG 在课程驱动中虽有宏定义,却没有在此初始化,因此代码依赖器件此前状态/复位默认值;它也没有读取 WHO_AM_I 验证芯片身份。

课程源码还有一个直接影响应用判断的问题:mpu6050_init() 失败会返回 -1,但 mpu6050_open() 忽略该返回值并固定返回 0。因此应用可能看到 open() 成功,同时内核日志已经报告 I²C 传输失败。printk() 只是调试输出,不是设备初始化的必要步骤,也不能代替错误返回。

1.5.2 open函数之i2c_write_mpu6050分析

一次寄存器写使用一条 i2c_msg,数据缓冲区包含:

START
→ 从地址 0x68 + Write
→ 寄存器地址 address
→ 寄存器值 data
→ STOP

i2c_transfer(mpu6050_client->adapter, &send_msg, 1) 的三个参数分别是 adapter、消息数组首地址、消息数量。原稿把第一个参数说成“设备地址”是错误的;从设备地址实际位于 send_msg.addr。client->addr 保存的是 7 位地址 0x68,不是把读写位拼进去后的 8 位 0xD0/0xD1。

函数成功返回已经执行的消息数,因此本次期望 1,不是期望发送的字节数 2。失败一般返回负 errno,也存在短执行的可能;课程统一转换成 -1,会丢失具体原因。Linux I²C 子系统 API

1.5.2.1 i2c_write_mpu6050之结构体分析

struct i2c_msg 的四个核心字段是:

字段本例写消息含义
addrmpu6050_client->addr7 位或 10 位从地址;10 位模式还要设置相应标志
flags0消息方向和特殊协议标志;未设置 I2C_M_RD 即写
bufwrite_data写时的数据源、读时的数据目的地
len2本条消息的数据字节数

I2C_M_RD 在课程内核中定义为位掩码 0x0001。因此不能把规则概括成“flags == 1 是读、flags == 0 是写”:flags 还能组合 10 位地址等其他标志,正确判断是检查 flags & I2C_M_RD。没有设置读位时才是写。Linux I²C 协议说明

write_data 是栈上的两字节数组。i2c_transfer() 是同步调用,返回前 adapter 已完成或终止这次传输,因此在本函数返回前该缓冲区有效;若改成异步接口,就不能把短生命周期栈缓冲区直接交给稍后使用。

1.6 read函数分析

字符设备 mpu6050_read() 依次读取 6 个 16 位原始量:

result[0..2]:X/Y/Z 三轴加速度
result[3..5]:X/Y/Z 三轴角速度
总大小:6 × sizeof(short);在本课程 ARM ABI 中为 12 字节

MPU6050 每个量由高、低两个连续寄存器组成。课程代码分别读一个字节,再用“高字节左移 8 位 + 低字节”组合。这里有三个需要特别警惕的实现问题:

  • data_H、data_L 被声明为普通 char;若目标编译器把 char 当有符号类型,字节大于 0x7F 时会发生符号扩展,左移负值还涉及未定义行为。处理原始字节通常应使用 u8 并显式转换;
  • 12 次 i2c_read_mpu6050() 的返回值全部被忽略,某次总线读取失败后仍会拼接并复制数据;
  • MPU6050 从 0x3B 到 0x48 的传感器输出寄存器是连续的,中间包含两字节温度值。可以一次连续读取 14 字节后提取六轴数据,减少总线启动次数;这属于改进方向,精校稿没有擅自改写课程源码。

用户传入的 cnt 也必须限制在内核数组大小以内。课程应用恰好传 12,但驱动直接执行 copy_to_user(buf, mpu6050_result, cnt);若其他程序传入大于 12 的值,就会越过局部数组读取内核栈内容,属于严重边界错误。

copy_to_user() 返回“未能复制的字节数”,所以用非零判断失败是对的;但 Linux 的 read() 成功返回值应是实际交给用户的字节数,返回 0 表示 EOF/没有更多数据。课程代码复制成功后仍返回 0,会让应用误以为未读到任何字节。常规失败还应返回诸如 -EFAULT 的具体负错误码,而不是笼统的 -1。

1.6.1 read函数之i2c_read_mpu6050分析

读取寄存器使用两条消息组成一次组合传输:

消息 0:START → 0x68 + Write → 目标寄存器地址
消息 1:Repeated START → 0x68 + Read → 读取 length 字节 → STOP

两条消息一次性交给 i2c_transfer(..., 2)。标准 I²C 组合传输不会在两条消息之间发 STOP,而是发 repeated START;这使 MPU6050 保持刚刚指定的寄存器位置。原稿所说“先发送一个消息,再读取一个消息”方向正确,但“之前发送一个数据、这里发送两个数据”应改为“之前提交 1 条消息,这里提交 2 条消息”,消息数不等于字节数。

第二条消息设置 I2C_M_RD,接收的数据由 adapter 驱动写入 data 指向的缓冲区。该宏能直接使用,是因为驱动包含的 Linux I²C 头文件提供了定义并按目标内核头文件编译;不是“只要编译时找到内核源码,任意宏都能自动生效”。

二、字符设备文件操作

2.1 字符设备文件操作函数集

字符设备函数表是内核与驱动之间的接口契约。没有实现的成员保持 NULL,VFS 会按各操作的规则处理,并不需要为了“看起来完整”把所有成员都填上。本例没有实现 write 或 unlocked_ioctl,因此不能从“设备节点可按 O_RDWR 打开”推出它支持写入或 ioctl;O_RDWR 只是应用申请的访问模式。

2.1.1 release函数实现

mpu6050_release() 只打印日志并返回 0,因为本例没有为每次打开分配独立资源。打印不是“加上以后更规范”,频繁打开/关闭时反而可能污染内核日志;是否保留应由调试需要和日志级别决定。

release 适合清理保存在 struct file 私有数据中的每次打开资源。它不同于 I²C 驱动的 remove():前者针对文件对象生命周期,后者针对设备与驱动解绑,并负责撤销 class/device、cdev 和设备号等驱动级资源。

配套应用的主循环是无限循环,位于循环之后的两次正常 close() 在现有控制流中到达不了;读取错误分支会显式关闭两个描述符,进程退出时内核也会关闭仍打开的描述符。这不改变 release 的原理,但说明“源码里写了 close”不等于该行在当前运行路径中一定执行。

2.2 平台驱动函数集

本标题沿用原稿结构,但课程 MPU6050 代码实际是 struct i2c_driver,其 probe() 签名接收 struct i2c_client *;这里不是 platform driver。“平台驱动函数集”是源码旧注释和原稿造成的分类错误。

同一份驱动同时处在两个层次:

  • I²C 总线层:i2c_driver、i2c_client、probe/remove、i2c_transfer;
  • 字符设备层:设备号、cdev、file_operations、class/device 与 /dev 节点。

I²C 层解决“驱动如何绑定硬件、如何在总线上收发”,字符设备层解决“普通应用如何通过文件接口进入课程驱动”。把二者的位置关系分清,比死记五个函数更重要。

三、思维导图梳理

本节已经把 MPU6050 课程驱动从模块注册、I²C 匹配、probe()、字符设备建立,一直追到应用 open/read/close 与底层 I²C 消息。最关键的对应关系是:

用户空间VFS/字符设备层课程驱动I²C 层
open()file_operations.openmpu6050_open() → mpu6050_init()4 次寄存器写
read()file_operations.readmpu6050_read()读取六轴原始寄存器并 copy_to_user()
最后一次 close()file_operations.releasempu6050_release()本例不访问总线
rmmod模块/驱动核心i2c_del_driver() → remove()解绑设备并清理字符接口

下节将分析 OLED 驱动。配套 oled_drv.c 同样定义 struct i2c_driver 并额外注册字符设备,所以框架相似;但 OLED 命令/数据协议、文件操作和具体错误路径不同,不能只替换名字就认为两个驱动完全等价。

复习时建议沿两条方向各追一次:从 insmod 向下追到 probe(),再从应用 read() 向下追到 i2c_transfer()。同时用返回值、dmesg、sysfs、/dev 节点和实际总线应答分别验证;任意一层成功都不能单独证明整条链路无误。