brintos

brintos / linux-shallow public Read only

0
0
Text · 9.7 KiB · 8b8b802 Raw
372 lines · c
1// SPDX-License-Identifier: GPL-2.0+2/*3 * Driver for Surface System Aggregator Module (SSAM) subsystem device hubs.4 *5 * Provides a driver for SSAM subsystems device hubs. This driver performs6 * instantiation of the devices managed by said hubs and takes care of7 * (hot-)removal.8 *9 * Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com>10 */11 12#include <linux/kernel.h>13#include <linux/limits.h>14#include <linux/module.h>15#include <linux/types.h>16#include <linux/workqueue.h>17 18#include <linux/surface_aggregator/device.h>19 20 21/* -- SSAM generic subsystem hub driver framework. -------------------------- */22 23enum ssam_hub_state {24	SSAM_HUB_UNINITIALIZED,		/* Only set during initialization. */25	SSAM_HUB_CONNECTED,26	SSAM_HUB_DISCONNECTED,27};28 29enum ssam_hub_flags {30	SSAM_HUB_HOT_REMOVED,31};32 33struct ssam_hub;34 35struct ssam_hub_ops {36	int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state);37};38 39struct ssam_hub {40	struct ssam_device *sdev;41 42	enum ssam_hub_state state;43	unsigned long flags;44 45	struct delayed_work update_work;46	unsigned long connect_delay;47 48	struct ssam_event_notifier notif;49	struct ssam_hub_ops ops;50};51 52struct ssam_hub_desc {53	struct {54		struct ssam_event_registry reg;55		struct ssam_event_id id;56		enum ssam_event_mask mask;57	} event;58 59	struct {60		u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event);61		int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state);62	} ops;63 64	unsigned long connect_delay_ms;65};66 67static void ssam_hub_update_workfn(struct work_struct *work)68{69	struct ssam_hub *hub = container_of(work, struct ssam_hub, update_work.work);70	enum ssam_hub_state state;71	int status = 0;72 73	status = hub->ops.get_state(hub, &state);74	if (status)75		return;76 77	/*78	 * There is a small possibility that hub devices were hot-removed and79	 * re-added before we were able to remove them here. In that case, both80	 * the state returned by get_state() and the state of the hub will81	 * equal SSAM_HUB_CONNECTED and we would bail early below, which would82	 * leave child devices without proper (re-)initialization and the83	 * hot-remove flag set.84	 *85	 * Therefore, we check whether devices have been hot-removed via an86	 * additional flag on the hub and, in this case, override the returned87	 * hub state. In case of a missed disconnect (i.e. get_state returned88	 * "connected"), we further need to re-schedule this work (with the89	 * appropriate delay) as the actual connect work submission might have90	 * been merged with this one.91	 *92	 * This then leads to one of two cases: Either we submit an unnecessary93	 * work item (which will get ignored via either the queue or the state94	 * checks) or, in the unlikely case that the work is actually required,95	 * double the normal connect delay.96	 */97	if (test_and_clear_bit(SSAM_HUB_HOT_REMOVED, &hub->flags)) {98		if (state == SSAM_HUB_CONNECTED)99			schedule_delayed_work(&hub->update_work, hub->connect_delay);100 101		state = SSAM_HUB_DISCONNECTED;102	}103 104	if (hub->state == state)105		return;106	hub->state = state;107 108	if (hub->state == SSAM_HUB_CONNECTED)109		status = ssam_device_register_clients(hub->sdev);110	else111		ssam_remove_clients(&hub->sdev->dev);112 113	if (status)114		dev_err(&hub->sdev->dev, "failed to update hub child devices: %d\n", status);115}116 117static int ssam_hub_mark_hot_removed(struct device *dev, void *_data)118{119	struct ssam_device *sdev = to_ssam_device(dev);120 121	if (is_ssam_device(dev))122		ssam_device_mark_hot_removed(sdev);123 124	return 0;125}126 127static void ssam_hub_update(struct ssam_hub *hub, bool connected)128{129	unsigned long delay;130 131	/* Mark devices as hot-removed before we remove any. */132	if (!connected) {133		set_bit(SSAM_HUB_HOT_REMOVED, &hub->flags);134		device_for_each_child_reverse(&hub->sdev->dev, NULL, ssam_hub_mark_hot_removed);135	}136 137	/*138	 * Delay update when the base/keyboard cover is being connected to give139	 * devices/EC some time to set up.140	 */141	delay = connected ? hub->connect_delay : 0;142 143	schedule_delayed_work(&hub->update_work, delay);144}145 146static int __maybe_unused ssam_hub_resume(struct device *dev)147{148	struct ssam_hub *hub = dev_get_drvdata(dev);149 150	schedule_delayed_work(&hub->update_work, 0);151	return 0;152}153static SIMPLE_DEV_PM_OPS(ssam_hub_pm_ops, NULL, ssam_hub_resume);154 155static int ssam_hub_probe(struct ssam_device *sdev)156{157	const struct ssam_hub_desc *desc;158	struct ssam_hub *hub;159	int status;160 161	desc = ssam_device_get_match_data(sdev);162	if (!desc) {163		WARN(1, "no driver match data specified");164		return -EINVAL;165	}166 167	hub = devm_kzalloc(&sdev->dev, sizeof(*hub), GFP_KERNEL);168	if (!hub)169		return -ENOMEM;170 171	hub->sdev = sdev;172	hub->state = SSAM_HUB_UNINITIALIZED;173 174	hub->notif.base.priority = INT_MAX;  /* This notifier should run first. */175	hub->notif.base.fn = desc->ops.notify;176	hub->notif.event.reg = desc->event.reg;177	hub->notif.event.id = desc->event.id;178	hub->notif.event.mask = desc->event.mask;179	hub->notif.event.flags = SSAM_EVENT_SEQUENCED;180 181	hub->connect_delay = msecs_to_jiffies(desc->connect_delay_ms);182	hub->ops.get_state = desc->ops.get_state;183 184	INIT_DELAYED_WORK(&hub->update_work, ssam_hub_update_workfn);185 186	ssam_device_set_drvdata(sdev, hub);187 188	status = ssam_device_notifier_register(sdev, &hub->notif);189	if (status)190		return status;191 192	schedule_delayed_work(&hub->update_work, 0);193	return 0;194}195 196static void ssam_hub_remove(struct ssam_device *sdev)197{198	struct ssam_hub *hub = ssam_device_get_drvdata(sdev);199 200	ssam_device_notifier_unregister(sdev, &hub->notif);201	cancel_delayed_work_sync(&hub->update_work);202	ssam_remove_clients(&sdev->dev);203}204 205 206/* -- SSAM base-subsystem hub driver. --------------------------------------- */207 208/*209 * Some devices (especially battery) may need a bit of time to be fully usable210 * after being (re-)connected. This delay has been determined via211 * experimentation.212 */213#define SSAM_BASE_UPDATE_CONNECT_DELAY		2500214 215SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode, u8, {216	.target_category = SSAM_SSH_TC_BAS,217	.target_id       = SSAM_SSH_TID_SAM,218	.command_id      = 0x0d,219	.instance_id     = 0x00,220});221 222#define SSAM_BAS_OPMODE_TABLET		0x00223#define SSAM_EVENT_BAS_CID_CONNECTION	0x0c224 225static int ssam_base_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state)226{227	u8 opmode;228	int status;229 230	status = ssam_retry(ssam_bas_query_opmode, hub->sdev->ctrl, &opmode);231	if (status < 0) {232		dev_err(&hub->sdev->dev, "failed to query base state: %d\n", status);233		return status;234	}235 236	if (opmode != SSAM_BAS_OPMODE_TABLET)237		*state = SSAM_HUB_CONNECTED;238	else239		*state = SSAM_HUB_DISCONNECTED;240 241	return 0;242}243 244static u32 ssam_base_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)245{246	struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);247 248	if (event->command_id != SSAM_EVENT_BAS_CID_CONNECTION)249		return 0;250 251	if (event->length < 1) {252		dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);253		return 0;254	}255 256	ssam_hub_update(hub, event->data[0]);257 258	/*259	 * Do not return SSAM_NOTIF_HANDLED: The event should be picked up and260	 * consumed by the detachment system driver. We're just a (more or less)261	 * silent observer.262	 */263	return 0;264}265 266static const struct ssam_hub_desc base_hub = {267	.event = {268		.reg = SSAM_EVENT_REGISTRY_SAM,269		.id = {270			.target_category = SSAM_SSH_TC_BAS,271			.instance = 0,272		},273		.mask = SSAM_EVENT_MASK_NONE,274	},275	.ops = {276		.notify = ssam_base_hub_notif,277		.get_state = ssam_base_hub_query_state,278	},279	.connect_delay_ms = SSAM_BASE_UPDATE_CONNECT_DELAY,280};281 282 283/* -- SSAM KIP-subsystem hub driver. ---------------------------------------- */284 285/*286 * Some devices may need a bit of time to be fully usable after being287 * (re-)connected. This delay has been determined via experimentation.288 */289#define SSAM_KIP_UPDATE_CONNECT_DELAY		250290 291#define SSAM_EVENT_KIP_CID_CONNECTION		0x2c292 293SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_query_state, u8, {294	.target_category = SSAM_SSH_TC_KIP,295	.target_id       = SSAM_SSH_TID_SAM,296	.command_id      = 0x2c,297	.instance_id     = 0x00,298});299 300static int ssam_kip_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state)301{302	int status;303	u8 connected;304 305	status = ssam_retry(__ssam_kip_query_state, hub->sdev->ctrl, &connected);306	if (status < 0) {307		dev_err(&hub->sdev->dev, "failed to query KIP connection state: %d\n", status);308		return status;309	}310 311	*state = connected ? SSAM_HUB_CONNECTED : SSAM_HUB_DISCONNECTED;312	return 0;313}314 315static u32 ssam_kip_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)316{317	struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);318 319	if (event->command_id != SSAM_EVENT_KIP_CID_CONNECTION)320		return 0;	/* Return "unhandled". */321 322	if (event->length < 1) {323		dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);324		return 0;325	}326 327	ssam_hub_update(hub, event->data[0]);328	return SSAM_NOTIF_HANDLED;329}330 331static const struct ssam_hub_desc kip_hub = {332	.event = {333		.reg = SSAM_EVENT_REGISTRY_SAM,334		.id = {335			.target_category = SSAM_SSH_TC_KIP,336			.instance = 0,337		},338		.mask = SSAM_EVENT_MASK_TARGET,339	},340	.ops = {341		.notify = ssam_kip_hub_notif,342		.get_state = ssam_kip_hub_query_state,343	},344	.connect_delay_ms = SSAM_KIP_UPDATE_CONNECT_DELAY,345};346 347 348/* -- Driver registration. -------------------------------------------------- */349 350static const struct ssam_device_id ssam_hub_match[] = {351	{ SSAM_VDEV(HUB, SAM, SSAM_SSH_TC_KIP, 0x00), (unsigned long)&kip_hub  },352	{ SSAM_VDEV(HUB, SAM, SSAM_SSH_TC_BAS, 0x00), (unsigned long)&base_hub },353	{ }354};355MODULE_DEVICE_TABLE(ssam, ssam_hub_match);356 357static struct ssam_device_driver ssam_subsystem_hub_driver = {358	.probe = ssam_hub_probe,359	.remove = ssam_hub_remove,360	.match_table = ssam_hub_match,361	.driver = {362		.name = "surface_aggregator_subsystem_hub",363		.probe_type = PROBE_PREFER_ASYNCHRONOUS,364		.pm = &ssam_hub_pm_ops,365	},366};367module_ssam_device_driver(ssam_subsystem_hub_driver);368 369MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");370MODULE_DESCRIPTION("Subsystem device hub driver for Surface System Aggregator Module");371MODULE_LICENSE("GPL");372