8465def499
The Greybus core code has been stable for a long time, and has been shipping for many years in millions of phones. With the advent of a recent Google Summer of Code project, and a number of new devices in the works from various companies, it is time to get the core greybus code out of staging as it really is going to be with us for a while. Cc: Johan Hovold <johan@kernel.org> Cc: linux-kernel@vger.kernel.org Cc: greybus-dev@lists.linaro.org Acked-by: Viresh Kumar <viresh.kumar@linaro.org> Acked-by: Alex Elder <elder@kernel.org> Link: https://lore.kernel.org/r/20190825055429.18547-9-gregkh@linuxfoundation.org Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
350 lines
8.0 KiB
C
350 lines
8.0 KiB
C
// 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 <gregkh@linuxfoundation.org>");
|