现在的位置: 首页 > 综合 > 正文

QEMU的PCI总线与设备(下)

2017年12月20日 ⁄ 综合 ⁄ 共 5083字 ⁄ 字号 评论关闭

在上文中,我们在QEMU中已经成功的虚拟了一个PCI桥和一个PCI设备,接下来我们就来给他们分配固定的IO基地址。

 

要给PCI设备分配固定的IO基地址,那么就需要先了解PCI设备是如何刷新和分配IO基地址的。 

1. PCI设备的重置与刷新

PCI在需要的时候,如第一次启动,IO重叠等就需要重置PCI设备,并且清空PCI bar上面的地址信息。主要调用函数pci_device_reset

 

void pci_device_reset(PCIDevice *dev)
{
    int r;

    ... ...

    ... ...

    dev->config[PCI_CACHE_LINE_SIZE] = 0x0;
    dev->config[PCI_INTERRUPT_LINE] = 0x0;
    for (r = 0; r < PCI_NUM_REGIONS; ++r) {    /*遍历所有的region,这个的region就是bar,清空region里面的IO地址*/
        PCIIORegion *region = &dev->io_regions[r];
        if (!region->size) {
            continue;
        }

        if (!(region->type & PCI_BASE_ADDRESS_SPACE_IO) &&
            region->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
            pci_set_quad(dev->config + pci_bar(dev, r), region->type);
        } else {

            /*用type将bar上所有的数据都覆盖,之前分配的IO基地址也没了*/
            pci_set_long(dev->config + pci_bar(dev, r), region->type);

            /*刷新设备*/

            pci_update_mappings(dev);

        }
    }

    /*刷新IO地址,更新IO读写映射*/
    pci_update_mappings(dev);
}

 

刷新IO地址函数展开如下:

static void pci_update_mappings(PCIDevice *d)
{
    PCIIORegion *r;
    int i;
    pcibus_t new_addr, filtered_size;

    for(i = 0; i < PCI_NUM_REGIONS; i++) {
        r = &d->io_regions[i];

        /* 如果没有注册region,那么不进行任何操作*/
        if (!r->size)
            continue;

         /* 得到设备bar上存储的基地址 */

        new_addr = pci_bar_address(d, i, r->type, r->size);

        /* bridge filtering */
        filtered_size = r->size;

         /* 如果分配了bar地址,那么比较设备地址与父桥的地址,看是否匹配*/
        if (new_addr != PCI_BAR_UNMAPPED) {
            pci_bridge_filter(d, &new_addr, &filtered_size, r->type);
        }

        /* 如果得到的新地址没有改变,大小也没变,那么不更新IO重映射,否则将IO读写进行重新映射。*/
        if (new_addr == r->addr && filtered_size == r->filtered_size)
            continue;

        /* 调用IO读写映射函数 */
       ... ...

       ... ...

    }
}

得到设备bar上存储的基地址的函数展开如下:

static pcibus_t pci_bar_address(PCIDevice *d, int reg, uint8_t type, pcibus_t size)
{
    pcibus_t new_addr, last_addr;

    /*获得region里基地址的偏移位置*/
    int bar = pci_bar(d, reg);

    /*检查PCI设备IO是否分配,分配以后command应该置1*/
    uint16_t cmd = pci_get_word(d->config + PCI_COMMAND);

    if (type & PCI_BASE_ADDRESS_SPACE_IO) {

        /*如果没有设置type或者没有分配IO那么直接返回地址未映射,将基地址重新置成-1*/
        if (!(cmd & PCI_COMMAND_IO)) {
            return PCI_BAR_UNMAPPED;          

        }

        /*将地址进行对齐,大小范围内清0,这个不是很好解释,因为前面我们这个size是制定为2的N此方的,所以减1就尾数全为1,取反为清0*/
        new_addr = pci_get_long(d->config + bar) & ~(size - 1);

        /*得到region结束地址*/
        last_addr = new_addr + size - 1;
        /* NOTE: we have only 64K ioports on PC */

        /*检查地址是否合法*/
        if (last_addr <= new_addr || new_addr == 0 || last_addr > UINT16_MAX) {
            return PCI_BAR_UNMAPPED;
        }

        /*返回新地址*/
        return new_addr;
    }

    ... ...

    ... ...

}

从这里可以看出,要保证地址不被清空,只要保证之前有基地址,而且合法,所以,只要reset不清空地址,那么在这里只要地址合法,就不会清楚映射好的地址。

当刷新得到新地址以后就进行与父桥的地址匹配,函数展开如下:

