[PATCH 3/3] phy: tegra: Add lane margin support

From: Manikanta Maddireddy
Date: Thu Nov 24 2022 - 03:36:42 EST


Per PCIe r6.0.1, section 4.2.18, Lane Margining at Receiver is mandatory
for all Ports supporting a data rate of 16.0 GT/s or higher. Tegra234
supports Gen4 and Receiver Lane Margining with per lane PIPE2UPHY instance
acting as a relay between PCIe controller and Universal PHY (UPHY).

P2U driver enables MARGIN_SW_READY and MARGIN_READY bits to start snooping
on changes in lane margin control register in PCIe configuration space.
P2U HW generates MARGIN_START or MARGIN_CHANGE interrupt after it copied
margin control data to P2U_RX_MARGIN_CTRL register. On MARGIN_START or
MARGIN_CHANGE interrupt, P2U driver copies margin control data to UPHY
via mailbox communication. UPHY HW performs margining operation and
P2U driver copies margin status from UPHY to P2U_RX_MARGIN_STATUS
register. P2U driver repeats this until PCIe controller issues margin
stop command.

Signed-off-by: Manikanta Maddireddy <mmaddireddy@xxxxxxxxxx>
---
drivers/phy/tegra/phy-tegra194-p2u.c | 272 +++++++++++++++++++++++++++
1 file changed, 272 insertions(+)

diff --git a/drivers/phy/tegra/phy-tegra194-p2u.c b/drivers/phy/tegra/phy-tegra194-p2u.c
index 633e6b747275..a86b4af70ab9 100644
--- a/drivers/phy/tegra/phy-tegra194-p2u.c
+++ b/drivers/phy/tegra/phy-tegra194-p2u.c
@@ -7,12 +7,15 @@
* Author: Vidya Sagar <vidyas@xxxxxxxxxx>
*/

+#include <linux/delay.h>
#include <linux/err.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/phy/phy.h>
+#include <soc/tegra/bpmp.h>
+#include <soc/tegra/bpmp-abi.h>

#define P2U_CONTROL_CMN 0x74
#define P2U_CONTROL_CMN_ENABLE_L2_EXIT_RATE_CHANGE BIT(13)
@@ -31,14 +34,57 @@
#define P2U_DIR_SEARCH_CTRL 0xd4
#define P2U_DIR_SEARCH_CTRL_GEN4_FINE_GRAIN_SEARCH_TWICE BIT(18)

+#define P2U_RX_MARGIN_SW_INT_EN 0xe0
+#define P2U_RX_MARGIN_SW_INT_EN_READINESS BIT(0)
+#define P2U_RX_MARGIN_SW_INT_EN_MARGIN_START BIT(1)
+#define P2U_RX_MARGIN_SW_INT_EN_MARGIN_CHANGE BIT(2)
+#define P2U_RX_MARGIN_SW_INT_EN_MARGIN_STOP BIT(3)
+
+#define P2U_RX_MARGIN_SW_INT 0xe4
+#define P2U_RX_MARGIN_SW_INT_MASK 0xf
+#define P2U_RX_MARGIN_SW_INT_READINESS BIT(0)
+#define P2U_RX_MARGIN_SW_INT_MARGIN_START BIT(1)
+#define P2U_RX_MARGIN_SW_INT_MARGIN_CHANGE BIT(2)
+#define P2U_RX_MARGIN_SW_INT_MARGIN_STOP BIT(3)
+
+#define P2U_RX_MARGIN_SW_STATUS 0xe8
+#define P2U_RX_MARGIN_SW_STATUS_MARGIN_SW_READY BIT(0)
+#define P2U_RX_MARGIN_SW_STATUS_MARGIN_READY BIT(1)
+#define P2U_RX_MARGIN_SW_STATUS_PHY_MARGIN_STATUS BIT(2)
+#define P2U_RX_MARGIN_SW_STATUS_PHY_MARGIN_ERROR_STATUS BIT(3)
+
+#define P2U_RX_MARGIN_CTRL 0xec
+
+#define P2U_RX_MARGIN_STATUS 0xf0
+#define P2U_RX_MARGIN_STATUS_ERRORS_MASK 0xffff
+
+enum margin_state {
+ RX_MARGIN_START_CHANGE = 1,
+ RX_MARGIN_STOP,
+ RX_MARGIN_GET_MARGIN,
+};
+
struct tegra_p2u_of_data {
bool one_dir_search;
+ bool lane_margin;
};

