From 62f4ebf13b4f78707deff1014b7b224fde008bd5 Mon Sep 17 00:00:00 2001 From: i-shm Date: Sat, 29 Aug 2026 08:15:32 +0000 Subject: deploy: 810ca18bd28fe958194021d4165c9564dcf69f3b --- search.xml | 63 ++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 33 deletions(-) (limited to 'search.xml') diff --git a/search.xml b/search.xml index 9a55caae..ee1748dc 100644 --- a/search.xml +++ b/search.xml @@ -6761,27 +6761,26 @@ NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的 给一台 Linux 笔记本做服务器化改造:休眠、充电上限与 EC 寄存器考古 /2026/08/29/%E7%BB%99%E4%B8%80%E5%8F%B0Linux%E7%AC%94%E8%AE%B0%E6%9C%AC%E5%81%9A%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%8C%96%E6%94%B9%E9%80%A0%EF%BC%9A%E4%BC%91%E7%9C%A0%E3%80%81%E5%85%85%E7%94%B5%E4%B8%8A%E9%99%90%E4%B8%8EEC%E5%AF%84%E5%AD%98%E5%99%A8%E8%80%83%E5%8F%A4/ - 一台旧笔记本装上 Hermes Agent 之后,就成了 7×24 挂在家庭网络里的常驻服务机。机器继续服役,但"笔记本"这个出厂身份留下的问题需要逐个拆除:拔电半小时自动休眠、合盖掉线、电池永远充到顶。这篇文章记录一次完整的改造过程——大部分是常规配置,最后一段是真正的硬骨头:Linux 下没有现成驱动的 Redmi EC,如何从固件代码里考古出充电上限的寄存器地址,并把 80% 上限真正写进硬件。

-

改造对象是 Redmi Book Pro 15 2022,系统 Pop!_OS 24.04,内核 6.18.7。文中所有验证步骤都给出了可复核的证据,文末附完整的脚本清单。

-

休眠问题的两个来源:一个在用户会话,一个在系统层

-

先说结论:这台机器的自动休眠由两套机制叠加控制,只改任何一处都会被另一处重新睡掉。

-

第一处在 GNOME。org.gnome.settings-daemon.plugins.power 这组键值把插电和用电池分成两个策略:这台机器出厂默认插电模式 nothing(永不睡),电池模式 suspend(30 分钟无操作就睡)。服务器场景下,这个"电池模式"就是拔电失联的直接原因。禁用只需两条命令:

+ 一台退役笔记本装载 Hermes Agent 后转为 7×24 常驻服务器,出厂的笔记本形态留下三类故障:拔电半小时后自动休眠、合盖即失联、电池持续充至满电。本文记录改造全程。前两项属常规配置;第三项在 Linux 下无现成驱动,需要从主板固件代码中定位充电上限的寄存器地址并写入硬件,构成本文主体。

+

改造对象为 Redmi Book Pro 15 2022,系统 Pop!_OS 24.04,内核 6.18.7。关键步骤均附可复核证据,脚本清单见文末。

+

自动休眠由两套机制叠加控制,仅修改其一必然复发

+

第一处位于 GNOME 会话。org.gnome.settings-daemon.plugins.power 键值组区分供电与电池两种策略,该机出厂默认前者为 nothing(不休眠)、后者为 suspend(30 分钟无操作休眠)。服务器拔电即失联,直接原因即电池策略。修改两条键值即可禁用:

gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type 'nothing'
gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'nothing'
-

第二处在 systemd-logind。不处理它,合盖依然会触发 suspend。做法是往 /etc/systemd/logind.conf.d/ 放一个drop-in 配置:

+

第二处位于 systemd-logind。不处理它,合盖仍触发 suspend。向 /etc/systemd/logind.conf.d/ 写入 drop-in 配置:

[Login]
HandleLidSwitch=ignore
HandleLidSwitchExternalPower=ignore
HandleLidSwitchDocked=ignore
AllowSuspend=no
AllowHibernation=no
-

