summaryrefslogtreecommitdiff
path: root/lib/pmbus.c
blob: 62a66593638269cb829f275ea22fe67bfa480854 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright 2026 Free Mobile, Vincent Jardin
 *
 * PMBus 1.x decoders, transport helpers, and standard status bit
 * tables for U-Boot. See include/pmbus.h for the API surface and
 * doc/develop/pmbus.rst for the porting guide.
 *
 * Decoder math is implemented from the PMBus 1.3 specification:
 *   1. Part I  (transport): see
 *      doc/PMBus/PMBus_Specification_Rev_1_3_1_Part_I_20150313.{pdf,txt}
 *   2. Part II (commands):  see
 *      doc/PMBus/PMBus_Specification_Rev_1_3_1_Part_II_20150313.{pdf,txt}
 *
 * Reference Linux implementation: linux/drivers/hwmon/pmbus/pmbus_core.c
 * (the kernel side `struct pmbus_data` caching and hwmon publication
 * layers do not apply; only the arithmetic carries over).
 *
 * No code in this file may reference a specific board, SoC, or
 * product. Per chip quirks (MPS DIRECT format LSBs, vendor registers,
 * VID coercion, ADDR_VBOOT auto promotion, and the like) belong in
 * per chip drivers under drivers/power/regulator/ or in board local
 * files under board/<vendor>/<board>/.
 */

#include <ctype.h>
#include <dm.h>
#include <i2c.h>
#include <log.h>
#include <pmbus.h>
#include <limits.h>
#include <linux/bitops.h>
#include <power/regulator.h>

static int pmbus_sign_extend(unsigned int v, unsigned int width)
{
	unsigned int mask = (1U << width) - 1U;
	unsigned int sign = 1U << (width - 1U);

	v &= mask;
	if (v & sign)
		v |= ~mask;
	return (int)v;
}

s64 pmbus_reg2data_linear11(u16 raw)
{
	int mantissa = pmbus_sign_extend(raw & PB_LINEAR11_MANT_MASK,
					 PB_LINEAR11_MANT_BITS);
	int exponent = pmbus_sign_extend((raw >> PB_LINEAR11_EXP_SHIFT) &
					 PB_LINEAR11_EXP_MASK,
					 PB_LINEAR11_EXP_BITS);
	s64 micro;

	/* Engineering value = mantissa * 2^exponent, scaled to micro units. */
	micro = (s64)mantissa * 1000000LL;
	if (exponent >= 0)
		micro <<= exponent;
	else
		micro >>= -exponent;
	return micro;
}

s64 pmbus_reg2data_linear16(u16 raw, u8 vout_mode)
{
	int exponent;
	s64 micro;

	/*
	 * VOUT_MODE bits[7:5] = mode; bits[4:0] = parameter. Linear mode
	 * (000) treats bits[4:0] as the signed 5-bit exponent. For other
	 * modes the caller must dispatch elsewhere.
	 */
	if ((vout_mode & PB_VOUT_MODE_MODE_MASK) != PB_VOUT_MODE_LINEAR)
		return 0;

	exponent = pmbus_sign_extend(vout_mode & PB_VOUT_MODE_PARAM_MASK, 5);

	/* Mantissa is unsigned 16-bit; scale to micro units. */
	micro = (s64)raw * 1000000LL;
	if (exponent >= 0)
		micro <<= exponent;
	else
		micro >>= -exponent;
	return micro;
}

s64 pmbus_reg2data_direct(s16 raw, int m, int b, int R)
{
	s64 acc;

	if (m == 0)
		return 0;

	/*
	 * PMBus Part II sec 8.4:  Y = (1/m) * (X * 10^-R - b)
	 *
	 * Pre scale acc to micro units so the final integer division by
	 * m absorbs the rounding loss into the least significant micro
	 * digit rather than into a coarser place.
	 */
	acc = (s64)raw * 1000000LL;

	/* Apply 10^-R: positive R means divide; negative R means multiply. */
	while (R > 0) {
		acc /= 10;
		R--;
	}
	while (R < 0) {
		acc *= 10;
		R++;
	}

	/* Subtract the offset b, also in micro units. */
	acc -= (s64)b * 1000000LL;

	/* Final: divide by m. */
	acc /= m;
	return acc;
}

