HTC Desire Z battery drain in milliamps, straight from sysfs

The battery icon said 100% for the whole session, and it was telling the truth. The phone was on a USB cable and full, so 100% was the correct answer to a question I had stopped asking. The number I wanted sat one layer down. In one 70-second stretch of my log, the phone's estimated time left on a full charge went from 6.8 hours to 15.3 and the percentage did not twitch. I have a soft spot for a gauge that is accurate and useless at the same time. This one gets a place on the shelf.

The phone is an HTC Desire Z from 2010, running CyanogenMod 7 (Android 2.3, a community build) on a kernel I had just rebuilt for battery life, under a home screen I wrote myself. I wanted to know whether the rebuild did anything, and Android 2.3 offers a percentage and nothing finer. This post is about where the finer number lives: a text file the kernel exports in sysfs, what its fields turned out to mean, and which of my measurements are worth the name. The tile that reads it on my home screen gets its own post.

smem_text: 41 lines nobody asked for#

The battery driver's directory, /sys/class/power_supply/battery/, holds the usual capacity and batt_vol, plus two files that are not in any Android documentation I know of: smem_raw and smem_text. The second one is the readable one. This is its content from the first read, on USB, at 100%, read with a plain adb shell cat, no su. These are the first 25 of 41 lines, verbatim:

batt_id: 2
batt_vol: 4205
batt_vol_last: 0
batt_temp: 609
batt_current: 237
batt_current_last: 0
batt_discharge_current: 198
VREF_2: 0
VREF: 1249
ADC4096_VREF: 65483
Rtemp: 76
Temp: 307
Temp_last: 0
pd_M: 54
MBAT_pd: 4259
I_MBAT: 12
pd_temp: 0
percent_last: 100
percent_update: 100
dis_percent: 100
vbus: 1
usbid: 1
charging_source: 1
MBAT_IN: 1
full_bat: 1300000

The other 16 run from eval_current to hv_enabled, mostly charger state.

Where the numbers come from is in htc_battery.c. On the Desire Z's board file the battery platform data says .guage_driver = GUAGE_MODEM (HTC's spelling): the gauging happens on the modem side, not in a Linux driver. The modem fills a struct in shared memory, SMEM_BATT_INFO, and the source carries the comment that this struct "is allocated by A9 after first A2M RPC is sent", A9 being the ARM9 modem core. smem_text formats that struct with one scnprintf and registers the file as S_IRUGO, readable by anyone. No root, no permission, no API.

The source matches the phone line for line, as long as it is the right branch of the Kali- fork, the GitHub kernel tree I built from. Its default branch is an older 2.6.32 tree whose smem_text formats 40 fields. The android-msm-2.6.35 branch at commit c5198968, the one in my kernel's version string, formats all 41, in the phone's order, ending with hv_enabled. I count fields now before I count on them.

Which fields mean what, and which only sound like it#

HTC never documented these, so the readings below are mine, checked against the phone:

FieldFirst readHow I read it
batt_discharge_current198device draw in mA
batt_current237current into the battery while charging, mA
dis_percent100the percentage
batt_vol4205cell voltage, mV
charging_source10 battery, 1 USB, 2 AC
full_bat1300000full capacity; 1,300 mAh in µAh, matching the 1,300 mAh battery in my device notes
Temp307battery temperature, 30.7 °C

The row I enjoy most is the one I left out. batt_temp in smem_text read 609. The plain sysfs file batt_temp, in the same directory and in the same read, said 307, and so did the smem_text field called Temp. The field named after the temperature does not hold the temperature, and it goes on my list of things that are named for one number and hold another. I would have plotted 60.9 °C and called the phone a hazard.

batt_discharge_current is also not in the sysfs directory as its own file. cat on /sys/class/power_supply/battery/batt_discharge_current returned No such file or directory, so it exists only as a line inside smem_text, and everything I built reads it from there.

On a cable all day: what the current actually measures#

The plan, said out loud before any of this was built, was to measure what the phone draws through USB, not to run the battery down. So nearly every number I have was taken on a cable at 100%, and I had to know whether batt_discharge_current means the phone's own draw or the battery's flow. The battery was full and charging, which should push battery flow to zero or below. The field said 198.

The best evidence came by accident. Late in the logging, the cable came out for less than a minute. The logger's CSV caught it (columns: time, mA from batt_discharge_current, mV, percent, source, CPU MHz, estimated hours):

11:47:13,86,4208,100,1,245,15.1
11:47:19,86,4208,100,0,245,15.1
11:47:29,86,4208,100,0,368,15.1
11:47:39,86,4208,100,0,245,15.1
11:47:49,88,4168,100,0,245,14.8
11:47:59,88,4168,100,1,245,14.8

The source column flips from 1 to 0 and back. The current stays at 86-88 mA through all of it, while the voltage drops 40 mV a few samples in. A field that tracked current through the battery would have swung by the charging current. This one did not move, so I read it as the phone's own consumption, cable or no cable. That reading rests on one unplug, and I know exactly how thin one unplug is.

The shell script that never ran#

My first tool for this was a shell script for the phone's terminal, pwr. It pulls the same fields out of smem_text with grep and cut, and it contains this line:

USB_DRAW=$((CUR + DIS))

Charging current plus device draw, labelled as the draw from USB. It is a reasonable guess that I never checked against a USB meter. I never checked it against anything, in fact: within a minute the job moved to a home-screen tile, and I have no record of pwr ever running on the phone. It sits in the repo with its arithmetic, untested. Of all the code in this post, it is the only piece that has never once been wrong.

What the numbers actually show, and what they don't#

Everything the logger recorded, honestly sorted:

  • On USB, at 100%, screen on: 85-191 mA. The 191 was the first sample after a reboot; within about a minute it settled at 85-88 mA, idle on the home screen.
  • On USB, airplane mode on, WiFi, GPS and Bluetooth off, brightness Low: 112-120 mA in the sample the tile post shows. A screenshot of the same screen two minutes earlier read 159 mA.
  • On battery, late that night, at 93%, with adb connected over WiFi: 140-162 mA.
  • Screen off: never measured. I have no screen-off number at all.

The CPU column in the same CSVs jumps between 245 and 806 MHz from one ten-second sample to the next. Earlier that day I had switched the governor from conservative to interactive because the interface felt sluggish, a switch the undervolt post covers. At idle the current barely followed the frequency: 120 mA at 245 MHz and 93 mA at 806 MHz sit next to each other in one sample.

The proper test was a full drain on battery. I unplugged for it, plugged back in about fifty minutes later to chase a 3D rendering bug, and never pulled that log. So the post that set out to measure battery life contains no battery life. It contains the instrument, calibrated against one pulled cable, and a field called batt_temp that I now treat as a personal enemy.

Filed under: android, java, htc-desire-z, cyanogenmod, power-management, gingerbread

Sources#