mirror of
				git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
				synced 2025-10-31 16:54:21 +00:00 
			
		
		
		
	 993a9e2aca
			
		
	
	
		993a9e2aca
		
	
	
	
	
		
			
			Split out subsystem device hub drivers into their own module. This allows us to load the hub drivers separately from the registry, which will help future DT/OF support. While doing so, also remove a small bit of code duplication. Signed-off-by: Maximilian Luz <luzmaximilian@gmail.com> Link: https://lore.kernel.org/r/20220624205800.1355621-3-luzmaximilian@gmail.com Reviewed-by: Hans de Goede <hdegoede@redhat.com> Signed-off-by: Hans de Goede <hdegoede@redhat.com>
		
			
				
	
	
		
			371 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			371 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| // SPDX-License-Identifier: GPL-2.0+
 | |
| /*
 | |
|  * Driver for Surface System Aggregator Module (SSAM) subsystem device hubs.
 | |
|  *
 | |
|  * Provides a driver for SSAM subsystems device hubs. This driver performs
 | |
|  * instantiation of the devices managed by said hubs and takes care of
 | |
|  * (hot-)removal.
 | |
|  *
 | |
|  * Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com>
 | |
|  */
 | |
| 
 | |
| #include <linux/kernel.h>
 | |
| #include <linux/limits.h>
 | |
| #include <linux/module.h>
 | |
| #include <linux/types.h>
 | |
| #include <linux/workqueue.h>
 | |
| 
 | |
| #include <linux/surface_aggregator/device.h>
 | |
| 
 | |
| 
 | |
| /* -- SSAM generic subsystem hub driver framework. -------------------------- */
 | |
| 
 | |
| enum ssam_hub_state {
 | |
| 	SSAM_HUB_UNINITIALIZED,		/* Only set during initialization. */
 | |
| 	SSAM_HUB_CONNECTED,
 | |
| 	SSAM_HUB_DISCONNECTED,
 | |
| };
 | |
| 
 | |
| enum ssam_hub_flags {
 | |
| 	SSAM_HUB_HOT_REMOVED,
 | |
| };
 | |
| 
 | |
| struct ssam_hub;
 | |
| 
 | |
| struct ssam_hub_ops {
 | |
| 	int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state);
 | |
| };
 | |
| 
 | |
| struct ssam_hub {
 | |
| 	struct ssam_device *sdev;
 | |
| 
 | |
| 	enum ssam_hub_state state;
 | |
| 	unsigned long flags;
 | |
| 
 | |
| 	struct delayed_work update_work;
 | |
| 	unsigned long connect_delay;
 | |
| 
 | |
| 	struct ssam_event_notifier notif;
 | |
| 	struct ssam_hub_ops ops;
 | |
| };
 | |
| 
 | |
| struct ssam_hub_desc {
 | |
| 	struct {
 | |
| 		struct ssam_event_registry reg;
 | |
| 		struct ssam_event_id id;
 | |
| 		enum ssam_event_mask mask;
 | |
| 	} event;
 | |
| 
 | |
| 	struct {
 | |
| 		u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event);
 | |
| 		int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state);
 | |
| 	} ops;
 | |
| 
 | |
| 	unsigned long connect_delay_ms;
 | |
| };
 | |
| 
 | |
| static void ssam_hub_update_workfn(struct work_struct *work)
 | |
