From 0663b4f553789f8d38220a543116563885693120 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Thu, 9 Jul 2026 23:02:39 +0000 Subject: power: regulator: Add helper to set voltage within an acceptable range Add regulator_set_value_clamp() that clamps a requested target voltage to both caller-provided limits and the regulator constraints before setting the regulator voltage value. Return -EINVAL when the caller limits cannot be satisfied by the regulator constraints or when the requested range is invalid. This helper will initially be used to set vqmmc-supply voltage within an acceptable range according to SD Standards, i.e. 1.70V-1.95V and 2.7V-3.6V. Signed-off-by: Jonas Karlman Signed-off-by: Peng Fan --- include/power/regulator.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) (limited to 'include') diff --git a/include/power/regulator.h b/include/power/regulator.h index 4011fb1d254..b8023721a16 100644 --- a/include/power/regulator.h +++ b/include/power/regulator.h @@ -315,6 +315,18 @@ int regulator_set_suspend_value(struct udevice *dev, int uV); */ int regulator_get_suspend_value(struct udevice *dev); +/** + * regulator_set_value_clamp: set clamped microvoltage value of a given regulator + * + * @dev - pointer to the regulator device + * @min_uV - the minimum output value to set [micro Volts] + * @target_uV - the target output value to set [micro Volts] + * @max_uV - the maximum output value to set [micro Volts] + * Return: - 0 on success or -errno val if fails + */ +int regulator_set_value_clamp(struct udevice *dev, + int min_uV, int target_uV, int max_uV); + /** * regulator_set_value_force: set the microvoltage value of a given regulator * without any min-,max condition check @@ -550,6 +562,12 @@ static inline int regulator_get_suspend_value(struct udevice *dev) return -ENOSYS; } +static inline int regulator_set_value_clamp(struct udevice *dev, + int min_uV, int target_uV, int max_uV) +{ + return -ENOSYS; +} + static inline int regulator_set_value_force(struct udevice *dev, int uV) { return -ENOSYS; -- cgit v1.3.1 From 10acd5bc9a6c453b86486156e76f1fbbe116e285 Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Thu, 9 Jul 2026 23:02:40 +0000 Subject: test: dm: regulator: Add regulator_set_value_clamp() tests Add a sandbox LDO3 with a configurable 1.8V to 3.3V range and use it to test regulator_set_value_clamp(). Test in-range requests, clamping against the regulator limits, invalid ranges outside the regulator limits and a min value higher than max. Signed-off-by: Jonas Karlman Reviewed-by: Peng Fan Signed-off-by: Peng Fan --- arch/sandbox/dts/sandbox_pmic.dtsi | 6 ++++++ doc/usage/cmd/dm.rst | 3 ++- drivers/power/regulator/sandbox.c | 11 ++++++----- include/power/sandbox_pmic.h | 15 +++++++++++++-- test/dm/regulator.c | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 8 deletions(-) (limited to 'include') diff --git a/arch/sandbox/dts/sandbox_pmic.dtsi b/arch/sandbox/dts/sandbox_pmic.dtsi index ff2cb42844c..d90ab040827 100644 --- a/arch/sandbox/dts/sandbox_pmic.dtsi +++ b/arch/sandbox/dts/sandbox_pmic.dtsi @@ -47,6 +47,12 @@ regulator-min-microvolt = <1500000>; regulator-max-microvolt = <1500000>; }; + + ldo3 { + regulator-name = "SUPPLY_1.8_3.3V"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <3300000>; + }; }; &mc34708 { diff --git a/doc/usage/cmd/dm.rst b/doc/usage/cmd/dm.rst index 196b22d1376..ac1c437bd9c 100644 --- a/doc/usage/cmd/dm.rst +++ b/doc/usage/cmd/dm.rst @@ -409,7 +409,8 @@ This example shows the abridged sandbox output:: regulator 1 [ ] sandbox_buck | | |-- buck2 regulator 2 [ ] sandbox_ldo | | |-- ldo1 regulator 3 [ ] sandbox_ldo | | |-- ldo2 - regulator 4 [ ] sandbox_buck | | `-- no_match_by_nodename + regulator 4 [ ] sandbox_buck | | |-- no_match_by_nodename + regulator 5 [ ] sandbox_ldo | | `-- ldo3 pmic 1 [ ] mc34708_pmic | `-- pmic@41 bootcount 0 [ + ] bootcount-rtc |-- bootcount@0 bootcount 1 [ ] bootcount-i2c-eeprom |-- bootcount diff --git a/drivers/power/regulator/sandbox.c b/drivers/power/regulator/sandbox.c index 80a68f5a30d..813ee301e73 100644 --- a/drivers/power/regulator/sandbox.c +++ b/drivers/power/regulator/sandbox.c @@ -56,10 +56,11 @@ static struct dm_regulator_mode sandbox_buck_modes[] = { MODE(BUCK_OM_PWM, OM2REG(BUCK_OM_PWM), "PWM"), }; -/* LDO: 1,2 - voltage range */ +/* LDO: 1,2,3 - voltage range */ static struct output_range ldo_voltage_range[] = { RANGE(OUT_LDO1_UV_MIN, OUT_LDO1_UV_MAX, OUT_LDO1_UV_STEP), RANGE(OUT_LDO2_UV_MIN, OUT_LDO2_UV_MAX, OUT_LDO2_UV_STEP), + RANGE(OUT_LDO3_UV_MIN, OUT_LDO3_UV_MAX, OUT_LDO3_UV_STEP), }; /* LDO: 1 - current range */ @@ -288,8 +289,8 @@ static int ldo_set_voltage(struct udevice *dev, int uV) static int ldo_get_current(struct udevice *dev) { - /* LDO2 - unsupported */ - if (dev->driver_data == 2) + /* LDO: 2,3 - unsupported */ + if (dev->driver_data >= 2) return -ENOSYS; return out_get_value(dev, SANDBOX_LDO_COUNT, OUT_REG_UA, @@ -298,8 +299,8 @@ static int ldo_get_current(struct udevice *dev) static int ldo_set_current(struct udevice *dev, int uA) { - /* LDO2 - unsupported */ - if (dev->driver_data == 2) + /* LDO: 2,3 - unsupported */ + if (dev->driver_data >= 2) return -ENOSYS; return out_set_value(dev, SANDBOX_LDO_COUNT, OUT_REG_UA, diff --git a/include/power/sandbox_pmic.h b/include/power/sandbox_pmic.h index 1dbd15b5235..50ef39b45e9 100644 --- a/include/power/sandbox_pmic.h +++ b/include/power/sandbox_pmic.h @@ -13,10 +13,10 @@ #define SANDBOX_OF_BUCK_PREFIX "buck" #define SANDBOX_BUCK_COUNT 3 -#define SANDBOX_LDO_COUNT 2 +#define SANDBOX_LDO_COUNT 3 /* * Sandbox PMIC registers: - * We have only 12 significant registers, but we alloc 16 for padding. + * We have only 15 significant registers, but we alloc 16 for padding. */ enum { SANDBOX_PMIC_REG_BUCK1_UV = 0, @@ -36,6 +36,10 @@ enum { SANDBOX_PMIC_REG_LDO2_UA, SANDBOX_PMIC_REG_LDO2_OM, + SANDBOX_PMIC_REG_LDO3_UV, + SANDBOX_PMIC_REG_LDO3_UA, + SANDBOX_PMIC_REG_LDO3_OM, + SANDBOX_PMIC_REG_COUNT = 16, }; @@ -94,6 +98,11 @@ enum { #define OUT_LDO2_UV_MAX 3950000 #define OUT_LDO2_UV_STEP 50000 +/* LDO3 Voltage: min: 0.75V, step: 50mV, max 3.95V */ +#define OUT_LDO3_UV_MIN 750000 +#define OUT_LDO3_UV_MAX 3950000 +#define OUT_LDO3_UV_STEP 50000 + /* register <-> value conversion */ #define REG2VAL(min, step, reg) ((min) + ((step) * (reg))) #define VAL2REG(min, step, val) (((val) - (min)) / (step)) @@ -116,6 +125,8 @@ enum { #define SANDBOX_LDO1_PLATNAME "VDD_EMMC_1.8V" #define SANDBOX_LDO2_DEVNAME "ldo2" #define SANDBOX_LDO2_PLATNAME "VDD_LCD_3.3V" +#define SANDBOX_LDO3_DEVNAME "ldo3" +#define SANDBOX_LDO3_PLATNAME "SUPPLY_1.8_3.3V" /* * Expected regulators setup after call of: diff --git a/test/dm/regulator.c b/test/dm/regulator.c index 449748ad52f..51007d4079d 100644 --- a/test/dm/regulator.c +++ b/test/dm/regulator.c @@ -28,6 +28,7 @@ enum { BUCK3, LDO1, LDO2, + LDO3, OUTPUT_COUNT, }; @@ -44,6 +45,7 @@ static const char *regulator_names[OUTPUT_COUNT][OUTPUT_NAME_COUNT] = { { SANDBOX_BUCK3_DEVNAME, SANDBOX_BUCK3_PLATNAME }, { SANDBOX_LDO1_DEVNAME, SANDBOX_LDO1_PLATNAME}, { SANDBOX_LDO2_DEVNAME, SANDBOX_LDO2_PLATNAME}, + { SANDBOX_LDO3_DEVNAME, SANDBOX_LDO3_PLATNAME}, }; /* Test regulator get method */ @@ -118,6 +120,42 @@ static int dm_test_power_regulator_set_get_voltage(struct unit_test_state *uts) } DM_TEST(dm_test_power_regulator_set_get_voltage, UTF_SCAN_FDT); +/* Test regulator set Voltage clamp method */ +static int dm_test_power_regulator_set_value_clamp(struct unit_test_state *uts) +{ + struct udevice *dev; + const char *platname; + + /* LDO3 have 'min' 1.8V and 'max' 3.3V */ + platname = regulator_names[LDO3][PLATNAME]; + ut_assertok(regulator_get_by_platname(platname, &dev)); + + /* 'target' in 'min'/'max' range - should not clamp voltage */ + ut_assertok(regulator_set_value_clamp(dev, 1700000, 1800000, 1950000)); + ut_asserteq(1800000, regulator_get_value(dev)); + ut_assertok(regulator_set_value_clamp(dev, 2700000, 3300000, 3600000)); + ut_asserteq(3300000, regulator_get_value(dev)); + + /* 'target' out of 'min'/'max' range - should clamp voltage */ + ut_assertok(regulator_set_value_clamp(dev, 1700000, 1700000, 1950000)); + ut_asserteq(1800000, regulator_get_value(dev)); + ut_assertok(regulator_set_value_clamp(dev, 2700000, 3400000, 3600000)); + ut_asserteq(3300000, regulator_get_value(dev)); + + /* 'min'/'max' out of range - should return -EINVAL */ + ut_asserteq(-EINVAL, + regulator_set_value_clamp(dev, 1200000, 1500000, 1700000)); + ut_asserteq(-EINVAL, + regulator_set_value_clamp(dev, 3500000, 4000000, 5000000)); + + /* 'min' higher than 'max' - should return -EINVAL */ + ut_asserteq(-EINVAL, + regulator_set_value_clamp(dev, 3100000, 3000000, 2900000)); + + return 0; +} +DM_TEST(dm_test_power_regulator_set_value_clamp, UTF_SCAN_FDT); + /* Test regulator set and get Current method */ static int dm_test_power_regulator_set_get_current(struct unit_test_state *uts) { -- cgit v1.3.1 From 0f04bbf9ab3751405308f0a8aef2b6016a072b7d Mon Sep 17 00:00:00 2001 From: Jonas Karlman Date: Fri, 10 Jul 2026 13:15:53 +0000 Subject: power: regulator: Fix regulator_enable/disable() macros The commit 076265b75e61 ("compat: regulator: add enable/disable macros") added regulator_enable/disable() macros to provide a closer analogue to the Linux API. These new macros wrap regulator_set_enable() that may return detailed error codes and should typically not directly be called by drivers. Change the macros to wrap regulator_set_enable_if_allowed() that masks error codes consumers typically do not expect to closer match the behavior of the Linux API. Also move the macros outside the #if #else blocks to make the macros available when !CONFIG_IS_ENABLED(DM_REGULATOR). Fixes: 076265b75e61 ("compat: regulator: add enable/disable macros") Signed-off-by: Jonas Karlman Signed-off-by: Peng Fan --- include/power/regulator.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'include') diff --git a/include/power/regulator.h b/include/power/regulator.h index b8023721a16..8d4d1450b90 100644 --- a/include/power/regulator.h +++ b/include/power/regulator.h @@ -371,9 +371,6 @@ int regulator_get_enable(struct udevice *dev); */ int regulator_set_enable(struct udevice *dev, bool enable); -#define regulator_enable(dev) regulator_set_enable(dev, true) -#define regulator_disable(dev) regulator_set_enable(dev, false) - /** * regulator_set_enable_if_allowed: set regulator enable state if allowed by * regulator @@ -651,4 +648,7 @@ static inline int device_get_supply_regulator(struct udevice *dev, const char *s } #endif +#define regulator_enable(dev) regulator_set_enable_if_allowed(dev, true) +#define regulator_disable(dev) regulator_set_enable_if_allowed(dev, false) + #endif /* _INCLUDE_REGULATOR_H_ */ -- cgit v1.3.1 From cfb427255287b357a4b08b3d3ba9154137270d61 Mon Sep 17 00:00:00 2001 From: Vincent Jardin Date: Sun, 12 Jul 2026 20:20:43 +0200 Subject: pmbus: add PMBus 1.x framework, CLI, and binding 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 Signed-off-by: Peng Fan --- MAINTAINERS | 8 + cmd/Kconfig | 14 + cmd/Makefile | 1 + cmd/pmbus.c | 814 +++++++++++++++++++++++++++++++++++++++++++++++ doc/develop/index.rst | 1 + doc/develop/pmbus.rst | 637 +++++++++++++++++++++++++++++++++++++ include/pmbus.h | 630 ++++++++++++++++++++++++++++++++++++ lib/Kconfig | 16 + lib/Makefile | 1 + lib/pmbus.c | 860 ++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 2982 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 (limited to 'include') diff --git a/MAINTAINERS b/MAINTAINERS index ddf7337d2fd..c16ea0e88e3 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1532,6 +1532,14 @@ S: Maintained F: cmd/pci_mps.c F: test/cmd/pci_mps.c +PMBUS +M: Vincent Jardin +S: Maintained +F: cmd/pmbus.c +F: doc/develop/pmbus.rst +F: include/pmbus.h +F: lib/pmbus.c + POWER M: Jaehoon Chung M: Peng Fan diff --git a/cmd/Kconfig b/cmd/Kconfig index a527b218949..35ce42f8397 100644 --- a/cmd/Kconfig +++ b/cmd/Kconfig @@ -2723,6 +2723,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 :'. + + Per chip drivers and board files publish vendor extensions in + the 'pmbus ...' 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..7b1be897373 --- /dev/null +++ b/cmd/pmbus.c @@ -0,0 +1,814 @@ +// 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 + * + lib/pmbus.c. + * + * See doc/develop/pmbus.rst for the full usage reference. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +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, 10, &b)) + return -EINVAL; + if (strict_strtoul(colon + 1, 16, &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; + + 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; + } + } + if (strict_strtoul(s, 16, &v) || v > 0xff) + return -EINVAL; + *reg = (u8)v; + return 0; +} + +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 :' 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 : 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 :'.\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 :' 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: \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 )\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->mfr_reverse) >= 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], 16, &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], 10, &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], 10, &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 ' 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 [:|] - show / select active PMBus device\n" + " ( decimal, // hex)\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 [b|w|s] - raw read (b=byte, w=word, s=string)\n" + "pmbus write [b|w] - raw write\n" + "pmbus clear [faults] - issue CLEAR_FAULTS (03h)\n" + "pmbus vout [] - read / set VOUT_COMMAND (microvolts)\n" + "pmbus scan [] - PMBus aware probe of one or all I2C buses\n" + "pmbus help - list registered vendor extensions\n" + "\n" + "Vendor extensions (pmbus ...) 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 51fd68fa04b..0b757e8601e 100644 --- a/doc/develop/index.rst +++ b/doc/develop/index.rst @@ -49,6 +49,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..f5550541e83 --- /dev/null +++ b/doc/develop/pmbus.rst @@ -0,0 +1,637 @@ +.. 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 ; pmbus telemetry``, + ``regulator dev ; 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/ | + | .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/// | + | _diag.c | + | Diagnostic commands only: | + | _info / _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///`` 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 (decimal sequence number) and 7 bit address (hex, +``0x`` optional, à la ``i2c`` convention):: + + => pmbus dev 0:10 + 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 [:] 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 [b|w|s] raw read (b=byte, w=word, s=string) + pmbus write [b|w] raw write + pmbus clear [faults] issue CLEAR_FAULTS (03h) + pmbus vout [] read or set VOUT_COMMAND (microvolts) + pmbus scan [] PMBus aware probe of one or all I2C buses + +The ```` argument accepts either a hexadecimal address +(``88``, ``0x`` optional) or a symbolic name (``READ_VIN``, +``VOUT_MODE``, ``MFR_ID``); symbolic names win when both parsed. +```` is hexadecimal too. Only ``pmbus vout``'s microvolt +argument and bus numbers are decimal. +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 ...`` 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 `` and ``vdd_read`` commands +remain available in their original form for compatibility with +existing AVS production scripts. The new ``pmbus vout`` and +``pmbus vout `` 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 ; regulator value`` +* the ``pmbus dev `` CLI command (resolves to the regulator) +* another DT consumer with a ``regulator-supplies`` reference + +The probe chain looks like this:: + + _probe(dev) + pmbus_regulator_probe_common(dev, &_info, page) + dev_read_addr(dev) -> reg = + i2c_get_chip(dev->parent, addr) -> I2C chip handle + priv->i2c_dev = handle + priv->info = &_info + priv->page = page + (page > 0) write PMBUS_PAGE + _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(&_match) [idempotent] + pmbus_register_vendor_handler(&_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 ``:`` through + ``pmbus_set_active()``) +* the chip's vendor extension subcommands (``pmbus ...``) + +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/.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 + => 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/.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 + ``,`` 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 = ``; +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..f86e0673e41 --- /dev/null +++ b/include/pmbus.h @@ -0,0 +1,630 @@ +/* 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///. + */ + +#ifndef _PMBUS_H_ +#define _PMBUS_H_ + +#include + +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 : (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]; + bool mfr_reverse; /* chip stores MFR strings LSB first */ + 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 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 , the framework + * looks up the registered handler for and calls it with + * the argv tail (argv[0] = ""). 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..4192abde91c --- /dev/null +++ b/lib/pmbus.c @@ -0,0 +1,860 @@ +// 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///. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +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" (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_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; +} -- cgit v1.3.1 From 350b7c83b6d5dcb7186562c479acd932cefc9292 Mon Sep 17 00:00:00 2001 From: Vincent Jardin Date: Sun, 12 Jul 2026 20:20:44 +0200 Subject: power: regulator: add generic PMBus UCLASS_REGULATOR adapter 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 Signed-off-by: Peng Fan --- MAINTAINERS | 3 + drivers/power/regulator/Kconfig | 24 +++ drivers/power/regulator/Makefile | 2 + drivers/power/regulator/pmbus_generic.c | 90 +++++++++ drivers/power/regulator/pmbus_helper.c | 315 ++++++++++++++++++++++++++++++++ drivers/power/regulator/pmbus_helper.h | 90 +++++++++ include/pmbus.h | 37 ++++ lib/pmbus.c | 17 ++ 8 files changed, 578 insertions(+) create mode 100644 drivers/power/regulator/pmbus_generic.c create mode 100644 drivers/power/regulator/pmbus_helper.c create mode 100644 drivers/power/regulator/pmbus_helper.h (limited to 'include') diff --git a/MAINTAINERS b/MAINTAINERS index c16ea0e88e3..6986d2ccd5c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1537,6 +1537,9 @@ M: Vincent Jardin S: Maintained F: cmd/pmbus.c F: doc/develop/pmbus.rst +F: drivers/power/regulator/pmbus_generic.c +F: drivers/power/regulator/pmbus_helper.c +F: drivers/power/regulator/pmbus_helper.h F: include/pmbus.h F: lib/pmbus.c diff --git a/drivers/power/regulator/Kconfig b/drivers/power/regulator/Kconfig index 3b3ed97eb9f..4336b78db15 100644 --- a/drivers/power/regulator/Kconfig +++ b/drivers/power/regulator/Kconfig @@ -557,3 +557,27 @@ config DM_REGULATOR_MT6359 MediaTek MT6359 PMIC. This driver supports the control of different power rails of device through regulator interface. + +config DM_REGULATOR_PMBUS_HELPER + bool "Shared regulator helpers for PMBus chip drivers" + depends on DM_REGULATOR && PMBUS && DM_I2C + help + Provide shared get_value / set_value / get_enable / set_enable + operations for UCLASS_REGULATOR drivers that bind PMBus 1.x + compliant voltage regulators. Per chip drivers + (mps,mpq8785, lltc,ltc3882, ...) consume this helper to avoid + duplicating the LINEAR16 / DIRECT decoder dispatch and the + VOUT_MODE / VOUT_COMMAND / OPERATION transport sequences. + +config DM_REGULATOR_PMBUS_GENERIC + bool "Generic PMBus 1.x regulator driver (compatible=\"pmbus\")" + depends on DM_REGULATOR_PMBUS_HELPER + help + Catch all UCLASS_REGULATOR driver bound to compatible = "pmbus". + Auto detects the VOUT numeric format from the chip's VOUT_MODE + register and exposes telemetry plus voltage set / get against + the standard PMBus 1.x command codes. Use this for PMBus + compliant chips that have no per chip driver yet; promote to a + per chip driver only when chip specific quirks (vendor + registers, VID coercion, ADDR pin auto promotion, non standard + m / b / R coefficients) need handling. diff --git a/drivers/power/regulator/Makefile b/drivers/power/regulator/Makefile index 36a84e7cd71..b084e9ac4bc 100644 --- a/drivers/power/regulator/Makefile +++ b/drivers/power/regulator/Makefile @@ -49,3 +49,5 @@ obj-$(CONFIG_REGULATOR_RZG2L_USBPHY) += rzg2l-usbphy-regulator.o obj-$(CONFIG_$(PHASE_)DM_REGULATOR_CPCAP) += cpcap_regulator.o obj-$(CONFIG_DM_REGULATOR_MT6357) += mt6357_regulator.o obj-$(CONFIG_DM_REGULATOR_MT6359) += mt6359_regulator.o +obj-$(CONFIG_DM_REGULATOR_PMBUS_HELPER) += pmbus_helper.o +obj-$(CONFIG_DM_REGULATOR_PMBUS_GENERIC) += pmbus_generic.o diff --git a/drivers/power/regulator/pmbus_generic.c b/drivers/power/regulator/pmbus_generic.c new file mode 100644 index 00000000000..6ba50ce08f8 --- /dev/null +++ b/drivers/power/regulator/pmbus_generic.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2026 Free Mobile, Vincent Jardin + * + * Generic PMBus 1.x compatible voltage regulator driver. + * + * Catch all driver bound to compatible = "pmbus" for chips that have + * no per chip driver under drivers/power/regulator/. The probe path + * detects the VOUT numeric format from VOUT_MODE bits[7:5]: + * + * - 0 LINEAR16 with the exponent supplied via VOUT_MODE bits[4:0] + * - 1 VID; mapped to pmbus_fmt_vid (decoder returns 0 today; per + * chip driver still required to plug a VID table) + * - 2 DIRECT; default coefficients m=1, b=0, R=0 (per chip + * coefficients arrive via PMBUS_QUERY / PMBUS_COEFFICIENTS, + * not yet consumed by U-Boot; values may need a per chip + * driver if telemetry numbers are wrong) + * - 3 IEEE754; mapped to pmbus_fmt_ieee754 (decoder returns 0 + * today; per chip driver required) + * + * Other sensor classes (VIN, IIN, IOUT, TEMPERATURE) default to + * LINEAR which is the spec baseline for compliant chips. If an + * operator sees wrong telemetry numbers on this driver, the answer + * is to write a per chip driver with the correct format[] / m / b / R. + * + * Adapted in spirit from linux/drivers/hwmon/pmbus/pmbus.c (the + * kernel's generic probe driver). The U-Boot version drops the + * page count auto detection (most generic compliant parts are + * single rail; multi rail chips are quirky enough to need a per + * chip driver) and the kernel hwmon publication layers. + */ + +#include +#include +#include +#include +#include + +#include "pmbus_helper.h" + +struct pmbus_generic_priv { + struct pmbus_regulator_priv base; /* must be first */ + struct pmbus_driver_info info; +}; + +static int pmbus_generic_probe(struct udevice *dev) +{ + struct pmbus_generic_priv *gpriv = dev_get_priv(dev); + struct pmbus_driver_info *info = &gpriv->info; + enum pmbus_sensor_classes c; + int ret; + + info->pages = 1; + for (c = 0; c < PSC_NUM_CLASSES; c++) { + info->format[c] = pmbus_fmt_linear; + info->m[c] = 0; + info->b[c] = 0; + info->R[c] = 0; + } + + ret = pmbus_regulator_probe_common(dev, info, 0); + if (ret) + return ret; + + /* + * Avoid reading non supported pages to avoid device's sticky + * status. + */ + info->pages = dev_read_u32_default(dev, "pmbus,num-pages", 1); + if (info->pages < 1) + info->pages = 1; + + pmbus_regulator_identify_vout(gpriv->base.i2c_dev, info); + + return 0; +} + +static const struct udevice_id pmbus_generic_ids[] = { + { .compatible = "pmbus" }, + { } +}; + +U_BOOT_DRIVER(pmbus_generic_regulator) = { + .name = "pmbus_generic_regulator", + .id = UCLASS_REGULATOR, + .of_match = pmbus_generic_ids, + .probe = pmbus_generic_probe, + .ops = &pmbus_regulator_ops, + .priv_auto = sizeof(struct pmbus_generic_priv), +}; diff --git a/drivers/power/regulator/pmbus_helper.c b/drivers/power/regulator/pmbus_helper.c new file mode 100644 index 00000000000..4763470442d --- /dev/null +++ b/drivers/power/regulator/pmbus_helper.c @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2026 Free Mobile, Vincent Jardin + * + * Shared UCLASS_REGULATOR operations over the PMBus 1.x framework. + * See pmbus_helper.h for the API surface and doc/develop/pmbus.rst + * for the porting guide. + * + * No code in this file may reference a specific chip family or + * board. Chip specific quirks (vendor registers, VID coercion, + * ADDR pin auto promotion, byte reversed MFR strings, etc.) belong + * in the per chip driver under drivers/power/regulator/.c. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pmbus_helper.h" + +static int pmbus_regulator_select_page(struct pmbus_regulator_priv *priv) +{ + u8 p; + + if (priv->page <= 0) + return 0; + p = (u8)priv->page; + return dm_i2c_write(priv->i2c_dev, PMBUS_PAGE, &p, 1); +} + +static int pmbus_regulator_get_value(struct udevice *dev) +{ + struct pmbus_regulator_priv *priv = dev_get_priv(dev); + u8 vout_mode = 0; + u16 raw = 0; + s64 uv; + int ret; + + ret = pmbus_regulator_select_page(priv); + if (ret) + return ret; + pmbus_read_byte(priv->i2c_dev, PMBUS_VOUT_MODE, &vout_mode); + if (pmbus_read_word(priv->i2c_dev, PMBUS_READ_VOUT, &raw)) + return -EIO; + + if (priv->info) + uv = pmbus_reg2data(priv->info, PSC_VOLTAGE_OUT, raw, vout_mode); + else + uv = pmbus_reg2data_linear16(raw, vout_mode); + + if (uv > INT_MAX) + uv = INT_MAX; + if (uv < INT_MIN) + uv = INT_MIN; + return (int)uv; +} + +static int pmbus_regulator_set_value(struct udevice *dev, int uV) +{ + struct pmbus_regulator_priv *priv = dev_get_priv(dev); + u8 vout_mode = 0; + u8 buf[2]; + u16 raw; + int ret; + + ret = pmbus_regulator_select_page(priv); + if (ret) + return ret; + if (pmbus_read_byte(priv->i2c_dev, PMBUS_VOUT_MODE, &vout_mode)) + return -EIO; + + /* + * Dispatch on the chip's VOUT_MODE selector. LINEAR16 and DIRECT + * are wired today; VID and IEEE754 return -ENOSYS until their + * encoders land. For DIRECT, the m / b / R triple comes from the + * chip's pmbus_driver_info[PSC_VOLTAGE_OUT]; if the per chip + * driver did not populate them, the encoder cannot run. + */ + switch (vout_mode & PB_VOUT_MODE_MODE_MASK) { + case PB_VOUT_MODE_LINEAR: + raw = pmbus_data2reg_linear16((s64)uV, vout_mode); + break; + case PB_VOUT_MODE_DIRECT: + if (!priv->info || + priv->info->format[PSC_VOLTAGE_OUT] != pmbus_fmt_direct) + return -ENODATA; + raw = pmbus_data2reg_direct((s64)uV, + priv->info->m[PSC_VOLTAGE_OUT], + priv->info->b[PSC_VOLTAGE_OUT], + priv->info->R[PSC_VOLTAGE_OUT]); + break; + default: + return -ENOSYS; + } + + buf[0] = (u8)(raw & 0xff); + buf[1] = (u8)((raw >> 8) & 0xff); + return dm_i2c_write(priv->i2c_dev, PMBUS_VOUT_COMMAND, buf, 2); +} + +static int pmbus_regulator_get_enable(struct udevice *dev) +{ + struct pmbus_regulator_priv *priv = dev_get_priv(dev); + u8 op = 0; + int ret; + + ret = pmbus_regulator_select_page(priv); + if (ret) + return ret; + if (pmbus_read_byte(priv->i2c_dev, PMBUS_OPERATION, &op)) + return -EIO; + return (op & PB_OPERATION_ON) ? 1 : 0; +} + +static int pmbus_regulator_set_enable(struct udevice *dev, bool enable) +{ + struct pmbus_regulator_priv *priv = dev_get_priv(dev); + u8 op = 0; + int ret; + + ret = pmbus_regulator_select_page(priv); + if (ret) + return ret; + if (pmbus_read_byte(priv->i2c_dev, PMBUS_OPERATION, &op)) + return -EIO; + + if (enable) + op |= PB_OPERATION_ON; + else + op &= (u8)~PB_OPERATION_ON; + return dm_i2c_write(priv->i2c_dev, PMBUS_OPERATION, &op, 1); +} + +const struct dm_regulator_ops pmbus_regulator_ops = { + .get_value = pmbus_regulator_get_value, + .set_value = pmbus_regulator_set_value, + .get_enable = pmbus_regulator_get_enable, + .set_enable = pmbus_regulator_set_enable, +}; + +int pmbus_regulator_read_temp(struct udevice *reg_dev, int *temp_mc) +{ + struct pmbus_regulator_priv *priv; + u16 raw = 0; + s64 udeg; + int ret; + + if (!reg_dev || !temp_mc) + return -EINVAL; + priv = dev_get_priv(reg_dev); + if (!priv || !priv->i2c_dev) + return -ENODEV; + + ret = pmbus_regulator_select_page(priv); + if (ret) + return ret; + if (pmbus_read_word(priv->i2c_dev, PMBUS_READ_TEMPERATURE_1, &raw)) + return -EIO; + + /* + * vout_mode is meaningless for the temperature class. With a + * chip info record the dispatcher honours its per-class format + * (DIRECT m/b/R for MPS, LINEAR11 for spec-compliant parts); + * without one, fall back to the PMBus 1.x standard LINEAR11. + */ + if (priv->info) + udeg = pmbus_reg2data(priv->info, PSC_TEMPERATURE, raw, 0); + else + udeg = pmbus_reg2data_linear11(raw); + + *temp_mc = (int)(udeg / 1000); + return 0; +} + +enum pmbus_data_format +pmbus_regulator_identify_vout(struct udevice *i2c_dev, + struct pmbus_driver_info *info) +{ + u8 vout_mode = 0; + + if (pmbus_read_byte(i2c_dev, PMBUS_VOUT_MODE, &vout_mode)) + return info->format[PSC_VOLTAGE_OUT]; + + switch (vout_mode & PB_VOUT_MODE_MODE_MASK) { + case PB_VOUT_MODE_LINEAR: + info->format[PSC_VOLTAGE_OUT] = pmbus_fmt_linear; + break; + case PB_VOUT_MODE_VID: + info->format[PSC_VOLTAGE_OUT] = pmbus_fmt_vid; + break; + case PB_VOUT_MODE_DIRECT: + info->format[PSC_VOLTAGE_OUT] = pmbus_fmt_direct; + info->m[PSC_VOLTAGE_OUT] = 1; + info->b[PSC_VOLTAGE_OUT] = 0; + info->R[PSC_VOLTAGE_OUT] = 0; + break; + case PB_VOUT_MODE_IEEE754: + info->format[PSC_VOLTAGE_OUT] = pmbus_fmt_ieee754; + break; + default: + break; + } + return info->format[PSC_VOLTAGE_OUT]; +} + +const struct pmbus_driver_info *pmbus_regulator_info_by_addr(int bus_seq, + u8 addr) +{ + struct uclass *uc; + struct udevice *r; + + if (uclass_get(UCLASS_REGULATOR, &uc)) + return NULL; + + uclass_foreach_dev(r, uc) { + struct udevice *parent = dev_get_parent(r); + struct pmbus_regulator_priv *priv; + 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; + + /* + * Address matches. Only chips driven through this helper + * carry a pmbus_regulator_priv at the head of their priv; + * identify them by their shared ops vector so we never + * misread a foreign regulator's private layout. + */ + if (!r->driver || r->driver->ops != &pmbus_regulator_ops) + return NULL; + if (device_probe(r)) + return NULL; + priv = dev_get_priv(r); + return priv ? priv->info : NULL; + } + return NULL; +} + +/* + * Spawn the generic UCLASS_THERMAL companion (drivers/thermal/ + * pmbus_thermal.c) as a child of this regulator so READ_TEMPERATURE_1 + * is reachable through the standard `temperature list` / `temperature + * get` interface. Named "-temp" so several PMBus rails + * on one board produce distinct, descriptive device names. Failure is + * non-fatal: the chip still works as a UCLASS_REGULATOR. + */ +static void pmbus_regulator_bind_thermal(struct udevice *dev) +{ + struct udevice *therm; + const char *rname; + char name[48]; + + if (!IS_ENABLED(CONFIG_PMBUS_THERMAL)) + return; + if (device_bind_driver(dev, "pmbus_thermal", "pmbus-temp", &therm)) + return; + rname = dev_read_string(dev, "regulator-name"); + snprintf(name, sizeof(name), "%s-temp", rname ? rname : dev->name); + device_set_name(therm, name); +} + +int pmbus_regulator_probe_common(struct udevice *dev, + const struct pmbus_driver_info *info, + int page) +{ + struct pmbus_regulator_priv *priv = dev_get_priv(dev); + int chip_addr; + int ret; + + chip_addr = dev_read_addr(dev); + if (chip_addr < 0) + return -EINVAL; + + ret = i2c_get_chip(dev_get_parent(dev), (u32)chip_addr, 1, &priv->i2c_dev); + if (ret) + return ret; + + priv->info = info; + priv->page = page; + + if (page > 0) { + u8 p = (u8)page; + + ret = dm_i2c_write(priv->i2c_dev, PMBUS_PAGE, &p, 1); + if (ret) + return ret; + } + + pmbus_regulator_bind_thermal(dev); + return 0; +} + +int pmbus_regulator_apply_voltage_scale(struct udevice *dev, + u32 fb_divider_permille) +{ + struct pmbus_regulator_priv *priv = dev_get_priv(dev); + u8 buf[2]; + + if (fb_divider_permille == 0) + return 0; + buf[0] = (u8)(fb_divider_permille & 0xff); + buf[1] = (u8)((fb_divider_permille >> 8) & 0xff); + return dm_i2c_write(priv->i2c_dev, PMBUS_VOUT_SCALE_LOOP, buf, 2); +} diff --git a/drivers/power/regulator/pmbus_helper.h b/drivers/power/regulator/pmbus_helper.h new file mode 100644 index 00000000000..82f263cd513 --- /dev/null +++ b/drivers/power/regulator/pmbus_helper.h @@ -0,0 +1,90 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Copyright 2026 Free Mobile, Vincent Jardin + * + * Shared UCLASS_REGULATOR ops for PMBus 1.x voltage regulator chips. + * + * Per chip drivers under drivers/power/regulator/.c bind a + * vendor,chip compatible from DT and call pmbus_regulator_probe_common() + * in their .probe. They install pmbus_regulator_ops as the .ops vector; + * the helper handles VOUT_MODE / READ_VOUT / VOUT_COMMAND / OPERATION + * via the tree level framework. + * + * Per chip drivers retain control of identify hooks (VOUT_MODE based + * format selection), chip specific quirks (vendor registers, ADDR pin + * auto promotion), and DT property handling (e.g. MPS + * mps,vout-fb-divider-ratio-permille). + */ + +#ifndef _DRIVERS_POWER_REGULATOR_PMBUS_HELPER_H_ +#define _DRIVERS_POWER_REGULATOR_PMBUS_HELPER_H_ + +#include +#include + +struct udevice; +struct dm_regulator_ops; + +/* + * Per chip private state. The first field of every per chip driver's + * priv_auto must be (or contain at offset 0) a struct + * pmbus_regulator_priv so the shared ops vector can recover it via + * dev_get_priv(dev). + * + * i2c_dev chip handle obtained from dev->parent at probe time + * (the parent must be a UCLASS_I2C bus). + * page PMBUS_PAGE selector for multi rail chips. Single rail + * chips set page = 0; the helper writes PMBUS_PAGE only + * when page > 0 to avoid wasted bus traffic on single + * rail parts. + * info pointer to the chip's pmbus_driver_info; consumed by + * pmbus_reg2data() / pmbus_data2reg_linear16() to pick + * the right format[] / m / b / R coefficients. + */ +struct pmbus_regulator_priv { + struct udevice *i2c_dev; + int page; + const struct pmbus_driver_info *info; +}; + +extern const struct dm_regulator_ops pmbus_regulator_ops; + +/* + * Per chip probe glue. Reads `reg` from DT, gets the I2C chip handle + * from dev->parent, populates priv->i2c_dev / page / info, and writes + * PMBUS_PAGE if page > 0. Per chip drivers call this in their .probe + * before any chip specific identification. + */ +int pmbus_regulator_probe_common(struct udevice *dev, + const struct pmbus_driver_info *info, + int page); + +/* + * Optional helper for per chip drivers that honour an external + * feedback divider DT property (e.g. MPS mps,vout-fb-divider-ratio- + * permille). Writes the supplied ratio to PMBUS_VOUT_SCALE_LOOP at + * probe time. fb_divider_permille == 0 leaves the chip default. + */ +int pmbus_regulator_apply_voltage_scale(struct udevice *dev, + u32 fb_divider_permille); + +/* + * Read PMBUS_VOUT_MODE and set info->format[PSC_VOLTAGE_OUT] from its + * mode selector bits[7:5] per PMBus 1.3 Part II sec 8.3: + * LINEAR -> pmbus_fmt_linear + * VID -> pmbus_fmt_vid + * DIRECT -> pmbus_fmt_direct (default coefficients m=1, b=0, R=0) + * IEEE754 -> pmbus_fmt_ieee754 + * + * The single place that knows the VOUT_MODE bit layout; both the + * generic regulator and per chip drivers call it so they never + * re-implement the switch. Returns the selected format so a chip + * driver can post-adjust a quirk (e.g. MPS encodes VOUT in DIRECT + * with m=64 R=1 even when VOUT_MODE reports VID). On a VOUT_MODE read + * failure the format is left unchanged and the prior value is returned. + */ +enum pmbus_data_format +pmbus_regulator_identify_vout(struct udevice *i2c_dev, + struct pmbus_driver_info *info); + +#endif /* _DRIVERS_POWER_REGULATOR_PMBUS_HELPER_H_ */ diff --git a/include/pmbus.h b/include/pmbus.h index f86e0673e41..0e121fa6637 100644 --- a/include/pmbus.h +++ b/include/pmbus.h @@ -464,6 +464,43 @@ bool pmbus_word_command_supported(struct udevice *dev, u8 reg); void pmbus_print_telemetry(struct udevice *chip); void pmbus_print_status_word(struct udevice *chip); +/* + * Regulator -> thermal bridge. + * + * Read READ_TEMPERATURE_1 (8Dh) from a UCLASS_REGULATOR device that + * was bound by a pmbus_helper based chip driver, decode it through + * the chip's pmbus_driver_info (so the MPS DIRECT 1 degC/LSB quirk + * and the standard LINEAR11 encoding are both handled), select the + * regulator's PAGE first on multi rail parts, and return the result + * in millidegrees Celsius. + * + * This is what the generic drivers/thermal/pmbus_thermal.c companion + * calls on its parent; keeping the decode here avoids exposing the + * regulator-private pmbus_regulator_priv layout to other subsystems. + * + * Returns 0 on success, or a negative errno (-ENODEV if reg is not a + * probed pmbus regulator, -EIO on bus error). + */ +int pmbus_regulator_read_temp(struct udevice *reg_dev, int *temp_mc); + +/* + * Look up the pmbus_driver_info of a probed UCLASS_REGULATOR device at + * (bus_seq, addr) that is driven by a pmbus_helper based chip driver + * (mpq8785, pmbus_generic, ...). Probes the device so its identify + * hook has run and format[] is populated, then returns its + * driver_info. Returns NULL if no such regulator is bound at that + * address, if the device is not a pmbus regulator, or if + * CONFIG_DM_REGULATOR_PMBUS_HELPER is disabled. + * + * Lets pmbus_set_active() -- and thus the pmbus CLI and the board + * boot snapshots -- reuse the rich, VOUT_MODE detected driver_info of + * a DT bound generic / chip regulator when the device is selected by + * raw : (which has no chip-match registry entry and would + * otherwise fall back to blanket LINEAR16 / LINEAR11 decoding). + */ +const struct pmbus_driver_info *pmbus_regulator_info_by_addr(int bus_seq, + u8 addr); + /* * Status bit name decoding. * diff --git a/lib/pmbus.c b/lib/pmbus.c index 4192abde91c..62a66593638 100644 --- a/lib/pmbus.c +++ b/lib/pmbus.c @@ -761,6 +761,23 @@ int pmbus_set_active(int bus_seq, u8 addr) 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 -- cgit v1.3.1