VxWorks设备驱动开发详解
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

5.2 VxWorks内核驱动基本结构——内核三张表

由于I/O子系统在整个驱动层次中起着管理的功能,其维护着系统设备和驱动的关键的三张表。故本节着重介绍I/O子系统层次的相关数据结构。

我们以一个字符设备为例,假设已经将该字符设备的驱动向I/O子系统进行了注册,并创建了一个该字符设备的文件节点。下面以用户打开设备操作为例,介绍用户层请求传递到底层驱动的调用流程。

用户在使用一个设备之前,必须先打开该设备。以字符设备的文件节点为路径名调用open函数,open函数将请求转移给iosOpen。I/O子系统维护着当前系统所有的驱动和设备。故其根据open函数调用时传入的设备节点名从系统设备列表中查询设备,查询到设备后,由设备结构中的相关字段值得知该设备对应的驱动程序索引号,I/O子系统根据该驱动程序号从系统驱动列表中获取该设备对应的驱动函数集合,调用底层驱动open响应函数,底层驱动open响应函数将进行中断注册,使能硬件工作,完成用户层打开设备的服务请求。

open函数返回一个整型数,我们将其称为文件描述符,I/O子系统除了系统驱动和系统设备两张表外,其维护的第三张表就是系统当前打开的所有的文件描述符表,该表中每个表项都是一个数据结构,表项在表中的位置索引号就是文件描述符本身,而表项的内容则表明了该文件描述符对应的设备以及驱动。此后对设备的读写、控制、关闭或其他任何操作将以文件描述符为依据。由文件描述符可以直接寻址到被操作设备的驱动程序。

5.2.1 系统设备表

VxWorks内核对每个设备使用DEV_HDR数据结构进行表示,该结构定义如下。

        /*h/iosLib.h*/
        typedef struct               /* DEV_HDR - device header for all device structures */
        {
            DL_NODE  node;           /* device linked list node */
            short     drvNum;        /* driver number for this device */
            char *   name;          /* device name */
        } DEV_HDR;

该结构中给出了链接指针(用以将该结构串入队列中)、驱动索引号、设备节点名。内核提供这个结构较为简单,只存储了一些设备关键系统。底层驱动对其驱动的设备都有一个自定义数据结构表示,其中包含了被驱动设备寄存器基地址、中断号、可能的数据缓冲区、保存内核回调函数的指针,以及一些标志位。最关键的一点是DEV_HDR内核结构必须是这个自定义数据结构的第一个成员变量,因为这个用户自定义结构最后需要添加到系统设备队列中,故必须能够在用户自定义结构与DEV_HDR结构之间进行转换,而将DEV_HDR结构设置为用户自定义结构的第一个成员变量就可以达到这个目的。如下代码为一个用户自定义设备结构的简单示例。

        typedef struct  xxDev
        {
              DEV_HDR  devHdr;                  //内核提供的结构,必须是自定义结构的第一个成员变量。
              UINT32         regBase;           //设备寄存器基地址。
              UINT32         buffPtr;           //数据缓冲区基地址。
              BOOL      isOpen;                 //设备已打开标志位。
              UINT8          intLvl;            //设备中断号。
              FUNCPTR  putData;                 //内核回调函数指针,该指针指向的函数向内核提供数据。
              FUNCPTR  getData;                 //内核回调函数指针,该指针指向的函数从内核获取数据。
              …                                //其他设备参数。
        }

为了能够让用户对设备进行操作,驱动程序必须将设备注册到I/O子系统中,这个过程也被称为创建设备节点。

I/O子系统提供的iosDevAdd函数用以被驱动程序调用注册一个设备。该函数调用原型如下。

        STATUS iosDevAdd
            (
            DEV_HDR *pDevHdr, /* pointer to device's structure */
            char *name,       /* name of device */
            int drvnum        /* no. of servicing driver, */
                        /* returned by iosDrvInstall() */
        );

注意传入iosDevAdd的参数:

