summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/Kconfig16
-rw-r--r--lib/Makefile1
-rw-r--r--lib/pmbus.c877
3 files changed, 894 insertions, 0 deletions
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..62a66593638
--- /dev/null
+++ b/lib/pmbus.c
@@ -0,0 +1,877 @@
+// 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 + 1]; /* length byte + payload */
+ 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) - 1)
+ 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));
+ }
+
+ /*
+ * No MFR_ID chip-match (a spec compliant part with no per chip
+ * driver, e.g. a Flex / Delta PSU): if a generic / chip
+ * UCLASS_REGULATOR is bound at this address, reuse its
+ * VOUT_MODE detected driver_info so telemetry decodes through
+ * the right per class formats instead of the blanket
+ * LINEAR16 / LINEAR11 fallback.
+ */
+ if (CONFIG_IS_ENABLED(DM_REGULATOR_PMBUS_HELPER) &&
+ !pmbus_active_state.info) {
+ const struct pmbus_driver_info *di =
+ pmbus_regulator_info_by_addr(bus_seq, addr);
+
+ if (di)
+ pmbus_active_state.info = di;
+ }
+
+ /*
+ * 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_active_state.mfr_reverse = 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, &reg);
+ 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;
+}