Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 806d9dd7 authored by MyungJoo Ham's avatar MyungJoo Ham Committed by Greg Kroah-Hartman
Browse files

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: default avatarChanwoo Choi <cw00.choi@samsung.com>
Signed-off-by: default avatarMyungJoo Ham <myungjoo.ham@samsung.com>
Signed-off-by: default avatarKyungmin 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: default avatarGreg Kroah-Hartman <gregkh@linuxfoundation.org>
parent 74c5d09b
Loading
Loading
Loading
Loading
+56 −7
Original line number Diff line number Diff line
What:		/sys/class/extcon/.../
Date:		December 2011
Date:		February 2012
Contact:	MyungJoo Ham <myungjoo.ham@samsung.com>
Description:
		Provide a place in sysfs for the extcon objects.
@@ -7,8 +7,16 @@ Description:
		The name of extcon object denoted as ... is the name given
		with extcon_dev_register.

		One extcon device denotes a single external connector
		port. An external connector may have multiple cables
		attached simultaneously. Many of docks, cradles, and
		accessory cables have such capability. For example,
		the 30-pin port of Nuri board (/arch/arm/mach-exynos)
		may have both HDMI and Charger attached, or analog audio,
		video, and USB cables attached simulteneously.

What:		/sys/class/extcon/.../name
Date:		December 2011
Date:		February 2012
Contact:	MyungJoo Ham <myungjoo.ham@samsung.com>
Description:
		The /sys/class/extcon/.../name shows the name of the extcon
@@ -17,10 +25,51 @@ Description:
		this sysfs node.

What:		/sys/class/extcon/.../state
Date:		December 2011
Date:		February 2012
Contact:	MyungJoo Ham <myungjoo.ham@samsung.com>
Description:
		The /sys/class/extcon/.../state shows and stores the cable
		attach/detach information of the corresponding extcon object.
		If the extcon object has an optional callback "show_state"
		defined, the showing function is overriden with the optional
		callback.

		If the default callback for showing function is used, the
		format is like this:
		# cat state
		USB_OTG=1
		HDMI=0
		TA=1
		EAR_JACK=0
		#
		In this example, the extcon device have USB_OTG and TA
		cables attached and HDMI and EAR_JACK cables detached.

		In order to update the state of an extcon device, enter a hex
		state number starting with 0x.
		 echo 0xHEX > state

		This updates the whole state of the extcon dev.
		Inputs of all the methods are required to meet the
		mutually_exclusive contidions if they exist.

		It is recommended to use this "global" state interface if
		you need to enter the value atomically. The later state
		interface associated with each cable cannot update
		multiple cable states of an extcon device simultaneously.

What:		/sys/class/extcon/.../cable.x/name
Date:		February 2012
Contact:	MyungJoo Ham <myungjoo.ham@samsung.com>
Description:
		The /sys/class/extcon/.../cable.x/name shows the name of cable
		"x" (integer between 0 and 31) of an extcon device.

What:		/sys/class/extcon/.../cable.x/state
Date:		February 2012
Contact:	MyungJoo Ham <myungjoo.ham@samsung.com>
Description:
		The /sys/class/extcon/.../state shows the cable attach/detach
		information of the corresponding extcon object. If the extcon
		objecct has an optional callback "show_state" defined, the
		callback will provide the name with this sysfs node.
		The /sys/class/extcon/.../cable.x/name shows and stores the
		state of cable "x" (integer between 0 and 31) of an extcon
		device. The state value is either 0 (detached) or 1
		(attached).
+426 −13
Original line number Diff line number Diff line
@@ -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 */
	}

	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;
	unsigned long flags;

	spin_lock_irqsave(&edev->lock, flags);

	if (edev->state != ((edev->state & ~mask) | (state & mask))) {
		u32 old_state = edev->state;

	if (edev->state != state) {
		edev->state = state;
		edev->state &= ~mask;
		edev->state |= state & mask;

		raw_notifier_call_chain(&edev->nh, old_state ^ edev->state,
					edev);
		raw_notifier_call_chain(&edev->nh, old_state, edev);

		prop_buf = (char *)get_zeroed_page(GFP_KERNEL);
		/* 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;
}
+185 −0

File changed.

Preview size limit exceeded, changes collapsed.