Chinaunix首页 | 论坛 | 博客
  • 博客访问: 415452
  • 博文数量: 76
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 100
  • 用 户 组: 普通用户
  • 注册时间: 2014-08-25 16:47
个人简介

嵌入式linux爱好者

文章分类

全部博文(76)

文章存档

2019年(37)

2018年(3)

2017年(24)

2016年(7)

2015年(1)

2014年(4)

我的朋友

分类: LINUX

2018-08-09 23:15:57

原文地址:PCI总线驱动概要 作者:

 PCI总线驱动概要


在《Linux内核修炼之道》里,文章作者对如何使用Kconfig和Makefile定位内核源码有精彩的阐述。并且该作者还在《Linux那些事儿 之 我是PCI》系列文章中对X86架构下的PCI总线和设备驱动做了很详细的分析。由于工作需要,我在此基础上分析了ARM体系结构的PCI总线驱动,并把心得和笔记整理一下,或许对日后解析其他子系统有所帮助。以下涉及到的Linux内核源码均为2.6.23版本。

PCI是一种总线标准。一般已经形成xxx标准的东西,尤其是总线这样可以被叫做“子系统”的东西,在Linux内核里基本上都已经得到了很好的支持。内核一般把PCI总线这类子系统的驱动分成三层:总线驱动、协议、设备驱动。其中Linux内核早已把协议层实现完毕,最大程度地精简了总线驱动和设备驱动这两层与硬件密切相关的工作。

PCI设备驱动相关知识在《Linux设备驱动程序 第三版》中已经有了详细介绍。在这一层里工作的程序员不必知道PCI总线驱动的实现细节,他们只需要利用内核提供的PCI设备驱动相关接口,就可以成功地注册、配置、控制和卸载任何PCI设备,无论那个设备究竟是一个大容量存储器、一个USB HUB、还是一个视频采集卡。正因为有了那些内核接口,设备驱动层的程序员们才能把大量的精力用在实现具体设备的功能的完善和性能的优化上。假如此时你正在为你的笔记本电脑编写一个PCI设备的驱动(现在的笔记本都有PCIE热插拔的插槽,所以有这样的设备是很可能的~),那么你就正在PCI设备驱动层上工作着。

当然,对于设备驱动层的程序员来说,一切的前提是这块开发板上的PCI总线驱动已经有人写好了,因为如果没有底层总线驱动的支持,协议层和设备驱动层写得再精彩也只是空中楼阁。可是如果开发板上并没有PCI总线驱动怎么办?要想在一个裸体开发板上使用某个PCI设备,很明显,写驱动的程序员就必须先把PCI的总线驱动给写了,然后再写设备的驱动。

相比设备驱动,总线驱动显得更加神秘和高深莫测,因为它总是跟CPU体系结构、跟具体的芯片或芯片组密切相关,它的工作非常抽象,而不像设备驱动那么具体和显而易见。不论如何,PCI总线也是一种硬件设备,处理器访问和控制任何设备的方式,无非就是探测某些管脚的电平的高低,以及把某些管脚的电平拉高或拉低(先不管这个过程中间可能经历的千山万水)。再抽象一点,其实就是CPU向设备上的某个寄存器读和写,也就是访问传说中的“I/O端口和I/O内存”(有关I/O端口和I/O内存的知识,参考《Linux设备驱动程序》“第九章 与硬件通信”)。芯片手册上通常会用很多篇幅来介绍如何访问寄存器,以及寄存器与设备功能之间的关系。

注:中断也是设备控制的重要方式,但并非所有设备都需要中断。

不过就算知道了有关硬件I/O的知识,如果不知道PCI驱动的架构,不知道Linux内核应该在何时何地与芯片手册上介绍的寄存器发生关系,也不可能写出PCI总线的驱动来。既然Linux内核已经把PCI子系统的架子搭了起来,并且还派了一些内核接口来跑龙套,那我们写总线驱动的程序员作为主角总要知道自己应该唱哪一出戏。

话说Linux内核里有一帮子专业龙套。最著名的要算module_init()宏,它甚至作为半个主角出现在Helloworld模块那为数不多的几行代码里。不过它也只是__define_initcall()的若干标准Pose之一。这个叫做__define_initcall()的宏,套上不同的行头摆出不同的Pose,几乎出现在Linux内核的每个模块里。周杰伦看完内核代码也不禁感叹“你出现在我诗的每一页”。有关__define_initcall()的定义,都在linux/init.h文件中。

