From: Tom Hsieh <[email protected]>
The driver defines PHY_CTRL3_TXVREF_TUNE_MASK,
PHY_CTRL3_TXRISE_TUNE_MASK, PHY_CTRL3_TXPREEMP_TUNE_MASK,
PHY_CTRL3_COMPDISTUNE_MASK, PHY_CTRL3_TX_VBOOST_LEVEL_MASK,
PHY_CTRL4_PCS_TX_DEEMPH_3P5DB_MASK and
PHY_CTRL5_PCS_TX_SWING_FULL_MASK, but uses none of them and reads
none of the fsl,phy-* properties that feed them. The PHY is brought
out of reset and left at its power-on defaults.
Several device trees in dts/upstream already carry those properties:
imx95-19x19-evk.dts, imx95-15x15-evk.dts, imx95-15x15-frdm.dts and
imx943-evk.dts each set five of them. Linux applies them on the same
hardware; U-Boot silently ignores them.
Port the tuning from Linux, keeping its structure so the two stay
comparable. The code is taken from
drivers/phy/freescale/phy-fsl-imx8mq-usb.c:
- imx8m_phy_tune(), the property parsing and the generic converters
from commit 63c85ad0cd81 ("phy: fsl-imx8mp-usb: add support for
phy tuning")
- the three i.MX95 converters from commit e75d564f6466
("phy: fsl-imx8mq-usb: add i.MX95 tuning support")
The i.MX95 field encodings differ from the i.MX8MQ/8MP ones rather
than being a coarser version of them, so they need their own
converters. For fsl,phy-tx-vref-tune-percent = 100:
generic (i.MX8MQ/8MP): clamp 94..124, (100 - 94) / 2 = 3
i.MX95: clamp 90..108, piecewise, 100 - 92 = 8
One deliberate difference from Linux: the i.MX95 comp-dis converter
uses clamp(percent, 94U, 104U) where Linux has clamp(percent, 94, 104).
U-Boot's clamp() in include/linux/kernel.h is the older type-checking
form, and the unsuffixed literals make it warn about comparison of
distinct pointer types. The other three converters already use U
suffixes in Linux.
As in Linux, the tuning is applied from imx8mp_usb_phy_init() only.
A board that sets none of the properties keeps the current
behaviour: every field stays at PHY_TUNE_DEFAULT and
imx8m_phy_tune() returns without writing anything.
This fills a gap against Linux. It is not a fix for any failure
observed in U-Boot.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Tom Hsieh <[email protected]>
---
This is a new feature rather than a fix, so it targets the next merge
window (v2027.01) rather than v2026.10. It applies cleanly on both
master and u-boot/next; neither file it touches has been modified in
next.
Verified on i.MX95 (Advantech AOM-2521), which sets the same five
properties as imx95-19x19-evk.dts. Reading the PHY registers after
`usb start`, with the patch applied:
PHY_CTRL3 0x4c1f004c = 0xaa14e6eb
PHY_CTRL4 0x4c1f0050 = 0x04289e09
PHY_CTRL5 0x4c1f0054 = 0x0000007f
TX_VBOOST_LEVEL [31:29] = 5 (1156 uV)
TXVREF_TUNE [25:22] = 8 (100 %)
TXPREEMP_TUNE [16:15] = 1 (600 uA)
PCS_TX_DEEMPH_3P5DB [20:15] = 17 (17 dB)
PCS_TX_SWING_FULL [6:0] = 127 (100 %)
Each matches what the converters produce for the properties in the
device tree. TXRISE_TUNE and COMPDISTUNE are not set there and keep
their reset values, as intended.
TXVREF_TUNE is the field that shows the i.MX95 conversion is the one
being applied: for fsl,phy-tx-vref-tune-percent = 100 the generic
i.MX8MQ/8MP converter yields 3, the i.MX95 one yields 8.
Test base: the tree also carried a local fix for the DWC3 reference
clock lookup, without which `usb start` does not get far enough on
i.MX95 to be useful. That fix is on the list separately from Stefan
Eichenberger as "usb: dwc3-generic: fix ref clock lookup indexing
wrong clk_bulk array". This patch does not depend on it to build or
to apply.
Tested on i.MX95 only. The i.MX8MQ and i.MX8MP paths are unchanged in
behaviour for a device tree that sets none of these properties, but
were not tested on hardware.
Build-tested with imx8mp_evk_defconfig and aquila-imx95_defconfig.
Thirteen defconfigs enable both CONFIG_PHY_IMX8MQ_USB and
CONFIG_USB_DWC3_GENERIC.
drivers/phy/phy-imx8mq-usb.c | 283 +++++++++++++++++++++++++++++++++++
1 file changed, 283 insertions(+)
diff --git a/drivers/phy/phy-imx8mq-usb.c b/drivers/phy/phy-imx8mq-usb.c
index 387db843c50..3308a3a8b58 100644
--- a/drivers/phy/phy-imx8mq-usb.c
+++ b/drivers/phy/phy-imx8mq-usb.c
@@ -118,6 +118,9 @@
#define TCA_INFO 0xfc
+/* Tuning fields left at this value are not programmed. */
+#define PHY_TUNE_DEFAULT 0xffffffff
+
enum imx8mpq_phy_type {
IMX8MQ_PHY,
IMX8MP_PHY,
@@ -130,6 +133,13 @@ struct imx8mq_usb_phy {
enum imx8mpq_phy_type type;
struct udevice *vbus_supply;
void __iomem *tca_base;
+ u32 pcs_tx_swing_full;
+ u32 pcs_tx_deemph_3p5db;
+ u32 tx_vref_tune;
+ u32 tx_rise_tune;
+ u32 tx_preemp_amp_tune;
+ u32 tx_vboost_level;
+ u32 comp_dis_tune;
};
static const struct udevice_id imx8mq_usb_phy_of_match[] = {
@@ -201,6 +211,275 @@ static int imx8mq_usb_phy_init(struct phy *usb_phy)
return 0;
}
+/*
+ * Tuning helpers, ported from the Linux driver
+ * (drivers/phy/freescale/phy-fsl-imx8mq-usb.c). Without them the PHY keeps
+ * its reset defaults and the fsl,phy-* properties in the device tree have no
+ * effect.
+ *
+ * i.MX95 encodes three of these fields differently from i.MX8MQ/8MP, so it
+ * needs its own converters -- feeding the generic ones a percentage would
+ * program a different register value, not merely a less precise one.
+ */
+static u32 phy_tx_vref_tune_from_property(u32 percent)
+{
+ percent = clamp(percent, 94U, 124U);
+
+ return DIV_ROUND_CLOSEST(percent - 94U, 2);
+}
+
+static u32 imx95_phy_tx_vref_tune_from_property(u32 percent)
+{
+ percent = clamp(percent, 90U, 108U);
+
+ switch (percent) {
+ case 90 ... 91:
+ percent = 0;
+ break;
+ case 92 ... 96:
+ percent -= 91;
+ break;
+ case 97 ... 104:
+ percent -= 92;
+ break;
+ case 105 ... 108:
+ percent -= 93;
+ break;
+ }
+
+ return percent;
+}
+
+static u32 phy_tx_rise_tune_from_property(u32 percent)
+{
+ switch (percent) {
+ case 0 ... 98:
+ return 3;
+ case 99:
+ return 2;
+ case 100 ... 101:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static u32 imx95_phy_tx_rise_tune_from_property(u32 percent)
+{
+ percent = clamp(percent, 90U, 120U);
+
+ switch (percent) {
+ case 90 ... 99:
+ return 3;
+ case 101 ... 115:
+ return 1;
+ case 116 ... 120:
+ return 0;
+ default:
+ return 2;
+ }
+}
+
+static u32 phy_tx_preemp_amp_tune_from_property(u32 microamp)
+{
+ microamp = min(microamp, 1800U);
+
+ return microamp / 600;
+}
+
+static u32 phy_tx_vboost_level_from_property(u32 microvolt)
+{
+ switch (microvolt) {
+ case 1156:
+ return 5;
+ case 844:
+ return 3;
+ default:
+ return 4;
+ }
+}
+
+static u32 phy_comp_dis_tune_from_property(u32 percent)
+{
+ switch (percent) {
+ case 0 ... 92:
+ return 0;
+ case 93 ... 95:
+ return 1;
+ case 96 ... 97:
+ return 2;
+ case 98 ... 102:
+ return 3;
+ case 103 ... 105:
+ return 4;
+ case 106 ... 109:
+ return 5;
+ case 110 ... 113:
+ return 6;
+ default:
+ return 7;
+ }
+}
+
+static u32 imx95_phy_comp_dis_tune_from_property(u32 percent)
+{
+ percent = clamp(percent, 94U, 104U);
+
+ switch (percent) {
+ case 94 ... 95:
+ percent = 0;
+ break;
+ case 96 ... 98:
+ percent -= 95;
+ break;
+ case 99 ... 102:
+ percent -= 96;
+ break;
+ case 103 ... 104:
+ percent -= 97;
+ break;
+ }
+
+ return percent;
+}
+
+static u32 phy_pcs_tx_deemph_3p5db_from_property(u32 decibel)
+{
+ return min(decibel, 36U);
+}
+
+static u32 phy_pcs_tx_swing_full_from_property(u32 percent)
+{
+ percent = min(percent, 100U);
+
+ return (percent * 127) / 100;
+}
+
+static void imx8m_get_phy_tuning_data(struct udevice *dev)
+{
+ struct imx8mq_usb_phy *imx_phy = dev_get_priv(dev);
+ bool is_imx95 = imx_phy->type == IMX95_PHY;
+
+ if (dev_read_u32(dev, "fsl,phy-tx-vref-tune-percent",
+ &imx_phy->tx_vref_tune))
+ imx_phy->tx_vref_tune = PHY_TUNE_DEFAULT;
+ else if (is_imx95)
+ imx_phy->tx_vref_tune =
+
imx95_phy_tx_vref_tune_from_property(imx_phy->tx_vref_tune);
+ else
+ imx_phy->tx_vref_tune =
+ phy_tx_vref_tune_from_property(imx_phy->tx_vref_tune);
+
+ if (dev_read_u32(dev, "fsl,phy-tx-rise-tune-percent",
+ &imx_phy->tx_rise_tune))
+ imx_phy->tx_rise_tune = PHY_TUNE_DEFAULT;
+ else if (is_imx95)
+ imx_phy->tx_rise_tune =
+
imx95_phy_tx_rise_tune_from_property(imx_phy->tx_rise_tune);
+ else
+ imx_phy->tx_rise_tune =
+ phy_tx_rise_tune_from_property(imx_phy->tx_rise_tune);
+
+ if (dev_read_u32(dev, "fsl,phy-tx-preemp-amp-tune-microamp",
+ &imx_phy->tx_preemp_amp_tune))
+ imx_phy->tx_preemp_amp_tune = PHY_TUNE_DEFAULT;
+ else
+ imx_phy->tx_preemp_amp_tune =
+
phy_tx_preemp_amp_tune_from_property(imx_phy->tx_preemp_amp_tune);
+
+ if (dev_read_u32(dev, "fsl,phy-tx-vboost-level-microvolt",
+ &imx_phy->tx_vboost_level))
+ imx_phy->tx_vboost_level = PHY_TUNE_DEFAULT;
+ else
+ imx_phy->tx_vboost_level =
+
phy_tx_vboost_level_from_property(imx_phy->tx_vboost_level);
+
+ if (dev_read_u32(dev, "fsl,phy-comp-dis-tune-percent",
+ &imx_phy->comp_dis_tune))
+ imx_phy->comp_dis_tune = PHY_TUNE_DEFAULT;
+ else if (is_imx95)
+ imx_phy->comp_dis_tune =
+
imx95_phy_comp_dis_tune_from_property(imx_phy->comp_dis_tune);
+ else
+ imx_phy->comp_dis_tune =
+ phy_comp_dis_tune_from_property(imx_phy->comp_dis_tune);
+
+ if (dev_read_u32(dev, "fsl,phy-pcs-tx-deemph-3p5db-attenuation-db",
+ &imx_phy->pcs_tx_deemph_3p5db))
+ imx_phy->pcs_tx_deemph_3p5db = PHY_TUNE_DEFAULT;
+ else
+ imx_phy->pcs_tx_deemph_3p5db =
+
phy_pcs_tx_deemph_3p5db_from_property(imx_phy->pcs_tx_deemph_3p5db);
+
+ if (dev_read_u32(dev, "fsl,phy-pcs-tx-swing-full-percent",
+ &imx_phy->pcs_tx_swing_full))
+ imx_phy->pcs_tx_swing_full = PHY_TUNE_DEFAULT;
+ else
+ imx_phy->pcs_tx_swing_full =
+
phy_pcs_tx_swing_full_from_property(imx_phy->pcs_tx_swing_full);
+}
+
+static void imx8m_phy_tune(struct imx8mq_usb_phy *imx_phy)
+{
+ u32 value;
+
+ if (imx_phy->pcs_tx_deemph_3p5db != PHY_TUNE_DEFAULT) {
+ value = readl(imx_phy->base + PHY_CTRL4);
+ value &= ~PHY_CTRL4_PCS_TX_DEEMPH_3P5DB_MASK;
+ value |= FIELD_PREP(PHY_CTRL4_PCS_TX_DEEMPH_3P5DB_MASK,
+ imx_phy->pcs_tx_deemph_3p5db);
+ writel(value, imx_phy->base + PHY_CTRL4);
+ }
+
+ if (imx_phy->pcs_tx_swing_full != PHY_TUNE_DEFAULT) {
+ value = readl(imx_phy->base + PHY_CTRL5);
+ value &= ~PHY_CTRL5_PCS_TX_SWING_FULL_MASK;
+ value |= FIELD_PREP(PHY_CTRL5_PCS_TX_SWING_FULL_MASK,
+ imx_phy->pcs_tx_swing_full);
+ writel(value, imx_phy->base + PHY_CTRL5);
+ }
+
+ if ((imx_phy->tx_vref_tune & imx_phy->tx_rise_tune &
+ imx_phy->tx_preemp_amp_tune & imx_phy->comp_dis_tune &
+ imx_phy->tx_vboost_level) == PHY_TUNE_DEFAULT)
+ /* If all are the default values, no need update. */
+ return;
+
+ value = readl(imx_phy->base + PHY_CTRL3);
+
+ if (imx_phy->tx_vref_tune != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_TXVREF_TUNE_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_TXVREF_TUNE_MASK,
+ imx_phy->tx_vref_tune);
+ }
+
+ if (imx_phy->tx_rise_tune != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_TXRISE_TUNE_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_TXRISE_TUNE_MASK,
+ imx_phy->tx_rise_tune);
+ }
+
+ if (imx_phy->tx_preemp_amp_tune != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_TXPREEMP_TUNE_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_TXPREEMP_TUNE_MASK,
+ imx_phy->tx_preemp_amp_tune);
+ }
+
+ if (imx_phy->comp_dis_tune != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_COMPDISTUNE_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_COMPDISTUNE_MASK,
+ imx_phy->comp_dis_tune);
+ }
+
+ if (imx_phy->tx_vboost_level != PHY_TUNE_DEFAULT) {
+ value &= ~PHY_CTRL3_TX_VBOOST_LEVEL_MASK;
+ value |= FIELD_PREP(PHY_CTRL3_TX_VBOOST_LEVEL_MASK,
+ imx_phy->tx_vboost_level);
+ }
+
+ writel(value, imx_phy->base + PHY_CTRL3);
+}
+
static int imx8mp_usb_phy_init(struct phy *usb_phy)
{
struct udevice *dev = usb_phy->dev;
@@ -237,6 +516,8 @@ static int imx8mp_usb_phy_init(struct phy *usb_phy)
value &= ~(PHY_CTRL1_RESET | PHY_CTRL1_ATERESET);
writel(value, imx_phy->base + PHY_CTRL1);
+ imx8m_phy_tune(imx_phy);
+
if (imx_phy->tca_base)
tca_blk_init(usb_phy);
@@ -353,6 +634,8 @@ int imx8mq_usb_phy_probe(struct udevice *dev)
if (priv->type == IMX95_PHY)
priv->tca_base = dev_read_addr_index_ptr(dev, 1);
+ imx8m_get_phy_tuning_data(dev);
+
return 0;
}
--
2.43.0