diff options
| author | Tom Rini <trini@konsulko.com> | 2026-07-14 07:59:50 -0600 |
|---|---|---|
| committer | Tom Rini <trini@konsulko.com> | 2026-07-14 07:59:50 -0600 |
| commit | 501d76ca801343cce6dafb17740a8b679ed17072 (patch) | |
| tree | 1751270d21bbb6eb5feb444bd77dee625e41c3ad | |
| parent | 83f6cbb87177350e32eabcbe6dd92b951dabae25 (diff) | |
| parent | 722a82278752f4b9e6cf052a67e722508503f2c7 (diff) | |
Merge tag 'mmc-for-2026.10-rc1' of https://git.u-boot-project.org/u-boot/custodians/u-boot-mmcHEADmaster
CI: https://git.u-boot-project.org/u-boot/custodians/u-boot-mmc/-/pipelines/614
- Add PMBUS regulator, thermal and test
- Add regulator helper to set voltage within an acceptable range
- Update dw_mmc to use in-spec voltage range for vqmmc
- Fix regulator_enable/disable() macros
- Clear LPUART OR STAT in tstc to avoid hang
- Add MAINTAINERS entry for SDHCI
38 files changed, 4759 insertions, 75 deletions
diff --git a/MAINTAINERS b/MAINTAINERS index e53cb0a485c..e5b2a2e373c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1532,6 +1532,21 @@ S: Maintained F: cmd/pci_mps.c F: test/cmd/pci_mps.c +PMBUS +M: Vincent Jardin <vjardin@free.fr> +S: Maintained +F: cmd/pmbus.c +F: doc/develop/pmbus.rst +F: drivers/power/regulator/mpq8785.c +F: drivers/power/regulator/pmbus_generic.c +F: drivers/power/regulator/pmbus_helper.c +F: drivers/power/regulator/pmbus_helper.h +F: drivers/power/regulator/sandbox_pmbus.c +F: drivers/thermal/pmbus_thermal.c +F: include/pmbus.h +F: lib/pmbus.c +F: test/dm/pmbus.c + POWER M: Jaehoon Chung <jh80.chung@samsung.com> M: Peng Fan <peng.fan@nxp.com> @@ -1658,6 +1673,11 @@ F: drivers/firmware/scmi/ F: include/scmi* N: scmi +SDHCI CADENCE +M: Tanmay Kathpalia <tanmay.kathpalia@altera.com> +S: Maintained +F: drivers/mmc/sdhci-cadence* + SEAMA M: Linus Walleij <linusw@kernel.org> S: Maintained diff --git a/arch/Kconfig.nxp b/arch/Kconfig.nxp index ef9087d0197..152692e3230 100644 --- a/arch/Kconfig.nxp +++ b/arch/Kconfig.nxp @@ -242,24 +242,28 @@ config VOL_MONITOR_IR36021_SET config VOL_MONITOR_LTC3882_READ bool "Enable the LTC3882 voltage monitor read" + select PMBUS help This option enables LTC3882 voltage monitor read functionality. It is used by the common VID driver. config VOL_MONITOR_LTC3882_SET bool "Enable the LTC3882 voltage monitor set" + select PMBUS help This option enables LTC3882 voltage monitor set functionality. It is used by the common VID driver. config VOL_MONITOR_ISL68233_READ bool "Enable the ISL68233 voltage monitor read" + select PMBUS help This option enables ISL68233 voltage monitor read functionality. It is used by the common VID driver. config VOL_MONITOR_ISL68233_SET bool "Enable the ISL68233 voltage monitor set" + select PMBUS help This option enables ISL68233 voltage monitor set functionality. It is used by the common VID driver. 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/arch/sandbox/dts/test.dts b/arch/sandbox/dts/test.dts index 074e5c06ec8..fa23b179e38 100644 --- a/arch/sandbox/dts/test.dts +++ b/arch/sandbox/dts/test.dts @@ -1027,6 +1027,9 @@ emul1: emull { compatible = "sandbox,i2c-rtc-emul"; }; + emul_pmbus: emul-pmbus { + compatible = "sandbox,i2c-pmbus"; + }; }; sandbox_pmic: sandbox_pmic@40 { @@ -1038,6 +1041,13 @@ reg = <0x41>; sandbox,emul = <&emul_pmic1>; }; + + pmbus@70 { + reg = <0x70>; + compatible = "pmbus"; + regulator-name = "sandbox-pmbus-vout"; + sandbox,emul = <&emul_pmbus>; + }; }; i3c0 { diff --git a/board/nxp/common/vid.c b/board/nxp/common/vid.c index 84cb43fad56..70c3b3ecfd8 100644 --- a/board/nxp/common/vid.c +++ b/board/nxp/common/vid.c @@ -11,6 +11,7 @@ #include <i2c.h> #include <irq_func.h> #include <log.h> +#include <pmbus.h> #include <vsprintf.h> #include <asm/io.h> #ifdef CONFIG_FSL_LSCH2 @@ -260,45 +261,38 @@ static int read_voltage_from_IR(int i2caddress) */ #define VOUT_WARNING "VID: VOUT_MODE exponent has resolution worse than 1 V!\n" -/* Checks the PMBus voltage monitor for the format used for voltage values */ -static int get_pmbus_multiplier(DEVICE_HANDLE_T dev) +/* + * Read VOUT_MODE for downstream LINEAR16 decode/encode through the + * tree level <pmbus.h> helpers (pmbus_reg2data_linear16, + * pmbus_data2reg_linear16). Stores the raw VOUT_MODE byte in *mode + * and returns 0 on success, or the negative bus error (a failed read + * must not be decoded: raw 0 aliases Linear mode with exponent 0). + * Emits VOUT_WARNING on Linear mode chips with a non negative + * exponent (resolution >= 1 V is unusable for sub volt SoC rails) + * and an informational note on the unsupported VID format. + */ +static int vid_read_vout_mode(DEVICE_HANDLE_T dev, u8 *mode) { - u8 mode; - int exponent, multiplier, ret; + int ret; - ret = I2C_READ(dev, PMBUS_CMD_VOUT_MODE, &mode, sizeof(mode)); + ret = pmbus_read_byte(dev, PMBUS_VOUT_MODE, mode); if (ret) { printf("VID: unable to determine voltage multiplier\n"); - return 1; + return ret; } - /* Upper 3 bits is mode, lower 5 bits is exponent */ - exponent = (int)mode & 0x1F; - mode >>= 5; - switch (mode) { - case 0: - /* Linear, 5 bit twos component exponent */ - if (exponent & 0x10) { - multiplier = 1 << (16 - (exponent & 0xF)); - } else { - /* If exponent is >= 0, then resolution is 1 V! */ + switch (*mode & PB_VOUT_MODE_MODE_MASK) { + case PB_VOUT_MODE_LINEAR: + if (!(*mode & 0x10)) printf(VOUT_WARNING); - multiplier = 1; - } break; - case 1: - /* VID code identifier */ + case PB_VOUT_MODE_VID: printf("VID: custom VID codes are not supported\n"); - multiplier = MV_PER_V; break; default: - /* Direct, in mV */ - multiplier = MV_PER_V; break; } - - debug("VID: calculated multiplier is %d\n", multiplier); - return multiplier; + return 0; } #endif @@ -306,8 +300,8 @@ static int get_pmbus_multiplier(DEVICE_HANDLE_T dev) defined(CONFIG_VOL_MONITOR_LTC3882_READ) static int read_voltage_from_pmbus(int i2caddress) { - int ret, multiplier, vout; - u8 channel = PWM_CHANNEL0; + int ret, vout; + u8 channel = PWM_CHANNEL0, vout_mode; u16 vcode; DEVICE_HANDLE_T dev; @@ -317,25 +311,33 @@ static int read_voltage_from_pmbus(int i2caddress) return ret; /* Select the right page */ - ret = I2C_WRITE(dev, PMBUS_CMD_PAGE, &channel, sizeof(channel)); + ret = pmbus_write_byte(dev, PMBUS_PAGE, channel); if (ret) { printf("VID: failed to select VDD page %d\n", channel); return ret; } - /* VOUT is little endian */ - ret = I2C_READ(dev, PMBUS_CMD_READ_VOUT, (void *)&vcode, sizeof(vcode)); + ret = pmbus_read_word(dev, PMBUS_READ_VOUT, &vcode); if (ret) { printf("VID: failed to read core voltage\n"); return ret; } - /* Scale down to the real mV */ - multiplier = get_pmbus_multiplier(dev); - vout = (int)vcode; - /* Multiplier 1000 (direct mode) requires no change to convert */ - if (multiplier != MV_PER_V) - vout = DIV_ROUND_UP(vout * MV_PER_V, multiplier); + /* + * Decode LINEAR16 via the tree level helper from <pmbus.h>. For + * non Linear VOUT_MODE settings the helper returns 0; fall back + * to the historic mV pass through so existing LSCH boards keep. + */ + ret = vid_read_vout_mode(dev, &vout_mode); + if (ret) + return ret; + if ((vout_mode & PB_VOUT_MODE_MODE_MASK) == PB_VOUT_MODE_LINEAR) { + s64 uv = pmbus_reg2data_linear16(vcode, vout_mode); + + vout = (int)((uv + 500) / 1000); /* round to mV */ + } else { + vout = (int)vcode; + } return vout - board_vdd_drop_compensation(); } #endif @@ -463,11 +465,13 @@ static int set_voltage_to_IR(int i2caddress, int vdd) static int set_voltage_to_pmbus(int i2caddress, int vdd) { int ret, vdd_last, vdd_target = vdd; - int count = MAX_LOOP_WAIT_NEW_VOL, temp = 0, multiplier; + int count = MAX_LOOP_WAIT_NEW_VOL, temp = 0; + u8 vout_mode; + u16 raw; unsigned char value; /* The data to be sent with the PMBus command PAGE_PLUS_WRITE */ - u8 buffer[5] = { 0x04, PWM_CHANNEL0, PMBUS_CMD_VOUT_COMMAND, 0, 0 }; + u8 buffer[5] = { 0x04, PWM_CHANNEL0, PMBUS_VOUT_COMMAND, 0, 0 }; DEVICE_HANDLE_T dev; /* Open device handle */ @@ -475,24 +479,32 @@ static int set_voltage_to_pmbus(int i2caddress, int vdd) if (ret) return ret; - /* Scale up to the proper value for the VOUT command, little endian */ - multiplier = get_pmbus_multiplier(dev); + /* + * Encode target mV as LINEAR16 raw via the tree level helper + * from <pmbus.h>. For non Linear VOUT_MODE settings the helper + * returns 0; fall back to the historic mV pass through. A failed + * VOUT_MODE read aborts: never write a voltage code whose + * encoding could not be determined. + */ vdd += board_vdd_drop_compensation(); - if (multiplier != MV_PER_V) - vdd = DIV_ROUND_UP(vdd * multiplier, MV_PER_V); - buffer[3] = vdd & 0xFF; - buffer[4] = (vdd & 0xFF00) >> 8; + ret = vid_read_vout_mode(dev, &vout_mode); + if (ret) + return ret; + if ((vout_mode & PB_VOUT_MODE_MODE_MASK) == PB_VOUT_MODE_LINEAR) + raw = pmbus_data2reg_linear16((s64)vdd * 1000LL, vout_mode); + else + raw = (u16)vdd; + buffer[3] = raw & 0xFF; + buffer[4] = (raw & 0xFF00) >> 8; /* Check write protect state */ - ret = I2C_READ(dev, PMBUS_CMD_WRITE_PROTECT, (void *)&value, - sizeof(value)); + ret = pmbus_read_byte(dev, PMBUS_WRITE_PROTECT, &value); if (ret) goto exit; if (value != EN_WRITE_ALL_CMD) { value = EN_WRITE_ALL_CMD; - ret = I2C_WRITE(dev, PMBUS_CMD_WRITE_PROTECT, - (void *)&value, sizeof(value)); + ret = pmbus_write_byte(dev, PMBUS_WRITE_PROTECT, value); if (ret) goto exit; } diff --git a/board/nxp/common/vid.h b/board/nxp/common/vid.h index b34c080b4ba..72c1c089aa1 100644 --- a/board/nxp/common/vid.h +++ b/board/nxp/common/vid.h @@ -22,8 +22,9 @@ #define IR_VDD_STEP_UP 5 /* LTC3882 */ -#define PMBUS_CMD_WRITE_PROTECT 0x10 /* + * PMBUS_WRITE_PROTECT (10h) provided by <pmbus.h> + * * WRITE_PROTECT command supported values * 0x80: Disable all writes except WRITE_PROTECT, PAGE, * STORE_USER_ALL and MFR_EE_UNLOCK commands. @@ -51,12 +52,14 @@ #define VDD_MV_MAX 925 #endif -/* PM Bus commands code for LTC3882*/ +/* + * PM Bus commands code for LTC3882. PMBUS_PAGE / PMBUS_READ_VOUT / + * PMBUS_VOUT_MODE / PMBUS_VOUT_COMMAND are provided by <pmbus.h>. + * PMBUS_CMD_PAGE_PLUS_WRITE (05h) is the LTC3882 SMBus block write + * transaction not in <pmbus.h>'s standard subset, so keep its + * definition here. + */ #define PWM_CHANNEL0 0x0 -#define PMBUS_CMD_PAGE 0x0 -#define PMBUS_CMD_READ_VOUT 0x8B -#define PMBUS_CMD_VOUT_MODE 0x20 -#define PMBUS_CMD_VOUT_COMMAND 0x21 #define PMBUS_CMD_PAGE_PLUS_WRITE 0x05 #if defined(CONFIG_TARGET_LX2160AQDS) || defined(CONFIG_TARGET_LX2162AQDS) || \ diff --git a/board/nxp/ls1088a/ls1088a.c b/board/nxp/ls1088a/ls1088a.c index 1b477e83676..8ea76fe3760 100644 --- a/board/nxp/ls1088a/ls1088a.c +++ b/board/nxp/ls1088a/ls1088a.c @@ -7,6 +7,7 @@ #include <display_options.h> #include <env.h> #include <i2c.h> +#include <pmbus.h> #include <init.h> #include <log.h> #include <malloc.h> @@ -667,13 +668,13 @@ int get_serdes_volt(void) /* Select the PAGE 0 using PMBus commands PAGE for VDD */ #if !CONFIG_IS_ENABLED(DM_I2C) ret = i2c_write(I2C_SVDD_MONITOR_ADDR, - PMBUS_CMD_PAGE, 1, &chan, 1); + PMBUS_PAGE, 1, &chan, 1); #else struct udevice *dev; ret = i2c_get_chip_for_busnum(0, I2C_SVDD_MONITOR_ADDR, 1, &dev); if (!ret) - ret = dm_i2c_write(dev, PMBUS_CMD_PAGE, + ret = dm_i2c_write(dev, PMBUS_PAGE, &chan, 1); #endif @@ -685,9 +686,9 @@ int get_serdes_volt(void) /* Read the output voltage using PMBus command READ_VOUT */ #if !CONFIG_IS_ENABLED(DM_I2C) ret = i2c_read(I2C_SVDD_MONITOR_ADDR, - PMBUS_CMD_READ_VOUT, 1, (void *)&vcode, 2); + PMBUS_READ_VOUT, 1, (void *)&vcode, 2); #else - dm_i2c_read(dev, PMBUS_CMD_READ_VOUT, (void *)&vcode, 2); + dm_i2c_read(dev, PMBUS_READ_VOUT, (void *)&vcode, 2); #endif if (ret) { printf("VID: failed to read the voltage\n"); @@ -700,7 +701,7 @@ int get_serdes_volt(void) int set_serdes_volt(int svdd) { int ret, vdd_last; - u8 buff[5] = {0x04, PWM_CHANNEL0, PMBUS_CMD_VOUT_COMMAND, + u8 buff[5] = {0x04, PWM_CHANNEL0, PMBUS_VOUT_COMMAND, svdd & 0xFF, (svdd & 0xFF00) >> 8}; /* Write the desired voltage code to the SVDD regulator */ 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 <bus>:<addr>'. + + Per chip drivers and board files publish vendor extensions in + the 'pmbus <vendor> ...' namespace. + + See doc/develop/pmbus.rst for the full usage reference. + config CMD_REGULATOR bool "Enable Driver Model REGULATOR command" depends on DM_REGULATOR diff --git a/cmd/Makefile b/cmd/Makefile index bbbdfcc4ded..ce772e5555b 100644 --- a/cmd/Makefile +++ b/cmd/Makefile @@ -228,6 +228,7 @@ obj-$(CONFIG_CMD_AXI) += axi.o obj-$(CONFIG_CMD_PVBLOCK) += pvblock.o # Power +obj-$(CONFIG_CMD_PMBUS) += pmbus.o obj-$(CONFIG_CMD_PMIC) += pmic.o obj-$(CONFIG_CMD_REGULATOR) += regulator.o diff --git a/cmd/pmbus.c b/cmd/pmbus.c new file mode 100644 index 00000000000..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 + * <pmbus.h> + lib/pmbus.c. + * + * See doc/develop/pmbus.rst for the full usage reference. + */ + +#include <command.h> +#include <dm.h> +#include <i2c.h> +#include <log.h> +#include <pmbus.h> +#include <vsprintf.h> +#include <linux/ctype.h> +#include <power/regulator.h> + +static int parse_bus_addr(const char *s, int *bus_seq, u8 *addr) +{ + char busbuf[8]; + const char *colon; + unsigned long b, a; + size_t buslen; + + colon = strchr(s, ':'); + if (!colon) + return -EINVAL; + buslen = colon - s; + if (buslen == 0 || buslen >= sizeof(busbuf)) + return -EINVAL; + memcpy(busbuf, s, buslen); + busbuf[buslen] = '\0'; + if (strict_strtoul(busbuf, 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 <bus>:<addr>' first.\n"); + return CMD_RET_FAILURE; + } + if (pmbus_active_get_i2c(chip)) { + printf("pmbus: cannot reach i2c%d:0x%02x\n", + (*act)->bus_seq, (*act)->addr); + return CMD_RET_FAILURE; + } + return CMD_RET_SUCCESS; +} + +static void print_micro(s64 micro, const char *unit) +{ + s64 abs_milli; + + abs_milli = (micro < 0 ? -micro : micro) / 1000LL; + printf("%lld.%03lld%s", + (long long)(micro / 1000000LL), + (long long)(abs_milli % 1000LL), unit); +} + +static void print_active(const struct pmbus_active_dev *act) +{ + printf("pmbus: active i2c%d:0x%02x", act->bus_seq, act->addr); + if (act->name[0]) + printf(" rail=\"%s\"", act->name); + printf(" MFR_ID=\"%s\" MODEL=\"%s\" vendor=%s%s\n", + act->mfr_id[0] ? act->mfr_id : "?", + act->mfr_model[0] ? act->mfr_model : "?", + act->vendor[0] ? act->vendor : "(generic)", + act->info ? "" : " [no driver_info]"); +} + +static int do_dev(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + const struct pmbus_active_dev *act; + int bus_seq, ret; + u8 addr; + + if (argc < 2) { + act = pmbus_active(); + if (!act) { + printf("pmbus: no active device\n"); + return CMD_RET_SUCCESS; + } + print_active(act); + return CMD_RET_SUCCESS; + } + + if (parse_bus_addr(argv[1], &bus_seq, &addr) < 0) { + ret = pmbus_resolve_by_name(argv[1], &bus_seq, &addr); + if (ret) { + printf("pmbus: '%s' is neither <bus>:<addr> nor a known regulator-name (%d)\n", + argv[1], ret); + return CMD_RET_FAILURE; + } + } + ret = pmbus_set_active(bus_seq, addr); + if (ret) { + printf("pmbus: cannot select i2c%d:0x%02x (%d)\n", + bus_seq, addr, ret); + return CMD_RET_FAILURE; + } + act = pmbus_active(); + if (act) + print_active(act); + return CMD_RET_SUCCESS; +} + +static int do_list(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + struct udevice *dev; + struct uclass *uc; + int ret, n = 0; + + if (!IS_ENABLED(CONFIG_DM_REGULATOR)) { + printf("pmbus: CONFIG_DM_REGULATOR not enabled in this build. Use 'pmbus dev <bus>:<addr>'.\n"); + return CMD_RET_SUCCESS; + } + + ret = uclass_get(UCLASS_REGULATOR, &uc); + if (ret) { + printf("pmbus: UCLASS_REGULATOR not available\n"); + return CMD_RET_SUCCESS; + } + uclass_foreach_dev(dev, uc) { + struct dm_regulator_uclass_plat *up = dev_get_uclass_plat(dev); + struct udevice *parent = dev_get_parent(dev); + const char *rname = (up && up->name) ? up->name : ""; + const char *drv = (dev->driver && dev->driver->name) + ? dev->driver->name : "?"; + int bus_seq = -1; + int addr = -1; + + if (parent && device_get_uclass_id(parent) == UCLASS_I2C) { + bus_seq = dev_seq(parent); + addr = dev_read_addr(dev); + } + + if (n == 0) + printf("UCLASS_REGULATOR devices (no PMBus filter):\n"); + if (bus_seq >= 0 && addr >= 0) { + printf(" i2c%d:0x%02x rail=\"%s\" node=%s driver=%s\n", + bus_seq, addr, rname, dev->name, drv); + } else { + printf(" (non-I2C) rail=\"%s\" node=%s driver=%s\n", + rname, dev->name, drv); + } + n++; + } + if (!n) + printf("pmbus: no UCLASS_REGULATOR devices bound. Use 'pmbus dev <bus>:<addr>' to select a chip directly.\n"); + return CMD_RET_SUCCESS; +} + +static int do_info(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + static const char * const cls_names[PSC_NUM_CLASSES] = { + "VOLTAGE_IN", "VOLTAGE_OUT", "CURRENT_IN", "CURRENT_OUT", + "POWER", "TEMPERATURE", + }; + static const char * const fmt_names[] = { + "LINEAR", "IEEE754", "DIRECT", "VID", + }; + const struct pmbus_active_dev *act; + struct udevice *chip; + int rc, c, rrev; + u8 rev = 0; + + rc = require_active(&chip, &act); + if (rc) + return rc; + + rrev = pmbus_read_byte(chip, PMBUS_REVISION, &rev); + + printf("pmbus device i2c%d:0x%02x\n", act->bus_seq, act->addr); + if (act->name[0]) + printf(" regulator-name: \"%s\"\n", act->name); + printf(" MFR_ID : \"%s\"\n", act->mfr_id[0] ? act->mfr_id : "?"); + printf(" MFR_MODEL : \"%s\"\n", act->mfr_model[0] ? act->mfr_model : "?"); + + /* + * MFR_REVISION may encodes the revision as a non printable byte + * (BCD nibbles, packed major / minor, etc.). Show both the + * printable form and the raw bytes the chip returned. + */ + { + u8 raw[PMBUS_MFR_STRING_MAX]; + int len, i; + + if (dm_i2c_read(chip, PMBUS_MFR_REVISION, raw, 1) || + raw[0] < 1 || raw[0] > sizeof(raw) - 1 || + dm_i2c_read(chip, PMBUS_MFR_REVISION, raw, raw[0] + 1)) { + printf(" MFR_REVISION : \"%s\"\n", + act->mfr_revision[0] ? act->mfr_revision : "?"); + } else { + len = raw[0]; + printf(" MFR_REVISION : \"%s\" raw=0x", + act->mfr_revision[0] ? act->mfr_revision : "?"); + for (i = 1; i <= len; i++) + printf("%02x", raw[i]); + printf("\n"); + } + } + + if (rrev) + printf(" PMBUS_REVISION: <read failed (%d)>\n", rrev); + else + printf(" PMBUS_REVISION: 0x%02x (%s)\n", rev, + rev == PMBUS_REV_13 ? "PMBus 1.3" : + rev == PMBUS_REV_12 ? "PMBus 1.2" : + rev == PMBUS_REV_11 ? "PMBus 1.1" : + rev == PMBUS_REV_10 ? "PMBus 1.0" : "unknown"); + printf(" vendor : %s\n", act->vendor[0] ? act->vendor : "(none)"); + + if (!act->info) { + printf(" driver_info : not registered (decoders fall back to LINEAR16 / LINEAR11)\n"); + return CMD_RET_SUCCESS; + } + printf(" driver_info : pages=%d\n", act->info->pages); + for (c = 0; c < PSC_NUM_CLASSES; c++) { + printf(" [%-12s] format=%s", + cls_names[c], fmt_names[act->info->format[c]]); + if (act->info->format[c] == pmbus_fmt_direct) + printf(", m=%d, b=%d, R=%d", + act->info->m[c], act->info->b[c], act->info->R[c]); + printf("\n"); + } + return CMD_RET_SUCCESS; +} + +static int do_telemetry(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + const struct pmbus_active_dev *act; + struct udevice *chip; + int rc; + + rc = require_active(&chip, &act); + if (rc) + return rc; + + printf("pmbus telemetry @ i2c%d:0x%02x\n", act->bus_seq, act->addr); + pmbus_print_telemetry(chip); + return CMD_RET_SUCCESS; +} + +static int do_status(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + const struct pmbus_active_dev *act; + struct udevice *chip; + u16 word = 0; + u8 b; + int rc; + + rc = require_active(&chip, &act); + if (rc) + return rc; + + if (pmbus_read_word(chip, PMBUS_STATUS_WORD, &word)) { + printf("pmbus: STATUS_WORD read failed\n"); + return CMD_RET_FAILURE; + } + + const struct pmbus_status_override *ovr = + act->info ? act->info->status_overrides : NULL; + + printf("pmbus status @ i2c%d:0x%02x\n", act->bus_seq, act->addr); + printf(" STATUS_WORD (79h) = 0x%04x [", word); + pmbus_print_status_bits(PMBUS_STATUS_WORD, word, + pmbus_status_word_bits, ovr); + printf("]\n"); + + if (pmbus_read_byte(chip, PMBUS_STATUS_VOUT, &b) == 0) { + printf(" STATUS_VOUT (7Ah) = 0x%02x [", b); + pmbus_print_status_bits(PMBUS_STATUS_VOUT, b, + pmbus_status_vout_bits, ovr); + printf("]\n"); + } + if (pmbus_read_byte(chip, PMBUS_STATUS_IOUT, &b) == 0) { + printf(" STATUS_IOUT (7Bh) = 0x%02x [", b); + pmbus_print_status_bits(PMBUS_STATUS_IOUT, b, + pmbus_status_iout_bits, ovr); + printf("]\n"); + } + if (pmbus_read_byte(chip, PMBUS_STATUS_INPUT, &b) == 0) { + printf(" STATUS_INPUT (7Ch) = 0x%02x [", b); + pmbus_print_status_bits(PMBUS_STATUS_INPUT, b, + pmbus_status_input_bits, ovr); + printf("]\n"); + } + if (pmbus_read_byte(chip, PMBUS_STATUS_TEMPERATURE, &b) == 0) { + printf(" STATUS_TEMP (7Dh) = 0x%02x [", b); + pmbus_print_status_bits(PMBUS_STATUS_TEMPERATURE, b, + pmbus_status_temp_bits, ovr); + printf("]\n"); + } + if (pmbus_read_byte(chip, PMBUS_STATUS_CML, &b) == 0) { + printf(" STATUS_CML (7Eh) = 0x%02x [", b); + pmbus_print_status_bits(PMBUS_STATUS_CML, b, + pmbus_status_cml_bits, ovr); + printf("]\n"); + } + + return CMD_RET_SUCCESS; +} + +static int do_dump(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + const struct pmbus_active_dev *act; + struct udevice *chip; + unsigned int i; + int rc; + + rc = require_active(&chip, &act); + if (rc) + return rc; + + printf("pmbus dump @ i2c%d:0x%02x (registers known to <pmbus.h>)\n", + act->bus_seq, act->addr); + for (i = 0; i < ARRAY_SIZE(pmbus_reg_syms); i++) { + u8 reg = pmbus_reg_syms[i].reg; + u8 b = 0; + u16 w = 0; + + switch (reg) { + case PMBUS_PAGE: + case PMBUS_OPERATION: + case PMBUS_ON_OFF_CONFIG: + case PMBUS_WRITE_PROTECT: + case PMBUS_CAPABILITY: + case PMBUS_VOUT_MODE: + case PMBUS_STATUS_BYTE: + case PMBUS_STATUS_VOUT: + case PMBUS_STATUS_IOUT: + case PMBUS_STATUS_INPUT: + case PMBUS_STATUS_TEMPERATURE: + case PMBUS_STATUS_CML: + case PMBUS_REVISION: + if (pmbus_read_byte(chip, reg, &b) == 0) + printf(" %02xh %-15s b=0x%02x\n", + reg, pmbus_reg_syms[i].name, b); + break; + case PMBUS_MFR_ID: + case PMBUS_MFR_MODEL: + case PMBUS_MFR_REVISION: { + char s[PMBUS_MFR_STRING_MAX]; + + if (pmbus_read_string(chip, reg, s, sizeof(s), + act->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 <name>' or a\n"); + printf(" board hook (boot snapshot) and re run 'pmbus help'.\n"); + return CMD_RET_SUCCESS; + } + + printf("Registered pmbus vendor extensions (%u):\n\n", n); + for (i = 0; i < n; i++) { + const struct pmbus_vendor_op *op = pmbus_vendor_at(i); + + if (!op) + continue; + printf("[vendor: %s]\n", op->vendor); + if (op->help) + printf("%s", op->help); + printf("\n"); + } + return CMD_RET_SUCCESS; +} + +static struct cmd_tbl pmbus_subcmd[] = { + U_BOOT_CMD_MKENT(dev, 2, 1, do_dev, "", ""), + U_BOOT_CMD_MKENT(list, 1, 1, do_list, "", ""), + U_BOOT_CMD_MKENT(info, 1, 1, do_info, "", ""), + U_BOOT_CMD_MKENT(telemetry, 1, 1, do_telemetry, "", ""), + U_BOOT_CMD_MKENT(status, 1, 1, do_status, "", ""), + U_BOOT_CMD_MKENT(dump, 1, 1, do_dump, "", ""), + U_BOOT_CMD_MKENT(read, 3, 1, do_read, "", ""), + U_BOOT_CMD_MKENT(write, 4, 1, do_write, "", ""), + U_BOOT_CMD_MKENT(clear, 2, 1, do_clear, "", ""), + U_BOOT_CMD_MKENT(vout, 2, 1, do_vout, "", ""), + U_BOOT_CMD_MKENT(scan, 2, 1, do_scan, "", ""), + U_BOOT_CMD_MKENT(help, 1, 1, do_help, "", ""), +}; + +static int do_pmbus(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + const struct pmbus_vendor_op *vop; + struct cmd_tbl *cmd; + + if (argc < 2) + return CMD_RET_USAGE; + + argc--; + argv++; + + cmd = find_cmd_tbl(argv[0], pmbus_subcmd, ARRAY_SIZE(pmbus_subcmd)); + if (cmd) { + if (argc > cmd->maxargs) + return CMD_RET_USAGE; + return cmd->cmd(cmdtp, flag, argc, argv); + } + + /* Vendor namespace dispatch */ + vop = pmbus_lookup_vendor(argv[0]); + if (vop) + return vop->handler(cmdtp, flag, argc, argv); + + printf("pmbus: unknown subcommand '%s'\n", argv[0]); + return CMD_RET_USAGE; +} + +U_BOOT_CMD(pmbus, CONFIG_SYS_MAXARGS, 1, do_pmbus, + "PMBus 1.x device interrogation and control", + "list - list UCLASS_REGULATOR devices (DM bound)\n" + "pmbus dev [<bus>:<addr>|<name>] - show / select active PMBus device\n" + " (<bus> decimal, <addr>/<reg>/<val> 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 <reg> [b|w|s] - raw read (b=byte, w=word, s=string)\n" + "pmbus write <reg> <val> [b|w] - raw write\n" + "pmbus clear [faults] - issue CLEAR_FAULTS (03h)\n" + "pmbus vout [<uV>] - read / set VOUT_COMMAND (microvolts)\n" + "pmbus scan [<bus>] - PMBus aware probe of one or all I2C buses\n" + "pmbus help - list registered vendor extensions\n" + "\n" + "Vendor extensions (pmbus <vendor> ...) are registered by per chip\n" + "drivers at probe time. Run 'pmbus help' after a chip is probed to\n" + "see the available subcommands.\n" +); diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig index 512dbd6a0c6..79f46317e45 100644 --- a/configs/sandbox_defconfig +++ b/configs/sandbox_defconfig @@ -138,6 +138,7 @@ CONFIG_CMD_PSTORE=y CONFIG_CMD_PSTORE_MEM_ADDR=0x3000000 CONFIG_CMD_BOOTSTAGE=y CONFIG_CMD_PMIC=y +CONFIG_CMD_PMBUS=y CONFIG_CMD_REGULATOR=y CONFIG_CMD_AES=y CONFIG_CMD_TPM=y @@ -312,6 +313,9 @@ CONFIG_REGULATOR_S5M8767=y CONFIG_DM_REGULATOR_SANDBOX=y CONFIG_REGULATOR_TPS65090=y CONFIG_DM_REGULATOR_SCMI=y +CONFIG_DM_REGULATOR_PMBUS_HELPER=y +CONFIG_DM_REGULATOR_PMBUS_GENERIC=y +CONFIG_SANDBOX_PMBUS=y CONFIG_DM_PWM=y CONFIG_PWM_CROS_EC=y CONFIG_PWM_SANDBOX=y @@ -341,6 +345,7 @@ CONFIG_SYSINFO=y CONFIG_SYSINFO_SANDBOX=y CONFIG_SYSINFO_GPIO=y CONFIG_DM_THERMAL=y +CONFIG_PMBUS_THERMAL=y CONFIG_TIMER_EARLY=y CONFIG_SANDBOX_TIMER=y CONFIG_USB=y @@ -381,6 +386,7 @@ CONFIG_FS_EXFAT=y CONFIG_FS_CRAMFS=y CONFIG_ADDR_MAP=y CONFIG_PANIC_HANG=y +CONFIG_PMBUS=y CONFIG_CMD_DHRYSTONE=y CONFIG_MBEDTLS_LIB=y CONFIG_HKDF_MBEDTLS=y 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 <name>; pmbus telemetry``, + ``regulator dev <name>; regulator value``). +* Decode chip alerts when a rail trips an over/under voltage, + over current, or thermal threshold; so a boot log shows why the + previous boot failed, before the kernel even comes up. +* Optionally trim a critical rail (typically the SoC core) before + the kernel takes over; "set the voltage prior to a kernel boot + to better protect the board". This is the existing + ``board/nxp/common/vid.c`` AVS path and any future per board + speed binning trim. + +Out of scope, by design: + +* No periodic polling. No worker thread. No background updates. +* No sysfs / procfs / userspace surface. U-Boot has none. +* No fan speed control loop. The kernel runs that. +* No long tail of virtual sensor registers (``PMBUS_VIRT_*``). +* No sensor caching / update timestamps. + +If you find yourself wanting any of those, the answer is "wait until +Linux comes up". Keep U-Boot's PMBus surface minimal. + +Architecture overview +--------------------- + +The framework is split into four layers (layer 3 comes in two +flavours, 3a and 3b), + +:: + + +----------------------------------------+ + | Layer 1: include/pmbus.h | + | Standard PMBus 1.x command codes, | + | numeric format enum, sensor class | + | enum, struct pmbus_driver_info, | + | decoder + transport prototypes, | + | STATUS_WORD bit names. | + +----------------------------------------+ + | Layer 2: lib/pmbus.c | + | Format decoders (LINEAR11/LINEAR16/| + | DIRECT) and encoder (LINEAR16), | + | two stage SMBus block read helper, | + | STATUS_*-bit print tables, generic | + | dispatcher pmbus_reg2data(). | + +----------------------------------------+ + | Layer 3a: drivers/power/regulator/ | + | <chip>.c | + | UCLASS_REGULATOR per chip drivers | + | ; one struct pmbus_driver_info | + | plus regulator set_value/get_value | + | ops. Optional: per chip identify() | + | hook to refine format from the | + | chip's own VOUT_MODE. | + +----------------------------------------+ + | Layer 3b: drivers/power/regulator/ | + | pmbus_generic.c | + | Catch all driver matching | + | compatible = "pmbus". | + | Auto detects format via VOUT_MODE | + | and PMBUS_QUERY where supported. | + | Use for compliant chips with no | + | per chip driver yet; ship | + | telemetry today, write a per chip | + | driver later only if quirks demand | + | it. | + +----------------------------------------+ + | Layer 4: board/<vendor>/<board>/ | + | <chip>_diag.c | + | Diagnostic commands only: | + | <chip>_info / <chip>_raw | + | Reads via regulator_get_value() | + | and lib/pmbus.c helpers. LINEAR / | + | DIRECT math NOT here. | + +----------------------------------------+ + +Generic vs. board specific separation rule. Layer 1, 2, and 3 +files are tree level and platform agnostic. Their comments may +reference only: + +* the PMBus 1.x specification, and +* chip manufacturer datasheets. + +Never a specific board, SoC, or product. Board-specific quirks +(a particular bus number, a particular slave address, a particular +PCB feedback divider, board local design notes) live exclusively in +``board/<vendor>/<board>/`` files. + +CLI commands +------------ + +The framework publishes one top level command, ``pmbus``, plus a +vendor namespace dispatcher so per chip code can register chip +specific extensions without touching the framework. + +Active device model +~~~~~~~~~~~~~~~~~~~ + +``pmbus`` mirrors the ``regulator`` command: select an active device +once, then operate on it across subcommands. The active device is +selected by I2C bus (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 [<bus>:<addr>] show / select active PMBus device + pmbus info identification banner + driver_info + pmbus telemetry decoded VIN, VOUT, IIN, IOUT, TEMP + pmbus status decode every STATUS_* register + pmbus dump hex dump of every standard register + pmbus read <reg> [b|w|s] raw read (b=byte, w=word, s=string) + pmbus write <reg> <val> [b|w] raw write + pmbus clear [faults] issue CLEAR_FAULTS (03h) + pmbus vout [<uV>] read or set VOUT_COMMAND (microvolts) + pmbus scan [<bus>] PMBus aware probe of one or all I2C buses + +The ``<reg>`` argument accepts either a hexadecimal address +(``88``, ``0x`` optional) or a symbolic name (``READ_VIN``, +``VOUT_MODE``, ``MFR_ID``); symbolic names win when both parsed. +``<val>`` 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 <vendor> ...`` namespace by calling +``pmbus_register_vendor_handler()`` at init time. The framework +dispatches ``pmbus mps last``, ``pmbus mps clear last``, and +``pmbus mps clear force`` to the MPS handler when the active +device matches the ``mps`` vendor. Additional vendor handlers for +``lltc``, ``renesas``, etc. land alongside the per chip drivers +that need them. + +Relationship to ``vdd_override`` / ``vdd_read`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The NXP Layerscape ``vdd_override <mV>`` and ``vdd_read`` commands +remain available in their original form for compatibility with +existing AVS production scripts. The new ``pmbus vout`` and +``pmbus vout <uV>`` subcommands cover the read and single shot +write paths against the same chips, but do not implement +``vdd_override``'s full sequence (board drop compensation, fuse +target derivation, multi step convergence loop, atomic +``PAGE_PLUS_WRITE`` block transaction, ``WRITE_PROTECT`` dance). +For interactive bring up ``pmbus vout`` is sufficient; for +production AVS, ``vdd_override`` stays canonical. + +Lifecycle: from board boot to Linux handoff +------------------------------------------- + +The PMBus framework spans the entire U-Boot lifecycle. This section +walks the boot timeline phase by phase, showing when each piece +comes online and how the regulator uclass and the ``pmbus`` CLI +converge on the same chip. + +Timeline overview +~~~~~~~~~~~~~~~~~ + +:: + + Phase 0 chip power on chip ramps to NVM default VOUT + Phase 1 boot ROM / SPL / TF-A PMBus typically untouched + Phase 2 U-Boot relocation, DM init regulators bound, not probed + Phase 3 first regulator probe chip driver runs, framework lights up + Phase 4 board hooks / boot scripts snapshot, AVS trim, gating + Phase 5 Linux handoff DT passed, chip state preserved + Phase 6 Linux runtime kernel pmbus driver takes over + +Phase 0: chip power on +~~~~~~~~~~~~~~~~~~~~~~ + +When the regulator chip receives its input voltage, it ramps its +output to the VOUT default programmed into its NVM at factory +provisioning. PMBus is silent: no software runs anywhere on the +SoC yet. + +Phase 1: pre U-Boot stages +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Boot ROMs, secondary boot loaders (SPL, ARM TF-A BL2 / BL31) +typically do not touch PMBus. They focus on PLLs, DDR PHY init, +and bringing up enough hardware to load the next stage. Some +platforms have a pre U-Boot AVS path in board specific TF-A +code that writes ``VOUT_COMMAND`` from a fuse derived target; +that path is independent of the U-Boot framework described here. + +Phase 2: U-Boot relocation and DM init +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After relocation, U-Boot binds device tree nodes to drivers but +does not probe them. UCLASS_REGULATOR devices for PMBus chips +are bound (driver and DT match resolved) but the ``.probe`` +callback has not run yet. + +Framework state at this point: + +* chip match registry: empty +* vendor handler registry: empty +* active device: none +* regulator uclass: devices bound, none probed + +Phase 3: lazy regulator probe +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The first caller into the regulator uclass for a given chip +triggers the chip driver's ``.probe``. Typical first callers: + +* a board ``EVENT_SPY`` at ``EVT_LAST_STAGE_INIT`` (boot snapshot) +* a U-Boot script: ``regulator dev <name>; regulator value`` +* the ``pmbus dev <name>`` CLI command (resolves to the regulator) +* another DT consumer with a ``regulator-supplies`` reference + +The probe chain looks like this:: + + <chip>_probe(dev) + pmbus_regulator_probe_common(dev, &<chip>_info, page) + dev_read_addr(dev) -> reg = <addr> + i2c_get_chip(dev->parent, addr) -> I2C chip handle + priv->i2c_dev = handle + priv->info = &<chip>_info + priv->page = page + (page > 0) write PMBUS_PAGE + <chip>_identify_vout(priv->i2c_dev) [optional] + read VOUT_MODE; refine info->format[PSC_VOLTAGE_OUT] + pmbus_regulator_apply_voltage_scale(dev, fb_div) [optional] + write PMBUS_VOUT_SCALE_LOOP if DT property set + pmbus_register_chip(&<chip>_match) [idempotent] + pmbus_register_vendor_handler(&<chip>_op) [idempotent] + +Once probed, three independent surfaces are functional against +the same chip: + +* the regulator uclass API (``regulator_get_value``, + ``regulator_set_value``, ``regulator_get_enable``, + ``regulator_set_enable``) +* the ``pmbus`` CLI (chip is reachable by name through + ``pmbus_resolve_by_name()``, by raw ``<bus>:<addr>`` through + ``pmbus_set_active()``) +* the chip's vendor extension subcommands (``pmbus <vendor> ...``) + +Phase 4: board hooks and boot scripts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Boards hook the boot flow at well known points to drive board +specific PMBus behaviour. The framework prescribes none of these; +they are conventions: + +* boot time rail snapshot. An ``EVENT_SPY`` at + ``EVT_LAST_STAGE_INIT`` reads telemetry through the regulator + uclass and prints a one shot summary to the console. Useful + for operator visibility on serial during bring up. + +* pre kernel rail trim (AVS). A board hook in + ``board_late_init`` or a custom event spy reads a fuse derived + target voltage and calls ``regulator_set_value_force()`` to + trim the SoC core rail before kernel handoff. + +* Linux handoff gate. A bootcmd reads the rail voltage + through the regulator command and refuses to boot Linux if + the rail is outside the expected range. + +Phase 5: Linux handoff +~~~~~~~~~~~~~~~~~~~~~~ + +When U-Boot transfers control to Linux, it passes the device +tree (potentially patched). The DT compatible strings for PMBus +regulators must match those in the upstream kernel binding so +the kernel's ``drivers/hwmon/pmbus/<chip>.c`` picks them up. +Property names are shared with the kernel binding +(``regulator-name``, ``regulator-min-microvolt``, +``mps,vout-fb-divider-ratio-permille``, etc.); see "DT alignment +with Linux" below. + +The chip itself is left in the state U-Boot wrote it to. If +U-Boot trimmed VOUT, the chip stays at the trimmed voltage +through handoff. ``CLEAR_FAULTS`` state is preserved unless an +operator explicitly issued one. + +Phase 6: Linux runtime +~~~~~~~~~~~~~~~~~~~~~~ + +Linux's ``drivers/hwmon/pmbus/pmbus_core.c`` probes the chip, +exposes telemetry under ``/sys/class/hwmon``, and takes over +runtime voltage management through its regulator subsystem. +The hwmon framework polls periodically; U-Boot does not. + +Operation paths through the regulator uclass +-------------------------------------------- + +After the first probe completes, calls into the regulator uclass +for a PMBus chip flow through the shared helper. + +Read VOUT:: + + regulator_get_value(dev) + -> dm_regulator_ops->get_value + -> pmbus_regulator_get_value(dev) + pmbus_regulator_select_page(priv) + pmbus_read_byte(priv->i2c_dev, VOUT_MODE, &mode) + pmbus_read_word(priv->i2c_dev, READ_VOUT, &raw) + pmbus_reg2data(priv->info, PSC_VOLTAGE_OUT, raw, mode) + -> reg2data_linear16 (mode = 0) + -> reg2data_direct (chip configured for DIRECT) + return engineering value (microvolts) + +Write VOUT:: + + regulator_set_value(dev, uV) + -> dm_regulator_ops->set_value + -> pmbus_regulator_set_value(dev, uV) + pmbus_regulator_select_page(priv) + pmbus_read_byte(VOUT_MODE) + check (mode == LINEAR) [LINEAR16 only today] + raw = pmbus_data2reg_linear16(uV, mode) + dm_i2c_write(VOUT_COMMAND, raw) + +Read / write enable bit:: + + regulator_get_enable(dev) + -> pmbus_regulator_get_enable(dev) + pmbus_read_byte(OPERATION) & PB_OPERATION_ON + + regulator_set_enable(dev, on) + -> pmbus_regulator_set_enable(dev, on) + read OPERATION, set or clear PB_OPERATION_ON, write back + +Bus traffic per call: + +* ``get_value`` : 1 byte read (VOUT_MODE) + 1 word read (READ_VOUT) + + 1 byte write (PAGE) when ``page > 0`` +* ``set_value`` : 1 byte read (VOUT_MODE) + 1 word write (VOUT_COMMAND) + + 1 byte write (PAGE) when ``page > 0`` +* ``get_enable`` : 1 byte read (OPERATION) +* ``set_enable`` : 1 byte read (OPERATION) + 1 byte write (OPERATION) + +Common board hook patterns +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Boot time rail snapshot:: + + static int my_board_pmbus_snapshot(void) + { + struct udevice *reg; + + if (regulator_get_by_platname("MY_RAIL", ®)) + return 0; + printf("MY_RAIL: VOUT = %d uV, enabled = %d\n", + regulator_get_value(reg), + regulator_get_enable(reg)); + return 0; + } + EVENT_SPY_SIMPLE(EVT_LAST_STAGE_INIT, my_board_pmbus_snapshot); + +The first call to ``regulator_get_value()`` triggers the chip +driver's ``.probe``, which seeds the chip match and vendor +extension registries. Subsequent ``pmbus`` CLI commands work +without further setup. + +Pre kernel rail trim (AVS):: + + int board_late_init(void) + { + struct udevice *reg; + int target_uV = compute_avs_target(); + + if (regulator_get_by_platname("VDD_CORE", ®)) + return 0; + return regulator_set_value_force(reg, target_uV); + } + +Use ``regulator_set_value_force()`` when the target may sit +outside the DT declared ``regulator-min-microvolt`` / +``regulator-max-microvolt`` range; force bypasses the bounds +check. + +Adding a new PMBus chip from scratch +------------------------------------ + +Use this path when the chip has no Linux driver yet, or when you want +to validate the U-Boot port against the datasheet alone. + +1. Confirm PMBus 1.x compliance level. Locate in the chip + datasheet: + + which PMBus standard command codes the chip implements + (``READ_VIN``, ``READ_VOUT``, ``STATUS_WORD``, ``MFR_ID`` ...), + which numeric format(s) it uses for VOUT (LINEAR16 with the + exponent in ``VOUT_MODE``, DIRECT with chip specific m/b/R, or + VID with one of the documented VRM tables), + which numeric format it uses for VIN, IIN, IOUT, TEMPERATURE + (most commonly LINEAR11; some MPS / MPS derivative chips use + DIRECT instead), + how many output rails it exposes (single page parts vs. + multi rail PMBus pages). + +2. Declare a ``struct pmbus_driver_info``. Wire each sensor + class to one ``enum pmbus_data_format``, plus the m/b/R triple if + the format is DIRECT:: + + static struct pmbus_driver_info chipname_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = pmbus_fmt_direct, + .format[PSC_VOLTAGE_OUT] = pmbus_fmt_linear, + .format[PSC_CURRENT_OUT] = pmbus_fmt_direct, + .format[PSC_TEMPERATURE] = pmbus_fmt_direct, + .m[PSC_VOLTAGE_IN] = 4, .R[PSC_VOLTAGE_IN] = 1, + .m[PSC_CURRENT_OUT] = 16, .R[PSC_CURRENT_OUT] = 0, + .m[PSC_TEMPERATURE] = 1, .R[PSC_TEMPERATURE] = 0, + }; + +3. Bind to a DT compatible. Use the lowercase ``vendor,chip`` + tuple Linux uses (see "DT alignment with Linux" below). Add the + driver under ``drivers/power/regulator/`` matching the existing + skeleton (``fan53555.c``, ``pca9450.c``). + +4. Rely on the DT binding from the Linux kernel which is imported into + U-Boot under ``dts/upstream/Bindings/`` (for PMBus chips, + ``dts/upstream/Bindings/hwmon/pmbus/``). + +5. Smoke test. With the chip wired up in DT:: + + => regulator dev <name> + => regulator value + => regulator info + + Numbers should match the bench measurement to within the chip's + advertised LSB. + +Porting an existing Linux PMBus driver to U-Boot +------------------------------------------------ + +When the chip already has a ``linux/drivers/hwmon/pmbus/<chip>.c``, +that driver is the authoritative reference for format, coefficients, +and quirks. Take what carries; leave what does not. + +What carries verbatim +~~~~~~~~~~~~~~~~~~~~~ + +* Numeric formats (``format[PSC_*]``). +* DIRECT coefficients (``m[]``, ``b[]``, ``R[]``). +* Per page count and per page functionality bits (``pages``, + ``func[]``). +* VOUT_MODE driven per chip identify hook (e.g. MPQ8785's + switch between LINEAR16 and VID coerced DIRECT m=64 R=1). +* Vendor register addresses for chip specific quirks (fault + history, scale-loop, page mapping). + +What does not carry +~~~~~~~~~~~~~~~~~~~~~~~ + +* ``hwmon_device_register()`` and the attribute groups it consumes. +* ``struct pmbus_data`` / ``update_lock`` / ``last_updated`` + U-Boot has no caching layer. +* ALERT# IRQ wiring; U-Boot is single threaded boot code. +* Fan control hooks (``read_fan_*``, ``set_pwm_*``). +* Virtual register handling (``PMBUS_VIRT_READ_VIN_*`` etc.); those + are entirely a hwmon publication aid. +* ``module_i2c_driver(...)`` and ``MODULE_*`` macros; U-Boot uses + ``U_BOOT_DRIVER(...)``. + +Worked example: porting MPQ8785 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Linux's ``drivers/hwmon/pmbus/mpq8785.c`` is 193 LOC; the U-Boot +equivalent is ~150 LOC. + +The ``mpq8785_info`` struct transcribes verbatim:: + + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, .m[PSC_VOLTAGE_IN] = 4, .R[PSC_VOLTAGE_IN] = 1, + .format[PSC_CURRENT_OUT] = direct, .m[PSC_CURRENT_OUT] = 16, .R[PSC_CURRENT_OUT] = 0, + .format[PSC_TEMPERATURE] = direct, .m[PSC_TEMPERATURE] = 1, .R[PSC_TEMPERATURE] = 0, + +The VOUT format is decided at probe time from VOUT_MODE bits[7:5] : +mode 0 means LINEAR16, mode 1 or 2 means DIRECT m=64 R=1 (the chip's +"VID" mode is coerced to DIRECT by the driver). Translate Linux's +``mpq8785_identify()`` 1:1. + +The per chip quirks that carry over: + +* MPS NVM string byte order: chip stores ``S P M`` for the human + string ``MPS``. ``pmbus_read_string()`` accepts a ``reverse_bytes`` + flag for this case. +* ``mps,vout-fb-divider-ratio-permille`` DT property maps to + ``VOUT_SCALE_LOOP`` write at probe time. + +The quirks that do not carry over: + +* The ``PMBUS_VIRT_*`` virtual sensor wiring. Drop entirely. +* The ``hwmon_chip_info`` attribute group registration. +* The ``MODULE_AUTHOR`` / ``MODULE_LICENSE`` declarations. + +Using the generic ``compatible = "pmbus"`` driver +------------------------------------------------- + +When a board carries a PMBus chip without a per chip U-Boot driver, +the catch all ``drivers/power/regulator/pmbus_generic.c`` (Layer 3b) +binds against ``compatible = "pmbus"``. It auto detects format via +``VOUT_MODE`` and ``PMBUS_QUERY`` (where the chip supports it) and +provides telemetry + voltage set/get against the standard PMBus 1.x +subset. + +Decision tree: + +1. Try the generic driver first. Add the regulator node to the + board DT with ``compatible = "pmbus"`` plus the standard + regulator properties. Boot, run ``regulator value``, compare + against bench measurement. +2. Switch to a per chip driver only when the generic one is + wrong: telemetry shows wrong values (chip uses DIRECT with + non default coefficients), an alert can't be decoded (chip has + vendor specific status bits), AVS is needed (the boot path has + to actively trim VOUT before kernel handoff), or the chip has an + ADDR-pin auto promotion / VID coercion / vendor register quirk. + +DT alignment with Linux +----------------------- + +The same ``.dts`` file should work under both U-Boot (BL33) and Linux +post handoff. To make that possible: + +* Reuse the upstream Linux compatible for every PMBus chip. Look + in ``linux/Documentation/devicetree/bindings/hwmon/pmbus/`` and + ``linux/Documentation/devicetree/bindings/regulator/``. The + ``<vendor>,<chip>`` tuple from the kernel binding goes into U-Boot's + ``of_match_table`` unchanged. +* Reuse Linux property names verbatim: ``regulator-name``, + ``regulator-min-microvolt``, ``regulator-max-microvolt``, + ``regulator-boot-on``, ``regulator-always-on``, + ``mps,vout-fb-divider-ratio-permille``, etc. +* The DT binding is the kernel's, imported under + ``dts/upstream/Bindings/`` (PMBus chips live in + ``dts/upstream/Bindings/hwmon/pmbus/``). + +Multi rail/multi page chips (e.g. ISL68137 with seven outputs) +declare each rail as a child regulator node with ``reg = <page>``; +each child binds as a UCLASS_REGULATOR with that PMBus PAGE setting +applied at every read/write. + +Common pitfalls +--------------- + +These have all bitten contributors during nbxv3 bring up; record them +here so the next port doesn't repeat them. + +* VOUT_MODE/DIRECT format confusion. Most generic PMBus call + sites assume LINEAR16. Several MPS chips report VOUT in DIRECT + format with chip specific m/b/R after a single VOUT_MODE read, + the same chip read at the same address produces different + numbers depending on the format the driver applies. Always read + ``VOUT_MODE`` at probe time and switch the decoder accordingly. + Linux's per chip ``identify()`` callbacks document the exact + rules; copy them rather than guessing. +* SMBus block read protocol. Some I2C controllers strict check + block read transactions: the master must read the length byte + first, then reissue the read for the payload. Over reading a + fixed length and ignoring the length byte works on lenient + controllers but errors on strict ones. ``pmbus_read_string()`` + does the two stage read; use it. +* I2C bus number stability. ``uclass_get_device_by_seq()`` + uses the DT alias index (``i2c0`` -> ``UCLASS_I2C`` seq 0) when + aliases are declared, otherwise falls back to probe order which + varies with which controllers are enabled in the defconfig. + Always declare DT aliases for I2C buses you reference by index. +* ADDR-pin auto addressessing. Some chips (notably MPS parts) decode + their PMBus 7-bit address from an external resistor divider on + ADDR_VBOOT. The "default" address in the datasheet is the + factory fused slot; a board with a different divider or a die + with a different revision can land in another window. If the + driver hardcodes the default and the board side scan finds the + chip in another window, auto promote the working address rather + than failing the probe. +* MFR string byte order. Most PMBus chips return ``MFR_ID`` + characters in human order. Some MPS personalities reverse them. + Pass ``reverse_bytes=true`` to ``pmbus_read_string()`` for those; + spec compliant chips pass false. + +References +---------- + +* Linux PMBus core: ``linux/drivers/hwmon/pmbus/pmbus_core.c``, + decoder reference; ignore the hwmon publication and caching layers. +* Linux PMBus header: ``linux/drivers/hwmon/pmbus/pmbus.h``; API + surface reference; many constants and the ``struct + pmbus_driver_info`` shape are mirrored verbatim into U-Boot's + ``include/pmbus.h``. +* Linux DT bindings: + ``linux/Documentation/devicetree/bindings/hwmon/pmbus/``. diff --git a/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/mmc/dw_mmc.c b/drivers/mmc/dw_mmc.c index d9c05b223d5..c2424d7875c 100644 --- a/drivers/mmc/dw_mmc.c +++ b/drivers/mmc/dw_mmc.c @@ -645,9 +645,11 @@ static int dwmci_set_ios(struct mmc *mmc) int ret; if (mmc->signal_voltage == MMC_SIGNAL_VOLTAGE_180) - ret = regulator_set_value(mmc->vqmmc_supply, 1800000); + ret = regulator_set_value_clamp(mmc->vqmmc_supply, + 1700000, 1800000, 1950000); else - ret = regulator_set_value(mmc->vqmmc_supply, 3300000); + ret = regulator_set_value_clamp(mmc->vqmmc_supply, + 2700000, 3300000, 3600000); if (ret && ret != -ENOSYS) return ret; } diff --git a/drivers/mmc/sdhci.c b/drivers/mmc/sdhci.c index 08594e10266..71a6d8ea2f5 100644 --- a/drivers/mmc/sdhci.c +++ b/drivers/mmc/sdhci.c @@ -551,7 +551,7 @@ void sdhci_set_uhs_timing(struct sdhci_host *host) void sdhci_set_voltage(struct sdhci_host *host) { - if (IS_ENABLED(CONFIG_MMC_IO_VOLTAGE)) { + if (CONFIG_IS_ENABLED(MMC_IO_VOLTAGE)) { struct mmc *mmc = (struct mmc *)host->mmc; u32 ctrl; diff --git a/drivers/power/regulator/Kconfig b/drivers/power/regulator/Kconfig index 3b3ed97eb9f..9f80bb953e8 100644 --- a/drivers/power/regulator/Kconfig +++ b/drivers/power/regulator/Kconfig @@ -557,3 +557,46 @@ 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. + +config DM_REGULATOR_MPQ8785 + bool "MPS MPQ8785 / MPM3695 / MPM82504 PMBus voltage regulator" + depends on DM_REGULATOR_PMBUS_HELPER + help + Driver for the Monolithic Power Systems MPQ8785 family of + digital multiphase voltage regulators with PMBus. Supports + MPM3695, MPM3695-25, MPM82504, and MPQ8785. Adapted from the + Linux drivers/hwmon/pmbus/mpq8785.c reference. + +config SANDBOX_PMBUS + bool "Sandbox PMBus 1.x chip emulator" + depends on SANDBOX && PMBUS && DM_I2C + help + Emulate a PMBus 1.x compliant chip behind a sandbox I2C bus so + the PMBus framework (lib/pmbus.c), the generic regulator + (DM_REGULATOR_PMBUS_GENERIC) and the pmbus CLI command can be + exercised by the dm unit tests with no real hardware. Only + useful for testing; say N on real boards. diff --git a/drivers/power/regulator/Makefile b/drivers/power/regulator/Makefile index 36a84e7cd71..b8599ea7fd9 100644 --- a/drivers/power/regulator/Makefile +++ b/drivers/power/regulator/Makefile @@ -49,3 +49,7 @@ 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 +obj-$(CONFIG_DM_REGULATOR_MPQ8785) += mpq8785.o +obj-$(CONFIG_SANDBOX_PMBUS) += sandbox_pmbus.o diff --git a/drivers/power/regulator/mpq8785.c b/drivers/power/regulator/mpq8785.c new file mode 100644 index 00000000000..dc321f0e410 --- /dev/null +++ b/drivers/power/regulator/mpq8785.c @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2026 Free Mobile, Vincent Jardin + * + * MPS MPQ8785 / MPM3695 / MPM3695-25 / MPM82504 PMBus voltage + * regulator driver. Adapted from + * linux/drivers/hwmon/pmbus/mpq8785.c + * (Charles Hsu, GPL-2.0-or-later) with the kernel hwmon publication + * and caching layers stripped. + * + * Hooks the shared pmbus_helper UCLASS_REGULATOR ops + adds the MPS + * specific identify (VOUT_MODE switch between LINEAR16 and DIRECT + * m=64 R=1 for the chip's "VID" mode), the MPS vendor extension + * (pmbus mps last|clear last|clear force), and ADDR_VBOOT auto + * promotion when the DT declared address fails the MFR_ID probe. + */ + +#include <command.h> +#include <dm.h> +#include <i2c.h> +#include <log.h> +#include <pmbus.h> +#include <vsprintf.h> +#include <linux/bitops.h> +#include <power/regulator.h> + +#include "pmbus_helper.h" + +/* Chip family identifiers (driver_data). */ +enum mpq_chip_id { + MPQ_MPM3695 = 0, + MPQ_MPM3695_25 = 1, + MPQ_MPM82504 = 2, + MPQ_MPQ8785 = 3, +}; + +/* + * MPS vendor extended command codes (NOT in PMBus 1.3 Part II). + * + * CLEAR_LAST_FAULT (08h) clears the NVM backed PROTECTION_LAST + * register. Gated by MFR_CFG_EXT (F5h) + * bit[6] = 1; chip silently no ops if the + * gate is cleared. + * PROTECTION_LAST (FBh) single event, NVM backed log of the last + * protection event. Survives the chip's own + * power cycle. The boot time post mortem the + * SoC has no other way to obtain. + * + * NEVER issue CLEAR_LAST_FAULT (08h) implicitly; it would erase the + * post mortem trail. Only the explicit pmbus mps clear last and + * pmbus mps clear force subcommands write it. + */ +#define PMBUS_CLEAR_LAST_FAULT 0x08 +#define MPS_PROTECTION_LAST 0xfb + +/* + * MFR_CFG_EXT (F5h) is an MPS extended config WORD (16 bits, not a + * byte). Bit[6] (MFR_CLR_FAULT_CFG) gates CLEAR_LAST_FAULT (08h) + * clearing PROTECTION_LAST. Several other bits are fixed and MUST be + * preserved on writeback; always read modify write the full 16 bits, + * only flip bit[6], restore on the way out. + */ +#define MPS_MFR_CFG_EXT 0xf5 +#define MPS_MFR_CFG_EXT_CLR_LAST_EN BIT(6) + +/* + * MPQ8785 driver_info (transcribed from Linux's + * drivers/hwmon/pmbus/mpq8785.c::mpq8785_info). DIRECT format with + * chip specific m / b / R coefficients on VIN, IOUT, TEMPERATURE. + * VOUT format is selected at probe time from VOUT_MODE: bits[7:5] == 0 + * selects LINEAR16, bits[7:5] == 1 or 2 selects DIRECT m=64 R=1. + */ + +/* + * MPS-extended STATUS_* bit names. The MPQ8785 family reuses several + * bit positions documented as RESERVED / UNKNOWN / NONE_ABOVE / + * MFR_SPECIFIC by PMBus 1.3 for chip specific signals. The override + * table below substitutes the chip name for the standard one when + * the bit is set, leaving every other PMBus 1.3 standard bit + * (VOUT_OV, IOUT_OC, TEMP, CML, ...) unchanged. + * + * STATUS_WORD bit[12] spec MFR_SPECIFIC chip NVM_SUMMARY (NVM + * backed PROTECTION_LAST + * register is non zero) + * STATUS_WORD bit[8] spec UNKNOWN chip WATCH_DOG (internal + * calculation FSM watchdog + * overflow) + * STATUS_WORD bit[0] spec NONE_ABOVE chip DRMOS_FAULT (DrMOS + * stage fault) + * STATUS_CML bit[4] spec MEMORY chip MTP_CRC_FAULT (NVM + * CRC mismatch on restore) + * STATUS_CML bit[0] spec OTHER_MEM_LOGIC chip MTP_FAULT (NVM + * signature fault) + * STATUS_TEMPERATURE bit[0] (PMBus leaves bits[3:0] reserved on + * this register) chip + * OT_SELF (controller die + * OT condition) + */ +static const struct pmbus_status_override mpq8785_status_overrides[] = { + { PMBUS_STATUS_WORD, BIT(12), "NVM_SUMMARY" }, + { PMBUS_STATUS_WORD, BIT(8), "WATCH_DOG" }, + { PMBUS_STATUS_WORD, BIT(0), "DRMOS_FAULT" }, + { PMBUS_STATUS_CML, BIT(4), "MTP_CRC_FAULT" }, + { PMBUS_STATUS_CML, BIT(0), "MTP_FAULT" }, + { PMBUS_STATUS_TEMPERATURE, BIT(0), "OT_SELF" }, + { /* sentinel */ } +}; + +static struct pmbus_driver_info mpq8785_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = pmbus_fmt_direct, + .format[PSC_VOLTAGE_OUT] = pmbus_fmt_linear, /* refined per VOUT_MODE */ + .format[PSC_CURRENT_OUT] = pmbus_fmt_direct, + .format[PSC_TEMPERATURE] = pmbus_fmt_direct, + .m[PSC_VOLTAGE_IN] = 4, .b[PSC_VOLTAGE_IN] = 0, .R[PSC_VOLTAGE_IN] = 1, + .m[PSC_CURRENT_OUT] = 16, .b[PSC_CURRENT_OUT] = 0, .R[PSC_CURRENT_OUT] = 0, + .m[PSC_TEMPERATURE] = 1, .b[PSC_TEMPERATURE] = 0, .R[PSC_TEMPERATURE] = 0, + /* + * Sensor set this family actually implements with calibrated + * coefficients: VIN, VOUT, IOUT, TEMP. READ_IIN / READ_POUT are + * ACKed by the silicon but uncalibrated here (the kernel's mpq8646 + * / mpq8785 drivers expose neither), so declaring the set keeps + * pmbus_print_telemetry from printing a bogus POUT / IIN -- matching + * the kernel's per-chip sensor list. + */ + .classes_present = BIT(PSC_VOLTAGE_IN) | BIT(PSC_VOLTAGE_OUT) | + BIT(PSC_CURRENT_OUT) | BIT(PSC_TEMPERATURE), + .status_overrides = mpq8785_status_overrides, +}; + +/* + * MPM3695 / MPM3695-25 / MPM82504 driver_info: VOUT in DIRECT format + * with chip family default m=8 R=2. Other sensor classes default to + * LINEAR (the chip family does not document non standard formats for + * VIN / IOUT / TEMPERATURE; the helper falls back to LINEAR11 when + * the active info is non NULL but format[c] is linear). + */ +static struct pmbus_driver_info mpm82504_info = { + .pages = 1, + .format[PSC_VOLTAGE_OUT] = pmbus_fmt_direct, + .m[PSC_VOLTAGE_OUT] = 8, .b[PSC_VOLTAGE_OUT] = 0, .R[PSC_VOLTAGE_OUT] = 2, + .format[PSC_VOLTAGE_IN] = pmbus_fmt_linear, + .format[PSC_CURRENT_OUT] = pmbus_fmt_linear, + .format[PSC_TEMPERATURE] = pmbus_fmt_linear, +}; + +/* + * Chip match for the framework's pmbus dev <bus>:<addr> raw I2C + * path. Used when the operator selects the chip directly by address + * instead of by regulator-name; the framework probes MFR_ID, sees + * "MPS" (after the byte reverse helper), and caches mpq8785_info. + */ +static const struct pmbus_chip_match mpq8785_match = { + .mfr_id = "MPS", + .mfr_id_reverse = true, + .vendor = "mps", + .info = &mpq8785_info, +}; + +static const struct pmbus_bit mpq_protection_last_bits[] = { + { 1u << 15, "INIT_FAULT" }, + { 1u << 14, "NVM_CRC_ERROR" }, + { 1u << 13, "NVM_FAULT" }, + { 1u << 12, "OC_PHASE_FAULT" }, + { 1u << 11, "OTP_SELF_FAULT" }, + { 1u << 9, "SWITCH_PRD_FAULT" }, + { 1u << 8, "VIN_OV_FAULT" }, + { 1u << 7, "VOUT_OV_FAULT" }, + { 1u << 6, "VOUT_UV_FAULT" }, + { 1u << 5, "OC_TOT_FAULT" }, + { 1u << 4, "VIN_UVLO_FAULT" }, + { 1u << 3, "DRMOS_OTP" }, + { /* sentinel */ } +}; + +static int mps_require_active(struct udevice **chip) +{ + const struct pmbus_active_dev *act = pmbus_active(); + + if (!act) { + printf("pmbus mps: no active device. Use 'pmbus dev <bus>:<addr>' first.\n"); + return CMD_RET_FAILURE; + } + if (strcmp(act->vendor, "mps") != 0) { + printf("pmbus mps: active device is not from vendor 'mps' (got '%s')\n", + act->vendor[0] ? act->vendor : "(generic)"); + return CMD_RET_FAILURE; + } + if (pmbus_active_get_i2c(chip)) { + printf("pmbus mps: cannot reach i2c%d:0x%02x\n", + act->bus_seq, act->addr); + return CMD_RET_FAILURE; + } + return CMD_RET_SUCCESS; +} + +static int mps_do_last(struct udevice *chip) +{ + u16 prot_last = 0; + + if (pmbus_read_word(chip, MPS_PROTECTION_LAST, &prot_last)) { + printf("pmbus mps: PROTECTION_LAST (FBh) read failed\n"); + return CMD_RET_FAILURE; + } + printf("PROTECTION_LAST (FBh) = 0x%04x [", prot_last); + pmbus_print_bits(prot_last, mpq_protection_last_bits); + printf("] (NVM, survives MPQ power cycle)\n"); + return CMD_RET_SUCCESS; +} + +static int mps_do_clear_last(struct udevice *chip) +{ + int ret; + + printf("pmbus mps: WARNING, erasing NVM PROTECTION_LAST (FBh) post mortem\n"); + ret = dm_i2c_write(chip, PMBUS_CLEAR_LAST_FAULT, NULL, 0); + if (ret) { + printf("pmbus mps: CLEAR_LAST_FAULT (08h) write failed (%d)\n", ret); + return CMD_RET_FAILURE; + } + printf("pmbus mps: CLEAR_LAST_FAULT (08h) issued; gated by MFR_CFG_EXT bit[6]\n"); + printf(" (chip silently no ops if F5h bit[6] = 0; verify by re reading FBh)\n"); + return CMD_RET_SUCCESS; +} + +static int mps_do_clear_force(struct udevice *chip) +{ + u8 wp_orig = 0; + u16 cfg_orig = 0, cfg_unlocked; + int ret, last_rc = 0, rc; + + printf("pmbus mps: FORCE; temporarily lowering WRITE_PROTECT and MFR_CFG_EXT.CLEAR_LAST_EN\n"); + printf("pmbus mps: WARNING, erasing NVM PROTECTION_LAST (FBh) post mortem\n"); + + ret = pmbus_read_byte(chip, PMBUS_WRITE_PROTECT, &wp_orig); + if (ret) { + printf("pmbus mps: WRITE_PROTECT (10h) read failed (%d), aborting force\n", ret); + return CMD_RET_FAILURE; + } + ret = pmbus_read_word(chip, MPS_MFR_CFG_EXT, &cfg_orig); + if (ret) { + printf("pmbus mps: MFR_CFG_EXT (F5h) read failed (%d), aborting force\n", ret); + return CMD_RET_FAILURE; + } + printf("pmbus mps: saved WRITE_PROTECT=0x%02x MFR_CFG_EXT=0x%04x\n", + wp_orig, cfg_orig); + + if (wp_orig != 0) { + u8 wp_open = 0x00; + + ret = dm_i2c_write(chip, PMBUS_WRITE_PROTECT, &wp_open, 1); + if (ret) { + printf("pmbus mps: WRITE_PROTECT clear failed (%d), chip refuses unlock\n", + ret); + return CMD_RET_FAILURE; + } + } + + cfg_unlocked = cfg_orig | MPS_MFR_CFG_EXT_CLR_LAST_EN; + ret = pmbus_write_word(chip, MPS_MFR_CFG_EXT, cfg_unlocked); + if (ret) { + printf("pmbus mps: MFR_CFG_EXT <- 0x%04x failed (%d)\n", + cfg_unlocked, ret); + goto restore_wp; + } + + last_rc = dm_i2c_write(chip, PMBUS_CLEAR_LAST_FAULT, NULL, 0); + if (last_rc) + printf("pmbus mps: CLEAR_LAST_FAULT (08h) write failed (%d) even with gate open\n", + last_rc); + else + printf("pmbus mps: CLEAR_LAST_FAULT (08h) issued with MFR_CFG_EXT bit[6]=1, PROTECTION_LAST should now read 0x0000\n"); + + rc = pmbus_write_word(chip, MPS_MFR_CFG_EXT, cfg_orig); + if (rc) + printf("pmbus mps: MFR_CFG_EXT restore failed (%d), gate may stay open until POR\n", + rc); + +restore_wp: + if (wp_orig != 0) { + rc = dm_i2c_write(chip, PMBUS_WRITE_PROTECT, &wp_orig, 1); + if (rc) + printf("pmbus mps: WRITE_PROTECT restore failed (%d), chip stays unlocked until POR\n", + rc); + } + + return (ret || last_rc) ? CMD_RET_FAILURE : CMD_RET_SUCCESS; +} + +static int mps_vendor_handler(struct cmd_tbl *cmdtp, int flag, int argc, + char *const argv[]) +{ + struct udevice *chip; + int rc; + + if (argc < 2) + return CMD_RET_USAGE; + + rc = mps_require_active(&chip); + if (rc) + return rc; + + if (!strcmp(argv[1], "last") && argc == 2) + return mps_do_last(chip); + if (!strcmp(argv[1], "clear") && argc >= 3) { + if (!strcmp(argv[2], "last")) + return mps_do_clear_last(chip); + if (!strcmp(argv[2], "force")) + return mps_do_clear_force(chip); + } + return CMD_RET_USAGE; +} + +static const struct pmbus_vendor_op mps_vendor_op = { + .vendor = "mps", + .handler = mps_vendor_handler, + .help = "pmbus mps last : read MPS PROTECTION_LAST (FBh)\n" + "pmbus mps clear last : issue MPS CLEAR_LAST_FAULT (08h) (DESTRUCTIVE)\n" + "pmbus mps clear force : force clear via MFR_CFG_EXT bit[6] (DESTRUCTIVE)\n", +}; + +static void mpq8785_identify_vout(struct udevice *i2c_dev) +{ + enum pmbus_data_format fmt; + + /* + * Let the shared helper read VOUT_MODE and pick the base format + * (the single source of truth for the bit layout). The MPS quirk: + * this family encodes VOUT in DIRECT with m=64 R=1 whenever + * VOUT_MODE reports VID *or* DIRECT -- override the helper's + * generic DIRECT m=1 / VID-unwired result in those two modes. + * LINEAR and IEEE754 keep the helper's selection unchanged. + */ + fmt = pmbus_regulator_identify_vout(i2c_dev, &mpq8785_info); + if (fmt == pmbus_fmt_vid || fmt == pmbus_fmt_direct) { + mpq8785_info.format[PSC_VOLTAGE_OUT] = pmbus_fmt_direct; + mpq8785_info.m[PSC_VOLTAGE_OUT] = 64; + mpq8785_info.b[PSC_VOLTAGE_OUT] = 0; + mpq8785_info.R[PSC_VOLTAGE_OUT] = 1; + } +} + +/* + * The MPQ8785 datasheet revision letter changes which window the + * analog ADDR_VBOOT level resolves to. Boards have been observed at + * 0x10 (later die rev) versus the 0x20 the original driver assumed. + * If the DT declared address fails the MFR_ID probe at probe time, + * walk the three documented windows looking for an MPS responder. + * + * Each window covers 16 consecutive 7 bit I2C addresses; the low + * nibble selects the chip's MFR_ADDR_PMBUS slot within the window. + */ +#define MPS_ADDR_VBOOT_WINDOW_SIZE 16 +static const u8 mps_addr_window_starts[] = { 0x10, 0x20, 0x60 }; + +static int mpq8785_probe_addr(struct udevice *bus, u8 addr, + struct udevice **chip_out) +{ + char id[PMBUS_MFR_STRING_MAX] = ""; + struct udevice *chip; + int ret; + + ret = i2c_get_chip(bus, addr, 1, &chip); + if (ret) + return ret; + ret = pmbus_read_string(chip, PMBUS_MFR_ID, id, sizeof(id), true); + if (ret < 0) + return ret; + if (strncmp(id, "MPS", 3) != 0) + return -ENODEV; + *chip_out = chip; + return 0; +} + +static int mpq8785_scan_windows(struct udevice *bus, u8 *found_addr, + struct udevice **chip_out) +{ + unsigned int i, j; + + for (i = 0; i < ARRAY_SIZE(mps_addr_window_starts); i++) { + for (j = 0; j < MPS_ADDR_VBOOT_WINDOW_SIZE; j++) { + u8 addr = mps_addr_window_starts[i] + j; + + if (mpq8785_probe_addr(bus, addr, chip_out) == 0) { + *found_addr = addr; + return 0; + } + } + } + return -ENODEV; +} + +static struct pmbus_driver_info *mpq8785_pick_info(enum mpq_chip_id chip_id) +{ + switch (chip_id) { + case MPQ_MPM3695: + case MPQ_MPM3695_25: + case MPQ_MPM82504: + return &mpm82504_info; + case MPQ_MPQ8785: + default: + return &mpq8785_info; + } +} + +static int mpq8785_probe(struct udevice *dev) +{ + enum mpq_chip_id chip_id = (enum mpq_chip_id)dev_get_driver_data(dev); + struct pmbus_regulator_priv *priv = dev_get_priv(dev); + struct pmbus_driver_info *info = mpq8785_pick_info(chip_id); + static bool match_registered; + static bool vendor_registered; + u32 fb_div; + int ret; + + ret = pmbus_regulator_probe_common(dev, info, 0); + if (ret) + return ret; + + /* + * Verify the chip answers MFR_ID="MPS" at the DT declared + * address. If it doesn't, walk the documented ADDR_VBOOT windows + * looking for it (a die rev address shift). On a hit, replace + * priv->i2c_dev with the discovered chip handle and continue. + */ + { + char id[PMBUS_MFR_STRING_MAX] = ""; + + ret = pmbus_read_string(priv->i2c_dev, PMBUS_MFR_ID, id, + sizeof(id), true); + if (ret < 0 || strncmp(id, "MPS", 3) != 0) { + struct udevice *bus = dev_get_parent(dev); + struct udevice *promoted; + u8 found = 0; + + if (mpq8785_scan_windows(bus, &found, &promoted) == 0) { + printf("MPQ8785: DT addr 0x%02x silent, auto promoted to 0x%02x\n", + (unsigned int)dev_read_addr(dev), found); + priv->i2c_dev = promoted; + } else { + printf("MPQ8785: no MPS responder found in 0x10..0x1f / 0x20..0x2f / 0x60..0x6f\n"); + return -ENODEV; + } + } + } + + /* MPQ8785 specific: refine VOUT format from VOUT_MODE. */ + if (chip_id == MPQ_MPQ8785) + mpq8785_identify_vout(priv->i2c_dev); + + /* Apply mps,vout-fb-divider-ratio-permille if present in DT. */ + fb_div = dev_read_u32_default(dev, "mps,vout-fb-divider-ratio-permille", 0); + if (fb_div) { + ret = pmbus_regulator_apply_voltage_scale(dev, fb_div); + if (ret) { + printf("MPQ8785: VOUT_SCALE_LOOP write failed (%d)\n", ret); + return ret; + } + } + + /* + * Register the chip match and the MPS vendor handler exactly + * once across all bound MPS regulators (a board could legally + * carry several). Both registries are global and idempotent + * matches return -ENOSPC, so the static guards keep things + * tidy. + */ + if (!match_registered) { + if (pmbus_register_chip(&mpq8785_match) == 0) + match_registered = true; + } + if (!vendor_registered) { + if (pmbus_register_vendor_handler(&mps_vendor_op) == 0) + vendor_registered = true; + } + return 0; +} + +static const struct udevice_id mpq8785_ids[] = { + { .compatible = "mps,mpm3695", .data = MPQ_MPM3695 }, + { .compatible = "mps,mpm3695-25", .data = MPQ_MPM3695_25 }, + { .compatible = "mps,mpm82504", .data = MPQ_MPM82504 }, + { .compatible = "mps,mpq8785", .data = MPQ_MPQ8785 }, + { } +}; + +U_BOOT_DRIVER(mpq8785_regulator) = { + .name = "mpq8785_regulator", + .id = UCLASS_REGULATOR, + .of_match = mpq8785_ids, + .probe = mpq8785_probe, + .ops = &pmbus_regulator_ops, + .priv_auto = sizeof(struct pmbus_regulator_priv), +}; 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 <dm.h> +#include <i2c.h> +#include <log.h> +#include <pmbus.h> +#include <power/regulator.h> + +#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/<chip>.c. + */ + +#include <dm.h> +#include <dm/device-internal.h> +#include <dm/lists.h> +#include <i2c.h> +#include <log.h> +#include <pmbus.h> +#include <vsprintf.h> +#include <linux/types.h> +#include <power/regulator.h> + +#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 "<regulator-name>-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/<chip>.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 <pmbus.h> 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 <linux/types.h> +#include <pmbus.h> + +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/drivers/power/regulator/regulator-uclass.c b/drivers/power/regulator/regulator-uclass.c index 1c7f75a9338..0f5ba51ad6c 100644 --- a/drivers/power/regulator/regulator-uclass.c +++ b/drivers/power/regulator/regulator-uclass.c @@ -111,6 +111,33 @@ int regulator_get_suspend_value(struct udevice *dev) return ops->get_suspend_value(dev); } +int regulator_set_value_clamp(struct udevice *dev, + int min_uV, int target_uV, int max_uV) +{ + const struct dm_regulator_ops *ops = dev_get_driver_ops(dev); + struct dm_regulator_uclass_plat *uc_pdata; + int uV; + + if (!ops || !ops->set_value) + return -ENOSYS; + + uc_pdata = dev_get_uclass_plat(dev); + if (uc_pdata->min_uV != -ENODATA && max_uV < uc_pdata->min_uV) + return -EINVAL; + if (uc_pdata->max_uV != -ENODATA && min_uV > uc_pdata->max_uV) + return -EINVAL; + if (min_uV > max_uV) + return -EINVAL; + + if (uc_pdata->min_uV != -ENODATA) + min_uV = max(min_uV, uc_pdata->min_uV); + if (uc_pdata->max_uV != -ENODATA) + max_uV = min(max_uV, uc_pdata->max_uV); + uV = clamp(target_uV, min_uV, max_uV); + + return regulator_set_value(dev, uV); +} + /* * To be called with at most caution as there is no check * before setting the actual voltage value. 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/drivers/power/regulator/sandbox_pmbus.c b/drivers/power/regulator/sandbox_pmbus.c new file mode 100644 index 00000000000..7b52d2ca293 --- /dev/null +++ b/drivers/power/regulator/sandbox_pmbus.c @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2026 Free Mobile - Vincent Jardin + * + * Sandbox PMBus 1.x chip stub (UCLASS_I2C_EMUL). + * + * Stub a DT i2c node so the PMBus framework, the generic pmbus + * regulator and the pmbus CLI command can be tested. + * The model is a flat per-command 16 bit + * register file combined with some fixed identification strings. + */ + +#include <dm.h> +#include <i2c.h> +#include <pmbus.h> +#include <linux/ctype.h> + +#define PMBUS_EMUL_NREG 256 + +struct sandbox_pmbus_priv { + u16 reg[PMBUS_EMUL_NREG]; + bool supported[PMBUS_EMUL_NREG]; +}; + +/* Identification strings reported in the natural (forward) byte order. */ +static const char *pmbus_emul_string(u8 cmd) +{ + switch (cmd) { + case PMBUS_MFR_ID: + return "SANDBOX"; + case PMBUS_MFR_MODEL: + return "PMBUS-EMUL"; + case PMBUS_MFR_REVISION: + return "1.0"; + default: + return NULL; + } +} + +static int sandbox_pmbus_read(struct sandbox_pmbus_priv *priv, u8 cmd, + u8 *buf, int len) +{ + const char *str = pmbus_emul_string(cmd); + int i; + + if (len < 1) + return -EINVAL; + + if (str) { + int slen = strlen(str); + + /* + * Block payload: [length][bytes...]. A one-byte read is + * the SMBus length probe and reports the true length (the + * master just NAKs early). Any longer read must have room + * for length + payload, otherwise the length byte would + * lie about the data actually returned. + */ + if (len > 1 && len < slen + 1) + return -EREMOTEIO; + buf[0] = (u8)slen; + for (i = 1; i < len; i++) + buf[i] = (i - 1 < slen) ? (u8)str[i - 1] : 0; + return 0; + } + + if (!priv->supported[cmd]) + return -EREMOTEIO; /* chip NAKs an unimplemented command */ + + for (i = 0; i < len; i++) + buf[i] = (u8)(priv->reg[cmd] >> (8 * i)); + return 0; +} + +static int sandbox_pmbus_write(struct sandbox_pmbus_priv *priv, u8 cmd, + const u8 *buf, int len) +{ + if (len == 0) + return 0; /* send-byte (eg CLEAR_FAULTS): just ACK */ + if (!priv->supported[cmd]) + return -EREMOTEIO; + if (len == 1) + priv->reg[cmd] = buf[0]; + else + priv->reg[cmd] = (u16)buf[0] | ((u16)buf[1] << 8); + return 0; +} + +static int sandbox_pmbus_xfer(struct udevice *emul, struct i2c_msg *msg, + int nmsgs) +{ + struct sandbox_pmbus_priv *priv = dev_get_priv(emul); + u8 cmd; + + if (nmsgs == 0) + return 0; + /* A PMBus transaction always opens with the command-code write. */ + if (msg[0].flags & I2C_M_RD) + return -EIO; + if (msg[0].len == 0) + return 0; /* address-only probe */ + cmd = msg[0].buf[0]; + + if (nmsgs >= 2 && (msg[1].flags & I2C_M_RD)) + return sandbox_pmbus_read(priv, cmd, msg[1].buf, msg[1].len); + + return sandbox_pmbus_write(priv, cmd, msg[0].buf + 1, msg[0].len - 1); +} + +static void sandbox_pmbus_support(struct sandbox_pmbus_priv *priv, u8 cmd, + u16 val) +{ + priv->supported[cmd] = true; + priv->reg[cmd] = val; +} + +static int sandbox_pmbus_probe(struct udevice *emul) +{ + struct sandbox_pmbus_priv *priv = dev_get_priv(emul); + + /* Configuration / identification. */ + sandbox_pmbus_support(priv, PMBUS_PAGE, 0); + sandbox_pmbus_support(priv, PMBUS_OPERATION, PB_OPERATION_ON); + sandbox_pmbus_support(priv, PMBUS_ON_OFF_CONFIG, 0); + sandbox_pmbus_support(priv, PMBUS_WRITE_PROTECT, 0); + sandbox_pmbus_support(priv, PMBUS_CAPABILITY, 0x30); + sandbox_pmbus_support(priv, PMBUS_VOUT_MODE, 0x18); /* LINEAR 2^-8 */ + sandbox_pmbus_support(priv, PMBUS_VOUT_COMMAND, 0x0200); + sandbox_pmbus_support(priv, PMBUS_VOUT_TRIM, 0); + sandbox_pmbus_support(priv, PMBUS_VOUT_MAX, 0x0400); + sandbox_pmbus_support(priv, PMBUS_VOUT_SCALE_LOOP, 0); + sandbox_pmbus_support(priv, PMBUS_REVISION, PMBUS_REV_13); + + /* Status registers, all clean. */ + sandbox_pmbus_support(priv, PMBUS_STATUS_BYTE, 0); + sandbox_pmbus_support(priv, PMBUS_STATUS_WORD, 0); + sandbox_pmbus_support(priv, PMBUS_STATUS_VOUT, 0); + sandbox_pmbus_support(priv, PMBUS_STATUS_IOUT, 0); + sandbox_pmbus_support(priv, PMBUS_STATUS_INPUT, 0); + sandbox_pmbus_support(priv, PMBUS_STATUS_TEMPERATURE, 0); + sandbox_pmbus_support(priv, PMBUS_STATUS_CML, 0); + + /* + * Telemetry the emulated chip implements. READ_IIN and READ_POUT + * are intentionally absent so callers see the unsupported path. + */ + sandbox_pmbus_support(priv, PMBUS_READ_VIN, 0x0abc); + sandbox_pmbus_support(priv, PMBUS_READ_VOUT, 0x0200); + sandbox_pmbus_support(priv, PMBUS_READ_IOUT, 0x0123); + sandbox_pmbus_support(priv, PMBUS_READ_TEMPERATURE_1, 0x0019); + + return 0; +} + +static struct dm_i2c_ops sandbox_pmbus_emul_ops = { + .xfer = sandbox_pmbus_xfer, +}; + +static const struct udevice_id sandbox_pmbus_ids[] = { + { .compatible = "sandbox,i2c-pmbus" }, + { } +}; + +U_BOOT_DRIVER(sandbox_pmbus_emul) = { + .name = "sandbox_pmbus_emul", + .id = UCLASS_I2C_EMUL, + .of_match = sandbox_pmbus_ids, + .probe = sandbox_pmbus_probe, + .priv_auto = sizeof(struct sandbox_pmbus_priv), + .ops = &sandbox_pmbus_emul_ops, +}; diff --git a/drivers/serial/serial_lpuart.c b/drivers/serial/serial_lpuart.c index 955f1c96407..917445e1ee2 100644 --- a/drivers/serial/serial_lpuart.c +++ b/drivers/serial/serial_lpuart.c @@ -362,10 +362,14 @@ static int _lpuart32_serial_putc(struct lpuart_serial_plat *plat, static int _lpuart32_serial_tstc(struct lpuart_serial_plat *plat) { struct lpuart_fsl_reg32 *base = plat->reg; - u32 water; + u32 stat, water; lpuart_read32(plat->flags, &base->water, &water); + lpuart_read32(plat->flags, &base->stat, &stat); + if (stat & STAT_OR) + lpuart_write32(plat->flags, &base->stat, STAT_OR); + if ((water >> 24) == 0) return 0; diff --git a/drivers/thermal/Kconfig b/drivers/thermal/Kconfig index 9ad0d699850..3b7e807a024 100644 --- a/drivers/thermal/Kconfig +++ b/drivers/thermal/Kconfig @@ -63,4 +63,12 @@ config DM_THERMAL_JC42 Enable support for the JEDEC JC-42.4 temperature sensor found on the SPD bus of DDR3 and DDR4 DIMMs (TSE2004av and compatible). +config PMBUS_THERMAL + bool "Generic PMBus temperature" + depends on DM_REGULATOR_PMBUS_HELPER + help + Expose the die-temperature reading of any PMBus voltage + regulator bound by a pmbus_helper based chip driver + as a UCLASS_THERMAL device. + endif # if DM_THERMAL diff --git a/drivers/thermal/Makefile b/drivers/thermal/Makefile index c9fa7561b45..475026a5ab2 100644 --- a/drivers/thermal/Makefile +++ b/drivers/thermal/Makefile @@ -12,3 +12,4 @@ obj-$(CONFIG_SANDBOX) += thermal_sandbox.o obj-$(CONFIG_TI_DRA7_THERMAL) += ti-bandgap.o obj-$(CONFIG_TI_LM74_THERMAL) += ti-lm74.o obj-$(CONFIG_DM_THERMAL_JC42) += jc42.o +obj-$(CONFIG_PMBUS_THERMAL) += pmbus_thermal.o diff --git a/drivers/thermal/pmbus_thermal.c b/drivers/thermal/pmbus_thermal.c new file mode 100644 index 00000000000..c251cea8ddf --- /dev/null +++ b/drivers/thermal/pmbus_thermal.c @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2026 Free Mobile - Vincent Jardin + * + * Generic UCLASS_THERMAL companion for PMBus voltage regulators. + * + * Works with any chip bound by a pmbus_helper based regulator driver + * (drivers/power/regulator/<chip>.c calling + * pmbus_regulator_probe_common()). It auto-spawns one + * of these thermal devices per regulator. + * + * The reading is the chip's READ_TEMPERATURE_1 (PMBus 0x8D), decoded + * through the parent's pmbus_driver_info. + */ + +#include <dm.h> +#include <pmbus.h> +#include <thermal.h> + +static int pmbus_thermal_get_temp(struct udevice *dev, int *temp) +{ + return pmbus_regulator_read_temp(dev_get_parent(dev), temp); +} + +static const struct dm_thermal_ops pmbus_thermal_ops = { + .get_temp = pmbus_thermal_get_temp, +}; + +U_BOOT_DRIVER(pmbus_thermal) = { + .name = "pmbus_thermal", + .id = UCLASS_THERMAL, + .ops = &pmbus_thermal_ops, +}; diff --git a/include/pmbus.h b/include/pmbus.h new file mode 100644 index 00000000000..0e121fa6637 --- /dev/null +++ b/include/pmbus.h @@ -0,0 +1,667 @@ +/* SPDX-License-Identifier: GPL-2.0+ */ +/* + * Copyright 2026 Free Mobile, Vincent Jardin + * + * PMBus 1.x command codes, numeric format decoders, and driver_info + * scaffolding for U-Boot. + * + * Intents + * + * U-Boot's PMBus support is not a hwmon clone. It shall be used to: + * 1. identify PMBus regulators a board carries at boot, + * 2. print telemetry so an operator can confirm rail voltages and + * fault status before handing off to the kernel, + * 3. decode chip alerts when a rail trips an over/under voltage, + * over current, or thermal threshold, + * 4. optionally trim a critical rail before kernel boot, to better + * protect the board. + * + * No periodic polling, no /sys, no userspace surface, no fan control + * loops. Linux owns those. See doc/develop/pmbus.rst for the full + * porting guide and policy notes. + * + * Linux relationship + * + * Constants (command codes, status bit names, sensor class enum, + * format enum) are mirrored verbatim from + * linux/drivers/hwmon/pmbus/pmbus.h + * with attribution; this is standardised data and copying it avoids + * accidental drift. Decoders (LINEAR11/16, DIRECT, VID, IEEE754) are + * reimplemented from the PMBus 1.x spec rather than copied. The + * surrounding kernel context (struct pmbus_data, hwmon caching, + * sysfs publication) does not apply to U-Boot. + * + * Tree level files (this header, lib/pmbus.c, future per chip drivers + * under drivers/power/regulator/) must stay platform agnostic. They + * may reference only the PMBus 1.x specification and chip datasheets, + * never a specific board, SoC, or product. Board specific quirks + * live under board/<vendor>/<board>/. + */ + +#ifndef _PMBUS_H_ +#define _PMBUS_H_ + +#include <linux/types.h> + +struct udevice; +struct pmbus_driver_info; +struct pmbus_status_override; + +/* + * PMBus 1.3 standard command codes (Part II). + * + * Subset relevant to U-Boot's needs: + * - configuration (PAGE, OPERATION, VOUT_*), + * - telemetry (READ_VIN, READ_IOUT, READ_TEMPERATURE_1, ...), + * - status (STATUS_WORD, STATUS_VOUT, ...), + * - identification (MFR_ID, MFR_MODEL, MFR_REVISION). + * Chip specific extensions (for example MPS PROTECTION_LAST 0xFB) shall be in + * the per chip driver file. + */ +#define PMBUS_PAGE 0x00 +#define PMBUS_OPERATION 0x01 +#define PMBUS_ON_OFF_CONFIG 0x02 +#define PMBUS_CLEAR_FAULTS 0x03 +#define PMBUS_PHASE 0x04 + +#define PMBUS_WRITE_PROTECT 0x10 + +#define PMBUS_CAPABILITY 0x19 +#define PMBUS_QUERY 0x1a +#define PMBUS_SMBALERT_MASK 0x1b +#define PMBUS_VOUT_MODE 0x20 +#define PMBUS_VOUT_COMMAND 0x21 +#define PMBUS_VOUT_TRIM 0x22 +#define PMBUS_VOUT_CAL_OFFSET 0x23 +#define PMBUS_VOUT_MAX 0x24 +#define PMBUS_VOUT_MARGIN_HIGH 0x25 +#define PMBUS_VOUT_MARGIN_LOW 0x26 +#define PMBUS_VOUT_TRANSITION_RATE 0x27 +#define PMBUS_VOUT_DROOP 0x28 +#define PMBUS_VOUT_SCALE_LOOP 0x29 +#define PMBUS_VOUT_SCALE_MONITOR 0x2a + +#define PMBUS_COEFFICIENTS 0x30 +#define PMBUS_POUT_MAX 0x31 + +#define PMBUS_STATUS_BYTE 0x78 +#define PMBUS_STATUS_WORD 0x79 +#define PMBUS_STATUS_VOUT 0x7a +#define PMBUS_STATUS_IOUT 0x7b +#define PMBUS_STATUS_INPUT 0x7c +#define PMBUS_STATUS_TEMPERATURE 0x7d +#define PMBUS_STATUS_CML 0x7e +#define PMBUS_STATUS_OTHER 0x7f +#define PMBUS_STATUS_MFR_SPECIFIC 0x80 + +#define PMBUS_READ_VIN 0x88 +#define PMBUS_READ_IIN 0x89 +#define PMBUS_READ_VCAP 0x8a +#define PMBUS_READ_VOUT 0x8b +#define PMBUS_READ_IOUT 0x8c +#define PMBUS_READ_TEMPERATURE_1 0x8d +#define PMBUS_READ_TEMPERATURE_2 0x8e +#define PMBUS_READ_TEMPERATURE_3 0x8f +#define PMBUS_READ_DUTY_CYCLE 0x94 +#define PMBUS_READ_FREQUENCY 0x95 +#define PMBUS_READ_POUT 0x96 +#define PMBUS_READ_PIN 0x97 + +#define PMBUS_REVISION 0x98 +#define PMBUS_MFR_ID 0x99 +#define PMBUS_MFR_MODEL 0x9a +#define PMBUS_MFR_REVISION 0x9b +#define PMBUS_MFR_LOCATION 0x9c +#define PMBUS_MFR_DATE 0x9d +#define PMBUS_MFR_SERIAL 0x9e + +#define PMBUS_IC_DEVICE_ID 0xad +#define PMBUS_IC_DEVICE_REV 0xae + +/* VOUT_MODE upper bits: numeric format selector (Part II sec 8.3). */ +#define PB_VOUT_MODE_MODE_MASK 0xe0 +#define PB_VOUT_MODE_PARAM_MASK 0x1f +#define PB_VOUT_MODE_LINEAR 0x00 +#define PB_VOUT_MODE_VID 0x20 +#define PB_VOUT_MODE_DIRECT 0x40 +#define PB_VOUT_MODE_IEEE754 0x60 + +/* STATUS_WORD lower byte (= STATUS_BYTE), Part II sec 10.1.1. */ +#define PB_STATUS_NONE_ABOVE BIT(0) +#define PB_STATUS_CML BIT(1) +#define PB_STATUS_TEMPERATURE BIT(2) +#define PB_STATUS_VIN_UV BIT(3) +#define PB_STATUS_IOUT_OC BIT(4) +#define PB_STATUS_VOUT_OV BIT(5) +#define PB_STATUS_OFF BIT(6) +#define PB_STATUS_BUSY BIT(7) + +/* STATUS_WORD upper byte. */ +#define PB_STATUS_UNKNOWN BIT(8) +#define PB_STATUS_OTHER BIT(9) +#define PB_STATUS_FANS BIT(10) +#define PB_STATUS_POWER_GOOD_N BIT(11) +#define PB_STATUS_WORD_MFR BIT(12) +#define PB_STATUS_INPUT BIT(13) +#define PB_STATUS_IOUT_POUT BIT(14) +#define PB_STATUS_VOUT BIT(15) + +/* STATUS_VOUT (PMBus 1.3.1 Part II sec 17.3, Table 17). */ +#define PB_VOLTAGE_VOUT_MAX_MIN_WARN BIT(3) +#define PB_VOLTAGE_UV_FAULT BIT(4) +#define PB_VOLTAGE_UV_WARNING BIT(5) +#define PB_VOLTAGE_OV_WARNING BIT(6) +#define PB_VOLTAGE_OV_FAULT BIT(7) + +/* STATUS_IOUT (Part II sec 10.6). */ +#define PB_POUT_OP_WARNING BIT(0) +#define PB_POUT_OP_FAULT BIT(1) +#define PB_POWER_LIMITING BIT(2) +#define PB_CURRENT_SHARE_FAULT BIT(3) +#define PB_IOUT_UC_FAULT BIT(4) +#define PB_IOUT_OC_WARNING BIT(5) +#define PB_IOUT_OC_LV_FAULT BIT(6) +#define PB_IOUT_OC_FAULT BIT(7) + +/* STATUS_INPUT (Part II sec 10.7). */ +#define PB_PIN_OP_WARNING BIT(0) +#define PB_IIN_OC_WARNING BIT(1) +#define PB_IIN_OC_FAULT BIT(2) + +/* STATUS_TEMPERATURE (Part II sec 10.8). */ +#define PB_TEMP_UT_FAULT BIT(4) +#define PB_TEMP_UT_WARNING BIT(5) +#define PB_TEMP_OT_WARNING BIT(6) +#define PB_TEMP_OT_FAULT BIT(7) + +/* STATUS_CML (Part II sec 10.9). */ +#define PB_CML_FAULT_OTHER_MEM_LOGIC BIT(0) +#define PB_CML_FAULT_OTHER_COMM BIT(1) +#define PB_CML_FAULT_PROCESSOR BIT(3) +#define PB_CML_FAULT_MEMORY BIT(4) +#define PB_CML_FAULT_PACKET_ERROR BIT(5) +#define PB_CML_FAULT_INVALID_DATA BIT(6) +#define PB_CML_FAULT_INVALID_COMMAND BIT(7) + +/* + * OPERATION (01h) command bits per PMBus 1.3 Part II sec 9.1. Bit[7] + * is the master rail enable; the lower bits select margin high/low + * and turn off behaviour (subset surfaced for the regulator helper). + */ +#define PB_OPERATION_ON BIT(7) + +/* + * LINEAR11 numeric format (PMBus 1.3 Part II sec 7): 16 bit register + * with a signed 11 bit mantissa in bits[10:0] and a signed 5 bit + * exponent in bits[15:11]. Engineering value = mantissa * 2^exponent. + */ +#define PB_LINEAR11_MANT_MASK 0x07ff +#define PB_LINEAR11_MANT_BITS 11 +#define PB_LINEAR11_EXP_SHIFT 11 +#define PB_LINEAR11_EXP_MASK 0x1f +#define PB_LINEAR11_EXP_BITS 5 + +/* + * Cache buffer sizes for the active device singleton + MFR_* block + * reads. PMBus block reads return up to 32 bytes per the SMBus spec; + * 16 covers every MFR string seen in practice on regulator class + * chips and keeps the singleton compact. + */ +#define PMBUS_MFR_STRING_MAX 16 +#define PMBUS_VENDOR_NAME_MAX 8 +#define PMBUS_REGULATOR_NAME_MAX 24 + +/* PMBus revision identifiers reported by PMBUS_REVISION (98h). */ +#define PMBUS_REV_10 0x00 /* PMBus 1.0 */ +#define PMBUS_REV_11 0x11 /* PMBus 1.1 */ +#define PMBUS_REV_12 0x22 /* PMBus 1.2 */ +#define PMBUS_REV_13 0x33 /* PMBus 1.3 */ + +/* + * Numeric formats and sensor classes. + * + * Mirrors linux/drivers/hwmon/pmbus/pmbus.h enum pmbus_data_format + * and enum pmbus_sensor_classes. A chip's pmbus_driver_info wires + * each sensor class to one format and (for DIRECT) to its m/b/R + * coefficients. + */ +enum pmbus_data_format { + pmbus_fmt_linear = 0, + pmbus_fmt_ieee754, + pmbus_fmt_direct, + pmbus_fmt_vid, +}; + +enum pmbus_sensor_classes { + PSC_VOLTAGE_IN = 0, + PSC_VOLTAGE_OUT, + PSC_CURRENT_IN, + PSC_CURRENT_OUT, + PSC_POWER, + PSC_TEMPERATURE, + PSC_NUM_CLASSES +}; + +/* + * Per chip identification record. Each per chip driver declares one + * of these and points the framework at it. Subset of the kernel + * struct pmbus_driver_info: U-Boot has no per page caches, no fan + * accessors, no virtual registers, no async sysfs publication. + * + * pages number of PAGE distinct rails the chip exposes + * (1 for single rail parts). + * format[] numeric format per sensor class. + * m/b/R[] DIRECT format coefficients per sensor class. See + * pmbus_reg2data_direct() below for the formula. + * Unused for non DIRECT classes. + * read_byte_data, read_word_data + * optional per chip register translators. Return the + * standard register value on success, ENODATA to fall + * through to the generic transport, any other negative + * errno on bus error. + * identify optional probe time hook to discover format and + * page count from the chip itself (for example, the + * MPQ8785 VOUT_MODE switch between LINEAR and DIRECT). + */ +struct pmbus_driver_info { + int pages; + enum pmbus_data_format format[PSC_NUM_CLASSES]; + int m[PSC_NUM_CLASSES]; + int b[PSC_NUM_CLASSES]; + int R[PSC_NUM_CLASSES]; + + int (*read_byte_data)(struct udevice *dev, int page, int reg); + int (*read_word_data)(struct udevice *dev, int page, int reg); + int (*identify)(struct udevice *dev, struct pmbus_driver_info *info); + + /* + * Optional sparse table of chip specific STATUS_* bit name + * substitutions. Terminated by an entry with .name = NULL + * (matching the convention used by struct udevice_id and + * other U-Boot driver tables). NULL pointer means the chip + * uses only PMBus 1.x standard names. See struct + * pmbus_status_override and pmbus_print_status_bits(). + */ + const struct pmbus_status_override *status_overrides; + + /* + * Bitmask (BIT(enum pmbus_sensor_classes)) of the sensor classes + * this chip actually implements. When non-zero, pmbus_print_telemetry + * prints exactly these classes -- mirroring the kernel's per chip + * sensor set -- and skips the rest. This is how an MPS buck that + * ACKs READ_POUT / READ_IIN with an uncalibrated value still hides + * POWER / CURRENT_IN (the kernel's mpq8646 driver exposes neither). + * Zero means "not declared": the telemetry printer falls back to a + * live pmbus_word_command_supported() probe per class, which is what + * the generic driver (compatible = "pmbus") relies on. + */ + u8 classes_present; +}; + +/* + * Decoder helpers (raw register, returns engineering value in micro + * units). + * + * All return signed micro units (uV, uA, udegC), 64 bit to avoid + * overflow on large mantissa times exponent products. The caller + * divides by 1000 for milli units, or by 1_000_000 for the integer + * engineering value. + */ + +/* + * LINEAR11. Bits[15:11] = signed 5 bit exponent Y, bits[10:0] = + * signed 11 bit mantissa N. Engineering value = N * 2^Y. + * + * Used by most PMBus chips for VIN, IIN, IOUT, TEMP. Some MPS + * parts deviate (they report DIRECT format with chip specific m/b/R + * coefficients); check the chip datasheet against PMBUS_VOUT_MODE + * and the Linux per chip driver if porting. + */ +s64 pmbus_reg2data_linear11(u16 raw); + +/* + * LINEAR16. 16 bit unsigned mantissa multiplied by 2^Y, where Y is + * the signed 5 bit exponent supplied via VOUT_MODE bits[4:0]. The + * caller must read VOUT_MODE (cmd 0x20) and pass it in vout_mode. + * Only the mode_mask bits[7:5] = 0 selector is the LINEAR16 path. + * + * Used for READ_VOUT (8Bh) on chips whose VOUT_MODE selects Linear. + * Returns 0 if VOUT_MODE indicates a non Linear mode; the caller + * is then expected to dispatch to pmbus_reg2data_direct() with the + * appropriate per chip m/b/R, or to pmbus_reg2data_vid() / _ieee754() + * if the chip uses those formats. + */ +s64 pmbus_reg2data_linear16(u16 raw, u8 vout_mode); + +/* + * DIRECT. PMBus 1.3 Part II sec 8.4. The chip stores a signed 16 bit + * value X; the engineering value Y is one over m, multiplied by the + * quantity (X scaled by ten to the power minus R, then offset by + * minus b), with chip specific (m, b, R) coefficients. + * + * In symbolic form: Y = (1/m) * (X * 10**(-R) - b). The negative + * exponent and trailing subtraction are math operators in the + * formula, not punctuation in the prose. + * + * Returns micro units. Implementation order matches the Linux + * reference (multiply before subtract, scale R then divide by m) to + * minimise quantisation drift. m == 0 returns 0. + */ +s64 pmbus_reg2data_direct(s16 raw, int m, int b, int R); + +/* + * Encoder: engineering value (micro units) to LINEAR16 raw register + * value. Used by pre kernel rail trim code (see board/nxp/common/vid.c) + * to write VOUT_COMMAND from a target voltage. The exponent is + * recovered from VOUT_MODE. + * + * Returns 0 if VOUT_MODE indicates a non Linear format; the caller + * must then dispatch to pmbus_data2reg_direct() (DIRECT format) or + * the VID / IEEE754 encoders (when those land) per the chip's actual + * VOUT_MODE selector. + */ +u16 pmbus_data2reg_linear16(s64 micro, u8 vout_mode); + +/* + * Encoder: engineering value (micro units) to DIRECT raw register + * value. Inverse of pmbus_reg2data_direct(): + * + * X = (m * Y + b) * 10^R + * + * with chip specific (m, b, R) coefficients (typically taken from + * the chip's pmbus_driver_info[PSC_VOLTAGE_OUT]). m == 0 returns 0. + * + * The result is saturated to the s16 range mandated by the PMBus + * 1.3 Part II sec 8.4 DIRECT encoding; out of range targets return + * 0x7fff or 0x8000 rather than wrapping. + */ +u16 pmbus_data2reg_direct(s64 micro, int m, int b, int R); + +/* + * Dispatcher: pick the right reg2data_* helper based on the chip's + * pmbus_driver_info[class]. vout_mode is consulted only for + * VOLTAGE_OUT in linear format. For DIRECT, m/b/R are taken from + * info. For VID and IEEE754 the dispatcher returns 0 (formats + * not yet wired up; add when a consumer lands). + */ +s64 pmbus_reg2data(const struct pmbus_driver_info *info, + enum pmbus_sensor_classes class, + u16 raw, u8 vout_mode); + +/* + * Transport helpers. + * + * Thin wrappers over the U-Boot DM I2C primitives that handle PMBus + * framing details (little endian word layout, two stage block read, + * CLEAR_FAULTS pseudo command without payload). + */ + +/* Read a byte register. */ +int pmbus_read_byte(struct udevice *dev, u8 cmd, u8 *val); + +/* Read a 16 bit register, little endian on the wire. */ +int pmbus_read_word(struct udevice *dev, u8 cmd, u16 *val); + +/* Write a byte register. */ +int pmbus_write_byte(struct udevice *dev, u8 cmd, u8 val); + +/* Write a 16 bit register, little endian on the wire. */ +int pmbus_write_word(struct udevice *dev, u8 cmd, u16 val); + +/* + * Block read of a vendor string register (MFR_ID, MFR_MODEL, + * MFR_REVISION). The first wire byte is the payload length; the + * helper does the second read for the payload itself, so even strict + * I2C controllers (which forbid over read on block transactions) + * work. Output is null terminated and printable only (non printable + * bytes are substituted with '.'). + * + * reverse_bytes: some MPS NVM personalities store ASCII strings + * LSB first (chip returns "SPM" for the human string "MPS"); pass + * true to reverse on copy. Spec compliant chips pass false. + * + * Returns string length on success or a negative errno on bus error + * or invalid length byte. outsz must be at least 2. + */ +int pmbus_read_string(struct udevice *dev, u8 cmd, char *out, int outsz, + bool reverse_bytes); + +/* Issue a CLEAR_FAULTS (03h) write. Clears RAM sticky STATUS_*. */ +int pmbus_clear_faults(struct udevice *dev); + +/* + * Capability probe: is a word sized command implemented by the chip? + * + * Primary signal is the bus NAK -- compliant parts do not ACK a + * command they do not implement, so the word read fails. Secondary + * signal is a clean -> dirty transition of STATUS_CML[INVALID_COMMAND] + * across the read (a chip that ACKs but does not implement the + * register raises it). Non destructive: never issues CLEAR_FAULTS, so + * sticky fault history survives for a subsequent pmbus status; a + * pre existing CML fault disables the secondary signal so it cannot + * produce a false "unsupported". + * + * Returns true if the command appears supported, false otherwise. + */ +bool pmbus_word_command_supported(struct udevice *dev, u8 reg); + +/* + * High level snapshot printers shared by the pmbus CLI and board + * boot time diagnostics. Both operate on the current pmbus_active() + * device (select it first via pmbus_set_active()); chip is the I2C + * handle from pmbus_active_get_i2c() / the CLI's require_active(). + * + * pmbus_print_telemetry: decodes VIN / VOUT / IIN / IOUT / POUT / TEMP + * through the active device's pmbus_driver_info (LINEAR / DIRECT / VID + * per VOUT_MODE and per class format), skipping commands the chip does + * not implement (pmbus_word_command_supported). Falls back to + * LINEAR16 / LINEAR11 when no driver_info is cached. Caller prints the + * header line. + * + * pmbus_print_status_word: reads + decodes STATUS_WORD with the active + * device's chip specific status_overrides, if any. + */ +void pmbus_print_telemetry(struct udevice *chip); +void pmbus_print_status_word(struct udevice *chip); + +/* + * 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 <bus>:<addr> (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. + * + * Sparse mask to name table. pmbus_print_bits() emits only the bits + * that are SET in v, joined by |; if no bit is set, prints + * clean. Bits not in the table are silently ignored (RESERVED bits + * or chip specific bits handled by a separate per chip table). + */ +struct pmbus_bit { + u16 mask; + const char *name; +}; + +void pmbus_print_bits(u16 v, const struct pmbus_bit *tab); + +/* + * Per chip override entry. When a chip reuses a PMBus standard + * STATUS bit for a documented chip specific signal (for example MPS + * uses STATUS_WORD bit[12] = MFR_SPECIFIC as NVM_SUMMARY, bit[8] = + * UNKNOWN as WATCH_DOG, bit[0] = NONE_ABOVE as DRMOS_FAULT), the + * per chip driver supplies a sparse table of (reg, mask, name) + * triples and pmbus_print_status_bits() substitutes the chip name + * for the standard one when the bit is set. + * + * reg is one of PMBUS_STATUS_WORD / VOUT / IOUT / INPUT / + * TEMPERATURE / CML, so the same table can carry overrides for + * every status register on the chip in one place. Tables that omit + * a (reg, mask) leave the standard name in place. + * + * Override entries whose mask is NOT in the standard table are + * still printed (the chip can extend coverage beyond PMBus 1.x for + * vendor specific bits in standard registers). + * + * Tables are NULL terminated: the last entry has .name = NULL. + * Following the same convention U-Boot uses for struct udevice_id + * and other driver tables avoids the explicit-count foot-gun. + */ +struct pmbus_status_override { + u8 reg; + u16 mask; + const char *name; +}; + +/* + * Print the bit names of a STATUS_* register value. For each bit + * set in v, prefer a chip override matching (reg, mask) over the + * standard std table entry; if neither matches, the bit is + * silently skipped (RESERVED). If no bit is set at all, prints + * clean. Pass ovr = NULL to disable the override path. + */ +void pmbus_print_status_bits(u8 reg, u16 v, + const struct pmbus_bit *std, + const struct pmbus_status_override *ovr); + +/* + * Built in PMBus 1.3 standard bit tables (use these from per chip + * drivers and board diagnostics; vendor extensions go in chip local + * tables that the per chip driver passes alongside these). + */ +/* + * All tables below are NULL terminated (last entry has .name = NULL), + * so callers walk with for (t = tab; t && t->name; t++) and the + * helpers above need no count argument. + */ +extern const struct pmbus_bit pmbus_status_word_bits[]; +extern const struct pmbus_bit pmbus_status_vout_bits[]; +extern const struct pmbus_bit pmbus_status_iout_bits[]; +extern const struct pmbus_bit pmbus_status_input_bits[]; +extern const struct pmbus_bit pmbus_status_temp_bits[]; +extern const struct pmbus_bit pmbus_status_cml_bits[]; + +/* + * Active device tracking for the pmbus U-Boot CLI. + * + * The framework keeps one active PMBus device. It is selected by + * pmbus dev <bus>:<addr> (raw I2C tuple) and remembered across + * subcommands so subsequent invocations of pmbus telemetry, + * pmbus status, pmbus dump, etc. operate on the same chip + * without re-typing the address. + * + * pmbus_set_active() probes the chip's MFR_ID at the given address, + * looks the result up in the chip-match registry (populated by + * per chip drivers via pmbus_register_chip()), and caches the + * resulting struct pmbus_driver_info. Subcommands consult + * pmbus_active() to find the cached metadata. + */ +struct pmbus_active_dev { + bool valid; + int bus_seq; + u8 addr; + char vendor[PMBUS_VENDOR_NAME_MAX]; + char name[PMBUS_REGULATOR_NAME_MAX]; /* DT regulator-name when bound; "" otherwise */ + char mfr_id[PMBUS_MFR_STRING_MAX]; + char mfr_model[PMBUS_MFR_STRING_MAX]; + char mfr_revision[PMBUS_MFR_STRING_MAX]; + 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 <name> CLI alias so a chip bound through DT can be + * selected by its human readable rail name (e.g. "+0V8_VDD") + * instead of the i2c bus / address pair. Returns 0 on success + * (out parameters populated and the regulator probed), or a + * negative errno if no match is found or the bus / address cannot + * be derived. Available only when CONFIG_DM_REGULATOR is set. + */ +int pmbus_resolve_by_name(const char *name, int *bus_seq, u8 *addr); + +/* + * Vendor extension dispatcher. + * + * When the user types pmbus <vendor> <args...>, the framework + * looks up the registered handler for <vendor> and calls it with + * the argv tail (argv[0] = "<vendor>"). The handler operates on + * pmbus_active(), or returns CMD_RET_USAGE if the active device is + * not from this vendor. + * + * Per chip drivers register their vendor handler at init time. The + * MPS extension publishes pmbus mps last, pmbus mps clear last, + * and pmbus mps clear force. + */ +typedef int (*pmbus_vendor_handler_t)(struct cmd_tbl *cmdtp, int flag, + int argc, char *const argv[]); + +struct pmbus_vendor_op { + const char *vendor; + pmbus_vendor_handler_t handler; + const char *help; +}; + +int pmbus_register_vendor_handler(const struct pmbus_vendor_op *op); +const struct pmbus_vendor_op *pmbus_lookup_vendor(const char *vendor); +unsigned int pmbus_vendor_count(void); +const struct pmbus_vendor_op *pmbus_vendor_at(unsigned int i); + +#endif /* _PMBUS_H_ */ diff --git a/include/power/regulator.h b/include/power/regulator.h index 4011fb1d254..8d4d1450b90 100644 --- a/include/power/regulator.h +++ b/include/power/regulator.h @@ -316,6 +316,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 * @@ -359,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 @@ -550,6 +559,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; @@ -633,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_ */ 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/lib/Kconfig b/lib/Kconfig index 9e0f0ad7d06..24e55ade4d3 100644 --- a/lib/Kconfig +++ b/lib/Kconfig @@ -283,6 +283,22 @@ config PANIC_HANG development since you can try to debug the conditions that lead to the situation. +config PMBUS + bool "PMBus 1.x decoder and transport helpers" + depends on DM_I2C + help + Enable include/pmbus.h and lib/pmbus.c: standard PMBus 1.x + command codes, LINEAR11, LINEAR16, and DIRECT numeric format + decoders, the two stage SMBus block read helper used for + MFR_ID, MFR_MODEL, and MFR_REVISION reads, and the table + driven STATUS_* bit print helper. + + This is the substrate for any pre kernel PMBus consumer in + U-Boot (board/nxp/common/vid.c rail trim, board local telemetry + diagnostics, future per chip regulator drivers under + drivers/power/regulator/). It is not a hwmon clone. See + doc/develop/pmbus.rst for the policy notes. + config REGEX bool "Enable regular expression support" default y if NET_LEGACY diff --git a/lib/Makefile b/lib/Makefile index d0ffabc2b47..222378a8531 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -53,6 +53,7 @@ obj-y += rc4.o obj-$(CONFIG_RBTREE) += rbtree.o obj-$(CONFIG_BITREVERSE) += bitrev.o obj-y += list_sort.o +obj-$(CONFIG_PMBUS) += pmbus.o endif obj-$(CONFIG_$(PHASE_)TPM) += tpm-common.o diff --git a/lib/pmbus.c b/lib/pmbus.c new file mode 100644 index 00000000000..62a66593638 --- /dev/null +++ b/lib/pmbus.c @@ -0,0 +1,877 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2026 Free Mobile, Vincent Jardin + * + * PMBus 1.x decoders, transport helpers, and standard status bit + * tables for U-Boot. See include/pmbus.h for the API surface and + * doc/develop/pmbus.rst for the porting guide. + * + * Decoder math is implemented from the PMBus 1.3 specification: + * 1. Part I (transport): see + * doc/PMBus/PMBus_Specification_Rev_1_3_1_Part_I_20150313.{pdf,txt} + * 2. Part II (commands): see + * doc/PMBus/PMBus_Specification_Rev_1_3_1_Part_II_20150313.{pdf,txt} + * + * Reference Linux implementation: linux/drivers/hwmon/pmbus/pmbus_core.c + * (the kernel side `struct pmbus_data` caching and hwmon publication + * layers do not apply; only the arithmetic carries over). + * + * No code in this file may reference a specific board, SoC, or + * product. Per chip quirks (MPS DIRECT format LSBs, vendor registers, + * VID coercion, ADDR_VBOOT auto promotion, and the like) belong in + * per chip drivers under drivers/power/regulator/ or in board local + * files under board/<vendor>/<board>/. + */ + +#include <ctype.h> +#include <dm.h> +#include <i2c.h> +#include <log.h> +#include <pmbus.h> +#include <limits.h> +#include <linux/bitops.h> +#include <power/regulator.h> + +static int pmbus_sign_extend(unsigned int v, unsigned int width) +{ + unsigned int mask = (1U << width) - 1U; + unsigned int sign = 1U << (width - 1U); + + v &= mask; + if (v & sign) + v |= ~mask; + return (int)v; +} + +s64 pmbus_reg2data_linear11(u16 raw) +{ + int mantissa = pmbus_sign_extend(raw & PB_LINEAR11_MANT_MASK, + PB_LINEAR11_MANT_BITS); + int exponent = pmbus_sign_extend((raw >> PB_LINEAR11_EXP_SHIFT) & + PB_LINEAR11_EXP_MASK, + PB_LINEAR11_EXP_BITS); + s64 micro; + + /* Engineering value = mantissa * 2^exponent, scaled to micro units. */ + micro = (s64)mantissa * 1000000LL; + if (exponent >= 0) + micro <<= exponent; + else + micro >>= -exponent; + return micro; +} + +s64 pmbus_reg2data_linear16(u16 raw, u8 vout_mode) +{ + int exponent; + s64 micro; + + /* + * VOUT_MODE bits[7:5] = mode; bits[4:0] = parameter. Linear mode + * (000) treats bits[4:0] as the signed 5-bit exponent. For other + * modes the caller must dispatch elsewhere. + */ + if ((vout_mode & PB_VOUT_MODE_MODE_MASK) != PB_VOUT_MODE_LINEAR) + return 0; + + exponent = pmbus_sign_extend(vout_mode & PB_VOUT_MODE_PARAM_MASK, 5); + + /* Mantissa is unsigned 16-bit; scale to micro units. */ + micro = (s64)raw * 1000000LL; + if (exponent >= 0) + micro <<= exponent; + else + micro >>= -exponent; + return micro; +} + +s64 pmbus_reg2data_direct(s16 raw, int m, int b, int R) +{ + s64 acc; + + if (m == 0) + return 0; + + /* + * PMBus Part II sec 8.4: Y = (1/m) * (X * 10^-R - b) + * + * Pre scale acc to micro units so the final integer division by + * m absorbs the rounding loss into the least significant micro + * digit rather than into a coarser place. + */ + acc = (s64)raw * 1000000LL; + + /* Apply 10^-R: positive R means divide; negative R means multiply. */ + while (R > 0) { + acc /= 10; + R--; + } + while (R < 0) { + acc *= 10; + R++; + } + + /* Subtract the offset b, also in micro units. */ + acc -= (s64)b * 1000000LL; + + /* Final: divide by m. */ + acc /= m; + return acc; +} + +u16 pmbus_data2reg_linear16(s64 micro, u8 vout_mode) +{ + int exponent; + s64 raw; + + if ((vout_mode & PB_VOUT_MODE_MODE_MASK) != PB_VOUT_MODE_LINEAR) + return 0; + + exponent = pmbus_sign_extend(vout_mode & PB_VOUT_MODE_PARAM_MASK, 5); + + /* raw = micro / (2^exponent * 10^6). */ + raw = micro; + if (exponent >= 0) + raw >>= exponent; + else + raw <<= -exponent; + raw /= 1000000LL; + + if (raw < 0) + raw = 0; + if (raw > U16_MAX) + raw = U16_MAX; + return (u16)raw; +} + +u16 pmbus_data2reg_direct(s64 micro, int m, int b, int R) +{ + s64 acc; + + if (m == 0) + return 0; + + /* + * Inverse of pmbus_reg2data_direct(): X = (m * Y + b) * 10^R. + * Work in micro units throughout: acc = m * Y_micro + b * 10^6, + * then scale by 10^R, finally divide by 10^6 to get the raw + * chip count. Order chosen to match the decoder's quantisation + * pattern so a round trip (data2reg then reg2data) returns the + * input within +/- one LSB. + */ + acc = (s64)m * micro + (s64)b * 1000000LL; + + while (R > 0) { + acc *= 10; + R--; + } + while (R < 0) { + acc /= 10; + R++; + } + + acc /= 1000000LL; + + /* PMBus 1.3 Part II sec 8.4 mandates a signed 16 bit raw value. */ + if (acc > S16_MAX) + acc = S16_MAX; + if (acc < S16_MIN) + acc = S16_MIN; + return (u16)(s16)acc; +} + +s64 pmbus_reg2data(const struct pmbus_driver_info *info, + enum pmbus_sensor_classes class, + u16 raw, u8 vout_mode) +{ + if (!info || class >= PSC_NUM_CLASSES) + return 0; + + switch (info->format[class]) { + case pmbus_fmt_linear: + if (class == PSC_VOLTAGE_OUT) + return pmbus_reg2data_linear16(raw, vout_mode); + return pmbus_reg2data_linear11(raw); + + case pmbus_fmt_direct: + return pmbus_reg2data_direct((s16)raw, + info->m[class], + info->b[class], + info->R[class]); + + case pmbus_fmt_vid: + case pmbus_fmt_ieee754: + /* + * Not yet wired up. Add when a consumer lands. VID needs + * the per page vrm_version table from the kernel's + * pmbus_reg2data_vid(); IEEE754 needs the half precision + * decoder from pmbus_reg2data_ieee754(). + */ + return 0; + } + + return 0; +} + +int pmbus_read_byte(struct udevice *dev, u8 cmd, u8 *val) +{ + return dm_i2c_read(dev, cmd, val, 1); +} + +int pmbus_read_word(struct udevice *dev, u8 cmd, u16 *val) +{ + u8 raw[2]; + int ret; + + ret = dm_i2c_read(dev, cmd, raw, 2); + if (ret) + return ret; + *val = (u16)raw[0] | ((u16)raw[1] << 8); + return 0; +} + +int pmbus_write_byte(struct udevice *dev, u8 cmd, u8 val) +{ + return dm_i2c_write(dev, cmd, &val, 1); +} + +int pmbus_write_word(struct udevice *dev, u8 cmd, u16 val) +{ + u8 raw[2]; + + raw[0] = (u8)(val & 0xff); /* PMBus words are little-endian */ + raw[1] = (u8)(val >> 8); + return dm_i2c_write(dev, cmd, raw, 2); +} + +int pmbus_read_string(struct udevice *dev, u8 cmd, char *out, int outsz, + bool reverse_bytes) +{ + u8 raw[PMBUS_MFR_STRING_MAX + 1]; /* length byte + payload */ + int ret, len, i; + + if (outsz < 2) + return -EINVAL; + + /* Stage 1: read the length byte. */ + ret = dm_i2c_read(dev, cmd, raw, 1); + if (ret) + return ret; + + len = raw[0]; + if (len <= 0 || len > (int)sizeof(raw) - 1) + return -EBADMSG; + if (len > outsz - 1) + len = outsz - 1; + + /* Stage 2: reread length + payload (some controllers mandate this). */ + ret = dm_i2c_read(dev, cmd, raw, len + 1); + if (ret) + return ret; + + if (reverse_bytes) { + for (i = 0; i < len; i++) { + u8 b = raw[len - i]; + + out[i] = isprint(b) ? (char)b : '.'; + } + } else { + for (i = 0; i < len; i++) { + u8 b = raw[i + 1]; + + out[i] = isprint(b) ? (char)b : '.'; + } + } + out[len] = '\0'; + return len; +} + +int pmbus_clear_faults(struct udevice *dev) +{ + return dm_i2c_write(dev, PMBUS_CLEAR_FAULTS, NULL, 0); +} + +void pmbus_print_bits(u16 v, const struct pmbus_bit *tab) +{ + const struct pmbus_bit *t; + int first = 1; + + if (v == 0) { + printf("clean"); + return; + } + for (t = tab; t && t->name; t++) { + if (v & t->mask) { + printf("%s%s", first ? "" : "|", t->name); + first = 0; + } + } +} + +void pmbus_print_status_bits(u8 reg, u16 v, + const struct pmbus_bit *std, + const struct pmbus_status_override *ovr) +{ + const struct pmbus_bit *s; + const struct pmbus_status_override *o; + int first = 1; + + if (v == 0) { + printf("clean"); + return; + } + + /* + * Pass 1: walk the standard table in declared order so the + * printout retains the conventional bit-15-first ordering. For + * each set bit, prefer a chip override matching (reg, mask). + */ + for (s = std; s && s->name; s++) { + const char *name; + + if (!(v & s->mask)) + continue; + name = s->name; + for (o = ovr; o && o->name; o++) { + if (o->reg == reg && o->mask == s->mask) { + name = o->name; + break; + } + } + printf("%s%s", first ? "" : "|", name); + first = 0; + } + + /* + * Pass 2: print overrides whose mask is not in the standard + * table at all (chip-specific bit at a position the spec + * leaves RESERVED). These would otherwise be swallowed. + */ + for (o = ovr; o && o->name; o++) { + bool in_std = false; + + if (o->reg != reg) + continue; + if (!(v & o->mask)) + continue; + for (s = std; s && s->name; s++) { + if (s->mask == o->mask) { + in_std = true; + break; + } + } + if (in_std) + continue; + printf("%s%s", first ? "" : "|", o->name); + first = 0; + } +} + +/* + * Standard PMBus 1.3 status bit tables. Per-chip drivers may publish + * their own tables for vendor extended bits (e.g. NVM summary bits, + * DR MOS faults) but the standard layout below is the safe baseline. + * + * All tables are NULL terminated (`name = NULL` sentinel), matching + * the convention used elsewhere in U-Boot for driver tables. + */ +const struct pmbus_bit pmbus_status_word_bits[] = { + { PB_STATUS_VOUT, "VOUT" }, + { PB_STATUS_IOUT_POUT, "IOUT_POUT" }, + { PB_STATUS_INPUT, "INPUT" }, + { PB_STATUS_WORD_MFR, "MFR" }, + { PB_STATUS_POWER_GOOD_N, "PG#" }, + { PB_STATUS_FANS, "FANS" }, + { PB_STATUS_OTHER, "OTHER" }, + { PB_STATUS_UNKNOWN, "UNKNOWN" }, + { PB_STATUS_BUSY, "BUSY" }, + { PB_STATUS_OFF, "OFF" }, + { PB_STATUS_VOUT_OV, "VOUT_OV" }, + { PB_STATUS_IOUT_OC, "IOUT_OC" }, + { PB_STATUS_VIN_UV, "VIN_UV" }, + { PB_STATUS_TEMPERATURE, "TEMP" }, + { PB_STATUS_CML, "CML" }, + { PB_STATUS_NONE_ABOVE, "NONE_ABOVE" }, + { /* sentinel */ } +}; + +const struct pmbus_bit pmbus_status_vout_bits[] = { + { PB_VOLTAGE_OV_FAULT, "VOUT_OV_FAULT" }, + { PB_VOLTAGE_OV_WARNING, "VOUT_OV_WARN" }, + { PB_VOLTAGE_UV_WARNING, "VOUT_UV_WARN" }, + { PB_VOLTAGE_UV_FAULT, "VOUT_UV_FAULT" }, + { PB_VOLTAGE_VOUT_MAX_MIN_WARN, "VOUT_MAX_MIN_WARN" }, + { /* sentinel */ } +}; + +const struct pmbus_bit pmbus_status_iout_bits[] = { + { PB_IOUT_OC_FAULT, "IOUT_OC_FAULT" }, + { PB_IOUT_OC_LV_FAULT, "IOUT_OC_LV_FAULT" }, + { PB_IOUT_OC_WARNING, "IOUT_OC_WARN" }, + { PB_IOUT_UC_FAULT, "IOUT_UC_FAULT" }, + { PB_CURRENT_SHARE_FAULT, "ISHARE_FAULT" }, + { PB_POWER_LIMITING, "POWER_LIMITING" }, + { PB_POUT_OP_FAULT, "POUT_OP_FAULT" }, + { PB_POUT_OP_WARNING, "POUT_OP_WARN" }, + { /* sentinel */ } +}; + +const struct pmbus_bit pmbus_status_input_bits[] = { + { PB_IIN_OC_FAULT, "IIN_OC_FAULT" }, + { PB_IIN_OC_WARNING, "IIN_OC_WARN" }, + { PB_PIN_OP_WARNING, "PIN_OP_WARN" }, + { /* sentinel */ } +}; + +const struct pmbus_bit pmbus_status_temp_bits[] = { + { PB_TEMP_OT_FAULT, "OT_FAULT" }, + { PB_TEMP_OT_WARNING, "OT_WARN" }, + { PB_TEMP_UT_WARNING, "UT_WARN" }, + { PB_TEMP_UT_FAULT, "UT_FAULT" }, + { /* sentinel */ } +}; + +const struct pmbus_bit pmbus_status_cml_bits[] = { + { PB_CML_FAULT_INVALID_COMMAND, "INVALID_CMD" }, + { PB_CML_FAULT_INVALID_DATA, "INVALID_DATA" }, + { PB_CML_FAULT_PACKET_ERROR, "PEC" }, + { PB_CML_FAULT_MEMORY, "MEM" }, + { PB_CML_FAULT_PROCESSOR, "PROC" }, + { PB_CML_FAULT_OTHER_COMM, "OTHER_COMM" }, + { PB_CML_FAULT_OTHER_MEM_LOGIC, "OTHER_MEM_LOGIC" }, + { /* sentinel */ } +}; + +/* + * Active device tracking + chip / vendor registries (consumed by the + * `pmbus` U-Boot CLI command in cmd/pmbus.c). + */ + +#define PMBUS_MAX_CHIP_MATCHES 8 +#define PMBUS_MAX_VENDOR_HANDLERS 4 + +static struct pmbus_active_dev pmbus_active_state; +static const struct pmbus_chip_match *pmbus_chip_table[PMBUS_MAX_CHIP_MATCHES]; +static unsigned int pmbus_chip_table_n; +static const struct pmbus_vendor_op *pmbus_vendor_table[PMBUS_MAX_VENDOR_HANDLERS]; +static unsigned int pmbus_vendor_table_n; + +const struct pmbus_active_dev *pmbus_active(void) +{ + return pmbus_active_state.valid ? &pmbus_active_state : NULL; +} + +void pmbus_clear_active(void) +{ + memset(&pmbus_active_state, 0, sizeof(pmbus_active_state)); +} + +int pmbus_active_get_i2c(struct udevice **i2c_dev) +{ + struct udevice *bus; + int ret; + + if (!pmbus_active_state.valid) + return -ENODEV; + ret = uclass_get_device_by_seq(UCLASS_I2C, pmbus_active_state.bus_seq, &bus); + if (ret) + return ret; + return i2c_get_chip(bus, pmbus_active_state.addr, 1, i2c_dev); +} + +/* engineering value in micro units -> "I.FFF<unit>" (3 fractional digits) */ +static void pmbus_emit_micro(s64 micro, const char *unit) +{ + s64 abs_milli = (micro < 0 ? -micro : micro) / 1000LL; + + printf("%lld.%03lld%s", (long long)(micro / 1000000LL), + (long long)(abs_milli % 1000LL), unit); +} + +struct pmbus_telem_entry { + u8 reg; + const char *label; + enum pmbus_sensor_classes class; + const char *unit; +}; + +/* + * Telemetry register set, in print order. POUT is included so PSU + * class parts report input/output power; chips that do not implement + * a given command are skipped via pmbus_word_command_supported(). + */ +static const struct pmbus_telem_entry pmbus_telem_table[] = { + { PMBUS_READ_VIN, "VIN ", PSC_VOLTAGE_IN, "V" }, + { PMBUS_READ_VOUT, "VOUT", PSC_VOLTAGE_OUT, "V" }, + { PMBUS_READ_IIN, "IIN ", PSC_CURRENT_IN, "A" }, + { PMBUS_READ_IOUT, "IOUT", PSC_CURRENT_OUT, "A" }, + { PMBUS_READ_POUT, "POUT", PSC_POWER, "W" }, + { PMBUS_READ_TEMPERATURE_1, "TEMP", PSC_TEMPERATURE, "C" }, +}; + +bool pmbus_word_command_supported(struct udevice *dev, u8 reg) +{ + u8 cml_before = 0, cml_after = 0; + bool have_cml; + u16 w; + + have_cml = !pmbus_read_byte(dev, PMBUS_STATUS_CML, &cml_before); + + if (pmbus_read_word(dev, reg, &w)) + return false; /* NAK: unsupported command not ACKed */ + + if (have_cml && !(cml_before & PB_CML_FAULT_INVALID_COMMAND) && + !pmbus_read_byte(dev, PMBUS_STATUS_CML, &cml_after) && + (cml_after & PB_CML_FAULT_INVALID_COMMAND)) + return false; /* ACKed but chip raised INVALID_COMMAND */ + + return true; +} + +/* Telemetry of the currently selected page. */ +static void pmbus_print_telemetry_page(struct udevice *chip, + const struct pmbus_active_dev *act) +{ + u8 vout_mode = 0; + unsigned int i; + + /* + * On a read failure vout_mode stays 0 (LINEAR, exponent 0). That is + * a silent mis-scale of every VOLTAGE_OUT reading, so make the + * fallback visible rather than printing a wrong voltage as if good. + */ + if (pmbus_read_byte(chip, PMBUS_VOUT_MODE, &vout_mode)) + printf(" (VOUT_MODE read failed; VOUT decode assumes LINEAR exp 0)\n"); + + for (i = 0; i < ARRAY_SIZE(pmbus_telem_table); i++) { + const struct pmbus_telem_entry *e = &pmbus_telem_table[i]; + u16 raw = 0; + + /* + * Class gating. A chip driver that declares classes_present + * lists exactly the sensors it implements (kernel-style + * per-chip sensor set), so unlisted classes are skipped + * silently -- this is what hides the MPS buck's uncalibrated + * POUT / IIN. A generic / undeclared device instead gets a + * live capability probe per class. + */ + if (act->info && act->info->classes_present) { + if (!(act->info->classes_present & BIT(e->class))) + continue; + } else if (!pmbus_word_command_supported(chip, e->reg)) { + printf(" %s : (not supported)\n", e->label); + continue; + } + + if (pmbus_read_word(chip, e->reg, &raw)) { + printf(" %s : (read failed)\n", e->label); + continue; + } + + printf(" %s : raw=0x%04x ", e->label, raw); + if (act->info) { + u16 dec = raw; + + /* + * Some DIRECT format parts (e.g. MPS) report + * temperature as 1 degC/LSB in the low byte only; + * mask there. LINEAR temperatures use all 16 bits + * and must NOT be masked. + */ + if (e->class == PSC_TEMPERATURE && + act->info->format[PSC_TEMPERATURE] == pmbus_fmt_direct) + dec = raw & 0x00ff; + + pmbus_emit_micro(pmbus_reg2data(act->info, e->class, + dec, vout_mode), + e->unit); + } else if (e->class == PSC_VOLTAGE_OUT) { + pmbus_emit_micro(pmbus_reg2data_linear16(raw, vout_mode), + e->unit); + } else { + pmbus_emit_micro(pmbus_reg2data_linear11(raw), e->unit); + printf(" (LINEAR11 fallback)"); + } + printf("\n"); + } +} + +void pmbus_print_telemetry(struct udevice *chip) +{ + const struct pmbus_active_dev *act = pmbus_active(); + int npages, p; + u8 zero = 0; + + if (!act) + return; + + /* + * Multi-rail parts (PSU bricks) expose one rail per PMBUS_PAGE. + * Chip drivers set pmbus_driver_info.pages; the generic driver + * takes it from the DT `pmbus,num-pages` (default 1). We always + * write PMBUS_PAGE before reading a page -- including page 0 -- + * because a device may power up selected on a different page, which + * is what made the 48V PSU read all-zeros before. Only valid pages + * (0..npages-1) are ever written, so we never induce the + * out-of-range-PAGE STATUS_CML fault and the device's sticky fault + * log is left untouched (no CLEAR_FAULTS, no scrubbing). + */ + npages = (act->info && act->info->pages > 0) ? act->info->pages : 1; + + for (p = 0; p < npages; p++) { + u8 pg = (u8)p; + + if (dm_i2c_write(chip, PMBUS_PAGE, &pg, 1)) { + printf(" [page %d] PAGE select failed\n", p); + continue; + } + if (npages > 1) + printf(" [page %d]\n", p); + pmbus_print_telemetry_page(chip, act); + } + + if (npages > 1) + dm_i2c_write(chip, PMBUS_PAGE, &zero, 1); /* leave on page 0 */ +} + +void pmbus_print_status_word(struct udevice *chip) +{ + const struct pmbus_active_dev *act = pmbus_active(); + const struct pmbus_status_override *ovr = + (act && act->info) ? act->info->status_overrides : NULL; + u16 word = 0; + + if (pmbus_read_word(chip, PMBUS_STATUS_WORD, &word)) { + printf(" STATUS_WORD (79h) = (read failed)\n"); + return; + } + printf(" STATUS_WORD (79h) = 0x%04x [", word); + pmbus_print_status_bits(PMBUS_STATUS_WORD, word, + pmbus_status_word_bits, ovr); + printf("]\n"); +} + +static const struct pmbus_chip_match *pmbus_match_mfr(const char *id) +{ + unsigned int i; + + if (!id || !id[0]) + return NULL; + for (i = 0; i < pmbus_chip_table_n; i++) { + const struct pmbus_chip_match *m = pmbus_chip_table[i]; + size_t plen = strlen(m->mfr_id); + + if (strlen(id) >= plen && !strncmp(id, m->mfr_id, plen)) + return m; + } + return NULL; +} + +/* + * Walk UCLASS_REGULATOR looking for a regulator whose I2C parent + * bus seq + DT reg address match the requested (bus_seq, addr). + * Returns the regulator-name (uclass plat .name) on hit, or NULL if + * no UCLASS_REGULATOR device matches (chip not bound through DT, or + * CONFIG_DM_REGULATOR disabled). + */ +static const char *pmbus_lookup_regname(int bus_seq, u8 addr) +{ + struct uclass *uc; + struct udevice *r; + + if (!IS_ENABLED(CONFIG_DM_REGULATOR)) + return NULL; + + if (uclass_get(UCLASS_REGULATOR, &uc)) + return NULL; + uclass_foreach_dev(r, uc) { + struct dm_regulator_uclass_plat *up; + struct udevice *parent = dev_get_parent(r); + int ra; + + if (!parent || device_get_uclass_id(parent) != UCLASS_I2C) + continue; + if (dev_seq(parent) != bus_seq) + continue; + ra = dev_read_addr(r); + if (ra < 0 || (u8)ra != addr) + continue; + up = dev_get_uclass_plat(r); + if (up && up->name) + return up->name; + return r->name; + } + return NULL; +} + +int pmbus_set_active(int bus_seq, u8 addr) +{ + const struct pmbus_chip_match *match = NULL; + struct udevice *bus, *chip; + char id_fwd[PMBUS_MFR_STRING_MAX] = ""; + char id_rev[PMBUS_MFR_STRING_MAX] = ""; + const char *rname; + int ret; + + pmbus_clear_active(); + + ret = uclass_get_device_by_seq(UCLASS_I2C, bus_seq, &bus); + if (ret) + return ret; + ret = i2c_get_chip(bus, addr, 1, &chip); + if (ret) + return ret; + + pmbus_active_state.bus_seq = bus_seq; + pmbus_active_state.addr = addr; + + /* + * Probe MFR_ID in both byte orders. Spec compliant chips return + * "MPS" / "TI" / etc. in the natural reading (forward); MPS NVM + * personalities store the string LSB first and need the reverse + * read. Chip table entries declare which one is canonical for + * the chip family they describe. + */ + if (pmbus_read_string(chip, PMBUS_MFR_ID, id_fwd, sizeof(id_fwd), false) < 0) + id_fwd[0] = '\0'; + if (pmbus_read_string(chip, PMBUS_MFR_ID, id_rev, sizeof(id_rev), true) < 0) + id_rev[0] = '\0'; + + match = pmbus_match_mfr(id_fwd); + if (match && !match->mfr_id_reverse) { + strlcpy(pmbus_active_state.mfr_id, id_fwd, + sizeof(pmbus_active_state.mfr_id)); + } else { + match = pmbus_match_mfr(id_rev); + if (match && match->mfr_id_reverse) { + strlcpy(pmbus_active_state.mfr_id, id_rev, + sizeof(pmbus_active_state.mfr_id)); + } else { + /* No registered match; cache the forward read as best effort. */ + strlcpy(pmbus_active_state.mfr_id, + id_fwd[0] ? id_fwd : id_rev, + sizeof(pmbus_active_state.mfr_id)); + } + } + + if (match) { + pmbus_active_state.info = match->info; + if (match->vendor) + strlcpy(pmbus_active_state.vendor, match->vendor, + sizeof(pmbus_active_state.vendor)); + } + + /* + * No MFR_ID chip-match (a spec compliant part with no per chip + * driver, e.g. a Flex / Delta PSU): if a generic / chip + * UCLASS_REGULATOR is bound at this address, reuse its + * VOUT_MODE detected driver_info so telemetry decodes through + * the right per class formats instead of the blanket + * LINEAR16 / LINEAR11 fallback. + */ + if (CONFIG_IS_ENABLED(DM_REGULATOR_PMBUS_HELPER) && + !pmbus_active_state.info) { + const struct pmbus_driver_info *di = + pmbus_regulator_info_by_addr(bus_seq, addr); + + if (di) + pmbus_active_state.info = di; + } + + /* + * MFR_MODEL / MFR_REVISION are best effort. Use the same byte + * order the matched chip declared; if nothing matched, use the + * forward order. + */ + { + bool reverse = match && match->mfr_id_reverse; + + pmbus_active_state.mfr_reverse = reverse; + pmbus_read_string(chip, PMBUS_MFR_MODEL, + pmbus_active_state.mfr_model, + sizeof(pmbus_active_state.mfr_model), reverse); + pmbus_read_string(chip, PMBUS_MFR_REVISION, + pmbus_active_state.mfr_revision, + sizeof(pmbus_active_state.mfr_revision), reverse); + } + + rname = pmbus_lookup_regname(bus_seq, addr); + if (rname) + strlcpy(pmbus_active_state.name, rname, + sizeof(pmbus_active_state.name)); + + pmbus_active_state.valid = true; + return 0; +} + +int pmbus_register_chip(const struct pmbus_chip_match *match) +{ + if (!match || !match->mfr_id) + return -EINVAL; + if (pmbus_chip_table_n >= PMBUS_MAX_CHIP_MATCHES) + return -ENOSPC; + pmbus_chip_table[pmbus_chip_table_n++] = match; + return 0; +} + +int pmbus_register_vendor_handler(const struct pmbus_vendor_op *op) +{ + if (!op || !op->vendor || !op->handler) + return -EINVAL; + if (pmbus_vendor_table_n >= PMBUS_MAX_VENDOR_HANDLERS) + return -ENOSPC; + pmbus_vendor_table[pmbus_vendor_table_n++] = op; + return 0; +} + +const struct pmbus_vendor_op *pmbus_lookup_vendor(const char *vendor) +{ + unsigned int i; + + if (!vendor) + return NULL; + for (i = 0; i < pmbus_vendor_table_n; i++) + if (!strcmp(pmbus_vendor_table[i]->vendor, vendor)) + return pmbus_vendor_table[i]; + return NULL; +} + +unsigned int pmbus_vendor_count(void) +{ + return pmbus_vendor_table_n; +} + +const struct pmbus_vendor_op *pmbus_vendor_at(unsigned int i) +{ + return i < pmbus_vendor_table_n ? pmbus_vendor_table[i] : NULL; +} + +int pmbus_resolve_by_name(const char *name, int *bus_seq, u8 *addr) +{ + struct udevice *reg; + struct udevice *parent; + int ret; + int a; + + if (!IS_ENABLED(CONFIG_DM_REGULATOR)) + return -ENOSYS; + + if (!name || !bus_seq || !addr) + return -EINVAL; + + ret = regulator_get_by_platname(name, ®); + if (ret) + return ret; + + parent = dev_get_parent(reg); + if (!parent || device_get_uclass_id(parent) != UCLASS_I2C) + return -ENODEV; + + a = dev_read_addr(reg); + if (a < 0 || a > 0x7f) + return -EINVAL; + + *bus_seq = dev_seq(parent); + *addr = (u8)a; + return 0; +} diff --git a/test/dm/Makefile b/test/dm/Makefile index 0e3c63568dd..76aa1fff9ba 100644 --- a/test/dm/Makefile +++ b/test/dm/Makefile @@ -95,6 +95,7 @@ obj-$(CONFIG_PINCONF) += pinmux.o endif obj-$(CONFIG_POWER_DOMAIN) += power-domain.o obj-$(CONFIG_ACPI_PMC) += pmc.o +obj-$(CONFIG_CMD_PMBUS) += pmbus.o obj-$(CONFIG_DM_PMIC) += pmic.o obj-$(CONFIG_DM_PWM) += pwm.o obj-$(CONFIG_ARM_FFA_TRANSPORT) += ffa.o diff --git a/test/dm/pmbus.c b/test/dm/pmbus.c new file mode 100644 index 00000000000..0184b201829 --- /dev/null +++ b/test/dm/pmbus.c @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Copyright 2026 Free Mobile - Vincent Jardin + * + * Unit tests for the PMBus 1.x framework, the generic + * PMBus regulator and the pmbus CLI command. + */ + +#include <dm.h> +#include <i2c.h> +#include <pmbus.h> +#include <dm/test.h> +#include <test/test.h> +#include <test/ut.h> + +/* The line pmbus dev prints when a chip is selected */ +#define PMBUS_ACTIVE_LINE \ + "pmbus: active i2c0:0x70 rail=\"sandbox-pmbus-vout\" " \ + "MFR_ID=\"SANDBOX\" MODEL=\"PMBUS-EMUL\" vendor=(generic)" + +/* The line pmbus list prints for the bound chip */ +#define PMBUS_LIST_LINE \ + " i2c0:0x70 rail=\"sandbox-pmbus-vout\" node=pmbus@70 " \ + "driver=pmbus_generic_regulator" + +/* Select the emulated chip and check the resulting banner line */ +static int pmbus_select(struct unit_test_state *uts) +{ + ut_assertok(run_command("pmbus dev 0:70", 0)); + ut_assert_nextline(PMBUS_ACTIVE_LINE); + return 0; +} + +/* The chip is reachable via UCLASS_REGULATOR (compatible = "pmbus") */ +static int dm_test_pmbus_bind(struct unit_test_state *uts) +{ + struct udevice *dev; + + ut_assertok(uclass_get_device_by_name(UCLASS_REGULATOR, "pmbus@70", + &dev)); + ut_asserteq_str("pmbus_generic_regulator", dev->driver->name); + ut_asserteq(UCLASS_I2C, device_get_uclass_id(dev_get_parent(dev))); + + return 0; +} + +DM_TEST(dm_test_pmbus_bind, UTF_SCAN_FDT); + +/* pmbus dev by <bus>:<addr> and by regulator-name select the chip */ +static int dm_test_pmbus_dev(struct unit_test_state *uts) +{ + ut_assertok(run_command("pmbus dev 0:70", 0)); + ut_assert_nextline(PMBUS_ACTIVE_LINE); + ut_assert_console_end(); + + /* Selecting by DT regulator-name resolves to the same chip */ + ut_assertok(run_command("pmbus dev sandbox-pmbus-vout", 0)); + ut_assert_nextline(PMBUS_ACTIVE_LINE); + ut_assert_console_end(); + + /* pmbus dev with no argument reprints the active chip */ + ut_assertok(run_command("pmbus dev", 0)); + ut_assert_nextline(PMBUS_ACTIVE_LINE); + ut_assert_console_end(); + + return 0; +} + +DM_TEST(dm_test_pmbus_dev, UTF_SCAN_FDT | UTF_CONSOLE); + +/* pmbus list enumerates the bound chip among the UCLASS_REGULATOR devices */ +static int dm_test_pmbus_list(struct unit_test_state *uts) +{ + ut_assertok(run_command("pmbus list", 0)); + ut_assert_skip_to_line(PMBUS_LIST_LINE); + + return 0; +} + +DM_TEST(dm_test_pmbus_list, UTF_SCAN_FDT | UTF_CONSOLE); + +/* pmbus info decodes identification + the detected driver_info */ +static int dm_test_pmbus_info(struct unit_test_state *uts) +{ + ut_assertok(pmbus_select(uts)); + + ut_assertok(run_command("pmbus info", 0)); + ut_assert_nextline("pmbus device i2c0:0x70"); + ut_assert_nextline(" regulator-name: \"sandbox-pmbus-vout\""); + ut_assert_nextline(" MFR_ID : \"SANDBOX\""); + ut_assert_nextline(" MFR_MODEL : \"PMBUS-EMUL\""); + ut_assert_nextline(" MFR_REVISION : \"1.0\" raw=0x312e30"); + ut_assert_nextline(" PMBUS_REVISION: 0x33 (PMBus 1.3)"); + ut_assert_nextline(" vendor : (none)"); + ut_assert_nextline(" driver_info : pages=1"); + ut_assert_nextline(" [VOLTAGE_IN ] format=LINEAR"); + ut_assert_nextline(" [VOLTAGE_OUT ] format=LINEAR"); + ut_assert_nextline(" [CURRENT_IN ] format=LINEAR"); + ut_assert_nextline(" [CURRENT_OUT ] format=LINEAR"); + ut_assert_nextline(" [POWER ] format=LINEAR"); + ut_assert_nextline(" [TEMPERATURE ] format=LINEAR"); + ut_assert_console_end(); + + return 0; +} + +DM_TEST(dm_test_pmbus_info, UTF_SCAN_FDT | UTF_CONSOLE); + +/* + * pmbus telemetry decodes the implemented sensors and prints + * "(not supported)" for the commands the emulator NAKs (READ_IIN, + * READ_POUT). LINEAR11 is used for VIN/IOUT/TEMP, LINEAR16 (with the + * VOUT_MODE 2^-8 exponent) for VOUT. + */ +static int dm_test_pmbus_telemetry(struct unit_test_state *uts) +{ + ut_assertok(pmbus_select(uts)); + + ut_assertok(run_command("pmbus telemetry", 0)); + ut_assert_nextline("pmbus telemetry @ i2c0:0x70"); + ut_assert_nextline(" VIN : raw=0x0abc 1400.000V"); + ut_assert_nextline(" VOUT : raw=0x0200 2.000V"); + ut_assert_nextline(" IIN : (not supported)"); + ut_assert_nextline(" IOUT : raw=0x0123 291.000A"); + ut_assert_nextline(" POUT : (not supported)"); + ut_assert_nextline(" TEMP : raw=0x0019 25.000C"); + ut_assert_console_end(); + + return 0; +} + +DM_TEST(dm_test_pmbus_telemetry, UTF_SCAN_FDT | UTF_CONSOLE); + +/* pmbus status decodes every STATUS_* register; the emulator is clean */ +static int dm_test_pmbus_status(struct unit_test_state *uts) +{ + ut_assertok(pmbus_select(uts)); + + ut_assertok(run_command("pmbus status", 0)); + ut_assert_nextline("pmbus status @ i2c0:0x70"); + ut_assert_nextline(" STATUS_WORD (79h) = 0x0000 [clean]"); + ut_assert_nextline(" STATUS_VOUT (7Ah) = 0x00 [clean]"); + ut_assert_nextline(" STATUS_IOUT (7Bh) = 0x00 [clean]"); + ut_assert_nextline(" STATUS_INPUT (7Ch) = 0x00 [clean]"); + ut_assert_nextline(" STATUS_TEMP (7Dh) = 0x00 [clean]"); + ut_assert_nextline(" STATUS_CML (7Eh) = 0x00 [clean]"); + ut_assert_console_end(); + + return 0; +} + +DM_TEST(dm_test_pmbus_status, UTF_SCAN_FDT | UTF_CONSOLE); + +/* pmbus read/pmbus write raw register access (byte, word, string) */ +static int dm_test_pmbus_read_write(struct unit_test_state *uts) +{ + ut_assertok(pmbus_select(uts)); + + /* Symbolic and numeric register names both resolve */ + ut_assertok(run_command("pmbus read VOUT_MODE", 0)); + ut_assert_nextline(" 20h VOUT_MODE b=0x18"); + ut_assertok(run_command("pmbus read 8b w", 0)); + ut_assert_nextline(" 8bh READ_VOUT w=0x0200"); + ut_assertok(run_command("pmbus read MFR_ID s", 0)); + ut_assert_nextline(" 99h MFR_ID s=\"SANDBOX\""); + + /* A word write is observable on the next read-back */ + ut_assertok(run_command("pmbus write VOUT_COMMAND 123 w", 0)); + ut_assert_nextline("pmbus: wrote 0x123 to 21h (VOUT_COMMAND)"); + ut_assertok(run_command("pmbus read VOUT_COMMAND w", 0)); + ut_assert_nextline(" 21h VOUT_COMMAND w=0x0123"); + ut_assert_console_end(); + + return 0; +} + +DM_TEST(dm_test_pmbus_read_write, UTF_SCAN_FDT | UTF_CONSOLE); + +/* pmbus vout reads back VOUT via the active driver_info decoder */ +static int dm_test_pmbus_vout(struct unit_test_state *uts) +{ + ut_assertok(pmbus_select(uts)); + + ut_assertok(run_command("pmbus vout", 0)); + ut_assert_nextline("pmbus VOUT @ i2c0:0x70 raw=0x0200 2.000V"); + ut_assert_console_end(); + + return 0; +} + +DM_TEST(dm_test_pmbus_vout, UTF_SCAN_FDT | UTF_CONSOLE); + +/* pmbus dump walks every standard register; spot-check one line */ +static int dm_test_pmbus_dump(struct unit_test_state *uts) +{ + ut_assertok(pmbus_select(uts)); + + ut_assertok(run_command("pmbus dump", 0)); + ut_assert_nextline("pmbus dump @ i2c0:0x70 (registers known to <pmbus.h>)"); + ut_assert_skip_to_line(" 20h VOUT_MODE b=0x18"); + + return 0; +} + +DM_TEST(dm_test_pmbus_dump, UTF_SCAN_FDT | UTF_CONSOLE); + +/* pmbus clear [faults] issues CLEAR_FAULTS (03h) */ +static int dm_test_pmbus_clear(struct unit_test_state *uts) +{ + ut_assertok(pmbus_select(uts)); + + ut_assertok(run_command("pmbus clear faults", 0)); + ut_assert_nextline("pmbus: CLEAR_FAULTS (03h) issued (RAM sticky STATUS_* cleared)"); + ut_assert_console_end(); + + return 0; +} + +DM_TEST(dm_test_pmbus_clear, UTF_SCAN_FDT | UTF_CONSOLE); + +/* pmbus scan finds the emulated chip by its MFR_ID block read */ +static int dm_test_pmbus_scan(struct unit_test_state *uts) +{ + ut_assertok(run_command("pmbus scan 0", 0)); + ut_assert_skip_to_line(" i2c0:0x70 MFR_ID=\"SANDBOX\""); + + return 0; +} + +DM_TEST(dm_test_pmbus_scan, UTF_SCAN_FDT | UTF_CONSOLE); + +/* pmbus help lists vendor extensions; none are registered here */ +static int dm_test_pmbus_help(struct unit_test_state *uts) +{ + ut_assertok(run_command("pmbus help", 0)); + ut_assert_nextline("pmbus: no vendor extensions registered."); + ut_assert_skip_to_line(" board hook (boot snapshot) and re run 'pmbus help'."); + + return 0; +} + +DM_TEST(dm_test_pmbus_help, UTF_SCAN_FDT | UTF_CONSOLE); 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) { |
