explorer

万丈高楼平地起,勿在浮沙筑高台

0%

[What]Linux设备驱动的软件架构思想

参考宋宝华老师的书 Linux设备驱动开发详解 ,来理解Linux设备驱动的总体思想。

在实际的linux驱动应用中,linux内核已经为不同的开发者分好了软件层次,其实很多时候开发者仅仅需要关注自己修改的部分即可。

驱动的软件架构的三大思想

总线,设备,驱动

问题

当同样的芯片位于不同的开发板,或者一个开发板具有几个相同的芯片,驱动都要兼容这些复杂的不同之处的信息,这会造成驱动代码过于臃肿.

解决

使用总线,设备和驱动模型可以将这些信息分离开来, 使得驱动只管驱动, 设备只管设备, 总线负责匹配设备和驱动,而驱动则以标准途径拿到板级信息,这样驱动就变得纯净了.

  • 设备:以目前的linux系统来说,设备就是用户以 设备树 为接口输入设备信息,而最终在内核以结构体的形式存在
  • 驱动:驱动就是用于操作数据的方法,而设备就是数据实例,这和面向对象的思想很类似
    • 一个驱动可以用于多个同类但具体参数不同的设备
  • 总线:总线通过驱动和设备中的匹配字符串(compatible)来实现二者的匹配
    • 匹配成功便会调用驱动的 probe 函数并将代表设备的数据传给驱动

驱动分层

问题

在编写驱动时,如果从头开始编写,那么需要花费的功夫比较多并且会有大量重复的代码产生(比如很多字符设备驱动的很多代码都大同小异)。

解决

将软件进行分层设计,提炼各种类似驱动的相同部分,分离不同部分。用户实现不同的部分即可,使得用户编写驱动代码的工作尽量的少.

驱动分隔

问题

当使用CPU控制器控制外设时,我们需要编写:

  1. CPU对应控制器驱动
  2. 外设驱动

如果将CPU与对应的外设驱动强耦合到一起,那外设代码就无法复用, 那也会造成内核的臃肿.

解决

主机单独实现自己的控制器驱动,外设也单独实现自己的驱动,这两个驱动通过统一的接口来进行访问。

主机控制器驱动不用关系外设,而外设驱动也不关心主机,外设只是访问核心层的通用API进行数据传输,主机和外设之间可以任意组合.

编写驱动前的思考

通过上面3大思想,可以总结出作为一个用户编写驱动时的思路:

我需要完成什么功能?

首先明确自己是要实现什么功能,以确定自己是需要实现一个驱动还是仅仅一个设备节点即可。

实现设备节点

如果设备的厂商已经为此设备提供了驱动,那么用户仅仅需要完成节点描述即可。

  • 简单设备节点
    • 先查看设备的数据手册,了解其特性
    • 然后查看设备驱动说明完成节点,一般文档位于 Documentation/devicetree/bindings/ 文件夹下
    • 对于设备驱动的使用细节,还是需要查看代码
  • 复杂设备节点
    • 访问设备官网,查看其参考linux设备节点配置及使用说明
    • 如果官网没有说明或不够详细,那么就按照 简单设备节点 的步骤来添加

实现设备驱动

如果厂商没有提供设备驱动,或需要深度定制驱动,那么就需要以源码着手。

在实现设备驱动前,需要弄清楚的是:

  1. 此设备需要使用什么协议来驱动?
  2. 协议控制器驱动是否已经实现?
  3. 此设备具体作用是什么,可以应用在哪个子系统下?
  4. 根据设备作用,来查看对应的子系统使用说明
    • 通常子系统已经实现了很多功能,我们只需要使用子系统提供的接口即可
  5. 根据设备驱动协议,来查看对应协议驱动的使用说明
    • 用户只需要使用协议中间层提供的接口即可使用对应的控制器产生时序
  6. 基于以上两点,查看设备数据手册,实现设备驱动
  7. 在设备树中增加设备节点
  8. 验证并测试

实现控制器驱动

  • 查看控制器手册了解驱动
  • 根据系统提供的统一接口实现驱动代码
  • 在设备树中增加控制器节点

实例驱动概览

platform 设备驱动(总线,设备,驱动实例)