AllowSuspend=no 这几行比合盖策略更进一步——它让 suspend/hibernate 这两个电源目标在系统层面不可达。从此无论哪个组件想休眠这台机器,systemd 都会拒绝执行。对服务器而言这是最保险的一层。

-

充电上限:配置了两年,从来没生效过

-

机器上一直装着 TLP,配置文件里写得明明白白:

+

AllowSuspend=no 与 AllowHibernation=no 使 suspend、hibernate 两个 systemd 目标在系统层不可达:任何组件发起休眠请求,systemd 直接拒绝执行。这一层保证无论用户会话如何变更,服务器不会被睡掉。

+

充电上限配置存在两年,从未生效

+

机器长期安装 TLP,配置如下:

START_CHARGE_THRESH_BAT0=20
STOP_CHARGE_THRESH_BAT0=80
-

但电量依然冲到了 94%(对应衰减后的满充点)。原因在 TLP 的工作方式:它自己不碰硬件,只调用内核提供的接口。Linux 内核标准接口是 /sys/class/power_supply/BAT0/charge_control_end_threshold,TLP 依次尝试三种后端——natacpi(内核 ACPI 驱动直写)、acpi_call(通过第三方模块调用任意 ACPI 方法)、sysfs(上述标准接口)。三种在这台机器上全部缺席:Redmi 没有为自家 EC 提交过任何内核驱动,charge_control_end_threshold 文件不存在,acpi_call 也没有安装。

-

TLP 的处理方式是静默降级——配置留着,什么都不做。这解释了"设置了却充到顶"的全部现象。

-

从 DSDT 考古:找到 EC 里的充电上限字段

-

内核不提供接口,就要自己读固件。ACPI 的 DSDT 表是主板固件的执行代码,用 iasl 反汇编后就是一份可检索的"硬件说明书":

+

电量仍充至 94%(衰减后的实际满充点)。TLP 自身不操作硬件,仅调用内核接口。内核标准接口为 /sys/class/power_supply/BAT0/charge_control_end_threshold;TLP 依次探测三种后端:natacpi(内核 ACPI 驱动直写)、acpi_call(第三方模块调用任意 ACPI 方法)、sysfs(上述标准接口)。三种后端在本机全部缺失:Redmi 未向内核提交任何 EC 驱动,charge_control_end_threshold 不存在,acpi_call 未安装。

+

TLP 对后端缺失的处理是静默降级:保留配置,不执行任何操作。此即"设置了上限却充到满"的完整成因。

+

从 DSDT 定位 EC 充电上限字段

+

内核不提供接口时,固件代码是唯一可靠的文档。ACPI 的 DSDT 表是主板固件的 AML 执行代码,用 iasl 反汇编后可检索:

cp /sys/firmware/acpi/tables/DSDT /tmp/probe/
iasl -d /tmp/probe/DSDT.bin
-

在反汇编产物里,Device (EC0) 是嵌入式控制器(Embedded Controller,主板上负责电池、键盘、风扇等底层事务的 8 位单片机)。EC 暴露给 ACPI 的字段表以 Field (ERAM…) 开头,逐项列出每个字段的名称和位宽。搜索电池相关的命名,一段关键结构浮现出来:

+

反汇编产物中,Device (EC0) 即嵌入式控制器(Embedded Controller,主板上管理电池、键盘、风扇等底层事务的 8 位单片机)。EC 向 ACPI 暴露的字段表以 Field (ERAM…) 开头,逐项列出字段名与位宽。检索电池相关命名,得到关键结构:

Offset (0x80),
ACIN, 1, // AC 已插电
BTIN, 1, // 电池在位
BTST, 4, // 电池状态
ADPW, 8,
BTSN, 16, // 序列号
BTDC, 16, // Design Capacity 设计容量
BTDV, 16, // Design Voltage 设计电压
BTFC, 16, // Full Charge Capacity 满充容量
BTTP, 16,
BTCT, 16, // Cycle Count 循环次数
BTPR, 16, // Present Capacity 当前容量
BTVT, 16, // Voltage 电压
RSOC, 8, // Relative State of Charge 电量百分比
… // 8 个状态位占满一个字节
BTCC, 16, // Charge Cap 充电上限
BATM, 16,
-

按 ACPI Field 的位累积规则手工推算,每个字段落在 EC RAM 的具体字节位置:电量百分比 RSOC 在偏移 0x92,充电上限 BTCC 在 0x95-0x96(16 位小端)。这台机器的 EC RAM 通过内存映射窗口 0xFE0B0400 可直接访问。

-

推断需要验证。验证方法是交叉比对:把 32 个字节一次性读出来,看各项读数能否和系统已知的数值对上。实际读取结果:

+

依据 ACPI Field 的位累积规则逐字段推算,各字段在 EC RAM 中的字节位置随之确定:电量百分比 RSOC 位于偏移 0x92,充电上限 BTCC 位于 0x95-0x96(16 位小端)。本机 EC RAM 经内存映射窗口 0xFE0B0400 可直接访问。

+

偏移推算须经读取验证。一次读出 32 字节,与系统已知值交叉比对:

@@ -6795,7 +6794,7 @@ NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的 - + @@ -6819,29 +6818,27 @@ NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的 - +
RSOC 89%upower 实时 89%(自放电后的实时值)upower 实时 89% 命中
BTPR/BTFC 88.9%≈ RSOC 89%,内部自洽与 RSOC 89% 自洽 命中
-

六项全部命中,偏移表确认无误。这次读取还带来一个意外收获:循环次数 1196 次——内核 sysfs 一直报 0,真实数字一直躺在 EC 里。84.5% 的健康度对一千二百次循环来说衰减曲线相当正常,而 80% 上限从现在开始会显著放缓后续衰减。

-

验证时 BTCC 读数为 0,即从未有系统写入过它——TLP 静默失效、小米自己的管理软件从未在 Linux 侧运行,这个字段空了两年。

-

写入与固化:从一次性验证到开机自启

-

写入本身是对内存映射窗口的 2 字节操作。用 dd 定位到 0xFE0B0495,写入小端编码的 80,读回确认 EC 接受。行为验证更直接:写入时电量 87%,高于上限,状态立即表现为 Discharging(拒绝充电);电量自然回落到 83% 后,状态转为 Charging——EC 正在按"低于 80 才充、充到 80 停"的逻辑工作。

-

EC 的值在冷启动后可能复位,所以最后一步是固化:一个 20 行的 shell 脚本(写入 + 最多 5 次读回重试 + 写 syslog),注册成 oneshot 的 systemd 服务,WantedBy=multi-user.target。每次开机自动重写。日志验证一行即可:journalctl -t ec-charge-limit。

-

为什么绕了这么多路

-

这套探索的完整链条值得复盘,因为它演示了一个通用模式:当厂商不为 Linux 提供驱动时,固件代码是最后的、也是完全可靠的文档。

-

TLP 配置存在但从未生效,说明用户态工具依赖于内核 ABI 的覆盖面。内核接口缺失时,acpi_call 这类"万能调用器"是第一层补救——但它的前提仍是 ACPI 层存在可调用的方法。DSDT 反汇编显示这台机器的 EC 确实声明了 BTCC 字段,但没有任何 AML 代码(ACPI 层的解释执行代码)读写它——小米把充电控制做成了纯 EC 寄存器协议,只有 Windows 侧的私有驱动知道怎么写。这一层是 acpi_call 也救不了的,于是只剩最后一条路:直接读写 EC RAM。

-

这已经触及了此类探索的边界。直接写 EC 是有真实风险的操作——这段内存窗口同时被固件本身使用,写错偏移可能影响风扇、键盘背光乃至供电策略。整个过程中风险控制靠三件事:所有写入前先完成只读交叉验证(六个字段全命中才敢写);写入目标收窄到 2 字节;写入后立即读回并观察行为。至于回滚,同样的通道写 100 就能恢复原状。

