blob: 6712ca2e62390f9929774ff38b38b097611f1c8e [file] [log] [blame]
/* Copyright (c) 2014-2018, The Linux Foundation. All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 and
* only version 2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include <linux/kernel.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/termios.h>
#include <linux/netdevice.h>
#include <linux/debugfs.h>
#include <linux/bitops.h>
#include <linux/termios.h>
#include <linux/usb_bam.h>
#include "u_data_ipa.h"
struct ipa_data_ch_info {
struct usb_request *rx_req;
struct usb_request *tx_req;
unsigned long flags;
unsigned int id;
enum ipa_func_type func_type;
bool is_connected;
unsigned int port_num;
spinlock_t port_lock;
struct work_struct connect_w;
struct work_struct disconnect_w;
struct work_struct suspend_w;
struct work_struct resume_w;
u32 src_pipe_idx;
u32 dst_pipe_idx;
u8 src_connection_idx;
u8 dst_connection_idx;
enum usb_ctrl usb_bam_type;
struct data_port *port_usb;
struct usb_gadget *gadget;
atomic_t pipe_connect_notified;
struct usb_bam_connect_ipa_params ipa_params;
};
struct rndis_data_ch_info {
/* this provides downlink (device->host i.e host) side configuration*/
u32 dl_max_transfer_size;
/* this provides uplink (host->device i.e device) side configuration */
u32 ul_max_transfer_size;
u32 ul_max_packets_number;
bool ul_aggregation_enable;
u32 prod_clnt_hdl;
u32 cons_clnt_hdl;
void *priv;
};
static struct workqueue_struct *ipa_data_wq;
struct ipa_data_ch_info *ipa_data_ports[IPA_N_PORTS];
static struct rndis_data_ch_info *rndis_data;
/**
* ipa_data_endless_complete() - completion callback for endless TX/RX request
* @ep: USB endpoint for which this completion happen
* @req: USB endless request
*
* This completion is being called when endless (TX/RX) transfer is terminated
* i.e. disconnect or suspend case.
*/
static void ipa_data_endless_complete(struct usb_ep *ep,
struct usb_request *req)
{
pr_debug("%s: endless complete for(%s) with status: %d\n",
__func__, ep->name, req->status);
}
/**
* ipa_data_start_endless_xfer() - configure USB endpoint and
* queue endless TX/RX request
* @port: USB IPA data channel information
* @in: USB endpoint direction i.e. true: IN(Device TX), false: OUT(Device RX)
*
* It is being used to queue endless TX/RX request with UDC driver.
* It does set required DBM endpoint configuration before queueing endless
* TX/RX request.
*/
static void ipa_data_start_endless_xfer(struct ipa_data_ch_info *port, bool in)
{
unsigned long flags;
int status;
struct usb_ep *ep;
spin_lock_irqsave(&port->port_lock, flags);
if (!port->port_usb || (in && !port->tx_req)
|| (!in && !port->rx_req)) {
spin_unlock_irqrestore(&port->port_lock, flags);
pr_err("%s(): port_usb/req is NULL.\n", __func__);
return;
}
if (in)
ep = port->port_usb->in;
else
ep = port->port_usb->out;
spin_unlock_irqrestore(&port->port_lock, flags);
if (in) {
pr_debug("%s: enqueue endless TX_REQ(IN)\n", __func__);
status = usb_ep_queue(ep, port->tx_req, GFP_ATOMIC);
if (status)
pr_err("error enqueuing endless TX_REQ, %d\n", status);
} else {
pr_debug("%s: enqueue endless RX_REQ(OUT)\n", __func__);
status = usb_ep_queue(ep, port->rx_req, GFP_ATOMIC);
if (status)
pr_err("error enqueuing endless RX_REQ, %d\n", status);
}
}
/**
* ipa_data_stop_endless_xfer() - terminate and dequeue endless TX/RX request
* @port: USB IPA data channel information
* @in: USB endpoint direction i.e. IN - Device TX, OUT - Device RX
*
* It is being used to terminate and dequeue endless TX/RX request with UDC
* driver.
*/
static void ipa_data_stop_endless_xfer(struct ipa_data_ch_info *port, bool in)
{
unsigned long flags;
int status;
struct usb_ep *ep;
spin_lock_irqsave(&port->port_lock, flags);
if (!port->port_usb || (in && !port->tx_req)
|| (!in && !port->rx_req)) {
spin_unlock_irqrestore(&port->port_lock, flags);
pr_err("%s(): port_usb/req is NULL.\n", __func__);
return;
}
if (in)
ep = port->port_usb->in;
else
ep = port->port_usb->out;
spin_unlock_irqrestore(&port->port_lock, flags);
if (in) {
pr_debug("%s: dequeue endless TX_REQ(IN)\n", __func__);
status = usb_ep_dequeue(ep, port->tx_req);
if (status)
pr_err("error dequeueing endless TX_REQ, %d\n", status);
} else {
pr_debug("%s: dequeue endless RX_REQ(OUT)\n", __func__);
status = usb_ep_dequeue(ep, port->rx_req);
if (status)
pr_err("error dequeueing endless RX_REQ, %d\n", status);
}
}
/*
* Called when IPA triggers us that the network interface is up.
* Starts the transfers on bulk endpoints.
* (optimization reasons, the pipes and bam with IPA are already connected)
*/
void ipa_data_start_rx_tx(enum ipa_func_type func)
{
struct ipa_data_ch_info *port;
unsigned long flags;
struct usb_ep *epin, *epout;
pr_debug("%s: Triggered: starting tx, rx", __func__);
/* queue in & out requests */
port = ipa_data_ports[func];
if (!port) {
pr_err("%s: port is NULL, can't start tx, rx", __func__);
return;
}
spin_lock_irqsave(&port->port_lock, flags);
if (!port->port_usb || !port->port_usb->in ||
!port->port_usb->out) {
pr_err("%s: Can't start tx, rx, ep not enabled", __func__);
spin_unlock_irqrestore(&port->port_lock, flags);
return;
}
if (!port->rx_req || !port->tx_req) {
pr_err("%s: No request d->rx_req=%pK, d->tx_req=%pK", __func__,
port->rx_req, port->tx_req);
spin_unlock_irqrestore(&port->port_lock, flags);
return;
}
if (!port->is_connected) {
pr_debug("%s: pipes are disconnected", __func__);
spin_unlock_irqrestore(&port->port_lock, flags);
return;
}
epout = port->port_usb->out;
epin = port->port_usb->in;
spin_unlock_irqrestore(&port->port_lock, flags);
/* queue in & out requests */
pr_debug("%s: Starting rx", __func__);
if (epout)
ipa_data_start_endless_xfer(port, false);
pr_debug("%s: Starting tx", __func__);
if (epin)
ipa_data_start_endless_xfer(port, true);
}
/**
* ipa_data_disconnect_work() - Perform USB IPA BAM disconnect
* @w: disconnect work
*
* It is being schedule from ipa_data_disconnect() API when particular function
* is being disable due to USB disconnect or USB composition switch is being
* trigger . This API performs disconnect of USB BAM pipe, IPA BAM pipe and also
* initiate USB IPA BAM pipe handshake for USB Disconnect sequence. Due to
* handshake operation and involvement of SPS related APIs, this functioality
* can't be used from atomic context.
*/
static void ipa_data_disconnect_work(struct work_struct *w)
{
struct ipa_data_ch_info *port = container_of(w, struct ipa_data_ch_info,
disconnect_w);
unsigned long flags;
int ret;
spin_lock_irqsave(&port->port_lock, flags);
if (!port->is_connected) {
spin_unlock_irqrestore(&port->port_lock, flags);
pr_debug("Already disconnected.\n");
return;
}
port->is_connected = false;
pr_debug("%s(): prod_clnt_hdl:%d cons_clnt_hdl:%d\n", __func__,
port->ipa_params.prod_clnt_hdl,
port->ipa_params.cons_clnt_hdl);
spin_unlock_irqrestore(&port->port_lock, flags);
ret = usb_bam_disconnect_ipa(port->usb_bam_type, &port->ipa_params);
if (ret)
pr_err("usb_bam_disconnect_ipa failed: err:%d\n", ret);
if (port->func_type == USB_IPA_FUNC_RNDIS) {
/*
* NOTE: it is required to disconnect USB and IPA BAM related
* pipes before calling IPA tethered function related disconnect
* API. IPA tethered function related disconnect API delete
* depedency graph with IPA RM which would results into IPA not
* pulling data although there is pending data on USB BAM
* producer pipe.
*/
if (atomic_xchg(&port->pipe_connect_notified, 0) == 1) {
void *priv;
priv = rndis_qc_get_ipa_priv();
rndis_ipa_pipe_disconnect_notify(priv);
}
}
if (port->ipa_params.prod_clnt_hdl)
usb_bam_free_fifos(port->usb_bam_type,
port->dst_connection_idx);
if (port->ipa_params.cons_clnt_hdl)
usb_bam_free_fifos(port->usb_bam_type,
port->src_connection_idx);
if (port->func_type == USB_IPA_FUNC_RMNET)
teth_bridge_disconnect(port->ipa_params.src_client);
/*
* Decrement usage count which was incremented
* upon cable connect or cable disconnect in suspended state.
*/
usb_gadget_autopm_put_async(port->gadget);
pr_debug("%s(): disconnect work completed.\n", __func__);
}
/**
* ipa_data_disconnect() - Restore USB ep operation and disable USB endpoint
* @gp: USB gadget IPA Port
* @port_num: Port num used by function driver which need to be disable
*
* It is being called from atomic context from gadget driver when particular
* function is being disable due to USB cable disconnect or USB composition
* switch is being trigger. This API performs restoring USB endpoint operation
* and disable USB endpoint used for accelerated path.
*/
void ipa_data_disconnect(struct data_port *gp, enum ipa_func_type func)
{
struct ipa_data_ch_info *port;
unsigned long flags;
struct usb_gadget *gadget = NULL;
pr_debug("dev:%pK port number:%d\n", gp, func);
if (func >= USB_IPA_NUM_FUNCS) {
pr_err("invalid ipa portno#%d\n", func);
return;
}
if (!gp) {
pr_err("data port is null\n");
return;
}
port = ipa_data_ports[func];
if (!port) {
pr_err("port %u is NULL", func);
return;
}
spin_lock_irqsave(&port->port_lock, flags);
if (port->port_usb) {
gadget = port->port_usb->cdev->gadget;
port->port_usb->ipa_consumer_ep = -1;
port->port_usb->ipa_producer_ep = -1;
if (port->port_usb->in) {
/*
* Disable endpoints.
* Unlocking is needed since disabling the eps might
* stop active transfers and therefore the request
* complete function will be called, where we try
* to obtain the spinlock as well.
*/
msm_ep_unconfig(port->port_usb->in);
spin_unlock_irqrestore(&port->port_lock, flags);
usb_ep_disable(port->port_usb->in);
spin_lock_irqsave(&port->port_lock, flags);
if (port->tx_req) {
usb_ep_free_request(port->port_usb->in,
port->tx_req);
port->tx_req = NULL;
}
port->port_usb->in->endless = false;
}
if (port->port_usb->out) {
msm_ep_unconfig(port->port_usb->out);
spin_unlock_irqrestore(&port->port_lock, flags);
usb_ep_disable(port->port_usb->out);
spin_lock_irqsave(&port->port_lock, flags);
if (port->rx_req) {
usb_ep_free_request(port->port_usb->out,
port->rx_req);
port->rx_req = NULL;
}
port->port_usb->out->endless = false;
}
port->port_usb = NULL;
}
spin_unlock_irqrestore(&port->port_lock, flags);
queue_work(ipa_data_wq, &port->disconnect_w);
}
/**
* configure_fifo() - Configure USB BAM Pipe's data FIFO
* @idx: USB BAM Pipe index
* @ep: USB endpoint
*
* This function configures USB BAM data fifo using fetched pipe configuraion
* using provided index value. This function needs to used before starting
* endless transfer.
*/
static void configure_fifo(enum usb_ctrl bam_type, u8 idx, struct usb_ep *ep)
{
struct sps_mem_buffer data_fifo = {0};
u32 usb_bam_pipe_idx;
get_bam2bam_connection_info(bam_type, idx,
&usb_bam_pipe_idx,
NULL, &data_fifo, NULL);
msm_data_fifo_config(ep, data_fifo.phys_base, data_fifo.size,
usb_bam_pipe_idx);
}
/**
* ipa_data_connect_work() - Perform USB IPA BAM connect
* @w: connect work
*
* It is being schedule from ipa_data_connect() API when particular function
* which is using USB IPA accelerated path. This API performs allocating request
* for USB endpoint (tx/rx) for endless purpose, configure USB endpoint to be
* used in accelerated path, connect of USB BAM pipe, IPA BAM pipe and also
* initiate USB IPA BAM pipe handshake for connect sequence.
*/
static void ipa_data_connect_work(struct work_struct *w)
{
struct ipa_data_ch_info *port = container_of(w, struct ipa_data_ch_info,
connect_w);
struct data_port *gport;
struct usb_gadget *gadget = NULL;
struct teth_bridge_connect_params connect_params;
struct teth_bridge_init_params teth_bridge_params;
u32 sps_params;
int ret;
unsigned long flags;
bool is_ipa_disconnected = true;
pr_debug("%s: Connect workqueue started\n", __func__);
spin_lock_irqsave(&port->port_lock, flags);
if (!port->port_usb) {
spin_unlock_irqrestore(&port->port_lock, flags);
usb_gadget_autopm_put_async(port->gadget);
pr_err("%s(): port_usb is NULL.\n", __func__);
return;
}
gport = port->port_usb;
if (gport && gport->cdev)
gadget = gport->cdev->gadget;
if (!gadget) {
spin_unlock_irqrestore(&port->port_lock, flags);
usb_gadget_autopm_put_async(port->gadget);
pr_err("%s: gport is NULL.\n", __func__);
return;
}
/*
* check if connect_w got called two times during RNDIS resume as
* explicit flow control is called to start data transfers after
* ipa_data_connect()
*/
if (port->is_connected) {
pr_debug("IPA connect is already done & Transfers started\n");
spin_unlock_irqrestore(&port->port_lock, flags);
usb_gadget_autopm_put_async(port->gadget);
return;
}
gport->ipa_consumer_ep = -1;
gport->ipa_producer_ep = -1;
port->is_connected = true;
/* update IPA Parameteres here. */
port->ipa_params.usb_connection_speed = gadget->speed;
if (!gadget->is_chipidea)
port->ipa_params.reset_pipe_after_lpm =
msm_dwc3_reset_ep_after_lpm(gadget);
port->ipa_params.skip_ep_cfg = true;
port->ipa_params.keep_ipa_awake = true;
port->ipa_params.cons_clnt_hdl = -1;
port->ipa_params.prod_clnt_hdl = -1;
if (gport->out) {
spin_unlock_irqrestore(&port->port_lock, flags);
usb_bam_alloc_fifos(port->usb_bam_type,
port->src_connection_idx);
spin_lock_irqsave(&port->port_lock, flags);
if (!port->port_usb || port->rx_req == NULL) {
spin_unlock_irqrestore(&port->port_lock, flags);
pr_err("%s: port_usb is NULL, or rx_req cleaned\n",
__func__);
goto out;
}
if (!gadget->is_chipidea) {
sps_params = MSM_SPS_MODE | MSM_DISABLE_WB
| MSM_PRODUCER | port->src_pipe_idx;
port->rx_req->length = 32*1024;
port->rx_req->udc_priv = sps_params;
configure_fifo(port->usb_bam_type,
port->src_connection_idx,
port->port_usb->out);
ret = msm_ep_config(gport->out, port->rx_req);
if (ret) {
pr_err("msm_ep_config() failed for OUT EP\n");
spin_unlock_irqrestore(&port->port_lock, flags);
goto out;
}
} else {
/* gadget->is_chipidea */
get_bam2bam_connection_info(port->usb_bam_type,
port->src_connection_idx,
&port->src_pipe_idx,
NULL, NULL, NULL);
sps_params = (MSM_SPS_MODE | port->src_pipe_idx |
MSM_VENDOR_ID) & ~MSM_IS_FINITE_TRANSFER;
port->rx_req->udc_priv = sps_params;
}
}
if (gport->in) {
spin_unlock_irqrestore(&port->port_lock, flags);
usb_bam_alloc_fifos(port->usb_bam_type,
port->dst_connection_idx);
spin_lock_irqsave(&port->port_lock, flags);
if (!port->port_usb || port->tx_req == NULL) {
spin_unlock_irqrestore(&port->port_lock, flags);
pr_err("%s: port_usb is NULL, or tx_req cleaned\n",
__func__);
goto unconfig_msm_ep_out;
}
if (!gadget->is_chipidea) {
sps_params = MSM_SPS_MODE | MSM_DISABLE_WB |
port->dst_pipe_idx;
port->tx_req->length = 32*1024;
port->tx_req->udc_priv = sps_params;
configure_fifo(port->usb_bam_type,
port->dst_connection_idx, gport->in);
ret = msm_ep_config(gport->in, port->tx_req);
if (ret) {
pr_err("msm_ep_config() failed for IN EP\n");
spin_unlock_irqrestore(&port->port_lock, flags);
goto unconfig_msm_ep_out;
}
} else {
/* gadget->is_chipidea */
get_bam2bam_connection_info(port->usb_bam_type,
port->dst_connection_idx,
&port->dst_pipe_idx,
NULL, NULL, NULL);
sps_params = (MSM_SPS_MODE | port->dst_pipe_idx |
MSM_VENDOR_ID) & ~MSM_IS_FINITE_TRANSFER;
port->tx_req->udc_priv = sps_params;
}
}
if (port->func_type == USB_IPA_FUNC_RMNET) {
teth_bridge_params.client = port->ipa_params.src_client;
ret = teth_bridge_init(&teth_bridge_params);
if (ret) {
pr_err("%s:teth_bridge_init() failed\n", __func__);
spin_unlock_irqrestore(&port->port_lock, flags);
goto unconfig_msm_ep_in;
}
}
/*
* Perform below operations for Tx from Device (OUT transfer)
* 1. Connect with pipe of USB BAM with IPA BAM pipe
* 2. Update USB Endpoint related information using SPS Param.
* 3. Configure USB Endpoint/DBM for the same.
* 4. Override USB ep queue functionality for endless transfer.
*/
if (gport->out) {
pr_debug("configure bam ipa connect for USB OUT\n");
port->ipa_params.dir = USB_TO_PEER_PERIPHERAL;
if (port->func_type == USB_IPA_FUNC_RNDIS) {
port->ipa_params.notify = rndis_qc_get_ipa_rx_cb();
port->ipa_params.priv = rndis_qc_get_ipa_priv();
port->ipa_params.skip_ep_cfg =
rndis_qc_get_skip_ep_config();
} else if (port->func_type == USB_IPA_FUNC_RMNET) {
port->ipa_params.notify =
teth_bridge_params.usb_notify_cb;
port->ipa_params.priv =
teth_bridge_params.private_data;
port->ipa_params.reset_pipe_after_lpm =
msm_dwc3_reset_ep_after_lpm(gadget);
port->ipa_params.ipa_ep_cfg.mode.mode = IPA_BASIC;
port->ipa_params.skip_ep_cfg =
teth_bridge_params.skip_ep_cfg;
}
spin_unlock_irqrestore(&port->port_lock, flags);
ret = usb_bam_connect_ipa(port->usb_bam_type,
&port->ipa_params);
if (ret) {
pr_err("usb_bam_connect_ipa out failed err:%d\n", ret);
goto disconnect_usb_bam_ipa_out;
}
spin_lock_irqsave(&port->port_lock, flags);
is_ipa_disconnected = false;
/* check if USB cable is disconnected or not */
if (!port->port_usb) {
pr_debug("%s:%d: cable is disconnected.\n",
__func__, __LINE__);
spin_unlock_irqrestore(&port->port_lock, flags);
goto disconnect_usb_bam_ipa_out;
}
gport->ipa_consumer_ep = port->ipa_params.ipa_cons_ep_idx;
}
if (gport->in) {
pr_debug("configure bam ipa connect for USB IN\n");
port->ipa_params.dir = PEER_PERIPHERAL_TO_USB;
if (port->func_type == USB_IPA_FUNC_RNDIS) {
port->ipa_params.notify = rndis_qc_get_ipa_tx_cb();
port->ipa_params.priv = rndis_qc_get_ipa_priv();
port->ipa_params.skip_ep_cfg =
rndis_qc_get_skip_ep_config();
} else if (port->func_type == USB_IPA_FUNC_RMNET) {
port->ipa_params.notify =
teth_bridge_params.usb_notify_cb;
port->ipa_params.priv =
teth_bridge_params.private_data;
port->ipa_params.reset_pipe_after_lpm =
msm_dwc3_reset_ep_after_lpm(gadget);
port->ipa_params.ipa_ep_cfg.mode.mode = IPA_BASIC;
port->ipa_params.skip_ep_cfg =
teth_bridge_params.skip_ep_cfg;
}
if (port->func_type == USB_IPA_FUNC_DPL)
port->ipa_params.dst_client = IPA_CLIENT_USB_DPL_CONS;
spin_unlock_irqrestore(&port->port_lock, flags);
ret = usb_bam_connect_ipa(port->usb_bam_type,
&port->ipa_params);
if (ret) {
pr_err("usb_bam_connect_ipa IN failed err:%d\n", ret);
goto disconnect_usb_bam_ipa_out;
}
spin_lock_irqsave(&port->port_lock, flags);
is_ipa_disconnected = false;
/* check if USB cable is disconnected or not */
if (!port->port_usb) {
pr_debug("%s:%d: cable is disconnected.\n",
__func__, __LINE__);
spin_unlock_irqrestore(&port->port_lock, flags);
goto disconnect_usb_bam_ipa_out;
}
gport->ipa_producer_ep = port->ipa_params.ipa_prod_ep_idx;
}
spin_unlock_irqrestore(&port->port_lock, flags);
if (port->func_type == USB_IPA_FUNC_RNDIS) {
rndis_data->prod_clnt_hdl =
port->ipa_params.prod_clnt_hdl;
rndis_data->cons_clnt_hdl =
port->ipa_params.cons_clnt_hdl;
rndis_data->priv = port->ipa_params.priv;
pr_debug("ul_max_transfer_size:%d\n",
rndis_data->ul_max_transfer_size);
pr_debug("ul_max_packets_number:%d\n",
rndis_data->ul_max_packets_number);
pr_debug("dl_max_transfer_size:%d\n",
rndis_data->dl_max_transfer_size);
ret = rndis_ipa_pipe_connect_notify(
rndis_data->cons_clnt_hdl,
rndis_data->prod_clnt_hdl,
rndis_data->ul_max_transfer_size,
rndis_data->ul_max_packets_number,
rndis_data->dl_max_transfer_size,
rndis_data->priv);
if (ret) {
pr_err("%s: failed to connect IPA: err:%d\n",
__func__, ret);
return;
}
atomic_set(&port->pipe_connect_notified, 1);
} else if (port->func_type == USB_IPA_FUNC_RMNET ||
port->func_type == USB_IPA_FUNC_DPL) {
/* For RmNet and DPL need to update_ipa_pipes to qti */
enum qti_port_type qti_port_type = port->func_type ==
USB_IPA_FUNC_RMNET ? QTI_PORT_RMNET : QTI_PORT_DPL;
gqti_ctrl_update_ipa_pipes(port->port_usb, qti_port_type,
gport->ipa_producer_ep, gport->ipa_consumer_ep);
}
if (port->func_type == USB_IPA_FUNC_RMNET) {
connect_params.ipa_usb_pipe_hdl =
port->ipa_params.prod_clnt_hdl;
connect_params.usb_ipa_pipe_hdl =
port->ipa_params.cons_clnt_hdl;
connect_params.tethering_mode =
TETH_TETHERING_MODE_RMNET;
connect_params.client_type =
port->ipa_params.src_client;
ret = teth_bridge_connect(&connect_params);
if (ret) {
pr_err("%s:teth_bridge_connect() failed\n", __func__);
goto disconnect_usb_bam_ipa_out;
}
}
pr_debug("ipa_producer_ep:%d ipa_consumer_ep:%d\n",
gport->ipa_producer_ep,
gport->ipa_consumer_ep);
pr_debug("src_bam_idx:%d dst_bam_idx:%d\n",
port->src_connection_idx, port->dst_connection_idx);
/* Don't queue the transfers yet, only after network stack is up */
if (port->func_type == USB_IPA_FUNC_RNDIS) {
pr_debug("%s: Not starting now, waiting for network notify",
__func__);
return;
}
if (gport->out)
ipa_data_start_endless_xfer(port, false);
if (gport->in)
ipa_data_start_endless_xfer(port, true);
pr_debug("Connect workqueue done (port %pK)", port);
return;
disconnect_usb_bam_ipa_out:
if (!is_ipa_disconnected) {
usb_bam_disconnect_ipa(port->usb_bam_type, &port->ipa_params);
is_ipa_disconnected = true;
}
if (port->func_type == USB_IPA_FUNC_RMNET)
teth_bridge_disconnect(port->ipa_params.src_client);
unconfig_msm_ep_in:
spin_lock_irqsave(&port->port_lock, flags);
/* check if USB cable is disconnected or not */
if (port->port_usb && gport->in)
msm_ep_unconfig(port->port_usb->in);
spin_unlock_irqrestore(&port->port_lock, flags);
unconfig_msm_ep_out:
if (gport->in)
usb_bam_free_fifos(port->usb_bam_type,
port->dst_connection_idx);
spin_lock_irqsave(&port->port_lock, flags);
/* check if USB cable is disconnected or not */
if (port->port_usb && gport->out)
msm_ep_unconfig(port->port_usb->out);
spin_unlock_irqrestore(&port->port_lock, flags);
out:
if (gport->out)
usb_bam_free_fifos(port->usb_bam_type,
port->src_connection_idx);
spin_lock_irqsave(&port->port_lock, flags);
port->is_connected = false;
spin_unlock_irqrestore(&port->port_lock, flags);
usb_gadget_autopm_put_async(port->gadget);
}
/**
* ipa_data_connect() - Prepare IPA params and enable USB endpoints
* @gp: USB IPA gadget port
* @port_num: port number used by accelerated function
* @src_connection_idx: USB BAM pipe index used as producer
* @dst_connection_idx: USB BAM pipe index used as consumer
*
* It is being called from accelerated function driver (from set_alt()) to
* initiate USB BAM IPA connection. This API is enabling accelerated endpoints
* and schedule connect_work() which establishes USB IPA BAM communication.
*/
int ipa_data_connect(struct data_port *gp, enum ipa_func_type func,
u8 src_connection_idx, u8 dst_connection_idx)
{
struct ipa_data_ch_info *port;
unsigned long flags;
int ret = 0;
pr_debug("dev:%pK port#%d src_connection_idx:%d dst_connection_idx:%d\n",
gp, func, src_connection_idx, dst_connection_idx);
if (func >= USB_IPA_NUM_FUNCS) {
pr_err("invalid portno#%d\n", func);
ret = -ENODEV;
goto err;
}
if (!gp) {
pr_err("gadget port is null\n");
ret = -ENODEV;
goto err;
}
port = ipa_data_ports[func];
spin_lock_irqsave(&port->port_lock, flags);
port->port_usb = gp;
port->gadget = gp->cdev->gadget;
if (gp->out) {
port->rx_req = usb_ep_alloc_request(gp->out, GFP_ATOMIC);
if (!port->rx_req) {
spin_unlock_irqrestore(&port->port_lock, flags);
pr_err("%s: failed to allocate rx_req\n", __func__);
goto err;
}
port->rx_req->context = port;
port->rx_req->complete = ipa_data_endless_complete;
port->rx_req->length = 0;
port->rx_req->no_interrupt = 1;
}
if (gp->in) {
port->tx_req = usb_ep_alloc_request(gp->in, GFP_ATOMIC);
if (!port->tx_req) {
pr_err("%s: failed to allocate tx_req\n", __func__);
goto free_rx_req;
}
port->tx_req->context = port;
port->tx_req->complete = ipa_data_endless_complete;
port->tx_req->length = 0;
port->tx_req->no_interrupt = 1;
}
port->src_connection_idx = src_connection_idx;
port->dst_connection_idx = dst_connection_idx;
port->usb_bam_type = usb_bam_get_bam_type(gp->cdev->gadget->name);
port->ipa_params.src_pipe = &(port->src_pipe_idx);
port->ipa_params.dst_pipe = &(port->dst_pipe_idx);
port->ipa_params.src_idx = src_connection_idx;
port->ipa_params.dst_idx = dst_connection_idx;
/*
* Disable Xfer complete and Xfer not ready interrupts by
* marking endless flag which is used in UDC driver to enable
* these interrupts. with this set, these interrupts for selected
* endpoints won't be enabled.
*/
if (port->port_usb->in) {
port->port_usb->in->endless = true;
ret = usb_ep_enable(port->port_usb->in);
if (ret) {
pr_err("usb_ep_enable failed eptype:IN ep:%pK",
port->port_usb->in);
usb_ep_free_request(port->port_usb->in, port->tx_req);
port->tx_req = NULL;
port->port_usb->in->endless = false;
goto err_usb_in;
}
}
if (port->port_usb->out) {
port->port_usb->out->endless = true;
ret = usb_ep_enable(port->port_usb->out);
if (ret) {
pr_err("usb_ep_enable failed eptype:OUT ep:%pK",
port->port_usb->out);
usb_ep_free_request(port->port_usb->out, port->rx_req);
port->rx_req = NULL;
port->port_usb->out->endless = false;
goto err_usb_out;
}
}
/* Wait for host to enable flow_control */
if (port->func_type == USB_IPA_FUNC_RNDIS) {
spin_unlock_irqrestore(&port->port_lock, flags);
ret = 0;
return ret;
}
/*
* Increment usage count upon cable connect. Decrement after IPA
* handshake is done in disconnect work (due to cable disconnect)
* or in suspend work.
*/
usb_gadget_autopm_get_noresume(port->gadget);
queue_work(ipa_data_wq, &port->connect_w);
spin_unlock_irqrestore(&port->port_lock, flags);
return ret;
err_usb_out:
if (port->port_usb->in) {
usb_ep_disable(port->port_usb->in);
port->port_usb->in->endless = false;
}
err_usb_in:
if (gp->in && port->tx_req) {
usb_ep_free_request(gp->in, port->tx_req);
port->tx_req = NULL;
}
free_rx_req:
if (gp->out && port->rx_req) {
usb_ep_free_request(gp->out, port->rx_req);
port->rx_req = NULL;
}
spin_unlock_irqrestore(&port->port_lock, flags);
err:
pr_debug("%s(): failed with error:%d\n", __func__, ret);
return ret;
}
/**
* ipa_data_start() - Restart USB endless transfer
* @param: IPA data channel information
* @dir: USB BAM pipe direction
*
* It is being used to restart USB endless transfer for USB bus resume.
* For USB consumer case, it restarts USB endless RX transfer, whereas
* for USB producer case, it resets DBM endpoint and restart USB endless
* TX transfer.
*/
static void ipa_data_start(void *param, enum usb_bam_pipe_dir dir)
{
struct ipa_data_ch_info *port = param;
struct usb_gadget *gadget = NULL;
if (!port || !port->port_usb || !port->port_usb->cdev->gadget) {
pr_err("%s:port,cdev or gadget is NULL\n", __func__);
return;
}
gadget = port->port_usb->cdev->gadget;
if (dir == USB_TO_PEER_PERIPHERAL) {
pr_debug("%s(): start endless RX\n", __func__);
ipa_data_start_endless_xfer(port, false);
} else {
pr_debug("%s(): start endless TX\n", __func__);
if (msm_dwc3_reset_ep_after_lpm(gadget)) {
configure_fifo(port->usb_bam_type,
port->dst_connection_idx, port->port_usb->in);
}
ipa_data_start_endless_xfer(port, true);
}
}
/**
* ipa_data_stop() - Stop endless Tx/Rx transfers
* @param: IPA data channel information
* @dir: USB BAM pipe direction
*
* It is being used to stop endless Tx/Rx transfers. It is being used
* for USB bus suspend functionality.
*/
static void ipa_data_stop(void *param, enum usb_bam_pipe_dir dir)
{
struct ipa_data_ch_info *port = param;
struct usb_gadget *gadget = NULL;
if (!port || !port->port_usb || !port->port_usb->cdev->gadget) {
pr_err("%s:port,cdev or gadget is NULL\n", __func__);
return;
}
gadget = port->port_usb->cdev->gadget;
if (dir == USB_TO_PEER_PERIPHERAL) {
pr_debug("%s(): stop endless RX transfer\n", __func__);
ipa_data_stop_endless_xfer(port, false);
} else {
pr_debug("%s(): stop endless TX transfer\n", __func__);
ipa_data_stop_endless_xfer(port, true);
}
}
void ipa_data_flush_workqueue(void)
{
pr_debug("%s(): Flushing workqueue\n", __func__);
flush_workqueue(ipa_data_wq);
}
/**
* ipa_data_suspend() - Initiate USB BAM IPA suspend functionality
* @gp: Gadget IPA port
* @port_num: port number used by function
*
* It is being used to initiate USB BAM IPA suspend functionality
* for USB bus suspend functionality.
*/
void ipa_data_suspend(struct data_port *gp, enum ipa_func_type func,
bool remote_wakeup_enabled)
{
struct ipa_data_ch_info *port;
unsigned long flags;
if (func >= USB_IPA_NUM_FUNCS) {
pr_err("invalid ipa portno#%d\n", func);
return;
}
if (!gp) {
pr_err("data port is null\n");
return;
}
pr_debug("%s: suspended port %d\n", __func__, func);
port = ipa_data_ports[func];
if (!port) {
pr_err("%s(): Port is NULL.\n", __func__);
return;
}
/* suspend with remote wakeup disabled */
if (!remote_wakeup_enabled) {
/*
* When remote wakeup is disabled, IPA BAM is disconnected
* because it cannot send new data until the USB bus is resumed.
* Endpoint descriptors info is saved before it gets reset by
* the BAM disconnect API. This lets us restore this info when
* the USB bus is resumed.
*/
if (gp->in) {
gp->in_ep_desc_backup = gp->in->desc;
pr_debug("in_ep_desc_backup = %pK\n",
gp->in_ep_desc_backup);
}
if (gp->out) {
gp->out_ep_desc_backup = gp->out->desc;
pr_debug("out_ep_desc_backup = %pK\n",
gp->out_ep_desc_backup);
}
ipa_data_disconnect(gp, func);
return;
}
spin_lock_irqsave(&port->port_lock, flags);
queue_work(ipa_data_wq, &port->suspend_w);
spin_unlock_irqrestore(&port->port_lock, flags);
}
static void bam2bam_data_suspend_work(struct work_struct *w)
{
struct ipa_data_ch_info *port = container_of(w, struct ipa_data_ch_info,
connect_w);
unsigned long flags;
int ret;
pr_debug("%s: suspend started\n", __func__);
spin_lock_irqsave(&port->port_lock, flags);
/* In case of RNDIS, host enables flow_control invoking connect_w. If it
* is delayed then we may end up having suspend_w run before connect_w.
* In this scenario, connect_w may or may not at all start if cable gets
* disconnected or if host changes configuration e.g. RNDIS --> MBIM
* For these cases don't do runtime_put as there was no _get yet, and
* detect this condition on disconnect to not do extra pm_runtme_get
* for SUSPEND --> DISCONNECT scenario.
*/
if (!port->is_connected) {
pr_err("%s: Not yet connected. SUSPEND pending.\n", __func__);
spin_unlock_irqrestore(&port->port_lock, flags);
return;
}
ret = usb_bam_register_wake_cb(port->usb_bam_type,
port->dst_connection_idx, NULL, port);
if (ret) {
pr_err("%s(): Failed to register BAM wake callback.\n",
__func__);
return;
}
usb_bam_register_start_stop_cbs(port->usb_bam_type,
port->dst_connection_idx, ipa_data_start,
ipa_data_stop, port);
/*
* release lock here because bam_data_start() or
* bam_data_stop() called from usb_bam_suspend()
* re-acquires port lock.
*/
spin_unlock_irqrestore(&port->port_lock, flags);
usb_bam_suspend(port->usb_bam_type, &port->ipa_params);
spin_lock_irqsave(&port->port_lock, flags);
/*
* Decrement usage count after IPA handshake is done
* to allow gadget parent to go to lpm. This counter was
* incremented upon cable connect.
*/
usb_gadget_autopm_put_async(port->gadget);
spin_unlock_irqrestore(&port->port_lock, flags);
}
/**
* ipa_data_resume() - Initiate USB resume functionality
* @gp: Gadget IPA port
* @port_num: port number used by function
*
* It is being used to initiate USB resume functionality
* for USB bus resume case.
*/
void ipa_data_resume(struct data_port *gp, enum ipa_func_type func,
bool remote_wakeup_enabled)
{
struct ipa_data_ch_info *port;
unsigned long flags;
struct usb_gadget *gadget = NULL;
u8 src_connection_idx = 0;
u8 dst_connection_idx = 0;
enum usb_ctrl usb_bam_type;
pr_debug("dev:%pK port number:%d\n", gp, func);
if (func >= USB_IPA_NUM_FUNCS) {
pr_err("invalid ipa portno#%d\n", func);
return;
}
if (!gp) {
pr_err("data port is null\n");
return;
}
port = ipa_data_ports[func];
if (!port) {
pr_err("port %u is NULL", func);
return;
}
gadget = gp->cdev->gadget;
/* resume with remote wakeup disabled */
if (!remote_wakeup_enabled) {
int bam_pipe_num = (func == USB_IPA_FUNC_DPL) ? 1 : 0;
usb_bam_type = usb_bam_get_bam_type(gadget->name);
/* Restore endpoint descriptors info. */
if (gp->in) {
gp->in->desc = gp->in_ep_desc_backup;
pr_debug("in_ep_desc_backup = %pK\n",
gp->in_ep_desc_backup);
dst_connection_idx = usb_bam_get_connection_idx(
usb_bam_type, IPA_P_BAM, PEER_PERIPHERAL_TO_USB,
USB_BAM_DEVICE, bam_pipe_num);
}
if (gp->out) {
gp->out->desc = gp->out_ep_desc_backup;
pr_debug("out_ep_desc_backup = %pK\n",
gp->out_ep_desc_backup);
src_connection_idx = usb_bam_get_connection_idx(
usb_bam_type, IPA_P_BAM, USB_TO_PEER_PERIPHERAL,
USB_BAM_DEVICE, bam_pipe_num);
}
ipa_data_connect(gp, func,
src_connection_idx, dst_connection_idx);
return;
}
spin_lock_irqsave(&port->port_lock, flags);
/*
* Increment usage count here to disallow gadget
* parent suspend. This counter will decrement
* after IPA handshake is done in disconnect work
* (due to cable disconnect) or in bam_data_disconnect
* in suspended state.
*/
usb_gadget_autopm_get_noresume(port->gadget);
queue_work(ipa_data_wq, &port->resume_w);
spin_unlock_irqrestore(&port->port_lock, flags);
}
static void bam2bam_data_resume_work(struct work_struct *w)
{
struct ipa_data_ch_info *port = container_of(w, struct ipa_data_ch_info,
connect_w);
struct usb_gadget *gadget;
unsigned long flags;
int ret;
spin_lock_irqsave(&port->port_lock, flags);
if (!port->port_usb || !port->port_usb->cdev) {
pr_err("port->port_usb or cdev is NULL");
goto exit;
}
if (!port->port_usb->cdev->gadget) {
pr_err("port->port_usb->cdev->gadget is NULL");
goto exit;
}
pr_debug("%s: resume started\n", __func__);
gadget = port->port_usb->cdev->gadget;
if (!gadget) {
spin_unlock_irqrestore(&port->port_lock, flags);
pr_err("%s(): Gadget is NULL.\n", __func__);
return;
}
ret = usb_bam_register_wake_cb(port->usb_bam_type,
port->dst_connection_idx, NULL, NULL);
if (ret) {
spin_unlock_irqrestore(&port->port_lock, flags);
pr_err("%s(): Failed to register BAM wake callback.\n",
__func__);
return;
}
if (msm_dwc3_reset_ep_after_lpm(gadget)) {
configure_fifo(port->usb_bam_type, port->src_connection_idx,
port->port_usb->out);
configure_fifo(port->usb_bam_type, port->dst_connection_idx,
port->port_usb->in);
spin_unlock_irqrestore(&port->port_lock, flags);
msm_dwc3_reset_dbm_ep(port->port_usb->in);
spin_lock_irqsave(&port->port_lock, flags);
}
usb_bam_resume(port->usb_bam_type, &port->ipa_params);
exit:
spin_unlock_irqrestore(&port->port_lock, flags);
}
/**
* ipa_data_port_alloc() - Allocate IPA USB Port structure
* @portno: port number to be used by particular USB function
*
* It is being used by USB function driver to allocate IPA data port
* for USB IPA data accelerated path.
*
* Retrun: 0 in case of success, otherwise errno.
*/
static int ipa_data_port_alloc(enum ipa_func_type func)
{
struct ipa_data_ch_info *port = NULL;
if (ipa_data_ports[func] != NULL) {
pr_debug("port %d already allocated.\n", func);
return 0;
}
port = kzalloc(sizeof(struct ipa_data_ch_info), GFP_KERNEL);
if (!port)
return -ENOMEM;
ipa_data_ports[func] = port;
pr_debug("port:%pK with portno:%d allocated\n", port, func);
return 0;
}
/**
* ipa_data_port_select() - Select particular port for BAM2BAM IPA mode
* @portno: port number to be used by particular USB function
* @func_type: USB gadget function type
*
* It is being used by USB function driver to select which BAM2BAM IPA
* port particular USB function wants to use.
*
*/
void ipa_data_port_select(enum ipa_func_type func)
{
struct ipa_data_ch_info *port = NULL;
pr_debug("portno:%d\n", func);
port = ipa_data_ports[func];
port->port_num = func;
port->is_connected = false;
spin_lock_init(&port->port_lock);
if (!work_pending(&port->connect_w))
INIT_WORK(&port->connect_w, ipa_data_connect_work);
if (!work_pending(&port->disconnect_w))
INIT_WORK(&port->disconnect_w, ipa_data_disconnect_work);
INIT_WORK(&port->suspend_w, bam2bam_data_suspend_work);
INIT_WORK(&port->resume_w, bam2bam_data_resume_work);
port->ipa_params.src_client = IPA_CLIENT_USB_PROD;
port->ipa_params.dst_client = IPA_CLIENT_USB_CONS;
port->func_type = func;
};
void ipa_data_free(enum ipa_func_type func)
{
pr_debug("freeing %d IPA BAM port", func);
kfree(ipa_data_ports[func]);
ipa_data_ports[func] = NULL;
if (func == USB_IPA_FUNC_RNDIS)
kfree(rndis_data);
if (ipa_data_wq) {
destroy_workqueue(ipa_data_wq);
ipa_data_wq = NULL;
}
}
/**
* ipa_data_setup() - setup BAM2BAM IPA port
*
* Each USB function who wants to use BAM2BAM IPA port would
* be counting number of IPA port to use and initialize those
* ports at time of bind_config() in android gadget driver.
*
* Retrun: 0 in case of success, otherwise errno.
*/
int ipa_data_setup(enum ipa_func_type func)
{
int ret;
pr_debug("requested %d IPA BAM port", func);
if (func >= USB_IPA_NUM_FUNCS) {
pr_err("Invalid num of ports count:%d\n", func);
return -EINVAL;
}
ret = ipa_data_port_alloc(func);
if (ret) {
pr_err("Failed to alloc port:%d\n", func);
return ret;
}
if (func == USB_IPA_FUNC_RNDIS) {
rndis_data = kzalloc(sizeof(*rndis_data), GFP_KERNEL);
if (!rndis_data) {
pr_err("%s: fail allocate and initialize new instance\n",
__func__);
goto free_ipa_ports;
}
}
if (ipa_data_wq) {
pr_debug("ipa_data_wq is already setup.");
return 0;
}
ipa_data_wq = alloc_workqueue("k_usb_ipa_data",
WQ_UNBOUND | WQ_MEM_RECLAIM, 1);
if (!ipa_data_wq) {
pr_err("Failed to create workqueue\n");
ret = -ENOMEM;
goto free_rndis_data;
}
return 0;
free_rndis_data:
if (func == USB_IPA_FUNC_RNDIS)
kfree(rndis_data);
free_ipa_ports:
kfree(ipa_data_ports[func]);
ipa_data_ports[func] = NULL;
return ret;
}
void ipa_data_set_ul_max_xfer_size(u32 max_transfer_size)
{
if (!max_transfer_size) {
pr_err("%s: invalid parameters\n", __func__);
return;
}
rndis_data->ul_max_transfer_size = max_transfer_size;
pr_debug("%s(): ul_max_xfer_size:%d\n", __func__, max_transfer_size);
}
void ipa_data_set_dl_max_xfer_size(u32 max_transfer_size)
{
if (!max_transfer_size) {
pr_err("%s: invalid parameters\n", __func__);
return;
}
rndis_data->dl_max_transfer_size = max_transfer_size;
pr_debug("%s(): dl_max_xfer_size:%d\n", __func__, max_transfer_size);
}
void ipa_data_set_ul_max_pkt_num(u8 max_packets_number)
{
if (!max_packets_number) {
pr_err("%s: invalid parameters\n", __func__);
return;
}
rndis_data->ul_max_packets_number = max_packets_number;
if (max_packets_number > 1)
rndis_data->ul_aggregation_enable = true;
else
rndis_data->ul_aggregation_enable = false;
pr_debug("%s(): ul_aggregation enable:%d ul_max_packets_number:%d\n",
__func__, rndis_data->ul_aggregation_enable,
max_packets_number);
}
void ipa_data_start_rndis_ipa(enum ipa_func_type func)
{
struct ipa_data_ch_info *port;
pr_debug("%s\n", __func__);
port = ipa_data_ports[func];
if (!port) {
pr_err("%s: port is NULL", __func__);
return;
}
if (atomic_read(&port->pipe_connect_notified)) {
pr_debug("%s: Transfers already started?\n", __func__);
return;
}
/*
* Increment usage count upon cable connect. Decrement after IPA
* handshake is done in disconnect work due to cable disconnect
* or in suspend work.
*/
usb_gadget_autopm_get_noresume(port->gadget);
queue_work(ipa_data_wq, &port->connect_w);
}
void ipa_data_stop_rndis_ipa(enum ipa_func_type func)
{
struct ipa_data_ch_info *port;
unsigned long flags;
pr_debug("%s\n", __func__);
port = ipa_data_ports[func];
if (!port) {
pr_err("%s: port is NULL", __func__);
return;
}
if (!atomic_read(&port->pipe_connect_notified))
return;
rndis_ipa_reset_trigger();
ipa_data_stop_endless_xfer(port, true);
ipa_data_stop_endless_xfer(port, false);
spin_lock_irqsave(&port->port_lock, flags);
/* check if USB cable is disconnected or not */
if (port->port_usb) {
msm_ep_unconfig(port->port_usb->in);
msm_ep_unconfig(port->port_usb->out);
}
spin_unlock_irqrestore(&port->port_lock, flags);
queue_work(ipa_data_wq, &port->disconnect_w);
}