Google Pixel 6 Pro: Limiting the battery’s charge level

This post was written by eli on May 13, 2024
Posted Under: Android,Linux kernel

Introduction

This post is a spin-off from another post of mine. It’s the result of my wish to limit the battery’s charge level, so it doesn’t turn into a balloon again.

I’ve written this post in chronological order (i.e. in the order that I found out things). If you’re here for the “what do I do”, just jump to “The Google pixel way”.

I assume that you’re fine with adb and that your Pixel phone is rooted.

Failed attempts to use an app

I installed Charge Control (version 3.5) which appears to be the only app of this sort available for my phone in Google Play. It’s a bit scary to install an app with root control, but without root it can’t possibly work. It’s a nice app, with only one drawback: It doesn’t work, at least not on my phone. The phone nevertheless went on charging up to 100% despite the (annoying) notification that charging is disabled. I also tried turning off the “Adaptive Battery” and “Adaptive Charging” features but that made no difference. So I gave this app the boot.

I first wanted Battery Charge Limit app at Google Play. This app is announced at XDA developers (which is a good sign, if 2.9k reviews isn’t good enough), and has a long trace of versions. Even better, this app’s source is published at Github, which is how I found out that it hasn’t been updated since 2020. It was kicked off in 2017, according to the same repo. Unfortunately, as this app isn’t maintained, so it doesn’t support recent phones. Not mine, for sure.

The Google pixel way

Based upon this post in XDA forums, I found the way to actually limit the charging level. This is based upon this kernel commit on a driver that is specific to Google devices. This driver, google_charger.c, is not part of the kernel itself, but is included as an external kernel module. I’m therefore not sure exactly which version of this driver is used on my phone. However, as I’ve identified the kernel as slightly before “12.0.0 r0.36″, I suppose the driver is more or less in that region too, as it appears in the msm git repo as drivers/power/supply/google/google_charger.c.

git clone https://android.googlesource.com/kernel/msm

Now to some hands-on: I do this directly with adb, as root (more about adb on my other post):

# cd /sys/devices/platform/google,charger
# ls -F
bd_clear             bd_resume_temp   bd_trigger_voltage  of_node@
bd_drainto_soc       bd_resume_time   charge_start_level  power/
bd_recharge_soc      bd_temp_dry_run  charge_stop_level   subsystem@
bd_recharge_voltage  bd_temp_enable   driver@             uevent
bd_resume_abs_temp   bd_trigger_temp  driver_override
bd_resume_soc        bd_trigger_time  modalias
# cat charge_start_level
0
# cat charge_stop_level
100

This sysfs directory belongs to the google_charger kernel module (i.e. it’s listed on lsmod).

As one would expect, charging is disabled when the battery’s level equals or is above charge_stop_level, and resumes when the battery level equals or is below charge_start_level. The driver ignores both values if they are 0 and 100, or else the phone would reach 0% before starting to charge.

The important point is: If you change charge_stop_level, don’t leave charge_start_level at zero, or the battery will be emptied.

For my own purposes, I went for charge levels between 70% and 80%:

# echo 70 > charge_start_level
# echo 80 > charge_stop_level

When charging is disabled this way, the phone clearly gets confused about the situation, and the information on the screen is misleading.

What actually happens is that if the charging level is above the stop level (80% in my case), the phone will discharge the battery, as if the power supply isn’t connected at all.

When the level reaches the stop level from above, the phone behaves as if the power supply was just plugged in (with a graphic animation and sound) and then goes back and forth between “charging slowly/rapidly” or it just shows the percentage. But it doesn’t charge the battery. Judging by the very slow discharging rate, the external power is used to run the phone, and the battery is just left on its own. Which is ideal: It’s equivalent to storing the battery within its ideal charging percentage for that purpose.

So the charging level will not oscillate all the time. Rather, it will more or less dwell at a random level between 70% and 80% each time, with a very slow descent.

When disconnecting the phone from external power, the phone works on battery of course, and will discharge. If it reaches below 70%, it will go up to 80% on the next opportunity to charge. If it doesn’t reach that level, it sometimes remains where it was after connection to external power, and sometimes it goes up to 80% anyhow. Go figure.

The “Ampere” app that I use to get info about the battery gets confused as well, saying “Not charging” most of the time, but often contradicts itself. I don’t blame it.

As for the regular Android settings in relation to the battery: Battery saver off, Adaptive Battery off and Adaptive Charging off. Not that I think these matter.

The relevant driver emits messages to the kernel log, so it looked like this when the charging level was 66% and I set charge_stop_level to 67:

# dmesg | grep google_charger
[ ... ]
[14866.762578] google_charger: MSC_CHG lowerbd=0, upperbd=67, capacity=66, charging on
[14866.771714] google_charger: usbchg=USB typec=null usbv=4725 usbc=882 usbMv=5000 usbMc=900
[14896.794038] google_charger: MSC_CHG lowerbd=0, upperbd=67, capacity=66, charging on
[14896.799065] google_charger: usbchg=USB typec=null usbv=4725 usbc=872 usbMv=5000 usbMc=900
[14923.236443] google_charger: MSC_CHG lowerbd=0, upperbd=67, capacity=67, lowerdb_reached=1->0, charging off
[14923.236543] google_charger: MSC_CHG disable_charging 0 -> 1
[14923.247057] google_charger: usbchg=USB typec=null usbv=4725 usbc=880 usbMv=5000 usbMc=900
[14923.286307] google_charger: MSC_CHG fv_uv=4200000->4200000 cc_max=4910000->0 rc=0
[14926.809992] google_charger: MSC_CHG lowerbd=0, upperbd=67, capacity=67, charging off
[14926.811376] google_charger: usbchg=USB typec=null usbv=4975 usbc=0 usbMv=5000 usbMc=900
[14953.952837] google_charger: MSC_CHG lowerbd=0, upperbd=67, capacity=67, charging off
[14953.954431] google_charger: usbchg=USB typec=null usbv=4725 usbc=0 usbMv=5000 usbMc=900
[15046.115042] google_charger: MSC_CHG lowerbd=0, upperbd=67, capacity=67, charging off
[15046.117563] google_charger: usbchg=USB typec=null usbv=4975 usbc=0 usbMv=5000 usbMc=900

As with all settings in /sys/, this is temporary until next boot. A short script that runs on boot is necessary to make this permanent. I haven’t delved into this yet, and I’m not sure I really want it to be permanent. It’s actually a good idea to be able to restore normal behavior just by rebooting. Say, if I realize that I’m about to have a long day out with the phone, just reboot and charge to 100% in the car.

The Tasker app and a Magisk module have been mentioned as possible solutions. Haven’t looked into that. To me, it would be more natural to add a script that runs on system start, but I am yet to figure out how to do that.

And by the way, there’s also a google,battery directory, but with nothing interesting there.

And then I read the source

Wanting to be sure I’m not messing up something, I read through google_charger.c as of tag android-12.0.0_r0.35 (commit ID 44d65f8296b034061d76efb5409c3bf4d7dc1272, to be accurate). I’m not 100% sure this is what is running on my phone, however the code that I related to has no changes for at least a year in either direction (according to the git repo).

I should mention that google_charger.c is a bit of a mess. The signs of hacking without cleaning up afterwards are there.

Anyhow, I noted that setting charge_start_level and/or charge_stop_level disables another mechanism, which is referred to as Battery Defender. This mechanism stops charging in response to a hot battery.

The thing is that if a battery becomes defective, the only thing that prevents it from burning is that charging stops in response to a high temperature reading. So can turning off Battery Defender have really unpleasant consequences?

After looking closely at chg_run_defender(), my conclusion is that Battery Defender only halts charging when the battery’s voltage exceeds bd_trigger_voltage and when the average temperature is above bd_trigger_temp. According the values in my sysfs, this is 4.27V (i.e. 100% full and beyond) and 35.0°C.

All other bd_* parameters set the condition for resumption of charging.

In other words, this mechanism wouldn’t kick in anyhow, because the battery voltage is lower than this. So limiting the battery’s charge this way poses no additional risk.

I surely hope there’s another mechanism that stops charging if the battery gets really hot. I didn’t find such (but didn’t really look much for one).

The rest of this post consists of really messy jots.

Dissection notes

These are random notes that I took as I read through the code. Written as I went, so not necessarily fully accurate.

  • “bd” stands for Battery Defender.
  • chg_work() is the battery charging work item. It calls chg_run_defender() for updating chg_drv->disable_pwrsrc and chg_drv->disable_charging, in other words, to implement the Battery Defender.
  • chg_is_custom_enabled() returns true when charge_stop_level and/or charge_start_level have non-default values that are legal.
  • bd_state->triggered becomes one when the temperature (of the battery, I presume) exceeds a trigger temperature (bd_trigger_temp) AND when the battery’s voltage exceeds the trigger voltage (bd_trigger_voltage). This is done in bd_update_stats().
  • In chg_run_defender(), if chg_is_custom_enabled() returns true, bd_reset() is called if bd_state->triggered is true, hence zeroing bd_state->triggered. In fact, the entire trigger mechanism.
  • bd_work() is a work item that merely runs in the background after disconnect for the purpose of resetting the trigger (according to the comment). No more than this.

