接前一篇文章:
上一回介绍了普通设备的模拟,这里介绍一个特殊的设备——北桥的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中,代码如下:
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中(就在下边),代码如下:
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中,代码如下:
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中,代码如下:
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中,代码如下:
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中,如下:
/* Size of the standard PCI config space */
#define PCI_CONFIG_SPACE_SIZE 0x100
pci_host_config_write_common函数在hw/pci/pci_host.c中,代码如下:
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回调函数。
欲知后事如何,且看下回分解。