1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
|
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="author" content="韩暮秋">
<meta name="subtitle" content="暮秋小屋">
<meta name="description" content="这里是暮秋小屋,思念和灵感的寄存处">
<meta name="keywords" content="韩暮秋,MuqiuHan,'Muqiu Han', 'muqiu han', muqiuhan">
<title>
给一台 Linux 笔记本做服务器化改造:休眠、充电上限与 EC 寄存器考古 |
暮秋小屋
</title>
<link rel="icon" href="/favicon.ico">
<!-- stylesheets list from _config.yml -->
<link rel="stylesheet" href="/css/style.css">
<link rel="preload" href="/fonts/FZYouSongS-509R.woff2" as="font" type="font/woff2" crossorigin>
<!-- scripts list from _config.yml -->
<script
src="/js/menu.js"></script>
<script
src="https://polyfill.alicdn.com/polyfill.js?features=es6"></script>
<script
id="MathJax-script"
async
src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/mathjax/3.2.0/es5/tex-mml-chtml.js"></script>
<meta name="generator" content="Hexo 6.3.0"></head>
<body>
<div class="wrapper">
<div class="header">
<div class="flex-container">
<div class="header-inner">
<div class="site-brand-container">
<a href="/">
暮秋小屋
</a>
</div>
<div id="menu-btn" class="menu-btn" onclick="toggleMenu()">
菜单
</div>
<nav class="site-nav">
<ul class="menu-list">
<li class="menu-item">
<a href="/">
主页
</a>
</li>
<li class="menu-item">
<a href="/categories/gallery/">
日记本
</a>
</li>
<li class="menu-item">
<a href="/tags/Medicine/">
泛医学
</a>
</li>
<li class="menu-item">
<a href="/tags/Technique/">
计算机
</a>
</li>
<li class="menu-item">
<a href="/tags/Life/">
生活
</a>
</li>
<li class="menu-item">
<a href="/archives">
全部
</a>
</li>
<li class="menu-item">
<a href="/about">
关于
</a>
</li>
<li class="menu-item">
<a href="/search">搜索</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="main">
<div class="flex-container">
<article id="post">
<div class="post-head">
<div class="post-info">
<div class="tag-list">
<span class="post-tag">
<a href="/tags/Technique/">
Technique
</a>
</span>
<span class="post-tag">
<a href="/tags/Linux/">
Linux
</a>
</span>
</div>
<div class="post-title">
给一台 Linux 笔记本做服务器化改造:休眠、充电上限与 EC 寄存器考古
</div>
<span class="post-date">
Aug 29, 2026
</span>
</div>
<div class="post-img">
<div class="h-line-primary"></div>
</div>
</div>
<div class="post-content">
<p>一台旧笔记本装上 Hermes Agent 之后,就成了 7×24 挂在家庭网络里的常驻服务机。机器继续服役,但"笔记本"这个出厂身份留下的问题需要逐个拆除:拔电半小时自动休眠、合盖掉线、电池永远充到顶。这篇文章记录一次完整的改造过程——大部分是常规配置,最后一段是真正的硬骨头:Linux 下没有现成驱动的 Redmi EC,如何从固件代码里考古出充电上限的寄存器地址,并把 80% 上限真正写进硬件。</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>先说结论:这台机器的自动休眠由两套机制叠加控制,只改任何一处都会被另一处重新睡掉。</p>
<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">'nothing'</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">'nothing'</span></span><br></pre></td></tr></table></figure>
<p>第二处在 systemd-logind。不处理它,合盖依然会触发 suspend。做法是往 <code>/etc/systemd/logind.conf.d/</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> 这几行比合盖策略更进一步——它让 suspend/hibernate 这两个电源目标在系统层面不可达。从此无论哪个组件想休眠这台机器,systemd 都会拒绝执行。对服务器而言这是最保险的一层。</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 的工作方式:它自己不碰硬件,只调用内核提供的接口。Linux 内核标准接口是 <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 的处理方式是静默降级——配置留着,什么都不做。这解释了"设置了却充到顶"的全部现象。</p>
<h2 id="从-DSDT-考古:找到-EC-里的充电上限字段"><a class="header-anchor" href="#从-DSDT-考古:找到-EC-里的充电上限字段">¶</a>从 DSDT 考古:找到 EC 里的充电上限字段</h2>
<p>内核不提供接口,就要自己读固件。ACPI 的 DSDT 表是主板固件的执行代码,用 <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>
<table>
<thead>
<tr>
<th>EC 字段</th>
<th>读数</th>
<th>系统对照值</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td>RSOC</td>
<td>89%</td>
<td>upower 实时 89%(自放电后的实时值)</td>
<td>命中</td>
</tr>
<tr>
<td>BTDC</td>
<td>4664 mAh × 15440 mV = 72.0 Wh</td>
<td>energy_full_design 72.012 Wh</td>
<td>命中</td>
</tr>
<tr>
<td>BTFC</td>
<td>3939 mAh → 60.8 Wh</td>
<td>energy_full 60.818 Wh</td>
<td>命中</td>
</tr>
<tr>
<td>BTFC/BTDC</td>
<td>84.5%</td>
<td>电池健康度 84.455%</td>
<td>命中</td>
</tr>
<tr>
<td>BTPR/BTFC</td>
<td>88.9%</td>
<td>≈ RSOC 89%,内部自洽</td>
<td>命中</td>
</tr>
</tbody>
</table>
<p>六项全部命中,偏移表确认无误。这次读取还带来一个意外收获:<strong>循环次数 1196 次</strong>——内核 sysfs 一直报 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,读回确认 EC 接受。行为验证更直接:写入时电量 87%,高于上限,状态立即表现为 Discharging(拒绝充电);电量自然回落到 83% 后,状态转为 Charging——EC 正在按"低于 80 才充、充到 80 停"的逻辑工作。</p>
<p>EC 的值在冷启动后可能复位,所以最后一步是固化:一个 20 行的 shell 脚本(写入 + 最多 5 次读回重试 + 写 syslog),注册成 oneshot 的 systemd 服务,<code>WantedBy=multi-user.target</code>。每次开机自动重写。日志验证一行即可:<code>journalctl -t ec-charge-limit</code>。</p>
<h2 id="为什么绕了这么多路"><a class="header-anchor" href="#为什么绕了这么多路">¶</a>为什么绕了这么多路</h2>
<p>这套探索的完整链条值得复盘,因为它演示了一个通用模式:<strong>当厂商不为 Linux 提供驱动时,固件代码是最后的、也是完全可靠的文档</strong>。</p>
<p>TLP 配置存在但从未生效,说明用户态工具依赖于内核 ABI 的覆盖面。内核接口缺失时,<code>acpi_call</code> 这类"万能调用器"是第一层补救——但它的前提仍是 ACPI 层存在可调用的方法。DSDT 反汇编显示这台机器的 EC 确实声明了 BTCC 字段,但没有任何 AML 代码(ACPI 层的解释执行代码)读写它——小米把充电控制做成了纯 EC 寄存器协议,只有 Windows 侧的私有驱动知道怎么写。这一层是 acpi_call 也救不了的,于是只剩最后一条路:直接读写 EC RAM。</p>
<p>这已经触及了此类探索的边界。直接写 EC 是有真实风险的操作——这段内存窗口同时被固件本身使用,写错偏移可能影响风扇、键盘背光乃至供电策略。整个过程中风险控制靠三件事:所有写入前先完成只读交叉验证(六个字段全命中才敢写);写入目标收窄到 2 字节;写入后立即读回并观察行为。至于回滚,同样的通道写 100 就能恢复原状。</p>
<h2 id="交付清单"><a class="header-anchor" href="#交付清单">¶</a>交付清单</h2>
<p>改造后的最终状态:</p>
<ul>
<li>拔电、合盖均不休眠,suspend 目标在 systemd 层全局禁用</li>
<li>充电到 80% 由 EC 强制执行,不依赖任何用户态进程存活</li>
<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>(一键回滚)。如果你有一台同样"EC 沉默"的笔记本,这套只读验证优先的流程可以直接复用——先证明地址表是对的,再谈写入。</p>
</div>
<script>
window.onload = detectors();
</script>
<div class="post-footer">
<div class="h-line-primary"></div>
<nav class="post-nav">
<div class="prev-item">
</div>
<div class="next-item">
<div class="icon arrow-right"></div>
<div class="post-link">
<a href="/2026/08/25/%E9%9D%9E%E5%B0%8F%E7%BB%86%E8%83%9E%E8%82%BA%E7%99%8C%E9%B3%9E%E7%8A%B6%E5%88%86%E5%8C%96%E7%9A%84%E5%85%8D%E7%96%AB%E7%BB%84%E5%8C%96%E5%88%A4%E8%AF%BB%E5%8F%8A%E5%85%B6%E6%B2%BB%E7%96%97%E8%BE%B9%E7%95%8C/">Next</a>
</div>
</div>
</nav>
</div>
<div class="post-comment">
</div>
</article>
</div>
</div>
<div class="footer">
<div class="flex-container">
<div class="footer-text">
韩暮秋 |
希望路过的人可以添点柴火让这里暖和点
</div>
</div>
</div>
</div>
<script src="/js/mermaid-zoom.js"></script>
</body>
</html>
|