PCI: mediatek: Add MediaTek PCIe host controller support
authorRyder Lee <ryder.lee@mediatek.com>
Sun, 21 May 2017 03:42:24 +0000 (11:42 +0800)
committerBjorn Helgaas <bhelgaas@google.com>
Sun, 2 Jul 2017 23:44:45 +0000 (18:44 -0500)
Add support for the MediaTek PCIe Gen2 controller which can be found on
MT7623 series SoCs.

[bhelgaas: fold in mtk_pcie_parse_and_add_res() bugfix from
http://lkml.kernel.org/r/1496644078-27122-1-git-send-email-ryder.lee@mediatek.com]
[bhelgaas: fold in MAINTAINERS update from
http://lkml.kernel.org/r/1497588789-28607-1-git-send-email-ryder.lee@mediatek.com]
[bhelgaas: fold in pci_scan_root_bus_bridge() update and leak fix from
http://lkml.kernel.org/r/1498555451-55073-2-git-send-email-ryder.lee@mediatek.com]
[bhelgaas: fold in powerup fixes from
http://lkml.kernel.org/r/1497866400-41844-2-git-send-email-ryder.lee@mediatek.com]
[bhelgaas: fold in poweroff when link down fixes from
http://lkml.kernel.org/r/1497866400-41844-3-git-send-email-ryder.lee@mediatek.com]
[bhelgaas: fold in optional property fixes from
http://lkml.kernel.org/r/1497866400-41844-4-git-send-email-ryder.lee@mediatek.com]
[bhelgaas: set host->map_irq and host->swizzle_irq and drop
pci_fixup_irqs(), remove unnecessary "return", rename mtk_pcie_link_is_up()
to mtk_pcie_link_up() for consistency, add local struct device pointer]
[bhelgaas: fold in pci_add_flags() removal from
http://lkml.kernel.org/r/1499061300-55951-1-git-send-email-ryder.lee@mediatek.com]
Signed-off-by: Ryder Lee <ryder.lee@mediatek.com>
Signed-off-by: Bjorn Helgaas <bhelgaas@google.com>
MAINTAINERS
drivers/pci/host/Kconfig
drivers/pci/host/Makefile
drivers/pci/host/pcie-mediatek.c [new file with mode: 0644]

index f7d568b8f133d9919e3823c102d7ac78f89c894a..736648ef5658efec35981436ea2018b8e653a637 100644 (file)
@@ -9957,6 +9957,14 @@ S:       Supported
 F:     Documentation/devicetree/bindings/pci/pci-thunder-*
 F:     drivers/pci/host/pci-thunder-*
 
+PCIE DRIVER FOR MEDIATEK
+M:      Ryder Lee <ryder.lee@mediatek.com>
+L:      linux-pci@vger.kernel.org
+L:      linux-mediatek@lists.infradead.org
+S:      Supported
+F:      Documentation/devicetree/bindings/pci/mediatek*
+F:      drivers/pci/host/*mediatek*
+
 PCMCIA SUBSYSTEM
 P:     Linux PCMCIA Team
 L:     linux-pcmcia@lists.infradead.org
index 7f47cd5e10a57c4f7de8f175b3034fe375d6785f..0cd5b30dccb15d1655424faeff8385cd528d48b4 100644 (file)
@@ -180,6 +180,17 @@ config PCIE_ROCKCHIP
          There is 1 internal PCIe port available to support GEN2 with
          4 slots.
 
+config PCIE_MEDIATEK
+       bool "MediaTek PCIe controller"
+       depends on ARM && (ARCH_MEDIATEK || COMPILE_TEST)
+       depends on OF
+       depends on PCI
+       select PCIEPORTBUS
+       help
+         Say Y here if you want to enable PCIe controller support on
+         MT7623 series SoCs.  There is one single root complex with 3 root
+         ports available.  Each port supports Gen2 lane x1.
+
 config VMD
        depends on PCI_MSI && X86_64 && SRCU
        tristate "Intel Volume Management Device Driver"
index cab879578003f6c287adbbb058d00aab5d90ff99..b10d104c85fdd43e4baa2e06ea418268d8c7c749 100644 (file)
@@ -18,6 +18,7 @@ obj-$(CONFIG_PCIE_IPROC_BCMA) += pcie-iproc-bcma.o
 obj-$(CONFIG_PCIE_ALTERA) += pcie-altera.o
 obj-$(CONFIG_PCIE_ALTERA_MSI) += pcie-altera-msi.o
 obj-$(CONFIG_PCIE_ROCKCHIP) += pcie-rockchip.o
+obj-$(CONFIG_PCIE_MEDIATEK) += pcie-mediatek.o
 obj-$(CONFIG_VMD) += vmd.o
 
 # The following drivers are for devices that use the generic ACPI
diff --git a/drivers/pci/host/pcie-mediatek.c b/drivers/pci/host/pcie-mediatek.c
new file mode 100644 (file)
index 0000000..5a9d858
--- /dev/null
@@ -0,0 +1,554 @@
+/*
+ * MediaTek PCIe host controller driver.
+ *
+ * Copyright (c) 2017 MediaTek Inc.
+ * Author: Ryder Lee <ryder.lee@mediatek.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ */
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/kernel.h>
+#include <linux/of_address.h>
+#include <linux/of_pci.h>
+#include <linux/of_platform.h>
+#include <linux/pci.h>
+#include <linux/phy/phy.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/reset.h>
+
+/* PCIe shared registers */
+#define PCIE_SYS_CFG           0x00
+#define PCIE_INT_ENABLE                0x0c
+#define PCIE_CFG_ADDR          0x20
+#define PCIE_CFG_DATA          0x24
+
+/* PCIe per port registers */
+#define PCIE_BAR0_SETUP                0x10
+#define PCIE_CLASS             0x34
+#define PCIE_LINK_STATUS       0x50
+
+#define PCIE_PORT_INT_EN(x)    BIT(20 + (x))
+#define PCIE_PORT_PERST(x)     BIT(1 + (x))
+#define PCIE_PORT_LINKUP       BIT(0)
+#define PCIE_BAR_MAP_MAX       GENMASK(31, 16)
+
+#define PCIE_BAR_ENABLE                BIT(0)
+#define PCIE_REVISION_ID       BIT(0)
+#define PCIE_CLASS_CODE                (0x60400 << 8)
+#define PCIE_CONF_REG(regn)    (((regn) & GENMASK(7, 2)) | \
+                               ((((regn) >> 8) & GENMASK(3, 0)) << 24))
+#define PCIE_CONF_FUN(fun)     (((fun) << 8) & GENMASK(10, 8))
+#define PCIE_CONF_DEV(dev)     (((dev) << 11) & GENMASK(15, 11))
+#define PCIE_CONF_BUS(bus)     (((bus) << 16) & GENMASK(23, 16))
+#define PCIE_CONF_ADDR(regn, fun, dev, bus) \
+       (PCIE_CONF_REG(regn) | PCIE_CONF_FUN(fun) | \
+        PCIE_CONF_DEV(dev) | PCIE_CONF_BUS(bus))
+
+/* MediaTek specific configuration registers */
+#define PCIE_FTS_NUM           0x70c
+#define PCIE_FTS_NUM_MASK      GENMASK(15, 8)
+#define PCIE_FTS_NUM_L0(x)     ((x) & 0xff << 8)
+
+#define PCIE_FC_CREDIT         0x73c
+#define PCIE_FC_CREDIT_MASK    (GENMASK(31, 31) | GENMASK(28, 16))
+#define PCIE_FC_CREDIT_VAL(x)  ((x) << 16)
+
+/**
+ * struct mtk_pcie_port - PCIe port information
+ * @base: IO mapped register base
+ * @list: port list
+ * @pcie: pointer to PCIe host info
+ * @reset: pointer to port reset control
+ * @sys_ck: pointer to bus clock
+ * @phy: pointer to phy control block
+ * @lane: lane count
+ * @index: port index
+ */
+struct mtk_pcie_port {
+       void __iomem *base;
+       struct list_head list;
+       struct mtk_pcie *pcie;
+       struct reset_control *reset;
+       struct clk *sys_ck;
+       struct phy *phy;
+       u32 lane;
+       u32 index;
+};
+
+/**
+ * struct mtk_pcie - PCIe host information
+ * @dev: pointer to PCIe device
+ * @base: IO mapped register base
+ * @free_ck: free-run reference clock
+ * @io: IO resource
+ * @pio: PIO resource
+ * @mem: non-prefetchable memory resource
+ * @busn: bus range
+ * @offset: IO / Memory offset
+ * @ports: pointer to PCIe port information
+ */
+struct mtk_pcie {
+       struct device *dev;
+       void __iomem *base;
+       struct clk *free_ck;
+
+       struct resource io;
+       struct resource pio;
+       struct resource mem;
+       struct resource busn;
+       struct {
+               resource_size_t mem;
+               resource_size_t io;
+       } offset;
+       struct list_head ports;
+};
+
+static inline bool mtk_pcie_link_up(struct mtk_pcie_port *port)
+{
+       return !!(readl(port->base + PCIE_LINK_STATUS) & PCIE_PORT_LINKUP);
+}
+
+static void mtk_pcie_subsys_powerdown(struct mtk_pcie *pcie)
+{
+       struct device *dev = pcie->dev;
+
+       clk_disable_unprepare(pcie->free_ck);
+
+       if (dev->pm_domain) {
+               pm_runtime_put_sync(dev);
+               pm_runtime_disable(dev);
+       }
+}
+
+static void mtk_pcie_port_free(struct mtk_pcie_port *port)
+{
+       struct mtk_pcie *pcie = port->pcie;
+       struct device *dev = pcie->dev;
+
+       devm_iounmap(dev, port->base);
+       list_del(&port->list);
+       devm_kfree(dev, port);
+}
+
+static void mtk_pcie_put_resources(struct mtk_pcie *pcie)
+{
+       struct mtk_pcie_port *port, *tmp;
+
+       list_for_each_entry_safe(port, tmp, &pcie->ports, list) {
+               phy_power_off(port->phy);
+               clk_disable_unprepare(port->sys_ck);
+               mtk_pcie_port_free(port);
+       }
+
+       mtk_pcie_subsys_powerdown(pcie);
+}
+
+static void __iomem *mtk_pcie_map_bus(struct pci_bus *bus,
+                                     unsigned int devfn, int where)
+{
+       struct pci_host_bridge *host = pci_find_host_bridge(bus);
+       struct mtk_pcie *pcie = pci_host_bridge_priv(host);
+
+       writel(PCIE_CONF_ADDR(where, PCI_FUNC(devfn), PCI_SLOT(devfn),
+                             bus->number), pcie->base + PCIE_CFG_ADDR);
+
+       return pcie->base + PCIE_CFG_DATA + (where & 3);
+}
+
+static struct pci_ops mtk_pcie_ops = {
+       .map_bus = mtk_pcie_map_bus,
+       .read  = pci_generic_config_read,
+       .write = pci_generic_config_write,
+};
+
+static void mtk_pcie_configure_rc(struct mtk_pcie_port *port)
+{
+       struct mtk_pcie *pcie = port->pcie;
+       u32 func = PCI_FUNC(port->index << 3);
+       u32 slot = PCI_SLOT(port->index << 3);
+       u32 val;
+
+       /* enable interrupt */
+       val = readl(pcie->base + PCIE_INT_ENABLE);
+       val |= PCIE_PORT_INT_EN(port->index);
+       writel(val, pcie->base + PCIE_INT_ENABLE);
+
+       /* map to all DDR region. We need to set it before cfg operation. */
+       writel(PCIE_BAR_MAP_MAX | PCIE_BAR_ENABLE,
+              port->base + PCIE_BAR0_SETUP);
+
+       /* configure class code and revision ID */
+       writel(PCIE_CLASS_CODE | PCIE_REVISION_ID, port->base + PCIE_CLASS);
+
+       /* configure FC credit */
+       writel(PCIE_CONF_ADDR(PCIE_FC_CREDIT, func, slot, 0),
+              pcie->base + PCIE_CFG_ADDR);
+       val = readl(pcie->base + PCIE_CFG_DATA);
+       val &= ~PCIE_FC_CREDIT_MASK;
+       val |= PCIE_FC_CREDIT_VAL(0x806c);
+       writel(PCIE_CONF_ADDR(PCIE_FC_CREDIT, func, slot, 0),
+              pcie->base + PCIE_CFG_ADDR);
+       writel(val, pcie->base + PCIE_CFG_DATA);
+
+       /* configure RC FTS number to 250 when it leaves L0s */
+       writel(PCIE_CONF_ADDR(PCIE_FTS_NUM, func, slot, 0),
+              pcie->base + PCIE_CFG_ADDR);
+       val = readl(pcie->base + PCIE_CFG_DATA);
+       val &= ~PCIE_FTS_NUM_MASK;
+       val |= PCIE_FTS_NUM_L0(0x50);
+       writel(PCIE_CONF_ADDR(PCIE_FTS_NUM, func, slot, 0),
+              pcie->base + PCIE_CFG_ADDR);
+       writel(val, pcie->base + PCIE_CFG_DATA);
+}
+
+static void mtk_pcie_assert_ports(struct mtk_pcie_port *port)
+{
+       struct mtk_pcie *pcie = port->pcie;
+       u32 val;
+
+       /* assert port PERST_N */
+       val = readl(pcie->base + PCIE_SYS_CFG);
+       val |= PCIE_PORT_PERST(port->index);
+       writel(val, pcie->base + PCIE_SYS_CFG);
+
+       /* de-assert port PERST_N */
+       val = readl(pcie->base + PCIE_SYS_CFG);
+       val &= ~PCIE_PORT_PERST(port->index);
+       writel(val, pcie->base + PCIE_SYS_CFG);
+
+       /* PCIe v2.0 need at least 100ms delay to train from Gen1 to Gen2 */
+       msleep(100);
+}
+
+static void mtk_pcie_enable_ports(struct mtk_pcie_port *port)
+{
+       struct device *dev = port->pcie->dev;
+       int err;
+
+       err = clk_prepare_enable(port->sys_ck);
+       if (err) {
+               dev_err(dev, "failed to enable port%d clock\n", port->index);
+               goto err_sys_clk;
+       }
+
+       reset_control_assert(port->reset);
+       reset_control_deassert(port->reset);
+
+       err = phy_power_on(port->phy);
+       if (err) {
+               dev_err(dev, "failed to power on port%d phy\n", port->index);
+               goto err_phy_on;
+       }
+
+       mtk_pcie_assert_ports(port);
+
+       /* if link up, then setup root port configuration space */
+       if (mtk_pcie_link_up(port)) {
+               mtk_pcie_configure_rc(port);
+               return;
+       }
+
+       dev_info(dev, "Port%d link down\n", port->index);
+
+       phy_power_off(port->phy);
+err_phy_on:
+       clk_disable_unprepare(port->sys_ck);
+err_sys_clk:
+       mtk_pcie_port_free(port);
+}
+
+static int mtk_pcie_parse_ports(struct mtk_pcie *pcie,
+                               struct device_node *node,
+                               int index)
+{
+       struct mtk_pcie_port *port;
+       struct resource *regs;
+       struct device *dev = pcie->dev;
+       struct platform_device *pdev = to_platform_device(dev);
+       char name[10];
+       int err;
+
+       port = devm_kzalloc(dev, sizeof(*port), GFP_KERNEL);
+       if (!port)
+               return -ENOMEM;
+
+       err = of_property_read_u32(node, "num-lanes", &port->lane);
+       if (err) {
+               dev_err(dev, "missing num-lanes property\n");
+               return err;
+       }
+
+       regs = platform_get_resource(pdev, IORESOURCE_MEM, index + 1);
+       port->base = devm_ioremap_resource(dev, regs);
+       if (IS_ERR(port->base)) {
+               dev_err(dev, "failed to map port%d base\n", index);
+               return PTR_ERR(port->base);
+       }
+
+       snprintf(name, sizeof(name), "sys_ck%d", index);
+       port->sys_ck = devm_clk_get(dev, name);
+       if (IS_ERR(port->sys_ck)) {
+               dev_err(dev, "failed to get port%d clock\n", index);
+               return PTR_ERR(port->sys_ck);
+       }
+
+       snprintf(name, sizeof(name), "pcie-rst%d", index);
+       port->reset = devm_reset_control_get_optional(dev, name);
+       if (PTR_ERR(port->reset) == -EPROBE_DEFER)
+               return PTR_ERR(port->reset);
+
+       /* some platforms may use default PHY setting */
+       snprintf(name, sizeof(name), "pcie-phy%d", index);
+       port->phy = devm_phy_optional_get(dev, name);
+       if (IS_ERR(port->phy))
+               return PTR_ERR(port->phy);
+
+       port->index = index;
+       port->pcie = pcie;
+
+       INIT_LIST_HEAD(&port->list);
+       list_add_tail(&port->list, &pcie->ports);
+
+       return 0;
+}
+
+static int mtk_pcie_subsys_powerup(struct mtk_pcie *pcie)
+{
+       struct device *dev = pcie->dev;
+       struct platform_device *pdev = to_platform_device(dev);
+       struct resource *regs;
+       int err;
+
+       /* get shared registers */
+       regs = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+       pcie->base = devm_ioremap_resource(dev, regs);
+       if (IS_ERR(pcie->base)) {
+               dev_err(dev, "failed to map shared register\n");
+               return PTR_ERR(pcie->base);
+       }
+
+       pcie->free_ck = devm_clk_get(dev, "free_ck");
+       if (IS_ERR(pcie->free_ck)) {
+               if (PTR_ERR(pcie->free_ck) == -EPROBE_DEFER)
+                       return -EPROBE_DEFER;
+
+               pcie->free_ck = NULL;
+       }
+
+       if (dev->pm_domain) {
+               pm_runtime_enable(dev);
+               pm_runtime_get_sync(dev);
+       }
+
+       /* enable top level clock */
+       err = clk_prepare_enable(pcie->free_ck);
+       if (err) {
+               dev_err(dev, "failed to enable free_ck\n");
+               goto err_free_ck;
+       }
+
+       return 0;
+
+err_free_ck:
+       if (dev->pm_domain) {
+               pm_runtime_put_sync(dev);
+               pm_runtime_disable(dev);
+       }
+
+       return err;
+}
+
+static int mtk_pcie_setup(struct mtk_pcie *pcie)
+{
+       struct device *dev = pcie->dev;
+       struct device_node *node = dev->of_node, *child;
+       struct of_pci_range_parser parser;
+       struct of_pci_range range;
+       struct resource res;
+       struct mtk_pcie_port *port, *tmp;
+       int err;
+
+       if (of_pci_range_parser_init(&parser, node)) {
+               dev_err(dev, "missing \"ranges\" property\n");
+               return -EINVAL;
+       }
+
+       for_each_of_pci_range(&parser, &range) {
+               err = of_pci_range_to_resource(&range, node, &res);
+               if (err < 0)
+                       return err;
+
+               switch (res.flags & IORESOURCE_TYPE_BITS) {
+               case IORESOURCE_IO:
+                       pcie->offset.io = res.start - range.pci_addr;
+
+                       memcpy(&pcie->pio, &res, sizeof(res));
+                       pcie->pio.name = node->full_name;
+
+                       pcie->io.start = range.cpu_addr;
+                       pcie->io.end = range.cpu_addr + range.size - 1;
+                       pcie->io.flags = IORESOURCE_MEM;
+                       pcie->io.name = "I/O";
+
+                       memcpy(&res, &pcie->io, sizeof(res));
+                       break;
+
+               case IORESOURCE_MEM:
+                       pcie->offset.mem = res.start - range.pci_addr;
+
+                       memcpy(&pcie->mem, &res, sizeof(res));
+                       pcie->mem.name = "non-prefetchable";
+                       break;
+               }
+       }
+
+       err = of_pci_parse_bus_range(node, &pcie->busn);
+       if (err < 0) {
+               dev_err(dev, "failed to parse bus ranges property: %d\n", err);
+               pcie->busn.name = node->name;
+               pcie->busn.start = 0;
+               pcie->busn.end = 0xff;
+               pcie->busn.flags = IORESOURCE_BUS;
+       }
+
+       for_each_available_child_of_node(node, child) {
+               int index;
+
+               err = of_pci_get_devfn(child);
+               if (err < 0) {
+                       dev_err(dev, "failed to parse devfn: %d\n", err);
+                       return err;
+               }
+
+               index = PCI_SLOT(err);
+
+               err = mtk_pcie_parse_ports(pcie, child, index);
+               if (err)
+                       return err;
+       }
+
+       err = mtk_pcie_subsys_powerup(pcie);
+       if (err)
+               return err;
+
+       /* enable each port, and then check link status */
+       list_for_each_entry_safe(port, tmp, &pcie->ports, list)
+               mtk_pcie_enable_ports(port);
+
+       /* power down PCIe subsys if slots are all empty (link down) */
+       if (list_empty(&pcie->ports))
+               mtk_pcie_subsys_powerdown(pcie);
+
+       return 0;
+}
+
+static int mtk_pcie_request_resources(struct mtk_pcie *pcie)
+{
+       struct pci_host_bridge *host = pci_host_bridge_from_priv(pcie);
+       struct list_head *windows = &host->windows;
+       struct device *dev = pcie->dev;
+       int err;
+
+       pci_add_resource_offset(windows, &pcie->pio, pcie->offset.io);
+       pci_add_resource_offset(windows, &pcie->mem, pcie->offset.mem);
+       pci_add_resource(windows, &pcie->busn);
+
+       err = devm_request_pci_bus_resources(dev, windows);
+       if (err < 0)
+               return err;
+
+       pci_remap_iospace(&pcie->pio, pcie->io.start);
+
+       return 0;
+}
+
+static int mtk_pcie_register_host(struct pci_host_bridge *host)
+{
+       struct mtk_pcie *pcie = pci_host_bridge_priv(host);
+       struct pci_bus *child;
+       int err;
+
+       host->busnr = pcie->busn.start;
+       host->dev.parent = pcie->dev;
+       host->ops = &mtk_pcie_ops;
+       host->map_irq = of_irq_parse_and_map_pci;
+       host->swizzle_irq = pci_common_swizzle;
+
+       err = pci_scan_root_bus_bridge(host);
+       if (err < 0)
+               return err;
+
+       pci_bus_size_bridges(host->bus);
+       pci_bus_assign_resources(host->bus);
+
+       list_for_each_entry(child, &host->bus->children, node)
+               pcie_bus_configure_settings(child);
+
+       pci_bus_add_devices(host->bus);
+
+       return 0;
+}
+
+static int mtk_pcie_probe(struct platform_device *pdev)
+{
+       struct device *dev = &pdev->dev;
+       struct mtk_pcie *pcie;
+       struct pci_host_bridge *host;
+       int err;
+
+       host = devm_pci_alloc_host_bridge(dev, sizeof(*pcie));
+       if (!host)
+               return -ENOMEM;
+
+       pcie = pci_host_bridge_priv(host);
+
+       pcie->dev = dev;
+       platform_set_drvdata(pdev, pcie);
+       INIT_LIST_HEAD(&pcie->ports);
+
+       err = mtk_pcie_setup(pcie);
+       if (err)
+               return err;
+
+       err = mtk_pcie_request_resources(pcie);
+       if (err)
+               goto put_resources;
+
+       err = mtk_pcie_register_host(host);
+       if (err)
+               goto put_resources;
+
+       return 0;
+
+put_resources:
+       if (!list_empty(&pcie->ports))
+               mtk_pcie_put_resources(pcie);
+
+       return err;
+}
+
+static const struct of_device_id mtk_pcie_ids[] = {
+       { .compatible = "mediatek,mt7623-pcie"},
+       { .compatible = "mediatek,mt2701-pcie"},
+       {},
+};
+
+static struct platform_driver mtk_pcie_driver = {
+       .probe = mtk_pcie_probe,
+       .driver = {
+               .name = "mtk-pcie",
+               .of_match_table = mtk_pcie_ids,
+               .suppress_bind_attrs = true,
+       },
+};
+builtin_platform_driver(mtk_pcie_driver);