Lenovo Legion Q7CN 风扇控制协议 — 完整逆向记录
目标:将 Legion Zone 的风扇控制功能移植到 Linux 结论:协议已完整破解,可在 Linux 上实现
背景
硬件信息
| 项目 | 值 |
|---|---|
| 型号 | Lenovo Legion Pro 7 (MTM: 83F4) |
| BIOS | Q7CN40WW (Gen 10, 2025 款) |
| EC 芯片 | ITE IT5508 (0x5508) |
| 风扇数量 | 3 (CPU/GPU/VRM) |
| 操作系统 | Windows 11 + Ubuntu 26.04 双系统 |
目标
Legion Zone 是 Lenovo 官方的 Windows 风扇/性能控制软件。Linux 下缺少等效工具,已有的开源项目(LenovoLegionLinux、lenovo-legion-linux-toolkit)不兼容 Q7CN 固件:
- LenovoLegionLinux (johnfanv2):风扇曲线读取全零,写入无效
- lenovo-legion-linux-toolkit (bolekjar):虽声称支持 16IRX9H,但风扇曲线实际控不住
- ChaoticSi1ence 的 fork:Custom 模式下会硬关机
必须从 Windows 端逆向出真实协议。
逆向过程
第一阶段:尝试抓 USB HID 包(失败)
最初的假设是风扇控制走 USB HID Feature Report 0xCC,尝试用 Wireshark + USBPcap 抓包。但随后发现 EC(嵌入式控制器)走的是 LPC/eSPI 总线,不经过 USB 控制器,USBPcap 抓不到任何数据。
第二阶段:WMI 静态探测
使用 Python WMI 库 dump 了所有 Lenovo 相关的 WMI 类:
LENOVO_FAN_METHOD ← 风扇方法LENOVO_FAN_TABLE_DATA ← 风扇表数据(只读结构)LENOVO_OTHER_METHOD ← WMAE 通用接口LENOVO_GAMEZONE_DATA ← 游戏区数据LENOVO_CAPABILITY_DATA_00 ~ 02 ← 功能能力表LENOVO_DISCRETE_DATA ← 离散值从静态 dump 中确认了 3 组风扇配置:
| 风扇 | Fan ID | Sensor ID | 最大转速 |
|---|---|---|---|
| CPU | 1 | 1 | 5100 RPM |
| GPU | 2 | 5 | 5100 RPM |
| VRM | 4 | 4 | 6500 RPM |
但静态 WMI 属性读取无法捕获动态变化——两次 dump 完全一致。
第三阶段:发现真实 WMI 方法
通过 PowerShell 的 Get-CimInstance 查看 LENOVO_FAN_METHOD 的实际方法签名:
Fan_Get_Table (FanID:UInt8, SensorID:UInt8, → FanTable:UInt32Array, SensorTable:UInt32Array)Fan_Set_Table (FanTable:UInt8Array)这与 LenovoLegionToolkit 源码中的 WMI.LenovoFanMethod.FanSetTableAsync() 一致。
第四阶段:缓冲区格式破解
首先基于 LenovoLegionToolkit 源码尝试 4 字节头部格式:
byte[0]=FSTM=1, byte[1]=FSID=0, byte[2-3]=FSTL=0, byte[4-23]=10×ushort结果:写入 RPM 值(1600-3500)触发 EC 保护断电!
发现 Fan_Get_Table 返回的不是 RPM,而是档位值 0-10。
改用档位值后写入不败但验证失败——数据偏移了 1 位。通过系统性测试不同偏移量:
| 测试 | 头部 | 结果 |
|---|---|---|
| offset=1 (8-bit, no header) | 仅 FSTM | [6,8,10,0,0,…] ← 偏移 |
| offset=2 (16-bit, FSTM+FSID) | 2 字节头 | [3,4,5,6,7,8,9,10,0,0] ← 偏移 |
| offset=6 (16-bit interlaced) | 6 字节头 | [1,2,3,4,5,6,7,8,9,10] ← 完美! |
| offset=1 interlaced | 2 字节头 | [6,8,10,0,…] |
| reverse test | 6 字节头 | [10,9,8,7,6,5,4,3,2,1] ← 完美! |
关键发现:Q7CN 风扇表缓冲区头部是 6 字节,不是 4 字节,且数据是 8 位值交替排列在 16 位字段的低字节中。
第五阶段:Process Monitor 追踪(辅助验证)
用 ProcMon 捕获 Legion Zone 的文件和注册表操作,发现 Legion Zone 内部日志路径。但 DLL(SystemThermalPlugin.dll、WSHardwarePlugin.dll 等)中没有直接暴露 WMI GUID 字符串,说明 WMI 方法是动态调用的。
ETW 追踪(Microsoft-Windows-WMI-Activity provider)也未捕获到有效事件——该 provider 可能需要在特定配置下才能记录方法调用。
最终协议
Fan_Set_Table 缓冲区格式
总长度:64 字节
偏移 长度 内容0x00 1 FSTM = 0x01(必须为 1,否则 EC 忽略。Linux 驱动曾发 0,是 bug 根因)0x01 5 保留(全 0)0x06 2 FSS0(位 0: 档位值, 位 1: 始终为 0)0x08 2 FSS10x0A 2 FSS20x0C 2 FSS30x0E 2 FSS40x10 2 FSS50x12 2 FSS60x14 2 FSS70x16 2 FSS80x18 2 FSS90x1A-0x3F 填充 0档位-转速对照
| 档位 | 转速(约) |
|---|---|
| 0 | 停转 |
| 1 | ~1600 RPM |
| 2 | ~1800 RPM |
| 3 | ~2000 RPM |
| 4 | ~2200 RPM |
| 5 | ~2400 RPM |
| 6 | ~2800 RPM |
| 7 | ~3200 RPM |
| 8 | ~3600 RPM |
| 9 | ~4000 RPM |
| 10 | ~5100 RPM (CPU/GPU) / ~6500 RPM (VRM) |
Fan_Get_Table 调用
输入:FanID (UInt8), SensorID (UInt8)输出:FanTable (UInt32[10]) — 10 个档位值 0-10 SensorTable (UInt32[10]) — 10 个温度阈值限制
- 风扇表仅在自定义模式下生效:安静/均衡/性能模式使用 EC 内部固话曲线,写入的风扇表被忽略
- 必须用 Fn+Q 切换到自定义模式:模式切换后 EC 重新加载风扇表
- 写入超范围值(>10 或 RPM 值)可能触发保护断电
Windows 实现
# 读取 3 个风扇曲线$fan = Get-CimInstance -Namespace root/WMI -ClassName LENOVO_FAN_METHOD$cpu = Invoke-CimMethod $fan -MethodName Fan_Get_Table -Args @{FanID=1;SensorID=1}$gpu = Invoke-CimMethod $fan -MethodName Fan_Get_Table -Args @{FanID=2;SensorID=5}$vrm = Invoke-CimMethod $fan -MethodName Fan_Get_Table -Args @{FanID=4;SensorID=4}
# 写入function Set-FanTable($vals) { $buf = [byte[]]::new(64) $buf[0] = 1 # FSTM for ($i=0; $i -lt 10; $i++) { $buf[6 + 2*$i] = [byte][Math]::Min(10, [Math]::Max(0, $vals[$i])) } Invoke-CimMethod $fan -MethodName Fan_Set_Table -Args @{FanTable=$buf}}
# 拉满Set-FanTable @(10,10,10,10,10,10,10,10,10,10)# 静音Set-FanTable @(1,1,1,1,1,1,1,1,1,1)Linux 移植方案
方案 A:修补 ChaoticSi1ence fork
git clone https://github.com/ChaoticSi1ence/legion-pro-7-16iax10h-linux.gitcd legion-pro-7-16iax10h-linuxsudo bash kernel_module/build-legion-module.sh需修补的点:
- Fan_Set_Table 缓冲区头部从 4 字节改为 6 字节
- Fan_Get_Table 的档位值从 UInt32 正确解析(已是 0-10)
方案 B:直接用 WMI char 设备
# /dev/wmi/ 下找到 GUID 92549549-4BDE-4F06-AC04-CE8BF898DBAA# 使用 ioctl(WMI_CMD) 发送 Fan_Set_Table 缓冲区方案 C:通过 firmware-attributes sysfs(如果内核暴露)
cat /sys/class/firmware-attributes/lenovo/attributes/fan_curve/current_value待办:在 Ubuntu 26.04 上实际测试方案 A 并修补
经验教训
- 不要假设 USB:笔记本 EC 通常走 LPC/eSPI,USB 抓包工具无效
- WMI 方法调用 vs 属性读取:
Get-CimInstance读的是静态类属性,真正的方法要Invoke-CimMethod - 缓冲区偏移要系统性测试:4 字节头是 LLT 的标准但 Q7CN 用了 6 字节,差 2 字节一切全乱
- 值域很重要:写 RPM 值而非档位值导致保护断电,差点以为协议完全错了
- Process Monitor 抓不到 WMI:ProcMon 追踪的是文件/注册表/进程,WMI 调用需要 ETW 或专门的 WMI 追踪工具
参考
- LenovoLegionLinux (johnfanv2) — 最早的开源 Linux 驱动
- LenovoLegionToolkit — Windows 端开源控制工具(协议参考)
- ChaoticSi1ence/legion-pro-7-16iax10h-linux — Q7CN 专用 fork
- lenovo-legion-linux-toolkit (bolekjar) — 另一个 Gen9/Gen10 驱动
- Q7CN PR #396 — ChaoticSi1ence 的 Q7CN 支持 PR
- Q7CN PR #407 — nuclearcat 的 WMAE 改进 PR
- Issue #385 — FSTM Bug 发现 — FSTM=0 vs FSTM=1 的发现过程
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时
