Chinaunix首页 | 论坛 | 博客
  • 博客访问: 830900
  • 博文数量: 281
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 2770
  • 用 户 组: 普通用户
  • 注册时间: 2009-08-02 19:45
个人简介

邮箱:zhuimengcanyang@163.com 痴爱嵌入式技术的蜗牛

文章分类
文章存档

2020年(1)

2018年(1)

2017年(56)

2016年(72)

2015年(151)

分类: LINUX

2017-03-01 15:03:54

本章导读
    嵌入式 Linux 产品开发,很大一部分工作量是驱动开发。驱动程序的好坏直接影响和决
    定着产品的稳定性,稳定的驱动程序是产品可靠性的基石,所以驱动开发对嵌入式 Linux 开发至关重要。
    编写 Linux 驱动,首先要具备相关的电路基础知识,只有了解了硬件的基本工作原理才
    能编写出可靠的驱动程序。同时,必须对 Linux 驱动体系有清晰的认识,才能将设备在 Linux下驱动起来。
    
2.1 Linux 内核模块
    2.1.1 Linux 和模块
        在 32 位系统上, Linux 内核将 4G 空间分为 0~3G 的用户空间和 3~4G 的内核空间[注]。
        用户程序运行在用户空间,可通过中断或者系统调用进入内核空间; Linux 内核以及内核模
        块则只能在内核空间运行。
        
        Linux 内核具有很强的可裁剪性,很多功能或者外设驱动都可以编译成模块,在系统运
        行中动态插入或者卸载,在此过程中无需重启系统。模块化设计使得 Linux 系统很灵活,可
        以将一些很少用到或者暂时不用的功能编译为模块,在需要的时候再动态加载进内核,可以
        减小内核的体积,加快启动速度,这对嵌入式应用极为重要。
        [注]:目前内核已经支持用户/内核空间 3:1、 2:2、 1:3 比例划分。
        
    2.1.2 编写内核模块
        1. 头文件
            内核模块需要包含内核相关头文件,不同模块根据功能的差异,所需要的头文件也不相
            同,但是是必不可少的。         
  1. #include <linux/module.h>
  2. #include <linux/init.h>
        2. 模块初始化
            模块的初始化负责注册模块本身。如果一个内核模块没有被注册,则其内部的各种方法
            无法被应用程序使用,只有已注册模块的各种方法才能够被应用程序使用并发挥各方法的实
            际功能。模块并不是内核内部的代码,而是独立于内核之外[注],通过初始化,能够让内核之
            外的代码来替内核完成本应该由内核完成的功能,模块初始化的功能相当于模块与内核之间
            衔接的桥梁,告知内核“我进来了, 我已经做好准备为您服务了”。
            [注]:当内核树内某部分代码被配置为模块时,可理解为:这部分代码已经不属于当前配置下的内核。
            
            模块的初始化定义通常如程序清单 2.1 所示。
                程序清单 2.1 模块初始化定义
                static int __init module_init_func(void)
                {
                    初始化代码
                }
                module_init(module_init_func);
            几点说明:
            ( 1) 模块初始化函数一般都需声明为 static,因为初始化函数对于其它文件没有任何意义;
            ( 2) __init 表示初始化函数仅仅在初始化期间使用,一旦初始化完毕,将释放初始化函数所占用的内存
                类似的还有__initdata;
            ( 3) module_init 是必须的,没有这个定义,内核将无法执行初始化代码。 module_init宏定义会在模块
                的目标代码中增加一个特殊的代码段,用于说明该初始化函数所在的位置。                 
            当使用 insmod 将模块加载进内核的时候,初始化函数的代码将会被执行。模块初始化代码只与内核模块管理
            子系统打交道,并不与应用程序交互。
            
        3. 模块退出
            当系统不再需要某个模块,可以卸载这个模块以释放该模块所占用的资源。模块的退出相当于告知内核“我要离开了,
            将不再为您服务了”。
            
            实现模块退出的函数常称为模块的退出函数或者清除函数,一般定义如程序清单 2.2所示。
            程序清单 2.2 模块退出函数
                static void __exit module_exit_func(void)
                {
                    模块退出代码
                }
                module_exit(module_exit_func);
            几点说明:
            ( 1) 模块退出函数没有返回值;
            ( 2) __exit 标记这段代码仅用于模块卸载;
            ( 3) module_exit 不是必须的。但是,没有 module_exit 定义的模块无法被卸载,如果需要支持模块卸载则
                    必须有 module_exit。
                    
            当使用 rmmod 卸载模块时,退出函数的代码将被执行。 模块退出代码只与内核模块管理子系统打交道,并不直接
            与应用程序交互。
            
        4. 许可证
            Linux 内核是开源的,遵守 GPL 协议,所以要求加载进内核的模块也最好遵循相关协议。为模块指定遵守的协议用
            MODULE_LINCENSE 来声明,如:
                MODULE_LICENSE("GPL");
            
            内核能够识别的协议有“ GPL”、“ GPL v2”、“ GPL and additional rights( GPL 及附加权利)”、
            “ Dual BSD/GPL( BSD/GPL 双重许可)”、“ Dual MPL/GPL( MPL/GPL 双重许可)”以及“ Proprietary(私有)”。
            如果一个模块没有指定任何许可协议,则会被认为是私有协议。采用私有协议的模块,在加载过程中会出现警告,
            并且不能被静态编译进内核。
            
        5. 符号导出
            Linux 2.6 中,所有的内核符号默认都是不导出的。如果希望一个模块的符号能被其它
            模块使用,则必须显式的用 EXPORT_SYMBOL 将符号导出。如:
            EXPORT_SYMBOL(module_symbol);
        
        6. 模块描述
            模块编写者还可以为所编写的模块增加一些其它描述信息,如模块作者、模块本身的描
            述或者模块版本等,例如:
                MODULE_AUTHOR("grant ");
                MODULE_DESCRIPTION("smdk2440 beep Driver");
                MODULE_VERSION("V1.00");
            模块描述以及许可证声明一般放在文件末尾。
            
        7. 编译
            模块代码编写完毕,需要进行编译,得到模块文件才能使用。编译模块需要内核代码,并配置和编译内核代码,
            就算有源码,但是没经过编译,也是不能用于编译模块的。编译模块的内核配置必须与所运行内核的编译配置一样,
            否则将有可能无法加载或者运行。
            
            在 Linux 2.6 中,编译内核很简单。假定一个模块文件 hello.c,欲编译得到 hello.ko 文
            件,则只需在 Makefile 文件中编写一行:
                obj-m := hello.o
                
            再假定内核源码在~/linux 目录下,则在 Shell 中输入:
                make -C ~/linux M=`pwd` modules
            就可以得到 hello.ko 模块文件。
            
            如果一个模块由 file1.c 和 file2.c 等多个文件组成, 要编译得到 module.ko 文件, 则makefile 内容如下:
                obj-m := module.o
                module-objs := file1.o file2.o
            
            当然,这样编译比较繁琐,利用 GNU make 的强大功能,重写 Makefile,简化编译。 程序清单 2.3 所示是 Linux 2.6
            在内核树之外编译内核模块的典型 Makefile 文件。
            
            程序清单 2.3 Linux 2.6 内核模块 Makefile 范例
            # Makefile2.6
            ifneq ($(KERNELRELEASE),)
            
                #kbuild syntax. dependency relationshsip of files and target modules are listed here.
                obj-m := beepdrv.o
            else
            
                PWD := $(shell pwd)
                KVER = 2.6.27.8
                KDIR := /work/system/linux-2.6.27.8
                
            all:
                $(MAKE) -C $(KDIR) M=$(PWD) modules
            clean:
                rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions
            
            endif
            
            这是 Linux 2.6 编译内核模块的通用 Makefile,只需修改 obj-m 和 KDIR 为实际环境的值,在 Shell 下输入
            make 就可以完成模块编译[注]。
            注:
                对 ARM 平台而言,如果内核源码的 Makefile 中没有指定 ARCH 和 CROSS_COMPILE的值, 则需要在 make
                命令中指定, 例如:$ make ARCH=arm CROSS_COMPILE= arm-linux-gnueabi-
                
        8. 加载和卸载
            加载模块使用 insmod 命令,卸载模块使用 rmmod 命令。例如加载和卸载 hello.ko 模块:
                #insmod hello.ko
                #rmmod hello.ko
                
            加载和卸载模块必须具有 root 权限。
            对于可接受参数的模块,在加载模块的时候为变量赋值即可,卸载模块无需参数。假如
            hello.ko 模块有变量 num,在插入的时候设置 num 的值为 8,则加载和卸载命令为:
                #insmod hello.ko num=8
                #rmmod hello.ko
            
    2.1.3 最简单的内核模块
        这是第一个内核模块程序,也是最简单的内核模块。仅仅完成模块的加载和卸载功能,
        同时在加载和卸载的时候打印提示信息,完整的代码如程序清单 2.4 所示。
        
        程序清单 2.4 第一个内核模块的代码
            #include
            #include
            
            static int __init hello_init(void)
            {
                printk("Hello, I'm ready!\n");
                return 0;
            }
            
            static void __exit hello_exit(void)
            {
                printk("I'll be leaving, bye!\n");
            }
            module_init(hello_init);
            module_exit(hello_exit);
            MODULE_LICENSE("GPL");
        
        
        可以看到,这个最简单的内核模块,涉及的知识点都是上一节所讲述的内容。唯一多了一点就是用 printk 函数打印信息。
        printk 函数是由内核定义并导出给模块使用的一个 printf 的内核版本,用法基本与 printf函数相同,
        不过 printk 不支持浮点数。
        
        编写 Makefile 文件,编译后得到 hello.ko 模块,插入内核将会打印初始化代码中的提
        示信息,卸载模块则会打印退出函数所打印的信息:
            # insmod hello.ko
            Hello, I'm ready!
            # rmmod hello
            I'll be leaving, bye!
            
        注意:
            如果在 PC Linux 下编译、插入/卸载驱动模块,由于各发行版控制台打印级别设置不同,
            可能在一些发行版上看不到提示信息打印,可以输入 dmesg 命令查看或者另打开一个终端,
            输入 tail -f /var/log/messages 命令,实时查看提示信息。
            
    2.1.4 带参数的内核模块
        1. 模块参数
            Linux 内核允许模块在加载的时候指定参数。模块接受参数传入能够实现一个模块在多
            个系统上运行,或者根据插入时参数的不同提供多种不同的服务。
            
            模块参数必须使用 module_param 宏来声明,通常放在文件头部。 module_param 需要 3
            个参数:变量名称、类型以及用于 sysfs 入口的访问掩码。模块最好为参数指定一个默认值,
            以防加载模块的时候忘记传参而带来错误。如下的示例在插入模块时候没有指定 num 参数
            的话,模块将会使用默认值 5:
                static int num = 5;
                module_param(num, int, S_IRUGO);
            说明:
            1) 内核模块支持的参数类型有: bool、 invbool、 charp、 int、 short、 long、 uint、 ushort
            和 ulong。
            2) 访问掩码的值在定义, S_IRUGO 表示任何人都可以读取该参数,但不能修改。
            3) 支持传参的模块需包含 moduleparam.h 头文件。
            
        2. 完整范例
            这一节将给出一个能够接受参数的模块范例,请与上一个范例对比,体会其中的差异和
            用法。如程序清单 2.5 所示的模块,可以接受一个整型参数 num 和一个字符串变量 whom,
            在加载模块的时候打印这两个变量的值。
            程序清单 2.5 可接受参数的内核模块

点击(此处)折叠或打开

  1. #include <linux/module.h>
  2. #include <linux/init.h>

  3. static int num = 3;
  4. static char *whom = "master";

  5. module_param(num, int, S_IRUGO);
  6. module_param(whom, charp, S_IRUGO);

  7. static int __init hello_init(void)
  8. {
  9.     printk(KERN_INFO "%s, I get %d\n", whom, num);
  10.     return 0;
  11. }

  12. static void __exit hello_exit(void)
  13. {
  14.     printk("I'll be leaving, bye!\n");
  15. }

  16. module_init(hello_init);
  17. module_exit(hello_exit);

  18. MODULE_LICENSE("GPL");
在 2.1.2 小节讲模块参数的时候提到, 接收参数的模块代码需要包含 moduleparam.h 文
        件,而程序清单 2.5中却没有,这是因为 moduleparam.h 文件已经包含在 module.h 文件中了。
        程序清单 2.5 第 12 行的 printk 语句中的 KERN_INFO 表示这条打印信息的级别。 printk
        能分级别打印, 这也是与 printf 不同的地方。

        Linux 内核在文件中定义了 7
个打印级别, 各级别的定义和说明如程序清单 2.6 所示。
            程序清单 2.6 Linux 内核定义的打印级别
            #define KERN_EMERG "<0>"     /* system is unusable */
            #define KERN_ALERT "<1>"     /* action must be taken immediately */
            #define KERN_CRIT "<2>"     /* critical conditions */
            #define KERN_ERR "<3>"         /* error conditions */
            #define KERN_WARNING "<4>"     /* warning conditions */
            #define KERN_NOTICE "<5>"     /* normal but significant condition */
            #define KERN_INFO "<6>"     /* informational */
            #define KERN_DEBUG "<7>"     /* debug-level messages */
                
        编译文件,得到内核模块,假定文件名为 hellop.ko。不带参数插入内核,各变量将使用默认值:
            # insmod hellop.ko
            master, I get 3
            
        在加载模块的时候指定参数的值:
            # insmod hellop.ko whom="MASTER" num=5
            MASTER, I get 5
            
        卸载模块:
            # rmmod hellop
            I'll be leaving, bye!

2.2 Linux 设备
    2.2.1 Linux 设备和分类
        Linux 系统中的设备可以分为字符设备、块设备和网络设备这 3 类。
        字符设备:
            字符设备是能够像字节流一样被访问的设备,当对字符设备发出读写请求,
            相应的 I/O 操作立即发生。 Linux 系统中很多设备都是字符设备,如字符终端、串口、键盘、
            鼠标等。在嵌入式 Linux 开发中,接触最多的就是字符设备以及驱动。
            
        块设备:
            块设备是 Linux 系统中进行 I/O 操作时必须以块为单位进行访问的设备,块设
            备能够安装文件系统。块设备驱动会利用一块系统内存作为缓冲区,因此对块设备发出读写
            访问,并不一定立即产生硬件 I/O 操作。 Linux 系统中常见的块设备有如硬盘、软驱等等。
        
        网络设备:
            网络设备既可以是网卡这样的硬件设备,也可以是一个纯软件设备如回环设
            备。网络设备由 Linux 的网络子系统驱动,负责数据包的发送和接收,而不是面向流设备,
            因此在 Linux系统文件系统中网络设备没有节点。对网络设备的访问是通过 socket调用产生,
            而不是普通的文件操作如 open/close 和 read/write 等。
            
    2.2.2 设备节点和设备号
        1. 设备节点
            设备(包括硬件设备)在 Linux 系统下,表现为设备节点,也称设备文件。设备文件是
            一种特殊的文件,它们存储在文件系统中(通常在/dev 目录下),但它们仅占用文件目录项
            而不涉及存储数据。事实上,它们仅仅记录了其所属的设备类别、主设备号和从设备号等设
            备相关信息。
            来看两个典型的设备文件的详细信息:
                root@gitserver:~$ ls -l /dev/ttyS0 /dev/sda1
                brw-rw---- 1 root disk 8, 1 2011-01-07 17:48 /dev/sda1
                crw-rw---- 1 root dialout 4, 64 2011-01-07 17:48 /dev/ttyS0
                以/dev/ttyS0 的信息为例,对其中几项进行说明:
                    crw-rw----          1 root dialout     4,     64       2011-01-07 17:48    /dev/ttyS0
                    设备类型   访问权限                 次设备号  主设备号                     设备文件名  
                    
            /dev/ttyS0 是设备节点名称, c 表示该设备是字符设备,主设备号为 4,从设备号为 64,
            该设备节点对应于系统的串口 0。    
            
            设备分为字符设备、块设备和网络设备,而网络设备没有设备节点,所以设备文件基本
            上就分为字符设备文件和块设备文件两类,在设备节点属性中,分别以 c 和 b 来表示,即 c
            表示字符设备节点文件, b 表示块设备节点文件。
            
            当程序打开一个设备文件时,内核就可以获取对应设备的设备类型、主设备号和次设备
            号等信息,内核也就知道了程序需要操作使用哪个设备驱动程序。在程序随后对这个文件的
            操作都会调用相应的驱动程序的函数,同时把从设备号传递给驱动程序。
            
        2. 设备编号
            设备编号由主设备号和从设备号构成。在 Linux 内核中,使用 dev_t 类型来保存设备编
            号。在 2.6 版本的 Linux 内核中, dev_t 是一个 32 位数,高 12 位是主设备号,低 20 位是次
            设备号。
            
            主设备号标识设备对应的驱动程序,告诉 Linux 内核使用哪个驱动程序驱动该设备。如
            果多个设备使用同一个驱动程序,则它们拥有相同的主设备号。例如/dev/ttyS0~3 这 4 个设
            备,拥有相同的主设备号 4,说明它们使用同一份驱动:
            root@EasyARM-iMX283 ~# ls /dev/ttyS* -l
            crw-rw----    1 root     uucp      242,   0 Jan  1 01:29 /dev/ttySP0
            crw-rw----    1 root     uucp      242,   1 Jan  1 01:29 /dev/ttySP1
            crw-rw----    1 root     uucp      242,   2 Jan  1 01:29 /dev/ttySP2
            crw-rw----    1 root     uucp      242,   3 Jan  1 01:29 /dev/ttySP3
            crw-rw----    1 root     uucp      242,   4 Jan  1 01:29 /dev/ttySP4

                
            主设备号由系统来维护,尽管 2.6 Linux 可以容纳大量的设备,但是在使用主设备号的
            时候,注意一定不要使用系统已经使用的主设备号。一般来说, 231~239 这几个设备号是系
            统没有分配的,用户可以自行安排使用。当前运行系统占用了哪些主设备号,可通过查看
            /proc/devices 文件得到。例如,在shell中断输入命令:
            # cat /proc/devices
            
            从设备号也称次设备号,用于确定该设备文件所指定的设备。如果一个设备驱动可以驱
            动一组相似的设备,此时就需要依赖于次设备号对这些外设进行区分。
            
            获取一个设备的设备编号,应当使用中定义的宏,而不应当对设备号的
            位数和表述结构做任何假设,因为这样会导致不兼容以前的内核,或者未来版本设备号结构
            和表述方式发生变化。例如获取一个设备 dev 的主次设备号,可用:
                MAJOR(dev_t dev);
                MINOR(dev_t dev);
            如果已知一个设备的主次设备号,要转换成 dev_t 类型的设备编号,则应当使用:
                MKDEV(int major, int minor);
                
        3. 获取和释放设备编号
            在建立一个设备节点之前,驱动程序首先应当为这个设备获得一个可用的设备号,注销
            设备需要释放所占用的设备号。设备号的生命周期是从设备注册到设备注销,在此期间,所
            占用的设备号不能被其它驱动使用。 Linux 内核支持静态获取和动态获取设备号,下面以字
            符设备为例讲述设备号的获取与释放。
            
            (1)静态获取主设备号
                静态设备号的方式适用于下列情况:
                1) 该驱动只在特定系统运行,且系统设备号使用情况明确;
                2) 系统应用所要求;如为了快速启动等。
                如果要从系统获得几个或者几个既定的主设备号, 可用 register_chrdev_region 函数来获
                取。该函数在中声明, 函数定义如下:    
                    int register_chrdev_region(dev_t first,unsigned int count,char *name);
                这个函数可以向系统注册 1 个或者多个主设备号, first 是起始编号, count 是主设备号
                的数量, name 则是设备名称。注册成功返回 0,否则返回错误码。
            
            (2)动态获取主设备号
                如果事先不知道设备的设备号,或者一个驱动可能在多个系统上运行,为了避免出现设
                备号冲突,必须采用动态设备号。调用 alloc_chrdev_region 函数可以从系统获得一个或者多
                个主设备号。
                alloc_chrdev_region 函数在中定义:
                    alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
                
                alloc_chrdev_region 函数可以从系统动态获得一个或者多个主设备号。 dev 用于保存已
                经获得的编号范围的第一个值, firstminor 是第一个次设备号,通常是 0, count 是获得的编
                号数量, name 是设备名称。
                动态获取得到的设备号,一定要用一个全局变量保存下来,以便卸载使用,否则该设备
                号将不能被释放。 程序清单 2.7 是一个动态获取设备号的使用范例。
                    程序清单 2.7 动态获取设备号
                        ret = alloc_chrdev_region(&devno, minor, 1, "char_cdev"); /* 从系统获取主设备号 */
                        major = MAJOR(devno); /* 保存获得的主设备号 */
                        if (ret < 0) {
                            printk(KERN_ERR "cannot get major %d \n", major);
                            return -1;
                        }
                
            (3)释放设备号
                在设备注销的时候必须释放占用的主设备号,调用 unregister_chrdev_region 可以释放设
                备号。函数原型:
                void unregister_chrdev_region(dev_t from, unsigned count);
                
    2.2.3 设备的注册和注销
        2.6 内核用 cdev 数据结构来描述字符设备,cdev 在中定义,如程序清单 2.8所示。
            程序清单 2.8 cdev 结构定义
                struct cdev {
                    struct kobject kobj;
                    struct module *owner;
                    const struct file_operations *ops;
                    struct list_head list;
                    dev_t dev;
                    unsigned int count;
                };
                
                kobj 是 2.6 内核设备模型的基本结构, cdev 可以被设备模型管理;
                owner 表示所属对象, 一般设置为 THIS_MODULE;
                ops 是与设备相关联的操作方法;
                dev 是 2.6 内核中设备的设备号。
            使用 cdev 大体步骤是先分配 cdev 结构,然后初始化,最后往系统添加,如果不再需要,可以从系统中删除。
            
            (1)分配 cdev 结构
                在注册设备之前,必须分配并注册一个或者多个 cdev 结构,可用 cdev_alloc 实现,如:
                struct cdev *char_cdev = cdev_alloc(); /* 分配 char_cdev 结构 */
                
            (2)初始化 cdev 结构
                初始化 cdev 结构通过调用 cdev_init()实现, cdev_init()函数原型:
                    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
                    
                参数 fops 用于指定设备的操作方法,在此结构中定义与设备相关的各种操作方法。假
                定一个设备需要实现除打开关闭之外,还需实现 read、 write 以及 ioctl 方法,则文件操作接
                口 fops 结构可以用程序清单 2.9 这样的方式定义。
                    程序清单 2.9 fops 结构定义
                        struct file_operations char_old_fops = {
                            .owner = THIS_MODULE,
                            .read = char_old_read,
                            .write = char_old_write,
                            .open = char_old_open,
                            .release = char_old_release,
                            .ioctl = char_old_ioctl
                        };
                定义好 fops 后, cdev 初始化很简单::
                    cdev_init(char_cdev, &char_cdev_fops); /* 初始化 char_cdev 结构 */
                    
            (3)往系统添加一个 cdev
                分配到 cdev 结构并初始化后, 就可以通过调用 cdev_add 将 cdev 添加到系统中了。不
                过在调用 cdev_add 之前,还需设置 cdev 的 owner 成员,一般设置为 THIS_MODULE,设置
                完毕通过 cdev_add 添加,如程序清单 2.10 所示。
                    程序清单 2.10 cdev_add 添加 cdev 设备
                        char_cdev->owner = THIS_MODULE;
                        if (cdev_add(char_cdev, devno, 1) != 0) { /* 增加 char_cdev 到系统中 */
                            printk(KERN_ERR "add cdev error!\n");
                            goto error1;
                        }
                必须检查 cdev_add 的返回值,因为 cdev_add 不一定保证成功,添加成功返回 0,失败返回返回错误码。
            
            (4) 删除 cdev    
                将一个 cdev 结构从系统删除, 调用 cdev_del()就可以了, 如:
                    cdev_del(char_cdev); /* 移除字符设备 */
                    
                在 2.6 的内核中,依然实现了 2.4 内核的字符驱动注册接口函数 register_chardev()和对应
                的注销函数 unregister_chrdev():
                    int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
                    void unregister_chrdev(unsigned int major, const char *name);
                    
                这两个函数封装实际上是对 cdev 的使用方法进行了封装,只是同一个主设备号允许的
                次设备号最多为 256 个,并且能一次性完成设备号和设备的注册与注销。尽管在很多文献里
                面都不建议再使用这对函数,担心将来版本不再支持这对函数,但是实际上在嵌入式 Linux 领
                域,使用的内核版本相对稳定,在满足次设备号的限制条件下,还是可以使用的,并且能够
                简化驱动编写。

阅读(930) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~