MPU6050驱动分析(2)

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

347.5.3MPU6050驱动分析(2)

// 把字符设备节点使用的名称保存在宏中;这只是名称字符串,不是设备号。
#define DEV_NAME "I2C1_mpu6050"
// 本驱动只申请一个连续的字符设备号。
#define DEV_CNT (1)

// 保存内核分配给本驱动的第一个字符设备号,其中同时编码了主、次设备号。
static dev_t mpu6050_devno;

// 向内核动态申请从次设备号 0 开始、数量为 DEV_CNT 的连续设备号。
ret = alloc_chrdev_region(&mpu6050_devno, 0, DEV_CNT, DEV_NAME);
// 检查设备号申请是否失败;内核函数失败时返回负错误码。
if (ret < 0)
{ // 进入设备号申请失败的处理分支。
    // 向内核日志输出失败提示。
    printk("fail to alloc mpu6050_devno\n");
    // 保持课程源码原样:向 probe() 的调用者返回 -1。
    return -1;
} // 设备号申请失败的处理分支结束。

// 应用程序按路径打开 mdev 创建的 MPU6050 字符设备节点。
fd_mpu = open("/dev/I2C1_mpu6050", O_RDWR);

源码边界:代码区只收录本节实际聚焦的设备名、设备数量、dev_t 变量、alloc_chrdev_region() 及应用层 open()。它们均来自课程配套源码,只移除原注释并增加逐行解释;没有提前抄入下一节才继续分析的 cdev_init()、cdev_add() 等代码,也没有抄入 Linux 内核库函数的实现。

一、思维导图

本节沿着 mpu6050_probe() 继续向下分析。课程思维导图把 MPU6050 与 OLED 的 probe() 都概括为五个阶段,便于先认识共性:

  1. 申请字符设备号;
  2. 初始化并添加 cdev,把设备号与 file_operations 关联;
  3. 创建 class;
  4. 创建设备对象,由用户空间设备管理程序生成 /dev 节点;
  5. 保存总线设备对象等私有数据,供后续读写使用。

这是当前两份课程驱动的结构总结,不是所有 Linux 驱动都必须执行的固定五步。许多内核驱动只向某个子系统提供服务,并不注册字符设备;即使都使用字符设备框架,错误回滚、资源管理和硬件初始化也可能不同。

本节重点是第一步 alloc_chrdev_region(),随后借助 /dev、/proc 和 /sys 观察设备号与内核设备模型。cdev 的建立过程留到后续课程继续展开。

二、驱动共性分析

alloc_chrdev_region() 用于动态申请一段尚未占用的字符设备号。课程所用 Linux 4.1.15 中的接口可概括为:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor,
                        unsigned count, const char *name);

这里展示的是函数声明,帮助理解参数,不是把内核库函数实现补进课程业务代码。

参数本例实参准确含义
dev&mpu6050_devno输出参数;成功后写回所分配区间的第一个设备号
baseminor0起始次设备号
countDEV_CNT,即 1申请的连续设备号数量,而不是“要加载的驱动数量”
nameDEV_NAME登记名称,可在 /proc/devices 中辅助识别;它本身不是设备号

成功返回 0,失败返回负错误码。课程源码用 ret < 0 判断失败,这是正确的;但随后固定返回 -1 会丢失内核给出的具体 errno。精校稿保留源码原样,只提示这个诊断局限。

必须把这一步的能力边界分清:

  • 它只在内核的字符设备号命名空间中预留一个区间;
  • 它不会初始化或添加 cdev;
  • 它不会建立 class、sysfs 设备对象或 /dev 节点;
  • 它也不会访问 MPU6050 寄存器,更不能据此判断传感器硬件正常。

课程演示用源码搜索跳转到函数定义。更稳妥的阅读方法是先查头文件声明和内核 API 注释,再按符号追到 fs/char_dev.c 的实现;不能依据 OCR 得到的模糊文件名猜测源码位置。Linux 字符设备 API

三、字符设备号分配

3.1 设备号定义及作用

设备号、登记名称和设备节点名称是三个不同概念:

对象本例用途
字符设备号mpu6050_devno 保存的 dev_t 值由主设备号和次设备号组成,是 VFS 查找字符设备处理对象的关键编号
设备号登记名称DEV_NAME,内容为 I2C1_mpu6050供内核登记和 /proc/devices 显示,便于人识别
设备节点路径/dev/I2C1_mpu6050应用传给 open() 的文件系统路径;节点内部记录设备类型和设备号
字符设备对象struct cdev把设备号范围连接到驱动的 file_operations

