From: Mark Salter msalter@redhat.com
Add support for parsing MCFG table and provide functions to read/write PCI configuration space based on the parsed info. This provides the low-level raw_pci_read/raw_pci_write functionality.
Signed-off-by: Mark Salter msalter@redhat.com --- arch/arm64/Kconfig | 3 + arch/arm64/include/asm/pci.h | 16 ++ arch/arm64/pci/Makefile | 1 + arch/arm64/pci/mmconfig.c | 409 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 429 insertions(+) create mode 100644 arch/arm64/pci/mmconfig.c
diff --git a/arch/arm64/Kconfig b/arch/arm64/Kconfig index 7789db7..fd2fa10 100644 --- a/arch/arm64/Kconfig +++ b/arch/arm64/Kconfig @@ -186,6 +186,9 @@ config PCI_DOMAINS_GENERIC config PCI_SYSCALL def_bool PCI
+config PCI_MMCONFIG + def_bool y if PCI && ACPI + source "drivers/pci/Kconfig" source "drivers/pci/pcie/Kconfig" source "drivers/pci/hotplug/Kconfig" diff --git a/arch/arm64/include/asm/pci.h b/arch/arm64/include/asm/pci.h index 9e23df3..3b04035 100644 --- a/arch/arm64/include/asm/pci.h +++ b/arch/arm64/include/asm/pci.h @@ -48,5 +48,21 @@ struct pci_raw_ops {
extern const struct pci_raw_ops *raw_pci_ops;
+/* "PCI MMCONFIG %04x [bus %02x-%02x]" */ +#define PCI_MMCFG_RESOURCE_NAME_LEN (22 + 4 + 2 + 2) + +struct pci_mmcfg_region { + struct list_head list; + struct resource res; + u64 address; + char __iomem *virt; + u16 segment; + u8 start_bus; + u8 end_bus; + char name[PCI_MMCFG_RESOURCE_NAME_LEN]; +}; + +extern struct pci_mmcfg_region *pci_mmconfig_lookup(int segment, int bus); + #endif /* __KERNEL__ */ #endif /* __ASM_PCI_H */ diff --git a/arch/arm64/pci/Makefile b/arch/arm64/pci/Makefile index b8d5dbd..7038b51 100644 --- a/arch/arm64/pci/Makefile +++ b/arch/arm64/pci/Makefile @@ -1 +1,2 @@ obj-y += pci.o +obj-$(CONFIG_ACPI) += mmconfig.o diff --git a/arch/arm64/pci/mmconfig.c b/arch/arm64/pci/mmconfig.c new file mode 100644 index 0000000..0bda9b7 --- /dev/null +++ b/arch/arm64/pci/mmconfig.c @@ -0,0 +1,409 @@ +/* + * mmconfig.c - Low-level direct PCI config space access via MMCONFIG + * + * Borrowed heavily from x86 + */ + +#include <linux/pci.h> +#include <linux/acpi.h> +#include <linux/init.h> +#include <linux/bitmap.h> +#include <linux/dmi.h> +#include <linux/slab.h> +#include <linux/mutex.h> +#include <linux/rculist.h> +#include <linux/rcupdate.h> + +#define PREFIX "PCI: " + +/* Indicate if the mmcfg resources have been placed into the resource table. */ +static bool pci_mmcfg_running_state; +static bool pci_mmcfg_arch_init_failed; +static DEFINE_MUTEX(pci_mmcfg_lock); + +#define PCI_MMCFG_BUS_OFFSET(bus) ((bus) << 20) + +LIST_HEAD(pci_mmcfg_list); + +static inline unsigned char mmio_config_readb(void __iomem *pos) +{ + return readb(pos); +} + +static inline unsigned short mmio_config_readw(void __iomem *pos) +{ + return readw(pos); +} + +static inline unsigned int mmio_config_readl(void __iomem *pos) +{ + return readl(pos); +} + +static inline void mmio_config_writeb(void __iomem *pos, u8 val) +{ + writeb(val, pos); +} + +static inline void mmio_config_writew(void __iomem *pos, u16 val) +{ + writew(val, pos); +} + +static inline void mmio_config_writel(void __iomem *pos, u32 val) +{ + writel(val, pos); +} + +struct pci_mmcfg_region *pci_mmconfig_lookup(int segment, int bus) +{ + struct pci_mmcfg_region *cfg; + + list_for_each_entry_rcu(cfg, &pci_mmcfg_list, list) + if (cfg->segment == segment && + cfg->start_bus <= bus && bus <= cfg->end_bus) + return cfg; + + return NULL; +} + +static char __iomem *pci_dev_base(struct pci_mmcfg_region *cfg, + unsigned int bus, unsigned int devfn) +{ + return cfg->virt + (PCI_MMCFG_BUS_OFFSET(bus) | (devfn << 12)); +} + +static int __pci_mmcfg_read(struct pci_mmcfg_region *cfg, unsigned int bus, + unsigned int devfn, int reg, int len, u32 *value) +{ + char __iomem *addr = pci_dev_base(cfg, bus, devfn); + + switch (len) { + case 1: + *value = mmio_config_readb(addr + reg); + break; + case 2: + *value = mmio_config_readw(addr + reg); + break; + case 4: + *value = mmio_config_readl(addr + reg); + break; + } + return 0; +} + +static int pci_mmcfg_read(unsigned int seg, unsigned int bus, + unsigned int devfn, int reg, int len, u32 *value) +{ + struct pci_mmcfg_region *cfg; + int ret; + + /* Why do we have this when nobody checks it. How about a BUG()!? -AK */ + if (unlikely((bus > 255) || (devfn > 255) || (reg > 4095))) { +err: *value = -1; + return -EINVAL; + } + + rcu_read_lock(); + cfg = pci_mmconfig_lookup(seg, bus); + if (!cfg || !cfg->virt) { + rcu_read_unlock(); + goto err; + } + + ret = __pci_mmcfg_read(cfg, bus, devfn, reg, len, value); + + rcu_read_unlock(); + + return ret; +} + +static int __pci_mmcfg_write(struct pci_mmcfg_region *cfg, unsigned int bus, + unsigned int devfn, int reg, int len, u32 value) +{ + char __iomem *addr = pci_dev_base(cfg, bus, devfn); + + switch (len) { + case 1: + mmio_config_writeb(addr + reg, value); + break; + case 2: + mmio_config_writew(addr + reg, value); + break; + case 4: + mmio_config_writel(addr + reg, value); + break; + } + return 0; +} + +static int pci_mmcfg_write(unsigned int seg, unsigned int bus, + unsigned int devfn, int reg, int len, u32 value) +{ + struct pci_mmcfg_region *cfg; + int ret; + + /* Why do we have this when nobody checks it. How about a BUG()!? -AK */ + if (unlikely((bus > 255) || (devfn > 255) || (reg > 4095))) + return -EINVAL; + + rcu_read_lock(); + cfg = pci_mmconfig_lookup(seg, bus); + if (!cfg || !cfg->virt) { + rcu_read_unlock(); + return -EINVAL; + } + + ret = __pci_mmcfg_write(cfg, bus, devfn, reg, len, value); + + rcu_read_unlock(); + + return ret; +} + +const struct pci_raw_ops pci_mmcfg = { + .read = pci_mmcfg_read, + .write = pci_mmcfg_write, +}; + +static void __iomem *mcfg_ioremap(struct pci_mmcfg_region *cfg) +{ + void __iomem *addr; + u64 start, size; + int num_buses; + + start = cfg->address + PCI_MMCFG_BUS_OFFSET(cfg->start_bus); + num_buses = cfg->end_bus - cfg->start_bus + 1; + size = PCI_MMCFG_BUS_OFFSET(num_buses); + addr = ioremap_nocache(start, size); + if (addr) + addr -= PCI_MMCFG_BUS_OFFSET(cfg->start_bus); + return addr; +} + +void pci_mmcfg_arch_unmap(struct pci_mmcfg_region *cfg) +{ + if (cfg && cfg->virt) { + iounmap(cfg->virt + PCI_MMCFG_BUS_OFFSET(cfg->start_bus)); + cfg->virt = NULL; + } +} + +void __init pci_mmcfg_arch_free(void) +{ + struct pci_mmcfg_region *cfg; + + list_for_each_entry(cfg, &pci_mmcfg_list, list) + pci_mmcfg_arch_unmap(cfg); +} + +int pci_mmcfg_arch_map(struct pci_mmcfg_region *cfg) +{ + cfg->virt = mcfg_ioremap(cfg); + if (!cfg->virt) { + pr_err(PREFIX "can't map MMCONFIG at %pR\n", &cfg->res); + return -ENOMEM; + } + + return 0; +} + +static void __init pci_mmconfig_remove(struct pci_mmcfg_region *cfg) +{ + if (cfg->res.parent) + release_resource(&cfg->res); + list_del(&cfg->list); + kfree(cfg); +} + +static void __init free_all_mmcfg(void) +{ + struct pci_mmcfg_region *cfg, *tmp; + + pci_mmcfg_arch_free(); + list_for_each_entry_safe(cfg, tmp, &pci_mmcfg_list, list) + pci_mmconfig_remove(cfg); +} + +static void list_add_sorted(struct pci_mmcfg_region *new) +{ + struct pci_mmcfg_region *cfg; + + /* keep list sorted by segment and starting bus number */ + list_for_each_entry_rcu(cfg, &pci_mmcfg_list, list) { + if (cfg->segment > new->segment || + (cfg->segment == new->segment && + cfg->start_bus >= new->start_bus)) { + list_add_tail_rcu(&new->list, &cfg->list); + return; + } + } + list_add_tail_rcu(&new->list, &pci_mmcfg_list); +} + +static struct pci_mmcfg_region *pci_mmconfig_alloc(int segment, int start, + int end, u64 addr) +{ + struct pci_mmcfg_region *new; + struct resource *res; + + if (addr == 0) + return NULL; + + new = kzalloc(sizeof(*new), GFP_KERNEL); + if (!new) + return NULL; + + new->address = addr; + new->segment = segment; + new->start_bus = start; + new->end_bus = end; + + res = &new->res; + res->start = addr + PCI_MMCFG_BUS_OFFSET(start); + res->end = addr + PCI_MMCFG_BUS_OFFSET(end + 1) - 1; + res->flags = IORESOURCE_MEM | IORESOURCE_BUSY; + snprintf(new->name, PCI_MMCFG_RESOURCE_NAME_LEN, + "PCI MMCONFIG %04x [bus %02x-%02x]", segment, start, end); + res->name = new->name; + + return new; +} + +static struct pci_mmcfg_region *__init pci_mmconfig_add(int segment, int start, + int end, u64 addr) +{ + struct pci_mmcfg_region *new; + + new = pci_mmconfig_alloc(segment, start, end, addr); + if (new) { + mutex_lock(&pci_mmcfg_lock); + list_add_sorted(new); + mutex_unlock(&pci_mmcfg_lock); + + pr_info(PREFIX + "MMCONFIG for domain %04x [bus %02x-%02x] at %pR " + "(base %#lx)\n", + segment, start, end, &new->res, (unsigned long)addr); + } + + return new; +} + +static int __init pci_parse_mcfg(struct acpi_table_header *header) +{ + struct acpi_table_mcfg *mcfg; + struct acpi_mcfg_allocation *cfg_table, *cfg; + unsigned long i; + int entries; + + if (!header) + return -EINVAL; + + mcfg = (struct acpi_table_mcfg *)header; + + /* how many config structures do we have */ + free_all_mmcfg(); + entries = 0; + i = header->length - sizeof(struct acpi_table_mcfg); + while (i >= sizeof(struct acpi_mcfg_allocation)) { + entries++; + i -= sizeof(struct acpi_mcfg_allocation); + } + if (entries == 0) { + pr_err(PREFIX "MMCONFIG has no entries\n"); + return -ENODEV; + } + + cfg_table = (struct acpi_mcfg_allocation *) &mcfg[1]; + for (i = 0; i < entries; i++) { + cfg = &cfg_table[i]; + + if (pci_mmconfig_add(cfg->pci_segment, cfg->start_bus_number, + cfg->end_bus_number, cfg->address) == NULL) { + pr_warn(PREFIX "no memory for MCFG entries\n"); + free_all_mmcfg(); + return -ENOMEM; + } + } + + return 0; +} + +int __init pci_mmcfg_arch_init(void) +{ + struct pci_mmcfg_region *cfg; + + list_for_each_entry(cfg, &pci_mmcfg_list, list) + if (pci_mmcfg_arch_map(cfg)) { + pci_mmcfg_arch_free(); + return 0; + } + + raw_pci_ops = &pci_mmcfg; + + return 1; +} + +static void __init __pci_mmcfg_init(int early) +{ + if (list_empty(&pci_mmcfg_list)) { + pr_info("No MCFG table found!\n"); + pci_mmcfg_arch_init_failed = true; + return; + } + + if (!pci_mmcfg_arch_init()) { + pr_info("pci_mmcfg_arch_init failed!\n"); + free_all_mmcfg(); + pci_mmcfg_arch_init_failed = true; + } +} + +void __init pci_mmcfg_early_init(void) +{ + acpi_table_parse(ACPI_SIG_MCFG, pci_parse_mcfg); + + __pci_mmcfg_init(1); +} + +static int __init pci_mmcfg_init(void) +{ + pci_mmcfg_early_init(); + return 0; +} +arch_initcall(pci_mmcfg_init); + +void __init pci_mmcfg_late_init(void) +{ + /* MMCONFIG hasn't been enabled yet, try again */ + if (pci_mmcfg_arch_init_failed) { + acpi_table_parse(ACPI_SIG_MCFG, pci_parse_mcfg); + __pci_mmcfg_init(0); + } +} + +static int __init pci_mmcfg_late_insert_resources(void) +{ + struct pci_mmcfg_region *cfg; + + pci_mmcfg_running_state = true; + + /* + * Attempt to insert the mmcfg resources but not with the busy flag + * marked so it won't cause request errors when __request_region is + * called. + */ + list_for_each_entry(cfg, &pci_mmcfg_list, list) + if (!cfg->res.parent) + insert_resource(&iomem_resource, &cfg->res); + + return 0; +} + +/* + * Perform MMCONFIG resource insertion after PCI initialization to allow for + * misprogrammed MCFG tables that state larger sizes but actually conflict + * with other system resources. + */ +late_initcall(pci_mmcfg_late_insert_resources);