[PATCH v6] Input: add a driver for wdt87xx touchscreen controller

From: HungNien Chen
Date: Sat Jun 13 2015 - 02:39:10 EST


Below is the modification this version:
1. remove the union structure declaration and use the buffer and offset to manupulate data
2. remove redundant comments and debug messages
3. remove useless include files
4. modify some inproper messages
5. have a reliable value for the checksum delay
6. modify some data structure declaration and shorten names of some variables
7. modify functions of request_irq and create_input_device to be more reasonable

Signed-off-by: HungNien Chen <hn.chen@xxxxxxxxxxxxxxx>
---
drivers/input/touchscreen/Kconfig | 12 +
drivers/input/touchscreen/Makefile | 1 +
drivers/input/touchscreen/wdt87xx_i2c.c | 1328 +++++++++++++++++++++++++++++++
3 files changed, 1341 insertions(+)
create mode 100644 drivers/input/touchscreen/wdt87xx_i2c.c

diff --git a/drivers/input/touchscreen/Kconfig b/drivers/input/touchscreen/Kconfig
index 80f6386..0c1a6cc 100644
--- a/drivers/input/touchscreen/Kconfig
+++ b/drivers/input/touchscreen/Kconfig
@@ -658,6 +658,18 @@ config TOUCHSCREEN_PIXCIR
To compile this driver as a module, choose M here: the
module will be called pixcir_i2c_ts.

