Skip to content

feat: lower CPU freq on idle, add HalPowerManager#852

Merged
osteotek merged 21 commits intocrosspoint-reader:masterfrom
ngxson:xsn/idle_cpu_freq
Feb 18, 2026
Merged

feat: lower CPU freq on idle, add HalPowerManager#852
osteotek merged 21 commits intocrosspoint-reader:masterfrom
ngxson:xsn/idle_cpu_freq

Conversation

@ngxson
Copy link
Contributor

@ngxson ngxson commented Feb 12, 2026

Summary

Continue my experiment from #801

This PR add the ability to lower the CPU frequency on extended idle period (currently set to 3 seconds). By default, the esp32c3 CPU is set to 160MHz, and now on idle, we can reduce it to just 10MHz.

Note that while this functionality is already provided by esp power management, the current Arduino build lacks of this, and enabling it is just too complicated (not worth the effort compared to this PR)

Update: more info in #852 (comment)

Testing

Pre-condition for each test case: the battery is charged to 100%, and is left plugged in after fully charged for an extra 1 hour.

The table below shows how much battery is used for a given duration:

case / duration 6 hrs 12 hrs
delay(10) 26% 48%
delay(50), PR #801 20% Not tested
delay(50) + low CPU freq (This PR) Not tested 25%
delay(10) + low CPU freq (1) Not tested Not tested

(1) I decided not to test this case because it may not make sense. The problem is that CPU frequency vs power consumption do not follow a linear relationship, see this as an example. So, tight loop (10ms) + lower CPU freq significantly impact battery life, because the active CPU time is now much higher compared to the wall time.

So in conclusion, this PR improves ~150% to ~200% battery use time per charge.

The projected battery life is now: ~36-48 hrs of reading time (normal reading, no wifi)


AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? NO

@ngxson ngxson requested a review from a team February 12, 2026 12:35
@osteotek
Copy link
Member

Does it have any noticeable difference in reading mode? Like increased time to turn pages?

@ngxson
Copy link
Contributor Author

ngxson commented Feb 12, 2026

@osteotek No I don't see any additional delays, the frequency change is instantly

osteotek
osteotek previously approved these changes Feb 12, 2026
@osteotek osteotek requested a review from a team February 12, 2026 23:15
osteotek
osteotek previously approved these changes Feb 13, 2026
@ngxson
Copy link
Contributor Author

ngxson commented Feb 15, 2026

Found a quite nice article which confirms my research: https://robdobson.com/2023/11/investigating-esp32-c3-power-management/

TL;DR: delay loop 10ms consumes 20.1mA, and 10MHz bring it all the way down to 7.4mA

The CPU is now x16 times slower, and since our loop is not an empty loop, the delay of 50ms is worth keeping in this case, as it allows the CPU to go to lower-power mode (the WFI) for most of the time

@osteotek osteotek requested a review from a team February 15, 2026 17:30
@jdk2pq
Copy link
Contributor

jdk2pq commented Feb 15, 2026

Testing this out today on my device. So far, my initial impressions are that I see no noticeable change in the on-device activities (which is GREAT 😄). If I notice anything throughout the day, I'll post back in here. Otherwise, seems great so far 👍

@jdk2pq
Copy link
Contributor

jdk2pq commented Feb 15, 2026

Testing this out today on my device. So far, my initial impressions are that I see no noticeable change in the on-device activities (which is GREAT 😄). If I notice anything throughout the day, I'll post back in here. Otherwise, seems great so far 👍

Ugh, had I waited 5 more minutes to post this...

In the first time section file creation of any EPUB, if the chapter is large (I have a book with a chapter of 279 pages), low-power mode will be entered part way through and cause a significant slow down in processing the rest of the section. Here's the relevant logs:

[13:15:55] [DBG] [PWR] Going to low-power mode
[13:15:59] [DBG] [PWR] Restoring normal CPU frequency
[13:15:59] [DBG] [ACT] Exiting activity: Home
[13:15:59] [DBG] [ACT] Entering activity: Reader
[13:15:59] [DBG] [EBP] Loading ePub: /Ja_to_ktos_inny_Septologia_3-5.epub
[13:15:59] [BMC] File does not exist: /.crosspoint/epub_3095971703/book.bin
[13:15:59] [DBG] [EBP] Cache not found, building spine/TOC cache
[13:15:59] [DBG] [BMC] Entering write mode
[13:15:59] [DBG] [BMC] Beginning content opf pass
[13:15:59] [ERR] [ZIP] Decompressed 178 bytes into 263 bytes
[13:15:59] [DBG] [EBP] Parsing content.opf: OEBPS/content.opf
[13:15:59] [DBG] [COF] Entering guide state.
[13:15:59] [DBG] [COF] Skipping non-text reference in guide: title-page
[13:15:59] [DBG] [COF] Skipping non-text reference in guide: cover
[13:15:59] [DBG] [COF] Skipping non-text reference in guide: toc
[13:15:59] [DBG] [COF] Skipping non-text reference in guide: copyright-page
[13:15:59] [DBG] [COF] Skipping non-text reference in guide: epigraph
[13:15:59] [ERR] [ZIP] Decompressed 1080 bytes into 3731 bytes
[13:15:59] [DBG] [EBP] Successfully parsed content.opf
[13:15:59] [DBG] [EBP] OPF pass completed in 301 ms
[13:15:59] [DBG] [BMC] Beginning toc pass
[13:15:59] [DBG] [EBP] Falling back to NCX TOC
[13:15:59] [DBG] [EBP] Parsing toc ncx file: OEBPS/toc.ncx
[13:15:59] [ERR] [ZIP] Decompressed 538 bytes into 1841 bytes
[13:15:59] [DBG] [EBP] Parsed TOC items
[13:15:59] [DBG] [EBP] TOC pass completed in 134 ms
[13:15:59] [DBG] [BMC] Wrote 10 spine, 7 TOC entries
[13:16:00] [DBG] [BMC] Warning: Could not find TOC entry for spine item 1: OEBPS/Text/default-info.xhtml, using title from last section
[13:16:00] [DBG] [BMC] Warning: Could not find TOC entry for spine item 3: OEBPS/Text/epi.xhtml, using title from last section
[13:16:00] [DBG] [BMC] Warning: Could not find TOC entry for spine item 7: OEBPS/Text/polecenia.xhtml, using title from last section
[13:16:00] [DBG] [BMC] Successfully built book.bin
[13:16:00] [DBG] [EBP] buildBookBin completed in 244 ms
[13:16:00] [DBG] [EBP] Total indexing completed in 679 ms
[13:16:00] [DBG] [BMC] Loaded cache data: 10 spine, 7 TOC entries
[13:16:00] [DBG] [EBP] Parsing CSS file: OEBPS/Styles/style.css
[13:16:00] [ERR] [ZIP] Decompressed 1126 bytes into 4733 bytes
[13:16:00] [DBG] [CSS] Parsed 64 rules from 4733 bytes
[13:16:00] [DBG] [EBP] Parsing CSS file: OEBPS/Styles/sgc-toc.css
[13:16:00] [ERR] [ZIP] Decompressed 138 bytes into 425 bytes
[13:16:00] [DBG] [CSS] Parsed 71 rules from 425 bytes
[13:16:00] [DBG] [CSS] Saved 71 rules to cache
[13:16:00] [DBG] [EBP] Loaded 0 CSS style rules from 2 files
[13:16:00] [DBG] [EBP] Loaded ePub: /Ja_to_ktos_inny_Septologia_3-5.epub
[13:16:00] [DBG] [ACT] Entering activity: EpubReader
[13:16:00] [DBG] [ERS] Loaded cache: 4, 0
[13:16:00] [DBG] [RBS] Recent books saved to file (3 entries)
[13:16:00] [DBG] [LOOP] New max loop duration: 1020 ms (activity: 1019 ms)
[13:16:00] [DBG] [ERS] Loading file: OEBPS/Text/001.xhtml, index: 4
[13:16:00] [SCT] File does not exist: /.crosspoint/epub_3095971703/sections/4.bin
[13:16:00] [DBG] [ERS] Cache not found, building...
[13:16:01] [ERR] [ZIP] Decompressed 56343 bytes into 198370 bytes
[13:16:01] [DBG] [SCT] Streamed temp HTML to /.crosspoint/epub_3095971703/.tmp_4.html (198370 bytes)
[13:16:01] [DBG] [CSS] Loaded 71 rules from cache
[13:16:01] [DBG] [GFX] Time = 7299 ms from clearScreen to displayBuffer
[13:16:01]   Writing frame buffer to BW RAM (48000 bytes)...
[13:16:01]   BW RAM write complete (13 ms)
[13:16:01]   Powering on display 0x1C (fast refresh)...
[13:16:01]   Waiting for display refresh...
[13:16:02]   Wait complete: fast (419 ms)
[13:16:02]   Writing frame buffer to RED RAM (48000 bytes)...
[13:16:02]   RED RAM write complete (12 ms)
[13:16:02] [DBG] [SCT] Page 0 processed
[13:16:02] [DBG] [SCT] Page 1 processed
[13:16:02] [DBG] [SCT] Page 2 processed
[13:16:02] [DBG] [SCT] Page 3 processed
[13:16:02] [DBG] [SCT] Page 4 processed
[13:16:02] [DBG] [SCT] Page 5 processed
[13:16:02] [DBG] [SCT] Page 6 processed
[13:16:02] [DBG] [SCT] Page 7 processed
[13:16:02] [DBG] [SCT] Page 8 processed
[13:16:02] [DBG] [SCT] Page 9 processed
[13:16:02] [DBG] [SCT] Page 10 processed
[13:16:02] [DBG] [SCT] Page 11 processed
[13:16:02] [DBG] [SCT] Page 12 processed
[13:16:02] [DBG] [SCT] Page 13 processed
[13:16:02] [DBG] [SCT] Page 14 processed
[13:16:02] [DBG] [SCT] Page 15 processed
[13:16:02] [DBG] [SCT] Page 16 processed
[13:16:02] [DBG] [PWR] Going to low-power mode
[13:16:02] [DBG] [SCT] Page 17 processed
[13:16:02] [DBG] [SCT] Page 18 processed
[13:16:03] [DBG] [SCT] Page 19 processed
[13:16:03] [DBG] [SCT] Page 20 processed
[13:16:03] [DBG] [SCT] Page 21 processed
[13:16:03] [INF] [MEM] Free: 161384 bytes, Total: 223468 bytes, Min Free: 142332 bytes
[13:16:03] [DBG] [SCT] Page 22 processed
[13:16:04] [DBG] [SCT] Page 23 processed
[13:16:04] [DBG] [SCT] Page 24 processed
[13:16:04] [DBG] [SCT] Page 25 processed
[13:16:05] [DBG] [SCT] Page 26 processed
[13:16:05] [DBG] [SCT] Page 27 processed
[13:16:05] [DBG] [SCT] Page 28 processed
[13:16:05] [DBG] [SCT] Page 29 processed
[13:16:06] [DBG] [SCT] Page 30 processed
[13:16:06] [DBG] [SCT] Page 31 processed
[13:16:06] [DBG] [SCT] Page 32 processed
[13:16:06] [DBG] [SCT] Page 33 processed
[13:16:07] [DBG] [SCT] Page 34 processed
[13:16:07] [DBG] [SCT] Page 35 processed
[13:16:07] [DBG] [SCT] Page 36 processed
[13:16:08] [DBG] [EHP] Text block too long, splitting into multiple pages
[13:16:08] [DBG] [SCT] Page 37 processed
[13:16:08] [DBG] [SCT] Page 38 processed
[13:16:08] [DBG] [SCT] Page 39 processed
[13:16:09] [DBG] [SCT] Page 40 processed
[13:16:09] [DBG] [SCT] Page 41 processed
[13:16:09] [DBG] [SCT] Page 42 processed
[13:16:09] [DBG] [EHP] Text block too long, splitting into multiple pages
[13:16:10] [DBG] [SCT] Page 43 processed
[13:16:10] [DBG] [SCT] Page 44 processed
[13:16:10] [DBG] [SCT] Page 45 processed
[13:16:10] [DBG] [SCT] Page 46 processed
[13:16:10] [DBG] [SCT] Page 47 processed
[13:16:10] [DBG] [SCT] Page 48 processed
[13:16:11] [DBG] [SCT] Page 49 processed
[13:16:11] [DBG] [SCT] Page 50 processed
[13:16:11] [DBG] [SCT] Page 51 processed
[13:16:11] [DBG] [SCT] Page 52 processed
[13:16:11] [DBG] [SCT] Page 53 processed
[13:16:12] [DBG] [SCT] Page 54 processed
[13:16:12] [DBG] [SCT] Page 55 processed
[13:16:12] [DBG] [SCT] Page 56 processed
[13:16:12] [DBG] [SCT] Page 57 processed
[13:16:13] [DBG] [SCT] Page 58 processed
[13:16:13] [DBG] [SCT] Page 59 processed
[13:16:13] [DBG] [SCT] Page 60 processed
[13:16:13] [INF] [MEM] Free: 163772 bytes, Total: 224444 bytes, Min Free: 84216 bytes
[13:16:14] [DBG] [SCT] Page 61 processed
[13:16:14] [DBG] [SCT] Page 62 processed
[13:16:14] [DBG] [SCT] Page 63 processed
[13:16:15] [DBG] [SCT] Page 64 processed
[13:16:15] [DBG] [SCT] Page 65 processed
[13:16:15] [DBG] [SCT] Page 66 processed
[13:16:15] [DBG] [SCT] Page 67 processed
[13:16:15] [DBG] [SCT] Page 68 processed
[13:16:16] [DBG] [SCT] Page 69 processed
[13:16:16] [DBG] [SCT] Page 70 processed
[13:16:16] [DBG] [SCT] Page 71 processed
[13:16:16] [DBG] [SCT] Page 72 processed
[13:16:17] [DBG] [SCT] Page 73 processed
[13:16:17] [DBG] [SCT] Page 74 processed
[13:16:17] [DBG] [SCT] Page 75 processed
[13:16:17] [DBG] [SCT] Page 76 processed
[13:16:18] [DBG] [SCT] Page 77 processed
[13:16:18] [DBG] [SCT] Page 78 processed
[13:16:18] [DBG] [SCT] Page 79 processed
[13:16:18] [DBG] [SCT] Page 80 processed
[13:16:19] [DBG] [SCT] Page 81 processed
[13:16:19] [DBG] [SCT] Page 82 processed
[13:16:19] [DBG] [SCT] Page 83 processed
[13:16:20] [DBG] [SCT] Page 84 processed
[13:16:20] [DBG] [SCT] Page 85 processed
[13:16:20] [DBG] [SCT] Page 86 processed
[13:16:21] [DBG] [SCT] Page 87 processed
[13:16:21] [DBG] [SCT] Page 88 processed
[13:16:21] [DBG] [SCT] Page 89 processed
[13:16:21] [DBG] [SCT] Page 90 processed
[13:16:21] [DBG] [SCT] Page 91 processed
[13:16:22] [DBG] [SCT] Page 92 processed
[13:16:22] [DBG] [SCT] Page 93 processed
[13:16:22] [DBG] [SCT] Page 94 processed
[13:16:22] [DBG] [SCT] Page 95 processed
[13:16:23] [DBG] [SCT] Page 96 processed
[13:16:23] [DBG] [SCT] Page 97 processed
[13:16:23] [DBG] [SCT] Page 98 processed
[13:16:23] [DBG] [SCT] Page 99 processed
[13:16:24] [DBG] [SCT] Page 101 processed
[13:16:24] [DBG] [SCT] Page 102 processed
[13:16:24] [DBG] [SCT] Page 103 processed
[13:16:25] [DBG] [SCT] Page 104 processed
[13:16:25] [DBG] [SCT] Page 105 processed
[13:16:25] [DBG] [SCT] Page 106 processed
[13:16:25] [DBG] [SCT] Page 107 processed
[13:16:26] [DBG] [SCT] Page 108 processed
[13:16:26] [DBG] [SCT] Page 109 processed
[13:16:26] [DBG] [SCT] Page 110 processed
[13:16:26] [DBG] [SCT] Page 111 processed
[13:16:27] [DBG] [SCT] Page 112 processed
[13:16:27] [DBG] [SCT] Page 113 processed
[13:16:27] [DBG] [SCT] Page 114 processed
[13:16:28] [DBG] [SCT] Page 115 processed
[13:16:28] [DBG] [SCT] Page 116 processed
[13:16:28] [DBG] [SCT] Page 117 processed
[13:16:29] [DBG] [EHP] Text block too long, splitting into multiple pages
[13:16:29] [DBG] [SCT] Page 118 processed
[13:16:29] [DBG] [SCT] Page 119 processed
[13:16:29] [DBG] [SCT] Page 120 processed
[13:16:29] [DBG] [SCT] Page 121 processed
[13:16:30] [DBG] [SCT] Page 122 processed
[13:16:30] [DBG] [SCT] Page 123 processed
[13:16:30] [DBG] [SCT] Page 124 processed
[13:16:30] [DBG] [SCT] Page 125 processed
[13:16:31] [DBG] [SCT] Page 126 processed
[13:16:31] [DBG] [SCT] Page 127 processed
[13:16:31] [DBG] [SCT] Page 128 processed
[13:16:31] [DBG] [SCT] Page 129 processed
[13:16:31] [DBG] [SCT] Page 130 processed
[13:16:32] [DBG] [SCT] Page 131 processed
[13:16:32] [DBG] [SCT] Page 132 processed
[13:16:32] [DBG] [SCT] Page 133 processed
[13:16:32] [DBG] [SCT] Page 134 processed
[13:16:32] [DBG] [SCT] Page 135 processed
[13:16:33] [DBG] [SCT] Page 136 processed
[13:16:33] [DBG] [SCT] Page 137 processed
[13:16:33] [DBG] [SCT] Page 138 processed
[13:16:33] [DBG] [SCT] Page 139 processed
[13:16:34] [DBG] [SCT] Page 140 processed
[13:16:35] [DBG] [SCT] Page 141 processed
[13:16:35] [DBG] [SCT] Page 142 processed
[13:16:35] [DBG] [SCT] Page 143 processed
[13:16:35] [DBG] [SCT] Page 144 processed
[13:16:35] [DBG] [SCT] Page 145 processed
[13:16:35] [DBG] [SCT] Page 146 processed
[13:16:36] [DBG] [SCT] Page 147 processed
[13:16:36] [DBG] [SCT] Page 148 processed
[13:16:36] [DBG] [SCT] Page 149 processed
[13:16:36] [DBG] [SCT] Page 150 processed
[13:16:36] [DBG] [SCT] Page 151 processed
[13:16:37] [DBG] [SCT] Page 152 processed
[13:16:37] [DBG] [SCT] Page 153 processed
[13:16:37] [DBG] [SCT] Page 154 processed
[13:16:37] [DBG] [SCT] Page 155 processed
[13:16:38] [DBG] [SCT] Page 156 processed
[13:16:38] [DBG] [SCT] Page 157 processed
[13:16:38] [DBG] [SCT] Page 158 processed
[13:16:39] [DBG] [SCT] Page 159 processed
[13:16:39] [DBG] [SCT] Page 160 processed
[13:16:39] [DBG] [SCT] Page 161 processed
[13:16:40] [DBG] [SCT] Page 162 processed
[13:16:40] [DBG] [SCT] Page 163 processed
[13:16:40] [DBG] [SCT] Page 164 processed
[13:16:41] [DBG] [EHP] Text block too long, splitting into multiple pages
[13:16:41] [DBG] [SCT] Page 165 processed
[13:16:41] [DBG] [SCT] Page 166 processed
[13:16:41] [DBG] [SCT] Page 167 processed
[13:16:41] [DBG] [SCT] Page 168 processed
[13:16:41] [DBG] [SCT] Page 169 processed
[13:16:42] [DBG] [SCT] Page 170 processed
[13:16:42] [DBG] [SCT] Page 171 processed
[13:16:43] [DBG] [EHP] Text block too long, splitting into multiple pages
[13:16:43] [DBG] [SCT] Page 172 processed
[13:16:43] [DBG] [SCT] Page 173 processed
[13:16:43] [DBG] [SCT] Page 174 processed
[13:16:43] [DBG] [SCT] Page 175 processed
[13:16:43] [DBG] [SCT] Page 176 processed
[13:16:43] [DBG] [SCT] Page 177 processed
[13:16:44] [DBG] [SCT] Page 178 processed
[13:16:44] [INF] [MEM] Free: 172144 bytes, Total: 228764 bytes, Min Free: 84216 bytes
[13:16:44] [DBG] [SCT] Page 179 processed
[13:16:44] [DBG] [SCT] Page 180 processed
[13:16:45] [DBG] [SCT] Page 181 processed
[13:16:45] [DBG] [SCT] Page 182 processed
[13:16:45] [DBG] [SCT] Page 183 processed
[13:16:45] [DBG] [SCT] Page 184 processed
[13:16:45] [DBG] [SCT] Page 185 processed
[13:16:45] [DBG] [SCT] Page 186 processed
[13:16:46] [DBG] [SCT] Page 187 processed
[13:16:46] [DBG] [SCT] Page 188 processed
[13:16:46] [DBG] [SCT] Page 189 processed
[13:16:46] [DBG] [SCT] Page 190 processed
[13:16:47] [DBG] [SCT] Page 191 processed
[13:16:47] [DBG] [SCT] Page 192 processed
[13:16:47] [DBG] [EHP] Text block too long, splitting into multiple pages
[13:16:48] [DBG] [SCT] Page 193 processed
[13:16:48] [DBG] [SCT] Page 194 processed
[13:16:48] [DBG] [SCT] Page 195 processed
[13:16:48] [DBG] [SCT] Page 196 processed
[13:16:48] [DBG] [SCT] Page 197 processed
[13:16:48] [DBG] [SCT] Page 198 processed
[13:16:49] [DBG] [EHP] Text block too long, splitting into multiple pages
[13:16:49] [DBG] [SCT] Page 199 processed
[13:16:49] [DBG] [SCT] Page 200 processed
[13:16:49] [DBG] [SCT] Page 201 processed
[13:16:50] [DBG] [SCT] Page 202 processed
[13:16:50] [DBG] [SCT] Page 203 processed
[13:16:50] [DBG] [SCT] Page 204 processed
[13:16:50] [DBG] [SCT] Page 205 processed
[13:16:51] [DBG] [SCT] Page 206 processed
[13:16:51] [DBG] [SCT] Page 207 processed
[13:16:51] [DBG] [SCT] Page 208 processed
[13:16:51] [DBG] [SCT] Page 209 processed
[13:16:51] [DBG] [SCT] Page 210 processed
[13:16:51] [DBG] [SCT] Page 211 processed
[13:16:52] [DBG] [SCT] Page 212 processed
[13:16:52] [DBG] [SCT] Page 213 processed
[13:16:52] [DBG] [SCT] Page 214 processed
[13:16:52] [DBG] [SCT] Page 215 processed
[13:16:53] [DBG] [SCT] Page 216 processed
[13:16:53] [DBG] [SCT] Page 217 processed
[13:16:53] [DBG] [SCT] Page 218 processed
[13:16:54] [DBG] [SCT] Page 219 processed
[13:16:54] [INF] [MEM] Free: 161832 bytes, Total: 223836 bytes, Min Free: 84216 bytes
[13:16:54] [DBG] [SCT] Page 220 processed
[13:16:54] [DBG] [SCT] Page 221 processed
[13:16:54] [DBG] [SCT] Page 222 processed
[13:16:55] [DBG] [SCT] Page 223 processed
[13:16:55] [DBG] [SCT] Page 224 processed
[13:16:55] [DBG] [SCT] Page 225 processed
[13:16:55] [DBG] [SCT] Page 226 processed
[13:16:56] [DBG] [SCT] Page 227 processed
[13:16:56] [DBG] [SCT] Page 228 processed
[13:16:56] [DBG] [SCT] Page 229 processed
[13:16:56] [DBG] [SCT] Page 230 processed
[13:16:57] [DBG] [SCT] Page 231 processed
[13:16:57] [DBG] [SCT] Page 232 processed
[13:16:57] [DBG] [SCT] Page 233 processed
[13:16:58] [DBG] [SCT] Page 234 processed
[13:16:58] [DBG] [SCT] Page 235 processed
[13:16:58] [DBG] [SCT] Page 236 processed
[13:16:58] [DBG] [SCT] Page 237 processed
[13:16:59] [DBG] [SCT] Page 238 processed
[13:16:59] [DBG] [SCT] Page 239 processed
[13:16:59] [DBG] [SCT] Page 240 processed
[13:16:59] [DBG] [SCT] Page 241 processed
[13:17:00] [DBG] [SCT] Page 242 processed
[13:17:00] [DBG] [SCT] Page 243 processed
[13:17:00] [DBG] [SCT] Page 244 processed
[13:17:00] [DBG] [SCT] Page 245 processed
[13:17:01] [DBG] [SCT] Page 246 processed
[13:17:01] [DBG] [SCT] Page 247 processed
[13:17:01] [DBG] [SCT] Page 248 processed
[13:17:01] [DBG] [SCT] Page 249 processed
[13:17:02] [DBG] [SCT] Page 250 processed
[13:17:02] [DBG] [SCT] Page 251 processed
[13:17:02] [DBG] [SCT] Page 252 processed
[13:17:03] [DBG] [SCT] Page 253 processed
[13:17:03] [DBG] [SCT] Page 254 processed
[13:17:03] [DBG] [SCT] Page 255 processed
[13:17:03] [DBG] [SCT] Page 256 processed
[13:17:04] [INF] [MEM] Free: 158000 bytes, Total: 222540 bytes, Min Free: 84216 bytes
[13:17:04] [DBG] [SCT] Page 257 processed
[13:17:04] [DBG] [SCT] Page 258 processed
[13:17:04] [DBG] [SCT] Page 259 processed
[13:17:04] [DBG] [SCT] Page 260 processed
[13:17:05] [DBG] [SCT] Page 261 processed
[13:17:05] [DBG] [SCT] Page 262 processed
[13:17:05] [DBG] [SCT] Page 263 processed
[13:17:06] [DBG] [SCT] Page 264 processed
[13:17:06] [DBG] [SCT] Page 265 processed
[13:17:06] [DBG] [SCT] Page 266 processed
[13:17:07] [DBG] [EHP] Text block too long, splitting into multiple pages
[13:17:07] [DBG] [SCT] Page 267 processed
[13:17:07] [DBG] [SCT] Page 268 processed
[13:17:07] [DBG] [SCT] Page 269 processed
[13:17:07] [DBG] [SCT] Page 270 processed
[13:17:07] [DBG] [SCT] Page 271 processed
[13:17:08] [DBG] [SCT] Page 272 processed
[13:17:08] [DBG] [SCT] Page 273 processed
[13:17:08] [DBG] [SCT] Page 274 processed
[13:17:09] [DBG] [SCT] Page 275 processed
[13:17:09] [DBG] [SCT] Page 276 processed
[13:17:09] [DBG] [SCT] Page 277 processed
[13:17:09] [DBG] [SCT] Page 278 processed
[13:17:09] [DBG] [SCT] Page 279 processed
[13:17:10] [DBG] [GFX] Time = 598 ms from clearScreen to displayBuffer
[13:17:10]   Writing frame buffer to BW RAM (48000 bytes)...
[13:17:10]   BW RAM write complete (116 ms)
[13:17:10]   Writing frame buffer to RED RAM (48000 bytes)...
[13:17:10]   RED RAM write complete (114 ms)
[13:17:10]   Powering on display 0xD4 (half refresh)...
[13:17:10]   Waiting for display refresh...
[13:17:12]   Wait complete: half (1578 ms)
[13:17:12]   Writing frame buffer to RED RAM (48000 bytes)...
[13:17:12]   RED RAM write complete (115 ms)
[13:17:12] [DBG] [GFX] Stored BW buffer in 6 chunks (8000 bytes each)
[13:17:12]   Writing frame buffer to BW RAM (48000 bytes)...
[13:17:12]   BW RAM write complete (117 ms)
[13:17:12]   Writing frame buffer to RED RAM (48000 bytes)...
[13:17:13]   RED RAM write complete (116 ms)
[13:17:13]   Loading custom LUT...
[13:17:13]   Custom LUT loaded
[13:17:13]   Powering on display 0x0C (fast refresh)...
[13:17:13]   Waiting for display refresh...
[13:17:13]   Wait complete: fast (62 ms)
[13:17:13]   Custom LUT disabled
[13:17:13]   Writing frame buffer to RED RAM (48000 bytes)...
[13:17:13]   RED RAM write complete (115 ms)
[13:17:13] [DBG] [GFX] Restored and freed BW buffer chunks
[13:17:13] [DBG] [ERS] Rendered page in 3514ms
[13:17:13] [DBG] [ERS] Progress saved: Chapter 4, Page 0
[13:17:14] [INF] [MEM] Free: 198380 bytes, Total: 232716 bytes, Min Free: 84216 bytes

