Merge tag 'for-v7.1' of git://git.kernel.org/pub/scm/linux/kernel/git/sre/linux-power-supply

Pull power supply and reset updates from Sebastian Reichel:
 "Power-supply drivers:
   - S2MU005: new battery fuel gauge driver
   - macsmc-power: new driver for Apple Silicon
   - qcom_battmgr: Add support for Glymur and Kaanapali
   - max17042: add support for max77759
   - qcom_smbx: allow disabling charging
   - bd71828: add input current limit support
   - multiple drivers: use new device managed workqueue allocation
     function
   - misc small cleanups and fixes

  Reset core:
   - Expose sysfs for registered reboot_modes

  Reset drivers
   - misc small cleanups and fixes"

* tag 'for-v7.1' of git://git.kernel.org/pub/scm/linux/kernel/git/sre/linux-power-supply: (36 commits)
  power: supply: qcom_smbx: allow disabling charging
  power: reset: drop unneeded dependencies on OF_GPIO
  power: supply: bd71828: add input current limit property
  dt-bindings: power: reset: cortina,gemini-power-controller: convert to DT schema
  power: supply: add support for S2MU005 battery fuel gauge device
  dt-bindings: power: supply: document Samsung S2MU005 battery fuel gauge
  power: reset: reboot-mode: fix -Wformat-security warning
  power: supply: ipaq_micro: Simplify with devm
  power: supply: mt6370: Simplify with devm_alloc_ordered_workqueue()
  power: supply: max77705: Free allocated workqueue and fix removal order
  power: supply: max77705: Drop duplicated IRQ error message
  power: supply: cw2015: Free allocated workqueue
  power: reset: keystone: Use register_sys_off_handler(SYS_OFF_MODE_RESTART)
  power: supply: twl4030_madc: Drop unused header includes
  power: supply: bq24190: Avoid rescheduling after cancelling work
  power: supply: axp288_charger: Simplify returns of dev_err_probe()
  power: supply: axp288_charger: Do not cancel work before initializing it
  power: supply: cpcap-battery: pass static battery cell data from device tree
  dt-bindings: power: supply: cpcap-battery: document monitored-battery property
  power: supply: qcom_battmgr: Add support for Glymur and Kaanapali
  ...
This commit is contained in:
Linus Torvalds
2026-04-17 11:41:33 -07:00
28 changed files with 1785 additions and 181 deletions

View File

@@ -0,0 +1,36 @@
What: /sys/class/reboot-mode/<driver>/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-<argument> 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.

View File

@@ -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 <linusw@kernel.org>
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 <dt-bindings/interrupt-controller/irq.h>
poweroff@4b000000 {
compatible = "cortina,gemini-power-controller";
reg = <0x4b000000 0x100>;
interrupts = <26 IRQ_TYPE_EDGE_FALLING>;
};
...

View File

@@ -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>;
};

View File

@@ -55,6 +55,7 @@ properties:
- const: chg_isense
- const: batti
monitored-battery: true
power-supplies: true
required:

View File

@@ -20,6 +20,7 @@ properties:
- maxim,max17050
- maxim,max17055
- maxim,max77705-battery
- maxim,max77759-fg
- maxim,max77849-battery
reg:
@@ -27,36 +28,42 @@ properties:
interrupts:
maxItems: 1
description: |
The ALRT pin, an open-drain interrupt.
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: |
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.

View File

@@ -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 <kauschluss@disroot.org>
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 <dt-bindings/interrupt-controller/irq.h>
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>;
};
};

View File

@@ -2547,6 +2547,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/*

View File

@@ -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.

View File

@@ -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);

View File

@@ -4,12 +4,16 @@
*/
#include <linux/device.h>
#include <linux/err.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/reboot.h>
#include <linux/reboot-mode.h>
#include <linux/slab.h>
#include <linux/string.h>
#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,52 @@ 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, "%s",
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 +211,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 +263,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 +331,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 <andy.yan@rock-chips.com>");
MODULE_DESCRIPTION("System reboot mode core library");
MODULE_LICENSE("GPL v2");

View File

@@ -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
@@ -1132,4 +1143,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

View File

@@ -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
@@ -128,3 +129,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

View File

