Linux 驱动学习:基于 Platform 架构编写驱动
之前编写 LED 字符设备驱动时,需要自己申请设备号、创建设备节点,并且将寄存器地址直接写在驱动代码中。这种方式虽然可以实现功能,但是不方便管理和移植。
为了让驱动结构更加清晰,Linux 引入了 platform 总线模型,并结合设备树、pinctrl 子系统和 gpio 子系统,实现设备信息与驱动代码分离。
一、混杂设备驱动(misc)
之前编写字符设备驱动时,需要通过 alloc_chrdev_region() 申请设备号,再通过 cdev 注册字符设备。
实际上,Linux 已经为一些通用设备预留了主设备号。例如,混杂设备(misc)使用的主设备号为 10,蜂鸣器、LED 等简单设备可以根据实际情况使用混杂设备框架。
使用 misc 框架可以省去部分字符设备注册流程,主要通过以下结构体和函数完成注册:
-
struct miscdevice:描述混杂设备。 -
misc_register():注册混杂设备。 -
misc_deregister():注销混杂设备。
不过,misc 只是简化字符设备注册的一种方式,并不是所有 LED、蜂鸣器都必须使用 misc 驱动。
二、设备树驱动
之前编写驱动时,外设的寄存器地址、GPIO 编号等信息可能直接写在驱动代码中。如果更换开发板或者引脚,这些代码就需要修改,不利于驱动移植。
因此,引入了设备树(Device Tree)。
设备树主要负责描述硬件信息,例如:
-
外设使用的寄存器地址。
-
GPIO 引脚信息。
-
中断信息。
-
时钟、电源等硬件资源。
驱动代码则负责获取这些信息,并实现设备的初始化和操作方法。
以 IMX6ULL 为例,设备树源码通常位于:
arch/arm/boot/dts/imx6ull-alientek-emmc.dts
修改设备树后,需要重新编译对应的 DTB 文件,并确保开发板实际加载了更新后的设备树。
需要注意,设备树并不是自动让驱动适配所有外设。驱动还需要通过设备树匹配相应的设备,并正确解析其中的硬件资源。
三、Platform 驱动框架
Platform 是 Linux 中用于管理片上外设的一种总线模型,常用于 GPIO 控制器、串口、LED 等 SoC 内部设备。
它的核心是将设备和驱动分开管理。
1. 设备与驱动分离
-
设备端(platform_device):描述设备的硬件资源。
-
驱动端(platform_driver):实现设备的操作方法。
在使用设备树的情况下,设备信息通常定义在 DTS 文件中,由内核将设备树节点转换为相应的 platform 设备。
驱动通过 compatible 属性与设备树节点进行匹配。匹配成功后,内核调用驱动中的 probe() 函数,完成设备初始化。
当设备被移除时,内核可以调用 remove() 函数执行清理工作。
常用成员和函数:
-
struct platform_driver:定义 platform 驱动。 -
probe():设备匹配成功后执行。 -
remove():设备移除时执行。 -
platform_driver_register():注册 platform 驱动。 -
platform_driver_unregister():注销 platform 驱动。
2. Platform 驱动的基本流程
-
在设备树中描述硬件信息。
-
在驱动中编写
platform_driver。 -
设置驱动的
of_match_table,通过compatible匹配设备树。 -
注册 platform 驱动。
-
设备匹配成功后,执行
probe()。 -
在
probe()中获取 GPIO、寄存器等资源,并完成设备初始化。 -
编写文件操作接口,实现用户空间与驱动之间的交互。
如果使用 misc 框架,还可以在 probe() 中注册 misc 设备,在 remove() 中注销。
四、驱动分层思想
Linux 驱动开发不仅强调设备与驱动分离,也强调分层设计。
对于 GPIO 控制的 LED,可以使用 pinctrl 和 GPIO 两个子系统,避免直接操作底层寄存器。
1. Pinctrl 子系统
pinctrl 主要负责引脚复用和电气属性配置,例如:
-
设置引脚的复用功能。
-
配置上下拉。
-
设置驱动能力等电气属性。
以 IMX6ULL 为例,在:
arch/arm/boot/dts/imx6ull-alientek-emmc.dts
文件的 iomuxc 节点中,可以添加引脚配置方案。
例如,下面是一个简化的 GPIO 引脚配置示例,具体引脚复用值需要根据实际硬件和芯片手册确定:
&iomuxc {
pinctrl_led: ledgrp {
fsl,pins = <
/* 在这里填写实际的引脚复用配置 */
>;
};
};
这里的 pinctrl_led 是引脚配置节点的标签,后续可以通过 pinctrl-0 引用它。
2. GPIO 子系统
GPIO 子系统主要负责 GPIO 的输入输出控制。
例如:
-
将 GPIO 配置为输入或输出。
-
读取 GPIO 输入电平。
-
设置 GPIO 输出高低电平。
在设备树中,可以将引脚配置与 GPIO 设备信息关联起来:
led {
compatible = "alientek,myled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led>;
/* 根据实际硬件填写 GPIO 控制器和引脚编号 */
led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
status = "okay";
};
以上代码仅为结构示例,GPIO 控制器、引脚编号以及宏的定义必须根据实际硬件进行调整。使用 GPIO_ACTIVE_LOW 表示该 GPIO 采用低电平有效的逻辑。
驱动中可以通过 GPIO 描述符接口获取并控制 GPIO,例如 devm_gpiod_get() 和 gpiod_set_value_cansleep()。
3. Pinctrl 和 GPIO 的区别
简单理解:
-
pinctrl:负责配置引脚应该使用什么功能,以及相关电气属性。
-
GPIO:负责控制引脚的输入输出状态。
例如,LED 所连接的引脚需要先配置为 GPIO 功能,再由 GPIO 子系统控制输出高低电平。
在使用设备树和标准 GPIO 接口时,通常不需要在驱动中直接操作 IOMUXC、GPIO 的物理寄存器地址。
五、总结
学习 Linux 驱动时,可以将整个过程理解为三个方面:
-
设备树:描述硬件信息,解决硬件信息写死在驱动中的问题。
-
Platform 框架 :实现设备与驱动分离,通过匹配机制调用
probe()。 -
pinctrl 和 GPIO 子系统:分别负责引脚配置和 GPIO 输入输出控制。
在此基础上,再结合 misc 或其他字符设备框架,就可以完成从设备树解析、硬件初始化到用户空间访问的完整驱动流程。
这种方式能够减少重复代码,也更方便驱动移植和维护。