一、Linux驱动的软件架构
1.1 出发点
为适应多种体系架构的硬件,增强系统的可重用和跨平台能力。
1.2 分离思想
为达到一个驱动最好一行都不改就可以适用任何硬件平台的目的,将驱动与设备分离开来,驱动只管驱动,设备只管设备,而驱动以某种通用的标准途径去拿板级信息,从而降低驱动与设备的耦合程度。
1.3 分层思想
对于同类设备,其基本框架都是一样的,那么提炼出一个中间层,例如:对于 Input 设备(按键、键盘、触摸屏、鼠标)来说,尽管 file_operation、I/O模型不可或缺,但基本框架都是一样的,因此可提炼出一个Input核心层,把跟 Linux 接口以及整个一套input事件的汇报机制都在这里面实现。
二、platform设备驱动
platform:linux中的一种虚拟总线。一个现实的linux设备和驱动通常都需要挂接在一种总线上(方便管理),例如PCI、USB、I2C、SPI等,但是对于在Soc系统中集成的独立外设控制器、挂接在Soc内存空间的外设等却不能依附于上述总线,这时候linux就发明了一种虚拟总线,来管理这一类的设备(没有实体的硬件总线电路)。
platform设备驱动模型中,分为设备、驱动、总线3个实体,分别称为platform_device、paltform_driver和platform总线,总线负责将设备和驱动进行绑定。在系统每注册一个设备时,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动时,会寻找与之匹配的设备,而匹配的过程则由总线完成。
2.1 platform设备
platform设备:由platform_device结构体构成,负责管理外设的资源,例如I/O资源、内存资源、中断资源等等。
原型:linux/platform_device.h中
2.1.1 resource 结构体
resource结构体,描述了platform_device的资源:
参数flags常用类型IORESOURCE_IO、IORESOURCE_MEM、IORESOURCE_IRQ、IORESOURCE_DMA等。参数start和end的含义会随着flags的不同有所变化。
1)flags为IORESOURCE_MEM,start、end分别表示该platform_device占据的内存的开始与结束地址;
2)flags 为IORESOURCE_IRQ,start、end分别表示该platform_device 使用的中断号的开始值与结束值,如果使用 1个中断号,开始与结束值相同;
同类型的资源可以有多份,例如某设备占据了多个内存区域,则可以定义多个IORESOURCE_MEM。
例如在 arch/arm/mach-at91/board-sam9261ek.c 板文件中为 DM9000 网卡定义的 resource:
2.1.2 device 结构体中的 platform_data 资源
设备除了可在 BSP 中定义资源以外,还可以附加一些数据信息,因为对设备的硬件描述除了中断、内存等标准资源以外,可能还会有一些配置信息,而这些配置信息也依赖于板,不适宜直接放在设备驱动上。
因此,platform_device提供可供每个设备驱动自定义的platform_data形式以支持添加一些数据信息,即 Linux 内核不对这块的数据做要求。
device结构体:
例如在 arch/arm/mach-at91/board-sam9261ek.c 板文件中,将platform_data定义了dm9000_plat_data结构体,完成定义后,将MAC地址、总线宽度、板上有无EEPROM信息等放入:
2.1.3 platform_device 的注册
对于Linux 2.6 ARM 平台而言,对platform_device的定义通常在 BSP 的板文件中实现,在板文件中,将platform_device归纳为一个数组,随着板文件的加载,最终通过platform_add_devices()函数统一注册。
platform_add_devices()函数可以将平台设备添加到系统中,这个函数的原型为:
第一个参数为平台设备数组的指针,第二个参数为平台设备的数量,函数的内部是调用platform_device_register()函数逐一注册平台设备。
如果注册顺利,可在 sys/devices/platform 目录下看到相应名字的子目录。
Linux 3.x 之后,ARM Linux 不太以编码的形式去填写platform_device和注册,更倾向于根据设备树中的内容自动展开platform_device。
2.2 platform驱动
platform驱动:由platform_driver结构体构成,负责驱动的操作实现,例如加载、卸载、关闭、悬挂、恢复等。原型位于 linux/platform_driver.h中:
probe()和remove()分别对应驱动在加载、卸载时执行的操作。
而直接填充platform_driver的suspend()、resume()做电源管理回调的方法目前已经过时,较好的做法是实现platfrom_driver的device_driver中dev_pm_ops结构体成员(详细的参考电源管理章节)。
2.2.1 device_driver 结构体
与platform_driver地位对等的i2c_driver、spi_driver、usb_driver、pci_driver中都包含了device_driver结构体实例成员。它其实描述了各种 xxx_driver(xxx是总线名)在驱动意义上的一些共性。
2.2.2 驱动中获取板的资源
获取设备中resource资源:drivers/net/dm9000.c 中的dm9000_probe()函数
或者:
实际上是调用了platform_get_resource(dev, IORESOURCE_IRQ, num);
获取设备中platform_data资源:drivers/net/dm9000.c 中的dm9000_probe()函数
2.2.3 platform_driver 的注册
通过platform_driver_register()、platform_driver_unregister()进行platform_driver的注册于注销。
而原本的字符设备(或其它设备)的注册和注销工作移交到platform_driver的probe()和remove()成员函数中。以这样的形式对字符设备驱动进行注册,只是套了一层platform_driver的外壳,并没有改变是字符设备的本质。
例如在 drivers/net/dm9000.c 中,还是将其定义为网络设备,只是将网络设备驱动的注册流程放在probe()中:
2.3 platform总线
platform总线:负责管理外设与驱动之间的匹配。
系统为 platfrom总线 定义了一个bus_type的实例platform_bus_type,其定义位于 drivers/base/platform.c下:
2.3.1 .match 成员函数
重点关注其match()成员函数,此成员函数确定了platform_device和platform_driver之间是如何进行匹配的。
可以看出platform_device和platform_driver之间匹配有 3 种可能性:
基于设备树风格的匹配;
匹配 ID 表(即 platform_device 设备名是否出现在 platform_driver 的 ID 表内);
匹配 platform_device 设备名和驱动的名字。
2.3.2 platform总线的注册
2.3.3 platform总线自动匹配
无论是先注册设备还是先注册设备驱动,都会进行一次设备与设备驱动的匹配过程,匹配成功之后就会将其进行绑定,匹配的原理就是去遍历总线下设备或者设备驱动的链表。
2.4 platform 的优点
使得设备被挂接在一个总线上,符合 Linux 2.6 以后内核的设备模型。其结果是使配套的 sysfs 节点、设备电源管理都成为可能。
将 BSP 和 驱动隔离。在 BSP 中定义 platform 设备和设备使用的资源、设备的具体配置信息,而在驱动中,只需要通过通用 API 去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。
让一个驱动支持多个设备实例。譬如 DM9000 的驱动只有一份,但是我们可以在板级添加多份 DM9000 的 platform_device,他们都可以与唯一的驱动匹配。
在 Linux 3.x之后的内核中,DM9000 驱动可通过设备树的方法被枚举,添加的动作只需要简单的修改 dts 文件。(详细的后续再贴链接)
三、设备驱动的分层思想
在面向对象的程序设计中,可以为某一类相似的事物定义一个基类,而具体的事物可以继承这个基类中的函数。如果对于继承的这个事物而言,某成员函数的实现与基类一致,那它就可以直接继承基类的函数;相反,它也可以重写(Overriding),对父类的函数进行重新定义。若子类中的方法与父类中的某方法具有相同的方法名、返回类型和参数表,则新方法将覆盖原有的方法。这样可以极大的提高代码的可重用能力。
虽然 Linux 内核完全是由 C 和 汇编写的,但却频繁用到了面向对象的设计思想。在设备驱动方面,往往为同类的设备设计一个框架,而框架中的核心层则实现了该设备通用的一些功能。同样的,如果具体的设备不想使用核心层的函数,也可以重写。
例1:
在 core_funca() 函数的实现中,会检查底层设备是否重载了 core_funca()。如果重载了,就调用底层的代码,否则,直接使用通用层的。这样做的好处是,核心层的代码可以处理绝大多数该类设备的 core_funca() 对应的功能,只有少数特殊设备需要重新实现 core_funca()。
例2:
上述代码假定为了实现funca(),对于同类设备而言,操作流程一致,都要经过“通用代码A、底层ops1、通用代码B、底层ops2、通用代码C、底层ops3”这几步,分层设计明显带来的好处是,对于通用代码A、B、C,具体的底层驱动不需要再实现(抽离出来,放到核心层实现),而仅仅只关心其底层的操作ops1、ops2、ops3。下图明确反映了设备驱动的核心层与具体设备驱动的关系,实际上,这种分层可能只有2层,也可能是多层。
这样的分层设计在 Linux 的 Input、RTC、MTD、I2C、SPI、tty、USB等诸多类型设备驱动中都存在。
3.1 输入设备驱动
输入设备(如按键、键盘、触摸屏、鼠标等)是典型的字符设备,其一般的工作机理是底层在按键、触摸等动作发送时产生一个中断(或驱动通过 Timer 定时查询),然后CPU通过SPI、I2C 或外部存储器总线读取键值、坐标等数据,放入1个缓冲区,字符设备驱动管理该缓冲区,而驱动的 read() 接口让用户可以读取键值、坐标等数据。
显然,在这些工作中,只有中断、读值是设备相关的,而输入事件的缓冲区管理以及字符设备驱动的 file_operations 接口则对输入设备是通用的。基于此,内核设计了输入子系统,由核心层处理公共的工作。
3.1.1 输入核心提供了底层输入设备驱动程序所需的API
如分配/释放一个输入设备:
注册/注销输入设备用的如下接口:
报告输入事件用的如下接口:
而所有的输入事件,内核都用统一的数据结构来描述,这个数据结构是input_event:
3.1.2 案例:gpio按键驱动
drivers/input/keyboard/gpio_keys.c 是基于 input 架构实现的一个通用的 GPIO 按键驱动。该驱动基于 platform_driver架构,名为 “gpio-keys”。它将硬件相关的信息(如使用的GPIO号,电平等)屏蔽在板文件 platform_device 的 platform_data 中,因此该驱动可应用于各个处理器,具有良好的跨平台性。
该驱动的probe()函数:
在注册输入设备后,底层输入设备驱动的核心工作只剩下在按键、触摸等人为动作发生时报告事件。在中断服务函数中,GPIO 按键驱动通过input_event()、input_sync()这样的函数来汇报按键事件以及同步事件。
从底层的 GPIO 按键驱动可以看出,该驱动中没有任何 file_operation 的动作,也没有各种 I/O 模型,注册进入系统也用的是input_register_device()这样与 input 相关的 API。
这是由于与 Linux VFS 接口的这一部分代码全部都在 drivers/input/evdev.c 中实现了:
input核心层的 file_operations 和 read() 函数:
3.2 RTC 设备驱动
RTC (实时时钟)借助电池供电,在系统掉电的情况下依然可以正常计时。通常还具有产生周期性中断以及闹钟中断的能力,是一种典型的字符设备。
作为一种字符设备驱动,RTC 需要实现 file_operations 中的接口函数,例如 open()、read()等等。而 RTC 典型的 IOCTL 包括RTC_SET_TIME、RTC_ALM_READ、RTC_ALM_SET、RTC_IRQP_SET、RTC_IRQP_READ等,这些对于 RTC 来说是通用的,那么这些通用的就放在 RTC 的核心层,而与设备相关的具体实现则放在底层。
RTC 驱动模型如下图:
下面主要了解 RTC 核心 的以下几点:
实现 file_operations 的成员函数以及一些通用的关于 RTC 的控制代码;
向底层导出rtc_device_register()、rtc_device_unregister()以注册和注销 RTC;
导出 rtc_class_ops 结构体以描述底层的 RTC 硬件操作。
在这样的驱动模型下,底层的 RTC 驱动不再需要关心 RTC 作为字符设备驱动的具体实现,也无需关心一些通用的 RTC 控制逻辑。关系如下:
以S3C6410 的 RTC驱动为例:
RTC 核心:
1. 在文件 drivers/rtc/rtc-dev.c 中:实现 file_operations 相关成员函数
2. 在文件 drivers/rtc/class.c中:向底层提供注册/注销接口
3. 在文件 drivers/rtc/class.h中:导出 rtc_class_ops 结构体
S3C6410底层:在drivers/rtc/rtc-s3c.c 文件中
其注册 RTC 以及绑定 rtc_class_ops:
drivers/rtc/rtc-dev.c 以及其调用的drivers/rtc/interface.c 等 RTC 核心层相当于把 file_operations 中的 open()、release()、读取和设置时间等,都间接 “转发” 给了底层的实例。如下摘取部分 RTC 核心层调用具体底层驱动 callback 的过程:
1)open:
2)IOCTL的 命令:
3.3 Framebuffer 设备驱动
未深入,参考《Linux设备驱动开发详解:基于最新的Linux 4.0内核》
3.4 终端设备驱动
在 Linux 系统中,终端是一种字符型设备,它有多种类型,通常使用 tty (Teletype)来简称各种类型的终端设备。在嵌入式系统中,最常用的是 UART 串行端口。
3.4.1 内核中 tty 的层次结构
图中包含三个层次:
tty_io.c:tty 核心;
n_tty.c:tty 线路规程;
xxx_tty.c:tty 驱动实例。
3.4.1.1 tty_io.c
tty_io.c本身是一个标准的字符设备驱动,因此,它对上有字符设备的职责,需实现file_operations结构体成员函数。
但 tty 核心层对下又定义了tty_driver的架构,因此 tty 设备驱动的主体工作就变成了填充tty_driver结构体中的成员,实现其成员tty_operations结构体的成员函数,而不再是去实现file_operations结构体成员函数这一级的工作。
3.4.1.2 n_tty.c
n_tty.c:tty 线路规程的工作是以特殊的方式格式化从一个用户或者硬件收到的数据,这种格式化常常采用一个协议转换的形式。
3.4.2 tty 设备的发送/接收流程
发送流程:tty 核心从一个用户获取将要发送给一个 tty 设备的数据,tty 核心将数据传递给 tty 线路规程驱动,接着数据被传递到 tty 驱动,tty 驱动将数据转换为可以发送给硬件的格式。
从tty_driver操作集tty_operations的成员函数 write() 函数接收3个参数:tty_struct、发送数据指针和发送的字节数。该函数是被file_operations的 write() 成员函数间接触发调用的。
接收流程:从 tty 硬件接收到的数据向上交给 tty 驱动,接着进入 tty 线路规程驱动,再进入 tty 核心,在这里它被一个用户获取。
tty 驱动一般收到字符后会通过tty_flip_buffer_push()将接收缓冲区推到线路规程。
3.4.3 串口核心层
尽管一个特定的底层 UART 设备驱动完全可以遵循上述tty_driver的方法来设计,即定义tty_driver 并实现tty_operations中的成员函数,但是鉴于串口之间的共性,Linux 考虑在文件 drivers/tty/serial/serial_core.c 中实现 UART 设备的通用 tty 驱动层(称为串口核心层)。这样,UART 驱动的主要任务就进一步演变成了实现 文件 serial_core.c中定义的一组 uart_xxx 接口,而不是 tty_xxx 接口。
按照面向对象的思想,可认为 tty_driver 是字符设备的泛化、serial_core 是 tty_driver 的泛化,而具体的串口驱动又是 serial_core 的泛化。
在串口核心层又定义新的uart_driver结构体和其操作集uart_ops。一个底层的 UART 驱动需要创建和通过uart_register_driver()注册一个uart_driver而不是tty_driver。
uart_driver 结构体在本质上是派生自 tty_driver 结构体,因此,uart_driver 结构体中包含 tty_dirver 结构体成员。
tty_operations在UART 这个层面上也被进一步泛化为uart_ops:
由于 driver/tty/serial/serial_core.c 是一个tty_driver,因此在 serial_core.c 中,存在一个 tty_operations 的实例,这个实例的成员函数会进一步调用 struct uart_ops 的成员函数,这样就把 file_operaions 里的成员函数、tty_operations 的成员函数和 uart_ops 的成员函数串起来。
3.5 misc 设备驱动
......
3.6 驱动核心层
核心层的 3 大职责:
对上提供接口。file_operations 的读、写、ioctl 都被中间层搞定,各种 I/O 模型也被处理掉了。
中间层实现通用逻辑。可以被底层各种实例共享的代码都被中间层搞定,避免底层重复实现。
对下定义框架。底层的驱动不再需要关心 Linux 内核 VFS 的接口和各种可能的 I/O 模型,而只需处理与具体硬件相关的访问。
这种分层有时候还不是两层,可以有更多层,在软件上呈现为面向对象里类继承和多态的状态。
四、主机驱动与外设驱动分离的设计思想
4.1 主机驱动与外设驱动分离
Linux 中的 SPI、I2C、USB 等子系统都是典型的利用主机驱动和外设驱动分离的思想。
让主机端只负责产生总线上的传输波形,而外设端只是通过标准的 API 来让主机端以适当的波形访问自身。涉及 4 个软件模块:
1. 主机端的驱动。根据具体的 SPI、I2C、USB 等控制器的硬件手册,操作具体的控制器,产生总线的各种波形。
2. 连接主机和外设的纽带。外设不直接调用主机端的驱动来产生波形,而是调用一个标准的 API。由这个标准的 API 把这个波形的传输请求间接 “转发” 给具体的主机端驱动。最好在这里把关于波形的描述也以某种数据结构标准化。
3. 外设端的驱动。外设接在 SPI、I2C、USB 这样的总线上,但是它们本身可以是触摸屏、网卡、声卡或任意一种类型的设备。当这些外设要求 SPI 、I2C、USB等去访问它的时候,它调用 “连接主机和外设的纽带” 模块的标准 API。
4. 板级逻辑。用来描述主机和外设是如何互联的,它相当于一个 “路由表”。假设板子上有多个 SPI 控制器和多个 SPI 外设,那究竟谁接在谁上面?管理互联关系,既不是主机端的责任,也不是外设端的责任,这属于板级逻辑的责任。
linux 通过上述设计方法,划分为 4 个轻量级的小模块,各个模块各司其职。
4.2 Linux SPI 主机和设备驱动
4.2.1 SPI 主机驱动
在 Linux 中,通过spi_master结构体来描述一个 SPI 主动控制器驱动其主要成员由主机控制器的序号、片选数量、SPI 模式、时钟设置相关函数 和 数据传输相关函数。
文件spi/spi.h 中
分配、注册和注销 SPI 主机的 API 由 SPI 核心提供:文件 drivers/spi/spi.c
SPI 主机控制器驱动主体是实现了spi_master的 transfer()、setup() 这样的成员函数。也可能实现spi_bitbang的 txrx_buf()、setup_transfer()、chipselect() 这样的成员函数。
例如在文件 driver/spi/spi_s3c24xx.c 中:
4.2.2 纽带4.2.3 SPI 外设驱动
在 Linux 中,通过 spi_driver 结构体来描述一个 SPI 外设驱动,这个外设驱动可以认为是 spi_mater 的客户端驱动。SPI 只是一种总线,spi_driver 的作用只是将 SPI 外设挂接在该总线上,因此在 spi_driver 的 probe() 成员函数中,将注册 SPI 外设本身所属设备驱动的类型。
文件 spi/spi.h 中:
可看出,spi_driver 结构体 和 platform_driver 结构体有极大的相似性,都有 prob()、remove()、suspend()、resume()这样的接口和 device_driver 的实例。(这几乎是一切客户端驱动的常用模板)
在SPI 外设驱动中(文件 spi/spi.h 与 driver/spi/spi.c ):
1.spi_tansfer结构体:通过 SPI 总线进行数据传输的接口。
2.spi_message结构体:组织一个或多个spi_transfer,从而完成一次完整的 SPI 传输流程。
3. 初始化spi_message:
4. 将spi_transfer添加到spi_message队列:
5.spi_message的同步传输 API,阻塞等待这个消息被处理完:
6.spi_message的异步传输 API,不会阻塞等待这个消息被处理完,但可在 spi_message 的complete字段挂接一个回调函数,当消息被处理完成后,该函数会被调用:
7. 初始化spi_transfer、spi_message并进行 SPI 数据传输的例子,同时spi_write()、spi_read()也是SPI 核心层的两个通用API,在外设驱动中可直接调用进行简单的纯写、纯读操作:
4.2.4 SPI 板级逻辑
通 platform_driver 对应着一个platform_device一样,spi_driver 也对应着一个 spi_device;platform_device 需要在 BSP 的板文件中添加板信息数据,同样的 spi_device 也需要。
spi_device 的板信息用spi_board_info结构体描述,该结构体记录着 SPI 外设使用的主机控制器序号、片选序号、数据比特率、SPI 传输模式等。
两种方式添加板级信息:
1. 与 platfrom_add_devices 添加 platform_device 类似,通过spi_register_board_info()在 Linux 启动过程中的 机器init_machine()函数中进行注册:
在文件 arch/arm/mach-exynos/mach-itop4412.c 中:
2. 在 ARM Linux 3.x 之后的内核在改为设备树后,不再需要正在 arch/arm/mach-xxx 中编码 SPI 的板级信息了,而倾向于在 SPI 控制器节点下填写子节点。
免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。
这是一口君的新书,感谢大家支持!
领取专属 10元无门槛券
私享最新 技术干货