| {
 | |
| 	struct ssam_hub *hub = container_of(work, struct ssam_hub, update_work.work);
 | |
| 	enum ssam_hub_state state;
 | |
| 	int status = 0;
 | |
| 
 | |
| 	status = hub->ops.get_state(hub, &state);
 | |
| 	if (status)
 | |
| 		return;
 | |
| 
 | |
| 	/*
 | |
| 	 * There is a small possibility that hub devices were hot-removed and
 | |
| 	 * re-added before we were able to remove them here. In that case, both
 | |
| 	 * the state returned by get_state() and the state of the hub will
 | |
| 	 * equal SSAM_HUB_CONNECTED and we would bail early below, which would
 | |
| 	 * leave child devices without proper (re-)initialization and the
 | |
| 	 * hot-remove flag set.
 | |
| 	 *
 | |
| 	 * Therefore, we check whether devices have been hot-removed via an
 | |
| 	 * additional flag on the hub and, in this case, override the returned
 | |
| 	 * hub state. In case of a missed disconnect (i.e. get_state returned
 | |
| 	 * "connected"), we further need to re-schedule this work (with the
 | |
| 	 * appropriate delay) as the actual connect work submission might have
 | |
| 	 * been merged with this one.
 | |
| 	 *
 | |
| 	 * This then leads to one of two cases: Either we submit an unnecessary
 | |
| 	 * work item (which will get ignored via either the queue or the state
 | |
| 	 * checks) or, in the unlikely case that the work is actually required,
 | |
| 	 * double the normal connect delay.
 | |
| 	 */
 | |
| 	if (test_and_clear_bit(SSAM_HUB_HOT_REMOVED, &hub->flags)) {
 | |
| 		if (state == SSAM_HUB_CONNECTED)
 | |
| 			schedule_delayed_work(&hub->update_work, hub->connect_delay);
 | |
| 
 | |
| 		state = SSAM_HUB_DISCONNECTED;
 | |
| 	}
 | |
| 
 | |
| 	if (hub->state == state)
 | |
| 		return;
 | |
| 	hub->state = state;
 | |
| 
 | |
| 	if (hub->state == SSAM_HUB_CONNECTED)
 | |
| 		status = ssam_device_register_clients(hub->sdev);
 | |
| 	else
 | |
| 		ssam_remove_clients(&hub->sdev->dev);
 | |
| 
 | |
| 	if (status)
 | |
| 		dev_err(&hub->sdev->dev, "failed to update hub child devices: %d\n", status);
 | |
| }
 | |
| 
 | |
| static int ssam_hub_mark_hot_removed(struct device *dev, void *_data)
 | |
| {
 | |
| 	struct ssam_device *sdev = to_ssam_device(dev);
 | |
| 
 | |
| 	if (is_ssam_device(dev))
 | |
| 		ssam_device_mark_hot_removed(sdev);
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static void ssam_hub_update(struct ssam_hub *hub, bool connected)
 | |
| {
 | |
| 	unsigned long delay;
 | |
| 
 | |
| 	/* Mark devices as hot-removed before we remove any. */
 | |
| 	if (!connected) {
 | |
| 		set_bit(SSAM_HUB_HOT_REMOVED, &hub->flags);
 | |
| 		device_for_each_child_reverse(&hub->sdev->dev, NULL, ssam_hub_mark_hot_removed);
 | |
| 	}
 | |
| 
 | |
| 	/*
 | |
| 	 * Delay update when the base/keyboard cover is being connected to give
 | |
| 	 * devices/EC some time to set up.
 | |
| 	 */
 | |
| 	delay = connected ? hub->connect_delay : 0;
 | |
| 
 | |
| 	schedule_delayed_work(&hub->update_work, delay);
 | |
| }
 | |
| 
 | |
| static int __maybe_unused ssam_hub_resume(struct device *dev)
 | |
| {
 | |
| 	struct ssam_hub *hub = dev_get_drvdata(dev);
 | |
| 
 | |
| 	schedule_delayed_work(&hub->update_work, 0);
 | |
| 	return 0;
 | |
| }
 | |
| static SIMPLE_DEV_PM_OPS(ssam_hub_pm_ops, NULL, ssam_hub_resume);
 | |
| 
 | |
| static int ssam_hub_probe(struct ssam_device *sdev)
 | |
| {
 | |
| 	const struct ssam_hub_desc *desc;
 | |
| 	struct ssam_hub *hub;
 | |
| 	int status;
 | |
| 
 | |
| 	desc = ssam_device_get_match_data(sdev);
 | |
| 	if (!desc) {
 | |
| 		WARN(1, "no driver match data specified");
 | |
| 		return -EINVAL;
 | |
| 	}
 | |
| 
 | |
| 	hub = devm_kzalloc(&sdev->dev, sizeof(*hub), GFP_KERNEL);
 | |
| 	if (!hub)
 | |
| 		return -ENOMEM;
 | |
| 
 | |
| 	hub->sdev = sdev;
 | |
| 	hub->state = SSAM_HUB_UNINITIALIZED;
 | |
| 
 | |
| 	hub->notif.base.priority = INT_MAX;  /* This notifier should run first. */
 | |
| 	hub->notif.base.fn = desc->ops.notify;
 | |
| 	hub->notif.event.reg = desc->event.reg;
 | |
| 	hub->notif.event.id = desc->event.id;
 | |
| 	hub->notif.event.mask = desc->event.mask;
 | |
| 	hub->notif.event.flags = SSAM_EVENT_SEQUENCED;
 | |
| 
 | |
| 	hub->connect_delay = msecs_to_jiffies(desc->connect_delay_ms);
 | |
| 	hub->ops.get_state = desc->ops.get_state;
 | |
| 
 | |
| 	INIT_DELAYED_WORK(&hub->update_work, ssam_hub_update_workfn);
 | |
| 
 | |
| 	ssam_device_set_drvdata(sdev, hub);
 | |
| 
 | |
| 	status = ssam_device_notifier_register(sdev, &hub->notif);
 | |
| 	if (status)
 | |
| 		return status;
 | |
| 
 | |
| 	schedule_delayed_work(&hub->update_work, 0);
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static void ssam_hub_remove(struct ssam_device *sdev)
 | |
| {
 | |
| 	struct ssam_hub *hub = ssam_device_get_drvdata(sdev);
 | |
| 
 | |
| 	ssam_device_notifier_unregister(sdev, &hub->notif);
 | |
| 	cancel_delayed_work_sync(&hub->update_work);
 | |
| 	ssam_remove_clients(&sdev->dev);
 | |
| }
 | |
| 
 | |
| 
 | |
| /* -- SSAM base-subsystem hub driver. --------------------------------------- */
 | |
| 
 | |
| /*
 | |
|  * Some devices (especially battery) may need a bit of time to be fully usable
 | |
|  * after being (re-)connected. This delay has been determined via
 | |
|  * experimentation.
 | |
|  */
 | |
| #define SSAM_BASE_UPDATE_CONNECT_DELAY		2500
 | |
| 
 | |
| SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode, u8, {
 | |
| 	.target_category = SSAM_SSH_TC_BAS,
 | |
| 	.target_id       = 0x01,
 | |
| 	.command_id      = 0x0d,
 | |
| 	.instance_id     = 0x00,
 | |
| });
 | |
| 
 | |
| #define SSAM_BAS_OPMODE_TABLET		0x00
 | |
| #define SSAM_EVENT_BAS_CID_CONNECTION	0x0c
 | |
| 
 | |
| static int ssam_base_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state)
 | |
