Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c8545410 authored by Linux Build Service Account's avatar Linux Build Service Account Committed by Gerrit - the friendly Code Review server
Browse files

Merge "soc: qcom: Add support for QDSS bridge driver"

parents 7b45e712 fa04d094
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
Qualcomm Technologies, Inc. QDSS bridge Driver

This device will enable routing debug data from modem
subsystem to APSS host.

Required properties:
-compatible : "qcom,qdss-mhi".
-qcom,mhi : phandle of MHI Device to connect to.

Example:
	qcom,qdss-mhi {
		compatible = "qcom,qdss-mhi";
		qcom,mhi = <&mhi_0>;
	};
+8 −0
Original line number Diff line number Diff line
@@ -686,3 +686,11 @@ config MSM_JTAGV8
	  and ETM registers are saved and restored across power collapse.
	  If unsure, say 'N' here to avoid potential power, performance and
	  memory penalty.

config QCOM_QDSS_BRIDGE
	bool "Configure bridge driver for QTI/Qualcomm Technologies, Inc. MDM"
	depends on MSM_MHI
	help
	  The driver will help route diag traffic from modem side over the QDSS
	  sub-system to USB on APSS side. The driver acts as a bridge between the
	  MHI and USB interface. If unsure, say N.
 No newline at end of file
+1 −0
Original line number Diff line number Diff line
@@ -79,3 +79,4 @@ obj-$(CONFIG_QCOM_SMCINVOKE) += smcinvoke.o
obj-$(CONFIG_QMP_DEBUGFS_CLIENT) += qmp-debugfs-client.o
obj-$(CONFIG_MSM_REMOTEQDSS) += remoteqdss.o
obj-$(CONFIG_QSEE_IPC_IRQ_BRIDGE) += qsee_ipc_irq_bridge.o
obj-$(CONFIG_QCOM_QDSS_BRIDGE) += qdss_bridge.o
 No newline at end of file
+463 −0
Original line number Diff line number Diff line
/* Copyright (c) 2017, 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.
 */

#define KMSG_COMPONENT "QDSS diag bridge"
#define pr_fmt(fmt) KMSG_COMPONENT ": " fmt

#include <linux/slab.h>
#include <linux/module.h>
#include <linux/ratelimit.h>
#include <linux/workqueue.h>
#include <linux/platform_device.h>
#include <linux/moduleparam.h>
#include <linux/msm_mhi.h>
#include <linux/usb/usb_qdss.h>
#include "qdss_bridge.h"

#define MODULE_NAME "qdss_bridge"

#define QDSS_BUF_SIZE		(16*1024)
#define MHI_CLIENT_QDSS_IN	9

/* Max number of objects needed */
static int poolsize = 32;
module_param(poolsize, int, 0644);

/* Size of single buffer */
static int itemsize = QDSS_BUF_SIZE;
module_param(itemsize, int, 0644);

static int qdss_destroy_buf_tbl(struct qdss_bridge_drvdata *drvdata)
{
	struct list_head *start, *temp;
	struct qdss_buf_tbl_lst *entry = NULL;

	list_for_each_safe(start, temp, &drvdata->buf_tbl) {
		entry = list_entry(start, struct qdss_buf_tbl_lst, link);
		list_del(&entry->link);
		kfree(entry->buf);
		kfree(entry->usb_req);
		kfree(entry);
	}

	return 0;
}

static int qdss_create_buf_tbl(struct qdss_bridge_drvdata *drvdata)
{
	struct qdss_buf_tbl_lst *entry;
	void *buf;
	struct qdss_request *usb_req;
	int i;

	for (i = 0; i < poolsize; i++) {
		entry = kzalloc(sizeof(*entry), GFP_KERNEL);
		if (!entry)
			goto err;

		buf = kzalloc(QDSS_BUF_SIZE, GFP_KERNEL);
		usb_req = kzalloc(sizeof(*usb_req), GFP_KERNEL);

		entry->buf = buf;
		entry->usb_req = usb_req;
		atomic_set(&entry->available, 1);
		list_add_tail(&entry->link, &drvdata->buf_tbl);

		if (!buf || !usb_req)
			goto err;
	}

	return 0;
err:
	qdss_destroy_buf_tbl(drvdata);
	return -ENOMEM;
}

