[PATCH v2 1/7] pmbus: add PMBus 1.x framework, CLI, and binding
Vincent Jardin
vjardin at free.fr
Thu Jul 2 21:56:03 CEST 2026
Add U-Boot's PMBus 1.x layer: the decoder/transport library, the
pmbus CLI command and a generic DT binding.
The subsequent commits provide the UCLASS_REGULATOR adapter and per-chip
drivers.
U-Boot's PMBus support is not a hwmon clone of Linux's
drivers/hwmon/pmbus/. Linux owns the runtime side (polling, sysfs,
alert IRQs, fan loops). U-Boot owns the boot-time side in order to,
- identify the PMBus regulators a board carries: MFR_ID/
MFR_MODEL/MFR_REVISION + sanity checks.
- print telemetry (VIN/VOUT/IIN/IOUT/POUT/TEMP) so an
operator can confirm rail voltages and faults before the kernel
- decode any chip alerts (STATUS_VOUT/STATUS_IOUT/STATUS_INPUT/
STATUS_TEMPERATURE/STATUS_CML) so a boot log shows why the
previous boot failed or the board had been power cycled because
of an outage (typically over temperature or under current).
Out of scope by design: no periodic polling, no sysfs, no fan-speed
control loop, no PMBUS_VIRT_* sensor virtualisation, no caching.
If a use case needs any of those, the answer should be "wait until
Linux comes up". It shall remain a thin layer.
The constants and structural shape (command codes, status bit names,
sensor-class enum, format enum, struct pmbus_driver_info) are
mirrored from Linux drivers/hwmon/pmbus/pmbus.h verbatim. The
decoders/encoders are reimplemented from the PMBus 1.3
specification because the surrounding hwmon context (struct
pmbus_data, sysfs caching, hwmon publication) does not apply.
The main benefits:
- One framework + CLI for any board carrying PMBus regulators:
no per-board PMBus implementation required anymore.
- Boards call pmbus_print_telemetry() / pmbus_print_status_word()
directly from boot init for a snapshot, sharing all decode +
format-dispatch with the CLI.
- Linux-compatible constants and DT binding so porting an existing
drivers/hwmon/pmbus/ chip is mechanical.
- Boot-time AVS/VID rail trim reuses the same decoders and
encoders as the CLI and the regulator path: no duplicate math.
Signed-off-by: Vincent Jardin <vjardin at free.fr>
---
Changes in v2:
- Apply Tom Rini's v1 review: drop the local DT binding YAML files
MAINTAINERS | 8 +
cmd/Kconfig | 14 +
cmd/Makefile | 1 +
cmd/pmbus.c | 816 +++++++++++++++++++++++++++++++++++++++
doc/develop/index.rst | 1 +
doc/develop/pmbus.rst | 633 +++++++++++++++++++++++++++++++
include/pmbus.h | 629 +++++++++++++++++++++++++++++++
lib/Kconfig | 16 +
lib/Makefile | 1 +
lib/pmbus.c | 859 ++++++++++++++++++++++++++++++++++++++++++
10 files changed, 2978 insertions(+)
create mode 100644 cmd/pmbus.c
create mode 100644 doc/develop/pmbus.rst
create mode 100644 include/pmbus.h
create mode 100644 lib/pmbus.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 571af196465..39e333f8f70 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1580,6 +1580,14 @@ S: Maintained
F: cmd/pci_mps.c
F: test/cmd/pci_mps.c
+PMBUS
+M: Vincent Jardin <vjardin at free.fr>
+S: Maintained
+F: cmd/pmbus.c
+F: doc/develop/pmbus.rst
+F: include/pmbus.h
+F: lib/pmbus.c
+
POWER
M: Jaehoon Chung <jh80.chung at samsung.com>
M: Peng Fan <peng.fan at nxp.com>
diff --git a/cmd/Kconfig b/cmd/Kconfig
index c71c6824a19..63f783ac0b9 100644
--- a/cmd/Kconfig
+++ b/cmd/Kconfig
@@ -2715,6 +2715,20 @@ config CMD_PMIC
- pmic write address - write byte to register at address
The only one change for this command is 'dev' subcommand.
+config CMD_PMBUS
+ bool "pmbus device interrogation and control command"
+ depends on PMBUS
+ help
+ Enable the pmbus U-Boot CLI command. Provides identification,
+ decoded telemetry, STATUS_* decoding, raw register read/write,
+ CLEAR_FAULTS, and VOUT_COMMAND set/get against any PMBus 1.x
+ compliant device selectable via 'pmbus dev <bus>:<addr>'.
+
+ Per chip drivers and board files publish vendor extensions in
+ the 'pmbus <vendor> ...' namespace.
+
+ See doc/develop/pmbus.rst for the full usage reference.
+
config CMD_REGULATOR
bool "Enable Driver Model REGULATOR command"
depends on DM_REGULATOR
diff --git a/cmd/Makefile b/cmd/Makefile
index bbbdfcc4ded..ce772e5555b 100644
--- a/cmd/Makefile
+++ b/cmd/Makefile
@@ -228,6 +228,7 @@ obj-$(CONFIG_CMD_AXI) += axi.o
obj-$(CONFIG_CMD_PVBLOCK) += pvblock.o
# Power
+obj-$(CONFIG_CMD_PMBUS) += pmbus.o
obj-$(CONFIG_CMD_PMIC) += pmic.o
obj-$(CONFIG_CMD_REGULATOR) += regulator.o
diff --git a/cmd/pmbus.c b/cmd/pmbus.c
new file mode 100644
index 00000000000..5ddaf3665fc
--- /dev/null
+++ b/cmd/pmbus.c
@@ -0,0 +1,816 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2026 Free Mobile, Vincent Jardin
+ *
+ * pmbus U-Boot CLI command.
+ *
+ * Generic command surface over the PMBus 1.x framework defined in
+ * <pmbus.h> + lib/pmbus.c.
+ *
+ * See doc/develop/pmbus.rst for the full usage reference.
+ */
+
+#include <command.h>
+#include <dm.h>
+#include <i2c.h>
+#include <log.h>
+#include <pmbus.h>
+#include <vsprintf.h>
+#include <linux/ctype.h>
+#include <power/regulator.h>
+
+static int parse_bus_addr(const char *s, int *bus_seq, u8 *addr)
+{
+ char busbuf[8];
+ const char *colon;
+ unsigned long b, a;
+ size_t buslen;
+
+ colon = strchr(s, ':');
+ if (!colon)
+ return -EINVAL;
+ buslen = colon - s;
+ if (buslen == 0 || buslen >= sizeof(busbuf))
+ return -EINVAL;
+ memcpy(busbuf, s, buslen);
+ busbuf[buslen] = '\0';
+ if (strict_strtoul(busbuf, 0, &b))
+ return -EINVAL;
+ if (strict_strtoul(colon + 1, 0, &a) || a > 0x7f)
+ return -EINVAL;
+ *bus_seq = (int)b;
+ *addr = (u8)a;
+ return 0;
+}
+
+static const struct {
+ const char *name;
+ u8 reg;
+} pmbus_reg_syms[] = {
+ { "PAGE", PMBUS_PAGE },
+ { "OPERATION", PMBUS_OPERATION },
+ { "ON_OFF_CONFIG", PMBUS_ON_OFF_CONFIG },
+ { "CLEAR_FAULTS", PMBUS_CLEAR_FAULTS },
+ { "WRITE_PROTECT", PMBUS_WRITE_PROTECT },
+ { "CAPABILITY", PMBUS_CAPABILITY },
+ { "VOUT_MODE", PMBUS_VOUT_MODE },
+ { "VOUT_COMMAND", PMBUS_VOUT_COMMAND },
+ { "VOUT_TRIM", PMBUS_VOUT_TRIM },
+ { "VOUT_MAX", PMBUS_VOUT_MAX },
+ { "VOUT_SCALE_LOOP", PMBUS_VOUT_SCALE_LOOP },
+ { "STATUS_BYTE", PMBUS_STATUS_BYTE },
+ { "STATUS_WORD", PMBUS_STATUS_WORD },
+ { "STATUS_VOUT", PMBUS_STATUS_VOUT },
+ { "STATUS_IOUT", PMBUS_STATUS_IOUT },
+ { "STATUS_INPUT", PMBUS_STATUS_INPUT },
+ { "STATUS_TEMP", PMBUS_STATUS_TEMPERATURE },
+ { "STATUS_CML", PMBUS_STATUS_CML },
+ { "READ_VIN", PMBUS_READ_VIN },
+ { "READ_IIN", PMBUS_READ_IIN },
+ { "READ_VOUT", PMBUS_READ_VOUT },
+ { "READ_IOUT", PMBUS_READ_IOUT },
+ { "READ_TEMP1", PMBUS_READ_TEMPERATURE_1 },
+ { "READ_TEMP2", PMBUS_READ_TEMPERATURE_2 },
+ { "READ_TEMP3", PMBUS_READ_TEMPERATURE_3 },
+ { "READ_DUTY", PMBUS_READ_DUTY_CYCLE },
+ { "READ_FREQ", PMBUS_READ_FREQUENCY },
+ { "READ_POUT", PMBUS_READ_POUT },
+ { "READ_PIN", PMBUS_READ_PIN },
+ { "REVISION", PMBUS_REVISION },
+ { "MFR_ID", PMBUS_MFR_ID },
+ { "MFR_MODEL", PMBUS_MFR_MODEL },
+ { "MFR_REVISION", PMBUS_MFR_REVISION },
+};
+
+static int parse_reg(const char *s, u8 *reg)
+{
+ unsigned long v;
+ unsigned int i;
+
+ if (isdigit((unsigned char)s[0])) {
+ if (strict_strtoul(s, 0, &v) || v > 0xff)
+ return -EINVAL;
+ *reg = (u8)v;
+ return 0;
+ }
+ for (i = 0; i < ARRAY_SIZE(pmbus_reg_syms); i++) {
+ if (!strcasecmp(s, pmbus_reg_syms[i].name)) {
+ *reg = pmbus_reg_syms[i].reg;
+ return 0;
+ }
+ }
+ return -EINVAL;
+}
+
+static const char *pmbus_reg_name(u8 reg)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(pmbus_reg_syms); i++)
+ if (pmbus_reg_syms[i].reg == reg)
+ return pmbus_reg_syms[i].name;
+ return "?";
+}
+
+static int require_active(struct udevice **chip,
+ const struct pmbus_active_dev **act)
+{
+ *act = pmbus_active();
+ if (!*act) {
+ printf("pmbus: no active device. Use 'pmbus dev <bus>:<addr>' first.\n");
+ return CMD_RET_FAILURE;
+ }
+ if (pmbus_active_get_i2c(chip)) {
+ printf("pmbus: cannot reach i2c%d:0x%02x\n",
+ (*act)->bus_seq, (*act)->addr);
+ return CMD_RET_FAILURE;
+ }
+ return CMD_RET_SUCCESS;
+}
+
+static void print_micro(s64 micro, const char *unit)
+{
+ s64 abs_milli;
+
+ abs_milli = (micro < 0 ? -micro : micro) / 1000LL;
+ printf("%lld.%03lld%s",
+ (long long)(micro / 1000000LL),
+ (long long)(abs_milli % 1000LL), unit);
+}
+
+static void print_active(const struct pmbus_active_dev *act)
+{
+ printf("pmbus: active i2c%d:0x%02x", act->bus_seq, act->addr);
+ if (act->name[0])
+ printf(" rail=\"%s\"", act->name);
+ printf(" MFR_ID=\"%s\" MODEL=\"%s\" vendor=%s%s\n",
+ act->mfr_id[0] ? act->mfr_id : "?",
+ act->mfr_model[0] ? act->mfr_model : "?",
+ act->vendor[0] ? act->vendor : "(generic)",
+ act->info ? "" : " [no driver_info]");
+}
+
+static int do_dev(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ const struct pmbus_active_dev *act;
+ int bus_seq, ret;
+ u8 addr;
+
+ if (argc < 2) {
+ act = pmbus_active();
+ if (!act) {
+ printf("pmbus: no active device\n");
+ return CMD_RET_SUCCESS;
+ }
+ print_active(act);
+ return CMD_RET_SUCCESS;
+ }
+
+ if (parse_bus_addr(argv[1], &bus_seq, &addr) < 0) {
+ ret = pmbus_resolve_by_name(argv[1], &bus_seq, &addr);
+ if (ret) {
+ printf("pmbus: '%s' is neither <bus>:<addr> nor a known regulator-name (%d)\n",
+ argv[1], ret);
+ return CMD_RET_FAILURE;
+ }
+ }
+ ret = pmbus_set_active(bus_seq, addr);
+ if (ret) {
+ printf("pmbus: cannot select i2c%d:0x%02x (%d)\n",
+ bus_seq, addr, ret);
+ return CMD_RET_FAILURE;
+ }
+ act = pmbus_active();
+ if (act)
+ print_active(act);
+ return CMD_RET_SUCCESS;
+}
+
+static int do_list(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ struct udevice *dev;
+ struct uclass *uc;
+ int ret, n = 0;
+
+ if (!IS_ENABLED(CONFIG_DM_REGULATOR)) {
+ printf("pmbus: CONFIG_DM_REGULATOR not enabled in this build. Use 'pmbus dev <bus>:<addr>'.\n");
+ return CMD_RET_SUCCESS;
+ }
+
+ ret = uclass_get(UCLASS_REGULATOR, &uc);
+ if (ret) {
+ printf("pmbus: UCLASS_REGULATOR not available\n");
+ return CMD_RET_SUCCESS;
+ }
+ uclass_foreach_dev(dev, uc) {
+ struct dm_regulator_uclass_plat *up = dev_get_uclass_plat(dev);
+ struct udevice *parent = dev_get_parent(dev);
+ const char *rname = (up && up->name) ? up->name : "";
+ const char *drv = (dev->driver && dev->driver->name)
+ ? dev->driver->name : "?";
+ int bus_seq = -1;
+ int addr = -1;
+
+ if (parent && device_get_uclass_id(parent) == UCLASS_I2C) {
+ bus_seq = dev_seq(parent);
+ addr = dev_read_addr(dev);
+ }
+
+ if (n == 0)
+ printf("UCLASS_REGULATOR devices (no PMBus filter):\n");
+ if (bus_seq >= 0 && addr >= 0) {
+ printf(" i2c%d:0x%02x rail=\"%s\" node=%s driver=%s\n",
+ bus_seq, addr, rname, dev->name, drv);
+ } else {
+ printf(" (non-I2C) rail=\"%s\" node=%s driver=%s\n",
+ rname, dev->name, drv);
+ }
+ n++;
+ }
+ if (!n)
+ printf("pmbus: no UCLASS_REGULATOR devices bound. Use 'pmbus dev <bus>:<addr>' to select a chip directly.\n");
+ return CMD_RET_SUCCESS;
+}
+
+static int do_info(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ static const char * const cls_names[PSC_NUM_CLASSES] = {
+ "VOLTAGE_IN", "VOLTAGE_OUT", "CURRENT_IN", "CURRENT_OUT",
+ "POWER", "TEMPERATURE",
+ };
+ static const char * const fmt_names[] = {
+ "LINEAR", "IEEE754", "DIRECT", "VID",
+ };
+ const struct pmbus_active_dev *act;
+ struct udevice *chip;
+ int rc, c, rrev;
+ u8 rev = 0;
+
+ rc = require_active(&chip, &act);
+ if (rc)
+ return rc;
+
+ rrev = pmbus_read_byte(chip, PMBUS_REVISION, &rev);
+
+ printf("pmbus device i2c%d:0x%02x\n", act->bus_seq, act->addr);
+ if (act->name[0])
+ printf(" regulator-name: \"%s\"\n", act->name);
+ printf(" MFR_ID : \"%s\"\n", act->mfr_id[0] ? act->mfr_id : "?");
+ printf(" MFR_MODEL : \"%s\"\n", act->mfr_model[0] ? act->mfr_model : "?");
+
+ /*
+ * MFR_REVISION may encodes the revision as a non printable byte
+ * (BCD nibbles, packed major / minor, etc.). Show both the
+ * printable form and the raw bytes the chip returned.
+ */
+ {
+ u8 raw[PMBUS_MFR_STRING_MAX];
+ int len, i;
+
+ if (dm_i2c_read(chip, PMBUS_MFR_REVISION, raw, 1) ||
+ raw[0] < 1 || raw[0] > sizeof(raw) - 1 ||
+ dm_i2c_read(chip, PMBUS_MFR_REVISION, raw, raw[0] + 1)) {
+ printf(" MFR_REVISION : \"%s\"\n",
+ act->mfr_revision[0] ? act->mfr_revision : "?");
+ } else {
+ len = raw[0];
+ printf(" MFR_REVISION : \"%s\" raw=0x",
+ act->mfr_revision[0] ? act->mfr_revision : "?");
+ for (i = 1; i <= len; i++)
+ printf("%02x", raw[i]);
+ printf("\n");
+ }
+ }
+
+ if (rrev)
+ printf(" PMBUS_REVISION: <read failed (%d)>\n", rrev);
+ else
+ printf(" PMBUS_REVISION: 0x%02x (%s)\n", rev,
+ rev == PMBUS_REV_13 ? "PMBus 1.3" :
+ rev == PMBUS_REV_12 ? "PMBus 1.2" :
+ rev == PMBUS_REV_11 ? "PMBus 1.1" :
+ rev == PMBUS_REV_10 ? "PMBus 1.0" : "unknown");
+ printf(" vendor : %s\n", act->vendor[0] ? act->vendor : "(none)");
+
+ if (!act->info) {
+ printf(" driver_info : not registered (decoders fall back to LINEAR16 / LINEAR11)\n");
+ return CMD_RET_SUCCESS;
+ }
+ printf(" driver_info : pages=%d\n", act->info->pages);
+ for (c = 0; c < PSC_NUM_CLASSES; c++) {
+ printf(" [%-12s] format=%s",
+ cls_names[c], fmt_names[act->info->format[c]]);
+ if (act->info->format[c] == pmbus_fmt_direct)
+ printf(", m=%d, b=%d, R=%d",
+ act->info->m[c], act->info->b[c], act->info->R[c]);
+ printf("\n");
+ }
+ return CMD_RET_SUCCESS;
+}
+
+static int do_telemetry(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ const struct pmbus_active_dev *act;
+ struct udevice *chip;
+ int rc;
+
+ rc = require_active(&chip, &act);
+ if (rc)
+ return rc;
+
+ printf("pmbus telemetry @ i2c%d:0x%02x\n", act->bus_seq, act->addr);
+ pmbus_print_telemetry(chip);
+ return CMD_RET_SUCCESS;
+}
+
+static int do_status(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ const struct pmbus_active_dev *act;
+ struct udevice *chip;
+ u16 word = 0;
+ u8 b;
+ int rc;
+
+ rc = require_active(&chip, &act);
+ if (rc)
+ return rc;
+
+ if (pmbus_read_word(chip, PMBUS_STATUS_WORD, &word)) {
+ printf("pmbus: STATUS_WORD read failed\n");
+ return CMD_RET_FAILURE;
+ }
+
+ const struct pmbus_status_override *ovr =
+ act->info ? act->info->status_overrides : NULL;
+
+ printf("pmbus status @ i2c%d:0x%02x\n", act->bus_seq, act->addr);
+ printf(" STATUS_WORD (79h) = 0x%04x [", word);
+ pmbus_print_status_bits(PMBUS_STATUS_WORD, word,
+ pmbus_status_word_bits, ovr);
+ printf("]\n");
+
+ if (pmbus_read_byte(chip, PMBUS_STATUS_VOUT, &b) == 0) {
+ printf(" STATUS_VOUT (7Ah) = 0x%02x [", b);
+ pmbus_print_status_bits(PMBUS_STATUS_VOUT, b,
+ pmbus_status_vout_bits, ovr);
+ printf("]\n");
+ }
+ if (pmbus_read_byte(chip, PMBUS_STATUS_IOUT, &b) == 0) {
+ printf(" STATUS_IOUT (7Bh) = 0x%02x [", b);
+ pmbus_print_status_bits(PMBUS_STATUS_IOUT, b,
+ pmbus_status_iout_bits, ovr);
+ printf("]\n");
+ }
+ if (pmbus_read_byte(chip, PMBUS_STATUS_INPUT, &b) == 0) {
+ printf(" STATUS_INPUT (7Ch) = 0x%02x [", b);
+ pmbus_print_status_bits(PMBUS_STATUS_INPUT, b,
+ pmbus_status_input_bits, ovr);
+ printf("]\n");
+ }
+ if (pmbus_read_byte(chip, PMBUS_STATUS_TEMPERATURE, &b) == 0) {
+ printf(" STATUS_TEMP (7Dh) = 0x%02x [", b);
+ pmbus_print_status_bits(PMBUS_STATUS_TEMPERATURE, b,
+ pmbus_status_temp_bits, ovr);
+ printf("]\n");
+ }
+ if (pmbus_read_byte(chip, PMBUS_STATUS_CML, &b) == 0) {
+ printf(" STATUS_CML (7Eh) = 0x%02x [", b);
+ pmbus_print_status_bits(PMBUS_STATUS_CML, b,
+ pmbus_status_cml_bits, ovr);
+ printf("]\n");
+ }
+
+ return CMD_RET_SUCCESS;
+}
+
+static int do_dump(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ const struct pmbus_active_dev *act;
+ struct udevice *chip;
+ unsigned int i;
+ int rc;
+
+ rc = require_active(&chip, &act);
+ if (rc)
+ return rc;
+
+ printf("pmbus dump @ i2c%d:0x%02x (registers known to <pmbus.h>)\n",
+ act->bus_seq, act->addr);
+ for (i = 0; i < ARRAY_SIZE(pmbus_reg_syms); i++) {
+ u8 reg = pmbus_reg_syms[i].reg;
+ u8 b = 0;
+ u16 w = 0;
+
+ switch (reg) {
+ case PMBUS_PAGE:
+ case PMBUS_OPERATION:
+ case PMBUS_ON_OFF_CONFIG:
+ case PMBUS_WRITE_PROTECT:
+ case PMBUS_CAPABILITY:
+ case PMBUS_VOUT_MODE:
+ case PMBUS_STATUS_BYTE:
+ case PMBUS_STATUS_VOUT:
+ case PMBUS_STATUS_IOUT:
+ case PMBUS_STATUS_INPUT:
+ case PMBUS_STATUS_TEMPERATURE:
+ case PMBUS_STATUS_CML:
+ case PMBUS_REVISION:
+ if (pmbus_read_byte(chip, reg, &b) == 0)
+ printf(" %02xh %-15s b=0x%02x\n",
+ reg, pmbus_reg_syms[i].name, b);
+ break;
+ case PMBUS_MFR_ID:
+ case PMBUS_MFR_MODEL:
+ case PMBUS_MFR_REVISION: {
+ char s[PMBUS_MFR_STRING_MAX];
+
+ if (pmbus_read_string(chip, reg, s, sizeof(s),
+ act->info ? false : false) >= 0)
+ printf(" %02xh %-15s s=\"%s\"\n",
+ reg, pmbus_reg_syms[i].name, s);
+ break;
+ }
+ default:
+ if (pmbus_read_word(chip, reg, &w) == 0)
+ printf(" %02xh %-15s w=0x%04x\n",
+ reg, pmbus_reg_syms[i].name, w);
+ break;
+ }
+ }
+ return CMD_RET_SUCCESS;
+}
+
+static int do_read(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ const struct pmbus_active_dev *act;
+ struct udevice *chip;
+ const char *fmt = "b";
+ u8 reg, b;
+ u16 w;
+ char s[PMBUS_MFR_STRING_MAX];
+ int rc, ret;
+
+ if (argc < 2)
+ return CMD_RET_USAGE;
+ rc = require_active(&chip, &act);
+ if (rc)
+ return rc;
+ if (parse_reg(argv[1], ®) < 0) {
+ printf("pmbus: invalid register '%s'\n", argv[1]);
+ return CMD_RET_USAGE;
+ }
+ if (argc >= 3)
+ fmt = argv[2];
+
+ if (!strcmp(fmt, "b")) {
+ ret = pmbus_read_byte(chip, reg, &b);
+ if (ret) {
+ printf("pmbus: read byte 0x%02x failed (%d)\n", reg, ret);
+ return CMD_RET_FAILURE;
+ }
+ printf(" %02xh %-15s b=0x%02x\n", reg, pmbus_reg_name(reg), b);
+ } else if (!strcmp(fmt, "w")) {
+ ret = pmbus_read_word(chip, reg, &w);
+ if (ret) {
+ printf("pmbus: read word 0x%02x failed (%d)\n", reg, ret);
+ return CMD_RET_FAILURE;
+ }
+ printf(" %02xh %-15s w=0x%04x\n", reg, pmbus_reg_name(reg), w);
+ } else if (!strcmp(fmt, "s")) {
+ ret = pmbus_read_string(chip, reg, s, sizeof(s), false);
+ if (ret < 0) {
+ printf("pmbus: read string 0x%02x failed (%d)\n", reg, ret);
+ return CMD_RET_FAILURE;
+ }
+ printf(" %02xh %-15s s=\"%s\"\n", reg, pmbus_reg_name(reg), s);
+ } else {
+ printf("pmbus: unknown format '%s' (expected b, w, or s)\n", fmt);
+ return CMD_RET_USAGE;
+ }
+ return CMD_RET_SUCCESS;
+}
+
+static int do_write(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ const struct pmbus_active_dev *act;
+ struct udevice *chip;
+ const char *fmt = "b";
+ unsigned long val;
+ u8 reg, b;
+ u8 buf[2];
+ int rc, ret;
+
+ if (argc < 3)
+ return CMD_RET_USAGE;
+ rc = require_active(&chip, &act);
+ if (rc)
+ return rc;
+ if (parse_reg(argv[1], ®) < 0) {
+ printf("pmbus: invalid register '%s'\n", argv[1]);
+ return CMD_RET_USAGE;
+ }
+ if (strict_strtoul(argv[2], 0, &val)) {
+ printf("pmbus: invalid value '%s'\n", argv[2]);
+ return CMD_RET_USAGE;
+ }
+ if (argc >= 4)
+ fmt = argv[3];
+
+ if (!strcmp(fmt, "b")) {
+ if (val > 0xff) {
+ printf("pmbus: byte value out of range\n");
+ return CMD_RET_USAGE;
+ }
+ b = (u8)val;
+ ret = dm_i2c_write(chip, reg, &b, 1);
+ } else if (!strcmp(fmt, "w")) {
+ if (val > 0xffff) {
+ printf("pmbus: word value out of range\n");
+ return CMD_RET_USAGE;
+ }
+ buf[0] = (u8)(val & 0xff);
+ buf[1] = (u8)((val >> 8) & 0xff);
+ ret = dm_i2c_write(chip, reg, buf, 2);
+ } else {
+ printf("pmbus: unknown format '%s' (expected b or w)\n", fmt);
+ return CMD_RET_USAGE;
+ }
+ if (ret) {
+ printf("pmbus: write 0x%02x failed (%d)\n", reg, ret);
+ return CMD_RET_FAILURE;
+ }
+ printf("pmbus: wrote 0x%lx to %02xh (%s)\n", val, reg, pmbus_reg_name(reg));
+ return CMD_RET_SUCCESS;
+}
+
+static int do_clear(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ const struct pmbus_active_dev *act;
+ struct udevice *chip;
+ int rc, ret;
+
+ if (argc >= 2 && strcmp(argv[1], "faults") != 0) {
+ printf("pmbus: unknown clear subcommand '%s' (expected 'faults')\n",
+ argv[1]);
+ return CMD_RET_USAGE;
+ }
+ rc = require_active(&chip, &act);
+ if (rc)
+ return rc;
+ ret = pmbus_clear_faults(chip);
+ if (ret) {
+ printf("pmbus: CLEAR_FAULTS (03h) failed (%d)\n", ret);
+ return CMD_RET_FAILURE;
+ }
+ printf("pmbus: CLEAR_FAULTS (03h) issued (RAM sticky STATUS_* cleared)\n");
+ return CMD_RET_SUCCESS;
+}
+
+static int do_vout(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ const struct pmbus_active_dev *act;
+ struct udevice *chip;
+ u8 vout_mode = 0;
+ u16 raw;
+ s64 uv;
+ int rc, ret;
+
+ rc = require_active(&chip, &act);
+ if (rc)
+ return rc;
+
+ if (pmbus_read_byte(chip, PMBUS_VOUT_MODE, &vout_mode)) {
+ printf("pmbus: VOUT_MODE read failed\n");
+ return CMD_RET_FAILURE;
+ }
+
+ if (argc < 2) {
+ if (pmbus_read_word(chip, PMBUS_READ_VOUT, &raw)) {
+ printf("pmbus: READ_VOUT failed\n");
+ return CMD_RET_FAILURE;
+ }
+ if (act->info)
+ uv = pmbus_reg2data(act->info, PSC_VOLTAGE_OUT, raw, vout_mode);
+ else
+ uv = pmbus_reg2data_linear16(raw, vout_mode);
+ printf("pmbus VOUT @ i2c%d:0x%02x raw=0x%04x ",
+ act->bus_seq, act->addr, raw);
+ print_micro(uv, "V");
+ printf("\n");
+ return CMD_RET_SUCCESS;
+ }
+
+ {
+ unsigned long target_uv;
+ const char *fmt_name;
+ u8 buf[2];
+
+ if (strict_strtoul(argv[1], 0, &target_uv)) {
+ printf("pmbus: invalid microvolt value '%s'\n", argv[1]);
+ return CMD_RET_USAGE;
+ }
+
+ switch (vout_mode & PB_VOUT_MODE_MODE_MASK) {
+ case PB_VOUT_MODE_LINEAR:
+ raw = pmbus_data2reg_linear16((s64)target_uv, vout_mode);
+ fmt_name = "LINEAR16";
+ break;
+ case PB_VOUT_MODE_DIRECT:
+ if (!act->info ||
+ act->info->format[PSC_VOLTAGE_OUT] != pmbus_fmt_direct) {
+ printf("pmbus: VOUT_MODE selects DIRECT but the active driver_info has no DIRECT coefficients for VOLTAGE_OUT\n");
+ return CMD_RET_FAILURE;
+ }
+ raw = pmbus_data2reg_direct((s64)target_uv,
+ act->info->m[PSC_VOLTAGE_OUT],
+ act->info->b[PSC_VOLTAGE_OUT],
+ act->info->R[PSC_VOLTAGE_OUT]);
+ fmt_name = "DIRECT";
+ break;
+ default:
+ printf("pmbus: VOUT_MODE 0x%02x selects an encoder not yet implemented (VID / IEEE754)\n",
+ vout_mode);
+ return CMD_RET_FAILURE;
+ }
+
+ buf[0] = raw & 0xff;
+ buf[1] = (raw >> 8) & 0xff;
+ ret = dm_i2c_write(chip, PMBUS_VOUT_COMMAND, buf, 2);
+ if (ret) {
+ printf("pmbus: VOUT_COMMAND write failed (%d)\n", ret);
+ return CMD_RET_FAILURE;
+ }
+ printf("pmbus: VOUT_COMMAND <- 0x%04x (%s, target %lu uV)\n",
+ raw, fmt_name, target_uv);
+ }
+ return CMD_RET_SUCCESS;
+}
+
+static int pmbus_scan_one_bus(struct udevice *bus, int bus_seq)
+{
+ int hits = 0;
+ int addr;
+
+ for (addr = 0x08; addr <= 0x77; addr++) {
+ struct udevice *chip;
+ u8 b;
+
+ if (i2c_get_chip(bus, addr, 1, &chip))
+ continue;
+ /* MFR_ID block read: 1st byte is the length of the string. */
+ if (dm_i2c_read(chip, PMBUS_MFR_ID, &b, 1))
+ continue;
+ if (b >= 1 && b <= PMBUS_MFR_STRING_MAX - 1) {
+ char s[PMBUS_MFR_STRING_MAX] = "";
+
+ pmbus_read_string(chip, PMBUS_MFR_ID, s, sizeof(s), false);
+ if (!s[0])
+ pmbus_read_string(chip, PMBUS_MFR_ID, s, sizeof(s), true);
+ printf(" i2c%d:0x%02x MFR_ID=\"%s\"\n",
+ bus_seq, addr, s[0] ? s : "(unprintable)");
+ hits++;
+ }
+ }
+ return hits;
+}
+
+static int do_scan(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ int total = 0;
+
+ if (argc >= 2) {
+ struct udevice *bus;
+ unsigned long val;
+ int bus_seq;
+
+ if (strict_strtoul(argv[1], 0, &val)) {
+ printf("pmbus: invalid bus seq '%s'\n", argv[1]);
+ return CMD_RET_USAGE;
+ }
+ bus_seq = (int)val;
+ if (uclass_get_device_by_seq(UCLASS_I2C, bus_seq, &bus)) {
+ printf("pmbus: i2c%d not available\n", bus_seq);
+ return CMD_RET_FAILURE;
+ }
+ printf("pmbus scan i2c%d:\n", bus_seq);
+ total = pmbus_scan_one_bus(bus, bus_seq);
+ } else {
+ struct uclass *uc;
+ struct udevice *bus;
+
+ if (uclass_get(UCLASS_I2C, &uc))
+ return CMD_RET_FAILURE;
+ uclass_foreach_dev(bus, uc) {
+ int seq = dev_seq(bus);
+
+ if (seq < 0)
+ continue;
+ printf("pmbus scan i2c%d:\n", seq);
+ total += pmbus_scan_one_bus(bus, seq);
+ }
+ }
+ if (!total)
+ printf("pmbus: no PMBus responders found\n");
+ return CMD_RET_SUCCESS;
+}
+
+static int do_help(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ unsigned int i, n = pmbus_vendor_count();
+
+ if (n == 0) {
+ printf("pmbus: no vendor extensions registered.\n");
+ printf(" Vendor handlers are registered by per chip drivers at\n");
+ printf(" probe time; trigger a probe via 'pmbus dev <name>' or a\n");
+ printf(" board hook (boot snapshot) and re run 'pmbus help'.\n");
+ return CMD_RET_SUCCESS;
+ }
+
+ printf("Registered pmbus vendor extensions (%u):\n\n", n);
+ for (i = 0; i < n; i++) {
+ const struct pmbus_vendor_op *op = pmbus_vendor_at(i);
+
+ if (!op)
+ continue;
+ printf("[vendor: %s]\n", op->vendor);
+ if (op->help)
+ printf("%s", op->help);
+ printf("\n");
+ }
+ return CMD_RET_SUCCESS;
+}
+
+static struct cmd_tbl pmbus_subcmd[] = {
+ U_BOOT_CMD_MKENT(dev, 2, 1, do_dev, "", ""),
+ U_BOOT_CMD_MKENT(list, 1, 1, do_list, "", ""),
+ U_BOOT_CMD_MKENT(info, 1, 1, do_info, "", ""),
+ U_BOOT_CMD_MKENT(telemetry, 1, 1, do_telemetry, "", ""),
+ U_BOOT_CMD_MKENT(status, 1, 1, do_status, "", ""),
+ U_BOOT_CMD_MKENT(dump, 1, 1, do_dump, "", ""),
+ U_BOOT_CMD_MKENT(read, 3, 1, do_read, "", ""),
+ U_BOOT_CMD_MKENT(write, 4, 1, do_write, "", ""),
+ U_BOOT_CMD_MKENT(clear, 2, 1, do_clear, "", ""),
+ U_BOOT_CMD_MKENT(vout, 2, 1, do_vout, "", ""),
+ U_BOOT_CMD_MKENT(scan, 2, 1, do_scan, "", ""),
+ U_BOOT_CMD_MKENT(help, 1, 1, do_help, "", ""),
+};
+
+static int do_pmbus(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ const struct pmbus_vendor_op *vop;
+ struct cmd_tbl *cmd;
+
+ if (argc < 2)
+ return CMD_RET_USAGE;
+
+ argc--;
+ argv++;
+
+ cmd = find_cmd_tbl(argv[0], pmbus_subcmd, ARRAY_SIZE(pmbus_subcmd));
+ if (cmd) {
+ if (argc > cmd->maxargs)
+ return CMD_RET_USAGE;
+ return cmd->cmd(cmdtp, flag, argc, argv);
+ }
+
+ /* Vendor namespace dispatch */
+ vop = pmbus_lookup_vendor(argv[0]);
+ if (vop)
+ return vop->handler(cmdtp, flag, argc, argv);
+
+ printf("pmbus: unknown subcommand '%s'\n", argv[0]);
+ return CMD_RET_USAGE;
+}
+
+U_BOOT_CMD(pmbus, CONFIG_SYS_MAXARGS, 1, do_pmbus,
+ "PMBus 1.x device interrogation and control",
+ "list - list UCLASS_REGULATOR devices (DM bound)\n"
+ "pmbus dev [<bus>:<addr>|<name>] - show / select active PMBus device\n"
+ "pmbus info - identification banner + driver_info\n"
+ "pmbus telemetry - decoded VIN, VOUT, IIN, IOUT, TEMP\n"
+ "pmbus status - decode every STATUS_* register\n"
+ "pmbus dump - hex dump of every standard register\n"
+ "pmbus read <reg> [b|w|s] - raw read (b=byte, w=word, s=string)\n"
+ "pmbus write <reg> <val> [b|w] - raw write\n"
+ "pmbus clear [faults] - issue CLEAR_FAULTS (03h)\n"
+ "pmbus vout [<uV>] - read / set VOUT_COMMAND (microvolts)\n"
+ "pmbus scan [<bus>] - PMBus aware probe of one or all I2C buses\n"
+ "pmbus help - list registered vendor extensions\n"
+ "\n"
+ "Vendor extensions (pmbus <vendor> ...) are registered by per chip\n"
+ "drivers at probe time. Run 'pmbus help' after a chip is probed to\n"
+ "see the available subcommands.\n"
+);
diff --git a/doc/develop/index.rst b/doc/develop/index.rst
index 3c044e67927..2ce67b7bfe2 100644
--- a/doc/develop/index.rst
+++ b/doc/develop/index.rst
@@ -50,6 +50,7 @@ Implementation
logging
makefiles
menus
+ pmbus
printf
smbios
spl
diff --git a/doc/develop/pmbus.rst b/doc/develop/pmbus.rst
new file mode 100644
index 00000000000..12e84c60078
--- /dev/null
+++ b/doc/develop/pmbus.rst
@@ -0,0 +1,633 @@
+.. SPDX-License-Identifier: GPL-2.0+
+
+PMBus support in U-Boot
+=======================
+
+This document describes U-Boot's PMBus 1.x support: what it is for,
+how it is structured, and how to add support for a new PMBus chipn
+either from scratch from a chip datasheet or by porting an existing
+Linux ``drivers/hwmon/pmbus/`` driver.
+
+.. contents::
+ :local:
+ :depth: 2
+
+Intent and scope
+----------------
+
+U-Boot's PMBus layer is not a hardware monitoring (hwmon) clone of
+the Linux kernel's ``drivers/hwmon/pmbus/`` subsystem. Linux owns the
+runtime side: continuous polling, sysfs publication, alert IRQ
+handling, fan control loops. U-Boot owns the boot time side. Concretely
+the U-Boot PMBus support exists to:
+
+* Identify the PMBus regulator(s) a board carries at boot:
+ ``MFR_ID``, ``MFR_MODEL``, ``MFR_REVISION`` reads, plus a quick
+ ``STATUS_WORD`` sanity check.
+* Print telemetry so an operator can confirm rail voltages, input
+ current, and temperature before handing off to the kernel. One shot
+ reads, on demand, via the ``pmbus`` and ``regulator`` U-Boot
+ commands (``pmbus dev <name>; pmbus telemetry``,
+ ``regulator dev <name>; regulator value``).
+* Decode chip alerts when a rail trips an over/under voltage,
+ over current, or thermal threshold; so a boot log shows why the
+ previous boot failed, before the kernel even comes up.
+* Optionally trim a critical rail (typically the SoC core) before
+ the kernel takes over; "set the voltage prior to a kernel boot
+ to better protect the board". This is the existing
+ ``board/nxp/common/vid.c`` AVS path and any future per board
+ speed binning trim.
+
+Out of scope, by design:
+
+* No periodic polling. No worker thread. No background updates.
+* No sysfs / procfs / userspace surface. U-Boot has none.
+* No fan speed control loop. The kernel runs that.
+* No long tail of virtual sensor registers (``PMBUS_VIRT_*``).
+* No sensor caching / update timestamps.
+
+If you find yourself wanting any of those, the answer is "wait until
+Linux comes up". Keep U-Boot's PMBus surface minimal.
+
+Architecture overview
+---------------------
+
+The framework is split into four layers (layer 3 comes in two
+flavours, 3a and 3b),
+
+::
+
+ +----------------------------------------+
+ | Layer 1: include/pmbus.h |
+ | Standard PMBus 1.x command codes, |
+ | numeric format enum, sensor class |
+ | enum, struct pmbus_driver_info, |
+ | decoder + transport prototypes, |
+ | STATUS_WORD bit names. |
+ +----------------------------------------+
+ | Layer 2: lib/pmbus.c |
+ | Format decoders (LINEAR11/LINEAR16/|
+ | DIRECT) and encoder (LINEAR16), |
+ | two stage SMBus block read helper, |
+ | STATUS_*-bit print tables, generic |
+ | dispatcher pmbus_reg2data(). |
+ +----------------------------------------+
+ | Layer 3a: drivers/power/regulator/ |
+ | <chip>.c |
+ | UCLASS_REGULATOR per chip drivers |
+ | ; one struct pmbus_driver_info |
+ | plus regulator set_value/get_value |
+ | ops. Optional: per chip identify() |
+ | hook to refine format from the |
+ | chip's own VOUT_MODE. |
+ +----------------------------------------+
+ | Layer 3b: drivers/power/regulator/ |
+ | pmbus_generic.c |
+ | Catch all driver matching |
+ | compatible = "pmbus". |
+ | Auto detects format via VOUT_MODE |
+ | and PMBUS_QUERY where supported. |
+ | Use for compliant chips with no |
+ | per chip driver yet; ship |
+ | telemetry today, write a per chip |
+ | driver later only if quirks demand |
+ | it. |
+ +----------------------------------------+
+ | Layer 4: board/<vendor>/<board>/ |
+ | <chip>_diag.c |
+ | Diagnostic commands only: |
+ | <chip>_info / <chip>_raw |
+ | Reads via regulator_get_value() |
+ | and lib/pmbus.c helpers. LINEAR / |
+ | DIRECT math NOT here. |
+ +----------------------------------------+
+
+Generic vs. board specific separation rule. Layer 1, 2, and 3
+files are tree level and platform agnostic. Their comments may
+reference only:
+
+* the PMBus 1.x specification, and
+* chip manufacturer datasheets.
+
+Never a specific board, SoC, or product. Board-specific quirks
+(a particular bus number, a particular slave address, a particular
+PCB feedback divider, board local design notes) live exclusively in
+``board/<vendor>/<board>/`` files.
+
+CLI commands
+------------
+
+The framework publishes one top level command, ``pmbus``, plus a
+vendor namespace dispatcher so per chip code can register chip
+specific extensions without touching the framework.
+
+Active device model
+~~~~~~~~~~~~~~~~~~~
+
+``pmbus`` mirrors the ``regulator`` command: select an active device
+once, then operate on it across subcommands. The active device is
+selected by I2C bus and 7 bit address::
+
+ => pmbus dev 0:0x10
+ pmbus: active i2c0:0x10 MFR_ID="MPS" MFR_MODEL="MPQ8785" vendor=mps
+
+The framework probes ``MFR_ID`` (in both natural and reverse byte
+orders) at selection time, looks the result up in the chip match
+registry populated by per chip code via ``pmbus_register_chip()``,
+and caches the matched ``pmbus_driver_info``. All subsequent
+subcommands consume that cached metadata.
+
+Standard subcommands
+~~~~~~~~~~~~~~~~~~~~
+
+::
+
+ pmbus list list UCLASS_REGULATOR devices (DM bound)
+ pmbus dev [<bus>:<addr>] show / select active PMBus device
+ pmbus info identification banner + driver_info
+ pmbus telemetry decoded VIN, VOUT, IIN, IOUT, TEMP
+ pmbus status decode every STATUS_* register
+ pmbus dump hex dump of every standard register
+ pmbus read <reg> [b|w|s] raw read (b=byte, w=word, s=string)
+ pmbus write <reg> <val> [b|w] raw write
+ pmbus clear [faults] issue CLEAR_FAULTS (03h)
+ pmbus vout [<uV>] read or set VOUT_COMMAND (microvolts)
+ pmbus scan [<bus>] PMBus aware probe of one or all I2C buses
+
+The ``<reg>`` argument accepts either a hexadecimal address
+(``0x88``) or a symbolic name (``READ_VIN``, ``VOUT_MODE``,
+``MFR_ID``). Format selectors after the register select the SMBus
+transaction width: ``b`` for byte, ``w`` for 16 bit little endian
+word, ``s`` for the SMBus block read used by string registers.
+
+Decoded telemetry honours the active device's ``pmbus_driver_info``;
+when no chip match has been registered, VOUT falls back to LINEAR16
+driven by ``VOUT_MODE`` and the other sensors fall back to LINEAR11.
+
+Vendor namespace
+~~~~~~~~~~~~~~~~
+
+Per chip drivers and board files publish chip specific subcommands
+in the ``pmbus <vendor> ...`` namespace by calling
+``pmbus_register_vendor_handler()`` at init time. The framework
+dispatches ``pmbus mps last``, ``pmbus mps clear last``, and
+``pmbus mps clear force`` to the MPS handler when the active
+device matches the ``mps`` vendor. Additional vendor handlers for
+``lltc``, ``renesas``, etc. land alongside the per chip drivers
+that need them.
+
+Relationship to ``vdd_override`` / ``vdd_read``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The NXP Layerscape ``vdd_override <mV>`` and ``vdd_read`` commands
+remain available in their original form for compatibility with
+existing AVS production scripts. The new ``pmbus vout`` and
+``pmbus vout <uV>`` subcommands cover the read and single shot
+write paths against the same chips, but do not implement
+``vdd_override``'s full sequence (board drop compensation, fuse
+target derivation, multi step convergence loop, atomic
+``PAGE_PLUS_WRITE`` block transaction, ``WRITE_PROTECT`` dance).
+For interactive bring up ``pmbus vout`` is sufficient; for
+production AVS, ``vdd_override`` stays canonical.
+
+Lifecycle: from board boot to Linux handoff
+-------------------------------------------
+
+The PMBus framework spans the entire U-Boot lifecycle. This section
+walks the boot timeline phase by phase, showing when each piece
+comes online and how the regulator uclass and the ``pmbus`` CLI
+converge on the same chip.
+
+Timeline overview
+~~~~~~~~~~~~~~~~~
+
+::
+
+ Phase 0 chip power on chip ramps to NVM default VOUT
+ Phase 1 boot ROM / SPL / TF-A PMBus typically untouched
+ Phase 2 U-Boot relocation, DM init regulators bound, not probed
+ Phase 3 first regulator probe chip driver runs, framework lights up
+ Phase 4 board hooks / boot scripts snapshot, AVS trim, gating
+ Phase 5 Linux handoff DT passed, chip state preserved
+ Phase 6 Linux runtime kernel pmbus driver takes over
+
+Phase 0: chip power on
+~~~~~~~~~~~~~~~~~~~~~~
+
+When the regulator chip receives its input voltage, it ramps its
+output to the VOUT default programmed into its NVM at factory
+provisioning. PMBus is silent: no software runs anywhere on the
+SoC yet.
+
+Phase 1: pre U-Boot stages
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Boot ROMs, secondary boot loaders (SPL, ARM TF-A BL2 / BL31)
+typically do not touch PMBus. They focus on PLLs, DDR PHY init,
+and bringing up enough hardware to load the next stage. Some
+platforms have a pre U-Boot AVS path in board specific TF-A
+code that writes ``VOUT_COMMAND`` from a fuse derived target;
+that path is independent of the U-Boot framework described here.
+
+Phase 2: U-Boot relocation and DM init
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+After relocation, U-Boot binds device tree nodes to drivers but
+does not probe them. UCLASS_REGULATOR devices for PMBus chips
+are bound (driver and DT match resolved) but the ``.probe``
+callback has not run yet.
+
+Framework state at this point:
+
+* chip match registry: empty
+* vendor handler registry: empty
+* active device: none
+* regulator uclass: devices bound, none probed
+
+Phase 3: lazy regulator probe
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The first caller into the regulator uclass for a given chip
+triggers the chip driver's ``.probe``. Typical first callers:
+
+* a board ``EVENT_SPY`` at ``EVT_LAST_STAGE_INIT`` (boot snapshot)
+* a U-Boot script: ``regulator dev <name>; regulator value``
+* the ``pmbus dev <name>`` CLI command (resolves to the regulator)
+* another DT consumer with a ``regulator-supplies`` reference
+
+The probe chain looks like this::
+
+ <chip>_probe(dev)
+ pmbus_regulator_probe_common(dev, &<chip>_info, page)
+ dev_read_addr(dev) -> reg = <addr>
+ i2c_get_chip(dev->parent, addr) -> I2C chip handle
+ priv->i2c_dev = handle
+ priv->info = &<chip>_info
+ priv->page = page
+ (page > 0) write PMBUS_PAGE
+ <chip>_identify_vout(priv->i2c_dev) [optional]
+ read VOUT_MODE; refine info->format[PSC_VOLTAGE_OUT]
+ pmbus_regulator_apply_voltage_scale(dev, fb_div) [optional]
+ write PMBUS_VOUT_SCALE_LOOP if DT property set
+ pmbus_register_chip(&<chip>_match) [idempotent]
+ pmbus_register_vendor_handler(&<chip>_op) [idempotent]
+
+Once probed, three independent surfaces are functional against
+the same chip:
+
+* the regulator uclass API (``regulator_get_value``,
+ ``regulator_set_value``, ``regulator_get_enable``,
+ ``regulator_set_enable``)
+* the ``pmbus`` CLI (chip is reachable by name through
+ ``pmbus_resolve_by_name()``, by raw ``<bus>:<addr>`` through
+ ``pmbus_set_active()``)
+* the chip's vendor extension subcommands (``pmbus <vendor> ...``)
+
+Phase 4: board hooks and boot scripts
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Boards hook the boot flow at well known points to drive board
+specific PMBus behaviour. The framework prescribes none of these;
+they are conventions:
+
+* boot time rail snapshot. An ``EVENT_SPY`` at
+ ``EVT_LAST_STAGE_INIT`` reads telemetry through the regulator
+ uclass and prints a one shot summary to the console. Useful
+ for operator visibility on serial during bring up.
+
+* pre kernel rail trim (AVS). A board hook in
+ ``board_late_init`` or a custom event spy reads a fuse derived
+ target voltage and calls ``regulator_set_value_force()`` to
+ trim the SoC core rail before kernel handoff.
+
+* Linux handoff gate. A bootcmd reads the rail voltage
+ through the regulator command and refuses to boot Linux if
+ the rail is outside the expected range.
+
+Phase 5: Linux handoff
+~~~~~~~~~~~~~~~~~~~~~~
+
+When U-Boot transfers control to Linux, it passes the device
+tree (potentially patched). The DT compatible strings for PMBus
+regulators must match those in the upstream kernel binding so
+the kernel's ``drivers/hwmon/pmbus/<chip>.c`` picks them up.
+Property names are shared with the kernel binding
+(``regulator-name``, ``regulator-min-microvolt``,
+``mps,vout-fb-divider-ratio-permille``, etc.); see "DT alignment
+with Linux" below.
+
+The chip itself is left in the state U-Boot wrote it to. If
+U-Boot trimmed VOUT, the chip stays at the trimmed voltage
+through handoff. ``CLEAR_FAULTS`` state is preserved unless an
+operator explicitly issued one.
+
+Phase 6: Linux runtime
+~~~~~~~~~~~~~~~~~~~~~~
+
+Linux's ``drivers/hwmon/pmbus/pmbus_core.c`` probes the chip,
+exposes telemetry under ``/sys/class/hwmon``, and takes over
+runtime voltage management through its regulator subsystem.
+The hwmon framework polls periodically; U-Boot does not.
+
+Operation paths through the regulator uclass
+--------------------------------------------
+
+After the first probe completes, calls into the regulator uclass
+for a PMBus chip flow through the shared helper.
+
+Read VOUT::
+
+ regulator_get_value(dev)
+ -> dm_regulator_ops->get_value
+ -> pmbus_regulator_get_value(dev)
+ pmbus_regulator_select_page(priv)
+ pmbus_read_byte(priv->i2c_dev, VOUT_MODE, &mode)
+ pmbus_read_word(priv->i2c_dev, READ_VOUT, &raw)
+ pmbus_reg2data(priv->info, PSC_VOLTAGE_OUT, raw, mode)
+ -> reg2data_linear16 (mode = 0)
+ -> reg2data_direct (chip configured for DIRECT)
+ return engineering value (microvolts)
+
+Write VOUT::
+
+ regulator_set_value(dev, uV)
+ -> dm_regulator_ops->set_value
+ -> pmbus_regulator_set_value(dev, uV)
+ pmbus_regulator_select_page(priv)
+ pmbus_read_byte(VOUT_MODE)
+ check (mode == LINEAR) [LINEAR16 only today]
+ raw = pmbus_data2reg_linear16(uV, mode)
+ dm_i2c_write(VOUT_COMMAND, raw)
+
+Read / write enable bit::
+
+ regulator_get_enable(dev)
+ -> pmbus_regulator_get_enable(dev)
+ pmbus_read_byte(OPERATION) & PB_OPERATION_ON
+
+ regulator_set_enable(dev, on)
+ -> pmbus_regulator_set_enable(dev, on)
+ read OPERATION, set or clear PB_OPERATION_ON, write back
+
+Bus traffic per call:
+
+* ``get_value`` : 1 byte read (VOUT_MODE) + 1 word read (READ_VOUT)
+ + 1 byte write (PAGE) when ``page > 0``
+* ``set_value`` : 1 byte read (VOUT_MODE) + 1 word write (VOUT_COMMAND)
+ + 1 byte write (PAGE) when ``page > 0``
+* ``get_enable`` : 1 byte read (OPERATION)
+* ``set_enable`` : 1 byte read (OPERATION) + 1 byte write (OPERATION)
+
+Common board hook patterns
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Boot time rail snapshot::
+
+ static int my_board_pmbus_snapshot(void)
+ {
+ struct udevice *reg;
+
+ if (regulator_get_by_platname("MY_RAIL", ®))
+ return 0;
+ printf("MY_RAIL: VOUT = %d uV, enabled = %d\n",
+ regulator_get_value(reg),
+ regulator_get_enable(reg));
+ return 0;
+ }
+ EVENT_SPY_SIMPLE(EVT_LAST_STAGE_INIT, my_board_pmbus_snapshot);
+
+The first call to ``regulator_get_value()`` triggers the chip
+driver's ``.probe``, which seeds the chip match and vendor
+extension registries. Subsequent ``pmbus`` CLI commands work
+without further setup.
+
+Pre kernel rail trim (AVS)::
+
+ int board_late_init(void)
+ {
+ struct udevice *reg;
+ int target_uV = compute_avs_target();
+
+ if (regulator_get_by_platname("VDD_CORE", ®))
+ return 0;
+ return regulator_set_value_force(reg, target_uV);
+ }
+
+Use ``regulator_set_value_force()`` when the target may sit
+outside the DT declared ``regulator-min-microvolt`` /
+``regulator-max-microvolt`` range; force bypasses the bounds
+check.
+
+Adding a new PMBus chip from scratch
+------------------------------------
+
+Use this path when the chip has no Linux driver yet, or when you want
+to validate the U-Boot port against the datasheet alone.
+
+1. Confirm PMBus 1.x compliance level. Locate in the chip
+ datasheet:
+
+ which PMBus standard command codes the chip implements
+ (``READ_VIN``, ``READ_VOUT``, ``STATUS_WORD``, ``MFR_ID`` ...),
+ which numeric format(s) it uses for VOUT (LINEAR16 with the
+ exponent in ``VOUT_MODE``, DIRECT with chip specific m/b/R, or
+ VID with one of the documented VRM tables),
+ which numeric format it uses for VIN, IIN, IOUT, TEMPERATURE
+ (most commonly LINEAR11; some MPS / MPS derivative chips use
+ DIRECT instead),
+ how many output rails it exposes (single page parts vs.
+ multi rail PMBus pages).
+
+2. Declare a ``struct pmbus_driver_info``. Wire each sensor
+ class to one ``enum pmbus_data_format``, plus the m/b/R triple if
+ the format is DIRECT::
+
+ static struct pmbus_driver_info chipname_info = {
+ .pages = 1,
+ .format[PSC_VOLTAGE_IN] = pmbus_fmt_direct,
+ .format[PSC_VOLTAGE_OUT] = pmbus_fmt_linear,
+ .format[PSC_CURRENT_OUT] = pmbus_fmt_direct,
+ .format[PSC_TEMPERATURE] = pmbus_fmt_direct,
+ .m[PSC_VOLTAGE_IN] = 4, .R[PSC_VOLTAGE_IN] = 1,
+ .m[PSC_CURRENT_OUT] = 16, .R[PSC_CURRENT_OUT] = 0,
+ .m[PSC_TEMPERATURE] = 1, .R[PSC_TEMPERATURE] = 0,
+ };
+
+3. Bind to a DT compatible. Use the lowercase ``vendor,chip``
+ tuple Linux uses (see "DT alignment with Linux" below). Add the
+ driver under ``drivers/power/regulator/`` matching the existing
+ skeleton (``fan53555.c``, ``pca9450.c``).
+
+4. Rely on the DT binding from the Linux kernel which is imported into
+ U-Boot under ``dts/upstream/Bindings/`` (for PMBus chips,
+ ``dts/upstream/Bindings/hwmon/pmbus/``).
+
+5. Smoke test. With the chip wired up in DT::
+
+ => regulator dev <name>
+ => regulator value
+ => regulator info
+
+ Numbers should match the bench measurement to within the chip's
+ advertised LSB.
+
+Porting an existing Linux PMBus driver to U-Boot
+------------------------------------------------
+
+When the chip already has a ``linux/drivers/hwmon/pmbus/<chip>.c``,
+that driver is the authoritative reference for format, coefficients,
+and quirks. Take what carries; leave what does not.
+
+What carries verbatim
+~~~~~~~~~~~~~~~~~~~~~
+
+* Numeric formats (``format[PSC_*]``).
+* DIRECT coefficients (``m[]``, ``b[]``, ``R[]``).
+* Per page count and per page functionality bits (``pages``,
+ ``func[]``).
+* VOUT_MODE driven per chip identify hook (e.g. MPQ8785's
+ switch between LINEAR16 and VID coerced DIRECT m=64 R=1).
+* Vendor register addresses for chip specific quirks (fault
+ history, scale-loop, page mapping).
+
+What does not carry
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* ``hwmon_device_register()`` and the attribute groups it consumes.
+* ``struct pmbus_data`` / ``update_lock`` / ``last_updated``
+ U-Boot has no caching layer.
+* ALERT# IRQ wiring; U-Boot is single threaded boot code.
+* Fan control hooks (``read_fan_*``, ``set_pwm_*``).
+* Virtual register handling (``PMBUS_VIRT_READ_VIN_*`` etc.); those
+ are entirely a hwmon publication aid.
+* ``module_i2c_driver(...)`` and ``MODULE_*`` macros; U-Boot uses
+ ``U_BOOT_DRIVER(...)``.
+
+Worked example: porting MPQ8785
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Linux's ``drivers/hwmon/pmbus/mpq8785.c`` is 193 LOC; the U-Boot
+equivalent is ~150 LOC.
+
+The ``mpq8785_info`` struct transcribes verbatim::
+
+ .pages = 1,
+ .format[PSC_VOLTAGE_IN] = direct, .m[PSC_VOLTAGE_IN] = 4, .R[PSC_VOLTAGE_IN] = 1,
+ .format[PSC_CURRENT_OUT] = direct, .m[PSC_CURRENT_OUT] = 16, .R[PSC_CURRENT_OUT] = 0,
+ .format[PSC_TEMPERATURE] = direct, .m[PSC_TEMPERATURE] = 1, .R[PSC_TEMPERATURE] = 0,
+
+The VOUT format is decided at probe time from VOUT_MODE bits[7:5] :
+mode 0 means LINEAR16, mode 1 or 2 means DIRECT m=64 R=1 (the chip's
+"VID" mode is coerced to DIRECT by the driver). Translate Linux's
+``mpq8785_identify()`` 1:1.
+
+The per chip quirks that carry over:
+
+* MPS NVM string byte order: chip stores ``S P M`` for the human
+ string ``MPS``. ``pmbus_read_string()`` accepts a ``reverse_bytes``
+ flag for this case.
+* ``mps,vout-fb-divider-ratio-permille`` DT property maps to
+ ``VOUT_SCALE_LOOP`` write at probe time.
+
+The quirks that do not carry over:
+
+* The ``PMBUS_VIRT_*`` virtual sensor wiring. Drop entirely.
+* The ``hwmon_chip_info`` attribute group registration.
+* The ``MODULE_AUTHOR`` / ``MODULE_LICENSE`` declarations.
+
+Using the generic ``compatible = "pmbus"`` driver
+-------------------------------------------------
+
+When a board carries a PMBus chip without a per chip U-Boot driver,
+the catch all ``drivers/power/regulator/pmbus_generic.c`` (Layer 3b)
+binds against ``compatible = "pmbus"``. It auto detects format via
+``VOUT_MODE`` and ``PMBUS_QUERY`` (where the chip supports it) and
+provides telemetry + voltage set/get against the standard PMBus 1.x
+subset.
+
+Decision tree:
+
+1. Try the generic driver first. Add the regulator node to the
+ board DT with ``compatible = "pmbus"`` plus the standard
+ regulator properties. Boot, run ``regulator value``, compare
+ against bench measurement.
+2. Switch to a per chip driver only when the generic one is
+ wrong: telemetry shows wrong values (chip uses DIRECT with
+ non default coefficients), an alert can't be decoded (chip has
+ vendor specific status bits), AVS is needed (the boot path has
+ to actively trim VOUT before kernel handoff), or the chip has an
+ ADDR-pin auto promotion / VID coercion / vendor register quirk.
+
+DT alignment with Linux
+-----------------------
+
+The same ``.dts`` file should work under both U-Boot (BL33) and Linux
+post handoff. To make that possible:
+
+* Reuse the upstream Linux compatible for every PMBus chip. Look
+ in ``linux/Documentation/devicetree/bindings/hwmon/pmbus/`` and
+ ``linux/Documentation/devicetree/bindings/regulator/``. The
+ ``<vendor>,<chip>`` tuple from the kernel binding goes into U-Boot's
+ ``of_match_table`` unchanged.
+* Reuse Linux property names verbatim: ``regulator-name``,
+ ``regulator-min-microvolt``, ``regulator-max-microvolt``,
+ ``regulator-boot-on``, ``regulator-always-on``,
+ ``mps,vout-fb-divider-ratio-permille``, etc.
+* The DT binding is the kernel's, imported under
+ ``dts/upstream/Bindings/`` (PMBus chips live in
+ ``dts/upstream/Bindings/hwmon/pmbus/``).
+
+Multi rail/multi page chips (e.g. ISL68137 with seven outputs)
+declare each rail as a child regulator node with ``reg = <page>``;
+each child binds as a UCLASS_REGULATOR with that PMBus PAGE setting
+applied at every read/write.
+
+Common pitfalls
+---------------
+
+These have all bitten contributors during nbxv3 bring up; record them
+here so the next port doesn't repeat them.
+
+* VOUT_MODE/DIRECT format confusion. Most generic PMBus call
+ sites assume LINEAR16. Several MPS chips report VOUT in DIRECT
+ format with chip specific m/b/R after a single VOUT_MODE read,
+ the same chip read at the same address produces different
+ numbers depending on the format the driver applies. Always read
+ ``VOUT_MODE`` at probe time and switch the decoder accordingly.
+ Linux's per chip ``identify()`` callbacks document the exact
+ rules; copy them rather than guessing.
+* SMBus block read protocol. Some I2C controllers strict check
+ block read transactions: the master must read the length byte
+ first, then reissue the read for the payload. Over reading a
+ fixed length and ignoring the length byte works on lenient
+ controllers but errors on strict ones. ``pmbus_read_string()``
+ does the two stage read; use it.
+* I2C bus number stability. ``uclass_get_device_by_seq()``
+ uses the DT alias index (``i2c0`` -> ``UCLASS_I2C`` seq 0) when
+ aliases are declared, otherwise falls back to probe order which
+ varies with which controllers are enabled in the defconfig.
+ Always declare DT aliases for I2C buses you reference by index.
+* ADDR-pin auto addressessing. Some chips (notably MPS parts) decode
+ their PMBus 7-bit address from an external resistor divider on
+ ADDR_VBOOT. The "default" address in the datasheet is the
+ factory fused slot; a board with a different divider or a die
+ with a different revision can land in another window. If the
+ driver hardcodes the default and the board side scan finds the
+ chip in another window, auto promote the working address rather
+ than failing the probe.
+* MFR string byte order. Most PMBus chips return ``MFR_ID``
+ characters in human order. Some MPS personalities reverse them.
+ Pass ``reverse_bytes=true`` to ``pmbus_read_string()`` for those;
+ spec compliant chips pass false.
+
+References
+----------
+
+* Linux PMBus core: ``linux/drivers/hwmon/pmbus/pmbus_core.c``,
+ decoder reference; ignore the hwmon publication and caching layers.
+* Linux PMBus header: ``linux/drivers/hwmon/pmbus/pmbus.h``; API
+ surface reference; many constants and the ``struct
+ pmbus_driver_info`` shape are mirrored verbatim into U-Boot's
+ ``include/pmbus.h``.
+* Linux DT bindings:
+ ``linux/Documentation/devicetree/bindings/hwmon/pmbus/``.
diff --git a/include/pmbus.h b/include/pmbus.h
new file mode 100644
index 00000000000..969108e5b53
--- /dev/null
+++ b/include/pmbus.h
@@ -0,0 +1,629 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright 2026 Free Mobile, Vincent Jardin
+ *
+ * PMBus 1.x command codes, numeric format decoders, and driver_info
+ * scaffolding for U-Boot.
+ *
+ * Intents
+ *
+ * U-Boot's PMBus support is not a hwmon clone. It shall be used to:
+ * 1. identify PMBus regulators a board carries at boot,
+ * 2. print telemetry so an operator can confirm rail voltages and
+ * fault status before handing off to the kernel,
+ * 3. decode chip alerts when a rail trips an over/under voltage,
+ * over current, or thermal threshold,
+ * 4. optionally trim a critical rail before kernel boot, to better
+ * protect the board.
+ *
+ * No periodic polling, no /sys, no userspace surface, no fan control
+ * loops. Linux owns those. See doc/develop/pmbus.rst for the full
+ * porting guide and policy notes.
+ *
+ * Linux relationship
+ *
+ * Constants (command codes, status bit names, sensor class enum,
+ * format enum) are mirrored verbatim from
+ * linux/drivers/hwmon/pmbus/pmbus.h
+ * with attribution; this is standardised data and copying it avoids
+ * accidental drift. Decoders (LINEAR11/16, DIRECT, VID, IEEE754) are
+ * reimplemented from the PMBus 1.x spec rather than copied. The
+ * surrounding kernel context (struct pmbus_data, hwmon caching,
+ * sysfs publication) does not apply to U-Boot.
+ *
+ * Tree level files (this header, lib/pmbus.c, future per chip drivers
+ * under drivers/power/regulator/) must stay platform agnostic. They
+ * may reference only the PMBus 1.x specification and chip datasheets,
+ * never a specific board, SoC, or product. Board specific quirks
+ * live under board/<vendor>/<board>/.
+ */
+
+#ifndef _PMBUS_H_
+#define _PMBUS_H_
+
+#include <linux/types.h>
+
+struct udevice;
+struct pmbus_driver_info;
+struct pmbus_status_override;
+
+/*
+ * PMBus 1.3 standard command codes (Part II).
+ *
+ * Subset relevant to U-Boot's needs:
+ * - configuration (PAGE, OPERATION, VOUT_*),
+ * - telemetry (READ_VIN, READ_IOUT, READ_TEMPERATURE_1, ...),
+ * - status (STATUS_WORD, STATUS_VOUT, ...),
+ * - identification (MFR_ID, MFR_MODEL, MFR_REVISION).
+ * Chip specific extensions (for example MPS PROTECTION_LAST 0xFB) shall be in
+ * the per chip driver file.
+ */
+#define PMBUS_PAGE 0x00
+#define PMBUS_OPERATION 0x01
+#define PMBUS_ON_OFF_CONFIG 0x02
+#define PMBUS_CLEAR_FAULTS 0x03
+#define PMBUS_PHASE 0x04
+
+#define PMBUS_WRITE_PROTECT 0x10
+
+#define PMBUS_CAPABILITY 0x19
+#define PMBUS_QUERY 0x1a
+#define PMBUS_SMBALERT_MASK 0x1b
+#define PMBUS_VOUT_MODE 0x20
+#define PMBUS_VOUT_COMMAND 0x21
+#define PMBUS_VOUT_TRIM 0x22
+#define PMBUS_VOUT_CAL_OFFSET 0x23
+#define PMBUS_VOUT_MAX 0x24
+#define PMBUS_VOUT_MARGIN_HIGH 0x25
+#define PMBUS_VOUT_MARGIN_LOW 0x26
+#define PMBUS_VOUT_TRANSITION_RATE 0x27
+#define PMBUS_VOUT_DROOP 0x28
+#define PMBUS_VOUT_SCALE_LOOP 0x29
+#define PMBUS_VOUT_SCALE_MONITOR 0x2a
+
+#define PMBUS_COEFFICIENTS 0x30
+#define PMBUS_POUT_MAX 0x31
+
+#define PMBUS_STATUS_BYTE 0x78
+#define PMBUS_STATUS_WORD 0x79
+#define PMBUS_STATUS_VOUT 0x7a
+#define PMBUS_STATUS_IOUT 0x7b
+#define PMBUS_STATUS_INPUT 0x7c
+#define PMBUS_STATUS_TEMPERATURE 0x7d
+#define PMBUS_STATUS_CML 0x7e
+#define PMBUS_STATUS_OTHER 0x7f
+#define PMBUS_STATUS_MFR_SPECIFIC 0x80
+
+#define PMBUS_READ_VIN 0x88
+#define PMBUS_READ_IIN 0x89
+#define PMBUS_READ_VCAP 0x8a
+#define PMBUS_READ_VOUT 0x8b
+#define PMBUS_READ_IOUT 0x8c
+#define PMBUS_READ_TEMPERATURE_1 0x8d
+#define PMBUS_READ_TEMPERATURE_2 0x8e
+#define PMBUS_READ_TEMPERATURE_3 0x8f
+#define PMBUS_READ_DUTY_CYCLE 0x94
+#define PMBUS_READ_FREQUENCY 0x95
+#define PMBUS_READ_POUT 0x96
+#define PMBUS_READ_PIN 0x97
+
+#define PMBUS_REVISION 0x98
+#define PMBUS_MFR_ID 0x99
+#define PMBUS_MFR_MODEL 0x9a
+#define PMBUS_MFR_REVISION 0x9b
+#define PMBUS_MFR_LOCATION 0x9c
+#define PMBUS_MFR_DATE 0x9d
+#define PMBUS_MFR_SERIAL 0x9e
+
+#define PMBUS_IC_DEVICE_ID 0xad
+#define PMBUS_IC_DEVICE_REV 0xae
+
+/* VOUT_MODE upper bits: numeric format selector (Part II sec 8.3). */
+#define PB_VOUT_MODE_MODE_MASK 0xe0
+#define PB_VOUT_MODE_PARAM_MASK 0x1f
+#define PB_VOUT_MODE_LINEAR 0x00
+#define PB_VOUT_MODE_VID 0x20
+#define PB_VOUT_MODE_DIRECT 0x40
+#define PB_VOUT_MODE_IEEE754 0x60
+
+/* STATUS_WORD lower byte (= STATUS_BYTE), Part II sec 10.1.1. */
+#define PB_STATUS_NONE_ABOVE BIT(0)
+#define PB_STATUS_CML BIT(1)
+#define PB_STATUS_TEMPERATURE BIT(2)
+#define PB_STATUS_VIN_UV BIT(3)
+#define PB_STATUS_IOUT_OC BIT(4)
+#define PB_STATUS_VOUT_OV BIT(5)
+#define PB_STATUS_OFF BIT(6)
+#define PB_STATUS_BUSY BIT(7)
+
+/* STATUS_WORD upper byte. */
+#define PB_STATUS_UNKNOWN BIT(8)
+#define PB_STATUS_OTHER BIT(9)
+#define PB_STATUS_FANS BIT(10)
+#define PB_STATUS_POWER_GOOD_N BIT(11)
+#define PB_STATUS_WORD_MFR BIT(12)
+#define PB_STATUS_INPUT BIT(13)
+#define PB_STATUS_IOUT_POUT BIT(14)
+#define PB_STATUS_VOUT BIT(15)
+
+/* STATUS_VOUT (PMBus 1.3.1 Part II sec 17.3, Table 17). */
+#define PB_VOLTAGE_VOUT_MAX_MIN_WARN BIT(3)
+#define PB_VOLTAGE_UV_FAULT BIT(4)
+#define PB_VOLTAGE_UV_WARNING BIT(5)
+#define PB_VOLTAGE_OV_WARNING BIT(6)
+#define PB_VOLTAGE_OV_FAULT BIT(7)
+
+/* STATUS_IOUT (Part II sec 10.6). */
+#define PB_POUT_OP_WARNING BIT(0)
+#define PB_POUT_OP_FAULT BIT(1)
+#define PB_POWER_LIMITING BIT(2)
+#define PB_CURRENT_SHARE_FAULT BIT(3)
+#define PB_IOUT_UC_FAULT BIT(4)
+#define PB_IOUT_OC_WARNING BIT(5)
+#define PB_IOUT_OC_LV_FAULT BIT(6)
+#define PB_IOUT_OC_FAULT BIT(7)
+
+/* STATUS_INPUT (Part II sec 10.7). */
+#define PB_PIN_OP_WARNING BIT(0)
+#define PB_IIN_OC_WARNING BIT(1)
+#define PB_IIN_OC_FAULT BIT(2)
+
+/* STATUS_TEMPERATURE (Part II sec 10.8). */
+#define PB_TEMP_UT_FAULT BIT(4)
+#define PB_TEMP_UT_WARNING BIT(5)
+#define PB_TEMP_OT_WARNING BIT(6)
+#define PB_TEMP_OT_FAULT BIT(7)
+
+/* STATUS_CML (Part II sec 10.9). */
+#define PB_CML_FAULT_OTHER_MEM_LOGIC BIT(0)
+#define PB_CML_FAULT_OTHER_COMM BIT(1)
+#define PB_CML_FAULT_PROCESSOR BIT(3)
+#define PB_CML_FAULT_MEMORY BIT(4)
+#define PB_CML_FAULT_PACKET_ERROR BIT(5)
+#define PB_CML_FAULT_INVALID_DATA BIT(6)
+#define PB_CML_FAULT_INVALID_COMMAND BIT(7)
+
+/*
+ * OPERATION (01h) command bits per PMBus 1.3 Part II sec 9.1. Bit[7]
+ * is the master rail enable; the lower bits select margin high/low
+ * and turn off behaviour (subset surfaced for the regulator helper).
+ */
+#define PB_OPERATION_ON BIT(7)
+
+/*
+ * LINEAR11 numeric format (PMBus 1.3 Part II sec 7): 16 bit register
+ * with a signed 11 bit mantissa in bits[10:0] and a signed 5 bit
+ * exponent in bits[15:11]. Engineering value = mantissa * 2^exponent.
+ */
+#define PB_LINEAR11_MANT_MASK 0x07ff
+#define PB_LINEAR11_MANT_BITS 11
+#define PB_LINEAR11_EXP_SHIFT 11
+#define PB_LINEAR11_EXP_MASK 0x1f
+#define PB_LINEAR11_EXP_BITS 5
+
+/*
+ * Cache buffer sizes for the active device singleton + MFR_* block
+ * reads. PMBus block reads return up to 32 bytes per the SMBus spec;
+ * 16 covers every MFR string seen in practice on regulator class
+ * chips and keeps the singleton compact.
+ */
+#define PMBUS_MFR_STRING_MAX 16
+#define PMBUS_VENDOR_NAME_MAX 8
+#define PMBUS_REGULATOR_NAME_MAX 24
+
+/* PMBus revision identifiers reported by PMBUS_REVISION (98h). */
+#define PMBUS_REV_10 0x00 /* PMBus 1.0 */
+#define PMBUS_REV_11 0x11 /* PMBus 1.1 */
+#define PMBUS_REV_12 0x22 /* PMBus 1.2 */
+#define PMBUS_REV_13 0x33 /* PMBus 1.3 */
+
+/*
+ * Numeric formats and sensor classes.
+ *
+ * Mirrors linux/drivers/hwmon/pmbus/pmbus.h enum pmbus_data_format
+ * and enum pmbus_sensor_classes. A chip's pmbus_driver_info wires
+ * each sensor class to one format and (for DIRECT) to its m/b/R
+ * coefficients.
+ */
+enum pmbus_data_format {
+ pmbus_fmt_linear = 0,
+ pmbus_fmt_ieee754,
+ pmbus_fmt_direct,
+ pmbus_fmt_vid,
+};
+
+enum pmbus_sensor_classes {
+ PSC_VOLTAGE_IN = 0,
+ PSC_VOLTAGE_OUT,
+ PSC_CURRENT_IN,
+ PSC_CURRENT_OUT,
+ PSC_POWER,
+ PSC_TEMPERATURE,
+ PSC_NUM_CLASSES
+};
+
+/*
+ * Per chip identification record. Each per chip driver declares one
+ * of these and points the framework at it. Subset of the kernel
+ * struct pmbus_driver_info: U-Boot has no per page caches, no fan
+ * accessors, no virtual registers, no async sysfs publication.
+ *
+ * pages number of PAGE distinct rails the chip exposes
+ * (1 for single rail parts).
+ * format[] numeric format per sensor class.
+ * m/b/R[] DIRECT format coefficients per sensor class. See
+ * pmbus_reg2data_direct() below for the formula.
+ * Unused for non DIRECT classes.
+ * read_byte_data, read_word_data
+ * optional per chip register translators. Return the
+ * standard register value on success, ENODATA to fall
+ * through to the generic transport, any other negative
+ * errno on bus error.
+ * identify optional probe time hook to discover format and
+ * page count from the chip itself (for example, the
+ * MPQ8785 VOUT_MODE switch between LINEAR and DIRECT).
+ */
+struct pmbus_driver_info {
+ int pages;
+ enum pmbus_data_format format[PSC_NUM_CLASSES];
+ int m[PSC_NUM_CLASSES];
+ int b[PSC_NUM_CLASSES];
+ int R[PSC_NUM_CLASSES];
+
+ int (*read_byte_data)(struct udevice *dev, int page, int reg);
+ int (*read_word_data)(struct udevice *dev, int page, int reg);
+ int (*identify)(struct udevice *dev, struct pmbus_driver_info *info);
+
+ /*
+ * Optional sparse table of chip specific STATUS_* bit name
+ * substitutions. Terminated by an entry with .name = NULL
+ * (matching the convention used by struct udevice_id and
+ * other U-Boot driver tables). NULL pointer means the chip
+ * uses only PMBus 1.x standard names. See struct
+ * pmbus_status_override and pmbus_print_status_bits().
+ */
+ const struct pmbus_status_override *status_overrides;
+
+ /*
+ * Bitmask (BIT(enum pmbus_sensor_classes)) of the sensor classes
+ * this chip actually implements. When non-zero, pmbus_print_telemetry
+ * prints exactly these classes -- mirroring the kernel's per chip
+ * sensor set -- and skips the rest. This is how an MPS buck that
+ * ACKs READ_POUT / READ_IIN with an uncalibrated value still hides
+ * POWER / CURRENT_IN (the kernel's mpq8646 driver exposes neither).
+ * Zero means "not declared": the telemetry printer falls back to a
+ * live pmbus_word_command_supported() probe per class, which is what
+ * the generic driver (compatible = "pmbus") relies on.
+ */
+ u8 classes_present;
+};
+
+/*
+ * Decoder helpers (raw register, returns engineering value in micro
+ * units).
+ *
+ * All return signed micro units (uV, uA, udegC), 64 bit to avoid
+ * overflow on large mantissa times exponent products. The caller
+ * divides by 1000 for milli units, or by 1_000_000 for the integer
+ * engineering value.
+ */
+
+/*
+ * LINEAR11. Bits[15:11] = signed 5 bit exponent Y, bits[10:0] =
+ * signed 11 bit mantissa N. Engineering value = N * 2^Y.
+ *
+ * Used by most PMBus chips for VIN, IIN, IOUT, TEMP. Some MPS
+ * parts deviate (they report DIRECT format with chip specific m/b/R
+ * coefficients); check the chip datasheet against PMBUS_VOUT_MODE
+ * and the Linux per chip driver if porting.
+ */
+s64 pmbus_reg2data_linear11(u16 raw);
+
+/*
+ * LINEAR16. 16 bit unsigned mantissa multiplied by 2^Y, where Y is
+ * the signed 5 bit exponent supplied via VOUT_MODE bits[4:0]. The
+ * caller must read VOUT_MODE (cmd 0x20) and pass it in vout_mode.
+ * Only the mode_mask bits[7:5] = 0 selector is the LINEAR16 path.
+ *
+ * Used for READ_VOUT (8Bh) on chips whose VOUT_MODE selects Linear.
+ * Returns 0 if VOUT_MODE indicates a non Linear mode; the caller
+ * is then expected to dispatch to pmbus_reg2data_direct() with the
+ * appropriate per chip m/b/R, or to pmbus_reg2data_vid() / _ieee754()
+ * if the chip uses those formats.
+ */
+s64 pmbus_reg2data_linear16(u16 raw, u8 vout_mode);
+
+/*
+ * DIRECT. PMBus 1.3 Part II sec 8.4. The chip stores a signed 16 bit
+ * value X; the engineering value Y is one over m, multiplied by the
+ * quantity (X scaled by ten to the power minus R, then offset by
+ * minus b), with chip specific (m, b, R) coefficients.
+ *
+ * In symbolic form: Y = (1/m) * (X * 10**(-R) - b). The negative
+ * exponent and trailing subtraction are math operators in the
+ * formula, not punctuation in the prose.
+ *
+ * Returns micro units. Implementation order matches the Linux
+ * reference (multiply before subtract, scale R then divide by m) to
+ * minimise quantisation drift. m == 0 returns 0.
+ */
+s64 pmbus_reg2data_direct(s16 raw, int m, int b, int R);
+
+/*
+ * Encoder: engineering value (micro units) to LINEAR16 raw register
+ * value. Used by pre kernel rail trim code (see board/nxp/common/vid.c)
+ * to write VOUT_COMMAND from a target voltage. The exponent is
+ * recovered from VOUT_MODE.
+ *
+ * Returns 0 if VOUT_MODE indicates a non Linear format; the caller
+ * must then dispatch to pmbus_data2reg_direct() (DIRECT format) or
+ * the VID / IEEE754 encoders (when those land) per the chip's actual
+ * VOUT_MODE selector.
+ */
+u16 pmbus_data2reg_linear16(s64 micro, u8 vout_mode);
+
+/*
+ * Encoder: engineering value (micro units) to DIRECT raw register
+ * value. Inverse of pmbus_reg2data_direct():
+ *
+ * X = (m * Y + b) * 10^R
+ *
+ * with chip specific (m, b, R) coefficients (typically taken from
+ * the chip's pmbus_driver_info[PSC_VOLTAGE_OUT]). m == 0 returns 0.
+ *
+ * The result is saturated to the s16 range mandated by the PMBus
+ * 1.3 Part II sec 8.4 DIRECT encoding; out of range targets return
+ * 0x7fff or 0x8000 rather than wrapping.
+ */
+u16 pmbus_data2reg_direct(s64 micro, int m, int b, int R);
+
+/*
+ * Dispatcher: pick the right reg2data_* helper based on the chip's
+ * pmbus_driver_info[class]. vout_mode is consulted only for
+ * VOLTAGE_OUT in linear format. For DIRECT, m/b/R are taken from
+ * info. For VID and IEEE754 the dispatcher returns 0 (formats
+ * not yet wired up; add when a consumer lands).
+ */
+s64 pmbus_reg2data(const struct pmbus_driver_info *info,
+ enum pmbus_sensor_classes class,
+ u16 raw, u8 vout_mode);
+
+/*
+ * Transport helpers.
+ *
+ * Thin wrappers over the U-Boot DM I2C primitives that handle PMBus
+ * framing details (little endian word layout, two stage block read,
+ * CLEAR_FAULTS pseudo command without payload).
+ */
+
+/* Read a byte register. */
+int pmbus_read_byte(struct udevice *dev, u8 cmd, u8 *val);
+
+/* Read a 16 bit register, little endian on the wire. */
+int pmbus_read_word(struct udevice *dev, u8 cmd, u16 *val);
+
+/* Write a byte register. */
+int pmbus_write_byte(struct udevice *dev, u8 cmd, u8 val);
+
+/* Write a 16 bit register, little endian on the wire. */
+int pmbus_write_word(struct udevice *dev, u8 cmd, u16 val);
+
+/*
+ * Block read of a vendor string register (MFR_ID, MFR_MODEL,
+ * MFR_REVISION). The first wire byte is the payload length; the
+ * helper does the second read for the payload itself, so even strict
+ * I2C controllers (which forbid over read on block transactions)
+ * work. Output is null terminated and printable only (non printable
+ * bytes are substituted with '.').
+ *
+ * reverse_bytes: some MPS NVM personalities store ASCII strings
+ * LSB first (chip returns "SPM" for the human string "MPS"); pass
+ * true to reverse on copy. Spec compliant chips pass false.
+ *
+ * Returns string length on success or a negative errno on bus error
+ * or invalid length byte. outsz must be at least 2.
+ */
+int pmbus_read_string(struct udevice *dev, u8 cmd, char *out, int outsz,
+ bool reverse_bytes);
+
+/* Issue a CLEAR_FAULTS (03h) write. Clears RAM sticky STATUS_*. */
+int pmbus_clear_faults(struct udevice *dev);
+
+/*
+ * Capability probe: is a word sized command implemented by the chip?
+ *
+ * Primary signal is the bus NAK -- compliant parts do not ACK a
+ * command they do not implement, so the word read fails. Secondary
+ * signal is a clean -> dirty transition of STATUS_CML[INVALID_COMMAND]
+ * across the read (a chip that ACKs but does not implement the
+ * register raises it). Non destructive: never issues CLEAR_FAULTS, so
+ * sticky fault history survives for a subsequent pmbus status; a
+ * pre existing CML fault disables the secondary signal so it cannot
+ * produce a false "unsupported".
+ *
+ * Returns true if the command appears supported, false otherwise.
+ */
+bool pmbus_word_command_supported(struct udevice *dev, u8 reg);
+
+/*
+ * High level snapshot printers shared by the pmbus CLI and board
+ * boot time diagnostics. Both operate on the current pmbus_active()
+ * device (select it first via pmbus_set_active()); chip is the I2C
+ * handle from pmbus_active_get_i2c() / the CLI's require_active().
+ *
+ * pmbus_print_telemetry: decodes VIN / VOUT / IIN / IOUT / POUT / TEMP
+ * through the active device's pmbus_driver_info (LINEAR / DIRECT / VID
+ * per VOUT_MODE and per class format), skipping commands the chip does
+ * not implement (pmbus_word_command_supported). Falls back to
+ * LINEAR16 / LINEAR11 when no driver_info is cached. Caller prints the
+ * header line.
+ *
+ * pmbus_print_status_word: reads + decodes STATUS_WORD with the active
+ * device's chip specific status_overrides, if any.
+ */
+void pmbus_print_telemetry(struct udevice *chip);
+void pmbus_print_status_word(struct udevice *chip);
+
+/*
+ * Status bit name decoding.
+ *
+ * Sparse mask to name table. pmbus_print_bits() emits only the bits
+ * that are SET in v, joined by |; if no bit is set, prints
+ * clean. Bits not in the table are silently ignored (RESERVED bits
+ * or chip specific bits handled by a separate per chip table).
+ */
+struct pmbus_bit {
+ u16 mask;
+ const char *name;
+};
+
+void pmbus_print_bits(u16 v, const struct pmbus_bit *tab);
+
+/*
+ * Per chip override entry. When a chip reuses a PMBus standard
+ * STATUS bit for a documented chip specific signal (for example MPS
+ * uses STATUS_WORD bit[12] = MFR_SPECIFIC as NVM_SUMMARY, bit[8] =
+ * UNKNOWN as WATCH_DOG, bit[0] = NONE_ABOVE as DRMOS_FAULT), the
+ * per chip driver supplies a sparse table of (reg, mask, name)
+ * triples and pmbus_print_status_bits() substitutes the chip name
+ * for the standard one when the bit is set.
+ *
+ * reg is one of PMBUS_STATUS_WORD / VOUT / IOUT / INPUT /
+ * TEMPERATURE / CML, so the same table can carry overrides for
+ * every status register on the chip in one place. Tables that omit
+ * a (reg, mask) leave the standard name in place.
+ *
+ * Override entries whose mask is NOT in the standard table are
+ * still printed (the chip can extend coverage beyond PMBus 1.x for
+ * vendor specific bits in standard registers).
+ *
+ * Tables are NULL terminated: the last entry has .name = NULL.
+ * Following the same convention U-Boot uses for struct udevice_id
+ * and other driver tables avoids the explicit-count foot-gun.
+ */
+struct pmbus_status_override {
+ u8 reg;
+ u16 mask;
+ const char *name;
+};
+
+/*
+ * Print the bit names of a STATUS_* register value. For each bit
+ * set in v, prefer a chip override matching (reg, mask) over the
+ * standard std table entry; if neither matches, the bit is
+ * silently skipped (RESERVED). If no bit is set at all, prints
+ * clean. Pass ovr = NULL to disable the override path.
+ */
+void pmbus_print_status_bits(u8 reg, u16 v,
+ const struct pmbus_bit *std,
+ const struct pmbus_status_override *ovr);
+
+/*
+ * Built in PMBus 1.3 standard bit tables (use these from per chip
+ * drivers and board diagnostics; vendor extensions go in chip local
+ * tables that the per chip driver passes alongside these).
+ */
+/*
+ * All tables below are NULL terminated (last entry has .name = NULL),
+ * so callers walk with for (t = tab; t && t->name; t++) and the
+ * helpers above need no count argument.
+ */
+extern const struct pmbus_bit pmbus_status_word_bits[];
+extern const struct pmbus_bit pmbus_status_vout_bits[];
+extern const struct pmbus_bit pmbus_status_iout_bits[];
+extern const struct pmbus_bit pmbus_status_input_bits[];
+extern const struct pmbus_bit pmbus_status_temp_bits[];
+extern const struct pmbus_bit pmbus_status_cml_bits[];
+
+/*
+ * Active device tracking for the pmbus U-Boot CLI.
+ *
+ * The framework keeps one active PMBus device. It is selected by
+ * pmbus dev <bus>:<addr> (raw I2C tuple) and remembered across
+ * subcommands so subsequent invocations of pmbus telemetry,
+ * pmbus status, pmbus dump, etc. operate on the same chip
+ * without re-typing the address.
+ *
+ * pmbus_set_active() probes the chip's MFR_ID at the given address,
+ * looks the result up in the chip-match registry (populated by
+ * per chip drivers via pmbus_register_chip()), and caches the
+ * resulting struct pmbus_driver_info. Subcommands consult
+ * pmbus_active() to find the cached metadata.
+ */
+struct pmbus_active_dev {
+ bool valid;
+ int bus_seq;
+ u8 addr;
+ char vendor[PMBUS_VENDOR_NAME_MAX];
+ char name[PMBUS_REGULATOR_NAME_MAX]; /* DT regulator-name when bound; "" otherwise */
+ char mfr_id[PMBUS_MFR_STRING_MAX];
+ char mfr_model[PMBUS_MFR_STRING_MAX];
+ char mfr_revision[PMBUS_MFR_STRING_MAX];
+ const struct pmbus_driver_info *info;
+};
+
+const struct pmbus_active_dev *pmbus_active(void);
+int pmbus_active_get_i2c(struct udevice **i2c_dev);
+int pmbus_set_active(int bus_seq, u8 addr);
+void pmbus_clear_active(void);
+
+/*
+ * Per chip driver / board file registers a chip match so the
+ * framework can associate an MFR_ID prefix (read at probe time)
+ * with a vendor namespace ("mps", "lltc", "renesas", ...) and a
+ * pmbus_driver_info pointer. The first matching entry wins.
+ *
+ * mfr_id_reverse flags MPS style chips that store the MFR_ID
+ * string LSB first (chip returns "SPM" for the human string
+ * "MPS"); the framework reads the string in both orderings and
+ * matches against the prefix in the natural reading.
+ */
+struct pmbus_chip_match {
+ const char *mfr_id;
+ bool mfr_id_reverse;
+ const char *vendor;
+ const struct pmbus_driver_info *info;
+};
+
+int pmbus_register_chip(const struct pmbus_chip_match *match);
+
+/*
+ * Resolve a regulator-name (DT regulator-name property) to its
+ * (bus, addr) tuple by walking UCLASS_REGULATOR. Used by the
+ * pmbus dev <name> CLI alias so a chip bound through DT can be
+ * selected by its human readable rail name (e.g. "+0V8_VDD")
+ * instead of the i2c bus / address pair. Returns 0 on success
+ * (out parameters populated and the regulator probed), or a
+ * negative errno if no match is found or the bus / address cannot
+ * be derived. Available only when CONFIG_DM_REGULATOR is set.
+ */
+int pmbus_resolve_by_name(const char *name, int *bus_seq, u8 *addr);
+
+/*
+ * Vendor extension dispatcher.
+ *
+ * When the user types pmbus <vendor> <args...>, the framework
+ * looks up the registered handler for <vendor> and calls it with
+ * the argv tail (argv[0] = "<vendor>"). The handler operates on
+ * pmbus_active(), or returns CMD_RET_USAGE if the active device is
+ * not from this vendor.
+ *
+ * Per chip drivers register their vendor handler at init time. The
+ * MPS extension publishes pmbus mps last, pmbus mps clear last,
+ * and pmbus mps clear force.
+ */
+typedef int (*pmbus_vendor_handler_t)(struct cmd_tbl *cmdtp, int flag,
+ int argc, char *const argv[]);
+
+struct pmbus_vendor_op {
+ const char *vendor;
+ pmbus_vendor_handler_t handler;
+ const char *help;
+};
+
+int pmbus_register_vendor_handler(const struct pmbus_vendor_op *op);
+const struct pmbus_vendor_op *pmbus_lookup_vendor(const char *vendor);
+unsigned int pmbus_vendor_count(void);
+const struct pmbus_vendor_op *pmbus_vendor_at(unsigned int i);
+
+#endif /* _PMBUS_H_ */
diff --git a/lib/Kconfig b/lib/Kconfig
index 9e0f0ad7d06..24e55ade4d3 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -283,6 +283,22 @@ config PANIC_HANG
development since you can try to debug the conditions that lead to
the situation.
+config PMBUS
+ bool "PMBus 1.x decoder and transport helpers"
+ depends on DM_I2C
+ help
+ Enable include/pmbus.h and lib/pmbus.c: standard PMBus 1.x
+ command codes, LINEAR11, LINEAR16, and DIRECT numeric format
+ decoders, the two stage SMBus block read helper used for
+ MFR_ID, MFR_MODEL, and MFR_REVISION reads, and the table
+ driven STATUS_* bit print helper.
+
+ This is the substrate for any pre kernel PMBus consumer in
+ U-Boot (board/nxp/common/vid.c rail trim, board local telemetry
+ diagnostics, future per chip regulator drivers under
+ drivers/power/regulator/). It is not a hwmon clone. See
+ doc/develop/pmbus.rst for the policy notes.
+
config REGEX
bool "Enable regular expression support"
default y if NET_LEGACY
diff --git a/lib/Makefile b/lib/Makefile
index d0ffabc2b47..222378a8531 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -53,6 +53,7 @@ obj-y += rc4.o
obj-$(CONFIG_RBTREE) += rbtree.o
obj-$(CONFIG_BITREVERSE) += bitrev.o
obj-y += list_sort.o
+obj-$(CONFIG_PMBUS) += pmbus.o
endif
obj-$(CONFIG_$(PHASE_)TPM) += tpm-common.o
diff --git a/lib/pmbus.c b/lib/pmbus.c
new file mode 100644
index 00000000000..d49576ad3bd
--- /dev/null
+++ b/lib/pmbus.c
@@ -0,0 +1,859 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2026 Free Mobile, Vincent Jardin
+ *
+ * PMBus 1.x decoders, transport helpers, and standard status bit
+ * tables for U-Boot. See include/pmbus.h for the API surface and
+ * doc/develop/pmbus.rst for the porting guide.
+ *
+ * Decoder math is implemented from the PMBus 1.3 specification:
+ * 1. Part I (transport): see
+ * doc/PMBus/PMBus_Specification_Rev_1_3_1_Part_I_20150313.{pdf,txt}
+ * 2. Part II (commands): see
+ * doc/PMBus/PMBus_Specification_Rev_1_3_1_Part_II_20150313.{pdf,txt}
+ *
+ * Reference Linux implementation: linux/drivers/hwmon/pmbus/pmbus_core.c
+ * (the kernel side `struct pmbus_data` caching and hwmon publication
+ * layers do not apply; only the arithmetic carries over).
+ *
+ * No code in this file may reference a specific board, SoC, or
+ * product. Per chip quirks (MPS DIRECT format LSBs, vendor registers,
+ * VID coercion, ADDR_VBOOT auto promotion, and the like) belong in
+ * per chip drivers under drivers/power/regulator/ or in board local
+ * files under board/<vendor>/<board>/.
+ */
+
+#include <ctype.h>
+#include <dm.h>
+#include <i2c.h>
+#include <log.h>
+#include <pmbus.h>
+#include <limits.h>
+#include <linux/bitops.h>
+#include <power/regulator.h>
+
+static int pmbus_sign_extend(unsigned int v, unsigned int width)
+{
+ unsigned int mask = (1U << width) - 1U;
+ unsigned int sign = 1U << (width - 1U);
+
+ v &= mask;
+ if (v & sign)
+ v |= ~mask;
+ return (int)v;
+}
+
+s64 pmbus_reg2data_linear11(u16 raw)
+{
+ int mantissa = pmbus_sign_extend(raw & PB_LINEAR11_MANT_MASK,
+ PB_LINEAR11_MANT_BITS);
+ int exponent = pmbus_sign_extend((raw >> PB_LINEAR11_EXP_SHIFT) &
+ PB_LINEAR11_EXP_MASK,
+ PB_LINEAR11_EXP_BITS);
+ s64 micro;
+
+ /* Engineering value = mantissa * 2^exponent, scaled to micro units. */
+ micro = (s64)mantissa * 1000000LL;
+ if (exponent >= 0)
+ micro <<= exponent;
+ else
+ micro >>= -exponent;
+ return micro;
+}
+
+s64 pmbus_reg2data_linear16(u16 raw, u8 vout_mode)
+{
+ int exponent;
+ s64 micro;
+
+ /*
+ * VOUT_MODE bits[7:5] = mode; bits[4:0] = parameter. Linear mode
+ * (000) treats bits[4:0] as the signed 5-bit exponent. For other
+ * modes the caller must dispatch elsewhere.
+ */
+ if ((vout_mode & PB_VOUT_MODE_MODE_MASK) != PB_VOUT_MODE_LINEAR)
+ return 0;
+
+ exponent = pmbus_sign_extend(vout_mode & PB_VOUT_MODE_PARAM_MASK, 5);
+
+ /* Mantissa is unsigned 16-bit; scale to micro units. */
+ micro = (s64)raw * 1000000LL;
+ if (exponent >= 0)
+ micro <<= exponent;
+ else
+ micro >>= -exponent;
+ return micro;
+}
+
+s64 pmbus_reg2data_direct(s16 raw, int m, int b, int R)
+{
+ s64 acc;
+
+ if (m == 0)
+ return 0;
+
+ /*
+ * PMBus Part II sec 8.4: Y = (1/m) * (X * 10^-R - b)
+ *
+ * Pre scale acc to micro units so the final integer division by
+ * m absorbs the rounding loss into the least significant micro
+ * digit rather than into a coarser place.
+ */
+ acc = (s64)raw * 1000000LL;
+
+ /* Apply 10^-R: positive R means divide; negative R means multiply. */
+ while (R > 0) {
+ acc /= 10;
+ R--;
+ }
+ while (R < 0) {
+ acc *= 10;
+ R++;
+ }
+
+ /* Subtract the offset b, also in micro units. */
+ acc -= (s64)b * 1000000LL;
+
+ /* Final: divide by m. */
+ acc /= m;
+ return acc;
+}
+
+u16 pmbus_data2reg_linear16(s64 micro, u8 vout_mode)
+{
+ int exponent;
+ s64 raw;
+
+ if ((vout_mode & PB_VOUT_MODE_MODE_MASK) != PB_VOUT_MODE_LINEAR)
+ return 0;
+
+ exponent = pmbus_sign_extend(vout_mode & PB_VOUT_MODE_PARAM_MASK, 5);
+
+ /* raw = micro / (2^exponent * 10^6). */
+ raw = micro;
+ if (exponent >= 0)
+ raw >>= exponent;
+ else
+ raw <<= -exponent;
+ raw /= 1000000LL;
+
+ if (raw < 0)
+ raw = 0;
+ if (raw > U16_MAX)
+ raw = U16_MAX;
+ return (u16)raw;
+}
+
+u16 pmbus_data2reg_direct(s64 micro, int m, int b, int R)
+{
+ s64 acc;
+
+ if (m == 0)
+ return 0;
+
+ /*
+ * Inverse of pmbus_reg2data_direct(): X = (m * Y + b) * 10^R.
+ * Work in micro units throughout: acc = m * Y_micro + b * 10^6,
+ * then scale by 10^R, finally divide by 10^6 to get the raw
+ * chip count. Order chosen to match the decoder's quantisation
+ * pattern so a round trip (data2reg then reg2data) returns the
+ * input within +/- one LSB.
+ */
+ acc = (s64)m * micro + (s64)b * 1000000LL;
+
+ while (R > 0) {
+ acc *= 10;
+ R--;
+ }
+ while (R < 0) {
+ acc /= 10;
+ R++;
+ }
+
+ acc /= 1000000LL;
+
+ /* PMBus 1.3 Part II sec 8.4 mandates a signed 16 bit raw value. */
+ if (acc > S16_MAX)
+ acc = S16_MAX;
+ if (acc < S16_MIN)
+ acc = S16_MIN;
+ return (u16)(s16)acc;
+}
+
+s64 pmbus_reg2data(const struct pmbus_driver_info *info,
+ enum pmbus_sensor_classes class,
+ u16 raw, u8 vout_mode)
+{
+ if (!info || class >= PSC_NUM_CLASSES)
+ return 0;
+
+ switch (info->format[class]) {
+ case pmbus_fmt_linear:
+ if (class == PSC_VOLTAGE_OUT)
+ return pmbus_reg2data_linear16(raw, vout_mode);
+ return pmbus_reg2data_linear11(raw);
+
+ case pmbus_fmt_direct:
+ return pmbus_reg2data_direct((s16)raw,
+ info->m[class],
+ info->b[class],
+ info->R[class]);
+
+ case pmbus_fmt_vid:
+ case pmbus_fmt_ieee754:
+ /*
+ * Not yet wired up. Add when a consumer lands. VID needs
+ * the per page vrm_version table from the kernel's
+ * pmbus_reg2data_vid(); IEEE754 needs the half precision
+ * decoder from pmbus_reg2data_ieee754().
+ */
+ return 0;
+ }
+
+ return 0;
+}
+
+int pmbus_read_byte(struct udevice *dev, u8 cmd, u8 *val)
+{
+ return dm_i2c_read(dev, cmd, val, 1);
+}
+
+int pmbus_read_word(struct udevice *dev, u8 cmd, u16 *val)
+{
+ u8 raw[2];
+ int ret;
+
+ ret = dm_i2c_read(dev, cmd, raw, 2);
+ if (ret)
+ return ret;
+ *val = (u16)raw[0] | ((u16)raw[1] << 8);
+ return 0;
+}
+
+int pmbus_write_byte(struct udevice *dev, u8 cmd, u8 val)
+{
+ return dm_i2c_write(dev, cmd, &val, 1);
+}
+
+int pmbus_write_word(struct udevice *dev, u8 cmd, u16 val)
+{
+ u8 raw[2];
+
+ raw[0] = (u8)(val & 0xff); /* PMBus words are little-endian */
+ raw[1] = (u8)(val >> 8);
+ return dm_i2c_write(dev, cmd, raw, 2);
+}
+
+int pmbus_read_string(struct udevice *dev, u8 cmd, char *out, int outsz,
+ bool reverse_bytes)
+{
+ u8 raw[PMBUS_MFR_STRING_MAX];
+ int ret, len, i;
+
+ if (outsz < 2)
+ return -EINVAL;
+
+ /* Stage 1: read the length byte. */
+ ret = dm_i2c_read(dev, cmd, raw, 1);
+ if (ret)
+ return ret;
+
+ len = raw[0];
+ if (len <= 0 || len > (int)sizeof(raw))
+ return -EBADMSG;
+ if (len > outsz - 1)
+ len = outsz - 1;
+
+ /* Stage 2: reread length + payload (some controllers mandate this). */
+ ret = dm_i2c_read(dev, cmd, raw, len + 1);
+ if (ret)
+ return ret;
+
+ if (reverse_bytes) {
+ for (i = 0; i < len; i++) {
+ u8 b = raw[len - i];
+
+ out[i] = isprint(b) ? (char)b : '.';
+ }
+ } else {
+ for (i = 0; i < len; i++) {
+ u8 b = raw[i + 1];
+
+ out[i] = isprint(b) ? (char)b : '.';
+ }
+ }
+ out[len] = '\0';
+ return len;
+}
+
+int pmbus_clear_faults(struct udevice *dev)
+{
+ return dm_i2c_write(dev, PMBUS_CLEAR_FAULTS, NULL, 0);
+}
+
+void pmbus_print_bits(u16 v, const struct pmbus_bit *tab)
+{
+ const struct pmbus_bit *t;
+ int first = 1;
+
+ if (v == 0) {
+ printf("clean");
+ return;
+ }
+ for (t = tab; t && t->name; t++) {
+ if (v & t->mask) {
+ printf("%s%s", first ? "" : "|", t->name);
+ first = 0;
+ }
+ }
+}
+
+void pmbus_print_status_bits(u8 reg, u16 v,
+ const struct pmbus_bit *std,
+ const struct pmbus_status_override *ovr)
+{
+ const struct pmbus_bit *s;
+ const struct pmbus_status_override *o;
+ int first = 1;
+
+ if (v == 0) {
+ printf("clean");
+ return;
+ }
+
+ /*
+ * Pass 1: walk the standard table in declared order so the
+ * printout retains the conventional bit-15-first ordering. For
+ * each set bit, prefer a chip override matching (reg, mask).
+ */
+ for (s = std; s && s->name; s++) {
+ const char *name;
+
+ if (!(v & s->mask))
+ continue;
+ name = s->name;
+ for (o = ovr; o && o->name; o++) {
+ if (o->reg == reg && o->mask == s->mask) {
+ name = o->name;
+ break;
+ }
+ }
+ printf("%s%s", first ? "" : "|", name);
+ first = 0;
+ }
+
+ /*
+ * Pass 2: print overrides whose mask is not in the standard
+ * table at all (chip-specific bit at a position the spec
+ * leaves RESERVED). These would otherwise be swallowed.
+ */
+ for (o = ovr; o && o->name; o++) {
+ bool in_std = false;
+
+ if (o->reg != reg)
+ continue;
+ if (!(v & o->mask))
+ continue;
+ for (s = std; s && s->name; s++) {
+ if (s->mask == o->mask) {
+ in_std = true;
+ break;
+ }
+ }
+ if (in_std)
+ continue;
+ printf("%s%s", first ? "" : "|", o->name);
+ first = 0;
+ }
+}
+
+/*
+ * Standard PMBus 1.3 status bit tables. Per-chip drivers may publish
+ * their own tables for vendor extended bits (e.g. NVM summary bits,
+ * DR MOS faults) but the standard layout below is the safe baseline.
+ *
+ * All tables are NULL terminated (`name = NULL` sentinel), matching
+ * the convention used elsewhere in U-Boot for driver tables.
+ */
+const struct pmbus_bit pmbus_status_word_bits[] = {
+ { PB_STATUS_VOUT, "VOUT" },
+ { PB_STATUS_IOUT_POUT, "IOUT_POUT" },
+ { PB_STATUS_INPUT, "INPUT" },
+ { PB_STATUS_WORD_MFR, "MFR" },
+ { PB_STATUS_POWER_GOOD_N, "PG#" },
+ { PB_STATUS_FANS, "FANS" },
+ { PB_STATUS_OTHER, "OTHER" },
+ { PB_STATUS_UNKNOWN, "UNKNOWN" },
+ { PB_STATUS_BUSY, "BUSY" },
+ { PB_STATUS_OFF, "OFF" },
+ { PB_STATUS_VOUT_OV, "VOUT_OV" },
+ { PB_STATUS_IOUT_OC, "IOUT_OC" },
+ { PB_STATUS_VIN_UV, "VIN_UV" },
+ { PB_STATUS_TEMPERATURE, "TEMP" },
+ { PB_STATUS_CML, "CML" },
+ { PB_STATUS_NONE_ABOVE, "NONE_ABOVE" },
+ { /* sentinel */ }
+};
+
+const struct pmbus_bit pmbus_status_vout_bits[] = {
+ { PB_VOLTAGE_OV_FAULT, "VOUT_OV_FAULT" },
+ { PB_VOLTAGE_OV_WARNING, "VOUT_OV_WARN" },
+ { PB_VOLTAGE_UV_WARNING, "VOUT_UV_WARN" },
+ { PB_VOLTAGE_UV_FAULT, "VOUT_UV_FAULT" },
+ { PB_VOLTAGE_VOUT_MAX_MIN_WARN, "VOUT_MAX_MIN_WARN" },
+ { /* sentinel */ }
+};
+
+const struct pmbus_bit pmbus_status_iout_bits[] = {
+ { PB_IOUT_OC_FAULT, "IOUT_OC_FAULT" },
+ { PB_IOUT_OC_LV_FAULT, "IOUT_OC_LV_FAULT" },
+ { PB_IOUT_OC_WARNING, "IOUT_OC_WARN" },
+ { PB_IOUT_UC_FAULT, "IOUT_UC_FAULT" },
+ { PB_CURRENT_SHARE_FAULT, "ISHARE_FAULT" },
+ { PB_POWER_LIMITING, "POWER_LIMITING" },
+ { PB_POUT_OP_FAULT, "POUT_OP_FAULT" },
+ { PB_POUT_OP_WARNING, "POUT_OP_WARN" },
+ { /* sentinel */ }
+};
+
+const struct pmbus_bit pmbus_status_input_bits[] = {
+ { PB_IIN_OC_FAULT, "IIN_OC_FAULT" },
+ { PB_IIN_OC_WARNING, "IIN_OC_WARN" },
+ { PB_PIN_OP_WARNING, "PIN_OP_WARN" },
+ { /* sentinel */ }
+};
+
+const struct pmbus_bit pmbus_status_temp_bits[] = {
+ { PB_TEMP_OT_FAULT, "OT_FAULT" },
+ { PB_TEMP_OT_WARNING, "OT_WARN" },
+ { PB_TEMP_UT_WARNING, "UT_WARN" },
+ { PB_TEMP_UT_FAULT, "UT_FAULT" },
+ { /* sentinel */ }
+};
+
+const struct pmbus_bit pmbus_status_cml_bits[] = {
+ { PB_CML_FAULT_INVALID_COMMAND, "INVALID_CMD" },
+ { PB_CML_FAULT_INVALID_DATA, "INVALID_DATA" },
+ { PB_CML_FAULT_PACKET_ERROR, "PEC" },
+ { PB_CML_FAULT_MEMORY, "MEM" },
+ { PB_CML_FAULT_PROCESSOR, "PROC" },
+ { PB_CML_FAULT_OTHER_COMM, "OTHER_COMM" },
+ { PB_CML_FAULT_OTHER_MEM_LOGIC, "OTHER_MEM_LOGIC" },
+ { /* sentinel */ }
+};
+
+/*
+ * Active device tracking + chip / vendor registries (consumed by the
+ * `pmbus` U-Boot CLI command in cmd/pmbus.c).
+ */
+
+#define PMBUS_MAX_CHIP_MATCHES 8
+#define PMBUS_MAX_VENDOR_HANDLERS 4
+
+static struct pmbus_active_dev pmbus_active_state;
+static const struct pmbus_chip_match *pmbus_chip_table[PMBUS_MAX_CHIP_MATCHES];
+static unsigned int pmbus_chip_table_n;
+static const struct pmbus_vendor_op *pmbus_vendor_table[PMBUS_MAX_VENDOR_HANDLERS];
+static unsigned int pmbus_vendor_table_n;
+
+const struct pmbus_active_dev *pmbus_active(void)
+{
+ return pmbus_active_state.valid ? &pmbus_active_state : NULL;
+}
+
+void pmbus_clear_active(void)
+{
+ memset(&pmbus_active_state, 0, sizeof(pmbus_active_state));
+}
+
+int pmbus_active_get_i2c(struct udevice **i2c_dev)
+{
+ struct udevice *bus;
+ int ret;
+
+ if (!pmbus_active_state.valid)
+ return -ENODEV;
+ ret = uclass_get_device_by_seq(UCLASS_I2C, pmbus_active_state.bus_seq, &bus);
+ if (ret)
+ return ret;
+ return i2c_get_chip(bus, pmbus_active_state.addr, 1, i2c_dev);
+}
+
+/* engineering value in micro units -> "I.FFF<unit>" (3 fractional digits) */
+static void pmbus_emit_micro(s64 micro, const char *unit)
+{
+ s64 abs_milli = (micro < 0 ? -micro : micro) / 1000LL;
+
+ printf("%lld.%03lld%s", (long long)(micro / 1000000LL),
+ (long long)(abs_milli % 1000LL), unit);
+}
+
+struct pmbus_telem_entry {
+ u8 reg;
+ const char *label;
+ enum pmbus_sensor_classes class;
+ const char *unit;
+};
+
+/*
+ * Telemetry register set, in print order. POUT is included so PSU
+ * class parts report input/output power; chips that do not implement
+ * a given command are skipped via pmbus_word_command_supported().
+ */
+static const struct pmbus_telem_entry pmbus_telem_table[] = {
+ { PMBUS_READ_VIN, "VIN ", PSC_VOLTAGE_IN, "V" },
+ { PMBUS_READ_VOUT, "VOUT", PSC_VOLTAGE_OUT, "V" },
+ { PMBUS_READ_IIN, "IIN ", PSC_CURRENT_IN, "A" },
+ { PMBUS_READ_IOUT, "IOUT", PSC_CURRENT_OUT, "A" },
+ { PMBUS_READ_POUT, "POUT", PSC_POWER, "W" },
+ { PMBUS_READ_TEMPERATURE_1, "TEMP", PSC_TEMPERATURE, "C" },
+};
+
+bool pmbus_word_command_supported(struct udevice *dev, u8 reg)
+{
+ u8 cml_before = 0, cml_after = 0;
+ bool have_cml;
+ u16 w;
+
+ have_cml = !pmbus_read_byte(dev, PMBUS_STATUS_CML, &cml_before);
+
+ if (pmbus_read_word(dev, reg, &w))
+ return false; /* NAK: unsupported command not ACKed */
+
+ if (have_cml && !(cml_before & PB_CML_FAULT_INVALID_COMMAND) &&
+ !pmbus_read_byte(dev, PMBUS_STATUS_CML, &cml_after) &&
+ (cml_after & PB_CML_FAULT_INVALID_COMMAND))
+ return false; /* ACKed but chip raised INVALID_COMMAND */
+
+ return true;
+}
+
+/* Telemetry of the currently selected page. */
+static void pmbus_print_telemetry_page(struct udevice *chip,
+ const struct pmbus_active_dev *act)
+{
+ u8 vout_mode = 0;
+ unsigned int i;
+
+ /*
+ * On a read failure vout_mode stays 0 (LINEAR, exponent 0). That is
+ * a silent mis-scale of every VOLTAGE_OUT reading, so make the
+ * fallback visible rather than printing a wrong voltage as if good.
+ */
+ if (pmbus_read_byte(chip, PMBUS_VOUT_MODE, &vout_mode))
+ printf(" (VOUT_MODE read failed; VOUT decode assumes LINEAR exp 0)\n");
+
+ for (i = 0; i < ARRAY_SIZE(pmbus_telem_table); i++) {
+ const struct pmbus_telem_entry *e = &pmbus_telem_table[i];
+ u16 raw = 0;
+
+ /*
+ * Class gating. A chip driver that declares classes_present
+ * lists exactly the sensors it implements (kernel-style
+ * per-chip sensor set), so unlisted classes are skipped
+ * silently -- this is what hides the MPS buck's uncalibrated
+ * POUT / IIN. A generic / undeclared device instead gets a
+ * live capability probe per class.
+ */
+ if (act->info && act->info->classes_present) {
+ if (!(act->info->classes_present & BIT(e->class)))
+ continue;
+ } else if (!pmbus_word_command_supported(chip, e->reg)) {
+ printf(" %s : (not supported)\n", e->label);
+ continue;
+ }
+
+ if (pmbus_read_word(chip, e->reg, &raw)) {
+ printf(" %s : (read failed)\n", e->label);
+ continue;
+ }
+
+ printf(" %s : raw=0x%04x ", e->label, raw);
+ if (act->info) {
+ u16 dec = raw;
+
+ /*
+ * Some DIRECT format parts (e.g. MPS) report
+ * temperature as 1 degC/LSB in the low byte only;
+ * mask there. LINEAR temperatures use all 16 bits
+ * and must NOT be masked.
+ */
+ if (e->class == PSC_TEMPERATURE &&
+ act->info->format[PSC_TEMPERATURE] == pmbus_fmt_direct)
+ dec = raw & 0x00ff;
+
+ pmbus_emit_micro(pmbus_reg2data(act->info, e->class,
+ dec, vout_mode),
+ e->unit);
+ } else if (e->class == PSC_VOLTAGE_OUT) {
+ pmbus_emit_micro(pmbus_reg2data_linear16(raw, vout_mode),
+ e->unit);
+ } else {
+ pmbus_emit_micro(pmbus_reg2data_linear11(raw), e->unit);
+ printf(" (LINEAR11 fallback)");
+ }
+ printf("\n");
+ }
+}
+
+void pmbus_print_telemetry(struct udevice *chip)
+{
+ const struct pmbus_active_dev *act = pmbus_active();
+ int npages, p;
+ u8 zero = 0;
+
+ if (!act)
+ return;
+
+ /*
+ * Multi-rail parts (PSU bricks) expose one rail per PMBUS_PAGE.
+ * Chip drivers set pmbus_driver_info.pages; the generic driver
+ * takes it from the DT `pmbus,num-pages` (default 1). We always
+ * write PMBUS_PAGE before reading a page -- including page 0 --
+ * because a device may power up selected on a different page, which
+ * is what made the 48V PSU read all-zeros before. Only valid pages
+ * (0..npages-1) are ever written, so we never induce the
+ * out-of-range-PAGE STATUS_CML fault and the device's sticky fault
+ * log is left untouched (no CLEAR_FAULTS, no scrubbing).
+ */
+ npages = (act->info && act->info->pages > 0) ? act->info->pages : 1;
+
+ for (p = 0; p < npages; p++) {
+ u8 pg = (u8)p;
+
+ if (dm_i2c_write(chip, PMBUS_PAGE, &pg, 1)) {
+ printf(" [page %d] PAGE select failed\n", p);
+ continue;
+ }
+ if (npages > 1)
+ printf(" [page %d]\n", p);
+ pmbus_print_telemetry_page(chip, act);
+ }
+
+ if (npages > 1)
+ dm_i2c_write(chip, PMBUS_PAGE, &zero, 1); /* leave on page 0 */
+}
+
+void pmbus_print_status_word(struct udevice *chip)
+{
+ const struct pmbus_active_dev *act = pmbus_active();
+ const struct pmbus_status_override *ovr =
+ (act && act->info) ? act->info->status_overrides : NULL;
+ u16 word = 0;
+
+ if (pmbus_read_word(chip, PMBUS_STATUS_WORD, &word)) {
+ printf(" STATUS_WORD (79h) = (read failed)\n");
+ return;
+ }
+ printf(" STATUS_WORD (79h) = 0x%04x [", word);
+ pmbus_print_status_bits(PMBUS_STATUS_WORD, word,
+ pmbus_status_word_bits, ovr);
+ printf("]\n");
+}
+
+static const struct pmbus_chip_match *pmbus_match_mfr(const char *id)
+{
+ unsigned int i;
+
+ if (!id || !id[0])
+ return NULL;
+ for (i = 0; i < pmbus_chip_table_n; i++) {
+ const struct pmbus_chip_match *m = pmbus_chip_table[i];
+ size_t plen = strlen(m->mfr_id);
+
+ if (strlen(id) >= plen && !strncmp(id, m->mfr_id, plen))
+ return m;
+ }
+ return NULL;
+}
+
+/*
+ * Walk UCLASS_REGULATOR looking for a regulator whose I2C parent
+ * bus seq + DT reg address match the requested (bus_seq, addr).
+ * Returns the regulator-name (uclass plat .name) on hit, or NULL if
+ * no UCLASS_REGULATOR device matches (chip not bound through DT, or
+ * CONFIG_DM_REGULATOR disabled).
+ */
+static const char *pmbus_lookup_regname(int bus_seq, u8 addr)
+{
+ struct uclass *uc;
+ struct udevice *r;
+
+ if (!IS_ENABLED(CONFIG_DM_REGULATOR))
+ return NULL;
+
+ if (uclass_get(UCLASS_REGULATOR, &uc))
+ return NULL;
+ uclass_foreach_dev(r, uc) {
+ struct dm_regulator_uclass_plat *up;
+ struct udevice *parent = dev_get_parent(r);
+ int ra;
+
+ if (!parent || device_get_uclass_id(parent) != UCLASS_I2C)
+ continue;
+ if (dev_seq(parent) != bus_seq)
+ continue;
+ ra = dev_read_addr(r);
+ if (ra < 0 || (u8)ra != addr)
+ continue;
+ up = dev_get_uclass_plat(r);
+ if (up && up->name)
+ return up->name;
+ return r->name;
+ }
+ return NULL;
+}
+
+int pmbus_set_active(int bus_seq, u8 addr)
+{
+ const struct pmbus_chip_match *match = NULL;
+ struct udevice *bus, *chip;
+ char id_fwd[PMBUS_MFR_STRING_MAX] = "";
+ char id_rev[PMBUS_MFR_STRING_MAX] = "";
+ const char *rname;
+ int ret;
+
+ pmbus_clear_active();
+
+ ret = uclass_get_device_by_seq(UCLASS_I2C, bus_seq, &bus);
+ if (ret)
+ return ret;
+ ret = i2c_get_chip(bus, addr, 1, &chip);
+ if (ret)
+ return ret;
+
+ pmbus_active_state.bus_seq = bus_seq;
+ pmbus_active_state.addr = addr;
+
+ /*
+ * Probe MFR_ID in both byte orders. Spec compliant chips return
+ * "MPS" / "TI" / etc. in the natural reading (forward); MPS NVM
+ * personalities store the string LSB first and need the reverse
+ * read. Chip table entries declare which one is canonical for
+ * the chip family they describe.
+ */
+ if (pmbus_read_string(chip, PMBUS_MFR_ID, id_fwd, sizeof(id_fwd), false) < 0)
+ id_fwd[0] = '\0';
+ if (pmbus_read_string(chip, PMBUS_MFR_ID, id_rev, sizeof(id_rev), true) < 0)
+ id_rev[0] = '\0';
+
+ match = pmbus_match_mfr(id_fwd);
+ if (match && !match->mfr_id_reverse) {
+ strlcpy(pmbus_active_state.mfr_id, id_fwd,
+ sizeof(pmbus_active_state.mfr_id));
+ } else {
+ match = pmbus_match_mfr(id_rev);
+ if (match && match->mfr_id_reverse) {
+ strlcpy(pmbus_active_state.mfr_id, id_rev,
+ sizeof(pmbus_active_state.mfr_id));
+ } else {
+ /* No registered match; cache the forward read as best effort. */
+ strlcpy(pmbus_active_state.mfr_id,
+ id_fwd[0] ? id_fwd : id_rev,
+ sizeof(pmbus_active_state.mfr_id));
+ }
+ }
+
+ if (match) {
+ pmbus_active_state.info = match->info;
+ if (match->vendor)
+ strlcpy(pmbus_active_state.vendor, match->vendor,
+ sizeof(pmbus_active_state.vendor));
+ }
+
+ /*
+ * MFR_MODEL / MFR_REVISION are best effort. Use the same byte
+ * order the matched chip declared; if nothing matched, use the
+ * forward order.
+ */
+ {
+ bool reverse = match && match->mfr_id_reverse;
+
+ pmbus_read_string(chip, PMBUS_MFR_MODEL,
+ pmbus_active_state.mfr_model,
+ sizeof(pmbus_active_state.mfr_model), reverse);
+ pmbus_read_string(chip, PMBUS_MFR_REVISION,
+ pmbus_active_state.mfr_revision,
+ sizeof(pmbus_active_state.mfr_revision), reverse);
+ }
+
+ rname = pmbus_lookup_regname(bus_seq, addr);
+ if (rname)
+ strlcpy(pmbus_active_state.name, rname,
+ sizeof(pmbus_active_state.name));
+
+ pmbus_active_state.valid = true;
+ return 0;
+}
+
+int pmbus_register_chip(const struct pmbus_chip_match *match)
+{
+ if (!match || !match->mfr_id)
+ return -EINVAL;
+ if (pmbus_chip_table_n >= PMBUS_MAX_CHIP_MATCHES)
+ return -ENOSPC;
+ pmbus_chip_table[pmbus_chip_table_n++] = match;
+ return 0;
+}
+
+int pmbus_register_vendor_handler(const struct pmbus_vendor_op *op)
+{
+ if (!op || !op->vendor || !op->handler)
+ return -EINVAL;
+ if (pmbus_vendor_table_n >= PMBUS_MAX_VENDOR_HANDLERS)
+ return -ENOSPC;
+ pmbus_vendor_table[pmbus_vendor_table_n++] = op;
+ return 0;
+}
+
+const struct pmbus_vendor_op *pmbus_lookup_vendor(const char *vendor)
+{
+ unsigned int i;
+
+ if (!vendor)
+ return NULL;
+ for (i = 0; i < pmbus_vendor_table_n; i++)
+ if (!strcmp(pmbus_vendor_table[i]->vendor, vendor))
+ return pmbus_vendor_table[i];
+ return NULL;
+}
+
+unsigned int pmbus_vendor_count(void)
+{
+ return pmbus_vendor_table_n;
+}
+
+const struct pmbus_vendor_op *pmbus_vendor_at(unsigned int i)
+{
+ return i < pmbus_vendor_table_n ? pmbus_vendor_table[i] : NULL;
+}
+
+int pmbus_resolve_by_name(const char *name, int *bus_seq, u8 *addr)
+{
+ struct udevice *reg;
+ struct udevice *parent;
+ int ret;
+ int a;
+
+ if (!IS_ENABLED(CONFIG_DM_REGULATOR))
+ return -ENOSYS;
+
+ if (!name || !bus_seq || !addr)
+ return -EINVAL;
+
+ ret = regulator_get_by_platname(name, ®);
+ if (ret)
+ return ret;
+
+ parent = dev_get_parent(reg);
+ if (!parent || device_get_uclass_id(parent) != UCLASS_I2C)
+ return -ENODEV;
+
+ a = dev_read_addr(reg);
+ if (a < 0 || a > 0x7f)
+ return -EINVAL;
+
+ *bus_seq = dev_seq(parent);
+ *addr = (u8)a;
+ return 0;
+}
--
2.43.0
More information about the U-Boot
mailing list