linux设备驱动程序--串行通讯驱动框架分析

linux 串行通讯接口驱动框架

在学习linux内核驱动时,不管是看linux相关的书籍,又或者是直接看linux的源码,老是能在linux中看到各类各样的框架,linux内核极其庞杂,linux各类框架理解起来并不容易,若是直接硬着头皮死记硬背,意义也不大。html

博主学习东西一直秉持着追本溯源的态度,要弄清一个东西是怎么样的,若是可以了解它的发展,了解它为何会变成这样,理解起来就很是简单了。抓住主干,沿着线头就能够将整个框架慢慢梳理清楚。linux

从i2c开始

在嵌入式中,无论是单片机仍是单板机,i2c做为板级通讯协议是很是受欢迎的,尤为是在传感器的使用领域,其主从结构加上对硬件资源的低要求,稳稳地占据着主导地位。程序员

咱们就以i2c协议为例,聊一聊linux内核中串行通讯接口框架。 (注:在这篇文章只讨论大体框架,并不涉及具体细节,linux内核驱动部分的框架分得很细,没法所有覆盖,但求创建一个大致的概念)编程

单片机中的i2c

每一个MCU基本上都会集成硬件i2c控制器,在单片机编程中,对于操做硬件i2c控制器,咱们只须要操做一些相应的寄存器便可实现数据的收发。安全

那若是没有硬件i2c控制器或者i2c控制器不够用呢?框架

事情也不麻烦,咱们可使用两个gpio来软件模拟i2c协议,代码不过几十行,虽然i2c协议自己有必定的复杂性,可是若是仅仅是实现通讯,在单片机上仍是很是简单的。模块化

单片机中实现i2c程序

咱们不妨回想一下,在单片机中编写一个i2c驱动程序的流程:函数

以sht31(这是一个经常使用的i2c接口的温湿度传感器)为例,刚入行的新手程序员可能这样写主机程序(伪代码):性能

int sht31_read_temprature(){                 //读取温度值实现函数
    设置i2c写寄存器,发送i2c器件地址
    设置i2c写寄存器,发送i2c寄存器地址
    设置i2c读
    temperature = 读取目标器件发回的数据
    return temperature;
}   

int sht31_read_humidity(){                   //读取湿度值实现函数
    设置i2c写寄存器,发送i2c器件地址
    设置i2c写寄存器,发送i2c寄存器地址
    设置i2c读
    humidity = 读取目标器件发回的数据
    return humidity;
}   
....

程序优化

每次读写函数都对硬件i2c的寄存器进行设置,很显然,这样的代码有不少重复的部分,咱们能够将重复的读写部分提取出来做为公共函数,写成这样:学习

array sht31_read_data(sht31数据寄存器地址){
    设置i2c写寄存器,发送i2c器件地址
    设置i2c写寄存器,发送i2c寄存器地址
    设置i2c读
    return 读取目标器件发回的数据;
}

因此,上例中的读温湿度就能够写成这样:

array sht31_read_temprature(){
    return sht31_read_data(sht31温度数据寄存器地址);
}
array sht31_read_humidity(){
    return sht31_read_data(sht31湿度数据寄存器地址);
}
...

通过这一步优化,这个驱动程序就变成了两层:

  • i2c硬件操做部分,好比i2c与设备的读写,在同一平台上,硬件读写的寄存器操做都是一致的。
  • 设备的操做函数,不一样的设备有不一样的寄存器,对于存储设备而言就是存取数据,对于传感器而言就是读写传感器数据,须要读写设备时,直接调用第一步中的接口,传入不一样的参数。

能够明显看到的是,第一步中的i2c操做函数部分能够被抽象出来。

这就是软件的分层

若是你仔细看了上面的示例,基本上就理解了软件分层是什么概念,它其实就是不断地将公共的部分和重复代码提取出来,将其做为一个统一的模块,向外提供访问的接口。

从宏观上来看程序就被分红了两部分,因为是调用与被调用的关系,因此层次结构能够更明显地体现他们之间的关系。

分层的好处

最直观地看过去,软件分层第一个好处就是节省代码空间,代码量更少,层次更清晰,对于代码的可读性和后期的维护都是很是大的好处。

