Advice on MFD-style probing of DSA switch SoCs

Vladimir Oltean
2022-12-22T13:49:17Z

Hi,

Many DSA switches (drivers/net/dsa/) are in fact complex SoCs with more
embedded peripherals than just the Ethernet switching IP. For example,
I was trying to add interrupt support for the internal PHYs of the NXP
SJA1110 switch. For context, one SJA1110 switch as seen in fsl-lx2160a-bluebox3.dts
currently has bindings which look like this:

sw2: ethernet-switch@2 {
compatible = "nxp,sja1110a";
reg = <2>;
spi-max-frequency = <4000000>;
spi-cpol;
dsa,member = <0 1>;

ethernet-ports {
#address-cells = <1>;
#size-cells = <0>;

/* Microcontroller port */
port@0 {
reg = <0>;
status = "disabled";
};

sw2p1: port@1 {
reg = <1>;
link = <&sw1p4>;
phy-mode = "sgmii";

fixed-link {
speed = <1000>;
full-duplex;
};
};

port@2 {
reg = <2>;
ethernet = <&dpmac18>;
phy-mode = "rgmii-id";
rx-internal-delay-ps = <2000>;
tx-internal-delay-ps = <2000>;

fixed-link {
speed = <1000>;
full-duplex;
};
};

port@3 {
reg = <3>;
label = "1ge_p2";
phy-mode = "rgmii-id";
phy-handle = <&sw2_mii3_phy>;
};

port@4 {
reg = <4>;
label = "to_sw3";
phy-mode = "2500base-x";

fixed-link {
speed = <2500>;
full-duplex;
};
};

port@5 {
reg = <5>;
label = "trx7";
phy-mode = "internal";
phy-handle = <&sw2_port5_base_t1_phy>;
};

port@6 {
reg = <6>;
label = "trx8";
phy-mode = "internal";
phy-handle = <&sw2_port6_base_t1_phy>;
};

port@7 {
reg = <7>;
label = "trx9";
phy-mode = "internal";
phy-handle = <&sw2_port7_base_t1_phy>;
};

port@8 {
reg = <8>;
label = "trx10";
phy-mode = "internal";
phy-handle = <&sw2_port8_base_t1_phy>;
};

port@9 {
reg = <9>;
label = "trx11";
phy-mode = "internal";
phy-handle = <&sw2_port9_base_t1_phy>;
};

port@a {
reg = <10>;
label = "trx12";
phy-mode = "internal";
phy-handle = <&sw2_port10_base_t1_phy>;
};
};

mdios {
#address-cells = <1>;
#size-cells = <0>;

mdio@0 {
compatible = "nxp,sja1110-base-t1-mdio";
#address-cells = <1>;
#size-cells = <0>;
reg = <0>;

sw2_port5_base_t1_phy: ethernet-phy@1 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x1>;
};

sw2_port6_base_t1_phy: ethernet-phy@2 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x2>;
};

sw2_port7_base_t1_phy: ethernet-phy@3 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x3>;
};

sw2_port8_base_t1_phy: ethernet-phy@4 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x4>;
};

sw2_port9_base_t1_phy: ethernet-phy@5 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x5>;
};

sw2_port10_base_t1_phy: ethernet-phy@6 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x6>;
};
};
};
};

To add interrupts in a naive way, similar to how other DSA drivers have
done it, it would have to be done like this:

#include <dt-bindings/interrupt-controller/nxp-sja1110-acu-slir.h>

sw2: ethernet-switch@2 {
compatible = "nxp,sja1110a";
reg = <2>;
spi-max-frequency = <4000000>;
spi-cpol;
dsa,member = <0 1>;

slir2: interrupt-controller {
compatible = "nxp,sja1110-acu-slir";
interrupt-controller;
#interrupt-cells = <1>;
interrupt-parent = <&gpio 10>;
};

ethernet-ports {
#address-cells = <1>;
#size-cells = <0>;

/* Microcontroller port */
port@0 {
reg = <0>;
status = "disabled";
};

sw2p1: port@1 {
reg = <1>;
link = <&sw1p4>;
phy-mode = "sgmii";

fixed-link {
speed = <1000>;
full-duplex;
};
};

port@2 {
reg = <2>;
ethernet = <&dpmac18>;
phy-mode = "rgmii-id";
rx-internal-delay-ps = <2000>;
tx-internal-delay-ps = <2000>;

fixed-link {
speed = <1000>;
full-duplex;
};
};

port@3 {
reg = <3>;
label = "1ge_p2";
phy-mode = "rgmii-id";
phy-handle = <&sw2_mii3_phy>;
};

port@4 {
reg = <4>;
label = "to_sw3";
phy-mode = "2500base-x";

fixed-link {
speed = <2500>;
full-duplex;
};
};

port@5 {
reg = <5>;
label = "trx7";
phy-mode = "internal";
phy-handle = <&sw2_port5_base_t1_phy>;
};

port@6 {
reg = <6>;
label = "trx8";
phy-mode = "internal";
phy-handle = <&sw2_port6_base_t1_phy>;
};

port@7 {
reg = <7>;
label = "trx9";
phy-mode = "internal";
phy-handle = <&sw2_port7_base_t1_phy>;
};

port@8 {
reg = <8>;
label = "trx10";
phy-mode = "internal";
phy-handle = <&sw2_port8_base_t1_phy>;
};

port@9 {
reg = <9>;
label = "trx11";
phy-mode = "internal";
phy-handle = <&sw2_port9_base_t1_phy>;
};

port@a {
reg = <10>;
label = "trx12";
phy-mode = "internal";
phy-handle = <&sw2_port10_base_t1_phy>;
};
};

mdios {
#address-cells = <1>;
#size-cells = <0>;

mdio@0 {
compatible = "nxp,sja1110-base-t1-mdio";
#address-cells = <1>;
#size-cells = <0>;
reg = <0>;

sw2_port5_base_t1_phy: ethernet-phy@1 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x1>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY1>;
};

sw2_port6_base_t1_phy: ethernet-phy@2 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x2>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY2>;
};

sw2_port7_base_t1_phy: ethernet-phy@3 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x3>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY3>;
};

sw2_port8_base_t1_phy: ethernet-phy@4 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x4>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY4>;
};

sw2_port9_base_t1_phy: ethernet-phy@5 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x5>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY5>;
};

sw2_port10_base_t1_phy: ethernet-phy@6 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x6>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY6>;
};
};
};
};

However, the irq_domain/irqchip handling code in this case will go to
drivers/net/dsa/, and it won't really be a "driver" (there is no struct
device of its own). I don't really like that, the drivers/net/dsa/
folder should ideally contain only drivers for the switching IP.
It doesn't scale to find here code for GPIO, interrupts, MDIO, hwmon and
what have you; not only because the folder gets bloated with irrelevant
stuff, but also because DSA maintainers are not the best reviewers of
drivers which really belong to (and make use of infrastructure from)
other subsystems.

Not only that, but "interrupt-controller" cannot even have a unit
address with these bindings, because it's on the same level as
"ethernet-ports" which requires not having it. But there are multiple
interrupt controllers in the SJA1110 block (I could count 3). Not clear
how the other 3 would be defined in the device tree in this format.
The logical continuation of the existing bindings would be to do what
was done for the multiple MDIO controllers: an "interrupt-controllers"
container node, with children with #address-cells = <1>.

I think that doesn't scale very well either, so I was looking into
transitioning the sja1105 bindings to something similar to what Colin
Foster has done with vsc7512 (ocelot). For this switch, new-style
bindings would look like this:

soc@2 {
compatible = "nxp,sja1110-soc";
reg = <2>;
spi-max-frequency = <4000000>;
spi-cpol;
#address-cells = <1>;
#size-cells = <1>;

sw2: ethernet-switch@0 {
compatible = "nxp,sja1110a";
reg = <0x000000 0x400000>;
resets = <&sw2_rgu SJA1110_RGU_ETHSW_RST>;
dsa,member = <0 1>;

ethernet-ports {
#address-cells = <1>;
#size-cells = <0>;

/* Microcontroller port */
port@0 {
reg = <0>;
status = "disabled";
};

sw2p1: port@1 {
reg = <1>;
link = <&sw1p4>;
phy-mode = "sgmii";

fixed-link {
speed = <1000>;
full-duplex;
};
};

port@2 {
reg = <2>;
ethernet = <&dpmac18>;
phy-mode = "rgmii-id";
rx-internal-delay-ps = <2000>;
tx-internal-delay-ps = <2000>;

fixed-link {
speed = <1000>;
full-duplex;
};
};

port@3 {
reg = <3>;
label = "1ge_p2";
phy-mode = "rgmii-id";
phy-handle = <&sw2_mii3_phy>;
};

port@4 {
reg = <4>;
label = "to_sw3";
phy-mode = "2500base-x";

fixed-link {
speed = <2500>;
full-duplex;
};
};

port@5 {
reg = <5>;
label = "trx7";
phy-mode = "internal";
phy-handle = <&sw2_port5_base_t1_phy>;
};

port@6 {
reg = <6>;
label = "trx8";
phy-mode = "internal";
phy-handle = <&sw2_port6_base_t1_phy>;
};

port@7 {
reg = <7>;
label = "trx9";
phy-mode = "internal";
phy-handle = <&sw2_port7_base_t1_phy>;
};

port@8 {
reg = <8>;
label = "trx10";
phy-mode = "internal";
phy-handle = <&sw2_port8_base_t1_phy>;
};

port@9 {
reg = <9>;
label = "trx11";
phy-mode = "internal";
phy-handle = <&sw2_port9_base_t1_phy>;
};

port@a {
reg = <10>;
label = "trx12";
phy-mode = "internal";
phy-handle = <&sw2_port10_base_t1_phy>;
};
};
};

mdio@704000 {
compatible = "nxp,sja1110-base-t1-mdio";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x704000 0x1000>;

sw2_port5_base_t1_phy: ethernet-phy@1 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x1>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY1>;
};

sw2_port6_base_t1_phy: ethernet-phy@2 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x2>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY2>;
};

sw2_port7_base_t1_phy: ethernet-phy@3 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x3>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY3>;
};

sw2_port8_base_t1_phy: ethernet-phy@4 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x4>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY4>;
};

sw2_port9_base_t1_phy: ethernet-phy@5 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x5>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY5>;
};

sw2_port10_base_t1_phy: ethernet-phy@6 {
compatible = "ethernet-phy-ieee802.3-c45";
reg = <0x6>;
interrupts-extended = <&slir2 SJA1110_IRQ_CBT1_PHY6>;
};
};

mdio@709000 {
compatible = "nxp,sja1110-base-tx-mdio";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x709000 0x1000>;
};

slir2: interrupt-controller@711fe0 {
compatible = "nxp,sja1110-acu-slir";
reg = <0x711fe0 0x10>;
interrupt-controller;
#interrupt-cells = <1>;
interrupt-parent = <&gpio 10>;
};

gpio@712000 {
compatible = "nxp,sja1110-gpio";
reg = <0x712000 0x1000>;
gpio-controller;
};

slir2_gpio: interrupt-controller@712fe0 {
compatible = "nxp,sja1110-gpio-slir";
reg = <0x712fe0 0x10>;
interrupt-controller;
#interrupt-cells = <1>;
};

sw2_rgu: reset@718000 {
compatible = "nxp,sja1110-rgu";
reg = <0x718000 0x1000>;
#reset-cells = <1>;
};

sw2_cgu: clock-controller@719000 {
compatible = "nxp,sja1110-cgu";
reg = <0x719000 0x1000>;
#clock-cells = <1>;
};

slir2_pmu: interrupt-controller@71afe0 {
compatible = "nxp,sja1110-pmu-slir";
reg = <0x71afe0 0x10>;
interrupt-controller;
#interrupt-cells = <1>;
};
};

In this model, the Ethernet switch is just one of the children of the
SoC driver (the one owning the spi_device, and creating regmaps for its
children). The DSA driver doesn't have to concern itself with the other
drivers, which can live in their own folders.

It seems like mfd is a tool which could help with the driver for
"nxp,sja1110-soc", but mfd_add_devices() requires an array of struct
mfd_cell, and I don't really like that. I want a driver which just
creates IORESOURCE_REG arrays of resources for each child OF node
according to their "reg" properties, creates regmaps for each resource
using a regmap_config that I give it, associates the regmaps with the
parent using devres, and allocates/probes a platform device driver for
each of these child OF nodes. I don't want to spell out a different
mfd_cell for each driver that I'd like the SoC to probe.

It looks like of_platform_populate() would be an alternative option for
this task, but that doesn't live up to the task either. It will assume
that the addresses of the SoC children are in the CPU's address space
(IORESOURCE_MEM), and attempt to translate them. It simply doesn't have
the concept of IORESOURCE_REG. The MFD drivers which call
of_platform_populate() (simple-mfd-i2c.c) simply don't have unit
addresses for their children, and this is why address translation isn't
a problem for them.

In fact, this seems to be a rather large limitation of include/linux/of_address.h.
Even something as simple as of_address_count() will end up trying to
translate the address into the CPU memory space, so not even open-coding
the resource creation in the SoC driver is as simple as it appears.

Is there a better way than completely open-coding the parsing of the OF
addresses when turning them into IORESOURCE_REG resources (or open-coding
mfd_cells for each child)? Would there be a desire in creating a generic
set of helpers which create platform devices with IORESOURCE_REG resources,
based solely on OF addresses of children? What would be the correct
scope for these helpers?