Skip to content

arm64: dts: qcom: sdm429-lenovo-tbx505x: Enable charger and fuel gauge - #60

Open
lostsignal-502 wants to merge 33 commits into
msm89x7-mainline:msm89x7/7.1.3from
TB-X505X-Labs:tbx505x-battery
Open

lostsignal-502 wants to merge 33 commits into
msm89x7-mainline:msm89x7/7.1.3from
TB-X505X-Labs:tbx505x-battery

Conversation

@lostsignal-502

@lostsignal-502 lostsignal-502 commented Oct 11, 2026 •

Copy link
Copy Markdown

Enables charging and the fuel gauge on the TB-X505X. Driver from László Gergely Lajosházi's PMI632 series, unchanged, including his ADC5 BAT_THERM entry (Marc Lainez sent the same fix earlier).

Tested on Android: charging works, battery reports 98%, health Good.

llg179 and others added 28 commits October 11, 2026 14:54
The driver behind this binding was generalised into qcom_smbx and now
serves
the PMI632's SMB5 charger alongside the PMI8998/PM660 SMB2, so document the
third compatible here rather than in a second file: the two generations
differ
in their register layout and in which analog channels the PMIC routes, not
in
the shape of the binding.

That reach shows up as three additions:

- three further optional io-channels. usbin_i and usbin_v stay required;
  vbat,
  bat_therm and bat_id are each taken with devm_iio_channel_get() and
  dropped
  on anything but -EPROBE_DEFER, so a board describes only what it wires.
  The
  schema says minItems: 2 for the same reason.
- qcom,thermal-mitigation and #cooling-cells, for the cooling device the
  charger registers. The description states what the driver enforces: at
  least
  two states, most permissive first, never increasing.
- qcom,batt-id-pullup-ohm, the board's pull-up on the BAT_ID net, which
  turns
  the voltage on the bat_id channel into the resistance of the resistor
  inside
  the pack.

Checked with dt_binding_check (both examples) and with dtbs_check against
sdm632-fairphone-fp3.dtb, where the charger node validates clean; yamllint
is
clean against the bindings' own configuration.

The maxItems: 16 on qcom,thermal-mitigation is a schema-side bound only.
The
driver sizes the table from the property and imposes no maximum, so this is
a
sanity limit rather than a hardware fact.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
A board that can be fitted with more than one pack tells them apart by a
resistor built into the pack, and a charger that measures it can refuse to
apply one battery's limits to a different battery - the failure that
actually hurts, because the device tree names only one battery and cannot
choose.

Name the resistance here, generically. It is not a Qualcomm idea, and the
alternative this port used first - a vendor-prefixed property on the
battery node - is rejected outright by this schema, which sets
additionalProperties: false and carries no vendor-prefixed property at all.

How the resistance is measured is deliberately not described here: the
divider, its pull-up and the ADC belong to the board and to the charger,
not to the cell.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
…ng policy

The JEITA thresholds, the currents to fall back to inside the soft zones and
the recharge voltage were described on the monitored battery. They cannot
stay there: battery.yaml sets additionalProperties: false and carries no
vendor-prefixed property, so dtbs_check rejects every one of them, and the
only JEITA precedent in tree - qcom,jeita-extended-temp-range in
qcom,pm8941-charger.yaml - is on a charger node.

There is a layering argument as well as a schema one, and it is the reason
this is not merely moved to get past a checker. A threshold here is a raw
BAT_THERM ADC code, and which code a given temperature produces depends on
the PMIC's ADC full scale and on the board's pull-up as much as on the cell.
A code is therefore not a property of the pack and cannot travel with one;
a board that fits two packs would have to repeat the same codes on both.

Rename qcom,batt-id-pullup-ohm to qcom,batt-id-pullup-ohms in the same
change. The canonical unit suffix is plural, and the property is this
port's own addition, so nothing outside this tree can be relying on the
old spelling. The battery-ID tolerance joins it here for the same reason
the pull-up is here: it has to cover the divider and the ADC, not just the
resistor.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
…ady imply

Caught by running the checker rather than by reading the file: with the
property renamed to qcom,batt-id-pullup-ohms, dt-validate rejected the board
with "100000 is not of type 'array'". A recognised unit suffix already gives a
property its type - uint32-array - and an explicit $ref to uint32 contradicts
it rather than narrowing it. The old spelling, -ohm, was not a recognised
suffix, so the $ref was doing the work and the rename silently took that away.

So: -ohms, -percent and -microamp properties get maxItems (or minItems and
maxItems) and no $ref, which is what dt-doc-validate asks for in as many
words. The -thresholds arrays keep theirs, since that is not a unit suffix and
nothing else would type them.

