Extcon: support multiple states at a device.

One switch device (e.g., MUIC(MAX8997, MAX77686, ...), and some 30-pin
devices) may have multiple cables attached. For example, one
30-pin port may inhabit a USB cable, an HDMI cable, and a mic.
Thus, one switch device requires multiple state bits each representing
a type of cable.

For such purpose, we use the 32bit state variable; thus, up to 32
different type of cables may be defined for a switch device. The list of
possible cables is defined by the array of cable names in the switch_dev
struct given to the class.

Signed-off-by: Chanwoo Choi <cw00.choi@samsung.com>
Signed-off-by: MyungJoo Ham <myungjoo.ham@samsung.com>
Signed-off-by: Kyungmin Park <kyungmin.park@samsung.com>

--
Changes from V7
- Bugfixed in _call_per_cable() (incorrect nb) (Chanwoo Choi)
- Compiler error in header for !CONFIG_EXTCON (Chanwoo Choi)

Changes from V5
- Sysfs style reformed: subdirectory per cable.
- Updated standard cable names
- Removed unnecessary printf
- Bugfixes after testing

Changes from V4
- Bugfixes after more testing at Exynos4412 boards with userspace
  processses.

Changes from V3
- Bugfixes after more testing at Exynos4412 boards.

Changes from V2
- State can be stored by user
- Documentation updated

Changes from RFC
- Switch is renamed to extcon
- Added kerneldoc comments
- Added APIs to support "standard" cable names
- Added helper APIs to support notifier block registration with cable
  name.