view plaincopy to clipboardprint?
#define __define_initcall(level,fn,id) /   
    static initcall_t __initcall_##fn##id __attribute_used__ /   
    __attribute__((__section__(".initcall" level ".init"))) = fn   
  
#define pure_initcall(fn)               __define_initcall("0",fn,0)   
  
#define core_initcall(fn)               __define_initcall("1",fn,1)   
#define core_initcall_sync(fn)          __define_initcall("1s",fn,1s)   
#define postcore_initcall(fn)           __define_initcall("2",fn,2)   
#define postcore_initcall_sync(fn)      __define_initcall("2s",fn,2s)   
#define arch_initcall(fn)               __define_initcall("3",fn,3)   
#define arch_initcall_sync(fn)          __define_initcall("3s",fn,3s)   
#define subsys_initcall(fn)             __define_initcall("4",fn,4)   
#define subsys_initcall_sync(fn)        __define_initcall("4s",fn,4s)   
#define fs_initcall(fn)                 __define_initcall("5",fn,5)   
#define fs_initcall_sync(fn)            __define_initcall("5s",fn,5s)   
#define rootfs_initcall(fn)             __define_initcall("rootfs",fn,rootfs)   
#define device_initcall(fn)             __define_initcall("6",fn,6)   
#define device_initcall_sync(fn)        __define_initcall("6s",fn,6s)   
#define late_initcall(fn)               __define_initcall("7",fn,7)   
#define late_initcall_sync(fn)          __define_initcall("7s",fn,7s)   
  
#define __initcall(fn)                  device_initcall(fn)   
  
#define module_init(x)                  __initcall(x);  
#define __define_initcall(level,fn,id) /
 static initcall_t __initcall_##fn##id __attribute_used__ /
 __attribute__((__section__(".initcall" level ".init"))) = fn

#define pure_initcall(fn)               __define_initcall("0",fn,0)

#define core_initcall(fn)               __define_initcall("1",fn,1)
#define core_initcall_sync(fn)          __define_initcall("1s",fn,1s)
#define postcore_initcall(fn)           __define_initcall("2",fn,2)
#define postcore_initcall_sync(fn)      __define_initcall("2s",fn,2s)
#define arch_initcall(fn)               __define_initcall("3",fn,3)
#define arch_initcall_sync(fn)          __define_initcall("3s",fn,3s)
#define subsys_initcall(fn)             __define_initcall("4",fn,4)
#define subsys_initcall_sync(fn)        __define_initcall("4s",fn,4s)
#define fs_initcall(fn)                 __define_initcall("5",fn,5)
#define fs_initcall_sync(fn)            __define_initcall("5s",fn,5s)
#define rootfs_initcall(fn)             __define_initcall("rootfs",fn,rootfs)
#define device_initcall(fn)             __define_initcall("6",fn,6)
#define device_initcall_sync(fn)        __define_initcall("6s",fn,6s)
#define late_initcall(fn)               __define_initcall("7",fn,7)
#define late_initcall_sync(fn)          __define_initcall("7s",fn,7s)

#define __initcall(fn)                  device_initcall(fn)

#define module_init(x)                  __initcall(x);

__define_initcall()宏的作用,其实是告诉编译器在编译的时候,把它所修饰的函数按照已经定义好的顺序放入程序段中。而内核在初始化的过程中,会调用到init/main.c中的一个名为do_initcalls()的函数,在那里,所有的被__define_initcall()修饰的函数会严格地按照标准顺序执行一遍。你懂的,这就是传说中的模块入口。其实模块本没有入口,但是内核的模块多了,就有了模块的入口;写模块时重启系统的次数多了,就有了模块的动态加载。

终于要说到PCI子系统框架的入口了。说起PCI框架入口,不得不佩服Linux内核的模块化布局。有关PCI子系统的源码,实际上只会出现在两个地方,一个是drivers/pci路径下,另一个是arch里的相关路径下。《我是PCI》文章作者已经把i386架构的PCI入口贴了出来,这里抄袭如下:


文件 函数 入口 内存位置 
arch/i386/pci/acpi.c pci_acpi_init subsys_initcall .initcall4.init 
arch/i386/pci/ common.c pcibios_init subsys_initcall .initcall4.init 
arch/i386/pci/i386.c pcibios_assign_resources fs_initcall .initcall5.init 
arch/i386/pci/ legacy.c pci_legacy_init subsys_initcall .initcall4.init 
drivers/pci/pci-acpi.c acpi_pci_init arch_initcall .initcall3.init 
drivers/pci/pci- driver.c pci_driver_init postcore_initcall .initcall2.init 
drivers/pci/pci- sysfs.c pci_sysfs_init late_initcall .initcall7.init 
drivers/pci/pci.c pci_init device_initcall .initcall6.init 
drivers/pci/probe.c pcibus_class_init postcore_initcall .initcall2.init 
drivers/pci/proc.c pci_proc_init __initcall .initcall6.init 
arch/i386/pci/init.c pci_access_init arch_initcall .initcall3.init 
 

PCI毕竟是Intel抻头搞出来的,所以i386架构自然是先吃到了螃蟹。不过到底还是第一次吃,所以吃得并不风雅,反而显得冗余罗嗦,光是在arch/i386/pci路径下,就有好几个入口函数。

后来者之所以能够居上,通常是既借鉴了前人的经验,又总结了前人的教训。想来Arm对PCI的支持要比X86晚一些,所以Arm架构下PCI子系统的initcall函数要少一些——通常来说,arch/arm/mach-xxx/路径下基本上只有一个用subsys_initcall()修饰的PCI子系统相关的初始化函数。不过PCI子系统在Arm下的初始化逻辑也要复杂一点,因为X86有bios帮它打杂,而Arm的一切都要靠内核自力更生。

《我是PCI》文章里用了很长篇幅介绍如何确定这些initcall函数的顺序。其实只有三点:

第一,先看它们在内存中的位置,位置靠前的先被执行。所以被postcore_initcall()修饰的函数就一定会在被subsys_initcall()修饰的函数之前执行。

第二,如果两个函数被放在相同的内存区里,则谁先被编译谁就先执行。而gcc的规则是“The order of files in $(obj-y) is significant.”,因此哪个函数所在的编译单元写在前面,谁就先执行。

第三,如果两个函数并不在同一个Makefile中,那么哪个Makefile先被调用,哪个函数就先被编译,因此也就会先被执行。

有了这几条判断依据,自然可以给出一个准确的initcall函数执行顺序(不包含i386的函数)。

view plaincopy to clipboardprint?
pcibus_class_init()   
pci_driver_init()   
acpi_pci_init()   
/* mach_spec_pci_init() */  
pci_init()   
pci_proc_init()   
pci_sysfs_init()  
pcibus_class_init()
pci_driver_init()
acpi_pci_init()
/* mach_spec_pci_init() */
pci_init()
pci_proc_init()
pci_sysfs_init()

从理论上讲,真正落实了如何与PCI总线通信的初始化工作的函数,就是注释的这一行。只不过,为各种类型的板子开发底层驱动的大侠们会把这个函数起成各种各样的名字(一般来说,叫“xxx_pci_init()”的比较常见)。所以我也只是随便给它起个一目了然的名字而已。

前面说过,Arm不像X86那样有bios给她当助理,所以在X86下可以由bios完成的工作,在Arm下都得由内核源码自己解决。有好奇心的童鞋现在一定想知道到底bios帮X86搞定了些什么事情,既然有好奇心,不妨自己搜一下CONFIG_PCI_BIOS这个宏一探究竟。Arm虽然没有bios,但Arm上的工程师也不想因为搞特殊化而打乱X86上已经正常运转的PCI总线驱动的初始化流程。Arm工程师发挥温州人的精神,自己伪造了一个软bios,于是arch/arm/kernel/bios32.c源码就应运而生。与之相对的头文件是include/asm-arm/mach/pci.h,它包含着简单而重要的bios32.c源码的外部接口:

view plaincopy to clipboardprint?
struct pci_sys_data;   
struct pci_bus;   
  
struct hw_pci {   
    struct list_head buses;   
    int     nr_controllers;   
    int     (*setup)(int nr, struct pci_sys_data *);   
    struct pci_bus *(*scan)(int nr, struct pci_sys_data *);   
    void        (*preinit)(void);   
    void        (*postinit)(void);   
    u8      (*swizzle)(struct pci_dev *dev, u8 *pin);   
    int     (*map_irq)(struct pci_dev *dev, u8 slot, u8 pin);   
};   
  