Measured after the fix: the board's charger node no longer reports a type
error, and the battery node reports nothing at all - which was the point of
moving those properties in the first place.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
qcom_smbx currently drives the SMB2 charger in the pmi8998/pm660 family. The
PMI632 carries an SMB5 block: same overall design, but several registers moved
peripheral and a few fields changed width or position.

Describe the differences in the variant structure rather than open-coding them:
the status register prefix (MISC 0x600 on SMB2, DCDC 0x100 on SMB5), the
current step (25 mA vs 50 mA), the charge-status bit positions, and the JEITA
status register, which SMB5 splits out into BATTERY_CHARGER_STATUS_7 instead
of sharing BATTERY_CHARGER_STATUS_2.

Register layout and field values are taken from the Qualcomm downstream
qpnp-smb2 and qpnp-smb5 drivers, published in the Fairphone 3 kernel source
release.

PMI632 has no coulomb-counting fuel gauge in mainline, so capacity comes from
the OCV table in the board's simple-battery node.

Based on the upstream driver by Casey Connolly <casey.connolly@linaro.org>
(drivers/power/supply/qcom_smbx.c, Linaro).

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The driver already reads VBAT_SNS through the PMIC ADC to drive the
voltage-based fuel gauge; the pack thermistor sits on the same ADC and needs
nothing more than a second channel.

Take it as optional, like VBAT_SNS: a board that does not route the thermistor
to the PMIC simply keeps a battery without POWER_SUPPLY_PROP_TEMP, rather than
failing to probe.

Provenance: new here, but nothing about it is invented. It is modelled directly
on this driver's existing vbat_chan handling - the same optional
devm_iio_channel_get(), the same -EPROBE_DEFER passthrough, the same
iio_read_channel_processed() - so battery temperature arrives the same way
battery voltage already does. That the thermistor is on the PMIC ADC at all,
rather than behind the fuel gauge, is Qualcomm's design, visible in the
downstream pmi632 device tree. The only judgement call is the unit conversion:
the ADC returns millidegrees C and the power supply class wants decidegrees.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The charger has a JEITA block that compares the battery thermistor against
four comparator thresholds and acts on the result in hardware: outside the
hard pair it stops charging, inside the soft pair it subtracts a fixed offset
from the fast-charge current. The driver already reads the resulting status
bits for POWER_SUPPLY_PROP_HEALTH, but nothing programs the thresholds, so
every board runs on the PMIC's power-on defaults.

Measured on a Fairphone 3 (PMI632, 2026-07-29) those defaults are hard limits
at roughly 11 and 58 degC, soft limits at 16 and 50 degC, and a 250 mA
compensation, with JEITA_EN_CFG reading 0x1f - that is, the soft compensation
is already enabled, just against generic thresholds. That is tolerable while a
board charges at 1 A. It is not what a board wants once it charges at its
battery's rated current, which is the point of describing the thresholds.

Add three optional properties, all no-ops when absent so no existing board
changes behaviour:

  qcom,jeita-hard-thresholds = <cold hot>
  qcom,jeita-soft-thresholds = <cold hot>
  qcom,jeita-soft-fcc-microamp = <cool warm>

The thresholds are raw BAT_THERM ADC codes, which is how the comparators are
specified and how Qualcomm's own device trees carry them. Deriving them from
degrees Celsius instead was tried and rejected: mainline has the inverse
conversion (qcom_adc_tm5_temp_volt_scale), but it uses the generic 100k
pull-up curve, and against Fairphone's characterisation of this pack it is
late at every one of the four thresholds - by 1.5 degC at 0 degC, 0.3 at 20,
1.3 at 45 and 2.0 at 55 - which widens each limit in the unsafe direction. A
raw code involves no curve at all.

The third property is the current to be left in each soft zone, rather than
the register's own offset, so the device tree describes a charge current and
the driver works out the delta from whatever is programmed. It reads the
fast-charge current back out of the hardware for that, rather than trusting
the device tree to match.

Only the two charge-current compensation bits are enabled. The float-voltage
halves are left alone: the register they scale by is not documented for this
generation in any source available here, so there is nothing to program it
from.

Provenance: the register offsets, the big-endian hot-then-cold layout of each
threshold block and the 25 mA compensation step are read out of Qualcomm's
downstream qpnp-smb5 driver (smb5-reg.h, smblib_update_jeita(), the
jeita_cc_comp entries of smb5_pmi632_params) as shipped in Fairphone's
published Fairphone 3 kernel source release. The default register values above
were read off a live device. New here is the device-tree interface and
expressing the compensation as a target current.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
There is no path in this driver from "the phone is hot" to "charge slower".
The charger is a substantial heat source at its rated current - on a Fairphone
3 charging a 3.8 V cell at 2 A from a 5 V port, over two watts land in the
PMIC - and the only thing that currently limits it is the fixed fast-charge
current programmed at probe.

