Linux Hardware Monitor development
 help / color / mirror / Atom feed
* [PATCH v4 0/3] hwmon: pmbus: add MPS MPQ8646 support
@ 2026-07-23 21:38 Vincent Jardin via B4 Relay
  2026-07-23 21:38 ` [PATCH v4 1/3] hwmon: pmbus: event notification with alarms Vincent Jardin via B4 Relay
                   ` (2 more replies)
  0 siblings, 3 replies; 7+ messages in thread
From: Vincent Jardin via B4 Relay @ 2026-07-23 21:38 UTC (permalink / raw)
  To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Jonathan Corbet, Shuah Khan
  Cc: linux-hwmon, linux-kernel, devicetree, linux-doc, Vincent Jardin

Add support for the Monolithic Power Systems MPQ8646 step-down
converter as a PMBus device.

  1/3 pmbus core: add and export pmbus_get_hwmon_device() so a chip
      driver can call hwmon_notify_event() from an in-driver
      alarm-poll fallback (lm90-style) on boards where the chip's
      SMBALERT# pin is unavailable to the CPU.
  2/3 dt-bindings: the MPQ8646 binding; schema inspired from the
      mpq8785 (same mps,vout-fb-divider-ratio-permille property).
  3/3 the MPQ8646 driver: PMBus telemetry plus MFR_CFG_EXT
      gate-close retry after CLEAR_LAST_FAULT, alarm acknowledge via
      inX_reset_history, MPS-extended STATUS_WORD decode and
      post-mortem clear, the alarm-poll fallback for boards without
      SMBALERT, and on_off_config / vout_margin / mfr_pmbus_lock
      sysfs.

Signed-off-by: Vincent Jardin <vjardin@free.fr>
---
Changes in v4 (all reported by the Sashiko AI review on v3):
- driver: register debugfs only after a successful pmbus_do_probe() --
  the debugfs handlers take pmbus_lock() and rely on the pmbus clientdata
  that pmbus_do_probe() installs (NULL-deref window in v3)
- driver: in remove(), unregister debugfs before cancel_delayed_work_sync()
  so a debugfs write cannot re-arm the poll worker after it is cancelled
- driver: serialise the shared debugfs root create against concurrent
  probes with a static mutex
- driver: move the remaining last_probe_* updates (clear_protection_last,
  store_all, restore_all) inside mps_lock
- dt-bindings: describe the hardware, not the driver, in the description
- Link to v3: https://lore.kernel.org/r/20260723-mpq8646_v0-v3-0-1af1f3d38848@free.fr

Changes in v3 (all reported by the Sashiko AI review on v2):
- driver: alarm_poll_interval_ms is now a debugfs fops
- driver: pmbus_lock() around the async raw i2c accesses: the poll
  worker and the CLEAR_LAST_FAULT force sequence, the nvmem snapshot read
  keeps mps_lock (read-only, no chip-state change)
- driver: update last_probe_rc/last_probe_data under mps_lock to avoid
  torn diagnostics
- driver: remove the dead PMBUS_PAGE swallow in write_byte
- driver: remove the probe_page_write debugfs hook
- Link to v2: https://lore.kernel.org/r/20260723-mpq8646_v0-v2-0-3c4cb71f23c0@free.fr

Changes in v2:
- driver: register debugfs only after the DT-property validation, to
  avoid an early probe error to avoid dangling debugfs entries
- driver: do not schedule the alarm-poll worker when SMBALERT# (irq)
  is wired, and do not re-arm it from the worker in that case
- driver: dput() the dentry returned by debugfs_lookup()
- driver: hold mps_lock around the nvmem snapshot reads
- driver: fix VID coefficients comment
- dt-bindings: fix typo of the commit message
- Link to v1: https://lore.kernel.org/r/20260723-mpq8646_v0-v1-0-14363c75916f@free.fr

---
Vincent Jardin (3):
      hwmon: pmbus: event notification with alarms
      dt-bindings: hwmon: pmbus: add MPS MPQ8646 binding
      hwmon: pmbus: add MPQ8646 driver

 .../bindings/hwmon/pmbus/mps,mpq8646.yaml          |   50 +
 Documentation/hwmon/mpq8646.rst                    |  314 +++++
 MAINTAINERS                                        |    8 +
 drivers/hwmon/pmbus/Kconfig                        |   11 +
 drivers/hwmon/pmbus/Makefile                       |    1 +
 drivers/hwmon/pmbus/mpq8646.c                      | 1237 ++++++++++++++++++++
 drivers/hwmon/pmbus/pmbus.h                        |    1 +
 drivers/hwmon/pmbus/pmbus_core.c                   |    8 +
 8 files changed, 1630 insertions(+)
---
base-commit: 248951ddc14de84de3910f9b13f51491a8cd91df
change-id: 20260723-mpq8646_v0-3383cb574d7a

Best regards,
-- 
Vincent Jardin <vjardin@free.fr>



^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v4 1/3] hwmon: pmbus: event notification with alarms
  2026-07-23 21:38 [PATCH v4 0/3] hwmon: pmbus: add MPS MPQ8646 support Vincent Jardin via B4 Relay
@ 2026-07-23 21:38 ` Vincent Jardin via B4 Relay
  2026-07-23 21:50   ` sashiko-bot
  2026-07-23 21:38 ` [PATCH v4 2/3] dt-bindings: hwmon: pmbus: add MPS MPQ8646 binding Vincent Jardin via B4 Relay
  2026-07-23 21:38 ` [PATCH v4 3/3] hwmon: pmbus: add MPQ8646 driver Vincent Jardin via B4 Relay
  2 siblings, 1 reply; 7+ messages in thread
From: Vincent Jardin via B4 Relay @ 2026-07-23 21:38 UTC (permalink / raw)
  To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Jonathan Corbet, Shuah Khan
  Cc: linux-hwmon, linux-kernel, devicetree, linux-doc, Vincent Jardin

From: Vincent Jardin <vjardin@free.fr>

Let's add an accessor for the hwmon class device the pmbus core
registered for an i2c_client. Export it with PMBUS so chip
drivers can call hwmon_notify_event() on per-sensor *_alarm
attributes from the driver's work items.

The needs: for boards when the chip's SMBALERT# pin is
not available for the CPU, pmbus_irq_setup() cannot deliver
poll(POLLPRI) wakes or udev change@... events on the inX_alarm
files. So the drivers can install a delayed_work-based polling
fallback (like the lm90 driver) that periodically reads STATUS_WORD
and calls hwmon_notify_event() on 0->1 transitions.

Signed-off-by: Vincent Jardin <vjardin@free.fr>
---
 drivers/hwmon/pmbus/pmbus.h      | 1 +
 drivers/hwmon/pmbus/pmbus_core.c | 8 ++++++++
 2 files changed, 9 insertions(+)

diff --git a/drivers/hwmon/pmbus/pmbus.h b/drivers/hwmon/pmbus/pmbus.h
index 23e3eda58870..46ab056c0058 100644
--- a/drivers/hwmon/pmbus/pmbus.h
+++ b/drivers/hwmon/pmbus/pmbus.h
@@ -565,6 +565,7 @@ bool pmbus_check_word_register(struct i2c_client *client, int page, int reg);
 int pmbus_do_probe(struct i2c_client *client, struct pmbus_driver_info *info);
 const struct pmbus_driver_info *pmbus_get_driver_info(struct i2c_client
 						      *client);
+struct device *pmbus_get_hwmon_device(struct i2c_client *client);
 int pmbus_get_fan_rate_device(struct i2c_client *client, int page, int id,
 			      enum pmbus_fan_mode mode);
 int pmbus_get_fan_rate_cached(struct i2c_client *client, int page, int id,
diff --git a/drivers/hwmon/pmbus/pmbus_core.c b/drivers/hwmon/pmbus/pmbus_core.c
index 3143b9e0316c..6ba00ad73297 100644
--- a/drivers/hwmon/pmbus/pmbus_core.c
+++ b/drivers/hwmon/pmbus/pmbus_core.c
@@ -695,6 +695,14 @@ const struct pmbus_driver_info *pmbus_get_driver_info(struct i2c_client *client)
 }
 EXPORT_SYMBOL_NS_GPL(pmbus_get_driver_info, "PMBUS");
 
+struct device *pmbus_get_hwmon_device(struct i2c_client *client)
+{
+	struct pmbus_data *data = i2c_get_clientdata(client);
+
+	return data->hwmon_dev;
+}
+EXPORT_SYMBOL_NS_GPL(pmbus_get_hwmon_device, "PMBUS");
+
 static int pmbus_get_status(struct i2c_client *client, int page, int reg)
 {
 	struct pmbus_data *data = i2c_get_clientdata(client);

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v4 2/3] dt-bindings: hwmon: pmbus: add MPS MPQ8646 binding
  2026-07-23 21:38 [PATCH v4 0/3] hwmon: pmbus: add MPS MPQ8646 support Vincent Jardin via B4 Relay
  2026-07-23 21:38 ` [PATCH v4 1/3] hwmon: pmbus: event notification with alarms Vincent Jardin via B4 Relay
@ 2026-07-23 21:38 ` Vincent Jardin via B4 Relay
  2026-07-23 21:45   ` sashiko-bot
  2026-07-23 21:38 ` [PATCH v4 3/3] hwmon: pmbus: add MPQ8646 driver Vincent Jardin via B4 Relay
  2 siblings, 1 reply; 7+ messages in thread
From: Vincent Jardin via B4 Relay @ 2026-07-23 21:38 UTC (permalink / raw)
  To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Jonathan Corbet, Shuah Khan
  Cc: linux-hwmon, linux-kernel, devicetree, linux-doc, Vincent Jardin

From: Vincent Jardin <vjardin@free.fr>

Add a device-tree binding for the Monolithic Power Systems
MPQ8646 step-down converter as a PMBus device.

The schema is inspired from the mpq8785e:
same mps,vout-fb-divider-ratio-permille property (maximum: 2047),
and const: mps,mpq8646.

Signed-off-by: Vincent Jardin <vjardin@free.fr>
---
 .../bindings/hwmon/pmbus/mps,mpq8646.yaml          | 50 ++++++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/Documentation/devicetree/bindings/hwmon/pmbus/mps,mpq8646.yaml b/Documentation/devicetree/bindings/hwmon/pmbus/mps,mpq8646.yaml
new file mode 100644
index 000000000000..7acf4f235b12
--- /dev/null
+++ b/Documentation/devicetree/bindings/hwmon/pmbus/mps,mpq8646.yaml
@@ -0,0 +1,50 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+# Copyright (c) 2026 Free Mobile - Vincent Jardin <vjardin@free.fr>
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/hwmon/pmbus/mps,mpq8646.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Monolithic Power Systems MPQ8646 PMBus Step-Down Converter (extended)
+
+maintainers:
+  - Vincent Jardin <vjardin@free.fr>
+
+description: |
+  MPS MPQ8646 PMBus step-down converter.
+
+properties:
+  compatible:
+    const: mps,mpq8646
+
+  reg:
+    maxItems: 1
+
+  mps,vout-fb-divider-ratio-permille:
+    description:
+      The feedback resistor divider ratio, expressed in permille
+      (Vfb / Vout * 1000). This value is written to the
+      PMBUS_VOUT_SCALE_LOOP register and is required for correct output
+      voltage presentation.
+    $ref: /schemas/types.yaml#/definitions/uint32
+    minimum: 1
+    maximum: 2047
+    default: 706
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    i2c {
+      #address-cells = <1>;
+      #size-cells = <0>;
+
+      regulator@10 {
+        compatible = "mps,mpq8646";
+        reg = <0x10>;
+      };
+    };

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v4 3/3] hwmon: pmbus: add MPQ8646 driver
  2026-07-23 21:38 [PATCH v4 0/3] hwmon: pmbus: add MPS MPQ8646 support Vincent Jardin via B4 Relay
  2026-07-23 21:38 ` [PATCH v4 1/3] hwmon: pmbus: event notification with alarms Vincent Jardin via B4 Relay
  2026-07-23 21:38 ` [PATCH v4 2/3] dt-bindings: hwmon: pmbus: add MPS MPQ8646 binding Vincent Jardin via B4 Relay
@ 2026-07-23 21:38 ` Vincent Jardin via B4 Relay
  2026-07-23 21:54   ` sashiko-bot
  2 siblings, 1 reply; 7+ messages in thread
From: Vincent Jardin via B4 Relay @ 2026-07-23 21:38 UTC (permalink / raw)
  To: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Jonathan Corbet, Shuah Khan
  Cc: linux-hwmon, linux-kernel, devicetree, linux-doc, Vincent Jardin

From: Vincent Jardin <vjardin@free.fr>

Add a new single-chip driver for the MPS MPQ8646 that is
a PMBus device.

Beyond basic PMBus telemetry, the driver adds:
  - MFR_CFG_EXT gate-close retry after CLEAR_LAST_FAULT.
  - alarm acknowledge via inX_reset_history.
  - STATUS_WORD MPS-extended bit decode + post-mortem clear.
  - In-driver alarm-poll fallback work item (thanks lm90) for
    boards without SMBALERT
  - on_off_config/vout_margin/mfr_pmbus_lock sysfs.

Signed-off-by: Vincent Jardin <vjardin@free.fr>
---
 Documentation/hwmon/mpq8646.rst |  314 ++++++++++
 MAINTAINERS                     |    8 +
 drivers/hwmon/pmbus/Kconfig     |   11 +
 drivers/hwmon/pmbus/Makefile    |    1 +
 drivers/hwmon/pmbus/mpq8646.c   | 1237 +++++++++++++++++++++++++++++++++++++++
 5 files changed, 1571 insertions(+)

diff --git a/Documentation/hwmon/mpq8646.rst b/Documentation/hwmon/mpq8646.rst
new file mode 100644
index 000000000000..61ca2a5387da
--- /dev/null
+++ b/Documentation/hwmon/mpq8646.rst
@@ -0,0 +1,314 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+.. Copyright (c) 2026 Free Mobile - Vincent Jardin <vjardin@free.fr>
+
+Kernel driver mpq8646
+=====================
+
+Supported chips:
+
+  * MPS MPQ8646
+
+    Prefix: 'mpq8646'
+
+Author:
+  - Vincent Jardin <vjardin@free.fr>
+
+Chip-identity
+-------------
+
+Support the boards that are designed with the MPS MPQ8646 probed thanks
+to``MFR_MODEL`` register.
+
+This driver targets the MPQ8646 silicon specifically.
+
+Description
+-----------
+
+The MPQ8646 is a fully integrated, PMBus-compatible, high-frequency,
+synchronous buck converter. It offers a compact solution that
+achieves up high Amps output current per phase, with excellent load
+and line regulation over a wide input supply range. The chip
+operates at high efficiency over a wide output current load range.
+
+The PMBus interface provides converter configurations and key
+parameters monitoring.
+
+The device adopts MPS's proprietary multi-phase digital
+constant-on-time (MCOT) control, which provides fast transient
+response and eases loop stabilization. The MCOT scheme also allows
+multiple devices or channels to be connected in parallel with
+excellent current sharing and phase interleaving for high-current
+applications.
+
+Fully integrated protection features include over-current
+protection (OCP), over-voltage protection (OVP), under-voltage
+protection (UVP), and over-temperature protection (OTP).
+
+This device is compliant with:
+
+- PMBus rev 1.3 interface.
+
+The driver exports the following attributes via the 'sysfs' files
+for input voltage:
+
+- in1_input
+- in1_label
+- in1_max
+- in1_max_alarm
+- in1_min
+- in1_min_alarm
+- in1_crit
+- in1_crit_alarm
+
+The driver provides the following attributes for output voltage:
+
+- in2_input
+- in2_label
+- in2_alarm
+- in2_max
+- in2_max_alarm
+- in2_min
+- in2_min_alarm
+- in2_crit
+- in2_crit_alarm
+- in2_lcrit
+- in2_lcrit_alarm
+
+The driver provides the following attributes for output current:
+
+- curr1_input
+- curr1_label
+- curr1_max
+- curr1_max_alarm
+- curr1_crit
+- curr1_crit_alarm
+
+The driver provides the following attributes for temperature:
+
+- temp1_input
+- temp1_max
+- temp1_max_alarm
+- temp1_crit
+- temp1_crit_alarm
+
+Alarm acknowledgment
+---------------------
+
+The hwmon-class ``inX_alarm``/``currX_alarm``/``tempX_alarm`` files are
+read-only in pmbus_core. The driver exposes the standard
+``PMBUS_VIRT_RESET_*_HISTORY`` virtual-register channel for fault
+acknowledgment: writing ``1`` to ``inX_reset_history`` /
+``currX_reset_history`` / ``tempX_reset_history`` sends the chip a
+``CLEAR_FAULTS`` (0x03) Send-Byte, which clears the latched
+``STATUS_WORD`` / ``STATUS_VOUT`` / ``STATUS_IOUT`` /
+``STATUS_INPUT`` / ``STATUS_TEMPERATURE`` bits the chip is currently
+exposing.
+
+The MPS-specific NVM post-mortem register
+``PROTECTION_LAST`` (0xFB) is not cleared by this path, see the use of the
+``clear_protection_last[_force]`` debugfs entries described below.
+
+Regulator framework integration
+-------------------------------
+
+When ``CONFIG_REGULATOR=y`` is set, the chip is
+exposed under ``/sys/class/regulator/`` and accepts the standard
+regulator framework operations:
+
+- ``regulator_enable()`` / ``regulator_disable()`` -> ``OPERATION`` (0x01)
+- ``regulator_set_voltage()`` -> ``VOUT_COMMAND`` (0x21)
+- ``regulator_get_voltage()`` -> ``READ_VOUT`` (0x8B)
+- ``regulator_is_enabled()`` -> ``OPERATION`` bit-decode
+
+The single regulator descriptor is named ``"vout"`` (page 0). For a
+multi-page configuration the descriptor table can be extended in
+the driver.
+
+In-driver alarm-poll fallback
+-----------------------------
+
+On boards where the chip's ``SMBALERT#`` pin is unavailable to the
+SoC, ``pmbus_core::pmbus_irq_setup`` cannot deliver SMBALERT-driven
+``poll(POLLPRI)`` wakes or ``udev change@...`` events on the
+``inX_alarm`` files. The driver provides a ``delayed_work``-based
+polling fallback (inspired from ``drivers/hwmon/lm90.c``) that
+periodically reads ``STATUS_WORD`` and calls ``hwmon_notify_event()``
+on 0->1 transitions. The frequency of polling is tunable:
+
+::
+
+  /sys/kernel/debug/mpq8646/<bus>-<addr>/alarm_poll_interval_ms
+
+The default is 1000 ms.
+
+Set it to 0 to disable. The worker self-suppresses
+when ``client->irq != 0`` (i.e. when DT supplies an
+``interrupts = <...>``  property on the regulator node), so adding
+``SMBALERT#`` wiring is a zero-driver-change uplift on a future
+board rev.
+
+MPS post-mortem (PROTECTION_LAST)
+---------------------------------
+
+The MPQ8646 silicon keeps a single 16-bit NVM-backed record of the
+last protection event in ``PROTECTION_LAST`` (0xFB). It survives
+chip power/reset. The following debugfs entries expose it:
+
+::
+
+  /sys/kernel/debug/mpq8646/<bus>-<addr>/protection_last           (RO)
+  /sys/kernel/debug/mpq8646/<bus>-<addr>/clear_protection_last     (WO)
+  /sys/kernel/debug/mpq8646/<bus>-<addr>/clear_protection_last_force (WO)
+  /sys/kernel/debug/mpq8646/<bus>-<addr>/status_decoded            (RO)
+
+``protection_last`` decodes the 16-bit value based on the datasheet
+fault names (``INIT_FAULT``, ``NVM_CRC_ERROR``, ``NVM_FAULT``,
+``OC_PHASE_FAULT``, ``OTP_SELF_FAULT``, ``SWITCH_PRD_FAULT``,
+``VIN_OV_FAULT``, ``VOUT_OV_FAULT``, ``VOUT_UV_FAULT``,
+``OC_TOT_FAULT``, ``VIN_UVLO_FAULT``, ``DRMOS_OTP``).
+
+``clear_protection_last`` writes ``CLEAR_LAST_FAULT`` (0x08) Send-Byte.
+The chip silently no-ops unless ``MFR_CFG_EXT`` (0xF5) bit[6] is set.
+
+``clear_protection_last_force`` performs the unlock with the following
+six-step dansing:
+
+1. read ``WRITE_PROTECT`` (0x10) and ``MFR_CFG_EXT`` (0xF5) for restore
+2. clear ``WRITE_PROTECT`` if set
+3. set ``MFR_CFG_EXT`` bit[6] = 1, preserving other bits
+4. send ``CLEAR_LAST_FAULT`` (0x08)
+5. restore ``MFR_CFG_EXT`` (with retry to handle the chip's
+   undocumented post-NVM-write busy window)
+6. restore ``WRITE_PROTECT``
+
+``status_decoded`` reads ``STATUS_WORD`` (0x79) and renders the
+16 bits with MPS-extension labels (bit12 = ``NVM_SUMMARY``,
+bit8 = ``WATCH_DOG``, bit0 = ``DRMOS_FAULT``) instead of the
+PMBus 1.3 spec generic names.
+
+NVMEM snapshot
+--------------
+
+When ``CONFIG_NVMEM=y`` is set, the chip's NVM-backed
+observability registers are exposed as a single 16-byte read-only
+``nvmem_device`` at ``/sys/bus/nvmem/devices/<i2c-name>/nvmem``.
+Layout (little-endian for 16-bit fields, zero-fill on per-entry read
+failure and for the reserved tail):
+
+::
+
+  offset 0..1   PROTECTION_LAST  (0xFB)  word
+  offset 2..3   MFR_RETRY_TIMES  (0xF4)  word
+  offset 4..5   MFR_CONFIG_ID    (0xC0)  word
+  offset 6..7   MFR_VBOOT_CFG    (0xFC)  word
+  offset 8      MFR_SILICON_REV  (0xC3)  byte
+  offset 9..15  reserved (zero)
+
+Suitable for single-``cat`` post-mortem capture by a fleet daemon.
+
+Diagnostics and introspection
+-----------------------------
+
+When ``CONFIG_DEBUG_FS=y`` is set, the driver exposes the following
+entries under ``/sys/kernel/debug/mpq8646/<bus>-<addr>/``. Most are diagnostic
+probes; the read/write entries (marked R/W) are direct accessors to
+the underlying chip command.
+
+Identity / observability (read-only):
+
+==========================  ==================  =================================
+File                        PMBus / MFR cmd     Description
+==========================  ==================  =================================
+``mfr_config_id``           0xC0 (word)         board / SKU NVM identifier
+``mfr_config_code_rev``     0xC1 (word)         NVM image revision (PART_RECOG +
+                                                config code rev fields)
+``mfr_silicon_rev``         0xC3 (byte)         die revision
+``mfr_retry_times``         0xF4 (word)         per-fault-class recovery-mode
+                                                configuration (NOT a retry
+                                                count). Four 4-bit fields
+                                                [15:12]=OTP, [11:8]=VOUT_OV,
+                                                [7:4]=VOUT_UV, [3:0]=OCP.
+                                                Each field: 0x0=latch-off,
+                                                0x1..0xE=retry N times then
+                                                latch off, 0xF=hiccup
+                                                (retry indefinitely). The
+                                                chip exposes no in-NVM
+                                                retry-event counter; track
+                                                transitions externally if
+                                                needed (poll
+                                                ``protection_last`` or watch
+                                                ``inX_alarm`` / ``temp1_alarm``
+                                                ``poll(POLLPRI)`` wakes).
+``mfr_vboot_cfg``           0xFC (word)         ADDR/VBOOT latched at POR
+==========================  ==================  =================================
+
+Timing / UVLO knobs (read-only):
+
+==========================  ==================  =================================
+File                        PMBus cmd           Description
+==========================  ==================  =================================
+``vin_on``                  0x35 (word)         UVLO turn-on threshold
+``vin_off``                 0x36 (word)         UVLO turn-off threshold
+``ton_delay``               0x60 (word)         soft-start delay
+``ton_rise``                0x61 (word)         soft-start ramp time
+``toff_delay``              0x64 (word)         soft-stop delay
+``toff_fall``               0x65 (word)         soft-stop ramp time
+==========================  ==================  =================================
+
+Configuration (read/write):
+
+==========================  ==================  =================================
+File                        PMBus / MFR cmd     Description
+==========================  ==================  =================================
+``on_off_config``           0x02 (byte)         PMBus vs CTRL-pin on/off source +
+                                                active polarity
+``vout_margin_high``        0x25 (word)         production margin-high VOUT setpoint
+``vout_margin_low``         0x26 (word)         production margin-low VOUT setpoint
+``mfr_pmbus_lock``          0xEE (word)         programmable PMBus write-lock
+                                                (independent of WRITE_PROTECT)
+``mfr_product_rev_user``    0xC2 (word)         user-programmable product revision
+``alarm_poll_interval_ms``  --                  alarm-poll worker cadence (driver-
+                                                local, not a chip register)
+==========================  ==================  =================================
+
+NVM commit / revert (write-only):
+
+==========================  ==================  =================================
+File                        PMBus cmd           Description
+==========================  ==================  =================================
+``store_all``               0x15                STORE_USER_ALL Send-Byte (commit RAM
+                                                config to chip NVM)
+``restore_all``             0x16                RESTORE_USER_ALL Send-Byte (revert
+                                                RAM to last-NVM image)
+==========================  ==================  =================================
+
+Bring-up probes (write-only triggers, results in ``last_probe``):
+
+==========================  ==================================================
+File                        Action
+==========================  ==================================================
+``probe_smbus_rword <reg>``  i2c_smbus_read_word_data on <reg>
+``probe_smbus_rbyte <reg>``  i2c_smbus_read_byte_data on <reg>
+``probe_raw_xfer <reg>``     raw i2c_transfer read-word on <reg>
+``probe_page_write <pg>``    write PMBUS_PAGE = <pg>
+``probe_clear_faults``       send CLEAR_FAULTS (0x03) Send-Byte
+``force_raw_xfer``           if 1, the read_word_data hook uses raw i2c_transfer
+``delay_us``                 udelay before each chip-driver hook call
+``stats``                    per-hook call / error counters
+``last_probe``               result of the most recent probe_* operation
+``reset_stats``              zero the stats counters
+==========================  ==================================================
+
+Devicetree
+----------
+
+The driver uses ``compatible = "mps,mpq8646"``. There are some few optional
+properties:
+
+- ``mps,vout-fb-divider-ratio-permille`` : it writes ``VOUT_SCALE_LOOP``
+  (0x29) at probe to compensate for an external resistor divider in
+  the VOUT feedback path. The valid range is 11-bit.
+- ``interrupts = <...>`` : if the board routes the chip's
+  ``SMBALERT#`` pin to a SoC GPIO, declaring it here lights up
+  ``pmbus_core::pmbus_irq_setup`` and disables the in-driver
+  alarm-poll fallback
diff --git a/MAINTAINERS b/MAINTAINERS
index 1ab8736850ea..f4766a851df5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18328,6 +18328,14 @@ S:	Maintained
 F:	Documentation/hwmon/mp9945.rst
 F:	drivers/hwmon/pmbus/mp9945.c
 
