summaryrefslogtreecommitdiff
path: root/2026
diff options
context:
space:
mode:
Diffstat (limited to '2026')
-rw-r--r--2026/08/29/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古/index.html23
1 files changed, 15 insertions, 8 deletions
diff --git a/2026/08/29/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古/index.html b/2026/08/29/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古/index.html
index 2420a461..f93eb930 100644
--- a/2026/08/29/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古/index.html
+++ b/2026/08/29/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古/index.html
@@ -192,26 +192,29 @@
</div>
</div>
<div class="post-content">
- <p>一台退役笔记本装载 Hermes Agent 后转为 7×24 常驻服务器,出厂的笔记本形态留下三类故障:拔电半小时后自动休眠、合盖即失联、电池持续充至满电。本文记录改造全程。前两项属常规配置;第三项在 Linux 下无现成驱动,需要从主板固件代码中定位充电上限的寄存器地址并写入硬件,构成本文主体。</p>
-<p>改造对象为 Redmi Book Pro 15 2022,系统 Pop!_OS 24.04,内核 6.18.7。关键步骤均附可复核证据,脚本清单见文末。</p>
+ <p>一台退役笔记本装载 Hermes Agent 后转为 7×24 常驻服务器,笔记本的默认行为与服务器角色有三处冲突:电源管理在拔电半小时后自动休眠、合盖即失联、充电不设上限。这些是厂商为移动场景设计的合理默认值,对服务器却是故障。本文记录改造全程。前两项属常规配置;第三项在 Linux 下无现成驱动,需要从主板固件代码中定位充电上限的寄存器地址并写入硬件,构成本文主体。</p>
+<p>改造对象为 Redmi Book Pro 15 2022,系统 Pop!_OS 24.04,内核 6.18.7。所有脚本完整源码收录于正文。</p>
<h2 id="自动休眠由两套机制叠加控制,仅修改其一必然复发"><a class="header-anchor" href="#自动休眠由两套机制叠加控制,仅修改其一必然复发">¶</a>自动休眠由两套机制叠加控制,仅修改其一必然复发</h2>
<p>第一处位于 GNOME 会话。<code>org.gnome.settings-daemon.plugins.power</code> 键值组区分供电与电池两种策略,该机出厂默认前者为 <code>nothing</code>(不休眠)、后者为 <code>suspend</code>(30 分钟无操作休眠)。服务器拔电即失联,直接原因即电池策略。修改两条键值即可禁用:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">gsettings <span class="built_in">set</span> org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type <span class="string">&#x27;nothing&#x27;</span></span><br><span class="line">gsettings <span class="built_in">set</span> org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type <span class="string">&#x27;nothing&#x27;</span></span><br></pre></td></tr></table></figure>
-<p>第二处位于 systemd-logind。不处理它,合盖仍触发 suspend。向 <code>/etc/systemd/logind.conf.d/</code> 写入 drop-in 配置:</p>
+<p>第二处位于 systemd-logind。不处理它,合盖仍触发 suspend。向 <code>/etc/systemd/logind.conf.d/99-server-nosuspend.conf</code> 写入 drop-in 配置:</p>
<figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Login]</span></span><br><span class="line"><span class="attr">HandleLidSwitch</span>=ignore</span><br><span class="line"><span class="attr">HandleLidSwitchExternalPower</span>=ignore</span><br><span class="line"><span class="attr">HandleLidSwitchDocked</span>=ignore</span><br><span class="line"><span class="attr">AllowSuspend</span>=<span class="literal">no</span></span><br><span class="line"><span class="attr">AllowHibernation</span>=<span class="literal">no</span></span><br></pre></td></tr></table></figure>
<p><code>AllowSuspend=no</code> 与 <code>AllowHibernation=no</code> 使 suspend、hibernate 两个 systemd 目标在系统层不可达:任何组件发起休眠请求,systemd 直接拒绝执行。这一层保证无论用户会话如何变更,服务器不会被睡掉。</p>
+<p>上述配置对应脚本 <code>setup-server-power.sh</code>,其主体即本节三条命令加上 logind 重启,此处不再展开。</p>
<h2 id="充电上限配置存在两年,从未生效"><a class="header-anchor" href="#充电上限配置存在两年,从未生效">¶</a>充电上限配置存在两年,从未生效</h2>
<p>机器长期安装 TLP,配置如下:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">START_CHARGE_THRESH_BAT0=20</span><br><span class="line">STOP_CHARGE_THRESH_BAT0=80</span><br></pre></td></tr></table></figure>
<p>电量仍充至 94%(衰减后的实际满充点)。TLP 自身不操作硬件,仅调用内核接口。内核标准接口为 <code>/sys/class/power_supply/BAT0/charge_control_end_threshold</code>;TLP 依次探测三种后端:<code>natacpi</code>(内核 ACPI 驱动直写)、<code>acpi_call</code>(第三方模块调用任意 ACPI 方法)、<code>sysfs</code>(上述标准接口)。三种后端在本机全部缺失:Redmi 未向内核提交任何 EC 驱动,<code>charge_control_end_threshold</code> 不存在,<code>acpi_call</code> 未安装。</p>
-<p>TLP 对后端缺失的处理是静默降级:保留配置,不执行任何操作。此即&quot;设置了上限却充到满&quot;的完整成因。</p>
+<p>TLP 对后端缺失的处理是静默降级:保留配置,不执行任何操作。此即&quot;设置了上限却充到满&quot;的完整成因。补装 <code>acpi-call-dkms</code> 后重启 TLP,<code>tlp-stat</code> 报告 <code>natacpi (system76_acpi) = inactive (laptop not supported)</code>,阈值接口依旧未出现——ACPI 层无方法可调,此路不通。</p>
<h2 id="从-DSDT-定位-EC-充电上限字段"><a class="header-anchor" href="#从-DSDT-定位-EC-充电上限字段">¶</a>从 DSDT 定位 EC 充电上限字段</h2>
<p>内核不提供接口时,固件代码是唯一可靠的文档。ACPI 的 DSDT 表是主板固件的 AML 执行代码,用 <code>iasl</code> 反汇编后可检索:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> /sys/firmware/acpi/tables/DSDT /tmp/probe/</span><br><span class="line">iasl -d /tmp/probe/DSDT.bin</span><br></pre></td></tr></table></figure>
<p>反汇编产物中,<code>Device (EC0)</code> 即嵌入式控制器(Embedded Controller,主板上管理电池、键盘、风扇等底层事务的 8 位单片机)。EC 向 ACPI 暴露的字段表以 <code>Field (ERAM…)</code> 开头,逐项列出字段名与位宽。检索电池相关命名,得到关键结构:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">Offset (0x80),</span><br><span class="line">ACIN, 1, // AC 已插电</span><br><span class="line">BTIN, 1, // 电池在位</span><br><span class="line">BTST, 4, // 电池状态</span><br><span class="line">ADPW, 8,</span><br><span class="line">BTSN, 16, // 序列号</span><br><span class="line">BTDC, 16, // Design Capacity 设计容量</span><br><span class="line">BTDV, 16, // Design Voltage 设计电压</span><br><span class="line">BTFC, 16, // Full Charge Capacity 满充容量</span><br><span class="line">BTTP, 16,</span><br><span class="line">BTCT, 16, // Cycle Count 循环次数</span><br><span class="line">BTPR, 16, // Present Capacity 当前容量</span><br><span class="line">BTVT, 16, // Voltage 电压</span><br><span class="line">RSOC, 8, // Relative State of Charge 电量百分比</span><br><span class="line">… // 8 个状态位占满一个字节</span><br><span class="line">BTCC, 16, // Charge Cap 充电上限</span><br><span class="line">BATM, 16,</span><br></pre></td></tr></table></figure>
<p>依据 ACPI Field 的位累积规则逐字段推算,各字段在 EC RAM 中的字节位置随之确定:电量百分比 RSOC 位于偏移 0x92,充电上限 BTCC 位于 0x95-0x96(16 位小端)。本机 EC RAM 经内存映射窗口 <code>0xFE0B0400</code> 可直接访问。</p>
-<p>偏移推算须经读取验证。一次读出 32 字节,与系统已知值交叉比对:</p>
+<p>偏移推算须经读取验证。验证脚本用 <code>dd</code> 从 <code>/dev/mem</code> 读出窗口内 32 字节,再按偏移表逐字段解码:</p>
+<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment"># ec-verify-readonly.sh — 只读验证偏移表,不做任何写入</span></span><br><span class="line">[ <span class="string">&quot;<span class="subst">$(id -u)</span>&quot;</span> -eq 0 ] || &#123; <span class="built_in">echo</span> <span class="string">&quot;请用 sudo 运行&quot;</span>; <span class="built_in">exit</span> 1; &#125;</span><br><span class="line"><span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mem bs=1 skip=$((<span class="number">0</span>xFE0B0480)) count=32 of=/tmp/ec-read.bin status=none</span><br><span class="line">python3 - &lt;&lt;<span class="string">&#x27;PYEOF&#x27;</span></span><br><span class="line">data = open(<span class="string">&quot;/tmp/ec-read.bin&quot;</span>,<span class="string">&quot;rb&quot;</span>).<span class="built_in">read</span>()</span><br><span class="line">def u8(o): <span class="built_in">return</span> data[o]</span><br><span class="line">def u16(o): <span class="built_in">return</span> data[o] | (data[o+1] &lt;&lt; <span class="string">8)</span></span><br><span class="line"><span class="string">b80 = data[0]</span></span><br><span class="line"><span class="string">print(f&quot;ACIN(AC插电) = &#123;b80 &amp; 1&#125; (期望 1)&quot;)</span></span><br><span class="line"><span class="string">print(f&quot;BTDC 0x84 = &#123;u16(4)&#125; mAh (对照 energy_full_design/电压)&quot;)</span></span><br><span class="line"><span class="string">print(f&quot;BTDV 0x86 = &#123;u16(6)&#125; mV (期望约 15440)&quot;)</span></span><br><span class="line"><span class="string">print(f&quot;BTFC 0x88 = &#123;u16(8</span>)&#125; mAh (对照 energy_full)<span class="string">&quot;)</span></span><br><span class="line"><span class="string">print(f&quot;</span>BTPR 0x8E = &#123;u16(14)&#125; mAh (对照当前容量)<span class="string">&quot;)</span></span><br><span class="line"><span class="string">print(f&quot;</span>BTVT 0x90 = &#123;u16(16)&#125; mV (期望 16000-16800)<span class="string">&quot;)</span></span><br><span class="line"><span class="string">print(f&quot;</span>RSOC 0x92 = &#123;u8(18)&#125;% (对照 upower 实时值)<span class="string">&quot;)</span></span><br><span class="line"><span class="string">print(f&quot;</span>BTCC 0x95 = &#123;u16(21)&#125;% (充电上限,验证目标)<span class="string">&quot;)</span></span><br><span class="line"><span class="string">print(f&quot;</span>BTCT 0x8C = &#123;u16(12)&#125; 次 (循环次数)<span class="string">&quot;)</span></span><br><span class="line"><span class="string">PYEOF</span></span><br></pre></td></tr></table></figure>
+<p>一次读出 32 字节,与系统已知值交叉比对:</p>
<table>
<thead>
<tr>
@@ -257,8 +260,12 @@
<p>五项独立指标全部命中,偏移表成立。读取同时得到一项内核长期隐瞒的数据:循环次数 1196 次——sysfs 的 <code>cycle_count</code> 一直报 0,真实值存于 EC。84.5% 的健康度对应一千二百次循环,衰减幅度在正常区间;80% 上限将显著延缓后续衰减。</p>
<p>首次读取时 BTCC 为 0:该字段从未被任何系统写入过。TLP 静默失效,小米管理软件从未在 Linux 侧运行,此字段空置两年。</p>
<h2 id="写入与开机固化"><a class="header-anchor" href="#写入与开机固化">¶</a>写入与开机固化</h2>
-<p>写入是针对内存映射窗口的 2 字节操作:<code>dd</code> 定位 <code>0xFE0B0495</code>,写入小端编码的 80,读回确认。行为验证给出更强的证据:写入时电量 87%,高于上限,电池状态立即转为 Discharging;电量自然回落至 83% 后状态转为 Charging。EC 严格执行&quot;低于 80 充电、达到 80 停充&quot;。</p>
-<p>EC 值可能在冷启动后复位,需开机重写。方案为 oneshot 型 systemd 服务(<code>WantedBy=multi-user.target</code>),执行 20 行 shell 脚本:写入、至多 5 次读回重试、结果写 syslog。验证一行命令:<code>journalctl -t ec-charge-limit</code>。</p>
+<p>写入是针对内存映射窗口的 2 字节操作。写入时电量 87%,高于上限,电池状态立即转为 Discharging;电量自然回落至 83% 后状态转为 Charging。EC 严格执行&quot;低于 80 充电、达到 80 停充&quot;。</p>
+<p>EC 值可能在冷启动后复位,需开机重写。方案为 oneshot 型 systemd 服务,执行 18 行运行时脚本 <code>/usr/local/bin/ec-charge-limit</code>——这是全套配置中唯一长期驻留的代码,全文如下:</p>
+<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment"># ec-charge-limit — BTCC @ 0xFE0B0495 (little-endian u16)</span></span><br><span class="line"><span class="comment"># 用法: ec-charge-limit [20-100],由 ec-charge-limit.service 开机调用</span></span><br><span class="line">LIMIT=<span class="variable">$&#123;1:-80&#125;</span></span><br><span class="line">[ <span class="string">&quot;<span class="variable">$LIMIT</span>&quot;</span> -ge 20 ] &amp;&amp; [ <span class="string">&quot;<span class="variable">$LIMIT</span>&quot;</span> -le 100 ] || <span class="built_in">exit</span> 1</span><br><span class="line">BASE=$((<span class="number">0</span>xFE0B0400)); OFF=$((<span class="number">0</span>x95))</span><br><span class="line">[ <span class="string">&quot;<span class="subst">$(id -u)</span>&quot;</span> -eq 0 ] || <span class="built_in">exit</span> 1</span><br><span class="line"><span class="keyword">for</span> i <span class="keyword">in</span> 1 2 3 4 5; <span class="keyword">do</span></span><br><span class="line"> <span class="built_in">printf</span> <span class="string">&quot;<span class="subst">$(printf &#x27;\\x%02x\\x00&#x27; <span class="string">&quot;<span class="variable">$LIMIT</span>&quot;</span>)</span>&quot;</span> \</span><br><span class="line"> | <span class="built_in">dd</span> of=/dev/mem bs=1 seek=$((BASE + OFF)) count=2 conv=notrunc status=none \</span><br><span class="line"> || &#123; <span class="built_in">sleep</span> 2; <span class="built_in">continue</span>; &#125;</span><br><span class="line"> <span class="built_in">sleep</span> 1</span><br><span class="line"> <span class="built_in">dd</span> <span class="keyword">if</span>=/dev/mem bs=1 skip=$((BASE + OFF)) count=2 of=/tmp/.btcc-check status=none</span><br><span class="line"> V=$(python3 -c <span class="string">&quot;d=open(&#x27;/tmp/.btcc-check&#x27;,&#x27;rb&#x27;).read(); print(d[0]|(d[1]&lt;&lt;8))&quot;</span>)</span><br><span class="line"> [ <span class="string">&quot;<span class="variable">$V</span>&quot;</span> = <span class="string">&quot;<span class="variable">$LIMIT</span>&quot;</span> ] &amp;&amp; &#123; logger -t ec-charge-limit <span class="string">&quot;BTCC set to <span class="variable">$LIMIT</span> (attempt <span class="variable">$i</span>)&quot;</span>; <span class="built_in">exit</span> 0; &#125;</span><br><span class="line"> <span class="built_in">sleep</span> 2</span><br><span class="line"><span class="keyword">done</span></span><br><span class="line">logger -t ec-charge-limit <span class="string">&quot;FAILED to set BTCC=<span class="variable">$LIMIT</span> after 5 attempts&quot;</span></span><br><span class="line"><span class="built_in">exit</span> 1</span><br></pre></td></tr></table></figure>
+<p>脚本包含三重保护:参数限制在 20-100;写入后读回比对,不匹配则重试;至多 5 次,失败写 syslog 并以非零码退出。服务单元 5 行:</p>
+<figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Set Redmi Book Pro charge limit to <span class="number">80</span>% via EC memory window</span><br><span class="line"><span class="attr">After</span>=multi-user.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=<span class="literal">on</span>eshot</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/local/bin/ec-charge-limit <span class="number">80</span></span><br><span class="line"><span class="attr">RemainAfterExit</span>=<span class="literal">yes</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></table></figure>
+<p>部署后验证:<code>journalctl -t ec-charge-limit</code> 应含 <code>BTCC set to 80 (attempt 1)</code>。</p>
<h2 id="方法论:固件代码是最后的文档"><a class="header-anchor" href="#方法论:固件代码是最后的文档">¶</a>方法论:固件代码是最后的文档</h2>
<p>TLP 配置存在而从未生效,根源在于用户态工具依赖内核 ABI 的覆盖面。内核接口缺失时,<code>acpi_call</code> 一类通用调用器是第一层补救,但其前提仍是 ACPI 层存在可调用的方法。DSDT 反汇编显示本机 EC 声明了 BTCC 字段,而全部 AML 代码均未读写它:小米将充电控制实现为纯 EC 寄存器协议,仅 Windows 侧私有驱动知晓协议细节。这一层 acpi_call 无法覆盖,剩余路径只有直接读写 EC RAM。</p>
<p>直接写 EC 有真实风险:该内存窗口同时被固件自身使用,写错偏移可能影响风扇、键盘背光乃至供电策略。本次风险控制依赖三项措施:写入前完成只读交叉验证,五项指标全部命中后才执行写入;写入范围收窄至 2 字节;写入后立即读回并观察充电行为。回滚与写入同路:向同一地址写 100 即恢复满充。</p>
@@ -269,7 +276,7 @@
<li>开机自动重写上限,冷启动不丢配置</li>
<li>电池档案:设计 72 Wh,满充 60.8 Wh(健康度 84.5%),循环 1196 次</li>
</ul>
-<p>脚本归档:<code>setup-server-power.sh</code>(休眠禁用)、<code>acpi-probe-battery.sh</code>(DSDT 反汇编与只读探测)、<code>ec-verify-readonly.sh</code>(字段交叉验证)、<code>ec-set-charge-limit.sh</code>(写入与行为观察)、<code>power-rollback.sh</code>(一键回滚)。其他&quot;EC 沉默&quot;的机型可直接复用该流程:先以只读交叉验证证明地址表,再执行写入。</p>
+<p>辅助脚本归档于本地(篇幅所限未全文收录):<code>setup-server-power.sh</code>(休眠禁用,含备份与逐步回显)、<code>acpi-probe-battery.sh</code>(DSDT 反汇编与 ACPI 只读探测)、<code>ec-set-charge-limit.sh</code>(首次写入与 60 秒行为观察)、<code>power-rollback.sh</code>(一键回滚)。其他&quot;EC 沉默&quot;的机型可直接复用核心流程:反汇编 DSDT 检索字段表 → 手工推算偏移 → 只读交叉验证 → 验证命中后再写入。</p>
</div>