Register the fast-charge current as a thermal cooling device, so a thermal
zone throttles charging the way it throttles a CPU. The states come from an
optional qcom,thermal-mitigation array of currents, most permissive first;
state 0 is what the board asked for unmitigated. Boards that do not describe
it keep exactly today's behaviour.

The array is checked at probe rather than trusted: at least two states, never
increasing, and the first one no higher than the fast-charge current actually
programmed in the hardware.

This composes with the JEITA compensation added in the previous change without
either needing to know about the other, because the compensation is a
subtraction from whatever is programmed here - a mitigated current stays
mitigated inside the JEITA soft zones.

Provenance: qcom,thermal-mitigation is Qualcomm's own property name and unit,
used by the downstream qpnp-smb5 driver, where the equivalent table is driven
through a POWER_SUPPLY_PROP_SYSTEM_TEMP_LEVEL vendor property from a userspace
thermal daemon. Driving it from a thermal zone instead is what is new here.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
power_supply_get_battery_info() is called at probe, but the only field this
driver applies to the hardware is voltage_max_design_uv. In particular
constant-charge-current-max-microamp does nothing at all: the fast-charge
current is whatever the per-generation init sequence wrote, ~1 A, chosen as a
deliberately conservative bring-up limit to minimise fire-hazard risk. A board
that has characterised its pack has no way to say so.

Program the fast-charge current from the monitored battery, bounded by a new
per-generation ceiling, smb_variant::fcc_max_ua. The ceiling is what keeps the
original intent: a device tree can ask for a current, but not for one this
driver has not been taught is safe on that PMIC generation, and raising the
ceiling stays a change to the driver that someone has to justify.

Set to 1 A on SMB2 (pmi8998, pm660), which is exactly what those boards get
today - sdm845-oneplus-enchilada and -fajita already ask for 1.8 A in their
battery nodes and continue to charge at 1 A, unchanged by this patch.

Set to 2 A on SMB5 (pmi632). That number is measured rather than picked: it is
the most a Fairphone 3 can usefully take, since without high-voltage
negotiation the input side tops out near 1.5 A at 5 V, and it leaves the JEITA
soft-zone compensation - a six-bit field, at most 1575 mA of reduction - able
to express this battery's characterised 600 mA cool-zone current, which it
could not at the pack's rated 2.7 A.

Also move the JEITA and cooling-device setup below this point, since both read
the fast-charge current back out of the hardware.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
…y policy

The previous change made the monitored battery's constant-charge-current-max
reach the hardware, and then bounded it with a new per-generation constant -
1 A on SMB2, 2 A on SMB5. That constant is the same defect the change set out
to remove, one layer over. Neither number is a fact about the PMIC:

  - the PMI632's fast-charge register goes to 3 A, not 2. The 2 A was the
    rating of the battery in one Fairphone 3, which every other PMI632 board
    would then have been held to;
  - the pmi8998's goes to 4.5 A, not 1.

Set smb_variant::fcc_max_ua to the datasheet maxima Qualcomm's own drivers
carry (smb_params.fcc.max_u in qpnp-smb2 and qpnp-smb5), and let the battery
node decide everything below that. How much current a pack should take, and
whether the board can carry the heat, are properties of the pack and of the
board; this file knows neither.

That does change the two SMB2 boards in mainline: sdm845-oneplus-enchilada and
-fajita ask for 1.8 A in their battery nodes and have been charging at 1 A
because of the init-sequence cap. They will now charge at the current their own
device trees describe. That is the point of the property, but it is a
behaviour change on hardware not tested here, and it should be acked by someone
with those boards.

Boards that describe no monitored-battery are unaffected: the init sequence
still leaves ~1 A.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The JEITA thresholds and the per-zone charge currents were read off the charger
node, which is where the driver already had a struct device and therefore the
convenient place to put them. It is the wrong place: which temperatures a cell
may be charged at, and how hard, is a property of the cell, and Qualcomm's own
trees keep those numbers with the battery data for exactly that reason.

The cost of having them on the charger was not theoretical. A board that fits
more than one pack cannot describe both - and the board this was developed on,
the Fairphone 3, ships two 3000 mAh packs with different cool-band thresholds,
told apart at runtime by a battery-ID resistor.