在Linux 2.6以后的设备驱动模型中, 需要关心总线,设备和驱动这3个实体,总线将设备和驱动绑定.在系统每注册一个设备的时候,会寻找与之匹配的驱动.相反的, 在系统每注册一个驱动的时候, 会寻找与之匹配的设备,而匹配由总线完成. 对于像PCI, USB, IIC, SPI等设备而言, 总线,设备,驱动概念比较对应,而对于像 直接连接到CPU的控制器 而言,就不那么直白.所以linux发明了虚拟总线,称为 platform 总线, 相应的设备称为 platform_device, 对应于上面说的 CPU控制器数据结构描述,而驱动称为 platform_driver, 相对于上面说的 CPU控制器驱动.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct platform_device{
const char *name;
int id;
bool id_auto;
struct device dev;
u32 num_resources;
struct resource *resource;
const stuct platform_device_id *id_entry;
char *driver_overide;/*Driver name to force a match*/
/*MFD cell pointer*/
struct mfd_cell *mfd_cell;
/*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;
bool prevent_deferred_probe;
};

struct device_driver{
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name;
bool suppress_bind_attrs;
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe)(struct device *dev);
int (*remove)(struct device *dev);
void (*shutdown)(struct device *dev);
int (*suspend)(struct device *dev, pm_message_t state);
int (*resume)(struct device *dev);
const struct dev_pm_ops *pm;
struct driver_private *p;
};

与platform地位对等的 i2c_driver, spi_driver,usb_driver,pci_driver中都包含了device_driver结构体实例成员.它其实描述了各种 xxx_driver 在驱动意义上的一些共性. platform_device 和 platform_driver 的匹配是通过函数 platform_match 来匹配的,有4种可能性:

  1. 基于设备树风格的匹配
  2. 基于ACPI风格的匹配
  3. 匹配ID表(即platform_device 设备名是否出现在platform_driver的ID表内)
  4. 匹配platform_device设备名和驱动的名字

platform 设备资源和数据

设备(控制器)资源本身由 resource 结构体描述,定义代码如下:

1
2
3
4
5
6
7
struct resource{
resource__size_t start;// 资源开始值
resource_size_t end;// 资源结束值
const char *name;
unsigned long flags;//类型 IORESOURCE_IO,IORESOURCE_MEM,IORESOURCE_IRQ,IORESOURCE_DMA
struct resource *parent,*sibling,*child;
};

在具体的设备驱动中,通过以下函数来获取资源:

1
2
3
4
//设备,类型,序列
struct resource *platform_get_resource(struct platform_device *,unsigned int, unsigned int);
//设备,序列,等同于: platform_get_resource(dev, IORESOURCE_IRQ,num);
int platform_get_irq(struct platform_device *dev, unsigned int num);

除此之外,设备可能还会有一些配置信息,这些信息由 platform_data 保存,此结构的形式由每个驱动自定义. 比如在DM9000中定义信息:

1
2
3
4
5
6
7
8
9
10
11
12
static struct dm9000_plat_data dm9000_platdata={
.flags = DM9000_PLATF_16BITNOLY | DM9000_PLATF_NO_EEPROM,
};
static struct platform_device dm9000_device = {
.name = "dm9000",
.id = 0,
.num_resources = ARRAY_SIZE(dm9000_resource),
.resource = dm9000_resource,
.dev = {
.platform_data = &dm9000_platdata,
}
};

而在取得此数据时则使用: struct dm9000_plat_data *pdata = dev_get_platdata(&pdev->dev); 由上分析可知,在设备驱动中引入platform的概念至少由如下好处:

  1. 使得设备被挂接在一个总线上,符合linux 2.6以后内核的设备模型.其结果是使配套的sysfs节点和设备电源管理都成为可能.
  2. 隔离BSP和驱动.在BSP中定义platform设备和设备使用的资源,设备的具体配置信息.而在驱动中,只需要通过通用的API去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性.
  3. 让一个驱动支持多个设备实例.

驱动分层实例

设备驱动核心层的例化

  1. 重写核心层代码

Linux内核完全是由C语言和汇编语言写成,但是却频繁用到了面向对象的设计思想.在设备驱动方面,往往为同类的设备设计了一个框架,而框架中的核心层则 实现了该设备通用的一些功能.同样的, 如果具体的设备不想使用核心层的函数,也可以重写.

1
2
3
4
5
6
7
return_type core_funca(xxx_device *bootm_dev, param1_type param1, param2_type param2)
{
if(bootm_dev->funca)
return bootm_dev->funca(param1, param2);
/*核心层通用的funca代码*/
...
}

