blob: 3feec5cb4d9769a2434f430b66aa9c478343d66f [file] [edit]
// 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 BLOCKING_NOTIFIER_HEAD(aoc_usb_notifier_list);
int register_aoc_usb_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_register(&aoc_usb_notifier_list, nb);
}
int unregister_aoc_usb_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_unregister(&aoc_usb_notifier_list, nb);
}
int notify_offload_state(bool enabled)
{
blocking_notifier_call_chain(&aoc_usb_notifier_list, SET_OFFLOAD_STATE, &enabled);
return 0;
}
int xhci_set_dcbaa_ptr(u64 aoc_dcbaa_ptr)
{
blocking_notifier_call_chain(&aoc_usb_notifier_list, SET_DCBAA_PTR, &aoc_dcbaa_ptr);
return 0;
}
int xhci_setup_done(void)
{
blocking_notifier_call_chain(&aoc_usb_notifier_list, SETUP_DONE, NULL);
return 0;
}
int xhci_sync_conn_stat(unsigned int bus_id, unsigned int dev_num, unsigned int slot_id,
unsigned int conn_stat)
{
struct conn_stat_args args;
args.bus_id = bus_id;
args.dev_num = dev_num;
args.slot_id = slot_id;
args.conn_stat = conn_stat;
blocking_notifier_call_chain(&aoc_usb_notifier_list, SYNC_CONN_STAT, &args);
return 0;
}
int usb_host_mode_state_notify(enum aoc_usb_state usb_state)
{
return xhci_sync_conn_stat(0, 0, 0, usb_state);
}
int xhci_set_isoc_tr_info(u16 ep_id, u16 dir, struct xhci_ring *ep_ring)
{
struct get_isoc_tr_info_args tr_info;
tr_info.ep_id = ep_id;
tr_info.dir = dir;
tr_info.num_segs = ep_ring->num_segs;
tr_info.max_packet = ep_ring->bounce_buf_len;
tr_info.type = ep_ring->type;
tr_info.seg_ptr = ep_ring->first_seg->dma;
tr_info.cycle_state = ep_ring->cycle_state;
tr_info.num_trbs_free = ep_ring->num_trbs_free;
blocking_notifier_call_chain(&aoc_usb_notifier_list, SET_ISOC_TR_INFO, &tr_info);
return 0;
}
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_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;
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_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_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;
switch (code) {
case SET_DCBAA_PTR:
ret = aoc_usb_set_dcbaa_ptr(drvdata, data);
break;
case SETUP_DONE:
ret = aoc_usb_setup_done(drvdata);
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;
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 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(void)
{
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;
}
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) {
dev_err(&drvdata->adev->dev, "wakeup_source_register failed!\n");
return -ENOMEM;
}
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);
schedule_work(&usb_host_mode_checking_ws);
/* USB host mode needs support from AoC. */
dwc3_otg_host_ready(true);
return 0;
}
static int aoc_usb_remove(struct aoc_service_dev *adev)
{
struct aoc_usb_drvdata *drvdata = dev_get_drvdata(&adev->dev);
/* USB host mode needs support from AoC. */
dwc3_otg_host_ready(false);
unregister_aoc_usb_notifier(&drvdata->nb);
wakeup_source_unregister(drvdata->ws);
mutex_destroy(&drvdata->lock);
kfree(drvdata);
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_offload_helper_init();
usb_vendor_helper_init();
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");