首页 / 资讯中心 / 文章详情

Linux下GT9xx触摸屏驱动non-DT移植实战:从i2c_board_info到中断线程化

Linux下GT9xx触摸屏驱动non-DT移植实战:从i2c_board_info到中断线程化 ★ FEATURED ARTICLE
1. 传统的x86/ARM平台为什么还在坚持non-DT移植这年头说起Linux触摸屏驱动几乎所有的资料都在讲device tree怎么写node怎么配。但老实说直到2024年我在实际项目中仍然有一大半时间在处理非device tree的移植。手头这个GT9xx触控芯片的移植任务就是典型case——目标平台是一颗老旧的ARM SoC内核版本停在3.x/4.x平台代码还在用传统的board file描述硬件。客户明确要求不碰设备树说穿了就一句话现有BSP改不起设备树动一发牵全身。先解释一下这个“非基于device tree”到底怎么回事。早在device tree大规模普及之前ARM Linux平台上描述硬件的方式是直接在arch/arm/mach-xxx/目录下的板级文件里写死资源比如platform_device结构体、I2C board info、GPIO编号、中断号。驱动侧则通过i2c_driver/platform_driver的id_table或of_match_table完成匹配。GT9xx这种I2C接口的电容触控芯片在老内核里最正统的做法就是static struct i2c_board_info __initdata xxx_i2c_devices[] { { I2C_BOARD_INFO(gt9xx, 0x5D), .platform_data gt9xx_platform_data, .irq gpio_to_irq(CTP_GT9XX_INT_GPIO), }, };然后调用i2c_register_board_info(bus_num, xxx_i2c_devices, ARRAY_SIZE(xxx_i2c_devices))挂到对应I2C总线上。这套流程放在device tree时代看简直“土得掉渣”但在不改内核版本、不动bootloader的前提下它反而是最稳的方案。选择non-DT还有一层工程上的考量老平台的内核态往往塞了大量厂商私有修改显示、电源管理、PMIC等等升级到支持设备树的内核版本成本极高而单纯增加一个触控芯片驱动改动范围完全可以控制在几个文件内——一个板级补丁加一个驱动目录不碰配置系统、不动内存映射。只要芯片本身OK这个方案是性价比最高的。2. 移植前必须搞清楚的硬件细节引脚、I2C地址和时序要求GT9xx系列GT911/GT912/GT913等的寄存器配置和初始化逻辑基本一脉相承但不同型号某些细节会有差异。这里分享一套我反复用、很少出错的准备工作流程耗时大约半天但能省下后面至少三天调试时间。2.1 原理图与GPIO分配核对拿到原理图后第一件事不是看驱动代码而是核对GT9xx的四个关键引脚INT中断输出通常接SoC的一个GPIO需支持中断触发。GT9xx默认高有效但也可以通过配置寄存器改成低有效。RST复位输入低电平复位。通常接一个GPIO控制部分方案直接和SoC复位共连这种做法不推荐但确实常见。SCL/SDAI2C时钟和数据线确认接在哪个I2C控制器上几号bus。VDDIO/VDD供电引脚确认电压域1.8V或者3.3V这个直接关系到I2C上拉电阻的取值。一个很容易踩的坑GT9xx的I2C地址是可配置的。芯片有ADD引脚接高电平时7位地址是0x5D8位读写地址对应0xBA/0xBB接低电平是0x148位地址0x28/0x29。我遇到过一次硬件工程师拍脑袋把ADD接高驱动里写0x14结果i2c_detect怎么都扫不到设备。所以移植前一定要确认地址代码里用的地址必须和硬件实打实对得上。2.2 I2C时序与外设时钟确认GT9xx的标准I2C速率上限是400kHz快速模式。但老SoC的I2C控制器经常有坑——默认分频算出来的实际频率可能跑到500k甚至更高。别小看这个问题触控IC对时序相当敏感速率过高会导致读回的数据偶尔错位在触摸坐标上报时表现为“跳点”或者“鬼点”。我自己习惯在代码里做一个I2C速率探测struct i2c_client *client; client-adapter-algo-master_xfer(client-adapter, msgs, num);如果发现通信不稳定直接去board文件里确认I2C控制器时钟源和分频系数。以某常见老平台为例I2C时钟计算公式大致是PCLK / (prescale * 2)调制到300kHz~380kHz区间工作最稳。3. 板级资源的非设备树描述platform_data与i2c_board_info在non-DT模式下驱动获取硬件信息的核心手段就是platform_data和i2c_board_info。理解这两者的关系是整个移植的关键。3.1 定义GT9xx的platform_data结构GT9xx驱动往往需要拿到以下硬件参数复位引脚号GPIO中断引脚号GPIO触控面板的物理尺寸X轴/Y轴最大坐标翻转/镜像标志用于面板装配方向修正是否支持按键虚拟按键区域坐标在板级文件中我们可以这样定义#include linux/input/gt9xx.h static struct gt9xx_platform_data gt9xx_config { .reset_gpio GPIO_CTP_RESET, .int_gpio GPIO_CTP_INT, .x_max 1080, .y_max 1920, .swap_xy false, .mirror_x false, .mirror_y false, .key_num 0, };这个结构体是专门为GT9xx驱动定义的需要查阅芯片原厂发布的驱动源码或者datasheet中对寄存器参数的说明。它的好处是编译期就能检查字段是否匹配比DT的字符串属性通过of_property_read_u32多一层安全检查。3.2 在I2C board info里注册设备接下来是核心挂载代码。这个函数要放到板级文件的__init函数里执行而且必须确保它所属的I2C控制器已经被初始化i2c_register_board_info本身只登记信息真正创建设备是在对应adapter注册时。static struct i2c_board_info __initdata gt9xx_i2c_devs[] { { I2C_BOARD_INFO(gt9xx, GT9XX_I2C_ADDR), .platform_data gt9xx_config, .irq gpio_to_irq(GPIO_CTP_INT), }, }; static int __init xxx_board_init(void) { i2c_register_board_info(2, gt9xx_i2c_devs, ARRAY_SIZE(gt9xx_i2c_devs)); return 0; }注意一点gpio_to_irq在部分老平台上可能返回无效值尤其是GPIO控制器和中断控制器不完全一致时。稳妥做法是先调gpio_request申请GPIO再调gpio_direction_input设置方向最后调用gpio_to_irq。如果是用GpioI2c之类的方式模拟I2C那就走platform_device platform_driver的路线但GT9xx用硬件I2C接口是主流做法。4. 驱动核心移植从probe到中断上报的完整链路在拿到芯片驱动代码后大部分厂商会提供Linux驱动但质量参差不齐移植的核心工作是把驱动里的匹配方式改为非DT的id_table方式并适配platform_data获取参数。下面直接展示一个可用性较高的驱动骨架。4.1 I2C driver注册与匹配GT9xx原厂驱动往往默认按device tree的compatible goodix,gt9xx来做匹配。我们需要给它加上传统的i2c_device_idstatic const struct i2c_device_id gt9xx_i2c_id[] { { gt9xx, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, gt9xx_i2c_id); static struct i2c_driver gt9xx_i2c_driver { .probe gt9xx_probe, .remove gt9xx_remove, .id_table gt9xx_i2c_id, .driver { .name gt9xx, .pm gt9xx_pm_ops, }, }; module_i2c_driver(gt9xx_i2c_driver);注意module_i2c_driver这个宏在2.6.38以后的内核里都能用但如果你的内核特别老2.6.3x可能需要手动写module_init和module_exit函数。事实上我当时在内核3.4的老平台上就遇到过这个宏不通用的情况所以给大家留个心。4.2 probe函数中的资源获取probe函数里的资源获取在non-DT下和DT下有明显的风格差异static int gt9xx_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct gt9xx_platform_data *pdata client-dev.platform_data; struct input_dev *input_dev; struct gt9xx_ts *ts; int ret; if (!pdata) { dev_err(client-dev, No platform data found\n); return -ENODEV; } ts kzalloc(sizeof(*ts), GFP_KERNEL); ... ts-client client; i2c_set_clientdata(client, ts); // 申请复位GPIO并将触控IC复位于正确状态 ret gpio_request(pdata-reset_gpio, gt9xx_reset); gpio_direction_output(pdata-reset_gpio, 1); msleep(5); gpio_set_value(pdata-reset_gpio, 0); msleep(15); gpio_set_value(pdata-reset_gpio, 1); msleep(50); ... }这里顺带讲清楚GT9xx的上电时序需要经过一次完整的“拉低复位、保持若干ms、拉高复位、等待固件初始化完成”的过程。时序不够尤其是拉高后的等待时间少于50ms芯片内部固件可能没跑起来后续I2C读取时设备完全无响应。这个50ms不是拍脑袋而是原厂spec里明确写的——不过实际工作中我发现有些批次芯片需要更长的等待保险起见可以拉到100ms影响微乎其微。4.3 中断处理与input子系统上报GT9xx通过INT引脚通知主机“有触摸数据啦”主机在中断处理函数里通过I2C读取触摸坐标并通过input子系统上报。代码如下static irqreturn_t gt9xx_irq_handler(int irq, void *dev_id) { struct gt9xx_ts *ts dev_id; struct gt9xx_touch_info touch_info; u8 buf[8]; int i; // 读取GT9xx的坐标状态寄存器 gt9xx_i2c_read(ts-client, GT9XX_TOUCH_DATA, buf, sizeof(buf)); touch_info.num buf[0] 0x0F; if (touch_info.num 0) { for (i 0; i touch_info.num; i) { u8 *ptr buf[1 i * 6]; u16 x (ptr[1] 8) | ptr[0]; u16 y (ptr[3] 8) | ptr[2]; input_report_key(ts-input_dev, BTN_TOUCH, 1); input_report_abs(ts-input_dev, ABS_MT_POSITION_X, x); input_report_abs(ts-input_dev, ABS_MT_POSITION_Y, y); input_mt_sync(ts-input_dev); } input_sync(ts-input_dev); } else { input_report_key(ts-input_dev, BTN_TOUCH, 0); input_mt_sync(ts-input_dev); input_sync(ts-input_dev); } return IRQ_HANDLED; }关于坐标读取有一点必须提醒GT9xx的寄存器地址是16位的I2C读取时要用寄存器地址高位低位分别发送我见过不少人在这个地方搞错。另外坐标寄存器每包数据里除了X/Y坐标还带触摸ID、面积、轨迹编号等信息是否需要解析取决于上层应用的需求。简单的Android触摸屏只需要坐标Windows或某些特殊行业应用可能需要ID来跟踪多点轨迹。4.4 注册input设备input_dev input_allocate_device(); if (!input_dev) return -ENOMEM; input_dev-name gt9xx-touchscreen; input_dev-phys i2c-gt9xx; input_dev-id.bustype BUS_I2C; input_dev-dev.parent client-dev; input_set_capability(input_dev, EV_KEY, BTN_TOUCH); input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0, pdata-x_max, 0, 0); input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0, pdata-y_max, 0, 0); ret input_register_device(input_dev);input子系统这里没什么玄学但有一点要注意如果你要支持多点触摸用input_mt_init_slots扩展出一个带ABS_MT_SLOT的协议比上面用input_mt_sync的type B半残版本更正规。老内核可能没有input_mt_init_slots这个API这时退回type A也不是不能用只是Android框架处理多点会有兼容瑕疵。5. 中断引脚配置的两条路线硬中断和线程化中断GT9xx芯片在触摸时产生中断信号主机怎么响应这个中断在移植时有讲究。老内核里最常见的方式是直接request_irq 一个workqueue来处理I2C读取因为中断上下文不允许直接调用I2C传输函数可能导致睡眠。但老内核里I2C传输本身可能依赖信号量、等待队列等睡眠操作直接调会被内核报“BUG: scheduling while atomic”。5.1 传统方式request_irq workqueue我见过原厂驱动的经典实现static irqreturn_t gt9xx_irq_handler(int irq, void *dev_id) { struct gt9xx_ts *ts dev_id; disable_irq_nosync(irq); schedule_work(ts-work); return IRQ_HANDLED; } static void gt9xx_work_func(struct work_struct *work) { struct gt9xx_ts *ts container_of(work, struct gt9xx_ts, work); gt9xx_read_and_report(ts); enable_irq(ts-irq); }这套流程的缺点是中断可能在sleep前被屏蔽吞吐量低还容易丢失中断。好处是内核兼容性好2.6.x都能编译过。我自己在非常老的内核2.6.35上用的就是这个方案。5.2 推荐方式request_threaded_irq线程化中断如果内核版本在3.x以上强烈建议改用线程化中断ret request_threaded_irq(client-irq, NULL, gt9xx_irq_thread, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, gt9xx, ts);在线程函数里可以直接调用I2C读函数不用关心原子上下文问题static irqreturn_t gt9xx_irq_thread(int irq, void *dev_id) { struct gt9xx_ts *ts dev_id; gt9xx_read_and_report(ts); return IRQ_HANDLED; }我强烈不建议再用第一代workqueue方案非线程化做新项目——中断延迟高还有disable_irq丢失事件的风险。实测中线程化中断方案在Android触摸体验上明显更跟手主要表现在快速滑动时坐标流更平滑不会因为中断处理不及时导致一帧里缺少关键坐标点。触发方式这块也值得聊一下。GT9xx的INT引脚行为默认配置下只要芯片有触摸数据INT引脚会一直拉低直到主机读完数据后短暂释放再准备下一次。所以用IRQF_TRIGGER_LOW做电平触发其实也可以但用边沿触发读数据后清标志的方式更常规。我在两个平台上分别做过多组对照边沿触发但需要读完后释放避免中断风暴与低电平触发但必须保证读数据及时最终结论是边沿触发更省心不容易出现中断风暴导致CPU占用飙高的问题。6. 配置寄存器写入与固件升级让触摸IC按你的意愿工作GT9xx除了读坐标还有一类核心操作是向芯片写入config数据。所谓config其实是芯片内置的触控参数灵敏度、滤波系数、面板尺寸、按键映射等的寄存器集芯片上电后从内置Flash加载。驱动初始化时往往需要重新把这些参数写入以配合实际屏幕模组。6.1 config数据应该写在哪GT9xx内部有独立的Flash驱动初始化时可以通过一个专门的寄存器控制位加载配置。原厂一般提供一份cfg配置文件有时叫gt9xx_config.h或cfg_data.h里面是一串十六进制数组static const u8 gt9xx_config_data[] { 0x00, 0x00, 0x02, 0x10, 0x02, ... };这串数据的第一段包含了触控IC的基础配置区域数值必须在初始化阶段写入。写入方法是通过I2C先向芯片发送一个“进入配置模式”的命令然后把数据逐字节写入到对应的配置寄存器地址。6.2 实际写入流程static int gt9xx_update_config(struct gt9xx_ts *ts) { int ret; u8 buf[2]; // 进入配置模式 buf[0] 0x80; buf[1] 0x00; ret gt9xx_i2c_write(ts-client, 0xFFFF, buf, 2); // 写入配置数据主体 ret gt9xx_i2c_write(ts-client, 0x8047, (u8 *)gt9xx_config_data, sizeof(gt9xx_config_data)); // 软复位让配置生效 gt9xx_reset(ts); ... }写配置这事有讲究不同批次/版本的GT9xx芯片配置数据集的长度和部分字段可能不一样混用会导致触摸坐标偏移或者灵敏度异常。所以移植时最好从模组厂获取匹配该批次芯片的最终配置数据而不是直接用驱动包里默认的。如果不方便从模组厂要调试阶段可以先用原厂demo配置等屏幕装配后的实际效果出来再微调配参。6.3 固件升级要不要做很多项目会问“需不需要驱动里集成固件升级功能”。我的答案很直接量产阶段没必要除非芯片固件有已知bug或需要新增手势支持。驱动集成固件升级意味着固件bin要存放在固定分区驱动需要拥有写Flash的能力并处理好升级失败后的回退升级过程不可被打断不能断电、不能进suspend非要做的话可以放在工厂模式里等应用层工具触发。驱动运行时跑在线升级万一碰到嵌入式产品量产恢复会额外增加售后压力。7. 常见问题与排查技巧实录这一部分是我最想写的因为移植驱动时90%的时间都耗在这些“不按套路出牌”的问题上。以下每个case都是我至少踩过一次的实际经验。7.1 I2C读不到设备扫描都是FF出现这种情况先别怀疑驱动代码。按下面顺序排查硬件供电VDDIO、VDD量了吗用万用表量芯片引脚3.3V域必须稳定在3.0V以上我之前遇到一次电池供电电压跌落导致设备反复reset的case。I2C地址对不对ADD引脚的上下拉和探测地址是否一致。复位是否被拉死万用表看RST引脚电平如果是低电平找找是不是板上某个看门狗电路在捣乱。上I2C逻辑分析仪实测看看发出的地址有没有ACK。如果Windows下调试方便接USB-I2C适配器先试读芯片把确认芯片是好的这个前提先提下来。很多新手一上来就改driver代码改一整天发现是虚焊这种教训实在太多。7.2 触摸坐标乱串滑动轨迹断裂但点击识别正常坐标乱串一般是坐标解析错位要么是寄存器地址定义和实际固件版本不匹配要么是X字节序搞反。GT9xx坐标寄存器是16位默认是大端格式高位在前低位在后。如果代码按小端读取坐标会变成某个固定偏移或者出现跳跃式错误。还有一种情况是x/y像素最大值设置偏小导致坐标被input子系统按比例截断。在驱动probe里打印原始寄存器值比对实际按角落时的坐标值是最直接的定位手段。7.3 中断一直触发CPU占用100%十有八九是中断触发方式和芯片输出电平不匹配。GT9xx在没触摸时INT是高电平触摸时INT拉低。如果注册中断用了IRQF_TRIGGER_HIGH空空荡荡的手机屏幕上任何一点噪波都可能引发中断风暴。修复方法ret request_threaded_irq(client-irq, NULL, gt9xx_irq_thread, IRQF_TRIGGER_FALLING | IRQF_ONESHOT, gt9xx, ts);另外如果读数据不够快读完后INT还没释放下一次边沿检测就会被触发形成反复进入中断。因此配置数据更新后一定要做一次软复位清掉芯片内部的挂起条件。7.4 suspend/resume后触摸失效suspend里没做电源关断/复位时序或者中断线没正确唤醒都是原因。一个比较稳妥的suspend流程是static int gt9xx_suspend(struct device *dev) { struct gt9xx_ts *ts dev_get_drvdata(dev); disable_irq(ts-irq); // 如果系统的GPIO在suspend期间能保持只需把触控IC置为休眠 gt9xx_i2c_write(ts-client, 0x8040, power_ctrl, 1); return 0; } static int gt9xx_resume(struct device *dev) { struct gt9xx_ts *ts dev_get_drvdata(dev); gt9xx_reset(ts); gt9xx_update_config(ts); enable_irq(ts-irq); return 0; }特别提醒resume后如果发现触摸坐标漂移优先检查是不是配置数据没重写成功。我碰到过一次就是resume只做了复位但没重新写config芯片按Flash里的默认参数跑坐标全飘。7.5 多指触摸跳点、合并一个是中断频率不够另一个是坐标读回后没按触摸ID分slot。老旧的type A协议本来就不支持稳定的多点跟踪问题解决方向的优先级是优先迁移到type B的input_mt_init_slotsinput_mt_slot方式然后保证中断频率不低于芯片自身扫描率的两倍。8. 如何让GT9xx在non-DT内核中稳妥落地一套可复用的调试清单如果你手头正好有这个需求给出一份可以直接照着做的checklist按顺序执行能省下大量盲调时间。8.1 内核配置确认在menuconfig里确认以下几项是开着的I2C controller driver对应硬件接口Input core supportInput event devices如果你要编译成模块则不需要改内核主体但要在板文件里预留模块加载顺序依赖firmware loader的依赖等8.2 板上确认清单我每次做新板移植必查一遍以下项目把结果记录下来再开始敲代码检查项预期值实测值触控IC型号GT911 / GT912 / 具体批次打印丝印I2C地址0x14 / 0x5D 由ADD决定逻辑分析仪测ACKINT GPIO无中断时高电平万用表RST GPIO上电瞬间有脉冲示波器供电电压1.8V/3.3V稳定万用表面板分辨率从模组厂规格确认查规格书坐标方向是否需镜像/翻转实装后测试8.3 驱动日志分级建议开发阶段在驱动里加上动态打印用的宏#define GT9XX_DBG(fmt, args...) \ pr_debug([GT9XX] %s: fmt \n, __func__, ##args)然后通过内核命令行参数dynamic_debug或echo file gt9xx.c p /sys/kernel/debug/dynamic_debug/control打开调试打印。真正上量产时把debug关闭或者降为error级别。8.4 坐标校准需不需要做GT9xx这类电容屏出厂时固件内部已经做过线性校准Linux端不需要像电阻屏那种五点/九点校准。除非你的屏幕模组贴装严重偏移否则不需要交给用户去校准。如果真出现整体偏移优先调整的是config数据里的坐标原点和缩放参数而不是依赖用户空间校准。9. 对比一下device tree挂载GT9xx的差异虽然项目用的是non-DT但理解DT方式有助于写代码时避开部分隐含的API变更。两者核心差异如下对比项non-DT传统board filedevice tree资源描述platform_data / i2c_board_infoDT节点属性匹配机制i2c_device_id / platform_driver.id_tableof_match_table compatibleGPIO获取代码里写死GPIO编号of_get_named_gpio中断获取gpio_to_irqirq_of_parse_and_map扩展性每次换硬件平台要改C代码改dts比较灵活老内核兼容2.6.x到3.x全都行需要3.x以上且支持DT调试友好度编译期检查强但板级改动体验差可运行时改DT调试方便为什么两者要同时了解因为在现实项目中你会遇到一些特殊的混合平台主系统用device tree但某个协处理器的BSP还在走传统板级文件。我做过的方案里就有主控跑Linux 4.9 device tree协处理器跑Linux 3.0 board file两边各自管一路GT9xx互不干扰。如果你只熟悉device tree一条路遇到这种项目会非常难受。从工程角度看这块驱动代码如果同时保留两种路径的适配通过#ifdef CONFIG_OF做条件编译代码可维护性会好很多。但这也有代价宏污染会让代码阅读体验变差。我个人的建议是如果项目注定长期跑在non-DT内核上就干净利落地只保留id_table匹配 platform_data读取别写一堆#ifdef CONFIG_OF的死分支。10. 关于这块驱动源码的后续演进所谓“不做device tree”指的是当前内核版本下不用of_match_table而不是说代码结构可以不管DT。写驱动时留意把“获取硬件资源”和“操作硬件”分成两个层次以后平台升级到DT内核时只需要替换资源获取层、保留操作层即可。以probe函数为例static int gt9xx_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct gt9xx_platform_data *pdata dev_get_platdata(client-dev); // 在这之上再做一层资源抽象未来如果切换到DT只需要单独实现 // gt9xx_parse_dt(struct device *dev) 即可下面的逻辑完全复用。 }我在另一个项目里就是这样做的老内核上保留board file升级内核版本后新建一个gt9xx_dt.c专门解析设备树节点然后probe入口以of_match_table为主。两次改动加起来不超过200行驱动主体代码一行没动就完成了跨版本迁移。这块经验证明了一件事移植驱动时比“当前能不能跑”更重要的是“代码边界是不是划得干净”。你现在花20分钟做的抽象可能在未来节省三天返工时间。另外如果后续要支持Android/GMS认证或者CTS的触控测试则需要把ABS_MT_TRACKING_ID这类type B协议的字段补全并处理好通过input子系统上报时的时间戳单调性。这些工作都不会太困难但都需要你在驱动里预留口子。GT9xx的移植说穿了不难难的是把每一个细节都做对从I2C时序到中断线程化、从config数据到suspend/resume这些基本功扎实了任何类似的老式触控IC项目汇顶的GT系列、思立微、新思等都能举一反三快速上手。
阅读完成 · 觉得有帮助?
咨询建站