The EPUB I was testing with came from here: #757 (comment) but any EPUB with a large number of pages in a chapter will likely cause this.

@ngxson
Copy link
Contributor Author

ngxson commented Feb 15, 2026

Hmm right, fixing it now, should not be too complicated.

Btw technically I can even make the epub loading even faster, by temporary bump the CPU speed to 240MHz, but that will be a future PR.

@ngxson
Copy link
Contributor Author

ngxson commented Feb 15, 2026

@jdk2pq sorry I just pushed one more fix, should be good now (testing on my side too, so far so good)

@jdk2pq
Copy link
Contributor

jdk2pq commented Feb 16, 2026

@jdk2pq sorry I just pushed one more fix, should be good now (testing on my side too, so far so good)

Haven't noticed any issues! I didn't notice a drain in battery % while in sleep before this change, and with this change, my battery % is still at what it was last night before I put the device to sleep, so it seems like it's working well to me!

jdk2pq
jdk2pq previously approved these changes Feb 16, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Walkthrough

Power management was migrated from HalGPIO to a new HalPowerManager. HalGPIO had deep-sleep and battery APIs removed. HalPowerManager implements CPU frequency scaling, deep-sleep coordination, battery reading, an RAII Lock, and is integrated across main and activity render loops.

Changes

Cohort / File(s) Summary
HalGPIO Reduction
lib/hal/HalGPIO.h, lib/hal/HalGPIO.cpp
Removed startDeepSleep() and getBatteryPercentage() APIs; removed esp_sleep.h include and BAT_GPIO0 pinMode initialization.
HalPowerManager Implementation
lib/hal/HalPowerManager.h, lib/hal/HalPowerManager.cpp
Added HalPowerManager singleton with begin(), setPowerSaving(bool), startDeepSleep(HalGPIO&), getBatteryPercentage(), constants LOW_POWER_FREQ and IDLE_POWER_SAVING_MS, and nested RAII Lock for guarding low-power state.
Main Integration
src/main.cpp
Initialize powerManager in setup; replace direct GPIO deep-sleep calls with powerManager.startDeepSleep(gpio); call powerManager.setPowerSaving(false) on activity/user actions and enable it after idle using IDLE_POWER_SAVING_MS.
Activity Power Locking
src/activities/.../Activity.cpp, src/activities/.../ActivityWithSubactivity.cpp
Introduce HalPowerManager::Lock (powerLock) around render loops to prevent entering low-power mode during rendering.
Activity Skip Loop Delay
src/activities/settings/ClearCacheActivity.h, src/activities/settings/OtaUpdateActivity.h
Add bool skipLoopDelay() override { return true; } to both activities to avoid loop delay and keep performance during these operations.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Activity
participant Main
participant PowerMgr as HalPowerManager
participant GPIO as HalGPIO
participant CPU
participant Battery as BatteryMonitor

