ACPI: battery: _BIX data (last full charge capacity) stays stale forever on firmware that never sends Notify 0x81
Stefan Reisinger <[email protected]>
| Newsgroups | org.kernel.vger.linux-acpi |
|---|---|
| Message-ID | <[email protected]> |
Hi,
on machines whose EC never issues Notify 0x81 (Battery Information Changed),
the values the ACPI battery driver exposes from _BIX -- most importantly last
full charge capacity (energy_full/charge_full) and cycle count -- remain stale
indefinitely. On systems that run continuously and are never suspended or
rebooted (for example a laptop repurposed as a small always-on server), the
reported battery health never updates, potentially for years.
Observed on: Lenovo ThinkPad T460 (20FMS1VA09), BIOS R06ET71W (1.45), EC
firmware 1.12, kernel 7.1.9-arch1-2 (Arch Linux). BIOS 1.45 is the current (and
presumably final) release for this model, so this is reproduced against the
latest available firmware. I can reproduce and test patches on this hardware.
Observation
The battery's fuel gauge (SANYO pack, SBS over EC) commits a new
FullChargeCapacity after an undisturbed full-charge termination. Directly
observable: after a full charge, energy_now settled at 17750000 uWh while
sysfs energy_full still reported 17830000 uWh (the pre-learning value). The
new FCC of 17750000 only became visible after a reboot. Replugging AC does not
help (EC sends Notify 0x80 only). The threshold-based charge stop confirms the
gauge itself uses the new value immediately, so the stale data is purely on
the reporting path.
The impact is not limited to a stale health figure. The driver derives
POWER_SUPPLY_STATUS by comparing fresh _BST data against cached _BIX data:
acpi_battery_is_full() tests full_charge_capacity == capacity_now, i.e. it
compares values sampled at two different points in time. With a stale cache the
comparison can never succeed, so the driver reports "Not charging" indefinitely
for a battery that is physically full -- while the firmware-side data is
perfectly self-consistent (a fresh _BIX evaluation yields energy_full ==
energy_now with rate 0). I could observe this directly: with identical
physical state, sysfs reported "Not charging" (energy_now 17750000 vs cached
energy_full 17830000) before a reboot and "Full" (17750000 == 17750000) after
it. Userspace should be able to treat POWER_SUPPLY_STATUS as authoritative;
anything that does and waits for "Full" (charging indicators, calibration
tooling) never completes on such systems.
Firmware analysis (DSDT of the T460)
BAT0 (internal): a query method containing Notify 0x81 exists (Method _Q4A:
CLPM() + Notify(BAT0, 0x81)), but the EC never raises this query when the
gauge commits a new FCC. Manually evaluating _Q4A (e.g. via acpi_call)
triggers the expected refresh, confirming the kernel side works when notified.
BAT1 (removable): the DSDT contains no Notify(BAT1, 0x81) at all; the only
notifications for BAT1 are 0x80 (status) in the status/hotplug queries.
Firmware relies on physical reinsertion for info refresh.
Both contradict the platform requirements Microsoft documents for battery
firmware ("the platform firmware must generate ... a Notify(0x81) ... whenever
any of the battery state data in _BIX changes. This includes last full charge
capacity, design capacity, and cycle count.") [1].
I have verified this on one platform only. I suspect it is not unique to it --
under Windows the frequent suspend/resume cycles of modern usage would mask
such a firmware omission entirely, and Linux likewise refreshes on resume
(PM_POST_SUSPEND in battery_notify()), so it would only ever surface on
machines that never suspend. But I want to be explicit that the cross-vendor
part is a suspicion, not something I have data for. For what it is worth, user
reports of the same symptom and folklore workaround exist for other ThinkPad
models -- reported capacity exceeding 100% after a recalibration (remaining
energy running past the stale energy_full), fixed by a reboot or by physically
reinserting the battery: https://www.reddit.com/r/thinkpad/comments/tqc4cw/
tlp_recalibration_110_charge/
Current kernel behavior
drivers/acpi/battery.c evaluates _BIX only at probe, on Notify 0x81
(acpi_battery_refresh()), and after suspend/hibernate (PM_POST_SUSPEND handler
/ acpi_battery_resume()). There is no other path that ever re-reads _BIX. That
is spec-conformant, but combined with the firmware behavior above it produces
permanently stale health data.
Possible directions
I would be happy to work on a patch, but since I have not worked on the ACPI
subsystem before, I cannot judge the trade-offs well and would appreciate a
pointer on which direction (if any) would be acceptable before writing code:
(a) Re-read battery info on charge-state transitions that plausibly coincide
with a gauge commit: "charging" -> "neither charging nor discharging while on
AC" (charge completed -- the moment fuel gauges typically commit a newly
learned FCC, and the moment [1] recommends firmware to update it), and possibly
also "discharging" -> "charging"/"not charging" (the empty point, where gauges
resynchronize as well). Would need a rate limit (e.g. once per 60 s at most),
since ECs oscillate between charging and not-charging near the stop threshold
(top-off cycles). I am aware of the recent work on merging consecutive battery
notifications to reduce pressure on the ACPI methods and the EC (acpi_notif_fifo
/ NOTIF_MERGING_MS) -- though that effort targeted call rates of several per
second, while transition-triggered _BIX reads would happen at most every few
minutes, several orders of magnitude below that, so I would not expect it to
add meaningful load back.
(b) A maximum cache age for the _BIX data, checked lazily: when a _BST update
runs anyway (i.e. something is reading the sysfs values) and the cached
battery info is older than some limit, re-read it along the way. No timers, no
wakeups, zero cost on an idle system; additionally covers commits that happen
without any observable charge-state transition. Downside: up to the cache age
of latency instead of seconds. I have no good intuition for the limit -- tens
of minutes? hours? -- you probably have a much better sense of how often well-
behaved firmware refreshes this data, so I would defer to your judgment there.
(c) A DMI-gated quirk (bat_dmi_table, as done for the notification-delay quirk)
limiting either behavior to known affected machines.
(d) An explicit userspace-triggered refresh (similar in spirit to the SCSI/PCI
rescan attributes), leaving the default behavior untouched.
My current workaround is invoking the DSDT's EC query method that contains the
Notify (via acpi_call), which works but is obviously not a general solution.
The driver already carries several quirks compensating for non-conformant
battery firmware (percentage capacity, ThinkPad mAh units, degraded full
charge, notification delay), so I hope this fits the established pattern. If
this has been discussed before, pointers welcome -- I did not find prior
threads.
[1] https://learn.microsoft.com/en-us/windows-hardware/design/component-guidelines/acpi-battery-and-power-subsystem-firmware-implementation
This is my first report to this list and my first contact with kernel
development -- apologies in advance if I got parts of the process wrong; happy
to redo this via bugzilla or in any other form if preferred.
Thanks, Stefan