Linux驱动之平台设备(张栖银详谈)
一、设备驱动的分层与分离
相信经过前面对字符设备驱动、杂项设备驱动和输入子系统的介绍,大家对Linux系统中的驱动已经基本算是入门了。可是我们发现,掌握这些知识之后,还不足以支撑我们去分析LCD、UART、USB等Linux内核驱动。原因主要有如下两点:
- 内核中大量的使用了同步、互斥机制,以及内核提供的编程接口;
- LCD、UART、USB都有自己的设备驱动框架,而且都使用了平台设备驱动;
针对第一点,大家可以去看看宋宝华的《Linux设备驱动开发详解》的3、4、7、8、9、10、11章节的内容,里面大部分知识在字符设备驱动、块设备驱动、LCD驱动、USB驱动中大量的使用,学好这些基础知识,对阅读Linux内核源代码也是非常的有帮助的。
针对第二点,就是本文介绍的重点内容,希望能带给广大阅读者、技术宅带来帮助。
在开始介绍platform之前,首先来介绍一下Linux设备驱动中的分层和分离的思想,便于大家理解Linux内核中的设备、总线啊、驱动模型,从而更加清晰的认识到Linux内核引入platform的重要作用和意义。
1.1 设备驱动的分层思想
在面向对象的程序设计中,可以为某一类相似的事物定义一个基类,而具体的事物可以继承这个基类中的函数。如果对于继承的这个事物而言,其某成员函数的实现与基类一致,那它就可以直接继承基类的函数;相反,它也可以重载之。这种面向对象的设计思想极大地提高了代码的可重用能力,是对现实世界事物间关系的一种良好呈现。
Linux内核完全由C语言和汇编语言写成,但是却频繁用到了面向对象的设计思想。在设备驱动方面,往往为同类的设备设计了一个框架,而框架中的核心层则实现了该设备通用的一些功能。同样的,如果具体的设备不想使用核心层的函数,它也可以重载之。举个例子:
return_type core_funca(xxx_device *bottom_dev, paraml_type param1, paraml_type param2)
{
if(bottom_dev->funca)
return bottom_dev->funca(param1, param2); //调用重载代码
/* 核心通用的funca代码 */
. . . . . .
}
上述core_funca的实现中,会检查底层设备是否重载了funca(),如果重载了,就调用底层的代码,否则就直接使用通用层代码。这样做的好处是:核心层的代码可以处理绝大多数该类设备的funca()对应的功能,只有少数特殊设备需要重新实现funca()。
下面再来看一个例子:
return_type core_funca(xxx_device *bottom_dev, paraml_type param1, paraml_type param2)
{
/* 通用步骤代码A */
typea_dev_commonA();
. . . . . .
/* 底层操作ops1 */
bottom_dev->funca_ops1()
. . . . . .
/* 通用步骤代码B */
typea_dev_commonB();
. . . . . .
/* 底层操作ops2 */
bottom_dev->funca_ops2()
. . . . . .
/* 通用步骤代码C */
typea_dev_commonB();
. . . . . .
/* 底层操作ops3 */
bottom_dev->funca_ops3()
. . . . . .
}
上述代码假定为了实现funca(),对于同类设备而言,其操作流程是一致的,都要经过“通用代码A、底层ops1、通用代码B、底层ops2、通用代码C、底层ops3”这几步,分层设计明显带来的好处是:对于通用代码A、B、C,具体的底层驱动不需要再实现,而仅仅只关心其底层的操作ops1、ops2和ops3。
图1.1中明确反映了设备驱动的核心层与具体设备驱动的关系,实际上,这种分层可能只有两层,也可能是多层的。
图1.1 Linux设备驱动的分层设计思想
image
这样的分层化的设计在Linux的input、RTC、MTD、I2C、SPI、TTY、USB等诸多设备驱动类型中屡见不鲜。针对input,前面我们已经介绍了,接下来本文将介绍platform,后续还将持续更新RTC、I2C、SPI、LCD、USB等设备驱动详细分析,欢迎大家订阅。
1.2 设备驱动的分离思想
在Linux设备驱动框架的设计中,除了有分层设计实现以外,还有分离的思想。这里所谓的设备驱动的分离思想,实际上是指主机驱动与外设驱动分离的思想。举一个简单的例子,假设我们要通过SPI总线访问某个外设,在这个访问过程中,要通过操作CPU_xxx上的SPI控制器的寄存器来达到访问SPI外设YYY的目的,最简单的方法是:
return_type xxx_write_spi_yyy(. . .)
{
xxx_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上的时候,它访问CPU_xxx的SPI主机控制寄存器,当用在CPU_xxx1的时候,它访问CPU_xxx1的SPI主机控制寄存器:
return_type xxx1_write_spi_yyy(. . .)
{
xxx1_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和CPU_xxx1上的时候需要不同的驱动。那么,我们可以用图1.2所示的思想对主机控制器驱动和外设驱动进行分离。这样的结果是:外设a、b、c的驱动与主机控制器A、B、C的驱动不想关,主机控制器驱动不关心外设,而外设驱动也不关心主机,外设只是访问核心层通用的API进行数据传输,主机和外设之间可以进行任意的组合。
图1.2 Linux设备驱动的主机、外设驱动分离的思想
image
如果我们不进行图1.2所示的主机和外设分离,那么外设a、b、c和主机A、B、C进行组合的时候,就需要9个不同的驱动。设想一共有m个主机控制器i,n个外设,分离的结果是需要m+n个驱动,不分离则需要m*n个驱动。
这样的分离的设计在Linux的I2C、SPI、USB等诸多设备驱动类型中也屡见不鲜。但是其基础和核心还是设备、总线、驱动模型。后续将对I2C、SPI、USB设备驱动进行详细分析。
Linux设备分层与分离的思想,对应到Linux内核就是device、bus、driver驱动模型,这也是Linux内核驱动的核心内容。与此相关的内容,可见《深入Linux设备驱动程序内核机制》第9章 Linux设备驱动模型,其中还详细介绍了kobject和kset结构体。
二、平台设备驱动概述
在理解了Linux内核中的分层与分离思想以后,接下来咱们开始拿内核中具体的device、bus、driver设备驱动模型的实例进行学习和理解。由于在Linux的各种类型驱动中,很多设备都使用到了platform设备驱动模型,这里就首先拿platform开刀,展开学习之旅。
2.1 platform的引入
在设备驱动程序中经常会见到和platform相关的字段,分布在驱动程序的多个角落,这也是2.6内核中比较重要的一种机制,把它原理弄懂,对以后分析驱动程序很有帮助:在linux2.6设备模型中,关心总线、设备、驱动这三个实体,总线将设备和驱动绑定,在系统每注册一个设备的时候,会寻找与之匹配的驱动。相反,在系统每注册一个驱动的时候,寻找与之匹配的设备,匹配是由总线来完成的。
一个现实的Linux 设备和驱动通常都需要挂接在一种总线上,对于本身依附于PCI、USB、I2C、SPI等的设备而言,这自然不是问题,但是在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设等确不依附于此类总线。基于这一背景,Linux 发明了一种虚拟的总线,称为platform总线。SOC系统中集成的独立外设单元(LCD,RTC,WDT等)都被当作平台设备来处理,而它们本身是字符型设备。
从Linux2.6内核起,引入一套新的驱动管理和注册机制:platform_device和platform_driver。++platform是一个虚拟的地址总线,相比pci、usb,它主要用于描述SOC上的片上资源,比如S3C2440上集成的控制器(lcd、watchdog、rtc等)。platform所描述的资源有一个共同点,就是在CPU的总线上直接取址++。Linux中大部分的设备驱动,都可以使用这套机制,设备用platform_device表示,驱动用platform_driver进行注册。
Linux platform driver机制和传统的device driver机制(通过driver_register函数进行注册)相比,一个十分明显的++优势在于platform机制将设备本身的资源注册进内核,由内核统一管理,在驱动程序中使用这些资源时通过platform device提供的标准接口进行申请并使用,这样提高了驱动和资源管理的独立性,并且拥有较好的可移植性和安全性(这些标准接口是安全的)++。
platform机制将设备本身的资源注册进内核,由内核统一管理,在驱动程序中使用这些资源时通过platform设备提供的标准接口进行申请并使用,从而达到了统一管理系统外设资源的目的,并将驱动和资源分离,提高了驱动和资源管理独立性,且拥有更好的移植性。
2.2 platform与chrdev、blockdev和netdev的关系
注意,前面所讲的platform_device并不是与字符设备、块设备和网络设备并列的概念,而是Linux系统提供的一种附加手段。比如s3c2410中的看门狗驱动,它既是字符设备驱动,也是杂项设备驱动,同时还是平台设备驱动:
字符设备描述了看门狗的访问方式是串行、顺序的,而不是随机、缓冲的;混杂设备意味着看门狗这个字符设备被丢在了使用同一设备号的混杂设备里面;平台设备意味着看门狗这个设备是属于平台的独立模块,它完全是一项附加信息。
对于看门狗而言,“字符设备”是对其“本质”的描述,“混杂设备”是存放这个字符设备的“容器”,“平台设备”则描述了看门狗的一种“特征”或“属性”。
例如一颗花生,如果我们把它本身看作一个字符设备,当放在八宝粥里面,它就成了八宝粥混杂设备之一,而花生本身挂接在花生树苗的根部,成为整个花生树苗中一个独立的硬件单元而存在,因此,也可以定义为一个平台设备。
下图所示为看门狗设备驱动中其作为字符设备、混杂设备和平台设备时的各组成元素,看门狗设备集三重身份于一身。
2.3 引入platform的好处
为了提高大家的学习的兴趣,好保证一鼓作气一气拿下platform,这里列出内核引入platform的好处:
- 保证了Linux内核所有驱动结构的清晰;
- 这就是Linux内核2.6版本之后驱动模型的核心:设备、总线、驱动模型;
- 更好的体现Linux内核分层的思想,同时也体现了Linux内核分离的思想;
- 使得每一个硬件设备都挂接在一个总线上,从而又保证配套的sysfs节点、设备电源管理都成为可能;
- 隔离BSP和驱动。在BSP中定义platform设备和设备使用的资源、设备的具体配置信息,而在驱动中,只需要通过通用API去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。
三、平台设备驱动详解
下面结合Linux-2.6.32.2内核版本,重点分析platform内核源码,详细讲解platform实现的原理、机制,以及提供的接口函数及使用方法,后面章节还将以实例讲解平台设备驱动的开发方法。