课程源码为了统一命名,在设备号申请和后续设备创建时都复用了 DEV_NAME,所以登记名称与最终节点名看起来相同;这不代表两者由 alloc_chrdev_region() 自动绑定。最终 /dev 名称来自后续设备对象的命名,并由本项目的 BusyBox mdev 创建节点。

课程内核中的 dev_t 最终基于 32 位无符号整数类型,并采用 20 位次设备号:

高 12 位:主设备号
低 20 位:次设备号

内核提供 MAJOR(dev)、MINOR(dev) 和 MKDEV(major, minor) 宏用于拆分与组合。原稿说 dev_t “本质是 unsigned long”不符合本课程 Linux 4.1.15 的类型定义;写驱动时也应使用公开类型和这些宏,不要依赖某一平台的基础整数类型。

主设备号标识的是已登记的字符设备处理范围,次设备号由该驱动在自己的主设备号范围内解释。把它们类比成“地区码和个人号”只能帮助记忆层级,不能推出“所有同类硬件必然共用一个主设备号”:

  • /dev/i2c-0、/dev/i2c-1 属于通用 i2c-dev 接口,通常同为主设备号 89,次设备号区分适配器;
  • 课程自定义的 /dev/I2C1_mpu6050 并不属于 i2c-dev 节点,它由课程驱动另行动态申请设备号;
  • tty0 等虚拟终端常见主设备号 4,但不同类型的串口、伪终端和其他 TTY 驱动并不都使用主设备号 4。

因此,主设备号反映内核字符设备注册关系,而不是对硬件协议或产品类别的全局分类。

3.2 应用程序代码

应用程序执行 fd_mpu = open("/dev/I2C1_mpu6050", O_RDWR);。

VFS 先解析路径并取得设备节点中记录的设备号,再根据该设备号找到已注册的字符设备对象和对应的 file_operations。所以真正完成应用与驱动分派的是“节点中的设备号 → cdev → 文件操作回调”这条链,而不是文件名字符串本身。

可在开发板上查看节点:

ls -l /dev/I2C1_mpu6050 /dev/oled /dev/i2c-0 /dev/i2c-1
stat /dev/I2C1_mpu6050

ls -l 输出开头的 c 表示字符设备,原本显示普通文件大小的位置会列出“主设备号, 次设备号”。视频某次启动中,MPU6050 和 OLED 的动态主设备号分别出现过 248、247,次设备号都是 0;这些数字由当时内核的空闲情况决定,不能写进应用,也不能假设重启后永远不变。

设备号有两种常见申请方式:

  • 动态申请:alloc_chrdev_region() 让内核选择可用主设备号,适合课程驱动和大多数新驱动;
  • 指定申请:先用 MKDEV() 组合期望的主、次设备号,再用 register_chrdev_region() 登记。它不是“已经过时”,但必须避免与现有编号冲突,也通常需要正式分配或项目内约定。

查看当前已登记的主设备号:

cat /proc/devices

输出分为 Character devices 与 Block devices。它列出已注册的主设备号及登记名称,不是开发板上所有物理器件的数量清单,也不包含网络接口这类非字符/块设备。

课程卸载 OLED 驱动后再次查看,OLED 对应条目消失。原因是设备解绑时,配套驱动的清理路径最终调用 unregister_chrdev_region() 释放了号码范围。这个实验能证明该范围已被注销;不能单凭条目消失判断硬件是否断电,也不能把 rmmod 后所有同名对象的变化推广到共享设备号范围的复杂驱动。

四、根文件系统

/dev、/proc 和 /sys 都以文件路径提供接口,但职责不同:

路径本项目中的来源主要内容不应误解为
/dev根文件系统目录,节点由 BusyBox mdev 管理字符/块设备访问入口及少量伪设备“每个硬件的完整信息库”或普通数据文件目录
/proc挂载的 proc 虚拟文件系统进程信息及部分内核运行状态,如 CPU、IRQ、已注册设备号磁盘中长期保存的日志
/sys挂载的 sysfs 虚拟文件系统内核设备模型、总线、驱动、class 和对象属性与 /proc 完全等价的另一个目录

课程根文件系统的 /etc/fstab 分别把 proc 挂到 /proc、把 sysfs 挂到 /sys;/etc/init.d/S10mdev 则设置 mdev 热插拔处理程序并执行 mdev -s 扫描已有设备。也就是说,挂载点目录存在于根文件系统中,但 /proc、/sys 下运行时看到的内容由内核动态生成;/dev 节点由设备管理流程建立。

“Linux 中一切皆文件”应理解为:Linux 尽量把不同资源暴露成统一的文件描述符或文件式接口。它不表示所有对象都是磁盘上的普通文件,也不表示所有驱动都必须出现于 /dev。

五、驱动问题排查

5.1 驱动问题排查方法

视频用下面的命令检查中断:

cat /proc/interrupts

它显示已注册 IRQ 的每 CPU 计数、控制器信息及处理程序名称,可用于判断驱动是否成功申请了某个中断、运行期间计数是否增长。但证据边界要严格:

  • 出现条目,通常只能证明某个 IRQ action 已注册;不能证明整套驱动、硬件数据和应用都正常;
  • 条目存在但计数不增长,可能是设备没有产生中断、触发类型或接线错误,也可能只是当前没有事件;
  • 没有条目,可能是驱动未绑定、request_irq() 失败或尚未执行,也可能驱动本来就采用轮询而不申请中断。

课程 MPU6050 驱动没有调用 request_irq(),设备树中的中断属性也未启用,因此不能期待它在 /proc/interrupts 中出现。遇到问题应先根据源码确认“本驱动是否设计为使用中断”,再选择检查项。

5.2 内核数据查看

排查时可按层次选接口,而不是在三个目录中盲目搜索:

要确认的问题推荐观察点
模块是否装入、初始化是否报错lsmod、dmesg
I²C 设备对象是否实例化、是否绑定驱动/sys/bus/i2c/devices、对应 driver 链接
字符设备号是否登记/proc/devices
设备号是否关联到 sysfs 设备对象/sys/dev/char/<主设备号>:<次设备号>
mdev 是否建立正确节点ls -l /dev/...、stat /dev/...
中断是否注册、计数是否变化/proc/interrupts

lsmod 只说明模块在内核中,不能证明它已匹配设备;/proc/devices 有登记项也只说明字符设备号注册成功,不能证明 /dev 节点存在或传感器通信正常。

5.3 /sys目录介绍

sysfs 是以内核对象模型为基础的虚拟文件系统。它把 device、driver、bus、class 等对象及其关系表示成目录、属性文件和符号链接。常见视角包括:

  • /sys/devices:设备层次的主要实体位置;
  • /sys/bus:按总线组织设备和驱动;
  • /sys/class:按功能类别组织设备;
  • /sys/module:已装入模块及其参数、引用等信息。

同一个内核对象可能从多个视角被引用,所以 /sys/bus/.../devices 中常见指向 /sys/devices 的符号链接。这不是重复创建了多份硬件,而是对同一对象提供不同索引。sysfs 文档

5.4 /sys目录下设备信息查看

通过开发板串口终端执行命令,只是与板上 shell 交互的一种方式;/sys 内容来自开发板内核,并不属于串口本身。原稿中的“CD-SSRS”等是语音识别错误,不能据此确定串口设备名。

查看 I²C 设备模型可执行:

cd /sys/bus/i2c
ls -l
ls -l devices
ls -l drivers

这里 devices 组织已经实例化的 I²C adapter/client,drivers 组织已注册的 I²C 驱动。目录存在不等于目标设备已经绑定,必须继续查看具体条目和 driver 链接。

5.5 例题1: 设备节点信息查看

课程中出现的 0-003c、0-0068 可按以下规则阅读:

0-0068
│ └── I²C 7 位从地址 0x68,以 4 位十六进制显示
└──── I²C 总线编号 0,以十进制显示

因此,0-0068 对应总线 0 上地址 0x68 的 MPU6050 client;0-003c 对应同一总线上地址 0x3C 的 OLED client。它们是 I²C 设备模型名称,不是 /dev 字符设备节点,也不能写成 OCR 误识别的“3c 对应 rad 设备”。Linux I²C sysfs 说明

可逐项验证:

cd /sys/bus/i2c/devices
ls -l
cat 0-0068/name
ls -l 0-0068/driver
readlink -f 0-0068/driver