struct qdss_buf_tbl_lst *qdss_get_buf_tbl_entry(
					struct qdss_bridge_drvdata *drvdata,
					void *buf)
{
	struct qdss_buf_tbl_lst *entry;

	list_for_each_entry(entry, &drvdata->buf_tbl, link) {
		if (atomic_read(&entry->available))
			continue;
		if (entry->buf == buf)
			return entry;
	}

	return NULL;
}

struct qdss_buf_tbl_lst *qdss_get_entry(struct qdss_bridge_drvdata *drvdata)
{
	struct qdss_buf_tbl_lst *item;

	list_for_each_entry(item, &drvdata->buf_tbl, link)
		if (atomic_cmpxchg(&item->available, 1, 0) == 1)
			return item;

	return NULL;
}

static void qdss_buf_tbl_remove(struct qdss_bridge_drvdata *drvdata,
				void *buf)
{
	struct qdss_buf_tbl_lst *entry = NULL;

	list_for_each_entry(entry, &drvdata->buf_tbl, link) {
		if (entry->buf != buf)
			continue;
		atomic_set(&entry->available, 1);
		return;
	}

	pr_err_ratelimited("Failed to find buffer for removal\n");
}

static void mhi_ch_close(struct qdss_bridge_drvdata *drvdata)
{
	flush_workqueue(drvdata->mhi_wq);
	qdss_destroy_buf_tbl(drvdata);
	mhi_close_channel(drvdata->hdl);
}

static void mhi_close_work_fn(struct work_struct *work)
{
	struct qdss_bridge_drvdata *drvdata =
				container_of(work,
					     struct qdss_bridge_drvdata,
					     close_work);

	usb_qdss_close(drvdata->usb_ch);
	mhi_ch_close(drvdata);
}

static void mhi_read_work_fn(struct work_struct *work)
{
	int err = 0;
	enum MHI_FLAGS mhi_flags = MHI_EOT;
	struct qdss_buf_tbl_lst *entry;

	struct qdss_bridge_drvdata *drvdata =
				container_of(work,
					     struct qdss_bridge_drvdata,
					     read_work);

	do {
		if (!drvdata->opened)
			break;
		entry = qdss_get_entry(drvdata);
		if (!entry)
			break;

		err = mhi_queue_xfer(drvdata->hdl, entry->buf, QDSS_BUF_SIZE,
				      mhi_flags);
		if (err) {
			pr_err_ratelimited("Unable to read from MHI buffer err:%d",
					   err);
			goto fail;
		}
	} while (entry);

	return;
fail:
	qdss_buf_tbl_remove(drvdata, entry->buf);
	queue_work(drvdata->mhi_wq, &drvdata->read_work);
}

static int mhi_queue_read(struct qdss_bridge_drvdata *drvdata)
{
	queue_work(drvdata->mhi_wq, &(drvdata->read_work));
	return 0;
}

static int usb_write(struct qdss_bridge_drvdata *drvdata,
			     struct mhi_result *result)
{
	int ret = 0;
	struct qdss_buf_tbl_lst *entry;

	entry = qdss_get_buf_tbl_entry(drvdata, result->buf_addr);
	if (!entry)
		return -EINVAL;

	entry->usb_req->buf = result->buf_addr;
	entry->usb_req->length = result->bytes_xferd;
	ret = usb_qdss_data_write(drvdata->usb_ch, entry->usb_req);

	return ret;
}

static void mhi_read_done_work_fn(struct work_struct *work)
{
	unsigned char *buf = NULL;
	struct mhi_result result;
	int err = 0;
	struct qdss_bridge_drvdata *drvdata =
				container_of(work,
					     struct qdss_bridge_drvdata,
					     read_done_work);

	do {
		err = mhi_poll_inbound(drvdata->hdl, &result);
		if (err) {
			pr_debug("MHI poll failed err:%d\n", err);
			break;
		}
		buf = result.buf_addr;
		if (!buf)
			break;
		err = usb_write(drvdata, &result);
		if (err)
			qdss_buf_tbl_remove(drvdata, buf);
	} while (1);
}

static void usb_write_done(struct qdss_bridge_drvdata *drvdata,
				   struct qdss_request *d_req)
{
	if (d_req->status) {
		pr_err_ratelimited("USB write failed err:%d\n", d_req->status);
		mhi_queue_read(drvdata);
		return;
	}
	qdss_buf_tbl_remove(drvdata, d_req->buf);
	mhi_queue_read(drvdata);
}

static void usb_notifier(void *priv, unsigned int event,
			struct qdss_request *d_req, struct usb_qdss_ch *ch)
{
	struct qdss_bridge_drvdata *drvdata = priv;

	if (!drvdata)
		return;

	switch (event) {
	case USB_QDSS_CONNECT:
		usb_qdss_alloc_req(drvdata->usb_ch, poolsize, 0);
		mhi_queue_read(drvdata);
		break;

	case USB_QDSS_DISCONNECT:
		/* Leave MHI/USB open.Only close on MHI disconnect */
		break;

	case USB_QDSS_DATA_WRITE_DONE:
		usb_write_done(drvdata, d_req);
		break;

	default:
		break;
	}
}

static int mhi_ch_open(struct qdss_bridge_drvdata *drvdata)
{
	int ret;

	if (drvdata->opened)
		return 0;

	ret = mhi_open_channel(drvdata->hdl);
	if (ret) {
		pr_err("Unable to open MHI channel\n");
		return ret;
	}

	ret = mhi_get_free_desc(drvdata->hdl);
	if (ret <= 0)
		return -EIO;

	drvdata->opened = 1;
	return 0;
}

static void qdss_bridge_open_work_fn(struct work_struct *work)
{
	struct qdss_bridge_drvdata *drvdata =
				container_of(work,
					     struct qdss_bridge_drvdata,
					     open_work);
	int ret;

	ret = mhi_ch_open(drvdata);
	if (ret)
		goto err_open;

	ret = qdss_create_buf_tbl(drvdata);
	if (ret)
		goto err;

	drvdata->usb_ch = usb_qdss_open("qdss_mdm", drvdata, usb_notifier);
	if (IS_ERR_OR_NULL(drvdata->usb_ch)) {
		ret = PTR_ERR(drvdata->usb_ch);
		goto err;
	}

	return;
err:
	mhi_ch_close(drvdata);
err_open:
	pr_err("Open work failed with err:%d\n", ret);
}

static void mhi_notifier(struct mhi_cb_info *cb_info)
{
	struct mhi_result *result;
	struct qdss_bridge_drvdata *drvdata;

	if (!cb_info)
		return;

	result = cb_info->result;
	if (!result) {
		pr_err_ratelimited("Failed to obtain MHI result\n");
		return;
	}

	drvdata = (struct qdss_bridge_drvdata *)cb_info->result->user_data;
	if (!drvdata) {
		pr_err_ratelimited("MHI returned invalid drvdata\n");
		return;
	}

	switch (cb_info->cb_reason) {
	case MHI_CB_MHI_ENABLED:
		queue_work(drvdata->mhi_wq, &drvdata->open_work);
		break;

	case MHI_CB_XFER:
		if (!drvdata->opened)
			break;

		queue_work(drvdata->mhi_wq, &drvdata->read_done_work);
		break;

	case MHI_CB_MHI_DISABLED:
		if (!drvdata->opened)
			break;

		drvdata->opened = 0;
		queue_work(drvdata->mhi_wq, &drvdata->close_work);
		break;

	default:
		pr_err_ratelimited("MHI returned invalid cb reason 0x%x\n",
		       cb_info->cb_reason);
		break;
	}
}

