接前一篇文章:
上一回讲到,在SeaBIOS的调用链dopost->maininit->platform_hardware_setup->qemu_platform_setup->pci_setup->pci_bios_map_devices过程中,最后这个函数负责完成 PCI 设备BAR的设置。
其中包括I/O、MEM以及PREFMEM三种BAR的设置,MEM和PREFMEM是一起的,这里以上述命令行为例,讨论SeaBIOS如何给PCI设备设置BAR基址。
pci_bios_map_devices首先调用pci_bios_init_root_regions_io和pci_bios_init_root_regions_mem函数做初始化的工作,以mem为例,调用的是src/fw/pcinit.c中的pci_bios_init_root_regions_mem函数。
该函数在 QEMU 源码根目录/roms/seabios/src/fw/pciinit.c中,代码如下:
- static int pci_bios_init_root_regions_mem(struct pci_bus *bus)
- {
- struct pci_region *r_end = &bus->r[PCI_REGION_TYPE_PREFMEM];
- struct pci_region *r_start = &bus->r[PCI_REGION_TYPE_MEM];
-
- if (pci_region_align(r_start) < pci_region_align(r_end)) {
- // Swap regions to improve alignment.
- r_end = r_start;
- r_start = &bus->r[PCI_REGION_TYPE_PREFMEM];
- }
- u64 sum = pci_region_sum(r_end);
- u64 align = pci_region_align(r_end);
- r_end->base = ALIGN_DOWN((pcimem_end - sum), align);
- sum = pci_region_sum(r_start);
- align = pci_region_align(r_start);
- r_start->base = ALIGN_DOWN((r_end->base - sum), align);
-
- if ((r_start->base < pcimem_start) ||
- (r_start->base > pcimem_end))
- // Memory range requested is larger than available.
- return -1;
- return 0;
- }
pci_region结构表示该 虚拟机 所有设备的某一类BAR(如PCI_REGION_TYPE_MEM表示mem BAR)。其定义也在roms/seabios/src/fw/pciinit.c中,如下:
- struct pci_region {
- /* pci region assignments */
- u64 base;
- struct hlist_head list;
- };
struct pci_region的base成员表示这类BAR的起始地址;list成员用来链接所有这类BAR的设备。
- PCI: map device bdf=00:03.0 bar 1, addr 0000c000, size 00000040 [io]
- PCI: map device bdf=00:01.1 bar 4, addr 0000c040, size 00000010 [io]
- PCI: map device bdf=00:04.0 bar 0, addr fea00000, size 00100000 [mem]
- PCI: map device bdf=00:03.0 bar 6, addr feb00000, size 00040000 [mem]
- PCI: map device bdf=00:03.0 bar 0, addr feb40000, size 00020000 [mem]
- PCI: map device bdf=00:02.0 bar 6, addr feb60000, size 00100000 [mem]
- PCI: map device bdf=00:02.0 bar 2, addr feb70000, size 00001000 [mem]
- PCI: map device bdf=00:02.0 bar 0, addr fd000000, size 01000000 [prefmem]
pci_region_align函数会返回最大的align值,每个设备的BAR地址的alignment就是其大小。这里首先比较align,大的尽量往前面放。在命令行启动的虚拟机中最大的是VGA的16MB ROM区域,所以会把PREFMEM放在更前面,也就是其地址比较低。pci_region_sum函数返回某一类BAR的所有空间和,将pcimem_end设置为0xfec00000。
pci_region_sum函数在roms/seabios/src/fw/pciinit.c中,代码如下:
- static u64 pci_region_sum(struct pci_region *r)
- {
- u64 sum = 0;
- struct pci_region_entry *entry;
- hlist_for_each_entry(entry, &r->list, node) {
- sum += entry->size;
- }
- return sum;
- }
这里r_end表示的就是mem BAR,此例中所有PCI设备的mem BAR的sum为0x171000(上边所有[mem]项的和,0x00100000+0x00040000+0x00020000+0x00100000+0x00001000=0x171000),align为mem中最大的那一个值,此例中是0x00100000,所以r_end->base就是0xfec00000-0x171000之后与0x00100000进行与运算的结果,该值为0xfea00000。
r_start表示PREFMEM BAR,这里只有一个PREFMEM,大小为0x01000000,所以align为0x01000000,0xfea000000-0x01000000=0xfd000000,所以那个PREFMEM的起始在0xfd000000。
将每种BAR的基址计算出来后就好处理了,调用pci_region_map_entries函数设置每个BAR的基址。pci_region_map_entries函数在roms/seabios/src/fw/pciinit.c中,代码如下:
- static void pci_region_map_entries(struct pci_bus *busses, struct pci_region *r)
- {
- struct hlist_node *n;
- struct pci_region_entry *entry;
- hlist_for_each_entry_safe(entry, n, &r->list, node) {
- u64 addr = r->base;
- r->base += entry->size;
- if (entry->bar == -1)
- // Update bus base address if entry is a bridge region
- busses[entry->dev->secondary_bus].r[entry->type].base = addr;
- pci_region_map_one_entry(entry, addr);
- hlist_del(&entry->node);
- free(entry);
- }
- }
该函数调用pci_region_map_one_entry函数来完成每一项的设置。pci_region_map_one_entry函数也在roms/seabios/src/fw/pciinit.c中(就在上边),代码如下:
- static void
- pci_region_map_one_entry(struct pci_region_entry *entry, u64 addr)
- {
- if (entry->bar >= 0) {
- dprintf(1, "PCI: map device bdf=%pP"
- " bar %d, addr %08llx, size %08llx [%s]\n",
- entry->dev,
- entry->bar, addr, entry->size, region_type_name[entry->type]);
-
- pci_set_io_region_addr(entry->dev, entry->bar, addr, entry->is64);
- return;
- }
-
- u16 bdf = entry->dev->bdf;
- u64 limit = addr + entry->size - 1;
- if (entry->type == PCI_REGION_TYPE_IO) {
- pci_config_writeb(bdf, PCI_IO_BASE, addr >> PCI_IO_SHIFT);
- pci_config_writew(bdf, PCI_IO_BASE_UPPER16, 0);
- pci_config_writeb(bdf, PCI_IO_LIMIT, limit >> PCI_IO_SHIFT);
- pci_config_writew(bdf, PCI_IO_LIMIT_UPPER16, 0);
- }
- if (entry->type == PCI_REGION_TYPE_MEM) {
- pci_config_writew(bdf, PCI_MEMORY_BASE, addr >> PCI_MEMORY_SHIFT);
- pci_config_writew(bdf, PCI_MEMORY_LIMIT, limit >> PCI_MEMORY_SHIFT);
- }
- if (entry->type == PCI_REGION_TYPE_PREFMEM) {
- pci_config_writew(bdf, PCI_PREF_MEMORY_BASE, addr >> PCI_PREF_MEMORY_SHIFT);
- pci_config_writew(bdf, PCI_PREF_MEMORY_LIMIT, limit >> PCI_PREF_MEMORY_SHIFT);
- pci_config_writel(bdf, PCI_PREF_BASE_UPPER32, addr >> 32);
- pci_config_writel(bdf, PCI_PREF_LIMIT_UPPER32, limit >> 32);
- }
- }
pci_set_io_region_addr函数同样在roms/seabios/src/fw/pciinit.c中,代码如下:
- static void
- pci_set_io_region_addr(struct pci_device *pci, int bar, u64 addr, int is64)
- {
- u32 ofs = pci_bar(pci, bar);
- pci_config_writel(pci->bdf, ofs, addr);
- if (is64)
- pci_config_writel(pci->bdf, ofs + 4, addr >> 32);
- }
(1)pci_bar函数用来得到需要写入的BAR在PCI配置空间的地址。
pci_bar函数在hw/pci/pci.c中,代码如下:
- int pci_bar(PCIDevice *d, int reg)
- {
- uint8_t type;
-
- /* PCIe virtual functions do not have their own BARs */
- assert(!pci_is_vf(d));
-
- if (reg != PCI_ROM_SLOT)
- return PCI_BASE_ADDRESS_0 + reg * 4;
-
- type = d->config[PCI_HEADER_TYPE] & ~PCI_HEADER_TYPE_MULTI_FUNCTION;
- return type == PCI_HEADER_TYPE_BRIDGE ? PCI_ROM_ADDRESS1 : PCI_ROM_ADDRESS;
- }
(2)然后调用pci_cobfig_writel函数。
pci_config_writel函数在roms/qemu-palcode/pci.h中,代码如下:
- static inline void pci_config_writel(int bdf, uint8_t addr, uint32_t val)
- {
- *(volatile uint32_t *)(pci_conf_base + bdf * 256 + addr) = val;
- }
-
- static inline void pci_config_writew(int bdf, uint8_t addr, uint16_t val)
- {
- *(volatile uint16_t *)(pci_conf_base + bdf * 256 + addr) = val;
- }
-
- static inline void pci_config_writeb(int bdf, uint8_t addr, uint8_t val)
- {
- *(volatile uint8_t *)(pci_conf_base + bdf * 256 + addr) = val;
- }
通过上述过程,SeaBIOS就把所有设备的BAR基址赋值好了。
欲知后事如何,且看下回分解。