hwmon: (pmbus) Add MPQ8646 driver

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

Beyond basic PMBus telemetry, the driver adds:
  - alarm acknowledge via inX_reset_history.
  - STATUS_WORD MPS-extended bit decode and the NVM-backed
    PROTECTION_LAST post-mortem, exposed as a read-only debugfs
    decoder.
  - In-driver alarm-poll fallback work item (thanks lm90) for
    boards without SMBALERT

Signed-off-by: Vincent Jardin <vjardin@free.fr>
Link: https://lore.kernel.org/r/20260730-mpq8646_v0-v7-3-e7c7ad768d5d@free.fr
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
This commit is contained in:
Vincent Jardin
2026-07-30 17:44:01 +02:00
committed by Guenter Roeck
parent e7ba311513
commit 24be24bebc
6 changed files with 969 additions and 0 deletions

View File

@@ -203,6 +203,7 @@ Hardware Monitoring Kernel Drivers
mp9941
mp9945
mpq82d00
mpq8646
mpq8785
nct6683
nct6775

View File

@@ -0,0 +1,261 @@
.. 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; it can be read
(decoded) via the ``protection_last`` debugfs entry 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 that
periodically invokes the pmbus core fault check,
``pmbus_check_and_notify_faults()``, which sends the notifications and
then clears the latched faults, exactly like the ``SMBALERT#``
interrupt path. The frequency of polling is tunable:
::
/sys/kernel/debug/i2c/i2c-<bus>/<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/i2c/i2c-<bus>/<bus>-<addr>/protection_last (RO)
/sys/kernel/debug/i2c/i2c-<bus>/<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``).
``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 adds a read-only
decode surface to the client's pmbus debugfs directory,
``/sys/kernel/debug/i2c/i2c-<bus>/<bus>-<addr>/``.
The driver intentionally exposes no raw register poke/peek debugfs;
use i2c-dev (``i2cget``/``i2cset``/``i2ctransfer``) for that.
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
========================== ================== =================================
The only writable entry is ``alarm_poll_interval_ms`` (the alarm-poll
worker cadence, see above), it is driver-local and never touches the
chip.
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

View File

@@ -18340,6 +18340,13 @@ S: Maintained
F: Documentation/hwmon/mpq82d00.rst
F: drivers/hwmon/pmbus/mpq82d00.c
MPS MPQ8646 PMBUS DRIVER
M: Vincent Jardin <vjardin@free.fr>
L: linux-hwmon@vger.kernel.org
S: Maintained
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

View File

@@ -626,6 +626,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

View File

@@ -61,6 +61,7 @@ obj-$(CONFIG_SENSORS_MP9945) += mp9945.o
obj-$(CONFIG_SENSORS_MPQ7932) += mpq7932.o
obj-$(CONFIG_SENSORS_MPQ82D00) += mpq82d00.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

View File

@@ -0,0 +1,688 @@
// 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/i2c.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_PROTECTION_LAST 0xFB
/* PMBus 1.3 timing / UVLO command codes */
#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_SILICON_REV 0xC3
#define MPS_MFR_RETRY_TIMES 0xF4
#define MPS_MFR_VBOOT_CFG 0xFC
#define MPQ8646_DEBUG(client, fmt, ...) \
dev_dbg(&(client)->dev, 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;
/* Serialises CLEAR_LAST_FAULT sequences and PROTECTION_LAST reads */
struct mutex mps_lock;
/* Set alarm_poll_interval_ms = 0 to disable. */
struct delayed_work alarm_poll_work;
u32 alarm_poll_interval_ms;
#ifdef CONFIG_DEBUG_FS
/* the only debugfs file that can re-arm the poll worker */
struct dentry *dbg_poll;
struct mpq8646_dbg_reg_ctx *dbg_reg_ctx;
#endif
};
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);
}
/*
* 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;
switch (vout_mode & PB_VOUT_MODE_MODE_MASK) {
case PB_VOUT_MODE_LINEAR:
info->format[PSC_VOLTAGE_OUT] = linear;
break;
case PB_VOUT_MODE_VID:
case PB_VOUT_MODE_DIRECT:
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)
{
int ret;
MPQ8646_DEBUG(client, "read_byte_data page=%d reg=0x%02x\n", page, reg);
switch (reg) {
case PMBUS_VOUT_MODE:
ret = pmbus_read_byte_data(client, page, reg);
MPQ8646_DEBUG(client, " VOUT_MODE raw=0x%02x ret=%d\n", ret, ret);
if (ret < 0)
return ret;
if ((ret & PB_VOUT_MODE_MODE_MASK) == PB_VOUT_MODE_VID)
return PB_VOUT_MODE_DIRECT;
return ret;
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_DEBUG(client, " unsupported mfr-info reg 0x%02x -> ENXIO\n", reg);
return -ENXIO;
default:
return -ENODATA;
}
}
/*
* 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_DEBUG(client, "reset_history virt reg=0x%04x -> CLEAR_FAULTS\n",
reg);
rc = i2c_smbus_write_byte(priv->client, PMBUS_CLEAR_FAULTS);
if (rc < 0)
MPQ8646_DEBUG(client, " 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)
{
int rc;
MPQ8646_DEBUG(client, "read_word_data page=%d phase=%d reg=0x%02x\n",
page, phase, reg);
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;
}
rc = i2c_smbus_read_word_data(client, reg);
MPQ8646_DEBUG(client, " 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);
guard(mutex)(&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];
}
}
return 0;
}
#endif /* CONFIG_NVMEM */
static const struct i2c_device_id mpq8646_id[] = {
{ .name = "mpq8646" },
{ },
};
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
/*
* Read-only decode cases in the client's pmbus debugfs directory:
* the MPS-specific STATUS_WORD and PROTECTION_LAST bit decode plus the
* identity/timing registers.
*/
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 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;
scoped_guard(pmbus_lock, priv->client)
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;
guard(pmbus_lock)(priv->client);
scoped_guard(mutex, &priv->mps_lock)
rc = i2c_smbus_read_word_data(priv->client, MPS_PROTECTION_LAST);
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)\n");
return 0;
}
DEFINE_SHOW_ATTRIBUTE(mpq8646_dbg_protection_last);
struct mpq8646_dbg_reg {
u8 reg;
bool is_word;
const char *name;
};
static const struct mpq8646_dbg_reg mpq8646_dbg_regs[] = {
/* MPS vendor extensions (read-only identity / observability) */
{ MPS_MFR_CONFIG_ID, true, "mfr_config_id" },
{ MPS_MFR_CONFIG_CODE_REV, true, "mfr_config_code_rev" },
{ MPS_MFR_SILICON_REV, false, "mfr_silicon_rev" },
{ MPS_MFR_RETRY_TIMES, true, "mfr_retry_times" },
{ MPS_MFR_VBOOT_CFG, true, "mfr_vboot_cfg" },
/* PMBus 1.3 standard timing / UVLO (read-only introspection) */
{ PMBUS_VIN_ON, true, "vin_on" },
{ PMBUS_VIN_OFF, true, "vin_off" },
{ PMBUS_TON_DELAY, true, "ton_delay" },
{ PMBUS_TON_RISE, true, "ton_rise" },
{ PMBUS_TOFF_DELAY, true, "toff_delay" },
{ PMBUS_TOFF_FALL, true, "toff_fall" },
};
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;
guard(pmbus_lock)(ctx->priv->client);
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_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");
static void mpq8646_debugfs_register(struct mpq8646_priv *priv)
{
struct dentry *root;
size_t i;
root = pmbus_get_debugfs_dir(priv->client);
if (!root)
return;
/* 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);
priv->dbg_poll =
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];
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(priv->dbg_poll);
}
#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 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);
if (priv->client->irq)
return; /* SMBALERT# wired; polling not needed */
if (!priv->alarm_poll_interval_ms)
return; /* polling disabled; don't re-arm */
pmbus_check_and_notify_faults(priv->client);
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_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);
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");