u16 pmbus_data2reg_linear16(s64 micro, u8 vout_mode)
{
	int exponent;
	s64 raw;

	if ((vout_mode & PB_VOUT_MODE_MODE_MASK) != PB_VOUT_MODE_LINEAR)
		return 0;

	exponent = pmbus_sign_extend(vout_mode & PB_VOUT_MODE_PARAM_MASK, 5);

	/* raw = micro / (2^exponent * 10^6). */
	raw = micro;
	if (exponent >= 0)
		raw >>= exponent;
	else
		raw <<= -exponent;
	raw /= 1000000LL;

	if (raw < 0)
		raw = 0;
	if (raw > U16_MAX)
		raw = U16_MAX;
	return (u16)raw;
}

u16 pmbus_data2reg_direct(s64 micro, int m, int b, int R)
{
	s64 acc;

	if (m == 0)
		return 0;

	/*
	 * Inverse of pmbus_reg2data_direct():  X = (m * Y + b) * 10^R.
	 * Work in micro units throughout: acc = m * Y_micro + b * 10^6,
	 * then scale by 10^R, finally divide by 10^6 to get the raw
	 * chip count. Order chosen to match the decoder's quantisation
	 * pattern so a round trip (data2reg then reg2data) returns the
	 * input within +/- one LSB.
	 */
	acc = (s64)m * micro + (s64)b * 1000000LL;

	while (R > 0) {
		acc *= 10;
		R--;
	}
	while (R < 0) {
		acc /= 10;
		R++;
	}

	acc /= 1000000LL;

	/* PMBus 1.3 Part II sec 8.4 mandates a signed 16 bit raw value. */
	if (acc > S16_MAX)
		acc = S16_MAX;
	if (acc < S16_MIN)
		acc = S16_MIN;
	return (u16)(s16)acc;
}

s64 pmbus_reg2data(const struct pmbus_driver_info *info,
		   enum pmbus_sensor_classes class,
		   u16 raw, u8 vout_mode)
{
	if (!info || class >= PSC_NUM_CLASSES)
		return 0;

	switch (info->format[class]) {
	case pmbus_fmt_linear:
		if (class == PSC_VOLTAGE_OUT)
			return pmbus_reg2data_linear16(raw, vout_mode);
		return pmbus_reg2data_linear11(raw);

	case pmbus_fmt_direct:
		return pmbus_reg2data_direct((s16)raw,
					     info->m[class],
					     info->b[class],
					     info->R[class]);

	case pmbus_fmt_vid:
	case pmbus_fmt_ieee754:
		/*
		 * Not yet wired up. Add when a consumer lands. VID needs
		 * the per page vrm_version table from the kernel's
		 * pmbus_reg2data_vid(); IEEE754 needs the half precision
		 * decoder from pmbus_reg2data_ieee754().
		 */
		return 0;
	}

	return 0;
}

int pmbus_read_byte(struct udevice *dev, u8 cmd, u8 *val)
{
	return dm_i2c_read(dev, cmd, val, 1);
}

int pmbus_read_word(struct udevice *dev, u8 cmd, u16 *val)
{
	u8 raw[2];
	int ret;

	ret = dm_i2c_read(dev, cmd, raw, 2);
	if (ret)
		return ret;
	*val = (u16)raw[0] | ((u16)raw[1] << 8);
	return 0;
}

int pmbus_write_byte(struct udevice *dev, u8 cmd, u8 val)
{
	return dm_i2c_write(dev, cmd, &val, 1);
}

int pmbus_write_word(struct udevice *dev, u8 cmd, u16 val)
{
	u8 raw[2];

	raw[0] = (u8)(val & 0xff);	/* PMBus words are little-endian */
	raw[1] = (u8)(val >> 8);
	return dm_i2c_write(dev, cmd, raw, 2);
}

