在部分 Intel MacBook 上运行 Linux 时,内置 eDP 液晶面板可能存在一个较隐蔽的兼容性问题:系统关屏、休眠、重新点亮屏幕或重新初始化显示链路时,面板的断电—重新上电周期过短,可能导致白屏、绿屏、显示异常,甚至需要重新关机才能恢复。
在特定硬件上,通过延长 Intel i915 驱动中的 Panel Power Cycle Delay 可以显著改善这一问题。
本文记录一次 Arch Linux LTS 内核从旧版本升级到新版本后,重新为新内核构建自定义 i915 模块,并将 eDP 面板最低电源周期固定为 2000 ms 的完整维护过程。
重点不在于重新编译整个 Linux 内核,而是:
在保持 Arch 官方内核不变的情况下,仅重新编译带有补丁的 i915 模块,并让它优先于官方 i915 被加载。
一、为什么每次内核升级后都需要重新构建 i915
假设系统原本运行:
6.18.45-2-lts
系统更新后安装:
6.18.49-1-lts
虽然两者都属于 Linux 6.18 LTS,但对于内核模块而言,它们是两个不同的 Kernel Release。
自定义编译的:
i915.ko
并不能简单地从:
6.18.45-2-lts
复制到:
6.18.49-1-lts
原因包括:
- Kernel ABI 可能发生变化
- 导出符号可能变化
CONFIG_MODVERSIONS相关 CRC 可能变化Module.symvers不同vermagic不同- i915 源码本身可能变化
因此,只要目标 Kernel Release 改变,自定义 i915 原则上就应该重新构建。
二、先不要重启
这是整个维护过程最重要的操作原则之一。
完成:
sudo pacman -Syu
以后,如果发现:
linux-lts
linux-lts-headers
已经升级到了新的版本,应当暂时保持当前旧内核继续运行。
例如:
uname -r
仍然显示:
6.18.45-2-lts
而:
pacman -Q linux-lts linux-lts-headers
已经显示:
linux-lts 6.18.49-1
linux-lts-headers 6.18.49-1
这正是理想状态。
此时:
- 旧内核仍在内存中运行
- 旧 i915 已经加载
- 新内核文件已经写入磁盘
- 可以安全地为新内核构建新的自定义 i915
- 完成全部准备之后再重启
这样可以避免启动进入尚未修补的新内核。
三、确认目标 Kernel Release
首先确定目标版本:
NEWREL="6.18.49-1-lts"
检查新内核:
pacman -Q linux-lts linux-lts-headers
然后检查:
cat /usr/lib/modules/$NEWREL/build/include/config/kernel.release
输出应精确为:
6.18.49-1-lts
再确认新内核官方 i915:
modinfo -k "$NEWREL" -n i915
通常会得到:
/lib/modules/6.18.49-1-lts/kernel/drivers/gpu/drm/i915/i915.ko.zst
以及:
modinfo -k "$NEWREL" i915 | grep '^vermagic:'
例如:
vermagic: 6.18.49-1-lts SMP preempt mod_unload
四、获取与 Arch 内核包完全对应的源码
不建议仅仅从 kernel.org 下载一个同版本的 vanilla Linux 源码然后直接编译。
更稳妥的方法是使用 Arch Linux 官方的 linux-lts packaging repository。
例如:
git clone https://gitlab.archlinux.org/archlinux/packaging/packages/linux-lts.git
cd linux-lts
拉取 tags:
git fetch --tags --force
检查目标包版本:
git tag -l "6.18.49-1"
切换到精确 tag:
git checkout --detach "6.18.49-1"
检查:
grep -E '^(pkgbase|pkgver|pkgrel)=' PKGBUILD
应类似:
pkgbase=linux-lts
pkgver=6.18.49
pkgrel=1
这样可以保证使用的不是“差不多的 Linux 6.18.49”,而是 Arch 实际用于构建:
linux-lts 6.18.49-1
的源码与补丁集合。
五、让 makepkg 准备完整源码树
执行:
makepkg --nobuild --nodeps
这一步会:
- 下载 Linux 源码
- 验证 SHA256 / BLAKE2
- 验证内核签名
- 解压源码
- 应用 Arch 自己的补丁
- 设置 Arch 的 kernel config
- 设置 local version
- 执行 PKGBUILD 的
prepare()
完成后通常会看到:
Prepared linux-lts version 6.18.49-1-lts
Sources are ready.
源码目录类似:
src/linux-6.18.49/
进入:
cd src/linux-6.18.49
然后确认:
make -s kernelrelease
必须得到:
6.18.49-1-lts
六、确认 i915 是模块
检查:
grep '^CONFIG_DRM_I915=' .config
应该为:
CONFIG_DRM_I915=m
这意味着 i915 会作为:
i915.ko
构建,而不是静态编入内核。
这也是能够单独重新编译 i915 的前提。
七、定位 eDP Panel Power Cycle Delay
目标文件:
drivers/gpu/drm/i915/display/intel_pps.c
搜索:
grep -n -B8 -A12 'panel_power_cycle_delay' \
drivers/gpu/drm/i915/display/intel_pps.c
其中关键代码类似:
intel_dp->pps.panel_power_up_delay =
pps_units_to_msecs(final->power_up);
intel_dp->pps.backlight_on_delay =
pps_units_to_msecs(final->backlight_on);
intel_dp->pps.backlight_off_delay =
pps_units_to_msecs(final->backlight_off);
intel_dp->pps.panel_power_down_delay =
pps_units_to_msecs(final->power_down);
intel_dp->pps.panel_power_cycle_delay =
pps_units_to_msecs(final->power_cycle);
最后这一行就是需要处理的位置。
八、加入最低 2000 ms 限制
原始代码:
intel_dp->pps.panel_power_cycle_delay =
pps_units_to_msecs(final->power_cycle);
修改为:
intel_dp->pps.panel_power_cycle_delay =
pps_units_to_msecs(final->power_cycle);
intel_dp->pps.panel_power_cycle_delay =
max(intel_dp->pps.panel_power_cycle_delay, 2000);
这里没有把:
panel_power_cycle_delay
简单强制设为 2000。
使用的是:
max(original_value, 2000)
因此逻辑是:
如果原始值 < 2000 ms:
使用 2000 ms
如果原始值 >= 2000 ms:
保留原始值
这比:
intel_dp->pps.panel_power_cycle_delay = 2000;
更加稳妥。
九、为什么修改的是最终计算结果
i915 的 Panel Power Sequencer 参数可能来自:
- VBT
- BIOS
- 硬件寄存器
- 默认值
- 驱动内部推导结果
在最终值已经计算完成之后再加入:
max(..., 2000)
相当于增加一个最终安全下限。
因此既不需要改变前面的硬件检测逻辑,也不需要重构 i915 的 PPS 代码。
十、重新确认 Kernel Release
补丁完成后再次执行:
make -s kernelrelease
仍然必须是:
6.18.49-1-lts
不要在这一阶段随意修改:
EXTRAVERSION
LOCALVERSION
CONFIG_LOCALVERSION
否则最终模块可能得到错误的 vermagic。
十一、准备模块构建环境
执行:
make -j"$(nproc)" modules_prepare
然后使用当前已安装 Arch 新内核自带的:
Module.symvers
例如:
KBUILD="/usr/lib/modules/6.18.49-1-lts/build"
cp "$KBUILD/Module.symvers" ./Module.symvers
检查两者:
cmp -s Module.symvers "$KBUILD/Module.symvers" &&
echo "Module.symvers: MATCH"
这里非常关键。
因为目标不是自己重新构建一个完整的新内核,而是给 Arch 已安装好的:
6.18.49-1-lts
构建一个兼容的额外模块。
因此应该使用该内核对应的官方 Module.symvers。
十二、只编译 i915
执行:
make -j"$(nproc)" M=drivers/gpu/drm/i915 modules
这会编译整个 i915 模块子树,而不是整个 Linux Kernel。
过程中会看到大量:
CC [M]
最终应出现:
LD [M] i915.ko
生成:
drivers/gpu/drm/i915/i915.ko
有些配置还可能同时生成:
kvmgt.ko
这属于正常现象。
十三、BTF 警告并不一定意味着失败
单独构建模块时可能出现:
Skipping BTF generation for i915.ko due to unavailability of vmlinux
这通常只是因为源码目录中没有完整内核构建产生的:
vmlinux
如果:
i915.ko
已经成功生成,而且后面的 modinfo 正常,则这不是构建失败。
十四、验证 vermagic
执行:
modinfo drivers/gpu/drm/i915/i915.ko |
grep '^vermagic:'
应与目标内核一致:
vermagic: 6.18.49-1-lts SMP preempt mod_unload
如果这里仍然出现旧内核版本,则绝对不要安装。
十五、剥离调试符号
刚刚编译出的 i915 可能非常大,例如超过 100 MB。
复制一份作为安装版本:
cp drivers/gpu/drm/i915/i915.ko /tmp/i915.ko
然后:
strip -g /tmp/i915.ko
体积通常会显著下降。
再次确认:
modinfo /tmp/i915.ko |
grep -E '^(name|vermagic):'
十六、压缩成 Arch 使用的 Zstandard 格式
Arch 内核模块通常使用:
.ko.zst
执行:
zstd -T0 -19 -f \
/tmp/i915.ko \
-o /tmp/i915.ko.zst
验证:
zstd -t /tmp/i915.ko.zst
十七、不要覆盖 Arch 官方 i915
官方模块位于:
/usr/lib/modules/6.18.49-1-lts/kernel/drivers/gpu/drm/i915/i915.ko.zst
不要覆盖它。
创建:
/usr/lib/modules/6.18.49-1-lts/updates/
安装自定义版本:
sudo install -d -m755 \
/usr/lib/modules/6.18.49-1-lts/updates
sudo install -m644 \
/tmp/i915.ko.zst \
/usr/lib/modules/6.18.49-1-lts/updates/i915.ko.zst
现在系统中同时存在:
官方版本:
kernel/drivers/gpu/drm/i915/i915.ko.zst
自定义版本:
updates/i915.ko.zst
这是一种非常适合维护自定义模块的结构。
十八、更新 depmod
执行:
sudo depmod -a 6.18.49-1-lts
然后检查:
modinfo -k 6.18.49-1-lts -n i915
理想结果:
/lib/modules/6.18.49-1-lts/updates/i915.ko.zst
这意味着:
updates/
中的版本已经成为 i915 的优先模块。
十九、进一步验证 modprobe 实际加载路径
执行:
modprobe -S 6.18.49-1-lts \
--show-depends i915
最后应该出现:
insmod /lib/modules/6.18.49-1-lts/updates/i915.ko.zst
这比仅检查文件是否存在更可靠。
它验证的是:
如果当前真的启动到这个内核并请求加载 i915,modprobe 会选哪个模块。
二十、重新生成 initramfs
执行:
sudo mkinitcpio -p linux-lts
如果配置中:
MODULES=()
没有强制加入 i915,同时:
HOOKS
中也没有让 i915 被提前打包进去,那么:
initramfs-linux-lts.img
里可能根本没有 i915。
这并不一定是问题。
此时 i915 会在根文件系统挂载后,由正常 module loader 从:
/usr/lib/modules/<kernelrelease>/
加载。
只要:
modinfo -k <kernelrelease> -n i915
以及:
modprobe -S <kernelrelease> --show-depends i915
都选择:
updates/i915.ko.zst
即可。
二十一、一个容易造成误判的权限问题
检查 initramfs 时,可能执行:
lsinitcpio /boot/initramfs-linux-lts.img
却得到:
ERROR: Unable to read file
这并不一定表示文件不存在。
很多系统生成的 initramfs 权限可能是:
-rw------- root root
因此普通用户无法读取。
应使用:
sudo lsinitcpio /boot/initramfs-linux-lts.img
先确认权限,再判断文件状态。
二十二、警惕 set -e
自动化维护脚本中经常会写:
set -e
它的含义是:
任意未被特殊处理的命令返回非零状态时,立即退出 shell。
例如:
grep pattern file
如果没有匹配结果,grep 返回:
1
这通常只是代表“没有找到”,不是严重错误。
但在:
set -e
环境下,它可能直接结束整个 shell。
如果终端模拟器设置成:
shell 结束后关闭窗口
表现就会像是:
终端突然关闭。
对于纯检查操作,可以使用:
grep ... || true
或者先:
set +e
避免这种情况。
二十三、重启前最终状态
在重启之前至少确认:
modinfo -k 6.18.49-1-lts -n i915
返回:
/lib/modules/6.18.49-1-lts/updates/i915.ko.zst
检查:
modinfo -k 6.18.49-1-lts i915 |
grep '^vermagic:'
正确。
检查:
modprobe -S 6.18.49-1-lts \
--show-depends i915
最终选择自定义模块。
然后才重启。
二十四、重启后的最终验证
启动后首先确认:
uname -r
例如:
6.18.49-1-lts
然后:
modinfo -n i915
应该是:
/lib/modules/6.18.49-1-lts/updates/i915.ko.zst
检查:
lsmod | grep '^i915'
确认模块已经正常加载。
二十五、最关键的验证:Panel Power Cycle Delay
找到:
i915_panel_timings
例如:
sudo find /sys/kernel/debug/dri \
-type f \
-name 'i915_panel_timings'
然后读取:
sudo cat \
/sys/kernel/debug/dri/<PCI-ID>/eDP-1/i915_panel_timings
成功情况下可以看到:
Panel power up delay: 200
Panel power down delay: 60
Panel power cycle delay: 2000
Backlight on delay: 1
Backlight off delay: 200
其中决定补丁是否真正生效的是:
Panel power cycle delay: 2000
只要这个值正确,就说明:
源码补丁
→ i915 编译
→ 模块安装
→ depmod
→ 模块选择
→ 系统启动
→ 驱动初始化
整条链路都已经真正生效。
二十六、自定义模块的签名警告
启动日志中可能出现:
i915: module verification failed:
signature and/or required key missing - tainting kernel
这是因为手工构建的:
i915.ko
没有 Arch 官方内核模块签名。
如果系统没有启用强制模块签名策略,而:
lsmod | grep i915
已经确认模块成功加载,则这通常只是:
kernel taint
标记,而不是 i915 功能异常。
对于本地维护的自定义内核模块,这是预期现象。
二十七、为什么 updates/ 方案值得保留
相比直接覆盖:
kernel/drivers/gpu/drm/i915/i915.ko.zst
将模块放入:
updates/i915.ko.zst
有几个明显优势。
第一,Arch 官方模块仍然完整保留。
第二,排障时可以快速删除:
updates/i915.ko.zst
然后:
sudo depmod -a <kernelrelease>
即可恢复官方模块。
第三,系统目录结构能够明确区分:
发行版模块
和:
本地维护模块
第四,验证非常简单:
modinfo -n i915
一眼就可以判断当前到底使用哪个版本。
二十八、每次 LTS 内核升级后的维护模板
今后如果:
6.18.x-y-lts
升级到新的:
6.18.x-z-lts
可以重复下面的流程:
pacman -Syu
│
▼
暂时不要重启
│
▼
确认新的 kernelrelease
│
▼
取得 Arch linux-lts 对应精确 tag
│
▼
makepkg --nobuild
│
▼
确认 make kernelrelease
│
▼
修改 intel_pps.c
│
▼
增加 max(panel_power_cycle_delay, 2000)
│
▼
modules_prepare
│
▼
复制目标内核 Module.symvers
│
▼
M=drivers/gpu/drm/i915 modules
│
▼
检查 i915.ko vermagic
│
▼
strip
│
▼
zstd 压缩
│
▼
安装到 modules/<release>/updates/
│
▼
depmod
│
▼
modinfo / modprobe 验证
│
▼
mkinitcpio
│
▼
重启
│
▼
确认实际加载自定义 i915
│
▼
读取 i915_panel_timings
│
▼
Panel power cycle delay = 2000
结论
Intel MacBook 上某些 eDP 面板异常并不一定是 LCD 本身损坏,也不一定是 GPU 故障。
如果问题与面板重新上电间隔有关,可以从 Intel i915 的:
Panel Power Sequencer
入手分析。
通过在:
drivers/gpu/drm/i915/display/intel_pps.c
中加入:
intel_dp->pps.panel_power_cycle_delay =
max(intel_dp->pps.panel_power_cycle_delay, 2000);
可以为面板重新上电周期设置一个最低 2000 ms 的安全限制。
更重要的是,维护方式不需要长期运行完整自定义内核。
只要:
- 使用 Arch 精确对应的内核源码
- 匹配正确的 Kernel Release
- 使用目标内核的 Module.symvers
- 单独重新编译 i915
- 验证 vermagic
- 将模块安装到
updates/ - 更新 depmod
- 在重启后检查实际模块路径
- 最终通过
i915_panel_timings确认 2000 ms
就可以在最大程度保留发行版内核维护便利性的同时,长期维护一个非常小而明确的硬件兼容性补丁。
对于需要依赖特定 i915 workaround 的 Intel MacBook,这种方案比长期维护整套自定义 Kernel 更轻量、更透明,也更容易回退。