diff options
Diffstat (limited to 'drivers/hwmon/pmbus')
86 files changed, 29784 insertions, 0 deletions
diff --git a/drivers/hwmon/pmbus/Kconfig b/drivers/hwmon/pmbus/Kconfig new file mode 100644 index 000000000..eb71a0ac0 --- /dev/null +++ b/drivers/hwmon/pmbus/Kconfig @@ -0,0 +1,911 @@ +# SPDX-License-Identifier: GPL-2.0-only +# +# PMBus chip drivers configuration +# + +menuconfig PMBUS + tristate "PMBus support" + depends on I2C + help + Say yes here if you want to enable PMBus support. + + This driver can also be built as a module. If so, the module will + be called pmbus_core. + +if PMBUS + +config SENSORS_PMBUS + tristate "Generic PMBus devices" + default y + help + If you say yes here you get hardware monitoring support for generic + PMBus devices, including but not limited to ADP4000, BMR310, BMR453, + BMR454, BMR456, BMR457, BMR458, BMR480, BMR490, BMR491, BMR492, + MAX20796, MDT040, NCP4200, NCP4208, PDT003, PDT006, PDT012, + TPS40400, TPS544B20, TPS544B25, TPS544C20, TPS544C25, and UDT020. + + This driver can also be built as a module. If so, the module will + be called pmbus. + +config SENSORS_ACBEL_FSG032 + tristate "ACBEL FSG032 Power Supply" + help + If you say yes here you get hardware monitoring support for the ACBEL + FSG032 Power Supply. + + This driver can also be built as a module. If so, the module will + be called acbel-fsg032. + +config SENSORS_ADM1266 + tristate "Analog Devices ADM1266 Sequencer" + select CRC8 + depends on GPIOLIB + help + If you say yes here you get hardware monitoring support for Analog + Devices ADM1266 Cascadable Super Sequencer. + + This driver can also be built as a module. If so, the module will + be called adm1266. + +config SENSORS_ADM1275 + tristate "Analog Devices ADM1275 and compatibles" + help + If you say yes here you get hardware monitoring support for Analog + Devices ADM1075, ADM1272, ADM1273, ADM1275, ADM1276, ADM1278, ADM1281, + ADM1293, ADM1294, ROHM BD12780, ROHM BD12790, and SQ24905C + Hot-Swap Controller and Digital Power Monitors. + + This driver can also be built as a module. If so, the module will + be called adm1275. + +config SENSORS_ADP1050 + tristate "Analog Devices ADP1050 digital controller for Power Supplies" + help + If you say yes here you get hardware monitoring support for Analog + Devices ADP1050 digital controller for isolated power supply with + PMBus interface. + + This driver can also be built as a module. If so, the module will + be called adp1050. + +config SENSORS_ADP1050_REGULATOR + bool "Regulator support for ADP1050 and compatibles" + depends on SENSORS_ADP1050 && REGULATOR + help + If you say yes here you get regulator support for Analog Devices + LTP8800-1A, LTP8800-4A, and LTP8800-2. LTP8800 is a family of DC/DC + µModule regulators that can provide microprocessor power from 54V + power distribution architecture. + +config SENSORS_APS_379 + tristate "Sony APS-379 Power Supplies" + help + If you say yes here you get hardware monitoring support for Sony + APS-379 Power Supplies. + + This driver can also be built as a module. If so, the module will + be called aps-379. + +config SENSORS_BEL_PFE + tristate "Bel PFE Compatible Power Supplies" + help + If you say yes here you get hardware monitoring support for BEL + PFE1100 and PFE3000 Power Supplies. + + This driver can also be built as a module. If so, the module will + be called bel-pfe. + +config SENSORS_BPA_RS600 + tristate "BluTek BPA-RS600 Power Supplies" + help + If you say yes here you get hardware monitoring support for BluTek + BPA-RS600 Power Supplies. + + This driver can also be built as a module. If so, the module will + be called bpa-rs600. + +config SENSORS_CRPS + tristate "Intel Common Redundant Power Supply" + help + If you say yes here you get hardware monitoring support for the Intel + Common Redundant Power Supply. + + This driver can also be built as a module. If so, the module will + be called crps. + +config SENSORS_D1U74T + tristate "Murata D1U74T Power Supply" + help + If you say yes here you get hardware monitoring support for the Murata + D1U74T Power Supply. + + This driver can also be built as a module. If so, the module will + be called d1u74t. + +config SENSORS_DELTA_AHE50DC_FAN + tristate "Delta AHE-50DC fan control module" + help + If you say yes here you get hardware monitoring support for + the integrated fan control module of the Delta AHE-50DC + Open19 power shelf. + + This driver can also be built as a module. If so, the module + will be called delta-ahe50dc-fan. + +config SENSORS_FSP_3Y + tristate "FSP/3Y-Power power supplies" + help + If you say yes here you get hardware monitoring support for + FSP/3Y-Power hot-swap power supplies. + Supported models: YH-5151E, YM-2151E + + This driver can also be built as a module. If so, the module will + be called fsp-3y. + +config SENSORS_HAC300S + tristate "Hitron HAC300S-D120E PSU" + help + If you say yes here you get hardware monitoring support for the + Hitron HAC300S-D120E Power Supply. + + This driver can also be built as a module. If so, the module will + be called hac300s. + +config SENSORS_IBM_CFFPS + tristate "IBM Common Form Factor Power Supply" + depends on LEDS_CLASS + help + If you say yes here you get hardware monitoring support for the IBM + Common Form Factor power supply. + + This driver can also be built as a module. If so, the module will + be called ibm-cffps. + +config SENSORS_DPS920AB + tristate "Delta DPS920AB Power Supply" + help + If you say yes here you get hardware monitoring support for Delta + DPS920AB Power Supplies. + + This driver can also be built as a module. If so, the module will + be called dps920ab. + +config SENSORS_E50SN12051 + tristate "Delta E50SN12051 Power Modules" + help + If you say yes here you get hardware monitoring support for Delta + E50SN12051 Power Modules. + + This driver can also be built as a module. If so, the module will + be called e50sn12051. + +config SENSORS_INA233 + tristate "Texas Instruments INA233 and compatibles" + help + If you say yes here you get hardware monitoring support for Texas + Instruments INA233. + + This driver can also be built as a module. If so, the module will + be called ina233. + +config SENSORS_INSPUR_IPSPS + tristate "INSPUR Power System Power Supply" + help + If you say yes here you get hardware monitoring support for the INSPUR + Power System power supply. + + This driver can also be built as a module. If so, the module will + be called inspur-ipsps. + +config SENSORS_IR35221 + tristate "Infineon IR35221" + help + If you say yes here you get hardware monitoring support for the + Infineon IR35221 controller. + + This driver can also be built as a module. If so, the module will + be called ir35221. + +config SENSORS_IR36021 + tristate "Infineon IR36021" + help + If you say yes here you get hardware monitoring support for Infineon + IR36021. + + This driver can also be built as a module. If so, the module will + be called ir36021. + +config SENSORS_IR38064 + tristate "Infineon IR38064 and compatibles" + help + If you say yes here you get hardware monitoring support for Infineon + IR38060, IR38064, IR38164 and IR38263. + + This driver can also be built as a module. If so, the module will + be called ir38064. + +config SENSORS_IR38064_REGULATOR + bool "Regulator support for IR38064 and compatibles" + depends on SENSORS_IR38064 && REGULATOR + help + Uses the IR38064 or compatible as regulator. + +config SENSORS_IRPS5401 + tristate "Infineon IRPS5401" + help + If you say yes here you get hardware monitoring support for the + Infineon IRPS5401 controller. + + This driver can also be built as a module. If so, the module will + be called irps5401. + +config SENSORS_ISL68137 + tristate "Renesas Digital Multiphase Voltage Regulators" + help + If you say yes here you get hardware monitoring support for Renesas + digital multiphase voltage regulators. + + This driver can also be built as a module. If so, the module will + be called isl68137. + +config SENSORS_LM25066 + tristate "National Semiconductor LM25066 and compatibles" + help + If you say yes here you get hardware monitoring support for National + Semiconductor LM25056, LM25066, LM5064, and LM5066. + + This driver can also be built as a module. If so, the module will + be called lm25066. + +config SENSORS_LM25066_REGULATOR + bool "Regulator support for LM25066 and compatibles" + depends on SENSORS_LM25066 && REGULATOR + help + If you say yes here you get regulator support for National + Semiconductor LM25066, LM5064, and LM5066. + +config SENSORS_LT3074 + tristate "Analog Devices LT3074" + help + If you say yes here you get hardware monitoring support for Analog + Devices LT3074. + + This driver can also be built as a module. If so, the module will + be called lt3074. + +config SENSORS_LT3074_REGULATOR + tristate "Regulator support for LT3074" + depends on SENSORS_LT3074 && REGULATOR + help + If you say yes here you get regulator support for Analog Devices + LT3074. The LT3074 is a low voltage, ultralow noise, high PSRR, + dropout linear regulator. The device supplies up to 3A with a + typical dropout voltage of 45mV. + +config SENSORS_LT7182S + tristate "Analog Devices LT7182S" + help + If you say yes here you get hardware monitoring support for Analog + Devices LT7182S. + + This driver can also be built as a module. If so, the module will + be called lt7182s. + +config SENSORS_LTC2978 + tristate "Linear Technologies LTC2978 and compatibles" + help + If you say yes here you get hardware monitoring support for Linear + Technology LTC2972, LTC2974, LTC2975, LTC2977, LTC2978, LTC2979, + LTC2980, and LTM2987. + + This driver can also be built as a module. If so, the module will + be called ltc2978. + +config SENSORS_LTC2978_REGULATOR + bool "Regulator support for LTC2978 and compatibles" + depends on SENSORS_LTC2978 && REGULATOR + help + If you say yes here you get regulator support for Linear Technology + LT7170, LT7171, LTC3880, LTC3883, LTC3884, LTC3886, LTC3887, LTC3889, + LTC7841, LTC7880, LTM4644, LTM4673, LTM4675, LTM4676, LTM4677, + LTM4678, LTM4680, LTM4686, and LTM4700. + +config SENSORS_LTC3815 + tristate "Linear Technologies LTC3815" + help + If you say yes here you get hardware monitoring support for Linear + Technology LTC3815. + + This driver can also be built as a module. If so, the module will + be called ltc3815. + +config SENSORS_LTC4286 + bool "Analog Devices LTC4286" + help + LTC4286 is an integrated solution for hot swap applications that + allows a board to be safely inserted and removed from a live + backplane. + This chip could be used to monitor voltage, current, ...etc. + If you say yes here you get hardware monitoring support for Analog + Devices LTC4286. + +config SENSORS_LX1308 + tristate "Luxshare LX1308 DC/DC Power Module" + help + If you say yes here you get hardware monitoring support for + Luxshare LX1308, a high-efficiency 12V 860W DC/DC power module + with PMBus interface. + + This driver can also be built as a module. If so, the module will + be called lx1308. + +config SENSORS_MAX15301 + tristate "Maxim MAX15301" + help + If you say yes here you get hardware monitoring support for Maxim + MAX15301, MAX15303, as well as for Flex BMR461. + + This driver can also be built as a module. If so, the module will + be called max15301. + +config SENSORS_MAX16064 + tristate "Maxim MAX16064" + help + If you say yes here you get hardware monitoring support for Maxim + MAX16064. + + This driver can also be built as a module. If so, the module will + be called max16064. + +config SENSORS_MAX16601 + tristate "Maxim MAX16508, MAX16600, MAX16601, and MAX16602" + help + If you say yes here you get hardware monitoring support for Maxim + MAX16508, MAX16600, MAX16601, and MAX16602. + + This driver can also be built as a module. If so, the module will + be called max16601. + +config SENSORS_MAX17616 + tristate "Analog Devices MAX17616/MAX17616A" + help + If you say yes here you get hardware monitoring support for Analog + Devices MAX17616/MAX17616A. + + This driver can also be built as a module. If so, the module will + be called max17616. + +config SENSORS_MAX20730 + tristate "Maxim MAX20710, MAX20730, MAX20734, MAX20743" + help + If you say yes here you get hardware monitoring support for Maxim + MAX20710, MAX20730, MAX20734, and MAX20743. + + This driver can also be built as a module. If so, the module will + be called max20730. + +config SENSORS_MAX20751 + tristate "Maxim MAX20751" + help + If you say yes here you get hardware monitoring support for Maxim + MAX20751. + + This driver can also be built as a module. If so, the module will + be called max20751. + +config SENSORS_MAX20830 + tristate "Analog Devices MAX20830 and compatibles" + help + If you say yes here you get hardware monitoring support for Analog + Devices MAX20830, MAX20830C, and MAX20840C. + + This driver can also be built as a module. If so, the module will + be called max20830. + +config SENSORS_MAX20860A + tristate "Analog Devices MAX20860A" + help + If you say yes here you get hardware monitoring support for Analog + Devices MAX20860A step-down converter. + + This driver can also be built as a module. If so, the module will + be called max20860a. + +config SENSORS_MAX20860A_REGULATOR + bool "Regulator support for MAX20860A" + depends on SENSORS_MAX20860A && REGULATOR + help + If you say yes here you get regulator support for Analog Devices + MAX20860A step-down converter. + + This enables the MAX20860A to be used as a regulator device, + providing voltage control through the regulator framework. + +config SENSORS_MAX31785 + tristate "Maxim MAX31785 and compatibles" + help + If you say yes here you get hardware monitoring support for Maxim + MAX31785. + + This driver can also be built as a module. If so, the module will + be called max31785. + +config SENSORS_MAX34440 + tristate "Maxim MAX34440 and compatibles" + help + If you say yes here you get hardware monitoring support for Maxim + MAX34440, MAX34441, MAX34446, MAX34451, MAX34452, MAX34460, and + MAX34461. Other compatible devices are ADPM12160, ADPM12200, and + ADPM12250. + + This driver can also be built as a module. If so, the module will + be called max34440. + +config SENSORS_MAX8688 + tristate "Maxim MAX8688" + help + If you say yes here you get hardware monitoring support for Maxim + MAX8688. + + This driver can also be built as a module. If so, the module will + be called max8688. + +config SENSORS_MP2856 + tristate "MPS MP2856" + help + If you say yes here you get hardware monitoring support for MPS + MP2856 MP2857 Dual Loop Digital Multi-Phase Controller. + + This driver can also be built as a module. If so, the module will + be called mp2856. + +config SENSORS_MP2869 + tristate "MPS MP2869" + help + If you say yes here you get hardware monitoring support for MPS + MP2869 Dual Loop Digital Multi-Phase Controller. + + This driver can also be built as a module. If so, the module will + be called mp2869. + +config SENSORS_MP2888 + tristate "MPS MP2888" + help + If you say yes here you get hardware monitoring support for MPS + MP2888 Digital, Multi-Phase, Pulse-Width Modulation Controller. + + This driver can also be built as a module. If so, the module will + be called mp2888. + +config SENSORS_MP2891 + tristate "MPS MP2891" + help + If you say yes here you get hardware monitoring support for MPS + MP2891 Dual Loop Digital Multi-Phase Controller. + + This driver can also be built as a module. If so, the module will + be called mp2891. + +config SENSORS_MP2925 + tristate "MPS MP2925" + help + If you say yes here you get hardware monitoring support for MPS + MP2925 Dual Loop Digital Multi-Phase Controller. + + This driver can also be built as a module. If so, the module will + be called mp2925. + +config SENSORS_MP29502 + tristate "MPS MP29502" + help + If you say yes here you get hardware monitoring support for MPS + MP29502 Dual Loop Digital Multi-Phase Controller. + + This driver can also be built as a module. If so, the module will + be called mp29502. + +config SENSORS_MP2975 + tristate "MPS MP2975" + help + If you say yes here you get hardware monitoring support for MPS + MP2975 Dual Loop Digital Multi-Phase Controller. + + This driver can also be built as a module. If so, the module will + be called mp2975. + +config SENSORS_MP2985 + tristate "MPS MP2985" + help + If you say yes here you get hardware monitoring support for MPS + MP2985 Dual Loop Digital Multi-Phase Controller. + + This driver can also be built as a module. If so, the module will + be called mp2985. + +config SENSORS_MP2993 + tristate "MPS MP2993" + help + If you say yes here you get hardware monitoring support for MPS + MP2993 Dual Loop Digital Multi-Phase Controller. + + This driver can also be built as a module. If so, the module will + be called mp2993. + +config SENSORS_MP2975_REGULATOR + depends on SENSORS_MP2975 && REGULATOR + bool "Regulator support for MPS MP2975" + help + If you say yes here you get regulator support for MPS MP2975 + Dual Loop Digital Multi-Phase Controller. + +config SENSORS_MP5023 + tristate "MPS MP5023" + help + If you say yes here you get hardware monitoring support for MPS + MP5023. + + This driver can also be built as a module. If so, the module will + be called mp5023. + +config SENSORS_MP5920 + tristate "MPS MP5920" + help + If you say yes here you get hardware monitoring support for Monolithic + MP5920. + + This driver can also be built as a module. If so, the module will + be called mp5920. + +config SENSORS_MP5926 + tristate "MPS MP5926" + help + If you say yes here you get hardware monitoring support for Monolithic + MP5926. + + This driver can also be built as a module. If so, the module will + be called mp5926. + +config SENSORS_MP5990 + tristate "MPS MP5990" + help + If you say yes here you get hardware monitoring support for MPS + MP5990. + + This driver can also be built as a module. If so, the module will + be called mp5990. + +config SENSORS_MP9941 + tristate "MPS MP9941" + help + If you say yes here you get hardware monitoring support for MPS + MP9941. + + This driver can also be built as a module. If so, the module will + be called mp9941. + +config SENSORS_MP9945 + tristate "MPS MP9945" + help + If you say yes here you get hardware monitoring support for MPS + MP9945. + + This driver can also be built as a module. If so, the module will + be called mp9945. + +config SENSORS_MPQ7932_REGULATOR + bool "Regulator support for MPQ7932" + depends on SENSORS_MPQ7932 && REGULATOR + help + If you say yes here you get six integrated buck converter regulator + support for power management IC MPS MPQ7932. + +config SENSORS_MPQ7932 + tristate "MPS MPQ7932" + help + If you say yes here you get hardware monitoring functionality support + for power management IC MPS MPQ7932. + + This driver can also be built as a module. If so, the module will + be called mpq7932. + +config SENSORS_MPQ82D00 + tristate "MPS MPQ82D00" + help + If you say yes here you get hardware monitoring functionality support + for power management IC MPS MPQ82D00. + + This driver can also be built as a module. If so, the module will + be called mpq82d00. + +config SENSORS_MPQ8785 + tristate "MPS MPQ8785" + help + If you say yes here you get hardware monitoring functionality support + for power management IC MPS MPQ8785. + + This driver can also be built as a module. If so, the module will + be called mpq8785. + +config SENSORS_MPQ8646 + tristate "MPS MPQ8646" + depends on REGULATOR || !REGULATOR + depends on NVMEM || !NVMEM + help + If you say yes here you get hardware monitoring support for the + Monolithic Power Systems MPQ8646. + + This driver can also be built as a module. If so, the module + will be called mpq8646. + +config SENSORS_MPQ8646_DEBUG_UNSAFE + bool "MPQ8646 unsafe write/provisioning debugfs (DANGEROUS)" + depends on SENSORS_MPQ8646 && DEBUG_FS + default n + help + Expose additional WRITE-able debugfs files in the client's + pmbus debugfs directory, + /sys/kernel/debug/i2c/i2c-<bus>/<bus>-<addr>/ + + These are provisioning and bring-up aids. On many designs the + MPQ8646 powers the SoC core rail, so a wrong write can brown out + or permanently mis-provision the board. + + WARNING: Wrong register writes can and likely will physically + damage or destroy the chip and/or the board. + + It shall never be set in a production, shipping, or default configuration. + + If unsure, say N. + +config SENSORS_PIM4328 + tristate "Flex PIM4328 and compatibles" + help + If you say yes here you get hardware monitoring support for Flex + PIM4328, PIM4820 and PIM4006 Power Interface Modules. + + This driver can also be built as a module. If so, the module will + be called pim4328. + +config SENSORS_PLI1209BC + tristate "Vicor PLI1209BC" + help + If you say yes here you get hardware monitoring support for Vicor + PLI1209BC Digital Supervisor. + + This driver can also be built as a module. If so, the module will + be called pli1209bc. + +config SENSORS_PLI1209BC_REGULATOR + bool "Regulator support for PLI1209BC" + depends on SENSORS_PLI1209BC && REGULATOR + help + If you say yes here you get regulator support for Vicor PLI1209BC + Digital Supervisor. + +config SENSORS_PM6764TR + tristate "ST PM6764TR" + help + If you say yes here you get hardware monitoring support for ST + PM6764TR. + + This driver can also be built as a module. If so, the module will + be called pm6764tr. + +config SENSORS_PXE1610 + tristate "Infineon PXE1610" + help + If you say yes here you get hardware monitoring support for Infineon + PXE1610. + + This driver can also be built as a module. If so, the module will + be called pxe1610. + +config SENSORS_Q54SJ108A2 + tristate "Delta Power Supplies Q54SJ108A2" + help + If you say yes here you get hardware monitoring support for Delta + Q54SJ108A2 series Power Supplies. + + This driver can also be built as a module. If so, the module will + be called q54sj108a2. + +config SENSORS_STEF48H28 + tristate "ST STEF48H28" + help + If you say yes here you get hardware monitoring support for ST + STEF48H28. + + This driver can also be built as a module. If so, the module will + be called stef48h28. + +config SENSORS_SQ24860 + tristate "Silergy SQ24860" + help + If you say yes here you get hardware monitoring support for Silergy + SQ24860 eFuse. + + This driver can also be built as a module. If so, the module will + be called sq24860. + +config SENSORS_SQ24860_REGULATOR + bool "Regulator support for SQ24860" + depends on SENSORS_SQ24860 && REGULATOR + default SENSORS_SQ24860 + help + If you say yes here you get regulator support for Silergy SQ24860. + The regulator is registered through the PMBus regulator framework and + can be used to control the output exposed by the device. + This option is only useful if regulator framework support is needed. + +config SENSORS_STPDDC60 + tristate "ST STPDDC60" + help + If you say yes here you get hardware monitoring support for ST + STPDDC60 Universal Digital Multicell Controller, as well as for + Flex BMR481. + + This driver can also be built as a module. If so, the module will + be called stpddc60. + +config SENSORS_TDA38640 + tristate "Infineon TDA38640" + help + If you say yes here you get hardware monitoring support for Infineon + TDA38640. + + This driver can also be built as a module. If so, the module will + be called tda38640. + +config SENSORS_TDA38640_REGULATOR + bool "Regulator support for TDA38640 and compatibles" + depends on SENSORS_TDA38640 && REGULATOR + help + If you say yes here you get regulator support for Infineon + TDA38640 as regulator. + +config SENSORS_TPS25990 + tristate "TI TPS25990" + help + If you say yes here you get hardware monitoring support for TI + TPS25990. + + This driver can also be built as a module. If so, the module will + be called tps25990. + +config SENSORS_TPS25990_REGULATOR + bool "Regulator support for TPS25990 and compatibles" + depends on SENSORS_TPS25990 && REGULATOR + default SENSORS_TPS25990 + help + If you say yes here you get regulator support for Texas Instruments + TPS25990. + +config SENSORS_TPS40422 + tristate "TI TPS40422" + help + If you say yes here you get hardware monitoring support for TI + TPS40422. + + This driver can also be built as a module. If so, the module will + be called tps40422. + +config SENSORS_TPS53679 + tristate "TI TPS53647, TPS53667, TPS53676, TPS53679, TPS53681, TPS53688" + help + If you say yes here you get hardware monitoring support for TI + TPS53647, TPS53667, TPS53676, TPS53679, TPS53681, and TPS53688. + + This driver can also be built as a module. If so, the module will + be called tps53679. + +config SENSORS_TPS546D24 + tristate "TPS546D24" + help + If you say yes here you get hardware monitoring support for TEXAS + TPS546D24. + + This driver can also be built as a module. If so, the module will + be called tps546d24 + +config SENSORS_UCD9000 + tristate "TI UCD90120, UCD90124, UCD90160, UCD90320, UCD9090, UCD90910" + help + If you say yes here you get hardware monitoring support for TI + UCD90120, UCD90124, UCD90160, UCD90320, UCD9090, UCD90910, Sequencer + and System Health Controllers. + + This driver can also be built as a module. If so, the module will + be called ucd9000. + +config SENSORS_UCD9200 + tristate "TI UCD9220, UCD9222, UCD9224, UCD9240, UCD9244, UCD9246, UCD9248" + help + If you say yes here you get hardware monitoring support for TI + UCD9220, UCD9222, UCD9224, UCD9240, UCD9244, UCD9246, and UCD9248 + Digital PWM System Controllers. + + This driver can also be built as a module. If so, the module will + be called ucd9200. + +config SENSORS_VT7505 + tristate "Analog Devices MAX16545, MAX16550 and Volterra VT7505" + help + If you say yes here you get hardware monitoring support for Analog + Devices MAX16545, MAX16550 and Volterra VT7505 PMBus controllers. + + This driver can also be built as a module. If so, the module will + be called vt7505. + +config SENSORS_XDP710 + tristate "Infineon XDP710 family" + help + If you say yes here you get hardware monitoring support for Infineon + XDP710. + + This driver can also be built as a module. If so, the module will + be called xdp710. + +config SENSORS_XDP720 + tristate "Infineon XDP720 family" + help + If you say yes here you get hardware monitoring support for Infineon + XDP720 and XDP730 Digital eFuse Controllers. + + This driver can also be built as a module. If so, the module will + be called xdp720. + +config SENSORS_XDPE152 + tristate "Infineon XDPE152 family" + help + If you say yes here you get hardware monitoring support for Infineon + XDPE15284, XDPE152C4, device. + + This driver can also be built as a module. If so, the module will + be called xdpe152c4. + +config SENSORS_XDPE1A2G7B + tristate "Infineon XDPE1A2G7B" + help + If you say yes here you get hardware monitoring support for Infineon + XDPE1A2G5B and XDPE1A2G7B. + + This driver can also be built as a module. If so, the module will + be called xdpe1a2g7b. + +config SENSORS_XDPE1A2G7B_REGULATOR + bool "Regulator support for XDPE1A2G7B and compatibles" + depends on SENSORS_XDPE1A2G7B && REGULATOR + help + If you say yes here you get regulator support for Infineon + XDPE1A2G5B and XDPE1A2G7B multi-phase digital controllers. + + This enables the controllers to be used as regulator devices, + providing voltage control through the regulator framework. + +config SENSORS_XDPE122 + tristate "Infineon XDPE122 family" + help + If you say yes here you get hardware monitoring support for Infineon + XDPE12254, XDPE12284, device. + + This driver can also be built as a module. If so, the module will + be called xdpe12284. + +config SENSORS_XDPE122_REGULATOR + bool "Regulator support for XDPE122 and compatibles" + depends on SENSORS_XDPE122 && REGULATOR + help + Uses the xdpe12284 or compatible as regulator. + +config SENSORS_ZL6100 + tristate "Intersil ZL6100 and compatibles" + help + If you say yes here you get hardware monitoring support for Intersil + ZL2004, ZL2005, ZL2006, ZL2008, ZL2105, ZL2106, ZL6100, ZL6105, + ZL9101M, and ZL9117M Digital DC/DC Controllers, as well as for + Ericsson BMR450, BMR451, BMR462, BMR463, and BMR464. + + This driver can also be built as a module. If so, the module will + be called zl6100. + +endif # PMBUS diff --git a/drivers/hwmon/pmbus/Makefile b/drivers/hwmon/pmbus/Makefile new file mode 100644 index 000000000..e288fe72a --- /dev/null +++ b/drivers/hwmon/pmbus/Makefile @@ -0,0 +1,88 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Makefile for PMBus chip drivers. +# + +obj-$(CONFIG_PMBUS) += pmbus_core.o +obj-$(CONFIG_SENSORS_PMBUS) += pmbus.o +obj-$(CONFIG_SENSORS_ACBEL_FSG032) += acbel-fsg032.o +obj-$(CONFIG_SENSORS_ADM1266) += adm1266.o +obj-$(CONFIG_SENSORS_ADM1275) += adm1275.o +obj-$(CONFIG_SENSORS_ADP1050) += adp1050.o +obj-$(CONFIG_SENSORS_APS_379) += aps-379.o +obj-$(CONFIG_SENSORS_BEL_PFE) += bel-pfe.o +obj-$(CONFIG_SENSORS_BPA_RS600) += bpa-rs600.o +obj-$(CONFIG_SENSORS_DELTA_AHE50DC_FAN) += delta-ahe50dc-fan.o +obj-$(CONFIG_SENSORS_FSP_3Y) += fsp-3y.o +obj-$(CONFIG_SENSORS_HAC300S) += hac300s.o +obj-$(CONFIG_SENSORS_IBM_CFFPS) += ibm-cffps.o +obj-$(CONFIG_SENSORS_DPS920AB) += dps920ab.o +obj-$(CONFIG_SENSORS_E50SN12051) += e50sn12051.o +obj-$(CONFIG_SENSORS_INA233) += ina233.o +obj-$(CONFIG_SENSORS_INSPUR_IPSPS) += inspur-ipsps.o +obj-$(CONFIG_SENSORS_IR35221) += ir35221.o +obj-$(CONFIG_SENSORS_IR36021) += ir36021.o +obj-$(CONFIG_SENSORS_IR38064) += ir38064.o +obj-$(CONFIG_SENSORS_IRPS5401) += irps5401.o +obj-$(CONFIG_SENSORS_ISL68137) += isl68137.o +obj-$(CONFIG_SENSORS_LM25066) += lm25066.o +obj-$(CONFIG_SENSORS_LT3074) += lt3074.o +obj-$(CONFIG_SENSORS_LT7182S) += lt7182s.o +obj-$(CONFIG_SENSORS_LTC2978) += ltc2978.o +obj-$(CONFIG_SENSORS_LTC3815) += ltc3815.o +obj-$(CONFIG_SENSORS_LTC4286) += ltc4286.o +obj-$(CONFIG_SENSORS_LX1308) += lx1308.o +obj-$(CONFIG_SENSORS_MAX15301) += max15301.o +obj-$(CONFIG_SENSORS_MAX16064) += max16064.o +obj-$(CONFIG_SENSORS_MAX16601) += max16601.o +obj-$(CONFIG_SENSORS_MAX17616) += max17616.o +obj-$(CONFIG_SENSORS_MAX20730) += max20730.o +obj-$(CONFIG_SENSORS_MAX20751) += max20751.o +obj-$(CONFIG_SENSORS_MAX20830) += max20830.o +obj-$(CONFIG_SENSORS_MAX20860A) += max20860a.o +obj-$(CONFIG_SENSORS_MAX31785) += max31785.o +obj-$(CONFIG_SENSORS_MAX34440) += max34440.o +obj-$(CONFIG_SENSORS_MAX8688) += max8688.o +obj-$(CONFIG_SENSORS_MP2856) += mp2856.o +obj-$(CONFIG_SENSORS_MP2869) += mp2869.o +obj-$(CONFIG_SENSORS_MP2888) += mp2888.o +obj-$(CONFIG_SENSORS_MP2891) += mp2891.o +obj-$(CONFIG_SENSORS_MP2925) += mp2925.o +obj-$(CONFIG_SENSORS_MP29502) += mp29502.o +obj-$(CONFIG_SENSORS_MP2975) += mp2975.o +obj-$(CONFIG_SENSORS_MP2985) += mp2985.o +obj-$(CONFIG_SENSORS_MP2993) += mp2993.o +obj-$(CONFIG_SENSORS_MP5023) += mp5023.o +obj-$(CONFIG_SENSORS_MP5920) += mp5920.o +obj-$(CONFIG_SENSORS_MP5926) += mp5926.o +obj-$(CONFIG_SENSORS_MP5990) += mp5990.o +obj-$(CONFIG_SENSORS_MP9941) += mp9941.o +obj-$(CONFIG_SENSORS_MP9945) += mp9945.o +obj-$(CONFIG_SENSORS_MPQ7932) += mpq7932.o +obj-$(CONFIG_SENSORS_MPQ82D00) += mpq82d00.o +obj-$(CONFIG_SENSORS_MPQ8785) += mpq8785.o +obj-$(CONFIG_SENSORS_MPQ8646) += mpq8646.o +obj-$(CONFIG_SENSORS_PLI1209BC) += pli1209bc.o +obj-$(CONFIG_SENSORS_PM6764TR) += pm6764tr.o +obj-$(CONFIG_SENSORS_PXE1610) += pxe1610.o +obj-$(CONFIG_SENSORS_Q54SJ108A2) += q54sj108a2.o +obj-$(CONFIG_SENSORS_STEF48H28) += stef48h28.o +obj-$(CONFIG_SENSORS_SQ24860) += sq24860.o +obj-$(CONFIG_SENSORS_STPDDC60) += stpddc60.o +obj-$(CONFIG_SENSORS_TDA38640) += tda38640.o +obj-$(CONFIG_SENSORS_TPS25990) += tps25990.o +obj-$(CONFIG_SENSORS_TPS40422) += tps40422.o +obj-$(CONFIG_SENSORS_TPS53679) += tps53679.o +obj-$(CONFIG_SENSORS_TPS546D24) += tps546d24.o +obj-$(CONFIG_SENSORS_UCD9000) += ucd9000.o +obj-$(CONFIG_SENSORS_UCD9200) += ucd9200.o +obj-$(CONFIG_SENSORS_VT7505) += vt7505.o +obj-$(CONFIG_SENSORS_XDP710) += xdp710.o +obj-$(CONFIG_SENSORS_XDP720) += xdp720.o +obj-$(CONFIG_SENSORS_XDPE122) += xdpe12284.o +obj-$(CONFIG_SENSORS_XDPE152) += xdpe152c4.o +obj-$(CONFIG_SENSORS_XDPE1A2G7B) += xdpe1a2g7b.o +obj-$(CONFIG_SENSORS_ZL6100) += zl6100.o +obj-$(CONFIG_SENSORS_PIM4328) += pim4328.o +obj-$(CONFIG_SENSORS_CRPS) += crps.o +obj-$(CONFIG_SENSORS_D1U74T) += d1u74t.o diff --git a/drivers/hwmon/pmbus/acbel-fsg032.c b/drivers/hwmon/pmbus/acbel-fsg032.c new file mode 100644 index 000000000..5222e7aec --- /dev/null +++ b/drivers/hwmon/pmbus/acbel-fsg032.c @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright 2023 IBM Corp. + */ + +#include <linux/debugfs.h> +#include <linux/device.h> +#include <linux/fs.h> +#include <linux/i2c.h> +#include <linux/minmax.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include <linux/hwmon-sysfs.h> +#include "pmbus.h" + +#define ACBEL_MFR_FW_REVISION 0xd9 + +static ssize_t acbel_fsg032_debugfs_read(struct file *file, char __user *buf, size_t count, + loff_t *ppos) +{ + struct i2c_client *client = file->private_data; + u8 data[I2C_SMBUS_BLOCK_MAX + 2] = { 0 }; + char out[8]; + int rc; + + rc = i2c_smbus_read_block_data(client, ACBEL_MFR_FW_REVISION, data); + if (rc < 0) + return rc; + + rc = snprintf(out, sizeof(out), "%*phN\n", min(rc, 3), data); + return simple_read_from_buffer(buf, count, ppos, out, rc); +} + +static const struct file_operations acbel_debugfs_ops = { + .llseek = noop_llseek, + .read = acbel_fsg032_debugfs_read, + .write = NULL, + .open = simple_open, +}; + +static void acbel_fsg032_init_debugfs(struct i2c_client *client) +{ + struct dentry *debugfs = pmbus_get_debugfs_dir(client); + + if (!debugfs) + return; + + debugfs_create_file("fw_version", 0444, debugfs, client, &acbel_debugfs_ops); +} + +static const struct i2c_device_id acbel_fsg032_id[] = { + { .name = "acbel_fsg032" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, acbel_fsg032_id); + +static struct pmbus_driver_info acbel_fsg032_info = { + .pages = 1, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | + PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_TEMP3 | + PMBUS_HAVE_FAN12 | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_FAN12, +}; + +static int acbel_fsg032_probe(struct i2c_client *client) +{ + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + struct device *dev = &client->dev; + int rc; + + rc = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, buf); + if (rc < 0) { + dev_err(dev, "Failed to read PMBUS_MFR_ID\n"); + return rc; + } + if (strncmp(buf, "ACBEL", 5)) { + buf[rc] = '\0'; + dev_err(dev, "Manufacturer '%s' not supported\n", buf); + return -ENODEV; + } + + rc = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (rc < 0) { + dev_err(dev, "Failed to read PMBUS_MFR_MODEL\n"); + return rc; + } + + if (strncmp(buf, "FSG032", 6)) { + buf[rc] = '\0'; + dev_err(dev, "Model '%s' not supported\n", buf); + return -ENODEV; + } + + rc = pmbus_do_probe(client, &acbel_fsg032_info); + if (rc) + return rc; + + acbel_fsg032_init_debugfs(client); + return 0; +} + +static const struct of_device_id acbel_fsg032_of_match[] = { + { .compatible = "acbel,fsg032" }, + {} +}; +MODULE_DEVICE_TABLE(of, acbel_fsg032_of_match); + +static struct i2c_driver acbel_fsg032_driver = { + .driver = { + .name = "acbel-fsg032", + .of_match_table = acbel_fsg032_of_match, + }, + .probe = acbel_fsg032_probe, + .id_table = acbel_fsg032_id, +}; + +module_i2c_driver(acbel_fsg032_driver); + +MODULE_AUTHOR("Lakshmi Yadlapati"); +MODULE_DESCRIPTION("PMBus driver for AcBel Power System power supplies"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/adm1266.c b/drivers/hwmon/pmbus/adm1266.c new file mode 100644 index 000000000..22ccb721d --- /dev/null +++ b/drivers/hwmon/pmbus/adm1266.c @@ -0,0 +1,673 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * ADM1266 - Cascadable Super Sequencer with Margin + * Control and Fault Recording + * + * Copyright 2020 Analog Devices Inc. + */ + +#include <linux/bitfield.h> +#include <linux/crc8.h> +#include <linux/debugfs.h> +#include <linux/gpio/driver.h> +#include <linux/i2c.h> +#include <linux/i2c-smbus.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/nvmem-consumer.h> +#include <linux/nvmem-provider.h> +#include "pmbus.h" +#include <linux/slab.h> +#include <linux/timekeeping.h> + +#define ADM1266_IC_DEVICE_REV 0xAE +#define ADM1266_BLACKBOX_CONFIG 0xD3 +#define ADM1266_PDIO_CONFIG 0xD4 +#define ADM1266_READ_STATE 0xD9 +#define ADM1266_READ_BLACKBOX 0xDE +#define ADM1266_SET_RTC 0xDF +#define ADM1266_GPIO_CONFIG 0xE1 +#define ADM1266_POWERUP_COUNTER 0xE4 +#define ADM1266_BLACKBOX_INFO 0xE6 +#define ADM1266_PDIO_STATUS 0xE9 +#define ADM1266_GPIO_STATUS 0xEA + +/* ADM1266 GPIO defines */ +#define ADM1266_GPIO_NR 9 +#define ADM1266_GPIO_FUNCTIONS(x) FIELD_GET(BIT(0), x) +#define ADM1266_GPIO_INPUT_EN(x) FIELD_GET(BIT(2), x) +#define ADM1266_GPIO_OUTPUT_EN(x) FIELD_GET(BIT(3), x) +#define ADM1266_GPIO_OPEN_DRAIN(x) FIELD_GET(BIT(4), x) + +/* ADM1266 PDIO defines */ +#define ADM1266_PDIO_NR 16 +#define ADM1266_PDIO_PIN_CFG(x) FIELD_GET(GENMASK(15, 13), x) +#define ADM1266_PDIO_GLITCH_FILT(x) FIELD_GET(GENMASK(12, 9), x) +#define ADM1266_PDIO_OUT_CFG(x) FIELD_GET(GENMASK(2, 0), x) + +#define ADM1266_BLACKBOX_OFFSET 0 +#define ADM1266_BLACKBOX_SIZE 64 +#define ADM1266_BLACKBOX_MAX_RECORDS 32 + +#define ADM1266_PMBUS_BLOCK_MAX 255 + +struct adm1266_data { + struct pmbus_driver_info info; + struct gpio_chip gc; + const char *gpio_names[ADM1266_GPIO_NR + ADM1266_PDIO_NR]; + struct i2c_client *client; + struct dentry *debugfs_dir; + struct nvmem_config nvmem_config; + struct nvmem_device *nvmem; + u8 *dev_mem; + struct mutex buf_mutex; + u8 write_buf[ADM1266_PMBUS_BLOCK_MAX + 1] ____cacheline_aligned; + u8 read_buf[ADM1266_PMBUS_BLOCK_MAX + 2] ____cacheline_aligned; +}; + +static const struct nvmem_cell_info adm1266_nvmem_cells[] = { + { + .name = "blackbox", + .offset = ADM1266_BLACKBOX_OFFSET, + .bytes = 2048, + }, +}; + +DECLARE_CRC8_TABLE(pmbus_crc_table); + +/* + * Different from Block Read as it sends data and waits for the slave to + * return a value dependent on that data. The protocol is simply a Write Block + * followed by a Read Block without the Read-Block command field and the + * Write-Block STOP bit. + */ +static int adm1266_pmbus_block_xfer(struct adm1266_data *data, u8 cmd, u8 w_len, u8 *data_w, + u8 *data_r) +{ + struct i2c_client *client = data->client; + struct i2c_msg msgs[2] = { + { + .addr = client->addr, + .flags = I2C_M_DMA_SAFE, + .buf = data->write_buf, + .len = w_len + 2, + }, + { + .addr = client->addr, + .flags = I2C_M_RD | I2C_M_DMA_SAFE, + .buf = data->read_buf, + .len = ADM1266_PMBUS_BLOCK_MAX + 2, + } + }; + u8 addr; + u8 crc; + int ret; + + mutex_lock(&data->buf_mutex); + + msgs[0].buf[0] = cmd; + msgs[0].buf[1] = w_len; + memcpy(&msgs[0].buf[2], data_w, w_len); + + ret = i2c_transfer(client->adapter, msgs, 2); + if (ret != 2) { + if (ret >= 0) + ret = -EPROTO; + + mutex_unlock(&data->buf_mutex); + + return ret; + } + + if (client->flags & I2C_CLIENT_PEC) { + addr = i2c_8bit_addr_from_msg(&msgs[0]); + crc = crc8(pmbus_crc_table, &addr, 1, 0); + crc = crc8(pmbus_crc_table, msgs[0].buf, msgs[0].len, crc); + + addr = i2c_8bit_addr_from_msg(&msgs[1]); + crc = crc8(pmbus_crc_table, &addr, 1, crc); + crc = crc8(pmbus_crc_table, msgs[1].buf, msgs[1].buf[0] + 1, crc); + + if (crc != msgs[1].buf[msgs[1].buf[0] + 1]) { + mutex_unlock(&data->buf_mutex); + return -EBADMSG; + } + } + + memcpy(data_r, &msgs[1].buf[1], msgs[1].buf[0]); + + ret = msgs[1].buf[0]; + mutex_unlock(&data->buf_mutex); + + return ret; +} + +static const unsigned int adm1266_gpio_mapping[ADM1266_GPIO_NR][2] = { + {1, 0}, + {2, 1}, + {3, 2}, + {4, 8}, + {5, 9}, + {6, 10}, + {7, 11}, + {8, 6}, + {9, 7}, +}; + +static const char *adm1266_names[ADM1266_GPIO_NR + ADM1266_PDIO_NR] = { + "GPIO1", "GPIO2", "GPIO3", "GPIO4", "GPIO5", "GPIO6", "GPIO7", "GPIO8", + "GPIO9", "PDIO1", "PDIO2", "PDIO3", "PDIO4", "PDIO5", "PDIO6", + "PDIO7", "PDIO8", "PDIO9", "PDIO10", "PDIO11", "PDIO12", "PDIO13", + "PDIO14", "PDIO15", "PDIO16", +}; + +static int adm1266_gpio_get(struct gpio_chip *chip, unsigned int offset) +{ + struct adm1266_data *data = gpiochip_get_data(chip); + u8 read_buf[I2C_SMBUS_BLOCK_MAX + 1]; + unsigned long pins_status; + unsigned int pmbus_cmd; + int ret; + + if (offset < ADM1266_GPIO_NR) + pmbus_cmd = ADM1266_GPIO_STATUS; + else + pmbus_cmd = ADM1266_PDIO_STATUS; + + guard(pmbus_lock)(data->client); + + ret = i2c_smbus_read_block_data(data->client, pmbus_cmd, read_buf); + if (ret < 0) + return ret; + if (ret < 2) + return -EIO; + + pins_status = read_buf[0] + (read_buf[1] << 8); + if (offset < ADM1266_GPIO_NR) + return test_bit(adm1266_gpio_mapping[offset][1], &pins_status); + + return test_bit(offset - ADM1266_GPIO_NR, &pins_status); +} + +static int adm1266_gpio_get_multiple(struct gpio_chip *chip, unsigned long *mask, + unsigned long *bits) +{ + struct adm1266_data *data = gpiochip_get_data(chip); + u8 read_buf[ADM1266_PMBUS_BLOCK_MAX + 1]; + unsigned long status; + unsigned int gpio_nr; + int ret; + + guard(pmbus_lock)(data->client); + + ret = i2c_smbus_read_block_data(data->client, ADM1266_GPIO_STATUS, read_buf); + if (ret < 0) + return ret; + if (ret < 2) + return -EIO; + + status = read_buf[0] + (read_buf[1] << 8); + + *bits = 0; + for_each_set_bit(gpio_nr, mask, ADM1266_GPIO_NR) { + if (test_bit(adm1266_gpio_mapping[gpio_nr][1], &status)) + set_bit(gpio_nr, bits); + } + + ret = i2c_smbus_read_block_data(data->client, ADM1266_PDIO_STATUS, read_buf); + if (ret < 0) + return ret; + if (ret < 2) + return -EIO; + + status = read_buf[0] + (read_buf[1] << 8); + + for_each_set_bit_from(gpio_nr, mask, ADM1266_GPIO_NR + ADM1266_PDIO_NR) { + if (test_bit(gpio_nr - ADM1266_GPIO_NR, &status)) + set_bit(gpio_nr, bits); + } + + return 0; +} + +static void adm1266_gpio_dbg_show(struct seq_file *s, struct gpio_chip *chip) +{ + struct adm1266_data *data = gpiochip_get_data(chip); + u8 read_buf[ADM1266_PMBUS_BLOCK_MAX + 1]; + unsigned long gpio_config; + unsigned long pdio_config; + unsigned long pin_cfg; + u8 write_cmd; + int ret; + int i; + + guard(pmbus_lock)(data->client); + + for (i = 0; i < ADM1266_GPIO_NR; i++) { + write_cmd = adm1266_gpio_mapping[i][1]; + ret = adm1266_pmbus_block_xfer(data, ADM1266_GPIO_CONFIG, 1, &write_cmd, read_buf); + if (ret != 2) + return; + + gpio_config = read_buf[0]; + seq_puts(s, adm1266_names[i]); + + seq_puts(s, " ( "); + if (!ADM1266_GPIO_FUNCTIONS(gpio_config)) { + seq_puts(s, "high-Z )\n"); + continue; + } + if (ADM1266_GPIO_INPUT_EN(gpio_config)) + seq_puts(s, "input "); + if (ADM1266_GPIO_OUTPUT_EN(gpio_config)) + seq_puts(s, "output "); + if (ADM1266_GPIO_OPEN_DRAIN(gpio_config)) + seq_puts(s, "open-drain )\n"); + else + seq_puts(s, "push-pull )\n"); + } + + write_cmd = 0xFF; + ret = adm1266_pmbus_block_xfer(data, ADM1266_PDIO_CONFIG, 1, &write_cmd, read_buf); + if (ret != 32) + return; + + for (i = 0; i < ADM1266_PDIO_NR; i++) { + seq_puts(s, adm1266_names[ADM1266_GPIO_NR + i]); + + pdio_config = read_buf[2 * i]; + pdio_config += (read_buf[2 * i + 1] << 8); + pin_cfg = ADM1266_PDIO_PIN_CFG(pdio_config); + + seq_puts(s, " ( "); + if (!pin_cfg || pin_cfg > 5) { + seq_puts(s, "high-Z )\n"); + continue; + } + + if (pin_cfg & BIT(0)) + seq_puts(s, "output "); + + if (pin_cfg & BIT(1)) + seq_puts(s, "input "); + + seq_puts(s, ")\n"); + } +} + +static int adm1266_config_gpio(struct adm1266_data *data) +{ + const char *name = dev_name(&data->client->dev); + char *gpio_name; + int ret; + int i; + + for (i = 0; i < ARRAY_SIZE(data->gpio_names); i++) { + gpio_name = devm_kasprintf(&data->client->dev, GFP_KERNEL, "adm1266-%d-%x-%s", + data->client->adapter->nr, data->client->addr, + adm1266_names[i]); + if (!gpio_name) + return -ENOMEM; + + data->gpio_names[i] = gpio_name; + } + + data->gc.label = name; + data->gc.parent = &data->client->dev; + data->gc.owner = THIS_MODULE; + data->gc.can_sleep = true; + data->gc.base = -1; + data->gc.names = data->gpio_names; + data->gc.ngpio = ARRAY_SIZE(data->gpio_names); + data->gc.get = adm1266_gpio_get; + data->gc.get_multiple = adm1266_gpio_get_multiple; + data->gc.dbg_show = adm1266_gpio_dbg_show; + + ret = devm_gpiochip_add_data(&data->client->dev, &data->gc, data); + if (ret) + dev_err(&data->client->dev, "GPIO registering failed (%d)\n", ret); + + return ret; +} + +static int adm1266_state_read(struct seq_file *s, void *pdata) +{ + struct device *dev = s->private; + struct i2c_client *client = to_i2c_client(dev); + int ret; + + guard(pmbus_lock)(client); + ret = i2c_smbus_read_word_data(client, ADM1266_READ_STATE); + if (ret < 0) + return ret; + + seq_printf(s, "%d\n", ret); + + return 0; +} + +/* + * IC_DEVICE_REV (0xAE) returns an 8-byte block (datasheet Rev. D, Table 80): + * [2:0] firmware revision major.minor.patch + * [5:3] bootloader revision major.minor.patch + * [7:6] silicon revision two ASCII characters + */ +static int adm1266_firmware_revision_read(struct seq_file *s, void *pdata) +{ + struct device *dev = s->private; + struct i2c_client *client = to_i2c_client(dev); + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + + guard(pmbus_lock)(client); + ret = i2c_smbus_read_block_data(client, ADM1266_IC_DEVICE_REV, buf); + if (ret < 0) + return ret; + if (ret < 3) + return -EIO; + + seq_printf(s, "%u.%u.%u\n", buf[0], buf[1], buf[2]); + + return 0; +} + +/* + * POWERUP_COUNTER (0xE4) is a 2-byte little-endian non-volatile counter + * that increments on every device power cycle (datasheet Rev. D, Table + * 93). It saturates at 65535 and cannot be reset by the host. Each + * blackbox record embeds the counter value at record time, so this live + * read is mainly useful for matching a record back to its boot. + */ +static int adm1266_powerup_counter_read(struct seq_file *s, void *pdata) +{ + struct device *dev = s->private; + struct i2c_client *client = to_i2c_client(dev); + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + + guard(pmbus_lock)(client); + ret = i2c_smbus_read_block_data(client, ADM1266_POWERUP_COUNTER, buf); + if (ret < 0) + return ret; + if (ret != 2) + return -EIO; + + seq_printf(s, "%u\n", buf[0] | (buf[1] << 8)); + + return 0; +} + +/* + * Clearing the blackbox is required when the device is configured in + * single-recording mode (BLACKBOX_CONFIG[0] = 0): once the 32-record + * buffer is full the device stops recording until cleared. + * + * The clear is issued as a 2-byte block-write to READ_BLACKBOX with + * payload {0xFE, 0x00} per the datasheet. READ_BLACKBOX is also used + * by adm1266_nvmem_read_blackbox() to walk records one at a time; + * both paths run under pmbus_lock so the clear cannot interleave + * mid-iteration and corrupt the read sequence. + */ +static ssize_t adm1266_clear_blackbox_write(struct file *file, const char __user *ubuf, + size_t count, loff_t *ppos) +{ + struct i2c_client *client = file->private_data; + u8 payload[2] = { 0xFE, 0x00 }; + int ret; + + guard(pmbus_lock)(client); + ret = i2c_smbus_write_block_data(client, ADM1266_READ_BLACKBOX, + sizeof(payload), payload); + if (ret < 0) + return ret; + + return count; +} + +static const struct file_operations adm1266_clear_blackbox_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = adm1266_clear_blackbox_write, + .llseek = noop_llseek, +}; + +/* + * SET_RTC (0xDF) is a 6-byte block (datasheet Rev. D, Table 84): + * bytes [1:0] - fractional seconds (1/65536 s, written as zero) + * bytes [5:2] - seconds since 1970-01-01 UTC, little-endian + * + * The driver seeds it once at probe via adm1266_rtc_set(). Over a + * long uptime the chip's counter drifts away from host wall-clock, + * so expose it via debugfs: + * + * read -- returns the chip's current seconds counter, which lets + * userspace observe host-vs-chip drift. + * write -- the kernel re-reads ktime_get_real_seconds() and writes + * it to SET_RTC. The write payload is ignored; userspace + * does not get to supply its own timestamp value, so + * there is no way to push a wrong time into the chip. + * + * A small userspace agent (chrony hook, systemd-timesyncd script, + * or a periodic cron job) can write to this file to keep the + * timestamp embedded in each blackbox record aligned with + * wall-clock across long uptimes. + */ +static int adm1266_rtc_get(void *data, u64 *val) +{ + struct i2c_client *client = data; + u8 buf[I2C_SMBUS_BLOCK_MAX]; + u32 seconds = 0; + int ret, i; + + guard(pmbus_lock)(client); + ret = i2c_smbus_read_block_data(client, ADM1266_SET_RTC, buf); + if (ret < 0) + return ret; + if (ret < 6) + return -EIO; + + for (i = 0; i < 4; i++) + seconds |= (u32)buf[2 + i] << (i * 8); + + *val = seconds; + + return 0; +} + +static int adm1266_rtc_set(void *data, u64 val) +{ + struct i2c_client *client = data; + time64_t kt = ktime_get_real_seconds(); + u8 write_buf[6] = { 0 }; + int i; + + /* User-supplied @val is ignored on purpose; the kernel owns the + * time source so userspace cannot push a wrong value into the chip. + */ + for (i = 0; i < 4; i++) + write_buf[2 + i] = (kt >> (i * 8)) & 0xFF; + + guard(pmbus_lock)(client); + return i2c_smbus_write_block_data(client, ADM1266_SET_RTC, + sizeof(write_buf), write_buf); +} +DEFINE_DEBUGFS_ATTRIBUTE(adm1266_rtc_fops, + adm1266_rtc_get, adm1266_rtc_set, "%llu\n"); + +static void adm1266_init_debugfs(struct adm1266_data *data) +{ + struct dentry *root; + + root = pmbus_get_debugfs_dir(data->client); + if (!root) + return; + + data->debugfs_dir = debugfs_create_dir(data->client->name, root); + + debugfs_create_devm_seqfile(&data->client->dev, "sequencer_state", data->debugfs_dir, + adm1266_state_read); + debugfs_create_devm_seqfile(&data->client->dev, "firmware_revision", data->debugfs_dir, + adm1266_firmware_revision_read); + debugfs_create_devm_seqfile(&data->client->dev, "powerup_counter", data->debugfs_dir, + adm1266_powerup_counter_read); + debugfs_create_file("clear_blackbox", 0200, data->debugfs_dir, data->client, + &adm1266_clear_blackbox_fops); + debugfs_create_file("rtc", 0600, data->debugfs_dir, data->client, + &adm1266_rtc_fops); +} + +static int adm1266_nvmem_read_blackbox(struct adm1266_data *data, u8 *read_buff) +{ + u8 record[ADM1266_PMBUS_BLOCK_MAX]; + int record_count; + char index; + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + + ret = i2c_smbus_read_block_data(data->client, ADM1266_BLACKBOX_INFO, buf); + if (ret < 0) + return ret; + + if (ret != 4) + return -EIO; + + record_count = buf[3]; + if (record_count > ADM1266_BLACKBOX_MAX_RECORDS) + return -EIO; + + for (index = 0; index < record_count; index++) { + ret = adm1266_pmbus_block_xfer(data, ADM1266_READ_BLACKBOX, 1, &index, record); + if (ret < 0) + return ret; + + if (ret != ADM1266_BLACKBOX_SIZE) + return -EIO; + + memcpy(read_buff, record, ADM1266_BLACKBOX_SIZE); + read_buff += ADM1266_BLACKBOX_SIZE; + } + + return 0; +} + +static int adm1266_nvmem_read(void *priv, unsigned int offset, void *val, size_t bytes) +{ + struct adm1266_data *data = priv; + int ret; + + if (offset + bytes > data->nvmem_config.size) + return -EINVAL; + + guard(pmbus_lock)(data->client); + + if (offset == 0) { + memset(data->dev_mem, 0, data->nvmem_config.size); + + ret = adm1266_nvmem_read_blackbox(data, data->dev_mem); + if (ret) { + dev_err(&data->client->dev, "Could not read blackbox!"); + return ret; + } + } + + memcpy(val, data->dev_mem + offset, bytes); + + return 0; +} + +static int adm1266_config_nvmem(struct adm1266_data *data) +{ + data->nvmem_config.name = dev_name(&data->client->dev); + data->nvmem_config.dev = &data->client->dev; + data->nvmem_config.root_only = true; + data->nvmem_config.read_only = true; + data->nvmem_config.owner = THIS_MODULE; + data->nvmem_config.reg_read = adm1266_nvmem_read; + data->nvmem_config.cells = adm1266_nvmem_cells; + data->nvmem_config.ncells = ARRAY_SIZE(adm1266_nvmem_cells); + data->nvmem_config.priv = data; + data->nvmem_config.stride = 1; + data->nvmem_config.word_size = 1; + data->nvmem_config.size = adm1266_nvmem_cells[0].bytes; + + data->dev_mem = devm_kzalloc(&data->client->dev, data->nvmem_config.size, GFP_KERNEL); + if (!data->dev_mem) + return -ENOMEM; + + data->nvmem = devm_nvmem_register(&data->client->dev, &data->nvmem_config); + if (IS_ERR(data->nvmem)) { + dev_err(&data->client->dev, "Could not register nvmem!"); + return PTR_ERR(data->nvmem); + } + + return 0; +} + +static int adm1266_probe(struct i2c_client *client) +{ + struct adm1266_data *data; + int ret; + int i; + + data = devm_kzalloc(&client->dev, sizeof(struct adm1266_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->client = client; + data->info.pages = 17; + data->info.format[PSC_VOLTAGE_OUT] = linear; + for (i = 0; i < data->info.pages; i++) + data->info.func[i] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT; + + crc8_populate_msb(pmbus_crc_table, 0x7); + mutex_init(&data->buf_mutex); + + ret = pmbus_do_probe(client, &data->info); + if (ret) + return ret; + + ret = adm1266_rtc_set(client, 0); + if (ret < 0) + return ret; + + ret = adm1266_config_nvmem(data); + if (ret < 0) + return ret; + + ret = adm1266_config_gpio(data); + if (ret < 0) + return ret; + + adm1266_init_debugfs(data); + + return 0; +} + +static const struct of_device_id adm1266_of_match[] = { + { .compatible = "adi,adm1266" }, + { } +}; +MODULE_DEVICE_TABLE(of, adm1266_of_match); + +static const struct i2c_device_id adm1266_id[] = { + { .name = "adm1266" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, adm1266_id); + +static struct i2c_driver adm1266_driver = { + .driver = { + .name = "adm1266", + .of_match_table = adm1266_of_match, + }, + .probe = adm1266_probe, + .id_table = adm1266_id, +}; + +module_i2c_driver(adm1266_driver); + +MODULE_AUTHOR("Alexandru Tachici <alexandru.tachici@analog.com>"); +MODULE_DESCRIPTION("PMBus driver for Analog Devices ADM1266"); +MODULE_LICENSE("GPL v2"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/adm1275.c b/drivers/hwmon/pmbus/adm1275.c new file mode 100644 index 000000000..beb32a878 --- /dev/null +++ b/drivers/hwmon/pmbus/adm1275.c @@ -0,0 +1,1009 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Analog Devices ADM1275 Hot-Swap Controller + * and Digital Power Monitor + * + * Copyright (c) 2011 Ericsson AB. + * Copyright (c) 2018 Guenter Roeck + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/bitops.h> +#include <linux/bitfield.h> +#include <linux/log2.h> +#include "pmbus.h" + +enum chips { adm1075, adm1272, adm1273, adm1275, adm1276, adm1278, adm1281, + adm1293, adm1294, bd12780, bd12790, sq24905c }; + +#define ADM1275_MFR_STATUS_IOUT_WARN2 BIT(0) +#define ADM1293_MFR_STATUS_VAUX_UV_WARN BIT(5) +#define ADM1293_MFR_STATUS_VAUX_OV_WARN BIT(6) + +#define ADM1275_PEAK_IOUT 0xd0 +#define ADM1275_PEAK_VIN 0xd1 +#define ADM1275_PEAK_VOUT 0xd2 +#define ADM1275_PMON_CONTROL 0xd3 +#define ADM1275_PMON_CONFIG 0xd4 + +#define ADM1275_CONVERT_EN BIT(0) + +#define ADM1275_VIN_VOUT_SELECT BIT(6) +#define ADM1275_VRANGE BIT(5) +#define ADM1075_IRANGE_50 BIT(4) +#define ADM1075_IRANGE_25 BIT(3) +#define ADM1075_IRANGE_MASK (BIT(3) | BIT(4)) + +#define ADM1272_IRANGE BIT(0) + +#define ADM1278_TSFILT BIT(15) +#define ADM1278_TEMP1_EN BIT(3) +#define ADM1278_VIN_EN BIT(2) +#define ADM1278_VOUT_EN BIT(1) + +#define ADM1278_PMON_DEFCONFIG (ADM1278_VOUT_EN | ADM1278_TEMP1_EN | ADM1278_TSFILT) +/* The BD127[89]0 data sheets mark TSFILT bit as reserved. */ +#define BD12780_PMON_DEFCONFIG (ADM1278_VOUT_EN | ADM1278_TEMP1_EN) + +#define ADM1293_IRANGE_25 0 +#define ADM1293_IRANGE_50 BIT(6) +#define ADM1293_IRANGE_100 BIT(7) +#define ADM1293_IRANGE_200 (BIT(6) | BIT(7)) +#define ADM1293_IRANGE_MASK (BIT(6) | BIT(7)) + +#define ADM1293_VIN_SEL_012 BIT(2) +#define ADM1293_VIN_SEL_074 BIT(3) +#define ADM1293_VIN_SEL_210 (BIT(2) | BIT(3)) +#define ADM1293_VIN_SEL_MASK (BIT(2) | BIT(3)) + +#define ADM1293_VAUX_EN BIT(1) + +#define ADM1278_PEAK_TEMP 0xd7 +#define ADM1275_IOUT_WARN2_LIMIT 0xd7 +#define ADM1275_DEVICE_CONFIG 0xd8 + +#define ADM1275_IOUT_WARN2_SELECT BIT(4) + +#define ADM1276_PEAK_PIN 0xda +#define ADM1075_READ_VAUX 0xdd +#define ADM1075_VAUX_OV_WARN_LIMIT 0xde +#define ADM1075_VAUX_UV_WARN_LIMIT 0xdf +#define ADM1293_IOUT_MIN 0xe3 +#define ADM1293_PIN_MIN 0xe4 +#define ADM1075_VAUX_STATUS 0xf6 + +#define ADM1075_VAUX_OV_WARN BIT(7) +#define ADM1075_VAUX_UV_WARN BIT(6) + +#define ADM1275_VI_AVG_SHIFT 0 +#define ADM1275_VI_AVG_MASK GENMASK(ADM1275_VI_AVG_SHIFT + 2, \ + ADM1275_VI_AVG_SHIFT) +#define ADM1275_SAMPLES_AVG_MAX 128 + +#define ADM1278_PWR_AVG_SHIFT 11 +#define ADM1278_PWR_AVG_MASK GENMASK(ADM1278_PWR_AVG_SHIFT + 2, \ + ADM1278_PWR_AVG_SHIFT) +#define ADM1278_VI_AVG_SHIFT 8 +#define ADM1278_VI_AVG_MASK GENMASK(ADM1278_VI_AVG_SHIFT + 2, \ + ADM1278_VI_AVG_SHIFT) + +struct adm1275_data { + int id; + bool have_oc_fault; + bool have_uc_fault; + bool have_vout; + bool have_vaux_status; + bool have_mfr_vaux_status; + bool have_iout_min; + bool have_pin_min; + bool have_pin_max; + bool have_temp_max; + bool have_power_sampling; + struct pmbus_driver_info info; +}; + +#define to_adm1275_data(x) container_of(x, struct adm1275_data, info) + +struct coefficients { + s16 m; + s16 b; + s16 R; +}; + +static const struct coefficients adm1075_coefficients[] = { + [0] = { 27169, 0, -1 }, /* voltage */ + [1] = { 806, 20475, -1 }, /* current, irange25 */ + [2] = { 404, 20475, -1 }, /* current, irange50 */ + [3] = { 8549, 0, -1 }, /* power, irange25 */ + [4] = { 4279, 0, -1 }, /* power, irange50 */ +}; + +static const struct coefficients adm1272_coefficients[] = { + [0] = { 6770, 0, -2 }, /* voltage, vrange 60V */ + [1] = { 4062, 0, -2 }, /* voltage, vrange 100V */ + [2] = { 1326, 20480, -1 }, /* current, vsense range 15mV */ + [3] = { 663, 20480, -1 }, /* current, vsense range 30mV */ + [4] = { 3512, 0, -2 }, /* power, vrange 60V, irange 15mV */ + [5] = { 21071, 0, -3 }, /* power, vrange 100V, irange 15mV */ + [6] = { 17561, 0, -3 }, /* power, vrange 60V, irange 30mV */ + [7] = { 10535, 0, -3 }, /* power, vrange 100V, irange 30mV */ + [8] = { 42, 31871, -1 }, /* temperature */ + +}; + +/* + * BD12790 coefficients derived from preliminary datasheet, Table 1 (p.18) + * and the PMBus direct-format relationship X = (Y * 10^(-R) - b) / m. + * + * Voltage: V[V] = 14.77e-3 * code (60V) / 24.62e-3 * code (100V) + * -> m = 6770, R=-2 / m = 4062, R=-2 + * Current: code = I[A] * RS * 132802.1 + 2048 (15mV) / * 66401.06 + 2048 (30mV) + * -> m = 1328, b = 2048 * 10^(-R) = 20480, R=-1 / m = 664, same b and R + * Power: code = k * RS * PIN, k = 35119.94 / 17559.97 / 21071.44 / 10535.72 + * -> m = round(k * 10^(-3-R)), R=-2 for 60V/15mV, R=-3 for the other three + * Temperature: code = 4.2 * T + 3188 -> m = 42, b = 3188 * 10 = 31880, R=-1 + */ +static const struct coefficients bd12790_coefficients[] = { + [0] = { 6770, 0, -2 }, /* voltage, vrange 60V */ + [1] = { 4062, 0, -2 }, /* voltage, vrange 100V */ + [2] = { 1328, 20480, -1 }, /* current, vsense range 15mV */ + [3] = { 664, 20480, -1 }, /* current, vsense range 30mV */ + [4] = { 3512, 0, -2 }, /* power, vrange 60V, irange 15mV */ + [5] = { 21071, 0, -3 }, /* power, vrange 100V, irange 15mV */ + [6] = { 17560, 0, -3 }, /* power, vrange 60V, irange 30mV */ + [7] = { 10536, 0, -3 }, /* power, vrange 100V, irange 30mV */ + [8] = { 42, 31880, -1 }, /* temperature */ +}; + +static const struct coefficients adm1275_coefficients[] = { + [0] = { 19199, 0, -2 }, /* voltage, vrange set */ + [1] = { 6720, 0, -1 }, /* voltage, vrange not set */ + [2] = { 807, 20475, -1 }, /* current */ +}; + +static const struct coefficients adm1276_coefficients[] = { + [0] = { 19199, 0, -2 }, /* voltage, vrange set */ + [1] = { 6720, 0, -1 }, /* voltage, vrange not set */ + [2] = { 807, 20475, -1 }, /* current */ + [3] = { 6043, 0, -2 }, /* power, vrange set */ + [4] = { 2115, 0, -1 }, /* power, vrange not set */ +}; + +static const struct coefficients adm1278_coefficients[] = { + [0] = { 19599, 0, -2 }, /* voltage */ + [1] = { 800, 20475, -1 }, /* current */ + [2] = { 6123, 0, -2 }, /* power */ + [3] = { 42, 31880, -1 }, /* temperature */ +}; + +static const struct coefficients adm1293_coefficients[] = { + [0] = { 3333, -1, 0 }, /* voltage, vrange 1.2V */ + [1] = { 5552, -5, -1 }, /* voltage, vrange 7.4V */ + [2] = { 19604, -50, -2 }, /* voltage, vrange 21V */ + [3] = { 8000, -100, -2 }, /* current, irange25 */ + [4] = { 4000, -100, -2 }, /* current, irange50 */ + [5] = { 20000, -1000, -3 }, /* current, irange100 */ + [6] = { 10000, -1000, -3 }, /* current, irange200 */ + [7] = { 10417, 0, -1 }, /* power, 1.2V, irange25 */ + [8] = { 5208, 0, -1 }, /* power, 1.2V, irange50 */ + [9] = { 26042, 0, -2 }, /* power, 1.2V, irange100 */ + [10] = { 13021, 0, -2 }, /* power, 1.2V, irange200 */ + [11] = { 17351, 0, -2 }, /* power, 7.4V, irange25 */ + [12] = { 8676, 0, -2 }, /* power, 7.4V, irange50 */ + [13] = { 4338, 0, -2 }, /* power, 7.4V, irange100 */ + [14] = { 21689, 0, -3 }, /* power, 7.4V, irange200 */ + [15] = { 6126, 0, -2 }, /* power, 21V, irange25 */ + [16] = { 30631, 0, -3 }, /* power, 21V, irange50 */ + [17] = { 15316, 0, -3 }, /* power, 21V, irange100 */ + [18] = { 7658, 0, -3 }, /* power, 21V, irange200 */ +}; + +static int adm1275_read_samples(const struct adm1275_data *data, + struct i2c_client *client, bool is_power) +{ + int shift, ret; + u16 mask; + + /* + * The PMON configuration register is a 16-bit register only on chips + * supporting power average sampling. On other chips it is an 8-bit + * register. + */ + if (data->have_power_sampling) { + ret = i2c_smbus_read_word_data(client, ADM1275_PMON_CONFIG); + mask = is_power ? ADM1278_PWR_AVG_MASK : ADM1278_VI_AVG_MASK; + shift = is_power ? ADM1278_PWR_AVG_SHIFT : ADM1278_VI_AVG_SHIFT; + } else { + ret = i2c_smbus_read_byte_data(client, ADM1275_PMON_CONFIG); + mask = ADM1275_VI_AVG_MASK; + shift = ADM1275_VI_AVG_SHIFT; + } + if (ret < 0) + return ret; + + return (ret & mask) >> shift; +} + +static int adm1275_write_pmon_config(const struct adm1275_data *data, + struct i2c_client *client, u16 word) +{ + int ret, ret2; + + ret = i2c_smbus_write_byte_data(client, ADM1275_PMON_CONTROL, 0); + if (ret) + return ret; + + if (data->have_power_sampling) + ret = i2c_smbus_write_word_data(client, ADM1275_PMON_CONFIG, + word); + else + ret = i2c_smbus_write_byte_data(client, ADM1275_PMON_CONFIG, + word); + + /* + * We still want to re-enable conversions if writing into + * ADM1275_PMON_CONFIG failed. + */ + ret2 = i2c_smbus_write_byte_data(client, ADM1275_PMON_CONTROL, + ADM1275_CONVERT_EN); + if (!ret) + ret = ret2; + + return ret; +} + +static int adm1275_write_samples(const struct adm1275_data *data, + struct i2c_client *client, + bool is_power, u16 word) +{ + int shift, ret; + u16 mask; + + if (data->have_power_sampling) { + ret = i2c_smbus_read_word_data(client, ADM1275_PMON_CONFIG); + mask = is_power ? ADM1278_PWR_AVG_MASK : ADM1278_VI_AVG_MASK; + shift = is_power ? ADM1278_PWR_AVG_SHIFT : ADM1278_VI_AVG_SHIFT; + } else { + ret = i2c_smbus_read_byte_data(client, ADM1275_PMON_CONFIG); + mask = ADM1275_VI_AVG_MASK; + shift = ADM1275_VI_AVG_SHIFT; + } + if (ret < 0) + return ret; + + word = (ret & ~mask) | ((word << shift) & mask); + + return adm1275_write_pmon_config(data, client, word); +} + +static int adm1275_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct adm1275_data *data = to_adm1275_data(info); + int ret = 0; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_IOUT_UC_FAULT_LIMIT: + if (!data->have_uc_fault) + return -ENXIO; + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1275_IOUT_WARN2_LIMIT); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + if (!data->have_oc_fault) + return -ENXIO; + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1275_IOUT_WARN2_LIMIT); + break; + case PMBUS_VOUT_OV_WARN_LIMIT: + if (data->have_vout) + return -ENODATA; + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1075_VAUX_OV_WARN_LIMIT); + break; + case PMBUS_VOUT_UV_WARN_LIMIT: + if (data->have_vout) + return -ENODATA; + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1075_VAUX_UV_WARN_LIMIT); + break; + case PMBUS_READ_VOUT: + if (data->have_vout) + return -ENODATA; + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1075_READ_VAUX); + break; + case PMBUS_VIRT_READ_IOUT_MIN: + if (!data->have_iout_min) + return -ENXIO; + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1293_IOUT_MIN); + break; + case PMBUS_VIRT_READ_IOUT_MAX: + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1275_PEAK_IOUT); + break; + case PMBUS_VIRT_READ_VOUT_MAX: + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1275_PEAK_VOUT); + break; + case PMBUS_VIRT_READ_VIN_MAX: + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1275_PEAK_VIN); + break; + case PMBUS_VIRT_READ_PIN_MIN: + if (!data->have_pin_min) + return -ENXIO; + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1293_PIN_MIN); + break; + case PMBUS_VIRT_READ_PIN_MAX: + if (!data->have_pin_max) + return -ENXIO; + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1276_PEAK_PIN); + break; + case PMBUS_VIRT_READ_TEMP_MAX: + if (!data->have_temp_max) + return -ENXIO; + ret = pmbus_read_word_data(client, 0, 0xff, + ADM1278_PEAK_TEMP); + break; + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_RESET_VOUT_HISTORY: + case PMBUS_VIRT_RESET_VIN_HISTORY: + break; + case PMBUS_VIRT_RESET_PIN_HISTORY: + if (!data->have_pin_max) + return -ENXIO; + break; + case PMBUS_VIRT_RESET_TEMP_HISTORY: + if (!data->have_temp_max) + return -ENXIO; + break; + case PMBUS_VIRT_POWER_SAMPLES: + if (!data->have_power_sampling) + return -ENXIO; + ret = adm1275_read_samples(data, client, true); + if (ret < 0) + break; + ret = BIT(ret); + break; + case PMBUS_VIRT_IN_SAMPLES: + case PMBUS_VIRT_CURR_SAMPLES: + ret = adm1275_read_samples(data, client, false); + if (ret < 0) + break; + ret = BIT(ret); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int adm1275_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct adm1275_data *data = to_adm1275_data(info); + int ret; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_IOUT_UC_FAULT_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + ret = pmbus_write_word_data(client, 0, ADM1275_IOUT_WARN2_LIMIT, + word); + break; + case PMBUS_VIRT_RESET_IOUT_HISTORY: + ret = pmbus_write_word_data(client, 0, ADM1275_PEAK_IOUT, 0); + if (!ret && data->have_iout_min) + ret = pmbus_write_word_data(client, 0, + ADM1293_IOUT_MIN, 0); + break; + case PMBUS_VIRT_RESET_VOUT_HISTORY: + ret = pmbus_write_word_data(client, 0, ADM1275_PEAK_VOUT, 0); + break; + case PMBUS_VIRT_RESET_VIN_HISTORY: + ret = pmbus_write_word_data(client, 0, ADM1275_PEAK_VIN, 0); + break; + case PMBUS_VIRT_RESET_PIN_HISTORY: + ret = pmbus_write_word_data(client, 0, ADM1276_PEAK_PIN, 0); + if (!ret && data->have_pin_min) + ret = pmbus_write_word_data(client, 0, + ADM1293_PIN_MIN, 0); + break; + case PMBUS_VIRT_RESET_TEMP_HISTORY: + ret = pmbus_write_word_data(client, 0, ADM1278_PEAK_TEMP, 0); + break; + case PMBUS_VIRT_POWER_SAMPLES: + if (!data->have_power_sampling) + return -ENXIO; + word = clamp_val(word, 1, ADM1275_SAMPLES_AVG_MAX); + ret = adm1275_write_samples(data, client, true, ilog2(word)); + break; + case PMBUS_VIRT_IN_SAMPLES: + case PMBUS_VIRT_CURR_SAMPLES: + word = clamp_val(word, 1, ADM1275_SAMPLES_AVG_MAX); + ret = adm1275_write_samples(data, client, false, ilog2(word)); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int adm1275_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct adm1275_data *data = to_adm1275_data(info); + int mfr_status, ret; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_STATUS_IOUT: + ret = pmbus_read_byte_data(client, page, PMBUS_STATUS_IOUT); + if (ret < 0) + break; + if (!data->have_oc_fault && !data->have_uc_fault) + break; + mfr_status = pmbus_read_byte_data(client, page, + PMBUS_STATUS_MFR_SPECIFIC); + if (mfr_status < 0) + return mfr_status; + if (mfr_status & ADM1275_MFR_STATUS_IOUT_WARN2) { + ret |= data->have_oc_fault ? + PB_IOUT_OC_FAULT : PB_IOUT_UC_FAULT; + } + break; + case PMBUS_STATUS_VOUT: + if (data->have_vout) + return -ENODATA; + ret = 0; + if (data->have_vaux_status) { + mfr_status = pmbus_read_byte_data(client, 0, + ADM1075_VAUX_STATUS); + if (mfr_status < 0) + return mfr_status; + if (mfr_status & ADM1075_VAUX_OV_WARN) + ret |= PB_VOLTAGE_OV_WARNING; + if (mfr_status & ADM1075_VAUX_UV_WARN) + ret |= PB_VOLTAGE_UV_WARNING; + } else if (data->have_mfr_vaux_status) { + mfr_status = pmbus_read_byte_data(client, page, + PMBUS_STATUS_MFR_SPECIFIC); + if (mfr_status < 0) + return mfr_status; + if (mfr_status & ADM1293_MFR_STATUS_VAUX_OV_WARN) + ret |= PB_VOLTAGE_OV_WARNING; + if (mfr_status & ADM1293_MFR_STATUS_VAUX_UV_WARN) + ret |= PB_VOLTAGE_UV_WARNING; + } + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static const struct i2c_device_id adm1275_id[] = { + { .name = "adm1075", .driver_data = adm1075 }, + { .name = "adm1272", .driver_data = adm1272 }, + { .name = "adm1273", .driver_data = adm1273 }, + { .name = "adm1275", .driver_data = adm1275 }, + { .name = "adm1276", .driver_data = adm1276 }, + { .name = "adm1278", .driver_data = adm1278 }, + { .name = "adm1281", .driver_data = adm1281 }, + { .name = "adm1293", .driver_data = adm1293 }, + { .name = "adm1294", .driver_data = adm1294 }, + { .name = "bd12780", .driver_data = bd12780 }, + { .name = "bd12790", .driver_data = bd12790 }, + { .name = "mc09c", .driver_data = sq24905c }, + { } +}; +MODULE_DEVICE_TABLE(i2c, adm1275_id); + +/* Enable VOUT & TEMP1 if not enabled (disabled by default) */ +static int adm1275_enable_vout_temp(struct adm1275_data *data, + struct i2c_client *client, int config, + u16 defconfig) +{ + int ret; + + if ((config & defconfig) != defconfig) { + config |= defconfig; + ret = adm1275_write_pmon_config(data, client, config); + if (ret < 0) { + dev_err(&client->dev, "Failed to enable VOUT/TEMP1 monitoring\n"); + return ret; + } + } + return 0; +} + +static int adm1275_probe(struct i2c_client *client) +{ + s32 (*config_read_fn)(const struct i2c_client *client, u8 reg); + u8 block_buffer[I2C_SMBUS_BLOCK_MAX + 1] = {0}; + int config, device_config; + int ret; + struct pmbus_driver_info *info; + struct adm1275_data *data; + const struct i2c_device_id *mid; + const struct coefficients *coefficients; + int vindex = -1, voindex = -1, cindex = -1, pindex = -1; + int tindex = -1; + u32 shunt; + u32 avg; + + ret = pmbus_read_smbus_i2c_block_data(client, PMBUS_MFR_ID, block_buffer); + if (ret < 0) { + dev_err(&client->dev, "Failed to read Manufacturer ID\n"); + return ret; + } + if ((ret != 3 || strncmp(block_buffer, "ADI", 3)) && + (ret != 2 || strncmp(block_buffer, "SY", 2)) && + (ret != 4 || strncmp(block_buffer, "ROHM", 4))) { + dev_err(&client->dev, "Unsupported Manufacturer ID\n"); + return -ENODEV; + } + + ret = pmbus_read_smbus_i2c_block_data(client, PMBUS_MFR_MODEL, block_buffer); + if (ret < 0) { + dev_err(&client->dev, "Failed to read Manufacturer Model\n"); + return ret; + } + for (mid = adm1275_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, block_buffer, strlen(mid->name))) + break; + } + if (!mid->name[0]) { + dev_err(&client->dev, "Unsupported device\n"); + return -ENODEV; + } + + if (strcmp(client->name, mid->name) != 0) + dev_notice(&client->dev, + "Device mismatch: Configured %s, detected %s\n", + client->name, mid->name); + + if (mid->driver_data == adm1272 || mid->driver_data == adm1273 || + mid->driver_data == adm1278 || mid->driver_data == adm1281 || + mid->driver_data == adm1293 || mid->driver_data == adm1294 || + mid->driver_data == bd12780 || mid->driver_data == bd12790 || + mid->driver_data == sq24905c) + config_read_fn = i2c_smbus_read_word_data; + else + config_read_fn = i2c_smbus_read_byte_data; + config = config_read_fn(client, ADM1275_PMON_CONFIG); + if (config < 0) + return config; + + device_config = config_read_fn(client, ADM1275_DEVICE_CONFIG); + if (device_config < 0) + return device_config; + + data = devm_kzalloc(&client->dev, sizeof(struct adm1275_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + if (of_property_read_u32(client->dev.of_node, + "shunt-resistor-micro-ohms", &shunt)) + shunt = 1000; /* 1 mOhm if not set via DT */ + + if (shunt == 0) + return -EINVAL; + + data->id = mid->driver_data; + + info = &data->info; + + info->pages = 1; + info->format[PSC_VOLTAGE_IN] = direct; + info->format[PSC_VOLTAGE_OUT] = direct; + info->format[PSC_CURRENT_OUT] = direct; + info->format[PSC_POWER] = direct; + info->format[PSC_TEMPERATURE] = direct; + info->func[0] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_SAMPLES; + + info->read_word_data = adm1275_read_word_data; + info->read_byte_data = adm1275_read_byte_data; + info->write_word_data = adm1275_write_word_data; + + switch (data->id) { + case adm1075: + if (device_config & ADM1275_IOUT_WARN2_SELECT) + data->have_oc_fault = true; + else + data->have_uc_fault = true; + data->have_pin_max = true; + data->have_vaux_status = true; + + coefficients = adm1075_coefficients; + vindex = 0; + switch (config & ADM1075_IRANGE_MASK) { + case ADM1075_IRANGE_25: + cindex = 1; + pindex = 3; + break; + case ADM1075_IRANGE_50: + cindex = 2; + pindex = 4; + break; + default: + dev_err(&client->dev, "Invalid input current range"); + break; + } + + info->func[0] |= PMBUS_HAVE_VIN | PMBUS_HAVE_PIN + | PMBUS_HAVE_STATUS_INPUT; + if (config & ADM1275_VIN_VOUT_SELECT) + info->func[0] |= + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT; + break; + case adm1272: + case adm1273: + data->have_vout = true; + data->have_pin_max = true; + data->have_temp_max = true; + data->have_power_sampling = true; + + coefficients = adm1272_coefficients; + + vindex = (config & ADM1275_VRANGE) ? 1 : 0; + cindex = (config & ADM1272_IRANGE) ? 3 : 2; + /* pindex depends on the combination of the above */ + switch (config & (ADM1275_VRANGE | ADM1272_IRANGE)) { + case 0: + default: + pindex = 4; + break; + case ADM1275_VRANGE: + pindex = 5; + break; + case ADM1272_IRANGE: + pindex = 6; + break; + case ADM1275_VRANGE | ADM1272_IRANGE: + pindex = 7; + break; + } + tindex = 8; + + info->func[0] |= PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + + ret = adm1275_enable_vout_temp(data, client, config, + ADM1278_PMON_DEFCONFIG); + if (ret) + return ret; + + if (config & ADM1278_VIN_EN) + info->func[0] |= PMBUS_HAVE_VIN; + break; + + /* + * The BD12790 is almost identical to the adm1272. Only the defconfig + * and coefficients have minor differences. + */ + case bd12790: + data->have_vout = true; + data->have_pin_max = true; + data->have_temp_max = true; + data->have_power_sampling = true; + + coefficients = bd12790_coefficients; + + vindex = (config & ADM1275_VRANGE) ? 1 : 0; + cindex = (config & ADM1272_IRANGE) ? 3 : 2; + /* pindex depends on the combination of the above */ + switch (config & (ADM1275_VRANGE | ADM1272_IRANGE)) { + case 0: + default: + pindex = 4; + break; + case ADM1275_VRANGE: + pindex = 5; + break; + case ADM1272_IRANGE: + pindex = 6; + break; + case ADM1275_VRANGE | ADM1272_IRANGE: + pindex = 7; + break; + } + tindex = 8; + + info->func[0] |= PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + + ret = adm1275_enable_vout_temp(data, client, config, + BD12780_PMON_DEFCONFIG); + if (ret) + return ret; + + if (config & ADM1278_VIN_EN) + info->func[0] |= PMBUS_HAVE_VIN; + break; + case adm1275: + if (device_config & ADM1275_IOUT_WARN2_SELECT) + data->have_oc_fault = true; + else + data->have_uc_fault = true; + data->have_vout = true; + + coefficients = adm1275_coefficients; + vindex = (config & ADM1275_VRANGE) ? 0 : 1; + cindex = 2; + + if (config & ADM1275_VIN_VOUT_SELECT) + info->func[0] |= + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT; + else + info->func[0] |= + PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT; + break; + case adm1276: + if (device_config & ADM1275_IOUT_WARN2_SELECT) + data->have_oc_fault = true; + else + data->have_uc_fault = true; + data->have_vout = true; + data->have_pin_max = true; + + coefficients = adm1276_coefficients; + vindex = (config & ADM1275_VRANGE) ? 0 : 1; + cindex = 2; + pindex = (config & ADM1275_VRANGE) ? 3 : 4; + + info->func[0] |= PMBUS_HAVE_VIN | PMBUS_HAVE_PIN + | PMBUS_HAVE_STATUS_INPUT; + if (config & ADM1275_VIN_VOUT_SELECT) + info->func[0] |= + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT; + break; + case adm1278: + case adm1281: + case sq24905c: + data->have_vout = true; + data->have_pin_max = true; + data->have_temp_max = true; + data->have_power_sampling = true; + + coefficients = adm1278_coefficients; + vindex = 0; + cindex = 1; + pindex = 2; + tindex = 3; + + info->func[0] |= PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + + ret = adm1275_enable_vout_temp(data, client, config, + ADM1278_PMON_DEFCONFIG); + if (ret) + return ret; + + if (config & ADM1278_VIN_EN) + info->func[0] |= PMBUS_HAVE_VIN; + break; + + /* + * The BD12780 is almost functionally identical with the adm1278 above. + * Only differences visible to the driver are lack of TSFILT bits and + * different identification register contents. + */ + case bd12780: + data->have_vout = true; + data->have_pin_max = true; + data->have_temp_max = true; + data->have_power_sampling = true; + + coefficients = adm1278_coefficients; + vindex = 0; + cindex = 1; + pindex = 2; + tindex = 3; + + info->func[0] |= PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + + ret = adm1275_enable_vout_temp(data, client, config, + BD12780_PMON_DEFCONFIG); + if (ret) + return ret; + + if (config & ADM1278_VIN_EN) + info->func[0] |= PMBUS_HAVE_VIN; + + break; + case adm1293: + case adm1294: + data->have_iout_min = true; + data->have_pin_min = true; + data->have_pin_max = true; + data->have_mfr_vaux_status = true; + data->have_power_sampling = true; + + coefficients = adm1293_coefficients; + + voindex = 0; + switch (config & ADM1293_VIN_SEL_MASK) { + case ADM1293_VIN_SEL_012: /* 1.2V */ + vindex = 0; + break; + case ADM1293_VIN_SEL_074: /* 7.4V */ + vindex = 1; + break; + case ADM1293_VIN_SEL_210: /* 21V */ + vindex = 2; + break; + default: /* disabled */ + break; + } + + switch (config & ADM1293_IRANGE_MASK) { + case ADM1293_IRANGE_25: + cindex = 3; + break; + case ADM1293_IRANGE_50: + cindex = 4; + break; + case ADM1293_IRANGE_100: + cindex = 5; + break; + case ADM1293_IRANGE_200: + cindex = 6; + break; + } + + if (vindex >= 0) + pindex = 7 + vindex * 4 + (cindex - 3); + + if (config & ADM1293_VAUX_EN) + info->func[0] |= + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT; + + info->func[0] |= PMBUS_HAVE_PIN | + PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT; + + break; + default: + dev_err(&client->dev, "Unsupported device\n"); + return -ENODEV; + } + + if (data->have_power_sampling && + of_property_read_u32(client->dev.of_node, + "adi,power-sample-average", &avg) == 0) { + if (!avg || avg > ADM1275_SAMPLES_AVG_MAX || + BIT(__fls(avg)) != avg) { + dev_err(&client->dev, + "Invalid number of power samples"); + return -EINVAL; + } + ret = adm1275_write_samples(data, client, true, ilog2(avg)); + if (ret < 0) { + dev_err(&client->dev, + "Setting power sample averaging failed with error %d", + ret); + return ret; + } + } + + if (of_property_read_u32(client->dev.of_node, + "adi,volt-curr-sample-average", &avg) == 0) { + if (!avg || avg > ADM1275_SAMPLES_AVG_MAX || + BIT(__fls(avg)) != avg) { + dev_err(&client->dev, + "Invalid number of voltage/current samples"); + return -EINVAL; + } + ret = adm1275_write_samples(data, client, false, ilog2(avg)); + if (ret < 0) { + dev_err(&client->dev, + "Setting voltage and current sample averaging failed with error %d", + ret); + return ret; + } + } + + if (voindex < 0) + voindex = vindex; + if (vindex >= 0) { + info->m[PSC_VOLTAGE_IN] = coefficients[vindex].m; + info->b[PSC_VOLTAGE_IN] = coefficients[vindex].b; + info->R[PSC_VOLTAGE_IN] = coefficients[vindex].R; + } + if (voindex >= 0) { + info->m[PSC_VOLTAGE_OUT] = coefficients[voindex].m; + info->b[PSC_VOLTAGE_OUT] = coefficients[voindex].b; + info->R[PSC_VOLTAGE_OUT] = coefficients[voindex].R; + } + if (cindex >= 0) { + u32 m; + + /* Scale current with sense resistor value */ + if (unlikely(check_mul_overflow(coefficients[cindex].m, shunt, &m))) { + dev_err(&client->dev, "Current coefficient overflow\n"); + return -EOVERFLOW; + } + info->m[PSC_CURRENT_OUT] = m / 1000; + info->b[PSC_CURRENT_OUT] = coefficients[cindex].b; + info->R[PSC_CURRENT_OUT] = coefficients[cindex].R; + } + if (pindex >= 0) { + u32 m; + + if (unlikely(check_mul_overflow(coefficients[pindex].m, shunt, &m))) { + dev_err(&client->dev, "Power coefficient overflow\n"); + return -EOVERFLOW; + } + info->m[PSC_POWER] = m / 1000; + info->b[PSC_POWER] = coefficients[pindex].b; + info->R[PSC_POWER] = coefficients[pindex].R; + } + if (tindex >= 0) { + info->m[PSC_TEMPERATURE] = coefficients[tindex].m; + info->b[PSC_TEMPERATURE] = coefficients[tindex].b; + info->R[PSC_TEMPERATURE] = coefficients[tindex].R; + } + + return pmbus_do_probe(client, info); +} + +static const struct of_device_id adm1275_of_match[] = { + { .compatible = "adi,adm1075", }, + { .compatible = "adi,adm1272", }, + { .compatible = "adi,adm1273", }, + { .compatible = "adi,adm1275", }, + { .compatible = "adi,adm1276", }, + { .compatible = "adi,adm1278", }, + { .compatible = "adi,adm1281", }, + { .compatible = "adi,adm1293", }, + { .compatible = "adi,adm1294", }, + { .compatible = "rohm,bd12780", }, + { .compatible = "rohm,bd12790", }, + { .compatible = "silergy,mc09c", }, + { } +}; +MODULE_DEVICE_TABLE(of, adm1275_of_match); + +static struct i2c_driver adm1275_driver = { + .driver = { + .name = "adm1275", + .of_match_table = adm1275_of_match, + }, + .probe = adm1275_probe, + .id_table = adm1275_id, +}; + +module_i2c_driver(adm1275_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for Analog Devices ADM1275 and compatibles"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/adp1050.c b/drivers/hwmon/pmbus/adp1050.c new file mode 100644 index 000000000..c3ad33024 --- /dev/null +++ b/drivers/hwmon/pmbus/adp1050.c @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Hardware monitoring driver for Analog Devices ADP1050 + * + * Copyright (C) 2024 Analog Devices, Inc. + */ +#include <linux/bits.h> +#include <linux/i2c.h> +#include <linux/module.h> + +#include "pmbus.h" + +#if IS_ENABLED(CONFIG_SENSORS_ADP1050_REGULATOR) +static const struct regulator_desc adp1050_reg_desc[] = { + PMBUS_REGULATOR_ONE("vout"), +}; +#endif /* CONFIG_SENSORS_ADP1050_REGULATOR */ + +static struct pmbus_driver_info adp1050_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_IIN | PMBUS_HAVE_TEMP + | PMBUS_HAVE_STATUS_TEMP, +}; + +static struct pmbus_driver_info adp1051_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT + | PMBUS_HAVE_TEMP + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_STATUS_TEMP, +}; + +static struct pmbus_driver_info adp1055_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT + | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_TEMP3 + | PMBUS_HAVE_POUT + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_STATUS_TEMP, +}; + +static struct pmbus_driver_info ltp8800_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT + | PMBUS_HAVE_TEMP + | PMBUS_HAVE_POUT + | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_STATUS_TEMP, +#if IS_ENABLED(CONFIG_SENSORS_ADP1050_REGULATOR) + .num_regulators = 1, + .reg_desc = adp1050_reg_desc, +#endif +}; + +static int adp1050_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + + info = (struct pmbus_driver_info *)i2c_get_match_data(client); + if (!info) + return -ENODEV; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id adp1050_id[] = { + { .name = "adp1050", .driver_data = (kernel_ulong_t)&adp1050_info }, + { .name = "adp1051", .driver_data = (kernel_ulong_t)&adp1051_info }, + { .name = "adp1055", .driver_data = (kernel_ulong_t)&adp1055_info }, + { .name = "ltp8800", .driver_data = (kernel_ulong_t)<p8800_info }, + {} +}; +MODULE_DEVICE_TABLE(i2c, adp1050_id); + +static const struct of_device_id adp1050_of_match[] = { + { .compatible = "adi,adp1050", .data = &adp1050_info }, + { .compatible = "adi,adp1051", .data = &adp1051_info }, + { .compatible = "adi,adp1055", .data = &adp1055_info }, + { .compatible = "adi,ltp8800", .data = <p8800_info }, + {} +}; +MODULE_DEVICE_TABLE(of, adp1050_of_match); + +static struct i2c_driver adp1050_driver = { + .driver = { + .name = "adp1050", + .of_match_table = adp1050_of_match, + }, + .probe = adp1050_probe, + .id_table = adp1050_id, +}; +module_i2c_driver(adp1050_driver); + +MODULE_AUTHOR("Radu Sabau <radu.sabau@analog.com>"); +MODULE_DESCRIPTION("Analog Devices ADP1050 HWMON PMBus Driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/aps-379.c b/drivers/hwmon/pmbus/aps-379.c new file mode 100644 index 000000000..3ec0940ae --- /dev/null +++ b/drivers/hwmon/pmbus/aps-379.c @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Sony APS-379 Power Supplies + * + * Copyright 2026 Allied Telesis Labs + */ + +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +/* + * The VOUT format used by the chip is linear11, not linear16. Provide a hard + * coded VOUT_MODE that says VOUT is in linear mode with a fixed exponent of + * 2^-4. + */ +#define APS_379_VOUT_MODE ((u8)(-4 & 0x1f)) + +static int aps_379_read_byte_data(struct i2c_client *client, int page, int reg) +{ + switch (reg) { + case PMBUS_VOUT_MODE: + return APS_379_VOUT_MODE; + default: + return -ENODATA; + } +} + +/* + * The APS-379 uses linear11 format instead of linear16. We've reported the exponent + * via the PMBUS_VOUT_MODE so we just return the mantissa here. + */ +static int aps_379_read_vout(struct i2c_client *client) +{ + int ret; + + ret = pmbus_read_word_data(client, 0, 0xff, PMBUS_READ_VOUT); + if (ret < 0) + return ret; + + return clamp_val(sign_extend32(ret & 0x7ff, 10), 0, 0x3ff); +} + +static int aps_379_read_word_data(struct i2c_client *client, int page, int phase, int reg) +{ + switch (reg) { + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_VOUT_OV_WARN_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_IOUT_UC_FAULT_LIMIT: + case PMBUS_UT_WARN_LIMIT: + case PMBUS_UT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + case PMBUS_POUT_OP_WARN_LIMIT: + case PMBUS_MFR_IIN_MAX: + case PMBUS_MFR_PIN_MAX: + case PMBUS_MFR_VOUT_MIN: + case PMBUS_MFR_VOUT_MAX: + case PMBUS_MFR_IOUT_MAX: + case PMBUS_MFR_POUT_MAX: + case PMBUS_MFR_MAX_TEMP_1: + /* These commands return data but it is invalid/un-documented */ + return -ENXIO; + case PMBUS_IOUT_OC_FAULT_LIMIT: + /* + * The standard requires this to be a value in Amps but it's + * actually a percentage of the rated output (123A for + * 110-240Vac, 110A for 90-100Vac) which we don't know. Ignore + * it rather than guessing. + */ + return -ENXIO; + case PMBUS_READ_VOUT: + return aps_379_read_vout(client); + default: + return -ENODATA; + } +} + +static struct pmbus_driver_info aps_379_info = { + .pages = 1, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_FAN] = linear, + .func[0] = PMBUS_HAVE_VOUT | + PMBUS_HAVE_IOUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP | + PMBUS_HAVE_FAN12, + .read_byte_data = aps_379_read_byte_data, + .read_word_data = aps_379_read_word_data, +}; + +static const struct i2c_device_id aps_379_id[] = { + { .name = "aps-379" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, aps_379_id); + +static int aps_379_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + u8 buf[I2C_SMBUS_BLOCK_MAX + 1] = { 0 }; + int ret; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA + | I2C_FUNC_SMBUS_READ_WORD_DATA + | I2C_FUNC_SMBUS_READ_BLOCK_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) { + dev_err(dev, "Failed to read Manufacturer Model\n"); + return ret; + } + + if (strncasecmp(buf, aps_379_id[0].name, strlen(aps_379_id[0].name)) != 0) { + buf[ret] = '\0'; + dev_err(dev, "Unsupported Manufacturer Model '%s'\n", buf); + return -ENODEV; + } + + return pmbus_do_probe(client, &aps_379_info); +} + +static const struct of_device_id __maybe_unused aps_379_of_match[] = { + { .compatible = "sony,aps-379" }, + {}, +}; +MODULE_DEVICE_TABLE(of, aps_379_of_match); + +static struct i2c_driver aps_379_driver = { + .driver = { + .name = "aps-379", + .of_match_table = of_match_ptr(aps_379_of_match), + }, + .probe = aps_379_probe, + .id_table = aps_379_id, +}; + +module_i2c_driver(aps_379_driver); + +MODULE_AUTHOR("Chris Packham"); +MODULE_DESCRIPTION("PMBus driver for Sony APS-379"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/bel-pfe.c b/drivers/hwmon/pmbus/bel-pfe.c new file mode 100644 index 000000000..9e3dc9d29 --- /dev/null +++ b/drivers/hwmon/pmbus/bel-pfe.c @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for BEL PFE family power supplies. + * + * Copyright (c) 2019 Facebook Inc. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> + +#include "pmbus.h" + +enum chips {pfe1100, pfe3000}; + +/* + * Disable status check because some devices report communication error + * (invalid command) for VOUT_MODE command (0x20) although the correct + * VOUT_MODE (0x16) is returned: it leads to incorrect exponent in linear + * mode. + * This affects both pfe3000 and pfe1100. + */ +static struct pmbus_platform_data pfe_plat_data = { + .flags = PMBUS_SKIP_STATUS_CHECK, +}; + +static struct pmbus_driver_info pfe_driver_info[] = { + [pfe1100] = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_FAN] = linear, + + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_POUT | + PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | + PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | + PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_FAN12, + }, + + [pfe3000] = { + .pages = 7, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_FAN] = linear, + + /* Page 0: V1. */ + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_POUT | PMBUS_HAVE_FAN12 | + PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | + PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | + PMBUS_HAVE_TEMP3 | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_VCAP, + + /* Page 1: Vsb. */ + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_POUT, + + /* + * Page 2: V1 Ishare. + * Page 3: Reserved. + * Page 4: V1 Cathode. + * Page 5: Vsb Cathode. + * Page 6: V1 Sense. + */ + .func[2] = PMBUS_HAVE_VOUT, + .func[4] = PMBUS_HAVE_VOUT, + .func[5] = PMBUS_HAVE_VOUT, + .func[6] = PMBUS_HAVE_VOUT, + }, +}; + +static int pfe_pmbus_probe(struct i2c_client *client) +{ + int model = (uintptr_t)i2c_get_match_data(client); + + client->dev.platform_data = &pfe_plat_data; + + /* + * PFE3000-12-069RA devices may not stay in page 0 during device + * probe which leads to probe failure (read status word failed). + * So let's set the device to page 0 at the beginning. + */ + if (model == pfe3000) + i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + + return pmbus_do_probe(client, &pfe_driver_info[model]); +} + +static const struct i2c_device_id pfe_device_id[] = { + { .name = "pfe1100", .driver_data = pfe1100 }, + { .name = "pfe3000", .driver_data = pfe3000 }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, pfe_device_id); + +static struct i2c_driver pfe_pmbus_driver = { + .driver = { + .name = "bel-pfe", + }, + .probe = pfe_pmbus_probe, + .id_table = pfe_device_id, +}; + +module_i2c_driver(pfe_pmbus_driver); + +MODULE_AUTHOR("Tao Ren <rentao.bupt@gmail.com>"); +MODULE_DESCRIPTION("PMBus driver for BEL PFE Family Power Supplies"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/bpa-rs600.c b/drivers/hwmon/pmbus/bpa-rs600.c new file mode 100644 index 000000000..e364dcb59 --- /dev/null +++ b/drivers/hwmon/pmbus/bpa-rs600.c @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for BluTek BPA-RS600 Power Supplies + * + * Copyright 2021 Allied Telesis Labs + */ + +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +enum chips { bpa_rs600, bpd_rs600 }; + +static int bpa_rs600_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_FAN_CONFIG_12: + /* + * Two fans are reported in PMBUS_FAN_CONFIG_12 but there is + * only one fan in the module. Mask out the FAN2 bits. + */ + ret = pmbus_read_byte_data(client, 0, PMBUS_FAN_CONFIG_12); + if (ret >= 0) + ret &= ~(PB_FAN_2_INSTALLED | PB_FAN_2_PULSE_MASK); + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +/* + * The BPA-RS600 violates the PMBus spec. Specifically it treats the + * mantissa as unsigned. Deal with this here to allow the PMBus core + * to work with correctly encoded data. + */ +static int bpa_rs600_read_vin(struct i2c_client *client) +{ + int ret, exponent, mantissa; + + ret = pmbus_read_word_data(client, 0, 0xff, PMBUS_READ_VIN); + if (ret < 0) + return ret; + + if (ret & BIT(10)) { + exponent = ret >> 11; + mantissa = ret & 0x7ff; + + exponent++; + mantissa >>= 1; + + ret = (exponent << 11) | mantissa; + } + + return ret; +} + +/* + * Firmware V5.70 incorrectly reports 1640W for MFR_PIN_MAX. + * Deal with this by returning a sensible value. + */ +static int bpa_rs600_read_pin_max(struct i2c_client *client) +{ + int ret; + + ret = pmbus_read_word_data(client, 0, 0xff, PMBUS_MFR_PIN_MAX); + if (ret < 0) + return ret; + + /* Detect invalid 1640W (linear encoding) */ + if (ret == 0x0b34) + /* Report 700W (linear encoding) */ + return 0x095e; + + return ret; +} + +static int bpa_rs600_read_word_data(struct i2c_client *client, int page, int phase, int reg) +{ + int ret; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_VOUT_OV_WARN_LIMIT: + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + case PMBUS_POUT_OP_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + case PMBUS_VOUT_OV_FAULT_LIMIT: + /* These commands return data but it is invalid/un-documented */ + ret = -ENXIO; + break; + case PMBUS_READ_VIN: + ret = bpa_rs600_read_vin(client); + break; + case PMBUS_MFR_PIN_MAX: + ret = bpa_rs600_read_pin_max(client); + break; + default: + if (reg >= PMBUS_VIRT_BASE) + ret = -ENXIO; + else + ret = -ENODATA; + break; + } + + return ret; +} + +static struct pmbus_driver_info bpa_rs600_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_FAN] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | + PMBUS_HAVE_FAN12 | + PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_FAN12, + .read_byte_data = bpa_rs600_read_byte_data, + .read_word_data = bpa_rs600_read_word_data, +}; + +static const struct i2c_device_id bpa_rs600_id[] = { + { .name = "bpa-rs600", .driver_data = bpa_rs600 }, + { .name = "bpd-rs600", .driver_data = bpd_rs600 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, bpa_rs600_id); + +static int bpa_rs600_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + int ret; + const struct i2c_device_id *mid; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA + | I2C_FUNC_SMBUS_READ_WORD_DATA + | I2C_FUNC_SMBUS_READ_BLOCK_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) { + dev_err(dev, "Failed to read Manufacturer Model\n"); + return ret; + } + + for (mid = bpa_rs600_id; mid->name[0]; mid++) { + if (!strncasecmp(buf, mid->name, strlen(mid->name))) + break; + } + if (!mid->name[0]) { + buf[ret] = '\0'; + dev_err(dev, "Unsupported Manufacturer Model '%s'\n", buf); + return -ENODEV; + } + + return pmbus_do_probe(client, &bpa_rs600_info); +} + +static const struct of_device_id __maybe_unused bpa_rs600_of_match[] = { + { .compatible = "blutek,bpa-rs600" }, + {}, +}; +MODULE_DEVICE_TABLE(of, bpa_rs600_of_match); + +static struct i2c_driver bpa_rs600_driver = { + .driver = { + .name = "bpa-rs600", + .of_match_table = of_match_ptr(bpa_rs600_of_match), + }, + .probe = bpa_rs600_probe, + .id_table = bpa_rs600_id, +}; + +module_i2c_driver(bpa_rs600_driver); + +MODULE_AUTHOR("Chris Packham"); +MODULE_DESCRIPTION("PMBus driver for BluTek BPA-RS600"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/crps.c b/drivers/hwmon/pmbus/crps.c new file mode 100644 index 000000000..266ec8947 --- /dev/null +++ b/drivers/hwmon/pmbus/crps.c @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright 2024 IBM Corp. + */ + +#include <linux/i2c.h> +#include <linux/of.h> +#include <linux/pmbus.h> + +#include "pmbus.h" + +static const struct i2c_device_id crps_id[] = { + { .name = "intel_crps185" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, crps_id); + +static struct pmbus_driver_info crps_info = { + .pages = 1, + /* PSU uses default linear data format. */ + .func[0] = PMBUS_HAVE_PIN | PMBUS_HAVE_IOUT | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_IIN | + PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | + PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_FAN12 | PMBUS_HAVE_STATUS_FAN12, +}; + +static int crps_probe(struct i2c_client *client) +{ + int rc; + struct device *dev = &client->dev; + char buf[I2C_SMBUS_BLOCK_MAX + 2] = { 0 }; + + rc = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (rc < 0) + return dev_err_probe(dev, rc, "Failed to read PMBUS_MFR_MODEL\n"); + + if (rc != 7 || strncmp(buf, "03NK260", 7)) { + buf[rc] = '\0'; + return dev_err_probe(dev, -ENODEV, "Model '%s' not supported\n", buf); + } + + rc = pmbus_do_probe(client, &crps_info); + if (rc) + return dev_err_probe(dev, rc, "Failed to probe\n"); + + return 0; +} + +static const struct of_device_id crps_of_match[] = { + { + .compatible = "intel,crps185", + }, + {} +}; +MODULE_DEVICE_TABLE(of, crps_of_match); + +static struct i2c_driver crps_driver = { + .driver = { + .name = "crps", + .of_match_table = crps_of_match, + }, + .probe = crps_probe, + .id_table = crps_id, +}; + +module_i2c_driver(crps_driver); + +MODULE_AUTHOR("Ninad Palsule"); +MODULE_DESCRIPTION("PMBus driver for Intel Common Redundant power supplies"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/d1u74t.c b/drivers/hwmon/pmbus/d1u74t.c new file mode 100644 index 000000000..752df3af9 --- /dev/null +++ b/drivers/hwmon/pmbus/d1u74t.c @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright 2026 Nexthop Systems. + */ + +#include <linux/i2c.h> +#include <linux/of.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include <linux/string.h> + +#include "pmbus.h" + +static const struct i2c_device_id d1u74t_id[] = { + { "d1u74t" }, + {}, +}; +MODULE_DEVICE_TABLE(i2c, d1u74t_id); + +static struct pmbus_driver_info d1u74t_info = { + .pages = 1, + /* PSU uses default linear data format. */ + .func[0] = PMBUS_HAVE_PIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_TEMP | + PMBUS_HAVE_TEMP2 | PMBUS_HAVE_TEMP3 | + PMBUS_HAVE_STATUS_TEMP | PMBUS_HAVE_FAN12 | + PMBUS_HAVE_STATUS_FAN12, +}; + +static int d1u74t_probe(struct i2c_client *client) +{ + char buf[I2C_SMBUS_BLOCK_MAX + 2] = { 0 }; + struct device *dev = &client->dev; + int rc; + + rc = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, buf); + if (rc < 0) + return dev_err_probe(dev, rc, "Failed to read PMBUS_MFR_ID\n"); + + if (rc != 9 || strncmp(buf, "Murata-PS", 9)) { + buf[rc] = '\0'; + return dev_err_probe(dev, -ENODEV, + "Unsupported Manufacturer ID '%s'\n", + buf); + } + + rc = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (rc < 0) + return dev_err_probe(dev, rc, + "Failed to read PMBUS_MFR_MODEL\n"); + + if (rc < 8 || strncmp(buf, "D1U74T-W", 8)) { + buf[rc] = '\0'; + return dev_err_probe(dev, -ENODEV, "Model '%s' not supported\n", + buf); + } + + rc = pmbus_do_probe(client, &d1u74t_info); + if (rc) + return dev_err_probe(dev, rc, "Failed to probe\n"); + + return 0; +} + +static const struct of_device_id d1u74t_of_match[] = { + { + .compatible = "murata,d1u74t", + }, + {}, +}; +MODULE_DEVICE_TABLE(of, d1u74t_of_match); + +static struct i2c_driver d1u74t_driver = { + .driver = { + .name = "d1u74t", + .of_match_table = d1u74t_of_match, + }, + .probe = d1u74t_probe, + .id_table = d1u74t_id, +}; + +module_i2c_driver(d1u74t_driver); + +MODULE_AUTHOR("Abdurrahman Hussain"); +MODULE_DESCRIPTION("PMBus driver for Murata D1U74T-W power supplies"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/delta-ahe50dc-fan.c b/drivers/hwmon/pmbus/delta-ahe50dc-fan.c new file mode 100644 index 000000000..2df655b20 --- /dev/null +++ b/drivers/hwmon/pmbus/delta-ahe50dc-fan.c @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Delta AHE-50DC power shelf fan control module driver + * + * Copyright 2021 Zev Weiss <zev@bewilderbeest.net> + */ + +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> + +#include "pmbus.h" + +#define AHE50DC_PMBUS_READ_TEMP4 0xd0 + +static int ahe50dc_fan_write_byte(struct i2c_client *client, int page, u8 value) +{ + /* + * The CLEAR_FAULTS operation seems to sometimes (unpredictably, perhaps + * 5% of the time or so) trigger a problematic phenomenon in which the + * fan speeds surge momentarily and at least some (perhaps all?) of the + * system's power outputs experience a glitch. + * + * However, according to Delta it should be OK to simply not send any + * CLEAR_FAULTS commands (the device doesn't seem to be capable of + * reporting any faults anyway), so just blackhole them unconditionally. + */ + return value == PMBUS_CLEAR_FAULTS ? -EOPNOTSUPP : -ENODATA; +} + +static int ahe50dc_fan_read_word_data(struct i2c_client *client, int page, int phase, int reg) +{ + /* temp1 in (virtual) page 1 is remapped to mfr-specific temp4 */ + if (page == 1) { + if (reg == PMBUS_READ_TEMPERATURE_1) + return i2c_smbus_read_word_data(client, AHE50DC_PMBUS_READ_TEMP4); + return -EOPNOTSUPP; + } + + /* + * There's a fairly limited set of commands this device actually + * supports, so here we block attempts to read anything else (which + * return 0xffff and would cause confusion elsewhere). + */ + switch (reg) { + case PMBUS_STATUS_WORD: + case PMBUS_FAN_COMMAND_1: + case PMBUS_FAN_COMMAND_2: + case PMBUS_FAN_COMMAND_3: + case PMBUS_FAN_COMMAND_4: + case PMBUS_STATUS_FAN_12: + case PMBUS_STATUS_FAN_34: + case PMBUS_READ_VIN: + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_READ_TEMPERATURE_2: + case PMBUS_READ_TEMPERATURE_3: + case PMBUS_READ_FAN_SPEED_1: + case PMBUS_READ_FAN_SPEED_2: + case PMBUS_READ_FAN_SPEED_3: + case PMBUS_READ_FAN_SPEED_4: + return -ENODATA; + default: + return -EOPNOTSUPP; + } +} + +static struct pmbus_driver_info ahe50dc_fan_info = { + .pages = 2, + .format[PSC_FAN] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_FAN] = 1, + .b[PSC_FAN] = 0, + .R[PSC_FAN] = 0, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 1, + .m[PSC_VOLTAGE_IN] = 1, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 3, + .func[0] = PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_TEMP3 | + PMBUS_HAVE_VIN | PMBUS_HAVE_FAN12 | PMBUS_HAVE_FAN34 | + PMBUS_HAVE_STATUS_FAN12 | PMBUS_HAVE_STATUS_FAN34 | PMBUS_PAGE_VIRTUAL, + .func[1] = PMBUS_HAVE_TEMP | PMBUS_PAGE_VIRTUAL, + .write_byte = ahe50dc_fan_write_byte, + .read_word_data = ahe50dc_fan_read_word_data, +}; + +/* + * CAPABILITY returns 0xff, which appears to be this device's way indicating + * it doesn't support something (and if we enable I2C_CLIENT_PEC on seeing bit + * 7 being set it generates bad PECs, so let's not go there). + */ +static struct pmbus_platform_data ahe50dc_fan_data = { + .flags = PMBUS_NO_CAPABILITY, +}; + +static int ahe50dc_fan_probe(struct i2c_client *client) +{ + client->dev.platform_data = &ahe50dc_fan_data; + return pmbus_do_probe(client, &ahe50dc_fan_info); +} + +static const struct i2c_device_id ahe50dc_fan_id[] = { + { .name = "ahe50dc_fan" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ahe50dc_fan_id); + +static const struct of_device_id __maybe_unused ahe50dc_fan_of_match[] = { + { .compatible = "delta,ahe50dc-fan" }, + { } +}; +MODULE_DEVICE_TABLE(of, ahe50dc_fan_of_match); + +static struct i2c_driver ahe50dc_fan_driver = { + .driver = { + .name = "ahe50dc_fan", + .of_match_table = of_match_ptr(ahe50dc_fan_of_match), + }, + .probe = ahe50dc_fan_probe, + .id_table = ahe50dc_fan_id, +}; +module_i2c_driver(ahe50dc_fan_driver); + +MODULE_AUTHOR("Zev Weiss <zev@bewilderbeest.net>"); +MODULE_DESCRIPTION("Driver for Delta AHE-50DC power shelf fan control module"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/dps920ab.c b/drivers/hwmon/pmbus/dps920ab.c new file mode 100644 index 000000000..0d2901c31 --- /dev/null +++ b/drivers/hwmon/pmbus/dps920ab.c @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for Delta DPS920AB PSU + * + * Copyright (C) 2021 Delta Networks, Inc. + * Copyright (C) 2021 Sartura Ltd. + */ + +#include <linux/debugfs.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of.h> +#include "pmbus.h" + +struct dps920ab_data { + char *mfr_model; + char *mfr_id; +}; + +static int dps920ab_read_word_data(struct i2c_client *client, int page, int phase, int reg) +{ + /* + * This masks commands which are not supported. + * PSU advertises that all features are supported, + * in reality that unfortunately is not true. + * So enable only those that the datasheet confirms. + */ + switch (reg) { + case PMBUS_FAN_COMMAND_1: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_STATUS_WORD: + case PMBUS_READ_VIN: + case PMBUS_READ_IIN: + case PMBUS_READ_VOUT: + case PMBUS_READ_IOUT: + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_READ_TEMPERATURE_2: + case PMBUS_READ_TEMPERATURE_3: + case PMBUS_READ_FAN_SPEED_1: + case PMBUS_READ_POUT: + case PMBUS_READ_PIN: + case PMBUS_MFR_VOUT_MIN: + case PMBUS_MFR_VOUT_MAX: + case PMBUS_MFR_IOUT_MAX: + case PMBUS_MFR_POUT_MAX: + return pmbus_read_word_data(client, page, phase, reg); + default: + return -ENXIO; + } +} + +static int dps920ab_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + /* + * This masks commands which are not supported. + * PSU only has one R/W register and that is + * for the fan. + */ + switch (reg) { + case PMBUS_FAN_COMMAND_1: + return pmbus_write_word_data(client, page, reg, word); + default: + return -EACCES; + } +} + +static struct pmbus_driver_info dps920ab_info = { + .pages = 1, + + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_FAN] = linear, + .format[PSC_TEMPERATURE] = linear, + + .func[0] = + PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | + PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_TEMP3 | + PMBUS_HAVE_FAN12 | PMBUS_HAVE_STATUS_FAN12 | + PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, + .read_word_data = dps920ab_read_word_data, + .write_word_data = dps920ab_write_word_data, +}; + +static int dps920ab_mfr_id_show(struct seq_file *s, void *data) +{ + struct dps920ab_data *priv = s->private; + + seq_printf(s, "%s\n", priv->mfr_id); + + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(dps920ab_mfr_id); + +static int dps920ab_mfr_model_show(struct seq_file *s, void *data) +{ + struct dps920ab_data *priv = s->private; + + seq_printf(s, "%s\n", priv->mfr_model); + + return 0; +} + +DEFINE_SHOW_ATTRIBUTE(dps920ab_mfr_model); + +static void dps920ab_init_debugfs(struct dps920ab_data *data, struct i2c_client *client) +{ + struct dentry *debugfs_dir; + struct dentry *root; + + root = pmbus_get_debugfs_dir(client); + if (!root) + return; + + debugfs_dir = debugfs_create_dir(client->name, root); + + debugfs_create_file("mfr_id", + 0400, + debugfs_dir, + data, + &dps920ab_mfr_id_fops); + + debugfs_create_file("mfr_model", + 0400, + debugfs_dir, + data, + &dps920ab_mfr_model_fops); +} + +static int dps920ab_probe(struct i2c_client *client) +{ + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + struct dps920ab_data *data; + int ret; + + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, buf); + if (ret < 0) { + dev_err(&client->dev, "Failed to read Manufacturer ID\n"); + return ret; + } + + buf[ret] = '\0'; + if (ret != 5 || strncmp(buf, "DELTA", 5)) { + buf[ret] = '\0'; + dev_err(&client->dev, "Unsupported Manufacturer ID '%s'\n", buf); + return -ENODEV; + } + data->mfr_id = devm_kstrdup(&client->dev, buf, GFP_KERNEL); + if (!data->mfr_id) + return -ENOMEM; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) { + dev_err(&client->dev, "Failed to read Manufacturer Model\n"); + return ret; + } + + buf[ret] = '\0'; + if (ret != 11 || strncmp(buf, "DPS-920AB", 9)) { + dev_err(&client->dev, "Unsupported Manufacturer Model '%s'\n", buf); + return -ENODEV; + } + data->mfr_model = devm_kstrdup(&client->dev, buf, GFP_KERNEL); + if (!data->mfr_model) + return -ENOMEM; + + ret = pmbus_do_probe(client, &dps920ab_info); + if (ret) + return ret; + + dps920ab_init_debugfs(data, client); + + return 0; +} + +static const struct of_device_id __maybe_unused dps920ab_of_match[] = { + { .compatible = "delta,dps920ab", }, + {} +}; + +MODULE_DEVICE_TABLE(of, dps920ab_of_match); + +static const struct i2c_device_id dps920ab_device_id[] = { + { .name = "dps920ab" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, dps920ab_device_id); + +static struct i2c_driver dps920ab_driver = { + .driver = { + .name = "dps920ab", + .of_match_table = of_match_ptr(dps920ab_of_match), + }, + .probe = dps920ab_probe, + .id_table = dps920ab_device_id, +}; + +module_i2c_driver(dps920ab_driver); + +MODULE_AUTHOR("Robert Marko <robert.marko@sartura.hr>"); +MODULE_DESCRIPTION("PMBus driver for Delta DPS920AB PSU"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/e50sn12051.c b/drivers/hwmon/pmbus/e50sn12051.c new file mode 100644 index 000000000..abad39bdb --- /dev/null +++ b/drivers/hwmon/pmbus/e50sn12051.c @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for E50SN12051 + */ + +#include <linux/i2c.h> +#include <linux/module.h> +#include "pmbus.h" + +static struct pmbus_driver_info e50sn12051_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, +}; + +static const struct i2c_device_id e50sn12051_id[] = { { "e50sn12051", 0 }, {} }; +MODULE_DEVICE_TABLE(i2c, e50sn12051_id); + +static const struct of_device_id e50sn12051_of_match[] = { + { .compatible = "delta,e50sn12051" }, + {}, +}; +MODULE_DEVICE_TABLE(of, e50sn12051_of_match); + +static int e50sn12051_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &e50sn12051_info); +} + +static struct i2c_driver e50sn12051_driver = { + .driver = { + .name = "e50sn12051", + .of_match_table = e50sn12051_of_match, + }, + .probe = e50sn12051_probe, + + .id_table = e50sn12051_id, +}; + +module_i2c_driver(e50sn12051_driver); + +MODULE_AUTHOR("Kevin Chang <kevin.chang2@amd.com>"); +MODULE_DESCRIPTION("PMBus driver for E50SN12051"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/fsp-3y.c b/drivers/hwmon/pmbus/fsp-3y.c new file mode 100644 index 000000000..44cf2db93 --- /dev/null +++ b/drivers/hwmon/pmbus/fsp-3y.c @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for FSP 3Y-Power PSUs + * + * Copyright (c) 2021 Václav Kubernát, CESNET + * + * This driver is mostly reverse engineered with the help of a tool called pmbus_peek written by + * David Brownell (and later adopted by Jan Kundrát). The device has some sort of a timing issue + * when switching pages, details are explained in the code. The driver support is limited. It + * exposes only the values, that have been tested to work correctly. Unsupported values either + * aren't supported by the devices or their encondings are unknown. + */ + +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define YM2151_PAGE_12V_LOG 0x00 +#define YM2151_PAGE_12V_REAL 0x00 +#define YM2151_PAGE_5VSB_LOG 0x01 +#define YM2151_PAGE_5VSB_REAL 0x20 +#define YH5151E_PAGE_12V_LOG 0x00 +#define YH5151E_PAGE_12V_REAL 0x00 +#define YH5151E_PAGE_5V_LOG 0x01 +#define YH5151E_PAGE_5V_REAL 0x10 +#define YH5151E_PAGE_3V3_LOG 0x02 +#define YH5151E_PAGE_3V3_REAL 0x11 + +enum chips { + ym2151e, + yh5151e +}; + +struct fsp3y_data { + struct pmbus_driver_info info; + int chip; + int page; + + bool vout_linear_11; +}; + +#define to_fsp3y_data(x) container_of(x, struct fsp3y_data, info) + +static int page_log_to_page_real(int page_log, enum chips chip) +{ + switch (chip) { + case ym2151e: + switch (page_log) { + case YM2151_PAGE_12V_LOG: + return YM2151_PAGE_12V_REAL; + case YM2151_PAGE_5VSB_LOG: + return YM2151_PAGE_5VSB_REAL; + } + return -EINVAL; + case yh5151e: + switch (page_log) { + case YH5151E_PAGE_12V_LOG: + return YH5151E_PAGE_12V_REAL; + case YH5151E_PAGE_5V_LOG: + return YH5151E_PAGE_5V_REAL; + case YH5151E_PAGE_3V3_LOG: + return YH5151E_PAGE_3V3_REAL; + } + return -EINVAL; + } + + return -EINVAL; +} + +static int set_page(struct i2c_client *client, int page_log) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct fsp3y_data *data = to_fsp3y_data(info); + int rv; + int page_real; + + if (page_log < 0) + return 0; + + page_real = page_log_to_page_real(page_log, data->chip); + if (page_real < 0) + return page_real; + + if (data->page != page_real) { + rv = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page_real); + if (rv < 0) + return rv; + + data->page = page_real; + + /* + * Testing showed that the device has a timing issue. After + * setting a page, it takes a while, before the device actually + * gives the correct values from the correct page. 20 ms was + * tested to be enough to not give wrong values (15 ms wasn't + * enough). + */ + usleep_range(20000, 30000); + } + + return 0; +} + +static int fsp3y_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct fsp3y_data *data = to_fsp3y_data(info); + int rv; + + /* + * Inject an exponent for non-compliant YH5151-E. + */ + if (data->vout_linear_11 && reg == PMBUS_VOUT_MODE) + return 0x1A; + + rv = set_page(client, page); + if (rv < 0) + return rv; + + return i2c_smbus_read_byte_data(client, reg); +} + +static int fsp3y_read_word_data(struct i2c_client *client, int page, int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct fsp3y_data *data = to_fsp3y_data(info); + int rv; + + /* + * This masks commands which weren't tested to work correctly. Some of + * the masked commands return 0xFFFF. These would probably get tagged as + * invalid by pmbus_core. Other ones do return values which might be + * useful (that is, they are not 0xFFFF), but their encoding is unknown, + * and so they are unsupported. + */ + switch (reg) { + case PMBUS_READ_FAN_SPEED_1: + case PMBUS_READ_IIN: + case PMBUS_READ_IOUT: + case PMBUS_READ_PIN: + case PMBUS_READ_POUT: + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_READ_TEMPERATURE_2: + case PMBUS_READ_TEMPERATURE_3: + case PMBUS_READ_VIN: + case PMBUS_READ_VOUT: + case PMBUS_STATUS_WORD: + break; + default: + return -ENXIO; + } + + rv = set_page(client, page); + if (rv < 0) + return rv; + + rv = i2c_smbus_read_word_data(client, reg); + if (rv < 0) + return rv; + + /* + * Handle YH-5151E non-compliant linear11 vout voltage. + */ + if (data->vout_linear_11 && reg == PMBUS_READ_VOUT) + rv = sign_extend32(rv, 10) & 0xffff; + + return rv; +} + +static struct pmbus_driver_info fsp3y_info[] = { + [ym2151e] = { + .pages = 2, + .func[YM2151_PAGE_12V_LOG] = + PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | + PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | + PMBUS_HAVE_FAN12, + .func[YM2151_PAGE_5VSB_LOG] = + PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT, + .read_word_data = fsp3y_read_word_data, + .read_byte_data = fsp3y_read_byte_data, + }, + [yh5151e] = { + .pages = 3, + .func[YH5151E_PAGE_12V_LOG] = + PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_TEMP3, + .func[YH5151E_PAGE_5V_LOG] = + PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_POUT, + .func[YH5151E_PAGE_3V3_LOG] = + PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_POUT, + .read_word_data = fsp3y_read_word_data, + .read_byte_data = fsp3y_read_byte_data, + } +}; + +static int fsp3y_detect(struct i2c_client *client) +{ + int rv; + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + + rv = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (rv < 0) + return rv; + + buf[rv] = '\0'; + + if (rv == 8) { + if (!strcmp(buf, "YM-2151E")) + return ym2151e; + else if (!strcmp(buf, "YH-5151E")) + return yh5151e; + } + + dev_err(&client->dev, "Unsupported model %.*s\n", rv, buf); + return -ENODEV; +} + +static int fsp3y_probe(struct i2c_client *client) +{ + struct fsp3y_data *data; + const struct i2c_device_id *id; + int rv; + + data = devm_kzalloc(&client->dev, sizeof(struct fsp3y_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->chip = fsp3y_detect(client); + if (data->chip < 0) + return data->chip; + + id = i2c_client_get_device_id(client); + if (data->chip != id->driver_data) + dev_warn(&client->dev, "Device mismatch: Configured %s (%d), detected %d\n", + id->name, (int)id->driver_data, data->chip); + + rv = i2c_smbus_read_byte_data(client, PMBUS_PAGE); + if (rv < 0) + return rv; + data->page = rv; + + data->info = fsp3y_info[data->chip]; + + /* + * YH-5151E sometimes reports vout in linear11 and sometimes in + * linear16. This depends on the exact individual piece of hardware. One + * YH-5151E can use linear16 and another might use linear11 instead. + * + * The format can be recognized by reading VOUT_MODE - if it doesn't + * report a valid exponent, then vout uses linear11. Otherwise, the + * device is compliant and uses linear16. + */ + data->vout_linear_11 = false; + if (data->chip == yh5151e) { + rv = i2c_smbus_read_byte_data(client, PMBUS_VOUT_MODE); + if (rv < 0) + return rv; + + if (rv == 0xFF) + data->vout_linear_11 = true; + } + + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id fsp3y_id[] = { + { .name = "ym2151e", .driver_data = ym2151e }, + { .name = "yh5151e", .driver_data = yh5151e }, + { } +}; +MODULE_DEVICE_TABLE(i2c, fsp3y_id); + +static struct i2c_driver fsp3y_driver = { + .driver = { + .name = "fsp3y", + }, + .probe = fsp3y_probe, + .id_table = fsp3y_id +}; + +module_i2c_driver(fsp3y_driver); + +MODULE_AUTHOR("Václav Kubernát"); +MODULE_DESCRIPTION("PMBus driver for FSP/3Y-Power power supplies"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/hac300s.c b/drivers/hwmon/pmbus/hac300s.c new file mode 100644 index 000000000..761e53890 --- /dev/null +++ b/drivers/hwmon/pmbus/hac300s.c @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: GPL-2.0-only +// SPDX-FileCopyrightText: 2024 CERN (home.cern) +/* + * Hardware monitoring driver for Hi-Tron HAC300S PSU. + * + * NOTE: The HAC300S device does not support the PMBUS_VOUT_MODE register. + * On top of that, it returns the Voltage output values in Linear11 which is + * not adhering to the PMBus specifications. (PMBus Specification Part II, + * Section 7.1-7.3). For that reason the PMBUS_VOUT_MODE register is being faked + * and returns the exponent value of the READ_VOUT register. The exponent part + * of the VOUT_* registers is being cleared in order to return the mantissa to + * the pmbus core. + */ + +#include <linux/bitfield.h> +#include <linux/bits.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> + +#include "pmbus.h" + +#define LINEAR11_EXPONENT_MASK GENMASK(15, 11) +#define LINEAR11_MANTISSA_MASK GENMASK(10, 0) + +#define to_hac300s_data(x) container_of(x, struct hac300s_data, info) + +struct hac300s_data { + struct pmbus_driver_info info; + s8 exponent; +}; + +static int hac300s_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct hac300s_data *data = to_hac300s_data(info); + + if (reg == PMBUS_VOUT_MODE) + return data->exponent; + + return -ENODATA; +} + +static int hac300s_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int rv; + + switch (reg) { + case PMBUS_VOUT_OV_WARN_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + case PMBUS_MFR_VOUT_MAX: + case PMBUS_MFR_VOUT_MIN: + case PMBUS_READ_VOUT: + rv = pmbus_read_word_data(client, page, phase, reg); + if (rv < 0) + return rv; + return FIELD_GET(LINEAR11_MANTISSA_MASK, rv); + default: + return -ENODATA; + } +} + +static struct pmbus_driver_info hac300s_info = { + .pages = 1, + .func[0] = PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | \ + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_POUT | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = hac300s_read_byte_data, + .read_word_data = hac300s_read_word_data, + .format[PSC_VOLTAGE_OUT] = linear, +}; + +static struct pmbus_platform_data hac300s_pdata = { + .flags = PMBUS_NO_CAPABILITY, +}; + +static int hac300s_probe(struct i2c_client *client) +{ + struct hac300s_data *data; + int rv; + + data = devm_kzalloc(&client->dev, sizeof(struct hac300s_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA | + I2C_FUNC_SMBUS_READ_WORD_DATA)) + return -ENODEV; + + rv = i2c_smbus_read_word_data(client, PMBUS_READ_VOUT); + if (rv < 0) + return dev_err_probe(&client->dev, rv, "Failed to read vout_mode\n"); + + data->exponent = FIELD_GET(LINEAR11_EXPONENT_MASK, rv); + data->info = hac300s_info; + client->dev.platform_data = &hac300s_pdata; + return pmbus_do_probe(client, &data->info); +} + +static const struct of_device_id hac300s_of_match[] = { + { .compatible = "hitron,hac300s" }, + {} +}; +MODULE_DEVICE_TABLE(of, hac300s_of_match); + +static const struct i2c_device_id hac300s_id[] = { + { .name = "hac300s" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, hac300s_id); + +static struct i2c_driver hac300s_driver = { + .driver = { + .name = "hac300s", + .of_match_table = hac300s_of_match, + }, + .probe = hac300s_probe, + .id_table = hac300s_id, + +}; +module_i2c_driver(hac300s_driver); + +MODULE_AUTHOR("Vasileios Amoiridis"); +MODULE_DESCRIPTION("PMBus driver for Hi-Tron HAC300S PSU"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ibm-cffps.c b/drivers/hwmon/pmbus/ibm-cffps.c new file mode 100644 index 000000000..aad94bcb9 --- /dev/null +++ b/drivers/hwmon/pmbus/ibm-cffps.c @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright 2017 IBM Corp. + */ + +#include <linux/bitfield.h> +#include <linux/bitops.h> +#include <linux/debugfs.h> +#include <linux/device.h> +#include <linux/fs.h> +#include <linux/i2c.h> +#include <linux/jiffies.h> +#include <linux/leds.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/pmbus.h> + +#include "pmbus.h" + +#define CFFPS_CCIN_CMD 0xBD +#define CFFPS_FW_CMD 0xFA +#define CFFPS1_FW_NUM_BYTES 4 +#define CFFPS2_FW_NUM_WORDS 3 +#define CFFPS_SYS_CONFIG_CMD 0xDA +#define CFFPS_12VCS_VOUT_CMD 0xDE + +#define CFFPS_INPUT_HISTORY_CMD 0xD6 +#define CFFPS_INPUT_HISTORY_SIZE 101 + +#define CFFPS_CCIN_REVISION GENMASK(7, 0) +#define CFFPS_CCIN_REVISION_LEGACY 0xde +#define CFFPS_CCIN_VERSION GENMASK(15, 8) +#define CFFPS_CCIN_VERSION_1 0x2b +#define CFFPS_CCIN_VERSION_2 0x2e +#define CFFPS_CCIN_VERSION_3 0x51 + +/* STATUS_MFR_SPECIFIC bits */ +#define CFFPS_MFR_FAN_FAULT BIT(0) +#define CFFPS_MFR_THERMAL_FAULT BIT(1) +#define CFFPS_MFR_OV_FAULT BIT(2) +#define CFFPS_MFR_UV_FAULT BIT(3) +#define CFFPS_MFR_PS_KILL BIT(4) +#define CFFPS_MFR_OC_FAULT BIT(5) +#define CFFPS_MFR_VAUX_FAULT BIT(6) +#define CFFPS_MFR_CURRENT_SHARE_WARNING BIT(7) + +#define CFFPS_LED_BLINK (BIT(0) | BIT(6)) +#define CFFPS_LED_ON (BIT(1) | BIT(6)) +#define CFFPS_LED_OFF (BIT(2) | BIT(6)) +#define CFFPS_BLINK_RATE_MS 250 + +enum { + CFFPS_DEBUGFS_MAX_POWER_OUT = 0, + CFFPS_DEBUGFS_CCIN, + CFFPS_DEBUGFS_FW, + CFFPS_DEBUGFS_ON_OFF_CONFIG, + CFFPS_DEBUGFS_NUM_ENTRIES +}; + +enum versions { cffps_unknown, cffps1, cffps2 }; + +struct ibm_cffps { + enum versions version; + struct i2c_client *client; + + u8 input_history[CFFPS_INPUT_HISTORY_SIZE]; + + int debugfs_entries[CFFPS_DEBUGFS_NUM_ENTRIES]; + + char led_name[32]; + u8 led_state; + struct led_classdev led; +}; + +#define to_psu(x, y) container_of((x), struct ibm_cffps, debugfs_entries[(y)]) + +static ssize_t ibm_cffps_debugfs_read_input_history(struct file *file, char __user *buf, + size_t count, loff_t *ppos) +{ + int rc; + u8 cmd = CFFPS_INPUT_HISTORY_CMD; + struct ibm_cffps *psu = file->private_data; + struct i2c_msg msg[2] = { + { + .addr = psu->client->addr, + .flags = psu->client->flags, + .len = 1, + .buf = &cmd, + }, { + .addr = psu->client->addr, + .flags = psu->client->flags | I2C_M_RD, + .len = CFFPS_INPUT_HISTORY_SIZE, + .buf = psu->input_history, + }, + }; + + if (!*ppos) { + rc = pmbus_lock_interruptible(psu->client); + if (rc) + return rc; + + rc = pmbus_set_page(psu->client, 0, 0xff); + if (rc) { + pmbus_unlock(psu->client); + return rc; + } + + /* + * Use a raw i2c transfer, since we need more bytes + * than Linux I2C supports through smbus xfr (only 32). + */ + rc = i2c_transfer(psu->client->adapter, msg, 2); + pmbus_unlock(psu->client); + if (rc < 0) + return rc; + } + + return simple_read_from_buffer(buf, count, ppos, + psu->input_history + 1, + psu->input_history[0]); +} + +static const struct file_operations ibm_cffps_input_history_fops = { + .llseek = noop_llseek, + .read = ibm_cffps_debugfs_read_input_history, + .open = simple_open, +}; + +static ssize_t ibm_cffps_debugfs_read(struct file *file, char __user *buf, + size_t count, loff_t *ppos) +{ + int i, rc; + int *idxp = file->private_data; + int idx = *idxp; + struct ibm_cffps *psu = to_psu(idxp, idx); + char data[I2C_SMBUS_BLOCK_MAX + 2] = { 0 }; + + rc = pmbus_lock_interruptible(psu->client); + if (rc) + return rc; + + rc = pmbus_set_page(psu->client, 0, 0xff); + if (rc) + goto unlock; + + switch (idx) { + case CFFPS_DEBUGFS_MAX_POWER_OUT: + if (psu->version == cffps1) + rc = i2c_smbus_read_word_swapped(psu->client, PMBUS_MFR_POUT_MAX); + else + rc = i2c_smbus_read_word_data(psu->client, PMBUS_MFR_POUT_MAX); + if (rc >= 0) + rc = snprintf(data, I2C_SMBUS_BLOCK_MAX, "%d", rc); + break; + case CFFPS_DEBUGFS_CCIN: + rc = i2c_smbus_read_word_swapped(psu->client, CFFPS_CCIN_CMD); + if (rc >= 0) + rc = snprintf(data, 5, "%04X", rc); + break; + case CFFPS_DEBUGFS_FW: + switch (psu->version) { + case cffps1: + for (i = 0; i < CFFPS1_FW_NUM_BYTES; ++i) { + rc = i2c_smbus_read_byte_data(psu->client, CFFPS_FW_CMD + i); + if (rc < 0) + goto unlock; + + snprintf(&data[i * 2], 3, "%02X", rc); + } + + rc = i * 2; + break; + case cffps2: + for (i = 0; i < CFFPS2_FW_NUM_WORDS; ++i) { + rc = i2c_smbus_read_word_data(psu->client, CFFPS_FW_CMD + i); + if (rc < 0) + goto unlock; + + snprintf(&data[i * 4], 5, "%04X", rc); + } + + rc = i * 4; + break; + default: + rc = -EOPNOTSUPP; + break; + } + break; + case CFFPS_DEBUGFS_ON_OFF_CONFIG: + rc = i2c_smbus_read_byte_data(psu->client, PMBUS_ON_OFF_CONFIG); + if (rc >= 0) + rc = snprintf(data, 3, "%02x", rc); + break; + default: + rc = -EINVAL; + break; + } + +unlock: + pmbus_unlock(psu->client); + if (rc < 0) + return rc; + + data[rc] = '\n'; + rc += 2; + + return simple_read_from_buffer(buf, count, ppos, data, rc); +} + +static ssize_t ibm_cffps_debugfs_write(struct file *file, + const char __user *buf, size_t count, + loff_t *ppos) +{ + u8 data; + ssize_t rc; + int *idxp = file->private_data; + int idx = *idxp; + struct ibm_cffps *psu = to_psu(idxp, idx); + + switch (idx) { + case CFFPS_DEBUGFS_ON_OFF_CONFIG: + rc = simple_write_to_buffer(&data, 1, ppos, buf, count); + if (rc <= 0) + return rc; + + rc = pmbus_lock_interruptible(psu->client); + if (rc) + return rc; + + rc = pmbus_set_page(psu->client, 0, 0xff); + if (rc) { + pmbus_unlock(psu->client); + return rc; + } + + rc = i2c_smbus_write_byte_data(psu->client, PMBUS_ON_OFF_CONFIG, data); + pmbus_unlock(psu->client); + if (rc) + return rc; + + rc = 1; + break; + default: + return -EINVAL; + } + + return rc; +} + +static const struct file_operations ibm_cffps_fops = { + .llseek = noop_llseek, + .read = ibm_cffps_debugfs_read, + .write = ibm_cffps_debugfs_write, + .open = simple_open, +}; + +static int ibm_cffps_read_byte_data(struct i2c_client *client, int page, + int reg) +{ + int rc, mfr; + + switch (reg) { + case PMBUS_STATUS_VOUT: + case PMBUS_STATUS_IOUT: + case PMBUS_STATUS_TEMPERATURE: + case PMBUS_STATUS_FAN_12: + rc = pmbus_read_byte_data(client, page, reg); + if (rc < 0) + return rc; + + mfr = pmbus_read_byte_data(client, page, + PMBUS_STATUS_MFR_SPECIFIC); + if (mfr < 0) + /* + * Return the status register instead of an error, + * since we successfully read status. + */ + return rc; + + /* Add MFR_SPECIFIC bits to the standard pmbus status regs. */ + if (reg == PMBUS_STATUS_FAN_12) { + if (mfr & CFFPS_MFR_FAN_FAULT) + rc |= PB_FAN_FAN1_FAULT; + } else if (reg == PMBUS_STATUS_TEMPERATURE) { + if (mfr & CFFPS_MFR_THERMAL_FAULT) + rc |= PB_TEMP_OT_FAULT; + } else if (reg == PMBUS_STATUS_VOUT) { + if (mfr & (CFFPS_MFR_OV_FAULT | CFFPS_MFR_VAUX_FAULT)) + rc |= PB_VOLTAGE_OV_FAULT; + if (mfr & CFFPS_MFR_UV_FAULT) + rc |= PB_VOLTAGE_UV_FAULT; + } else if (reg == PMBUS_STATUS_IOUT) { + if (mfr & CFFPS_MFR_OC_FAULT) + rc |= PB_IOUT_OC_FAULT; + if (mfr & CFFPS_MFR_CURRENT_SHARE_WARNING) + rc |= PB_CURRENT_SHARE_FAULT; + } + break; + default: + rc = -ENODATA; + break; + } + + return rc; +} + +static int ibm_cffps_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int rc, mfr; + + switch (reg) { + case PMBUS_STATUS_WORD: + rc = pmbus_read_word_data(client, page, phase, reg); + if (rc < 0) + return rc; + + mfr = pmbus_read_byte_data(client, page, + PMBUS_STATUS_MFR_SPECIFIC); + if (mfr < 0) + /* + * Return the status register instead of an error, + * since we successfully read status. + */ + return rc; + + if (mfr & CFFPS_MFR_PS_KILL) + rc |= PB_STATUS_OFF; + break; + case PMBUS_VIRT_READ_VMON: + rc = pmbus_read_word_data(client, page, phase, + CFFPS_12VCS_VOUT_CMD); + break; + default: + rc = -ENODATA; + break; + } + + return rc; +} + +static int ibm_cffps_led_brightness_set(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + int rc; + u8 next_led_state; + struct ibm_cffps *psu = container_of(led_cdev, struct ibm_cffps, led); + + if (brightness == LED_OFF) { + next_led_state = CFFPS_LED_OFF; + } else { + brightness = LED_FULL; + + if (psu->led_state != CFFPS_LED_BLINK) + next_led_state = CFFPS_LED_ON; + else + next_led_state = CFFPS_LED_BLINK; + } + + dev_dbg(&psu->client->dev, "LED brightness set: %d. Command: %d.\n", + brightness, next_led_state); + + rc = pmbus_lock_interruptible(psu->client); + if (rc) + return rc; + + rc = pmbus_set_page(psu->client, 0, 0xff); + if (rc) { + pmbus_unlock(psu->client); + return rc; + } + + rc = i2c_smbus_write_byte_data(psu->client, CFFPS_SYS_CONFIG_CMD, + next_led_state); + pmbus_unlock(psu->client); + if (rc < 0) + return rc; + + psu->led_state = next_led_state; + led_cdev->brightness = brightness; + + return 0; +} + +static int ibm_cffps_led_blink_set(struct led_classdev *led_cdev, + unsigned long *delay_on, + unsigned long *delay_off) +{ + int rc; + struct ibm_cffps *psu = container_of(led_cdev, struct ibm_cffps, led); + + dev_dbg(&psu->client->dev, "LED blink set.\n"); + + rc = pmbus_lock_interruptible(psu->client); + if (rc) + return rc; + + rc = pmbus_set_page(psu->client, 0, 0xff); + if (rc) { + pmbus_unlock(psu->client); + return rc; + } + + rc = i2c_smbus_write_byte_data(psu->client, CFFPS_SYS_CONFIG_CMD, + CFFPS_LED_BLINK); + pmbus_unlock(psu->client); + if (rc < 0) + return rc; + + psu->led_state = CFFPS_LED_BLINK; + led_cdev->brightness = LED_FULL; + *delay_on = CFFPS_BLINK_RATE_MS; + *delay_off = CFFPS_BLINK_RATE_MS; + + return 0; +} + +static void ibm_cffps_create_led_class(struct ibm_cffps *psu) +{ + int rc; + struct i2c_client *client = psu->client; + struct device *dev = &client->dev; + + snprintf(psu->led_name, sizeof(psu->led_name), "%s-%02x", client->name, + client->addr); + psu->led.name = psu->led_name; + psu->led.max_brightness = LED_FULL; + psu->led.brightness_set_blocking = ibm_cffps_led_brightness_set; + psu->led.blink_set = ibm_cffps_led_blink_set; + + rc = devm_led_classdev_register(dev, &psu->led); + if (rc) + dev_warn(dev, "failed to register led class: %d\n", rc); + else + i2c_smbus_write_byte_data(client, CFFPS_SYS_CONFIG_CMD, + CFFPS_LED_OFF); +} + +static struct pmbus_driver_info ibm_cffps_info[] = { + [cffps1] = { + .pages = 1, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_FAN12 | PMBUS_HAVE_TEMP | + PMBUS_HAVE_TEMP2 | PMBUS_HAVE_TEMP3 | + PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_FAN12, + .read_byte_data = ibm_cffps_read_byte_data, + .read_word_data = ibm_cffps_read_word_data, + }, + [cffps2] = { + .pages = 2, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_FAN12 | PMBUS_HAVE_TEMP | + PMBUS_HAVE_TEMP2 | PMBUS_HAVE_TEMP3 | + PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_FAN12 | PMBUS_HAVE_VMON, + .func[1] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | + PMBUS_HAVE_TEMP3 | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = ibm_cffps_read_byte_data, + .read_word_data = ibm_cffps_read_word_data, + }, +}; + +static struct pmbus_platform_data ibm_cffps_pdata = { + .flags = PMBUS_SKIP_STATUS_CHECK | PMBUS_NO_CAPABILITY, +}; + +static const struct i2c_device_id ibm_cffps_id[] = { + { .name = "ibm_cffps1", .driver_data = cffps1 }, + { .name = "ibm_cffps2", .driver_data = cffps2 }, + { .name = "ibm_cffps", .driver_data = cffps_unknown }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ibm_cffps_id); + +static int ibm_cffps_probe(struct i2c_client *client) +{ + int i, rc; + enum versions vs = (uintptr_t)i2c_get_match_data(client); + struct dentry *debugfs; + struct ibm_cffps *psu; + + if (vs == cffps_unknown) { + u16 ccin_revision = 0; + u16 ccin_version = CFFPS_CCIN_VERSION_1; + int ccin = i2c_smbus_read_word_swapped(client, CFFPS_CCIN_CMD); + char mfg_id[I2C_SMBUS_BLOCK_MAX + 2] = { 0 }; + + if (ccin > 0) { + ccin_revision = FIELD_GET(CFFPS_CCIN_REVISION, ccin); + ccin_version = FIELD_GET(CFFPS_CCIN_VERSION, ccin); + } + + rc = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, mfg_id); + if (rc < 0) { + dev_err(&client->dev, "Failed to read Manufacturer ID\n"); + return rc; + } + + switch (ccin_version) { + default: + case CFFPS_CCIN_VERSION_1: + if ((strncmp(mfg_id, "ACBE", 4) == 0) || + (strncmp(mfg_id, "ARTE", 4) == 0)) + vs = cffps1; + else + vs = cffps2; + break; + case CFFPS_CCIN_VERSION_2: + vs = cffps2; + break; + case CFFPS_CCIN_VERSION_3: + if (ccin_revision == CFFPS_CCIN_REVISION_LEGACY) + vs = cffps1; + else + vs = cffps2; + break; + } + + /* Set the client name to include the version number. */ + snprintf(client->name, I2C_NAME_SIZE, "cffps%d", vs); + } + + client->dev.platform_data = &ibm_cffps_pdata; + rc = pmbus_do_probe(client, &ibm_cffps_info[vs]); + if (rc) + return rc; + + /* + * Don't fail the probe if there isn't enough memory for leds and + * debugfs. + */ + psu = devm_kzalloc(&client->dev, sizeof(*psu), GFP_KERNEL); + if (!psu) + return 0; + + psu->version = vs; + psu->client = client; + + ibm_cffps_create_led_class(psu); + + /* Don't fail the probe if we can't create debugfs */ + debugfs = pmbus_get_debugfs_dir(client); + if (!debugfs) + return 0; + + for (i = 0; i < CFFPS_DEBUGFS_NUM_ENTRIES; ++i) + psu->debugfs_entries[i] = i; + + debugfs_create_file("input_history", 0444, debugfs, psu, &ibm_cffps_input_history_fops); + debugfs_create_file("max_power_out", 0444, debugfs, + &psu->debugfs_entries[CFFPS_DEBUGFS_MAX_POWER_OUT], + &ibm_cffps_fops); + debugfs_create_file("ccin", 0444, debugfs, + &psu->debugfs_entries[CFFPS_DEBUGFS_CCIN], + &ibm_cffps_fops); + debugfs_create_file("fw_version", 0444, debugfs, + &psu->debugfs_entries[CFFPS_DEBUGFS_FW], + &ibm_cffps_fops); + debugfs_create_file("on_off_config", 0644, debugfs, + &psu->debugfs_entries[CFFPS_DEBUGFS_ON_OFF_CONFIG], + &ibm_cffps_fops); + + /* For compatibility with users of the old naming scheme. */ + debugfs_create_symlink(client->name, debugfs, "."); + + return 0; +} + +static const struct of_device_id ibm_cffps_of_match[] = { + { + .compatible = "ibm,cffps1", + .data = (void *)cffps1 + }, + { + .compatible = "ibm,cffps2", + .data = (void *)cffps2 + }, + { + .compatible = "ibm,cffps", + .data = (void *)cffps_unknown + }, + {} +}; +MODULE_DEVICE_TABLE(of, ibm_cffps_of_match); + +static struct i2c_driver ibm_cffps_driver = { + .driver = { + .name = "ibm-cffps", + .of_match_table = ibm_cffps_of_match, + }, + .probe = ibm_cffps_probe, + .id_table = ibm_cffps_id, +}; + +module_i2c_driver(ibm_cffps_driver); + +MODULE_AUTHOR("Eddie James"); +MODULE_DESCRIPTION("PMBus driver for IBM Common Form Factor power supplies"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ina233.c b/drivers/hwmon/pmbus/ina233.c new file mode 100644 index 000000000..c72aa258b --- /dev/null +++ b/drivers/hwmon/pmbus/ina233.c @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for ina233 + * + * Copyright (c) 2025 Leo Yang + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define MFR_READ_VSHUNT 0xd1 +#define MFR_CALIBRATION 0xd4 + +#define INA233_MAX_CURRENT_DEFAULT 32768000 /* uA */ +#define INA233_RSHUNT_DEFAULT 2000 /* uOhm */ + +#define MAX_M_VAL 32767 + +static void calculate_coef(int *m, int *R, u32 current_lsb, int power_coef) +{ + u64 scaled_m; + int scale_factor = 0; + int scale_coef = 1; + + /* + * 1000000 from Current_LSB A->uA . + * scale_coef is for scaling up to minimize rounding errors, + * If there is no decimal information, no need to scale. + */ + if (1000000 % current_lsb) { + /* Scaling to keep integer precision */ + scale_factor = -3; + scale_coef = 1000; + } + + /* + * Unit Conversion (Current_LSB A->uA) and use scaling(scale_factor) + * to keep integer precision. + * Formulae referenced from spec. + */ + scaled_m = div64_u64(1000000 * scale_coef, (u64)current_lsb * power_coef); + + /* Maximize while keeping it bounded.*/ + while (scaled_m > MAX_M_VAL) { + scaled_m = div_u64(scaled_m, 10); + scale_factor++; + } + /* Scale up only if fractional part exists. */ + while (scaled_m * 10 < MAX_M_VAL && scale_coef != 1) { + scaled_m *= 10; + scale_factor--; + } + + *m = scaled_m; + *R = scale_factor; +} + +static int ina233_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VMON: + ret = pmbus_read_word_data(client, 0, 0xff, MFR_READ_VSHUNT); + if (ret < 0) + return ret; + + /* Adjust returned value to match VIN coefficients */ + /* VIN: 1.25 mV VSHUNT: 2.5 uV LSB */ + ret = clamp_val(DIV_ROUND_CLOSEST((s16)ret * 25, 12500), + S16_MIN, S16_MAX) & 0xffff; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int ina233_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + const char *propname; + int ret, m, R; + u32 rshunt; + u32 max_current; + u32 current_lsb; + u16 calibration; + struct pmbus_driver_info *info; + + info = devm_kzalloc(dev, sizeof(struct pmbus_driver_info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->pages = 1; + info->format[PSC_VOLTAGE_IN] = direct; + info->format[PSC_VOLTAGE_OUT] = direct; + info->format[PSC_CURRENT_OUT] = direct; + info->format[PSC_POWER] = direct; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT + | PMBUS_HAVE_VMON | PMBUS_HAVE_STATUS_VMON; + info->m[PSC_VOLTAGE_IN] = 8; + info->R[PSC_VOLTAGE_IN] = 2; + info->m[PSC_VOLTAGE_OUT] = 8; + info->R[PSC_VOLTAGE_OUT] = 2; + info->read_word_data = ina233_read_word_data; + + /* If INA233 skips current/power, shunt-resistor and current-lsb aren't needed. */ + /* read rshunt value (uOhm) */ + propname = "shunt-resistor"; + if (device_property_present(dev, propname)) { + ret = device_property_read_u32(dev, propname, &rshunt); + if (ret) + return dev_err_probe(dev, ret, "%s property read fail.\n", propname); + } else { + rshunt = INA233_RSHUNT_DEFAULT; + } + if (!rshunt) + return dev_err_probe(dev, -EINVAL, "%s cannot be zero.\n", propname); + + /* read Maximum expected current value (uA) */ + propname = "ti,maximum-expected-current-microamp"; + if (device_property_present(dev, propname)) { + ret = device_property_read_u32(dev, propname, &max_current); + if (ret) + return dev_err_probe(dev, ret, "%s property read fail.\n", propname); + } else { + max_current = INA233_MAX_CURRENT_DEFAULT; + } + if (max_current < 32768) + return dev_err_probe(dev, -EINVAL, "%s cannot be less than 32768.\n", propname); + + /* Calculate Current_LSB according to the spec formula */ + current_lsb = max_current / 32768; + + /* calculate current coefficient */ + calculate_coef(&m, &R, current_lsb, 1); + info->m[PSC_CURRENT_OUT] = m; + info->R[PSC_CURRENT_OUT] = R; + + /* calculate power coefficient */ + calculate_coef(&m, &R, current_lsb, 25); + info->m[PSC_POWER] = m; + info->R[PSC_POWER] = R; + + /* write MFR_CALIBRATION register, Apply formula from spec with unit scaling. */ + calibration = div64_u64(5120000000ULL, (u64)rshunt * current_lsb); + if (calibration > 0x7FFF) + return dev_err_probe(dev, -EINVAL, + "Product of Current_LSB %u and shunt resistor %u too small, MFR_CALIBRATION reg exceeds 0x7FFF.\n", + current_lsb, rshunt); + ret = i2c_smbus_write_word_data(client, MFR_CALIBRATION, calibration); + if (ret < 0) + return dev_err_probe(dev, ret, "Unable to write calibration.\n"); + + dev_dbg(dev, "power monitor %s (Rshunt = %u uOhm, Current_LSB = %u uA/bit)\n", + client->name, rshunt, current_lsb); + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id ina233_id[] = { + { .name = "ina233" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ina233_id); + +static const struct of_device_id __maybe_unused ina233_of_match[] = { + { .compatible = "ti,ina233" }, + {} +}; +MODULE_DEVICE_TABLE(of, ina233_of_match); + +static struct i2c_driver ina233_driver = { + .driver = { + .name = "ina233", + .of_match_table = of_match_ptr(ina233_of_match), + }, + .probe = ina233_probe, + .id_table = ina233_id, +}; + +module_i2c_driver(ina233_driver); + +MODULE_AUTHOR("Leo Yang <leo.yang.sy0@gmail.com>"); +MODULE_DESCRIPTION("PMBus driver for INA233 and compatible chips"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/inspur-ipsps.c b/drivers/hwmon/pmbus/inspur-ipsps.c new file mode 100644 index 000000000..57c0fc169 --- /dev/null +++ b/drivers/hwmon/pmbus/inspur-ipsps.c @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright 2019 Inspur Corp. + */ + +#include <linux/debugfs.h> +#include <linux/device.h> +#include <linux/fs.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include <linux/hwmon-sysfs.h> + +#include "pmbus.h" + +#define IPSPS_REG_VENDOR_ID 0x99 +#define IPSPS_REG_MODEL 0x9A +#define IPSPS_REG_FW_VERSION 0x9B +#define IPSPS_REG_PN 0x9C +#define IPSPS_REG_SN 0x9E +#define IPSPS_REG_HW_VERSION 0xB0 +#define IPSPS_REG_MODE 0xFC + +#define MODE_ACTIVE 0x55 +#define MODE_STANDBY 0x0E +#define MODE_REDUNDANCY 0x00 + +#define MODE_ACTIVE_STRING "active" +#define MODE_STANDBY_STRING "standby" +#define MODE_REDUNDANCY_STRING "redundancy" + +enum ipsps_index { + vendor, + model, + fw_version, + part_number, + serial_number, + hw_version, + mode, + num_regs, +}; + +static const u8 ipsps_regs[num_regs] = { + [vendor] = IPSPS_REG_VENDOR_ID, + [model] = IPSPS_REG_MODEL, + [fw_version] = IPSPS_REG_FW_VERSION, + [part_number] = IPSPS_REG_PN, + [serial_number] = IPSPS_REG_SN, + [hw_version] = IPSPS_REG_HW_VERSION, + [mode] = IPSPS_REG_MODE, +}; + +static ssize_t ipsps_string_show(struct device *dev, + struct device_attribute *devattr, + char *buf) +{ + u8 reg; + int rc; + char *p; + char data[I2C_SMBUS_BLOCK_MAX + 1]; + struct i2c_client *client = to_i2c_client(dev->parent); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + + reg = ipsps_regs[attr->index]; + rc = i2c_smbus_read_block_data(client, reg, data); + if (rc < 0) + return rc; + + /* filled with printable characters, ending with # */ + p = memscan(data, '#', rc); + *p = '\0'; + + return sysfs_emit(buf, "%s\n", data); +} + +static ssize_t ipsps_fw_version_show(struct device *dev, + struct device_attribute *devattr, + char *buf) +{ + u8 reg; + int rc; + u8 data[I2C_SMBUS_BLOCK_MAX] = { 0 }; + struct i2c_client *client = to_i2c_client(dev->parent); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + + reg = ipsps_regs[attr->index]; + rc = i2c_smbus_read_block_data(client, reg, data); + if (rc < 0) + return rc; + + if (rc != 6) + return -EPROTO; + + return sysfs_emit(buf, "%u.%02u%u-%u.%02u\n", + data[1], data[2]/* < 100 */, data[3]/*< 10*/, + data[4], data[5]/* < 100 */); +} + +static ssize_t ipsps_mode_show(struct device *dev, + struct device_attribute *devattr, char *buf) +{ + u8 reg; + int rc; + struct i2c_client *client = to_i2c_client(dev->parent); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + + reg = ipsps_regs[attr->index]; + rc = i2c_smbus_read_byte_data(client, reg); + if (rc < 0) + return rc; + + switch (rc) { + case MODE_ACTIVE: + return sysfs_emit(buf, "[%s] %s %s\n", + MODE_ACTIVE_STRING, + MODE_STANDBY_STRING, MODE_REDUNDANCY_STRING); + case MODE_STANDBY: + return sysfs_emit(buf, "%s [%s] %s\n", + MODE_ACTIVE_STRING, + MODE_STANDBY_STRING, MODE_REDUNDANCY_STRING); + case MODE_REDUNDANCY: + return sysfs_emit(buf, "%s %s [%s]\n", + MODE_ACTIVE_STRING, + MODE_STANDBY_STRING, MODE_REDUNDANCY_STRING); + default: + return sysfs_emit(buf, "unspecified\n"); + } +} + +static ssize_t ipsps_mode_store(struct device *dev, + struct device_attribute *devattr, + const char *buf, size_t count) +{ + u8 reg; + int rc; + struct i2c_client *client = to_i2c_client(dev->parent); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + + reg = ipsps_regs[attr->index]; + if (sysfs_streq(MODE_STANDBY_STRING, buf)) { + rc = i2c_smbus_write_byte_data(client, reg, + MODE_STANDBY); + if (rc < 0) + return rc; + return count; + } else if (sysfs_streq(MODE_ACTIVE_STRING, buf)) { + rc = i2c_smbus_write_byte_data(client, reg, + MODE_ACTIVE); + if (rc < 0) + return rc; + return count; + } + + return -EINVAL; +} + +static SENSOR_DEVICE_ATTR_RO(vendor, ipsps_string, vendor); +static SENSOR_DEVICE_ATTR_RO(model, ipsps_string, model); +static SENSOR_DEVICE_ATTR_RO(part_number, ipsps_string, part_number); +static SENSOR_DEVICE_ATTR_RO(serial_number, ipsps_string, serial_number); +static SENSOR_DEVICE_ATTR_RO(hw_version, ipsps_string, hw_version); +static SENSOR_DEVICE_ATTR_RO(fw_version, ipsps_fw_version, fw_version); +static SENSOR_DEVICE_ATTR_RW(mode, ipsps_mode, mode); + +static struct attribute *ipsps_attrs[] = { + &sensor_dev_attr_vendor.dev_attr.attr, + &sensor_dev_attr_model.dev_attr.attr, + &sensor_dev_attr_part_number.dev_attr.attr, + &sensor_dev_attr_serial_number.dev_attr.attr, + &sensor_dev_attr_hw_version.dev_attr.attr, + &sensor_dev_attr_fw_version.dev_attr.attr, + &sensor_dev_attr_mode.dev_attr.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(ipsps); + +static struct pmbus_driver_info ipsps_info = { + .pages = 1, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_POUT | PMBUS_HAVE_PIN | + PMBUS_HAVE_FAN12 | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | + PMBUS_HAVE_TEMP3 | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_STATUS_TEMP | PMBUS_HAVE_STATUS_FAN12, + .groups = ipsps_groups, +}; + +static struct pmbus_platform_data ipsps_pdata = { + .flags = PMBUS_SKIP_STATUS_CHECK, +}; + +static int ipsps_probe(struct i2c_client *client) +{ + client->dev.platform_data = &ipsps_pdata; + return pmbus_do_probe(client, &ipsps_info); +} + +static const struct i2c_device_id ipsps_id[] = { + { .name = "ipsps1" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ipsps_id); + +#ifdef CONFIG_OF +static const struct of_device_id ipsps_of_match[] = { + { .compatible = "inspur,ipsps1" }, + {} +}; +MODULE_DEVICE_TABLE(of, ipsps_of_match); +#endif + +static struct i2c_driver ipsps_driver = { + .driver = { + .name = "inspur-ipsps", + .of_match_table = of_match_ptr(ipsps_of_match), + }, + .probe = ipsps_probe, + .id_table = ipsps_id, +}; + +module_i2c_driver(ipsps_driver); + +MODULE_AUTHOR("John Wang"); +MODULE_DESCRIPTION("PMBus driver for Inspur Power System power supplies"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ir35221.c b/drivers/hwmon/pmbus/ir35221.c new file mode 100644 index 000000000..b2120fc76 --- /dev/null +++ b/drivers/hwmon/pmbus/ir35221.c @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for IR35221 + * + * Copyright (C) IBM Corporation 2017. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define IR35221_MFR_VIN_PEAK 0xc5 +#define IR35221_MFR_VOUT_PEAK 0xc6 +#define IR35221_MFR_IOUT_PEAK 0xc7 +#define IR35221_MFR_TEMP_PEAK 0xc8 +#define IR35221_MFR_VIN_VALLEY 0xc9 +#define IR35221_MFR_VOUT_VALLEY 0xca +#define IR35221_MFR_IOUT_VALLEY 0xcb +#define IR35221_MFR_TEMP_VALLEY 0xcc + +static int ir35221_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VIN_MAX: + ret = pmbus_read_word_data(client, page, phase, + IR35221_MFR_VIN_PEAK); + break; + case PMBUS_VIRT_READ_VOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, + IR35221_MFR_VOUT_PEAK); + break; + case PMBUS_VIRT_READ_IOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, + IR35221_MFR_IOUT_PEAK); + break; + case PMBUS_VIRT_READ_TEMP_MAX: + ret = pmbus_read_word_data(client, page, phase, + IR35221_MFR_TEMP_PEAK); + break; + case PMBUS_VIRT_READ_VIN_MIN: + ret = pmbus_read_word_data(client, page, phase, + IR35221_MFR_VIN_VALLEY); + break; + case PMBUS_VIRT_READ_VOUT_MIN: + ret = pmbus_read_word_data(client, page, phase, + IR35221_MFR_VOUT_VALLEY); + break; + case PMBUS_VIRT_READ_IOUT_MIN: + ret = pmbus_read_word_data(client, page, phase, + IR35221_MFR_IOUT_VALLEY); + break; + case PMBUS_VIRT_READ_TEMP_MIN: + ret = pmbus_read_word_data(client, page, phase, + IR35221_MFR_TEMP_VALLEY); + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int ir35221_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA + | I2C_FUNC_SMBUS_READ_WORD_DATA + | I2C_FUNC_SMBUS_READ_BLOCK_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, buf); + if (ret < 0) { + dev_err(&client->dev, "Failed to read PMBUS_MFR_ID\n"); + return ret; + } + if (ret != 2 || strncmp(buf, "RI", strlen("RI"))) { + dev_err(&client->dev, "MFR_ID unrecognised\n"); + return -ENODEV; + } + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) { + dev_err(&client->dev, "Failed to read PMBUS_MFR_MODEL\n"); + return ret; + } + if (ret != 2 || !(buf[0] == 0x6c && buf[1] == 0x00)) { + dev_err(&client->dev, "MFR_MODEL unrecognised\n"); + return -ENODEV; + } + + info = devm_kzalloc(&client->dev, sizeof(struct pmbus_driver_info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + info->read_word_data = ir35221_read_word_data; + + info->pages = 2; + info->format[PSC_VOLTAGE_IN] = linear; + info->format[PSC_VOLTAGE_OUT] = linear; + info->format[PSC_CURRENT_IN] = linear; + info->format[PSC_CURRENT_OUT] = linear; + info->format[PSC_POWER] = linear; + info->format[PSC_TEMPERATURE] = linear; + + info->func[0] = PMBUS_HAVE_VIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_IIN + | PMBUS_HAVE_IOUT | PMBUS_HAVE_PIN + | PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP; + info->func[1] = info->func[0]; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id ir35221_id[] = { + { .name = "ir35221" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, ir35221_id); + +static struct i2c_driver ir35221_driver = { + .driver = { + .name = "ir35221", + }, + .probe = ir35221_probe, + .id_table = ir35221_id, +}; + +module_i2c_driver(ir35221_driver); + +MODULE_AUTHOR("Samuel Mendoza-Jonas <sam@mendozajonas.com"); +MODULE_DESCRIPTION("PMBus driver for IR35221"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ir36021.c b/drivers/hwmon/pmbus/ir36021.c new file mode 100644 index 000000000..58d8c4aac --- /dev/null +++ b/drivers/hwmon/pmbus/ir36021.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Infineon IR36021 + * + * Copyright (c) 2021 Allied Telesis + */ +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +static struct pmbus_driver_info ir36021_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT + | PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT + | PMBUS_HAVE_PIN | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 + | PMBUS_HAVE_STATUS_TEMP, +}; + +static int ir36021_probe(struct i2c_client *client) +{ + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + + ret = pmbus_read_smbus_i2c_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "Failed to read PMBUS_MFR_MODEL\n"); + if (ret != 1 || buf[0] != 0x2d) + return dev_err_probe(&client->dev, -ENODEV, + "MFR_MODEL unrecognised\n"); + + return pmbus_do_probe(client, &ir36021_info); +} + +static const struct i2c_device_id ir36021_id[] = { + { .name = "ir36021" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ir36021_id); + +static const struct of_device_id __maybe_unused ir36021_of_id[] = { + { .compatible = "infineon,ir36021" }, + {}, +}; +MODULE_DEVICE_TABLE(of, ir36021_of_id); + +static struct i2c_driver ir36021_driver = { + .driver = { + .name = "ir36021", + .of_match_table = of_match_ptr(ir36021_of_id), + }, + .probe = ir36021_probe, + .id_table = ir36021_id, +}; + +module_i2c_driver(ir36021_driver); + +MODULE_AUTHOR("Chris Packham <chris.packham@alliedtelesis.co.nz>"); +MODULE_DESCRIPTION("PMBus driver for Infineon IR36021"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ir38064.c b/drivers/hwmon/pmbus/ir38064.c new file mode 100644 index 000000000..47ce88e9d --- /dev/null +++ b/drivers/hwmon/pmbus/ir38064.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Infineon IR38064 + * + * Copyright (c) 2017 Google Inc + * + * VOUT_MODE is not supported by the device. The driver fakes VOUT linear16 + * mode with exponent value -8 as direct mode with m=256/b=0/R=0. + * + * The device supports VOUT_PEAK, IOUT_PEAK, and TEMPERATURE_PEAK, however + * this driver does not currently support them. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/regulator/driver.h> +#include "pmbus.h" + +#if IS_ENABLED(CONFIG_SENSORS_IR38064_REGULATOR) +static const struct regulator_desc ir38064_reg_desc[] = { + PMBUS_REGULATOR_ONE("vout"), +}; +#endif /* CONFIG_SENSORS_IR38064_REGULATOR */ + +static struct pmbus_driver_info ir38064_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .m[PSC_VOLTAGE_OUT] = 256, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT, +#if IS_ENABLED(CONFIG_SENSORS_IR38064_REGULATOR) + .num_regulators = 1, + .reg_desc = ir38064_reg_desc, +#endif +}; + +static int ir38064_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &ir38064_info); +} + +static const struct i2c_device_id ir38064_id[] = { + { .name = "ir38060" }, + { .name = "ir38064" }, + { .name = "ir38164" }, + { .name = "ir38263" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, ir38064_id); + +static const struct of_device_id __maybe_unused ir38064_of_match[] = { + { .compatible = "infineon,ir38060" }, + { .compatible = "infineon,ir38064" }, + { .compatible = "infineon,ir38164" }, + { .compatible = "infineon,ir38263" }, + {} +}; + +MODULE_DEVICE_TABLE(of, ir38064_of_match); + +/* This is the driver that will be inserted */ +static struct i2c_driver ir38064_driver = { + .driver = { + .name = "ir38064", + .of_match_table = of_match_ptr(ir38064_of_match), + }, + .probe = ir38064_probe, + .id_table = ir38064_id, +}; + +module_i2c_driver(ir38064_driver); + +MODULE_AUTHOR("Maxim Sloyko <maxims@google.com>"); +MODULE_DESCRIPTION("PMBus driver for Infineon IR38064 and compatible chips"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/irps5401.c b/drivers/hwmon/pmbus/irps5401.c new file mode 100644 index 000000000..1694b96d7 --- /dev/null +++ b/drivers/hwmon/pmbus/irps5401.c @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for the Infineon IRPS5401M PMIC. + * + * Copyright (c) 2019 SED Systems, a division of Calian Ltd. + * + * The device supports VOUT_PEAK, IOUT_PEAK, and TEMPERATURE_PEAK, however + * this driver does not currently support them. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define IRPS5401_SW_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | \ + PMBUS_HAVE_STATUS_INPUT | \ + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP) + +#define IRPS5401_LDO_FUNC (PMBUS_HAVE_VIN | \ + PMBUS_HAVE_STATUS_INPUT | \ + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP) + +static struct pmbus_driver_info irps5401_info = { + .pages = 5, + .func[0] = IRPS5401_SW_FUNC, + .func[1] = IRPS5401_SW_FUNC, + .func[2] = IRPS5401_SW_FUNC, + .func[3] = IRPS5401_SW_FUNC, + .func[4] = IRPS5401_LDO_FUNC, +}; + +static int irps5401_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &irps5401_info); +} + +static const struct i2c_device_id irps5401_id[] = { + { .name = "irps5401" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, irps5401_id); + +static struct i2c_driver irps5401_driver = { + .driver = { + .name = "irps5401", + }, + .probe = irps5401_probe, + .id_table = irps5401_id, +}; + +module_i2c_driver(irps5401_driver); + +MODULE_AUTHOR("Robert Hancock"); +MODULE_DESCRIPTION("PMBus driver for Infineon IRPS5401"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/isl68137.c b/drivers/hwmon/pmbus/isl68137.c new file mode 100644 index 000000000..2f7f825bf --- /dev/null +++ b/drivers/hwmon/pmbus/isl68137.c @@ -0,0 +1,529 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Renesas Digital Multiphase Voltage Regulators + * + * Copyright (c) 2017 Google Inc + * Copyright (c) 2020 Renesas Electronics America + * + */ + +#include <linux/err.h> +#include <linux/hwmon-sysfs.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/string.h> +#include <linux/sysfs.h> + +#include "pmbus.h" + +#define ISL68137_VOUT_AVS 0x30 +#define RAA_DMPVR2_READ_VMON 0xc8 +#define MAX_CHANNELS 4 + +enum variants { + raa_dmpvr1_2rail, + raa_dmpvr2_1rail, + raa_dmpvr2_2rail, + raa_dmpvr2_2rail_nontc, + raa_dmpvr2_2rail_pmbus, + raa_dmpvr2_3rail, + raa_dmpvr2_hv, +}; + +struct isl68137_channel { + u32 vout_voltage_divider[2]; +}; + +struct isl68137_data { + struct pmbus_driver_info info; + struct isl68137_channel channel[MAX_CHANNELS]; +}; + +#define to_isl68137_data(x) container_of(x, struct isl68137_data, info) + +static ssize_t isl68137_avs_enable_show_page(struct i2c_client *client, + int page, + char *buf) +{ + int val; + + val = pmbus_lock_interruptible(client); + if (val) + return val; + + val = pmbus_read_byte_data(client, page, PMBUS_OPERATION); + + pmbus_unlock(client); + + if (val < 0) + return val; + + return sysfs_emit(buf, "%d\n", + (val & ISL68137_VOUT_AVS) == ISL68137_VOUT_AVS); +} + +static ssize_t isl68137_avs_enable_store_page(struct i2c_client *client, + int page, + const char *buf, size_t count) +{ + int rc, op_val; + bool result; + + rc = kstrtobool(buf, &result); + if (rc) + return rc; + + op_val = result ? ISL68137_VOUT_AVS : 0; + + rc = pmbus_lock_interruptible(client); + if (rc) + return rc; + + /* + * Writes to VOUT setpoint over AVSBus will persist after the VRM is + * switched to PMBus control. Switching back to AVSBus control + * restores this persisted setpoint rather than re-initializing to + * PMBus VOUT_COMMAND. Writing VOUT_COMMAND first over PMBus before + * enabling AVS control is the workaround. + */ + if (op_val == ISL68137_VOUT_AVS) { + rc = pmbus_read_word_data(client, page, 0xff, + PMBUS_VOUT_COMMAND); + if (rc < 0) + goto unlock; + + rc = pmbus_write_word_data(client, page, PMBUS_VOUT_COMMAND, + rc); + if (rc < 0) + goto unlock; + } + + rc = pmbus_update_byte_data(client, page, PMBUS_OPERATION, + ISL68137_VOUT_AVS, op_val); + +unlock: + pmbus_unlock(client); + + return (rc < 0) ? rc : count; +} + +static ssize_t isl68137_avs_enable_show(struct device *dev, + struct device_attribute *devattr, + char *buf) +{ + struct i2c_client *client = to_i2c_client(dev->parent); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + + return isl68137_avs_enable_show_page(client, attr->index, buf); +} + +static ssize_t isl68137_avs_enable_store(struct device *dev, + struct device_attribute *devattr, + const char *buf, size_t count) +{ + struct i2c_client *client = to_i2c_client(dev->parent); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + + return isl68137_avs_enable_store_page(client, attr->index, buf, count); +} + +static SENSOR_DEVICE_ATTR_RW(avs0_enable, isl68137_avs_enable, 0); +static SENSOR_DEVICE_ATTR_RW(avs1_enable, isl68137_avs_enable, 1); + +static struct attribute *enable_attrs[] = { + &sensor_dev_attr_avs0_enable.dev_attr.attr, + &sensor_dev_attr_avs1_enable.dev_attr.attr, + NULL, +}; + +static const struct attribute_group enable_group = { + .attrs = enable_attrs, +}; + +static const struct attribute_group *isl68137_attribute_groups[] = { + &enable_group, + NULL, +}; + +static int raa_dmpvr2_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct isl68137_data *data = to_isl68137_data(info); + int ret; + u64 temp; + + switch (reg) { + case PMBUS_VIRT_READ_VMON: + ret = pmbus_read_word_data(client, page, phase, + RAA_DMPVR2_READ_VMON); + break; + case PMBUS_READ_POUT: + case PMBUS_READ_VOUT: + /* + * In cases where a voltage divider is attached to the target + * rail between Vout and the Vsense pin, both Vout and Pout + * should be scaled by the voltage divider scaling factor. + * I.e. Vout = Vsense * Rtotal / Rout + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret > 0) { + temp = DIV_U64_ROUND_CLOSEST((u64)ret * + data->channel[page].vout_voltage_divider[1], + data->channel[page].vout_voltage_divider[0]); + ret = clamp_val(temp, 0, 0xffff); + } + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int raa_dmpvr2_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct isl68137_data *data = to_isl68137_data(info); + int ret; + u64 temp; + + switch (reg) { + case PMBUS_VOUT_MAX: + case PMBUS_VOUT_MARGIN_HIGH: + case PMBUS_VOUT_MARGIN_LOW: + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + case PMBUS_VOUT_COMMAND: + /* + * In cases where a voltage divider is attached to the target + * rail between Vout and the Vsense pin, Vout related PMBus + * commands should be scaled based on the expected voltage + * at the Vsense pin. + * I.e. Vsense = Vout * Rout / Rtotal + */ + temp = DIV_U64_ROUND_CLOSEST((u64)word * + data->channel[page].vout_voltage_divider[0], + data->channel[page].vout_voltage_divider[1]); + ret = clamp_val(temp, 0, 0xffff); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static struct pmbus_driver_info raa_dmpvr_info = { + .pages = 3, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 1, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 2, + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_CURRENT_IN] = 1, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = 2, + .m[PSC_CURRENT_OUT] = 1, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 1, + .m[PSC_POWER] = 1, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = 0, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 0, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | PMBUS_HAVE_PIN + | PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 + | PMBUS_HAVE_TEMP3 | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_POUT + | PMBUS_HAVE_VMON, + .func[1] = PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP3 | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_IOUT + | PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_POUT, + .func[2] = PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP3 | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_IOUT + | PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_POUT, +}; + +static int isl68137_probe_child_from_dt(struct device *dev, + struct device_node *child, + struct isl68137_data *data) +{ + u32 channel, rout, rtotal; + int err; + + err = of_property_read_u32(child, "reg", &channel); + if (err) { + dev_err(dev, "missing reg property of %pOFn\n", child); + return err; + } + if (channel >= data->info.pages) { + dev_err(dev, "invalid reg %d of %pOFn\n", channel, child); + return -EINVAL; + } + + err = of_property_read_u32_array(child, "vout-voltage-divider", + data->channel[channel].vout_voltage_divider, + ARRAY_SIZE(data->channel[channel].vout_voltage_divider)); + if (err && err != -EINVAL) { + dev_err(dev, + "malformed vout-voltage-divider value for channel %d\n", + channel); + return err; + } + + rout = data->channel[channel].vout_voltage_divider[0]; + rtotal = data->channel[channel].vout_voltage_divider[1]; + if (rout == 0) { + dev_err(dev, + "Voltage divider output resistance must be greater than 0\n"); + return -EINVAL; + } + if (rtotal < rout) { + dev_err(dev, + "Voltage divider total resistance is less than output resistance\n"); + return -EINVAL; + } + + return 0; +} + +static int isl68137_probe_from_dt(struct device *dev, + struct isl68137_data *data) +{ + const struct device_node *np = dev->of_node; + int err; + + for_each_child_of_node_scoped(np, child) { + if (strcmp(child->name, "channel")) + continue; + + err = isl68137_probe_child_from_dt(dev, child, data); + if (err) + return err; + } + + return 0; +} + +static int isl68137_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct pmbus_driver_info *info; + struct isl68137_data *data; + int i, err; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + /* + * Initialize all voltage dividers to Rout=1 and Rtotal=1 to simplify + * logic in PMBus word read/write functions + */ + for (i = 0; i < MAX_CHANNELS; i++) + memset(data->channel[i].vout_voltage_divider, + 1, + sizeof(data->channel[i].vout_voltage_divider)); + + memcpy(&data->info, &raa_dmpvr_info, sizeof(data->info)); + info = &data->info; + + switch ((uintptr_t)i2c_get_match_data(client)) { + case raa_dmpvr1_2rail: + info->pages = 2; + info->R[PSC_VOLTAGE_IN] = 3; + info->func[0] &= ~PMBUS_HAVE_VMON; + info->func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT; + info->read_word_data = raa_dmpvr2_read_word_data; + info->write_word_data = raa_dmpvr2_write_word_data; + info->groups = isl68137_attribute_groups; + break; + case raa_dmpvr2_1rail: + info->pages = 1; + info->read_word_data = raa_dmpvr2_read_word_data; + info->write_word_data = raa_dmpvr2_write_word_data; + break; + case raa_dmpvr2_2rail_nontc: + info->func[0] &= ~PMBUS_HAVE_TEMP3; + info->func[1] &= ~PMBUS_HAVE_TEMP3; + fallthrough; + case raa_dmpvr2_2rail: + info->pages = 2; + info->read_word_data = raa_dmpvr2_read_word_data; + info->write_word_data = raa_dmpvr2_write_word_data; + break; + case raa_dmpvr2_2rail_pmbus: + info->format[PSC_VOLTAGE_IN] = linear, + info->format[PSC_VOLTAGE_OUT] = linear, + info->format[PSC_CURRENT_IN] = linear; + info->format[PSC_CURRENT_OUT] = linear; + info->format[PSC_POWER] = linear; + info->format[PSC_TEMPERATURE] = linear; + info->pages = 2; + info->read_word_data = raa_dmpvr2_read_word_data; + info->write_word_data = raa_dmpvr2_write_word_data; + break; + case raa_dmpvr2_3rail: + info->read_word_data = raa_dmpvr2_read_word_data; + info->write_word_data = raa_dmpvr2_write_word_data; + break; + case raa_dmpvr2_hv: + info->pages = 1; + info->R[PSC_VOLTAGE_IN] = 1; + info->m[PSC_VOLTAGE_OUT] = 2; + info->R[PSC_VOLTAGE_OUT] = 2; + info->m[PSC_CURRENT_IN] = 2; + info->m[PSC_POWER] = 2; + info->R[PSC_POWER] = -1; + info->read_word_data = raa_dmpvr2_read_word_data; + info->write_word_data = raa_dmpvr2_write_word_data; + break; + default: + return -ENODEV; + } + + err = isl68137_probe_from_dt(dev, data); + if (err) + return err; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id raa_dmpvr_id[] = { + { .name = "isl68137", .driver_data = raa_dmpvr1_2rail }, + { .name = "isl68220", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl68221", .driver_data = raa_dmpvr2_3rail }, + { .name = "isl68222", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl68223", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl68224", .driver_data = raa_dmpvr2_3rail }, + { .name = "isl68225", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl68226", .driver_data = raa_dmpvr2_3rail }, + { .name = "isl68227", .driver_data = raa_dmpvr2_1rail }, + { .name = "isl68229", .driver_data = raa_dmpvr2_3rail }, + { .name = "isl68233", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl68239", .driver_data = raa_dmpvr2_3rail }, + + { .name = "isl69222", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69223", .driver_data = raa_dmpvr2_3rail }, + { .name = "isl69224", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69225", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69227", .driver_data = raa_dmpvr2_3rail }, + { .name = "isl69228", .driver_data = raa_dmpvr2_3rail }, + { .name = "isl69234", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69236", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69239", .driver_data = raa_dmpvr2_3rail }, + { .name = "isl69242", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69243", .driver_data = raa_dmpvr2_1rail }, + { .name = "isl69247", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69248", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69254", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69255", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69256", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69259", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69260", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69268", .driver_data = raa_dmpvr2_2rail }, + { .name = "isl69269", .driver_data = raa_dmpvr2_3rail }, + { .name = "isl69298", .driver_data = raa_dmpvr2_2rail }, + + { .name = "raa228000", .driver_data = raa_dmpvr2_hv }, + { .name = "raa228004", .driver_data = raa_dmpvr2_hv }, + { .name = "raa228006", .driver_data = raa_dmpvr2_hv }, + { .name = "raa228228", .driver_data = raa_dmpvr2_2rail_nontc }, + { .name = "raa228244", .driver_data = raa_dmpvr2_2rail_nontc }, + { .name = "raa228246", .driver_data = raa_dmpvr2_2rail_nontc }, + { .name = "raa228942", .driver_data = raa_dmpvr2_2rail_nontc }, + { .name = "raa228943", .driver_data = raa_dmpvr2_2rail_nontc }, + { .name = "raa229001", .driver_data = raa_dmpvr2_2rail }, + { .name = "raa229004", .driver_data = raa_dmpvr2_2rail }, + { .name = "raa229141", .driver_data = raa_dmpvr2_2rail_pmbus }, + { .name = "raa229621", .driver_data = raa_dmpvr2_2rail }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, raa_dmpvr_id); + +static const struct of_device_id isl68137_of_match[] = { + { .compatible = "isil,isl68137", .data = (void *)raa_dmpvr1_2rail }, + { .compatible = "renesas,isl68220", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl68221", .data = (void *)raa_dmpvr2_3rail }, + { .compatible = "renesas,isl68222", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl68223", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl68224", .data = (void *)raa_dmpvr2_3rail }, + { .compatible = "renesas,isl68225", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl68226", .data = (void *)raa_dmpvr2_3rail }, + { .compatible = "renesas,isl68227", .data = (void *)raa_dmpvr2_1rail }, + { .compatible = "renesas,isl68229", .data = (void *)raa_dmpvr2_3rail }, + { .compatible = "renesas,isl68233", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl68239", .data = (void *)raa_dmpvr2_3rail }, + + { .compatible = "renesas,isl69222", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69223", .data = (void *)raa_dmpvr2_3rail }, + { .compatible = "renesas,isl69224", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69225", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69227", .data = (void *)raa_dmpvr2_3rail }, + { .compatible = "renesas,isl69228", .data = (void *)raa_dmpvr2_3rail }, + { .compatible = "renesas,isl69234", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69236", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69239", .data = (void *)raa_dmpvr2_3rail }, + { .compatible = "renesas,isl69242", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69243", .data = (void *)raa_dmpvr2_1rail }, + { .compatible = "renesas,isl69247", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69248", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69254", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69255", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69256", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69259", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "isil,isl69260", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,isl69268", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "isil,isl69269", .data = (void *)raa_dmpvr2_3rail }, + { .compatible = "renesas,isl69298", .data = (void *)raa_dmpvr2_2rail }, + + { .compatible = "renesas,raa228000", .data = (void *)raa_dmpvr2_hv }, + { .compatible = "renesas,raa228004", .data = (void *)raa_dmpvr2_hv }, + { .compatible = "renesas,raa228006", .data = (void *)raa_dmpvr2_hv }, + { .compatible = "renesas,raa228228", .data = (void *)raa_dmpvr2_2rail_nontc }, + { .compatible = "renesas,raa228244", .data = (void *)raa_dmpvr2_2rail_nontc }, + { .compatible = "renesas,raa228246", .data = (void *)raa_dmpvr2_2rail_nontc }, + { .compatible = "renesas,raa229001", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,raa229004", .data = (void *)raa_dmpvr2_2rail }, + { .compatible = "renesas,raa229621", .data = (void *)raa_dmpvr2_2rail }, + { }, +}; + +MODULE_DEVICE_TABLE(of, isl68137_of_match); + +/* This is the driver that will be inserted */ +static struct i2c_driver isl68137_driver = { + .driver = { + .name = "isl68137", + .of_match_table = isl68137_of_match, + }, + .probe = isl68137_probe, + .id_table = raa_dmpvr_id, +}; + +module_i2c_driver(isl68137_driver); + +MODULE_AUTHOR("Maxim Sloyko <maxims@google.com>"); +MODULE_DESCRIPTION("PMBus driver for Renesas digital multiphase voltage regulators"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/lm25066.c b/drivers/hwmon/pmbus/lm25066.c new file mode 100644 index 000000000..497802c64 --- /dev/null +++ b/drivers/hwmon/pmbus/lm25066.c @@ -0,0 +1,612 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for LM25056 / LM25066 / LM5064 / LM5066 + * + * Copyright (c) 2011 Ericsson AB. + * Copyright (c) 2013 Guenter Roeck + */ + +#include <linux/bitops.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/log2.h> +#include <linux/math.h> +#include <linux/of.h> +#include "pmbus.h" + +enum chips { lm25056, lm25066, lm5064, lm5066, lm5066i }; + +#define LM25066_READ_VAUX 0xd0 +#define LM25066_MFR_READ_IIN 0xd1 +#define LM25066_MFR_READ_PIN 0xd2 +#define LM25066_MFR_IIN_OC_WARN_LIMIT 0xd3 +#define LM25066_MFR_PIN_OP_WARN_LIMIT 0xd4 +#define LM25066_READ_PIN_PEAK 0xd5 +#define LM25066_CLEAR_PIN_PEAK 0xd6 +#define LM25066_DEVICE_SETUP 0xd9 +#define LM25066_READ_AVG_VIN 0xdc +#define LM25066_SAMPLES_FOR_AVG 0xdb +#define LM25066_READ_AVG_VOUT 0xdd +#define LM25066_READ_AVG_IIN 0xde +#define LM25066_READ_AVG_PIN 0xdf + +#define LM25066_DEV_SETUP_CL BIT(4) /* Current limit */ +#define LM25066_DEV_SETUP_CL_CFG BIT(2) /* Current limit configuration */ + +#define LM25066_SAMPLES_FOR_AVG_MAX 4096 + +/* LM25056 only */ + +#define LM25056_VAUX_OV_WARN_LIMIT 0xe3 +#define LM25056_VAUX_UV_WARN_LIMIT 0xe4 + +#define LM25056_MFR_STS_VAUX_OV_WARN BIT(1) +#define LM25056_MFR_STS_VAUX_UV_WARN BIT(0) + +struct __coeff { + short m, b, R; +}; + +#define PSC_CURRENT_IN_L (PSC_NUM_CLASSES) +#define PSC_POWER_L (PSC_NUM_CLASSES + 1) + +static const struct __coeff lm25066_coeff[][PSC_NUM_CLASSES + 2] = { + [lm25056] = { + [PSC_VOLTAGE_IN] = { + .m = 16296, + .b = 1343, + .R = -2, + }, + [PSC_CURRENT_IN] = { + .m = 13797, + .b = -1833, + .R = -2, + }, + [PSC_CURRENT_IN_L] = { + .m = 6726, + .b = -537, + .R = -2, + }, + [PSC_POWER] = { + .m = 5501, + .b = -2908, + .R = -3, + }, + [PSC_POWER_L] = { + .m = 26882, + .b = -5646, + .R = -4, + }, + [PSC_TEMPERATURE] = { + .m = 1580, + .b = -14500, + .R = -2, + }, + }, + [lm25066] = { + [PSC_VOLTAGE_IN] = { + .m = 22070, + .b = -1800, + .R = -2, + }, + [PSC_VOLTAGE_OUT] = { + .m = 22070, + .b = -1800, + .R = -2, + }, + [PSC_CURRENT_IN] = { + .m = 13661, + .b = -5200, + .R = -2, + }, + [PSC_CURRENT_IN_L] = { + .m = 6854, + .b = -3100, + .R = -2, + }, + [PSC_POWER] = { + .m = 736, + .b = -3300, + .R = -2, + }, + [PSC_POWER_L] = { + .m = 369, + .b = -1900, + .R = -2, + }, + [PSC_TEMPERATURE] = { + .m = 16, + }, + }, + [lm5064] = { + [PSC_VOLTAGE_IN] = { + .m = 4611, + .b = -642, + .R = -2, + }, + [PSC_VOLTAGE_OUT] = { + .m = 4621, + .b = 423, + .R = -2, + }, + [PSC_CURRENT_IN] = { + .m = 5456, + .b = 2118, + .R = -2, + }, + [PSC_CURRENT_IN_L] = { + .m = 10742, + .b = 1552, + .R = -2, + }, + [PSC_POWER] = { + .m = 612, + .b = 11202, + .R = -3, + }, + [PSC_POWER_L] = { + .m = 1204, + .b = 8524, + .R = -3, + }, + [PSC_TEMPERATURE] = { + .m = 16, + }, + }, + [lm5066] = { + [PSC_VOLTAGE_IN] = { + .m = 4587, + .b = -1200, + .R = -2, + }, + [PSC_VOLTAGE_OUT] = { + .m = 4587, + .b = -2400, + .R = -2, + }, + [PSC_CURRENT_IN] = { + .m = 5405, + .b = -600, + .R = -2, + }, + [PSC_CURRENT_IN_L] = { + .m = 10753, + .b = -1200, + .R = -2, + }, + [PSC_POWER] = { + .m = 605, + .b = -8000, + .R = -3, + }, + [PSC_POWER_L] = { + .m = 1204, + .b = -6000, + .R = -3, + }, + [PSC_TEMPERATURE] = { + .m = 16, + }, + }, + [lm5066i] = { + [PSC_VOLTAGE_IN] = { + .m = 4617, + .b = -140, + .R = -2, + }, + [PSC_VOLTAGE_OUT] = { + .m = 4602, + .b = 500, + .R = -2, + }, + [PSC_CURRENT_IN] = { + .m = 7645, + .b = 100, + .R = -2, + }, + [PSC_CURRENT_IN_L] = { + .m = 15076, + .b = -504, + .R = -2, + }, + [PSC_POWER] = { + .m = 861, + .b = -965, + .R = -3, + }, + [PSC_POWER_L] = { + .m = 1701, + .b = -4000, + .R = -3, + }, + [PSC_TEMPERATURE] = { + .m = 16, + }, + }, +}; + +struct lm25066_data { + int id; + u16 rlimit; /* Maximum register value */ + struct pmbus_driver_info info; +}; + +#define to_lm25066_data(x) container_of(x, struct lm25066_data, info) + +static int lm25066_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct lm25066_data *data = to_lm25066_data(info); + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VMON: + ret = pmbus_read_word_data(client, 0, 0xff, LM25066_READ_VAUX); + if (ret < 0) + break; + /* Adjust returned value to match VIN coefficients */ + switch (data->id) { + case lm25056: + /* VIN: 6.14 mV VAUX: 293 uV LSB */ + ret = DIV_ROUND_CLOSEST(ret * 293, 6140); + break; + case lm25066: + /* VIN: 4.54 mV VAUX: 283.2 uV LSB */ + ret = DIV_ROUND_CLOSEST(ret * 2832, 45400); + break; + case lm5064: + /* VIN: 4.53 mV VAUX: 700 uV LSB */ + ret = DIV_ROUND_CLOSEST(ret * 70, 453); + break; + case lm5066: + case lm5066i: + /* VIN: 2.18 mV VAUX: 725 uV LSB */ + ret = DIV_ROUND_CLOSEST(ret * 725, 2180); + break; + } + break; + case PMBUS_READ_IIN: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25066_MFR_READ_IIN); + break; + case PMBUS_READ_PIN: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25066_MFR_READ_PIN); + break; + case PMBUS_IIN_OC_WARN_LIMIT: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25066_MFR_IIN_OC_WARN_LIMIT); + break; + case PMBUS_PIN_OP_WARN_LIMIT: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25066_MFR_PIN_OP_WARN_LIMIT); + break; + case PMBUS_VIRT_READ_VIN_AVG: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25066_READ_AVG_VIN); + break; + case PMBUS_VIRT_READ_VOUT_AVG: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25066_READ_AVG_VOUT); + break; + case PMBUS_VIRT_READ_IIN_AVG: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25066_READ_AVG_IIN); + break; + case PMBUS_VIRT_READ_PIN_AVG: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25066_READ_AVG_PIN); + break; + case PMBUS_VIRT_READ_PIN_MAX: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25066_READ_PIN_PEAK); + break; + case PMBUS_VIRT_RESET_PIN_HISTORY: + ret = 0; + break; + case PMBUS_VIRT_SAMPLES: + ret = pmbus_read_byte_data(client, 0, LM25066_SAMPLES_FOR_AVG); + if (ret < 0) + break; + ret = 1 << ret; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int lm25056_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_VMON_UV_WARN_LIMIT: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25056_VAUX_UV_WARN_LIMIT); + if (ret < 0) + break; + /* Adjust returned value to match VIN coefficients */ + ret = DIV_ROUND_CLOSEST(ret * 293, 6140); + break; + case PMBUS_VIRT_VMON_OV_WARN_LIMIT: + ret = pmbus_read_word_data(client, 0, 0xff, + LM25056_VAUX_OV_WARN_LIMIT); + if (ret < 0) + break; + /* Adjust returned value to match VIN coefficients */ + ret = DIV_ROUND_CLOSEST(ret * 293, 6140); + break; + default: + ret = lm25066_read_word_data(client, page, phase, reg); + break; + } + return ret; +} + +static int lm25056_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret, s; + + switch (reg) { + case PMBUS_VIRT_STATUS_VMON: + ret = pmbus_read_byte_data(client, 0, + PMBUS_STATUS_MFR_SPECIFIC); + if (ret < 0) + break; + s = 0; + if (ret & LM25056_MFR_STS_VAUX_UV_WARN) + s |= PB_VOLTAGE_UV_WARNING; + if (ret & LM25056_MFR_STS_VAUX_OV_WARN) + s |= PB_VOLTAGE_OV_WARNING; + ret = s; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int lm25066_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct lm25066_data *data = to_lm25066_data(info); + int ret; + + switch (reg) { + case PMBUS_POUT_OP_FAULT_LIMIT: + case PMBUS_POUT_OP_WARN_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + case PMBUS_IIN_OC_FAULT_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + word = ((s16)word < 0) ? 0 : clamp_val(word, 0, data->rlimit); + ret = pmbus_write_word_data(client, 0, reg, word); + break; + case PMBUS_IIN_OC_WARN_LIMIT: + word = ((s16)word < 0) ? 0 : clamp_val(word, 0, data->rlimit); + ret = pmbus_write_word_data(client, 0, + LM25066_MFR_IIN_OC_WARN_LIMIT, + word); + break; + case PMBUS_PIN_OP_WARN_LIMIT: + word = ((s16)word < 0) ? 0 : clamp_val(word, 0, data->rlimit); + ret = pmbus_write_word_data(client, 0, + LM25066_MFR_PIN_OP_WARN_LIMIT, + word); + break; + case PMBUS_VIRT_VMON_UV_WARN_LIMIT: + /* Adjust from VIN coefficients (for LM25056) */ + word = DIV_ROUND_CLOSEST((int)word * 6140, 293); + word = ((s16)word < 0) ? 0 : clamp_val(word, 0, data->rlimit); + ret = pmbus_write_word_data(client, 0, + LM25056_VAUX_UV_WARN_LIMIT, word); + break; + case PMBUS_VIRT_VMON_OV_WARN_LIMIT: + /* Adjust from VIN coefficients (for LM25056) */ + word = DIV_ROUND_CLOSEST((int)word * 6140, 293); + word = ((s16)word < 0) ? 0 : clamp_val(word, 0, data->rlimit); + ret = pmbus_write_word_data(client, 0, + LM25056_VAUX_OV_WARN_LIMIT, word); + break; + case PMBUS_VIRT_RESET_PIN_HISTORY: + ret = pmbus_write_byte(client, 0, LM25066_CLEAR_PIN_PEAK); + break; + case PMBUS_VIRT_SAMPLES: + word = clamp_val(word, 1, LM25066_SAMPLES_FOR_AVG_MAX); + ret = pmbus_write_byte_data(client, 0, LM25066_SAMPLES_FOR_AVG, + ilog2(word)); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +#if IS_ENABLED(CONFIG_SENSORS_LM25066_REGULATOR) +static const struct regulator_desc lm25066_reg_desc[] = { + PMBUS_REGULATOR_ONE_NODE("vout"), +}; +#endif + +static const struct i2c_device_id lm25066_id[] = { + { .name = "lm25056", .driver_data = lm25056 }, + { .name = "lm25066", .driver_data = lm25066 }, + { .name = "lm5064", .driver_data = lm5064 }, + { .name = "lm5066", .driver_data = lm5066 }, + { .name = "lm5066i", .driver_data = lm5066i }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lm25066_id); + +static const struct of_device_id __maybe_unused lm25066_of_match[] = { + { .compatible = "ti,lm25056", .data = (void *)lm25056, }, + { .compatible = "ti,lm25066", .data = (void *)lm25066, }, + { .compatible = "ti,lm5064", .data = (void *)lm5064, }, + { .compatible = "ti,lm5066", .data = (void *)lm5066, }, + { .compatible = "ti,lm5066i", .data = (void *)lm5066i, }, + { }, +}; +MODULE_DEVICE_TABLE(of, lm25066_of_match); + +static int lm25066_probe(struct i2c_client *client) +{ + int config; + u32 shunt; + struct lm25066_data *data; + struct pmbus_driver_info *info; + const struct __coeff *coeff; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA)) + return -ENODEV; + + data = devm_kzalloc(&client->dev, sizeof(struct lm25066_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + config = i2c_smbus_read_byte_data(client, LM25066_DEVICE_SETUP); + if (config < 0) + return config; + + data->id = (enum chips)(unsigned long)i2c_get_match_data(client); + + if (data->id != lm25056) { + int config_new = config; + const char *cl_setting; + int ret; + + if (!of_property_read_string(client->dev.of_node, + "ti,current-range", &cl_setting)) { + config_new |= LM25066_DEV_SETUP_CL_CFG; + if (strcmp(cl_setting, "high") == 0) { + if (data->id == lm25066) + config_new |= LM25066_DEV_SETUP_CL; + else + config_new &= ~LM25066_DEV_SETUP_CL; + } else if (strcmp(cl_setting, "low") == 0) { + if (data->id == lm25066) + config_new &= ~LM25066_DEV_SETUP_CL; + else + config_new |= LM25066_DEV_SETUP_CL; + } else { + dev_err(&client->dev, + "invalid current-range setting: %s\n", + cl_setting); + return -EINVAL; + } + } + + if (config_new != config) { + ret = i2c_smbus_write_byte_data(client, + LM25066_DEVICE_SETUP, + config_new); + if (ret < 0) + return ret; + config = config_new; + } + } + + info = &data->info; + + info->pages = 1; + info->format[PSC_VOLTAGE_IN] = direct; + info->format[PSC_VOLTAGE_OUT] = direct; + info->format[PSC_CURRENT_IN] = direct; + info->format[PSC_TEMPERATURE] = direct; + info->format[PSC_POWER] = direct; + + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VMON + | PMBUS_HAVE_PIN | PMBUS_HAVE_IIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | PMBUS_HAVE_SAMPLES; + + if (data->id == lm25056) { + info->func[0] |= PMBUS_HAVE_STATUS_VMON; + info->read_word_data = lm25056_read_word_data; + info->read_byte_data = lm25056_read_byte_data; + data->rlimit = 0x0fff; + } else { + info->func[0] |= PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT; + info->read_word_data = lm25066_read_word_data; + data->rlimit = 0x0fff; + } + info->write_word_data = lm25066_write_word_data; + + coeff = &lm25066_coeff[data->id][0]; + info->m[PSC_TEMPERATURE] = coeff[PSC_TEMPERATURE].m; + info->b[PSC_TEMPERATURE] = coeff[PSC_TEMPERATURE].b; + info->R[PSC_TEMPERATURE] = coeff[PSC_TEMPERATURE].R; + info->m[PSC_VOLTAGE_IN] = coeff[PSC_VOLTAGE_IN].m; + info->b[PSC_VOLTAGE_IN] = coeff[PSC_VOLTAGE_IN].b; + info->R[PSC_VOLTAGE_IN] = coeff[PSC_VOLTAGE_IN].R; + info->m[PSC_VOLTAGE_OUT] = coeff[PSC_VOLTAGE_OUT].m; + info->b[PSC_VOLTAGE_OUT] = coeff[PSC_VOLTAGE_OUT].b; + info->R[PSC_VOLTAGE_OUT] = coeff[PSC_VOLTAGE_OUT].R; + if (config & LM25066_DEV_SETUP_CL) { + info->m[PSC_CURRENT_IN] = coeff[PSC_CURRENT_IN_L].m; + info->b[PSC_CURRENT_IN] = coeff[PSC_CURRENT_IN_L].b; + info->R[PSC_CURRENT_IN] = coeff[PSC_CURRENT_IN_L].R; + info->m[PSC_POWER] = coeff[PSC_POWER_L].m; + info->b[PSC_POWER] = coeff[PSC_POWER_L].b; + info->R[PSC_POWER] = coeff[PSC_POWER_L].R; + } else { + info->m[PSC_CURRENT_IN] = coeff[PSC_CURRENT_IN].m; + info->b[PSC_CURRENT_IN] = coeff[PSC_CURRENT_IN].b; + info->R[PSC_CURRENT_IN] = coeff[PSC_CURRENT_IN].R; + info->m[PSC_POWER] = coeff[PSC_POWER].m; + info->b[PSC_POWER] = coeff[PSC_POWER].b; + info->R[PSC_POWER] = coeff[PSC_POWER].R; + } + + /* + * Values in the TI datasheets are normalized for a 1mOhm sense + * resistor; assume that unless DT specifies a value explicitly. + */ + if (of_property_read_u32(client->dev.of_node, "shunt-resistor-micro-ohms", &shunt)) + shunt = 1000; + + info->m[PSC_CURRENT_IN] = DIV_ROUND_CLOSEST_ULL((u64)info->m[PSC_CURRENT_IN] * shunt, 1000); + info->m[PSC_POWER] = DIV_ROUND_CLOSEST_ULL((u64)info->m[PSC_POWER] * shunt, 1000); + +#if IS_ENABLED(CONFIG_SENSORS_LM25066_REGULATOR) + /* LM25056 doesn't support OPERATION */ + if (data->id != lm25056) { + info->num_regulators = ARRAY_SIZE(lm25066_reg_desc); + info->reg_desc = lm25066_reg_desc; + } +#endif + + return pmbus_do_probe(client, info); +} + +/* This is the driver that will be inserted */ +static struct i2c_driver lm25066_driver = { + .driver = { + .name = "lm25066", + .of_match_table = of_match_ptr(lm25066_of_match), + }, + .probe = lm25066_probe, + .id_table = lm25066_id, +}; + +module_i2c_driver(lm25066_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for LM25066 and compatible chips"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/lt3074.c b/drivers/hwmon/pmbus/lt3074.c new file mode 100644 index 000000000..a7f9edf02 --- /dev/null +++ b/drivers/hwmon/pmbus/lt3074.c @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Hardware monitoring driver for Analog Devices LT3074 + * + * Copyright (C) 2025 Analog Devices, Inc. + */ +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/module.h> + +#include "pmbus.h" + +#define LT3074_MFR_READ_VBIAS 0xc6 +#define LT3074_MFR_BIAS_OV_WARN_LIMIT 0xc7 +#define LT3074_MFR_BIAS_UV_WARN_LIMIT 0xc8 +#define LT3074_MFR_SPECIAL_ID 0xe7 + +#define LT3074_SPECIAL_ID_VALUE 0x1c1d + +static const struct regulator_desc __maybe_unused lt3074_reg_desc[] = { + PMBUS_REGULATOR_ONE("regulator"), +}; + +static int lt3074_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + switch (reg) { + case PMBUS_VIRT_READ_VMON: + return pmbus_read_word_data(client, page, phase, + LT3074_MFR_READ_VBIAS); + case PMBUS_VIRT_VMON_UV_WARN_LIMIT: + return pmbus_read_word_data(client, page, phase, + LT3074_MFR_BIAS_UV_WARN_LIMIT); + case PMBUS_VIRT_VMON_OV_WARN_LIMIT: + return pmbus_read_word_data(client, page, phase, + LT3074_MFR_BIAS_OV_WARN_LIMIT); + default: + return -ENODATA; + } +} + +static int lt3074_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + switch (reg) { + case PMBUS_VIRT_VMON_UV_WARN_LIMIT: + return pmbus_write_word_data(client, 0, + LT3074_MFR_BIAS_UV_WARN_LIMIT, + word); + case PMBUS_VIRT_VMON_OV_WARN_LIMIT: + return pmbus_write_word_data(client, 0, + LT3074_MFR_BIAS_OV_WARN_LIMIT, + word); + default: + return -ENODATA; + } +} + +static struct pmbus_driver_info lt3074_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_VMON | + PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, + .read_word_data = lt3074_read_word_data, + .write_word_data = lt3074_write_word_data, +#if IS_ENABLED(CONFIG_SENSORS_LT3074_REGULATOR) + .num_regulators = 1, + .reg_desc = lt3074_reg_desc, +#endif +}; + +static int lt3074_probe(struct i2c_client *client) +{ + int ret; + struct device *dev = &client->dev; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_WORD_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_word_data(client, LT3074_MFR_SPECIAL_ID); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to read ID\n"); + + if (ret != LT3074_SPECIAL_ID_VALUE) + return dev_err_probe(dev, -ENODEV, "ID mismatch\n"); + + return pmbus_do_probe(client, <3074_info); +} + +static const struct i2c_device_id lt3074_id[] = { + { .name = "lt3074" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lt3074_id); + +static const struct of_device_id __maybe_unused lt3074_of_match[] = { + { .compatible = "adi,lt3074" }, + {} +}; +MODULE_DEVICE_TABLE(of, lt3074_of_match); + +static struct i2c_driver lt3074_driver = { + .driver = { + .name = "lt3074", + .of_match_table = of_match_ptr(lt3074_of_match), + }, + .probe = lt3074_probe, + .id_table = lt3074_id, +}; +module_i2c_driver(lt3074_driver); + +MODULE_AUTHOR("Cedric Encarnacion <cedricjustine.encarnacion@analog.com>"); +MODULE_DESCRIPTION("PMBus driver for Analog Devices LT3074"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/lt7182s.c b/drivers/hwmon/pmbus/lt7182s.c new file mode 100644 index 000000000..f8971a786 --- /dev/null +++ b/drivers/hwmon/pmbus/lt7182s.c @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Analog Devices LT7182S + * + * Copyright (c) 2022 Guenter Roeck + * + */ + +#include <linux/bits.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include "pmbus.h" + +#define LT7182S_NUM_PAGES 2 + +#define MFR_READ_EXTVCC 0xcd +#define MFR_READ_ITH 0xce +#define MFR_CONFIG_ALL_LT7182S 0xd1 +#define MFR_IOUT_PEAK 0xd7 +#define MFR_ADC_CONTROL_LT7182S 0xd8 + +#define MFR_DEBUG_TELEMETRY BIT(0) + +#define MFR_VOUT_PEAK 0xdd +#define MFR_VIN_PEAK 0xde +#define MFR_TEMPERATURE_1_PEAK 0xdf +#define MFR_CLEAR_PEAKS 0xe3 + +#define MFR_CONFIG_IEEE BIT(8) + +static int lt7182s_read_word_data(struct i2c_client *client, int page, int phase, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VMON: + if (page == 0 || page == 1) + ret = pmbus_read_word_data(client, page, phase, MFR_READ_ITH); + else + ret = pmbus_read_word_data(client, 0, phase, MFR_READ_EXTVCC); + break; + case PMBUS_VIRT_READ_IOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, MFR_IOUT_PEAK); + break; + case PMBUS_VIRT_READ_VOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, MFR_VOUT_PEAK); + break; + case PMBUS_VIRT_READ_VIN_MAX: + ret = pmbus_read_word_data(client, page, phase, MFR_VIN_PEAK); + break; + case PMBUS_VIRT_READ_TEMP_MAX: + ret = pmbus_read_word_data(client, page, phase, MFR_TEMPERATURE_1_PEAK); + break; + case PMBUS_VIRT_RESET_VIN_HISTORY: + ret = (page == 0) ? 0 : -ENODATA; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int lt7182s_write_word_data(struct i2c_client *client, int page, int reg, u16 word) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_RESET_VIN_HISTORY: + ret = pmbus_write_byte(client, 0, MFR_CLEAR_PEAKS); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static struct pmbus_driver_info lt7182s_info = { + .pages = LT7182S_NUM_PAGES, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_POWER] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, + .func[1] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | + PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT, + .read_word_data = lt7182s_read_word_data, + .write_word_data = lt7182s_write_word_data, +}; + +static int lt7182s_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct pmbus_driver_info *info; + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + int ret; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA | + I2C_FUNC_SMBUS_READ_WORD_DATA | + I2C_FUNC_SMBUS_READ_BLOCK_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, buf); + if (ret < 0) { + dev_err(dev, "Failed to read PMBUS_MFR_ID\n"); + return ret; + } + if (ret != 3 || strncmp(buf, "ADI", 3)) { + buf[ret] = '\0'; + dev_err(dev, "Manufacturer '%s' not supported\n", buf); + return -ENODEV; + } + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) { + dev_err(dev, "Failed to read PMBUS_MFR_MODEL\n"); + return ret; + } + if (ret != 7 || strncmp(buf, "LT7182S", 7)) { + buf[ret] = '\0'; + dev_err(dev, "Model '%s' not supported\n", buf); + return -ENODEV; + } + + info = devm_kmemdup(dev, <7182s_info, + sizeof(struct pmbus_driver_info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + /* Set data format to IEEE754 if configured */ + ret = i2c_smbus_read_word_data(client, MFR_CONFIG_ALL_LT7182S); + if (ret < 0) + return ret; + if (ret & MFR_CONFIG_IEEE) { + info->format[PSC_VOLTAGE_IN] = ieee754; + info->format[PSC_VOLTAGE_OUT] = ieee754; + info->format[PSC_CURRENT_IN] = ieee754; + info->format[PSC_CURRENT_OUT] = ieee754; + info->format[PSC_TEMPERATURE] = ieee754; + info->format[PSC_POWER] = ieee754; + } + + /* Enable VMON output if configured */ + ret = i2c_smbus_read_byte_data(client, MFR_ADC_CONTROL_LT7182S); + if (ret < 0) + return ret; + if (ret & MFR_DEBUG_TELEMETRY) { + info->pages = 3; + info->func[0] |= PMBUS_HAVE_VMON; + info->func[1] |= PMBUS_HAVE_VMON; + info->func[2] = PMBUS_HAVE_VMON; + } + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id lt7182s_id[] = { + { .name = "lt7182s" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, lt7182s_id); + +static const struct of_device_id __maybe_unused lt7182s_of_match[] = { + { .compatible = "adi,lt7182s" }, + {} +}; + +static struct i2c_driver lt7182s_driver = { + .driver = { + .name = "lt7182s", + .of_match_table = of_match_ptr(lt7182s_of_match), + }, + .probe = lt7182s_probe, + .id_table = lt7182s_id, +}; + +module_i2c_driver(lt7182s_driver); + +MODULE_AUTHOR("Guenter Roeck <linux@roeck-us.net>"); +MODULE_DESCRIPTION("PMBus driver for Analog Devices LT7182S"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ltc2978.c b/drivers/hwmon/pmbus/ltc2978.c new file mode 100644 index 000000000..aea7b6c93 --- /dev/null +++ b/drivers/hwmon/pmbus/ltc2978.c @@ -0,0 +1,1011 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for LTC2978 and compatible chips. + * + * Copyright (c) 2011 Ericsson AB. + * Copyright (c) 2013, 2014, 2015 Guenter Roeck + * Copyright (c) 2015 Linear Technology + * Copyright (c) 2018 Analog Devices Inc. + */ + +#include <linux/delay.h> +#include <linux/jiffies.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/regulator/driver.h> +#include "pmbus.h" + +enum chips { + /* Managers */ + ltc2972, ltc2974, ltc2975, ltc2977, ltc2978, ltc2979, ltc2980, + /* Controllers */ + lt7170, lt7171, ltc3880, ltc3882, ltc3883, ltc3884, ltc3886, ltc3887, + ltc3889, ltc7132, ltc7841, ltc7880, + /* Modules */ + ltm2987, ltm4664, ltm4673, ltm4675, ltm4676, ltm4677, ltm4678, ltm4680, + ltm4686, ltm4700, +}; + +/* Common for all chips */ +#define LTC2978_MFR_VOUT_PEAK 0xdd +#define LTC2978_MFR_VIN_PEAK 0xde +#define LTC2978_MFR_TEMPERATURE_PEAK 0xdf +#define LTC2978_MFR_SPECIAL_ID 0xe7 /* Undocumented on LTC3882 */ +#define LTC2978_MFR_COMMON 0xef + +/* LTC2974, LTC2975, LCT2977, LTC2980, LTC2978, and LTM2987 */ +#define LTC2978_MFR_VOUT_MIN 0xfb +#define LTC2978_MFR_VIN_MIN 0xfc +#define LTC2978_MFR_TEMPERATURE_MIN 0xfd + +/* LTC2974, LTC2975 */ +#define LTC2974_MFR_IOUT_PEAK 0xd7 +#define LTC2974_MFR_IOUT_MIN 0xd8 + +/* LTC3880, LTC3882, LTC3883, LTC3887, LTM4675, LTM4676, LTC7132 */ +#define LTC3880_MFR_IOUT_PEAK 0xd7 +#define LTC3880_MFR_CLEAR_PEAKS 0xe3 +#define LTC3880_MFR_TEMPERATURE2_PEAK 0xf4 + +/* LTC3883, LTC3884, LTC3886, LTC3889, LTC7132, LTC7841 and LTC7880 only */ +#define LTC3883_MFR_IIN_PEAK 0xe1 + +/* LTC2975 only */ +#define LTC2975_MFR_IIN_PEAK 0xc4 +#define LTC2975_MFR_IIN_MIN 0xc5 +#define LTC2975_MFR_PIN_PEAK 0xc6 +#define LTC2975_MFR_PIN_MIN 0xc7 + +#define LTC2978_ID_MASK 0xfff0 + +#define LT7170_ID 0x1C10 +#define LTC2972_ID 0x0310 +#define LTC2974_ID 0x0210 +#define LTC2975_ID 0x0220 +#define LTC2977_ID 0x0130 +#define LTC2978_ID_REV1 0x0110 /* Early revision */ +#define LTC2978_ID_REV2 0x0120 +#define LTC2979_ID_A 0x8060 +#define LTC2979_ID_B 0x8070 +#define LTC2980_ID_A 0x8030 /* A/B for two die IDs */ +#define LTC2980_ID_B 0x8040 +#define LTC3880_ID 0x4020 +#define LTC3882_ID 0x4200 +#define LTC3882_ID_D1 0x4240 /* Dash 1 */ +#define LTC3883_ID 0x4300 +#define LTC3884_ID 0x4C00 +#define LTC3886_ID 0x4600 +#define LTC3887_ID 0x4700 +#define LTC3889_ID 0x4900 +#define LTC7132_ID 0x4CE0 +#define LTC7841_ID 0x40D0 +#define LTC7880_ID 0x49E0 +#define LTM2987_ID_A 0x8010 /* A/B for two die IDs */ +#define LTM2987_ID_B 0x8020 +#define LTM4664_ID 0x4120 +#define LTM4673_ID_REV1 0x0230 +#define LTM4673_ID 0x4480 +#define LTM4675_ID 0x47a0 +#define LTM4676_ID_REV1 0x4400 +#define LTM4676_ID_REV2 0x4480 +#define LTM4676A_ID 0x47e0 +#define LTM4677_ID_REV1 0x47B0 +#define LTM4677_ID_REV2 0x47D0 +#define LTM4678_ID_REV1 0x4100 +#define LTM4678_ID_REV2 0x4110 +#define LTM4680_ID 0x4140 +#define LTM4686_ID 0x4770 +#define LTM4700_ID 0x4130 + +#define LTC2972_NUM_PAGES 2 +#define LTC2974_NUM_PAGES 4 +#define LTC2978_NUM_PAGES 8 +#define LTC3880_NUM_PAGES 2 +#define LTC3883_NUM_PAGES 1 + +#define LTC_POLL_TIMEOUT 100 /* in milli-seconds */ + +#define LTC_NOT_BUSY BIT(6) +#define LTC_NOT_PENDING BIT(5) + +/* + * LTC2978 clears peak data whenever the CLEAR_FAULTS command is executed, which + * happens pretty much each time chip data is updated. Raw peak data therefore + * does not provide much value. To be able to provide useful peak data, keep an + * internal cache of measured peak data, which is only cleared if an explicit + * "clear peak" command is executed for the sensor in question. + */ + +struct ltc2978_data { + enum chips id; + u16 vin_min, vin_max; + u16 temp_min[LTC2974_NUM_PAGES], temp_max[LTC2974_NUM_PAGES]; + u16 vout_min[LTC2978_NUM_PAGES], vout_max[LTC2978_NUM_PAGES]; + u16 iout_min[LTC2974_NUM_PAGES], iout_max[LTC2974_NUM_PAGES]; + u16 iin_min, iin_max; + u16 pin_min, pin_max; + u16 temp2_max; + struct pmbus_driver_info info; + u32 features; +}; +#define to_ltc2978_data(x) container_of(x, struct ltc2978_data, info) + +#define FEAT_CLEAR_PEAKS BIT(0) +#define FEAT_NEEDS_POLLING BIT(1) + +#define has_clear_peaks(d) ((d)->features & FEAT_CLEAR_PEAKS) +#define needs_polling(d) ((d)->features & FEAT_NEEDS_POLLING) + +static int ltc_wait_ready(struct i2c_client *client) +{ + unsigned long timeout = jiffies + msecs_to_jiffies(LTC_POLL_TIMEOUT); + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct ltc2978_data *data = to_ltc2978_data(info); + int status; + u8 mask; + + if (!needs_polling(data)) + return 0; + + /* + * LTC3883 does not support LTC_NOT_PENDING, even though + * the datasheet claims that it does. + */ + mask = LTC_NOT_BUSY; + if (data->id != ltc3883) + mask |= LTC_NOT_PENDING; + + do { + status = pmbus_read_byte_data(client, 0, LTC2978_MFR_COMMON); + if (status == -EBADMSG || status == -ENXIO) { + /* PEC error or NACK: chip may be busy, try again */ + usleep_range(50, 100); + continue; + } + if (status < 0) + return status; + + if ((status & mask) == mask) + return 0; + + usleep_range(50, 100); + } while (time_before(jiffies, timeout)); + + return -ETIMEDOUT; +} + +static int ltc_read_word_data(struct i2c_client *client, int page, int phase, + int reg) +{ + int ret; + + ret = ltc_wait_ready(client); + if (ret < 0) + return ret; + + return pmbus_read_word_data(client, page, 0xff, reg); +} + +static int ltc_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + ret = ltc_wait_ready(client); + if (ret < 0) + return ret; + + return pmbus_read_byte_data(client, page, reg); +} + +static int ltc_write_byte_data(struct i2c_client *client, int page, int reg, u8 value) +{ + int ret; + + ret = ltc_wait_ready(client); + if (ret < 0) + return ret; + + return pmbus_write_byte_data(client, page, reg, value); +} + +static int ltc_write_byte(struct i2c_client *client, int page, u8 byte) +{ + int ret; + + ret = ltc_wait_ready(client); + if (ret < 0) + return ret; + + return pmbus_write_byte(client, page, byte); +} + +static inline int lin11_to_val(int data) +{ + s16 e = ((s16)data) >> 11; + s32 m = (((s16)(data << 5)) >> 5); + + /* + * mantissa is 10 bit + sign, exponent adds up to 15 bit. + * Add 6 bit to exponent for maximum accuracy (10 + 15 + 6 = 31). + */ + e += 6; + return (e < 0 ? m >> -e : m << e); +} + +static int ltc_get_max(struct ltc2978_data *data, struct i2c_client *client, + int page, int reg, u16 *pmax) +{ + int ret; + + ret = ltc_read_word_data(client, page, 0xff, reg); + if (ret >= 0) { + if (lin11_to_val(ret) > lin11_to_val(*pmax)) + *pmax = ret; + ret = *pmax; + } + return ret; +} + +static int ltc_get_min(struct ltc2978_data *data, struct i2c_client *client, + int page, int reg, u16 *pmin) +{ + int ret; + + ret = ltc_read_word_data(client, page, 0xff, reg); + if (ret >= 0) { + if (lin11_to_val(ret) < lin11_to_val(*pmin)) + *pmin = ret; + ret = *pmin; + } + return ret; +} + +static int ltc2978_read_word_data_common(struct i2c_client *client, int page, + int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct ltc2978_data *data = to_ltc2978_data(info); + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VIN_MAX: + ret = ltc_get_max(data, client, page, LTC2978_MFR_VIN_PEAK, + &data->vin_max); + break; + case PMBUS_VIRT_READ_VOUT_MAX: + ret = ltc_read_word_data(client, page, 0xff, + LTC2978_MFR_VOUT_PEAK); + if (ret >= 0) { + /* + * VOUT is 16 bit unsigned with fixed exponent, + * so we can compare it directly + */ + if (ret > data->vout_max[page]) + data->vout_max[page] = ret; + ret = data->vout_max[page]; + } + break; + case PMBUS_VIRT_READ_TEMP_MAX: + ret = ltc_get_max(data, client, page, + LTC2978_MFR_TEMPERATURE_PEAK, + &data->temp_max[page]); + break; + case PMBUS_VIRT_RESET_VOUT_HISTORY: + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_TEMP_HISTORY: + ret = 0; + break; + default: + ret = ltc_wait_ready(client); + if (ret < 0) + return ret; + ret = -ENODATA; + break; + } + return ret; +} + +static int ltc2978_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct ltc2978_data *data = to_ltc2978_data(info); + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VIN_MIN: + ret = ltc_get_min(data, client, page, LTC2978_MFR_VIN_MIN, + &data->vin_min); + break; + case PMBUS_VIRT_READ_VOUT_MIN: + ret = ltc_read_word_data(client, page, phase, + LTC2978_MFR_VOUT_MIN); + if (ret >= 0) { + /* + * VOUT_MIN is known to not be supported on some lots + * of LTC2978 revision 1, and will return the maximum + * possible voltage if read. If VOUT_MAX is valid and + * lower than the reading of VOUT_MIN, use it instead. + */ + if (data->vout_max[page] && ret > data->vout_max[page]) + ret = data->vout_max[page]; + if (ret < data->vout_min[page]) + data->vout_min[page] = ret; + ret = data->vout_min[page]; + } + break; + case PMBUS_VIRT_READ_TEMP_MIN: + ret = ltc_get_min(data, client, page, + LTC2978_MFR_TEMPERATURE_MIN, + &data->temp_min[page]); + break; + case PMBUS_VIRT_READ_IOUT_MAX: + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_READ_TEMP2_MAX: + case PMBUS_VIRT_RESET_TEMP2_HISTORY: + ret = -ENXIO; + break; + default: + ret = ltc2978_read_word_data_common(client, page, reg); + break; + } + return ret; +} + +static int ltc2974_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct ltc2978_data *data = to_ltc2978_data(info); + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_IOUT_MAX: + ret = ltc_get_max(data, client, page, LTC2974_MFR_IOUT_PEAK, + &data->iout_max[page]); + break; + case PMBUS_VIRT_READ_IOUT_MIN: + ret = ltc_get_min(data, client, page, LTC2974_MFR_IOUT_MIN, + &data->iout_min[page]); + break; + case PMBUS_VIRT_RESET_IOUT_HISTORY: + ret = 0; + break; + default: + ret = ltc2978_read_word_data(client, page, phase, reg); + break; + } + return ret; +} + +static int ltc2975_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct ltc2978_data *data = to_ltc2978_data(info); + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_IIN_MAX: + ret = ltc_get_max(data, client, page, LTC2975_MFR_IIN_PEAK, + &data->iin_max); + break; + case PMBUS_VIRT_READ_IIN_MIN: + ret = ltc_get_min(data, client, page, LTC2975_MFR_IIN_MIN, + &data->iin_min); + break; + case PMBUS_VIRT_READ_PIN_MAX: + ret = ltc_get_max(data, client, page, LTC2975_MFR_PIN_PEAK, + &data->pin_max); + break; + case PMBUS_VIRT_READ_PIN_MIN: + ret = ltc_get_min(data, client, page, LTC2975_MFR_PIN_MIN, + &data->pin_min); + break; + case PMBUS_VIRT_RESET_IIN_HISTORY: + case PMBUS_VIRT_RESET_PIN_HISTORY: + ret = 0; + break; + default: + ret = ltc2978_read_word_data(client, page, phase, reg); + break; + } + return ret; +} + +static int ltc3880_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct ltc2978_data *data = to_ltc2978_data(info); + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_IOUT_MAX: + ret = ltc_get_max(data, client, page, LTC3880_MFR_IOUT_PEAK, + &data->iout_max[page]); + break; + case PMBUS_VIRT_READ_TEMP2_MAX: + ret = ltc_get_max(data, client, page, + LTC3880_MFR_TEMPERATURE2_PEAK, + &data->temp2_max); + break; + case PMBUS_VIRT_READ_VIN_MIN: + case PMBUS_VIRT_READ_VOUT_MIN: + case PMBUS_VIRT_READ_TEMP_MIN: + ret = -ENXIO; + break; + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_RESET_TEMP2_HISTORY: + ret = 0; + break; + default: + ret = ltc2978_read_word_data_common(client, page, reg); + break; + } + return ret; +} + +static int ltc3883_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct ltc2978_data *data = to_ltc2978_data(info); + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_IIN_MAX: + ret = ltc_get_max(data, client, page, LTC3883_MFR_IIN_PEAK, + &data->iin_max); + break; + case PMBUS_VIRT_RESET_IIN_HISTORY: + ret = 0; + break; + default: + ret = ltc3880_read_word_data(client, page, phase, reg); + break; + } + return ret; +} + +static int ltc2978_clear_peaks(struct ltc2978_data *data, + struct i2c_client *client, int page) +{ + int ret; + + if (has_clear_peaks(data)) + ret = ltc_write_byte(client, 0, LTC3880_MFR_CLEAR_PEAKS); + else + ret = ltc_write_byte(client, page, PMBUS_CLEAR_FAULTS); + + return ret; +} + +static int ltc2978_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct ltc2978_data *data = to_ltc2978_data(info); + int ret; + + switch (reg) { + case PMBUS_VIRT_RESET_IIN_HISTORY: + data->iin_max = 0x7c00; + data->iin_min = 0x7bff; + ret = ltc2978_clear_peaks(data, client, 0); + break; + case PMBUS_VIRT_RESET_PIN_HISTORY: + data->pin_max = 0x7c00; + data->pin_min = 0x7bff; + ret = ltc2978_clear_peaks(data, client, 0); + break; + case PMBUS_VIRT_RESET_IOUT_HISTORY: + data->iout_max[page] = 0x7c00; + data->iout_min[page] = 0xfbff; + ret = ltc2978_clear_peaks(data, client, page); + break; + case PMBUS_VIRT_RESET_TEMP2_HISTORY: + data->temp2_max = 0x7c00; + ret = ltc2978_clear_peaks(data, client, page); + break; + case PMBUS_VIRT_RESET_VOUT_HISTORY: + data->vout_min[page] = 0xffff; + data->vout_max[page] = 0; + ret = ltc2978_clear_peaks(data, client, page); + break; + case PMBUS_VIRT_RESET_VIN_HISTORY: + data->vin_min = 0x7bff; + data->vin_max = 0x7c00; + ret = ltc2978_clear_peaks(data, client, page); + break; + case PMBUS_VIRT_RESET_TEMP_HISTORY: + data->temp_min[page] = 0x7bff; + data->temp_max[page] = 0x7c00; + ret = ltc2978_clear_peaks(data, client, page); + break; + default: + ret = ltc_wait_ready(client); + if (ret < 0) + return ret; + ret = -ENODATA; + break; + } + return ret; +} + +static const struct i2c_device_id ltc2978_id[] = { + { .name = "lt7170", .driver_data = lt7170 }, + { .name = "lt7171", .driver_data = lt7171 }, + { .name = "ltc2972", .driver_data = ltc2972 }, + { .name = "ltc2974", .driver_data = ltc2974 }, + { .name = "ltc2975", .driver_data = ltc2975 }, + { .name = "ltc2977", .driver_data = ltc2977 }, + { .name = "ltc2978", .driver_data = ltc2978 }, + { .name = "ltc2979", .driver_data = ltc2979 }, + { .name = "ltc2980", .driver_data = ltc2980 }, + { .name = "ltc3880", .driver_data = ltc3880 }, + { .name = "ltc3882", .driver_data = ltc3882 }, + { .name = "ltc3883", .driver_data = ltc3883 }, + { .name = "ltc3884", .driver_data = ltc3884 }, + { .name = "ltc3886", .driver_data = ltc3886 }, + { .name = "ltc3887", .driver_data = ltc3887 }, + { .name = "ltc3889", .driver_data = ltc3889 }, + { .name = "ltc7132", .driver_data = ltc7132 }, + { .name = "ltc7841", .driver_data = ltc7841 }, + { .name = "ltc7880", .driver_data = ltc7880 }, + { .name = "ltm2987", .driver_data = ltm2987 }, + { .name = "ltm4664", .driver_data = ltm4664 }, + { .name = "ltm4673", .driver_data = ltm4673 }, + { .name = "ltm4675", .driver_data = ltm4675 }, + { .name = "ltm4676", .driver_data = ltm4676 }, + { .name = "ltm4677", .driver_data = ltm4677 }, + { .name = "ltm4678", .driver_data = ltm4678 }, + { .name = "ltm4680", .driver_data = ltm4680 }, + { .name = "ltm4686", .driver_data = ltm4686 }, + { .name = "ltm4700", .driver_data = ltm4700 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ltc2978_id); + +#if IS_ENABLED(CONFIG_SENSORS_LTC2978_REGULATOR) +#define LTC2978_ADC_RES 0xFFFF +#define LTC2978_N_ADC 122 +#define LTC2978_MAX_UV (LTC2978_ADC_RES * LTC2978_N_ADC) +#define LTC2978_UV_STEP 1000 +#define LTC2978_N_VOLTAGES ((LTC2978_MAX_UV / LTC2978_UV_STEP) + 1) + +static const struct regulator_desc ltc2978_reg_desc[] = { + PMBUS_REGULATOR_STEP("vout", 0, LTC2978_N_VOLTAGES, LTC2978_UV_STEP, 0), + PMBUS_REGULATOR_STEP("vout", 1, LTC2978_N_VOLTAGES, LTC2978_UV_STEP, 0), + PMBUS_REGULATOR_STEP("vout", 2, LTC2978_N_VOLTAGES, LTC2978_UV_STEP, 0), + PMBUS_REGULATOR_STEP("vout", 3, LTC2978_N_VOLTAGES, LTC2978_UV_STEP, 0), + PMBUS_REGULATOR_STEP("vout", 4, LTC2978_N_VOLTAGES, LTC2978_UV_STEP, 0), + PMBUS_REGULATOR_STEP("vout", 5, LTC2978_N_VOLTAGES, LTC2978_UV_STEP, 0), + PMBUS_REGULATOR_STEP("vout", 6, LTC2978_N_VOLTAGES, LTC2978_UV_STEP, 0), + PMBUS_REGULATOR_STEP("vout", 7, LTC2978_N_VOLTAGES, LTC2978_UV_STEP, 0), +}; + +static const struct regulator_desc ltc2978_reg_desc_default[] = { + PMBUS_REGULATOR("vout", 0), + PMBUS_REGULATOR("vout", 1), + PMBUS_REGULATOR("vout", 2), + PMBUS_REGULATOR("vout", 3), + PMBUS_REGULATOR("vout", 4), + PMBUS_REGULATOR("vout", 5), + PMBUS_REGULATOR("vout", 6), + PMBUS_REGULATOR("vout", 7), +}; +#endif /* CONFIG_SENSORS_LTC2978_REGULATOR */ + +static int ltc2978_get_id(struct i2c_client *client) +{ + int chip_id; + + chip_id = i2c_smbus_read_word_data(client, LTC2978_MFR_SPECIAL_ID); + if (chip_id < 0) { + const struct i2c_device_id *id; + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + + ret = pmbus_read_smbus_i2c_block_data(client, PMBUS_MFR_ID, buf); + if (ret < 0) + return ret; + if (ret < 3 || (strncmp(buf, "LTC", 3) && strncmp(buf, "ADI", 3))) + return -ENODEV; + + ret = pmbus_read_smbus_i2c_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) + return ret; + for (id = <c2978_id[0]; strlen(id->name); id++) { + if (!strncasecmp(id->name, buf, strlen(id->name))) + return (int)id->driver_data; + } + return -ENODEV; + } + + chip_id &= LTC2978_ID_MASK; + + if (chip_id == LT7170_ID) { + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + + ret = pmbus_read_smbus_i2c_block_data(client, PMBUS_IC_DEVICE_ID, + buf); + if (ret < 0) + return ret; + + if (ret < 6) + return -ENODEV; + + if (!strncmp(buf, "LT7170", 6)) + return lt7170; + if (!strncmp(buf, "LT7171", 6)) + return lt7171; + + return -ENODEV; + } + + if (chip_id == LTC2972_ID) + return ltc2972; + else if (chip_id == LTC2974_ID) + return ltc2974; + else if (chip_id == LTC2975_ID) + return ltc2975; + else if (chip_id == LTC2977_ID) + return ltc2977; + else if (chip_id == LTC2978_ID_REV1 || chip_id == LTC2978_ID_REV2) + return ltc2978; + else if (chip_id == LTC2979_ID_A || chip_id == LTC2979_ID_B) + return ltc2979; + else if (chip_id == LTC2980_ID_A || chip_id == LTC2980_ID_B) + return ltc2980; + else if (chip_id == LTC3880_ID) + return ltc3880; + else if (chip_id == LTC3882_ID || chip_id == LTC3882_ID_D1) + return ltc3882; + else if (chip_id == LTC3883_ID) + return ltc3883; + else if (chip_id == LTC3884_ID) + return ltc3884; + else if (chip_id == LTC3886_ID) + return ltc3886; + else if (chip_id == LTC3887_ID) + return ltc3887; + else if (chip_id == LTC3889_ID) + return ltc3889; + else if (chip_id == LTC7132_ID) + return ltc7132; + else if (chip_id == LTC7841_ID) + return ltc7841; + else if (chip_id == LTC7880_ID) + return ltc7880; + else if (chip_id == LTM2987_ID_A || chip_id == LTM2987_ID_B) + return ltm2987; + else if (chip_id == LTM4664_ID) + return ltm4664; + else if (chip_id == LTM4673_ID || chip_id == LTM4673_ID_REV1) + return ltm4673; + else if (chip_id == LTM4675_ID) + return ltm4675; + else if (chip_id == LTM4676_ID_REV1 || chip_id == LTM4676_ID_REV2 || + chip_id == LTM4676A_ID) + return ltm4676; + else if (chip_id == LTM4677_ID_REV1 || chip_id == LTM4677_ID_REV2) + return ltm4677; + else if (chip_id == LTM4678_ID_REV1 || chip_id == LTM4678_ID_REV2) + return ltm4678; + else if (chip_id == LTM4680_ID) + return ltm4680; + else if (chip_id == LTM4686_ID) + return ltm4686; + else if (chip_id == LTM4700_ID) + return ltm4700; + + dev_err(&client->dev, "Unsupported chip ID 0x%x\n", chip_id); + return -ENODEV; +} + +static int ltc2978_probe(struct i2c_client *client) +{ + int i, chip_id; + struct ltc2978_data *data; + struct pmbus_driver_info *info; + const struct i2c_device_id *id; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_WORD_DATA)) + return -ENODEV; + + data = devm_kzalloc(&client->dev, sizeof(struct ltc2978_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + chip_id = ltc2978_get_id(client); + if (chip_id < 0) + return chip_id; + + data->id = chip_id; + id = i2c_client_get_device_id(client); + if (data->id != id->driver_data) + dev_warn(&client->dev, + "Device mismatch: Configured %s (%d), detected %d\n", + id->name, + (int) id->driver_data, + chip_id); + + info = &data->info; + info->write_word_data = ltc2978_write_word_data; + info->write_byte = ltc_write_byte; + info->write_byte_data = ltc_write_byte_data; + info->read_word_data = ltc_read_word_data; + info->read_byte_data = ltc_read_byte_data; + + data->vin_min = 0x7bff; + data->vin_max = 0x7c00; + for (i = 0; i < ARRAY_SIZE(data->vout_min); i++) + data->vout_min[i] = 0xffff; + for (i = 0; i < ARRAY_SIZE(data->iout_min); i++) + data->iout_min[i] = 0xfbff; + for (i = 0; i < ARRAY_SIZE(data->iout_max); i++) + data->iout_max[i] = 0x7c00; + for (i = 0; i < ARRAY_SIZE(data->temp_min); i++) + data->temp_min[i] = 0x7bff; + for (i = 0; i < ARRAY_SIZE(data->temp_max); i++) + data->temp_max[i] = 0x7c00; + data->temp2_max = 0x7c00; + + switch (data->id) { + case lt7170: + case lt7171: + data->features |= FEAT_CLEAR_PEAKS | FEAT_NEEDS_POLLING; + info->read_word_data = ltc3883_read_word_data; + info->pages = LTC3883_NUM_PAGES; + info->format[PSC_VOLTAGE_IN] = ieee754; + info->format[PSC_VOLTAGE_OUT] = ieee754; + info->format[PSC_CURRENT_OUT] = ieee754; + info->format[PSC_TEMPERATURE] = ieee754; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + break; + case ltc2972: + info->read_word_data = ltc2975_read_word_data; + info->pages = LTC2972_NUM_PAGES; + info->func[0] = PMBUS_HAVE_IIN | PMBUS_HAVE_PIN + | PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP2; + for (i = 0; i < info->pages; i++) { + info->func[i] |= PMBUS_HAVE_VOUT + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT; + } + break; + case ltc2974: + info->read_word_data = ltc2974_read_word_data; + info->pages = LTC2974_NUM_PAGES; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP2; + for (i = 0; i < info->pages; i++) { + info->func[i] |= PMBUS_HAVE_VOUT + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT; + } + break; + case ltc2975: + info->read_word_data = ltc2975_read_word_data; + info->pages = LTC2974_NUM_PAGES; + info->func[0] = PMBUS_HAVE_IIN | PMBUS_HAVE_PIN + | PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP2; + for (i = 0; i < info->pages; i++) { + info->func[i] |= PMBUS_HAVE_VOUT + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT; + } + break; + + case ltc2977: + case ltc2978: + case ltc2979: + case ltc2980: + case ltm2987: + info->read_word_data = ltc2978_read_word_data; + info->pages = LTC2978_NUM_PAGES; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + for (i = 1; i < LTC2978_NUM_PAGES; i++) { + info->func[i] = PMBUS_HAVE_VOUT + | PMBUS_HAVE_STATUS_VOUT; + } + break; + case ltc3880: + case ltc3887: + case ltm4675: + case ltm4676: + case ltm4677: + case ltm4686: + data->features |= FEAT_CLEAR_PEAKS | FEAT_NEEDS_POLLING; + info->read_word_data = ltc3880_read_word_data; + info->pages = LTC3880_NUM_PAGES; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN + | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_STATUS_TEMP; + info->func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + break; + case ltc3882: + data->features |= FEAT_CLEAR_PEAKS | FEAT_NEEDS_POLLING; + info->read_word_data = ltc3880_read_word_data; + info->pages = LTC3880_NUM_PAGES; + info->func[0] = PMBUS_HAVE_VIN + | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_STATUS_TEMP; + info->func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + break; + case ltc3883: + data->features |= FEAT_CLEAR_PEAKS | FEAT_NEEDS_POLLING; + info->read_word_data = ltc3883_read_word_data; + info->pages = LTC3883_NUM_PAGES; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN + | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_STATUS_TEMP; + break; + case ltc3884: + case ltc3886: + case ltc3889: + case ltc7132: + case ltc7880: + case ltm4664: + case ltm4678: + case ltm4680: + case ltm4700: + data->features |= FEAT_CLEAR_PEAKS | FEAT_NEEDS_POLLING; + info->read_word_data = ltc3883_read_word_data; + info->pages = LTC3880_NUM_PAGES; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN + | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_STATUS_TEMP; + info->func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + break; + case ltc7841: + data->features |= FEAT_CLEAR_PEAKS; + info->read_word_data = ltc3883_read_word_data; + info->pages = LTC3883_NUM_PAGES; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN + | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + break; + case ltm4673: + data->features |= FEAT_NEEDS_POLLING; + info->read_word_data = ltc2975_read_word_data; + info->pages = LTC2974_NUM_PAGES; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP2; + for (i = 0; i < info->pages; i++) { + info->func[i] |= PMBUS_HAVE_IIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_PIN + | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + } + break; + default: + return -ENODEV; + } + +#if IS_ENABLED(CONFIG_SENSORS_LTC2978_REGULATOR) + info->num_regulators = info->pages; + switch (data->id) { + case ltc2972: + case ltc2974: + case ltc2975: + case ltc2977: + case ltc2978: + case ltc2979: + case ltc2980: + case ltm2987: + info->reg_desc = ltc2978_reg_desc; + if (info->num_regulators > ARRAY_SIZE(ltc2978_reg_desc)) { + dev_warn(&client->dev, "num_regulators too large!"); + info->num_regulators = ARRAY_SIZE(ltc2978_reg_desc); + } + break; + default: + info->reg_desc = ltc2978_reg_desc_default; + if (info->num_regulators > ARRAY_SIZE(ltc2978_reg_desc_default)) { + dev_warn(&client->dev, "num_regulators too large!"); + info->num_regulators = + ARRAY_SIZE(ltc2978_reg_desc_default); + } + break; + } +#endif + + return pmbus_do_probe(client, info); +} + + +#ifdef CONFIG_OF +static const struct of_device_id ltc2978_of_match[] = { + { .compatible = "lltc,lt7170" }, + { .compatible = "lltc,lt7171" }, + { .compatible = "lltc,ltc2972" }, + { .compatible = "lltc,ltc2974" }, + { .compatible = "lltc,ltc2975" }, + { .compatible = "lltc,ltc2977" }, + { .compatible = "lltc,ltc2978" }, + { .compatible = "lltc,ltc2979" }, + { .compatible = "lltc,ltc2980" }, + { .compatible = "lltc,ltc3880" }, + { .compatible = "lltc,ltc3882" }, + { .compatible = "lltc,ltc3883" }, + { .compatible = "lltc,ltc3884" }, + { .compatible = "lltc,ltc3886" }, + { .compatible = "lltc,ltc3887" }, + { .compatible = "lltc,ltc3889" }, + { .compatible = "lltc,ltc7132" }, + { .compatible = "lltc,ltc7841" }, + { .compatible = "lltc,ltc7880" }, + { .compatible = "lltc,ltm2987" }, + { .compatible = "lltc,ltm4664" }, + { .compatible = "lltc,ltm4673" }, + { .compatible = "lltc,ltm4675" }, + { .compatible = "lltc,ltm4676" }, + { .compatible = "lltc,ltm4677" }, + { .compatible = "lltc,ltm4678" }, + { .compatible = "lltc,ltm4680" }, + { .compatible = "lltc,ltm4686" }, + { .compatible = "lltc,ltm4700" }, + { } +}; +MODULE_DEVICE_TABLE(of, ltc2978_of_match); +#endif + +static struct i2c_driver ltc2978_driver = { + .driver = { + .name = "ltc2978", + .of_match_table = of_match_ptr(ltc2978_of_match), + }, + .probe = ltc2978_probe, + .id_table = ltc2978_id, +}; + +module_i2c_driver(ltc2978_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for LTC2978 and compatible chips"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ltc3815.c b/drivers/hwmon/pmbus/ltc3815.c new file mode 100644 index 000000000..0219d03ad --- /dev/null +++ b/drivers/hwmon/pmbus/ltc3815.c @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for LTC3815 + * + * Copyright (c) 2015 Linear Technology + * Copyright (c) 2015 Guenter Roeck + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/jiffies.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define LTC3815_MFR_IOUT_PEAK 0xd7 +#define LTC3815_MFR_VOUT_PEAK 0xdd +#define LTC3815_MFR_VIN_PEAK 0xde +#define LTC3815_MFR_TEMP_PEAK 0xdf +#define LTC3815_MFR_IIN_PEAK 0xe1 +#define LTC3815_MFR_SPECIAL_ID 0xe7 + +#define LTC3815_ID 0x8000 +#define LTC3815_ID_MASK 0xff00 + +static int ltc3815_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VOUT_MODE: + /* + * The chip returns 0x3e, suggesting VID mode with manufacturer + * specific VID codes. Since the output voltage is reported + * with a LSB of 0.5mV, override and report direct mode with + * appropriate coefficients. + */ + ret = 0x40; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int ltc3815_write_byte(struct i2c_client *client, int page, u8 reg) +{ + int ret; + + switch (reg) { + case PMBUS_CLEAR_FAULTS: + /* + * LTC3815 does not support the CLEAR_FAULTS command. + * Emulate it by clearing the status register. + */ + ret = pmbus_read_word_data(client, 0, 0xff, PMBUS_STATUS_WORD); + if (ret > 0) { + pmbus_write_word_data(client, 0, PMBUS_STATUS_WORD, + ret); + ret = 0; + } + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int ltc3815_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VIN_MAX: + ret = pmbus_read_word_data(client, page, phase, + LTC3815_MFR_VIN_PEAK); + break; + case PMBUS_VIRT_READ_VOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, + LTC3815_MFR_VOUT_PEAK); + break; + case PMBUS_VIRT_READ_TEMP_MAX: + ret = pmbus_read_word_data(client, page, phase, + LTC3815_MFR_TEMP_PEAK); + break; + case PMBUS_VIRT_READ_IOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, + LTC3815_MFR_IOUT_PEAK); + break; + case PMBUS_VIRT_READ_IIN_MAX: + ret = pmbus_read_word_data(client, page, phase, + LTC3815_MFR_IIN_PEAK); + break; + case PMBUS_VIRT_RESET_VOUT_HISTORY: + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_TEMP_HISTORY: + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_RESET_IIN_HISTORY: + ret = 0; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int ltc3815_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_RESET_IIN_HISTORY: + ret = pmbus_write_word_data(client, page, + LTC3815_MFR_IIN_PEAK, 0); + break; + case PMBUS_VIRT_RESET_IOUT_HISTORY: + ret = pmbus_write_word_data(client, page, + LTC3815_MFR_IOUT_PEAK, 0); + break; + case PMBUS_VIRT_RESET_VOUT_HISTORY: + ret = pmbus_write_word_data(client, page, + LTC3815_MFR_VOUT_PEAK, 0); + break; + case PMBUS_VIRT_RESET_VIN_HISTORY: + ret = pmbus_write_word_data(client, page, + LTC3815_MFR_VIN_PEAK, 0); + break; + case PMBUS_VIRT_RESET_TEMP_HISTORY: + ret = pmbus_write_word_data(client, page, + LTC3815_MFR_TEMP_PEAK, 0); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static const struct i2c_device_id ltc3815_id[] = { + { .name = "ltc3815" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ltc3815_id); + +static struct pmbus_driver_info ltc3815_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 250, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + .m[PSC_VOLTAGE_OUT] = 2, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_CURRENT_IN] = 1, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = 2, + .m[PSC_CURRENT_OUT] = 1, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 2, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 0, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | PMBUS_HAVE_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP, + .read_byte_data = ltc3815_read_byte_data, + .read_word_data = ltc3815_read_word_data, + .write_byte = ltc3815_write_byte, + .write_word_data = ltc3815_write_word_data, +}; + +static int ltc3815_probe(struct i2c_client *client) +{ + int chip_id; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_WORD_DATA)) + return -ENODEV; + + chip_id = i2c_smbus_read_word_data(client, LTC3815_MFR_SPECIAL_ID); + if (chip_id < 0) + return chip_id; + if ((chip_id & LTC3815_ID_MASK) != LTC3815_ID) + return -ENODEV; + + return pmbus_do_probe(client, <c3815_info); +} + +static struct i2c_driver ltc3815_driver = { + .driver = { + .name = "ltc3815", + }, + .probe = ltc3815_probe, + .id_table = ltc3815_id, +}; + +module_i2c_driver(ltc3815_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for LTC3815"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ltc4286.c b/drivers/hwmon/pmbus/ltc4286.c new file mode 100644 index 000000000..8715d3807 --- /dev/null +++ b/drivers/hwmon/pmbus/ltc4286.c @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +/* LTC4286 register */ +#define LTC4286_MFR_CONFIG1 0xF2 + +/* LTC4286 configuration */ +#define VRANGE_SELECT_BIT BIT(1) + +#define LTC4286_MFR_ID_SIZE 3 + +/* + * Initialize the MBR as default settings which is referred to LTC4286 datasheet + * (March 22, 2022 version) table 3 page 16 + */ +static struct pmbus_driver_info ltc4286_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 32, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 1, + .m[PSC_VOLTAGE_OUT] = 32, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 1, + .m[PSC_CURRENT_OUT] = 1024, + .b[PSC_CURRENT_OUT] = 0, + /* + * The rsense value used in MBR formula in LTC4286 datasheet should be ohm unit. + * However, the rsense value that user input is micro ohm. + * Thus, the MBR setting which involves rsense should be shifted by 6 digits. + */ + .R[PSC_CURRENT_OUT] = 3 - 6, + .m[PSC_POWER] = 1, + .b[PSC_POWER] = 0, + /* + * The rsense value used in MBR formula in LTC4286 datasheet should be ohm unit. + * However, the rsense value that user input is micro ohm. + * Thus, the MBR setting which involves rsense should be shifted by 6 digits. + */ + .R[PSC_POWER] = 4 - 6, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 273, + .R[PSC_TEMPERATURE] = 0, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_TEMP, +}; + +static const struct i2c_device_id ltc4286_id[] = { + { "ltc4286", }, + { "ltc4287", }, + {} +}; +MODULE_DEVICE_TABLE(i2c, ltc4286_id); + +static int ltc4286_probe(struct i2c_client *client) +{ + int ret; + const struct i2c_device_id *mid; + u8 block_buffer[I2C_SMBUS_BLOCK_MAX + 1]; + struct pmbus_driver_info *info; + u32 rsense; + int vrange_nval, vrange_oval; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, block_buffer); + if (ret < 0) { + return dev_err_probe(&client->dev, ret, + "Failed to read manufacturer id\n"); + } + + /* + * Refer to ltc4286 datasheet page 20 + * the manufacturer id is LTC + */ + if (ret != LTC4286_MFR_ID_SIZE || + strncmp(block_buffer, "LTC", LTC4286_MFR_ID_SIZE)) { + return dev_err_probe(&client->dev, -ENODEV, + "Manufacturer id mismatch\n"); + } + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, block_buffer); + if (ret < 0) { + return dev_err_probe(&client->dev, ret, + "Failed to read manufacturer model\n"); + } + + for (mid = ltc4286_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, block_buffer, strlen(mid->name))) + break; + } + if (!mid->name[0]) + return dev_err_probe(&client->dev, -ENODEV, + "Unsupported device\n"); + + if (of_property_read_u32(client->dev.of_node, + "shunt-resistor-micro-ohms", &rsense)) + rsense = 300; /* 0.3 mOhm if not set via DT */ + + if (rsense == 0) + return -EINVAL; + + /* Check for the latter MBR value won't overflow */ + if (rsense > (INT_MAX / 1024)) + return -EINVAL; + + info = devm_kmemdup(&client->dev, <c4286_info, sizeof(*info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + /* Check MFR1 CONFIG register bit 1 VRANGE_SELECT before driver loading */ + vrange_oval = i2c_smbus_read_word_data(client, LTC4286_MFR_CONFIG1); + if (vrange_oval < 0) + return dev_err_probe(&client->dev, vrange_oval, + "Failed to read manufacturer configuration one\n"); + vrange_nval = vrange_oval; + + if (device_property_read_bool(&client->dev, "adi,vrange-low-enable")) { + vrange_nval &= + ~VRANGE_SELECT_BIT; /* VRANGE_SELECT = 0, 25.6 volts */ + + info->m[PSC_VOLTAGE_IN] = 128; + info->m[PSC_VOLTAGE_OUT] = 128; + info->m[PSC_POWER] = 4 * rsense; + } else { + vrange_nval |= + VRANGE_SELECT_BIT; /* VRANGE_SELECT = 1, 102.4 volts */ + + info->m[PSC_POWER] = rsense; + } + if (vrange_nval != vrange_oval) { + /* Set MFR1 CONFIG register bit 1 VRANGE_SELECT */ + ret = i2c_smbus_write_word_data(client, LTC4286_MFR_CONFIG1, + vrange_nval); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "Failed to set vrange\n"); + } + + info->m[PSC_CURRENT_OUT] = 1024 * rsense; + + return pmbus_do_probe(client, info); +} + +static const struct of_device_id ltc4286_of_match[] = { + { .compatible = "lltc,ltc4286" }, + { .compatible = "lltc,ltc4287" }, + {} +}; + +static struct i2c_driver ltc4286_driver = { + .driver = { + .name = "ltc4286", + .of_match_table = ltc4286_of_match, + }, + .probe = ltc4286_probe, + .id_table = ltc4286_id, +}; + +module_i2c_driver(ltc4286_driver); + +MODULE_AUTHOR("Delphine CC Chiu <Delphine_CC_Chiu@wiwynn.com>"); +MODULE_DESCRIPTION("PMBUS driver for LTC4286 and compatibles"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/lx1308.c b/drivers/hwmon/pmbus/lx1308.c new file mode 100644 index 000000000..0c4533937 --- /dev/null +++ b/drivers/hwmon/pmbus/lx1308.c @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define LX1308_MFR_IOUT_OCP3_FAULT 0xBE +#define LX1308_MFR_IOUT_OCP3_WARN 0xBF + +/* + * Decode a Linear11-encoded word to an integer value. + * Linear11 format: bits[15:11] = signed 5-bit exponent, + * bits[10:0] = signed 11-bit mantissa. Result = mant * 2^exp. + */ +static inline int linear11_to_int(u16 word) +{ + s16 exp = ((s16)word) >> 11; + s16 mant = ((s16)((word & 0x7ff) << 5)) >> 5; + + return (exp >= 0) ? (int)((u32)mant << exp) : (mant >> -exp); +} + +static int lx1308_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + if (page > 0) + return -ENXIO; + + switch (reg) { + /* + * The LX1308 OCP3 registers (slow OCP, 60ms delay) use a + * manufacturer-specific U8.0 format. Read the byte value N and present + * it as a Linear11 word with exponent 0. + */ + case PMBUS_IOUT_OC_FAULT_LIMIT: + ret = i2c_smbus_read_byte_data(client, LX1308_MFR_IOUT_OCP3_FAULT); + if (ret < 0) + break; + ret &= 0x7FF; + break; + + case PMBUS_IOUT_OC_WARN_LIMIT: + ret = i2c_smbus_read_byte_data(client, LX1308_MFR_IOUT_OCP3_WARN); + if (ret < 0) + break; + ret &= 0x7FF; + break; + + /* + * The following registers are not implemented by the LX1308. Return + * -ENXIO to suppress the corresponding sysfs attributes. + */ + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_IIN_OC_FAULT_LIMIT: + case PMBUS_IOUT_UC_FAULT_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + case PMBUS_POUT_OP_WARN_LIMIT: + case PMBUS_UT_WARN_LIMIT: + case PMBUS_UT_FAULT_LIMIT: + case PMBUS_MFR_IIN_MAX: + case PMBUS_MFR_IOUT_MAX: + case PMBUS_MFR_VIN_MIN: + case PMBUS_MFR_VIN_MAX: + case PMBUS_MFR_VOUT_MIN: + case PMBUS_MFR_VOUT_MAX: + case PMBUS_MFR_PIN_MAX: + case PMBUS_MFR_POUT_MAX: + case PMBUS_MFR_MAX_TEMP_1: + ret = -ENXIO; + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int lx1308_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + int ret; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_IOUT_OC_FAULT_LIMIT: + /* + * Decode Linear11 word from pmbus_core back to a plain integer + * and write as the U8.0 byte the device expects. + */ + ret = i2c_smbus_write_byte_data(client, LX1308_MFR_IOUT_OCP3_FAULT, + clamp_val(linear11_to_int(word), 0, 255)); + break; + + case PMBUS_IOUT_OC_WARN_LIMIT: + ret = i2c_smbus_write_byte_data(client, LX1308_MFR_IOUT_OCP3_WARN, + clamp_val(linear11_to_int(word), 0, 255)); + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static struct pmbus_driver_info lx1308_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_PIN | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_STATUS_INPUT, + + .read_word_data = lx1308_read_word_data, + .write_word_data = lx1308_write_word_data, +}; + +static const struct of_device_id lx1308_of_match[] = { + { .compatible = "luxshare,lx1308" }, + { } +}; + +MODULE_DEVICE_TABLE(of, lx1308_of_match); + +static const struct i2c_device_id lx1308_id[] = { + { "lx1308" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, lx1308_id); + +static int lx1308_probe(struct i2c_client *client) +{ + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + const struct i2c_device_id *mid; + int ret; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, buf); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "Failed to read manufacturer id\n"); + buf[ret] = '\0'; + + if (ret != 12 || strncmp(buf, "LUXSHARE", 8)) + return dev_err_probe(&client->dev, -ENODEV, + "Unsupported Manufacturer ID '%s'\n", buf); + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "Failed to read Manufacturer Model\n"); + buf[ret] = '\0'; + + for (mid = lx1308_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, buf, strlen(mid->name))) + break; + } + if (!mid->name[0]) + return dev_err_probe(&client->dev, -ENODEV, + "Unsupported Manufacturer Model '%s'\n", buf); + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_REVISION, buf); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "Failed to read Manufacturer Revision\n"); + buf[ret] = '\0'; + + if (ret != 12 || buf[0] != 'V') + return dev_err_probe(&client->dev, -ENODEV, + "Unsupported Manufacturer Revision '%s'\n", buf); + return pmbus_do_probe(client, &lx1308_info); +} + +static struct i2c_driver lx1308_driver = { + .driver = { + .name = "lx1308", + .of_match_table = lx1308_of_match, + }, + .probe = lx1308_probe, + .id_table = lx1308_id, +}; + +module_i2c_driver(lx1308_driver); + +MODULE_AUTHOR("Brian Chiang <chiang.brian@inventec.com>"); +MODULE_DESCRIPTION("PMBus driver for Luxshare LX1308"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max15301.c b/drivers/hwmon/pmbus/max15301.c new file mode 100644 index 000000000..4c36f1ea2 --- /dev/null +++ b/drivers/hwmon/pmbus/max15301.c @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Maxim MAX15301 + * + * Copyright (c) 2021 Flextronics International Sweden AB + * + * Even though the specification does not specifically mention it, + * extensive empirical testing has revealed that auto-detection of + * limit-registers will fail in a random fashion unless the delay + * parameter is set to above about 80us. The default delay is set + * to 100us to include some safety margin. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/ktime.h> +#include <linux/delay.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +static const struct i2c_device_id max15301_id[] = { + { .name = "bmr461" }, + { .name = "max15301" }, + { .name = "max15303" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, max15301_id); + +struct max15301_data { + int id; + struct pmbus_driver_info info; +}; + +#define to_max15301_data(x) container_of(x, struct max15301_data, info) + +#define MAX15301_WAIT_TIME 100 /* us */ + +static ushort delay = MAX15301_WAIT_TIME; +module_param(delay, ushort, 0644); +MODULE_PARM_DESC(delay, "Delay between chip accesses in us"); + +static struct max15301_data max15301_data = { + .info = { + .pages = 1, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 + | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + } +}; + +static int max15301_probe(struct i2c_client *client) +{ + int status; + u8 device_id[I2C_SMBUS_BLOCK_MAX + 1]; + const struct i2c_device_id *mid; + struct pmbus_driver_info *info = &max15301_data.info; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA + | I2C_FUNC_SMBUS_BLOCK_DATA)) + return -ENODEV; + + status = i2c_smbus_read_block_data(client, PMBUS_IC_DEVICE_ID, device_id); + if (status < 0) { + dev_err(&client->dev, "Failed to read Device Id\n"); + return status; + } + for (mid = max15301_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, device_id, strlen(mid->name))) + break; + } + if (!mid->name[0]) { + dev_err(&client->dev, "Unsupported device\n"); + return -ENODEV; + } + + info->access_delay = delay; + + return pmbus_do_probe(client, info); +} + +static struct i2c_driver max15301_driver = { + .driver = { + .name = "max15301", + }, + .probe = max15301_probe, + .id_table = max15301_id, +}; + +module_i2c_driver(max15301_driver); + +MODULE_AUTHOR("Erik Rosen <erik.rosen@metormote.com>"); +MODULE_DESCRIPTION("PMBus driver for Maxim MAX15301"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max16064.c b/drivers/hwmon/pmbus/max16064.c new file mode 100644 index 000000000..351a1f535 --- /dev/null +++ b/drivers/hwmon/pmbus/max16064.c @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Maxim MAX16064 + * + * Copyright (c) 2011 Ericsson AB. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include "pmbus.h" + +#define MAX16064_MFR_VOUT_PEAK 0xd4 +#define MAX16064_MFR_TEMPERATURE_PEAK 0xd6 + +static int max16064_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, + MAX16064_MFR_VOUT_PEAK); + break; + case PMBUS_VIRT_READ_TEMP_MAX: + ret = pmbus_read_word_data(client, page, phase, + MAX16064_MFR_TEMPERATURE_PEAK); + break; + case PMBUS_VIRT_RESET_VOUT_HISTORY: + case PMBUS_VIRT_RESET_TEMP_HISTORY: + ret = 0; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int max16064_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_RESET_VOUT_HISTORY: + ret = pmbus_write_word_data(client, page, + MAX16064_MFR_VOUT_PEAK, 0); + break; + case PMBUS_VIRT_RESET_TEMP_HISTORY: + ret = pmbus_write_word_data(client, page, + MAX16064_MFR_TEMPERATURE_PEAK, + 0xffff); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static struct pmbus_driver_info max16064_info = { + .pages = 4, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 19995, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -1, + .m[PSC_VOLTAGE_OUT] = 19995, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = -1, + .m[PSC_TEMPERATURE] = -7612, + .b[PSC_TEMPERATURE] = 335, + .R[PSC_TEMPERATURE] = -3, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_TEMP, + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[2] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[3] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .read_word_data = max16064_read_word_data, + .write_word_data = max16064_write_word_data, +}; + +static int max16064_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &max16064_info); +} + +static const struct i2c_device_id max16064_id[] = { + { .name = "max16064" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, max16064_id); + +/* This is the driver that will be inserted */ +static struct i2c_driver max16064_driver = { + .driver = { + .name = "max16064", + }, + .probe = max16064_probe, + .id_table = max16064_id, +}; + +module_i2c_driver(max16064_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for Maxim MAX16064"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max16601.c b/drivers/hwmon/pmbus/max16601.c new file mode 100644 index 000000000..3dd1f6fd0 --- /dev/null +++ b/drivers/hwmon/pmbus/max16601.c @@ -0,0 +1,369 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Hardware monitoring driver for Maxim MAX16508, MAX16600, MAX16601, + * and MAX16602. + * + * Implementation notes: + * + * This chip series supports two rails, VCORE and VSA. Telemetry information + * for the two rails is reported in two subsequent I2C addresses. The driver + * instantiates a dummy I2C client at the second I2C address to report + * information for the VSA rail in a single instance of the driver. + * Telemetry for the VSA rail is reported to the PMBus core in PMBus page 2. + * + * The chip reports input current using two separate methods. The input current + * reported with the standard READ_IIN command is derived from the output + * current. The first method is reported to the PMBus core with PMBus page 0, + * the second method is reported with PMBus page 1. + * + * The chip supports reading per-phase temperatures and per-phase input/output + * currents for VCORE. Telemetry is reported in vendor specific registers. + * The driver translates the vendor specific register values to PMBus standard + * register values and reports per-phase information in PMBus page 0. + * + * Copyright 2019, 2020 Google LLC. + */ + +#include <linux/bits.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> + +#include "pmbus.h" + +enum chips { max16508, max16600, max16601, max16602 }; + +#define REG_DEFAULT_NUM_POP 0xc4 +#define REG_SETPT_DVID 0xd1 +#define DAC_10MV_MODE BIT(4) +#define REG_IOUT_AVG_PK 0xee +#define REG_IIN_SENSOR 0xf1 +#define REG_TOTAL_INPUT_POWER 0xf2 +#define REG_PHASE_ID 0xf3 +#define CORE_RAIL_INDICATOR BIT(7) +#define REG_PHASE_REPORTING 0xf4 + +#define MAX16601_NUM_PHASES 8 + +struct max16601_data { + enum chips id; + struct pmbus_driver_info info; + struct i2c_client *vsa; + int iout_avg_pkg; +}; + +#define to_max16601_data(x) container_of(x, struct max16601_data, info) + +static int max16601_read_byte(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max16601_data *data = to_max16601_data(info); + + if (page > 0) { + if (page == 2) /* VSA */ + return i2c_smbus_read_byte_data(data->vsa, reg); + return -EOPNOTSUPP; + } + return -ENODATA; +} + +static int max16601_read_word(struct i2c_client *client, int page, int phase, + int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max16601_data *data = to_max16601_data(info); + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + int ret; + + switch (page) { + case 0: /* VCORE */ + if (phase == 0xff) + return -ENODATA; + switch (reg) { + case PMBUS_READ_IIN: + case PMBUS_READ_IOUT: + case PMBUS_READ_TEMPERATURE_1: + ret = i2c_smbus_write_byte_data(client, REG_PHASE_ID, + phase); + if (ret) + return ret; + ret = i2c_smbus_read_block_data(client, + REG_PHASE_REPORTING, + buf); + if (ret < 0) + return ret; + if (ret < 6) + return -EIO; + switch (reg) { + case PMBUS_READ_TEMPERATURE_1: + return buf[1] << 8 | buf[0]; + case PMBUS_READ_IOUT: + return buf[3] << 8 | buf[2]; + case PMBUS_READ_IIN: + return buf[5] << 8 | buf[4]; + default: + break; + } + } + return -EOPNOTSUPP; + case 1: /* VCORE, read IIN/PIN from sensor element */ + switch (reg) { + case PMBUS_READ_IIN: + return i2c_smbus_read_word_data(client, REG_IIN_SENSOR); + case PMBUS_READ_PIN: + return i2c_smbus_read_word_data(client, + REG_TOTAL_INPUT_POWER); + default: + break; + } + return -EOPNOTSUPP; + case 2: /* VSA */ + switch (reg) { + case PMBUS_VIRT_READ_IOUT_MAX: + ret = i2c_smbus_read_word_data(data->vsa, + REG_IOUT_AVG_PK); + if (ret < 0) + return ret; + if (sign_extend32(ret, 10) > + sign_extend32(data->iout_avg_pkg, 10)) + data->iout_avg_pkg = ret; + return data->iout_avg_pkg; + case PMBUS_VIRT_RESET_IOUT_HISTORY: + return 0; + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + case PMBUS_READ_IIN: + case PMBUS_READ_IOUT: + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_STATUS_WORD: + return i2c_smbus_read_word_data(data->vsa, reg); + default: + return -EOPNOTSUPP; + } + default: + return -EOPNOTSUPP; + } +} + +static int max16601_write_byte(struct i2c_client *client, int page, u8 reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max16601_data *data = to_max16601_data(info); + + if (page == 2) { + if (reg == PMBUS_CLEAR_FAULTS) + return i2c_smbus_write_byte(data->vsa, reg); + return -EOPNOTSUPP; + } + return -ENODATA; +} + +static int max16601_write_word(struct i2c_client *client, int page, int reg, + u16 value) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct max16601_data *data = to_max16601_data(info); + + switch (page) { + case 0: /* VCORE */ + return -ENODATA; + case 1: /* VCORE IIN/PIN from sensor element */ + default: + return -EOPNOTSUPP; + case 2: /* VSA */ + switch (reg) { + case PMBUS_VIRT_RESET_IOUT_HISTORY: + data->iout_avg_pkg = 0xfc00; + return 0; + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + return i2c_smbus_write_word_data(data->vsa, reg, value); + default: + return -EOPNOTSUPP; + } + } +} + +static int max16601_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + struct max16601_data *data = to_max16601_data(info); + int reg; + + reg = i2c_smbus_read_byte_data(client, REG_SETPT_DVID); + if (reg < 0) + return reg; + if (reg & DAC_10MV_MODE) + info->vrm_version[0] = vr13; + else + info->vrm_version[0] = vr12; + + if (data->id != max16600 && data->id != max16601 && data->id != max16602) + return 0; + + reg = i2c_smbus_read_byte_data(client, REG_DEFAULT_NUM_POP); + if (reg < 0) + return reg; + + /* + * If REG_DEFAULT_NUM_POP returns 0, we don't know how many phases + * are populated. Stick with the default in that case. + */ + reg &= 0x0f; + if (reg && reg <= MAX16601_NUM_PHASES) + info->phases[0] = reg; + + return 0; +} + +static struct pmbus_driver_info max16601_info = { + .pages = 3, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = vid, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_POWER] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | + PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_POUT | PMBUS_PAGE_VIRTUAL | PMBUS_PHASE_VIRTUAL, + .func[1] = PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | PMBUS_PAGE_VIRTUAL, + .func[2] = PMBUS_HAVE_IIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | PMBUS_PAGE_VIRTUAL, + .phases[0] = MAX16601_NUM_PHASES, + .pfunc[0] = PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP, + .pfunc[1] = PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT, + .pfunc[2] = PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP, + .pfunc[3] = PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT, + .pfunc[4] = PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP, + .pfunc[5] = PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT, + .pfunc[6] = PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP, + .pfunc[7] = PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT, + .identify = max16601_identify, + .read_byte_data = max16601_read_byte, + .read_word_data = max16601_read_word, + .write_byte = max16601_write_byte, + .write_word_data = max16601_write_word, +}; + +static void max16601_remove(void *_data) +{ + struct max16601_data *data = _data; + + i2c_unregister_device(data->vsa); +} + +static const struct i2c_device_id max16601_id[] = { + { .name = "max16508", .driver_data = max16508 }, + { .name = "max16600", .driver_data = max16600 }, + { .name = "max16601", .driver_data = max16601 }, + { .name = "max16602", .driver_data = max16602 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, max16601_id); + +static int max16601_get_id(struct i2c_client *client) +{ + struct device *dev = &client->dev; + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + enum chips id; + int ret; + + ret = i2c_smbus_read_block_data(client, PMBUS_IC_DEVICE_ID, buf); + if (ret < 0 || ret < 11) + return -ENODEV; + + /* + * PMBUS_IC_DEVICE_ID is expected to return MAX1660[012]y.xx", + * "MAX16500y.xx".cdxxcccccccccc, or "MAX16508y.xx". + */ + if (!strncmp(buf, "MAX16500", 8) || !strncmp(buf, "MAX16508", 8)) { + id = max16508; + } else if (!strncmp(buf, "MAX16600", 8)) { + id = max16600; + } else if (!strncmp(buf, "MAX16601", 8)) { + id = max16601; + } else if (!strncmp(buf, "MAX16602", 8)) { + id = max16602; + } else { + buf[ret] = '\0'; + dev_err(dev, "Unsupported chip '%s'\n", buf); + return -ENODEV; + } + return id; +} + +static int max16601_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + const struct i2c_device_id *id; + struct max16601_data *data; + int ret, chip_id; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA | + I2C_FUNC_SMBUS_READ_BLOCK_DATA)) + return -ENODEV; + + chip_id = max16601_get_id(client); + if (chip_id < 0) + return chip_id; + + id = i2c_client_get_device_id(client); + if (chip_id != id->driver_data) + dev_warn(&client->dev, + "Device mismatch: Configured %s (%d), detected %d\n", + id->name, (int) id->driver_data, chip_id); + + ret = i2c_smbus_read_byte_data(client, REG_PHASE_ID); + if (ret < 0) + return ret; + if (!(ret & CORE_RAIL_INDICATOR)) { + dev_err(dev, + "Driver must be instantiated on CORE rail I2C address\n"); + return -ENODEV; + } + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->id = chip_id; + data->iout_avg_pkg = 0xfc00; + data->vsa = i2c_new_dummy_device(client->adapter, client->addr + 1); + if (IS_ERR(data->vsa)) { + dev_err(dev, "Failed to register VSA client\n"); + return PTR_ERR(data->vsa); + } + ret = devm_add_action_or_reset(dev, max16601_remove, data); + if (ret) + return ret; + + data->info = max16601_info; + + return pmbus_do_probe(client, &data->info); +} + +static struct i2c_driver max16601_driver = { + .driver = { + .name = "max16601", + }, + .probe = max16601_probe, + .id_table = max16601_id, +}; + +module_i2c_driver(max16601_driver); + +MODULE_AUTHOR("Guenter Roeck <linux@roeck-us.net>"); +MODULE_DESCRIPTION("PMBus driver for Maxim MAX16601"); +MODULE_LICENSE("GPL v2"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max17616.c b/drivers/hwmon/pmbus/max17616.c new file mode 100644 index 000000000..7636bbf06 --- /dev/null +++ b/drivers/hwmon/pmbus/max17616.c @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Hardware monitoring driver for Analog Devices MAX17616/MAX17616A + * + * Copyright (C) 2025 Analog Devices, Inc. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/module.h> + +#include "pmbus.h" + +static struct pmbus_driver_info max17616_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_VOLTAGE_IN] = 512, + .b[PSC_VOLTAGE_IN] = -18, + .R[PSC_VOLTAGE_IN] = -1, + + .format[PSC_VOLTAGE_OUT] = direct, + .m[PSC_VOLTAGE_OUT] = 512, + .b[PSC_VOLTAGE_OUT] = -18, + .R[PSC_VOLTAGE_OUT] = -1, + + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_CURRENT_OUT] = 5845, + .b[PSC_CURRENT_OUT] = 80, + .R[PSC_CURRENT_OUT] = -1, + + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 71, + .b[PSC_TEMPERATURE] = 19653, + .R[PSC_TEMPERATURE] = -1, + + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_STATUS_TEMP, +}; + +static int max17616_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &max17616_info); +} + +static const struct i2c_device_id max17616_id[] = { + { .name = "max17616" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, max17616_id); + +static const struct of_device_id max17616_of_match[] = { + { .compatible = "adi,max17616" }, + { } +}; +MODULE_DEVICE_TABLE(of, max17616_of_match); + +static struct i2c_driver max17616_driver = { + .driver = { + .name = "max17616", + .of_match_table = max17616_of_match, + }, + .probe = max17616_probe, + .id_table = max17616_id, +}; +module_i2c_driver(max17616_driver); + +MODULE_AUTHOR("Kim Seer Paller <kimseer.paller@analog.com>"); +MODULE_DESCRIPTION("PMBus driver for Analog Devices MAX17616/MAX17616A"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max20730.c b/drivers/hwmon/pmbus/max20730.c new file mode 100644 index 000000000..4031f894e --- /dev/null +++ b/drivers/hwmon/pmbus/max20730.c @@ -0,0 +1,787 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for MAX20710, MAX20730, MAX20734, and MAX20743 Integrated, + * Step-Down Switching Regulators + * + * Copyright 2019 Google LLC. + * Copyright 2020 Maxim Integrated + */ + +#include <linux/bits.h> +#include <linux/debugfs.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/pmbus.h> +#include <linux/util_macros.h> +#include "pmbus.h" + +enum chips { + max20710, + max20730, + max20734, + max20743 +}; + +enum { + MAX20730_DEBUGFS_VOUT_MIN = 0, + MAX20730_DEBUGFS_FREQUENCY, + MAX20730_DEBUGFS_PG_DELAY, + MAX20730_DEBUGFS_INTERNAL_GAIN, + MAX20730_DEBUGFS_BOOT_VOLTAGE, + MAX20730_DEBUGFS_OUT_V_RAMP_RATE, + MAX20730_DEBUGFS_OC_PROTECT_MODE, + MAX20730_DEBUGFS_SS_TIMING, + MAX20730_DEBUGFS_IMAX, + MAX20730_DEBUGFS_OPERATION, + MAX20730_DEBUGFS_ON_OFF_CONFIG, + MAX20730_DEBUGFS_SMBALERT_MASK, + MAX20730_DEBUGFS_VOUT_MODE, + MAX20730_DEBUGFS_VOUT_COMMAND, + MAX20730_DEBUGFS_VOUT_MAX, + MAX20730_DEBUGFS_NUM_ENTRIES +}; + +struct max20730_data { + enum chips id; + struct pmbus_driver_info info; + struct mutex lock; /* Used to protect against parallel writes */ + u16 mfr_devset1; + u16 mfr_devset2; + u16 mfr_voutmin; + u32 vout_voltage_divider[2]; +}; + +#define to_max20730_data(x) container_of(x, struct max20730_data, info) + +#define VOLT_FROM_REG(val) DIV_ROUND_CLOSEST((val), 1 << 9) + +#define PMBUS_SMB_ALERT_MASK 0x1B + +#define MAX20730_MFR_VOUT_MIN 0xd1 +#define MAX20730_MFR_DEVSET1 0xd2 +#define MAX20730_MFR_DEVSET2 0xd3 + +#define MAX20730_MFR_VOUT_MIN_MASK GENMASK(9, 0) +#define MAX20730_MFR_VOUT_MIN_BIT_POS 0 + +#define MAX20730_MFR_DEVSET1_RGAIN_MASK (BIT(13) | BIT(14)) +#define MAX20730_MFR_DEVSET1_OTP_MASK (BIT(11) | BIT(12)) +#define MAX20730_MFR_DEVSET1_VBOOT_MASK (BIT(8) | BIT(9)) +#define MAX20730_MFR_DEVSET1_OCP_MASK (BIT(5) | BIT(6)) +#define MAX20730_MFR_DEVSET1_FSW_MASK GENMASK(4, 2) +#define MAX20730_MFR_DEVSET1_TSTAT_MASK (BIT(0) | BIT(1)) + +#define MAX20730_MFR_DEVSET1_RGAIN_BIT_POS 13 +#define MAX20730_MFR_DEVSET1_OTP_BIT_POS 11 +#define MAX20730_MFR_DEVSET1_VBOOT_BIT_POS 8 +#define MAX20730_MFR_DEVSET1_OCP_BIT_POS 5 +#define MAX20730_MFR_DEVSET1_FSW_BIT_POS 2 +#define MAX20730_MFR_DEVSET1_TSTAT_BIT_POS 0 + +#define MAX20730_MFR_DEVSET2_IMAX_MASK GENMASK(10, 8) +#define MAX20730_MFR_DEVSET2_VRATE (BIT(6) | BIT(7)) +#define MAX20730_MFR_DEVSET2_OCPM_MASK BIT(5) +#define MAX20730_MFR_DEVSET2_SS_MASK (BIT(0) | BIT(1)) + +#define MAX20730_MFR_DEVSET2_IMAX_BIT_POS 8 +#define MAX20730_MFR_DEVSET2_VRATE_BIT_POS 6 +#define MAX20730_MFR_DEVSET2_OCPM_BIT_POS 5 +#define MAX20730_MFR_DEVSET2_SS_BIT_POS 0 + +#define DEBUG_FS_DATA_MAX 16 + +struct max20730_debugfs_data { + struct i2c_client *client; + int debugfs_entries[MAX20730_DEBUGFS_NUM_ENTRIES]; +}; + +#define to_psu(x, y) container_of((x), \ + struct max20730_debugfs_data, debugfs_entries[(y)]) + +#ifdef CONFIG_DEBUG_FS +static ssize_t max20730_debugfs_read(struct file *file, char __user *buf, + size_t count, loff_t *ppos) +{ + int ret, len; + int *idxp = file->private_data; + int idx = *idxp; + struct max20730_debugfs_data *psu = to_psu(idxp, idx); + const struct pmbus_driver_info *info; + const struct max20730_data *data; + char tbuf[DEBUG_FS_DATA_MAX] = { 0 }; + char *result = tbuf; + u16 val; + + info = pmbus_get_driver_info(psu->client); + data = to_max20730_data(info); + + switch (idx) { + case MAX20730_DEBUGFS_VOUT_MIN: + ret = VOLT_FROM_REG(data->mfr_voutmin * 10000); + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, "%d.%d\n", + ret / 10000, ret % 10000); + break; + case MAX20730_DEBUGFS_FREQUENCY: + val = (data->mfr_devset1 & MAX20730_MFR_DEVSET1_FSW_MASK) + >> MAX20730_MFR_DEVSET1_FSW_BIT_POS; + + if (val == 0) + ret = 400; + else if (val == 1) + ret = 500; + else if (val == 2 || val == 3) + ret = 600; + else if (val == 4) + ret = 700; + else if (val == 5) + ret = 800; + else + ret = 900; + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, "%d\n", ret); + break; + case MAX20730_DEBUGFS_PG_DELAY: + val = (data->mfr_devset1 & MAX20730_MFR_DEVSET1_TSTAT_MASK) + >> MAX20730_MFR_DEVSET1_TSTAT_BIT_POS; + + if (val == 0) + result = "2000\n"; + else if (val == 1) + result = "125\n"; + else if (val == 2) + result = "62.5\n"; + else + result = "32\n"; + break; + case MAX20730_DEBUGFS_INTERNAL_GAIN: + val = (data->mfr_devset1 & MAX20730_MFR_DEVSET1_RGAIN_MASK) + >> MAX20730_MFR_DEVSET1_RGAIN_BIT_POS; + + if (data->id == max20734) { + /* AN6209 */ + if (val == 0) + result = "0.8\n"; + else if (val == 1) + result = "3.2\n"; + else if (val == 2) + result = "1.6\n"; + else + result = "6.4\n"; + } else if (data->id == max20730 || data->id == max20710) { + /* AN6042 or AN6140 */ + if (val == 0) + result = "0.9\n"; + else if (val == 1) + result = "3.6\n"; + else if (val == 2) + result = "1.8\n"; + else + result = "7.2\n"; + } else if (data->id == max20743) { + /* AN6042 */ + if (val == 0) + result = "0.45\n"; + else if (val == 1) + result = "1.8\n"; + else if (val == 2) + result = "0.9\n"; + else + result = "3.6\n"; + } else { + result = "Not supported\n"; + } + break; + case MAX20730_DEBUGFS_BOOT_VOLTAGE: + val = (data->mfr_devset1 & MAX20730_MFR_DEVSET1_VBOOT_MASK) + >> MAX20730_MFR_DEVSET1_VBOOT_BIT_POS; + + if (val == 0) + result = "0.6484\n"; + else if (val == 1) + result = "0.8984\n"; + else if (val == 2) + result = "1.0\n"; + else + result = "Invalid\n"; + break; + case MAX20730_DEBUGFS_OUT_V_RAMP_RATE: + val = (data->mfr_devset2 & MAX20730_MFR_DEVSET2_VRATE) + >> MAX20730_MFR_DEVSET2_VRATE_BIT_POS; + + if (val == 0) + result = "4\n"; + else if (val == 1) + result = "2\n"; + else if (val == 2) + result = "1\n"; + else + result = "Invalid\n"; + break; + case MAX20730_DEBUGFS_OC_PROTECT_MODE: + ret = (data->mfr_devset2 & MAX20730_MFR_DEVSET2_OCPM_MASK) + >> MAX20730_MFR_DEVSET2_OCPM_BIT_POS; + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, "%d\n", ret); + break; + case MAX20730_DEBUGFS_SS_TIMING: + val = (data->mfr_devset2 & MAX20730_MFR_DEVSET2_SS_MASK) + >> MAX20730_MFR_DEVSET2_SS_BIT_POS; + + if (val == 0) + result = "0.75\n"; + else if (val == 1) + result = "1.5\n"; + else if (val == 2) + result = "3\n"; + else + result = "6\n"; + break; + case MAX20730_DEBUGFS_IMAX: + ret = (data->mfr_devset2 & MAX20730_MFR_DEVSET2_IMAX_MASK) + >> MAX20730_MFR_DEVSET2_IMAX_BIT_POS; + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, "%d\n", ret); + break; + case MAX20730_DEBUGFS_OPERATION: + ret = i2c_smbus_read_byte_data(psu->client, PMBUS_OPERATION); + if (ret < 0) + return ret; + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, "%d\n", ret); + break; + case MAX20730_DEBUGFS_ON_OFF_CONFIG: + ret = i2c_smbus_read_byte_data(psu->client, PMBUS_ON_OFF_CONFIG); + if (ret < 0) + return ret; + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, "%d\n", ret); + break; + case MAX20730_DEBUGFS_SMBALERT_MASK: + ret = i2c_smbus_read_word_data(psu->client, + PMBUS_SMB_ALERT_MASK); + if (ret < 0) + return ret; + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, "%d\n", ret); + break; + case MAX20730_DEBUGFS_VOUT_MODE: + ret = i2c_smbus_read_byte_data(psu->client, PMBUS_VOUT_MODE); + if (ret < 0) + return ret; + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, "%d\n", ret); + break; + case MAX20730_DEBUGFS_VOUT_COMMAND: + ret = i2c_smbus_read_word_data(psu->client, PMBUS_VOUT_COMMAND); + if (ret < 0) + return ret; + + ret = VOLT_FROM_REG(ret * 10000); + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, + "%d.%d\n", ret / 10000, ret % 10000); + break; + case MAX20730_DEBUGFS_VOUT_MAX: + ret = i2c_smbus_read_word_data(psu->client, PMBUS_VOUT_MAX); + if (ret < 0) + return ret; + + ret = VOLT_FROM_REG(ret * 10000); + len = scnprintf(tbuf, DEBUG_FS_DATA_MAX, + "%d.%d\n", ret / 10000, ret % 10000); + break; + default: + result = "Invalid\n"; + } + + len = strlen(result); + return simple_read_from_buffer(buf, count, ppos, result, len); +} + +static const struct file_operations max20730_fops = { + .llseek = noop_llseek, + .read = max20730_debugfs_read, + .write = NULL, + .open = simple_open, +}; + +static int max20730_init_debugfs(struct i2c_client *client, + struct max20730_data *data) +{ + int ret, i; + struct dentry *debugfs; + struct dentry *max20730_dir; + struct max20730_debugfs_data *psu; + + ret = i2c_smbus_read_word_data(client, MAX20730_MFR_DEVSET2); + if (ret < 0) + return ret; + data->mfr_devset2 = ret; + + ret = i2c_smbus_read_word_data(client, MAX20730_MFR_VOUT_MIN); + if (ret < 0) + return ret; + data->mfr_voutmin = ret; + + psu = devm_kzalloc(&client->dev, sizeof(*psu), GFP_KERNEL); + if (!psu) + return -ENOMEM; + psu->client = client; + + debugfs = pmbus_get_debugfs_dir(client); + if (!debugfs) + return -ENOENT; + + max20730_dir = debugfs_create_dir(client->name, debugfs); + + for (i = 0; i < MAX20730_DEBUGFS_NUM_ENTRIES; ++i) + psu->debugfs_entries[i] = i; + + debugfs_create_file("vout_min", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_VOUT_MIN], + &max20730_fops); + debugfs_create_file("frequency", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_FREQUENCY], + &max20730_fops); + debugfs_create_file("power_good_delay", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_PG_DELAY], + &max20730_fops); + debugfs_create_file("internal_gain", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_INTERNAL_GAIN], + &max20730_fops); + debugfs_create_file("boot_voltage", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_BOOT_VOLTAGE], + &max20730_fops); + debugfs_create_file("out_voltage_ramp_rate", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_OUT_V_RAMP_RATE], + &max20730_fops); + debugfs_create_file("oc_protection_mode", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_OC_PROTECT_MODE], + &max20730_fops); + debugfs_create_file("soft_start_timing", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_SS_TIMING], + &max20730_fops); + debugfs_create_file("imax", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_IMAX], + &max20730_fops); + debugfs_create_file("operation", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_OPERATION], + &max20730_fops); + debugfs_create_file("on_off_config", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_ON_OFF_CONFIG], + &max20730_fops); + debugfs_create_file("smbalert_mask", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_SMBALERT_MASK], + &max20730_fops); + debugfs_create_file("vout_mode", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_VOUT_MODE], + &max20730_fops); + debugfs_create_file("vout_command", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_VOUT_COMMAND], + &max20730_fops); + debugfs_create_file("vout_max", 0444, max20730_dir, + &psu->debugfs_entries[MAX20730_DEBUGFS_VOUT_MAX], + &max20730_fops); + + return 0; +} +#else +static int max20730_init_debugfs(struct i2c_client *client, + struct max20730_data *data) +{ + return 0; +} +#endif /* CONFIG_DEBUG_FS */ + +static const struct i2c_device_id max20730_id[]; + +/* + * Convert discreet value to direct data format. Strictly speaking, all passed + * values are constants, so we could do that calculation manually. On the + * downside, that would make the driver more difficult to maintain, so lets + * use this approach. + */ +static u16 val_to_direct(int v, enum pmbus_sensor_classes class, + const struct pmbus_driver_info *info) +{ + int R = info->R[class] - 3; /* take milli-units into account */ + int b = info->b[class] * 1000; + long d; + + d = v * info->m[class] + b; + /* + * R < 0 is true for all callers, so we don't need to bother + * about the R > 0 case. + */ + while (R < 0) { + d = DIV_ROUND_CLOSEST(d, 10); + R++; + } + return (u16)d; +} + +static long direct_to_val(u16 w, enum pmbus_sensor_classes class, + const struct pmbus_driver_info *info) +{ + int R = info->R[class] - 3; + int b = info->b[class] * 1000; + int m = info->m[class]; + long d = (s16)w; + + if (m == 0) + return 0; + + while (R < 0) { + d *= 10; + R++; + } + d = (d - b) / m; + return d; +} + +static u32 max_current[][5] = { + [max20710] = { 6200, 8000, 9700, 11600 }, + [max20730] = { 13000, 16600, 20100, 23600 }, + [max20734] = { 21000, 27000, 32000, 38000 }, + [max20743] = { 18900, 24100, 29200, 34100 }, +}; + +static int max20730_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct max20730_data *data = to_max20730_data(info); + int ret = 0; + u32 max_c; + + switch (reg) { + case PMBUS_OT_FAULT_LIMIT: + switch ((data->mfr_devset1 >> 11) & 0x3) { + case 0x0: + ret = val_to_direct(150000, PSC_TEMPERATURE, info); + break; + case 0x1: + ret = val_to_direct(130000, PSC_TEMPERATURE, info); + break; + default: + ret = -ENODATA; + break; + } + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + max_c = max_current[data->id][(data->mfr_devset1 >> 5) & 0x3]; + ret = val_to_direct(max_c, PSC_CURRENT_OUT, info); + break; + case PMBUS_READ_VOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret > 0 && data->vout_voltage_divider[0] && data->vout_voltage_divider[1]) { + u64 temp = DIV_ROUND_CLOSEST_ULL((u64)ret * data->vout_voltage_divider[1], + data->vout_voltage_divider[0]); + ret = clamp_val(temp, 0, 0xffff); + } + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int max20730_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + struct pmbus_driver_info *info; + struct max20730_data *data; + u16 devset1; + int ret = 0; + int idx; + + info = (struct pmbus_driver_info *)pmbus_get_driver_info(client); + data = to_max20730_data(info); + + mutex_lock(&data->lock); + devset1 = data->mfr_devset1; + + switch (reg) { + case PMBUS_OT_FAULT_LIMIT: + devset1 &= ~(BIT(11) | BIT(12)); + if (direct_to_val(word, PSC_TEMPERATURE, info) < 140000) + devset1 |= BIT(11); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + devset1 &= ~(BIT(5) | BIT(6)); + + idx = find_closest(direct_to_val(word, PSC_CURRENT_OUT, info), + max_current[data->id], 4); + devset1 |= (idx << 5); + break; + default: + ret = -ENODATA; + break; + } + + if (!ret && devset1 != data->mfr_devset1) { + ret = i2c_smbus_write_word_data(client, MAX20730_MFR_DEVSET1, + devset1); + if (!ret) { + data->mfr_devset1 = devset1; + pmbus_clear_cache(client); + } + } + mutex_unlock(&data->lock); + return ret; +} + +static const struct pmbus_driver_info max20730_info[] = { + [max20710] = { + .pages = 1, + .read_word_data = max20730_read_word_data, + .write_word_data = max20730_write_word_data, + + /* Source : Maxim AN6140 and AN6042 */ + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 21, + .b[PSC_TEMPERATURE] = 5887, + .R[PSC_TEMPERATURE] = -1, + + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_VOLTAGE_IN] = 3609, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -2, + + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_CURRENT_OUT] = 153, + .b[PSC_CURRENT_OUT] = 4976, + .R[PSC_CURRENT_OUT] = -1, + + .format[PSC_VOLTAGE_OUT] = linear, + + .func[0] = PMBUS_HAVE_VIN | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_INPUT, + }, + [max20730] = { + .pages = 1, + .read_word_data = max20730_read_word_data, + .write_word_data = max20730_write_word_data, + + /* Source : Maxim AN6042 */ + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 21, + .b[PSC_TEMPERATURE] = 5887, + .R[PSC_TEMPERATURE] = -1, + + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_VOLTAGE_IN] = 3609, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -2, + + /* + * Values in the datasheet are adjusted for temperature and + * for the relationship between Vin and Vout. + * Unfortunately, the data sheet suggests that Vout measurement + * may be scaled with a resistor array. This is indeed the case + * at least on the evaulation boards. As a result, any in-driver + * adjustments would either be wrong or require elaborate means + * to configure the scaling. Instead of doing that, just report + * raw values and let userspace handle adjustments. + */ + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_CURRENT_OUT] = 153, + .b[PSC_CURRENT_OUT] = 4976, + .R[PSC_CURRENT_OUT] = -1, + + .format[PSC_VOLTAGE_OUT] = linear, + + .func[0] = PMBUS_HAVE_VIN | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_INPUT, + }, + [max20734] = { + .pages = 1, + .read_word_data = max20730_read_word_data, + .write_word_data = max20730_write_word_data, + + /* Source : Maxim AN6209 */ + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 21, + .b[PSC_TEMPERATURE] = 5887, + .R[PSC_TEMPERATURE] = -1, + + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_VOLTAGE_IN] = 3592, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -2, + + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_CURRENT_OUT] = 111, + .b[PSC_CURRENT_OUT] = 3461, + .R[PSC_CURRENT_OUT] = -1, + + .format[PSC_VOLTAGE_OUT] = linear, + + .func[0] = PMBUS_HAVE_VIN | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_INPUT, + }, + [max20743] = { + .pages = 1, + .read_word_data = max20730_read_word_data, + .write_word_data = max20730_write_word_data, + + /* Source : Maxim AN6042 */ + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 21, + .b[PSC_TEMPERATURE] = 5887, + .R[PSC_TEMPERATURE] = -1, + + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_VOLTAGE_IN] = 3597, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -2, + + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_CURRENT_OUT] = 95, + .b[PSC_CURRENT_OUT] = 5014, + .R[PSC_CURRENT_OUT] = -1, + + .format[PSC_VOLTAGE_OUT] = linear, + + .func[0] = PMBUS_HAVE_VIN | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_INPUT, + }, +}; + +static int max20730_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + struct max20730_data *data; + enum chips chip_id; + int ret; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA | + I2C_FUNC_SMBUS_READ_WORD_DATA | + I2C_FUNC_SMBUS_BLOCK_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, buf); + if (ret < 0) { + dev_err(&client->dev, "Failed to read Manufacturer ID\n"); + return ret; + } + if (ret != 5 || strncmp(buf, "MAXIM", 5)) { + buf[ret] = '\0'; + dev_err(dev, "Unsupported Manufacturer ID '%s'\n", buf); + return -ENODEV; + } + + /* + * The chips support reading PMBUS_MFR_MODEL. On both MAX20730 + * and MAX20734, reading it returns M20743. Presumably that is + * the reason why the command is not documented. Unfortunately, + * that means that there is no reliable means to detect the chip. + * However, we can at least detect the chip series. Compare + * the returned value against 'M20743' and bail out if there is + * a mismatch. If that doesn't work for all chips, we may have + * to remove this check. + */ + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) { + dev_err(dev, "Failed to read Manufacturer Model\n"); + return ret; + } + if (ret != 6 || strncmp(buf, "M20743", 6)) { + buf[ret] = '\0'; + dev_err(dev, "Unsupported Manufacturer Model '%s'\n", buf); + return -ENODEV; + } + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_REVISION, buf); + if (ret < 0) { + dev_err(dev, "Failed to read Manufacturer Revision\n"); + return ret; + } + if (ret != 1 || buf[0] != 'F') { + buf[ret] = '\0'; + dev_err(dev, "Unsupported Manufacturer Revision '%s'\n", buf); + return -ENODEV; + } + + chip_id = (uintptr_t)i2c_get_match_data(client); + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + data->id = chip_id; + mutex_init(&data->lock); + memcpy(&data->info, &max20730_info[chip_id], sizeof(data->info)); + if (of_property_read_u32_array(client->dev.of_node, "vout-voltage-divider", + data->vout_voltage_divider, + ARRAY_SIZE(data->vout_voltage_divider)) != 0) + memset(data->vout_voltage_divider, 0, sizeof(data->vout_voltage_divider)); + if (data->vout_voltage_divider[1] < data->vout_voltage_divider[0]) { + dev_err(dev, + "The total resistance of voltage divider is less than output resistance\n"); + return -EINVAL; + } + + ret = i2c_smbus_read_word_data(client, MAX20730_MFR_DEVSET1); + if (ret < 0) + return ret; + data->mfr_devset1 = ret; + + ret = pmbus_do_probe(client, &data->info); + if (ret < 0) + return ret; + + ret = max20730_init_debugfs(client, data); + if (ret) + dev_warn(dev, "Failed to register debugfs: %d\n", + ret); + + return 0; +} + +static const struct i2c_device_id max20730_id[] = { + { .name = "max20710", .driver_data = max20710 }, + { .name = "max20730", .driver_data = max20730 }, + { .name = "max20734", .driver_data = max20734 }, + { .name = "max20743", .driver_data = max20743 }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, max20730_id); + +static const struct of_device_id max20730_of_match[] = { + { .compatible = "maxim,max20710", .data = (void *)max20710 }, + { .compatible = "maxim,max20730", .data = (void *)max20730 }, + { .compatible = "maxim,max20734", .data = (void *)max20734 }, + { .compatible = "maxim,max20743", .data = (void *)max20743 }, + { }, +}; + +MODULE_DEVICE_TABLE(of, max20730_of_match); + +static struct i2c_driver max20730_driver = { + .driver = { + .name = "max20730", + .of_match_table = max20730_of_match, + }, + .probe = max20730_probe, + .id_table = max20730_id, +}; + +module_i2c_driver(max20730_driver); + +MODULE_AUTHOR("Guenter Roeck <linux@roeck-us.net>"); +MODULE_DESCRIPTION("PMBus driver for Maxim MAX20710 / MAX20730 / MAX20734 / MAX20743"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max20751.c b/drivers/hwmon/pmbus/max20751.c new file mode 100644 index 000000000..e85676433 --- /dev/null +++ b/drivers/hwmon/pmbus/max20751.c @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Maxim MAX20751 + * + * Copyright (c) 2015 Guenter Roeck + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include "pmbus.h" + +static struct pmbus_driver_info max20751_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = vid, + .vrm_version[0] = vr12, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_POUT, +}; + +static int max20751_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &max20751_info); +} + +static const struct i2c_device_id max20751_id[] = { + { .name = "max20751" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, max20751_id); + +static struct i2c_driver max20751_driver = { + .driver = { + .name = "max20751", + }, + .probe = max20751_probe, + .id_table = max20751_id, +}; + +module_i2c_driver(max20751_driver); + +MODULE_AUTHOR("Guenter Roeck <linux@roeck-us.net>"); +MODULE_DESCRIPTION("PMBus driver for Maxim MAX20751"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max20830.c b/drivers/hwmon/pmbus/max20830.c new file mode 100644 index 000000000..4d25a084a --- /dev/null +++ b/drivers/hwmon/pmbus/max20830.c @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Hardware monitoring driver for Analog Devices MAX20830 + * + * Copyright (C) 2026 Analog Devices, Inc. + */ + +#include <linux/errno.h> +#include <linux/i2c.h> +#include <linux/math64.h> +#include <linux/module.h> +#include <linux/string.h> +#include "pmbus.h" + +#define MAX20830_IC_DEVICE_ID_LENGTH 9 + +struct max20830_data { + struct pmbus_driver_info info; + u32 vout_rfb1; + u32 vout_rfb2; +}; + +static const char * const supported_chip_ids[] = { + "MAX20830", + "MAX20830C", + "MAX20840C", +}; + +/* + * MAX20830 only supports READ_VOUT for VOUT monitoring. + * + * Limit registers (VOUT_OV_WARN_LIMIT, VOUT_OV_FAULT_LIMIT, etc.) are not + * supported by this driver and return -ENODATA. This means sysfs attributes + * like in1_max, in1_crit, etc. will not be available. Only in1_input (the + * scaled output voltage) is supported. + * + * MAX20830 uses an external resistor divider for voltage sensing: + * - VOUT_COMMAND and VOUT_MAX set the reference voltage at the feedback pin + * - READ_VOUT reports the feedback voltage, which needs to be scaled for actual + * output voltage + * + * Scaling formula: vout_actual = vout_fb × (1 + RFB1 / RFB2) + * + * If regulator support is added in the future, some adjustments are needed to + * ensure correct feedback voltages are set. + */ +static int max20830_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct max20830_data *data = container_of(info, struct max20830_data, info); + int ret; + u64 temp; + + switch (reg) { + case PMBUS_READ_VOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + /* Apply voltage divider scaling if resistors are non-zero */ + if (data->vout_rfb1 && data->vout_rfb2) { + temp = (u64)data->vout_rfb1 + (u64)data->vout_rfb2; + temp = DIV_ROUND_CLOSEST_ULL((u64)ret * temp, data->vout_rfb2); + ret = clamp_val(temp, 0, 0xFFFF); + } + return ret; + default: + return -ENODATA; + } +} + +static struct pmbus_driver_info max20830_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_TEMP | + PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, + .read_word_data = max20830_read_word_data, + .have_pmbus_revision = true, + .pmbus_revision = PMBUS_REV_13, +}; + +static int max20830_probe(struct i2c_client *client) +{ + u8 buf[I2C_SMBUS_BLOCK_MAX + 1] = {}; + struct max20830_data *data; + int i, ret; + + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->info = max20830_info; + + /* Read optional voltage divider resistor values */ + device_property_read_u32(&client->dev, "adi,vout-rfb1-ohms", &data->vout_rfb1); + device_property_read_u32(&client->dev, "adi,vout-rfb2-ohms", &data->vout_rfb2); + + ret = pmbus_read_smbus_i2c_block_data(client, PMBUS_IC_DEVICE_ID, buf); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "Failed to read IC_DEVICE_ID\n"); + + /* Verify we read the expected number of bytes */ + if (ret < MAX20830_IC_DEVICE_ID_LENGTH) + return dev_err_probe(&client->dev, -ENODEV, + "IC_DEVICE_ID too short: expected %d bytes, got %d\n", + MAX20830_IC_DEVICE_ID_LENGTH, ret); + + /* Null-terminate the string */ + buf[ret] = '\0'; + + /* Verify the device ID matches what we expect */ + for (i = 0; i < ARRAY_SIZE(supported_chip_ids); i++) { + if (!strcmp(buf, supported_chip_ids[i])) + break; + } + + /* No match found - unsupported device */ + if (i == ARRAY_SIZE(supported_chip_ids)) + return dev_err_probe(&client->dev, -ENODEV, + "Unsupported device: '%*pE'\n", ret, buf); + + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id max20830_id[] = { + {"max20830"}, + { } +}; +MODULE_DEVICE_TABLE(i2c, max20830_id); + +static const struct of_device_id max20830_of_match[] = { + { .compatible = "adi,max20830" }, + { } +}; +MODULE_DEVICE_TABLE(of, max20830_of_match); + +static struct i2c_driver max20830_driver = { + .driver = { + .name = "max20830", + .of_match_table = max20830_of_match, + }, + .probe = max20830_probe, + .id_table = max20830_id, +}; + +module_i2c_driver(max20830_driver); + +MODULE_AUTHOR("Alexis Czezar Torreno <alexisczezar.torreno@analog.com>"); +MODULE_DESCRIPTION("PMBus driver for Analog Devices MAX20830"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max20860a.c b/drivers/hwmon/pmbus/max20860a.c new file mode 100644 index 000000000..905f916f6 --- /dev/null +++ b/drivers/hwmon/pmbus/max20860a.c @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Analog Devices MAX20860A + * + * SPDX-FileCopyrightText: Copyright Hewlett Packard Enterprise Development LP + */ + +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/regulator/driver.h> +#include "pmbus.h" + +#if IS_ENABLED(CONFIG_SENSORS_MAX20860A_REGULATOR) +static const struct regulator_desc max20860a_reg_desc[] = { + PMBUS_REGULATOR_ONE("vout"), +}; +#endif + +static struct pmbus_driver_info max20860a_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | + PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | + PMBUS_HAVE_STATUS_TEMP | PMBUS_HAVE_STATUS_INPUT, +#if IS_ENABLED(CONFIG_SENSORS_MAX20860A_REGULATOR) + .num_regulators = 1, + .reg_desc = max20860a_reg_desc, +#endif +}; + +static int max20860a_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &max20860a_info); +} + +static const struct i2c_device_id max20860a_id[] = { + {"max20860a"}, + {} +}; +MODULE_DEVICE_TABLE(i2c, max20860a_id); + +static const struct of_device_id max20860a_of_match[] = { + { .compatible = "adi,max20860a" }, + {} +}; +MODULE_DEVICE_TABLE(of, max20860a_of_match); + +static struct i2c_driver max20860a_driver = { + .driver = { + .name = "max20860a", + .of_match_table = max20860a_of_match, + }, + .probe = max20860a_probe, + .id_table = max20860a_id, +}; + +module_i2c_driver(max20860a_driver); + +MODULE_AUTHOR("Syed Arif <arif.syed@hpe.com>"); +MODULE_AUTHOR("Sanman Pradhan <psanman@juniper.net>"); +MODULE_DESCRIPTION("PMBus driver for Analog Devices MAX20860A"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max31785.c b/drivers/hwmon/pmbus/max31785.c new file mode 100644 index 000000000..bdcce810a --- /dev/null +++ b/drivers/hwmon/pmbus/max31785.c @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2017 IBM Corp. + */ + +#include <linux/delay.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include "pmbus.h" + +enum max31785_regs { + MFR_REVISION = 0x9b, + MFR_FAN_CONFIG = 0xf1, +}; + +#define MAX31785 0x3030 +#define MAX31785A 0x3040 +#define MAX31785B 0x3061 + +#define MFR_FAN_CONFIG_DUAL_TACH BIT(12) + +#define MAX31785_NR_PAGES 23 +#define MAX31785_NR_FAN_PAGES 6 +#define MAX31785_WAIT_DELAY_US 250 + +struct max31785_data { + ktime_t access; /* Chip access time */ + struct pmbus_driver_info info; +}; + +/* + * MAX31785 Driver Workaround + * + * The MAX31785 fan controller occasionally exhibits communication issues. + * These issues are not indicated by the device itself, except for occasional + * NACK responses during master transactions. No error bits are set in STATUS_BYTE. + * + * Keep minimal local delay handling for raw pre-probe SMBus accesses. + * Normal PMBus-mediated accesses use pmbus_driver_info.access_delay instead. + */ +static inline void max31785_wait(const struct max31785_data *data) +{ + s64 delta = ktime_us_delta(ktime_get(), data->access); + + if (delta < MAX31785_WAIT_DELAY_US) + usleep_range(MAX31785_WAIT_DELAY_US - delta, + MAX31785_WAIT_DELAY_US); +} + +static int max31785_i2c_write_byte_data(struct i2c_client *client, + struct max31785_data *data, + int command, u8 value) +{ + int rc; + + max31785_wait(data); + rc = i2c_smbus_write_byte_data(client, command, value); + data->access = ktime_get(); + return rc; +} + +static int max31785_i2c_read_word_data(struct i2c_client *client, + struct max31785_data *data, + int command) +{ + int rc; + + max31785_wait(data); + rc = i2c_smbus_read_word_data(client, command); + data->access = ktime_get(); + return rc; +} + +static int max31785_read_byte_data(struct i2c_client *client, int page, int reg) +{ + switch (reg) { + case PMBUS_VOUT_MODE: + return -ENOTSUPP; + case PMBUS_FAN_CONFIG_12: + if (page < MAX31785_NR_PAGES) + return -ENODATA; + return pmbus_read_byte_data(client, + page - MAX31785_NR_PAGES, + reg); + } + + return -ENODATA; +} + +static int max31785_write_byte(struct i2c_client *client, int page, u8 value) +{ + if (page < MAX31785_NR_PAGES) + return -ENODATA; + + return -ENOTSUPP; +} + +static int max31785_read_long_data(struct i2c_client *client, int page, + int reg, u32 *data) +{ + unsigned char cmdbuf[1]; + unsigned char rspbuf[4]; + int rc; + + struct i2c_msg msg[2] = { + { + .addr = client->addr, + .flags = 0, + .len = sizeof(cmdbuf), + .buf = cmdbuf, + }, + { + .addr = client->addr, + .flags = I2C_M_RD, + .len = sizeof(rspbuf), + .buf = rspbuf, + }, + }; + + cmdbuf[0] = reg; + + rc = pmbus_set_page(client, page, 0xff); + if (rc < 0) + return rc; + + /* + * Ensure the raw transfer is properly spaced from the + * preceding PMBus transaction. + */ + pmbus_wait(client); + + rc = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg)); + + /* + * Update PMBus core timing state for the raw transfer, even on error. + * Pass 0 as the operation mask since this is a raw read, intentionally + * neither PMBUS_OP_WRITE nor PMBUS_OP_PAGE_CHANGE. + */ + pmbus_update_ts(client, 0); + + if (rc != ARRAY_SIZE(msg)) + return rc < 0 ? rc : -EIO; + + *data = (rspbuf[0] << (0 * 8)) | (rspbuf[1] << (1 * 8)) | + (rspbuf[2] << (2 * 8)) | (rspbuf[3] << (3 * 8)); + + return 0; +} + +static int max31785_get_pwm(struct i2c_client *client, int page) +{ + int rv; + + rv = pmbus_get_fan_rate_device(client, page, 0, percent); + if (rv < 0) + return rv; + else if (rv >= 0x8000) + return 0; + else if (rv >= 0x2711) + return 0x2710; + + return rv; +} + +static int max31785_get_pwm_mode(struct i2c_client *client, int page) +{ + int config; + int command; + + config = pmbus_read_byte_data(client, page, PMBUS_FAN_CONFIG_12); + if (config < 0) + return config; + + command = pmbus_read_word_data(client, page, 0xff, PMBUS_FAN_COMMAND_1); + if (command < 0) + return command; + + if (config & PB_FAN_1_RPM) + return (command >= 0x8000) ? 3 : 2; + + if (command >= 0x8000) + return 3; + else if (command >= 0x2711) + return 0; + + return 1; +} + +static int max31785_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + u32 val; + int rv; + + switch (reg) { + case PMBUS_READ_FAN_SPEED_1: + if (page < MAX31785_NR_PAGES) + return -ENODATA; + + rv = max31785_read_long_data(client, page - MAX31785_NR_PAGES, + reg, &val); + if (rv < 0) + return rv; + + rv = (val >> 16) & 0xffff; + break; + case PMBUS_FAN_COMMAND_1: + /* + * PMBUS_FAN_COMMAND_x is probed to judge whether or not to + * expose fan control registers. + * + * Don't expose fan_target attribute for virtual pages. + */ + rv = (page >= MAX31785_NR_PAGES) ? -ENOTSUPP : -ENODATA; + break; + case PMBUS_VIRT_PWM_1: + rv = max31785_get_pwm(client, page); + break; + case PMBUS_VIRT_PWM_ENABLE_1: + rv = max31785_get_pwm_mode(client, page); + break; + default: + rv = -ENODATA; + break; + } + + return rv; +} + +static inline u32 max31785_scale_pwm(u32 sensor_val) +{ + /* + * The datasheet describes the accepted value range for manual PWM as + * [0, 0x2710], while the hwmon pwmX sysfs interface accepts values in + * [0, 255]. The MAX31785 uses DIRECT mode to scale the FAN_COMMAND + * registers and in PWM mode the coefficients are m=1, b=0, R=2. The + * important observation here is that 0x2710 == 10000 == 100 * 100. + * + * R=2 (== 10^2 == 100) accounts for scaling the value provided at the + * sysfs interface into the required hardware resolution, but it does + * not yet yield a value that we can write to the device (this initial + * scaling is handled by pmbus_data2reg()). Multiplying by 100 below + * translates the parameter value into the percentage units required by + * PMBus, and then we scale back by 255 as required by the hwmon pwmX + * interface to yield the percentage value at the appropriate + * resolution for hardware. + */ + return (sensor_val * 100) / 255; +} + +static int max31785_pwm_enable(struct i2c_client *client, int page, + u16 word) +{ + int config = 0; + int rate; + + switch (word) { + case 0: + rate = 0x7fff; + break; + case 1: + rate = pmbus_get_fan_rate_cached(client, page, 0, percent); + if (rate < 0) + return rate; + rate = max31785_scale_pwm(rate); + break; + case 2: + config = PB_FAN_1_RPM; + rate = pmbus_get_fan_rate_cached(client, page, 0, rpm); + if (rate < 0) + return rate; + break; + case 3: + rate = 0xffff; + break; + default: + return -EINVAL; + } + + return pmbus_update_fan(client, page, 0, config, + PB_FAN_1_RPM, rate); +} + +static int max31785_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + switch (reg) { + case PMBUS_VIRT_PWM_1: + return pmbus_update_fan(client, page, 0, 0, + PB_FAN_1_RPM, + max31785_scale_pwm(word)); + case PMBUS_VIRT_PWM_ENABLE_1: + return max31785_pwm_enable(client, page, word); + default: + break; + } + + return -ENODATA; +} + +#define MAX31785_FAN_FUNCS \ + (PMBUS_HAVE_FAN12 | PMBUS_HAVE_STATUS_FAN12 | PMBUS_HAVE_PWM12) + +#define MAX31785_TEMP_FUNCS \ + (PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP) + +#define MAX31785_VOUT_FUNCS \ + (PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT) + +static const struct pmbus_driver_info max31785_info = { + .pages = MAX31785_NR_PAGES, + + .write_word_data = max31785_write_word_data, + .read_byte_data = max31785_read_byte_data, + .read_word_data = max31785_read_word_data, + .write_byte = max31785_write_byte, + .access_delay = MAX31785_WAIT_DELAY_US, + + /* RPM */ + .format[PSC_FAN] = direct, + .m[PSC_FAN] = 1, + .b[PSC_FAN] = 0, + .R[PSC_FAN] = 0, + /* PWM */ + .format[PSC_PWM] = direct, + .m[PSC_PWM] = 1, + .b[PSC_PWM] = 0, + .R[PSC_PWM] = 2, + .func[0] = MAX31785_FAN_FUNCS, + .func[1] = MAX31785_FAN_FUNCS, + .func[2] = MAX31785_FAN_FUNCS, + .func[3] = MAX31785_FAN_FUNCS, + .func[4] = MAX31785_FAN_FUNCS, + .func[5] = MAX31785_FAN_FUNCS, + + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + .func[6] = MAX31785_TEMP_FUNCS, + .func[7] = MAX31785_TEMP_FUNCS, + .func[8] = MAX31785_TEMP_FUNCS, + .func[9] = MAX31785_TEMP_FUNCS, + .func[10] = MAX31785_TEMP_FUNCS, + .func[11] = MAX31785_TEMP_FUNCS, + .func[12] = MAX31785_TEMP_FUNCS, + .func[13] = MAX31785_TEMP_FUNCS, + .func[14] = MAX31785_TEMP_FUNCS, + .func[15] = MAX31785_TEMP_FUNCS, + .func[16] = MAX31785_TEMP_FUNCS, + + .format[PSC_VOLTAGE_OUT] = direct, + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .func[17] = MAX31785_VOUT_FUNCS, + .func[18] = MAX31785_VOUT_FUNCS, + .func[19] = MAX31785_VOUT_FUNCS, + .func[20] = MAX31785_VOUT_FUNCS, + .func[21] = MAX31785_VOUT_FUNCS, + .func[22] = MAX31785_VOUT_FUNCS, +}; + +static int max31785_configure_dual_tach(struct i2c_client *client, + struct max31785_data *data) +{ + struct pmbus_driver_info *info = &data->info; + int ret; + int i; + + for (i = 0; i < MAX31785_NR_FAN_PAGES; i++) { + ret = max31785_i2c_write_byte_data(client, data, + PMBUS_PAGE, i); + if (ret < 0) + return ret; + + ret = max31785_i2c_read_word_data(client, data, + MFR_FAN_CONFIG); + if (ret < 0) + return ret; + + if (ret & MFR_FAN_CONFIG_DUAL_TACH) { + int vpage = MAX31785_NR_PAGES + i; + + info->pages = vpage + 1; + info->func[vpage] |= PMBUS_HAVE_FAN12; + info->func[vpage] |= PMBUS_PAGE_VIRTUAL; + } + } + + return 0; +} + +static int max31785_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct pmbus_driver_info *info; + struct max31785_data *data; + bool dual_tach = false; + int ret; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_BYTE_DATA | + I2C_FUNC_SMBUS_WORD_DATA)) + return -ENODEV; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->access = ktime_get(); + info = &data->info; + *info = max31785_info; + + ret = max31785_i2c_write_byte_data(client, data, + PMBUS_PAGE, 0xff); + if (ret < 0) + return ret; + + ret = max31785_i2c_read_word_data(client, data, MFR_REVISION); + if (ret < 0) + return ret; + + if (ret == MAX31785A || ret == MAX31785B) { + dual_tach = true; + } else if (ret == MAX31785) { + if (!strcmp("max31785a", client->name) || + !strcmp("max31785b", client->name)) + dev_warn(dev, "Expected max31785a/b, found max31785: cannot provide secondary tachometer readings\n"); + } else { + dev_err(dev, "Unrecognized MAX31785 revision: %x\n", ret); + return -ENODEV; + } + + if (dual_tach) { + ret = max31785_configure_dual_tach(client, data); + if (ret < 0) + return ret; + } + + max31785_wait(data); + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id max31785_id[] = { + { .name = "max31785" }, + { .name = "max31785a" }, + { .name = "max31785b" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, max31785_id); + +static const struct of_device_id max31785_of_match[] = { + { .compatible = "maxim,max31785" }, + { .compatible = "maxim,max31785a" }, + { .compatible = "maxim,max31785b" }, + { }, +}; + +MODULE_DEVICE_TABLE(of, max31785_of_match); + +static struct i2c_driver max31785_driver = { + .driver = { + .name = "max31785", + .of_match_table = max31785_of_match, + }, + .probe = max31785_probe, + .id_table = max31785_id, +}; + +module_i2c_driver(max31785_driver); + +MODULE_AUTHOR("Andrew Jeffery <andrew@aj.id.au>"); +MODULE_DESCRIPTION("PMBus driver for the Maxim MAX31785"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max34440.c b/drivers/hwmon/pmbus/max34440.c new file mode 100644 index 000000000..866d25ae8 --- /dev/null +++ b/drivers/hwmon/pmbus/max34440.c @@ -0,0 +1,844 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Maxim MAX34440/MAX34441 + * + * Copyright (c) 2011 Ericsson AB. + * Copyright (c) 2012 Guenter Roeck + */ + +#include <linux/bitops.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/delay.h> +#include "pmbus.h" + +enum chips { + adpm12160, + adpm12200, + adpm12250, + max34440, + max34441, + max34446, + max34451, + max34452, + max34460, + max34461, +}; + +/* + * Firmware is sometimes not ready if we try and read the + * data from the page immediately after setting. Maxim + * recommends 50us delay due to the chip failing to clock + * stretch long enough here. + */ +#define MAX34440_PAGE_CHANGE_DELAY 50 + +#define MAX34440_MFR_VOUT_PEAK 0xd4 +#define MAX34440_MFR_IOUT_PEAK 0xd5 +#define MAX34440_MFR_TEMPERATURE_PEAK 0xd6 +#define MAX34440_MFR_VOUT_MIN 0xd7 + +#define MAX34446_MFR_POUT_PEAK 0xe0 +#define MAX34446_MFR_POUT_AVG 0xe1 +#define MAX34446_MFR_IOUT_AVG 0xe2 +#define MAX34446_MFR_TEMPERATURE_AVG 0xe3 + +#define MAX34440_STATUS_OC_WARN BIT(0) +#define MAX34440_STATUS_OC_FAULT BIT(1) +#define MAX34440_STATUS_OT_FAULT BIT(5) +#define MAX34440_STATUS_OT_WARN BIT(6) + +/* + * The whole max344* family have IOUT_OC_WARN_LIMIT and IOUT_OC_FAULT_LIMIT + * swapped from the standard pmbus spec addresses. + * For max34451, version MAX34451ETNA6+ and later has this issue fixed. + */ +#define MAX34440_IOUT_OC_WARN_LIMIT 0x46 +#define MAX34440_IOUT_OC_FAULT_LIMIT 0x4A + +#define MAX34451ETNA6_MFR_REV 0x0012 +#define MAX34451ETNA8_MFR_REV 0x0014 + +#define MAX34451_MFR_CHANNEL_CONFIG 0xe4 +#define MAX34451_MFR_CHANNEL_CONFIG_SEL_MASK 0x3f + +struct max34440_data { + int id; + struct pmbus_driver_info info; + u8 iout_oc_warn_limit; + u8 iout_oc_fault_limit; +}; + +#define to_max34440_data(x) container_of(x, struct max34440_data, info) + +static int max34440_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct max34440_data *data = to_max34440_data(info); + + switch (reg) { + case PMBUS_IOUT_OC_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, + data->iout_oc_fault_limit); + break; + case PMBUS_IOUT_OC_WARN_LIMIT: + ret = pmbus_read_word_data(client, page, phase, + data->iout_oc_warn_limit); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_MFR_VIN_MIN: + case PMBUS_MFR_VIN_MAX: + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_IIN_OC_FAULT_LIMIT: + case PMBUS_MFR_IIN_MAX: + case PMBUS_MFR_VOUT_MIN: + case PMBUS_MFR_VOUT_MAX: + case PMBUS_IOUT_UC_FAULT_LIMIT: + case PMBUS_MFR_IOUT_MAX: + case PMBUS_UT_WARN_LIMIT: + case PMBUS_UT_FAULT_LIMIT: + case PMBUS_MFR_MAX_TEMP_1: + /* + * MAX34451/MAX34452/ADPM family do not support VIN/IIN limit + * registers, manufacturer-specific min/max registers, or + * undercurrent/undertemperature fault limits. Accessing these + * triggers CML error and asserts ALERT. + */ + if (data->id == max34451 || data->id == max34452 || + data->id == adpm12160 || data->id == adpm12200 || + data->id == adpm12250) + return -ENXIO; + ret = -ENODATA; + break; + case PMBUS_VOUT_OV_WARN_LIMIT: + if (data->id == max34452) + return -ENXIO; + return -ENODATA; + case PMBUS_VIRT_READ_VOUT_MIN: + ret = pmbus_read_word_data(client, page, phase, + MAX34440_MFR_VOUT_MIN); + break; + case PMBUS_VIRT_READ_VOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, + MAX34440_MFR_VOUT_PEAK); + break; + case PMBUS_VIRT_READ_IOUT_AVG: + if (data->id != max34446 && data->id != max34451 && + data->id != max34452 && data->id != adpm12160 && + data->id != adpm12200 && data->id != adpm12250) + return -ENXIO; + ret = pmbus_read_word_data(client, page, phase, + MAX34446_MFR_IOUT_AVG); + break; + case PMBUS_VIRT_READ_IOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, + MAX34440_MFR_IOUT_PEAK); + break; + case PMBUS_VIRT_READ_POUT_AVG: + if (data->id != max34446) + return -ENXIO; + ret = pmbus_read_word_data(client, page, phase, + MAX34446_MFR_POUT_AVG); + break; + case PMBUS_VIRT_READ_POUT_MAX: + if (data->id != max34446) + return -ENXIO; + ret = pmbus_read_word_data(client, page, phase, + MAX34446_MFR_POUT_PEAK); + break; + case PMBUS_VIRT_READ_TEMP_AVG: + if (data->id != max34446 && data->id != max34460 && + data->id != max34461) + return -ENXIO; + ret = pmbus_read_word_data(client, page, phase, + MAX34446_MFR_TEMPERATURE_AVG); + break; + case PMBUS_VIRT_READ_TEMP_MAX: + ret = pmbus_read_word_data(client, page, phase, + MAX34440_MFR_TEMPERATURE_PEAK); + break; + case PMBUS_VIRT_RESET_POUT_HISTORY: + if (data->id != max34446) + return -ENXIO; + ret = 0; + break; + case PMBUS_VIRT_RESET_VOUT_HISTORY: + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_RESET_TEMP_HISTORY: + ret = 0; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int max34440_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct max34440_data *data = to_max34440_data(info); + int ret; + + switch (reg) { + case PMBUS_IOUT_OC_FAULT_LIMIT: + ret = pmbus_write_word_data(client, page, data->iout_oc_fault_limit, + word); + break; + case PMBUS_IOUT_OC_WARN_LIMIT: + ret = pmbus_write_word_data(client, page, data->iout_oc_warn_limit, + word); + break; + case PMBUS_VOUT_OV_WARN_LIMIT: + if (data->id == max34452) + return -ENXIO; + return -ENODATA; + case PMBUS_VIRT_RESET_POUT_HISTORY: + ret = pmbus_write_word_data(client, page, + MAX34446_MFR_POUT_PEAK, 0); + if (ret) + break; + ret = pmbus_write_word_data(client, page, + MAX34446_MFR_POUT_AVG, 0); + break; + case PMBUS_VIRT_RESET_VOUT_HISTORY: + ret = pmbus_write_word_data(client, page, + MAX34440_MFR_VOUT_MIN, 0x7fff); + if (ret) + break; + ret = pmbus_write_word_data(client, page, + MAX34440_MFR_VOUT_PEAK, 0); + break; + case PMBUS_VIRT_RESET_IOUT_HISTORY: + ret = pmbus_write_word_data(client, page, + MAX34440_MFR_IOUT_PEAK, 0); + if (!ret && (data->id == max34446 || data->id == max34451 || + data->id == max34452 || data->id == adpm12160 || + data->id == adpm12200 || data->id == adpm12250)) + ret = pmbus_write_word_data(client, page, + MAX34446_MFR_IOUT_AVG, 0); + + break; + case PMBUS_VIRT_RESET_TEMP_HISTORY: + ret = pmbus_write_word_data(client, page, + MAX34440_MFR_TEMPERATURE_PEAK, + 0x8000); + if (!ret && data->id == max34446) + ret = pmbus_write_word_data(client, page, + MAX34446_MFR_TEMPERATURE_AVG, 0); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int max34440_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret = 0; + int mfg_status; + + if (page >= 0) { + ret = pmbus_set_page(client, page, 0xff); + if (ret < 0) + return ret; + } + + switch (reg) { + case PMBUS_STATUS_IOUT: + mfg_status = pmbus_read_word_data(client, 0, 0xff, + PMBUS_STATUS_MFR_SPECIFIC); + if (mfg_status < 0) + return mfg_status; + if (mfg_status & MAX34440_STATUS_OC_WARN) + ret |= PB_IOUT_OC_WARNING; + if (mfg_status & MAX34440_STATUS_OC_FAULT) + ret |= PB_IOUT_OC_FAULT; + break; + case PMBUS_STATUS_TEMPERATURE: + mfg_status = pmbus_read_word_data(client, 0, 0xff, + PMBUS_STATUS_MFR_SPECIFIC); + if (mfg_status < 0) + return mfg_status; + if (mfg_status & MAX34440_STATUS_OT_WARN) + ret |= PB_TEMP_OT_WARNING; + if (mfg_status & MAX34440_STATUS_OT_FAULT) + ret |= PB_TEMP_OT_FAULT; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int max34451_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct max34440_data *data = to_max34440_data(info); + + switch (reg) { + case PMBUS_STATUS_BYTE: + case PMBUS_STATUS_OTHER: + /* + * MAX34451/MAX34452/ADPM family do not support STATUS_BYTE or + * STATUS_OTHER registers. Accessing them triggers CML + * error and asserts ALERT. + */ + if (data->id == max34451 || data->id == max34452 || + data->id == adpm12160 || data->id == adpm12200 || + data->id == adpm12250) + return -ENXIO; + return -ENODATA; + default: + return -ENODATA; + } +} + +static int max34451_write_byte_data(struct i2c_client *client, int page, + int reg, u8 byte) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + const struct max34440_data *data = to_max34440_data(info); + + switch (reg) { + case PMBUS_STATUS_BYTE: + case PMBUS_STATUS_OTHER: + /* + * MAX34451/MAX34452/ADPM family do not support STATUS_BYTE or + * STATUS_OTHER registers. Writing to them triggers CML + * error and asserts ALERT. + */ + if (data->id == max34451 || data->id == max34452 || + data->id == adpm12160 || data->id == adpm12200 || + data->id == adpm12250) + return -ENXIO; + return -ENODATA; + default: + return -ENODATA; + } +} + +static int max34451_set_supported_funcs(struct i2c_client *client, + struct max34440_data *data) +{ + /* + * Each of the channel 0-15 can be configured to monitor the following + * functions based on MFR_CHANNEL_CONFIG[5:0] + * 0x10: Sequencing + voltage monitoring (only valid for PAGES 0–11) + * 0x20: Voltage monitoring (no sequencing) + * 0x21: Voltage read only + * 0x22: Current monitoring + * 0x23: Current read only + * 0x30: General-purpose input active low + * 0x34: General-purpose input active high + * 0x00: Disabled + */ + + int page, rv; + bool max34451_na6 = false; + + rv = i2c_smbus_read_word_data(client, PMBUS_MFR_REVISION); + if (rv < 0) + return rv; + + if (data->id == max34451 && rv >= MAX34451ETNA6_MFR_REV) { + max34451_na6 = true; + data->info.format[PSC_VOLTAGE_IN] = direct; + data->info.format[PSC_CURRENT_IN] = direct; + + data->info.b[PSC_VOLTAGE_IN] = 0; + data->info.b[PSC_CURRENT_IN] = 0; + if (rv >= MAX34451ETNA8_MFR_REV) { + data->info.m[PSC_VOLTAGE_IN] = 125; + data->info.R[PSC_VOLTAGE_IN] = 0; + data->info.m[PSC_VOLTAGE_OUT] = 125; + data->info.R[PSC_VOLTAGE_OUT] = 0; + data->info.m[PSC_CURRENT_IN] = 250; + data->info.R[PSC_CURRENT_IN] = -1; + data->info.m[PSC_CURRENT_OUT] = 250; + data->info.R[PSC_CURRENT_OUT] = -1; + } else { + data->info.m[PSC_VOLTAGE_IN] = 1; + data->info.R[PSC_VOLTAGE_IN] = 3; + data->info.m[PSC_CURRENT_IN] = 1; + data->info.R[PSC_CURRENT_IN] = 2; + } + data->iout_oc_fault_limit = PMBUS_IOUT_OC_FAULT_LIMIT; + data->iout_oc_warn_limit = PMBUS_IOUT_OC_WARN_LIMIT; + } + + for (page = 0; page < 16; page++) { + rv = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + fsleep(MAX34440_PAGE_CHANGE_DELAY); + if (rv < 0) + return rv; + + rv = i2c_smbus_read_word_data(client, + MAX34451_MFR_CHANNEL_CONFIG); + if (rv < 0) + return rv; + + switch (rv & MAX34451_MFR_CHANNEL_CONFIG_SEL_MASK) { + case 0x10: + case 0x20: + data->info.func[page] = PMBUS_HAVE_VOUT | + PMBUS_HAVE_STATUS_VOUT; + + if (max34451_na6) + data->info.func[page] |= PMBUS_HAVE_VIN | + PMBUS_HAVE_STATUS_INPUT; + break; + case 0x21: + data->info.func[page] = PMBUS_HAVE_VOUT; + + if (max34451_na6) + data->info.func[page] |= PMBUS_HAVE_VIN; + break; + case 0x22: + data->info.func[page] = PMBUS_HAVE_IOUT | + PMBUS_HAVE_STATUS_IOUT; + + if (max34451_na6) + data->info.func[page] |= PMBUS_HAVE_IIN | + PMBUS_HAVE_STATUS_INPUT; + break; + case 0x23: + data->info.func[page] = PMBUS_HAVE_IOUT; + + if (max34451_na6) + data->info.func[page] |= PMBUS_HAVE_IIN; + break; + default: + break; + } + } + + return 0; +} + +static struct pmbus_driver_info max34440_info[] = { + [adpm12160] = { + .pages = 19, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 125, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + .m[PSC_VOLTAGE_OUT] = 125, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .m[PSC_CURRENT_IN] = 250, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = -1, + .m[PSC_CURRENT_OUT] = 250, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = -1, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + /* absent func below [18] are not for monitoring */ + .func[2] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[4] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[5] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[6] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[7] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[8] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[9] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT, + .func[10] = PMBUS_HAVE_IIN | PMBUS_HAVE_STATUS_INPUT, + .func[18] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = max34451_read_byte_data, + .read_word_data = max34440_read_word_data, + .write_byte_data = max34451_write_byte_data, + .write_word_data = max34440_write_word_data, + }, + [adpm12200] = { + .pages = 19, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 125, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + .m[PSC_VOLTAGE_OUT] = 125, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .m[PSC_CURRENT_IN] = 250, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = -1, + .m[PSC_CURRENT_OUT] = 250, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = -1, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + /* absent func below [18] are not for monitoring */ + .func[2] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[4] = PMBUS_HAVE_STATUS_IOUT, + .func[5] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[6] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[7] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[8] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[9] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT, + .func[10] = PMBUS_HAVE_IIN | PMBUS_HAVE_STATUS_INPUT, + .func[14] = PMBUS_HAVE_IOUT, + .func[18] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = max34451_read_byte_data, + .read_word_data = max34440_read_word_data, + .write_byte_data = max34451_write_byte_data, + .write_word_data = max34440_write_word_data, + }, + [adpm12250] = { + .pages = 19, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 125, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + .m[PSC_VOLTAGE_OUT] = 125, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .m[PSC_CURRENT_IN] = 250, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = -1, + .m[PSC_CURRENT_OUT] = 250, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = -1, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + /* absent func below [18] are not for monitoring */ + .func[2] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[4] = PMBUS_HAVE_STATUS_IOUT, + .func[5] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[6] = PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[9] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT, + .func[10] = PMBUS_HAVE_IIN | PMBUS_HAVE_STATUS_INPUT, + .func[14] = PMBUS_HAVE_IOUT, + .func[18] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = max34451_read_byte_data, + .read_word_data = max34440_read_word_data, + .write_byte_data = max34451_write_byte_data, + .write_word_data = max34440_write_word_data, + }, + [max34440] = { + .pages = 14, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_VOLTAGE_IN] = 1, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 3, /* R = 0 in datasheet reflects mV */ + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 3, /* R = 0 in datasheet reflects mV */ + .m[PSC_CURRENT_OUT] = 1, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 3, /* R = 0 in datasheet reflects mA */ + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[2] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[3] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[4] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[5] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[6] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[7] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[8] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[9] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[10] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[11] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[12] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[13] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = max34440_read_byte_data, + .read_word_data = max34440_read_word_data, + .write_word_data = max34440_write_word_data, + .page_change_delay = MAX34440_PAGE_CHANGE_DELAY, + }, + [max34441] = { + .pages = 12, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_FAN] = direct, + .m[PSC_VOLTAGE_IN] = 1, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 3, + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_CURRENT_OUT] = 1, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 3, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + .m[PSC_FAN] = 1, + .b[PSC_FAN] = 0, + .R[PSC_FAN] = 0, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[2] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[3] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[4] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[5] = PMBUS_HAVE_FAN12 | PMBUS_HAVE_STATUS_FAN12, + .func[6] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[7] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[8] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[9] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[10] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[11] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = max34440_read_byte_data, + .read_word_data = max34440_read_word_data, + .write_word_data = max34440_write_word_data, + .page_change_delay = MAX34440_PAGE_CHANGE_DELAY, + }, + [max34446] = { + .pages = 7, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .m[PSC_VOLTAGE_IN] = 1, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 3, + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_CURRENT_OUT] = 1, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 3, + .m[PSC_POWER] = 1, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = 3, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_POUT, + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[2] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_POUT, + .func[3] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[4] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[5] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[6] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = max34440_read_byte_data, + .read_word_data = max34440_read_word_data, + .write_word_data = max34440_write_word_data, + .page_change_delay = MAX34440_PAGE_CHANGE_DELAY, + }, + [max34451] = { + .pages = 21, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_CURRENT_OUT] = 1, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 2, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + /* func 0-15 is set dynamically before probing */ + .func[16] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[17] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[18] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[19] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[20] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = max34451_read_byte_data, + .read_word_data = max34440_read_word_data, + .write_byte_data = max34451_write_byte_data, + .write_word_data = max34440_write_word_data, + .page_change_delay = MAX34440_PAGE_CHANGE_DELAY, + }, + [max34452] = { + .pages = 21, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_CURRENT_OUT] = 1, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 2, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + /* func 0-15 is set dynamically before probing */ + .func[16] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[17] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[18] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[19] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[20] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = max34451_read_byte_data, + .read_word_data = max34440_read_word_data, + .write_byte_data = max34451_write_byte_data, + .write_word_data = max34440_write_word_data, + .page_change_delay = MAX34440_PAGE_CHANGE_DELAY, + }, + [max34460] = { + .pages = 18, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[2] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[3] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[4] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[5] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[6] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[7] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[8] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[9] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[10] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[11] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[13] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[14] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[15] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[16] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[17] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_word_data = max34440_read_word_data, + .write_word_data = max34440_write_word_data, + .page_change_delay = MAX34440_PAGE_CHANGE_DELAY, + }, + [max34461] = { + .pages = 23, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 2, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[2] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[3] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[4] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[5] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[6] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[7] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[8] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[9] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[10] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[11] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[12] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[13] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[14] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + .func[15] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT, + /* page 16 is reserved */ + .func[17] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[18] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[19] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[20] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .func[21] = PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_word_data = max34440_read_word_data, + .write_word_data = max34440_write_word_data, + .page_change_delay = MAX34440_PAGE_CHANGE_DELAY, + }, +}; + +static int max34440_probe(struct i2c_client *client) +{ + struct max34440_data *data; + int rv; + + data = devm_kzalloc(&client->dev, sizeof(struct max34440_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + data->id = (uintptr_t)i2c_get_match_data(client); + data->info = max34440_info[data->id]; + data->iout_oc_fault_limit = MAX34440_IOUT_OC_FAULT_LIMIT; + data->iout_oc_warn_limit = MAX34440_IOUT_OC_WARN_LIMIT; + + if (data->id == max34451 || data->id == max34452) { + rv = max34451_set_supported_funcs(client, data); + if (rv) + return rv; + } else if (data->id == adpm12160 || data->id == adpm12200 || + data->id == adpm12250) { + data->iout_oc_fault_limit = PMBUS_IOUT_OC_FAULT_LIMIT; + data->iout_oc_warn_limit = PMBUS_IOUT_OC_WARN_LIMIT; + } + + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id max34440_id[] = { + { .name = "adpm12160", .driver_data = adpm12160 }, + { .name = "adpm12200", .driver_data = adpm12200 }, + { .name = "adpm12250", .driver_data = adpm12250 }, + { .name = "max34440", .driver_data = max34440 }, + { .name = "max34441", .driver_data = max34441 }, + { .name = "max34446", .driver_data = max34446 }, + { .name = "max34451", .driver_data = max34451 }, + { .name = "max34452", .driver_data = max34452 }, + { .name = "max34460", .driver_data = max34460 }, + { .name = "max34461", .driver_data = max34461 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, max34440_id); + +/* This is the driver that will be inserted */ +static struct i2c_driver max34440_driver = { + .driver = { + .name = "max34440", + }, + .probe = max34440_probe, + .id_table = max34440_id, +}; + +module_i2c_driver(max34440_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for Maxim MAX34440/MAX34441"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/max8688.c b/drivers/hwmon/pmbus/max8688.c new file mode 100644 index 000000000..212b28fa4 --- /dev/null +++ b/drivers/hwmon/pmbus/max8688.c @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Maxim MAX8688 + * + * Copyright (c) 2011 Ericsson AB. + */ + +#include <linux/bitops.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include "pmbus.h" + +#define MAX8688_MFR_VOUT_PEAK 0xd4 +#define MAX8688_MFR_IOUT_PEAK 0xd5 +#define MAX8688_MFR_TEMPERATURE_PEAK 0xd6 +#define MAX8688_MFG_STATUS 0xd8 + +#define MAX8688_STATUS_OC_FAULT BIT(4) +#define MAX8688_STATUS_OV_FAULT BIT(5) +#define MAX8688_STATUS_OV_WARNING BIT(8) +#define MAX8688_STATUS_UV_FAULT BIT(9) +#define MAX8688_STATUS_UV_WARNING BIT(10) +#define MAX8688_STATUS_UC_FAULT BIT(11) +#define MAX8688_STATUS_OC_WARNING BIT(12) +#define MAX8688_STATUS_OT_FAULT BIT(13) +#define MAX8688_STATUS_OT_WARNING BIT(14) + +static int max8688_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_VIRT_READ_VOUT_MAX: + ret = pmbus_read_word_data(client, 0, 0xff, + MAX8688_MFR_VOUT_PEAK); + break; + case PMBUS_VIRT_READ_IOUT_MAX: + ret = pmbus_read_word_data(client, 0, 0xff, + MAX8688_MFR_IOUT_PEAK); + break; + case PMBUS_VIRT_READ_TEMP_MAX: + ret = pmbus_read_word_data(client, 0, 0xff, + MAX8688_MFR_TEMPERATURE_PEAK); + break; + case PMBUS_VIRT_RESET_VOUT_HISTORY: + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_RESET_TEMP_HISTORY: + ret = 0; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int max8688_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_RESET_VOUT_HISTORY: + ret = pmbus_write_word_data(client, 0, MAX8688_MFR_VOUT_PEAK, + 0); + break; + case PMBUS_VIRT_RESET_IOUT_HISTORY: + ret = pmbus_write_word_data(client, 0, MAX8688_MFR_IOUT_PEAK, + 0); + break; + case PMBUS_VIRT_RESET_TEMP_HISTORY: + ret = pmbus_write_word_data(client, 0, + MAX8688_MFR_TEMPERATURE_PEAK, + 0xffff); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int max8688_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret = 0; + int mfg_status; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_STATUS_VOUT: + mfg_status = pmbus_read_word_data(client, 0, 0xff, + MAX8688_MFG_STATUS); + if (mfg_status < 0) + return mfg_status; + if (mfg_status & MAX8688_STATUS_UV_WARNING) + ret |= PB_VOLTAGE_UV_WARNING; + if (mfg_status & MAX8688_STATUS_UV_FAULT) + ret |= PB_VOLTAGE_UV_FAULT; + if (mfg_status & MAX8688_STATUS_OV_WARNING) + ret |= PB_VOLTAGE_OV_WARNING; + if (mfg_status & MAX8688_STATUS_OV_FAULT) + ret |= PB_VOLTAGE_OV_FAULT; + break; + case PMBUS_STATUS_IOUT: + mfg_status = pmbus_read_word_data(client, 0, 0xff, + MAX8688_MFG_STATUS); + if (mfg_status < 0) + return mfg_status; + if (mfg_status & MAX8688_STATUS_UC_FAULT) + ret |= PB_IOUT_UC_FAULT; + if (mfg_status & MAX8688_STATUS_OC_WARNING) + ret |= PB_IOUT_OC_WARNING; + if (mfg_status & MAX8688_STATUS_OC_FAULT) + ret |= PB_IOUT_OC_FAULT; + break; + case PMBUS_STATUS_TEMPERATURE: + mfg_status = pmbus_read_word_data(client, 0, 0xff, + MAX8688_MFG_STATUS); + if (mfg_status < 0) + return mfg_status; + if (mfg_status & MAX8688_STATUS_OT_WARNING) + ret |= PB_TEMP_OT_WARNING; + if (mfg_status & MAX8688_STATUS_OT_FAULT) + ret |= PB_TEMP_OT_FAULT; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static struct pmbus_driver_info max8688_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_CURRENT_OUT] = direct, + .m[PSC_VOLTAGE_IN] = 19995, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -1, + .m[PSC_VOLTAGE_OUT] = 19995, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = -1, + .m[PSC_CURRENT_OUT] = 23109, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = -2, + .m[PSC_TEMPERATURE] = -7612, + .b[PSC_TEMPERATURE] = 335, + .R[PSC_TEMPERATURE] = -3, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = max8688_read_byte_data, + .read_word_data = max8688_read_word_data, + .write_word_data = max8688_write_word_data, +}; + +static int max8688_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &max8688_info); +} + +static const struct i2c_device_id max8688_id[] = { + { .name = "max8688" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, max8688_id); + +/* This is the driver that will be inserted */ +static struct i2c_driver max8688_driver = { + .driver = { + .name = "max8688", + }, + .probe = max8688_probe, + .id_table = max8688_id, +}; + +module_i2c_driver(max8688_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for Maxim MAX8688"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp2856.c b/drivers/hwmon/pmbus/mp2856.c new file mode 100644 index 000000000..3d6621e36 --- /dev/null +++ b/drivers/hwmon/pmbus/mp2856.c @@ -0,0 +1,466 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS2856/2857 + * Monolithic Power Systems VR Controllers + * + * Copyright (C) 2023 Quanta Computer lnc. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +/* Vendor specific registers. */ +#define MP2856_MFR_VR_MULTI_CONFIG_R1 0x0d +#define MP2856_MFR_VR_MULTI_CONFIG_R2 0x1d + +#define MP2856_MUL1_BOOT_SR_R2 0x10 +#define MP2856_VR_ACTIVE BIT(15) + +#define MP2856_MFR_VR_CONFIG2 0x5e +#define MP2856_VOUT_MODE BIT(11) + +#define MP2856_MFR_VR_CONFIG1 0x68 +#define MP2856_DRMOS_KCS GENMASK(13, 12) + +#define MP2856_MFR_READ_CS1_2_R1 0x82 +#define MP2856_MFR_READ_CS3_4_R1 0x83 +#define MP2856_MFR_READ_CS5_6_R1 0x84 +#define MP2856_MFR_READ_CS7_8_R1 0x85 +#define MP2856_MFR_READ_CS9_10_R1 0x86 +#define MP2856_MFR_READ_CS11_12_R1 0x87 + +#define MP2856_MFR_READ_CS1_2_R2 0x85 +#define MP2856_MFR_READ_CS3_4_R2 0x86 +#define MP2856_MFR_READ_CS5_6_R2 0x87 + +#define MP2856_MAX_PHASE_RAIL1 8 +#define MP2856_MAX_PHASE_RAIL2 4 + +#define MP2857_MAX_PHASE_RAIL1 12 +#define MP2857_MAX_PHASE_RAIL2 4 + +#define MP2856_PAGE_NUM 2 + +enum chips { mp2856, mp2857 }; + +static const int mp2856_max_phases[][MP2856_PAGE_NUM] = { + [mp2856] = { MP2856_MAX_PHASE_RAIL1, MP2856_MAX_PHASE_RAIL2 }, + [mp2857] = { MP2857_MAX_PHASE_RAIL1, MP2857_MAX_PHASE_RAIL2 }, +}; + +static const struct i2c_device_id mp2856_id[] = { + { .name = "mp2856", .driver_data = mp2856 }, + { .name = "mp2857", .driver_data = mp2857 }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, mp2856_id); + +struct mp2856_data { + struct pmbus_driver_info info; + int vout_format[MP2856_PAGE_NUM]; + int curr_sense_gain[MP2856_PAGE_NUM]; + int max_phases[MP2856_PAGE_NUM]; +}; + +#define to_mp2856_data(x) container_of(x, struct mp2856_data, info) + +#define MAX_LIN_MANTISSA (1023 * 1000) +#define MIN_LIN_MANTISSA (511 * 1000) + +static u16 val2linear11(s64 val) +{ + s16 exponent = 0, mantissa; + bool negative = false; + + if (val == 0) + return 0; + + if (val < 0) { + negative = true; + val = -val; + } + + /* Reduce large mantissa until it fits into 10 bit */ + while (val >= MAX_LIN_MANTISSA && exponent < 15) { + exponent++; + val >>= 1; + } + /* Increase small mantissa to improve precision */ + while (val < MIN_LIN_MANTISSA && exponent > -15) { + exponent--; + val <<= 1; + } + + /* Convert mantissa from milli-units to units */ + mantissa = clamp_val(DIV_ROUND_CLOSEST_ULL(val, 1000), 0, 0x3ff); + + /* restore sign */ + if (negative) + mantissa = -mantissa; + + /* Convert to 5 bit exponent, 11 bit mantissa */ + return (mantissa & 0x7ff) | ((exponent << 11) & 0xf800); +} + +static int +mp2856_read_word_helper(struct i2c_client *client, int page, int phase, u8 reg, + u16 mask) +{ + int ret = pmbus_read_word_data(client, page, phase, reg); + + return (ret > 0) ? ret & mask : ret; +} + +static int +mp2856_read_vout(struct i2c_client *client, struct mp2856_data *data, int page, + int phase, u8 reg) +{ + int ret; + + ret = mp2856_read_word_helper(client, page, phase, reg, + GENMASK(9, 0)); + if (ret < 0) + return ret; + + /* convert vout result to direct format */ + ret = (data->vout_format[page] == vid) ? + ((ret + 49) * 5) : ((ret * 1000) >> 8); + + return ret; +} + +static int +mp2856_read_phase(struct i2c_client *client, struct mp2856_data *data, + int page, int phase, u8 reg) +{ + int ret; + int val; + + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + if (!((phase + 1) % MP2856_PAGE_NUM)) + ret >>= 8; + ret &= 0xff; + + /* + * Output value is calculated as: (READ_CSx * 12.5mV - 1.23V) / (Kcs * Rcs) + */ + val = (ret * 125) - 12300; + + return val2linear11(val); +} + +static int +mp2856_read_phases(struct i2c_client *client, struct mp2856_data *data, + int page, int phase) +{ + int ret; + + if (page == 0) { + switch (phase) { + case 0 ... 1: + ret = mp2856_read_phase(client, data, page, phase, + MP2856_MFR_READ_CS1_2_R1); + break; + case 2 ... 3: + ret = mp2856_read_phase(client, data, page, phase, + MP2856_MFR_READ_CS3_4_R1); + break; + case 4 ... 5: + ret = mp2856_read_phase(client, data, page, phase, + MP2856_MFR_READ_CS5_6_R1); + break; + case 6 ... 7: + ret = mp2856_read_phase(client, data, page, phase, + MP2856_MFR_READ_CS7_8_R1); + break; + default: + return -ENODATA; + } + } else { + switch (phase) { + case 0 ... 1: + ret = mp2856_read_phase(client, data, page, phase, + MP2856_MFR_READ_CS1_2_R2); + break; + case 2 ... 3: + ret = mp2856_read_phase(client, data, page, phase, + MP2856_MFR_READ_CS1_2_R2); + break; + default: + return -ENODATA; + } + } + return ret; +} + +static int +mp2856_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2856_data *data = to_mp2856_data(info); + int ret; + + switch (reg) { + case PMBUS_READ_VOUT: + ret = mp2856_read_vout(client, data, page, phase, reg); + break; + case PMBUS_READ_IOUT: + if (phase != 0xff) + ret = mp2856_read_phases(client, data, page, phase); + else + ret = pmbus_read_word_data(client, page, phase, reg); + break; + default: + return -ENODATA; + } + + return ret; +} + +static int +mp2856_read_byte_data(struct i2c_client *client, int page, int reg) +{ + switch (reg) { + case PMBUS_VOUT_MODE: + /* Enforce VOUT direct format. */ + return PB_VOUT_MODE_DIRECT; + default: + return -ENODATA; + } +} + +static int +mp2856_identify_multiphase(struct i2c_client *client, u8 reg, u8 max_phase, + u16 mask) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 2); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, reg); + if (ret < 0) + return ret; + + ret &= mask; + return (ret >= max_phase) ? max_phase : ret; +} + +static int +mp2856_identify_multiphase_rail1(struct i2c_client *client, + struct mp2856_data *data) +{ + int ret, i; + + ret = mp2856_identify_multiphase(client, MP2856_MFR_VR_MULTI_CONFIG_R1, + MP2856_MAX_PHASE_RAIL1, GENMASK(3, 0)); + if (ret < 0) + return ret; + + data->info.phases[0] = (ret > data->max_phases[0]) ? + data->max_phases[0] : ret; + + for (i = 0 ; i < data->info.phases[0]; i++) + data->info.pfunc[i] |= PMBUS_HAVE_IOUT; + + return 0; +} + +static int +mp2856_identify_multiphase_rail2(struct i2c_client *client, + struct mp2856_data *data) +{ + int ret, i; + + ret = mp2856_identify_multiphase(client, MP2856_MFR_VR_MULTI_CONFIG_R2, + MP2856_MAX_PHASE_RAIL2, GENMASK(2, 0)); + if (ret < 0) + return ret; + + data->info.phases[1] = (ret > data->max_phases[1]) ? + data->max_phases[1] : ret; + + for (i = 0 ; i < data->info.phases[0]; i++) + data->info.pfunc[i] |= PMBUS_HAVE_IOUT; + + return 0; +} + +static int +mp2856_current_sense_gain_get(struct i2c_client *client, + struct mp2856_data *data) +{ + int i, ret; + + /* + * Obtain DrMOS current sense gain of power stage from the register + * MP2856_MFR_VR_CONFIG1, bits 13-12. The value is selected as below: + * 00b - 5µA/A, 01b - 8.5µA/A, 10b - 9.7µA/A, 11b - 10µA/A. Other + * values are invalid. + */ + for (i = 0 ; i < data->info.pages; i++) { + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, i); + if (ret < 0) + return ret; + ret = i2c_smbus_read_word_data(client, + MP2856_MFR_VR_CONFIG1); + if (ret < 0) + return ret; + + switch ((ret & MP2856_DRMOS_KCS) >> 12) { + case 0: + data->curr_sense_gain[i] = 50; + break; + case 1: + data->curr_sense_gain[i] = 85; + break; + case 2: + data->curr_sense_gain[i] = 97; + break; + default: + data->curr_sense_gain[i] = 100; + break; + } + } + return 0; +} + +static int +mp2856_identify_vout_format(struct i2c_client *client, + struct mp2856_data *data) +{ + int i, ret; + + for (i = 0; i < data->info.pages; i++) { + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, i); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MP2856_MFR_VR_CONFIG2); + if (ret < 0) + return ret; + + data->vout_format[i] = (ret & MP2856_VOUT_MODE) ? linear : vid; + } + return 0; +} + +static bool +mp2856_is_rail2_active(struct i2c_client *client) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 2); + if (ret < 0) + return true; + + ret = i2c_smbus_read_word_data(client, MP2856_MUL1_BOOT_SR_R2); + if (ret < 0) + return true; + + return (ret & MP2856_VR_ACTIVE) ? true : false; +} + +static struct pmbus_driver_info mp2856_info = { + .pages = MP2856_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | PMBUS_HAVE_POUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT, + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP, + .read_byte_data = mp2856_read_byte_data, + .read_word_data = mp2856_read_word_data, +}; + +static int mp2856_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + struct mp2856_data *data; + enum chips chip_id; + int ret; + + data = devm_kzalloc(&client->dev, sizeof(struct mp2856_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + chip_id = (kernel_ulong_t)i2c_get_match_data(client); + + memcpy(data->max_phases, mp2856_max_phases[chip_id], + sizeof(data->max_phases)); + + memcpy(&data->info, &mp2856_info, sizeof(*info)); + info = &data->info; + + /* Identify multiphase configuration. */ + ret = mp2856_identify_multiphase_rail1(client, data); + if (ret < 0) + return ret; + + if (mp2856_is_rail2_active(client)) { + ret = mp2856_identify_multiphase_rail2(client, data); + if (ret < 0) + return ret; + } else { + /* rail2 is not active */ + info->pages = 1; + } + + /* Obtain current sense gain of power stage. */ + ret = mp2856_current_sense_gain_get(client, data); + if (ret) + return ret; + + /* Identify vout format. */ + ret = mp2856_identify_vout_format(client, data); + if (ret) + return ret; + + /* set the device to page 0 */ + i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + + return pmbus_do_probe(client, info); +} + +static const struct of_device_id __maybe_unused mp2856_of_match[] = { + {.compatible = "mps,mp2856", .data = (void *)mp2856}, + {.compatible = "mps,mp2857", .data = (void *)mp2857}, + {} +}; +MODULE_DEVICE_TABLE(of, mp2856_of_match); + +static struct i2c_driver mp2856_driver = { + .driver = { + .name = "mp2856", + .of_match_table = mp2856_of_match, + }, + .probe = mp2856_probe, + .id_table = mp2856_id, +}; + +module_i2c_driver(mp2856_driver); + +MODULE_AUTHOR("Peter Yin <peter.yin@quantatw.com>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP2856/MP2857 device"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp2869.c b/drivers/hwmon/pmbus/mp2869.c new file mode 100644 index 000000000..76c601750 --- /dev/null +++ b/drivers/hwmon/pmbus/mp2869.c @@ -0,0 +1,666 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Multi-phase Digital VR Controllers(MP2869) + */ + +#include <linux/bitfield.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +/* + * Vender specific registers, the register MFR_SVI3_IOUT_PRT(0x67), + * READ_PIN_EST(0x94)and READ_IIN_EST(0x95) redefine the standard + * PMBUS register. The MFR_VOUT_LOOP_CTRL(0x29) is used to identify + * the vout scale and the MFR_SVI3_IOUT_PRT(0x67) is used to identify + * the iout scale. The READ_PIN_EST(0x94) is used to read input power + * per rail. The MP2891 does not have standard READ_IIN register(0x89), + * the iin telemetry can be obtained through the vendor redefined + * register READ_IIN_EST(0x95). + */ +#define MFR_SVI3_IOUT_PRT 0x67 +#define MFR_READ_PIN_EST 0x94 +#define MFR_READ_IIN_EST 0x95 +#define MFR_TSNS_FLT_SET 0xBB + +#define MP2869_VIN_OV_FAULT_GAIN 4 +#define MP2869_READ_VOUT_DIV 1024 +#define MP2869_READ_IOUT_DIV 32 +#define MP2869_OVUV_LIMIT_SCALE 10 +#define MP2869_OVUV_DELTA_SCALE 50 +#define MP2869_TEMP_LIMIT_OFFSET 40 +#define MP2869_IOUT_LIMIT_UINT 8 +#define MP2869_POUT_OP_GAIN 2 + +#define MP2869_PAGE_NUM 2 + +#define MP2869_RAIL1_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_PIN | \ + PMBUS_HAVE_IIN | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +#define MP2869_RAIL2_FUNC (PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | \ + PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP | \ + PMBUS_HAVE_PIN | PMBUS_HAVE_IIN | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +struct mp2869_data { + struct pmbus_driver_info info; + bool mfr_thwn_flt_en; + int vout_scale[MP2869_PAGE_NUM]; + int iout_scale[MP2869_PAGE_NUM]; +}; + +static const int mp2869_vout_sacle[8] = {6400, 5120, 2560, 2048, 1024, + 4, 2, 1}; +static const int mp2869_iout_sacle[8] = {32, 1, 2, 4, 8, 16, 32, 64}; + +#define to_mp2869_data(x) container_of(x, struct mp2869_data, info) + +static u16 mp2869_reg2data_linear11(u16 word) +{ + s16 exponent; + s32 mantissa; + s64 val; + + exponent = ((s16)word) >> 11; + mantissa = ((s16)((word & 0x7ff) << 5)) >> 5; + val = mantissa; + + if (exponent >= 0) + val <<= exponent; + else + val >>= -exponent; + + return val; +} + +static int +mp2869_identify_thwn_flt(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp2869_data *data = to_mp2869_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_TSNS_FLT_SET); + if (ret < 0) + return ret; + + data->mfr_thwn_flt_en = FIELD_GET(GENMASK(13, 13), ret); + + return 0; +} + +static int +mp2869_identify_vout_scale(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp2869_data *data = to_mp2869_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, PMBUS_VOUT_SCALE_LOOP); + if (ret < 0) + return ret; + + /* + * The output voltage is equal to the READ_VOUT(0x8B) register value multiply + * by vout_scale. + * Obtain vout scale from the register PMBUS_VOUT_SCALE_LOOP, bits 12-10 + * PMBUS_VOUT_SCALE_LOOP[12:10]: + * 000b - 6.25mV/LSB, 001b - 5mV/LSB, 010b - 2.5mV/LSB, 011b - 2mV/LSB + * 100b - 1mV/Lsb, 101b - (1/256)mV/LSB, 110b - (1/512)mV/LSB, + * 111b - (1/1024)mV/LSB + */ + data->vout_scale[page] = mp2869_vout_sacle[FIELD_GET(GENMASK(12, 10), ret)]; + + return 0; +} + +static int +mp2869_identify_iout_scale(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp2869_data *data = to_mp2869_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_SVI3_IOUT_PRT); + if (ret < 0) + return ret; + + /* + * The output current is equal to the READ_IOUT(0x8C) register value + * multiply by iout_scale. + * Obtain iout_scale from the register MFR_SVI3_IOUT_PRT[2:0]. + * The value is selected as below: + * 000b - 1A/LSB, 001b - (1/32)A/LSB, 010b - (1/16)A/LSB, + * 011b - (1/8)A/LSB, 100b - (1/4)A/LSB, 101b - (1/2)A/LSB + * 110b - 1A/LSB, 111b - 2A/LSB + */ + data->iout_scale[page] = mp2869_iout_sacle[FIELD_GET(GENMASK(2, 0), ret)]; + + return 0; +} + +static int mp2869_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2869_data *data = to_mp2869_data(info); + int ret, mfr; + + switch (reg) { + case PMBUS_VOUT_MODE: + /* + * The calculation of vout in this driver is based on direct format. + * As a result, the format of vout is enforced to direct. + */ + ret = PB_VOUT_MODE_DIRECT; + break; + case PMBUS_STATUS_BYTE: + /* + * If the tsns digital fault is enabled, the TEMPERATURE flag + * of PMBUS_STATUS_BYTE should come from STATUS_MFR_SPECIFIC + * register bit1. + */ + if (!data->mfr_thwn_flt_en) + return -ENODATA; + + ret = pmbus_read_byte_data(client, page, reg); + if (ret < 0) + return ret; + + mfr = pmbus_read_byte_data(client, page, + PMBUS_STATUS_MFR_SPECIFIC); + if (mfr < 0) + return mfr; + + ret = (ret & ~GENMASK(2, 2)) | + FIELD_PREP(GENMASK(2, 2), + FIELD_GET(GENMASK(1, 1), mfr)); + break; + case PMBUS_STATUS_TEMPERATURE: + /* + * If the tsns digital fault is enabled, the OT Fault and OT Warning + * flag of PMBUS_STATUS_TEMPERATURE should come from STATUS_MFR_SPECIFIC + * register bit1. + */ + if (!data->mfr_thwn_flt_en) + return -ENODATA; + + ret = pmbus_read_byte_data(client, page, reg); + if (ret < 0) + return ret; + + mfr = pmbus_read_byte_data(client, page, + PMBUS_STATUS_MFR_SPECIFIC); + if (mfr < 0) + return mfr; + + ret = (ret & ~GENMASK(7, 6)) | + FIELD_PREP(GENMASK(6, 6), + FIELD_GET(GENMASK(1, 1), mfr)) | + FIELD_PREP(GENMASK(7, 7), + FIELD_GET(GENMASK(1, 1), mfr)); + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int mp2869_read_word_data(struct i2c_client *client, int page, int phase, + int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2869_data *data = to_mp2869_data(info); + int ret, mfr; + + switch (reg) { + case PMBUS_STATUS_WORD: + /* + * If the tsns digital fault is enabled, the OT Fault flag + * of PMBUS_STATUS_WORD should come from STATUS_MFR_SPECIFIC + * register bit1. + */ + if (!data->mfr_thwn_flt_en) + return -ENODATA; + + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + mfr = pmbus_read_byte_data(client, page, + PMBUS_STATUS_MFR_SPECIFIC); + if (mfr < 0) + return mfr; + + ret = (ret & ~GENMASK(2, 2)) | + FIELD_PREP(GENMASK(2, 2), + FIELD_GET(GENMASK(1, 1), mfr)); + break; + case PMBUS_READ_VIN: + /* + * The MP2869 PMBUS_READ_VIN[10:0] is the vin value, the vin scale is + * 31.25mV/LSB. And the vin scale is set to 31.25mV/Lsb(using r/m/b scale) + * in MP2869 pmbus_driver_info struct, so the word data bit0-bit10 can be + * returned to pmbus core directly. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = FIELD_GET(GENMASK(10, 0), ret); + break; + case PMBUS_READ_IIN: + /* + * The MP2869 redefine the standard 0x95 register as iin telemetry + * per rail. + */ + ret = pmbus_read_word_data(client, page, phase, MFR_READ_IIN_EST); + if (ret < 0) + return ret; + + break; + case PMBUS_READ_PIN: + /* + * The MP2869 redefine the standard 0x94 register as pin telemetry + * per rail. The MP2869 MFR_READ_PIN_EST register is linear11 format, + * but the pin scale is set to 1W/Lsb(using r/m/b scale). As a result, + * the pin read from MP2869 should be converted to W, then return + * the result to pmbus core. + */ + ret = pmbus_read_word_data(client, page, phase, MFR_READ_PIN_EST); + if (ret < 0) + return ret; + + ret = mp2869_reg2data_linear11(ret); + break; + case PMBUS_READ_VOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(11, 0)) * data->vout_scale[page], + MP2869_READ_VOUT_DIV); + break; + case PMBUS_READ_IOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(10, 0)) * data->iout_scale[page], + MP2869_READ_IOUT_DIV); + break; + case PMBUS_READ_POUT: + /* + * The MP2869 PMBUS_READ_POUT register is linear11 format, but the pout + * scale is set to 1W/Lsb(using r/m/b scale). As a result, the pout read + * from MP2869 should be converted to W, then return the result to pmbus + * core. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = mp2869_reg2data_linear11(ret); + break; + case PMBUS_READ_TEMPERATURE_1: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = FIELD_GET(GENMASK(10, 0), ret); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(12, 9), ret)) + ret = FIELD_GET(GENMASK(8, 0), ret) * MP2869_OVUV_LIMIT_SCALE + + (FIELD_GET(GENMASK(12, 9), ret) + 1) * MP2869_OVUV_DELTA_SCALE; + else + ret = FIELD_GET(GENMASK(8, 0), ret) * MP2869_OVUV_LIMIT_SCALE; + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(12, 9), ret)) + ret = FIELD_GET(GENMASK(8, 0), ret) * MP2869_OVUV_LIMIT_SCALE - + (FIELD_GET(GENMASK(12, 9), ret) + 1) * MP2869_OVUV_DELTA_SCALE; + else + ret = FIELD_GET(GENMASK(8, 0), ret) * MP2869_OVUV_LIMIT_SCALE; + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * The scale of MP2869 PMBUS_OT_FAULT_LIMIT and PMBUS_OT_WARN_LIMIT + * is 1°C/LSB and they have 40°C offset. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = (ret & GENMASK(7, 0)) - MP2869_TEMP_LIMIT_OFFSET; + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = (ret & GENMASK(7, 0)) * MP2869_VIN_OV_FAULT_GAIN; + break; + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = FIELD_GET(GENMASK(9, 0), ret); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(7, 0)) * data->iout_scale[page] * + MP2869_IOUT_LIMIT_UINT, MP2869_READ_IOUT_DIV); + break; + case PMBUS_POUT_OP_WARN_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = (ret & GENMASK(7, 0)) * MP2869_POUT_OP_GAIN; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int mp2869_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2869_data *data = to_mp2869_data(info); + int ret; + + switch (reg) { + case PMBUS_VOUT_UV_FAULT_LIMIT: + /* + * The MP2869 PMBUS_VOUT_UV_FAULT_LIMIT[8:0] is the limit value, + * and bit9-bit15 should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(12, 9), ret)) + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(8, 0)) | + FIELD_PREP(GENMASK(8, 0), + DIV_ROUND_CLOSEST(word + + (FIELD_GET(GENMASK(12, 9), + ret) + 1) * + MP2869_OVUV_DELTA_SCALE, + MP2869_OVUV_LIMIT_SCALE))); + else + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(8, 0)) | + FIELD_PREP(GENMASK(8, 0), + DIV_ROUND_CLOSEST(word, + MP2869_OVUV_LIMIT_SCALE))); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + /* + * The MP2869 PMBUS_VOUT_OV_FAULT_LIMIT[8:0] is the limit value, + * and bit9-bit15 should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(12, 9), ret)) + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(8, 0)) | + FIELD_PREP(GENMASK(8, 0), + DIV_ROUND_CLOSEST(word - + (FIELD_GET(GENMASK(12, 9), + ret) + 1) * + MP2869_OVUV_DELTA_SCALE, + MP2869_OVUV_LIMIT_SCALE))); + else + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(8, 0)) | + FIELD_PREP(GENMASK(8, 0), + DIV_ROUND_CLOSEST(word, + MP2869_OVUV_LIMIT_SCALE))); + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * If the tsns digital fault is enabled, the PMBUS_OT_FAULT_LIMIT and + * PMBUS_OT_WARN_LIMIT can not be written. + */ + if (data->mfr_thwn_flt_en) + return -EINVAL; + + /* + * The MP2869 scale of MP2869 PMBUS_OT_FAULT_LIMIT and PMBUS_OT_WARN_LIMIT + * have 40°C offset. The bit0-bit7 is the limit value, and bit8-bit15 + * should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), + word + MP2869_TEMP_LIMIT_OFFSET)); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + /* + * The MP2869 PMBUS_VIN_OV_FAULT_LIMIT[7:0] is the limit value, and bit8-bit15 + * should not be changed. The scale of PMBUS_VIN_OV_FAULT_LIMIT is 125mV/Lsb, + * but the vin scale is set to 31.25mV/Lsb(using r/m/b scale), so the word data + * should divide by MP2869_VIN_OV_FAULT_GAIN(4) + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), + DIV_ROUND_CLOSEST(word, + MP2869_VIN_OV_FAULT_GAIN))); + break; + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + /* + * The PMBUS_VIN_UV_LIMIT[9:0] is the limit value, and bit10-bit15 should + * not be changed. The scale of PMBUS_VIN_UV_LIMIT is 31.25mV/Lsb, and the + * vin scale is set to 31.25mV/Lsb(using r/m/b scale), so the word data can + * be written directly. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(9, 0)) | + FIELD_PREP(GENMASK(9, 0), + word)); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + ret = pmbus_write_word_data(client, page, reg, + DIV_ROUND_CLOSEST(word * MP2869_READ_IOUT_DIV, + MP2869_IOUT_LIMIT_UINT * + data->iout_scale[page])); + break; + case PMBUS_POUT_OP_WARN_LIMIT: + /* + * The POUT_OP_WARN_LIMIT[11:0] is the limit value, and bit12-bit15 should + * not be changed. The scale of POUT_OP_WARN_LIMIT is 2W/Lsb. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(11, 0)) | + FIELD_PREP(GENMASK(11, 0), + DIV_ROUND_CLOSEST(word, + MP2869_POUT_OP_GAIN))); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int mp2869_identify(struct i2c_client *client, struct pmbus_driver_info *info) +{ + int ret; + + /* Identify whether tsns digital fault is enable */ + ret = mp2869_identify_thwn_flt(client, info, 1); + if (ret < 0) + return 0; + + /* Identify vout scale for rail1. */ + ret = mp2869_identify_vout_scale(client, info, 0); + if (ret < 0) + return ret; + + /* Identify vout scale for rail2. */ + ret = mp2869_identify_vout_scale(client, info, 1); + if (ret < 0) + return ret; + + /* Identify iout scale for rail 1. */ + ret = mp2869_identify_iout_scale(client, info, 0); + if (ret < 0) + return ret; + + /* Identify iout scale for rail 2. */ + return mp2869_identify_iout_scale(client, info, 1); +} + +static const struct pmbus_driver_info mp2869_info = { + .pages = MP2869_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_POWER] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + + .m[PSC_VOLTAGE_IN] = 32, + .R[PSC_VOLTAGE_IN] = 0, + .b[PSC_VOLTAGE_IN] = 0, + + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .b[PSC_VOLTAGE_OUT] = 0, + + .m[PSC_CURRENT_OUT] = 1, + .R[PSC_CURRENT_OUT] = 0, + .b[PSC_CURRENT_OUT] = 0, + + .m[PSC_TEMPERATURE] = 1, + .R[PSC_TEMPERATURE] = 0, + .b[PSC_TEMPERATURE] = 0, + + .m[PSC_POWER] = 1, + .R[PSC_POWER] = 0, + .b[PSC_POWER] = 0, + + .func[0] = MP2869_RAIL1_FUNC, + .func[1] = MP2869_RAIL2_FUNC, + .read_word_data = mp2869_read_word_data, + .write_word_data = mp2869_write_word_data, + .read_byte_data = mp2869_read_byte_data, + .identify = mp2869_identify, +}; + +static int mp2869_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + struct mp2869_data *data; + + data = devm_kzalloc(&client->dev, sizeof(struct mp2869_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mp2869_info, sizeof(*info)); + info = &data->info; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id mp2869_id[] = { + { .name = "mp2869" }, + { .name = "mp29608" }, + { .name = "mp29612" }, + { .name = "mp29816" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp2869_id); + +static const struct of_device_id mp2869_of_match[] = { + { .compatible = "mps,mp2869" }, + { .compatible = "mps,mp29608" }, + { .compatible = "mps,mp29612" }, + { .compatible = "mps,mp29816" }, + { } +}; +MODULE_DEVICE_TABLE(of, mp2869_of_match); + +static struct i2c_driver mp2869_driver = { + .driver = { + .name = "mp2869", + .of_match_table = mp2869_of_match, + }, + .probe = mp2869_probe, + .id_table = mp2869_id, +}; + +module_i2c_driver(mp2869_driver); + +MODULE_AUTHOR("Wensheng Wang <wenswang@yeah.net>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP2869"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp2888.c b/drivers/hwmon/pmbus/mp2888.c new file mode 100644 index 000000000..c5f35daa3 --- /dev/null +++ b/drivers/hwmon/pmbus/mp2888.c @@ -0,0 +1,407 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Multi-phase Digital VR Controllers + * + * Copyright (C) 2020 Nvidia Technologies Ltd. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +/* Vendor specific registers. */ +#define MP2888_MFR_SYS_CONFIG 0x44 +#define MP2888_MFR_READ_CS1_2 0x73 +#define MP2888_MFR_READ_CS3_4 0x74 +#define MP2888_MFR_READ_CS5_6 0x75 +#define MP2888_MFR_READ_CS7_8 0x76 +#define MP2888_MFR_READ_CS9_10 0x77 +#define MP2888_MFR_VR_CONFIG1 0xe1 + +#define MP2888_TOTAL_CURRENT_RESOLUTION BIT(3) +#define MP2888_PHASE_CURRENT_RESOLUTION BIT(4) +#define MP2888_DRMOS_KCS GENMASK(2, 0) +#define MP2888_TEMP_UNIT 10 +#define MP2888_MAX_PHASE 10 + +struct mp2888_data { + struct pmbus_driver_info info; + int total_curr_resolution; + int phase_curr_resolution; + int curr_sense_gain; +}; + +#define to_mp2888_data(x) container_of(x, struct mp2888_data, info) + +static int mp2888_read_byte_data(struct i2c_client *client, int page, int reg) +{ + switch (reg) { + case PMBUS_VOUT_MODE: + /* Enforce VOUT direct format. */ + return PB_VOUT_MODE_DIRECT; + default: + return -ENODATA; + } +} + +static int +mp2888_current_sense_gain_and_resolution_get(struct i2c_client *client, struct mp2888_data *data) +{ + int ret; + + /* + * Obtain DrMOS current sense gain of power stage from the register + * , bits 0-2. The value is selected as below: + * 00b - 5µA/A, 01b - 8.5µA/A, 10b - 9.7µA/A, 11b - 10µA/A. Other + * values are reserved. + */ + ret = i2c_smbus_read_word_data(client, MP2888_MFR_SYS_CONFIG); + if (ret < 0) + return ret; + + switch (ret & MP2888_DRMOS_KCS) { + case 0: + data->curr_sense_gain = 85; + break; + case 1: + data->curr_sense_gain = 97; + break; + case 2: + data->curr_sense_gain = 100; + break; + case 3: + data->curr_sense_gain = 50; + break; + default: + return -EINVAL; + } + + /* + * Obtain resolution selector for total and phase current report and protection. + * 0: original resolution; 1: half resolution (in such case phase current value should + * be doubled. + */ + data->total_curr_resolution = (ret & MP2888_TOTAL_CURRENT_RESOLUTION) >> 3; + data->phase_curr_resolution = (ret & MP2888_PHASE_CURRENT_RESOLUTION) >> 4; + + return 0; +} + +static int +mp2888_read_phase(struct i2c_client *client, struct mp2888_data *data, int page, int phase, u8 reg) +{ + int ret; + + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + if (!((phase + 1) % 2)) + ret >>= 8; + ret &= 0xff; + + /* + * Output value is calculated as: (READ_CSx / 80 – 1.23) / (Kcs * Rcs) + * where: + * - Kcs is the DrMOS current sense gain of power stage, which is obtained from the + * register MP2888_MFR_VR_CONFIG1, bits 13-12 with the following selection of DrMOS + * (data->curr_sense_gain): + * 00b - 8.5µA/A, 01b - 9.7µA/A, 1b - 10µA/A, 11b - 5µA/A. + * - Rcs is the internal phase current sense resistor. This parameter depends on hardware + * assembly. By default it is set to 1kΩ. In case of different assembly, user should + * scale this parameter by dividing it by Rcs. + * If phase current resolution bit is set to 1, READ_CSx value should be doubled. + * Note, that current phase sensing, providing by the device is not accurate. This is + * because sampling of current occurrence of bit weight has a big deviation, especially for + * light load. + */ + ret = DIV_ROUND_CLOSEST(ret * 200 - 19600, data->curr_sense_gain); + /* Scale according to total current resolution. */ + ret = (data->total_curr_resolution) ? ret * 2 : ret; + return ret; +} + +static int +mp2888_read_phases(struct i2c_client *client, struct mp2888_data *data, int page, int phase) +{ + int ret; + + switch (phase) { + case 0 ... 1: + ret = mp2888_read_phase(client, data, page, phase, MP2888_MFR_READ_CS1_2); + break; + case 2 ... 3: + ret = mp2888_read_phase(client, data, page, phase, MP2888_MFR_READ_CS3_4); + break; + case 4 ... 5: + ret = mp2888_read_phase(client, data, page, phase, MP2888_MFR_READ_CS5_6); + break; + case 6 ... 7: + ret = mp2888_read_phase(client, data, page, phase, MP2888_MFR_READ_CS7_8); + break; + case 8 ... 9: + ret = mp2888_read_phase(client, data, page, phase, MP2888_MFR_READ_CS9_10); + break; + default: + return -ENODATA; + } + return ret; +} + +static int mp2888_read_word_data(struct i2c_client *client, int page, int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2888_data *data = to_mp2888_data(info); + int ret; + + switch (reg) { + case PMBUS_READ_VIN: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret <= 0) + return ret; + + /* + * READ_VIN requires fixup to scale it to linear11 format. Register data format + * provides 10 bits for mantissa and 6 bits for exponent. Bits 15:10 are set with + * the fixed value 111011b. + */ + ret = (ret & GENMASK(9, 0)) | ((ret & GENMASK(31, 10)) << 1); + break; + case PMBUS_OT_WARN_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + /* + * Chip reports limits in degrees C, but the actual temperature in 10th of + * degrees C - scaling is needed to match both. + */ + ret *= MP2888_TEMP_UNIT; + break; + case PMBUS_READ_IOUT: + if (phase != 0xff) + return mp2888_read_phases(client, data, page, phase); + + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + /* + * READ_IOUT register has unused bits 15:12 with fixed value 1110b. Clear these + * bits and scale with total current resolution. Data is provided in direct format. + */ + ret &= GENMASK(11, 0); + ret = data->total_curr_resolution ? ret * 2 : ret; + break; + case PMBUS_IOUT_OC_WARN_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + ret &= GENMASK(9, 0); + /* + * Chip reports limits with resolution 1A or 2A, if total current resolution bit is + * set 1. Actual current is reported with 0.25A or respectively 0.5A resolution. + * Scaling is needed to match both. + */ + ret = data->total_curr_resolution ? ret * 8 : ret * 4; + break; + case PMBUS_READ_POUT: + case PMBUS_READ_PIN: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + ret = data->total_curr_resolution ? ret : DIV_ROUND_CLOSEST(ret, 2); + break; + case PMBUS_POUT_OP_WARN_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + /* + * Chip reports limits with resolution 1W or 2W, if total current resolution bit is + * set 1. Actual power is reported with 0.5W or 1W respectively resolution. Scaling + * is needed to match both. + */ + ret = data->total_curr_resolution ? ret * 2 : ret; + break; + /* + * The below registers are not implemented by device or implemented not according to the + * spec. Skip all of them to avoid exposing non-relevant inputs to sysfs. + */ + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_UT_WARN_LIMIT: + case PMBUS_UT_FAULT_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_VOUT_OV_WARN_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_IOUT_OC_LV_FAULT_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_POUT_MAX: + case PMBUS_IOUT_UC_FAULT_LIMIT: + case PMBUS_POUT_OP_FAULT_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + case PMBUS_MFR_VIN_MIN: + case PMBUS_MFR_VOUT_MIN: + case PMBUS_MFR_VIN_MAX: + case PMBUS_MFR_VOUT_MAX: + case PMBUS_MFR_IIN_MAX: + case PMBUS_MFR_IOUT_MAX: + case PMBUS_MFR_PIN_MAX: + case PMBUS_MFR_POUT_MAX: + case PMBUS_MFR_MAX_TEMP_1: + return -ENXIO; + default: + return -ENODATA; + } + + return ret; +} + +static int mp2888_write_word_data(struct i2c_client *client, int page, int reg, u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2888_data *data = to_mp2888_data(info); + + switch (reg) { + case PMBUS_OT_WARN_LIMIT: + word = DIV_ROUND_CLOSEST(word, MP2888_TEMP_UNIT); + /* Drop unused bits 15:8. */ + word = clamp_val(word, 0, GENMASK(7, 0)); + break; + case PMBUS_IOUT_OC_WARN_LIMIT: + /* Fix limit according to total curent resolution. */ + word = data->total_curr_resolution ? DIV_ROUND_CLOSEST(word, 8) : + DIV_ROUND_CLOSEST(word, 4); + /* Drop unused bits 15:10. */ + word = clamp_val(word, 0, GENMASK(9, 0)); + break; + case PMBUS_POUT_OP_WARN_LIMIT: + /* Fix limit according to total curent resolution. */ + word = data->total_curr_resolution ? DIV_ROUND_CLOSEST(word, 4) : + DIV_ROUND_CLOSEST(word, 2); + /* Drop unused bits 15:10. */ + word = clamp_val(word, 0, GENMASK(9, 0)); + break; + default: + return -ENODATA; + } + return pmbus_write_word_data(client, page, reg, word); +} + +static int +mp2888_identify_multiphase(struct i2c_client *client, struct mp2888_data *data, + struct pmbus_driver_info *info) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + /* Identify multiphase number - could be from 1 to 10. */ + ret = i2c_smbus_read_word_data(client, MP2888_MFR_VR_CONFIG1); + if (ret <= 0) + return ret; + + info->phases[0] = ret & GENMASK(3, 0); + + /* + * The device provides a total of 10 PWM pins, and can be configured to different phase + * count applications for rail. + */ + if (info->phases[0] > MP2888_MAX_PHASE) + return -EINVAL; + + return 0; +} + +static struct pmbus_driver_info mp2888_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .m[PSC_TEMPERATURE] = 1, + .R[PSC_TEMPERATURE] = 1, + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_CURRENT_OUT] = 4, + .m[PSC_POWER] = 1, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_POUT | PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_PHASE_VIRTUAL, + .pfunc[0] = PMBUS_HAVE_IOUT, + .pfunc[1] = PMBUS_HAVE_IOUT, + .pfunc[2] = PMBUS_HAVE_IOUT, + .pfunc[3] = PMBUS_HAVE_IOUT, + .pfunc[4] = PMBUS_HAVE_IOUT, + .pfunc[5] = PMBUS_HAVE_IOUT, + .pfunc[6] = PMBUS_HAVE_IOUT, + .pfunc[7] = PMBUS_HAVE_IOUT, + .pfunc[8] = PMBUS_HAVE_IOUT, + .pfunc[9] = PMBUS_HAVE_IOUT, + .read_byte_data = mp2888_read_byte_data, + .read_word_data = mp2888_read_word_data, + .write_word_data = mp2888_write_word_data, +}; + +static int mp2888_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + struct mp2888_data *data; + int ret; + + data = devm_kzalloc(&client->dev, sizeof(struct mp2888_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mp2888_info, sizeof(*info)); + info = &data->info; + + /* Identify multiphase configuration. */ + ret = mp2888_identify_multiphase(client, data, info); + if (ret) + return ret; + + /* Obtain current sense gain of power stage and current resolution. */ + ret = mp2888_current_sense_gain_and_resolution_get(client, data); + if (ret) + return ret; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id mp2888_id[] = { + { .name = "mp2888" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, mp2888_id); + +static const struct of_device_id __maybe_unused mp2888_of_match[] = { + {.compatible = "mps,mp2888"}, + {} +}; +MODULE_DEVICE_TABLE(of, mp2888_of_match); + +static struct i2c_driver mp2888_driver = { + .driver = { + .name = "mp2888", + .of_match_table = of_match_ptr(mp2888_of_match), + }, + .probe = mp2888_probe, + .id_table = mp2888_id, +}; + +module_i2c_driver(mp2888_driver); + +MODULE_AUTHOR("Vadim Pasternak <vadimp@nvidia.com>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP2888 device"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp2891.c b/drivers/hwmon/pmbus/mp2891.c new file mode 100644 index 000000000..316c1fc0d --- /dev/null +++ b/drivers/hwmon/pmbus/mp2891.c @@ -0,0 +1,600 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Multi-phase Digital VR Controllers(MP2891) + */ + +#include <linux/bitfield.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +/* + * Vender specific registers, the register MFR_SVI3_IOUT_PRT(0x65), + * MFR_VOUT_LOOP_CTRL(0xBD), READ_PIN_EST(0x94)and READ_IIN_EST(0x95) + * redefine the standard PMBUS register. The MFR_SVI3_IOUT_PRT(0x65) + * is used to identify the iout scale and the MFR_VOUT_LOOP_CTRL(0xBD) + * is used to identify the vout scale. The READ_PIN_EST(0x94) is used + * to read input power per rail. The MP2891 does not have standard + * READ_IIN register(0x89), the iin telemetry can be obtained through + * the vendor redefined register READ_IIN_EST(0x95). + */ +#define MFR_VOUT_LOOP_CTRL 0xBD +#define READ_PIN_EST 0x94 +#define READ_IIN_EST 0x95 +#define MFR_SVI3_IOUT_PRT 0x65 + +#define MP2891_TEMP_LIMIT_OFFSET 40 +#define MP2891_PIN_LIMIT_UINT 2 +#define MP2891_IOUT_LIMIT_UINT 8 +#define MP2891_IOUT_SCALE_DIV 32 +#define MP2891_VOUT_SCALE_DIV 100 +#define MP2891_OVUV_DELTA_SCALE 50 +#define MP2891_OV_LIMIT_SCALE 20 +#define MP2891_UV_LIMIT_SCALE 5 + +#define MP2891_PAGE_NUM 2 + +#define MP2891_RAIL1_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP | \ + PMBUS_HAVE_POUT | PMBUS_HAVE_PIN | \ + PMBUS_HAVE_IIN | PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_INPUT | \ + PMBUS_HAVE_STATUS_TEMP) + +#define MP2891_RAIL2_FUNC (PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_PIN | PMBUS_HAVE_IIN | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_INPUT | \ + PMBUS_HAVE_STATUS_TEMP) + +struct mp2891_data { + struct pmbus_driver_info info; + int vout_scale[MP2891_PAGE_NUM]; + int iout_scale[MP2891_PAGE_NUM]; +}; + +#define to_mp2891_data(x) container_of(x, struct mp2891_data, info) + +/* Converts a LINEAR11 value to DIRECT format */ +static u16 mp2891_reg2data_linear11(u16 word) +{ + s16 exponent; + s32 mantissa; + s64 val; + + exponent = ((s16)word) >> 11; + mantissa = ((s16)((word & 0x7ff) << 5)) >> 5; + val = mantissa; + + if (exponent >= 0) + val <<= exponent; + else + val >>= -exponent; + + return val; +} + +static int +mp2891_identify_vout_scale(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp2891_data *data = to_mp2891_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_VOUT_LOOP_CTRL); + if (ret < 0) + return ret; + + /* + * The output voltage is equal to the READ_VOUT(0x8B) register value multiplied + * by vout_scale. + * Obtain vout scale from the register MFR_VOUT_LOOP_CTRL, bits 15-14,bit 13. + * If MFR_VOUT_LOOP_CTRL[13] = 1, the vout scale is below: + * 2.5mV/LSB + * If MFR_VOUT_LOOP_CTRL[13] = 0, the vout scale is decided by + * MFR_VOUT_LOOP_CTRL[15:14]: + * 00b - 6.25mV/LSB, 01b - 5mV/LSB, 10b - 2mV/LSB, 11b - 1mV + */ + if (ret & GENMASK(13, 13)) { + data->vout_scale[page] = 250; + } else { + ret = FIELD_GET(GENMASK(15, 14), ret); + if (ret == 0) + data->vout_scale[page] = 625; + else if (ret == 1) + data->vout_scale[page] = 500; + else if (ret == 2) + data->vout_scale[page] = 200; + else + data->vout_scale[page] = 100; + } + + return 0; +} + +static int +mp2891_identify_iout_scale(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp2891_data *data = to_mp2891_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_SVI3_IOUT_PRT); + if (ret < 0) + return ret; + + /* + * The output current is equal to the READ_IOUT(0x8C) register value + * multiplied by iout_scale. + * Obtain iout_scale from the register MFR_SVI3_IOUT_PRT[2:0]. + * The value is selected as below: + * 000b - 1A/LSB, 001b - (1/32)A/LSB, 010b - (1/16)A/LSB, + * 011b - (1/8)A/LSB, 100b - (1/4)A/LSB, 101b - (1/2)A/LSB + * 110b - 1A/LSB, 111b - 2A/LSB + */ + switch (ret & GENMASK(2, 0)) { + case 0: + case 6: + data->iout_scale[page] = 32; + break; + case 1: + data->iout_scale[page] = 1; + break; + case 2: + data->iout_scale[page] = 2; + break; + case 3: + data->iout_scale[page] = 4; + break; + case 4: + data->iout_scale[page] = 8; + break; + case 5: + data->iout_scale[page] = 16; + break; + default: + data->iout_scale[page] = 64; + break; + } + + return 0; +} + +static int mp2891_identify(struct i2c_client *client, struct pmbus_driver_info *info) +{ + int ret; + + /* Identify vout scale for rail 1. */ + ret = mp2891_identify_vout_scale(client, info, 0); + if (ret < 0) + return ret; + + /* Identify vout scale for rail 2. */ + ret = mp2891_identify_vout_scale(client, info, 1); + if (ret < 0) + return ret; + + /* Identify iout scale for rail 1. */ + ret = mp2891_identify_iout_scale(client, info, 0); + if (ret < 0) + return ret; + + /* Identify iout scale for rail 2. */ + return mp2891_identify_iout_scale(client, info, 1); +} + +static int mp2891_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VOUT_MODE: + /* + * The MP2891 does not follow standard PMBus protocol completely, the + * PMBUS_VOUT_MODE(0x20) in MP2891 is reserved and 0x00 is always + * returned when the register is read. But the calculation of vout in + * this driver is based on direct format. As a result, the format of + * vout is enforced to direct. + */ + ret = PB_VOUT_MODE_DIRECT; + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int mp2891_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2891_data *data = to_mp2891_data(info); + int ret; + + switch (reg) { + case PMBUS_READ_VIN: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = ret & GENMASK(9, 0); + break; + case PMBUS_READ_IIN: + /* + * The MP2891 does not have standard PMBUS_READ_IIN register(0x89), + * the iin telemetry can be obtained through the vender redefined + * register READ_IIN_EST(0x95). The MP2891 PMBUS_READ_IIN register + * is linear11 format, But the pout scale is set to 1A/Lsb(using + * r/m/b scale). As a result, the iin read from MP2891 should be + * calculated to A, then return the result to pmbus core. + */ + ret = pmbus_read_word_data(client, page, phase, READ_IIN_EST); + if (ret < 0) + return ret; + + ret = mp2891_reg2data_linear11(ret); + break; + case PMBUS_READ_PIN: + /* + * The MP2891 has standard PMBUS_READ_PIN register(0x97), but this + * is not used to read the input power per rail. The input power + * per rail is read through the vender redefined register + * READ_PIN_EST(0x94). The MP2891 PMBUS_READ_PIN register is linear11 + * format, But the pout scale is set to 1W/Lsb(using r/m/b scale). + * As a result, the pin read from MP2891 should be calculated to W, + * then return the result to pmbus core. + */ + ret = pmbus_read_word_data(client, page, phase, READ_PIN_EST); + if (ret < 0) + return ret; + + ret = mp2891_reg2data_linear11(ret); + break; + case PMBUS_READ_POUT: + /* + * The MP2891 PMBUS_READ_POUT register is linear11 format, and the + * exponent is not a constant value. But the pout scale is set to + * 1W/Lsb(using r/m/b scale). As a result, the pout read from MP2891 + * should be calculated to W, then return the result to pmbus core. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = mp2891_reg2data_linear11(ret); + break; + case PMBUS_READ_VOUT: + case PMBUS_VOUT_UV_WARN_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST(ret * data->vout_scale[page], MP2891_VOUT_SCALE_DIV); + break; + case PMBUS_READ_IOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(10, 0)) * data->iout_scale[page], + MP2891_IOUT_SCALE_DIV); + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * The scale of MP2891 PMBUS_OT_FAULT_LIMIT and PMBUS_OT_WARN_LIMIT + * is 1°C/LSB and they have 40°C offset. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = (ret & GENMASK(7, 0)) - MP2891_TEMP_LIMIT_OFFSET; + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + /* + * The MP2891 PMBUS_VIN_OV_FAULT_LIMIT scale is 125mV/Lsb. + * but the vin scale is set to 31.25mV/Lsb(using r/m/b scale). + * As a result, the limit value should be multiplied by 4. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = (ret & GENMASK(7, 0)) * 4; + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(11, 8), ret)) + ret = FIELD_GET(GENMASK(7, 0), ret) * MP2891_UV_LIMIT_SCALE - + (FIELD_GET(GENMASK(11, 8), ret) + 1) * MP2891_OVUV_DELTA_SCALE; + else + ret = FIELD_GET(GENMASK(7, 0), ret) * MP2891_UV_LIMIT_SCALE; + + ret = ret < 0 ? 0 : ret; + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(11, 8), ret)) + ret = FIELD_GET(GENMASK(7, 0), ret) * MP2891_OV_LIMIT_SCALE + + (FIELD_GET(GENMASK(11, 8), ret) + 1) * MP2891_OVUV_DELTA_SCALE; + else + ret = FIELD_GET(GENMASK(7, 0), ret) * MP2891_OV_LIMIT_SCALE; + break; + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(7, 0)) * data->iout_scale[page] * + MP2891_IOUT_LIMIT_UINT, MP2891_IOUT_SCALE_DIV); + break; + case PMBUS_IIN_OC_WARN_LIMIT: + /* + * The scale of PMBUS_IIN_OC_WARN_LIMIT is 0.5A/Lsb, but the iin scale + * is set to 1A/Lsb(using r/m/b scale), so the word data should be + * divided by 2. + */ + ret = pmbus_read_word_data(client, 0, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(9, 0)), 2); + break; + case PMBUS_PIN_OP_WARN_LIMIT: + /* + * The scale of PMBUS_PIN_OP_WARN_LIMIT is 2W/Lsb, but the pin scale + * is set to 1W/Lsb(using r/m/b scale), so the word data should be + * multiplied by 2. + */ + ret = pmbus_read_word_data(client, 0, phase, reg); + if (ret < 0) + return ret; + + ret = (ret & GENMASK(9, 0)) * MP2891_PIN_LIMIT_UINT; + break; + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + ret = -ENODATA; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int mp2891_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2891_data *data = to_mp2891_data(info); + int ret; + + switch (reg) { + case PMBUS_VOUT_UV_WARN_LIMIT: + ret = pmbus_write_word_data(client, page, reg, + DIV_ROUND_CLOSEST(word * MP2891_VOUT_SCALE_DIV, + data->vout_scale[page])); + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + /* + * The PMBUS_VOUT_UV_FAULT_LIMIT[7:0] is the limit value, and bit8-bit15 + * should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(11, 8), ret)) + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), + DIV_ROUND_CLOSEST(word + + (FIELD_GET(GENMASK(11, 8), ret) + 1) * + MP2891_OVUV_DELTA_SCALE, + MP2891_UV_LIMIT_SCALE))); + else + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), + DIV_ROUND_CLOSEST(word, + MP2891_UV_LIMIT_SCALE))); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + /* + * The PMBUS_VOUT_OV_FAULT_LIMIT[7:0] is the limit value, and bit8-bit15 + * should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(11, 8), ret)) + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), + DIV_ROUND_CLOSEST(word - + (FIELD_GET(GENMASK(11, 8), ret) + 1) * + MP2891_OVUV_DELTA_SCALE, + MP2891_OV_LIMIT_SCALE))); + else + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), + DIV_ROUND_CLOSEST(word, + MP2891_OV_LIMIT_SCALE))); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + /* + * The PMBUS_VIN_OV_FAULT_LIMIT[7:0] is the limit value, and bit8-bit15 + * should not be changed. The scale of PMBUS_VIN_OV_FAULT_LIMIT is 125mV/Lsb, + * but the vin scale is set to 31.25mV/Lsb(using r/m/b scale), so the word data + * should be divided by 4. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), + DIV_ROUND_CLOSEST(word, 4))); + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * The scale of MP2891 PMBUS_OT_FAULT_LIMIT and PMBUS_OT_WARN_LIMIT + * have 40°C offset. The bit0-bit7 is the limit value, and bit8-bit15 + * should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), word + MP2891_TEMP_LIMIT_OFFSET)); + break; + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + ret = pmbus_write_word_data(client, page, reg, + DIV_ROUND_CLOSEST(word * MP2891_IOUT_SCALE_DIV, + MP2891_IOUT_LIMIT_UINT * + data->iout_scale[page])); + break; + case PMBUS_IIN_OC_WARN_LIMIT: + /* + * The scale of PMBUS_IIN_OC_WARN_LIMIT is 0.5A/Lsb, but the iin scale + * is set to 1A/Lsb(using r/m/b scale), so the word data should be + * multiplied by 2. + */ + ret = pmbus_write_word_data(client, page, reg, word * 2); + break; + case PMBUS_PIN_OP_WARN_LIMIT: + /* + * The scale of PMBUS_PIN_OP_WARN_LIMIT is 2W/Lsb, but the pin scale + * is set to 1W/Lsb(using r/m/b scale), so the word data should be + * divided by 2. + */ + ret = pmbus_write_word_data(client, page, reg, + DIV_ROUND_CLOSEST(word, MP2891_PIN_LIMIT_UINT)); + break; + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + ret = -ENODATA; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static const struct pmbus_driver_info mp2891_info = { + .pages = MP2891_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_POWER] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + + /* set vin scale 31.25mV/Lsb */ + .m[PSC_VOLTAGE_IN] = 32, + .R[PSC_VOLTAGE_IN] = 0, + .b[PSC_VOLTAGE_IN] = 0, + + /* set temp scale 1000m°C/Lsb */ + .m[PSC_TEMPERATURE] = 1, + .R[PSC_TEMPERATURE] = 0, + .b[PSC_TEMPERATURE] = 0, + + .m[PSC_CURRENT_IN] = 1, + .R[PSC_CURRENT_IN] = 0, + .b[PSC_CURRENT_IN] = 0, + + .m[PSC_CURRENT_OUT] = 1, + .R[PSC_CURRENT_OUT] = 0, + .b[PSC_CURRENT_OUT] = 0, + + .m[PSC_POWER] = 1, + .R[PSC_POWER] = 0, + .b[PSC_POWER] = 0, + + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .b[PSC_VOLTAGE_OUT] = 0, + + .func[0] = MP2891_RAIL1_FUNC, + .func[1] = MP2891_RAIL2_FUNC, + .read_word_data = mp2891_read_word_data, + .write_word_data = mp2891_write_word_data, + .read_byte_data = mp2891_read_byte_data, + .identify = mp2891_identify, +}; + +static int mp2891_probe(struct i2c_client *client) +{ + struct mp2891_data *data; + + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mp2891_info, sizeof(mp2891_info)); + + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id mp2891_id[] = { + { .name = "mp2891" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp2891_id); + +static const struct of_device_id __maybe_unused mp2891_of_match[] = { + {.compatible = "mps,mp2891"}, + {} +}; +MODULE_DEVICE_TABLE(of, mp2891_of_match); + +static struct i2c_driver mp2891_driver = { + .driver = { + .name = "mp2891", + .of_match_table = mp2891_of_match, + }, + .probe = mp2891_probe, + .id_table = mp2891_id, +}; + +module_i2c_driver(mp2891_driver); + +MODULE_AUTHOR("Noah Wang <noahwang.wang@outlook.com>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP2891"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp2925.c b/drivers/hwmon/pmbus/mp2925.c new file mode 100644 index 000000000..0a58b1ffd --- /dev/null +++ b/drivers/hwmon/pmbus/mp2925.c @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Multi-phase Digital VR Controllers(MP2925) + */ + +#include <linux/bitfield.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +/* + * Vender specific register MFR_VR_MULTI_CONFIG(0x08). + * This register is used to obtain vid scale. + */ +#define MFR_VR_MULTI_CONFIG 0x08 + +#define MP2925_VOUT_DIV 512 +#define MP2925_VOUT_OVUV_UINT 195 +#define MP2925_VOUT_OVUV_DIV 100 + +#define MP2925_PAGE_NUM 2 + +#define MP2925_RAIL1_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_PIN | \ + PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | \ + PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +#define MP2925_RAIL2_FUNC (PMBUS_HAVE_PIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_IIN | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +struct mp2925_data { + struct pmbus_driver_info info; + int vout_scale[MP2925_PAGE_NUM]; + int vid_offset[MP2925_PAGE_NUM]; +}; + +#define to_mp2925_data(x) container_of(x, struct mp2925_data, info) + +static u16 mp2925_linear_exp_transfer(u16 word, u16 expect_exponent) +{ + s16 exponent, mantissa, target_exponent; + + exponent = ((s16)word) >> 11; + mantissa = ((s16)((word & 0x7ff) << 5)) >> 5; + target_exponent = (s16)((expect_exponent & 0x1f) << 11) >> 11; + + if (exponent > target_exponent) + mantissa = mantissa << (exponent - target_exponent); + else + mantissa = mantissa >> (target_exponent - exponent); + + return (mantissa & 0x7ff) | ((expect_exponent << 11) & 0xf800); +} + +static int mp2925_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VOUT_MODE: + /* + * The MP2925 does not follow standard PMBus protocol completely, + * and the calculation of vout in this driver is based on direct + * format. As a result, the format of vout is enforced to direct. + */ + ret = PB_VOUT_MODE_DIRECT; + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int mp2925_read_word_data(struct i2c_client *client, int page, int phase, + int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2925_data *data = to_mp2925_data(info); + int ret; + + switch (reg) { + case PMBUS_READ_VOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + /* + * In vid mode, the MP2925 vout telemetry has 49 vid step offset, but + * PMBUS_VOUT_OV_FAULT_LIMIT and PMBUS_VOUT_UV_FAULT_LIMIT do not take + * this into consideration, its resolution is 1.95mV/LSB, as a result, + * format[PSC_VOLTAGE_OUT] can not be set to vid directly. Adding extra + * vid_offset variable for vout telemetry. + */ + ret = DIV_ROUND_CLOSEST(((ret & GENMASK(11, 0)) + data->vid_offset[page]) * + data->vout_scale[page], MP2925_VOUT_DIV); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(11, 0)) * MP2925_VOUT_OVUV_UINT, + MP2925_VOUT_OVUV_DIV); + break; + case PMBUS_STATUS_WORD: + case PMBUS_READ_VIN: + case PMBUS_READ_IOUT: + case PMBUS_READ_POUT: + case PMBUS_READ_PIN: + case PMBUS_READ_IIN: + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + ret = -ENODATA; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int mp2925_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + int ret; + + switch (reg) { + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + /* + * The PMBUS_VIN_OV_FAULT_LIMIT, PMBUS_VIN_OV_WARN_LIMIT, + * PMBUS_VIN_UV_WARN_LIMIT and PMBUS_VIN_UV_FAULT_LIMIT + * of MP2925 is linear11 format, and the exponent is a + * constant value(5'b11100), so the exponent of word + * parameter should be converted to 5'b11100(0x1C). + */ + ret = pmbus_write_word_data(client, page, reg, + mp2925_linear_exp_transfer(word, 0x1C)); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + /* + * The bit0-bit11 is the limit value, and bit12-bit15 + * should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(11, 0)) | + FIELD_PREP(GENMASK(11, 0), + DIV_ROUND_CLOSEST(word * MP2925_VOUT_OVUV_DIV, + MP2925_VOUT_OVUV_UINT))); + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * The PMBUS_OT_FAULT_LIMIT and PMBUS_OT_WARN_LIMIT of + * MP2925 is linear11 format, and the exponent is a + * constant value(5'b00000), so the exponent of word + * parameter should be converted to 5'b00000. + */ + ret = pmbus_write_word_data(client, page, reg, + mp2925_linear_exp_transfer(word, 0x00)); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + /* + * The PMBUS_IOUT_OC_FAULT_LIMIT and PMBUS_IOUT_OC_WARN_LIMIT + * of MP2925 is linear11 format, and the exponent can not be + * changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + mp2925_linear_exp_transfer(word, + FIELD_GET(GENMASK(15, 11), + ret))); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int +mp2925_identify_vout_scale(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp2925_data *data = to_mp2925_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_byte_data(client, PMBUS_VOUT_MODE); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(5, 5), ret)) { + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, + page == 0 ? 3 : 4); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_VR_MULTI_CONFIG); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(5, 5), ret)) + data->vout_scale[page] = 2560; + else + data->vout_scale[page] = 5120; + + /* + * In vid mode, the MP2925 vout telemetry has 49 vid step offset, but + * PMBUS_VOUT_OV_FAULT_LIMIT and PMBUS_VOUT_UV_FAULT_LIMIT do not take + * this into consideration, its resolution is 1.95mV/LSB, as a result, + * format[PSC_VOLTAGE_OUT] can not be set to vid directly. Adding extra + * vid_offset variable for vout telemetry. + */ + data->vid_offset[page] = 49; + } else if (FIELD_GET(GENMASK(4, 4), ret)) { + data->vout_scale[page] = 1; + data->vid_offset[page] = 0; + } else { + data->vout_scale[page] = 512; + data->vid_offset[page] = 0; + } + + return 0; +} + +static int mp2925_identify(struct i2c_client *client, struct pmbus_driver_info *info) +{ + int ret; + + ret = mp2925_identify_vout_scale(client, info, 0); + if (ret < 0) + return ret; + + return mp2925_identify_vout_scale(client, info, 1); +} + +static const struct pmbus_driver_info mp2925_info = { + .pages = MP2925_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .b[PSC_VOLTAGE_OUT] = 0, + + .func[0] = MP2925_RAIL1_FUNC, + .func[1] = MP2925_RAIL2_FUNC, + .read_word_data = mp2925_read_word_data, + .read_byte_data = mp2925_read_byte_data, + .write_word_data = mp2925_write_word_data, + .identify = mp2925_identify, +}; + +static int mp2925_probe(struct i2c_client *client) +{ + struct mp2925_data *data; + + data = devm_kzalloc(&client->dev, sizeof(struct mp2925_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mp2925_info, sizeof(mp2925_info)); + + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id mp2925_id[] = { + { .name = "mp2925" }, + { .name = "mp2929" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp2925_id); + +static const struct of_device_id __maybe_unused mp2925_of_match[] = { + {.compatible = "mps,mp2925"}, + {.compatible = "mps,mp2929"}, + {} +}; +MODULE_DEVICE_TABLE(of, mp2925_of_match); + +static struct i2c_driver mp2925_driver = { + .driver = { + .name = "mp2925", + .of_match_table = mp2925_of_match, + }, + .probe = mp2925_probe, + .id_table = mp2925_id, +}; + +module_i2c_driver(mp2925_driver); + +MODULE_AUTHOR("Wensheng Wang <wenswang@yeah.net>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP2925"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp29502.c b/drivers/hwmon/pmbus/mp29502.c new file mode 100644 index 000000000..afc5e8c07 --- /dev/null +++ b/drivers/hwmon/pmbus/mp29502.c @@ -0,0 +1,670 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Multi-phase Digital VR Controllers(MP29502) + */ + +#include <linux/bitfield.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +#define MFR_VOUT_SCALE_LOOP 0x29 +#define MFR_SVI3_IOUT_PRT 0x67 +#define MFR_READ_PIN_EST 0x94 +#define MFR_READ_IIN_EST 0x95 +#define MFR_VOUT_PROT1 0x3D +#define MFR_VOUT_PROT2 0x51 +#define MFR_SLOPE_CNT_SET 0xA8 +#define MFR_TSNS_FLT_SET 0xBB + +#define MP29502_VIN_OV_GAIN 4 +#define MP29502_TEMP_LIMIT_OFFSET 40 +#define MP29502_READ_VOUT_DIV 1024 +#define MP29502_READ_IOUT_DIV 32 +#define MP29502_IOUT_LIMIT_UINT 8 +#define MP29502_OVUV_LIMIT_SCALE 10 +#define MP28502_VOUT_OV_GAIN 512 +#define MP28502_VOUT_OV_SCALE 40 +#define MP29502_VOUT_UV_OFFSET 36 +#define MP29502_PIN_GAIN 2 +#define MP29502_IIN_DIV 2 + +#define MP29502_PAGE_NUM 1 + +#define MP29502_RAIL_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_PIN | \ + PMBUS_HAVE_IIN | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +struct mp29502_data { + struct pmbus_driver_info info; + int vout_scale; + int vout_bottom_div; + int vout_top_div; + int ovp_div; + int iout_scale; +}; + +#define to_mp29502_data(x) container_of(x, struct mp29502_data, info) + +static u16 mp29502_reg2data_linear11(u16 word) +{ + s16 exponent; + s32 mantissa; + s64 val; + + exponent = ((s16)word) >> 11; + mantissa = ((s16)((word & 0x7ff) << 5)) >> 5; + val = mantissa; + + if (exponent >= 0) + val <<= exponent; + else + val >>= -exponent; + + return val; +} + +static int +mp29502_identify_vout_scale(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp29502_data *data = to_mp29502_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_VOUT_SCALE_LOOP); + if (ret < 0) + return ret; + + switch (FIELD_GET(GENMASK(12, 10), ret)) { + case 0: + data->vout_scale = 6400; + break; + case 1: + data->vout_scale = 5120; + break; + case 2: + data->vout_scale = 2560; + break; + case 3: + data->vout_scale = 2048; + break; + case 4: + data->vout_scale = 1024; + break; + case 5: + data->vout_scale = 4; + break; + case 6: + data->vout_scale = 2; + break; + case 7: + data->vout_scale = 1; + break; + default: + data->vout_scale = 1; + break; + } + + return 0; +} + +static int +mp29502_identify_vout_divider(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp29502_data *data = to_mp29502_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_VOUT_PROT1); + if (ret < 0) + return ret; + + data->vout_bottom_div = FIELD_GET(GENMASK(11, 0), ret); + + ret = i2c_smbus_read_word_data(client, MFR_VOUT_PROT2); + if (ret < 0) + return ret; + + data->vout_top_div = FIELD_GET(GENMASK(14, 0), ret); + + return 0; +} + +static int +mp29502_identify_ovp_divider(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp29502_data *data = to_mp29502_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_SLOPE_CNT_SET); + if (ret < 0) + return ret; + + data->ovp_div = FIELD_GET(GENMASK(9, 0), ret); + + return 0; +} + +static int +mp29502_identify_iout_scale(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp29502_data *data = to_mp29502_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_SVI3_IOUT_PRT); + if (ret < 0) + return ret; + + switch (ret & GENMASK(2, 0)) { + case 0: + case 6: + data->iout_scale = 32; + break; + case 1: + data->iout_scale = 1; + break; + case 2: + data->iout_scale = 2; + break; + case 3: + data->iout_scale = 4; + break; + case 4: + data->iout_scale = 8; + break; + case 5: + data->iout_scale = 16; + break; + default: + data->iout_scale = 64; + break; + } + + return 0; +} + +static int mp29502_read_vout_ov_limit(struct i2c_client *client, struct mp29502_data *data) +{ + int ret; + int ov_value; + + /* + * This is because the vout ov fault limit value comes from + * page1 MFR_TSNS_FLT_SET reg, and other telemetry and limit + * value comes from page0 reg. So the page should be set to + * 0 after the reading of vout ov limit. + */ + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 1); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_TSNS_FLT_SET); + if (ret < 0) + return ret; + + ov_value = DIV_ROUND_CLOSEST(FIELD_GET(GENMASK(12, 7), ret) * + MP28502_VOUT_OV_GAIN * MP28502_VOUT_OV_SCALE, + data->ovp_div); + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + return ov_value; +} + +static int mp29502_write_vout_ov_limit(struct i2c_client *client, u16 word, + struct mp29502_data *data) +{ + int ret; + + /* + * This is because the vout ov fault limit value comes from + * page1 MFR_TSNS_FLT_SET reg, and other telemetry and limit + * value comes from page0 reg. So the page should be set to + * 0 after the writing of vout ov limit. + */ + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 1); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_TSNS_FLT_SET); + if (ret < 0) + return ret; + + ret = i2c_smbus_write_word_data(client, MFR_TSNS_FLT_SET, + (ret & ~GENMASK(12, 7)) | + FIELD_PREP(GENMASK(12, 7), + DIV_ROUND_CLOSEST(word * data->ovp_div, + MP28502_VOUT_OV_GAIN * MP28502_VOUT_OV_SCALE))); + + return i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); +} + +static int mp29502_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + switch (reg) { + case PMBUS_VOUT_MODE: + ret = PB_VOUT_MODE_DIRECT; + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int mp29502_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp29502_data *data = to_mp29502_data(info); + int ret; + + switch (reg) { + case PMBUS_STATUS_WORD: + ret = -ENODATA; + break; + case PMBUS_READ_VIN: + /* + * The MP29502 PMBUS_READ_VIN[10:0] is the vin value, the vin scale is + * 125mV/LSB. And the vin scale is set to 125mV/Lsb(using r/m/b scale) + * in MP29502 pmbus_driver_info struct, so the word data bit0-bit10 can + * be returned to pmbus core directly. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = FIELD_GET(GENMASK(10, 0), ret); + break; + case PMBUS_READ_VOUT: + /* + * The MP29502 PMBUS_READ_VOUT[11:0] is the vout value, and vout + * value is calculated based on vout scale and vout divider. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(11, 0)) * + data->vout_scale * + (data->vout_bottom_div + + 4 * data->vout_top_div), + MP29502_READ_VOUT_DIV * + data->vout_bottom_div); + break; + case PMBUS_READ_IIN: + /* + * The MP29502 MFR_READ_IIN_EST register is linear11 format, and the + * exponent is not a constant value. But the iin scale is set to + * 1A/Lsb(using r/m/b scale). As a result, the iin read from MP29502 + * should be calculated to A, then return the result to pmbus core. + */ + ret = pmbus_read_word_data(client, page, phase, MFR_READ_IIN_EST); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST(mp29502_reg2data_linear11(ret), + MP29502_IIN_DIV); + break; + case PMBUS_READ_PIN: + /* + * The MP29502 MFR_READ_PIN_EST register is linear11 format, and the + * exponent is not a constant value. But the pin scale is set to + * 1W/Lsb(using r/m/b scale). As a result, the pout read from MP29502 + * should be calculated to W, then return the result to pmbus core. + */ + ret = pmbus_read_word_data(client, page, phase, MFR_READ_PIN_EST); + if (ret < 0) + return ret; + + ret = mp29502_reg2data_linear11(ret) * MP29502_PIN_GAIN; + break; + case PMBUS_READ_POUT: + /* + * The MP29502 PMBUS_READ_POUT register is linear11 format, and the + * exponent is not a constant value. But the pout scale is set to + * 1W/Lsb(using r/m/b scale). As a result, the pout read from MP29502 + * should be calculated to W, then return the result to pmbus core. + * And the pout is calculated based on vout divider. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST(mp29502_reg2data_linear11(ret) * + (data->vout_bottom_div + + 4 * data->vout_top_div), + data->vout_bottom_div); + break; + case PMBUS_READ_IOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(10, 0)) * data->iout_scale, + MP29502_READ_IOUT_DIV); + break; + case PMBUS_READ_TEMPERATURE_1: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = FIELD_GET(GENMASK(10, 0), ret); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + /* + * The MP29502 PMBUS_VIN_OV_FAULT_LIMIT is 500mV/Lsb, but + * the vin scale is set to 125mV/Lsb(using r/m/b scale), + * so the word data should multiply by 4. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = FIELD_GET(GENMASK(7, 0), ret) * MP29502_VIN_OV_GAIN; + break; + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + /* + * The MP29502 PMBUS_VIN_UV_WARN_LIMIT and PMBUS_VIN_UV_FAULT_LIMIT + * scale is 125mV/Lsb, but the vin scale is set to 125mV/Lsb(using + * r/m/b scale), so the word data bit0-bit9 can be returned to pmbus + * core directly. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = FIELD_GET(GENMASK(9, 0), ret); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + /* + * The MP29502 vout ov fault limit value comes from + * page1 MFR_TSNS_FLT_SET[12:7]. + */ + ret = mp29502_read_vout_ov_limit(client, data); + if (ret < 0) + return ret; + + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((FIELD_GET(GENMASK(8, 0), ret) * + MP29502_OVUV_LIMIT_SCALE - + MP29502_VOUT_UV_OFFSET) * + (data->vout_bottom_div + + 4 * data->vout_top_div), + data->vout_bottom_div); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(7, 0)) * + data->iout_scale * + MP29502_IOUT_LIMIT_UINT, + MP29502_READ_IOUT_DIV); + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * The scale of MP29502 PMBUS_OT_FAULT_LIMIT and PMBUS_OT_WARN_LIMIT + * is 1°C/LSB and they have 40°C offset. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = (ret & GENMASK(7, 0)) - MP29502_TEMP_LIMIT_OFFSET; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int mp29502_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp29502_data *data = to_mp29502_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + switch (reg) { + case PMBUS_VIN_OV_FAULT_LIMIT: + /* + * The PMBUS_VIN_OV_FAULT_LIMIT[7:0] is the limit value, + * and bit8-bit15 should not be changed. The scale of + * PMBUS_VIN_OV_FAULT_LIMIT is 500mV/Lsb, but the vin + * scale is set to 125mV/Lsb(using r/m/b scale), so + * the word data should divide by 4. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), + DIV_ROUND_CLOSEST(word, + MP29502_VIN_OV_GAIN))); + break; + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + /* + * The PMBUS_VIN_UV_WARN_LIMIT[9:0] and PMBUS_VIN_UV_FAULT_LIMIT[9:0] + * are the limit value, and bit10-bit15 should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(9, 0)) | + FIELD_PREP(GENMASK(9, 0), + word)); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + ret = mp29502_write_vout_ov_limit(client, word, data); + if (ret < 0) + return ret; + + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(8, 0)) | + FIELD_PREP(GENMASK(8, 0), + DIV_ROUND_CLOSEST(word * + data->vout_bottom_div + + MP29502_VOUT_UV_OFFSET * + (data->vout_bottom_div + + 4 * data->vout_top_div), + MP29502_OVUV_LIMIT_SCALE * + (data->vout_bottom_div + + 4 * data->vout_top_div)))); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + ret = pmbus_write_word_data(client, page, reg, + DIV_ROUND_CLOSEST(word * + MP29502_READ_IOUT_DIV, + MP29502_IOUT_LIMIT_UINT * + data->iout_scale)); + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * The PMBUS_OT_FAULT_LIMIT[7:0] and PMBUS_OT_WARN_LIMIT[7:0] + * are the limit value, and bit8-bit15 should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(7, 0)) | + FIELD_PREP(GENMASK(7, 0), + word + MP29502_TEMP_LIMIT_OFFSET)); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int mp29502_identify(struct i2c_client *client, struct pmbus_driver_info *info) +{ + int ret; + + /* Identify vout scale */ + ret = mp29502_identify_vout_scale(client, info, 0); + if (ret < 0) + return ret; + + /* Identify vout divider. */ + ret = mp29502_identify_vout_divider(client, info, 1); + if (ret < 0) + return ret; + + /* Identify ovp divider. */ + ret = mp29502_identify_ovp_divider(client, info, 1); + if (ret < 0) + return ret; + + /* Identify iout scale */ + return mp29502_identify_iout_scale(client, info, 0); +} + +static const struct pmbus_driver_info mp29502_info = { + .pages = MP29502_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_POWER] = direct, + + .m[PSC_VOLTAGE_IN] = 8, + .R[PSC_VOLTAGE_IN] = 0, + .b[PSC_VOLTAGE_IN] = 0, + + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .b[PSC_VOLTAGE_OUT] = 0, + + .m[PSC_TEMPERATURE] = 1, + .R[PSC_TEMPERATURE] = 0, + .b[PSC_TEMPERATURE] = 0, + + .m[PSC_CURRENT_IN] = 1, + .R[PSC_CURRENT_IN] = 0, + .b[PSC_CURRENT_IN] = 0, + + .m[PSC_CURRENT_OUT] = 1, + .R[PSC_CURRENT_OUT] = 0, + .b[PSC_CURRENT_OUT] = 0, + + .m[PSC_POWER] = 1, + .R[PSC_POWER] = 0, + .b[PSC_POWER] = 0, + + .func[0] = MP29502_RAIL_FUNC, + .read_word_data = mp29502_read_word_data, + .read_byte_data = mp29502_read_byte_data, + .write_word_data = mp29502_write_word_data, + .identify = mp29502_identify, +}; + +static int mp29502_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + struct mp29502_data *data; + + data = devm_kzalloc(&client->dev, sizeof(struct mp29502_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mp29502_info, sizeof(*info)); + info = &data->info; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id mp29502_id[] = { + { .name = "mp29502", .driver_data = 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp29502_id); + +static const struct of_device_id __maybe_unused mp29502_of_match[] = { + {.compatible = "mps,mp29502"}, + {} +}; +MODULE_DEVICE_TABLE(of, mp29502_of_match); + +static struct i2c_driver mp29502_driver = { + .driver = { + .name = "mp29502", + .of_match_table = mp29502_of_match, + }, + .probe = mp29502_probe, + .id_table = mp29502_id, +}; + +module_i2c_driver(mp29502_driver); + +MODULE_AUTHOR("Wensheng Wang <wenswang@yeah.net"); +MODULE_DESCRIPTION("PMBus driver for MPS MP29502"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp2975.c b/drivers/hwmon/pmbus/mp2975.c new file mode 100644 index 000000000..8e5193933 --- /dev/null +++ b/drivers/hwmon/pmbus/mp2975.c @@ -0,0 +1,1100 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Multi-phase Digital VR Controllers + * + * Copyright (C) 2020 Nvidia Technologies Ltd. + */ + +#include <linux/bitops.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> + +#include "pmbus.h" + +/* Vendor specific registers. */ +#define MP2975_MFR_APS_HYS_R2 0x0d +#define MP2975_MFR_SLOPE_TRIM3 0x1d +#define MP2975_MFR_VR_MULTI_CONFIG_R1 0x0d +#define MP2975_MFR_VR_MULTI_CONFIG_R2 0x1d +#define MP2975_MFR_APS_DECAY_ADV 0x56 +#define MP2975_MFR_DC_LOOP_CTRL 0x59 +#define MP2975_MFR_OCP_UCP_PHASE_SET 0x65 +#define MP2975_MFR_VR_CONFIG1 0x68 +#define MP2975_MFR_READ_CS1_2 0x82 +#define MP2975_MFR_READ_CS3_4 0x83 +#define MP2975_MFR_READ_CS5_6 0x84 +#define MP2975_MFR_READ_CS7_8 0x85 +#define MP2975_MFR_READ_CS9_10 0x86 +#define MP2975_MFR_READ_CS11_12 0x87 +#define MP2975_MFR_READ_IOUT_PK 0x90 +#define MP2975_MFR_READ_POUT_PK 0x91 +#define MP2975_MFR_READ_VREF_R1 0xa1 +#define MP2975_MFR_READ_VREF_R2 0xa3 +#define MP2975_MFR_OVP_TH_SET 0xe5 +#define MP2975_MFR_UVP_SET 0xe6 + +#define MP2973_MFR_RESO_SET 0xc7 + +#define MP2975_VOUT_FORMAT BIT(15) +#define MP2975_VID_STEP_SEL_R1 BIT(4) +#define MP2975_IMVP9_EN_R1 BIT(13) +#define MP2975_VID_STEP_SEL_R2 BIT(3) +#define MP2975_IMVP9_EN_R2 BIT(12) +#define MP2975_PRT_THRES_DIV_OV_EN BIT(14) +#define MP2975_DRMOS_KCS GENMASK(13, 12) +#define MP2975_PROT_DEV_OV_OFF 10 +#define MP2975_PROT_DEV_OV_ON 5 +#define MP2975_SENSE_AMPL BIT(11) +#define MP2975_SENSE_AMPL_UNIT 1 +#define MP2975_SENSE_AMPL_HALF 2 +#define MP2975_VIN_UV_LIMIT_UNIT 8 + +#define MP2973_VOUT_FORMAT_R1 GENMASK(7, 6) +#define MP2973_VOUT_FORMAT_R2 GENMASK(4, 3) +#define MP2973_VOUT_FORMAT_DIRECT_R1 BIT(7) +#define MP2973_VOUT_FORMAT_LINEAR_R1 BIT(6) +#define MP2973_VOUT_FORMAT_DIRECT_R2 BIT(4) +#define MP2973_VOUT_FORMAT_LINEAR_R2 BIT(3) + +#define MP2973_MFR_VR_MULTI_CONFIG_R1 0x0d +#define MP2973_MFR_VR_MULTI_CONFIG_R2 0x1d +#define MP2973_VID_STEP_SEL_R1 BIT(4) +#define MP2973_IMVP9_EN_R1 BIT(14) +#define MP2973_VID_STEP_SEL_R2 BIT(3) +#define MP2973_IMVP9_EN_R2 BIT(13) + +#define MP2973_MFR_OCP_TOTAL_SET 0x5f +#define MP2973_OCP_TOTAL_CUR_MASK GENMASK(6, 0) +#define MP2973_MFR_OCP_LEVEL_RES BIT(15) + +#define MP2973_MFR_READ_IOUT_PK 0x90 +#define MP2973_MFR_READ_POUT_PK 0x91 + +#define MP2975_MAX_PHASE_RAIL1 8 +#define MP2975_MAX_PHASE_RAIL2 4 + +#define MP2973_MAX_PHASE_RAIL1 14 +#define MP2973_MAX_PHASE_RAIL2 6 + +#define MP2971_MAX_PHASE_RAIL1 8 +#define MP2971_MAX_PHASE_RAIL2 3 + +#define MP2975_PAGE_NUM 2 + +#define MP2975_RAIL2_FUNC (PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_POUT | PMBUS_PHASE_VIRTUAL) + +enum chips { + mp2971, mp2973, mp2975 +}; + +static const int mp2975_max_phases[][MP2975_PAGE_NUM] = { + [mp2975] = { MP2975_MAX_PHASE_RAIL1, MP2975_MAX_PHASE_RAIL2 }, + [mp2973] = { MP2973_MAX_PHASE_RAIL1, MP2973_MAX_PHASE_RAIL2 }, + [mp2971] = { MP2971_MAX_PHASE_RAIL1, MP2971_MAX_PHASE_RAIL2 }, +}; + +struct mp2975_driver_info { + const struct pmbus_driver_info *info; + enum chips chip_id; +}; + +struct mp2975_data { + struct pmbus_driver_info info; + enum chips chip_id; + int vout_scale; + int max_phases[MP2975_PAGE_NUM]; + int vid_step[MP2975_PAGE_NUM]; + int vref[MP2975_PAGE_NUM]; + int vref_off[MP2975_PAGE_NUM]; + int vout_max[MP2975_PAGE_NUM]; + int vout_ov_fixed[MP2975_PAGE_NUM]; + int curr_sense_gain[MP2975_PAGE_NUM]; +}; + +static const struct regulator_desc __maybe_unused mp2975_reg_desc[] = { + PMBUS_REGULATOR("vout", 0), + PMBUS_REGULATOR("vout", 1), +}; + +#define to_mp2975_data(x) container_of(x, struct mp2975_data, info) + +static int mp2975_read_byte_data(struct i2c_client *client, int page, int reg) +{ + switch (reg) { + case PMBUS_VOUT_MODE: + /* + * Report direct format as configured by MFR_DC_LOOP_CTRL. + * Unlike on MP2971/MP2973 the reported VOUT_MODE isn't automatically + * internally updated, but always reads as PB_VOUT_MODE_VID. + */ + return PB_VOUT_MODE_DIRECT; + default: + return -ENODATA; + } +} + +static int +mp2975_read_word_helper(struct i2c_client *client, int page, int phase, u8 reg, + u16 mask) +{ + int ret = pmbus_read_word_data(client, page, phase, reg); + + return (ret > 0) ? ret & mask : ret; +} + +static int +mp2975_vid2direct(int vrf, int val) +{ + switch (vrf) { + case vr12: + if (val >= 0x01) + return 250 + (val - 1) * 5; + break; + case vr13: + if (val >= 0x01) + return 500 + (val - 1) * 10; + break; + case imvp9: + if (val >= 0x01) + return 200 + (val - 1) * 10; + break; + default: + return -EINVAL; + } + return 0; +} + +#define MAX_LIN_MANTISSA (1023 * 1000) +#define MIN_LIN_MANTISSA (511 * 1000) + +/* Converts a milli-unit DIRECT value to LINEAR11 format */ +static u16 mp2975_data2reg_linear11(s64 val) +{ + s16 exponent = 0, mantissa; + + /* simple case */ + if (val == 0) + return 0; + + /* Reduce large mantissa until it fits into 10 bit */ + while (val >= MAX_LIN_MANTISSA && exponent < 15) { + exponent++; + val >>= 1; + } + /* Increase small mantissa to improve precision */ + while (val < MIN_LIN_MANTISSA && exponent > -15) { + exponent--; + val <<= 1; + } + + /* Convert mantissa from milli-units to units */ + mantissa = clamp_val(DIV_ROUND_CLOSEST_ULL(val, 1000), 0, 0x3ff); + + /* Convert to 5 bit exponent, 11 bit mantissa */ + return (mantissa & 0x7ff) | ((exponent << 11) & 0xf800); +} + +static int +mp2975_read_phase(struct i2c_client *client, struct mp2975_data *data, + int page, int phase, u8 reg) +{ + int ph_curr, ret; + + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + if (!((phase + 1) % MP2975_PAGE_NUM)) + ret >>= 8; + ret &= 0xff; + + /* + * Output value is calculated as: (READ_CSx / 80 – 1.23) / (Kcs * Rcs) + * where: + * - Kcs is the DrMOS current sense gain of power stage, which is + * obtained from the register MP2975_MFR_VR_CONFIG1, bits 13-12 with + * the following selection of DrMOS (data->curr_sense_gain[page]): + * 00b - 5µA/A, 01b - 8.5µA/A, 10b - 9.7µA/A, 11b - 10µA/A. + * - Rcs is the internal phase current sense resistor which is constant + * value 1kΩ. + */ + ph_curr = ret * 100 - 9800; + + /* + * Current phase sensing, providing by the device is not accurate + * for the light load. This because sampling of current occurrence of + * bit weight has a big deviation for light load. For handling such + * case phase current is represented as the maximum between the value + * calculated above and total rail current divided by number phases. + */ + ret = pmbus_read_word_data(client, page, phase, PMBUS_READ_IOUT); + if (ret < 0) + return ret; + + return max_t(int, DIV_ROUND_CLOSEST(ret, data->info.phases[page]), + DIV_ROUND_CLOSEST(ph_curr, data->curr_sense_gain[page])); +} + +static int +mp2975_read_phases(struct i2c_client *client, struct mp2975_data *data, + int page, int phase) +{ + int ret; + + if (page) { + switch (phase) { + case 0 ... 1: + ret = mp2975_read_phase(client, data, page, phase, + MP2975_MFR_READ_CS7_8); + break; + case 2 ... 3: + ret = mp2975_read_phase(client, data, page, phase, + MP2975_MFR_READ_CS9_10); + break; + case 4 ... 5: + ret = mp2975_read_phase(client, data, page, phase, + MP2975_MFR_READ_CS11_12); + break; + default: + return -ENODATA; + } + } else { + switch (phase) { + case 0 ... 1: + ret = mp2975_read_phase(client, data, page, phase, + MP2975_MFR_READ_CS1_2); + break; + case 2 ... 3: + ret = mp2975_read_phase(client, data, page, phase, + MP2975_MFR_READ_CS3_4); + break; + case 4 ... 5: + ret = mp2975_read_phase(client, data, page, phase, + MP2975_MFR_READ_CS5_6); + break; + case 6 ... 7: + ret = mp2975_read_phase(client, data, page, phase, + MP2975_MFR_READ_CS7_8); + break; + case 8 ... 9: + ret = mp2975_read_phase(client, data, page, phase, + MP2975_MFR_READ_CS9_10); + break; + case 10 ... 11: + ret = mp2975_read_phase(client, data, page, phase, + MP2975_MFR_READ_CS11_12); + break; + default: + return -ENODATA; + } + } + return ret; +} + +static int mp2973_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2975_data *data = to_mp2975_data(info); + int ret; + + switch (reg) { + case PMBUS_STATUS_WORD: + /* MP2973 & MP2971 return PGOOD instead of PB_STATUS_POWER_GOOD_N. */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + ret ^= PB_STATUS_POWER_GOOD_N; + break; + case PMBUS_OT_FAULT_LIMIT: + ret = mp2975_read_word_helper(client, page, phase, reg, + GENMASK(7, 0)); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + ret = mp2975_read_word_helper(client, page, phase, reg, + GENMASK(7, 0)); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST(ret, MP2975_VIN_UV_LIMIT_UNIT); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + /* + * MP2971 and mp2973 only supports tracking (ovp1) mode. + */ + ret = mp2975_read_word_helper(client, page, phase, + MP2975_MFR_OVP_TH_SET, + GENMASK(2, 0)); + if (ret < 0) + return ret; + + ret = data->vout_max[page] + 50 * (ret + 1); + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = mp2975_read_word_helper(client, page, phase, reg, + GENMASK(8, 0)); + if (ret < 0) + return ret; + ret = mp2975_vid2direct(info->vrm_version[page], ret); + break; + case PMBUS_VIRT_READ_POUT_MAX: + ret = pmbus_read_word_data(client, page, phase, + MP2973_MFR_READ_POUT_PK); + break; + case PMBUS_VIRT_READ_IOUT_MAX: + ret = pmbus_read_word_data(client, page, phase, + MP2973_MFR_READ_IOUT_PK); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + ret = mp2975_read_word_helper(client, page, phase, + MP2973_MFR_OCP_TOTAL_SET, + GENMASK(15, 0)); + if (ret < 0) + return ret; + + if (ret & MP2973_MFR_OCP_LEVEL_RES) + ret = 2 * (ret & MP2973_OCP_TOTAL_CUR_MASK); + else + ret = ret & MP2973_OCP_TOTAL_CUR_MASK; + + ret = mp2975_data2reg_linear11(ret * info->phases[page] * 1000); + break; + case PMBUS_UT_WARN_LIMIT: + case PMBUS_UT_FAULT_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_VOUT_OV_WARN_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_IIN_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_LV_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_IOUT_UC_FAULT_LIMIT: + case PMBUS_POUT_OP_FAULT_LIMIT: + case PMBUS_POUT_OP_WARN_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + return -ENXIO; + default: + return -ENODATA; + } + + return ret; +} + +static int mp2973_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + u8 target, mask; + long ret; + + if (reg != PMBUS_SMBALERT_MASK) + return -ENODATA; + + /* + * Vendor-specific SMBALERT_MASK register with 16 maskable bits. + */ + ret = pmbus_read_word_data(client, 0, 0, PMBUS_SMBALERT_MASK); + if (ret < 0) + return ret; + + target = word & 0xff; + mask = word >> 8; + +/* + * Set/Clear 'bit' in 'ret' based on condition followed by define for each bit in SMBALERT_MASK. + * Also bit 2 & 15 are reserved. + */ + +#define MP2973_TEMP_OT 0 +#define MP2973_VIN_UVLO 1 +#define MP2973_VIN_OVP 3 +#define MP2973_MTP_FAULT 4 +#define MP2973_OTHER_COMM 5 +#define MP2973_MTP_BLK_TRIG 6 +#define MP2973_PACKET_ERROR 7 +#define MP2973_INVALID_DATA 8 +#define MP2973_INVALID_COMMAND 9 +#define MP2973_IOUT_OC_LV 10 +#define MP2973_IOUT_OC 11 +#define MP2973_VOUT_MAX_MIN_WARNING 12 +#define MP2973_VOLTAGE_UV 13 +#define MP2973_VOLTAGE_OV 14 + + switch (target) { + case PMBUS_STATUS_CML: + __assign_bit(MP2973_INVALID_DATA, &ret, !(mask & PB_CML_FAULT_INVALID_DATA)); + __assign_bit(MP2973_INVALID_COMMAND, &ret, !(mask & PB_CML_FAULT_INVALID_COMMAND)); + __assign_bit(MP2973_OTHER_COMM, &ret, !(mask & PB_CML_FAULT_OTHER_COMM)); + __assign_bit(MP2973_PACKET_ERROR, &ret, !(mask & PB_CML_FAULT_PACKET_ERROR)); + break; + case PMBUS_STATUS_VOUT: + __assign_bit(MP2973_VOLTAGE_UV, &ret, !(mask & PB_VOLTAGE_UV_FAULT)); + __assign_bit(MP2973_VOLTAGE_OV, &ret, !(mask & PB_VOLTAGE_OV_FAULT)); + break; + case PMBUS_STATUS_IOUT: + __assign_bit(MP2973_IOUT_OC, &ret, !(mask & PB_IOUT_OC_FAULT)); + __assign_bit(MP2973_IOUT_OC_LV, &ret, !(mask & PB_IOUT_OC_LV_FAULT)); + break; + case PMBUS_STATUS_TEMPERATURE: + __assign_bit(MP2973_TEMP_OT, &ret, !(mask & PB_TEMP_OT_FAULT)); + break; + /* + * Map remaining bits to MFR specific to let the PMBUS core mask + * those bits by default. + */ + case PMBUS_STATUS_MFR_SPECIFIC: + __assign_bit(MP2973_VIN_UVLO, &ret, !(mask & BIT(1))); + __assign_bit(MP2973_VIN_OVP, &ret, !(mask & BIT(3))); + __assign_bit(MP2973_MTP_FAULT, &ret, !(mask & BIT(4))); + __assign_bit(MP2973_MTP_BLK_TRIG, &ret, !(mask & BIT(6))); + break; + default: + return 0; + } + + return pmbus_write_word_data(client, 0, PMBUS_SMBALERT_MASK, ret); +} + +static int mp2975_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2975_data *data = to_mp2975_data(info); + int ret; + + switch (reg) { + case PMBUS_OT_FAULT_LIMIT: + ret = mp2975_read_word_helper(client, page, phase, reg, + GENMASK(7, 0)); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + ret = mp2975_read_word_helper(client, page, phase, reg, + GENMASK(7, 0)); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST(ret, MP2975_VIN_UV_LIMIT_UNIT); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + /* + * Register provides two values for over-voltage protection + * threshold for fixed (ovp2) and tracking (ovp1) modes. The + * minimum of these two values is provided as over-voltage + * fault alarm. + */ + ret = mp2975_read_word_helper(client, page, phase, + MP2975_MFR_OVP_TH_SET, + GENMASK(2, 0)); + if (ret < 0) + return ret; + + ret = min_t(int, data->vout_max[page] + 50 * (ret + 1), + data->vout_ov_fixed[page]); + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = mp2975_read_word_helper(client, page, phase, + MP2975_MFR_UVP_SET, + GENMASK(2, 0)); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST(data->vref[page] * 10 - 50 * + (ret + 1) * data->vout_scale, 10); + break; + case PMBUS_VIRT_READ_POUT_MAX: + ret = mp2975_read_word_helper(client, page, phase, + MP2975_MFR_READ_POUT_PK, + GENMASK(12, 0)); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST(ret, 4); + break; + case PMBUS_VIRT_READ_IOUT_MAX: + ret = mp2975_read_word_helper(client, page, phase, + MP2975_MFR_READ_IOUT_PK, + GENMASK(12, 0)); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST(ret, 4); + break; + case PMBUS_READ_IOUT: + ret = mp2975_read_phases(client, data, page, phase); + if (ret < 0) + return ret; + + break; + case PMBUS_UT_WARN_LIMIT: + case PMBUS_UT_FAULT_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_VOUT_OV_WARN_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_IIN_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_LV_FAULT_LIMIT: + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_UC_FAULT_LIMIT: + case PMBUS_POUT_OP_FAULT_LIMIT: + case PMBUS_POUT_OP_WARN_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + return -ENXIO; + default: + return -ENODATA; + } + + return ret; +} + +static int mp2975_identify_multiphase_rail2(struct i2c_client *client, + struct mp2975_data *data) +{ + int ret; + + /* + * Identify multiphase for rail 2 - could be from 0 to data->max_phases[1]. + * In case phase number is zero – only page zero is supported + */ + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 2); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MP2975_MFR_VR_MULTI_CONFIG_R2); + if (ret < 0) + return ret; + + ret &= GENMASK(2, 0); + return (ret >= data->max_phases[1]) ? data->max_phases[1] : ret; +} + +static void mp2975_set_phase_rail1(struct pmbus_driver_info *info) +{ + int i; + + for (i = 0 ; i < info->phases[0]; i++) + info->pfunc[i] = PMBUS_HAVE_IOUT; +} + +static void +mp2975_set_phase_rail2(struct pmbus_driver_info *info, int num_phases) +{ + int i; + + /* Set phases for rail 2 from upper to lower. */ + for (i = 1; i <= num_phases; i++) + info->pfunc[MP2975_MAX_PHASE_RAIL1 - i] = PMBUS_HAVE_IOUT; +} + +static int +mp2975_identify_multiphase(struct i2c_client *client, struct mp2975_data *data, + struct pmbus_driver_info *info) +{ + int num_phases2, ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 2); + if (ret < 0) + return ret; + + /* Identify multiphase for rail 1 - could be from 1 to data->max_phases[0]. */ + ret = i2c_smbus_read_word_data(client, MP2975_MFR_VR_MULTI_CONFIG_R1); + if (ret <= 0) + return ret; + + info->phases[0] = ret & GENMASK(3, 0); + + /* + * The device provides a total of $n PWM pins, and can be configured + * to different phase count applications for rail 1 and rail 2. + * Rail 1 can be set to $n phases, while rail 2 can be set to less than + * that. When rail 1’s phase count is configured as 0, rail + * 1 operates with 1-phase DCM. When rail 2 phase count is configured + * as 0, rail 2 is disabled. + */ + if (info->phases[0] > data->max_phases[0]) + return -EINVAL; + + if (data->chip_id == mp2975) { + mp2975_set_phase_rail1(info); + num_phases2 = min(data->max_phases[0] - info->phases[0], + data->max_phases[1]); + if (info->phases[1] && info->phases[1] <= num_phases2) + mp2975_set_phase_rail2(info, num_phases2); + } + + return 0; +} + +static int +mp2975_identify_vid(struct i2c_client *client, struct mp2975_data *data, + struct pmbus_driver_info *info, u32 reg, int page, + u32 imvp_bit, u32 vr_bit) +{ + int ret; + + /* Identify VID mode and step selection. */ + ret = i2c_smbus_read_word_data(client, reg); + if (ret < 0) + return ret; + + if (ret & imvp_bit) { + info->vrm_version[page] = imvp9; + data->vid_step[page] = MP2975_PROT_DEV_OV_OFF; + } else if (ret & vr_bit) { + info->vrm_version[page] = vr12; + data->vid_step[page] = MP2975_PROT_DEV_OV_ON; + } else { + info->vrm_version[page] = vr13; + data->vid_step[page] = MP2975_PROT_DEV_OV_OFF; + } + + return 0; +} + +static int +mp2975_identify_rails_vid(struct i2c_client *client, struct mp2975_data *data, + struct pmbus_driver_info *info) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 2); + if (ret < 0) + return ret; + + /* Identify VID mode for rail 1. */ + ret = mp2975_identify_vid(client, data, info, + MP2975_MFR_VR_MULTI_CONFIG_R1, 0, + MP2975_IMVP9_EN_R1, MP2975_VID_STEP_SEL_R1); + if (ret < 0) + return ret; + + /* Identify VID mode for rail 2, if connected. */ + if (info->phases[1]) + ret = mp2975_identify_vid(client, data, info, + MP2975_MFR_VR_MULTI_CONFIG_R2, 1, + MP2975_IMVP9_EN_R2, + MP2975_VID_STEP_SEL_R2); + + return ret; +} + +static int +mp2973_identify_rails_vid(struct i2c_client *client, struct mp2975_data *data, + struct pmbus_driver_info *info) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 2); + if (ret < 0) + return ret; + + /* Identify VID mode for rail 1. */ + ret = mp2975_identify_vid(client, data, info, + MP2973_MFR_VR_MULTI_CONFIG_R1, 0, + MP2973_IMVP9_EN_R1, MP2973_VID_STEP_SEL_R1); + + if (ret < 0) + return ret; + + /* Identify VID mode for rail 2, if connected. */ + if (info->phases[1]) + ret = mp2975_identify_vid(client, data, info, + MP2973_MFR_VR_MULTI_CONFIG_R2, 1, + MP2973_IMVP9_EN_R2, + MP2973_VID_STEP_SEL_R2); + + return ret; +} + +static int +mp2975_current_sense_gain_get(struct i2c_client *client, + struct mp2975_data *data) +{ + int i, ret; + + /* + * Obtain DrMOS current sense gain of power stage from the register + * MP2975_MFR_VR_CONFIG1, bits 13-12. The value is selected as below: + * 00b - 5µA/A, 01b - 8.5µA/A, 10b - 9.7µA/A, 11b - 10µA/A. Other + * values are invalid. + */ + for (i = 0 ; i < data->info.pages; i++) { + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, i); + if (ret < 0) + return ret; + ret = i2c_smbus_read_word_data(client, + MP2975_MFR_VR_CONFIG1); + if (ret < 0) + return ret; + + switch ((ret & MP2975_DRMOS_KCS) >> 12) { + case 0: + data->curr_sense_gain[i] = 50; + break; + case 1: + data->curr_sense_gain[i] = 85; + break; + case 2: + data->curr_sense_gain[i] = 97; + break; + default: + data->curr_sense_gain[i] = 100; + break; + } + } + + return 0; +} + +static int +mp2975_vref_get(struct i2c_client *client, struct mp2975_data *data, + struct pmbus_driver_info *info) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 3); + if (ret < 0) + return ret; + + /* Get voltage reference value for rail 1. */ + ret = i2c_smbus_read_word_data(client, MP2975_MFR_READ_VREF_R1); + if (ret < 0) + return ret; + + data->vref[0] = ret * data->vid_step[0]; + + /* Get voltage reference value for rail 2, if connected. */ + if (data->info.pages == MP2975_PAGE_NUM) { + ret = i2c_smbus_read_word_data(client, MP2975_MFR_READ_VREF_R2); + if (ret < 0) + return ret; + + data->vref[1] = ret * data->vid_step[1]; + } + return 0; +} + +static int +mp2975_vref_offset_get(struct i2c_client *client, struct mp2975_data *data, + int page) +{ + int ret; + + ret = i2c_smbus_read_word_data(client, MP2975_MFR_OVP_TH_SET); + if (ret < 0) + return ret; + + switch ((ret & GENMASK(5, 3)) >> 3) { + case 1: + data->vref_off[page] = 140; + break; + case 2: + data->vref_off[page] = 220; + break; + case 4: + data->vref_off[page] = 400; + break; + default: + return -EINVAL; + } + return 0; +} + +static int +mp2975_vout_max_get(struct i2c_client *client, struct mp2975_data *data, + struct pmbus_driver_info *info, int page) +{ + int ret; + + /* Get maximum reference voltage of VID-DAC in VID format. */ + ret = i2c_smbus_read_word_data(client, PMBUS_VOUT_MAX); + if (ret < 0) + return ret; + + data->vout_max[page] = mp2975_vid2direct(info->vrm_version[page], ret & + GENMASK(8, 0)); + return 0; +} + +static int +mp2975_set_vout_format(struct i2c_client *client, + struct mp2975_data *data, int page) +{ + int ret, i; + + /* Enable DIRECT VOUT format 1mV/LSB */ + if (data->chip_id == mp2975) { + ret = i2c_smbus_read_word_data(client, MP2975_MFR_DC_LOOP_CTRL); + if (ret < 0) + return ret; + if (ret & MP2975_VOUT_FORMAT) { + ret &= ~MP2975_VOUT_FORMAT; + ret = i2c_smbus_write_word_data(client, MP2975_MFR_DC_LOOP_CTRL, ret); + } + } else { + ret = i2c_smbus_read_word_data(client, MP2973_MFR_RESO_SET); + if (ret < 0) + return ret; + i = ret; + + if (page == 0) { + i &= ~MP2973_VOUT_FORMAT_R1; + i |= MP2973_VOUT_FORMAT_DIRECT_R1; + } else { + i &= ~MP2973_VOUT_FORMAT_R2; + i |= MP2973_VOUT_FORMAT_DIRECT_R2; + } + if (i != ret) + ret = i2c_smbus_write_word_data(client, MP2973_MFR_RESO_SET, i); + } + return ret; +} + +static int +mp2975_vout_ov_scale_get(struct i2c_client *client, struct mp2975_data *data, + struct pmbus_driver_info *info) +{ + int thres_dev, sense_ampl, ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + /* + * Get divider for over- and under-voltage protection thresholds + * configuration from the Advanced Options of Auto Phase Shedding and + * decay register. + */ + ret = i2c_smbus_read_word_data(client, MP2975_MFR_APS_DECAY_ADV); + if (ret < 0) + return ret; + thres_dev = ret & MP2975_PRT_THRES_DIV_OV_EN ? MP2975_PROT_DEV_OV_ON : + MP2975_PROT_DEV_OV_OFF; + + /* Select the gain of remote sense amplifier. */ + ret = i2c_smbus_read_word_data(client, PMBUS_VOUT_SCALE_LOOP); + if (ret < 0) + return ret; + sense_ampl = ret & MP2975_SENSE_AMPL ? MP2975_SENSE_AMPL_HALF : + MP2975_SENSE_AMPL_UNIT; + + data->vout_scale = sense_ampl * thres_dev; + + return 0; +} + +static int +mp2975_vout_per_rail_config_get(struct i2c_client *client, + struct mp2975_data *data, + struct pmbus_driver_info *info) +{ + int i, ret; + + for (i = 0; i < data->info.pages; i++) { + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, i); + if (ret < 0) + continue; + + /* Set VOUT format for READ_VOUT command : direct. */ + ret = mp2975_set_vout_format(client, data, i); + if (ret < 0) + return ret; + + /* Obtain maximum voltage values. */ + ret = mp2975_vout_max_get(client, data, info, i); + if (ret < 0) + return ret; + + /* Skip if reading Vref is unsupported */ + if (data->chip_id != mp2975) + continue; + + /* Obtain voltage reference offsets. */ + ret = mp2975_vref_offset_get(client, data, i); + if (ret < 0) + return ret; + + /* + * Set over-voltage fixed value. Thresholds are provided as + * fixed value, and tracking value. The minimum of them are + * exposed as over-voltage critical threshold. + */ + data->vout_ov_fixed[i] = data->vref[i] + + DIV_ROUND_CLOSEST(data->vref_off[i] * + data->vout_scale, + 10); + } + + return 0; +} + +static const struct pmbus_driver_info mp2975_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .m[PSC_TEMPERATURE] = 1, + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .m[PSC_CURRENT_OUT] = 1, + .m[PSC_POWER] = 1, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | PMBUS_HAVE_POUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT | PMBUS_PHASE_VIRTUAL, + .read_byte_data = mp2975_read_byte_data, + .read_word_data = mp2975_read_word_data, +#if IS_ENABLED(CONFIG_SENSORS_MP2975_REGULATOR) + .num_regulators = 1, + .reg_desc = mp2975_reg_desc, +#endif +}; + +static const struct pmbus_driver_info mp2973_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | PMBUS_HAVE_POUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT, + .read_word_data = mp2973_read_word_data, + .write_word_data = mp2973_write_word_data, +#if IS_ENABLED(CONFIG_SENSORS_MP2975_REGULATOR) + .num_regulators = 1, + .reg_desc = mp2975_reg_desc, +#endif +}; + +static const struct mp2975_driver_info mp2975_ddinfo[] = { + [mp2975] = { .info = &mp2975_info, .chip_id = mp2975 }, + [mp2973] = { .info = &mp2973_info, .chip_id = mp2973 }, + [mp2971] = { .info = &mp2973_info, .chip_id = mp2971 }, +}; + +static int mp2975_probe(struct i2c_client *client) +{ + const struct mp2975_driver_info *ddinfo; + struct pmbus_driver_info *info; + struct mp2975_data *data; + int ret; + + ddinfo = i2c_get_match_data(client); + if (!ddinfo) + return -ENODEV; + + data = devm_kzalloc(&client->dev, sizeof(struct mp2975_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->chip_id = ddinfo->chip_id; + + memcpy(data->max_phases, mp2975_max_phases[data->chip_id], + sizeof(data->max_phases)); + + memcpy(&data->info, ddinfo->info, sizeof(data->info)); + + info = &data->info; + + /* Identify multiphase configuration for rail 2. */ + ret = mp2975_identify_multiphase_rail2(client, data); + if (ret < 0) + return ret; + + if (ret) { + /* Two rails are connected. */ + data->info.pages = MP2975_PAGE_NUM; + data->info.phases[1] = ret; + data->info.func[1] = MP2975_RAIL2_FUNC; + if (IS_ENABLED(CONFIG_SENSORS_MP2975_REGULATOR)) + data->info.num_regulators = MP2975_PAGE_NUM; + } + + /* Identify multiphase configuration. */ + ret = mp2975_identify_multiphase(client, data, info); + if (ret) + return ret; + + if (data->chip_id == mp2975) { + /* Identify VID setting per rail. */ + ret = mp2975_identify_rails_vid(client, data, info); + if (ret < 0) + return ret; + + /* Obtain current sense gain of power stage. */ + ret = mp2975_current_sense_gain_get(client, data); + if (ret) + return ret; + + /* Obtain voltage reference values. */ + ret = mp2975_vref_get(client, data, info); + if (ret) + return ret; + + /* Obtain vout over-voltage scales. */ + ret = mp2975_vout_ov_scale_get(client, data, info); + if (ret < 0) + return ret; + } else { + /* Identify VID setting per rail. */ + ret = mp2973_identify_rails_vid(client, data, info); + if (ret < 0) + return ret; + } + + /* Obtain offsets, maximum and format for vout. */ + ret = mp2975_vout_per_rail_config_get(client, data, info); + if (ret) + return ret; + + return pmbus_do_probe(client, info); +} + +static const struct of_device_id mp2975_of_match[] = { + {.compatible = "mps,mp2971", .data = &mp2975_ddinfo[mp2971]}, + {.compatible = "mps,mp2973", .data = &mp2975_ddinfo[mp2973]}, + {.compatible = "mps,mp2975", .data = &mp2975_ddinfo[mp2975]}, + {} +}; +MODULE_DEVICE_TABLE(of, mp2975_of_match); + +static const struct i2c_device_id mp2975_id[] = { + { .name = "mp2971", .driver_data = (kernel_ulong_t)&mp2975_ddinfo[mp2971] }, + { .name = "mp2973", .driver_data = (kernel_ulong_t)&mp2975_ddinfo[mp2973] }, + { .name = "mp2975", .driver_data = (kernel_ulong_t)&mp2975_ddinfo[mp2975] }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp2975_id); + +static struct i2c_driver mp2975_driver = { + .driver = { + .name = "mp2975", + .of_match_table = mp2975_of_match, + }, + .probe = mp2975_probe, + .id_table = mp2975_id, +}; + +module_i2c_driver(mp2975_driver); + +MODULE_AUTHOR("Vadim Pasternak <vadimp@nvidia.com>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP2975 device"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp2985.c b/drivers/hwmon/pmbus/mp2985.c new file mode 100644 index 000000000..b0a4796ca --- /dev/null +++ b/drivers/hwmon/pmbus/mp2985.c @@ -0,0 +1,402 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Multi-phase Digital VR Controllers(MP2985) + * + * Copyright (C) 2026 MPS + */ + +#include <linux/bitfield.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +/* + * Vender specific register READ_PIN_EST(0x93), READ_IIN_EST(0x8E), + * MFR_VR_MULTI_CONFIG_R1(0x0D) and MFR_VR_MULTI_CONFIG_R2(0x1D). + * The READ_PIN_EST is used to read pin telemetry, the READ_IIN_EST + * is used to read iin telemetry and the MFR_VR_MULTI_CONFIG_R1, + * MFR_VR_MULTI_CONFIG_R2 are used to obtain vid scale. + */ +#define READ_PIN_EST 0x93 +#define READ_IIN_EST 0x8E +#define MFR_VR_MULTI_CONFIG_R1 0x0D +#define MFR_VR_MULTI_CONFIG_R2 0x1D + +#define MP2985_VOUT_DIV 64 +#define MP2985_VOUT_OVUV_UINT 125 +#define MP2985_VOUT_OVUV_DIV 64 + +#define MP2985_PAGE_NUM 2 + +#define MP2985_RAIL1_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_PIN | \ + PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | \ + PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +#define MP2985_RAIL2_FUNC (PMBUS_HAVE_PIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_IIN | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +struct mp2985_data { + struct pmbus_driver_info info; + int vout_scale[MP2985_PAGE_NUM]; + int vid_offset[MP2985_PAGE_NUM]; +}; + +#define to_mp2985_data(x) container_of(x, struct mp2985_data, info) + +static u16 mp2985_linear_exp_transfer(u16 word, u16 expect_exponent) +{ + s16 exponent, mantissa, target_exponent; + + exponent = ((s16)word) >> 11; + mantissa = ((s16)((word & 0x7ff) << 5)) >> 5; + target_exponent = (s16)((expect_exponent & 0x1f) << 11) >> 11; + + /* + * The MP2985 does not support negtive limit value, if a negtive + * limit value is written, the limit value will become to 0. And + * the maximum positive limit value is limitted to 0x3FF. + */ + if (mantissa < 0) { + mantissa = 0; + } else { + if (exponent > target_exponent) { + mantissa = (1023 >> (exponent - target_exponent)) >= mantissa ? + mantissa << (exponent - target_exponent) : + 0x3FF; + } else { + mantissa = clamp_val(mantissa >> (target_exponent - exponent), + 0, 0x3FF); + } + } + + return mantissa | ((expect_exponent << 11) & 0xf800); +} + +static int mp2985_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VOUT_MODE: + /* + * The MP2985 does not follow standard PMBus protocol completely, + * and the calculation of vout in this driver is based on direct + * format. As a result, the format of vout is enforced to direct. + */ + ret = PB_VOUT_MODE_DIRECT; + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int mp2985_read_word_data(struct i2c_client *client, int page, int phase, + int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp2985_data *data = to_mp2985_data(info); + int ret; + + switch (reg) { + case PMBUS_READ_VOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + /* + * The MP2985 supports three vout mode, direct, linear11 and vid mode. + * In vid mode, the MP2985 vout telemetry has 49 vid step offset, but + * PMBUS_VOUT_OV_FAULT_LIMIT and PMBUS_VOUT_UV_FAULT_LIMIT do not take + * this into consideration, their resolution are 1.953125mV/LSB, as a + * result, format[PSC_VOLTAGE_OUT] can not be set to vid mode directly. + * Adding extra vid_offset variable for vout telemetry. + */ + ret = clamp_val(DIV_ROUND_CLOSEST(((ret & GENMASK(11, 0)) + + data->vid_offset[page]) * + data->vout_scale[page], MP2985_VOUT_DIV), + 0, 0x7FFF); + break; + case PMBUS_READ_IIN: + /* + * The MP2985 has standard PMBUS_READ_IIN register(0x89), but this is + * not used to read the input current of per rail. The input current + * is read through the vender redefined register READ_IIN_EST(0x8E). + */ + ret = pmbus_read_word_data(client, page, phase, READ_IIN_EST); + break; + case PMBUS_READ_PIN: + /* + * The MP2985 has standard PMBUS_READ_PIN register(0x97), but this + * is not used to read the input power of per rail. The input power + * of per rail is read through the vender redefined register + * READ_PIN_EST(0x93). + */ + ret = pmbus_read_word_data(client, page, phase, READ_PIN_EST); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(11, 0)) * MP2985_VOUT_OVUV_UINT, + MP2985_VOUT_OVUV_DIV); + break; + case PMBUS_STATUS_WORD: + case PMBUS_READ_VIN: + case PMBUS_READ_IOUT: + case PMBUS_READ_POUT: + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * These register is not explicitly handled by the driver, + * as a result, return -ENODATA directly. + */ + ret = -ENODATA; + break; + default: + /* + * The MP2985 do not support other telemetry and limit value + * reading, so, return -EINVAL directly. + */ + ret = -EINVAL; + break; + } + + return ret; +} + +static int mp2985_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + int ret; + + switch (reg) { + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + /* + * The PMBUS_VIN_OV_FAULT_LIMIT, PMBUS_VIN_OV_WARN_LIMIT, + * PMBUS_VIN_UV_WARN_LIMIT and PMBUS_VIN_UV_FAULT_LIMIT + * of MP2985 is linear11 format, and the exponent is a + * constant value(5'b11101), so the exponent of word + * parameter should be converted to 5'b11101(0x1D). + */ + ret = pmbus_write_word_data(client, page, reg, + mp2985_linear_exp_transfer(word, 0x1D)); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + /* + * The bit0-bit11 is the limit value, and bit12-bit15 + * should not be changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + (ret & ~GENMASK(11, 0)) | + clamp_val(DIV_ROUND_CLOSEST(word * MP2985_VOUT_OVUV_DIV, + MP2985_VOUT_OVUV_UINT), 0, 0xFFF)); + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * The PMBUS_OT_FAULT_LIMIT and PMBUS_OT_WARN_LIMIT of + * MP2985 is linear11 format, and the exponent is a + * constant value(5'b00000), so the exponent of word + * parameter should be converted to 5'b00000. + */ + ret = pmbus_write_word_data(client, page, reg, + mp2985_linear_exp_transfer(word, 0x00)); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + /* + * The PMBUS_IOUT_OC_FAULT_LIMIT and PMBUS_IOUT_OC_WARN_LIMIT + * of MP2985 is linear11 format, and the exponent can not be + * changed. + */ + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + + ret = pmbus_write_word_data(client, page, reg, + mp2985_linear_exp_transfer(word, + FIELD_GET(GENMASK(15, 11), + ret))); + break; + default: + /* + * The MP2985 do not support other limit value configuration, + * so, return -EINVAL directly. + */ + ret = -EINVAL; + break; + } + + return ret; +} + +static int +mp2985_identify_vout_scale(struct i2c_client *client, struct pmbus_driver_info *info, + int page) +{ + struct mp2985_data *data = to_mp2985_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_byte_data(client, PMBUS_VOUT_MODE); + if (ret < 0) + return ret; + + /* + * The MP2985 supports three vout mode. If PMBUS_VOUT_MODE + * bit5 is 1, it is vid mode. If PMBUS PMBUS_VOUT_MODE bit4 + * is 1, it is linear11 mode, the vout scale is 1.953125mv/LSB. + * If PMBUS PMBUS_VOUT_MODE bit6 is 1, it is direct mode, the + * vout scale is 1mv/LSB. In vid mode, the MP2985 vout telemetry + * has 49 vid step offset. + */ + if (FIELD_GET(BIT(5), ret)) { + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 2); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, page == 0 ? + MFR_VR_MULTI_CONFIG_R1 : + MFR_VR_MULTI_CONFIG_R2); + if (ret < 0) + return ret; + + if (page == 0) { + if (FIELD_GET(BIT(4), ret)) + data->vout_scale[page] = 320; + else + data->vout_scale[page] = 640; + } else { + if (FIELD_GET(BIT(3), ret)) + data->vout_scale[page] = 320; + else + data->vout_scale[page] = 640; + } + + data->vid_offset[page] = 49; + + /* + * For vid mode, the MP2985 should be changed to page 2 + * to obtain vout scale value, this may confuse the PMBus + * core. To avoid this, switch back to the previous page + * again. + */ + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + } else if (FIELD_GET(BIT(4), ret)) { + data->vout_scale[page] = 125; + data->vid_offset[page] = 0; + } else { + data->vout_scale[page] = 64; + data->vid_offset[page] = 0; + } + + return 0; +} + +static int mp2985_identify(struct i2c_client *client, struct pmbus_driver_info *info) +{ + int ret; + + ret = mp2985_identify_vout_scale(client, info, 0); + if (ret < 0) + return ret; + + return mp2985_identify_vout_scale(client, info, 1); +} + +static struct pmbus_driver_info mp2985_info = { + .pages = MP2985_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .b[PSC_VOLTAGE_OUT] = 0, + + .func[0] = MP2985_RAIL1_FUNC, + .func[1] = MP2985_RAIL2_FUNC, + .read_word_data = mp2985_read_word_data, + .read_byte_data = mp2985_read_byte_data, + .write_word_data = mp2985_write_word_data, + .identify = mp2985_identify, +}; + +static int mp2985_probe(struct i2c_client *client) +{ + struct mp2985_data *data; + + data = devm_kzalloc(&client->dev, sizeof(struct mp2985_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mp2985_info, sizeof(mp2985_info)); + + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id mp2985_id[] = { + {"mp2985", 0}, + {} +}; +MODULE_DEVICE_TABLE(i2c, mp2985_id); + +static const struct of_device_id __maybe_unused mp2985_of_match[] = { + {.compatible = "mps,mp2985"}, + {} +}; +MODULE_DEVICE_TABLE(of, mp2985_of_match); + +static struct i2c_driver mp2985_driver = { + .driver = { + .name = "mp2985", + .of_match_table = mp2985_of_match, + }, + .probe = mp2985_probe, + .id_table = mp2985_id, +}; + +module_i2c_driver(mp2985_driver); + +MODULE_AUTHOR("Wensheng Wang <wenswang@yeah.net>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP2985 device"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp2993.c b/drivers/hwmon/pmbus/mp2993.c new file mode 100644 index 000000000..3a6a6c55a --- /dev/null +++ b/drivers/hwmon/pmbus/mp2993.c @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Multi-phase Digital VR Controllers(MP2993) + */ + +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +#define MP2993_VOUT_OVUV_UINT 125 +#define MP2993_VOUT_OVUV_DIV 64 +#define MP2993_VIN_LIMIT_UINT 1 +#define MP2993_VIN_LIMIT_DIV 8 +#define MP2993_READ_VIN_UINT 1 +#define MP2993_READ_VIN_DIV 32 + +#define MP2993_PAGE_NUM 2 + +#define MP2993_RAIL1_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_PIN | \ + PMBUS_HAVE_IIN | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +#define MP2993_RAIL2_FUNC (PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | \ + PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +/* Converts a linear11 data exponent to a specified value */ +static u16 mp2993_linear11_exponent_transfer(u16 word, u16 expect_exponent) +{ + s16 exponent, mantissa, target_exponent; + + exponent = ((s16)word) >> 11; + mantissa = ((s16)((word & 0x7ff) << 5)) >> 5; + target_exponent = (s16)((expect_exponent & 0x1f) << 11) >> 11; + + if (exponent > target_exponent) + mantissa = mantissa << (exponent - target_exponent); + else + mantissa = mantissa >> (target_exponent - exponent); + + return (mantissa & 0x7ff) | ((expect_exponent << 11) & 0xf800); +} + +static int +mp2993_set_vout_format(struct i2c_client *client, int page, int format) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + if (ret < 0) + return ret; + + return i2c_smbus_write_byte_data(client, PMBUS_VOUT_MODE, format); +} + +static int mp2993_identify(struct i2c_client *client, struct pmbus_driver_info *info) +{ + int ret; + + /* Set vout to direct format for rail1. */ + ret = mp2993_set_vout_format(client, 0, PB_VOUT_MODE_DIRECT); + if (ret < 0) + return ret; + + /* Set vout to direct format for rail2. */ + return mp2993_set_vout_format(client, 1, PB_VOUT_MODE_DIRECT); +} + +static int mp2993_read_word_data(struct i2c_client *client, int page, int phase, + int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST(ret * MP2993_VOUT_OVUV_UINT, MP2993_VOUT_OVUV_DIV); + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * The MP2993 ot fault limit value and ot warn limit value + * per rail are always the same, so only PMBUS_OT_FAULT_LIMIT + * and PMBUS_OT_WARN_LIMIT register in page 0 are defined to + * indicates the limit value. + */ + ret = pmbus_read_word_data(client, 0, phase, reg); + break; + case PMBUS_READ_VIN: + /* The MP2993 vin scale is (1/32V)/Lsb */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(9, 0)) * MP2993_READ_VIN_UINT, + MP2993_READ_VIN_DIV); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + /* The MP2993 vin limit scale is (1/8V)/Lsb */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(7, 0)) * MP2993_VIN_LIMIT_UINT, + MP2993_VIN_LIMIT_DIV); + break; + case PMBUS_READ_IOUT: + case PMBUS_READ_IIN: + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_READ_VOUT: + case PMBUS_READ_PIN: + case PMBUS_READ_POUT: + case PMBUS_READ_TEMPERATURE_1: + ret = -ENODATA; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int mp2993_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + int ret; + + switch (reg) { + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = DIV_ROUND_CLOSEST(word * MP2993_VOUT_OVUV_DIV, MP2993_VOUT_OVUV_UINT); + ret = pmbus_write_word_data(client, 0, reg, ret); + break; + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + /* + * The MP2993 ot fault limit value and ot warn limit value + * per rail are always the same, so only PMBUS_OT_FAULT_LIMIT + * and PMBUS_OT_WARN_LIMIT register in page 0 are defined to + * config the ot limit value. + */ + ret = pmbus_write_word_data(client, 0, reg, word); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + /* The MP2993 vin limit scale is (1/8V)/Lsb */ + ret = pmbus_write_word_data(client, 0, reg, + DIV_ROUND_CLOSEST(word * MP2993_VIN_LIMIT_DIV, + MP2993_VIN_LIMIT_UINT)); + break; + case PMBUS_IIN_OC_WARN_LIMIT: + /* + * The PMBUS_IIN_OC_WARN_LIMIT of MP2993 is linear11 format, + * and the exponent is a constant value(5'b00000), so the + * exponent of word parameter should be converted to 5'b00000. + */ + ret = pmbus_write_word_data(client, page, reg, + mp2993_linear11_exponent_transfer(word, 0x00)); + break; + // + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + /* + * The PMBUS_IOUT_OC_FAULT_LIMIT and PMBUS_IOUT_OC_WARN_LIMIT + * of MP2993 can be regarded as linear11 format, and the + * exponent is a 5'b00001 or 5'b00000. To ensure a larger + * range of limit value, so the exponent of word parameter + * should be converted to 5'b00001. + */ + ret = pmbus_write_word_data(client, page, reg, + mp2993_linear11_exponent_transfer(word, 0x01)); + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static struct pmbus_driver_info mp2993_info = { + .pages = MP2993_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_POWER] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .b[PSC_VOLTAGE_OUT] = 0, + + .m[PSC_VOLTAGE_IN] = 1, + .R[PSC_VOLTAGE_IN] = 0, + .b[PSC_VOLTAGE_IN] = 0, + + .m[PSC_TEMPERATURE] = 1, + .R[PSC_TEMPERATURE] = 0, + .b[PSC_TEMPERATURE] = 0, + + .func[0] = MP2993_RAIL1_FUNC, + .func[1] = MP2993_RAIL2_FUNC, + .read_word_data = mp2993_read_word_data, + .write_word_data = mp2993_write_word_data, + .identify = mp2993_identify, +}; + +static int mp2993_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &mp2993_info); +} + +static const struct i2c_device_id mp2993_id[] = { + { .name = "mp2993" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp2993_id); + +static const struct of_device_id __maybe_unused mp2993_of_match[] = { + {.compatible = "mps,mp2993"}, + {} +}; +MODULE_DEVICE_TABLE(of, mp2993_of_match); + +static struct i2c_driver mp2993_driver = { + .driver = { + .name = "mp2993", + .of_match_table = mp2993_of_match, + }, + .probe = mp2993_probe, + .id_table = mp2993_id, +}; + +module_i2c_driver(mp2993_driver); + +MODULE_AUTHOR("Noah Wang <noahwang.wang@outlook.com>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP2993"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp5023.c b/drivers/hwmon/pmbus/mp5023.c new file mode 100644 index 000000000..c466d67e9 --- /dev/null +++ b/drivers/hwmon/pmbus/mp5023.c @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for MPS MP5023 Hot-Swap Controller + */ + +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of.h> +#include "pmbus.h" + +static struct pmbus_driver_info mp5023_info = { + .pages = 1, + + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + + .m[PSC_VOLTAGE_IN] = 32, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + .m[PSC_VOLTAGE_OUT] = 32, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .m[PSC_CURRENT_OUT] = 16, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 0, + .m[PSC_POWER] = 1, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = 0, + .m[PSC_TEMPERATURE] = 2, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 0, + + .func[0] = + PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_PIN | + PMBUS_HAVE_TEMP | PMBUS_HAVE_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, +}; + +static int mp5023_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &mp5023_info); +} + +static const struct of_device_id __maybe_unused mp5023_of_match[] = { + { .compatible = "mps,mp5023", }, + {} +}; + +MODULE_DEVICE_TABLE(of, mp5023_of_match); + +static struct i2c_driver mp5023_driver = { + .driver = { + .name = "mp5023", + .of_match_table = of_match_ptr(mp5023_of_match), + }, + .probe = mp5023_probe, +}; + +module_i2c_driver(mp5023_driver); + +MODULE_AUTHOR("Howard Chiu <howard.chiu@quantatw.com>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP5023 HSC"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp5920.c b/drivers/hwmon/pmbus/mp5920.c new file mode 100644 index 000000000..b803f3ddf --- /dev/null +++ b/drivers/hwmon/pmbus/mp5920.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MP5920 and compatible chips. + */ + +#include <linux/i2c.h> +#include <linux/minmax.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +static struct pmbus_driver_info mp5920_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 2266, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -1, + .m[PSC_VOLTAGE_OUT] = 2266, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = -1, + .m[PSC_CURRENT_OUT] = 546, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = -2, + .m[PSC_POWER] = 5840, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = -3, + .m[PSC_TEMPERATURE] = 1067, + .b[PSC_TEMPERATURE] = 20500, + .R[PSC_TEMPERATURE] = -2, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP, +}; + +static int mp5920_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + int ret; + u8 buf[I2C_SMBUS_BLOCK_MAX]; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_WORD_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) + return dev_err_probe(dev, ret, "Failed to read PMBUS_MFR_MODEL\n"); + + if (ret != 6 || strncmp(buf, "MP5920", 6)) { + return dev_err_probe(dev, -ENODEV, "Model '%.*s' not supported\n", + min_t(int, ret, sizeof(buf)), buf); + } + + return pmbus_do_probe(client, &mp5920_info); +} + +static const struct of_device_id mp5920_of_match[] = { + { .compatible = "mps,mp5920" }, + { } +}; + +MODULE_DEVICE_TABLE(of, mp5920_of_match); + +static const struct i2c_device_id mp5920_id[] = { + { .name = "mp5920" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, mp5920_id); + +static struct i2c_driver mp5920_driver = { + .driver = { + .name = "mp5920", + .of_match_table = mp5920_of_match, + }, + .probe = mp5920_probe, + .id_table = mp5920_id, +}; + +module_i2c_driver(mp5920_driver); + +MODULE_AUTHOR("Tony Ao <tony_ao@wiwynn.com>"); +MODULE_AUTHOR("Alex Vdovydchenko <xzeol@yahoo.com>"); +MODULE_DESCRIPTION("PMBus driver for MP5920 HSC"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp5926.c b/drivers/hwmon/pmbus/mp5926.c new file mode 100644 index 000000000..a40647472 --- /dev/null +++ b/drivers/hwmon/pmbus/mp5926.c @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * mp5926.c - pmbus driver for mps mp5926 + * + * Copyright 2025 Monolithic Power Systems, Inc + * + * Author: Yuxi Wang <Yuxi.Wang@monolithicpower.com> + */ + +#include <linux/bitfield.h> +#include <linux/bits.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +#define PAGE 0x01 +#define EFUSE_CFG 0xCF +#define I_SCALE_SEL 0xC6 +#define MP5926_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_INPUT | \ + PMBUS_HAVE_STATUS_TEMP | PMBUS_HAVE_STATUS_VOUT) + +struct mp5926_data { + struct pmbus_driver_info info; + u8 vout_mode; + u8 vout_linear_exponent; +}; + +#define to_mp5926_data(x) container_of(x, struct mp5926_data, info) + +static int mp5926_read_byte_data(struct i2c_client *client, int page, + int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp5926_data *data = to_mp5926_data(info); + int ret; + + switch (reg) { + case PMBUS_VOUT_MODE: + if (data->vout_mode == linear) { + /* + * The VOUT format used by the chip is linear11, + * not linear16. Report that VOUT is in linear mode + * and return exponent value extracted while probing + * the chip. + */ + return data->vout_linear_exponent; + } + return PB_VOUT_MODE_DIRECT; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static int mp5926_read_word_data(struct i2c_client *client, int page, int phase, + int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp5926_data *data = to_mp5926_data(info); + int ret; + + switch (reg) { + case PMBUS_READ_VOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + /* + * Because the VOUT format used by the chip is linear11 and not + * linear16, we disregard bits[15:11]. The exponent is reported + * as part of the VOUT_MODE command. + */ + if (data->vout_mode == linear) + ret = ((s16)((ret & 0x7ff) << 5)) >> 5; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static struct pmbus_driver_info mp5926_info = { + .pages = PAGE, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_POWER] = direct, + + .m[PSC_VOLTAGE_IN] = 16, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + + .m[PSC_CURRENT_IN] = 16, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = 0, + + .m[PSC_VOLTAGE_OUT] = 16, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + + .m[PSC_TEMPERATURE] = 4, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 0, + + .m[PSC_POWER] = 25, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = -2, + + .read_word_data = mp5926_read_word_data, + .read_byte_data = mp5926_read_byte_data, + .func[0] = MP5926_FUNC, +}; + +static int mp5926_probe(struct i2c_client *client) +{ + struct mp5926_data *data; + struct pmbus_driver_info *info; + int ret; + + data = devm_kzalloc(&client->dev, sizeof(struct mp5926_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mp5926_info, sizeof(*info)); + info = &data->info; + ret = i2c_smbus_read_word_data(client, EFUSE_CFG); + if (ret < 0) + return ret; + if (ret & BIT(12)) { + data->vout_mode = linear; + data->info.format[PSC_VOLTAGE_IN] = linear; + data->info.format[PSC_CURRENT_IN] = linear; + data->info.format[PSC_VOLTAGE_OUT] = linear; + data->info.format[PSC_TEMPERATURE] = linear; + data->info.format[PSC_POWER] = linear; + ret = i2c_smbus_read_word_data(client, PMBUS_READ_VOUT); + if (ret < 0) + return dev_err_probe(&client->dev, ret, "Can't get vout exponent."); + data->vout_linear_exponent = (u8)((ret >> 11) & 0x1f); + } else { + data->vout_mode = direct; + ret = i2c_smbus_read_word_data(client, I_SCALE_SEL); + if (ret < 0) + return ret; + if (ret & BIT(6)) + data->info.m[PSC_CURRENT_IN] = 4; + } + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id mp5926_id[] = { + { .name = "mp5926", .driver_data = 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp5926_id); + +static const struct of_device_id mp5926_of_match[] = { + { .compatible = "mps,mp5926" }, + {} +}; +MODULE_DEVICE_TABLE(of, mp5926_of_match); + +static struct i2c_driver mp5926_driver = { + .probe = mp5926_probe, + .driver = { + .name = "mp5926", + .of_match_table = mp5926_of_match, + }, + .id_table = mp5926_id, +}; + +module_i2c_driver(mp5926_driver); +MODULE_AUTHOR("Yuxi Wang <Yuxi.Wang@monolithicpower.com>"); +MODULE_DESCRIPTION("MPS MP5926 pmbus driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp5990.c b/drivers/hwmon/pmbus/mp5990.c new file mode 100644 index 000000000..f8a5d1b42 --- /dev/null +++ b/drivers/hwmon/pmbus/mp5990.c @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for MPS MP5990 Hot-Swap Controller + */ + +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +enum chips { mp5990, mp5998 }; + +#define MP5990_EFUSE_CFG (0xC4) +#define MP5990_VOUT_FORMAT BIT(9) + +struct mp5990_data { + struct pmbus_driver_info info; + u8 vout_mode; + u8 vout_linear_exponent; +}; + +#define to_mp5990_data(x) container_of(x, struct mp5990_data, info) + +static int mp5990_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp5990_data *data = to_mp5990_data(info); + + switch (reg) { + case PMBUS_VOUT_MODE: + if (data->vout_mode == linear) { + /* + * The VOUT format used by the chip is linear11, + * not linear16. Report that VOUT is in linear mode + * and return exponent value extracted while probing + * the chip. + */ + return data->vout_linear_exponent; + } + + /* + * The datasheet does not support the VOUT command, + * but the device responds with a default value of 0x17. + * In the standard, 0x17 represents linear mode. + * Therefore, we should report that VOUT is in direct + * format when the chip is configured for it. + */ + return PB_VOUT_MODE_DIRECT; + + default: + return -ENODATA; + } +} + +static int mp5990_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp5990_data *data = to_mp5990_data(info); + int ret; + s32 mantissa; + + switch (reg) { + case PMBUS_READ_VOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + /* + * Because the VOUT format used by the chip is linear11 and not + * linear16, we disregard bits[15:11]. The exponent is reported + * as part of the VOUT_MODE command. + */ + if (data->vout_mode == linear) { + mantissa = ((s16)((ret & 0x7ff) << 5)) >> 5; + ret = mantissa; + } + break; + default: + return -ENODATA; + } + + return ret; +} + +static struct pmbus_driver_info mp5990_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 32, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + .m[PSC_VOLTAGE_OUT] = 32, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .m[PSC_CURRENT_OUT] = 16, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 0, + .m[PSC_POWER] = 1, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = 0, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 0, + .func[0] = + PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_PIN | + PMBUS_HAVE_TEMP | PMBUS_HAVE_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = mp5990_read_byte_data, + .read_word_data = mp5990_read_word_data, +}; + +static struct pmbus_driver_info mp5998_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 64, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + .m[PSC_VOLTAGE_OUT] = 64, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .m[PSC_CURRENT_IN] = 16, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = 0, + .m[PSC_CURRENT_OUT] = 16, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 0, + .m[PSC_POWER] = 2, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = 0, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 0, + .func[0] = + PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_IOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, + .read_byte_data = mp5990_read_byte_data, + .read_word_data = mp5990_read_word_data, +}; + +static const struct i2c_device_id mp5990_id[] = { + { .name = "mp5990", .driver_data = mp5990 }, + { .name = "mp5998", .driver_data = mp5998 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp5990_id); + +static int mp5990_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + struct mp5990_data *data; + enum chips chip; + int ret; + + data = devm_kzalloc(&client->dev, sizeof(struct mp5990_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + if (client->dev.of_node) + chip = (uintptr_t)of_device_get_match_data(&client->dev); + else + chip = i2c_match_id(mp5990_id, client)->driver_data; + + if (chip == mp5990) + memcpy(&data->info, &mp5990_info, sizeof(*info)); + else + memcpy(&data->info, &mp5998_info, sizeof(*info)); + info = &data->info; + + /* Read Vout Config */ + ret = i2c_smbus_read_word_data(client, MP5990_EFUSE_CFG); + if (ret < 0) { + dev_err(&client->dev, "Can't get vout mode."); + return ret; + } + + /* + * EFUSE_CFG (0xC4) bit9=1 is linear mode, bit=0 is direct mode. + */ + if (ret & MP5990_VOUT_FORMAT) { + data->vout_mode = linear; + data->info.format[PSC_VOLTAGE_IN] = linear; + data->info.format[PSC_VOLTAGE_OUT] = linear; + data->info.format[PSC_CURRENT_OUT] = linear; + data->info.format[PSC_POWER] = linear; + if (chip == mp5998) + data->info.format[PSC_CURRENT_IN] = linear; + + ret = i2c_smbus_read_word_data(client, PMBUS_READ_VOUT); + if (ret < 0) { + dev_err(&client->dev, "Can't get vout exponent."); + return ret; + } + data->vout_linear_exponent = (u8)((ret >> 11) & 0x1f); + } else { + data->vout_mode = direct; + } + return pmbus_do_probe(client, info); +} + +static const struct of_device_id mp5990_of_match[] = { + { .compatible = "mps,mp5990", .data = (void *)mp5990 }, + { .compatible = "mps,mp5998", .data = (void *)mp5998 }, + {} +}; + +static struct i2c_driver mp5990_driver = { + .driver = { + .name = "mp5990", + .of_match_table = mp5990_of_match, + }, + .probe = mp5990_probe, + .id_table = mp5990_id, +}; +module_i2c_driver(mp5990_driver); + +MODULE_AUTHOR("Peter Yin <peter.yin@quantatw.com>"); +MODULE_DESCRIPTION("PMBus driver for MP5990 HSC"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp9941.c b/drivers/hwmon/pmbus/mp9941.c new file mode 100644 index 000000000..d9049d326 --- /dev/null +++ b/drivers/hwmon/pmbus/mp9941.c @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Multi-phase Digital VR Controllers(MP9941) + */ + +#include <linux/bitfield.h> +#include <linux/bits.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +/* + * Vender specific registers. The MFR_ICC_MAX(0x02) is used to + * config the iin scale. The MFR_RESO_SET(0xC7) is used to + * config the vout format. The MFR_VR_MULTI_CONFIG_R1(0x0D) is + * used to identify the vout vid step. + */ +#define MFR_ICC_MAX 0x02 +#define MFR_RESO_SET 0xC7 +#define MFR_VR_MULTI_CONFIG_R1 0x0D + +#define MP9941_VIN_LIMIT_UINT 1 +#define MP9941_VIN_LIMIT_DIV 8 +#define MP9941_READ_VIN_UINT 1 +#define MP9941_READ_VIN_DIV 32 + +#define MP9941_PAGE_NUM 1 + +#define MP9941_RAIL1_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_TEMP | PMBUS_HAVE_PIN | \ + PMBUS_HAVE_IIN | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +struct mp9941_data { + struct pmbus_driver_info info; + int vid_resolution; +}; + +#define to_mp9941_data(x) container_of(x, struct mp9941_data, info) + +static int mp9941_set_vout_format(struct i2c_client *client) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_RESO_SET); + if (ret < 0) + return ret; + + /* + * page = 0, MFR_RESO_SET[7:6] defines the vout format + * 2'b11 set the vout format as direct + */ + ret = (ret & ~GENMASK(7, 6)) | FIELD_PREP(GENMASK(7, 6), 3); + + return i2c_smbus_write_word_data(client, MFR_RESO_SET, ret); +} + +static int +mp9941_identify_vid_resolution(struct i2c_client *client, struct pmbus_driver_info *info) +{ + struct mp9941_data *data = to_mp9941_data(info); + int ret; + + /* + * page = 2, MFR_VR_MULTI_CONFIG_R1[4:4] defines rail1 vid step value + * 1'b0 represents the vid step value is 10mV + * 1'b1 represents the vid step value is 5mV + */ + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 2); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_VR_MULTI_CONFIG_R1); + if (ret < 0) + return ret; + + if (FIELD_GET(GENMASK(4, 4), ret)) + data->vid_resolution = 5; + else + data->vid_resolution = 10; + + return 0; +} + +static int mp9941_identify_iin_scale(struct i2c_client *client) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_RESO_SET); + if (ret < 0) + return ret; + + ret = (ret & ~GENMASK(3, 2)) | FIELD_PREP(GENMASK(3, 2), 0); + + ret = i2c_smbus_write_word_data(client, MFR_RESO_SET, ret); + if (ret < 0) + return ret; + + /* + * page = 2, MFR_ICC_MAX[15:13] defines the iin scale + * 3'b000 set the iout scale as 0.5A/Lsb + */ + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 2); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_ICC_MAX); + if (ret < 0) + return ret; + + ret = (ret & ~GENMASK(15, 13)) | FIELD_PREP(GENMASK(15, 13), 0); + + return i2c_smbus_write_word_data(client, MFR_ICC_MAX, ret); +} + +static int mp9941_identify(struct i2c_client *client, struct pmbus_driver_info *info) +{ + int ret; + + ret = mp9941_identify_iin_scale(client); + if (ret < 0) + return ret; + + ret = mp9941_identify_vid_resolution(client, info); + if (ret < 0) + return ret; + + return mp9941_set_vout_format(client); +} + +static int mp9941_read_word_data(struct i2c_client *client, int page, int phase, + int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp9941_data *data = to_mp9941_data(info); + int ret; + + switch (reg) { + case PMBUS_READ_VIN: + /* The MP9941 vin scale is (1/32V)/Lsb */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(9, 0)) * MP9941_READ_VIN_UINT, + MP9941_READ_VIN_DIV); + break; + case PMBUS_READ_IIN: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = ret & GENMASK(10, 0); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + /* The MP9941 vin ov limit scale is (1/8V)/Lsb */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(7, 0)) * MP9941_VIN_LIMIT_UINT, + MP9941_VIN_LIMIT_DIV); + break; + case PMBUS_IIN_OC_WARN_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = ret & GENMASK(7, 0); + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + case PMBUS_MFR_VOUT_MIN: + case PMBUS_MFR_VOUT_MAX: + /* + * The vout scale is set to 1mV/Lsb(using r/m/b scale). + * But the vout uv limit and vout max/min scale is 1VID/Lsb, + * so the vout uv limit and vout max/min value should be + * multiplied by vid resolution. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = ret * data->vid_resolution; + break; + case PMBUS_READ_IOUT: + case PMBUS_READ_POUT: + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_READ_VOUT: + case PMBUS_READ_PIN: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + ret = -ENODATA; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static int mp9941_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp9941_data *data = to_mp9941_data(info); + int ret; + + switch (reg) { + case PMBUS_VIN_OV_FAULT_LIMIT: + /* The MP9941 vin ov limit scale is (1/8V)/Lsb */ + ret = pmbus_write_word_data(client, page, reg, + DIV_ROUND_CLOSEST(word * MP9941_VIN_LIMIT_DIV, + MP9941_VIN_LIMIT_UINT)); + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + case PMBUS_MFR_VOUT_MIN: + case PMBUS_MFR_VOUT_MAX: + ret = pmbus_write_word_data(client, page, reg, + DIV_ROUND_CLOSEST(word, data->vid_resolution)); + break; + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + ret = -ENODATA; + break; + default: + ret = -EINVAL; + break; + } + + return ret; +} + +static const struct pmbus_driver_info mp9941_info = { + .pages = MP9941_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + + .m[PSC_TEMPERATURE] = 1, + .R[PSC_TEMPERATURE] = 0, + .b[PSC_TEMPERATURE] = 0, + + .m[PSC_VOLTAGE_IN] = 1, + .R[PSC_VOLTAGE_IN] = 0, + .b[PSC_VOLTAGE_IN] = 0, + + .m[PSC_CURRENT_IN] = 2, + .R[PSC_CURRENT_IN] = 0, + .b[PSC_CURRENT_IN] = 0, + + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .b[PSC_VOLTAGE_OUT] = 0, + + .func[0] = MP9941_RAIL1_FUNC, + .read_word_data = mp9941_read_word_data, + .write_word_data = mp9941_write_word_data, + .identify = mp9941_identify, +}; + +static int mp9941_probe(struct i2c_client *client) +{ + struct mp9941_data *data; + + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mp9941_info, sizeof(mp9941_info)); + + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id mp9941_id[] = { + { .name = "mp9941" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp9941_id); + +static const struct of_device_id __maybe_unused mp9941_of_match[] = { + {.compatible = "mps,mp9941"}, + {} +}; +MODULE_DEVICE_TABLE(of, mp9941_of_match); + +static struct i2c_driver mp9941_driver = { + .driver = { + .name = "mp9941", + .of_match_table = mp9941_of_match, + }, + .probe = mp9941_probe, + .id_table = mp9941_id, +}; + +module_i2c_driver(mp9941_driver); + +MODULE_AUTHOR("Noah Wang <noahwang.wang@outlook.com>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP9941"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mp9945.c b/drivers/hwmon/pmbus/mp9945.c new file mode 100644 index 000000000..199480d85 --- /dev/null +++ b/drivers/hwmon/pmbus/mp9945.c @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Single-phase Digital VR Controllers(MP9945) + */ + +#include <linux/bitfield.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +#define MFR_VR_MULTI_CONFIG_R1 0x08 +#define MFR_SVID_CFG_R1 0xBD + +/* VOUT_MODE register values */ +#define VOUT_MODE_LINEAR16 0x17 +#define VOUT_MODE_VID 0x21 +#define VOUT_MODE_DIRECT 0x40 + +#define MP9945_PAGE_NUM 1 + +#define MP9945_RAIL1_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | \ + PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | \ + PMBUS_HAVE_TEMP | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +enum mp9945_vout_mode { + MP9945_VOUT_MODE_VID, + MP9945_VOUT_MODE_DIRECT, + MP9945_VOUT_MODE_LINEAR16, +}; + +struct mp9945_data { + struct pmbus_driver_info info; + enum mp9945_vout_mode vout_mode; + int vid_resolution; + int vid_offset; +}; + +#define to_mp9945_data(x) container_of(x, struct mp9945_data, info) + +static int mp9945_read_vout(struct i2c_client *client, struct mp9945_data *data) +{ + int ret; + + ret = i2c_smbus_read_word_data(client, PMBUS_READ_VOUT); + if (ret < 0) + return ret; + + ret &= GENMASK(11, 0); + + switch (data->vout_mode) { + case MP9945_VOUT_MODE_VID: + if (ret > 0) + ret = (ret + data->vid_offset) * data->vid_resolution; + break; + case MP9945_VOUT_MODE_DIRECT: + break; + case MP9945_VOUT_MODE_LINEAR16: + /* LSB: 1000 * 2^-9 (mV) */ + ret = DIV_ROUND_CLOSEST(ret * 125, 64); + break; + default: + return -ENODEV; + } + + return ret; +} + +static int mp9945_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + switch (reg) { + case PMBUS_VOUT_MODE: + /* + * Override VOUT_MODE to DIRECT as the driver handles custom + * VOUT format conversions internally. + */ + return PB_VOUT_MODE_DIRECT; + default: + return -ENODATA; + } +} + +static int mp9945_read_word_data(struct i2c_client *client, int page, int phase, + int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mp9945_data *data = to_mp9945_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + switch (reg) { + case PMBUS_READ_VOUT: + ret = mp9945_read_vout(client, data); + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = i2c_smbus_read_word_data(client, reg); + if (ret < 0) + return ret; + + /* LSB: 1.95 (mV) */ + ret = DIV_ROUND_CLOSEST((ret & GENMASK(11, 0)) * 39, 20); + break; + case PMBUS_VOUT_UV_WARN_LIMIT: + ret = i2c_smbus_read_word_data(client, reg); + if (ret < 0) + return ret; + + ret &= GENMASK(9, 0); + if (ret > 0) + ret = (ret + data->vid_offset) * data->vid_resolution; + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int mp9945_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + struct mp9945_data *data = to_mp9945_data(info); + int ret; + + ret = i2c_smbus_read_byte_data(client, PMBUS_VOUT_MODE); + if (ret < 0) + return ret; + + switch (ret) { + case VOUT_MODE_LINEAR16: + data->vout_mode = MP9945_VOUT_MODE_LINEAR16; + break; + case VOUT_MODE_VID: + data->vout_mode = MP9945_VOUT_MODE_VID; + break; + case VOUT_MODE_DIRECT: + data->vout_mode = MP9945_VOUT_MODE_DIRECT; + break; + default: + return -ENODEV; + } + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 3); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_word_data(client, MFR_VR_MULTI_CONFIG_R1); + if (ret < 0) + return ret; + + data->vid_resolution = (FIELD_GET(BIT(2), ret)) ? 5 : 10; + + ret = i2c_smbus_read_word_data(client, MFR_SVID_CFG_R1); + if (ret < 0) + return ret; + + data->vid_offset = (FIELD_GET(BIT(15), ret)) ? 19 : 49; + + return i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); +} + +static struct pmbus_driver_info mp9945_info = { + .pages = MP9945_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .b[PSC_VOLTAGE_OUT] = 0, + .func[0] = MP9945_RAIL1_FUNC, + .read_word_data = mp9945_read_word_data, + .read_byte_data = mp9945_read_byte_data, + .identify = mp9945_identify, +}; + +static int mp9945_probe(struct i2c_client *client) +{ + struct mp9945_data *data; + int ret; + + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mp9945_info, sizeof(mp9945_info)); + + /* + * Set page 0 before probe. The core reads paged registers which are + * only on page 0 for this device. + */ + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id mp9945_id[] = { + { .name = "mp9945" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mp9945_id); + +static const struct of_device_id __maybe_unused mp9945_of_match[] = { + {.compatible = "mps,mp9945"}, + {} +}; +MODULE_DEVICE_TABLE(of, mp9945_of_match); + +static struct i2c_driver mp9945_driver = { + .driver = { + .name = "mp9945", + .of_match_table = of_match_ptr(mp9945_of_match), + }, + .probe = mp9945_probe, + .id_table = mp9945_id, +}; + +module_i2c_driver(mp9945_driver); + +MODULE_AUTHOR("Cosmo Chou <chou.cosmo@gmail.com>"); +MODULE_DESCRIPTION("PMBus driver for MPS MP9945"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mpq7932.c b/drivers/hwmon/pmbus/mpq7932.c new file mode 100644 index 000000000..f49610adf --- /dev/null +++ b/drivers/hwmon/pmbus/mpq7932.c @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * mpq7932.c - hwmon with optional regulator driver for mps mpq7932 + * Copyright 2022 Monolithic Power Systems, Inc + * + * Author: Saravanan Sekar <saravanan@linumiz.com> + */ + +#include <linux/bits.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +#define MPQ7932_BUCK_UV_MIN 206250 +#define MPQ7932_UV_STEP 6250 +#define MPQ7932_N_VOLTAGES 256 +#define MPQ7932_VOUT_MAX 0xFF +#define MPQ7932_NUM_PAGES 6 +#define MPQ2286_NUM_PAGES 1 + +#define MPQ7932_TON_DELAY 0x60 +#define MPQ7932_VOUT_STARTUP_SLEW 0xA3 +#define MPQ7932_VOUT_SHUTDOWN_SLEW 0xA5 +#define MPQ7932_VOUT_SLEW_MASK GENMASK(1, 0) +#define MPQ7932_TON_DELAY_MASK GENMASK(4, 0) + +struct mpq7932_data { + struct pmbus_driver_info info; + struct pmbus_platform_data pdata; +}; + +#if IS_ENABLED(CONFIG_SENSORS_MPQ7932_REGULATOR) +static const struct regulator_desc mpq7932_regulators_desc[] = { + PMBUS_REGULATOR_STEP("buck", 0, MPQ7932_N_VOLTAGES, + MPQ7932_UV_STEP, MPQ7932_BUCK_UV_MIN), + PMBUS_REGULATOR_STEP("buck", 1, MPQ7932_N_VOLTAGES, + MPQ7932_UV_STEP, MPQ7932_BUCK_UV_MIN), + PMBUS_REGULATOR_STEP("buck", 2, MPQ7932_N_VOLTAGES, + MPQ7932_UV_STEP, MPQ7932_BUCK_UV_MIN), + PMBUS_REGULATOR_STEP("buck", 3, MPQ7932_N_VOLTAGES, + MPQ7932_UV_STEP, MPQ7932_BUCK_UV_MIN), + PMBUS_REGULATOR_STEP("buck", 4, MPQ7932_N_VOLTAGES, + MPQ7932_UV_STEP, MPQ7932_BUCK_UV_MIN), + PMBUS_REGULATOR_STEP("buck", 5, MPQ7932_N_VOLTAGES, + MPQ7932_UV_STEP, MPQ7932_BUCK_UV_MIN), +}; + +static const struct regulator_desc mpq7932_regulators_desc_one[] = { + PMBUS_REGULATOR_STEP_ONE_NODE("buck", MPQ7932_N_VOLTAGES, + MPQ7932_UV_STEP, MPQ7932_BUCK_UV_MIN), +}; +#endif + +static int mpq7932_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + switch (reg) { + /* + * chip supports only byte access for VOUT_COMMAND otherwise + * access results -EREMOTEIO + */ + case PMBUS_VOUT_COMMAND: + return pmbus_write_byte_data(client, page, reg, word & 0xFF); + + default: + return -ENODATA; + } +} + +static int mpq7932_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + switch (reg) { + /* + * chip supports neither (PMBUS_VOUT_MARGIN_HIGH, PMBUS_VOUT_MARGIN_LOW) + * nor (PMBUS_MFR_VOUT_MIN, PMBUS_MFR_VOUT_MAX). As a result set voltage + * fails due to error in pmbus_regulator_get_low_margin, so faked. + */ + case PMBUS_MFR_VOUT_MIN: + return 0; + + case PMBUS_MFR_VOUT_MAX: + return MPQ7932_VOUT_MAX; + + /* + * chip supports only byte access for VOUT_COMMAND otherwise + * access results in -EREMOTEIO + */ + case PMBUS_READ_VOUT: + return pmbus_read_byte_data(client, page, PMBUS_VOUT_COMMAND); + + default: + return -ENODATA; + } +} + +static int mpq7932_probe(struct i2c_client *client) +{ + struct mpq7932_data *data; + struct pmbus_driver_info *info; + struct device *dev = &client->dev; + int i; + + data = devm_kzalloc(dev, sizeof(struct mpq7932_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + info = &data->info; + info->pages = (int)(unsigned long)device_get_match_data(&client->dev); + info->format[PSC_VOLTAGE_OUT] = direct; + info->m[PSC_VOLTAGE_OUT] = 160; + info->b[PSC_VOLTAGE_OUT] = -33; + for (i = 0; i < info->pages; i++) { + info->func[i] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_STATUS_TEMP; + } + +#if IS_ENABLED(CONFIG_SENSORS_MPQ7932_REGULATOR) + info->num_regulators = info->pages; + if (info->num_regulators == 1) + info->reg_desc = mpq7932_regulators_desc_one; + else + info->reg_desc = mpq7932_regulators_desc; +#endif + + info->read_word_data = mpq7932_read_word_data; + info->write_word_data = mpq7932_write_word_data; + + data->pdata.flags = PMBUS_NO_CAPABILITY; + dev->platform_data = &data->pdata; + + return pmbus_do_probe(client, info); +} + +static const struct of_device_id mpq7932_of_match[] = { + { .compatible = "mps,mpq2286", .data = (void *)MPQ2286_NUM_PAGES }, + { .compatible = "mps,mpq7932", .data = (void *)MPQ7932_NUM_PAGES }, + {}, +}; +MODULE_DEVICE_TABLE(of, mpq7932_of_match); + +static const struct i2c_device_id mpq7932_id[] = { + { .name = "mpq2286" }, + { .name = "mpq7932" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mpq7932_id); + +static struct i2c_driver mpq7932_regulator_driver = { + .driver = { + .name = "mpq7932", + .of_match_table = mpq7932_of_match, + }, + .probe = mpq7932_probe, + .id_table = mpq7932_id, +}; +module_i2c_driver(mpq7932_regulator_driver); + +MODULE_AUTHOR("Saravanan Sekar <saravanan@linumiz.com>"); +MODULE_DESCRIPTION("MPQ7932 PMIC regulator driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mpq82d00.c b/drivers/hwmon/pmbus/mpq82d00.c new file mode 100644 index 000000000..d51fcffcc --- /dev/null +++ b/drivers/hwmon/pmbus/mpq82d00.c @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for MPS Synchronous Step-Down Converter(MPQ82D00) + */ + +#include <linux/bitfield.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define MPQ82D00_VOUT_DIV 64 + +#define MPQ82D00_PAGE_NUM 1 + +#define MPQ82D00_RAIL1_FUNC (PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | \ + PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP | \ + PMBUS_HAVE_POUT | PMBUS_HAVE_PIN | \ + PMBUS_HAVE_STATUS_VOUT | \ + PMBUS_HAVE_STATUS_IOUT | \ + PMBUS_HAVE_STATUS_TEMP | \ + PMBUS_HAVE_STATUS_INPUT) + +struct mpq82d00_data { + struct pmbus_driver_info info; + int vout_scale; +}; + +#define to_mpq82d00_data(x) container_of(x, struct mpq82d00_data, info) + +static u16 mpq82d00_linear_exp_transfer(u16 word, u16 expect_exponent) +{ + s16 exponent, mantissa, target_exponent; + + exponent = ((s16)word) >> 11; + mantissa = ((s16)((word & 0x7ff) << 5)) >> 5; + target_exponent = (s16)((expect_exponent & 0x1f) << 11) >> 11; + + /* + * The MPQ82D00 does not support negtive limit value, if a negtive + * limit value is written, the limit value will become to 0. And + * the maximum positive limit value is limitted to 0x3FF. + */ + if (mantissa < 0) { + mantissa = 0; + } else { + if (exponent > target_exponent) { + mantissa = (1023 >> (exponent - target_exponent)) >= mantissa ? + mantissa << (exponent - target_exponent) : + 0x3FF; + } else { + mantissa = clamp_val(mantissa >> (target_exponent - exponent), + 0, 0x3FF); + } + } + + return mantissa | ((expect_exponent << 11) & 0xf800); +} + +static int mpq82d00_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VOUT_MODE: + /* + * The MPQ82D00 does not follow standard PMBus protocol completely, + * and the calculation of vout in this driver is based on direct + * format. As a result, the format of vout is enforced to direct. + */ + ret = PB_VOUT_MODE_DIRECT; + break; + default: + /* + * These registers are not explicitly handled by the driver, + * as a result, return -ENODATA directly. + */ + ret = -ENODATA; + break; + } + + return ret; +} + +static int mpq82d00_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct mpq82d00_data *data = to_mpq82d00_data(info); + int ret; + + switch (reg) { + case PMBUS_STATUS_WORD: + case PMBUS_READ_VIN: + case PMBUS_READ_IOUT: + case PMBUS_READ_POUT: + case PMBUS_READ_PIN: + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + /* + * These registers are not explicitly handled by the driver, + * as a result, return -ENODATA directly. + */ + ret = -ENODATA; + break; + case PMBUS_READ_VOUT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + ret = DIV_ROUND_CLOSEST((ret & GENMASK(11, 0)) * data->vout_scale, + MPQ82D00_VOUT_DIV); + break; + default: + /* + * The MPQ82D00 do not support other telemetry and limit + * value reading, so, return -EINVAL directly. + */ + ret = -EINVAL; + break; + } + + return ret; +} + +static int mpq82d00_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + int ret; + + switch (reg) { + case PMBUS_OT_FAULT_LIMIT: + /* + * The PMBUS_OT_FAULT_LIMIT of MPQ82D00 is linear11 format, + * and the exponent is a constant value(5'b00000), so the + * exponent of word parameter should be converted to 5'b00000. + */ + ret = pmbus_write_word_data(client, page, reg, + mpq82d00_linear_exp_transfer(word, 0x00)); + break; + case PMBUS_IOUT_OC_FAULT_LIMIT: + /* + * The PMBUS_IOUT_OC_FAULT_LIMIT of MPQ82D00 is linear11 format, + * and the exponent is a constant value(5'b11100), so the + * exponent of word parameter should be converted to 5'b11100. + */ + ret = pmbus_write_word_data(client, page, reg, + mpq82d00_linear_exp_transfer(word, 0x1C)); + break; + default: + /* + * The MPQ82D00 do not support other limit value configuration, + * so, return -EINVAL directly. + */ + ret = -EINVAL; + break; + } + + return ret; +} + +static int mpq82d00_identify(struct i2c_client *client, struct pmbus_driver_info *info) +{ + struct mpq82d00_data *data = to_mpq82d00_data(info); + int ret; + + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return ret; + + ret = i2c_smbus_read_byte_data(client, PMBUS_VOUT_MODE); + if (ret < 0) + return ret; + + /* + * The MPQ82D00 supports three vout mode. If PMBUS_VOUT_MODE + * bit5 is 1, the vout scale is 5mv/LSB.If PMBUS_VOUT_MODE bit5 + * is 0, it is linear mode, the vout scale is 1.953125mv/LSB. + */ + if (FIELD_GET(GENMASK(5, 5), ret)) + data->vout_scale = 320; + else + data->vout_scale = 125; + + return 0; +} + +static struct pmbus_driver_info mpq82d00_info = { + .pages = MPQ82D00_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_POWER] = linear, + + .m[PSC_VOLTAGE_OUT] = 1, + .R[PSC_VOLTAGE_OUT] = 3, + .b[PSC_VOLTAGE_OUT] = 0, + + .func[0] = MPQ82D00_RAIL1_FUNC, + .read_word_data = mpq82d00_read_word_data, + .read_byte_data = mpq82d00_read_byte_data, + .write_word_data = mpq82d00_write_word_data, + .identify = mpq82d00_identify, +}; + +static int mpq82d00_probe(struct i2c_client *client) +{ + struct mpq82d00_data *data; + + data = devm_kzalloc(&client->dev, sizeof(struct mpq82d00_data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + memcpy(&data->info, &mpq82d00_info, sizeof(mpq82d00_info)); + + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id mpq82d00_id[] = { + {.name = "mpq82d00"}, + {} +}; +MODULE_DEVICE_TABLE(i2c, mpq82d00_id); + +static const struct of_device_id __maybe_unused mpq82d00_of_match[] = { + {.compatible = "mps,mpq82d00"}, + {} +}; +MODULE_DEVICE_TABLE(of, mpq82d00_of_match); + +static struct i2c_driver mpq82d00_driver = { + .driver = { + .name = "mpq82d00", + .of_match_table = mpq82d00_of_match, + }, + .probe = mpq82d00_probe, + .id_table = mpq82d00_id, +}; + +module_i2c_driver(mpq82d00_driver); + +MODULE_AUTHOR("Wensheng Wang <wenswang@yeah.net"); +MODULE_DESCRIPTION("PMBus driver for MPS MPQ82D00 device"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mpq8646.c b/drivers/hwmon/pmbus/mpq8646.c new file mode 100644 index 000000000..6ac82beb0 --- /dev/null +++ b/drivers/hwmon/pmbus/mpq8646.c @@ -0,0 +1,976 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for MPS MPQ8646 step-down converter. + * + * Copyright (c) 2026 Free Mobile - Vincent Jardin <vjardin@free.fr> + */ + +#include <linux/bitops.h> +#include <linux/debugfs.h> +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/mutex.h> +#include <linux/nvmem-provider.h> +#include <linux/of_device.h> +#include <linux/pmbus.h> +#include <linux/property.h> +#include <linux/regulator/driver.h> +#include <linux/seq_file.h> +#include <linux/workqueue.h> +#include "pmbus.h" + +/* Default cadence for the in-driver alarm-poll fallback (ms) */ +#define MPQ8646_ALARM_POLL_MS_DEFAULT 1000 + +/* MPS vendor-extended command codes (NOT in PMBus 1.3 Part II) */ +#define MPS_CLEAR_LAST_FAULT 0x08 +#define MPS_MFR_CFG_EXT 0xF5 +#define MPS_MFR_CFG_EXT_CLR_LAST_EN BIT(6) +#define MPS_PROTECTION_LAST 0xFB + +/* + * PMBus 1.3 NVM commit / revert commands. MPS equivalent of + * STORE_ALL (15h) and RESTORE_ALL (16h). + */ +#define PMBUS_STORE_USER_ALL 0x15 +#define PMBUS_RESTORE_USER_ALL 0x16 + +/* PMBus 1.3 timing / UVLO command codes */ +#define PMBUS_VIN_ON 0x35 +#define PMBUS_VIN_OFF 0x36 +#define PMBUS_TON_DELAY 0x60 +#define PMBUS_TON_RISE 0x61 +#define PMBUS_TOFF_DELAY 0x64 +#define PMBUS_TOFF_FALL 0x65 + +/* MPS vendor-extended observability / identity registers */ +#define MPS_MFR_CONFIG_ID 0xC0 +#define MPS_MFR_CONFIG_CODE_REV 0xC1 +#define MPS_MFR_PRODUCT_REV_USER 0xC2 +#define MPS_MFR_SILICON_REV 0xC3 +#define MPS_MFR_RETRY_TIMES 0xF4 +#define MPS_MFR_VBOOT_CFG 0xFC + +/* + * MPS_MFR_PMBUS_LOCK (EEh): 16-bit WORD whose low two bits gate + * subsequent PMBus writes + * bits[1:0] = 00 -- unlocked (POR default) + * 01 -- lock all writes EXCEPT VOUT_COMMAND (0x21) + * so the operator can still DVFS the rail + * 11 -- lock all writes + * A negative-going PG edge resets these bits to 00, the lock + * is operationally reversible without a full chip POR. + */ +#define MPS_MFR_PMBUS_LOCK 0xEE + +/* + * Retry parameters for the MFR_CFG_EXT gate-close write after + * CLEAR_LAST_FAULT. Bench-observed NVM-busy NACK window on this + * silicon is about 1 ms; the datasheet does not have information. + */ +#define MPQ8646_NVM_RETRY_MAX 5 +#define MPQ8646_NVM_RETRY_DELAY_US_MIN 2000 +#define MPQ8646_NVM_RETRY_DELAY_US_MAX 4000 + +#define MPQ8646_DEBUG(client, fmt, ...) \ + dev_dbg(&(client)->dev, fmt, ##__VA_ARGS__) + +/* + * Maximum legal value for VOUT_SCALE_LOOP (0x29) on the MPQ8646 silicon + * The chip uses an 11-bit VOUT feedback-divider scale register. + */ +#define MPQ8646_VOUT_SCALE_LOOP_MAX GENMASK(10, 0) + +/* Forward declaration */ +struct mpq8646_dbg_reg_ctx; + +/* Per-instance state */ +struct mpq8646_priv { + struct pmbus_driver_info info; /* must be first, container_of target */ + struct i2c_client *client; + + /* Serialises CLEAR_LAST_FAULT sequences and PROTECTION_LAST reads */ + struct mutex mps_lock; + + /* Set alarm_poll_interval_ms = 0 to disable. */ + struct delayed_work alarm_poll_work; + u32 alarm_poll_interval_ms; + +#ifdef CONFIG_DEBUG_FS + /* the only debugfs file that can re-arm the poll worker */ + struct dentry *dbg_poll; + struct mpq8646_dbg_reg_ctx *dbg_reg_ctx; +#endif +}; + +static inline struct mpq8646_priv *mpq8646_priv_from_client(struct i2c_client *client) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + + return container_of(info, struct mpq8646_priv, info); +} + +/* + * VID-mode m/b/R coefficients, same values as the mpq8785 driver for + * the MPS VID encoding. Per the PMBus Direct formula used by + * pmbus_core, X = (Y * 10^-R - b) / m, so m=64 / b=0 / R=1 yields + * 1.5625 mV per LSB. + */ +#define MPQ8646_VID_M 64 +#define MPQ8646_VID_B 0 +#define MPQ8646_VID_R 1 + +static int mpq8646_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + int vout_mode; + + vout_mode = pmbus_read_byte_data(client, 0, PMBUS_VOUT_MODE); + if (vout_mode < 0) + return vout_mode; + + switch (vout_mode & PB_VOUT_MODE_MODE_MASK) { + case PB_VOUT_MODE_LINEAR: + info->format[PSC_VOLTAGE_OUT] = linear; + break; + case PB_VOUT_MODE_VID: + case PB_VOUT_MODE_DIRECT: + info->format[PSC_VOLTAGE_OUT] = direct; + info->m[PSC_VOLTAGE_OUT] = MPQ8646_VID_M; + info->b[PSC_VOLTAGE_OUT] = MPQ8646_VID_B; + info->R[PSC_VOLTAGE_OUT] = MPQ8646_VID_R; + break; + default: + return -ENODEV; + } + + return 0; +}; + +static int mpq8646_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + MPQ8646_DEBUG(client, "read_byte_data page=%d reg=0x%02x\n", page, reg); + + switch (reg) { + case PMBUS_VOUT_MODE: + ret = pmbus_read_byte_data(client, page, reg); + MPQ8646_DEBUG(client, " VOUT_MODE raw=0x%02x ret=%d\n", ret, ret); + if (ret < 0) + return ret; + + if ((ret & PB_VOUT_MODE_MODE_MASK) == PB_VOUT_MODE_VID) + return PB_VOUT_MODE_DIRECT; + + return ret; + case PMBUS_STATUS_BYTE: + case PMBUS_STATUS_CML: + case PMBUS_STATUS_OTHER: + case PMBUS_STATUS_MFR_SPECIFIC: + case PMBUS_STATUS_FAN_12: + case PMBUS_STATUS_FAN_34: + return -ENXIO; + case PMBUS_MFR_LOCATION: + case PMBUS_MFR_DATE: + case PMBUS_MFR_SERIAL: + case PMBUS_IC_DEVICE_ID: + case PMBUS_IC_DEVICE_REV: + MPQ8646_DEBUG(client, " unsupported mfr-info reg 0x%02x -> ENXIO\n", reg); + return -ENXIO; + default: + return -ENODATA; + } +} + +/* + * Reference: ltc2978.c::ltc2978_write_word_data which uses the + * same virtual-register channel for its real chip-side peak reset. + */ +static int mpq8646_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + struct mpq8646_priv *priv = mpq8646_priv_from_client(client); + int rc; + + switch (reg) { + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_VOUT_HISTORY: + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_RESET_TEMP_HISTORY: + MPQ8646_DEBUG(client, "reset_history virt reg=0x%04x -> CLEAR_FAULTS\n", + reg); + rc = i2c_smbus_write_byte(priv->client, PMBUS_CLEAR_FAULTS); + if (rc < 0) + MPQ8646_DEBUG(client, " CLEAR_FAULTS rc=%d\n", rc); + return rc < 0 ? rc : 0; + default: + return -ENODATA; /* let pmbus_core do the direct write */ + } +} + +static int mpq8646_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int rc; + + MPQ8646_DEBUG(client, "read_word_data page=%d phase=%d reg=0x%02x\n", + page, phase, reg); + + switch (reg) { + case PMBUS_READ_VIN: + case PMBUS_READ_VOUT: + case PMBUS_READ_IOUT: + case PMBUS_READ_TEMPERATURE_1: + case PMBUS_STATUS_WORD: + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_OV_WARN_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_MFR_VIN_MAX: + case PMBUS_MFR_VOUT_MAX: + case PMBUS_MFR_IOUT_MAX: + case PMBUS_MFR_MAX_TEMP_1: + break; + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_VOUT_HISTORY: + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_RESET_TEMP_HISTORY: + return 0; + default: + return -ENODATA; + } + + rc = i2c_smbus_read_word_data(client, reg); + + MPQ8646_DEBUG(client, " reg=0x%02x rc=%d\n", reg, rc); + return rc; +} + +static struct pmbus_driver_info mpq8646_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_TEMPERATURE] = 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, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_TEMP | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_TEMP, +}; + +#if IS_ENABLED(CONFIG_REGULATOR) +static const struct regulator_desc mpq8646_reg_desc[] = { + PMBUS_REGULATOR("vout", 0), +}; +#endif /* CONFIG_REGULATOR */ + +#if IS_ENABLED(CONFIG_NVMEM) +/* + * Expose using + * /sys/bus/nvmem/devices/<i2c-name>/nvmem + * + * Layout (16 bytes): + * offset 0..1 PROTECTION_LAST (0xFB) word, LE -- NVM + * offset 2..3 MFR_RETRY_TIMES (0xF4) word, LE -- NVM + * offset 4..5 MFR_CONFIG_ID (0xC0) word, LE -- NVM + * offset 6..7 MFR_VBOOT_CFG (0xFC) word, LE -- NVM + * offset 8 MFR_SILICON_REV (0xC3) byte -- NVM + * offset 9..15 reserved (zero-fill, leaves room for additions) + */ +#define MPQ8646_NVMEM_SIZE 16 + +struct mpq8646_nvmem_entry { + unsigned int off; + u8 reg; + bool is_word; +}; + +static const struct mpq8646_nvmem_entry mpq8646_nvmem_map[] = { + { 0, MPS_PROTECTION_LAST, true }, + { 2, MPS_MFR_RETRY_TIMES, true }, + { 4, MPS_MFR_CONFIG_ID, true }, + { 6, MPS_MFR_VBOOT_CFG, true }, + { 8, MPS_MFR_SILICON_REV, false }, +}; + +static int mpq8646_nvmem_read(void *data, unsigned int offset, void *val, + size_t bytes) +{ + struct mpq8646_priv *priv = data; + u8 *out = val; + size_t i; + + if (offset >= MPQ8646_NVMEM_SIZE) + return -EINVAL; + if (offset + bytes > MPQ8646_NVMEM_SIZE) + bytes = MPQ8646_NVMEM_SIZE - offset; + + memset(out, 0, bytes); + + guard(mutex)(&priv->mps_lock); + for (i = 0; i < ARRAY_SIZE(mpq8646_nvmem_map); i++) { + const struct mpq8646_nvmem_entry *e = &mpq8646_nvmem_map[i]; + unsigned int e_start = e->off; + unsigned int e_end = e_start + (e->is_word ? 2 : 1); + u8 raw[2]; + int rc; + unsigned int j; + + if (e_end <= offset || e_start >= offset + bytes) + continue; /* outside requested slice */ + + if (e->is_word) + rc = i2c_smbus_read_word_data(priv->client, e->reg); + else + rc = i2c_smbus_read_byte_data(priv->client, e->reg); + if (rc < 0) + continue; /* leave the zero-fill in place */ + + raw[0] = rc & 0xff; + raw[1] = (rc >> 8) & 0xff; + + for (j = 0; j < e_end - e_start; j++) { + unsigned int abs = e_start + j; + + if (abs >= offset && abs < offset + bytes) + out[abs - offset] = raw[j]; + } + } + return 0; +} +#endif /* CONFIG_NVMEM */ + +static const struct i2c_device_id mpq8646_id[] = { + { .name = "mpq8646" }, + { }, +}; +MODULE_DEVICE_TABLE(i2c, mpq8646_id); + +static const struct of_device_id __maybe_unused mpq8646_of_match[] = { + { .compatible = "mps,mpq8646" }, + {} +}; +MODULE_DEVICE_TABLE(of, mpq8646_of_match); + +static struct pmbus_platform_data mpq8646_no_pec_pdata = { + .flags = PMBUS_NO_CAPABILITY, +}; + +#ifdef CONFIG_DEBUG_FS +/* + * Read-only decode cases in the client's pmbus debugfs directory: + * the MPS-specific STATUS_WORD and PROTECTION_LAST bit decode plus the + * identity/timing registers. + */ + +struct mpq_status_bit { + u16 mask; + const char *name; +}; + +static const struct mpq_status_bit mpq8646_status_word_bits[] = { + { PB_STATUS_VOUT, "VOUT" }, + { PB_STATUS_IOUT_POUT, "IOUT_POUT" }, + { PB_STATUS_INPUT, "INPUT" }, + { PB_STATUS_WORD_MFR, "NVM_SUMMARY" }, + { PB_STATUS_POWER_GOOD_N, "POWER_GOOD#" }, + { PB_STATUS_FANS, "FANS" }, + { PB_STATUS_OTHER, "OTHER" }, + { PB_STATUS_UNKNOWN, "WATCH_DOG" }, + { PB_STATUS_BUSY, "BUSY" }, + { PB_STATUS_OFF, "OFF" }, + { PB_STATUS_VOUT_OV, "VOUT_OV_FAULT" }, + { PB_STATUS_IOUT_OC, "IOUT_OC_FAULT" }, + { PB_STATUS_VIN_UV, "VIN_UV_FAULT" }, + { PB_STATUS_TEMPERATURE, "TEMP" }, + { PB_STATUS_CML, "CML" }, + { PB_STATUS_NONE_ABOVE, "DRMOS_FAULT" }, + { /* sentinel */ } +}; + +/* PROTECTION_LAST (0xFB) bit names, it survives chip POR */ +static const struct mpq_status_bit mpq8646_protection_last_bits[] = { + { BIT(15), "INIT_FAULT" }, + { BIT(14), "NVM_CRC_ERROR" }, + { BIT(13), "NVM_FAULT" }, + { BIT(12), "OC_PHASE_FAULT" }, + { BIT(11), "OTP_SELF_FAULT" }, + { BIT(9), "SWITCH_PRD_FAULT" }, + { BIT(8), "VIN_OV_FAULT" }, + { BIT(7), "VOUT_OV_FAULT" }, + { BIT(6), "VOUT_UV_FAULT" }, + { BIT(5), "OC_TOT_FAULT" }, + { BIT(4), "VIN_UVLO_FAULT" }, + { BIT(3), "DRMOS_OTP" }, + { /* sentinel */ } +}; + +static void mpq8646_print_bits(struct seq_file *s, u16 v, + const struct mpq_status_bit *tab) +{ + const struct mpq_status_bit *t; + bool first = true; + + seq_printf(s, "0x%04x", v); + if (!v) { + seq_puts(s, " [clean]\n"); + return; + } + seq_puts(s, " ["); + for (t = tab; t->mask; t++) { + if (v & t->mask) { + if (!first) + seq_putc(s, ' '); + seq_puts(s, t->name); + first = false; + } + } + seq_puts(s, "]\n"); +} + +static int mpq8646_dbg_status_decoded_show(struct seq_file *s, void *unused) +{ + struct mpq8646_priv *priv = s->private; + int rc; + + scoped_guard(pmbus_lock, priv->client) + rc = i2c_smbus_read_word_data(priv->client, PMBUS_STATUS_WORD); + if (rc < 0) { + seq_printf(s, "ERROR: STATUS_WORD read failed (%d)\n", rc); + return 0; + } + seq_puts(s, "STATUS_WORD: "); + mpq8646_print_bits(s, (u16)rc, mpq8646_status_word_bits); + seq_puts(s, "(MPS extensions: bit12=NVM_SUMMARY, bit8=WATCH_DOG, bit0=DRMOS_FAULT)\n"); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(mpq8646_dbg_status_decoded); + +static int mpq8646_dbg_protection_last_show(struct seq_file *s, void *unused) +{ + struct mpq8646_priv *priv = s->private; + int rc; + + guard(pmbus_lock)(priv->client); + scoped_guard(mutex, &priv->mps_lock) + rc = i2c_smbus_read_word_data(priv->client, MPS_PROTECTION_LAST); + + if (rc < 0) { + seq_printf(s, "ERROR: PROTECTION_LAST read failed (%d)\n", rc); + return 0; + } + seq_puts(s, "PROTECTION_LAST: "); + mpq8646_print_bits(s, (u16)rc, mpq8646_protection_last_bits); + seq_puts(s, "(NVM-backed, survives chip POR)\n"); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(mpq8646_dbg_protection_last); + +struct mpq8646_dbg_reg { + u8 reg; + bool is_word; + const char *name; +}; + +static const struct mpq8646_dbg_reg mpq8646_dbg_regs[] = { + /* MPS vendor extensions (read-only identity / observability) */ + { MPS_MFR_CONFIG_ID, true, "mfr_config_id" }, + { MPS_MFR_CONFIG_CODE_REV, true, "mfr_config_code_rev" }, + { MPS_MFR_SILICON_REV, false, "mfr_silicon_rev" }, + { MPS_MFR_RETRY_TIMES, true, "mfr_retry_times" }, + { MPS_MFR_VBOOT_CFG, true, "mfr_vboot_cfg" }, + /* PMBus 1.3 standard timing / UVLO (read-only introspection) */ + { PMBUS_VIN_ON, true, "vin_on" }, + { PMBUS_VIN_OFF, true, "vin_off" }, + { PMBUS_TON_DELAY, true, "ton_delay" }, + { PMBUS_TON_RISE, true, "ton_rise" }, + { PMBUS_TOFF_DELAY, true, "toff_delay" }, + { PMBUS_TOFF_FALL, true, "toff_fall" }, +}; + +struct mpq8646_dbg_reg_ctx { + struct mpq8646_priv *priv; + const struct mpq8646_dbg_reg *desc; +}; + +static int mpq8646_dbg_reg_show(struct seq_file *s, void *unused) +{ + struct mpq8646_dbg_reg_ctx *ctx = s->private; + int rc; + + guard(pmbus_lock)(ctx->priv->client); + if (ctx->desc->is_word) + rc = i2c_smbus_read_word_data(ctx->priv->client, + ctx->desc->reg); + else + rc = i2c_smbus_read_byte_data(ctx->priv->client, + ctx->desc->reg); + + if (rc < 0) { + seq_printf(s, "ERROR: reg 0x%02x (%s) read failed (%d)\n", + ctx->desc->reg, ctx->desc->name, rc); + return 0; + } + seq_printf(s, "0x%0*x\n", ctx->desc->is_word ? 4 : 2, rc); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(mpq8646_dbg_reg); + +#ifdef CONFIG_SENSORS_MPQ8646_DEBUG_UNSAFE +/* + * Write/provisioning data, disabled by default: NVM commit and + * revert, the CLEAR_LAST_FAULT sequences and a small set of named + * writable registers. + */ + +static void mpq8646_unsafe_banner(struct mpq8646_priv *priv) +{ + dev_warn(&priv->client->dev, + "**********************************************************\n" + "** WARNING WARNING WARNING WARNING WARNING WARNING **\n" + "** **\n" + "** The MPQ8646 provisioning debugfs writes are enabled. **\n" + "** Wrong register writes can and likely will physically **\n" + "** damage or destroy the chip and/or the board. **\n" + "** **\n" + "** If you see this message and you are not debugging **\n" + "** the kernel, report this immediately to your system **\n" + "** administrator! **\n" + "** **\n" + "** WARNING WARNING WARNING WARNING WARNING WARNING **\n" + "**********************************************************\n"); +} + +static int mpq8646_dbg_clear_protection_last(void *data, u64 val) +{ + struct mpq8646_priv *priv = data; + int rc; + + if (!val) + return 0; + + guard(pmbus_lock)(priv->client); + scoped_guard(mutex, &priv->mps_lock) + rc = i2c_smbus_write_byte(priv->client, MPS_CLEAR_LAST_FAULT); + if (rc < 0) + dev_warn(&priv->client->dev, + "clear_protection_last: CLEAR_LAST_FAULT write failed (%d)\n", + rc); + return rc; +} +DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_clear_protection_last_fops, + NULL, mpq8646_dbg_clear_protection_last, "%llu\n"); + +static int mpq8646_dbg_clear_protection_last_force(void *data, u64 val) +{ + struct mpq8646_priv *priv = data; + int rc, ret; + int wp_orig, cfg_orig; + + if (!val) + return 0; + + guard(pmbus_lock)(priv->client); + guard(mutex)(&priv->mps_lock); + + wp_orig = i2c_smbus_read_byte_data(priv->client, PMBUS_WRITE_PROTECT); + if (wp_orig < 0) { + dev_warn(&priv->client->dev, + "clear_protection_last_force: WRITE_PROTECT read failed (%d), aborting\n", + wp_orig); + return wp_orig; + } + cfg_orig = i2c_smbus_read_word_data(priv->client, MPS_MFR_CFG_EXT); + if (cfg_orig < 0) { + dev_warn(&priv->client->dev, + "clear_protection_last_force: MFR_CFG_EXT read failed (%d), aborting\n", + cfg_orig); + return cfg_orig; + } + + if (wp_orig != 0) { + rc = i2c_smbus_write_byte_data(priv->client, + PMBUS_WRITE_PROTECT, 0); + if (rc < 0) { + dev_warn(&priv->client->dev, + "clear_protection_last_force: WP clear failed (%d), aborting\n", + rc); + return rc; + } + } + + ret = i2c_smbus_write_word_data(priv->client, MPS_MFR_CFG_EXT, + (u16)cfg_orig | MPS_MFR_CFG_EXT_CLR_LAST_EN); + if (ret < 0) { + dev_warn(&priv->client->dev, + "clear_protection_last_force: gate open failed (%d)\n", + ret); + } else { + ret = i2c_smbus_write_byte(priv->client, MPS_CLEAR_LAST_FAULT); + if (ret < 0) + dev_warn(&priv->client->dev, + "clear_protection_last_force: CLEAR_LAST_FAULT failed (%d) even with gate open\n", + ret); + + for (int attempt = 0; attempt < MPQ8646_NVM_RETRY_MAX; attempt++) { + rc = i2c_smbus_write_word_data(priv->client, + MPS_MFR_CFG_EXT, + (u16)cfg_orig); + if (rc >= 0) + break; + usleep_range(MPQ8646_NVM_RETRY_DELAY_US_MIN, + MPQ8646_NVM_RETRY_DELAY_US_MAX); + } + if (rc < 0) { + dev_warn(&priv->client->dev, + "clear_protection_last_force: MFR_CFG_EXT restore failed after retries (%d): gate may stay open until POR\n", + rc); + if (ret == 0) + ret = rc; + } + } + + if (wp_orig != 0) { + rc = i2c_smbus_write_byte_data(priv->client, + PMBUS_WRITE_PROTECT, + (u8)wp_orig); + if (rc < 0) { + dev_warn(&priv->client->dev, + "clear_protection_last_force: WP restore failed (%d)\n", + rc); + if (ret == 0) + ret = rc; + } + } + return ret; +} +DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_clear_protection_last_force_fops, + NULL, mpq8646_dbg_clear_protection_last_force, "%llu\n"); + +static int mpq8646_dbg_store_all(void *data, u64 val) +{ + struct mpq8646_priv *priv = data; + int rc; + + if (!val) + return 0; + + guard(pmbus_lock)(priv->client); + scoped_guard(mutex, &priv->mps_lock) + rc = i2c_smbus_write_byte(priv->client, PMBUS_STORE_USER_ALL); + if (rc < 0) + dev_warn(&priv->client->dev, + "store_all: STORE_USER_ALL (0x15) write failed (%d)\n", + rc); + return rc; +} +DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_store_all_fops, + NULL, mpq8646_dbg_store_all, "%llu\n"); + +static int mpq8646_dbg_restore_all(void *data, u64 val) +{ + struct mpq8646_priv *priv = data; + int rc; + + if (!val) + return 0; + + guard(pmbus_lock)(priv->client); + scoped_guard(mutex, &priv->mps_lock) + rc = i2c_smbus_write_byte(priv->client, PMBUS_RESTORE_USER_ALL); + if (rc < 0) + dev_warn(&priv->client->dev, + "restore_all: RESTORE_USER_ALL (0x16) write failed (%d)\n", + rc); + return rc; +} +DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_restore_all_fops, + NULL, mpq8646_dbg_restore_all, "%llu\n"); + +static const struct mpq8646_dbg_reg mpq8646_dbg_regs_unsafe[] = { + /* PMBus 1.3 control / margin */ + { PMBUS_ON_OFF_CONFIG, false, "on_off_config" }, + { PMBUS_VOUT_MARGIN_HIGH, true, "vout_margin_high" }, + { PMBUS_VOUT_MARGIN_LOW, true, "vout_margin_low" }, + /* MPS PMBus-level write-protect */ + { MPS_MFR_PMBUS_LOCK, true, "mfr_pmbus_lock" }, + /* MPS user-writable product revision */ + { MPS_MFR_PRODUCT_REV_USER, true, "mfr_product_rev_user" }, +}; + +static int mpq8646_dbg_reg_get(void *data, u64 *val) +{ + struct mpq8646_dbg_reg_ctx *ctx = data; + int rc; + + guard(pmbus_lock)(ctx->priv->client); + if (ctx->desc->is_word) + rc = i2c_smbus_read_word_data(ctx->priv->client, + ctx->desc->reg); + else + rc = i2c_smbus_read_byte_data(ctx->priv->client, + ctx->desc->reg); + if (rc < 0) + return rc; + *val = rc; + return 0; +} + +static int mpq8646_dbg_reg_set(void *data, u64 val) +{ + struct mpq8646_dbg_reg_ctx *ctx = data; + int rc; + + guard(pmbus_lock)(ctx->priv->client); + if (ctx->desc->is_word) + rc = i2c_smbus_write_word_data(ctx->priv->client, + ctx->desc->reg, (u16)val); + else + rc = i2c_smbus_write_byte_data(ctx->priv->client, + ctx->desc->reg, (u8)val); + return rc < 0 ? rc : 0; +} +DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_reg_rw_fops, + mpq8646_dbg_reg_get, mpq8646_dbg_reg_set, "0x%llx\n"); + +static void mpq8646_debugfs_register_unsafe(struct mpq8646_priv *priv, + struct dentry *root) +{ + struct mpq8646_dbg_reg_ctx *ctx; + size_t i; + + mpq8646_unsafe_banner(priv); + + debugfs_create_file_unsafe("clear_protection_last", 0200, root, priv, + &mpq8646_dbg_clear_protection_last_fops); + debugfs_create_file_unsafe("clear_protection_last_force", 0200, root, + priv, + &mpq8646_dbg_clear_protection_last_force_fops); + debugfs_create_file_unsafe("store_all", 0200, root, priv, + &mpq8646_dbg_store_all_fops); + debugfs_create_file_unsafe("restore_all", 0200, root, priv, + &mpq8646_dbg_restore_all_fops); + + ctx = devm_kcalloc(&priv->client->dev, + ARRAY_SIZE(mpq8646_dbg_regs_unsafe), + sizeof(*ctx), GFP_KERNEL); + if (!ctx) + return; + for (i = 0; i < ARRAY_SIZE(mpq8646_dbg_regs_unsafe); i++) { + ctx[i].priv = priv; + ctx[i].desc = &mpq8646_dbg_regs_unsafe[i]; + debugfs_create_file_unsafe(mpq8646_dbg_regs_unsafe[i].name, + 0600, root, &ctx[i], + &mpq8646_dbg_reg_rw_fops); + } +} +#else +static inline void mpq8646_debugfs_register_unsafe(struct mpq8646_priv *priv, + struct dentry *root) {} +#endif /* CONFIG_SENSORS_MPQ8646_DEBUG_UNSAFE */ + +static int mpq8646_dbg_poll_interval_get(void *data, u64 *val) +{ + struct mpq8646_priv *priv = data; + + *val = priv->alarm_poll_interval_ms; + return 0; +} + +static int mpq8646_dbg_poll_interval_set(void *data, u64 val) +{ + struct mpq8646_priv *priv = data; + bool was_off = !priv->alarm_poll_interval_ms; + + priv->alarm_poll_interval_ms = (u32)val; + + if (val && was_off && !priv->client->irq) + schedule_delayed_work(&priv->alarm_poll_work, + msecs_to_jiffies((u32)val)); + return 0; +} +DEFINE_DEBUGFS_ATTRIBUTE(mpq8646_dbg_poll_interval_fops, + mpq8646_dbg_poll_interval_get, + mpq8646_dbg_poll_interval_set, "%llu\n"); + +static void mpq8646_debugfs_register(struct mpq8646_priv *priv) +{ + struct dentry *root; + size_t i; + + root = pmbus_get_debugfs_dir(priv->client); + if (!root) + return; + + /* MPS extensions: status decode + NVM-backed PROTECTION_LAST */ + debugfs_create_file("status_decoded", 0400, root, priv, + &mpq8646_dbg_status_decoded_fops); + debugfs_create_file("protection_last", 0400, root, priv, + &mpq8646_dbg_protection_last_fops); + priv->dbg_poll = + debugfs_create_file_unsafe("alarm_poll_interval_ms", 0600, + root, priv, + &mpq8646_dbg_poll_interval_fops); + + priv->dbg_reg_ctx = devm_kcalloc(&priv->client->dev, + ARRAY_SIZE(mpq8646_dbg_regs), + sizeof(*priv->dbg_reg_ctx), + GFP_KERNEL); + if (!priv->dbg_reg_ctx) + return; + for (i = 0; i < ARRAY_SIZE(mpq8646_dbg_regs); i++) { + priv->dbg_reg_ctx[i].priv = priv; + priv->dbg_reg_ctx[i].desc = &mpq8646_dbg_regs[i]; + debugfs_create_file(mpq8646_dbg_regs[i].name, 0400, + root, &priv->dbg_reg_ctx[i], + &mpq8646_dbg_reg_fops); + } + + mpq8646_debugfs_register_unsafe(priv, root); +} + +static void mpq8646_debugfs_unregister(struct mpq8646_priv *priv) +{ + debugfs_remove(priv->dbg_poll); +} +#else +static inline void mpq8646_debugfs_register(struct mpq8646_priv *priv) {} +static inline void mpq8646_debugfs_unregister(struct mpq8646_priv *priv) {} +#endif /* CONFIG_DEBUG_FS */ + +static void mpq8646_alarm_poll_work(struct work_struct *work) +{ + struct mpq8646_priv *priv = container_of(to_delayed_work(work), + struct mpq8646_priv, + alarm_poll_work); + + if (priv->client->irq) + return; /* SMBALERT# wired; polling not needed */ + + if (!priv->alarm_poll_interval_ms) + return; /* polling disabled; don't re-arm */ + + pmbus_check_and_notify_faults(priv->client); + + schedule_delayed_work(&priv->alarm_poll_work, + msecs_to_jiffies(priv->alarm_poll_interval_ms)); +} + +static int mpq8646_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct pmbus_driver_info *info; + struct mpq8646_priv *priv; + u32 voltage_scale; + int ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + priv->client = client; + mutex_init(&priv->mps_lock); + memcpy(&priv->info, &mpq8646_info, sizeof(priv->info)); + info = &priv->info; + + info->identify = mpq8646_identify; + info->read_byte_data = mpq8646_read_byte_data; + info->read_word_data = mpq8646_read_word_data; + info->write_word_data = mpq8646_write_word_data; + dev->platform_data = &mpq8646_no_pec_pdata; + +#if IS_ENABLED(CONFIG_REGULATOR) + info->reg_desc = mpq8646_reg_desc; + info->num_regulators = ARRAY_SIZE(mpq8646_reg_desc); +#endif + + INIT_DELAYED_WORK(&priv->alarm_poll_work, mpq8646_alarm_poll_work); + priv->alarm_poll_interval_ms = MPQ8646_ALARM_POLL_MS_DEFAULT; + + if (!device_property_read_u32(dev, "mps,vout-fb-divider-ratio-permille", + &voltage_scale)) { + if (voltage_scale > MPQ8646_VOUT_SCALE_LOOP_MAX) + return -EINVAL; + + ret = i2c_smbus_write_word_data(client, PMBUS_VOUT_SCALE_LOOP, + voltage_scale); + if (ret) + return ret; + } + + ret = pmbus_do_probe(client, info); + if (ret) + return ret; + + mpq8646_debugfs_register(priv); + + if (!client->irq) + schedule_delayed_work(&priv->alarm_poll_work, + msecs_to_jiffies(priv->alarm_poll_interval_ms)); + +#if IS_ENABLED(CONFIG_NVMEM) + { + struct nvmem_config cfg = { + .dev = &client->dev, + .name = dev_name(&client->dev), + .owner = THIS_MODULE, + .read_only = true, + .root_only = true, + .word_size = 1, + .stride = 1, + .size = MPQ8646_NVMEM_SIZE, + .reg_read = mpq8646_nvmem_read, + .priv = priv, + }; + struct nvmem_device *nv = devm_nvmem_register(&client->dev, &cfg); + + if (IS_ERR(nv)) + dev_warn(&client->dev, + "nvmem snapshot register failed (%pe)\n", + nv); + } +#endif + return 0; +}; + +static void mpq8646_remove(struct i2c_client *client) +{ + struct mpq8646_priv *priv = mpq8646_priv_from_client(client); + + mpq8646_debugfs_unregister(priv); + cancel_delayed_work_sync(&priv->alarm_poll_work); +} + +static struct i2c_driver mpq8646_driver = { + .driver = { + .name = "mpq8646", + .of_match_table = of_match_ptr(mpq8646_of_match), + }, + .probe = mpq8646_probe, + .remove = mpq8646_remove, + .id_table = mpq8646_id, +}; + +module_i2c_driver(mpq8646_driver); + +MODULE_AUTHOR("Vincent Jardin <vjardin@free.fr>"); +MODULE_DESCRIPTION("PMBus driver for MPS MPQ8646 (extended observability)"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/mpq8785.c b/drivers/hwmon/pmbus/mpq8785.c new file mode 100644 index 000000000..bbde55028 --- /dev/null +++ b/drivers/hwmon/pmbus/mpq8785.c @@ -0,0 +1,193 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for MPS MPQ8785 Step-Down Converter + */ + +#include <linux/i2c.h> +#include <linux/bitops.h> +#include <linux/module.h> +#include <linux/property.h> +#include <linux/of_device.h> +#include "pmbus.h" + +#define MPM82504_READ_TEMPERATURE_1_SIGN_POS 9 + +enum chips { mpm3695, mpm3695_25, mpm82504, mpq8785 }; + +static u16 voltage_scale_loop_max_val[] = { + [mpm3695] = GENMASK(9, 0), + [mpm3695_25] = GENMASK(11, 0), + [mpm82504] = GENMASK(9, 0), + [mpq8785] = GENMASK(10, 0), +}; + +static int mpq8785_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + int vout_mode; + + vout_mode = pmbus_read_byte_data(client, 0, PMBUS_VOUT_MODE); + if (vout_mode < 0 || vout_mode == 0xff) + return vout_mode < 0 ? vout_mode : -ENODEV; + switch (vout_mode >> 5) { + case 0: + info->format[PSC_VOLTAGE_OUT] = linear; + break; + case 1: + case 2: + info->format[PSC_VOLTAGE_OUT] = direct; + info->m[PSC_VOLTAGE_OUT] = 64; + info->b[PSC_VOLTAGE_OUT] = 0; + info->R[PSC_VOLTAGE_OUT] = 1; + break; + default: + return -ENODEV; + } + + return 0; +}; + +static int mpq8785_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VOUT_MODE: + ret = pmbus_read_byte_data(client, page, reg); + if (ret < 0) + return ret; + + if ((ret >> 5) == 1) { + /* + * The MPQ8785 chip reports VOUT_MODE as VID mode, but the driver + * treats VID as direct mode. Without this, identification would fail + * due to mode mismatch. + * This override ensures the reported mode matches the driver + * configuration, allowing successful initialization. + */ + return PB_VOUT_MODE_DIRECT; + } + + return ret; + default: + return -ENODATA; + } +} + +static int mpm82504_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + ret = pmbus_read_word_data(client, page, phase, reg); + + if (ret < 0 || reg != PMBUS_READ_TEMPERATURE_1) + return ret; + + /* Fix PMBUS_READ_TEMPERATURE_1 signedness */ + return sign_extend32(ret, MPM82504_READ_TEMPERATURE_1_SIGN_POS) & 0xffff; +} + +static struct pmbus_driver_info mpq8785_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_TEMPERATURE] = 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, + .func[0] = + PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, +}; + +static const struct i2c_device_id mpq8785_id[] = { + { .name = "mpm3695", .driver_data = mpm3695 }, + { .name = "mpm3695-25", .driver_data = mpm3695_25 }, + { .name = "mpm82504", .driver_data = mpm82504 }, + { .name = "mpq8785", .driver_data = mpq8785 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, mpq8785_id); + +static const struct of_device_id __maybe_unused mpq8785_of_match[] = { + { .compatible = "mps,mpm3695", .data = (void *)mpm3695 }, + { .compatible = "mps,mpm3695-25", .data = (void *)mpm3695_25 }, + { .compatible = "mps,mpm82504", .data = (void *)mpm82504 }, + { .compatible = "mps,mpq8785", .data = (void *)mpq8785 }, + {} +}; +MODULE_DEVICE_TABLE(of, mpq8785_of_match); + +static int mpq8785_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct pmbus_driver_info *info; + enum chips chip_id; + u32 voltage_scale; + int ret; + + info = devm_kmemdup(dev, &mpq8785_info, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + if (dev->of_node) + chip_id = (kernel_ulong_t)of_device_get_match_data(dev); + else + chip_id = (kernel_ulong_t)i2c_get_match_data(client); + + switch (chip_id) { + case mpm3695: + case mpm3695_25: + case mpm82504: + info->format[PSC_VOLTAGE_OUT] = direct; + info->m[PSC_VOLTAGE_OUT] = 8; + info->b[PSC_VOLTAGE_OUT] = 0; + info->R[PSC_VOLTAGE_OUT] = 2; + info->read_word_data = mpm82504_read_word_data; + break; + case mpq8785: + info->identify = mpq8785_identify; + info->read_byte_data = mpq8785_read_byte_data; + break; + default: + return -ENODEV; + } + + if (!device_property_read_u32(dev, "mps,vout-fb-divider-ratio-permille", + &voltage_scale)) { + if (voltage_scale > voltage_scale_loop_max_val[chip_id]) + return -EINVAL; + + ret = i2c_smbus_write_word_data(client, PMBUS_VOUT_SCALE_LOOP, + voltage_scale); + if (ret) + return ret; + } + + return pmbus_do_probe(client, info); +}; + +static struct i2c_driver mpq8785_driver = { + .driver = { + .name = "mpq8785", + .of_match_table = of_match_ptr(mpq8785_of_match), + }, + .probe = mpq8785_probe, + .id_table = mpq8785_id, +}; + +module_i2c_driver(mpq8785_driver); + +MODULE_AUTHOR("Charles Hsu <ythsu0511@gmail.com>"); +MODULE_DESCRIPTION("PMBus driver for MPS MPQ8785"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/pim4328.c b/drivers/hwmon/pmbus/pim4328.c new file mode 100644 index 000000000..9056dc387 --- /dev/null +++ b/drivers/hwmon/pmbus/pim4328.c @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for PIM4006, PIM4328 and PIM4820 + * + * Copyright (c) 2021 Flextronics International Sweden AB + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include <linux/slab.h> +#include "pmbus.h" + +enum chips { pim4006, pim4328, pim4820 }; + +struct pim4328_data { + enum chips id; + struct pmbus_driver_info info; +}; + +#define to_pim4328_data(x) container_of(x, struct pim4328_data, info) + +/* PIM4006 and PIM4328 */ +#define PIM4328_MFR_READ_VINA 0xd3 +#define PIM4328_MFR_READ_VINB 0xd4 + +/* PIM4006 */ +#define PIM4328_MFR_READ_IINA 0xd6 +#define PIM4328_MFR_READ_IINB 0xd7 +#define PIM4328_MFR_FET_CHECKSTATUS 0xd9 + +/* PIM4328 */ +#define PIM4328_MFR_STATUS_BITS 0xd5 + +/* PIM4820 */ +#define PIM4328_MFR_READ_STATUS 0xd0 + +static const struct i2c_device_id pim4328_id[] = { + { .name = "bmr455", .driver_data = pim4328 }, + { .name = "pim4006", .driver_data = pim4006 }, + { .name = "pim4106", .driver_data = pim4006 }, + { .name = "pim4206", .driver_data = pim4006 }, + { .name = "pim4306", .driver_data = pim4006 }, + { .name = "pim4328", .driver_data = pim4328 }, + { .name = "pim4406", .driver_data = pim4006 }, + { .name = "pim4820", .driver_data = pim4820 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, pim4328_id); + +static int pim4328_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + if (page > 0) + return -ENXIO; + + if (phase == 0xff) + return -ENODATA; + + switch (reg) { + case PMBUS_READ_VIN: + ret = pmbus_read_word_data(client, page, phase, + phase == 0 ? PIM4328_MFR_READ_VINA + : PIM4328_MFR_READ_VINB); + break; + case PMBUS_READ_IIN: + ret = pmbus_read_word_data(client, page, phase, + phase == 0 ? PIM4328_MFR_READ_IINA + : PIM4328_MFR_READ_IINB); + break; + default: + ret = -ENODATA; + } + + return ret; +} + +static int pim4328_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct pim4328_data *data = to_pim4328_data(info); + int ret, status; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_STATUS_BYTE: + ret = pmbus_read_byte_data(client, page, PMBUS_STATUS_BYTE); + if (ret < 0) + return ret; + if (data->id == pim4006) { + status = pmbus_read_word_data(client, page, 0xff, + PIM4328_MFR_FET_CHECKSTATUS); + if (status < 0) + return status; + if (status & 0x0630) /* Input UV */ + ret |= PB_STATUS_VIN_UV; + } else if (data->id == pim4328) { + status = pmbus_read_byte_data(client, page, + PIM4328_MFR_STATUS_BITS); + if (status < 0) + return status; + if (status & 0x04) /* Input UV */ + ret |= PB_STATUS_VIN_UV; + if (status & 0x40) /* Output UV */ + ret |= PB_STATUS_NONE_ABOVE; + } else if (data->id == pim4820) { + status = pmbus_read_byte_data(client, page, + PIM4328_MFR_READ_STATUS); + if (status < 0) + return status; + if (status & 0x05) /* Input OV or OC */ + ret |= PB_STATUS_NONE_ABOVE; + if (status & 0x1a) /* Input UV */ + ret |= PB_STATUS_VIN_UV; + if (status & 0x40) /* OT */ + ret |= PB_STATUS_TEMPERATURE; + } + break; + default: + ret = -ENODATA; + } + + return ret; +} + +static int pim4328_probe(struct i2c_client *client) +{ + int status; + u8 device_id[I2C_SMBUS_BLOCK_MAX + 1]; + const struct i2c_device_id *mid; + struct pim4328_data *data; + struct pmbus_driver_info *info; + struct pmbus_platform_data *pdata; + struct device *dev = &client->dev; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA + | I2C_FUNC_SMBUS_BLOCK_DATA)) + return -ENODEV; + + data = devm_kzalloc(&client->dev, sizeof(struct pim4328_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + status = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, device_id); + if (status < 0) { + dev_err(&client->dev, "Failed to read Manufacturer Model\n"); + return status; + } + for (mid = pim4328_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, device_id, strlen(mid->name))) + break; + } + if (!mid->name[0]) { + dev_err(&client->dev, "Unsupported device\n"); + return -ENODEV; + } + + if (strcmp(client->name, mid->name)) + dev_notice(&client->dev, + "Device mismatch: Configured %s, detected %s\n", + client->name, mid->name); + + data->id = mid->driver_data; + info = &data->info; + info->pages = 1; + info->read_byte_data = pim4328_read_byte_data; + info->read_word_data = pim4328_read_word_data; + + pdata = devm_kzalloc(dev, sizeof(struct pmbus_platform_data), + GFP_KERNEL); + if (!pdata) + return -ENOMEM; + dev->platform_data = pdata; + pdata->flags = PMBUS_NO_CAPABILITY | PMBUS_NO_WRITE_PROTECT; + + switch (data->id) { + case pim4006: + info->phases[0] = 2; + info->func[0] = PMBUS_PHASE_VIRTUAL | PMBUS_HAVE_VIN + | PMBUS_HAVE_TEMP | PMBUS_HAVE_IOUT; + info->pfunc[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN; + info->pfunc[1] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN; + break; + case pim4328: + info->phases[0] = 2; + info->func[0] = PMBUS_PHASE_VIRTUAL + | PMBUS_HAVE_VCAP | PMBUS_HAVE_VIN + | PMBUS_HAVE_TEMP | PMBUS_HAVE_IOUT; + info->pfunc[0] = PMBUS_HAVE_VIN; + info->pfunc[1] = PMBUS_HAVE_VIN; + info->format[PSC_VOLTAGE_IN] = direct; + info->format[PSC_TEMPERATURE] = direct; + info->format[PSC_CURRENT_OUT] = direct; + pdata->flags |= PMBUS_USE_COEFFICIENTS_CMD; + break; + case pim4820: + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_TEMP + | PMBUS_HAVE_IIN; + info->format[PSC_VOLTAGE_IN] = direct; + info->format[PSC_TEMPERATURE] = direct; + info->format[PSC_CURRENT_IN] = direct; + pdata->flags |= PMBUS_USE_COEFFICIENTS_CMD; + break; + default: + return -ENODEV; + } + + return pmbus_do_probe(client, info); +} + +static struct i2c_driver pim4328_driver = { + .driver = { + .name = "pim4328", + }, + .probe = pim4328_probe, + .id_table = pim4328_id, +}; + +module_i2c_driver(pim4328_driver); + +MODULE_AUTHOR("Erik Rosen <erik.rosen@metormote.com>"); +MODULE_DESCRIPTION("PMBus driver for PIM4006, PIM4328, PIM4820 power interface modules"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/pli1209bc.c b/drivers/hwmon/pmbus/pli1209bc.c new file mode 100644 index 000000000..d5ab085a2 --- /dev/null +++ b/drivers/hwmon/pmbus/pli1209bc.c @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Vicor PLI1209BC Digital Supervisor + * + * Copyright (c) 2022 9elements GmbH + */ + +#include <linux/delay.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include <linux/regulator/driver.h> +#include "pmbus.h" + +/* + * The capability command is only supported at page 0. Probing the device while + * the page register is set to 1 will falsely enable PEC support. Disable + * capability probing accordingly, since the PLI1209BC does not have any + * additional capabilities. + */ +static struct pmbus_platform_data pli1209bc_plat_data = { + .flags = PMBUS_NO_CAPABILITY, +}; + +static int pli1209bc_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int data; + + switch (reg) { + /* PMBUS_READ_POUT uses a direct format with R=0 */ + case PMBUS_READ_POUT: + data = pmbus_read_word_data(client, page, phase, reg); + if (data < 0) + return data; + data = sign_extend32(data, 15) * 10; + return clamp_val(data, -32768, 32767) & 0xffff; + /* + * PMBUS_READ_VOUT and PMBUS_READ_TEMPERATURE_1 return invalid data + * when the BCM is turned off. Since it is not possible to return + * ENODATA error, return zero instead. + */ + case PMBUS_READ_VOUT: + case PMBUS_READ_TEMPERATURE_1: + data = pmbus_read_word_data(client, page, phase, + PMBUS_STATUS_WORD); + if (data < 0) + return data; + if (data & PB_STATUS_POWER_GOOD_N) + return 0; + return pmbus_read_word_data(client, page, phase, reg); + default: + return -ENODATA; + } +} + +#if IS_ENABLED(CONFIG_SENSORS_PLI1209BC_REGULATOR) +static const struct regulator_desc pli1209bc_reg_desc = { + .name = "vout2", + .id = 1, + .of_match = of_match_ptr("vout2"), + .regulators_node = of_match_ptr("regulators"), + .ops = &pmbus_regulator_ops, + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, +}; +#endif + +static struct pmbus_driver_info pli1209bc_info = { + .pages = 2, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 1, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 1, + .m[PSC_VOLTAGE_OUT] = 1, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 1, + .m[PSC_CURRENT_IN] = 1, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = 3, + .m[PSC_CURRENT_OUT] = 1, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 2, + .m[PSC_POWER] = 1, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = 1, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 0, + /* + * Page 0 sums up all attributes except voltage readings. + * The pli1209 digital supervisor only contains a single BCM, making + * page 0 redundant. + */ + .func[1] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT + | PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT + | PMBUS_HAVE_PIN | PMBUS_HAVE_POUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_INPUT, + .read_word_data = pli1209bc_read_word_data, + .write_delay = 250, +#if IS_ENABLED(CONFIG_SENSORS_PLI1209BC_REGULATOR) + .num_regulators = 1, + .reg_desc = &pli1209bc_reg_desc, +#endif +}; + +static int pli1209bc_probe(struct i2c_client *client) +{ + client->dev.platform_data = &pli1209bc_plat_data; + return pmbus_do_probe(client, &pli1209bc_info); +} + +static const struct i2c_device_id pli1209bc_id[] = { + { .name = "pli1209bc" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, pli1209bc_id); + +#ifdef CONFIG_OF +static const struct of_device_id pli1209bc_of_match[] = { + { .compatible = "vicor,pli1209bc" }, + { }, +}; +MODULE_DEVICE_TABLE(of, pli1209bc_of_match); +#endif + +static struct i2c_driver pli1209bc_driver = { + .driver = { + .name = "pli1209bc", + .of_match_table = of_match_ptr(pli1209bc_of_match), + }, + .probe = pli1209bc_probe, + .id_table = pli1209bc_id, +}; + +module_i2c_driver(pli1209bc_driver); + +MODULE_AUTHOR("Marcello Sylvester Bauer <sylv@sylv.io>"); +MODULE_DESCRIPTION("PMBus driver for Vicor PLI1209BC"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/pm6764tr.c b/drivers/hwmon/pmbus/pm6764tr.c new file mode 100644 index 000000000..613654b15 --- /dev/null +++ b/drivers/hwmon/pmbus/pm6764tr.c @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for STMicroelectronics digital controller PM6764TR + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +#define PM6764TR_PMBUS_READ_VOUT 0xD4 + +static int pm6764tr_read_word_data(struct i2c_client *client, int page, int phase, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VMON: + ret = pmbus_read_word_data(client, page, phase, PM6764TR_PMBUS_READ_VOUT); + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static struct pmbus_driver_info pm6764tr_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = vid, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | + PMBUS_HAVE_IOUT | PMBUS_HAVE_POUT | PMBUS_HAVE_VMON | + PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, + .read_word_data = pm6764tr_read_word_data, +}; + +static int pm6764tr_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &pm6764tr_info); +} + +static const struct i2c_device_id pm6764tr_id[] = { + { .name = "pm6764tr" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, pm6764tr_id); + +static const struct of_device_id __maybe_unused pm6764tr_of_match[] = { + {.compatible = "st,pm6764tr"}, + {} +}; + +/* This is the driver that will be inserted */ +static struct i2c_driver pm6764tr_driver = { + .driver = { + .name = "pm6764tr", + .of_match_table = of_match_ptr(pm6764tr_of_match), + }, + .probe = pm6764tr_probe, + .id_table = pm6764tr_id, +}; + +module_i2c_driver(pm6764tr_driver); + +MODULE_AUTHOR("Charles Hsu"); +MODULE_DESCRIPTION("PMBus driver for ST PM6764TR"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/pmbus.c b/drivers/hwmon/pmbus/pmbus.c new file mode 100644 index 000000000..7a7eac90d --- /dev/null +++ b/drivers/hwmon/pmbus/pmbus.c @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for PMBus devices + * + * Copyright (c) 2010, 2011 Ericsson AB. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/mutex.h> +#include <linux/i2c.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +struct pmbus_device_info { + int pages; + u32 flags; +}; + +/* + * Find sensor groups and status registers on each page. + */ +static void pmbus_find_sensor_groups(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + int page; + + /* Sensors detected on page 0 only */ + if (pmbus_check_word_register(client, 0, PMBUS_READ_VIN)) + info->func[0] |= PMBUS_HAVE_VIN; + if (pmbus_check_word_register(client, 0, PMBUS_READ_VCAP)) + info->func[0] |= PMBUS_HAVE_VCAP; + if (pmbus_check_word_register(client, 0, PMBUS_READ_IIN)) + info->func[0] |= PMBUS_HAVE_IIN; + if (pmbus_check_word_register(client, 0, PMBUS_READ_PIN)) + info->func[0] |= PMBUS_HAVE_PIN; + if (info->func[0] + && pmbus_check_byte_register(client, 0, PMBUS_STATUS_INPUT)) + info->func[0] |= PMBUS_HAVE_STATUS_INPUT; + if (pmbus_check_byte_register(client, 0, PMBUS_FAN_CONFIG_12) && + pmbus_check_word_register(client, 0, PMBUS_READ_FAN_SPEED_1)) { + info->func[0] |= PMBUS_HAVE_FAN12; + if (pmbus_check_byte_register(client, 0, PMBUS_STATUS_FAN_12)) + info->func[0] |= PMBUS_HAVE_STATUS_FAN12; + } + if (pmbus_check_byte_register(client, 0, PMBUS_FAN_CONFIG_34) && + pmbus_check_word_register(client, 0, PMBUS_READ_FAN_SPEED_3)) { + info->func[0] |= PMBUS_HAVE_FAN34; + if (pmbus_check_byte_register(client, 0, PMBUS_STATUS_FAN_34)) + info->func[0] |= PMBUS_HAVE_STATUS_FAN34; + } + if (pmbus_check_word_register(client, 0, PMBUS_READ_TEMPERATURE_1)) + info->func[0] |= PMBUS_HAVE_TEMP; + if (pmbus_check_word_register(client, 0, PMBUS_READ_TEMPERATURE_2)) + info->func[0] |= PMBUS_HAVE_TEMP2; + if (pmbus_check_word_register(client, 0, PMBUS_READ_TEMPERATURE_3)) + info->func[0] |= PMBUS_HAVE_TEMP3; + if (info->func[0] & (PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 + | PMBUS_HAVE_TEMP3) + && pmbus_check_byte_register(client, 0, + PMBUS_STATUS_TEMPERATURE)) + info->func[0] |= PMBUS_HAVE_STATUS_TEMP; + + /* Sensors detected on all pages */ + for (page = 0; page < info->pages; page++) { + if (pmbus_check_word_register(client, page, PMBUS_READ_VOUT)) { + info->func[page] |= PMBUS_HAVE_VOUT; + if (pmbus_check_byte_register(client, page, + PMBUS_STATUS_VOUT)) + info->func[page] |= PMBUS_HAVE_STATUS_VOUT; + } + if (pmbus_check_word_register(client, page, PMBUS_READ_IOUT)) { + info->func[page] |= PMBUS_HAVE_IOUT; + if (pmbus_check_byte_register(client, 0, + PMBUS_STATUS_IOUT)) + info->func[page] |= PMBUS_HAVE_STATUS_IOUT; + } + if (pmbus_check_word_register(client, page, PMBUS_READ_POUT)) + info->func[page] |= PMBUS_HAVE_POUT; + } +} + +/* + * Identify chip parameters. + */ +static int pmbus_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + int ret = 0; + + if (!info->pages) { + /* + * Check if the PAGE command is supported. If it is, + * keep setting the page number until it fails or until the + * maximum number of pages has been reached. Assume that + * this is the number of pages supported by the chip. + */ + if (pmbus_check_byte_register(client, 0, PMBUS_PAGE)) { + int page; + + info->pages = PMBUS_PAGES; + + for (page = 1; page < PMBUS_PAGES; page++) { + if (pmbus_set_page(client, page, 0xff) < 0) + break; + } + pmbus_set_page(client, 0, 0xff); + info->pages = page; + } else { + info->pages = 1; + } + + pmbus_clear_faults(client); + } + + if (pmbus_check_byte_register(client, 0, PMBUS_VOUT_MODE)) { + int vout_mode, i; + + vout_mode = pmbus_read_byte_data(client, 0, PMBUS_VOUT_MODE); + if (vout_mode >= 0 && vout_mode != 0xff) { + switch (vout_mode >> 5) { + case 0: + break; + case 1: + info->format[PSC_VOLTAGE_OUT] = vid; + for (i = 0; i < info->pages; i++) + info->vrm_version[i] = vr11; + break; + case 2: + info->format[PSC_VOLTAGE_OUT] = direct; + break; + default: + ret = -ENODEV; + goto abort; + } + } + } + + /* + * We should check if the COEFFICIENTS register is supported. + * If it is, and the chip is configured for direct mode, we can read + * the coefficients from the chip, one set per group of sensor + * registers. + * + * To do this, we will need access to a chip which actually supports the + * COEFFICIENTS command, since the command is too complex to implement + * without testing it. Until then, abort if a chip configured for direct + * mode was detected. + */ + if (info->format[PSC_VOLTAGE_OUT] == direct) { + ret = -ENODEV; + goto abort; + } + + /* Try to find sensor groups */ + pmbus_find_sensor_groups(client, info); +abort: + return ret; +} + +static int pmbus_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + struct pmbus_platform_data *pdata = NULL; + struct device *dev = &client->dev; + struct pmbus_device_info *device_info; + + info = devm_kzalloc(dev, sizeof(struct pmbus_driver_info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + device_info = (struct pmbus_device_info *)i2c_get_match_data(client); + if (device_info->flags) { + pdata = devm_kzalloc(dev, sizeof(struct pmbus_platform_data), + GFP_KERNEL); + if (!pdata) + return -ENOMEM; + + pdata->flags = device_info->flags; + } + + info->pages = device_info->pages; + info->identify = pmbus_identify; + dev->platform_data = pdata; + + return pmbus_do_probe(client, info); +} + +static const struct pmbus_device_info pmbus_info_one = { + .pages = 1, + .flags = 0 +}; + +static const struct pmbus_device_info pmbus_info_zero = { + .pages = 0, + .flags = 0 +}; + +static const struct pmbus_device_info pmbus_info_one_skip = { + .pages = 1, + .flags = PMBUS_SKIP_STATUS_CHECK +}; + +static const struct pmbus_device_info pmbus_info_one_status = { + .pages = 1, + .flags = PMBUS_READ_STATUS_AFTER_FAILED_CHECK +}; + +/* + * Use driver_data to set the number of pages supported by the chip. + */ +static const struct i2c_device_id pmbus_id[] = { + { .name = "adp4000", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "bmr310", .driver_data = (kernel_ulong_t)&pmbus_info_one_status }, + { .name = "bmr316", .driver_data = (kernel_ulong_t)&pmbus_info_one}, + { .name = "bmr321", .driver_data = (kernel_ulong_t)&pmbus_info_one}, + { .name = "bmr350", .driver_data = (kernel_ulong_t)&pmbus_info_one}, + { .name = "bmr351", .driver_data = (kernel_ulong_t)&pmbus_info_one}, + { .name = "bmr453", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "bmr454", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "bmr456", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "bmr457", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "bmr458", .driver_data = (kernel_ulong_t)&pmbus_info_one_status }, + { .name = "bmr480", .driver_data = (kernel_ulong_t)&pmbus_info_one_status }, + { .name = "bmr490", .driver_data = (kernel_ulong_t)&pmbus_info_one_status }, + { .name = "bmr491", .driver_data = (kernel_ulong_t)&pmbus_info_one_status }, + { .name = "bmr492", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "dps460", .driver_data = (kernel_ulong_t)&pmbus_info_one_skip }, + { .name = "dps650ab", .driver_data = (kernel_ulong_t)&pmbus_info_one_skip }, + { .name = "dps800", .driver_data = (kernel_ulong_t)&pmbus_info_one_skip }, + { .name = "max20796", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "mdt040", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "ncp4200", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "ncp4208", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "pdt003", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "pdt006", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "pdt012", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "pmbus", .driver_data = (kernel_ulong_t)&pmbus_info_zero }, + { .name = "sgd009", .driver_data = (kernel_ulong_t)&pmbus_info_one_skip }, + { .name = "tps40400", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "tps544b20", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "tps544b25", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "tps544c20", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "tps544c25", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { .name = "udt020", .driver_data = (kernel_ulong_t)&pmbus_info_one }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, pmbus_id); + +/* This is the driver that will be inserted */ +static struct i2c_driver pmbus_driver = { + .driver = { + .name = "pmbus", + }, + .probe = pmbus_probe, + .id_table = pmbus_id, +}; + +module_i2c_driver(pmbus_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("Generic PMBus driver"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/pmbus.h b/drivers/hwmon/pmbus/pmbus.h new file mode 100644 index 000000000..2cd3216b3 --- /dev/null +++ b/drivers/hwmon/pmbus/pmbus.h @@ -0,0 +1,596 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * pmbus.h - Common defines and structures for PMBus devices + * + * Copyright (c) 2010, 2011 Ericsson AB. + * Copyright (c) 2012 Guenter Roeck + */ + +#ifndef PMBUS_H +#define PMBUS_H + +#include <linux/bitops.h> +#include <linux/cleanup.h> +#include <linux/regulator/driver.h> + +/* + * Registers + */ +enum pmbus_regs { + PMBUS_PAGE = 0x00, + PMBUS_OPERATION = 0x01, + PMBUS_ON_OFF_CONFIG = 0x02, + PMBUS_CLEAR_FAULTS = 0x03, + PMBUS_PHASE = 0x04, + + PMBUS_WRITE_PROTECT = 0x10, + + PMBUS_CAPABILITY = 0x19, + PMBUS_QUERY = 0x1A, + PMBUS_SMBALERT_MASK = 0x1B, + PMBUS_VOUT_MODE = 0x20, + PMBUS_VOUT_COMMAND = 0x21, + PMBUS_VOUT_TRIM = 0x22, + PMBUS_VOUT_CAL_OFFSET = 0x23, + PMBUS_VOUT_MAX = 0x24, + PMBUS_VOUT_MARGIN_HIGH = 0x25, + PMBUS_VOUT_MARGIN_LOW = 0x26, + PMBUS_VOUT_TRANSITION_RATE = 0x27, + PMBUS_VOUT_DROOP = 0x28, + PMBUS_VOUT_SCALE_LOOP = 0x29, + PMBUS_VOUT_SCALE_MONITOR = 0x2A, + + PMBUS_COEFFICIENTS = 0x30, + PMBUS_POUT_MAX = 0x31, + + PMBUS_FAN_CONFIG_12 = 0x3A, + PMBUS_FAN_COMMAND_1 = 0x3B, + PMBUS_FAN_COMMAND_2 = 0x3C, + PMBUS_FAN_CONFIG_34 = 0x3D, + PMBUS_FAN_COMMAND_3 = 0x3E, + PMBUS_FAN_COMMAND_4 = 0x3F, + + PMBUS_VOUT_OV_FAULT_LIMIT = 0x40, + PMBUS_VOUT_OV_FAULT_RESPONSE = 0x41, + PMBUS_VOUT_OV_WARN_LIMIT = 0x42, + PMBUS_VOUT_UV_WARN_LIMIT = 0x43, + PMBUS_VOUT_UV_FAULT_LIMIT = 0x44, + PMBUS_VOUT_UV_FAULT_RESPONSE = 0x45, + PMBUS_IOUT_OC_FAULT_LIMIT = 0x46, + PMBUS_IOUT_OC_FAULT_RESPONSE = 0x47, + PMBUS_IOUT_OC_LV_FAULT_LIMIT = 0x48, + PMBUS_IOUT_OC_LV_FAULT_RESPONSE = 0x49, + PMBUS_IOUT_OC_WARN_LIMIT = 0x4A, + PMBUS_IOUT_UC_FAULT_LIMIT = 0x4B, + PMBUS_IOUT_UC_FAULT_RESPONSE = 0x4C, + + PMBUS_OT_FAULT_LIMIT = 0x4F, + PMBUS_OT_FAULT_RESPONSE = 0x50, + PMBUS_OT_WARN_LIMIT = 0x51, + PMBUS_UT_WARN_LIMIT = 0x52, + PMBUS_UT_FAULT_LIMIT = 0x53, + PMBUS_UT_FAULT_RESPONSE = 0x54, + PMBUS_VIN_OV_FAULT_LIMIT = 0x55, + PMBUS_VIN_OV_FAULT_RESPONSE = 0x56, + PMBUS_VIN_OV_WARN_LIMIT = 0x57, + PMBUS_VIN_UV_WARN_LIMIT = 0x58, + PMBUS_VIN_UV_FAULT_LIMIT = 0x59, + + PMBUS_IIN_OC_FAULT_LIMIT = 0x5B, + PMBUS_IIN_OC_WARN_LIMIT = 0x5D, + + PMBUS_POUT_OP_FAULT_LIMIT = 0x68, + PMBUS_POUT_OP_WARN_LIMIT = 0x6A, + PMBUS_PIN_OP_WARN_LIMIT = 0x6B, + + PMBUS_STATUS_BYTE = 0x78, + PMBUS_STATUS_WORD = 0x79, + PMBUS_STATUS_VOUT = 0x7A, + PMBUS_STATUS_IOUT = 0x7B, + PMBUS_STATUS_INPUT = 0x7C, + PMBUS_STATUS_TEMPERATURE = 0x7D, + PMBUS_STATUS_CML = 0x7E, + PMBUS_STATUS_OTHER = 0x7F, + PMBUS_STATUS_MFR_SPECIFIC = 0x80, + PMBUS_STATUS_FAN_12 = 0x81, + PMBUS_STATUS_FAN_34 = 0x82, + + PMBUS_READ_VIN = 0x88, + PMBUS_READ_IIN = 0x89, + PMBUS_READ_VCAP = 0x8A, + PMBUS_READ_VOUT = 0x8B, + PMBUS_READ_IOUT = 0x8C, + PMBUS_READ_TEMPERATURE_1 = 0x8D, + PMBUS_READ_TEMPERATURE_2 = 0x8E, + PMBUS_READ_TEMPERATURE_3 = 0x8F, + PMBUS_READ_FAN_SPEED_1 = 0x90, + PMBUS_READ_FAN_SPEED_2 = 0x91, + PMBUS_READ_FAN_SPEED_3 = 0x92, + PMBUS_READ_FAN_SPEED_4 = 0x93, + PMBUS_READ_DUTY_CYCLE = 0x94, + PMBUS_READ_FREQUENCY = 0x95, + PMBUS_READ_POUT = 0x96, + PMBUS_READ_PIN = 0x97, + + PMBUS_REVISION = 0x98, + PMBUS_MFR_ID = 0x99, + PMBUS_MFR_MODEL = 0x9A, + PMBUS_MFR_REVISION = 0x9B, + PMBUS_MFR_LOCATION = 0x9C, + PMBUS_MFR_DATE = 0x9D, + PMBUS_MFR_SERIAL = 0x9E, + + PMBUS_MFR_VIN_MIN = 0xA0, + PMBUS_MFR_VIN_MAX = 0xA1, + PMBUS_MFR_IIN_MAX = 0xA2, + PMBUS_MFR_PIN_MAX = 0xA3, + PMBUS_MFR_VOUT_MIN = 0xA4, + PMBUS_MFR_VOUT_MAX = 0xA5, + PMBUS_MFR_IOUT_MAX = 0xA6, + PMBUS_MFR_POUT_MAX = 0xA7, + + PMBUS_IC_DEVICE_ID = 0xAD, + PMBUS_IC_DEVICE_REV = 0xAE, + + PMBUS_MFR_MAX_TEMP_1 = 0xC0, + PMBUS_MFR_MAX_TEMP_2 = 0xC1, + PMBUS_MFR_MAX_TEMP_3 = 0xC2, + +/* + * Virtual registers. + * Useful to support attributes which are not supported by standard PMBus + * registers but exist as manufacturer specific registers on individual chips. + * Must be mapped to real registers in device specific code. + * + * Semantics: + * Virtual registers are all word size. + * READ registers are read-only; writes are either ignored or return an error. + * RESET registers are read/write. Reading reset registers returns zero + * (used for detection), writing any value causes the associated history to be + * reset. + * Virtual registers have to be handled in device specific driver code. Chip + * driver code returns non-negative register values if a virtual register is + * supported, or a negative error code if not. The chip driver may return + * -ENODATA or any other error code in this case, though an error code other + * than -ENODATA is handled more efficiently and thus preferred. Either case, + * the calling PMBus core code will abort if the chip driver returns an error + * code when reading or writing virtual registers. + */ + PMBUS_VIRT_BASE = 0x100, + PMBUS_VIRT_READ_TEMP_AVG, + PMBUS_VIRT_READ_TEMP_MIN, + PMBUS_VIRT_READ_TEMP_MAX, + PMBUS_VIRT_RESET_TEMP_HISTORY, + PMBUS_VIRT_READ_VIN_AVG, + PMBUS_VIRT_READ_VIN_MIN, + PMBUS_VIRT_READ_VIN_MAX, + PMBUS_VIRT_RESET_VIN_HISTORY, + PMBUS_VIRT_READ_IIN_AVG, + PMBUS_VIRT_READ_IIN_MIN, + PMBUS_VIRT_READ_IIN_MAX, + PMBUS_VIRT_RESET_IIN_HISTORY, + PMBUS_VIRT_READ_PIN_AVG, + PMBUS_VIRT_READ_PIN_MIN, + PMBUS_VIRT_READ_PIN_MAX, + PMBUS_VIRT_RESET_PIN_HISTORY, + PMBUS_VIRT_READ_POUT_AVG, + PMBUS_VIRT_READ_POUT_MIN, + PMBUS_VIRT_READ_POUT_MAX, + PMBUS_VIRT_RESET_POUT_HISTORY, + PMBUS_VIRT_READ_VOUT_AVG, + PMBUS_VIRT_READ_VOUT_MIN, + PMBUS_VIRT_READ_VOUT_MAX, + PMBUS_VIRT_RESET_VOUT_HISTORY, + PMBUS_VIRT_READ_IOUT_AVG, + PMBUS_VIRT_READ_IOUT_MIN, + PMBUS_VIRT_READ_IOUT_MAX, + PMBUS_VIRT_RESET_IOUT_HISTORY, + PMBUS_VIRT_READ_TEMP2_AVG, + PMBUS_VIRT_READ_TEMP2_MIN, + PMBUS_VIRT_READ_TEMP2_MAX, + PMBUS_VIRT_RESET_TEMP2_HISTORY, + + PMBUS_VIRT_READ_VMON, + PMBUS_VIRT_VMON_UV_WARN_LIMIT, + PMBUS_VIRT_VMON_OV_WARN_LIMIT, + PMBUS_VIRT_VMON_UV_FAULT_LIMIT, + PMBUS_VIRT_VMON_OV_FAULT_LIMIT, + PMBUS_VIRT_STATUS_VMON, + + /* + * RPM and PWM Fan control + * + * Drivers wanting to expose PWM control must define the behaviour of + * PMBUS_VIRT_PWM_[1-4] and PMBUS_VIRT_PWM_ENABLE_[1-4] in the + * {read,write}_word_data callback. + * + * pmbus core provides a default implementation for + * PMBUS_VIRT_FAN_TARGET_[1-4]. + * + * TARGET, PWM and PWM_ENABLE members must be defined sequentially; + * pmbus core uses the difference between the provided register and + * it's _1 counterpart to calculate the FAN/PWM ID. + */ + PMBUS_VIRT_FAN_TARGET_1, + PMBUS_VIRT_FAN_TARGET_2, + PMBUS_VIRT_FAN_TARGET_3, + PMBUS_VIRT_FAN_TARGET_4, + PMBUS_VIRT_PWM_1, + PMBUS_VIRT_PWM_2, + PMBUS_VIRT_PWM_3, + PMBUS_VIRT_PWM_4, + PMBUS_VIRT_PWM_ENABLE_1, + PMBUS_VIRT_PWM_ENABLE_2, + PMBUS_VIRT_PWM_ENABLE_3, + PMBUS_VIRT_PWM_ENABLE_4, + + /* Samples for average + * + * Drivers wanting to expose functionality for changing the number of + * samples used for average values should implement support in + * {read,write}_word_data callback for either PMBUS_VIRT_SAMPLES if it + * applies to all types of measurements, or any number of specific + * PMBUS_VIRT_*_SAMPLES registers to allow for individual control. + */ + PMBUS_VIRT_SAMPLES, + PMBUS_VIRT_IN_SAMPLES, + PMBUS_VIRT_CURR_SAMPLES, + PMBUS_VIRT_POWER_SAMPLES, + PMBUS_VIRT_TEMP_SAMPLES, +}; + +/* + * OPERATION + */ +#define PB_OPERATION_CONTROL_ON BIT(7) + +/* + * ON_OFF_CONFIG + */ +#define PB_ON_OFF_CONFIG_POWERUP_CONTROL BIT(4) +#define PB_ON_OFF_CONFIG_OPERATION_REQ BIT(3) +#define PB_ON_OFF_CONFIG_EN_PIN_REQ BIT(2) +#define PB_ON_OFF_CONFIG_POLARITY_HIGH BIT(1) +#define PB_ON_OFF_CONFIG_TURN_OFF_FAST BIT(0) + +/* + * WRITE_PROTECT + */ +#define PB_WP_ALL BIT(7) /* all but WRITE_PROTECT */ +#define PB_WP_OP BIT(6) /* all but WP, OPERATION, PAGE */ +#define PB_WP_VOUT BIT(5) /* all but WP, OPERATION, PAGE, VOUT, ON_OFF */ + +#define PB_WP_ANY (PB_WP_ALL | PB_WP_OP | PB_WP_VOUT) + +/* + * CAPABILITY + */ +#define PB_CAPABILITY_SMBALERT BIT(4) +#define PB_CAPABILITY_ERROR_CHECK BIT(7) + +/* + * VOUT_MODE + */ +#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 + +/* + * Fan configuration + */ +#define PB_FAN_2_PULSE_MASK (BIT(0) | BIT(1)) +#define PB_FAN_2_RPM BIT(2) +#define PB_FAN_2_INSTALLED BIT(3) +#define PB_FAN_1_PULSE_MASK (BIT(4) | BIT(5)) +#define PB_FAN_1_RPM BIT(6) +#define PB_FAN_1_INSTALLED BIT(7) + +enum pmbus_fan_mode { percent = 0, rpm }; + +/* + * STATUS_BYTE, STATUS_WORD (lower) + */ +#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) + */ +#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_IOUT + */ +#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_VOUT, STATUS_INPUT + */ +#define PB_VOLTAGE_VIN_OFF 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_INPUT + */ +#define PB_PIN_OP_WARNING BIT(0) +#define PB_IIN_OC_WARNING BIT(1) +#define PB_IIN_OC_FAULT BIT(2) + +/* + * STATUS_TEMPERATURE + */ +#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_FAN + */ +#define PB_FAN_AIRFLOW_WARNING BIT(0) +#define PB_FAN_AIRFLOW_FAULT BIT(1) +#define PB_FAN_FAN2_SPEED_OVERRIDE BIT(2) +#define PB_FAN_FAN1_SPEED_OVERRIDE BIT(3) +#define PB_FAN_FAN2_WARNING BIT(4) +#define PB_FAN_FAN1_WARNING BIT(5) +#define PB_FAN_FAN2_FAULT BIT(6) +#define PB_FAN_FAN1_FAULT BIT(7) + +/* + * CML_FAULT_STATUS + */ +#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) + +enum pmbus_sensor_classes { + PSC_VOLTAGE_IN = 0, + PSC_VOLTAGE_OUT, + PSC_CURRENT_IN, + PSC_CURRENT_OUT, + PSC_POWER, + PSC_TEMPERATURE, + PSC_FAN, + PSC_PWM, + PSC_NUM_CLASSES /* Number of power sensor classes */ +}; + +#define PMBUS_PAGES 32 /* Per PMBus specification */ +#define PMBUS_PHASES 10 /* Maximum number of phases per page */ + +/* Functionality bit mask */ +#define PMBUS_HAVE_VIN BIT(0) +#define PMBUS_HAVE_VCAP BIT(1) +#define PMBUS_HAVE_VOUT BIT(2) +#define PMBUS_HAVE_IIN BIT(3) +#define PMBUS_HAVE_IOUT BIT(4) +#define PMBUS_HAVE_PIN BIT(5) +#define PMBUS_HAVE_POUT BIT(6) +#define PMBUS_HAVE_FAN12 BIT(7) +#define PMBUS_HAVE_FAN34 BIT(8) +#define PMBUS_HAVE_TEMP BIT(9) +#define PMBUS_HAVE_TEMP2 BIT(10) +#define PMBUS_HAVE_TEMP3 BIT(11) +#define PMBUS_HAVE_STATUS_VOUT BIT(12) +#define PMBUS_HAVE_STATUS_IOUT BIT(13) +#define PMBUS_HAVE_STATUS_INPUT BIT(14) +#define PMBUS_HAVE_STATUS_TEMP BIT(15) +#define PMBUS_HAVE_STATUS_FAN12 BIT(16) +#define PMBUS_HAVE_STATUS_FAN34 BIT(17) +#define PMBUS_HAVE_VMON BIT(18) +#define PMBUS_HAVE_STATUS_VMON BIT(19) +#define PMBUS_HAVE_PWM12 BIT(20) +#define PMBUS_HAVE_PWM34 BIT(21) +#define PMBUS_HAVE_SAMPLES BIT(22) + +#define PMBUS_PHASE_VIRTUAL BIT(30) /* Phases on this page are virtual */ +#define PMBUS_PAGE_VIRTUAL BIT(31) /* Page is virtual */ + +enum pmbus_data_format { linear = 0, ieee754, direct, vid }; +enum vrm_version { vr11 = 0, vr12, vr13, imvp9, amd625mv, nvidia195mv }; + +/* PMBus revision identifiers */ +#define PMBUS_REV_10 0x00 /* PMBus revision 1.0 */ +#define PMBUS_REV_11 0x11 /* PMBus revision 1.1 */ +#define PMBUS_REV_12 0x22 /* PMBus revision 1.2 */ +#define PMBUS_REV_13 0x33 /* PMBus revision 1.3 */ +#define PMBUS_REV_131 0x44 /* PMBus revision 1.3.1 */ +#define PMBUS_REV_14 0x55 /* PMBus revision 1.4 */ + +/* Operation type flags for pmbus_update_ts */ +#define PMBUS_OP_WRITE BIT(0) +#define PMBUS_OP_PAGE_CHANGE BIT(1) + +struct pmbus_driver_info { + int pages; /* Total number of pages */ + u8 phases[PMBUS_PAGES]; /* Number of phases per page */ + enum pmbus_data_format format[PSC_NUM_CLASSES]; + enum vrm_version vrm_version[PMBUS_PAGES]; /* vrm version per page */ + /* + * Support one set of coefficients for each sensor type + * Used for chips providing data in direct mode. + */ + int m[PSC_NUM_CLASSES]; /* mantissa for direct data format */ + int b[PSC_NUM_CLASSES]; /* offset */ + int R[PSC_NUM_CLASSES]; /* exponent */ + + u32 func[PMBUS_PAGES]; /* Functionality, per page */ + u32 pfunc[PMBUS_PHASES];/* Functionality, per phase */ + /* + * The following functions map manufacturing specific register values + * to PMBus standard register values. Specify only if mapping is + * necessary. + * Functions return the register value (read) or zero (write) if + * successful. A return value of -ENODATA indicates that there is no + * manufacturer specific register, but that a standard PMBus register + * may exist. Any other negative return value indicates that the + * register does not exist, and that no attempt should be made to read + * the standard register. + */ + int (*read_byte_data)(struct i2c_client *client, int page, int reg); + int (*read_word_data)(struct i2c_client *client, int page, int phase, + int reg); + int (*write_byte_data)(struct i2c_client *client, int page, int reg, + u8 byte); + int (*write_word_data)(struct i2c_client *client, int page, int reg, + u16 word); + int (*write_byte)(struct i2c_client *client, int page, u8 value); + /* + * The identify function determines supported PMBus functionality. + * This function is only necessary if a chip driver supports multiple + * chips, and the chip functionality is not pre-determined. + */ + int (*identify)(struct i2c_client *client, + struct pmbus_driver_info *info); + + /* Regulator functionality, if supported by this chip driver. */ + int num_regulators; + const struct regulator_desc *reg_desc; + + /* custom attributes */ + const struct attribute_group **groups; + + /* + * Some chips need a little delay between SMBus communication. When + * set, the generic PMBus helper functions will wait if necessary + * to meet this requirement. The access delay is honored after + * every SMBus operation. The write delay is only honored after + * SMBus write operations. + */ + int access_delay; /* in microseconds */ + int write_delay; /* in microseconds */ + int page_change_delay; /* in microseconds */ + + /* + * Some chips do not support the PMBUS_REVISION command. + * Drivers for such chips can report the supported PMBus revision here. + * + * Drivers must set have_pmbus_revision to true and provide the + * supported PMBus version in pmbus_revision. + */ + bool have_pmbus_revision; /* true if pmbus_revision is valid */ + u8 pmbus_revision; /* PMBus revision */ +}; + +/* Regulator ops */ + +extern const struct regulator_ops pmbus_regulator_ops; +int pmbus_regulator_init_cb(struct regulator_dev *rdev, + struct regulator_config *config); + +/* Macros for filling in array of struct regulator_desc */ +#define PMBUS_REGULATOR_STEP(_name, _id, _voltages, _step, _min_uV) \ + [_id] = { \ + .name = (_name # _id), \ + .id = (_id), \ + .of_match = of_match_ptr(_name # _id), \ + .regulators_node = of_match_ptr("regulators"), \ + .ops = &pmbus_regulator_ops, \ + .type = REGULATOR_VOLTAGE, \ + .owner = THIS_MODULE, \ + .n_voltages = _voltages, \ + .uV_step = _step, \ + .min_uV = _min_uV, \ + .init_cb = pmbus_regulator_init_cb, \ + } + +#define PMBUS_REGULATOR(_name, _id) PMBUS_REGULATOR_STEP(_name, _id, 0, 0, 0) + +#define __PMBUS_REGULATOR_STEP_ONE(_name, _node, _voltages, _step, _min_uV) \ + { \ + .name = (_name), \ + .of_match = of_match_ptr(_name), \ + .regulators_node = of_match_ptr(_node), \ + .ops = &pmbus_regulator_ops, \ + .type = REGULATOR_VOLTAGE, \ + .owner = THIS_MODULE, \ + .n_voltages = _voltages, \ + .uV_step = _step, \ + .min_uV = _min_uV, \ + .init_cb = pmbus_regulator_init_cb, \ + } + +/* + * _NODE macros are defined for historic reasons and MUST NOT be used in new + * drivers. + */ +#define PMBUS_REGULATOR_STEP_ONE_NODE(_name, _voltages, _step, _min_uV) \ + __PMBUS_REGULATOR_STEP_ONE(_name, "regulators", _voltages, _step, _min_uV) + +#define PMBUS_REGULATOR_ONE_NODE(_name) PMBUS_REGULATOR_STEP_ONE_NODE(_name, 0, 0, 0) + +#define PMBUS_REGULATOR_STEP_ONE(_name, _voltages, _step, _min_uV) \ + __PMBUS_REGULATOR_STEP_ONE(_name, NULL, _voltages, _step, _min_uV) + +#define PMBUS_REGULATOR_ONE(_name) PMBUS_REGULATOR_STEP_ONE(_name, 0, 0, 0) + +/* Function declarations */ + +void pmbus_clear_cache(struct i2c_client *client); +void pmbus_set_update(struct i2c_client *client, u8 reg, bool update); +void pmbus_wait(struct i2c_client *client); +void pmbus_update_ts(struct i2c_client *client, int op); +int pmbus_set_page(struct i2c_client *client, int page, int phase); +int pmbus_read_smbus_i2c_block_data(struct i2c_client *client, u8 reg, char *data_buf); +int pmbus_read_word_data(struct i2c_client *client, int page, int phase, + u8 reg); +int pmbus_write_word_data(struct i2c_client *client, int page, u8 reg, + u16 word); +int pmbus_read_byte_data(struct i2c_client *client, int page, u8 reg); +int pmbus_write_byte(struct i2c_client *client, int page, u8 value); +int pmbus_write_byte_data(struct i2c_client *client, int page, u8 reg, + u8 value); +int pmbus_update_byte_data(struct i2c_client *client, int page, u8 reg, + u8 mask, u8 value); +void pmbus_clear_faults(struct i2c_client *client); +void pmbus_check_and_notify_faults(struct i2c_client *client); +bool pmbus_check_byte_register(struct i2c_client *client, int page, int reg); +bool pmbus_check_word_register(struct i2c_client *client, int page, int reg); +int pmbus_do_probe(struct i2c_client *client, struct pmbus_driver_info *info); +const struct pmbus_driver_info *pmbus_get_driver_info(struct i2c_client + *client); +int pmbus_get_fan_rate_device(struct i2c_client *client, int page, int id, + enum pmbus_fan_mode mode); +int pmbus_get_fan_rate_cached(struct i2c_client *client, int page, int id, + enum pmbus_fan_mode mode); +int pmbus_lock_interruptible(struct i2c_client *client); +void pmbus_lock(struct i2c_client *client); +void pmbus_unlock(struct i2c_client *client); + +DEFINE_GUARD(pmbus_lock, struct i2c_client *, pmbus_lock(_T), pmbus_unlock(_T)) + +int pmbus_update_fan(struct i2c_client *client, int page, int id, + u8 config, u8 mask, u16 command); +struct dentry *pmbus_get_debugfs_dir(struct i2c_client *client); + +#endif /* PMBUS_H */ diff --git a/drivers/hwmon/pmbus/pmbus_core.c b/drivers/hwmon/pmbus/pmbus_core.c new file mode 100644 index 000000000..806c9a491 --- /dev/null +++ b/drivers/hwmon/pmbus/pmbus_core.c @@ -0,0 +1,4002 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for PMBus devices + * + * Copyright (c) 2010, 2011 Ericsson AB. + * Copyright (c) 2012 Guenter Roeck + */ + +#include <linux/atomic.h> +#include <linux/debugfs.h> +#include <linux/delay.h> +#include <linux/dcache.h> +#include <linux/kernel.h> +#include <linux/math64.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/hwmon.h> +#include <linux/hwmon-sysfs.h> +#include <linux/pmbus.h> +#include <linux/regulator/driver.h> +#include <linux/regulator/machine.h> +#include <linux/thermal.h> +#include <linux/workqueue.h> +#include "pmbus.h" + +/* + * Number of additional attribute pointers to allocate + * with each call to krealloc + */ +#define PMBUS_ATTR_ALLOC_SIZE 32 +#define PMBUS_NAME_SIZE 24 + +/* + * The type of operation used for picking the delay between + * successive pmbus operations. + */ +/* PMBUS_OP_WRITE and PMBUS_OP_PAGE_CHANGE are defined in pmbus.h */ + +static int wp = -1; +module_param(wp, int, 0444); + +struct pmbus_sensor { + struct pmbus_sensor *next; + char name[PMBUS_NAME_SIZE]; /* sysfs sensor name */ + struct sensor_device_attribute attribute; + u8 page; /* page number */ + u8 phase; /* phase number, 0xff for all phases */ + u16 reg; /* register */ + enum pmbus_sensor_classes class; /* sensor class */ + bool update; /* runtime sensor update needed */ + bool convert; /* Whether or not to apply linear/vid/direct */ + int data; /* Sensor data; negative if there was a read error */ +}; +#define to_pmbus_sensor(_attr) \ + container_of(_attr, struct pmbus_sensor, attribute) + +struct pmbus_boolean { + char name[PMBUS_NAME_SIZE]; /* sysfs boolean name */ + struct sensor_device_attribute attribute; + struct pmbus_sensor *s1; + struct pmbus_sensor *s2; +}; +#define to_pmbus_boolean(_attr) \ + container_of(_attr, struct pmbus_boolean, attribute) + +struct pmbus_label { + char name[PMBUS_NAME_SIZE]; /* sysfs label name */ + struct sensor_device_attribute attribute; + char label[PMBUS_NAME_SIZE]; /* label */ +}; +#define to_pmbus_label(_attr) \ + container_of(_attr, struct pmbus_label, attribute) + +/* Macros for converting between sensor index and register/page/status mask */ + +#define PB_STATUS_MASK 0xffff +#define PB_REG_SHIFT 16 +#define PB_REG_MASK 0x3ff +#define PB_PAGE_SHIFT 26 +#define PB_PAGE_MASK 0x3f + +#define pb_reg_to_index(page, reg, mask) (((page) << PB_PAGE_SHIFT) | \ + ((reg) << PB_REG_SHIFT) | (mask)) + +#define pb_index_to_page(index) (((index) >> PB_PAGE_SHIFT) & PB_PAGE_MASK) +#define pb_index_to_reg(index) (((index) >> PB_REG_SHIFT) & PB_REG_MASK) +#define pb_index_to_mask(index) ((index) & PB_STATUS_MASK) + +struct pmbus_data { + struct device *dev; + struct device *hwmon_dev; + struct regulator_dev **rdevs; + + u32 flags; /* from platform data */ + + bool have_pmbus_revision; + u8 revision; /* The PMBus revision the device is compliant with */ + + int exponent[PMBUS_PAGES]; + /* linear mode: exponent for output voltages */ + + const struct pmbus_driver_info *info; + + int max_attributes; + int num_attributes; + struct attribute_group group; + const struct attribute_group **groups; + + struct pmbus_sensor *sensors; + + struct mutex update_lock; + +#if IS_ENABLED(CONFIG_REGULATOR) + atomic_t regulator_events[PMBUS_PAGES]; + struct work_struct regulator_notify_work; +#endif + + bool has_status_word; /* device uses STATUS_WORD register */ + int (*read_status)(struct i2c_client *client, int page); + + s16 currpage; /* current page, -1 for unknown/unset */ + s16 currphase; /* current phase, 0xff for all, -1 for unknown/unset */ + + int vout_low[PMBUS_PAGES]; /* voltage low margin */ + int vout_high[PMBUS_PAGES]; /* voltage high margin */ + + ktime_t next_access_backoff; /* Wait until at least this time */ +}; + +struct pmbus_debugfs_entry { + struct i2c_client *client; + u8 page; + u8 reg; +}; + +static const int pmbus_fan_rpm_mask[] = { + PB_FAN_1_RPM, + PB_FAN_2_RPM, + PB_FAN_1_RPM, + PB_FAN_2_RPM, +}; + +static const int pmbus_fan_config_registers[] = { + PMBUS_FAN_CONFIG_12, + PMBUS_FAN_CONFIG_12, + PMBUS_FAN_CONFIG_34, + PMBUS_FAN_CONFIG_34 +}; + +static const int pmbus_fan_command_registers[] = { + PMBUS_FAN_COMMAND_1, + PMBUS_FAN_COMMAND_2, + PMBUS_FAN_COMMAND_3, + PMBUS_FAN_COMMAND_4, +}; + +void pmbus_clear_cache(struct i2c_client *client) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + struct pmbus_sensor *sensor; + + for (sensor = data->sensors; sensor; sensor = sensor->next) + sensor->data = -ENODATA; +} +EXPORT_SYMBOL_NS_GPL(pmbus_clear_cache, "PMBUS"); + +void pmbus_set_update(struct i2c_client *client, u8 reg, bool update) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + struct pmbus_sensor *sensor; + + for (sensor = data->sensors; sensor; sensor = sensor->next) + if (sensor->reg == reg) + sensor->update = update; +} +EXPORT_SYMBOL_NS_GPL(pmbus_set_update, "PMBUS"); + +/* Some chips need a delay between accesses. */ +void pmbus_wait(struct i2c_client *client) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + s64 delay; + + if (!data) + return; + + delay = ktime_us_delta(data->next_access_backoff, ktime_get()); + + if (delay > 0) + fsleep(delay); +} +EXPORT_SYMBOL_NS_GPL(pmbus_wait, "PMBUS"); + +/* Sets the last operation timestamp for pmbus_wait */ +void pmbus_update_ts(struct i2c_client *client, int op) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + const struct pmbus_driver_info *info; + int delay; + + if (!data) + return; + + info = data->info; + delay = info->access_delay; + + if (op & PMBUS_OP_WRITE) + delay = max(delay, info->write_delay); + if (op & PMBUS_OP_PAGE_CHANGE) + delay = max(delay, info->page_change_delay); + + if (delay > 0) + data->next_access_backoff = ktime_add_us(ktime_get(), delay); +} +EXPORT_SYMBOL_NS_GPL(pmbus_update_ts, "PMBUS"); + +int pmbus_set_page(struct i2c_client *client, int page, int phase) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + int rv; + + if (page < 0) + return 0; + + if (!(data->info->func[page] & PMBUS_PAGE_VIRTUAL) && + data->info->pages > 1 && page != data->currpage) { + pmbus_wait(client); + rv = i2c_smbus_write_byte_data(client, PMBUS_PAGE, page); + pmbus_update_ts(client, PMBUS_OP_WRITE | PMBUS_OP_PAGE_CHANGE); + if (rv < 0) + return rv; + + pmbus_wait(client); + rv = i2c_smbus_read_byte_data(client, PMBUS_PAGE); + pmbus_update_ts(client, 0); + if (rv < 0) + return rv; + + if (rv != page) + return -EIO; + } + data->currpage = page; + + if (data->info->phases[page] && data->currphase != phase && + !(data->info->func[page] & PMBUS_PHASE_VIRTUAL)) { + pmbus_wait(client); + rv = i2c_smbus_write_byte_data(client, PMBUS_PHASE, + phase); + pmbus_update_ts(client, PMBUS_OP_WRITE); + if (rv) + return rv; + } + data->currphase = phase; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(pmbus_set_page, "PMBUS"); + +int pmbus_write_byte(struct i2c_client *client, int page, u8 value) +{ + int rv; + + rv = pmbus_set_page(client, page, 0xff); + if (rv < 0) + return rv; + + pmbus_wait(client); + rv = i2c_smbus_write_byte(client, value); + pmbus_update_ts(client, PMBUS_OP_WRITE); + + return rv; +} +EXPORT_SYMBOL_NS_GPL(pmbus_write_byte, "PMBUS"); + +/* + * _pmbus_write_byte() is similar to pmbus_write_byte(), but checks if + * a device specific mapping function exists and calls it if necessary. + */ +static int _pmbus_write_byte(struct i2c_client *client, int page, u8 value) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + const struct pmbus_driver_info *info = data->info; + int status; + + if (info->write_byte) { + status = info->write_byte(client, page, value); + if (status != -ENODATA) + return status; + } + return pmbus_write_byte(client, page, value); +} + +int pmbus_write_word_data(struct i2c_client *client, int page, u8 reg, + u16 word) +{ + int rv; + + rv = pmbus_set_page(client, page, 0xff); + if (rv < 0) + return rv; + + pmbus_wait(client); + rv = i2c_smbus_write_word_data(client, reg, word); + pmbus_update_ts(client, PMBUS_OP_WRITE); + + return rv; +} +EXPORT_SYMBOL_NS_GPL(pmbus_write_word_data, "PMBUS"); + +static int pmbus_write_virt_reg(struct i2c_client *client, int page, int reg, + u16 word) +{ + int bit; + int id; + int rv; + + switch (reg) { + case PMBUS_VIRT_FAN_TARGET_1 ... PMBUS_VIRT_FAN_TARGET_4: + id = reg - PMBUS_VIRT_FAN_TARGET_1; + bit = pmbus_fan_rpm_mask[id]; + rv = pmbus_update_fan(client, page, id, bit, bit, word); + break; + default: + rv = -ENXIO; + break; + } + + return rv; +} + +/* + * _pmbus_write_word_data() is similar to pmbus_write_word_data(), but checks if + * a device specific mapping function exists and calls it if necessary. + */ +static int _pmbus_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + const struct pmbus_driver_info *info = data->info; + int status; + + if (info->write_word_data) { + status = info->write_word_data(client, page, reg, word); + if (status != -ENODATA) + return status; + } + + if (reg >= PMBUS_VIRT_BASE) + return pmbus_write_virt_reg(client, page, reg, word); + + return pmbus_write_word_data(client, page, reg, word); +} + +/* + * _pmbus_write_byte_data() is similar to pmbus_write_byte_data(), but checks if + * a device specific mapping function exists and calls it if necessary. + */ +static int _pmbus_write_byte_data(struct i2c_client *client, int page, int reg, u8 value) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + const struct pmbus_driver_info *info = data->info; + int status; + + if (info->write_byte_data) { + status = info->write_byte_data(client, page, reg, value); + if (status != -ENODATA) + return status; + } + return pmbus_write_byte_data(client, page, reg, value); +} + +/* + * _pmbus_read_byte_data() is similar to pmbus_read_byte_data(), but checks if + * a device specific mapping function exists and calls it if necessary. + */ +static int _pmbus_read_byte_data(struct i2c_client *client, int page, int reg) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + const struct pmbus_driver_info *info = data->info; + int status; + + if (info->read_byte_data) { + status = info->read_byte_data(client, page, reg); + if (status != -ENODATA) + return status; + } + return pmbus_read_byte_data(client, page, reg); +} + +int pmbus_update_fan(struct i2c_client *client, int page, int id, + u8 config, u8 mask, u16 command) +{ + int from; + int rv; + u8 to; + + from = _pmbus_read_byte_data(client, page, + pmbus_fan_config_registers[id]); + if (from < 0) + return from; + + to = (from & ~mask) | (config & mask); + if (to != from) { + rv = _pmbus_write_byte_data(client, page, + pmbus_fan_config_registers[id], to); + if (rv < 0) + return rv; + } + + return _pmbus_write_word_data(client, page, + pmbus_fan_command_registers[id], command); +} +EXPORT_SYMBOL_NS_GPL(pmbus_update_fan, "PMBUS"); + +int pmbus_read_word_data(struct i2c_client *client, int page, int phase, u8 reg) +{ + int rv; + + rv = pmbus_set_page(client, page, phase); + if (rv < 0) + return rv; + + pmbus_wait(client); + rv = i2c_smbus_read_word_data(client, reg); + pmbus_update_ts(client, 0); + + return rv; +} +EXPORT_SYMBOL_NS_GPL(pmbus_read_word_data, "PMBUS"); + +static int pmbus_read_virt_reg(struct i2c_client *client, int page, int reg) +{ + int rv; + int id; + + switch (reg) { + case PMBUS_VIRT_FAN_TARGET_1 ... PMBUS_VIRT_FAN_TARGET_4: + id = reg - PMBUS_VIRT_FAN_TARGET_1; + rv = pmbus_get_fan_rate_device(client, page, id, rpm); + break; + default: + rv = -ENXIO; + break; + } + + return rv; +} + +/* + * _pmbus_read_word_data() is similar to pmbus_read_word_data(), but checks if + * a device specific mapping function exists and calls it if necessary. + */ +static int _pmbus_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + const struct pmbus_driver_info *info = data->info; + int status; + + if (info->read_word_data) { + status = info->read_word_data(client, page, phase, reg); + if (status != -ENODATA) + return status; + } + + if (reg >= PMBUS_VIRT_BASE) + return pmbus_read_virt_reg(client, page, reg); + + return pmbus_read_word_data(client, page, phase, reg); +} + +/* Same as above, but without phase parameter, for use in check functions */ +static int __pmbus_read_word_data(struct i2c_client *client, int page, int reg) +{ + return _pmbus_read_word_data(client, page, 0xff, reg); +} + +int pmbus_read_byte_data(struct i2c_client *client, int page, u8 reg) +{ + int rv; + + rv = pmbus_set_page(client, page, 0xff); + if (rv < 0) + return rv; + + pmbus_wait(client); + rv = i2c_smbus_read_byte_data(client, reg); + pmbus_update_ts(client, 0); + + return rv; +} +EXPORT_SYMBOL_NS_GPL(pmbus_read_byte_data, "PMBUS"); + +int pmbus_write_byte_data(struct i2c_client *client, int page, u8 reg, u8 value) +{ + int rv; + + rv = pmbus_set_page(client, page, 0xff); + if (rv < 0) + return rv; + + pmbus_wait(client); + rv = i2c_smbus_write_byte_data(client, reg, value); + pmbus_update_ts(client, PMBUS_OP_WRITE); + + return rv; +} +EXPORT_SYMBOL_NS_GPL(pmbus_write_byte_data, "PMBUS"); + +int pmbus_update_byte_data(struct i2c_client *client, int page, u8 reg, + u8 mask, u8 value) +{ + unsigned int tmp; + int rv; + + rv = _pmbus_read_byte_data(client, page, reg); + if (rv < 0) + return rv; + + tmp = (rv & ~mask) | (value & mask); + + if (tmp != rv) + rv = _pmbus_write_byte_data(client, page, reg, tmp); + + return rv < 0 ? rv : 0; +} +EXPORT_SYMBOL_NS_GPL(pmbus_update_byte_data, "PMBUS"); + +/** + * pmbus_read_smbus_i2c_block_data() - Read SMBus/I2C block data + * @client: Handle to slave device + * @reg: Byte interpreted by slave + * @data_buf: Byte array into which data will be read + * Return: Negative errno or number of bytes read + * + * PMBus internal function to read a SMBus block from a PMBus chip. + * + * PMBus chips report various properties using SMBus block read operations. + * However, not all I2C controllers support this operation. + * + * Execute SMBus block read if supported. If not supported, but SMBus I2C block + * read is supported, use it instead. Note that at most 31 data bytes can be + * read from the device if i2c_smbus_read_i2c_block_data() is used to read the + * data. This is a SMBUs protocol limit which can not be avoided. + * + * Return -EOPNOTSUPP if neither I2C_FUNC_SMBUS_READ_BLOCK_DATA nor + * I2C_FUNC_SMBUS_READ_I2C_BLOCK is supported. + * + * Callers must hold pmbus_lock or execute calls from the probe function. + */ +int pmbus_read_smbus_i2c_block_data(struct i2c_client *client, u8 reg, char *data_buf) +{ + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int blen, len, ret; + + if (i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BLOCK_DATA)) { + pmbus_wait(client); + ret = i2c_smbus_read_block_data(client, reg, data_buf); + pmbus_update_ts(client, 0); + return ret; + } + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_READ_I2C_BLOCK)) { + dev_err_once(&client->dev, "I2C adapter does not support I2C_FUNC_SMBUS_READ_I2C_BLOCK\n"); + return -EOPNOTSUPP; + } + + /* + * If the returned data is valid SMBus block data, the first byte + * must be the data length. + * + * i2c_smbus_read_i2c_block_data() may return an error if the chip + * sends NACK before the number of requested bytes is received. + * Handle this by reading the data length first, then reading the + * entire message up to I2C_SMBUS_BLOCK_MAX bytes. This ensures + * that requested number of bytes never exceeds the number of + * bytes sent by the chip. + */ + pmbus_wait(client); + ret = i2c_smbus_read_i2c_block_data(client, reg, 1, buf); + pmbus_update_ts(client, 0); + if (ret < 0) + return ret; + + len = buf[0]; + if (len == 0) + return 0; + blen = len; + if (len >= I2C_SMBUS_BLOCK_MAX) + len = I2C_SMBUS_BLOCK_MAX - 1; + pmbus_wait(client); + ret = i2c_smbus_read_i2c_block_data(client, reg, len + 1, buf); + pmbus_update_ts(client, 0); + if (ret < 0) + return ret; + if (buf[0] != blen) + return -EIO; + memcpy(data_buf, buf + 1, len); + return len; +} +EXPORT_SYMBOL_NS_GPL(pmbus_read_smbus_i2c_block_data, "PMBUS"); + +static int pmbus_read_block_data(struct i2c_client *client, int page, u8 reg, + char *data_buf) +{ + int rv; + + rv = pmbus_set_page(client, page, 0xff); + if (rv < 0) + return rv; + + return pmbus_read_smbus_i2c_block_data(client, reg, data_buf); +} + +static struct pmbus_sensor *pmbus_find_sensor(struct pmbus_data *data, int page, + int reg) +{ + struct pmbus_sensor *sensor; + + for (sensor = data->sensors; sensor; sensor = sensor->next) { + if (sensor->page == page && sensor->reg == reg) + return sensor; + } + + return ERR_PTR(-EINVAL); +} + +static int pmbus_get_fan_rate(struct i2c_client *client, int page, int id, + enum pmbus_fan_mode mode, + bool from_cache) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + bool want_rpm, have_rpm; + struct pmbus_sensor *s; + int config; + int reg; + + want_rpm = (mode == rpm); + + if (from_cache) { + reg = want_rpm ? PMBUS_VIRT_FAN_TARGET_1 : PMBUS_VIRT_PWM_1; + s = pmbus_find_sensor(data, page, reg + id); + if (IS_ERR(s)) + return PTR_ERR(s); + + return s->data; + } + + config = _pmbus_read_byte_data(client, page, + pmbus_fan_config_registers[id]); + if (config < 0) + return config; + + have_rpm = !!(config & pmbus_fan_rpm_mask[id]); + if (want_rpm == have_rpm) + return pmbus_read_word_data(client, page, 0xff, + pmbus_fan_command_registers[id]); + + /* Can't sensibly map between RPM and PWM, just return zero */ + return 0; +} + +int pmbus_get_fan_rate_device(struct i2c_client *client, int page, int id, + enum pmbus_fan_mode mode) +{ + return pmbus_get_fan_rate(client, page, id, mode, false); +} +EXPORT_SYMBOL_NS_GPL(pmbus_get_fan_rate_device, "PMBUS"); + +int pmbus_get_fan_rate_cached(struct i2c_client *client, int page, int id, + enum pmbus_fan_mode mode) +{ + return pmbus_get_fan_rate(client, page, id, mode, true); +} +EXPORT_SYMBOL_NS_GPL(pmbus_get_fan_rate_cached, "PMBUS"); + +static void pmbus_clear_fault_page(struct i2c_client *client, int page) +{ + _pmbus_write_byte(client, page, PMBUS_CLEAR_FAULTS); +} + +void pmbus_clear_faults(struct i2c_client *client) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + int i; + + for (i = 0; i < data->info->pages; i++) + pmbus_clear_fault_page(client, i); +} +EXPORT_SYMBOL_NS_GPL(pmbus_clear_faults, "PMBUS"); + +static int pmbus_check_status_cml(struct i2c_client *client) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + int status, status2; + + status = data->read_status(client, -1); + if (status < 0 || (status & PB_STATUS_CML)) { + status2 = _pmbus_read_byte_data(client, -1, PMBUS_STATUS_CML); + if (status2 < 0 || (status2 & PB_CML_FAULT_INVALID_COMMAND)) + return -EIO; + } + return 0; +} + +static bool pmbus_check_register(struct i2c_client *client, + int (*func)(struct i2c_client *client, + int page, int reg), + int page, int reg) +{ + int rv; + struct pmbus_data *data = i2c_get_clientdata(client); + + rv = func(client, page, reg); + if (rv >= 0 && !(data->flags & PMBUS_SKIP_STATUS_CHECK)) + rv = pmbus_check_status_cml(client); + if (rv < 0 && (data->flags & PMBUS_READ_STATUS_AFTER_FAILED_CHECK)) + data->read_status(client, -1); + if (reg < PMBUS_VIRT_BASE) + pmbus_clear_fault_page(client, -1); + return rv >= 0; +} + +static bool pmbus_check_status_register(struct i2c_client *client, int page) +{ + int status; + struct pmbus_data *data = i2c_get_clientdata(client); + + status = data->read_status(client, page); + if (status >= 0 && !(data->flags & PMBUS_SKIP_STATUS_CHECK) && + (status & PB_STATUS_CML)) { + status = _pmbus_read_byte_data(client, -1, PMBUS_STATUS_CML); + if (status < 0 || (status & PB_CML_FAULT_INVALID_COMMAND)) + status = -EIO; + } + + pmbus_clear_fault_page(client, -1); + return status >= 0; +} + +bool pmbus_check_byte_register(struct i2c_client *client, int page, int reg) +{ + return pmbus_check_register(client, _pmbus_read_byte_data, page, reg); +} +EXPORT_SYMBOL_NS_GPL(pmbus_check_byte_register, "PMBUS"); + +bool pmbus_check_word_register(struct i2c_client *client, int page, int reg) +{ + return pmbus_check_register(client, __pmbus_read_word_data, page, reg); +} +EXPORT_SYMBOL_NS_GPL(pmbus_check_word_register, "PMBUS"); + +static bool __maybe_unused pmbus_check_block_register(struct i2c_client *client, + int page, int reg) +{ + int rv; + struct pmbus_data *data = i2c_get_clientdata(client); + char data_buf[I2C_SMBUS_BLOCK_MAX + 2]; + + rv = pmbus_read_block_data(client, page, reg, data_buf); + if (rv >= 0 && !(data->flags & PMBUS_SKIP_STATUS_CHECK)) + rv = pmbus_check_status_cml(client); + if (rv < 0 && (data->flags & PMBUS_READ_STATUS_AFTER_FAILED_CHECK)) + data->read_status(client, -1); + pmbus_clear_fault_page(client, -1); + return rv >= 0; +} + +const struct pmbus_driver_info *pmbus_get_driver_info(struct i2c_client *client) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + + return data->info; +} +EXPORT_SYMBOL_NS_GPL(pmbus_get_driver_info, "PMBUS"); + +static int pmbus_get_status(struct i2c_client *client, int page, int reg) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + int status; + + switch (reg) { + case PMBUS_STATUS_WORD: + status = data->read_status(client, page); + break; + default: + status = _pmbus_read_byte_data(client, page, reg); + break; + } + if (status < 0) + pmbus_clear_faults(client); + return status; +} + +static void pmbus_update_sensor_data(struct i2c_client *client, struct pmbus_sensor *sensor) +{ + if (sensor->data < 0 || sensor->update) + sensor->data = _pmbus_read_word_data(client, sensor->page, + sensor->phase, sensor->reg); +} + +/* + * Convert ieee754 sensor values to milli- or micro-units + * depending on sensor type. + * + * ieee754 data format: + * bit 15: sign + * bit 10..14: exponent + * bit 0..9: mantissa + * exponent=0: + * v=(−1)^signbit * 2^(−14) * 0.significantbits + * exponent=1..30: + * v=(−1)^signbit * 2^(exponent - 15) * 1.significantbits + * exponent=31: + * v=NaN + * + * Add the number mantissa bits into the calculations for simplicity. + * To do that, add '10' to the exponent. By doing that, we can just add + * 0x400 to normal values and get the expected result. + */ +static long pmbus_reg2data_ieee754(struct pmbus_data *data, + struct pmbus_sensor *sensor) +{ + int exponent; + bool sign; + long val; + + /* only support half precision for now */ + sign = sensor->data & 0x8000; + exponent = (sensor->data >> 10) & 0x1f; + val = sensor->data & 0x3ff; + + if (exponent == 0) { /* subnormal */ + exponent = -(14 + 10); + } else if (exponent == 0x1f) { /* NaN, convert to min/max */ + exponent = 0; + val = 65504; + } else { + exponent -= (15 + 10); /* normal */ + val |= 0x400; + } + + /* scale result to milli-units for all sensors except fans */ + if (sensor->class != PSC_FAN) + val = val * 1000L; + + /* scale result to micro-units for power sensors */ + if (sensor->class == PSC_POWER) + val = val * 1000L; + + if (exponent >= 0) + val <<= exponent; + else + val >>= -exponent; + + if (sign) + val = -val; + + return val; +} + +/* + * Convert linear sensor values to milli- or micro-units + * depending on sensor type. + */ +static s64 pmbus_reg2data_linear(struct pmbus_data *data, + struct pmbus_sensor *sensor) +{ + s16 exponent; + s32 mantissa; + s64 val; + + if (sensor->class == PSC_VOLTAGE_OUT) { /* LINEAR16 */ + exponent = data->exponent[sensor->page]; + mantissa = (u16)sensor->data; + } else { /* LINEAR11 */ + exponent = ((s16)sensor->data) >> 11; + mantissa = ((s16)((sensor->data & 0x7ff) << 5)) >> 5; + } + + val = mantissa; + + /* scale result to milli-units for all sensors except fans */ + if (sensor->class != PSC_FAN) + val = val * 1000LL; + + /* scale result to micro-units for power sensors */ + if (sensor->class == PSC_POWER) + val = val * 1000LL; + + if (exponent >= 0) + val <<= exponent; + else + val >>= -exponent; + + return val; +} + +/* + * Convert direct sensor values to milli- or micro-units + * depending on sensor type. + */ +static s64 pmbus_reg2data_direct(struct pmbus_data *data, + struct pmbus_sensor *sensor) +{ + s64 b, val = (s16)sensor->data; + s32 m, R; + + m = data->info->m[sensor->class]; + b = data->info->b[sensor->class]; + R = data->info->R[sensor->class]; + + if (m == 0) + return 0; + + /* X = 1/m * (Y * 10^-R - b) */ + R = -R; + /* scale result to milli-units for everything but fans */ + if (!(sensor->class == PSC_FAN || sensor->class == PSC_PWM)) { + R += 3; + b *= 1000; + } + + /* scale result to micro-units for power sensors */ + if (sensor->class == PSC_POWER) { + R += 3; + b *= 1000; + } + + while (R > 0) { + val *= 10; + R--; + } + while (R < 0) { + val = div_s64(val + 5LL, 10L); /* round closest */ + R++; + } + + val = div_s64(val - b, m); + return val; +} + +/* + * Convert VID sensor values to milli- or micro-units + * depending on sensor type. + */ +static s64 pmbus_reg2data_vid(struct pmbus_data *data, + struct pmbus_sensor *sensor) +{ + long val = sensor->data; + long rv = 0; + + switch (data->info->vrm_version[sensor->page]) { + case vr11: + if (val >= 0x02 && val <= 0xb2) + rv = DIV_ROUND_CLOSEST(160000 - (val - 2) * 625, 100); + break; + case vr12: + if (val >= 0x01) + rv = 250 + (val - 1) * 5; + break; + case vr13: + if (val >= 0x01) + rv = 500 + (val - 1) * 10; + break; + case imvp9: + if (val >= 0x01) + rv = 200 + (val - 1) * 10; + break; + case amd625mv: + if (val >= 0x0 && val <= 0xd8) + rv = DIV_ROUND_CLOSEST(155000 - val * 625, 100); + break; + case nvidia195mv: + if (val >= 0x01) + rv = 195 + (val - 1) * 5; /* VID step is 5mv */ + break; + } + return rv; +} + +static s64 pmbus_reg2data(struct pmbus_data *data, struct pmbus_sensor *sensor) +{ + s64 val; + + if (!sensor->convert) + return sensor->data; + + switch (data->info->format[sensor->class]) { + case direct: + val = pmbus_reg2data_direct(data, sensor); + break; + case vid: + val = pmbus_reg2data_vid(data, sensor); + break; + case ieee754: + val = pmbus_reg2data_ieee754(data, sensor); + break; + case linear: + default: + val = pmbus_reg2data_linear(data, sensor); + break; + } + return val; +} + +#define MAX_IEEE_MANTISSA (0x7ff * 1000) +#define MIN_IEEE_MANTISSA (0x400 * 1000) + +static u16 pmbus_data2reg_ieee754(struct pmbus_data *data, + struct pmbus_sensor *sensor, long val) +{ + u16 exponent = (15 + 10); + long mantissa; + u16 sign = 0; + + /* simple case */ + if (val == 0) + return 0; + + if (val < 0) { + sign = 0x8000; + val = -val; + } + + /* Power is in uW. Convert to mW before converting. */ + if (sensor->class == PSC_POWER) + val = DIV_ROUND_CLOSEST(val, 1000L); + + /* + * For simplicity, convert fan data to milli-units + * before calculating the exponent. + */ + if (sensor->class == PSC_FAN) + val = val * 1000; + + /* Reduce large mantissa until it fits into 10 bit */ + while (val > MAX_IEEE_MANTISSA && exponent < 30) { + exponent++; + val >>= 1; + } + /* + * Increase small mantissa to generate valid 'normal' + * number + */ + while (val < MIN_IEEE_MANTISSA && exponent > 1) { + exponent--; + val <<= 1; + } + + /* Convert mantissa from milli-units to units */ + mantissa = DIV_ROUND_CLOSEST(val, 1000); + + /* + * Ensure that the resulting number is within range. + * Valid range is 0x400..0x7ff, where bit 10 reflects + * the implied high bit in normalized ieee754 numbers. + * Set the range to 0x400..0x7ff to reflect this. + * The upper bit is then removed by the mask against + * 0x3ff in the final assignment. + */ + if (mantissa > 0x7ff) + mantissa = 0x7ff; + else if (mantissa < 0x400) + mantissa = 0x400; + + /* Convert to sign, 5 bit exponent, 10 bit mantissa */ + return sign | (mantissa & 0x3ff) | ((exponent << 10) & 0x7c00); +} + +#define MAX_LIN_MANTISSA (1023 * 1000) +#define MIN_LIN_MANTISSA (511 * 1000) + +static u16 pmbus_data2reg_linear(struct pmbus_data *data, + struct pmbus_sensor *sensor, s64 val) +{ + s16 exponent = 0, mantissa; + bool negative = false; + + /* simple case */ + if (val == 0) + return 0; + + if (sensor->class == PSC_VOLTAGE_OUT) { + /* LINEAR16 does not support negative voltages */ + if (val < 0) + return 0; + + /* + * For a static exponents, we don't have a choice + * but to adjust the value to it. + */ + if (data->exponent[sensor->page] < 0) + val <<= -data->exponent[sensor->page]; + else + val >>= data->exponent[sensor->page]; + val = DIV_ROUND_CLOSEST_ULL(val, 1000); + return clamp_val(val, 0, 0xffff); + } + + if (val < 0) { + negative = true; + val = -val; + } + + /* Power is in uW. Convert to mW before converting. */ + if (sensor->class == PSC_POWER) + val = DIV_ROUND_CLOSEST_ULL(val, 1000); + + /* + * For simplicity, convert fan data to milli-units + * before calculating the exponent. + */ + if (sensor->class == PSC_FAN) + val = val * 1000LL; + + /* Reduce large mantissa until it fits into 10 bit */ + while (val >= MAX_LIN_MANTISSA && exponent < 15) { + exponent++; + val >>= 1; + } + /* Increase small mantissa to improve precision */ + while (val < MIN_LIN_MANTISSA && exponent > -15) { + exponent--; + val <<= 1; + } + + /* Convert mantissa from milli-units to units */ + mantissa = clamp_val(DIV_ROUND_CLOSEST_ULL(val, 1000), 0, 0x3ff); + + /* restore sign */ + if (negative) + mantissa = -mantissa; + + /* Convert to 5 bit exponent, 11 bit mantissa */ + return (mantissa & 0x7ff) | ((exponent << 11) & 0xf800); +} + +static u16 pmbus_data2reg_direct(struct pmbus_data *data, + struct pmbus_sensor *sensor, s64 val) +{ + s64 b; + s32 m, R; + + m = data->info->m[sensor->class]; + b = data->info->b[sensor->class]; + R = data->info->R[sensor->class]; + + /* Power is in uW. Adjust R and b. */ + if (sensor->class == PSC_POWER) { + R -= 3; + b *= 1000; + } + + /* Calculate Y = (m * X + b) * 10^R */ + if (!(sensor->class == PSC_FAN || sensor->class == PSC_PWM)) { + R -= 3; /* Adjust R and b for data in milli-units */ + b *= 1000; + } + val = val * m + b; + + while (R > 0) { + val *= 10; + R--; + } + while (R < 0) { + val = div_s64(val + 5LL, 10L); /* round closest */ + R++; + } + + return (u16)clamp_val(val, S16_MIN, S16_MAX); +} + +static u16 pmbus_data2reg_vid(struct pmbus_data *data, + struct pmbus_sensor *sensor, s64 val) +{ + switch (data->info->vrm_version[sensor->page]) { + case vr12: + val = clamp_val(val, 250, 1520); + return 1 + DIV_ROUND_CLOSEST_ULL(val - 250, 5); + case vr13: + val = clamp_val(val, 500, 3040); + return 1 + DIV_ROUND_CLOSEST_ULL(val - 500, 10); + case imvp9: + val = clamp_val(val, 200, 2740); + return 1 + DIV_ROUND_CLOSEST_ULL(val - 200, 10); + case amd625mv: + val = clamp_val(val, 200, 1550); + return DIV_ROUND_CLOSEST_ULL((1550LL - val) * 100LL, 625); + case nvidia195mv: + val = clamp_val(val, 195, 1465); + return 1 + DIV_ROUND_CLOSEST_ULL(val - 195, 5); + case vr11: + default: + val = clamp_val(val, 500, 1600); + return 2 + DIV_ROUND_CLOSEST_ULL((1600LL - val) * 100LL, 625); + } +} + +static u16 pmbus_data2reg(struct pmbus_data *data, + struct pmbus_sensor *sensor, s64 val) +{ + u16 regval; + + if (!sensor->convert) + return val; + + switch (data->info->format[sensor->class]) { + case direct: + regval = pmbus_data2reg_direct(data, sensor, val); + break; + case vid: + regval = pmbus_data2reg_vid(data, sensor, val); + break; + case ieee754: + regval = pmbus_data2reg_ieee754(data, sensor, val); + break; + case linear: + default: + regval = pmbus_data2reg_linear(data, sensor, val); + break; + } + return regval; +} + +/* + * Return boolean calculated from converted data. + * <index> defines a status register index and mask. + * The mask is in the lower 8 bits, the register index is in bits 8..23. + * + * The associated pmbus_boolean structure contains optional pointers to two + * sensor attributes. If specified, those attributes are compared against each + * other to determine if a limit has been exceeded. + * + * If the sensor attribute pointers are NULL, the function returns true if + * (status[reg] & mask) is true. + * + * If sensor attribute pointers are provided, a comparison against a specified + * limit has to be performed to determine the boolean result. + * In this case, the function returns true if v1 >= v2 (where v1 and v2 are + * sensor values referenced by sensor attribute pointers s1 and s2). + * + * To determine if an object exceeds upper limits, specify <s1,s2> = <v,limit>. + * To determine if an object exceeds lower limits, specify <s1,s2> = <limit,v>. + * + * If a negative value is stored in any of the referenced registers, this value + * reflects an error code which will be returned. + */ +static int pmbus_get_boolean(struct i2c_client *client, struct pmbus_boolean *b, + int index) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + struct pmbus_sensor *s1 = b->s1; + struct pmbus_sensor *s2 = b->s2; + u16 mask = pb_index_to_mask(index); + u8 page = pb_index_to_page(index); + u16 reg = pb_index_to_reg(index); + int ret, status; + u16 regval; + + guard(pmbus_lock)(client); + + status = pmbus_get_status(client, page, reg); + if (status < 0) + return status; + + if (s1) + pmbus_update_sensor_data(client, s1); + if (s2) + pmbus_update_sensor_data(client, s2); + + regval = status & mask; + if (regval) { + if (data->revision >= PMBUS_REV_12) { + ret = _pmbus_write_byte_data(client, page, reg, regval); + if (ret) + return ret; + } else { + pmbus_clear_fault_page(client, page); + } + } + if (s1 && s2) { + s64 v1, v2; + + if (s1->data < 0) + return s1->data; + if (s2->data < 0) + return s2->data; + + v1 = pmbus_reg2data(data, s1); + v2 = pmbus_reg2data(data, s2); + ret = !!(regval && v1 >= v2); + } else { + ret = !!regval; + } + return ret; +} + +static ssize_t pmbus_show_boolean(struct device *dev, + struct device_attribute *da, char *buf) +{ + struct sensor_device_attribute *attr = to_sensor_dev_attr(da); + struct pmbus_boolean *boolean = to_pmbus_boolean(attr); + struct i2c_client *client = to_i2c_client(dev->parent); + int val; + + val = pmbus_get_boolean(client, boolean, attr->index); + if (val < 0) + return val; + return sysfs_emit(buf, "%d\n", val); +} + +static ssize_t pmbus_show_zero(struct device *dev, + struct device_attribute *devattr, char *buf) +{ + return sysfs_emit(buf, "0\n"); +} + +static ssize_t pmbus_show_sensor(struct device *dev, + struct device_attribute *devattr, char *buf) +{ + struct i2c_client *client = to_i2c_client(dev->parent); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + struct pmbus_sensor *sensor = to_pmbus_sensor(attr); + struct pmbus_data *data = i2c_get_clientdata(client); + s64 val; + + scoped_guard(pmbus_lock, client) { + pmbus_update_sensor_data(client, sensor); + if (sensor->data < 0) + return sensor->data; + val = pmbus_reg2data(data, sensor); + } + + return sysfs_emit(buf, "%lld\n", val); +} + +static ssize_t pmbus_set_sensor(struct device *dev, + struct device_attribute *devattr, + const char *buf, size_t count) +{ + struct i2c_client *client = to_i2c_client(dev->parent); + struct pmbus_data *data = i2c_get_clientdata(client); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + struct pmbus_sensor *sensor = to_pmbus_sensor(attr); + s64 val; + int ret; + u16 regval; + + if (kstrtos64(buf, 10, &val) < 0) + return -EINVAL; + + guard(pmbus_lock)(client); + + regval = pmbus_data2reg(data, sensor, val); + ret = _pmbus_write_word_data(client, sensor->page, sensor->reg, regval); + if (ret < 0) + return ret; + + sensor->data = -ENODATA; + return count; +} + +static ssize_t pmbus_show_label(struct device *dev, + struct device_attribute *da, char *buf) +{ + struct sensor_device_attribute *attr = to_sensor_dev_attr(da); + struct pmbus_label *label = to_pmbus_label(attr); + + return sysfs_emit(buf, "%s\n", label->label); +} + +static int pmbus_add_attribute(struct pmbus_data *data, struct attribute *attr) +{ + if (data->num_attributes >= data->max_attributes - 1) { + int new_max_attrs = data->max_attributes + PMBUS_ATTR_ALLOC_SIZE; + void *new_attrs = devm_krealloc_array(data->dev, data->group.attrs, + new_max_attrs, sizeof(void *), + GFP_KERNEL); + if (!new_attrs) + return -ENOMEM; + data->group.attrs = new_attrs; + data->max_attributes = new_max_attrs; + } + + data->group.attrs[data->num_attributes++] = attr; + data->group.attrs[data->num_attributes] = NULL; + return 0; +} + +static void pmbus_dev_attr_init(struct device_attribute *dev_attr, + const char *name, + umode_t mode, + ssize_t (*show)(struct device *dev, + struct device_attribute *attr, + char *buf), + ssize_t (*store)(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count)) +{ + sysfs_attr_init(&dev_attr->attr); + dev_attr->attr.name = name; + dev_attr->attr.mode = mode; + dev_attr->show = show; + dev_attr->store = store; +} + +static void pmbus_attr_init(struct sensor_device_attribute *a, + const char *name, + umode_t mode, + ssize_t (*show)(struct device *dev, + struct device_attribute *attr, + char *buf), + ssize_t (*store)(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count), + int idx) +{ + pmbus_dev_attr_init(&a->dev_attr, name, mode, show, store); + a->index = idx; +} + +static int pmbus_add_boolean(struct pmbus_data *data, + const char *name, const char *type, int seq, + struct pmbus_sensor *s1, + struct pmbus_sensor *s2, + u8 page, u16 reg, u16 mask) +{ + struct pmbus_boolean *boolean; + struct sensor_device_attribute *a; + + if (WARN((s1 && !s2) || (!s1 && s2), "Bad s1/s2 parameters\n")) + return -EINVAL; + + boolean = devm_kzalloc(data->dev, sizeof(*boolean), GFP_KERNEL); + if (!boolean) + return -ENOMEM; + + a = &boolean->attribute; + + snprintf(boolean->name, sizeof(boolean->name), "%s%d_%s", + name, seq, type); + boolean->s1 = s1; + boolean->s2 = s2; + pmbus_attr_init(a, boolean->name, 0444, pmbus_show_boolean, NULL, + pb_reg_to_index(page, reg, mask)); + + return pmbus_add_attribute(data, &a->dev_attr.attr); +} + +/* of thermal for pmbus temperature sensors */ +struct pmbus_thermal_data { + struct pmbus_data *pmbus_data; + struct pmbus_sensor *sensor; +}; + +static int pmbus_thermal_get_temp(struct thermal_zone_device *tz, int *temp) +{ + struct pmbus_thermal_data *tdata = thermal_zone_device_priv(tz); + struct pmbus_sensor *sensor = tdata->sensor; + struct pmbus_data *pmbus_data = tdata->pmbus_data; + struct i2c_client *client = to_i2c_client(pmbus_data->dev); + struct device *dev = pmbus_data->hwmon_dev; + int _temp; + + if (!dev) { + /* May not even get to hwmon yet */ + *temp = 0; + return 0; + } + + scoped_guard(pmbus_lock, client) { + pmbus_update_sensor_data(client, sensor); + if (sensor->data < 0) + return sensor->data; + _temp = (int)pmbus_reg2data(pmbus_data, sensor); + } + + *temp = _temp; + return 0; +} + +static const struct thermal_zone_device_ops pmbus_thermal_ops = { + .get_temp = pmbus_thermal_get_temp, +}; + +static int pmbus_thermal_add_sensor(struct pmbus_data *pmbus_data, + struct pmbus_sensor *sensor, int index) +{ + struct device *dev = pmbus_data->dev; + struct pmbus_thermal_data *tdata; + struct thermal_zone_device *tzd; + + tdata = devm_kzalloc(dev, sizeof(*tdata), GFP_KERNEL); + if (!tdata) + return -ENOMEM; + + tdata->sensor = sensor; + tdata->pmbus_data = pmbus_data; + + tzd = devm_thermal_of_zone_register(dev, index, tdata, + &pmbus_thermal_ops); + /* + * If CONFIG_THERMAL_OF is disabled, this returns -ENODEV, + * so ignore that error but forward any other error. + */ + if (IS_ERR(tzd) && (PTR_ERR(tzd) != -ENODEV)) + return PTR_ERR(tzd); + + return 0; +} + +static struct pmbus_sensor *pmbus_add_sensor(struct pmbus_data *data, + const char *name, const char *type, + int seq, int page, int phase, + int reg, + enum pmbus_sensor_classes class, + bool update, bool readonly, + bool writeonly, bool convert) +{ + struct sensor_device_attribute *a; + struct pmbus_sensor *sensor; + + sensor = devm_kzalloc(data->dev, sizeof(*sensor), GFP_KERNEL); + if (!sensor) + return NULL; + a = &sensor->attribute; + + if (type) + snprintf(sensor->name, sizeof(sensor->name), "%s%d_%s", + name, seq, type); + else + snprintf(sensor->name, sizeof(sensor->name), "%s%d", + name, seq); + + if (data->flags & PMBUS_WRITE_PROTECTED) + readonly = true; + + sensor->page = page; + sensor->phase = phase; + sensor->reg = reg; + sensor->class = class; + sensor->update = update; + sensor->convert = convert; + sensor->data = -ENODATA; + pmbus_attr_init(a, sensor->name, readonly ? 0444 : 0644, + writeonly ? pmbus_show_zero : pmbus_show_sensor, + pmbus_set_sensor, -1); + + if (pmbus_add_attribute(data, &a->dev_attr.attr)) + return NULL; + + sensor->next = data->sensors; + data->sensors = sensor; + + /* temperature sensors with _input values are registered with thermal */ + if (class == PSC_TEMPERATURE && strcmp(type, "input") == 0) + pmbus_thermal_add_sensor(data, sensor, seq); + + return sensor; +} + +static int pmbus_add_label(struct pmbus_data *data, + const char *name, int seq, + const char *lstring, int index, int phase) +{ + struct sensor_device_attribute *a; + struct pmbus_label *label; + + label = devm_kzalloc(data->dev, sizeof(*label), GFP_KERNEL); + if (!label) + return -ENOMEM; + + a = &label->attribute; + + snprintf(label->name, sizeof(label->name), "%s%d_label", name, seq); + if (!index) { + if (phase == 0xff) + strscpy(label->label, lstring); + else + snprintf(label->label, sizeof(label->label), "%s.%d", + lstring, phase); + } else { + if (phase == 0xff) + snprintf(label->label, sizeof(label->label), "%s%d", + lstring, index); + else + snprintf(label->label, sizeof(label->label), "%s%d.%d", + lstring, index, phase); + } + + pmbus_attr_init(a, label->name, 0444, pmbus_show_label, NULL, -1); + return pmbus_add_attribute(data, &a->dev_attr.attr); +} + +/* + * Search for attributes. Allocate sensors, booleans, and labels as needed. + */ + +/* + * The pmbus_limit_attr structure describes a single limit attribute + * and its associated alarm attribute. + */ +struct pmbus_limit_attr { + u16 reg; /* Limit register */ + u16 sbit; /* Alarm attribute status bit */ + bool readonly:1; /* True if the attribute is read-only */ + bool writeonly:1; /* True if the attribute is write-only */ + bool update:1; /* True if register needs updates */ + bool low:1; /* True if low limit; for limits with compare functions only */ + const char *attr; /* Attribute name */ + const char *alarm; /* Alarm attribute name */ +}; + +/* + * The pmbus_sensor_attr structure describes one sensor attribute. This + * description includes a reference to the associated limit attributes. + */ +struct pmbus_sensor_attr { + u16 reg; /* sensor register */ + u16 gbit; /* generic status bit */ + u8 nlimit; /* # of limit registers */ + enum pmbus_sensor_classes class;/* sensor class */ + const char *label; /* sensor label */ + bool paged:1; /* true if paged sensor */ + bool update:1; /* true if update needed */ + bool compare:1; /* true if compare function needed */ + u32 func; /* sensor mask */ + u32 sfunc; /* sensor status mask */ + int sreg; /* status register */ + const struct pmbus_limit_attr *limit;/* limit registers */ +}; + +/* + * Add a set of limit attributes and, if supported, the associated + * alarm attributes. + * returns 0 if no alarm register found, 1 if an alarm register was found, + * < 0 on errors. + */ +static int pmbus_add_limit_attrs(struct i2c_client *client, + struct pmbus_data *data, + const struct pmbus_driver_info *info, + const char *name, int index, int page, + struct pmbus_sensor *base, + const struct pmbus_sensor_attr *attr) +{ + const struct pmbus_limit_attr *l = attr->limit; + int nlimit = attr->nlimit; + int have_alarm = 0; + int i, ret; + struct pmbus_sensor *curr; + + for (i = 0; i < nlimit; i++) { + if (pmbus_check_word_register(client, page, l->reg)) { + curr = pmbus_add_sensor(data, name, l->attr, index, + page, 0xff, l->reg, attr->class, + attr->update || l->update, + l->readonly, l->writeonly, true); + if (!curr) + return -ENOMEM; + if (l->sbit && (info->func[page] & attr->sfunc)) { + ret = pmbus_add_boolean(data, name, + l->alarm, index, + attr->compare ? l->low ? curr : base + : NULL, + attr->compare ? l->low ? base : curr + : NULL, + page, attr->sreg, l->sbit); + if (ret) + return ret; + have_alarm = 1; + } + } + l++; + } + return have_alarm; +} + +static int pmbus_add_sensor_attrs_one(struct i2c_client *client, + struct pmbus_data *data, + const struct pmbus_driver_info *info, + const char *name, + int index, int page, int phase, + const struct pmbus_sensor_attr *attr, + bool paged) +{ + struct pmbus_sensor *base; + bool upper = !!(attr->gbit & 0xff00); /* need to check STATUS_WORD */ + int ret; + + if (attr->label) { + ret = pmbus_add_label(data, name, index, attr->label, + paged ? page + 1 : 0, phase); + if (ret) + return ret; + } + base = pmbus_add_sensor(data, name, "input", index, page, phase, + attr->reg, attr->class, true, true, false, true); + if (!base) + return -ENOMEM; + /* No limit and alarm attributes for phase specific sensors */ + if (attr->sfunc && phase == 0xff) { + ret = pmbus_add_limit_attrs(client, data, info, name, + index, page, base, attr); + if (ret < 0) + return ret; + /* + * Add generic alarm attribute only if there are no individual + * alarm attributes, if there is a global alarm bit, and if + * the generic status register (word or byte, depending on + * which global bit is set) for this page is accessible. + */ + if (!ret && attr->gbit && + (!upper || data->has_status_word) && + pmbus_check_status_register(client, page)) { + ret = pmbus_add_boolean(data, name, "alarm", index, + NULL, NULL, + page, PMBUS_STATUS_WORD, + attr->gbit); + if (ret) + return ret; + } + } + return 0; +} + +static bool pmbus_sensor_is_paged(const struct pmbus_driver_info *info, + const struct pmbus_sensor_attr *attr) +{ + int p; + + if (attr->paged) + return true; + + /* + * Some attributes may be present on more than one page despite + * not being marked with the paged attribute. If that is the case, + * then treat the sensor as being paged and add the page suffix to the + * attribute name. + * We don't just add the paged attribute to all such attributes, in + * order to maintain the un-suffixed labels in the case where the + * attribute is only on page 0. + */ + for (p = 1; p < info->pages; p++) { + if (info->func[p] & attr->func) + return true; + } + return false; +} + +static int pmbus_add_sensor_attrs(struct i2c_client *client, + struct pmbus_data *data, + const char *name, + const struct pmbus_sensor_attr *attrs, + int nattrs) +{ + const struct pmbus_driver_info *info = data->info; + int index, i; + int ret; + + index = 1; + for (i = 0; i < nattrs; i++) { + int page, pages; + bool paged = pmbus_sensor_is_paged(info, attrs); + + pages = paged ? info->pages : 1; + for (page = 0; page < pages; page++) { + if (info->func[page] & attrs->func) { + ret = pmbus_add_sensor_attrs_one(client, data, info, + name, index, page, + 0xff, attrs, paged); + if (ret) + return ret; + index++; + } + if (info->phases[page]) { + int phase; + + for (phase = 0; phase < info->phases[page]; + phase++) { + if (!(info->pfunc[phase] & attrs->func)) + continue; + ret = pmbus_add_sensor_attrs_one(client, + data, info, name, index, page, + phase, attrs, paged); + if (ret) + return ret; + index++; + } + } + } + attrs++; + } + return 0; +} + +static const struct pmbus_limit_attr vin_limit_attrs[] = { + { + .reg = PMBUS_VIN_UV_WARN_LIMIT, + .attr = "min", + .alarm = "min_alarm", + .sbit = PB_VOLTAGE_UV_WARNING, + }, { + .reg = PMBUS_VIN_UV_FAULT_LIMIT, + .attr = "lcrit", + .alarm = "lcrit_alarm", + .sbit = PB_VOLTAGE_UV_FAULT | PB_VOLTAGE_VIN_OFF, + }, { + .reg = PMBUS_VIN_OV_WARN_LIMIT, + .attr = "max", + .alarm = "max_alarm", + .sbit = PB_VOLTAGE_OV_WARNING, + }, { + .reg = PMBUS_VIN_OV_FAULT_LIMIT, + .attr = "crit", + .alarm = "crit_alarm", + .sbit = PB_VOLTAGE_OV_FAULT, + }, { + .reg = PMBUS_VIRT_READ_VIN_AVG, + .update = true, + .readonly = true, + .attr = "average", + }, { + .reg = PMBUS_VIRT_READ_VIN_MIN, + .update = true, + .readonly = true, + .attr = "lowest", + }, { + .reg = PMBUS_VIRT_READ_VIN_MAX, + .update = true, + .readonly = true, + .attr = "highest", + }, { + .reg = PMBUS_VIRT_RESET_VIN_HISTORY, + .writeonly = true, + .attr = "reset_history", + }, { + .reg = PMBUS_MFR_VIN_MIN, + .readonly = true, + .attr = "rated_min", + }, { + .reg = PMBUS_MFR_VIN_MAX, + .readonly = true, + .attr = "rated_max", + }, +}; + +static const struct pmbus_limit_attr vmon_limit_attrs[] = { + { + .reg = PMBUS_VIRT_VMON_UV_WARN_LIMIT, + .attr = "min", + .alarm = "min_alarm", + .sbit = PB_VOLTAGE_UV_WARNING, + }, { + .reg = PMBUS_VIRT_VMON_UV_FAULT_LIMIT, + .attr = "lcrit", + .alarm = "lcrit_alarm", + .sbit = PB_VOLTAGE_UV_FAULT, + }, { + .reg = PMBUS_VIRT_VMON_OV_WARN_LIMIT, + .attr = "max", + .alarm = "max_alarm", + .sbit = PB_VOLTAGE_OV_WARNING, + }, { + .reg = PMBUS_VIRT_VMON_OV_FAULT_LIMIT, + .attr = "crit", + .alarm = "crit_alarm", + .sbit = PB_VOLTAGE_OV_FAULT, + } +}; + +static const struct pmbus_limit_attr vout_limit_attrs[] = { + { + .reg = PMBUS_VOUT_UV_WARN_LIMIT, + .attr = "min", + .alarm = "min_alarm", + .sbit = PB_VOLTAGE_UV_WARNING, + }, { + .reg = PMBUS_VOUT_UV_FAULT_LIMIT, + .attr = "lcrit", + .alarm = "lcrit_alarm", + .sbit = PB_VOLTAGE_UV_FAULT, + }, { + .reg = PMBUS_VOUT_OV_WARN_LIMIT, + .attr = "max", + .alarm = "max_alarm", + .sbit = PB_VOLTAGE_OV_WARNING, + }, { + .reg = PMBUS_VOUT_OV_FAULT_LIMIT, + .attr = "crit", + .alarm = "crit_alarm", + .sbit = PB_VOLTAGE_OV_FAULT, + }, { + .reg = PMBUS_VIRT_READ_VOUT_AVG, + .update = true, + .readonly = true, + .attr = "average", + }, { + .reg = PMBUS_VIRT_READ_VOUT_MIN, + .update = true, + .readonly = true, + .attr = "lowest", + }, { + .reg = PMBUS_VIRT_READ_VOUT_MAX, + .update = true, + .readonly = true, + .attr = "highest", + }, { + .reg = PMBUS_VIRT_RESET_VOUT_HISTORY, + .writeonly = true, + .attr = "reset_history", + }, { + .reg = PMBUS_MFR_VOUT_MIN, + .readonly = true, + .attr = "rated_min", + }, { + .reg = PMBUS_MFR_VOUT_MAX, + .readonly = true, + .attr = "rated_max", + }, +}; + +static const struct pmbus_sensor_attr voltage_attributes[] = { + { + .reg = PMBUS_READ_VIN, + .class = PSC_VOLTAGE_IN, + .label = "vin", + .func = PMBUS_HAVE_VIN, + .sfunc = PMBUS_HAVE_STATUS_INPUT, + .sreg = PMBUS_STATUS_INPUT, + .gbit = PB_STATUS_VIN_UV, + .limit = vin_limit_attrs, + .nlimit = ARRAY_SIZE(vin_limit_attrs), + }, { + .reg = PMBUS_VIRT_READ_VMON, + .class = PSC_VOLTAGE_IN, + .label = "vmon", + .func = PMBUS_HAVE_VMON, + .sfunc = PMBUS_HAVE_STATUS_VMON, + .sreg = PMBUS_VIRT_STATUS_VMON, + .limit = vmon_limit_attrs, + .nlimit = ARRAY_SIZE(vmon_limit_attrs), + }, { + .reg = PMBUS_READ_VCAP, + .class = PSC_VOLTAGE_IN, + .label = "vcap", + .func = PMBUS_HAVE_VCAP, + }, { + .reg = PMBUS_READ_VOUT, + .class = PSC_VOLTAGE_OUT, + .label = "vout", + .paged = true, + .func = PMBUS_HAVE_VOUT, + .sfunc = PMBUS_HAVE_STATUS_VOUT, + .sreg = PMBUS_STATUS_VOUT, + .gbit = PB_STATUS_VOUT_OV, + .limit = vout_limit_attrs, + .nlimit = ARRAY_SIZE(vout_limit_attrs), + } +}; + +/* Current attributes */ + +static const struct pmbus_limit_attr iin_limit_attrs[] = { + { + .reg = PMBUS_IIN_OC_WARN_LIMIT, + .attr = "max", + .alarm = "max_alarm", + .sbit = PB_IIN_OC_WARNING, + }, { + .reg = PMBUS_IIN_OC_FAULT_LIMIT, + .attr = "crit", + .alarm = "crit_alarm", + .sbit = PB_IIN_OC_FAULT, + }, { + .reg = PMBUS_VIRT_READ_IIN_AVG, + .update = true, + .readonly = true, + .attr = "average", + }, { + .reg = PMBUS_VIRT_READ_IIN_MIN, + .update = true, + .readonly = true, + .attr = "lowest", + }, { + .reg = PMBUS_VIRT_READ_IIN_MAX, + .update = true, + .readonly = true, + .attr = "highest", + }, { + .reg = PMBUS_VIRT_RESET_IIN_HISTORY, + .writeonly = true, + .attr = "reset_history", + }, { + .reg = PMBUS_MFR_IIN_MAX, + .readonly = true, + .attr = "rated_max", + }, +}; + +static const struct pmbus_limit_attr iout_limit_attrs[] = { + { + .reg = PMBUS_IOUT_OC_WARN_LIMIT, + .attr = "max", + .alarm = "max_alarm", + .sbit = PB_IOUT_OC_WARNING, + }, { + .reg = PMBUS_IOUT_UC_FAULT_LIMIT, + .attr = "lcrit", + .alarm = "lcrit_alarm", + .sbit = PB_IOUT_UC_FAULT, + }, { + .reg = PMBUS_IOUT_OC_FAULT_LIMIT, + .attr = "crit", + .alarm = "crit_alarm", + .sbit = PB_IOUT_OC_FAULT, + }, { + .reg = PMBUS_VIRT_READ_IOUT_AVG, + .update = true, + .readonly = true, + .attr = "average", + }, { + .reg = PMBUS_VIRT_READ_IOUT_MIN, + .update = true, + .readonly = true, + .attr = "lowest", + }, { + .reg = PMBUS_VIRT_READ_IOUT_MAX, + .update = true, + .readonly = true, + .attr = "highest", + }, { + .reg = PMBUS_VIRT_RESET_IOUT_HISTORY, + .writeonly = true, + .attr = "reset_history", + }, { + .reg = PMBUS_MFR_IOUT_MAX, + .readonly = true, + .attr = "rated_max", + }, +}; + +static const struct pmbus_sensor_attr current_attributes[] = { + { + .reg = PMBUS_READ_IIN, + .class = PSC_CURRENT_IN, + .label = "iin", + .func = PMBUS_HAVE_IIN, + .sfunc = PMBUS_HAVE_STATUS_INPUT, + .sreg = PMBUS_STATUS_INPUT, + .gbit = PB_STATUS_INPUT, + .limit = iin_limit_attrs, + .nlimit = ARRAY_SIZE(iin_limit_attrs), + }, { + .reg = PMBUS_READ_IOUT, + .class = PSC_CURRENT_OUT, + .label = "iout", + .paged = true, + .func = PMBUS_HAVE_IOUT, + .sfunc = PMBUS_HAVE_STATUS_IOUT, + .sreg = PMBUS_STATUS_IOUT, + .gbit = PB_STATUS_IOUT_OC, + .limit = iout_limit_attrs, + .nlimit = ARRAY_SIZE(iout_limit_attrs), + } +}; + +/* Power attributes */ + +static const struct pmbus_limit_attr pin_limit_attrs[] = { + { + .reg = PMBUS_PIN_OP_WARN_LIMIT, + .attr = "max", + .alarm = "alarm", + .sbit = PB_PIN_OP_WARNING, + }, { + .reg = PMBUS_VIRT_READ_PIN_AVG, + .update = true, + .readonly = true, + .attr = "average", + }, { + .reg = PMBUS_VIRT_READ_PIN_MIN, + .update = true, + .readonly = true, + .attr = "input_lowest", + }, { + .reg = PMBUS_VIRT_READ_PIN_MAX, + .update = true, + .readonly = true, + .attr = "input_highest", + }, { + .reg = PMBUS_VIRT_RESET_PIN_HISTORY, + .writeonly = true, + .attr = "reset_history", + }, { + .reg = PMBUS_MFR_PIN_MAX, + .readonly = true, + .attr = "rated_max", + }, +}; + +static const struct pmbus_limit_attr pout_limit_attrs[] = { + { + .reg = PMBUS_POUT_MAX, + .attr = "cap", + .alarm = "cap_alarm", + .sbit = PB_POWER_LIMITING, + }, { + .reg = PMBUS_POUT_OP_WARN_LIMIT, + .attr = "max", + .alarm = "max_alarm", + .sbit = PB_POUT_OP_WARNING, + }, { + .reg = PMBUS_POUT_OP_FAULT_LIMIT, + .attr = "crit", + .alarm = "crit_alarm", + .sbit = PB_POUT_OP_FAULT, + }, { + .reg = PMBUS_VIRT_READ_POUT_AVG, + .update = true, + .readonly = true, + .attr = "average", + }, { + .reg = PMBUS_VIRT_READ_POUT_MIN, + .update = true, + .readonly = true, + .attr = "input_lowest", + }, { + .reg = PMBUS_VIRT_READ_POUT_MAX, + .update = true, + .readonly = true, + .attr = "input_highest", + }, { + .reg = PMBUS_VIRT_RESET_POUT_HISTORY, + .writeonly = true, + .attr = "reset_history", + }, { + .reg = PMBUS_MFR_POUT_MAX, + .readonly = true, + .attr = "rated_max", + }, +}; + +static const struct pmbus_sensor_attr power_attributes[] = { + { + .reg = PMBUS_READ_PIN, + .class = PSC_POWER, + .label = "pin", + .func = PMBUS_HAVE_PIN, + .sfunc = PMBUS_HAVE_STATUS_INPUT, + .sreg = PMBUS_STATUS_INPUT, + .gbit = PB_STATUS_INPUT, + .limit = pin_limit_attrs, + .nlimit = ARRAY_SIZE(pin_limit_attrs), + }, { + .reg = PMBUS_READ_POUT, + .class = PSC_POWER, + .label = "pout", + .paged = true, + .func = PMBUS_HAVE_POUT, + .sfunc = PMBUS_HAVE_STATUS_IOUT, + .sreg = PMBUS_STATUS_IOUT, + .limit = pout_limit_attrs, + .nlimit = ARRAY_SIZE(pout_limit_attrs), + } +}; + +/* Temperature atributes */ + +static const struct pmbus_limit_attr temp_limit_attrs[] = { + { + .reg = PMBUS_UT_WARN_LIMIT, + .low = true, + .attr = "min", + .alarm = "min_alarm", + .sbit = PB_TEMP_UT_WARNING, + }, { + .reg = PMBUS_UT_FAULT_LIMIT, + .low = true, + .attr = "lcrit", + .alarm = "lcrit_alarm", + .sbit = PB_TEMP_UT_FAULT, + }, { + .reg = PMBUS_OT_WARN_LIMIT, + .attr = "max", + .alarm = "max_alarm", + .sbit = PB_TEMP_OT_WARNING, + }, { + .reg = PMBUS_OT_FAULT_LIMIT, + .attr = "crit", + .alarm = "crit_alarm", + .sbit = PB_TEMP_OT_FAULT, + }, { + .reg = PMBUS_VIRT_READ_TEMP_MIN, + .readonly = true, + .attr = "lowest", + }, { + .reg = PMBUS_VIRT_READ_TEMP_AVG, + .readonly = true, + .attr = "average", + }, { + .reg = PMBUS_VIRT_READ_TEMP_MAX, + .readonly = true, + .attr = "highest", + }, { + .reg = PMBUS_VIRT_RESET_TEMP_HISTORY, + .writeonly = true, + .attr = "reset_history", + }, { + .reg = PMBUS_MFR_MAX_TEMP_1, + .readonly = true, + .attr = "rated_max", + }, +}; + +static const struct pmbus_limit_attr temp_limit_attrs2[] = { + { + .reg = PMBUS_UT_WARN_LIMIT, + .low = true, + .attr = "min", + .alarm = "min_alarm", + .sbit = PB_TEMP_UT_WARNING, + }, { + .reg = PMBUS_UT_FAULT_LIMIT, + .low = true, + .attr = "lcrit", + .alarm = "lcrit_alarm", + .sbit = PB_TEMP_UT_FAULT, + }, { + .reg = PMBUS_OT_WARN_LIMIT, + .attr = "max", + .alarm = "max_alarm", + .sbit = PB_TEMP_OT_WARNING, + }, { + .reg = PMBUS_OT_FAULT_LIMIT, + .attr = "crit", + .alarm = "crit_alarm", + .sbit = PB_TEMP_OT_FAULT, + }, { + .reg = PMBUS_VIRT_READ_TEMP2_MIN, + .readonly = true, + .attr = "lowest", + }, { + .reg = PMBUS_VIRT_READ_TEMP2_AVG, + .readonly = true, + .attr = "average", + }, { + .reg = PMBUS_VIRT_READ_TEMP2_MAX, + .readonly = true, + .attr = "highest", + }, { + .reg = PMBUS_VIRT_RESET_TEMP2_HISTORY, + .writeonly = true, + .attr = "reset_history", + }, { + .reg = PMBUS_MFR_MAX_TEMP_2, + .readonly = true, + .attr = "rated_max", + }, +}; + +static const struct pmbus_limit_attr temp_limit_attrs3[] = { + { + .reg = PMBUS_UT_WARN_LIMIT, + .low = true, + .attr = "min", + .alarm = "min_alarm", + .sbit = PB_TEMP_UT_WARNING, + }, { + .reg = PMBUS_UT_FAULT_LIMIT, + .low = true, + .attr = "lcrit", + .alarm = "lcrit_alarm", + .sbit = PB_TEMP_UT_FAULT, + }, { + .reg = PMBUS_OT_WARN_LIMIT, + .attr = "max", + .alarm = "max_alarm", + .sbit = PB_TEMP_OT_WARNING, + }, { + .reg = PMBUS_OT_FAULT_LIMIT, + .attr = "crit", + .alarm = "crit_alarm", + .sbit = PB_TEMP_OT_FAULT, + }, { + .reg = PMBUS_MFR_MAX_TEMP_3, + .readonly = true, + .attr = "rated_max", + }, +}; + +static const struct pmbus_sensor_attr temp_attributes[] = { + { + .reg = PMBUS_READ_TEMPERATURE_1, + .class = PSC_TEMPERATURE, + .paged = true, + .update = true, + .compare = true, + .func = PMBUS_HAVE_TEMP, + .sfunc = PMBUS_HAVE_STATUS_TEMP, + .sreg = PMBUS_STATUS_TEMPERATURE, + .gbit = PB_STATUS_TEMPERATURE, + .limit = temp_limit_attrs, + .nlimit = ARRAY_SIZE(temp_limit_attrs), + }, { + .reg = PMBUS_READ_TEMPERATURE_2, + .class = PSC_TEMPERATURE, + .paged = true, + .update = true, + .compare = true, + .func = PMBUS_HAVE_TEMP2, + .sfunc = PMBUS_HAVE_STATUS_TEMP, + .sreg = PMBUS_STATUS_TEMPERATURE, + .gbit = PB_STATUS_TEMPERATURE, + .limit = temp_limit_attrs2, + .nlimit = ARRAY_SIZE(temp_limit_attrs2), + }, { + .reg = PMBUS_READ_TEMPERATURE_3, + .class = PSC_TEMPERATURE, + .paged = true, + .update = true, + .compare = true, + .func = PMBUS_HAVE_TEMP3, + .sfunc = PMBUS_HAVE_STATUS_TEMP, + .sreg = PMBUS_STATUS_TEMPERATURE, + .gbit = PB_STATUS_TEMPERATURE, + .limit = temp_limit_attrs3, + .nlimit = ARRAY_SIZE(temp_limit_attrs3), + } +}; + +static const int pmbus_fan_registers[] = { + PMBUS_READ_FAN_SPEED_1, + PMBUS_READ_FAN_SPEED_2, + PMBUS_READ_FAN_SPEED_3, + PMBUS_READ_FAN_SPEED_4 +}; + +static const int pmbus_fan_status_registers[] = { + PMBUS_STATUS_FAN_12, + PMBUS_STATUS_FAN_12, + PMBUS_STATUS_FAN_34, + PMBUS_STATUS_FAN_34 +}; + +static const u32 pmbus_fan_flags[] = { + PMBUS_HAVE_FAN12, + PMBUS_HAVE_FAN12, + PMBUS_HAVE_FAN34, + PMBUS_HAVE_FAN34 +}; + +static const u32 pmbus_fan_status_flags[] = { + PMBUS_HAVE_STATUS_FAN12, + PMBUS_HAVE_STATUS_FAN12, + PMBUS_HAVE_STATUS_FAN34, + PMBUS_HAVE_STATUS_FAN34 +}; + +/* Fans */ + +/* Precondition: FAN_CONFIG_x_y and FAN_COMMAND_x must exist for the fan ID */ +static int pmbus_add_fan_ctrl(struct i2c_client *client, + struct pmbus_data *data, int index, int page, + int id, u8 config) +{ + struct pmbus_sensor *sensor; + + sensor = pmbus_add_sensor(data, "fan", "target", index, page, + 0xff, PMBUS_VIRT_FAN_TARGET_1 + id, PSC_FAN, + false, false, false, true); + + if (!sensor) + return -ENOMEM; + + if (!((data->info->func[page] & PMBUS_HAVE_PWM12) || + (data->info->func[page] & PMBUS_HAVE_PWM34))) + return 0; + + sensor = pmbus_add_sensor(data, "pwm", NULL, index, page, + 0xff, PMBUS_VIRT_PWM_1 + id, PSC_PWM, + false, false, false, true); + + if (!sensor) + return -ENOMEM; + + sensor = pmbus_add_sensor(data, "pwm", "enable", index, page, + 0xff, PMBUS_VIRT_PWM_ENABLE_1 + id, PSC_PWM, + true, false, false, false); + + if (!sensor) + return -ENOMEM; + + return 0; +} + +static int pmbus_add_fan_attributes(struct i2c_client *client, + struct pmbus_data *data) +{ + const struct pmbus_driver_info *info = data->info; + int index = 1; + int page; + int ret; + + for (page = 0; page < info->pages; page++) { + int f; + + for (f = 0; f < ARRAY_SIZE(pmbus_fan_registers); f++) { + int regval; + + if (!(info->func[page] & pmbus_fan_flags[f])) + break; + + if (!pmbus_check_word_register(client, page, + pmbus_fan_registers[f])) + break; + + /* + * Skip fan if not installed. + * Each fan configuration register covers multiple fans, + * so we have to do some magic. + */ + regval = _pmbus_read_byte_data(client, page, + pmbus_fan_config_registers[f]); + if (regval < 0 || + (!(regval & (PB_FAN_1_INSTALLED >> ((f & 1) * 4))))) + continue; + + if (pmbus_add_sensor(data, "fan", "input", index, + page, 0xff, pmbus_fan_registers[f], + PSC_FAN, true, true, false, true) == NULL) + return -ENOMEM; + + /* Fan control */ + if (pmbus_check_word_register(client, page, + pmbus_fan_command_registers[f])) { + ret = pmbus_add_fan_ctrl(client, data, index, + page, f, regval); + if (ret < 0) + return ret; + } + + /* + * Each fan status register covers multiple fans, + * so we have to do some magic. + */ + if ((info->func[page] & pmbus_fan_status_flags[f]) && + pmbus_check_byte_register(client, + page, pmbus_fan_status_registers[f])) { + int reg; + + if (f > 1) /* fan 3, 4 */ + reg = PMBUS_STATUS_FAN_34; + else + reg = PMBUS_STATUS_FAN_12; + ret = pmbus_add_boolean(data, "fan", + "alarm", index, NULL, NULL, page, reg, + PB_FAN_FAN1_WARNING >> (f & 1)); + if (ret) + return ret; + ret = pmbus_add_boolean(data, "fan", + "fault", index, NULL, NULL, page, reg, + PB_FAN_FAN1_FAULT >> (f & 1)); + if (ret) + return ret; + } + index++; + } + } + return 0; +} + +struct pmbus_samples_attr { + int reg; + char *name; +}; + +struct pmbus_samples_reg { + int page; + struct pmbus_samples_attr *attr; + struct sensor_device_attribute attribute; +}; + +static struct pmbus_samples_attr pmbus_samples_registers[] = { + { + .reg = PMBUS_VIRT_SAMPLES, + .name = "samples", + }, { + .reg = PMBUS_VIRT_IN_SAMPLES, + .name = "in_samples", + }, { + .reg = PMBUS_VIRT_CURR_SAMPLES, + .name = "curr_samples", + }, { + .reg = PMBUS_VIRT_POWER_SAMPLES, + .name = "power_samples", + }, { + .reg = PMBUS_VIRT_TEMP_SAMPLES, + .name = "temp_samples", + } +}; + +#define to_samples_reg(x) container_of(x, struct pmbus_samples_reg, attribute) + +static ssize_t pmbus_show_samples(struct device *dev, + struct device_attribute *devattr, char *buf) +{ + int val; + struct i2c_client *client = to_i2c_client(dev->parent); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + struct pmbus_samples_reg *reg = to_samples_reg(attr); + + scoped_guard(pmbus_lock, client) { + val = _pmbus_read_word_data(client, reg->page, 0xff, reg->attr->reg); + if (val < 0) + return val; + } + + return sysfs_emit(buf, "%d\n", val); +} + +static ssize_t pmbus_set_samples(struct device *dev, + struct device_attribute *devattr, + const char *buf, size_t count) +{ + int ret; + long val; + struct i2c_client *client = to_i2c_client(dev->parent); + struct sensor_device_attribute *attr = to_sensor_dev_attr(devattr); + struct pmbus_samples_reg *reg = to_samples_reg(attr); + + if (kstrtol(buf, 0, &val) < 0) + return -EINVAL; + + guard(pmbus_lock)(client); + + ret = _pmbus_write_word_data(client, reg->page, reg->attr->reg, val); + + return ret ? : count; +} + +static int pmbus_add_samples_attr(struct pmbus_data *data, int page, + struct pmbus_samples_attr *attr) +{ + struct sensor_device_attribute *a; + struct pmbus_samples_reg *reg; + + reg = devm_kzalloc(data->dev, sizeof(*reg), GFP_KERNEL); + if (!reg) + return -ENOMEM; + + reg->attr = attr; + reg->page = page; + + a = ®->attribute; + + pmbus_attr_init(a, attr->name, 0644, + pmbus_show_samples, pmbus_set_samples, -1); + + return pmbus_add_attribute(data, &a->dev_attr.attr); +} + +static int pmbus_add_samples_attributes(struct i2c_client *client, + struct pmbus_data *data) +{ + const struct pmbus_driver_info *info = data->info; + int s; + + if (!(info->func[0] & PMBUS_HAVE_SAMPLES)) + return 0; + + for (s = 0; s < ARRAY_SIZE(pmbus_samples_registers); s++) { + struct pmbus_samples_attr *attr; + int ret; + + attr = &pmbus_samples_registers[s]; + if (!pmbus_check_word_register(client, 0, attr->reg)) + continue; + + ret = pmbus_add_samples_attr(data, 0, attr); + if (ret) + return ret; + } + + return 0; +} + +static int pmbus_find_attributes(struct i2c_client *client, + struct pmbus_data *data) +{ + int ret; + + /* Voltage sensors */ + ret = pmbus_add_sensor_attrs(client, data, "in", voltage_attributes, + ARRAY_SIZE(voltage_attributes)); + if (ret) + return ret; + + /* Current sensors */ + ret = pmbus_add_sensor_attrs(client, data, "curr", current_attributes, + ARRAY_SIZE(current_attributes)); + if (ret) + return ret; + + /* Power sensors */ + ret = pmbus_add_sensor_attrs(client, data, "power", power_attributes, + ARRAY_SIZE(power_attributes)); + if (ret) + return ret; + + /* Temperature sensors */ + ret = pmbus_add_sensor_attrs(client, data, "temp", temp_attributes, + ARRAY_SIZE(temp_attributes)); + if (ret) + return ret; + + /* Fans */ + ret = pmbus_add_fan_attributes(client, data); + if (ret) + return ret; + + ret = pmbus_add_samples_attributes(client, data); + return ret; +} + +/* + * The pmbus_class_attr_map structure maps one sensor class to + * it's corresponding sensor attributes array. + */ +struct pmbus_class_attr_map { + enum pmbus_sensor_classes class; + int nattr; + const struct pmbus_sensor_attr *attr; +}; + +static const struct pmbus_class_attr_map class_attr_map[] = { + { + .class = PSC_VOLTAGE_IN, + .attr = voltage_attributes, + .nattr = ARRAY_SIZE(voltage_attributes), + }, { + .class = PSC_VOLTAGE_OUT, + .attr = voltage_attributes, + .nattr = ARRAY_SIZE(voltage_attributes), + }, { + .class = PSC_CURRENT_IN, + .attr = current_attributes, + .nattr = ARRAY_SIZE(current_attributes), + }, { + .class = PSC_CURRENT_OUT, + .attr = current_attributes, + .nattr = ARRAY_SIZE(current_attributes), + }, { + .class = PSC_POWER, + .attr = power_attributes, + .nattr = ARRAY_SIZE(power_attributes), + }, { + .class = PSC_TEMPERATURE, + .attr = temp_attributes, + .nattr = ARRAY_SIZE(temp_attributes), + } +}; + +/* + * Read the coefficients for direct mode. + */ +static int pmbus_read_coefficients(struct i2c_client *client, + struct pmbus_driver_info *info, + const struct pmbus_sensor_attr *attr) +{ + int rv; + union i2c_smbus_data data; + enum pmbus_sensor_classes class = attr->class; + s8 R; + s16 m, b; + + data.block[0] = 2; + data.block[1] = attr->reg; + data.block[2] = 0x01; + + pmbus_wait(client); + rv = i2c_smbus_xfer(client->adapter, client->addr, client->flags, + I2C_SMBUS_WRITE, PMBUS_COEFFICIENTS, + I2C_SMBUS_BLOCK_PROC_CALL, &data); + pmbus_update_ts(client, PMBUS_OP_WRITE); + + if (rv < 0) + return rv; + + if (data.block[0] != 5) + return -EIO; + + m = data.block[1] | (data.block[2] << 8); + b = data.block[3] | (data.block[4] << 8); + R = data.block[5]; + info->m[class] = m; + info->b[class] = b; + info->R[class] = R; + + return rv; +} + +static int pmbus_init_coefficients(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + int i, n, ret = -EINVAL; + const struct pmbus_class_attr_map *map; + const struct pmbus_sensor_attr *attr; + + for (i = 0; i < ARRAY_SIZE(class_attr_map); i++) { + map = &class_attr_map[i]; + if (info->format[map->class] != direct) + continue; + for (n = 0; n < map->nattr; n++) { + attr = &map->attr[n]; + if (map->class != attr->class) + continue; + ret = pmbus_read_coefficients(client, info, attr); + if (ret >= 0) + break; + } + if (ret < 0) { + dev_err(&client->dev, + "No coefficients found for sensor class %d\n", + map->class); + return -EINVAL; + } + } + + return 0; +} + +/* + * Identify chip parameters. + * This function is called for all chips. + */ +static int pmbus_identify_common(struct i2c_client *client, + struct pmbus_data *data, int page) +{ + int vout_mode = -1; + + if (pmbus_check_byte_register(client, page, PMBUS_VOUT_MODE)) + vout_mode = _pmbus_read_byte_data(client, page, + PMBUS_VOUT_MODE); + if (vout_mode >= 0 && vout_mode != 0xff) { + /* + * Not all chips support the VOUT_MODE command, + * so a failure to read it is not an error. + */ + switch (vout_mode >> 5) { + case 0: /* linear mode */ + if (data->info->format[PSC_VOLTAGE_OUT] != linear) + return -ENODEV; + + data->exponent[page] = ((s8)(vout_mode << 3)) >> 3; + break; + case 1: /* VID mode */ + if (data->info->format[PSC_VOLTAGE_OUT] != vid) + return -ENODEV; + break; + case 2: /* direct mode */ + if (data->info->format[PSC_VOLTAGE_OUT] != direct) + return -ENODEV; + break; + case 3: /* ieee 754 half precision */ + if (data->info->format[PSC_VOLTAGE_OUT] != ieee754) + return -ENODEV; + break; + default: + return -ENODEV; + } + } + + return 0; +} + +static int pmbus_read_status_byte(struct i2c_client *client, int page) +{ + return _pmbus_read_byte_data(client, page, PMBUS_STATUS_BYTE); +} + +static int pmbus_read_status_word(struct i2c_client *client, int page) +{ + return _pmbus_read_word_data(client, page, 0xff, PMBUS_STATUS_WORD); +} + +/* PEC attribute support */ + +static ssize_t pec_show(struct device *dev, struct device_attribute *dummy, + char *buf) +{ + struct i2c_client *client = to_i2c_client(dev); + + return sysfs_emit(buf, "%d\n", !!(client->flags & I2C_CLIENT_PEC)); +} + +static ssize_t pec_store(struct device *dev, struct device_attribute *dummy, + const char *buf, size_t count) +{ + struct i2c_client *client = to_i2c_client(dev); + bool enable; + int err; + + err = kstrtobool(buf, &enable); + if (err < 0) + return err; + + if (enable) + client->flags |= I2C_CLIENT_PEC; + else + client->flags &= ~I2C_CLIENT_PEC; + + return count; +} + +static DEVICE_ATTR_RW(pec); + +static void pmbus_remove_pec(void *dev) +{ + device_remove_file(dev, &dev_attr_pec); +} + +static void pmbus_init_wp(struct i2c_client *client, struct pmbus_data *data) +{ + int ret; + + switch (wp) { + case 0: + _pmbus_write_byte_data(client, -1, + PMBUS_WRITE_PROTECT, 0); + break; + + case 1: + _pmbus_write_byte_data(client, -1, + PMBUS_WRITE_PROTECT, PB_WP_VOUT); + break; + + case 2: + _pmbus_write_byte_data(client, -1, + PMBUS_WRITE_PROTECT, PB_WP_OP); + break; + + case 3: + _pmbus_write_byte_data(client, -1, + PMBUS_WRITE_PROTECT, PB_WP_ALL); + break; + + default: + /* Ignore the other values */ + break; + } + + ret = _pmbus_read_byte_data(client, -1, PMBUS_WRITE_PROTECT); + if (ret < 0) + return; + + switch (ret & PB_WP_ANY) { + case PB_WP_ALL: + data->flags |= PMBUS_OP_PROTECTED; + fallthrough; + case PB_WP_OP: + data->flags |= PMBUS_VOUT_PROTECTED; + fallthrough; + case PB_WP_VOUT: + data->flags |= PMBUS_WRITE_PROTECTED | PMBUS_SKIP_STATUS_CHECK; + break; + + default: + break; + } +} + +static int pmbus_init_common(struct i2c_client *client, struct pmbus_data *data, + struct pmbus_driver_info *info) +{ + struct device *dev = &client->dev; + int page, ret; + + /* + * Figure out if PEC is enabled before accessing any other register. + * Make sure PEC is disabled, will be enabled later if needed. + */ + client->flags &= ~I2C_CLIENT_PEC; + + /* Enable PEC if the controller and bus supports it */ + if (!(data->flags & PMBUS_NO_CAPABILITY)) { + pmbus_wait(client); + ret = i2c_smbus_read_byte_data(client, PMBUS_CAPABILITY); + pmbus_update_ts(client, 0); + + if (ret >= 0 && (ret & PB_CAPABILITY_ERROR_CHECK)) { + if (i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_PEC)) + client->flags |= I2C_CLIENT_PEC; + } + } + + /* + * Some PMBus chips don't support PMBUS_STATUS_WORD, so try + * to use PMBUS_STATUS_BYTE instead if that is the case. + * Bail out if both registers are not supported. + */ + data->read_status = pmbus_read_status_word; + pmbus_wait(client); + ret = i2c_smbus_read_word_data(client, PMBUS_STATUS_WORD); + pmbus_update_ts(client, 0); + + if (ret < 0 || ret == 0xffff) { + data->read_status = pmbus_read_status_byte; + pmbus_wait(client); + ret = i2c_smbus_read_byte_data(client, PMBUS_STATUS_BYTE); + pmbus_update_ts(client, 0); + + if (ret < 0 || ret == 0xff) { + dev_err(dev, "PMBus status register not found\n"); + return -ENODEV; + } + } else { + data->has_status_word = true; + } + + /* + * Check if the chip is write protected. If it is, we can not clear + * faults, and we should not try it. Also, in that case, writes into + * limit registers need to be disabled. + */ + if (!(data->flags & PMBUS_NO_WRITE_PROTECT)) + pmbus_init_wp(client, data); + + if (info->have_pmbus_revision) { + data->have_pmbus_revision = true; + data->revision = info->pmbus_revision; + } else { + ret = i2c_smbus_read_byte_data(client, PMBUS_REVISION); + if (ret >= 0) { + data->have_pmbus_revision = true; + data->revision = ret; + } + } + + if (data->info->pages) + pmbus_clear_faults(client); + else + pmbus_clear_fault_page(client, -1); + + if (info->identify) { + ret = (*info->identify)(client, info); + if (ret < 0) { + dev_err(dev, "Chip identification failed\n"); + return ret; + } + } + + if (info->pages <= 0 || info->pages > PMBUS_PAGES) { + dev_err(dev, "Bad number of PMBus pages: %d\n", info->pages); + return -ENODEV; + } + + for (page = 0; page < info->pages; page++) { + ret = pmbus_identify_common(client, data, page); + if (ret < 0) { + dev_err(dev, "Failed to identify chip capabilities\n"); + return ret; + } + } + + if (data->flags & PMBUS_USE_COEFFICIENTS_CMD) { + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_BLOCK_PROC_CALL)) + return -ENODEV; + + ret = pmbus_init_coefficients(client, info); + if (ret < 0) + return ret; + } + + if (client->flags & I2C_CLIENT_PEC) { + /* + * If I2C_CLIENT_PEC is set here, both the I2C adapter and the + * chip support PEC. Add 'pec' attribute to client device to let + * the user control it. + */ + ret = device_create_file(dev, &dev_attr_pec); + if (ret) + return ret; + ret = devm_add_action_or_reset(dev, pmbus_remove_pec, dev); + if (ret) + return ret; + } + + return 0; +} + +/* A PMBus status flag and the corresponding REGULATOR_ERROR_* and REGULATOR_EVENTS_* flag */ +struct pmbus_status_assoc { + int pflag, rflag, eflag; +}; + +/* PMBus->regulator bit mappings for a PMBus status register */ +struct pmbus_status_category { + int func; + int reg; + const struct pmbus_status_assoc *bits; /* zero-terminated */ +}; + +static const struct pmbus_status_category __maybe_unused pmbus_status_flag_map[] = { + { + .func = PMBUS_HAVE_STATUS_VOUT, + .reg = PMBUS_STATUS_VOUT, + .bits = (const struct pmbus_status_assoc[]) { + { PB_VOLTAGE_UV_WARNING, REGULATOR_ERROR_UNDER_VOLTAGE_WARN, + REGULATOR_EVENT_UNDER_VOLTAGE_WARN }, + { PB_VOLTAGE_UV_FAULT, REGULATOR_ERROR_UNDER_VOLTAGE, + REGULATOR_EVENT_UNDER_VOLTAGE }, + { PB_VOLTAGE_OV_WARNING, REGULATOR_ERROR_OVER_VOLTAGE_WARN, + REGULATOR_EVENT_OVER_VOLTAGE_WARN }, + { PB_VOLTAGE_OV_FAULT, REGULATOR_ERROR_REGULATION_OUT, + REGULATOR_EVENT_OVER_VOLTAGE_WARN }, + { }, + }, + }, { + .func = PMBUS_HAVE_STATUS_IOUT, + .reg = PMBUS_STATUS_IOUT, + .bits = (const struct pmbus_status_assoc[]) { + { PB_IOUT_OC_WARNING, REGULATOR_ERROR_OVER_CURRENT_WARN, + REGULATOR_EVENT_OVER_CURRENT_WARN }, + { PB_IOUT_OC_FAULT, REGULATOR_ERROR_OVER_CURRENT, + REGULATOR_EVENT_OVER_CURRENT }, + { PB_IOUT_OC_LV_FAULT, REGULATOR_ERROR_OVER_CURRENT, + REGULATOR_EVENT_OVER_CURRENT }, + { }, + }, + }, { + .func = PMBUS_HAVE_STATUS_TEMP, + .reg = PMBUS_STATUS_TEMPERATURE, + .bits = (const struct pmbus_status_assoc[]) { + { PB_TEMP_OT_WARNING, REGULATOR_ERROR_OVER_TEMP_WARN, + REGULATOR_EVENT_OVER_TEMP_WARN }, + { PB_TEMP_OT_FAULT, REGULATOR_ERROR_OVER_TEMP, + REGULATOR_EVENT_OVER_TEMP }, + { }, + }, + }, +}; + +static int _pmbus_is_enabled(struct i2c_client *client, u8 page) +{ + int ret; + + ret = _pmbus_read_byte_data(client, page, PMBUS_OPERATION); + + if (ret < 0) + return ret; + + return !!(ret & PB_OPERATION_CONTROL_ON); +} + +static int __maybe_unused pmbus_is_enabled(struct i2c_client *client, u8 page) +{ + guard(pmbus_lock)(client); + + return _pmbus_is_enabled(client, page); +} + +#define to_dev_attr(_dev_attr) \ + container_of(_dev_attr, struct device_attribute, attr) + +static void pmbus_notify(struct pmbus_data *data, int page, int reg, int flags) +{ + int i; + + for (i = 0; i < data->num_attributes; i++) { + struct device_attribute *da = to_dev_attr(data->group.attrs[i]); + struct sensor_device_attribute *attr = to_sensor_dev_attr(da); + int index = attr->index; + u16 smask, sreg; + u8 spage; + + if (index == -1) + continue; + + smask = pb_index_to_mask(index); + spage = pb_index_to_page(index); + sreg = pb_index_to_reg(index); + + if (reg == sreg && page == spage && (smask & flags)) { + dev_dbg(data->dev, "sysfs notify: %s", da->attr.name); + sysfs_notify(&data->hwmon_dev->kobj, NULL, + da->attr.name); + kobject_uevent(&data->hwmon_dev->kobj, KOBJ_CHANGE); + flags &= ~smask; + } + + if (!flags) + break; + } +} + +static int _pmbus_get_flags(struct i2c_client *client, u8 page, unsigned int *flags, + unsigned int *event, bool notify) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + int i, status; + const struct pmbus_status_category *cat; + const struct pmbus_status_assoc *bit; + int func = data->info->func[page]; + + *flags = 0; + *event = 0; + + for (i = 0; i < ARRAY_SIZE(pmbus_status_flag_map); i++) { + cat = &pmbus_status_flag_map[i]; + if (!(func & cat->func)) + continue; + + status = _pmbus_read_byte_data(client, page, cat->reg); + if (status < 0) + return status; + + for (bit = cat->bits; bit->pflag; bit++) + if (status & bit->pflag) { + *flags |= bit->rflag; + *event |= bit->eflag; + } + + if (notify && status) + pmbus_notify(data, page, cat->reg, status); + } + + /* + * Map what bits of STATUS_{WORD,BYTE} we can to REGULATOR_ERROR_* + * bits. Some of the other bits are tempting (especially for cases + * where we don't have the relevant PMBUS_HAVE_STATUS_* + * functionality), but there's an unfortunate ambiguity in that + * they're defined as indicating a fault *or* a warning, so we can't + * easily determine whether to report REGULATOR_ERROR_<foo> or + * REGULATOR_ERROR_<foo>_WARN. + */ + status = pmbus_get_status(client, page, PMBUS_STATUS_WORD); + if (status < 0) + return status; + + if (_pmbus_is_enabled(client, page)) { + if (status & PB_STATUS_OFF) { + *flags |= REGULATOR_ERROR_FAIL; + *event |= REGULATOR_EVENT_FAIL; + } + + if (status & PB_STATUS_POWER_GOOD_N) { + *flags |= REGULATOR_ERROR_REGULATION_OUT; + *event |= REGULATOR_EVENT_REGULATION_OUT; + } + } + /* + * Unlike most other status bits, PB_STATUS_{IOUT_OC,VOUT_OV} are + * defined strictly as fault indicators (not warnings). + */ + if (status & PB_STATUS_IOUT_OC) { + *flags |= REGULATOR_ERROR_OVER_CURRENT; + *event |= REGULATOR_EVENT_OVER_CURRENT; + } + if (status & PB_STATUS_VOUT_OV) { + *flags |= REGULATOR_ERROR_REGULATION_OUT; + *event |= REGULATOR_EVENT_FAIL; + } + + /* + * If we haven't discovered any thermal faults or warnings via + * PMBUS_STATUS_TEMPERATURE, map PB_STATUS_TEMPERATURE to a warning as + * a (conservative) best-effort interpretation. + */ + if (!(*flags & (REGULATOR_ERROR_OVER_TEMP | REGULATOR_ERROR_OVER_TEMP_WARN)) && + (status & PB_STATUS_TEMPERATURE)) { + *flags |= REGULATOR_ERROR_OVER_TEMP_WARN; + *event |= REGULATOR_EVENT_OVER_TEMP_WARN; + } + + return 0; +} + +static int __maybe_unused pmbus_get_flags(struct i2c_client *client, u8 page, unsigned int *flags, + unsigned int *event, bool notify) +{ + guard(pmbus_lock)(client); + + return _pmbus_get_flags(client, page, flags, event, notify); +} + +#if IS_ENABLED(CONFIG_REGULATOR) +static int pmbus_regulator_is_enabled(struct regulator_dev *rdev) +{ + struct device *dev = rdev_get_dev(rdev); + struct i2c_client *client = to_i2c_client(dev->parent); + + return pmbus_is_enabled(client, rdev_get_id(rdev)); +} + +static int _pmbus_regulator_on_off(struct regulator_dev *rdev, bool enable) +{ + struct device *dev = rdev_get_dev(rdev); + struct i2c_client *client = to_i2c_client(dev->parent); + u8 page = rdev_get_id(rdev); + + guard(pmbus_lock)(client); + + return pmbus_update_byte_data(client, page, PMBUS_OPERATION, + PB_OPERATION_CONTROL_ON, + enable ? PB_OPERATION_CONTROL_ON : 0); +} + +static int pmbus_regulator_enable(struct regulator_dev *rdev) +{ + return _pmbus_regulator_on_off(rdev, 1); +} + +static int pmbus_regulator_disable(struct regulator_dev *rdev) +{ + return _pmbus_regulator_on_off(rdev, 0); +} + +static int pmbus_regulator_get_error_flags(struct regulator_dev *rdev, unsigned int *flags) +{ + struct device *dev = rdev_get_dev(rdev); + struct i2c_client *client = to_i2c_client(dev->parent); + int event; + + return pmbus_get_flags(client, rdev_get_id(rdev), flags, &event, false); +} + +static int pmbus_regulator_get_status(struct regulator_dev *rdev) +{ + struct device *dev = rdev_get_dev(rdev); + struct i2c_client *client = to_i2c_client(dev->parent); + u8 page = rdev_get_id(rdev); + int status, ret; + int event; + + guard(pmbus_lock)(client); + + status = pmbus_get_status(client, page, PMBUS_STATUS_WORD); + if (status < 0) + return status; + + if (status & PB_STATUS_OFF) + return REGULATOR_STATUS_OFF; + + /* If regulator is ON & reports power good then return ON */ + if (!(status & PB_STATUS_POWER_GOOD_N)) + return REGULATOR_STATUS_ON; + + ret = _pmbus_get_flags(client, rdev_get_id(rdev), &status, &event, false); + if (ret) + return ret; + + if (status & (REGULATOR_ERROR_UNDER_VOLTAGE | REGULATOR_ERROR_OVER_CURRENT | + REGULATOR_ERROR_REGULATION_OUT | REGULATOR_ERROR_FAIL | REGULATOR_ERROR_OVER_TEMP)) + return REGULATOR_STATUS_ERROR; + + return REGULATOR_STATUS_UNDEFINED; +} + +static int pmbus_regulator_get_low_margin(struct i2c_client *client, int page) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + struct pmbus_sensor s = { + .page = page, + .class = PSC_VOLTAGE_OUT, + .convert = true, + .data = -1, + }; + + if (data->vout_low[page] < 0) { + if (pmbus_check_word_register(client, page, PMBUS_MFR_VOUT_MIN)) + s.data = _pmbus_read_word_data(client, page, 0xff, + PMBUS_MFR_VOUT_MIN); + if (s.data < 0) { + s.data = _pmbus_read_word_data(client, page, 0xff, + PMBUS_VOUT_MARGIN_LOW); + if (s.data < 0) + return s.data; + } + data->vout_low[page] = pmbus_reg2data(data, &s); + } + + return data->vout_low[page]; +} + +static int pmbus_regulator_get_high_margin(struct i2c_client *client, int page) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + struct pmbus_sensor s = { + .page = page, + .class = PSC_VOLTAGE_OUT, + .convert = true, + .data = -1, + }; + + if (data->vout_high[page] < 0) { + if (pmbus_check_word_register(client, page, PMBUS_MFR_VOUT_MAX)) + s.data = _pmbus_read_word_data(client, page, 0xff, + PMBUS_MFR_VOUT_MAX); + if (s.data < 0) { + s.data = _pmbus_read_word_data(client, page, 0xff, + PMBUS_VOUT_MARGIN_HIGH); + if (s.data < 0) + return s.data; + } + data->vout_high[page] = pmbus_reg2data(data, &s); + } + + return data->vout_high[page]; +} + +static int pmbus_regulator_get_voltage(struct regulator_dev *rdev) +{ + struct device *dev = rdev_get_dev(rdev); + struct i2c_client *client = to_i2c_client(dev->parent); + struct pmbus_data *data = i2c_get_clientdata(client); + struct pmbus_sensor s = { + .page = rdev_get_id(rdev), + .class = PSC_VOLTAGE_OUT, + .convert = true, + }; + int voltage; + + scoped_guard(pmbus_lock, client) { + s.data = _pmbus_read_word_data(client, s.page, 0xff, PMBUS_READ_VOUT); + if (s.data < 0) + return s.data; + voltage = (int)pmbus_reg2data(data, &s); + } + + return voltage * 1000; /* unit is uV */ +} + +static int pmbus_regulator_set_voltage(struct regulator_dev *rdev, int min_uv, + int max_uv, unsigned int *selector) +{ + struct device *dev = rdev_get_dev(rdev); + struct i2c_client *client = to_i2c_client(dev->parent); + struct pmbus_data *data = i2c_get_clientdata(client); + struct pmbus_sensor s = { + .page = rdev_get_id(rdev), + .class = PSC_VOLTAGE_OUT, + .convert = true, + .data = -1, + }; + int val = DIV_ROUND_CLOSEST(min_uv, 1000); /* convert to mV */ + int low, high; + + *selector = 0; + + guard(pmbus_lock)(client); + + low = pmbus_regulator_get_low_margin(client, s.page); + if (low < 0) + return low; + + high = pmbus_regulator_get_high_margin(client, s.page); + if (high < 0) + return high; + + /* Make sure we are within margins */ + if (low > val) + val = low; + if (high < val) + val = high; + + val = pmbus_data2reg(data, &s, val); + + return _pmbus_write_word_data(client, s.page, PMBUS_VOUT_COMMAND, (u16)val); +} + +static int pmbus_regulator_list_voltage(struct regulator_dev *rdev, + unsigned int selector) +{ + struct device *dev = rdev_get_dev(rdev); + struct i2c_client *client = to_i2c_client(dev->parent); + struct pmbus_data *data = i2c_get_clientdata(client); + int val, low, high; + + if (data->flags & PMBUS_VOUT_PROTECTED) + return 0; + + if (selector >= rdev->desc->n_voltages || + selector < rdev->desc->linear_min_sel) + return -EINVAL; + + selector -= rdev->desc->linear_min_sel; + val = DIV_ROUND_CLOSEST(rdev->desc->min_uV + + (rdev->desc->uV_step * selector), 1000); /* convert to mV */ + + guard(pmbus_lock)(client); + + low = pmbus_regulator_get_low_margin(client, rdev_get_id(rdev)); + if (low < 0) + return low; + + high = pmbus_regulator_get_high_margin(client, rdev_get_id(rdev)); + if (high < 0) + return high; + + if (val >= low && val <= high) + return val * 1000; /* unit is uV */ + + return 0; +} + +const struct regulator_ops pmbus_regulator_ops = { + .enable = pmbus_regulator_enable, + .disable = pmbus_regulator_disable, + .is_enabled = pmbus_regulator_is_enabled, + .get_error_flags = pmbus_regulator_get_error_flags, + .get_status = pmbus_regulator_get_status, + .get_voltage = pmbus_regulator_get_voltage, + .set_voltage = pmbus_regulator_set_voltage, + .list_voltage = pmbus_regulator_list_voltage, +}; +EXPORT_SYMBOL_NS_GPL(pmbus_regulator_ops, "PMBUS"); + +int pmbus_regulator_init_cb(struct regulator_dev *rdev, + struct regulator_config *config) +{ + struct pmbus_data *data = config->driver_data; + struct regulation_constraints *constraints = rdev->constraints; + + if (data->flags & PMBUS_OP_PROTECTED) + constraints->valid_ops_mask &= ~REGULATOR_CHANGE_STATUS; + + if (data->flags & PMBUS_VOUT_PROTECTED) + constraints->valid_ops_mask &= ~REGULATOR_CHANGE_VOLTAGE; + + return 0; +} +EXPORT_SYMBOL_NS_GPL(pmbus_regulator_init_cb, "PMBUS"); + +static void pmbus_regulator_notify_work_cancel(void *data) +{ + struct pmbus_data *pdata = data; + + cancel_work_sync(&pdata->regulator_notify_work); +} + +static void pmbus_regulator_notify_worker(struct work_struct *work) +{ + struct pmbus_data *data = + container_of(work, struct pmbus_data, regulator_notify_work); + int i, j; + + for (i = 0; i < data->info->pages; i++) { + unsigned int event; + + event = atomic_xchg(&data->regulator_events[i], 0); + if (!event) + continue; + + for (j = 0; j < data->info->num_regulators; j++) { + if (i != rdev_get_id(data->rdevs[j])) + continue; + while (event) { + unsigned int _event = BIT(__ffs(event)); + + regulator_notifier_call_chain(data->rdevs[j], + _event, NULL); + event &= ~_event; + } + break; + } + } +} + +static int pmbus_regulator_register(struct pmbus_data *data) +{ + struct device *dev = data->dev; + const struct pmbus_driver_info *info = data->info; + const struct pmbus_platform_data *pdata = dev_get_platdata(dev); + int i, ret; + + data->rdevs = devm_kzalloc(dev, sizeof(struct regulator_dev *) * info->num_regulators, + GFP_KERNEL); + if (!data->rdevs) + return -ENOMEM; + + for (i = 0; i < info->num_regulators; i++) { + struct regulator_config config = { }; + + config.dev = dev; + config.driver_data = data; + + if (pdata && pdata->reg_init_data) + config.init_data = &pdata->reg_init_data[i]; + + data->rdevs[i] = devm_regulator_register(dev, &info->reg_desc[i], + &config); + if (IS_ERR(data->rdevs[i])) + return dev_err_probe(dev, PTR_ERR(data->rdevs[i]), + "Failed to register %s regulator\n", + info->reg_desc[i].name); + } + + INIT_WORK(&data->regulator_notify_work, pmbus_regulator_notify_worker); + + ret = devm_add_action_or_reset(dev, pmbus_regulator_notify_work_cancel, data); + if (ret) + return ret; + + return 0; +} + +static void pmbus_regulator_notify(struct pmbus_data *data, int page, int event) +{ + atomic_or(event, &data->regulator_events[page]); + schedule_work(&data->regulator_notify_work); +} +#else +static int pmbus_regulator_register(struct pmbus_data *data) +{ + return 0; +} + +static void pmbus_regulator_notify(struct pmbus_data *data, int page, int event) +{ +} +#endif + +static int pmbus_write_smbalert_mask(struct i2c_client *client, u8 page, u8 reg, u8 val) +{ + int ret; + + guard(pmbus_lock)(client); + + ret = _pmbus_write_word_data(client, page, PMBUS_SMBALERT_MASK, reg | (val << 8)); + + /* + * Clear fault systematically in case writing PMBUS_SMBALERT_MASK + * is not supported by the chip. + */ + pmbus_clear_fault_page(client, page); + + return ret; +} + +void pmbus_check_and_notify_faults(struct i2c_client *client) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + int i, status, event; + + guard(pmbus_lock)(client); + + for (i = 0; i < data->info->pages; i++) { + _pmbus_get_flags(client, i, &status, &event, true); + + if (event) + pmbus_regulator_notify(data, i, event); + } + + pmbus_clear_faults(client); +} +EXPORT_SYMBOL_NS_GPL(pmbus_check_and_notify_faults, "PMBUS"); + +static irqreturn_t pmbus_fault_handler(int irq, void *pdata) +{ + struct pmbus_data *data = pdata; + struct i2c_client *client = to_i2c_client(data->dev); + + pmbus_check_and_notify_faults(client); + + return IRQ_HANDLED; +} + +static int pmbus_irq_setup(struct i2c_client *client, struct pmbus_data *data) +{ + struct device *dev = &client->dev; + const struct pmbus_status_category *cat; + const struct pmbus_status_assoc *bit; + int i, j, err, func; + u8 mask; + + static const u8 misc_status[] = {PMBUS_STATUS_CML, PMBUS_STATUS_OTHER, + PMBUS_STATUS_MFR_SPECIFIC, PMBUS_STATUS_FAN_12, + PMBUS_STATUS_FAN_34}; + + if (!client->irq) + return 0; + + for (i = 0; i < data->info->pages; i++) { + func = data->info->func[i]; + + for (j = 0; j < ARRAY_SIZE(pmbus_status_flag_map); j++) { + cat = &pmbus_status_flag_map[j]; + if (!(func & cat->func)) + continue; + mask = 0; + for (bit = cat->bits; bit->pflag; bit++) + mask |= bit->pflag; + + err = pmbus_write_smbalert_mask(client, i, cat->reg, ~mask); + if (err) + dev_dbg_once(dev, "Failed to set smbalert for reg 0x%02x\n", + cat->reg); + } + + for (j = 0; j < ARRAY_SIZE(misc_status); j++) + pmbus_write_smbalert_mask(client, i, misc_status[j], 0xff); + } + + /* Register notifiers */ + err = devm_request_threaded_irq(dev, client->irq, NULL, pmbus_fault_handler, + IRQF_ONESHOT, "pmbus-irq", data); + if (err) + return err; + + return 0; +} + +static struct dentry *pmbus_debugfs_dir; /* pmbus debugfs directory */ + +static int pmbus_debugfs_get(void *data, u64 *val) +{ + struct pmbus_debugfs_entry *entry = data; + struct i2c_client *client = entry->client; + int rc; + + guard(pmbus_lock)(client); + + rc = _pmbus_read_byte_data(client, entry->page, entry->reg); + if (rc < 0) + return rc; + + *val = rc; + + return 0; +} +DEFINE_DEBUGFS_ATTRIBUTE(pmbus_debugfs_ops, pmbus_debugfs_get, NULL, + "0x%02llx\n"); + +static int pmbus_debugfs_get_revision(void *data, u64 *val) +{ + struct pmbus_data *pdata = data; + + *val = pdata->revision; + + return 0; +} +DEFINE_DEBUGFS_ATTRIBUTE(pmbus_debugfs_revision_ops, pmbus_debugfs_get_revision, NULL, + "0x%02llx\n"); + +static int pmbus_debugfs_get_status(void *data, u64 *val) +{ + struct pmbus_debugfs_entry *entry = data; + struct i2c_client *client = entry->client; + struct pmbus_data *pdata = i2c_get_clientdata(client); + int rc; + + guard(pmbus_lock)(client); + + rc = pdata->read_status(client, entry->page); + if (rc < 0) + return rc; + + *val = rc; + + return 0; +} +DEFINE_DEBUGFS_ATTRIBUTE(pmbus_debugfs_ops_status, pmbus_debugfs_get_status, + NULL, "0x%04llx\n"); + +static ssize_t pmbus_debugfs_block_read(struct file *file, char __user *buf, + size_t count, loff_t *ppos) +{ + int rc; + struct pmbus_debugfs_entry *entry = file->private_data; + struct i2c_client *client = entry->client; + char data[I2C_SMBUS_BLOCK_MAX + 2] = { 0 }; + + scoped_guard(pmbus_lock, client) { + rc = pmbus_read_block_data(client, entry->page, entry->reg, data); + if (rc < 0) + return rc; + } + + /* Add newline at the end of a read data */ + data[rc] = '\n'; + + /* Include newline into the length */ + rc += 1; + + return simple_read_from_buffer(buf, count, ppos, data, rc); +} + +static const struct file_operations pmbus_debugfs_block_ops = { + .llseek = noop_llseek, + .read = pmbus_debugfs_block_read, + .write = NULL, + .open = simple_open, +}; + +static void pmbus_remove_symlink(void *symlink) +{ + debugfs_remove(symlink); +} + +struct pmbus_debugfs_data { + u8 reg; + u32 flag; + const char *name; +}; + +static const struct pmbus_debugfs_data pmbus_debugfs_block_data[] = { + { .reg = PMBUS_MFR_ID, .name = "mfr_id" }, + { .reg = PMBUS_MFR_MODEL, .name = "mfr_model" }, + { .reg = PMBUS_MFR_REVISION, .name = "mfr_revision" }, + { .reg = PMBUS_MFR_LOCATION, .name = "mfr_location" }, + { .reg = PMBUS_MFR_DATE, .name = "mfr_date" }, + { .reg = PMBUS_MFR_SERIAL, .name = "mfr_serial" }, +}; + +static const struct pmbus_debugfs_data pmbus_debugfs_status_data[] = { + { .reg = PMBUS_STATUS_VOUT, .flag = PMBUS_HAVE_STATUS_VOUT, .name = "status%d_vout" }, + { .reg = PMBUS_STATUS_IOUT, .flag = PMBUS_HAVE_STATUS_IOUT, .name = "status%d_iout" }, + { .reg = PMBUS_STATUS_INPUT, .flag = PMBUS_HAVE_STATUS_INPUT, .name = "status%d_input" }, + { .reg = PMBUS_STATUS_TEMPERATURE, .flag = PMBUS_HAVE_STATUS_TEMP, + .name = "status%d_temp" }, + { .reg = PMBUS_STATUS_FAN_12, .flag = PMBUS_HAVE_STATUS_FAN12, .name = "status%d_fan12" }, + { .reg = PMBUS_STATUS_FAN_34, .flag = PMBUS_HAVE_STATUS_FAN34, .name = "status%d_fan34" }, + { .reg = PMBUS_STATUS_CML, .name = "status%d_cml" }, + { .reg = PMBUS_STATUS_OTHER, .name = "status%d_other" }, + { .reg = PMBUS_STATUS_MFR_SPECIFIC, .name = "status%d_mfr" }, +}; + +static void pmbus_init_debugfs(struct i2c_client *client, + struct pmbus_data *data) +{ + struct dentry *symlink_d, *debugfs = client->debugfs; + struct pmbus_debugfs_entry *entries; + const char *pathname, *symlink; + char name[PMBUS_NAME_SIZE]; + int page, i, idx = 0; + + /* + * client->debugfs may be NULL or an ERR_PTR(). dentry_path_raw() + * does not check if its parameters are valid, so validate + * client->debugfs before using it. + */ + if (!pmbus_debugfs_dir || IS_ERR_OR_NULL(debugfs)) + return; + + /* + * Backwards compatibility: Create symlink from /pmbus/<hwmon_device> + * to i2c debugfs directory. + */ + pathname = dentry_path_raw(debugfs, name, sizeof(name)); + if (IS_ERR(pathname)) + return; + + /* + * The path returned by dentry_path_raw() starts with '/'. Prepend it + * with ".." to get the symlink relative to the pmbus root directory. + */ + symlink = kasprintf(GFP_KERNEL, "..%s", pathname); + if (!symlink) + return; + + symlink_d = debugfs_create_symlink(dev_name(data->hwmon_dev), + pmbus_debugfs_dir, symlink); + kfree(symlink); + + devm_add_action_or_reset(data->dev, pmbus_remove_symlink, symlink_d); + + /* + * Allocate the max possible entries we need. + * device specific: + * ARRAY_SIZE(pmbus_debugfs_block_data) + 2 + * page specific: + * ARRAY_SIZE(pmbus_debugfs_status_data) + 1 + */ + entries = devm_kcalloc(data->dev, + ARRAY_SIZE(pmbus_debugfs_block_data) + 2 + + data->info->pages * (ARRAY_SIZE(pmbus_debugfs_status_data) + 1), + sizeof(*entries), GFP_KERNEL); + if (!entries) + return; + + guard(pmbus_lock)(client); + + /* + * Add device-specific entries. + * Please note that the PMBUS standard allows all registers to be + * page-specific. + * To reduce the number of debugfs entries for devices with many pages + * assume that values of the following registers are the same for all + * pages and report values only for page 0. + */ + if (!(data->flags & PMBUS_NO_CAPABILITY) && + pmbus_check_byte_register(client, 0, PMBUS_CAPABILITY)) { + entries[idx].client = client; + entries[idx].page = 0; + entries[idx].reg = PMBUS_CAPABILITY; + debugfs_create_file("capability", 0444, debugfs, + &entries[idx++], + &pmbus_debugfs_ops); + } + if (data->have_pmbus_revision) + debugfs_create_file("pmbus_revision", 0444, debugfs, data, + &pmbus_debugfs_revision_ops); + + for (i = 0; i < ARRAY_SIZE(pmbus_debugfs_block_data); i++) { + const struct pmbus_debugfs_data *d = &pmbus_debugfs_block_data[i]; + + if (pmbus_check_block_register(client, 0, d->reg)) { + entries[idx].client = client; + entries[idx].page = 0; + entries[idx].reg = d->reg; + debugfs_create_file(d->name, 0444, debugfs, + &entries[idx++], + &pmbus_debugfs_block_ops); + } + } + + /* Add page specific entries */ + for (page = 0; page < data->info->pages; ++page) { + /* Check accessibility of status register if it's not page 0 */ + if (!page || pmbus_check_status_register(client, page)) { + /* No need to set reg as we have special read op. */ + entries[idx].client = client; + entries[idx].page = page; + scnprintf(name, PMBUS_NAME_SIZE, "status%d", page); + debugfs_create_file(name, 0444, debugfs, + &entries[idx++], + &pmbus_debugfs_ops_status); + } + + for (i = 0; i < ARRAY_SIZE(pmbus_debugfs_status_data); i++) { + const struct pmbus_debugfs_data *d = + &pmbus_debugfs_status_data[i]; + + if ((data->info->func[page] & d->flag) || + (!d->flag && pmbus_check_byte_register(client, page, d->reg))) { + entries[idx].client = client; + entries[idx].page = page; + entries[idx].reg = d->reg; + scnprintf(name, PMBUS_NAME_SIZE, d->name, page); + debugfs_create_file(name, 0444, debugfs, + &entries[idx++], + &pmbus_debugfs_ops); + } + } + } +} + +int pmbus_do_probe(struct i2c_client *client, struct pmbus_driver_info *info) +{ + struct device *dev = &client->dev; + const struct pmbus_platform_data *pdata = dev_get_platdata(dev); + struct pmbus_data *data; + size_t groups_num = 0; + int ret; + int i; + char *name; + + if (!info) + return -ENODEV; + + if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WRITE_BYTE + | I2C_FUNC_SMBUS_BYTE_DATA + | I2C_FUNC_SMBUS_WORD_DATA)) + return -ENODEV; + + data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + if (info->groups) + while (info->groups[groups_num]) + groups_num++; + + data->groups = devm_kcalloc(dev, groups_num + 2, sizeof(void *), + GFP_KERNEL); + if (!data->groups) + return -ENOMEM; + + i2c_set_clientdata(client, data); + mutex_init(&data->update_lock); + data->dev = dev; + + if (pdata) + data->flags = pdata->flags; + data->info = info; + data->currpage = -1; + data->currphase = -1; + + for (i = 0; i < ARRAY_SIZE(data->vout_low); i++) { + data->vout_low[i] = -1; + data->vout_high[i] = -1; + } + + ret = pmbus_init_common(client, data, info); + if (ret < 0) + return ret; + + ret = pmbus_find_attributes(client, data); + if (ret) + return ret; + + /* + * If there are no attributes, something is wrong. + * Bail out instead of trying to register nothing. + */ + if (!data->num_attributes) { + dev_err(dev, "No attributes found\n"); + return -ENODEV; + } + + name = devm_kstrdup(dev, client->name, GFP_KERNEL); + if (!name) + return -ENOMEM; + strreplace(name, '-', '_'); + + data->groups[0] = &data->group; + memcpy(data->groups + 1, info->groups, sizeof(void *) * groups_num); + data->hwmon_dev = devm_hwmon_device_register_with_groups(dev, name, + data, data->groups); + if (IS_ERR(data->hwmon_dev)) { + dev_err(dev, "Failed to register hwmon device\n"); + return PTR_ERR(data->hwmon_dev); + } + + ret = pmbus_regulator_register(data); + if (ret) + return ret; + + ret = pmbus_irq_setup(client, data); + if (ret) + return ret; + + pmbus_init_debugfs(client, data); + + return 0; +} +EXPORT_SYMBOL_NS_GPL(pmbus_do_probe, "PMBUS"); + +struct dentry *pmbus_get_debugfs_dir(struct i2c_client *client) +{ + /* + * client->debugfs may be an ERR_PTR(). Returning that to + * the calling code would potentially require additional + * complexity in the calling code and otherwise add no + * value. Return NULL in that case. + */ + if (IS_ERR_OR_NULL(client->debugfs)) + return NULL; + return client->debugfs; +} +EXPORT_SYMBOL_NS_GPL(pmbus_get_debugfs_dir, "PMBUS"); + +void pmbus_lock(struct i2c_client *client) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + + mutex_lock(&data->update_lock); +} +EXPORT_SYMBOL_NS_GPL(pmbus_lock, "PMBUS"); + +int pmbus_lock_interruptible(struct i2c_client *client) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + + return mutex_lock_interruptible(&data->update_lock); +} +EXPORT_SYMBOL_NS_GPL(pmbus_lock_interruptible, "PMBUS"); + +void pmbus_unlock(struct i2c_client *client) +{ + struct pmbus_data *data = i2c_get_clientdata(client); + + mutex_unlock(&data->update_lock); +} +EXPORT_SYMBOL_NS_GPL(pmbus_unlock, "PMBUS"); + +static int __init pmbus_core_init(void) +{ + pmbus_debugfs_dir = debugfs_create_dir("pmbus", NULL); + if (IS_ERR(pmbus_debugfs_dir)) + pmbus_debugfs_dir = NULL; + + return 0; +} + +static void __exit pmbus_core_exit(void) +{ + debugfs_remove_recursive(pmbus_debugfs_dir); +} + +module_init(pmbus_core_init); +module_exit(pmbus_core_exit); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus core driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/hwmon/pmbus/pxe1610.c b/drivers/hwmon/pmbus/pxe1610.c new file mode 100644 index 000000000..6ba2b3e0e --- /dev/null +++ b/drivers/hwmon/pmbus/pxe1610.c @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Infineon PXE1610 + * + * Copyright (c) 2019 Facebook Inc + * + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define PXE1610_NUM_PAGES 3 + +/* Identify chip parameters. */ +static int pxe1610_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + int i; + + for (i = 0; i < PXE1610_NUM_PAGES; i++) { + if (pmbus_check_byte_register(client, i, PMBUS_VOUT_MODE)) { + u8 vout_mode; + int ret; + + /* Read the register with VOUT scaling value.*/ + ret = pmbus_read_byte_data(client, i, PMBUS_VOUT_MODE); + if (ret < 0) + return ret; + + vout_mode = ret & GENMASK(4, 0); + + switch (vout_mode) { + case 1: + info->vrm_version[i] = vr12; + break; + case 2: + info->vrm_version[i] = vr13; + break; + default: + /* + * If prior pages are available limit operation + * to them + */ + if (i != 0) { + info->pages = i; + return 0; + } + + return -ENODEV; + } + } + } + + return 0; +} + +static struct pmbus_driver_info pxe1610_info = { + .pages = PXE1610_NUM_PAGES, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = vid, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_POWER] = linear, + .func[0] = PMBUS_HAVE_VIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_IIN + | PMBUS_HAVE_IOUT | PMBUS_HAVE_PIN + | PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, + .func[1] = PMBUS_HAVE_VIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_IIN + | PMBUS_HAVE_IOUT | PMBUS_HAVE_PIN + | PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, + .func[2] = PMBUS_HAVE_VIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_IIN + | PMBUS_HAVE_IOUT | PMBUS_HAVE_PIN + | PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, + .identify = pxe1610_identify, +}; + +static int pxe1610_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + + if (!i2c_check_functionality( + client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA + | I2C_FUNC_SMBUS_READ_WORD_DATA + | I2C_FUNC_SMBUS_READ_BLOCK_DATA)) + return -ENODEV; + + /* + * By default this device doesn't boot to page 0, so set page 0 + * to access all pmbus registers. + */ + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "Failed to set page 0\n"); + + /* Read Manufacturer id */ + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, buf); + if (ret < 0) { + dev_err(&client->dev, "Failed to read PMBUS_MFR_ID\n"); + return ret; + } + if (ret != 2 || strncmp(buf, "XP", 2)) { + dev_err(&client->dev, "MFR_ID unrecognized\n"); + return -ENODEV; + } + + info = devm_kmemdup(&client->dev, &pxe1610_info, + sizeof(struct pmbus_driver_info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id pxe1610_id[] = { + { .name = "pxe1610" }, + { .name = "pxe1110" }, + { .name = "pxm1310" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, pxe1610_id); + +static struct i2c_driver pxe1610_driver = { + .driver = { + .name = "pxe1610", + }, + .probe = pxe1610_probe, + .id_table = pxe1610_id, +}; + +module_i2c_driver(pxe1610_driver); + +MODULE_AUTHOR("Vijay Khemka <vijaykhemka@fb.com>"); +MODULE_DESCRIPTION("PMBus driver for Infineon PXE1610, PXE1110 and PXM1310"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/q54sj108a2.c b/drivers/hwmon/pmbus/q54sj108a2.c new file mode 100644 index 000000000..ecac405d7 --- /dev/null +++ b/drivers/hwmon/pmbus/q54sj108a2.c @@ -0,0 +1,435 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for Delta modules, Q54SJ108A2 series 1/4 Brick DC/DC + * Regulated Power Module + * + * Copyright 2020 Delta LLC. + */ + +#include <linux/debugfs.h> +#include <linux/hex.h> +#include <linux/i2c.h> +#include <linux/kstrtox.h> +#include <linux/module.h> +#include <linux/of.h> +#include "pmbus.h" + +#define STORE_DEFAULT_ALL 0x11 +#define ERASE_BLACKBOX_DATA 0xD1 +#define READ_HISTORY_EVENT_NUMBER 0xD2 +#define READ_HISTORY_EVENTS 0xE0 +#define SET_HISTORY_EVENT_OFFSET 0xE1 +#define PMBUS_FLASH_KEY_WRITE 0xEC + +enum chips { + q54sj108a2 +}; + +enum { + Q54SJ108A2_DEBUGFS_OPERATION = 0, + Q54SJ108A2_DEBUGFS_CLEARFAULT, + Q54SJ108A2_DEBUGFS_WRITEPROTECT, + Q54SJ108A2_DEBUGFS_STOREDEFAULT, + Q54SJ108A2_DEBUGFS_VOOV_RESPONSE, + Q54SJ108A2_DEBUGFS_IOOC_RESPONSE, + Q54SJ108A2_DEBUGFS_PMBUS_VERSION, + Q54SJ108A2_DEBUGFS_MFR_ID, + Q54SJ108A2_DEBUGFS_MFR_MODEL, + Q54SJ108A2_DEBUGFS_MFR_REVISION, + Q54SJ108A2_DEBUGFS_MFR_LOCATION, + Q54SJ108A2_DEBUGFS_BLACKBOX_ERASE, + Q54SJ108A2_DEBUGFS_BLACKBOX_READ_OFFSET, + Q54SJ108A2_DEBUGFS_BLACKBOX_SET_OFFSET, + Q54SJ108A2_DEBUGFS_BLACKBOX_READ, + Q54SJ108A2_DEBUGFS_FLASH_KEY, + Q54SJ108A2_DEBUGFS_NUM_ENTRIES +}; + +struct q54sj108a2_data { + enum chips chip; + struct i2c_client *client; + + int debugfs_entries[Q54SJ108A2_DEBUGFS_NUM_ENTRIES]; +}; + +#define to_psu(x, y) container_of((x), struct q54sj108a2_data, debugfs_entries[(y)]) + +static struct pmbus_driver_info q54sj108a2_info[] = { + [q54sj108a2] = { + .pages = 1, + + /* Source : Delta Q54SJ108A2 */ + .format[PSC_TEMPERATURE] = linear, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + + .func[0] = PMBUS_HAVE_VIN | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_STATUS_INPUT, + }, +}; + +static ssize_t q54sj108a2_debugfs_read(struct file *file, char __user *buf, + size_t count, loff_t *ppos) +{ + int rc; + int *idxp = file->private_data; + int idx = *idxp; + struct q54sj108a2_data *psu = to_psu(idxp, idx); + char data[I2C_SMBUS_BLOCK_MAX + 2] = { 0 }; + char data_char[I2C_SMBUS_BLOCK_MAX * 2 + 2] = { 0 }; + char *out = data; + char *res; + + switch (idx) { + case Q54SJ108A2_DEBUGFS_OPERATION: + rc = i2c_smbus_read_byte_data(psu->client, PMBUS_OPERATION); + if (rc < 0) + return rc; + + rc = snprintf(data, 3, "%02x", rc); + break; + case Q54SJ108A2_DEBUGFS_WRITEPROTECT: + rc = i2c_smbus_read_byte_data(psu->client, PMBUS_WRITE_PROTECT); + if (rc < 0) + return rc; + + rc = snprintf(data, 3, "%02x", rc); + break; + case Q54SJ108A2_DEBUGFS_VOOV_RESPONSE: + rc = i2c_smbus_read_byte_data(psu->client, PMBUS_VOUT_OV_FAULT_RESPONSE); + if (rc < 0) + return rc; + + rc = snprintf(data, 3, "%02x", rc); + break; + case Q54SJ108A2_DEBUGFS_IOOC_RESPONSE: + rc = i2c_smbus_read_byte_data(psu->client, PMBUS_IOUT_OC_FAULT_RESPONSE); + if (rc < 0) + return rc; + + rc = snprintf(data, 3, "%02x", rc); + break; + case Q54SJ108A2_DEBUGFS_PMBUS_VERSION: + rc = i2c_smbus_read_byte_data(psu->client, PMBUS_REVISION); + if (rc < 0) + return rc; + + rc = snprintf(data, 3, "%02x", rc); + break; + case Q54SJ108A2_DEBUGFS_MFR_ID: + rc = i2c_smbus_read_block_data(psu->client, PMBUS_MFR_ID, data); + if (rc < 0) + return rc; + break; + case Q54SJ108A2_DEBUGFS_MFR_MODEL: + rc = i2c_smbus_read_block_data(psu->client, PMBUS_MFR_MODEL, data); + if (rc < 0) + return rc; + break; + case Q54SJ108A2_DEBUGFS_MFR_REVISION: + rc = i2c_smbus_read_block_data(psu->client, PMBUS_MFR_REVISION, data); + if (rc < 0) + return rc; + break; + case Q54SJ108A2_DEBUGFS_MFR_LOCATION: + rc = i2c_smbus_read_block_data(psu->client, PMBUS_MFR_LOCATION, data); + if (rc < 0) + return rc; + break; + case Q54SJ108A2_DEBUGFS_BLACKBOX_READ_OFFSET: + rc = i2c_smbus_read_byte_data(psu->client, READ_HISTORY_EVENT_NUMBER); + if (rc < 0) + return rc; + + rc = snprintf(data, 3, "%02x", rc); + break; + case Q54SJ108A2_DEBUGFS_BLACKBOX_READ: + rc = i2c_smbus_read_block_data(psu->client, READ_HISTORY_EVENTS, data); + if (rc < 0) + return rc; + + res = bin2hex(data_char, data, rc); + rc = res - data_char; + out = data_char; + break; + case Q54SJ108A2_DEBUGFS_FLASH_KEY: + rc = i2c_smbus_read_block_data(psu->client, PMBUS_FLASH_KEY_WRITE, data); + if (rc < 0) + return rc; + + res = bin2hex(data_char, data, rc); + rc = res - data_char; + out = data_char; + break; + default: + return -EINVAL; + } + + out[rc] = '\n'; + rc += 2; + + return simple_read_from_buffer(buf, count, ppos, out, rc); +} + +static ssize_t q54sj108a2_debugfs_write(struct file *file, const char __user *buf, + size_t count, loff_t *ppos) +{ + u8 flash_key[4]; + u8 dst_data; + ssize_t rc; + int *idxp = file->private_data; + int idx = *idxp; + struct q54sj108a2_data *psu = to_psu(idxp, idx); + + rc = i2c_smbus_write_byte_data(psu->client, PMBUS_WRITE_PROTECT, 0); + if (rc) + return rc; + + switch (idx) { + case Q54SJ108A2_DEBUGFS_OPERATION: + rc = kstrtou8_from_user(buf, count, 0, &dst_data); + if (rc < 0) + return rc; + + rc = i2c_smbus_write_byte_data(psu->client, PMBUS_OPERATION, dst_data); + if (rc < 0) + return rc; + + break; + case Q54SJ108A2_DEBUGFS_CLEARFAULT: + rc = i2c_smbus_write_byte(psu->client, PMBUS_CLEAR_FAULTS); + if (rc < 0) + return rc; + + break; + case Q54SJ108A2_DEBUGFS_STOREDEFAULT: + flash_key[0] = 0x7E; + flash_key[1] = 0x15; + flash_key[2] = 0xDC; + flash_key[3] = 0x42; + rc = i2c_smbus_write_block_data(psu->client, PMBUS_FLASH_KEY_WRITE, 4, flash_key); + if (rc < 0) + return rc; + + rc = i2c_smbus_write_byte(psu->client, STORE_DEFAULT_ALL); + if (rc < 0) + return rc; + + break; + case Q54SJ108A2_DEBUGFS_VOOV_RESPONSE: + rc = kstrtou8_from_user(buf, count, 0, &dst_data); + if (rc < 0) + return rc; + + rc = i2c_smbus_write_byte_data(psu->client, PMBUS_VOUT_OV_FAULT_RESPONSE, dst_data); + if (rc < 0) + return rc; + + break; + case Q54SJ108A2_DEBUGFS_IOOC_RESPONSE: + rc = kstrtou8_from_user(buf, count, 0, &dst_data); + if (rc < 0) + return rc; + + rc = i2c_smbus_write_byte_data(psu->client, PMBUS_IOUT_OC_FAULT_RESPONSE, dst_data); + if (rc < 0) + return rc; + + break; + case Q54SJ108A2_DEBUGFS_BLACKBOX_ERASE: + rc = i2c_smbus_write_byte(psu->client, ERASE_BLACKBOX_DATA); + if (rc < 0) + return rc; + + break; + case Q54SJ108A2_DEBUGFS_BLACKBOX_SET_OFFSET: + rc = kstrtou8_from_user(buf, count, 0, &dst_data); + if (rc < 0) + return rc; + + rc = i2c_smbus_write_byte_data(psu->client, SET_HISTORY_EVENT_OFFSET, dst_data); + if (rc < 0) + return rc; + + break; + default: + return -EINVAL; + } + + return count; +} + +static const struct file_operations q54sj108a2_fops = { + .llseek = noop_llseek, + .read = q54sj108a2_debugfs_read, + .write = q54sj108a2_debugfs_write, + .open = simple_open, +}; + +static const struct i2c_device_id q54sj108a2_id[] = { + { .name = "q54sj108a2", .driver_data = q54sj108a2 }, + { .name = "q54sn120a1", .driver_data = q54sj108a2 }, + { .name = "q54sw120a7", .driver_data = q54sj108a2 }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, q54sj108a2_id); + +static int q54sj108a2_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + u8 buf[I2C_SMBUS_BLOCK_MAX + 1]; + const struct i2c_device_id *mid; + enum chips chip_id; + int ret, i; + struct dentry *debugfs; + struct dentry *q54sj108a2_dir; + struct q54sj108a2_data *psu; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_BYTE_DATA | + I2C_FUNC_SMBUS_WORD_DATA | + I2C_FUNC_SMBUS_BLOCK_DATA)) + return -ENODEV; + + chip_id = (enum chips)(uintptr_t)i2c_get_match_data(client); + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_ID, buf); + if (ret < 0) { + dev_err(&client->dev, "Failed to read Manufacturer ID\n"); + return ret; + } + if (ret != 6 || strncmp(buf, "DELTA", 5)) { + buf[ret] = '\0'; + dev_err(dev, "Unsupported Manufacturer ID '%s'\n", buf); + return -ENODEV; + } + + /* + * The chips support reading PMBUS_MFR_MODEL. + */ + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, buf); + if (ret < 0) { + dev_err(dev, "Failed to read Manufacturer Model\n"); + return ret; + } + buf[ret] = '\0'; + for (mid = q54sj108a2_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, buf, strlen(mid->name))) + break; + } + if (!mid->name[0]) { + dev_err(dev, "Unsupported Manufacturer Model '%s'\n", buf); + return -ENODEV; + } + + ret = i2c_smbus_read_block_data(client, PMBUS_MFR_REVISION, buf); + if (ret < 0) { + dev_err(dev, "Failed to read Manufacturer Revision\n"); + return ret; + } + /* + * accept manufacturer revision with optional NUL byte + */ + if (!(ret == 4 || ret == 5) || buf[0] != 'S') { + buf[ret] = '\0'; + dev_err(dev, "Unsupported Manufacturer Revision '%s'\n", buf); + return -ENODEV; + } + + ret = pmbus_do_probe(client, &q54sj108a2_info[chip_id]); + if (ret) + return ret; + + psu = devm_kzalloc(&client->dev, sizeof(*psu), GFP_KERNEL); + if (!psu) + return 0; + + psu->client = client; + + debugfs = pmbus_get_debugfs_dir(client); + + q54sj108a2_dir = debugfs_create_dir(client->name, debugfs); + + for (i = 0; i < Q54SJ108A2_DEBUGFS_NUM_ENTRIES; ++i) + psu->debugfs_entries[i] = i; + + debugfs_create_file("operation", 0644, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_OPERATION], + &q54sj108a2_fops); + debugfs_create_file("clear_fault", 0200, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_CLEARFAULT], + &q54sj108a2_fops); + debugfs_create_file("write_protect", 0444, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_WRITEPROTECT], + &q54sj108a2_fops); + debugfs_create_file("store_default", 0200, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_STOREDEFAULT], + &q54sj108a2_fops); + debugfs_create_file("vo_ov_response", 0644, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_VOOV_RESPONSE], + &q54sj108a2_fops); + debugfs_create_file("io_oc_response", 0644, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_IOOC_RESPONSE], + &q54sj108a2_fops); + debugfs_create_file("pmbus_revision", 0444, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_PMBUS_VERSION], + &q54sj108a2_fops); + debugfs_create_file("mfr_id", 0444, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_MFR_ID], + &q54sj108a2_fops); + debugfs_create_file("mfr_model", 0444, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_MFR_MODEL], + &q54sj108a2_fops); + debugfs_create_file("mfr_revision", 0444, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_MFR_REVISION], + &q54sj108a2_fops); + debugfs_create_file("mfr_location", 0444, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_MFR_LOCATION], + &q54sj108a2_fops); + debugfs_create_file("blackbox_erase", 0200, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_BLACKBOX_ERASE], + &q54sj108a2_fops); + debugfs_create_file("blackbox_read_offset", 0444, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_BLACKBOX_READ_OFFSET], + &q54sj108a2_fops); + debugfs_create_file("blackbox_set_offset", 0200, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_BLACKBOX_SET_OFFSET], + &q54sj108a2_fops); + debugfs_create_file("blackbox_read", 0444, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_BLACKBOX_READ], + &q54sj108a2_fops); + debugfs_create_file("flash_key", 0444, q54sj108a2_dir, + &psu->debugfs_entries[Q54SJ108A2_DEBUGFS_FLASH_KEY], + &q54sj108a2_fops); + + return 0; +} + +static const struct of_device_id q54sj108a2_of_match[] = { + { .compatible = "delta,q54sj108a2", .data = (void *)q54sj108a2 }, + { .compatible = "delta,q54sn120a1", .data = (void *)q54sj108a2 }, + { .compatible = "delta,q54sw120a7", .data = (void *)q54sj108a2 }, + { }, +}; + +MODULE_DEVICE_TABLE(of, q54sj108a2_of_match); + +static struct i2c_driver q54sj108a2_driver = { + .driver = { + .name = "q54sj108a2", + .of_match_table = q54sj108a2_of_match, + }, + .probe = q54sj108a2_probe, + .id_table = q54sj108a2_id, +}; + +module_i2c_driver(q54sj108a2_driver); + +MODULE_AUTHOR("Xiao.Ma <xiao.mx.ma@deltaww.com>"); +MODULE_DESCRIPTION("PMBus driver for Delta Q54SJ108A2 series modules"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/sq24860.c b/drivers/hwmon/pmbus/sq24860.c new file mode 100644 index 000000000..30202a4b3 --- /dev/null +++ b/drivers/hwmon/pmbus/sq24860.c @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Author: Ziming Zhu <ziming.zhu@silergycorp.com> + */ + +#include <linux/bitfield.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/math64.h> + +#include "pmbus.h" + +#define SQ24860_IIN_CAL_GAIN 0x38 +#define SQ24860_READ_VAUX 0xd0 +#define SQ24860_READ_VIN_MIN 0xd1 +#define SQ24860_READ_VIN_PEAK 0xd2 +#define SQ24860_READ_IIN_PEAK 0xd4 +#define SQ24860_READ_PIN_PEAK 0xd5 +#define SQ24860_READ_TEMP_AVG 0xd6 +#define SQ24860_READ_TEMP_PEAK 0xd7 +#define SQ24860_READ_VOUT_MIN 0xda +#define SQ24860_READ_VIN_AVG 0xdc +#define SQ24860_READ_VOUT_AVG 0xdd +#define SQ24860_READ_IIN_AVG 0xde +#define SQ24860_READ_PIN_AVG 0xdf +#define SQ24860_VIREF 0xe0 +#define SQ24860_PK_MIN_AVG 0xea +#define PK_MIN_AVG_RST_PEAK BIT(7) +#define PK_MIN_AVG_RST_AVG BIT(6) +#define PK_MIN_AVG_RST_MIN BIT(5) +#define PK_MIN_AVG_AVG_CNT GENMASK(2, 0) +#define SQ24860_MFR_WRITE_PROTECT 0xf8 +#define SQ24860_UNLOCKED BIT(7) + +#define SQ24860_8B_SHIFT 2 +#define SQ24860_IIN_OCF_NUM 1000000 +#define SQ24860_IIN_OCF_DIV 129278 +#define SQ24860_IIN_OCF_OFF 165 + +#define PK_MIN_AVG_RST_MASK (PK_MIN_AVG_RST_PEAK | \ + PK_MIN_AVG_RST_AVG | \ + PK_MIN_AVG_RST_MIN) +#define SQ24860_MAX_SAMPLES BIT(FIELD_MAX(PK_MIN_AVG_AVG_CNT)) +/* + * Arbitrary default Rimon value: 1.6kOhm + */ +#define SQ24860_DEFAULT_RIMON 1600000000 +#define SQ24860_GIMON 18180 + +#define SQ24860_VAUX_DIV 20 + +static int sq24860_write_iin_cal_gain(struct i2c_client *client, u32 rimon) +{ + u64 temp = 6400ULL * 1000000000ULL * 1000ULL; + u64 denom; + u64 word; + + if (!rimon) + return -EINVAL; + + denom = (u64)rimon * SQ24860_GIMON; + word = div64_u64(temp, denom); + if (!word || word > U16_MAX) + return -EINVAL; + + return i2c_smbus_write_word_data(client, SQ24860_IIN_CAL_GAIN, + (u16)word); +} + +static int sq24860_mfr_write_protect_set(struct i2c_client *client, + u8 protect) +{ + u8 val; + + switch (protect) { + case 0: + val = 0xa2; + break; + case PB_WP_ALL: + val = 0x0; + break; + default: + return -EINVAL; + } + + return pmbus_write_byte_data(client, -1, SQ24860_MFR_WRITE_PROTECT, + val); +} + +static int sq24860_mfr_write_protect_get(struct i2c_client *client) +{ + int ret = pmbus_read_byte_data(client, -1, SQ24860_MFR_WRITE_PROTECT); + + if (ret < 0) + return ret; + + return (ret & SQ24860_UNLOCKED) ? 0 : PB_WP_ALL; +} + +static int sq24860_read_word_data(struct i2c_client *client, + int page, int phase, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VIN_MAX: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_VIN_PEAK); + break; + + case PMBUS_VIRT_READ_VIN_MIN: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_VIN_MIN); + break; + + case PMBUS_VIRT_READ_VIN_AVG: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_VIN_AVG); + break; + + case PMBUS_VIRT_READ_VOUT_MIN: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_VOUT_MIN); + break; + + case PMBUS_VIRT_READ_VOUT_AVG: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_VOUT_AVG); + break; + + case PMBUS_VIRT_READ_IIN_AVG: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_IIN_AVG); + break; + + case PMBUS_VIRT_READ_IIN_MAX: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_IIN_PEAK); + break; + + case PMBUS_VIRT_READ_TEMP_AVG: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_TEMP_AVG); + break; + + case PMBUS_VIRT_READ_TEMP_MAX: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_TEMP_PEAK); + break; + + case PMBUS_VIRT_READ_PIN_AVG: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_PIN_AVG); + break; + + case PMBUS_VIRT_READ_PIN_MAX: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_PIN_PEAK); + break; + + case PMBUS_VIRT_READ_VMON: + ret = pmbus_read_word_data(client, page, phase, + SQ24860_READ_VAUX); + if (ret < 0) + break; + ret = DIV_ROUND_CLOSEST(ret, SQ24860_VAUX_DIV); + break; + + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_OT_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + /* + * These registers provide an 8 bits value instead of a + * 10bits one. Just shifting twice the register value is + * enough to make the sensor type conversion work, even + * if the datasheet provides different m, b and R for + * those. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + break; + ret <<= SQ24860_8B_SHIFT; + break; + + case PMBUS_IIN_OC_FAULT_LIMIT: + /* + * VIREF directly sets the over-current limit at which the eFuse + * will turn the FET off and trigger a fault. Expose it through + * this generic property instead of a manufacturer specific one. + */ + ret = pmbus_read_byte_data(client, page, SQ24860_VIREF); + if (ret < 0) + break; + ret = DIV_ROUND_CLOSEST(ret * SQ24860_IIN_OCF_NUM, + SQ24860_IIN_OCF_DIV); + ret += SQ24860_IIN_OCF_OFF; + break; + + case PMBUS_VIRT_SAMPLES: + ret = pmbus_read_byte_data(client, page, SQ24860_PK_MIN_AVG); + if (ret < 0) + break; + ret = BIT(FIELD_GET(PK_MIN_AVG_AVG_CNT, ret)); + break; + + case PMBUS_VIRT_RESET_TEMP_HISTORY: + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_IIN_HISTORY: + case PMBUS_VIRT_RESET_PIN_HISTORY: + case PMBUS_VIRT_RESET_VOUT_HISTORY: + ret = 0; + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int sq24860_write_word_data(struct i2c_client *client, + int page, int reg, u16 value) +{ + int ret; + + switch (reg) { + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_OT_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + value = max_t(s16, (s16)value, 0); + value >>= SQ24860_8B_SHIFT; + value = clamp_val(value, 0, 0xff); + ret = pmbus_write_word_data(client, page, reg, value); + break; + + case PMBUS_IIN_OC_FAULT_LIMIT: + value = max_t(s16, (s16)value, SQ24860_IIN_OCF_OFF); + value -= SQ24860_IIN_OCF_OFF; + value = DIV_ROUND_CLOSEST(((unsigned int)value) * SQ24860_IIN_OCF_DIV, + SQ24860_IIN_OCF_NUM); + value = clamp_val(value, 0, 0x3f); + ret = pmbus_write_byte_data(client, page, SQ24860_VIREF, value); + break; + + case PMBUS_VIRT_SAMPLES: + value = clamp_val(value, 1, SQ24860_MAX_SAMPLES); + value = ilog2(value); + ret = pmbus_update_byte_data(client, page, SQ24860_PK_MIN_AVG, + PK_MIN_AVG_AVG_CNT, + FIELD_PREP(PK_MIN_AVG_AVG_CNT, value)); + break; + + case PMBUS_VIRT_RESET_TEMP_HISTORY: + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_IIN_HISTORY: + case PMBUS_VIRT_RESET_PIN_HISTORY: + case PMBUS_VIRT_RESET_VOUT_HISTORY: + /* + * SQ24860 has history resets based on MIN/AVG/PEAK instead of per + * sensor type. Exposing this quirk in hwmon is not desirable so + * reset MIN, AVG and PEAK together. Even is there effectively only + * one reset, which resets everything, expose the 5 entries so + * userspace is not required map a sensor type to another to trigger + * a reset + */ + ret = pmbus_update_byte_data(client, 0, SQ24860_PK_MIN_AVG, + PK_MIN_AVG_RST_MASK, + PK_MIN_AVG_RST_MASK); + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int sq24860_read_byte_data(struct i2c_client *client, + int page, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_WRITE_PROTECT: + ret = sq24860_mfr_write_protect_get(client); + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int sq24860_write_byte_data(struct i2c_client *client, + int page, int reg, u8 byte) +{ + int ret; + + switch (reg) { + case PMBUS_WRITE_PROTECT: + ret = sq24860_mfr_write_protect_set(client, byte); + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +#if IS_ENABLED(CONFIG_SENSORS_SQ24860_REGULATOR) +static const struct regulator_desc sq24860_reg_desc[] = { + PMBUS_REGULATOR_ONE_NODE("vout"), +}; +#endif + +static const struct pmbus_driver_info sq24860_base_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_VOLTAGE_IN] = 64, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + .format[PSC_VOLTAGE_OUT] = direct, + .m[PSC_VOLTAGE_OUT] = 64, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 1, + .b[PSC_TEMPERATURE] = 0, + .R[PSC_TEMPERATURE] = 0, + /* + * Current and power measurements depend on the calibration gain + * programmed from the board-specific IMON resistor value. + */ + .format[PSC_CURRENT_IN] = direct, + .m[PSC_CURRENT_IN] = 16, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = 0, + .format[PSC_POWER] = direct, + .m[PSC_POWER] = 2, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = 0, + .func[0] = PMBUS_HAVE_VIN | + PMBUS_HAVE_VOUT | + PMBUS_HAVE_VMON | + PMBUS_HAVE_IIN | + PMBUS_HAVE_PIN | + PMBUS_HAVE_TEMP | + PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_SAMPLES, + .read_word_data = sq24860_read_word_data, + .write_word_data = sq24860_write_word_data, + .read_byte_data = sq24860_read_byte_data, + .write_byte_data = sq24860_write_byte_data, + +#if IS_ENABLED(CONFIG_SENSORS_SQ24860_REGULATOR) + .reg_desc = sq24860_reg_desc, + .num_regulators = ARRAY_SIZE(sq24860_reg_desc), +#endif +}; + +static const struct i2c_device_id sq24860_i2c_id[] = { + { "sq24860" }, + {} +}; +MODULE_DEVICE_TABLE(i2c, sq24860_i2c_id); + +static const struct of_device_id sq24860_of_match[] = { + { .compatible = "silergy,sq24860" }, + {} +}; +MODULE_DEVICE_TABLE(of, sq24860_of_match); + +static int sq24860_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct pmbus_driver_info *info; + u32 rimon; + int ret; + + if (device_property_read_u32(dev, "silergy,rimon-micro-ohms", &rimon)) + rimon = SQ24860_DEFAULT_RIMON; + ret = sq24860_write_iin_cal_gain(client, rimon); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "Failed to set gain\n"); + info = devm_kmemdup(dev, &sq24860_base_info, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + return pmbus_do_probe(client, info); +} + +static struct i2c_driver sq24860_driver = { + .driver = { + .name = "sq24860", + .of_match_table = sq24860_of_match, + }, + .probe = sq24860_probe, + .id_table = sq24860_i2c_id, +}; +module_i2c_driver(sq24860_driver); + +MODULE_AUTHOR("Ziming Zhu <ziming.zhu@silergycorp.com>"); +MODULE_DESCRIPTION("PMBUS driver for SQ24860 eFuse"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/stef48h28.c b/drivers/hwmon/pmbus/stef48h28.c new file mode 100644 index 000000000..6aa536c37 --- /dev/null +++ b/drivers/hwmon/pmbus/stef48h28.c @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for STMicroelectronics digital controller stef48h28 + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/module.h> + +#include "pmbus.h" + +static struct pmbus_driver_info stef48h28_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_IN] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 50, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = 0, + .m[PSC_VOLTAGE_OUT] = 50, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = 0, + .m[PSC_CURRENT_IN] = 100, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = 0, + .m[PSC_CURRENT_OUT] = 100, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = 0, + .m[PSC_POWER] = 9765, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = -3, + .m[PSC_TEMPERATURE] = 25, + .b[PSC_TEMPERATURE] = 500, + .R[PSC_TEMPERATURE] = 0, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | PMBUS_HAVE_PIN + | PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 + | PMBUS_HAVE_STATUS_TEMP | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_POUT +}; + +static int stef48h28_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &stef48h28_info); +} + +static const struct i2c_device_id stef48h28_id[] = { + { .name = "stef48h28" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, stef48h28_id); + +static const struct of_device_id __maybe_unused stef48h28_of_match[] = { + {.compatible = "st,stef48h28"}, + {} +}; + +static struct i2c_driver stef48h28_driver = { + .driver = { + .name = "stef48h28", + .of_match_table = of_match_ptr(stef48h28_of_match), + }, + .probe = stef48h28_probe, + .id_table = stef48h28_id, +}; + +module_i2c_driver(stef48h28_driver); + +MODULE_AUTHOR("Charles Hsu <hsu.yungteng@gmail.com>"); +MODULE_DESCRIPTION("PMBus driver for ST stef48h28"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/stpddc60.c b/drivers/hwmon/pmbus/stpddc60.c new file mode 100644 index 000000000..b5ce7f975 --- /dev/null +++ b/drivers/hwmon/pmbus/stpddc60.c @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for the STPDDC60 controller + * + * Copyright (c) 2021 Flextronics International Sweden AB. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +#define STPDDC60_MFR_READ_VOUT 0xd2 +#define STPDDC60_MFR_OV_LIMIT_OFFSET 0xe5 +#define STPDDC60_MFR_UV_LIMIT_OFFSET 0xe6 + +static const struct i2c_device_id stpddc60_id[] = { + { .name = "stpddc60" }, + { .name = "bmr481" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, stpddc60_id); + +static struct pmbus_driver_info stpddc60_info = { + .pages = 1, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT, +}; + +/* + * Calculate the closest absolute offset between commanded vout value + * and limit value in steps of 50mv in the range 0 (50mv) to 7 (400mv). + * Return 0 if the upper limit is lower than vout or if the lower limit + * is higher than vout. + */ +static u8 stpddc60_get_offset(int vout, u16 limit, bool over) +{ + int offset; + long v, l; + + v = 250 + (vout - 1) * 5; /* Convert VID to mv */ + l = (limit * 1000L) >> 8; /* Convert LINEAR to mv */ + + if (over == (l < v)) + return 0; + + offset = DIV_ROUND_CLOSEST(abs(l - v), 50); + + if (offset > 0) + offset--; + + return clamp_val(offset, 0, 7); +} + +/* + * Adjust the linear format word to use the given fixed exponent. + */ +static u16 stpddc60_adjust_linear(u16 word, s16 fixed) +{ + s16 e, m, d; + + e = ((s16)word) >> 11; + m = ((s16)((word & 0x7ff) << 5)) >> 5; + d = e - fixed; + + if (d >= 0) + m <<= d; + else + m >>= -d; + + return clamp_val(m, 0, 0x3ff) | ((fixed << 11) & 0xf800); +} + +/* + * The VOUT_COMMAND register uses the VID format but the vout alarm limit + * registers use the LINEAR format so we override VOUT_MODE here to force + * LINEAR format for all registers. + */ +static int stpddc60_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_VOUT_MODE: + ret = 0x18; + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +/* + * The vout related registers return values in LINEAR11 format when LINEAR16 + * is expected. Clear the top 5 bits to set the exponent part to zero to + * convert the value to LINEAR16 format. + */ +static int stpddc60_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + int ret; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_READ_VOUT: + ret = pmbus_read_word_data(client, page, phase, + STPDDC60_MFR_READ_VOUT); + if (ret < 0) + return ret; + ret &= 0x7ff; + break; + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + ret &= 0x7ff; + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +/* + * The vout under- and over-voltage limits are set as an offset relative to + * the commanded vout voltage. The vin, iout, pout and temp limits must use + * the same fixed exponent the chip uses to encode the data when read. + */ +static int stpddc60_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + int ret; + u8 offset; + + if (page > 0) + return -ENXIO; + + switch (reg) { + case PMBUS_VOUT_OV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, 0xff, + PMBUS_VOUT_COMMAND); + if (ret < 0) + return ret; + offset = stpddc60_get_offset(ret, word, true); + ret = pmbus_write_byte_data(client, page, + STPDDC60_MFR_OV_LIMIT_OFFSET, + offset); + break; + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, 0xff, + PMBUS_VOUT_COMMAND); + if (ret < 0) + return ret; + offset = stpddc60_get_offset(ret, word, false); + ret = pmbus_write_byte_data(client, page, + STPDDC60_MFR_UV_LIMIT_OFFSET, + offset); + break; + case PMBUS_VIN_OV_FAULT_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_OT_WARN_LIMIT: + case PMBUS_IOUT_OC_FAULT_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + case PMBUS_POUT_OP_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, 0xff, reg); + if (ret < 0) + return ret; + word = stpddc60_adjust_linear(word, ret >> 11); + ret = pmbus_write_word_data(client, page, reg, word); + break; + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int stpddc60_probe(struct i2c_client *client) +{ + int status; + u8 device_id[I2C_SMBUS_BLOCK_MAX + 1]; + const struct i2c_device_id *mid; + struct pmbus_driver_info *info = &stpddc60_info; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE_DATA + | I2C_FUNC_SMBUS_BLOCK_DATA)) + return -ENODEV; + + status = i2c_smbus_read_block_data(client, PMBUS_MFR_MODEL, device_id); + if (status < 0) { + dev_err(&client->dev, "Failed to read Manufacturer Model\n"); + return status; + } + for (mid = stpddc60_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, device_id, strlen(mid->name))) + break; + } + if (!mid->name[0]) { + dev_err(&client->dev, "Unsupported device\n"); + return -ENODEV; + } + + info->read_byte_data = stpddc60_read_byte_data; + info->read_word_data = stpddc60_read_word_data; + info->write_word_data = stpddc60_write_word_data; + + status = pmbus_do_probe(client, info); + if (status < 0) + return status; + + pmbus_set_update(client, PMBUS_VOUT_OV_FAULT_LIMIT, true); + pmbus_set_update(client, PMBUS_VOUT_UV_FAULT_LIMIT, true); + + return 0; +} + +static struct i2c_driver stpddc60_driver = { + .driver = { + .name = "stpddc60", + }, + .probe = stpddc60_probe, + .id_table = stpddc60_id, +}; + +module_i2c_driver(stpddc60_driver); + +MODULE_AUTHOR("Erik Rosen <erik.rosen@metormote.com>"); +MODULE_DESCRIPTION("PMBus driver for ST STPDDC60"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/tda38640.c b/drivers/hwmon/pmbus/tda38640.c new file mode 100644 index 000000000..803926633 --- /dev/null +++ b/drivers/hwmon/pmbus/tda38640.c @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Infineon TDA38640 + * + * Copyright (c) 2023 9elements GmbH + * + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/regulator/driver.h> +#include "pmbus.h" + +static const struct regulator_desc __maybe_unused tda38640_reg_desc[] = { + PMBUS_REGULATOR_ONE_NODE("vout"), +}; + +struct tda38640_data { + struct pmbus_driver_info info; + u32 en_pin_lvl; +}; + +#define to_tda38640_data(x) container_of(x, struct tda38640_data, info) + +/* + * Map PB_ON_OFF_CONFIG_POLARITY_HIGH to PB_OPERATION_CONTROL_ON. + */ +static int tda38640_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct tda38640_data *data = to_tda38640_data(info); + int ret, on_off_config, enabled; + + if (reg != PMBUS_OPERATION) + return -ENODATA; + + ret = pmbus_read_byte_data(client, page, reg); + if (ret < 0) + return ret; + + on_off_config = pmbus_read_byte_data(client, page, + PMBUS_ON_OFF_CONFIG); + if (on_off_config < 0) + return on_off_config; + + enabled = !!(on_off_config & PB_ON_OFF_CONFIG_POLARITY_HIGH); + + enabled ^= data->en_pin_lvl; + if (enabled) + ret &= ~PB_OPERATION_CONTROL_ON; + else + ret |= PB_OPERATION_CONTROL_ON; + + return ret; +} + +/* + * Map PB_OPERATION_CONTROL_ON to PB_ON_OFF_CONFIG_POLARITY_HIGH. + */ +static int tda38640_write_byte_data(struct i2c_client *client, int page, + int reg, u8 byte) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct tda38640_data *data = to_tda38640_data(info); + int enable, ret; + + if (reg != PMBUS_OPERATION) + return -ENODATA; + + enable = !!(byte & PB_OPERATION_CONTROL_ON); + + byte &= ~PB_OPERATION_CONTROL_ON; + ret = pmbus_write_byte_data(client, page, reg, byte); + if (ret < 0) + return ret; + + enable ^= data->en_pin_lvl; + + return pmbus_update_byte_data(client, page, PMBUS_ON_OFF_CONFIG, + PB_ON_OFF_CONFIG_POLARITY_HIGH, + enable ? 0 : PB_ON_OFF_CONFIG_POLARITY_HIGH); +} + +static int svid_mode(struct i2c_client *client, struct tda38640_data *data) +{ + /* PMBUS_MFR_READ(0xD0) + MTP Address offset */ + u8 write_buf[] = {0xd0, 0x44, 0x00}; + u8 read_buf[2]; + int ret, svid; + bool off, reg_en_pin_pol; + + struct i2c_msg msgs[2] = { + { + .addr = client->addr, + .flags = 0, + .buf = write_buf, + .len = sizeof(write_buf), + }, + { + .addr = client->addr, + .flags = I2C_M_RD, + .buf = read_buf, + .len = sizeof(read_buf), + } + }; + + ret = i2c_transfer(client->adapter, msgs, 2); + if (ret < 0) { + dev_err(&client->dev, "i2c_transfer failed. %d", ret); + return ret; + } + + /* + * 0x44[15] determines PMBus Operating Mode + * If bit is set then it is SVID mode. + */ + svid = !!(read_buf[1] & BIT(7)); + + /* + * Determine EN pin level for use in SVID mode. + * This is done with help of STATUS_BYTE bit 6(OFF) & ON_OFF_CONFIG bit 2(EN pin polarity). + */ + if (svid) { + ret = i2c_smbus_read_byte_data(client, PMBUS_STATUS_BYTE); + if (ret < 0) + return ret; + off = !!(ret & PB_STATUS_OFF); + + ret = i2c_smbus_read_byte_data(client, PMBUS_ON_OFF_CONFIG); + if (ret < 0) + return ret; + reg_en_pin_pol = !!(ret & PB_ON_OFF_CONFIG_POLARITY_HIGH); + data->en_pin_lvl = off ^ reg_en_pin_pol; + } + + return svid; +} + +static struct pmbus_driver_info tda38640_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_POWER] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_IIN + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_POUT | PMBUS_HAVE_PIN, +#if IS_ENABLED(CONFIG_SENSORS_TDA38640_REGULATOR) + .num_regulators = 1, + .reg_desc = tda38640_reg_desc, +#endif +}; + +static int tda38640_probe(struct i2c_client *client) +{ + struct tda38640_data *data; + int svid; + + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + memcpy(&data->info, &tda38640_info, sizeof(tda38640_info)); + + if (IS_ENABLED(CONFIG_SENSORS_TDA38640_REGULATOR) && + of_property_read_bool(client->dev.of_node, "infineon,en-pin-fixed-level")) { + svid = svid_mode(client, data); + if (svid < 0) { + dev_err_probe(&client->dev, svid, "Could not determine operating mode."); + return svid; + } + + /* + * Apply ON_OFF_CONFIG workaround as enabling the regulator using the + * OPERATION register doesn't work in SVID mode. + * + * One should configure PMBUS_ON_OFF_CONFIG here, but + * PB_ON_OFF_CONFIG_POWERUP_CONTROL and PB_ON_OFF_CONFIG_EN_PIN_REQ + * are ignored by the device. + * Only PB_ON_OFF_CONFIG_POLARITY_HIGH has an effect. + */ + if (svid) { + data->info.read_byte_data = tda38640_read_byte_data; + data->info.write_byte_data = tda38640_write_byte_data; + } + } + return pmbus_do_probe(client, &data->info); +} + +static const struct i2c_device_id tda38640_id[] = { + { .name = "tda38640" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, tda38640_id); + +static const struct of_device_id __maybe_unused tda38640_of_match[] = { + { .compatible = "infineon,tda38640"}, + { }, +}; +MODULE_DEVICE_TABLE(of, tda38640_of_match); + +/* This is the driver that will be inserted */ +static struct i2c_driver tda38640_driver = { + .driver = { + .name = "tda38640", + .of_match_table = of_match_ptr(tda38640_of_match), + }, + .probe = tda38640_probe, + .id_table = tda38640_id, +}; + +module_i2c_driver(tda38640_driver); + +MODULE_AUTHOR("Patrick Rudolph <patrick.rudolph@9elements.com>"); +MODULE_DESCRIPTION("PMBus driver for Infineon TDA38640"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/tps25990.c b/drivers/hwmon/pmbus/tps25990.c new file mode 100644 index 000000000..9d318e650 --- /dev/null +++ b/drivers/hwmon/pmbus/tps25990.c @@ -0,0 +1,442 @@ +// SPDX-License-Identifier: GPL-2.0 +// +// Copyright (c) 2024 BayLibre, SAS. +// Author: Jerome Brunet <jbrunet@baylibre.com> + +#include <linux/bitfield.h> +#include <linux/debugfs.h> +#include <linux/err.h> +#include <linux/hwmon-sysfs.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> + +#include "pmbus.h" + +#define TPS25990_READ_VAUX 0xd0 +#define TPS25990_READ_VIN_MIN 0xd1 +#define TPS25990_READ_VIN_PEAK 0xd2 +#define TPS25990_READ_IIN_PEAK 0xd4 +#define TPS25990_READ_PIN_PEAK 0xd5 +#define TPS25990_READ_TEMP_AVG 0xd6 +#define TPS25990_READ_TEMP_PEAK 0xd7 +#define TPS25990_READ_VOUT_MIN 0xda +#define TPS25990_READ_VIN_AVG 0xdc +#define TPS25990_READ_VOUT_AVG 0xdd +#define TPS25990_READ_IIN_AVG 0xde +#define TPS25990_READ_PIN_AVG 0xdf +#define TPS25990_VIREF 0xe0 +#define TPS25990_PK_MIN_AVG 0xea +#define PK_MIN_AVG_RST_PEAK BIT(7) +#define PK_MIN_AVG_RST_AVG BIT(6) +#define PK_MIN_AVG_RST_MIN BIT(5) +#define PK_MIN_AVG_AVG_CNT GENMASK(2, 0) +#define TPS25990_MFR_WRITE_PROTECT 0xf8 +#define TPS25990_UNLOCKED BIT(7) + +#define TPS25990_8B_SHIFT 2 +#define TPS25990_VIN_OVF_NUM 525100 +#define TPS25990_VIN_OVF_DIV 10163 +#define TPS25990_VIN_OVF_OFF 155 +#define TPS25990_IIN_OCF_NUM 953800 +#define TPS25990_IIN_OCF_DIV 129278 +#define TPS25990_IIN_OCF_OFF 157 + +#define PK_MIN_AVG_RST_MASK (PK_MIN_AVG_RST_PEAK | \ + PK_MIN_AVG_RST_AVG | \ + PK_MIN_AVG_RST_MIN) + +/* + * Arbitrary default Rimon value: 1kOhm + * This correspond to an overcurrent limit of 55A, close to the specified limit + * of un-stacked TPS25990 and makes further calculation easier to setup in + * sensor.conf, if necessary + */ +#define TPS25990_DEFAULT_RIMON 1000000000 + +static void tps25990_set_m(int *m, u32 rimon) +{ + u64 val = ((u64)*m) * rimon; + + /* Make sure m fits the s32 type */ + *m = DIV_ROUND_CLOSEST_ULL(val, 1000000); +} + +static int tps25990_mfr_write_protect_set(struct i2c_client *client, + u8 protect) +{ + u8 val; + + switch (protect) { + case 0: + val = 0xa2; + break; + case PB_WP_ALL: + val = 0x0; + break; + default: + return -EINVAL; + } + + return pmbus_write_byte_data(client, -1, TPS25990_MFR_WRITE_PROTECT, + val); +} + +static int tps25990_mfr_write_protect_get(struct i2c_client *client) +{ + int ret = pmbus_read_byte_data(client, -1, TPS25990_MFR_WRITE_PROTECT); + + if (ret < 0) + return ret; + + return (ret & TPS25990_UNLOCKED) ? 0 : PB_WP_ALL; +} + +static int tps25990_read_word_data(struct i2c_client *client, + int page, int phase, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_VIRT_READ_VIN_MAX: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_VIN_PEAK); + break; + + case PMBUS_VIRT_READ_VIN_MIN: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_VIN_MIN); + break; + + case PMBUS_VIRT_READ_VIN_AVG: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_VIN_AVG); + break; + + case PMBUS_VIRT_READ_VOUT_MIN: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_VOUT_MIN); + break; + + case PMBUS_VIRT_READ_VOUT_AVG: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_VOUT_AVG); + break; + + case PMBUS_VIRT_READ_IIN_AVG: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_IIN_AVG); + break; + + case PMBUS_VIRT_READ_IIN_MAX: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_IIN_PEAK); + break; + + case PMBUS_VIRT_READ_TEMP_AVG: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_TEMP_AVG); + break; + + case PMBUS_VIRT_READ_TEMP_MAX: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_TEMP_PEAK); + break; + + case PMBUS_VIRT_READ_PIN_AVG: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_PIN_AVG); + break; + + case PMBUS_VIRT_READ_PIN_MAX: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_PIN_PEAK); + break; + + case PMBUS_VIRT_READ_VMON: + ret = pmbus_read_word_data(client, page, phase, + TPS25990_READ_VAUX); + break; + + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_OT_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + /* + * These registers provide an 8 bits value instead of a + * 10bits one. Just shifting twice the register value is + * enough to make the sensor type conversion work, even + * if the datasheet provides different m, b and R for + * those. + */ + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + break; + ret <<= TPS25990_8B_SHIFT; + break; + + case PMBUS_VIN_OV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + break; + ret = DIV_ROUND_CLOSEST(ret * TPS25990_VIN_OVF_NUM, + TPS25990_VIN_OVF_DIV); + ret += TPS25990_VIN_OVF_OFF; + break; + + case PMBUS_IIN_OC_FAULT_LIMIT: + /* + * VIREF directly sets the over-current limit at which the eFuse + * will turn the FET off and trigger a fault. Expose it through + * this generic property instead of a manufacturer specific one. + */ + ret = pmbus_read_byte_data(client, page, TPS25990_VIREF); + if (ret < 0) + break; + ret = DIV_ROUND_CLOSEST(ret * TPS25990_IIN_OCF_NUM, + TPS25990_IIN_OCF_DIV); + ret += TPS25990_IIN_OCF_OFF; + break; + + case PMBUS_VIRT_SAMPLES: + ret = pmbus_read_byte_data(client, page, TPS25990_PK_MIN_AVG); + if (ret < 0) + break; + ret = 1 << FIELD_GET(PK_MIN_AVG_AVG_CNT, ret); + break; + + case PMBUS_VIRT_RESET_TEMP_HISTORY: + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_IIN_HISTORY: + case PMBUS_VIRT_RESET_PIN_HISTORY: + case PMBUS_VIRT_RESET_VOUT_HISTORY: + ret = 0; + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int tps25990_write_word_data(struct i2c_client *client, + int page, int reg, u16 value) +{ + int ret; + + switch (reg) { + case PMBUS_VIN_UV_WARN_LIMIT: + case PMBUS_VIN_UV_FAULT_LIMIT: + case PMBUS_VIN_OV_WARN_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_IIN_OC_WARN_LIMIT: + case PMBUS_OT_WARN_LIMIT: + case PMBUS_OT_FAULT_LIMIT: + case PMBUS_PIN_OP_WARN_LIMIT: + value >>= TPS25990_8B_SHIFT; + value = clamp_val(value, 0, 0xff); + ret = pmbus_write_word_data(client, page, reg, value); + break; + + case PMBUS_VIN_OV_FAULT_LIMIT: + value -= TPS25990_VIN_OVF_OFF; + value = DIV_ROUND_CLOSEST(((unsigned int)value) * TPS25990_VIN_OVF_DIV, + TPS25990_VIN_OVF_NUM); + value = clamp_val(value, 0, 0xf); + ret = pmbus_write_word_data(client, page, reg, value); + break; + + case PMBUS_IIN_OC_FAULT_LIMIT: + value -= TPS25990_IIN_OCF_OFF; + value = DIV_ROUND_CLOSEST(((unsigned int)value) * TPS25990_IIN_OCF_DIV, + TPS25990_IIN_OCF_NUM); + value = clamp_val(value, 0, 0x3f); + ret = pmbus_write_byte_data(client, page, TPS25990_VIREF, value); + break; + + case PMBUS_VIRT_SAMPLES: + value = clamp_val(value, 1, 1 << PK_MIN_AVG_AVG_CNT); + value = ilog2(value); + ret = pmbus_update_byte_data(client, page, TPS25990_PK_MIN_AVG, + PK_MIN_AVG_AVG_CNT, + FIELD_PREP(PK_MIN_AVG_AVG_CNT, value)); + break; + + case PMBUS_VIRT_RESET_TEMP_HISTORY: + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_IIN_HISTORY: + case PMBUS_VIRT_RESET_PIN_HISTORY: + case PMBUS_VIRT_RESET_VOUT_HISTORY: + /* + * TPS25990 has history resets based on MIN/AVG/PEAK instead of per + * sensor type. Exposing this quirk in hwmon is not desirable so + * reset MIN, AVG and PEAK together. Even is there effectively only + * one reset, which resets everything, expose the 5 entries so + * userspace is not required map a sensor type to another to trigger + * a reset + */ + ret = pmbus_update_byte_data(client, 0, TPS25990_PK_MIN_AVG, + PK_MIN_AVG_RST_MASK, + PK_MIN_AVG_RST_MASK); + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int tps25990_read_byte_data(struct i2c_client *client, + int page, int reg) +{ + int ret; + + switch (reg) { + case PMBUS_WRITE_PROTECT: + ret = tps25990_mfr_write_protect_get(client); + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +static int tps25990_write_byte_data(struct i2c_client *client, + int page, int reg, u8 byte) +{ + int ret; + + switch (reg) { + case PMBUS_WRITE_PROTECT: + ret = tps25990_mfr_write_protect_set(client, byte); + break; + + default: + ret = -ENODATA; + break; + } + + return ret; +} + +#if IS_ENABLED(CONFIG_SENSORS_TPS25990_REGULATOR) +static const struct regulator_desc tps25990_reg_desc[] = { + PMBUS_REGULATOR_ONE_NODE("vout"), +}; +#endif + +static const struct pmbus_driver_info tps25990_base_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .m[PSC_VOLTAGE_IN] = 5251, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -2, + .format[PSC_VOLTAGE_OUT] = direct, + .m[PSC_VOLTAGE_OUT] = 5251, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = -2, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_TEMPERATURE] = 140, + .b[PSC_TEMPERATURE] = 32100, + .R[PSC_TEMPERATURE] = -2, + /* + * Current and Power measurement depends on the ohm value + * of Rimon. m is multiplied by 1000 below to have an integer + * and -3 is added to R to compensate. + */ + .format[PSC_CURRENT_IN] = direct, + .m[PSC_CURRENT_IN] = 9538, + .b[PSC_CURRENT_IN] = 0, + .R[PSC_CURRENT_IN] = -6, + .format[PSC_POWER] = direct, + .m[PSC_POWER] = 4901, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = -7, + .func[0] = (PMBUS_HAVE_VIN | + PMBUS_HAVE_VOUT | + PMBUS_HAVE_VMON | + PMBUS_HAVE_IIN | + PMBUS_HAVE_PIN | + PMBUS_HAVE_TEMP | + PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_SAMPLES), + .read_word_data = tps25990_read_word_data, + .write_word_data = tps25990_write_word_data, + .read_byte_data = tps25990_read_byte_data, + .write_byte_data = tps25990_write_byte_data, + +#if IS_ENABLED(CONFIG_SENSORS_TPS25990_REGULATOR) + .reg_desc = tps25990_reg_desc, + .num_regulators = ARRAY_SIZE(tps25990_reg_desc), +#endif +}; + +static const struct i2c_device_id tps25990_i2c_id[] = { + { .name = "tps25990" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, tps25990_i2c_id); + +static const struct of_device_id tps25990_of_match[] = { + { .compatible = "ti,tps25990" }, + {} +}; +MODULE_DEVICE_TABLE(of, tps25990_of_match); + +static int tps25990_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct pmbus_driver_info *info; + const char *propname; + u32 rimon; + int ret; + + propname = "ti,rimon-micro-ohms"; + if (device_property_present(dev, propname)) { + ret = device_property_read_u32(dev, propname, &rimon); + if (ret) + return dev_err_probe(dev, ret, "failed to get %s\n", propname); + } else { + rimon = TPS25990_DEFAULT_RIMON; + } + + info = devm_kmemdup(dev, &tps25990_base_info, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + /* Adapt the current and power scale for each instance */ + tps25990_set_m(&info->m[PSC_CURRENT_IN], rimon); + tps25990_set_m(&info->m[PSC_POWER], rimon); + + return pmbus_do_probe(client, info); +} + +static struct i2c_driver tps25990_driver = { + .driver = { + .name = "tps25990", + .of_match_table = tps25990_of_match, + }, + .probe = tps25990_probe, + .id_table = tps25990_i2c_id, +}; +module_i2c_driver(tps25990_driver); + +MODULE_AUTHOR("Jerome Brunet <jbrunet@baylibre.com>"); +MODULE_DESCRIPTION("PMBUS driver for TPS25990 eFuse"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/tps40422.c b/drivers/hwmon/pmbus/tps40422.c new file mode 100644 index 000000000..f7be075dc --- /dev/null +++ b/drivers/hwmon/pmbus/tps40422.c @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for TI TPS40422 + * + * Copyright (c) 2014 Nokia Solutions and Networks. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include "pmbus.h" + +static struct pmbus_driver_info tps40422_info = { + .pages = 2, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .func[0] = PMBUS_HAVE_VOUT | PMBUS_HAVE_TEMP2 + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_TEMP2 + | PMBUS_HAVE_STATUS_VOUT | PMBUS_HAVE_STATUS_TEMP + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT, +}; + +static int tps40422_probe(struct i2c_client *client) +{ + return pmbus_do_probe(client, &tps40422_info); +} + +static const struct i2c_device_id tps40422_id[] = { + { .name = "tps40422" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, tps40422_id); + +/* This is the driver that will be inserted */ +static struct i2c_driver tps40422_driver = { + .driver = { + .name = "tps40422", + }, + .probe = tps40422_probe, + .id_table = tps40422_id, +}; + +module_i2c_driver(tps40422_driver); + +MODULE_AUTHOR("Zhu Laiwen <richard.zhu@nsn.com>"); +MODULE_DESCRIPTION("PMBus driver for TI TPS40422"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/tps53679.c b/drivers/hwmon/pmbus/tps53679.c new file mode 100644 index 000000000..31e54608b --- /dev/null +++ b/drivers/hwmon/pmbus/tps53679.c @@ -0,0 +1,333 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Texas Instruments TPS53679 + * + * Copyright (c) 2017 Mellanox Technologies. All rights reserved. + * Copyright (c) 2017 Vadim Pasternak <vadimp@mellanox.com> + */ + +#include <linux/bits.h> +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include "pmbus.h" + +enum chips { + tps53647, tps53667, tps53676, tps53679, tps53681, tps53685, tps53688 +}; + +#define TPS53647_PAGE_NUM 1 + +#define TPS53676_USER_DATA_03 0xb3 +#define TPS53676_MAX_PHASES 7 + +#define TPS53679_PROT_VR12_5MV 0x01 /* VR12.0 mode, 5-mV DAC */ +#define TPS53679_PROT_VR12_5_10MV 0x02 /* VR12.5 mode, 10-mV DAC */ +#define TPS53679_PROT_VR13_10MV 0x04 /* VR13.0 mode, 10-mV DAC */ +#define TPS53679_PROT_IMVP8_5MV 0x05 /* IMVP8 mode, 5-mV DAC */ +#define TPS53679_PROT_VR13_5MV 0x07 /* VR13.0 mode, 5-mV DAC */ +#define TPS53679_PAGE_NUM 2 + +#define TPS53681_DEVICE_ID "\x81" +#define TPS53685_DEVICE_ID "TIShP" + +#define TPS53681_PMBUS_REVISION 0x33 + +#define TPS53681_MFR_SPECIFIC_20 0xe4 /* Number of phases, per page */ + +static const struct i2c_device_id tps53679_id[]; + +static int tps53679_identify_mode(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + u8 vout_params; + int i, ret; + + for (i = 0; i < info->pages; i++) { + /* Read the register with VOUT scaling value.*/ + ret = pmbus_read_byte_data(client, i, PMBUS_VOUT_MODE); + if (ret < 0) + return ret; + + vout_params = ret & GENMASK(4, 0); + + switch (vout_params) { + case TPS53679_PROT_VR13_10MV: + case TPS53679_PROT_VR12_5_10MV: + info->vrm_version[i] = vr13; + break; + case TPS53679_PROT_VR13_5MV: + case TPS53679_PROT_VR12_5MV: + case TPS53679_PROT_IMVP8_5MV: + info->vrm_version[i] = vr12; + break; + default: + return -EINVAL; + } + } + + return 0; +} + +static int tps53679_identify_phases(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + int ret; + + /* On TPS53681, only channel A provides per-phase output current */ + ret = pmbus_read_byte_data(client, 0, TPS53681_MFR_SPECIFIC_20); + if (ret < 0) + return ret; + info->phases[0] = (ret & 0x07) + 1; + + return 0; +} + +static int tps53679_identify_chip(struct i2c_client *client, + u8 revision, char *id) +{ + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int ret; + int buf_len; + int id_len; + + ret = pmbus_read_byte_data(client, 0, PMBUS_REVISION); + if (ret < 0) + return ret; + if (ret != revision) { + dev_err(&client->dev, "Unexpected PMBus revision 0x%x\n", ret); + return -ENODEV; + } + + ret = i2c_smbus_read_block_data(client, PMBUS_IC_DEVICE_ID, buf); + if (ret <= 0) + return ret < 0 ? ret : -EIO; + + /* Adjust length if null terminator is present */ + buf_len = (buf[ret - 1] != '\x00' ? ret : ret - 1); + + id_len = strlen(id); + + if (buf_len != id_len || strncmp(id, buf, id_len)) { + dev_err(&client->dev, "Unexpected device ID: %*ph\n", ret, buf); + return -ENODEV; + } + return 0; +} + +/* + * Common identification function for chips with multi-phase support. + * Since those chips have special configuration registers, we want to have + * some level of reassurance that we are really talking with the chip + * being probed. Check PMBus revision and chip ID. + */ +static int tps53679_identify_multiphase(struct i2c_client *client, + struct pmbus_driver_info *info, + int pmbus_rev, char *device_id) +{ + int ret; + + ret = tps53679_identify_chip(client, pmbus_rev, device_id); + if (ret < 0) + return ret; + + ret = tps53679_identify_mode(client, info); + if (ret < 0) + return ret; + + return tps53679_identify_phases(client, info); +} + +static int tps53679_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + return tps53679_identify_mode(client, info); +} + +static int tps53685_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + info->func[1] |= PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | + PMBUS_HAVE_STATUS_INPUT; + info->format[PSC_VOLTAGE_OUT] = linear; + return tps53679_identify_chip(client, TPS53681_PMBUS_REVISION, + TPS53685_DEVICE_ID); +} + +static int tps53681_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + return tps53679_identify_multiphase(client, info, + TPS53681_PMBUS_REVISION, + TPS53681_DEVICE_ID); +} + +static int tps53676_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + u8 buf[I2C_SMBUS_BLOCK_MAX]; + int phases_a = 0, phases_b = 0; + int i, ret; + + ret = i2c_smbus_read_block_data(client, PMBUS_IC_DEVICE_ID, buf); + if (ret < 0) + return ret; + if (ret != 6 || memcmp(buf, "TI\x53\x67\x60\x00", 6)) { + dev_err(&client->dev, "Unexpected device ID: %*ph\n", ret, buf); + return -ENODEV; + } + + ret = i2c_smbus_read_block_data(client, TPS53676_USER_DATA_03, buf); + if (ret < 0) + return ret; + if (ret != 24) + return -EIO; + for (i = 0; i < 2 * TPS53676_MAX_PHASES; i += 2) { + if (buf[i + 1] & 0x80) { + if (buf[i] & 0x08) + phases_b++; + else + phases_a++; + } + } + + info->format[PSC_VOLTAGE_OUT] = linear; + info->pages = 1; + info->phases[0] = phases_a; + if (phases_b > 0) { + info->pages = 2; + info->phases[1] = phases_b; + } + return 0; +} + +static int tps53681_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + /* + * For reading the total output current (READ_IOUT) for all phases, + * the chip datasheet is a bit vague. It says "PHASE must be set to + * FFh to access all phases simultaneously. PHASE may also be set to + * 80h readack (!) the total phase current". + * Experiments show that the command does _not_ report the total + * current for all phases if the phase is set to 0xff. Instead, it + * appears to report the current of one of the phases. Override phase + * parameter with 0x80 when reading the total output current on page 0. + */ + if (reg == PMBUS_READ_IOUT && page == 0 && phase == 0xff) + return pmbus_read_word_data(client, page, 0x80, reg); + return -ENODATA; +} + +static struct pmbus_driver_info tps53679_info = { + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = vid, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | + PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_POUT, + .func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_POUT, + .pfunc[0] = PMBUS_HAVE_IOUT, + .pfunc[1] = PMBUS_HAVE_IOUT, + .pfunc[2] = PMBUS_HAVE_IOUT, + .pfunc[3] = PMBUS_HAVE_IOUT, + .pfunc[4] = PMBUS_HAVE_IOUT, + .pfunc[5] = PMBUS_HAVE_IOUT, + .pfunc[6] = PMBUS_HAVE_IOUT, +}; + +static int tps53679_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct pmbus_driver_info *info; + enum chips chip_id; + + chip_id = (uintptr_t)i2c_get_match_data(client); + + info = devm_kmemdup(dev, &tps53679_info, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + switch (chip_id) { + case tps53647: + case tps53667: + info->pages = TPS53647_PAGE_NUM; + info->identify = tps53679_identify; + break; + case tps53676: + info->identify = tps53676_identify; + break; + case tps53679: + case tps53688: + info->pages = TPS53679_PAGE_NUM; + info->identify = tps53679_identify; + break; + case tps53681: + info->pages = TPS53679_PAGE_NUM; + info->phases[0] = 6; + info->identify = tps53681_identify; + info->read_word_data = tps53681_read_word_data; + break; + case tps53685: + info->pages = TPS53679_PAGE_NUM; + info->identify = tps53685_identify; + break; + default: + return -ENODEV; + } + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id tps53679_id[] = { + { .name = "bmr474", .driver_data = tps53676 }, + { .name = "tps53647", .driver_data = tps53647 }, + { .name = "tps53667", .driver_data = tps53667 }, + { .name = "tps53676", .driver_data = tps53676 }, + { .name = "tps53679", .driver_data = tps53679 }, + { .name = "tps53681", .driver_data = tps53681 }, + { .name = "tps53685", .driver_data = tps53685 }, + { .name = "tps53688", .driver_data = tps53688 }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, tps53679_id); + +static const struct of_device_id __maybe_unused tps53679_of_match[] = { + {.compatible = "ti,tps53647", .data = (void *)tps53647}, + {.compatible = "ti,tps53667", .data = (void *)tps53667}, + {.compatible = "ti,tps53676", .data = (void *)tps53676}, + {.compatible = "ti,tps53679", .data = (void *)tps53679}, + {.compatible = "ti,tps53681", .data = (void *)tps53681}, + {.compatible = "ti,tps53685", .data = (void *)tps53685}, + {.compatible = "ti,tps53688", .data = (void *)tps53688}, + {} +}; +MODULE_DEVICE_TABLE(of, tps53679_of_match); + +static struct i2c_driver tps53679_driver = { + .driver = { + .name = "tps53679", + .of_match_table = of_match_ptr(tps53679_of_match), + }, + .probe = tps53679_probe, + .id_table = tps53679_id, +}; + +module_i2c_driver(tps53679_driver); + +MODULE_AUTHOR("Vadim Pasternak <vadimp@mellanox.com>"); +MODULE_DESCRIPTION("PMBus driver for Texas Instruments TPS53679"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/tps546d24.c b/drivers/hwmon/pmbus/tps546d24.c new file mode 100644 index 000000000..4222ff355 --- /dev/null +++ b/drivers/hwmon/pmbus/tps546d24.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for TEXAS TPS546D24 buck converter + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +static struct pmbus_driver_info tps546d24_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_OUT] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_IIN + | PMBUS_HAVE_IOUT | PMBUS_HAVE_VOUT + | PMBUS_HAVE_STATUS_IOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP, +}; + +static int tps546d24_probe(struct i2c_client *client) +{ + int reg; + + reg = i2c_smbus_read_byte_data(client, PMBUS_VOUT_MODE); + if (reg < 0) + return reg; + + if (reg & 0x80) { + int err; + + err = i2c_smbus_write_byte_data(client, PMBUS_VOUT_MODE, reg & 0x7f); + if (err < 0) + return err; + } + return pmbus_do_probe(client, &tps546d24_info); +} + +static const struct i2c_device_id tps546d24_id[] = { + { .name = "tps546d24" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, tps546d24_id); + +static const struct of_device_id __maybe_unused tps546d24_of_match[] = { + {.compatible = "ti,tps546d24"}, + {} +}; +MODULE_DEVICE_TABLE(of, tps546d24_of_match); + +/* This is the driver that will be inserted */ +static struct i2c_driver tps546d24_driver = { + .driver = { + .name = "tps546d24", + .of_match_table = of_match_ptr(tps546d24_of_match), + }, + .probe = tps546d24_probe, + .id_table = tps546d24_id, +}; + +module_i2c_driver(tps546d24_driver); + +MODULE_AUTHOR("Duke Du <dukedu83@gmail.com>"); +MODULE_DESCRIPTION("PMBus driver for TI tps546d24"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ucd9000.c b/drivers/hwmon/pmbus/ucd9000.c new file mode 100644 index 000000000..f76c2913c --- /dev/null +++ b/drivers/hwmon/pmbus/ucd9000.c @@ -0,0 +1,648 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for UCD90xxx Sequencer and System Health + * Controller series + * + * Copyright (C) 2011 Ericsson AB. + */ + +#include <linux/debugfs.h> +#include <linux/delay.h> +#include <linux/hex.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/pmbus.h> +#include <linux/gpio/driver.h> +#include <linux/timekeeping.h> +#include "pmbus.h" + +enum chips { ucd9000, ucd90120, ucd90124, ucd90160, ucd90320, ucd9090, + ucd90910 }; + +#define UCD9000_MONITOR_CONFIG 0xd5 +#define UCD9000_NUM_PAGES 0xd6 +#define UCD9000_FAN_CONFIG_INDEX 0xe7 +#define UCD9000_FAN_CONFIG 0xe8 +#define UCD9000_MFR_STATUS 0xf3 +#define UCD9000_GPIO_SELECT 0xfa +#define UCD9000_GPIO_CONFIG 0xfb +#define UCD9000_DEVICE_ID 0xfd + +/* GPIO CONFIG bits */ +#define UCD9000_GPIO_CONFIG_ENABLE BIT(0) +#define UCD9000_GPIO_CONFIG_OUT_ENABLE BIT(1) +#define UCD9000_GPIO_CONFIG_OUT_VALUE BIT(2) +#define UCD9000_GPIO_CONFIG_STATUS BIT(3) +#define UCD9000_GPIO_INPUT 0 +#define UCD9000_GPIO_OUTPUT 1 + +#define UCD9000_MON_TYPE(x) (((x) >> 5) & 0x07) +#define UCD9000_MON_PAGE(x) ((x) & 0x1f) + +#define UCD9000_MON_VOLTAGE 1 +#define UCD9000_MON_TEMPERATURE 2 +#define UCD9000_MON_CURRENT 3 +#define UCD9000_MON_VOLTAGE_HW 4 + +#define UCD9000_NUM_FAN 4 + +#define UCD9000_GPIO_NAME_LEN 16 +#define UCD9090_NUM_GPIOS 23 +#define UCD901XX_NUM_GPIOS 26 +#define UCD90320_NUM_GPIOS 84 +#define UCD90910_NUM_GPIOS 26 + +#define UCD9000_DEBUGFS_NAME_LEN 24 +#define UCD9000_GPI_COUNT 8 +#define UCD90320_GPI_COUNT 32 + +struct ucd9000_data { + u8 fan_data[UCD9000_NUM_FAN][I2C_SMBUS_BLOCK_MAX]; + struct pmbus_driver_info info; +#ifdef CONFIG_GPIOLIB + struct gpio_chip gpio; +#endif + struct dentry *debugfs; +}; +#define to_ucd9000_data(_info) container_of(_info, struct ucd9000_data, info) + +struct ucd9000_debugfs_entry { + struct i2c_client *client; + u8 index; +}; + +/* + * It has been observed that the UCD90320 randomly fails register access when + * doing another access right on the back of a register write. To mitigate this + * make sure that there is a minimum delay between a write access and the + * following access. The 500 is based on experimental data. At a delay of + * 350us the issue seems to go away. Add a bit of extra margin to allow for + * system to system differences. + */ +#define UCD90320_WAIT_DELAY_US 500 + +static int ucd9000_get_fan_config(struct i2c_client *client, int fan) +{ + int fan_config = 0; + struct ucd9000_data *data + = to_ucd9000_data(pmbus_get_driver_info(client)); + + if (data->fan_data[fan][3] & 1) + fan_config |= PB_FAN_2_INSTALLED; /* Use lower bit position */ + + /* Pulses/revolution */ + fan_config |= (data->fan_data[fan][3] & 0x06) >> 1; + + return fan_config; +} + +static int ucd9000_read_byte_data(struct i2c_client *client, int page, int reg) +{ + int ret = 0; + int fan_config; + + switch (reg) { + case PMBUS_FAN_CONFIG_12: + if (page > 0) + return -ENXIO; + + ret = ucd9000_get_fan_config(client, 0); + if (ret < 0) + return ret; + fan_config = ret << 4; + ret = ucd9000_get_fan_config(client, 1); + if (ret < 0) + return ret; + fan_config |= ret; + ret = fan_config; + break; + case PMBUS_FAN_CONFIG_34: + if (page > 0) + return -ENXIO; + + ret = ucd9000_get_fan_config(client, 2); + if (ret < 0) + return ret; + fan_config = ret << 4; + ret = ucd9000_get_fan_config(client, 3); + if (ret < 0) + return ret; + fan_config |= ret; + ret = fan_config; + break; + default: + ret = -ENODATA; + break; + } + return ret; +} + +static const struct i2c_device_id ucd9000_id[] = { + { .name = "ucd9000", .driver_data = ucd9000 }, + { .name = "ucd90120", .driver_data = ucd90120 }, + { .name = "ucd90124", .driver_data = ucd90124 }, + { .name = "ucd90160", .driver_data = ucd90160 }, + { .name = "ucd90320", .driver_data = ucd90320 }, + { .name = "ucd9090", .driver_data = ucd9090 }, + { .name = "ucd90910", .driver_data = ucd90910 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ucd9000_id); + +static const struct of_device_id __maybe_unused ucd9000_of_match[] = { + { + .compatible = "ti,ucd9000", + .data = (void *)ucd9000 + }, + { + .compatible = "ti,ucd90120", + .data = (void *)ucd90120 + }, + { + .compatible = "ti,ucd90124", + .data = (void *)ucd90124 + }, + { + .compatible = "ti,ucd90160", + .data = (void *)ucd90160 + }, + { + .compatible = "ti,ucd90320", + .data = (void *)ucd90320 + }, + { + .compatible = "ti,ucd9090", + .data = (void *)ucd9090 + }, + { + .compatible = "ti,ucd90910", + .data = (void *)ucd90910 + }, + { }, +}; +MODULE_DEVICE_TABLE(of, ucd9000_of_match); + +#ifdef CONFIG_GPIOLIB +static int ucd9000_gpio_read_config(struct i2c_client *client, + unsigned int offset) +{ + int ret; + + /* No page set required */ + ret = i2c_smbus_write_byte_data(client, UCD9000_GPIO_SELECT, offset); + if (ret < 0) + return ret; + + return i2c_smbus_read_byte_data(client, UCD9000_GPIO_CONFIG); +} + +static int ucd9000_gpio_get(struct gpio_chip *gc, unsigned int offset) +{ + struct i2c_client *client = gpiochip_get_data(gc); + int ret; + + ret = ucd9000_gpio_read_config(client, offset); + if (ret < 0) + return ret; + + return !!(ret & UCD9000_GPIO_CONFIG_STATUS); +} + +static int ucd9000_gpio_set(struct gpio_chip *gc, unsigned int offset, + int value) +{ + struct i2c_client *client = gpiochip_get_data(gc); + int ret; + + ret = ucd9000_gpio_read_config(client, offset); + if (ret < 0) { + dev_dbg(&client->dev, "failed to read GPIO %d config: %d\n", + offset, ret); + return ret; + } + + if (value) { + if (ret & UCD9000_GPIO_CONFIG_OUT_VALUE) + return 0; + + ret |= UCD9000_GPIO_CONFIG_OUT_VALUE; + } else { + if (!(ret & UCD9000_GPIO_CONFIG_OUT_VALUE)) + return 0; + + ret &= ~UCD9000_GPIO_CONFIG_OUT_VALUE; + } + + ret |= UCD9000_GPIO_CONFIG_ENABLE; + + /* Page set not required */ + ret = i2c_smbus_write_byte_data(client, UCD9000_GPIO_CONFIG, ret); + if (ret < 0) { + dev_dbg(&client->dev, "Failed to write GPIO %d config: %d\n", + offset, ret); + return ret; + } + + ret &= ~UCD9000_GPIO_CONFIG_ENABLE; + + ret = i2c_smbus_write_byte_data(client, UCD9000_GPIO_CONFIG, ret); + if (ret < 0) + dev_dbg(&client->dev, "Failed to write GPIO %d config: %d\n", + offset, ret); + + return ret; +} + +static int ucd9000_gpio_get_direction(struct gpio_chip *gc, + unsigned int offset) +{ + struct i2c_client *client = gpiochip_get_data(gc); + int ret; + + ret = ucd9000_gpio_read_config(client, offset); + if (ret < 0) + return ret; + + return !(ret & UCD9000_GPIO_CONFIG_OUT_ENABLE); +} + +static int ucd9000_gpio_set_direction(struct gpio_chip *gc, + unsigned int offset, bool direction_out, + int requested_out) +{ + struct i2c_client *client = gpiochip_get_data(gc); + int ret, config, out_val; + + ret = ucd9000_gpio_read_config(client, offset); + if (ret < 0) + return ret; + + if (direction_out) { + out_val = requested_out ? UCD9000_GPIO_CONFIG_OUT_VALUE : 0; + + if (ret & UCD9000_GPIO_CONFIG_OUT_ENABLE) { + if ((ret & UCD9000_GPIO_CONFIG_OUT_VALUE) == out_val) + return 0; + } else { + ret |= UCD9000_GPIO_CONFIG_OUT_ENABLE; + } + + if (out_val) + ret |= UCD9000_GPIO_CONFIG_OUT_VALUE; + else + ret &= ~UCD9000_GPIO_CONFIG_OUT_VALUE; + + } else { + if (!(ret & UCD9000_GPIO_CONFIG_OUT_ENABLE)) + return 0; + + ret &= ~UCD9000_GPIO_CONFIG_OUT_ENABLE; + } + + ret |= UCD9000_GPIO_CONFIG_ENABLE; + config = ret; + + /* Page set not required */ + ret = i2c_smbus_write_byte_data(client, UCD9000_GPIO_CONFIG, config); + if (ret < 0) + return ret; + + config &= ~UCD9000_GPIO_CONFIG_ENABLE; + + return i2c_smbus_write_byte_data(client, UCD9000_GPIO_CONFIG, config); +} + +static int ucd9000_gpio_direction_input(struct gpio_chip *gc, + unsigned int offset) +{ + return ucd9000_gpio_set_direction(gc, offset, UCD9000_GPIO_INPUT, 0); +} + +static int ucd9000_gpio_direction_output(struct gpio_chip *gc, + unsigned int offset, int val) +{ + return ucd9000_gpio_set_direction(gc, offset, UCD9000_GPIO_OUTPUT, + val); +} + +static void ucd9000_probe_gpio(struct i2c_client *client, + const struct i2c_device_id *mid, + struct ucd9000_data *data) +{ + int rc; + + switch (mid->driver_data) { + case ucd9090: + data->gpio.ngpio = UCD9090_NUM_GPIOS; + break; + case ucd90120: + case ucd90124: + case ucd90160: + data->gpio.ngpio = UCD901XX_NUM_GPIOS; + break; + case ucd90320: + data->gpio.ngpio = UCD90320_NUM_GPIOS; + break; + case ucd90910: + data->gpio.ngpio = UCD90910_NUM_GPIOS; + break; + default: + return; /* GPIO support is optional. */ + } + + /* + * Pinmux support has not been added to the new gpio_chip. + * This support should be added when possible given the mux + * behavior of these IO devices. + */ + data->gpio.label = client->name; + data->gpio.get_direction = ucd9000_gpio_get_direction; + data->gpio.direction_input = ucd9000_gpio_direction_input; + data->gpio.direction_output = ucd9000_gpio_direction_output; + data->gpio.get = ucd9000_gpio_get; + data->gpio.set = ucd9000_gpio_set; + data->gpio.can_sleep = true; + data->gpio.base = -1; + data->gpio.parent = &client->dev; + + rc = devm_gpiochip_add_data(&client->dev, &data->gpio, client); + if (rc) + dev_warn(&client->dev, "Could not add gpiochip: %d\n", rc); +} +#else +static void ucd9000_probe_gpio(struct i2c_client *client, + const struct i2c_device_id *mid, + struct ucd9000_data *data) +{ +} +#endif /* CONFIG_GPIOLIB */ + +#ifdef CONFIG_DEBUG_FS +static int ucd9000_get_mfr_status(struct i2c_client *client, u8 *buffer) +{ + int ret = pmbus_set_page(client, 0, 0xff); + + if (ret < 0) + return ret; + + return i2c_smbus_read_block_data(client, UCD9000_MFR_STATUS, buffer); +} + +static int ucd9000_debugfs_show_mfr_status_bit(void *data, u64 *val) +{ + struct ucd9000_debugfs_entry *entry = data; + struct i2c_client *client = entry->client; + u8 buffer[I2C_SMBUS_BLOCK_MAX]; + int ret, i; + + ret = ucd9000_get_mfr_status(client, buffer); + if (ret < 0) + return ret; + + /* + * GPI fault bits are in sets of 8, two bytes from end of response. + */ + i = ret - 3 - entry->index / 8; + if (i >= 0) + *val = !!(buffer[i] & BIT(entry->index % 8)); + + return 0; +} +DEFINE_DEBUGFS_ATTRIBUTE(ucd9000_debugfs_mfr_status_bit, + ucd9000_debugfs_show_mfr_status_bit, NULL, "%1lld\n"); + +static ssize_t ucd9000_debugfs_read_mfr_status(struct file *file, + char __user *buf, size_t count, + loff_t *ppos) +{ + struct i2c_client *client = file->private_data; + u8 buffer[I2C_SMBUS_BLOCK_MAX]; + char str[(I2C_SMBUS_BLOCK_MAX * 2) + 2]; + char *res; + int rc; + + rc = ucd9000_get_mfr_status(client, buffer); + if (rc < 0) + return rc; + + res = bin2hex(str, buffer, min(rc, I2C_SMBUS_BLOCK_MAX)); + *res++ = '\n'; + *res = 0; + + return simple_read_from_buffer(buf, count, ppos, str, res - str); +} + +static const struct file_operations ucd9000_debugfs_show_mfr_status_fops = { + .llseek = noop_llseek, + .read = ucd9000_debugfs_read_mfr_status, + .open = simple_open, +}; + +static int ucd9000_init_debugfs(struct i2c_client *client, + const struct i2c_device_id *mid, + struct ucd9000_data *data) +{ + struct dentry *debugfs; + struct ucd9000_debugfs_entry *entries; + int i, gpi_count; + char name[UCD9000_DEBUGFS_NAME_LEN]; + + debugfs = pmbus_get_debugfs_dir(client); + if (!debugfs) + return -ENOENT; + + data->debugfs = debugfs_create_dir(client->name, debugfs); + + /* + * Of the chips this driver supports, only the UCD9090, UCD90160, + * UCD90320, and UCD90910 report GPI faults in their MFR_STATUS + * register, so only create the GPI fault debugfs attributes for those + * chips. + */ + if (mid->driver_data == ucd9090 || mid->driver_data == ucd90160 || + mid->driver_data == ucd90320 || mid->driver_data == ucd90910) { + gpi_count = mid->driver_data == ucd90320 ? UCD90320_GPI_COUNT + : UCD9000_GPI_COUNT; + entries = devm_kcalloc(&client->dev, + gpi_count, sizeof(*entries), + GFP_KERNEL); + if (!entries) + return -ENOMEM; + + for (i = 0; i < gpi_count; i++) { + entries[i].client = client; + entries[i].index = i; + scnprintf(name, UCD9000_DEBUGFS_NAME_LEN, + "gpi%d_alarm", i + 1); + debugfs_create_file(name, 0444, data->debugfs, + &entries[i], + &ucd9000_debugfs_mfr_status_bit); + } + } + + scnprintf(name, UCD9000_DEBUGFS_NAME_LEN, "mfr_status"); + debugfs_create_file(name, 0444, data->debugfs, client, + &ucd9000_debugfs_show_mfr_status_fops); + + return 0; +} +#else +static int ucd9000_init_debugfs(struct i2c_client *client, + const struct i2c_device_id *mid, + struct ucd9000_data *data) +{ + return 0; +} +#endif /* CONFIG_DEBUG_FS */ + +static int ucd9000_probe(struct i2c_client *client) +{ + u8 block_buffer[I2C_SMBUS_BLOCK_MAX + 1]; + struct ucd9000_data *data; + struct pmbus_driver_info *info; + const struct i2c_device_id *mid; + enum chips chip; + int i, ret; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_BYTE_DATA | + I2C_FUNC_SMBUS_BLOCK_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_block_data(client, UCD9000_DEVICE_ID, + block_buffer); + if (ret < 0) { + dev_err(&client->dev, "Failed to read device ID\n"); + return ret; + } + block_buffer[ret] = '\0'; + dev_info(&client->dev, "Device ID %s\n", block_buffer); + + for (mid = ucd9000_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, block_buffer, strlen(mid->name))) + break; + } + if (!mid->name[0]) { + dev_err(&client->dev, "Unsupported device\n"); + return -ENODEV; + } + + if (client->dev.of_node) + chip = (uintptr_t)of_device_get_match_data(&client->dev); + else + chip = mid->driver_data; + + if (chip != ucd9000 && strcmp(client->name, mid->name) != 0) + dev_notice(&client->dev, + "Device mismatch: Configured %s, detected %s\n", + client->name, mid->name); + + data = devm_kzalloc(&client->dev, sizeof(struct ucd9000_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + info = &data->info; + + ret = i2c_smbus_read_byte_data(client, UCD9000_NUM_PAGES); + if (ret < 0) { + dev_err(&client->dev, + "Failed to read number of active pages\n"); + return ret; + } + info->pages = ret; + if (!info->pages) { + dev_err(&client->dev, "No pages configured\n"); + return -ENODEV; + } + + /* The internal temperature sensor is always active */ + info->func[0] = PMBUS_HAVE_TEMP; + + /* Everything else is configurable */ + ret = i2c_smbus_read_block_data(client, UCD9000_MONITOR_CONFIG, + block_buffer); + if (ret <= 0) { + dev_err(&client->dev, "Failed to read configuration data\n"); + return -ENODEV; + } + for (i = 0; i < ret; i++) { + int page = UCD9000_MON_PAGE(block_buffer[i]); + + if (page >= info->pages) + continue; + + switch (UCD9000_MON_TYPE(block_buffer[i])) { + case UCD9000_MON_VOLTAGE: + case UCD9000_MON_VOLTAGE_HW: + info->func[page] |= PMBUS_HAVE_VOUT + | PMBUS_HAVE_STATUS_VOUT; + break; + case UCD9000_MON_TEMPERATURE: + info->func[page] |= PMBUS_HAVE_TEMP2 + | PMBUS_HAVE_STATUS_TEMP; + break; + case UCD9000_MON_CURRENT: + info->func[page] |= PMBUS_HAVE_IOUT + | PMBUS_HAVE_STATUS_IOUT; + break; + default: + break; + } + } + + /* Fan configuration */ + if (mid->driver_data == ucd90124) { + for (i = 0; i < UCD9000_NUM_FAN; i++) { + i2c_smbus_write_byte_data(client, + UCD9000_FAN_CONFIG_INDEX, i); + ret = i2c_smbus_read_block_data(client, + UCD9000_FAN_CONFIG, + data->fan_data[i]); + if (ret < 0) + return ret; + } + i2c_smbus_write_byte_data(client, UCD9000_FAN_CONFIG_INDEX, 0); + + info->read_byte_data = ucd9000_read_byte_data; + info->func[0] |= PMBUS_HAVE_FAN12 | PMBUS_HAVE_STATUS_FAN12 + | PMBUS_HAVE_FAN34 | PMBUS_HAVE_STATUS_FAN34; + } else if (mid->driver_data == ucd90320) { + /* Delay SMBus operations after a write */ + info->write_delay = UCD90320_WAIT_DELAY_US; + } + + ucd9000_probe_gpio(client, mid, data); + + ret = pmbus_do_probe(client, info); + if (ret) + return ret; + + ret = ucd9000_init_debugfs(client, mid, data); + if (ret) + dev_warn(&client->dev, "Failed to register debugfs: %d\n", + ret); + + return 0; +} + +/* This is the driver that will be inserted */ +static struct i2c_driver ucd9000_driver = { + .driver = { + .name = "ucd9000", + .of_match_table = of_match_ptr(ucd9000_of_match), + }, + .probe = ucd9000_probe, + .id_table = ucd9000_id, +}; + +module_i2c_driver(ucd9000_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for TI UCD90xxx"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/ucd9200.c b/drivers/hwmon/pmbus/ucd9200.c new file mode 100644 index 000000000..5e07bba11 --- /dev/null +++ b/drivers/hwmon/pmbus/ucd9200.c @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for ucd9200 series Digital PWM System Controllers + * + * Copyright (C) 2011 Ericsson AB. + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/pmbus.h> +#include "pmbus.h" + +#define UCD9200_PHASE_INFO 0xd2 +#define UCD9200_DEVICE_ID 0xfd + +enum chips { ucd9200, ucd9220, ucd9222, ucd9224, ucd9240, ucd9244, ucd9246, + ucd9248 }; + +static const struct i2c_device_id ucd9200_id[] = { + { .name = "ucd9200", .driver_data = ucd9200 }, + { .name = "ucd9220", .driver_data = ucd9220 }, + { .name = "ucd9222", .driver_data = ucd9222 }, + { .name = "ucd9224", .driver_data = ucd9224 }, + { .name = "ucd9240", .driver_data = ucd9240 }, + { .name = "ucd9244", .driver_data = ucd9244 }, + { .name = "ucd9246", .driver_data = ucd9246 }, + { .name = "ucd9248", .driver_data = ucd9248 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ucd9200_id); + +static const struct of_device_id __maybe_unused ucd9200_of_match[] = { + { + .compatible = "ti,cd9200", + .data = (void *)ucd9200 + }, + { + .compatible = "ti,cd9220", + .data = (void *)ucd9220 + }, + { + .compatible = "ti,cd9222", + .data = (void *)ucd9222 + }, + { + .compatible = "ti,cd9224", + .data = (void *)ucd9224 + }, + { + .compatible = "ti,cd9240", + .data = (void *)ucd9240 + }, + { + .compatible = "ti,cd9244", + .data = (void *)ucd9244 + }, + { + .compatible = "ti,cd9246", + .data = (void *)ucd9246 + }, + { + .compatible = "ti,cd9248", + .data = (void *)ucd9248 + }, + { }, +}; +MODULE_DEVICE_TABLE(of, ucd9200_of_match); + +static int ucd9200_probe(struct i2c_client *client) +{ + u8 block_buffer[I2C_SMBUS_BLOCK_MAX + 1]; + struct pmbus_driver_info *info; + const struct i2c_device_id *mid; + enum chips chip; + int i, j, ret; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_BYTE_DATA | + I2C_FUNC_SMBUS_BLOCK_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_block_data(client, UCD9200_DEVICE_ID, + block_buffer); + if (ret < 0) { + dev_err(&client->dev, "Failed to read device ID\n"); + return ret; + } + block_buffer[ret] = '\0'; + dev_info(&client->dev, "Device ID %s\n", block_buffer); + + for (mid = ucd9200_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, block_buffer, strlen(mid->name))) + break; + } + if (!mid->name[0]) { + dev_err(&client->dev, "Unsupported device\n"); + return -ENODEV; + } + + if (client->dev.of_node) + chip = (uintptr_t)of_device_get_match_data(&client->dev); + else + chip = mid->driver_data; + + if (chip != ucd9200 && strcmp(client->name, mid->name) != 0) + dev_notice(&client->dev, + "Device mismatch: Configured %s, detected %s\n", + client->name, mid->name); + + info = devm_kzalloc(&client->dev, sizeof(struct pmbus_driver_info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + ret = i2c_smbus_read_block_data(client, UCD9200_PHASE_INFO, + block_buffer); + if (ret < 0) { + dev_err(&client->dev, "Failed to read phase information\n"); + return ret; + } + + /* + * Calculate number of configured pages (rails) from PHASE_INFO + * register. + * Rails have to be sequential, so we can abort after finding + * the first unconfigured rail. + */ + info->pages = 0; + for (i = 0; i < ret; i++) { + if (!block_buffer[i]) + break; + info->pages++; + } + if (!info->pages) { + dev_err(&client->dev, "No rails configured\n"); + return -ENODEV; + } + dev_info(&client->dev, "%d rails configured\n", info->pages); + + /* + * Set PHASE registers on all pages to 0xff to ensure that phase + * specific commands will apply to all phases of a given page (rail). + * This only affects the READ_IOUT and READ_TEMPERATURE2 registers. + * READ_IOUT will return the sum of currents of all phases of a rail, + * and READ_TEMPERATURE2 will return the maximum temperature detected + * for the phases of the rail. + */ + for (i = 0; i < info->pages; i++) { + /* + * Setting PAGE & PHASE fails once in a while for no obvious + * reason, so we need to retry a couple of times. + */ + for (j = 0; j < 3; j++) { + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, i); + if (ret < 0) + continue; + ret = i2c_smbus_write_byte_data(client, PMBUS_PHASE, + 0xff); + if (ret < 0) + continue; + break; + } + if (ret < 0) { + dev_err(&client->dev, + "Failed to initialize PHASE registers\n"); + return ret; + } + } + if (info->pages > 1) + i2c_smbus_write_byte_data(client, PMBUS_PAGE, 0); + + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_PIN | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_POUT | PMBUS_HAVE_TEMP | + PMBUS_HAVE_TEMP2 | PMBUS_HAVE_STATUS_TEMP; + + for (i = 1; i < info->pages; i++) + info->func[i] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_POUT | + PMBUS_HAVE_TEMP2 | PMBUS_HAVE_STATUS_TEMP; + + /* ucd9240 supports a single fan */ + if (mid->driver_data == ucd9240) + info->func[0] |= PMBUS_HAVE_FAN12 | PMBUS_HAVE_STATUS_FAN12; + + return pmbus_do_probe(client, info); +} + +/* This is the driver that will be inserted */ +static struct i2c_driver ucd9200_driver = { + .driver = { + .name = "ucd9200", + .of_match_table = of_match_ptr(ucd9200_of_match), + }, + .probe = ucd9200_probe, + .id_table = ucd9200_id, +}; + +module_i2c_driver(ucd9200_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for TI UCD922x, UCD924x"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/vt7505.c b/drivers/hwmon/pmbus/vt7505.c new file mode 100644 index 000000000..6a66f06ed --- /dev/null +++ b/drivers/hwmon/pmbus/vt7505.c @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Analog Devices MAX16545/MAX16550 and + * Volterra VT7505 PMBus controllers. + * + * Copyright 2026 Hewlett Packard Enterprise Development LP + */ + +#include <linux/bitfield.h> +#include <linux/i2c.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/pmbus.h> + +#include "pmbus.h" + +#define VT7505_MFR_CONFIG 0xd0 +#define VT7505_CFG_OCP_S_FILT_MASK GENMASK(15, 14) + +#define VT7505_MFR_PEAK_VIN 0xd1 +#define VT7505_MFR_PEAK_IOUT 0xd2 +#define VT7505_MFR_PEAK_PIN 0xd3 +#define VT7505_MFR_PEAK_TEMP 0xd4 +#define VT7505_MFR_CLEAR_PEAKS 0xd5 +#define VT7505_MFR_PEAK_VOUT 0xfd + +#define VT7505_RLOAD_DEFAULT 4750 +/* Largest RLOAD for which coeff * rload / 1000 still fits in s32. */ +#define VT7505_RLOAD_MAX 283383959U + +struct vt7505_chip_data { + int temp_m; + int temp_b; + bool has_ocp_filter; +}; + +static const struct vt7505_chip_data max16545_data = { + .temp_m = 205, + .temp_b = 6545, +}; + +static const struct vt7505_chip_data max16550_data = { + .temp_m = 199, + .temp_b = 7046, + .has_ocp_filter = true, +}; + +static const struct vt7505_chip_data vt7505_data = { + .temp_m = 205, + .temp_b = 6545, + .has_ocp_filter = true, +}; + +static int vt7505_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + switch (reg) { + case PMBUS_VIRT_READ_VIN_MAX: + return pmbus_read_word_data(client, page, phase, + VT7505_MFR_PEAK_VIN); + case PMBUS_VIRT_READ_IOUT_MAX: + return pmbus_read_word_data(client, page, phase, + VT7505_MFR_PEAK_IOUT); + case PMBUS_VIRT_READ_PIN_MAX: + return pmbus_read_word_data(client, page, phase, + VT7505_MFR_PEAK_PIN); + case PMBUS_VIRT_READ_TEMP_MAX: + return pmbus_read_word_data(client, page, phase, + VT7505_MFR_PEAK_TEMP); + case PMBUS_VIRT_READ_VOUT_MAX: + return pmbus_read_word_data(client, page, phase, + VT7505_MFR_PEAK_VOUT); + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_RESET_PIN_HISTORY: + case PMBUS_VIRT_RESET_TEMP_HISTORY: + case PMBUS_VIRT_RESET_VOUT_HISTORY: + return 0; + default: + return -ENODATA; + } +} + +static int vt7505_write_word_data(struct i2c_client *client, int page, + int reg, u16 word) +{ + switch (reg) { + /* + * A single reset command clears all peak values. CLEAR_PEAKS is a + * send-byte command; the device NAKs a word or byte-data write to it. + */ + case PMBUS_VIRT_RESET_VIN_HISTORY: + case PMBUS_VIRT_RESET_IOUT_HISTORY: + case PMBUS_VIRT_RESET_PIN_HISTORY: + case PMBUS_VIRT_RESET_TEMP_HISTORY: + case PMBUS_VIRT_RESET_VOUT_HISTORY: + return pmbus_write_byte(client, page, + VT7505_MFR_CLEAR_PEAKS); + default: + return -ENODATA; + } +} + +/* + * None of these controllers implement the standard PMBus WRITE_PROTECT + * (0x10) register, so tell the core not to access it. + */ +static struct pmbus_platform_data vt7505_pdata = { + .flags = PMBUS_NO_WRITE_PROTECT, +}; + +static int vt7505_set_ocp_filter(struct i2c_client *client) +{ + u32 ocp_us; + u8 field; + int ret; + u16 word; + + if (of_property_read_u32(client->dev.of_node, "adi,ocp-severe-filter-us", + &ocp_us)) + return 0; + + switch (ocp_us) { + case 0: + field = 0; + break; + case 1: + field = 1; + break; + case 2: + field = 2; + break; + case 10: + field = 3; + break; + default: + return dev_err_probe(&client->dev, -EINVAL, + "invalid adi,ocp-severe-filter-us value %u\n", + ocp_us); + } + + ret = i2c_smbus_read_word_data(client, VT7505_MFR_CONFIG); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "failed to read MFR_CONFIG\n"); + + word = ret & ~VT7505_CFG_OCP_S_FILT_MASK; + word |= FIELD_PREP(VT7505_CFG_OCP_S_FILT_MASK, field); + + ret = i2c_smbus_write_word_data(client, VT7505_MFR_CONFIG, word); + if (ret < 0) + return dev_err_probe(&client->dev, ret, + "failed to write MFR_CONFIG\n"); + + return 0; +} + +static void vt7505_set_m(int *m, u32 rload) +{ + u64 val = (u64)*m * rload; + + /* rload is range-checked in probe, so the result fits in int. */ + *m = DIV_ROUND_CLOSEST_ULL(val, 1000); +} + +static int vt7505_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + const struct vt7505_chip_data *chip; + struct pmbus_driver_info *info; + u32 rload; + int ret; + + chip = i2c_get_match_data(client); + if (!chip) + return -ENODEV; + + info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL); + if (!info) + return -ENOMEM; + + dev->platform_data = &vt7505_pdata; + + /* + * The m coefficient used in the direct-format current and power + * calculations depends on RLOAD, the external current-report resistor + * connected between the ILOAD pin and ground. Use the default value if + * none is specified. + */ + if (of_property_read_u32(dev->of_node, "adi,rload-ohms", &rload)) + rload = VT7505_RLOAD_DEFAULT; + + if (!rload || rload > VT7505_RLOAD_MAX) + return dev_err_probe(dev, -EINVAL, + "adi,rload-ohms must be 1-%u\n", + VT7505_RLOAD_MAX); + + info->pages = 1; + info->read_word_data = vt7505_read_word_data; + info->write_word_data = vt7505_write_word_data; + + info->format[PSC_VOLTAGE_IN] = direct; + info->format[PSC_VOLTAGE_OUT] = direct; + info->format[PSC_CURRENT_IN] = direct; + info->format[PSC_CURRENT_OUT] = direct; + info->format[PSC_POWER] = direct; + info->format[PSC_TEMPERATURE] = direct; + + /* + * Direct data format coefficients from the device datasheet ("PMBus + * Equation Parameters"). The current and power m coefficients scale + * with RLOAD; vt7505_set_m() applies the 1/1000 factor below, giving + * m = 3.824 * RLOAD for current and 0.895 * RLOAD for power. The + * temperature coefficients are chip specific (see the chip data). + */ + info->m[PSC_VOLTAGE_IN] = 7578; + info->R[PSC_VOLTAGE_IN] = -2; + info->m[PSC_VOLTAGE_OUT] = 7578; + info->R[PSC_VOLTAGE_OUT] = -2; + info->m[PSC_CURRENT_IN] = 3824; + info->b[PSC_CURRENT_IN] = -4300; + info->R[PSC_CURRENT_IN] = -3; + info->m[PSC_CURRENT_OUT] = 3824; + info->b[PSC_CURRENT_OUT] = -4300; + info->R[PSC_CURRENT_OUT] = -3; + info->m[PSC_POWER] = 895; + info->b[PSC_POWER] = -9100; + info->R[PSC_POWER] = -2; + + vt7505_set_m(&info->m[PSC_CURRENT_IN], rload); + vt7505_set_m(&info->m[PSC_CURRENT_OUT], rload); + vt7505_set_m(&info->m[PSC_POWER], rload); + + info->m[PSC_TEMPERATURE] = chip->temp_m; + info->b[PSC_TEMPERATURE] = chip->temp_b; + info->R[PSC_TEMPERATURE] = -2; + + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_IIN | PMBUS_HAVE_PIN; + + /* + * The severe OCP deglitch filter is programmable on the MAX16550 and + * the VT7505, but fixed on the MAX16545. + */ + if (chip->has_ocp_filter) { + ret = vt7505_set_ocp_filter(client); + if (ret) + return ret; + } + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id vt7505_id[] = { + { .name = "max16545", .driver_data = (kernel_ulong_t)&max16545_data }, + { .name = "max16550", .driver_data = (kernel_ulong_t)&max16550_data }, + { .name = "vt7505", .driver_data = (kernel_ulong_t)&vt7505_data }, + { } +}; +MODULE_DEVICE_TABLE(i2c, vt7505_id); + +static const struct of_device_id vt7505_of_match[] = { + { .compatible = "adi,max16545", .data = &max16545_data }, + { .compatible = "adi,max16550", .data = &max16550_data }, + { .compatible = "adi,vt7505", .data = &vt7505_data }, + { } +}; +MODULE_DEVICE_TABLE(of, vt7505_of_match); + +static struct i2c_driver vt7505_driver = { + .driver = { + .name = "vt7505", + .of_match_table = vt7505_of_match, + }, + .probe = vt7505_probe, + .id_table = vt7505_id, +}; +module_i2c_driver(vt7505_driver); + +MODULE_AUTHOR("Georgi Vlaev <gvlaev@juniper.net>"); +MODULE_DESCRIPTION("PMBus driver for Analog Devices MAX16545/MAX16550 and Volterra VT7505"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/xdp710.c b/drivers/hwmon/pmbus/xdp710.c new file mode 100644 index 000000000..494dbe45e --- /dev/null +++ b/drivers/hwmon/pmbus/xdp710.c @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Driver for Infineon XDP710 Hot-Swap Controller + */ + +#include <linux/bitops.h> +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/of_device.h> +#include "pmbus.h" + +#define XDP710_REG_CFG 0xD3 +#define XDP710_V_SNS_CFG 0xD4 +#define XDP710_CS_RNG 0xD5 + +/* + * The table to map configuration register values + * to sense resistor values + */ +static const int micro_ohm_rsense[] = { + 200, 250, 300, 330, 400, 470, 500, 600, + 670, 700, 750, 800, 900, 1000, 1100, 1200, + 1250, 1300, 1400, 1500, 1600, 1700, 1800, 1900, + 2000, 2100, 2200, 2300, 2400, 2500, 2600, 2700, + 2800, 3000, 3100, 3200, 3300, 3400, 3500, 3600, + 3700, 3800, 3900, 4000, 4100, 4200, 4300, 4400, + 4500, 4600, 4700, 4800, 4900, 5000, 5500, 6000, + 6500, 7000, 7500, 8000, 8500, 9000, 9500, 10000 +}; + +static struct pmbus_driver_info xdp710_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + .m[PSC_VOLTAGE_IN] = 4653, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -2, + .m[PSC_VOLTAGE_OUT] = 4653, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = -2, + .m[PSC_CURRENT_OUT] = 23165, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = -2, + .m[PSC_POWER] = 4211, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = -2, + .m[PSC_TEMPERATURE] = 52, + .b[PSC_TEMPERATURE] = 14321, + .R[PSC_TEMPERATURE] = -1, + .func[0] = + PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_PIN | + PMBUS_HAVE_TEMP | PMBUS_HAVE_IOUT | + PMBUS_HAVE_STATUS_INPUT | PMBUS_HAVE_STATUS_TEMP, +}; + +static int xdp710_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + u8 cs_rng; + u8 vtlm_rng; + int rsense; + int ret; + int m = 0; + + info = devm_kmemdup(&client->dev, &xdp710_info, sizeof(*info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + ret = i2c_smbus_read_word_data(client, XDP710_CS_RNG); + if (ret < 0) { + dev_err(&client->dev, "Can't get CS_RNG"); + return ret; + } + cs_rng = (ret >> 6) & GENMASK(1, 0); + + ret = i2c_smbus_read_word_data(client, XDP710_V_SNS_CFG); + if (ret < 0) { + dev_err(&client->dev, "Can't get V_SNS_CFG"); + return ret; + } + vtlm_rng = ret & GENMASK(1, 0); + + ret = i2c_smbus_read_word_data(client, XDP710_REG_CFG); + if (ret < 0) { + dev_err(&client->dev, "Can't get REG_CFG"); + return ret; + } + ret &= GENMASK(5, 0); + rsense = micro_ohm_rsense[ret]; + + info->m[PSC_VOLTAGE_IN] <<= vtlm_rng; + info->m[PSC_VOLTAGE_OUT] <<= vtlm_rng; + + m = info->m[PSC_CURRENT_OUT]; + info->m[PSC_CURRENT_OUT] = DIV_ROUND_CLOSEST(m * rsense >> cs_rng, 1000); + + m = info->m[PSC_POWER]; + info->m[PSC_POWER] = DIV_ROUND_CLOSEST(m * rsense >> cs_rng, 1000); + + return pmbus_do_probe(client, info); +} + +static const struct of_device_id xdp710_of_match[] = { + { .compatible = "infineon,xdp710" }, + {} +}; + +static const struct i2c_device_id xdp710_id[] = { + { .name = "xdp710" }, + { } +}; +MODULE_DEVICE_TABLE(i2c, xdp710_id); + +static struct i2c_driver xdp710_driver = { + .driver = { + .name = "xdp710", + .of_match_table = xdp710_of_match, + }, + .probe = xdp710_probe, + .id_table = xdp710_id, +}; +module_i2c_driver(xdp710_driver); + +MODULE_AUTHOR("Peter Yin <peter.yin@quantatw.com>"); +MODULE_DESCRIPTION("PMBus driver for XDP710 HSC"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/xdp720.c b/drivers/hwmon/pmbus/xdp720.c new file mode 100644 index 000000000..5a162084f --- /dev/null +++ b/drivers/hwmon/pmbus/xdp720.c @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Infineon XDP720 / XDP730 Digital + * eFuse Controllers. + * + * Both parts share the same PMBus register map and direct-format + * coefficients; they differ in the GIMON gain step exposed via + * the TELEMETRY_AVG register and in the VDD_VIN pin number. + * + * Copyright (c) 2026 Infineon Technologies. All rights reserved. + */ + +#include <linux/i2c.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/of_device.h> +#include <linux/bitops.h> +#include <linux/math64.h> +#include <linux/property.h> +#include <linux/regulator/consumer.h> +#include "pmbus.h" + +/* + * The IMON resistor required to generate the system overcurrent protection. + * Arbitrary default Rimon value: 2k Ohm + */ +#define XDP720_DEFAULT_RIMON 2000000000U /* 2k ohm */ +#define XDP720_TELEMETRY_AVG 0xE9 +#define XDP720_TELEMETRY_AVG_GIMON BIT(10) /* high/low GIMON select */ + +/* Chip identifiers carried in OF match-data and i2c_device_id->driver_data. */ +enum xdp720_chip_id { + CHIP_XDP720 = 0, + CHIP_XDP730, +}; + +struct xdp720_data { + enum xdp720_chip_id id; + struct pmbus_driver_info info; +}; + +static const struct pmbus_driver_info xdp720_info = { + .pages = 1, + .format[PSC_VOLTAGE_IN] = direct, + .format[PSC_VOLTAGE_OUT] = direct, + .format[PSC_CURRENT_OUT] = direct, + .format[PSC_POWER] = direct, + .format[PSC_TEMPERATURE] = direct, + + .m[PSC_VOLTAGE_IN] = 4653, + .b[PSC_VOLTAGE_IN] = 0, + .R[PSC_VOLTAGE_IN] = -2, + .m[PSC_VOLTAGE_OUT] = 4653, + .b[PSC_VOLTAGE_OUT] = 0, + .R[PSC_VOLTAGE_OUT] = -2, + /* + * Current and Power measurement depends on the RIMON (kOhm) and + * GIMON(microA/A) values. + */ + .m[PSC_CURRENT_OUT] = 24668, + .b[PSC_CURRENT_OUT] = 0, + .R[PSC_CURRENT_OUT] = -4, + .m[PSC_POWER] = 4486, + .b[PSC_POWER] = 0, + .R[PSC_POWER] = -1, + .m[PSC_TEMPERATURE] = 54, + .b[PSC_TEMPERATURE] = 22521, + .R[PSC_TEMPERATURE] = -1, + + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_PIN | + PMBUS_HAVE_TEMP | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_INPUT | + PMBUS_HAVE_STATUS_TEMP, +}; + +static int xdp720_probe(struct i2c_client *client) +{ + struct xdp720_data *data; + int ret; + u32 rimon; + int gimon; + + data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->id = (enum xdp720_chip_id)(uintptr_t)i2c_get_match_data(client); + data->info = xdp720_info; + + ret = devm_regulator_get_enable(&client->dev, "vdd-vin"); + if (ret) + return dev_err_probe(&client->dev, ret, + "failed to enable vdd-vin supply\n"); + + ret = i2c_smbus_read_word_data(client, XDP720_TELEMETRY_AVG); + if (ret < 0) { + dev_err(&client->dev, "Can't get TELEMETRY_AVG\n"); + return ret; + } + + /* Bit 10 of TELEMETRY_AVG selects the GIMON gain step in microA/A */ + switch (data->id) { + case CHIP_XDP720: + gimon = (ret & XDP720_TELEMETRY_AVG_GIMON) ? 18200 : 9100; + dev_info(&client->dev, "Initialised XDP720 instance\n"); + break; + case CHIP_XDP730: + gimon = (ret & XDP720_TELEMETRY_AVG_GIMON) ? 20000 : 10000; + dev_info(&client->dev, "Initialised XDP730 instance\n"); + break; + default: + return -EINVAL; + } + + if (device_property_read_u32(&client->dev, + "infineon,rimon-micro-ohms", &rimon)) + rimon = XDP720_DEFAULT_RIMON; /* Default if not in FW */ + if (rimon == 0) + return -EINVAL; + + /* Adapt the current and power scale for each instance */ + data->info.m[PSC_CURRENT_OUT] = DIV64_U64_ROUND_CLOSEST((u64) + data->info.m[PSC_CURRENT_OUT] * rimon * gimon, + 1000000000000ULL); + data->info.m[PSC_POWER] = DIV64_U64_ROUND_CLOSEST((u64) + data->info.m[PSC_POWER] * rimon * gimon, + 1000000000000000ULL); + + return pmbus_do_probe(client, &data->info); +} + +static const struct of_device_id xdp720_of_match[] = { + { .compatible = "infineon,xdp720", .data = (void *)CHIP_XDP720 }, + { .compatible = "infineon,xdp730", .data = (void *)CHIP_XDP730 }, + {} +}; +MODULE_DEVICE_TABLE(of, xdp720_of_match); + +static const struct i2c_device_id xdp720_id[] = { + { .name = "xdp720", .driver_data = CHIP_XDP720 }, + { .name = "xdp730", .driver_data = CHIP_XDP730 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, xdp720_id); + +static struct i2c_driver xdp720_driver = { + .driver = { + .name = "xdp720", + .of_match_table = xdp720_of_match, + }, + .probe = xdp720_probe, + .id_table = xdp720_id, +}; + +module_i2c_driver(xdp720_driver); + +MODULE_AUTHOR("Ashish Yadav <ashish.yadav@infineon.com>"); +MODULE_DESCRIPTION("PMBus driver for Infineon XDP720/XDP730 Digital eFuse Controllers"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/xdpe12284.c b/drivers/hwmon/pmbus/xdpe12284.c new file mode 100644 index 000000000..ed93e17ce --- /dev/null +++ b/drivers/hwmon/pmbus/xdpe12284.c @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Infineon Multi-phase Digital VR Controllers + * + * Copyright (c) 2020 Mellanox Technologies. All rights reserved. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/regulator/driver.h> + +#include "pmbus.h" + +#define XDPE122_PROT_VR12_5MV 0x01 /* VR12.0 mode, 5-mV DAC */ +#define XDPE122_PROT_VR12_5_10MV 0x02 /* VR12.5 mode, 10-mV DAC */ +#define XDPE122_PROT_IMVP9_10MV 0x03 /* IMVP9 mode, 10-mV DAC */ +#define XDPE122_AMD_625MV 0x10 /* AMD mode 6.25mV */ +#define XDPE122_PAGE_NUM 2 + +static int xdpe122_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + long val; + s16 exponent; + s32 mantissa; + int ret; + + switch (reg) { + case PMBUS_VOUT_OV_FAULT_LIMIT: + case PMBUS_VOUT_UV_FAULT_LIMIT: + ret = pmbus_read_word_data(client, page, phase, reg); + if (ret < 0) + return ret; + + /* Convert register value to LINEAR11 data. */ + exponent = ((s16)ret) >> 11; + mantissa = ((s16)((ret & GENMASK(10, 0)) << 5)) >> 5; + val = mantissa * 1000L; + if (exponent >= 0) + val <<= exponent; + else + val >>= -exponent; + + /* Convert data to VID register. */ + switch (info->vrm_version[page]) { + case vr13: + if (val >= 500) + return 1 + DIV_ROUND_CLOSEST(val - 500, 10); + return 0; + case vr12: + if (val >= 250) + return 1 + DIV_ROUND_CLOSEST(val - 250, 5); + return 0; + case imvp9: + if (val >= 200) + return 1 + DIV_ROUND_CLOSEST(val - 200, 10); + return 0; + case amd625mv: + if (val >= 200 && val <= 1550) + return DIV_ROUND_CLOSEST((1550 - val) * 100, + 625); + return 0; + default: + return -EINVAL; + } + default: + return -ENODATA; + } + + return 0; +} + +static int xdpe122_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + u8 vout_params; + int i, ret, vout_mode; + + vout_mode = pmbus_read_byte_data(client, 0, PMBUS_VOUT_MODE); + if (vout_mode >= 0 && vout_mode != 0xff) { + switch (vout_mode >> 5) { + case 0: + info->format[PSC_VOLTAGE_OUT] = linear; + return 0; + case 1: + info->format[PSC_VOLTAGE_OUT] = vid; + info->read_word_data = xdpe122_read_word_data; + break; + default: + return -ENODEV; + } + } + + for (i = 0; i < XDPE122_PAGE_NUM; i++) { + /* Read the register with VOUT scaling value.*/ + ret = pmbus_read_byte_data(client, i, PMBUS_VOUT_MODE); + if (ret < 0) + return ret; + + vout_params = ret & GENMASK(4, 0); + + switch (vout_params) { + case XDPE122_PROT_VR12_5_10MV: + info->vrm_version[i] = vr13; + break; + case XDPE122_PROT_VR12_5MV: + info->vrm_version[i] = vr12; + break; + case XDPE122_PROT_IMVP9_10MV: + info->vrm_version[i] = imvp9; + break; + case XDPE122_AMD_625MV: + info->vrm_version[i] = amd625mv; + break; + default: + return -EINVAL; + } + } + + return 0; +} + +static const struct regulator_desc __maybe_unused xdpe122_reg_desc[] = { + PMBUS_REGULATOR("vout", 0), + PMBUS_REGULATOR("vout", 1), +}; + +static struct pmbus_driver_info xdpe122_info = { + .pages = XDPE122_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_POUT | PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT, + .func[1] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_POUT | PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT, + .identify = xdpe122_identify, +#if IS_ENABLED(CONFIG_SENSORS_XDPE122_REGULATOR) + .num_regulators = 2, + .reg_desc = xdpe122_reg_desc, +#endif +}; + +static int xdpe122_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + + info = devm_kmemdup(&client->dev, &xdpe122_info, sizeof(*info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id xdpe122_id[] = { + { .name = "xdpe11280" }, + { .name = "xdpe12254" }, + { .name = "xdpe12284" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, xdpe122_id); + +static const struct of_device_id __maybe_unused xdpe122_of_match[] = { + {.compatible = "infineon,xdpe11280"}, + {.compatible = "infineon,xdpe12254"}, + {.compatible = "infineon,xdpe12284"}, + {} +}; +MODULE_DEVICE_TABLE(of, xdpe122_of_match); + +static struct i2c_driver xdpe122_driver = { + .driver = { + .name = "xdpe12284", + .of_match_table = of_match_ptr(xdpe122_of_match), + }, + .probe = xdpe122_probe, + .id_table = xdpe122_id, +}; + +module_i2c_driver(xdpe122_driver); + +MODULE_AUTHOR("Vadim Pasternak <vadimp@mellanox.com>"); +MODULE_DESCRIPTION("PMBus driver for Infineon XDPE122 family"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/xdpe152c4.c b/drivers/hwmon/pmbus/xdpe152c4.c new file mode 100644 index 000000000..b557d2971 --- /dev/null +++ b/drivers/hwmon/pmbus/xdpe152c4.c @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for Infineon Multi-phase Digital VR Controllers + * + * Copyright (c) 2022 Infineon Technologies. All rights reserved. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define XDPE152_PAGE_NUM 2 + +static struct pmbus_driver_info xdpe152_info = { + .pages = XDPE152_PAGE_NUM, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_VOLTAGE_OUT] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_POUT | PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT, + .func[1] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_POUT | PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT, +}; + +static int xdpe152_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + + info = devm_kmemdup(&client->dev, &xdpe152_info, sizeof(*info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id xdpe152_id[] = { + { .name = "xdpe152c4" }, + { .name = "xdpe15284" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, xdpe152_id); + +static const struct of_device_id __maybe_unused xdpe152_of_match[] = { + {.compatible = "infineon,xdpe152c4"}, + {.compatible = "infineon,xdpe15284"}, + {} +}; +MODULE_DEVICE_TABLE(of, xdpe152_of_match); + +static struct i2c_driver xdpe152_driver = { + .driver = { + .name = "xdpe152c4", + .of_match_table = of_match_ptr(xdpe152_of_match), + }, + .probe = xdpe152_probe, + .id_table = xdpe152_id, +}; + +module_i2c_driver(xdpe152_driver); + +MODULE_AUTHOR("Greg Schwendimann <greg.schwendimann@infineon.com>"); +MODULE_DESCRIPTION("PMBus driver for Infineon XDPE152 family"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/xdpe1a2g7b.c b/drivers/hwmon/pmbus/xdpe1a2g7b.c new file mode 100644 index 000000000..9455ef443 --- /dev/null +++ b/drivers/hwmon/pmbus/xdpe1a2g7b.c @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Hardware monitoring driver for Infineon Multi-phase Digital XDPE1A2G5B + * and XDPE1A2G7B Controllers + * + * Copyright (c) 2026 Infineon Technologies. All rights reserved. + */ + +#include <linux/err.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include "pmbus.h" + +#define XDPE1A2G7B_PAGE_NUM 2 +#define XDPE1A2G7B_NVIDIA_195MV 0x1E /* NVIDIA mode 1.95mV, VID step is 5mV */ + +static int xdpe1a2g7b_identify(struct i2c_client *client, + struct pmbus_driver_info *info) +{ + u8 vout_params; + int vout_mode; + + /* + * XDPE1A2G5B and XDPE1A2G7B support both Linear and NVIDIA PWM VID data + * formats via VOUT_MODE. Note that the device pages/loops are not fully + * independent: configuration is shared, so programming each page/loop + * separately is not supported. + */ + vout_mode = pmbus_read_byte_data(client, 0, PMBUS_VOUT_MODE); + if (vout_mode < 0) + return vout_mode; + + switch (vout_mode >> 5) { + case 0: + info->format[PSC_VOLTAGE_OUT] = linear; + return 0; + case 1: + info->format[PSC_VOLTAGE_OUT] = vid; + vout_params = vout_mode & GENMASK(4, 0); + /* Check for VID Code Type */ + switch (vout_params) { + case XDPE1A2G7B_NVIDIA_195MV: + /* VID vrm_version for PAGE0 and PAGE1 */ + info->vrm_version[0] = nvidia195mv; + info->vrm_version[1] = nvidia195mv; + break; + default: + return -EINVAL; + } + break; + default: + return -ENODEV; + } + + return 0; +} + +#if IS_ENABLED(CONFIG_SENSORS_XDPE1A2G7B_REGULATOR) +static const struct regulator_desc xdpe1a2g7b_reg_desc[] = { + PMBUS_REGULATOR("vout", 0), + PMBUS_REGULATOR("vout", 1), +}; +#endif + +static struct pmbus_driver_info xdpe1a2g7b_info = { + .pages = XDPE1A2G7B_PAGE_NUM, + .identify = xdpe1a2g7b_identify, + .format[PSC_VOLTAGE_IN] = linear, + .format[PSC_TEMPERATURE] = linear, + .format[PSC_CURRENT_IN] = linear, + .format[PSC_CURRENT_OUT] = linear, + .format[PSC_POWER] = linear, + .func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_TEMP | PMBUS_HAVE_TEMP2 | PMBUS_HAVE_STATUS_TEMP | + PMBUS_HAVE_POUT | PMBUS_HAVE_PIN | PMBUS_HAVE_STATUS_INPUT, + .func[1] = PMBUS_HAVE_VIN | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT | + PMBUS_HAVE_IIN | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT | + PMBUS_HAVE_PIN | PMBUS_HAVE_POUT | PMBUS_HAVE_STATUS_INPUT, +#if IS_ENABLED(CONFIG_SENSORS_XDPE1A2G7B_REGULATOR) + .num_regulators = XDPE1A2G7B_PAGE_NUM, + .reg_desc = xdpe1a2g7b_reg_desc, +#endif +}; + +static int xdpe1a2g7b_probe(struct i2c_client *client) +{ + struct pmbus_driver_info *info; + + info = devm_kmemdup(&client->dev, &xdpe1a2g7b_info, sizeof(*info), + GFP_KERNEL); + if (!info) + return -ENOMEM; + + return pmbus_do_probe(client, info); +} + +static const struct i2c_device_id xdpe1a2g7b_id[] = { + { .name = "xdpe1a2g5b" }, + { .name = "xdpe1a2g7b" }, + { } +}; + +MODULE_DEVICE_TABLE(i2c, xdpe1a2g7b_id); + +static const struct of_device_id __maybe_unused xdpe1a2g7b_of_match[] = { + { .compatible = "infineon,xdpe1a2g5b" }, + { .compatible = "infineon,xdpe1a2g7b" }, + {} +}; + +MODULE_DEVICE_TABLE(of, xdpe1a2g7b_of_match); + +static struct i2c_driver xdpe1a2g7b_driver = { + .driver = { + .name = "xdpe1a2g7b", + .of_match_table = of_match_ptr(xdpe1a2g7b_of_match), + }, + .probe = xdpe1a2g7b_probe, + .id_table = xdpe1a2g7b_id, +}; + +module_i2c_driver(xdpe1a2g7b_driver); + +MODULE_AUTHOR("Ashish Yadav <ashish.yadav@infineon.com>"); +MODULE_DESCRIPTION("PMBus driver for Infineon XDPE1A2G5B/7B"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); diff --git a/drivers/hwmon/pmbus/zl6100.c b/drivers/hwmon/pmbus/zl6100.c new file mode 100644 index 000000000..5db2a7818 --- /dev/null +++ b/drivers/hwmon/pmbus/zl6100.c @@ -0,0 +1,423 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Hardware monitoring driver for ZL6100 and compatibles + * + * Copyright (c) 2011 Ericsson AB. + * Copyright (c) 2012 Guenter Roeck + */ + +#include <linux/bitops.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/err.h> +#include <linux/slab.h> +#include <linux/i2c.h> +#include <linux/ktime.h> +#include <linux/delay.h> +#include "pmbus.h" + +enum chips { zl2004, zl2005, zl2006, zl2008, zl2105, zl2106, zl6100, zl6105, + zl8802, zl9101, zl9117, zls1003, zls4009 }; + +struct zl6100_data { + int id; + struct pmbus_driver_info info; +}; + +#define to_zl6100_data(x) container_of(x, struct zl6100_data, info) + +#define ZL6100_MFR_CONFIG 0xd0 +#define ZL6100_DEVICE_ID 0xe4 + +#define ZL6100_MFR_XTEMP_ENABLE BIT(7) + +#define ZL8802_MFR_USER_GLOBAL_CONFIG 0xe9 +#define ZL8802_MFR_TMON_ENABLE BIT(12) +#define ZL8802_MFR_USER_CONFIG 0xd1 +#define ZL8802_MFR_XTEMP_ENABLE_2 BIT(1) +#define ZL8802_MFR_DDC_CONFIG 0xd3 +#define ZL8802_MFR_PHASES_MASK 0x0007 + +#define MFR_VMON_OV_FAULT_LIMIT 0xf5 +#define MFR_VMON_UV_FAULT_LIMIT 0xf6 +#define MFR_READ_VMON 0xf7 + +#define VMON_UV_WARNING BIT(5) +#define VMON_OV_WARNING BIT(4) +#define VMON_UV_FAULT BIT(1) +#define VMON_OV_FAULT BIT(0) + +#define ZL6100_WAIT_TIME 1000 /* uS */ + +static ushort delay = ZL6100_WAIT_TIME; +module_param(delay, ushort, 0644); +MODULE_PARM_DESC(delay, "Delay between chip accesses in uS"); + +/* Convert linear sensor value to milli-units */ +static long zl6100_l2d(s16 l) +{ + s16 exponent; + s32 mantissa; + long val; + + exponent = l >> 11; + mantissa = ((s16)((l & 0x7ff) << 5)) >> 5; + + val = mantissa; + + /* scale result to milli-units */ + val = val * 1000L; + + if (exponent >= 0) + val <<= exponent; + else + val >>= -exponent; + + return val; +} + +#define MAX_MANTISSA (1023 * 1000) +#define MIN_MANTISSA (511 * 1000) + +static u16 zl6100_d2l(long val) +{ + s16 exponent = 0, mantissa; + bool negative = false; + + /* simple case */ + if (val == 0) + return 0; + + if (val < 0) { + negative = true; + val = -val; + } + + /* Reduce large mantissa until it fits into 10 bit */ + while (val >= MAX_MANTISSA && exponent < 15) { + exponent++; + val >>= 1; + } + /* Increase small mantissa to improve precision */ + while (val < MIN_MANTISSA && exponent > -15) { + exponent--; + val <<= 1; + } + + /* Convert mantissa from milli-units to units */ + mantissa = DIV_ROUND_CLOSEST(val, 1000); + + /* Ensure that resulting number is within range */ + if (mantissa > 0x3ff) + mantissa = 0x3ff; + + /* restore sign */ + if (negative) + mantissa = -mantissa; + + /* Convert to 5 bit exponent, 11 bit mantissa */ + return (mantissa & 0x7ff) | ((exponent << 11) & 0xf800); +} + +static int zl6100_read_word_data(struct i2c_client *client, int page, + int phase, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + struct zl6100_data *data = to_zl6100_data(info); + int ret, vreg; + + if (page >= info->pages) + return -ENXIO; + + if (data->id == zl2005) { + /* + * Limit register detection is not reliable on ZL2005. + * Make sure registers are not erroneously detected. + */ + switch (reg) { + case PMBUS_VOUT_OV_WARN_LIMIT: + case PMBUS_VOUT_UV_WARN_LIMIT: + case PMBUS_IOUT_OC_WARN_LIMIT: + return -ENXIO; + } + } + + switch (reg) { + case PMBUS_VIRT_READ_VMON: + vreg = MFR_READ_VMON; + break; + case PMBUS_VIRT_VMON_OV_WARN_LIMIT: + case PMBUS_VIRT_VMON_OV_FAULT_LIMIT: + vreg = MFR_VMON_OV_FAULT_LIMIT; + break; + case PMBUS_VIRT_VMON_UV_WARN_LIMIT: + case PMBUS_VIRT_VMON_UV_FAULT_LIMIT: + vreg = MFR_VMON_UV_FAULT_LIMIT; + break; + default: + if (reg >= PMBUS_VIRT_BASE) + return -ENXIO; + vreg = reg; + break; + } + + ret = pmbus_read_word_data(client, page, phase, vreg); + if (ret < 0) + return ret; + + switch (reg) { + case PMBUS_VIRT_VMON_OV_WARN_LIMIT: + ret = zl6100_d2l(DIV_ROUND_CLOSEST(zl6100_l2d(ret) * 9, 10)); + break; + case PMBUS_VIRT_VMON_UV_WARN_LIMIT: + ret = zl6100_d2l(DIV_ROUND_CLOSEST(zl6100_l2d(ret) * 11, 10)); + break; + } + + return ret; +} + +static int zl6100_read_byte_data(struct i2c_client *client, int page, int reg) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + int ret, status; + + if (page >= info->pages) + return -ENXIO; + + switch (reg) { + case PMBUS_VIRT_STATUS_VMON: + ret = pmbus_read_byte_data(client, 0, + PMBUS_STATUS_MFR_SPECIFIC); + if (ret < 0) + break; + + status = 0; + if (ret & VMON_UV_WARNING) + status |= PB_VOLTAGE_UV_WARNING; + if (ret & VMON_OV_WARNING) + status |= PB_VOLTAGE_OV_WARNING; + if (ret & VMON_UV_FAULT) + status |= PB_VOLTAGE_UV_FAULT; + if (ret & VMON_OV_FAULT) + status |= PB_VOLTAGE_OV_FAULT; + ret = status; + break; + default: + ret = pmbus_read_byte_data(client, page, reg); + break; + } + + return ret; +} + +static int zl6100_write_word_data(struct i2c_client *client, int page, int reg, + u16 word) +{ + const struct pmbus_driver_info *info = pmbus_get_driver_info(client); + int vreg; + + if (page >= info->pages) + return -ENXIO; + + switch (reg) { + case PMBUS_VIRT_VMON_OV_WARN_LIMIT: + word = zl6100_d2l(DIV_ROUND_CLOSEST(zl6100_l2d(word) * 10, 9)); + vreg = MFR_VMON_OV_FAULT_LIMIT; + pmbus_clear_cache(client); + break; + case PMBUS_VIRT_VMON_OV_FAULT_LIMIT: + vreg = MFR_VMON_OV_FAULT_LIMIT; + pmbus_clear_cache(client); + break; + case PMBUS_VIRT_VMON_UV_WARN_LIMIT: + word = zl6100_d2l(DIV_ROUND_CLOSEST(zl6100_l2d(word) * 10, 11)); + vreg = MFR_VMON_UV_FAULT_LIMIT; + pmbus_clear_cache(client); + break; + case PMBUS_VIRT_VMON_UV_FAULT_LIMIT: + vreg = MFR_VMON_UV_FAULT_LIMIT; + pmbus_clear_cache(client); + break; + default: + if (reg >= PMBUS_VIRT_BASE) + return -ENXIO; + vreg = reg; + } + + return pmbus_write_word_data(client, page, vreg, word); +} + +static const struct i2c_device_id zl6100_id[] = { + { .name = "bmr450", .driver_data = zl2005 }, + { .name = "bmr451", .driver_data = zl2005 }, + { .name = "bmr462", .driver_data = zl2008 }, + { .name = "bmr463", .driver_data = zl2008 }, + { .name = "bmr464", .driver_data = zl2008 }, + { .name = "bmr465", .driver_data = zls4009 }, + { .name = "bmr466", .driver_data = zls1003 }, + { .name = "bmr467", .driver_data = zls4009 }, + { .name = "bmr469", .driver_data = zl8802 }, + { .name = "zl2004", .driver_data = zl2004 }, + { .name = "zl2005", .driver_data = zl2005 }, + { .name = "zl2006", .driver_data = zl2006 }, + { .name = "zl2008", .driver_data = zl2008 }, + { .name = "zl2105", .driver_data = zl2105 }, + { .name = "zl2106", .driver_data = zl2106 }, + { .name = "zl6100", .driver_data = zl6100 }, + { .name = "zl6105", .driver_data = zl6105 }, + { .name = "zl8802", .driver_data = zl8802 }, + { .name = "zl9101", .driver_data = zl9101 }, + { .name = "zl9117", .driver_data = zl9117 }, + { .name = "zls1003", .driver_data = zls1003 }, + { .name = "zls4009", .driver_data = zls4009 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, zl6100_id); + +static int zl6100_probe(struct i2c_client *client) +{ + int ret, i; + struct zl6100_data *data; + struct pmbus_driver_info *info; + u8 device_id[I2C_SMBUS_BLOCK_MAX + 1]; + const struct i2c_device_id *mid; + + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_WORD_DATA + | I2C_FUNC_SMBUS_READ_BLOCK_DATA)) + return -ENODEV; + + ret = i2c_smbus_read_block_data(client, ZL6100_DEVICE_ID, + device_id); + if (ret < 0) { + dev_err(&client->dev, "Failed to read device ID\n"); + return ret; + } + device_id[ret] = '\0'; + dev_info(&client->dev, "Device ID %s\n", device_id); + + mid = NULL; + for (mid = zl6100_id; mid->name[0]; mid++) { + if (!strncasecmp(mid->name, device_id, strlen(mid->name))) + break; + } + if (!mid->name[0]) { + dev_err(&client->dev, "Unsupported device\n"); + return -ENODEV; + } + if (strcmp(client->name, mid->name) != 0) + dev_notice(&client->dev, + "Device mismatch: Configured %s, detected %s\n", + client->name, mid->name); + + data = devm_kzalloc(&client->dev, sizeof(struct zl6100_data), + GFP_KERNEL); + if (!data) + return -ENOMEM; + + data->id = mid->driver_data; + + /* + * According to information from the chip vendor, all currently + * supported chips are known to require a wait time between I2C + * accesses. + */ + udelay(delay); + + info = &data->info; + + info->pages = 1; + info->func[0] = PMBUS_HAVE_VIN | PMBUS_HAVE_STATUS_INPUT + | PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT + | PMBUS_HAVE_TEMP | PMBUS_HAVE_STATUS_TEMP; + + /* + * ZL2004, ZL8802, ZL9101M, ZL9117M and ZLS4009 support monitoring + * an extra voltage (VMON for ZL2004, ZL8802 and ZLS4009, + * VDRV for ZL9101M and ZL9117M). Report it as vmon. + */ + if (data->id == zl2004 || data->id == zl8802 || data->id == zl9101 || + data->id == zl9117 || data->id == zls4009) + info->func[0] |= PMBUS_HAVE_VMON | PMBUS_HAVE_STATUS_VMON; + + /* + * ZL8802 has two outputs that can be used either independently or in + * a current sharing configuration. The driver uses the DDC_CONFIG + * register to check if the module is running with independent or + * shared outputs. If the module is in shared output mode, only one + * output voltage will be reported. + */ + if (data->id == zl8802) { + info->pages = 2; + info->func[0] |= PMBUS_HAVE_IIN; + + ret = i2c_smbus_read_word_data(client, ZL8802_MFR_DDC_CONFIG); + if (ret < 0) + return ret; + + udelay(delay); + + if (ret & ZL8802_MFR_PHASES_MASK) + info->func[1] |= PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT; + else + info->func[1] = PMBUS_HAVE_VOUT | PMBUS_HAVE_STATUS_VOUT + | PMBUS_HAVE_IOUT | PMBUS_HAVE_STATUS_IOUT; + + for (i = 0; i < 2; i++) { + ret = i2c_smbus_write_byte_data(client, PMBUS_PAGE, i); + if (ret < 0) + return ret; + + udelay(delay); + + ret = i2c_smbus_read_word_data(client, ZL8802_MFR_USER_CONFIG); + if (ret < 0) + return ret; + + if (ret & ZL8802_MFR_XTEMP_ENABLE_2) + info->func[i] |= PMBUS_HAVE_TEMP2; + + udelay(delay); + } + ret = i2c_smbus_read_word_data(client, ZL8802_MFR_USER_GLOBAL_CONFIG); + if (ret < 0) + return ret; + + if (ret & ZL8802_MFR_TMON_ENABLE) + info->func[0] |= PMBUS_HAVE_TEMP3; + } else { + ret = i2c_smbus_read_word_data(client, ZL6100_MFR_CONFIG); + if (ret < 0) + return ret; + + if (ret & ZL6100_MFR_XTEMP_ENABLE) + info->func[0] |= PMBUS_HAVE_TEMP2; + } + + udelay(delay); + + info->access_delay = delay; + info->read_word_data = zl6100_read_word_data; + info->read_byte_data = zl6100_read_byte_data; + info->write_word_data = zl6100_write_word_data; + + return pmbus_do_probe(client, info); +} + +static struct i2c_driver zl6100_driver = { + .driver = { + .name = "zl6100", + }, + .probe = zl6100_probe, + .id_table = zl6100_id, +}; + +module_i2c_driver(zl6100_driver); + +MODULE_AUTHOR("Guenter Roeck"); +MODULE_DESCRIPTION("PMBus driver for ZL6100 and compatibles"); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("PMBUS"); |
