Skip to contentSkip to conversation

ptp: ocp: Add R4006 I2C peripheral support

RFC Superseded Ahmad Byagowi 2 participants 9 messages Aug 2 – Aug 3

Subsystems (10)

v1 of 7 Superseded Latest is v7 →

lore
Share & tools
lore
#0 lore raw

cover ptp: ocp: Add R4006 I2C peripheral support

The R4006 Time Card places a PCA9546 I2C mux behind the FPGA XIIC controller. Its downstream buses contain temperature, humidity, and pressure sensors, an 18-channel LED controller, and a BNO08x inertial sensor. The current ptp_ocp driver exposes the root bus but does not describe these peripherals.

This RFC adds the generic infrastructure and board description needed to let existing subsystem drivers bind to the R4006 devices:

1. A binding for the Lumissil IS32FL3207 LED controller.
2. A generic IS32FL3207 LED class driver with individual and multicolor LED support and per-output current limits.
3. Software-node propagation from an I2C mux to its channel adapters.
4. EEPROM-based R4006 detection and a per-card software-node topology in ptp_ocp.

The BNO08x is intentionally not instantiated because there is no upstream driver for it. LED status policy is also left to userspace. The kernel exports one multicolor LED class device for GNSS and one for each SMA connector.

The zero-based IS32FL3207 output mapping, confirmed on production hardware, is:

red green blue
GNSS 13 12 14

  SMA1         7     6     8
  SMA2        10     9    11
  SMA3         1     0     2
  SMA4         4     3     5

Questions for this RFC:

- Should IS32FL3207 remain a standalone multicolor driver, or should support be integrated into the older is31fl32xx driver?
- Is assigning software-node children in the generic I2C mux core the preferred way to describe PCI-card mux topologies?
- Which tree should carry the cross-subsystem series once the LED and I2C changes are reviewed?

Validation performed:

- dt_binding_check, including schema lint and example compilation
- ARM64 and x86-64 W=1 builds of all three modified driver objects
- x86-64 sparse C=2 checks with endian checking enabled
- strict checkpatch and git diff --check
- I2C addresses and LED channel mapping validated on an R4006 using the existing downstream implementation

The complete upstream series has not yet been boot-tested on the card. One portability limitation is that the ICP10100 has a fixed 1.8 V board rail but its driver requires a vdd regulator. ACPI server systems provide the dummy regulator used by this PCI target; an ACPI-disabled system may leave the pressure sensor unbound. Feedback on whether that supply needs an explicit representation before v1 would be useful.

Ahmad Byagowi (4):
dt-bindings: leds: Add IS32FL3207 controller
leds: rgb: Add IS32FL3207 controller driver
i2c: mux: Propagate software nodes to channel adapters
ptp: ocp: Add R4006 I2C peripheral topology

 .../bindings/leds/issi,is32fl3207.yaml        | 204 +++++++
 MAINTAINERS                                   |   7 +
 drivers/i2c/i2c-mux.c                         |  55 ++
 drivers/leds/rgb/Kconfig                      |  11 +
 drivers/leds/rgb/Makefile                     |   1 +
 drivers/leds/rgb/leds-is32fl3207.c            | 541 ++++++++++++++++++
 drivers/ptp/ptp_ocp.c                         | 485 +++++++++++++++-
 7 files changed, 1278 insertions(+), 26 deletions(-)
 create mode 100644 Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml
 create mode 100644 drivers/leds/rgb/leds-is32fl3207.c

base-commit: 69963a0678a347d57c4ac8b16939dba216eb95ce

Signature · 1 line
2.50.1 (Apple Git-155)
#1 lore raw

RFC 1/4 dt-bindings: leds: Add IS32FL3207 controller

The IS32FL3207 is an 18-channel constant-current LED controller with per-output PWM and current scaling.

Describe I2C addressing, power and shutdown controls, RISET, and individual or multicolor output groupings. Require board current limits for each output.

---
 .../bindings/leds/issi,is32fl3207.yaml        | 204 ++++++++++++++++++
 MAINTAINERS                                   |   6 +
 2 files changed, 210 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml
Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml new file +204 -0
new file mode 100644
index 000000000..06a12cfb1
@@ -0,0 +1,204 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/leds/issi,is32fl3207.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Lumissil IS32FL3207 18-channel LED controller
+
+maintainers:
+  - Ahmad Byagowi <[email protected]>
+
+description: |
+  The IS32FL3207 is an I2C LED controller with 18 individually controlled
+  constant-current outputs. Each output has 8-bit current scaling and up to
+  16-bit PWM control. Outputs may be exposed as individual LEDs or grouped as
+  multicolor LEDs.
+
+  The data sheet is available at:
+    https://www.lumissil.com/assets/pdf/core/IS32FL3207_DS.pdf
+
+properties:
+  compatible:
+    const: issi,is32fl3207
+
+  reg:
+    enum: [0x34, 0x35, 0x36, 0x37]
+
+  vcc-supply:
+    description: Regulator providing power to the VCC pin.
+
+  enable-gpios:
+    maxItems: 1
+    description: GPIO connected to SDB, asserted to enable the controller.
+
+  issi,riset-ohms:
+    minimum: 2000
+    maximum: 76500000
+    description: |
+      Resistance in ohms of the external RISET resistor. The resistor sets the
+      maximum output current according to IOUT(MAX) = 76.5 / RISET amperes.
+
+  "#address-cells":
+    const: 1
+
+  "#size-cells":
+    const: 0
+
+patternProperties:
+  "^led@[0-9a-f]+$":
+    type: object
+    $ref: common.yaml#
+    unevaluatedProperties: false
+
+    properties:
+      reg:
+        minimum: 0
+        maximum: 17
+        description: LED output number.
+
+      led-max-microamp:
+        minimum: 1
+        maximum: 38250
+
+      color:
+        not:
+          enum: [8, 9]
+
+      max-brightness:
+        minimum: 1
+        maximum: 255
+
+      default-state:
+        const: "off"
+
+      default-brightness: false
+
+      retain-state-shutdown: false
+
+    required:
+      - reg
+      - led-max-microamp
+
+  "^multi-led@[0-9a-f]+$":
+    type: object
+    $ref: leds-class-multicolor.yaml#
+    unevaluatedProperties: false
+
+    properties:
+      reg:
+        minimum: 0
+        maximum: 17
+        description: Lowest-numbered LED output used by the group.
+
+      "#address-cells":
+        const: 1
+
+      "#size-cells":
+        const: 0
+
+      max-brightness:
+        minimum: 1
+        maximum: 255
+
+      default-state:
+        const: "off"
+
+      default-brightness: false
+
+      retain-state-shutdown: false
+
+    patternProperties:
+      "^led@[0-9a-f]+$":
+        type: object
+        $ref: common.yaml#
+        unevaluatedProperties: false
+
+        properties:
+          reg:
+            minimum: 0
+            maximum: 17
+            description: LED output number.
+
+          led-max-microamp:
+            minimum: 1
+            maximum: 38250
+
+          color:
+            not:
+              enum: [8, 9]
+
+          max-brightness: false
+
+          default-state: false
+
+          default-brightness: false
+
+          retain-state-shutdown: false
+
+        required:
+          - reg
+          - color
+          - led-max-microamp
+
+    required:
+      - reg
+      - "#address-cells"
+      - "#size-cells"
+
+required:
+  - compatible
+  - reg
+  - issi,riset-ohms
+  - "#address-cells"
+  - "#size-cells"
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/leds/common.h>
+
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        led-controller@34 {
+            compatible = "issi,is32fl3207";
+            reg = <0x34>;
+            #address-cells = <1>;
+            #size-cells = <0>;
+
+            vcc-supply = <&led_3v3>;
+            enable-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;
+            issi,riset-ohms = <4700>;
+
+            multi-led@0 {
+                reg = <0>;
+                color = <LED_COLOR_ID_RGB>;
+                function = LED_FUNCTION_STATUS;
+                #address-cells = <1>;
+                #size-cells = <0>;
+
+                led@0 {
+                    reg = <0>;
+                    color = <LED_COLOR_ID_RED>;
+                    led-max-microamp = <8100>;
+                };
+
+                led@1 {
+                    reg = <1>;
+                    color = <LED_COLOR_ID_GREEN>;
+                    led-max-microamp = <8100>;
+                };
+
+                led@2 {
+                    reg = <2>;
+                    color = <LED_COLOR_ID_BLUE>;
+                    led-max-microamp = <8100>;
+                };
+            };
+        };
+    };
+...
MAINTAINERS +6 -0
index 932ea1db0..ff3a93474 100644
@@ -13821,6 +13821,12 @@ F:	Documentation/devicetree/bindings/hwmon/renesas,isl28022.yaml
 F:	Documentation/hwmon/isl28022.rst
 F:	drivers/hwmon/isl28022.c
 
