MPU6050驱动分析(1)
346.5.2MPU6050驱动分析(1)
// 打开课程 OLED 驱动提供的字符设备节点,并请求可读写访问。
fd_oled = open("/dev/oled",O_RDWR);
// 打开课程 MPU6050 驱动提供的字符设备节点,并请求可读写访问。
fd_mpu = open("/dev/I2C1_mpu6050", O_RDWR);
// 定义非设备树场景可使用的 I²C 设备 ID 表。
static const struct i2c_device_id gtp_device_id[] = {
// 声明该驱动支持名称为 mpu6050 的 I²C 设备,后面的 0 是本例未使用的私有数据。
{"mpu6050", 0},
// 用全零表项标记数组结束,I²C 核心遍历到这里停止。
{}
};
// 定义 Open Firmware/Device Tree 匹配表。
static const struct of_device_id mpu6050_of_match_table[] = {
// 该字符串必须与目标设备树节点中的 compatible 值匹配。
{.compatible = "mpu6050,INV_MPU6050"},
// 用全零表项标记数组结束。
{}
};
// 定义并初始化一个 I²C 驱动描述对象,供 I²C 核心注册和匹配。
struct i2c_driver mpu6050_driver = {
// 匹配并绑定设备时,由驱动核心调用 mpu6050_probe。
.probe = mpu6050_probe,
// 设备解绑或驱动注销时,由驱动核心调用 mpu6050_remove。
.remove = mpu6050_remove,
// 指向前面定义的传统 I²C 设备 ID 表。
.id_table = gtp_device_id,
// 填写所有 Linux 设备驱动共有的 device_driver 子对象。
.driver = {
// 设置驱动名称;它也用于日志、sysfs 等内核驱动模型信息。
.name = "mpu6050",
// 声明该驱动对象属于当前模块,供模块引用计数等机制使用。
.owner = THIS_MODULE,
// 告诉驱动核心使用前面的设备树匹配表参与 OF 匹配。
.of_match_table = mpu6050_of_match_table,
// device_driver 子对象初始化结束。
},
// i2c_driver 对象初始化结束。
};
// 定义模块加载时运行的初始化函数。
static int __init mpu6050_driver_init(void)
{ // 初始化函数开始。
// 保存 I²C 驱动注册函数的返回值。
int ret;
// 在内核日志中记录模块初始化入口已经执行。
pr_info("mpu6050_driver_init\n");
// 把 mpu6050_driver 注册到 Linux I²C 核心。
ret = i2c_add_driver(&mpu6050_driver);
// 将注册结果返回给模块加载器,0 表示注册成功,负值表示失败。
return ret;
} // 初始化函数结束。
// 定义模块卸载时运行的退出函数。
static void __exit mpu6050_driver_exit(void)
{ // 退出函数开始。
// 在内核日志中记录模块退出入口已经执行。
pr_info("mpu6050_driver_exit\n");
// 从 I²C 核心注销驱动,并解除它与已绑定设备的关系。
i2c_del_driver(&mpu6050_driver);
} // 退出函数结束。
// 将 mpu6050_driver_init 登记为该模块的初始化入口。
module_init(mpu6050_driver_init);
// 将 mpu6050_driver_exit 登记为该模块的退出入口。
module_exit(mpu6050_driver_exit);
// 声明模块使用 GPL 兼容许可证。
MODULE_LICENSE("GPL");
// 这是课程在 mpu6050_probe() 开头加入的调试输出;字符串中的 successed 拼写保留源码原样。
printk(KERN_EMERG "mpu6050 match successed \n");
// 在 I²C 控制器节点下声明地址为 0x68 的 MPU6050 设备。
mpu6050@68 {
// 提供设备身份字符串,供驱动的 of_match_table 匹配。
compatible = "mpu6050,INV_MPU6050";
// 指定该 I²C 从设备使用 7 位地址 0x68。
reg = <0x68>;
// 启用该设备树节点。
status = "okay";
// MPU6050 设备节点结束。
};
# 指定课程用于构建外部模块的目标内核源码/构建目录。
KERN_DIR = /home/wfeng/Linux/kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga
# 声明 Makefile 的默认构建目标。
all:
# 进入目标内核目录,通过 Kbuild 为当前目录中的 ARM 外部模块执行交叉编译。
make -C $(KERN_DIR) M=`pwd` modules ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# 告诉 Kbuild 分别生成 oled_drv.ko 和 mpu6050_drv.ko 两个可加载模块。
obj-m += oled_drv.o mpu6050_drv.o
源码边界:代码区收录了视频本节实际聚焦的应用层
open()、MPU6050 匹配表、i2c_driver、模块 init/exit、probe()调试输出、设备树节点,以及用于说明内核构建依赖的 Makefile 关键部分。代码均来自课程配套文件,只去掉旧说明并增加逐行注释;没有抄入i2c_register_driver()等 Linux 内核库函数的实现,也没有提前补入下一课才展开的字符设备注册代码。
一、学驱动
本节开始分析 MPU6050 驱动的注册与匹配过程,核心链路是:
装入 mpu6050_drv.ko
→ module_init() 登记的 mpu6050_driver_init()
→ i2c_add_driver(&mpu6050_driver)
→ I²C 核心注册驱动
→ 驱动核心把该驱动与已有 I²C 设备对象进行匹配
→ 匹配成功后调用 mpu6050_probe()
→ probe 再注册本课程使用的字符设备接口
其中必须分清四件事:模块已经装入、I²C 驱动已经注册、设备与驱动已经绑定、字符设备节点已经建立。这四个状态前后相关,但不能相互替代。
1.1 回顾上节内容
视频把驱动描述为“从 module_init 开始执行”,这是便于阅读代码的简化说法。module_init() 本身是登记初始化函数的宏,不是 CPU 跳入驱动的普通函数入口:
- 编译为
.ko时,内核装入模块后调用它登记的初始化函数; - 编译进内核时,初始化函数在相应 initcall 阶段执行;
- 应用程序在进入
main()前也有加载器和 C 运行时准备,裸机 MCU 在main()前同样有复位和启动代码,所以“其他程序绝对从 main 第一行开始”也只是入门近似。
设备树描述板级硬件拓扑和属性。对于本例,启用的 I²C 控制器及其 mpu6050@68 子节点会使内核建立相应的 I²C 设备对象;驱动注册后,I²C 总线的匹配逻辑再尝试将该设备与 mpu6050_driver 绑定。设备树只是对硬件的描述,并不会凭空确认芯片真的焊接正确或能在 0x68 应答。Linux Device Tree 使用模型
在课程配套的 Linux 4.1.15 内核中,I²C 匹配顺序包括 OF/Device Tree、ACPI 和 i2c_device_id。本开发板走设备树路径时,主要比较设备节点的 compatible 与驱动的 of_match_table;因此不能把“所有 Linux 驱动都只用 compatible 匹配”当成通用规则。
匹配成功后,驱动核心调用 probe()。probe() 返回 0 才表示此次设备绑定成功;如果它在后续资源申请或硬件初始化中返回负错误码,即使 compatible 已经匹配,绑定仍会失败。
应用层回顾中的真实节点名是 /dev/oled 和 /dev/I2C1_mpu6050,不是原稿 OCR 得到的 /dev/mpu6050、/dev/oad。应用通过系统调用进入 VFS,再由字符设备号和 file_operations 分派到课程驱动的回调。这里也不能扩展成“每个驱动都会生成 /dev 文件”:网络、时钟、供电、引脚控制等许多内核驱动只服务其他内核子系统,并不直接向普通应用提供设备节点。
本项目中 /dev 节点也不是驱动代码直接向根文件系统写出的普通文件。probe() 注册 class/device 后产生设备模型信息和 uevent,根文件系统中的 BusyBox mdev 再据此创建节点。
1.2 看源代码
视频用思维导图把 MPU6050 与 OLED 驱动分成若干相似部分,目的是识别共同框架。配套源码确实都有“总线驱动注册、匹配、probe、字符设备接口、退出清理”等层次,但它们并非完全相同:
- MPU6050 定义
struct i2c_driver,通过 I²C 核心与i2c_client交互; - 课程 OLED 代码定义
struct platform_driver,使用平台总线匹配,再自行通过 GPIO 模拟 I²C 时序; - 两者都额外注册字符设备,因而应用能通过
/dev节点操作它们;这属于课程设计,不代表所有 I²C 或 platform 驱动都必须这样做。
“长得相似”来自 Linux 驱动模型和字符设备框架的共同约束,不应理解成改几个名字就能安全复制出新驱动。设备的供电、寄存器协议、中断、并发、错误恢复和电源管理仍与具体硬件有关。
1.3 学驱动
从 51、STM32 到 i.MX6ULL,I²C 总线仍遵循起始、地址、应答、数据和停止等基本协议,但软件层次不同:
| 裸机 MCU 常见做法 | Linux 中的本例 |
|---|---|
| 应用代码直接操作 I²C 外设寄存器或 HAL | I²C 控制器驱动实现 adapter/algorithm |
| 单个固件同时承担业务和硬件访问 | I²C core 管理 adapter、client 与 driver |
初始化顺序由 main() 或启动代码安排 | 驱动模型按设备与驱动注册时机执行匹配和 probe() |
| 业务函数直接调用硬件访问函数 | 用户应用经 VFS/字符设备回调进入本课程驱动 |
因此,学习 Linux 驱动既要理解 I²C 硬件,也要学习内核对象的生命周期、总线匹配、回调、资源管理和用户接口。代码框架不是“乱七八糟的附加内容”,而是让多个设备、多个驱动和并发访问能够统一管理的机制。
从文件底部的 module_init() 向上追踪,是本节很实用的阅读入口;但不是唯一方法。也可以先看 struct i2c_driver、设备树 binding、probe() 或 file_operations,再按问题所在层次追踪。
1.3.1 mpu6050驱动初始化函数
标题下方已经完整保留本节分析的初始化函数。对课程所用 Linux 4.1.15 内核,i2c_add_driver(driver) 是一个宏,它把当前模块 THIS_MODULE 与驱动对象转交给 i2c_register_driver()。后者不是“配置 MPU6050 寄存器”,而是把一份 I²C 驱动描述注册进驱动模型。
mpu6050_driver 中本节涉及的成员如下:
| 成员 | 作用 |
|---|---|
.probe | 每个匹配设备绑定时的初始化回调 |
.remove | 设备解绑时的清理回调 |
.id_table | 传统 I²C 名称匹配表,也可参与模块别名生成 |
.driver.name | 驱动模型中的名称 |
.driver.owner | 指向当前模块,参与模块生命周期管理 |
.driver.of_match_table | 设备树匹配表 |
注册函数返回 0,只能说明驱动注册过程成功。若系统没有相匹配的 I²C 设备对象,模块仍可能成功装入,但 mpu6050_probe() 一次也不会执行。反之,一个驱动若匹配多个设备实例,probe() 可以被调用多次,因此真实驱动通常把每个设备的状态保存到与对应 i2c_client 关联的私有结构中,而不应全靠单个全局变量。
1.3.1.1 内核源码编译
视频打开 Makefile,是为了说明外部驱动模块依赖目标内核的 Kbuild 环境。关键命令为:
make -C $(KERN_DIR) M=`pwd` modules ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
-C $(KERN_DIR):先切换到目标内核的源码/构建目录,由该内核的顶层 Makefile 负责构建;M=...:告诉 Kbuild,当前目录是外部模块源码目录;modules:构建可加载模块;ARCH=arm:目标架构为 32 位 ARM;CROSS_COMPILE=arm-linux-gnueabihf-:指定交叉工具链前缀;obj-m += oled_drv.o mpu6050_drv.o:分别生成两个.ko模块。
这里只给一个“解压出的内核源码目录”还不一定够。该目录还应包含与目标内核匹配的配置和必要构建产物;若启用了 CONFIG_MODVERSIONS,还要注意 Module.symvers。官方 Kbuild 文档同样要求使用 make -C <kernel_dir> M=$PWD,并说明目标内核需已适当构建或准备。Linux 外部模块构建文档
外部模块不会把整套内核实现复制进 .ko。编译阶段利用内核头文件、配置和符号信息检查接口;模块装入时,其引用的导出符号再由当前运行内核解析。因此,目标内核版本、配置、架构和符号版本不匹配都可能导致模块加载失败。
视频把 struct i2c_driver 类比为 MCU HAL 初始化结构体,有助于理解“先填写结构体,再把地址传给框架”,但两者目的不同:HAL 结构体常直接描述串口波特率、停止位等硬件配置;i2c_driver 主要描述驱动模型的名称、匹配信息和回调函数,真正的 MPU6050 寄存器配置发生在后续回调路径中。
1.3.1.2 打开内核源码
课程使用 Source Insight 浏览内核源码。工具不是重点,关键是必须打开与开发板正在运行的内核相匹配的源码版本。本项目配套压缩包为 linux-imx-rel_imx_4.1.15_2.1.0_ga_20200903.tar.gz。
查找本例接口时可以按两步追踪:
- 在
include/linux/i2c.h中找到i2c_add_driver,确认它是调用i2c_register_driver(THIS_MODULE, driver)的宏; - 在
drivers/i2c/i2c-core.c中找到i2c_register_driver()的函数实现。
若编辑器的“转到定义”只能看到课程目录而找不到内核符号,通常是因为工程索引没有包含内核源码,或宏解析不完整,并不表示函数不存在。可使用工程级符号搜索、文本搜索、ctags/cscope 或命令行 rg 交叉确认。原稿中的 “Set/Please” 是界面文字识别错误,实际操作重点只是搜索符号和跳转定义。
1.3.1.3 mpu3050驱动代码分析
配套内核确实包含 drivers/input/misc/mpu3050.c。MPU3050 与 MPU6050 都属于 InvenSense 的 I²C 运动传感器,因而驱动结构中都有 struct i2c_driver、ID/OF 匹配表、probe() 和 remove();但 MPU3050 是三轴陀螺仪,MPU6050 还集成加速度计,寄存器、数据通道和上层接口不能直接视为完全相同。
内核中的 MPU3050 驱动使用:
module_i2c_driver(mpu3050_i2c_driver)
这不是另一种完全不同的初始化原理,而是 I²C 子系统提供的便捷宏,用来生成与 module_init() + i2c_add_driver()、module_exit() + i2c_del_driver() 等价的样板入口。官方 I²C 驱动文档也把两种写法并列说明。Linux I²C 驱动编写指南
1.3.1.4 ad5380分析
本节通过内核树中的 AD5380/AD5446 等 I²C 器件驱动练习符号跳转。它们的具体芯片功能不是本课重点;真正要观察的是不同驱动都会构造自己的 struct i2c_driver,再通过 i2c_add_driver() 进入同一个 I²C 核心。
搜索结果中的结构体不是课程的 mpu6050_driver 很正常:每个文件传入的是自己的驱动对象。应跟踪函数或宏的定义,而不是误把示例驱动的对象名、probe() 或 ID 表当成 MPU6050 的实现。
Source Insight 中需要连续跳转,是因为第一层常落到 i2c_add_driver 宏,继续追踪才会到 i2c_register_driver() 实体。宏包装不仅传入驱动指针,还补入了 THIS_MODULE。
1.3.1.5 i2c_register_driver分析
在项目配套 Linux 4.1.15 源码中,i2c_register_driver() 位于 drivers/i2c/i2c-core.c 第 1719 行。行号只对这份源码成立,内核版本或补丁变化后应按符号名搜索,不能把 1719 当作固定 API 位置。
该版本函数的关键步骤是:
- 确认 I²C 总线类型已经初始化;
- 设置底层
device_driver的 owner 与 bus; - 调用
driver_register()把驱动加入 I²C 总线的驱动列表,并对已有、尚未绑定的设备尝试匹配; - 初始化该驱动的 detected-client 链表;
- 遍历已经存在的 I²C adapter,处理旧式
detect()/attach_adapter路径。
视频把它概括为“从 I²C 总线上寻找各种设备”,容易误解成注册任何驱动都会在所有地址主动读一遍。本课程 mpu6050_driver 没有实现 detect(),也没有 address_list,所以这条路径不是盲扫物理总线发现 MPU6050;实际设备对象来自设备树节点,驱动核心在已有对象与新驱动之间做匹配。Linux 官方文档也强调,通常由板级描述或固件报告设备,I²C 驱动自身并不创建这些 client;主动检测是受限制的旧式机制。I²C 设备实例化
二、i2c_register_driver函数意义
对本例而言,i2c_register_driver() 的准确含义是“把一份 I²C 驱动登记到 Linux 驱动模型,并触发可用的设备—驱动绑定流程”,不是直接读写 MPU6050,也不负责创建 /dev/I2C1_mpu6050。
需要区分三层对象:
| 对象 | 本例来源 | 作用 |
|---|---|---|
i2c_adapter | i.MX6ULL I²C 控制器驱动 | 表示一条可执行 I²C 传输的总线控制器 |
i2c_client | 设备树中的 mpu6050@68 被 I²C 核心实例化 | 表示某条 adapter 上地址为 0x68 的一个设备实例 |
i2c_driver | 课程 mpu6050_drv.ko 注册 | 表示能管理某类 I²C 设备的匹配信息与回调集合 |
驱动注册和设备注册都可能触发匹配:先有设备、后装驱动可以绑定;先有驱动、后出现设备也可以绑定。Linux 驱动模型文档说明,新设备会遍历总线驱动列表,新驱动也会遍历总线设备列表;匹配成功才调用 probe()。Linux Driver Binding
三、iPhone ci的driver的两项工作
本标题中的 “iPhone ci” 是转写错误,视频实际讲的是 i2c_add_driver/I²C driver。课程归纳的“两项工作”可以更准确地写为:
- 把
mpu6050_driver注册到 I²C 总线类型; - 注册过程触发驱动模型对已有未绑定 I²C 设备的匹配,匹配成功后调用该驱动的
probe()。
第二项不是 i2c_add_driver() 单独解析整个设备树。设备树在更早阶段已经帮助创建 i2c_client,驱动核心此时面对的是内核设备对象。
对配套 4.1.15 内核,I²C 匹配函数依次尝试:
OF/Device Tree 匹配 → ACPI 匹配 → i2c_device_id 名称匹配
设备树场景通过 .driver.of_match_table;非设备树平台还可能使用 .id_table。成功匹配只代表允许调用 probe(),最终是否绑定要看 probe() 返回值。一个模块已出现在 lsmod 中,只证明它已装入,不能证明 probe() 成功。
四、设备树匹配表
课程驱动中的关键联系是:
设备树节点:compatible = "mpu6050,INV_MPU6050"
⇅ 精确字符串匹配
驱动匹配表:.compatible = "mpu6050,INV_MPU6050"
mpu6050_of_match_table 以全零表项结束,并通过 .driver.of_match_table 挂入 mpu6050_driver。设备树中的 reg = <0x68> 则表示该设备在父 I²C 总线上的 7 位地址;compatible 用来描述设备身份,reg 用来描述总线地址,两者职责不同。
课程自定义字符串只要两边完全一致,就能在当前工程中匹配;但规范的 Device Tree binding 通常使用稳定、文档化的 vendor,device 小写字符串。"mpu6050,INV_MPU6050" 把器件名放在厂商位置且混用大写,适合保留以复现课程,不适合作为新 binding 的命名范例。Device Tree binding 编写规则
这份课程驱动还没有写 MODULE_DEVICE_TABLE(of, mpu6050_of_match_table)。.of_match_table 已足以让手工装入后的驱动参与 OF 匹配,但缺少 OF 模块别名可能影响依据设备树 modalias 自动加载模块;课程使用 insmod/启动脚本手动加载,所以仍能工作。
修改 compatible 的目标是让设备对象与正确驱动绑定,不会单独保证 OLED 点亮或 MPU6050 有数据。完整成功还依赖正确 DTB 已被启动加载、I²C 控制器启用、地址和连线正确、模块装入、probe() 全部成功以及应用程序正常运行。
五、设备数修改
本标题的“设备数”是语音识别错误,本节讨论的是“设备树”。课程修改的文件是:
arch/arm/boot/dts/imx6ull-14x14-evk.dts
项目还提供了对应的“更改后的设备树”文件。编辑 .dts 本身不会改变开发板,必须重新编译生成 .dtb,再把开发板启动时真正读取的 DTB 替换为新文件并重启。若启动器仍加载旧 DTB,源码中字符串一致也没有效果。
课程在 mpu6050_probe() 开头加入:
printk(KERN_EMERG "mpu6050 match successed \n")
看到这条日志能证明以下路径至少已经发生:模块初始化、I²C 驱动注册、设备对象匹配、驱动核心进入 probe()。但日志文字是驱动作者手工写的,并非内核自动生成的“匹配成功证书”;而且它位于 probe() 最前面,不能证明后面的设备号申请、cdev、class/device 创建或硬件访问全部成功。源码中的 successed 也只是英文拼写错误,精校稿为保持源码原样不改动。
若没有看到该日志,排查范围不仅是 compatible:还应检查模块是否真的装入、初始化函数是否返回错误、目标 DTB 是否生效、父 I²C 控制器和节点是否启用、设备对象是否已实例化,以及日志是否被过滤。可结合:
dmesg
lsmod
ls -l /sys/bus/i2c/devices
ls -l /sys/bus/i2c/drivers/mpu6050
probe() 是总线型 Linux 驱动的核心绑定回调,通常负责验证设备、申请每设备资源、初始化硬件并注册上层接口;成功返回 0,失败返回负 errno,并应撤销此前已完成的申请。并非“每一种 Linux 驱动都必然有一个名为 probe 的函数”,但 I²C、platform、SPI、PCI 等驱动模型普遍采用这一绑定模式。