You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
547 lines
13 KiB
547 lines
13 KiB
/* |
|
* class-dual-role.c |
|
* |
|
* Copyright (C) 2015 Google, Inc. |
|
* |
|
* This software is licensed under the terms of the GNU General Public |
|
* License version 2, as published by the Free Software Foundation, and |
|
* may be copied, distributed, and modified under those terms. |
|
* |
|
* 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/ctype.h> |
|
#include <linux/device.h> |
|
#include <linux/usb/class-dual-role.h> |
|
#include <linux/err.h> |
|
#include <linux/init.h> |
|
#include <linux/module.h> |
|
#include <linux/slab.h> |
|
#include <linux/stat.h> |
|
#include <linux/types.h> |
|
|
|
#define DUAL_ROLE_NOTIFICATION_TIMEOUT 2000 |
|
|
|
static ssize_t dual_role_store_property(struct device *dev, |
|
struct device_attribute *attr, |
|
const char *buf, size_t count); |
|
static ssize_t dual_role_show_property(struct device *dev, |
|
struct device_attribute *attr, |
|
char *buf); |
|
|
|
#define DUAL_ROLE_ATTR(_name) \ |
|
{ \ |
|
.attr = { .name = #_name }, \ |
|
.show = dual_role_show_property, \ |
|
.store = dual_role_store_property, \ |
|
} |
|
|
|
static struct device_attribute dual_role_attrs[] = { |
|
DUAL_ROLE_ATTR(supported_modes), |
|
DUAL_ROLE_ATTR(mode), |
|
DUAL_ROLE_ATTR(power_role), |
|
DUAL_ROLE_ATTR(data_role), |
|
DUAL_ROLE_ATTR(powers_vconn), |
|
}; |
|
|
|
struct class *dual_role_class; |
|
EXPORT_SYMBOL_GPL(dual_role_class); |
|
|
|
static struct device_type dual_role_dev_type; |
|
|
|
static char *kstrdupcase(const char *str, gfp_t gfp, bool to_upper) |
|
{ |
|
char *ret, *ustr; |
|
|
|
ustr = ret = kmalloc(strlen(str) + 1, gfp); |
|
|
|
if (!ret) |
|
return NULL; |
|
|
|
while (*str) |
|
*ustr++ = to_upper ? toupper(*str++) : tolower(*str++); |
|
|
|
*ustr = 0; |
|
|
|
return ret; |
|
} |
|
|
|
static void dual_role_changed_work(struct work_struct *work) |
|
{ |
|
struct dual_role_phy_instance *dual_role = |
|
container_of(work, struct dual_role_phy_instance, |
|
changed_work); |
|
|
|
dev_dbg(&dual_role->dev, "%s\n", __func__); |
|
kobject_uevent(&dual_role->dev.kobj, KOBJ_CHANGE); |
|
} |
|
|
|
void dual_role_instance_changed(struct dual_role_phy_instance *dual_role) |
|
{ |
|
dev_dbg(&dual_role->dev, "%s\n", __func__); |
|
pm_wakeup_event(&dual_role->dev, DUAL_ROLE_NOTIFICATION_TIMEOUT); |
|
schedule_work(&dual_role->changed_work); |
|
} |
|
EXPORT_SYMBOL_GPL(dual_role_instance_changed); |
|
|
|
int dual_role_get_property(struct dual_role_phy_instance *dual_role, |
|
enum dual_role_property prop, |
|
unsigned int *val) |
|
{ |
|
return dual_role->desc->get_property(dual_role, prop, val); |
|
} |
|
EXPORT_SYMBOL_GPL(dual_role_get_property); |
|
|
|
int dual_role_set_property(struct dual_role_phy_instance *dual_role, |
|
enum dual_role_property prop, |
|
const unsigned int *val) |
|
{ |
|
if (!dual_role->desc->set_property) |
|
return -ENODEV; |
|
|
|
return dual_role->desc->set_property(dual_role, prop, val); |
|
} |
|
EXPORT_SYMBOL_GPL(dual_role_set_property); |
|
|
|
int dual_role_property_is_writeable(struct dual_role_phy_instance *dual_role, |
|
enum dual_role_property prop) |
|
{ |
|
if (!dual_role->desc->property_is_writeable) |
|
return -ENODEV; |
|
|
|
return dual_role->desc->property_is_writeable(dual_role, prop); |
|
} |
|
EXPORT_SYMBOL_GPL(dual_role_property_is_writeable); |
|
|
|
static int dual_role_match_device_byname(struct device *dev, const void *data) |
|
{ |
|
const char *name = data; |
|
struct dual_role_phy_instance *dual_role = dev_get_drvdata(dev); |
|
|
|
return strcmp(dual_role->desc->name, name) == 0; |
|
} |
|
|
|
struct dual_role_phy_instance |
|
*dual_role_phy_instance_get_byname(const char *name) |
|
{ |
|
struct device *dev = class_find_device(dual_role_class, NULL, name, |
|
dual_role_match_device_byname); |
|
|
|
return dev ? dev_get_drvdata(dev) : NULL; |
|
} |
|
EXPORT_SYMBOL_GPL(dual_role_phy_instance_get_byname); |
|
|
|
static void dual_role_dev_release(struct device *dev) |
|
{ |
|
struct dual_role_phy_instance *dual_role = |
|
container_of(dev, struct dual_role_phy_instance, dev); |
|
pr_debug("device: '%s': %s\n", dev_name(dev), __func__); |
|
kfree(dual_role); |
|
} |
|
|
|
static struct dual_role_phy_instance *__must_check |
|
__dual_role_register(struct device *parent, |
|
const struct dual_role_phy_desc *desc) |
|
{ |
|
struct device *dev; |
|
struct dual_role_phy_instance *dual_role; |
|
int rc; |
|
|
|
dual_role = kzalloc(sizeof(*dual_role), GFP_KERNEL); |
|
if (!dual_role) |
|
return ERR_PTR(-ENOMEM); |
|
|
|
dev = &dual_role->dev; |
|
|
|
device_initialize(dev); |
|
|
|
dev->class = dual_role_class; |
|
dev->type = &dual_role_dev_type; |
|
dev->parent = parent; |
|
dev->release = dual_role_dev_release; |
|
dev_set_drvdata(dev, dual_role); |
|
dual_role->desc = desc; |
|
|
|
rc = dev_set_name(dev, "%s", desc->name); |
|
if (rc) |
|
goto dev_set_name_failed; |
|
|
|
INIT_WORK(&dual_role->changed_work, dual_role_changed_work); |
|
|
|
rc = device_init_wakeup(dev, true); |
|
if (rc) |
|
goto wakeup_init_failed; |
|
|
|
rc = device_add(dev); |
|
if (rc) |
|
goto device_add_failed; |
|
|
|
dual_role_instance_changed(dual_role); |
|
|
|
return dual_role; |
|
|
|
device_add_failed: |
|
device_init_wakeup(dev, false); |
|
wakeup_init_failed: |
|
dev_set_name_failed: |
|
put_device(dev); |
|
kfree(dual_role); |
|
|
|
return ERR_PTR(rc); |
|
} |
|
|
|
static void dual_role_instance_unregister(struct dual_role_phy_instance |
|
*dual_role) |
|
{ |
|
cancel_work_sync(&dual_role->changed_work); |
|
device_init_wakeup(&dual_role->dev, false); |
|
device_unregister(&dual_role->dev); |
|
} |
|
|
|
static void devm_dual_role_release(struct device *dev, void *res) |
|
{ |
|
struct dual_role_phy_instance **dual_role = res; |
|
|
|
dual_role_instance_unregister(*dual_role); |
|
} |
|
|
|
struct dual_role_phy_instance *__must_check |
|
devm_dual_role_instance_register(struct device *parent, |
|
const struct dual_role_phy_desc *desc) |
|
{ |
|
struct dual_role_phy_instance **ptr, *dual_role; |
|
|
|
ptr = devres_alloc(devm_dual_role_release, sizeof(*ptr), GFP_KERNEL); |
|
|
|
if (!ptr) |
|
return ERR_PTR(-ENOMEM); |
|
dual_role = __dual_role_register(parent, desc); |
|
if (IS_ERR(dual_role)) { |
|
devres_free(ptr); |
|
} else { |
|
*ptr = dual_role; |
|
devres_add(parent, ptr); |
|
} |
|
return dual_role; |
|
} |
|
EXPORT_SYMBOL_GPL(devm_dual_role_instance_register); |
|
|
|
static int devm_dual_role_match(struct device *dev, void *res, void *data) |
|
{ |
|
struct dual_role_phy_instance **r = res; |
|
|
|
if (WARN_ON(!r || !*r)) |
|
return 0; |
|
|
|
return *r == data; |
|
} |
|
|
|
void devm_dual_role_instance_unregister(struct device *dev, |
|
struct dual_role_phy_instance |
|
*dual_role) |
|
{ |
|
int rc; |
|
|
|
rc = devres_release(dev, devm_dual_role_release, |
|
devm_dual_role_match, dual_role); |
|
WARN_ON(rc); |
|
} |
|
EXPORT_SYMBOL_GPL(devm_dual_role_instance_unregister); |
|
|
|
void *dual_role_get_drvdata(struct dual_role_phy_instance *dual_role) |
|
{ |
|
return dual_role->drv_data; |
|
} |
|
EXPORT_SYMBOL_GPL(dual_role_get_drvdata); |
|
|
|
/***************** Device attribute functions **************************/ |
|
|
|
/* port type */ |
|
static char *supported_modes_text[] = { |
|
"ufp dfp", "dfp", "ufp" |
|
}; |
|
|
|
/* current mode */ |
|
static char *mode_text[] = { |
|
"ufp", "dfp", "none" |
|
}; |
|
|
|
/* Power role */ |
|
static char *pr_text[] = { |
|
"source", "sink", "none" |
|
}; |
|
|
|
/* Data role */ |
|
static char *dr_text[] = { |
|
"host", "device", "none" |
|
}; |
|
|
|
/* Vconn supply */ |
|
static char *vconn_supply_text[] = { |
|
"n", "y" |
|
}; |
|
|
|
static ssize_t dual_role_show_property(struct device *dev, |
|
struct device_attribute *attr, char *buf) |
|
{ |
|
ssize_t ret = 0; |
|
struct dual_role_phy_instance *dual_role = dev_get_drvdata(dev); |
|
const ptrdiff_t off = attr - dual_role_attrs; |
|
unsigned int value; |
|
|
|
if (off == DUAL_ROLE_PROP_SUPPORTED_MODES) { |
|
value = dual_role->desc->supported_modes; |
|
} else { |
|
ret = dual_role_get_property(dual_role, off, &value); |
|
|
|
if (ret < 0) { |
|
if (ret == -ENODATA) |
|
dev_dbg(dev, |
|
"driver has no data for `%s' property\n", |
|
attr->attr.name); |
|
else if (ret != -ENODEV) |
|
dev_err(dev, |
|
"driver failed to report `%s' property: %zd\n", |
|
attr->attr.name, ret); |
|
return ret; |
|
} |
|
} |
|
|
|
if (off == DUAL_ROLE_PROP_SUPPORTED_MODES) { |
|
BUILD_BUG_ON(DUAL_ROLE_PROP_SUPPORTED_MODES_TOTAL != |
|
ARRAY_SIZE(supported_modes_text)); |
|
if (value < DUAL_ROLE_PROP_SUPPORTED_MODES_TOTAL) |
|
return snprintf(buf, PAGE_SIZE, "%s\n", |
|
supported_modes_text[value]); |
|
else |
|
return -EIO; |
|
} else if (off == DUAL_ROLE_PROP_MODE) { |
|
BUILD_BUG_ON(DUAL_ROLE_PROP_MODE_TOTAL != |
|
ARRAY_SIZE(mode_text)); |
|
if (value < DUAL_ROLE_PROP_MODE_TOTAL) |
|
return snprintf(buf, PAGE_SIZE, "%s\n", |
|
mode_text[value]); |
|
else |
|
return -EIO; |
|
} else if (off == DUAL_ROLE_PROP_PR) { |
|
BUILD_BUG_ON(DUAL_ROLE_PROP_PR_TOTAL != ARRAY_SIZE(pr_text)); |
|
if (value < DUAL_ROLE_PROP_PR_TOTAL) |
|
return snprintf(buf, PAGE_SIZE, "%s\n", |
|
pr_text[value]); |
|
else |
|
return -EIO; |
|
} else if (off == DUAL_ROLE_PROP_DR) { |
|
BUILD_BUG_ON(DUAL_ROLE_PROP_DR_TOTAL != ARRAY_SIZE(dr_text)); |
|
if (value < DUAL_ROLE_PROP_DR_TOTAL) |
|
return snprintf(buf, PAGE_SIZE, "%s\n", |
|
dr_text[value]); |
|
else |
|
return -EIO; |
|
} else if (off == DUAL_ROLE_PROP_VCONN_SUPPLY) { |
|
BUILD_BUG_ON(DUAL_ROLE_PROP_VCONN_SUPPLY_TOTAL != |
|
ARRAY_SIZE(vconn_supply_text)); |
|
if (value < DUAL_ROLE_PROP_VCONN_SUPPLY_TOTAL) |
|
return snprintf(buf, PAGE_SIZE, "%s\n", |
|
vconn_supply_text[value]); |
|
else |
|
return -EIO; |
|
} else |
|
return -EIO; |
|
} |
|
|
|
static ssize_t dual_role_store_property(struct device *dev, |
|
struct device_attribute *attr, |
|
const char *buf, size_t count) |
|
{ |
|
ssize_t ret; |
|
struct dual_role_phy_instance *dual_role = dev_get_drvdata(dev); |
|
const ptrdiff_t off = attr - dual_role_attrs; |
|
unsigned int value; |
|
int total, i; |
|
char *dup_buf, **text_array; |
|
bool result = false; |
|
|
|
dup_buf = kstrdupcase(buf, GFP_KERNEL, false); |
|
switch (off) { |
|
case DUAL_ROLE_PROP_MODE: |
|
total = DUAL_ROLE_PROP_MODE_TOTAL; |
|
text_array = mode_text; |
|
break; |
|
case DUAL_ROLE_PROP_PR: |
|
total = DUAL_ROLE_PROP_PR_TOTAL; |
|
text_array = pr_text; |
|
break; |
|
case DUAL_ROLE_PROP_DR: |
|
total = DUAL_ROLE_PROP_DR_TOTAL; |
|
text_array = dr_text; |
|
break; |
|
case DUAL_ROLE_PROP_VCONN_SUPPLY: |
|
ret = strtobool(dup_buf, &result); |
|
value = result; |
|
if (!ret) |
|
goto setprop; |
|
default: |
|
ret = -EINVAL; |
|
goto error; |
|
} |
|
|
|
for (i = 0; i <= total; i++) { |
|
if (i == total) { |
|
ret = -ENOTSUPP; |
|
goto error; |
|
} |
|
if (!strncmp(*(text_array + i), dup_buf, |
|
strlen(*(text_array + i)))) { |
|
value = i; |
|
break; |
|
} |
|
} |
|
|
|
setprop: |
|
ret = dual_role->desc->set_property(dual_role, off, &value); |
|
|
|
error: |
|
kfree(dup_buf); |
|
|
|
if (ret < 0) |
|
return ret; |
|
|
|
return count; |
|
} |
|
|
|
static umode_t dual_role_attr_is_visible(struct kobject *kobj, |
|
struct attribute *attr, int attrno) |
|
{ |
|
struct device *dev = container_of(kobj, struct device, kobj); |
|
struct dual_role_phy_instance *dual_role = dev_get_drvdata(dev); |
|
umode_t mode = S_IRUSR | S_IRGRP | S_IROTH; |
|
int i; |
|
|
|
if (attrno == DUAL_ROLE_PROP_SUPPORTED_MODES) |
|
return mode; |
|
|
|
for (i = 0; i < dual_role->desc->num_properties; i++) { |
|
int property = dual_role->desc->properties[i]; |
|
|
|
if (property == attrno) { |
|
if (dual_role->desc->property_is_writeable && |
|
dual_role_property_is_writeable(dual_role, property) |
|
> 0) |
|
mode |= S_IWUSR; |
|
|
|
return mode; |
|
} |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static struct attribute *__dual_role_attrs[ARRAY_SIZE(dual_role_attrs) + 1]; |
|
|
|
static struct attribute_group dual_role_attr_group = { |
|
.attrs = __dual_role_attrs, |
|
.is_visible = dual_role_attr_is_visible, |
|
}; |
|
|
|
static const struct attribute_group *dual_role_attr_groups[] = { |
|
&dual_role_attr_group, |
|
NULL, |
|
}; |
|
|
|
void dual_role_init_attrs(struct device_type *dev_type) |
|
{ |
|
int i; |
|
|
|
dev_type->groups = dual_role_attr_groups; |
|
|
|
for (i = 0; i < ARRAY_SIZE(dual_role_attrs); i++) |
|
__dual_role_attrs[i] = &dual_role_attrs[i].attr; |
|
} |
|
|
|
int dual_role_uevent(struct device *dev, struct kobj_uevent_env *env) |
|
{ |
|
struct dual_role_phy_instance *dual_role = dev_get_drvdata(dev); |
|
int ret = 0, j; |
|
char *prop_buf; |
|
char *attrname; |
|
|
|
dev_dbg(dev, "uevent\n"); |
|
|
|
if (!dual_role || !dual_role->desc) { |
|
dev_dbg(dev, "No dual_role phy yet\n"); |
|
return ret; |
|
} |
|
|
|
dev_dbg(dev, "DUAL_ROLE_NAME=%s\n", dual_role->desc->name); |
|
|
|
ret = add_uevent_var(env, "DUAL_ROLE_NAME=%s", dual_role->desc->name); |
|
if (ret) |
|
return ret; |
|
|
|
prop_buf = (char *)get_zeroed_page(GFP_KERNEL); |
|
if (!prop_buf) |
|
return -ENOMEM; |
|
|
|
for (j = 0; j < dual_role->desc->num_properties; j++) { |
|
struct device_attribute *attr; |
|
char *line; |
|
|
|
attr = &dual_role_attrs[dual_role->desc->properties[j]]; |
|
|
|
ret = dual_role_show_property(dev, attr, prop_buf); |
|
if (ret == -ENODEV || ret == -ENODATA) { |
|
ret = 0; |
|
continue; |
|
} |
|
|
|
if (ret < 0) |
|
goto out; |
|
line = strnchr(prop_buf, PAGE_SIZE, '\n'); |
|
if (line) |
|
*line = 0; |
|
|
|
attrname = kstrdupcase(attr->attr.name, GFP_KERNEL, true); |
|
if (!attrname) |
|
ret = -ENOMEM; |
|
|
|
dev_dbg(dev, "prop %s=%s\n", attrname, prop_buf); |
|
|
|
ret = add_uevent_var(env, "DUAL_ROLE_%s=%s", attrname, |
|
prop_buf); |
|
kfree(attrname); |
|
if (ret) |
|
goto out; |
|
} |
|
|
|
out: |
|
free_page((unsigned long)prop_buf); |
|
|
|
return ret; |
|
} |
|
|
|
/******************* Module Init ***********************************/ |
|
|
|
static int __init dual_role_class_init(void) |
|
{ |
|
dual_role_class = class_create(THIS_MODULE, "mtk_dual_role_usb"); |
|
|
|
if (IS_ERR(dual_role_class)) |
|
return PTR_ERR(dual_role_class); |
|
|
|
dual_role_class->dev_uevent = dual_role_uevent; |
|
dual_role_init_attrs(&dual_role_dev_type); |
|
|
|
return 0; |
|
} |
|
|
|
static void __exit dual_role_class_exit(void) |
|
{ |
|
class_destroy(dual_role_class); |
|
} |
|
|
|
subsys_initcall(dual_role_class_init); |
|
module_exit(dual_role_class_exit);
|
|
|