diff options
Diffstat (limited to 'drivers/greybus/core.c')
| -rw-r--r-- | drivers/greybus/core.c | 349 | 
1 files changed, 349 insertions, 0 deletions
diff --git a/drivers/greybus/core.c b/drivers/greybus/core.c new file mode 100644 index 000000000000..e546c6431877 --- /dev/null +++ b/drivers/greybus/core.c @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Greybus "Core" + * + * Copyright 2014-2015 Google Inc. + * Copyright 2014-2015 Linaro Ltd. + */ + +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt + +#define CREATE_TRACE_POINTS +#include <linux/greybus.h> +#include "greybus_trace.h" + +#define GB_BUNDLE_AUTOSUSPEND_MS	3000 + +/* Allow greybus to be disabled at boot if needed */ +static bool nogreybus; +#ifdef MODULE +module_param(nogreybus, bool, 0444); +#else +core_param(nogreybus, nogreybus, bool, 0444); +#endif +int greybus_disabled(void) +{ +	return nogreybus; +} +EXPORT_SYMBOL_GPL(greybus_disabled); + +static bool greybus_match_one_id(struct gb_bundle *bundle, +				 const struct greybus_bundle_id *id) +{ +	if ((id->match_flags & GREYBUS_ID_MATCH_VENDOR) && +	    (id->vendor != bundle->intf->vendor_id)) +		return false; + +	if ((id->match_flags & GREYBUS_ID_MATCH_PRODUCT) && +	    (id->product != bundle->intf->product_id)) +		return false; + +	if ((id->match_flags & GREYBUS_ID_MATCH_CLASS) && +	    (id->class != bundle->class)) +		return false; + +	return true; +} + +static const struct greybus_bundle_id * +greybus_match_id(struct gb_bundle *bundle, const struct greybus_bundle_id *id) +{ +	if (!id) +		return NULL; + +	for (; id->vendor || id->product || id->class || id->driver_info; +									id++) { +		if (greybus_match_one_id(bundle, id)) +			return id; +	} + +	return NULL; +} + +static int greybus_match_device(struct device *dev, struct device_driver *drv) +{ +	struct greybus_driver *driver = to_greybus_driver(drv); +	struct gb_bundle *bundle; +	const struct greybus_bundle_id *id; + +	if (!is_gb_bundle(dev)) +		return 0; + +	bundle = to_gb_bundle(dev); + +	id = greybus_match_id(bundle, driver->id_table); +	if (id) +		return 1; +	/* FIXME - Dynamic ids? */ +	return 0; +} + +static int greybus_uevent(struct device *dev, struct kobj_uevent_env *env) +{ +	struct gb_host_device *hd; +	struct gb_module *module = NULL; +	struct gb_interface *intf = NULL; +	struct gb_control *control = NULL; +	struct gb_bundle *bundle = NULL; +	struct gb_svc *svc = NULL; + +	if (is_gb_host_device(dev)) { +		hd = to_gb_host_device(dev); +	} else if (is_gb_module(dev)) { +		module = to_gb_module(dev); +		hd = module->hd; +	} else if (is_gb_interface(dev)) { +		intf = to_gb_interface(dev); +		module = intf->module; +		hd = intf->hd; +	} else if (is_gb_control(dev)) { +		control = to_gb_control(dev); +		intf = control->intf; +		module = intf->module; +		hd = intf->hd; +	} else if (is_gb_bundle(dev)) { +		bundle = to_gb_bundle(dev); +		intf = bundle->intf; +		module = intf->module; +		hd = intf->hd; +	} else if (is_gb_svc(dev)) { +		svc = to_gb_svc(dev); +		hd = svc->hd; +	} else { +		dev_WARN(dev, "uevent for unknown greybus device \"type\"!\n"); +		return -EINVAL; +	} + +	if (add_uevent_var(env, "BUS=%u", hd->bus_id)) +		return -ENOMEM; + +	if (module) { +		if (add_uevent_var(env, "MODULE=%u", module->module_id)) +			return -ENOMEM; +	} + +	if (intf) { +		if (add_uevent_var(env, "INTERFACE=%u", intf->interface_id)) +			return -ENOMEM; +		if (add_uevent_var(env, "GREYBUS_ID=%08x/%08x", +				   intf->vendor_id, intf->product_id)) +			return -ENOMEM; +	} + +	if (bundle) { +		// FIXME +		// add a uevent that can "load" a bundle type +		// This is what we need to bind a driver to so use the info +		// in gmod here as well + +		if (add_uevent_var(env, "BUNDLE=%u", bundle->id)) +			return -ENOMEM; +		if (add_uevent_var(env, "BUNDLE_CLASS=%02x", bundle->class)) +			return -ENOMEM; +	} + +	return 0; +} + +static void greybus_shutdown(struct device *dev) +{ +	if (is_gb_host_device(dev)) { +		struct gb_host_device *hd; + +		hd = to_gb_host_device(dev); +		gb_hd_shutdown(hd); +	} +} + +struct bus_type greybus_bus_type = { +	.name =		"greybus", +	.match =	greybus_match_device, +	.uevent =	greybus_uevent, +	.shutdown =	greybus_shutdown, +}; + +static int greybus_probe(struct device *dev) +{ +	struct greybus_driver *driver = to_greybus_driver(dev->driver); +	struct gb_bundle *bundle = to_gb_bundle(dev); +	const struct greybus_bundle_id *id; +	int retval; + +	/* match id */ +	id = greybus_match_id(bundle, driver->id_table); +	if (!id) +		return -ENODEV; + +	retval = pm_runtime_get_sync(&bundle->intf->dev); +	if (retval < 0) { +		pm_runtime_put_noidle(&bundle->intf->dev); +		return retval; +	} + +	retval = gb_control_bundle_activate(bundle->intf->control, bundle->id); +	if (retval) { +		pm_runtime_put(&bundle->intf->dev); +		return retval; +	} + +	/* +	 * Unbound bundle devices are always deactivated. During probe, the +	 * Runtime PM is set to enabled and active and the usage count is +	 * incremented. If the driver supports runtime PM, it should call +	 * pm_runtime_put() in its probe routine and pm_runtime_get_sync() +	 * in remove routine. +	 */ +	pm_runtime_set_autosuspend_delay(dev, GB_BUNDLE_AUTOSUSPEND_MS); +	pm_runtime_use_autosuspend(dev); +	pm_runtime_get_noresume(dev); +	pm_runtime_set_active(dev); +	pm_runtime_enable(dev); + +	retval = driver->probe(bundle, id); +	if (retval) { +		/* +		 * Catch buggy drivers that fail to destroy their connections. +		 */ +		WARN_ON(!list_empty(&bundle->connections)); + +		gb_control_bundle_deactivate(bundle->intf->control, bundle->id); + +		pm_runtime_disable(dev); +		pm_runtime_set_suspended(dev); +		pm_runtime_put_noidle(dev); +		pm_runtime_dont_use_autosuspend(dev); +		pm_runtime_put(&bundle->intf->dev); + +		return retval; +	} + +	pm_runtime_put(&bundle->intf->dev); + +	return 0; +} + +static int greybus_remove(struct device *dev) +{ +	struct greybus_driver *driver = to_greybus_driver(dev->driver); +	struct gb_bundle *bundle = to_gb_bundle(dev); +	struct gb_connection *connection; +	int retval; + +	retval = pm_runtime_get_sync(dev); +	if (retval < 0) +		dev_err(dev, "failed to resume bundle: %d\n", retval); + +	/* +	 * Disable (non-offloaded) connections early in case the interface is +	 * already gone to avoid unceccessary operation timeouts during +	 * driver disconnect. Otherwise, only disable incoming requests. +	 */ +	list_for_each_entry(connection, &bundle->connections, bundle_links) { +		if (gb_connection_is_offloaded(connection)) +			continue; + +		if (bundle->intf->disconnected) +			gb_connection_disable_forced(connection); +		else +			gb_connection_disable_rx(connection); +	} + +	driver->disconnect(bundle); + +	/* Catch buggy drivers that fail to destroy their connections. */ +	WARN_ON(!list_empty(&bundle->connections)); + +	if (!bundle->intf->disconnected) +		gb_control_bundle_deactivate(bundle->intf->control, bundle->id); + +	pm_runtime_put_noidle(dev); +	pm_runtime_disable(dev); +	pm_runtime_set_suspended(dev); +	pm_runtime_dont_use_autosuspend(dev); +	pm_runtime_put_noidle(dev); + +	return 0; +} + +int greybus_register_driver(struct greybus_driver *driver, struct module *owner, +			    const char *mod_name) +{ +	int retval; + +	if (greybus_disabled()) +		return -ENODEV; + +	driver->driver.bus = &greybus_bus_type; +	driver->driver.name = driver->name; +	driver->driver.probe = greybus_probe; +	driver->driver.remove = greybus_remove; +	driver->driver.owner = owner; +	driver->driver.mod_name = mod_name; + +	retval = driver_register(&driver->driver); +	if (retval) +		return retval; + +	pr_info("registered new driver %s\n", driver->name); +	return 0; +} +EXPORT_SYMBOL_GPL(greybus_register_driver); + +void greybus_deregister_driver(struct greybus_driver *driver) +{ +	driver_unregister(&driver->driver); +} +EXPORT_SYMBOL_GPL(greybus_deregister_driver); + +static int __init gb_init(void) +{ +	int retval; + +	if (greybus_disabled()) +		return -ENODEV; + +	BUILD_BUG_ON(CPORT_ID_MAX >= (long)CPORT_ID_BAD); + +	gb_debugfs_init(); + +	retval = bus_register(&greybus_bus_type); +	if (retval) { +		pr_err("bus_register failed (%d)\n", retval); +		goto error_bus; +	} + +	retval = gb_hd_init(); +	if (retval) { +		pr_err("gb_hd_init failed (%d)\n", retval); +		goto error_hd; +	} + +	retval = gb_operation_init(); +	if (retval) { +		pr_err("gb_operation_init failed (%d)\n", retval); +		goto error_operation; +	} +	return 0;	/* Success */ + +error_operation: +	gb_hd_exit(); +error_hd: +	bus_unregister(&greybus_bus_type); +error_bus: +	gb_debugfs_cleanup(); + +	return retval; +} +module_init(gb_init); + +static void __exit gb_exit(void) +{ +	gb_operation_exit(); +	gb_hd_exit(); +	bus_unregister(&greybus_bus_type); +	gb_debugfs_cleanup(); +	tracepoint_synchronize_unregister(); +} +module_exit(gb_exit); +MODULE_LICENSE("GPL v2"); +MODULE_AUTHOR("Greg Kroah-Hartman <[email protected]>");  |