将相关的数据和操做封装成一个模块,对外提供接口,屏蔽实现细节,便于移植和协做,由于在移植和修改时,只须要修改当前模块的代码,保持对外接口不变便可,减小了移植和维护成本。

举个例子:在上述的代码中,若是将sht31换成其余i2c设备,我只须要修改设备的操做函数部分,而i2c的读写部分能够复用.又或者一样的设备,切换成了spi协议通讯,那么,设备的操做函数部分能够不用修改,只须要将i2c硬件读写换成spi硬件读写。

程序的再次优化

在程序的第一次优化中,i2c被抽象出两层:i2c硬件读写层和i2c的应用层(暂且这么命名吧),读写层只负责读写数据,而应用层则根据不一样设备进行不一样的读写操做,调用读写层接口。

划分红读写层和驱动层以后,在空间上和程序复用性上已经走了一大步。

可是在以后的开发中又发现一个问题:对于单个的厂商而言,生产的设备每每具备高度类似的寄存器操做方式,好比对于咱们经常使用的sht3x(温湿度传感器)而言,这些系列的传感器设备之间的不一样仅仅是测量范围、测量精度的不一样,其余的寄存器配置实际上是同样的,有经验的程序员就想到能够抽象出这样一个统一接口来针对全部这些同系列设备:

sht3x_init(int sht3x_type,int measurement_range,int resolution){
    switch(sht3x_type){
        case sht31:
        set_measurement_range(sht31,measurement_range);
        set_resolution(sht31,resolution);
        break;
        case sht35:
        set_measurement_range(sht35,measurement_range);
        set_resolution(sht35,resolution);
        break;
        case ...
    }
}

仅仅是在设置的时候设置不一样的测量范围和精度,而其余的设置,数据读写部分都是彻底相同的。

对于这些同系列的设备,咱们一样能够抽象出一个驱动层,以sht3x为例,同系列设备的操做变成这样:

  • i2c硬件读写层。
  • sht3x的公共操做函数,经过调用上层接口实现初始化、设置温度阈值等函数。
  • 具体设备的操做函数,对于sht31而言,经过传入sht31的参数,调用上层接口来读写sht31中的数据,须要传入的参数主要是i2c地址,设置精度等sht3x系列之间的差别化部分。

这样,对于sht3x而言,用户在使用这一类设备的时候就只须要简单地调用诸如sht3x_init(u8 i2c_addr),sht3x_set_resolution(u8 resolution)这一类的函数便可完成设备的操做,经过传入不一样的参数执行不一样的操做。

这里贴上一个图来加深理解:

到这里,对于一个单片机上的i2c设备而言,基本上已经有了比较好的层次结构:

  • i2c硬件读写层
  • 驱动层(主要是解决同系列设备驱动重复的寄存器操做问题)
  • 设备层(应用程序调用上一层接口实现具体的设备读写)

将上述分层的1.2步集成到系统中,用户只须要调用相应接口直接就能够操做到设备,对用户来讲简化了操做流程,对系统来讲节省了程序空间。

到这里,这一份i2c程序就已经比较完善了,当须要添加sht3x设备时,只须要在设备层添加便可。

当须要添加其余i2c设备时,则须要提供驱动层和设备层的实现而复用硬件读写层。

当须要将sht3x驱动程序移植到另外一个平台时,只须要修改i2c硬件读写层,由于平台之间的i2c读写操做可能不一致,驱动层和设备层不须要修改。

整个分层的思想就是复用。大大节省了调试时间,在debug的时候也能很方便地定位是哪一部分出了问题,同时能够将程序发布给其余用户,其余用户根据须要能够很方便地进行裁剪移植,避免浪费过多时间在同一件事情上。

再看看linux中的i2c

在单片机上能够碰到的问题,在linux系统上一样能碰到,单片机上运行的程序通常而言不会太庞大,驱动部分更是所占甚微,因此设备驱动程序的好坏并不会太过于影响程序的执行效率。

可是在linux中,有时须要集成大量的驱动设备到系统中,同时内核代码是多人维护的模式,因此也必须采用驱动分层的模式来提升内存使用效率,下降程序耦合性。

那么,在linux中是否也是像单片机中同样分为i2c硬件读写层、驱动层、设备层便可呢?

其实大体的思想是同样的:将硬件读写抽象成一层,将同系列产品驱动抽象成一层,将具体设备的添加抽象成顶层,可是具体实现彻底不同。

与单片机中程序不同的是:linux中内核空间和用户空间是区分开来的,驱动程序将会被加载到内核中,提供接口给用户进行操做。

从单片机切换到linux的第一种解决方案

不难想到的解决方案是:分层模型不变,将上述分层中的设备层改成由驱动层直接在用户空间注册文件接口,用户程序经过用户文件对设备进行操做。

因而,分层模型变成了这样:

  • i2c硬件读写层
  • 驱动层(主要是解决同系列设备驱动重复问题,同时在用户空间注册用户接口以供访问)
  • 应用层(相对于内核而言,等于单片机分层中的设备层,应用程序经过操做文件调用上一层接口实现具体的设备读写)

问题就这样获得解决。

用户在操做用户空间文件接口的时候,依次地经过文件接口传递目标设备的资源对设备进行初始化,各类设置,而后读写便可,对于sht3x而言,这些资源包括i2c地址、精度、阈值等等。

可是,这样的作法的缺陷是:

  • 提升了驱动程序和应用开发的耦合性,同时驱动程序不具备独立性和安全性,程序的可移植性也不好。
  • 同时,此时的驱动层对应的是同系列设备,注册到用户空间的接口会更加抽象,用户须要对驱动程序进行二次开发.

想想,当用户须要使用sht31时,用户还得去阅读sht31的datasheet来查看并设置各类参数,这样驱动和用户程序不分离彻底不符合高内聚低耦合的程序思想。

最理想的状态天然是:

  • 驱动程序自己和驱动程序须要的资源由内核统一管理,这样才能提升驱动和管理资源的独立性,而且拥有较好的额可移植性和安全性,提供尽可能简单的操做接口给用户空间。
  • 用户空间只须要直接使用驱动,而不须要对驱动程序进行二次开发。

第二种解决方案

既然驱动部分没法针对单一设备,而是针对诸如sht3x这一类设备,那咱们为每一个单一设备添加一份描述设备信息来提供资源(i2c地址等),好比sht3一、sht35分别提供一份设备描述信息,而一个驱动程序能够对应多份设备描述信息。

当须要使用某个具体设备好比sht31时,再将sht31的设备描述信息和sht3x的驱动程序结合起来,生成一个完整的sht31设备驱动程序,并在用户空间注册sht31文件接口,这样文件接口就能够针对具体的设备而实现具体的操做功能,用户能够经过简单的参数选择而直接使用。

同时将设备描述信息同时注册到内核中,由内核统一管理,提升了驱动和资源管理的独立性和安全性,同时移植性能较好。

这样,在分层模型中,咱们将设备描述信息(设备资源)部分和驱动程序放置在同一层,驱动模型就变成了这样:

  1. i2c硬件读写层
  2. 驱动部分(提供系列设备的公共驱动部分)-------设备部分(提供单一设备的资源,驱动程序获取资源进行相应配置,与驱动层为一对多的关系),统称为驱动层
  3. 应用层(经过上层提供的接口对设备进行访问)

这样的分层有个好处,在第二层驱动层中,直接实现了每一个单一设备的程序,对应用层提供简便的访问接口,这样在用户层不用再进行复杂的设置,就能够直接对设备进行读写。

它的分层是这样的:

i2c硬件读写层

linux i2c设备驱动程序中的硬件读写层由struct i2c_adapter来描述,它的内容是这样的: struct i2c_adapter { ... const struct i2c_algorithm algo; / the algorithm to access the bus */ void *algo_data; int nr;
char name[48]; ... }; 其中struct i2c_algorithm *algo结构体中master_xfer函数指针指向i2c的硬件读写函数,咱们能够简单地认为一个struct i2c_adapter结构体描述一个硬件i2c控制器。在驱动编写的过程当中,这一层的实现由系统提供。驱动编写者只须要调用i2c_get_adapter()接口来获取相应的adapter.

驱动层(中间层,包含设备部分和驱动部分)

linux总线机制

在上面的分层讨论中可知:驱动层由驱动部分和设备部分组成,驱动部分由struct i2c_driver描述,而设备部分由struct i2c_device部分组成。