/*  
 * Per-controller structure  
 */  
struct pci_sys_data {   
    struct list_head node;   
    int     busnr;      /* primary bus number           */  
    u64     mem_offset; /* bus->cpu memory mapping offset    */  
    unsigned long   io_offset;  /* bus->cpu IO mapping offset        */  
    struct pci_bus  *bus;       /* PCI bus              */  
    struct resource *resource[3];   /* Primary PCI bus resources        */  
                    /* Bridge swizzling         */  
    u8      (*swizzle)(struct pci_dev *, u8 *);   
                    /* IRQ mapping              */  
    int     (*map_irq)(struct pci_dev *, u8, u8);   
    struct hw_pci   *hw;   
};   
  
/*  
 * This is the standard PCI-PCI bridge swizzling algorithm.  
 */  
u8 pci_std_swizzle(struct pci_dev *dev, u8 *pinp);   
  
/*  
 * Call this with your hw_pci struct to initialise the PCI system.  
 */  
void pci_common_init(struct hw_pci *);  
struct pci_sys_data;
struct pci_bus;

struct hw_pci {
 struct list_head buses;
 int  nr_controllers;
 int  (*setup)(int nr, struct pci_sys_data *);
 struct pci_bus *(*scan)(int nr, struct pci_sys_data *);
 void  (*preinit)(void);
 void  (*postinit)(void);
 u8  (*swizzle)(struct pci_dev *dev, u8 *pin);
 int  (*map_irq)(struct pci_dev *dev, u8 slot, u8 pin);
};

/*
 * Per-controller structure
 */
struct pci_sys_data {
 struct list_head node;
 int  busnr;  /* primary bus number   */
 u64  mem_offset; /* bus->cpu memory mapping offset */
 unsigned long io_offset; /* bus->cpu IO mapping offset  */
 struct pci_bus *bus;  /* PCI bus    */
 struct resource *resource[3]; /* Primary PCI bus resources  */
     /* Bridge swizzling   */
 u8  (*swizzle)(struct pci_dev *, u8 *);
     /* IRQ mapping    */
 int  (*map_irq)(struct pci_dev *, u8, u8);
 struct hw_pci *hw;
};

/*
 * This is the standard PCI-PCI bridge swizzling algorithm.
 */
u8 pci_std_swizzle(struct pci_dev *dev, u8 *pinp);

/*
 * Call this with your hw_pci struct to initialise the PCI system.
 */
void pci_common_init(struct hw_pci *);
 

既然这个头文件是在include/asm-arm路径下,就说明你不能够期待这里的接口也会出现在i386里,若真的有雷同,也绝对纯属巧合,或者只不过是程序员为了提示Arm上的某个接口与i386上的某个接口有着对应关系。所以从现在开始分析代码,就只能捡arch/arm路径下的代码来看了。你可以随便看看arch路径下的任何mach的pci初始化相关的subsys_initcall函数(通常名叫xxx_pci_init()函数),你会发现一个共同的特点就是,它们基本上只是调用了pci_common_init(struct hw_pci *)这个函数。而这个函数又早已经被维护arch体系结构的大侠写好了。调用一个已经被实现的内核接口——难道一切就这样简单地结束了吗?想得美。那不是还有一个参数么。一看参数恍然大悟,这又是一个Linux内核搭台,我们来唱戏的模式。

我们先来看看struct hw_pci这个结构体。.buses是一个链表,链表中的每个节点指向一个PCI子总线的私有数据(也就是struct pci_sys_data结构体)。.nr_controllers指出PCI总线有几个控制器,这通常要看芯片手册才能确定。下面的六个函数指针,都是pci_common_init()所需要的回调函数。pci_common_init()函数无疑是Arm体系下PCI子系统总线驱动初始化的剧本,我们虽然是主角,但也只能在固定的时间地点大背景下自我发挥。下面我们就先熟悉一下这个剧本吧。pci_common_init()函数就在arch/arm/kernel/bios32.c源码中。

