[net-next RFC PATCH v2 11/11] net: phy: qca807x: Add support for configurable LED

From: Christian Marangi
Date: Fri Nov 24 2023 - 19:36:26 EST


QCA8072/5 have up to 2 LEDs attached for PHY.

LEDs can be configured to be ON/hw blink or be set to HW control.

Hw blink mode is set to blink at 4Hz or 250ms.

PHY can support both copper (TP) or fiber (FIBRE) kind and supports
different HW control modes based on the port type.

HW control modes supported for netdev trigger for copper ports are:
- LINK_10
- LINK_100
- LINK_1000
- TX
- RX
- FULL_DUPLEX
- HALF_DUPLEX

HW control modes supported for netdev trigger for fiber ports are:
- LINK_100
- LINK_1000
- TX
- RX
- FULL_DUPLEX
- HALF_DUPLEX

LED support conflicts with GPIO controller feature and must be disabled
if gpio-controller is used for the PHY.

Signed-off-by: Christian Marangi <ansuelsmth@xxxxxxxxx>
---
drivers/net/phy/qca807x.c | 382 +++++++++++++++++++++++++++++++++++++-
1 file changed, 375 insertions(+), 7 deletions(-)

diff --git a/drivers/net/phy/qca807x.c b/drivers/net/phy/qca807x.c
index 6a1bad1b95c3..d1648f801ff3 100644
--- a/drivers/net/phy/qca807x.c
+++ b/drivers/net/phy/qca807x.c
@@ -79,17 +79,60 @@
#define QCA807X_MMD7_1000BASE_T_POWER_SAVE_PER_CABLE_LENGTH 0x801a
#define QCA807X_CONTROL_DAC_MASK GENMASK(2, 0)

