Re: [PATCH v2 02/10] hwmon: Add Qualcomm PMIC BCL driver
Guenter Roeck <[email protected]> Tue, 21 Jul 2026 13:44:53 -0700
| Newsgroups | dev.linux.lists.mfd,org.kernel.vger.linux-arm-msm,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-hwmon,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
On 7/21/26 11:30, Manaf Meethalavalappu Pallikunhi wrote: > Add driver for Qualcomm SPMI PMIC Battery Current Limiting (BCL) > hardware monitor. The driver exposes battery voltage and current > monitoring through hwmon interface. > > The BCL driver provides > - Real-time voltage and current readings > - Configurable threshold-based alarms > - Interrupt-driven notifications when thresholds are exceeded > - Automatic threshold management with polling-based recovery > - Hardware-specific scaling factors and threshold representations > > Signed-off-by: Manaf Meethalavalappu Pallikunhi <[email protected]> > --- > MAINTAINERS | 1 + > drivers/hwmon/Kconfig | 11 + > drivers/hwmon/Makefile | 1 + > drivers/hwmon/qcom-bcl-hwmon.c | 1443 ++++++++++++++++++++++++++++++++++++++++ > 4 files changed, 1456 insertions(+) > > diff --git a/MAINTAINERS b/MAINTAINERS > index 1b178d3a91ad..8b7d3c3c6252 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -22182,6 +22182,7 @@ L: [email protected] > L: [email protected] > S: Maintained > F: Documentation/devicetree/bindings/hwmon/qcom,pm7250b-bcl.yaml > +F: drivers/hwmon/qcom-bcl-hwmon.c > > QUALCOMM BLUETOOTH DRIVER > M: Bartosz Golaszewski <[email protected]> > diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig > index 50d744db3045..8618c88932c3 100644 > --- a/drivers/hwmon/Kconfig > +++ b/drivers/hwmon/Kconfig > @@ -1955,6 +1955,17 @@ config SENSORS_PWM_FAN > This driver can also be built as a module. If so, the module > will be called pwm-fan. > > +config SENSORS_QCOM_SPMI_BCL > + tristate "Qualcomm SPMI BCL hardware monitoring" > + depends on SPMI > + select REGMAP_SPMI > + help > + Say yes here to enable support for Qualcomm battery over current > + and under voltage alarms monitor. > + > + This driver can also be built as a module. If so, the module > + will be called qcom-bcl-hwmon. > + > config SENSORS_QNAP_MCU_HWMON > tristate "QNAP MCU hardware monitoring" > depends on MFD_QNAP_MCU > diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile > index 63809eeec2f4..cb61983d437c 100644 > --- a/drivers/hwmon/Makefile > +++ b/drivers/hwmon/Makefile > @@ -204,6 +204,7 @@ obj-$(CONFIG_SENSORS_POWR1220) += powr1220.o > obj-$(CONFIG_SENSORS_PROM21_XHCI) += prom21-xhci.o > obj-$(CONFIG_SENSORS_PT5161L) += pt5161l.o > obj-$(CONFIG_SENSORS_PWM_FAN) += pwm-fan.o > +obj-$(CONFIG_SENSORS_QCOM_SPMI_BCL) += qcom-bcl-hwmon.o > obj-$(CONFIG_SENSORS_QNAP_MCU_HWMON) += qnap-mcu-hwmon.o > obj-$(CONFIG_SENSORS_RASPBERRYPI_HWMON) += raspberrypi-hwmon.o > obj-$(CONFIG_SENSORS_SBTSI) += sbtsi_temp.o > diff --git a/drivers/hwmon/qcom-bcl-hwmon.c b/drivers/hwmon/qcom-bcl-hwmon.c > new file mode 100644 > index 000000000000..a21433b45482 > --- /dev/null > +++ b/drivers/hwmon/qcom-bcl-hwmon.c > @@ -0,0 +1,1443 @@ > +// SPDX-License-Identifier: GPL-2.0-only > +/* > + * Qualcomm pmic BCL driver for battery overcurrent and > + * battery or system under voltage monitor > + * > + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. > + */ > + > +#include <linux/bitfield.h> > +#include <linux/devm-helpers.h> > +#include <linux/err.h> > +#include <linux/hwmon.h> > +#include <linux/interrupt.h> > +#include <linux/kernel.h> > +#include <linux/module.h> > +#include <linux/mod_devicetable.h> > +#include <linux/mutex.h> > +#include <linux/platform_device.h> > +#include <linux/property.h> > +#include <linux/regmap.h> > +#include <linux/workqueue.h> > + > +/* BCL common regmap offset */ > +#define REVISION1 0x0 > +#define REVISION2 0x1 > +#define STATUS 0x8 > +#define INT_RT_STS 0x10 > +#define EN_CTL1 0x46 > + > +/* BCL GEN1 regmap offsets */ > +#define MODE_CTL1 0x41 > +#define VADC_L0_THR 0x48 > +#define VCMP_L1_THR 0x49 > +#define IADC_H0_THR 0x4b > +#define IADC_H1_THR 0x4c > +#define VADC_CONV_REQ 0x72 > +#define IADC_CONV_REQ 0x82 > +#define VADC_DATA1 0x76 > +#define IADC_DATA1 0x86 > + > +/* BCL GEN3 regmap offsets */ > +#define VCMP_CTL 0x44 > +#define VCMP_L0_THR 0x47 > +#define PARAM_1 0x0e > +#define IADC_H1_THR_GEN3 0x4d > + > +#define BCL_IN_INC_MV 25 > +#define BCL_ALARM_POLLING_MS 50 > + > +/** > + * enum bcl_limit_alarm - BCL alarm threshold levels > + * @BCL_LIMIT_ALARM_LVL0: Level 0 alarm threshold > + * @BCL_LIMIT_ALARM_LVL1: Level 1 alarm threshold > + * @BCL_LIMIT_ALARM_MAX: sentinel value > + * > + * Defines the two threshold levels for BCL monitoring. Each level corresponds > + * to different severity of in or curr conditions. > + */ > +enum bcl_limit_alarm { > + BCL_LIMIT_ALARM_LVL0, > + BCL_LIMIT_ALARM_LVL1, > + > + BCL_LIMIT_ALARM_MAX, > +}; > + > +/** > + * enum bcl_channel - BCL supported sensor channel type > + * @CHANNEL_IN: in (voltage) channel > + * @CHANNEL_CURR: curr (current) channel > + * @CHANNEL_MAX: sentinel value > + * > + * Defines the supported channel types for bcl. > + */ > +enum bcl_channel { > + CHANNEL_IN, > + CHANNEL_CURR, > + > + CHANNEL_MAX, > +}; > + > +/** > + * enum bcl_thresh_type - voltage or current threshold representation type > + * @THRESH_TYPE_ADC: Raw ADC value representation > + * @THRESH_TYPE_INDEX: Index-based voltage or current representation > + * > + * Specifies how voltage or current thresholds are stored and interpreted in > + * registers. Some PMICs use raw ADC values while others use indexed values. > + */ > +enum bcl_thresh_type { > + THRESH_TYPE_ADC, > + THRESH_TYPE_INDEX, > +}; > + > +/** > + * enum bcl_battery_config - Battery configuration types > + * @BCL_BATT_1S: Single cell battery > + * @BCL_BATT_2S: Two cells in series > + * @BCL_BATT_3S: Three cells in series > + * @BCL_BATT_UNKNOWN: Unknown or not applicable > + */ > +enum bcl_battery_config { > + BCL_BATT_1S, > + BCL_BATT_2S, > + BCL_BATT_3S, > + BCL_BATT_UNKNOWN, > +}; > + > +/** > + * enum bcl_fields - BCL register field identifiers > + * @F_V_MAJOR: Major revision info field > + * @F_V_MINOR: Minor revision info field > + * @F_CTL_EN: Monitor enable control field > + * @F_LVL0_ALARM: Level 0 alarm status field > + * @F_LVL1_ALARM: Level 1 alarm status field > + * @F_IN_MON_EN: voltage monitor enable control field > + * @F_IN_L0_THR: voltage level 0 threshold field > + * @F_IN_L1_THR: voltage level 1 threshold field > + * @F_IN_INPUT_EN: voltage input enable control field > + * @F_IN_INPUT: voltage input data field (LSB for 16-bit data) > + * @F_IN_INPUT1: voltage input data MSB for 16-bit voltage data > + * @F_CURR_MON_EN: current monitor enable control field > + * @F_CURR_H0_THR: current level 0 threshold field > + * @F_CURR_H1_THR: current level 1 threshold field > + * @F_CURR_INPUT: current input data field (LSB for 16-bit data) > + * @F_CURR_INPUT1: current input data MSB for 16-bit current data > + * @F_MAX_FIELDS: sentinel value > + * > + * Enumeration of all register fields used by the BCL driver for accessing > + * registers through regmap fields. > + */ > +enum bcl_fields { > + /* Common fields - present in all BCL variants */ > + F_V_MAJOR, > + F_V_MINOR, > + F_CTL_EN, > + F_LVL0_ALARM, > + F_LVL1_ALARM, > + > + /* Voltage monitoring fields */ > + F_IN_MON_EN, > + F_IN_L0_THR, > + F_IN_L1_THR, > + F_IN_INPUT_EN, > + F_IN_INPUT, > + F_IN_INPUT1, /* MSB for 16-bit voltage data */ > + > + /* Current monitoring fields */ > + F_CURR_MON_EN, > + F_CURR_H0_THR, > + F_CURR_H1_THR, > + F_CURR_INPUT, > + F_CURR_INPUT1, /* MSB for 16-bit current data */ > + > + F_MAX_FIELDS > +}; > + > +/** > + * struct bcl_channel_cfg - BCL channel related configuration > + * @default_scale_nu: Default scaling factor in nano unit > + * @base: Base threshold value in milli unit > + * @max: Maximum threshold value in milli unit > + * @step: step increment value between two indexed threshold value > + * @thresh_type: Array specifying threshold representation type for each alarm level > + * > + * Contains hardware-specific configuration and scaling parameters for different > + * channel(voltage and current).. > + */ > +struct bcl_channel_cfg { > + u32 default_scale_nu; > + u32 base; > + u32 max; > + u32 step; > + u8 thresh_type[BCL_LIMIT_ALARM_MAX]; > +}; > + > +/** > + * struct bcl_desc - BCL device descriptor > + * @reg_fields: Array of register field definitions for this device variant > + * @channel_cfg: Array of channel configurations indexed by battery config > + * PMICs without battery detection: only [BCL_BATT_1S] is defined > + * PMICs with battery detection: [BCL_BATT_2S], [BCL_BATT_3S] > + * @battery_config_field: Register field for battery configuration detection > + * NOTE: This is an ABSOLUTE address, not relative to BCL base > + * Set to REG_FIELD(0, 0, 0) if not used > + * @num_reg_fields: Number of register field definitions for this device variant > + * @data_field_bits_size: data read register bit size > + * @thresh_field_bits_size: lsb bit size those are not included in threshold register > + * > + * Contains hardware-specific configuration and scaling parameters for different > + * BCL variants. Each PMIC model may have different register layouts and > + * conversion factors. > + */ > +struct bcl_desc { > + const struct reg_field *reg_fields; > + struct bcl_channel_cfg channel_cfg[BCL_BATT_3S + 1][CHANNEL_MAX]; > + const struct reg_field battery_config_field; > + u8 num_reg_fields; > + u8 data_field_bits_size; > + u8 thresh_field_bits_size; > +}; > + > +/** > + * struct bcl_alarm_data - BCL alarm interrupt data > + * @irq: IRQ number assigned to this alarm > + * @irq_enabled: Flag indicating if IRQ is enabled > + * @irq_wake_enabled: Flag indicating if IRQ wake is enabled > + * @shutting_down: Flag preventing work from re-enabling IRQ during teardown > + * @type: Alarm level type (LVL0, or LVL1) > + * @device: Pointer to parent BCL device structure > + * @a_lock: Mutex for protecting alarm state > + * @alarm_poll_work: delayed_work to poll alarm status > + * > + * Stores interrupt-related information for each alarm threshold level. > + * Used by the IRQ handler to identify which alarm triggered. > + */ > +struct bcl_alarm_data { > + int irq; > + bool irq_enabled; > + bool irq_wake_enabled; > + bool shutting_down; > + enum bcl_limit_alarm type; > + void *device; > + /* Protects alarm IRQ enable/disable state */ > + struct mutex a_lock; > + struct delayed_work alarm_poll_work; > +}; > + > +/** > + * struct bcl_device - Main BCL device structure > + * @dev: Pointer to device structure > + * @regmap: Regmap for accessing PMIC registers > + * @fields: Array of regmap fields for register access > + * @bcl_alarms: Array of alarm data structures for each threshold level > + * @lock: Mutex for protecting concurrent hardware access > + * @base: the BCL regbase offset from regmap > + * @last_in_input: Last valid voltage input reading in millivolts > + * @last_curr_input: Last valid current input reading in milliamps > + * @last_in_updated: Timestamp of last voltage input update > + * @last_curr_updated: Timestamp of last current input update > + * @desc: Pointer to device descriptor with hardware-specific parameters > + * @hwmon_dev: Pointer to registered hwmon device > + * @hwmon_info: Dynamically built hwmon channel info array > + * @hwmon_chip_info: Dynamically built hwmon chip info structure > + * @hwmon_name: Sanitized name for hwmon device > + * @batt_config: Detected battery configuration (only for SMB2360/2370) > + * @batt_config_regfield: Regmap field for battery configuration register > + * @in_attrs: Voltage channel attributes mask > + * @curr_attrs: Current channel attributes mask > + * > + * Main driver structure containing all state and configuration for a BCL > + * monitoring instance. Manages voltage and current monitoring, thresholds, > + * and alarm handling. > + */ > +struct bcl_device { > + struct device *dev; > + struct regmap *regmap; > + u16 base; > + struct regmap_field *fields[F_MAX_FIELDS]; > + struct bcl_alarm_data bcl_alarms[BCL_LIMIT_ALARM_MAX]; > + /* Protects hardware register access and device state */ > + struct mutex lock; > + u32 last_in_input; > + s32 last_curr_input; > + unsigned long last_in_updated; > + unsigned long last_curr_updated; > + const struct bcl_desc *desc; > + struct device *hwmon_dev; > + const struct hwmon_channel_info **hwmon_info; > + struct hwmon_chip_info hwmon_chip_info; > + char *hwmon_name; > + enum bcl_battery_config batt_config; > + struct regmap_field *batt_config_regfield; > + u32 in_attrs; > + u32 curr_attrs; > +}; > + > +static const u8 in_attr_to_lvl_map[] = { > + [hwmon_in_min] = BCL_LIMIT_ALARM_LVL0, > + [hwmon_in_lcrit] = BCL_LIMIT_ALARM_LVL1, > + [hwmon_in_min_alarm] = BCL_LIMIT_ALARM_LVL0, > + [hwmon_in_lcrit_alarm] = BCL_LIMIT_ALARM_LVL1, > +}; > + > +static const u8 in_lvl_to_attr_map[BCL_LIMIT_ALARM_MAX] = { > + [BCL_LIMIT_ALARM_LVL0] = hwmon_in_min_alarm, > + [BCL_LIMIT_ALARM_LVL1] = hwmon_in_lcrit_alarm, > +}; > + > +static const u8 curr_attr_to_lvl_map[] = { > + [hwmon_curr_max] = BCL_LIMIT_ALARM_LVL0, > + [hwmon_curr_crit] = BCL_LIMIT_ALARM_LVL1, > + [hwmon_curr_max_alarm] = BCL_LIMIT_ALARM_LVL0, > + [hwmon_curr_crit_alarm] = BCL_LIMIT_ALARM_LVL1, > +}; > + > +static const u8 curr_lvl_to_attr_map[BCL_LIMIT_ALARM_MAX] = { > + [BCL_LIMIT_ALARM_LVL0] = hwmon_curr_max_alarm, > + [BCL_LIMIT_ALARM_LVL1] = hwmon_curr_crit_alarm, > +}; > + > +/* Interrupt names for each alarm level */ > +static const char * const bcl_int_names[BCL_LIMIT_ALARM_MAX] = { > + [BCL_LIMIT_ALARM_LVL0] = "max-min", > + [BCL_LIMIT_ALARM_LVL1] = "critical", > +}; > + > +static const struct reg_field bcl_pm7250b_reg_fields[] = { > + [F_V_MAJOR] = REG_FIELD(REVISION2, 0, 7), > + [F_V_MINOR] = REG_FIELD(REVISION1, 0, 7), > + [F_CTL_EN] = REG_FIELD(EN_CTL1, 7, 7), > + [F_LVL0_ALARM] = REG_FIELD(STATUS, 0, 0), > + [F_LVL1_ALARM] = REG_FIELD(STATUS, 1, 1), > + [F_IN_MON_EN] = REG_FIELD(MODE_CTL1, 0, 2), > + [F_IN_L0_THR] = REG_FIELD(VADC_L0_THR, 0, 7), > + [F_IN_L1_THR] = REG_FIELD(VCMP_L1_THR, 0, 5), > + [F_IN_INPUT_EN] = REG_FIELD(VADC_CONV_REQ, 0, 0), > + [F_IN_INPUT] = REG_FIELD(VADC_DATA1, 0, 7), > + [F_CURR_MON_EN] = REG_FIELD(IADC_CONV_REQ, 0, 0), > + [F_CURR_H0_THR] = REG_FIELD(IADC_H0_THR, 0, 7), > + [F_CURR_H1_THR] = REG_FIELD(IADC_H1_THR, 0, 7), > + [F_CURR_INPUT] = REG_FIELD(IADC_DATA1, 0, 7), > +}; > + > +static const struct reg_field bcl_pm8350c_reg_fields[] = { > + [F_V_MAJOR] = REG_FIELD(REVISION2, 0, 7), > + [F_V_MINOR] = REG_FIELD(REVISION1, 0, 7), > + [F_CTL_EN] = REG_FIELD(EN_CTL1, 7, 7), > + [F_LVL0_ALARM] = REG_FIELD(STATUS, 0, 0), > + [F_LVL1_ALARM] = REG_FIELD(STATUS, 1, 1), > + [F_IN_MON_EN] = REG_FIELD(VCMP_CTL, 0, 1), > + [F_IN_L0_THR] = REG_FIELD(VADC_L0_THR, 0, 7), > + [F_IN_L1_THR] = REG_FIELD(VCMP_L1_THR, 0, 5), > + [F_IN_INPUT_EN] = REG_FIELD(VADC_CONV_REQ, 0, 0), > + [F_IN_INPUT] = REG_FIELD(VADC_DATA1, 0, 7), > + [F_CURR_MON_EN] = REG_FIELD(IADC_CONV_REQ, 0, 0), > + [F_CURR_H0_THR] = REG_FIELD(IADC_H0_THR, 0, 7), > + [F_CURR_H1_THR] = REG_FIELD(IADC_H1_THR, 0, 7), > + [F_CURR_INPUT] = REG_FIELD(IADC_DATA1, 0, 7), > +}; > + > +static const struct reg_field bcl_pm8550_reg_fields[] = { > + [F_V_MAJOR] = REG_FIELD(REVISION2, 0, 7), > + [F_V_MINOR] = REG_FIELD(REVISION1, 0, 7), > + [F_CTL_EN] = REG_FIELD(EN_CTL1, 7, 7), > + [F_LVL0_ALARM] = REG_FIELD(STATUS, 0, 0), > + [F_LVL1_ALARM] = REG_FIELD(STATUS, 1, 1), > + [F_IN_MON_EN] = REG_FIELD(VCMP_CTL, 0, 2), > + [F_IN_L0_THR] = REG_FIELD(VCMP_L0_THR, 0, 5), > + [F_IN_L1_THR] = REG_FIELD(VCMP_L1_THR, 0, 5), > + [F_IN_INPUT_EN] = REG_FIELD(PARAM_1, 0, 0), > + [F_IN_INPUT] = REG_FIELD(VADC_DATA1, 0, 7), > + [F_CURR_MON_EN] = REG_FIELD(PARAM_1, 1, 1), > +}; > + > +static const struct reg_field bcl_pmh0101_reg_fields[] = { > + [F_V_MAJOR] = REG_FIELD(REVISION2, 0, 7), > + [F_V_MINOR] = REG_FIELD(REVISION1, 0, 7), > + [F_CTL_EN] = REG_FIELD(EN_CTL1, 7, 7), > + [F_LVL0_ALARM] = REG_FIELD(STATUS, 0, 0), > + [F_LVL1_ALARM] = REG_FIELD(STATUS, 1, 1), > + [F_IN_MON_EN] = REG_FIELD(VCMP_CTL, 0, 2), > + [F_IN_L0_THR] = REG_FIELD(VCMP_L0_THR, 0, 6), > + [F_IN_L1_THR] = REG_FIELD(VCMP_L1_THR, 0, 6), > + [F_IN_INPUT_EN] = REG_FIELD(PARAM_1, 0, 0), > + [F_IN_INPUT] = REG_FIELD(VADC_DATA1, 0, 7), > + [F_IN_INPUT1] = REG_FIELD(VADC_DATA1 + 1, 0, 7), > + [F_CURR_MON_EN] = REG_FIELD(PARAM_1, 1, 1), > +}; > + > +static const struct reg_field bcl_pmih0108_reg_fields[] = { > + [F_V_MAJOR] = REG_FIELD(REVISION2, 0, 7), > + [F_V_MINOR] = REG_FIELD(REVISION1, 0, 7), > + [F_CTL_EN] = REG_FIELD(EN_CTL1, 7, 7), > + [F_LVL0_ALARM] = REG_FIELD(STATUS, 0, 0), > + [F_LVL1_ALARM] = REG_FIELD(STATUS, 1, 1), > + [F_IN_MON_EN] = REG_FIELD(VCMP_CTL, 0, 2), > + [F_IN_L0_THR] = REG_FIELD(VADC_L0_THR, 0, 7), > + [F_IN_L1_THR] = REG_FIELD(VCMP_L1_THR, 0, 5), > + [F_IN_INPUT_EN] = REG_FIELD(PARAM_1, 0, 0), > + [F_IN_INPUT] = REG_FIELD(VADC_DATA1, 0, 7), > + [F_IN_INPUT1] = REG_FIELD(VADC_DATA1 + 1, 0, 7), > + [F_CURR_MON_EN] = REG_FIELD(PARAM_1, 1, 1), > + [F_CURR_H0_THR] = REG_FIELD(IADC_H0_THR, 0, 7), > + [F_CURR_H1_THR] = REG_FIELD(IADC_H1_THR_GEN3, 0, 7), > + [F_CURR_INPUT] = REG_FIELD(IADC_DATA1, 0, 7), > + [F_CURR_INPUT1] = REG_FIELD(IADC_DATA1 + 1, 0, 7), > +}; > + > +static const struct reg_field bcl_smb2360_reg_fields[] = { > + [F_V_MAJOR] = REG_FIELD(REVISION2, 0, 7), > + [F_V_MINOR] = REG_FIELD(REVISION1, 0, 7), > + [F_CTL_EN] = REG_FIELD(EN_CTL1, 7, 7), > + [F_LVL0_ALARM] = REG_FIELD(STATUS, 0, 0), > + [F_LVL1_ALARM] = REG_FIELD(STATUS, 1, 1), > + [F_IN_MON_EN] = REG_FIELD(VCMP_CTL, 0, 2), > + [F_IN_L0_THR] = REG_FIELD(VADC_L0_THR, 0, 7), > + [F_IN_L1_THR] = REG_FIELD(VCMP_L1_THR, 0, 5), > + [F_IN_INPUT_EN] = REG_FIELD(PARAM_1, 0, 0), > + [F_IN_INPUT] = REG_FIELD(VADC_DATA1, 0, 7), > + [F_CURR_MON_EN] = REG_FIELD(PARAM_1, 1, 1), > + [F_CURR_H0_THR] = REG_FIELD(IADC_H0_THR, 0, 7), > + [F_CURR_H1_THR] = REG_FIELD(IADC_H1_THR_GEN3, 0, 7), > + [F_CURR_INPUT] = REG_FIELD(IADC_DATA1, 0, 7), > +}; > + > +static const struct reg_field bcl_smb2370_reg_fields[] = { > + [F_V_MAJOR] = REG_FIELD(REVISION2, 0, 7), > + [F_V_MINOR] = REG_FIELD(REVISION1, 0, 7), > + [F_CTL_EN] = REG_FIELD(EN_CTL1, 7, 7), > + [F_LVL0_ALARM] = REG_FIELD(STATUS, 0, 0), > + [F_LVL1_ALARM] = REG_FIELD(STATUS, 1, 1), > + [F_IN_MON_EN] = REG_FIELD(VCMP_CTL, 0, 2), > + [F_IN_L0_THR] = REG_FIELD(VADC_L0_THR, 0, 7), > + [F_IN_L1_THR] = REG_FIELD(VCMP_L1_THR, 0, 5), > + [F_IN_INPUT_EN] = REG_FIELD(PARAM_1, 0, 0), > + [F_IN_INPUT] = REG_FIELD(VADC_DATA1, 0, 7), > + [F_IN_INPUT1] = REG_FIELD(VADC_DATA1 + 1, 0, 7), > + [F_CURR_MON_EN] = REG_FIELD(PARAM_1, 1, 1), > + [F_CURR_H0_THR] = REG_FIELD(IADC_H0_THR, 0, 7), > + [F_CURR_H1_THR] = REG_FIELD(IADC_H1_THR_GEN3, 0, 7), > + [F_CURR_INPUT] = REG_FIELD(IADC_DATA1, 0, 7), > + [F_CURR_INPUT1] = REG_FIELD(IADC_DATA1 + 1, 0, 7), > +}; > + > +static const struct bcl_desc pm7250b_data = { > + .reg_fields = bcl_pm7250b_reg_fields, > + .num_reg_fields = F_CURR_INPUT + 1, > + .data_field_bits_size = 8, > + .thresh_field_bits_size = 7, > + .battery_config_field = REG_FIELD(0, 0, 0), > + .channel_cfg[BCL_BATT_1S][CHANNEL_IN] = { > + .base = 2250, > + .max = 3600, > + .step = 25, > + .default_scale_nu = 194637, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_INDEX}, > + }, > + .channel_cfg[BCL_BATT_1S][CHANNEL_CURR] = { > + .max = 10000, > + .default_scale_nu = 305180, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_ADC}, > + }, > +}; > + > +static const struct bcl_desc pm8350c_data = { > + .reg_fields = bcl_pm8350c_reg_fields, > + .num_reg_fields = F_CURR_INPUT + 1, > + .data_field_bits_size = 8, > + .thresh_field_bits_size = 8, > + .battery_config_field = REG_FIELD(0, 0, 0), > + .channel_cfg[BCL_BATT_1S][CHANNEL_IN] = { > + .base = 2250, > + .max = 3600, > + .step = 25, > + .default_scale_nu = 194637, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_INDEX}, > + }, > + .channel_cfg[BCL_BATT_1S][CHANNEL_CURR] = { > + .max = 10000, > + .default_scale_nu = 305180, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_ADC}, > + }, > +}; > + > +static const struct bcl_desc pm8550_data = { > + .reg_fields = bcl_pm8550_reg_fields, > + .num_reg_fields = F_CURR_MON_EN + 1, > + .data_field_bits_size = 0, > + .thresh_field_bits_size = 8, > + .battery_config_field = REG_FIELD(0, 0, 0), > + .channel_cfg[BCL_BATT_1S][CHANNEL_IN] = { > + .base = 2250, > + .max = 3600, > + .step = 25, > + .thresh_type = {THRESH_TYPE_INDEX, THRESH_TYPE_INDEX}, > + }, > +}; > + > +static const struct bcl_desc pmih0108_data = { > + .reg_fields = bcl_pmih0108_reg_fields, > + .num_reg_fields = F_MAX_FIELDS, > + .data_field_bits_size = 16, > + .thresh_field_bits_size = 8, > + .battery_config_field = REG_FIELD(0, 0, 0), > + .channel_cfg[BCL_BATT_1S][CHANNEL_IN] = { > + .base = 2250, > + .max = 3600, > + .step = 25, > + .default_scale_nu = 194637, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_INDEX}, > + }, > + .channel_cfg[BCL_BATT_1S][CHANNEL_CURR] = { > + .max = 20000, > + .default_scale_nu = 610370, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_ADC}, > + }, > +}; > + > +static const struct bcl_desc pmh0101_data = { > + .reg_fields = bcl_pmh0101_reg_fields, > + .num_reg_fields = F_CURR_MON_EN + 1, > + .thresh_field_bits_size = 8, > + .battery_config_field = REG_FIELD(0, 0, 0), > + .channel_cfg[BCL_BATT_1S][CHANNEL_IN] = { > + .base = 1500, > + .max = 4000, > + .step = 25, > + .thresh_type = {THRESH_TYPE_INDEX, THRESH_TYPE_INDEX}, > + }, > +}; > + > +/* Register 0x2a50 mapping: 0 -> 2S, 1 -> 3S */ > +static const struct bcl_desc smb2360_data = { > + .reg_fields = bcl_smb2360_reg_fields, > + .num_reg_fields = F_CURR_INPUT + 1, > + .data_field_bits_size = 8, > + .thresh_field_bits_size = 8, > + .battery_config_field = REG_FIELD(0x2a50, 0, 1), > + .channel_cfg[BCL_BATT_2S][CHANNEL_IN] = { > + .base = 4500, > + .max = 8400, > + .step = 50, > + .default_scale_nu = 432918, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_INDEX}, > + }, > + .channel_cfg[BCL_BATT_2S][CHANNEL_CURR] = { > + .max = 20000, > + .default_scale_nu = 540679, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_ADC}, > + }, > + .channel_cfg[BCL_BATT_3S][CHANNEL_IN] = { > + .base = 6750, > + .max = 12600, > + .step = 75, > + .default_scale_nu = 648790, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_INDEX}, > + }, > + .channel_cfg[BCL_BATT_3S][CHANNEL_CURR] = { > + .max = 20000, > + .default_scale_nu = 540679, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_ADC}, > + }, > +}; > + > +static const struct bcl_desc smb2370_data = { > + .reg_fields = bcl_smb2370_reg_fields, > + .num_reg_fields = F_MAX_FIELDS, > + .data_field_bits_size = 16, > + .thresh_field_bits_size = 8, > + .battery_config_field = REG_FIELD(0x2a50, 0, 1), > + .channel_cfg[BCL_BATT_2S][CHANNEL_IN] = { > + .base = 4500, > + .max = 8400, > + .step = 50, > + .default_scale_nu = 432918, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_INDEX}, > + }, > + .channel_cfg[BCL_BATT_2S][CHANNEL_CURR] = { > + .max = 20000, > + .default_scale_nu = 1441603, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_ADC}, > + }, > + .channel_cfg[BCL_BATT_3S][CHANNEL_IN] = { > + .base = 6750, > + .max = 12600, > + .step = 75, > + .default_scale_nu = 648790, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_INDEX}, > + }, > + .channel_cfg[BCL_BATT_3S][CHANNEL_CURR] = { > + .max = 20000, > + .default_scale_nu = 1441603, > + .thresh_type = {THRESH_TYPE_ADC, THRESH_TYPE_ADC}, > + }, > +}; > + > +/** > + * bcl_convert_raw_to_milliunit - Convert raw value to milli unit > + * @bcl: BCL device structure > + * @raw_val: Raw ADC value from hardware (signed for current, unsigned for voltage) > + * @type: type of the channel, in or curr > + * @field_width: bits size for data or threshold field > + * > + * Return: value in milli unit > + */ > +static int bcl_convert_raw_to_milliunit(const struct bcl_device *bcl, > + s32 raw_val, > + enum bcl_channel type, > + u8 field_width) > +{ > + const struct bcl_desc *desc = bcl->desc; > + u32 def_scale = desc->channel_cfg[bcl->batt_config][type].default_scale_nu; > + u32 scaling_factor = (field_width > 8) ? def_scale : (def_scale << field_width); > + > + return DIV_ROUND_CLOSEST((s64)raw_val * scaling_factor, 1000000); > +} > + > +/** > + * bcl_convert_milliunit_to_raw - Convert milli unit to raw value > + * @bcl: BCL device structure > + * @mval: threshold value in milli unit > + * @type: type of the channel, in or curr > + * @field_width: bits size for data or threshold field > + * > + * Return: Raw ADC value for hardware > + */ > +static unsigned int bcl_convert_milliunit_to_raw(const struct bcl_device *bcl, > + int mval, > + enum bcl_channel type, > + u8 field_width) > +{ > + const struct bcl_desc *desc = bcl->desc; > + u32 def_scale = desc->channel_cfg[bcl->batt_config][type].default_scale_nu; > + u32 scaling_factor = (field_width > 8) ? def_scale : (def_scale << field_width); > + > + return DIV_ROUND_CLOSEST_ULL((u64)mval * 1000000, scaling_factor); > +} > + > +/** > + * bcl_convert_milliunit_to_index - Convert milliunit to in or curr index > + * @bcl: BCL device structure > + * @val: in or curr value in milli unit > + * @type: type of the channel, in or curr > + * > + * Converts a value in milli unit to an index for BCL that use indexed thresholds. > + * > + * Return: Index value > + */ > +static unsigned int bcl_convert_milliunit_to_index(const struct bcl_device *bcl, > + int val, > + enum bcl_channel type) > +{ > + const struct bcl_desc *desc = bcl->desc; > + const struct bcl_channel_cfg *cfg = &desc->channel_cfg[bcl->batt_config][type]; > + u64 diff = (u64)val - cfg->base; > + > + return DIV_ROUND_CLOSEST_ULL(diff, cfg->step); > +} > + > +/** > + * bcl_convert_index_to_milliunit - Convert in or curr index to milli unit > + * @bcl: BCL device structure > + * @val: index value > + * @type: type of the channel, in or curr > + * > + * Converts an index value to milli unit for BCL that use indexed thresholds. > + * > + * Return: Value in millivolts or milliamps > + */ > +static unsigned int bcl_convert_index_to_milliunit(const struct bcl_device *bcl, > + int val, > + enum bcl_channel type) > +{ > + const struct bcl_desc *desc = bcl->desc; > + const struct bcl_channel_cfg *cfg = &desc->channel_cfg[bcl->batt_config][type]; > + > + return cfg->base + val * cfg->step; > +} > + > +static int bcl_in_thresh_write(struct bcl_device *bcl, long value, enum bcl_limit_alarm lvl) > +{ > + const struct bcl_desc *desc = bcl->desc; > + u32 raw_val; > + > + int thresh = clamp_val(value, desc->channel_cfg[bcl->batt_config][CHANNEL_IN].base, > + desc->channel_cfg[bcl->batt_config][CHANNEL_IN].max); > + > + if (desc->channel_cfg[bcl->batt_config][CHANNEL_IN].thresh_type[lvl] == THRESH_TYPE_ADC) > + raw_val = bcl_convert_milliunit_to_raw(bcl, thresh, CHANNEL_IN, > + desc->thresh_field_bits_size); > + else > + raw_val = bcl_convert_milliunit_to_index(bcl, thresh, CHANNEL_IN); > + > + return regmap_field_write(bcl->fields[F_IN_L0_THR + lvl], raw_val); > +} > + > +static int bcl_curr_thresh_write(struct bcl_device *bcl, long value, enum bcl_limit_alarm lvl) > +{ > + const struct bcl_desc *desc = bcl->desc; > + u32 raw_val; > + > + int thresh = clamp_val(value, 0, desc->channel_cfg[bcl->batt_config][CHANNEL_CURR].max); > + > + if (desc->channel_cfg[bcl->batt_config][CHANNEL_CURR].thresh_type[lvl] == THRESH_TYPE_ADC) > + raw_val = bcl_convert_milliunit_to_raw(bcl, thresh, CHANNEL_CURR, > + desc->thresh_field_bits_size); > + else > + raw_val = bcl_convert_milliunit_to_index(bcl, thresh, CHANNEL_CURR); > + > + return regmap_field_write(bcl->fields[F_CURR_H0_THR + lvl], raw_val); > +} > + > +static int bcl_in_thresh_read(struct bcl_device *bcl, enum bcl_limit_alarm lvl, long *out) > +{ > + int ret, thresh; > + u32 raw_val = 0; > + const struct bcl_desc *desc = bcl->desc; > + > + ret = regmap_field_read(bcl->fields[F_IN_L0_THR + lvl], &raw_val); > + if (ret) > + return ret; > + > + if (desc->channel_cfg[bcl->batt_config][CHANNEL_IN].thresh_type[lvl] == THRESH_TYPE_ADC) > + thresh = bcl_convert_raw_to_milliunit(bcl, raw_val, CHANNEL_IN, > + desc->thresh_field_bits_size); > + else > + thresh = bcl_convert_index_to_milliunit(bcl, raw_val, CHANNEL_IN); > + > + *out = thresh; > + > + return 0; > +} > + > +static int bcl_curr_thresh_read(struct bcl_device *bcl, enum bcl_limit_alarm lvl, long *out) > +{ > + int ret, thresh; > + u32 raw_val = 0; > + const struct bcl_desc *desc = bcl->desc; > + > + ret = regmap_field_read(bcl->fields[F_CURR_H0_THR + lvl], &raw_val); > + if (ret) > + return ret; > + > + if (desc->channel_cfg[bcl->batt_config][CHANNEL_CURR].thresh_type[lvl] == THRESH_TYPE_ADC) > + thresh = bcl_convert_raw_to_milliunit(bcl, raw_val, CHANNEL_CURR, > + desc->thresh_field_bits_size); > + else > + thresh = bcl_convert_index_to_milliunit(bcl, raw_val, CHANNEL_CURR); > + > + *out = thresh; > + > + return 0; > +} > + > +static int bcl_curr_input_read(struct bcl_device *bcl, long *out) > +{ > + int ret; > + u32 raw_val = 0, msb = 0; > + s32 signed_val; > + const struct bcl_desc *desc = bcl->desc; > + > + /* Return cached value if read too soon after last update */ > + if (time_before(jiffies, bcl->last_curr_updated + HZ)) { > + *out = bcl->last_curr_input; > + return 0; > + } > + > + ret = regmap_field_read(bcl->fields[F_CURR_INPUT], &raw_val); > + if (ret) > + return ret; > + > + /* For 16-bit data, read MSB and combine with LSB */ > + if (desc->data_field_bits_size == 16) { > + ret = regmap_field_read(bcl->fields[F_CURR_INPUT1], &msb); > + if (ret) > + return ret; > + raw_val |= FIELD_PREP(GENMASK(15, 8), msb); > + } > + > + /* > + * Current ADC reading is in 2's complement form. > + * Sign extend the value based on data field bit size. > + */ > + signed_val = sign_extend32(raw_val, desc->data_field_bits_size - 1); > + > + bcl->last_curr_input = > + bcl_convert_raw_to_milliunit(bcl, signed_val, CHANNEL_CURR, > + desc->data_field_bits_size); > + bcl->last_curr_updated = jiffies; > + > + *out = bcl->last_curr_input; > + > + return 0; > +} > + > +static int bcl_in_input_read(struct bcl_device *bcl, long *out) > +{ > + int ret; > + u32 raw_val = 0, msb = 0; > + const struct bcl_desc *desc = bcl->desc; > + > + /* Return cached value if read too soon after last update */ > + if (time_before(jiffies, bcl->last_in_updated + HZ)) { > + *out = bcl->last_in_input; > + return 0; > + } > + > + ret = regmap_field_read(bcl->fields[F_IN_INPUT], &raw_val); > + if (ret) > + return ret; > + > + /* For 16-bit data, read MSB and combine with LSB */ > + if (desc->data_field_bits_size == 16) { > + ret = regmap_field_read(bcl->fields[F_IN_INPUT1], &msb); > + if (ret) > + return ret; > + raw_val |= FIELD_PREP(GENMASK(15, 8), msb); > + } > + > + bcl->last_in_input = > + bcl_convert_raw_to_milliunit(bcl, raw_val, CHANNEL_IN, > + desc->data_field_bits_size); > + bcl->last_in_updated = jiffies; > + > + *out = bcl->last_in_input; > + > + return 0; > +} > + > +static int bcl_read_alarm_status(struct bcl_device *bcl, > + enum bcl_limit_alarm lvl, long *status) > +{ > + int ret; > + u32 raw_val = 0; > + > + ret = regmap_field_read(bcl->fields[F_LVL0_ALARM + lvl], &raw_val); > + if (ret) > + return ret; > + > + *status = raw_val; > + > + return 0; > +} > + > +static unsigned int bcl_get_version_major(const struct bcl_device *bcl) > +{ > + u32 raw_val = 0; > + > + regmap_field_read(bcl->fields[F_V_MAJOR], &raw_val); > + > + return raw_val; > +} > + > +static unsigned int bcl_get_version_minor(const struct bcl_device *bcl) > +{ > + u32 raw_val = 0; > + > + regmap_field_read(bcl->fields[F_V_MINOR], &raw_val); > + > + return raw_val; > +} > + > +static void bcl_hwmon_notify_event(struct bcl_device *bcl, enum bcl_limit_alarm alarm) > +{ > + if (bcl->in_attrs) > + hwmon_notify_event(bcl->hwmon_dev, hwmon_in, > + in_lvl_to_attr_map[alarm], 0); > + > + if (bcl->curr_attrs) > + hwmon_notify_event(bcl->hwmon_dev, hwmon_curr, > + curr_lvl_to_attr_map[alarm], 0); > +} > + > +static void bcl_alarm_enable_poll(struct work_struct *work) > +{ > + struct bcl_alarm_data *alarm = container_of(work, struct bcl_alarm_data, > + alarm_poll_work.work); > + struct bcl_device *bcl = (struct bcl_device *)alarm->device; > + long status; > + int ret; > + > + scoped_guard(mutex, &bcl->lock) > + ret = bcl_read_alarm_status(bcl, alarm->type, &status); > + > + /* If read failed or shutting down, reschedule */ > + if (ret || READ_ONCE(alarm->shutting_down)) { > + if (!READ_ONCE(alarm->shutting_down)) > + schedule_delayed_work(&alarm->alarm_poll_work, > + msecs_to_jiffies(BCL_ALARM_POLLING_MS)); > + return; > + } > + > + /* Check if alarm cleared and IRQ needs re-enabling */ > + scoped_guard(mutex, &alarm->a_lock) { > + if (!status && !alarm->irq_enabled) { > + alarm->irq_enabled = true; > + enable_irq(alarm->irq); > + > + if (!alarm->irq_wake_enabled && !enable_irq_wake(alarm->irq)) > + alarm->irq_wake_enabled = true; > + > + bcl_hwmon_notify_event(bcl, alarm->type); > + return; > + } > + } > + > + /* Alarm still active, reschedule polling */ > + if (!READ_ONCE(alarm->shutting_down)) > + schedule_delayed_work(&alarm->alarm_poll_work, > + msecs_to_jiffies(BCL_ALARM_POLLING_MS)); > +} > + > +static irqreturn_t bcl_handle_alarm(int irq, void *data) > +{ > + struct bcl_alarm_data *alarm = data; > + struct bcl_device *bcl = (struct bcl_device *)alarm->device; > + long status; > + > + if (READ_ONCE(alarm->shutting_down)) > + return IRQ_HANDLED; > + > + guard(mutex)(&bcl->lock); > + > + if (bcl_read_alarm_status(bcl, alarm->type, &status) || !status) > + return IRQ_HANDLED; > + > + bcl_hwmon_notify_event(bcl, alarm->type); > + > + guard(mutex)(&alarm->a_lock); > + if (alarm->shutting_down) > + return IRQ_HANDLED; > + > + if (alarm->irq_enabled) { > + alarm->irq_enabled = false; > + disable_irq_nosync(alarm->irq); > + } > + > + if (alarm->irq_wake_enabled) { > + disable_irq_wake(alarm->irq); > + alarm->irq_wake_enabled = false; > + } > + > + schedule_delayed_work(&alarm->alarm_poll_work, > + msecs_to_jiffies(BCL_ALARM_POLLING_MS)); > + > + return IRQ_HANDLED; > +} > + > +static umode_t bcl_hwmon_is_visible(const void *data, > + enum hwmon_sensor_types type, > + u32 attr, int channel) > +{ > + switch (type) { > + case hwmon_in: > + switch (attr) { > + case hwmon_in_input: > + case hwmon_in_label: > + case hwmon_in_min_alarm: > + case hwmon_in_lcrit_alarm: > + return 0444; > + case hwmon_in_min: > + case hwmon_in_lcrit: > + return 0644; > + default: > + return 0; > + } > + case hwmon_curr: > + switch (attr) { > + case hwmon_curr_input: > + case hwmon_curr_label: > + case hwmon_curr_max_alarm: > + case hwmon_curr_crit_alarm: > + return 0444; > + case hwmon_curr_max: > + case hwmon_curr_crit: > + return 0644; > + default: > + return 0; > + } > + default: > + return 0; > + } > +} > + > +static int bcl_hwmon_write(struct device *dev, enum hwmon_sensor_types type, > + u32 attr, int channel, long val) > +{ > + struct bcl_device *bcl = dev_get_drvdata(dev); > + > + guard(mutex)(&bcl->lock); > + > + switch (type) { > + case hwmon_in: > + switch (attr) { > + case hwmon_in_min: > + case hwmon_in_lcrit: > + return bcl_in_thresh_write(bcl, val, in_attr_to_lvl_map[attr]); > + default: > + return -EOPNOTSUPP; > + } > + case hwmon_curr: > + switch (attr) { > + case hwmon_curr_max: > + case hwmon_curr_crit: > + return bcl_curr_thresh_write(bcl, val, curr_attr_to_lvl_map[attr]); > + default: > + return -EOPNOTSUPP; > + } > + default: > + return -EOPNOTSUPP; > + } > +} > + > +static int bcl_in_read(struct bcl_device *bcl, u32 attr, long *value) > +{ > + guard(mutex)(&bcl->lock); > + > + switch (attr) { > + case hwmon_in_input: > + return bcl_in_input_read(bcl, value); > + case hwmon_in_min: > + case hwmon_in_lcrit: > + return bcl_in_thresh_read(bcl, in_attr_to_lvl_map[attr], value); > + case hwmon_in_min_alarm: > + case hwmon_in_lcrit_alarm: > + return bcl_read_alarm_status(bcl, in_attr_to_lvl_map[attr], value); > + default: > + return -EOPNOTSUPP; > + } > +} > + > +static int bcl_curr_read(struct bcl_device *bcl, u32 attr, long *value) > +{ > + guard(mutex)(&bcl->lock); > + > + switch (attr) { > + case hwmon_curr_input: > + return bcl_curr_input_read(bcl, value); > + case hwmon_curr_max: > + case hwmon_curr_crit: > + return bcl_curr_thresh_read(bcl, curr_attr_to_lvl_map[attr], value); > + case hwmon_curr_max_alarm: > + case hwmon_curr_crit_alarm: > + return bcl_read_alarm_status(bcl, curr_attr_to_lvl_map[attr], value); > + default: > + return -EOPNOTSUPP; > + } > +} > + > +static int bcl_hwmon_read(struct device *dev, enum hwmon_sensor_types type, > + u32 attr, int channel, long *value) > +{ > + struct bcl_device *bcl = dev_get_drvdata(dev); > + > + switch (type) { > + case hwmon_in: > + return bcl_in_read(bcl, attr, value); > + case hwmon_curr: > + return bcl_curr_read(bcl, attr, value); > + default: > + return -EOPNOTSUPP; > + } > +} > + > +static int bcl_hwmon_read_string(struct device *dev, > + enum hwmon_sensor_types type, > + u32 attr, int channel, const char **str) > +{ > + if (type == hwmon_in && attr == hwmon_in_label) > + *str = "Voltage"; > + else if (type == hwmon_curr && attr == hwmon_curr_label) > + *str = "Current"; > + else > + *str = NULL; > + > + return *str ? 0 : -EOPNOTSUPP; > +} Please drop those labels. Labels are intended to show _where_ a sensor is located or which current/voltage is monitored. That a voltage is a voltage and that a current is a current does not need a label and defeats the purpose of the label. > + > +static const struct hwmon_ops bcl_hwmon_ops = { > + .is_visible = bcl_hwmon_is_visible, > + .read = bcl_hwmon_read, > + .read_string = bcl_hwmon_read_string, > + .write = bcl_hwmon_write, > +}; > + > +static int bcl_detect_battery_config(struct bcl_device *bcl) > +{ > + u32 reg_val = 0; > + int ret; > + > + ret = regmap_field_read(bcl->batt_config_regfield, ®_val); > + if (ret) { > + dev_err(bcl->dev, "Failed to read battery config register: %d\n", ret); > + return ret; > + } > + > + /* > + * Map register value to battery configuration. > + * As per hardware documentation: > + * 0 -> 2S, 1 -> 3S, 2 -> 4S (unsupported) > + */ > + switch (reg_val) { > + case 0: > + bcl->batt_config = BCL_BATT_2S; > + return 0; > + case 1: > + bcl->batt_config = BCL_BATT_3S; > + return 0; > + default: > + dev_err(bcl->dev, "Unsupported battery configuration: 0x%x\n", > + reg_val); > + return -EINVAL; > + } > +} > + > +static int bcl_update_scaling_factors(struct bcl_device *bcl) > +{ > + const struct bcl_desc *desc = bcl->desc; > + int ret; > + > + /* Check if battery detection is supported */ > + if (desc->battery_config_field.reg == 0) { > + /* No battery detection - use BCL_BATT_1S */ > + bcl->batt_config = BCL_BATT_1S; > + dev_dbg(bcl->dev, "Using default 1S battery configuration\n"); > + return 0; > + } > + > + bcl->batt_config_regfield = devm_regmap_field_alloc(bcl->dev, > + bcl->regmap, > + desc->battery_config_field); > + if (IS_ERR(bcl->batt_config_regfield)) { > + dev_err(bcl->dev, "Failed to allocate battery config regmap field\n"); > + return PTR_ERR(bcl->batt_config_regfield); > + } > + > + ret = bcl_detect_battery_config(bcl); > + if (ret < 0) > + return ret; > + > + dev_dbg(bcl->dev, > + "%dS battery: BASE=%u mV,MAX=%u mV,STEP=%u mV,V_SCALE=%u nV, I_SCALE=%u nA\n", > + bcl->batt_config + 1, > + desc->channel_cfg[bcl->batt_config][CHANNEL_IN].base, > + desc->channel_cfg[bcl->batt_config][CHANNEL_IN].max, > + desc->channel_cfg[bcl->batt_config][CHANNEL_IN].step, > + desc->channel_cfg[bcl->batt_config][CHANNEL_IN].default_scale_nu, > + desc->channel_cfg[bcl->batt_config][CHANNEL_CURR].default_scale_nu); > + > + return 0; > +} > + > +static int bcl_build_hwmon_info(struct bcl_device *bcl) > +{ > + const struct bcl_desc *desc = bcl->desc; > + struct hwmon_channel_info *in_info = NULL, *curr_info = NULL; > + const struct hwmon_channel_info **info_array; > + int num_channels = 0; > + u32 val = 0; > + bool in_mon_enabled = false; > + bool in_input_enabled = false; > + bool curr_mon_enabled = false; > + int ret; > + > + /* Check if voltage monitoring is enabled */ > + ret = regmap_field_read(bcl->fields[F_IN_MON_EN], &val); > + if (ret) > + return ret; > + in_mon_enabled = !!val; > + > + /* Check if voltage input reading is enabled */ > + if (desc->data_field_bits_size != 0) { > + ret = regmap_field_read(bcl->fields[F_IN_INPUT_EN], &val); > + if (ret) > + return ret; > + in_input_enabled = !!val; > + } > + > + /* Check if current monitoring is enabled */ > + if (desc->num_reg_fields > F_CURR_H0_THR) { > + ret = regmap_field_read(bcl->fields[F_CURR_MON_EN], &val); > + if (ret) > + return ret; > + curr_mon_enabled = !!val; > + } > + > + /* Ensure at least one channel is enabled */ > + if (!in_mon_enabled && !curr_mon_enabled) { > + dev_err(bcl->dev, "No BCL channels enabled in hardware\n"); > + return -ENODEV; > + } > + > + if (in_mon_enabled) { > + u32 *in_config; > + > + bcl->in_attrs = HWMON_I_LABEL | HWMON_I_MIN | HWMON_I_LCRIT | > + HWMON_I_MIN_ALARM | HWMON_I_LCRIT_ALARM; > + > + if (in_input_enabled) > + bcl->in_attrs |= HWMON_I_INPUT; > + > + in_info = devm_kzalloc(bcl->dev, sizeof(*in_info), GFP_KERNEL); > + if (!in_info) > + return -ENOMEM; > + > + in_config = devm_kzalloc(bcl->dev, 2 * sizeof(u32), GFP_KERNEL); > + if (!in_config) > + return -ENOMEM; > + > + in_config[0] = bcl->in_attrs; > + in_info->type = hwmon_in; > + in_info->config = in_config; > + num_channels++; > + } > + > + if (curr_mon_enabled) { > + u32 *curr_config; > + > + bcl->curr_attrs = HWMON_C_INPUT | HWMON_C_LABEL | HWMON_C_MAX | > + HWMON_C_CRIT | HWMON_C_MAX_ALARM | HWMON_C_CRIT_ALARM; > + > + curr_info = devm_kzalloc(bcl->dev, sizeof(*curr_info), GFP_KERNEL); > + if (!curr_info) > + return -ENOMEM; > + > + curr_config = devm_kzalloc(bcl->dev, 2 * sizeof(u32), GFP_KERNEL); > + if (!curr_config) > + return -ENOMEM; > + > + curr_config[0] = bcl->curr_attrs; > + curr_info->type = hwmon_curr; > + curr_info->config = curr_config; > + num_channels++; > + } > + > + /* Allocate info array (num_channels + 1 for NULL terminator) */ > + info_array = devm_kcalloc(bcl->dev, num_channels + 1, > + sizeof(*info_array), GFP_KERNEL); > + if (!info_array) > + return -ENOMEM; > + > + num_channels = 0; > + if (in_info) > + info_array[num_channels++] = in_info; > + if (curr_info) > + info_array[num_channels++] = curr_info; > + > + bcl->hwmon_info = info_array; > + > + bcl->hwmon_chip_info.ops = &bcl_hwmon_ops; > + bcl->hwmon_chip_info.info = bcl->hwmon_info; > + I am curious: Why not use static initialization and use the is_visible function to determine if an attribute is visible or not ? From the earlier exchange I had the impression that there is a large number of current and voltage channels, but it looks like there is only one each. That doesn't really warrant or need all this dynamic code to generate the description. > + return 0; > +} > + > +static void bcl_alarm_work_cleanup_action(void *data) > +{ > + struct bcl_alarm_data *alarm = data; > + > + /* Set shutting_down flag to prevent work from being rescheduled */ > + scoped_guard(mutex, &alarm->a_lock) > + WRITE_ONCE(alarm->shutting_down, true); > + > + cancel_delayed_work_sync(&alarm->alarm_poll_work); > +} > + > +static void bcl_alarm_wake_cleanup_action(void *data) > +{ > + struct bcl_alarm_data *alarm = data; > + > + guard(mutex)(&alarm->a_lock); > + if (alarm->irq_wake_enabled) { > + disable_irq_wake(alarm->irq); > + alarm->irq_wake_enabled = false; > + } > +} > + > +static int bcl_alarm_irq_init(struct platform_device *pdev, > + struct bcl_device *bcl) > +{ > + int ret, irq_num, i; > + struct bcl_alarm_data *alarm; > + > + for (i = 0; i < ARRAY_SIZE(bcl->bcl_alarms); i++) { > + alarm = &bcl->bcl_alarms[i]; > + alarm->type = i; > + alarm->device = bcl; > + ret = devm_mutex_init(bcl->dev, &alarm->a_lock); > + if (ret) > + return ret; > + > + /* Initialize work before IRQ request */ > + INIT_DELAYED_WORK(&alarm->alarm_poll_work, bcl_alarm_enable_poll); > + > + irq_num = platform_get_irq_byname(pdev, bcl_int_names[i]); > + if (irq_num < 0) > + return irq_num; > + > + alarm->irq = irq_num; > + alarm->irq_enabled = true; > + > + ret = devm_request_threaded_irq(&pdev->dev, irq_num, NULL, > + bcl_handle_alarm, IRQF_ONESHOT, > + bcl_int_names[i], alarm); > + if (ret) > + return ret; > + > + ret = devm_add_action_or_reset(&pdev->dev, bcl_alarm_work_cleanup_action, > + alarm); > + if (ret) > + return ret; > + > + if (!enable_irq_wake(irq_num)) > + alarm->irq_wake_enabled = true; > + > + ret = devm_add_action_or_reset(&pdev->dev, bcl_alarm_wake_cleanup_action, > + alarm); > + if (ret) > + return ret; > + } > + > + return 0; > +} > + > +static int bcl_regmap_field_init(struct device *dev, struct bcl_device *bcl, > + const struct bcl_desc *data) > +{ > + int i; > + > + /* > + * Note: We use a mutable local copy of struct reg_field (not const) > + * because we need to modify the .reg field to add the BCL base offset. > + */ > + for (i = 0; i < data->num_reg_fields; i++) { > + struct reg_field field = data->reg_fields[i]; > + > + /* Skip uninitialized fields */ > + if (field.reg == 0 && field.lsb == 0 && field.msb == 0) > + continue; > + > + field.reg += bcl->base; > + > + bcl->fields[i] = devm_regmap_field_alloc(dev, bcl->regmap, field); > + if (IS_ERR(bcl->fields[i])) > + return PTR_ERR(bcl->fields[i]); > + } > + > + return 0; > +} > + > +static int bcl_probe(struct platform_device *pdev) > +{ > + struct bcl_device *bcl; > + int ret; > + u32 reg; > + > + bcl = devm_kzalloc(&pdev->dev, sizeof(*bcl), GFP_KERNEL); > + if (!bcl) > + return -ENOMEM; > + > + bcl->dev = &pdev->dev; > + bcl->desc = device_get_match_data(&pdev->dev); > + if (!bcl->desc) > + return dev_err_probe(&pdev->dev, -EINVAL, "Failed to get device match data\n"); > + > + ret = devm_mutex_init(bcl->dev, &bcl->lock); > + if (ret) > + return ret; > + > + bcl->regmap = dev_get_regmap(pdev->dev.parent, NULL); > + if (!bcl->regmap) > + return dev_err_probe(&pdev->dev, -EINVAL, "Couldn't get parent's regmap\n"); > + > + ret = device_property_read_u32(&pdev->dev, "reg", ®); > + if (ret < 0) > + return dev_err_probe(&pdev->dev, ret, "Failed to read 'reg' property\n"); > + > + bcl->base = reg; > + > + ret = bcl_regmap_field_init(bcl->dev, bcl, bcl->desc); > + if (ret < 0) > + return dev_err_probe(&pdev->dev, ret, "Unable to allocate regmap fields\n"); > + > + ret = regmap_field_read(bcl->fields[F_CTL_EN], ®); > + if (ret < 0) > + return dev_err_probe(&pdev->dev, ret, "Failed to read BCL enable status\n"); > + > + if (!reg) > + return dev_err_probe(&pdev->dev, -ENODEV, "BCL is not enabled by bootloader\n"); > + > + ret = bcl_update_scaling_factors(bcl); > + if (ret < 0) > + return dev_err_probe(&pdev->dev, ret, "Failed to update scaling factors\n"); > + > + ret = bcl_build_hwmon_info(bcl); > + if (ret < 0) > + return dev_err_probe(&pdev->dev, ret, "Failed to build hwmon info\n"); > + > + dev_set_drvdata(&pdev->dev, bcl); > + > + bcl->hwmon_name = devm_hwmon_sanitize_name(&pdev->dev, > + dev_name(bcl->dev)); > + if (IS_ERR(bcl->hwmon_name)) > + return PTR_ERR(bcl->hwmon_name); > + > + bcl->hwmon_dev = devm_hwmon_device_register_with_info(&pdev->dev, > + bcl->hwmon_name, > + bcl, > + &bcl->hwmon_chip_info, > + NULL); > + if (IS_ERR(bcl->hwmon_dev)) > + return dev_err_probe(&pdev->dev, PTR_ERR(bcl->hwmon_dev), > + "Failed to register hwmon device\n"); > + > + ret = bcl_alarm_irq_init(pdev, bcl); > + if (ret < 0) > + return dev_err_probe(&pdev->dev, ret, "Failed to initialize alarm IRQs\n"); > + > + dev_dbg(&pdev->dev, "BCL hwmon device with version: %u.%u registered\n", > + bcl_get_version_major(bcl), bcl_get_version_minor(bcl)); > + > + return 0; > +} > + > +static const struct of_device_id bcl_match[] = { > + { > + .compatible = "qcom,pm7250b-bcl", > + .data = &pm7250b_data, > + }, { > + .compatible = "qcom,pm8350c-bcl", > + .data = &pm8350c_data, > + }, { > + .compatible = "qcom,pm8550-bcl", > + .data = &pm8550_data, > + }, { > + .compatible = "qcom,pmh0101-bcl", > + .data = &pmh0101_data, > + }, { > + .compatible = "qcom,pmih0108-bcl", > + .data = &pmih0108_data, > + }, { > + .compatible = "qcom,smb2360-bcl", > + .data = &smb2360_data, > + }, { > + .compatible = "qcom,smb2370-bcl", > + .data = &smb2370_data, > + }, > + { } > +}; > +MODULE_DEVICE_TABLE(of, bcl_match); > + > +static struct platform_driver bcl_driver = { > + .probe = bcl_probe, > + .driver = { > + .name = "qcom-bcl-hwmon", > + .of_match_table = bcl_match, > + }, > +}; > + > +module_platform_driver(bcl_driver); > + > +MODULE_DESCRIPTION("Qualcomm SPMI BCL HWMON driver"); > +MODULE_LICENSE("GPL"); >