view plaincopy to clipboardprint?
void __init pci_common_init(struct hw_pci *hw)   
{   
    struct pci_sys_data *sys;   
  
    INIT_LIST_HEAD(&hw->buses);   
  
    if (hw->preinit)   
        hw->preinit();   
    pcibios_init_hw(hw);   
    if (hw->postinit)   
        hw->postinit();   
  
    pci_fixup_irqs(pcibios_swizzle, pcibios_map_irq);   
  
    list_for_each_entry(sys, &hw->buses, node) {   
        struct pci_bus *bus = sys->bus;   
  
        if (!use_firmware) {   
            /*  
             * Size the bridge windows.  
             */  
            pci_bus_size_bridges(bus);   
  
            /*  
             * Assign resources.  
             */  
            pci_bus_assign_resources(bus);   
        }   
  
        /*  
         * Tell drivers about devices found.  
         */  
        pci_bus_add_devices(bus);   
    }   
}  
void __init pci_common_init(struct hw_pci *hw)
{
 struct pci_sys_data *sys;

 INIT_LIST_HEAD(&hw->buses);

 if (hw->preinit)
  hw->preinit();
 pcibios_init_hw(hw);
 if (hw->postinit)
  hw->postinit();

 pci_fixup_irqs(pcibios_swizzle, pcibios_map_irq);

 list_for_each_entry(sys, &hw->buses, node) {
  struct pci_bus *bus = sys->bus;

  if (!use_firmware) {
   /*
    * Size the bridge windows.
    */
   pci_bus_size_bridges(bus);

   /*
    * Assign resources.
    */
   pci_bus_assign_resources(bus);
  }

  /*
   * Tell drivers about devices found.
   */
  pci_bus_add_devices(bus);
 }
}
 

剧情梗概:

569行,pcibios_init_hw()函数初始化每一个controller,初始化它并且递归地枚举出它的子总线。在这个过程中会调用到.setup和.scan回调函数。如果需要,可以实现.preinit和.postinit这两个回调函数,它们俩会在pcibios_init_hw()函数执行之前和之后被调用。

573行,设置设备的irq号。按理说,根据《Linux设备驱动程序》里的介绍,PCI设备的irq号不是已经在配置空间里明确指定了吗,为什么还要多此一举呢?配置空间里的确是指定了,但是考虑到一个PCI设备有可能经过很多个级联的PCI-PCI桥才连接到主桥上,所以它到底使用哪个中断号还真是说不准啊。Linux内核眼睛里是揉不得沙子的,她说不准的事,一定会让我们来说准。.swizzle和.map_irq两个回调函数就是用来干这个的。值得一提的是,pci_fixup_irqs()函数中会使用pci_read_config_xxx()和pci_write_config_xxx()这些PCI子系统的内核接口,这说明在此之前内核必须已经知道如何访问PCI设备的配置空间。显然,聪明的你一定猜到,这件事也是在569行里面做的,至于究竟怎么做,后面会说到。

575-594行,遍历每个子总线,初始化子总线所在的PCI桥设备,然后把这条子总线上的设备(在569行里已经被枚举出来了)加入一个全局的设备列表中去(同时也会加入到子总线的设备列表中去)。

代码看到这里,已经敞亮了很多。我们可以确定的是.setup、.preinit、.postinit、.scan、.swizzle和.map_irq这六个回调函数就是Linux内核PCI总线驱动跟芯片手册发生关系的地方!

.setup、.preinit、.postinit这三个函数跟芯片手册密切相关。通常芯片手册会详细介绍PCI总线初始化时读写哪些寄存器以及具体步骤。如果controller只有一个,那么很可能就不需要实现.preinit、.postinit这两个函数了,但.setup函数必须实现。

另一个跟芯片手册密切相关而且必须实现的回调函数是.map_irq函数。它使device、slot和pin三个元素与一个irq中断号关联起来。

至于.swizzle函数,bios32.c源码中已经实现了一个名叫pci_std_swizzle()的通用函数,如果我们拿到的芯片手册上对swizzle这件事情没有特殊说明,就可以直接使用这个现成的函数。如果我们能够确定板子上根本不会有PCI子总线(根本没有PCI-PCI桥这种设备,很多嵌入式开发板上就没有这种设备),那么.swizzle函数就可以省略。