- Regrouped function list in the header file.
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
MyungJoo Ham
2012-04-20 14:16:25 +09:00
committed by Greg Kroah-Hartman
parent 74c5d09bd5
commit 806d9dd71f
3 changed files with 667 additions and 20 deletions
+426 -13
View File
@@ -31,6 +31,39 @@
#include <linux/extcon.h>
#include <linux/slab.h>
/*
* extcon_cable_name suggests the standard cable names for commonly used
* cable types.
*
* However, please do not use extcon_cable_name directly for extcon_dev
* struct's supported_cable pointer unless your device really supports
* every single port-type of the following cable names. Please choose cable
* names that are actually used in your extcon device.
*/
const char *extcon_cable_name[] = {
[EXTCON_USB] = "USB",
[EXTCON_USB_HOST] = "USB-Host",
[EXTCON_TA] = "TA",
[EXTCON_FAST_CHARGER] = "Fast-charger",
[EXTCON_SLOW_CHARGER] = "Slow-charger",
[EXTCON_CHARGE_DOWNSTREAM] = "Charge-downstream",
[EXTCON_HDMI] = "HDMI",
[EXTCON_MHL] = "MHL",
[EXTCON_DVI] = "DVI",
[EXTCON_VGA] = "VGA",
[EXTCON_DOCK] = "Dock",
[EXTCON_LINE_IN] = "Line-in",
[EXTCON_LINE_OUT] = "Line-out",
[EXTCON_MIC_IN] = "Microphone",
[EXTCON_HEADPHONE_OUT] = "Headphone",
[EXTCON_SPDIF_IN] = "SPDIF-in",
[EXTCON_SPDIF_OUT] = "SPDIF-out",
[EXTCON_VIDEO_IN] = "Video-in",
[EXTCON_VIDEO_OUT] = "Video-out",
NULL,
};
struct class *extcon_class;
#if defined(CONFIG_ANDROID) && !defined(CONFIG_ANDROID_SWITCH)
static struct class_compat *switch_class;
@@ -42,6 +75,7 @@ static DEFINE_MUTEX(extcon_dev_list_lock);
static ssize_t state_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
int i, count = 0;
struct extcon_dev *edev = (struct extcon_dev *) dev_get_drvdata(dev);
if (edev->print_state) {
@@ -51,7 +85,39 @@ static ssize_t state_show(struct device *dev, struct device_attribute *attr,
return ret;
/* Use default if failed */
}
return sprintf(buf, "%u\n", edev->state);
if (edev->max_supported == 0)
return sprintf(buf, "%u\n", edev->state);
for (i = 0; i < SUPPORTED_CABLE_MAX; i++) {
if (!edev->supported_cable[i])
break;
count += sprintf(buf + count, "%s=%d\n",
edev->supported_cable[i],
!!(edev->state & (1 << i)));
}
return count;
}
void extcon_set_state(struct extcon_dev *edev, u32 state);
static ssize_t state_store(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count)
{
u32 state;
ssize_t ret = 0;
struct extcon_dev *edev = (struct extcon_dev *) dev_get_drvdata(dev);
ret = sscanf(buf, "0x%x", &state);
if (ret == 0)
ret = -EINVAL;
else
extcon_set_state(edev, state);
if (ret < 0)
return ret;
return count;
}
static ssize_t name_show(struct device *dev, struct device_attribute *attr,
@@ -69,9 +135,52 @@ static ssize_t name_show(struct device *dev, struct device_attribute *attr,
return sprintf(buf, "%s\n", dev_name(edev->dev));
}
static ssize_t cable_name_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct extcon_cable *cable = container_of(attr, struct extcon_cable,
attr_name);
return sprintf(buf, "%s\n",
cable->edev->supported_cable[cable->cable_index]);
}
static ssize_t cable_state_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct extcon_cable *cable = container_of(attr, struct extcon_cable,
attr_state);
return sprintf(buf, "%d\n",
extcon_get_cable_state_(cable->edev,
cable->cable_index));
}
static ssize_t cable_state_store(struct device *dev,
struct device_attribute *attr, const char *buf,
size_t count)
{
struct extcon_cable *cable = container_of(attr, struct extcon_cable,
attr_state);
int ret, state;
ret = sscanf(buf, "%d", &state);
if (ret == 0)
ret = -EINVAL;
else
ret = extcon_set_cable_state_(cable->edev, cable->cable_index,
state);
if (ret < 0)
return ret;
return count;
}
/**
* extcon_set_state() - Set the cable attach states of the extcon device.
* extcon_update_state() - Update the cable attach states of the extcon device
* only for the masked bits.
* @edev: the extcon device
* @mask: the bit mask to designate updated bits.
* @state: new cable attach status for @edev
*
* Changing the state sends uevent with environment variable containing
@@ -79,10 +188,10 @@ static ssize_t name_show(struct device *dev, struct device_attribute *attr,
* Tizen uses this format for extcon device to get events from ports.
* Android uses this format as well.
*
* Note that notifier provides the which bits are changes in the state
* variable with "val" to the callback.
* Note that the notifier provides which bits are changed in the state
* variable with the val parameter (second) to the callback.
*/
void extcon_set_state(struct extcon_dev *edev, u32 state)
void extcon_update_state(struct extcon_dev *edev, u32 mask, u32 state)
{
char name_buf[120];
char state_buf[120];
@@ -90,15 +199,20 @@ void extcon_set_state(struct extcon_dev *edev, u32 state)
char *envp[3];
int env_offset = 0;
int length;
u32 old_state = edev->state;
unsigned long flags;
if (edev->state != state) {
edev->state = state;
spin_lock_irqsave(&edev->lock, flags);
raw_notifier_call_chain(&edev->nh, old_state ^ edev->state,
edev);
if (edev->state != ((edev->state & ~mask) | (state & mask))) {
u32 old_state = edev->state;
prop_buf = (char *)get_zeroed_page(GFP_KERNEL);
edev->state &= ~mask;
edev->state |= state & mask;
raw_notifier_call_chain(&edev->nh, old_state, edev);
/* This could be in interrupt handler */
prop_buf = (char *)get_zeroed_page(GFP_ATOMIC);
if (prop_buf) {
length = name_show(edev->dev, NULL, prop_buf);
if (length > 0) {
@@ -117,16 +231,131 @@ void extcon_set_state(struct extcon_dev *edev, u32 state)
envp[env_offset++] = state_buf;
}
envp[env_offset] = NULL;
/* Unlock early before uevent */
spin_unlock_irqrestore(&edev->lock, flags);
kobject_uevent_env(&edev->dev->kobj, KOBJ_CHANGE, envp);
free_page((unsigned long)prop_buf);
} else {
/* Unlock early before uevent */
spin_unlock_irqrestore(&edev->lock, flags);
dev_err(edev->dev, "out of memory in extcon_set_state\n");
kobject_uevent(&edev->dev->kobj, KOBJ_CHANGE);
}
} else {
/* No changes */
spin_unlock_irqrestore(&edev->lock, flags);
}
}
EXPORT_SYMBOL_GPL(extcon_update_state);
/**
* extcon_set_state() - Set the cable attach states of the extcon device.
* @edev: the extcon device
* @state: new cable attach status for @edev
*
* Note that notifier provides which bits are changed in the state
* variable with the val parameter (second) to the callback.
*/
void extcon_set_state(struct extcon_dev *edev, u32 state)
{
extcon_update_state(edev, 0xffffffff, state);
}
EXPORT_SYMBOL_GPL(extcon_set_state);
/**
* extcon_find_cable_index() - Get the cable index based on the cable name.
* @edev: the extcon device that has the cable.
* @cable_name: cable name to be searched.
*
* Note that accessing a cable state based on cable_index is faster than
* cable_name because using cable_name induces a loop with strncmp().
* Thus, when get/set_cable_state is repeatedly used, using cable_index
* is recommended.
*/
int extcon_find_cable_index(struct extcon_dev *edev, const char *cable_name)
{
int i;
if (edev->supported_cable) {
for (i = 0; edev->supported_cable[i]; i++) {
if (!strncmp(edev->supported_cable[i],
cable_name, CABLE_NAME_MAX))
return i;
}
}
return -EINVAL;
}
EXPORT_SYMBOL_GPL(extcon_find_cable_index);
/**
* extcon_get_cable_state_() - Get the status of a specific cable.
* @edev: the extcon device that has the cable.
* @index: cable index that can be retrieved by extcon_find_cable_index().
*/
int extcon_get_cable_state_(struct extcon_dev *edev, int index)
{
if (index < 0 || (edev->max_supported && edev->max_supported <= index))
return -EINVAL;
return !!(edev->state & (1 << index));
}
EXPORT_SYMBOL_GPL(extcon_get_cable_state_);
/**
* extcon_get_cable_state() - Get the status of a specific cable.
* @edev: the extcon device that has the cable.
* @cable_name: cable name.
*
* Note that this is slower than extcon_get_cable_state_.
*/
int extcon_get_cable_state(struct extcon_dev *edev, const char *cable_name)
{
return extcon_get_cable_state_(edev, extcon_find_cable_index
(edev, cable_name));
}
EXPORT_SYMBOL_GPL(extcon_get_cable_state);
/**
* extcon_get_cable_state_() - Set the status of a specific cable.
* @edev: the extcon device that has the cable.
* @index: cable index that can be retrieved by extcon_find_cable_index().
* @cable_state: the new cable status. The default semantics is
* true: attached / false: detached.
*/
int extcon_set_cable_state_(struct extcon_dev *edev,
int index, bool cable_state)
{
u32 state;
if (index < 0 || (edev->max_supported && edev->max_supported <= index))
return -EINVAL;
state = cable_state ? (1 << index) : 0;
extcon_update_state(edev, 1 << index, state);
return 0;
}
EXPORT_SYMBOL_GPL(extcon_set_cable_state_);
/**
* extcon_get_cable_state() - Set the status of a specific cable.
* @edev: the extcon device that has the cable.
* @cable_name: cable name.
* @cable_state: the new cable status. The default semantics is
* true: attached / false: detached.
*
* Note that this is slower than extcon_set_cable_state_.
*/
int extcon_set_cable_state(struct extcon_dev *edev,
const char *cable_name, bool cable_state)
{
return extcon_set_cable_state_(edev, extcon_find_cable_index
(edev, cable_name), cable_state);
}
EXPORT_SYMBOL_GPL(extcon_set_cable_state);
/**
* extcon_get_extcon_dev() - Get the extcon device instance from the name
* @extcon_name: The extcon name provided with extcon_dev_register()
@@ -147,11 +376,88 @@ out:
}
EXPORT_SYMBOL_GPL(extcon_get_extcon_dev);
static int _call_per_cable(struct notifier_block *nb, unsigned long val,
void *ptr)
{
struct extcon_specific_cable_nb *obj = container_of(nb,
struct extcon_specific_cable_nb, internal_nb);
struct extcon_dev *edev = ptr;
if ((val & (1 << obj->cable_index)) !=
(edev->state & (1 << obj->cable_index))) {
obj->previous_value = val;
return obj->user_nb->notifier_call(obj->user_nb, val, ptr);
}
return NOTIFY_OK;
}
/**
* extcon_register_interest() - Register a notifier for a state change of a
* specific cable, not a entier set of cables of a
* extcon device.
* @obj: an empty extcon_specific_cable_nb object to be returned.
* @extcon_name: the name of extcon device.
* @cable_name: the target cable name.
* @nb: the notifier block to get notified.
*
* Provide an empty extcon_specific_cable_nb. extcon_register_interest() sets
* the struct for you.
*
* extcon_register_interest is a helper function for those who want to get
* notification for a single specific cable's status change. If a user wants
* to get notification for any changes of all cables of a extcon device,
* he/she should use the general extcon_register_notifier().
*
* Note that the second parameter given to the callback of nb (val) is
* "old_state", not the current state. The current state can be retrieved
* by looking at the third pameter (edev pointer)'s state value.
*/
int extcon_register_interest(struct extcon_specific_cable_nb *obj,
const char *extcon_name, const char *cable_name,
struct notifier_block *nb)
{
if (!obj || !extcon_name || !cable_name || !nb)
return -EINVAL;
obj->edev = extcon_get_extcon_dev(extcon_name);
if (!obj->edev)
return -ENODEV;
obj->cable_index = extcon_find_cable_index(obj->edev, cable_name);
if (obj->cable_index < 0)
return -ENODEV;
obj->user_nb = nb;
obj->internal_nb.notifier_call = _call_per_cable;
return raw_notifier_chain_register(&obj->edev->nh, &obj->internal_nb);
}
/**
* extcon_unregister_interest() - Unregister the notifier registered by
* extcon_register_interest().
* @obj: the extcon_specific_cable_nb object returned by
* extcon_register_interest().
*/
int extcon_unregister_interest(struct extcon_specific_cable_nb *obj)
{
if (!obj)
return -EINVAL;
return raw_notifier_chain_unregister(&obj->edev->nh, &obj->internal_nb);
}
/**
* extcon_register_notifier() - Register a notifee to get notified by
* any attach status changes from the extcon.
* @edev: the extcon device.
* @nb: a notifier block to be registered.
*
* Note that the second parameter given to the callback of nb (val) is
* "old_state", not the current state. The current state can be retrieved
* by looking at the third pameter (edev pointer)'s state value.
*/
int extcon_register_notifier(struct extcon_dev *edev,
struct notifier_block *nb)
@@ -173,8 +479,9 @@ int extcon_unregister_notifier(struct extcon_dev *edev,
EXPORT_SYMBOL_GPL(extcon_unregister_notifier);
static struct device_attribute extcon_attrs[] = {
__ATTR_RO(state),
__ATTR(state, S_IRUGO | S_IWUSR, state_show, state_store),
__ATTR_RO(name),
__ATTR_NULL,
};
static int create_extcon_class(void)
@@ -202,6 +509,16 @@ static void extcon_cleanup(struct extcon_dev *edev, bool skip)
mutex_unlock(&extcon_dev_list_lock);
if (!skip && get_device(edev->dev)) {
int index;
for (index = 0; index < edev->max_supported; index++)
kfree(edev->cables[index].attr_g.name);
if (edev->max_supported) {
kfree(edev->extcon_dev_type.groups);
kfree(edev->cables);
}
device_unregister(edev->dev);
put_device(edev->dev);
}
@@ -216,6 +533,10 @@ static void extcon_dev_release(struct device *dev)
extcon_cleanup(edev, true);
}
static void dummy_sysfs_dev_release(struct device *dev)
{
}
/**
* extcon_dev_register() - Register a new extcon device
* @edev : the new extcon device (should be allocated before calling)
@@ -228,7 +549,7 @@ static void extcon_dev_release(struct device *dev)
*/
int extcon_dev_register(struct extcon_dev *edev, struct device *dev)
{
int ret;
int ret, index = 0;
if (!extcon_class) {
ret = create_extcon_class();
@@ -236,12 +557,93 @@ int extcon_dev_register(struct extcon_dev *edev, struct device *dev)
return ret;
}
if (edev->supported_cable) {
/* Get size of array */
for (index = 0; edev->supported_cable[index]; index++)
;
edev->max_supported = index;
} else {
edev->max_supported = 0;
}
if (index > SUPPORTED_CABLE_MAX) {
dev_err(edev->dev, "extcon: maximum number of supported cables exceeded.\n");
return -EINVAL;
}
edev->dev = kzalloc(sizeof(struct device), GFP_KERNEL);
edev->dev->parent = dev;
edev->dev->class = extcon_class;
edev->dev->release = extcon_dev_release;
dev_set_name(edev->dev, edev->name ? edev->name : dev_name(dev));
if (edev->max_supported) {
char buf[10];
char *str;
struct extcon_cable *cable;
edev->cables = kzalloc(sizeof(struct extcon_cable) *
edev->max_supported, GFP_KERNEL);
if (!edev->cables) {
ret = -ENOMEM;
goto err_sysfs_alloc;
}
for (index = 0; index < edev->max_supported; index++) {
cable = &edev->cables[index];
snprintf(buf, 10, "cable.%d", index);
str = kzalloc(sizeof(char) * (strlen(buf) + 1),
GFP_KERNEL);
if (!str) {
for (index--; index >= 0; index--) {
cable = &edev->cables[index];
kfree(cable->attr_g.name);
}
ret = -ENOMEM;
goto err_alloc_cables;
}
strcpy(str, buf);
cable->edev = edev;
cable->cable_index = index;
cable->attrs[0] = &cable->attr_name.attr;
cable->attrs[1] = &cable->attr_state.attr;
cable->attrs[2] = NULL;
cable->attr_g.name = str;
cable->attr_g.attrs = cable->attrs;
cable->attr_name.attr.name = "name";
cable->attr_name.attr.mode = 0444;
cable->attr_name.show = cable_name_show;
cable->attr_state.attr.name = "state";
cable->attr_state.attr.mode = 0644;
cable->attr_state.show = cable_state_show;
cable->attr_state.store = cable_state_store;
}
}
if (edev->max_supported) {
edev->extcon_dev_type.groups =
kzalloc(sizeof(struct attribute_group *) *
(edev->max_supported + 1), GFP_KERNEL);
if (!edev->extcon_dev_type.groups) {
ret = -ENOMEM;
goto err_alloc_groups;
}
edev->extcon_dev_type.name = dev_name(edev->dev);
edev->extcon_dev_type.release = dummy_sysfs_dev_release;
for (index = 0; index < edev->max_supported; index++)
edev->extcon_dev_type.groups[index] =
&edev->cables[index].attr_g;
edev->dev->type = &edev->extcon_dev_type;
}
ret = device_register(edev->dev);
if (ret) {
put_device(edev->dev);
@@ -253,6 +655,8 @@ int extcon_dev_register(struct extcon_dev *edev, struct device *dev)
dev);
#endif /* CONFIG_ANDROID && !defined(CONFIG_ANDROID_SWITCH) */
spin_lock_init(&edev->lock);
RAW_INIT_NOTIFIER_HEAD(&edev->nh);
dev_set_drvdata(edev->dev, edev);
@@ -265,6 +669,15 @@ int extcon_dev_register(struct extcon_dev *edev, struct device *dev)
return 0;
err_dev:
if (edev->max_supported)
kfree(edev->extcon_dev_type.groups);
err_alloc_groups:
for (index = 0; index < edev->max_supported; index++)
kfree(edev->cables[index].attr_g.name);
err_alloc_cables:
if (edev->max_supported)
kfree(edev->cables);
err_sysfs_alloc:
kfree(edev->dev);
return ret;
}