| {
 | |
| 	u8 opmode;
 | |
| 	int status;
 | |
| 
 | |
| 	status = ssam_retry(ssam_bas_query_opmode, hub->sdev->ctrl, &opmode);
 | |
| 	if (status < 0) {
 | |
| 		dev_err(&hub->sdev->dev, "failed to query base state: %d\n", status);
 | |
| 		return status;
 | |
| 	}
 | |
| 
 | |
| 	if (opmode != SSAM_BAS_OPMODE_TABLET)
 | |
| 		*state = SSAM_HUB_CONNECTED;
 | |
| 	else
 | |
| 		*state = SSAM_HUB_DISCONNECTED;
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static u32 ssam_base_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
 | |
| {
 | |
| 	struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);
 | |
| 
 | |
| 	if (event->command_id != SSAM_EVENT_BAS_CID_CONNECTION)
 | |
| 		return 0;
 | |
| 
 | |
| 	if (event->length < 1) {
 | |
| 		dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	ssam_hub_update(hub, event->data[0]);
 | |
| 
 | |
| 	/*
 | |
| 	 * Do not return SSAM_NOTIF_HANDLED: The event should be picked up and
 | |
| 	 * consumed by the detachment system driver. We're just a (more or less)
 | |
| 	 * silent observer.
 | |
| 	 */
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static const struct ssam_hub_desc base_hub = {
 | |
| 	.event = {
 | |
| 		.reg = SSAM_EVENT_REGISTRY_SAM,
 | |
| 		.id = {
 | |
| 			.target_category = SSAM_SSH_TC_BAS,
 | |
| 			.instance = 0,
 | |
| 		},
 | |
| 		.mask = SSAM_EVENT_MASK_NONE,
 | |
| 	},
 | |
| 	.ops = {
 | |
| 		.notify = ssam_base_hub_notif,
 | |
| 		.get_state = ssam_base_hub_query_state,
 | |
| 	},
 | |
| 	.connect_delay_ms = SSAM_BASE_UPDATE_CONNECT_DELAY,
 | |
| };
 | |
| 
 | |
| 
 | |
| /* -- SSAM KIP-subsystem hub driver. ---------------------------------------- */
 | |
| 
 | |
| /*
 | |
|  * Some devices may need a bit of time to be fully usable after being
 | |
|  * (re-)connected. This delay has been determined via experimentation.
 | |
|  */
 | |
| #define SSAM_KIP_UPDATE_CONNECT_DELAY		250
 | |
| 
 | |
| #define SSAM_EVENT_KIP_CID_CONNECTION		0x2c
 | |
| 
 | |
| SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_query_state, u8, {
 | |
| 	.target_category = SSAM_SSH_TC_KIP,
 | |
| 	.target_id       = 0x01,
 | |
| 	.command_id      = 0x2c,
 | |
| 	.instance_id     = 0x00,
 | |
| });
 | |
| 
 | |
| static int ssam_kip_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state)
 | |