Read them through the monitored-battery reference instead, so each described
battery carries its own. No functional change for a board with one battery
beyond where the properties are written.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
A power supply has one monitored-battery, so a board that can be fitted with
more than one pack has to name a single one and hope. The Fairphone 3 ships
two 3000 mAh packs - different part numbers, 2.0 A against 2.7 A, and JEITA
cool bands at 15 and 20 degC - and picking the wrong one in the device tree is
undetectable from software today: the charger simply applies one cell's
currents and temperature limits to the other.

The hardware does provide the answer. The packs carry different ID resistors,
read through a divider against the ADC's 1.875 V reference, and that is how the
vendor stack tells them apart.

This does not add battery *selection*; there is no binding for a second
monitored-battery, and inventing one belongs in a separate discussion. It adds
the check that makes the single description honest: given

  - a "bat_id" io-channel on the charger,
  - qcom,batt-id-pullup-ohm on the charger (which pull-up the board wires), and
  - qcom,batt-id-ohm on the battery (what that pack should read),

the measured resistance is compared against the described one, within
qcom,batt-id-tolerance-percent (default 15, matching the vendor's
batt-id-range-pct on this board). A mismatch, or an open or shorted ID line,
leaves the charger on the init sequence's conservative defaults instead of on
another cell's limits, and says so in the log.

Boards that describe none of the three are unaffected.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The cooling device rejected a qcom,thermal-mitigation table whose first state
was above the programmed fast-charge current. That check is wrong in exactly
the case the battery-ID verification exists to create: when the fitted battery
cannot be identified the charger stays on the init sequence's ~1 A, the board's
table still starts at the current it was written for, and probe then fails with
-EINVAL - leaving the phone with no charger driver at all, as the outcome of a
safety fallback.

Clamp each state to the current actually programmed instead. Mitigation may
only ever reduce, so a table written for 2 A collapses harmlessly onto 1 A when
that is all the charger is running, and the states stay monotonic. The
DT-sanity check that the table does not increase is kept.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
Two continuation lines were one space short of the open parenthesis, and one
exceeded 100 columns; give the long expression a local instead. No functional
change.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The reported capacity came from looking the battery's terminal voltage up
in the monitored-battery OCV table. That table maps an open-circuit
voltage, and a phone's terminal voltage is anything but: on a Fairphone 3
a stretch of CPU load moves it by 250 mV, while the same table spends
eighteen points of charge on the 40 mV between 3.80 V and 3.84 V. The
result was not an imprecise gauge but one that tracked the load. Measured
on that phone against the vendor stack, which held 57% throughout: this
driver read 65% at rest, fell to 6% over three minutes of eight-thread
CPU burn, and climbed back to 60% within a minute of the burn ending.

SMB5 PMICs sit next to a QG fuel-gauge peripheral, and it is already
running - the PMIC's own boot sequence starts it, so its sample registers
are live with nothing here configuring them. That gives us the battery
current alongside the voltage, from one converter at one instant, which
is what the usual arrangement needs: integrate the current, and correct
the integral against the OCV table only while the current is small enough
for the IR term to be worth trusting.

Two currents gate that correction, both well below what this phone draws
awake, so the gauge steers while it idles or charges gently and coasts on
the integral otherwise. A poll that arrives more than a minute late means
the system was suspended, and the sample says nothing about the interval
that passed - but a suspended phone is a rested battery, the one state
where the table needs no correction at all, so those re-anchor outright.
The gauge starts from the open-circuit voltage the PMIC measured with
nothing drawing, in preference to a live sample taken while the machine
is busy booting.

The battery current and the open-circuit voltage are worth having on
their own, so report both. A PMIC without a QG peripheral keeps the old
voltage lookup, with the caveat above it written down.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude <Opus 5> <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
Two things in this register are generation-dependent and were being read
as if they were not, so an SMB5 PMIC reported the wrong charge status at
both ends of a charge.

BAT_OV is bit 5 of STATUS_2 on SMB2 and bit 1 on SMB5. Every other reader
of that register already takes the bit from smb_variant::ov_bit; this one
had the SMB2 constant compiled in. On a Fairphone 3 finishing a charge the
register reads 0x28, whose bit 5 is not an overvoltage, and the driver
called it "Not charging" while 300 mA was flowing into the battery.

The eight status codes themselves were also renumbered: SMB5 moved INHIBIT
from 6 down to 0, shifted the three codes below it up by one, and gave the
vacated 6 to PAUSE, which SMB2 does not have. Codes 3, 4, 5 and 7 agree,
which is why the SMB2 table looks right in the middle of a charge - but an
inhibited SMB5 charger read as trickle-charging, and a paused one read as
full. Reporting a paused charge as full is the worse of the two, because
anything that trusts a full battery will pin its own reading to it.

