嵌入式Linux Platform 设备驱动 设备驱动分层和分离

Linux 设备驱动分层和分离
在面向对象的程序设计中, 可以为某一类相似的事物定义一个基类, 而具体的事物可以继承这个基类中的函数。 如果对于继承的这个事物而言, 其某函数的实现与基类一致, 那它就可以直接继承基类的函数;相反, 它可以重载之。 这种面向对象的设计思想极大地提高了代码的可重用能力, 是对现实世界事物间关系的一种良好呈现。
Linux 内核完全由 C 语言和汇编语言写成, 但是却频繁用到了面向对象的设计思想。 在设备驱动方面,往往为同类的设备设计了一个框架, 而框架中的核心层则实现了该设备通用的一些功能。 同样的, 如果具体的设备不想使用核心层的函数, 它可以重载之。 举个例子:

return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{ 
    if(bottom_dev->funca)
       return bottom_dev->funca(param1, param2);
/* 核心层通用的 funca 代码 */
...
}

上述 core_funca 的实现中, 会检查底层设备是否重载了 funca(), 如果重载了, 就调用底层的代码, 否则, 直接使用通用层的。 这样做的好处是, 核心层的代码可以处理绝大多数该类设备的 funca()对应的功能,只有少数特殊设备需要重新实现 funca()。
再看一个例子:

copyreturn_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{ 
/*通用的步骤代码 A */
...
bottom_dev->funca_ops1();
/*通用的步骤代码 B */
...
bottom_dev->funca_ops2();
/*通用的步骤代码 C */
...
bottom_dev->funca_ops3();
}

上述代码假定为了实现 funca(), 对于同类设备而言, 操作流程一致, 都要经过“通用代码 A、 底层 ops1、通用代码 B、 底层 ops2、 通用代码 C、 底层 ops3” 这几步, 分层设计明显带来的好处是, 对于通用代码 A、B、 C, 具体的底层驱动不需要再实现, 而仅仅只关心其底层的操作 ops1、 ops2、 ops3。 这样的分层化设计在 Linux 的 input、 RTC、 MTD、 I2 C、 SPI、 TTY、 USB 等诸多设备驱动类型中屡见不鲜。

主机驱动和外设驱动分离思想
在 Linux 设备驱动框架的设计中, 除了有分层设计实现以外, 还有分隔的思想。 举一个简单的例子,假设我们要通过 SPI 总线访问某外设, 在这个访问过程中, 要通过操作 CPU XXX 上的 SPI 控制器的寄存器来达到访问 SPI 外设 YYY 的目的, 最简单的方法是:

copyreturn_type xxx_write_spi_yyy(...)
{ x
xx_write_spi_host_ctrl_reg(ctrl);
xxx_ write_spi_host_data_reg(buf);
while(!(xxx_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
...
}

如果按照这种方式来设计驱动, 结果是对于任何一个 SPI 外设来讲, 它的驱动代码都是 CPU 相关的。也就是说, 当然用在 CPU XXX 上的时候, 它访问 XXX 的 SPI 主机控制寄存器, 当用在 XXX1 的时候, 它访问 XXX1 的 SPI 主机控制寄存器:

return_type xxx1_write_spi_yyy(...)
{ x
xx1_write_spi_host_ctrl_reg(ctrl);
xxx1_ write_spi_host_data_reg(buf);
while(!(xxx1_spi_host_status_reg()&SPI_DATA_TRANSFER_DONE));
...
}

这显然是不能接受的, 因为这意味着外设 YYY 用在不同的 CPU XXX 和 XXX1 上的时候需要不同的驱动。 那么, 我们可以用如图的思想对主机控制器驱动和外设驱动进行分离。 这样的结构是, 外设 a、 b、 c 的驱动与主机控制器 A、 B、 C 的驱动不相关, 主机控制器驱动不关心外设, 而外设驱动也不关心主机, 外设只是访问核心层的通用的 API 进行数据传输, 主机和外设之间可以进行任意的组合。
如果我们不进行上图的主机和外设分离, 外设 a、 b、 c 和主机 A、 B、 C 进行组合的时候, 需要 9 个不同的驱动。 设想一共有 m 个主机控制器, n 个外设, 分离的结果是需要 m+n 个驱动, 不分离则需要 m*n个驱动。 Linux SPI、 I2C、 USB、 ASoC(ALSA SoC)等子系统都典型地利用了这种分离的设计思想。

Platform 平台驱动模型
在 Linux 2.6 以后的设备驱动模型中, 需关心总线、 设备和驱动这 3 个实体, 总线将设备和驱动绑定。在系统每注册一个设备的时候, 会寻找与之匹配的驱动; 相反的, 在系统每注册一个驱动的时候, 会寻找与之匹配的设备, 而匹配由总线完成。
一个现实的 Linux 设备和驱动通常都需要挂接在一种总线上, 对于本身依附于 PCI、 USB、 I2C、 SPI 等的设备而言, 这自然不是问题, 但是在嵌入式系统里面, 在 SoC 系统中集成的独立外设控制器、 挂接在 SoC内存空间的外设等却不依附于此类总线。 基于这一背景, Linux 发明了一种虚拟的总线, 称为 platform 总线, 相应的设备称为 platform_device, 而驱动成为 platform_driver。
显然, 这样做的好处是, 实现了此类设备和驱动的分离, 增强设备驱动的可移植性。 平台总线模型也叫 platform 总线模型。 是 Linux 内核虚拟出来的一条总线, 不是真实的导线。 平台总线模型就是把原来的驱动 C 文件给分成了俩个 C 文件, 一个是 device.c, 一个是 driver.c 把稳定不变的放在 driver.c 里面, 需要变得就放在了 device.c 里面。
我们前几章内容给大家讲了杂项设备和字符设备驱动文件, 这俩种驱动文件都有一个特点, 它把驱动和设备都写在一个驱动文件里面了, 但是如果相同的设备很多, 就会出现一系列的问题。 比如说我有一个硬件平台, 这个硬件平台上有很多的模块, 比如说有 500 个模块, 这 500 个模块上都用到了 led 灯, 如果说我用杂项设备来写, 虽然用杂项设备比用字符设备的代码量要少, 那么我是不是也要写 500 份这样的代码然后生成设备节点, 供我们上层应用控制在不同模块上的 led 灯呢? 那么写 500 份代码就带来俩个问题, 第一个是你写了大量重复性的代码。 第二个是代码的重用性不是很好, 这些驱动我从 NXP 的平台上移植到三星的平台上, 那么要一个个改我们的驱动, 但是实际上每个驱动你改的东西并不多, 只改了相应的和硬件相关的部分。 平台总线模型就很好地解决了这俩个问题。
(1) 可以提高代码的重用性
(2) 减少重复性代码。
如下图所示, 平台总线模型将设备代码和驱动代码分离, 将和硬件设备相关的都放到 device.c 文件里面,驱动部分代码都放到 driver.c 文件里面, 那么 500 个 led 的驱动有重复的代码只要写一遍就可以了。
设备, 平台总线, 驱动的关系如下图所示:

Platform 设备
 首先来看一下在 platform 平台模型下硬件设备信息如何表示和注册。
在 platform 平台下用 platform_device 这个结构体表示 platform 设备, 如果内核支持设备树的话就不用使用 platform_device 来描述设备了, 因为改用设备树去描述了 platform_device。 具体定义在内核源码/include/linux/platform_device.h 里面, 结构体内容如下:

22 struct platform_device {
23 const char *name;
24 int id;
25 bool id_auto;
26 struct device dev;
27 u32 num_resources;
28 struct resource *resource;
29
30 const struct platform_device_id *id_entry;
31 char *driver_override; /* Driver name to force a match */
32
33 /* MFD cell pointer */
34 struct mfd_cell *mfd_cell;
35
36 /* arch specific additions */
37 struct pdev_archdata archdata;
38 };

第 23 行, platform 设备的名字, 用来和 platform 驱动相匹配。 名字相同才能匹配成功。
第 24 行, ID 是用来区分如果设备名字相同的时候(通过在后面添加一个数字来代表不同的设备,因为有时候有这种需求)
第 25 行, 内置的 device 结构体。
第 27 行, 资源结构体数量。
第 28 行, 指向一个资源结构体数组。 一般包含设备信息。 Linux 内核使用 resource
结构体表示资源, resource 结构体内容如下:

18 struct resource {
19 resource_size_t start;
20 resource_size_t end;
21 const char *name;
22 unsigned long flags;
23 struct resource *parent, *sibling, *child;
24 };

start 和 end 分别表示资源的起始和终止信息, 对于内存类的资源, 就表示内存起始和终止地址, name表示资源名字, flags 表示资源类型, 可选的资源类型都定义在了文件 include/linux/ioport.h 里面, 如下所示:

29 #define IORESOURCE_BITS 0x000000ff /* Bus-specific bits */
30
31 #define IORESOURCE_TYPE_BITS 0x00001f00 /* Resource type */
32 #define IORESOURCE_IO 0x00000100 /* PCI/ISA I/O ports */
33 #define IORESOURCE_MEM 0x00000200
34 #define IORESOURCE_REG 0x00000300 /* Register offsets */
35 #define IORESOURCE_IRQ 0x00000400
36 #define IORESOURCE_DMA 0x00000800
37 #define IORESOURCE_BUS 0x00001000
......
104 /* PCI control bits. Shares IORESOURCE_BITS with above PCI ROM. */
105 #define IORESOURCE_PCI_FIXED (1<<4) /* Do not move resource */

第 30 行, 用来进行与设备驱动匹配用的 id_table 表
第 37 行, 添加自己的私有数据。
常用 flags 宏定义如下所示:

#define IORESOURCE_IO IO 的内存
#define IORESOURCE_MEM 表述一段物理内存
#define IORESOURCE_IRQ 表示中断

在不支持设备树的 Linux 内核版本中需要在通过 platform_device 结构体来描述设备信息, 然后使用platform_device_register 函数将设备信息注册到 Linux 内核中, 此函数原型如下所示:

int platform_device_register(struct platform_device *pdev)

如果内核支持设备树的话就不用使用 platform_device 来描述设备了, 因为改用设备树去描述了
platform_device。
Platform 驱动
在 Linux 内核中, 用 platform_driver 结构体表示 platform 驱动, platform_driver 结构体定义指定名称的平台设备驱动注册函数和平台设备驱动注销函数, 此结构体定义在文件 include/linux/platform_device.h中, 内容如下:

struct platform_driver {
/*当 driver 和 device 匹配成功的时候, 就会执行 probe 函数*/
int (*probe)(struct platform_device *);
/*当 driver 和 device 任意一个 remove 的时候, 就会执行这个函数*/
int (*remove)(struct platform_device *);
/*当设备收到 shutdown 命令的时候, 就会执行这个函数*/
void (*shutdown)(struct platform_device *);
/*当设备收到 suspend 命令的时候, 就会执行这个函数*/
int (*suspend)(struct platform_device *, pm_message_t state);
/*当设备收到 resume 命令的时候, 就会执行这个函数*/
int (*resume)(struct platform_device *);
// 内置的 device_driver 结构体
struct device_driver driver;
// 该设备驱动支持的设备的列表 他是通过这个指针去指向 platform_device_id 类型的数组
const struct platform_device_id *id_table;
bool prevent_deferred_probe;
};

id_table 表保存了很多 id 信息。 这些 id 信息存放着这个 platformd 驱动所支持的驱动类型。 id_table是个表(也就是数组), 每个元素的类型为 platform_device_id, platform_device_id 结构体内容如下:

struct platform_device_id {
char name[PLATFORM_NAME_SIZE];
kernel_ulong_t driver_data;
};
device_driver 结构体定义在 include/linux/device.h, device_driver 结构体内容如下:
1 struct device_driver {
2 const char *name;
3 struct bus_type *bus;
4 5
struct module *owner;
6 const char *mod_name; /* used for built-in modules */
7 8
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
9 1
0 const struct of_device_id *of_match_table;
11 const struct acpi_device_id *acpi_match_table;
12
13 int (*probe) (struct device *dev);
14 int (*remove) (struct device *dev);
15 void (*shutdown) (struct device *dev);
16 int (*suspend) (struct device *dev, pm_message_t state);
17 int (*resume) (struct device *dev);
18 const struct attribute_group **groups;
19
20 const struct dev_pm_ops *pm;
21
22 struct driver_private *p;
23 };

第 10 行, of_match_table 就是采用设备树的时候驱动使用的匹配表, 同样是数组, 每个匹配项都为of_device_id 结构体类型, 此结构体定义在文件 include/linux/mod_devicetable.h 中, 内容如下:

struct of_device_id {
char name[32];
char type[32];
char compatible[128];
const void *data;
};

compatible 成员, 在支持设备树的内核中, 就是通过设备节点的 compatible 属 性值和 of_match_table中每个项目的 compatible 成员变量进行比较, 如果有相等的就表示设备和此驱动匹配成功。
在编写 platform 驱动的时候, 首先定义一个 platform_driver 结构体变量, 然后实现结构体中的各个成员变量, 重点是实现匹配方法以及 probe 函数。 当驱动和设备匹配成功以后 probe 函数就会执行, 驱动程序具体功能的实现在 probe 函数里面编写。
platform 驱动的注册使用 platform_driver_register 函数来实现, 函数原型如下:

int platform_driver_register (struct platform_driver *driver)

参数 driver 为预先创建的 platform_driver 结构体。
通过 platform_driver_unregister 函数来卸载 platform 驱动, 函数原型如下:

void platform_driver_unregister(struct platform_driver *drv)

Platform 总线
前面讲了 platform 设备和 platform 驱动, 这就相当于把设备和驱动分离了, 那么他们要如何进行匹配呢, 这就需要 platform 总线, 前面的 platform 设备和 platform 驱动进行内核注册时, 也都是注册到总线上。 打个比方, 就好比相亲, 总线是红娘, 设备是男方, 驱动是女方。
a -- 红娘(总线) 负责男方(设备) 和女方(驱动) 的撮合;
b -- 男方(女方) 找到红娘, 说我来登记一下, 看有没有合适的姑娘(汉子) —— 设备或驱动的注册;

c -- 红娘这时候就需要看看有没有八字(二者的 name 字段) 匹配的姑娘(汉子) ——match 函数进行匹配,看 name 是否相同;
d -- 如果八字不合, 就告诉男方(女方) 没有合适的对象, 先等着 —— 设备和驱动会等待, 直到匹配成功;
e -- 终于遇到八字匹配的了, 那结婚呗, 接完婚, 男方就向女方交代, 我有多少存款, 我的房子在哪, 钱放在哪等等( struct resource *resource) , 女方说好啊, 于是去房子里拿钱, 去给男方买菜啦, 给自己买衣服、 化妆品、 首饰啊等等(int (*probe)(struct platform_device *) 匹配成功后驱动执行的第一个函数) , 当然如果男的跟小三跑了(设备卸载) , 女方也不会继续待下去的( int (*remove)(struct platform_device *))。
platform 总线模型如下图所示:

当内核中有驱动注册时, 总线就会在右侧的设备中查找, 是否有匹配的设备, 同样的, 当有设备注册到内核中时, 也会在总线左侧查找是否有匹配的驱动。 我们来看一下 platform 总线是如何定义的。
在 Linux 内核中使用 bus_type 结构体表示总线, 此结构体定义在文件 include/linux/device.h, bus_type结构体内容如下:
 

1 struct bus_type {
2 const char *name; /* 总线名字 */
3 const char *dev_name;
4 struct device *dev_root;
5 struct device_attribute *dev_attrs;
6 const struct attribute_group **bus_groups; /* 总线属性 */
7 const struct attribute_group **dev_groups; /* 设备属性 */
8 const struct attribute_group **drv_groups; /* 驱动属性 */
9 
10 int (*match)(struct device *dev, struct device_driver *drv);
11 int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
12 int (*probe)(struct device *dev);
13 int (*remove)(struct device *dev);
14 void (*shutdown)(struct device *dev);
15
16 int (*online)(struct device *dev);
17 int (*offline)(struct device *dev);
18 int (*suspend)(struct device *dev, pm_message_t state);
19 int (*resume)(struct device *dev);
20 const struct dev_pm_ops *pm;
21 const struct iommu_ops *iommu_ops;
22 struct subsys_private *p;
23 struct lock_class_key lock_key;
24}

第 10 行, match 函数, 此函数就是完成设备和驱动之间的匹配任务, , 因此每一条总线都必须实现此函数。 match 函数有两个参数: dev 和 drv, 这两个参数分别为 device 和 device_driver 类型, 也就是设备和驱动。
platform 总线是 bus_type 的一个具体实例, 定义在文件 drivers/base/platform.c, platform 总线定义如下:

1 struct bus_type platform_bus_type = {
2 .name = "platform",
3 .dev_groups = platform_dev_groups,
4 .match = platform_match,
5 .uevent = platform_uevent,
6 .pm = &platform_dev_pm_ops,
7 };

其中 platform_bus_type 结构体实例就表示 platform 总线, 其中重要的就是 platform_match 匹配函数, 用来匹配注册到 platform 总线的设备和驱动。 我们来看一下 platform 总线上的设备和驱动是如何匹配的, platform_match 函数定义在文件 drivers/base/platform.c 中, 函数内容如下所示:

1 static int platform_match(struct device *dev, struct device_driver *drv)
2 {
3 struct platform_device *pdev = to_platform_device(dev);
4 struct platform_driver *pdrv = to_platform_driver(drv);
5 6
/*When driver_override is set,only bind to the matching driver*/
7 if (pdev->driver_override)
8 return !strcmp(pdev->driver_override, drv->name);
9 1
0 /* Attempt an OF style match first */
11 if (of_driver_match_device(dev, drv))
12 return 1;
13
14 /* Then try ACPI style match */
15 if (acpi_driver_match_device(dev, drv))
16 return 1;
17
18 /* Then try to match against the id table */
19 if (pdrv->id_table)
20 return platform_match_id(pdrv->id_table, pdev) != NULL;
21
22 /* fall-back to driver name match */
23 return (strcmp(pdev->name, drv->name) == 0);
24 }

从上面代码可以看出, platform 总线上设备和驱动的匹配方法一共有四种。
第 11、 12 行, OF 类型的匹配, 也是设备树采用的匹配方式, of_driver_match_device 函数定义在文件include/linux/of_device.h 中。 device_driver 结构体(表示设备驱动)中有个名为 of_match_table 的成员变量,此成员变量保存着驱动的 compatible 匹配表, 设备树中的每个设备节点的 compatible 属性会和of_match_table 表中的所有成员比较, 查看是否有相同的条目, 如果有的话就表示设备和此驱动匹配, 设备和驱动匹配成功以后 probe 函数就会执行。 第 15、 16 行, ACPI 匹配方式。 第 19、 20 行, id_table 匹配方式, 每个 platform_driver 结构体有一个 id_table 成员变量, 顾名思义, 保存了很多 id 信息。 这些 id信息存放着这个 platformd 驱动所支持的驱动类型。 在上一小节 platform 驱动中也注意到了, 在驱动程序中需要创建platform_device_id 结构体, 并指定驱动名称信息。 第 23 行, 如果 id_table 不存在的话, 就直接比较驱动和设备的 name 字段, 是不是相等, 如果相等的话就匹配成功。

编写 probe 函数的思路
(1) 从 device.c 里面获得硬件资源, 因为我们的平台总线将驱动拆成了俩部分, 第一部分是 device.c, 另一部分是 driver.c。 那么匹配成功了之后, driver.c 要从 device.c 中获得硬件资源, 那么 driver.c 就是在 probe 函数中获得的。
( 2) 获得硬件资源之后, 就可以在 probe 函数中注册杂项/字符设备, 完善 file_operation 结构体, 并生成设备节点。
获得硬件资源有两种方法:

方法一: 直接获取, 不推荐

int beep_probe(struct platform_device *pdev){
printk("beep_probe\n");
return 0;
}

beep_probe 函数里面有一个形参 pdev, 他指向了 platform_device, 那么我们可以直接通过指针访问结构体 platform_device 的成员变量。 比如说我要访问 beep_device 中的 beep_res 中的 name, 参考如下的代码:

struct resource beep_res[] = {
    [0] ={
         .start = 0x020AC000,
         .end = 0x020AC003,
         .flags = IORESOURCE_MEM,
         .name = "GPIO5_DR",
        }
};

struct platform_device beep_device = {
      .name = "beep_test",
      .id = -1,
      .resource=beep_res,
      .num_resources =ARRAY_SIZE(beep_res),
      .dev = {
             .release = beep_release
       }
};

那么 probe 函数可以直接获取, 如下所示:

int beep_probe(struct platform_device *pdev){
    printk("beep_probe\n");
    printk("beep_res is %s\n",pdev->resource[0].name);
    return 0;
}

修改完毕后, 编译 driver.c 文件, 加载驱动模块后, 如下图所示;

方法二: 使用函数来获取资源
我们也可以使用函数来获取资源, 函数如下表所示:

函数extern struct resource *platform_get_resource(struct platform_device *,unsigned int, unsigned
int);
第一个
参数
平台设备名
第二个
参数
资源类型
第三个索引号, 资源处在同类资源的哪个位置上, 大家注意理解同类资源是指 flags 一模一样。

申请 I/O 内存
申请 I/O 内存使用 request_region 函数, 其定义在内核源码/include/linux/ioport.h 里面, 如下图所示:

#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n),(name), 0)

 第一个参数: 起始地址, 第二个参数: 长度, 第三个参数: 名字
Linux把基于I/O映射方式的I/O端口和基于内存映射方式的I/O端口资源统称为“I/O区域”(I/O Region)。I/O Region 仍然是一种 I/O 资源, 因此它仍然可以用 resource 结构类型来描述。
Linux 是以一种倒置的树形结构来管理每一类 I/O 资源(如: I/O 端口、 外设内存、 DMA 和 IRQ) 的。 每一类 I/O 资源都对应有一颗倒置的资源树, 树中的每一个节点都是一个 resource 结构, 而树的根结点 root则描述了该类资源的整个资源空间。 其实说白了, request_mem_region 函数并没有做实际性的映射工作,只是告诉内核要使用一块内存地址, 声明占有, 也方便内核管理这些资源。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木士易

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值