+ISSI IS32FL3207 LED DRIVER
+M:	Ahmad Byagowi <[email protected]>
+L:	[email protected]
+S:	Maintained
+F:	Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml
+
 ISOFS FILESYSTEM
 M:	Jan Kara <[email protected]>
 L:	[email protected]
Signature · 1 line
2.50.1 (Apple Git-155)
#2 lore raw

Re: [RFC net-next 1/4] dt-bindings: leds: Add IS32FL3207 controller

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider: - [Medium] Arbitrary restriction of standard LED properties (`default-state`, `default-brightness`…

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] Arbitrary restriction of standard LED properties (default-state, default-brightness, retain-state-shutdown).
- [Medium] The max-brightness limit contradicts the 16-bit PWM capability of the hardware.
- [Low] MAINTAINERS file entry breaks strict alphabetical ordering.
--

--- Patch [1]: [RFC net-next 1/4] dt-bindings: leds: Add IS32FL3207 controller --- commit 7a01cdde2c240d5fb659f162c75c91152d916f4e
Author: Ahmad Byagowi <[email protected]>

dt-bindings: leds: Add IS32FL3207 controller

This commit adds a new device tree binding for the Lumissil IS32FL3207 18-channel LED controller. It describes the controller properties, such as I2C addressing, output groupings, and current limits.

Quoted 5 lines
Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml new file +0 -0
new file mode 100644
@@ -0,0 +1,204 @@

[ ... ]

Quoted 13 lines
Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml new file +13 -0
+patternProperties:
+  "^led@[0-9a-f]+$":
+    type: object
+    $ref: common.yaml#
+    unevaluatedProperties: false
+
+    properties:
+      reg:
+        minimum: 0
+        maximum: 17
+        description: LED output number.
+
+      led-max-microamp:

Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml line 70

Quoted 10 lines
Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml new file +10 -0
+        minimum: 1
+        maximum: 38250
+
+      color:
+        not:
+          enum: [8, 9]
+
+      max-brightness:
+        minimum: 1
+        maximum: 255

[Severity: Medium]
Since the commit message states this hardware has up to 16-bit PWM control, does limiting max-brightness to 255 artificially restrict the hardware's PWM resolution? Should this allow values up to 65535 instead?

Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml line 77

Quoted 7 lines
Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml new file +7 -0
+
+      default-state:
+        const: "off"
+
+      default-brightness: false
+
+      retain-state-shutdown: false

[Severity: Medium]
Are these standard LED property restrictions necessary? Restricting default-state to off and disabling default-brightness and retain-state-shutdown entirely prevents device trees from configuring these LEDs to be on at boot or retain their state.

Quoted 15 lines
Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml new file +15 -0
+
+    required:
+      - reg
+      - led-max-microamp
+
+  "^multi-led@[0-9a-f]+$":
+    type: object
+    $ref: leds-class-multicolor.yaml#
+    unevaluatedProperties: false
+
+    properties:
+      reg:
+        minimum: 0
+        maximum: 17
+        description: Lowest-numbered LED output used by the group.

Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml line 102

Quoted 10 lines
Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml new file +10 -0
+
+      "#address-cells":
+        const: 1
+
+      "#size-cells":
+        const: 0
+
+      max-brightness:
+        minimum: 1
+        maximum: 255

[Severity: Medium]
Does this 8-bit limit also restrict the 16-bit PWM capabilities for multicolor LED groups?

Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml line 77

Quoted 7 lines
Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml new file +7 -0
+
+      default-state:
+        const: "off"
+
+      default-brightness: false
+
+      retain-state-shutdown: false

[Severity: Medium]
Should multicolor LEDs be permitted to use standard LED state configurations like default-brightness and retain-state-shutdown, similar to the standalone LEDs above?

[ ... ]

Quoted 4 lines

MAINTAINERS F: Documentation/devicetree/bindings/hwmon/renesas,isl28022.yaml · line 13830

Quoted 10 lines
MAINTAINERS +6 -0
@@ -13821,6 +13821,12 @@ F:	Documentation/devicetree/bindings/hwmon/renesas,isl28022.yaml
 F:	Documentation/hwmon/isl28022.rst
 F:	drivers/hwmon/isl28022.c
 
+ISSI IS32FL3207 LED DRIVER
+M:	Ahmad Byagowi <[email protected]>
+L:	[email protected]
+S:	Maintained
+F:	Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml
+
 ISOFS FILESYSTEM

[Severity: Low]
This isn't a bug, but does inserting ISSI before ISOFS break the strict alphabetical ordering of the MAINTAINERS file?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=1
#3 lore raw

RFC 2/4 leds: rgb: Add IS32FL3207 controller driver

In reply to Ahmad Byagowi

Add a generic I2C driver for the Lumissil IS32FL3207 18-channel LED controller.

Expose individual and multicolor LEDs through the LED class, use the default 8-bit 62 kHz PWM mode, derive output current from RISET, and enforce each output current limit with the scaling registers. Handle an optional supply and enable GPIO and serialize controller-wide update latches.

---
 MAINTAINERS                        |   1 +
 drivers/leds/rgb/Kconfig           |  11 +
 drivers/leds/rgb/Makefile          |   1 +
 drivers/leds/rgb/leds-is32fl3207.c | 541 +++++++++++++++++++++++++++++
 4 files changed, 554 insertions(+)
 create mode 100644 drivers/leds/rgb/leds-is32fl3207.c
MAINTAINERS +1 -0
index ff3a93474..459dd5147 100644
@@ -13826,6 +13826,7 @@ M:	Ahmad Byagowi <[email protected]>
 L:	[email protected]
 S:	Maintained
 F:	Documentation/devicetree/bindings/leds/issi,is32fl3207.yaml
+F:	drivers/leds/rgb/leds-is32fl3207.c
 
 ISOFS FILESYSTEM
 M:	Jan Kara <[email protected]>
drivers/leds/rgb/Kconfig +11 -0
index 6e9ab5f60..c896be131 100644
@@ -14,6 +14,17 @@ config LEDS_GROUP_MULTICOLOR
 	  To compile this driver as a module, choose M here: the module
 	  will be called leds-group-multicolor.
 
+config LEDS_IS32FL3207
+	tristate "LED support for ISSI IS32FL3207"
+	depends on I2C
+	select REGMAP_I2C
+	help
+	  Say Y here to include support for the Lumissil IS32FL3207
+	  18-channel I2C LED controller.
+
+	  To compile this driver as a module, choose M here: the module will
+	  be called leds-is32fl3207.
+
 config LEDS_KTD202X
 	tristate "LED support for KTD202x Chips"
 	depends on I2C
drivers/leds/rgb/Makefile +1 -0
index cc0f2df66..228923e8b 100644
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0
 
 obj-$(CONFIG_LEDS_GROUP_MULTICOLOR)	+= leds-group-multicolor.o
+obj-$(CONFIG_LEDS_IS32FL3207)		+= leds-is32fl3207.o
 obj-$(CONFIG_LEDS_KTD202X)		+= leds-ktd202x.o
 obj-$(CONFIG_LEDS_LP5812)		+= leds-lp5812.o
 obj-$(CONFIG_LEDS_LP5860_CORE)		+= leds-lp5860-core.o
drivers/leds/rgb/leds-is32fl3207.c new file +541 -0

541 lines not on this page · raw message

Signature · 1 line
2.50.1 (Apple Git-155)
#4 lore raw

Re: [RFC net-next 2/4] leds: rgb: Add IS32FL3207 controller driver

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Low] The i2c_device_id array is initialized without named initializers. --

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The i2c_device_id array is initialized without named initializers. --

--- Patch [2]: [RFC net-next 2/4] leds: rgb: Add IS32FL3207 controller driver --- commit 2f0ef1606e8fe9f37cd22e9360f765264557dd4d
Author: Ahmad Byagowi <[email protected]>

leds: rgb: Add IS32FL3207 controller driver

This commit adds a generic I2C driver for the Lumissil IS32FL3207 18-channel LED controller. It exposes individual and multicolor LEDs through the LED class, handling optional supply and enable GPIO.

Quoted 5 lines
drivers/leds/rgb/leds-is32fl3207.c new file +0 -0
new file mode 100644
index 0000000000000..8f04534644dce

[ ... ]

drivers/leds/rgb/leds-is32fl3207.c line 523

