diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/Kconfig | 16 | ||||
| -rw-r--r-- | lib/Makefile | 1 | ||||
| -rw-r--r-- | lib/pmbus.c | 877 |
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, ®); + 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; +} |