int pmbus_read_string(struct udevice *dev, u8 cmd, char *out, int outsz,
		      bool reverse_bytes)
{
	u8 raw[PMBUS_MFR_STRING_MAX + 1];	/* length byte + payload */
	int ret, len, i;

	if (outsz < 2)
		return -EINVAL;

	/* Stage 1: read the length byte. */
	ret = dm_i2c_read(dev, cmd, raw, 1);
	if (ret)
		return ret;

	len = raw[0];
	if (len <= 0 || len > (int)sizeof(raw) - 1)
		return -EBADMSG;
	if (len > outsz - 1)
		len = outsz - 1;

	/* Stage 2: reread length + payload (some controllers mandate this). */
	ret = dm_i2c_read(dev, cmd, raw, len + 1);
	if (ret)
		return ret;

	if (reverse_bytes) {
		for (i = 0; i < len; i++) {
			u8 b = raw[len - i];

			out[i] = isprint(b) ? (char)b : '.';
		}
	} else {
		for (i = 0; i < len; i++) {
			u8 b = raw[i + 1];

			out[i] = isprint(b) ? (char)b : '.';
		}
	}
	out[len] = '\0';
	return len;
}

int pmbus_clear_faults(struct udevice *dev)
{
	return dm_i2c_write(dev, PMBUS_CLEAR_FAULTS, NULL, 0);
}

void pmbus_print_bits(u16 v, const struct pmbus_bit *tab)
{
	const struct pmbus_bit *t;
	int first = 1;

	if (v == 0) {
		printf("clean");
		return;
	}
	for (t = tab; t && t->name; t++) {
		if (v & t->mask) {
			printf("%s%s", first ? "" : "|", t->name);
			first = 0;
		}
	}
}

void pmbus_print_status_bits(u8 reg, u16 v,
			     const struct pmbus_bit *std,
			     const struct pmbus_status_override *ovr)
{
	const struct pmbus_bit *s;
	const struct pmbus_status_override *o;
	int first = 1;

	if (v == 0) {
		printf("clean");
		return;
	}

	/*
	 * Pass 1: walk the standard table in declared order so the
	 * printout retains the conventional bit-15-first ordering. For
	 * each set bit, prefer a chip override matching (reg, mask).
	 */
	for (s = std; s && s->name; s++) {
		const char *name;

		if (!(v & s->mask))
			continue;
		name = s->name;
		for (o = ovr; o && o->name; o++) {
			if (o->reg == reg && o->mask == s->mask) {
				name = o->name;
				break;
			}
		}
		printf("%s%s", first ? "" : "|", name);
		first = 0;
	}

	/*
	 * Pass 2: print overrides whose mask is not in the standard
	 * table at all (chip-specific bit at a position the spec
	 * leaves RESERVED). These would otherwise be swallowed.
	 */
	for (o = ovr; o && o->name; o++) {
		bool in_std = false;

		if (o->reg != reg)
			continue;
		if (!(v & o->mask))
			continue;
		for (s = std; s && s->name; s++) {
			if (s->mask == o->mask) {
				in_std = true;
				break;
			}
		}
		if (in_std)
			continue;
		printf("%s%s", first ? "" : "|", o->name);
		first = 0;
	}
}

/*
 * Standard PMBus 1.3 status bit tables. Per-chip drivers may publish
 * their own tables for vendor extended bits (e.g. NVM summary bits,
 * DR MOS faults) but the standard layout below is the safe baseline.
 *
 * All tables are NULL terminated (`name = NULL` sentinel), matching
 * the convention used elsewhere in U-Boot for driver tables.
 */
