| // 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), (unsigned long)&kip_hub }, |
| { SSAM_VDEV(HUB, SAM, 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"); |