+MPS MPQ8646 PMBUS DRIVER
+M:	Vincent Jardin <vjardin@free.fr>
+L:	linux-hwmon@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/hwmon/pmbus/mps,mpq8646.yaml
+F:	Documentation/hwmon/mpq8646.rst
+F:	drivers/hwmon/pmbus/mpq8646.c
+
 MR800 AVERMEDIA USB FM RADIO DRIVER
 M:	Alexey Klimov <alexey.klimov@linaro.org>
 L:	linux-media@vger.kernel.org
diff --git a/drivers/hwmon/pmbus/Kconfig b/drivers/hwmon/pmbus/Kconfig
index c8cda160b5f8..9f44e76b0b64 100644
--- a/drivers/hwmon/pmbus/Kconfig
+++ b/drivers/hwmon/pmbus/Kconfig
@@ -616,6 +616,17 @@ config SENSORS_MPQ8785
 	  This driver can also be built as a module. If so, the module will
 	  be called mpq8785.
 
+config SENSORS_MPQ8646
+	tristate "MPS MPQ8646"
+	depends on REGULATOR || !REGULATOR
+	depends on NVMEM || !NVMEM
+	help
+	  If you say yes here you get hardware monitoring support for the
+	  Monolithic Power Systems MPQ8646.
+
+	  This driver can also be built as a module. If so, the module
+	  will be called mpq8646.
+
 config SENSORS_PIM4328
 	tristate "Flex PIM4328 and compatibles"
 	help
diff --git a/drivers/hwmon/pmbus/Makefile b/drivers/hwmon/pmbus/Makefile
index ffc05f493213..6f8fda966600 100644
--- a/drivers/hwmon/pmbus/Makefile
+++ b/drivers/hwmon/pmbus/Makefile
@@ -60,6 +60,7 @@ obj-$(CONFIG_SENSORS_MP9941)	+= mp9941.o
 obj-$(CONFIG_SENSORS_MP9945)	+= mp9945.o
 obj-$(CONFIG_SENSORS_MPQ7932)	+= mpq7932.o
 obj-$(CONFIG_SENSORS_MPQ8785)	+= mpq8785.o
+obj-$(CONFIG_SENSORS_MPQ8646)	+= mpq8646.o
 obj-$(CONFIG_SENSORS_PLI1209BC)	+= pli1209bc.o
 obj-$(CONFIG_SENSORS_PM6764TR)	+= pm6764tr.o
 obj-$(CONFIG_SENSORS_PXE1610)	+= pxe1610.o
