[PATCH v3 6/7] PCI: mvebu: Add support for Orion PCIe controller

Pali Rohár
2022-09-05T19:49:36Z

Orion PCIe controller has same register sets, same APIs, same functionality
and also same bugs related to PCIe Root Ports as new PCIe controllers from
Marvell found on Kirkwood, Dove or Armada products. So pci-mvebu.c should
work fine with Orion PCIe controllers too.

But Orion PCIe controller has additional bug - broken Marvell CF8/CFC
registers which are used for config space access. This hardware bug is
documented in Marvell Orion Functional Errata, linked from kernel docs
https://www.kernel.org/doc/html/latest/arm/marvell.html. Document contains
also official workaround, but that workaround does not work when CPU or
other side of PCIe link is doing PCIe TLP operations. Because of race
conditions that official workaround is unusable for kernel, any other
multi-threaded systems and any PCIe card with bus mastering support.

Instead Linux kernel for a long time implements different undocumented
workaround (which does not have this issue) via platform code found in
arch/arm/mach-orion5x/pci.c and arch/arm/plat-orion/pcie.c files. It maps
PCIe config space directly into CPU physical address space via Marvell mbus
driver and then kernel access directly mapped physical address of config
space. The only disadvantage of this workaround is that it required
additional 16 MB of physical address space and only standard PCI config
space registers are accessible.

Testing proved that via mbus the physical size can be of this special
address range increased to 256 MB and then whole PCIe config space,
including PCIe registers, is accessible. So the only disadvantage is
requirement of free 256 MB in physical address space.

So for Orion support in pci-mvebu.c driver, this change reimplements
arch-specific mapping of config space with existing mbus driver and
kernel function pci_remap_cfgspace() which simplify physical space
mappings. Therefore pci-mvebu.c driver does not have to relay on static
virtual MMU mappings like old arch-specific code.

Like with any address ranges settings and configurations, this Orion code
reads physical config space address range from device tree files. There are
no fixed settings in the pci-mvebu.c driver as opposite of the old
arch-specific implementation. It is up to the board or platform dts file to
define how PCIe config space could be mapped into physical CPU address
space.

mbus specific target and attribute numbers, required for mbus mappings of
config space registers are also read from device tree files, in exactly
same format as are read for PCIe MEM and PCIe IO space mappings.

During probing of Orion PCIe controller, pci-mvebu.c driver ask kernel mbus
driver to map config space into CPU physical address range (based on DTS
offset and size). Config space stays mapped in physical address range while
PCIe controller is bind to driver.

Note that layout of PCIe config space is not standard PCIe ECAM. It is same
as layout accessed indirectly via CF8/CFC registers used in other Armada
PCIe controllers. Support for extended PCIe registers in this layout is
non-standard and hence it is Marvell specific layout.

The main issue with this layout is that config space of PCIe bus in not
coherent in address range. And neither address range of one PCIe device is
coherent. Therefore it is not possible to use PCI core .map_bus callback
for implementing access into config space. And because Orion is 32-bit
platform it is not a wise idea to map whole 256 MB PCIe config space into
CPU virtual address space permanently.

So because layout of config space via direct or indirect method is same,
functions mvebu_pcie_child_rd_conf() and mvebu_pcie_child_wd_conf() are
slightly modified to support also direct config space access method.
Functions calculates where in physical address range is requested config
space register and do virtual mapping at request time. After register
access it is unmapped from virtual address range.

Links: https://www.kernel.org/doc/html/latest/arm/marvell.html
Signed-off-by: Pali Rohár <pali@kernel.org>
---
Changes in v3:
* Completely rewritten
* Implement full support for accessing PCIe config space based on device
tree details without any harcoded address in pci-mvebu.c driver
---
drivers/pci/controller/Kconfig | 4 +-
drivers/pci/controller/pci-mvebu.c | 142 +++++++++++++++++++++++++++--
2 files changed, 136 insertions(+), 10 deletions(-)

diff --git a/drivers/pci/controller/Kconfig b/drivers/pci/controller/Kconfig
index 8da2efdc5177..ae5a430387bc 100644
--- a/drivers/pci/controller/Kconfig
+++ b/drivers/pci/controller/Kconfig
@@ -5,7 +5,7 @@ menu "PCI controller drivers"

config PCI_MVEBU
tristate "Marvell EBU PCIe controller"
- depends on ARCH_MVEBU || ARCH_DOVE || COMPILE_TEST
+ depends on ARCH_MVEBU || ARCH_DOVE || ARCH_ORION5X || COMPILE_TEST
depends on MVEBU_MBUS
depends on ARM
depends on OF
@@ -15,7 +15,7 @@ config PCI_MVEBU
select HOTPLUG_PCI_PCIE
help
Add support for Marvell EBU PCIe controller. This PCIe controller
- is used on 32-bit Marvell ARM SoCs: Dove, Kirkwood, Armada 370,
+ is used on 32-bit Marvell ARM SoCs: Orion, Kirkwood, Dove, Armada 370,
Armada XP, Armada 375, Armada 38x and Armada 39x.

config PCI_AARDVARK
diff --git a/drivers/pci/controller/pci-mvebu.c b/drivers/pci/controller/pci-mvebu.c
index 9986dd486680..2ef04a8241fc 100644
--- a/drivers/pci/controller/pci-mvebu.c
+++ b/drivers/pci/controller/pci-mvebu.c
@@ -96,6 +96,7 @@ struct mvebu_pcie {
struct resource io;
struct resource realio;
struct resource mem;
+ struct resource cfg;
int nports;
};

@@ -127,6 +128,7 @@ struct mvebu_pcie_port {
struct mvebu_pcie_window iowin;
u32 saved_pcie_stat;
struct resource regs;
+ struct resource cfg;
u8 slot_power_limit_value;
u8 slot_power_limit_scale;
struct irq_domain *rp_irq_domain;
@@ -409,6 +411,7 @@ static int mvebu_pcie_child_rd_conf(struct pci_bus *bus, u32 devfn, int where,
struct mvebu_pcie *pcie = bus->sysdata;
struct mvebu_pcie_port *port;
void __iomem *conf_data;
+ u32 offset;

port = mvebu_pcie_find_port(pcie, bus, devfn);
if (!port)
@@ -417,10 +420,20 @@ static int mvebu_pcie_child_rd_conf(struct pci_bus *bus, u32 devfn, int where,
if (!mvebu_pcie_link_up(port))
return PCIBIOS_DEVICE_NOT_FOUND;

- conf_data = port->base + PCIE_CONF_DATA_OFF;
+ if (resource_size(&port->cfg)) {
+ offset = PCIE_CONF_ADDR(bus->number, devfn, where) & ~PCIE_CONF_ADDR_EN;
+ if (offset >= resource_size(&port->cfg))
+ return PCIBIOS_DEVICE_NOT_FOUND;

- mvebu_writel(port, PCIE_CONF_ADDR(bus->number, devfn, where),
- PCIE_CONF_ADDR_OFF);
+ conf_data = pci_remap_cfgspace(port->cfg.start + offset, size);
+ if (!conf_data)
+ return PCIBIOS_DEVICE_NOT_FOUND;
+ } else {
+ conf_data = port->base + PCIE_CONF_DATA_OFF;
+
+ mvebu_writel(port, PCIE_CONF_ADDR(bus->number, devfn, where),
+ PCIE_CONF_ADDR_OFF);
+ }

switch (size) {
case 1:
@@ -433,9 +446,14 @@ static int mvebu_pcie_child_rd_conf(struct pci_bus *bus, u32 devfn, int where,
*val = readl_relaxed(conf_data);
break;
default:
+ if (resource_size(&port->cfg))
+ iounmap(conf_data);
return PCIBIOS_BAD_REGISTER_NUMBER;
}

+ if (resource_size(&port->cfg))
+ iounmap(conf_data);
+
return PCIBIOS_SUCCESSFUL;
}

@@ -445,6 +463,7 @@ static int mvebu_pcie_child_wr_conf(struct pci_bus *bus, u32 devfn,
struct mvebu_pcie *pcie = bus->sysdata;
struct mvebu_pcie_port *port;
void __iomem *conf_data;
+ u32 offset;

port = mvebu_pcie_find_port(pcie, bus, devfn);
if (!port)
@@ -453,10 +472,20 @@ static int mvebu_pcie_child_wr_conf(struct pci_bus *bus, u32 devfn,
if (!mvebu_pcie_link_up(port))
return PCIBIOS_DEVICE_NOT_FOUND;

- conf_data = port->base + PCIE_CONF_DATA_OFF;
+ if (resource_size(&port->cfg)) {
+ offset = PCIE_CONF_ADDR(bus->number, devfn, where) & ~PCIE_CONF_ADDR_EN;
+ if (offset >= resource_size(&port->cfg))
+ return PCIBIOS_DEVICE_NOT_FOUND;
+
+ conf_data = pci_remap_cfgspace(port->cfg.start + offset, size);
+ if (!conf_data)
+ return PCIBIOS_DEVICE_NOT_FOUND;
+ } else {
+ conf_data = port->base + PCIE_CONF_DATA_OFF;

- mvebu_writel(port, PCIE_CONF_ADDR(bus->number, devfn, where),
- PCIE_CONF_ADDR_OFF);
+ mvebu_writel(port, PCIE_CONF_ADDR(bus->number, devfn, where),
+ PCIE_CONF_ADDR_OFF);
+ }

switch (size) {
case 1:
@@ -469,9 +498,14 @@ static int mvebu_pcie_child_wr_conf(struct pci_bus *bus, u32 devfn,
writel(val, conf_data);
break;
default:
+ if (resource_size(&port->cfg))
+ iounmap(conf_data);
return PCIBIOS_BAD_REGISTER_NUMBER;
}

+ if (resource_size(&port->cfg))
+ iounmap(conf_data);
+
return PCIBIOS_SUCCESSFUL;
}

@@ -1472,6 +1506,7 @@ static void __iomem *mvebu_pcie_map_registers(struct platform_device *pdev,

static int mvebu_get_tgt_attr(struct device_node *np, int devfn,
unsigned long type,
+ unsigned long offset,
unsigned int *tgt,
unsigned int *attr)
{
@@ -1493,6 +1528,7 @@ static int mvebu_get_tgt_attr(struct device_node *np, int devfn,
for (i = 0; i < nranges; i++, range += rangesz) {
u32 flags = of_read_number(range, 1);
u32 slot = of_read_number(range + 1, 1);
+ u32 dtaddr = of_read_number(range + 2, 1);
u64 cpuaddr = of_read_number(range + na, pna);
unsigned long rtype;

@@ -1503,6 +1539,9 @@ static int mvebu_get_tgt_attr(struct device_node *np, int devfn,
else
continue;

+ if (dtaddr != offset)
+ continue;
+
if (slot == PCI_SLOT(devfn) && type == rtype) {
*tgt = DT_CPUADDR_TO_TARGET(cpuaddr);
*attr = DT_CPUADDR_TO_ATTR(cpuaddr);
@@ -1513,6 +1552,43 @@ static int mvebu_get_tgt_attr(struct device_node *np, int devfn,
return -ENOENT;
}

+static int mvebu_get_cfg_tgt_attr(struct device_node *np, phys_addr_t start,
+ struct resource *res,
+ unsigned int *tgt, unsigned int *attr)
+{
+ const __be32 *addrp;
+ unsigned int flags;
+ u64 offset;
+ u64 size;
+ int ret;
+
+ /* get second cell from assigned-addresses property */
+ addrp = of_get_address(np, 1, &size, &flags);
+ if (!addrp)
+ return -EINVAL;
+
+ if (!(flags & IORESOURCE_MEM))
+ return -EINVAL;
+
+ if (be32_to_cpu(addrp[1]) != 0x0)
+ return -EINVAL;
+
+ flags |= IORESOURCE_MEM_NONPOSTED;
+ offset = be32_to_cpu(addrp[2]);
+
+ ret = mvebu_get_tgt_attr(of_get_parent(np), 0, IORESOURCE_MEM, offset, tgt, attr);
+ if (ret)
+ return ret;
+
+ memset(res, 0, sizeof(*res));
+ res->start = start;
+ res->end = start + size - 1;
+ res->flags = flags;
+ res->name = "PCI CFG";
+
+ return 0;
+}
+
static int mvebu_pcie_suspend(struct device *dev)
{
struct mvebu_pcie *pcie;
@@ -1592,7 +1668,7 @@ static int mvebu_pcie_parse_port(struct mvebu_pcie *pcie,
goto skip;
}

- ret = mvebu_get_tgt_attr(dev->of_node, port->devfn, IORESOURCE_MEM,
+ ret = mvebu_get_tgt_attr(dev->of_node, port->devfn, IORESOURCE_MEM, 0,
&port->mem_target, &port->mem_attr);
if (ret < 0) {
dev_err(dev, "%s: cannot get tgt/attr for mem window\n",
@@ -1601,7 +1677,7 @@ static int mvebu_pcie_parse_port(struct mvebu_pcie *pcie,
}

if (resource_size(&pcie->io) != 0) {
- mvebu_get_tgt_attr(dev->of_node, port->devfn, IORESOURCE_IO,
+ mvebu_get_tgt_attr(dev->of_node, port->devfn, IORESOURCE_IO, 0,
&port->io_target, &port->io_attr);
} else {
port->io_target = -1;
@@ -1794,6 +1870,20 @@ static int mvebu_pcie_parse_request_resources(struct mvebu_pcie *pcie)
return ret;
}

+ if (of_device_is_compatible(dev->of_node, "marvell,orion5x-pcie")) {
+ /* Get the PCIe configuration space aperture */
+ mvebu_mbus_get_pcie_cfg_aperture(&pcie->cfg);
+ if (resource_size(&pcie->cfg) == 0) {
+ dev_err(dev, "invalid config space aperature size\n");
+ return -EINVAL;
+ }
+
+ pcie->cfg.name = "PCI CFG";
+ ret = devm_request_resource(dev, &iomem_resource, &pcie->cfg);
+ if (ret)
+ return ret;
+ }
+
return 0;
}

@@ -1804,6 +1894,7 @@ static int mvebu_pcie_probe(struct platform_device *pdev)
struct pci_host_bridge *bridge;
struct device_node *np = dev->of_node;
struct device_node *child;
+ phys_addr_t pcie_cfg_offset;
int num, i, ret;

bridge = devm_pci_alloc_host_bridge(dev, sizeof(struct mvebu_pcie));
@@ -1818,6 +1909,8 @@ static int mvebu_pcie_probe(struct platform_device *pdev)
if (ret)
return ret;

+ pcie_cfg_offset = pcie->cfg.start;
+
num = of_get_available_child_count(np);

pcie->ports = devm_kcalloc(dev, num, sizeof(*pcie->ports), GFP_KERNEL);
@@ -1852,6 +1945,32 @@ static int mvebu_pcie_probe(struct platform_device *pdev)
if (ret < 0)
continue;

+ if (resource_size(&pcie->cfg) != 0) {
+ unsigned int cfg_target, cfg_attr;
+
+ ret = mvebu_get_cfg_tgt_attr(child, pcie_cfg_offset, &port->cfg, &cfg_target, &cfg_attr);
+ if (ret) {
+ dev_err(dev, "%s: missing address range for cfg space\n", port->name);
+ goto err_port_down;
+ }
+
+ if (port->cfg.end > pcie->cfg.end) {
+ dev_err(dev, "%s: requested cfg space of %u bytes is too large, available only %u bytes\n",
+ port->name, resource_size(&port->cfg), pcie->cfg.end - pcie_cfg_offset + 1);
+ port->cfg.start = port->cfg.end = 0;
+ goto err_port_down;
+ }
+
+ ret = mvebu_mbus_add_window_by_id(cfg_target, cfg_attr, port->cfg.start, resource_size(&port->cfg));
+ if (ret) {
+ dev_info(dev, "%s: cannot add mbus window for cfg space: %d\n", port->name, ret);
+ port->cfg.start = port->cfg.end = 0;
+ goto err_port_down;
+ }
+
+ pcie_cfg_offset += resource_size(&port->cfg);
+ }
+
port->base = mvebu_pcie_map_registers(pdev, child, port);
if (IS_ERR(port->base)) {
dev_err(dev, "%s: cannot map registers\n", port->name);
@@ -2011,6 +2130,9 @@ static int mvebu_pcie_probe(struct platform_device *pdev)
port->base = NULL;

err_port_down:
+ if (port->cfg.end && resource_size(&port->cfg))
+ mvebu_mbus_del_window(port->cfg.start, resource_size(&port->cfg));
+
mvebu_pcie_powerdown(port);
}

@@ -2090,6 +2212,9 @@ static int mvebu_pcie_remove(struct platform_device *pdev)
if (port->memwin.size)
mvebu_pcie_del_windows(port, port->memwin.base, port->memwin.size);

+ if (port->cfg.end && resource_size(&port->cfg))
+ mvebu_mbus_del_window(port->cfg.start, resource_size(&port->cfg));
+
/* Power down card and disable clocks. Must be the last step. */
mvebu_pcie_powerdown(port);
}
@@ -2102,6 +2227,7 @@ static const struct of_device_id mvebu_pcie_of_match_table[] = {
{ .compatible = "marvell,armada-370-pcie", },
{ .compatible = "marvell,dove-pcie", },
{ .compatible = "marvell,kirkwood-pcie", },
+ { .compatible = "marvell,orion5x-pcie", },
{},
};

--
2.20.1