Desire Z undervolt: a battery kernel next to CONFIG_JESUS_PHONE
Contents
The whole job, in the order it happened: twelve minutes of reading kernel source and writing a plan, a defconfig, an undervolt, three build failures, seven hours in which nobody touched it, a fourth build failure, a successful build, one fastboot flash boot, and a phone that came back up on a kernel I had compiled myself. The phone is my HTC Desire Z, the 2010 keyboard slider that runs CyanogenMod 7 (Android 2.3) in this series, and that I had just told, in so many words, to put its battery ahead of everything else. Speed second. Fun, apparently, third.
This post is the kernel half of that request: what I changed in the CPU voltage table, the idle settings and the config, what the build refused to let me change, and the one setting I took back within the hour. I went in expecting the hard part to be the voltages. The voltages were the easy part. Almost everything that fought back was a debug option.
The neighbour: CONFIG_JESUS_PHONE#
The tree is the Kali- kernel fork for MSM7x30 phones (the Desire Z's chip is an MSM7230), Linux 2.6.35. Its clock driver, arch/arm/mach-msm/acpuclock-7x30.c, carries two frequency tables. With CONFIG_ACPUCLOCK_OVERCLOCKING on, the CPU gets ten steps from 245 MHz to 1516.8 MHz. With one more option on top of that, it gets three more:
#ifndef CONFIG_JESUS_PHONE
{ 10, CPUFREQ_TABLE_END },
#else
/* Just an example of some of the insanity I was able to pull off on my
device */
{ 10, 1612800 },
{ 11, 1708800 },
{ 12, 1804800 },
{ 13, CPUFREQ_TABLE_END },
#endif
The voltages to match:
#ifdef CONFIG_JESUS_PHONE
{ 1612800, PLL_2, 3, 0, 192000000, 1400, VDD_RAW(1400) },
{ 1708800, PLL_2, 3, 0, 192000000, 1400, VDD_RAW(1400) },
{ 1804800, PLL_2, 3, 0, 192000000, 1450, VDD_RAW(1450) },
#endif
The Kconfig help for the option is one line: "Enable support for unstable CPU frequencies". I have read a lot of marketing for a lot of chips, and that is the most honest feature description I have met in any of them. The CyanogenMod defconfig in the same tree leaves it off, and so did I.
For scale, with the usual rule of thumb that dynamic power goes with voltage squared times frequency: 1804.8 MHz at 1450 mV against the plain table's top step, 806.4 MHz at 1100 mV, is (1450/1100)² × (1804.8/806.4), about 3.9 times the dynamic power. That is an estimate on paper; nobody in this story ran it, least of all on a 1300 mAh battery.
Going the other way: the voltage table#
My direction was down. The CyanogenMod defconfig in this tree turns overclocking on and sets the frequency ceiling to 1024 MHz. Mine turns overclocking off, which leaves the plain table with four CPU steps, and caps the ceiling at the top one:
CONFIG_MSM_CPU_FREQ_MAX=806400
CONFIG_MSM_CPU_FREQ_MIN=245000
Then the undervolt, which is a C change, not a config one. Three of its rows against the plain table, as applied:
- { 245760, PLL_3, 5, 2, 61440000, 1000, VDD_RAW(1000) },
- { 368640, PLL_3, 5, 1, 122800000, 1050, VDD_RAW(1050) },
- { 768000, PLL_1, 2, 0, 153600000, 1100, VDD_RAW(1100) },
+ { 245760, PLL_3, 5, 2, 61440000, 975, VDD_RAW(975) },
+ { 368640, PLL_3, 5, 1, 122800000, 1000, VDD_RAW(1000) },
+ { 768000, PLL_1, 2, 0, 153600000, 1050, VDD_RAW(1050) },
The 806.4 MHz step went from 1100 mV to 1075 mV, and the slow internal steps below 245 MHz from 1000 mV to 950 mV. Per step that is 25 to 50 mV, which by the same V² rule is roughly 5 to 10 percent less dynamic power at the same clock. Not heroic. The overclock table in the same file already runs its 245 and 368 MHz steps at 900 mV, so my "aggressive" undervolt is still more cautious than the table someone else wrote for going fast.
The voltages have to be multiples of 25 mV, and the driver enforces that in a way I enjoyed:
/* Cause a compile error if the voltage is not a multiple of the step size. */
#define MV(mv) ((mv) / (!((mv) % V_STEP)))
/* mv = (750mV + (raw * 25mV)) * (2 - VREF_SEL) */
#define VDD_RAW(mv) (((MV(mv) / V_STEP) - 30) | VREG_DATA)
V_STEP is 25 here. For 975, 975 % 25 is 0, !0 is 1, and MV returns 975. For 980, the remainder is 5, !5 is 0, and the macro divides by zero. The table is a static initializer, so that expression has to be a compile-time constant, and the build stops. A wrong voltage never reaches the regulator. The second comment also says where the scale starts: raw 0 is 750 mV, so 950 mV is not a hardware floor, just as far as I chose to go.
Idle: from apps sleep to power collapse#
Voltage under load is one axis. The other is what the CPU does when it has nothing to do. The stock defconfig:
CONFIG_MSM7X00A_IDLE_SLEEP_MODE=2
CONFIG_MSM7X00A_IDLE_SLEEP_MIN_TIME=20000000
CONFIG_MSM7X00A_IDLE_SPIN_TIME=80000
Mine:
CONFIG_MSM7X00A_IDLE_SLEEP_MODE=1
CONFIG_MSM7X00A_IDLE_SLEEP_MIN_TIME=5000000
CONFIG_MSM7X00A_IDLE_SPIN_TIME=20000
In this tree's Kconfig, mode 2 is "Apps Sleep" and mode 1 is "Power collapse", described as a "Sleep state that returns through reset vector": the CPU loses its state and comes back through the reset path. pm.c uses the number as the deepest state idle may pick. Both times are in nanoseconds: the minimum idle time before a low-power state drops from 20 ms to 5 ms, and the spin before it from 80 µs to 20 µs. The spin loop shifts the value right by ten and waits one microsecond per count, so in practice that is 78 polls of the interrupt line before, 19 after.
Two things I did not have to do. The driver already drops the CPU to 192 MHz (the AXI bus speed) on every wait-for-interrupt and before every power collapse: msm_acpu_clock_init sets both wfi_ramp_down and pwrc_ramp_down to 1. And I also switched on CONFIG_CPU_IDLE and CONFIG_PM_RUNTIME, the generic idle and runtime-suspend frameworks. I never measured what either did on this board, so I claim nothing for them.
What the build would not let me remove#
The defconfig I ended with differs from CyanogenMod's on 51 lines. Most of that is the plan going through as written: overclocking off, IPv6 and most netfilter matches off, the idle and governor settings above, a row of debug options off. Some of those debug options came straight back, because the build would not compile without them.
DEBUG_FS:smd_debug.cdefines its sleep-stat functions unconditionally but registers them only inside aDEBUG_FSblock, so with debugfs off they became unused, and-Werrorturned unused into fatal.WAKELOCK_STAT: code outside the#ifdefcallswakelock_stats_show, which only exists inside it.MSM_IDLE_STATS: re-enabled in the same pass, as a precaution after the first two.
The other two failures were the era showing through: alignment.c tripped a maybe-uninitialized warning from the Linaro GCC 4.9 toolchain, which CONFIG_FORCE_WARNING_AS_ERROR made fatal (I turned that option off), and kernel/timeconst.pl uses defined(@array), which modern Perl no longer accepts.
I keep a list of settings that a build lets you change only in theory. This one build gave it two entries, and I added a third on suspicion: all three with "stat" or "debug" in the name, all switched off in the defconfig and switched back on before the first image that linked. The battery-first kernel ships with its debugfs, because the debugfs code was holding up something else.
The flash, and what the phone said back#
With a 1.8 MB zImage, I dumped the phone's boot partition as a backup, repacked the image with the new kernel and the original ramdisk, and flashed it from fastboot. It booted on the first try, and /proc/version named my build: 2.6.35.14-cyanogenmod-gc5198968-dirty, compiled with Linaro GCC 4.9.4. The frequency table:
$ adb shell "cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies" 2>&1 | tr -d '\r'
245760 368640 768000 806400
Four steps, ceiling 806.4 MHz, exactly the table I had asked for. The init script I pushed next did one thing the kernel ignored. It sets the minimum frequency to 122880, and the phone reads back:
=== Freq range ===
245760
806400
122.88 MHz is in the kernel's internal clock table but not in the cpufreq one, so the request lands on the lowest step there is, without a word of complaint. I left the line in. It does no harm, and it is a useful reminder of what asking a kernel politely gets you.
The governor I took back after 32 minutes#
On paper, the conservative governor was the battery choice: the first version of the init script tuned it to ramp up at 80 percent load and down at 20, so the CPU has to prove it needs the speed. The kernel booted with it as the default. Thirty-two minutes later I had typed that the UI felt a bit sluggish, and the governor was back to interactive, the CyanogenMod default, which climbs to a high step as soon as the load rises and drops back quickly. With the ceiling now at 806 MHz instead of 1 GHz and up, interactive's eagerness costs less than it used to. The final script says so in its first lines:
# --- CPU Governor: interactive (fast ramp-up on touch, quick drop on idle) ---
echo interactive > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# --- Frequency cap (safety, kernel should already enforce) ---
echo 806400 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
echo 122880 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq
The defconfig still says conservative. The script overrides it on every boot. I have decided that this is a layered design and not two opinions arguing.
The same script also switches Bluetooth off through rfkill, picks the BFQ I/O scheduler, stretches the VM writeback intervals, quiets the kernel log, and near the end does this:
# --- Allow screen-off freq capping from MeshLauncher ---
chmod 664 /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
chown system:system /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
MeshLauncher, the home screen I wrote for this phone, runs as the system user, UID 1000, thanks to a platform signature (the test-keys post has that story). The idea was a launcher that writes the frequency cap directly when the screen goes off. The launcher I ended up with does not cap on screen-off at all, and its battery-saver tile writes the same file through su -c anyway. I built it a ramp and it still takes the stairs.
What the home screen shows#
All of this ends up behind one swipe, on the launcher's Essentials screen:

The Power tile reads the phone's fuel-gauge numbers from sysfs: 159 mA, 100 percent and an 8-hour estimate in this capture, taken with the USB cable in. The Save tile is the blunt instrument: it sets brightness to 10 out of 255 and the screen timeout to 15 seconds, turns WiFi off and caps the CPU at 245.76 MHz, and while it is on, the Bright and Sleep tiles show "Locked" instead of a value. How those fuel-gauge numbers are read, and what they turned out to mean, is a later part.
The plan I wrote before any of this estimated 30 to 60 percent more battery life from the combined changes. I never ran the stock kernel through the same logger, so that range is still exactly what it was the day I wrote it: a forecast, sitting in a plan, looking very sure of itself. The kernel boots. The table says 806 MHz. The battery is the only part of this story that has not been asked for its opinion, and I am trying very hard not to take that personally.
Filed under: android, kernel, htc-desire-z, cyanogenmod, power-management
Read next#
- Why WiFi died after the custom kernel: twelve minutes after this kernel booted I reported WiFi dead, and that is the next part.
- Reading the fuel gauge from a home-screen tile: where the Power tile's numbers come from.
- AOSP test-keys on the Desire Z: how the launcher ended up running as the system user.
- The first part of the series: where this phone, sold as spare parts, comes in.
Sources#
- Kali- kernel for MSM7x30 (commit
c5198968d16d2b6867b982028db2790d14073ae7) --arch/arm/mach-msm/acpuclock-7x30.c(frequency and voltage tables,MV/VDD_RAW, ramp-down flags),arch/arm/mach-msm/Kconfig(JESUS_PHONEhelp text, idle sleep modes, nanosecond units),arch/arm/mach-msm/pm.c(idle spin loop),arch/arm/configs/cyanogen_msm7230_defconfig(the starting defconfig) - Linux cpufreq governors documentation -- conservative and on-demand governor behaviour
- CyanogenMod init.d wiki (archived) --
/system/etc/init.dexecution at boot