Quoted 8 lines
drivers/leds/rgb/leds-is32fl3207.c new file +8 -0
+static const struct of_device_id is32fl3207_of_match[] = {
+	{ .compatible = "issi,is32fl3207" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, is32fl3207_of_match);
+
+static const struct i2c_device_id is32fl3207_id[] = {
+	{ "is32fl3207" },

[Severity: Low]
This isn't a bug, but should this array use a named initializer (e.g., { .name = "is32fl3207" })? The I2C subsystem guidelines request that initialized arrays of type struct i2c_device_id use named initializers.

Quoted 3 lines
drivers/leds/rgb/leds-is32fl3207.c new file +3 -0
+	{ }
+};
+MODULE_DEVICE_TABLE(i2c, is32fl3207_id);

[ ... ]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=2
#5 lore raw

RFC 3/4 i2c: mux: Propagate software nodes to channel adapters

In reply to Ahmad Byagowi

Device Tree channel nodes are associated with the adapters created by i2c-mux, but equivalent software-node descriptions are not.

Find the software-node child whose reg value matches the channel and assign it to the new adapter. Keep a reference for the adapter lifetime so downstream clients can find the channel by firmware node.

---
 drivers/i2c/i2c-mux.c | 55 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 55 insertions(+)
drivers/i2c/i2c-mux.c +55 -0
index 681a201c2..1e587ea4e 100644
@@ -25,6 +25,7 @@
 #include <linux/kernel.h>
 #include <linux/module.h>
 #include <linux/of.h>
+#include <linux/property.h>
 #include <linux/slab.h>
 #include <linux/sysfs.h>
 
@@ -264,6 +265,49 @@ static const struct i2c_lock_operations i2c_parent_lock_ops = {
 	.unlock_bus =  i2c_parent_unlock_bus,
 };
 
+static struct fwnode_handle *
+i2c_mux_get_channel_swnode(struct i2c_mux_core *muxc, u32 chan_id)
+{
+	struct fwnode_handle *dev_node = dev_fwnode(muxc->dev);
+	struct fwnode_handle *mux_node, *child = NULL;
+	u32 reg;
+
+	if (!is_software_node(dev_node))
+		return NULL;
+
+	if (muxc->arbitrator)
+		mux_node = fwnode_get_named_child_node(dev_node, "i2c-arb");
+	else if (muxc->gate)
+		mux_node = fwnode_get_named_child_node(dev_node, "i2c-gate");
+	else
+		mux_node = fwnode_get_named_child_node(dev_node, "i2c-mux");
+
+	if (mux_node) {
+		/* A "reg" property indicates an old-style firmware entry. */
+		if (!fwnode_property_read_u32(mux_node, "reg", &reg)) {
+			fwnode_handle_put(mux_node);
+			mux_node = NULL;
+		}
+	}
+
+	if (!mux_node)
+		mux_node = fwnode_handle_get(dev_node);
+	else if (muxc->arbitrator || muxc->gate)
+		child = fwnode_handle_get(mux_node);
+
+	if (!child) {
+		fwnode_for_each_child_node(mux_node, child) {
+			if (fwnode_property_read_u32(child, "reg", &reg))
+				continue;
+			if (chan_id == reg)
+				break;
+		}
+	}
+
+	fwnode_handle_put(mux_node);
+	return child;
+}
+
 int i2c_mux_add_adapter(struct i2c_mux_core *muxc,
 			u32 force_nr, u32 chan_id)
 {
@@ -364,6 +408,12 @@ int i2c_mux_add_adapter(struct i2c_mux_core *muxc,
 
 		priv->adap.dev.of_node = child;
 		of_node_put(mux_node);
+	} else {
+		struct fwnode_handle *fwnode;
+
+		fwnode = i2c_mux_get_channel_swnode(muxc, chan_id);
+		if (fwnode)
+			device_set_node(&priv->adap.dev, fwnode);
 	}
 
 	/*
@@ -408,6 +458,8 @@ int i2c_mux_add_adapter(struct i2c_mux_core *muxc,
 	return 0;
 
 err_free_priv:
+	if (is_software_node(dev_fwnode(&priv->adap.dev)))
+		fwnode_handle_put(dev_fwnode(&priv->adap.dev));
 	kfree(priv);
 	return ret;
 }
@@ -420,6 +472,7 @@ void i2c_mux_del_adapters(struct i2c_mux_core *muxc)
 	while (muxc->num_adapters) {
 		struct i2c_adapter *adap = muxc->adapter[--muxc->num_adapters];
 		struct i2c_mux_priv *priv = adap->algo_data;
+		struct fwnode_handle *fwnode = dev_fwnode(&adap->dev);
 		struct device_node *np = adap->dev.of_node;
 
 		muxc->adapter[muxc->num_adapters] = NULL;
@@ -430,6 +483,8 @@ void i2c_mux_del_adapters(struct i2c_mux_core *muxc)
 
 		sysfs_remove_link(&priv->adap.dev.kobj, "mux_device");
 		i2c_del_adapter(adap);
+		if (is_software_node(fwnode))
+			fwnode_handle_put(fwnode);
 		of_node_put(np);
 		kfree(priv);
 	}
Signature · 1 line
2.50.1 (Apple Git-155)
#6 lore raw

Re: [RFC net-next 3/4] i2c: mux: Propagate software nodes to channel adapters

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 8 lines

Device Tree channel nodes are associated with the adapters created by i2c-mux, but equivalent software-node descriptions are not.

Find the software-node child whose reg value matches the channel and assign it to the new adapter. Keep a reference for the adapter lifetime so downstream clients can find the channel by firmware node.

Sashiko has reviewed this patch and found no issues. It looks great!

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=3
#7 lore raw

RFC 4/4 ptp: ocp: Add R4006 I2C peripheral topology

In reply to Ahmad Byagowi

R4006 cards place a PCA9546 mux behind the FPGA I2C controller. The mux exposes three LM75B temperature sensors, an SHT3x humidity sensor, an ICP10100 pressure sensor, and an IS32FL3207 controller for the GNSS and SMA LEDs.

Detect the board through the existing EEPROM nvmem path and describe its per-card topology with software nodes. Instantiate the standard I2C clients after their adapters appear, allowing the component drivers to bind normally. Leave the channel containing the BNO08x empty because there is no upstream driver for that device.

Serialize EEPROM discovery, retry topology population from I2C bus notifications, and atomically disable the delayed work during teardown. Limit the existing i2c sysfs link to the root adapter.

---
 drivers/ptp/ptp_ocp.c | 485 +++++++++++++++++++++++++++++++++++++++---
 1 file changed, 459 insertions(+), 26 deletions(-)
drivers/ptp/ptp_ocp.c +459 -26
index 35e911f1a..b362a7c4d 100644
@@ -21,10 +21,14 @@
 #include <net/devlink.h>
 #include <linux/i2c.h>
 #include <linux/mtd/mtd.h>
+#include <linux/mutex.h>
 #include <linux/nvmem-consumer.h>
+#include <linux/property.h>
 #include <linux/crc16.h>
 #include <linux/dpll.h>
 
+#include <dt-bindings/leds/common.h>
+
 #define PCI_DEVICE_ID_META_TIMECARD		0x0400
 
 #define PCI_VENDOR_ID_CELESTICA			0x18d4
@@ -347,6 +351,32 @@ struct ptp_ocp_serial_port {
 #define OCP_SIGNAL_NUM			4
 #define OCP_FREQ_NUM			4
 
+#define OCP_R4006_MUX_CHANNELS		4
+#define OCP_R4006_SENSOR_COUNT		5
+#define OCP_R4006_LED_COUNT		5
+#define OCP_R4006_LED_COMPONENT_COUNT	(3 * OCP_R4006_LED_COUNT)
+#define OCP_R4006_NODE_COUNT		(1 + OCP_R4006_MUX_CHANNELS + \
+					 OCP_R4006_SENSOR_COUNT + 1 + \
+					 OCP_R4006_LED_COUNT + \
+					 OCP_R4006_LED_COMPONENT_COUNT)
+
+struct ptp_ocp_r4006 {
+	struct software_node mux_node;
+	struct property_entry mux_props[6];
+	struct software_node channel_nodes[OCP_R4006_MUX_CHANNELS];
+	struct property_entry channel_props[OCP_R4006_MUX_CHANNELS][4];
+	struct software_node sensor_nodes[OCP_R4006_SENSOR_COUNT];
+	struct property_entry sensor_props[OCP_R4006_SENSOR_COUNT][3];
+	struct software_node led_node;
+	struct property_entry led_props[6];
+	struct software_node led_nodes[OCP_R4006_LED_COUNT];
+	struct property_entry led_group_props[OCP_R4006_LED_COUNT][7];
+	struct software_node component_nodes[OCP_R4006_LED_COMPONENT_COUNT];
+	struct property_entry component_props[OCP_R4006_LED_COMPONENT_COUNT][4];
+	const struct software_node *node_group[OCP_R4006_NODE_COUNT + 1];
+	bool registered;
+};
+
 enum {
 	PORT_GNSS,
 	PORT_GNSS2,
@@ -403,6 +433,7 @@ struct ptp_ocp {
 	u16			fw_version;
 	u8			board_id[OCP_BOARD_ID_LEN];
 	u8			serial[OCP_SERIAL_LEN];
+	struct mutex		eeprom_lock; /* Serializes EEPROM discovery. */
 	bool			has_eeprom_data;
 	u32			pps_req_map;
 	int			flash_start;
@@ -414,6 +445,8 @@ struct ptp_ocp {
 	const struct ocp_sma_op *sma_op;
 	struct dpll_device *dpll;
 	dpll_tracker tracker;
+	struct ptp_ocp_r4006 *r4006;
+	struct delayed_work i2c_work;
 	int signals_nr;
 	int freq_in_nr;
 };
@@ -488,6 +521,71 @@ static struct ptp_ocp_eeprom_map art_eeprom_map[] = {
 	{ }
 };
 
+struct ptp_ocp_r4006_i2c_device {
+	const char *node_name;
+	const char *compatible;
+	const char *type;
+	u8 channel;
+	u8 address;
+};
+
+/* Channel 3's BNO08x at 0x4a has no upstream Linux driver, so omit it. */
+static const struct ptp_ocp_r4006_i2c_device ptp_ocp_r4006_sensors[] = {
+	{ "temperature@48", "national,lm75b", "lm75b", 0, 0x48 },
+	{ "temperature@49", "national,lm75b", "lm75b", 0, 0x49 },
+	{ "temperature@4a", "national,lm75b", "lm75b", 0, 0x4a },
+	{ "humidity@44", "sensirion,sht3x", "sht3x", 1, 0x44 },
+	{ "pressure@63", "invensense,icp10100", "icp10100", 2, 0x63 },
+};
+
+static_assert(ARRAY_SIZE(ptp_ocp_r4006_sensors) == OCP_R4006_SENSOR_COUNT);
+
+struct ptp_ocp_r4006_led {
+	const char *node_name;
+	const char *function;
+	u8 function_enumerator;
+	bool has_function_enumerator;
+	u8 channel[3];
+};
+
+static const struct ptp_ocp_r4006_led ptp_ocp_r4006_leds[] = {
+	{
+		.node_name = "multi-led@c",
+		.function = LED_FUNCTION_STATUS,
+		.channel = { 13, 12, 14 },
+	},
+	{
+		.node_name = "multi-led@6",
+		.function = LED_FUNCTION_INDICATOR,
+		.function_enumerator = 1,
+		.has_function_enumerator = true,
+		.channel = { 7, 6, 8 },
+	},
+	{
+		.node_name = "multi-led@9",
+		.function = LED_FUNCTION_INDICATOR,
+		.function_enumerator = 2,
+		.has_function_enumerator = true,
+		.channel = { 10, 9, 11 },
+	},
+	{
+		.node_name = "multi-led@0",
+		.function = LED_FUNCTION_INDICATOR,
+		.function_enumerator = 3,
+		.has_function_enumerator = true,
+		.channel = { 1, 0, 2 },
+	},
+	{
+		.node_name = "multi-led@3",
+		.function = LED_FUNCTION_INDICATOR,
+		.function_enumerator = 4,
+		.has_function_enumerator = true,
+		.channel = { 4, 3, 5 },
+	},
+};
+
+static_assert(ARRAY_SIZE(ptp_ocp_r4006_leds) == OCP_R4006_LED_COUNT);
+
 #define bp_assign_entry(bp, res, val) ({				\
 	uintptr_t addr = (uintptr_t)(bp) + (res)->bp_offset;		\
 	*(typeof(val) *)addr = val;					\
@@ -1969,6 +2067,12 @@ ptp_ocp_nvmem_device_put(struct nvmem_device **nvmemp)
 	*nvmemp = NULL;
 }
 
+static bool
+ptp_ocp_has_eeprom_data(struct ptp_ocp *bp)
+{
+	return smp_load_acquire(&bp->has_eeprom_data);
+}
+
 static void
 ptp_ocp_read_eeprom(struct ptp_ocp *bp)
 {
@@ -1977,11 +2081,11 @@ ptp_ocp_read_eeprom(struct ptp_ocp *bp)
 	const void *tag;
 	int ret;
 
-	if (!bp->i2c_ctrl)
-		return;
-
 	tag = NULL;
 	nvmem = NULL;
+	mutex_lock(&bp->eeprom_lock);
+	if (!bp->i2c_ctrl || ptp_ocp_has_eeprom_data(bp))
+		goto out;
 
 	for (map = bp->eeprom_map; map->len; map++) {
 		if (map->tag != tag) {
@@ -2001,10 +2105,12 @@ ptp_ocp_read_eeprom(struct ptp_ocp *bp)
 			goto fail;
 	}
 
-	bp->has_eeprom_data = true;
+	/* Publish the EEPROM fields before readers observe valid data. */
+	smp_store_release(&bp->has_eeprom_data, true);
 
 out:
 	ptp_ocp_nvmem_device_put(&nvmem);
+	mutex_unlock(&bp->eeprom_lock);
 	return;
 
 fail:
@@ -2012,6 +2118,291 @@ ptp_ocp_read_eeprom(struct ptp_ocp *bp)
 	goto out;
 }
 
+static int
+ptp_ocp_i2c_adapter_match(struct device *dev, const void *data)
+{
+	return !!i2c_verify_adapter(dev);
+}
+
+static struct i2c_adapter *
+ptp_ocp_i2c_root_adapter(struct ptp_ocp *bp)
+{
+	struct i2c_adapter *adapter;
+	struct device *dev;
+
+	if (!bp->i2c_ctrl)
+		return NULL;
+
+	dev = device_find_child(&bp->i2c_ctrl->dev, NULL,
+				ptp_ocp_i2c_adapter_match);
+	if (!dev)
+		return NULL;
+
+	adapter = i2c_verify_adapter(dev);
+	if (!adapter || !try_module_get(adapter->owner)) {
+		put_device(dev);
+		return NULL;
+	}
+
+	/* The device reference from device_find_child() is owned by the caller. */
+	return adapter;
+}
+
+static int
+ptp_ocp_r4006_init_nodes(struct ptp_ocp *bp)
+{
+	static const char * const channel_names[] = {
+		"i2c@0", "i2c@1", "i2c@2", "i2c@3",
+	};
+	static const char * const output_names[] = {
+		"led@0", "led@1", "led@2", "led@3", "led@4", "led@5",
+		"led@6", "led@7", "led@8", "led@9", "led@a", "led@b",
+		"led@c", "led@d", "led@e", "led@f", "led@10", "led@11",
+	};
+	static const u32 colors[] = {
+		LED_COLOR_ID_RED, LED_COLOR_ID_GREEN, LED_COLOR_ID_BLUE,
+	};
+	struct device *dev = &bp->pdev->dev;
+	struct ptp_ocp_r4006 *r4006;
+	unsigned int component = 0;
+	unsigned int node = 0;
+	unsigned int i, j, prop;
+	int ret;
+
+	if (bp->r4006)
+		return 0;
+
+	r4006 = devm_kzalloc(dev, sizeof(*r4006), GFP_KERNEL);
+	if (!r4006)
+		return -ENOMEM;
+
+	r4006->mux_node.name =
+		devm_kasprintf(dev, GFP_KERNEL, "ocp%d-r4006-mux", bp->id);
+	if (!r4006->mux_node.name)
+		return -ENOMEM;
+	r4006->mux_node.properties = r4006->mux_props;
+	r4006->mux_props[0] =
+		PROPERTY_ENTRY_STRING("compatible", "nxp,pca9546");
+	r4006->mux_props[1] = PROPERTY_ENTRY_U32("reg", 0x70);
+	r4006->mux_props[2] = PROPERTY_ENTRY_BOOL("i2c-mux-idle-disconnect");
+	r4006->mux_props[3] = PROPERTY_ENTRY_U32("#address-cells", 1);
+	r4006->mux_props[4] = PROPERTY_ENTRY_U32("#size-cells", 0);
+	r4006->node_group[node++] = &r4006->mux_node;
+
+	for (i = 0; i < OCP_R4006_MUX_CHANNELS; i++) {
+		r4006->channel_nodes[i].name = channel_names[i];
+		r4006->channel_nodes[i].parent = &r4006->mux_node;
+		r4006->channel_nodes[i].properties = r4006->channel_props[i];
+		r4006->channel_props[i][0] = PROPERTY_ENTRY_U32("reg", i);
+		r4006->channel_props[i][1] = PROPERTY_ENTRY_U32("#address-cells", 1);
+		r4006->channel_props[i][2] = PROPERTY_ENTRY_U32("#size-cells", 0);
+		r4006->node_group[node++] = &r4006->channel_nodes[i];
+	}
+
+	for (i = 0; i < ARRAY_SIZE(ptp_ocp_r4006_sensors); i++) {
+		const struct ptp_ocp_r4006_i2c_device *sensor;
+
+		sensor = &ptp_ocp_r4006_sensors[i];
+		r4006->sensor_nodes[i].name = sensor->node_name;
+		r4006->sensor_nodes[i].parent =
+			&r4006->channel_nodes[sensor->channel];
+		r4006->sensor_nodes[i].properties = r4006->sensor_props[i];
+		r4006->sensor_props[i][0] =
+			PROPERTY_ENTRY_STRING("compatible", sensor->compatible);
+		r4006->sensor_props[i][1] =
+			PROPERTY_ENTRY_U32("reg", sensor->address);
+		r4006->node_group[node++] = &r4006->sensor_nodes[i];
+	}
+
+	r4006->led_node.name = "led-controller@34";
+	r4006->led_node.parent = &r4006->channel_nodes[1];
+	r4006->led_node.properties = r4006->led_props;
+	r4006->led_props[0] =
+		PROPERTY_ENTRY_STRING("compatible", "issi,is32fl3207");
+	r4006->led_props[1] = PROPERTY_ENTRY_U32("reg", 0x34);
+	r4006->led_props[2] = PROPERTY_ENTRY_U32("issi,riset-ohms", 4700);
+	r4006->led_props[3] = PROPERTY_ENTRY_U32("#address-cells", 1);
+	r4006->led_props[4] = PROPERTY_ENTRY_U32("#size-cells", 0);
+	r4006->node_group[node++] = &r4006->led_node;
+
+	for (i = 0; i < ARRAY_SIZE(ptp_ocp_r4006_leds); i++) {
+		const struct ptp_ocp_r4006_led *led = &ptp_ocp_r4006_leds[i];
+		u32 group_reg;
+
+		group_reg = min3(led->channel[0], led->channel[1],
+				 led->channel[2]);
+		r4006->led_nodes[i].name = led->node_name;
+		r4006->led_nodes[i].parent = &r4006->led_node;
+		r4006->led_nodes[i].properties = r4006->led_group_props[i];
+		prop = 0;
+		r4006->led_group_props[i][prop++] =
+			PROPERTY_ENTRY_U32("reg", group_reg);
+		r4006->led_group_props[i][prop++] =
+			PROPERTY_ENTRY_U32("color", LED_COLOR_ID_RGB);
+		r4006->led_group_props[i][prop++] =
+			PROPERTY_ENTRY_STRING("function", led->function);
+		if (led->has_function_enumerator)
+			r4006->led_group_props[i][prop++] =
+				PROPERTY_ENTRY_U32("function-enumerator",
+						   led->function_enumerator);
+		r4006->led_group_props[i][prop++] =
+			PROPERTY_ENTRY_U32("#address-cells", 1);
+		r4006->led_group_props[i][prop] =
+			PROPERTY_ENTRY_U32("#size-cells", 0);
+		r4006->node_group[node++] = &r4006->led_nodes[i];
+
+		for (j = 0; j < ARRAY_SIZE(led->channel); j++, component++) {
+			u8 channel = led->channel[j];
+
+			r4006->component_nodes[component].name =
+				output_names[channel];
+			r4006->component_nodes[component].parent =
+				&r4006->led_nodes[i];
+			r4006->component_nodes[component].properties =
+				r4006->component_props[component];
+			r4006->component_props[component][0] =
+				PROPERTY_ENTRY_U32("reg", channel);
+			r4006->component_props[component][1] =
+				PROPERTY_ENTRY_U32("color", colors[j]);
+			r4006->component_props[component][2] =
+				PROPERTY_ENTRY_U32("led-max-microamp", 8150);
+			r4006->node_group[node++] =
+				&r4006->component_nodes[component];
+		}
+	}
+
+	if (WARN_ON(node != OCP_R4006_NODE_COUNT))
+		return -EINVAL;
+
+	ret = software_node_register_node_group(r4006->node_group);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to register R4006 firmware nodes\n");
+
+	r4006->registered = true;
+	bp->r4006 = r4006;
+	return 0;
+}
+
+static int
+ptp_ocp_r4006_add_device(struct ptp_ocp *bp, struct i2c_adapter *adapter,
+			 const struct software_node *node, const char *type,
+			 u16 address)
+{
+	struct fwnode_handle *fwnode = software_node_fwnode(node);
+	struct i2c_board_info info = { };
+	struct i2c_client *client;
+
+	client = i2c_find_device_by_fwnode(fwnode);
+	if (client) {
+		put_device(&client->dev);
+		return 0;
+	}
+
+	strscpy(info.type, type, sizeof(info.type));
+	info.addr = address;
+	info.fwnode = fwnode;
+	client = i2c_new_client_device(adapter, &info);
+	if (IS_ERR(client))
+		return dev_err_probe(&bp->pdev->dev, PTR_ERR(client),
+				     "failed to add %s at %d-00%02x\n",
+				     type, i2c_adapter_id(adapter), address);
+
+	return 0;
+}
+
+static void
+ptp_ocp_r4006_populate_channel(struct ptp_ocp *bp, unsigned int channel)
+{
+	struct ptp_ocp_r4006 *r4006 = bp->r4006;
+	const struct software_node *node;
+	struct fwnode_handle *fwnode;
+	struct i2c_adapter *adapter;
+	unsigned int i;
+
+	fwnode = software_node_fwnode(&r4006->channel_nodes[channel]);
+	adapter = i2c_get_adapter_by_fwnode(fwnode);
+	if (!adapter)
+		return;
+
+	for (i = 0; i < ARRAY_SIZE(ptp_ocp_r4006_sensors); i++) {
+		const struct ptp_ocp_r4006_i2c_device *sensor;
+
+		sensor = &ptp_ocp_r4006_sensors[i];
+		if (sensor->channel != channel)
+			continue;
+
+		node = &r4006->sensor_nodes[i];
+		ptp_ocp_r4006_add_device(bp, adapter, node, sensor->type,
+					 sensor->address);
+	}
+
+	if (channel == 1)
+		ptp_ocp_r4006_add_device(bp, adapter, &r4006->led_node,
+					 "is32fl3207", 0x34);
+
+	i2c_put_adapter(adapter);
+}
+
+static void
+ptp_ocp_i2c_work(struct work_struct *work)
+{
+	struct ptp_ocp *bp = container_of(work, struct ptp_ocp, i2c_work.work);
+	const struct software_node *node;
+	struct i2c_adapter *adapter;
+	unsigned int channel;
+	int ret;
+
+	if (!bp->eeprom_map)
+		return;
+
+	adapter = ptp_ocp_i2c_root_adapter(bp);
+	if (!adapter)
+		return;
+
+	if (!ptp_ocp_has_eeprom_data(bp))
+		ptp_ocp_read_eeprom(bp);
+	if (!ptp_ocp_has_eeprom_data(bp) || memcmp(bp->board_id, "R4006", 5))
+		goto out_put_adapter;
+
+	ret = ptp_ocp_r4006_init_nodes(bp);
+	if (ret)
+		goto out_put_adapter;
+
+	node = &bp->r4006->mux_node;
+	ret = ptp_ocp_r4006_add_device(bp, adapter, node, "pca9546", 0x70);
+	if (ret)
+		goto out_put_adapter;
+
+	for (channel = 0; channel < OCP_R4006_MUX_CHANNELS; channel++)
+		ptp_ocp_r4006_populate_channel(bp, channel);
+
+out_put_adapter:
+	i2c_put_adapter(adapter);
+}
+
+static void
+ptp_ocp_r4006_unregister(struct ptp_ocp *bp)
+{
+	struct fwnode_handle *fwnode;
+	struct i2c_client *client;
+
+	disable_delayed_work_sync(&bp->i2c_work);
+	if (!bp->r4006)
+		return;
+
+	fwnode = software_node_fwnode(&bp->r4006->mux_node);
+	client = i2c_find_device_by_fwnode(fwnode);
+	if (client) {
+		i2c_unregister_device(client);
+		put_device(&client->dev);
+	}
+
+	if (bp->r4006->registered)
+		software_node_unregister_node_group(bp->r4006->node_group);
+	bp->r4006 = NULL;
+}
+
 static struct device *
 ptp_ocp_find_flash(struct ptp_ocp *bp)
 {
@@ -2166,9 +2557,9 @@ ptp_ocp_devlink_info_get(struct devlink *devlink, struct devlink_info_req *req,
 	if (err)
 		return err;
 
-	if (!bp->has_eeprom_data) {
+	if (!ptp_ocp_has_eeprom_data(bp)) {
 		ptp_ocp_read_eeprom(bp);
-		if (!bp->has_eeprom_data)
+		if (!ptp_ocp_has_eeprom_data(bp))
 			return 0;
 	}
 
@@ -3756,7 +4147,7 @@ serialnum_show(struct device *dev, struct device_attribute *attr, char *buf)
 {
 	struct ptp_ocp *bp = dev_get_drvdata(dev);
 
-	if (!bp->has_eeprom_data)
+	if (!ptp_ocp_has_eeprom_data(bp))
 		ptp_ocp_read_eeprom(bp);
 
 	return sysfs_emit(buf, "%pM\n", bp->serial);
@@ -4727,6 +5118,7 @@ ptp_ocp_device_init(struct ptp_ocp *bp, struct pci_dev *pdev)
 
 	bp->ptp_info = ptp_ocp_clock_info;
 	spin_lock_init(&bp->lock);
+	mutex_init(&bp->eeprom_lock);
 
 	for (i = 0; i < __PORT_COUNT; i++)
 		bp->port[i].line = -1;
@@ -4865,6 +5257,7 @@ ptp_ocp_detach(struct ptp_ocp *bp)
 {
 	int i;
 
+	ptp_ocp_r4006_unregister(bp);
 	ptp_ocp_debugfs_remove_device(bp);
 	ptp_ocp_detach_sysfs(bp);
 	ptp_ocp_attr_group_del(bp);
@@ -5079,6 +5472,7 @@ ptp_ocp_probe(struct pci_dev *pdev, const struct pci_device_id *id)
 		goto out_disable;
 
 	INIT_DELAYED_WORK(&bp->sync_work, ptp_ocp_sync_work);
+	INIT_DELAYED_WORK(&bp->i2c_work, ptp_ocp_i2c_work);
 
 	/* compat mode.
 	 * Older FPGA firmware only returns 2 irq's.
@@ -5096,6 +5490,7 @@ ptp_ocp_probe(struct pci_dev *pdev, const struct pci_device_id *id)
 	err = ptp_ocp_register_resources(bp, id->driver_data);
 	if (err)
 		goto out;
+	mod_delayed_work(system_wq, &bp->i2c_work, 1);
 
 	bp->ptp = ptp_clock_register(&bp->ptp_info, &pdev->dev);
 	if (IS_ERR(bp->ptp)) {
@@ -5189,38 +5584,76 @@ static struct pci_driver ptp_ocp_driver = {
 	.shutdown	= ptp_ocp_remove,
 };
 
+static struct ptp_ocp *
+ptp_ocp_i2c_parent(struct device *child)
+{
+	struct device *dev = child;
+
+	while ((dev = dev->parent))
+		if (dev->driver && !strcmp(dev->driver->name, KBUILD_MODNAME))
+			return dev_get_drvdata(dev);
+
+	return NULL;
+}
+
+static bool
+ptp_ocp_i2c_is_root(struct ptp_ocp *bp, struct i2c_adapter *adapter)
+{
+	struct device *parent = adapter->dev.parent;
+
+	if (!parent)
+		return false;
+	if (bp->i2c_ctrl && parent == &bp->i2c_ctrl->dev)
+		return true;
+
+	/* The adapter can notify before ptp_ocp_register_i2c() stores it. */
+	return parent->parent == &bp->pdev->dev;
+}
+
 static int
 ptp_ocp_i2c_notifier_call(struct notifier_block *nb,
 			  unsigned long action, void *data)
 {
-	struct device *dev, *child = data;
+	struct i2c_adapter *adapter;
+	struct i2c_client *client;
+	struct device *child = data;
 	struct ptp_ocp *bp;
-	bool add;
 
 	switch (action) {
 	case BUS_NOTIFY_ADD_DEVICE:
-	case BUS_NOTIFY_DEL_DEVICE:
-		add = action == BUS_NOTIFY_ADD_DEVICE;
-		break;
-	default:
+		adapter = i2c_verify_adapter(child);
+		if (!adapter)
+			return 0;
+		bp = ptp_ocp_i2c_parent(child);
+		if (!bp)
+			return 0;
+		if (ptp_ocp_i2c_is_root(bp, adapter))
+			ptp_ocp_symlink(bp, child, "i2c");
+		mod_delayed_work(system_wq, &bp->i2c_work, 1);
 		return 0;
-	}
 
-	if (!i2c_verify_adapter(child))
+	case BUS_NOTIFY_DEL_DEVICE:
+		adapter = i2c_verify_adapter(child);
+		if (!adapter)
+			return 0;
+		bp = ptp_ocp_i2c_parent(child);
+		if (bp && ptp_ocp_i2c_is_root(bp, adapter))
+			sysfs_remove_link(&bp->dev.kobj, "i2c");
 		return 0;
 
-	dev = child;
-	while ((dev = dev->parent))
-		if (dev->driver && !strcmp(dev->driver->name, KBUILD_MODNAME))
-			goto found;
-	return 0;
+	case BUS_NOTIFY_BOUND_DRIVER:
+		client = i2c_verify_client(child);
+		if (!client || client->addr != 0x50)
+			return 0;
+		bp = ptp_ocp_i2c_parent(child);
+		if (!bp || !ptp_ocp_i2c_is_root(bp, client->adapter))
+			return 0;
+		mod_delayed_work(system_wq, &bp->i2c_work, 1);
+		return 0;
 
-found:
-	bp = dev_get_drvdata(dev);
-	if (add)
-		ptp_ocp_symlink(bp, child, "i2c");
-	else
-		sysfs_remove_link(&bp->dev.kobj, "i2c");
+	default:
+		return 0;
+	}
 
 	return 0;
 }
Signature · 1 line
2.50.1 (Apple Git-155)
#8 lore raw

Re: [RFC net-next 4/4] ptp: ocp: Add R4006 I2C peripheral topology

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

New issues:
- [Medium] Repeated failures in firmware node registration inside ptp_ocp_r4006_init_nodes will leak devm-allocated memory because the function is called from a retriable workqueue.

Pre-existing issues:
- [High] Lockless access to dev->driver in the global I2C notifier parent tree traversal creates a TOCTOU race condition leading to a NULL pointer dereference. --

--- Patch [4]: [RFC net-next 4/4] ptp: ocp: Add R4006 I2C peripheral topology --- commit 6482a6be1fc3b33821906c9b60253b2b40290bb4
Author: Ahmad Byagowi <[email protected]>

ptp: ocp: Add R4006 I2C peripheral topology

This commit adds support for the R4006 I2C peripheral topology, detecting the board via EEPROM and describing the topology with software nodes. It registers standard I2C clients upon adapter appearance and handles I2C bus notifications for dynamic population.

Quoted 4 lines
drivers/ptp/ptp_ocp.c +0 -0
index 35e911f1ad78c..b362a7c4dbb5f 100644

[ ... ]

Quoted 3 lines
drivers/ptp/ptp_ocp.c +3 -0
+static int
+ptp_ocp_r4006_init_nodes(struct ptp_ocp *bp)
+{

[ ... ]

Quoted 6 lines
drivers/ptp/ptp_ocp.c +6 -0
+	r4006 = devm_kzalloc(dev, sizeof(*r4006), GFP_KERNEL);
+	if (!r4006)
+		return -ENOMEM;
+
+	r4006->mux_node.name =
+		devm_kasprintf(dev, GFP_KERNEL, "ocp%d-r4006-mux", bp->id);

[ ... ]

drivers/ptp/ptp_ocp.c ptp_ocp_read_eeprom(struct ptp_ocp *bp) · line 2285

Quoted 9 lines
drivers/ptp/ptp_ocp.c +9 -0
+	ret = software_node_register_node_group(r4006->node_group);
+	if (ret)
+		return dev_err_probe(dev, ret,
+				     "failed to register R4006 firmware nodes\n");
+
+	r4006->registered = true;
+	bp->r4006 = r4006;
+	return 0;
+}

[Severity: Medium]
Will this leak devm-allocated memory on failure?

Since ptp_ocp_r4006_init_nodes() is called from ptp_ocp_i2c_work() (which can be dynamically re-triggered by I2C bus notifications), any deterministic failure in software_node_register_node_group() will cause an early return without freeing the memory allocated via devm_kzalloc() and devm_kasprintf() earlier in this function. Could this repeatedly leak memory on the device's devres list with each worker execution?

[ ... ]

drivers/ptp/ptp_ocp.c static struct pci_driver ptp_ocp_driver = { · line 5597

Quoted 11 lines
drivers/ptp/ptp_ocp.c +11 -0
+static struct ptp_ocp *
+ptp_ocp_i2c_parent(struct device *child)
+{
+	struct device *dev = child;
+
+	while ((dev = dev->parent))
+		if (dev->driver && !strcmp(dev->driver->name, KBUILD_MODNAME))
+			return dev_get_drvdata(dev);
+
+	return NULL;
+}

[Severity: High]
This is a pre-existing issue, but does this lockless check of dev->driver risk a NULL pointer dereference?

If a parent device is concurrently unbinding while this global I2C notifier is traversing the tree, the driver core clears its dev->driver pointer. If dev->driver is evaluated as non-NULL but becomes NULL right before strcmp() accesses dev->driver->name, it could crash the kernel.

Should we be taking the device_lock() before evaluating dev->driver?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/[email protected]?part=4