| // SPDX-License-Identifier: GPL-2.0-only |
| /* Copyright 2020 Google LLC. All Rights Reserved. |
| * |
| * Interface to the AoC USB control service |
| */ |
| |
| #define pr_fmt(fmt) "aoc_usb_control: " fmt |
| |
| #include <linux/device.h> |
| #include <linux/init.h> |
| #include <linux/jiffies.h> |
| #include <linux/module.h> |
| #include <linux/of_platform.h> |
| #include <linux/platform_device.h> |
| #include <linux/slab.h> |
| |
| #include "aoc.h" |
| #include "aoc-interface.h" |
| |
| #include "aoc_usb.h" |
| |
| #define AOC_USB_NAME "aoc_usb" |
| |
| enum { NONBLOCKING = 0, BLOCKING = 1 }; |
| |
| enum { |
| TYPE_SCRATCH_PAD = 0, |
| TYPE_DEVICE_CONTEXT, |
| TYPE_END_OF_SETUP, |
| TYPE_DCBAA |
| }; |
| |
| static ssize_t aoc_usb_send_command(struct aoc_usb_drvdata *drvdata, |
| void *in_cmd, size_t in_size, void *out_cmd, |
| size_t out_size) |
| { |
| struct aoc_service_dev *adev = drvdata->adev; |
| ssize_t ret; |
| |
| ret = mutex_lock_interruptible(&drvdata->lock); |
| if (ret != 0) |
| return ret; |
| |
| __pm_stay_awake(drvdata->ws); |
| |
| if (aoc_service_flush_read_data(adev)) |
| dev_err(&drvdata->adev->dev ,"Previous response left in channel\n"); |
| |
| dev_dbg(&drvdata->adev->dev, "send cmd id [%u]\n", ((struct CMD_CORE_GENERIC *)in_cmd)->parent.id); |
| |
| ret = aoc_service_write_timeout(adev, in_cmd, in_size, drvdata->service_timeout); |
| if (ret != in_size) { |
| ret = -EIO; |
| goto out; |
| } |
| |
| ret = aoc_service_read_timeout(adev, out_cmd, out_size, drvdata->service_timeout); |
| if (ret != out_size) |
| ret = -EIO; |
| |
| out: |
| __pm_relax(drvdata->ws); |
| mutex_unlock(&drvdata->lock); |
| return ret; |
| } |
| |
| static int aoc_usb_get_dev_ctx(struct aoc_usb_drvdata *drvdata, |
| unsigned int slot_id, size_t length, u8 *dev_ctx) |
| { |
| int ret = 0; |
| struct CMD_USB_CONTROL_GET_DEVICE_CONTEXT *cmd; |
| |
| cmd = kzalloc(sizeof(struct CMD_USB_CONTROL_GET_DEVICE_CONTEXT), GFP_KERNEL); |
| if (!cmd) |
| return -ENOMEM; |
| |
| AocCmdHdrSet(&cmd->parent, |
| CMD_USB_CONTROL_GET_DEVICE_CONTEXT_ID, |
| sizeof(*cmd)); |
| |
| cmd->device_id = slot_id; |
| cmd->length = length; |
| |
| dev_dbg(&drvdata->adev->dev, "cmd=(%u, %u)\n", cmd->device_id, cmd->length); |
| ret = aoc_usb_send_command(drvdata, cmd, sizeof(*cmd), cmd, sizeof(*cmd)); |
| if (ret < 0) { |
| kfree(cmd); |
| return ret; |
| } |
| |
| memcpy(dev_ctx, cmd->payload, length); |
| |
| kfree(cmd); |
| |
| return 0; |
| } |
| |
| static int aoc_usb_get_dcbaa_ptr(struct aoc_usb_drvdata *drvdata, |
| u64 *aoc_dcbaa_ptr) |
| { |
| int ret = 0; |
| struct CMD_USB_CONTROL_GET_DCBAA_PTR *cmd; |
| |
| cmd = kzalloc(sizeof(struct CMD_USB_CONTROL_GET_DCBAA_PTR), GFP_KERNEL); |
| if (!cmd) |
| return -ENOMEM; |
| |
| AocCmdHdrSet(&cmd->parent, |
| CMD_USB_CONTROL_GET_DCBAA_PTR_ID, |
| sizeof(*cmd)); |
| |
| ret = aoc_usb_send_command(drvdata, cmd, sizeof(*cmd), cmd, sizeof(*cmd)); |
| if (ret < 0) { |
| kfree(cmd); |
| return ret; |
| } |
| |
| *aoc_dcbaa_ptr = cmd->aoc_dcbaa_ptr; |
| |
| kfree(cmd); |
| |
| return 0; |
| } |
| |
| static int aoc_usb_set_dcbaa_ptr(struct aoc_usb_drvdata *drvdata, |
| u64 *aoc_dcbaa_ptr) |
| { |
| int ret = 0; |
| struct CMD_USB_CONTROL_SET_DCBAA_PTR *cmd; |
| |
| cmd = kzalloc(sizeof(struct CMD_USB_CONTROL_SET_DCBAA_PTR), GFP_KERNEL); |
| if (!cmd) |
| return -ENOMEM; |
| |
| AocCmdHdrSet(&cmd->parent, |
| CMD_USB_CONTROL_SET_DCBAA_PTR_ID, |
| sizeof(*cmd)); |
| |
| cmd->aoc_dcbaa_ptr = *aoc_dcbaa_ptr; |
| ret = aoc_usb_send_command(drvdata, cmd, sizeof(*cmd), cmd, sizeof(*cmd)); |
| if (ret < 0) { |
| kfree(cmd); |
| return ret; |
| } |
| |
| kfree(cmd); |
| |
| return 0; |
| } |
| |
| static int aoc_usb_setup_done(struct aoc_usb_drvdata *drvdata) |
| { |
| int ret; |
| struct CMD_USB_CONTROL_SETUP *cmd; |
| |
| cmd = kzalloc(sizeof(struct CMD_USB_CONTROL_SETUP), GFP_KERNEL); |
| if (!cmd) |
| return -ENOMEM; |
| |
| AocCmdHdrSet(&cmd->parent, |
| CMD_USB_CONTROL_SETUP_ID, |
| sizeof(*cmd)); |
| |
| cmd->type = TYPE_END_OF_SETUP; |
| cmd->ctx_idx = 0; |
| cmd->spbuf_idx = 0; |
| cmd->length = 0; |
| ret = aoc_usb_send_command(drvdata, cmd, sizeof(*cmd), cmd, sizeof(*cmd)); |
| if (ret < 0) { |
| kfree(cmd); |
| return ret; |
| } |
| |
| kfree(cmd); |
| |
| return 0; |
| } |
| |
| static int aoc_usb_notify_conn_stat(struct aoc_usb_drvdata *drvdata, void *data) |
| { |
| int ret = 0; |
| struct CMD_USB_CONTROL_NOTIFY_CONN_STAT_V2 *cmd; |
| struct conn_stat_args *args = data; |
| |
| // Don't update usb audio device counter if the notification is for xhci driver state. |
| if (args->bus_id != 0 && args->dev_num != 0 && args->slot_id != 0) { |
| if (args->conn_stat) |
| drvdata->usb_conn_state++; |
| else |
| drvdata->usb_conn_state--; |
| |
| dev_dbg(&drvdata->adev->dev, "currently connected usb audio device count = %u\n", |
| drvdata->usb_conn_state); |
| } |
| |
| cmd = kzalloc(sizeof(struct CMD_USB_CONTROL_NOTIFY_CONN_STAT_V2), GFP_KERNEL); |
| if (!cmd) |
| return -ENOMEM; |
| |
| AocCmdHdrSet(&cmd->parent, |
| CMD_USB_CONTROL_NOTIFY_CONN_STAT_V2_ID, |
| sizeof(*cmd)); |
| |
| cmd->bus_id = args->bus_id; |
| cmd->dev_num = args->dev_num; |
| cmd->slot_id = args->slot_id; |
| cmd->conn_state = args->conn_stat; |
| |
| ret = aoc_usb_send_command(drvdata, cmd, sizeof(*cmd), cmd, sizeof(*cmd)); |
| if (ret < 0) { |
| kfree(cmd); |
| return ret; |
| } |
| |
| kfree(cmd); |
| |
| return 0; |
| } |
| |
| static int aoc_usb_get_isoc_tr_info(struct aoc_usb_drvdata *drvdata, void *args) |
| { |
| int ret; |
| struct get_isoc_tr_info_args *tr_info_args = |
| (struct get_isoc_tr_info_args *)args; |
| struct CMD_USB_CONTROL_GET_ISOC_TR_INFO *cmd; |
| |
| cmd = kzalloc(sizeof(struct CMD_USB_CONTROL_GET_ISOC_TR_INFO), GFP_KERNEL); |
| if (!cmd) |
| return -ENOMEM; |
| |
| AocCmdHdrSet(&cmd->parent, |
| CMD_USB_CONTROL_GET_ISOC_TR_INFO_ID, |
| sizeof(*cmd)); |
| |
| cmd->ep_id = tr_info_args->ep_id; |
| cmd->dir = tr_info_args->dir; |
| |
| dev_dbg(&drvdata->adev->dev, "ep_id=%u, dir=%u\n", cmd->ep_id, cmd->dir); |
| ret = aoc_usb_send_command(drvdata, cmd, sizeof(*cmd), cmd, sizeof(*cmd)); |
| if (ret < 0) { |
| kfree(cmd); |
| return ret; |
| } |
| |
| tr_info_args->type = cmd->type; |
| tr_info_args->num_segs = cmd->num_segs; |
| tr_info_args->seg_ptr = cmd->seg_ptr; |
| tr_info_args->max_packet = cmd->max_packet; |
| tr_info_args->cycle_state = cmd->cycle_state; |
| tr_info_args->num_trbs_free = cmd->num_trbs_free; |
| |
| kfree(cmd); |
| |
| return 0; |
| } |
| |
| static int aoc_usb_set_isoc_tr_info(struct aoc_usb_drvdata *drvdata, void *args) |
| { |
| int ret; |
| struct get_isoc_tr_info_args *tr_info_args = |
| (struct get_isoc_tr_info_args *)args; |
| struct CMD_USB_CONTROL_SET_ISOC_TR_INFO *cmd; |
| |
| cmd = kzalloc(sizeof(struct CMD_USB_CONTROL_SET_ISOC_TR_INFO), GFP_KERNEL); |
| if (!cmd) |
| return -ENOMEM; |
| |
| AocCmdHdrSet(&cmd->parent, |
| CMD_USB_CONTROL_SET_ISOC_TR_INFO_ID, |
| sizeof(*cmd)); |
| |
| cmd->ep_id = tr_info_args->ep_id; |
| cmd->dir = tr_info_args->dir; |
| cmd->type = tr_info_args->type; |
| cmd->num_segs = tr_info_args->num_segs; |
| cmd->seg_ptr = tr_info_args->seg_ptr; |
| cmd->max_packet = tr_info_args->max_packet; |
| cmd->cycle_state = tr_info_args->cycle_state; |
| cmd->num_trbs_free = tr_info_args->num_trbs_free; |
| |
| dev_dbg(&drvdata->adev->dev, "%s: ep_id=%u, dir=%u\n", __func__, cmd->ep_id, cmd->dir); |
| ret = aoc_usb_send_command(drvdata, cmd, sizeof(*cmd), cmd, sizeof(*cmd)); |
| if (ret < 0) { |
| kfree(cmd); |
| return ret; |
| } |
| |
| kfree(cmd); |
| |
| return 0; |
| } |
| |
| static int aoc_usb_set_offload_state(struct aoc_usb_drvdata *drvdata, bool *enabled) |
| { |
| int ret = 0; |
| struct CMD_USB_CONTROL_SET_OFFLOAD_STATE *cmd; |
| |
| cmd = kzalloc(sizeof(struct CMD_USB_CONTROL_SET_OFFLOAD_STATE), GFP_KERNEL); |
| if (!cmd) |
| return -ENOMEM; |
| |
| AocCmdHdrSet(&cmd->parent, |
| CMD_USB_CONTROL_SET_OFFLOAD_STATE_ID, |
| sizeof(*cmd)); |
| |
| cmd->offloading = *enabled; |
| ret = aoc_usb_send_command(drvdata, cmd, sizeof(*cmd), cmd, sizeof(*cmd)); |
| if (ret < 0) { |
| kfree(cmd); |
| return ret; |
| } |
| |
| kfree(cmd); |
| |
| return 0; |
| } |
| |
| static int aoc_usb_send_feedback_ep_info(struct aoc_usb_drvdata *drvdata, void *args) |
| { |
| int ret = 0; |
| struct feedback_ep_info_args *fb_ep_info_args = |
| (struct feedback_ep_info_args *)args; |
| struct CMD_USB_CONTROL_SEND_FEEDBACK_EP_INFO *cmd; |
| |
| cmd = kzalloc(sizeof(struct CMD_USB_CONTROL_SEND_FEEDBACK_EP_INFO), GFP_KERNEL); |
| if (!cmd) |
| return -ENOMEM; |
| |
| AocCmdHdrSet(&cmd->parent, |
| CMD_USB_CONTROL_SEND_FEEDBACK_EP_INFO_ID, |
| sizeof(*cmd)); |
| |
| cmd->enabled = fb_ep_info_args->enabled; |
| cmd->bus_id = fb_ep_info_args->bus_id; |
| cmd->dev_num = fb_ep_info_args->dev_num; |
| cmd->slot_id = fb_ep_info_args->slot_id; |
| cmd->ep_num = fb_ep_info_args->ep_num; |
| cmd->max_packet = fb_ep_info_args->max_packet; |
| cmd->binterval = fb_ep_info_args->binterval; |
| cmd->brefresh = fb_ep_info_args->brefresh; |
| ret = aoc_usb_send_command(drvdata, cmd, sizeof(*cmd), cmd, sizeof(*cmd)); |
| |
| kfree(cmd); |
| |
| return ret; |
| } |
| |
| static int aoc_usb_notify(struct notifier_block *this, |
| unsigned long code, void *data) |
| { |
| struct aoc_usb_drvdata *drvdata = |
| container_of(this, struct aoc_usb_drvdata, nb); |
| int ret; |
| struct get_dev_ctx_args *dev_ctx_args; |
| |
| switch (code) { |
| case SYNC_DEVICE_CONTEXT: |
| dev_ctx_args = data; |
| ret = aoc_usb_get_dev_ctx(drvdata, dev_ctx_args->slot_id, |
| dev_ctx_args->length, |
| dev_ctx_args->dev_ctx); |
| break; |
| case GET_DCBAA_PTR: |
| ret = aoc_usb_get_dcbaa_ptr(drvdata, data); |
| break; |
| case SET_DCBAA_PTR: |
| ret = aoc_usb_set_dcbaa_ptr(drvdata, data); |
| break; |
| case SETUP_DONE: |
| ret = aoc_usb_setup_done(drvdata); |
| break; |
| case GET_ISOC_TR_INFO: |
| ret = aoc_usb_get_isoc_tr_info(drvdata, data); |
| break; |
| case SET_ISOC_TR_INFO: |
| ret = aoc_usb_set_isoc_tr_info(drvdata, data); |
| break; |
| case SYNC_CONN_STAT: |
| ret = aoc_usb_notify_conn_stat(drvdata, data); |
| break; |
| case SET_OFFLOAD_STATE: |
| ret = aoc_usb_set_offload_state(drvdata, data); |
| break; |
| case SEND_FB_EP_INFO: |
| ret = aoc_usb_send_feedback_ep_info(drvdata, data); |
| break; |
| default: |
| dev_warn(&drvdata->adev->dev, "Code %lu is not supported\n", code); |
| ret = -EINVAL; |
| break; |
| } |
| |
| if (ret < 0) |
| dev_err(&drvdata->adev->dev, "Fail to handle code %lu, ret = %d", code, ret); |
| |
| return ret; |
| } |
| |
| static enum usb_recover_state recover_state; |
| static struct work_struct usb_recovery_ws; |
| static void usb_recovery_work(struct work_struct *ws) |
| { |
| pr_debug("%s: recover_state: %d\n", __func__, recover_state); |
| |
| switch(recover_state) { |
| case RECOVER_HOST_OFF: |
| dwc3_otg_host_enable(false); |
| break; |
| case RECOVER_HOST_ON: |
| dwc3_otg_host_enable(true); |
| recover_state = RECOVERED; |
| break; |
| default: |
| pr_err("%s: unhandled recover_state: %d\n", __func__, recover_state); |
| break; |
| } |
| |
| return; |
| } |
| |
| static int aoc_usb_match(struct device *dev, void *data) |
| { |
| if (sysfs_streq(dev_driver_string(dev), "xhci-hcd-exynos")) |
| return 1; |
| |
| return 0; |
| } |
| |
| static bool aoc_usb_is_hcd_working() |
| { |
| struct device_node *np; |
| struct platform_device *pdev; |
| struct device *udev; |
| int ret; |
| |
| np = of_find_node_by_name(NULL, "dwc3"); |
| if (!np || !of_device_is_available(np)) { |
| pr_err("Cannot find dwc3 device node\n"); |
| return false; |
| } |
| |
| pdev = of_find_device_by_node(np); |
| if (!pdev) |
| return false; |
| |
| udev = device_find_child(&pdev->dev, NULL, aoc_usb_match); |
| if (!udev) |
| return false; |
| |
| ret = usb_host_mode_state_notify(USB_CONNECTED); |
| if (ret) |
| dev_err(udev, "Notifying AoC for xhci driver status is failed.\n"); |
| |
| return true; |
| } |
| |
| static struct work_struct usb_host_mode_checking_ws; |
| static void usb_host_mode_checking_work(struct work_struct *ws) |
| { |
| if (aoc_usb_is_hcd_working()) |
| pr_info("USB HCD is working, send notification to AoC\n"); |
| |
| return; |
| } |
| |
| bool aoc_usb_probe_done; |
| static int aoc_usb_probe(struct aoc_service_dev *adev) |
| { |
| struct device *dev = &adev->dev; |
| struct aoc_usb_drvdata *drvdata; |
| |
| drvdata = kzalloc(sizeof(*drvdata), GFP_KERNEL); |
| if (!drvdata) |
| return -ENOMEM; |
| |
| drvdata->adev = adev; |
| |
| mutex_init(&drvdata->lock); |
| |
| drvdata->ws = wakeup_source_register(dev, dev_name(dev)); |
| if (!drvdata->ws) |
| return -ENOMEM; |
| |
| drvdata->usb_conn_state = 0; |
| drvdata->service_timeout = msecs_to_jiffies(100); |
| drvdata->nb.notifier_call = aoc_usb_notify; |
| register_aoc_usb_notifier(&drvdata->nb); |
| |
| dev_set_drvdata(dev, drvdata); |
| |
| aoc_usb_probe_done = true; |
| |
| /* Restart host if recover_state was triggered */ |
| if (recover_state == RECOVER_HOST_OFF) { |
| dev_dbg(&drvdata->adev->dev, "restart usb device\n"); |
| recover_state = RECOVER_HOST_ON; |
| schedule_work(&usb_recovery_ws); |
| } else { |
| recover_state = NONE; |
| } |
| |
| schedule_work(&usb_host_mode_checking_ws); |
| |
| return 0; |
| } |
| |
| static int aoc_usb_remove(struct aoc_service_dev *adev) |
| { |
| struct aoc_usb_drvdata *drvdata = dev_get_drvdata(&adev->dev); |
| |
| /* |
| * Trigger recovery if usb accessory is connected. |
| * We only disable host at this moment, it will restart host |
| * after aoc usb probe again. |
| */ |
| if (drvdata->usb_conn_state) { |
| dev_dbg(&drvdata->adev->dev, "need to recover usb device\n"); |
| recover_state = RECOVER_HOST_OFF; |
| schedule_work(&usb_recovery_ws); |
| } |
| |
| unregister_aoc_usb_notifier(&drvdata->nb); |
| wakeup_source_unregister(drvdata->ws); |
| mutex_destroy(&drvdata->lock); |
| |
| kfree(drvdata); |
| |
| aoc_usb_probe_done = false; |
| |
| return 0; |
| } |
| |
| static const char *const aoc_usb_service_names[] = { |
| "usb_control", |
| NULL, |
| }; |
| |
| static struct aoc_driver aoc_usb_driver = { |
| .drv = { |
| .name = AOC_USB_NAME, |
| }, |
| .service_names = aoc_usb_service_names, |
| .probe = aoc_usb_probe, |
| .remove = aoc_usb_remove, |
| }; |
| |
| static int __init aoc_usb_init(void) |
| { |
| xhci_vendor_helper_init(); |
| usb_vendor_helper_init(); |
| snd_usb_audio_vendor_helper_init(); |
| |
| INIT_WORK(&usb_recovery_ws, usb_recovery_work); |
| INIT_WORK(&usb_host_mode_checking_ws, usb_host_mode_checking_work); |
| |
| return aoc_driver_register(&aoc_usb_driver); |
| } |
| |
| static void __exit aoc_usb_exit(void) |
| { |
| aoc_driver_unregister(&aoc_usb_driver); |
| } |
| |
| module_init(aoc_usb_init); |
| module_exit(aoc_usb_exit); |
| |
| MODULE_LICENSE("GPL v2"); |
| MODULE_AUTHOR("Howard Yen (Google)"); |
| MODULE_DESCRIPTION("USB driver for AoC"); |