● 参数1(pDevHdr)是一个DEV_HDR结构类型,一般我们将用户自定义结构作为第一个参数传入,这也是必须将DEV_HDR结构类型的成员变量作为用户自定义结构的第一个成员的原因所在。

● 参数2(name)表示设备节点名,这个名称将被用户程序调用作为打开设备时的路径使用。

● 参数3(drvnum)是设备对应的驱动程序索引号。这个驱动号是iosDrvInstall函数的返回值。在设备初始化函数中,我们首先调用iosDrvInstall注册驱动,然后使用iosDrvInstall函数返回的驱动号调用iosDevAdd添加设备到系统中,这两步完成之后,设备就可以被用户程序使用了。

iosDevAdd函数将一个设备添加到由I/O子系统维护的系统设备列表中,该列表是一个队列,队列中的成员通过指针链接在一起,这是由DEV_HDR结构中的node成员变量完成的。系统设备列表由iosDvList内核变量指向,如图5-3所示为系统设备列表示意图。

图5-3 系统设备表示意图

注意

图5-3 中,对于iosDvList的指针表示只是一种简单方式,实际上,iosDvList还有一个尾部指针指向队列的尾部,图5-3中没有显示出来。

系统设备列表中第一个设备是内核本身添加的,这是一个null设备,所有写入null设备的数据都将被直接丢弃,这种机制对于屏蔽一些输出十分有效。null设备是内核内部设备,驱动号0被专门预留给null设备。null设备单独有一个DEV_HDR结构表示,不存在其他参数,故图5-3 中对null设备只显示了一个DEV_HDR结构,而其他设备一般都需要在DEV_HDR结构之外定义额外的参数。

图5-3中还显示了系统中存在的两个串口设备,这两个串口使用相同的驱动,实际上,此处显示的驱动索引号是TTY驱动的驱动号,还不是真正的底层串口驱动号,底层串口驱动通过TTY进行管理,故对不同串口的操作在TTY驱动层才进行分离,所有的串口驱动首先都需要通过相同的TTY驱动层的处理,而后请求被转发到具体的底层串口驱动。

用户可在命令行下输入iosDevShow或devs,显示系统设备中的所有设备。

用户调用open函数打开一个设备文件时,I/O子系统将以传入的文件路径名匹配系统设备中的设备节点名,匹配方式是最佳匹配,即名称最相近的设备被返回。如输入的文件路径为“/pipe/xyzu”,如果系统设备表中存在两个设备:“/pipe/xy”、“/pipe/xyz”,那么“/pipe/xyz”设备将被返回,无论其位置在前还是在后。当然如果传入的文件路径名长度较小,那么此时系统设备表最前面的设备将被返回。例如,如果传入的文件路径名为“/pipe/x”,那么对于系统设备中的“/pipe/xy”和“/pipe/xyz”两个设备,谁位于设备表的前面,谁就被返回。对于路径名比设备名长的情况,在对块设备的操作中比较普遍。一般我们在块设备上创建一个文件系统,我们对块设备创建一个设备节点,而对块设备的所有操作都是在这个根节点下,此时块设备节点就成为判断一个被操作的文件或者目录到底属于哪个块设备(如果系统中存在多个块设备的话)。

5.2.2 系统驱动表

I/O子系统维护的系统驱动表包含了当前注册到I/O子系统下的所有驱动。这些驱动可以是直接驱动硬件工作的驱动层,如一般的字符驱动,也可以是驱动中间层,如文件系统中间层、TTY中间层、USB I/O中间层等。对于中间层驱动,下层硬件驱动将由这些中间层自身负责管理,而不再通过I/O子系统。如串口底层驱动将通过TTY中间层进行管理,而不再通过I/O子系统。

系统驱动表底层实现是一个数组,数组元素数目在VxWorks内核初始化过程中初始化I/O子系统时指定。iosInit函数用以初始化I/O子系统,iosInit函数调用原型如下。

        STATUS iosInit
            (
            int max_drivers,             /* maximum number of drivers allowed */
            int max_files,               /* max number of files allowed open at once */
            char *nullDevName           /* name of the null device (bit bucket) */
            );

其中:

