| // SPDX-License-Identifier: GPL-2.0+ |
| /* |
| * Amlogic DesignWare based PCIe host controller driver |
| * |
| * Copyright (c) 2021 BayLibre, SAS |
| * Author: Neil Armstrong <narmstrong@baylibre.com> |
| * |
| * Based on pcie_dw_rockchip.c |
| * Copyright (c) 2021 Rockchip, Inc. |
| */ |
| |
| #include <common.h> |
| #include <clk.h> |
| #include <dm.h> |
| #include <generic-phy.h> |
| #include <pci.h> |
| #include <power-domain.h> |
| #include <reset.h> |
| #include <syscon.h> |
| #include <asm/global_data.h> |
| #include <asm/io.h> |
| #include <asm-generic/gpio.h> |
| #include <dm/device_compat.h> |
| #include <linux/iopoll.h> |
| #include <linux/delay.h> |
| #include <linux/log2.h> |
| #include <linux/bitfield.h> |
| |
| #include "pcie_dw_common.h" |
| |
| DECLARE_GLOBAL_DATA_PTR; |
| |
| /** |
| * struct meson_pcie - Amlogic Meson DW PCIe controller state |
| * |
| * @pci: The common PCIe DW structure |
| * @meson_cfg_base: The base address of vendor regs |
| * @phy |
| * @clk_port |
| * @clk_general |
| * @clk_pclk |
| * @rsts |
| * @rst_gpio: The #PERST signal for slot |
| */ |
| struct meson_pcie { |
| /* Must be first member of the struct */ |
| struct pcie_dw dw; |
| void *meson_cfg_base; |
| struct phy phy; |
| struct clk clk_port; |
| struct clk clk_general; |
| struct clk clk_pclk; |
| struct reset_ctl_bulk rsts; |
| struct gpio_desc rst_gpio; |
| }; |
| |
| #define PCI_EXP_DEVCTL_PAYLOAD 0x00e0 /* Max_Payload_Size */ |
| |
| #define PCIE_CAP_MAX_PAYLOAD_SIZE(x) ((x) << 5) |
| #define PCIE_CAP_MAX_READ_REQ_SIZE(x) ((x) << 12) |
| |
| /* PCIe specific config registers */ |
| #define PCIE_CFG0 0x0 |
| #define APP_LTSSM_ENABLE BIT(7) |
| |
| #define PCIE_CFG_STATUS12 0x30 |
| #define IS_SMLH_LINK_UP(x) ((x) & (1 << 6)) |
| #define IS_RDLH_LINK_UP(x) ((x) & (1 << 16)) |
| #define IS_LTSSM_UP(x) ((((x) >> 10) & 0x1f) == 0x11) |
| |
| #define PCIE_CFG_STATUS17 0x44 |
| #define PM_CURRENT_STATE(x) (((x) >> 7) & 0x1) |
| |
| #define WAIT_LINKUP_TIMEOUT 4000 |
| #define PORT_CLK_RATE 100000000UL |
| #define MAX_PAYLOAD_SIZE 256 |
| #define MAX_READ_REQ_SIZE 256 |
| #define PCIE_RESET_DELAY 500 |
| #define PCIE_SHARED_RESET 1 |
| #define PCIE_NORMAL_RESET 0 |
| |
| enum pcie_data_rate { |
| PCIE_GEN1, |
| PCIE_GEN2, |
| PCIE_GEN3, |
| PCIE_GEN4 |
| }; |
| |
| /* Parameters for the waiting for #perst signal */ |
| #define PERST_WAIT_US 1000000 |
| |
| static inline u32 meson_cfg_readl(struct meson_pcie *priv, u32 reg) |
| { |
| return readl(priv->meson_cfg_base + reg); |
| } |
| |
| static inline void meson_cfg_writel(struct meson_pcie *priv, u32 val, u32 reg) |
| { |
| writel(val, priv->meson_cfg_base + reg); |
| } |
| |
| /** |
| * meson_pcie_configure() - Configure link |
| * |
| * @meson_pcie: Pointer to the PCI controller state |
| * |
| * Configure the link mode and width |
| */ |
| static void meson_pcie_configure(struct meson_pcie *priv) |
| { |
| u32 val; |
| |
| dw_pcie_dbi_write_enable(&priv->dw, true); |
| |
| val = readl(priv->dw.dbi_base + PCIE_PORT_LINK_CONTROL); |
| val &= ~PORT_LINK_FAST_LINK_MODE; |
| val |= PORT_LINK_DLL_LINK_EN; |
| val &= ~PORT_LINK_MODE_MASK; |
| val |= PORT_LINK_MODE_1_LANES; |
| writel(val, priv->dw.dbi_base + PCIE_PORT_LINK_CONTROL); |
| |
| val = readl(priv->dw.dbi_base + PCIE_LINK_WIDTH_SPEED_CONTROL); |
| val &= ~PORT_LOGIC_LINK_WIDTH_MASK; |
| val |= PORT_LOGIC_LINK_WIDTH_1_LANES; |
| writel(val, priv->dw.dbi_base + PCIE_LINK_WIDTH_SPEED_CONTROL); |
| |
| dw_pcie_dbi_write_enable(&priv->dw, false); |
| } |
| |
| static inline void meson_pcie_enable_ltssm(struct meson_pcie *priv) |
| { |
| u32 val; |
| |
| val = meson_cfg_readl(priv, PCIE_CFG0); |
| val |= APP_LTSSM_ENABLE; |
| meson_cfg_writel(priv, val, PCIE_CFG0); |
| } |
| |
| static int meson_pcie_wait_link_up(struct meson_pcie *priv) |
| { |
| u32 speed_okay = 0; |
| u32 cnt = 0; |
| u32 state12, state17, smlh_up, ltssm_up, rdlh_up; |
| |
| do { |
| state12 = meson_cfg_readl(priv, PCIE_CFG_STATUS12); |
| state17 = meson_cfg_readl(priv, PCIE_CFG_STATUS17); |
| smlh_up = IS_SMLH_LINK_UP(state12); |
| rdlh_up = IS_RDLH_LINK_UP(state12); |
| ltssm_up = IS_LTSSM_UP(state12); |
| |
| if (PM_CURRENT_STATE(state17) < PCIE_GEN3) |
| speed_okay = 1; |
| |
| if (smlh_up) |
| debug("%s: smlh_link_up is on\n", __func__); |
| if (rdlh_up) |
| debug("%s: rdlh_link_up is on\n", __func__); |
| if (ltssm_up) |
| debug("%s: ltssm_up is on\n", __func__); |
| if (speed_okay) |
| debug("%s: speed_okay\n", __func__); |
| |
| if (smlh_up && rdlh_up && ltssm_up && speed_okay) |
| return 0; |
| |
| cnt++; |
| |
| udelay(10); |
| } while (cnt < WAIT_LINKUP_TIMEOUT); |
| |
| printf("%s: error: wait linkup timeout\n", __func__); |
| return -EIO; |
| } |
| |
| /** |
| * meson_pcie_link_up() - Wait for the link to come up |
| * |
| * @meson_pcie: Pointer to the PCI controller state |
| * @cap_speed: Desired link speed |
| * |
| * Return: 1 (true) for active line and negative (false) for no link (timeout) |
| */ |
| static int meson_pcie_link_up(struct meson_pcie *priv, u32 cap_speed) |
| { |
| /* DW link configurations */ |
| meson_pcie_configure(priv); |
| |
| /* Reset the device */ |
| if (dm_gpio_is_valid(&priv->rst_gpio)) { |
| dm_gpio_set_value(&priv->rst_gpio, 1); |
| /* |
| * Minimal is 100ms from spec but we see |
| * some wired devices need much more, such as 600ms. |
| * Add a enough delay to cover all cases. |
| */ |
| udelay(PERST_WAIT_US); |
| dm_gpio_set_value(&priv->rst_gpio, 0); |
| } |
| |
| /* Enable LTSSM */ |
| meson_pcie_enable_ltssm(priv); |
| |
| return meson_pcie_wait_link_up(priv); |
| } |
| |
| static int meson_size_to_payload(int size) |
| { |
| /* |
| * dwc supports 2^(val+7) payload size, which val is 0~5 default to 1. |
| * So if input size is not 2^order alignment or less than 2^7 or bigger |
| * than 2^12, just set to default size 2^(1+7). |
| */ |
| if (!is_power_of_2(size) || size < 128 || size > 4096) { |
| debug("%s: payload size %d, set to default 256\n", __func__, size); |
| return 1; |
| } |
| |
| return fls(size) - 8; |
| } |
| |
| static void meson_set_max_payload(struct meson_pcie *priv, int size) |
| { |
| u32 val; |
| u16 offset = dm_pci_find_capability(priv->dw.dev, PCI_CAP_ID_EXP); |
| int max_payload_size = meson_size_to_payload(size); |
| |
| dw_pcie_dbi_write_enable(&priv->dw, true); |
| |
| val = readl(priv->dw.dbi_base + offset + PCI_EXP_DEVCTL); |
| val &= ~PCI_EXP_DEVCTL_PAYLOAD; |
| writel(val, priv->dw.dbi_base + offset + PCI_EXP_DEVCTL); |
| |
| val = readl(priv->dw.dbi_base + offset + PCI_EXP_DEVCTL); |
| val |= PCIE_CAP_MAX_PAYLOAD_SIZE(max_payload_size); |
| writel(val, priv->dw.dbi_base + PCI_EXP_DEVCTL); |
| |
| dw_pcie_dbi_write_enable(&priv->dw, false); |
| } |
| |
| static void meson_set_max_rd_req_size(struct meson_pcie *priv, int size) |
| { |
| u32 val; |
| u16 offset = dm_pci_find_capability(priv->dw.dev, PCI_CAP_ID_EXP); |
| int max_rd_req_size = meson_size_to_payload(size); |
| |
| dw_pcie_dbi_write_enable(&priv->dw, true); |
| |
| val = readl(priv->dw.dbi_base + offset + PCI_EXP_DEVCTL); |
| val &= ~PCI_EXP_DEVCTL_PAYLOAD; |
| writel(val, priv->dw.dbi_base + offset + PCI_EXP_DEVCTL); |
| |
| val = readl(priv->dw.dbi_base + offset + PCI_EXP_DEVCTL); |
| val |= PCIE_CAP_MAX_READ_REQ_SIZE(max_rd_req_size); |
| writel(val, priv->dw.dbi_base + PCI_EXP_DEVCTL); |
| |
| dw_pcie_dbi_write_enable(&priv->dw, false); |
| } |
| |
| static int meson_pcie_init_port(struct udevice *dev) |
| { |
| int ret; |
| struct meson_pcie *priv = dev_get_priv(dev); |
| |
| ret = generic_phy_init(&priv->phy); |
| if (ret) { |
| dev_err(dev, "failed to init phy (ret=%d)\n", ret); |
| return ret; |
| } |
| |
| ret = generic_phy_power_on(&priv->phy); |
| if (ret) { |
| dev_err(dev, "failed to power on phy (ret=%d)\n", ret); |
| goto err_exit_phy; |
| } |
| |
| ret = generic_phy_reset(&priv->phy); |
| if (ret) { |
| dev_err(dev, "failed to reset phy (ret=%d)\n", ret); |
| goto err_exit_phy; |
| } |
| |
| ret = reset_assert_bulk(&priv->rsts); |
| if (ret) { |
| dev_err(dev, "failed to assert resets (ret=%d)\n", ret); |
| goto err_power_off_phy; |
| } |
| |
| udelay(PCIE_RESET_DELAY); |
| |
| ret = reset_deassert_bulk(&priv->rsts); |
| if (ret) { |
| dev_err(dev, "failed to deassert resets (ret=%d)\n", ret); |
| goto err_power_off_phy; |
| } |
| |
| udelay(PCIE_RESET_DELAY); |
| |
| ret = clk_set_rate(&priv->clk_port, PORT_CLK_RATE); |
| if (ret) { |
| dev_err(dev, "failed to set port clk rate (ret=%d)\n", ret); |
| goto err_deassert_bulk; |
| } |
| |
| ret = clk_enable(&priv->clk_general); |
| if (ret) { |
| dev_err(dev, "failed to enable clk general (ret=%d)\n", ret); |
| goto err_deassert_bulk; |
| } |
| |
| ret = clk_enable(&priv->clk_pclk); |
| if (ret) { |
| dev_err(dev, "failed to enable pclk (ret=%d)\n", ret); |
| goto err_deassert_bulk; |
| } |
| |
| meson_set_max_payload(priv, MAX_PAYLOAD_SIZE); |
| meson_set_max_rd_req_size(priv, MAX_READ_REQ_SIZE); |
| |
| pcie_dw_setup_host(&priv->dw); |
| |
| meson_pcie_link_up(priv, LINK_SPEED_GEN_2); |
| |
| return 0; |
| err_deassert_bulk: |
| reset_assert_bulk(&priv->rsts); |
| err_power_off_phy: |
| generic_phy_power_off(&priv->phy); |
| err_exit_phy: |
| generic_phy_exit(&priv->phy); |
| |
| return ret; |
| } |
| |
| static int meson_pcie_parse_dt(struct udevice *dev) |
| { |
| struct meson_pcie *priv = dev_get_priv(dev); |
| int ret; |
| |
| priv->dw.dbi_base = (void *)dev_read_addr_index(dev, 0); |
| if (!priv->dw.dbi_base) |
| return -ENODEV; |
| |
| dev_dbg(dev, "ELBI address is 0x%p\n", priv->dw.dbi_base); |
| |
| priv->meson_cfg_base = (void *)dev_read_addr_index(dev, 1); |
| if (!priv->meson_cfg_base) |
| return -ENODEV; |
| |
| dev_dbg(dev, "CFG address is 0x%p\n", priv->meson_cfg_base); |
| |
| ret = gpio_request_by_name(dev, "reset-gpios", 0, |
| &priv->rst_gpio, GPIOD_IS_OUT); |
| if (ret) { |
| dev_err(dev, "failed to find reset-gpios property\n"); |
| return ret; |
| } |
| |
| ret = reset_get_bulk(dev, &priv->rsts); |
| if (ret) { |
| dev_err(dev, "Can't get reset: %d\n", ret); |
| return ret; |
| } |
| |
| ret = clk_get_by_name(dev, "port", &priv->clk_port); |
| if (ret) { |
| dev_err(dev, "Can't get port clock: %d\n", ret); |
| return ret; |
| } |
| |
| ret = clk_get_by_name(dev, "general", &priv->clk_general); |
| if (ret) { |
| dev_err(dev, "Can't get port clock: %d\n", ret); |
| return ret; |
| } |
| |
| ret = clk_get_by_name(dev, "pclk", &priv->clk_pclk); |
| if (ret) { |
| dev_err(dev, "Can't get port clock: %d\n", ret); |
| return ret; |
| } |
| |
| ret = generic_phy_get_by_index(dev, 0, &priv->phy); |
| if (ret) { |
| dev_err(dev, "failed to get pcie phy (ret=%d)\n", ret); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * meson_pcie_probe() - Probe the PCIe bus for active link |
| * |
| * @dev: A pointer to the device being operated on |
| * |
| * Probe for an active link on the PCIe bus and configure the controller |
| * to enable this port. |
| * |
| * Return: 0 on success, else -ENODEV |
| */ |
| static int meson_pcie_probe(struct udevice *dev) |
| { |
| struct meson_pcie *priv = dev_get_priv(dev); |
| struct udevice *ctlr = pci_get_controller(dev); |
| struct pci_controller *hose = dev_get_uclass_priv(ctlr); |
| int ret = 0; |
| |
| priv->dw.first_busno = dev_seq(dev); |
| priv->dw.dev = dev; |
| |
| ret = meson_pcie_parse_dt(dev); |
| if (ret) |
| return ret; |
| |
| ret = meson_pcie_init_port(dev); |
| if (ret) { |
| dm_gpio_free(dev, &priv->rst_gpio); |
| return ret; |
| } |
| |
| printf("PCIE-%d: Link up (Gen%d-x%d, Bus%d)\n", |
| dev_seq(dev), pcie_dw_get_link_speed(&priv->dw), |
| pcie_dw_get_link_width(&priv->dw), |
| hose->first_busno); |
| |
| return pcie_dw_prog_outbound_atu_unroll(&priv->dw, |
| PCIE_ATU_REGION_INDEX0, |
| PCIE_ATU_TYPE_MEM, |
| priv->dw.mem.phys_start, |
| priv->dw.mem.bus_start, |
| priv->dw.mem.size); |
| } |
| |
| static const struct dm_pci_ops meson_pcie_ops = { |
| .read_config = pcie_dw_read_config, |
| .write_config = pcie_dw_write_config, |
| }; |
| |
| static const struct udevice_id meson_pcie_ids[] = { |
| { .compatible = "amlogic,axg-pcie" }, |
| { .compatible = "amlogic,g12a-pcie" }, |
| { } |
| }; |
| |
| U_BOOT_DRIVER(meson_dw_pcie) = { |
| .name = "pcie_dw_meson", |
| .id = UCLASS_PCI, |
| .of_match = meson_pcie_ids, |
| .ops = &meson_pcie_ops, |
| .probe = meson_pcie_probe, |
| .priv_auto = sizeof(struct meson_pcie), |
| }; |