+config TOUCHSCREEN_WDT87XX_I2C
+ tristate "Weida HiTech I2C touchscreen"
+ depends on I2C
+ help
+ Say Y here if you have an Weida WDT87XX I2C touchscreen
+ connected to your system.
+
+ If unsure, say N.
+
+ To compile this driver as a module, choose M here: the
+ module will be called wdt87xx_i2c.
+
config TOUCHSCREEN_WM831X
tristate "Support for WM831x touchscreen controllers"
depends on MFD_WM831X
diff --git a/drivers/input/touchscreen/Makefile b/drivers/input/touchscreen/Makefile
index 44deea7..fa3d33b 100644
--- a/drivers/input/touchscreen/Makefile
+++ b/drivers/input/touchscreen/Makefile
@@ -72,6 +72,7 @@ obj-$(CONFIG_TOUCHSCREEN_TSC2007) += tsc2007.o
obj-$(CONFIG_TOUCHSCREEN_UCB1400) += ucb1400_ts.o
obj-$(CONFIG_TOUCHSCREEN_WACOM_W8001) += wacom_w8001.o
obj-$(CONFIG_TOUCHSCREEN_WACOM_I2C) += wacom_i2c.o
+obj-$(CONFIG_TOUCHSCREEN_WDT87XX_I2C) += wdt87xx_i2c.o
obj-$(CONFIG_TOUCHSCREEN_WM831X) += wm831x-ts.o
obj-$(CONFIG_TOUCHSCREEN_WM97XX) += wm97xx-ts.o
wm97xx-ts-$(CONFIG_TOUCHSCREEN_WM9705) += wm9705.o
diff --git a/drivers/input/touchscreen/wdt87xx_i2c.c b/drivers/input/touchscreen/wdt87xx_i2c.c
new file mode 100644
index 0000000..c8ec8b9
--- /dev/null
+++ b/drivers/input/touchscreen/wdt87xx_i2c.c
@@ -0,0 +1,1328 @@
+/*
+ * Weida HiTech WDT87xx TouchScreen I2C driver
+ *
+ * Copyright (c) 2015 Weida Hi-Tech Co., Ltd.
+ * HN Chen <hn.chen@xxxxxxxxxxxxxxx>
+ *
+ * This software is licensed under the terms of the GNU General Public
+ * License, as published by the Free Software Foundation, and
+ * may be copied, distributed, and modified under those terms.
+ *
+ */
+
+#include <linux/i2c.h>
+#include <linux/input.h>
+#include <linux/interrupt.h>
+#include <linux/delay.h>
+#include <linux/irq.h>
+#include <linux/io.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/firmware.h>
+#include <linux/input/mt.h>
+#include <linux/acpi.h>
+#include <asm/unaligned.h>
+
+#define WDT87XX_NAME "wdt87xx_i2c"
+#define WDT87XX_DRV_VER "0.9.5"
+#define WDT87XX_FW_NAME "wdt87xx_fw.bin"
+
+#define WDT87XX_FW 1
+#define WDT87XX_CFG 2
+
+#define MODE_ACTIVE 0x01
+#define MODE_READY 0x02
+#define MODE_IDLE 0x03
+#define MODE_SLEEP 0x04
+#define MODE_STOP 0xFF
+
+#define WDT_PKT_V0 0
+#define WDT_PKT_V1 1
+
+#define WDT_MAX_FINGER 10
+#define WDT_RAW_BUF_COUNT 54
+#define WDT_V1_RAW_BUF_COUNT 74
+#define WDT_FIRMWARE_ID 0xa9e368f5
+
+#define PG_SIZE 0x1000
+#define MAX_RETRIES 3
+
+#define MAX_UNIT_AXIS 0x7FFF
+
+#define PKT_READ_SIZE 72
+#define PKT_WRITE_SIZE 80
+
+/* the finger definition of the report event */
+#define FINGER_EV_OFFSET_ID 0
+#define FINGER_EV_OFFSET_X 1
+#define FINGER_EV_OFFSET_Y 3
+#define FINGER_EV_SIZE 5
+
+#define FINGER_EV_V1_OFFSET_ID 0
+#define FINGER_EV_V1_OFFSET_W 1
+#define FINGER_EV_V1_OFFSET_H 2
+#define FINGER_EV_V1_OFFSET_X 3
+#define FINGER_EV_V1_OFFSET_Y 5
+#define FINGER_EV_V1_SIZE 7
+
+/* the definition of a report packet */
+#define TOUCH_PK_OFFSET_REPORT_ID 0
+#define TOUCH_PK_OFFSET_EVENT 1
+#define TOUCH_PK_OFFSET_SCAN_TIME 51
+#define TOUCH_PK_OFFSET_FNGR_NUM 53
+
+#define TOUCH_PK_V1_OFFSET_REPORT_ID 0
+#define TOUCH_PK_V1_OFFSET_EVENT 1
+#define TOUCH_PK_V1_OFFSET_SCAN_TIME 71
+#define TOUCH_PK_V1_OFFSET_FNGR_NUM 73
+
+/* the definition of the controller parameters */
+#define CTL_PARAM_OFFSET_FW_ID 0
+#define CTL_PARAM_OFFSET_PLAT_ID 2
+#define CTL_PARAM_OFFSET_XMLS_ID1 4
+#define CTL_PARAM_OFFSET_XMLS_ID2 6
+#define CTL_PARAM_OFFSET_PHY_CH_X 8
+#define CTL_PARAM_OFFSET_PHY_CH_Y 10
+#define CTL_PARAM_OFFSET_PHY_X0 12
+#define CTL_PARAM_OFFSET_PHY_X1 14
+#define CTL_PARAM_OFFSET_PHY_Y0 16
+#define CTL_PARAM_OFFSET_PHY_Y1 18
+#define CTL_PARAM_OFFSET_PHY_W 22
+#define CTL_PARAM_OFFSET_PHY_H 24
+
+/* communication commands */
+#define PACKET_SIZE 56
+#define VND_REQ_READ 0x06
+#define VND_READ_DATA 0x07
+#define VND_REQ_WRITE 0x08
+
+#define VND_CMD_START 0x00
+#define VND_CMD_STOP 0x01
+#define VND_CMD_RESET 0x09
+
+#define VND_CMD_ERASE 0x1A
+
+#define VND_GET_CHECKSUM 0x66
+
+#define VND_SET_DATA 0x83
+#define VND_SET_COMMAND_DATA 0x84
+#define VND_SET_CHECKSUM_CALC 0x86
+#define VND_SET_CHECKSUM_LENGTH 0x87
+
+#define VND_CMD_SFLCK 0xFC
+#define VND_CMD_SFUNL 0xFD
+
+#define CMD_SFLCK_KEY 0xC39B
+#define CMD_SFUNL_KEY 0x95DA
+
+#define STRIDX_PLATFORM_ID 0x80
+#define STRIDX_PARAMETERS 0x81
+
+#define CMD_BUF_SIZE 8
+#define PKT_BUF_SIZE 64
+
+/* the definition of the command packet */
+#define CMD_REPORT_ID_OFFSET 0x0
+#define CMD_TYPE_OFFSET 0x1
+#define CMD_INDEX_OFFSET 0x2
+#define CMD_KEY_OFFSET 0x3
+#define CMD_LENGTH_OFFSET 0x4
+#define CMD_DATA_OFFSET 0x8
+
+/* the definition of firmware chunk tags */
+#define FOURCC_ID_RIFF 0x46464952
+#define FOURCC_ID_WHIF 0x46494857
+#define FOURCC_ID_FRMT 0x544D5246
+#define FOURCC_ID_FRWR 0x52575246
+#define FOURCC_ID_CNFG 0x47464E43
+
+#define CHUNK_ID_FRMT FOURCC_ID_FRMT
+#define CHUNK_ID_FRWR FOURCC_ID_FRWR
+#define CHUNK_ID_CNFG FOURCC_ID_CNFG
+
+struct sys_param {
+ u16 fw_id;
+ u16 plat_id;
+ u16 xmls_id1;
+ u16 xmls_id2;
+ u16 phy_ch_x;
+ u16 phy_ch_y;
+ u16 phy_w;
+ u16 phy_h;
+};
+
+/* the definition for this driver needed */
+struct wdt_ts_data {
+ struct i2c_client *client;
+ struct input_dev *input_dev;
+/* to protect the operation in sysfs */
+ struct mutex sysfs_mutex;
+ struct sys_param param;
+ u8 phys[32];
+ u32 packet_type;
+ u32 max_x;
+ u32 max_y;
+};
+
+/* the definition of firmware data structure */
+struct chunk_info {
+ u32 target_start_addr;
+ u32 length;
+ u32 source_start_addr;
+ u32 version_number;
+ u32 attribute;
+ u32 temp;
+};
+
+struct chunk_data {
+ u32 ck_id;
+ u32 ck_size;
+ struct chunk_info chunk_info;
+ u8 *data;
+};
+
+struct format_chunk {
+ u32 ck_id;
+ u32 ck_size;
+ u32 number_chunk;
+ u32 enable_flag;
+ u32 checksum;
+ u32 temp1;
+ u32 temp2;
+};
+
+struct chunk_info_ex {
+ struct chunk_info chunk_info;
+ u8 *data;
+ u32 length;
+};
+
+static int wdt87xx_i2c_txrxdata(struct i2c_client *client, char *txdata,
+ int txlen, char *rxdata, int rxlen);
+static int wdt87xx_i2c_rxdata(struct i2c_client *client, char *rxdata,
+ int length);
+static int wdt87xx_i2c_txdata(struct i2c_client *client, char *txdata,
+ int length);
+static int wdt87xx_set_feature(struct i2c_client *client, u8 *buf,
+ u32 buf_size);
+static int wdt87xx_get_feature(struct i2c_client *client, u8 *buf,
+ u32 buf_size);
+static int wdt87xx_get_string(struct i2c_client *client, u8 str_idx,
+ u8 *buf, u32 buf_size);
+
+static int get_chunk_info(const struct firmware *fw, u32 chunk_four_cc,
+ struct chunk_info_ex *fw_chunk_info,
+ struct format_chunk *wif_format_chunk)
+{
+ const char *data;
+ u32 data_len;
+ bool is_found = 0;
+ u32 start_pos;
+ struct chunk_data chunk;
+ u32 ck_id, ck_size;
+
+ data = fw->data;
+ data_len = fw->size;
+
+ /* check if the chunk is existed */
+ start_pos = 12 + sizeof(struct format_chunk);
+
+ while (start_pos < data_len && !is_found) {
+ ck_id = get_unaligned_le32(&data[start_pos]);
+ ck_size = get_unaligned_le32(&data[start_pos + 4]);
+
+ /* the chunk is found */
+ if (ck_id == chunk_four_cc) {
+ chunk.ck_id = ck_id;
+ chunk.ck_size = ck_size;
+
+ chunk.data = (u8 *)&data[start_pos + 8
+ + sizeof(struct chunk_info)];
+ chunk.chunk_info.target_start_addr =
+ get_unaligned_le32(&data[start_pos + 8]);
+ chunk.chunk_info.length =
+ get_unaligned_le32(&data[start_pos + 12]);
+ chunk.chunk_info.source_start_addr =
+ get_unaligned_le32(&data[start_pos + 16]);
+ chunk.chunk_info.version_number =
+ get_unaligned_le32(&data[start_pos + 20]);
+ chunk.chunk_info.attribute =
+ get_unaligned_le32(&data[start_pos + 24]);
+ chunk.chunk_info.temp =
+ get_unaligned_le32(&data[start_pos + 28]);
+
+ memcpy(&fw_chunk_info->chunk_info, &chunk.chunk_info,
+ sizeof(struct chunk_info));
+ fw_chunk_info->length = chunk.chunk_info.length;
+ fw_chunk_info->data = chunk.data;
+
+ is_found = 1;
+ } else {
+ start_pos = start_pos + ck_size + 8;
+ }
+ }
+
+ if (is_found)
+ return 0;
+
+ return -ENODATA;
+}
+
+static int wdt87xx_get_sysparam(struct i2c_client *client)
+{
+ struct wdt_ts_data *wdt_dev = i2c_get_clientdata(client);
+ struct sys_param *ctr_param = &wdt_dev->param;
+ u8 buffer[PKT_READ_SIZE];
+ int err;
+
+ err = wdt87xx_get_string(client, STRIDX_PARAMETERS, buffer, 32);
+ if (err) {
+ dev_err(&client->dev, "get parameters failed\n");
+ return err;
+ }
+
+ ctr_param->xmls_id1 =
+ get_unaligned_le16(buffer + CTL_PARAM_OFFSET_XMLS_ID1);
+ ctr_param->xmls_id2 =
+ get_unaligned_le16(buffer + CTL_PARAM_OFFSET_XMLS_ID2);
+ ctr_param->phy_ch_x =
+ get_unaligned_le16(buffer + CTL_PARAM_OFFSET_PHY_CH_X);
+ ctr_param->phy_ch_y =
+ get_unaligned_le16(buffer + CTL_PARAM_OFFSET_PHY_CH_Y);
+ ctr_param->phy_w =
+ (get_unaligned_le16(buffer + CTL_PARAM_OFFSET_PHY_W) / 10);
+ ctr_param->phy_h =
+ (get_unaligned_le16(buffer + CTL_PARAM_OFFSET_PHY_H) / 10);
+
+ err = wdt87xx_get_string(client, STRIDX_PLATFORM_ID, buffer, 8);
+ if (err) {
+ dev_err(&client->dev, "get platform id failed\n");
+ return err;
+ }
+
+ ctr_param->plat_id = buffer[1];
+
+ buffer[0] = 0xf2;
+ err = wdt87xx_get_feature(client, buffer, 16);
+ if (err) {
+ dev_err(&client->dev, "get firmware id failed\n");
+ return err;
+ }
+
+ if (buffer[0] != 0xf2) {
+ dev_err(&client->dev, "wrong id of this packet: (0x%x)\n",
+ buffer[0]);
+ return -EINVAL;
+ }
+
+ ctr_param->fw_id = get_unaligned_le16(&buffer[1]);
+
+ if ((ctr_param->fw_id & 0xFFF) > 0x335)
+ wdt_dev->packet_type = WDT_PKT_V1;
+ else
+ wdt_dev->packet_type = WDT_PKT_V0;
+
+ dev_info(&client->dev,
+ "fw_id: 0x%x, plat_id: 0x%x\nxml_id1: %4x, xml_id2: %4x\n",
+ ctr_param->fw_id, ctr_param->plat_id,
+ ctr_param->xmls_id1, ctr_param->xmls_id2);
+
+ return 0;
+}
+
+static int process_fw_data(struct i2c_client *client, const struct firmware *fw,
+ struct format_chunk *wif_format_chunk)
+{
+ struct wdt_ts_data *wdt_dev = i2c_get_clientdata(client);
+ struct chunk_info_ex fw_chunk_info;
+ const u8 *data_buf;
+ int err;
+ u32 length;
+ u8 fw_id;
+ u8 chip_id;
+ u32 data1, data2;
+
+ data_buf = fw->data;
+ length = fw->size;
+
+ data1 = get_unaligned_le32(data_buf);
+ data2 = get_unaligned_le32(data_buf + 8);
+ if (data1 != FOURCC_ID_RIFF || data2 != FOURCC_ID_WHIF) {
+ dev_err(&client->dev, "check fw tag failed\n");
+ return -EINVAL;
+ }
+
+ /* the length should be equal */
+ data1 = get_unaligned_le32(data_buf + 4);
+ if (data1 != length) {
+ dev_err(&client->dev, "check fw length failed\n");
+ return -EINVAL;
+ }
+
+ wif_format_chunk->ck_id = get_unaligned_le32(data_buf + 12);
+ wif_format_chunk->ck_size = get_unaligned_le32(data_buf + 16);
+ wif_format_chunk->number_chunk = get_unaligned_le32(data_buf + 20);
+ wif_format_chunk->enable_flag = get_unaligned_le32(data_buf + 24);
+ wif_format_chunk->checksum = get_unaligned_le32(data_buf + 28);
+ wif_format_chunk->temp1 = get_unaligned_le32(data_buf + 32);
+ wif_format_chunk->temp2 = get_unaligned_le32(data_buf + 36);
+
+ dev_info(&client->dev, "version check\n");
+
+ /* get the version number from the firmware */
+ err = get_chunk_info(fw, CHUNK_ID_FRWR, &fw_chunk_info,
+ wif_format_chunk);
+ if (err) {
+ dev_err(&client->dev, "extract fw failed\n");
+ return -EBADR;
+ }
+
+ fw_id = ((fw_chunk_info.chunk_info.version_number >> 12) & 0xF);
+ chip_id = (((wdt_dev->param.fw_id) >> 12) & 0xF);
+
+ if (fw_id != chip_id) {
+ dev_err(&client->dev, "fw is not match: fw(%d), chip(%d)\n",
+ fw_id, chip_id);
+ return -ENODEV;
+ }
+
+ return 0;
+}
+
+/* functions for the sysfs implementation */
+static int wdt87xx_check_firmware(struct chunk_info_ex *fw_chunk_info,
+ int ck_id)
+{
+ if (ck_id == CHUNK_ID_FRWR) {
+ u32 fw_id;
+
+ fw_id = get_unaligned_le32(fw_chunk_info->data);
+ if (fw_id == WDT_FIRMWARE_ID)
+ return 0;
+ else
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int wdt87xx_set_feature(struct i2c_client *client, u8 *buf,
+ u32 buf_size)
+{
+ int err;
+ int data_len = 0;
+ /* for set/get packets used */
+ u8 xfer_buffer[PKT_WRITE_SIZE];
+
+ /* set feature command packet */
+ xfer_buffer[data_len++] = 0x22;
+ xfer_buffer[data_len++] = 0x00;
+ if (buf[CMD_REPORT_ID_OFFSET] > 0xF) {
+ xfer_buffer[data_len++] = 0x30;
+ xfer_buffer[data_len++] = 0x03;
+ xfer_buffer[data_len++] = buf[CMD_REPORT_ID_OFFSET];
+ } else {
+ xfer_buffer[data_len++] = 0x30 | buf[CMD_REPORT_ID_OFFSET];
+ xfer_buffer[data_len++] = 0x03;
+ }
+ xfer_buffer[data_len++] = 0x23;
+ xfer_buffer[data_len++] = 0x00;
+ xfer_buffer[data_len++] = (buf_size & 0xFF);
+ xfer_buffer[data_len++] = ((buf_size & 0xFF00) >> 8);
+
+ memcpy(&xfer_buffer[data_len], buf, buf_size);
+
+ err = wdt87xx_i2c_txdata(client, xfer_buffer, data_len + buf_size);
+
+ if (err < 0) {
+ dev_err(&client->dev, "set feature failed\n");
+ return err;
+ }
+
+ mdelay(2);
+
+ return 0;
+}
+
+static int wdt87xx_get_feature(struct i2c_client *client, u8 *buf,
+ u32 buf_size)
+{
+ int err;
+ u8 tx_buffer[8];
+ u8 xfer_buffer[PKT_WRITE_SIZE];
+ int data_len = 0;
+ u32 xfer_length = 0;
+
+ /* get feature command packet */
+ tx_buffer[data_len++] = 0x22;
+ tx_buffer[data_len++] = 0x00;
+ if (buf[CMD_REPORT_ID_OFFSET] > 0xF) {
+ tx_buffer[data_len++] = 0x30;
+ tx_buffer[data_len++] = 0x02;
+ tx_buffer[data_len++] = buf[CMD_REPORT_ID_OFFSET];
+ } else {
+ tx_buffer[data_len++] = 0x30 | buf[CMD_REPORT_ID_OFFSET];
+ tx_buffer[data_len++] = 0x02;
+ }
+ tx_buffer[data_len++] = 0x23;
+ tx_buffer[data_len++] = 0x00;
+
+ err = wdt87xx_i2c_txrxdata(client, tx_buffer, data_len, xfer_buffer,
+ buf_size + 2);
+
+ if (err < 0) {
+ dev_err(&client->dev, "get feature failed\n");
+ return err;
+ }
+
+ /* check size and copy the return data */
+ xfer_length = get_unaligned_le16(xfer_buffer);
+
+ if (buf_size < xfer_length)
+ xfer_length = buf_size;
+
+ memcpy(buf, &xfer_buffer[2], xfer_length);
+
+ mdelay(2);
+
+ return 0;
+}
+
+static int wdt87xx_get_string(struct i2c_client *client, u8 str_idx,
+ u8 *buf, u32 buf_size)
+{
+ int err;
+ u8 tx_buffer[8] = { 0x22, 0x00, 0x13, 0x0E,
+ 0x00, 0x23, 0x00, 0x00 };
+ u8 xfer_buffer[PKT_WRITE_SIZE];
+ u32 xfer_length;
+
+ tx_buffer[4] = str_idx;
+
+ err = wdt87xx_i2c_txrxdata(client, tx_buffer, 7, xfer_buffer,
+ buf_size + 2);
+
+ if (err < 0) {
+ dev_err(&client->dev, "get string failed\n");
+ return err;
+ }
+
+ if (xfer_buffer[1] != 0x03) {
+ dev_err(&client->dev, "wrong packet id: (%d)\n",
+ xfer_buffer[1]);
+ return -EINVAL;
+ }
+
+ xfer_length = xfer_buffer[0];
+
+ if (buf_size < xfer_length)
+ xfer_length = buf_size;
+
+ memcpy(buf, &xfer_buffer[2], xfer_length);
+
+ mdelay(2);
+
+ return 0;
+}
+
+static int wdt87xx_send_command(struct i2c_client *client, int cmd, int value)
+{
+ u8 cmd_buf[CMD_BUF_SIZE];
+
+ /* set the command packet */
+ cmd_buf[CMD_REPORT_ID_OFFSET] = VND_REQ_WRITE;
+ cmd_buf[CMD_TYPE_OFFSET] = VND_SET_COMMAND_DATA;
+ put_unaligned_le16((u16)cmd, &cmd_buf[CMD_INDEX_OFFSET]);
+
+ switch (cmd) {
+ case VND_CMD_START:
+ case VND_CMD_STOP:
+ case VND_CMD_RESET:
+ /* mode selector */
+ put_unaligned_le32((value & 0xFF), &cmd_buf[CMD_LENGTH_OFFSET]);
+ break;
+ case VND_CMD_SFLCK:
+ put_unaligned_le16(CMD_SFLCK_KEY, &cmd_buf[CMD_KEY_OFFSET]);
+ break;
+ case VND_CMD_SFUNL:
+ put_unaligned_le16(CMD_SFUNL_KEY, &cmd_buf[CMD_KEY_OFFSET]);
+ break;
+ case VND_CMD_ERASE:
+ case VND_SET_CHECKSUM_CALC:
+ case VND_SET_CHECKSUM_LENGTH:
+ put_unaligned_le32(value, &cmd_buf[CMD_KEY_OFFSET]);
+ break;
+ default:
+ cmd_buf[CMD_REPORT_ID_OFFSET] = 0;
+ dev_err(&client->dev, "Invalid command: (%d)", cmd);
+ return -EINVAL;
+ }
+
+ return wdt87xx_set_feature(client, cmd_buf, sizeof(cmd_buf));
+}
+
+static int wdt87xx_write_data(struct i2c_client *client, const char *data,
+ u32 address, int length)
+{
+ u32 addr_start, data_len;
+ u16 packet_size;
+ int count = 0;
+ int err;
+ const char *source_data = 0;
+ u8 pkt_buf[PKT_BUF_SIZE];
+
+ source_data = data;
+ data_len = length;
+ addr_start = address;
+
+ /* address and length should be 4 bytes aligned */
+ if ((addr_start & 0x3) != 0 || (data_len & 0x3) != 0) {
+ dev_err(&client->dev, "addr & len must be 4 bytes aligned %x, %x\n",
+ addr_start, data_len);
+ return -EFAULT;
+ }
+
+ packet_size = PACKET_SIZE;
+
+ pkt_buf[CMD_REPORT_ID_OFFSET] = VND_REQ_WRITE;
+ pkt_buf[CMD_TYPE_OFFSET] = VND_SET_DATA;
+
+ while (data_len) {
+ if (data_len < PACKET_SIZE)
+ packet_size = data_len;
+
+ put_unaligned_le16(packet_size, &pkt_buf[CMD_INDEX_OFFSET]);
+ put_unaligned_le32(addr_start, &pkt_buf[CMD_LENGTH_OFFSET]);
+
+ memcpy(&pkt_buf[CMD_DATA_OFFSET], source_data, packet_size);
+
+ err = wdt87xx_set_feature(client, pkt_buf, sizeof(pkt_buf));
+
+ if (err)
+ break;
+
+ data_len = data_len - packet_size;
+ source_data = source_data + packet_size;
+ addr_start = addr_start + packet_size;
+
+ count++;
+ mdelay(4);
+
+ if ((count % 32) == 0) {
+ count = 0;
+ msleep(20);
+ }
+ }
+
+ return err;
+}
+
+static u16 misr(u16 cur_value, u8 new_value)
+{
+ u32 a, b;
+ u32 bit0;
+ u32 y;
+
+ a = cur_value;
+ b = new_value;
+ bit0 = a ^ (b & 1);
+ bit0 ^= a >> 1;
+ bit0 ^= a >> 2;
+ bit0 ^= a >> 4;
+ bit0 ^= a >> 5;
+ bit0 ^= a >> 7;
+ bit0 ^= a >> 11;
+ bit0 ^= a >> 15;
+ y = (a << 1) ^ b;
+ y = (y & ~1) | (bit0 & 1);
+
+ return (u16)y;
+}
+
+static int wdt87xx_get_checksum(struct i2c_client *client, u32 *checksum,
+ u32 address, int length)
+{
+ int err;
+ int time_delay;
+ u8 pkt_buf[PKT_BUF_SIZE];
+ u8 cmd_buf[CMD_BUF_SIZE];
+
+ err = wdt87xx_send_command(client, VND_SET_CHECKSUM_LENGTH, length);
+ if (err) {
+ dev_err(&client->dev, "set checksum length failed\n");
+ return err;
+ }
+
+ err = wdt87xx_send_command(client, VND_SET_CHECKSUM_CALC, address);
+ if (err) {
+ dev_err(&client->dev, "calc checksum failed\n");
+ return err;
+ }
+
+ time_delay = (length + 1023) / 1024;
+ /* to wait for the operation to complete */
+ msleep(time_delay * 30);
+
+ memset(cmd_buf, 0, sizeof(cmd_buf));
+ cmd_buf[CMD_REPORT_ID_OFFSET] = VND_REQ_READ;
+ cmd_buf[CMD_TYPE_OFFSET] = VND_GET_CHECKSUM;
+ err = wdt87xx_set_feature(client, cmd_buf, sizeof(cmd_buf));
+ if (err) {
+ dev_err(&client->dev, "checksum set read failed\n");
+ return err;
+ }
+
+ memset(pkt_buf, 0, sizeof(pkt_buf));
+ pkt_buf[CMD_REPORT_ID_OFFSET] = VND_READ_DATA;
+ err = wdt87xx_get_feature(client, pkt_buf, sizeof(pkt_buf));
+ if (err) {
+ dev_err(&client->dev, "read checksum failed\n");
+ return err;
+ }
+
+ *checksum = get_unaligned_le16(&pkt_buf[CMD_DATA_OFFSET]);
+
+ return err;
+}
+
+static u16 fw_checksum(const u8 *data, u32 length)
+{
+ u32 i;
+ u16 checksum = 0;
+
+ for (i = 0; i < length; i++)
+ checksum = misr(checksum, data[i]);
+
+ return checksum;
+}
+
+static int wdt87xx_write_firmware(
+ struct i2c_client *client,
+ struct chunk_info_ex *fw_chunk_info, int type)
+{
+ int err;
+ int err1;
+ int size;
+ int start_addr;
+ int page_size;
+ int retry_count = 0;
+ int is_equal = 0;
+ int max_retries;
+ u32 calc_checksum = 0;
+ u32 read_checksum = 0;
+ const char *data;
+
+ dev_info(&client->dev, "start 4k page program\n");
+
+ err = wdt87xx_send_command(client, VND_CMD_STOP, MODE_STOP);
+ if (err) {
+ dev_err(&client->dev, "stop report mode failed\n");
+ return err;
+ }
+
+ err = wdt87xx_send_command(client, VND_CMD_SFUNL, 0);
+ if (err) {
+ dev_err(&client->dev, "unlock failed\n");
+ goto write_fail;
+ }
+
+ mdelay(10);
+
+ start_addr = fw_chunk_info->chunk_info.target_start_addr;
+ size = fw_chunk_info->chunk_info.length;
+ data = fw_chunk_info->data;
+
+ max_retries = MAX_RETRIES;
+
+ dev_info(&client->dev, "%x, %x, %d\n", start_addr, size, max_retries);
+
+ while (size && !err) {
+ is_equal = 0;
+ if (size > PG_SIZE) {
+ page_size = PG_SIZE;
+ size = size - PG_SIZE;
+ } else {
+ page_size = size;
+ size = 0;
+ }
+
+ for (retry_count = 0; retry_count < max_retries && !is_equal;
+ retry_count++) {
+ err = wdt87xx_send_command(client, VND_CMD_ERASE,
+ start_addr);
+ if (err) {
+ dev_err(&client->dev, "erase failed\n");
+ break;
+ }
+
+ msleep(50);
+
+ err = wdt87xx_write_data(client, data, start_addr,
+ page_size);
+ if (err) {
+ dev_err(&client->dev, "write failed\n");
+ break;
+ }
+
+ read_checksum = 0;
+ err = wdt87xx_get_checksum(client, &read_checksum,
+ start_addr, page_size);
+ if (err)
+ break;
+
+ calc_checksum = fw_checksum(data, page_size);
+
+ if (read_checksum == calc_checksum)
+ is_equal = 1;
+ else
+ dev_err(&client->dev,
+ "csum fail: (%d), (%d), (%d)\n",
+ retry_count,
+ read_checksum, calc_checksum);
+ }
+
+ if (retry_count == MAX_RETRIES) {
+ dev_err(&client->dev, "page write failed\n");
+ err = -EIO;
+ }
+
+ start_addr = start_addr + page_size;
+ data = data + page_size;
+ dev_info(&client->dev, "%x, %x\n", start_addr, size);
+ }
+write_fail:
+ err1 = wdt87xx_send_command(client, VND_CMD_SFLCK, 0);
+ if (err1)
+ dev_err(&client->dev, "lock failed\n");
+
+ mdelay(10);
+
+ err1 = wdt87xx_send_command(client, VND_CMD_START, 0);
+ if (err1)
+ dev_err(&client->dev, "start to report failed\n");
+
+ dev_info(&client->dev, "stop 4k page program : ");
+
+ if (err || err1)
+ dev_info(&client->dev, "fail\n");
+ else
+ dev_info(&client->dev, "pass\n");
+
+ if (err1)
+ return err1;
+
+ return err;
+}
+
+static int wdt87xx_sw_reset(struct i2c_client *client)
+{
+ int err;
+
+ dev_info(&client->dev, "reset device now\n");
+
+ err = wdt87xx_send_command(client, VND_CMD_RESET, 0);
+ if (err) {
+ dev_err(&client->dev, "reset failed\n");
+ return err;
+ }
+
+ /* wait the device to be ready */
+ msleep(200);
+
+ return 0;
+}
+
+static int wdt87xx_load_chunk(
+ struct i2c_client *client, const struct firmware *fw,
+ struct format_chunk *wif_format_chunk, u32 ck_id)
+{
+ int err;
+ struct chunk_info_ex fw_chunk_info;
+
+ err = get_chunk_info(fw, ck_id, &fw_chunk_info, wif_format_chunk);
+ if (err) {
+ dev_err(&client->dev, "can not find the chunk\n");
+ goto failed;
+ }
+
+ /* check the bin file */
+ err = wdt87xx_check_firmware(&fw_chunk_info, ck_id);
+ if (err) {
+ dev_err(&client->dev, "check bin id: (%d)\n", ck_id);
+ goto failed;
+ }
+
+ err = wdt87xx_write_firmware(client, &fw_chunk_info, ck_id);
+ if (err)
+ dev_err(&client->dev, "write bin failed\n");
+
+failed:
+ return err;
+}
+
+static int wdt87xx_load_fw(struct device *dev, const char *fn, u8 type)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ const struct firmware *fw = 0;
+ int err;
+
+ struct format_chunk wif_format_chunk;
+
+ err = request_firmware(&fw, fn, dev);
+ if (err) {
+ dev_err(&client->dev, "unable to open firmware %s: (%d)\n",
+ fn, err);
+ return err;
+ }
+
+ disable_irq(client->irq);
+
+ err = process_fw_data(client, fw, &wif_format_chunk);
+ if (err) {
+ dev_err(&client->dev, "bad fw file\n");
+ goto release_firmware;
+ }
+
+ if (type & WDT87XX_FW) {
+ err = wdt87xx_load_chunk(client, fw, &wif_format_chunk,
+ CHUNK_ID_FRWR);
+ if (err) {
+ dev_err(&client->dev, "load fw chunk failed\n");
+ goto release_firmware;
+ }
+ }
+
+ if (type & WDT87XX_CFG) {
+ err = wdt87xx_load_chunk(client, fw, &wif_format_chunk,
+ CHUNK_ID_CNFG);
+ if (err) {
+ dev_err(&client->dev, "load cfg chunk failed\n");
+ goto release_firmware;
+ }
+ }
+
+ err = wdt87xx_sw_reset(client);
+ if (err)
+ dev_err(&client->dev, "soft reset failed\n");
+
+ /* refresh the parameters */
+ wdt87xx_get_sysparam(client);
+release_firmware:
+ enable_irq(client->irq);
+ mdelay(10);
+
+ release_firmware(fw);
+ return err;
+}
+
+static ssize_t update_fw_store(
+ struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct wdt_ts_data *wdt_dev = i2c_get_clientdata(client);
+ int err;
+ u8 option = 0;
+
+ if (count <= 0)
+ return -EINVAL;
+
+ err = kstrtou8(buf, 0, &option);
+ if (err)
+ return err;
+
+ dev_info(dev, "update option (%d)\n", option);
+ if (option < 1 || option > 3) {
+ dev_err(&client->dev, "option is not supported\n");
+ return -1;
+ }
+
+ err = mutex_lock_interruptible(&wdt_dev->sysfs_mutex);
+ if (err)
+ return err;
+
+ err = wdt87xx_load_fw(dev, WDT87XX_FW_NAME, option);
+ if (err) {
+ dev_err(&client->dev, "the firmware update failed\n");
+ count = err;
+ }
+
+ mutex_unlock(&wdt_dev->sysfs_mutex);
+
+ return count;
+}
+
+static ssize_t fw_version_show(
+ struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct wdt_ts_data *wdt_dev = i2c_get_clientdata(client);
+
+ return scnprintf(buf, PAGE_SIZE, "%x\n", wdt_dev->param.fw_id);
+}
+
+static ssize_t plat_id_show(
+ struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct wdt_ts_data *wdt_dev = i2c_get_clientdata(client);
+
+ return scnprintf(buf, PAGE_SIZE, "%x\n", wdt_dev->param.plat_id);
+}
+
+static ssize_t config_csum_show(
+ struct device *dev, struct device_attribute *attr, char *buf)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct wdt_ts_data *wdt_dev = i2c_get_clientdata(client);
+ u32 cfg_csum;
+
+ cfg_csum = wdt_dev->param.xmls_id1;
+ cfg_csum = (cfg_csum << 16) | wdt_dev->param.xmls_id2;
+
+ return scnprintf(buf, PAGE_SIZE, "%x\n", cfg_csum);
+}
+
+static DEVICE_ATTR_WO(update_fw);
+static DEVICE_ATTR_RO(fw_version);
+static DEVICE_ATTR_RO(plat_id);
+static DEVICE_ATTR_RO(config_csum);
+
+static struct attribute *wdt87xx_attrs[] = {
+ &dev_attr_update_fw.attr,
+ &dev_attr_fw_version.attr,
+ &dev_attr_plat_id.attr,
+ &dev_attr_config_csum.attr,
+ NULL
+};
+
+static const struct attribute_group wdt87xx_attr_group = {
+ .attrs = wdt87xx_attrs,
+};
+
+static int wdt87xx_i2c_txrxdata(struct i2c_client *client, char *txdata,
+ int txlen, char *rxdata, int rxlen)
+{
+ int err;
+
+ struct i2c_msg msgs[] = {
+ {
+ .addr = client->addr,
+ .flags = 0,
+ .len = txlen,
+ .buf = txdata,
+ },
+ {
+ .addr = client->addr,
+ .flags = I2C_M_RD,
+ .len = rxlen,
+ .buf = rxdata,
+ },
+ };
+
+ err = i2c_transfer(client->adapter, msgs, 2);
+
+ if (err < 0)
+ dev_err(&client->dev, "%s: i2c read error (%d)\n",
+ __func__, err);
+
+ return err < 0 ? err : (err != ARRAY_SIZE(msgs) ? -EIO : 0);
+}
+
+static int wdt87xx_i2c_rxdata(struct i2c_client *client,
+ char *rxdata, int length)
+{
+ int err;
+
+ err = i2c_master_recv(client, rxdata, length);
+
+ if (err < 0)
+ dev_err(&client->dev, "%s: i2c read error (%d)\n",
+ __func__, err);
+
+ return err;
+}
+
+static int wdt87xx_i2c_txdata(struct i2c_client *client,
+ char *txdata, int length)
+{
+ int err;
+
+ err = i2c_master_send(client, txdata, length);
+ if (err < 0)
+ dev_err(&client->dev, "%s: i2c write error (%d)\n",
+ __func__, err);
+
+ return err;
+}
+
+static irqreturn_t wdt87xx_ts_interrupt(int irq, void *dev_id)
+{
+ struct wdt_ts_data *wdt_dev = dev_id;
+ int err;
+ int i, fingers;
+ struct i2c_client *client = wdt_dev->client;
+ struct input_dev *input_dev = wdt_dev->input_dev;
+ struct sys_param *param = &wdt_dev->param;
+ u8 raw_buf[WDT_V1_RAW_BUF_COUNT] = {0};
+ u8 *ptr_raw_buf = 0;
+
+ err = wdt87xx_i2c_rxdata(client, raw_buf, WDT_V1_RAW_BUF_COUNT);
+
+ if (err < 0) {
+ dev_err(&client->dev, "read v1 raw data failed\n");
+ goto irq_exit;
+ }
+
+ /* touch finger count */
+ fingers = raw_buf[TOUCH_PK_V1_OFFSET_FNGR_NUM];
+
+ /* skip this packet */
+ if (fingers == 0)
+ goto irq_exit;
+
+ ptr_raw_buf = &raw_buf[TOUCH_PK_V1_OFFSET_EVENT];
+ for (i = 0; i < WDT_MAX_FINGER; i++) {
+ int finger_id = (*ptr_raw_buf >> 3) - 1;
+
+ /* something wrong */
+ if (finger_id < 0)
+ break;
+
+ if (*ptr_raw_buf & 0x1) {
+ u32 coor_x, coor_y;
+ u8 w, h, p;
+ u16 value;
+
+ w = *(ptr_raw_buf + FINGER_EV_V1_OFFSET_W);
+ h = *(ptr_raw_buf + FINGER_EV_V1_OFFSET_H);
+ value = w * h;
+ p = (value >> 2);
+
+ coor_x = get_unaligned_le16(ptr_raw_buf +
+ FINGER_EV_V1_OFFSET_X);
+ coor_y = get_unaligned_le16(ptr_raw_buf +
+ FINGER_EV_V1_OFFSET_Y);
+
+ coor_y = DIV_ROUND_CLOSEST(
+ coor_y * param->phy_h, param->phy_w);
+
+ /* incorrect coordinate */
+ if (coor_x > wdt_dev->max_x || coor_y > wdt_dev->max_y)
+ break;
+
+ dev_dbg(&client->dev, "tip on (%d), x(%d), y(%d)\n",
+ i, coor_x, coor_y);
+
+ input_mt_slot(input_dev, finger_id);
+ input_mt_report_slot_state(
+ input_dev, MT_TOOL_FINGER, 1);
+ input_report_abs(input_dev, ABS_MT_TOUCH_MAJOR, w);
+ input_report_abs(input_dev, ABS_MT_PRESSURE, p);
+ input_report_abs(input_dev, ABS_MT_POSITION_X, coor_x);
+ input_report_abs(input_dev, ABS_MT_POSITION_Y, coor_y);
+ }
+ ptr_raw_buf += FINGER_EV_V1_SIZE;
+ }
+
+ input_mt_sync_frame(input_dev);
+ input_sync(input_dev);
+
+irq_exit:
+ return IRQ_HANDLED;
+}
+
+static int wdt87xx_ts_request_irq(struct i2c_client *client)
+{
+ int err;
+ struct wdt_ts_data *wdt_dev = i2c_get_clientdata(client);
+
+ err = devm_request_threaded_irq(&client->dev, client->irq, NULL,
+ wdt87xx_ts_interrupt, IRQF_ONESHOT,
+ client->name, wdt_dev);
+
+ if (err < 0) {
+ dev_err(&client->dev, "request threaded irq failed\n");
+ return err;
+ }
+
+ return 0;
+}
+
+static int wdt87xx_ts_create_input_device(struct i2c_client *client)
+{
+ int err;
+ struct wdt_ts_data *wdt_dev = i2c_get_clientdata(client);
+ struct input_dev *input_dev;
+ u32 res;
+
+ input_dev = devm_input_allocate_device(&client->dev);
+ if (!input_dev) {
+ dev_err(&client->dev, "failed to allocate input device\n");
+ return -ENOMEM;
+ }
+
+ wdt_dev->input_dev = input_dev;
+
+ wdt_dev->max_x = MAX_UNIT_AXIS;
+ wdt_dev->max_y = DIV_ROUND_CLOSEST(
+ MAX_UNIT_AXIS * wdt_dev->param.phy_h, wdt_dev->param.phy_w);
+
+ res = DIV_ROUND_CLOSEST(MAX_UNIT_AXIS, wdt_dev->param.phy_w);
+
+ input_dev->name = "WDT87xx Touchscreen";
+ input_dev->id.bustype = BUS_I2C;
+ input_dev->phys = wdt_dev->phys;
+ input_dev->dev.parent = &wdt_dev->client->dev;
+
+ __set_bit(EV_ABS, input_dev->evbit);
+ __set_bit(EV_KEY, input_dev->evbit);
+ __set_bit(BTN_TOUCH, input_dev->keybit);
+
+ input_set_abs_params(input_dev, ABS_MT_POSITION_X, 0,
+ wdt_dev->max_x, 0, 0);
+ input_set_abs_params(input_dev, ABS_MT_POSITION_Y, 0,
+ wdt_dev->max_y, 0, 0);
+ input_abs_set_res(input_dev, ABS_MT_POSITION_X, res);
+ input_abs_set_res(input_dev, ABS_MT_POSITION_Y, res);
+
+ input_set_abs_params(input_dev, ABS_MT_TOUCH_MAJOR, 0, 0xFF, 0, 0);
+ input_set_abs_params(input_dev, ABS_MT_PRESSURE, 0, 0xFF, 0, 0);
+
+ input_mt_init_slots(input_dev, WDT_MAX_FINGER,
+ INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
+
+ err = input_register_device(input_dev);
+ if (err) {
+ dev_err(&client->dev, "failed to register input device\n");
+ return err;
+ }
+
+ return 0;
+}
+
+static int wdt87xx_ts_probe(
+ struct i2c_client *client, const struct i2c_device_id *id)
+{
+ struct wdt_ts_data *wdt_dev;
+ int err;
+
+ dev_info(&client->dev, "wdt87xx : adapter=(%d), client irq:(%d)\n",
+ client->adapter->nr, client->irq);
+
+ /* check if the I2C function is ok in this adaptor */
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
+ return -ENODEV;
+
+ wdt_dev = devm_kzalloc(&client->dev, sizeof(struct wdt_ts_data),
+ GFP_KERNEL);
+ if (!wdt_dev)
+ return -ENOMEM;
+
+ wdt_dev->client = client;
+ mutex_init(&wdt_dev->sysfs_mutex);
+ i2c_set_clientdata(client, wdt_dev);
+
+ snprintf(wdt_dev->phys, sizeof(wdt_dev->phys), "i2c-%u-%04x/input0",
+ client->adapter->nr, client->addr);
+
+ wdt87xx_get_sysparam(client);
+
+ err = wdt87xx_ts_create_input_device(client);
+ if (err < 0) {
+ dev_err(&client->dev, "create input device failed: (%d)\n",
+ err);
+ return err;
+ }
+
+ err = wdt87xx_ts_request_irq(client);
+ if (err < 0) {
+ dev_err(&client->dev, "request irq failed: (%d)\n", err);
+ return err;
+ }
+
+ err = sysfs_create_group(&client->dev.kobj, &wdt87xx_attr_group);
+ if (err) {
+ dev_err(&client->dev, "create sysfs failed: (%d)\n", err);
+ return err;
+ }
+
+ return 0;
+}
+
+static int wdt87xx_ts_remove(struct i2c_client *client)
+{
+ sysfs_remove_group(&client->dev.kobj, &wdt87xx_attr_group);
+
+ return 0;
+}
+
+static int __maybe_unused wdt87xx_suspend(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ int err;
+
+ disable_irq(client->irq);
+
+ err = wdt87xx_send_command(client, VND_CMD_STOP, MODE_IDLE);
+ if (err)
+ dev_err(&client->dev, "%s: command stop failed\n",
+ __func__);
+
+ return err;
+}
+
+static int __maybe_unused wdt87xx_resume(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ int err;
+
+ /* once the chip is reset before resume, */
+ /* we need some time to wait it is stable */
+ mdelay(100);
+
+ err = wdt87xx_send_command(client, VND_CMD_START, 0);
+ if (err)
+ dev_err(&client->dev, "%s: command start failed\n",
+ __func__);
+
+ enable_irq(client->irq);
+
+ return 0;
+}
+
+static SIMPLE_DEV_PM_OPS(wdt87xx_pm_ops, wdt87xx_suspend, wdt87xx_resume);
+
+static const struct i2c_device_id wdt87xx_dev_id[] = {
+ { WDT87XX_NAME, 0 },
+ { }
+};
+
+MODULE_DEVICE_TABLE(i2c, wdt87xx_dev_id);
+
+static const struct acpi_device_id wdt87xx_acpi_id[] = {
+ { "WDHT0001", 0 },
+ { }
+};
+
+MODULE_DEVICE_TABLE(acpi, wdt87xx_acpi_id);
+
+static struct i2c_driver wdt87xx_driver = {
+ .probe = wdt87xx_ts_probe,
+ .remove = wdt87xx_ts_remove,
+ .id_table = wdt87xx_dev_id,
+ .driver = {
+ .name = WDT87XX_NAME,
+ .owner = THIS_MODULE,
+ .pm = &wdt87xx_pm_ops,
+ .acpi_match_table = ACPI_PTR(wdt87xx_acpi_id),
+ },
+};
+
+module_i2c_driver(wdt87xx_driver);
+
+MODULE_AUTHOR("HN Chen <hn.chen@xxxxxxxxxxxxxxx>");
+MODULE_DESCRIPTION("WeidaHiTech WDT87XX Touchscreen driver");
+MODULE_VERSION(WDT87XX_DRV_VER);
+MODULE_LICENSE("GPL");
+
--
1.9.1

--
To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/