+#define QCA807X_MMD7_LED_GLOBAL 0x8073
+#define QCA807X_LED_BLINK_1 GENMASK(11, 6)
+#define QCA807X_LED_BLINK_2 GENMASK(5, 0)
+/* Values are the same for both BLINK_1 and BLINK_2 */
+#define QCA807X_LED_BLINK_FREQ_MASK GENMASK(5, 3)
+#define QCA807X_LED_BLINK_FREQ_2HZ FIELD_PREP(QCA807X_LED_BLINK_FREQ_MASK, 0x0)
+#define QCA807X_LED_BLINK_FREQ_4HZ FIELD_PREP(QCA807X_LED_BLINK_FREQ_MASK, 0x1)
+#define QCA807X_LED_BLINK_FREQ_8HZ FIELD_PREP(QCA807X_LED_BLINK_FREQ_MASK, 0x2)
+#define QCA807X_LED_BLINK_FREQ_16HZ FIELD_PREP(QCA807X_LED_BLINK_FREQ_MASK, 0x3)
+#define QCA807X_LED_BLINK_FREQ_32HZ FIELD_PREP(QCA807X_LED_BLINK_FREQ_MASK, 0x4)
+#define QCA807X_LED_BLINK_FREQ_64HZ FIELD_PREP(QCA807X_LED_BLINK_FREQ_MASK, 0x5)
+#define QCA807X_LED_BLINK_FREQ_128HZ FIELD_PREP(QCA807X_LED_BLINK_FREQ_MASK, 0x6)
+#define QCA807X_LED_BLINK_FREQ_256HZ FIELD_PREP(QCA807X_LED_BLINK_FREQ_MASK, 0x7)
+#define QCA807X_LED_BLINK_DUTY_MASK GENMASK(2, 0)
+#define QCA807X_LED_BLINK_DUTY_50_50 FIELD_PREP(QCA807X_LED_BLINK_DUTY_MASK, 0x0)
+#define QCA807X_LED_BLINK_DUTY_75_25 FIELD_PREP(QCA807X_LED_BLINK_DUTY_MASK, 0x1)
+#define QCA807X_LED_BLINK_DUTY_25_75 FIELD_PREP(QCA807X_LED_BLINK_DUTY_MASK, 0x2)
+#define QCA807X_LED_BLINK_DUTY_33_67 FIELD_PREP(QCA807X_LED_BLINK_DUTY_MASK, 0x3)
+#define QCA807X_LED_BLINK_DUTY_67_33 FIELD_PREP(QCA807X_LED_BLINK_DUTY_MASK, 0x4)
+#define QCA807X_LED_BLINK_DUTY_17_83 FIELD_PREP(QCA807X_LED_BLINK_DUTY_MASK, 0x5)
+#define QCA807X_LED_BLINK_DUTY_83_17 FIELD_PREP(QCA807X_LED_BLINK_DUTY_MASK, 0x6)
+#define QCA807X_LED_BLINK_DUTY_8_92 FIELD_PREP(QCA807X_LED_BLINK_DUTY_MASK, 0x7)
#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_LED_TXACT_BLK_EN_2 BIT(10)
-#define QCA807X_LED_RXACT_BLK_EN_2 BIT(9)
-#define QCA807X_LED_GT_ON_EN_2 BIT(6)
-#define QCA807X_LED_HT_ON_EN_2 BIT(5)
-#define QCA807X_LED_BT_ON_EN_2 BIT(4)
-#define QCA807X_GPIO_FORCE_EN BIT(15)
-#define QCA807X_GPIO_FORCE_MODE_MASK GENMASK(14, 13)
+/* Values are the same for LED1 and LED2 */
+/* Values for control 1 */
+#define QCA807X_LED_COPPER_ON_BLINK_MASK GENMASK(12, 0)
+#define QCA807X_LED_FDX_ON_EN BIT(12)
+#define QCA807X_LED_HDX_ON_EN BIT(11)
+#define QCA807X_LED_TXACT_BLK_EN BIT(10)
+#define QCA807X_LED_RXACT_BLK_EN BIT(9)
+#define QCA807X_LED_GT_ON_EN BIT(6)
+#define QCA807X_LED_HT_ON_EN BIT(5)
+#define QCA807X_LED_BT_ON_EN BIT(4)
+/* Values for control 2 */
+#define QCA807X_LED_FORCE_EN BIT(15)
+#define QCA807X_LED_FORCE_MODE_MASK GENMASK(14, 13)
+#define QCA807X_LED_FORCE_BLINK_1 FIELD_PREP(QCA807X_LED_FORCE_MODE_MASK, 0x3)
+#define QCA807X_LED_FORCE_BLINK_2 FIELD_PREP(QCA807X_LED_FORCE_MODE_MASK, 0x2)
+#define QCA807X_LED_FORCE_ON FIELD_PREP(QCA807X_LED_FORCE_MODE_MASK, 0x1)
+#define QCA807X_LED_FORCE_OFF FIELD_PREP(QCA807X_LED_FORCE_MODE_MASK, 0x0)
+#define QCA807X_LED_FIBER_ON_BLINK_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 QCA807X_LED_FORCE_EN
+#define QCA807X_GPIO_FORCE_MODE_MASK QCA807X_LED_FORCE_MODE_MASK

#define QCA807X_INTR_ENABLE 0x12
#define QCA807X_INTR_STATUS 0x13
@@ -357,6 +400,320 @@ static int qca807x_cable_test_start(struct phy_device *phydev)
return ret;
}