diff --git a/drivers/hwmon/pmbus/mpq8646.c b/drivers/hwmon/pmbus/mpq8646.c
new file mode 100644
index 000000000000..95abd347e3e9
--- /dev/null
+++ b/drivers/hwmon/pmbus/mpq8646.c
@@ -0,0 +1,1237 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Driver for MPS MPQ8646 step-down converter.
+ *
+ * Copyright (c) 2026 Free Mobile - Vincent Jardin <vjardin@free.fr>
+ */
+
+#include <linux/bitops.h>
+#include <linux/debugfs.h>
+#include <linux/delay.h>
+#include <linux/hwmon.h>
+#include <linux/i2c.h>
+#include <linux/ktime.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/nvmem-provider.h>
+#include <linux/of_device.h>
+#include <linux/pmbus.h>
+#include <linux/property.h>
+#include <linux/regulator/driver.h>
+#include <linux/seq_file.h>
+#include <linux/workqueue.h>
+#include "pmbus.h"
+
+/* Default cadence for the in-driver alarm-poll fallback (ms) */
+#define MPQ8646_ALARM_POLL_MS_DEFAULT	1000
+
+/* MPS vendor-extended command codes (NOT in PMBus 1.3 Part II) */
+#define MPS_CLEAR_LAST_FAULT		0x08
+#define MPS_MFR_CFG_EXT			0xF5
+#define MPS_MFR_CFG_EXT_CLR_LAST_EN	BIT(6)
+#define MPS_PROTECTION_LAST		0xFB
+
+/*
+ * PMBus 1.3 NVM commit / revert commands. pmbus.h does not provide
+ * named constants for these. MPS equivalent of STORE_ALL (15h) and
+ * RESTORE_ALL (16h).
+ */
+#define PMBUS_STORE_USER_ALL		0x15
+#define PMBUS_RESTORE_USER_ALL		0x16
+
+/* PMBus 1.3 timing / UVLO command codes that pmbus.h doesn't expose */
+#define PMBUS_VIN_ON			0x35
+#define PMBUS_VIN_OFF			0x36
+#define PMBUS_TON_DELAY			0x60
+#define PMBUS_TON_RISE			0x61
+#define PMBUS_TOFF_DELAY		0x64
+#define PMBUS_TOFF_FALL			0x65
+
+/* MPS vendor-extended observability / identity registers */
+#define MPS_MFR_CONFIG_ID		0xC0
+#define MPS_MFR_CONFIG_CODE_REV		0xC1
+#define MPS_MFR_PRODUCT_REV_USER	0xC2
+#define MPS_MFR_SILICON_REV		0xC3
+#define MPS_MFR_RETRY_TIMES		0xF4
+#define MPS_MFR_VBOOT_CFG		0xFC
+
+/*
+ * MPS_MFR_PMBUS_LOCK (EEh): 16-bit WORD whose low two bits gate
+ * subsequent PMBus writes
+ *   bits[1:0] = 00  -- unlocked (POR default)
+ *               01  -- lock all writes EXCEPT VOUT_COMMAND (0x21)
+ *                      so the operator can still DVFS the rail
+ *               11  -- lock all writes
+ * A negative-going PG edge resets these bits to 00, the lock
+ * is operationally reversible without a full chip POR.
+ */
+#define MPS_MFR_PMBUS_LOCK		0xEE
+
+/*
+ * Retry parameters for the MFR_CFG_EXT gate-close write after
+ * CLEAR_LAST_FAULT. Bench-observed NVM-busy NACK window on this
+ * silicon is about 1 ms; the datasheet does not have information.
+ */
+#define MPQ8646_NVM_RETRY_MAX		5
+#define MPQ8646_NVM_RETRY_DELAY_US_MIN	2000
+#define MPQ8646_NVM_RETRY_DELAY_US_MAX	4000
+
+/*
+ * VOUT_MODE invalid-value sentinel. PMBus chips return all-ones on
+ * unimplemented-register reads (a 0xFF byte is otherwise a legal
+ * encoding for VID mode 0b111, so we treat exactly 0xFF as
+ * "register absent" rather than "valid mode 7").
+ */
+#define PB_VOUT_MODE_INVALID		0xFF
+
+/*
+ * VOUT_MODE byte layout per PMBus 1.3 Part II Table 2:
+ *   [7:5] mode (0=LINEAR16, 1=VID, 2=DIRECT, ...)
+ *   [4:0] mode-specific parameter (e.g. linear-format exponent)
+ * pmbus.h gives us PB_VOUT_MODE_MODE_MASK (0xE0) and
+ * PB_VOUT_MODE_LINEAR/VID/DIRECT as bit-aligned mask values; we
+ * still need an explicit shift to compare them after a right-shift
+ * to-the-LSB.
+ */
+#define PB_VOUT_MODE_MODE_SHIFT		5
+
+#define MPQ8646_TRACE(fmt, ...) \
+	pr_debug("mpq8646-trace: " fmt, ##__VA_ARGS__)
+
+/*
+ * Maximum legal value for VOUT_SCALE_LOOP (0x29) on the MPQ8646 silicon
+ * The chip uses an 11-bit VOUT feedback-divider scale register.
+ */
+#define MPQ8646_VOUT_SCALE_LOOP_MAX	GENMASK(10, 0)
+
+/* Forward declaration */
+struct mpq8646_dbg_reg_ctx;
+
+/* Per-instance state */
+struct mpq8646_priv {
+	struct pmbus_driver_info info;	/* must be first, container_of target */
+	struct i2c_client	*client;
+
+	atomic_t		read_byte_calls;
+	atomic_t		read_word_calls;
+	atomic_t		write_byte_calls;
+	atomic_t		read_byte_err;
+	atomic_t		read_word_err;
+	atomic_t		clear_faults_swallowed;
+
+	bool			debug_force_raw_xfer;
+	unsigned int		debug_delay_us;
+
+	int			last_probe_rc;
+	u16			last_probe_data;
+
+	/* Serialises CLEAR_LAST_FAULT sequences and PROTECTION_LAST reads */
+	struct mutex		mps_lock;
+
+	/*
+	 * Adapted from lm90's alert_work pattern
+	 * Set alarm_poll_interval_ms = 0 to disable.
+	 */
+	struct delayed_work	alarm_poll_work;
+	struct device		*hwmon_dev;
+	u16			last_status_word;
+	u32			alarm_poll_interval_ms;
+
+#ifdef CONFIG_DEBUG_FS
+	struct dentry		*debugfs_root;
+	struct mpq8646_dbg_reg_ctx	*dbg_reg_ctx;
+#endif
+};
+
+struct mpq_status_bit {
+	u16		mask;
+	const char	*name;
+};
+
+static const struct mpq_status_bit mpq8646_status_word_bits[] = {
+	{ PB_STATUS_VOUT,         "VOUT" },
+	{ PB_STATUS_IOUT_POUT,    "IOUT_POUT" },
+	{ PB_STATUS_INPUT,        "INPUT" },
+	{ PB_STATUS_WORD_MFR,     "NVM_SUMMARY" },
+	{ PB_STATUS_POWER_GOOD_N, "POWER_GOOD#" },
+	{ PB_STATUS_FANS,         "FANS" },
+	{ PB_STATUS_OTHER,        "OTHER" },
+	{ PB_STATUS_UNKNOWN,      "WATCH_DOG" },
+	{ PB_STATUS_BUSY,         "BUSY" },
+	{ PB_STATUS_OFF,          "OFF" },
+	{ PB_STATUS_VOUT_OV,      "VOUT_OV_FAULT" },
+	{ PB_STATUS_IOUT_OC,      "IOUT_OC_FAULT" },
+	{ PB_STATUS_VIN_UV,       "VIN_UV_FAULT" },
+	{ PB_STATUS_TEMPERATURE,  "TEMP" },
+	{ PB_STATUS_CML,          "CML" },
+	{ PB_STATUS_NONE_ABOVE,   "DRMOS_FAULT" },
+	{ /* sentinel */ }
+};
+
+/* PROTECTION_LAST (0xFB) bit names, it survives chip POR */
+static const struct mpq_status_bit mpq8646_protection_last_bits[] = {
+	{ BIT(15), "INIT_FAULT" },
+	{ BIT(14), "NVM_CRC_ERROR" },
+	{ BIT(13), "NVM_FAULT" },
+	{ BIT(12), "OC_PHASE_FAULT" },
+	{ BIT(11), "OTP_SELF_FAULT" },
+	{ BIT(9),  "SWITCH_PRD_FAULT" },
+	{ BIT(8),  "VIN_OV_FAULT" },
+	{ BIT(7),  "VOUT_OV_FAULT" },
+	{ BIT(6),  "VOUT_UV_FAULT" },
+	{ BIT(5),  "OC_TOT_FAULT" },
+	{ BIT(4),  "VIN_UVLO_FAULT" },
+	{ BIT(3),  "DRMOS_OTP" },
+	{ /* sentinel */ }
+};
+
+static inline struct mpq8646_priv *mpq8646_priv_from_client(struct i2c_client *client)
+{
+	const struct pmbus_driver_info *info = pmbus_get_driver_info(client);
+
+	return container_of(info, struct mpq8646_priv, info);
+}
+
+static int mpq8646_raw_xfer_rword(struct i2c_client *client, u8 reg)
+{
+	u8 cmd = reg;
+	__le16 data = 0;
+	struct i2c_msg msg[] = {
+		{
+			.addr = client->addr,
+			.flags = 0,
+			.len = sizeof(cmd),
+			.buf = &cmd,
+		},
+		{
+			.addr = client->addr,
+			.flags = I2C_M_RD,
+			.len = sizeof(data),
+			.buf = (u8 *)&data,
+		},
+	};
+	int rc;
+
+	rc = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+	if (rc < 0)
+		return rc;
+	if (rc != ARRAY_SIZE(msg))
+		return -EIO;
+	return le16_to_cpu(data);
+}
+
+/*
+ * VID-mode m/b/R coefficients, same values as the mpq8785 driver for
+ * the MPS VID encoding. Per the PMBus Direct formula used by
+ * pmbus_core, X = (Y * 10^-R - b) / m, so m=64 / b=0 / R=1 yields
+ * 1.5625 mV per LSB.
+ */
+#define MPQ8646_VID_M			64
+#define MPQ8646_VID_B			0
+#define MPQ8646_VID_R			1
+
+static int mpq8646_identify(struct i2c_client *client,
+			    struct pmbus_driver_info *info)
+{
+	int vout_mode;
+
+	vout_mode = pmbus_read_byte_data(client, 0, PMBUS_VOUT_MODE);
+	if (vout_mode < 0)
+		return vout_mode;
+	if (vout_mode == PB_VOUT_MODE_INVALID)
+		return -ENODEV;
+
+	switch ((vout_mode & PB_VOUT_MODE_MODE_MASK) >> PB_VOUT_MODE_MODE_SHIFT) {
+	case PB_VOUT_MODE_LINEAR >> PB_VOUT_MODE_MODE_SHIFT:
+		info->format[PSC_VOLTAGE_OUT] = linear;
+		break;
+	case PB_VOUT_MODE_VID >> PB_VOUT_MODE_MODE_SHIFT:
+	case PB_VOUT_MODE_DIRECT >> PB_VOUT_MODE_MODE_SHIFT:
+		info->format[PSC_VOLTAGE_OUT] = direct;
+		info->m[PSC_VOLTAGE_OUT] = MPQ8646_VID_M;
+		info->b[PSC_VOLTAGE_OUT] = MPQ8646_VID_B;
+		info->R[PSC_VOLTAGE_OUT] = MPQ8646_VID_R;
+		break;
+	default:
+		return -ENODEV;
+	}
+
+	return 0;
+};
+
+static int mpq8646_read_byte_data(struct i2c_client *client, int page, int reg)
+{
+	struct mpq8646_priv *priv = mpq8646_priv_from_client(client);
+	int ret;
+
+	atomic_inc(&priv->read_byte_calls);
+	MPQ8646_TRACE("read_byte_data page=%d reg=0x%02x\n", page, reg);
+
+	if (priv->debug_delay_us)
+		udelay(priv->debug_delay_us);
+
+	switch (reg) {
+	case PMBUS_VOUT_MODE:
+		ret = pmbus_read_byte_data(client, page, reg);
+		MPQ8646_TRACE("  VOUT_MODE raw=0x%02x ret=%d\n", ret, ret);
+		if (ret < 0) {
+			atomic_inc(&priv->read_byte_err);
+			return ret;
+		}
+
+		if ((ret >> 5) == 1)
+			return PB_VOUT_MODE_DIRECT;
+
+		return ret;
+	case PMBUS_WRITE_PROTECT:
+		MPQ8646_TRACE("  WRITE_PROTECT shimmed to 0 (framework path)\n");
+		return 0;
+	case PMBUS_STATUS_BYTE:
+	case PMBUS_STATUS_CML:
+	case PMBUS_STATUS_OTHER:
+	case PMBUS_STATUS_MFR_SPECIFIC:
+	case PMBUS_STATUS_FAN_12:
+	case PMBUS_STATUS_FAN_34:
+		return -ENXIO;
+	case PMBUS_MFR_LOCATION:
+	case PMBUS_MFR_DATE:
+	case PMBUS_MFR_SERIAL:
+	case PMBUS_IC_DEVICE_ID:
+	case PMBUS_IC_DEVICE_REV:
+		MPQ8646_TRACE("  unsupported mfr-info reg 0x%02x -> ENXIO\n", reg);
+		return -ENXIO;
+	default:
+		return -ENODATA;
+	}
+}
+
+static int mpq8646_write_byte(struct i2c_client *client, int page, u8 value)
+{
+	struct mpq8646_priv *priv = mpq8646_priv_from_client(client);
+
+	atomic_inc(&priv->write_byte_calls);
+	MPQ8646_TRACE("write_byte page=%d value=0x%02x\n", page, value);
+
+	if (priv->debug_delay_us)
+		udelay(priv->debug_delay_us);
+
+	return -ENODATA;	/* let core do the standard direct write */
+}
+
+/*
+ * Reference: ltc2978.c::ltc2978_write_word_data which uses the
+ * same virtual-register channel for its real chip-side peak reset.
+ */
+static int mpq8646_write_word_data(struct i2c_client *client, int page,
+				   int reg, u16 word)
+{
+	struct mpq8646_priv *priv = mpq8646_priv_from_client(client);
+	int rc;
+
+	switch (reg) {
+	case PMBUS_VIRT_RESET_VIN_HISTORY:
+	case PMBUS_VIRT_RESET_VOUT_HISTORY:
+	case PMBUS_VIRT_RESET_IOUT_HISTORY:
+	case PMBUS_VIRT_RESET_TEMP_HISTORY:
+		MPQ8646_TRACE("reset_history virt reg=0x%04x -> CLEAR_FAULTS\n",
+			      reg);
+		rc = i2c_smbus_write_byte(priv->client, PMBUS_CLEAR_FAULTS);
+		if (rc < 0)
+			MPQ8646_TRACE("  CLEAR_FAULTS rc=%d\n", rc);
+		return rc < 0 ? rc : 0;
+	default:
+		return -ENODATA;	/* let pmbus_core do the direct write */
+	}
+}
+
+static int mpq8646_read_word_data(struct i2c_client *client, int page,
+				  int phase, int reg)
+{
+	struct mpq8646_priv *priv = mpq8646_priv_from_client(client);
+	int rc;
+
+	atomic_inc(&priv->read_word_calls);
+	MPQ8646_TRACE("read_word_data page=%d phase=%d reg=0x%02x%s\n",
+		      page, phase, reg,
+		      priv->debug_force_raw_xfer ? " [force-raw]" : "");
+
+	if (priv->debug_delay_us)
+		udelay(priv->debug_delay_us);
+
+	switch (reg) {
+	case PMBUS_READ_VIN:
+	case PMBUS_READ_VOUT:
+	case PMBUS_READ_IOUT:
+	case PMBUS_READ_TEMPERATURE_1:
+	case PMBUS_STATUS_WORD:
+	case PMBUS_VOUT_OV_FAULT_LIMIT:
+	case PMBUS_VOUT_OV_WARN_LIMIT:
+	case PMBUS_VOUT_UV_WARN_LIMIT:
+	case PMBUS_VOUT_UV_FAULT_LIMIT:
+	case PMBUS_IOUT_OC_FAULT_LIMIT:
+	case PMBUS_IOUT_OC_WARN_LIMIT:
+	case PMBUS_OT_FAULT_LIMIT:
+	case PMBUS_OT_WARN_LIMIT:
+	case PMBUS_VIN_OV_FAULT_LIMIT:
+	case PMBUS_VIN_OV_WARN_LIMIT:
+	case PMBUS_VIN_UV_WARN_LIMIT:
+	case PMBUS_VIN_UV_FAULT_LIMIT:
+	case PMBUS_MFR_VIN_MAX:
+	case PMBUS_MFR_VOUT_MAX:
+	case PMBUS_MFR_IOUT_MAX:
+	case PMBUS_MFR_MAX_TEMP_1:
+		break;
+	case PMBUS_VIRT_RESET_VIN_HISTORY:
+	case PMBUS_VIRT_RESET_VOUT_HISTORY:
+	case PMBUS_VIRT_RESET_IOUT_HISTORY:
+	case PMBUS_VIRT_RESET_TEMP_HISTORY:
+		return 0;
+	default:
+		return -ENODATA;
+	}
+
+	if (priv->debug_force_raw_xfer)
+		rc = mpq8646_raw_xfer_rword(client, reg);
+	else
+		rc = i2c_smbus_read_word_data(client, reg);
+
+	if (rc < 0)
+		atomic_inc(&priv->read_word_err);
+
+	MPQ8646_TRACE("  reg=0x%02x rc=%d\n", reg, rc);
+	return rc;
+}
+
+static struct pmbus_driver_info mpq8646_info = {
+	.pages = 1,
+	.format[PSC_VOLTAGE_IN] = direct,
+	.format[PSC_CURRENT_OUT] = direct,
+	.format[PSC_TEMPERATURE] = direct,
+	.m[PSC_VOLTAGE_IN] = 4,
+	.b[PSC_VOLTAGE_IN] = 0,
+	.R[PSC_VOLTAGE_IN] = 1,
+	.m[PSC_CURRENT_OUT] = 16,
+	.b[PSC_CURRENT_OUT] = 0,
+	.R[PSC_CURRENT_OUT] = 0,
+	.m[PSC_TEMPERATURE] = 1,
+	.b[PSC_TEMPERATURE] = 0,
+	.R[PSC_TEMPERATURE] = 0,
+	.func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT |
+		   PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP |
+		   PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_VOUT |
+		   PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_TEMP,
+};
+
+#if IS_ENABLED(CONFIG_REGULATOR)
+static const struct regulator_desc mpq8646_reg_desc[] = {
+	PMBUS_REGULATOR("vout", 0),
+};
+#endif /* CONFIG_REGULATOR */
+
+#if IS_ENABLED(CONFIG_NVMEM)
+/*
+ * Expose using
+ *   /sys/bus/nvmem/devices/<i2c-name>/nvmem
+ *
+ * Layout (16 bytes):
+ *   offset 0..1   PROTECTION_LAST (0xFB)    word, LE     -- NVM
+ *   offset 2..3   MFR_RETRY_TIMES (0xF4)    word, LE     -- NVM
+ *   offset 4..5   MFR_CONFIG_ID   (0xC0)    word, LE     -- NVM
+ *   offset 6..7   MFR_VBOOT_CFG   (0xFC)    word, LE     -- NVM
+ *   offset 8      MFR_SILICON_REV (0xC3)    byte         -- NVM
+ *   offset 9..15  reserved (zero-fill, leaves room for additions)
+ */
+#define MPQ8646_NVMEM_SIZE	16
+
+struct mpq8646_nvmem_entry {
+	unsigned int	off;
+	u8		reg;
+	bool		is_word;
+};
+
+static const struct mpq8646_nvmem_entry mpq8646_nvmem_map[] = {
+	{ 0, MPS_PROTECTION_LAST, true  },
+	{ 2, MPS_MFR_RETRY_TIMES, true  },
+	{ 4, MPS_MFR_CONFIG_ID,   true  },
+	{ 6, MPS_MFR_VBOOT_CFG,   true  },
+	{ 8, MPS_MFR_SILICON_REV, false },
+};
+
+static int mpq8646_nvmem_read(void *data, unsigned int offset, void *val,
+			      size_t bytes)
+{
+	struct mpq8646_priv *priv = data;
+	u8 *out = val;
+	size_t i;
+
+	if (offset >= MPQ8646_NVMEM_SIZE)
+		return -EINVAL;
+	if (offset + bytes > MPQ8646_NVMEM_SIZE)
+		bytes = MPQ8646_NVMEM_SIZE - offset;
+
+	memset(out, 0, bytes);
+
+	mutex_lock(&priv->mps_lock);
+	for (i = 0; i < ARRAY_SIZE(mpq8646_nvmem_map); i++) {
+		const struct mpq8646_nvmem_entry *e = &mpq8646_nvmem_map[i];
+		unsigned int e_start = e->off;
+		unsigned int e_end = e_start + (e->is_word ? 2 : 1);
+		u8 raw[2];
+		int rc;
+		unsigned int j;
+
+		if (e_end <= offset || e_start >= offset + bytes)
+			continue;	/* outside requested slice */
+
+		if (e->is_word)
+			rc = i2c_smbus_read_word_data(priv->client, e->reg);
+		else
+			rc = i2c_smbus_read_byte_data(priv->client, e->reg);
+		if (rc < 0)
+			continue;	/* leave the zero-fill in place */
+
+		raw[0] = rc & 0xff;
+		raw[1] = (rc >> 8) & 0xff;
+
+		for (j = 0; j < e_end - e_start; j++) {
+			unsigned int abs = e_start + j;
+
+			if (abs >= offset && abs < offset + bytes)
+				out[abs - offset] = raw[j];
+		}
+	}
+	mutex_unlock(&priv->mps_lock);
+	return 0;
+}
+#endif /* CONFIG_NVMEM */
+
+static const struct i2c_device_id mpq8646_id[] = {
+	{ "mpq8646", 0 },
+	{ },
+};
+MODULE_DEVICE_TABLE(i2c, mpq8646_id);
+
+static const struct of_device_id __maybe_unused mpq8646_of_match[] = {
+	{ .compatible = "mps,mpq8646" },
+	{}
+};
+MODULE_DEVICE_TABLE(of, mpq8646_of_match);
+
+static struct pmbus_platform_data mpq8646_no_pec_pdata = {
+	.flags = PMBUS_NO_CAPABILITY,
+};
+
+#ifdef CONFIG_DEBUG_FS
+/*
+ *   /sys/kernel/debug/mpq8646/<bus>-<addr>/
+ *       probe_smbus_rword   echo <reg>  call i2c_smbus_read_word_data
+ *       probe_smbus_rbyte   echo <reg>  call i2c_smbus_read_byte_data
+ *       probe_raw_xfer      echo <reg>  call i2c_transfer manually
+ *       probe_clear_faults  echo 1      send CLEAR_FAULTS Send-Byte
+ *       force_raw_xfer      0|1         production read_word_data path
+ *       delay_us            <N>         udelay before each hook
+ *       last_probe          read-only   "rc=<rc> data=0x<hex>"
+ *       stats               read-only   counters
+ *       reset_stats         write-only  zero counters
+ */
+
+static int mpq8646_dbg_probe_smbus_rword(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+	int rc;
+
+	mutex_lock(&priv->mps_lock);
+	rc = i2c_smbus_read_word_data(priv->client, (u8)val);
+	priv->last_probe_rc = rc;
+	priv->last_probe_data = (rc < 0) ? 0 : (u16)rc;
+	mutex_unlock(&priv->mps_lock);
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_probe_smbus_rword_fops,
+			 NULL, mpq8646_dbg_probe_smbus_rword, "%llu\n");
+
+static int mpq8646_dbg_probe_smbus_rbyte(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+	int rc;
+
+	mutex_lock(&priv->mps_lock);
+	rc = i2c_smbus_read_byte_data(priv->client, (u8)val);
+	priv->last_probe_rc = rc;
+	priv->last_probe_data = (rc < 0) ? 0 : (u8)rc;
+	mutex_unlock(&priv->mps_lock);
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_probe_smbus_rbyte_fops,
+			 NULL, mpq8646_dbg_probe_smbus_rbyte, "%llu\n");
+
+static int mpq8646_dbg_probe_raw_xfer(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+	int rc;
+
+	mutex_lock(&priv->mps_lock);
+	rc = mpq8646_raw_xfer_rword(priv->client, (u8)val);
+	priv->last_probe_rc = rc;
+	priv->last_probe_data = (rc < 0) ? 0 : (u16)rc;
+	mutex_unlock(&priv->mps_lock);
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_probe_raw_xfer_fops,
+			 NULL, mpq8646_dbg_probe_raw_xfer, "%llu\n");
+
+static int mpq8646_dbg_probe_clear_faults(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+	int rc;
+
+	if (!val)
+		return 0;
+	mutex_lock(&priv->mps_lock);
+	rc = i2c_smbus_write_byte(priv->client, PMBUS_CLEAR_FAULTS);
+	priv->last_probe_rc = rc;
+	priv->last_probe_data = 0;
+	mutex_unlock(&priv->mps_lock);
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_probe_clear_faults_fops,
+			 NULL, mpq8646_dbg_probe_clear_faults, "%llu\n");
+
+static int mpq8646_dbg_last_probe_show(struct seq_file *s, void *unused)
+{
+	struct mpq8646_priv *priv = s->private;
+
+	mutex_lock(&priv->mps_lock);
+	seq_printf(s, "rc=%d data=0x%04x\n",
+		   priv->last_probe_rc, priv->last_probe_data);
+	mutex_unlock(&priv->mps_lock);
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(mpq8646_dbg_last_probe);
+
+static int mpq8646_dbg_stats_show(struct seq_file *s, void *unused)
+{
+	struct mpq8646_priv *priv = s->private;
+
+	seq_printf(s,
+		   "read_byte_calls        : %d\n"
+		   "read_word_calls        : %d\n"
+		   "write_byte_calls       : %d\n"
+		   "read_byte_err          : %d\n"
+		   "read_word_err          : %d\n"
+		   "clear_faults_swallowed : %d\n"
+		   "force_raw_xfer         : %d\n"
+		   "delay_us               : %u\n",
+		   atomic_read(&priv->read_byte_calls),
+		   atomic_read(&priv->read_word_calls),
+		   atomic_read(&priv->write_byte_calls),
+		   atomic_read(&priv->read_byte_err),
+		   atomic_read(&priv->read_word_err),
+		   atomic_read(&priv->clear_faults_swallowed),
+		   priv->debug_force_raw_xfer,
+		   priv->debug_delay_us);
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(mpq8646_dbg_stats);
+
+static int mpq8646_dbg_reset_stats(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+
+	if (!val)
+		return 0;
+	atomic_set(&priv->read_byte_calls, 0);
+	atomic_set(&priv->read_word_calls, 0);
+	atomic_set(&priv->write_byte_calls, 0);
+	atomic_set(&priv->read_byte_err, 0);
+	atomic_set(&priv->read_word_err, 0);
+	atomic_set(&priv->clear_faults_swallowed, 0);
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_reset_stats_fops,
+			 NULL, mpq8646_dbg_reset_stats, "%llu\n");
+
+static void mpq8646_print_bits(struct seq_file *s, u16 v,
+			       const struct mpq_status_bit *tab)
+{
+	const struct mpq_status_bit *t;
+	bool first = true;
+
+	seq_printf(s, "0x%04x", v);
+	if (!v) {
+		seq_puts(s, " [clean]\n");
+		return;
+	}
+	seq_puts(s, " [");
+	for (t = tab; t->mask; t++) {
+		if (v & t->mask) {
+			if (!first)
+				seq_putc(s, ' ');
+			seq_puts(s, t->name);
+			first = false;
+		}
+	}
+	seq_puts(s, "]\n");
+}
+
+static int mpq8646_dbg_status_decoded_show(struct seq_file *s, void *unused)
+{
+	struct mpq8646_priv *priv = s->private;
+	int rc;
+
+	rc = i2c_smbus_read_word_data(priv->client, PMBUS_STATUS_WORD);
+	if (rc < 0) {
+		seq_printf(s, "ERROR: STATUS_WORD read failed (%d)\n", rc);
+		return 0;
+	}
+	seq_puts(s, "STATUS_WORD: ");
+	mpq8646_print_bits(s, (u16)rc, mpq8646_status_word_bits);
+	seq_puts(s, "(MPS extensions: bit12=NVM_SUMMARY, bit8=WATCH_DOG, bit0=DRMOS_FAULT)\n");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(mpq8646_dbg_status_decoded);
+
+static int mpq8646_dbg_protection_last_show(struct seq_file *s, void *unused)
+{
+	struct mpq8646_priv *priv = s->private;
+	int rc;
+
+	mutex_lock(&priv->mps_lock);
+	rc = i2c_smbus_read_word_data(priv->client, MPS_PROTECTION_LAST);
+	mutex_unlock(&priv->mps_lock);
+
+	if (rc < 0) {
+		seq_printf(s, "ERROR: PROTECTION_LAST read failed (%d)\n", rc);
+		return 0;
+	}
+	seq_puts(s, "PROTECTION_LAST: ");
+	mpq8646_print_bits(s, (u16)rc, mpq8646_protection_last_bits);
+	seq_puts(s, "(NVM-backed, survives chip POR; clear via clear_protection_last[_force])\n");
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(mpq8646_dbg_protection_last);
+
+struct mpq8646_dbg_reg {
+	u8		reg;
+	bool		is_word;
+	bool		writable;
+	const char	*name;
+};
+
+static const struct mpq8646_dbg_reg mpq8646_dbg_regs[] = {
+	/* MPS vendor extensions (read-only identity / observability) */
+	{ MPS_MFR_CONFIG_ID,        true,  false, "mfr_config_id" },
+	{ MPS_MFR_CONFIG_CODE_REV,  true,  false, "mfr_config_code_rev" },
+	{ MPS_MFR_SILICON_REV,      false, false, "mfr_silicon_rev" },
+	{ MPS_MFR_RETRY_TIMES,      true,  false, "mfr_retry_times" },
+	{ MPS_MFR_VBOOT_CFG,        true,  false, "mfr_vboot_cfg" },
+	/* MPS vendor extension -- user-writable product revision string */
+	{ MPS_MFR_PRODUCT_REV_USER, true,  true,  "mfr_product_rev_user" },
+	/* PMBus 1.3 standard timing / UVLO (read-only introspection) */
+	{ PMBUS_VIN_ON,             true,  false, "vin_on" },
+	{ PMBUS_VIN_OFF,            true,  false, "vin_off" },
+	{ PMBUS_TON_DELAY,          true,  false, "ton_delay" },
+	{ PMBUS_TON_RISE,           true,  false, "ton_rise" },
+	{ PMBUS_TOFF_DELAY,         true,  false, "toff_delay" },
+	{ PMBUS_TOFF_FALL,          true,  false, "toff_fall" },
+	/* PMBus 1.3 control / margin (read+write) */
+	{ PMBUS_ON_OFF_CONFIG,      false, true,  "on_off_config" },
+	{ PMBUS_VOUT_MARGIN_HIGH,   true,  true,  "vout_margin_high" },
+	{ PMBUS_VOUT_MARGIN_LOW,    true,  true,  "vout_margin_low" },
+	/* MPS PMBus-level write-protect (read+write) */
+	{ MPS_MFR_PMBUS_LOCK,       true,  true,  "mfr_pmbus_lock" },
+};
+
+struct mpq8646_dbg_reg_ctx {
+	struct mpq8646_priv		*priv;
+	const struct mpq8646_dbg_reg	*desc;
+};
+
+static int mpq8646_dbg_reg_show(struct seq_file *s, void *unused)
+{
+	struct mpq8646_dbg_reg_ctx *ctx = s->private;
+	int rc;
+
+	if (ctx->desc->is_word)
+		rc = i2c_smbus_read_word_data(ctx->priv->client,
+					      ctx->desc->reg);
+	else
+		rc = i2c_smbus_read_byte_data(ctx->priv->client,
+					      ctx->desc->reg);
+
+	if (rc < 0) {
+		seq_printf(s, "ERROR: reg 0x%02x (%s) read failed (%d)\n",
+			   ctx->desc->reg, ctx->desc->name, rc);
+		return 0;
+	}
+	seq_printf(s, "0x%0*x\n", ctx->desc->is_word ? 4 : 2, rc);
+	return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(mpq8646_dbg_reg);
+
+static int mpq8646_dbg_reg_get(void *data, u64 *val)
+{
+	struct mpq8646_dbg_reg_ctx *ctx = data;
+	int rc;
+
+	if (ctx->desc->is_word)
+		rc = i2c_smbus_read_word_data(ctx->priv->client,
+					      ctx->desc->reg);
+	else
+		rc = i2c_smbus_read_byte_data(ctx->priv->client,
+					      ctx->desc->reg);
+	if (rc < 0)
+		return rc;
+	*val = rc;
+	return 0;
+}
+
+static int mpq8646_dbg_reg_set(void *data, u64 val)
+{
+	struct mpq8646_dbg_reg_ctx *ctx = data;
+	int rc;
+
+	if (ctx->desc->is_word)
+		rc = i2c_smbus_write_word_data(ctx->priv->client,
+					       ctx->desc->reg, (u16)val);
+	else
+		rc = i2c_smbus_write_byte_data(ctx->priv->client,
+					       ctx->desc->reg, (u8)val);
+	return rc < 0 ? rc : 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_reg_rw_fops,
+			 mpq8646_dbg_reg_get, mpq8646_dbg_reg_set, "0x%llx\n");
+
+static int mpq8646_dbg_clear_protection_last(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+	int rc;
+
+	if (!val)
+		return 0;
+
+	mutex_lock(&priv->mps_lock);
+	rc = i2c_smbus_write_byte(priv->client, MPS_CLEAR_LAST_FAULT);
+	priv->last_probe_rc = rc;
+	priv->last_probe_data = 0;
+	mutex_unlock(&priv->mps_lock);
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_clear_protection_last_fops,
+			 NULL, mpq8646_dbg_clear_protection_last, "%llu\n");
+
+static int mpq8646_dbg_clear_protection_last_force(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+	int rc, last_rc;
+	int wp_orig, cfg_orig;
+
+	if (!val)
+		return 0;
+
+	pmbus_lock(priv->client);
+	mutex_lock(&priv->mps_lock);
+
+	wp_orig = i2c_smbus_read_byte_data(priv->client, PMBUS_WRITE_PROTECT);
+	if (wp_orig < 0) {
+		priv->last_probe_rc = wp_orig;
+		dev_warn(&priv->client->dev,
+			 "clear_protection_last_force: WRITE_PROTECT read failed (%d), aborting\n",
+			 wp_orig);
+		goto out;
+	}
+	cfg_orig = i2c_smbus_read_word_data(priv->client, MPS_MFR_CFG_EXT);
+	if (cfg_orig < 0) {
+		priv->last_probe_rc = cfg_orig;
+		dev_warn(&priv->client->dev,
+			 "clear_protection_last_force: MFR_CFG_EXT read failed (%d), aborting\n",
+			 cfg_orig);
+		goto out;
+	}
+
+	if (wp_orig != 0) {
+		rc = i2c_smbus_write_byte_data(priv->client,
+					       PMBUS_WRITE_PROTECT, 0);
+		if (rc < 0) {
+			priv->last_probe_rc = rc;
+			dev_warn(&priv->client->dev,
+				 "clear_protection_last_force: WP clear failed (%d), aborting\n",
+				 rc);
+			goto out;
+		}
+	}
+
+	rc = i2c_smbus_write_word_data(priv->client, MPS_MFR_CFG_EXT,
+				       (u16)cfg_orig | MPS_MFR_CFG_EXT_CLR_LAST_EN);
+	if (rc < 0) {
+		priv->last_probe_rc = rc;
+		dev_warn(&priv->client->dev,
+			 "clear_protection_last_force: gate open failed (%d)\n",
+			 rc);
+		goto restore_wp;
+	}
+
+	last_rc = i2c_smbus_write_byte(priv->client, MPS_CLEAR_LAST_FAULT);
+	priv->last_probe_rc = last_rc;
+	if (last_rc < 0)
+		dev_warn(&priv->client->dev,
+			 "clear_protection_last_force: CLEAR_LAST_FAULT failed (%d) even with gate open\n",
+			 last_rc);
+
+	for (int attempt = 0; attempt < MPQ8646_NVM_RETRY_MAX; attempt++) {
+		rc = i2c_smbus_write_word_data(priv->client, MPS_MFR_CFG_EXT,
+					       (u16)cfg_orig);
+		if (rc >= 0)
+			break;
+		usleep_range(MPQ8646_NVM_RETRY_DELAY_US_MIN,
+			     MPQ8646_NVM_RETRY_DELAY_US_MAX);
+	}
+	if (rc < 0)
+		dev_warn(&priv->client->dev,
+			 "clear_protection_last_force: MFR_CFG_EXT restore failed after retries (%d) -- gate may stay open until POR\n",
+			 rc);
+
+restore_wp:
+	if (wp_orig != 0) {
+		rc = i2c_smbus_write_byte_data(priv->client,
+					       PMBUS_WRITE_PROTECT,
+					       (u8)wp_orig);
+		if (rc < 0)
+			dev_warn(&priv->client->dev,
+				 "clear_protection_last_force: WP restore failed (%d)\n",
+				 rc);
+	}
+out:
+	mutex_unlock(&priv->mps_lock);
+	pmbus_unlock(priv->client);
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_clear_protection_last_force_fops,
+			 NULL, mpq8646_dbg_clear_protection_last_force, "%llu\n");
+
+static int mpq8646_dbg_store_all(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+	int rc;
+
+	if (!val)
+		return 0;
+
+	mutex_lock(&priv->mps_lock);
+	rc = i2c_smbus_write_byte(priv->client, PMBUS_STORE_USER_ALL);
+	priv->last_probe_rc = rc;
+	priv->last_probe_data = 0;
+	mutex_unlock(&priv->mps_lock);
+	if (rc < 0)
+		dev_warn(&priv->client->dev,
+			 "store_all: STORE_DEFAULT_ALL (0x11/0x15) write failed (%d)\n",
+			 rc);
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_store_all_fops,
+			 NULL, mpq8646_dbg_store_all, "%llu\n");
+
+static int mpq8646_dbg_restore_all(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+	int rc;
+
+	if (!val)
+		return 0;
+
+	mutex_lock(&priv->mps_lock);
+	rc = i2c_smbus_write_byte(priv->client, PMBUS_RESTORE_USER_ALL);
+	priv->last_probe_rc = rc;
+	priv->last_probe_data = 0;
+	mutex_unlock(&priv->mps_lock);
+	if (rc < 0)
+		dev_warn(&priv->client->dev,
+			 "restore_all: RESTORE_DEFAULT_ALL (0x12/0x16) write failed (%d)\n",
+			 rc);
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_restore_all_fops,
+			 NULL, mpq8646_dbg_restore_all, "%llu\n");
+
+static int mpq8646_dbg_poll_interval_get(void *data, u64 *val)
+{
+	struct mpq8646_priv *priv = data;
+
+	*val = priv->alarm_poll_interval_ms;
+	return 0;
+}
+
+static int mpq8646_dbg_poll_interval_set(void *data, u64 val)
+{
+	struct mpq8646_priv *priv = data;
+	bool was_off = !priv->alarm_poll_interval_ms;
+
+	priv->alarm_poll_interval_ms = (u32)val;
+
+	if (val && was_off && !priv->client->irq)
+		schedule_delayed_work(&priv->alarm_poll_work,
+				      msecs_to_jiffies((u32)val));
+	return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_poll_interval_fops,
+			 mpq8646_dbg_poll_interval_get,
+			 mpq8646_dbg_poll_interval_set, "%llu\n");
+
+#define MPQ8646_DEBUGFS_DIRNAME_MAX	16
+
+#define MPQ8646_DEBUGFS_ROOT_NAME	"mpq8646"
+
+/* Serialises creation of the shared debugfs root across concurrent probes */
+static DEFINE_MUTEX(mpq8646_debugfs_root_lock);
+
+static void mpq8646_debugfs_register(struct mpq8646_priv *priv)
+{
+	struct dentry *root, *parent;
+	char name[MPQ8646_DEBUGFS_DIRNAME_MAX];
+	bool parent_ref = false;
+	size_t i;
+
+	mutex_lock(&mpq8646_debugfs_root_lock);
+	parent = debugfs_lookup(MPQ8646_DEBUGFS_ROOT_NAME, NULL);
+	if (!parent)
+		parent = debugfs_create_dir(MPQ8646_DEBUGFS_ROOT_NAME, NULL);
+	else
+		parent_ref = true;
+	mutex_unlock(&mpq8646_debugfs_root_lock);
+	if (IS_ERR_OR_NULL(parent))
+		return;
+
+	snprintf(name, sizeof(name), "%d-%04x",
+		 i2c_adapter_id(priv->client->adapter), priv->client->addr);
+	root = debugfs_create_dir(name, parent);
+	if (parent_ref)
+		dput(parent);
+	if (IS_ERR_OR_NULL(root))
+		return;
+
+	priv->debugfs_root = root;
+
+	debugfs_create_file_unsafe("probe_smbus_rword", 0200, root, priv,
+				   &mpq8646_dbg_probe_smbus_rword_fops);
+	debugfs_create_file_unsafe("probe_smbus_rbyte", 0200, root, priv,
+				   &mpq8646_dbg_probe_smbus_rbyte_fops);
+	debugfs_create_file_unsafe("probe_raw_xfer", 0200, root, priv,
+				   &mpq8646_dbg_probe_raw_xfer_fops);
+	debugfs_create_file_unsafe("probe_clear_faults", 0200, root, priv,
+				   &mpq8646_dbg_probe_clear_faults_fops);
+	debugfs_create_bool("force_raw_xfer", 0600, root,
+			    &priv->debug_force_raw_xfer);
+	debugfs_create_u32("delay_us", 0600, root, &priv->debug_delay_us);
+	debugfs_create_file("last_probe", 0400, root, priv,
+			    &mpq8646_dbg_last_probe_fops);
+	debugfs_create_file("stats", 0400, root, priv,
+			    &mpq8646_dbg_stats_fops);
+	debugfs_create_file_unsafe("reset_stats", 0200, root, priv,
+				   &mpq8646_dbg_reset_stats_fops);
+	/* MPS extensions: status decode + NVM-backed PROTECTION_LAST */
+	debugfs_create_file("status_decoded", 0400, root, priv,
+			    &mpq8646_dbg_status_decoded_fops);
+	debugfs_create_file("protection_last", 0400, root, priv,
+			    &mpq8646_dbg_protection_last_fops);
+	debugfs_create_file_unsafe("clear_protection_last", 0200, root, priv,
+				   &mpq8646_dbg_clear_protection_last_fops);
+	debugfs_create_file_unsafe("clear_protection_last_force", 0200, root, priv,
+				   &mpq8646_dbg_clear_protection_last_force_fops);
+	debugfs_create_file_unsafe("store_all", 0200, root, priv,
+				   &mpq8646_dbg_store_all_fops);
+	debugfs_create_file_unsafe("restore_all", 0200, root, priv,
+				   &mpq8646_dbg_restore_all_fops);
+	debugfs_create_file_unsafe("alarm_poll_interval_ms", 0600, root, priv,
+				   &mpq8646_dbg_poll_interval_fops);
+
+	priv->dbg_reg_ctx = devm_kcalloc(&priv->client->dev,
+					 ARRAY_SIZE(mpq8646_dbg_regs),
+					 sizeof(*priv->dbg_reg_ctx),
+					 GFP_KERNEL);
+	if (!priv->dbg_reg_ctx)
+		return;
+	for (i = 0; i < ARRAY_SIZE(mpq8646_dbg_regs); i++) {
+		priv->dbg_reg_ctx[i].priv = priv;
+		priv->dbg_reg_ctx[i].desc = &mpq8646_dbg_regs[i];
+		if (mpq8646_dbg_regs[i].writable)
+			debugfs_create_file_unsafe(mpq8646_dbg_regs[i].name,
+						   0600, root,
+						   &priv->dbg_reg_ctx[i],
+						   &mpq8646_dbg_reg_rw_fops);
+		else
+			debugfs_create_file(mpq8646_dbg_regs[i].name, 0400,
+					    root, &priv->dbg_reg_ctx[i],
+					    &mpq8646_dbg_reg_fops);
+	}
+}
+
+static void mpq8646_debugfs_unregister(struct mpq8646_priv *priv)
+{
+	debugfs_remove_recursive(priv->debugfs_root);
+}
+#else
+static inline void mpq8646_debugfs_register(struct mpq8646_priv *priv) {}
+static inline void mpq8646_debugfs_unregister(struct mpq8646_priv *priv) {}
+#endif /* CONFIG_DEBUG_FS */
+
+static const struct {
+	u16 mask;
+	enum hwmon_sensor_types type;
+	u32 attr;
+	int channel;
+} mpq8646_alarm_map[] = {
+	{ PB_STATUS_INPUT,       hwmon_in,   hwmon_in_alarm,   0 },	/* in1_alarm  (VIN)  */
+	{ PB_STATUS_VOUT,        hwmon_in,   hwmon_in_alarm,   1 },	/* in2_alarm  (VOUT) */
+	{ PB_STATUS_IOUT_POUT,   hwmon_curr, hwmon_curr_alarm, 0 },	/* curr1_alarm        */
+	{ PB_STATUS_TEMPERATURE, hwmon_temp, hwmon_temp_alarm, 0 },	/* temp1_alarm        */
+};
+
+/* Adapted from lm90.c */
+static void mpq8646_alarm_poll_work(struct work_struct *work)
+{
+	struct mpq8646_priv *priv = container_of(to_delayed_work(work),
+						 struct mpq8646_priv,
+						 alarm_poll_work);
+	int rc;
+	u16 cur, newly_set;
+	size_t i;
+
+	if (priv->client->irq)
+		return;	/* SMBALERT# wired; polling not needed */
+
+	if (!priv->alarm_poll_interval_ms)
+		return;	/* polling disabled; don't re-arm */
+
+	if (!priv->hwmon_dev)
+		goto rearm;	/* hwmon not ready yet; try again next tick */
+
+	/* Serialise with the pmbus core's own transactions on this client. */
+	pmbus_lock(priv->client);
+	rc = i2c_smbus_read_word_data(priv->client, PMBUS_STATUS_WORD);
+	pmbus_unlock(priv->client);
+	if (rc < 0)
+		goto rearm;
+
+	cur = (u16)rc;
+	newly_set = cur & ~priv->last_status_word;
+	priv->last_status_word = cur;
+
+	if (!newly_set)
+		goto rearm;
+
+	for (i = 0; i < ARRAY_SIZE(mpq8646_alarm_map); i++) {
+		if (newly_set & mpq8646_alarm_map[i].mask)
+			hwmon_notify_event(priv->hwmon_dev,
+					   mpq8646_alarm_map[i].type,
+					   mpq8646_alarm_map[i].attr,
+					   mpq8646_alarm_map[i].channel);
+	}
+
+rearm:
+	if (priv->alarm_poll_interval_ms)
+		schedule_delayed_work(&priv->alarm_poll_work,
+				      msecs_to_jiffies(priv->alarm_poll_interval_ms));
+}
+
+static int mpq8646_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct pmbus_driver_info *info;
+	struct mpq8646_priv *priv;
+	u32 voltage_scale;
+	int ret;
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+	priv->client = client;
+	mutex_init(&priv->mps_lock);
+	memcpy(&priv->info, &mpq8646_info, sizeof(priv->info));
+	info = &priv->info;
+
+	info->identify = mpq8646_identify;
+	info->read_byte_data = mpq8646_read_byte_data;
+	info->read_word_data = mpq8646_read_word_data;
+	info->write_byte = mpq8646_write_byte;
+	info->write_word_data = mpq8646_write_word_data;
+	dev->platform_data = &mpq8646_no_pec_pdata;
+
+#if IS_ENABLED(CONFIG_REGULATOR)
+	info->reg_desc = mpq8646_reg_desc;
+	info->num_regulators = ARRAY_SIZE(mpq8646_reg_desc);
+#endif
+
+	INIT_DELAYED_WORK(&priv->alarm_poll_work, mpq8646_alarm_poll_work);
+	priv->alarm_poll_interval_ms = MPQ8646_ALARM_POLL_MS_DEFAULT;
+
+	if (!device_property_read_u32(dev, "mps,vout-fb-divider-ratio-permille",
+				      &voltage_scale)) {
+		if (voltage_scale > MPQ8646_VOUT_SCALE_LOOP_MAX)
+			return -EINVAL;
+
+		ret = i2c_smbus_write_word_data(client, PMBUS_VOUT_SCALE_LOOP,
+						voltage_scale);
+		if (ret)
+			return ret;
+	}
+
+	ret = pmbus_do_probe(client, info);
+	if (ret)
+		return ret;
+
+	mpq8646_debugfs_register(priv);
+
+	priv->hwmon_dev = pmbus_get_hwmon_device(client);
+	if (!client->irq)
+		schedule_delayed_work(&priv->alarm_poll_work,
+				      msecs_to_jiffies(priv->alarm_poll_interval_ms));
+
+#if IS_ENABLED(CONFIG_NVMEM)
+	{
+		struct nvmem_config cfg = {
+			.dev		= &client->dev,
+			.name		= dev_name(&client->dev),
+			.owner		= THIS_MODULE,
+			.read_only	= true,
+			.root_only	= true,
+			.word_size	= 1,
+			.stride		= 1,
+			.size		= MPQ8646_NVMEM_SIZE,
+			.reg_read	= mpq8646_nvmem_read,
+			.priv		= priv,
+		};
+		struct nvmem_device *nv = devm_nvmem_register(&client->dev, &cfg);
+
+		if (IS_ERR(nv))
+			dev_warn(&client->dev,
+				 "nvmem snapshot register failed (%pe)\n",
+				 nv);
+	}
+#endif
+	return 0;
+};
+
+static void mpq8646_remove(struct i2c_client *client)
+{
+	struct mpq8646_priv *priv = mpq8646_priv_from_client(client);
+
+	mpq8646_debugfs_unregister(priv);
+	cancel_delayed_work_sync(&priv->alarm_poll_work);
+}
+
+static struct i2c_driver mpq8646_driver = {
+	.driver = {
+		   .name = "mpq8646",
+		   .of_match_table = of_match_ptr(mpq8646_of_match),
+	},
+	.probe = mpq8646_probe,
+	.remove = mpq8646_remove,
+	.id_table = mpq8646_id,
+};
+
+module_i2c_driver(mpq8646_driver);
+
+MODULE_AUTHOR("Vincent Jardin <vjardin@free.fr>");
+MODULE_DESCRIPTION("PMBus driver for MPS MPQ8646 (extended observability)");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("PMBUS");

-- 
2.43.0



^ permalink raw reply related	[flat|nested] 7+ messages in thread

* Re: [PATCH v4 2/3] dt-bindings: hwmon: pmbus: add MPS MPQ8646 binding
  2026-07-23 21:38 ` [PATCH v4 2/3] dt-bindings: hwmon: pmbus: add MPS MPQ8646 binding Vincent Jardin via B4 Relay
@ 2026-07-23 21:45   ` sashiko-bot
  0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-07-23 21:45 UTC (permalink / raw)
  To: Vincent Jardin; +Cc: conor+dt, linux-hwmon, devicetree, robh

> From: Vincent Jardin <vjardin@free.fr>
> 
> Add a device-tree binding for the Monolithic Power Systems
> MPQ8646 step-down converter as a PMBus device.
> 
> The schema is inspired from the mpq8785e:
> same mps,vout-fb-divider-ratio-permille property (maximum: 2047),
> and const: mps,mpq8646.
> 
> Signed-off-by: Vincent Jardin <vjardin@free.fr>

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

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260723-mpq8646_v0-v4-0-b6323d5fc524@free.fr?part=2


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v4 1/3] hwmon: pmbus: event notification with alarms
  2026-07-23 21:38 ` [PATCH v4 1/3] hwmon: pmbus: event notification with alarms Vincent Jardin via B4 Relay
@ 2026-07-23 21:50   ` sashiko-bot
  0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-07-23 21:50 UTC (permalink / raw)
  To: Vincent Jardin; +Cc: conor+dt, robh, devicetree, linux-hwmon

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

Pre-existing issues:
- [High] This problem wasn't introduced by this patch, but the PMBus core notifies the wrong kobject when hardware monitoring events occur, functionally breaking IRQ-based notifications.
--

--- Patch [1]: [PATCH v4 1/3] hwmon: pmbus: event notification with alarms ---
commit eeca7812901a45049c95eb874da22e86da04ee7e
Author: Vincent Jardin <vjardin@free.fr>

hwmon: pmbus: event notification with alarms

Let's add an accessor for the hwmon class device the pmbus core
registered for an i2c_client. Export it with PMBUS so chip
drivers can call hwmon_notify_event() on per-sensor *_alarm
attributes from the driver's work items.

The needs: for boards when the chip's SMBALERT# pin is
not available for the CPU, pmbus_irq_setup() cannot deliver
poll(POLLPRI) wakes or udev change@... events on the inX_alarm
files. So the drivers can install a delayed_work-based polling
fallback (like the lm90 driver) that periodically reads STATUS_WORD
and calls hwmon_notify_event() on 0->1 transitions.

> diff --git a/drivers/hwmon/pmbus/pmbus_core.c b/drivers/hwmon/pmbus/pmbus_core.c
> index 3143b9e0316c4..6ba00ad732973 100644
> --- a/drivers/hwmon/pmbus/pmbus_core.c
> +++ b/drivers/hwmon/pmbus/pmbus_core.c
> @@ -695,6 +695,14 @@ const struct pmbus_driver_info *pmbus_get_driver_info(struct i2c_client *client)
>  }
>  EXPORT_SYMBOL_NS_GPL(pmbus_get_driver_info, "PMBUS");
>  
> +struct device *pmbus_get_hwmon_device(struct i2c_client *client)
> +{
> +	struct pmbus_data *data = i2c_get_clientdata(client);
> +
> +	return data->hwmon_dev;
> +}
> +EXPORT_SYMBOL_NS_GPL(pmbus_get_hwmon_device, "PMBUS");

[Severity: High]
This isn't a bug introduced by this patch, but the PMBus core notifies the
wrong kobject when hardware monitoring events occur.

While this patch adds a way for polling drivers to notify the hwmon device
properly, does the standard interrupt path silently fail to notify userspace?

When an interrupt is triggered, the event propagates through
pmbus_fault_handler() -> _pmbus_get_flags() -> pmbus_notify().

Looking at pmbus_notify() in drivers/hwmon/pmbus/pmbus_core.c, it appears to
notify the I2C parent device's kobject rather than the hwmon device:

drivers/hwmon/pmbus/pmbus_core.c:pmbus_notify() {
    ...
		if (reg == sreg && page == spage && (smask & flags)) {
			dev_dbg(data->dev, "sysfs notify: %s", da->attr.name);
			sysfs_notify(&data->dev->kobj, NULL, da->attr.name);
			kobject_uevent(&data->dev->kobj, KOBJ_CHANGE);
			flags &= ~smask;
		}
    ...
}

Since the sysfs attributes are registered on the hwmon class device
(data->hwmon_dev), doesn't calling sysfs_notify() and kobject_uevent() on
data->dev->kobj prevent userspace applications from receiving POLLPRI or udev
events when an interrupt triggers an alarm?

Could this be fixed to use data->hwmon_dev instead?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260723-mpq8646_v0-v4-0-b6323d5fc524@free.fr?part=1

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v4 3/3] hwmon: pmbus: add MPQ8646 driver
  2026-07-23 21:38 ` [PATCH v4 3/3] hwmon: pmbus: add MPQ8646 driver Vincent Jardin via B4 Relay
@ 2026-07-23 21:54   ` sashiko-bot
  0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-07-23 21:54 UTC (permalink / raw)
  To: Vincent Jardin; +Cc: conor+dt, linux-hwmon, robh, devicetree

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Stack-allocated variables are passed to `i2c_transfer()`, violating DMA safety constraints.
- [Medium] Incorrect channel indices in mpq8646_alarm_map cause off-by-one uevent generation for voltage alarms.
- [Low] The shared debugfs root directory 'mpq8646' is leaked upon module unload.
- [Low] The debugfs entry `probe_page_write` is documented but its implementation is missing.
- [Low] Spelling mistake in the documentation.
--

--- Patch [3]: [PATCH v4 3/3] hwmon: pmbus: add MPQ8646 driver ---
commit f8de07401397e8a8db3fd6012638f3548f012fb7
Author: Vincent Jardin <vjardin@free.fr>
Date:   Thu Jul 23 21:38:46 2026 +0000

    hwmon: pmbus: add MPQ8646 driver
    
    Add a new single-chip driver for the MPS MPQ8646 that is
    a PMBus device.
    
    Beyond basic PMBus telemetry, the driver adds:
      - MFR_CFG_EXT gate-close retry after CLEAR_LAST_FAULT.
      - alarm acknowledge via inX_reset_history.
      - STATUS_WORD MPS-extended bit decode + post-mortem clear.
      - In-driver alarm-poll fallback work item (thanks lm90) for
        boards without SMBALERT
      - on_off_config/vout_margin/mfr_pmbus_lock sysfs.

diff --git a/Documentation/hwmon/mpq8646.rst b/Documentation/hwmon/mpq8646.rst
new file mode 100644
index 0000000000000..61ca2a5387daa
--- /dev/null
+++ b/Documentation/hwmon/mpq8646.rst
@@ -0,0 +1,314 @@
[ ... ]
> +clear_protection_last_force performs the unlock with the following
> +six-step dansing:

[Severity: Low]
This isn't a bug, but there is a typo here (dansing instead of dancing).

[ ... ]
> +==========================  ==================================================
> +File                        Action
> +==========================  ==================================================
> +``probe_smbus_rword <reg>``  i2c_smbus_read_word_data on <reg>
> +``probe_smbus_rbyte <reg>``  i2c_smbus_read_byte_data on <reg>
> +``probe_raw_xfer <reg>``     raw i2c_transfer read-word on <reg>
> +``probe_page_write <pg>``    write PMBUS_PAGE = <pg>

[Severity: Low]
The documentation explicitly describes the probe_page_write debugfs entry,
but its implementation appears to be missing from the driver in
mpq8646_debugfs_register().

[ ... ]
diff --git a/drivers/hwmon/pmbus/mpq8646.c b/drivers/hwmon/pmbus/mpq8646.c
new file mode 100644
index 0000000000000..95abd347e3e9b
--- /dev/null
+++ b/drivers/hwmon/pmbus/mpq8646.c
@@ -0,0 +1,1237 @@
[ ... ]
> +static int mpq8646_raw_xfer_rword(struct i2c_client *client, u8 reg)
> +{
> +	u8 cmd = reg;
> +	__le16 data = 0;
> +	struct i2c_msg msg[] = {
> +		{
> +			.addr = client->addr,
> +			.flags = 0,
> +			.len = sizeof(cmd),
> +			.buf = &cmd,
> +		},
> +		{
> +			.addr = client->addr,
> +			.flags = I2C_M_RD,
> +			.len = sizeof(data),
> +			.buf = (u8 *)&data,
> +		},
> +	};

[Severity: High]
Are these stack-allocated variables safe for DMA operations?

Passing stack addresses like &cmd and &data in i2c_msg.buf to i2c_transfer()
violates the DMA API when the kernel is compiled with CONFIG_VMAP_STACK=y.
This could lead to a system crash if the underlying I2C controller driver
relies on DMA for transfers.

[ ... ]
> +static void mpq8646_debugfs_register(struct mpq8646_priv *priv)
> +{
> +	struct dentry *root, *parent;
> +	char name[MPQ8646_DEBUGFS_DIRNAME_MAX];
> +	bool parent_ref = false;
> +	size_t i;
> +
> +	mutex_lock(&mpq8646_debugfs_root_lock);
> +	parent = debugfs_lookup(MPQ8646_DEBUGFS_ROOT_NAME, NULL);
> +	if (!parent)
> +		parent = debugfs_create_dir(MPQ8646_DEBUGFS_ROOT_NAME, NULL);

[Severity: Low]
Does this leave a stale directory in debugfs upon module unload?

The shared mpq8646 root directory is created here, but there does not seem
to be a module_exit function handling global teardown to remove it. The
driver relies on module_i2c_driver(), and mpq8646_debugfs_unregister()
only removes the per-device subdirectories, which might leak this root
directory when the module is removed.

[ ... ]
> +static const struct {
> +	u16 mask;
> +	enum hwmon_sensor_types type;
> +	u32 attr;
> +	int channel;
> +} mpq8646_alarm_map[] = {
> +	{ PB_STATUS_INPUT,       hwmon_in,   hwmon_in_alarm,   0 },	/* in1_alarm  (VIN)  */
> +	{ PB_STATUS_VOUT,        hwmon_in,   hwmon_in_alarm,   1 },	/* in2_alarm  (VOUT) */

[Severity: Medium]
Will this correctly attribute the uevents to the intended channels?

Since hwmon_attr_base(hwmon_in) returns 0, hwmon_notify_event() requires
channel 1 for in1_alarm and channel 2 for in2_alarm.

Using 0 and 1 here in mpq8646_alarm_map would misattribute VOUT faults to
in1_alarm, and incorrectly attempt to generate events for in0_alarm which
does not exist.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260723-mpq8646_v0-v4-0-b6323d5fc524@free.fr?part=3

^ permalink raw reply related	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-07-23 21:54 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-23 21:38 [PATCH v4 0/3] hwmon: pmbus: add MPS MPQ8646 support Vincent Jardin via B4 Relay
2026-07-23 21:38 ` [PATCH v4 1/3] hwmon: pmbus: event notification with alarms Vincent Jardin via B4 Relay
2026-07-23 21:50   ` sashiko-bot
2026-07-23 21:38 ` [PATCH v4 2/3] dt-bindings: hwmon: pmbus: add MPS MPQ8646 binding Vincent Jardin via B4 Relay
2026-07-23 21:45   ` sashiko-bot
2026-07-23 21:38 ` [PATCH v4 3/3] hwmon: pmbus: add MPQ8646 driver Vincent Jardin via B4 Relay
2026-07-23 21:54   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox