U2HTS触摸屏驱动编写实践
提示
本文由笔者编写,使用了DeepSeek与Claude进行润色。
U2HTS开源仓库:GitHub
如果想要了解基础的触摸屏逻辑,请查看上一篇文章。
本文将介绍如何在拥有数据手册或Linux内核驱动的情况下,编写一个U2HTS触摸屏控制器驱动。
准备代码框架
正如Linux内核模块有一定的框架一样,U2HTS的驱动程序也有其固定结构:
#include "u2hts_core.h"
static bool mycontroller_setup(U2HTS_BUS_TYPES bus_type);
static bool mycontroller_coord_fetch();
static void mycontroller_get_config(u2hts_touch_controller_config* cfg);
static u2hts_touch_controller_operations mycontroller_ops = {
.setup = &mycontroller_setup,
.fetch = &mycontroller_coord_fetch,
.get_config = &mycontroller_get_config};
static u2hts_touch_controller mycontroller = {
.name = "mycontroller", // controller name 控制器名称
.irq_type = IRQ_TYPE_EDGE_FALLING, // irq type 中断类型
.report_mode = UTC_REPORT_MODE_CONTINOUS, // report mode 回报模式
// I2C
.i2c_config =
{
.addr = 0xFF, // I2C slave addr I2C从机地址
.speed_hz = 400 * 1000, // I2C speed in Hz I2C速度,单位为Hz
},
.alt_i2c_addr = 0xFE, // Alternative I2C addr 替代I2C地址
// SPI
.spi_config =
{
.cpha = false,
.cpol = false,
.speed_hz = 1000 * 1000,
},
.operations = &mycontroller_ops};
U2HTS_TOUCH_CONTROLLER(mycontroller);看起来内容很多,但主要分为三大部分:
- 回调函数原型
- 控制器对象
- 注册控制器宏
回调原型
首先是第一部分:三个回调函数,无需赘述,在 u2hts_core 中的调用顺序为 setup -> get_config -> loop(coord_fetch)。
控制器对象
关于控制器对象,参数较多,逐一说明:
name:不必多说。
report_mode:控制器上报模式。有的控制器只在手指移动时发包,手指抬起时会自动将对应tp id 的 contact清零,此时应将上报模式设置为 UTC_REPORT_MODE_EVENT。另一种控制器在手指触屏期间会持续不断地发包,则将上报模式设置为 UTC_REPORT_MODE_CONTINOUS,或留空。
irq_type:中断类型,同样无需赘述。
bus_config(如 i2c_config、spi_config):总线配置,需根据控制器实际情况填写,例如只支持 I2C 就只填 I2C 相关配置。
alt_i2c_addr:若控制器有两个可配置的 I2C 地址,将次要地址填入此处即可。
operations:设为包含三个回调函数的结构体变量 mycontroller_ops 的地址。
注册控制器宏
最后使用 U2HTS_TOUCH_CONTROLLER(mycontroller) 宏将控制器注册到系统中。
以上是整体框架,重点在 setup 和 coord_fetch 这两个函数上。
有数据手册
有手册的情况就很简单,比如 GT9XX 系列,寄存器定义随处可见,直接对照手册编写即可,具体可参考 u2hts_touch_controllers/gt9xx.c。
无手册,但有 Linux 驱动
目前较新的触控 IC 大多如此——既无数据手册,也无寄存器文档,一问就说要签 NDA,普通开发者根本无从下手。以汇顶的 Normandy 系列 GT9886 为例,网上别说寄存器定义了,连引脚定义都找不到。但办法总比困难多——在 linux kernel archive 里,找到了开源的驱动程序。既然 Linux 内核已有现成的驱动,以它为蓝本,同样可以写出可用的触摸驱动。
下面贴上驱动源码:
/* SPDX-License-Identifier: GPL-2.0-only */
#ifndef __GOODIX_GTX8_H__
#define __GOODIX_GTX8_H__
#define GOODIX_GTX8_NORMAL_RESET_DELAY_MS 100
#define GOODIX_GTX8_POWER_ON_DELAY_MS 300
#define GOODIX_GTX8_TOUCH_EVENT BIT(7)
#define GOODIX_GTX8_REQUEST_EVENT BIT(6)
#define GOODIX_GTX8_TOUCH_COUNT_MASK GENMASK(3, 0)
#define GOODIX_GTX8_FINGER_ID_MASK_YELLOWSTONE GENMASK(7, 4)
#define GOODIX_GTX8_MAX_TOUCH 10
#define GOODIX_GTX8_CHECKSUM_SIZE sizeof(u16)
#define GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY 0x4535
#define GOODIX_GTX8_FW_VERSION_ADDR_YELLOWSTONE 0x4022
#define GOODIX_GTX8_TOUCH_DATA_ADDR_NORMANDY 0x4100
#define GOODIX_GTX8_TOUCH_DATA_ADDR_YELLOWSTONE 0x4180
#define I2C_MAX_TRANSFER_SIZE 256
enum goodix_gtx8_ic_type {
IC_TYPE_NORMANDY,
IC_TYPE_YELLOWSTONE,
};
struct goodix_gtx8_ic_data {
size_t event_size;
/*
* This is technically not the firmware version address
* referenced in the vendor driver, but rather the
* address of the product ID part. The meaning of the
* other parts is unknown and they are therefore omitted
* for now.
*/
int fw_version_addr;
size_t header_size;
enum goodix_gtx8_ic_type ic_type;
char product_id[4];
int touch_data_addr;
};
struct goodix_gtx8_header_normandy {
u8 status;
/* Only the lower 4 bits are actually used */
u8 touch_num;
};
#define GOODIX_GTX8_HEADER_SIZE_NORMANDY \
sizeof(struct goodix_gtx8_header_normandy)
struct goodix_gtx8_header_yellowstone {
u8 status;
/* Most likely unused */
u8 __unknown1;
/* Only the lower 4 bits are actually used */
u8 touch_num;
/* Most likely unused */
u8 __unknown2[3];
__le16 checksum;
} __packed __aligned(1);
#define GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE \
sizeof(struct goodix_gtx8_header_yellowstone)
struct goodix_gtx8_touch_normandy {
u8 finger_id;
__le16 x;
__le16 y;
u8 w;
u8 __unknown[2];
} __packed __aligned(1);
struct goodix_gtx8_touch_yellowstone {
/*
* Only the upper 4 bits are used, lower 4 bits are
* probably the sensor ID.
*/
u8 finger_id;
u8 __unknown1;
__be16 x;
__be16 y;
/*
* Vendor driver claims that this is a single __be16,
* but testing shows that it likely isn't.
*/
u8 __unknown2;
u8 w;
} __packed __aligned(1);
union goodix_gtx8_touch {
struct goodix_gtx8_touch_normandy normandy;
struct goodix_gtx8_touch_yellowstone yellowstone;
};
#define GOODIX_GTX8_TOUCH_SIZE sizeof(union goodix_gtx8_touch)
struct goodix_gtx8_event_normandy {
struct goodix_gtx8_header_normandy hdr;
/* The data below is u16 aligned */
u8 data[GOODIX_GTX8_TOUCH_SIZE * GOODIX_GTX8_MAX_TOUCH +
GOODIX_GTX8_CHECKSUM_SIZE];
};
#define GOODIX_GTX8_EVENT_SIZE_NORMANDY \
sizeof(struct goodix_gtx8_event_normandy)
struct goodix_gtx8_event_yellowstone {
struct goodix_gtx8_header_yellowstone hdr;
/* The data below is u16 aligned */
u8 data[GOODIX_GTX8_TOUCH_SIZE * GOODIX_GTX8_MAX_TOUCH +
GOODIX_GTX8_CHECKSUM_SIZE];
};
#define GOODIX_GTX8_EVENT_SIZE_YELLOWSTONE \
sizeof(struct goodix_gtx8_event_yellowstone)
struct goodix_gtx8_fw_version {
/* 4 digits IC number */
char product_id[4];
/* Most likely unused */
u8 __unknown[4];
/* Four components version number */
u8 fw_version[4];
};
struct goodix_gtx8_core {
struct device *dev;
struct regmap *regmap;
struct regulator *avdd;
struct regulator *vddio;
struct gpio_desc *reset_gpio;
struct touchscreen_properties props;
struct goodix_gtx8_fw_version fw_version;
struct input_dev *input_dev;
int irq;
const struct goodix_gtx8_ic_data *ic_data;
u8 *event_buffer;
};
#endif// SPDX-License-Identifier: GPL-2.0-only
/*
* Driver for Goodix GTX8 Touchscreens
*
* Copyright (c) 2019 - 2020 Goodix, Inc.
* Copyright (C) 2023 Linaro Ltd.
* Copyright (c) 2025 Jens Reidel <[email protected]>
*
* Based on gtx8_driver_linux vendor driver and goodix_berlin kernel driver.
*
* The driver currently relies on the pre-flashed firmware and only supports
* Normandy / Yellowstone ICs.
* Pen support is also missing.
*/
#include <linux/gpio/consumer.h>
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/input/mt.h>
#include <linux/input/touchscreen.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/unaligned.h>
#include "goodix_gtx8.h"
static const struct regmap_config goodix_gtx8_regmap_conf = {
.reg_bits = 16,
.val_bits = 8,
.max_raw_read = I2C_MAX_TRANSFER_SIZE,
.max_raw_write = I2C_MAX_TRANSFER_SIZE,
};
/* vendor & product left unassigned here, should probably be updated from fw info */
static const struct input_id goodix_gtx8_input_id = {
.bustype = BUS_I2C,
};
static bool goodix_gtx8_checksum_valid_normandy(const u8 *data, int size)
{
u8 cal_checksum = 0;
int i;
if (size < GOODIX_GTX8_CHECKSUM_SIZE)
return false;
for (i = 0; i < size; i++)
cal_checksum += data[i];
return cal_checksum == 0;
}
static bool goodix_gtx8_checksum_valid_yellowstone(const u8 *data, int size)
{
u16 cal_checksum = 0;
u16 r_checksum;
int i;
if (size < GOODIX_GTX8_CHECKSUM_SIZE)
return false;
for (i = 0; i < size - GOODIX_GTX8_CHECKSUM_SIZE; i++)
cal_checksum += data[i];
r_checksum = get_unaligned_be16(&data[i]);
return cal_checksum == r_checksum;
}
static int goodix_gtx8_get_remaining_contacts(struct goodix_gtx8_core *cd,
int n)
{
size_t offset = cd->ic_data->header_size + GOODIX_GTX8_TOUCH_SIZE +
GOODIX_GTX8_CHECKSUM_SIZE;
u32 addr = cd->ic_data->touch_data_addr + offset;
int error;
error = regmap_raw_read(cd->regmap, addr, &cd->event_buffer[offset],
(n - 1) * GOODIX_GTX8_TOUCH_SIZE);
if (error) {
dev_err_ratelimited(cd->dev, "failed to get touch data, %d\n",
error);
return error;
}
return 0;
}
static void goodix_gtx8_report_state(struct goodix_gtx8_core *cd, u8 touch_num,
union goodix_gtx8_touch *touch_data)
{
union goodix_gtx8_touch *t;
int i;
u8 finger_id;
for (i = 0; i < touch_num; i++) {
t = &touch_data[i];
if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
input_mt_slot(cd->input_dev, t->normandy.finger_id);
input_mt_report_slot_state(cd->input_dev,
MT_TOOL_FINGER, true);
touchscreen_report_pos(cd->input_dev, &cd->props,
__le16_to_cpu(t->normandy.x),
__le16_to_cpu(t->normandy.y),
true);
input_report_abs(cd->input_dev, ABS_MT_TOUCH_MAJOR,
t->normandy.w);
} else {
finger_id = FIELD_GET(
GOODIX_GTX8_FINGER_ID_MASK_YELLOWSTONE,
t->yellowstone.finger_id);
input_mt_slot(cd->input_dev, finger_id);
input_mt_report_slot_state(cd->input_dev,
MT_TOOL_FINGER, true);
touchscreen_report_pos(cd->input_dev, &cd->props,
__be16_to_cpu(t->yellowstone.x),
__be16_to_cpu(t->yellowstone.y),
true);
input_report_abs(cd->input_dev, ABS_MT_TOUCH_MAJOR,
t->yellowstone.w);
}
}
input_mt_sync_frame(cd->input_dev);
input_sync(cd->input_dev);
}
static void goodix_gtx8_touch_handler(struct goodix_gtx8_core *cd, u8 touch_num,
union goodix_gtx8_touch *touch_data)
{
int error;
touch_num = FIELD_GET(GOODIX_GTX8_TOUCH_COUNT_MASK, touch_num);
if (touch_num > GOODIX_GTX8_MAX_TOUCH) {
dev_warn(cd->dev, "invalid touch num %d\n", touch_num);
return;
}
if (touch_num > 1) {
/* read additional contact data if more than 1 touch event */
error = goodix_gtx8_get_remaining_contacts(cd, touch_num);
if (error)
return;
}
if (touch_num) {
/*
* Normandy checksum is for the entire read buffer,
* Yellowstone is only for the touch data (since header
* has a separate checksum)
*/
if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
int len = GOODIX_GTX8_HEADER_SIZE_NORMANDY +
touch_num * GOODIX_GTX8_TOUCH_SIZE +
GOODIX_GTX8_CHECKSUM_SIZE;
if (!goodix_gtx8_checksum_valid_normandy(
cd->event_buffer, len)) {
dev_err(cd->dev,
"touch data checksum error: %*ph\n",
len, cd->event_buffer);
return;
}
} else {
int len = touch_num * GOODIX_GTX8_TOUCH_SIZE +
GOODIX_GTX8_CHECKSUM_SIZE;
if (!goodix_gtx8_checksum_valid_yellowstone(
(u8 *)touch_data, len)) {
dev_err(cd->dev,
"touch data checksum error: %*ph\n",
len, (u8 *)touch_data);
return;
}
}
}
goodix_gtx8_report_state(cd, touch_num, touch_data);
}
static irqreturn_t goodix_gtx8_irq(int irq, void *data)
{
struct goodix_gtx8_core *cd = data;
struct goodix_gtx8_event_normandy *ev_normandy;
struct goodix_gtx8_event_yellowstone *ev_yellowstone;
union goodix_gtx8_touch *touch_data;
int error;
u8 status, touch_num;
error = regmap_raw_read(
cd->regmap, cd->ic_data->touch_data_addr, cd->event_buffer,
cd->ic_data->header_size + GOODIX_GTX8_TOUCH_SIZE +
GOODIX_GTX8_CHECKSUM_SIZE);
if (error) {
dev_warn_ratelimited(
cd->dev, "failed to get event head data: %d\n", error);
goto out;
}
/*
* Both IC types have the same data in the header, just at different
* offsets
*/
if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
ev_normandy =
(struct goodix_gtx8_event_normandy *)cd->event_buffer;
status = ev_normandy->hdr.status;
touch_num = ev_normandy->hdr.touch_num;
touch_data = (union goodix_gtx8_touch *)ev_normandy->data;
} else {
ev_yellowstone = (struct goodix_gtx8_event_yellowstone *)
cd->event_buffer;
status = ev_yellowstone->hdr.status;
touch_num = ev_yellowstone->hdr.touch_num;
touch_data = (union goodix_gtx8_touch *)ev_yellowstone->data;
}
if (status == 0)
goto out;
/* Yellowstone ICs have a checksum for the header */
if (cd->ic_data->ic_type == IC_TYPE_YELLOWSTONE &&
!goodix_gtx8_checksum_valid_yellowstone(
cd->event_buffer, GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE)) {
dev_warn_ratelimited(cd->dev,
"touch head checksum error: %*ph\n",
(int)GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE,
cd->event_buffer);
goto out_clear;
}
if (status & GOODIX_GTX8_TOUCH_EVENT)
goodix_gtx8_touch_handler(cd, touch_num, touch_data);
if (status & GOODIX_GTX8_REQUEST_EVENT) {
/*
* All configs seen so far either set the firmware request
* address to 0 (Normandy) or have it equal the touch data
* address (Yellowstone). Neither seems correct, and this
* is not testable. Therefore it is currently omitted.
*/
dev_dbg(cd->dev, "received request event, ignoring\n");
}
out_clear:
/* Clear up status field */
regmap_write(cd->regmap, cd->ic_data->touch_data_addr, 0);
out:
return IRQ_HANDLED;
}
static int goodix_gtx8_input_dev_config(struct goodix_gtx8_core *cd)
{
struct input_dev *input_dev;
int error;
input_dev = devm_input_allocate_device(cd->dev);
if (!input_dev)
return -ENOMEM;
cd->input_dev = input_dev;
input_set_drvdata(input_dev, cd);
input_dev->name = "Goodix GTX8 Capacitive TouchScreen";
input_dev->phys = "input/ts";
input_dev->id = goodix_gtx8_input_id;
input_set_abs_params(cd->input_dev, ABS_MT_POSITION_X, 0, SZ_64K - 1, 0,
0);
input_set_abs_params(cd->input_dev, ABS_MT_POSITION_Y, 0, SZ_64K - 1, 0,
0);
input_set_abs_params(cd->input_dev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
touchscreen_parse_properties(cd->input_dev, true, &cd->props);
error = input_mt_init_slots(cd->input_dev, GOODIX_GTX8_MAX_TOUCH,
INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
if (error)
return error;
error = input_register_device(cd->input_dev);
if (error)
return error;
return 0;
}
static int goodix_gtx8_read_version(struct goodix_gtx8_core *cd)
{
int error;
/*
* The vendor driver reads a whole lot more data to calculate and
* verify a checksum. Without documentation, we don't know what
* most of that data is, so we only read the parts we know about
* and instead ensure their values are as expected
*/
error = regmap_raw_read(cd->regmap, cd->ic_data->fw_version_addr,
&cd->fw_version, sizeof(cd->fw_version));
if (error) {
dev_err(cd->dev, "error reading fw version, %d\n", error);
return error;
}
/*
* Since we don't verify the checksum, do a basic check that the
* product ID meets expectations
*/
if (memcmp(cd->fw_version.product_id, cd->ic_data->product_id,
sizeof(cd->fw_version.product_id))) {
dev_err(cd->dev, "unexpected product ID, got: %c%c%c%c\n",
cd->fw_version.product_id[0],
cd->fw_version.product_id[1],
cd->fw_version.product_id[2],
cd->fw_version.product_id[3]);
return -EINVAL;
}
return 0;
}
static int goodix_gtx8_dev_confirm(struct goodix_gtx8_core *cd)
{
u8 rx_buf[1];
int retry = 3;
int error;
while (retry--) {
/*
* test_addr appears to always be the touch_data_addr for
* Normandy, but it doesn't really matter since all we
* need is a valid address
*/
error = regmap_raw_read(cd->regmap,
cd->ic_data->touch_data_addr, rx_buf,
sizeof(rx_buf));
if (!error)
return 0;
usleep_range(5000, 5100);
}
dev_err(cd->dev, "device confirm failed\n");
return -EINVAL;
}
static int goodix_gtx8_power_on(struct goodix_gtx8_core *cd)
{
int error;
error = regulator_enable(cd->vddio);
if (error) {
dev_err(cd->dev, "Failed to enable VDDIO: %d\n", error);
return error;
}
error = regulator_enable(cd->avdd);
if (error) {
dev_err(cd->dev, "Failed to enable AVDD: %d\n", error);
goto err_vddio_disable;
}
/* Vendors usually configure the power on delay as 300ms */
msleep(GOODIX_GTX8_POWER_ON_DELAY_MS);
gpiod_set_value_cansleep(cd->reset_gpio, 0);
/* Vendor waits 5ms for firmware to initialize */
usleep_range(5000, 5100);
error = goodix_gtx8_dev_confirm(cd);
if (error)
goto err_dev_reset;
/* Vendor waits 100ms for firmware to fully boot */
msleep(GOODIX_GTX8_NORMAL_RESET_DELAY_MS);
return 0;
err_dev_reset:
gpiod_set_value_cansleep(cd->reset_gpio, 1);
regulator_disable(cd->avdd);
err_vddio_disable:
regulator_disable(cd->vddio);
return error;
}
static void goodix_gtx8_power_off(struct goodix_gtx8_core *cd)
{
gpiod_set_value_cansleep(cd->reset_gpio, 1);
regulator_disable(cd->avdd);
regulator_disable(cd->vddio);
}
static int goodix_gtx8_suspend(struct device *dev)
{
struct goodix_gtx8_core *cd = dev_get_drvdata(dev);
disable_irq(cd->irq);
goodix_gtx8_power_off(cd);
return 0;
}
static int goodix_gtx8_resume(struct device *dev)
{
struct goodix_gtx8_core *cd = dev_get_drvdata(dev);
int error;
error = goodix_gtx8_power_on(cd);
if (error)
return error;
enable_irq(cd->irq);
return 0;
}
EXPORT_GPL_SIMPLE_DEV_PM_OPS(goodix_gtx8_pm_ops, goodix_gtx8_suspend,
goodix_gtx8_resume);
static void goodix_gtx8_power_off_act(void *data)
{
struct goodix_gtx8_core *cd = data;
goodix_gtx8_power_off(cd);
}
static int goodix_gtx8_probe(struct i2c_client *client)
{
struct goodix_gtx8_core *cd;
struct regmap *regmap;
int error;
cd = devm_kzalloc(&client->dev, sizeof(*cd), GFP_KERNEL);
if (!cd)
return -ENOMEM;
regmap = devm_regmap_init_i2c(client, &goodix_gtx8_regmap_conf);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
cd->dev = &client->dev;
cd->irq = client->irq;
cd->regmap = regmap;
cd->ic_data = i2c_get_match_data(client);
cd->event_buffer =
devm_kzalloc(cd->dev, cd->ic_data->event_size, GFP_KERNEL);
if (!cd->event_buffer)
return -ENOMEM;
cd->reset_gpio =
devm_gpiod_get_optional(cd->dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(cd->reset_gpio))
return dev_err_probe(cd->dev, PTR_ERR(cd->reset_gpio),
"Failed to request reset GPIO\n");
cd->avdd = devm_regulator_get(cd->dev, "avdd");
if (IS_ERR(cd->avdd))
return dev_err_probe(cd->dev, PTR_ERR(cd->avdd),
"Failed to request AVDD regulator\n");
cd->vddio = devm_regulator_get(cd->dev, "vddio");
if (IS_ERR(cd->vddio))
return dev_err_probe(cd->dev, PTR_ERR(cd->vddio),
"Failed to request VDDIO regulator\n");
error = goodix_gtx8_power_on(cd);
if (error) {
dev_err(cd->dev, "failed power on");
return error;
}
error = devm_add_action_or_reset(cd->dev, goodix_gtx8_power_off_act,
cd);
if (error)
return error;
error = goodix_gtx8_read_version(cd);
if (error) {
dev_err(cd->dev, "failed to get version info");
return error;
}
error = goodix_gtx8_input_dev_config(cd);
if (error) {
dev_err(cd->dev, "failed to set input device");
return error;
}
error = devm_request_threaded_irq(cd->dev, cd->irq, NULL,
goodix_gtx8_irq, IRQF_ONESHOT,
"goodix-gtx8", cd);
if (error) {
dev_err(cd->dev, "request threaded IRQ failed: %d\n", error);
return error;
}
dev_set_drvdata(cd->dev, cd);
dev_dbg(cd->dev,
"Goodix GT%c%c%c%c Touchscreen Controller, Version %d.%d.%d.%d\n",
cd->fw_version.product_id[0], cd->fw_version.product_id[1],
cd->fw_version.product_id[2], cd->fw_version.product_id[3],
cd->fw_version.fw_version[0], cd->fw_version.fw_version[1],
cd->fw_version.fw_version[2], cd->fw_version.fw_version[3]);
return 0;
}
static const struct goodix_gtx8_ic_data gt9886_data = {
.event_size = GOODIX_GTX8_EVENT_SIZE_NORMANDY,
.fw_version_addr = GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY,
.header_size = GOODIX_GTX8_HEADER_SIZE_NORMANDY,
.ic_type = IC_TYPE_NORMANDY,
.product_id = { '9', '8', '8', '6' },
.touch_data_addr = GOODIX_GTX8_TOUCH_DATA_ADDR_NORMANDY,
};
static const struct goodix_gtx8_ic_data gt9896_data = {
.event_size = GOODIX_GTX8_EVENT_SIZE_YELLOWSTONE,
.fw_version_addr = GOODIX_GTX8_FW_VERSION_ADDR_YELLOWSTONE,
.header_size = GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE,
.ic_type = IC_TYPE_YELLOWSTONE,
.product_id = { '9', '8', '9', '6' },
.touch_data_addr = GOODIX_GTX8_TOUCH_DATA_ADDR_YELLOWSTONE,
};
static const struct i2c_device_id goodix_gtx8_i2c_id[] = {
{ .name = "gt9886", .driver_data = (long)>9886_data },
{ .name = "gt9896", .driver_data = (long)>9896_data },
{},
};
MODULE_DEVICE_TABLE(i2c, goodix_gtx8_i2c_id);
static const struct of_device_id goodix_gtx8_of_match[] = {
{ .compatible = "goodix,gt9886", .data = >9886_data },
{ .compatible = "goodix,gt9896", .data = >9896_data },
{},
};
MODULE_DEVICE_TABLE(of, goodix_gtx8_of_match);
static struct i2c_driver goodix_gtx8_driver = {
.probe = goodix_gtx8_probe,
.id_table = goodix_gtx8_i2c_id,
.driver = {
.name = "goodix-gtx8",
.of_match_table = of_match_ptr(goodix_gtx8_of_match),
.pm = pm_sleep_ptr(&goodix_gtx8_pm_ops),
},
};
module_i2c_driver(goodix_gtx8_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Goodix GTX8 Touchscreen driver");
MODULE_AUTHOR("Jens Reidel <[email protected]>");编写
上面这份驱动看起来篇幅不小,初次接触 Linux 内核驱动的读者可能会觉得有些复杂。但所有复杂的东西,都是由简单的东西堆叠而成的,下面逐步分析。
Linux input 驱动的整体结构网上已有大量文章介绍,这里不再赘述,直接切入重点:驱动入口通常是 probe 函数,中断处理程序在 probe 中通过 request_irq 注册,中断触发后再调用对应的数据读取与处理函数完成坐标上报。
probe 函数分析
我们先看 probe 函数:
static int goodix_gtx8_probe(struct i2c_client *client)
{
struct goodix_gtx8_core *cd;
struct regmap *regmap;
int error;
cd = devm_kzalloc(&client->dev, sizeof(*cd), GFP_KERNEL);
if (!cd)
return -ENOMEM;
regmap = devm_regmap_init_i2c(client, &goodix_gtx8_regmap_conf);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
cd->dev = &client->dev;
cd->irq = client->irq;
cd->regmap = regmap;
cd->ic_data = i2c_get_match_data(client);
cd->event_buffer =
devm_kzalloc(cd->dev, cd->ic_data->event_size, GFP_KERNEL);
if (!cd->event_buffer)
return -ENOMEM;
cd->reset_gpio =
devm_gpiod_get_optional(cd->dev, "reset", GPIOD_OUT_HIGH);
if (IS_ERR(cd->reset_gpio))
return dev_err_probe(cd->dev, PTR_ERR(cd->reset_gpio),
"Failed to request reset GPIO\n");
cd->avdd = devm_regulator_get(cd->dev, "avdd");
if (IS_ERR(cd->avdd))
return dev_err_probe(cd->dev, PTR_ERR(cd->avdd),
"Failed to request AVDD regulator\n");
cd->vddio = devm_regulator_get(cd->dev, "vddio");
if (IS_ERR(cd->vddio))
return dev_err_probe(cd->dev, PTR_ERR(cd->vddio),
"Failed to request VDDIO regulator\n");
error = goodix_gtx8_power_on(cd);
if (error) {
dev_err(cd->dev, "failed power on");
return error;
}
error = devm_add_action_or_reset(cd->dev, goodix_gtx8_power_off_act,
cd);
if (error)
return error;
error = goodix_gtx8_read_version(cd);
if (error) {
dev_err(cd->dev, "failed to get version info");
return error;
}
error = goodix_gtx8_input_dev_config(cd);
if (error) {
dev_err(cd->dev, "failed to set input device");
return error;
}
error = devm_request_threaded_irq(cd->dev, cd->irq, NULL,
goodix_gtx8_irq, IRQF_ONESHOT,
"goodix-gtx8", cd);
if (error) {
dev_err(cd->dev, "request threaded IRQ failed: %d\n", error);
return error;
}
dev_set_drvdata(cd->dev, cd);
dev_dbg(cd->dev,
"Goodix GT%c%c%c%c Touchscreen Controller, Version %d.%d.%d.%d\n",
cd->fw_version.product_id[0], cd->fw_version.product_id[1],
cd->fw_version.product_id[2], cd->fw_version.product_id[3],
cd->fw_version.fw_version[0], cd->fw_version.fw_version[1],
cd->fw_version.fw_version[2], cd->fw_version.fw_version[3]);
return 0;
}前面几行是设置 regmap 参数以及配置 GPIO 状态等,并非关键。
上电流程分析
往下看,发现调用了 goodix_gtx8_power_on,我们来看看它的定义:
static int goodix_gtx8_power_on(struct goodix_gtx8_core *cd)
{
int error;
error = regulator_enable(cd->vddio);
if (error) {
dev_err(cd->dev, "Failed to enable VDDIO: %d\n", error);
return error;
}
error = regulator_enable(cd->avdd);
if (error) {
dev_err(cd->dev, "Failed to enable AVDD: %d\n", error);
goto err_vddio_disable;
}
/* Vendors usually configure the power on delay as 300ms */
msleep(GOODIX_GTX8_POWER_ON_DELAY_MS);
gpiod_set_value_cansleep(cd->reset_gpio, 0);
/* Vendor waits 5ms for firmware to initialize */
usleep_range(5000, 5100);
error = goodix_gtx8_dev_confirm(cd);
if (error)
goto err_dev_reset;
/* Vendor waits 100ms for firmware to fully boot */
msleep(GOODIX_GTX8_NORMAL_RESET_DELAY_MS);
return 0;
err_dev_reset:
gpiod_set_value_cansleep(cd->reset_gpio, 1);
regulator_disable(cd->avdd);
err_vddio_disable:
regulator_disable(cd->vddio);
return error;
}梳理流程:先上电,延时 300ms,然后将 RST 拉低,延时 5ms,随后调用 goodix_gtx8_dev_confirm:
static int goodix_gtx8_dev_confirm(struct goodix_gtx8_core *cd)
{
u8 rx_buf[1];
int retry = 3;
int error;
while (retry--) {
/*
* test_addr appears to always be the touch_data_addr for
* Normandy, but it doesn't really matter since all we
* need is a valid address
*/
error = regmap_raw_read(cd->regmap,
cd->ic_data->touch_data_addr, rx_buf,
sizeof(rx_buf));
if (!error)
return 0;
usleep_range(5000, 5100);
}
dev_err(cd->dev, "device confirm failed\n");
return -EINVAL;
}读下来,这个函数只是读了一个寄存器地址,且对返回值并不做处理,只要读取不报错就视为成功。这步可以忽略。
读取版本信息
继续往下看 goodix_gtx8_read_version(cd):
static int goodix_gtx8_read_version(struct goodix_gtx8_core *cd)
{
int error;
/*
* The vendor driver reads a whole lot more data to calculate and
* verify a checksum. Without documentation, we don't know what
* most of that data is, so we only read the parts we know about
* and instead ensure their values are as expected
*/
error = regmap_raw_read(cd->regmap, cd->ic_data->fw_version_addr,
&cd->fw_version, sizeof(cd->fw_version));
if (error) {
dev_err(cd->dev, "error reading fw version, %d\n", error);
return error;
}
/*
* Since we don't verify the checksum, do a basic check that the
* product ID meets expectations
*/
if (memcmp(cd->fw_version.product_id, cd->ic_data->product_id,
sizeof(cd->fw_version.product_id))) {
dev_err(cd->dev, "unexpected product ID, got: %c%c%c%c\n",
cd->fw_version.product_id[0],
cd->fw_version.product_id[1],
cd->fw_version.product_id[2],
cd->fw_version.product_id[3]);
return -EINVAL;
}
return 0;
}有意思的来了:很多触控 IC 都有对应的读取 ID 寄存器。全局搜索一下 fw_version_addr 的定义:
static const struct goodix_gtx8_ic_data gt9886_data = {
.event_size = GOODIX_GTX8_EVENT_SIZE_NORMANDY,
.fw_version_addr = GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY,
.header_size = GOODIX_GTX8_HEADER_SIZE_NORMANDY,
.ic_type = IC_TYPE_NORMANDY,
.product_id = { '9', '8', '8', '6' },
.touch_data_addr = GOODIX_GTX8_TOUCH_DATA_ADDR_NORMANDY,
};再查看 GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY:
#define GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY 0x4535哈哈,产品信息寄存器有了。
再看 fw_version 结构体的定义:
struct goodix_gtx8_fw_version {
/* 4 digits IC number */
char product_id[4];
/* Most likely unused */
u8 __unknown[4];
/* Four components version number */
u8 fw_version[4];
};共 12 个字节,前 4 个字节为 IC 型号。有了这些,便可以写一段简单的测试代码来验证 ID 读取是否正常:
// this is from linux/drivers/input/touchscreen/goodix_gtx8.h
typedef struct __packed {
/* 4 digits IC number */
uint8_t product_id[4];
/* Most likely unused */
uint32_t __unknown;
/* Four components version number */
uint32_t fw_version;
} goodix_normandy_firmware_version;
static bool goodix_normandy_setup(U2HTS_BUS_TYPES bus_type) {
U2HTS_UNUSED(bus_type);
u2hts_tpint_set(false);
u2hts_tprst_set(false);
u2hts_delay_ms(100);
u2hts_tprst_set(true);
u2hts_delay_ms(200);
u2hts_tpint_set(true);
bool ret = u2hts_i2c_detect_slave(GOODIX_NORMANDY_I2C_ADDR);
if (!ret) return ret;
goodix_normandy_firmware_version fwver = {0};
goodix_normandy_read_reg(GOODIX_NORMANDY_VERSION_ADDR_NORMANDY, &fwver,
sizeof(fwver));
U2HTS_LOG_INFO("goodix_normandy product id = %s, fwver = %d",
fwver.product_id, fwver.fw_version);
return false;
}接上屏幕,查看串口输出:
goodix_normandy product id = 9886, fwver = 50241623ID 读取正常,说明已经成功了一半。接下来只需把坐标获取的逻辑梳理清楚即可。
中断处理函数分析
继续看 probe:
error = devm_request_threaded_irq(cd->dev, cd->irq, NULL,
goodix_gtx8_irq, IRQF_ONESHOT,
"goodix-gtx8", cd);中断处理函数也找到了。通读 probe 剩余部分,没有额外的寄存器配置操作,直接跳到 goodix_gtx8_irq:
static irqreturn_t goodix_gtx8_irq(int irq, void *data)
{
struct goodix_gtx8_core *cd = data;
struct goodix_gtx8_event_normandy *ev_normandy;
struct goodix_gtx8_event_yellowstone *ev_yellowstone;
union goodix_gtx8_touch *touch_data;
int error;
u8 status, touch_num;
error = regmap_raw_read(
cd->regmap, cd->ic_data->touch_data_addr, cd->event_buffer,
cd->ic_data->header_size + GOODIX_GTX8_TOUCH_SIZE +
GOODIX_GTX8_CHECKSUM_SIZE);
if (error) {
dev_warn_ratelimited(
cd->dev, "failed to get event head data: %d\n", error);
goto out;
}
/*
* Both IC types have the same data in the header, just at different
* offsets
*/
if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
ev_normandy =
(struct goodix_gtx8_event_normandy *)cd->event_buffer;
status = ev_normandy->hdr.status;
touch_num = ev_normandy->hdr.touch_num;
touch_data = (union goodix_gtx8_touch *)ev_normandy->data;
} else {
ev_yellowstone = (struct goodix_gtx8_event_yellowstone *)
cd->event_buffer;
status = ev_yellowstone->hdr.status;
touch_num = ev_yellowstone->hdr.touch_num;
touch_data = (union goodix_gtx8_touch *)ev_yellowstone->data;
}
if (status == 0)
goto out;
/* Yellowstone ICs have a checksum for the header */
if (cd->ic_data->ic_type == IC_TYPE_YELLOWSTONE &&
!goodix_gtx8_checksum_valid_yellowstone(
cd->event_buffer, GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE)) {
dev_warn_ratelimited(cd->dev,
"touch head checksum error: %*ph\n",
(int)GOODIX_GTX8_HEADER_SIZE_YELLOWSTONE,
cd->event_buffer);
goto out_clear;
}
if (status & GOODIX_GTX8_TOUCH_EVENT)
goodix_gtx8_touch_handler(cd, touch_num, touch_data);
if (status & GOODIX_GTX8_REQUEST_EVENT) {
/*
* All configs seen so far either set the firmware request
* address to 0 (Normandy) or have it equal the touch data
* address (Yellowstone). Neither seems correct, and this
* is not testable. Therefore it is currently omitted.
*/
dev_dbg(cd->dev, "received request event, ignoring\n");
}
out_clear:
/* Clear up status field */
regmap_write(cd->regmap, cd->ic_data->touch_data_addr, 0);
out:
return IRQ_HANDLED;
}前几行是变量声明,跳过。看到 regmap_raw_read,搜索 touch_data_addr 的赋值位置,在 gt9886_data 中找到,查看定义:
#define GOODIX_GTX8_TOUCH_DATA_ADDR_NORMANDY 0x4100再看读取的数据量:GOODIX_GTX8_HEADER_SIZE_NORMANDY + GOODIX_GTX8_TOUCH_SIZE + GOODIX_GTX8_CHECKSUM_SIZE,将相关结构体定义整理如下:
struct goodix_gtx8_header_normandy {
u8 status;
/* Only the lower 4 bits are actually used */
u8 touch_num;
};
struct goodix_gtx8_touch_normandy {
u8 finger_id;
__le16 x;
__le16 y;
u8 w;
u8 __unknown[2];
} __packed __aligned(1);这就是触摸点的数据结构。计算总大小为 2 + 8 + 2 = 12 字节,继续往下:
if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
ev_normandy =
(struct goodix_gtx8_event_normandy *)cd->event_buffer;
status = ev_normandy->hdr.status;
touch_num = ev_normandy->hdr.touch_num;
touch_data = (union goodix_gtx8_touch *)ev_normandy->data;
}触摸事件状态和触摸点数量都有了,继续看 goodix_gtx8_touch_handler:
if (status & GOODIX_GTX8_TOUCH_EVENT)
goodix_gtx8_touch_handler(cd, touch_num, touch_data);static void goodix_gtx8_touch_handler(struct goodix_gtx8_core *cd, u8 touch_num,
union goodix_gtx8_touch *touch_data)
{
int error;
touch_num = FIELD_GET(GOODIX_GTX8_TOUCH_COUNT_MASK, touch_num);
if (touch_num > GOODIX_GTX8_MAX_TOUCH) {
dev_warn(cd->dev, "invalid touch num %d\n", touch_num);
return;
}
if (touch_num > 1) {
/* read additional contact data if more than 1 touch event */
error = goodix_gtx8_get_remaining_contacts(cd, touch_num);
if (error)
return;
}
if (touch_num) {
/*
* Normandy checksum is for the entire read buffer,
* Yellowstone is only for the touch data (since header
* has a separate checksum)
*/
if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
int len = GOODIX_GTX8_HEADER_SIZE_NORMANDY +
touch_num * GOODIX_GTX8_TOUCH_SIZE +
GOODIX_GTX8_CHECKSUM_SIZE;
if (!goodix_gtx8_checksum_valid_normandy(
cd->event_buffer, len)) {
dev_err(cd->dev,
"touch data checksum error: %*ph\n",
len, cd->event_buffer);
return;
}
} else {
int len = touch_num * GOODIX_GTX8_TOUCH_SIZE +
GOODIX_GTX8_CHECKSUM_SIZE;
if (!goodix_gtx8_checksum_valid_yellowstone(
(u8 *)touch_data, len)) {
dev_err(cd->dev,
"touch data checksum error: %*ph\n",
len, (u8 *)touch_data);
return;
}
}
}
goodix_gtx8_report_state(cd, touch_num, touch_data);
}首先对 touch_num 做了位掩码运算,去除标志位,得到纯粹的触摸点数量。若触摸点数量大于 1,则继续读取剩余触摸数据——goodix_gtx8_get_remaining_contacts 函数按偏移量将数据读入 cd->event_buffer,这里不再进行分析。 接着是校验和计算,这部分可以忽略。
触摸数据解析
继续往下看坐标上报部分goodix_gtx8_report_state:
static void goodix_gtx8_report_state(struct goodix_gtx8_core *cd, u8 touch_num,
union goodix_gtx8_touch *touch_data)
{
union goodix_gtx8_touch *t;
int i;
u8 finger_id;
for (i = 0; i < touch_num; i++) {
t = &touch_data[i];
if (cd->ic_data->ic_type == IC_TYPE_NORMANDY) {
input_mt_slot(cd->input_dev, t->normandy.finger_id);
input_mt_report_slot_state(cd->input_dev,
MT_TOOL_FINGER, true);
touchscreen_report_pos(cd->input_dev, &cd->props,
__le16_to_cpu(t->normandy.x),
__le16_to_cpu(t->normandy.y),
true);
input_report_abs(cd->input_dev, ABS_MT_TOUCH_MAJOR,
t->normandy.w);
} else {
finger_id = FIELD_GET(
GOODIX_GTX8_FINGER_ID_MASK_YELLOWSTONE,
t->yellowstone.finger_id);
input_mt_slot(cd->input_dev, finger_id);
input_mt_report_slot_state(cd->input_dev,
MT_TOOL_FINGER, true);
touchscreen_report_pos(cd->input_dev, &cd->props,
__be16_to_cpu(t->yellowstone.x),
__be16_to_cpu(t->yellowstone.y),
true);
input_report_abs(cd->input_dev, ABS_MT_TOUCH_MAJOR,
t->yellowstone.w);
}
}
input_mt_sync_frame(cd->input_dev);
input_sync(cd->input_dev);
}这里开始上报各触摸点的坐标。综合以上信息,可以推断出触摸数据包的结构:
- 总数据大小:2 + 8 × 触摸点数量 + 2(校验和)
- 第一个字节:状态
- 第二个字节:触摸点数量
- 每个触摸点的结构:
- 第一个字节:id
- 第二、三字节:x
- 第四、五字节:y
- 第六字节:w
- 第七、八字节:未知
再看 goodix_gtx8_irq 末尾:
out_clear:
/* Clear up status field */
regmap_write(cd->regmap, cd->ic_data->touch_data_addr, 0);向 touch_data_addr 写入 0 即可清除中断。
编写 fetch 函数
至此,所需信息已全部就绪,可以编写fetch函数了:
typedef struct {
uint8_t status;
uint8_t tp_count;
} goodix_normandy_touch_header;
typedef struct __packed {
uint8_t id;
uint16_t x;
uint16_t y;
uint8_t w;
uint8_t unknown_0;
uint8_t unknown_1;
} goodix_normandy_touch_data;
static bool goodix_normandy_coord_fetch() {
uint8_t buf[sizeof(goodix_normandy_touch_header) +
sizeof(goodix_normandy_touch_data) * 10 + 2 /*crc*/];
goodix_normandy_read_reg(GOODIX_NORMANDY_TOUCH_DATA_ADDR, buf, sizeof(buf));
goodix_normandy_touch_header* hdr = (goodix_normandy_touch_header*)buf;
U2HTS_LOG_DEBUG("status = %d, tp_count = %d", hdr->status, hdr->tp_count);
goodix_normandy_clear_irq();
if (hdr->status == 0 || hdr->tp_count == 0 || hdr->tp_count == 0x80)
return false;
U2HTS_SET_TP_COUNT_SAFE(hdr->tp_count);
for (uint8_t i = 0; i < hdr->tp_count; i++) {
goodix_normandy_touch_data* tp =
(goodix_normandy_touch_data*)(buf +
sizeof(goodix_normandy_touch_header) +
i * sizeof(goodix_normandy_touch_data));
u2hts_set_tp(i, true, tp->id, tp->x, tp->y, tp->w, 0, 0);
}
return true;
}编译烧录后,触摸屏幕,查看桌面,触摸轨迹正常显示,驱动成功。