diff options
Diffstat (limited to 'drivers/cdx/controller/mcdi.c')
| -rw-r--r-- | drivers/cdx/controller/mcdi.c | 903 | 
1 files changed, 903 insertions, 0 deletions
diff --git a/drivers/cdx/controller/mcdi.c b/drivers/cdx/controller/mcdi.c new file mode 100644 index 000000000000..a211a2ca762e --- /dev/null +++ b/drivers/cdx/controller/mcdi.c @@ -0,0 +1,903 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Management-Controller-to-Driver Interface + * + * Copyright 2008-2013 Solarflare Communications Inc. + * Copyright (C) 2022-2023, Advanced Micro Devices, Inc. + */ +#include <linux/delay.h> +#include <linux/slab.h> +#include <linux/io.h> +#include <linux/spinlock.h> +#include <linux/netdevice.h> +#include <linux/etherdevice.h> +#include <linux/ethtool.h> +#include <linux/if_vlan.h> +#include <linux/timer.h> +#include <linux/list.h> +#include <linux/pci.h> +#include <linux/device.h> +#include <linux/rwsem.h> +#include <linux/vmalloc.h> +#include <net/netevent.h> +#include <linux/log2.h> +#include <linux/net_tstamp.h> +#include <linux/wait.h> + +#include "bitfield.h" +#include "mcdi.h" + +struct cdx_mcdi_copy_buffer { +	struct cdx_dword buffer[DIV_ROUND_UP(MCDI_CTL_SDU_LEN_MAX, 4)]; +}; + +#ifdef CONFIG_MCDI_LOGGING +#define LOG_LINE_MAX		(1024 - 32) +#endif + +static void cdx_mcdi_cancel_cmd(struct cdx_mcdi *cdx, struct cdx_mcdi_cmd *cmd); +static void cdx_mcdi_wait_for_cleanup(struct cdx_mcdi *cdx); +static int cdx_mcdi_rpc_async_internal(struct cdx_mcdi *cdx, +				       struct cdx_mcdi_cmd *cmd, +				       unsigned int *handle); +static void cdx_mcdi_start_or_queue(struct cdx_mcdi_iface *mcdi, +				    bool allow_retry); +static void cdx_mcdi_cmd_start_or_queue(struct cdx_mcdi_iface *mcdi, +					struct cdx_mcdi_cmd *cmd); +static bool cdx_mcdi_complete_cmd(struct cdx_mcdi_iface *mcdi, +				  struct cdx_mcdi_cmd *cmd, +				  struct cdx_dword *outbuf, +				  int len, +				  struct list_head *cleanup_list); +static void cdx_mcdi_timeout_cmd(struct cdx_mcdi_iface *mcdi, +				 struct cdx_mcdi_cmd *cmd, +				 struct list_head *cleanup_list); +static void cdx_mcdi_cmd_work(struct work_struct *context); +static void cdx_mcdi_mode_fail(struct cdx_mcdi *cdx, struct list_head *cleanup_list); +static void _cdx_mcdi_display_error(struct cdx_mcdi *cdx, unsigned int cmd, +				    size_t inlen, int raw, int arg, int err_no); + +static bool cdx_cmd_cancelled(struct cdx_mcdi_cmd *cmd) +{ +	return cmd->state == MCDI_STATE_RUNNING_CANCELLED; +} + +static void cdx_mcdi_cmd_release(struct kref *ref) +{ +	kfree(container_of(ref, struct cdx_mcdi_cmd, ref)); +} + +static unsigned int cdx_mcdi_cmd_handle(struct cdx_mcdi_cmd *cmd) +{ +	return cmd->handle; +} + +static void _cdx_mcdi_remove_cmd(struct cdx_mcdi_iface *mcdi, +				 struct cdx_mcdi_cmd *cmd, +				 struct list_head *cleanup_list) +{ +	/* if cancelled, the completers have already been called */ +	if (cdx_cmd_cancelled(cmd)) +		return; + +	if (cmd->completer) { +		list_add_tail(&cmd->cleanup_list, cleanup_list); +		++mcdi->outstanding_cleanups; +		kref_get(&cmd->ref); +	} +} + +static void cdx_mcdi_remove_cmd(struct cdx_mcdi_iface *mcdi, +				struct cdx_mcdi_cmd *cmd, +				struct list_head *cleanup_list) +{ +	list_del(&cmd->list); +	_cdx_mcdi_remove_cmd(mcdi, cmd, cleanup_list); +	cmd->state = MCDI_STATE_FINISHED; +	kref_put(&cmd->ref, cdx_mcdi_cmd_release); +	if (list_empty(&mcdi->cmd_list)) +		wake_up(&mcdi->cmd_complete_wq); +} + +static unsigned long cdx_mcdi_rpc_timeout(struct cdx_mcdi *cdx, unsigned int cmd) +{ +	if (!cdx->mcdi_ops->mcdi_rpc_timeout) +		return MCDI_RPC_TIMEOUT; +	else +		return cdx->mcdi_ops->mcdi_rpc_timeout(cdx, cmd); +} + +int cdx_mcdi_init(struct cdx_mcdi *cdx) +{ +	struct cdx_mcdi_iface *mcdi; +	int rc = -ENOMEM; + +	cdx->mcdi = kzalloc(sizeof(*cdx->mcdi), GFP_KERNEL); +	if (!cdx->mcdi) +		goto fail; + +	mcdi = cdx_mcdi_if(cdx); +	mcdi->cdx = cdx; + +#ifdef CONFIG_MCDI_LOGGING +	mcdi->logging_buffer = kmalloc(LOG_LINE_MAX, GFP_KERNEL); +	if (!mcdi->logging_buffer) +		goto fail2; +#endif +	mcdi->workqueue = alloc_ordered_workqueue("mcdi_wq", 0); +	if (!mcdi->workqueue) +		goto fail3; +	mutex_init(&mcdi->iface_lock); +	mcdi->mode = MCDI_MODE_EVENTS; +	INIT_LIST_HEAD(&mcdi->cmd_list); +	init_waitqueue_head(&mcdi->cmd_complete_wq); + +	mcdi->new_epoch = true; + +	return 0; +fail3: +#ifdef CONFIG_MCDI_LOGGING +	kfree(mcdi->logging_buffer); +fail2: +#endif +	kfree(cdx->mcdi); +	cdx->mcdi = NULL; +fail: +	return rc; +} + +void cdx_mcdi_finish(struct cdx_mcdi *cdx) +{ +	struct cdx_mcdi_iface *mcdi; + +	mcdi = cdx_mcdi_if(cdx); +	if (!mcdi) +		return; + +	cdx_mcdi_wait_for_cleanup(cdx); + +#ifdef CONFIG_MCDI_LOGGING +	kfree(mcdi->logging_buffer); +#endif + +	destroy_workqueue(mcdi->workqueue); +	kfree(cdx->mcdi); +	cdx->mcdi = NULL; +} + +static bool cdx_mcdi_flushed(struct cdx_mcdi_iface *mcdi, bool ignore_cleanups) +{ +	bool flushed; + +	mutex_lock(&mcdi->iface_lock); +	flushed = list_empty(&mcdi->cmd_list) && +		  (ignore_cleanups || !mcdi->outstanding_cleanups); +	mutex_unlock(&mcdi->iface_lock); +	return flushed; +} + +/* Wait for outstanding MCDI commands to complete. */ +static void cdx_mcdi_wait_for_cleanup(struct cdx_mcdi *cdx) +{ +	struct cdx_mcdi_iface *mcdi = cdx_mcdi_if(cdx); + +	if (!mcdi) +		return; + +	wait_event(mcdi->cmd_complete_wq, +		   cdx_mcdi_flushed(mcdi, false)); +} + +int cdx_mcdi_wait_for_quiescence(struct cdx_mcdi *cdx, +				 unsigned int timeout_jiffies) +{ +	struct cdx_mcdi_iface *mcdi = cdx_mcdi_if(cdx); +	DEFINE_WAIT_FUNC(wait, woken_wake_function); +	int rc = 0; + +	if (!mcdi) +		return -EINVAL; + +	flush_workqueue(mcdi->workqueue); + +	add_wait_queue(&mcdi->cmd_complete_wq, &wait); + +	while (!cdx_mcdi_flushed(mcdi, true)) { +		rc = wait_woken(&wait, TASK_IDLE, timeout_jiffies); +		if (rc) +			continue; +		break; +	} + +	remove_wait_queue(&mcdi->cmd_complete_wq, &wait); + +	if (rc > 0) +		rc = 0; +	else if (rc == 0) +		rc = -ETIMEDOUT; + +	return rc; +} + +static u8 cdx_mcdi_payload_csum(const struct cdx_dword *hdr, size_t hdr_len, +				const struct cdx_dword *sdu, size_t sdu_len) +{ +	u8 *p = (u8 *)hdr; +	u8 csum = 0; +	int i; + +	for (i = 0; i < hdr_len; i++) +		csum += p[i]; + +	p = (u8 *)sdu; +	for (i = 0; i < sdu_len; i++) +		csum += p[i]; + +	return ~csum & 0xff; +} + +static void cdx_mcdi_send_request(struct cdx_mcdi *cdx, +				  struct cdx_mcdi_cmd *cmd) +{ +	struct cdx_mcdi_iface *mcdi = cdx_mcdi_if(cdx); +	const struct cdx_dword *inbuf = cmd->inbuf; +	size_t inlen = cmd->inlen; +	struct cdx_dword hdr[2]; +	size_t hdr_len; +	bool not_epoch; +	u32 xflags; +#ifdef CONFIG_MCDI_LOGGING +	char *buf; +#endif + +	if (!mcdi) +		return; +#ifdef CONFIG_MCDI_LOGGING +	buf = mcdi->logging_buffer; /* page-sized */ +#endif + +	mcdi->prev_seq = cmd->seq; +	mcdi->seq_held_by[cmd->seq] = cmd; +	mcdi->db_held_by = cmd; +	cmd->started = jiffies; + +	not_epoch = !mcdi->new_epoch; +	xflags = 0; + +	/* MCDI v2 */ +	WARN_ON(inlen > MCDI_CTL_SDU_LEN_MAX_V2); +	CDX_POPULATE_DWORD_7(hdr[0], +			     MCDI_HEADER_RESPONSE, 0, +			     MCDI_HEADER_RESYNC, 1, +			     MCDI_HEADER_CODE, MC_CMD_V2_EXTN, +			     MCDI_HEADER_DATALEN, 0, +			     MCDI_HEADER_SEQ, cmd->seq, +			     MCDI_HEADER_XFLAGS, xflags, +			     MCDI_HEADER_NOT_EPOCH, not_epoch); +	CDX_POPULATE_DWORD_3(hdr[1], +			     MC_CMD_V2_EXTN_IN_EXTENDED_CMD, cmd->cmd, +			     MC_CMD_V2_EXTN_IN_ACTUAL_LEN, inlen, +			     MC_CMD_V2_EXTN_IN_MESSAGE_TYPE, +			     MC_CMD_V2_EXTN_IN_MCDI_MESSAGE_TYPE_PLATFORM); +	hdr_len = 8; + +#ifdef CONFIG_MCDI_LOGGING +	if (!WARN_ON_ONCE(!buf)) { +		const struct cdx_dword *frags[] = { hdr, inbuf }; +		const size_t frag_len[] = { hdr_len, round_up(inlen, 4) }; +		int bytes = 0; +		int i, j; + +		for (j = 0; j < ARRAY_SIZE(frags); j++) { +			const struct cdx_dword *frag; + +			frag = frags[j]; +			for (i = 0; +			     i < frag_len[j] / 4; +			     i++) { +				/* +				 * Do not exceed the internal printk limit. +				 * The string before that is just over 70 bytes. +				 */ +				if ((bytes + 75) > LOG_LINE_MAX) { +					pr_info("MCDI RPC REQ:%s \\\n", buf); +					bytes = 0; +				} +				bytes += snprintf(buf + bytes, +						  LOG_LINE_MAX - bytes, " %08x", +						  le32_to_cpu(frag[i].cdx_u32)); +			} +		} + +		pr_info("MCDI RPC REQ:%s\n", buf); +	} +#endif +	hdr[0].cdx_u32 |= (__force __le32)(cdx_mcdi_payload_csum(hdr, hdr_len, inbuf, inlen) << +			 MCDI_HEADER_XFLAGS_LBN); +	cdx->mcdi_ops->mcdi_request(cdx, hdr, hdr_len, inbuf, inlen); + +	mcdi->new_epoch = false; +} + +static int cdx_mcdi_errno(struct cdx_mcdi *cdx, unsigned int mcdi_err) +{ +	switch (mcdi_err) { +	case 0: +	case MC_CMD_ERR_QUEUE_FULL: +		return mcdi_err; +	case MC_CMD_ERR_EPERM: +		return -EPERM; +	case MC_CMD_ERR_ENOENT: +		return -ENOENT; +	case MC_CMD_ERR_EINTR: +		return -EINTR; +	case MC_CMD_ERR_EAGAIN: +		return -EAGAIN; +	case MC_CMD_ERR_EACCES: +		return -EACCES; +	case MC_CMD_ERR_EBUSY: +		return -EBUSY; +	case MC_CMD_ERR_EINVAL: +		return -EINVAL; +	case MC_CMD_ERR_ERANGE: +		return -ERANGE; +	case MC_CMD_ERR_EDEADLK: +		return -EDEADLK; +	case MC_CMD_ERR_ENOSYS: +		return -EOPNOTSUPP; +	case MC_CMD_ERR_ETIME: +		return -ETIME; +	case MC_CMD_ERR_EALREADY: +		return -EALREADY; +	case MC_CMD_ERR_ENOSPC: +		return -ENOSPC; +	case MC_CMD_ERR_ENOMEM: +		return -ENOMEM; +	case MC_CMD_ERR_ENOTSUP: +		return -EOPNOTSUPP; +	case MC_CMD_ERR_ALLOC_FAIL: +		return -ENOBUFS; +	case MC_CMD_ERR_MAC_EXIST: +		return -EADDRINUSE; +	case MC_CMD_ERR_NO_EVB_PORT: +		return -EAGAIN; +	default: +		return -EPROTO; +	} +} + +static void cdx_mcdi_process_cleanup_list(struct cdx_mcdi *cdx, +					  struct list_head *cleanup_list) +{ +	struct cdx_mcdi_iface *mcdi = cdx_mcdi_if(cdx); +	unsigned int cleanups = 0; + +	if (!mcdi) +		return; + +	while (!list_empty(cleanup_list)) { +		struct cdx_mcdi_cmd *cmd = +			list_first_entry(cleanup_list, +					 struct cdx_mcdi_cmd, cleanup_list); +		cmd->completer(cdx, cmd->cookie, cmd->rc, +			       cmd->outbuf, cmd->outlen); +		list_del(&cmd->cleanup_list); +		kref_put(&cmd->ref, cdx_mcdi_cmd_release); +		++cleanups; +	} + +	if (cleanups) { +		bool all_done; + +		mutex_lock(&mcdi->iface_lock); +		CDX_WARN_ON_PARANOID(cleanups > mcdi->outstanding_cleanups); +		all_done = (mcdi->outstanding_cleanups -= cleanups) == 0; +		mutex_unlock(&mcdi->iface_lock); +		if (all_done) +			wake_up(&mcdi->cmd_complete_wq); +	} +} + +static void _cdx_mcdi_cancel_cmd(struct cdx_mcdi_iface *mcdi, +				 unsigned int handle, +				 struct list_head *cleanup_list) +{ +	struct cdx_mcdi_cmd *cmd; + +	list_for_each_entry(cmd, &mcdi->cmd_list, list) +		if (cdx_mcdi_cmd_handle(cmd) == handle) { +			switch (cmd->state) { +			case MCDI_STATE_QUEUED: +			case MCDI_STATE_RETRY: +				pr_debug("command %#x inlen %zu cancelled in queue\n", +					 cmd->cmd, cmd->inlen); +				/* if not yet running, properly cancel it */ +				cmd->rc = -EPIPE; +				cdx_mcdi_remove_cmd(mcdi, cmd, cleanup_list); +				break; +			case MCDI_STATE_RUNNING: +			case MCDI_STATE_RUNNING_CANCELLED: +			case MCDI_STATE_FINISHED: +			default: +				/* invalid state? */ +				WARN_ON(1); +			} +			break; +		} +} + +static void cdx_mcdi_cancel_cmd(struct cdx_mcdi *cdx, struct cdx_mcdi_cmd *cmd) +{ +	struct cdx_mcdi_iface *mcdi = cdx_mcdi_if(cdx); +	LIST_HEAD(cleanup_list); + +	if (!mcdi) +		return; + +	mutex_lock(&mcdi->iface_lock); +	cdx_mcdi_timeout_cmd(mcdi, cmd, &cleanup_list); +	mutex_unlock(&mcdi->iface_lock); +	cdx_mcdi_process_cleanup_list(cdx, &cleanup_list); +} + +struct cdx_mcdi_blocking_data { +	struct kref ref; +	bool done; +	wait_queue_head_t wq; +	int rc; +	struct cdx_dword *outbuf; +	size_t outlen; +	size_t outlen_actual; +}; + +static void cdx_mcdi_blocking_data_release(struct kref *ref) +{ +	kfree(container_of(ref, struct cdx_mcdi_blocking_data, ref)); +} + +static void cdx_mcdi_rpc_completer(struct cdx_mcdi *cdx, unsigned long cookie, +				   int rc, struct cdx_dword *outbuf, +				   size_t outlen_actual) +{ +	struct cdx_mcdi_blocking_data *wait_data = +		(struct cdx_mcdi_blocking_data *)cookie; + +	wait_data->rc = rc; +	memcpy(wait_data->outbuf, outbuf, +	       min(outlen_actual, wait_data->outlen)); +	wait_data->outlen_actual = outlen_actual; +	/* memory barrier */ +	smp_wmb(); +	wait_data->done = true; +	wake_up(&wait_data->wq); +	kref_put(&wait_data->ref, cdx_mcdi_blocking_data_release); +} + +static int cdx_mcdi_rpc_sync(struct cdx_mcdi *cdx, unsigned int cmd, +			     const struct cdx_dword *inbuf, size_t inlen, +			     struct cdx_dword *outbuf, size_t outlen, +			     size_t *outlen_actual, bool quiet) +{ +	struct cdx_mcdi_blocking_data *wait_data; +	struct cdx_mcdi_cmd *cmd_item; +	unsigned int handle; +	int rc; + +	if (outlen_actual) +		*outlen_actual = 0; + +	wait_data = kmalloc(sizeof(*wait_data), GFP_KERNEL); +	if (!wait_data) +		return -ENOMEM; + +	cmd_item = kmalloc(sizeof(*cmd_item), GFP_KERNEL); +	if (!cmd_item) { +		kfree(wait_data); +		return -ENOMEM; +	} + +	kref_init(&wait_data->ref); +	wait_data->done = false; +	init_waitqueue_head(&wait_data->wq); +	wait_data->outbuf = outbuf; +	wait_data->outlen = outlen; + +	kref_init(&cmd_item->ref); +	cmd_item->quiet = quiet; +	cmd_item->cookie = (unsigned long)wait_data; +	cmd_item->completer = &cdx_mcdi_rpc_completer; +	cmd_item->cmd = cmd; +	cmd_item->inlen = inlen; +	cmd_item->inbuf = inbuf; + +	/* Claim an extra reference for the completer to put. */ +	kref_get(&wait_data->ref); +	rc = cdx_mcdi_rpc_async_internal(cdx, cmd_item, &handle); +	if (rc) { +		kref_put(&wait_data->ref, cdx_mcdi_blocking_data_release); +		goto out; +	} + +	if (!wait_event_timeout(wait_data->wq, wait_data->done, +				cdx_mcdi_rpc_timeout(cdx, cmd)) && +	    !wait_data->done) { +		pr_err("MC command 0x%x inlen %zu timed out (sync)\n", +		       cmd, inlen); + +		cdx_mcdi_cancel_cmd(cdx, cmd_item); + +		wait_data->rc = -ETIMEDOUT; +		wait_data->outlen_actual = 0; +	} + +	if (outlen_actual) +		*outlen_actual = wait_data->outlen_actual; +	rc = wait_data->rc; + +out: +	kref_put(&wait_data->ref, cdx_mcdi_blocking_data_release); + +	return rc; +} + +static bool cdx_mcdi_get_seq(struct cdx_mcdi_iface *mcdi, unsigned char *seq) +{ +	*seq = mcdi->prev_seq; +	do { +		*seq = (*seq + 1) % ARRAY_SIZE(mcdi->seq_held_by); +	} while (mcdi->seq_held_by[*seq] && *seq != mcdi->prev_seq); +	return !mcdi->seq_held_by[*seq]; +} + +static int cdx_mcdi_rpc_async_internal(struct cdx_mcdi *cdx, +				       struct cdx_mcdi_cmd *cmd, +				       unsigned int *handle) +{ +	struct cdx_mcdi_iface *mcdi = cdx_mcdi_if(cdx); +	LIST_HEAD(cleanup_list); + +	if (!mcdi) { +		kref_put(&cmd->ref, cdx_mcdi_cmd_release); +		return -ENETDOWN; +	} + +	if (mcdi->mode == MCDI_MODE_FAIL) { +		kref_put(&cmd->ref, cdx_mcdi_cmd_release); +		return -ENETDOWN; +	} + +	cmd->mcdi = mcdi; +	INIT_WORK(&cmd->work, cdx_mcdi_cmd_work); +	INIT_LIST_HEAD(&cmd->list); +	INIT_LIST_HEAD(&cmd->cleanup_list); +	cmd->rc = 0; +	cmd->outbuf = NULL; +	cmd->outlen = 0; + +	queue_work(mcdi->workqueue, &cmd->work); +	return 0; +} + +static void cdx_mcdi_cmd_start_or_queue(struct cdx_mcdi_iface *mcdi, +					struct cdx_mcdi_cmd *cmd) +{ +	struct cdx_mcdi *cdx = mcdi->cdx; +	u8 seq; + +	if (!mcdi->db_held_by && +	    cdx_mcdi_get_seq(mcdi, &seq)) { +		cmd->seq = seq; +		cmd->reboot_seen = false; +		cdx_mcdi_send_request(cdx, cmd); +		cmd->state = MCDI_STATE_RUNNING; +	} else { +		cmd->state = MCDI_STATE_QUEUED; +	} +} + +/* try to advance other commands */ +static void cdx_mcdi_start_or_queue(struct cdx_mcdi_iface *mcdi, +				    bool allow_retry) +{ +	struct cdx_mcdi_cmd *cmd, *tmp; + +	list_for_each_entry_safe(cmd, tmp, &mcdi->cmd_list, list) +		if (cmd->state == MCDI_STATE_QUEUED || +		    (cmd->state == MCDI_STATE_RETRY && allow_retry)) +			cdx_mcdi_cmd_start_or_queue(mcdi, cmd); +} + +void cdx_mcdi_process_cmd(struct cdx_mcdi *cdx, struct cdx_dword *outbuf, int len) +{ +	struct cdx_mcdi_iface *mcdi; +	struct cdx_mcdi_cmd *cmd; +	LIST_HEAD(cleanup_list); +	unsigned int respseq; + +	if (!len || !outbuf) { +		pr_err("Got empty MC response\n"); +		return; +	} + +	mcdi = cdx_mcdi_if(cdx); +	if (!mcdi) +		return; + +	respseq = CDX_DWORD_FIELD(outbuf[0], MCDI_HEADER_SEQ); + +	mutex_lock(&mcdi->iface_lock); +	cmd = mcdi->seq_held_by[respseq]; + +	if (cmd) { +		if (cmd->state == MCDI_STATE_FINISHED) { +			mutex_unlock(&mcdi->iface_lock); +			kref_put(&cmd->ref, cdx_mcdi_cmd_release); +			return; +		} + +		cdx_mcdi_complete_cmd(mcdi, cmd, outbuf, len, &cleanup_list); +	} else { +		pr_err("MC response unexpected for seq : %0X\n", respseq); +	} + +	mutex_unlock(&mcdi->iface_lock); + +	cdx_mcdi_process_cleanup_list(mcdi->cdx, &cleanup_list); +} + +static void cdx_mcdi_cmd_work(struct work_struct *context) +{ +	struct cdx_mcdi_cmd *cmd = +		container_of(context, struct cdx_mcdi_cmd, work); +	struct cdx_mcdi_iface *mcdi = cmd->mcdi; + +	mutex_lock(&mcdi->iface_lock); + +	cmd->handle = mcdi->prev_handle++; +	list_add_tail(&cmd->list, &mcdi->cmd_list); +	cdx_mcdi_cmd_start_or_queue(mcdi, cmd); + +	mutex_unlock(&mcdi->iface_lock); +} + +/* + * Returns true if the MCDI module is finished with the command. + * (examples of false would be if the command was proxied, or it was + * rejected by the MC due to lack of resources and requeued). + */ +static bool cdx_mcdi_complete_cmd(struct cdx_mcdi_iface *mcdi, +				  struct cdx_mcdi_cmd *cmd, +				  struct cdx_dword *outbuf, +				  int len, +				  struct list_head *cleanup_list) +{ +	size_t resp_hdr_len, resp_data_len; +	struct cdx_mcdi *cdx = mcdi->cdx; +	unsigned int respcmd, error; +	bool completed = false; +	int rc; + +	/* ensure the command can't go away before this function returns */ +	kref_get(&cmd->ref); + +	respcmd = CDX_DWORD_FIELD(outbuf[0], MCDI_HEADER_CODE); +	error = CDX_DWORD_FIELD(outbuf[0], MCDI_HEADER_ERROR); + +	if (respcmd != MC_CMD_V2_EXTN) { +		resp_hdr_len = 4; +		resp_data_len = CDX_DWORD_FIELD(outbuf[0], MCDI_HEADER_DATALEN); +	} else { +		resp_data_len = 0; +		resp_hdr_len = 8; +		if (len >= 8) +			resp_data_len = +				CDX_DWORD_FIELD(outbuf[1], MC_CMD_V2_EXTN_IN_ACTUAL_LEN); +	} + +	if ((resp_hdr_len + resp_data_len) > len) { +		pr_warn("Incomplete MCDI response received %d. Expected %zu\n", +			len, (resp_hdr_len + resp_data_len)); +		resp_data_len = 0; +	} + +#ifdef CONFIG_MCDI_LOGGING +	if (!WARN_ON_ONCE(!mcdi->logging_buffer)) { +		char *log = mcdi->logging_buffer; +		int i, bytes = 0; +		size_t rlen; + +		WARN_ON_ONCE(resp_hdr_len % 4); + +		rlen = resp_hdr_len / 4 + DIV_ROUND_UP(resp_data_len, 4); + +		for (i = 0; i < rlen; i++) { +			if ((bytes + 75) > LOG_LINE_MAX) { +				pr_info("MCDI RPC RESP:%s \\\n", log); +				bytes = 0; +			} +			bytes += snprintf(log + bytes, LOG_LINE_MAX - bytes, +					  " %08x", le32_to_cpu(outbuf[i].cdx_u32)); +		} + +		pr_info("MCDI RPC RESP:%s\n", log); +	} +#endif + +	if (error && resp_data_len == 0) { +		/* MC rebooted during command */ +		rc = -EIO; +	} else { +		if (WARN_ON_ONCE(error && resp_data_len < 4)) +			resp_data_len = 4; +		if (error) { +			rc = CDX_DWORD_FIELD(outbuf[resp_hdr_len / 4], CDX_DWORD); +			if (!cmd->quiet) { +				int err_arg = 0; + +				if (resp_data_len >= MC_CMD_ERR_ARG_OFST + 4) { +					int offset = (resp_hdr_len + MC_CMD_ERR_ARG_OFST) / 4; + +					err_arg = CDX_DWORD_VAL(outbuf[offset]); +				} + +				_cdx_mcdi_display_error(cdx, cmd->cmd, +							cmd->inlen, rc, err_arg, +							cdx_mcdi_errno(cdx, rc)); +			} +			rc = cdx_mcdi_errno(cdx, rc); +		} else { +			rc = 0; +		} +	} + +	/* free doorbell */ +	if (mcdi->db_held_by == cmd) +		mcdi->db_held_by = NULL; + +	if (cdx_cmd_cancelled(cmd)) { +		list_del(&cmd->list); +		kref_put(&cmd->ref, cdx_mcdi_cmd_release); +		completed = true; +	} else if (rc == MC_CMD_ERR_QUEUE_FULL) { +		cmd->state = MCDI_STATE_RETRY; +	} else { +		cmd->rc = rc; +		cmd->outbuf = outbuf + DIV_ROUND_UP(resp_hdr_len, 4); +		cmd->outlen = resp_data_len; +		cdx_mcdi_remove_cmd(mcdi, cmd, cleanup_list); +		completed = true; +	} + +	/* free sequence number and buffer */ +	mcdi->seq_held_by[cmd->seq] = NULL; + +	cdx_mcdi_start_or_queue(mcdi, rc != MC_CMD_ERR_QUEUE_FULL); + +	/* wake up anyone waiting for flush */ +	wake_up(&mcdi->cmd_complete_wq); + +	kref_put(&cmd->ref, cdx_mcdi_cmd_release); + +	return completed; +} + +static void cdx_mcdi_timeout_cmd(struct cdx_mcdi_iface *mcdi, +				 struct cdx_mcdi_cmd *cmd, +				 struct list_head *cleanup_list) +{ +	struct cdx_mcdi *cdx = mcdi->cdx; + +	pr_err("MC command 0x%x inlen %zu state %d timed out after %u ms\n", +	       cmd->cmd, cmd->inlen, cmd->state, +	       jiffies_to_msecs(jiffies - cmd->started)); + +	cmd->rc = -ETIMEDOUT; +	cdx_mcdi_remove_cmd(mcdi, cmd, cleanup_list); + +	cdx_mcdi_mode_fail(cdx, cleanup_list); +} + +/** + * cdx_mcdi_rpc - Issue an MCDI command and wait for completion + * @cdx: NIC through which to issue the command + * @cmd: Command type number + * @inbuf: Command parameters + * @inlen: Length of command parameters, in bytes. Must be a multiple + *	of 4 and no greater than %MCDI_CTL_SDU_LEN_MAX_V1. + * @outbuf: Response buffer. May be %NULL if @outlen is 0. + * @outlen: Length of response buffer, in bytes. If the actual + *	response is longer than @outlen & ~3, it will be truncated + *	to that length. + * @outlen_actual: Pointer through which to return the actual response + *	length. May be %NULL if this is not needed. + * + * This function may sleep and therefore must be called in process + * context. + * + * Return: A negative error code, or zero if successful. The error + *	code may come from the MCDI response or may indicate a failure + *	to communicate with the MC. In the former case, the response + *	will still be copied to @outbuf and *@outlen_actual will be + *	set accordingly. In the latter case, *@outlen_actual will be + *	set to zero. + */ +int cdx_mcdi_rpc(struct cdx_mcdi *cdx, unsigned int cmd, +		 const struct cdx_dword *inbuf, size_t inlen, +		 struct cdx_dword *outbuf, size_t outlen, +		 size_t *outlen_actual) +{ +	return cdx_mcdi_rpc_sync(cdx, cmd, inbuf, inlen, outbuf, outlen, +				 outlen_actual, false); +} + +/** + * cdx_mcdi_rpc_async - Schedule an MCDI command to run asynchronously + * @cdx: NIC through which to issue the command + * @cmd: Command type number + * @inbuf: Command parameters + * @inlen: Length of command parameters, in bytes + * @complete: Function to be called on completion or cancellation. + * @cookie: Arbitrary value to be passed to @complete. + * + * This function does not sleep and therefore may be called in atomic + * context.  It will fail if event queues are disabled or if MCDI + * event completions have been disabled due to an error. + * + * If it succeeds, the @complete function will be called exactly once + * in process context, when one of the following occurs: + * (a) the completion event is received (in process context) + * (b) event queues are disabled (in the process that disables them) + */ +int +cdx_mcdi_rpc_async(struct cdx_mcdi *cdx, unsigned int cmd, +		   const struct cdx_dword *inbuf, size_t inlen, +		   cdx_mcdi_async_completer *complete, unsigned long cookie) +{ +	struct cdx_mcdi_cmd *cmd_item = +		kmalloc(sizeof(struct cdx_mcdi_cmd) + inlen, GFP_ATOMIC); + +	if (!cmd_item) +		return -ENOMEM; + +	kref_init(&cmd_item->ref); +	cmd_item->quiet = true; +	cmd_item->cookie = cookie; +	cmd_item->completer = complete; +	cmd_item->cmd = cmd; +	cmd_item->inlen = inlen; +	/* inbuf is probably not valid after return, so take a copy */ +	cmd_item->inbuf = (struct cdx_dword *)(cmd_item + 1); +	memcpy(cmd_item + 1, inbuf, inlen); + +	return cdx_mcdi_rpc_async_internal(cdx, cmd_item, NULL); +} + +static void _cdx_mcdi_display_error(struct cdx_mcdi *cdx, unsigned int cmd, +				    size_t inlen, int raw, int arg, int err_no) +{ +	pr_err("MC command 0x%x inlen %d failed err_no=%d (raw=%d) arg=%d\n", +	       cmd, (int)inlen, err_no, raw, arg); +} + +/* + * Set MCDI mode to fail to prevent any new commands, then cancel any + * outstanding commands. + * Caller must hold the mcdi iface_lock. + */ +static void cdx_mcdi_mode_fail(struct cdx_mcdi *cdx, struct list_head *cleanup_list) +{ +	struct cdx_mcdi_iface *mcdi = cdx_mcdi_if(cdx); + +	if (!mcdi) +		return; + +	mcdi->mode = MCDI_MODE_FAIL; + +	while (!list_empty(&mcdi->cmd_list)) { +		struct cdx_mcdi_cmd *cmd; + +		cmd = list_first_entry(&mcdi->cmd_list, struct cdx_mcdi_cmd, +				       list); +		_cdx_mcdi_cancel_cmd(mcdi, cdx_mcdi_cmd_handle(cmd), cleanup_list); +	} +}  |