| // 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 = SSAM_SSH_TID_SAM, | |
| .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 = SSAM_SSH_TID_SAM, | |
| .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, SAM, SSAM_SSH_TC_KIP, 0x00), | |
| .driver_data = (unsigned long)&kip_hub, | |
| }, { | |
| SSAM_VDEV(HUB, SAM, SSAM_SSH_TC_BAS, 0x00), | |
| .driver_data = (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"); |