mirror of
https://github.com/torvalds/linux.git
synced 2026-07-27 01:32:21 +02:00
Add driver for Wacom W9002 and two Wacom W9007A variants. These are penabled touchscreens supporting passive Wacom Pens and use I2C. Co-developed-by: Ferass El Hafidi <funderscore@postmarketos.org> Signed-off-by: Ferass El Hafidi <funderscore@postmarketos.org> Signed-off-by: Hendrik Noack <hendrik-noack@gmx.de> Link: https://patch.msgid.link/20260528074818.12151-3-hendrik-noack@gmx.de Signed-off-by: Dmitry Torokhov <dmitry.torokhov@gmail.com>
445 lines
12 KiB
C
445 lines
12 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* Wacom W9000-series penabled I2C touchscreen driver
|
|
*
|
|
* Copyright (c) 2026 Hendrik Noack <hendrik-noack@gmx.de>
|
|
*
|
|
* Partially based on vendor driver:
|
|
* Copyright (C) 2012, Samsung Electronics Co. Ltd.
|
|
*/
|
|
|
|
#include <linux/delay.h>
|
|
#include <linux/gpio/consumer.h>
|
|
#include <linux/i2c.h>
|
|
#include <linux/input.h>
|
|
#include <linux/input/touchscreen.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/lockdep.h>
|
|
#include <linux/regulator/consumer.h>
|
|
#include <linux/unaligned.h>
|
|
|
|
/* Some chips have flaky firmware that requires many retries before responding. */
|
|
#define CMD_QUERY_RETRIES 8
|
|
|
|
/* Message length */
|
|
#define CMD_QUERY_NUM_MAX 9
|
|
#define MSG_COORD_NUM_MAX 12
|
|
|
|
/* Commands */
|
|
#define CMD_QUERY 0x2a
|
|
|
|
struct wacom_w9000_variant {
|
|
const u8 cmd_query_num;
|
|
const u8 msg_coord_num;
|
|
const char *name;
|
|
};
|
|
|
|
struct wacom_w9000_data {
|
|
struct i2c_client *client;
|
|
struct input_dev *input_dev;
|
|
const struct wacom_w9000_variant *variant;
|
|
u16 fw_version;
|
|
|
|
struct touchscreen_properties prop;
|
|
u16 max_pressure;
|
|
|
|
struct regulator *regulator;
|
|
|
|
struct gpio_desc *flash_mode_gpio;
|
|
struct gpio_desc *reset_gpio;
|
|
|
|
unsigned int irq;
|
|
|
|
bool pen_proximity;
|
|
};
|
|
|
|
static int wacom_w9000_read(struct i2c_client *client, u8 command, u8 len, u8 *data)
|
|
{
|
|
int error, res;
|
|
struct i2c_msg msg[] = {
|
|
{
|
|
.addr = client->addr,
|
|
.flags = 0,
|
|
.buf = &command,
|
|
.len = sizeof(command),
|
|
}, {
|
|
.addr = client->addr,
|
|
.flags = I2C_M_RD,
|
|
.buf = data,
|
|
.len = len,
|
|
}
|
|
};
|
|
|
|
res = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
|
|
if (res != ARRAY_SIZE(msg)) {
|
|
error = res < 0 ? res : -EIO;
|
|
dev_err(&client->dev, "%s: i2c transfer failed: %d (%d)\n",
|
|
__func__, error, res);
|
|
return error;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int wacom_w9000_query(struct wacom_w9000_data *wacom_data)
|
|
{
|
|
struct i2c_client *client = wacom_data->client;
|
|
struct device *dev = &wacom_data->client->dev;
|
|
u8 data[CMD_QUERY_NUM_MAX];
|
|
int retry;
|
|
int error;
|
|
|
|
for (retry = 0; retry < CMD_QUERY_RETRIES; retry++) {
|
|
error = wacom_w9000_read(client, CMD_QUERY,
|
|
wacom_data->variant->cmd_query_num,
|
|
data);
|
|
if (!error && data[0] == 0x0f)
|
|
break;
|
|
}
|
|
|
|
if (error)
|
|
return error;
|
|
|
|
if (data[0] != 0x0f)
|
|
return -EIO;
|
|
|
|
dev_dbg(dev, "query: %*ph, %d\n",
|
|
wacom_data->variant->cmd_query_num, data, retry);
|
|
|
|
wacom_data->prop.max_x = get_unaligned_be16(&data[1]);
|
|
wacom_data->prop.max_y = get_unaligned_be16(&data[3]);
|
|
wacom_data->max_pressure = get_unaligned_be16(&data[5]);
|
|
wacom_data->fw_version = get_unaligned_be16(&data[7]);
|
|
|
|
dev_dbg(dev, "max_x:%d, max_y:%d, max_pressure:%d, fw:%#x\n",
|
|
wacom_data->prop.max_x, wacom_data->prop.max_y,
|
|
wacom_data->max_pressure, wacom_data->fw_version);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int wacom_w9000_power_on(struct wacom_w9000_data *wacom_data)
|
|
{
|
|
int error;
|
|
|
|
lockdep_assert_held(&wacom_data->input_dev->mutex);
|
|
|
|
error = regulator_enable(wacom_data->regulator);
|
|
if (error) {
|
|
dev_err(&wacom_data->client->dev, "Failed to enable regulators: %d\n", error);
|
|
return error;
|
|
}
|
|
|
|
msleep(200);
|
|
|
|
gpiod_set_value_cansleep(wacom_data->reset_gpio, 0);
|
|
enable_irq(wacom_data->irq);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int wacom_w9000_power_off(struct wacom_w9000_data *wacom_data)
|
|
{
|
|
lockdep_assert_held(&wacom_data->input_dev->mutex);
|
|
|
|
disable_irq(wacom_data->irq);
|
|
gpiod_set_value_cansleep(wacom_data->reset_gpio, 1);
|
|
regulator_disable(wacom_data->regulator);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void wacom_w9000_coord(struct wacom_w9000_data *wacom_data)
|
|
{
|
|
struct i2c_client *client = wacom_data->client;
|
|
struct device *dev = &wacom_data->client->dev;
|
|
u8 data[MSG_COORD_NUM_MAX];
|
|
bool touch, rubber, side_button;
|
|
u16 x, y, pressure;
|
|
u8 distance = 0;
|
|
int error;
|
|
|
|
error = i2c_master_recv(client, data, wacom_data->variant->msg_coord_num);
|
|
if (error != wacom_data->variant->msg_coord_num) {
|
|
if (error >= 0)
|
|
error = -EIO;
|
|
dev_err_ratelimited(dev, "%s: i2c receive failed (%d)\n", __func__, error);
|
|
return;
|
|
}
|
|
|
|
dev_dbg(dev, "data: %*ph\n", wacom_data->variant->msg_coord_num, data);
|
|
|
|
if (data[0] & BIT(7)) {
|
|
wacom_data->pen_proximity = true;
|
|
|
|
touch = !!(data[0] & BIT(4));
|
|
side_button = !!(data[0] & BIT(5));
|
|
rubber = !!(data[0] & BIT(6));
|
|
|
|
x = get_unaligned_be16(&data[1]);
|
|
y = get_unaligned_be16(&data[3]);
|
|
pressure = get_unaligned_be16(&data[5]);
|
|
|
|
if (wacom_data->variant->msg_coord_num > 7)
|
|
distance = data[7];
|
|
|
|
if (x > wacom_data->prop.max_x || y > wacom_data->prop.max_y) {
|
|
dev_warn_ratelimited(dev, "Coordinates out of range x=%d, y=%d\n", x, y);
|
|
return;
|
|
}
|
|
|
|
if (pressure > wacom_data->max_pressure) {
|
|
dev_warn_ratelimited(dev, "Pressure out of range %d\n", pressure);
|
|
return;
|
|
}
|
|
|
|
touchscreen_report_pos(wacom_data->input_dev, &wacom_data->prop, x, y, false);
|
|
input_report_abs(wacom_data->input_dev, ABS_PRESSURE, pressure);
|
|
|
|
if (wacom_data->variant->msg_coord_num > 7)
|
|
input_report_abs(wacom_data->input_dev, ABS_DISTANCE, distance);
|
|
|
|
input_report_key(wacom_data->input_dev, BTN_STYLUS, side_button);
|
|
input_report_key(wacom_data->input_dev, BTN_TOUCH, touch);
|
|
input_report_key(wacom_data->input_dev, BTN_TOOL_PEN, !rubber);
|
|
input_report_key(wacom_data->input_dev, BTN_TOOL_RUBBER, rubber);
|
|
input_sync(wacom_data->input_dev);
|
|
} else if (wacom_data->pen_proximity) {
|
|
input_report_abs(wacom_data->input_dev, ABS_PRESSURE, 0);
|
|
|
|
if (wacom_data->variant->msg_coord_num > 7)
|
|
input_report_abs(wacom_data->input_dev, ABS_DISTANCE, 255);
|
|
|
|
input_report_key(wacom_data->input_dev, BTN_STYLUS, 0);
|
|
input_report_key(wacom_data->input_dev, BTN_TOUCH, 0);
|
|
input_report_key(wacom_data->input_dev, BTN_TOOL_PEN, 0);
|
|
input_report_key(wacom_data->input_dev, BTN_TOOL_RUBBER, 0);
|
|
input_sync(wacom_data->input_dev);
|
|
|
|
wacom_data->pen_proximity = false;
|
|
}
|
|
}
|
|
|
|
static irqreturn_t wacom_w9000_interrupt(int irq, void *dev_id)
|
|
{
|
|
struct wacom_w9000_data *wacom_data = dev_id;
|
|
|
|
wacom_w9000_coord(wacom_data);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static int wacom_w9000_open(struct input_dev *dev)
|
|
{
|
|
struct wacom_w9000_data *wacom_data = input_get_drvdata(dev);
|
|
|
|
return wacom_w9000_power_on(wacom_data);
|
|
}
|
|
|
|
static void wacom_w9000_close(struct input_dev *dev)
|
|
{
|
|
struct wacom_w9000_data *wacom_data = input_get_drvdata(dev);
|
|
|
|
wacom_w9000_power_off(wacom_data);
|
|
}
|
|
|
|
static int wacom_w9000_probe(struct i2c_client *client)
|
|
{
|
|
struct device *dev = &client->dev;
|
|
struct wacom_w9000_data *wacom_data;
|
|
struct input_dev *input_dev;
|
|
int error;
|
|
u32 val;
|
|
|
|
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
|
|
dev_err(dev, "i2c_check_functionality error\n");
|
|
return -EIO;
|
|
}
|
|
|
|
wacom_data = devm_kzalloc(dev, sizeof(*wacom_data), GFP_KERNEL);
|
|
if (!wacom_data)
|
|
return -ENOMEM;
|
|
|
|
wacom_data->variant = i2c_get_match_data(client);
|
|
if (!wacom_data->variant) {
|
|
dev_err(dev, "No i2c match_data available\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (wacom_data->variant->cmd_query_num > CMD_QUERY_NUM_MAX ||
|
|
wacom_data->variant->msg_coord_num > MSG_COORD_NUM_MAX) {
|
|
dev_err(dev, "Length of message for %s exceeds the maximum\n",
|
|
wacom_data->variant->name);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (wacom_data->variant->msg_coord_num < 7) {
|
|
dev_err(dev, "Length of coordinates message for %s too short\n",
|
|
wacom_data->variant->name);
|
|
return -EINVAL;
|
|
}
|
|
|
|
wacom_data->client = client;
|
|
wacom_data->irq = client->irq;
|
|
i2c_set_clientdata(client, wacom_data);
|
|
|
|
wacom_data->regulator = devm_regulator_get(dev, "vdd");
|
|
if (IS_ERR(wacom_data->regulator))
|
|
return dev_err_probe(dev, PTR_ERR(wacom_data->regulator),
|
|
"Failed to get regulators\n");
|
|
|
|
wacom_data->flash_mode_gpio = devm_gpiod_get_optional(dev, "flash-mode", GPIOD_OUT_LOW);
|
|
if (IS_ERR(wacom_data->flash_mode_gpio))
|
|
return dev_err_probe(dev, PTR_ERR(wacom_data->flash_mode_gpio),
|
|
"Failed to get flash-mode gpio\n");
|
|
|
|
wacom_data->reset_gpio = devm_gpiod_get_optional(dev, "reset", GPIOD_OUT_HIGH);
|
|
if (IS_ERR(wacom_data->reset_gpio))
|
|
return dev_err_probe(dev, PTR_ERR(wacom_data->reset_gpio),
|
|
"Failed to get reset gpio\n");
|
|
|
|
error = regulator_enable(wacom_data->regulator);
|
|
if (error)
|
|
return dev_err_probe(dev, error, "Failed to enable regulators\n");
|
|
|
|
msleep(200);
|
|
|
|
gpiod_set_value_cansleep(wacom_data->reset_gpio, 0);
|
|
|
|
error = wacom_w9000_query(wacom_data);
|
|
|
|
gpiod_set_value_cansleep(wacom_data->reset_gpio, 1);
|
|
regulator_disable(wacom_data->regulator);
|
|
|
|
if (error)
|
|
return dev_err_probe(dev, error, "Failed to query\n");
|
|
|
|
error = devm_request_threaded_irq(dev, wacom_data->irq, NULL, wacom_w9000_interrupt,
|
|
IRQF_ONESHOT | IRQF_NO_AUTOEN, client->name, wacom_data);
|
|
if (error)
|
|
return dev_err_probe(dev, error, "Failed to register interrupt\n");
|
|
|
|
input_dev = devm_input_allocate_device(dev);
|
|
if (!input_dev)
|
|
return -ENOMEM;
|
|
|
|
wacom_data->input_dev = input_dev;
|
|
input_set_drvdata(input_dev, wacom_data);
|
|
|
|
input_dev->name = wacom_data->variant->name;
|
|
input_dev->id.bustype = BUS_I2C;
|
|
input_dev->id.vendor = 0x56a;
|
|
input_dev->id.version = wacom_data->fw_version;
|
|
input_dev->open = wacom_w9000_open;
|
|
input_dev->close = wacom_w9000_close;
|
|
|
|
input_set_capability(input_dev, EV_KEY, BTN_TOUCH);
|
|
input_set_capability(input_dev, EV_KEY, BTN_TOOL_PEN);
|
|
input_set_capability(input_dev, EV_KEY, BTN_TOOL_RUBBER);
|
|
input_set_capability(input_dev, EV_KEY, BTN_STYLUS);
|
|
|
|
input_set_abs_params(input_dev, ABS_X, 0, wacom_data->prop.max_x, 4, 0);
|
|
input_set_abs_params(input_dev, ABS_Y, 0, wacom_data->prop.max_y, 4, 0);
|
|
input_set_abs_params(input_dev, ABS_PRESSURE, 0, wacom_data->max_pressure, 0, 0);
|
|
|
|
if (wacom_data->variant->msg_coord_num > 7)
|
|
input_set_abs_params(input_dev, ABS_DISTANCE, 0, 255, 0, 0);
|
|
|
|
touchscreen_parse_properties(input_dev, false, &wacom_data->prop);
|
|
|
|
dev_info(dev, "%s size X%uY%u\n", wacom_data->variant->name,
|
|
wacom_data->prop.max_x, wacom_data->prop.max_y);
|
|
|
|
error = device_property_read_u32(dev, "touchscreen-x-mm", &val);
|
|
if (!error && val)
|
|
input_abs_set_res(input_dev, wacom_data->prop.swap_x_y ? ABS_Y : ABS_X,
|
|
wacom_data->prop.max_x / val);
|
|
error = device_property_read_u32(dev, "touchscreen-y-mm", &val);
|
|
if (!error && val)
|
|
input_abs_set_res(input_dev, wacom_data->prop.swap_x_y ? ABS_X : ABS_Y,
|
|
wacom_data->prop.max_y / val);
|
|
|
|
error = input_register_device(wacom_data->input_dev);
|
|
if (error)
|
|
return dev_err_probe(dev, error, "Failed to register input device\n");
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int wacom_w9000_suspend(struct device *dev)
|
|
{
|
|
struct i2c_client *client = to_i2c_client(dev);
|
|
struct wacom_w9000_data *wacom_data = i2c_get_clientdata(client);
|
|
|
|
guard(mutex)(&wacom_data->input_dev->mutex);
|
|
|
|
if (input_device_enabled(wacom_data->input_dev))
|
|
return wacom_w9000_power_off(wacom_data);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int wacom_w9000_resume(struct device *dev)
|
|
{
|
|
struct i2c_client *client = to_i2c_client(dev);
|
|
struct wacom_w9000_data *wacom_data = i2c_get_clientdata(client);
|
|
|
|
guard(mutex)(&wacom_data->input_dev->mutex);
|
|
|
|
if (input_device_enabled(wacom_data->input_dev))
|
|
return wacom_w9000_power_on(wacom_data);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static DEFINE_SIMPLE_DEV_PM_OPS(wacom_w9000_pm, wacom_w9000_suspend, wacom_w9000_resume);
|
|
|
|
static const struct wacom_w9000_variant w9002 = {
|
|
.cmd_query_num = 9,
|
|
.msg_coord_num = 7,
|
|
.name = "Wacom W9002 Digitizer",
|
|
};
|
|
|
|
static const struct wacom_w9000_variant w9007a_lt03 = {
|
|
.cmd_query_num = 9,
|
|
.msg_coord_num = 8,
|
|
.name = "Wacom W9007A LT03 Digitizer",
|
|
};
|
|
|
|
static const struct wacom_w9000_variant w9007a_v1 = {
|
|
.cmd_query_num = 9,
|
|
.msg_coord_num = 12,
|
|
.name = "Wacom W9007A V1 Digitizer",
|
|
};
|
|
|
|
static const struct of_device_id wacom_w9000_of_match[] = {
|
|
{ .compatible = "wacom,w9002", .data = &w9002 },
|
|
{ .compatible = "wacom,w9007a-lt03", .data = &w9007a_lt03, },
|
|
{ .compatible = "wacom,w9007a-v1", .data = &w9007a_v1, },
|
|
{ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, wacom_w9000_of_match);
|
|
|
|
static const struct i2c_device_id wacom_w9000_id[] = {
|
|
{ .name = "w9002", .driver_data = (kernel_ulong_t)&w9002 },
|
|
{ .name = "w9007a-lt03", .driver_data = (kernel_ulong_t)&w9007a_lt03 },
|
|
{ .name = "w9007a-v1", .driver_data = (kernel_ulong_t)&w9007a_v1 },
|
|
{ }
|
|
};
|
|
MODULE_DEVICE_TABLE(i2c, wacom_w9000_id);
|
|
|
|
static struct i2c_driver wacom_w9000_driver = {
|
|
.driver = {
|
|
.name = "wacom_w9000",
|
|
.of_match_table = wacom_w9000_of_match,
|
|
.pm = pm_sleep_ptr(&wacom_w9000_pm),
|
|
},
|
|
.probe = wacom_w9000_probe,
|
|
.id_table = wacom_w9000_id,
|
|
};
|
|
module_i2c_driver(wacom_w9000_driver);
|
|
|
|
MODULE_AUTHOR("Hendrik Noack <hendrik-noack@gmx.de>");
|
|
MODULE_DESCRIPTION("Wacom W9000-series penabled touchscreen driver");
|
|
MODULE_LICENSE("GPL");
|