Both now come from the variant. The mapping is a table per generation
rather than a switch, since what changed between them is which code means
what and not what the meanings are.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude <Opus 5> <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
Two states report as full and only one of them is a finished charge, so
the gauge was pinning itself to 100% on the wrong one. Caught on a
Fairphone 3 whose charger was inhibiting at 4.30 V of a 4.39 V float: the
gauge read 100% where its own open-circuit voltage put the pack at 93,
and where it had read 92 a minute earlier.

On SMB5 the inhibit code is not the post-termination resting state SMB2
uses it as - it is also where the charger sits whenever it is held off,
with no statement about how full the battery is. Report that as not
charging, which is all the state supports saying, and leave the reported
capacity to the gauge, whose open-circuit reading is accurate precisely
here because an inhibited charger draws no current.

The gauge now asks the status register for the termination code directly
rather than asking for the reported status, so that what pins it to full
is the charger having finished, not a status that several states share.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude <Opus 5> <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
…t is

The gauge takes charge completion from the charger, because termination is
the one point on the curve the charger knows better: it stopped because the
cell reached the float voltage below the termination current. It took it
only at the instant the register showed that code, and that is not how the
hardware behaves. Having terminated, the cell is by definition above the
recharge threshold, so the charger moves straight on to inhibit and stays
there for as long as the cable is in.

Two things follow. A ten-second poll has to land inside a window the
hardware leaves as fast as it can, and even when it does the next poll
resumes correcting toward the OCV table, at a quarter of the gap per poll
while the current is quiet - so a hundred percent that was latched
correctly is walked back down within a minute.

Where it lands is the second half of the problem, and it is not an error in
the table. The table's top entry is an OCV of 4.3756 V, which a cell held at
a 4.39 V float only shows while it is still being held there; let go, it
settles some seventy millivolts below that, which on this curve is six
percent. So a battery that is as full as this charger will ever make it
reads in the mid nineties, and keeps reading that for as long as it stays on
the cable. Measured on a Fairphone 3 that had just finished charging:
STATUS_1 0x40 - CC_SOFT_TERMINATE set, status code 0, inhibit - with
STATUS_7 clear, no JEITA, the float register at 0x4f (4.39 V), 0 mA into the
pack at 4.3149 V, and a reported capacity of 94%.

Remember the completion instead of catching it. Inhibit alone is not
evidence of a full pack - it is equally what a charger does when handed a
cell that was already above the threshold when the cable went in - but
inhibit after a charge that was actually running is the tail of that charge.
Tracking that pairing needs no knowledge of where the inhibit threshold sits,
survives any sampling interval, and clears itself when the input goes away or
the charger starts charging again. Report the status as full there too, which
the code alone cannot support saying.

What this does not fix: unplugging a full pack drops the reading to what the
table says about a rested cell, because nothing has re-anchored the table's
top to the OCV this pack actually rests at once full. Learning that anchor
is a separate change and needs a charge and a discharge measured against a
reference gauge.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude <Opus 5> <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
A terminated charge is meant to begin again once the cell has fallen back by
a little, and on this PMIC it never did: the pack came off the charger's
attention at the float voltage and then simply drifted down, for as long as
the cable stayed in.

CHGR_CFG2 is where that is configured, and its lower bits are not the same on
the two generations. SMB2 has AUTO_RECHG at BIT(2) and EN_ANALOG_DROP_IN_VBATT
at BIT(1); SMB5 replaces both with one two-bit field naming what a recharge is
decided by - clear for nothing at all, BIT(2) for the battery voltage, both
bits for the state of charge. The init sequence for pmi632 was derived from the
SMB2 one and wrote the same value, so the field came out clear, which is the
one setting under which no recharge ever happens.

State of charge is not an option here in any case: the gauge that would report
one to the PMIC is Qualcomm's own and is not part of this driver. So select the
battery voltage, take three comparator samples before acting on it as the
vendor driver does whenever it makes that choice, and program the threshold
itself - CHGR_ADC_RECHARGE_THRESHOLD_MSB/LSB, in the same 194637 nV units the
gauge reports, because it is the same ADC.

The threshold comes from the board, since it is a property of the cell rather
than of the PMIC, and is left at the hardware default where the board does not
name one. The Fairphone 3 names 4.30 V, which is what its own downstream node
asks for under a dated vendor comment, ninety millivolts below its float.

Measured before the change, on a phone that had finished charging and was
still on the cable: BATTERY_CHARGER_STATUS_1 0x40 - CC_SOFT_TERMINATE set,
status code inhibit - BATTERY_CHARGER_STATUS_7 clear so no charging stage
enabled at all, and the gauge reading exactly zero current into the pack at
4.24 V of a 4.39 V float. Pulling the cell to 4.14 V under load did not
restart it either, which is what rules out the inhibit threshold as the
explanation and leaves the recharge field.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude <Opus 5> <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
… drives

The gauge steers its integral with the OCV table whenever the current is low
enough for the table to be worth more, and a charger in its constant-voltage
phase looks exactly like that condition: the current has decayed to tens of
milliamps and everything appears quiet. It is not. In constant voltage the
charger holds the terminal at the float voltage and lets the current fall,
so what the ADC reads is the charger's number rather than the cell's, and
subtracting I*R does not recover an open-circuit voltage from it - at those
currents the correction is a few millivolts against a hold of tens.

The table, asked about a voltage above its top entry, answers a hundred
percent. Measured on a Fairphone 3 in taper: 4.4122 V terminal at 56 mA, so
4.405 V after the IR correction, against a table whose top entry is 4.3756 V.
The reading reached a hundred percent while the pack was still filling and
the charger was still charging - some five points of that climb were the
correction rather than charge that had gone in.

So count charge and nothing else while the charger is driving the pack. The
completion the charger reports is what ends that, and it is the honest
hundred percent. The same reasoning applies to the re-anchor a long poll gap
takes as evidence of a rested battery: a phone that suspended on a charger
has the least rested cell it ever has.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude <Opus 5> <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
… only

The gauge counts charge, which holds the reported capacity steady while the
load moves, and corrects that count against an open-circuit voltage. It used to
take that correction from the live sample whenever the current was low enough,
but on this pack the discharge curve is nearly flat from 28 to 46 percent -
eighteen points inside forty-one millivolts - and the overpotential a loaded
sample carries, which the constant series resistance does not model, becomes a
large, one-directional error there. Measured on a Fairphone 3: a gauge seeded
at 42% walked down into the low teens over a session while the cell was near
thirty, and under a one-amp load the recovered OCV read one percent.

So correct only from readings the hardware took at rest:

  - The gauge captures an open-circuit voltage on its own each time the current
    stays near zero long enough, and keeps it in S3_GOOD_OCV. Take a fresh one
    outright - from the poll, and where the device tree wires it, from the
    good-OCV interrupt, which catches the capture the instant it happens rather
    than whenever the next poll runs.

  - The charger's own completion is the honest hundred percent.

  - A suspend rests the cell, but the sample the first poll after it reads can
    be loaded; take that one only when it is itself at rest, and otherwise keep
    the count, which a suspend draws too little to have moved.

And persist the state of charge to the gauge's scratch SRAM, which a warm reboot
keeps, restoring it at probe instead of re-deriving it from a rest OCV that has
gone stale between the rare captures this pack allows - which had the reported
capacity jump on every reboot. Both the good-OCV interrupt and the SRAM store
mirror what the downstream qpnp-qg driver does with the same hardware.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude <Opus 4.8> <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
…e of it

Five changes to what this driver tells the charger about finishing a
charge, and to what it reports back afterwards. All are taken from the
downstream qpnp-smb5 driver in the Fairphone 3 vendor tree and were
measured against a Fairphone 3 running this driver.

  - Program the ADC termination-current threshold from the battery
    node's charge-term-current-microamp. This comparator was not
    missing: read back on a Fairphone 3 it held -101.8 mA with the ADC
    source already selected. What was wrong is whose number it was - a
    threshold the boot left behind, that nothing in this kernel chose or
    could state. Downstream takes it from qcom,chg-term-current-ma, and
    every battery profile this phone ships declares 100 mA.

  - Do not let the state of charge persisted in the gauge's scratch SRAM
    outvote a rested reading it disagrees with by more than ten points.
    What is stored is where a count had reached, and a count is only as
    good as the corrections it received; what the gauge captured at rest
    is a measurement of the pack itself.

  - Write 2 rather than 3 into NO_OF_SAMPLE_FOR_RCHG. The field counts
    from zero - downstream writes 2 and its comment calls that three
    samples - so the comment here claiming to match downstream did not.

  - Report POWER_SUPPLY_PROP_CHARGE_TYPE, which downstream exposes and
    this driver did not. Mainline has no value meaning taper, and the
    drivers that meet a constant-voltage phase do not report one, so the
    taper code maps to STANDARD against FULLON's FAST: the names fit
    loosely, but the transition userspace cares about still shows.

  - Regulate the input current against a connector thermistor on boards
    that describe one, as smb5_configure_mitigation() does: bias the
    thermistor, enable the ADC channel that reads it, and name that
    channel as a regulation source. A board that says nothing gets none
    of it, since biasing a pin with no thermistor on it measures the
    pull-up.

