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