Main->>PowerMgr: begin()
Activity->>PowerMgr: construct Lock() (prevent low power)
Activity->>PowerMgr: setPowerSaving(false) on user action
PowerMgr->>CPU: setCpuFrequency(normalFreq)
Note over PowerMgr,CPU: idle tracked via IDLE_POWER_SAVING_MS
PowerMgr->>CPU: setCpuFrequency(LOW_POWER_FREQ) when idle
Main->>PowerMgr: startDeepSleep(GPIO)
PowerMgr->>GPIO: wait for button release; configure wakeup
PowerMgr->>Battery: getBatteryPercentage()
PowerMgr->>GPIO: enter deep sleep

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main feature: introducing HalPowerManager and the ability to lower CPU frequency during idle periods.
Description check ✅ Passed The description clearly explains the motivation, implementation details, testing results, and battery life improvements directly related to the changeset.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@lib/hal/HalPowerManager.cpp`:
- Around line 31-33: Fix the typo in the comment that references the wrong
symbol name: change "currentLockMod" to "currentLockMode" in the comment above
the read of currentLockMode (the line referencing currentLockMod next to the
declaration const LockMode mode = currentLockMode). Ensure the comment matches
the actual variable name currentLockMode and rebuild.
- Around line 72-83: When acquiring the lock in HalPowerManager::Lock::Lock(),
after setting powerManager.currentLockMode = NormalSpeed and valid = true, also
ensure the CPU frequency is restored immediately by calling
setPowerSaving(false) if powerManager.isLowPower is true; perform this call
while holding the same modeMutex (i.e., before xSemaphoreGive) so the transition
is immediate and synchronized with currentLockMode to prevent races.

In `@lib/hal/HalPowerManager.h`:
- Around line 42-49: The Lock RAII class can be copied/moved which risks
double-release; make it non-copyable and non-movable by deleting the copy
constructor, copy assignment operator, move constructor and move assignment
operator in the Lock class declaration (class Lock) so instances cannot be
copied or moved; keep HalPowerManager friend as-is and ensure only the explicit
Lock() and ~Lock() remain public.

In `@src/activities/home/HomeActivity.cpp`:
- Line 6: Remove the unused include of HalPowerManager.h from HomeActivity.cpp:
locate the line containing `#include` <HalPowerManager.h> and delete it
(HomeActivity.cpp related to class HomeActivity), ensuring no other references
to HalPowerManager exist in HomeActivity.cpp or HomeActivity.h; run a quick
compile to confirm no missing symbols after removal.