static int qdss_mhi_register_ch(struct qdss_bridge_drvdata *drvdata)
{
	struct mhi_client_info_t *client_info;
	int ret;
	struct mhi_client_info_t *mhi_info;

	client_info = devm_kzalloc(drvdata->dev, sizeof(*client_info),
				   GFP_KERNEL);
	if (!client_info)
		return -ENOMEM;

	client_info->mhi_client_cb = mhi_notifier;
	drvdata->client_info = client_info;

	mhi_info = client_info;
	mhi_info->chan = MHI_CLIENT_QDSS_IN;
	mhi_info->dev = drvdata->dev;
	mhi_info->node_name = "qcom,mhi";
	mhi_info->user_data = drvdata;

	ret = mhi_register_channel(&drvdata->hdl, mhi_info);
	return ret;
}

int qdss_mhi_init(struct qdss_bridge_drvdata *drvdata)
{
	int ret;

	drvdata->mhi_wq = create_singlethread_workqueue(MODULE_NAME);
	if (!drvdata->mhi_wq)
		return -ENOMEM;

	INIT_WORK(&(drvdata->read_work), mhi_read_work_fn);
	INIT_WORK(&(drvdata->read_done_work), mhi_read_done_work_fn);
	INIT_WORK(&(drvdata->open_work), qdss_bridge_open_work_fn);
	INIT_WORK(&(drvdata->close_work), mhi_close_work_fn);
	INIT_LIST_HEAD(&drvdata->buf_tbl);
	drvdata->opened = 0;

	ret = qdss_mhi_register_ch(drvdata);
	if (ret) {
		destroy_workqueue(drvdata->mhi_wq);
		pr_err("Unable to register MHI read channel err:%d\n", ret);
		return ret;
	}

	return 0;
}

static int qdss_mhi_probe(struct platform_device *pdev)
{
	int ret;
	struct device *dev = &pdev->dev;
	struct qdss_bridge_drvdata *drvdata;

	drvdata = devm_kzalloc(dev, sizeof(*drvdata), GFP_KERNEL);
	if (!drvdata) {
		ret = -ENOMEM;
		return ret;
	}

	drvdata->dev = &pdev->dev;
	platform_set_drvdata(pdev, drvdata);

	ret = qdss_mhi_init(drvdata);
	if (ret)
		goto err;

	return 0;
err:
	pr_err("Device probe failed err:%d\n", ret);
	return ret;
}

static const struct of_device_id qdss_mhi_table[] = {
	{.compatible = "qcom,qdss-mhi"},
	{},
};

static struct platform_driver qdss_mhi_driver = {
	.probe = qdss_mhi_probe,
	.driver = {
		.name = MODULE_NAME,
		.owner = THIS_MODULE,
		.of_match_table = qdss_mhi_table,
	},
};

static int __init qdss_bridge_init(void)
{
	return platform_driver_register(&qdss_mhi_driver);
}

static void __exit qdss_bridge_exit(void)
{
	platform_driver_unregister(&qdss_mhi_driver);
}

module_init(qdss_bridge_init);
module_exit(qdss_bridge_exit);
MODULE_LICENSE("GPL v2")
MODULE_DESCRIPTION("QDSS Bridge driver");
+37 −0
Original line number Diff line number Diff line
/* Copyright (c) 2017, 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.
 */

#ifndef _QDSS_BRIDGE_H
#define _QDSS_BRIDGE_H

struct qdss_buf_tbl_lst {
	struct list_head link;
	unsigned char *buf;
	struct qdss_request *usb_req;
	atomic_t available;
};

struct qdss_bridge_drvdata {
	struct device *dev;
	bool opened;
	struct work_struct read_work;
	struct work_struct read_done_work;
	struct work_struct open_work;
	struct work_struct close_work;
	struct workqueue_struct *mhi_wq;
	struct mhi_client_handle *hdl;
	struct mhi_client_info_t *client_info;
	struct list_head buf_tbl;
	struct usb_qdss_ch *usb_ch;
};

#endif