None of this makes the charger terminate on a Fairphone 3, which it
still does not do: measured over a four-hour charge, the current sat
below the programmed threshold for one hour forty-nine without the
charger ever leaving taper. That remains open.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The pmi632 init sequence cleared I_TERM_BIT, on the strength of a comment
this driver inherited from the SMB2 half of itself:

	I_TERM_BIT - Current termination ?? 0 = enabled

The two question marks are the author saying they did not know, and on
SMB5 the answer is the other way round. Downstream leaves the bit set;
clearing it is what stopped the charger ever leaving taper.

Measured on a Fairphone 3 against the vendor stack running on the same
phone, same pack, same supply. Everything the decision depends on was
already identical between the two - termination threshold 99.9 mA in
CHGR_ADC_ITERM_UP_THD, the ADC comparator selected in
CHGR_ENG_CHARGING_CFG, the same count in NO_SAMPLE_TERM_RCHG_CFG - and
CHGR_CFG2 was 0x05 here against 0x2c there. With the bit cleared the
current sat below the threshold for one hour forty-nine minutes and the
charger stayed in taper, eight hours twenty-six in all, never once
reaching BATTERY_CHARGER_STATUS_1 code 5. With it set the charger
terminates within a minute of the current crossing - from 59% on a 2 A
supply and from 97% on a 500 mA one - ending at 0x45, the value the
vendor stack shows in the same state.

Left as it was, a charge that never terminates holds the pack at the
float voltage indefinitely instead of finishing and letting it relax to
the recharge threshold, so this was not only a reporting problem.

Only this bit changes. CHGR_CFG2 also differs from downstream in
CHARGER_INHIBIT and PRETOFAST_TRANSITION, and those are left alone: with
three bits moved at once, a charger that then worked would not say which
one had been the problem.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
…inked

A full pack stops being full once charge starts leaving it, so losing
that flag when the input goes away is right. Forgetting that a charge
happened at all is not: that is a fact about what the charger did, and
it does not unhappen because the cable flickered.

The input does flicker. Re-running APSD on a source change drops online
briefly, and so does anything at the other end of the cable. Clearing
the history there costs a full pack its full reading, because the
charger settles back into inhibit, inhibit without a charge behind it is
not completion, and the rested open-circuit voltage then pulls a
genuinely full battery about six percent down the table - which is the
exact drop smb_fg_track_completion() exists to prevent, described in its
own comment.

Seen three times on a Fairphone 3 before it was understood: twice from
the cable being handled, once from APSD alone, each time leaving a
terminated pack reporting 96% and "not charging" while sitting at 4.33 V
with nothing drawn from it. Reproduced deliberately afterwards by
suspending and unsuspending USBIN, which now returns to full instead of
dropping.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The percentage this driver reported says nothing about size, and a
percentage was all it reported, which leaves anything wanting to say how
long a charge has left with no way to work it out. UPower derives its
estimate from the charge remaining and the power going in, so without a
capacity it can only show a number and no time.

Report CHARGE_FULL_DESIGN, CHARGE_FULL and CHARGE_NOW. There is no
learned capacity here - this gauge does not measure how the pack has
aged - so full and full-design are the same value, the one the device
tree states. Reporting a learned figure it has not learned would be
worse than reporting the design one.

Measured on a Fairphone 3: before this there was no time estimate at all,
after it UPower reports one, and its arithmetic checks out against the
sysfs values - (13.4334 - 9.6707) Wh / 4.09352 W = 55 minutes, where the
energy comes from these properties and the rate from current_now times
voltage_now.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
Follow the bindings: the JEITA thresholds, the soft-zone currents, the
recharge voltage and the battery-ID tolerance are now properties of this
device, and the pack's identification resistor is the generic
id-resistor-ohms rather than a vendor-prefixed name the battery schema
rejects.

Three of the property readers lose their monitored-battery lookup entirely,
which is why smb_init_jeita() no longer gives up when there is no battery
node: none of what it programs comes from one any more.

No behavioural change is intended - the same values reach the same
registers, from a different node.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The gauge divides counted charge by charge_full_design_uah and reports the same
value as CHARGE_FULL. That value is a nameplate for a new cell. Measured on this
device, a full sweep of the OCV table from 4.3756 V to 3.000 V delivered 2175 mAh
against a declared 3060 mAh, and 2076 mAh down to the declared 3.400 V cutoff -
so one reported percent was worth 21.8 mAh rather than 30.6, every percentage the
gauge derived was optimistic by the difference, and a discharge to the hardware's
own shutdown stopped at 35 % with the pack empty.

Fixing that by editing the device tree's design value would be wrong twice over:
it is a nameplate the OCV table was characterised against, and it is a statement
about the hardware rather than about one aged pack.