name 是 I²C client 在内核中的名称。对课程 Linux 4.1.15 和 compatible = "mpu6050,INV_MPU6050",设备树实例化时会从逗号后的部分生成 I²C 类型名,因此运行时通常会看到 INV_MPU6050;它不等于字符设备节点名 I2C1_mpu6050。应以板上实际输出为最终证据。

driver 链接存在,说明当前 client 已绑定到某个 I²C 驱动;链接不存在可能是尚未匹配、probe() 失败、被手工解绑或驱动尚未装入。即使链接存在,也仍需检查 probe() 后续步骤、dmesg 和实际 I²C 读写。

5.6 内核信息查看方式总结

本节形成的排障证据链可以概括为:

模块层:lsmod、dmesg
    ↓
总线层:/sys/bus/i2c/devices 与 drivers
    ↓
绑定层:具体设备目录下的 driver 符号链接
    ↓
字符设备层:/proc/devices、/sys/dev/char、/dev 节点
    ↓
数据路径:应用 open/read/ioctl 的返回值与驱动日志
    ↓
硬件层:供电、地址、连线以及实际总线通信

这些接口提供的是不同层次的局部证据。比如:

  • dmesg 中出现 probe 开头日志,不代表后续设备号申请成功;
  • /proc/devices 出现名称,不代表 cdev_add()、mdev 或硬件访问成功;
  • /dev 节点存在,不代表节点中的设备号正确,也不代表驱动仍在工作;
  • sysfs 中存在 I²C client,不代表 driver 链接一定存在。

按层验证能缩小故障范围,避免仅凭某一个文件或一条日志宣布“驱动正常”。

5.7 例题2: 文件链接查看

ls -l 输出首字符为 l 时,表示该目录项是符号链接;-> 右侧是保存的目标路径。例如 sysfs 中的 driver、module、of_node 和 subsystem 常用链接表达对象关系。

常用验证命令:

ls -l driver
readlink driver
readlink -f driver
pwd -L
pwd -P
  • readlink 显示链接中原样保存的目标;
  • readlink -f 解析中间所有符号链接、. 与 ..,给出规范化路径;
  • pwd -L 倾向保留 shell 的逻辑路径,pwd -P 显示解析链接后的物理路径。

只能对最终指向目录的符号链接执行 cd;指向普通属性文件的链接不能“进入”。链接目标不存在时会成为悬空链接,ls -l 仍可看到链接本身,但访问目标会失败。

六、快捷方式

把符号链接类比成桌面快捷方式有助于理解“入口不等于目标本体”:删除链接通常不等于删除目标,通过链接访问则会到达目标对象。

但二者并不完全相同。POSIX 符号链接是文件系统目录项,内核会在路径解析过程中跟随它;Windows 常见的 .lnk 快捷方式通常由图形 shell 或应用解释。因此,应把“快捷方式”当作入门类比,不要据此推导系统调用行为。POSIX 路径名与符号链接解析

七、文件示例讲解

sysfs 链接目标常写成类似 ../../../bus/i2c/drivers/mpu6050 的相对路径。每个 .. 都从“符号链接所在目录”向上一级,再继续拼接后续目录;它不是从用户当前 shell 的工作目录起算。

课程中连续 cd 后感觉“又回到原处”,正是因为多个 sysfs 视图通过符号链接相互引用。例如:

/sys/bus/i2c/devices/0-0068
        └─ 指向 /sys/devices/... 下的真实设备对象
                ├─ driver  ─→ /sys/bus/i2c/drivers/mpu6050
                ├─ of_node ─→ /sys/firmware/devicetree/base/...
                └─ subsystem ─→ /sys/bus/i2c

这些链接表达“设备属于哪条总线、绑定哪个驱动、来自哪个设备树节点”等关系,并没有复制对象。沿链接浏览时应结合 ls -l 和 readlink -f,不要只凭命令提示符中的路径判断位置。

本节只要求理解并查看链接,链接的创建方法留待后续课程。实际文件系统中通常用 ln -s 目标 链接名 创建符号链接,但不要在 /sys 中随意手工增删:sysfs 的对象和链接由内核设备模型维护。

本节相关权威资料还包括 procfs 文档 与 Linux 设备号 API。结合课程内核源码、根文件系统配置和板端实际输出,才能区分“代码设计”“内核对象状态”和“硬件事实”。