MPU6050驱动分析(3)
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 字符设备操作
字符设备注册至少包含三个不同动作:
alloc_chrdev_region():预留设备号范围;cdev_init():初始化内存中的 cdev 对象并保存操作表;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) = 0x06 | DLPF_CFG=6,加速度计/陀螺仪带宽约 5 Hz,陀螺仪输出率为 1 kHz;结合上一行得到约 125 Hz 采样率 |
ACCEL_CONFIG(0x1C) = 0x01 | AFS_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 的四个核心字段是:
| 字段 | 本例写消息 | 含义 |
|---|---|---|
addr | mpu6050_client->addr | 7 位或 10 位从地址;10 位模式还要设置相应标志 |
flags | 0 | 消息方向和特殊协议标志;未设置 I2C_M_RD 即写 |
buf | write_data | 写时的数据源、读时的数据目的地 |
len | 2 | 本条消息的数据字节数 |
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.open | mpu6050_open() → mpu6050_init() | 4 次寄存器写 |
read() | file_operations.read | mpu6050_read() | 读取六轴原始寄存器并 copy_to_user() |
最后一次 close() | file_operations.release | mpu6050_release() | 本例不访问总线 |
rmmod | 模块/驱动核心 | i2c_del_driver() → remove() | 解绑设备并清理字符接口 |
下节将分析 OLED 驱动。配套 oled_drv.c 同样定义 struct i2c_driver 并额外注册字符设备,所以框架相似;但 OLED 命令/数据协议、文件操作和具体错误路径不同,不能只替换名字就认为两个驱动完全等价。
复习时建议沿两条方向各追一次:从 insmod 向下追到 probe(),再从应用 read() 向下追到 i2c_transfer()。同时用返回值、dmesg、sysfs、/dev 节点和实际总线应答分别验证;任意一层成功都不能单独证明整条链路无误。