计算机体系结构基础(第3版)
上QQ阅读APP看书,第一时间看更新

7.3 设备的探测及驱动加载

PCI总线于20世纪90年代初提出,发展到现在已经逐渐被PCIE等高速接口所替代,但其软件配置结构却基本没有发生变化,包括HyperTransport、PCIE等新一代高速总线都兼容PCI协议的软件框架。

在PCI软件框架下,系统可以灵活地支持设备的自动识别和驱动的自动加载。下面对PCI的软件框架进行简要说明。

在PCI协议下,IO的系统空间分为三个部分:配置空间、IO空间和Memory空间。配置空间存储设备的基本信息,主要用于设备的探测和发现;IO空间比较小,用于少量的设备寄存器访问;Memory空间可映射的区域较大,可以方便地映射设备所需要的大块物理地址空间。

对于X86架构来说,IO空间的访问需要使用IO指令操作,Memory空间的访问则需要使用通常的load/store指令操作。而对于MIPS或者LoongArch这种把设备和存储空间统一编址的体系结构来说,IO空间和Memory空间没有太大区别,都使用load/store指令操作。IO空间与Memory空间的区别仅在于所在的地址段不同,对于某些设备的Memory访问,可能可以采用更长的单次访问请求。例如对于IO空间,可以限制为仅能使用字访问,而对于Memory空间,则可以任意地使用字、双字甚至更长的Cache行访问。

配置空间的地址偏移由总线号、设备号、功能号和寄存器号的组合得到,通过对这个组合的全部枚举,可以很方便地检测到系统中存在的所有设备。

以HyperTransport总线为例,配置访问分为两种类型,即Type0和Type1,其区别在于基地址和对总线号的支持。如图7.3所示,只需要在图中总线号、设备号、功能号的位置上进行枚举,就可以遍历整个总线,检测到哪个地址上存在设备。

图7.3 HyperTransport总线配置访问的两种类型

通过这种方式,即使在某次上电前总线上的设备发生了变化,也可以在这个枚举的过程中被探测到。而每个设备都拥有唯一的识别号,即图7.4中的设备号和厂商号,通过加载这些识别号对应的驱动,就完成了设备的自动识别和驱动的自动加载。

图7.4 标准的设备配置空间寄存器分布

图7.4为标准的设备配置空间寄存器分布。对于所有设备,这个空间的分布都是一致的,以保证PCI协议对其进行统一的检索。

图7.4中的厂商识别号(Vendor ID)与设备识别号(Device ID)的组合是唯一的,由专门的组织进行管理。每一个提供PCI设备的厂商都应该拥有唯一的厂商识别号,以在设备枚举时正确地找到其对应的驱动程序。例如英特尔的厂商识别号为0x8086,龙芯的厂商识别号为0x0014。设备识别号对于每一个设备提供商的设备来说应该是唯一的。这两个识别号的组合就可以在系统中唯一地指明正确的驱动程序。

除了通过厂商识别号与设备识别号对设备进行识别并加载驱动程序之外,还可以通过设备配置空间寄存器中的类别代码(Class Code)对一些通用的设备进行识别,并加载通用驱动。例如USB接口所使用的OHCI(Open Host Controller Interface,用于USB2.0 Full Speed或其他接口)EHCI(Enhanced Host Controller Interface,用于USB2.0 High Speed)XHCI(eXtensible Host Controller Interface,用于USB3.0),SATA接口所使用的AHCI(Advance Host Controller Interface,用于SATA接口)等。这一类通用接口控制器符合OHCI、EHCI、XHCI或AHCI规范所规定的标准接口定义和操作方法,类似于处理器的指令集定义,只要符合相应的规范,即使真实的设备不同,也能够运行标准的驱动程序。

所谓驱动程序就是一组函数,包含用于初始化设备、关闭设备或是使用设备的各种相关操作。还是以最简单的串口设备为例,如果在设备枚举时找到了一个PCI串口设备,它的驱动程序里面可能包含哪些函数呢?首先是初始化函数,在找到设备后,首先执行一次初始化函数,以使设备到达可用状态。然后是发送数据函数和接收数据函数。在Linux内核中,系统通过调用读写函数接口实现真正的设备操作。在发送数据函数和接收数据函数中,需要将设备发送数据和接收数据的方法与读写函数的接口相配合,这样在系统调用串口写函数时,能够通过串口发送数据,调用串口读函数时,能够得到串口接收到的数据。此外还有中断处理函数,当串口中断发生时,让中断能够进入正确的处理函数,通过读取正确的中断状态寄存器,找到中断发生的原因,再进行对应的处理。

当然,为了实现所有设备的共同工作,还需要其他PCI协议特性的支持。

首先就是对于设备所需IO空间和Memory空间的灵活设置。从图7.4可以看到,在配置空间中,并没有设备本身功能上所使用的寄存器。这些寄存器实际上是由可配置的IO空间或Memory空间来索引的。

图7.4的配置空间中存在6组独立的基址寄存器(Base Address Register,简称BAR)。这些BAR一方面用于告诉软件该设备所需要的地址空间类型及其大小,另一方面用于接收软件给其配置的基地址。

