diff options
Diffstat (limited to 'source')
| -rw-r--r-- | source/_posts/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古.md | 82 |
1 files changed, 74 insertions, 8 deletions
diff --git a/source/_posts/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古.md b/source/_posts/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古.md index cb3d7f7f..3236421b 100644 --- a/source/_posts/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古.md +++ b/source/_posts/给一台Linux笔记本做服务器化改造:休眠、充电上限与EC寄存器考古.md @@ -3,9 +3,9 @@ date: 2026-08-29 15:30:00 tags: [Technique, Linux] --- -一台退役笔记本装载 Hermes Agent 后转为 7×24 常驻服务器,出厂的笔记本形态留下三类故障:拔电半小时后自动休眠、合盖即失联、电池持续充至满电。本文记录改造全程。前两项属常规配置;第三项在 Linux 下无现成驱动,需要从主板固件代码中定位充电上限的寄存器地址并写入硬件,构成本文主体。 +一台退役笔记本装载 Hermes Agent 后转为 7×24 常驻服务器,笔记本的默认行为与服务器角色有三处冲突:电源管理在拔电半小时后自动休眠、合盖即失联、充电不设上限。这些是厂商为移动场景设计的合理默认值,对服务器却是故障。本文记录改造全程。前两项属常规配置;第三项在 Linux 下无现成驱动,需要从主板固件代码中定位充电上限的寄存器地址并写入硬件,构成本文主体。 -改造对象为 Redmi Book Pro 15 2022,系统 Pop!_OS 24.04,内核 6.18.7。关键步骤均附可复核证据,脚本清单见文末。 +改造对象为 Redmi Book Pro 15 2022,系统 Pop!_OS 24.04,内核 6.18.7。所有脚本完整源码收录于正文。 ## 自动休眠由两套机制叠加控制,仅修改其一必然复发 @@ -16,7 +16,7 @@ gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-typ 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/99-server-nosuspend.conf` 写入 drop-in 配置: ```ini [Login] @@ -29,6 +29,8 @@ AllowHibernation=no `AllowSuspend=no` 与 `AllowHibernation=no` 使 suspend、hibernate 两个 systemd 目标在系统层不可达:任何组件发起休眠请求,systemd 直接拒绝执行。这一层保证无论用户会话如何变更,服务器不会被睡掉。 +上述配置对应脚本 `setup-server-power.sh`,其主体即本节三条命令加上 logind 重启,此处不再展开。 + ## 充电上限配置存在两年,从未生效 机器长期安装 TLP,配置如下: @@ -40,7 +42,7 @@ STOP_CHARGE_THRESH_BAT0=80 电量仍充至 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 对后端缺失的处理是静默降级:保留配置,不执行任何操作。此即"设置了上限却充到满"的完整成因。 +TLP 对后端缺失的处理是静默降级:保留配置,不执行任何操作。此即"设置了上限却充到满"的完整成因。补装 `acpi-call-dkms` 后重启 TLP,`tlp-stat` 报告 `natacpi (system76_acpi) = inactive (laptop not supported)`,阈值接口依旧未出现——ACPI 层无方法可调,此路不通。 ## 从 DSDT 定位 EC 充电上限字段 @@ -75,7 +77,31 @@ BATM, 16, 依据 ACPI Field 的位累积规则逐字段推算,各字段在 EC RAM 中的字节位置随之确定:电量百分比 RSOC 位于偏移 0x92,充电上限 BTCC 位于 0x95-0x96(16 位小端)。本机 EC RAM 经内存映射窗口 `0xFE0B0400` 可直接访问。 -偏移推算须经读取验证。一次读出 32 字节,与系统已知值交叉比对: +偏移推算须经读取验证。验证脚本用 `dd` 从 `/dev/mem` 读出窗口内 32 字节,再按偏移表逐字段解码: + +```bash +#!/bin/bash +# ec-verify-readonly.sh — 只读验证偏移表,不做任何写入 +[ "$(id -u)" -eq 0 ] || { echo "请用 sudo 运行"; exit 1; } +dd if=/dev/mem bs=1 skip=$((0xFE0B0480)) count=32 of=/tmp/ec-read.bin status=none +python3 - <<'PYEOF' +data = open("/tmp/ec-read.bin","rb").read() +def u8(o): return data[o] +def u16(o): return data[o] | (data[o+1] << 8) +b80 = data[0] +print(f"ACIN(AC插电) = {b80 & 1} (期望 1)") +print(f"BTDC 0x84 = {u16(4)} mAh (对照 energy_full_design/电压)") +print(f"BTDV 0x86 = {u16(6)} mV (期望约 15440)") +print(f"BTFC 0x88 = {u16(8)} mAh (对照 energy_full)") +print(f"BTPR 0x8E = {u16(14)} mAh (对照当前容量)") +print(f"BTVT 0x90 = {u16(16)} mV (期望 16000-16800)") +print(f"RSOC 0x92 = {u8(18)}% (对照 upower 实时值)") +print(f"BTCC 0x95 = {u16(21)}% (充电上限,验证目标)") +print(f"BTCT 0x8C = {u16(12)} 次 (循环次数)") +PYEOF +``` + +一次读出 32 字节,与系统已知值交叉比对: | EC 字段 | 读数 | 系统对照值 | 结果 | |---|---|---|---| @@ -91,9 +117,49 @@ BATM, 16, ## 写入与开机固化 -写入是针对内存映射窗口的 2 字节操作:`dd` 定位 `0xFE0B0495`,写入小端编码的 80,读回确认。行为验证给出更强的证据:写入时电量 87%,高于上限,电池状态立即转为 Discharging;电量自然回落至 83% 后状态转为 Charging。EC 严格执行"低于 80 充电、达到 80 停充"。 +写入是针对内存映射窗口的 2 字节操作。写入时电量 87%,高于上限,电池状态立即转为 Discharging;电量自然回落至 83% 后状态转为 Charging。EC 严格执行"低于 80 充电、达到 80 停充"。 + +EC 值可能在冷启动后复位,需开机重写。方案为 oneshot 型 systemd 服务,执行 18 行运行时脚本 `/usr/local/bin/ec-charge-limit`——这是全套配置中唯一长期驻留的代码,全文如下: + +```bash +#!/bin/bash +# ec-charge-limit — BTCC @ 0xFE0B0495 (little-endian u16) +# 用法: ec-charge-limit [20-100],由 ec-charge-limit.service 开机调用 +LIMIT=${1:-80} +[ "$LIMIT" -ge 20 ] && [ "$LIMIT" -le 100 ] || exit 1 +BASE=$((0xFE0B0400)); OFF=$((0x95)) +[ "$(id -u)" -eq 0 ] || exit 1 +for i in 1 2 3 4 5; do + printf "$(printf '\\x%02x\\x00' "$LIMIT")" \ + | dd of=/dev/mem bs=1 seek=$((BASE + OFF)) count=2 conv=notrunc status=none \ + || { sleep 2; continue; } + sleep 1 + dd if=/dev/mem bs=1 skip=$((BASE + OFF)) count=2 of=/tmp/.btcc-check status=none + V=$(python3 -c "d=open('/tmp/.btcc-check','rb').read(); print(d[0]|(d[1]<<8))") + [ "$V" = "$LIMIT" ] && { logger -t ec-charge-limit "BTCC set to $LIMIT (attempt $i)"; exit 0; } + sleep 2 +done +logger -t ec-charge-limit "FAILED to set BTCC=$LIMIT after 5 attempts" +exit 1 +``` + +脚本包含三重保护:参数限制在 20-100;写入后读回比对,不匹配则重试;至多 5 次,失败写 syslog 并以非零码退出。服务单元 5 行: + +```ini +[Unit] +Description=Set Redmi Book Pro charge limit to 80% via EC memory window +After=multi-user.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/ec-charge-limit 80 +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +``` -EC 值可能在冷启动后复位,需开机重写。方案为 oneshot 型 systemd 服务(`WantedBy=multi-user.target`),执行 20 行 shell 脚本:写入、至多 5 次读回重试、结果写 syslog。验证一行命令:`journalctl -t ec-charge-limit`。 +部署后验证:`journalctl -t ec-charge-limit` 应含 `BTCC set to 80 (attempt 1)`。 ## 方法论:固件代码是最后的文档 @@ -108,4 +174,4 @@ TLP 配置存在而从未生效,根源在于用户态工具依赖内核 ABI � - 开机自动重写上限,冷启动不丢配置 - 电池档案:设计 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 反汇编与 ACPI 只读探测)、`ec-set-charge-limit.sh`(首次写入与 60 秒行为观察)、`power-rollback.sh`(一键回滚)。其他"EC 沉默"的机型可直接复用核心流程:反汇编 DSDT 检索字段表 → 手工推算偏移 → 只读交叉验证 → 验证命中后再写入。 |
