647 lines
15 KiB
C
647 lines
15 KiB
C
|
|
/* Copyright (c) 2014-2015, The Linux Foundation. All rights reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 and
|
|
* only version 2 as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*/
|
|
|
|
#include <linux/poll.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/module.h>
|
|
#include <linux/init.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/miscdevice.h>
|
|
#include <linux/input/mt.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/regulator/consumer.h>
|
|
#include <uapi/linux/hbtp_input.h>
|
|
#include "../input-compat.h"
|
|
|
|
#if defined(CONFIG_FB)
|
|
#include <linux/notifier.h>
|
|
#include <linux/fb.h>
|
|
#endif
|
|
|
|
#define HBTP_INPUT_NAME "hbtp_input"
|
|
|
|
struct hbtp_data {
|
|
struct platform_device *pdev;
|
|
struct input_dev *input_dev;
|
|
s32 count;
|
|
struct mutex mutex;
|
|
bool touch_status[HBTP_MAX_FINGER];
|
|
#if defined(CONFIG_FB)
|
|
struct notifier_block fb_notif;
|
|
#endif
|
|
struct regulator *vcc_ana;
|
|
struct regulator *vcc_dig;
|
|
int afe_load_ua;
|
|
int afe_vtg_min_uv;
|
|
int afe_vtg_max_uv;
|
|
int dig_load_ua;
|
|
int dig_vtg_min_uv;
|
|
int dig_vtg_max_uv;
|
|
bool manage_afe_power_ana;
|
|
bool manage_power_dig;
|
|
};
|
|
|
|
static struct hbtp_data *hbtp;
|
|
|
|
#if defined(CONFIG_FB)
|
|
static int fb_notifier_callback(struct notifier_block *self,
|
|
unsigned long event, void *data)
|
|
{
|
|
int blank;
|
|
struct fb_event *evdata = data;
|
|
struct hbtp_data *hbtp_data =
|
|
container_of(self, struct hbtp_data, fb_notif);
|
|
char *envp[2] = {HBTP_EVENT_TYPE_DISPLAY, NULL};
|
|
|
|
if (evdata && evdata->data && event == FB_EVENT_BLANK &&
|
|
hbtp_data && hbtp_data->input_dev) {
|
|
blank = *(int *)(evdata->data);
|
|
if (blank == FB_BLANK_UNBLANK)
|
|
kobject_uevent_env(&hbtp_data->input_dev->dev.kobj,
|
|
KOBJ_ONLINE, envp);
|
|
else if (blank == FB_BLANK_POWERDOWN)
|
|
kobject_uevent_env(&hbtp_data->input_dev->dev.kobj,
|
|
KOBJ_OFFLINE, envp);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
static int hbtp_input_open(struct inode *inode, struct file *file)
|
|
{
|
|
mutex_lock(&hbtp->mutex);
|
|
if (hbtp->count) {
|
|
pr_err("%s is busy\n", HBTP_INPUT_NAME);
|
|
mutex_unlock(&hbtp->mutex);
|
|
return -EBUSY;
|
|
}
|
|
hbtp->count++;
|
|
mutex_unlock(&hbtp->mutex);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int hbtp_input_release(struct inode *inode, struct file *file)
|
|
{
|
|
mutex_lock(&hbtp->mutex);
|
|
if (!hbtp->count) {
|
|
pr_err("%s wasn't opened\n", HBTP_INPUT_NAME);
|
|
mutex_unlock(&hbtp->mutex);
|
|
return -ENOTTY;
|
|
}
|
|
hbtp->count--;
|
|
mutex_unlock(&hbtp->mutex);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int hbtp_input_create_input_dev(struct hbtp_input_absinfo *absinfo)
|
|
{
|
|
struct input_dev *input_dev;
|
|
struct hbtp_input_absinfo *abs;
|
|
int error;
|
|
int i;
|
|
|
|
input_dev = input_allocate_device();
|
|
if (!input_dev) {
|
|
pr_err("%s: input_allocate_device failed\n", __func__);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
kfree(input_dev->name);
|
|
input_dev->name = kstrndup(HBTP_INPUT_NAME, sizeof(HBTP_INPUT_NAME),
|
|
GFP_KERNEL);
|
|
__set_bit(EV_ABS, input_dev->evbit);
|
|
__set_bit(EV_KEY, input_dev->evbit);
|
|
__set_bit(BTN_TOUCH, input_dev->keybit);
|
|
__set_bit(INPUT_PROP_DIRECT, input_dev->propbit);
|
|
|
|
for (i = KEY_HOME; i <= KEY_MICMUTE; i++)
|
|
__set_bit(i, input_dev->keybit);
|
|
|
|
/* For multi touch */
|
|
input_mt_init_slots(input_dev, HBTP_MAX_FINGER, 0);
|
|
for (i = 0; i <= ABS_MT_LAST - ABS_MT_FIRST; i++) {
|
|
abs = absinfo + i;
|
|
if (abs->active)
|
|
input_set_abs_params(input_dev, abs->code,
|
|
abs->minimum, abs->maximum, 0, 0);
|
|
}
|
|
|
|
error = input_register_device(input_dev);
|
|
if (error) {
|
|
pr_err("%s: input_register_device failed\n", __func__);
|
|
goto err_input_reg_dev;
|
|
}
|
|
|
|
hbtp->input_dev = input_dev;
|
|
return 0;
|
|
|
|
err_input_reg_dev:
|
|
input_free_device(input_dev);
|
|
|
|
return error;
|
|
}
|
|
|
|
static int hbtp_input_report_events(struct hbtp_data *hbtp_data,
|
|
struct hbtp_input_mt *mt_data)
|
|
{
|
|
int i;
|
|
struct hbtp_input_touch *tch;
|
|
|
|
for (i = 0; i < HBTP_MAX_FINGER; i++) {
|
|
tch = &(mt_data->touches[i]);
|
|
if (tch->active || hbtp_data->touch_status[i]) {
|
|
input_mt_slot(hbtp_data->input_dev, i);
|
|
input_mt_report_slot_state(hbtp_data->input_dev,
|
|
MT_TOOL_FINGER, tch->active);
|
|
|
|
if (tch->active) {
|
|
input_report_abs(hbtp_data->input_dev,
|
|
ABS_MT_TOOL_TYPE,
|
|
tch->tool);
|
|
input_report_abs(hbtp_data->input_dev,
|
|
ABS_MT_TOUCH_MAJOR,
|
|
tch->major);
|
|
input_report_abs(hbtp_data->input_dev,
|
|
ABS_MT_TOUCH_MINOR,
|
|
tch->minor);
|
|
input_report_abs(hbtp_data->input_dev,
|
|
ABS_MT_ORIENTATION,
|
|
tch->orientation);
|
|
input_report_abs(hbtp_data->input_dev,
|
|
ABS_MT_PRESSURE,
|
|
tch->pressure);
|
|
input_report_abs(hbtp_data->input_dev,
|
|
ABS_MT_POSITION_X,
|
|
tch->x);
|
|
input_report_abs(hbtp_data->input_dev,
|
|
ABS_MT_POSITION_Y,
|
|
tch->y);
|
|
}
|
|
hbtp_data->touch_status[i] = tch->active;
|
|
}
|
|
}
|
|
|
|
input_report_key(hbtp->input_dev, BTN_TOUCH, mt_data->num_touches > 0);
|
|
input_sync(hbtp->input_dev);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int reg_set_optimum_mode_check(struct regulator *reg, int load_uA)
|
|
{
|
|
return (regulator_count_voltages(reg) > 0) ?
|
|
regulator_set_optimum_mode(reg, load_uA) : 0;
|
|
}
|
|
|
|
static int hbtp_pdev_power_on(struct hbtp_data *hbtp, bool on)
|
|
{
|
|
int ret;
|
|
|
|
if (!hbtp->vcc_ana)
|
|
pr_err("%s: analog regulator is not available\n", __func__);
|
|
|
|
if (!hbtp->vcc_dig)
|
|
pr_err("%s: digital regulator is not available\n", __func__);
|
|
|
|
if (!hbtp->vcc_ana && !hbtp->vcc_dig) {
|
|
pr_err("%s: no regulators available\n", __func__);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (!on)
|
|
goto reg_off;
|
|
|
|
if (hbtp->vcc_ana) {
|
|
ret = reg_set_optimum_mode_check(hbtp->vcc_ana,
|
|
hbtp->afe_load_ua);
|
|
if (ret < 0) {
|
|
pr_err("%s: Regulator vcc_ana set_opt failed rc=%d\n",
|
|
__func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = regulator_enable(hbtp->vcc_ana);
|
|
if (ret) {
|
|
pr_err("%s: Regulator vcc_ana enable failed rc=%d\n",
|
|
__func__, ret);
|
|
reg_set_optimum_mode_check(hbtp->vcc_ana, 0);
|
|
return ret;
|
|
}
|
|
}
|
|
if (hbtp->vcc_dig) {
|
|
ret = reg_set_optimum_mode_check(hbtp->vcc_dig,
|
|
hbtp->dig_load_ua);
|
|
if (ret < 0) {
|
|
pr_err("%s: Regulator vcc_dig set_opt failed rc=%d\n",
|
|
__func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = regulator_enable(hbtp->vcc_dig);
|
|
if (ret) {
|
|
pr_err("%s: Regulator vcc_dig enable failed rc=%d\n",
|
|
__func__, ret);
|
|
reg_set_optimum_mode_check(hbtp->vcc_dig, 0);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
|
|
reg_off:
|
|
if (hbtp->vcc_ana) {
|
|
reg_set_optimum_mode_check(hbtp->vcc_ana, 0);
|
|
regulator_disable(hbtp->vcc_ana);
|
|
}
|
|
if (hbtp->vcc_dig) {
|
|
reg_set_optimum_mode_check(hbtp->vcc_dig, 0);
|
|
regulator_disable(hbtp->vcc_dig);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static long hbtp_input_ioctl_handler(struct file *file, unsigned int cmd,
|
|
unsigned long arg, void __user *p)
|
|
{
|
|
int error;
|
|
struct hbtp_input_mt mt_data;
|
|
struct hbtp_input_absinfo absinfo[ABS_MT_LAST - ABS_MT_FIRST + 1];
|
|
struct hbtp_input_key key_data;
|
|
enum hbtp_afe_power_cmd power_cmd;
|
|
|
|
switch (cmd) {
|
|
case HBTP_SET_ABSPARAM:
|
|
if (hbtp && hbtp->input_dev) {
|
|
pr_err("%s: The input device is already created\n",
|
|
__func__);
|
|
return 0;
|
|
}
|
|
|
|
if (copy_from_user(absinfo, (void *)arg,
|
|
sizeof(struct hbtp_input_absinfo) *
|
|
(ABS_MT_LAST - ABS_MT_FIRST + 1))) {
|
|
pr_err("%s: Error copying data for ABS param\n",
|
|
__func__);
|
|
return -EFAULT;
|
|
}
|
|
|
|
error = hbtp_input_create_input_dev(absinfo);
|
|
if (error)
|
|
pr_err("%s, hbtp_input_create_input_dev failed (%d)\n",
|
|
__func__, error);
|
|
break;
|
|
|
|
case HBTP_SET_TOUCHDATA:
|
|
if (!hbtp || !hbtp->input_dev) {
|
|
pr_err("%s: The input device hasn't been created\n",
|
|
__func__);
|
|
return -EFAULT;
|
|
}
|
|
|
|
if (copy_from_user(&mt_data, (void *)arg,
|
|
sizeof(struct hbtp_input_mt))) {
|
|
pr_err("%s: Error copying data\n", __func__);
|
|
return -EFAULT;
|
|
}
|
|
|
|
hbtp_input_report_events(hbtp, &mt_data);
|
|
error = 0;
|
|
break;
|
|
|
|
case HBTP_SET_POWERSTATE:
|
|
if (!hbtp || !hbtp->input_dev) {
|
|
pr_err("%s: The input device hasn't been created\n",
|
|
__func__);
|
|
return -EFAULT;
|
|
}
|
|
|
|
if (copy_from_user(&power_cmd, (void *)arg,
|
|
sizeof(enum hbtp_afe_power_cmd))) {
|
|
pr_err("%s: Error copying data\n", __func__);
|
|
return -EFAULT;
|
|
}
|
|
|
|
switch (power_cmd) {
|
|
case HBTP_AFE_POWER_ON:
|
|
error = hbtp_pdev_power_on(hbtp, true);
|
|
if (error)
|
|
pr_err("%s: failed to power on\n", __func__);
|
|
break;
|
|
case HBTP_AFE_POWER_OFF:
|
|
error = hbtp_pdev_power_on(hbtp, false);
|
|
if (error)
|
|
pr_err("%s: failed to power off\n", __func__);
|
|
break;
|
|
default:
|
|
pr_err("%s: Unsupported command for power state, %d\n",
|
|
__func__, power_cmd);
|
|
return -EINVAL;
|
|
}
|
|
break;
|
|
|
|
case HBTP_SET_KEYDATA:
|
|
if (!hbtp || !hbtp->input_dev) {
|
|
pr_err("%s: The input device hasn't been created\n",
|
|
__func__);
|
|
return -EFAULT;
|
|
}
|
|
|
|
if (copy_from_user(&key_data, (void *)arg,
|
|
sizeof(struct hbtp_input_key))) {
|
|
pr_err("%s: Error copying data for key info\n",
|
|
__func__);
|
|
return -EFAULT;
|
|
}
|
|
|
|
input_report_key(hbtp->input_dev, key_data.code,
|
|
key_data.value);
|
|
input_sync(hbtp->input_dev);
|
|
break;
|
|
|
|
default:
|
|
pr_err("%s: Unsupported ioctl command %u\n", __func__, cmd);
|
|
error = -EINVAL;
|
|
break;
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
static long hbtp_input_ioctl(struct file *file, unsigned int cmd,
|
|
unsigned long arg)
|
|
{
|
|
return hbtp_input_ioctl_handler(file, cmd, arg, (void __user *)arg);
|
|
}
|
|
|
|
#ifdef CONFIG_COMPAT
|
|
static long hbtp_input_compat_ioctl(struct file *file, unsigned int cmd,
|
|
unsigned long arg)
|
|
{
|
|
return hbtp_input_ioctl_handler(file, cmd, arg, compat_ptr(arg));
|
|
}
|
|
#endif
|
|
|
|
static const struct file_operations hbtp_input_fops = {
|
|
.owner = THIS_MODULE,
|
|
.open = hbtp_input_open,
|
|
.release = hbtp_input_release,
|
|
.unlocked_ioctl = hbtp_input_ioctl,
|
|
#ifdef CONFIG_COMPAT
|
|
.compat_ioctl = hbtp_input_compat_ioctl,
|
|
#endif
|
|
};
|
|
|
|
static struct miscdevice hbtp_input_misc = {
|
|
.fops = &hbtp_input_fops,
|
|
.minor = MISC_DYNAMIC_MINOR,
|
|
.name = HBTP_INPUT_NAME,
|
|
};
|
|
MODULE_ALIAS_MISCDEV(MISC_DYNAMIC_MINOR);
|
|
MODULE_ALIAS("devname:" HBTP_INPUT_NAME);
|
|
|
|
#ifdef CONFIG_OF
|
|
static int hbtp_parse_dt(struct device *dev)
|
|
{
|
|
int rc;
|
|
struct device_node *np = dev->of_node;
|
|
u32 temp_val;
|
|
|
|
if (of_find_property(np, "vcc_ana-supply", NULL))
|
|
hbtp->manage_afe_power_ana = true;
|
|
if (of_find_property(np, "vcc_dig-supply", NULL))
|
|
hbtp->manage_power_dig = true;
|
|
|
|
if (hbtp->manage_afe_power_ana) {
|
|
rc = of_property_read_u32(np, "qcom,afe-load", &temp_val);
|
|
if (!rc) {
|
|
hbtp->afe_load_ua = (int) temp_val;
|
|
} else {
|
|
dev_err(dev, "Unable to read AFE load\n");
|
|
return rc;
|
|
}
|
|
|
|
rc = of_property_read_u32(np, "qcom,afe-vtg-min", &temp_val);
|
|
if (!rc) {
|
|
hbtp->afe_vtg_min_uv = (int) temp_val;
|
|
} else {
|
|
dev_err(dev, "Unable to read AFE min voltage\n");
|
|
return rc;
|
|
}
|
|
|
|
rc = of_property_read_u32(np, "qcom,afe-vtg-max", &temp_val);
|
|
if (!rc) {
|
|
hbtp->afe_vtg_max_uv = (int) temp_val;
|
|
} else {
|
|
dev_err(dev, "Unable to read AFE max voltage\n");
|
|
return rc;
|
|
}
|
|
}
|
|
if (hbtp->manage_power_dig) {
|
|
rc = of_property_read_u32(np, "qcom,dig-load", &temp_val);
|
|
if (!rc) {
|
|
hbtp->dig_load_ua = (int) temp_val;
|
|
} else {
|
|
dev_err(dev, "Unable to read digital load\n");
|
|
return rc;
|
|
}
|
|
|
|
rc = of_property_read_u32(np, "qcom,dig-vtg-min", &temp_val);
|
|
if (!rc) {
|
|
hbtp->dig_vtg_min_uv = (int) temp_val;
|
|
} else {
|
|
dev_err(dev, "Unable to read digital min voltage\n");
|
|
return rc;
|
|
}
|
|
|
|
rc = of_property_read_u32(np, "qcom,dig-vtg-max", &temp_val);
|
|
if (!rc) {
|
|
hbtp->dig_vtg_max_uv = (int) temp_val;
|
|
} else {
|
|
dev_err(dev, "Unable to read digital max voltage\n");
|
|
return rc;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
#else
|
|
static int hbtp_parse_dt(struct device *dev)
|
|
{
|
|
return -ENODEV;
|
|
}
|
|
#endif
|
|
|
|
static int hbtp_pdev_probe(struct platform_device *pdev)
|
|
{
|
|
int error;
|
|
struct regulator *vcc_ana, *vcc_dig;
|
|
|
|
if (pdev->dev.of_node) {
|
|
error = hbtp_parse_dt(&pdev->dev);
|
|
if (error) {
|
|
pr_err("%s: parse dt failed, rc=%d\n", __func__, error);
|
|
return error;
|
|
}
|
|
}
|
|
|
|
if (hbtp->manage_afe_power_ana) {
|
|
vcc_ana = regulator_get(&pdev->dev, "vcc_ana");
|
|
if (IS_ERR(vcc_ana)) {
|
|
error = PTR_ERR(vcc_ana);
|
|
pr_err("%s: regulator get failed vcc_ana rc=%d\n",
|
|
__func__, error);
|
|
return error;
|
|
}
|
|
|
|
if (regulator_count_voltages(vcc_ana) > 0) {
|
|
error = regulator_set_voltage(vcc_ana,
|
|
hbtp->afe_vtg_min_uv, hbtp->afe_vtg_max_uv);
|
|
if (error) {
|
|
pr_err("%s: regulator set vtg failed vcc_ana rc=%d\n",
|
|
__func__, error);
|
|
regulator_put(vcc_ana);
|
|
return error;
|
|
}
|
|
}
|
|
hbtp->vcc_ana = vcc_ana;
|
|
}
|
|
|
|
if (hbtp->manage_power_dig) {
|
|
vcc_dig = regulator_get(&pdev->dev, "vcc_dig");
|
|
if (IS_ERR(vcc_dig)) {
|
|
error = PTR_ERR(vcc_dig);
|
|
pr_err("%s: regulator get failed vcc_dig rc=%d\n",
|
|
__func__, error);
|
|
return error;
|
|
}
|
|
|
|
if (regulator_count_voltages(vcc_dig) > 0) {
|
|
error = regulator_set_voltage(vcc_dig,
|
|
hbtp->dig_vtg_min_uv, hbtp->dig_vtg_max_uv);
|
|
if (error) {
|
|
pr_err("%s: regulator set vtg failed vcc_dig rc=%d\n",
|
|
__func__, error);
|
|
regulator_put(vcc_dig);
|
|
return error;
|
|
}
|
|
}
|
|
hbtp->vcc_dig = vcc_dig;
|
|
}
|
|
|
|
hbtp->pdev = pdev;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int hbtp_pdev_remove(struct platform_device *pdev)
|
|
{
|
|
if (hbtp->vcc_ana || hbtp->vcc_dig) {
|
|
hbtp_pdev_power_on(hbtp, false);
|
|
if (hbtp->vcc_ana)
|
|
regulator_put(hbtp->vcc_ana);
|
|
if (hbtp->vcc_dig)
|
|
regulator_put(hbtp->vcc_dig);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_OF
|
|
static struct of_device_id hbtp_match_table[] = {
|
|
{ .compatible = "qcom,hbtp-input",},
|
|
{ },
|
|
};
|
|
#else
|
|
#define hbtp_match_table NULL
|
|
#endif
|
|
|
|
static struct platform_driver hbtp_pdev_driver = {
|
|
.probe = hbtp_pdev_probe,
|
|
.remove = hbtp_pdev_remove,
|
|
.driver = {
|
|
.name = "hbtp",
|
|
.owner = THIS_MODULE,
|
|
.of_match_table = hbtp_match_table,
|
|
},
|
|
};
|
|
|
|
static int __init hbtp_init(void)
|
|
{
|
|
int error;
|
|
|
|
hbtp = kzalloc(sizeof(struct hbtp_data), GFP_KERNEL);
|
|
if (!hbtp)
|
|
return -ENOMEM;
|
|
|
|
mutex_init(&hbtp->mutex);
|
|
|
|
error = misc_register(&hbtp_input_misc);
|
|
if (error) {
|
|
pr_err("%s: misc_register failed\n", HBTP_INPUT_NAME);
|
|
goto err_misc_reg;
|
|
}
|
|
|
|
#if defined(CONFIG_FB)
|
|
hbtp->fb_notif.notifier_call = fb_notifier_callback;
|
|
error = fb_register_client(&hbtp->fb_notif);
|
|
if (error) {
|
|
pr_err("%s: Unable to register fb_notifier: %d\n",
|
|
HBTP_INPUT_NAME, error);
|
|
goto err_fb_reg;
|
|
}
|
|
#endif
|
|
|
|
error = platform_driver_register(&hbtp_pdev_driver);
|
|
if (error) {
|
|
pr_err("Failed to register platform driver: %d\n", error);
|
|
goto err_platform_drv_reg;
|
|
}
|
|
|
|
return 0;
|
|
|
|
err_platform_drv_reg:
|
|
#if defined(CONFIG_FB)
|
|
fb_unregister_client(&hbtp->fb_notif);
|
|
err_fb_reg:
|
|
#endif
|
|
misc_deregister(&hbtp_input_misc);
|
|
err_misc_reg:
|
|
kfree(hbtp);
|
|
|
|
return error;
|
|
}
|
|
|
|
static void __exit hbtp_exit(void)
|
|
{
|
|
misc_deregister(&hbtp_input_misc);
|
|
if (hbtp->input_dev)
|
|
input_unregister_device(hbtp->input_dev);
|
|
|
|
#if defined(CONFIG_FB)
|
|
fb_unregister_client(&hbtp->fb_notif);
|
|
#endif
|
|
|
|
platform_driver_unregister(&hbtp_pdev_driver);
|
|
|
|
kfree(hbtp);
|
|
}
|
|
|
|
MODULE_DESCRIPTION("Kernel driver to support host based touch processing");
|
|
MODULE_LICENSE("GPLv2");
|
|
|
|
module_init(hbtp_init);
|
|
module_exit(hbtp_exit);
|