| {
 | |
| 	int status;
 | |
| 	u8 connected;
 | |
| 
 | |
| 	status = ssam_retry(__ssam_kip_query_state, hub->sdev->ctrl, &connected);
 | |
| 	if (status < 0) {
 | |
| 		dev_err(&hub->sdev->dev, "failed to query KIP connection state: %d\n", status);
 | |
| 		return status;
 | |
| 	}
 | |
| 
 | |
| 	*state = connected ? SSAM_HUB_CONNECTED : SSAM_HUB_DISCONNECTED;
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| static u32 ssam_kip_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
 | |
| {
 | |
| 	struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);
 | |
| 
 | |
| 	if (event->command_id != SSAM_EVENT_KIP_CID_CONNECTION)
 | |
| 		return 0;	/* Return "unhandled". */
 | |
| 
 | |
| 	if (event->length < 1) {
 | |
| 		dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);
 | |
| 		return 0;
 | |
| 	}
 | |
| 
 | |
| 	ssam_hub_update(hub, event->data[0]);
 | |
| 	return SSAM_NOTIF_HANDLED;
 | |
| }
 | |
| 
 | |
| static const struct ssam_hub_desc kip_hub = {
 | |
| 	.event = {
 | |
| 		.reg = SSAM_EVENT_REGISTRY_SAM,
 | |
| 		.id = {
 | |
| 			.target_category = SSAM_SSH_TC_KIP,
 | |
| 			.instance = 0,
 | |
| 		},
 | |
| 		.mask = SSAM_EVENT_MASK_TARGET,
 | |
| 	},
 | |
| 	.ops = {
 | |
| 		.notify = ssam_kip_hub_notif,
 | |
| 		.get_state = ssam_kip_hub_query_state,
 | |
| 	},
 | |
| 	.connect_delay_ms = SSAM_KIP_UPDATE_CONNECT_DELAY,
 | |
| };
 | |
| 
 | |
| 
 | |
| /* -- Driver registration. -------------------------------------------------- */
 | |
| 
 | |
| static const struct ssam_device_id ssam_hub_match[] = {
 | |
| 	{ SSAM_VDEV(HUB, 0x01, SSAM_SSH_TC_KIP, 0x00), (unsigned long)&kip_hub  },
 | |
| 	{ SSAM_VDEV(HUB, 0x02, SSAM_SSH_TC_BAS, 0x00), (unsigned long)&base_hub },
 | |
| 	{ }
 | |
| };
 | |
| MODULE_DEVICE_TABLE(ssam, ssam_hub_match);
 | |
| 
 | |
| static struct ssam_device_driver ssam_subsystem_hub_driver = {
 | |
| 	.probe = ssam_hub_probe,
 | |
| 	.remove = ssam_hub_remove,
 | |
| 	.match_table = ssam_hub_match,
 | |
| 	.driver = {
 | |
| 		.name = "surface_aggregator_subsystem_hub",
 | |
| 		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
 | |
| 		.pm = &ssam_hub_pm_ops,
 | |
| 	},
 | |
| };
 | |
| module_ssam_device_driver(ssam_subsystem_hub_driver);
 | |
| 
 | |
| MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
 | |
| MODULE_DESCRIPTION("Subsystem device hub driver for Surface System Aggregator Module");
 | |
| MODULE_LICENSE("GPL");
 |