{"id":536,"date":"2026-09-26T21:20:31","date_gmt":"2026-09-26T20:20:31","guid":{"rendered":"https:\/\/www.fabidouille.com\/?p=536"},"modified":"2026-10-03T19:23:26","modified_gmt":"2026-10-03T18:23:26","slug":"ezbook-10w","status":"publish","type":"post","link":"https:\/\/www.fabidouille.com\/?p=536","title":{"rendered":"Dix Watts"},"content":{"rendered":"<p>6 watts. That is all the firmware gives to the Celeron N3450 in my Jumper EZbook. As soon as it works a bit, the CPU drops to 1.5 GHz and stays there.<\/p>\n<p>Two register writes later, it runs at 10 W, and OpenArena goes from 69 to 111 fps. Finding those two registers took a lot longer than writing them.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/09\/ezbook.jpg\" alt=\"The Jumper EZbook, a thin silver 13 inch netbook, seen from the front and from the side\"\/><figcaption>The Jumper EZbook. (Product photos.)<\/figcaption><\/figure>\n<h2 class=\"wp-block-heading\">Results<\/h2>\n<p>6 W stock \u2192 10 W unlocked:<\/p>\n<ul class=\"wp-block-list\">\n<li>OpenArena: 69 \u2192 111 fps (+61 %)<\/li>\n<li>glmark2: 205 \u2192 310 (+51 %)<\/li>\n<li>7-zip: 4858 \u2192 6383 MIPS (+31 %)<\/li>\n<li>CPU + GPU together: 134 \u2192 257 fps (+91 %)<\/li>\n<\/ul>\n<p>Max temperature at 10 W: 84 \u00b0C, for a 105 \u00b0C limit. Heat was never the problem, it is just a setting.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/09\/openarena-frag.jpg\" alt=\"OpenArena running on the EZbook, watching a bot match: 'You fragged Ayumi, tied for 1st place'\"\/><figcaption>OpenArena at 111 fps instead of 69.<\/figcaption><\/figure>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/09\/glmark2.jpg\" alt=\"glmark2's refract scene, a glass rabbit, rendered by the Intel HD Graphics 500\"\/><figcaption>glmark2: 205 points at 6 W, 310 at 10 W.<\/figcaption><\/figure>\n<h2 class=\"wp-block-heading\">The fix<\/h2>\n<p>Two register writes at boot:<\/p>\n<pre class=\"wp-block-code\"><code>MMIO MCHBAR(0xFED10000)+0x70A8 := 0x00008f0000dd8a00   # PL1 10 W \/ PL2 15 W\nMSR  0x610                     := 0x80008f0000dd8a00   # same, + bit 63 = LOCK<\/code><\/pre>\n<p>The MSR part needs nothing special, msr-tools is enough:<\/p>\n<pre class=\"wp-block-code\"><code>sudo modprobe msr\nsudo wrmsr -a 0x610 0x80008f0000dd8a00<\/code><\/pre>\n<p>The MMIO part is a 64-bit write at <code>0xFED170A8<\/code> through <code>\/dev\/mem<\/code> (I do it with a small Python script that uses mmap).<\/p>\n<p>Both are lost at reboot, so a small systemd unit writes them again at every boot. These addresses are for the N3450 only: a wrong <code>\/dev\/mem<\/code> address freezes the machine.<\/p>\n<h2 class=\"wp-block-heading\">How I found it<\/h2>\n<p>1. The documented MSR first, <code>MSR_PKG_POWER_LIMIT<\/code> (0x610). The factory value:<\/p>\n<pre class=\"wp-block-code\"><code>0x610 = 0x00008f0000dd8600\n  PL1 (sustained) = 0x600 * 1\/256 W = 6.00 W\n  PL2 (burst)     = 0xF00 * 1\/256 W = 15.00 W\n  lock bit 63     = 0   (not locked, so I can write it)<\/code><\/pre>\n<p>I wrote PL1 = 10 W (<code>0x...8a00<\/code>). It reads back fine, powercap says 10 W. The CPU ignores it: 5.97 W, 1.5 GHz, and the power limit flag on in <code>THERM_STATUS<\/code> (0x19C). 2. Intel Core CPUs have a copy of the limit in MMIO, at MCHBAR + 0x59A0 (MCHBAR is at 0xFED10000 here, from PCI 0:0.0 offset 0x48). Here it reads <code>0x0000000000000000<\/code>. \u00ab\u00a0No copy on this chip\u00a0\u00bb, I said, very sure of myself. Wrong. 3. The config-TDP MSRs (0x648 to 0x64B) all give a #GP fault: not on this chip. And the BIOS setup has a \u00ab\u00a0TDP Limit\u00a0\u00bb setting (NVAR offset 0xEC, \u00ab\u00a0TDP Limit in Watts\u00a0\u00bb). I set it to 10 in both setup stores. Still 5.97 W: it is hidden behind a \u00ab\u00a0Turbo-XE supported\u00a0\u00bb flag that is 0 on this model, so the firmware never uses it. 4. The AI agent I was working with wrote it down: \u00ab\u00a06 W is locked, no lever left\u00a0\u00bb. It gave up, very professionally. I was quite sure we could do it, and asked it to look again. Real MMIO is never all zeros, so I scanned the whole 32 KB MCHBAR window (764 non-zero values) for the PL2 value 0x8F00:<\/p>\n<pre class=\"wp-block-code\"><code>+0x70A8 = 0x00008F0000DD8600   PL1=6.00W PL2=15.00W   &lt;-- here<\/code><\/pre>\n<p>On Apollo Lake the register is at 0x70A8, not 0x59A0. I wrote 10 W there: 10.12 W, 2.09 GHz. At last.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/09\/before_after.png\" alt=\"Chart: before and after the MMIO write, package power 5.97 to 10.12 W, all-core clock 1.5 to 2.09 GHz, temperature 62 to 76 \u00b0C\"\/><figcaption>Before and after the MMIO write, CPU only.<\/figcaption><\/figure>\n<p>5. Then the GPU wakes up, and the power goes back to 6 W. And stays there. Retour \u00e0 la case d\u00e9part. One trace per second, CPU load only:<\/p>\n<pre class=\"wp-block-code\"><code> t      GPU MHz  CPU MHz  package W\n 1-9      100     2089      ~10.0    GPU asleep: 10 W, full turbo\n 10       300     1392       6.65    the GPU wakes up for one second\n 16-30   ~100    ~1450      ~6.0     GPU asleep again, but still 6 W<\/code><\/pre>\n<p>Any window refresh or cursor blink wakes the GPU. So in real life (a browser open), always 6 W.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/09\/power_drawn.png\" alt=\"Chart: package power over time under CPU load, a flat 10 W until the GPU briefly wakes, then a drop to 6 W that never comes back\"\/><figcaption>The GPU wakes up for a second, and the 10 W are gone.<\/figcaption><\/figure>\n<p>The answer was in a coreboot commit message for Apollo Lake: \u00ab\u00a0This RAPL MMIO register is a physically separate instance from RAPL MSR register. The Punit algorithm constrains performance to whichever power limit is lower between both registers.\u00a0\u00bb<\/p>\n<p>So the real limit is min(MMIO, MSR). I checked the MSR during a GPU load: back to <code>0x...8600<\/code>, 6 W. The firmware puts the MSR back to 6 W each time the GPU wakes up. So I write the MSR too, with its lock bit. Now it stays at 10 W, with or without the GPU.<\/p>\n<h2 class=\"wp-block-heading\">Why not 15 W?<\/h2>\n<p>I tested it. Above 10 W the CPU is already at its max clock (2.09 GHz all cores), glmark2 gains only 7 % at 12 W, the game is already at its max, and mixed loads get worse: the GPU takes all the power and the CPU encode is slower than at 10 W. And 87 \u00b0C. Hotter and slower.<\/p>\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" src=\"https:\/\/www.fabidouille.com\/wp-content\/uploads\/2026\/09\/perf_vs_power.png\" alt=\"Chart: performance versus power limit from 6 to 15 W; most curves stay flat after 10 to 12 W and the mixed encode drops at 15 W\"\/><figcaption>Performance versus power limit. 10 W is the best compromise.<\/figcaption><\/figure>\n<p>All the details, including the dead ends: github.com\/fabien0008\/jumper-ezbook-power-unlock<\/p>\n<h2 class=\"wp-block-heading\">How AI was used<\/h2>\n<p>The register hunting was done with an AI coding agent (Claude Code) on the EZbook itself: it read and wrote the MSRs and MMIO, scanned MCHBAR, read the coreboot source, and ran the benchmarks. It also gave up twice: after the 0x59A0 zeros, and after the GPU latch. Both times I did not believe it, and both times there was a way. My job: insist, and check that the numbers made sense.<\/p>\n<p>Note: I don&rsquo;t have much time for the blog these days, so this post was written with the help of an AI, from my own notes, then read and corrected by me. Better to share the content this way than not at all.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A cheap netbook limited to 6 W, two hidden registers and one lock bit: now 10 W and 61 % more fps in OpenArena.<\/p>\n","protected":false},"author":0,"featured_media":529,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-536","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-non-classe"],"_links":{"self":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/536","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=536"}],"version-history":[{"count":4,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/536\/revisions"}],"predecessor-version":[{"id":580,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/posts\/536\/revisions\/580"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=\/wp\/v2\/media\/529"}],"wp:attachment":[{"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=536"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=536"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.fabidouille.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=536"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}