● 参数1(max_drivers)指定系统驱动表元素数目,即系统最多支持的驱动数。

● 参数2(max_files)指定系统同时打开的最大文件数,这个参数实际上指定了系统文件描述符表的元素数目。

● 参数3(nullDevName)指定了null设备的设备节点名,一般为“/null”。系统驱动表在内核中由drvTable表示,其声明如下:

        DRV_ENTRY *   drvTable;                /* driver entry point table */

在iosInit函数中根据传入的最大驱动数目对drvTable进行初始化,如以下代码所示。

        /* allocate driver table and make all entries null */
        size = maxDrivers * sizeof (DRV_ENTRY);
        drvTable = (DRV_ENTRY *) malloc ((unsigned) size);

系统驱动表中每个表项都是一个DRV_ENTRY类型的结构,该结构定义在h/private/iosLibP.h文件中,如下:

        typedef struct          /* DRV_ENTRY - entries in driver jump table */
            {
            FUNCPTR   de_create;
            FUNCPTR   de_delete;
            FUNCPTR   de_open;
            FUNCPTR   de_close;
            FUNCPTR   de_read;
            FUNCPTR   de_write;
            FUNCPTR   de_ioctl;
            BOOL  de_inuse;
        } DRV_ENTRY;

可以看出,DRV_ENTRY实际上就是一个函数指针结构,结构中的每个成员都指向一个完成特定功能的函数,这些函数与用户层提供标准函数的接口一一对应。成员de_inuse用以表示一个表项是否空闲。

iosInit函数创建系统驱动表,从以上代码示例来看,该表实际上是由drvTable指向的一个数组,数组大小由传入iosInit函数的第一个参数决定,每个表项在drvTable中的位置索引就作为驱动号。索引号为0的表项被内核预留,专门用做null设备的驱动号,故驱动号的分配实际上是从1开始的。

I/O子系统提供iosDrvInstall供驱动程序注册用,iosDrvInstall函数调用原型如下。

        int iosDrvInstall
            (
            FUNCPTR pCreate,    /* pointer to driver create function */
            FUNCPTR pDelete,    /* pointer to driver delete function */
            FUNCPTR pOpen,      /* pointer to driver open function */
            FUNCPTR pClose,     /* pointer to driver close function */
            FUNCPTR pRead,      /* pointer to driver read function */
            FUNCPTR pWrite,     /* pointer to driver write function */
            FUNCPTR pIoctl      /* pointer to driver ioctl function */
        );

一个设备驱动在初始化过程中一方面完成硬件设备寄存器的配置,另一方面就是向I/O子系统注册驱动和设备,从而使设备对用户可见。可以看到,iosDrvInstall函数参数为一系列的函数地址,这些函数对应了为用户层提供的标准接口函数。一个驱动无须提供以上所有函数的实现,对于无须实现的函数,直接传递NULL指针即可。iosDrvInstall函数的基本实现即遍历drvTable数组,查询一个空闲表项,用传入的函数地址对表项中各成员变量进行初始化,并将de_inuse设置为TRUE,最后返回该表项在数组中的索引作为驱动号。设备初始化函数将使用该驱动号调用iosDevAdd将设备添加到I/O子系统中。此后用户就可以使用iosDevAdd函数调用时设置的设备节点名对设备进行打开操作,打开后进行读写或控制等其他操作,完成用户要求的特定功能。

用户可在命令行下输入iosDrvShow,显示系统驱动表中当前存储的所有驱动。如图5-4所示为系统驱动表的一个简单示意图。

图5-4 系统驱动表示意图

5.2.3 系统文件描述符表

I/O子系统维护的第三张表就是系统文件描述符表,即当前系统范围内打开的所有文件描述符都将存储在该表中。文件描述符表底层实现上也是一个数组,正如设备驱动表表项索引用做驱动号,文件描述符表表项索引被用做文件描述符ID,即open函数返回值。对于文件描述符,有一点需要注意:标准输入、标准输出、标准错误输出虽然使用0、1、2三个文件描述符,但是可能在系统文件描述符表中只占用一个表项,即都使用同一个表项。VxWorks内核将0、1、2三个标准文件描述符与系统文件描述符表中的内容分开进行管理。实际上,系统文件描述符中的内容更多的是针对硬件设备,即使用一次open函数调用就占用一个表项。0、1、2三个标准文件描述符虽然占用ID空间(即其他描述符此时只能从3开始分配),但是其只使用了一次open函数调用,此后使用ioGlobalStdSet函数对open返回值进行了复制。