struct tegra_p2u {
void __iomem *base;
bool skip_sz_protection_en; /* Needed to support two retimers */
struct tegra_p2u_of_data *of_data;
+ struct device *dev;
+ struct tegra_bpmp *bpmp;
+ u32 id;
+ atomic_t margin_state;
+};
+
+struct margin_ctrl {
+ u32 en:1;
+ u32 clr:1;
+ u32 x:7;
+ u32 y:6;
+ u32 n_blks:8;
};

static inline void p2u_writel(struct tegra_p2u *phy, const u32 value,
@@ -83,6 +129,14 @@ static int tegra_p2u_power_on(struct phy *x)
p2u_writel(phy, val, P2U_DIR_SEARCH_CTRL);
}

+ if (phy->of_data->lane_margin) {
+ p2u_writel(phy, P2U_RX_MARGIN_SW_INT_EN_READINESS |
+ P2U_RX_MARGIN_SW_INT_EN_MARGIN_START |
+ P2U_RX_MARGIN_SW_INT_EN_MARGIN_CHANGE |
+ P2U_RX_MARGIN_SW_INT_EN_MARGIN_STOP,
+ P2U_RX_MARGIN_SW_INT_EN);
+ }
+
return 0;
}

@@ -104,17 +158,195 @@ static const struct phy_ops ops = {
.owner = THIS_MODULE,
};

