把 Surface Pro 7 当 NAS 用的时候,最别扭的一件事是:飞牛认不出它自带的电池。UPS 页面里空空如也,断电就直接硬关机。

现象

飞牛装好之后,系统里查不到任何电池设备:

ls /sys/class/power_supply
# 空目录

资源监控里的电池卡片、设置里的 UPS,自然都是空的。但硬件明明是好的——电池能充电、能续航。

原因:Surface 第 7 代把电池挪出了 ACPI

普通笔记本的电池走 ACPI 的 PNP0C0A 设备,内核的 acpi/battery 一加载,/sys/class/power_supply/BAT0 就出现了。发行版内核都会编这个,所以「笔记本当 NAS」一般是开箱即用的。

Surface Pro 7 属于微软第 7 代 Surface。从这一代开始,电池和供电信息不再通过标准 ACPI 暴露,改由 SSAM(Surface System Aggregator Module,板载嵌入式控制器) 直接提供,ACPI 命名空间里只留一个控制器节点:

MSHW0084   SSAM 控制器
MSHW0116   Surface Pro 7 的平台枢纽(7+ 是 MSHW0119)

硬件在、ACPI 也枚举出来了,但没有驱动去绑定它。而 NAS 内核里那些通用电池驱动——sbs-battery、max17042_battery、bq27xxx_battery、ds2760_battery——全都要 I2C 或 ACPI 电池接口,对 Surface 一个都用不上。

结果就是:系统认为这台机器没有电池。

思路:拿目标内核的头文件,把缺的驱动编出来

Linux 上游从 5.13 起就支持这套硬件了,代码一直都在,只是发行版没编。需要的模块有五个:

模块作用产出
surface_aggregatorSSAM 总线 / 控制器核心—
surface_aggregator_registry设备注册表,按 ACPI ID 认机型创建电池、供电等子设备
surface_battery电池驱动/sys/class/power_supply/BAT0
surface_charger供电驱动/sys/class/power_supply/ADP1
crc-itu-tSSAM 用到的 CRC-ITU-T 校验—

所以不需要换内核,只要用机器上现成的内核头文件做一次「外部模块编译」,把 .ko 塞进 /lib/modules/ 就行。

过程中踩到的三个坑

1. 缺 crc_itu_t

surface_aggregator 依赖内核的 crc_itu_t(),而 NAS 内核通常没开 CONFIG_CRC_ITU_T。加载时会报:

surface_aggregator: Unknown symbol crc_itu_t (err -2)

解决办法是从同一份内核源码里把 lib/crc/crc-itu-t.c 编成模块补上(6.9 之前这个文件在 lib/crc-itu-t.c)。

2. CONFIG_SURFACE_AGGREGATOR_BUS 没开,头文件退化成空实现

这个选项没启用时,include/linux/surface_aggregator/device.h 和 aggregator/bus.h 会把相关函数写成 static inline 空函数,和 bus.c 里的真实定义撞车:

bus.c:379:5: error: redefinition of 'ssam_bus_register'

不用改内核源码,编译时加一个宏就行:

make -C <headers> M=<src> KCFLAGS="-DCONFIG_SURFACE_AGGREGATOR_BUS" modules

3. 模块签名

内核 CONFIG_MODULE_SIG=y 但没有 CONFIG_MODULE_SIG_FORCE 时,未签名模块照样能加载(只会在 dmesg 里留一句 tainted)。所以编译时直接 CONFIG_MODULE_SIG=n,省掉配签名密钥的麻烦。如果目标内核开了 SIG_FORCE,这条路走不通。

安装

已经把这套东西打包开源了:FnOS-UPS-SurfacePro7

方式一:直接用预编译包(最快)

先去 Releases 下对应内核版本的压缩包,解压后一条命令:

uname -r      # 先确认内核版本,例如 6.18.18.c1186-trim
tar -xzf FnOS-UPS-SurfacePro7-6.18.18.c1186-trim.tar.gz
cd FnOS-UPS-SurfacePro7-6.18.18.c1186-trim
sudo sh install.sh

脚本会校验 SHA256、复制模块到 /lib/modules/<内核版本>/extra/surface-battery/、执行 depmod、写 /etc/modules-load.d/surface-battery.conf 实现开机自启,最后尝试立即加载。

预编译包的 vermagic 必须和当前内核完全一致,版本对不上会报 invalid module format——那就用方式二。

方式二:源码编译(任意内核版本)

需要系统里有 Docker(飞牛自带),以及两个文件:

ls /usr/src/linux-headers-$(uname -r)   # 要含 Module.symvers
ls /boot/config-$(uname -r)

然后:

git clone https://github.com/YunJian101/FnOS-UPS-SurfacePro7.git
cd FnOS-UPS-SurfacePro7
sudo sh install.sh

脚本会起一个临时的 Debian 容器装好 gcc/make,按 uname -r 对应的上游版本号下载内核源码,只解出需要的几个文件,编完直接装进系统。

验证

ls /sys/class/power_supply
# ADP1  BAT0

cat /sys/class/power_supply/BAT0/capacity     # 电量百分比
cat /sys/class/power_supply/BAT0/status       # Charging / Discharging / Full
cat /sys/class/power_supply/ADP1/online       # 1 = 接着电

modinfo surface_battery

我这边实测的数据:

属性值
电芯厂商SMP
型号M1109598
满电电压8.37 V
循环次数420
SSAM 固件14.800.139

重启后再 ls 一次,确认开机自动加载也正常。

已知问题

  • 资源监控里的电池卡片仍可能是空的。 实测 appcgi.resmon.battery 一直返回 errno 100030002,即使内核已经正确暴露了 BAT0;手动触发 uevent 重扫、重启 resmon_service 都无效。但 UPS 设置页面可以正常使用内置电池,不影响实际用途。
  • 只验证了 x86_64。
  • 模块未签名,内核开了 CONFIG_MODULE_SIG_FORCE 则不可用。

内核升级之后

模块是按内核版本编的,vermagic 必须匹配。系统升级内核后重新跑一次 sudo sh install.sh 即可(方式二会自动按新内核现编)。

链接

整套东西纯属折腾,但折腾完之后,那块 Surface 的电池终于能当 UPS 用了。