|  | // SPDX-License-Identifier: GPL-2.0-or-later | 
|  | /* | 
|  | * Copyright (c) 2023 Sartura Ltd. | 
|  | * | 
|  | * Author: Robert Marko <robert.marko@sartura.hr> | 
|  | *         Christian Marangi <ansuelsmth@gmail.com> | 
|  | * | 
|  | * Qualcomm QCA8072 and QCA8075 PHY driver | 
|  | */ | 
|  |  | 
|  | #include <linux/module.h> | 
|  | #include <linux/of.h> | 
|  | #include <linux/phy.h> | 
|  | #include <linux/bitfield.h> | 
|  | #include <linux/gpio/driver.h> | 
|  | #include <linux/sfp.h> | 
|  |  | 
|  | #include "qcom.h" | 
|  |  | 
|  | #define QCA807X_CHIP_CONFIGURATION				0x1f | 
|  | #define QCA807X_BT_BX_REG_SEL					BIT(15) | 
|  | #define QCA807X_BT_BX_REG_SEL_FIBER				0 | 
|  | #define QCA807X_BT_BX_REG_SEL_COPPER				1 | 
|  | #define QCA807X_CHIP_CONFIGURATION_MODE_CFG_MASK		GENMASK(3, 0) | 
|  | #define QCA807X_CHIP_CONFIGURATION_MODE_QSGMII_SGMII		4 | 
|  | #define QCA807X_CHIP_CONFIGURATION_MODE_PSGMII_FIBER		3 | 
|  | #define QCA807X_CHIP_CONFIGURATION_MODE_PSGMII_ALL_COPPER	0 | 
|  |  | 
|  | #define QCA807X_MEDIA_SELECT_STATUS				0x1a | 
|  | #define QCA807X_MEDIA_DETECTED_COPPER				BIT(5) | 
|  | #define QCA807X_MEDIA_DETECTED_1000_BASE_X			BIT(4) | 
|  | #define QCA807X_MEDIA_DETECTED_100_BASE_FX			BIT(3) | 
|  |  | 
|  | #define QCA807X_MMD7_FIBER_MODE_AUTO_DETECTION			0x807e | 
|  | #define QCA807X_MMD7_FIBER_MODE_AUTO_DETECTION_EN		BIT(0) | 
|  |  | 
|  | #define QCA807X_MMD7_1000BASE_T_POWER_SAVE_PER_CABLE_LENGTH	0x801a | 
|  | #define QCA807X_CONTROL_DAC_MASK				GENMASK(2, 0) | 
|  | /* List of tweaks enabled by this bit: | 
|  | * - With both FULL amplitude and FULL bias current: bias current | 
|  | *   is set to half. | 
|  | * - With only DSP amplitude: bias current is set to half and | 
|  | *   is set to 1/4 with cable < 10m. | 
|  | * - With DSP bias current (included both DSP amplitude and | 
|  | *   DSP bias current): bias current is half the detected current | 
|  | *   with cable < 10m. | 
|  | */ | 
|  | #define QCA807X_CONTROL_DAC_BIAS_CURRENT_TWEAK			BIT(2) | 
|  | #define QCA807X_CONTROL_DAC_DSP_BIAS_CURRENT			BIT(1) | 
|  | #define QCA807X_CONTROL_DAC_DSP_AMPLITUDE			BIT(0) | 
|  |  | 
|  | #define QCA807X_MMD7_LED_100N_1				0x8074 | 
|  | #define QCA807X_MMD7_LED_100N_2				0x8075 | 
|  | #define QCA807X_MMD7_LED_1000N_1			0x8076 | 
|  | #define QCA807X_MMD7_LED_1000N_2			0x8077 | 
|  |  | 
|  | #define QCA807X_MMD7_LED_CTRL(x)			(0x8074 + ((x) * 2)) | 
|  | #define QCA807X_MMD7_LED_FORCE_CTRL(x)			(0x8075 + ((x) * 2)) | 
|  |  | 
|  | /* LED hw control pattern for fiber port */ | 
|  | #define QCA807X_LED_FIBER_PATTERN_MASK			GENMASK(11, 1) | 
|  | #define QCA807X_LED_FIBER_TXACT_BLK_EN			BIT(10) | 
|  | #define QCA807X_LED_FIBER_RXACT_BLK_EN			BIT(9) | 
|  | #define QCA807X_LED_FIBER_FDX_ON_EN			BIT(6) | 
|  | #define QCA807X_LED_FIBER_HDX_ON_EN			BIT(5) | 
|  | #define QCA807X_LED_FIBER_1000BX_ON_EN			BIT(2) | 
|  | #define QCA807X_LED_FIBER_100FX_ON_EN			BIT(1) | 
|  |  | 
|  | /* Some device repurpose the LED as GPIO out */ | 
|  | #define QCA807X_GPIO_FORCE_EN				QCA808X_LED_FORCE_EN | 
|  | #define QCA807X_GPIO_FORCE_MODE_MASK			QCA808X_LED_FORCE_MODE_MASK | 
|  |  | 
|  | #define QCA807X_FUNCTION_CONTROL			0x10 | 
|  | #define QCA807X_FC_MDI_CROSSOVER_MODE_MASK		GENMASK(6, 5) | 
|  | #define QCA807X_FC_MDI_CROSSOVER_AUTO			3 | 
|  | #define QCA807X_FC_MDI_CROSSOVER_MANUAL_MDIX		1 | 
|  | #define QCA807X_FC_MDI_CROSSOVER_MANUAL_MDI		0 | 
|  |  | 
|  | /* PQSGMII Analog PHY specific */ | 
|  | #define PQSGMII_CTRL_REG				0x0 | 
|  | #define PQSGMII_ANALOG_SW_RESET				BIT(6) | 
|  | #define PQSGMII_DRIVE_CONTROL_1				0xb | 
|  | #define PQSGMII_TX_DRIVER_MASK				GENMASK(7, 4) | 
|  | #define PQSGMII_TX_DRIVER_140MV				0x0 | 
|  | #define PQSGMII_TX_DRIVER_160MV				0x1 | 
|  | #define PQSGMII_TX_DRIVER_180MV				0x2 | 
|  | #define PQSGMII_TX_DRIVER_200MV				0x3 | 
|  | #define PQSGMII_TX_DRIVER_220MV				0x4 | 
|  | #define PQSGMII_TX_DRIVER_240MV				0x5 | 
|  | #define PQSGMII_TX_DRIVER_260MV				0x6 | 
|  | #define PQSGMII_TX_DRIVER_280MV				0x7 | 
|  | #define PQSGMII_TX_DRIVER_300MV				0x8 | 
|  | #define PQSGMII_TX_DRIVER_320MV				0x9 | 
|  | #define PQSGMII_TX_DRIVER_400MV				0xa | 
|  | #define PQSGMII_TX_DRIVER_500MV				0xb | 
|  | #define PQSGMII_TX_DRIVER_600MV				0xc | 
|  | #define PQSGMII_MODE_CTRL				0x6d | 
|  | #define PQSGMII_MODE_CTRL_AZ_WORKAROUND_MASK		BIT(0) | 
|  | #define PQSGMII_MMD3_SERDES_CONTROL			0x805a | 
|  |  | 
|  | #define PHY_ID_QCA8072		0x004dd0b2 | 
|  | #define PHY_ID_QCA8075		0x004dd0b1 | 
|  |  | 
|  | #define QCA807X_COMBO_ADDR_OFFSET			4 | 
|  | #define QCA807X_PQSGMII_ADDR_OFFSET			5 | 
|  | #define SERDES_RESET_SLEEP				100 | 
|  |  | 
|  | enum qca807x_global_phy { | 
|  | QCA807X_COMBO_ADDR = 4, | 
|  | QCA807X_PQSGMII_ADDR = 5, | 
|  | }; | 
|  |  | 
|  | struct qca807x_shared_priv { | 
|  | unsigned int package_mode; | 
|  | u32 tx_drive_strength; | 
|  | }; | 
|  |  | 
|  | struct qca807x_gpio_priv { | 
|  | struct phy_device *phy; | 
|  | }; | 
|  |  | 
|  | struct qca807x_priv { | 
|  | bool dac_full_amplitude; | 
|  | bool dac_full_bias_current; | 
|  | bool dac_disable_bias_current_tweak; | 
|  | }; | 
|  |  | 
|  | static int qca807x_cable_test_start(struct phy_device *phydev) | 
|  | { | 
|  | /* we do all the (time consuming) work later */ | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qca807x_led_parse_netdev(struct phy_device *phydev, unsigned long rules, | 
|  | u16 *offload_trigger) | 
|  | { | 
|  | /* Parsing specific to netdev trigger */ | 
|  | switch (phydev->port) { | 
|  | case PORT_TP: | 
|  | if (test_bit(TRIGGER_NETDEV_TX, &rules)) | 
|  | *offload_trigger |= QCA808X_LED_TX_BLINK; | 
|  | if (test_bit(TRIGGER_NETDEV_RX, &rules)) | 
|  | *offload_trigger |= QCA808X_LED_RX_BLINK; | 
|  | if (test_bit(TRIGGER_NETDEV_LINK_10, &rules)) | 
|  | *offload_trigger |= QCA808X_LED_SPEED10_ON; | 
|  | if (test_bit(TRIGGER_NETDEV_LINK_100, &rules)) | 
|  | *offload_trigger |= QCA808X_LED_SPEED100_ON; | 
|  | if (test_bit(TRIGGER_NETDEV_LINK_1000, &rules)) | 
|  | *offload_trigger |= QCA808X_LED_SPEED1000_ON; | 
|  | if (test_bit(TRIGGER_NETDEV_HALF_DUPLEX, &rules)) | 
|  | *offload_trigger |= QCA808X_LED_HALF_DUPLEX_ON; | 
|  | if (test_bit(TRIGGER_NETDEV_FULL_DUPLEX, &rules)) | 
|  | *offload_trigger |= QCA808X_LED_FULL_DUPLEX_ON; | 
|  | break; | 
|  | case PORT_FIBRE: | 
|  | if (test_bit(TRIGGER_NETDEV_TX, &rules)) | 
|  | *offload_trigger |= QCA807X_LED_FIBER_TXACT_BLK_EN; | 
|  | if (test_bit(TRIGGER_NETDEV_RX, &rules)) | 
|  | *offload_trigger |= QCA807X_LED_FIBER_RXACT_BLK_EN; | 
|  | if (test_bit(TRIGGER_NETDEV_LINK_100, &rules)) | 
|  | *offload_trigger |= QCA807X_LED_FIBER_100FX_ON_EN; | 
|  | if (test_bit(TRIGGER_NETDEV_LINK_1000, &rules)) | 
|  | *offload_trigger |= QCA807X_LED_FIBER_1000BX_ON_EN; | 
|  | if (test_bit(TRIGGER_NETDEV_HALF_DUPLEX, &rules)) | 
|  | *offload_trigger |= QCA807X_LED_FIBER_HDX_ON_EN; | 
|  | if (test_bit(TRIGGER_NETDEV_FULL_DUPLEX, &rules)) | 
|  | *offload_trigger |= QCA807X_LED_FIBER_FDX_ON_EN; | 
|  | break; | 
|  | default: | 
|  | return -EOPNOTSUPP; | 
|  | } | 
|  |  | 
|  | if (rules && !*offload_trigger) | 
|  | return -EOPNOTSUPP; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qca807x_led_hw_control_enable(struct phy_device *phydev, u8 index) | 
|  | { | 
|  | u16 reg; | 
|  |  | 
|  | if (index > 1) | 
|  | return -EINVAL; | 
|  |  | 
|  | reg = QCA807X_MMD7_LED_FORCE_CTRL(index); | 
|  | return qca808x_led_reg_hw_control_enable(phydev, reg); | 
|  | } | 
|  |  | 
|  | static int qca807x_led_hw_is_supported(struct phy_device *phydev, u8 index, | 
|  | unsigned long rules) | 
|  | { | 
|  | u16 offload_trigger = 0; | 
|  |  | 
|  | if (index > 1) | 
|  | return -EINVAL; | 
|  |  | 
|  | return qca807x_led_parse_netdev(phydev, rules, &offload_trigger); | 
|  | } | 
|  |  | 
|  | static int qca807x_led_hw_control_set(struct phy_device *phydev, u8 index, | 
|  | unsigned long rules) | 
|  | { | 
|  | u16 reg, mask, offload_trigger = 0; | 
|  | int ret; | 
|  |  | 
|  | if (index > 1) | 
|  | return -EINVAL; | 
|  |  | 
|  | ret = qca807x_led_parse_netdev(phydev, rules, &offload_trigger); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | ret = qca807x_led_hw_control_enable(phydev, index); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | switch (phydev->port) { | 
|  | case PORT_TP: | 
|  | reg = QCA807X_MMD7_LED_CTRL(index); | 
|  | mask = QCA808X_LED_PATTERN_MASK; | 
|  | break; | 
|  | case PORT_FIBRE: | 
|  | /* HW control pattern bits are in LED FORCE reg */ | 
|  | reg = QCA807X_MMD7_LED_FORCE_CTRL(index); | 
|  | mask = QCA807X_LED_FIBER_PATTERN_MASK; | 
|  | break; | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | return phy_modify_mmd(phydev, MDIO_MMD_AN, reg, mask, | 
|  | offload_trigger); | 
|  | } | 
|  |  | 
|  | static bool qca807x_led_hw_control_status(struct phy_device *phydev, u8 index) | 
|  | { | 
|  | u16 reg; | 
|  |  | 
|  | if (index > 1) | 
|  | return false; | 
|  |  | 
|  | reg = QCA807X_MMD7_LED_FORCE_CTRL(index); | 
|  | return qca808x_led_reg_hw_control_status(phydev, reg); | 
|  | } | 
|  |  | 
|  | static int qca807x_led_hw_control_get(struct phy_device *phydev, u8 index, | 
|  | unsigned long *rules) | 
|  | { | 
|  | u16 reg; | 
|  | int val; | 
|  |  | 
|  | if (index > 1) | 
|  | return -EINVAL; | 
|  |  | 
|  | /* Check if we have hw control enabled */ | 
|  | if (qca807x_led_hw_control_status(phydev, index)) | 
|  | return -EINVAL; | 
|  |  | 
|  | /* Parsing specific to netdev trigger */ | 
|  | switch (phydev->port) { | 
|  | case PORT_TP: | 
|  | reg = QCA807X_MMD7_LED_CTRL(index); | 
|  | val = phy_read_mmd(phydev, MDIO_MMD_AN, reg); | 
|  | if (val & QCA808X_LED_TX_BLINK) | 
|  | set_bit(TRIGGER_NETDEV_TX, rules); | 
|  | if (val & QCA808X_LED_RX_BLINK) | 
|  | set_bit(TRIGGER_NETDEV_RX, rules); | 
|  | if (val & QCA808X_LED_SPEED10_ON) | 
|  | set_bit(TRIGGER_NETDEV_LINK_10, rules); | 
|  | if (val & QCA808X_LED_SPEED100_ON) | 
|  | set_bit(TRIGGER_NETDEV_LINK_100, rules); | 
|  | if (val & QCA808X_LED_SPEED1000_ON) | 
|  | set_bit(TRIGGER_NETDEV_LINK_1000, rules); | 
|  | if (val & QCA808X_LED_HALF_DUPLEX_ON) | 
|  | set_bit(TRIGGER_NETDEV_HALF_DUPLEX, rules); | 
|  | if (val & QCA808X_LED_FULL_DUPLEX_ON) | 
|  | set_bit(TRIGGER_NETDEV_FULL_DUPLEX, rules); | 
|  | break; | 
|  | case PORT_FIBRE: | 
|  | /* HW control pattern bits are in LED FORCE reg */ | 
|  | reg = QCA807X_MMD7_LED_FORCE_CTRL(index); | 
|  | val = phy_read_mmd(phydev, MDIO_MMD_AN, reg); | 
|  | if (val & QCA807X_LED_FIBER_TXACT_BLK_EN) | 
|  | set_bit(TRIGGER_NETDEV_TX, rules); | 
|  | if (val & QCA807X_LED_FIBER_RXACT_BLK_EN) | 
|  | set_bit(TRIGGER_NETDEV_RX, rules); | 
|  | if (val & QCA807X_LED_FIBER_100FX_ON_EN) | 
|  | set_bit(TRIGGER_NETDEV_LINK_100, rules); | 
|  | if (val & QCA807X_LED_FIBER_1000BX_ON_EN) | 
|  | set_bit(TRIGGER_NETDEV_LINK_1000, rules); | 
|  | if (val & QCA807X_LED_FIBER_HDX_ON_EN) | 
|  | set_bit(TRIGGER_NETDEV_HALF_DUPLEX, rules); | 
|  | if (val & QCA807X_LED_FIBER_FDX_ON_EN) | 
|  | set_bit(TRIGGER_NETDEV_FULL_DUPLEX, rules); | 
|  | break; | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qca807x_led_hw_control_reset(struct phy_device *phydev, u8 index) | 
|  | { | 
|  | u16 reg, mask; | 
|  |  | 
|  | if (index > 1) | 
|  | return -EINVAL; | 
|  |  | 
|  | switch (phydev->port) { | 
|  | case PORT_TP: | 
|  | reg = QCA807X_MMD7_LED_CTRL(index); | 
|  | mask = QCA808X_LED_PATTERN_MASK; | 
|  | break; | 
|  | case PORT_FIBRE: | 
|  | /* HW control pattern bits are in LED FORCE reg */ | 
|  | reg = QCA807X_MMD7_LED_FORCE_CTRL(index); | 
|  | mask = QCA807X_LED_FIBER_PATTERN_MASK; | 
|  | break; | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | return phy_clear_bits_mmd(phydev, MDIO_MMD_AN, reg, mask); | 
|  | } | 
|  |  | 
|  | static int qca807x_led_brightness_set(struct phy_device *phydev, | 
|  | u8 index, enum led_brightness value) | 
|  | { | 
|  | u16 reg; | 
|  | int ret; | 
|  |  | 
|  | if (index > 1) | 
|  | return -EINVAL; | 
|  |  | 
|  | /* If we are setting off the LED reset any hw control rule */ | 
|  | if (!value) { | 
|  | ret = qca807x_led_hw_control_reset(phydev, index); | 
|  | if (ret) | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | reg = QCA807X_MMD7_LED_FORCE_CTRL(index); | 
|  | return qca808x_led_reg_brightness_set(phydev, reg, value); | 
|  | } | 
|  |  | 
|  | static int qca807x_led_blink_set(struct phy_device *phydev, u8 index, | 
|  | unsigned long *delay_on, | 
|  | unsigned long *delay_off) | 
|  | { | 
|  | u16 reg; | 
|  |  | 
|  | if (index > 1) | 
|  | return -EINVAL; | 
|  |  | 
|  | reg = QCA807X_MMD7_LED_FORCE_CTRL(index); | 
|  | return qca808x_led_reg_blink_set(phydev, reg, delay_on, delay_off); | 
|  | } | 
|  |  | 
|  | #ifdef CONFIG_GPIOLIB | 
|  | static int qca807x_gpio_get_direction(struct gpio_chip *gc, unsigned int offset) | 
|  | { | 
|  | return GPIO_LINE_DIRECTION_OUT; | 
|  | } | 
|  |  | 
|  | static int qca807x_gpio_get(struct gpio_chip *gc, unsigned int offset) | 
|  | { | 
|  | struct qca807x_gpio_priv *priv = gpiochip_get_data(gc); | 
|  | u16 reg; | 
|  | int val; | 
|  |  | 
|  | reg = QCA807X_MMD7_LED_FORCE_CTRL(offset); | 
|  | val = phy_read_mmd(priv->phy, MDIO_MMD_AN, reg); | 
|  |  | 
|  | return FIELD_GET(QCA807X_GPIO_FORCE_MODE_MASK, val); | 
|  | } | 
|  |  | 
|  | static void qca807x_gpio_set(struct gpio_chip *gc, unsigned int offset, int value) | 
|  | { | 
|  | struct qca807x_gpio_priv *priv = gpiochip_get_data(gc); | 
|  | u16 reg; | 
|  | int val; | 
|  |  | 
|  | reg = QCA807X_MMD7_LED_FORCE_CTRL(offset); | 
|  |  | 
|  | val = phy_read_mmd(priv->phy, MDIO_MMD_AN, reg); | 
|  | val &= ~QCA807X_GPIO_FORCE_MODE_MASK; | 
|  | val |= QCA807X_GPIO_FORCE_EN; | 
|  | val |= FIELD_PREP(QCA807X_GPIO_FORCE_MODE_MASK, value); | 
|  |  | 
|  | phy_write_mmd(priv->phy, MDIO_MMD_AN, reg, val); | 
|  | } | 
|  |  | 
|  | static int qca807x_gpio_dir_out(struct gpio_chip *gc, unsigned int offset, int value) | 
|  | { | 
|  | qca807x_gpio_set(gc, offset, value); | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qca807x_gpio(struct phy_device *phydev) | 
|  | { | 
|  | struct device *dev = &phydev->mdio.dev; | 
|  | struct qca807x_gpio_priv *priv; | 
|  | struct gpio_chip *gc; | 
|  |  | 
|  | priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); | 
|  | if (!priv) | 
|  | return -ENOMEM; | 
|  |  | 
|  | priv->phy = phydev; | 
|  |  | 
|  | gc = devm_kzalloc(dev, sizeof(*gc), GFP_KERNEL); | 
|  | if (!gc) | 
|  | return -ENOMEM; | 
|  |  | 
|  | gc->label = dev_name(dev); | 
|  | gc->base = -1; | 
|  | gc->ngpio = 2; | 
|  | gc->parent = dev; | 
|  | gc->owner = THIS_MODULE; | 
|  | gc->can_sleep = true; | 
|  | gc->get_direction = qca807x_gpio_get_direction; | 
|  | gc->direction_output = qca807x_gpio_dir_out; | 
|  | gc->get = qca807x_gpio_get; | 
|  | gc->set = qca807x_gpio_set; | 
|  |  | 
|  | return devm_gpiochip_add_data(dev, gc, priv); | 
|  | } | 
|  | #endif | 
|  |  | 
|  | static int qca807x_read_fiber_status(struct phy_device *phydev) | 
|  | { | 
|  | bool changed; | 
|  | int ss, err; | 
|  |  | 
|  | err = genphy_c37_read_status(phydev, &changed); | 
|  | if (err || !changed) | 
|  | return err; | 
|  |  | 
|  | /* Read the QCA807x PHY-Specific Status register fiber page, | 
|  | * which indicates the speed and duplex that the PHY is actually | 
|  | * using, irrespective of whether we are in autoneg mode or not. | 
|  | */ | 
|  | ss = phy_read(phydev, AT803X_SPECIFIC_STATUS); | 
|  | if (ss < 0) | 
|  | return ss; | 
|  |  | 
|  | phydev->speed = SPEED_UNKNOWN; | 
|  | phydev->duplex = DUPLEX_UNKNOWN; | 
|  | if (ss & AT803X_SS_SPEED_DUPLEX_RESOLVED) { | 
|  | switch (FIELD_GET(AT803X_SS_SPEED_MASK, ss)) { | 
|  | case AT803X_SS_SPEED_100: | 
|  | phydev->speed = SPEED_100; | 
|  | break; | 
|  | case AT803X_SS_SPEED_1000: | 
|  | phydev->speed = SPEED_1000; | 
|  | break; | 
|  | } | 
|  |  | 
|  | if (ss & AT803X_SS_DUPLEX) | 
|  | phydev->duplex = DUPLEX_FULL; | 
|  | else | 
|  | phydev->duplex = DUPLEX_HALF; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qca807x_read_status(struct phy_device *phydev) | 
|  | { | 
|  | if (linkmode_test_bit(ETHTOOL_LINK_MODE_FIBRE_BIT, phydev->supported)) { | 
|  | switch (phydev->port) { | 
|  | case PORT_FIBRE: | 
|  | return qca807x_read_fiber_status(phydev); | 
|  | case PORT_TP: | 
|  | return at803x_read_status(phydev); | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  | } | 
|  |  | 
|  | return at803x_read_status(phydev); | 
|  | } | 
|  |  | 
|  | static int qca807x_phy_package_probe_once(struct phy_device *phydev) | 
|  | { | 
|  | struct phy_package_shared *shared = phydev->shared; | 
|  | struct qca807x_shared_priv *priv = shared->priv; | 
|  | unsigned int tx_drive_strength; | 
|  | const char *package_mode_name; | 
|  |  | 
|  | /* Default to 600mw if not defined */ | 
|  | if (of_property_read_u32(shared->np, "qcom,tx-drive-strength-milliwatt", | 
|  | &tx_drive_strength)) | 
|  | tx_drive_strength = 600; | 
|  |  | 
|  | switch (tx_drive_strength) { | 
|  | case 140: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_140MV; | 
|  | break; | 
|  | case 160: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_160MV; | 
|  | break; | 
|  | case 180: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_180MV; | 
|  | break; | 
|  | case 200: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_200MV; | 
|  | break; | 
|  | case 220: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_220MV; | 
|  | break; | 
|  | case 240: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_240MV; | 
|  | break; | 
|  | case 260: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_260MV; | 
|  | break; | 
|  | case 280: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_280MV; | 
|  | break; | 
|  | case 300: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_300MV; | 
|  | break; | 
|  | case 320: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_320MV; | 
|  | break; | 
|  | case 400: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_400MV; | 
|  | break; | 
|  | case 500: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_500MV; | 
|  | break; | 
|  | case 600: | 
|  | priv->tx_drive_strength = PQSGMII_TX_DRIVER_600MV; | 
|  | break; | 
|  | default: | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | priv->package_mode = PHY_INTERFACE_MODE_NA; | 
|  | if (!of_property_read_string(shared->np, "qcom,package-mode", | 
|  | &package_mode_name)) { | 
|  | if (!strcasecmp(package_mode_name, | 
|  | phy_modes(PHY_INTERFACE_MODE_PSGMII))) | 
|  | priv->package_mode = PHY_INTERFACE_MODE_PSGMII; | 
|  | else if (!strcasecmp(package_mode_name, | 
|  | phy_modes(PHY_INTERFACE_MODE_QSGMII))) | 
|  | priv->package_mode = PHY_INTERFACE_MODE_QSGMII; | 
|  | else | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qca807x_phy_package_config_init_once(struct phy_device *phydev) | 
|  | { | 
|  | struct phy_package_shared *shared = phydev->shared; | 
|  | struct qca807x_shared_priv *priv = shared->priv; | 
|  | int val, ret; | 
|  |  | 
|  | /* Make sure PHY follow PHY package mode if enforced */ | 
|  | if (priv->package_mode != PHY_INTERFACE_MODE_NA && | 
|  | phydev->interface != priv->package_mode) | 
|  | return -EINVAL; | 
|  |  | 
|  | phy_lock_mdio_bus(phydev); | 
|  |  | 
|  | /* Set correct PHY package mode */ | 
|  | val = __phy_package_read(phydev, QCA807X_COMBO_ADDR, | 
|  | QCA807X_CHIP_CONFIGURATION); | 
|  | val &= ~QCA807X_CHIP_CONFIGURATION_MODE_CFG_MASK; | 
|  | /* package_mode can be QSGMII or PSGMII and we validate | 
|  | * this in probe_once. | 
|  | * With package_mode to NA, we default to PSGMII. | 
|  | */ | 
|  | switch (priv->package_mode) { | 
|  | case PHY_INTERFACE_MODE_QSGMII: | 
|  | val |= QCA807X_CHIP_CONFIGURATION_MODE_QSGMII_SGMII; | 
|  | break; | 
|  | case PHY_INTERFACE_MODE_PSGMII: | 
|  | default: | 
|  | val |= QCA807X_CHIP_CONFIGURATION_MODE_PSGMII_ALL_COPPER; | 
|  | } | 
|  | ret = __phy_package_write(phydev, QCA807X_COMBO_ADDR, | 
|  | QCA807X_CHIP_CONFIGURATION, val); | 
|  | if (ret) | 
|  | goto exit; | 
|  |  | 
|  | /* After mode change Serdes reset is required */ | 
|  | val = __phy_package_read(phydev, QCA807X_PQSGMII_ADDR, | 
|  | PQSGMII_CTRL_REG); | 
|  | val &= ~PQSGMII_ANALOG_SW_RESET; | 
|  | ret = __phy_package_write(phydev, QCA807X_PQSGMII_ADDR, | 
|  | PQSGMII_CTRL_REG, val); | 
|  | if (ret) | 
|  | goto exit; | 
|  |  | 
|  | msleep(SERDES_RESET_SLEEP); | 
|  |  | 
|  | val = __phy_package_read(phydev, QCA807X_PQSGMII_ADDR, | 
|  | PQSGMII_CTRL_REG); | 
|  | val |= PQSGMII_ANALOG_SW_RESET; | 
|  | ret = __phy_package_write(phydev, QCA807X_PQSGMII_ADDR, | 
|  | PQSGMII_CTRL_REG, val); | 
|  | if (ret) | 
|  | goto exit; | 
|  |  | 
|  | /* Workaround to enable AZ transmitting ability */ | 
|  | val = __phy_package_read_mmd(phydev, QCA807X_PQSGMII_ADDR, | 
|  | MDIO_MMD_PMAPMD, PQSGMII_MODE_CTRL); | 
|  | val &= ~PQSGMII_MODE_CTRL_AZ_WORKAROUND_MASK; | 
|  | ret = __phy_package_write_mmd(phydev, QCA807X_PQSGMII_ADDR, | 
|  | MDIO_MMD_PMAPMD, PQSGMII_MODE_CTRL, val); | 
|  | if (ret) | 
|  | goto exit; | 
|  |  | 
|  | /* Set PQSGMII TX AMP strength */ | 
|  | val = __phy_package_read(phydev, QCA807X_PQSGMII_ADDR, | 
|  | PQSGMII_DRIVE_CONTROL_1); | 
|  | val &= ~PQSGMII_TX_DRIVER_MASK; | 
|  | val |= FIELD_PREP(PQSGMII_TX_DRIVER_MASK, priv->tx_drive_strength); | 
|  | ret = __phy_package_write(phydev, QCA807X_PQSGMII_ADDR, | 
|  | PQSGMII_DRIVE_CONTROL_1, val); | 
|  | if (ret) | 
|  | goto exit; | 
|  |  | 
|  | /* Prevent PSGMII going into hibernation via PSGMII self test */ | 
|  | val = __phy_package_read_mmd(phydev, QCA807X_COMBO_ADDR, | 
|  | MDIO_MMD_PCS, PQSGMII_MMD3_SERDES_CONTROL); | 
|  | val &= ~BIT(1); | 
|  | ret = __phy_package_write_mmd(phydev, QCA807X_COMBO_ADDR, | 
|  | MDIO_MMD_PCS, PQSGMII_MMD3_SERDES_CONTROL, val); | 
|  |  | 
|  | exit: | 
|  | phy_unlock_mdio_bus(phydev); | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static int qca807x_sfp_insert(void *upstream, const struct sfp_eeprom_id *id) | 
|  | { | 
|  | struct phy_device *phydev = upstream; | 
|  | __ETHTOOL_DECLARE_LINK_MODE_MASK(support) = { 0, }; | 
|  | phy_interface_t iface; | 
|  | int ret; | 
|  | DECLARE_PHY_INTERFACE_MASK(interfaces); | 
|  |  | 
|  | sfp_parse_support(phydev->sfp_bus, id, support, interfaces); | 
|  | iface = sfp_select_interface(phydev->sfp_bus, support); | 
|  |  | 
|  | dev_info(&phydev->mdio.dev, "%s SFP module inserted\n", phy_modes(iface)); | 
|  |  | 
|  | switch (iface) { | 
|  | case PHY_INTERFACE_MODE_1000BASEX: | 
|  | case PHY_INTERFACE_MODE_100BASEX: | 
|  | /* Set PHY mode to PSGMII combo (1/4 copper + combo ports) mode */ | 
|  | ret = phy_modify(phydev, | 
|  | QCA807X_CHIP_CONFIGURATION, | 
|  | QCA807X_CHIP_CONFIGURATION_MODE_CFG_MASK, | 
|  | QCA807X_CHIP_CONFIGURATION_MODE_PSGMII_FIBER); | 
|  | /* Enable fiber mode autodection (1000Base-X or 100Base-FX) */ | 
|  | ret = phy_set_bits_mmd(phydev, | 
|  | MDIO_MMD_AN, | 
|  | QCA807X_MMD7_FIBER_MODE_AUTO_DETECTION, | 
|  | QCA807X_MMD7_FIBER_MODE_AUTO_DETECTION_EN); | 
|  | /* Select fiber page */ | 
|  | ret = phy_clear_bits(phydev, | 
|  | QCA807X_CHIP_CONFIGURATION, | 
|  | QCA807X_BT_BX_REG_SEL); | 
|  |  | 
|  | phydev->port = PORT_FIBRE; | 
|  | break; | 
|  | default: | 
|  | dev_err(&phydev->mdio.dev, "Incompatible SFP module inserted\n"); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | static void qca807x_sfp_remove(void *upstream) | 
|  | { | 
|  | struct phy_device *phydev = upstream; | 
|  |  | 
|  | /* Select copper page */ | 
|  | phy_set_bits(phydev, | 
|  | QCA807X_CHIP_CONFIGURATION, | 
|  | QCA807X_BT_BX_REG_SEL); | 
|  |  | 
|  | phydev->port = PORT_TP; | 
|  | } | 
|  |  | 
|  | static const struct sfp_upstream_ops qca807x_sfp_ops = { | 
|  | .attach = phy_sfp_attach, | 
|  | .detach = phy_sfp_detach, | 
|  | .module_insert = qca807x_sfp_insert, | 
|  | .module_remove = qca807x_sfp_remove, | 
|  | }; | 
|  |  | 
|  | static int qca807x_probe(struct phy_device *phydev) | 
|  | { | 
|  | struct device_node *node = phydev->mdio.dev.of_node; | 
|  | struct qca807x_shared_priv *shared_priv; | 
|  | struct device *dev = &phydev->mdio.dev; | 
|  | struct phy_package_shared *shared; | 
|  | struct qca807x_priv *priv; | 
|  | int ret; | 
|  |  | 
|  | ret = devm_of_phy_package_join(dev, phydev, sizeof(*shared_priv)); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | if (phy_package_probe_once(phydev)) { | 
|  | ret = qca807x_phy_package_probe_once(phydev); | 
|  | if (ret) | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | shared = phydev->shared; | 
|  | shared_priv = shared->priv; | 
|  |  | 
|  | priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); | 
|  | if (!priv) | 
|  | return -ENOMEM; | 
|  |  | 
|  | priv->dac_full_amplitude = of_property_read_bool(node, "qcom,dac-full-amplitude"); | 
|  | priv->dac_full_bias_current = of_property_read_bool(node, "qcom,dac-full-bias-current"); | 
|  | priv->dac_disable_bias_current_tweak = of_property_read_bool(node, | 
|  | "qcom,dac-disable-bias-current-tweak"); | 
|  |  | 
|  | #if IS_ENABLED(CONFIG_GPIOLIB) | 
|  | /* Make sure we don't have mixed leds node and gpio-controller | 
|  | * to prevent registering leds and having gpio-controller usage | 
|  | * conflicting with them. | 
|  | */ | 
|  | if (of_find_property(node, "leds", NULL) && | 
|  | of_find_property(node, "gpio-controller", NULL)) { | 
|  | phydev_err(phydev, "Invalid property detected. LEDs and gpio-controller are mutually exclusive."); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | /* Do not register a GPIO controller unless flagged for it */ | 
|  | if (of_property_read_bool(node, "gpio-controller")) { | 
|  | ret = qca807x_gpio(phydev); | 
|  | if (ret) | 
|  | return ret; | 
|  | } | 
|  | #endif | 
|  |  | 
|  | /* Attach SFP bus on combo port*/ | 
|  | if (phy_read(phydev, QCA807X_CHIP_CONFIGURATION)) { | 
|  | ret = phy_sfp_probe(phydev, &qca807x_sfp_ops); | 
|  | if (ret) | 
|  | return ret; | 
|  | linkmode_set_bit(ETHTOOL_LINK_MODE_FIBRE_BIT, phydev->supported); | 
|  | linkmode_set_bit(ETHTOOL_LINK_MODE_FIBRE_BIT, phydev->advertising); | 
|  | } | 
|  |  | 
|  | phydev->priv = priv; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int qca807x_config_init(struct phy_device *phydev) | 
|  | { | 
|  | struct qca807x_priv *priv = phydev->priv; | 
|  | u16 control_dac; | 
|  | int ret; | 
|  |  | 
|  | if (phy_package_init_once(phydev)) { | 
|  | ret = qca807x_phy_package_config_init_once(phydev); | 
|  | if (ret) | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | control_dac = phy_read_mmd(phydev, MDIO_MMD_AN, | 
|  | QCA807X_MMD7_1000BASE_T_POWER_SAVE_PER_CABLE_LENGTH); | 
|  | control_dac &= ~QCA807X_CONTROL_DAC_MASK; | 
|  | if (!priv->dac_full_amplitude) | 
|  | control_dac |= QCA807X_CONTROL_DAC_DSP_AMPLITUDE; | 
|  | if (!priv->dac_full_amplitude) | 
|  | control_dac |= QCA807X_CONTROL_DAC_DSP_BIAS_CURRENT; | 
|  | if (!priv->dac_disable_bias_current_tweak) | 
|  | control_dac |= QCA807X_CONTROL_DAC_BIAS_CURRENT_TWEAK; | 
|  | return phy_write_mmd(phydev, MDIO_MMD_AN, | 
|  | QCA807X_MMD7_1000BASE_T_POWER_SAVE_PER_CABLE_LENGTH, | 
|  | control_dac); | 
|  | } | 
|  |  | 
|  | static struct phy_driver qca807x_drivers[] = { | 
|  | { | 
|  | PHY_ID_MATCH_EXACT(PHY_ID_QCA8072), | 
|  | .name           = "Qualcomm QCA8072", | 
|  | .flags		= PHY_POLL_CABLE_TEST, | 
|  | /* PHY_GBIT_FEATURES */ | 
|  | .probe		= qca807x_probe, | 
|  | .config_init	= qca807x_config_init, | 
|  | .read_status	= qca807x_read_status, | 
|  | .config_intr	= at803x_config_intr, | 
|  | .handle_interrupt = at803x_handle_interrupt, | 
|  | .soft_reset	= genphy_soft_reset, | 
|  | .get_tunable	= at803x_get_tunable, | 
|  | .set_tunable	= at803x_set_tunable, | 
|  | .resume		= genphy_resume, | 
|  | .suspend	= genphy_suspend, | 
|  | .cable_test_start	= qca807x_cable_test_start, | 
|  | .cable_test_get_status	= qca808x_cable_test_get_status, | 
|  | }, | 
|  | { | 
|  | PHY_ID_MATCH_EXACT(PHY_ID_QCA8075), | 
|  | .name           = "Qualcomm QCA8075", | 
|  | .flags		= PHY_POLL_CABLE_TEST, | 
|  | /* PHY_GBIT_FEATURES */ | 
|  | .probe		= qca807x_probe, | 
|  | .config_init	= qca807x_config_init, | 
|  | .read_status	= qca807x_read_status, | 
|  | .config_intr	= at803x_config_intr, | 
|  | .handle_interrupt = at803x_handle_interrupt, | 
|  | .soft_reset	= genphy_soft_reset, | 
|  | .get_tunable	= at803x_get_tunable, | 
|  | .set_tunable	= at803x_set_tunable, | 
|  | .resume		= genphy_resume, | 
|  | .suspend	= genphy_suspend, | 
|  | .cable_test_start	= qca807x_cable_test_start, | 
|  | .cable_test_get_status	= qca808x_cable_test_get_status, | 
|  | .led_brightness_set = qca807x_led_brightness_set, | 
|  | .led_blink_set = qca807x_led_blink_set, | 
|  | .led_hw_is_supported = qca807x_led_hw_is_supported, | 
|  | .led_hw_control_set = qca807x_led_hw_control_set, | 
|  | .led_hw_control_get = qca807x_led_hw_control_get, | 
|  | }, | 
|  | }; | 
|  | module_phy_driver(qca807x_drivers); | 
|  |  | 
|  | static struct mdio_device_id __maybe_unused qca807x_tbl[] = { | 
|  | { PHY_ID_MATCH_EXACT(PHY_ID_QCA8072) }, | 
|  | { PHY_ID_MATCH_EXACT(PHY_ID_QCA8075) }, | 
|  | { } | 
|  | }; | 
|  |  | 
|  | MODULE_AUTHOR("Robert Marko <robert.marko@sartura.hr>"); | 
|  | MODULE_AUTHOR("Christian Marangi <ansuelsmth@gmail.com>"); | 
|  | MODULE_DESCRIPTION("Qualcomm QCA807x PHY driver"); | 
|  | MODULE_DEVICE_TABLE(mdio, qca807x_tbl); | 
|  | MODULE_LICENSE("GPL"); |