PCI总线在VxWorks中的实现

在最近发布的BSP中,风河公司基本上都提供了对PCI BUS的支持,本文主要探讨PCI BUS在VxWorks系统中的实现。

PCI Address Space and Memory Mapping

PCI BUS有三种地址空间:IO Space、Memory Space and Configuration Space。每个PCI设备都通过Configuration Space中的Base Address Registers(BAR)映射到内存或者IO空间,这样就不用像ISA BUS那样,通过硬件Jumpers来设置板卡的Address。PCI BUS的所有配置基本上都是通过Configuration Space的寄存器来控制的。但是,每个PCI Device都必须先配置才能使用,这意味着PCI Device的基地址和中断都必须被系统分配到资源,并且PCI Device能够对正常的PCI配置操作做出回应。

WRS提供一个名为pciConfigLib.c的标准库给用户访问PCI配置空间,该标准库使用PCI规范中定义的访问机制1和机制2来支持Host-Bridge,风河本身提供的第三个访问机制,名为机制0是针对非标准PCI Bridge的,机制0主要是靠调用BSP包中的特定routine来实现PCI配置空间的read/write的,与此同时,这些特定的routine接口和前面的介绍的机制1和机制2是相同的。pciConfigLib.c提供访问PCI配置空间内任何寄存器的routine,该库同样提供一些用于扫描PCI BUS寻找特定PCI 设备实例的方法,另外该库也提供一些简单的配置接口用于配置简单的PCI设备。

PCI Interrupt Handling

PCI 规范并没有详细说明PCI中断信号是如何路由到中断控制器的。每个PCI设备都有4个可用的中断PIN,分别命名为A,B,C和D。每个单功能的PCI设备都被要求使用中断PIN A来产生中断,而对于多功能PCI设备,每个功能使用一个中断PIN,但是根据PCI规范,每个PCI设备最多可提供8个功能,这样就必须两个功能共用一个中断PIN。当产生PCI中断时,PCI中断处理系统需要调用多个中断服务程序,那么最简单的方法就是每个ISR都调用一遍,ISR必须有能力判断该次中断源是否是自己产生的,如果不是,则立即返回,并接着会调用下一个中断服务程序。

pciIntLib.c提供一些routine来挂接多个ISR到一个中断LINE上,该库通过挂接一个特殊的ISR,该ISR会遍历一个中断链表,所有共用同一个中断的ISRs都被放在这个链表中。pciIntConnect()用于将设备的ISR挂接到中断链表上,而pciIntDisConnect()用于删除中断链表上的一个ISR。

例子:

pciInitConnect(Vector, ISR1, PARAM1);
pciInitConnect(Vector, ISR2, PARAM2);
pciInitConnect(Vector, ISR3, PARAM3);

上面3个语句把ISR1,ISR2,和ISR3分别挂接到中断向量为Vector的链表intList里面,那么当中断发生时,会执行下面一个函数:

void sISR(void)
{
       while(intList->next !=NULL)
        {
                (*intList->INT_ISR)(PARAM);/*分别调用ISR1,ISR2,ISR3,没写很具体,只是个大概理解*/
        }
}

VxWorks中的PCI 配置策略

在BSP中,必须定义宏INCLUDE_PCI来支持PCI BUS,宏PCI_CFG_TYPE必须定义为一下几种类型:

静态配置:PCI_CFG_FORCE

这种方式要求程序员必须手动通过数据表、配置宏或者其他方法来配置每个PCI设备,PCI设备的基地址和使用的中断号必须事先知道。

动态配置:PCI_CFG_AUTO

这种配置方法是最常用的,它通过扫描PCI总线并且给每一个found的设备赋予独立的内存或者IO地址,这也是典型的X86 BIOS的PCI初始化方式。程序员并不需要事先知道分配给PCI设备的系统资源,这个功能主要被实现在pciAutoConfigLib.c模块中。

未配置:PCI_CFG_NONE

这种方法主要是预留给那些不能使用上面的VxWorks配置方法的设备的。此时,所有的PCI设备都是在VxWorks内核启动之前配置好的,这种方法的困难在于VxWorks内核并没有在扫描过程中系统分配给PCI设备的资源信息,如果此时启用MMU,则在使用PCI设备之前,必须把PCI设备使用的地址动态映射到MMU。

PCI Initialization Sequences

当VxWorks内核起来之后,PCI设备的第一次使用都是必须在调用sysHwInit2()例程之后。但是由于MMU内存映射的初始化和激活是在例程sysHwInit()和sysHwInit2()之间的,所以推荐的PCI初始化顺序为:

  • sysHwInit() 默认的MMU table entries相当于将本地事务映射为PCI事务的HOST-Bridge的访问侧
  • sysHwInit2() 在该例程中,程序员必须静态配置所有的PCI设备,或者调用动态配置routine pciAutoConfig()

设备驱动程序初始化

传统的VxWorks的设备驱动模型一般都会设备创建routine called xxxDevCreate(),一般该routine都以设备的基地址和中断号做为参数。对于PCI设备,这种方式只适合于PCI_CFG_FORCE 配置策略,对于其他两种的配置策略,在编译阶段并不知道设备的基地址和中断号,这种情况下,在系统配置PCI设备完毕后,必须首先访问PCI配置空间获取相关的信息并传递给Device Driver。如果Drivers是针对于某些PCI设备的,那么Drivers通过设备的配置地址而不是内存或者IO地址来指定设备也是合乎情理的,但是PCI设备的配置地址并不是绝对和永恒不变的。

