Linux Platform设备驱动分析3(基于Linux6.6)---应用解析介绍
一、platform 驱动的工作过程
platform模型驱动编程,需要实现platform_device(设备)与platform_driver(驱动)在platform(虚拟总线)上的注册、匹配,相互绑定,然后再做为一个普通的字符设备进行相应的应用,总之如果编写的是基于字符设备的platform驱动,在遵循并实现platform总线上驱动与设备的特定接口的情况下,最核心的还是字符设备的核心结构:cdev、 file_operations(他包含的操作函数接口)、dev_t(设备号)、设备文件(/dev)等,因为用platform机制编写的字符驱动,它的本质是字符驱动。
platform 驱动只是在字符设备驱动外套一层platform_driver 的外壳。
1.1、platform驱动模型三个对象:platform总线、platform设备、platform驱动
在 Linux 内核中,platform
驱动模型由三个主要的对象组成:platform 总线(platform bus)、platform 设备(platform device)、platform 驱动(platform driver)。这三个对象共同协作,形成了平台设备驱动的工作模型。下面详细介绍这三个对象及其各自的作用和相互关系:
1. platform 总线(platform bus)
-
定义:
platform
总线是一种特殊的总线类型,用于连接那些不通过标准总线(如 PCI、I2C、SPI 等)连接到 CPU 或处理器的设备。通常这些设备直接连接到芯片或系统的某个地方,或是通过某个 SoC(系统级芯片)与处理器通信。 -
作用:
platform
总线是设备和驱动之间的媒介。设备通过platform
总线与驱动进行绑定,驱动则通过该总线与设备进行交互。通常,platform
总线是内核中的一个虚拟总线,设备通过它来注册到内核,而驱动则通过该总线来管理硬件设备。 -
特点:
platform
总线不依赖于硬件总线协议,而是直接映射到硬件设备(通常是嵌入式平台中的 SoC)。- 与其他总线(如 PCI、USB)不同,
platform
总线没有具体的硬件接口,而是用于描述与硬件直接相关的设备。
-
平台总线的注册:
platform
总线在内核中通常是自动注册的,不需要显式地创建。内核的platform_driver
和platform_device
使用此总线来进行设备和驱动的管理。
2. platform 设备(platform device)
-
定义:
platform device
是一种直接连接到系统处理器(通常是 SoC 或其他芯片)的设备。它没有标准总线协议(如 PCI、I2C 等)来管理硬件,而是通过platform
总线和驱动直接交互。 -
作用:
platform device
表示实际的硬件设备,在内核中用结构体platform_device
来表示。它是一个硬件设备实例,通常由嵌入式硬件平台或者开发板上的某个设备构成。 -
平台设备的创建:
- 可以通过手动创建(例如
platform_device_register()
)或通过设备树(Device Tree)来描述设备。 - 每个
platform_device
结构体包含了该设备的资源、IO 地址、硬件信息、设备树描述等信息。
- 可以通过手动创建(例如
-
平台设备的常见字段:
name
: 设备的名字。id
: 设备的 ID(通常用于标识多个相同类型的设备)。dev
: 指向device
结构体的指针,代表与设备相关的通用信息。num_resources
: 设备使用的资源数量(如内存、IO端口、中断等)。resource
: 设备的资源信息,如内存区、IO端口、IRQ等。
示例代码(设备注册):
-
struct platform_device *pdev; pdev = platform_device_register_simple("my_platform_device", -1, NULL, 0);
-
与驱动的绑定:
platform_device
会通过platform_driver
与具体的硬件驱动进行绑定,从而使驱动能够管理设备并进行操作。
3. platform 驱动(platform driver)
-
定义:
platform_driver
是用于管理platform_device
的驱动,通常用于嵌入式系统或者没有标准总线的设备。它通过platform_driver
结构体来定义驱动的功能,并与特定的设备进行匹配和管理。 -
作用:
platform_driver
用于描述设备驱动与平台设备之间的交互。驱动程序在内核加载时,会通过设备树或硬编码的方式与特定的platform_device
进行绑定。当设备被注册到系统时,驱动会根据设备类型或设备树的匹配信息来初始化硬件设备,配置资源和中断等。 -
平台驱动的常见字段:
driver.name
: 驱动名称,用于驱动与设备的匹配。driver.of_match_table
: 设备树匹配表,用于根据设备树中的信息选择合适的驱动。probe
: 设备初始化函数,当设备和驱动匹配时,会调用此函数来初始化设备。remove
: 卸载函数,当设备被移除时,会调用此函数来清理资源。shutdown
: 关机时调用的函数。
-
平台驱动的注册:
platform_driver_register()
用于将驱动注册到内核,使其能够管理与之匹配的设备。示例代码(平台驱动注册):
-
static struct platform_driver my_driver = { .driver = { .name = "my_platform_device", // 驱动名称 .of_match_table = my_driver_of_match, // 设备树匹配表 }, .probe = my_driver_probe, // 设备初始化函数 .remove = my_driver_remove, // 设备卸载函数 }; platform_driver_register(&my_driver);
-
与设备的匹配:
platform_driver
通过设备树中的compatible
字段与设备进行匹配,或者通过设备的名称(name
)进行匹配。如果匹配成功,内核将调用驱动的probe
函数来初始化设备。
4. 三个对象的关系与协作
这三个对象是 Linux 平台驱动模型中的核心组成部分,彼此之间有密切的协作关系。
-
平台总线(platform bus)提供了设备和驱动之间的通信通道。设备通过
platform
总线注册到系统,而驱动通过该总线与设备绑定。 -
平台设备(platform device)是实际硬件设备的代表,它在内核中描述了硬件设备的信息,并通过
platform
总线与驱动连接。设备可以通过设备树或者手动注册的方式被创建。 -
平台驱动(platform driver)用于管理
platform_device
设备,通常用于不需要标准总线的设备,如直接连接到处理器或 SoC 的外设。驱动通过设备名称或者设备树信息与设备匹配,一旦匹配成功,就会调用驱动的probe
函数进行设备的初始化工作。
1.2、具体platform驱动的工作过程是什么呢?
平台驱动的工作流程可以分为以下几个主要步骤:
1.驱动注册(Platform Driver Registration)
平台驱动通过 platform_driver_register()
来注册到内核。这个过程告诉内核该驱动的相关信息(包括设备匹配信息、驱动操作函数等),内核将根据设备树中描述的设备信息来决定是否加载该驱动。
驱动结构体定义如下:
static struct platform_driver my_driver = {
.driver = {
.name = "my_platform_device", // 驱动名称
.of_match_table = my_driver_of_match, // 设备树匹配表
},
.probe = my_driver_probe, // 设备初始化函数
.remove = my_driver_remove, // 设备卸载函数
};
其中,.probe
函数用于设备的初始化,.remove
函数则用于设备的卸载。of_match_table
用于设备树与驱动匹配。
2.设备匹配(Device Matching)
平台设备通常会在设备树中进行描述。设备树通过 compatible
属性与驱动程序中的 of_device_id
匹配数组进行匹配,从而确定是否加载该驱动。
例如:
设备树中的描述:
dts
my_platform_device {
compatible = "my,platform-device";
reg = <0x1000 0x100>;
interrupt-parent = <&intc>;
interrupts = <5>;
};
驱动中的匹配表:
static const struct of_device_id my_driver_of_match[] = {
{ .compatible = "my,platform-device", },
{},
};
当内核解析设备树并找到与 compatible
匹配的设备节点时,会调用与之匹配的驱动的 probe
函数。
3.Probe 函数的调用(设备初始化)
probe
函数用于初始化设备,它会在设备和驱动匹配成功后被调用。probe
函数中通常会完成设备的初始化工作,例如:
- 分配资源(内存、IO端口、GPIO等)。
- 注册设备。
- 初始化硬件。
static int my_driver_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
int ret;
// 读取设备树信息
int irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(dev, "Failed to get IRQ\n");
return irq;
}
// 请求中断
ret = devm_request_irq(dev, irq, my_irq_handler, 0, dev_name(dev), dev);
if (ret) {
dev_err(dev, "Failed to request IRQ\n");
return ret;
}
// 初始化硬件或注册设备
// 例如,配置GPIO、I2C、SPI等
dev_info(dev, "Device initialized successfully\n");
return 0;
}
在probe
函数中,通常会执行以下操作:
- 获取设备资源(如中断、IO端口、内存区域等)。
- 初始化硬件或外围设备。
- 通过调用
devm_*
API 函数来自动管理资源的分配和释放。
4.设备使用和操作
一旦设备初始化完成,用户空间或者内核空间的其他部分就可以开始与设备进行交互。例如,GPIO、I2C、SPI等接口会被初始化并配置,以便外部设备可以通过它们与系统进行通信。
5.Remove 函数的调用(设备卸载)
当设备被移除时,内核会调用 remove
函数。remove
函数用于清理设备资源,如释放内存、取消注册的中断、关闭硬件等。
static int my_driver_remove(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
// 清理资源
devm_free_irq(dev, platform_get_irq(pdev, 0), dev);
// 其他资源的清理,例如释放GPIO、关闭设备等
dev_info(dev, "Device removed successfully\n");
return 0;
}
6.驱动卸载(Platform Driver Unregistration)
当设备被卸载时,platform_driver_unregister()
函数会被调用,驱动会从内核中注销,内核将不再使用该驱动来管理硬件。
二、实现platform 驱动与设备的详细过程
1、思考问题?
在分析platform 之前,可以先思考一下下面的问题:
a -- 为什么要用 platform 驱动?不用platform驱动可以吗?
b -- 设备驱动中引入platform 概念有什么好处?
2、platform_device 结构体 VS platform_driver 结构体
这两个结构体分别描述了设备和驱动,二者有什么关系呢?先看一下具体结构体对比
设备(硬件部分):中断号,寄存器,DMA等 platform_device 结构体 |
驱动(软件部分) platform_driver 结构体 |
struct platform_device { const char *name; 名字 int id; bool id_auto; struct device dev; 硬件模块必须包含该结构体 u32 num_resources; 资源个数 struct resource *resource; 资源 人脉 const struct platform_device_id * id_entry; /* arch specific additions */ struct pdev_archdata archdata; }; |
struct platform_driver { int (* probe )(struct platform_device *); 硬件和软件匹配成功之后调用该函数 int (* remove)(struct platform_device *); 硬件卸载了调用该函数 void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*resume)(struct platform_device *); struct device_driver driver;内核里所有的驱动程序必须包含该结构体 const struct platform_device_id * id_table; 八字 }; |
设备实例: static struct platform_device hello_device= { .name = "bigbang", .id = -1, .dev.release = hello_release, }; |
驱动实例: static struct platform_driver hello_driver= { .driver.name = "bigbang", .probe = hello_probe, .remove = hello_remove, }; |
前面提到,实现platform模型的过程就是总线对设备和驱动的匹配过程 。
platform
模型的匹配过程简化流程图
+------------------------+
| 初始化设备树或平台设备 |<--------------------------------------------------+
+------------------------+ |
| |
v v
+----------------------------+ +-------------------------+
| platform_device 注册 | | platform_driver 注册 |
| (设备信息到内核) | | (驱动信息到内核) |
+----------------------------+ +-------------------------+
| |
v v
+------------------------+ +---------------------------+
| platform_bus 总线 | | platform 驱动匹配过程 |
| 创建并管理设备与驱动 | | (设备树描述与驱动名称匹配) |
+------------------------+ +---------------------------+
| |
v v
+------------------------+ +-------------------------+
| 设备与驱动匹配 | <----------------------------------| 驱动与设备绑定 |
| (平台设备与驱动匹配) | | (驱动初始化与设备关联) |
+------------------------+ +-------------------------+
| |
v v
+-------------------------+ +---------------------------+
| 注册驱动程序并启动 | <---------------------------------| 驱动初始化 |
| (驱动加载,设备开始工作) | | (驱动开始控制设备) |
+-------------------------+ +---------------------------+
详细说明每一步骤
(1)初始化设备树或平台设备
设备树(Device Tree)是描述硬件平台的关键数据结构。它包含了关于设备的基本信息,如设备类型、配置和连接等。设备树的解析通常发生在 Linux 内核启动时,系统会扫描设备树并提取设备信息,或者平台设备通过代码注册。
(2)platform_device 注册
platform_device
是平台设备的描述结构体,在此步骤中,设备通过 platform_device_register()
函数注册到内核中。它包含设备的资源信息(如内存区域、I/O 区域和中断),以及设备的名称和其他设备特性。
(3)platform_driver 注册
platform_driver
是一个用于操作平台设备的驱动描述结构体。在此步骤中,驱动通过 platform_driver_register()
函数注册到内核中。驱动结构体定义了驱动程序的一些操作函数(如 probe
、remove
等),并且该结构体会与平台设备进行匹配。
(4)platform 总线管理设备和驱动
内核通过 platform_bus
总线来管理和协调平台设备和驱动程序。platform_bus
本身并不直接对设备进行处理,而是充当平台设备和驱动之间的桥梁,负责设备与驱动的匹配与管理。
(5)设备与驱动匹配
在 platform
模型中,设备和驱动的匹配主要通过设备的名称、ID 或设备树中的 compatible
字段来实现。内核在 platform_bus
总线上遍历所有已注册的设备和驱动,尝试找到匹配的设备和驱动。驱动匹配的标准通常是设备树中的 compatible
字段与驱动的名称或ID匹配。
(6)驱动与设备绑定
匹配成功后,内核会通过 probe
函数将驱动与设备绑定。这时,驱动的初始化函数(probe
)会被调用,驱动程序可以在这里初始化设备并配置硬件资源(如 I/O 区域、中断、DMA 等)。
(7)注册驱动程序并启动
驱动程序被成功加载并开始控制设备。此时,设备开始工作,驱动程序将负责设备的后续操作,包括 I/O 请求、设备状态管理等。
3、设备资源结构体
在struct platform_device 结构体中有一重要成员 struct resource *resource
struct resource {
resource_size_t start; 资源起始地址
resource_size_t end; 资源结束地址
const char *name;
unsigned long flags; 区分是资源什么类型的
struct resource *parent, *sibling, *child;
};
#define IORESOURCE_MEM 0x00000200
#define IORESOURCE_IRQ 0x00000400
lags 指资源类型,我们常用的是 IORESOURCE_MEM、IORESOURCE_IRQ 这两种。start 和 end 的含义会随着 flags而变更,如
a -- flags为IORESOURCE_MEM 时,start 、end 分别表示该platform_device占据的内存的开始地址和结束值;
b -- flags为 IORESOURCE_IRQ 时,start 、end 分别表示该platform_device使用的中断号的开始地址和结束值;
下面看一个实例:
static struct resource beep_resource[] =
{
[0] = {
.start = 0x114000a0,
.end = 0x114000a0+0x4,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = 0x139D0000,
.end = 0x139D0000+0x14,
.flags = IORESOURCE_MEM,
},
};
4、将字符设备添加到 platform的driver中
platform 驱动只是在字符设备驱动外套一层platform_driver 的外壳,下面看一下添加的过程:
static struct file_operations hello_ops=
{
.open = hello_open,
.release = hello_release,
.unlocked_ioctl = hello_ioctl,
};
static int hello_remove(struct platform_device *pdev)
{
注销分配的各种资源
}
static int hello_probe(struct platform_device *pdev)
{
1.申请设备号
2.cdev初始化注册,&hello_ops
3.从pdev读出硬件资源
4.对硬件资源初始化,ioremap,request_irq( )
}
static int hello_init(void)
{
只注册 platform_driver
}
static void hello_exit(void)
{
只注销 platform_driver
}
可以看到,模块加载和卸载函数仅仅通过paltform_driver_register()、paltform_driver_unregister() 函数进行 platform_driver 的注册和注销,而原先注册和注销字符设备的工作已经被移交到 platform_driver 的 probe() 和 remove() 成员函数中。
5、platform是如何匹配device和driver
这时就该总线出场了,系统为platform总线定义了一个bus_type 的实例platform_bus_type,其定义如下:
struct bus_type platform_bus_type = {
.name = "platform",
.dev_groups = platform_dev_groups,
.match = platform_match,
.uevent = platform_uevent,
.pm = &platform_dev_pm_ops,
};
其又是怎样工作的呢?
__platform_driver_register()
{
drv->driver.bus = &platform_bus_type;
}
在 platform_bus_type 中调用 了platform_match:
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
匹配设备树信息,如果有设备树,就调用 of_driver_match_device() 函数进行匹配
if (of_driver_match_device(dev, drv))
return 1;
匹配id_table
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
最基本匹配规则
return (strcmp(pdev->name, drv->name) == 0);
}
6、解决问题
现在可以回答这两个问题了
a -- 为什么要用 platform 驱动?不用platform驱动可以吗?
b -- 设备驱动中引入platform 概念有什么好处?
引入platform模型符合Linux 设备模型 —— 总线、设备、驱动,设备模型中配套的sysfs节点都可以用,方便我们的开发;当然你也可以选择不用,不过就失去了一些platform带来的便利;
设备驱动中引入platform 概念,隔离BSP和驱动。在BSP中定义platform设备和设备使用的资源、设备的具体匹配信息,而在驱动中,只需要通过API去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。
三、举例应用
这是一个蜂鸣器的驱动, 下面来看一下,套上platform 外壳后的程序:
1、device.c
#include <linux/module.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/ioport.h>
static struct resource beep_resource[] =
{
[0] ={
.start = 0x114000a0,
.end = 0x114000a0 + 0x4,
.flags = IORESOURCE_MEM,
},
[1] ={
.start = 0x139D0000,
.end = 0x139D0000 + 0x14,
.flags = IORESOURCE_MEM,
}
};
static void hello_release(struct device *dev)
{
printk("hello_release\n");
return ;
}
static struct platform_device hello_device=
{
.name = "bigbang",
.id = -1,
.dev.release = hello_release,
.num_resources = ARRAY_SIZE(beep_resource),
.resource = beep_resource,
};
static int hello_init(void)
{
printk("hello_init");
return platform_device_register(&hello_device);
}
static void hello_exit(void)
{
printk("hello_exit");
platform_device_unregister(&hello_device);
return;
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
2、driver.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <asm/io.h>
static int major = 250;
static int minor=0;
static dev_t devno;
static struct class *cls;
static struct device *test_device;
#define TCFG0 0x0000
#define TCFG1 0x0004
#define TCON 0x0008
#define TCNTB0 0x000C
#define TCMPB0 0x0010
static unsigned int *gpd0con;
static void *timer_base;
#define MAGIC_NUMBER 'k'
#define BEEP_ON _IO(MAGIC_NUMBER ,0)
#define BEEP_OFF _IO(MAGIC_NUMBER ,1)
#define BEEP_FREQ _IO(MAGIC_NUMBER ,2)
static void fs4412_beep_init(void)
{
writel ((readl(gpd0con)&~(0xf<<0)) | (0x2<<0),gpd0con);
writel ((readl(timer_base +TCFG0 )&~(0xff<<0)) | (0xff <<0),timer_base +TCFG0);
writel ((readl(timer_base +TCFG1 )&~(0xf<<0)) | (0x2 <<0),timer_base +TCFG1 );
writel (500, timer_base +TCNTB0 );
writel (250, timer_base +TCMPB0 );
writel ((readl(timer_base +TCON )&~(0xf<<0)) | (0x2 <<0),timer_base +TCON );
}
void fs4412_beep_on(void)
{
writel ((readl(timer_base +TCON )&~(0xf<<0)) | (0x9 <<0),timer_base +TCON );
}
void fs4412_beep_off(void)
{
writel ((readl(timer_base +TCON )&~(0xf<<0)) | (0x0 <<0),timer_base +TCON );
}
static void beep_unmap(void)
{
iounmap(gpd0con);
iounmap(timer_base);
}
static int beep_open (struct inode *inode, struct file *filep)
{
fs4412_beep_on();
return 0;
}
static int beep_release(struct inode *inode, struct file *filep)
{
fs4412_beep_off();
return 0;
}
#define BEPP_IN_FREQ 100000
static void beep_freq(unsigned long arg)
{
writel(BEPP_IN_FREQ/arg, timer_base +TCNTB0 );
writel(BEPP_IN_FREQ/(2*arg), timer_base +TCMPB0 );
}
static long beep_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
{
switch(cmd)
{
case BEEP_ON:
fs4412_beep_on();
break;
case BEEP_OFF:
fs4412_beep_off();
break;
case BEEP_FREQ:
beep_freq( arg );
break;
default :
return -EINVAL;
}
return 0;
}
static struct file_operations beep_ops=
{
.open = beep_open,
.release = beep_release,
.unlocked_ioctl = beep_ioctl,
};
static int beep_probe(struct platform_device *pdev)
{
int ret;
printk("match ok!");
gpd0con = ioremap(pdev->resource[0].start,pdev->resource[0].end - pdev->resource[0].start);
timer_base = ioremap(pdev->resource[1].start, pdev->resource[1].end - pdev->resource[1].start);
devno = MKDEV(major,minor);
ret = register_chrdev(major,"beep",&beep_ops);
cls = class_create(THIS_MODULE, "myclass");
if(IS_ERR(cls))
{
unregister_chrdev(major,"beep");
return -EBUSY;
}
test_device = device_create(cls,NULL,devno,NULL,"beep");//mknod /dev/hello
if(IS_ERR(test_device))
{
class_destroy(cls);
unregister_chrdev(major,"beep");
return -EBUSY;
}
fs4412_beep_init();
return 0;
}
static int beep_remove(struct platform_device *pdev)
{
beep_unmap();
device_destroy(cls,devno);
class_destroy(cls);
unregister_chrdev(major,"beep");
return 0;
}
static struct platform_driver beep_driver=
{
.driver.name = "bigbang",
.probe = beep_probe,
.remove = beep_remove,
};
static int beep_init(void)
{
printk("beep_init");
return platform_driver_register(&beep_driver);
}
static void beep_exit(void)
{
printk("beep_exit");
platform_driver_unregister(&beep_driver);
return;
}
MODULE_LICENSE("GPL");
module_init(beep_init);
module_exit(beep_exit);
3、makefile
ifneq ($(KERNELRELEASE),)
obj-m:=device.o driver.o
$(info "2nd")
else
#KDIR := /lib/modules/$(shell uname -r)/build
KDIR := /home/fs/linux/linux-3.14-fs4412
PWD:=$(shell pwd)
all:
$(info "1st")
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order
endif
4、test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
main()
{
int fd,i,lednum;
fd = open("/dev/beep",O_RDWR);
if(fd<0)
{
perror("open fail \n");
return ;
}
sleep(10);
close(fd);
}