Skip to content

U2HTS触摸屏驱动编写实践

提示

本文由笔者编写,使用了DeepSeekClaude进行润色。
U2HTS开源仓库:GitHub
如果想要了解基础的触摸屏逻辑,请查看上一篇文章。

本文将介绍如何在拥有数据手册或Linux内核驱动的情况下,编写一个U2HTS触摸屏控制器驱动。

准备代码框架

正如Linux内核模块有一定的框架一样,U2HTS的驱动程序也有其固定结构:

c
#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 idcontact清零,此时应将上报模式设置为 UTC_REPORT_MODE_EVENT。另一种控制器在手指触屏期间会持续不断地发包,则将上报模式设置为 UTC_REPORT_MODE_CONTINOUS,或留空。

irq_type:中断类型,同样无需赘述。

bus_config(如 i2c_configspi_config):总线配置,需根据控制器实际情况填写,例如只支持 I2C 就只填 I2C 相关配置。

alt_i2c_addr:若控制器有两个可配置的 I2C 地址,将次要地址填入此处即可。

operations:设为包含三个回调函数的结构体变量 mycontroller_ops 的地址。

注册控制器宏

最后使用 U2HTS_TOUCH_CONTROLLER(mycontroller) 宏将控制器注册到系统中。

以上是整体框架,重点在 setupcoord_fetch 这两个函数上。

有数据手册

有手册的情况就很简单,比如 GT9XX 系列,寄存器定义随处可见,直接对照手册编写即可,具体可参考 u2hts_touch_controllers/gt9xx.c

无手册,但有 Linux 驱动

目前较新的触控 IC 大多如此——既无数据手册,也无寄存器文档,一问就说要签 NDA,普通开发者根本无从下手。以汇顶的 Normandy 系列 GT9886 为例,网上别说寄存器定义了,连引脚定义都找不到。但办法总比困难多——在 linux kernel archive 里,找到了开源的驱动程序。既然 Linux 内核已有现成的驱动,以它为蓝本,同样可以写出可用的触摸驱动。

下面贴上驱动源码:

c
/* 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
c
// 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)&gt9886_data },
	{ .name = "gt9896", .driver_data = (long)&gt9896_data },
	{},
};
MODULE_DEVICE_TABLE(i2c, goodix_gtx8_i2c_id);

static const struct of_device_id goodix_gtx8_of_match[] = {
	{ .compatible = "goodix,gt9886", .data = &gt9886_data },
	{ .compatible = "goodix,gt9896", .data = &gt9896_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 函数:

c
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,我们来看看它的定义:

c
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

c
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)

c
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 的定义:

c
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

c
#define GOODIX_GTX8_FW_VERSION_ADDR_NORMANDY	0x4535

哈哈,产品信息寄存器有了。

再看 fw_version 结构体的定义:

c
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 读取是否正常:

c
// 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;
}

接上屏幕,查看串口输出:

txt
goodix_normandy product id = 9886, fwver = 50241623

ID 读取正常,说明已经成功了一半。接下来只需把坐标获取的逻辑梳理清楚即可。

中断处理函数分析

继续看 probe

c
	error = devm_request_threaded_irq(cd->dev, cd->irq, NULL,
					  goodix_gtx8_irq, IRQF_ONESHOT,
					  "goodix-gtx8", cd);

中断处理函数也找到了。通读 probe 剩余部分,没有额外的寄存器配置操作,直接跳到 goodix_gtx8_irq

c
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 中找到,查看定义:

c
#define GOODIX_GTX8_TOUCH_DATA_ADDR_NORMANDY	0x4100

再看读取的数据量:GOODIX_GTX8_HEADER_SIZE_NORMANDY + GOODIX_GTX8_TOUCH_SIZE + GOODIX_GTX8_CHECKSUM_SIZE,将相关结构体定义整理如下:

c
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 字节,继续往下:

c
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

c
if (status & GOODIX_GTX8_TOUCH_EVENT)
		goodix_gtx8_touch_handler(cd, touch_num, touch_data);
c
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

c
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 末尾:

c
out_clear:
	/* Clear up status field */
	regmap_write(cd->regmap, cd->ic_data->touch_data_addr, 0);

touch_data_addr 写入 0 即可清除中断。

编写 fetch 函数

至此,所需信息已全部就绪,可以编写fetch函数了:

c
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;
}

编译烧录后,触摸屏幕,查看桌面,触摸轨迹正常显示,驱动成功。