const struct pmbus_bit pmbus_status_word_bits[] = {
	{ PB_STATUS_VOUT,        "VOUT" },
	{ PB_STATUS_IOUT_POUT,   "IOUT_POUT" },
	{ PB_STATUS_INPUT,       "INPUT" },
	{ PB_STATUS_WORD_MFR,    "MFR" },
	{ PB_STATUS_POWER_GOOD_N, "PG#" },
	{ PB_STATUS_FANS,        "FANS" },
	{ PB_STATUS_OTHER,       "OTHER" },
	{ PB_STATUS_UNKNOWN,     "UNKNOWN" },
	{ PB_STATUS_BUSY,        "BUSY" },
	{ PB_STATUS_OFF,         "OFF" },
	{ PB_STATUS_VOUT_OV,     "VOUT_OV" },
	{ PB_STATUS_IOUT_OC,     "IOUT_OC" },
	{ PB_STATUS_VIN_UV,      "VIN_UV" },
	{ PB_STATUS_TEMPERATURE, "TEMP" },
	{ PB_STATUS_CML,         "CML" },
	{ PB_STATUS_NONE_ABOVE,  "NONE_ABOVE" },
	{ /* sentinel */ }
};

const struct pmbus_bit pmbus_status_vout_bits[] = {
	{ PB_VOLTAGE_OV_FAULT,         "VOUT_OV_FAULT" },
	{ PB_VOLTAGE_OV_WARNING,       "VOUT_OV_WARN" },
	{ PB_VOLTAGE_UV_WARNING,       "VOUT_UV_WARN" },
	{ PB_VOLTAGE_UV_FAULT,         "VOUT_UV_FAULT" },
	{ PB_VOLTAGE_VOUT_MAX_MIN_WARN, "VOUT_MAX_MIN_WARN" },
	{ /* sentinel */ }
};

const struct pmbus_bit pmbus_status_iout_bits[] = {
	{ PB_IOUT_OC_FAULT,    "IOUT_OC_FAULT" },
	{ PB_IOUT_OC_LV_FAULT, "IOUT_OC_LV_FAULT" },
	{ PB_IOUT_OC_WARNING,  "IOUT_OC_WARN" },
	{ PB_IOUT_UC_FAULT,    "IOUT_UC_FAULT" },
	{ PB_CURRENT_SHARE_FAULT, "ISHARE_FAULT" },
	{ PB_POWER_LIMITING,   "POWER_LIMITING" },
	{ PB_POUT_OP_FAULT,    "POUT_OP_FAULT" },
	{ PB_POUT_OP_WARNING,  "POUT_OP_WARN" },
	{ /* sentinel */ }
};

const struct pmbus_bit pmbus_status_input_bits[] = {
	{ PB_IIN_OC_FAULT,     "IIN_OC_FAULT" },
	{ PB_IIN_OC_WARNING,   "IIN_OC_WARN" },
	{ PB_PIN_OP_WARNING,   "PIN_OP_WARN" },
	{ /* sentinel */ }
};

const struct pmbus_bit pmbus_status_temp_bits[] = {
	{ PB_TEMP_OT_FAULT,    "OT_FAULT" },
	{ PB_TEMP_OT_WARNING,  "OT_WARN" },
	{ PB_TEMP_UT_WARNING,  "UT_WARN" },
	{ PB_TEMP_UT_FAULT,    "UT_FAULT" },
	{ /* sentinel */ }
};

const struct pmbus_bit pmbus_status_cml_bits[] = {
	{ PB_CML_FAULT_INVALID_COMMAND, "INVALID_CMD" },
	{ PB_CML_FAULT_INVALID_DATA,    "INVALID_DATA" },
	{ PB_CML_FAULT_PACKET_ERROR,    "PEC" },
	{ PB_CML_FAULT_MEMORY,          "MEM" },
	{ PB_CML_FAULT_PROCESSOR,       "PROC" },
	{ PB_CML_FAULT_OTHER_COMM,      "OTHER_COMM" },
	{ PB_CML_FAULT_OTHER_MEM_LOGIC, "OTHER_MEM_LOGIC" },
	{ /* sentinel */ }
};

/*
 * Active device tracking + chip / vendor registries (consumed by the
 * `pmbus` U-Boot CLI command in cmd/pmbus.c).
 */