-

交付清单

-

改造后的最终状态:

-
    -
  • 拔电、合盖均不休眠,suspend 目标在 systemd 层全局禁用
  • -
  • 充电到 80% 由 EC 强制执行,不依赖任何用户态进程存活
  • +

    五项独立指标全部命中,偏移表成立。读取同时得到一项内核长期隐瞒的数据:循环次数 1196 次——sysfs 的 cycle_count 一直报 0,真实值存于 EC。84.5% 的健康度对应一千二百次循环,衰减幅度在正常区间;80% 上限将显著延缓后续衰减。

    +

    首次读取时 BTCC 为 0:该字段从未被任何系统写入过。TLP 静默失效,小米管理软件从未在 Linux 侧运行,此字段空置两年。

    +

    写入与开机固化

    +

    写入是针对内存映射窗口的 2 字节操作:dd 定位 0xFE0B0495,写入小端编码的 80,读回确认。行为验证给出更强的证据:写入时电量 87%,高于上限,电池状态立即转为 Discharging;电量自然回落至 83% 后状态转为 Charging。EC 严格执行"低于 80 充电、达到 80 停充"。

    +

    EC 值可能在冷启动后复位,需开机重写。方案为 oneshot 型 systemd 服务(WantedBy=multi-user.target),执行 20 行 shell 脚本:写入、至多 5 次读回重试、结果写 syslog。验证一行命令:journalctl -t ec-charge-limit。

    +

    方法论:固件代码是最后的文档

    +

    TLP 配置存在而从未生效,根源在于用户态工具依赖内核 ABI 的覆盖面。内核接口缺失时,acpi_call 一类通用调用器是第一层补救,但其前提仍是 ACPI 层存在可调用的方法。DSDT 反汇编显示本机 EC 声明了 BTCC 字段,而全部 AML 代码均未读写它:小米将充电控制实现为纯 EC 寄存器协议,仅 Windows 侧私有驱动知晓协议细节。这一层 acpi_call 无法覆盖,剩余路径只有直接读写 EC RAM。

    +

    直接写 EC 有真实风险:该内存窗口同时被固件自身使用,写错偏移可能影响风扇、键盘背光乃至供电策略。本次风险控制依赖三项措施:写入前完成只读交叉验证,五项指标全部命中后才执行写入;写入范围收窄至 2 字节;写入后立即读回并观察充电行为。回滚与写入同路:向同一地址写 100 即恢复满充。

    +

    改造结果

    +
      +
    • 拔电、合盖均不休眠;suspend 目标在 systemd 层全局禁用
    • +
    • 充电上限 80% 由 EC 强制执行,不依赖任何用户态进程存活
    • 开机自动重写上限,冷启动不丢配置
    • 电池档案:设计 72 Wh,满充 60.8 Wh(健康度 84.5%),循环 1196 次
    -

    完整脚本已归档:setup-server-power.sh(休眠部分)、acpi-probe-battery.sh(DSDT 反汇编 + 只读探测)、ec-verify-readonly.sh(字段交叉验证)、ec-set-charge-limit.sh(写入与行为观察)、power-rollback.sh(一键回滚)。如果你有一台同样"EC 沉默"的笔记本,这套只读验证优先的流程可以直接复用——先证明地址表是对的,再谈写入。

    +

    脚本归档:setup-server-power.sh(休眠禁用)、acpi-probe-battery.sh(DSDT 反汇编与只读探测)、ec-verify-readonly.sh(字段交叉验证)、ec-set-charge-limit.sh(写入与行为观察)、power-rollback.sh(一键回滚)。其他"EC 沉默"的机型可直接复用该流程:先以只读交叉验证证明地址表,再执行写入。

    ]]> Technique -- cgit v1.2.3