diff options
| author | Vincent Jardin <vjardin@free.fr> | 2026-07-30 17:44:01 +0200 |
|---|---|---|
| committer | Guenter Roeck <linux@roeck-us.net> | 2026-08-10 08:59:43 -0700 |
| commit | 24be24bebc1bc6d529068a2e725984a498b2aa98 (patch) | |
| tree | f69df6946548e76134a5ea1d31963e00c061c6aa /drivers | |
| parent | e7ba3115134b670992562ffaab86853cad452274 (diff) | |
| download | linux-24be24bebc1bc6d529068a2e725984a498b2aa98.tar.gz linux-24be24bebc1bc6d529068a2e725984a498b2aa98.zip | |
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>
Diffstat (limited to 'drivers')
| -rw-r--r-- | drivers/hwmon/pmbus/Kconfig | 11 | ||||
| -rw-r--r-- | drivers/hwmon/pmbus/Makefile | 1 | ||||
| -rw-r--r-- | drivers/hwmon/pmbus/mpq8646.c | 688 |
3 files changed, 700 insertions, 0 deletions
diff --git a/drivers/hwmon/pmbus/Kconfig b/drivers/hwmon/pmbus/Kconfig index 2758f9695577..a1187c876784 100644 --- a/drivers/hwmon/pmbus/Kconfig +++ b/drivers/hwmon/pmbus/Kconfig @@ -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 diff --git a/drivers/hwmon/pmbus/Makefile b/drivers/hwmon/pmbus/Makefile index daa4b49bc90f..e288fe72a437 100644 --- a/drivers/hwmon/pmbus/Makefile +++ b/drivers/hwmon/pmbus/Makefile @@ -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 diff --git a/drivers/hwmon/pmbus/mpq8646.c b/drivers/hwmon/pmbus/mpq8646.c new file mode 100644 index 000000000000..5133a2471873 --- /dev/null +++ b/drivers/hwmon/pmbus/mpq8646.c @@ -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"); |
