* [RFC PATCH net-next v2 0/2] net: pse-pd: Add LTC4266 PSE controller driver
@ 2026-08-20 14:25 Kyle Swenson
2026-08-20 14:25 ` [RFC PATCH net-next v2 1/2] dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller Kyle Swenson
2026-08-20 14:25 ` [RFC PATCH net-next v2 2/2] net: pse-pd: Add LTC4266 PSE controller driver Kyle Swenson
0 siblings, 2 replies; 6+ messages in thread
From: Kyle Swenson @ 2026-08-20 14:25 UTC (permalink / raw)
To: o.rempel@pengutronix.de, kory.maincent@bootlin.com,
andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
kuba@kernel.org, pabeni@redhat.com, robh@kernel.org,
krzk+dt@kernel.org, conor+dt@kernel.org
Cc: Kyle Swenson, netdev@vger.kernel.org, devicetree@vger.kernel.org,
linux-kernel@vger.kernel.org, Roland Kovács,
David Nyström
This RFC series intends to continue discussion around the support for
the LTC4266, an older PSE controller that supports powering Type 1 and
Type 2 PDs.
This chip has four individually controllable ports, each with its own
detection, classification and current-limiting abilities. The driver
declares the static power budgeting strategy.
The LTC4266 only enforces a current limit (I_CUT and I_LIM), not a power
limit, so an admin power limit has to be turned into a current limit.
There are two problems that fall out of this I don't feel like I've got
a great solution to either and would welcome feedback, hence the 2nd RFC
version.
The first problem is programming the current limit from the
admin-specified power limit for power limits other than a specific PD
class. For this, I replaced the linear regression in the first RFC with
the minimum port voltage for a Type 2 PSE (50V, from Table 33-11 in the
IEEE 802.3 spec) to convert power to current.
The second problem is that when the admin does something like
ethtool --set-pse eth1 c33-pse-avail-pw-limit 10000
the command fails with:
netlink error: Can't calculate the current, PSE voltage read is 0
netlink error: Result not representable
unless the PSE port has been enabled first, and the PSE has detected and
classified a PD on that port. I'm hopeful I'm just missing something I
can do differently in this driver, but if not I'm open to adjusting
things at other layers to support this.
---
RFC v1 -> RFC v2:
- Rebase on net-next/main
- Integrate and test with the PSE core's PSE_BUDGET_EVAL_STATIC.
- ltc4266: Convert register access to the regmap API (replaces the
open-coded read-modify-write helper and its explicit mutex). Note
the explicit mutex is gone because it only protected reading both the
high and low bytes of a port's voltage/current. This isn't actually
needed because reading the LSB will latch the MSB until the MSB is
read, so I2C transactions between the two reads are benign (Oleksij)
- ltc4266: Rename LTC4266_ICUT_RSENSE to LTC4266_ICUT_RSENSE_025_OHM
(Oleksij)
- ltc4266: Rename the ILIM AF/AT constants to TYPE1/TYPE2 (IEEE 802.3
terminology) (Oleksij)
- ltc4266: Add kernel-doc to ltc4266_port_set_ilim() citing IEEE Std
802.3-2022 Table 33-11 / Eq. (33-6) and the datasheet Table 5
sense-resistor dependence (Oleksij)
- ltc4266: Use the standard "2-event classification" term (IEEE
802.3-2022, Clause 33) instead of the datasheet's "Ping-Pong" name
(Oleksij)
- ltc4266: Document the ltc4266_port_mode enum
(SHUTDOWN/MANUAL/SEMI/AUTO). The suggestion to treat any OPMD other
than SHUTDOWN as ADMIN_STATE_ENABLED didn't really apply since the
PSE core added the PSE_BUDGET_EVAL_STRAT_STATIC (Oleksij)
- ltc4266: Return 0 from ltc4266_read_iv() rather than -EINVAL when the
port is not delivering power, so an "ethtool --show-pse" query is not
aborted, and clarify the "delivering power" wording (Oleksij)
- ltc4266: Replace the I_CUT (18750, 0x3F, 850) and current/voltage
(122070, 61035, 5835) magic numbers with named defines (Oleksij)
- dt-bindings: Drop the blank line after "channels:" (Krzysztof)
- dt-bindings: Move the channels "description" after
"additionalProperties" so the structural keywords stay together
(Krzysztof)
- dt-bindings: Drop the blank line before "type: object" (Krzysztof)
- dt-bindings: Collapse the double blank line before
"patternProperties" to one (Krzysztof)
- dt-bindings: Use single quotes consistently for
'#address-cells'/'#size-cells' (Krzysztof)
- dt-bindings: Move "unevaluatedProperties: false" after the top-level
"required" block (Krzysztof)
- dt-bindings: Drop 'status = "okay";' from the example and tidy the
surrounding blank lines, including separating the channels and
pse-pis nodes (Krzysztof)
- dt-bindings: Reword the channels description based on the review in
[1] (Kory)
[1] https://lore.kernel.org/netdev/20250517003525.2f6a5005@kmaincent-XPS-13-7390/
RFC v1: https://lore.kernel.org/netdev/20250603230422.2553046-1-kyle.swenson@est.tech/
Kyle Swenson (2):
dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller
net: pse-pd: Add LTC4266 PSE controller driver
.../bindings/net/pse-pd/lltc,ltc4266.yaml | 180 +++
drivers/net/pse-pd/Kconfig | 11 +
drivers/net/pse-pd/Makefile | 1 +
drivers/net/pse-pd/ltc4266.c | 1305 +++++++++++++++++
4 files changed, 1497 insertions(+)
create mode 100644 Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml
create mode 100644 drivers/net/pse-pd/ltc4266.c
--
2.55.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [RFC PATCH net-next v2 1/2] dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller
2026-08-20 14:25 [RFC PATCH net-next v2 0/2] net: pse-pd: Add LTC4266 PSE controller driver Kyle Swenson
@ 2026-08-20 14:25 ` Kyle Swenson
2026-08-21 14:25 ` sashiko-bot
2026-08-24 10:46 ` Kory Maincent
2026-08-20 14:25 ` [RFC PATCH net-next v2 2/2] net: pse-pd: Add LTC4266 PSE controller driver Kyle Swenson
1 sibling, 2 replies; 6+ messages in thread
From: Kyle Swenson @ 2026-08-20 14:25 UTC (permalink / raw)
To: o.rempel@pengutronix.de, kory.maincent@bootlin.com,
andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
kuba@kernel.org, pabeni@redhat.com, robh@kernel.org,
krzk+dt@kernel.org, conor+dt@kernel.org
Cc: Kyle Swenson, netdev@vger.kernel.org, devicetree@vger.kernel.org,
linux-kernel@vger.kernel.org, Roland Kovács,
David Nyström
Add the LTC4266 Power Sourcing Equipment controller device tree bindings
documentation.
Signed-off-by: Kyle Swenson <kyle.swenson@est.tech>
---
.../bindings/net/pse-pd/lltc,ltc4266.yaml | 180 ++++++++++++++++++
1 file changed, 180 insertions(+)
create mode 100644 Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml
diff --git a/Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml b/Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml
new file mode 100644
index 000000000000..b3da8fea0b07
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml
@@ -0,0 +1,180 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/pse-pd/lltc,ltc4266.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Linear Technology LTC4266 Power Sourcing Equipment controller
+
+maintainers:
+ - Kyle Swenson <kyle.swenson@est.tech>
+
+allOf:
+ - $ref: pse-controller.yaml#
+
+properties:
+ compatible:
+ enum:
+ - lltc,ltc4266
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ reset-gpios:
+ maxItems: 1
+
+ channels:
+ type: object
+ additionalProperties: false
+ description:
+ Defines the 4 physical delivery channels on the controller that can be
+ referenced by PSE PIs through their "pairsets" property. The actual port
+ matrix mapping is created when PSE PIs reference these channels in their
+ pairsets.
+
+ properties:
+ '#address-cells':
+ const: 1
+
+ '#size-cells':
+ const: 0
+
+ patternProperties:
+ '^channel@[0-3]$':
+ type: object
+ additionalProperties: false
+
+ properties:
+ reg:
+ maxItems: 1
+
+ sense-resistor-micro-ohms:
+ description: Sense resistor connected to the channel's MOSFET, used
+ for current measurement for overcurrent detection.
+ enum: [250000, 500000]
+
+ required:
+ - reg
+ - sense-resistor-micro-ohms
+
+ required:
+ - '#address-cells'
+ - '#size-cells'
+
+ pse-pis:
+ type: object
+ additionalProperties: false
+
+ properties:
+ '#address-cells':
+ const: 1
+
+ '#size-cells':
+ const: 0
+
+ patternProperties:
+ '^pse-pi@[0-3]$':
+ type: object
+ properties:
+ pairsets:
+ description: The LTC4266 delivers power to a PI over a single
+ pairset, driven by one of the controller's four channels. There
+ is no 4-pair mode spreading a PI over two channels, so exactly
+ one channel phandle is expected.
+ maxItems: 1
+ pairset-names:
+ maxItems: 1
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - channels
+ - pse-pis
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ ethernet-pse@2f {
+ compatible = "lltc,ltc4266";
+ reg = <0x2f>;
+ interrupts = <8 IRQ_TYPE_LEVEL_LOW>;
+ interrupt-parent = <&gpio>;
+
+ channels {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ phys0: channel@0 {
+ reg = <0>;
+ sense-resistor-micro-ohms = <500000>;
+ };
+
+ phys1: channel@1 {
+ reg = <1>;
+ sense-resistor-micro-ohms = <500000>;
+ };
+
+ phys2: channel@2 {
+ reg = <2>;
+ sense-resistor-micro-ohms = <500000>;
+ };
+
+ phys3: channel@3 {
+ reg = <3>;
+ sense-resistor-micro-ohms = <500000>;
+ };
+ };
+
+ pse-pis {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ pse_pi0: pse-pi@0 {
+ reg = <0>;
+ #pse-cells = <0>;
+ pairset-names = "alternative-a";
+ pairsets = <&phys0>;
+ polarity-supported = "MDI";
+ vpwr-supply = <&vreg_pse>;
+ };
+
+ pse_pi1: pse-pi@1 {
+ reg = <1>;
+ #pse-cells = <0>;
+ pairset-names = "alternative-a";
+ pairsets = <&phys1>;
+ polarity-supported = "MDI";
+ vpwr-supply = <&vreg_pse>;
+ };
+
+ pse_pi2: pse-pi@2 {
+ reg = <2>;
+ #pse-cells = <0>;
+ pairset-names = "alternative-a";
+ pairsets = <&phys2>;
+ polarity-supported = "MDI";
+ vpwr-supply = <&vreg_pse>;
+ };
+
+ pse_pi3: pse-pi@3 {
+ reg = <3>;
+ #pse-cells = <0>;
+ pairset-names = "alternative-a";
+ pairsets = <&phys3>;
+ polarity-supported = "MDI";
+ vpwr-supply = <&vreg_pse>;
+ };
+ };
+ };
+ };
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [RFC PATCH net-next v2 2/2] net: pse-pd: Add LTC4266 PSE controller driver
2026-08-20 14:25 [RFC PATCH net-next v2 0/2] net: pse-pd: Add LTC4266 PSE controller driver Kyle Swenson
2026-08-20 14:25 ` [RFC PATCH net-next v2 1/2] dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller Kyle Swenson
@ 2026-08-20 14:25 ` Kyle Swenson
2026-08-21 14:25 ` sashiko-bot
1 sibling, 1 reply; 6+ messages in thread
From: Kyle Swenson @ 2026-08-20 14:25 UTC (permalink / raw)
To: o.rempel@pengutronix.de, kory.maincent@bootlin.com,
andrew+netdev@lunn.ch, davem@davemloft.net, edumazet@google.com,
kuba@kernel.org, pabeni@redhat.com, robh@kernel.org,
krzk+dt@kernel.org, conor+dt@kernel.org
Cc: Kyle Swenson, netdev@vger.kernel.org, devicetree@vger.kernel.org,
linux-kernel@vger.kernel.org, Roland Kovács,
David Nyström
Add a new driver for the Linear Technology LTC4266 I2C Power Sourcing
Equipment controller and integrate with the PSE controller core. The
LTC4266 supports Type 1 and Type 2 Powered Devices.
Signed-off-by: Kyle Swenson <kyle.swenson@est.tech>
---
drivers/net/pse-pd/Kconfig | 11 +
drivers/net/pse-pd/Makefile | 1 +
drivers/net/pse-pd/ltc4266.c | 1305 ++++++++++++++++++++++++++++++++++
3 files changed, 1317 insertions(+)
create mode 100644 drivers/net/pse-pd/ltc4266.c
diff --git a/drivers/net/pse-pd/Kconfig b/drivers/net/pse-pd/Kconfig
index a0f2ae668c67..844c1b237df7 100644
--- a/drivers/net/pse-pd/Kconfig
+++ b/drivers/net/pse-pd/Kconfig
@@ -46,10 +46,21 @@ config PSE_REGULATOR
help
This module provides support for simple regulator based Ethernet Power
Sourcing Equipment without automatic classification support. For
example for basic implementation of PoDL (802.3bu) specification.
+config PSE_LTC4266
+ tristate "LTC4266 PSE controller"
+ depends on I2C
+ select REGMAP_I2C
+ help
+ This module provides support for LTC4266 regulator based Ethernet
+ Power Sourcing Equipment.
+
+ To compile this driver as a module, choose M here: the
+ module will be called ltc4266.
+
config PSE_PD692X0
tristate "PD692X0 PSE controller"
depends on I2C
select FW_LOADER
select FW_UPLOAD
diff --git a/drivers/net/pse-pd/Makefile b/drivers/net/pse-pd/Makefile
index 9cca5900fe34..c3fe4efcf1de 100644
--- a/drivers/net/pse-pd/Makefile
+++ b/drivers/net/pse-pd/Makefile
@@ -1,10 +1,11 @@
# SPDX-License-Identifier: GPL-2.0-only
# Makefile for Linux PSE drivers
obj-$(CONFIG_PSE_CONTROLLER) += pse_core.o
+obj-$(CONFIG_PSE_LTC4266) += ltc4266.o
obj-$(CONFIG_PSE_REALTEK_MCU) += realtek-pse-mcu-core.o
obj-$(CONFIG_PSE_REALTEK_MCU_I2C) += realtek-pse-mcu-i2c.o
obj-$(CONFIG_PSE_REALTEK_MCU_UART) += realtek-pse-mcu-uart.o
obj-$(CONFIG_PSE_REGULATOR) += pse_regulator.o
obj-$(CONFIG_PSE_PD692X0) += pd692x0.o
diff --git a/drivers/net/pse-pd/ltc4266.c b/drivers/net/pse-pd/ltc4266.c
new file mode 100644
index 000000000000..2f9c37ebf1d2
--- /dev/null
+++ b/drivers/net/pse-pd/ltc4266.c
@@ -0,0 +1,1305 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Driver for Linear LTC4266 PoE PSE Controller
+ *
+ * Original work:
+ * Copyright 2019 Cradlepoint Technology, Inc.
+ * Cradlepoint Technology, Inc. <source@cradlepoint.com>
+ *
+ * Re-written in 2026:
+ * Copyright 2026 Ericsson Software Technology
+ * Kyle Swenson <kyle.swenson@est.tech>
+ *
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/ethtool.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/kernel.h>
+#include <linux/math.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pse-pd/pse.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+
+#define LTC4266_REG_ID 0x1B
+#define LTC4266_ID 0x64
+
+#define TWO_BIT_WORD_OFFSET(_v, _pid) ((_v) << ((_pid) * 2))
+#define TWO_BIT_WORD_MASK(_pid) TWO_BIT_WORD_OFFSET(0x03, (_pid))
+
+#define LTC4266_IPLSB_REG(_p) (0x30 | ((_p) << 2))
+#define LTC4266_VPLSB_REG(_p) (LTC4266_IPLSB_REG(_p) + 2)
+
+/* Current-sense scaling, in nA per LSB, selected by the sense resistor.
+ * ltc4266_read_iv() divides by 1000 to return microamps.
+ */
+#define LTC4266_IP_NA_PER_LSB_RSENSE_025 122070 /* 122.07 uA/LSB */
+#define LTC4266_IP_NA_PER_LSB_RSENSE_050 61035 /* 61.035 uA/LSB */
+
+/* Voltage-sense scaling: 5.835 mV == 5835 uV per LSB. */
+#define LTC4266_VP_UV_PER_LSB 5835
+
+#define LTC4266_RSTPB_INTCLR BIT(7)
+#define LTC4266_RSTPB_PINCLR BIT(6)
+#define LTC4266_RSTPB_RSTALL BIT(4)
+
+/* Register definitions */
+#define LTC4266_REG_INTSTAT 0x00
+#define LTC4266_REG_INTMASK 0x01
+#define LTC4266_REG_DETEVN_COR 0x05
+#define LTC4266_REG_FLTEVN_COR 0x07
+#define LTC4266_REG_TSEVN_COR 0x09
+#define LTC4266_REG_SUPEVN_COR 0x0B
+#define LTC4266_REG_STAT(n) (0x0C + (n))
+#define LTC4266_REG_STATPWR 0x10
+#define LTC4266_REG_OPMD 0x12
+#define LTC4266_REG_DISENA 0x13 /* Disconnect detect enable */
+#define LTC4266_REG_MCONF 0x17
+#define LTC4266_REG_DETPB 0x18
+#define LTC4266_REG_PWRPB 0x19
+#define LTC4266_REG_RSTPB 0x1A
+#define LTC4266_REG_HPEN 0x44
+#define LTC4266_REG_HPMD(_p) (0x46 + (5 * (_p)))
+#define LTC4266_REG_ICUT_HP(_p) (LTC4266_REG_HPMD(_p) + 1)
+#define LTC4266_REG_ILIM(_p) (LTC4266_REG_HPMD(_p) + 2)
+#define LTC4266_REG_TLIM12 0x1E
+#define LTC4266_REG_TLIM34 0x1F
+
+/* Register field definitions */
+#define LTC4266_HPMD_PONGEN 0x01
+
+/* For LTC4266_REG_TLIM* */
+#define LTC4266_TLIM_VALUE 0x01
+
+/* LTC4266_REG_HPEN, enable "High Power" mode (Type 2, Class 4) */
+#define LTC4266_HPEN(_p) BIT(_p)
+
+/* LTC4266_REG_MCONF */
+#define LTC4266_MCONF_INTERRUPT_ENABLE BIT(7)
+/* Only report a detect event when the result changes, not every cycle */
+#define LTC4266_MCONF_DETCHG BIT(6)
+
+/* LTC4266_REG_DETPB */
+#define LTC4266_DETPB_CLASS_ENABLE(_p) BIT((_p) + 4)
+#define LTC4266_DETPB_DETECT_ENABLE(_p) BIT((_p))
+
+/* LTC4266_REG_STATPWR */
+#define LTC4266_STATPWR_PG(_p) BIT((_p) + 4)
+#define LTC4266_STATPWR_PE(_p) BIT(_p)
+
+/* statp<n> (0Ch-0Fh) detection result, and the "Signature Good" value */
+#define LTC4266_PORT_CLASS(_stat) FIELD_GET(GENMASK(6, 4), (_stat))
+#define LTC4266_PORT_DETECT(_stat) FIELD_GET(GENMASK(2, 0), (_stat))
+#define LTC4266_DETECT_GOOD 0x4
+
+/* if R_sense = 0.25 Ohm, this should be set otherwise 0 */
+#define LTC4266_ICUT_RSENSE_025_OHM BIT(7)
+
+/* if set, halve the range and double the precision */
+#define LTC4266_ICUT_RANGE BIT(6)
+
+/* I_CUT is programmed in a 6-bit field; each step is 18.75 mA (18750 uA). */
+#define LTC4266_ICUT_STEP_UA 18750
+#define LTC4266_ICUT_MASK GENMASK(5, 0)
+
+/* Cap I_CUT at the suggested value for a Type 2 PD at 638mA */
+#define LTC4266_ICUT_MAX_MA 638
+#define LTC4266_ICUT_MAX_STEPS 34
+
+/* In an effort to convert the PSE core's power limit to a current limit, we'll
+ * use 50V for the PSE port voltage (the minimum for a Type 2 PSE)
+ */
+
+#define LTC4266_VPORT_MIN_MV 50000
+
+/* Recommended lim<n> settings from datasheet Table 5.
+ *
+ * I_LIM (mA) RSENSE = 0.5 Ohm RSENSE = 0.25 Ohm
+ * 425 (Type 1) 0x00 0x80
+ * 850 (Type 2) 0x40 0xC0
+ */
+#define LTC4266_ILIM_TYPE1_RSENSE_050 0x00
+#define LTC4266_ILIM_TYPE1_RSENSE_025 0x80
+#define LTC4266_ILIM_TYPE2_RSENSE_050 0x40
+#define LTC4266_ILIM_TYPE2_RSENSE_025 0xC0
+
+/* LTC4266_REG_INTSTAT and LTC4266_REG_INTMASK */
+#define LTC4266_INT_SUPPLY BIT(7)
+#define LTC4266_INT_TSTART BIT(6)
+#define LTC4266_INT_TCUT BIT(5)
+#define LTC4266_INT_CLASS BIT(4)
+#define LTC4266_INT_DETECT BIT(3)
+#define LTC4266_INT_DIS BIT(2)
+
+/* Per-port event bits within the CoR event registers. Every per-port event
+ * register splits its 8 bits into a per-port low nibble and a per-port high
+ * nibble:
+ * detevn (05h): LO = detection complete, HI = classification complete
+ * fltevn (07h): LO = tCUT overcurrent, HI = tDIS DC disconnect
+ * tsevn (09h): LO = tSTART overcurrent, HI = tLIM current-limit timeout
+ */
+#define LTC4266_EVN_LO(_p) BIT(_p) /* ports 0-3 */
+#define LTC4266_EVN_HI(_p) BIT((_p) + 4) /* ports 0-3 */
+
+/* supevn (0Bh) */
+#define LTC4266_SUPEVN_OVERTEMP BIT(7)
+
+#define LTC4266_MAX_PORTS 4
+#define LTC4266_PW_LIMIT_MAX 30000
+#define LTC4266_PW_LIMIT_MIN 1000
+
+enum {
+ READ_CURRENT = 0,
+ READ_VOLTAGE = 2
+};
+
+/* LTC4266 Port Operating modes
+ *
+ * LTC4266_OPMD_SHUTDOWN: Port is completely off, will not run any
+ * classification or detection cycles
+ *
+ * LTC4266_OPMD_MANUAL: Port is "Manual" control mode, meaning it is
+ * possible to apply power to the port without a valid detection/classification
+ * result. This is dangerous and can easily violate the IEEE Specifications, and
+ * is not supported by this driver.
+ *
+ * LTC4266_OPMD_SEMI: Semi-auto mode, meaning that a port will detect and
+ * classify devices, but will not power the device until the host (us)
+ * instructs it to do so. This is the only mode this driver supports.
+ *
+ * LTC4266_OPMD_AUTO: Fully automatic mode. Requires the chip to be powered
+ * on with the AUTO pin high (which will set this mode for all ports) as well
+ * as an appropriately sized power supply.
+ */
+enum ltc4266_port_mode {
+ LTC4266_OPMD_SHUTDOWN = 0,
+ LTC4266_OPMD_MANUAL,
+ LTC4266_OPMD_SEMI,
+ LTC4266_OPMD_AUTO
+};
+
+/* Map LTC4266 Classification result to PD class. Note for a PD that has a
+ * valid detect signature, but doesn't produce a classification signature is
+ * still a valid PD. The LTC4266 indicates this with 0x06 in the statp<n>
+ * register and calls it "Class 0". This is a different state than when
+ * statp<n> indicates 0, which means classification isn't complete. This maps
+ * the result to either an errno or classification value suitable for use up
+ * the stack.
+ */
+static const int ltc4266_class_map[] = {
+ -EAGAIN, /* Classification is incomplete */
+ 1,
+ 2,
+ 3,
+ 4,
+ -EINVAL,
+ 0,
+ -ERANGE
+};
+
+/* Map a PD Class to I_CUT thresholds from the LTC4266 datasheet Table 2 */
+static const int ltc4266_class_to_icut[] = {
+ 375,
+ 112,
+ 206,
+ 375,
+ 638
+};
+
+/* Maximum power per IEEE 802.3 class in mW, indexed by class (0-4).
+ * Classes 0-3 are from Table 33-7 in the IEEE802.3 standard, and the
+ * class 4 value is determined from Table 33-1 using a Type 2 voltage of 50V.
+ */
+static const int ltc4266_class_pw[] = {
+ 15400, /* Class 0 */
+ 4000, /* Class 1 */
+ 7000, /* Class 2 */
+ 15400, /* Class 3 */
+ 30000, /* Class 4 (Type 2) */
+};
+
+enum sense_resistor {
+ LTC4266_RSENSE_500, /* Rsense 0.5 Ohm */
+ LTC4266_RSENSE_250 /* Rsense 0.25 Ohm */
+};
+
+struct ltc4266;
+
+/**
+ * struct ltc4266_port - per-PSE-PI context
+ *
+ * @ltc4266: the controller owning this port
+ * @chan: index of the LTC4266 delivery channel backing this PI, established
+ * from the PI pairset phandle by ltc4266_map_pis(). All register
+ * addressing uses this member.
+ * @rsense: sense resistor on @chan, used to scale current readings and
+ * to pick the I_CUT and I_LIM encodings.
+ * @pw_limit: Admin-configured power limit in mW.
+ */
+struct ltc4266_port {
+ struct ltc4266 *ltc4266;
+ u8 chan;
+ enum sense_resistor rsense;
+ int pw_limit;
+};
+
+/**
+ * struct ltc4266 - LTC4266 controller context
+ *
+ * @client: the I2C client
+ * @regmap: register map of @client
+ * @ports: table of PSE PI contexts, indexed by PSE PI id. A NULL entry is a PI
+ * that is not described in the device tree and therefore has no
+ * channel mapped.
+ * @dev: the underlying device
+ * @np: device node of @dev
+ * @pcdev: the PSE controller registered with the PSE core
+ */
+struct ltc4266 {
+ struct i2c_client *client;
+ struct regmap *regmap;
+ struct ltc4266_port *ports[LTC4266_MAX_PORTS];
+ struct device *dev;
+ struct device_node *np;
+ struct pse_controller_dev pcdev;
+};
+
+static struct ltc4266_port *ltc4266_pi_port(struct pse_controller_dev *pcdev,
+ int id)
+{
+ struct ltc4266 *ltc4266 = container_of(pcdev, struct ltc4266, pcdev);
+
+ return ltc4266->ports[id];
+}
+
+static int ltc4266_read_iv(struct ltc4266_port *port, u8 iv)
+{
+ struct ltc4266 *ltc4266 = port->ltc4266;
+ unsigned int lsb, msb;
+ unsigned int statpwr;
+ int lsb_reg;
+ int result;
+ u64 ivbits;
+
+ if (iv == READ_CURRENT)
+ lsb_reg = LTC4266_IPLSB_REG(port->chan);
+ else if (iv == READ_VOLTAGE)
+ lsb_reg = LTC4266_VPLSB_REG(port->chan);
+ else
+ return -EINVAL;
+
+ result = regmap_read(ltc4266->regmap, LTC4266_REG_STATPWR, &statpwr);
+ if (result < 0)
+ return result;
+
+ /* LTC4266 IV readings are only meaningful while the port is delivering
+ * power. When the PG (power good) bit is not set, the port is
+ * delivering nothing, so report 0 rather than an error. When this is
+ * reached from the PSE core (via pi_get_actual_pw), returning errno
+ * here would abort pse_ethtool_get_status() and fail the whole
+ * "ethtool --show-pse" query for an otherwise perfectly readable port.
+ * However, when this is reached from the regulator ops, 0 is turned
+ * into an ERANGE, causing "ethtool --set-pse eth1 c33-pse-avail-pw-limit"
+ * to return ERANGE back to the user unless the port is actually delivering power.
+ */
+ if (!(statpwr & LTC4266_STATPWR_PG(port->chan)))
+ return 0;
+
+ result = regmap_read(ltc4266->regmap, lsb_reg, &lsb);
+ if (result < 0)
+ return result;
+
+ result = regmap_read(ltc4266->regmap, lsb_reg + 1, &msb);
+ if (result < 0)
+ return result;
+
+ ivbits = (msb << 8) | lsb;
+
+ if (iv == READ_CURRENT)
+ if (port->rsense == LTC4266_RSENSE_250)
+ result = DIV_ROUND_CLOSEST_ULL(ivbits * LTC4266_IP_NA_PER_LSB_RSENSE_025,
+ 1000);
+ else
+ result = DIV_ROUND_CLOSEST_ULL(ivbits * LTC4266_IP_NA_PER_LSB_RSENSE_050,
+ 1000);
+ else
+ result = ivbits * LTC4266_VP_UV_PER_LSB;
+
+ return result;
+}
+
+/**
+ * ltc4266_port_set_ilim - Set the active current limit (ILIM) for a port
+ * @port: the port to configure
+ * @class: the detected PD class (0-4)
+ *
+ * This function configures the ILIM register of the LTC4266. The ILIM value
+ * determines the threshold at which the PSE actively limits current to the PD.
+ * The chosen values are based on IEEE Std 802.3-2022 requirements and typical
+ * operational values for the LTC4266 controller.
+ *
+ * IEEE Std 802.3-2022, Table 33-11 specifies ILIM parameter ranges:
+ * - For Type 1 PSE operation (typically PD Classes 0-3):
+ * The minimum ILIM is 0.400A. This driver uses 425mA. This value fits
+ * within typical Type 1 ILIM specifications (e.g., 0.400A min to
+ * around 0.440A-0.500A max for the programmed steady-state limit).
+ *
+ * - For Type 2 PSE operation (typically PD Class 4):
+ * The minimum ILIM is 1.14 * ICable (or ~1.05 * IPort_max from other
+ * interpretations, e.g., ~0.630A to ~0.684A). This driver uses 850mA.
+ * This value meets the minimum requirement and is a supported operational
+ * current limit for high power modes in the LTC4266.
+ *
+ * The overall PSE current output must not exceed the time-dependent PSE
+ * upperbound template, IPSEUT(t), described in IEEE Std 802.3-2022,
+ * Equation (33-6). The programmed ILIM values (425mA/850mA) serve as the
+ * long-term current limit (Ilimmin segment of IPSEUT(t)) and are well
+ * within the higher short-term current allowances of that template
+ * (e.g., 1.75A).
+ *
+ * The specific register values written depend on the sense resistor
+ * (0.25 Ohm or 0.50 Ohm) as detailed in the LTC4266 datasheet (Table 5).
+ *
+ * Returns: 0 on success or a negative errno.
+ */
+static int ltc4266_port_set_ilim(struct ltc4266_port *port, int class)
+{
+ bool rsense_250 = port->rsense == LTC4266_RSENSE_250;
+ u8 ilim;
+
+ if (class > 4 || class < 0)
+ return -EINVAL;
+
+ if (class < 4)
+ ilim = rsense_250 ? LTC4266_ILIM_TYPE1_RSENSE_025 :
+ LTC4266_ILIM_TYPE1_RSENSE_050;
+ else
+ ilim = rsense_250 ? LTC4266_ILIM_TYPE2_RSENSE_025 :
+ LTC4266_ILIM_TYPE2_RSENSE_050;
+
+ return regmap_write(port->ltc4266->regmap,
+ LTC4266_REG_ILIM(port->chan), ilim);
+}
+
+static int ltc4266_port_set_icut(struct ltc4266_port *port, int icut)
+{
+ u8 val;
+
+ if (icut > LTC4266_ICUT_MAX_MA)
+ return -ERANGE;
+
+ val = min(DIV_ROUND_UP(icut * 1000, LTC4266_ICUT_STEP_UA),
+ LTC4266_ICUT_MAX_STEPS) & LTC4266_ICUT_MASK;
+
+ if (port->rsense == LTC4266_RSENSE_250)
+ val |= LTC4266_ICUT_RSENSE_025_OHM | LTC4266_ICUT_RANGE;
+
+ return regmap_write(port->ltc4266->regmap,
+ LTC4266_REG_ICUT_HP(port->chan), val);
+}
+
+/**
+ * ltc4266_pw_limit_to_icut - Convert an admin power limit to an I_CUT threshold
+ * @max_mw: the power limit in mW
+ * @class: the detected PD class (0-4)
+ *
+ * The LTC4266 only enforces a current threshold, so a power limit has to be
+ * divided by a port voltage. We don't have a port voltage, and so we'll use
+ * the minimum voltage for a Type 2 PSE.
+ *
+ * Return: the threshold in mA, capped by the class limit.
+ */
+static int ltc4266_pw_limit_to_icut(int max_mw, int class)
+{
+ if (max_mw >= ltc4266_class_pw[class])
+ return ltc4266_class_to_icut[class];
+
+ return DIV_ROUND_UP(max_mw * 1000, LTC4266_VPORT_MIN_MV);
+}
+
+static int ltc4266_port_mode(struct ltc4266_port *port, enum ltc4266_port_mode opmd)
+{
+ if (opmd != LTC4266_OPMD_SEMI)
+ return -EINVAL;
+
+ return regmap_update_bits(port->ltc4266->regmap, LTC4266_REG_OPMD,
+ TWO_BIT_WORD_MASK(port->chan),
+ TWO_BIT_WORD_OFFSET(opmd, port->chan));
+}
+
+static int ltc4266_port_delivering(struct ltc4266_port *port)
+{
+ unsigned int result;
+ int ret;
+
+ ret = regmap_read(port->ltc4266->regmap, LTC4266_REG_STATPWR, &result);
+ if (ret < 0)
+ return ret;
+
+ return !!((result & LTC4266_STATPWR_PG(port->chan)) &&
+ (result & LTC4266_STATPWR_PE(port->chan)));
+}
+
+static int ltc4266_port_init(struct ltc4266_port *port)
+{
+ struct ltc4266 *ltc4266 = port->ltc4266;
+ u8 chan = port->chan;
+ u8 tlim_shift;
+ u8 tlim_mask;
+ u8 tlim_reg;
+ int ret;
+
+ /* Reset the port */
+ ret = regmap_write(ltc4266->regmap, LTC4266_REG_RSTPB, BIT(chan));
+ if (ret < 0)
+ return ret;
+
+ ret = ltc4266_port_mode(port, LTC4266_OPMD_SEMI);
+ if (ret < 0)
+ return ret;
+
+ /* Enable high power mode on the port (for Type 2 PD support) */
+ ret = regmap_update_bits(ltc4266->regmap, LTC4266_REG_HPEN,
+ LTC4266_HPEN(chan), LTC4266_HPEN(chan));
+ if (ret < 0)
+ return ret;
+
+ /* Enable 2-event classification (IEEE 802.3-2022, Clause 33), which the
+ * datasheet refers to as "Ping-Pong" classification.
+ */
+ ret = regmap_update_bits(ltc4266->regmap, LTC4266_REG_HPMD(chan),
+ LTC4266_HPMD_PONGEN, LTC4266_HPMD_PONGEN);
+ if (ret < 0)
+ return ret;
+
+ if (port->rsense == LTC4266_RSENSE_250)
+ ret = regmap_update_bits(ltc4266->regmap, LTC4266_REG_ICUT_HP(chan),
+ LTC4266_ICUT_RSENSE_025_OHM,
+ LTC4266_ICUT_RSENSE_025_OHM);
+ else
+ ret = regmap_update_bits(ltc4266->regmap, LTC4266_REG_ICUT_HP(chan),
+ LTC4266_ICUT_RSENSE_025_OHM, 0);
+
+ if (ret < 0)
+ return ret;
+
+ if (chan <= 1)
+ tlim_reg = LTC4266_REG_TLIM12;
+ else
+ tlim_reg = LTC4266_REG_TLIM34;
+
+ /* Each tlim register packs two ports: the even port in the low nibble
+ * and the odd port in the high nibble. Shift both the value and the
+ * mask into the correct nibble.
+ */
+ if (chan & BIT(0))
+ tlim_shift = 4;
+ else
+ tlim_shift = 0;
+
+ tlim_mask = GENMASK(3, 0) << tlim_shift;
+
+ ret = regmap_update_bits(ltc4266->regmap, tlim_reg,
+ tlim_mask, LTC4266_TLIM_VALUE << tlim_shift);
+ if (ret < 0)
+ return ret;
+
+ /* Enable disconnect detect. */
+ ret = regmap_update_bits(ltc4266->regmap, LTC4266_REG_DISENA,
+ BIT(chan), BIT(chan));
+ if (ret < 0)
+ return ret;
+
+ /* Enable detection (low nibble), classification (high nibble) on the port */
+ ret = regmap_write(ltc4266->regmap, LTC4266_REG_DETPB,
+ LTC4266_DETPB_CLASS_ENABLE(chan) |
+ LTC4266_DETPB_DETECT_ENABLE(chan));
+
+ if (ret < 0)
+ return ret;
+
+ dev_dbg(ltc4266->dev, "Channel %d has been initialized\n", chan);
+ return 0;
+}
+
+/* Read the port's classification result and return a class 0-4 or an error if
+ * the result isn't valid.
+ */
+static int ltc4266_port_get_class(struct ltc4266_port *port)
+{
+ struct ltc4266 *ltc4266 = port->ltc4266;
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(ltc4266->regmap, LTC4266_REG_STAT(port->chan), &val);
+ if (ret < 0) {
+ dev_warn(ltc4266->dev, "Failed to read status register, err=%d\n", ret);
+ return ret;
+ }
+
+ /* Can't have a valid classification result if we've not yet had a good
+ * detection result.
+ */
+ if (LTC4266_PORT_DETECT(val) != LTC4266_DETECT_GOOD)
+ return -EINVAL;
+
+ ret = ltc4266_class_map[LTC4266_PORT_CLASS(val)];
+ return ret;
+}
+
+/* Maximum power the classified PD is allowed. It does not depend on the port
+ * being powered, but it does depend on the port having a PD attached and being
+ * enabled to the point of running classification and detection cycles.
+ */
+static int ltc4266_port_max_pw(struct ltc4266_port *port)
+{
+ int class = ltc4266_port_get_class(port);
+
+ if (class < 0)
+ return class;
+
+ return ltc4266_class_pw[class];
+}
+
+static int ltc4266_pi_get_pw_status(struct pse_controller_dev *pcdev, int id,
+ struct pse_pw_status *pw_status)
+{
+ struct ltc4266_port *port = ltc4266_pi_port(pcdev, id);
+ int ret;
+
+ ret = ltc4266_port_delivering(port);
+ if (ret < 0)
+ return ret;
+ if (ret)
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
+ else
+ pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;
+
+ return 0;
+}
+
+/* With the static budget evaluation strategy the PSE core calls this only
+ * after a PD has been classified and the power budget has been allocated.
+ * Program the current limit for the classified PD, reduced if an admin power
+ * limit asks for less than the class is entitled to, and apply power.
+ */
+static int ltc4266_pi_enable(struct pse_controller_dev *pcdev, int id)
+{
+ struct ltc4266_port *port = ltc4266_pi_port(pcdev, id);
+ int class, icut, ret;
+
+ class = ltc4266_port_get_class(port);
+ if (class < 0)
+ return class;
+
+ ret = ltc4266_port_set_ilim(port, class);
+ if (ret < 0)
+ return ret;
+
+ /* Derive the threshold from the admin limit against the class in front of
+ * us now, since a different PD may have been plugged in since the limit
+ * was set.
+ */
+ if (port->pw_limit)
+ icut = ltc4266_pw_limit_to_icut(port->pw_limit, class);
+ else
+ icut = ltc4266_class_to_icut[class];
+
+ ret = ltc4266_port_set_icut(port, icut);
+ if (ret < 0)
+ return ret;
+
+ /* Apply power to the port. */
+ return regmap_write(port->ltc4266->regmap, LTC4266_REG_PWRPB,
+ BIT(port->chan));
+}
+
+static int ltc4266_pi_disable(struct pse_controller_dev *pcdev, int id)
+{
+ struct ltc4266_port *port = ltc4266_pi_port(pcdev, id);
+
+ /* Resetting the port (RSTPB, issued at the start of ltc4266_port_init)
+ * removes power AND clears all per-port configuration, so the port must
+ * be fully re-initialised. This returns it to semi-auto mode with
+ * detection and classification re-enabled (rather than shutting it
+ * down) so a reconnecting PD is detected and the PSE core can retry
+ * power delivery, e.g. when budget is freed by a higher-priority port.
+ */
+ return ltc4266_port_init(port);
+}
+
+static int ltc4266_pi_get_voltage(struct pse_controller_dev *pcdev, int id)
+{
+ return ltc4266_read_iv(ltc4266_pi_port(pcdev, id), READ_VOLTAGE);
+}
+
+static int ltc4266_pi_get_admin_state(struct pse_controller_dev *pcdev, int id,
+ struct pse_admin_state *admin_state)
+{
+ struct ltc4266_port *port = ltc4266_pi_port(pcdev, id);
+ unsigned int val;
+ int ret;
+
+ /* Report whether power is actually being delivered at the hardware
+ * level. The PSE core relies on this (pse_pi_is_hw_enabled()) to know
+ * which software-enabled ports still need power delivery attempted,
+ * e.g. after budget is freed. In software power control mode the core
+ * reports the administrative state to userspace separately.
+ *
+ * Use the power-good nibble (pgN): the power-enable nibble (peN) only
+ * means power was requested, whereas power-good means the port is
+ * actively sourcing power to the PD.
+ */
+ ret = regmap_read(port->ltc4266->regmap, LTC4266_REG_STATPWR, &val);
+ if (ret < 0)
+ return ret;
+
+ if (val & LTC4266_STATPWR_PG(port->chan))
+ admin_state->c33_admin_state =
+ ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED;
+ else
+ admin_state->c33_admin_state =
+ ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
+
+ return 0;
+}
+
+/* Get the PD Classification Result. If there isn't one, return 0 so ethtool
+ * omits the attribute.
+ */
+static int ltc4266_pi_get_pw_class(struct pse_controller_dev *pcdev, int id)
+{
+ int ret = ltc4266_port_get_class(ltc4266_pi_port(pcdev, id));
+
+ /* ltc4266_port_get_class will return either the class, or an errno.
+ * Returning an errno from this function will mean the ethtool command
+ * will abort with the error and not emit later useful information.
+ * Since the "power class" is expected to be returned here, and class 0
+ * and class 3 are equivalent in terms of power allocation, we'll return 3.
+ */
+
+ if (ret == 0)
+ ret = 3;
+ if (ret < 0)
+ ret = 0;
+ return ret;
+}
+
+/* Get the power requested by the PD before enabling the port: its
+ * classification power.
+ */
+static int ltc4266_pi_get_pw_req(struct pse_controller_dev *pcdev, int id)
+{
+ return ltc4266_port_max_pw(ltc4266_pi_port(pcdev, id));
+}
+
+static int ltc4266_pi_get_actual_pw(struct pse_controller_dev *pcdev, int id)
+{
+ struct ltc4266_port *port = ltc4266_pi_port(pcdev, id);
+ int uA, uV;
+ u64 uW;
+
+ uA = ltc4266_read_iv(port, READ_CURRENT);
+ if (uA < 0)
+ return uA;
+
+ uV = ltc4266_read_iv(port, READ_VOLTAGE);
+ if (uV < 0)
+ return uV;
+
+ /* Convert uA to mA and uV to mV; mA * mV = uW */
+ uW = DIV_ROUND_CLOSEST_ULL(uA, 1000) * DIV_ROUND_CLOSEST_ULL(uV, 1000);
+
+ return (int)DIV_ROUND_CLOSEST_ULL(uW, 1000);
+}
+
+static int ltc4266_pi_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
+ struct pse_pw_limit_ranges *pw_limit_ranges)
+{
+ struct ethtool_c33_pse_pw_limit_range *c33_pw_limit_ranges;
+ int class_pw_limit;
+
+ c33_pw_limit_ranges = kzalloc_obj(*c33_pw_limit_ranges);
+ if (!c33_pw_limit_ranges)
+ return -ENOMEM;
+
+ class_pw_limit = ltc4266_port_max_pw(ltc4266_pi_port(pcdev, id));
+ if (class_pw_limit < 0)
+ class_pw_limit = 0;
+
+ c33_pw_limit_ranges[0].min = LTC4266_PW_LIMIT_MIN;
+ c33_pw_limit_ranges[0].max = class_pw_limit ? class_pw_limit : LTC4266_PW_LIMIT_MAX;
+
+ pw_limit_ranges->c33_pw_limit_ranges = c33_pw_limit_ranges;
+
+ /* Return the number of ranges */
+ return 1;
+}
+
+static int ltc4266_pi_set_pw_limit(struct pse_controller_dev *pcdev,
+ int id, int max_mw)
+{
+ struct ltc4266_port *port = ltc4266_pi_port(pcdev, id);
+ int class_pw_limit;
+ int class;
+ int icut;
+ int ret;
+
+ class = ltc4266_port_get_class(port);
+ if (class < 0)
+ return class;
+
+ class_pw_limit = ltc4266_class_pw[class];
+
+ if (max_mw < LTC4266_PW_LIMIT_MIN || max_mw > class_pw_limit) {
+ dev_err(port->ltc4266->dev, "power limit %d is out of range [%d, %d]\n",
+ max_mw, LTC4266_PW_LIMIT_MIN, class_pw_limit);
+ return -ERANGE;
+ }
+
+ icut = ltc4266_pw_limit_to_icut(max_mw, class);
+
+ ret = ltc4266_port_set_icut(port, icut);
+ if (!ret)
+ port->pw_limit = max_mw;
+
+ return ret;
+}
+
+/* Configured power limit for the port, in mW. Defaults to the
+ * classification-based maximum; a lower value configured through
+ * pi_set_pw_limit() overrides it. If there is a PD connected and classified
+ * on the port, we'll use the minimum between the admin-configured limit and
+ * the maximum classification power for that class.
+ */
+static int ltc4266_pi_get_pw_limit(struct pse_controller_dev *pcdev, int id)
+{
+ int admin_limit = LTC4266_PW_LIMIT_MAX;
+ int class_limit;
+
+ struct ltc4266_port *port = ltc4266_pi_port(pcdev, id);
+
+ class_limit = ltc4266_port_max_pw(port);
+ if (class_limit < 0) {
+ /* We don't have a PD detected and classified */
+ class_limit = LTC4266_PW_LIMIT_MAX;
+ }
+ if (port->pw_limit) {
+ /* Admin has configured a power limit */
+ admin_limit = port->pw_limit;
+ }
+ return min(admin_limit, class_limit);
+}
+
+/* Description of one delivery channel parsed out of the "channels" node. Held
+ * only for the duration of ltc4266_setup_pi_matrix(): what survives is the
+ * per-PI struct ltc4266_port built from the channel a PI actually references.
+ */
+struct ltc4266_chan_desc {
+ struct device_node *np;
+ enum sense_resistor rsense;
+};
+
+static int ltc4266_get_of_channels(struct ltc4266 *ltc4266,
+ struct ltc4266_chan_desc *chans)
+{
+ struct device_node *channels_node;
+ u32 chan_id, sense;
+ int ret;
+
+ channels_node = of_get_child_by_name(ltc4266->np, "channels");
+ if (!channels_node)
+ return dev_err_probe(ltc4266->dev, -EINVAL,
+ "missing \"channels\" node\n");
+
+ for_each_child_of_node_scoped(channels_node, chan_node) {
+ if (!of_node_name_eq(chan_node, "channel"))
+ continue;
+
+ ret = of_property_read_u32(chan_node, "reg", &chan_id);
+ if (ret) {
+ ret = dev_err_probe(ltc4266->dev, ret,
+ "missing reg property in node %pOF\n",
+ chan_node);
+ goto out;
+ }
+
+ if (chan_id >= LTC4266_MAX_PORTS) {
+ ret = dev_err_probe(ltc4266->dev, -EINVAL,
+ "channel id %u is out of range in node %pOF\n",
+ chan_id, chan_node);
+ goto out;
+ }
+
+ if (chans[chan_id].np) {
+ ret = dev_err_probe(ltc4266->dev, -EINVAL,
+ "channel id %u is already used, please check the reg property in node %pOF\n",
+ chan_id, chan_node);
+ goto out;
+ }
+
+ ret = of_property_read_u32(chan_node, "sense-resistor-micro-ohms", &sense);
+ if (ret) {
+ ret = dev_err_probe(ltc4266->dev, ret,
+ "missing sense-resistor-micro-ohms property in node %pOF\n",
+ chan_node);
+ goto out;
+ }
+
+ if (sense == 250000) {
+ chans[chan_id].rsense = LTC4266_RSENSE_250;
+ } else if (sense == 500000) {
+ chans[chan_id].rsense = LTC4266_RSENSE_500;
+ } else {
+ ret = dev_err_probe(ltc4266->dev, -EINVAL,
+ "invalid sense resistor value %u in node %pOF\n",
+ sense, chan_node);
+ goto out;
+ }
+
+ chans[chan_id].np = of_node_get(chan_node);
+ }
+
+ of_node_put(channels_node);
+ return 0;
+
+out:
+ of_node_put(channels_node);
+ return ret;
+}
+
+/* Find the channel a PI pairset phandle points at. */
+static int ltc4266_match_channel(const struct pse_pi_pairset *pairset,
+ struct ltc4266_chan_desc *chans)
+{
+ int i;
+
+ for (i = 0; i < LTC4266_MAX_PORTS; i++)
+ if (pairset->np == chans[i].np)
+ return i;
+
+ return -ENODEV;
+}
+
+/* Build a port context for every PSE PI described in the device tree, bound to
+ * the channel its pairset references. A PI with no node is left NULL; the PSE
+ * core does not register a regulator for it and so never calls back for it.
+ */
+static int ltc4266_map_pis(struct ltc4266 *ltc4266,
+ struct ltc4266_chan_desc *chans)
+{
+ struct pse_controller_dev *pcdev = <c4266->pcdev;
+ struct ltc4266_port *port;
+ int i, j, chan;
+
+ for (i = 0; i < LTC4266_MAX_PORTS; i++) {
+ struct pse_pi *pi = &pcdev->pi[i];
+
+ if (!pi->np)
+ continue;
+
+ if (!pi->pairset[0].np)
+ return dev_err_probe(ltc4266->dev, -EINVAL,
+ "%pOF has no pairsets\n", pi->np);
+
+ /* The LTC4266 delivers over a single pairset per channel, so
+ * there is no 4-pair mode to spread a PI over two channels.
+ */
+ if (pi->pairset[1].np)
+ return dev_err_probe(ltc4266->dev, -EOPNOTSUPP,
+ "%pOF: 4-pair PSE PIs are not supported\n",
+ pi->np);
+
+ chan = ltc4266_match_channel(&pi->pairset[0], chans);
+ if (chan < 0)
+ return dev_err_probe(ltc4266->dev, chan,
+ "%pOF: pairset %pOF is not a channel of this controller\n",
+ pi->np, pi->pairset[0].np);
+
+ for (j = 0; j < i; j++)
+ if (ltc4266->ports[j] && ltc4266->ports[j]->chan == chan)
+ return dev_err_probe(ltc4266->dev, -EINVAL,
+ "%pOF: channel %d is already used by %pOF\n",
+ pi->np, chan, pcdev->pi[j].np);
+
+ port = devm_kzalloc(ltc4266->dev, sizeof(*port), GFP_KERNEL);
+ if (!port)
+ return -ENOMEM;
+
+ port->ltc4266 = ltc4266;
+ port->chan = chan;
+ port->rsense = chans[chan].rsense;
+ ltc4266->ports[i] = port;
+
+ dev_dbg(ltc4266->dev, "PI %d is backed by channel %d\n", i, chan);
+ }
+
+ return 0;
+}
+
+static int ltc4266_setup_pi_matrix(struct pse_controller_dev *pcdev)
+{
+ struct ltc4266 *ltc4266 = container_of(pcdev, struct ltc4266, pcdev);
+ struct ltc4266_chan_desc chans[LTC4266_MAX_PORTS] = { };
+ int i, ret;
+
+ if (pcdev->no_of_pse_pi)
+ return dev_err_probe(ltc4266->dev, -EINVAL,
+ "a \"pse-pis\" node is required\n");
+
+ ret = ltc4266_get_of_channels(ltc4266, chans);
+ if (!ret)
+ ret = ltc4266_map_pis(ltc4266, chans);
+
+ for (i = 0; i < LTC4266_MAX_PORTS; i++)
+ of_node_put(chans[i].np);
+
+ if (ret)
+ return ret;
+
+ for (i = 0; i < LTC4266_MAX_PORTS; i++) {
+ if (!ltc4266->ports[i])
+ continue;
+
+ ret = ltc4266_port_init(ltc4266->ports[i]);
+ if (ret < 0)
+ return dev_err_probe(ltc4266->dev, ret,
+ "Failed to initialize PI %d\n", i);
+ }
+
+ return 0;
+}
+
+static const struct pse_controller_ops ltc4266_ops = {
+ .setup_pi_matrix = ltc4266_setup_pi_matrix,
+ .pi_get_admin_state = ltc4266_pi_get_admin_state,
+ .pi_get_pw_status = ltc4266_pi_get_pw_status,
+ .pi_get_pw_class = ltc4266_pi_get_pw_class,
+ .pi_get_actual_pw = ltc4266_pi_get_actual_pw,
+ .pi_enable = ltc4266_pi_enable,
+ .pi_disable = ltc4266_pi_disable,
+ .pi_get_voltage = ltc4266_pi_get_voltage,
+ .pi_get_pw_limit = ltc4266_pi_get_pw_limit,
+ .pi_set_pw_limit = ltc4266_pi_set_pw_limit,
+ .pi_get_pw_limit_ranges = ltc4266_pi_get_pw_limit_ranges,
+ .pi_get_pw_req = ltc4266_pi_get_pw_req,
+};
+
+#define LTC4266_INTERRUPT_SOURCES (LTC4266_INT_SUPPLY | LTC4266_INT_TSTART | \
+ LTC4266_INT_TCUT | LTC4266_INT_CLASS | \
+ LTC4266_INT_DETECT | LTC4266_INT_DIS)
+
+static void ltc4266_enable_interrupts(struct ltc4266 *ltc4266)
+{
+ /* Unmask interrupts */
+ regmap_write(ltc4266->regmap, LTC4266_REG_INTMASK,
+ LTC4266_INTERRUPT_SOURCES);
+}
+
+static int ltc4266_disable_interrupts(struct ltc4266 *ltc4266)
+{
+ int ret;
+
+ ret = regmap_write(ltc4266->regmap, LTC4266_REG_INTMASK, 0x00);
+ if (ret < 0)
+ return ret;
+
+ /* Reset the (SMBus Alert) interrupt pin */
+ return regmap_write(ltc4266->regmap, LTC4266_REG_RSTPB, LTC4266_RSTPB_PINCLR);
+}
+
+static int ltc4266_map_event(int irq, struct pse_controller_dev *pcdev,
+ unsigned long *notifs, unsigned long *notifs_mask)
+{
+ struct ltc4266 *ltc4266 = container_of(pcdev, struct ltc4266, pcdev);
+ unsigned int detevn = 0, fltevn = 0, tsevn = 0, supevn = 0;
+ unsigned int intstat;
+ int ret;
+ int i;
+
+ ret = regmap_read(ltc4266->regmap, LTC4266_REG_INTSTAT, &intstat);
+ if (ret < 0)
+ return ret;
+
+ if (!intstat)
+ return 0;
+
+ ltc4266_disable_interrupts(ltc4266);
+
+ if (intstat & LTC4266_INT_SUPPLY) {
+ ret = regmap_read(ltc4266->regmap, LTC4266_REG_SUPEVN_COR, &supevn);
+ if (ret < 0) {
+ dev_err(<c4266->client->dev,
+ "Bus error reading SUPPLY Fault register ret = %d\n",
+ ret);
+ goto done;
+ }
+ if (supevn & LTC4266_SUPEVN_OVERTEMP) {
+ dev_err(<c4266->client->dev, "SUPPLY_FAULT, 0x%02x\n", supevn);
+ for (i = 0; i < LTC4266_MAX_PORTS; i++) {
+ if (!ltc4266->ports[i])
+ continue;
+
+ notifs[i] = ETHTOOL_PSE_EVENT_OVER_TEMP;
+ *notifs_mask |= BIT(i);
+ }
+ /* Supply faults are critical system-level hardware
+ * problems, they aren't fixable in software
+ */
+ goto done;
+ }
+ }
+
+ if (intstat & (LTC4266_INT_DIS | LTC4266_INT_TCUT)) {
+ ret = regmap_read(ltc4266->regmap, LTC4266_REG_FLTEVN_COR, &fltevn);
+ if (ret < 0) {
+ dev_err(<c4266->client->dev, "Failed to read fltevn err=%d\n", ret);
+ goto done;
+ }
+ }
+
+ if (intstat & (LTC4266_INT_TSTART | LTC4266_INT_TCUT)) {
+ ret = regmap_read(ltc4266->regmap, LTC4266_REG_TSEVN_COR, &tsevn);
+ if (ret < 0) {
+ dev_err(<c4266->client->dev, "Failed to read tsevn, err=%d\n", ret);
+ goto done;
+ }
+ }
+
+ if (intstat & (LTC4266_INT_CLASS | LTC4266_INT_DETECT)) {
+ ret = regmap_read(ltc4266->regmap, LTC4266_REG_DETEVN_COR, &detevn);
+ if (ret < 0) {
+ dev_err(<c4266->client->dev, "Failed to read detevn, err=%d\n", ret);
+ goto done;
+ }
+ }
+
+ /* The event registers are indexed by delivery channel while notifs[] is
+ * indexed by PSE PI id, so walk the PIs and use each one's channel to
+ * select the event bits.
+ */
+ for (i = 0; i < LTC4266_MAX_PORTS; i++) {
+ struct ltc4266_port *port = ltc4266->ports[i];
+ u8 chan;
+
+ if (!port)
+ continue;
+
+ chan = port->chan;
+
+ if ((tsevn | fltevn) & (LTC4266_EVN_HI(chan) | LTC4266_EVN_LO(chan))) {
+ /* This means the port has disconnected; if we see
+ * this, then we don't care if any of the remaining
+ * events are set. If the device did disconnect
+ * briefly, it'll redetect and reclassify accordingly
+ */
+ notifs[i] |= ETHTOOL_C33_PSE_EVENT_DISCONNECTION;
+ *notifs_mask |= BIT(i);
+
+ /* Report over-current if the port went down for any
+ * overcurrent reason: tSTART (tsevn low nibble, startup
+ * inrush), tLIM (tsevn high nibble, current-limit
+ * timeout) or tCUT (fltevn low nibble, I_CUT timeout).
+ */
+ if ((tsevn & (LTC4266_EVN_LO(chan) | LTC4266_EVN_HI(chan))) ||
+ (fltevn & LTC4266_EVN_LO(chan)))
+ notifs[i] |= ETHTOOL_PSE_EVENT_OVER_CURRENT;
+
+ dev_err(<c4266->client->dev, "tsevn=0x%02X fltevn=0x%02X\n",
+ tsevn, fltevn);
+ continue;
+ }
+ if (detevn & LTC4266_EVN_LO(chan)) {
+ /* Read the detect result, and if it isn't detect good,
+ * call it a disconnect
+ */
+ unsigned int detval;
+
+ ret = regmap_read(ltc4266->regmap, LTC4266_REG_STAT(chan), &detval);
+ if (ret < 0) {
+ dev_warn(ltc4266->dev, "Failed to read status register, err=%d\n",
+ ret);
+ continue;
+ }
+
+ if (LTC4266_PORT_DETECT(detval) != LTC4266_DETECT_GOOD) {
+ notifs[i] |= ETHTOOL_C33_PSE_EVENT_DISCONNECTION;
+ *notifs_mask |= BIT(i);
+ continue;
+ }
+ }
+
+ if (detevn & LTC4266_EVN_HI(chan)) {
+ int class = ltc4266_port_get_class(port);
+
+ if (class >= 0) {
+ notifs[i] |= ETHTOOL_C33_PSE_EVENT_CLASSIFICATION;
+ *notifs_mask |= BIT(i);
+ }
+ }
+ }
+
+done:
+ ltc4266_enable_interrupts(ltc4266);
+ return 0;
+}
+
+static const struct regmap_config ltc4266_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0x5F,
+};
+
+static int ltc4266_probe(struct i2c_client *client)
+{
+ struct ltc4266 *ltc4266;
+ struct regmap *regmap;
+ unsigned int id_reg;
+ int ret;
+
+ regmap = devm_regmap_init_i2c(client, <c4266_regmap_config);
+ if (IS_ERR(regmap))
+ return dev_err_probe(&client->dev, PTR_ERR(regmap),
+ "Failed to allocate regmap\n");
+
+ /* Confirm we are talking to an LTC4266: the id register (0x1B) should
+ * read back its documented reset value of 0x64.
+ */
+ ret = regmap_read(regmap, LTC4266_REG_ID, &id_reg);
+ if (ret < 0)
+ return dev_err_probe(&client->dev, ret, "Failed to read ID register\n");
+
+ if (id_reg != LTC4266_ID)
+ return dev_err_probe(&client->dev, -ENODEV,
+ "Expected an ID of 0x64, saw 0x%02X\n", id_reg);
+
+ /* Reset the chip */
+ regmap_write(regmap, LTC4266_REG_RSTPB, LTC4266_RSTPB_INTCLR | LTC4266_RSTPB_RSTALL);
+
+ /* LTC4266 requires approximately 10 ms after reset to be stable; if it
+ * isn't, then there is typically an undervoltage lockout/something pretty bad
+ * going on. We give it 50 ms here so we don't need to poll the chip and use I2C bandwidth
+ */
+ msleep(50);
+
+ /* Let's make sure the chip came out of reset (if not, the chip is probably
+ * either (no longer?) present, in thermal shutdown, or watchdogged....either
+ * way, there's nothing we can do in software to fix it)
+ */
+ ret = regmap_read(regmap, LTC4266_REG_ID, &id_reg);
+ if (ret < 0)
+ return dev_err_probe(&client->dev, ret,
+ "Failed to re-read ID register after reset\n");
+
+ if (id_reg != LTC4266_ID)
+ return dev_err_probe(&client->dev, -ENODEV,
+ "Failed to re-read device ID after reset 0x%02X\n",
+ id_reg);
+
+ ltc4266 = devm_kzalloc(&client->dev, sizeof(struct ltc4266), GFP_KERNEL);
+ if (!ltc4266)
+ return -ENOMEM;
+
+ i2c_set_clientdata(client, ltc4266);
+ ltc4266->client = client;
+ ltc4266->regmap = regmap;
+ ltc4266->np = client->dev.of_node;
+ ltc4266->dev = &client->dev;
+
+ /* After reset, the LTC4266 will interrupt with a (single) supply fault.
+ * Clear it here and discard the result
+ */
+ regmap_read(ltc4266->regmap, LTC4266_REG_SUPEVN_COR, &id_reg);
+
+ ltc4266_disable_interrupts(ltc4266);
+
+ ltc4266->pcdev.owner = THIS_MODULE;
+ ltc4266->pcdev.ops = <c4266_ops;
+ ltc4266->pcdev.dev = &client->dev;
+ ltc4266->pcdev.types = ETHTOOL_PSE_C33;
+ ltc4266->pcdev.nr_lines = LTC4266_MAX_PORTS;
+
+ /* Power delivery is gated by the PSE core static budget evaluation
+ * strategy: ports are only powered (pi_enable) once a PD has been
+ * classified and the shared power-domain budget allows it. Requires an
+ * interrupt so classification/disconnection events are reported.
+ */
+ ltc4266->pcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_STATIC;
+
+ ret = devm_pse_controller_register(ltc4266->dev, <c4266->pcdev);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Failed to register PSE controller\n");
+
+ if (client->irq) {
+ struct pse_irq_desc irq_desc = {
+ .name = "ltc4266-irq",
+ .map_event = ltc4266_map_event,
+ };
+
+ /* Enable the interrupt pin, and only report detect events on
+ * change (detchg) so idle ports continuously re-running
+ * detection in semi-auto mode don't flood the host with a
+ * detect event every cycle.
+ */
+ regmap_update_bits(ltc4266->regmap, LTC4266_REG_MCONF,
+ LTC4266_MCONF_INTERRUPT_ENABLE | LTC4266_MCONF_DETCHG,
+ LTC4266_MCONF_INTERRUPT_ENABLE | LTC4266_MCONF_DETCHG);
+
+ ret = devm_pse_irq_helper(<c4266->pcdev, client->irq,
+ 0, &irq_desc);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Failed to register PSE IRQ\n");
+
+ /* Unmask the chip interrupt sources now the handler is ready. */
+ ltc4266_enable_interrupts(ltc4266);
+ } else {
+ return dev_err_probe(&client->dev, -EINVAL,
+ "Interrupt is required for power budget management\n");
+ }
+
+ return 0;
+}
+
+static void ltc4266_remove(struct i2c_client *client)
+{
+ struct ltc4266 *ltc4266 = i2c_get_clientdata(client);
+
+ /* Reset all the ports and do not re-init */
+ regmap_write(ltc4266->regmap, LTC4266_REG_RSTPB, LTC4266_RSTPB_RSTALL);
+}
+
+static const struct i2c_device_id ltc4266_id[] = {
+ {.name = "ltc4266"},
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, ltc4266_id);
+
+static const struct of_device_id ltc4266_of_match[] = {
+ { .compatible = "lltc,ltc4266" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ltc4266_of_match);
+
+static struct i2c_driver ltc4266_driver = {
+ .driver = {
+ .name = "ltc4266",
+ .of_match_table = ltc4266_of_match,
+ },
+ .probe = ltc4266_probe,
+ .remove = ltc4266_remove,
+ .id_table = ltc4266_id,
+};
+module_i2c_driver(ltc4266_driver);
+
+MODULE_AUTHOR("Kyle Swenson <kyle.swenson@est.tech>");
+MODULE_DESCRIPTION("LTC4266 PoE PSE Controller Driver");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [RFC PATCH net-next v2 1/2] dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller
2026-08-20 14:25 ` [RFC PATCH net-next v2 1/2] dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller Kyle Swenson
@ 2026-08-21 14:25 ` sashiko-bot
2026-08-24 10:46 ` Kory Maincent
1 sibling, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-08-21 14:25 UTC (permalink / raw)
To: Kyle Swenson; +Cc: conor+dt, devicetree, robh
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The child node definition `^pse-pi@[0-3]$` is missing `unevaluatedProperties: false`.
--
--- Patch [1]: [RFC PATCH net-next v2 1/2] dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller ---
commit 1f8974966417493a9b12a4f2599002b08898882c
Author: Kyle Swenson <kyle.swenson@est.tech>
dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller
Add the LTC4266 Power Sourcing Equipment controller device tree bindings
documentation.
> diff --git a/Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml b/Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml
> new file mode 100644
> index 0000000000000..b3da8fea0b07a
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml
[ ... ]
> + pse-pis:
> + type: object
> + additionalProperties: false
> +
> + properties:
> + '#address-cells':
> + const: 1
> +
> + '#size-cells':
> + const: 0
> +
> + patternProperties:
> + '^pse-pi@[0-3]$':
> + type: object
> + properties:
> + pairsets:
[Severity: Low]
Is it possible to add unevaluatedProperties: false to this object?
The Device Tree schema meta-schema strictly requires that any node defined as
type: object must specify either additionalProperties: false or
unevaluatedProperties: false.
Omitting this on the ^pse-pi@[0-3]$ node causes the schema to silently allow
any undocumented or misspelled property inside the pse-pi@X nodes, which
defeats the purpose of dt-schema validation. Since this node relies on
properties defined in the referenced pse-controller.yaml, using
unevaluatedProperties: false seems to be needed here.
> + description: The LTC4266 delivers power to a PI over a single
> + pairset, driven by one of the controller's four channels. There
> + is no 4-pair mode spreading a PI over two channels, so exactly
> + one channel phandle is expected.
> + maxItems: 1
> + pairset-names:
> + maxItems: 1
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260820142429.2285172-1-kyle.swenson@est.tech?part=1
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC PATCH net-next v2 2/2] net: pse-pd: Add LTC4266 PSE controller driver
2026-08-20 14:25 ` [RFC PATCH net-next v2 2/2] net: pse-pd: Add LTC4266 PSE controller driver Kyle Swenson
@ 2026-08-21 14:25 ` sashiko-bot
0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-08-21 14:25 UTC (permalink / raw)
To: Kyle Swenson; +Cc: conor+dt, robh, devicetree
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] I2C read failures in the IRQ handler silently discard clear-on-read hardware events and return 0, risking an IRQ storm.
- [High] Incorrect devres registration sequence exposes the PSE controller before initialization completes and induces use-after-free during teardown.
- [Medium] Admin power limit configuration is inappropriately bounded by the currently connected PD class, preventing configuration on idle ports.
- [Medium] `ltc4266_pi_get_voltage` returns the voltage in microvolts (uV) instead of millivolts (mV).
- [Medium] Routine PD disconnections trigger an unconditional `dev_err` log.
- [Medium] Premature rounding in power calculation causes unnecessary precision loss.
--
--- Patch [2]: [RFC PATCH net-next v2 2/2] net: pse-pd: Add LTC4266 PSE controller driver ---
commit 5319fc10897e2c97c798780d619129c31798bf5a
Author: Kyle Swenson <kyle.swenson@est.tech>
net: pse-pd: Add LTC4266 PSE controller driver
Add a new driver for the Linear Technology LTC4266 I2C Power Sourcing
Equipment controller and integrate with the PSE controller core. The
LTC4266 supports Type 1 and Type 2 Powered Devices.
> diff --git a/drivers/net/pse-pd/ltc4266.c b/drivers/net/pse-pd/ltc4266.c
> new file mode 100644
> index 0000000000000..2f9c37ebf1d29
> --- /dev/null
> +++ b/drivers/net/pse-pd/ltc4266.c
[ ... ]
> +static int ltc4266_pi_get_voltage(struct pse_controller_dev *pcdev, int id)
> +{
> + return ltc4266_read_iv(ltc4266_pi_port(pcdev, id), READ_VOLTAGE);
> +}
[Severity: Medium]
ltc4266_read_iv() returns the voltage in microvolts. Does the PSE core API
expect this value in millivolts instead?
Returning microvolts here might cause ethtool to display a voltage 1000 times
larger than the actual value.
[ ... ]
> +static int ltc4266_pi_get_actual_pw(struct pse_controller_dev *pcdev, int id)
> +{
> + struct ltc4266_port *port = ltc4266_pi_port(pcdev, id);
> + int uA, uV;
> + u64 uW;
> +
> + uA = ltc4266_read_iv(port, READ_CURRENT);
> + if (uA < 0)
> + return uA;
> +
> + uV = ltc4266_read_iv(port, READ_VOLTAGE);
> + if (uV < 0)
> + return uV;
> +
> + /* Convert uA to mA and uV to mV; mA * mV = uW */
> + uW = DIV_ROUND_CLOSEST_ULL(uA, 1000) * DIV_ROUND_CLOSEST_ULL(uV, 1000);
> +
> + return (int)DIV_ROUND_CLOSEST_ULL(uW, 1000);
> +}
[Severity: Medium]
Could this calculation cause unnecessary precision loss?
Since uA * uV safely fits within a 64-bit unsigned integer, perhaps the
multiplication could happen before dividing by 1000 to avoid up to a 10%
mathematical error for smaller loads?
[ ... ]
> +static int ltc4266_pi_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
> + struct pse_pw_limit_ranges *pw_limit_ranges)
> +{
> + struct ethtool_c33_pse_pw_limit_range *c33_pw_limit_ranges;
> + int class_pw_limit;
> +
> + c33_pw_limit_ranges = kzalloc_obj(*c33_pw_limit_ranges);
> + if (!c33_pw_limit_ranges)
> + return -ENOMEM;
> +
> + class_pw_limit = ltc4266_port_max_pw(ltc4266_pi_port(pcdev, id));
> + if (class_pw_limit < 0)
> + class_pw_limit = 0;
> +
> + c33_pw_limit_ranges[0].min = LTC4266_PW_LIMIT_MIN;
> + c33_pw_limit_ranges[0].max = class_pw_limit ? class_pw_limit : LTC4266_PW_LIMIT_MAX;
[Severity: Medium]
This bounds the advertised maximum limit by the currently connected device
class. Will this prevent users from configuring an administrative power limit
for a higher-class device on an idle port?
[ ... ]
> +static int ltc4266_pi_set_pw_limit(struct pse_controller_dev *pcdev,
> + int id, int max_mw)
> +{
> + struct ltc4266_port *port = ltc4266_pi_port(pcdev, id);
> + int class_pw_limit;
> + int class;
> + int icut;
> + int ret;
> +
> + class = ltc4266_port_get_class(port);
> + if (class < 0)
> + return class;
> +
> + class_pw_limit = ltc4266_class_pw[class];
> +
> + if (max_mw < LTC4266_PW_LIMIT_MIN || max_mw > class_pw_limit) {
[Severity: Medium]
Similar to the range function above, tying the administrative configuration
to the instantaneously connected device class seems to prevent configuring
limits on an idle port.
Should administrative limits be independent of the current PD state so
they can restrict future connected devices?
[ ... ]
> +static int ltc4266_map_event(int irq, struct pse_controller_dev *pcdev,
> + unsigned long *notifs, unsigned long *notifs_mask)
> +{
[ ... ]
> + if (intstat & (LTC4266_INT_DIS | LTC4266_INT_TCUT)) {
> + ret = regmap_read(ltc4266->regmap, LTC4266_REG_FLTEVN_COR, &fltevn);
> + if (ret < 0) {
> + dev_err(<c4266->client->dev, "Failed to read fltevn err=%d\n", ret);
> + goto done;
> + }
> + }
[ ... ]
> + if ((tsevn & (LTC4266_EVN_LO(chan) | LTC4266_EVN_HI(chan))) ||
> + (fltevn & LTC4266_EVN_LO(chan)))
> + notifs[i] |= ETHTOOL_PSE_EVENT_OVER_CURRENT;
> +
> + dev_err(<c4266->client->dev, "tsevn=0x%02X fltevn=0x%02X\n",
> + tsevn, fltevn);
> + continue;
[Severity: Medium]
Does this code unconditionally print a dev_err() for routine device
disconnections?
It seems like this might generate spurious error logs whenever a user simply
unplugs a cable from the device.
[ ... ]
> +done:
> + ltc4266_enable_interrupts(ltc4266);
> + return 0;
> +}
[Severity: High]
If the I2C read fails in the IRQ handler here, the clear-on-read interrupt
status register is not cleared at the hardware level.
Because the handler jumps to done and re-enables the top-level interrupts,
could this cause an immediate IRQ storm that locks up the system since the
hardware will continually reassert the unhandled event?
[ ... ]
> +static int ltc4266_probe(struct i2c_client *client)
> +{
[ ... ]
> + ret = devm_pse_controller_register(ltc4266->dev, <c4266->pcdev);
> + if (ret)
> + return dev_err_probe(&client->dev, ret,
> + "Failed to register PSE controller\n");
> +
> + if (client->irq) {
> + struct pse_irq_desc irq_desc = {
> + .name = "ltc4266-irq",
> + .map_event = ltc4266_map_event,
> + };
> +
> + /* Enable the interrupt pin, and only report detect events on
> + * change (detchg) so idle ports continuously re-running
> + * detection in semi-auto mode don't flood the host with a
> + * detect event every cycle.
> + */
> + regmap_update_bits(ltc4266->regmap, LTC4266_REG_MCONF,
> + LTC4266_MCONF_INTERRUPT_ENABLE | LTC4266_MCONF_DETCHG,
> + LTC4266_MCONF_INTERRUPT_ENABLE | LTC4266_MCONF_DETCHG);
> +
> + ret = devm_pse_irq_helper(<c4266->pcdev, client->irq,
> + 0, &irq_desc);
[Severity: High]
The controller is registered with devres before the IRQ helper. Since devres
tears down resources in reverse order (LIFO), wouldn't the IRQ be freed
before the controller is unregistered?
If pse_controller_unregister() tries to disable the IRQ using pcdev->irq
during teardown, it might operate on a freed IRQ descriptor. Could this lead
to a use-after-free or accidentally disable an unrelated IRQ?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260820142429.2285172-1-kyle.swenson@est.tech?part=2
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [RFC PATCH net-next v2 1/2] dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller
2026-08-20 14:25 ` [RFC PATCH net-next v2 1/2] dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller Kyle Swenson
2026-08-21 14:25 ` sashiko-bot
@ 2026-08-24 10:46 ` Kory Maincent
1 sibling, 0 replies; 6+ messages in thread
From: Kory Maincent @ 2026-08-24 10:46 UTC (permalink / raw)
To: Kyle Swenson, o.rempel@pengutronix.de, andrew+netdev@lunn.ch,
davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com, robh@kernel.org, krzk+dt@kernel.org,
conor+dt@kernel.org
Cc: netdev@vger.kernel.org, devicetree@vger.kernel.org,
linux-kernel@vger.kernel.org, Roland Kovács,
David Nyström
Hello Kyle,
Glad to see the v2 landing in the mailing list.
On 8/20/26 16:25, Kyle Swenson wrote:
> Add the LTC4266 Power Sourcing Equipment controller device tree bindings
> documentation.
>
> Signed-off-by: Kyle Swenson <kyle.swenson@est.tech>
> ---
> .../bindings/net/pse-pd/lltc,ltc4266.yaml | 180 ++++++++++++++++++
> 1 file changed, 180 insertions(+)
> create mode 100644 Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml
>
> diff --git a/Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml b/Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml
> new file mode 100644
> index 000000000000..b3da8fea0b07
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/net/pse-pd/lltc,ltc4266.yaml
> @@ -0,0 +1,180 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/net/pse-pd/lltc,ltc4266.yaml#
> +$schema: http://devicetree.org/meta-schemas/core.yaml#
> +
> +title: Linear Technology LTC4266 Power Sourcing Equipment controller
> +
> +maintainers:
> + - Kyle Swenson <kyle.swenson@est.tech>
> +
> +allOf:
> + - $ref: pse-controller.yaml#
> +
> +properties:
> + compatible:
> + enum:
> + - lltc,ltc4266
> +
> + reg:
> + maxItems: 1
> +
> + interrupts:
> + maxItems: 1
> +
> + reset-gpios:
> + maxItems: 1
The reset-gpios is currently not implemented in your driver. Maybe add
it in the binding when it will be the case.
> + channels:
> + type: object
> + additionalProperties: false
> + description:
> + Defines the 4 physical delivery channels on the controller that can be
> + referenced by PSE PIs through their "pairsets" property. The actual port
> + matrix mapping is created when PSE PIs reference these channels in their
> + pairsets.
> +
> + properties:
> + '#address-cells':
> + const: 1
> +
> + '#size-cells':
> + const: 0
> +
> + patternProperties:
> + '^channel@[0-3]$':
> + type: object
> + additionalProperties: false
> +
> + properties:
> + reg:
> + maxItems: 1
> +
> + sense-resistor-micro-ohms:
> + description: Sense resistor connected to the channel's MOSFET, used
> + for current measurement for overcurrent detection.
> + enum: [250000, 500000]
> +
> + required:
> + - reg
> + - sense-resistor-micro-ohms
I am bit skeptical about having sense-resistor-micro-ohms as a mandatory
parameter. As you have described it's only for current measurement so
it's not a requirement for PSE functioning. Maybe set it as a default
value if not specified.
Else LGTM.
> + required:
> + - '#address-cells'
> + - '#size-cells'
> +
> + pse-pis:
> + type: object
> + additionalProperties: false
> +
> + properties:
> + '#address-cells':
> + const: 1
> +
> + '#size-cells':
> + const: 0
> +
> + patternProperties:
> + '^pse-pi@[0-3]$':
> + type: object
> + properties:
> + pairsets:
> + description: The LTC4266 delivers power to a PI over a single
> + pairset, driven by one of the controller's four channels. There
> + is no 4-pair mode spreading a PI over two channels, so exactly
> + one channel phandle is expected.
> + maxItems: 1
> + pairset-names:
> + maxItems: 1
> +
> +required:
> + - compatible
> + - reg
> + - interrupts
> + - channels
> + - pse-pis
> +
> +unevaluatedProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/interrupt-controller/irq.h>
> +
> + i2c {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + ethernet-pse@2f {
> + compatible = "lltc,ltc4266";
> + reg = <0x2f>;
> + interrupts = <8 IRQ_TYPE_LEVEL_LOW>;
> + interrupt-parent = <&gpio>;
> +
> + channels {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + phys0: channel@0 {
> + reg = <0>;
> + sense-resistor-micro-ohms = <500000>;
> + };
> +
> + phys1: channel@1 {
> + reg = <1>;
> + sense-resistor-micro-ohms = <500000>;
> + };
> +
> + phys2: channel@2 {
> + reg = <2>;
> + sense-resistor-micro-ohms = <500000>;
> + };
> +
> + phys3: channel@3 {
> + reg = <3>;
> + sense-resistor-micro-ohms = <500000>;
> + };
> + };
> +
> + pse-pis {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + pse_pi0: pse-pi@0 {
> + reg = <0>;
> + #pse-cells = <0>;
> + pairset-names = "alternative-a";
> + pairsets = <&phys0>;
> + polarity-supported = "MDI";
> + vpwr-supply = <&vreg_pse>;
> + };
> +
> + pse_pi1: pse-pi@1 {
> + reg = <1>;
> + #pse-cells = <0>;
> + pairset-names = "alternative-a";
> + pairsets = <&phys1>;
> + polarity-supported = "MDI";
> + vpwr-supply = <&vreg_pse>;
> + };
> +
> + pse_pi2: pse-pi@2 {
> + reg = <2>;
> + #pse-cells = <0>;
> + pairset-names = "alternative-a";
> + pairsets = <&phys2>;
> + polarity-supported = "MDI";
> + vpwr-supply = <&vreg_pse>;
> + };
> +
> + pse_pi3: pse-pi@3 {
> + reg = <3>;
> + #pse-cells = <0>;
> + pairset-names = "alternative-a";
> + pairsets = <&phys3>;
> + polarity-supported = "MDI";
> + vpwr-supply = <&vreg_pse>;
> + };
> + };
> + };
> + };
--
Köry Maincent, Bootlin
Embedded Linux and kernel engineering
https://bootlin.com
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-08-24 10:46 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-20 14:25 [RFC PATCH net-next v2 0/2] net: pse-pd: Add LTC4266 PSE controller driver Kyle Swenson
2026-08-20 14:25 ` [RFC PATCH net-next v2 1/2] dt-bindings: net: pse-pd: Add bindings for LTC4266 PSE Controller Kyle Swenson
2026-08-21 14:25 ` sashiko-bot
2026-08-24 10:46 ` Kory Maincent
2026-08-20 14:25 ` [RFC PATCH net-next v2 2/2] net: pse-pd: Add LTC4266 PSE controller driver Kyle Swenson
2026-08-21 14:25 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.