static void pci_bridge_filter(PCIDevice *d, pcibus_t *addr, pcibus_t *size, uint8_t type)
{

     ... ...

     ... ...

     /*取桥与设备基地址的最大值作为设备基地址,取桥与设备结束的最小值作为设备的结束地址,如果这个地址合法,那么保证设备在桥地址的范围内*/
     base = MAX(base, pci_bridge_get_base(br, type));
     limit = MIN(limit, pci_bridge_get_limit(br, type));
    /*如果取得地址不匹配,说明设备不在桥的范围内,而且无法截断,将设备地址设置成无效,重新匹配*/

    if (base > limit) {
        goto no_map;
    }

     /*匹配成功*/
    *addr = base;
    *size = limit - base + 1;
    return;
no_map:
    *addr = PCI_BAR_UNMAPPED;
    *size = 0;
}

从这个函数可以看出来,设备的地址分配是受桥的地址分配约束的,只要桥的地址分配了,设备的地址只能分配在桥的范围内,否则就会被置为无效,然后重新分配,一直到分配在桥的范围内为止。所以只要固定了桥的地址,自然就固定了设备的地址。

 

所以只需要初始化桥的地址,并且在reset的时候跳过桥的基地址重置,就能实现设备和桥地址的固定。添加的函数和代码如下:

添加桥的初始地址,因为桥的地址固定写在bar3上,通过写20可以将基地址固定在0x2000上,同时还需要写命令位,置1.

static int dec_21154_initfn(PCIDevice *dev)
{

     ... ...

     ... ...

     pci_set_word(dev->config + PCI_BASE_ADDRESS_3,0x2020);
     pci_set_word(dev->config + PCI_COMMAND,0x1);

     void pci_device_reset(PCIDevice *dev)

 

     return 1;
}

在重置桥里面过滤我们的桥,通过dev的名字可以识别我们自己定义的设备,如果是我们的设备就不重置,直接进行更新IO映射。

void pci_device_reset(PCIDevice *dev)
{

     if(strcmp(dev->name,"dec_name")==0){
          pci_update_mappings(dev);
          return   

     }

     ... ...

     ... ...

}

通过上面的步骤就能实现一般的IO基地址固定,我们可以在Linux中使用 cat /proc/ioports 命令来查看当前PCI设备的IO映射地址关系。

 

2. 直接重写config_write函数。

我用这种方法测试过几种操作系统,不同系统的PCI设备初始化可能会有区别,有些不能够自适应分配IO基地址设备的,那么我们就需要强行overide PCI配置读写函数。

 

在QEMU中,每一个PCI设备都要注册一个读写配置函数,用来提供给操作系统读写PCI设备的内存信息,通过读写这两个函数,就能实现对PCI设备IO基地址进行设置,而我们的IO基地址之所以会动态的变化,也就是因为这个函数将新的IO基地址写到了我们虚拟的PCI设备的bar里面,造成我们自己设置的基地址被覆盖。如果我们不重写它,就使用系统默认的配置函数,不改变重写的数值,如果我们有些特殊的需求,如强行给PCI内存赋值,就可以重写这个函数,虽然有些暴力,但是确实可行。

这样做我们需要修改之前定义的设备结构体。在结构体里面增添.config_write和.config_read。并且在write里面强行的把基地址写成我们想固定的地址。

 

static PCIDeviceInfo fpga_info={
     .qdev.name = "fpga",
     .qdev.size = sizeof(FPGAState),
     .init      = pci_fpga_init,

     .config_write = fpga_config_write,

     .config_read = fpga_config_read,
};

void fpga_write_config(PCIDevice *d, uint32_t addr, uint32_t val, int l)

{

      /*如果是bar0 则是0x10,这个必须根据我们分配的bar不同而变化*/

      if(addr = 0x10) pci_default_write_config(d,addr,0x20,l);

      else pci_default_write_config(d,addr,val,l);

 

同样的方法我们也可以用在桥里面,将桥的IO基地址固定,然而桥的PCI桥地址的基地址是放在bar3上的,所以判断起来要判断1d,如:

       if(addr==1d)   pci_bridge_write_config(d,addr,0x20,l);

       else   pci_bridge_write_config(d,addr,val,l);

 

这样我们就强行的将两者的IO基地址固定了,这个我在操作系统上测试通过了,并且KVM IO拦截运行正常。

总结

通过上面两种改写就能够确保模拟出来的PCI总线设备和桥固定在我们想要的IO空间段,不用系统随机的分配。这样做可以满足我们一些特殊化得需求,如某些板子的某些设备是固定IO地址的,而相应的操作系统不是通过class和subclass,vendor,device ID这些来读取设备,而是通过固定IO来访问设备的就能起到作用。对一些固定的操作系统有更强的兼容性。另外也在一定的程度上帮助我们更深入的理解了PCI设备,理解了硬件与操作系统的IO交互。

抱歉!评论已关闭.