In `@src/activities/reader/EpubReaderActivity.cpp`:
- Line 6: The HalPowerManager.h include is unused or the render() method's heavy
work (notably createSectionFile()) needs protection; either remove the
HalPowerManager include or wrap the heavy processing in render() with a
HalPowerManager::Lock to prevent CPU frequency scaling during EPUB section
creation. Locate the render() method and around the block that calls
createSectionFile() (and other expensive operations) instantiate a
HalPowerManager::Lock scoped object before the work and let it release on scope
exit, or if you choose not to use power management remove the HalPowerManager.h
include and any references to it to avoid unused headers.

In `@src/activities/reader/XtcReaderActivity.cpp`:
- Line 12: Remove the unused include: delete the line that includes
HalPowerManager.h since renderPage() does pixel rendering and the file does not
reference powerManager or HalPowerManager::Lock; ensure no other symbols from
HalPowerManager (e.g., powerManager, HalPowerManager::Lock) are referenced in
this file so the build remains clean.
🧹 Nitpick comments (2)
src/activities/settings/OtaUpdateActivity.h (1)

36-37: Gate skipLoopDelay() to active update states to avoid idle power drain.

Line 37 forces no delay even when the user is just selecting Wi‑Fi or waiting for confirmation. Consider mirroring the preventAutoSleep() condition so idle UI states still benefit from power saving.

