Merge "drivers: qcom: Enable IPC Router communication over MHI"
This commit is contained in:
commit
08009065c9
|
@ -0,0 +1,21 @@
|
|||
Qualcomm Technologies, Inc. IPC Router MHI Transport
|
||||
|
||||
Required properties:
|
||||
-compatible: should be "qcom,ipc_router_mhi_xprt"
|
||||
-qcom,out-chan-id: MHI Channel ID for the transmit path
|
||||
-qcom,in-chan-id: MHI Channel ID for the receive path
|
||||
-qcom,xprt-remote: string that defines the edge of the transport (PIL Name)
|
||||
-qcom,xprt-linkid: unique integer to identify the tier to which the link
|
||||
belongs to in the network and is used to avoid the
|
||||
routing loops while forwarding the broadcast messages
|
||||
-qcom,xprt-version: unique version ID used by MHI transport header
|
||||
|
||||
Example:
|
||||
qcom,ipc_router_external_modem_xprt2 {
|
||||
compatible = "qcom,ipc_router_mhi_xprt";
|
||||
qcom,out-chan-id = <34>;
|
||||
qcom,in-chan-id = <35>;
|
||||
qcom,xprt-remote = "external-modem";
|
||||
qcom,xprt-linkid = <1>;
|
||||
qcom,xprt-version = <3>;
|
||||
};
|
|
@ -108,6 +108,16 @@ config MSM_IPC_ROUTER_GLINK_XPRT
|
|||
this layer registers a transport with IPC Router and enable
|
||||
message exchange.
|
||||
|
||||
config MSM_IPC_ROUTER_MHI_XPRT
|
||||
depends on MSM_MHI
|
||||
depends on IPC_ROUTER
|
||||
bool "MSM MHI XPRT Layer"
|
||||
help
|
||||
MHI Transport Layer that enables off-chip communication of
|
||||
IPC Router. When the MHI endpoint becomes available, this layer
|
||||
registers the transport with IPC Router and enable message
|
||||
exchange.
|
||||
|
||||
config MSM_JTAG
|
||||
bool "Debug and ETM trace support across power collapse"
|
||||
help
|
||||
|
|
|
@ -18,6 +18,7 @@ obj-$(CONFIG_MSM_GLINK_SMEM_NATIVE_XPRT) += glink_smem_native_xprt.o
|
|||
obj-$(CONFIG_MSM_GLINK_PKT) += msm_glink_pkt.o
|
||||
obj-$(CONFIG_MSM_IPC_ROUTER_GLINK_XPRT) += ipc_router_glink_xprt.o
|
||||
obj-$(CONFIG_MSM_IPC_ROUTER_HSIC_XPRT) += ipc_router_hsic_xprt.o
|
||||
obj-$(CONFIG_MSM_IPC_ROUTER_MHI_XPRT) += ipc_router_mhi_xprt.o
|
||||
obj-$(CONFIG_MSM_IPC_ROUTER_SMD_XPRT) += ipc_router_smd_xprt.o
|
||||
obj-$(CONFIG_MSM_MEMORY_DUMP) += memory_dump.o
|
||||
obj-$(CONFIG_MSM_MEMORY_DUMP_V2) += memory_dump_v2.o
|
||||
|
|
|
@ -0,0 +1,880 @@
|
|||
/* Copyright (c) 2014, 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* IPC ROUTER MHI XPRT module.
|
||||
*/
|
||||
#include <linux/delay.h>
|
||||
#include <linux/ipc_router_xprt.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/msm_mhi.h>
|
||||
#include <linux/of.h>
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/sched.h>
|
||||
#include <linux/skbuff.h>
|
||||
#include <linux/types.h>
|
||||
|
||||
|
||||
static int ipc_router_mhi_xprt_debug_mask;
|
||||
module_param_named(debug_mask, ipc_router_mhi_xprt_debug_mask,
|
||||
int, S_IRUGO | S_IWUSR | S_IWGRP);
|
||||
|
||||
#define D(x...) do { \
|
||||
if (ipc_router_mhi_xprt_debug_mask) \
|
||||
pr_info(x); \
|
||||
} while (0)
|
||||
|
||||
#define NUM_MHI_XPRTS 1
|
||||
#define XPRT_NAME_LEN 32
|
||||
#define IPC_ROUTER_MHI_XPRT_MAX_PKT_SIZE 0x1000
|
||||
#define IPC_ROUTER_MHI_XPRT_NUM_TRBS 10
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_channel - MHI Channel related information
|
||||
* @out_chan_id: Out channel ID for use by IPC ROUTER enumerated in MHI driver.
|
||||
* @out_handle: MHI Output channel handle.
|
||||
* @out_clnt_info: IPC Router callbacks/info to be passed to the MHI driver.
|
||||
* @in_chan_id: In channel ID for use by IPC ROUTER enumerated in MHI driver.
|
||||
* @in_handle: MHI Input channel handle.
|
||||
* @in_clnt_info: IPC Router callbacks/info to be passed to the MHI driver.
|
||||
* @state_lock: Lock to protect access to the state information.
|
||||
* @out_chan_enabled: State of the outgoing channel.
|
||||
* @in_chan_enabled: State of the incoming channel.
|
||||
* @bytes_to_tx: Remaining bytes to be transmitted in a packet.
|
||||
* @bytes_to_rx: Remaining bytes to be received in a packet.
|
||||
* @in_skbq_lock: Lock to protect access to the input skbs queue.
|
||||
* @in_skbq: Queue containing the input buffers.
|
||||
* @max_packet_size: Possible maximum packet size.
|
||||
* @num_trbs: Number of TRBs.
|
||||
* @mhi_xprtp: Pointer to IPC Router MHI XPRT.
|
||||
*/
|
||||
struct ipc_router_mhi_channel {
|
||||
enum MHI_CLIENT_CHANNEL out_chan_id;
|
||||
struct mhi_client_handle *out_handle;
|
||||
struct mhi_client_info_t out_clnt_info;
|
||||
|
||||
enum MHI_CLIENT_CHANNEL in_chan_id;
|
||||
struct mhi_client_handle *in_handle;
|
||||
struct mhi_client_info_t in_clnt_info;
|
||||
|
||||
struct mutex state_lock;
|
||||
bool out_chan_enabled;
|
||||
bool in_chan_enabled;
|
||||
int bytes_to_tx;
|
||||
int bytes_to_rx;
|
||||
|
||||
struct mutex in_skbq_lock;
|
||||
struct sk_buff_head in_skbq;
|
||||
size_t max_packet_size;
|
||||
uint32_t num_trbs;
|
||||
void *mhi_xprtp;
|
||||
};
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_xprt - IPC Router's MHI XPRT structure
|
||||
* @list: IPC router's MHI XPRTs list.
|
||||
* @ch_hndl: Data Structure to hold MHI Channel information.
|
||||
* @xprt_name: Name of the XPRT to be registered with IPC Router.
|
||||
* @xprt: IPC Router XPRT structure to contain MHI XPRT specific info.
|
||||
* @wq: Workqueue to queue read & other XPRT related works.
|
||||
* @read_work: Read Work to perform read operation from MHI Driver.
|
||||
* @in_pkt: Pointer to any partially read packet.
|
||||
* @write_wait_q: Wait Queue to handle the write events.
|
||||
* @sft_close_complete: Variable to indicate completion of SSR handling
|
||||
* by IPC Router.
|
||||
* @xprt_version: IPC Router header version supported by this XPRT.
|
||||
* @xprt_option: XPRT specific options to be handled by IPC Router.
|
||||
*/
|
||||
struct ipc_router_mhi_xprt {
|
||||
struct list_head list;
|
||||
struct ipc_router_mhi_channel ch_hndl;
|
||||
char xprt_name[XPRT_NAME_LEN];
|
||||
struct msm_ipc_router_xprt xprt;
|
||||
struct workqueue_struct *wq;
|
||||
struct work_struct read_work;
|
||||
struct rr_packet *in_pkt;
|
||||
wait_queue_head_t write_wait_q;
|
||||
struct completion sft_close_complete;
|
||||
unsigned xprt_version;
|
||||
unsigned xprt_option;
|
||||
};
|
||||
|
||||
struct ipc_router_mhi_xprt_work {
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp;
|
||||
enum MHI_CLIENT_CHANNEL chan_id;
|
||||
struct work_struct work;
|
||||
};
|
||||
|
||||
static void mhi_xprt_read_data(struct work_struct *work);
|
||||
static void mhi_xprt_enable_event(struct work_struct *work);
|
||||
static void mhi_xprt_disable_event(struct work_struct *work);
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_xprt_config - Config. Info. of each MHI XPRT
|
||||
* @out_chan_id: Out channel ID for use by IPC ROUTER enumerated in MHI driver.
|
||||
* @in_chan_id: In channel ID for use by IPC ROUTER enumerated in MHI driver.
|
||||
* @xprt_name: Name of the XPRT to be registered with IPC Router.
|
||||
* @link_id: Network Cluster ID to which this XPRT belongs to.
|
||||
* @xprt_version: IPC Router header version supported by this XPRT.
|
||||
*/
|
||||
struct ipc_router_mhi_xprt_config {
|
||||
enum MHI_CLIENT_CHANNEL out_chan_id;
|
||||
enum MHI_CLIENT_CHANNEL in_chan_id;
|
||||
char xprt_name[XPRT_NAME_LEN];
|
||||
uint32_t link_id;
|
||||
uint32_t xprt_version;
|
||||
};
|
||||
|
||||
#define MODULE_NAME "ipc_router_mhi_xprt"
|
||||
static DEFINE_MUTEX(mhi_xprt_list_lock_lha1);
|
||||
static LIST_HEAD(mhi_xprt_list);
|
||||
|
||||
/*
|
||||
* mhi_xprt_queue_in_buffers() - Queue input buffers
|
||||
* @mhi_xprtp: MHI XPRT in which the input buffer has to be queued.
|
||||
* @num_trbs: Number of buffers to be queued.
|
||||
*
|
||||
* @return: number of buffers queued.
|
||||
*/
|
||||
int mhi_xprt_queue_in_buffers(struct ipc_router_mhi_xprt *mhi_xprtp,
|
||||
uint32_t num_trbs)
|
||||
{
|
||||
int i;
|
||||
struct sk_buff *skb;
|
||||
dma_addr_t dma_addr;
|
||||
uint32_t buf_size = mhi_xprtp->ch_hndl.max_packet_size;
|
||||
enum MHI_STATUS rc_val = MHI_STATUS_SUCCESS;
|
||||
|
||||
for (i = 0; i < num_trbs; i++) {
|
||||
skb = alloc_skb(buf_size, GFP_KERNEL);
|
||||
if (!skb) {
|
||||
IPC_RTR_ERR("%s: Could not allocate %d SKB(s)\n",
|
||||
__func__, (i + 1));
|
||||
break;
|
||||
}
|
||||
dma_addr = dma_map_single(NULL, skb->data,
|
||||
buf_size, DMA_BIDIRECTIONAL);
|
||||
if (dma_mapping_error(NULL, dma_addr)) {
|
||||
IPC_RTR_ERR("%s: Failed to map DMA for SKB # %d\n",
|
||||
__func__, (i + 1));
|
||||
kfree_skb(skb);
|
||||
break;
|
||||
}
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.in_skbq_lock);
|
||||
rc_val = mhi_queue_xfer(mhi_xprtp->ch_hndl.in_handle,
|
||||
dma_addr, buf_size, MHI_EOT);
|
||||
if (rc_val != MHI_STATUS_SUCCESS) {
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.in_skbq_lock);
|
||||
IPC_RTR_ERR("%s: Failed to queue TRB # %d into MHI\n",
|
||||
__func__, (i + 1));
|
||||
dma_unmap_single(NULL, dma_addr,
|
||||
buf_size, DMA_TO_DEVICE);
|
||||
kfree_skb(skb);
|
||||
break;
|
||||
}
|
||||
skb_queue_tail(&mhi_xprtp->ch_hndl.in_skbq, skb);
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.in_skbq_lock);
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_get_xprt_version() - Get IPC Router header version
|
||||
* supported by the XPRT
|
||||
* @xprt: XPRT for which the version information is required.
|
||||
*
|
||||
* @return: IPC Router header version supported by the XPRT.
|
||||
*/
|
||||
static int ipc_router_mhi_get_xprt_version(struct msm_ipc_router_xprt *xprt)
|
||||
{
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp;
|
||||
if (!xprt)
|
||||
return -EINVAL;
|
||||
mhi_xprtp = container_of(xprt, struct ipc_router_mhi_xprt, xprt);
|
||||
|
||||
return (int)mhi_xprtp->xprt_version;
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_get_xprt_option() - Get XPRT options
|
||||
* @xprt: XPRT for which the option information is required.
|
||||
*
|
||||
* @return: Options supported by the XPRT.
|
||||
*/
|
||||
static int ipc_router_mhi_get_xprt_option(struct msm_ipc_router_xprt *xprt)
|
||||
{
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp;
|
||||
if (!xprt)
|
||||
return -EINVAL;
|
||||
mhi_xprtp = container_of(xprt, struct ipc_router_mhi_xprt, xprt);
|
||||
|
||||
return (int)mhi_xprtp->xprt_option;
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_write_avail() - Get available write space
|
||||
* @xprt: XPRT for which the available write space info. is required.
|
||||
*
|
||||
* @return: Write space in bytes on success, 0 on SSR.
|
||||
*/
|
||||
static int ipc_router_mhi_write_avail(struct msm_ipc_router_xprt *xprt)
|
||||
{
|
||||
int write_avail;
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp =
|
||||
container_of(xprt, struct ipc_router_mhi_xprt, xprt);
|
||||
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
if (!mhi_xprtp->ch_hndl.out_chan_enabled)
|
||||
write_avail = 0;
|
||||
else
|
||||
write_avail = mhi_get_free_desc(mhi_xprtp->ch_hndl.out_handle) *
|
||||
mhi_xprtp->ch_hndl.max_packet_size;
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
return write_avail;
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_write_skb() - Write a single SKB onto the XPRT
|
||||
* @mhi_xprtp: XPRT in which the SKB has to be written.
|
||||
* @skb: SKB to be written.
|
||||
*
|
||||
* @return: return number of bytes written on success,
|
||||
* standard Linux error codes on failure.
|
||||
*/
|
||||
static int ipc_router_mhi_write_skb(struct ipc_router_mhi_xprt *mhi_xprtp,
|
||||
struct sk_buff *skb)
|
||||
{
|
||||
size_t sz_to_write = 0;
|
||||
size_t offset = 0;
|
||||
int rc;
|
||||
dma_addr_t dma_addr;
|
||||
|
||||
while (offset < skb->len) {
|
||||
wait_event(mhi_xprtp->write_wait_q,
|
||||
mhi_get_free_desc(mhi_xprtp->ch_hndl.out_handle) ||
|
||||
!mhi_xprtp->ch_hndl.out_chan_enabled);
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
if (!mhi_xprtp->ch_hndl.out_chan_enabled) {
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
IPC_RTR_ERR("%s: %s chnl reset\n",
|
||||
__func__, mhi_xprtp->xprt_name);
|
||||
return -ENETRESET;
|
||||
}
|
||||
|
||||
sz_to_write = min((size_t)(skb->len - offset),
|
||||
(size_t)IPC_ROUTER_MHI_XPRT_MAX_PKT_SIZE);
|
||||
dma_addr = dma_map_single(NULL, skb->data + offset,
|
||||
sz_to_write, DMA_TO_DEVICE);
|
||||
if (dma_mapping_error(NULL, dma_addr)) {
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
IPC_RTR_ERR("%s: Failed to map DMA 0x%x\n",
|
||||
__func__, sz_to_write);
|
||||
return -ENOMEM;
|
||||
}
|
||||
rc = mhi_queue_xfer(mhi_xprtp->ch_hndl.out_handle,
|
||||
dma_addr, sz_to_write, MHI_EOT | MHI_EOB);
|
||||
if (rc != 0) {
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
dma_unmap_single(NULL, dma_addr, sz_to_write,
|
||||
DMA_TO_DEVICE);
|
||||
IPC_RTR_ERR("%s: Error queueing mhi_xfer 0x%x\n",
|
||||
__func__, sz_to_write);
|
||||
return -EFAULT;
|
||||
} else {
|
||||
offset += sz_to_write;
|
||||
mhi_xprtp->ch_hndl.bytes_to_tx += sz_to_write;
|
||||
}
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
}
|
||||
return skb->len;
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_write() - Write to XPRT
|
||||
* @data: Data to be written to the XPRT.
|
||||
* @len: Length of the data to be written.
|
||||
* @xprt: XPRT to which the data has to be written.
|
||||
*
|
||||
* @return: Data Length on success, standard Linux error codes on failure.
|
||||
*/
|
||||
static int ipc_router_mhi_write(void *data,
|
||||
uint32_t len, struct msm_ipc_router_xprt *xprt)
|
||||
{
|
||||
struct rr_packet *pkt = (struct rr_packet *)data;
|
||||
struct sk_buff *ipc_rtr_pkt;
|
||||
int rc;
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp =
|
||||
container_of(xprt, struct ipc_router_mhi_xprt, xprt);
|
||||
|
||||
if (!pkt)
|
||||
return -EINVAL;
|
||||
|
||||
if (!len || pkt->length != len)
|
||||
return -EINVAL;
|
||||
|
||||
D("%s: Ready to write %d bytes\n", __func__, len);
|
||||
skb_queue_walk(pkt->pkt_fragment_q, ipc_rtr_pkt) {
|
||||
rc = ipc_router_mhi_write_skb(mhi_xprtp, ipc_rtr_pkt);
|
||||
if (rc < 0) {
|
||||
IPC_RTR_ERR("%s: Error writing SKB %d\n",
|
||||
__func__, rc);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
wait_event(mhi_xprtp->write_wait_q, !mhi_xprtp->ch_hndl.bytes_to_tx);
|
||||
if (rc < 0)
|
||||
return rc;
|
||||
else
|
||||
return len;
|
||||
}
|
||||
|
||||
/**
|
||||
* mhi_xprt_read_data() - Read work to read from the XPRT
|
||||
* @work: Read work to be executed.
|
||||
*
|
||||
* This function is a read work item queued on a XPRT specific workqueue.
|
||||
* The work parameter contains information regarding the XPRT on which this
|
||||
* read work has to be performed. The work item keeps reading from the MHI
|
||||
* endpoint, until the endpoint returns an error.
|
||||
*/
|
||||
static void mhi_xprt_read_data(struct work_struct *work)
|
||||
{
|
||||
dma_addr_t data_addr;
|
||||
ssize_t data_sz;
|
||||
void *skb_data;
|
||||
struct sk_buff *skb;
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp =
|
||||
container_of(work, struct ipc_router_mhi_xprt, read_work);
|
||||
struct mhi_result result;
|
||||
int rc;
|
||||
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
if (!mhi_xprtp->ch_hndl.in_chan_enabled) {
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
if (mhi_xprtp->in_pkt)
|
||||
release_pkt(mhi_xprtp->in_pkt);
|
||||
mhi_xprtp->in_pkt = NULL;
|
||||
mhi_xprtp->ch_hndl.bytes_to_rx = 0;
|
||||
IPC_RTR_ERR("%s: %s channel reset\n",
|
||||
__func__, mhi_xprtp->xprt.name);
|
||||
return;
|
||||
}
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
|
||||
while (1) {
|
||||
rc = mhi_poll_inbound(mhi_xprtp->ch_hndl.in_handle, &result);
|
||||
if (rc || !result.payload_buf || !result.bytes_xferd) {
|
||||
if (rc != MHI_STATUS_RING_EMPTY)
|
||||
IPC_RTR_ERR("%s: Poll failed %s:%d:%p:%zu\n",
|
||||
__func__, mhi_xprtp->xprt_name, rc,
|
||||
(void *)result.payload_buf,
|
||||
result.bytes_xferd);
|
||||
break;
|
||||
}
|
||||
data_addr = result.payload_buf;
|
||||
data_sz = result.bytes_xferd;
|
||||
|
||||
/* Create a new rr_packet, if first fragment */
|
||||
if (!mhi_xprtp->ch_hndl.bytes_to_rx) {
|
||||
mhi_xprtp->in_pkt = create_pkt(NULL);
|
||||
if (!mhi_xprtp->in_pkt) {
|
||||
IPC_RTR_ERR("%s: Couldn't alloc rr_packet\n",
|
||||
__func__);
|
||||
return;
|
||||
}
|
||||
D("%s: Allocated rr_packet\n", __func__);
|
||||
}
|
||||
|
||||
skb_data = dma_to_virt(NULL, data_addr);
|
||||
dma_unmap_single(NULL, data_addr, data_sz, DMA_BIDIRECTIONAL);
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.in_skbq_lock);
|
||||
skb_queue_walk(&mhi_xprtp->ch_hndl.in_skbq, skb) {
|
||||
if (skb->data == skb_data) {
|
||||
skb_unlink(skb, &mhi_xprtp->ch_hndl.in_skbq);
|
||||
break;
|
||||
}
|
||||
}
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.in_skbq_lock);
|
||||
skb_put(skb, data_sz);
|
||||
skb_queue_tail(mhi_xprtp->in_pkt->pkt_fragment_q, skb);
|
||||
mhi_xprtp->in_pkt->length += data_sz;
|
||||
if (!mhi_xprtp->ch_hndl.bytes_to_rx)
|
||||
mhi_xprtp->ch_hndl.bytes_to_rx =
|
||||
ipc_router_peek_pkt_size(skb_data) - data_sz;
|
||||
else
|
||||
mhi_xprtp->ch_hndl.bytes_to_rx -= data_sz;
|
||||
/* Packet is completely read, so notify to router */
|
||||
if (!mhi_xprtp->ch_hndl.bytes_to_rx) {
|
||||
D("%s: Packet size read %d\n",
|
||||
__func__, mhi_xprtp->in_pkt->length);
|
||||
msm_ipc_router_xprt_notify(&mhi_xprtp->xprt,
|
||||
IPC_ROUTER_XPRT_EVENT_DATA,
|
||||
(void *)mhi_xprtp->in_pkt);
|
||||
release_pkt(mhi_xprtp->in_pkt);
|
||||
mhi_xprtp->in_pkt = NULL;
|
||||
}
|
||||
|
||||
while (mhi_xprt_queue_in_buffers(mhi_xprtp, 1) != 1 &&
|
||||
mhi_xprtp->ch_hndl.in_chan_enabled)
|
||||
msleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_close() - Close the XPRT
|
||||
* @xprt: XPRT which needs to be closed.
|
||||
*
|
||||
* @return: 0 on success, standard Linux error codes on failure.
|
||||
*/
|
||||
static int ipc_router_mhi_close(struct msm_ipc_router_xprt *xprt)
|
||||
{
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp;
|
||||
|
||||
if (!xprt)
|
||||
return -EINVAL;
|
||||
mhi_xprtp = container_of(xprt, struct ipc_router_mhi_xprt, xprt);
|
||||
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
mhi_xprtp->ch_hndl.out_chan_enabled = false;
|
||||
mhi_xprtp->ch_hndl.in_chan_enabled = false;
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
flush_workqueue(mhi_xprtp->wq);
|
||||
mhi_close_channel(mhi_xprtp->ch_hndl.in_handle);
|
||||
mhi_close_channel(mhi_xprtp->ch_hndl.out_handle);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* mhi_xprt_sft_close_done() - Completion of XPRT reset
|
||||
* @xprt: XPRT on which the reset operation is complete.
|
||||
*
|
||||
* This function is used by IPC Router to signal this MHI XPRT Abstraction
|
||||
* Layer(XAL) that the reset of XPRT is completely handled by IPC Router.
|
||||
*/
|
||||
static void mhi_xprt_sft_close_done(struct msm_ipc_router_xprt *xprt)
|
||||
{
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp =
|
||||
container_of(xprt, struct ipc_router_mhi_xprt, xprt);
|
||||
|
||||
complete_all(&mhi_xprtp->sft_close_complete);
|
||||
}
|
||||
|
||||
/**
|
||||
* mhi_xprt_enable_event() - Enable the MHI link for communication
|
||||
* @work: Work containing some reference to the link to be enabled.
|
||||
*
|
||||
* This work is scheduled when the MHI link to the peripheral is up.
|
||||
*/
|
||||
static void mhi_xprt_enable_event(struct work_struct *work)
|
||||
{
|
||||
struct ipc_router_mhi_xprt_work *xprt_work =
|
||||
container_of(work, struct ipc_router_mhi_xprt_work, work);
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp = xprt_work->mhi_xprtp;
|
||||
int rc;
|
||||
bool notify = false;
|
||||
|
||||
if (xprt_work->chan_id == mhi_xprtp->ch_hndl.out_chan_id) {
|
||||
rc = mhi_open_channel(mhi_xprtp->ch_hndl.out_handle);
|
||||
if (rc != MHI_STATUS_SUCCESS) {
|
||||
IPC_RTR_ERR("%s Failed to open chan 0x%x, rc %d\n",
|
||||
__func__, mhi_xprtp->ch_hndl.out_chan_id, rc);
|
||||
goto out_enable_event;
|
||||
}
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
mhi_xprtp->ch_hndl.out_chan_enabled = true;
|
||||
notify = mhi_xprtp->ch_hndl.out_chan_enabled &&
|
||||
mhi_xprtp->ch_hndl.in_chan_enabled;
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
} else if (xprt_work->chan_id == mhi_xprtp->ch_hndl.in_chan_id) {
|
||||
rc = mhi_open_channel(mhi_xprtp->ch_hndl.in_handle);
|
||||
if (rc != MHI_STATUS_SUCCESS) {
|
||||
IPC_RTR_ERR("%s Failed to open chan 0x%x, rc %d\n",
|
||||
__func__, mhi_xprtp->ch_hndl.in_chan_id, rc);
|
||||
goto out_enable_event;
|
||||
}
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
mhi_xprtp->ch_hndl.in_chan_enabled = true;
|
||||
notify = mhi_xprtp->ch_hndl.out_chan_enabled &&
|
||||
mhi_xprtp->ch_hndl.in_chan_enabled;
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
}
|
||||
|
||||
/* Register the XPRT before receiving any data */
|
||||
if (notify) {
|
||||
msm_ipc_router_xprt_notify(&mhi_xprtp->xprt,
|
||||
IPC_ROUTER_XPRT_EVENT_OPEN, NULL);
|
||||
D("%s: Notified IPC Router of %s OPEN\n",
|
||||
__func__, mhi_xprtp->xprt.name);
|
||||
}
|
||||
|
||||
if (xprt_work->chan_id != mhi_xprtp->ch_hndl.in_chan_id)
|
||||
goto out_enable_event;
|
||||
|
||||
rc = mhi_xprt_queue_in_buffers(mhi_xprtp, mhi_xprtp->ch_hndl.num_trbs);
|
||||
if (rc > 0)
|
||||
goto out_enable_event;
|
||||
|
||||
IPC_RTR_ERR("%s: Could not queue one TRB atleast\n", __func__);
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
mhi_xprtp->ch_hndl.in_chan_enabled = false;
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
if (notify)
|
||||
msm_ipc_router_xprt_notify(&mhi_xprtp->xprt,
|
||||
IPC_ROUTER_XPRT_EVENT_CLOSE, NULL);
|
||||
mhi_close_channel(mhi_xprtp->ch_hndl.in_handle);
|
||||
out_enable_event:
|
||||
kfree(xprt_work);
|
||||
}
|
||||
|
||||
/**
|
||||
* mhi_xprt_disable_event() - Disable the MHI link for communication
|
||||
* @work: Work containing some reference to the link to be disabled.
|
||||
*
|
||||
* This work is scheduled when the MHI link to the peripheral is down.
|
||||
*/
|
||||
static void mhi_xprt_disable_event(struct work_struct *work)
|
||||
{
|
||||
struct ipc_router_mhi_xprt_work *xprt_work =
|
||||
container_of(work, struct ipc_router_mhi_xprt_work, work);
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp = xprt_work->mhi_xprtp;
|
||||
bool notify = false;
|
||||
|
||||
if (xprt_work->chan_id == mhi_xprtp->ch_hndl.out_chan_id) {
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
notify = mhi_xprtp->ch_hndl.out_chan_enabled &&
|
||||
mhi_xprtp->ch_hndl.in_chan_enabled;
|
||||
mhi_xprtp->ch_hndl.out_chan_enabled = false;
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
wake_up(&mhi_xprtp->write_wait_q);
|
||||
mhi_close_channel(mhi_xprtp->ch_hndl.out_handle);
|
||||
} else if (xprt_work->chan_id == mhi_xprtp->ch_hndl.in_chan_id) {
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
notify = mhi_xprtp->ch_hndl.out_chan_enabled &&
|
||||
mhi_xprtp->ch_hndl.in_chan_enabled;
|
||||
mhi_xprtp->ch_hndl.in_chan_enabled = false;
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
/* Queue a read work to remove any partially read packets */
|
||||
queue_work(mhi_xprtp->wq, &mhi_xprtp->read_work);
|
||||
flush_workqueue(mhi_xprtp->wq);
|
||||
mhi_close_channel(mhi_xprtp->ch_hndl.in_handle);
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
init_completion(&mhi_xprtp->sft_close_complete);
|
||||
msm_ipc_router_xprt_notify(&mhi_xprtp->xprt,
|
||||
IPC_ROUTER_XPRT_EVENT_CLOSE, NULL);
|
||||
D("%s: Notified IPC Router of %s CLOSE\n",
|
||||
__func__, mhi_xprtp->xprt.name);
|
||||
wait_for_completion(&mhi_xprtp->sft_close_complete);
|
||||
}
|
||||
kfree(xprt_work);
|
||||
}
|
||||
|
||||
/**
|
||||
* mhi_xprt_xfer_event() - Function to handle MHI XFER Callbacks
|
||||
* @cb_info: Information containing xfer callback details.
|
||||
*
|
||||
* This function is called when the MHI generates a XFER event to the
|
||||
* IPC Router. This function is used to handle events like tx/rx.
|
||||
*/
|
||||
static void mhi_xprt_xfer_event(struct mhi_cb_info *cb_info)
|
||||
{
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp;
|
||||
|
||||
mhi_xprtp = (struct ipc_router_mhi_xprt *)(cb_info->result->user_data);
|
||||
if (cb_info->chan == mhi_xprtp->ch_hndl.out_chan_id) {
|
||||
dma_unmap_single(NULL, (dma_addr_t)cb_info->result->payload_buf,
|
||||
cb_info->result->bytes_xferd, DMA_TO_DEVICE);
|
||||
mutex_lock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
mhi_xprtp->ch_hndl.bytes_to_tx -= cb_info->result->bytes_xferd;
|
||||
if (!mhi_xprtp->ch_hndl.bytes_to_tx)
|
||||
wake_up(&mhi_xprtp->write_wait_q);
|
||||
mutex_unlock(&mhi_xprtp->ch_hndl.state_lock);
|
||||
} else if (cb_info->chan == mhi_xprtp->ch_hndl.in_chan_id) {
|
||||
queue_work(mhi_xprtp->wq, &mhi_xprtp->read_work);
|
||||
} else {
|
||||
IPC_RTR_ERR("%s: chan_id %d not part of %s\n",
|
||||
__func__, cb_info->chan, mhi_xprtp->xprt_name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_xprt_cb() - Callback to notify events on a channel
|
||||
* @cb_info: Information containing the details of callback.
|
||||
*
|
||||
* This function is called by the MHI driver to notify different events
|
||||
* like successful tx/rx, SSR events etc.
|
||||
*/
|
||||
static void ipc_router_mhi_xprt_cb(struct mhi_cb_info *cb_info)
|
||||
{
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp;
|
||||
struct ipc_router_mhi_xprt_work *xprt_work;
|
||||
|
||||
if (cb_info->result == NULL) {
|
||||
IPC_RTR_ERR("%s: Result not available in cb_info\n", __func__);
|
||||
return;
|
||||
}
|
||||
|
||||
mhi_xprtp = (struct ipc_router_mhi_xprt *)(cb_info->result->user_data);
|
||||
switch (cb_info->cb_reason) {
|
||||
case MHI_CB_MHI_ENABLED:
|
||||
case MHI_CB_MHI_DISABLED:
|
||||
xprt_work = kmalloc(sizeof(*xprt_work), GFP_KERNEL);
|
||||
if (!xprt_work) {
|
||||
IPC_RTR_ERR("%s: Couldn't handle %d event on %s\n",
|
||||
__func__, cb_info->cb_reason,
|
||||
mhi_xprtp->xprt_name);
|
||||
return;
|
||||
}
|
||||
xprt_work->mhi_xprtp = mhi_xprtp;
|
||||
xprt_work->chan_id = cb_info->chan;
|
||||
if (cb_info->cb_reason == MHI_CB_MHI_ENABLED)
|
||||
INIT_WORK(&xprt_work->work, mhi_xprt_enable_event);
|
||||
else
|
||||
INIT_WORK(&xprt_work->work, mhi_xprt_disable_event);
|
||||
queue_work(mhi_xprtp->wq, &xprt_work->work);
|
||||
break;
|
||||
case MHI_CB_XFER:
|
||||
mhi_xprt_xfer_event(cb_info);
|
||||
break;
|
||||
default:
|
||||
IPC_RTR_ERR("%s: Invalid cb reason %x\n",
|
||||
__func__, cb_info->cb_reason);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_driver_register() - register for MHI channels
|
||||
*
|
||||
* @mhi_xprtp: pointer to IPC router mhi xprt structure.
|
||||
*
|
||||
* @return: 0 on success, standard Linux error codes on error.
|
||||
*
|
||||
* This function is called when a new XPRT is added.
|
||||
*/
|
||||
static int ipc_router_mhi_driver_register(
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp)
|
||||
{
|
||||
enum MHI_STATUS rc_status;
|
||||
|
||||
rc_status = mhi_register_channel(&mhi_xprtp->ch_hndl.out_handle,
|
||||
mhi_xprtp->ch_hndl.out_chan_id, 0,
|
||||
&mhi_xprtp->ch_hndl.out_clnt_info,
|
||||
(void *)mhi_xprtp);
|
||||
if (rc_status != MHI_STATUS_SUCCESS) {
|
||||
IPC_RTR_ERR("%s: Error %d registering out_chan for %s\n",
|
||||
__func__, rc_status, mhi_xprtp->xprt_name);
|
||||
return -EFAULT;
|
||||
}
|
||||
|
||||
rc_status = mhi_register_channel(&mhi_xprtp->ch_hndl.in_handle,
|
||||
mhi_xprtp->ch_hndl.in_chan_id, 0,
|
||||
&mhi_xprtp->ch_hndl.in_clnt_info,
|
||||
(void *)mhi_xprtp);
|
||||
if (rc_status != MHI_STATUS_SUCCESS) {
|
||||
mhi_deregister_channel(mhi_xprtp->ch_hndl.out_handle);
|
||||
IPC_RTR_ERR("%s: Error %d registering in_chan for %s\n",
|
||||
__func__, rc_status, mhi_xprtp->xprt_name);
|
||||
return -EFAULT;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_config_init() - init MHI xprt configs
|
||||
*
|
||||
* @mhi_xprt_config: pointer to MHI xprt configurations.
|
||||
*
|
||||
* @return: 0 on success, standard Linux error codes on error.
|
||||
*
|
||||
* This function is called to initialize the MHI XPRT pointer with
|
||||
* the MHI XPRT configurations from device tree.
|
||||
*/
|
||||
static int ipc_router_mhi_config_init(
|
||||
struct ipc_router_mhi_xprt_config *mhi_xprt_config)
|
||||
{
|
||||
struct ipc_router_mhi_xprt *mhi_xprtp;
|
||||
char wq_name[XPRT_NAME_LEN];
|
||||
int rc;
|
||||
|
||||
mhi_xprtp = kzalloc(sizeof(struct ipc_router_mhi_xprt), GFP_KERNEL);
|
||||
if (IS_ERR_OR_NULL(mhi_xprtp)) {
|
||||
IPC_RTR_ERR("%s: kzalloc() failed for mhi_xprtp:%s\n",
|
||||
__func__, mhi_xprt_config->xprt_name);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
scnprintf(wq_name, XPRT_NAME_LEN, "MHI_XPRT%x:%x",
|
||||
mhi_xprt_config->out_chan_id, mhi_xprt_config->in_chan_id);
|
||||
mhi_xprtp->wq = create_singlethread_workqueue(wq_name);
|
||||
if (!mhi_xprtp->wq) {
|
||||
IPC_RTR_ERR("%s: %s create WQ failed\n",
|
||||
__func__, mhi_xprt_config->xprt_name);
|
||||
kfree(mhi_xprtp);
|
||||
return -EFAULT;
|
||||
}
|
||||
|
||||
INIT_WORK(&mhi_xprtp->read_work, mhi_xprt_read_data);
|
||||
init_waitqueue_head(&mhi_xprtp->write_wait_q);
|
||||
mhi_xprtp->xprt_version = mhi_xprt_config->xprt_version;
|
||||
strlcpy(mhi_xprtp->xprt_name, mhi_xprt_config->xprt_name,
|
||||
XPRT_NAME_LEN);
|
||||
|
||||
/* Initialize XPRT operations and parameters registered with IPC RTR */
|
||||
mhi_xprtp->xprt.link_id = mhi_xprt_config->link_id;
|
||||
mhi_xprtp->xprt.name = mhi_xprtp->xprt_name;
|
||||
mhi_xprtp->xprt.get_version = ipc_router_mhi_get_xprt_version;
|
||||
mhi_xprtp->xprt.get_option = ipc_router_mhi_get_xprt_option;
|
||||
mhi_xprtp->xprt.read_avail = NULL;
|
||||
mhi_xprtp->xprt.read = NULL;
|
||||
mhi_xprtp->xprt.write_avail = ipc_router_mhi_write_avail;
|
||||
mhi_xprtp->xprt.write = ipc_router_mhi_write;
|
||||
mhi_xprtp->xprt.close = ipc_router_mhi_close;
|
||||
mhi_xprtp->xprt.sft_close_done = mhi_xprt_sft_close_done;
|
||||
mhi_xprtp->xprt.priv = NULL;
|
||||
|
||||
/* Initialize channel handle parameters */
|
||||
mhi_xprtp->ch_hndl.out_chan_id = mhi_xprt_config->out_chan_id;
|
||||
mhi_xprtp->ch_hndl.in_chan_id = mhi_xprt_config->in_chan_id;
|
||||
mhi_xprtp->ch_hndl.out_clnt_info.mhi_client_cb = ipc_router_mhi_xprt_cb;
|
||||
mhi_xprtp->ch_hndl.in_clnt_info.mhi_client_cb = ipc_router_mhi_xprt_cb;
|
||||
mutex_init(&mhi_xprtp->ch_hndl.state_lock);
|
||||
mutex_init(&mhi_xprtp->ch_hndl.in_skbq_lock);
|
||||
skb_queue_head_init(&mhi_xprtp->ch_hndl.in_skbq);
|
||||
mhi_xprtp->ch_hndl.max_packet_size = IPC_ROUTER_MHI_XPRT_MAX_PKT_SIZE;
|
||||
mhi_xprtp->ch_hndl.num_trbs = IPC_ROUTER_MHI_XPRT_NUM_TRBS;
|
||||
mhi_xprtp->ch_hndl.mhi_xprtp = mhi_xprtp;
|
||||
|
||||
rc = ipc_router_mhi_driver_register(mhi_xprtp);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/**
|
||||
* parse_devicetree() - parse device tree binding
|
||||
*
|
||||
* @node: pointer to device tree node
|
||||
* @mhi_xprt_config: pointer to MHI XPRT configurations
|
||||
*
|
||||
* @return: 0 on success, -ENODEV on failure.
|
||||
*/
|
||||
static int parse_devicetree(struct device_node *node,
|
||||
struct ipc_router_mhi_xprt_config *mhi_xprt_config)
|
||||
{
|
||||
int rc;
|
||||
uint32_t out_chan_id;
|
||||
uint32_t in_chan_id;
|
||||
const char *remote_ss;
|
||||
uint32_t link_id;
|
||||
uint32_t version;
|
||||
char *key;
|
||||
|
||||
key = "qcom,out-chan-id";
|
||||
rc = of_property_read_u32(node, key, &out_chan_id);
|
||||
if (rc)
|
||||
goto error;
|
||||
mhi_xprt_config->out_chan_id = out_chan_id;
|
||||
|
||||
key = "qcom,in-chan-id";
|
||||
rc = of_property_read_u32(node, key, &in_chan_id);
|
||||
if (rc)
|
||||
goto error;
|
||||
mhi_xprt_config->in_chan_id = in_chan_id;
|
||||
|
||||
key = "qcom,xprt-remote";
|
||||
remote_ss = of_get_property(node, key, NULL);
|
||||
if (!remote_ss)
|
||||
goto error;
|
||||
|
||||
key = "qcom,xprt-linkid";
|
||||
rc = of_property_read_u32(node, key, &link_id);
|
||||
if (rc)
|
||||
goto error;
|
||||
mhi_xprt_config->link_id = link_id;
|
||||
|
||||
key = "qcom,xprt-version";
|
||||
rc = of_property_read_u32(node, key, &version);
|
||||
if (rc)
|
||||
goto error;
|
||||
mhi_xprt_config->xprt_version = version;
|
||||
|
||||
scnprintf(mhi_xprt_config->xprt_name, XPRT_NAME_LEN,
|
||||
"IPCRTR_MHI%x:%x_%s",
|
||||
out_chan_id, in_chan_id, remote_ss);
|
||||
|
||||
return 0;
|
||||
error:
|
||||
IPC_RTR_ERR("%s: missing key: %s\n", __func__, key);
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
/**
|
||||
* ipc_router_mhi_xprt_probe() - Probe an MHI xprt
|
||||
* @pdev: Platform device corresponding to MHI xprt.
|
||||
*
|
||||
* @return: 0 on success, standard Linux error codes on error.
|
||||
*
|
||||
* This function is called when the underlying device tree driver registers
|
||||
* a platform device, mapped to an MHI transport.
|
||||
*/
|
||||
static int ipc_router_mhi_xprt_probe(struct platform_device *pdev)
|
||||
{
|
||||
int rc;
|
||||
struct ipc_router_mhi_xprt_config mhi_xprt_config;
|
||||
|
||||
if (pdev && pdev->dev.of_node) {
|
||||
rc = parse_devicetree(pdev->dev.of_node, &mhi_xprt_config);
|
||||
if (rc) {
|
||||
IPC_RTR_ERR("%s: failed to parse device tree\n",
|
||||
__func__);
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = ipc_router_mhi_config_init(&mhi_xprt_config);
|
||||
if (rc) {
|
||||
IPC_RTR_ERR("%s: init failed\n", __func__);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static struct of_device_id ipc_router_mhi_xprt_match_table[] = {
|
||||
{ .compatible = "qcom,ipc_router_mhi_xprt" },
|
||||
{},
|
||||
};
|
||||
|
||||
static struct platform_driver ipc_router_mhi_xprt_driver = {
|
||||
.probe = ipc_router_mhi_xprt_probe,
|
||||
.driver = {
|
||||
.name = MODULE_NAME,
|
||||
.owner = THIS_MODULE,
|
||||
.of_match_table = ipc_router_mhi_xprt_match_table,
|
||||
},
|
||||
};
|
||||
|
||||
static int __init ipc_router_mhi_xprt_init(void)
|
||||
{
|
||||
int rc;
|
||||
|
||||
rc = platform_driver_register(&ipc_router_mhi_xprt_driver);
|
||||
if (rc) {
|
||||
IPC_RTR_ERR("%s: ipc_router_mhi_xprt_driver reg. failed %d\n",
|
||||
__func__, rc);
|
||||
return rc;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
module_init(ipc_router_mhi_xprt_init);
|
||||
MODULE_DESCRIPTION("IPC Router MHI XPRT");
|
||||
MODULE_LICENSE("GPL v2");
|
Loading…
Reference in New Issue