#define PMBUS_MAX_CHIP_MATCHES		8
#define PMBUS_MAX_VENDOR_HANDLERS	4

static struct pmbus_active_dev pmbus_active_state;
static const struct pmbus_chip_match *pmbus_chip_table[PMBUS_MAX_CHIP_MATCHES];
static unsigned int pmbus_chip_table_n;
static const struct pmbus_vendor_op *pmbus_vendor_table[PMBUS_MAX_VENDOR_HANDLERS];
static unsigned int pmbus_vendor_table_n;

const struct pmbus_active_dev *pmbus_active(void)
{
	return pmbus_active_state.valid ? &pmbus_active_state : NULL;
}

void pmbus_clear_active(void)
{
	memset(&pmbus_active_state, 0, sizeof(pmbus_active_state));
}

int pmbus_active_get_i2c(struct udevice **i2c_dev)
{
	struct udevice *bus;
	int ret;

	if (!pmbus_active_state.valid)
		return -ENODEV;
	ret = uclass_get_device_by_seq(UCLASS_I2C, pmbus_active_state.bus_seq, &bus);
	if (ret)
		return ret;
	return i2c_get_chip(bus, pmbus_active_state.addr, 1, i2c_dev);
}

/* engineering value in micro units -> "I.FFF<unit>" (3 fractional digits) */
static void pmbus_emit_micro(s64 micro, const char *unit)
{
	s64 abs_milli = (micro < 0 ? -micro : micro) / 1000LL;

	printf("%lld.%03lld%s", (long long)(micro / 1000000LL),
	       (long long)(abs_milli % 1000LL), unit);
}

struct pmbus_telem_entry {
	u8 reg;
	const char *label;
	enum pmbus_sensor_classes class;
	const char *unit;
};

/*
 * Telemetry register set, in print order. POUT is included so PSU
 * class parts report input/output power; chips that do not implement
 * a given command are skipped via pmbus_word_command_supported().
 */
static const struct pmbus_telem_entry pmbus_telem_table[] = {
	{ PMBUS_READ_VIN,           "VIN ", PSC_VOLTAGE_IN,  "V" },
	{ PMBUS_READ_VOUT,          "VOUT", PSC_VOLTAGE_OUT, "V" },
	{ PMBUS_READ_IIN,           "IIN ", PSC_CURRENT_IN,  "A" },
	{ PMBUS_READ_IOUT,          "IOUT", PSC_CURRENT_OUT, "A" },
	{ PMBUS_READ_POUT,          "POUT", PSC_POWER,       "W" },
	{ PMBUS_READ_TEMPERATURE_1, "TEMP", PSC_TEMPERATURE, "C" },
};

bool pmbus_word_command_supported(struct udevice *dev, u8 reg)
{
	u8 cml_before = 0, cml_after = 0;
	bool have_cml;
	u16 w;

	have_cml = !pmbus_read_byte(dev, PMBUS_STATUS_CML, &cml_before);

	if (pmbus_read_word(dev, reg, &w))
		return false;	/* NAK: unsupported command not ACKed */

	if (have_cml && !(cml_before & PB_CML_FAULT_INVALID_COMMAND) &&
	    !pmbus_read_byte(dev, PMBUS_STATUS_CML, &cml_after) &&
	    (cml_after & PB_CML_FAULT_INVALID_COMMAND))
		return false;	/* ACKed but chip raised INVALID_COMMAND */

	return true;
}