BAR的寄存器定义如图7.5所示,其最低位表示该BAR是IO空间还是Memory空间。BAR中间有一部分只读位为0,正是这些0的个数表示该BAR所映射空间的大小,也就是说BAR所映射的空间为2的幂次方大小。BAR的高位是可写位,用来存储软件设置的基地址。

图7.5 BAR的寄存器定义

在这种情况下,对一个BAR的基地址配置方式首先是确定BAR所映射空间的大小,再分配一个合适的空间,给其高位基地址赋值。确定BAR空间大小的方法也很巧妙,只要给这个寄存器先写入全1的值,再读出来观察0的个数即可得到。

对PCI设备的探测和驱动加载是一个递归调用过程,大致算法如下:

1)将初始总线号、初始设备号、初始功能号设为0。

2)使用当前的总线号、设备号、功能号组成一个配置空间地址,这个地址的构成如图7.3所示,使用该地址,访问其0号寄存器,检查其设备号。

3)如果读出全1或全0,表示无设备。

4)如果该设备为有效设备,检查每个BAR所需的空间大小,并收集相关信息。

5)检测其是否为一个多功能设备,如果是则将功能号加1再重复扫描,执行第2步。

6)如果该设备为桥设备,则给该桥配置一个新的总线号,再使用该总线号,从设备号0、功能号0开始递归调用,执行第2步。

7)如果设备号非31,则设备号加1,继续执行第2步;如果设备号为31,且总线号为0,表示扫描结束,如果总线号非0,则退回上一层递归调用。

通过这个递归调用,就可以得到整个PCI总线上的所有设备及其所需要的所有空间信息。有了这些信息,就可以使用排序的方法对所有的空间从大到小进行分配。最后,利用分配的基地址和设备的ID信息,加载相应的驱动就能够正常使用该设备。

下面是从龙芯3A处理器PCI初始化代码中抽取出的程序片段。通过这个片段,可以比较清楚地看到整个软件处理过程。


void_pci_businit(intinit)
{
  ……


  for(i=0,pb=pci_head;i<pci_roots;i++,pb=pb->next){   #这里的pci_roots用于表示系统中有多少
                                                        #个根节点, 通常的计算机系统中都为1
    _ pci_ scan_ dev(pb, i, 0, init)
  }
  ……
  _ setup_ pcibuses(init)                           #对地址窗口等进行配置
}


static void _ pci_ scan_ dev(struct pci_ pdevice *dev, int bus, int device, int initialise)
{
  for(; device<32; device++){
    _ pci_ query_ dev(dev, bus, device, initialize); #对本级总线, 扫描所有32个设备
                                                         #位置, 判断是否存在设备
  }
}
static void _pci_query_dev(struct pci_device *dev, int bus, int device, int initialise)
{
  ……

  misc = _pci_conf_read(tag, PCI_BHLC_REG);
  if(PCI_HDRTYPE_MULTIFN(misc)){                 #检测是否为多功能设备
    for(function=0;function<8;function++){
      tag = _pci_make_tag(bus,device,function);
      id  = _pci_conf_read(tag, PCI_ID_REG);
      if(id==0 || id==0xFFFFFFFF){
        continue;
      }
      _pci_query_dev_func(dev,tag,initialise);
    }
  } else {
    _pci_query_dev_func(dev,tag,initialise);
  }
}


void _pci_query_dev_func(struct pci_device *dev, pcitag tag, int initialise)
{
  ……
  class = _pci_conf_read(tag, PCI_CLASS_REG);         #读取配置头上的类别信息
  id    = _pci_conf_read(tag, PCI_ID_REG);               #读取配置头上的厂商ID、设备ID
  ……
    if(PCI_ISCLASS(class,PCI_CLASS_BRIDGE,PCI_SUBCLASS_BRIDGE_PCI)){  #对于桥设备,
                                                                         #需要递归处理下级总线
      ……
      pd->bridge.pribus_num = bus;                       #设置桥上的总线号信息
      pd->bridge.secbus_num = ++_pci_nbus;
      ……
      _pci_scan_dev(pd, pd->bridge.secbus_num, 0, initialise); #开始递归调用
      ……
      /* 收集整个下级总线所需要的资源信息 */
  } else {
    ……
    /* 收集本设备所需要的资源信息 */
  }
}

假设Memory空间的起始地址为0x40000000,在设备扫描过程中发现了USB控制器、显示控制器和网络控制器,三个设备对于Memory空间的需求如表7.3所示。

表7.3 三个设备的空间需求

在得到以上信息后,软件对各个设备的空间需求进行排序,并依次从Memory空间的起始地址开始分配,最终得到的设备地址空间分布如表7.4所示。

表7.4 三个设备的地址空间分布

经过这样的设备探测和驱动加载过程,可以将键盘、显卡、硬盘或者网卡等设备驱动起来,在这些设备上加载预存的操作系统,就完成了整个系统的正常启动。

如果把CPU比作一个大房间,至此,房间内灯火通明,门窗均已打开,门窗外四通八达。CPU及相关硬件处于就绪状态。