Repository navigation
arm64: dts: qcom: sdm429-lenovo-tbx505x: Enable charger and fuel gauge - #60
Open
lostsignal-502 wants to merge 33 commits into
Open
lostsignal-502 wants to merge 33 commits into
lostsignal-502 wants to merge 33 commits into
Conversation
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>
lostsignal-502
force-pushed
the
tbx505x-battery
branch
from
October 11, 2026 09:25
6deba7f to
c8993e9
Compare
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>
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>
lostsignal-502
force-pushed
the
tbx505x-battery
branch
from
October 11, 2026 09:33
c8993e9 to
e55965d
Compare
Add the battery node from the stock LWN profile and enable the PMI632 charger. Signed-off-by: lostsignal-502 <viaan4542@gmail.com>
lostsignal-502
force-pushed
the
tbx505x-battery
branch
from
October 11, 2026 12:55
e55965d to
a9a20fc
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.