/* Telemetry of the currently selected page. */
static void pmbus_print_telemetry_page(struct udevice *chip,
				       const struct pmbus_active_dev *act)
{
	u8 vout_mode = 0;
	unsigned int i;

	/*
	 * On a read failure vout_mode stays 0 (LINEAR, exponent 0). That is
	 * a silent mis-scale of every VOLTAGE_OUT reading, so make the
	 * fallback visible rather than printing a wrong voltage as if good.
	 */
	if (pmbus_read_byte(chip, PMBUS_VOUT_MODE, &vout_mode))
		printf("  (VOUT_MODE read failed; VOUT decode assumes LINEAR exp 0)\n");

	for (i = 0; i < ARRAY_SIZE(pmbus_telem_table); i++) {
		const struct pmbus_telem_entry *e = &pmbus_telem_table[i];
		u16 raw = 0;

		/*
		 * Class gating. A chip driver that declares classes_present
		 * lists exactly the sensors it implements (kernel-style
		 * per-chip sensor set), so unlisted classes are skipped
		 * silently -- this is what hides the MPS buck's uncalibrated
		 * POUT / IIN. A generic / undeclared device instead gets a
		 * live capability probe per class.
		 */
		if (act->info && act->info->classes_present) {
			if (!(act->info->classes_present & BIT(e->class)))
				continue;
		} else if (!pmbus_word_command_supported(chip, e->reg)) {
			printf("  %s : (not supported)\n", e->label);
			continue;
		}

		if (pmbus_read_word(chip, e->reg, &raw)) {
			printf("  %s : (read failed)\n", e->label);
			continue;
		}

		printf("  %s : raw=0x%04x  ", e->label, raw);
		if (act->info) {
			u16 dec = raw;

			/*
			 * Some DIRECT format parts (e.g. MPS) report
			 * temperature as 1 degC/LSB in the low byte only;
			 * mask there. LINEAR temperatures use all 16 bits
			 * and must NOT be masked.
			 */
			if (e->class == PSC_TEMPERATURE &&
			    act->info->format[PSC_TEMPERATURE] == pmbus_fmt_direct)
				dec = raw & 0x00ff;

			pmbus_emit_micro(pmbus_reg2data(act->info, e->class,
							dec, vout_mode),
					 e->unit);
		} else if (e->class == PSC_VOLTAGE_OUT) {
			pmbus_emit_micro(pmbus_reg2data_linear16(raw, vout_mode),
					 e->unit);
		} else {
			pmbus_emit_micro(pmbus_reg2data_linear11(raw), e->unit);
			printf(" (LINEAR11 fallback)");
		}
		printf("\n");
	}
}

void pmbus_print_telemetry(struct udevice *chip)
{
	const struct pmbus_active_dev *act = pmbus_active();
	int npages, p;
	u8 zero = 0;

	if (!act)
		return;

	/*
	 * Multi-rail parts (PSU bricks) expose one rail per PMBUS_PAGE.
	 * Chip drivers set pmbus_driver_info.pages; the generic driver
	 * takes it from the DT `pmbus,num-pages` (default 1). We always
	 * write PMBUS_PAGE before reading a page -- including page 0 --
	 * because a device may power up selected on a different page, which
	 * is what made the 48V PSU read all-zeros before. Only valid pages
	 * (0..npages-1) are ever written, so we never induce the
	 * out-of-range-PAGE STATUS_CML fault and the device's sticky fault
	 * log is left untouched (no CLEAR_FAULTS, no scrubbing).
	 */
	npages = (act->info && act->info->pages > 0) ? act->info->pages : 1;

	for (p = 0; p < npages; p++) {
		u8 pg = (u8)p;

		if (dm_i2c_write(chip, PMBUS_PAGE, &pg, 1)) {
			printf("  [page %d] PAGE select failed\n", p);
			continue;
		}
		if (npages > 1)
			printf("  [page %d]\n", p);
		pmbus_print_telemetry_page(chip, act);
	}

	if (npages > 1)
		dm_i2c_write(chip, PMBUS_PAGE, &zero, 1);	/* leave on page 0 */
}

void pmbus_print_status_word(struct udevice *chip)
{
	const struct pmbus_active_dev *act = pmbus_active();
	const struct pmbus_status_override *ovr =
		(act && act->info) ? act->info->status_overrides : NULL;
	u16 word = 0;

	if (pmbus_read_word(chip, PMBUS_STATUS_WORD, &word)) {
		printf("  STATUS_WORD    (79h) = (read failed)\n");
		return;
	}
	printf("  STATUS_WORD    (79h) = 0x%04x  [", word);
	pmbus_print_status_bits(PMBUS_STATUS_WORD, word,
				pmbus_status_word_bits, ovr);
	printf("]\n");
}

static const struct pmbus_chip_match *pmbus_match_mfr(const char *id)
{
	unsigned int i;

	if (!id || !id[0])
		return NULL;
	for (i = 0; i < pmbus_chip_table_n; i++) {
		const struct pmbus_chip_match *m = pmbus_chip_table[i];
		size_t plen = strlen(m->mfr_id);

		if (strlen(id) >= plen && !strncmp(id, m->mfr_id, plen))
			return m;
	}
	return NULL;
}

/*
 * Walk UCLASS_REGULATOR looking for a regulator whose I2C parent
 * bus seq + DT reg address match the requested (bus_seq, addr).
 * Returns the regulator-name (uclass plat .name) on hit, or NULL if
 * no UCLASS_REGULATOR device matches (chip not bound through DT, or
 * CONFIG_DM_REGULATOR disabled).
 */
static const char *pmbus_lookup_regname(int bus_seq, u8 addr)
{
	struct uclass *uc;
	struct udevice *r;

	if (!IS_ENABLED(CONFIG_DM_REGULATOR))
		return NULL;

	if (uclass_get(UCLASS_REGULATOR, &uc))
		return NULL;
	uclass_foreach_dev(r, uc) {
		struct dm_regulator_uclass_plat *up;
		struct udevice *parent = dev_get_parent(r);
		int ra;

		if (!parent || device_get_uclass_id(parent) != UCLASS_I2C)
			continue;
		if (dev_seq(parent) != bus_seq)
			continue;
		ra = dev_read_addr(r);
		if (ra < 0 || (u8)ra != addr)
			continue;
		up = dev_get_uclass_plat(r);
		if (up && up->name)
			return up->name;
		return r->name;
	}
	return NULL;
}

int pmbus_set_active(int bus_seq, u8 addr)
{
	const struct pmbus_chip_match *match = NULL;
	struct udevice *bus, *chip;
	char id_fwd[PMBUS_MFR_STRING_MAX] = "";
	char id_rev[PMBUS_MFR_STRING_MAX] = "";
	const char *rname;
	int ret;

	pmbus_clear_active();

	ret = uclass_get_device_by_seq(UCLASS_I2C, bus_seq, &bus);
	if (ret)
		return ret;
	ret = i2c_get_chip(bus, addr, 1, &chip);
	if (ret)
		return ret;

	pmbus_active_state.bus_seq = bus_seq;
	pmbus_active_state.addr = addr;

	/*
	 * Probe MFR_ID in both byte orders. Spec compliant chips return
	 * "MPS" / "TI" / etc. in the natural reading (forward); MPS NVM
	 * personalities store the string LSB first and need the reverse
	 * read. Chip table entries declare which one is canonical for
	 * the chip family they describe.
	 */
	if (pmbus_read_string(chip, PMBUS_MFR_ID, id_fwd, sizeof(id_fwd), false) < 0)
		id_fwd[0] = '\0';
	if (pmbus_read_string(chip, PMBUS_MFR_ID, id_rev, sizeof(id_rev), true) < 0)
		id_rev[0] = '\0';

	match = pmbus_match_mfr(id_fwd);
	if (match && !match->mfr_id_reverse) {
		strlcpy(pmbus_active_state.mfr_id, id_fwd,
			sizeof(pmbus_active_state.mfr_id));
	} else {
		match = pmbus_match_mfr(id_rev);
		if (match && match->mfr_id_reverse) {
			strlcpy(pmbus_active_state.mfr_id, id_rev,
				sizeof(pmbus_active_state.mfr_id));
		} else {
			/* No registered match; cache the forward read as best effort. */
			strlcpy(pmbus_active_state.mfr_id,
				id_fwd[0] ? id_fwd : id_rev,
				sizeof(pmbus_active_state.mfr_id));
		}
	}

	if (match) {
		pmbus_active_state.info = match->info;
		if (match->vendor)
			strlcpy(pmbus_active_state.vendor, match->vendor,
				sizeof(pmbus_active_state.vendor));
	}

	/*
	 * No MFR_ID chip-match (a spec compliant part with no per chip
	 * driver, e.g. a Flex / Delta PSU): if a generic / chip
	 * UCLASS_REGULATOR is bound at this address, reuse its
	 * VOUT_MODE detected driver_info so telemetry decodes through
	 * the right per class formats instead of the blanket
	 * LINEAR16 / LINEAR11 fallback.
	 */
	if (CONFIG_IS_ENABLED(DM_REGULATOR_PMBUS_HELPER) &&
	    !pmbus_active_state.info) {
		const struct pmbus_driver_info *di =
			pmbus_regulator_info_by_addr(bus_seq, addr);

		if (di)
			pmbus_active_state.info = di;
	}

	/*
	 * MFR_MODEL / MFR_REVISION are best effort. Use the same byte
	 * order the matched chip declared; if nothing matched, use the
	 * forward order.
	 */
	{
		bool reverse = match && match->mfr_id_reverse;

		pmbus_active_state.mfr_reverse = reverse;
		pmbus_read_string(chip, PMBUS_MFR_MODEL,
				  pmbus_active_state.mfr_model,
				  sizeof(pmbus_active_state.mfr_model), reverse);
		pmbus_read_string(chip, PMBUS_MFR_REVISION,
				  pmbus_active_state.mfr_revision,
				  sizeof(pmbus_active_state.mfr_revision), reverse);
	}

	rname = pmbus_lookup_regname(bus_seq, addr);
	if (rname)
		strlcpy(pmbus_active_state.name, rname,
			sizeof(pmbus_active_state.name));

	pmbus_active_state.valid = true;
	return 0;
}