@@ -10,6 +10,7 @@
#include <linux/acpi.h>
#include <linux/bitops.h>
#include <linux/module.h>
#include <linux/devm-helpers.h>
#include <linux/device.h>
#include <linux/regmap.h>
#include <linux/workqueue.h>
@@ -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;
@@ -866,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
@@ -885,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");
}
@@ -904,38 +894,39 @@ 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_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);
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,
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,
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);
}
@@ -954,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;

View File

@@ -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 = {

View File

@@ -9,6 +9,7 @@
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/devm-helpers.h>
#include <linux/pm_runtime.h>
#include <linux/power_supply.h>
#include <linux/power/bq24190_charger.h>
@@ -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);

View File

@@ -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,
@@ -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;
@@ -429,8 +453,11 @@ 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;
}
if (ddata->psy)
cpcap_battery_update_battery_data(ddata);
}
/**

View File

@@ -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;

View File

@@ -7,6 +7,7 @@
* Author : Linus Walleij <linus.walleij@linaro.org>
*/
#include <linux/devm-helpers.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/platform_device.h>
@@ -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,
&micro_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,
&micro_batt_power_desc,
NULL);
if (IS_ERR(micro_batt_power))
return PTR_ERR(micro_batt_power);
micro_ac_power = power_supply_register(&pdev->dev,
&micro_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,
&micro_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 = &micro_batt_dev_pm_ops,
},
.probe = micro_batt_probe,
.remove = micro_batt_remove,
};
module_platform_driver(micro_batt_device_driver);

View File

@@ -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 <linux/ctype.h>
#include <linux/delay.h>
#include <linux/devm-helpers.h>
#include <linux/limits.h>
#include <linux/module.h>
#include <linux/mfd/macsmc.h>
#include <linux/notifier.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/power_supply.h>
#include <linux/reboot.h>
#include <linux/workqueue.h>
#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 <marcan@marcan.st>");
MODULE_AUTHOR("Michael Reeves <michael.reeves077@gmail.com>");

View File

@@ -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[] = {
@@ -88,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,
@@ -131,7 +133,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.
*/
@@ -201,7 +203,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;
}
@@ -331,6 +333,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 +344,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 +355,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 +366,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:
@@ -430,6 +440,25 @@ 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;
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:
@@ -646,7 +675,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);
}
@@ -783,7 +813,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);
@@ -792,7 +823,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);
}
}
@@ -921,8 +953,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;
}
@@ -1011,6 +1047,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,
@@ -1037,6 +1112,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;
@@ -1052,17 +1128,20 @@ 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 (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");
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;
@@ -1088,12 +1167,22 @@ 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)) {
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;
@@ -1236,6 +1325,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 },
{ },
@@ -1248,6 +1339,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 },
{ }
};

View File

@@ -646,51 +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) {
dev_err_probe(dev, ret, "Failed to Request chgin IRQ\n");
goto destroy_wq;
}
if (ret)
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) {
dev_err_probe(dev, ret, "Failed to Request aicl IRQ\n");
goto destroy_wq;
}
if (ret)
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[] = {

View File

@@ -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");

View File

@@ -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 },

View File

@@ -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:

View File

@@ -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 <y.oudjana@protonmail.com>
* Copyright (C) 2025 Kaustabh Chakraborty <kauschluss@disroot.org>
*/
#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
#include <linux/mod_devicetable.h>
#include <linux/mutex.h>
#include <linux/power_supply.h>
#include <linux/property.h>
#include <linux/regmap.h>
#include <linux/units.h>
#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, &current_now);
if (ret < 0)
return ret;
ret = s2mu005_fg_get_current_avg(priv, &current_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 <y.oudjana@protonmail.com>");
MODULE_AUTHOR("Kaustabh Chakraborty <kauschluss@disroot.org>");
MODULE_LICENSE("GPL");

View File

@@ -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));
}
/*

View File

@@ -11,9 +11,7 @@
*/
#include <linux/module.h>
#include <linux/param.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
#include <linux/platform_device.h>
#include <linux/power_supply.h>
#include <linux/slab.h>

View File

@@ -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
@@ -105,7 +106,7 @@ enum max17042_register {
MAX17042_OCV = 0xEE,
MAX17042_OCVInternal = 0xFB, /* MAX17055 VFOCV */
MAX17042_OCVInternal = 0xFB, /* MAX17055/77759 VFOCV */
MAX17042_VFSOC = 0xFF,
};
@@ -156,7 +157,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 +168,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
};