From 4a372b50f71b67563b0abb3ee09399d9a1c6d7f4 Mon Sep 17 00:00:00 2001 From: Peter Marshall Date: Tue, 7 Jul 2026 15:12:38 -0400 Subject: [PATCH 1/2] power: supply: surface_battery: Report battery authentication status Authentic batteries in Surface devices are verified and paired with the motherboard during manufacturing or repair for quality assurance. Read the authentication status from the SAM and report it. --- drivers/power/supply/surface_battery.c | 44 ++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/drivers/power/supply/surface_battery.c b/drivers/power/supply/surface_battery.c index c759add4df49d..56d816d5c9a2b 100644 --- a/drivers/power/supply/surface_battery.c +++ b/drivers/power/supply/surface_battery.c @@ -45,6 +45,12 @@ enum sam_battery_power_unit { SAM_BATTERY_POWER_UNIT_mA = 1, }; +enum sam_battery_auth_status { + SAM_BATTERY_AUTH_STATUS_GENUINE = 0, + SAM_BATTERY_AUTH_STATUS_UNAUTHORIZED = 1, + SAM_BATTERY_AUTH_STATUS_ERROR = 2, +}; + /* Equivalent to data returned in ACPI _BIX method, revision 0. */ struct spwr_bix { u8 revision; @@ -108,6 +114,12 @@ SSAM_DEFINE_SYNC_REQUEST_CL_W(ssam_bat_set_btp, __le32, { .command_id = 0x04, }); +/* Get battery authentication status */ +SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_auth, __le32, { + .target_category = SSAM_SSH_TC_SAM, + .command_id = 0x1b, +}); + /* -- Device structures. ---------------------------------------------------- */ @@ -134,6 +146,8 @@ struct spwr_battery_device { struct spwr_bix bix; struct spwr_bst bst; u32 alarm; + + bool authentic; }; @@ -186,6 +200,25 @@ static int spwr_battery_load_bix(struct spwr_battery_device *bat) return status; } +static int spwr_battery_load_auth(struct spwr_battery_device *bat) +{ + __le32 auth; + int status; + + lockdep_assert_held(&bat->lock); + + if (!spwr_battery_present(bat)) + return 0; + + status = ssam_retry(ssam_bat_get_auth, bat->sdev, &auth); + if (status) + return status; + + bat->authentic = le32_to_cpu(auth) == SAM_BATTERY_AUTH_STATUS_GENUINE; + + return 0; +} + static int spwr_battery_load_bst(struct spwr_battery_device *bat) { lockdep_assert_held(&bat->lock); @@ -253,6 +286,10 @@ static int spwr_battery_update_bix_unlocked(struct spwr_battery_device *bat) if (status) return status; + status = spwr_battery_load_auth(bat); + if (status) + return status; + status = spwr_battery_load_bst(bat); if (status) return status; @@ -441,6 +478,7 @@ static const enum power_supply_property spwr_battery_props_chg[] = { POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_MANUFACTURER, POWER_SUPPLY_PROP_SERIAL_NUMBER, + POWER_SUPPLY_PROP_AUTHENTIC, }; static const enum power_supply_property spwr_battery_props_eng[] = { @@ -459,6 +497,7 @@ static const enum power_supply_property spwr_battery_props_eng[] = { POWER_SUPPLY_PROP_MODEL_NAME, POWER_SUPPLY_PROP_MANUFACTURER, POWER_SUPPLY_PROP_SERIAL_NUMBER, + POWER_SUPPLY_PROP_AUTHENTIC, }; static int spwr_battery_prop_status(struct spwr_battery_device *bat) @@ -652,6 +691,11 @@ static int spwr_battery_get_property(struct power_supply *psy, enum power_supply val->strval = bat->bix.serial; break; + case POWER_SUPPLY_PROP_AUTHENTIC: + val->intval = bat->authentic; + break; + + default: status = -EINVAL; break; From 7aa02245f2c0ceeb17ae200421ce39857c290f51 Mon Sep 17 00:00:00 2001 From: Peter Marshall Date: Tue, 7 Jul 2026 15:15:38 -0400 Subject: [PATCH 2/2] power: supply: surface_battery: Support smart charging Newer Surface devices limit the battery charge to 80% based on usage patterns. Report the charge limit status and disengage it if requested. The charge limit is always controlled by the SAM and will be re-enabled after a few days depending on use. --- drivers/power/supply/surface_battery.c | 181 ++++++++++++++++++++++++- 1 file changed, 177 insertions(+), 4 deletions(-) diff --git a/drivers/power/supply/surface_battery.c b/drivers/power/supply/surface_battery.c index 56d816d5c9a2b..c0fc58e1061b1 100644 --- a/drivers/power/supply/surface_battery.c +++ b/drivers/power/supply/surface_battery.c @@ -51,6 +51,21 @@ enum sam_battery_auth_status { SAM_BATTERY_AUTH_STATUS_ERROR = 2, }; +enum sam_battery_protection_policy { + SAM_BATTERY_PROTECTION_POLICY_CHARGE_LIMIT = BIT(4), +}; + +enum sam_battery_charge_limit_status { + SAM_BATTERY_CHARGE_LIMIT_DISABLED = 0x00, + SAM_BATTERY_CHARGE_LIMIT_ENFORCED = 0x01, +}; + +enum sam_battery_charge_limit_disengage_result { + SAM_BATTERY_CHARGE_LIMIT_DISENGAGE_OK = 0, + SAM_BATTERY_CHARGE_LIMIT_DISENGAGE_COUNTDOWN = 1, + SAM_BATTERY_CHARGE_LIMIT_DISENGAGE_NOT_ENGAGED = 2, +}; + /* Equivalent to data returned in ACPI _BIX method, revision 0. */ struct spwr_bix { u8 revision; @@ -120,6 +135,24 @@ SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_auth, __le32, { .command_id = 0x1b, }); +/* Get battery protection policy */ +SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_protection_policy, __u8, { + .target_category = SSAM_SSH_TC_SAM, + .command_id = 0x3a, +}); + +/* Get smart charging status */ +SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_get_charge_limit_status, __le16, { + .target_category = SSAM_SSH_TC_SAM, + .command_id = 0x41, +}); + +/* Disengage smart charging */ +SSAM_DEFINE_SYNC_REQUEST_CL_R(ssam_bat_force_charge_limit_disengage, __le32, { + .target_category = SSAM_SSH_TC_SAM, + .command_id = 0x43, +}); + /* -- Device structures. ---------------------------------------------------- */ @@ -147,6 +180,9 @@ struct spwr_battery_device { struct spwr_bst bst; u32 alarm; + bool supports_smart_charging; + bool charge_limit_enforced; + bool authentic; }; @@ -229,6 +265,55 @@ static int spwr_battery_load_bst(struct spwr_battery_device *bat) return ssam_retry(ssam_bat_get_bst, bat->sdev, &bat->bst); } +static int spwr_battery_load_protection_policy(struct spwr_battery_device *bat) +{ + __u8 bpp; + int status; + + lockdep_assert_held(&bat->lock); + + status = ssam_retry(ssam_bat_get_protection_policy, bat->sdev, &bpp); + if (status) + return status; + + bat->supports_smart_charging = (le32_to_cpu(bpp) & SAM_BATTERY_PROTECTION_POLICY_CHARGE_LIMIT) == SAM_BATTERY_PROTECTION_POLICY_CHARGE_LIMIT; + + return 0; +} + +static int spwr_battery_load_charge_limit_status(struct spwr_battery_device *bat) +{ + __le16 bcl; + int status; + + lockdep_assert_held(&bat->lock); + + status = ssam_retry(ssam_bat_get_charge_limit_status, bat->sdev, &bcl); + if (status) + return status; + + bat->charge_limit_enforced = le32_to_cpu(bcl) == SAM_BATTERY_CHARGE_LIMIT_ENFORCED; + + return 0; +} + +static int spwr_battery_force_charge_limit_disengage(struct spwr_battery_device *bat) +{ + __le32 sta; + int status; + + lockdep_assert_held(&bat->lock); + + status = ssam_retry(ssam_bat_force_charge_limit_disengage, bat->sdev, &sta); + if (status) + return status; + + if (le32_to_cpu(sta) != SAM_BATTERY_CHARGE_LIMIT_DISENGAGE_OK) + dev_warn(&bat->sdev->dev, "failed to disengage charge limit\n"); + + return 0; +} + static int spwr_battery_set_alarm_unlocked(struct spwr_battery_device *bat, u32 value) { __le32 value_le = cpu_to_le32(value); @@ -257,6 +342,10 @@ static int spwr_battery_update_bst_unlocked(struct spwr_battery_device *bat, boo if (status) return status; + status = spwr_battery_load_charge_limit_status(bat); + if (status) + return status; + bat->timestamp = jiffies; return 0; } @@ -500,6 +589,10 @@ static const enum power_supply_property spwr_battery_props_eng[] = { POWER_SUPPLY_PROP_AUTHENTIC, }; +static const enum power_supply_property spwr_smart_charge_props[] = { + POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD, +}; + static int spwr_battery_prop_status(struct spwr_battery_device *bat) { u32 state = get_unaligned_le32(&bat->bst.state); @@ -691,6 +784,10 @@ static int spwr_battery_get_property(struct power_supply *psy, enum power_supply val->strval = bat->bix.serial; break; + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + val->intval = bat->charge_limit_enforced ? 80 : 100; + break; + case POWER_SUPPLY_PROP_AUTHENTIC: val->intval = bat->authentic; break; @@ -706,6 +803,51 @@ static int spwr_battery_get_property(struct power_supply *psy, enum power_supply return status; } +static int spwr_battery_set_property(struct power_supply *psy, enum power_supply_property psp, + const union power_supply_propval *val) +{ + struct spwr_battery_device *bat = power_supply_get_drvdata(psy); + int status; + + mutex_lock(&bat->lock); + + status = spwr_battery_update_bst_unlocked(bat, true); + if (status) + goto out; + + /* Abort if battery is not present. */ + if (!spwr_battery_present(bat) && psp != POWER_SUPPLY_PROP_PRESENT) { + status = -ENODEV; + goto out; + } + + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + if (val->intval >= 100 && bat->charge_limit_enforced) + spwr_battery_force_charge_limit_disengage(bat); + break; + + + default: + status = -EINVAL; + break; + } + +out: + mutex_unlock(&bat->lock); + return status; +} + +static int spwr_battery_property_is_writeable(struct power_supply *psy, enum power_supply_property psp) +{ + switch (psp) { + case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: + return true; + default: + return false; + } +} + /* -- Alarm attribute. ------------------------------------------------------ */ @@ -781,6 +923,8 @@ static void spwr_battery_init(struct spwr_battery_device *bat, struct ssam_devic bat->psy_desc.name = bat->name; bat->psy_desc.type = POWER_SUPPLY_TYPE_BATTERY; bat->psy_desc.get_property = spwr_battery_get_property; + bat->psy_desc.set_property = spwr_battery_set_property; + bat->psy_desc.property_is_writeable = spwr_battery_property_is_writeable; INIT_DELAYED_WORK(&bat->update_work, spwr_battery_update_bst_workfn); } @@ -790,6 +934,10 @@ static int spwr_battery_register(struct spwr_battery_device *bat) struct power_supply_config psy_cfg = {}; __le32 sta; int status; + const enum power_supply_property *base_properties;; + enum power_supply_property *properties; + int base_num_properties; + int num_properties; /* Make sure the device is there and functioning properly. */ status = ssam_retry(ssam_bat_get_sta, bat->sdev, &sta); @@ -808,6 +956,12 @@ static int spwr_battery_register(struct spwr_battery_device *bat) return status; } + status = spwr_battery_load_protection_policy(bat); + if (status) { + mutex_unlock(&bat->lock); + return status; + } + if (spwr_battery_present(bat)) { u32 cap_warn = get_unaligned_le32(&bat->bix.design_cap_warn); @@ -824,13 +978,13 @@ static int spwr_battery_register(struct spwr_battery_device *bat) switch (get_unaligned_le32(&bat->bix.power_unit)) { case SAM_BATTERY_POWER_UNIT_mW: - bat->psy_desc.properties = spwr_battery_props_eng; - bat->psy_desc.num_properties = ARRAY_SIZE(spwr_battery_props_eng); + base_properties = spwr_battery_props_eng; + base_num_properties = ARRAY_SIZE(spwr_battery_props_eng); break; case SAM_BATTERY_POWER_UNIT_mA: - bat->psy_desc.properties = spwr_battery_props_chg; - bat->psy_desc.num_properties = ARRAY_SIZE(spwr_battery_props_chg); + base_properties = spwr_battery_props_chg; + base_num_properties = ARRAY_SIZE(spwr_battery_props_chg); break; default: @@ -839,6 +993,25 @@ static int spwr_battery_register(struct spwr_battery_device *bat) return -EINVAL; } + num_properties = base_num_properties + ARRAY_SIZE(spwr_smart_charge_props); + + properties = devm_kcalloc(&bat->sdev->dev, + num_properties, + sizeof(*properties), + GFP_KERNEL); + + if (!properties) + return -ENOMEM; + + memcpy(properties, base_properties, base_num_properties * sizeof(*base_properties)); + + if (bat->supports_smart_charging) { + memcpy(properties + base_num_properties, spwr_smart_charge_props, sizeof(spwr_smart_charge_props)); + } + + bat->psy_desc.properties = properties; + bat->psy_desc.num_properties = num_properties; + psy_cfg.drv_data = bat; psy_cfg.attr_grp = spwr_battery_groups;