以下是usrConfig.c文件中对于三个标准文件描述符的初始化代码。

        consoleFd = (-1);
        if (3 > 0){
            ttyDrv();
            for (ix = 0; ix < 3; ix++)
            {
                  sprintf (tyName, "%s%d", "/tyCo/", ix);
                  (void) ttyDevCreate (tyName, sysSerialChanGet(ix), 512, 512);
                  if (ix == 0)
                  {
        strcpy (consoleName, tyName);
        consoleFd = open (consoleName, 2, 0);
        (void) ioctl (consoleFd, 4, 115200);
        (void) ioctl (consoleFd, 3, (0x01 | 0x02 | 0x04 |
                                  0x10 | 0x08 | 0x20 | 0x40));
                  }
            }
        }
        ioGlobalStdSet (0, consoleFd);
        ioGlobalStdSet (1, consoleFd);
        ioGlobalStdSet (2, consoleFd);

可以看到,usrRoot函数中将“/tyCo/0”作为了三个标准输入/输出,此处只使用了一次open函数调用,语句如下:

        consoleFd = open (consoleName, 2, 0);

实际上,consoleFd=3,因为标准输入/输出占用了0、1、2三个文件描述符,所以系统文件描述符表中存储的描述符最小值就是3。此后使用ioGlobalStdSet函数将这个描述符“3”指向的设备作为0、1、2三个描述符的默认设备。即此处将串口作为了标准输入/输出设备。内核将0、1、2三个文件描述符预留给了标准输入/输出,并将其与系统文件描述符表中的表项隔离开来,内核专门使用ioStdFd数组表示0、1、2三个文件描述符指向的具体系统文件描述符表中哪个表项。

        int ioStdFd [3];       /* global standard input/output/error */

所以ioGlobalStdSet(0,consoleFd); 语句实际上完成如下工作:

        ioStdFd[0] = consoleFd;

其他两条语句的实际结果为:

        ioStdFd[1] = consoleFd;
        ioStdFd[2] = consoleFd;

而consoleFd等于3,实际上是系统文件描述符表中的第一个表项,其索引为0,但是在作为文件描述符返回时,基于0、1、2已被预留为标准输入/输出,故做加3处理,实际上,系统文件描述符表项索引作为文件描述符返回时都做加3处理。

当使用一个文件描述符进行操作时,如调用write函数,内核首先检查文件描述符是否是0、1、2 标准输入/输出描述符,如是,则依次为索引查询ioStdFd,以ioStdFd[fd]作为索引查询系统文件描述符表,获得驱动号,进而索引系统驱动表,调用对应表项de_write指向的函数,完成对设备的写入操作;如果文件描述符大于2,表示这是一个普通的文件描述符,那么就直接以该描述符作为索引查询系统文件描述表,获得驱动号,进而索引系统驱动表,调用相关函数。

系统文件描述符表中每个表项都是一个FD_ENTRY类型的结构,该结构定义在h/private/iosLibP.h中,代码如下。

        typedef struct          /* FD_ENTRY - entries in file table */
              {
              DEV_HDR *     pDevHdr; /* device header for this file */
              int  value;         /* driver's id for this file */
              char *    name;          /* actual file name */
              int       taskId;        /* task to receive SIGIO when enabled */
              BOOL      inuse;         /* active entry */
              BOOL obsolete;     /* underlying driver has been deleted */
              void *    auxValue;     /* driver specific ptr, e.g. socket type */
              void *    reserved;     /* reserved for driver use */
        } FD_ENTRY;

注意

