From 4a53e15414c3ed9c73f9a725c774d68749d1f437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:00 +0000 Subject: [PATCH 01/36] dt-bindings: power: supply: max17042: add support for max77759 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Maxim MAX77759 is a companion PMIC intended for use in mobile phones and tablets. It is used on Google Pixel 6 and 6 Pro (oriole and raven). Amongst others, it contains a fuel gauge that is similar to the ones supported by this binding. The fuel gauge can measure battery charge and discharge current, battery voltage, battery temperature, and the Type C connector's temperature. Reviewed-by: Peter Griffin Acked-by: Conor Dooley Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-1-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- .../devicetree/bindings/power/supply/maxim,max17042.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml b/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml index 14242de7fc08..055d1f2ee0ba 100644 --- a/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml +++ b/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml @@ -20,6 +20,7 @@ properties: - maxim,max17050 - maxim,max17055 - maxim,max77705-battery + - maxim,max77759-fg - maxim,max77849-battery reg: @@ -28,7 +29,7 @@ properties: interrupts: maxItems: 1 description: | - The ALRT pin, an open-drain interrupt. + The ALRT pin (or FG_INTB pin on MAX77759), an open-drain interrupt. maxim,rsns-microohm: $ref: /schemas/types.yaml#/definitions/uint32 From f76deab4e9035bb054b38f067c374ca7ee1e1faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:01 +0000 Subject: [PATCH 02/36] dt-bindings: power: supply: max17042: support shunt-resistor-micro-ohms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This binding supports the vendor-specific property maxim,rsns-microohm to describe the value of a shunt resistor required when measuring currents. shunt-resistor-micro-ohms is a standard property with the same meaning. Standard properties should be used instead of vendor- specific ones of similar intention when possible. Allow this standard property here, while also deprecating the existing vendor-specific property maxim,rsns-microohm. Reviewed-by: Peter Griffin Acked-by: Conor Dooley Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-2-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- .../devicetree/bindings/power/supply/maxim,max17042.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml b/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml index 055d1f2ee0ba..25ea8e19b980 100644 --- a/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml +++ b/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml @@ -31,7 +31,13 @@ properties: description: | The ALRT pin (or FG_INTB pin on MAX77759), an open-drain interrupt. + shunt-resistor-micro-ohms: + description: + Resistance of rsns resistor in micro Ohms (datasheet-recommended value is 10000). + Defining this property enables current-sense functionality. + maxim,rsns-microohm: + deprecated: true $ref: /schemas/types.yaml#/definitions/uint32 description: | Resistance of rsns resistor in micro Ohms (datasheet-recommended value is 10000). From a060c6fe82d6ee41cb592fc4760e814b64a92f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:02 +0000 Subject: [PATCH 03/36] dt-bindings: power: supply: max17042: drop formatting specifier | MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | denotes a literal (preformatted) block and is not necessary here. Drop them from this file. Acked-by: Conor Dooley Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-3-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- .../bindings/power/supply/maxim,max17042.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml b/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml index 25ea8e19b980..242b33f2bcba 100644 --- a/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml +++ b/Documentation/devicetree/bindings/power/supply/maxim,max17042.yaml @@ -28,7 +28,7 @@ properties: interrupts: maxItems: 1 - description: | + description: The ALRT pin (or FG_INTB pin on MAX77759), an open-drain interrupt. shunt-resistor-micro-ohms: @@ -39,31 +39,31 @@ properties: maxim,rsns-microohm: deprecated: true $ref: /schemas/types.yaml#/definitions/uint32 - description: | + description: Resistance of rsns resistor in micro Ohms (datasheet-recommended value is 10000). Defining this property enables current-sense functionality. maxim,cold-temp: $ref: /schemas/types.yaml#/definitions/uint32 - description: | + description: Temperature threshold to report battery as cold (in tenths of degree Celsius). Default is not to report cold events. maxim,over-heat-temp: $ref: /schemas/types.yaml#/definitions/uint32 - description: | + description: Temperature threshold to report battery as over heated (in tenths of degree Celsius). Default is not to report over heating events. maxim,dead-volt: $ref: /schemas/types.yaml#/definitions/uint32 - description: | + description: Voltage threshold to report battery as dead (in mV). Default is not to report dead battery events. maxim,over-volt: $ref: /schemas/types.yaml#/definitions/uint32 - description: | + description: Voltage threshold to report battery as over voltage (in mV). Default is not to report over-voltage events. From e370b67c2ceb3e3c4577da0a882b1ede87ef485e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:03 +0000 Subject: [PATCH 04/36] power: supply: max17042: fix a comment typo (then -> than) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix this trivial typo where than should be used instead of then. Reviewed-by: Peter Griffin Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-4-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- drivers/power/supply/max17042_battery.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/power/supply/max17042_battery.c b/drivers/power/supply/max17042_battery.c index acea176101fa..07759d4fdc37 100644 --- a/drivers/power/supply/max17042_battery.c +++ b/drivers/power/supply/max17042_battery.c @@ -131,7 +131,7 @@ static int max17042_get_status(struct max17042_chip *chip, int *status) * FullCAP to match RepCap when it detects end of charging. * * When this cycle the battery gets charged to a higher (calculated) - * capacity then the previous cycle then FullCAP will get updated + * capacity than the previous cycle then FullCAP will get updated * continuously once end-of-charge detection kicks in, so allow the * 2 to differ a bit. */ From 699f0f71ac98cf79fecdcab0a604b25f11c580b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:04 +0000 Subject: [PATCH 05/36] power: supply: max17042: use dev_err_probe() where appropriate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev_err_probe() exists to simplify code, harmonise error messages, and set the deferred probe reason if relevant - there's no reason not to use it here. While at it, return the actual error from devm_regmap_init_i2c() rather than overwriting with -EINVAL, when relevant. Reviewed-by: Peter Griffin Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-5-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- drivers/power/supply/max17042_battery.c | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/drivers/power/supply/max17042_battery.c b/drivers/power/supply/max17042_battery.c index 07759d4fdc37..b9277f81a25d 100644 --- a/drivers/power/supply/max17042_battery.c +++ b/drivers/power/supply/max17042_battery.c @@ -1053,16 +1053,14 @@ static int max17042_probe(struct i2c_client *client, struct device *dev, int irq chip->dev = dev; chip->chip_type = chip_type; chip->regmap = devm_regmap_init_i2c(client, &max17042_regmap_config); - if (IS_ERR(chip->regmap)) { - dev_err(dev, "Failed to initialize regmap\n"); - return -EINVAL; - } + if (IS_ERR(chip->regmap)) + return dev_err_probe(dev, PTR_ERR(chip->regmap), + "Failed to initialize regmap\n"); chip->pdata = max17042_get_pdata(chip); - if (!chip->pdata) { - dev_err(dev, "no platform data provided\n"); - return -EINVAL; - } + if (!chip->pdata) + return dev_err_probe(dev, -EINVAL, + "no platform data provided\n"); dev_set_drvdata(dev, chip); psy_cfg.drv_data = chip; @@ -1090,10 +1088,9 @@ static int max17042_probe(struct i2c_client *client, struct device *dev, int irq chip->battery = devm_power_supply_register(dev, max17042_desc, &psy_cfg); - if (IS_ERR(chip->battery)) { - dev_err(dev, "failed: power supply register\n"); - return PTR_ERR(chip->battery); - } + if (IS_ERR(chip->battery)) + return dev_err_probe(dev, PTR_ERR(chip->battery), + "failed: power supply register\n"); if (irq) { unsigned int flags = IRQF_ONESHOT | IRQF_SHARED | IRQF_PROBE_SHARED; From 9a44949da669708f19d29141e65b3ac774d08f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:05 +0000 Subject: [PATCH 06/36] power: supply: max17042: avoid overflow when determining health MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If vmax has the default value of INT_MAX (e.g. because not specified in DT), battery health is reported as over-voltage. This is because adding any value to vmax (the vmax tolerance in this case) causes it to wrap around, making it negative and smaller than the measured battery voltage. Avoid that by using size_add(). Fixes: edd4ab055931 ("power: max17042_battery: add HEALTH and TEMP_* properties support") Cc: stable@vger.kernel.org Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-6-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- drivers/power/supply/max17042_battery.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/power/supply/max17042_battery.c b/drivers/power/supply/max17042_battery.c index b9277f81a25d..39091fb31711 100644 --- a/drivers/power/supply/max17042_battery.c +++ b/drivers/power/supply/max17042_battery.c @@ -201,7 +201,7 @@ static int max17042_get_battery_health(struct max17042_chip *chip, int *health) goto out; } - if (vbatt > chip->pdata->vmax + MAX17042_VMAX_TOLERANCE) { + if (vbatt > size_add(chip->pdata->vmax, MAX17042_VMAX_TOLERANCE)) { *health = POWER_SUPPLY_HEALTH_OVERVOLTAGE; goto out; } From 0c5a6dc85d739c41f5240fd149f42f26f0665aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:06 +0000 Subject: [PATCH 07/36] power: supply: max17042: time to empty is meaningless when charging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When charging, the fuel gauge reports U16_MAX as time to empty. Ignoring this special case (as this driver currently does), causes the remaining time to be reported as ~102hours, which is incorrect. Update the code to not return anything in this case. Reviewed-by: Peter Griffin Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-7-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- drivers/power/supply/max17042_battery.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/power/supply/max17042_battery.c b/drivers/power/supply/max17042_battery.c index 39091fb31711..0a6960bbf3a2 100644 --- a/drivers/power/supply/max17042_battery.c +++ b/drivers/power/supply/max17042_battery.c @@ -430,6 +430,10 @@ static int max17042_get_property(struct power_supply *psy, if (ret < 0) return ret; + /* when charging, the value is not meaningful */ + if (data == U16_MAX) + return -ENODATA; + val->intval = data * 5625 / 1000; break; default: From 2288d5eaca2249b1b8a80af063808cfc42e2a834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:07 +0000 Subject: [PATCH 08/36] power: supply: max17042: support standard shunt-resistor-micro-ohms DT property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit shunt-resistor-micro-ohms is a standard property used to describe the value of a shunt resistor required when measuring currents. Standard properties should be used instead of vendor-specific ones of similar intention when possible. Try to read it from DT, and fall back to the vendor-specific property maxim,rsns-microohm if unsuccessful for compatibility with existing DTs. Reviewed-by: Peter Griffin Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-8-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- drivers/power/supply/max17042_battery.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/power/supply/max17042_battery.c b/drivers/power/supply/max17042_battery.c index 0a6960bbf3a2..e21d2bd7e231 100644 --- a/drivers/power/supply/max17042_battery.c +++ b/drivers/power/supply/max17042_battery.c @@ -925,8 +925,12 @@ max17042_get_of_pdata(struct max17042_chip *chip) /* * Require current sense resistor value to be specified for * current-sense functionality to be enabled at all. + * maxim,rsns-microohm is the property name used by older DTs and kept + * for compatibility. */ - if (of_property_read_u32(np, "maxim,rsns-microohm", &prop) == 0) { + if ((of_property_read_u32(np, "shunt-resistor-micro-ohms", + &prop) == 0) || + (of_property_read_u32(np, "maxim,rsns-microohm", &prop) == 0)) { pdata->r_sns = prop; pdata->enable_current_sense = true; } From 2864fb6aa947703d290b52b1b030b0b74d0a6128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:08 +0000 Subject: [PATCH 09/36] power: supply: max17042: initial support for Maxim MAX77759 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Maxim MAX77759 is a companion PMIC intended for use in mobile phones and tablets. It is used on Google Pixel 6 and 6 Pro (oriole and raven). Amongst others, it contains a fuel gauge that is similar to the ones supported by this driver. The fuel gauge can measure battery charge and discharge current, battery voltage, battery temperature, and the Type C connector's temperature. The MAX77759 incorporates the Maxim ModelGauge m5 algorithm. It, as well as previous generations like m3 on max17047/max17050, requires the host to save/restore some register values across power cycles to maintain full accuracy. Extending the driver for such support is out of scope in this initial commit. Reviewed-by: Peter Griffin Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-9-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- drivers/power/supply/max17042_battery.c | 59 +++++++++++++++++++++++-- include/linux/power/max17042_battery.h | 24 +++++++++- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/drivers/power/supply/max17042_battery.c b/drivers/power/supply/max17042_battery.c index e21d2bd7e231..b9a21cef2cc6 100644 --- a/drivers/power/supply/max17042_battery.c +++ b/drivers/power/supply/max17042_battery.c @@ -650,7 +650,8 @@ static void max17042_write_config_regs(struct max17042_chip *chip) regmap_write(map, MAX17042_RelaxCFG, config->relax_cfg); if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17047 || chip->chip_type == MAXIM_DEVICE_TYPE_MAX17050 || - chip->chip_type == MAXIM_DEVICE_TYPE_MAX17055) + chip->chip_type == MAXIM_DEVICE_TYPE_MAX17055 || + chip->chip_type == MAXIM_DEVICE_TYPE_MAX77759) regmap_write(map, MAX17047_FullSOCThr, config->full_soc_thresh); } @@ -787,7 +788,8 @@ static inline void max17042_override_por_values(struct max17042_chip *chip) if ((chip->chip_type == MAXIM_DEVICE_TYPE_MAX17042) || (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17047) || - (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17050)) { + (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17050) || + (chip->chip_type == MAXIM_DEVICE_TYPE_MAX77759)) { max17042_override_por(map, MAX17042_IAvg_empty, config->iavg_empty); max17042_override_por(map, MAX17042_TempNom, config->temp_nom); max17042_override_por(map, MAX17042_TempLim, config->temp_lim); @@ -796,7 +798,8 @@ static inline void max17042_override_por_values(struct max17042_chip *chip) if ((chip->chip_type == MAXIM_DEVICE_TYPE_MAX17047) || (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17050) || - (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17055)) { + (chip->chip_type == MAXIM_DEVICE_TYPE_MAX17055) || + (chip->chip_type == MAXIM_DEVICE_TYPE_MAX77759)) { max17042_override_por(map, MAX17047_V_empty, config->vempty); } } @@ -1019,6 +1022,45 @@ static const struct regmap_config max17042_regmap_config = { .val_format_endian = REGMAP_ENDIAN_NATIVE, }; +static const struct regmap_range max77759_fg_registers[] = { + regmap_reg_range(MAX17042_STATUS, MAX77759_MixAtFull), + regmap_reg_range(MAX17042_VFSOC0Enable, MAX17042_VFSOC0Enable), + regmap_reg_range(MAX17042_MLOCKReg1, MAX17042_MLOCKReg2), + regmap_reg_range(MAX17042_MODELChrTbl, MAX17055_TimerH), + regmap_reg_range(MAX77759_IIn, MAX77759_IIn), + regmap_reg_range(MAX17055_AtQResidual, MAX17055_AtAvCap), + regmap_reg_range(MAX17042_OCVInternal, MAX17042_OCVInternal), + regmap_reg_range(MAX17042_VFSOC, MAX17042_VFSOC), +}; + +static const struct regmap_range max77759_fg_ro_registers[] = { + regmap_reg_range(MAX17042_FSTAT, MAX17042_FSTAT), + regmap_reg_range(MAX17042_OCVInternal, MAX17042_OCVInternal), + regmap_reg_range(MAX17042_VFSOC, MAX17042_VFSOC), +}; + +static const struct regmap_access_table max77759_fg_write_table = { + .yes_ranges = max77759_fg_registers, + .n_yes_ranges = ARRAY_SIZE(max77759_fg_registers), + .no_ranges = max77759_fg_ro_registers, + .n_no_ranges = ARRAY_SIZE(max77759_fg_ro_registers), +}; + +static const struct regmap_access_table max77759_fg_rd_table = { + .yes_ranges = max77759_fg_registers, + .n_yes_ranges = ARRAY_SIZE(max77759_fg_registers), +}; + +static const struct regmap_config max77759_fg_regmap_cfg = { + .reg_bits = 8, + .val_bits = 16, + .max_register = 0xff, + .wr_table = &max77759_fg_write_table, + .rd_table = &max77759_fg_rd_table, + .val_format_endian = REGMAP_ENDIAN_NATIVE, + .cache_type = REGCACHE_NONE, +}; + static const struct power_supply_desc max17042_psy_desc = { .name = "max170xx_battery", .type = POWER_SUPPLY_TYPE_BATTERY, @@ -1045,6 +1087,7 @@ static int max17042_probe(struct i2c_client *client, struct device *dev, int irq { struct i2c_adapter *adapter = client->adapter; const struct power_supply_desc *max17042_desc = &max17042_psy_desc; + const struct regmap_config *regmap_config; struct power_supply_config psy_cfg = {}; struct max17042_chip *chip; int ret; @@ -1060,7 +1103,12 @@ static int max17042_probe(struct i2c_client *client, struct device *dev, int irq chip->dev = dev; chip->chip_type = chip_type; - chip->regmap = devm_regmap_init_i2c(client, &max17042_regmap_config); + + if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX77759) + regmap_config = &max77759_fg_regmap_cfg; + else + regmap_config = &max17042_regmap_config; + chip->regmap = devm_regmap_init_i2c(client, regmap_config); if (IS_ERR(chip->regmap)) return dev_err_probe(dev, PTR_ERR(chip->regmap), "Failed to initialize regmap\n"); @@ -1241,6 +1289,8 @@ static const struct of_device_id max17042_dt_match[] __used = { .data = (void *) MAXIM_DEVICE_TYPE_MAX17055 }, { .compatible = "maxim,max77705-battery", .data = (void *) MAXIM_DEVICE_TYPE_MAX17047 }, + { .compatible = "maxim,max77759-fg", + .data = (void *) MAXIM_DEVICE_TYPE_MAX77759 }, { .compatible = "maxim,max77849-battery", .data = (void *) MAXIM_DEVICE_TYPE_MAX17047 }, { }, @@ -1253,6 +1303,7 @@ static const struct i2c_device_id max17042_id[] = { { "max17047", MAXIM_DEVICE_TYPE_MAX17047 }, { "max17050", MAXIM_DEVICE_TYPE_MAX17050 }, { "max17055", MAXIM_DEVICE_TYPE_MAX17055 }, + { "max77759-fg", MAXIM_DEVICE_TYPE_MAX77759 }, { "max77849-battery", MAXIM_DEVICE_TYPE_MAX17047 }, { } }; diff --git a/include/linux/power/max17042_battery.h b/include/linux/power/max17042_battery.h index c417abd2ab70..05097f08ea36 100644 --- a/include/linux/power/max17042_battery.h +++ b/include/linux/power/max17042_battery.h @@ -105,7 +105,7 @@ enum max17042_register { MAX17042_OCV = 0xEE, - MAX17042_OCVInternal = 0xFB, /* MAX17055 VFOCV */ + MAX17042_OCVInternal = 0xFB, /* MAX17055/77759 VFOCV */ MAX17042_VFSOC = 0xFF, }; @@ -156,7 +156,7 @@ enum max17055_register { MAX17055_AtAvCap = 0xDF, }; -/* Registers specific to max17047/50/55 */ +/* Registers specific to max17047/50/55/77759 */ enum max17047_register { MAX17047_QRTbl00 = 0x12, MAX17047_FullSOCThr = 0x13, @@ -167,12 +167,32 @@ enum max17047_register { MAX17047_QRTbl30 = 0x42, }; +enum max77759_register { + MAX77759_AvgTA0 = 0x26, + MAX77759_AtTTF = 0x33, + MAX77759_Tconvert = 0x34, + MAX77759_AvgCurrent0 = 0x3B, + MAX77759_THMHOT = 0x40, + MAX77759_CTESample = 0x41, + MAX77759_ISys = 0x43, + MAX77759_AvgVCell0 = 0x44, + MAX77759_RlxSOC = 0x47, + MAX77759_AvgISys = 0x4B, + MAX77759_QH0 = 0x4C, + MAX77759_MixAtFull = 0x4F, + MAX77759_VSys = 0xB1, + MAX77759_TAlrtTh2 = 0xB2, + MAX77759_VByp = 0xB3, + MAX77759_IIn = 0xD0, +}; + enum max170xx_chip_type { MAXIM_DEVICE_TYPE_UNKNOWN = 0, MAXIM_DEVICE_TYPE_MAX17042, MAXIM_DEVICE_TYPE_MAX17047, MAXIM_DEVICE_TYPE_MAX17050, MAXIM_DEVICE_TYPE_MAX17055, + MAXIM_DEVICE_TYPE_MAX77759, MAXIM_DEVICE_TYPE_NUM }; From 83a86e27c34d06ec2dc117fb293e80f78402df49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:09 +0000 Subject: [PATCH 10/36] power: supply: max17042: consider task period (max77759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several (register) values reported by the fuel gauge depend on its internal task period and it needs to be taken into account when calculating results. All relevant example formulas in the data sheet assume the default task period (of 5760) and final results need to be adjusted based on the task period in effect. Update the code as and where necessary. Reviewed-by: Peter Griffin Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-10-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- drivers/power/supply/max17042_battery.c | 20 ++++++++++++++++++++ include/linux/power/max17042_battery.h | 1 + 2 files changed, 21 insertions(+) diff --git a/drivers/power/supply/max17042_battery.c b/drivers/power/supply/max17042_battery.c index b9a21cef2cc6..bafbf8706055 100644 --- a/drivers/power/supply/max17042_battery.c +++ b/drivers/power/supply/max17042_battery.c @@ -61,6 +61,7 @@ struct max17042_chip { struct work_struct work; int init_complete; int irq; + int task_period; }; static enum power_supply_property max17042_battery_props[] = { @@ -331,6 +332,8 @@ static int max17042_get_property(struct power_supply *psy, return ret; data64 = data * 5000000ll; + data64 *= chip->task_period; + do_div(data64, MAX17042_DEFAULT_TASK_PERIOD); do_div(data64, chip->pdata->r_sns); val->intval = data64; break; @@ -340,6 +343,8 @@ static int max17042_get_property(struct power_supply *psy, return ret; data64 = data * 5000000ll; + data64 *= chip->task_period; + do_div(data64, MAX17042_DEFAULT_TASK_PERIOD); do_div(data64, chip->pdata->r_sns); val->intval = data64; break; @@ -349,6 +354,8 @@ static int max17042_get_property(struct power_supply *psy, return ret; data64 = data * 5000000ll; + data64 *= chip->task_period; + do_div(data64, MAX17042_DEFAULT_TASK_PERIOD); do_div(data64, chip->pdata->r_sns); val->intval = data64; break; @@ -358,6 +365,8 @@ static int max17042_get_property(struct power_supply *psy, return ret; data64 = sign_extend64(data, 15) * 5000000ll; + data64 *= chip->task_period; + data64 = div_s64(data64, MAX17042_DEFAULT_TASK_PERIOD); val->intval = div_s64(data64, chip->pdata->r_sns); break; case POWER_SUPPLY_PROP_TEMP: @@ -1142,6 +1151,17 @@ static int max17042_probe(struct i2c_client *client, struct device *dev, int irq regmap_write(chip->regmap, MAX17042_LearnCFG, 0x0007); } + chip->task_period = MAX17042_DEFAULT_TASK_PERIOD; + if (chip->chip_type == MAXIM_DEVICE_TYPE_MAX77759) { + ret = regmap_read(chip->regmap, MAX17042_TaskPeriod, &val); + if (ret) + return dev_err_probe(dev, ret, + "failed to read task period\n"); + chip->task_period = val; + } + dev_dbg(dev, "task period: %#.4x (%d)\n", chip->task_period, + chip->task_period); + chip->battery = devm_power_supply_register(dev, max17042_desc, &psy_cfg); if (IS_ERR(chip->battery)) diff --git a/include/linux/power/max17042_battery.h b/include/linux/power/max17042_battery.h index 05097f08ea36..d5b08313cf11 100644 --- a/include/linux/power/max17042_battery.h +++ b/include/linux/power/max17042_battery.h @@ -17,6 +17,7 @@ #define MAX17042_DEFAULT_VMAX (4500) /* LiHV cell max */ #define MAX17042_DEFAULT_TEMP_MIN (0) /* For sys without temp sensor */ #define MAX17042_DEFAULT_TEMP_MAX (700) /* 70 degrees Celcius */ +#define MAX17042_DEFAULT_TASK_PERIOD (5760) /* Consider RepCap which is less then 10 units below FullCAP full */ #define MAX17042_FULL_THRESHOLD 10 From c10b68e331c51aed8a615af701946dd85b2aca1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Draszik?= Date: Mon, 2 Mar 2026 13:32:10 +0000 Subject: [PATCH 11/36] power: supply: max17042: report time to full (max17055 & max77759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Report the remaining time to full as calculated by the firmware for devices that implement this. Similar to time to empty, the reported value is only meaningful when charging, i.e. if it is != U16_MAX. Reviewed-by: Peter Griffin Signed-off-by: André Draszik Link: https://patch.msgid.link/20260302-max77759-fg-v3-11-3c5f01dbda23@linaro.org Signed-off-by: Sebastian Reichel --- drivers/power/supply/max17042_battery.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/drivers/power/supply/max17042_battery.c b/drivers/power/supply/max17042_battery.c index bafbf8706055..167fb3fb3732 100644 --- a/drivers/power/supply/max17042_battery.c +++ b/drivers/power/supply/max17042_battery.c @@ -89,6 +89,7 @@ static enum power_supply_property max17042_battery_props[] = { POWER_SUPPLY_PROP_HEALTH, POWER_SUPPLY_PROP_SCOPE, POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW, + POWER_SUPPLY_PROP_TIME_TO_FULL_NOW, // these two have to be at the end on the list POWER_SUPPLY_PROP_CURRENT_NOW, POWER_SUPPLY_PROP_CURRENT_AVG, @@ -443,6 +444,21 @@ static int max17042_get_property(struct power_supply *psy, if (data == U16_MAX) return -ENODATA; + val->intval = data * 5625 / 1000; + break; + case POWER_SUPPLY_PROP_TIME_TO_FULL_NOW: + if (chip->chip_type != MAXIM_DEVICE_TYPE_MAX17055 && + chip->chip_type != MAXIM_DEVICE_TYPE_MAX77759) + return -EINVAL; + + ret = regmap_read(map, MAX17055_TTF, &data); + if (ret < 0) + return ret; + + /* when discharging, the value is not meaningful */ + if (data == U16_MAX) + return -ENODATA; + val->intval = data * 5625 / 1000; break; default: From d3da03025e6de538ca5af346c43526a2d5494582 Mon Sep 17 00:00:00 2001 From: Shivendra Pratap Date: Tue, 24 Feb 2026 12:12:26 +0530 Subject: [PATCH 12/36] Documentation: ABI: Add sysfs-class-reboot-mode-reboot_modes Add ABI documentation for /sys/class/reboot-mode/*/reboot_modes, a read-only sysfs attribute exposing the list of supported reboot-mode arguments. This file is created by reboot-mode framework and provides a user-readable interface to query available reboot-mode arguments. Reviewed-by: Bartosz Golaszewski Reviewed-by: Sebastian Reichel Signed-off-by: Shivendra Pratap Link: https://patch.msgid.link/20260224-next-15nov_expose_sysfs-v24-1-4ee5b49d5a06@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- .../sysfs-class-reboot-mode-reboot_modes | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Documentation/ABI/testing/sysfs-class-reboot-mode-reboot_modes diff --git a/Documentation/ABI/testing/sysfs-class-reboot-mode-reboot_modes b/Documentation/ABI/testing/sysfs-class-reboot-mode-reboot_modes new file mode 100644 index 000000000000..a16c54ab841b --- /dev/null +++ b/Documentation/ABI/testing/sysfs-class-reboot-mode-reboot_modes @@ -0,0 +1,36 @@ +What: /sys/class/reboot-mode//reboot_modes +Date: March 2026(TBD) +KernelVersion: TBD +Contact: linux-pm@vger.kernel.org + Description: + This interface exposes the reboot-mode arguments + registered with the reboot-mode framework. It is + a read-only interface and provides a space + separated list of reboot-mode arguments supported + on the current platform. + Example: + recovery fastboot bootloader + + The exact sysfs path may vary depending on the + name of the driver that registers the arguments. + Example: + /sys/class/reboot-mode/nvmem-reboot-mode/reboot_modes + /sys/class/reboot-mode/syscon-reboot-mode/reboot_modes + /sys/class/reboot-mode/qcom-pon/reboot_modes + + The supported arguments can be used by userspace to + invoke device reset using the standard reboot() system + call interface, with the "argument" as string to "*arg" + parameter along with LINUX_REBOOT_CMD_RESTART2. + + A driver can expose the supported arguments by + registering them with the reboot-mode framework + using the property names that follow the + mode- format. + Example: + mode-bootloader, mode-recovery. + + This attribute is useful for scripts or initramfs + logic that need to programmatically determine + which reboot-mode arguments are valid before + triggering a reboot. From cfaf0a90789ac74391ac7583c86cdaaada78cdbb Mon Sep 17 00:00:00 2001 From: Shivendra Pratap Date: Tue, 24 Feb 2026 12:12:27 +0530 Subject: [PATCH 13/36] power: reset: reboot-mode: Expose sysfs for registered reboot_modes Currently, there is no standardized mechanism for userspace to discover supported reboot modes on a platform. This limits userspace scripts, to rely on hardcoded assumptions about the available reboot-modes. Create a class 'reboot-mode' and a device under it. Use the name of the registering driver as device name. Expose a sysfs interface under this device to show available reboot mode arguments. This results in the creation of: /sys/class/reboot-mode//reboot_modes This read-only sysfs file will exposes the supported reboot mode arguments provided by the registering driver, enabling userspace to query the list of arguments. Reviewed-by: Bartosz Golaszewski Signed-off-by: Shivendra Pratap Link: https://patch.msgid.link/20260224-next-15nov_expose_sysfs-v24-2-4ee5b49d5a06@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/reset/reboot-mode.c | 150 +++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 3 deletions(-) diff --git a/drivers/power/reset/reboot-mode.c b/drivers/power/reset/reboot-mode.c index fba53f638da0..ad239e96774b 100644 --- a/drivers/power/reset/reboot-mode.c +++ b/drivers/power/reset/reboot-mode.c @@ -4,12 +4,16 @@ */ #include +#include #include #include +#include #include #include #include #include +#include +#include #define PREFIX "mode-" @@ -19,6 +23,54 @@ struct mode_info { struct list_head list; }; +struct reboot_mode_sysfs_data { + struct device *reboot_mode_device; + struct list_head head; +}; + +static inline void reboot_mode_release_list(struct reboot_mode_sysfs_data *priv) +{ + struct mode_info *info; + struct mode_info *next; + + list_for_each_entry_safe(info, next, &priv->head, list) { + list_del(&info->list); + kfree_const(info->mode); + kfree(info); + } +} + +static ssize_t reboot_modes_show(struct device *dev, struct device_attribute *attr, char *buf) +{ + struct reboot_mode_sysfs_data *priv; + struct mode_info *sysfs_info; + ssize_t size = 0; + + priv = dev_get_drvdata(dev); + if (!priv) + return -ENODATA; + + list_for_each_entry(sysfs_info, &priv->head, list) + size += sysfs_emit_at(buf, size, "%s ", sysfs_info->mode); + + if (!size) + return -ENODATA; + + return size + sysfs_emit_at(buf, size - 1, "\n"); +} +static DEVICE_ATTR_RO(reboot_modes); + +static struct attribute *reboot_mode_attrs[] = { + &dev_attr_reboot_modes.attr, + NULL, +}; +ATTRIBUTE_GROUPS(reboot_mode); + +static const struct class reboot_mode_class = { + .name = "reboot-mode", + .dev_groups = reboot_mode_groups, +}; + static unsigned int get_reboot_mode_magic(struct reboot_mode_driver *reboot, const char *cmd) { @@ -62,6 +114,51 @@ static int reboot_mode_notify(struct notifier_block *this, return NOTIFY_DONE; } +static int reboot_mode_create_device(struct reboot_mode_driver *reboot) +{ + struct reboot_mode_sysfs_data *priv; + struct mode_info *sysfs_info; + struct mode_info *info; + int ret; + + priv = kzalloc_obj(*priv, GFP_KERNEL); + if (!priv) + return -ENOMEM; + + INIT_LIST_HEAD(&priv->head); + + list_for_each_entry(info, &reboot->head, list) { + sysfs_info = kzalloc_obj(*sysfs_info, GFP_KERNEL); + if (!sysfs_info) { + ret = -ENOMEM; + goto error; + } + + sysfs_info->mode = kstrdup_const(info->mode, GFP_KERNEL); + if (!sysfs_info->mode) { + kfree(sysfs_info); + ret = -ENOMEM; + goto error; + } + + list_add_tail(&sysfs_info->list, &priv->head); + } + + priv->reboot_mode_device = device_create(&reboot_mode_class, NULL, 0, + (void *)priv, reboot->dev->driver->name); + if (IS_ERR(priv->reboot_mode_device)) { + ret = PTR_ERR(priv->reboot_mode_device); + goto error; + } + + return 0; + +error: + reboot_mode_release_list(priv); + kfree(priv); + return ret; +} + /** * reboot_mode_register - register a reboot mode driver * @reboot: reboot mode driver @@ -113,16 +210,49 @@ int reboot_mode_register(struct reboot_mode_driver *reboot) reboot->reboot_notifier.notifier_call = reboot_mode_notify; register_reboot_notifier(&reboot->reboot_notifier); + ret = reboot_mode_create_device(reboot); + if (ret) + goto error; + return 0; error: - list_for_each_entry(info, &reboot->head, list) - kfree_const(info->mode); - + reboot_mode_unregister(reboot); return ret; } EXPORT_SYMBOL_GPL(reboot_mode_register); +static int reboot_mode_match_by_name(struct device *dev, const void *data) +{ + const char *name = data; + + if (!dev || !data) + return 0; + + return dev_name(dev) && strcmp(dev_name(dev), name) == 0; +} + +static inline void reboot_mode_unregister_device(struct reboot_mode_driver *reboot) +{ + struct reboot_mode_sysfs_data *priv; + struct device *reboot_mode_device; + + reboot_mode_device = class_find_device(&reboot_mode_class, NULL, reboot->dev->driver->name, + reboot_mode_match_by_name); + + if (!reboot_mode_device) + return; + + priv = dev_get_drvdata(reboot_mode_device); + device_unregister(reboot_mode_device); + + if (!priv) + return; + + reboot_mode_release_list(priv); + kfree(priv); +} + /** * reboot_mode_unregister - unregister a reboot mode driver * @reboot: reboot mode driver @@ -132,6 +262,7 @@ int reboot_mode_unregister(struct reboot_mode_driver *reboot) struct mode_info *info; unregister_reboot_notifier(&reboot->reboot_notifier); + reboot_mode_unregister_device(reboot); list_for_each_entry(info, &reboot->head, list) kfree_const(info->mode); @@ -199,6 +330,19 @@ void devm_reboot_mode_unregister(struct device *dev, } EXPORT_SYMBOL_GPL(devm_reboot_mode_unregister); +static int __init reboot_mode_init(void) +{ + return class_register(&reboot_mode_class); +} + +static void __exit reboot_mode_exit(void) +{ + class_unregister(&reboot_mode_class); +} + +subsys_initcall(reboot_mode_init); +module_exit(reboot_mode_exit); + MODULE_AUTHOR("Andy Yan "); MODULE_DESCRIPTION("System reboot mode core library"); MODULE_LICENSE("GPL v2"); From 68e6343fbf54ef7dd6f3f94e93afa42a9fe0eaf7 Mon Sep 17 00:00:00 2001 From: Jaime Saguillo Revilla Date: Thu, 19 Feb 2026 21:23:53 +0000 Subject: [PATCH 14/36] power: supply: cpcap-battery: fix typo in config name Rename cpcap_battery_unkown_data to cpcap_battery_unknown_data to correct a spelling mistake in the identifier. No functional change. Signed-off-by: Jaime Saguillo Revilla Link: https://patch.msgid.link/20260219212353.49416-1-jaime.saguillo@gmail.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/cpcap-battery.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/power/supply/cpcap-battery.c b/drivers/power/supply/cpcap-battery.c index 507fdc1c866d..f58269c75509 100644 --- a/drivers/power/supply/cpcap-battery.c +++ b/drivers/power/supply/cpcap-battery.c @@ -387,7 +387,7 @@ static const struct cpcap_battery_config cpcap_battery_bw8x_data = { * Safe values for any lipo battery likely to fit into a mapphone * battery bay. */ -static const struct cpcap_battery_config cpcap_battery_unkown_data = { +static const struct cpcap_battery_config cpcap_battery_unknown_data = { .cd_factor = 0x3cc, .info.technology = POWER_SUPPLY_TECHNOLOGY_LION, .info.voltage_max_design = 4200000, @@ -429,7 +429,7 @@ static void cpcap_battery_detect_battery_type(struct cpcap_battery_ddata *ddata) ddata->config = cpcap_battery_bw8x_data; break; default: - ddata->config = cpcap_battery_unkown_data; + ddata->config = cpcap_battery_unknown_data; } } From 5c2ffc0b215a884dbc961d4737f636067348b8bd Mon Sep 17 00:00:00 2001 From: Dmitry Torokhov Date: Wed, 18 Feb 2026 12:59:49 -0800 Subject: [PATCH 15/36] power: supply: sbs-manager: normalize return value of gpio_get The GPIO get callback is expected to return 0 or 1 (or a negative error code). Ensure that the value returned by sbsm_gpio_get_value() is normalized to the [0, 1] range. Signed-off-by: Dmitry Torokhov Reviewed-by: Linus Walleij Reviewed-by: Bartosz Golaszewski Link: https://patch.msgid.link/aZYoL2MnTYU5FuQh@google.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/sbs-manager.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/power/supply/sbs-manager.c b/drivers/power/supply/sbs-manager.c index 6fe526222f7f..343ad4ab4082 100644 --- a/drivers/power/supply/sbs-manager.c +++ b/drivers/power/supply/sbs-manager.c @@ -199,7 +199,7 @@ static int sbsm_gpio_get_value(struct gpio_chip *gc, unsigned int off) if (ret < 0) return ret; - return ret & BIT(off); + return !!(ret & BIT(off)); } /* From 0ebf821cf6c75de2d95d3db277617ec685498e7c Mon Sep 17 00:00:00 2001 From: Hector Martin Date: Tue, 17 Feb 2026 21:47:25 +1100 Subject: [PATCH 16/36] power: supply: Add macsmc-power driver for Apple Silicon This driver provides battery and AC status monitoring for Apple Silicon Macs via the SMC (System Management Controller). It supports reporting capacity, voltage, current, and charging status, and modifying the charging behaviour across multiple generations of SMC firmware. Signed-off-by: Hector Martin Co-developed-by: Joey Gouly Signed-off-by: Joey Gouly Co-developed-by: Janne Grunau Signed-off-by: Janne Grunau Reviewed-by: Neal Gompa Reviewed-by: Sven Peter Co-developed-by: Michael Reeves Signed-off-by: Michael Reeves Link: https://patch.msgid.link/20260217-b4-macsmc-power-v7-1-4a4d63664362@gmail.com Signed-off-by: Sebastian Reichel --- MAINTAINERS | 1 + drivers/power/supply/Kconfig | 11 + drivers/power/supply/Makefile | 1 + drivers/power/supply/macsmc-power.c | 855 ++++++++++++++++++++++++++++ 4 files changed, 868 insertions(+) create mode 100644 drivers/power/supply/macsmc-power.c diff --git a/MAINTAINERS b/MAINTAINERS index 55af015174a5..9fb064945720 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2552,6 +2552,7 @@ F: drivers/nvmem/apple-spmi-nvmem.c F: drivers/phy/apple/ F: drivers/pinctrl/pinctrl-apple-gpio.c F: drivers/power/reset/macsmc-reboot.c +F: drivers/power/supply/macsmc-power.c F: drivers/pwm/pwm-apple.c F: drivers/rtc/rtc-macsmc.c F: drivers/soc/apple/* diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig index 92f9f7aae92f..3a5b7d9234c2 100644 --- a/drivers/power/supply/Kconfig +++ b/drivers/power/supply/Kconfig @@ -1132,4 +1132,15 @@ config FUEL_GAUGE_MM8013 the state of charge, temperature, cycle count, actual and design capacity, etc. +config MACSMC_POWER + tristate "Apple SMC Battery and Power Driver" + depends on MFD_MACSMC + help + This driver provides support for the battery and AC adapter on + Apple Silicon machines. It exposes battery telemetry (voltage, + current, health) and AC adapter status through the standard Linux + power supply framework. + + Say Y or M here if you have an Apple Silicon based Mac. + endif # POWER_SUPPLY diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile index 4b79d5abc49a..d14420b606d8 100644 --- a/drivers/power/supply/Makefile +++ b/drivers/power/supply/Makefile @@ -128,3 +128,4 @@ obj-$(CONFIG_CHARGER_SURFACE) += surface_charger.o obj-$(CONFIG_BATTERY_UG3105) += ug3105_battery.o obj-$(CONFIG_CHARGER_QCOM_SMB2) += qcom_smbx.o obj-$(CONFIG_FUEL_GAUGE_MM8013) += mm8013.o +obj-$(CONFIG_MACSMC_POWER) += macsmc-power.o diff --git a/drivers/power/supply/macsmc-power.c b/drivers/power/supply/macsmc-power.c new file mode 100644 index 000000000000..33ca07460f3a --- /dev/null +++ b/drivers/power/supply/macsmc-power.c @@ -0,0 +1,855 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Apple SMC Power/Battery Management Driver + * + * This driver exposes battery telemetry (voltage, current, temperature, health) + * and AC adapter status provided by the Apple SMC (System Management Controller) + * on Apple Silicon systems. + * + * Copyright The Asahi Linux Contributors + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_STRING_LENGTH 256 + +/* + * The SMC reports charge in mAh (Coulombs) but energy in mWh (Joules). + * We lack a register for "Nominal Voltage" or "Energy Accumulator". + * We use a fixed 3.8V/cell constant to approximate energy stats for userspace, + * derived from empirical data across supported MacBook models. + */ +#define MACSMC_NOMINAL_CELL_VOLTAGE_MV 3800 + +/* SMC Key Flags */ +#define CHNC_BATTERY_FULL BIT(0) +#define CHNC_NO_CHARGER BIT(7) +#define CHNC_NOCHG_CH0C BIT(14) +#define CHNC_NOCHG_CH0B_CH0K BIT(15) +#define CHNC_BATTERY_FULL_2 BIT(18) +#define CHNC_BMS_BUSY BIT(23) +#define CHNC_CHLS_LIMIT BIT(24) +#define CHNC_NOAC_CH0J BIT(53) +#define CHNC_NOAC_CH0I BIT(54) + +#define CH0R_LOWER_FLAGS GENMASK(15, 0) +#define CH0R_NOAC_CH0I BIT(0) +#define CH0R_NOAC_DISCONNECTED BIT(4) +#define CH0R_NOAC_CH0J BIT(5) +#define CH0R_BMS_BUSY BIT(8) +#define CH0R_NOAC_CH0K BIT(9) +#define CH0R_NOAC_CHWA BIT(11) + +#define CH0X_CH0C BIT(0) +#define CH0X_CH0B BIT(1) + +#define ACSt_CAN_BOOT_AP BIT(2) +#define ACSt_CAN_BOOT_IBOOT BIT(1) + +#define CHWA_CHLS_FIXED_START_OFFSET 5 +#define CHLS_MIN_END_THRESHOLD 10 +#define CHLS_FORCE_DISCHARGE 0x100 +#define CHWA_FIXED_END_THRESHOLD 80 +#define CHWA_PROP_WRITE_THRESHOLD 95 + +#define MACSMC_MAX_BATT_PROPS 50 +#define MACSMC_MAX_AC_PROPS 10 + +struct macsmc_power { + struct device *dev; + struct apple_smc *smc; + + struct power_supply_desc ac_desc; + struct power_supply_desc batt_desc; + + struct power_supply *batt; + struct power_supply *ac; + + char model_name[MAX_STRING_LENGTH]; + char serial_number[MAX_STRING_LENGTH]; + char mfg_date[MAX_STRING_LENGTH]; + + /* Supported feature flags based on SMC key presence */ + bool has_chwa; /* Charge limit (Modern firmware) */ + bool has_chls; /* Charge limit (Older firmware) */ + bool has_ch0i; /* Force discharge (Older firmware) */ + bool has_ch0c; /* Inhibit charge (Older firmware) */ + bool has_chte; /* Inhibit charge (Modern firmware) */ + + u8 num_cells; + int nominal_voltage_mv; + + struct notifier_block nb; + struct work_struct critical_work; + bool emergency_shutdown_triggered; + bool orderly_shutdown_triggered; +}; + +static int macsmc_battery_get_status(struct macsmc_power *power) +{ + u64 nocharge_flags; + u32 nopower_flags; + u16 ac_current; + int charge_limit = 0; + bool limited = false; + bool flag; + int ret; + + /* + * B0AV (Voltage) is fundamental. If we can't read it, we assume the + * battery is gone. CHCE (Hardware charger present) / CHCC (Hardware + * charger capable) are fundamental status flags. + * BSFC (System full charge) / CHSC (System charging) are fundamental + * status flags. + */ + + /* Check if power input is inhibited (e.g. BMS balancing cycle) */ + ret = apple_smc_read_u32(power->smc, SMC_KEY(CH0R), &nopower_flags); + if (!ret && (nopower_flags & CH0R_LOWER_FLAGS & ~CH0R_BMS_BUSY)) + return POWER_SUPPLY_STATUS_DISCHARGING; + + /* Check if charger is present */ + ret = apple_smc_read_flag(power->smc, SMC_KEY(CHCE), &flag); + if (ret < 0) + return ret; + if (!flag) + return POWER_SUPPLY_STATUS_DISCHARGING; + + /* Check if AC is charge capable */ + ret = apple_smc_read_flag(power->smc, SMC_KEY(CHCC), &flag); + if (ret < 0) + return ret; + if (!flag) + return POWER_SUPPLY_STATUS_DISCHARGING; + + /* Check if AC input limit is too low */ + ret = apple_smc_read_u16(power->smc, SMC_KEY(AC-i), &ac_current); + if (!ret && ac_current < 100) + return POWER_SUPPLY_STATUS_DISCHARGING; + + /* Check if battery is full */ + ret = apple_smc_read_flag(power->smc, SMC_KEY(BSFC), &flag); + if (ret < 0) + return ret; + if (flag) + return POWER_SUPPLY_STATUS_FULL; + + /* Check for user-defined charge limits */ + if (power->has_chls) { + u16 vu16; + + ret = apple_smc_read_u16(power->smc, SMC_KEY(CHLS), &vu16); + if (ret == 0 && (vu16 & 0xff) >= CHLS_MIN_END_THRESHOLD) + charge_limit = (vu16 & 0xff) - CHWA_CHLS_FIXED_START_OFFSET; + } else if (power->has_chwa) { + ret = apple_smc_read_flag(power->smc, SMC_KEY(CHWA), &flag); + if (ret == 0 && flag) + charge_limit = CHWA_FIXED_END_THRESHOLD - CHWA_CHLS_FIXED_START_OFFSET; + } + + if (charge_limit > 0) { + u8 buic = 0; + + if (apple_smc_read_u8(power->smc, SMC_KEY(BUIC), &buic) >= 0 && + buic >= charge_limit) + limited = true; + } + + /* Check charging inhibitors */ + ret = apple_smc_read_u64(power->smc, SMC_KEY(CHNC), &nocharge_flags); + if (!ret) { + if (nocharge_flags & CHNC_BATTERY_FULL) + return POWER_SUPPLY_STATUS_FULL; + /* BMS busy shows up as inhibit, but we treat it as charging */ + else if (nocharge_flags == CHNC_BMS_BUSY && !limited) + return POWER_SUPPLY_STATUS_CHARGING; + else if (nocharge_flags) + return POWER_SUPPLY_STATUS_NOT_CHARGING; + else + return POWER_SUPPLY_STATUS_CHARGING; + } + + /* Fallback: System charging flag */ + ret = apple_smc_read_flag(power->smc, SMC_KEY(CHSC), &flag); + if (ret < 0) + return ret; + if (!flag) + return POWER_SUPPLY_STATUS_NOT_CHARGING; + + return POWER_SUPPLY_STATUS_CHARGING; +} + +static int macsmc_battery_get_charge_behaviour(struct macsmc_power *power) +{ + int ret; + u8 val8; + u8 chte_buf[4]; + + if (power->has_ch0i) { + ret = apple_smc_read_u8(power->smc, SMC_KEY(CH0I), &val8); + if (ret) + return ret; + if (val8 & CH0R_NOAC_CH0I) + return POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE; + } + + if (power->has_chte) { + ret = apple_smc_read(power->smc, SMC_KEY(CHTE), chte_buf, 4); + if (ret < 0) + return ret; + + if (chte_buf[0] == 0x01) + return POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; + } else if (power->has_ch0c) { + ret = apple_smc_read_u8(power->smc, SMC_KEY(CH0C), &val8); + if (ret) + return ret; + if (val8 & CH0X_CH0C) + return POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE; + } + + return POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; +} + +static int macsmc_battery_set_charge_behaviour(struct macsmc_power *power, int val) +{ + int ret; + + switch (val) { + case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: + /* Reset all inhibitors to a known-good 'auto' state */ + if (power->has_ch0i) { + ret = apple_smc_write_u8(power->smc, SMC_KEY(CH0I), 0); + if (ret) + return ret; + } + + if (power->has_chte) { + ret = apple_smc_write_u32(power->smc, SMC_KEY(CHTE), 0); + if (ret) + return ret; + } else if (power->has_ch0c) { + ret = apple_smc_write_u8(power->smc, SMC_KEY(CH0C), 0); + if (ret) + return ret; + } + return 0; + + case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE: + if (power->has_chte) + return apple_smc_write_u32(power->smc, SMC_KEY(CHTE), 1); + else if (power->has_ch0c) + return apple_smc_write_u8(power->smc, SMC_KEY(CH0C), 1); + else + return -EOPNOTSUPP; + + case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE: + if (!power->has_ch0i) + return -EOPNOTSUPP; + return apple_smc_write_u8(power->smc, SMC_KEY(CH0I), 1); + + default: + return -EINVAL; + } +} + +static int macsmc_battery_get_date(const char *s, int *out) +{ + if (!isdigit(s[0]) || !isdigit(s[1])) + return -EOPNOTSUPP; + + *out = (s[0] - '0') * 10 + s[1] - '0'; + return 0; +} + +static int macsmc_battery_get_capacity_level(struct macsmc_power *power) +{ + bool flag; + u32 val; + int ret; + + /* Check for emergency shutdown condition */ + if (apple_smc_read_u32(power->smc, SMC_KEY(BCF0), &val) >= 0 && val) + return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + + /* Check AC status for whether we could boot in this state */ + if (apple_smc_read_u32(power->smc, SMC_KEY(ACSt), &val) >= 0) { + if (!(val & ACSt_CAN_BOOT_IBOOT)) + return POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + + if (!(val & ACSt_CAN_BOOT_AP)) + return POWER_SUPPLY_CAPACITY_LEVEL_LOW; + } + + /* BSFC = Battery System Full Charge */ + ret = apple_smc_read_flag(power->smc, SMC_KEY(BSFC), &flag); + if (ret < 0) + return POWER_SUPPLY_CAPACITY_LEVEL_UNKNOWN; + + if (flag) + return POWER_SUPPLY_CAPACITY_LEVEL_FULL; + else + return POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; +} + +static int macsmc_battery_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct macsmc_power *power = power_supply_get_drvdata(psy); + int ret = 0; + u8 vu8; + u16 vu16; + s16 vs16; + s32 vs32; + s64 vs64; + bool flag; + + switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + val->intval = macsmc_battery_get_status(power); + ret = val->intval < 0 ? val->intval : 0; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + break; + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + val->intval = macsmc_battery_get_charge_behaviour(power); + ret = val->intval < 0 ? val->intval : 0; + break; + case POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0TE), &vu16); + val->intval = vu16 == 0xffff ? 0 : vu16 * 60; + break; + case POWER_SUPPLY_PROP_TIME_TO_FULL_NOW: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0TF), &vu16); + val->intval = vu16 == 0xffff ? 0 : vu16 * 60; + break; + case POWER_SUPPLY_PROP_CAPACITY: + ret = apple_smc_read_u8(power->smc, SMC_KEY(BUIC), &vu8); + val->intval = vu8; + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + val->intval = macsmc_battery_get_capacity_level(power); + ret = val->intval < 0 ? val->intval : 0; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0AV), &vu16); + val->intval = vu16 * 1000; + break; + case POWER_SUPPLY_PROP_CURRENT_NOW: + ret = apple_smc_read_s16(power->smc, SMC_KEY(B0AC), &vs16); + val->intval = vs16 * 1000; + break; + case POWER_SUPPLY_PROP_POWER_NOW: + ret = apple_smc_read_s32(power->smc, SMC_KEY(B0AP), &vs32); + val->intval = vs32 * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN: + ret = apple_smc_read_u16(power->smc, SMC_KEY(BITV), &vu16); + val->intval = vu16 * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN: + /* Calculate total max design voltage from per-cell maximum voltage */ + ret = apple_smc_read_u16(power->smc, SMC_KEY(BVVN), &vu16); + val->intval = vu16 * 1000 * power->num_cells; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MIN: + /* Lifetime min */ + ret = apple_smc_read_s16(power->smc, SMC_KEY(BLPM), &vs16); + val->intval = vs16 * 1000; + break; + case POWER_SUPPLY_PROP_VOLTAGE_MAX: + /* Lifetime max */ + ret = apple_smc_read_s16(power->smc, SMC_KEY(BLPX), &vs16); + val->intval = vs16 * 1000; + break; + case POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RC), &vu16); + val->intval = vu16 * 1000; + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RI), &vu16); + val->intval = vu16 * 1000; + break; + case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RV), &vu16); + val->intval = vu16 * 1000; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0DC), &vu16); + val->intval = vu16 * 1000; + break; + case POWER_SUPPLY_PROP_CHARGE_FULL: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0FC), &vu16); + val->intval = vu16 * 1000; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RM), &vu16); + /* B0RM is Big Endian, likely pass through from TI gas gauge */ + val->intval = (s16)swab16(vu16) * 1000; + break; + case POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0DC), &vu16); + val->intval = vu16 * power->nominal_voltage_mv; + break; + case POWER_SUPPLY_PROP_ENERGY_FULL: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0FC), &vu16); + val->intval = vu16 * power->nominal_voltage_mv; + break; + case POWER_SUPPLY_PROP_ENERGY_NOW: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0RM), &vu16); + /* B0RM is Big Endian, likely pass through from TI gas gauge */ + val->intval = (s16)swab16(vu16) * power->nominal_voltage_mv; + break; + case POWER_SUPPLY_PROP_TEMP: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0AT), &vu16); + val->intval = vu16 - 2732; /* Kelvin x10 to Celsius x10 */ + break; + case POWER_SUPPLY_PROP_CHARGE_COUNTER: + ret = apple_smc_read_s64(power->smc, SMC_KEY(BAAC), &vs64); + val->intval = vs64; + break; + case POWER_SUPPLY_PROP_CYCLE_COUNT: + ret = apple_smc_read_u16(power->smc, SMC_KEY(B0CT), &vu16); + val->intval = vu16; + break; + case POWER_SUPPLY_PROP_SCOPE: + val->intval = POWER_SUPPLY_SCOPE_SYSTEM; + break; + case POWER_SUPPLY_PROP_HEALTH: + flag = false; + ret = apple_smc_read_flag(power->smc, SMC_KEY(BBAD), &flag); + val->intval = flag ? POWER_SUPPLY_HEALTH_DEAD : POWER_SUPPLY_HEALTH_GOOD; + break; + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = power->model_name; + break; + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + val->strval = power->serial_number; + break; + case POWER_SUPPLY_PROP_MANUFACTURE_YEAR: + ret = macsmc_battery_get_date(&power->mfg_date[0], &val->intval); + /* The SMC reports the manufacture year as an offset from 1992. */ + val->intval += 1992; + break; + case POWER_SUPPLY_PROP_MANUFACTURE_MONTH: + ret = macsmc_battery_get_date(&power->mfg_date[2], &val->intval); + break; + case POWER_SUPPLY_PROP_MANUFACTURE_DAY: + ret = macsmc_battery_get_date(&power->mfg_date[4], &val->intval); + break; + default: + return -EINVAL; + } + + return ret; +} + +static int macsmc_battery_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct macsmc_power *power = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + return macsmc_battery_set_charge_behaviour(power, val->intval); + default: + return -EINVAL; + } +} + +static int macsmc_battery_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: + return true; + default: + return false; + } +} + +static const struct power_supply_desc macsmc_battery_desc_template = { + .name = "macsmc-battery", + .type = POWER_SUPPLY_TYPE_BATTERY, + .get_property = macsmc_battery_get_property, + .set_property = macsmc_battery_set_property, + .property_is_writeable = macsmc_battery_property_is_writeable, +}; + +static int macsmc_ac_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct macsmc_power *power = power_supply_get_drvdata(psy); + int ret = 0; + u16 vu16; + u32 vu32; + + switch (psp) { + case POWER_SUPPLY_PROP_ONLINE: + ret = apple_smc_read_u32(power->smc, SMC_KEY(CHIS), &vu32); + val->intval = !!vu32; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + ret = apple_smc_read_u16(power->smc, SMC_KEY(AC-n), &vu16); + val->intval = vu16 * 1000; + break; + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + ret = apple_smc_read_u16(power->smc, SMC_KEY(AC-i), &vu16); + val->intval = vu16 * 1000; + break; + case POWER_SUPPLY_PROP_INPUT_POWER_LIMIT: + ret = apple_smc_read_u32(power->smc, SMC_KEY(ACPW), &vu32); + val->intval = vu32 * 1000; + break; + default: + return -EINVAL; + } + + return ret; +} + +static const struct power_supply_desc macsmc_ac_desc_template = { + .name = "macsmc-ac", + .type = POWER_SUPPLY_TYPE_MAINS, + .get_property = macsmc_ac_get_property, +}; + +static void macsmc_power_critical_work(struct work_struct *wrk) +{ + struct macsmc_power *power = container_of(wrk, struct macsmc_power, critical_work); + u16 bitv, b0av; + u32 bcf0; + + if (!power->batt) + return; + + /* + * Avoid duplicate atempts at emergency shutdown + */ + if (power->emergency_shutdown_triggered || system_state > SYSTEM_RUNNING) + return; + + /* + * EMERGENCY: Check voltage vs design minimum. + * If we are below BITV, the battery is physically exhausted. + * We must shut down NOW to protect the filesystem. + */ + if (apple_smc_read_u16(power->smc, SMC_KEY(BITV), &bitv) >= 0 && + apple_smc_read_u16(power->smc, SMC_KEY(B0AV), &b0av) >= 0 && + b0av < bitv) { + power->emergency_shutdown_triggered = true; + dev_emerg(power->dev, + "Battery voltage (%d mV) below design minimum (%d mV)! Emergency shutdown.\n", + b0av, bitv); + + /* + * Shutdown is now imminent. Kick userspace again and give it some + * brief time to (hopefully) flush what's needed, before forcing. + */ + hw_protection_trigger("Battery voltage below design minimum", 1500); + } + + /* + * Avoid duplicate attempts at orderly shutdown. + * Voltage check is above this as we may want to + * "upgrade" an orderly shutdown to a critical power + * off if voltage drops. + */ + if (power->orderly_shutdown_triggered || system_state > SYSTEM_RUNNING) + return; + + /* + * Check if SMC flagged the battery as empty. + * We trigger a graceful shutdown to let the OS save data. + */ + if (apple_smc_read_u32(power->smc, SMC_KEY(BCF0), &bcf0) == 0 && bcf0 != 0) { + power->orderly_shutdown_triggered = true; + dev_crit(power->dev, "Battery critical (empty flag set). Triggering orderly shutdown.\n"); + orderly_poweroff(true); + } +} + +static int macsmc_power_event(struct notifier_block *nb, unsigned long event, void *data) +{ + struct macsmc_power *power = container_of(nb, struct macsmc_power, nb); + + /* + * SMC Event IDs are correlated to physical events (e.g. charger + * connect/disconnect) but the exact meaning of each ID is predicted. + * 0x71... indicates power/battery events. + */ + if ((event & 0xffffff00) == 0x71010100 || /* Charger status change */ + (event & 0xffff0000) == 0x71060000 || /* Port charge state change */ + (event & 0xffff0000) == 0x71130000) { /* Connector insert/remove event */ + if (power->batt) + power_supply_changed(power->batt); + if (power->ac) + power_supply_changed(power->ac); + return NOTIFY_OK; + } else if (event == 0x71020000) { + /* Critical battery warning */ + if (power->batt) + schedule_work(&power->critical_work); + return NOTIFY_OK; + } + + return NOTIFY_DONE; +} + +static int macsmc_power_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct apple_smc *smc = dev_get_drvdata(pdev->dev.parent); + struct power_supply_config psy_cfg = {}; + struct macsmc_power *power; + bool has_battery = false; + bool has_ac_adapter = false; + int ret = -ENODEV; + bool flag; + u16 vu16; + u32 val32; + enum power_supply_property *props; + size_t nprops; + + if (!smc) + return -ENODEV; + + power = devm_kzalloc(dev, sizeof(*power), GFP_KERNEL); + if (!power) + return -ENOMEM; + + power->dev = dev; + power->smc = smc; + dev_set_drvdata(dev, power); + + INIT_WORK(&power->critical_work, macsmc_power_critical_work); + ret = devm_work_autocancel(dev, &power->critical_work, macsmc_power_critical_work); + if (ret) + return ret; + + /* + * Check for battery presence. + * B0AV is a fundamental key. + */ + if (apple_smc_read_u16(power->smc, SMC_KEY(B0AV), &vu16) == 0 && + macsmc_battery_get_status(power) > POWER_SUPPLY_STATUS_UNKNOWN) + has_battery = true; + + /* + * Check for AC adapter presence. + * CHIS is a fundamental key. + */ + if (apple_smc_key_exists(smc, SMC_KEY(CHIS))) + has_ac_adapter = true; + + if (!has_battery && !has_ac_adapter) + return -ENODEV; + + if (has_battery) { + power->batt_desc = macsmc_battery_desc_template; + props = devm_kcalloc(dev, MACSMC_MAX_BATT_PROPS, + sizeof(enum power_supply_property), + GFP_KERNEL); + if (!props) + return -ENOMEM; + + nprops = 0; + + /* Fundamental properties */ + props[nprops++] = POWER_SUPPLY_PROP_STATUS; + props[nprops++] = POWER_SUPPLY_PROP_PRESENT; + props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_NOW; + props[nprops++] = POWER_SUPPLY_PROP_CURRENT_NOW; + props[nprops++] = POWER_SUPPLY_PROP_POWER_NOW; + props[nprops++] = POWER_SUPPLY_PROP_CAPACITY; + props[nprops++] = POWER_SUPPLY_PROP_CAPACITY_LEVEL; + props[nprops++] = POWER_SUPPLY_PROP_TEMP; + props[nprops++] = POWER_SUPPLY_PROP_CYCLE_COUNT; + props[nprops++] = POWER_SUPPLY_PROP_HEALTH; + props[nprops++] = POWER_SUPPLY_PROP_SCOPE; + props[nprops++] = POWER_SUPPLY_PROP_MODEL_NAME; + props[nprops++] = POWER_SUPPLY_PROP_SERIAL_NUMBER; + props[nprops++] = POWER_SUPPLY_PROP_MANUFACTURE_YEAR; + props[nprops++] = POWER_SUPPLY_PROP_MANUFACTURE_MONTH; + props[nprops++] = POWER_SUPPLY_PROP_MANUFACTURE_DAY; + + /* Extended properties usually present */ + props[nprops++] = POWER_SUPPLY_PROP_TIME_TO_EMPTY_NOW; + props[nprops++] = POWER_SUPPLY_PROP_TIME_TO_FULL_NOW; + props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN; + props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN; + props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_MIN; + props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_MAX; + props[nprops++] = POWER_SUPPLY_PROP_CHARGE_TERM_CURRENT; + props[nprops++] = POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX; + props[nprops++] = POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE; + props[nprops++] = POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN; + props[nprops++] = POWER_SUPPLY_PROP_CHARGE_FULL; + props[nprops++] = POWER_SUPPLY_PROP_CHARGE_NOW; + props[nprops++] = POWER_SUPPLY_PROP_ENERGY_FULL_DESIGN; + props[nprops++] = POWER_SUPPLY_PROP_ENERGY_FULL; + props[nprops++] = POWER_SUPPLY_PROP_ENERGY_NOW; + props[nprops++] = POWER_SUPPLY_PROP_CHARGE_COUNTER; + + /* Detect features based on key availability */ + if (apple_smc_key_exists(smc, SMC_KEY(CHTE))) + power->has_chte = true; + if (apple_smc_key_exists(smc, SMC_KEY(CH0C))) + power->has_ch0c = true; + if (apple_smc_key_exists(smc, SMC_KEY(CH0I))) + power->has_ch0i = true; + + /* Reset "Optimised Battery Charging" flags to default state */ + if (power->has_chte) + apple_smc_write_u32(smc, SMC_KEY(CHTE), 0); + else if (power->has_ch0c) + apple_smc_write_u8(smc, SMC_KEY(CH0C), 0); + + if (power->has_ch0i) + apple_smc_write_u8(smc, SMC_KEY(CH0I), 0); + + apple_smc_write_u8(smc, SMC_KEY(CH0K), 0); + apple_smc_write_u8(smc, SMC_KEY(CH0B), 0); + + /* Configure charge behaviour if supported */ + if (power->has_ch0i || power->has_ch0c || power->has_chte) { + props[nprops++] = POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR; + + power->batt_desc.charge_behaviours = + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO); + + if (power->has_ch0i) + power->batt_desc.charge_behaviours |= + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE); + + if (power->has_chte || power->has_ch0c) + power->batt_desc.charge_behaviours |= + BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE); + } + + /* Detect charge limit method (CHWA vs CHLS) */ + if (apple_smc_read_flag(power->smc, SMC_KEY(CHWA), &flag) == 0) + power->has_chwa = true; + else if (apple_smc_read_u16(power->smc, SMC_KEY(CHLS), &vu16) >= 0) + power->has_chls = true; + + if (nprops > MACSMC_MAX_BATT_PROPS) + return -ENOMEM; + + power->batt_desc.properties = props; + power->batt_desc.num_properties = nprops; + + /* Fetch identity strings */ + apple_smc_read(smc, SMC_KEY(BMDN), power->model_name, + sizeof(power->model_name) - 1); + apple_smc_read(smc, SMC_KEY(BMSN), power->serial_number, + sizeof(power->serial_number) - 1); + apple_smc_read(smc, SMC_KEY(BMDT), power->mfg_date, + sizeof(power->mfg_date) - 1); + + apple_smc_read_u8(power->smc, SMC_KEY(BNCB), &power->num_cells); + power->nominal_voltage_mv = MACSMC_NOMINAL_CELL_VOLTAGE_MV * power->num_cells; + + /* Enable critical shutdown notifications by reading status once */ + apple_smc_read_u32(power->smc, SMC_KEY(BCF0), &val32); + + psy_cfg.drv_data = power; + power->batt = devm_power_supply_register(dev, &power->batt_desc, &psy_cfg); + if (IS_ERR(power->batt)) { + dev_err_probe(dev, PTR_ERR(power->batt), + "Failed to register battery\n"); + /* Don't return failure yet; try AC registration first */ + power->batt = NULL; + } + } + + if (has_ac_adapter) { + power->ac_desc = macsmc_ac_desc_template; + props = devm_kcalloc(dev, MACSMC_MAX_AC_PROPS, + sizeof(enum power_supply_property), + GFP_KERNEL); + if (!props) + return -ENOMEM; + + nprops = 0; + + /* Online status is fundamental */ + props[nprops++] = POWER_SUPPLY_PROP_ONLINE; + + /* Input power limits are usually available */ + if (apple_smc_key_exists(power->smc, SMC_KEY(ACPW))) + props[nprops++] = POWER_SUPPLY_PROP_INPUT_POWER_LIMIT; + + /* macOS 15.4+ firmware dropped legacy AC keys (AC-n, AC-i) */ + if (apple_smc_read_u16(power->smc, SMC_KEY(AC-n), &vu16) >= 0) { + props[nprops++] = POWER_SUPPLY_PROP_VOLTAGE_NOW; + props[nprops++] = POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT; + } + + if (nprops > MACSMC_MAX_AC_PROPS) + return -ENOMEM; + + power->ac_desc.properties = props; + power->ac_desc.num_properties = nprops; + + psy_cfg.drv_data = power; + power->ac = devm_power_supply_register(dev, &power->ac_desc, &psy_cfg); + if (IS_ERR(power->ac)) { + dev_err_probe(dev, PTR_ERR(power->ac), + "Failed to register AC adapter\n"); + power->ac = NULL; + } + } + + /* Final check: did we register anything? */ + if (!power->batt && !power->ac) + return -ENODEV; + + power->nb.notifier_call = macsmc_power_event; + blocking_notifier_chain_register(&smc->event_handlers, &power->nb); + + return 0; +} + +static void macsmc_power_remove(struct platform_device *pdev) +{ + struct macsmc_power *power = dev_get_drvdata(&pdev->dev); + + blocking_notifier_chain_unregister(&power->smc->event_handlers, &power->nb); +} + +static const struct platform_device_id macsmc_power_id[] = { + { "macsmc-power" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(platform, macsmc_power_id); + +static struct platform_driver macsmc_power_driver = { + .driver = { + .name = "macsmc-power", + }, + .id_table = macsmc_power_id, + .probe = macsmc_power_probe, + .remove = macsmc_power_remove, +}; +module_platform_driver(macsmc_power_driver); + +MODULE_LICENSE("Dual MIT/GPL"); +MODULE_DESCRIPTION("Apple SMC battery and power management driver"); +MODULE_AUTHOR("Hector Martin "); +MODULE_AUTHOR("Michael Reeves "); From 16a7c32e586ed9c7e1bbaac7c441afcf474a67bb Mon Sep 17 00:00:00 2001 From: Anjelique Melendez Date: Mon, 9 Feb 2026 12:49:15 -0800 Subject: [PATCH 17/36] power: supply: qcom_battmgr: Add support for Glymur and Kaanapali Glymur is a compute platform which has the same power supply properties as X1E80100 and Kaanapali is a mobile platform which has the same power supply properties as SM8550. Add support for the Glymur and Kaanapali compatible strings. Signed-off-by: Anjelique Melendez Reviewed-by: Bjorn Andersson Reviewed-by: Dmitry Baryshkov Reviewed-by: Konrad Dybcio Link: https://patch.msgid.link/20260209204915.1983997-6-anjelique.melendez@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/qcom_battmgr.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/power/supply/qcom_battmgr.c b/drivers/power/supply/qcom_battmgr.c index 80572ee945b4..490137a23d00 100644 --- a/drivers/power/supply/qcom_battmgr.c +++ b/drivers/power/supply/qcom_battmgr.c @@ -1611,6 +1611,8 @@ static void qcom_battmgr_pdr_notify(void *priv, int state) } static const struct of_device_id qcom_battmgr_of_variants[] = { + { .compatible = "qcom,glymur-pmic-glink", .data = (void *)QCOM_BATTMGR_X1E80100 }, + { .compatible = "qcom,kaanapali-pmic-glink", .data = (void *)QCOM_BATTMGR_SM8550 }, { .compatible = "qcom,sc8180x-pmic-glink", .data = (void *)QCOM_BATTMGR_SC8280XP }, { .compatible = "qcom,sc8280xp-pmic-glink", .data = (void *)QCOM_BATTMGR_SC8280XP }, { .compatible = "qcom,sm8550-pmic-glink", .data = (void *)QCOM_BATTMGR_SM8550 }, From 95a1fa0b0034d11a05b6b858bd6418e2b1ccab0a Mon Sep 17 00:00:00 2001 From: Svyatoslav Ryhel Date: Fri, 30 Jan 2026 15:40:20 +0200 Subject: [PATCH 18/36] dt-bindings: power: supply: cpcap-battery: document monitored-battery property Document monitored-battery used to describe static battery cell properties. Signed-off-by: Svyatoslav Ryhel Acked-by: Rob Herring (Arm) Link: https://patch.msgid.link/20260130134021.353688-2-clamor95@gmail.com Signed-off-by: Sebastian Reichel --- .../devicetree/bindings/power/supply/cpcap-battery.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/devicetree/bindings/power/supply/cpcap-battery.yaml b/Documentation/devicetree/bindings/power/supply/cpcap-battery.yaml index 694bfdb5815c..6dcca55d6d90 100644 --- a/Documentation/devicetree/bindings/power/supply/cpcap-battery.yaml +++ b/Documentation/devicetree/bindings/power/supply/cpcap-battery.yaml @@ -55,6 +55,7 @@ properties: - const: chg_isense - const: batti + monitored-battery: true power-supplies: true required: From f0c8407c83a596dcb5aeaa940b1f8ed43631ae46 Mon Sep 17 00:00:00 2001 From: Svyatoslav Ryhel Date: Fri, 30 Jan 2026 15:40:21 +0200 Subject: [PATCH 19/36] power: supply: cpcap-battery: pass static battery cell data from device tree Add an option to populate battery cell properties from the device tree if the driver cannot access the battery's NVMEM. Signed-off-by: Svyatoslav Ryhel Reviewed-by: Tony Lindgren Link: https://patch.msgid.link/20260130134021.353688-3-clamor95@gmail.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/cpcap-battery.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/drivers/power/supply/cpcap-battery.c b/drivers/power/supply/cpcap-battery.c index f58269c75509..7b7bdce3162f 100644 --- a/drivers/power/supply/cpcap-battery.c +++ b/drivers/power/supply/cpcap-battery.c @@ -404,6 +404,30 @@ static int cpcap_battery_match_nvmem(struct device *dev, const void *data) return 0; } +static void cpcap_battery_update_battery_data(struct cpcap_battery_ddata *ddata) +{ + struct power_supply_battery_info *info; + + if (power_supply_get_battery_info(ddata->psy, &info) < 0) + return; + + if (info->technology > 0) + ddata->config.info.technology = info->technology; + + if (info->voltage_max_design_uv > 0) + ddata->config.info.voltage_max_design = info->voltage_max_design_uv; + + if (info->voltage_min_design_uv > 0) + ddata->config.info.voltage_min_design = info->voltage_min_design_uv; + + if (info->charge_full_design_uah > 0) + ddata->config.info.charge_full_design = info->charge_full_design_uah; + + if (info->constant_charge_voltage_max_uv > 0) + ddata->config.bat.constant_charge_voltage_max_uv = + info->constant_charge_voltage_max_uv; +} + static void cpcap_battery_detect_battery_type(struct cpcap_battery_ddata *ddata) { struct nvmem_device *nvmem; @@ -431,6 +455,9 @@ static void cpcap_battery_detect_battery_type(struct cpcap_battery_ddata *ddata) default: ddata->config = cpcap_battery_unknown_data; } + + if (ddata->psy) + cpcap_battery_update_battery_data(ddata); } /** From 658342fd75b582cbb06544d513171c3d645faead Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Fri, 20 Feb 2026 18:49:39 +0100 Subject: [PATCH 20/36] power: supply: axp288_charger: Do not cancel work before initializing it Driver registered devm handler to cancel_work_sync() before even the work was initialized, thus leading to possible warning from kernel/workqueue.c on (!work->func) check, if the error path was hit before the initialization happened. Use devm_work_autocancel() on each work item independently, which handles the initialization and handler to cancel work. Fixes: 165c2357744e ("power: supply: axp288_charger: Properly stop work on probe-error / remove") Cc: stable@vger.kernel.org Signed-off-by: Krzysztof Kozlowski Reviewed-by: Hans de Goede Reviewed-by: Chen-Yu Tsai Link: https://patch.msgid.link/20260220174938.672883-5-krzysztof.kozlowski@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/axp288_charger.c | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/drivers/power/supply/axp288_charger.c b/drivers/power/supply/axp288_charger.c index ac05942e4e6a..ca52c2c82b2c 100644 --- a/drivers/power/supply/axp288_charger.c +++ b/drivers/power/supply/axp288_charger.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -821,14 +822,6 @@ static int charger_init_hw_regs(struct axp288_chrg_info *info) return 0; } -static void axp288_charger_cancel_work(void *data) -{ - struct axp288_chrg_info *info = data; - - cancel_work_sync(&info->otg.work); - cancel_work_sync(&info->cable.work); -} - static int axp288_charger_probe(struct platform_device *pdev) { int ret, i, pirq; @@ -911,12 +904,12 @@ static int axp288_charger_probe(struct platform_device *pdev) } /* Cancel our work on cleanup, register this before the notifiers */ - ret = devm_add_action(dev, axp288_charger_cancel_work, info); + ret = devm_work_autocancel(dev, &info->cable.work, + axp288_charger_extcon_evt_worker); if (ret) return ret; /* Register for extcon notification */ - INIT_WORK(&info->cable.work, axp288_charger_extcon_evt_worker); info->cable.nb.notifier_call = axp288_charger_handle_cable_evt; ret = devm_extcon_register_notifier_all(dev, info->cable.edev, &info->cable.nb); @@ -926,8 +919,12 @@ static int axp288_charger_probe(struct platform_device *pdev) } schedule_work(&info->cable.work); + ret = devm_work_autocancel(dev, &info->otg.work, + axp288_charger_otg_evt_worker); + if (ret) + return ret; + /* Register for OTG notification */ - INIT_WORK(&info->otg.work, axp288_charger_otg_evt_worker); info->otg.id_nb.notifier_call = axp288_charger_handle_otg_evt; if (info->otg.cable) { ret = devm_extcon_register_notifier(dev, info->otg.cable, From 727fe2e90ec6365771b3cd49dc0e263bc602d7c1 Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Fri, 20 Feb 2026 18:49:40 +0100 Subject: [PATCH 21/36] power: supply: axp288_charger: Simplify returns of dev_err_probe() One of benefits of dev_err_probe() is that it returns the error value greatly simplifying the error paths (e.g. three lines -> one line). Signed-off-by: Krzysztof Kozlowski Reviewed-by: Hans de Goede Reviewed-by: Chen-Yu Tsai Link: https://patch.msgid.link/20260220174938.672883-6-krzysztof.kozlowski@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/axp288_charger.c | 52 ++++++++++++--------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/drivers/power/supply/axp288_charger.c b/drivers/power/supply/axp288_charger.c index ca52c2c82b2c..ea0f5caee8f0 100644 --- a/drivers/power/supply/axp288_charger.c +++ b/drivers/power/supply/axp288_charger.c @@ -859,12 +859,10 @@ static int axp288_charger_probe(struct platform_device *pdev) info->regmap_irqc = axp20x->regmap_irqc; info->cable.edev = extcon_get_extcon_dev(AXP288_EXTCON_DEV_NAME); - if (IS_ERR(info->cable.edev)) { - dev_err_probe(dev, PTR_ERR(info->cable.edev), - "extcon_get_extcon_dev(%s) failed\n", - AXP288_EXTCON_DEV_NAME); - return PTR_ERR(info->cable.edev); - } + if (IS_ERR(info->cable.edev)) + return dev_err_probe(dev, PTR_ERR(info->cable.edev), + "extcon_get_extcon_dev(%s) failed\n", + AXP288_EXTCON_DEV_NAME); /* * On devices with broken ACPI GPIO event handlers there also is no ACPI @@ -878,12 +876,11 @@ static int axp288_charger_probe(struct platform_device *pdev) if (extcon_name) { info->otg.cable = extcon_get_extcon_dev(extcon_name); - if (IS_ERR(info->otg.cable)) { - dev_err_probe(dev, PTR_ERR(info->otg.cable), - "extcon_get_extcon_dev(%s) failed\n", - USB_HOST_EXTCON_NAME); - return PTR_ERR(info->otg.cable); - } + if (IS_ERR(info->otg.cable)) + return dev_err_probe(dev, PTR_ERR(info->otg.cable), + "extcon_get_extcon_dev(%s) failed\n", + USB_HOST_EXTCON_NAME); + dev_info(dev, "Using " USB_HOST_EXTCON_HID " extcon for usb-id\n"); } @@ -897,11 +894,9 @@ static int axp288_charger_probe(struct platform_device *pdev) charger_cfg.drv_data = info; info->psy_usb = devm_power_supply_register(dev, &axp288_charger_desc, &charger_cfg); - if (IS_ERR(info->psy_usb)) { - ret = PTR_ERR(info->psy_usb); - dev_err(dev, "failed to register power supply: %d\n", ret); - return ret; - } + if (IS_ERR(info->psy_usb)) + return dev_err_probe(dev, PTR_ERR(info->psy_usb), + "failed to register power supply: %d\n", ret); /* Cancel our work on cleanup, register this before the notifiers */ ret = devm_work_autocancel(dev, &info->cable.work, @@ -913,10 +908,9 @@ static int axp288_charger_probe(struct platform_device *pdev) info->cable.nb.notifier_call = axp288_charger_handle_cable_evt; ret = devm_extcon_register_notifier_all(dev, info->cable.edev, &info->cable.nb); - if (ret) { - dev_err(dev, "failed to register cable extcon notifier\n"); - return ret; - } + if (ret) + return dev_err_probe(dev, ret, "failed to register cable extcon notifier\n"); + schedule_work(&info->cable.work); ret = devm_work_autocancel(dev, &info->otg.work, @@ -929,10 +923,10 @@ static int axp288_charger_probe(struct platform_device *pdev) if (info->otg.cable) { ret = devm_extcon_register_notifier(dev, info->otg.cable, EXTCON_USB_HOST, &info->otg.id_nb); - if (ret) { - dev_err(dev, "failed to register EXTCON_USB_HOST notifier\n"); - return ret; - } + if (ret) + return dev_err_probe(dev, ret, + "failed to register EXTCON_USB_HOST notifier\n"); + schedule_work(&info->otg.work); } @@ -951,11 +945,9 @@ static int axp288_charger_probe(struct platform_device *pdev) ret = devm_request_threaded_irq(&info->pdev->dev, info->irq[i], NULL, axp288_charger_irq_thread_handler, IRQF_ONESHOT, info->pdev->name, info); - if (ret) { - dev_err(dev, "failed to request interrupt=%d\n", - info->irq[i]); - return ret; - } + if (ret) + return dev_err_probe(dev, ret, "failed to request interrupt=%d\n", + info->irq[i]); } return 0; From 4f73a52df7d282784f4f040efc9e124b477ca504 Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Fri, 20 Feb 2026 18:49:41 +0100 Subject: [PATCH 22/36] power: supply: bq24190: Avoid rescheduling after cancelling work Driver initializes delayed work and then registers interrupt handler with devm interface. This means that device removal will not use a reversed order, but first cancel pending work items and then, via devm release handlers, free the interrupt. The interrupt handler does not directly use/schedule work items on the workqueue, however it updates the status of the battery charger which might lead to calling power_supply_changed() and trigger chain of calls leading to scheduling the work items. If this happens during short time window after cancel_delayed_work_sync() in remove() callback, the work would be rescheduled. Avoid this by using devm interface to initialize and cancel work item, thus having exactly reverse order during remove() in respect to rest of the probe/cleanup paths. This is also more logical and readable code. Signed-off-by: Krzysztof Kozlowski Reviewed-by: Hans de Goede Link: https://patch.msgid.link/20260220174938.672883-7-krzysztof.kozlowski@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/bq24190_charger.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/drivers/power/supply/bq24190_charger.c b/drivers/power/supply/bq24190_charger.c index ed0ceae8d90b..55da91bacc3e 100644 --- a/drivers/power/supply/bq24190_charger.c +++ b/drivers/power/supply/bq24190_charger.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -2087,8 +2088,11 @@ static int bq24190_probe(struct i2c_client *client) bdi->charge_type = POWER_SUPPLY_CHARGE_TYPE_FAST; bdi->f_reg = 0; bdi->ss_reg = BQ24190_REG_SS_VBUS_STAT_MASK; /* impossible state */ - INIT_DELAYED_WORK(&bdi->input_current_limit_work, - bq24190_input_current_limit_work); + + ret = devm_delayed_work_autocancel(dev, &bdi->input_current_limit_work, + bq24190_input_current_limit_work); + if (ret) + return ret; i2c_set_clientdata(client, bdi); @@ -2198,7 +2202,6 @@ static void bq24190_remove(struct i2c_client *client) struct bq24190_dev_info *bdi = i2c_get_clientdata(client); int error; - cancel_delayed_work_sync(&bdi->input_current_limit_work); error = pm_runtime_resume_and_get(bdi->dev); if (error < 0) dev_warn(bdi->dev, "pm_runtime_get failed: %i\n", error); From e6d91eed847778dbc9a6a595d5fb3015ab305483 Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Fri, 20 Feb 2026 18:49:42 +0100 Subject: [PATCH 23/36] power: supply: twl4030_madc: Drop unused header includes Driver does not use any code from workqueue.h and param.h. Signed-off-by: Krzysztof Kozlowski Reviewed-by: Hans de Goede Link: https://patch.msgid.link/20260220174938.672883-8-krzysztof.kozlowski@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/twl4030_madc_battery.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/power/supply/twl4030_madc_battery.c b/drivers/power/supply/twl4030_madc_battery.c index 3935162e350b..a99b3ff26929 100644 --- a/drivers/power/supply/twl4030_madc_battery.c +++ b/drivers/power/supply/twl4030_madc_battery.c @@ -11,9 +11,7 @@ */ #include -#include #include -#include #include #include #include From c2bfe2edf741b6ae03acda7ab795974cf53d342c Mon Sep 17 00:00:00 2001 From: Andrew Davis Date: Tue, 3 Mar 2026 11:59:59 -0600 Subject: [PATCH 24/36] power: reset: keystone: Use register_sys_off_handler(SYS_OFF_MODE_RESTART) Function register_restart_handler() is deprecated. Using this new API removes our need to keep and manage a struct notifier_block. Signed-off-by: Andrew Davis Link: https://patch.msgid.link/20260303175959.75647-1-afd@ti.com Signed-off-by: Sebastian Reichel --- drivers/power/reset/keystone-reset.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/drivers/power/reset/keystone-reset.c b/drivers/power/reset/keystone-reset.c index d9268d150e1f..3c44cd6cee0a 100644 --- a/drivers/power/reset/keystone-reset.c +++ b/drivers/power/reset/keystone-reset.c @@ -48,8 +48,7 @@ static inline int rsctrl_enable_rspll_write(void) RSCTRL_KEY_MASK, RSCTRL_KEY); } -static int rsctrl_restart_handler(struct notifier_block *this, - unsigned long mode, void *cmd) +static int rsctrl_restart_handler(struct sys_off_data *data) { /* enable write access to RSTCTRL */ rsctrl_enable_rspll_write(); @@ -61,11 +60,6 @@ static int rsctrl_restart_handler(struct notifier_block *this, return NOTIFY_DONE; } -static struct notifier_block rsctrl_restart_nb = { - .notifier_call = rsctrl_restart_handler, - .priority = 128, -}; - static const struct of_device_id rsctrl_of_match[] = { {.compatible = "ti,keystone-reset", }, {}, @@ -140,7 +134,8 @@ static int rsctrl_probe(struct platform_device *pdev) return ret; } - ret = register_restart_handler(&rsctrl_restart_nb); + ret = devm_register_sys_off_handler(dev, SYS_OFF_MODE_RESTART, 128, + rsctrl_restart_handler, NULL); if (ret) dev_err(dev, "cannot register restart handler (err=%d)\n", ret); From db254b0b232358ab1aeadebe8d147c99a3569559 Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Thu, 5 Mar 2026 22:45:41 +0100 Subject: [PATCH 25/36] power: supply: cw2015: Free allocated workqueue Use devm interface so allocated workqueue will be freed during device removal and error paths, thus fixing a memory leak. Change is not equivalent in the workqueue itself: use non-legacy API which does not set (__WQ_LEGACY | WQ_MEM_RECLAIM). The workqueue is used to read updated data from the battery, thus there is no point to run it for memory reclaim. Cc: stable@vger.kernel.org Fixes: b4c7715c10c1 ("power: supply: add CellWise cw2015 fuel gauge driver") Signed-off-by: Krzysztof Kozlowski Reviewed-by: Andy Shevchenko Link: https://patch.msgid.link/20260305-workqueue-devm-v2-2-66a38741c652@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/cw2015_battery.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/power/supply/cw2015_battery.c b/drivers/power/supply/cw2015_battery.c index a05dcc4a48f2..286524d2318c 100644 --- a/drivers/power/supply/cw2015_battery.c +++ b/drivers/power/supply/cw2015_battery.c @@ -694,7 +694,8 @@ static int cw_bat_probe(struct i2c_client *client) "No monitored battery, some properties will be missing\n"); } - cw_bat->battery_workqueue = create_singlethread_workqueue("rk_battery"); + cw_bat->battery_workqueue = devm_alloc_ordered_workqueue(&client->dev, + "rk_battery", 0); if (!cw_bat->battery_workqueue) return -ENOMEM; From 2064c64ceb1996ee02a6bbb1de05fd6e8028e3e4 Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Thu, 5 Mar 2026 22:45:42 +0100 Subject: [PATCH 26/36] power: supply: max77705: Drop duplicated IRQ error message Core already prints error message on devm_request_threaded_irq() failure, so no need to do that second time. Suggested-by: Andy Shevchenko Signed-off-by: Krzysztof Kozlowski Reviewed-by: Andy Shevchenko Link: https://patch.msgid.link/20260305-workqueue-devm-v2-3-66a38741c652@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/max77705_charger.c | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/drivers/power/supply/max77705_charger.c b/drivers/power/supply/max77705_charger.c index 5dd02f658f5b..0dfe4ab10919 100644 --- a/drivers/power/supply/max77705_charger.c +++ b/drivers/power/supply/max77705_charger.c @@ -666,19 +666,15 @@ static int max77705_charger_probe(struct i2c_client *i2c) NULL, max77705_chgin_irq, IRQF_TRIGGER_NONE, "chgin-irq", chg); - if (ret) { - dev_err_probe(dev, ret, "Failed to Request chgin IRQ\n"); + if (ret) goto destroy_wq; - } ret = devm_request_threaded_irq(dev, regmap_irq_get_virq(irq_data, MAX77705_AICL_I), NULL, max77705_aicl_irq, IRQF_TRIGGER_NONE, "aicl-irq", chg); - if (ret) { - dev_err_probe(dev, ret, "Failed to Request aicl IRQ\n"); + if (ret) goto destroy_wq; - } ret = max77705_charger_enable(chg); if (ret) { From 1e668baadefb16e81269dbfebf3ffc2672e3a3bb Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Thu, 5 Mar 2026 22:45:43 +0100 Subject: [PATCH 27/36] power: supply: max77705: Free allocated workqueue and fix removal order Use devm interface for allocating workqueue to fix two bugs at the same time: 1. Driver leaks the memory on remove(), because the workqueue is not destroyed. 2. Driver allocates workqueue and then registers interrupt handlers with devm interface. This means that probe error paths will not use a reversed order, but first destroy the workqueue and then, via devm release handlers, free the interrupt. The interrupt handler schedules work on this exact workqueue, thus if interrupt is hit in this short time window - after destroying workqueue, but before devm() frees the interrupt - the schedulled work will lead to use of freed memory. Change is not equivalent in the workqueue itself: use non-legacy API which does not set (__WQ_LEGACY | WQ_MEM_RECLAIM). The workqueue is used to update power supply (power_supply_changed()) status, thus there is no point to run it for memory reclaim. Note that dev_name() is not directly used in second argument to prevent possible unlikely parsing any "%" character in device name as format. Fixes: 11741b8e382d ("power: supply: max77705: Fix workqueue error handling in probe") Fixes: a6a494c8e3ce ("power: supply: max77705: Add charger driver for Maxim 77705") Signed-off-by: Krzysztof Kozlowski Reviewed-by: Andy Shevchenko Link: https://patch.msgid.link/20260305-workqueue-devm-v2-4-66a38741c652@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/max77705_charger.c | 28 ++++++++----------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/drivers/power/supply/max77705_charger.c b/drivers/power/supply/max77705_charger.c index 0dfe4ab10919..63b0b4f0cd21 100644 --- a/drivers/power/supply/max77705_charger.c +++ b/drivers/power/supply/max77705_charger.c @@ -646,47 +646,37 @@ static int max77705_charger_probe(struct i2c_client *i2c) if (ret) return dev_err_probe(dev, ret, "failed to add irq chip\n"); - chg->wqueue = create_singlethread_workqueue(dev_name(dev)); + chg->wqueue = devm_alloc_ordered_workqueue(dev, "%s", 0, dev_name(dev)); if (!chg->wqueue) return -ENOMEM; ret = devm_work_autocancel(dev, &chg->chgin_work, max77705_chgin_isr_work); - if (ret) { - dev_err_probe(dev, ret, "failed to initialize interrupt work\n"); - goto destroy_wq; - } + if (ret) + return dev_err_probe(dev, ret, "failed to initialize interrupt work\n"); ret = max77705_charger_initialize(chg); - if (ret) { - dev_err_probe(dev, ret, "failed to initialize charger IC\n"); - goto destroy_wq; - } + if (ret) + return dev_err_probe(dev, ret, "failed to initialize charger IC\n"); ret = devm_request_threaded_irq(dev, regmap_irq_get_virq(irq_data, MAX77705_CHGIN_I), NULL, max77705_chgin_irq, IRQF_TRIGGER_NONE, "chgin-irq", chg); if (ret) - goto destroy_wq; + return ret; ret = devm_request_threaded_irq(dev, regmap_irq_get_virq(irq_data, MAX77705_AICL_I), NULL, max77705_aicl_irq, IRQF_TRIGGER_NONE, "aicl-irq", chg); if (ret) - goto destroy_wq; + return ret; ret = max77705_charger_enable(chg); - if (ret) { - dev_err_probe(dev, ret, "failed to enable charge\n"); - goto destroy_wq; - } + if (ret) + return dev_err_probe(dev, ret, "failed to enable charge\n"); return devm_add_action_or_reset(dev, max77705_charger_disable, chg); - -destroy_wq: - destroy_workqueue(chg->wqueue); - return ret; } static const struct of_device_id max77705_charger_of_match[] = { From f23afa01040a41882a048e4957a7acac1426da6f Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Thu, 5 Mar 2026 22:45:44 +0100 Subject: [PATCH 28/36] power: supply: mt6370: Simplify with devm_alloc_ordered_workqueue() Simplify the driver probe function by using devm_alloc_ordered_workqueue() which handles the cleanup already. Change is not equivalent in the workqueue itself: use non-legacy API which does not set (__WQ_LEGACY | WQ_MEM_RECLAIM). The workqueue is used to update power supply data (power_supply_changed()) status, thus there is no point to run it for memory reclaim. Note that dev_name() is not directly used in second argument to prevent possible unlikely parsing any "%" character in device name as format. Signed-off-by: Krzysztof Kozlowski Reviewed-by: Andy Shevchenko Link: https://patch.msgid.link/20260305-workqueue-devm-v2-5-66a38741c652@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/mt6370-charger.c | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/drivers/power/supply/mt6370-charger.c b/drivers/power/supply/mt6370-charger.c index e6db961d5818..916556baa854 100644 --- a/drivers/power/supply/mt6370-charger.c +++ b/drivers/power/supply/mt6370-charger.c @@ -761,13 +761,6 @@ static int mt6370_chg_init_psy(struct mt6370_priv *priv) return PTR_ERR_OR_ZERO(priv->psy); } -static void mt6370_chg_destroy_wq(void *data) -{ - struct workqueue_struct *wq = data; - - destroy_workqueue(wq); -} - static irqreturn_t mt6370_attach_i_handler(int irq, void *data) { struct mt6370_priv *priv = data; @@ -893,14 +886,10 @@ static int mt6370_chg_probe(struct platform_device *pdev) priv->attach = MT6370_ATTACH_STAT_DETACH; - priv->wq = create_singlethread_workqueue(dev_name(priv->dev)); + priv->wq = devm_alloc_ordered_workqueue(dev, "%s", 0, dev_name(priv->dev)); if (!priv->wq) return -ENOMEM; - ret = devm_add_action_or_reset(dev, mt6370_chg_destroy_wq, priv->wq); - if (ret) - return ret; - ret = devm_work_autocancel(dev, &priv->bc12_work, mt6370_chg_bc12_work_func); if (ret) return dev_err_probe(dev, ret, "Failed to init bc12 work\n"); From 2cfc7cac68e19c4acb236b8db6065bbaff5deee8 Mon Sep 17 00:00:00 2001 From: Krzysztof Kozlowski Date: Thu, 5 Mar 2026 22:45:45 +0100 Subject: [PATCH 29/36] power: supply: ipaq_micro: Simplify with devm Simplify the driver by using devm interfaces, which allow to drop probe() error paths and the remove() callback. Signed-off-by: Krzysztof Kozlowski Reviewed-by: Andy Shevchenko Link: https://patch.msgid.link/20260305-workqueue-devm-v2-6-66a38741c652@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/supply/ipaq_micro_battery.c | 50 ++++++++--------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/drivers/power/supply/ipaq_micro_battery.c b/drivers/power/supply/ipaq_micro_battery.c index ff8573a5ca6d..5e3fe3852d0e 100644 --- a/drivers/power/supply/ipaq_micro_battery.c +++ b/drivers/power/supply/ipaq_micro_battery.c @@ -7,6 +7,7 @@ * Author : Linus Walleij */ +#include #include #include #include @@ -232,49 +233,31 @@ static int micro_batt_probe(struct platform_device *pdev) return -ENOMEM; mb->micro = dev_get_drvdata(pdev->dev.parent); - mb->wq = alloc_workqueue("ipaq-battery-wq", - WQ_MEM_RECLAIM | WQ_PERCPU, 0); + mb->wq = devm_alloc_workqueue(&pdev->dev, "ipaq-battery-wq", + WQ_MEM_RECLAIM | WQ_PERCPU, 0); if (!mb->wq) return -ENOMEM; - INIT_DELAYED_WORK(&mb->update, micro_battery_work); + ret = devm_delayed_work_autocancel(&pdev->dev, &mb->update, micro_battery_work); + if (ret) + return ret; + platform_set_drvdata(pdev, mb); queue_delayed_work(mb->wq, &mb->update, 1); - micro_batt_power = power_supply_register(&pdev->dev, - µ_batt_power_desc, NULL); - if (IS_ERR(micro_batt_power)) { - ret = PTR_ERR(micro_batt_power); - goto batt_err; - } + micro_batt_power = devm_power_supply_register(&pdev->dev, + µ_batt_power_desc, + NULL); + if (IS_ERR(micro_batt_power)) + return PTR_ERR(micro_batt_power); - micro_ac_power = power_supply_register(&pdev->dev, - µ_ac_power_desc, NULL); - if (IS_ERR(micro_ac_power)) { - ret = PTR_ERR(micro_ac_power); - goto ac_err; - } + micro_ac_power = devm_power_supply_register(&pdev->dev, + µ_ac_power_desc, NULL); + if (IS_ERR(micro_ac_power)) + return PTR_ERR(micro_ac_power); dev_info(&pdev->dev, "iPAQ micro battery driver\n"); return 0; - -ac_err: - power_supply_unregister(micro_batt_power); -batt_err: - cancel_delayed_work_sync(&mb->update); - destroy_workqueue(mb->wq); - return ret; -} - -static void micro_batt_remove(struct platform_device *pdev) - -{ - struct micro_battery *mb = platform_get_drvdata(pdev); - - power_supply_unregister(micro_ac_power); - power_supply_unregister(micro_batt_power); - cancel_delayed_work_sync(&mb->update); - destroy_workqueue(mb->wq); } static int __maybe_unused micro_batt_suspend(struct device *dev) @@ -303,7 +286,6 @@ static struct platform_driver micro_batt_device_driver = { .pm = µ_batt_dev_pm_ops, }, .probe = micro_batt_probe, - .remove = micro_batt_remove, }; module_platform_driver(micro_batt_device_driver); From c7e05ab38adc44d0cae4888016829359dcbba7b2 Mon Sep 17 00:00:00 2001 From: Arnd Bergmann Date: Fri, 6 Mar 2026 16:07:34 +0100 Subject: [PATCH 30/36] power: reset: reboot-mode: fix -Wformat-security warning The device_create() function expects a format string to construct a device name, so passing a variable here introduces a possible vulnerability in case the string can contain '%' characters: drivers/power/reset/reboot-mode.c:148:22: error: format string is not a string literal (potentially insecure) [-Werror,-Wformat-security] drivers/power/reset/reboot-mode.c:148:22: note: treat the string as an argument to avoid this 148 | (void *)priv, reboot->dev->driver->name); Use an trivial "%s" format instead and pass the name as the string to be included here. Fixes: cfaf0a90789a ("power: reset: reboot-mode: Expose sysfs for registered reboot_modes") Signed-off-by: Arnd Bergmann Link: https://patch.msgid.link/20260306150738.497978-1-arnd@kernel.org Signed-off-by: Sebastian Reichel --- drivers/power/reset/reboot-mode.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/power/reset/reboot-mode.c b/drivers/power/reset/reboot-mode.c index ad239e96774b..d20e44db0532 100644 --- a/drivers/power/reset/reboot-mode.c +++ b/drivers/power/reset/reboot-mode.c @@ -145,7 +145,8 @@ static int reboot_mode_create_device(struct reboot_mode_driver *reboot) } priv->reboot_mode_device = device_create(&reboot_mode_class, NULL, 0, - (void *)priv, reboot->dev->driver->name); + (void *)priv, "%s", + reboot->dev->driver->name); if (IS_ERR(priv->reboot_mode_device)) { ret = PTR_ERR(priv->reboot_mode_device); goto error; From d74b4fcc8093f9dc84172adf15f93b858d3daaac Mon Sep 17 00:00:00 2001 From: Kaustabh Chakraborty Date: Wed, 4 Mar 2026 22:33:55 +0530 Subject: [PATCH 31/36] dt-bindings: power: supply: document Samsung S2MU005 battery fuel gauge Samsung S2MU005 is a PMIC device which has LED controllers, an MUIC and a battery charger. The battery charger is paired with an independent device connected via I2C which can be used to access various metrics of the battery. Document the device as a schema. Reviewed-by: Rob Herring (Arm) Acked-by: Conor Dooley Signed-off-by: Kaustabh Chakraborty Link: https://patch.msgid.link/20260304-s2mu005-fuelgauge-v3-1-e4dc4e47cde8@disroot.org Signed-off-by: Sebastian Reichel --- .../supply/samsung,s2mu005-fuel-gauge.yaml | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 Documentation/devicetree/bindings/power/supply/samsung,s2mu005-fuel-gauge.yaml diff --git a/Documentation/devicetree/bindings/power/supply/samsung,s2mu005-fuel-gauge.yaml b/Documentation/devicetree/bindings/power/supply/samsung,s2mu005-fuel-gauge.yaml new file mode 100644 index 000000000000..05e420316a26 --- /dev/null +++ b/Documentation/devicetree/bindings/power/supply/samsung,s2mu005-fuel-gauge.yaml @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/power/supply/samsung,s2mu005-fuel-gauge.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Battery Fuel Gauge for Samsung S2M series PMICs + +maintainers: + - Kaustabh Chakraborty + +allOf: + - $ref: power-supply.yaml# + +properties: + compatible: + enum: + - samsung,s2mu005-fuel-gauge + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + +required: + - compatible + - reg + +unevaluatedProperties: false + +examples: + - | + #include + + i2c { + #address-cells = <1>; + #size-cells = <0>; + + fuel-gauge@3b { + compatible = "samsung,s2mu005-fuel-gauge"; + reg = <0x3b>; + + interrupt-parent = <&gpa0>; + interrupts = <3 IRQ_TYPE_EDGE_BOTH>; + + monitored-battery = <&battery>; + }; + }; From aa2132799817fb052d95a87f0c23cc6af38541c0 Mon Sep 17 00:00:00 2001 From: Yassine Oudjana Date: Wed, 4 Mar 2026 22:33:56 +0530 Subject: [PATCH 32/36] power: supply: add support for S2MU005 battery fuel gauge device Samsung's S2MU005 PMIC, which contains battery charger functionality also includes a battery fuel gauge device, which is separate from the PMIC itself, and typically connected to an I2C bus. Add a generic driver to support said device. Signed-off-by: Yassine Oudjana Co-developed-by: Kaustabh Chakraborty Signed-off-by: Kaustabh Chakraborty Link: https://patch.msgid.link/20260304-s2mu005-fuelgauge-v3-2-e4dc4e47cde8@disroot.org [Moved mutex init before power-supply registration] Signed-off-by: Sebastian Reichel --- drivers/power/supply/Kconfig | 11 + drivers/power/supply/Makefile | 1 + drivers/power/supply/s2mu005-battery.c | 307 +++++++++++++++++++++++++ 3 files changed, 319 insertions(+) create mode 100644 drivers/power/supply/s2mu005-battery.c diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig index 3a5b7d9234c2..e8a1bdb246c4 100644 --- a/drivers/power/supply/Kconfig +++ b/drivers/power/supply/Kconfig @@ -229,6 +229,17 @@ config BATTERY_SAMSUNG_SDI Say Y to enable support for Samsung SDI battery data. These batteries are used in Samsung mobile phones. +config BATTERY_S2MU005 + tristate "Samsung S2MU005 PMIC fuel gauge driver" + depends on I2C + select REGMAP_I2C + help + Say Y to enable support for the Samsung S2MU005 PMIC integrated + fuel gauge, which works indepenently of the PMIC battery charger + counterpart, and reports battery metrics. + + This driver, if built as a module, will be called s2mu005-fuel-gauge. + config BATTERY_COLLIE tristate "Sharp SL-5500 (collie) battery" depends on SA1100_COLLIE && MCP_UCB1200 diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile index d14420b606d8..f2efbb82707c 100644 --- a/drivers/power/supply/Makefile +++ b/drivers/power/supply/Makefile @@ -40,6 +40,7 @@ obj-$(CONFIG_BATTERY_PMU) += pmu_battery.o obj-$(CONFIG_BATTERY_QCOM_BATTMGR) += qcom_battmgr.o obj-$(CONFIG_BATTERY_OLPC) += olpc_battery.o obj-$(CONFIG_BATTERY_SAMSUNG_SDI) += samsung-sdi-battery.o +obj-$(CONFIG_BATTERY_S2MU005) += s2mu005-battery.o obj-$(CONFIG_BATTERY_COLLIE) += collie_battery.o obj-$(CONFIG_BATTERY_INGENIC) += ingenic-battery.o obj-$(CONFIG_BATTERY_INTEL_DC_TI) += intel_dc_ti_battery.o diff --git a/drivers/power/supply/s2mu005-battery.c b/drivers/power/supply/s2mu005-battery.c new file mode 100644 index 000000000000..64c57a14ef7d --- /dev/null +++ b/drivers/power/supply/s2mu005-battery.c @@ -0,0 +1,307 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Battery Fuel Gauge Driver for Samsung S2MU005 PMIC. + * + * Copyright (C) 2015 Samsung Electronics + * Copyright (C) 2023 Yassine Oudjana + * Copyright (C) 2025 Kaustabh Chakraborty + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define S2MU005_FG_REG_STATUS 0x00 +#define S2MU005_FG_REG_IRQ 0x02 +#define S2MU005_FG_REG_RVBAT 0x04 +#define S2MU005_FG_REG_RCURCC 0x06 +#define S2MU005_FG_REG_RSOC 0x08 +#define S2MU005_FG_REG_MONOUT 0x0a +#define S2MU005_FG_REG_MONOUTSEL 0x0c +#define S2MU005_FG_REG_RBATCAP 0x0e +#define S2MU005_FG_REG_RZADJ 0x12 +#define S2MU005_FG_REG_RBATZ0 0x16 +#define S2MU005_FG_REG_RBATZ1 0x18 +#define S2MU005_FG_REG_IRQLVL 0x1a +#define S2MU005_FG_REG_START 0x1e + +#define S2MU005_FG_MONOUTSEL_AVGCURRENT 0x26 +#define S2MU005_FG_MONOUTSEL_AVGVOLTAGE 0x27 + +struct s2mu005_fg { + struct device *dev; + struct regmap *regmap; + struct power_supply *psy; + struct mutex monout_mutex; +}; + +static const struct regmap_config s2mu005_fg_regmap_config = { + .reg_bits = 8, + .val_bits = 16, + .val_format_endian = REGMAP_ENDIAN_LITTLE, +}; + +static irqreturn_t s2mu005_handle_irq(int irq, void *data) +{ + struct s2mu005_fg *priv = data; + + msleep(100); + power_supply_changed(priv->psy); + + return IRQ_HANDLED; +} + +static int s2mu005_fg_get_voltage_now(struct s2mu005_fg *priv, int *value) +{ + struct regmap *regmap = priv->regmap; + u32 val; + int ret; + + ret = regmap_read(regmap, S2MU005_FG_REG_RVBAT, &val); + if (ret < 0) { + dev_err(priv->dev, "failed to read voltage register (%d)\n", ret); + return ret; + } + + *value = (val * MICRO) >> 13; + + return 0; +} + +static int s2mu005_fg_get_voltage_avg(struct s2mu005_fg *priv, int *value) +{ + struct regmap *regmap = priv->regmap; + u32 val; + int ret; + + mutex_lock(&priv->monout_mutex); + + ret = regmap_write(regmap, S2MU005_FG_REG_MONOUTSEL, + S2MU005_FG_MONOUTSEL_AVGVOLTAGE); + if (ret < 0) { + dev_err(priv->dev, "failed to enable average voltage monitoring (%d)\n", + ret); + goto unlock; + } + + ret = regmap_read(regmap, S2MU005_FG_REG_MONOUT, &val); + if (ret < 0) { + dev_err(priv->dev, "failed to read current register (%d)\n", ret); + goto unlock; + } + + *value = (val * MICRO) >> 12; + +unlock: + mutex_unlock(&priv->monout_mutex); + + return ret; +} +static int s2mu005_fg_get_current_now(struct s2mu005_fg *priv, int *value) +{ + struct regmap *regmap = priv->regmap; + u32 val; + int ret; + + ret = regmap_read(regmap, S2MU005_FG_REG_RCURCC, &val); + if (ret < 0) { + dev_err(priv->dev, "failed to read current register (%d)\n", ret); + return ret; + } + + *value = -((s16)val * MICRO) >> 12; + + return 0; +} + +static int s2mu005_fg_get_current_avg(struct s2mu005_fg *priv, int *value) +{ + struct regmap *regmap = priv->regmap; + u32 val; + int ret; + + mutex_lock(&priv->monout_mutex); + + ret = regmap_write(regmap, S2MU005_FG_REG_MONOUTSEL, + S2MU005_FG_MONOUTSEL_AVGCURRENT); + if (ret < 0) { + dev_err(priv->dev, "failed to enable average current monitoring (%d)\n", + ret); + goto unlock; + } + + ret = regmap_read(regmap, S2MU005_FG_REG_MONOUT, &val); + if (ret < 0) { + dev_err(priv->dev, "failed to read current register (%d)\n", ret); + goto unlock; + } + + *value = -((s16)val * MICRO) >> 12; + +unlock: + mutex_unlock(&priv->monout_mutex); + + return ret; +} + +static int s2mu005_fg_get_capacity(struct s2mu005_fg *priv, int *value) +{ + struct regmap *regmap = priv->regmap; + u32 val; + int ret; + + ret = regmap_read(regmap, S2MU005_FG_REG_RSOC, &val); + if (ret < 0) { + dev_err(priv->dev, "failed to read capacity register (%d)\n", ret); + return ret; + } + + *value = (val * CENTI) >> 14; + + return 0; +} + +static int s2mu005_fg_get_status(struct s2mu005_fg *priv, int *value) +{ + int current_now, current_avg, capacity; + int ret; + + ret = s2mu005_fg_get_current_now(priv, ¤t_now); + if (ret < 0) + return ret; + + ret = s2mu005_fg_get_current_avg(priv, ¤t_avg); + if (ret < 0) + return ret; + + /* + * Verify both current values reported to reduce inaccuracies due to + * internal hysteresis. + */ + if (current_now < 0 && current_avg < 0) { + *value = POWER_SUPPLY_STATUS_DISCHARGING; + } else if (current_now == 0) { + *value = POWER_SUPPLY_STATUS_NOT_CHARGING; + } else { + *value = POWER_SUPPLY_STATUS_CHARGING; + + ret = s2mu005_fg_get_capacity(priv, &capacity); + if (!ret && capacity > 98) + *value = POWER_SUPPLY_STATUS_FULL; + return ret; + } + + return 0; +} + +static const enum power_supply_property s2mu005_fg_properties[] = { + POWER_SUPPLY_PROP_VOLTAGE_NOW, + POWER_SUPPLY_PROP_VOLTAGE_AVG, + POWER_SUPPLY_PROP_CURRENT_NOW, + POWER_SUPPLY_PROP_CURRENT_AVG, + POWER_SUPPLY_PROP_CAPACITY, + POWER_SUPPLY_PROP_STATUS, +}; + +static int s2mu005_fg_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + struct s2mu005_fg *priv = power_supply_get_drvdata(psy); + + switch (psp) { + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + return s2mu005_fg_get_voltage_now(priv, &val->intval); + case POWER_SUPPLY_PROP_VOLTAGE_AVG: + return s2mu005_fg_get_voltage_avg(priv, &val->intval); + case POWER_SUPPLY_PROP_CURRENT_NOW: + return s2mu005_fg_get_current_now(priv, &val->intval); + case POWER_SUPPLY_PROP_CURRENT_AVG: + return s2mu005_fg_get_current_avg(priv, &val->intval); + case POWER_SUPPLY_PROP_CAPACITY: + return s2mu005_fg_get_capacity(priv, &val->intval); + case POWER_SUPPLY_PROP_STATUS: + return s2mu005_fg_get_status(priv, &val->intval); + default: + return -EINVAL; + } +} + +static const struct power_supply_desc s2mu005_fg_desc = { + .name = "s2mu005-fuel-gauge", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = s2mu005_fg_properties, + .num_properties = ARRAY_SIZE(s2mu005_fg_properties), + .get_property = s2mu005_fg_get_property, +}; + +static int s2mu005_fg_i2c_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct s2mu005_fg *priv; + struct power_supply_config psy_cfg = {}; + const struct power_supply_desc *psy_desc; + int ret; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + dev_set_drvdata(dev, priv); + priv->dev = dev; + + priv->regmap = devm_regmap_init_i2c(client, &s2mu005_fg_regmap_config); + if (IS_ERR(priv->regmap)) + return dev_err_probe(dev, PTR_ERR(priv->regmap), + "failed to initialize regmap\n"); + + ret = devm_mutex_init(dev, &priv->monout_mutex); + if (ret) + dev_err_probe(dev, ret, "failed to initialize MONOUT mutex\n"); + + psy_desc = device_get_match_data(dev); + + psy_cfg.drv_data = priv; + psy_cfg.fwnode = dev_fwnode(dev); + priv->psy = devm_power_supply_register(priv->dev, psy_desc, &psy_cfg); + if (IS_ERR(priv->psy)) + return dev_err_probe(dev, PTR_ERR(priv->psy), + "failed to register power supply subsystem\n"); + + ret = devm_request_threaded_irq(priv->dev, client->irq, NULL, + s2mu005_handle_irq, IRQF_ONESHOT, + psy_desc->name, priv); + if (ret) + dev_err_probe(dev, ret, "failed to request IRQ\n"); + + return 0; +} + +static const struct of_device_id s2mu005_fg_of_match_table[] = { + { + .compatible = "samsung,s2mu005-fuel-gauge", + .data = &s2mu005_fg_desc, + }, + { } +}; +MODULE_DEVICE_TABLE(of, s2mu005_fg_of_match_table); + +static struct i2c_driver s2mu005_fg_i2c_driver = { + .probe = s2mu005_fg_i2c_probe, + .driver = { + .name = "s2mu005-fuel-gauge", + .of_match_table = s2mu005_fg_of_match_table, + }, +}; +module_i2c_driver(s2mu005_fg_i2c_driver); + +MODULE_DESCRIPTION("Samsung S2MU005 PMIC Battery Fuel Gauge Driver"); +MODULE_AUTHOR("Yassine Oudjana "); +MODULE_AUTHOR("Kaustabh Chakraborty "); +MODULE_LICENSE("GPL"); From 64a97c98f93e344be00d4ff10fef4119973938bd Mon Sep 17 00:00:00 2001 From: Khushal Chitturi Date: Mon, 30 Mar 2026 16:31:34 +0530 Subject: [PATCH 33/36] dt-bindings: power: reset: cortina,gemini-power-controller: convert to DT schema Convert the Cortina Systems Gemini Poweroff Controller bindings to DT schema. Signed-off-by: Khushal Chitturi Reviewed-by: Krzysztof Kozlowski Link: https://patch.msgid.link/20260330110135.10316-2-khushalchitturi@gmail.com Signed-off-by: Sebastian Reichel --- .../cortina,gemini-power-controller.yaml | 42 +++++++++++++++++++ .../bindings/power/reset/gemini-poweroff.txt | 17 -------- 2 files changed, 42 insertions(+), 17 deletions(-) create mode 100644 Documentation/devicetree/bindings/power/reset/cortina,gemini-power-controller.yaml delete mode 100644 Documentation/devicetree/bindings/power/reset/gemini-poweroff.txt diff --git a/Documentation/devicetree/bindings/power/reset/cortina,gemini-power-controller.yaml b/Documentation/devicetree/bindings/power/reset/cortina,gemini-power-controller.yaml new file mode 100644 index 000000000000..ef5e04f86be1 --- /dev/null +++ b/Documentation/devicetree/bindings/power/reset/cortina,gemini-power-controller.yaml @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/power/reset/cortina,gemini-power-controller.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Cortina Systems Gemini Poweroff Controller + +maintainers: + - Linus Walleij + +description: | + The Gemini power controller is a dedicated IP block in the Cortina Gemini SoC that + controls system power-down operations. + +properties: + compatible: + const: cortina,gemini-power-controller + + reg: + maxItems: 1 + + interrupts: + maxItems: 1 + +required: + - compatible + - reg + - interrupts + +additionalProperties: false + +examples: + - | + #include + + poweroff@4b000000 { + compatible = "cortina,gemini-power-controller"; + reg = <0x4b000000 0x100>; + interrupts = <26 IRQ_TYPE_EDGE_FALLING>; + }; +... diff --git a/Documentation/devicetree/bindings/power/reset/gemini-poweroff.txt b/Documentation/devicetree/bindings/power/reset/gemini-poweroff.txt deleted file mode 100644 index 7fec3e100214..000000000000 --- a/Documentation/devicetree/bindings/power/reset/gemini-poweroff.txt +++ /dev/null @@ -1,17 +0,0 @@ -* Device-Tree bindings for Cortina Systems Gemini Poweroff - -This is a special IP block in the Cortina Gemini SoC that only -deals with different ways to power the system down. - -Required properties: -- compatible: should be "cortina,gemini-power-controller" -- reg: should contain the physical memory base and size -- interrupts: should contain the power management interrupt - -Example: - -power-controller@4b000000 { - compatible = "cortina,gemini-power-controller"; - reg = <0x4b000000 0x100>; - interrupts = <26 IRQ_TYPE_EDGE_FALLING>; -}; From be353c6729d087925da702cf8c0ad3cb1ae53dec Mon Sep 17 00:00:00 2001 From: Andreas Kemnade Date: Wed, 1 Apr 2026 23:17:05 +0200 Subject: [PATCH 34/36] power: supply: bd71828: add input current limit property Add input current property to be able to work around issues created by automatic input limiting and have some control. Disabling the automatic management is another step. Signed-off-by: Andreas Kemnade Link: https://patch.msgid.link/20260401-bd-inp-limit-v1-1-689eb22531e2@kemnade.info Signed-off-by: Sebastian Reichel --- drivers/power/supply/bd71828-power.c | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/drivers/power/supply/bd71828-power.c b/drivers/power/supply/bd71828-power.c index 0e00acb58993..5e78faa0a4aa 100644 --- a/drivers/power/supply/bd71828-power.c +++ b/drivers/power/supply/bd71828-power.c @@ -24,6 +24,7 @@ #define BD7182x_MASK_CONF_PON BIT(0) #define BD71815_MASK_CONF_XSTB BIT(1) #define BD7182x_MASK_BAT_STAT 0x3f +#define BD7182x_MASK_ILIM 0x3f #define BD7182x_MASK_DCIN_STAT 0x07 #define BD7182x_MASK_WDT_AUTO 0x40 @@ -48,9 +49,11 @@ struct pwr_regs { unsigned int vbat_avg; unsigned int ibat; unsigned int ibat_avg; + unsigned int ilim_stat; unsigned int btemp_vth; unsigned int chg_state; unsigned int bat_temp; + unsigned int dcin_set; unsigned int dcin_stat; unsigned int dcin_online_mask; unsigned int dcin_collapse_limit; @@ -66,9 +69,11 @@ static const struct pwr_regs pwr_regs_bd71828 = { .vbat_avg = BD71828_REG_VBAT_U, .ibat = BD71828_REG_IBAT_U, .ibat_avg = BD71828_REG_IBAT_AVG_U, + .ilim_stat = BD71828_REG_ILIM_STAT, .btemp_vth = BD71828_REG_VM_BTMP_U, .chg_state = BD71828_REG_CHG_STATE, .bat_temp = BD71828_REG_BAT_TEMP, + .dcin_set = BD71828_REG_DCIN_SET, .dcin_stat = BD71828_REG_DCIN_STAT, .dcin_online_mask = BD7182x_MASK_DCIN_DET, .dcin_collapse_limit = BD71828_REG_DCIN_CLPS, @@ -441,6 +446,7 @@ static int bd71828_charger_get_property(struct power_supply *psy, struct bd71828_power *pwr = dev_get_drvdata(psy->dev.parent); u32 vot; u16 tmp; + int t; int online; int ret; @@ -459,6 +465,20 @@ static int bd71828_charger_get_property(struct power_supply *psy, vot = tmp; /* 5 milli volt steps */ val->intval = 5000 * vot; + break; + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + if (!pwr->regs->ilim_stat) + return -ENODATA; + + ret = regmap_read(pwr->regmap, pwr->regs->ilim_stat, &t); + if (ret) + return ret; + + t++; + val->intval = (t & BD7182x_MASK_ILIM) * 50000; + if (val->intval > 2000000) + val->intval = 2000000; + break; default: return -EINVAL; @@ -467,6 +487,45 @@ static int bd71828_charger_get_property(struct power_supply *psy, return 0; } +static int bd71828_charger_set_property(struct power_supply *psy, + enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct bd71828_power *pwr = dev_get_drvdata(psy->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + if (val->intval > 2000000) + return -EINVAL; + + if (val->intval < 50000) + return -EINVAL; + + if (!pwr->regs->dcin_set) + return -EINVAL; + + return regmap_update_bits(pwr->regmap, pwr->regs->dcin_set, + BD7182x_MASK_ILIM, + val->intval / 50000 - 1); + break; + default: + return -EINVAL; + } +} + +static int bd71828_charger_property_is_writeable(struct power_supply *psy, + enum power_supply_property psp) +{ + struct bd71828_power *pwr = dev_get_drvdata(psy->dev.parent); + + switch (psp) { + case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: + return !!(pwr->regs->dcin_set); + default: + return false; + } +} + static int bd71828_battery_get_property(struct power_supply *psy, enum power_supply_property psp, union power_supply_propval *val) @@ -571,6 +630,7 @@ static int bd71828_battery_property_is_writeable(struct power_supply *psy, /** @brief ac properties */ static const enum power_supply_property bd71828_charger_props[] = { + POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, POWER_SUPPLY_PROP_ONLINE, POWER_SUPPLY_PROP_VOLTAGE_NOW, }; @@ -600,6 +660,8 @@ static const struct power_supply_desc bd71828_ac_desc = { .properties = bd71828_charger_props, .num_properties = ARRAY_SIZE(bd71828_charger_props), .get_property = bd71828_charger_get_property, + .set_property = bd71828_charger_set_property, + .property_is_writeable = bd71828_charger_property_is_writeable, }; static const struct power_supply_desc bd71828_bat_desc = { From 0629c33fe1873a48e1e06078409de76c5a159fdb Mon Sep 17 00:00:00 2001 From: Bartosz Golaszewski Date: Mon, 16 Mar 2026 10:45:28 +0100 Subject: [PATCH 35/36] power: reset: drop unneeded dependencies on OF_GPIO OF_GPIO is selected automatically on all OF systems. Any symbols it controls also provide stubs so there's really no reason to select it explicitly. For Kconfig entries that have no other dependencies: convert it to requiring OF to avoid new symbols popping up for everyone in make config, for others just drop it altogether. Signed-off-by: Bartosz Golaszewski Link: https://patch.msgid.link/20260316-gpio-of-kconfig-v2-8-de2f4b00a0e4@oss.qualcomm.com Signed-off-by: Sebastian Reichel --- drivers/power/reset/Kconfig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/power/reset/Kconfig b/drivers/power/reset/Kconfig index f6c1bcbb57de..8af398b4e6f7 100644 --- a/drivers/power/reset/Kconfig +++ b/drivers/power/reset/Kconfig @@ -97,7 +97,7 @@ config POWER_RESET_GEMINI_POWEROFF config POWER_RESET_GPIO bool "GPIO power-off driver" - depends on OF_GPIO + depends on OF help This driver supports turning off your board via a GPIO line. If your board needs a GPIO high/low to power down, say Y and @@ -105,7 +105,7 @@ config POWER_RESET_GPIO config POWER_RESET_GPIO_RESTART bool "GPIO restart driver" - depends on OF_GPIO + depends on OF help This driver supports restarting your board via a GPIO line. If your board needs a GPIO high/low to restart, say Y and @@ -181,7 +181,7 @@ config POWER_RESET_PIIX4_POWEROFF config POWER_RESET_LTC2952 bool "LTC2952 PowerPath power-off driver" - depends on OF_GPIO + depends on OF help This driver supports an external powerdown trigger and board power down via the LTC2952. Bindings are made in the device tree. @@ -198,7 +198,7 @@ config POWER_RESET_MT6323 config POWER_RESET_QNAP bool "QNAP power-off driver" - depends on OF_GPIO && PLAT_ORION + depends on PLAT_ORION help This driver supports turning off QNAP NAS devices by sending commands to the microcontroller which controls the main power. From 98d68b74ebb9d5f145960ff7d96ce8e7a39fb965 Mon Sep 17 00:00:00 2001 From: Casey Connolly Date: Sun, 15 Mar 2026 20:40:16 +0100 Subject: [PATCH 36/36] power: supply: qcom_smbx: allow disabling charging Hook up USBIN_CMD_IL so that writing "0" to the status register will disable charging, this is useful to let users limit charging automatically. Signed-off-by: Casey Connolly Reviewed-by: Konrad Dybcio Signed-off-by: David Heidelberg Link: https://patch.msgid.link/20260315-smb2-cherry-pick-v1-1-b2710e470490@ixit.cz Signed-off-by: Sebastian Reichel --- drivers/power/supply/qcom_smbx.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/power/supply/qcom_smbx.c b/drivers/power/supply/qcom_smbx.c index b1cb925581ec..bf2e2ccc454a 100644 --- a/drivers/power/supply/qcom_smbx.c +++ b/drivers/power/supply/qcom_smbx.c @@ -134,6 +134,9 @@ #define OCP_CHARGER_BIT BIT(1) #define SDP_CHARGER_BIT BIT(0) +#define USBIN_CMD_IL 0x340 +#define USBIN_SUSPEND_BIT BIT(0) + #define TYPE_C_STATUS_1 0x30B #define UFP_TYPEC_MASK GENMASK(7, 5) #define UFP_TYPEC_RDSTD_BIT BIT(7) @@ -693,6 +696,9 @@ static int smb_set_property(struct power_supply *psy, struct smb_chip *chip = power_supply_get_drvdata(psy); switch (psp) { + case POWER_SUPPLY_PROP_STATUS: + return regmap_update_bits(chip->regmap, chip->base + USBIN_CMD_IL, + USBIN_SUSPEND_BIT, !val->intval); case POWER_SUPPLY_PROP_CURRENT_MAX: return smb_set_current_limit(chip, val->intval); default: @@ -705,6 +711,7 @@ static int smb_property_is_writable(struct power_supply *psy, enum power_supply_property psp) { switch (psp) { + case POWER_SUPPLY_PROP_STATUS: case POWER_SUPPLY_PROP_CURRENT_MAX: return 1; default: