Re: [PATCH RFC v2] net: dsa: mv88e6xxx: Support LED control
From: Andrew Lunn <andrew@lunn.ch>
Date: 2024-08-11 20:26:19
Also in:
lkml
On Sat, Aug 10, 2024 at 12:19:12AM +0200, Linus Walleij wrote:
This adds control over the hardware LEDs in the Marvell
MV88E6xxx DSA switch and enables it for MV88E6352.
This fixes an imminent problem on the Inteno XG6846 which
has a WAN LED that simply do not work with hardware
defaults: driver amendment is necessary.
The patch is modeled after Christian Marangis LED support
code for the QCA8k DSA switch, I got help with the register
definitions from Tim Harvey.
After this patch it is possible to activate hardware link
indication like this (or with a similar script):
cd /sys/class/leds/Marvell\ 88E6352:05:00:green:wan/
echo netdev > trigger
echo 1 > link
This makes the green link indicator come up on any link
speed. It is also possible to be more elaborate, like this:
cd /sys/class/leds/Marvell\ 88E6352:05:00:green:wan/
echo netdev > trigger
echo 1 > link_1000
cd /sys/class/leds/Marvell\ 88E6352:05:01:amber:wan/
echo netdev > trigger
echo 1 > link_100
Making the green LED come on for a gigabit link and the
amber LED come on for a 100 mbit link.
After the previous series rewriting the MV88E6xxx DT
bindings to use YAML a "leds" subnode is already valid
for each port, in my scratch device tree it looks like
this:
leds {
#address-cells = <1>;
#size-cells = <0>;
led@0 {
reg = <0>;
color = <LED_COLOR_ID_GREEN>;
function = LED_FUNCTION_LAN;
default-state = "off";
linux,default-trigger = "netdev";
};
led@1 {
reg = <1>;
color = <LED_COLOR_ID_AMBER>;
function = LED_FUNCTION_LAN;
default-state = "off";
};
};
This DT config is not yet configuring everything: the netdev
default trigger is assigned by the hw acceleration callbacks are
not called, and there is no way to set the netdev sub-trigger
type from the device tree, such as if you want a gigabit link
indicator. This has to be done from userspace at this point.
Signed-off-by: Linus Walleij <redacted>
---
The attempt to make LED support more generic didn't work out
as expected, so proposing to merge the very particular for now.Yes, i never found the time/energy to get my patches over the line. So lets go with this. However, i expect we have quite a bit of duplication with the qca8k, especially the parsing of DT and registering the LEDs. So it might be the third DSA driver wanting LEDs gets the job of refactoring. Please could you add something to the commit message about the SFP needs special handling. That will help reviewers understand the code.
static int mv88e6xxx_setup_port(struct mv88e6xxx_chip *chip, int port)
{
struct device_node *phy_handle = NULL;
+ struct fwnode_handle *ports_fwnode;
+ struct fwnode_handle *port_fwnode;
struct dsa_switch *ds = chip->ds;
+ struct mv88e6xxx_port *p;
struct dsa_port *dp;
int tx_amp;
int err;
u16 reg;
+ u32 val;
+
+ p = &chip->ports[port];
+ p->chip = chip;
+ p->port = port;
+
+ /* Look up corresponding fwnode if any */
+ ports_fwnode = device_get_named_child_node(chip->dev, "ethernet-ports");
+ if (!ports_fwnode)
+ ports_fwnode = device_get_named_child_node(chip->dev, "ports");
+ if (ports_fwnode) {
+ fwnode_for_each_child_node(ports_fwnode, port_fwnode) {
+ if (fwnode_property_read_u32(port_fwnode, "reg", &val))
+ continue;
+ if (val == port) {
+ p->fwnode = port_fwnode;
+ p->fiber = fwnode_property_present(port_fwnode, "sfp");
+ break;+ } + }
This can be simplified i think. struct dsa_port *dp has a member dn, which is the device tree node for the port. So p->fwnode is not needed, and you can directly do of_property_present(dp->dn, "sfp"). If you keep with the current code structure, do you need a fwnode_put() here? It seems like a common bug with *_for_each_child_node(), if you don't iterate to the end of the list, you need to do some manual cleanup.
+ } else {
+ dev_info(chip->dev,
+ "no ethernet ports node defined for the device\n");
+ }And i'm pretty sure this cannot happen, dsa_switch_parse_ports_of() will return -EINVAL. What can happen is there is no DT at all, because platform data is used. In that case, you need to silently skip LEDs.
quoted hunk ↗ jump to hunk
@@ -289,6 +292,12 @@ struct mv88e6xxx_port { struct devlink_region *region; void *pcs_private; + /* LED related information */ + bool fiber; + struct led_classdev led0; + struct led_classdev led1; + u16 ledreg; + /* MacAuth Bypass control flag */ bool mab; }; +static int mv88e6xxx_led_brightness_set(struct mv88e6xxx_port *p, int led, + int brightness) +{ + u16 reg; + + reg = p->ledreg; + + if (led == 1) + reg &= ~MV88E6XXX_PORT_LED_CONTROL_LED1_SEL_MASK; + else + reg &= ~MV88E6XXX_PORT_LED_CONTROL_LED0_SEL_MASK; + + if (brightness) { + /* Selector 0x0f == Force LED ON */ + if (led == 1) + reg |= MV88E6XXX_PORT_LED_CONTROL_LED1_SELF; + else + reg |= MV88E6XXX_PORT_LED_CONTROL_LED0_SELF; + } else { + /* Selector 0x0e == Force LED OFF */ + if (led == 1) + reg |= MV88E6XXX_PORT_LED_CONTROL_LED1_SELE; + else + reg |= MV88E6XXX_PORT_LED_CONTROL_LED0_SELE; + } + + p->ledreg = reg; + + reg |= MV88E6XXX_PORT_LED_CONTROL_UPDATE; + reg |= MV88E6XXX_PORT_LED_CONTROL_POINTER_LED01_CTRL; + + return mv88e6xxx_port_write(p->chip, p->port, MV88E6XXX_PORT_LED_CONTROL, reg);
It seems like a little helper would be useful here, something like
mv88e6xxx_port_led_write(chip, port, reg)
{
reg |= MV88E6XXX_PORT_LED_CONTROL_UPDATE;
return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_LED_CONTROL, reg);
}
You could also add a
mv88e6xxx_port_led_read(chip, port, *val)
{
int err;
err = mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_LED_CONTROL, reg);
*reg &= 0x3ff;
return err;
}
+}
+
+static int mv88e6xxx_led0_brightness_set_blocking(struct led_classdev *ldev,
+ enum led_brightness brightness)
+{
+ struct mv88e6xxx_port *p = container_of(ldev, struct mv88e6xxx_port, led0);
+ int err;
+
+ mv88e6xxx_reg_lock(p->chip);
+ err = mv88e6xxx_led_brightness_set(p, 0, brightness);
+ mv88e6xxx_reg_unlock(p->chip);
+Rather than have mv88e6xxx_led0_brightness_set_blocking() and mv88e6xxx_led1_brightness_set_blocking(), could you not do led = (port->led1 == ldev? 1 : 0); mv88e6xxx_reg_lock(p->chip); err = mv88e6xxx_led_brightness_set(p, led, brightness); mv88e6xxx_reg_unlock(p->chip);
+/* The following is a lookup table to check what rules we can support on a
+ * certain LED given restrictions such as that some rules only work with fiber
+ * (SFP) connections and some blink on activity by default.
+ */
+#define MV88E6XXX_PORTS_0_3 (BIT(0)|BIT(1)|BIT(2)|BIT(3))
+#define MV88E6XXX_PORTS_4_5 (BIT(4)|BIT(5))
+#define MV88E6XXX_PORT_4 BIT(4)
+#define MV88E6XXX_PORT_5 BIT(5)
+
+/* Entries are listed in selector order */
+static const struct mv88e6xxx_led_hwconfig mv88e6xxx_led_hwconfigs[] = {We should consider naming here. These are specific to the 6352, with its SERDES which can be connected to port 4 or port 5. Other families are going to have different tables. So this probably should be mv88e6352_led_hwconfigs, and any functions here which are specific to the 6352 should use that prefix, leaving space for other implementations for different families.
+/* Sets up the hardware blinking period */
+static int mv88e6xxx_led_set_blinking_period(struct mv88e6xxx_port *p, int led,
+ unsigned long *delay_on, unsigned long *delay_off)
+{
+ unsigned long period;
+ u16 reg;
+
+ period = *delay_on + *delay_off;
+
+ reg = 0;
+
+ switch (period) {
+ case 21:
+ reg |= MV88E6XXX_PORT_LED_CONTROL_0x06_BLINK_RATE_21MS;
+ break;
+ case 42:
+ reg |= MV88E6XXX_PORT_LED_CONTROL_0x06_BLINK_RATE_42MS;
+ break;
+ case 84:
+ reg |= MV88E6XXX_PORT_LED_CONTROL_0x06_BLINK_RATE_84MS;
+ break;
+ case 168:
+ reg |= MV88E6XXX_PORT_LED_CONTROL_0x06_BLINK_RATE_168MS;
+ break;
+ case 336:
+ reg |= MV88E6XXX_PORT_LED_CONTROL_0x06_BLINK_RATE_336MS;
+ break;
+ case 672:
+ reg |= MV88E6XXX_PORT_LED_CONTROL_0x06_BLINK_RATE_672MS;
+ break;
+ default:
+ /* Fall back to software blinking */
+ return -EINVAL;I _think_ if period is 0, i.e. not specified, the driver gets to pick values it can support. I might be remembering this wrong, so please check. This is why delay_on, an delay_off are pointers, not values....
+static int mv88e6xxx_led_blink_set(struct mv88e6xxx_port *p, int led,
+ unsigned long *delay_on, unsigned long *delay_off)
+{
+ u16 reg;
+ int err;
+
+ /* Choose a sensible default 336 ms (~3 Hz) */
+ if ((*delay_on == 0) && (*delay_off == 0)) {
+ *delay_on = 168;
+ *delay_off = 168;
+ }Ah, here it is :-)
+ + /* No off delay is just on */ + if (*delay_off == 0) + return mv88e6xxx_led_brightness_set(p, led, 1); + + err = mv88e6xxx_led_set_blinking_period(p, led, delay_on, delay_off);
Rather than pass pointers, you could pass the value, since the helper is not going to change it, and that avoids the trap i fell into while reviewing the code.
+mv88e6xxx_led_hw_control_get(struct mv88e6xxx_port *p, int led, unsigned long *rules)
+{
+ /* The hardware register cannot be read: no initial state determined */
+ return -EINVAL;
+}That is not what i expected. For the Marvell PHY driver, i walked the table to find a match, and returned -EOPNOTSUPP if there was no match. Can this be done here?
+int mv88e6xxx_port_setup_leds(struct mv88e6xxx_chip *chip, int port)
+{
+ struct fwnode_handle *led = NULL, *leds = NULL;
+ struct led_init_data init_data = { };
+ unsigned long delay_off = 168;
+ unsigned long delay_on = 168;
+ enum led_default_state state;
+ struct mv88e6xxx_port *p;
+ struct led_classdev *l;
+ struct device *dev;
+ u32 led_num;
+ int ret;
+
+ /* LEDs are on ports 1,2,3,4, 5 and 6 (index 0..5), no more */
+ if (port > 5)
+ return -EOPNOTSUPP;
+
+ p = &chip->ports[port];
+ if (!p->fwnode)
+ return 0;
+ p->ledreg = 0;
+
+ dev = chip->dev;
+
+ leds = fwnode_get_named_child_node(p->fwnode, "leds");
+ if (!leds) {
+ dev_info(dev, "No Leds node specified in device tree for port %d!\n",
+ port);
+ return 0;
+ }dev_dbg()? LEDs are optional, so we should not spam the log if they are not found.
+
+ fwnode_for_each_child_node(leds, led) {
+ /* Reg represent the led number of the port, max 2
+ * LEDs can be connected to each port, in some designs
+ * only one LED is connected.
+ */
+ if (fwnode_property_read_u32(led, "reg", &led_num))
+ continue;
+ if (led_num > 1) {
+ dev_err(dev, "invalid LED specified port %d\n", port);That is a real error, the DT is broken. So i would return -EINVAL.
+ continue;
+ }
+
+ if (led_num == 0)
+ l = &p->led0;
+ else
+ l = &p->led1;
+
+ state = led_init_default_state_get(led);
+ switch (state) {
+ case LEDS_DEFSTATE_ON:
+ l->brightness = 1;
+ mv88e6xxx_led_brightness_set(p, led_num, 1);
+ break;
+ case LEDS_DEFSTATE_KEEP:
+ break;
+ default:
+ l->brightness = 0;
+ mv88e6xxx_led_brightness_set(p, led_num, 0);
+ }
+
+ /* Default blinking period for LEDs */
+ mv88e6xxx_led_set_blinking_period(p, led_num, &delay_on, &delay_off);
+
+ l->max_brightness = 1;
+ if (led_num == 0) {
+ l->brightness_set_blocking = mv88e6xxx_led0_brightness_set_blocking;
+ l->blink_set = mv88e6xxx_led0_blink_set;
+ l->hw_control_is_supported = mv88e6xxx_led0_hw_control_is_supported;
+ l->hw_control_set = mv88e6xxx_led0_hw_control_set;
+ l->hw_control_get = mv88e6xxx_led0_hw_control_get;
+ l->hw_control_get_device = mv88e6xxx_led0_hw_control_get_device;
+ } else {
+ l->brightness_set_blocking = mv88e6xxx_led1_brightness_set_blocking;
+ l->blink_set = mv88e6xxx_led1_blink_set;
+ l->hw_control_is_supported = mv88e6xxx_led1_hw_control_is_supported;
+ l->hw_control_set = mv88e6xxx_led1_hw_control_set;
+ l->hw_control_get = mv88e6xxx_led1_hw_control_get;
+ l->hw_control_get_device = mv88e6xxx_led1_hw_control_get_device;
+ }
+ l->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:0%d", chip->info->name,
+ port, led_num);
+ if (!init_data.devicename)
+ return -ENOMEM;
+
+ ret = devm_led_classdev_register_ext(dev, l, &init_data);
+ if (ret)
+ dev_err(dev, "Failed to init LED %d for port %d", led_num, port);I would also return this error. If we got this far, we don't expect an error.
quoted hunk ↗ jump to hunk
+ + kfree(init_data.devicename); + } + + return 0; +}diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c index 5394a8cf7bf1..d72bba1969f7 100644 --- a/drivers/net/dsa/mv88e6xxx/port.c +++ b/drivers/net/dsa/mv88e6xxx/port.c@@ -12,6 +12,7 @@ #include <linux/if_bridge.h> #include <linux/phy.h> #include <linux/phylink.h> +#include <linux/property.h> #include "chip.h" #include "global2.h"
Since this is the complete change to this file, is the new include
really needed?
Andrew
---
pw-bot: cr