如果程序员使用总线号、设备号和功能号来定位地址的话,是可能会出现问题的,因为BIOS在扫描PCI BUS的过程中,会自动给BUS编号。例如某个设备的编号为(2,5,0)可能会由于通过PCI-PCI桥来扩展或者移除PCI设备而变为(1,5,0)。

PCI Device Driver的初始化应该是通过访问设备的配置空间来获取地址和其他对控制设备来说是必须的信息。如果这些信息已成功获取,那么PCI Device Driver的初始化就和以往一样了。在正常的设备操作过程中,不应该去访问PCI的配置空间。现在大多数PCI设备都把配置空间和其他寄存器都映射到内存空间,一般说来,访问Memory or IO Space比配置空间更加高效。

Dynamic MMU Mapping

和VME总线的模型一样,PCI主从访问必须根据标准的宏来操作,这些宏定义了BUS之间的对应关系。一般使用3个宏定义来描述两个总线之间的对应关系。

PCI Master Access Windows

第一个为host side的地址,采用宏PCI_XXX_LOCAL来定义,第二个为remote side 的地址,采用宏PCI_XXX_BUS来定义,第三个为描述内存大小的宏,采用PCI_XXX_SIZE来定义。可以通过设定PCI_XXX_SIZE宏为0来禁止相关的映射。

针对普通的Host-Bridge,从内存映射的角度看,有三种常用的映射方式:第一种为把本地内存访问映射为PCI IO访问,第二种把本地的内存访问映射为PCI MEMIO访问,这种方式是不可以prefectchable的,第三种是把本地的内存访问映射为PCI MEM 访问,这种方式可以prefectchable。由于主桥没有完整的详细规范,因此映射方式还不止这些,也有使用PCI-IACK signaling这种映射方式的。

The following is a typical excerpt from config.h in a typical PCI capable BSP:
/* Master window allows CPU to access PCI I/O addresses */
#define PCI_MSTR_IO_LOCAL 0xC0000000
#define PCI_MSTR_IO_BUS 0x00000000
#define PCI_MSTR_IO_SIZE 0x00010000

/* Master window allows CPU to access PCI Memory addresses (prefetch) */
#define PCI_MSTR_MEM_LOCAL 0x80000000
#define PCI_MSTR_MEM_BUS 0x00000000
#define PCI_MSTR_MEM_SIZE 0x01000000

/* Master window allows CPU to access PCI Memory (non-prefetch) */
#define PCI_MSTR_MEMIO_LOCAL 0x82000000
#define PCI_MSTR_MEMIO_BUS 0x00000000
#define PCI_MSTR_MEMIO_SIZE 0x01000000

/* Master window allows CPU to generate PCI_IACK cycles */
#define PCI_MSTR_IACK_LOCAL 0x8e000000
#define PCI_MSTR_IACK_BUS 0x0e000000

#define PCI_MSTR_IACK_SIZE 0x100

PCI中有三种地址空间:IO空间,配置空间,MEM空间。MEM和MEMIO并不是不同的地址空间,而是不同的总线操作,MEMIO是单纯的寄存器读写,而MEM访问会被翻译成该操作的CACHE类型,CACHE类型指明内存是否提前读取,写数据时是否采用CACHE的“write and invalidate”操作,其实这些都是考虑到CACHE数据完整性的问题。PCI规范并没有详细说明主桥如何把MEM访问映射为PCI总线事务,这是和特定主桥硬件相关的,需要参考主桥的相关手册。

为了访问特定的PCI内存位置,CPU必须该内存位置映射到本地的哪一个地址了,CPU操作都是使用本地地址,而不是设备的PCI地址。把PCI地址转换为本地地址的公式如下:

Local Addr = PCI addr + (PCI_MSTR_XXX_LOCAL - PCI_MSTR_XXX_BUS)

For example:

#define PCI_MEM2LOCAL(x) \
((int)(x) + PCI_MSTR_MEM_LOCAL - PCI_MSTR_MEM_BUS)
#define PCI_MEMIO2LOCAL(x) \
((int)(x) + PCI_MSTR_MEMIO_LOCAL - PCI_MSTR_MEMIO_BUS)
#define PCI_IO2LOCAL(x) \
((int)(x) + PCI_MSTR_IO_LOCAL - PCI_MSTR_IO_BUS)

PCI Slave Window

上面描述的都是主设备侧的映射方式,使用这些映射方式的主控器都是CPU,Slave Window是被总线上其他设备发起总线事务的目标设备,Slave Window千差万别,但是几乎所有的设备都有至少一种Slave Window来使本地地址对其他PCI设备例如DMA总线主控器或者CPU是可访问的。

/*
* Slave window that makes local memory visible to PCI
* devices
*/
PCI_SLV_MEM_LOCAL /* Local address of window */
PCI_SLV_MEM_BUS /* PCI Bus address of window */
PCI_SLV_MEM_SIZE /* window size,0 means disabled */
/*
* For X86 it is possible to have a slave window mapping
* PCI IO to Local IO
*/

PCI_SLV_IO_LOCAL /* Local address of window */
PCI_SLV_IO_BUS /* PCI Bus address of window */
PCI_SLV_IO_SIZE /* window size,0 means disabled */

对于X86机器,Slave Window将会把PCI IO事务映射为本地总线的IO请求。为了传递一个内存地址给Remote端设备,本地地址必须被翻译为相应的PCI地址,计算本地地址翻译为PCI地址的公式如下:

PCI addr = Local Addr + (PCI_SLV_XXX_BUS - PCI_SLV_XXX_LOCAL)

例如:

#define LOCAL2PCI_MEM(x) \
((x) + PCI_SLV_MEM_BUS - PCI_SLV_MEM_LOCAL)