+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 |= QCA807X_LED_TXACT_BLK_EN;
+ if (test_bit(TRIGGER_NETDEV_RX, &rules))
+ *offload_trigger |= QCA807X_LED_RXACT_BLK_EN;
+ if (test_bit(TRIGGER_NETDEV_LINK_10, &rules))
+ *offload_trigger |= QCA807X_LED_BT_ON_EN;
+ if (test_bit(TRIGGER_NETDEV_LINK_100, &rules))
+ *offload_trigger |= QCA807X_LED_HT_ON_EN;
+ if (test_bit(TRIGGER_NETDEV_LINK_1000, &rules))
+ *offload_trigger |= QCA807X_LED_GT_ON_EN;
+ if (test_bit(TRIGGER_NETDEV_HALF_DUPLEX, &rules))
+ *offload_trigger |= QCA807X_LED_HDX_ON_EN;
+ if (test_bit(TRIGGER_NETDEV_FULL_DUPLEX, &rules))
+ *offload_trigger |= QCA807X_LED_FDX_ON_EN;
+ 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)
+{
+ int val, reg, ret;
+
+ switch (index) {
+ case 0:
+ reg = QCA807X_MMD7_LED_100N_2;
+ break;
+ case 1:
+ reg = QCA807X_MMD7_LED_1000N_2;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, reg);
+ val &= ~QCA807X_LED_FORCE_EN;
+ ret = phy_write_mmd(phydev, MDIO_MMD_AN, reg, val);
+
+ return ret;
+}
+
+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)
+{
+ int val, ret, copper_reg, fibre_reg;
+ u16 offload_trigger = 0;
+
+ switch (index) {
+ case 0:
+ copper_reg = QCA807X_MMD7_LED_100N_1;
+ fibre_reg = QCA807X_MMD7_LED_100N_2;
+ break;
+ case 1:
+ copper_reg = QCA807X_MMD7_LED_1000N_1;
+ fibre_reg = QCA807X_MMD7_LED_1000N_2;
+ break;
+ default:
+ 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:
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, copper_reg);
+ val &= ~QCA807X_LED_COPPER_ON_BLINK_MASK;
+ val |= offload_trigger;
+ ret = phy_write_mmd(phydev, MDIO_MMD_AN, copper_reg, val);
+ break;
+ case PORT_FIBRE:
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, fibre_reg);
+ val &= ~QCA807X_LED_FIBER_ON_BLINK_MASK;
+ val |= offload_trigger;
+ ret = phy_write_mmd(phydev, MDIO_MMD_AN, fibre_reg, val);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return ret;
+}
+
+static bool qca807x_led_hw_control_status(struct phy_device *phydev, u8 index)
+{
+ int val, reg;
+
+ switch (index) {
+ case 0:
+ reg = QCA807X_MMD7_LED_100N_2;
+ break;
+ case 1:
+ reg = QCA807X_MMD7_LED_1000N_2;
+ break;
+ default:
+ return false;
+ }
+
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, reg);
+
+ return !(val & QCA807X_LED_FORCE_EN);
+}
+
+static int qca807x_led_hw_control_get(struct phy_device *phydev, u8 index,
+ unsigned long *rules)
+{
+ int val, copper_reg, fibre_reg;
+
+ switch (index) {
+ case 0:
+ copper_reg = QCA807X_MMD7_LED_100N_1;
+ fibre_reg = QCA807X_MMD7_LED_100N_2;
+ break;
+ case 1:
+ copper_reg = QCA807X_MMD7_LED_1000N_1;
+ fibre_reg = QCA807X_MMD7_LED_100N_2;
+ break;
+ default:
+ 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:
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, copper_reg);
+ if (val & QCA807X_LED_TXACT_BLK_EN)
+ set_bit(TRIGGER_NETDEV_TX, rules);
+ if (val & QCA807X_LED_RXACT_BLK_EN)
+ set_bit(TRIGGER_NETDEV_RX, rules);
+ if (val & QCA807X_LED_BT_ON_EN)
+ set_bit(TRIGGER_NETDEV_LINK_10, rules);
+ if (val & QCA807X_LED_HT_ON_EN)
+ set_bit(TRIGGER_NETDEV_LINK_100, rules);
+ if (val & QCA807X_LED_GT_ON_EN)
+ set_bit(TRIGGER_NETDEV_LINK_1000, rules);
+ if (val & QCA807X_LED_HDX_ON_EN)
+ set_bit(TRIGGER_NETDEV_HALF_DUPLEX, rules);
+ if (val & QCA807X_LED_FDX_ON_EN)
+ set_bit(TRIGGER_NETDEV_FULL_DUPLEX, rules);
+ break;
+ case PORT_FIBRE:
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, fibre_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)
+{
+ int val, copper_reg, fibre_reg, ret;
+
+ switch (index) {
+ case 0:
+ copper_reg = QCA807X_MMD7_LED_100N_1;
+ fibre_reg = QCA807X_MMD7_LED_100N_2;
+ break;
+ case 1:
+ copper_reg = QCA807X_MMD7_LED_1000N_1;
+ fibre_reg = QCA807X_MMD7_LED_100N_2;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ switch (phydev->port) {
+ case PORT_TP:
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, copper_reg);
+ val &= ~QCA807X_LED_COPPER_ON_BLINK_MASK;
+ ret = phy_write_mmd(phydev, MDIO_MMD_AN, copper_reg, val);
+ break;
+ case PORT_FIBRE:
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, fibre_reg);
+ val &= ~QCA807X_LED_FIBER_ON_BLINK_MASK;
+ ret = phy_write_mmd(phydev, MDIO_MMD_AN, fibre_reg, val);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return ret;
+}
+
+static int qca807x_led_brightness_set(struct phy_device *phydev,
+ u8 index, enum led_brightness value)
+{
+ int val, ret;
+ u16 reg;
+
+ switch (index) {
+ case 0:
+ reg = QCA807X_MMD7_LED_100N_2;
+ break;
+ case 1:
+ reg = QCA807X_MMD7_LED_1000N_2;
+ break;
+ default:
+ 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;
+ }
+
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, reg);
+ val &= ~(QCA807X_LED_FORCE_EN | QCA807X_LED_FORCE_MODE_MASK);
+ val |= QCA807X_LED_FORCE_EN;
+ if (value)
+ val |= QCA807X_LED_FORCE_ON;
+ ret = phy_write_mmd(phydev, MDIO_MMD_AN, reg, val);
+
+ return ret;
+}
+
+static int qca807x_led_blink_set(struct phy_device *phydev, u8 index,
+ unsigned long *delay_on,
+ unsigned long *delay_off)
+{
+ int val, ret;
+ u16 reg;
+
+ switch (index) {
+ case 0:
+ reg = QCA807X_MMD7_LED_100N_2;
+ break;
+ case 1:
+ reg = QCA807X_MMD7_LED_1000N_2;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* Set blink to 50% off, 50% on at 4Hz by default */
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, QCA807X_MMD7_LED_GLOBAL);
+ val &= ~(QCA807X_LED_BLINK_FREQ_MASK | QCA807X_LED_BLINK_DUTY_MASK);
+ val |= QCA807X_LED_BLINK_FREQ_4HZ | QCA807X_LED_BLINK_DUTY_50_50;
+ ret = phy_write_mmd(phydev, MDIO_MMD_AN, QCA807X_MMD7_LED_GLOBAL, val);
+
+ /* We use BLINK_1 for normal blinking */
+ val = phy_read_mmd(phydev, MDIO_MMD_AN, reg);
+ val &= ~(QCA807X_LED_FORCE_EN | QCA807X_LED_FORCE_MODE_MASK);
+ val |= QCA807X_LED_FORCE_EN | QCA807X_LED_FORCE_BLINK_1;
+ ret = phy_write_mmd(phydev, MDIO_MMD_AN, reg, val);
+
+ /* We set blink to 4Hz, aka 250ms */
+ *delay_on = 250 / 2;
+ *delay_off = 250 / 2;
+
+ return ret;
+}
+
#ifdef CONFIG_GPIOLIB
static int qca807x_gpio_get_direction(struct gpio_chip *gc, unsigned int offset)
{
@@ -746,6 +1103,12 @@ static int qca807x_probe(struct phy_device *phydev)
ret = qca807x_gpio(phydev);
if (ret)
return ret;
+
+ phydev->drv->led_brightness_set = NULL;
+ phydev->drv->led_blink_set = NULL;
+ phydev->drv->led_hw_is_supported = NULL;
+ phydev->drv->led_hw_control_set = NULL;
+ phydev->drv->led_hw_control_get = NULL;
}
}

@@ -933,6 +1296,11 @@ static struct phy_driver qca807x_drivers[] = {
.suspend = genphy_suspend,
.cable_test_start = qca807x_cable_test_start,
.cable_test_get_status = qca807x_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,
/* PHY package define */
.phy_package_global_phys_offset = qca807x_global_phys_offset,
.phy_package_global_phys_num = ARRAY_SIZE(qca807x_global_phys_offset),
--
2.40.1