686 lines
16 KiB
C
686 lines
16 KiB
C
/*
|
|
* Copyright (c) 2019-2020, 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/module.h>
|
|
#include <linux/property.h>
|
|
#include <linux/usb/composite.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/of.h>
|
|
#include <linux/kernel.h>
|
|
|
|
struct qti_usb_function {
|
|
struct usb_function_instance *fi;
|
|
struct usb_function *f;
|
|
|
|
struct list_head list;
|
|
};
|
|
|
|
#define MAX_FUNC_NAME_LEN 48
|
|
#define MAX_CFG_NAME_LEN 128
|
|
|
|
#define QW_SIGN_LEN 14
|
|
|
|
char qw_sign[QW_SIGN_LEN] = "MSFT100";
|
|
|
|
struct qti_usb_config {
|
|
struct usb_configuration c;
|
|
|
|
/* List of functions bound to this config */
|
|
struct list_head func_list;
|
|
/* List of qti_usb_functions bound to this config */
|
|
struct list_head qti_funcs;
|
|
};
|
|
|
|
struct qti_usb_gadget {
|
|
struct usb_composite_dev cdev;
|
|
struct usb_composite_driver composite;
|
|
|
|
const char *composition_funcs;
|
|
bool enabled;
|
|
struct device *dev;
|
|
struct device_node *cfg_node;
|
|
};
|
|
|
|
extern char *saved_command_line;
|
|
static char manufacturer_string[256] = "Qualcomm Technologies, Inc";
|
|
module_param_string(manufacturer, manufacturer_string,
|
|
sizeof(manufacturer_string), 0644);
|
|
MODULE_PARM_DESC(quirks, "String representing name of manufacturer");
|
|
|
|
static char product_string[256] = "USB_device!_SN:12345";
|
|
module_param_string(product, product_string,
|
|
sizeof(product_string), 0644);
|
|
MODULE_PARM_DESC(quirks, "String representing product string");
|
|
|
|
static char serialno_string[256] = "12345";
|
|
|
|
static char usb_pid_string[256];
|
|
module_param_string(usb_pid, usb_pid_string, sizeof(usb_pid_string), 0644);
|
|
MODULE_PARM_DESC(quirks, "String representing product id");
|
|
|
|
/* String Table */
|
|
static struct usb_string strings_dev[] = {
|
|
[USB_GADGET_MANUFACTURER_IDX].s = manufacturer_string,
|
|
[USB_GADGET_PRODUCT_IDX].s = product_string,
|
|
[USB_GADGET_SERIAL_IDX].s = serialno_string,
|
|
{ } /* end of list */
|
|
};
|
|
|
|
static struct usb_gadget_strings stringtab_dev = {
|
|
.language = 0x0409, /* en-us */
|
|
.strings = strings_dev,
|
|
};
|
|
|
|
static struct usb_gadget_strings *dev_strings[] = {
|
|
&stringtab_dev,
|
|
NULL,
|
|
};
|
|
|
|
static void qti_configs_remove_funcs(struct qti_usb_gadget *qg)
|
|
{
|
|
struct usb_configuration *c;
|
|
|
|
list_for_each_entry(c, &qg->cdev.configs, list) {
|
|
struct qti_usb_config *cfg;
|
|
struct usb_function *f, *tmp;
|
|
|
|
cfg = container_of(c, struct qti_usb_config, c);
|
|
|
|
list_for_each_entry_safe_reverse(f, tmp, &c->functions, list) {
|
|
|
|
list_move(&f->list, &cfg->func_list);
|
|
if (f->unbind) {
|
|
dev_dbg(&qg->cdev.gadget->dev,
|
|
"unbind function '%s'/%pK\n",
|
|
f->name, f);
|
|
f->unbind(c, f);
|
|
}
|
|
}
|
|
c->fullspeed = 0;
|
|
c->highspeed = 0;
|
|
c->superspeed = 0;
|
|
c->superspeed_plus = 0;
|
|
c->next_interface_id = 0;
|
|
memset(c->interface, 0, sizeof(c->interface));
|
|
}
|
|
}
|
|
|
|
static int qti_composite_bind(struct usb_gadget *gadget,
|
|
struct usb_gadget_driver *gdriver)
|
|
{
|
|
struct usb_composite_driver *composite = to_cdriver(gdriver);
|
|
struct qti_usb_gadget *qg = container_of(composite,
|
|
struct qti_usb_gadget, composite);
|
|
struct usb_composite_dev *cdev = &qg->cdev;
|
|
struct usb_configuration *c;
|
|
struct usb_string *s;
|
|
int ret = -EINVAL;
|
|
|
|
cdev->gadget = gadget;
|
|
set_gadget_data(gadget, cdev);
|
|
spin_lock_init(&qg->cdev.lock);
|
|
|
|
ret = composite_dev_prepare(composite, cdev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (list_empty(&cdev->configs)) {
|
|
pr_err("No configurations found in %s.\n", composite->name);
|
|
ret = -EINVAL;
|
|
goto composite_cleanup;
|
|
}
|
|
|
|
list_for_each_entry(c, &cdev->configs, list) {
|
|
struct qti_usb_config *qcfg;
|
|
|
|
qcfg = container_of(c, struct qti_usb_config, c);
|
|
if (list_empty(&qcfg->func_list)) {
|
|
pr_err("Config %s/%d of %s doesn't have a function.\n",
|
|
c->label, c->bConfigurationValue,
|
|
qg->composite.name);
|
|
goto composite_cleanup;
|
|
}
|
|
}
|
|
|
|
s = usb_gstrings_attach(cdev, dev_strings, USB_GADGET_FIRST_AVAIL_IDX);
|
|
if (IS_ERR(s)) {
|
|
ret = PTR_ERR(s);
|
|
goto composite_cleanup;
|
|
}
|
|
|
|
cdev->desc.iManufacturer = s[USB_GADGET_MANUFACTURER_IDX].id;
|
|
cdev->desc.iProduct = s[USB_GADGET_PRODUCT_IDX].id;
|
|
cdev->desc.iSerialNumber = s[USB_GADGET_SERIAL_IDX].id;
|
|
|
|
/* Go through all configs, attach all functions */
|
|
list_for_each_entry(c, &qg->cdev.configs, list) {
|
|
struct qti_usb_config *qcfg;
|
|
struct usb_function *f, *tmp;
|
|
|
|
qcfg = container_of(c, struct qti_usb_config, c);
|
|
|
|
list_for_each_entry_safe(f, tmp, &qcfg->func_list, list) {
|
|
list_del(&f->list);
|
|
ret = usb_add_function(c, f);
|
|
if (ret) {
|
|
list_add(&f->list, &qcfg->func_list);
|
|
goto remove_funcs;
|
|
}
|
|
}
|
|
usb_ep_autoconfig_reset(cdev->gadget);
|
|
}
|
|
|
|
if (cdev->use_os_string) {
|
|
ret = composite_os_desc_req_prepare(cdev, gadget->ep0);
|
|
if (ret)
|
|
goto remove_funcs;
|
|
}
|
|
|
|
usb_ep_autoconfig_reset(cdev->gadget);
|
|
|
|
return 0;
|
|
|
|
remove_funcs:
|
|
qti_configs_remove_funcs(qg);
|
|
composite_cleanup:
|
|
composite_dev_cleanup(cdev);
|
|
return ret;
|
|
}
|
|
|
|
static void qti_composite_unbind(struct usb_gadget *gadget)
|
|
{
|
|
struct usb_composite_dev *cdev;
|
|
struct qti_usb_gadget *qg;
|
|
|
|
cdev = get_gadget_data(gadget);
|
|
qg = container_of(cdev, struct qti_usb_gadget, cdev);
|
|
|
|
qti_configs_remove_funcs(qg);
|
|
composite_dev_cleanup(cdev);
|
|
usb_ep_autoconfig_reset(cdev->gadget);
|
|
cdev->gadget = NULL;
|
|
set_gadget_data(gadget, NULL);
|
|
}
|
|
|
|
static const struct usb_gadget_driver qti_gadget_driver = {
|
|
.bind = qti_composite_bind,
|
|
.unbind = qti_composite_unbind,
|
|
.setup = composite_setup,
|
|
.reset = composite_disconnect,
|
|
.disconnect = composite_disconnect,
|
|
.suspend = composite_suspend,
|
|
.resume = composite_resume,
|
|
|
|
.max_speed = USB_SPEED_SUPER_PLUS,
|
|
.driver = {
|
|
.owner = THIS_MODULE,
|
|
.name = "qti-gadget",
|
|
},
|
|
};
|
|
|
|
static void qti_usb_funcs_free(struct qti_usb_config *qcfg)
|
|
{
|
|
struct usb_function *f, *tmp;
|
|
struct qti_usb_function *qf, *qf_tmp;
|
|
|
|
list_for_each_entry_safe(f, tmp, &qcfg->func_list, list) {
|
|
list_del(&f->list);
|
|
usb_put_function(f);
|
|
|
|
/* find corresponding function_instance and free it */
|
|
list_for_each_entry_safe(qf, qf_tmp, &qcfg->qti_funcs, list) {
|
|
if (qf->f == f) {
|
|
list_del(&qf->list);
|
|
usb_put_function_instance(qf->fi);
|
|
kfree(qf);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void qti_cleanup_configs_funcs(struct qti_usb_gadget *qg)
|
|
{
|
|
struct usb_configuration *c, *c_tmp;
|
|
|
|
list_for_each_entry_safe(c, c_tmp, &qg->cdev.configs, list) {
|
|
struct qti_usb_config *qcfg;
|
|
|
|
qcfg = container_of(c, struct qti_usb_config, c);
|
|
WARN_ON(!list_empty(&qcfg->c.functions));
|
|
|
|
qti_usb_funcs_free(qcfg);
|
|
|
|
list_del(&qcfg->c.list);
|
|
kfree(qcfg->c.label);
|
|
kfree(qcfg);
|
|
}
|
|
}
|
|
|
|
static int qti_usb_func_alloc(struct qti_usb_config *qcfg,
|
|
const char *name)
|
|
{
|
|
struct qti_usb_function *qf;
|
|
struct usb_function_instance *fi;
|
|
struct usb_composite_dev *cdev = qcfg->c.cdev;
|
|
struct usb_function *f;
|
|
char buf[MAX_FUNC_NAME_LEN];
|
|
char *func_name;
|
|
char *instance_name;
|
|
int ret;
|
|
|
|
ret = snprintf(buf, MAX_FUNC_NAME_LEN, "%s", name);
|
|
if (ret >= MAX_FUNC_NAME_LEN)
|
|
return -ENAMETOOLONG;
|
|
|
|
func_name = buf;
|
|
instance_name = strnchr(func_name, MAX_FUNC_NAME_LEN, '.');
|
|
if (!instance_name) {
|
|
pr_err("Can't find . in <func>.<instance>:%s\n", buf);
|
|
return -EINVAL;
|
|
}
|
|
*instance_name = '\0';
|
|
instance_name++;
|
|
|
|
qf = kzalloc(sizeof(*qf), GFP_KERNEL);
|
|
if (!qf)
|
|
return -ENOMEM;
|
|
|
|
fi = usb_get_function_instance(func_name);
|
|
if (IS_ERR(fi)) {
|
|
kfree(qf);
|
|
return PTR_ERR(fi);
|
|
}
|
|
qf->fi = fi;
|
|
|
|
if (fi->set_inst_name) {
|
|
ret = fi->set_inst_name(fi, instance_name);
|
|
if (ret) {
|
|
kfree(qf);
|
|
usb_put_function_instance(fi);
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
f = usb_get_function(fi);
|
|
if (IS_ERR(f)) {
|
|
kfree(qf);
|
|
usb_put_function_instance(fi);
|
|
return PTR_ERR(f);
|
|
}
|
|
qf->f = f;
|
|
list_add_tail(&qf->list, &qcfg->qti_funcs);
|
|
|
|
/* stash the function until we bind it to the gadget */
|
|
list_add_tail(&f->list, &qcfg->func_list);
|
|
|
|
if (!strcmp(instance_name, "mbim")) {
|
|
cdev->os_desc_config = &qcfg->c;
|
|
cdev->use_os_string = true;
|
|
cdev->b_vendor_code = 0xA5;
|
|
memcpy(cdev->qw_sign, qw_sign, QW_SIGN_LEN);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int qti_usb_funcs_alloc(struct qti_usb_config *qcfg,
|
|
const char *funcs)
|
|
{
|
|
char buf[MAX_CFG_NAME_LEN];
|
|
char *fn_name, *next_fn;
|
|
int ret = 0;
|
|
|
|
ret = snprintf(buf, MAX_CFG_NAME_LEN, "%s", funcs);
|
|
if (ret >= MAX_CFG_NAME_LEN)
|
|
return -ENAMETOOLONG;
|
|
|
|
fn_name = buf;
|
|
while (fn_name) {
|
|
next_fn = strnchr(fn_name, MAX_CFG_NAME_LEN, ',');
|
|
if (next_fn)
|
|
*next_fn++ = '\0';
|
|
|
|
ret = qti_usb_func_alloc(qcfg, fn_name);
|
|
if (ret) {
|
|
qti_usb_funcs_free(qcfg);
|
|
break;
|
|
}
|
|
|
|
fn_name = next_fn;
|
|
};
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int qti_usb_config_add(struct qti_usb_gadget *gadget,
|
|
const char *name, u8 num)
|
|
{
|
|
struct qti_usb_config *qcfg;
|
|
int ret = 0, val = 0;
|
|
|
|
qcfg = kzalloc(sizeof(*qcfg), GFP_KERNEL);
|
|
if (!qcfg)
|
|
return -ENOMEM;
|
|
|
|
qcfg->c.label = kstrdup(name, GFP_KERNEL);
|
|
if (!qcfg->c.label) {
|
|
ret = -ENOMEM;
|
|
goto free_cfg;
|
|
}
|
|
qcfg->c.bConfigurationValue = num;
|
|
qcfg->c.bmAttributes = USB_CONFIG_ATT_ONE;
|
|
qcfg->c.MaxPower = CONFIG_USB_GADGET_VBUS_DRAW;
|
|
|
|
ret = of_property_read_u32(gadget->cfg_node, "qcom,bmAttributes", &val);
|
|
if (!ret)
|
|
qcfg->c.bmAttributes = (u8)val;
|
|
|
|
INIT_LIST_HEAD(&qcfg->func_list);
|
|
INIT_LIST_HEAD(&qcfg->qti_funcs);
|
|
|
|
ret = usb_add_config_only(&gadget->cdev, &qcfg->c);
|
|
if (ret)
|
|
goto free_label;
|
|
|
|
ret = qti_usb_funcs_alloc(qcfg, name);
|
|
if (ret)
|
|
goto cfg_del;
|
|
|
|
return ret;
|
|
|
|
cfg_del:
|
|
list_del(&qcfg->c.list);
|
|
free_label:
|
|
kfree(qcfg->c.label);
|
|
free_cfg:
|
|
kfree(qcfg);
|
|
return ret;
|
|
|
|
}
|
|
|
|
static int qti_usb_configs_make(struct qti_usb_gadget *gadget,
|
|
const char *cfgs)
|
|
{
|
|
char buf[MAX_CFG_NAME_LEN];
|
|
char *cfg_name, *next_cfg;
|
|
int ret = 0;
|
|
u8 num = 1;
|
|
|
|
ret = snprintf(buf, MAX_CFG_NAME_LEN, "%s", cfgs);
|
|
if (ret >= MAX_CFG_NAME_LEN)
|
|
return -ENAMETOOLONG;
|
|
|
|
cfg_name = buf;
|
|
while (cfg_name) {
|
|
next_cfg = strnchr(cfg_name, MAX_CFG_NAME_LEN, '|');
|
|
if (next_cfg)
|
|
*next_cfg++ = '\0';
|
|
|
|
ret = qti_usb_config_add(gadget, cfg_name, num);
|
|
if (ret)
|
|
break;
|
|
|
|
cfg_name = next_cfg;
|
|
num++;
|
|
};
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int qti_gadget_register(struct qti_usb_gadget *qg)
|
|
{
|
|
int ret;
|
|
|
|
if (qg->enabled)
|
|
return -EINVAL;
|
|
|
|
ret = qti_usb_configs_make(qg, qg->composition_funcs);
|
|
if (ret)
|
|
return ret;
|
|
|
|
qg->cdev.desc.bLength = USB_DT_DEVICE_SIZE;
|
|
qg->cdev.desc.bDescriptorType = USB_DT_DEVICE;
|
|
qg->cdev.desc.bcdDevice = cpu_to_le16(get_default_bcdDevice());
|
|
|
|
qg->composite.gadget_driver = qti_gadget_driver;
|
|
qg->composite.max_speed = qti_gadget_driver.max_speed;
|
|
|
|
qg->composite.gadget_driver.function = kstrdup("qti-gadget",
|
|
GFP_KERNEL);
|
|
qg->composite.name = qg->composite.gadget_driver.function;
|
|
|
|
if (!qg->composite.gadget_driver.function) {
|
|
ret = -ENOMEM;
|
|
goto free_configs;
|
|
}
|
|
|
|
ret = usb_gadget_probe_driver(&qg->composite.gadget_driver);
|
|
if (ret)
|
|
goto free_name;
|
|
|
|
qg->enabled = true;
|
|
|
|
return 0;
|
|
|
|
free_name:
|
|
kfree(qg->composite.gadget_driver.function);
|
|
free_configs:
|
|
qti_cleanup_configs_funcs(qg);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void qti_gadget_unregister(struct qti_usb_gadget *qg)
|
|
{
|
|
if (!qg->enabled)
|
|
return;
|
|
|
|
usb_gadget_unregister_driver(&qg->composite.gadget_driver);
|
|
kfree(qg->composite.gadget_driver.function);
|
|
qti_cleanup_configs_funcs(qg);
|
|
|
|
qg->enabled = false;
|
|
}
|
|
|
|
static int qti_gadget_get_properties(struct qti_usb_gadget *gadget)
|
|
{
|
|
struct device *dev = gadget->dev;
|
|
struct device_node *child = NULL;
|
|
int ret = 0, val = 0, pid = 0;
|
|
char *start = NULL, *end = NULL;
|
|
|
|
ret = device_property_read_u32(dev, "qcom,vid", &val);
|
|
if (ret) {
|
|
dev_err(dev, "USB gadget idVendor not specified\n");
|
|
return ret;
|
|
}
|
|
gadget->cdev.desc.idVendor = (u16)val;
|
|
|
|
ret = device_property_read_u32(dev, "qcom,class", &val);
|
|
if (!ret)
|
|
gadget->cdev.desc.bDeviceClass = (u8)val;
|
|
|
|
ret = device_property_read_u32(dev, "qcom,subclass", &val);
|
|
if (!ret)
|
|
gadget->cdev.desc.bDeviceSubClass = (u8)val;
|
|
|
|
ret = device_property_read_u32(dev, "qcom,protocol", &val);
|
|
if (!ret)
|
|
gadget->cdev.desc.bDeviceProtocol = (u8)val;
|
|
|
|
/* Check if pid passed via cmdline which takes precedence */
|
|
if (usb_pid_string[0] != 0) {
|
|
ret = kstrtoint(usb_pid_string, 16, &val);
|
|
if (ret)
|
|
return ret;
|
|
} else {
|
|
ret = device_property_read_u32(dev, "qcom,default-pid", &val);
|
|
if (ret) {
|
|
dev_dbg(dev, "USB gadget default-pid not specified\n");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
pid = val;
|
|
|
|
/* Extract serial # from kernel cmdline */
|
|
start = strnstr(saved_command_line, "androidboot.serialno=",
|
|
strlen(saved_command_line));
|
|
if (start) {
|
|
/* If serial # is at the end of the kernel cmdline
|
|
* it will be terminated by NULL character
|
|
*/
|
|
end = strchrnul(start, ' ');
|
|
start += strlen("androidboot.serialno=");
|
|
strlcpy(serialno_string, start, (end - start)+1);
|
|
}
|
|
|
|
/* Product string contains a space but in kernel
|
|
* cmdline it will be passed with a special character
|
|
* '!' instead of space to maintain the design of
|
|
* cmdline parameters
|
|
*/
|
|
strreplace(product_string, '!', ' ');
|
|
|
|
/* Go through all the child nodes and find matching pid */
|
|
while ((child = of_get_next_child(dev->of_node, child)) != NULL) {
|
|
of_property_read_u32(child, "qcom,pid", &val);
|
|
if (val == pid) {
|
|
of_property_read_string(child, "qcom,composition",
|
|
&gadget->composition_funcs);
|
|
|
|
gadget->cfg_node = child;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Check if couldn't find a matching composition */
|
|
if (gadget->composition_funcs == NULL)
|
|
return -EINVAL;
|
|
|
|
/* bail out if ffs is specified and let userspace handle it */
|
|
if (strstr(gadget->composition_funcs, "ffs.")) {
|
|
dev_err(dev, "user should enable ffs\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
gadget->cdev.desc.idProduct = (u16)pid;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static ssize_t enabled_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct qti_usb_gadget *qg = dev_get_drvdata(dev);
|
|
|
|
return snprintf(buf, PAGE_SIZE, "%c\n",
|
|
qg->enabled ? 'Y' : 'N');
|
|
}
|
|
|
|
static ssize_t enabled_store(struct device *dev,
|
|
struct device_attribute *attr, const char *buf, size_t count)
|
|
{
|
|
struct qti_usb_gadget *qg = dev_get_drvdata(dev);
|
|
bool enable;
|
|
int ret;
|
|
|
|
ret = strtobool(buf, &enable);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (enable)
|
|
qti_gadget_register(qg);
|
|
else
|
|
qti_gadget_unregister(qg);
|
|
|
|
return count;
|
|
}
|
|
static DEVICE_ATTR_RW(enabled);
|
|
|
|
static int qti_gadget_probe(struct platform_device *pdev)
|
|
{
|
|
int ret;
|
|
struct device *dev = &pdev->dev;
|
|
struct qti_usb_gadget *gadget;
|
|
|
|
gadget = devm_kzalloc(dev, sizeof(*gadget), GFP_KERNEL);
|
|
if (!gadget)
|
|
return -ENOMEM;
|
|
|
|
platform_set_drvdata(pdev, gadget);
|
|
gadget->dev = dev;
|
|
INIT_LIST_HEAD(&gadget->cdev.configs);
|
|
INIT_LIST_HEAD(&gadget->cdev.gstrings);
|
|
|
|
ret = qti_gadget_get_properties(gadget);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = qti_gadget_register(gadget);
|
|
if (ret)
|
|
return ret;
|
|
|
|
device_create_file(&pdev->dev, &dev_attr_enabled);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int qti_gadget_remove(struct platform_device *pdev)
|
|
{
|
|
struct qti_usb_gadget *qg = platform_get_drvdata(pdev);
|
|
|
|
device_remove_file(&pdev->dev, &dev_attr_enabled);
|
|
qti_gadget_unregister(qg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id qti_gadget_dt_match[] = {
|
|
{
|
|
.compatible = "qcom,usb-gadget",
|
|
},
|
|
{ },
|
|
};
|
|
MODULE_DEVICE_TABLE(of, qti_gadget_dt_match);
|
|
|
|
static struct platform_driver qti_gadget_platform_driver = {
|
|
.driver = {
|
|
.name = "qti_usb_gadget",
|
|
.of_match_table = qti_gadget_dt_match,
|
|
},
|
|
.probe = qti_gadget_probe,
|
|
.remove = qti_gadget_remove,
|
|
};
|
|
|
|
static int __init gadget_qti_init(void)
|
|
{
|
|
int ret;
|
|
|
|
ret = platform_driver_register(&qti_gadget_platform_driver);
|
|
if (ret) {
|
|
pr_err("%s: Failed to register qti gadget platform driver\n",
|
|
__func__);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
module_init(gadget_qti_init);
|
|
|
|
static void __exit gadget_qti_exit(void)
|
|
{
|
|
platform_driver_unregister(&qti_gadget_platform_driver);
|
|
}
|
|
module_exit(gadget_qti_exit);
|