💡 Suggested change
-  bool skipLoopDelay() override { return true; }  // Prevent power-saving mode
+  bool skipLoopDelay() override {
+    return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS;
+  }  // Prevent power-saving mode during active OTA
lib/hal/HalPowerManager.h (1)

12-13: Duplicate extern declaration.

The extern HalPowerManager powerManager; declaration appears twice (lines 13 and 52). Remove one of them to avoid redundancy.

♻️ Suggested fix
 class HalPowerManager;
-extern HalPowerManager powerManager;  // Singleton
 
 class HalPowerManager {

Keep only the declaration at line 52 (after the class definition).

Also applies to: 52-52

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6f38d4 and 82ddd8f.

📒 Files selected for processing (11)
  • lib/hal/HalGPIO.cpp
  • lib/hal/HalGPIO.h
  • lib/hal/HalPowerManager.cpp
  • lib/hal/HalPowerManager.h
  • src/activities/home/HomeActivity.cpp
  • src/activities/reader/EpubReaderActivity.cpp
  • src/activities/reader/TxtReaderActivity.cpp
  • src/activities/reader/XtcReaderActivity.cpp
  • src/activities/settings/ClearCacheActivity.h
  • src/activities/settings/OtaUpdateActivity.h
  • src/main.cpp
💤 Files with no reviewable changes (2)
  • lib/hal/HalGPIO.h
  • lib/hal/HalGPIO.cpp
🧰 Additional context used
🧬 Code graph analysis (3)
src/activities/settings/ClearCacheActivity.h (1)
src/activities/settings/OtaUpdateActivity.h (1)
  • skipLoopDelay (37-37)
lib/hal/HalPowerManager.cpp (1)
lib/hal/HalPowerManager.h (1)
  • Lock (42-49)
lib/hal/HalPowerManager.h (1)
lib/hal/HalPowerManager.cpp (10)
  • begin (13-18)
  • begin (13-13)
  • setPowerSaving (20-53)
  • setPowerSaving (20-20)
  • startDeepSleep (55-65)
  • startDeepSleep (55-55)
  • getBatteryPercentage (67-70)
  • getBatteryPercentage (67-67)
  • Lock (72-83)
  • Lock (85-91)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cppcheck
  • GitHub Check: build
🔇 Additional comments (8)
src/activities/settings/ClearCacheActivity.h (1)

16-16: LGTM!

The addition of skipLoopDelay() follows the established pattern from OtaUpdateActivity and appropriately prevents power-saving mode during cache clearing operations. This avoids potential slowdowns from CPU frequency reduction during file system I/O, which aligns with the fixes discussed in the PR for similar processing-intensive activities.

src/main.cpp (3)

6-6: Good power management integration.

The integration of HalPowerManager in main.cpp follows a clean pattern:

  • Initialized early in setup() via powerManager.begin()
  • Power saving disabled on user activity
  • Power saving enabled after IDLE_POWER_SAVING_MS of inactivity
  • Properly disabled during active operations (skipLoopDelay)

Also applies to: 287-287


396-398: LGTM: User activity resets power saving.

Correctly disables power saving on any button press/release or when an activity prevents auto-sleep. This ensures responsive behavior during user interaction.


431-443: LGTM: Power saving control in loop delay logic.

The logic properly:

  • Disables power saving when skipLoopDelay() is true (e.g., webserver active)
  • Enables power saving only after extended idle (IDLE_POWER_SAVING_MS)
  • Uses longer delay (50ms) in low-power mode vs short delay (10ms) for responsiveness
lib/hal/HalPowerManager.cpp (3)

13-18: LGTM: Initialization logic.

Properly initializes the battery GPIO, captures the baseline CPU frequency, and creates the mutex with an assertion for failure detection.


55-65: LGTM: Deep sleep implementation.

Correctly waits for power button release before arming the GPIO wakeup trigger, preventing immediate wake-up if the button is still held.


67-70: Code compiles correctly, const usage is safe.

readPercentage() is a const method on BatteryMonitor (external library from open-x4-sdk). The code successfully compiles, which proves the const usage is safe and not problematic.

BAT_GPIO0 initialization is properly sequenced: HalPowerManager::begin() calls pinMode(BAT_GPIO0, INPUT) during application setup (in src/main.cpp), before any battery reads occur.

No changes needed.

Likely an incorrect or invalid review comment.

src/activities/reader/TxtReaderActivity.cpp (1)

4-4: Remove unused include HalPowerManager.h.

#include <HalPowerManager.h> is included but never used in this file. There are no calls to powerManager or HalPowerManager::Lock anywhere in TxtReaderActivity.cpp, despite the similar include appearing in other reader activity files. Remove the include.

@ngxson
Copy link
Contributor Author

ngxson commented Feb 16, 2026

Alright, since #774 is merged, I did a final clean up pass, should be good now. Testing on my side, still working as expected (CPU is kept on performance mode during long epub parsing)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
lib/hal/HalPowerManager.h (1)

12-13: Duplicate extern declaration.

extern HalPowerManager powerManager; appears at both line 13 and line 58. While not harmful, only one declaration is needed. Consider keeping just one, typically after the class definition.

🧹 Suggested fix
 class HalPowerManager;
-extern HalPowerManager powerManager;  // Singleton
 
 class HalPowerManager {

Also applies to: 58-58

lib/hal/HalPowerManager.cpp (1)

37-40: Consider LOG_WARN for frequency change failures.

Failed CPU frequency changes are logged at DEBUG level. Since these failures could indicate a real problem (e.g., invalid frequency value), consider using LOG_WARN to ensure visibility in production.

Also applies to: 45-48

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edde1b7 and bb5ace9.

📒 Files selected for processing (2)
  • lib/hal/HalPowerManager.cpp
  • lib/hal/HalPowerManager.h
🧰 Additional context used
🧬 Code graph analysis (1)
lib/hal/HalPowerManager.cpp (1)
lib/hal/HalPowerManager.h (1)
  • Lock (42-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cppcheck
  • GitHub Check: build
🔇 Additional comments (5)
lib/hal/HalPowerManager.h (1)

42-55: LGTM!

The Lock class properly implements RAII semantics with deleted copy/move operations as addressed from previous review feedback.

lib/hal/HalPowerManager.cpp (4)

13-18: LGTM!

Initialization correctly stores the baseline CPU frequency and creates the mutex. The assert provides a fail-fast mechanism for debugging.


55-65: LGTM!

The deep sleep sequence correctly waits for button release before arming the wakeup trigger, preventing immediate wake after entering sleep.


72-95: LGTM!

The Lock implementation correctly addresses past review feedback by immediately restoring normal CPU frequency when acquired. The mutex protection and mode transitions are properly handled.


67-70: No issues found. The code compiles in a production environment and readPercentage() is called on the static const BatteryMonitor object in multiple const method contexts, confirming that BatteryMonitor::readPercentage() is properly const-qualified.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
lib/hal/HalPowerManager.h (1)

42-48: Guard against Lock usage before begin() initializes the mutex.
If a Lock is constructed before begin(), modeMutex will be null and the constructor will dereference it. Consider a defensive check or assert in the constructor to prevent a hard crash.

🛡️ Example defensive guard (apply in lib/hal/HalPowerManager.cpp)
HalPowerManager::Lock::Lock() {
  if (powerManager.modeMutex == nullptr) {
    LOG_ERR("PWR", "Lock used before begin()");
    valid = false;
    return;
  }
  // existing logic...
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bb5ace9 and f98fac8.

📒 Files selected for processing (1)
  • lib/hal/HalPowerManager.h
🧰 Additional context used
🧬 Code graph analysis (1)
lib/hal/HalPowerManager.h (1)
lib/hal/HalPowerManager.cpp (10)
  • begin (13-18)
  • begin (13-13)
  • setPowerSaving (20-53)
  • setPowerSaving (20-20)
  • startDeepSleep (55-65)
  • startDeepSleep (55-55)
  • getBatteryPercentage (67-70)
  • getBatteryPercentage (67-67)
  • Lock (72-87)
  • Lock (89-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: cppcheck
🔇 Additional comments (1)
lib/hal/HalPowerManager.h (1)

15-37: Clean API surface and constants.
The public API and the idle/low‑power constants are clear and appropriately scoped.

@osteotek
Copy link
Member

osteotek commented Feb 17, 2026

@ngxson there are other activities requiring wifi (KoReaderSyncActivity, KoReaderAuthActivity, WifiSelectionActivity, CalibreConnectActivity, CrosspointWebServerActivity, possibly others), should they also use skipLoopDelay?

@ngxson
Copy link
Contributor Author

ngxson commented Feb 17, 2026

@osteotek if wifi is turned on, the power saving will automatically be disabled (checking via WiFi.getMode())

@osteotek
Copy link
Member

Tested in various activities, seems to not to degrade performance. Haven't tested battery efficiency claims

@osteotek osteotek merged commit 6ec5fc5 into crosspoint-reader:master Feb 18, 2026
6 checks passed
ariel-lindemann pushed a commit to ariel-lindemann/crosspoint-reader that referenced this pull request Feb 19, 2026
)

## Summary

Continue my experiment from
crosspoint-reader#801

This PR add the ability to lower the CPU frequency on extended idle
period (currently set to 3 seconds). By default, the esp32c3 CPU is set
to 160MHz, and now on idle, we can reduce it to just 10MHz.

Note that while this functionality is already provided by [esp power
management](https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32c3/api-reference/system/power_management.html),
the current Arduino build lacks of this, and enabling it is just too
complicated (not worth the effort compared to this PR)

Update: more info in
crosspoint-reader#852 (comment)

## Testing

Pre-condition for each test case: the battery is charged to 100%, and is
left plugged in after fully charged for an extra 1 hour.

The table below shows how much battery is **used** for a given duration:

| case / duration | 6 hrs | 12 hrs |
| --- | --- | --- |
| `delay(10)` | 26% | 48% |
| `delay(50)`, PR
crosspoint-reader#801 | 20% |
Not tested |
| `delay(50)` + low CPU freq (This PR) | Not tested | 25% |
| `delay(10)` + low CPU freq (1) | Not tested | Not tested |

(1) I decided not to test this case because it may not make sense. The
problem is that CPU frequency vs power consumption do not follow a
linear relationship, see
[this](https://www.arrow.com/en/research-and-events/articles/esp32-power-consumption-can-be-reduced-with-sleep-modes)
as an example. So, tight loop (10ms) + lower CPU freq significantly
impact battery life, because the active CPU time is now much higher
compared to the wall time.

**So in conclusion, this PR improves ~150% to ~200% battery use time per
charge.**

The projected battery life is now: ~36-48 hrs of reading time (normal
reading, no wifi)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**
saslv pushed a commit to saslv/crosspoint-reader that referenced this pull request Feb 19, 2026
)

## Summary

Continue my experiment from
crosspoint-reader#801

This PR add the ability to lower the CPU frequency on extended idle
period (currently set to 3 seconds). By default, the esp32c3 CPU is set
to 160MHz, and now on idle, we can reduce it to just 10MHz.

Note that while this functionality is already provided by [esp power
management](https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32c3/api-reference/system/power_management.html),
the current Arduino build lacks of this, and enabling it is just too
complicated (not worth the effort compared to this PR)

Update: more info in
crosspoint-reader#852 (comment)

## Testing

Pre-condition for each test case: the battery is charged to 100%, and is
left plugged in after fully charged for an extra 1 hour.

The table below shows how much battery is **used** for a given duration:

| case / duration | 6 hrs | 12 hrs |
| --- | --- | --- |
| `delay(10)` | 26% | 48% |
| `delay(50)`, PR
crosspoint-reader#801 | 20% |
Not tested |
| `delay(50)` + low CPU freq (This PR) | Not tested | 25% |
| `delay(10)` + low CPU freq (1) | Not tested | Not tested |

(1) I decided not to test this case because it may not make sense. The
problem is that CPU frequency vs power consumption do not follow a
linear relationship, see
[this](https://www.arrow.com/en/research-and-events/articles/esp32-power-consumption-can-be-reduced-with-sleep-modes)
as an example. So, tight loop (10ms) + lower CPU freq significantly
impact battery life, because the active CPU time is now much higher
compared to the wall time.

**So in conclusion, this PR improves ~150% to ~200% battery use time per
charge.**

The projected battery life is now: ~36-48 hrs of reading time (normal
reading, no wifi)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**
el pushed a commit to el/crosspoint-reader that referenced this pull request Feb 19, 2026
)

## Summary

Continue my experiment from
crosspoint-reader#801

This PR add the ability to lower the CPU frequency on extended idle
period (currently set to 3 seconds). By default, the esp32c3 CPU is set
to 160MHz, and now on idle, we can reduce it to just 10MHz.

Note that while this functionality is already provided by [esp power
management](https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32c3/api-reference/system/power_management.html),
the current Arduino build lacks of this, and enabling it is just too
complicated (not worth the effort compared to this PR)

Update: more info in
crosspoint-reader#852 (comment)

## Testing

Pre-condition for each test case: the battery is charged to 100%, and is
left plugged in after fully charged for an extra 1 hour.

The table below shows how much battery is **used** for a given duration:

| case / duration | 6 hrs | 12 hrs |
| --- | --- | --- |
| `delay(10)` | 26% | 48% |
| `delay(50)`, PR
crosspoint-reader#801 | 20% |
Not tested |
| `delay(50)` + low CPU freq (This PR) | Not tested | 25% |
| `delay(10)` + low CPU freq (1) | Not tested | Not tested |

(1) I decided not to test this case because it may not make sense. The
problem is that CPU frequency vs power consumption do not follow a
linear relationship, see
[this](https://www.arrow.com/en/research-and-events/articles/esp32-power-consumption-can-be-reduced-with-sleep-modes)
as an example. So, tight loop (10ms) + lower CPU freq significantly
impact battery life, because the active CPU time is now much higher
compared to the wall time.

**So in conclusion, this PR improves ~150% to ~200% battery use time per
charge.**

The projected battery life is now: ~36-48 hrs of reading time (normal
reading, no wifi)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**
@coderabbitai coderabbitai bot mentioned this pull request Feb 19, 2026
lukestein pushed a commit to lukestein/crosspoint-reader that referenced this pull request Feb 20, 2026
)

## Summary

Continue my experiment from
crosspoint-reader#801

This PR add the ability to lower the CPU frequency on extended idle
period (currently set to 3 seconds). By default, the esp32c3 CPU is set
to 160MHz, and now on idle, we can reduce it to just 10MHz.

Note that while this functionality is already provided by [esp power
management](https://docs.espressif.com/projects/esp-idf/en/v4.3/esp32c3/api-reference/system/power_management.html),
the current Arduino build lacks of this, and enabling it is just too
complicated (not worth the effort compared to this PR)

Update: more info in
crosspoint-reader#852 (comment)

## Testing

Pre-condition for each test case: the battery is charged to 100%, and is
left plugged in after fully charged for an extra 1 hour.

The table below shows how much battery is **used** for a given duration:

| case / duration | 6 hrs | 12 hrs |
| --- | --- | --- |
| `delay(10)` | 26% | 48% |
| `delay(50)`, PR
crosspoint-reader#801 | 20% |
Not tested |
| `delay(50)` + low CPU freq (This PR) | Not tested | 25% |
| `delay(10)` + low CPU freq (1) | Not tested | Not tested |

(1) I decided not to test this case because it may not make sense. The
problem is that CPU frequency vs power consumption do not follow a
linear relationship, see
[this](https://www.arrow.com/en/research-and-events/articles/esp32-power-consumption-can-be-reduced-with-sleep-modes)
as an example. So, tight loop (10ms) + lower CPU freq significantly
impact battery life, because the active CPU time is now much higher
compared to the wall time.

**So in conclusion, this PR improves ~150% to ~200% battery use time per
charge.**

The projected battery life is now: ~36-48 hrs of reading time (normal
reading, no wifi)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants