简介:针对联想拯救者R7000笔记本在Linux系统下触摸板无法正常识别、点击漂移等问题,这份补丁包以i2c-hid驱动独立源码形式提供修复方案。用户只需安装对应Linux内核头文件,执行make即可生成i2c-hid.ko模块,替换系统原有驱动并启用轮询模式,即可恢复触摸板功能。适合具备基础编译操作、熟悉终端命令的中级Linux用户,尤其适合使用Manjaro等发行版的笔记本玩家。压缩包共5个文件,包含2个C源码、2个头文件和1个Makefile构建脚本,整体仅24KB,体积小巧,结构清晰,便于直接审阅和二次编译。目前已有1466人学习浏览,方案经过社区验证。通过阅读C源码中的dmi quirks与核心处理逻辑,可深入理解i2c-hid驱动在异常硬件上的适配方式,同时也能掌握内核模块替换、depmod更新依赖及grub引导参数调整的完整排错思路,对排查其他Linux外设问题同样有参考价值。
1. 先搞懂 i2c-hid_standalone.zip:它是触摸屏驱动调试里最值得留一份的“独立包”
做触摸屏 bring-up 的工程师,多半都经历过这种循环:拿到一块新屏,改设备树,编内核,烧镜像,重启,看一眼 dmesg,再回去改。一次轮回半小时起步,如果再来回几次,一上午就没了。i2c-hid_standalone.zip 要解决的就是这个——把 i2c-hid 驱动从内核源码树里单独拎出来,配一个外部模块 Makefile、若干设备树片段和加载脚本,让你在 x86_64 主机上交叉编译出一个 .ko,直接搬上板子 insmod。所谓 standalone,和常见的 standalone installer 一个意思:拿到就是完整可运行的一套,不依赖全量内核源码树。
这类包适合两类人。一类是做触摸屏、触摸板或指纹模组选型和验证的工程师,需要在几分钟里确认某个模组到底能不能跑起来;另一类是内核版本被 BSP 锁死、不想为了一个驱动重烧整个 boot 镜像的嵌入式软件工程师。要注意的是,它不是万能药:驱动依赖的 CONFIG_HID、CONFIG_I2C 还是得在内核里开着,它只是把验证周期从半小时压缩到几分钟,把“编整个内核”这个黑匣子拆开,让你只碰需要碰的那几个变量。
2. i2c-hid 驱动为什么能 standalone:拆开 probe 链路看包里真正需要的是什么
2.1 i2c-hid 的定位是“协议驱动”,不是某款触摸屏的专用驱动
先澄清一个容易误导人的点:i2c-hid 不关心你的触摸屏是汇顶、新思还是义隆。它实现的是 HID over I2C 协议,这是一套标准的寄存器读写规范。设备侧暴露一组寄存器,驱动侧只做标准动作——上电、复位、读 HID Descriptor、传输 Input/Output Report。真正区分设备型号的逻辑在固件的 report descriptor 里,内核这边只负责按协议搬运,不负责理解业务。
这颗驱动在内核源码里的位置是 drivers/hid/i2c-hid/,从 4.x 一路到 6.x 都有,只是内部结构做过拆分。老版本可能就一个 i2c-hid.c 文件,新版本拆成 i2c-hid-core.c、i2c-hid-acpi.c、i2c-hid-of.c(或者 i2c-hid-dt.c)几个文件。无论怎么拆,probe 做的事情是一样的:I2C 子系统根据设备树或 ACPI 表生成一个 i2c_client,驱动匹配上之后,先向设备发 RESET 命令,等待设备就绪,然后读取 HID Descriptor,拿到 report descriptor 长度和版本号,再把整个设备注册进 HID core,最后由 HID core 解析成 input 设备。
换句话说,这套链路只跟 I2C 子系统、HID 子系统、Input 子系统打交道,和具体 SoC 的 pinctrl、clk、电源域没有强耦合。这就是它能被抽出来做独立编译的根本原因。只要目标内核里这三块是打开的,并且编译时的内核版本和运行版本一致,模块就能在板上正常加载,不需要重新烧写整个内核镜像。
2.2 standalone 与全量内核编译的差别:少了 rebuild 周期,多了两个风险
全量编译的路径是:改 dts 或 defconfig,进入内核源码根目录,make ARCH=arm64,生成 boot.img 或 kernel 镜像,烧写分区,重启验证。这套流程最大的问题是周期长,而且每次烧写都有把板子变成砖头的风险。standalone 路径短得多:用目标内核的 build 目录做 KDIR,在主机上外部编译出 i2c-hid.ko,scp 到板子上,insmod 或 modprobe,看 dmesg。一次轮回三分钟以内。
代价有两个。第一个是 vermagic 必须严格一致。全量编译时你烧的镜像就是当前环境,模块和内核天然配套;standalone 如果 KDIR 指错了内核版本,insmod 会直接报 Invalid module format。第二个是外部依赖的符号必须被内核导出。如果驱动里用到的某个函数在这个版本里是 static,或者没有 EXPORT_SYMBOL,加载时会报 Unknown symbol,这类问题基本只能换内核版本,没有好的绕路办法。
所以 standalone 包里的 Makefile 通常不会让你随便指向一个版本,而是会强调“先准备好目标板同版本的 build 目录”。这也是为什么我拿到任何 standalone 包,第一件事不是解压,而是先确认版本匹配,这一条放在下一章讲,它值得排在所有命令前面。
2.3 standalone 包里通常会有的几类文件:一份文件职责对照
我经手过的 standalone 包没有完全统一的规格,但骨架都差不多,一般逃不出下面这几样。它可能叫 standalone,也可能只是个散装目录,本质是一回事。
| 文件/目录 | 典型职责 | 常见的翻车点 |
|---|---|---|
| i2c-hid 源码目录 | 驱动本体,取自内核 drivers/hid/i2c-hid/ | 源码版本与目标内核不一致 |
| 外部模块 Makefile | 指定 obj-m 与 KDIR 变量 | KDIR 指向作者电脑路径 |
| build.sh | 封装 ARCH、CROSS_COMPILE、KDIR 的编译入口 | 交叉工具链前缀写错 |
| dts/overlay 片段 | 描述 I2C 地址、中断、reset 时序 | 中断类型与 hid-descr-addr 配错 |
| insmod/rmmod 脚本 | 按顺序加载依赖模块 | 忘记 depmod,modprobe 找不到依赖 |
| README/说明 | 写清目标内核版本与支持平台 | 文字描述与实际源码版本漂移 |
注意,我并不建议把这当成一个固定标准。很多人做 standalone 包就是简单粗暴地从内核源码里拷出 i2c-hid 目录,再加一个自己写的 Makefile 就算完事,文件数量和目录布局怎么舒服怎么来。你要做的是拿到包之后,先看明白它从哪里来、对应哪个内核版本,再动手改东西。
3. 在 x86_64 主机上交叉编译 i2c-hid_standalone:从解压到打出 .ko 的完整命令
3.1 开工前先确认三件事:目标内核版本、Kconfig、交叉工具链
下面这套命令以 ARM64 板子为例,x86、ARM、RISC-V 的区别只在 ARCH 和 CROSS_COMPILE 两个变量。第一步不是解压,而是在目标板上确认三件事。
# 在目标板上执行:记录完整版本号 uname -r # 检查 i2c-hid 依赖的内核配置有没有打开 zcat /proc/config.gz | grep -E '^CONFIG_(I2C|HID|INPUT)='代码里的 zcat 需要内核开启了 CONFIG_IKCONFIG 才会存在 /proc/config.gz。如果这个文件不存在,去 /boot/config-$(uname -r) 下面找;如果也没有,只能去 BSP 提供的 defconfig 里人工确认。CONFIG_I2C、CONFIG_HID、CONFIG_INPUT 这三项只要不是 n 就行,y 或 m 都可以,i2c-hid 作为外部模块加载不要求它本身必须是 m。
拿到 uname -r 的输出之后,去准备一份同版本的完整内核源码树。常见做法是直接找 BSP 的 kernel 目录,或者从内核官网下载对应 tag 的 tar 包。这个源码目录后续会作为 KDIR 使用,前提是它已经在主机上 make 过一次,生成了 .config、Module.symvers 和 include/generated/utsrelease.h。
提示:没有 make 过的内核源码是不能直接当地外部模块 KDIR 用的,第一次编外部模块会报缺少 generated/utsrelease.h,解决办法是先进入该源码目录执行 make ARCH=arm64 defconfig 或你常用的配置文件,再执行 make ARCH=arm64 prepare scripts。
3.2 解压后先看 Makefile:KDIR 是作者电脑的路径,不改必然失败
unzip i2c-hid_standalone.zip -d ~/work/touch cd ~/work/touch find . -maxdepth 3 -type f | sort head -n 40 Makefile第一件事是看目录结构,而不是立刻 make。find 帮你确认源码放在哪个子目录、有没有 dts 片段和加载脚本;head 看 Makefile 里有没有写死 KDIR、ARCH、CROSS_COMPILE。
这个环节最容易翻车。很多包在作者机器上能编过,是因为作者把 KDIR 写成了自己电脑上的绝对路径,比如 /home/td/linux,到了你这边路径根本不存在;或者 CROSS_COMPILE 用了作者自己安装的交叉工具链前缀,和你环境里的不一样。不要试图在原 Makefile 上打补丁,直接把这几行改成你自己环境的值是最省事的。
如果包里压根没有 Makefile,也不是不行。你可以把目标内核源码里 drivers/hid/i2c-hid/Makefile 拷出来作为模板,再按外部模块的方式改写。如果连源码目录都没有,那这个包就只是个空壳,你需要先从同版本内核 tar 包里把 drivers/hid/i2c-hid/ 整个目录抽出来放进去。
3.3 外部模块 Makefile 的基本形态:obj-m、KDIR、ARCH、CROSS_COMPILE
# 外部模块编译用的最小 Makefile # 多文件组合方式参考目标内核中 drivers/hid/i2c-hid/Makefile obj-m += i2c-hid.o i2c-hid-objs := i2c-hid-core.o i2c-hid-acpi.o i2c-hid-of.o KDIR ?= /path/to/target-kernel ARCH ?= arm64 CROSS_COMPILE ?= aarch64-linux-gnu- all: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) cleanobj-m 这一行决定最终生成的模块名,i2c-hid-objs 决定这个模块由哪几个目标文件拼起来。不同内核版本的组合差异很大:老版本可能只需要 i2c-hid.o 一个文件,新版本里 acpi 和 of 是独立编成各自模块的。编译时报“缺少 i2c-hid-of.o”或者“重复定义”时,不要瞎猜,打开目标内核源码里同一路径的官方 Makefile,照它抄。
KDIR 必须指向目标板同版本、且已经 make 过的内核源码树,不要填 /lib/modules/$(uname -r)/build,那通常是主机内核,指向它只能编出 x86_64 的模块。ARCH 填目标平台架构,这个变量一旦错,你 file 出来的 .ko 架构全错。CROSS_COMPILE 要能在 PATH 里找到,比如 aarch64-linux-gnu-gcc,不确定就用 which aarch64-linux-gnu-gcc 验一下。
3.4 编出来的 .ko 先别急着上板:file 与 modinfo 两道检查
# 编译当前目录下的外部模块 make -j$(nproc) # 检查产物架构与内核版本 file i2c-hid.ko modinfo i2c-hid.komake 默认会执行 Makefile 里的 all 目标,编出来的 i2c-hid.ko 就在当前目录。file 命令看两样东西:架构是否是 ARM aarch64、格式是不是 ELF shared object。如果 file 显示 x86-64,说明 ARCH 没生效或者 Makefile 里覆盖了你的设置,去检查 ARCH 是不是被写成 x86_64,或者 make 命令后面有没有环境变量串台。
modinfo 是上板前最关键的检查。重点看 vermagic 和 depends 两行。vermagic 里包含内核版本、SMP、抢占模式、架构等编译参数,必须和目标板 uname -r 输出一致,差一个小版本号都会导致 insmod 报 Invalid module format。depends 给出这个模块依赖的其他模块,一般是 i2c-core 和 hid。如果 depends 里有内容,上板后要么先 modprobe 依赖,要么直接 modprobe i2c-hid 让系统自动处理,不要 solo 裸 insmod。
3.5 目标板上的加载流程:depmod、modprobe、dmesg 三步
# 把模块放到目标板内核模块目录 scp i2c-hid.ko root@192.168.1.10:/lib/modules/$(uname -r)/extra/ # 登入目标板,重建模块依赖并加载 ssh root@192.168.1.10 "depmod -a && modprobe i2c-hid" # 查看驱动加载日志 ssh root@192.168.1.10 "dmesg | tail -n 30"scp 传过去之后先 depmod 再 modprobe,是因为 modprobe 需要依赖文件来解析模块依赖和符号。如果跳过 depmod,会出现明明文件在却提示 modprobe: module i2c-hid not found 这类情况。放在 extra 目录只是一个习惯,放在 /lib/modules/$(uname -r)/ 下任意标准子目录都可以,前提是 depmod 扫得到。
dmesg 里如果出现 Unknown symbol,说明这个版本内核没有导出驱动用到的某个符号,基本无解,去匹配另一套内核版本;如果出现 Invalid module format,回到 3.4 检查 vermagic;如果没有任何报错但也没有产生设备节点,多半是设备树或 ACPI 表里还没有生成对应的 i2c_client,也就是匹配根本没发生。这时候去查 dts 里有没有 compatible 为“hid-over-i2c”的节点,I2C 总线号对不对。
4. i2c-hid 独立模块上板排查:5 个我把板子搞到黑屏/死机才记住的坑
4.1 现象:probe 显示成功,但所有 read 回来的都是 0xFF
原因:I2C 速率过高、上拉电阻缺失或设备地址配错。触摸屏模组一般标称支持 400kHz,但很多国产屏在 400kHz 下 SDA/SCL 时序跟不上,回应你的就是全 F。上拉电阻没焊或者阻值太大,总线被拉到不定状态,也是同样的表现。还有一种情况是设备树里 reg 写的 0x38,但实际模组地址是 0x40,probe 匹配到了错误的 I2C 地址,读出来自然全乱。
解决:先别让驱动背锅,用 i2c-tools 直接探测地址。
# 在目标板上扫描 5 号 I2C 总线上的设备地址 i2cdetect -y -r 5 # 尝试读取 0x38 地址的 0x00 寄存器 i2cget -y 5 0x38 0x00i2cdetect 能列出总线上响应的地址,i2cget 能读回具体寄存器内容。如果读回来不是 0xFF,说明链路是通的,问题在地址或速率;如果读回来是 0xFF,先把 dts 里 I2C 控制器的 clock-frequency 改成 100000 再试,同时检查板上 I2C 上拉电阻。这个顺序能省掉大量怀疑驱动的时间。
4.2 现象:insmod 之后突然中断风暴,系统直接卡死
原因:IRQ 触发类型配错。触摸屏 INT 引脚多数是低电平有效,设备主动拉低表示有数据要上报。如果设备树里配了 IRQ_TYPE_LEVEL_LOW,而硬件上 INT 外部上拉没做或者模块上电时序异常,驱动一加载就收到持续低电平,中断 handler 被反复触发,系统直接卡死。配成 IRQ_TYPE_EDGE_FALLING 可以暂时不风暴,但会丢中断,触摸表现为时灵时不灵。
解决:先查模组规格书确认 INT 有效电平,再改设备树重编 overlay。
// 设备树片段:修正中断触发类型为低电平有效 &i2c5 { touchscreen@38 { compatible = "hid-over-i2c"; reg = <0x38>; interrupt-parent = <&gpio1>; interrupts = <13 IRQ_TYPE_LEVEL_LOW>; hid-descr-addr = <0x0001>; }; };改完中断类型之后,用示波器量 INT 脚在没有触摸时的静态电平。如果低电平是常态,说明引脚配置或外部电路有问题,不是驱动能解决的。这个坑最容易在“把别的项目 dts 抄过来”的时候出现,抄的时候顺手把别人的 IRQ_TYPE 也抄了,但不同模组的 INT 极性可能相反。
4.3 现象:hid_dump_device 读 descriptor 失败,长度永远为 0
原因:复位后延时不够。HID over I2C 规范里,驱动复位设备之后要等待设备准备好才能读 HID Descriptor,但不同模组的准备时间差距很大。post-reset-delay-ms 配 5ms 甚至不配,很多屏还没从复位里恢复过来,驱动去读 descriptor 长度,读回来就是 0 或者根本无响应。
解决:把复位延时调大,同时检查 reset-gpios 的极性。有些国产屏的 reset 脚在模组上是反的,标称低有效实际要高有效,配反了等于一直按着复位键不放。
// 设备树片段:修正复位引脚与延时 touchscreen@38 { compatible = "hid-over-i2c"; reg = <0x38>; reset-gpios = <&gpio1 15 GPIO_ACTIVE_LOW>; post-reset-delay-ms = <80>; };post-reset-delay-ms 从 20 开始往上调,80 是常见的稳定值。如果加了延时仍然读不到,把示波器挂到 reset 脚上,看驱动加载时有没有先拉低再拉高的动作,没有就是 GPIO 没申请到或 dts 里 reset-gpios 写错引脚。
4.4 现象:系统睡眠唤醒后触摸没反应,重启才能恢复
原因:独立模块没有和板子的电源域、pinctrl 完整配合。很多触摸屏在 suspend 时被断电,唤醒后需要重新复位、重新读 descriptor,但 standalone 模块往往只实现了基础的 probe 和 remove,PM 回调没有正确处理。另外 reset 引脚在 suspend 时被 pinctrl 切到别的状态,导致设备始终处于复位中。
解决:短期用运行时手动 re-bind 来验证是不是 PM 问题。长期要进入驱动里补 power management 回调。
# 应急恢复触摸:重新绑定 i2c-hid 驱动 echo "i2c-hid" > /sys/bus/platform/drivers/i2c-hid/unbind echo "i2c-hid" > /sys/bus/platform/drivers/i2c-hid/bind如果这两条命令能让触摸恢复,基本可以确定是 PM 场景下的设备状态没有恢复。再深入查二点:suspend 期间 reset 引脚是否被 pinctrl 拉高,以及设备的 supply 是否被电源域关掉、上电时序是否满足模组要求。这种问题在全量编译的内核里一样存在,但独立模块更容易暴露,因为它没有跟着 BSP 一起走完 PM 适配。
4.5 现象:同一套包和 dts,换了另一批模组就失灵
原因:模组变更了 I2C 地址或 hid-descr-addr。这是最让人抓狂的情况。同一型号的屏,不同批次可能把地址从 0x38 改成 0x10,或者把 HID Descriptor 放在 0x0020 而不是标准默认的 0x0001。驱动代码没变,变的只是两个设备树字段,但现象是整块屏彻底不工作。
解决:换屏之后先扫描 I2C 地址再做修改。
# 扫描总线上现有设备,确认新模组实际地址 i2cdetect -y -r 5拿到实际地址后改 dts 的 reg。如果改了地址仍然 probe 失败,再检查 hid-descr-addr。标准 HID over I2C 规范里,HID Descriptor 所在寄存器偏移是 0x0001,但部分厂商会放在 0x0020,这个值配错,驱动会在 probe 阶段读到长度为 0 的 descriptor。
// 部分模组需要把 descriptor 偏移改到 0x0020 touchscreen@10 { compatible = "hid-over-i2c"; reg = <0x10>; hid-descr-addr = <0x0020>; };另外注意 compatible 字段。老内核里可能只识别“hid-over-i2c”,新内核支持带厂商前缀的 compatible 字符串。不要把厂商前缀和“hid-over-i2c”混在一起乱配,最好两个字符串都写上。
5. 用 standalone 包验证触摸屏设备树配置:建议保留的调试脚本与检查顺序
5.1 insmod 前用 i2c-tools 把“硬件链路”从“驱动问题”里剥出来
折腾次数多了之后,我养成一个固定习惯:驱动加载之前,先把 I2C 地址从硬件层面验一遍。这个动作能把至少一半的排错成本砍掉。如果 i2cdetect 都扫不到地址,就不用花时间调试驱动了,直接回去查原理图、焊接和上拉。
#!/bin/bash # check_touch.sh # 用法: ./check_touch.sh <i2c_bus> <i2c_addr> <event_device> BUS=$1 ADDR=$2 EVT=$3 # 第一步:确认 I2C 地址是否在线 i2cdetect -y -r ${BUS} # 第二步:读取设备 ID 寄存器,非 0xFF 则链路正常 i2cget -y ${BUS} ${ADDR} 0x00 2>/dev/null # 第三步:看驱动加载日志 dmesg -T | grep -i i2c # 第四步:确认内核是否识别出 i2c client ls -l /sys/bus/i2c/devices/ # 第五步:检查 input 子系统和触摸事件 cat /proc/bus/input/devices | grep -A 8 -i touch evtest ${EVT}这个脚本的思路是分层排查。第一步验证硬件,第二步验证数据线,第三步验证驱动,第四步验证 bus 匹配,第五步验证 input 上报。哪一步断了就只查哪一段,不要把时间浪费在怀疑整条链路上。
5.2 我的习惯:把地址、中断、hid-descr-addr 三样写进 dts 模板再编译
每次拿到新模组,先把三个值从规格书或实测里确认下来,再动 dts。
| 配置项 | 常见取值 | 确认方法 |
|---|---|---|
| reg 地址 | 0x10、0x38、0x40 都有 | i2cdetect 实测 |
| interrupt 类型 | LEVEL_LOW 或 EDGE_FALLING | 示波器看 INT 电平 |
| hid-descr-addr | 0x0001 或 0x0020 | probe 失败时逐一尝试 |
这三个值都不需要编译内核,只需要改 dts 重新生成 overlay,配合 standalone 模块在几分钟内完成验证。地址错读不到设备,中断错会死机,descriptor 偏移错探不到 HID 描述符,把这三个值稳定住,剩下的就是模组固件行为上的微调。
以前我拿到一块新屏,第一反应是改 dts、编内核、烧镜像,折腾半天才到 probe。后来我把 i2c-hid 抽成 standalone 包,把这张三个值的检查模板贴在工位上,每次只改三个字段,半小时一个轮回变成三分钟一个轮回。这个习惯帮我省下大量重复编译时间,也减少了很多因为内核版本不匹配带来的玄学问题。希望对你也有用。
本文还有配套的精品资源,点击获取