MPU6050驱动分析(2)
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() 都概括为五个阶段,便于先认识共性:
- 申请字符设备号;
- 初始化并添加
cdev,把设备号与file_operations关联; - 创建 class;
- 创建设备对象,由用户空间设备管理程序生成
/dev节点; - 保存总线设备对象等私有数据,供后续读写使用。
这是当前两份课程驱动的结构总结,不是所有 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 | 输出参数;成功后写回所分配区间的第一个设备号 |
baseminor | 0 | 起始次设备号 |
count | DEV_CNT,即 1 | 申请的连续设备号数量,而不是“要加载的驱动数量” |
name | DEV_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。结合课程内核源码、根文件系统配置和板端实际输出,才能区分“代码设计”“内核对象状态”和“硬件事实”。