
上个月在调一台RK3399平台、Android 7.1系统的收银机主板。客户在量产前临时换了一款触摸屏触摸IC用的是GSL5680。原厂给了一个压缩包里面是驱动源码、一份GSL5680参数配置文件外加一个简短的移植说明。当时我以为这活儿半天就能干完拷贝驱动、加设备树、编内核、烧录、完事。结果光编译这一步就折腾了两天报错一个接一个而且每个报错背后都牵扯到“新旧内核API代差”这个问题。这篇文章把我实际踩过的编译坑、排查思路和最终改法完整记下来给后面要在这套平台接GSL5680或者同系列的GSL3680/GSL1680的朋友做个参考。如果你和我一样是在RK3399的Android 7.1 SDK内核通常为4.4上移植一份老平台过来的GSL5680驱动这篇文章大概率能帮你省下不少时间。适合人群内核驱动移植刚入门的人可以照着改已经在做触摸驱动调试的人也可以看看我在中断和休眠框架上的处理方式对比一下自己的写法有没有隐患。1. 换一块屏幕为什么等于要重编一次内核驱动很多人会有疑问触摸屏驱动不就是一个.c文件加一个.h文件吗放进内核源码树里开个宏编译出来不就行了如果GSL5680驱动是原厂专门为RK3399的Android 7.1定制过的确实是这样。但现实恰恰相反。市场上大量GSL系列驱动源码是从全志、三星、MTK老平台流出来的里面的代码夹杂着各种mach/gpio.h、s3c_gpio_cfgpin这类老接口直接扔进RK3399的内核里根本编不过。1.1 原厂驱动包里的东西比想象中“乱”我拿到的压缩包解开后目录结构大概是这样的gslx680/ ├── gsl_ts.c // 主驱动文件 ├── gsl_ts.h // 驱动头文件 ├── gslx680.c // 芯片初始化、固件下载相关 ├── GSL5680.csv // 触摸屏参数配置 ├── Makefile // 独立编译用的Makefile基本要改 └── README.txt // 移植说明GSL系列驱动有个特点同一个gslx680.c里会保留GSL1680、GSL3680、GSL5680等多种型号的处理代码通过宏或者平台数据来区分配置。这个设计本意是“一套代码覆盖多个IC”但副作用就是代码里到处都是条件编译尤其是老平台留下的#ifdef块编译时稍不注意就走到错误的分支里。还有一个容易被忽略的点压缩包里的Makefile是给你在驱动目录里单独编译用的路径、交叉编译器都写死了。放进RK3399这个Android 7.1内核里这套独立Makefile基本不能用得把它改成由Kconfig/Makefile统一管理的形式否则编译系统根本不会把你的驱动编进去。1.2 Android 7.1编译内核的基本姿势RK3399的Android 7.1 SDK一般把内核放在kernel/目录下。完整编译内核镜像有两条路第一条在Android源码顶层编bootimage。source build/envsetup.sh lunch rk3399_box-userdebug make bootimage -j16这种方式编译的是Android的boot.img里面包含内核镜像和ramdisk。好处是不用手动管内核的交叉编译器SDK里的prebuilts工具链会自动带上。第二条单独编内核。cd kernel export ARCHarm64 export CROSS_COMPILE../prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin/aarch64-linux-android- make rockchip_defconfig make -j16单独编内核的好处是编得快出错了能快速迭代。我实际调试时基本是单独编内核每改一次代码只重新编内核确认没有编译错误后再回到Android源码顶层执行make bootimage。注意不同SDK版本里的defconfig文件名可能不一样。有的是rockchip_defconfig有的是rockchip_linux_defconfig先看kernel/arch/arm64/configs/下实际有哪些文件别照抄命令。我第一次把GSL5680驱动源码丢进内核目录、开好配置项后执行编译心里还想着“应该一遍过”结果终端里噼里啪啦刷出一堆红色报错。好在报错信息彼此独立一个一个来。2. 第一个编译错误mach/gpio.h 不存在老平台的头文件依赖在RK3399内核目录下我把驱动代码放进drivers/input/touchscreen/gslx680/然后修改drivers/input/touchscreen/Makefile加上一行obj-$(CONFIG_TOUCHSCREEN_GSLX680) gslx680/同时在Kconfig里加了配置项执行make menuconfig把CONFIG_TOUCHSCREEN_GSLX680勾上。接着编译终端弹出来的第一个致命错误就是这个drivers/input/touchscreen/gslx680/gsl_ts.c:39:31: fatal error: mach/gpio.h: No such file or directory #include mach/gpio.h ^ compilation terminated.2.1 为什么RK3399内核里没有mach/gpio.h先解释一下mach/gpio.h是从哪来的。早期Linux内核里的ARM平台每个厂商比如三星、全志、瑞芯微都会在arch/arm/mach-xxx/目录下维护一套专属的板级头文件mach/gpio.h就是其中一种用来定义这个平台的GPIO操作、中断号映射等。驱动代码里#include mach/gpio.h编译时由编译器到对应的平台目录下找这个头文件。但RK3399这套系统是arm64架构内核4.4之后主要靠Device Tree来描述硬件资源arch/arm64/下没有传统意义的mach-xxx目录自然也就不存在mach/gpio.h。老驱动写出#include mach/gpio.h到arm64内核这里直接找不到头文件编译立刻就死了。这里你可能会问为什么GSL原厂不给一个通用的驱动原因很现实。GSL的驱动大多是给方案商定制的有些方案商代码管理混乱一个版本用在所有平台上时间一长就堆积了大量平台相关代码。所以拿到驱动先不要急着改业务逻辑第一步先把平台相关的头文件和API清理干净。2.2 修复方法把平台头文件换成内核通用接口我的处理方式是把gsl_ts.c和gslx680.c里所有mach相关头文件全部删掉换成Linux内核通用的GPIO接口#include linux/gpio.h #include linux/of_gpio.h然后处理GPIO操作函数。老驱动里这种代码非常常见#include mach/gpio.h static void gsl_reset_chip(void) { s3c_gpio_cfgpin(GSL_RESET_GPIO, S3C_GPIO_OUTPUT); gpio_set_value(GSL_RESET_GPIO, 0); mdelay(10); gpio_set_value(GSL_RESET_GPIO, 1); mdelay(10); }其中s3c_gpio_cfgpin和S3C_GPIO_OUTPUT是典型的Samsung老平台API。在RK3399上GPIO的复用和方向控制已经统一走pinctrl子系统配合gpiolib驱动里不需要再手动配置引脚MUXDTS里会做所以这里直接替换成static void gsl_reset_chip(void) { if (gpio_is_valid(gsl_reset_gpio)) { gpio_request(gsl_reset_gpio, gsl_reset); gpio_direction_output(gsl_reset_gpio, 0); mdelay(10); gpio_direction_output(gsl_reset_gpio, 1); mdelay(10); } }gsl_reset_gpio这个变量不再写死某个gpio号而是从DTS里解析出来。这样就把驱动从“某个平台专用”改成了“通用设备树驱动”。这一步改完重新编译第一道报错消失紧接着第二道报错冒出来了。3. 第二个编译错误中断申请参数不匹配warning被当成error第一道头文件问题解决后编译继续往下走又卡在gsl_ts.c的中断申请函数附近报错信息类似这样drivers/input/touchscreen/gslx680/gsl_ts.c: In function gsl_ts_probe: drivers/input/touchscreen/gslx680/gsl_ts.c:582:18: error: passing argument 3 of request_irq makes pointer from integer without a cast [-Werrorint-to-pointer-cast] request_irq(ts-irq, gsl_ts_irq_handler, ts-irq_flags, gsl_ts, ts); ^ In file included from include/linux/interrupt.h:21:0, from drivers/input/touchscreen/gslx680/gsl_ts.c:55: include/linux/interrupt.h:134:12: note: expected irq_handler_t {aka int (*)(int, void *)} but argument is of type int3.1 报错里最关键的信息是什么很多人一看报错就开始猜是不是函数名写错了是不是头文件没包含其实从note那行就能看清了——request_irq的第三个参数应该是irq_handler_t也就是一个函数指针但驱动里传进去的是一个整数ts-irq_flags。这说明参数的个数不对老代码可能默认request_irq的旧原型是4个参数中断号、处理函数、flag但这份驱动代码在长期修改中把中断号和处理函数的位置写错了或者在做条件编译时把request_irq和request_threaded_irq混用导致参数错位。另一个点是-Werror。内核在4.4版本上很多厂商的SDK会默认打开-Werror把编译警告升级为错误。也就是说即使这段代码在别的平台上只是报警告在RK3399这套SDK里直接被当成错误中止编译。这是我建议所有人拿到新平台先确认-Werror是否打开的原因。3.2 RK3399下中断号应该从哪来GSL5680驱动的中断号来源在新平台上有标准做法。RK3399使用Device Tree描述中断驱动在probe阶段通过client-irq直接拿中断号前提是DTS里I2C子节点正确写了interrupts属性。但很多老驱动会写死一个GPIO宏比如#define GSL_IRQ_GPIO GPIO_PD3 ts-irq gpio_to_irq(GSL_IRQ_GPIO);这种写法在RK3399上最大的问题是GPIO编号不固定同一份内核可能因为DTS中GPIO bank分组的差异而映射错误。正确的做法是struct device_node *np ts-client-dev.of_node; ts-irq irq_of_parse_and_map(np, 0); if (!ts-irq) { dev_err(ts-client-dev, failed to get irq from dts\n); return -EINVAL; }irq_of_parse_and_map会把DTS中interrupts属性转换成Linux中断号避免手动调gpio_to_irq带来的映射问题。这里还有一个细节很多老驱动直接使用client-irq作为中断号这在设备树模式下可行但要求DTS节点里确实写了interrupt-parent和interrupts。我最开始只配了interrupt-parent gpio3漏了interrupts属性结果运行起来后触摸中断根本不触发这是后话。3.3 修复后的中断申请代码修复完中断号来源后我又把驱动里的中断申请改成request_threaded_irq因为在触摸驱动里真正的处理函数里要读取I2C寄存器上报数据不适合在硬中断上下文执行ret request_threaded_irq(ts-irq, NULL, gsl_ts_irq_handler, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, gsl_ts, ts); if (ret) { dev_err(ts-client-dev, request irq failed: %d\n, ret); return ret; }触发方式这里我选了IRQF_TRIGGER_FALLING因为GSL5680的中断脚是低电平有效按下时产生下降沿。如果你的硬件设计不一样可能要用IRQF_TRIGGER_RISING或双沿触发这取决于触摸板的中断脚接到主控哪个GPIO、以及触摸IC的中断输出极性。编译错误修完了运行起来才发现触发方式不对的情况我见过太多次所以编译阶段就确认好dts里interrupts gpio3 RK_PA6 IRQ_TYPE_EDGE_FALLING和驱动里的flag保持一致。4. 第三个编译错误休眠唤醒框架变了early_suspend的坑把中断申请修好继续编译这次报错落在驱动文件的末尾错误长这样drivers/input/touchscreen/gslx680/gsl_ts.c:941:2: error: implicit declaration of function register_early_suspend [-Werrorimplicit-function-declaration] register_early_suspend(gsl_early_suspend); ^ drivers/input/touchscreen/gslx680/gsl_ts.c:942:2: error: implicit declaration of function unregister_early_suspend [-Werrorimplicit-function-declaration] unregister_early_suspend(gsl_early_suspend); ^4.1 early_suspend机制为什么会失效register_early_suspend是Android早期为屏幕休眠设计的回调机制在老内核3.0、3.4、3.10时代配合CONFIG_HAS_EARLYSUSPEND使用。驱动注册一个early_suspend结构体系统在休眠时提前调用它的suspend回调让触摸屏先进入低功耗模式。到了Android 7.1、内核4.4时代这个机制已经被弱化。虽然某些厂商内核还保留着CONFIG_HAS_EARLYSUSPEND的兼容代码但RK3399的SDK里默认没有打开这个配置内核里甚至可能根本没有导出register_early_suspend这个符号。所以驱动里的#ifdef CONFIG_HAS_EARLYSUSPEND分支没走编译器直接报“隐式声明”。这类报错看起来是“函数没声明”本质上却是内核睡眠框架演进导致的兼容性问题不是加个头文件就能糊弄过去的。4.2 标准做法改用struct i2c_driver的suspend/resume回调RK3399 Android 7.1上触摸驱动做休眠唤醒最标准的做法是使用struct i2c_driver自带的suspend和resume回调。驱动probe时注册的是i2c_driver这个结构体本身就包含了电源管理回调接口。修改方式如下把驱动里早期休眠相关的#ifdef块整体删除然后在i2c_driver结构体里加上标准回调static const struct dev_pm_ops gsl_ts_pm_ops { .suspend gsl_ts_suspend, .resume gsl_ts_resume, }; static struct i2c_driver gsl_ts_driver { .probe gsl_ts_probe, .remove gsl_ts_remove, .id_table gsl_ts_id, .driver { .name gsl_ts, .of_match_table gsl_ts_of_match, .pm gsl_ts_pm_ops, }, };在suspend回调里关掉触摸中断、停掉工作队列、设置IC进入低功耗在resume回调里重新初始化IC、申请中断。GSL5680有个特点唤醒后如果不重新下发初始化参数可能出现触摸没反应或者坐标偏移所以我在resume里直接调用了gsl_ts_init_chip()确保触摸屏工作正常。4.3 如果驱动里早期休眠代码块太多怎么办有些厂家的GSL5680驱动里#ifdef CONFIG_HAS_EARLYSUSPEND的代码块东一块西一块直接删容易手误。我的习惯是先用grep -n EARLYSUSPEND -r drivers/input/touchscreen/gslx680/把所有引用点列出来再逐个确认。基本会有三类#include早期suspend头文件——直接删struct early_suspend定义和全局变量——直接删probe/remove里的register/unregister调用——替换成标准pm接口这一类问题修完后编译终于能完整走完生成kernel.img。但到这里我反而更警惕编译过了只是第一步真正让它跑起来才是重头戏。5. 编译通过后DTS这关没过照样不工作驱动编译干净之后我把编译出来的内核和资源镜像打包烧录到板子上开机串口日志里能看到gsl_ts驱动probe但也伴随着一堆错误触摸完全没反应。检查下来问题出在DTS配置上。可以说这个阶段比编译错误更隐蔽因为你看不到红色报错只有运行日志里的诡异现象。5.1 RK3399的I2C触摸节点怎么写RK3399有多个I2C控制器具体用哪个取决于触摸屏接到哪一组引脚。以接到I2C4为例DTS里要新增一个子节点i2c4 { status okay; gsl568040 { compatible gsl,gsl5680; reg 0x40; interrupt-parent gpio3; interrupts RK_PA6 IRQ_TYPE_EDGE_FALLING; reset-gpio gpio3 RK_PA5 GPIO_ACTIVE_LOW; irq-gpio gpio3 RK_PA6 GPIO_ACTIVE_LOW; }; };这个节点里最重要的几个属性compatible要跟驱动.of_match_table里的字符串完全一致否则设备不会和驱动匹配上。我在gsl_ts.c里看到的是gsl,gsl5680DTS里就必须写一模一样包括大小写。reg 0x40这是GSL5680在I2C总线上的从机地址。GSL5680的7位地址常见是0x40或0x41具体看芯片的ADDR引脚接法。如果地址不对I2C通信会失败驱动在probe阶段读取芯片ID就过不了。interrupts这行的RK_PA6表示gpio3组的A6脚。注意RK3399的GPIO分组是gpio0~gpio4每组A/B/C/D定义在dt-bindings/pinctrl/rockchip.h里。我这里初始踩过坑写成了gpio3 6 IRQ_TYPE_EDGE_FALLING没带RK_PA6宏导致中断号解析错乱。5.2 复位脚和中断脚不要搞反在DTS里配置复位脚和中断脚时很多人会下意识认为既然叫“irq-gpio”那它和interrupts的功能差不多。其实两者有明确分工interrupts是告诉内核这个设备的中断请求线接在哪个GPIO上用来注册Linux中断。irq-gpio是给驱动自己读取的GPIO号比如驱动在初始化时要判断中断脚当前电平。reset-gpio是复位脚驱动在初始化芯片时拉低再拉高让IC复位。如果irq-gpio和interrupts指向同一个引脚这是正常的。但reset-gpio一定不能和中断脚用同一个GPIO否则初始化时拉低复位脚会把中断脚也拉低系统永远检测不到中断边沿。这个顺序问题我排查了很久最后用万用表量引脚电平才发现。5.3 烧录后从内核日志确认驱动装载状态DTS改好后重新编译内核并烧录。开机后在串口执行dmesg | grep -i gsl正常情况下应该能看到类似输出gsl_ts: gsl_ts_probe enter gsl_ts: gsl5680 detected, fw_ver0x60 gsl_ts: input: gsl_ts as /devices/platform/ff160000.i2c/i2c-4/4-0040/input/input3如果只看到probe enter没有后续输出多半是I2C通信失败或者芯片初始化失败如果连probe enter都看不到说明设备树节点和驱动没匹配上检查compatible和I2C总线编号。6. 触摸唤醒、坐标错位、无中断上报的排查经验驱动能正常probe之后还有几类运行时问题几乎每次都会碰到。我把这三个高频问题的排查经验整理到一起这部分内容不算是编译错误但属于“编译过了之后一定用得上的知识”。6.1 用getevent快速验证触摸是否上报驱动加载成功后首先用getevent看输入事件getevent -l然后手指在屏幕上滑动如果终端里有EV_ABS、ABS_MT_POSITION_X、ABS_MT_POSITION_Y这类事件输出说明驱动的中断和触摸上报链路是通的。如果没任何事件先看中断触发方式cat /proc/interrupts | grep gsl观察中断计数有没有变化。如果中断数没变说明硬件中断没进来重点排查DTS里的interrupts和实际接线如果中断数在增加但没有输入事件说明I2C读取数据有问题用i2c-tools抓一下寄存器看芯片ID能否读出来。6.2 休眠唤醒后触摸失灵的处理我调试的这块板子在屏幕点亮时触摸正常但合盖休眠再唤醒后屏幕亮了触摸却死了。日志里没有报错中断也能触发但input子系统收不到数据。这个问题的根因在GSL5680的一个特性它依赖驱动在唤醒后重新初始化寄存器像固件参数重新下发的过程。如果resume回调里只做了中断恢复没有重新初始化芯片芯片内部的电源状态可能不对触摸坐标计算全乱。我最终的做法是在resume里加上完整的初始化流程static int gsl_ts_resume(struct device *dev) { struct gsl_ts *ts dev_get_drvdata(dev); enable_irq(ts-irq); gsl_ts_init_chip(ts); return 0; }注意顺序先开中断再初始化芯片。如果先初始化芯片开中断之前芯片已经准备好但中断脚可能在初始化过程中产生边沿被漏掉。先开中断能保证边沿不丢失。6.3 坐标反向或偏移时的调整方法坐标反向一般不涉及编译代码只需要在驱动里修改坐标映射方式。GSL5680驱动里通常有一段配置触摸方向的地方ts-swap_xy 0; ts-x_invert 0; ts-y_invert 0;不同的屏幕模组安装方向不同这四个值的组合直接用暴力试错法也行改一次编一次太慢。建议把这几个变量运行时可调通过内核启动参数传进去或者临时在sysfs里暴露几个节点调试效率会高很多。6.4 我个人一直在用的“三查”习惯移植GSL系列驱动尤其是从老平台拿源码时我总结了一个“三查”习惯查头文件全项目搜一遍mach/、plat/、asm/arch/这种路径的头文件。出现一个清理一个别让它们混过去。查API签名对request_irq、request_threaded_irq、gpio_direction_output、input_register_device这类常见函数直接在内核的头文件里查原型比对参数类型和数量。老驱动改平台后出错的八成都是这类问题。查配置宏打开Kconfig确认依赖的宏是否开启比如CONFIG_OF、CONFIG_INPUT、CONFIG_TOUCHSCREEN_GSLX680。有时候编译错误只是表象真正的问题是某个前置配置没开导致代码里大量条件编译分支没有定义。这三个习惯看似简单但每次都能帮我快速定位问题。有一次帮同事看一个编译报错他找了半个小时没头绪我用git grep搜了一下他加的代码里用到的GPIO函数在哪个头文件声明发现他少include了一个linux/of_gpio.h这问题在交叉编译工具链下不会自动带出必须手动加。这种细节只有多踩几次坑才能形成条件反射。GSL5680的驱动不算复杂但因为它长期在各个方案商之间流传代码里沉淀了大量历史包袱。只要把平台相关的东西清干净理解设备树、中断、休眠框架之间的关系编译和调试都只是时间问题。