+static int tegra_p2u_set_margin_control(struct tegra_p2u *phy, u32 ctrl_data)
+{
+ struct tegra_bpmp_message msg;
+ struct mrq_uphy_response resp;
+ struct mrq_uphy_request req;
+ struct margin_ctrl ctrl;
+ int err;
+
+ memcpy(&ctrl, &ctrl_data, sizeof(ctrl_data));
+
+ memset(&req, 0, sizeof(req));
+ memset(&resp, 0, sizeof(resp));
+
+ req.lane = phy->id;
+ req.cmd = CMD_UPHY_PCIE_LANE_MARGIN_CONTROL;
+ req.uphy_set_margin_control.en = ctrl.en;
+ req.uphy_set_margin_control.clr = ctrl.clr;
+ req.uphy_set_margin_control.x = ctrl.x;
+ req.uphy_set_margin_control.y = ctrl.y;
+ req.uphy_set_margin_control.nblks = ctrl.n_blks;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.mrq = MRQ_UPHY;
+ msg.tx.data = &req;
+ msg.tx.size = sizeof(req);
+ msg.rx.data = &resp;
+ msg.rx.size = sizeof(resp);
+
+ err = tegra_bpmp_transfer(phy->bpmp, &msg);
+ if (err)
+ return err;
+ if (msg.rx.ret)
+ return -EINVAL;
+
+ return 0;
+}
+
+static int tegra_p2u_get_margin_status(struct tegra_p2u *phy, u32 *val)
+{
+ struct tegra_bpmp_message msg;
+ struct mrq_uphy_response resp;
+ struct mrq_uphy_request req;
+ int rc;
+
+ req.lane = phy->id;
+ req.cmd = CMD_UPHY_PCIE_LANE_MARGIN_STATUS;
+
+ memset(&msg, 0, sizeof(msg));
+ msg.mrq = MRQ_UPHY;
+ msg.tx.data = &req;
+ msg.tx.size = sizeof(req);
+ msg.rx.data = &resp;
+ msg.rx.size = sizeof(resp);
+
+ rc = tegra_bpmp_transfer(phy->bpmp, &msg);
+ if (rc)
+ return rc;
+ if (msg.rx.ret)
+ return -EINVAL;
+
+ *val = resp.uphy_get_margin_status.status;
+
+ return 0;
+}
+
+static irqreturn_t tegra_p2u_irq_thread(int irq, void *arg)
+{
+ struct tegra_p2u *phy = arg;
+ struct device *dev = phy->dev;
+ u32 val;
+ int state, ret;
+
+ do {
+ state = atomic_read(&phy->margin_state);
+ switch (state) {
+ case RX_MARGIN_START_CHANGE:
+ case RX_MARGIN_STOP:
+ /* Read margin control data and copy it to UPHY. */
+ val = p2u_readl(phy, P2U_RX_MARGIN_CTRL);
+ ret = tegra_p2u_set_margin_control(phy, val);
+ if (ret) {
+ dev_err(dev, "failed to set margin control: %d\n", ret);
+ break;
+ }
+
+ p2u_writel(phy, P2U_RX_MARGIN_SW_STATUS_MARGIN_SW_READY |
+ P2U_RX_MARGIN_SW_STATUS_MARGIN_READY |
+ P2U_RX_MARGIN_SW_STATUS_PHY_MARGIN_STATUS |
+ P2U_RX_MARGIN_SW_STATUS_PHY_MARGIN_ERROR_STATUS,
+ P2U_RX_MARGIN_SW_STATUS);
+
+ usleep_range(10, 11);
+
+ if (state == RX_MARGIN_STOP) {
+ /* Return from the loop if PCIe ctrl issues margin stop cmd. */
+ p2u_writel(phy, P2U_RX_MARGIN_SW_STATUS_MARGIN_SW_READY |
+ P2U_RX_MARGIN_SW_STATUS_MARGIN_READY |
+ P2U_RX_MARGIN_SW_STATUS_PHY_MARGIN_ERROR_STATUS,
+ P2U_RX_MARGIN_SW_STATUS);
+
+ return IRQ_HANDLED;
+ }
+
+ atomic_set(&phy->margin_state, RX_MARGIN_GET_MARGIN);
+ break;
+
+ case RX_MARGIN_GET_MARGIN:
+ /*
+ * Read margin status from UPHY and program it in P2U_RX_MARGIN_STATUS
+ * register. This data will reflect in PCIe controller's margining lane
+ * status register.
+ */
+ ret = tegra_p2u_get_margin_status(phy, &val);
+ if (ret) {
+ dev_err(dev, "failed to get margin status: %d\n", ret);
+ break;
+ }
+ p2u_writel(phy, val & P2U_RX_MARGIN_STATUS_ERRORS_MASK,
+ P2U_RX_MARGIN_STATUS);
+
+ p2u_writel(phy, P2U_RX_MARGIN_SW_STATUS_MARGIN_SW_READY |
+ P2U_RX_MARGIN_SW_STATUS_MARGIN_READY |
+ P2U_RX_MARGIN_SW_STATUS_PHY_MARGIN_ERROR_STATUS,
+ P2U_RX_MARGIN_SW_STATUS);
+
+ msleep(20);
+ break;
+
+ default:
+ dev_err(dev, "Invalid margin state: %d\n", state);
+ return IRQ_HANDLED;
+ };
+ } while (1);
+
+ return IRQ_HANDLED;
+}
+
+static irqreturn_t tegra_p2u_irq_handler(int irq, void *arg)
+{
+ struct tegra_p2u *phy = (struct tegra_p2u *)arg;
+ u32 val = 0;
+ irqreturn_t ret = IRQ_HANDLED;
+
+ val = p2u_readl(phy, P2U_RX_MARGIN_SW_INT);
+ p2u_writel(phy, val, P2U_RX_MARGIN_SW_INT);
+
+ /*
+ * When PCIe link trains to Gen4, P2U HW generate READINESS interrupt. Set MARGIN_SW_READY
+ * and MARGIN_READY bits to enable P2U HW sample lane margin control data from PCIe
+ * controller's configuration space.
+ */
+ if (val & P2U_RX_MARGIN_SW_INT_READINESS)
+ p2u_writel(phy, P2U_RX_MARGIN_SW_STATUS_MARGIN_SW_READY |
+ P2U_RX_MARGIN_SW_STATUS_MARGIN_READY,
+ P2U_RX_MARGIN_SW_STATUS);
+
+ /*
+ * P2U HW generates MARGIN_START or MARGIN_CHANGE interrupt after it copied margin control
+ * data to P2U_RX_MARGIN_CTRL register.
+ */
+ if ((val & P2U_RX_MARGIN_SW_INT_MARGIN_START) ||
+ (val & P2U_RX_MARGIN_SW_INT_MARGIN_CHANGE)) {
+ atomic_set(&phy->margin_state, RX_MARGIN_START_CHANGE);
+ ret = IRQ_WAKE_THREAD;
+ }
+
+ /* P2U HW generates MARGIN_STOP interrupt when PCIe controller issues margin stop cmd. */
+ if (val & P2U_RX_MARGIN_SW_INT_MARGIN_STOP)
+ atomic_set(&phy->margin_state, RX_MARGIN_STOP);
+
+ return ret;
+}
+
static int tegra_p2u_probe(struct platform_device *pdev)
{
struct phy_provider *phy_provider;
struct device *dev = &pdev->dev;
struct phy *generic_phy;
struct tegra_p2u *phy;
+ int ret;
+ u32 irq;

phy = devm_kzalloc(dev, sizeof(*phy), GFP_KERNEL);
if (!phy)
return -ENOMEM;

+ phy->dev = dev;
+ platform_set_drvdata(pdev, phy);
+
phy->of_data =
(struct tegra_p2u_of_data *)of_device_get_match_data(dev);
if (!phy->of_data)
@@ -140,15 +372,54 @@ static int tegra_p2u_probe(struct platform_device *pdev)
if (IS_ERR(phy_provider))
return PTR_ERR(phy_provider);

+ if (phy->of_data->lane_margin) {
+ irq = platform_get_irq_byname(pdev, "intr");
+ if (irq < 0) {
+ dev_err(dev, "failed to get intr interrupt\n");
+ return irq;
+ }
+
+ ret = devm_request_threaded_irq(dev, irq, tegra_p2u_irq_handler,
+ tegra_p2u_irq_thread, 0,
+ "tegra-p2u-intr", phy);
+ if (ret) {
+ dev_err(dev, "failed to request intr irq\n");
+ return ret;
+ }
+
+ ret = of_property_read_u32_index(dev->of_node, "nvidia,bpmp",
+ 1, &phy->id);
+ if (ret) {
+ dev_err(dev, "failed to read P2U id: %d\n", ret);
+ return ret;
+ }
+
+ phy->bpmp = tegra_bpmp_get(dev);
+ if (IS_ERR(phy->bpmp))
+ return PTR_ERR(phy->bpmp);
+ }
+
+ return 0;
+}
+
+static int tegra_p2u_remove(struct platform_device *pdev)
+{
+ struct tegra_p2u *phy = platform_get_drvdata(pdev);
+
+ if (phy->of_data->lane_margin)
+ tegra_bpmp_put(phy->bpmp);
+
return 0;
}

static const struct tegra_p2u_of_data tegra194_p2u_of_data = {
.one_dir_search = false,
+ .lane_margin = false,
};

static const struct tegra_p2u_of_data tegra234_p2u_of_data = {
.one_dir_search = true,
+ .lane_margin = true,
};

static const struct of_device_id tegra_p2u_id_table[] = {
@@ -166,6 +437,7 @@ MODULE_DEVICE_TABLE(of, tegra_p2u_id_table);

static struct platform_driver tegra_p2u_driver = {
.probe = tegra_p2u_probe,
+ .remove = tegra_p2u_remove,
.driver = {
.name = "tegra194-p2u",
.of_match_table = tegra_p2u_id_table,
--
2.25.1