QEMU源码全解析 —— PCI设备模拟(8)

接前一篇文章:

上一回介绍了普通设备的模拟,这里介绍一个特殊的设备------北桥的I/O模拟。

北桥的PCI部分由结构体PCIHostState表示 。北桥的PCI部分有两个I/O寄存器 :其中一个是配置地址寄存器,叫作CONFGADDR ,其对应的MemoryRegion保存在PCIHostState的conf_mem 成员中,该寄存器的作用是选择PCI设备。另一个寄存器是配置数据寄存器,叫作CONFDATA ,其对应的MemoryRegion保存在PCIHostState的data_mem 成员中,当其CONFADDR寄存器最高位为1时,这个寄存器用来对CONFADDR中指定的设备进行配置

这两段MMIO地址是在北桥的instance_init函数i440fx_pcihost_initfn中初始化的。i440fx_pcihost_initfn函数在hw/pci-host/i440fx.c中,代码如下:

cpp 复制代码
static void i440fx_pcihost_initfn(Object *obj)
{
    I440FXState *s = I440FX_PCI_HOST_BRIDGE(obj);
    PCIHostState *phb = PCI_HOST_BRIDGE(obj);

    memory_region_init_io(&phb->conf_mem, obj, &pci_host_conf_le_ops, phb,
                          "pci-conf-idx", 4);
    memory_region_init_io(&phb->data_mem, obj, &pci_host_data_le_ops, phb,
                          "pci-conf-data", 4);

    object_property_add_link(obj, PCI_HOST_PROP_RAM_MEM, TYPE_MEMORY_REGION,
                             (Object **) &s->ram_memory,
                             qdev_prop_allow_set_link_before_realize, 0);

    object_property_add_link(obj, PCI_HOST_PROP_PCI_MEM, TYPE_MEMORY_REGION,
                             (Object **) &s->pci_address_space,
                             qdev_prop_allow_set_link_before_realize, 0);

    object_property_add_link(obj, PCI_HOST_PROP_SYSTEM_MEM, TYPE_MEMORY_REGION,
                             (Object **) &s->system_memory,
                             qdev_prop_allow_set_link_before_realize, 0);

    object_property_add_link(obj, PCI_HOST_PROP_IO_MEM, TYPE_MEMORY_REGION,
                             (Object **) &s->io_memory,
                             qdev_prop_allow_set_link_before_realize, 0);
}

其对应的MemoryRegionOps分别是pci_host_conf_le_ops和pci_host_data_le_ops。I/O地址的注册是在北桥的具现化函数i440fx_pcihost_realize中完成的。该函数也在hw/pci-host/i440fx.c中(就在下边),代码如下:

