Back to home page

OSCL-LXR

 
 

    


0001 // SPDX-License-Identifier: GPL-2.0+
0002 /*
0003  * Driver for Surface System Aggregator Module (SSAM) subsystem device hubs.
0004  *
0005  * Provides a driver for SSAM subsystems device hubs. This driver performs
0006  * instantiation of the devices managed by said hubs and takes care of
0007  * (hot-)removal.
0008  *
0009  * Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com>
0010  */
0011 
0012 #include <linux/kernel.h>
0013 #include <linux/limits.h>
0014 #include <linux/module.h>
0015 #include <linux/types.h>
0016 #include <linux/workqueue.h>
0017 
0018 #include <linux/surface_aggregator/device.h>
0019 
0020 
0021 /* -- SSAM generic subsystem hub driver framework. -------------------------- */
0022 
0023 enum ssam_hub_state {
0024     SSAM_HUB_UNINITIALIZED,     /* Only set during initialization. */
0025     SSAM_HUB_CONNECTED,
0026     SSAM_HUB_DISCONNECTED,
0027 };
0028 
0029 enum ssam_hub_flags {
0030     SSAM_HUB_HOT_REMOVED,
0031 };
0032 
0033 struct ssam_hub;
0034 
0035 struct ssam_hub_ops {
0036     int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state);
0037 };
0038 
0039 struct ssam_hub {
0040     struct ssam_device *sdev;
0041 
0042     enum ssam_hub_state state;
0043     unsigned long flags;
0044 
0045     struct delayed_work update_work;
0046     unsigned long connect_delay;
0047 
0048     struct ssam_event_notifier notif;
0049     struct ssam_hub_ops ops;
0050 };
0051 
0052 struct ssam_hub_desc {
0053     struct {
0054         struct ssam_event_registry reg;
0055         struct ssam_event_id id;
0056         enum ssam_event_mask mask;
0057     } event;
0058 
0059     struct {
0060         u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event);
0061         int (*get_state)(struct ssam_hub *hub, enum ssam_hub_state *state);
0062     } ops;
0063 
0064     unsigned long connect_delay_ms;
0065 };
0066 
0067 static void ssam_hub_update_workfn(struct work_struct *work)
0068 {
0069     struct ssam_hub *hub = container_of(work, struct ssam_hub, update_work.work);
0070     enum ssam_hub_state state;
0071     int status = 0;
0072 
0073     status = hub->ops.get_state(hub, &state);
0074     if (status)
0075         return;
0076 
0077     /*
0078      * There is a small possibility that hub devices were hot-removed and
0079      * re-added before we were able to remove them here. In that case, both
0080      * the state returned by get_state() and the state of the hub will
0081      * equal SSAM_HUB_CONNECTED and we would bail early below, which would
0082      * leave child devices without proper (re-)initialization and the
0083      * hot-remove flag set.
0084      *
0085      * Therefore, we check whether devices have been hot-removed via an
0086      * additional flag on the hub and, in this case, override the returned
0087      * hub state. In case of a missed disconnect (i.e. get_state returned
0088      * "connected"), we further need to re-schedule this work (with the
0089      * appropriate delay) as the actual connect work submission might have
0090      * been merged with this one.
0091      *
0092      * This then leads to one of two cases: Either we submit an unnecessary
0093      * work item (which will get ignored via either the queue or the state
0094      * checks) or, in the unlikely case that the work is actually required,
0095      * double the normal connect delay.
0096      */
0097     if (test_and_clear_bit(SSAM_HUB_HOT_REMOVED, &hub->flags)) {
0098         if (state == SSAM_HUB_CONNECTED)
0099             schedule_delayed_work(&hub->update_work, hub->connect_delay);
0100 
0101         state = SSAM_HUB_DISCONNECTED;
0102     }
0103 
0104     if (hub->state == state)
0105         return;
0106     hub->state = state;
0107 
0108     if (hub->state == SSAM_HUB_CONNECTED)
0109         status = ssam_device_register_clients(hub->sdev);
0110     else
0111         ssam_remove_clients(&hub->sdev->dev);
0112 
0113     if (status)
0114         dev_err(&hub->sdev->dev, "failed to update hub child devices: %d\n", status);
0115 }
0116 
0117 static int ssam_hub_mark_hot_removed(struct device *dev, void *_data)
0118 {
0119     struct ssam_device *sdev = to_ssam_device(dev);
0120 
0121     if (is_ssam_device(dev))
0122         ssam_device_mark_hot_removed(sdev);
0123 
0124     return 0;
0125 }
0126 
0127 static void ssam_hub_update(struct ssam_hub *hub, bool connected)
0128 {
0129     unsigned long delay;
0130 
0131     /* Mark devices as hot-removed before we remove any. */
0132     if (!connected) {
0133         set_bit(SSAM_HUB_HOT_REMOVED, &hub->flags);
0134         device_for_each_child_reverse(&hub->sdev->dev, NULL, ssam_hub_mark_hot_removed);
0135     }
0136 
0137     /*
0138      * Delay update when the base/keyboard cover is being connected to give
0139      * devices/EC some time to set up.
0140      */
0141     delay = connected ? hub->connect_delay : 0;
0142 
0143     schedule_delayed_work(&hub->update_work, delay);
0144 }
0145 
0146 static int __maybe_unused ssam_hub_resume(struct device *dev)
0147 {
0148     struct ssam_hub *hub = dev_get_drvdata(dev);
0149 
0150     schedule_delayed_work(&hub->update_work, 0);
0151     return 0;
0152 }
0153 static SIMPLE_DEV_PM_OPS(ssam_hub_pm_ops, NULL, ssam_hub_resume);
0154 
0155 static int ssam_hub_probe(struct ssam_device *sdev)
0156 {
0157     const struct ssam_hub_desc *desc;
0158     struct ssam_hub *hub;
0159     int status;
0160 
0161     desc = ssam_device_get_match_data(sdev);
0162     if (!desc) {
0163         WARN(1, "no driver match data specified");
0164         return -EINVAL;
0165     }
0166 
0167     hub = devm_kzalloc(&sdev->dev, sizeof(*hub), GFP_KERNEL);
0168     if (!hub)
0169         return -ENOMEM;
0170 
0171     hub->sdev = sdev;
0172     hub->state = SSAM_HUB_UNINITIALIZED;
0173 
0174     hub->notif.base.priority = INT_MAX;  /* This notifier should run first. */
0175     hub->notif.base.fn = desc->ops.notify;
0176     hub->notif.event.reg = desc->event.reg;
0177     hub->notif.event.id = desc->event.id;
0178     hub->notif.event.mask = desc->event.mask;
0179     hub->notif.event.flags = SSAM_EVENT_SEQUENCED;
0180 
0181     hub->connect_delay = msecs_to_jiffies(desc->connect_delay_ms);
0182     hub->ops.get_state = desc->ops.get_state;
0183 
0184     INIT_DELAYED_WORK(&hub->update_work, ssam_hub_update_workfn);
0185 
0186     ssam_device_set_drvdata(sdev, hub);
0187 
0188     status = ssam_device_notifier_register(sdev, &hub->notif);
0189     if (status)
0190         return status;
0191 
0192     schedule_delayed_work(&hub->update_work, 0);
0193     return 0;
0194 }
0195 
0196 static void ssam_hub_remove(struct ssam_device *sdev)
0197 {
0198     struct ssam_hub *hub = ssam_device_get_drvdata(sdev);
0199 
0200     ssam_device_notifier_unregister(sdev, &hub->notif);
0201     cancel_delayed_work_sync(&hub->update_work);
0202     ssam_remove_clients(&sdev->dev);
0203 }
0204 
0205 
0206 /* -- SSAM base-subsystem hub driver. --------------------------------------- */
0207 
0208 /*
0209  * Some devices (especially battery) may need a bit of time to be fully usable
0210  * after being (re-)connected. This delay has been determined via
0211  * experimentation.
0212  */
0213 #define SSAM_BASE_UPDATE_CONNECT_DELAY      2500
0214 
0215 SSAM_DEFINE_SYNC_REQUEST_R(ssam_bas_query_opmode, u8, {
0216     .target_category = SSAM_SSH_TC_BAS,
0217     .target_id       = 0x01,
0218     .command_id      = 0x0d,
0219     .instance_id     = 0x00,
0220 });
0221 
0222 #define SSAM_BAS_OPMODE_TABLET      0x00
0223 #define SSAM_EVENT_BAS_CID_CONNECTION   0x0c
0224 
0225 static int ssam_base_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state)
0226 {
0227     u8 opmode;
0228     int status;
0229 
0230     status = ssam_retry(ssam_bas_query_opmode, hub->sdev->ctrl, &opmode);
0231     if (status < 0) {
0232         dev_err(&hub->sdev->dev, "failed to query base state: %d\n", status);
0233         return status;
0234     }
0235 
0236     if (opmode != SSAM_BAS_OPMODE_TABLET)
0237         *state = SSAM_HUB_CONNECTED;
0238     else
0239         *state = SSAM_HUB_DISCONNECTED;
0240 
0241     return 0;
0242 }
0243 
0244 static u32 ssam_base_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
0245 {
0246     struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);
0247 
0248     if (event->command_id != SSAM_EVENT_BAS_CID_CONNECTION)
0249         return 0;
0250 
0251     if (event->length < 1) {
0252         dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);
0253         return 0;
0254     }
0255 
0256     ssam_hub_update(hub, event->data[0]);
0257 
0258     /*
0259      * Do not return SSAM_NOTIF_HANDLED: The event should be picked up and
0260      * consumed by the detachment system driver. We're just a (more or less)
0261      * silent observer.
0262      */
0263     return 0;
0264 }
0265 
0266 static const struct ssam_hub_desc base_hub = {
0267     .event = {
0268         .reg = SSAM_EVENT_REGISTRY_SAM,
0269         .id = {
0270             .target_category = SSAM_SSH_TC_BAS,
0271             .instance = 0,
0272         },
0273         .mask = SSAM_EVENT_MASK_NONE,
0274     },
0275     .ops = {
0276         .notify = ssam_base_hub_notif,
0277         .get_state = ssam_base_hub_query_state,
0278     },
0279     .connect_delay_ms = SSAM_BASE_UPDATE_CONNECT_DELAY,
0280 };
0281 
0282 
0283 /* -- SSAM KIP-subsystem hub driver. ---------------------------------------- */
0284 
0285 /*
0286  * Some devices may need a bit of time to be fully usable after being
0287  * (re-)connected. This delay has been determined via experimentation.
0288  */
0289 #define SSAM_KIP_UPDATE_CONNECT_DELAY       250
0290 
0291 #define SSAM_EVENT_KIP_CID_CONNECTION       0x2c
0292 
0293 SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_query_state, u8, {
0294     .target_category = SSAM_SSH_TC_KIP,
0295     .target_id       = 0x01,
0296     .command_id      = 0x2c,
0297     .instance_id     = 0x00,
0298 });
0299 
0300 static int ssam_kip_hub_query_state(struct ssam_hub *hub, enum ssam_hub_state *state)
0301 {
0302     int status;
0303     u8 connected;
0304 
0305     status = ssam_retry(__ssam_kip_query_state, hub->sdev->ctrl, &connected);
0306     if (status < 0) {
0307         dev_err(&hub->sdev->dev, "failed to query KIP connection state: %d\n", status);
0308         return status;
0309     }
0310 
0311     *state = connected ? SSAM_HUB_CONNECTED : SSAM_HUB_DISCONNECTED;
0312     return 0;
0313 }
0314 
0315 static u32 ssam_kip_hub_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
0316 {
0317     struct ssam_hub *hub = container_of(nf, struct ssam_hub, notif);
0318 
0319     if (event->command_id != SSAM_EVENT_KIP_CID_CONNECTION)
0320         return 0;   /* Return "unhandled". */
0321 
0322     if (event->length < 1) {
0323         dev_err(&hub->sdev->dev, "unexpected payload size: %u\n", event->length);
0324         return 0;
0325     }
0326 
0327     ssam_hub_update(hub, event->data[0]);
0328     return SSAM_NOTIF_HANDLED;
0329 }
0330 
0331 static const struct ssam_hub_desc kip_hub = {
0332     .event = {
0333         .reg = SSAM_EVENT_REGISTRY_SAM,
0334         .id = {
0335             .target_category = SSAM_SSH_TC_KIP,
0336             .instance = 0,
0337         },
0338         .mask = SSAM_EVENT_MASK_TARGET,
0339     },
0340     .ops = {
0341         .notify = ssam_kip_hub_notif,
0342         .get_state = ssam_kip_hub_query_state,
0343     },
0344     .connect_delay_ms = SSAM_KIP_UPDATE_CONNECT_DELAY,
0345 };
0346 
0347 
0348 /* -- Driver registration. -------------------------------------------------- */
0349 
0350 static const struct ssam_device_id ssam_hub_match[] = {
0351     { SSAM_VDEV(HUB, 0x01, SSAM_SSH_TC_KIP, 0x00), (unsigned long)&kip_hub  },
0352     { SSAM_VDEV(HUB, 0x02, SSAM_SSH_TC_BAS, 0x00), (unsigned long)&base_hub },
0353     { }
0354 };
0355 MODULE_DEVICE_TABLE(ssam, ssam_hub_match);
0356 
0357 static struct ssam_device_driver ssam_subsystem_hub_driver = {
0358     .probe = ssam_hub_probe,
0359     .remove = ssam_hub_remove,
0360     .match_table = ssam_hub_match,
0361     .driver = {
0362         .name = "surface_aggregator_subsystem_hub",
0363         .probe_type = PROBE_PREFER_ASYNCHRONOUS,
0364         .pm = &ssam_hub_pm_ops,
0365     },
0366 };
0367 module_ssam_device_driver(ssam_subsystem_hub_driver);
0368 
0369 MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
0370 MODULE_DESCRIPTION("Subsystem device hub driver for Surface System Aggregator Module");
0371 MODULE_LICENSE("GPL");