Message ID | 20180407145417.31463-1-hdegoede@redhat.com (mailing list archive) |
---|---|
State | Accepted |
Headers | show |
On Sat, Apr 07, 2018 at 04:54:17PM +0200, Hans de Goede wrote: > The ChipOne icn8505 is an i2c capacitive touchscreen controller typically > used in cheap x86 tablets, this commit adds a driver for it. > > Note the icn8505 is somewhat similar to the icn8318 and I started with > modifying that driver to support both, but in the end the differences were > too large and I decided to write a new driver instead. > > Cc: Antonio Davide Trogu <trogu.davide@gmail.com> > Signed-off-by: Hans de Goede <hdegoede@redhat.com> Applied, thank you. > --- > Changes in v2: > -Make icn8505_write/read_xfer return 0 rather then a positive value on success > -Rename the "ret" local variable to "error" where applicable > -Move printing of attempt %d/%d on firmware upload issues from > icn8505_try_fw_upload() to its caller, icn8505_upload_fw() > -Drop inline marking from icn8505_touch_active() > -Drop duplicate input->dev.parent = dev; > -Use __maybe_unused for suspend/resume() instead of #ifdef CONFIG_PM_SLEEP > --- > MAINTAINERS | 6 + > drivers/input/touchscreen/Kconfig | 11 + > drivers/input/touchscreen/Makefile | 1 + > drivers/input/touchscreen/chipone_icn8505.c | 516 ++++++++++++++++++++ > 4 files changed, 534 insertions(+) > create mode 100644 drivers/input/touchscreen/chipone_icn8505.c > > diff --git a/MAINTAINERS b/MAINTAINERS > index e20377cadbf5..8c0a1b8707db 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -3436,6 +3436,12 @@ S: Maintained > F: Documentation/devicetree/bindings/input/touchscreen/chipone_icn8318.txt > F: drivers/input/touchscreen/chipone_icn8318.c > > +CHIPONE ICN8505 I2C TOUCHSCREEN DRIVER > +M: Hans de Goede <hdegoede@redhat.com> > +L: linux-input@vger.kernel.org > +S: Maintained > +F: drivers/input/touchscreen/chipone_icn8505.c > + > CHROME HARDWARE PLATFORM SUPPORT > M: Benson Leung <bleung@chromium.org> > M: Olof Johansson <olof@lixom.net> > diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig > index 4f15496fec8b..94cc740a4203 100644 > --- a/drivers/input/touchscreen/Kconfig > +++ b/drivers/input/touchscreen/Kconfig > @@ -164,6 +164,17 @@ config TOUCHSCREEN_CHIPONE_ICN8318 > To compile this driver as a module, choose M here: the > module will be called chipone_icn8318. > > +config TOUCHSCREEN_CHIPONE_ICN8505 > + tristate "chipone icn8505 touchscreen controller" > + depends on I2C && ACPI > + help > + Say Y here if you have a ChipOne icn8505 based I2C touchscreen. > + > + If unsure, say N. > + > + To compile this driver as a module, choose M here: the > + module will be called chipone_icn8505. > + > config TOUCHSCREEN_CY8CTMG110 > tristate "cy8ctmg110 touchscreen" > depends on I2C > diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile > index dddae7973436..fd4fd32fb73f 100644 > --- a/drivers/input/touchscreen/Makefile > +++ b/drivers/input/touchscreen/Makefile > @@ -19,6 +19,7 @@ obj-$(CONFIG_TOUCHSCREEN_ATMEL_MXT) += atmel_mxt_ts.o > obj-$(CONFIG_TOUCHSCREEN_AUO_PIXCIR) += auo-pixcir-ts.o > obj-$(CONFIG_TOUCHSCREEN_BU21013) += bu21013_ts.o > obj-$(CONFIG_TOUCHSCREEN_CHIPONE_ICN8318) += chipone_icn8318.o > +obj-$(CONFIG_TOUCHSCREEN_CHIPONE_ICN8505) += chipone_icn8505.o > obj-$(CONFIG_TOUCHSCREEN_CY8CTMG110) += cy8ctmg110_ts.o > obj-$(CONFIG_TOUCHSCREEN_CYTTSP_CORE) += cyttsp_core.o > obj-$(CONFIG_TOUCHSCREEN_CYTTSP_I2C) += cyttsp_i2c.o cyttsp_i2c_common.o > diff --git a/drivers/input/touchscreen/chipone_icn8505.c b/drivers/input/touchscreen/chipone_icn8505.c > new file mode 100644 > index 000000000000..167b3c2d62c6 > --- /dev/null > +++ b/drivers/input/touchscreen/chipone_icn8505.c > @@ -0,0 +1,516 @@ > +// SPDX-License-Identifier: GPL-2.0+ > +/* > + * Driver for ChipOne icn8505 i2c touchscreen controller > + * > + * Copyright (c) 2015-2018 Red Hat Inc. > + * > + * Red Hat authors: > + * Hans de Goede <hdegoede@redhat.com> > + */ > + > +#include <asm/unaligned.h> > +#include <linux/acpi.h> > +#include <linux/crc32.h> > +#include <linux/delay.h> > +#include <linux/firmware.h> > +#include <linux/interrupt.h> > +#include <linux/i2c.h> > +#include <linux/input.h> > +#include <linux/input/mt.h> > +#include <linux/input/touchscreen.h> > +#include <linux/module.h> > + > +/* Normal operation mode defines */ > +#define ICN8505_REG_ADDR_WIDTH 16 > + > +#define ICN8505_REG_POWER 0x0004 > +#define ICN8505_REG_TOUCHDATA 0x1000 > +#define ICN8505_REG_CONFIGDATA 0x8000 > + > +/* ICN8505_REG_POWER commands */ > +#define ICN8505_POWER_ACTIVE 0x00 > +#define ICN8505_POWER_MONITOR 0x01 > +#define ICN8505_POWER_HIBERNATE 0x02 > +/* > + * The Android driver uses these to turn on/off the charger filter, but the > + * filter is way too aggressive making e.g. onscreen keyboards unusable. > + */ > +#define ICN8505_POWER_ENA_CHARGER_MODE 0x55 > +#define ICN8505_POWER_DIS_CHARGER_MODE 0x66 > + > +#define ICN8505_MAX_TOUCHES 10 > + > +/* Programming mode defines */ > +#define ICN8505_PROG_I2C_ADDR 0x30 > +#define ICN8505_PROG_REG_ADDR_WIDTH 24 > + > +#define MAX_FW_UPLOAD_TRIES 3 > + > +struct icn8505_touch { > + __u8 slot; > + __u8 x[2]; > + __u8 y[2]; > + __u8 pressure; /* Seems more like finger width then pressure really */ > + __u8 event; > +/* The difference between 2 and 3 is unclear */ > +#define ICN8505_EVENT_NO_DATA 1 /* No finger seen yet since wakeup */ > +#define ICN8505_EVENT_UPDATE1 2 /* New or updated coordinates */ > +#define ICN8505_EVENT_UPDATE2 3 /* New or updated coordinates */ > +#define ICN8505_EVENT_END 4 /* Finger lifted */ > +} __packed; > + > +struct icn8505_touch_data { > + __u8 softbutton; > + __u8 touch_count; > + struct icn8505_touch touches[ICN8505_MAX_TOUCHES]; > +} __packed; > + > +struct icn8505_data { > + struct i2c_client *client; > + struct input_dev *input; > + struct gpio_desc *wake_gpio; > + struct touchscreen_properties prop; > + char firmware_name[32]; > +}; > + > +static int icn8505_read_xfer(struct i2c_client *client, u16 i2c_addr, > + int reg_addr, int reg_addr_width, > + void *data, int len, bool silent) > +{ > + u8 buf[3]; > + int i, ret; > + struct i2c_msg msg[2] = { > + { > + .addr = i2c_addr, > + .buf = buf, > + .len = reg_addr_width / 8, > + }, > + { > + .addr = i2c_addr, > + .flags = I2C_M_RD, > + .buf = data, > + .len = len, > + } > + }; > + > + for (i = 0; i < (reg_addr_width / 8); i++) > + buf[i] = (reg_addr >> (reg_addr_width - (i + 1) * 8)) & 0xff; > + > + ret = i2c_transfer(client->adapter, msg, 2); > + if (ret != ARRAY_SIZE(msg)) { > + if (ret >= 0) > + ret = -EIO; > + if (!silent) > + dev_err(&client->dev, "Error reading addr %#x reg %#x: %d\n", > + i2c_addr, reg_addr, ret); > + return ret; > + } > + > + return 0; > +} > + > +static int icn8505_write_xfer(struct i2c_client *client, u16 i2c_addr, > + int reg_addr, int reg_addr_width, > + const void *data, int len, bool silent) > +{ > + u8 buf[3 + 32]; /* 3 bytes for 24 bit reg-addr + 32 bytes max len */ > + int i, ret; > + struct i2c_msg msg = { > + .addr = i2c_addr, > + .buf = buf, > + .len = reg_addr_width / 8 + len, > + }; > + > + if (WARN_ON(len > 32)) > + return -EINVAL; > + > + for (i = 0; i < (reg_addr_width / 8); i++) > + buf[i] = (reg_addr >> (reg_addr_width - (i + 1) * 8)) & 0xff; > + > + memcpy(buf + reg_addr_width / 8, data, len); > + > + ret = i2c_transfer(client->adapter, &msg, 1); > + if (ret != 1) { > + if (ret >= 0) > + ret = -EIO; > + if (!silent) > + dev_err(&client->dev, "Error writing addr %#x reg %#x: %d\n", > + i2c_addr, reg_addr, ret); > + return ret; > + } > + > + return 0; > +} > + > +static int icn8505_read_data(struct icn8505_data *icn8505, int reg, > + void *buf, int len) > +{ > + return icn8505_read_xfer(icn8505->client, icn8505->client->addr, reg, > + ICN8505_REG_ADDR_WIDTH, buf, len, false); > +} > + > +static int icn8505_read_reg_silent(struct icn8505_data *icn8505, int reg) > +{ > + u8 buf; > + int error; > + > + error = icn8505_read_xfer(icn8505->client, icn8505->client->addr, reg, > + ICN8505_REG_ADDR_WIDTH, &buf, 1, true); > + if (error) > + return error; > + > + return buf; > +} > + > +static int icn8505_write_reg(struct icn8505_data *icn8505, int reg, u8 val) > +{ > + return icn8505_write_xfer(icn8505->client, icn8505->client->addr, reg, > + ICN8505_REG_ADDR_WIDTH, &val, 1, false); > +} > + > +static int icn8505_read_prog_data(struct icn8505_data *icn8505, int reg, > + void *buf, int len) > +{ > + return icn8505_read_xfer(icn8505->client, ICN8505_PROG_I2C_ADDR, reg, > + ICN8505_PROG_REG_ADDR_WIDTH, buf, len, false); > +} > + > +static int icn8505_write_prog_data(struct icn8505_data *icn8505, int reg, > + const void *buf, int len) > +{ > + return icn8505_write_xfer(icn8505->client, ICN8505_PROG_I2C_ADDR, reg, > + ICN8505_PROG_REG_ADDR_WIDTH, buf, len, false); > +} > + > +static int icn8505_write_prog_reg(struct icn8505_data *icn8505, int reg, u8 val) > +{ > + return icn8505_write_xfer(icn8505->client, ICN8505_PROG_I2C_ADDR, reg, > + ICN8505_PROG_REG_ADDR_WIDTH, &val, 1, false); > +} > + > +/* > + * Note this function uses a number of magic register addresses and values, > + * there are deliberately no defines for these because the algorithm is taken > + * from the icn85xx Android driver and I do not want to make up possibly wrong > + * names for the addresses and/or values. > + */ > +static int icn8505_try_fw_upload(struct icn8505_data *icn8505, > + const struct firmware *fw) > +{ > + struct device *dev = &icn8505->client->dev; > + size_t offset, count; > + int error; > + u8 buf[4]; > + u32 crc; > + > + /* Put the controller in programming mode */ > + error = icn8505_write_prog_reg(icn8505, 0xcc3355, 0x5a); > + if (error) > + return error; > + > + usleep_range(2000, 5000); > + > + error = icn8505_write_prog_reg(icn8505, 0x040400, 0x01); > + if (error) > + return error; > + > + usleep_range(2000, 5000); > + > + error = icn8505_read_prog_data(icn8505, 0x040002, buf, 1); > + if (error) > + return error; > + > + if (buf[0] != 0x85) { > + dev_err(dev, "Failed to enter programming mode\n"); > + return -ENODEV; > + } > + > + usleep_range(1000, 5000); > + > + /* Enable CRC mode */ > + error = icn8505_write_prog_reg(icn8505, 0x40028, 1); > + if (error) > + return error; > + > + /* Send the firmware to SRAM */ > + for (offset = 0; offset < fw->size; offset += count) { > + count = min_t(size_t, fw->size - offset, 32); > + error = icn8505_write_prog_data(icn8505, offset, > + fw->data + offset, count); > + if (error) > + return error; > + } > + > + /* Disable CRC mode */ > + error = icn8505_write_prog_reg(icn8505, 0x40028, 0); > + if (error) > + return error; > + > + /* Get and check length and CRC */ > + error = icn8505_read_prog_data(icn8505, 0x40034, buf, 2); > + if (error) > + return error; > + > + if (get_unaligned_le16(buf) != fw->size) { > + dev_warn(dev, "Length mismatch after uploading fw\n"); > + return -EIO; > + } > + > + error = icn8505_read_prog_data(icn8505, 0x4002c, buf, 4); > + if (error) > + return error; > + > + crc = crc32_be(0, fw->data, fw->size); > + if (get_unaligned_le32(buf) != crc) { > + dev_warn(dev, "CRC mismatch after uploading fw\n"); > + return -EIO; > + } > + > + /* Boot controller from SRAM */ > + error = icn8505_write_prog_reg(icn8505, 0x40400, 0x03); > + if (error) > + return error; > + > + usleep_range(2000, 5000); > + return 0; > +} > + > +static int icn8505_upload_fw(struct icn8505_data *icn8505) > +{ > + struct device *dev = &icn8505->client->dev; > + const struct firmware *fw; > + int i, error; > + > + /* > + * Always load the firmware, even if we don't need it at boot, we > + * we may need it at resume. Having loaded it once will make the > + * firmware class code cache it at suspend/resume. > + */ > + error = request_firmware(&fw, icn8505->firmware_name, dev); > + if (error) { > + dev_err(dev, "Firmware request error %d\n", error); > + return error; > + } > + > + /* Check if the controller is not already up and running */ > + if (icn8505_read_reg_silent(icn8505, 0x000a) == 0x85) > + goto success; > + > + for (i = 1; i <= MAX_FW_UPLOAD_TRIES; i++) { > + error = icn8505_try_fw_upload(icn8505, fw); > + if (!error) > + goto success; > + > + dev_err(dev, "Failed to upload firmware: %d (attempt %d/%d)\n", > + error, i, MAX_FW_UPLOAD_TRIES); > + usleep_range(2000, 5000); > + } > + > +success: > + release_firmware(fw); > + return error; > +} > + > +static bool icn8505_touch_active(u8 event) > +{ > + return (event == ICN8505_EVENT_UPDATE1) || > + (event == ICN8505_EVENT_UPDATE2); > +} > + > +static irqreturn_t icn8505_irq(int irq, void *dev_id) > +{ > + struct icn8505_data *icn8505 = dev_id; > + struct device *dev = &icn8505->client->dev; > + struct icn8505_touch_data touch_data; > + int i, error; > + > + error = icn8505_read_data(icn8505, ICN8505_REG_TOUCHDATA, > + &touch_data, sizeof(touch_data)); > + if (error) { > + dev_err(dev, "Error reading touch data: %d\n", error); > + return IRQ_HANDLED; > + } > + > + if (touch_data.touch_count > ICN8505_MAX_TOUCHES) { > + dev_warn(dev, "Too many touches %d > %d\n", > + touch_data.touch_count, ICN8505_MAX_TOUCHES); > + touch_data.touch_count = ICN8505_MAX_TOUCHES; > + } > + > + for (i = 0; i < touch_data.touch_count; i++) { > + struct icn8505_touch *touch = &touch_data.touches[i]; > + bool act = icn8505_touch_active(touch->event); > + > + input_mt_slot(icn8505->input, touch->slot); > + input_mt_report_slot_state(icn8505->input, MT_TOOL_FINGER, act); > + if (!act) > + continue; > + > + touchscreen_report_pos(icn8505->input, &icn8505->prop, > + get_unaligned_le16(touch->x), > + get_unaligned_le16(touch->y), > + true); > + } > + > + input_mt_sync_frame(icn8505->input); > + input_report_key(icn8505->input, KEY_LEFTMETA, > + touch_data.softbutton == 1); > + input_sync(icn8505->input); > + > + return IRQ_HANDLED; > +} > + > +static int icn8505_probe_acpi(struct icn8505_data *icn8505, struct device *dev) > +{ > + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; > + const char *subsys = "unknown"; > + struct acpi_device *adev; > + union acpi_object *obj; > + acpi_status status; > + > + adev = ACPI_COMPANION(dev); > + if (!adev) > + return -ENODEV; > + > + status = acpi_evaluate_object(adev->handle, "_SUB", NULL, &buffer); > + if (ACPI_SUCCESS(status)) { > + obj = buffer.pointer; > + if (obj->type == ACPI_TYPE_STRING) > + subsys = obj->string.pointer; > + else > + dev_warn(dev, "Warning ACPI _SUB did not return a string\n"); > + } else { > + dev_warn(dev, "Warning ACPI _SUB failed: %#x\n", status); > + buffer.pointer = NULL; > + } > + > + snprintf(icn8505->firmware_name, sizeof(icn8505->firmware_name), > + "chipone/icn8505-%s.fw", subsys); > + > + kfree(buffer.pointer); > + return 0; > +} > + > +static int icn8505_probe(struct i2c_client *client) > +{ > + struct device *dev = &client->dev; > + struct icn8505_data *icn8505; > + struct input_dev *input; > + __le16 resolution[2]; > + int error; > + > + if (!client->irq) { > + dev_err(dev, "No irq specified\n"); > + return -EINVAL; > + } > + > + icn8505 = devm_kzalloc(dev, sizeof(*icn8505), GFP_KERNEL); > + if (!icn8505) > + return -ENOMEM; > + > + input = devm_input_allocate_device(dev); > + if (!input) > + return -ENOMEM; > + > + input->name = client->name; > + input->id.bustype = BUS_I2C; > + > + input_set_capability(input, EV_ABS, ABS_MT_POSITION_X); > + input_set_capability(input, EV_ABS, ABS_MT_POSITION_Y); > + input_set_capability(input, EV_KEY, KEY_LEFTMETA); > + > + icn8505->client = client; > + icn8505->input = input; > + input_set_drvdata(input, icn8505); > + > + error = icn8505_probe_acpi(icn8505, dev); > + if (error) > + return error; > + > + error = icn8505_upload_fw(icn8505); > + if (error) > + return error; > + > + error = icn8505_read_data(icn8505, ICN8505_REG_CONFIGDATA, > + resolution, sizeof(resolution)); > + if (error) { > + dev_err(dev, "Error reading resolution: %d\n", error); > + return error; > + } > + > + input_set_abs_params(input, ABS_MT_POSITION_X, 0, > + le16_to_cpu(resolution[0]) - 1, 0, 0); > + input_set_abs_params(input, ABS_MT_POSITION_Y, 0, > + le16_to_cpu(resolution[1]) - 1, 0, 0); > + > + touchscreen_parse_properties(input, true, &icn8505->prop); > + if (!input_abs_get_max(input, ABS_MT_POSITION_X) || > + !input_abs_get_max(input, ABS_MT_POSITION_Y)) { > + dev_err(dev, "Error touchscreen-size-x and/or -y missing\n"); > + return -EINVAL; > + } > + > + error = input_mt_init_slots(input, ICN8505_MAX_TOUCHES, > + INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED); > + if (error) > + return error; > + > + error = devm_request_threaded_irq(dev, client->irq, NULL, icn8505_irq, > + IRQF_ONESHOT, client->name, icn8505); > + if (error) { > + dev_err(dev, "Error requesting irq: %d\n", error); > + return error; > + } > + > + error = input_register_device(input); > + if (error) > + return error; > + > + i2c_set_clientdata(client, icn8505); > + return 0; > +} > + > +static int __maybe_unused icn8505_suspend(struct device *dev) > +{ > + struct icn8505_data *icn8505 = i2c_get_clientdata(to_i2c_client(dev)); > + > + disable_irq(icn8505->client->irq); > + icn8505_write_reg(icn8505, ICN8505_REG_POWER, ICN8505_POWER_HIBERNATE); > + return 0; > +} > + > +static int __maybe_unused icn8505_resume(struct device *dev) > +{ > + struct icn8505_data *icn8505 = i2c_get_clientdata(to_i2c_client(dev)); > + int error; > + > + error = icn8505_upload_fw(icn8505); > + if (error) > + return error; > + > + enable_irq(icn8505->client->irq); > + return 0; > +} > + > +static SIMPLE_DEV_PM_OPS(icn8505_pm_ops, icn8505_suspend, icn8505_resume); > + > +static const struct acpi_device_id icn8505_acpi_match[] = { > + { "CHPN0001" }, > + { } > +}; > +MODULE_DEVICE_TABLE(acpi, icn8505_acpi_match); > + > +static struct i2c_driver icn8505_driver = { > + .driver = { > + .name = "chipone_icn8505", > + .pm = &icn8505_pm_ops, > + .acpi_match_table = icn8505_acpi_match, > + }, > + .probe_new = icn8505_probe, > +}; > + > +module_i2c_driver(icn8505_driver); > + > +MODULE_DESCRIPTION("ChipOne icn8505 I2C Touchscreen Driver"); > +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); > +MODULE_LICENSE("GPL"); > -- > 2.17.0 >
diff --git a/MAINTAINERS b/MAINTAINERS index e20377cadbf5..8c0a1b8707db 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -3436,6 +3436,12 @@ S: Maintained F: Documentation/devicetree/bindings/input/touchscreen/chipone_icn8318.txt F: drivers/input/touchscreen/chipone_icn8318.c +CHIPONE ICN8505 I2C TOUCHSCREEN DRIVER +M: Hans de Goede <hdegoede@redhat.com> +L: linux-input@vger.kernel.org +S: Maintained +F: drivers/input/touchscreen/chipone_icn8505.c + CHROME HARDWARE PLATFORM SUPPORT M: Benson Leung <bleung@chromium.org> M: Olof Johansson <olof@lixom.net> diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig index 4f15496fec8b..94cc740a4203 100644 --- a/drivers/input/touchscreen/Kconfig +++ b/drivers/input/touchscreen/Kconfig @@ -164,6 +164,17 @@ config TOUCHSCREEN_CHIPONE_ICN8318 To compile this driver as a module, choose M here: the module will be called chipone_icn8318. +config TOUCHSCREEN_CHIPONE_ICN8505 + tristate "chipone icn8505 touchscreen controller" + depends on I2C && ACPI + help + Say Y here if you have a ChipOne icn8505 based I2C touchscreen. + + If unsure, say N. + + To compile this driver as a module, choose M here: the + module will be called chipone_icn8505. + config TOUCHSCREEN_CY8CTMG110 tristate "cy8ctmg110 touchscreen" depends on I2C diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile index dddae7973436..fd4fd32fb73f 100644 --- a/drivers/input/touchscreen/Makefile +++ b/drivers/input/touchscreen/Makefile @@ -19,6 +19,7 @@ obj-$(CONFIG_TOUCHSCREEN_ATMEL_MXT) += atmel_mxt_ts.o obj-$(CONFIG_TOUCHSCREEN_AUO_PIXCIR) += auo-pixcir-ts.o obj-$(CONFIG_TOUCHSCREEN_BU21013) += bu21013_ts.o obj-$(CONFIG_TOUCHSCREEN_CHIPONE_ICN8318) += chipone_icn8318.o +obj-$(CONFIG_TOUCHSCREEN_CHIPONE_ICN8505) += chipone_icn8505.o obj-$(CONFIG_TOUCHSCREEN_CY8CTMG110) += cy8ctmg110_ts.o obj-$(CONFIG_TOUCHSCREEN_CYTTSP_CORE) += cyttsp_core.o obj-$(CONFIG_TOUCHSCREEN_CYTTSP_I2C) += cyttsp_i2c.o cyttsp_i2c_common.o diff --git a/drivers/input/touchscreen/chipone_icn8505.c b/drivers/input/touchscreen/chipone_icn8505.c new file mode 100644 index 000000000000..167b3c2d62c6 --- /dev/null +++ b/drivers/input/touchscreen/chipone_icn8505.c @@ -0,0 +1,516 @@ +// SPDX-License-Identifier: GPL-2.0+ +/* + * Driver for ChipOne icn8505 i2c touchscreen controller + * + * Copyright (c) 2015-2018 Red Hat Inc. + * + * Red Hat authors: + * Hans de Goede <hdegoede@redhat.com> + */ + +#include <asm/unaligned.h> +#include <linux/acpi.h> +#include <linux/crc32.h> +#include <linux/delay.h> +#include <linux/firmware.h> +#include <linux/interrupt.h> +#include <linux/i2c.h> +#include <linux/input.h> +#include <linux/input/mt.h> +#include <linux/input/touchscreen.h> +#include <linux/module.h> + +/* Normal operation mode defines */ +#define ICN8505_REG_ADDR_WIDTH 16 + +#define ICN8505_REG_POWER 0x0004 +#define ICN8505_REG_TOUCHDATA 0x1000 +#define ICN8505_REG_CONFIGDATA 0x8000 + +/* ICN8505_REG_POWER commands */ +#define ICN8505_POWER_ACTIVE 0x00 +#define ICN8505_POWER_MONITOR 0x01 +#define ICN8505_POWER_HIBERNATE 0x02 +/* + * The Android driver uses these to turn on/off the charger filter, but the + * filter is way too aggressive making e.g. onscreen keyboards unusable. + */ +#define ICN8505_POWER_ENA_CHARGER_MODE 0x55 +#define ICN8505_POWER_DIS_CHARGER_MODE 0x66 + +#define ICN8505_MAX_TOUCHES 10 + +/* Programming mode defines */ +#define ICN8505_PROG_I2C_ADDR 0x30 +#define ICN8505_PROG_REG_ADDR_WIDTH 24 + +#define MAX_FW_UPLOAD_TRIES 3 + +struct icn8505_touch { + __u8 slot; + __u8 x[2]; + __u8 y[2]; + __u8 pressure; /* Seems more like finger width then pressure really */ + __u8 event; +/* The difference between 2 and 3 is unclear */ +#define ICN8505_EVENT_NO_DATA 1 /* No finger seen yet since wakeup */ +#define ICN8505_EVENT_UPDATE1 2 /* New or updated coordinates */ +#define ICN8505_EVENT_UPDATE2 3 /* New or updated coordinates */ +#define ICN8505_EVENT_END 4 /* Finger lifted */ +} __packed; + +struct icn8505_touch_data { + __u8 softbutton; + __u8 touch_count; + struct icn8505_touch touches[ICN8505_MAX_TOUCHES]; +} __packed; + +struct icn8505_data { + struct i2c_client *client; + struct input_dev *input; + struct gpio_desc *wake_gpio; + struct touchscreen_properties prop; + char firmware_name[32]; +}; + +static int icn8505_read_xfer(struct i2c_client *client, u16 i2c_addr, + int reg_addr, int reg_addr_width, + void *data, int len, bool silent) +{ + u8 buf[3]; + int i, ret; + struct i2c_msg msg[2] = { + { + .addr = i2c_addr, + .buf = buf, + .len = reg_addr_width / 8, + }, + { + .addr = i2c_addr, + .flags = I2C_M_RD, + .buf = data, + .len = len, + } + }; + + for (i = 0; i < (reg_addr_width / 8); i++) + buf[i] = (reg_addr >> (reg_addr_width - (i + 1) * 8)) & 0xff; + + ret = i2c_transfer(client->adapter, msg, 2); + if (ret != ARRAY_SIZE(msg)) { + if (ret >= 0) + ret = -EIO; + if (!silent) + dev_err(&client->dev, "Error reading addr %#x reg %#x: %d\n", + i2c_addr, reg_addr, ret); + return ret; + } + + return 0; +} + +static int icn8505_write_xfer(struct i2c_client *client, u16 i2c_addr, + int reg_addr, int reg_addr_width, + const void *data, int len, bool silent) +{ + u8 buf[3 + 32]; /* 3 bytes for 24 bit reg-addr + 32 bytes max len */ + int i, ret; + struct i2c_msg msg = { + .addr = i2c_addr, + .buf = buf, + .len = reg_addr_width / 8 + len, + }; + + if (WARN_ON(len > 32)) + return -EINVAL; + + for (i = 0; i < (reg_addr_width / 8); i++) + buf[i] = (reg_addr >> (reg_addr_width - (i + 1) * 8)) & 0xff; + + memcpy(buf + reg_addr_width / 8, data, len); + + ret = i2c_transfer(client->adapter, &msg, 1); + if (ret != 1) { + if (ret >= 0) + ret = -EIO; + if (!silent) + dev_err(&client->dev, "Error writing addr %#x reg %#x: %d\n", + i2c_addr, reg_addr, ret); + return ret; + } + + return 0; +} + +static int icn8505_read_data(struct icn8505_data *icn8505, int reg, + void *buf, int len) +{ + return icn8505_read_xfer(icn8505->client, icn8505->client->addr, reg, + ICN8505_REG_ADDR_WIDTH, buf, len, false); +} + +static int icn8505_read_reg_silent(struct icn8505_data *icn8505, int reg) +{ + u8 buf; + int error; + + error = icn8505_read_xfer(icn8505->client, icn8505->client->addr, reg, + ICN8505_REG_ADDR_WIDTH, &buf, 1, true); + if (error) + return error; + + return buf; +} + +static int icn8505_write_reg(struct icn8505_data *icn8505, int reg, u8 val) +{ + return icn8505_write_xfer(icn8505->client, icn8505->client->addr, reg, + ICN8505_REG_ADDR_WIDTH, &val, 1, false); +} + +static int icn8505_read_prog_data(struct icn8505_data *icn8505, int reg, + void *buf, int len) +{ + return icn8505_read_xfer(icn8505->client, ICN8505_PROG_I2C_ADDR, reg, + ICN8505_PROG_REG_ADDR_WIDTH, buf, len, false); +} + +static int icn8505_write_prog_data(struct icn8505_data *icn8505, int reg, + const void *buf, int len) +{ + return icn8505_write_xfer(icn8505->client, ICN8505_PROG_I2C_ADDR, reg, + ICN8505_PROG_REG_ADDR_WIDTH, buf, len, false); +} + +static int icn8505_write_prog_reg(struct icn8505_data *icn8505, int reg, u8 val) +{ + return icn8505_write_xfer(icn8505->client, ICN8505_PROG_I2C_ADDR, reg, + ICN8505_PROG_REG_ADDR_WIDTH, &val, 1, false); +} + +/* + * Note this function uses a number of magic register addresses and values, + * there are deliberately no defines for these because the algorithm is taken + * from the icn85xx Android driver and I do not want to make up possibly wrong + * names for the addresses and/or values. + */ +static int icn8505_try_fw_upload(struct icn8505_data *icn8505, + const struct firmware *fw) +{ + struct device *dev = &icn8505->client->dev; + size_t offset, count; + int error; + u8 buf[4]; + u32 crc; + + /* Put the controller in programming mode */ + error = icn8505_write_prog_reg(icn8505, 0xcc3355, 0x5a); + if (error) + return error; + + usleep_range(2000, 5000); + + error = icn8505_write_prog_reg(icn8505, 0x040400, 0x01); + if (error) + return error; + + usleep_range(2000, 5000); + + error = icn8505_read_prog_data(icn8505, 0x040002, buf, 1); + if (error) + return error; + + if (buf[0] != 0x85) { + dev_err(dev, "Failed to enter programming mode\n"); + return -ENODEV; + } + + usleep_range(1000, 5000); + + /* Enable CRC mode */ + error = icn8505_write_prog_reg(icn8505, 0x40028, 1); + if (error) + return error; + + /* Send the firmware to SRAM */ + for (offset = 0; offset < fw->size; offset += count) { + count = min_t(size_t, fw->size - offset, 32); + error = icn8505_write_prog_data(icn8505, offset, + fw->data + offset, count); + if (error) + return error; + } + + /* Disable CRC mode */ + error = icn8505_write_prog_reg(icn8505, 0x40028, 0); + if (error) + return error; + + /* Get and check length and CRC */ + error = icn8505_read_prog_data(icn8505, 0x40034, buf, 2); + if (error) + return error; + + if (get_unaligned_le16(buf) != fw->size) { + dev_warn(dev, "Length mismatch after uploading fw\n"); + return -EIO; + } + + error = icn8505_read_prog_data(icn8505, 0x4002c, buf, 4); + if (error) + return error; + + crc = crc32_be(0, fw->data, fw->size); + if (get_unaligned_le32(buf) != crc) { + dev_warn(dev, "CRC mismatch after uploading fw\n"); + return -EIO; + } + + /* Boot controller from SRAM */ + error = icn8505_write_prog_reg(icn8505, 0x40400, 0x03); + if (error) + return error; + + usleep_range(2000, 5000); + return 0; +} + +static int icn8505_upload_fw(struct icn8505_data *icn8505) +{ + struct device *dev = &icn8505->client->dev; + const struct firmware *fw; + int i, error; + + /* + * Always load the firmware, even if we don't need it at boot, we + * we may need it at resume. Having loaded it once will make the + * firmware class code cache it at suspend/resume. + */ + error = request_firmware(&fw, icn8505->firmware_name, dev); + if (error) { + dev_err(dev, "Firmware request error %d\n", error); + return error; + } + + /* Check if the controller is not already up and running */ + if (icn8505_read_reg_silent(icn8505, 0x000a) == 0x85) + goto success; + + for (i = 1; i <= MAX_FW_UPLOAD_TRIES; i++) { + error = icn8505_try_fw_upload(icn8505, fw); + if (!error) + goto success; + + dev_err(dev, "Failed to upload firmware: %d (attempt %d/%d)\n", + error, i, MAX_FW_UPLOAD_TRIES); + usleep_range(2000, 5000); + } + +success: + release_firmware(fw); + return error; +} + +static bool icn8505_touch_active(u8 event) +{ + return (event == ICN8505_EVENT_UPDATE1) || + (event == ICN8505_EVENT_UPDATE2); +} + +static irqreturn_t icn8505_irq(int irq, void *dev_id) +{ + struct icn8505_data *icn8505 = dev_id; + struct device *dev = &icn8505->client->dev; + struct icn8505_touch_data touch_data; + int i, error; + + error = icn8505_read_data(icn8505, ICN8505_REG_TOUCHDATA, + &touch_data, sizeof(touch_data)); + if (error) { + dev_err(dev, "Error reading touch data: %d\n", error); + return IRQ_HANDLED; + } + + if (touch_data.touch_count > ICN8505_MAX_TOUCHES) { + dev_warn(dev, "Too many touches %d > %d\n", + touch_data.touch_count, ICN8505_MAX_TOUCHES); + touch_data.touch_count = ICN8505_MAX_TOUCHES; + } + + for (i = 0; i < touch_data.touch_count; i++) { + struct icn8505_touch *touch = &touch_data.touches[i]; + bool act = icn8505_touch_active(touch->event); + + input_mt_slot(icn8505->input, touch->slot); + input_mt_report_slot_state(icn8505->input, MT_TOOL_FINGER, act); + if (!act) + continue; + + touchscreen_report_pos(icn8505->input, &icn8505->prop, + get_unaligned_le16(touch->x), + get_unaligned_le16(touch->y), + true); + } + + input_mt_sync_frame(icn8505->input); + input_report_key(icn8505->input, KEY_LEFTMETA, + touch_data.softbutton == 1); + input_sync(icn8505->input); + + return IRQ_HANDLED; +} + +static int icn8505_probe_acpi(struct icn8505_data *icn8505, struct device *dev) +{ + struct acpi_buffer buffer = { ACPI_ALLOCATE_BUFFER, NULL }; + const char *subsys = "unknown"; + struct acpi_device *adev; + union acpi_object *obj; + acpi_status status; + + adev = ACPI_COMPANION(dev); + if (!adev) + return -ENODEV; + + status = acpi_evaluate_object(adev->handle, "_SUB", NULL, &buffer); + if (ACPI_SUCCESS(status)) { + obj = buffer.pointer; + if (obj->type == ACPI_TYPE_STRING) + subsys = obj->string.pointer; + else + dev_warn(dev, "Warning ACPI _SUB did not return a string\n"); + } else { + dev_warn(dev, "Warning ACPI _SUB failed: %#x\n", status); + buffer.pointer = NULL; + } + + snprintf(icn8505->firmware_name, sizeof(icn8505->firmware_name), + "chipone/icn8505-%s.fw", subsys); + + kfree(buffer.pointer); + return 0; +} + +static int icn8505_probe(struct i2c_client *client) +{ + struct device *dev = &client->dev; + struct icn8505_data *icn8505; + struct input_dev *input; + __le16 resolution[2]; + int error; + + if (!client->irq) { + dev_err(dev, "No irq specified\n"); + return -EINVAL; + } + + icn8505 = devm_kzalloc(dev, sizeof(*icn8505), GFP_KERNEL); + if (!icn8505) + return -ENOMEM; + + input = devm_input_allocate_device(dev); + if (!input) + return -ENOMEM; + + input->name = client->name; + input->id.bustype = BUS_I2C; + + input_set_capability(input, EV_ABS, ABS_MT_POSITION_X); + input_set_capability(input, EV_ABS, ABS_MT_POSITION_Y); + input_set_capability(input, EV_KEY, KEY_LEFTMETA); + + icn8505->client = client; + icn8505->input = input; + input_set_drvdata(input, icn8505); + + error = icn8505_probe_acpi(icn8505, dev); + if (error) + return error; + + error = icn8505_upload_fw(icn8505); + if (error) + return error; + + error = icn8505_read_data(icn8505, ICN8505_REG_CONFIGDATA, + resolution, sizeof(resolution)); + if (error) { + dev_err(dev, "Error reading resolution: %d\n", error); + return error; + } + + input_set_abs_params(input, ABS_MT_POSITION_X, 0, + le16_to_cpu(resolution[0]) - 1, 0, 0); + input_set_abs_params(input, ABS_MT_POSITION_Y, 0, + le16_to_cpu(resolution[1]) - 1, 0, 0); + + touchscreen_parse_properties(input, true, &icn8505->prop); + if (!input_abs_get_max(input, ABS_MT_POSITION_X) || + !input_abs_get_max(input, ABS_MT_POSITION_Y)) { + dev_err(dev, "Error touchscreen-size-x and/or -y missing\n"); + return -EINVAL; + } + + error = input_mt_init_slots(input, ICN8505_MAX_TOUCHES, + INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED); + if (error) + return error; + + error = devm_request_threaded_irq(dev, client->irq, NULL, icn8505_irq, + IRQF_ONESHOT, client->name, icn8505); + if (error) { + dev_err(dev, "Error requesting irq: %d\n", error); + return error; + } + + error = input_register_device(input); + if (error) + return error; + + i2c_set_clientdata(client, icn8505); + return 0; +} + +static int __maybe_unused icn8505_suspend(struct device *dev) +{ + struct icn8505_data *icn8505 = i2c_get_clientdata(to_i2c_client(dev)); + + disable_irq(icn8505->client->irq); + icn8505_write_reg(icn8505, ICN8505_REG_POWER, ICN8505_POWER_HIBERNATE); + return 0; +} + +static int __maybe_unused icn8505_resume(struct device *dev) +{ + struct icn8505_data *icn8505 = i2c_get_clientdata(to_i2c_client(dev)); + int error; + + error = icn8505_upload_fw(icn8505); + if (error) + return error; + + enable_irq(icn8505->client->irq); + return 0; +} + +static SIMPLE_DEV_PM_OPS(icn8505_pm_ops, icn8505_suspend, icn8505_resume); + +static const struct acpi_device_id icn8505_acpi_match[] = { + { "CHPN0001" }, + { } +}; +MODULE_DEVICE_TABLE(acpi, icn8505_acpi_match); + +static struct i2c_driver icn8505_driver = { + .driver = { + .name = "chipone_icn8505", + .pm = &icn8505_pm_ops, + .acpi_match_table = icn8505_acpi_match, + }, + .probe_new = icn8505_probe, +}; + +module_i2c_driver(icn8505_driver); + +MODULE_DESCRIPTION("ChipOne icn8505 I2C Touchscreen Driver"); +MODULE_AUTHOR("Hans de Goede <hdegoede@redhat.com>"); +MODULE_LICENSE("GPL");
The ChipOne icn8505 is an i2c capacitive touchscreen controller typically used in cheap x86 tablets, this commit adds a driver for it. Note the icn8505 is somewhat similar to the icn8318 and I started with modifying that driver to support both, but in the end the differences were too large and I decided to write a new driver instead. Cc: Antonio Davide Trogu <trogu.davide@gmail.com> Signed-off-by: Hans de Goede <hdegoede@redhat.com> --- Changes in v2: -Make icn8505_write/read_xfer return 0 rather then a positive value on success -Rename the "ret" local variable to "error" where applicable -Move printing of attempt %d/%d on firmware upload issues from icn8505_try_fw_upload() to its caller, icn8505_upload_fw() -Drop inline marking from icn8505_touch_active() -Drop duplicate input->dev.parent = dev; -Use __maybe_unused for suspend/resume() instead of #ifdef CONFIG_PM_SLEEP --- MAINTAINERS | 6 + drivers/input/touchscreen/Kconfig | 11 + drivers/input/touchscreen/Makefile | 1 + drivers/input/touchscreen/chipone_icn8505.c | 516 ++++++++++++++++++++ 4 files changed, 534 insertions(+) create mode 100644 drivers/input/touchscreen/chipone_icn8505.c