这两部分虽然被咱们分在同一层,可是这是相互独立的,当添加进一个device或者driver,会根据某些条件寻找匹配的driver或者device,那这一部分匹配谁来作呢?

这就不得不提到linux中的总线机制,i2c总线担任了这个衔接的角色,诚然,i2c总线也属于总线的一种,i2c总线在系统启动时被注册到系统中,管理i2c设备。

咱们先来看看描述总线的结构体:

struct bus_type {
    ....
    const char		*name;
    int (*match)(struct device *dev, struct device_driver *drv);
    int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
    int (*probe)(struct device *dev);
    int (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);
    int (*online)(struct device *dev);
    int (*offline)(struct device *dev);
    int (*suspend)(struct device *dev, pm_message_t state);
    int (*resume)(struct device *dev);
    struct subsys_private *p;
};

struct subsys_private {
    ...
    struct klist klist_devices;
    struct klist klist_drivers;
    unsigned int drivers_autoprobe:1;
    ...
};

这个结构体描述了linux中各类各样的sub bus,好比spi,i2c,platform bus,能够看到,这个结构体中有一系列的函数,在struct subsys_private结构体定义的指针p中,有struct klist klist_devices和struct klist klist_drivers这两项。

当咱们向i2c bus注册一个driver时,这个driver被添加到klist_drivers这个链表中,当向i2c bus注册一个device时,这个device被添加到klist_devices这个链表中。

每有一个添加行为,都将调用总线的match函数,遍历两个链表,为新添加的device或者driver寻找对应的匹配,一旦匹配上,就调用probe函数,在probe函数中执行设备的初始化和建立用户操做接口。

应用层

在总线的(或者驱动部分的)probe函数被执行时,在/dev目录下建立对应的文件,例如/dev/sht3x,用户程序经过读写/dev/sht3x文件来操做设备。

同时,也能够在/sys目录下生成相应的操做文件来操做设备,应用层直接面对用户,因此接口理应是简单易用的。

驱动开发者的工做

通常来讲,若是只是开发i2c驱动,而不须要为新的芯片移植驱动的话,咱们只须要在驱动层作相应的工做,并向应用层提供接口。i2c硬件读写层已经集成在系统中。

抽象化仍在进行中

上文中提到,当驱动开发者想要开发驱动时,在驱动层编写一个driver部分,添加到i2c总线中,同时编写一个device部分,添加到i2c总线中。

driver部分主要包含了全部的操做接口,而device部分提供相应的资源。

可是,随着设备的增加,同时因为device部分老是静态定义在文件中,致使这一部分占用的内核空间愈来愈大,并且,最主要的问题时,对于资源来讲,大多都是一些重复的定义:好比时钟、定时器、引脚中断、i2c地址等同类型资源。

按照一向的风格,对于大量的重复定义,咱们必须对其进行抽象化,因而linus一怒之下对其进行大刀阔斧的整改,因而设备树横空出世,是的,设备树就是针对各类总线(i2c bus,platform bus等等)的device资源部分进行整合。

将设备部分的静态描述转换成设备树的形式,由内核在加载时进行解析,这样节省了大量的空间。

若是有兴趣能够看看博主的设备树解析篇:linux设备树解析--从dtb格式开始

小结

总的来讲,分层便是一种抽象,将重复部分的代码不断地提取出来做为一个独立的模块,由于模块之间是调用与被调用的关系,因此看起来就是一种层次结构。

高内聚,低耦合,这是程序设计界的六字箴言。

无论是linux仍是其余操做系统,又或者是其余应用程序,将程序模块化都是一种很好的编程习惯,linux内核的层次结构也是这种思想的产物。

这篇文章只是简单地分析了linux分层机制的由来,从原理上理解为何会有这样的层次结构,事实上,因为linux系统的复杂性,linux的驱动框架并不是这么简单地分红三个部分,博主只是抽去了大部分细节,展示了最粗犷的框架。

接下来,博主还会带你走进linux内核代码,剖析整个i2c框架的函数调用流程。

好了,关于linux驱动框架的讨论就到此为止啦,若是朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言

原创博客,转载请注明出处!

祝各位早日实现项目丛中过,bug不沾身.

相关文章
相关标签/搜索