| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * PCIe host controller driver for StarFive JH7110 Soc. | |
| * | |
| * Copyright (C) 2023 StarFive Technology Co., Ltd. | |
| */ | |
| #include <linux/bitfield.h> | |
| #include <linux/clk.h> | |
| #include <linux/delay.h> | |
| #include <linux/gpio/consumer.h> | |
| #include <linux/interrupt.h> | |
| #include <linux/kernel.h> | |
| #include <linux/mfd/syscon.h> | |
| #include <linux/module.h> | |
| #include <linux/of_address.h> | |
| #include <linux/of_irq.h> | |
| #include <linux/of_pci.h> | |
| #include <linux/pci.h> | |
| #include <linux/phy/phy.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/pm_runtime.h> | |
| #include <linux/regmap.h> | |
| #include <linux/reset.h> | |
| #include "../../pci.h" | |
| #include "pcie-plda.h" | |
| #define PCIE_FUNC_NUM 4 | |
| /* system control */ | |
| #define STG_SYSCON_PCIE0_BASE 0x48 | |
| #define STG_SYSCON_PCIE1_BASE 0x1f8 | |
| #define STG_SYSCON_AR_OFFSET 0x78 | |
| #define STG_SYSCON_AXI4_SLVL_AR_MASK GENMASK(22, 8) | |
| #define STG_SYSCON_AXI4_SLVL_PHY_AR(x) FIELD_PREP(GENMASK(20, 17), x) | |
| #define STG_SYSCON_AW_OFFSET 0x7c | |
| #define STG_SYSCON_AXI4_SLVL_AW_MASK GENMASK(14, 0) | |
| #define STG_SYSCON_AXI4_SLVL_PHY_AW(x) FIELD_PREP(GENMASK(12, 9), x) | |
| #define STG_SYSCON_CLKREQ BIT(22) | |
| #define STG_SYSCON_CKREF_SRC_MASK GENMASK(19, 18) | |
| #define STG_SYSCON_RP_NEP_OFFSET 0xe8 | |
| #define STG_SYSCON_K_RP_NEP BIT(8) | |
| #define STG_SYSCON_LNKSTA_OFFSET 0x170 | |
| #define DATA_LINK_ACTIVE BIT(5) | |
| struct starfive_jh7110_pcie { | |
| struct plda_pcie_rp plda; | |
| struct reset_control *resets; | |
| struct clk_bulk_data *clks; | |
| struct regmap *reg_syscon; | |
| struct regulator *vpcie3v3; | |
| struct gpio_desc *reset_gpio; | |
| struct phy *phy; | |
| unsigned int stg_pcie_base; | |
| int num_clks; | |
| }; | |
| /* | |
| * JH7110 PCIe port BAR0/1 can be configured as 64-bit prefetchable memory | |
| * space. PCIe read and write requests targeting BAR0/1 are routed to so called | |
| * 'Bridge Configuration space' in PLDA IP datasheet, which contains the bridge | |
| * internal registers, such as interrupt, DMA and ATU registers... | |
| * JH7110 can access the Bridge Configuration space by local bus, and don`t | |
| * want the bridge internal registers accessed by the DMA from EP devices. | |
| * Thus, they are unimplemented and should be hidden here. | |
| */ | |
| static bool starfive_pcie_hide_rc_bar(struct pci_bus *bus, unsigned int devfn, | |
| int offset) | |
| { | |
| if (pci_is_root_bus(bus) && !devfn && | |
| (offset == PCI_BASE_ADDRESS_0 || offset == PCI_BASE_ADDRESS_1)) | |
| return true; | |
| return false; | |
| } | |
| static int starfive_pcie_config_write(struct pci_bus *bus, unsigned int devfn, | |
| int where, int size, u32 value) | |
| { | |
| if (starfive_pcie_hide_rc_bar(bus, devfn, where)) | |
| return PCIBIOS_SUCCESSFUL; | |
| return pci_generic_config_write(bus, devfn, where, size, value); | |
| } | |
| static int starfive_pcie_config_read(struct pci_bus *bus, unsigned int devfn, | |
| int where, int size, u32 *value) | |
| { | |
| if (starfive_pcie_hide_rc_bar(bus, devfn, where)) { | |
| *value = 0; | |
| return PCIBIOS_SUCCESSFUL; | |
| } | |
| return pci_generic_config_read(bus, devfn, where, size, value); | |
| } | |
| static int starfive_pcie_parse_dt(struct starfive_jh7110_pcie *pcie, | |
| struct device *dev) | |
| { | |
| int domain_nr; | |
| pcie->num_clks = devm_clk_bulk_get_all(dev, &pcie->clks); | |
| if (pcie->num_clks < 0) | |
| return dev_err_probe(dev, pcie->num_clks, | |
| "failed to get pcie clocks\n"); | |
| pcie->resets = devm_reset_control_array_get_exclusive(dev); | |
| if (IS_ERR(pcie->resets)) | |
| return dev_err_probe(dev, PTR_ERR(pcie->resets), | |
| "failed to get pcie resets"); | |
| pcie->reg_syscon = | |
| syscon_regmap_lookup_by_phandle(dev->of_node, | |
| "starfive,stg-syscon"); | |
| if (IS_ERR(pcie->reg_syscon)) | |
| return dev_err_probe(dev, PTR_ERR(pcie->reg_syscon), | |
| "failed to parse starfive,stg-syscon\n"); | |
| pcie->phy = devm_phy_optional_get(dev, NULL); | |
| if (IS_ERR(pcie->phy)) | |
| return dev_err_probe(dev, PTR_ERR(pcie->phy), | |
| "failed to get pcie phy\n"); | |
| /* | |
| * The PCIe domain numbers are set to be static in JH7110 DTS. | |
| * As the STG system controller defines different bases in PCIe RP0 & | |
| * RP1, we use them to identify which controller is doing the hardware | |
| * initialization. | |
| */ | |
| domain_nr = of_get_pci_domain_nr(dev->of_node); | |
| if (domain_nr < 0 || domain_nr > 1) | |
| return dev_err_probe(dev, -ENODEV, | |
| "failed to get valid pcie domain\n"); | |
| if (domain_nr == 0) | |
| pcie->stg_pcie_base = STG_SYSCON_PCIE0_BASE; | |
| else | |
| pcie->stg_pcie_base = STG_SYSCON_PCIE1_BASE; | |
| pcie->reset_gpio = devm_gpiod_get_optional(dev, "perst", | |
| GPIOD_OUT_HIGH); | |
| if (IS_ERR(pcie->reset_gpio)) | |
| return dev_err_probe(dev, PTR_ERR(pcie->reset_gpio), | |
| "failed to get perst-gpio\n"); | |
| pcie->vpcie3v3 = devm_regulator_get_optional(dev, "vpcie3v3"); | |
| if (IS_ERR(pcie->vpcie3v3)) { | |
| if (PTR_ERR(pcie->vpcie3v3) != -ENODEV) | |
| return dev_err_probe(dev, PTR_ERR(pcie->vpcie3v3), | |
| "failed to get vpcie3v3 regulator\n"); | |
| pcie->vpcie3v3 = NULL; | |
| } | |
| return 0; | |
| } | |
| static struct pci_ops starfive_pcie_ops = { | |
| .map_bus = plda_pcie_map_bus, | |
| .read = starfive_pcie_config_read, | |
| .write = starfive_pcie_config_write, | |
| }; | |
| static int starfive_pcie_clk_rst_init(struct starfive_jh7110_pcie *pcie) | |
| { | |
| struct device *dev = pcie->plda.dev; | |
| int ret; | |
| ret = clk_bulk_prepare_enable(pcie->num_clks, pcie->clks); | |
| if (ret) | |
| return dev_err_probe(dev, ret, "failed to enable clocks\n"); | |
| ret = reset_control_deassert(pcie->resets); | |
| if (ret) { | |
| clk_bulk_disable_unprepare(pcie->num_clks, pcie->clks); | |
| dev_err_probe(dev, ret, "failed to deassert resets\n"); | |
| } | |
| return ret; | |
| } | |
| static void starfive_pcie_clk_rst_deinit(struct starfive_jh7110_pcie *pcie) | |
| { | |
| reset_control_assert(pcie->resets); | |
| clk_bulk_disable_unprepare(pcie->num_clks, pcie->clks); | |
| } | |
| static bool starfive_pcie_link_up(struct plda_pcie_rp *plda) | |
| { | |
| struct starfive_jh7110_pcie *pcie = | |
| container_of(plda, struct starfive_jh7110_pcie, plda); | |
| int ret; | |
| u32 stg_reg_val; | |
| ret = regmap_read(pcie->reg_syscon, | |
| pcie->stg_pcie_base + STG_SYSCON_LNKSTA_OFFSET, | |
| &stg_reg_val); | |
| if (ret) { | |
| dev_err(pcie->plda.dev, "failed to read link status\n"); | |
| return false; | |
| } | |
| return !!(stg_reg_val & DATA_LINK_ACTIVE); | |
| } | |
| static int starfive_pcie_host_wait_for_link(struct starfive_jh7110_pcie *pcie) | |
| { | |
| int retries; | |
| /* Check if the link is up or not */ | |
| for (retries = 0; retries < PCIE_LINK_WAIT_MAX_RETRIES; retries++) { | |
| if (starfive_pcie_link_up(&pcie->plda)) { | |
| dev_info(pcie->plda.dev, "port link up\n"); | |
| return 0; | |
| } | |
| msleep(PCIE_LINK_WAIT_SLEEP_MS); | |
| } | |
| return -ETIMEDOUT; | |
| } | |
| static int starfive_pcie_enable_phy(struct device *dev, | |
| struct starfive_jh7110_pcie *pcie) | |
| { | |
| int ret; | |
| if (!pcie->phy) | |
| return 0; | |
| ret = phy_init(pcie->phy); | |
| if (ret) | |
| return dev_err_probe(dev, ret, | |
| "failed to initialize pcie phy\n"); | |
| ret = phy_set_mode(pcie->phy, PHY_MODE_PCIE); | |
| if (ret) { | |
| dev_err_probe(dev, ret, "failed to set pcie mode\n"); | |
| goto err_phy_on; | |
| } | |
| ret = phy_power_on(pcie->phy); | |
| if (ret) { | |
| dev_err_probe(dev, ret, "failed to power on pcie phy\n"); | |
| goto err_phy_on; | |
| } | |
| return 0; | |
| err_phy_on: | |
| phy_exit(pcie->phy); | |
| return ret; | |
| } | |
| static void starfive_pcie_disable_phy(struct starfive_jh7110_pcie *pcie) | |
| { | |
| phy_power_off(pcie->phy); | |
| phy_exit(pcie->phy); | |
| } | |
| static void starfive_pcie_host_deinit(struct plda_pcie_rp *plda) | |
| { | |
| struct starfive_jh7110_pcie *pcie = | |
| container_of(plda, struct starfive_jh7110_pcie, plda); | |
| starfive_pcie_clk_rst_deinit(pcie); | |
| if (pcie->vpcie3v3) | |
| regulator_disable(pcie->vpcie3v3); | |
| starfive_pcie_disable_phy(pcie); | |
| } | |
| static int starfive_pcie_host_init(struct plda_pcie_rp *plda) | |
| { | |
| struct starfive_jh7110_pcie *pcie = | |
| container_of(plda, struct starfive_jh7110_pcie, plda); | |
| struct device *dev = plda->dev; | |
| int ret; | |
| int i; | |
| ret = starfive_pcie_enable_phy(dev, pcie); | |
| if (ret) | |
| return ret; | |
| regmap_update_bits(pcie->reg_syscon, | |
| pcie->stg_pcie_base + STG_SYSCON_RP_NEP_OFFSET, | |
| STG_SYSCON_K_RP_NEP, STG_SYSCON_K_RP_NEP); | |
| regmap_update_bits(pcie->reg_syscon, | |
| pcie->stg_pcie_base + STG_SYSCON_AW_OFFSET, | |
| STG_SYSCON_CKREF_SRC_MASK, | |
| FIELD_PREP(STG_SYSCON_CKREF_SRC_MASK, 2)); | |
| regmap_update_bits(pcie->reg_syscon, | |
| pcie->stg_pcie_base + STG_SYSCON_AW_OFFSET, | |
| STG_SYSCON_CLKREQ, STG_SYSCON_CLKREQ); | |
| ret = starfive_pcie_clk_rst_init(pcie); | |
| if (ret) | |
| goto err_disable_phy; | |
| if (pcie->vpcie3v3) { | |
| ret = regulator_enable(pcie->vpcie3v3); | |
| if (ret) { | |
| dev_err_probe(dev, ret, "failed to enable vpcie3v3 regulator\n"); | |
| goto err_clk_rst; | |
| } | |
| } | |
| if (pcie->reset_gpio) | |
| gpiod_set_value_cansleep(pcie->reset_gpio, 1); | |
| /* Disable physical functions except #0 */ | |
| for (i = 1; i < PCIE_FUNC_NUM; i++) { | |
| regmap_update_bits(pcie->reg_syscon, | |
| pcie->stg_pcie_base + STG_SYSCON_AR_OFFSET, | |
| STG_SYSCON_AXI4_SLVL_AR_MASK, | |
| STG_SYSCON_AXI4_SLVL_PHY_AR(i)); | |
| regmap_update_bits(pcie->reg_syscon, | |
| pcie->stg_pcie_base + STG_SYSCON_AW_OFFSET, | |
| STG_SYSCON_AXI4_SLVL_AW_MASK, | |
| STG_SYSCON_AXI4_SLVL_PHY_AW(i)); | |
| plda_pcie_disable_func(plda); | |
| } | |
| regmap_update_bits(pcie->reg_syscon, | |
| pcie->stg_pcie_base + STG_SYSCON_AR_OFFSET, | |
| STG_SYSCON_AXI4_SLVL_AR_MASK, 0); | |
| regmap_update_bits(pcie->reg_syscon, | |
| pcie->stg_pcie_base + STG_SYSCON_AW_OFFSET, | |
| STG_SYSCON_AXI4_SLVL_AW_MASK, 0); | |
| plda_pcie_enable_root_port(plda); | |
| plda_pcie_write_rc_bar(plda, 0); | |
| /* PCIe PCI Standard Configuration Identification Settings. */ | |
| plda_pcie_set_standard_class(plda); | |
| /* | |
| * The LTR message receiving is enabled by the register "PCIe Message | |
| * Reception" as default, but the forward id & addr are uninitialized. | |
| * If we do not disable LTR message forwarding here, or set a legal | |
| * forwarding address, the kernel will get stuck. | |
| * To workaround, disable the LTR message forwarding here before using | |
| * this feature. | |
| */ | |
| plda_pcie_disable_ltr(plda); | |
| /* | |
| * Enable the prefetchable memory window 64-bit addressing in JH7110. | |
| * The 64-bits prefetchable address translation configurations in ATU | |
| * can be work after enable the register setting below. | |
| */ | |
| plda_pcie_set_pref_win_64bit(plda); | |
| /* | |
| * Ensure that PERST has been asserted for at least 100 ms, | |
| * the sleep value is T_PVPERL from PCIe CEM spec r2.0 (Table 2-4) | |
| */ | |
| msleep(100); | |
| if (pcie->reset_gpio) | |
| gpiod_set_value_cansleep(pcie->reset_gpio, 0); | |
| /* | |
| * With a Downstream Port (<=5GT/s), software must wait a minimum | |
| * of 100ms following exit from a conventional reset before | |
| * sending a configuration request to the device. | |
| */ | |
| msleep(PCIE_RESET_CONFIG_WAIT_MS); | |
| if (starfive_pcie_host_wait_for_link(pcie)) | |
| dev_info(dev, "port link down\n"); | |
| return 0; | |
| err_clk_rst: | |
| starfive_pcie_clk_rst_deinit(pcie); | |
| err_disable_phy: | |
| starfive_pcie_disable_phy(pcie); | |
| return ret; | |
| } | |
| static const struct plda_pcie_host_ops sf_host_ops = { | |
| .host_init = starfive_pcie_host_init, | |
| .host_deinit = starfive_pcie_host_deinit, | |
| }; | |
| static const struct plda_event stf_pcie_event = { | |
| .intx_event = EVENT_PM_MSI_INT_INTX, | |
| .msi_event = EVENT_PM_MSI_INT_MSI | |
| }; | |
| static int starfive_pcie_probe(struct platform_device *pdev) | |
| { | |
| struct starfive_jh7110_pcie *pcie; | |
| struct device *dev = &pdev->dev; | |
| struct plda_pcie_rp *plda; | |
| int ret; | |
| pcie = devm_kzalloc(dev, sizeof(*pcie), GFP_KERNEL); | |
| if (!pcie) | |
| return -ENOMEM; | |
| plda = &pcie->plda; | |
| plda->dev = dev; | |
| ret = starfive_pcie_parse_dt(pcie, dev); | |
| if (ret) | |
| return ret; | |
| pm_runtime_enable(&pdev->dev); | |
| ret = pm_runtime_resume_and_get(&pdev->dev); | |
| if (ret < 0) { | |
| pm_runtime_disable(&pdev->dev); | |
| return dev_err_probe(dev, ret, "failed to resume device\n"); | |
| } | |
| plda->host_ops = &sf_host_ops; | |
| plda->num_events = PLDA_MAX_EVENT_NUM; | |
| /* mask doorbell event */ | |
| plda->events_bitmap = GENMASK(PLDA_INT_EVENT_NUM - 1, 0) | |
| & ~BIT(PLDA_AXI_DOORBELL) | |
| & ~BIT(PLDA_PCIE_DOORBELL); | |
| plda->events_bitmap <<= PLDA_NUM_DMA_EVENTS; | |
| ret = plda_pcie_host_init(&pcie->plda, &starfive_pcie_ops, | |
| &stf_pcie_event); | |
| if (ret) { | |
| pm_runtime_put_sync(&pdev->dev); | |
| pm_runtime_disable(&pdev->dev); | |
| return ret; | |
| } | |
| platform_set_drvdata(pdev, pcie); | |
| return 0; | |
| } | |
| static void starfive_pcie_remove(struct platform_device *pdev) | |
| { | |
| struct starfive_jh7110_pcie *pcie = platform_get_drvdata(pdev); | |
| plda_pcie_host_deinit(&pcie->plda); | |
| pm_runtime_put_sync(&pdev->dev); | |
| pm_runtime_disable(&pdev->dev); | |
| platform_set_drvdata(pdev, NULL); | |
| } | |
| static int starfive_pcie_suspend_noirq(struct device *dev) | |
| { | |
| struct starfive_jh7110_pcie *pcie = dev_get_drvdata(dev); | |
| clk_bulk_disable_unprepare(pcie->num_clks, pcie->clks); | |
| starfive_pcie_disable_phy(pcie); | |
| return 0; | |
| } | |
| static int starfive_pcie_resume_noirq(struct device *dev) | |
| { | |
| struct starfive_jh7110_pcie *pcie = dev_get_drvdata(dev); | |
| int ret; | |
| ret = starfive_pcie_enable_phy(dev, pcie); | |
| if (ret) | |
| return ret; | |
| ret = clk_bulk_prepare_enable(pcie->num_clks, pcie->clks); | |
| if (ret) { | |
| dev_err(dev, "failed to enable clocks\n"); | |
| starfive_pcie_disable_phy(pcie); | |
| return ret; | |
| } | |
| return 0; | |
| } | |
| static const struct dev_pm_ops starfive_pcie_pm_ops = { | |
| NOIRQ_SYSTEM_SLEEP_PM_OPS(starfive_pcie_suspend_noirq, | |
| starfive_pcie_resume_noirq) | |
| }; | |
| static const struct of_device_id starfive_pcie_of_match[] = { | |
| { .compatible = "starfive,jh7110-pcie", }, | |
| { /* sentinel */ } | |
| }; | |
| MODULE_DEVICE_TABLE(of, starfive_pcie_of_match); | |
| static struct platform_driver starfive_pcie_driver = { | |
| .driver = { | |
| .name = "pcie-starfive", | |
| .of_match_table = of_match_ptr(starfive_pcie_of_match), | |
| .pm = pm_sleep_ptr(&starfive_pcie_pm_ops), | |
| }, | |
| .probe = starfive_pcie_probe, | |
| .remove = starfive_pcie_remove, | |
| }; | |
| module_platform_driver(starfive_pcie_driver); | |
| MODULE_DESCRIPTION("StarFive JH7110 PCIe host driver"); | |
| MODULE_LICENSE("GPL v2"); |