So learn it instead. The gauge already has points it trusts enough to overwrite
the count outright - charge termination, an S3_GOOD_OCV re-anchor, and a wake
sample taken at rest - and the charge that flowed between two such points is the
size of the pack. Accumulate the counted charge between consecutive anchors and,
when the span is at least half the pack and the charge agrees with it about which
way the pack went, blend a quarter of the way toward what it says. Spans with a
hole in them are thrown away rather than used: a suspend gap counts no charge, so
it ends the span instead of shortening the answer.

CHARGE_FULL now reports the learned value and CHARGE_FULL_DESIGN keeps reporting
the device tree's, which is what the two properties are for. The learned value is
persisted beside the state of charge in the gauge's scratch SRAM behind the same
magic, so a warm reboot does not throw away hours of learning, and a battery swap
clears the magic and correctly falls back to design.

Expected convergence on the pack this was developed against: about 2076 mAh.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
Describe the SMB5 charger block and the ADC channels it reads (USB input
current and voltage, battery sense). Left disabled; boards that wire a battery
enable it and point it at their simple-battery node.

Interrupt numbers and the ADC channel assignment follow the Qualcomm
downstream pmi632 device tree from the Fairphone 3 kernel source release.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
ADC5_BAT_THERM_100K_PU is the battery pack thermistor, biased from the PMIC's
100k pull-up. The channel exists on every ADC5 PMIC and Qualcomm's own device
trees describe it, but the driver's channel table never listed it, so a device
tree that referenced it was rejected at probe.

Provenance:

 - the channel number (0x4a), the 100k pull-up variant and the ratiometric
   calibration are Qualcomm's, read off the downstream pmi632 device tree in
   Fairphone's published Fairphone 3 kernel source release, where the same
   channel appears as chan@4a "bat_therm";
 - the conversion curve is pre-existing mainline code: the table entry reuses
   SCALE_HW_CALIB_THERM_100K_PULLUP, the 100k pull-up thermistor scaling
   already used by the AMUX_THM and GPIO thermistor channels in this driver;
 - new here is only the table entry itself.

Downstream carries a per-pack thermistor table for this battery rather than the
generic 100k curve. Measured against each other the two agree at 25 degC and
diverge away from it, by roughly 1.5 degC at 0 degC and 2.5 degC at 60 degC.
The generic curve is therefore accurate enough to report a temperature, but a
board that wants to drive a JEITA profile from it should check that first.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
llg179 and others added 3 commits October 11, 2026 15:03
Describe the BAT_THERM channel and hand it to the charger, so the battery
reports a temperature.

Provenance: taken from Qualcomm's downstream pmi632 device tree, as published in
Fairphone's Fairphone 3 kernel source release, where the same channel appears as

	chan@4a {
		label = "bat_therm";
		reg = <0x4a>;
		qcom,calibration-type = "ratiometric";
		...
	};

The channel number, the 100k pull-up variant of it and the ratiometric
calibration are therefore Qualcomm's; the settle time follows the neighbouring
bat_id channel in this file rather than downstream, which leaves it at zero.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Assisted-by: Claude:claude-opus-5
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The gauge raises this the moment it captures a fresh rested open-circuit
voltage. The charger driver takes it as optional and uses it, when present, to
re-anchor the state of charge as soon as the capture happens instead of waiting
for its ten-second poll to notice. The interrupt number and name are the
downstream pmi632 QG node's.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude <Opus 4.8> <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
The charger node named five of the peripheral's interrupt sources. Add
the rest of the ones that describe charging - the CHGR, DCDC, BATIF and
MISC blocks - taking each specifier from the downstream qpnp-smb5 device
tree for this PMIC. Nothing is obliged to request them; a driver takes
what it uses, and describing the hardware is this file's job.

Two sources are left out on purpose. BATIF's all-chnl-conv-done marks an
ADC conversion rather than anything the charger decided, and CHGR's
fg-fvcal-qualified marks a float-voltage calibration sample - measured on
a Fairphone 3 at about one a second while merely discharging. The Type-C
block is not here either: it is a separate node with its own driver, and
naming its interrupts on this node would take them away from it.

usbin-icl-change was also pointing at the wrong index. It was copied from
pmi8998.dtsi, where the USB peripheral numbers icl-change as 6, but SMB5
inserted vashdn and revi-change into that block, which pushes src-change
to 6 and icl-change to 7. So the driver's ICL-change handler has been
wired to source-change on this PMIC. Both indices come from the
downstream device trees, which describe the two generations side by side.

Signed-off-by: Lajosházi, László Gergely <lajoshazilg@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
Add the battery node from the stock LWN profile and enable the PMI632
charger.

Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants