How to limit battery charge voltage and current

Like a few other users around here, I’m interested in limiting the maximum charge voltage and current to extend the longevity of the battery, so I dug in and read the datasheets and did some hacking and want to document what I’ve found for others. I have been doing this for several months now and it works well for me.

First, important warnings: messing about with the internals like this can be very dangerous if you don’t know exactly what you are doing, and could result in your battery catching fire and exploding if done wrong. For example, you can theoretically set your charge voltage to over 4.6V, which would almost certainly cause the lithium battery to “vent with flames”. Do not mess with this stuff without fully understanding what you are doing first.

The BQ25890 (datasheet: https://www.ti.com/lit/ds/symlink/bq25890.pdf ) chip in the phone is programmable via i2c, and is present on bus 3 at address 0x6a. The register that contains the stop charge threshold voltage is 0x06, with the upper 6 bits setting the voltage and the lower 2 setting start charge threshold when voltage drops by a certain amount while still plugged in, and the minimum voltage for fast charge threshold.

I decided that the 4.112 threshold was a good value for me, and calculated the value to put in the register as 0b1000110 (hex 0x46). The last two bits set fast charge threshold to 3V and start threshold to 100mV, both default. So, if we remove these two bits, we are left with 0b010001. The datasheet states that the lowest bit corresponds to 16mV and the highest 512mV, each step doubling. So, adding these two set bits up (16mV + 256mV) we get 272, then add the base offset (again, specified in the datasheet) of 3840mV, we get 4112mV or 4.112V. So, to put it all together and write this to the chip, I use:

i2cset -f -y 3 0x6a 0x06 0x46

I did also write a perl script to do this calculation and set the values a while back, but I never really ended up using or fully finishing it… maybe I could post that if anybody is interested.
Also, note that this value is temporary and gets reset on boot, so you have to set it after every boot. This can be made permanent by modifying the dtb file, which I plan to write about in another post along with how you can undervolt various components for lower temperatures and longer battery life, but is even more in-depth and fraught with gotchas like random crashing and potentially destroying your CPU or other hardware. For now, I just set it with a script so that if for whatever reason I want to fully charge the battery, I just reboot/power off and everything goes back to defaults.
It also seems to affect the battery capacity calibration, as after I had used this for a while, it eventually started reporting being 100% charged at 4.112V, so if you do this and want to go back, you may also wish to reset your battery calibration by shutting down and removing the battery for a bit, then doing a full charge/discharge cycle.

If you wanted to limit the charge by percentage instead of cell voltage, it may also be possible to toggle BATFET_DIS (bit 5, register 0x9) to immediately enable/disable charging, but I have not tested this.

Setting charge current:
I also wanted to control the speed at which the battery charges, and worked out how to control this as well. This setting actually controls the current that can be drawn on USB, so it can also be used as a workaround for chargers that don’t communicate their max current properly and cause slow charging at 500mA when they are actually capable of more. Also note, that since you can set currents higher than the charger supports, it is theoretically possible to damage your charger by drawing too much current, overloading and/or overheating it. I don’t know that I’ve ever seen one that would be damaged by overloading as they typically drop their voltage and/or shut off before any damage occurs, but again, beware that you are doing things that allow you to ignore the safety limits.

On the same chip as above, this time in register 0, the lower 6 bits control the input current limit, with the lowest bit being 50mA, and the highest being 1600mA, doubling with each step. I decided that for me, since I don’t have any chargers that aren’t capable of at least 1A, and that isn’t too slow to be useful to me, I would use 1A as my limit. I read the default value then calculated 0b00010010 (hex 0x12) as the value for 1A, with the highest 2 bits ignored, and the lower 6 being ((100mA+800mA)+base 100mA offset) = 1A. This one also has a convenient sysfs file that you can read to confirm what the current current limit is set to (even if you do not set your own value):

cat /sys/class/power_supply/bq25890-charger/input_current_limit

This value is in microAmps so a value of 1000000 corresponds to 1A, 900000 to 0.9A, and so on.
So, to write this value to the chip and confirm that it was set, I use the following one-liner:

i2cset -f -y 3 0x6a 0x00 0x12; cat /sys/class/power_supply/bq25890-charger/input_current_limit

Note that this limit also includes power being drawn by the phone, so if you were to set a very low limit, like 250mA then it would not charge at all, and would very slowly discharge as it draws somewhere around 350mA at idle. Another useful one-liner for monitoring battery level, current, voltage, temperature:

cd /sys/class/power_supply/max170xx_battery; watch -n5 grep . capacity current_now voltage_now temp

Note that this value is temporary and is reset on each plug/unplug of the USB cable, so you should set your charge current after it is already plugged in and charging, and it will be reset to the default each time it is plugged in. I wrote a udev rule to automatically set the current limit upon plugging it in, but due to reflashing the phone for troubleshooting another issue, the script is not convenient to access at the moment (I’ll have to pull it from encrypted backup). I’ll try to remember to edit that into this post later.

Edit: udev rule:
cat /etc/udev/rules.d/61-pluggedin.rules

SUBSYSTEM==“power_supply”,ENV{POWER_SUPPLY_ONLINE}==“1”,RUN+="/root/pluggedin.sh"
SUBSYSTEM==“power_supply”,ENV{POWER_SUPPLY_ONLINE}==“0”,RUN+="/root/unplugged.sh"

I also had it set up to set thresholds and run memory and CPU at higher clocks when plugged in for performance, and limited max CPU speed on unplug for better battery.

Other notes: I felt pretty confident about being safe about overloading certain things and decided to test it on my laptop. I intentionally set the current limit to the max of 3.25A while plugged into my laptop with a max output of about 1A, and it went up to about 1.2A, caused the USB voltage to sag to around 4.4V then after a minute or two, it tripped the overload on the laptop causing it to shut off the port. After it reset, everything worked fine again.

Happy hacking!

5 Likes

Original i2cget value is at 0x5e (4.16906V). Exact upper voltage value change should be visible with upower -d (I’d guess).

1 Like

Not sure I quite fully understand you. 0x5e is indeed the default value, corresponding to a threshold of 4.208V. The 4.16906 is likely what the cell voltage actually settles to after it stops charging. The values in upower currently only seem to show the current voltage, not any set thresholds.

Now that I think about it, I suppose it shouldn’t be too difficult to patch the kernel to add sysfs files for these settings that could more safely and easily set these values and read them back for use in upower or similar (if it even has any kind of support for reading charge thresholds). Maybe I’ll have a go at that at some point… certainly not today though.

2 Likes

Yes, your observation is indeed correct and inserted BPP-L503 battery voltage should never be set above 4.208V. And, IMHO, your calculations should be based on battery cell voltage of 3.7V and not based on some other LiHV 1S battery voltage value:

@twiggy, thanks for this thread!

1 Like

OK, now I understand, and agree. The cell in my device is rated for 3.8V nominal with a max voltage of 4.35V, but I don’t recommend actually doing that as it maybe out-of-spec for the components as designed by Purism.

Also, the 3840mV is a fixed value from the chip datasheet, not based on cell chemistry.

1 Like

Starting with kernel 6.2, you’re able to do all that without having to dangerously fiddle with i2c registers:

echo 500000 | sudo tee /sys/class/power_supply/bq25890-charger/constant_charge_current
echo 4000000 | sudo tee /sys/class/power_supply/bq25890-charger/constant_charge_voltage

The above will limit charging current to about 500mA and set the cutoff voltage to 4.0V. This way you don’t have to worry about setting too high values, as the kernel will automatically clamp it to the maximum specified in the device tree.

Input current limit (which is a different thing than charging current!) can be changed in the same way on older kernels already.

8 Likes

Oh, that is a much better option, and exactly what I was thinking of when I mentioned patching the kernel to add sysfs files. Glad to see I won’t have to do it myself!

1 Like

thx @dos, is kernel 6.2 out? if not, when do you think it will be released?

The kernel has release candidate status. will probably last until mid february. Until it is here in the repository probably a few more days.

The RC kernel is already in the repos, but not installed by default for obvious reasons. Once it’s released as stable upstream, it’s going to be pushed out as a regular update.

1 Like

I assume this no longer works?

I need to reduce the charge current to prevent my Librem 5 from overheating and blinking red when charging.

Try changing the part of the commands that read “/sys/class/power_supply/bq25890-charger/” to “/sys/class/power_supply/bq25890-charger-0/” (added ‘-0’ to the end, before the last slash). Not sure why it would change, but that’s the path that exists and works on mine. If that doesn’t work, check what does exist in that path:
ls /sys/class/power_supply/

That said, in my experience charging doesn’t seem to produce significant heat (at least for me). I have charged mine at ~1.5 Amps and it was barely warm to the touch, and temperature sensor reported low 30s °C.

You might also try watching the CPU load while it’s charging and make sure some process isn’t working the CPU hard in the background. I think some chargers can trigger a bug that causes it to spam messages in the kernel log, which will make the CPU heat up.

I think I’ll eventually write up another long technical post about power saving techniques I use, but one simple test you can try is to force the CPU to run at a lower speed (I run mine like this nearly all the time) and see if that keeps the temperature under control:
(as root)
echo 1000000 > /sys/devices/system/cpu/cpufreq/policy0/scaling_max_freq

(or with sudo)
echo 1000000 | sudo tee /sys/devices/system/cpu/cpufreq/policy0/scaling_max_freq

Note: this will reduce performance, but in my experience, it’s not too bad. Should also save a little power when running on battery.

To undo, change 1000000 to 1500000, or just reboot.

3 Likes

Can you post that in my workaround meta thread?

I was starting to worry I had a hardware issue because thats exactly what happens, some chargers overheat it, but the two I use at my home don’t. Perhaps in the workarond thread you could also share what chargers seem to work for you and ill do the same? Thanks!

It is better to follow @amarok’s suggestion.