# B10A-DRIVER-AUDIT — charge termination and recharge: stock vs upstream (code-only, read-only) Date: 2026-10-03. Board untouched (no PMIC writes, no build). Sources: - downstream: android.googlesource kernel/msm @7f1748ce `drivers/power/qpnp-linear-charger.c`, `qpnp-vm-bms.c` (copies: `b10/b10a/lbc-7f1748.c`, `vmbms-7f1748.c`). The stock Aurora kernel (3.10.28, `partitions/boot.bin`) is a slightly different revision: its strings match this LBC source ("Failed to override VBAT_DET", "vbatdet-lo triggered", "Failed to disable EOC comp", "resume-soc") and its BMS additionally has "soc dropped below resume_soc soc=%d resume_soc=%d, restart charging". Same design. - upstream: Linux 7.2.7 `drivers/power/supply/pm8916_lbc.c`, `pm8916_bms_vm.c`, `arch/arm64/boot/dts/qcom/pm8916.dtsi`, reference board `msm8916-longcheer-l8150.dts`. - live registers: `b10/b10a/b10a-live1.txt`, `b10a-live2.txt` (B10A). ## Register names (downstream defines, offsets from CHGR 0x1000 / BAT_IF 0x1200 / USB 0x1300 / MISC 0x1600) | Reg | Name | Live (SBL1 state) | Meaning | |---|---|---|---| | 0x1009 | CHG_STATUS, bit1 = VDD_LOOP (CV) | 05 / 03 / 07 / 00 | 03 = CV loop, 05 = other loop (CC/input), 00 = not charging (bits 0/2 not named in source) | | 0x1010 | CHGR INT_RT_STS (bit5 FAST_CHG_ON) | 01 | fast_chg RT was never seen set | | 0x1040/41 | VDD_MAX / VDD_SAFE | 08 / 08 | 4.200 V | | 0x1044/45 | IBAT_MAX / IBAT_SAFE (90 mA steps) | 00 / 0a | 90 mA / 990 mA | | 0x1047 | VIN_MIN (4200 + 27 mV·n) | 04 | 4308 mV = stock `vinmin-mv` (B10A table had this wrong: 0x1043 is not VIN_MIN) | | 0x1049 | CHG_CTRL: bit7 CHG_ENABLE, bit0 CHG_FORCE_BATT_ON | 90 | enabled; bit4 unknown | | 0x104A | CHG_FAILED (bit7) | 00 | | | 0x1052 | VBAT_WEAK | 0b | | | **0x105B** | **CHG_IBATTERM_EN, bit3 = HW end-of-charge (iterm) comparator** | **09 → bit3 = 1** | **HW termination ENABLED** | | 0x1060/61 | TCHG_MAX_EN / TCHG_MAX (4 min steps) | 80 / 1d | timer on, (0x1d+1)·4 = 120 min | | 0x1065 | CHG_WDOG_EN | 00 | off | | 0x10DA | PERPH_RESET_CTRL3 ("follow PMIC reset") | 0b | | | **0x10EE** | **CHG_COMP_OVR1, bits1:0 = VBAT_DET override** | **00** | **no override → VBAT_DET comparator active** | | 0x1309 | USB_PTH_STS (bits7:6 = USB_IN_VALID) | 90 | valid | | 0x1308 | (not in the downstream source) | 02 charging / 01 after EOC | empirical | | 0x1642 | MISC BOOT_DONE (bit7) | 00 | **never set** (downstream sets it at probe) | | 0x16F3/F4 | MISC TRIM3/TRIM4 (VDD trim) | 58 / 49 | | ## A. Stock (downstream) recharge logic Termination is a **software decision**, recharge is **software-triggered**: 1. Probe (`qpnp_lbc_probe`): misc_init (reads VDD trim, **writes BOOT_DONE**), chg_init (VBAT_WEAK, VIN_MIN, VDD_SAFE, VDD_MAX + VDD trim, IBAT_SAFE, TCHG_MAX if `tchg-mins`, **VBAT_DET override = 0 (CHG_COMP_OVR1)** "charge irrespective of VBAT above VBAT_DET", **HW iterm comparator OFF** (0x105B bit3 = 0) unless `qcom,charger-detect-eoc` or float-charge, **charger WDOG off**), bat_if_init (BPD source, force VREF_BAT_THM), usb_path_init (USB enum timer stop = 0, **CHG_CTRL CHG_ENABLE**), battery psy, initial status, IRQs, VDD-trim alarm. Stock DT has neither `charger-detect-eoc` nor `float-charge` nor `disable-vbatdet-based-recharge`, so all three SW paths below are active. 2. IRQs: chg_failed, fast_chg (on: clear chg_done, start 50 s VDD-trim alarm), chg_done (only sets a flag), **vbatdet_lo** (falling edge), batt_pres, batt_temp, usbin_valid, usb_overtemp. 3. EOC: VM-BMS (`qcom,report-charger-eoc`) decides "full" (SOC 100 / OCV at 100) and calls battery `set_property(STATUS, FULL)`. The LBC driver then **disables charging** (CHG_CTRL: CHG_EN=0, FORCE_BATT_ON=1), sets chg_done, **removes the VBAT_DET override** and **enables the vbatdet_lo IRQ**. 4. Recharge, three independent paths, all in software: - **vbatdet_lo IRQ** (VBAT fell below VBAT_DET): disable the IRQ, override VBAT_DET to 0 again, **CHG_ENABLE**. - **resume-soc** (DT 99 %): `get_prop_capacity` (polled by healthd/BMS) re-enables charging when SOC ≤ resume_soc and USB is in. - **usbin_valid** insert: override VBAT_DET to 0, CHG_ENABLE (and on removal: disable + 90 mA). - Also `external_power_changed`: IBAT_MAX = min(USB current_max (500 mA), thermal, JEITA) and enable/disable on suspend (≤ 2 mA). 5. Other runtime: VDD_MAX trim alarm every 50 s while fast charging (compensates VDD_MAX against measured VBAT), JEITA via ADC_TM batt_therm, thermal mitigation levels 1440/720/630/0 mA. ## B. Upstream pm8916_lbc **Only programs the LBC and leaves the state machine to the hardware.** No termination/recharge logic at all. - Probe writes: **VDD_SAFE** (`qcom,fast-charge-safe-voltage`), **IBAT_SAFE** (`qcom,fast-charge-safe-current`), **TCHG_MAX_EN = 0x00 (safety timer OFF)**, **VDD_MAX** (from `monitored-battery` voltage-max-design, clamped to safe), **IBAT_MAX** (= safe current, settable later via sysfs `constant_charge_current`), **CHG_CTRL = 0xA0 (CHG_EN | PSTG_EN)** (overwrites the live 0x90 → clears bit4, sets bit5). - Does NOT touch: IBATTERM_EN (HW termination stays as SBL1 left it = ON), CHG_COMP_OVR1/VBAT_DET, BOOT_DONE, WDOG, VIN_MIN, VBAT_WEAK, BAT_IF/BPD/BTC, VDD trim, USB enum timer. - IRQs: only **usb_vbus** (0x13 bit1) → `online` + its own extcon (EXTCON_USB) + power_supply_changed. chg_done/vbat_det/fast_chg/ chg_fail/bat/temp interrupts are listed in the DT binding but never requested. - Properties: ONLINE, CONSTANT_CHARGE_VOLTAGE_MAX, CONSTANT_CHARGE_CURRENT (writable → re-runs configure → rewrites VDD_MAX, IBAT_MAX and CHG_CTRL with the same value; no 0→1 edge on CHG_EN). - Requires `monitored-battery` (probe fails without battery info). Minor bug: the clamp result in configure() is discarded (line 100–103). - Upstream BMS-VM: probe writes S1/S2 sample interval (10/10), FIFO length (2/2), EN_CTL; suspend/resume force S3/normal via SEC_ACCESS. Reports VOLTAGE_NOW = FIFO·300 µV, OCV for 180 s after boot, STATUS = charging if supplied; **no SOC, no EOC, no recharge**. Downstream converts FIFO raw with VADC calibration: raw·97.656 µV·3, then gain-corrected with the 625 mV/1.25 V references (uncalibrated: 14682 → 4.301 V; our CV level means the gain correction is ≈ −2.4 %). The ×300 µV constant is ≈ +5 % high here. ## C. Difference | | Stock | Upstream | Our board now (no driver) | |---|---|---|---| | Who ends the charge | SW (BMS SOC=100 → disable) | HW iterm comparator (whatever SBL1 left) | HW iterm comparator (0x105B bit3 = 1) | | HW iterm | **disabled** at probe | untouched | **enabled** | | VBAT_DET override | 0 while charging; removed at EOC, re-armed on recharge | untouched | none (0x10EE = 0) | | Recharge | vbatdet_lo IRQ / resume-soc / USB insert → CHG_EN | **none** | **none** | | BOOT_DONE | set | not set | not set | | Safety timer | `tchg-mins` 232 | **disabled** | 120 min, on | | IBAT_MAX | min(USB 500 mA, thermal, JEITA) | fixed (safe current) | 90 mA field | | VDD_MAX trim | 50 s alarm | none | SBL1 trim | | USB extcon | msm_otg | LBC's own extcon (conflicts with our `pm8916_usbin` on the same IRQ) | `pm8916_usbin` | ## D. Why the hardware does not start a new cycle, and what our board needs Explanation (consistent with all B10A observations; the exact FSM rule is undocumented → hypothesis, high confidence): - SBL1 leaves the LBC in **pure hardware mode**: HW iterm termination ON, no VBAT_DET override, BOOT_DONE not set. - After the CV taper the iterm comparator ends the cycle: CHG_STATUS 03 → 00, USB path 02 → 01. VPH = VBAT (no power path), so the whole load moves to the battery. - The FSM stays latched in that state. Nothing in this design restarts it automatically: on stock, restart is always a **software CHG_EN edge** (vbatdet_lo IRQ / resume-soc / USB insert). A warm reset does not reset the LBC peripheral (class B kept the state, PERPH_RESET_CTRL3 = 0x0b); only a full PMIC POR (class A) does. - Not proven: whether the LBC would resume by itself at a much lower VBAT (VBAT_DET comparator with no override). In 30 min it fell ≈ 70–80 mV below the CV level without resuming. The VBAT_DET threshold register is not named in the source. **Is enabling upstream pm8916_lbc enough? No.** It programs the limits once and adds the usb_vbus extcon, but it has no chg_done/vbat_det handling and no recharge. After the first termination the board would be exactly in today's state. At best, a USB re-plug would not help either (upstream does not touch CHG_EN on insert). What our board needs (beyond upstream): 1. A **recharge mechanism** that produces a CHG_EN 0→1 edge when VBAT drops (and on USB insert), e.g.: - kernel patch to pm8916_lbc: request `chg_done` + `vbat_det` IRQs, emulate the stock flow (EOC → disable + arm VBAT_DET → re-enable), or simpler: a periodic check (VBAT/BMS below threshold ∧ USB valid ∧ CHG_STATUS == 00 → CHG_EN toggle); - or a userspace supervisor (shell + regmap/debugfs or a small sysfs knob) doing the same. Debugfs writes are a debug path only. 2. A decision on termination: either keep HW iterm (then recharge logic is mandatory) or disable it (0x105B bit3 = 0, like stock) and accept float charging at a reduced VDD_MAX — **float at 4.2 V forever is bad for the cell; not recommended without a lower float voltage**. 3. A battery description (`simple-battery`, voltage-max 4.20 V for now, voltage-min ≈3.4 V, capacity from the label) — mandatory for both drivers. 4. USB extcon: move `usb`/`usb_hs_phy` from `pm8916_usbin` to `pm8916_charger` (like l8150) and disable `pm8916_usbin`, otherwise both drivers request the same usb_vbus IRQ. 5. IIO + `QCOM_SPMI_VADC` (+ add channel 6 VBAT_SNS with pre-scaling 1/3, maybe 0x30 batt_therm / 0x31 batt_id) for a calibrated VBAT, and to calibrate the BMS FIFO scale. 6. Keep the safety timer in mind: upstream turns it off. With HW iterm + recharge it is acceptable; otherwise keep it. 7. USB gadget MaxPower 2 mA → 500 mA (separate, unrelated to the PMIC). ## E. Minimal B10B plan (RAM-only, each step its own GO) Common rules: RAM boot via RESET-held power-on + `fastboot boot` (cache/aboot untouched); class-A power-on before each step so the LBC starts from SBL1 defaults; USB meter in line; results streamed to 480s; full PMIC dump (`b10a-probe.sh`) before boot actions and after each probe → diff. - **B10B-0 (no new PMIC writes by drivers):** kernel b9c + `IIO`, `QCOM_SPMI_VADC`, `QCOM_SPMI_TEMP_ALARM`; DT adds VADC channel 6 (VBAT_SNS 1/3). Note: VADC conversions themselves write VADC control registers (0x31xx) — that is the only write class. Check: VBAT via IIO vs BMS FIFO (calibrate the scale), USBIN, VPH, die temp, PMIC thermal zone. Baseline/diff: CHGR/BAT_IF/USB/MISC/BMS unchanged. - **B10B-1 BMS:** + `BATTERY_PM8916_BMS_VM`, `simple-battery` node (4.20 V max, 3.4 V min). Expected writes: BMS 0x4055/56/47/46 only. Check: power_supply `pm8916-bms-vm` VOLTAGE_NOW/OCV vs IIO. Charger untouched. - **B10B-2 charger (upstream as-is, observation):** + `CHARGER_PM8916_LBC`, `qcom,fast-charge-safe-voltage = 4200000`, `qcom,fast-charge-safe-current = 500000`, battery voltage-max 4.20 V; extcon moved to `pm8916_charger`, `pm8916_usbin` disabled. Expected writes: 0x1041, 0x1045, 0x1060 (timer off!), 0x1040, 0x1044 (=4 → 450 mA: 500000/90000−1 = 4, i.e. 450 mA), 0x1049 = 0xA0. Check: USB enumeration still works (NCM), meter current, CHG_STATUS/0x1308 trend through CC → CV → termination, and confirm that **no recharge happens** (expected). Decide whether to keep the safety timer (would need a patch). - **B10B-3 recharge logic (needs design GO):** minimal patch or userspace supervisor: on (USB valid ∧ CHG_STATUS == 00 ∧ VBAT < 4.10 V [threshold TBD]) → CHG_CTRL 0x80 → 0x81/0x00 → 0x80 (or stock-style VBAT_DET override + vbat_det IRQ). Test: full cycle, termination, discharge to the threshold, **automatic recharge**, repeat ≥2 cycles, USB unplug/replug, class-B reboot during done-state. - Only after B10B-3 PASS: persistent integration (cache), separate GO.