PMBus regulators differ in numeric formats and quirks, not in how they are driven. Share that common behaviour as a regulator-uclass adapter so chip drivers and the pmbus CLI do not each reimplement the decode and transport, and add a catch-all driver on compatible = "pmbus" for compliant chips that have no dedicated driver yet. Gated by CONFIG_DM_REGULATOR_PMBUS_HELPER and CONFIG_DM_REGULATOR_PMBUS_GENERIC. Signed-off-by: Vincent Jardin <vjardin@free.fr> Signed-off-by: Peng Fan <peng.fan@nxp.com>
878 lines
23 KiB
C
878 lines
23 KiB
C
// 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;
|
|
}
|