8 KiB
HANDOFF — pmi8998 charge control (kernel + userspace)
Historical handoff. Current platform status belongs in
../../platform.md.
Written 2026-07-24. For a fresh model/reviewer picking this up cold.
What this is
Full read/write charge control for the Pixel 3 (blueline) charger, so the phone can report its charge stage and cap the charge for battery longevity — the capability LineageOS exposed downstream and mainline dropped.
Two layers, in two separate repos:
- Kernel driver —
drivers/power/supply/qcom_smbx.cin thelinux-bluelinetree. Adds charge_type reporting + charge-cap control. - Userspace —
rootfs-overlay/…in thePixel3Archrepo. A CLI, a udev rule, a persistence unit. Currently stale — see "Known gaps" #1.
Nothing is flashed. The phone is untouched. The driver builds clean but has not been packaged since the last fix, and has NOT run on hardware.
State of the tree — READ THIS FIRST
The gitea branch is behind local by two commits.
| commit | what | where |
|---|---|---|
f08dc70b3 |
original: charge_type + charge control | pushed to gitea blueline |
6b733d3ea |
fix pass after review #1 | LOCAL ONLY |
588929024 |
cell-aware current cap + probe guard | LOCAL ONLY |
gitea.wiuf.net/10.20…:4455 Fimeg/linux-blueline branch blueline tip is
f08dc70b369a — the flawed original. A reviewer who clones from gitea gets
code with the defects listed under "Review #1 found" still present. To review
the real thing, use the local tree at
~/Projects/Pixel3Arch/kernel/linux-blueline-current (branch mic-race-fix,
tip 588929024) or push the two commits first.
build.sh clones from gitea and hard-asserts the built package's hash matches
the branch tip, so a package build will not include the fixes until they are
pushed.
Files
Kernel (linux-blueline-current, branch mic-race-fix)
drivers/power/supply/qcom_smbx.c— the only changed file. +241 lines.smb_get_prop_charge_type— read-only, maps BATTERY_CHARGER_STATUS_1 bits 0:2 (trickle/pre/fast/fullon/taper/…) to POWER_SUPPLY_CHARGE_TYPE_*.smb_{get,set}_prop_voltage_max— CV target, FLOAT_VOLTAGE_CFG (0x70).smb_{get,set}_prop_constant_charge_current— FAST_CHARGE_CURRENT_CFG (0x61).smb_{get,set}_prop_charge_control_end— percentage form of voltage_max, on POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD.smb_fast_charge_current_max— 1C ceiling from charge_full_design_uah.smbx_ov_status— one-char fix,reg & mask→val & mask.- probe: design-limit guard + constant substitution.
Userspace (Pixel3Arch repo, committed at 8a35c45 / 194c422)
rootfs-overlay/usr/bin/blueline-charge— CLI. STALE.rootfs-overlay/etc/udev/rules.d/60-blueline-charge.rules— grants the writable attrs to group wheel. STALE.rootfs-overlay/etc/systemd/system/blueline-charge-limit.service— reapplies the cap on boot (registers are volatile).PAF/platform.md— "Charging" section documents the register map.
Property provenance (asked and answered)
Every property name is a pre-existing mainline power_supply core constant
(include/linux/power_supply.h), not invented here and not from any PR of
mine. charge_control_end_threshold has a documented ABI contract dated April
2019 (Documentation/ABI/testing/sysfs-class-power) — "battery percentage
level, above which charging will stop" — and is already implemented by seven
in-tree drivers, including the sibling qcom_battmgr.c on this same SoC family.
The one thing authored here is the charge_type register-state→enum mapping.
Review history
Three independent cold-start adversarial reviews were run (subagents, each told to refute safety). Summary of what each found:
Review #1 (against f08dc70b3) — four real defects, all since fixed in 6b733d3ea:
smbx_ov_statusmasked the register address not its value (reg & mask), so the overvoltage backstop returned 0 on both SMB2 and SMB5 and never fired — pre-existing, but the write path leaned on it.charge_control_limitis microamps by ABI; it was implemented as a percentage, inverting a thermal daemon's write. Moved tocharge_control_end_threshold(percent, ABI-correct).voltage_maxfloored at the register base (3.4875V), not the cell design minimum (3.6V).echo 3487500 > voltage_maxset CV below terminal voltage; with CHARGER_INHIBIT the charger stalls and the phone drains on a cable. Now floored at voltage_min_design_uv.- Current setter clamped against the DCP input limit (a different register and concept), so the getter returned 1.95A while the setter rejected it.
Review #2 (against 6b733d3ea) — confirmed the four fixes; flagged the current ceiling. Fixed in 588929024:
- The current cap was bounded by register width = 6.375A = 2.15C into a 2970mAh cell. Relying on "the adapter/AICL gate it" is the same external-backstop reasoning that hid the dead OV check. Now bounded at 1C (2.97A from charge_full_design_uah), register width as fallback.
- Probe didn't validate voltage design limits (latent, not blueline). Guarded.
Review #3 (against 588929024) — verdict FLASH for blueline. No overcharge (clamp-before-encode, encoder rounds down, 8-bit field can't truncate a legal value unsafe), no stall (3.6V floor on every path, registers volatile → reboot recovers), 1C cap confirmed, dispatch clean.
Known gaps — WHAT STILL NEEDS DOING
-
Userspace is stale.
blueline-chargeand60-blueline-charge.rulesstill referencecharge_control_limit, which the driver no longer exposes. On the fixed kernel the CLI fails its own[ -e "$PSY/charge_control_limit" ]guard. They must be updated tocharge_control_end_threshold. NOT done. -
Only
end_threshold, not thestart/endpair. The ABI definescharge_control_start_thresholdand_end_thresholdas a pair; some userspace (GNOME/UPower, some tlp configs) expects both. Onlyendis implemented.drivers/power/supply/cros_charge-control.cis the in-tree reference for doing both. -
The SMB2 overvoltage bit is unverified. No auditor could confirm from code alone that BIT(5) of BATTERY_CHARGER_STATUS_2 is the pmi8998 OV bit — there's no datasheet or downstream reference in-tree. It's pre-existing and read-only (can't overcharge/stall), but worth confirming against a datasheet.
-
Cosmetic: the max voltage write (4.4V → reg 122) reads back as 4.395V and 99% via the threshold getter, because the 7.5mV encoding step doesn't divide the range evenly. Errs low (safe). Not fixed.
-
Not packaged, not flashed. Single-object compiles pass under clang/LLD/ CFI with W=1. Full
build.shnot re-run since the fixes (and can't include them until pushed — see "State of the tree"). No hardware test.
What is NOT verified without hardware
- Whether lowering the float voltage actually stops the charge on the pmi8998, versus the fuel gauge continuing to report rising capacity.
- Whether charge_type reports sane transitions (Fast under load → Standard as it tapers near full).
Build & deploy (per PAF/build.md)
- Commit + push to gitea (uncommitted/unpushed work does not build).
ssh casey@10.10.20.123 'cd ~/Projects/Pixel3Arch/kernel && ./build.sh blueline'— self-locks with flock, invoke directly; asserts pkg hash == gitea tip.- Deploy: modules first, then flash boot_a, then refuse to bless unless
uname -rmatches the installed tree (7.1.1-sdm845-g<hash>). - Rollback:
staging/deploy-139aa/boot-139aa7748181-pmoschain.img. Slot B (LineageOS) is untouched and is the ultimate fallback.
Single-object compile check (fast, no full build)
scp qcom_smbx.c casey@10.10.20.123:/tmp/
ssh casey@10.10.20.123 'cd ~/Projects/Pixel3Arch/kernel/src/linux-blueline-src && \
cp /tmp/qcom_smbx.c drivers/power/supply/ && \
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- W=1 \
drivers/power/supply/qcom_smbx.o'
# restore the tree afterward — this is makepkg's throwaway clone