588 lines
14 KiB
C
Executable file
588 lines
14 KiB
C
Executable file
/* drivers/input/sec_input/stm/stm_core.c
|
|
*
|
|
* Copyright (C) 2020 Samsung Electronics Co., Ltd.
|
|
*
|
|
* Core file for Samsung TSC driver
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 as
|
|
* published by the Free Software Foundation.
|
|
*/
|
|
|
|
#include "stm_dev.h"
|
|
#include "stm_reg.h"
|
|
|
|
#if IS_ENABLED(CONFIG_INPUT_SEC_SECURE_TOUCH)
|
|
int stm_pm_runtime_get_sync(struct stm_ts_data *ts)
|
|
{
|
|
return pm_runtime_get_sync(ts->client->adapter->dev.parent);
|
|
}
|
|
|
|
void stm_pm_runtime_put_sync(struct stm_ts_data *ts)
|
|
{
|
|
pm_runtime_put_sync(ts->client->adapter->dev.parent);
|
|
}
|
|
#endif
|
|
|
|
#if IS_ENABLED(CONFIG_SAMSUNG_TUI)
|
|
extern int stui_i2c_lock(struct i2c_adapter *adap);
|
|
extern int stui_i2c_unlock(struct i2c_adapter *adap);
|
|
|
|
int stm_stui_tsp_enter(void)
|
|
{
|
|
struct stm_ts_data *ts = dev_get_drvdata(ptsp);
|
|
int ret = 0;
|
|
|
|
if (!ts)
|
|
return -EINVAL;
|
|
|
|
#if IS_ENABLED(CONFIG_INPUT_SEC_NOTIFIER)
|
|
sec_input_notify(&ts->stm_input_nb, NOTIFIER_SECURE_TOUCH_ENABLE, NULL);
|
|
#endif
|
|
|
|
disable_irq(ts->irq);
|
|
stm_ts_release_all_finger(ts);
|
|
|
|
ret = stui_i2c_lock(ts->client->adapter);
|
|
if (ret) {
|
|
pr_err("[STUI] stui_i2c_lock failed : %d\n", ret);
|
|
#if IS_ENABLED(CONFIG_INPUT_SEC_NOTIFIER)
|
|
sec_input_notify(&ts->stm_input_nb, NOTIFIER_SECURE_TOUCH_DISABLE, NULL);
|
|
#endif
|
|
enable_irq(ts->irq);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int stm_stui_tsp_exit(void)
|
|
{
|
|
struct stm_ts_data *ts = dev_get_drvdata(ptsp);
|
|
int ret = 0;
|
|
|
|
if (!ts)
|
|
return -EINVAL;
|
|
|
|
ret = stui_i2c_unlock(ts->client->adapter);
|
|
if (ret)
|
|
pr_err("[STUI] stui_i2c_unlock failed : %d\n", ret);
|
|
|
|
enable_irq(ts->irq);
|
|
|
|
#if IS_ENABLED(CONFIG_INPUT_SEC_NOTIFIER)
|
|
sec_input_notify(&ts->stm_input_nb, NOTIFIER_SECURE_TOUCH_DISABLE, NULL);
|
|
#endif
|
|
|
|
return ret;
|
|
}
|
|
|
|
int stm_stui_tsp_type(void)
|
|
{
|
|
return STUI_TSP_TYPE_STM;
|
|
}
|
|
#endif
|
|
|
|
#ifdef TCLM_CONCEPT
|
|
int stm_ts_tclm_execute_force_calibration(struct i2c_client *client, int cal_mode)
|
|
{
|
|
struct stm_ts_data *ts = (struct stm_ts_data *)i2c_get_clientdata(client);
|
|
|
|
return stm_ts_execute_autotune(ts, true);
|
|
}
|
|
|
|
int stm_tclm_data_read(struct stm_ts_data *ts, int address)
|
|
{
|
|
return ts->tdata->tclm_read(ts->tdata->client, address);
|
|
}
|
|
|
|
int stm_tclm_data_write(struct stm_ts_data *ts, int address)
|
|
{
|
|
return ts->tdata->tclm_write(ts->tdata->client, address);
|
|
}
|
|
|
|
int stm_tclm_i2c_data_read(struct i2c_client *client, int address)
|
|
{
|
|
struct stm_ts_data *ts = i2c_get_clientdata(client);
|
|
|
|
return _stm_tclm_data_read(ts, address);
|
|
}
|
|
int stm_tclm_i2c_data_write(struct i2c_client *client, int address)
|
|
{
|
|
struct stm_ts_data *ts = i2c_get_clientdata(client);
|
|
|
|
return _stm_tclm_data_write(ts, address);
|
|
}
|
|
#endif
|
|
|
|
int stm_ts_wire_mode_change(struct stm_ts_data *ts, u8 *reg)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int stm_ts_tool_proc_init(struct stm_ts_data *ts)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int stm_ts_tool_proc_remove(void)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
int stm_ts_read_from_sponge(struct stm_ts_data *ts, u8 *data, int length)
|
|
{
|
|
int ret;
|
|
u8 address[3];
|
|
|
|
mutex_lock(&ts->sponge_mutex);
|
|
address[0] = STM_TS_CMD_SPONGE_READ_WRITE_CMD;
|
|
address[1] = data[1];
|
|
address[2] = data[0];
|
|
ret = ts->stm_ts_read(ts, address, 3, data, length);
|
|
if (ret < 0)
|
|
input_err(true, &ts->client->dev, "%s: fail to read sponge command\n", __func__);
|
|
mutex_unlock(&ts->sponge_mutex);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int stm_ts_write_to_sponge(struct stm_ts_data *ts, u8 *data, int length)
|
|
{
|
|
int ret;
|
|
u8 address[3];
|
|
|
|
mutex_lock(&ts->sponge_mutex);
|
|
address[0] = STM_TS_CMD_SPONGE_READ_WRITE_CMD;
|
|
address[1] = data[1];
|
|
address[2] = data[0];
|
|
ret = ts->stm_ts_write(ts, address, 3, &data[2], length - 2);
|
|
if (ret < 0)
|
|
input_err(true, &ts->client->dev, "%s: Failed to write offset\n", __func__);
|
|
|
|
address[0] = STM_TS_CMD_SPONGE_NOTIFY_CMD;
|
|
ret = ts->stm_ts_write(ts, address, 3, NULL, 0);
|
|
if (ret < 0)
|
|
input_err(true, &ts->client->dev, "%s: Failed to send notify\n", __func__);
|
|
mutex_unlock(&ts->sponge_mutex);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
int stm_ts_i2c_write(struct stm_ts_data *ts, u8 *reg, int cnum, u8 *data, int len)
|
|
{
|
|
int ret;
|
|
unsigned char retry;
|
|
struct i2c_msg msg;
|
|
int i;
|
|
const int len_max = 0xffff;
|
|
u8 *buf;
|
|
u8 *msg_buff;
|
|
|
|
if (len + 1 > len_max) {
|
|
input_err(true, &ts->client->dev,
|
|
"%s: The i2c buffer size is exceeded.\n", __func__);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
if (!ts->plat_data->resume_done.done) {
|
|
ret = wait_for_completion_interruptible_timeout(&ts->plat_data->resume_done, msecs_to_jiffies(500));
|
|
if (ret <= 0) {
|
|
input_err(true, &ts->client->dev, "%s: LPM: pm resume is not handled:%d\n", __func__, ret);
|
|
return -EIO;
|
|
}
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_INPUT_SEC_SECURE_TOUCH)
|
|
if (atomic_read(&ts->secure_enabled) == SECURE_TOUCH_ENABLE) {
|
|
input_err(true, &ts->client->dev,
|
|
"%s: TSP no accessible from Linux, TUI is enabled!\n", __func__);
|
|
return -EBUSY;
|
|
}
|
|
#endif
|
|
#if IS_ENABLED(CONFIG_SAMSUNG_TUI)
|
|
if (STUI_MODE_TOUCH_SEC & stui_get_mode())
|
|
return -EBUSY;
|
|
#endif
|
|
buf = kzalloc(cnum, GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
|
|
memcpy(buf, reg, cnum);
|
|
|
|
msg_buff = kzalloc(len + cnum, GFP_KERNEL);
|
|
if (!msg_buff) {
|
|
kfree(buf);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
if (ts->plat_data->power_state == SEC_INPUT_STATE_POWER_OFF) {
|
|
input_err(true, &ts->client->dev, "%s: POWER_STATUS : OFF\n", __func__);
|
|
goto err;
|
|
}
|
|
|
|
memcpy(msg_buff, buf, cnum);
|
|
memcpy(msg_buff + cnum, data, len);
|
|
|
|
msg.addr = ts->client->addr;
|
|
msg.flags = 0 | I2C_M_DMA_SAFE;
|
|
msg.len = len + cnum;
|
|
msg.buf = msg_buff;
|
|
|
|
mutex_lock(&ts->read_write_mutex);
|
|
for (retry = 0; retry < SEC_TS_I2C_RETRY_CNT; retry++) {
|
|
ret = i2c_transfer(ts->client->adapter, &msg, 1);
|
|
if (ret == 1)
|
|
break;
|
|
|
|
if (ts->plat_data->power_state == SEC_INPUT_STATE_POWER_OFF) {
|
|
input_err(true, &ts->client->dev, "%s: POWER_STATUS : OFF, retry:%d\n", __func__, retry);
|
|
mutex_unlock(&ts->read_write_mutex);
|
|
goto err;
|
|
}
|
|
|
|
usleep_range(1 * 1000, 1 * 1000);
|
|
|
|
if (retry > 1) {
|
|
char result[32];
|
|
input_err(true, &ts->client->dev, "%s: I2C retry %d, ret:%d\n", __func__, retry + 1, ret);
|
|
ts->plat_data->hw_param.comm_err_count++;
|
|
|
|
snprintf(result, sizeof(result), "RESULT=I2C");
|
|
if (ts->probe_done)
|
|
sec_cmd_send_event_to_user(&ts->sec, NULL, result);
|
|
}
|
|
}
|
|
|
|
mutex_unlock(&ts->read_write_mutex);
|
|
|
|
if (retry == SEC_TS_I2C_RETRY_CNT) {
|
|
input_err(true, &ts->client->dev, "%s: I2C write over retry limit\n", __func__);
|
|
ret = -EIO;
|
|
if (ts->probe_done && !ts->reset_is_on_going && !ts->plat_data->shutdown_called)
|
|
schedule_delayed_work(&ts->reset_work, msecs_to_jiffies(TOUCH_RESET_DWORK_TIME));
|
|
}
|
|
|
|
if (ts->debug_flag & SEC_TS_DEBUG_PRINT_WRITE_CMD) {
|
|
pr_info("sec_input:i2c_cmd: W: %02X | ", *reg);
|
|
for (i = 0; i < len; i++)
|
|
pr_cont("%02X ", data[i]);
|
|
pr_cont("\n");
|
|
}
|
|
|
|
if (ret == 1) {
|
|
kfree(msg_buff);
|
|
kfree(buf);
|
|
return 0;
|
|
}
|
|
err:
|
|
kfree(msg_buff);
|
|
kfree(buf);
|
|
return -EIO;
|
|
}
|
|
|
|
int stm_ts_i2c_read(struct stm_ts_data *ts, u8 *reg, int cnum, u8 *data, int len)
|
|
{
|
|
int ret;
|
|
unsigned char retry;
|
|
struct i2c_msg msg[2];
|
|
int remain = len;
|
|
int i;
|
|
u8 *msg_buff;
|
|
u8 *buf;
|
|
|
|
if (!ts->plat_data->resume_done.done) {
|
|
ret = wait_for_completion_interruptible_timeout(&ts->plat_data->resume_done, msecs_to_jiffies(500));
|
|
if (ret <= 0) {
|
|
input_err(true, &ts->client->dev, "%s: LPM: pm resume is not handled:%d\n", __func__, ret);
|
|
return -EIO;
|
|
}
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_INPUT_SEC_SECURE_TOUCH)
|
|
if (atomic_read(&ts->secure_enabled) == SECURE_TOUCH_ENABLE) {
|
|
input_err(true, &ts->client->dev,
|
|
"%s: TSP no accessible from Linux, TUI is enabled!\n", __func__);
|
|
return -EBUSY;
|
|
}
|
|
#endif
|
|
#if IS_ENABLED(CONFIG_SAMSUNG_TUI)
|
|
if (STUI_MODE_TOUCH_SEC & stui_get_mode())
|
|
return -EBUSY;
|
|
#endif
|
|
buf = kzalloc(cnum, GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
|
|
memcpy(buf, reg, cnum);
|
|
|
|
msg_buff = kzalloc(len, GFP_KERNEL);
|
|
if (!msg_buff) {
|
|
kfree(buf);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
if (ts->plat_data->power_state == SEC_INPUT_STATE_POWER_OFF) {
|
|
input_err(true, &ts->client->dev, "%s: POWER_STATUS : OFF\n", __func__);
|
|
goto err;
|
|
}
|
|
|
|
msg[0].addr = ts->client->addr;
|
|
msg[0].flags = 0 | I2C_M_DMA_SAFE;
|
|
msg[0].len = cnum;
|
|
msg[0].buf = buf;
|
|
|
|
msg[1].addr = ts->client->addr;
|
|
msg[1].flags = I2C_M_RD | I2C_M_DMA_SAFE;
|
|
msg[1].buf = msg_buff;
|
|
|
|
mutex_lock(&ts->read_write_mutex);
|
|
if (len <= ts->plat_data->i2c_burstmax) {
|
|
msg[1].len = len;
|
|
for (retry = 0; retry < SEC_TS_I2C_RETRY_CNT; retry++) {
|
|
ret = i2c_transfer(ts->client->adapter, msg, 2);
|
|
if (ret == 2)
|
|
break;
|
|
usleep_range(1 * 1000, 1 * 1000);
|
|
if (ts->plat_data->power_state == SEC_INPUT_STATE_POWER_OFF) {
|
|
input_err(true, &ts->client->dev, "%s: POWER_STATUS : OFF, retry:%d\n", __func__, retry);
|
|
mutex_unlock(&ts->read_write_mutex);
|
|
goto err;
|
|
}
|
|
|
|
if (retry > 1) {
|
|
char result[32];
|
|
input_err(true, &ts->client->dev, "%s: I2C retry %d, ret:%d\n",
|
|
__func__, retry + 1, ret);
|
|
ts->plat_data->hw_param.comm_err_count++;
|
|
|
|
snprintf(result, sizeof(result), "RESULT=I2C");
|
|
if (ts->probe_done)
|
|
sec_cmd_send_event_to_user(&ts->sec, NULL, result);
|
|
}
|
|
}
|
|
} else {
|
|
/*
|
|
* I2C read buffer is 256 byte. do not support long buffer over than 256.
|
|
* So, try to seperate reading data about 256 bytes.
|
|
*/
|
|
for (retry = 0; retry < SEC_TS_I2C_RETRY_CNT; retry++) {
|
|
ret = i2c_transfer(ts->client->adapter, msg, 1);
|
|
if (ret == 1)
|
|
break;
|
|
usleep_range(1 * 1000, 1 * 1000);
|
|
if (ts->plat_data->power_state == SEC_INPUT_STATE_POWER_OFF) {
|
|
input_err(true, &ts->client->dev, "%s: POWER_STATUS : OFF, retry:%d\n", __func__, retry);
|
|
mutex_unlock(&ts->read_write_mutex);
|
|
goto err;
|
|
}
|
|
|
|
if (retry > 1) {
|
|
input_err(true, &ts->client->dev, "%s: I2C retry %d, ret:%d\n",
|
|
__func__, retry + 1, ret);
|
|
ts->plat_data->hw_param.comm_err_count++;
|
|
}
|
|
}
|
|
|
|
do {
|
|
if (remain > ts->plat_data->i2c_burstmax)
|
|
msg[1].len = ts->plat_data->i2c_burstmax;
|
|
else
|
|
msg[1].len = remain;
|
|
|
|
remain -= ts->plat_data->i2c_burstmax;
|
|
|
|
for (retry = 0; retry < SEC_TS_I2C_RETRY_CNT; retry++) {
|
|
ret = i2c_transfer(ts->client->adapter, &msg[1], 1);
|
|
if (ret == 1)
|
|
break;
|
|
usleep_range(1 * 1000, 1 * 1000);
|
|
if (ts->plat_data->power_state == SEC_INPUT_STATE_POWER_OFF) {
|
|
input_err(true, &ts->client->dev, "%s: POWER_STATUS : OFF, retry:%d\n", __func__, retry);
|
|
mutex_unlock(&ts->read_write_mutex);
|
|
goto err;
|
|
}
|
|
|
|
if (retry > 1) {
|
|
input_err(true, &ts->client->dev, "%s: I2C retry %d, ret:%d\n",
|
|
__func__, retry + 1, ret);
|
|
ts->plat_data->hw_param.comm_err_count++;
|
|
}
|
|
}
|
|
msg[1].buf += msg[1].len;
|
|
} while (remain > 0);
|
|
}
|
|
|
|
mutex_unlock(&ts->read_write_mutex);
|
|
|
|
if (retry == SEC_TS_I2C_RETRY_CNT) {
|
|
input_err(true, &ts->client->dev, "%s: I2C read over retry limit\n", __func__);
|
|
ret = -EIO;
|
|
if (ts->probe_done && !ts->reset_is_on_going && !ts->plat_data->shutdown_called)
|
|
schedule_delayed_work(&ts->reset_work, msecs_to_jiffies(TOUCH_RESET_DWORK_TIME));
|
|
}
|
|
|
|
memcpy(data, msg_buff, len);
|
|
if (ts->debug_flag & SEC_TS_DEBUG_PRINT_READ_CMD) {
|
|
pr_info("sec_input:i2c_cmd: R: %02X | ", *reg);
|
|
for (i = 0; i < len; i++)
|
|
pr_cont("%02X ", data[i]);
|
|
pr_cont("\n");
|
|
}
|
|
|
|
kfree(buf);
|
|
kfree(msg_buff);
|
|
return ret;
|
|
err:
|
|
kfree(buf);
|
|
kfree(msg_buff);
|
|
return -EIO;
|
|
}
|
|
|
|
int stm_ts_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
|
|
{
|
|
struct stm_ts_data *ts;
|
|
struct sec_ts_plat_data *pdata;
|
|
struct sec_tclm_data *tdata;
|
|
int ret = 0;
|
|
|
|
input_info(true, &client->dev, "%s\n", __func__);
|
|
|
|
if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
|
|
input_err(true, &client->dev, "%s: EIO err!\n", __func__);
|
|
return -EIO;
|
|
}
|
|
|
|
ts = devm_kzalloc(&client->dev, sizeof(struct stm_ts_data), GFP_KERNEL);
|
|
if (!ts) {
|
|
ret = -ENOMEM;
|
|
goto error_allocate_mem;
|
|
}
|
|
|
|
pdata = devm_kzalloc(&client->dev,
|
|
sizeof(struct sec_ts_plat_data), GFP_KERNEL);
|
|
if (!pdata) {
|
|
ret = -ENOMEM;
|
|
goto error_allocate_pdata;
|
|
}
|
|
|
|
tdata = devm_kzalloc(&client->dev,
|
|
sizeof(struct sec_tclm_data), GFP_KERNEL);
|
|
if (!tdata) {
|
|
ret = -ENOMEM;
|
|
goto error_allocate_tdata;
|
|
}
|
|
client->dev.platform_data = pdata;
|
|
|
|
ts->client = client;
|
|
ts->plat_data = pdata;
|
|
ts->stm_ts_read = stm_ts_i2c_read;
|
|
ts->stm_ts_write = stm_ts_i2c_write;
|
|
ts->stm_ts_read_sponge = stm_ts_read_from_sponge;
|
|
ts->stm_ts_write_sponge = stm_ts_write_to_sponge;
|
|
ts->tdata = tdata;
|
|
#ifdef TCLM_CONCEPT
|
|
ts->tdata->client = ts->client;
|
|
ts->tdata->tclm_read = stm_tclm_i2c_data_read;
|
|
ts->tdata->tclm_write = stm_tclm_i2c_data_write;
|
|
ts->tdata->tclm_execute_force_calibration = stm_ts_tclm_execute_force_calibration;
|
|
#endif
|
|
i2c_set_clientdata(client, ts);
|
|
|
|
#if IS_ENABLED(CONFIG_SAMSUNG_TUI)
|
|
ts->plat_data->stui_tsp_enter = stm_stui_tsp_enter;
|
|
ts->plat_data->stui_tsp_exit = stm_stui_tsp_exit;
|
|
ts->plat_data->stui_tsp_type = stm_stui_tsp_type;
|
|
#endif
|
|
ret = stm_ts_probe(ts);
|
|
return ret;
|
|
|
|
error_allocate_tdata:
|
|
error_allocate_pdata:
|
|
error_allocate_mem:
|
|
return ret;
|
|
}
|
|
|
|
int stm_ts_i2c_remove(struct i2c_client *client)
|
|
{
|
|
struct stm_ts_data *ts = i2c_get_clientdata(client);
|
|
int ret = 0;
|
|
|
|
ret = stm_ts_remove(ts);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void stm_ts_i2c_shutdown(struct i2c_client *client)
|
|
{
|
|
struct stm_ts_data *ts = i2c_get_clientdata(client);
|
|
|
|
stm_ts_shutdown(ts);
|
|
}
|
|
|
|
|
|
#if IS_ENABLED(CONFIG_PM)
|
|
static int stm_ts_i2c_pm_suspend(struct device *dev)
|
|
{
|
|
struct stm_ts_data *ts = dev_get_drvdata(dev);
|
|
|
|
stm_ts_pm_suspend(ts);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int stm_ts_i2c_pm_resume(struct device *dev)
|
|
{
|
|
struct stm_ts_data *ts = dev_get_drvdata(dev);
|
|
|
|
stm_ts_pm_resume(ts);
|
|
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
|
|
static const struct i2c_device_id stm_ts_id[] = {
|
|
{ STM_TS_I2C_NAME, 0 },
|
|
{ },
|
|
};
|
|
|
|
#if IS_ENABLED(CONFIG_PM)
|
|
static const struct dev_pm_ops stm_ts_dev_pm_ops = {
|
|
.suspend = stm_ts_i2c_pm_suspend,
|
|
.resume = stm_ts_i2c_pm_resume,
|
|
};
|
|
#endif
|
|
|
|
#if IS_ENABLED(CONFIG_OF)
|
|
static const struct of_device_id stm_ts_match_table[] = {
|
|
{ .compatible = "stm,stm_ts",},
|
|
{ },
|
|
};
|
|
#else
|
|
#define stm_ts_match_table NULL
|
|
#endif
|
|
|
|
static struct i2c_driver stm_ts_driver = {
|
|
.probe = stm_ts_i2c_probe,
|
|
.remove = stm_ts_i2c_remove,
|
|
.shutdown = stm_ts_i2c_shutdown,
|
|
.id_table = stm_ts_id,
|
|
.driver = {
|
|
.owner = THIS_MODULE,
|
|
.name = STM_TS_I2C_NAME,
|
|
#if IS_ENABLED(CONFIG_OF)
|
|
.of_match_table = stm_ts_match_table,
|
|
#endif
|
|
#if IS_ENABLED(CONFIG_PM)
|
|
.pm = &stm_ts_dev_pm_ops,
|
|
#endif
|
|
},
|
|
};
|
|
|
|
module_i2c_driver(stm_ts_driver);
|
|
|
|
MODULE_SOFTDEP("pre: acpm-mfd-bus");
|
|
MODULE_DESCRIPTION("stm TouchScreen driver");
|
|
MODULE_LICENSE("GPL");
|