cpp 复制代码
static void i440fx_pcihost_realize(DeviceState *dev, Error **errp)
{
    ERRP_GUARD();
    I440FXState *s = I440FX_PCI_HOST_BRIDGE(dev);
    PCIHostState *phb = PCI_HOST_BRIDGE(dev);
    SysBusDevice *sbd = SYS_BUS_DEVICE(dev);
    PCIBus *b;
    PCIDevice *d;
    PCII440FXState *f;
    unsigned i;

    memory_region_add_subregion(s->io_memory, 0xcf8, &phb->conf_mem);
    sysbus_init_ioports(sbd, 0xcf8, 4);

    memory_region_add_subregion(s->io_memory, 0xcfc, &phb->data_mem);
    sysbus_init_ioports(sbd, 0xcfc, 4);

    /* register i440fx 0xcf8 port as coalesced pio */
    memory_region_set_flush_coalesced(&phb->data_mem);
    memory_region_add_coalescing(&phb->conf_mem, 0, 4);

    b = pci_root_bus_new(dev, NULL, s->pci_address_space,
                         s->io_memory, 0, TYPE_PCI_BUS);
    phb->bus = b;

    d = pci_create_simple(b, 0, s->pci_type);
    f = I440FX_PCI_DEVICE(d);

    range_set_bounds(&s->pci_hole, s->below_4g_mem_size,
                     IO_APIC_DEFAULT_ADDRESS - 1);

    /* setup pci memory mapping */
    pc_pci_as_mapping_init(s->system_memory, s->pci_address_space);

    /* if *disabled* show SMRAM to all CPUs */
    memory_region_init_alias(&f->smram_region, OBJECT(d), "smram-region",
                             s->pci_address_space, SMRAM_C_BASE, SMRAM_C_SIZE);
    memory_region_add_subregion_overlap(s->system_memory, SMRAM_C_BASE,
                                        &f->smram_region, 1);
    memory_region_set_enabled(&f->smram_region, true);

    /* smram, as seen by SMM CPUs */
    memory_region_init(&f->smram, OBJECT(d), "smram", 4 * GiB);
    memory_region_set_enabled(&f->smram, true);
    memory_region_init_alias(&f->low_smram, OBJECT(d), "smram-low",
                             s->ram_memory, SMRAM_C_BASE, SMRAM_C_SIZE);
    memory_region_set_enabled(&f->low_smram, true);
    memory_region_add_subregion(&f->smram, SMRAM_C_BASE, &f->low_smram);
    object_property_add_const_link(qdev_get_machine(), "smram",
                                   OBJECT(&f->smram));

    init_pam(&f->pam_regions[0], OBJECT(d), s->ram_memory, s->system_memory,
             s->pci_address_space, PAM_BIOS_BASE, PAM_BIOS_SIZE);
    for (i = 0; i < ARRAY_SIZE(f->pam_regions) - 1; ++i) {
        init_pam(&f->pam_regions[i + 1], OBJECT(d), s->ram_memory,
                 s->system_memory, s->pci_address_space,
                 PAM_EXPAN_BASE + i * PAM_EXPAN_SIZE, PAM_EXPAN_SIZE);
    }

    ram_addr_t ram_size = s->below_4g_mem_size + s->above_4g_mem_size;
    ram_size = ram_size / 8 / 1024 / 1024;
    if (ram_size > 255) {
        ram_size = 255;
    }
    d->config[I440FX_COREBOOT_RAM_SIZE] = ram_size;

    i440fx_update_memory_mappings(f);
}

memory_region_add_subregion函数会将指定MemoryRegion设置为系统I/O地址空间的子MemoryRegion。sysbus_init_ioports会对SysBusDevice中的PIO端口数组初始化。i440fx_pcihost_realize函数将北桥的CONFADDR和CONFDATA两个寄存器地址加入到系统I/O地址空间中。其中,CONFADDR使用从端口0xcf8开始的4个端口,CONFDATA使用从0xcfc开始的4个端口。

这部分相关知识参见笔者博文《PCI Express体系结构导读》随记 ------ 第I篇 第2章 PCI总线的桥与配置(10)

写CONFADDR的行为会设置配置寄存器的值,指定选择的PCI设备,用于随后的数据访问。pci_host_config_write函数在hw/pci/pci_host.c中,代码如下:

cpp 复制代码
static void pci_host_config_write(void *opaque, hwaddr addr,
                                  uint64_t val, unsigned len)
{
    PCIHostState *s = opaque;

    PCI_DPRINTF("%s addr " HWADDR_FMT_plx " len %d val %"PRIx64"\n",
                __func__, addr, len, val);
    if (addr != 0 || len != 4) {
        return;
    }
    s->config_reg = val;
}

pci_host_config_write函数将虚拟机选中的PCI设备地址保存在了PCIHostState的config_reg寄存器中。CONFADDR必须通过4个字节访问。从Intel 440FX PCIset手册中(见上图)可知写入CONFADDR寄存器的数据的含义。CONFADDR寄存器的第31位表示是否使能PCI设备的配置功能如果要想读写PCI设备的配置空间,需要将该位设置为1 ;第24位到第30位为保留位;第16位到23位表示设置PCI总线号第11位到15位表示设置选择的总线上面的PCI设备号第8位到第10位表示选择总线上面设备号对应PCI设备的功能号第2位到第7位表示选定的总线、设备、功能号对应的PCI设备的寄存器值;第0位到第1位为保留位。

综上,CONFADDR寄存器指定了PCI设备的地址,当选定了PCI设备之后就可以向PC配置空间写数据了。下面是写CONFDATA寄存器的值。

pci_host_data_write函数也在hw/pci/pci_host.c中,代码如下:

cpp 复制代码
static void pci_host_data_write(void *opaque, hwaddr addr,
                                uint64_t val, unsigned len)
{
    PCIHostState *s = opaque;

    if (s->config_reg & (1u << 31))
        pci_data_write(s->bus, s->config_reg | (addr & 3), val, len);
}

首先判断配置寄存器的值中的第31位是否为1(使能),使能的情况下调用pci_data_write函数开始写设备的配置空间。

pci_data_write函数在hw/pci/pci_host.c中,代码如下:

cpp 复制代码
void pci_data_write(PCIBus *s, uint32_t addr, uint32_t val, unsigned len)
{
    PCIDevice *pci_dev = pci_dev_find_by_addr(s, addr);
    uint32_t config_addr = addr & (PCI_CONFIG_SPACE_SIZE - 1);

    if (!pci_dev) {
        trace_pci_cfg_write("empty", extract32(addr, 16, 8),
                            extract32(addr, 11, 5), extract32(addr, 8, 3),
                            config_addr, val);
        return;
    }

    pci_host_config_write_common(pci_dev, config_addr, PCI_CONFIG_SPACE_SIZE,
                                 val, len);
}

pci_data_write函数首先通过CONFADDR中的值,调用pci_dev_find_by_addr函数找到需要访问的PCI设备;然后再调用pci_host_config_write_common函数读写该设备的PCI配置空间。值得注意的是,addr和(PCI_CONIG_SPACE_SIZE-1)进行与操作,将addr限制在了PCI配置空间的大小256字节以内。

PCI_CONIG_SPACE_SIZE的定义在include/hw/pci/pci.h中,如下:

cpp 复制代码
/* Size of the standard PCI config space */
#define PCI_CONFIG_SPACE_SIZE 0x100

pci_host_config_write_common函数在hw/pci/pci_host.c中,代码如下:

cpp 复制代码
void pci_host_config_write_common(PCIDevice *pci_dev, uint32_t addr,
                                  uint32_t limit, uint32_t val, uint32_t len)
{
    pci_adjust_config_limit(pci_get_bus(pci_dev), &limit);
    if (limit <= addr) {
        return;
    }

    assert(len <= 4);
    /* non-zero functions are only exposed when function 0 is present,
     * allowing direct removal of unexposed functions.
     */
    if ((pci_dev->qdev.hotplugged && !pci_get_function_0(pci_dev)) ||
        !pci_dev->has_power || is_pci_dev_ejected(pci_dev)) {
        return;
    }

    trace_pci_cfg_write(pci_dev->name, pci_dev_bus_num(pci_dev),
                        PCI_SLOT(pci_dev->devfn),
                        PCI_FUNC(pci_dev->devfn), addr, val);
    pci_dev->config_write(pci_dev, addr, val, MIN(len, limit - addr));
}

pci_host_config_write_common函数在做一些基本的检查之后,调用了设备自己的config_write回调函数。

欲知后事如何,且看下回分解。

相关推荐
公西雒13 天前
关于在GitLab的CI/CD中用docker buildx本地化多架构打包dotnet应用的问题
ci/cd·docker·gitlab·qemu·dotnet
团儿.24 天前
KVM磁盘配置:构建高效虚拟环境的基石
linux·运维·centos·kvm·kvm磁盘
ywang_wnlo1 个月前
【Kenel】基于 QEMU 的 Linux 内核编译和安装
linux·qemu·kernel
ywang_wnlo1 个月前
【Kernel】基于 QEMU 的 Linux 内核编译和安装
linux·qemu·kernel
小哈里1 个月前
【虚拟化】内核级虚拟化技术KVM介绍,全/半虚拟化的区别,使用libvirt搭建虚拟化平台(go/java/c++)
java·c++·golang·虚拟化·kvm
plmm烟酒僧1 个月前
qemu模拟arm64环境-构建6.1内核以及debian12
linux·debian·qemu·虚拟机·香橙派·aarch64
igcllq2 个月前
ubuntu 安装kvm 创建windos虚拟机
linux·运维·服务器·ubuntu·虚拟机·kvm
思禾2 个月前
Qemu开发ARM篇-3、qemu运行uboot演示
linux·arm开发·qemu·uboot
清瞳清2 个月前
KVM环境下制作ubuntu qcow2格式镜像
ubuntu·kvm·qcow2
张世争2 个月前
rtems 5.3 qemu realview_pbx_a9 环境搭建:生成 rtems arm 工具链
qemu·rtems·realview_pbx_a9