Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/saf/device/evidence/power/HANDOFF-upower-charge-control.md
Fimeg bde961c6f2 saf: one spine — device, state, and work under the index
PAF becomes saf/device (history kept), STATE.md dissolves
into saf/state.md with the dated era archived, the substrate
SAF moves up from souveraine, and every agreement points at
saf/INDEX.md and nowhere else. one map, nothing to remember
2026-08-18 09:47:30 -04:00

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 driverdrivers/power/supply/qcom_smbx.c in the linux-blueline tree. Adds charge_type reporting + charge-cap control.
  • Userspacerootfs-overlay/… in the Pixel3Arch repo. 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 & maskval & 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:

  1. smbx_ov_status masked 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.
  2. charge_control_limit is microamps by ABI; it was implemented as a percentage, inverting a thermal daemon's write. Moved to charge_control_end_threshold (percent, ABI-correct).
  3. voltage_max floored at the register base (3.4875V), not the cell design minimum (3.6V). echo 3487500 > voltage_max set CV below terminal voltage; with CHARGER_INHIBIT the charger stalls and the phone drains on a cable. Now floored at voltage_min_design_uv.
  4. 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

  1. Userspace is stale. blueline-charge and 60-blueline-charge.rules still reference charge_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 to charge_control_end_threshold. NOT done.

  2. Only end_threshold, not the start/end pair. The ABI defines charge_control_start_threshold and _end_threshold as a pair; some userspace (GNOME/UPower, some tlp configs) expects both. Only end is implemented. drivers/power/supply/cros_charge-control.c is the in-tree reference for doing both.

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

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

  5. Not packaged, not flashed. Single-object compiles pass under clang/LLD/ CFI with W=1. Full build.sh not 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 -r matches 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