| // SPDX-License-Identifier: GPL-2.0 |
| #include <linux/property.h> |
| #include <linux/regmap.h> |
| #include <net/dsa.h> |
| |
| #include "qca8k.h" |
| #include "qca8k_leds.h" |
| |
| static u32 qca8k_phy_to_port(int phy) |
| { |
| /* Internal PHY 0 has port at index 1. |
| * Internal PHY 1 has port at index 2. |
| * Internal PHY 2 has port at index 3. |
| * Internal PHY 3 has port at index 4. |
| * Internal PHY 4 has port at index 5. |
| */ |
| |
| return phy + 1; |
| } |
| |
| static int |
| qca8k_get_enable_led_reg(int port_num, int led_num, struct qca8k_led_pattern_en *reg_info) |
| { |
| switch (port_num) { |
| case 0: |
| reg_info->reg = QCA8K_LED_CTRL_REG(led_num); |
| reg_info->shift = QCA8K_LED_PHY0123_CONTROL_RULE_SHIFT; |
| break; |
| case 1: |
| case 2: |
| case 3: |
| /* Port 123 are controlled on a different reg */ |
| reg_info->reg = QCA8K_LED_CTRL3_REG; |
| reg_info->shift = QCA8K_LED_PHY123_PATTERN_EN_SHIFT(port_num, led_num); |
| break; |
| case 4: |
| reg_info->reg = QCA8K_LED_CTRL_REG(led_num); |
| reg_info->shift = QCA8K_LED_PHY4_CONTROL_RULE_SHIFT; |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int |
| qca8k_get_control_led_reg(int port_num, int led_num, struct qca8k_led_pattern_en *reg_info) |
| { |
| reg_info->reg = QCA8K_LED_CTRL_REG(led_num); |
| |
| /* 6 total control rule: |
| * 3 control rules for phy0-3 that applies to all their leds |
| * 3 control rules for phy4 |
| */ |
| if (port_num == 4) |
| reg_info->shift = QCA8K_LED_PHY4_CONTROL_RULE_SHIFT; |
| else |
| reg_info->shift = QCA8K_LED_PHY0123_CONTROL_RULE_SHIFT; |
| |
| return 0; |
| } |
| |
| static int |
| qca8k_parse_netdev(unsigned long rules, u32 *offload_trigger) |
| { |
| /* Parsing specific to netdev trigger */ |
| if (test_bit(TRIGGER_NETDEV_TX, &rules)) |
| *offload_trigger |= QCA8K_LED_TX_BLINK_MASK; |
| if (test_bit(TRIGGER_NETDEV_RX, &rules)) |
| *offload_trigger |= QCA8K_LED_RX_BLINK_MASK; |
| if (test_bit(TRIGGER_NETDEV_LINK_10, &rules)) |
| *offload_trigger |= QCA8K_LED_LINK_10M_EN_MASK; |
| if (test_bit(TRIGGER_NETDEV_LINK_100, &rules)) |
| *offload_trigger |= QCA8K_LED_LINK_100M_EN_MASK; |
| if (test_bit(TRIGGER_NETDEV_LINK_1000, &rules)) |
| *offload_trigger |= QCA8K_LED_LINK_1000M_EN_MASK; |
| if (test_bit(TRIGGER_NETDEV_HALF_DUPLEX, &rules)) |
| *offload_trigger |= QCA8K_LED_HALF_DUPLEX_MASK; |
| if (test_bit(TRIGGER_NETDEV_FULL_DUPLEX, &rules)) |
| *offload_trigger |= QCA8K_LED_FULL_DUPLEX_MASK; |
| |
| if (rules && !*offload_trigger) |
| return -EOPNOTSUPP; |
| |
| /* Enable some default rule by default to the requested mode: |
| * - Blink at 4Hz by default |
| */ |
| *offload_trigger |= QCA8K_LED_BLINK_4HZ; |
| |
| return 0; |
| } |
| |
| static int |
| qca8k_led_brightness_set(struct qca8k_led *led, |
| enum led_brightness brightness) |
| { |
| struct qca8k_led_pattern_en reg_info; |
| struct qca8k_priv *priv = led->priv; |
| u32 mask, val; |
| |
| qca8k_get_enable_led_reg(led->port_num, led->led_num, ®_info); |
| |
| val = QCA8K_LED_ALWAYS_OFF; |
| if (brightness) |
| val = QCA8K_LED_ALWAYS_ON; |
| |
| /* HW regs to control brightness is special and port 1-2-3 |
| * are placed in a different reg. |
| * |
| * To control port 0 brightness: |
| * - the 2 bit (15, 14) of: |
| * - QCA8K_LED_CTRL0_REG for led1 |
| * - QCA8K_LED_CTRL1_REG for led2 |
| * - QCA8K_LED_CTRL2_REG for led3 |
| * |
| * To control port 4: |
| * - the 2 bit (31, 30) of: |
| * - QCA8K_LED_CTRL0_REG for led1 |
| * - QCA8K_LED_CTRL1_REG for led2 |
| * - QCA8K_LED_CTRL2_REG for led3 |
| * |
| * To control port 1: |
| * - the 2 bit at (9, 8) of QCA8K_LED_CTRL3_REG are used for led1 |
| * - the 2 bit at (11, 10) of QCA8K_LED_CTRL3_REG are used for led2 |
| * - the 2 bit at (13, 12) of QCA8K_LED_CTRL3_REG are used for led3 |
| * |
| * To control port 2: |
| * - the 2 bit at (15, 14) of QCA8K_LED_CTRL3_REG are used for led1 |
| * - the 2 bit at (17, 16) of QCA8K_LED_CTRL3_REG are used for led2 |
| * - the 2 bit at (19, 18) of QCA8K_LED_CTRL3_REG are used for led3 |
| * |
| * To control port 3: |
| * - the 2 bit at (21, 20) of QCA8K_LED_CTRL3_REG are used for led1 |
| * - the 2 bit at (23, 22) of QCA8K_LED_CTRL3_REG are used for led2 |
| * - the 2 bit at (25, 24) of QCA8K_LED_CTRL3_REG are used for led3 |
| * |
| * To abstract this and have less code, we use the port and led numm |
| * to calculate the shift and the correct reg due to this problem of |
| * not having a 1:1 map of LED with the regs. |
| */ |
| if (led->port_num == 0 || led->port_num == 4) { |
| mask = QCA8K_LED_PATTERN_EN_MASK; |
| val <<= QCA8K_LED_PATTERN_EN_SHIFT; |
| } else { |
| mask = QCA8K_LED_PHY123_PATTERN_EN_MASK; |
| } |
| |
| return regmap_update_bits(priv->regmap, reg_info.reg, |
| mask << reg_info.shift, |
| val << reg_info.shift); |
| } |
| |
| static int |
| qca8k_cled_brightness_set_blocking(struct led_classdev *ldev, |
| enum led_brightness brightness) |
| { |
| struct qca8k_led *led = container_of(ldev, struct qca8k_led, cdev); |
| |
| return qca8k_led_brightness_set(led, brightness); |
| } |
| |
| static enum led_brightness |
| qca8k_led_brightness_get(struct qca8k_led *led) |
| { |
| struct qca8k_led_pattern_en reg_info; |
| struct qca8k_priv *priv = led->priv; |
| u32 val; |
| int ret; |
| |
| qca8k_get_enable_led_reg(led->port_num, led->led_num, ®_info); |
| |
| ret = regmap_read(priv->regmap, reg_info.reg, &val); |
| if (ret) |
| return 0; |
| |
| val >>= reg_info.shift; |
| |
| if (led->port_num == 0 || led->port_num == 4) { |
| val &= QCA8K_LED_PATTERN_EN_MASK; |
| val >>= QCA8K_LED_PATTERN_EN_SHIFT; |
| } else { |
| val &= QCA8K_LED_PHY123_PATTERN_EN_MASK; |
| } |
| |
| /* Assume brightness ON only when the LED is set to always ON */ |
| return val == QCA8K_LED_ALWAYS_ON; |
| } |
| |
| static int |
| qca8k_cled_blink_set(struct led_classdev *ldev, |
| unsigned long *delay_on, |
| unsigned long *delay_off) |
| { |
| struct qca8k_led *led = container_of(ldev, struct qca8k_led, cdev); |
| u32 mask, val = QCA8K_LED_ALWAYS_BLINK_4HZ; |
| struct qca8k_led_pattern_en reg_info; |
| struct qca8k_priv *priv = led->priv; |
| |
| if (*delay_on == 0 && *delay_off == 0) { |
| *delay_on = 125; |
| *delay_off = 125; |
| } |
| |
| if (*delay_on != 125 || *delay_off != 125) { |
| /* The hardware only supports blinking at 4Hz. Fall back |
| * to software implementation in other cases. |
| */ |
| return -EINVAL; |
| } |
| |
| qca8k_get_enable_led_reg(led->port_num, led->led_num, ®_info); |
| |
| if (led->port_num == 0 || led->port_num == 4) { |
| mask = QCA8K_LED_PATTERN_EN_MASK; |
| val <<= QCA8K_LED_PATTERN_EN_SHIFT; |
| } else { |
| mask = QCA8K_LED_PHY123_PATTERN_EN_MASK; |
| } |
| |
| regmap_update_bits(priv->regmap, reg_info.reg, mask << reg_info.shift, |
| val << reg_info.shift); |
| |
| return 0; |
| } |
| |
| static int |
| qca8k_cled_trigger_offload(struct led_classdev *ldev, bool enable) |
| { |
| struct qca8k_led *led = container_of(ldev, struct qca8k_led, cdev); |
| |
| struct qca8k_led_pattern_en reg_info; |
| struct qca8k_priv *priv = led->priv; |
| u32 mask, val = QCA8K_LED_ALWAYS_OFF; |
| |
| qca8k_get_enable_led_reg(led->port_num, led->led_num, ®_info); |
| |
| if (enable) |
| val = QCA8K_LED_RULE_CONTROLLED; |
| |
| if (led->port_num == 0 || led->port_num == 4) { |
| mask = QCA8K_LED_PATTERN_EN_MASK; |
| val <<= QCA8K_LED_PATTERN_EN_SHIFT; |
| } else { |
| mask = QCA8K_LED_PHY123_PATTERN_EN_MASK; |
| } |
| |
| return regmap_update_bits(priv->regmap, reg_info.reg, mask << reg_info.shift, |
| val << reg_info.shift); |
| } |
| |
| static bool |
| qca8k_cled_hw_control_status(struct led_classdev *ldev) |
| { |
| struct qca8k_led *led = container_of(ldev, struct qca8k_led, cdev); |
| |
| struct qca8k_led_pattern_en reg_info; |
| struct qca8k_priv *priv = led->priv; |
| u32 val; |
| |
| qca8k_get_enable_led_reg(led->port_num, led->led_num, ®_info); |
| |
| regmap_read(priv->regmap, reg_info.reg, &val); |
| |
| val >>= reg_info.shift; |
| |
| if (led->port_num == 0 || led->port_num == 4) { |
| val &= QCA8K_LED_PATTERN_EN_MASK; |
| val >>= QCA8K_LED_PATTERN_EN_SHIFT; |
| } else { |
| val &= QCA8K_LED_PHY123_PATTERN_EN_MASK; |
| } |
| |
| return val == QCA8K_LED_RULE_CONTROLLED; |
| } |
| |
| static int |
| qca8k_cled_hw_control_is_supported(struct led_classdev *ldev, unsigned long rules) |
| { |
| u32 offload_trigger = 0; |
| |
| return qca8k_parse_netdev(rules, &offload_trigger); |
| } |
| |
| static int |
| qca8k_cled_hw_control_set(struct led_classdev *ldev, unsigned long rules) |
| { |
| struct qca8k_led *led = container_of(ldev, struct qca8k_led, cdev); |
| struct qca8k_led_pattern_en reg_info; |
| struct qca8k_priv *priv = led->priv; |
| u32 offload_trigger = 0; |
| int ret; |
| |
| ret = qca8k_parse_netdev(rules, &offload_trigger); |
| if (ret) |
| return ret; |
| |
| ret = qca8k_cled_trigger_offload(ldev, true); |
| if (ret) |
| return ret; |
| |
| qca8k_get_control_led_reg(led->port_num, led->led_num, ®_info); |
| |
| return regmap_update_bits(priv->regmap, reg_info.reg, |
| QCA8K_LED_RULE_MASK << reg_info.shift, |
| offload_trigger << reg_info.shift); |
| } |
| |
| static int |
| qca8k_cled_hw_control_get(struct led_classdev *ldev, unsigned long *rules) |
| { |
| struct qca8k_led *led = container_of(ldev, struct qca8k_led, cdev); |
| struct qca8k_led_pattern_en reg_info; |
| struct qca8k_priv *priv = led->priv; |
| u32 val; |
| int ret; |
| |
| /* With hw control not active return err */ |
| if (!qca8k_cled_hw_control_status(ldev)) |
| return -EINVAL; |
| |
| qca8k_get_control_led_reg(led->port_num, led->led_num, ®_info); |
| |
| ret = regmap_read(priv->regmap, reg_info.reg, &val); |
| if (ret) |
| return ret; |
| |
| val >>= reg_info.shift; |
| val &= QCA8K_LED_RULE_MASK; |
| |
| /* Parsing specific to netdev trigger */ |
| if (val & QCA8K_LED_TX_BLINK_MASK) |
| set_bit(TRIGGER_NETDEV_TX, rules); |
| if (val & QCA8K_LED_RX_BLINK_MASK) |
| set_bit(TRIGGER_NETDEV_RX, rules); |
| if (val & QCA8K_LED_LINK_10M_EN_MASK) |
| set_bit(TRIGGER_NETDEV_LINK_10, rules); |
| if (val & QCA8K_LED_LINK_100M_EN_MASK) |
| set_bit(TRIGGER_NETDEV_LINK_100, rules); |
| if (val & QCA8K_LED_LINK_1000M_EN_MASK) |
| set_bit(TRIGGER_NETDEV_LINK_1000, rules); |
| if (val & QCA8K_LED_HALF_DUPLEX_MASK) |
| set_bit(TRIGGER_NETDEV_HALF_DUPLEX, rules); |
| if (val & QCA8K_LED_FULL_DUPLEX_MASK) |
| set_bit(TRIGGER_NETDEV_FULL_DUPLEX, rules); |
| |
| return 0; |
| } |
| |
| static struct device *qca8k_cled_hw_control_get_device(struct led_classdev *ldev) |
| { |
| struct qca8k_led *led = container_of(ldev, struct qca8k_led, cdev); |
| struct qca8k_priv *priv = led->priv; |
| struct dsa_port *dp; |
| |
| dp = dsa_to_port(priv->ds, qca8k_phy_to_port(led->port_num)); |
| if (!dp) |
| return NULL; |
| if (dp->user) |
| return &dp->user->dev; |
| return NULL; |
| } |
| |
| static int |
| qca8k_parse_port_leds(struct qca8k_priv *priv, struct fwnode_handle *port, int port_num) |
| { |
| struct fwnode_handle *led = NULL, *leds = NULL; |
| struct led_init_data init_data = { }; |
| enum led_default_state state; |
| struct qca8k_led *port_led; |
| int led_num, led_index; |
| int ret; |
| |
| leds = fwnode_get_named_child_node(port, "leds"); |
| if (!leds) { |
| dev_dbg(priv->dev, "No Leds node specified in device tree for port %d!\n", |
| port_num); |
| return 0; |
| } |
| |
| fwnode_for_each_child_node(leds, led) { |
| /* Reg represent the led number of the port. |
| * Each port can have at most 3 leds attached |
| * Commonly: |
| * 1. is gigabit led |
| * 2. is mbit led |
| * 3. additional status led |
| */ |
| if (fwnode_property_read_u32(led, "reg", &led_num)) |
| continue; |
| |
| if (led_num >= QCA8K_LED_PORT_COUNT) { |
| dev_warn(priv->dev, "Invalid LED reg %d defined for port %d", |
| led_num, port_num); |
| continue; |
| } |
| |
| led_index = QCA8K_LED_PORT_INDEX(port_num, led_num); |
| |
| port_led = &priv->ports_led[led_index]; |
| port_led->port_num = port_num; |
| port_led->led_num = led_num; |
| port_led->priv = priv; |
| |
| state = led_init_default_state_get(led); |
| switch (state) { |
| case LEDS_DEFSTATE_ON: |
| port_led->cdev.brightness = 1; |
| qca8k_led_brightness_set(port_led, 1); |
| break; |
| case LEDS_DEFSTATE_KEEP: |
| port_led->cdev.brightness = |
| qca8k_led_brightness_get(port_led); |
| break; |
| default: |
| port_led->cdev.brightness = 0; |
| qca8k_led_brightness_set(port_led, 0); |
| } |
| |
| port_led->cdev.max_brightness = 1; |
| port_led->cdev.brightness_set_blocking = qca8k_cled_brightness_set_blocking; |
| port_led->cdev.blink_set = qca8k_cled_blink_set; |
| port_led->cdev.hw_control_is_supported = qca8k_cled_hw_control_is_supported; |
| port_led->cdev.hw_control_set = qca8k_cled_hw_control_set; |
| port_led->cdev.hw_control_get = qca8k_cled_hw_control_get; |
| port_led->cdev.hw_control_get_device = qca8k_cled_hw_control_get_device; |
| port_led->cdev.hw_control_trigger = "netdev"; |
| init_data.default_label = ":port"; |
| init_data.fwnode = led; |
| init_data.devname_mandatory = true; |
| init_data.devicename = kasprintf(GFP_KERNEL, "%s:0%d", |
| priv->internal_mdio_bus->id, |
| port_num); |
| if (!init_data.devicename) { |
| fwnode_handle_put(led); |
| fwnode_handle_put(leds); |
| return -ENOMEM; |
| } |
| |
| ret = devm_led_classdev_register_ext(priv->dev, &port_led->cdev, &init_data); |
| if (ret) |
| dev_warn(priv->dev, "Failed to init LED %d for port %d", led_num, port_num); |
| |
| kfree(init_data.devicename); |
| } |
| |
| fwnode_handle_put(leds); |
| return 0; |
| } |
| |
| int |
| qca8k_setup_led_ctrl(struct qca8k_priv *priv) |
| { |
| struct fwnode_handle *ports, *port; |
| int port_num; |
| int ret; |
| |
| ports = device_get_named_child_node(priv->dev, "ports"); |
| if (!ports) { |
| dev_info(priv->dev, "No ports node specified in device tree!"); |
| return 0; |
| } |
| |
| fwnode_for_each_child_node(ports, port) { |
| if (fwnode_property_read_u32(port, "reg", &port_num)) |
| continue; |
| |
| /* Skip checking for CPU port 0 and CPU port 6 as not supported */ |
| if (port_num == 0 || port_num == 6) |
| continue; |
| |
| /* Each port can have at most 3 different leds attached. |
| * Switch port starts from 0 to 6, but port 0 and 6 are CPU |
| * port. The port index needs to be decreased by one to identify |
| * the correct port for LED setup. |
| */ |
| ret = qca8k_parse_port_leds(priv, port, qca8k_port_to_phy(port_num)); |
| if (ret) { |
| fwnode_handle_put(port); |
| fwnode_handle_put(ports); |
| return ret; |
| } |
| } |
| |
| fwnode_handle_put(ports); |
| return 0; |
| } |