In conclusion, setting the charging levels disables the mechanism that disables charging based upon temperature. chg_run_defender() uses the charging levels instead of the mechanism that is based upon temperature.

More dissection of chg_run_defender():

  • chg_run_defender() is the only caller to chg_update_charging_state(). The latter sets the battery’s physical charging state with a GPSY_SET_PROP() call (which is a wrapper macro for power_supply_set_property()), setting the POWER_SUPPLY_PROP_SAFETY_TIMER_ENABLE property to the value of !disable_charging. Someone out there probably knows why this does the trick.
  • As a comment says, bd_state->triggered = 1 when charging needs to be disabled. So “trigger” is the term representing the fact that charging needs to be turned off for the sake of saving the battery.
  • bd_reset() clears @triggered as well as the other state variables (in particular temperature averaging). It also assigns bd_state->enabled with a “true” or “false” value, depending on whether the bd_* parameters allow that. In other words, it disables Battery Defender if the parameters are unfit. See listing below.
  • bd_update_stats() measures and averages the battery’s temperature. If the average temperature exceeds bd_trigger_temp, the @triggered is asserted. If the temperate is below bd_resume_abs_temp, bd_reset() is called. But there’s a plot twist: If the battery’s voltage is below bd_trigger_voltage, bd_update_stats() returns without doing anything, and the same goes if bd_state->enabled is false. The latter is obvious, but the former means that if the battery’s voltage is below 4.27V in my case, there’s no trigger based upon temperature. It’s worth citing the code snippet:
    	/* not over vbat and !triggered, nothing to see here */
    	if (vbatt < bd_state->bd_trigger_voltage && !triggered)
    		return 0;
  • bd_update_stats() is the only function that can change bd_state->triggered to true. In other words, no trigger as long as the battery voltage is below 4.27V.
  • If chg_is_custom_enabled() returns true, the chg_run_defender() ignores the trigger is ignored anyhow.
  • The Battery Defender is active only if chg_drv->bd_state.enabled is true.

As bd_reset() has a crucial role, here it is. Note that the parameters are the same as published in /sys/devices/platform/google,charger/.

static void bd_reset(struct bd_data *bd_state)
{
	bool can_resume = bd_state->bd_resume_abs_temp ||
			  bd_state->bd_resume_time ||
			  (bd_state->bd_resume_time == 0 &&
			  bd_state->bd_resume_soc == 0);

	bd_state->time_sum = 0;
	bd_state->temp_sum = 0;
	bd_state->last_update = 0;
	bd_state->last_voltage = 0;
	bd_state->last_temp = 0;
	bd_state->triggered = 0;

	/* also disabled when externally triggered, resume_temp is optional */
	bd_state->enabled = ((bd_state->bd_trigger_voltage &&
				bd_state->bd_recharge_voltage) ||
				(bd_state->bd_drainto_soc &&
				bd_state->bd_recharge_soc)) &&
			    bd_state->bd_trigger_time &&
			    bd_state->bd_trigger_temp &&
			    bd_state->bd_temp_enable &&
			    can_resume;
}

Current content of parameters

As read from /sys/devices/platform/google,charger/:

bd_drainto_soc:80
bd_recharge_soc:79
bd_recharge_voltage:4250000
bd_resume_abs_temp:280
bd_resume_soc:50
bd_resume_temp:290
bd_resume_time:14400
bd_temp_dry_run:1
bd_temp_enable:1
bd_trigger_temp:350
bd_trigger_time:21600
bd_trigger_voltage:4270000
charge_start_level:70
charge_stop_level:80
driver_override:(null)
modalias:of:Ngoogle,chargerT(null)Cgoogle,charger
uevent:DRIVER=google,charger
OF_NAME=google,charger
OF_FULLNAME=/google,charger
OF_COMPATIBLE_0=google,charger
OF_COMPATIBLE_N=1

Other notes

  • The Battery Defender was apparently added on commit ID 2859390b7e66fc2e1dc097f475a16423182585e2 (the commit is however titled “google_battery: sync battery defend feature”).
  • The voltage at 100% is 4.395V while connected to charger, but not charging (?), 4.32V after disconnecting the charger. The voltage while charging is higher.
  • For 80% it’s 4.156V. For 79% its 4.145V. For 70% it’s 4.048V.
  • All voltage measurements are according to the Ampere app, but they can be read from /sys/class/power_supply/battery/voltage_now (given in μV).

Add a Comment

required, use real name
required, will not be published
optional, your blog address