回调函数.scan是我们必须实现的。它的作用是,从主总线开始扫描总线上的PCI设备。一旦发现PCI-PCI桥,就初始化一条子总线,并且继续扫描子总线上的设备。就这样递归地扫描下去,直到形成一棵完整的PCI设备树,包含主总线和所有子总线上挂接的设备。整个过程听起来好像很复杂,事实上也确实如此。不过由于任何芯片的PCI总线的scan都是这个过程,因此Linux内核再次为我们准备好了一个通用函数:

static struct pci_bus *pci_scan_bus(int bus, struct pci_ops *ops, void *sysdata);

这个函数只是向我们额外索取一个struct pci_ops结构体,这个结构体只有.read和.write两个回调函数指针,作用是读取和写入PCI设备的配置空间——又是一个典型的内核搭台,我们唱戏的模式。

也就是说,只要我们实现了PCI设备配置空间的访问操作,则.scan的一切就尽在掌握。如果你有兴趣进入pci_scan_bus()简单地追踪一下,就可以知道这个ops参数通过pci_scan_bus_parented()-->pci_create_bus()的途径传递给主总线,然后又通过pci_scan_bus_parented()-->pci_scan_child_bus()-->pci_scan_bridge()-->pci_add_new_bus()-->pci_alloc_child_bus()的途径传递给了子总线。于是,PCI子系统中的所有子总线都知道如何访问设备的配置空间,那么PCI总线驱动初始化之后,PCI总线协议层向上提供的内核接口也就都可以正常运转了。因为这些协议说到底,都是在访问配置空间、I/O端口和I/O内存空间,而后两者又可以被CPU直接访问到。最后,访问PCI设备配置空间的方法,是与芯片手册密切相关的。

说明了.scan的流程和PCI设备树的概念,就可以解释一个名词了。前面好几次提到一个叫做controller的东西,却没有解释它到底是个什么东西。其实这个东西的全名叫做Host Controller,主控制器。这么说吧,每棵PCI设备树都是一棵单根树,而树根就是一个Host Controller。如果一块板子上有两个PCI Host Controller怎么办?很简单,那就有两棵独立的PCI设备树。那么Host Controller究竟是干啥的呢?前面也提到,编写PCI总线驱动,关键就是找到PCI子系统框架中与芯片手册发生关系的地方。OK,我们最终找到了,就是初始化、映射中断和访问配置空间。那么到底这个所谓的“关系”是怎么发生的呢?前面介绍过有关硬件I/O的知识,我们知道与硬件发生关系,靠的不是潜规则,而是读写硬件上的寄存器。你可以这样理解,这个Host Controller,就是一大片控制寄存器,CPU通过对它的读写,来控制PCI总线。所以,如果一块板子上有两个PCI Host Controller,那就有两条PCI主总线,它们俩绝对可以互不关心彼此的存在,此所谓相濡以沫不如相忘于江湖。当然,你也可能希望只用一个subsys_initcall就初始化两条主总线,struct hw_pci允许你这么干,因为它提供了.nr_controllers这个成员变量,你可以把它设置为2,或者更大值,你有多少个controller,你的.setup和.scan回调函数就会被调用多少次。

总结

1,Arm体系结构一般只需要一个被subsys_initcall()修饰的初始化函数。该函数的主体是pci_common_init()函数。

2,pci_common_init()函数需要一个已被初始化了的struct hw_pci结构体的指针。该结构体包含了PCI子系统的基本信息和回调函数。

3,在struct hw_pci结构体中,.setup()、.preinit()、.postinit()、.map_irq()等回调函数与芯片手册密切相关。不过.preinit()和.postinit()函数有时可以不实现。

4,在struct hw_pci结构体中,.swizzle函数已经有通用的实现。.scan函数的主体应为pci_scan_bus()函数。

5,pci_scan_bus()函数需要一个已被初始化了的struct pci_ops结构体的指针。该结构体包含了PCI设备配置空间的读取和写入函数。

6,读写配置空间与芯片手册密切相关。

关于PCIE总线驱动

PCIE在协议层完全兼容PCI。从PCIE子系统的协议层来看,新增加的功能(在drivers/pci/pcie路径下)全部通过访问设备的配置空间、I/O端口和I/O内存实现,没有为底层总线驱动添加任何负担。因此PCIE子系统与PCI子系统在总线驱动方面也没有本质不同,都沿用了PCI子系统的初始化过程,并需要程序员实现设备配置空间的读写函数。

阅读(10254) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~
评论热议
请登录后评论。

登录 注册