int pmbus_register_chip(const struct pmbus_chip_match *match)
{
	if (!match || !match->mfr_id)
		return -EINVAL;
	if (pmbus_chip_table_n >= PMBUS_MAX_CHIP_MATCHES)
		return -ENOSPC;
	pmbus_chip_table[pmbus_chip_table_n++] = match;
	return 0;
}

int pmbus_register_vendor_handler(const struct pmbus_vendor_op *op)
{
	if (!op || !op->vendor || !op->handler)
		return -EINVAL;
	if (pmbus_vendor_table_n >= PMBUS_MAX_VENDOR_HANDLERS)
		return -ENOSPC;
	pmbus_vendor_table[pmbus_vendor_table_n++] = op;
	return 0;
}

const struct pmbus_vendor_op *pmbus_lookup_vendor(const char *vendor)
{
	unsigned int i;

	if (!vendor)
		return NULL;
	for (i = 0; i < pmbus_vendor_table_n; i++)
		if (!strcmp(pmbus_vendor_table[i]->vendor, vendor))
			return pmbus_vendor_table[i];
	return NULL;
}

unsigned int pmbus_vendor_count(void)
{
	return pmbus_vendor_table_n;
}

const struct pmbus_vendor_op *pmbus_vendor_at(unsigned int i)
{
	return i < pmbus_vendor_table_n ? pmbus_vendor_table[i] : NULL;
}

int pmbus_resolve_by_name(const char *name, int *bus_seq, u8 *addr)
{
	struct udevice *reg;
	struct udevice *parent;
	int ret;
	int a;

	if (!IS_ENABLED(CONFIG_DM_REGULATOR))
		return -ENOSYS;

	if (!name || !bus_seq || !addr)
		return -EINVAL;

	ret = regulator_get_by_platname(name, &reg);
	if (ret)
		return ret;

	parent = dev_get_parent(reg);
	if (!parent || device_get_uclass_id(parent) != UCLASS_I2C)
		return -ENODEV;

	a = dev_read_addr(reg);
	if (a < 0 || a > 0x7f)
		return -EINVAL;

	*bus_seq = dev_seq(parent);
	*addr = (u8)a;
	return 0;
}