由上面的代码可以看出, 如果用户设备驱动实现了函数 funca ,那么就使用用户定义的函数,否则使用核心层的代码.这样大部份设备不需要重写此部分代码, 只有少数特殊设备需要重新实现.大大提高了代码的可重用性.

  1. 核心层同类代码

核心层可以将同类设备的运行流程提取出来,这样就不用用户再去重复实现这类代码,而仅仅需要关心其底层操作即可.

比如:对于外设的基本控制,有协议层的驱动,而在设备驱动之上还有子系统的分层。用户实现接口即可。

输入设备驱动

输入设备(如键盘,按键,触摸屏,鼠标等)是典型的字符设备,其一般的工作机理是底层在按键,触摸等动作发生时产生一个中断(或驱动通过Timer定时查询), 然后CPU通过SPI,IIC或外部存储器总线读取键值,坐标等数据,并将它们放入一个缓冲区,字符设备驱动管理该缓冲区,而驱动的read()接口让用户可以读取键值,坐标等数据. 这些工作中只有中断,读值与设备相关,而输入事件的缓冲区管理以及字符设备驱动的 file_operations 接口则对输入设备是通用的,所以内核设计了输入子系统,由核心层处理公共的工作.

RTC设备驱动

RTC借助电池供电,在系统掉电的情况下依然可以正常计时.在通常还具有产生周期性中断以及闹钟中断的能力,是一种典型的字符设备.做为一种字符设备驱动, RTC需要有 file_operations 中接口函数的实现,而典型的 IOCTL 包括 RTC_SET_TIME,RTC_ALM_READ,RTC_ALM_SET,RTC_IRQP_SET等,这些对于所有的RTC是通用的,只有底层的具体实现与设备有关.

misc设备驱动

由于linux驱动倾向于分层设计,所以各个具体的设备都可以找到它归属的类型,从而套到它相应的架构里面去,并且只需要实现最底层的那一部分. 但是也有部分设备不知道它属于什么类型,则一般采用 miscdevice 框架结构. miscdevice本质上也是字符设备,只是在miscdevice核心层的misc_init()函数中, 通过 register_chrdev(MISC_MAJOR,"misc",&misc_fops) 注册了字符设备,而具体miscdevice实例调用 misc_register()的时候自动完成了 device_create(),获取动态次设备号的动作. 通过上述实例,可以归纳出核心层肩负的3大职责:

  1. 对上提供接口 file_operations 的读写和ioctl都被中间层搞定,各种I/O模型也被处理掉了.
  2. 中间层实现 通用 逻辑.可以被底层各种实例共享的代码都被中间层搞定,避免底层重复实现.
  3. 对下定义框架.底层的驱动不需要关心LINUX内核VFS接口和各种可能的I/O模型,而只需要处理与具体硬件相关的访问.

这种分层有时候还不是两层,可以有更多的层,在软件上呈现为面向对象里继承和多态的思想.

主机驱动与外设驱动分离

Linux中的SPI,I2C,USB等子系统都利用了典型的把主机驱动和外设驱动分离的想法: 让主机端只负责产生总线上的传输波形,而外设端只是通过标准的API来让主机端以适当的波形访问自身. 因此这里面就涉及了4个软件模块:

  1. 主机端驱动

根据具体的控制器硬件手册操作,产生总线波形.

  1. 连接主机和外设的纽带

外设不直接调用主机端的驱动来产生波形,而是调用一个标准的API.由这个标准的API把这个波形的传输请求间接转发给具体的主机端驱动.

  1. 外设驱动

外设接在I2C,SPI,USB这样的总线上,但是它们本身可以是触摸屏,网卡,声卡或者任意一种类型的设备.我们在相关的 i2c_driver,spi_driver,usb_driver这种 xxx_driver的probe()函数中去注册它的具体类型.当这些外设要求I2C,SPI,USB等去访问它的时候,它调用"连接主机和外设的纽带"模块的标准API.

  1. 板级逻辑

板级逻辑用来描述主机和外设是如何互联的,相当于一个"路由表".实际由设备树来完成.

Last Updated 2021-01-26 二 12:12.
Render by hexo-renderer-org with Emacs 26.3 (Org mode 9.4)