FD_ENTRY结构的第一个成员就是DEV_HDR结构类型,该结构中存储了设备节点名和驱动号。FD_ENTRY结构中的value成员表示驱动附加信息,并非驱动号,实际上这个字段被用以保存底层驱动中open实现函数的返回值,这个返回值的意义重大,因为其后驱动中read、write等实现函数被调用时,I/O子系统就以这个返回值作为这些函数的第一个参数。所以,底层驱动open实现函数一般返回一个驱动自定义结构句柄。name成员变量按理应该设置为文件名,但是DEV_HDR结构中已经有设备节点名(也就是文件名),故该成员变量当前被设置为NULL,节省了内存空间。

系统文件描述符表由内核变量fdTable指向,该变量声明如下。

        FD_ENTRY *    fdTable;            /* table of fd entries */

fdTable的初始化在iosInit函数中完成,该函数调用原型如前文所示,传入该函数的第二个参数指定了fdTable数组的大小,该变量的初始化代码示例如下。

        size = maxFiles * sizeof (FD_ENTRY);
        fdTable = (FD_ENTRY *) malloc ((unsigned) size);

用户程序每调用一次open函数,系统文件描述符表中就增加一个有效表项,直到数组满,此时open函数调用将以失败返回。表项在表中的索引偏移3后作为文件描述符返回给用户,作为接下来其他所有操作的文件句柄。

用户可在命令行下输入iosFdShow,显示系统文件描述附表中当前所有的有效表项。如图5-5所示为系统文件描述符表的一个简单示意图。

图5-5 系统文件描述符表示意图

注意

每个文件描述符的确定由对应的设备文件被打开的时机决定,一般而言,串口被用做标准输入/输出是最早被打开的设备,其他设备只在需要时由用户程序打开。当然这也不可一概而论。

图5-5中也显示了0、1、2三个标准文件描述符对系统文件描述符表的索引。此时“/tyCo/0”设备同时也被用于标准输入/输出。ioStdFd数组有且仅有三个元素,0号元素表示标准输入,1号元素表示标准输出,2号元素表示标准错误输出。ioStdFd数组索引本身表示文件描述符,而元素内容表示实际操作设备时使用的文件描述符,usrRoot函数将ioStdFd三个元素均设置为3,即第一个串口设备对应的文件描述符“3”,也就是使用第一个串口设备作为标准输入/输出设备。

5.2.4 内核三张表之间的联系

下面我们以一个open调用为例,介绍I/O子系统维护的三张表是如何相互协作完成用户请求的。

如图5-6、图5-7所示,用户调用open函数打开“/xx0”文件,VxWorks内核I/O子系统将进行如下一系列响应。

图5-6 用户请求服务过程A

图5-7 用户请求服务过程B

① 其使用文件路径名匹配系统设备表,查询一个匹配设备。此处在设备列表中找到了一个匹配的设备。

② 其在系统文件描述符表中预留一个空闲项,用以创建一个文件描述符,如果后续调用成功,将以这个空闲项对应的索引(偏移3)值返回给用户,作为文件描述符使用。

③ 其根据设备列表匹配项中的信息得到驱动号,进而以此驱动号为索引从系统驱动表中获取底层设备驱动对应用户层open调用的响应函数x_open。x_open的第一个参数被设置为对应的硬件设备结构,第二个参数为除去设备名本身余下的部分,此处为NULL,第三、四个参数为用户传入的权限和模式参数。x_open将完成硬件设备的配置、使能工作、注册中断等,为用户接下来可能的读写设备操作做好准备。x_open同时对传入的第一个参数(设备结构)进行初始化,这个结构将在后续操作中一直被底层驱动使用。

④ 底层驱动返回设备结构,表示底层打开设备成功,否则返回NULL或ERROR表示调用失败。

⑤ I/O子系统对文件描述符表中预留的空闲项进行初始化,填入驱动号和设备结构。

⑥ 最后,open函数调用返回一个文件描述符,这个描述符是文件描述符之前被预留空闲项(现在已得到初始化,被使用)在表中的索引值(偏移3)。此处即文件描述符表中的第一个表项,即fd=0+3=3。