驱动综述
345.5.1驱动综述
// 定义 MPU6050 驱动模块的初始化函数;__init 表示模块完成初始化后,这段代码占用的内存可以被回收。
static int __init mpu6050_driver_init(void)
{ // 初始化函数开始。
// 保存注册 I²C 驱动时返回的状态码。
int ret;
// 向内核日志打印初始化信息,便于确认该函数已经运行。
pr_info("mpu6050_driver_init\n");
// 向 Linux I²C 核心注册课程定义的 mpu6050_driver;这里只是注册驱动,不代表 probe 一定已经成功。
ret = i2c_add_driver(&mpu6050_driver);
// 把 I²C 核心的注册结果返回给模块加载器,0 表示成功,负值表示失败。
return ret;
} // 初始化函数结束。
// 定义 MPU6050 驱动模块的退出函数;__exit 用于标记仅在模块退出时使用的代码。
static void __exit mpu6050_driver_exit(void)
{ // 退出函数开始。
// 向内核日志打印退出信息,便于确认卸载流程已经运行。
pr_info("mpu6050_driver_exit\n");
// 从 Linux I²C 核心注销课程定义的 mpu6050_driver,并解除它与已匹配设备的绑定。
i2c_del_driver(&mpu6050_driver);
} // 退出函数结束。
// 把 mpu6050_driver_init 登记为该 Linux 模块的初始化入口。
module_init(mpu6050_driver_init);
// 把 mpu6050_driver_exit 登记为该 Linux 模块的退出入口。
module_exit(mpu6050_driver_exit);
// 声明该模块使用 GPL 兼容许可证;它是模块元数据,不是源码许可证文本的替代品。
MODULE_LICENSE("GPL");
// 下面三行是视频回顾 RT-Thread PWM 工程时展示的关键片段,不属于上面的 Linux 驱动。
// 在当前 BSP 配置中启用 PWM3,使相应的条件编译分支能够进入构建结果。
#define BSP_USING_PWM3
// 在当前 BSP 配置中启用 PWM3 的通道 4。
#define BSP_USING_PWM3_CH4
// 把 stm32_pwm_init 登记到 RT-Thread 的设备初始化阶段,由自动初始化机制在启动时调用。
INIT_DEVICE_EXPORT(stm32_pwm_init);
源码边界:上半部分逐字保留自课程随附
mpu6050_drv.c中本节实际讲解的模块初始化、退出和许可证代码;下半部分保留视频回顾 RT-Thread 工程时聚焦的配置宏及自动初始化语句。未把下一节才会详细分析的i2c_driver、匹配表、probe()或file_operations强行搬到源码区,只在正文中说明它们的关系。
一、驱动入门
本节从应用程序与驱动的联系出发,建立 Linux 驱动的整体视图,再借 MPU6050 和 RT-Thread PWM 代码说明“注册入口、设备匹配、初始化阶段”这些概念。课程定位是入门,因此重点是能看懂调用链、会加载和排错,而不是在这一节完成一套新驱动。
需要先区分三个容易混用的“驱动”概念:
- 硬件驱动电路:把低功率控制信号转换成能够带动负载的电压和电流;
- 软件设备驱动:操作系统中管理硬件、向上层提供统一接口的代码;
- 驱动框架或子系统:内核用来组织某一类设备与驱动的公共机制,例如 I²C、SPI、输入、网络和块设备子系统。
1.1 虚拟文件系统
视频在开发板的 /dev 目录中查看了串口、I2C1_mpu6050 和 oled 等设备节点。字符或块设备节点不是普通数据文件,而是“设备号 + 文件类型 + 权限”等信息构成的访问入口。应用调用 open()、read()、write()、ioctl()、close() 时,VFS 根据已经打开的对象把操作分派到对应驱动的方法。
应用程序
└─ open/read/write/ioctl/close 等系统调用
└─ VFS 根据路径找到设备节点,并依据设备号关联字符设备
└─ file_operations 中的 open/read/... 回调
└─ MPU6050 驱动
└─ Linux I²C 核心与控制器驱动
└─ I²C 总线上的 MPU6050 硬件
所以,“应用读写 /dev 文件与驱动通信”适用于本课程自行注册的字符设备,但不能推广为所有硬件的唯一访问方式:网络设备通常经 socket 和网络协议栈访问;显示、输入、音频、摄像头、IIO 等子系统还有各自的用户空间接口。
原稿把设备节点简单写成“由驱动代码生成”,还缺少一个关键环节。项目源码中,MPU6050 的 probe() 调用 class_create() 和 device_create(),后者向设备模型注册对象并产生 sysfs 信息/uevent;本项目根文件系统的 /etc/init.d/S10mdev 又执行 mdev -s 并把 /sbin/mdev 配置为热插拔处理程序,最终由 mdev 根据内核信息在 /dev 中创建或删除节点。Linux 官方 SPI 文档也用 udev/mdev 创建 /dev/spidevB.C 来说明这一分工。Linux SPI userspace API
本课程两个节点名可直接由随附源码核对:MPU6050 使用 DEV_NAME "I2C1_mpu6050",OLED 的 device_create() 指定名称 "oled"。节点存在说明字符设备接口已注册并被设备管理程序处理,但要确认硬件能工作,还应结合驱动匹配日志和实际读写结果。
1.2 什么是驱动
软件驱动是操作系统中控制或管理硬件、处理中断与数据传输,并向其他内核组件或用户空间提供接口的代码。它通常位于通用框架与具体硬件之间:上层不必掌握每一种芯片的寄存器细节,底层驱动负责把通用操作转换为总线事务、寄存器读写或 DMA 等具体动作。
视频把驱动分成“物理驱动”和“软件驱动”,这是便于入门的日常说法,不是 Linux 内核对驱动的正式分类。更准确地说,“物理驱动”是驱动电路或功率级;Linux 本节讨论的驱动则是软件。
1.2.1 驱动的分类
从本节的两个语境看:
| 语境 | 例子 | 解决的问题 |
|---|---|---|
| 硬件驱动电路 | 电机驱动模块、栅极驱动器、LED 恒流驱动器 | 电平转换、提供负载电流、控制功率开关、保护控制器 |
| 软件设备驱动 | CH340 驱动、MPU6050 I²C 驱动、OLED 驱动 | 识别硬件、配置寄存器、传输数据、向上提供软件接口 |
课程后续安排是继续分析 MPU6050、OLED,并可能涉及 F1C 平台的驱动。原稿中的“FRC”是语音识别错误,应为视频口述的 F1C。
1.2.2 电机驱动模块介绍
电机是感性且常常需要较大电流的负载。MCU GPIO 只负责输出逻辑电平或 PWM 控制信号,不应直接承担电机电流。常见直流电机驱动模块内部包含半桥或 H 桥功率开关:
MCU GPIO/PWM ──低功率控制信号──> 电机驱动芯片
外部电机电源 ──大电流路径─────> 功率开关/H 桥 ──> 电机
MCU GND ───────────────────────> 与驱动模块逻辑地共地
H 桥通过改变电机两端电压方向实现正反转,通过 PWM 改变平均电压来调速。驱动芯片还可能集成续流路径、过流保护和过温保护;是否集成、允许的电压与电流必须查具体芯片数据手册。
1.2.3 电机驱动模块特点
视频商品页标出了约 1.5 A 工作电流和 2.5 A 峰值电流。这里的单位是安培(A),不是瓦特(W);电流与功率不能混用,直流侧近似有:
P = U × I
这组数值只能描述画面中的特定模块,不能当作所有电机驱动的统一规格。还要继续确认:
- 数值是每通道还是整板总电流;
- 峰值允许持续多长时间,以及重复周期;
- PCB 铜箔、散热条件和环境温度能否支撑标称电流;
- 电机堵转电流是否小于驱动允许值;
- 启动瞬间电源是否会掉压,是否需要就近放置储能与去耦电容。
GPIO 直接接电机不仅可能过流,电机断电时电感还会试图维持原电流并产生反向电压。正确的驱动级需要提供受控的续流路径;不要只根据“平时转动电流”选型,还要检查启动与堵转的最坏情况。
1.2.4 单片机与电机驱动模块的关系
视频展示了开发板上的 AMS1117-3.3,用它说明控制板电源也有输出能力限制。需要纠正两点:
- AMS1117 是板上的外部线性稳压器,不是单片机内部的稳压芯片;
- 稳压器的总输出能力与某个 GPIO 的源/灌电流上限是两套不同约束。即使 3.3 V 总电源还有余量,单个 GPIO 也不能直接输出电机所需的大电流。
合理连接通常是:MCU 的 GPIO/PWM 接驱动模块逻辑输入,电机由满足电压和堵转电流要求的电源经过驱动功率级供电,控制侧与驱动侧按模块要求共地。电机电源是否与逻辑电源隔离、是否允许共用输入,则要看模块原理图和数据手册。
1.2.5 AMS1117-3.3芯片的参数与应用
AMS1117-3.3 是固定 3.3 V 输出的低压差线性稳压器型号之一,常见用途是把 5 V 等较高电压变成 3.3 V。视频把“最大 1 A”理解成板上随时都能稳定输出 1 A,这不够严谨:数据手册的额定能力受输入输出压差、结温、封装、PCB 散热铜面积和器件具体厂商规格共同限制。
线性稳压器主要把压差消耗为热量,近似为:
P_loss = (V_in - V_out) × I_out
若从 5 V 降到 3.3 V,并真的输出 1 A,则仅稳压器上就约产生 (5 - 3.3) × 1 = 1.7 W 热量;普通小板散热条件下很可能先触发热限制。AMS1117 数据手册还给出随负载变化的 dropout、结温与热阻条件,因此选型不能只看标题中的“1 A”。AMS1117 数据手册
另外,STM32 常见工作电源为 3.3 V,但“51 单片机都工作在 5 V”也不能一概而论;8051 兼容器件很多,允许电压应查所用具体型号。某块 51 开发板是否放置 AMS1117,属于板级设计选择。
1.2.6 串口设备与软件驱动
CH340 是常见 USB 转串口芯片。插入 PC 后,USB 主控制器先完成设备枚举;匹配的 CH340 驱动再把它接入操作系统串口框架,通常在 Windows 中表现为一个 COM 端口。没有合适驱动时,系统可能仍能读到 USB 标识并显示未知或未正确配置的设备,只是不能提供正常串口功能;某些 Windows 版本也可能自动从系统或更新服务获得驱动。
这个例子与 MPU6050/OLED 的共同点是:软件必须通过相应总线和协议管理硬件。不同点是 CH340 位于 USB 子系统,MPU6050 位于 I²C 子系统,课程 OLED 驱动则通过自己的字符设备接口向应用开放功能,三者的具体绑定和用户接口并不相同。
1.3 Linux驱动框架
视频中的框图只描述驱动相关路径,不代表整个 Linux 内核。对本课程最重要的三条路径是:
字符设备:应用 → 系统调用 → VFS → 字符设备 file_operations → 具体驱动/总线 → 硬件
块设备: 应用/文件系统 → VFS → 块层与 I/O 调度 → 块设备驱动 → 存储硬件
网络设备:应用 → socket 系统调用 → TCP/IP 等协议栈 → net_device 驱动 → 网卡/PHY
VFS 为文件和设备节点提供统一的文件接口。Linux 官方文档说明,打开字符或块特殊文件时,VFS 会根据设备信息找到相应驱动,并把文件操作切换到驱动实现的 open() 等方法。Linux VFS 概览
1.3.1 驱动的分类
字符设备、块设备和网络设备是经典的三类入门模型,但不能仅凭外设使用哪条电气总线来分类:
- 字符设备通常以字节流或记录方式访问,并通过字符设备号和
file_operations提供接口,例如串口、输入事件节点及本课程自行注册的 MPU6050/OLED 节点; - 块设备面向可寻址的数据块,支持块层的请求合并、缓存和随机访问,典型例子是硬盘、eMMC 和 SD 卡;原始 NOR/NAND Flash 也可能走 MTD 等专门子系统,不必然表现为普通块设备;
- 网络设备注册为
net_device,数据经网络协议栈收发,应用通常使用 socket,不是直接对网卡执行普通read()/write(); - I²C 与 SPI 首先是总线子系统。挂在它们上面的设备可接入 hwmon、IIO、input、DRM 等其他子系统,也可以通过
i2c-dev、spidev或自定义字符设备暴露给用户空间。Linux 官方文档把/dev/i2c-N明确描述为由i2c-dev提供的字符设备接口,而不是说“I²C 天生就是字符设备”。Linux I²C userspace interface
视频还用网口说明速率等级。100M/1G/10G 通常指链路标称速率,不代表应用必然获得相同吞吐量。NXP 数据手册确认 i.MX6ULL 的 ENET MAC 支持 10/100 Mbit/s;笔记本是否带千兆口要看具体型号,不能一概而论。NXP i.MX6ULL 数据手册
视频展示的万兆网卡带有类似“小笼子”的插槽,通常称 SFP/SFP+ cage。光模块的金手指插入 cage,光纤再接到光模块;也可以使用 DAC/AOC 等不同介质。万兆以太网并非只能走光纤,IEEE 10GBASE-T 也规定了双绞线铜缆方案。IEEE 802.3an 10GBASE-T
1.4 学驱动,学的是什么
课程强调工作中常见的是移植、配置和调试既有驱动,而非从零编写所有代码。这个定位有现实意义,但“驱动都是现成的,所以不用看懂代码”过于绝对。至少需要掌握:
- 当前驱动属于哪个总线或子系统,用户空间接口是什么;
- 设备如何被描述,驱动如何注册、匹配并进入
probe(); - 时钟、GPIO、中断、寄存器、DMA 等资源从何而来;
- 初始化失败时返回值和日志如何沿调用链传播;
- 并发访问、睡眠上下文、内存与设备生命周期等内核约束;
- 加载/卸载、设备树或配置修改、冲突定位和回归验证方法。
本阶段可先抓住“入口、注册、匹配、回调、退出清理”这条主线,再在后续课程逐层深入。
1.5 例题: mpu6050驱动代码分析
标题下方的 Linux 代码展示了课程所称的“内核模块三要素”:初始化入口、退出入口和许可证声明。阅读时不能把宏本身与它登记的函数混为一谈:
module_init(mpu6050_driver_init)登记初始化函数。驱动编译为可加载模块时,它在模块插入时运行;编进内核时,它在内核启动的 initcall 阶段运行;module_exit(mpu6050_driver_exit)登记模块移除时的清理函数;若代码直接编进内核,该退出宏不产生可运行的卸载入口;MODULE_LICENSE("GPL")提供许可证类别元数据,具体含义在 1.7.4 节展开。
Linux 官方 module_init() 文档明确区分了内建驱动与可加载模块的调用时机。Linux Driver Basics
本例的实际调用关系是:
insmod mpu6050_drv.ko
→ 模块加载器装入并完成重定位
→ mpu6050_driver_init()
→ i2c_add_driver(&mpu6050_driver)
→ I²C 核心登记驱动并尝试与已有 I²C 设备匹配
→ 匹配成功后才调用 mpu6050_probe()
→ probe 注册字符设备、创建 class/device,并保存 i2c_client
所以,视频口述“insmod 后执行这个 .c 文件”只是简化说法。严格来说,运行的是 .ko 中登记的初始化入口;i2c_add_driver() 成功表示驱动注册成功,也不单独证明某个 MPU6050 已经匹配并完成 probe()。还要检查 mpu6050 match successed、节点和实际读取结果。
卸载方向则是:
rmmod mpu6050_drv
→ mpu6050_driver_exit()
→ i2c_del_driver(&mpu6050_driver)
→ 解除设备绑定,调用 remove() 清理节点、class、cdev 和设备号
若模块仍被使用、引用计数不允许卸载,或内核未启用模块卸载功能,rmmod 不一定成功。阅读驱动时从底部入口宏向上追踪函数是一种高效方法,但遇到复杂驱动还应结合数据结构、匹配表和回调间的引用关系,而不是机械地“只能从下往上看”。
1.6 例题: 人脸识别项目分析
视频打开的是之前 RT-Thread 人脸识别项目中的驱动目录,不是原稿 OCR 所写的“JAVA 文件夹”。该项目用 PWM 控制舵机,并在 STM32 PWM 驱动末尾使用:
INIT_DEVICE_EXPORT(stm32_pwm_init)
它会把 stm32_pwm_init 的函数指针放入 RT-Thread 自动初始化表的设备初始化级别。系统启动时遍历相应表并调用该函数。RT-Thread 官方文档列出的阶段依次包括 INIT_BOARD_EXPORT、INIT_PREV_EXPORT、INIT_DEVICE_EXPORT、INIT_COMPONENT_EXPORT、INIT_ENV_EXPORT 和 INIT_APP_EXPORT。RT-Thread 自动初始化机制
它与 Linux module_init() 的共同点是“登记一个由系统在规定时机调用的初始化函数”,但机制不同:
| 对比项 | Linux module_init() | RT-Thread INIT_DEVICE_EXPORT() |
|---|---|---|
| 典型产物 | 内建驱动或可加载 .ko 模块 | 与固件静态链接的函数指针表 |
| 调用时机 | 内核 initcall 或模块插入时 | RT-Thread 组件初始化的设备阶段 |
| 动态卸载 | 模块可配合 module_exit() 卸载 | 常见 MCU 固件通常不采用 Linux 式动态模块卸载 |
| 注册对象 | 本例继续向 I²C 核心注册驱动 | 本例初始化并注册 RT-Thread PWM 设备 |
因此,两者可以类比“自动调用入口”,但不能说实现本质完全相同。
1.7 内核基础
本节后半部分用 RT-Thread 启动流程帮助理解初始化入口,又用 board.h 和条件编译说明某段驱动为何进入最终固件。这里需要严格区分“编译时是否包含代码”和“运行时设备与驱动是否匹配”。
1.7.1 RT-Thread启动流程
以视频使用的 MDK-ARM 工程为例,处理器复位后先执行芯片启动汇编与 C 运行时准备,再进入 RT-Thread 的统一启动流程;RT-Thread 完成板级、内核对象、主线程、定时器线程、空闲线程等初始化后启动调度器。启用组件自动初始化时,系统还会遍历不同级别的初始化函数表。
流程可以简化为:
复位入口/启动汇编
→ 设置栈、向量表和 C 运行环境
→ rtthread_startup()
→ 关中断并执行 rt_hw_board_init()
→ 初始化定时器、调度器、信号等内核对象
→ 创建应用 main 线程、定时器线程和 idle 线程
→ 启动调度器
→ 在规定阶段调用自动初始化表中的函数
具体入口会随芯片、编译器和 RT-Thread 工程配置变化,不能把某个 MDK 工程的文件名当作所有平台的固定流程。
1.7.2 RT-Thread内核介绍
视频把 RT-Thread 启动汇编到内核初始化的流程类比成 Linux 的“U-Boot 到 kernel”。两者都经历从低级入口到高级软件环境的初始化,这个抽象类比有助于理解先后顺序,但不能把 RT-Thread 的 .s 启动文件直接等同于 U-Boot:
- MCU 启动文件通常属于同一固件,负责复位入口、栈、向量表和 C 运行环境;
- U-Boot 是 Linux 前独立运行的引导加载程序,通常还能初始化存储和网络、解析环境变量、加载内核/设备树并传递启动参数;
- Linux 内核启动与 RTOS 固件启动的内存管理、设备模型、进程模型也显著不同。
RT-Thread 官方启动流程同样说明,MDK 示例从启动汇编进入 rtthread_startup(),随后完成硬件、内核对象、主线程和调度器初始化。RT-Thread Kernel Basics
1.7.3 RT-Thread设备驱动
原稿把 board.h 直接称为 RT-Thread 的“设备树”,这是不准确的。视频中的 board.h 是 BSP 配置头文件;BSP_USING_PWM3、BSP_USING_PWM3_CH4 等宏在编译期选择 PWM3 及通道 4 的相关代码和配置。Linux Device Tree 则是一种独立的硬件描述数据结构,内核在运行时解析节点和属性,两者只能在“都表达板级硬件配置”这一宽泛意义上类比。
视频中 PWM 相关代码呈正常颜色、RTC 代码变灰,通常表示 IDE 根据当前宏配置判断 RTC 条件编译分支不会进入本次构建;这不是运行时“RTC 驱动加载失败”。以 PWM 为例,逻辑大致是:
board.h/配置系统定义 BSP_USING_PWM3
→ 预处理器保留 #ifdef BSP_USING_PWM3 包围的 PWM3_CONFIG
→ stm32_pwm_init() 遍历被编入的 PWM 对象并注册设备
→ INIT_DEVICE_EXPORT() 让系统在设备初始化阶段调用该函数
还要注意,是否定义宏只是第一关;引脚复用、定时器时钟、通道号和硬件连线也必须正确。RT-Thread 当前 BSP 通常还会通过 Kconfig/RT-Thread Settings 与生成的配置协同选择功能,并非所有项目都只手改 board.h。RT-Thread STM32 BSP 配置示例
1.7.4 内核模块许可证
课程把 MODULE_LICENSE("GPL") 解释成“加上后代码就开源,而且没有它就不能编译或加载”,这需要重点纠正:
MODULE_LICENSE()是提供给内核模块加载器和用户空间工具的许可证类别元数据;- 它不能替代源文件中的 SPDX 标识、版权声明和真实授权条款,也不会因为增加一行宏就自动完成合法开源发布;
- 未声明或声明为非 GPL 兼容许可证的外部模块会被视作 proprietary,并使内核带上
Ptaint 标志;这不等于它在任何配置下都绝对无法编译或加载; - proprietary 模块不能链接使用
EXPORT_SYMBOL_GPL()导出的 GPL-only 内核符号;此外,模块签名、版本、架构和系统策略也可能决定能否加载。
Linux 官方许可证规则明确说明,MODULE_LICENSE() 的唯一目的,是向模块加载器和工具提供“自由软件或专有软件”的足够信息,它不用于决定源码实际采用的精确许可证。Linux kernel licensing rules 内核 taint 文档也说明,缺少或未识别的 MODULE_LICENSE 会按 proprietary 处理。Tainted kernels
因此,把“入口、出口、许可证”称为入门三要素便于记忆,但不是判断模块能否编译和工作的充分条件。真实模块还需要头文件、构建规则、正确的内核接口、资源管理和错误处理。
1.7.5 内核模块代码框架
以课程 MPU6050 驱动为例,完整框架不只有最后三行,还包括:
struct i2c_driver:描述驱动名称、匹配表、probe()、remove()等回调;- I²C ID 与 OF 匹配表:把驱动同板级描述出来的设备关联起来;
probe()/remove():在绑定和解绑时申请、注册或释放资源;struct file_operations:把用户空间的open/read/release映射到课程驱动实现;- 字符设备、class 与 device:提供设备号、sysfs 表示及
/dev节点创建所需信息; - 模块 init/exit:向 I²C 核心注册或注销整个驱动。
这些层次相互配合:模块成功加载不等于设备匹配成功,设备匹配成功也不等于应用读取的数据一定正确。
1.7.6 例题1: 驱动代码框架
初学者可以按以下顺序阅读本项目驱动:
- 从
module_init()找到模块初始化函数,确认它向哪个内核子系统注册; - 找到
struct i2c_driver,看probe/remove和匹配表指向哪里; - 对照设备树的
compatible,确认设备与驱动的匹配条件; - 进入
probe(),记录设备号、cdev、class、device 等资源的申请顺序; - 查看
file_operations,建立应用系统调用与驱动回调之间的对应关系; - 顺着
read()等回调继续追踪到i2c_transfer(),理解数据如何到达硬件; - 反向检查
remove()和模块退出路径是否按生命周期释放资源; - 最后结合
dmesg、lsmod、/sys、/dev和应用实测,验证每一层是否真的成功。
本节只要求先建立这张地图。下一节分析 probe()、匹配表或 file_operations 时,再逐个展开细节;不能仅凭看到了 module_init() 或 /dev 节点,就跳过中间的注册、匹配和回调链。