| // 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)]; |
| }; |
| |
| 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; |
| |
| mcdi->workqueue = alloc_ordered_workqueue("mcdi_wq", 0); |
| if (!mcdi->workqueue) |
| goto fail2; |
| 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; |
| fail2: |
| 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); |
| |
| 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; |
| |
| if (!mcdi) |
| return; |
| |
| 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; |
| |
| hdr[0].cdx_u32 |= (__force __le32)(cdx_mcdi_payload_csum(hdr, hdr_len, inbuf, inlen) << |
| MCDI_HEADER_XFLAGS_LBN); |
| |
| print_hex_dump_debug("MCDI REQ HEADER: ", DUMP_PREFIX_NONE, 32, 4, hdr, hdr_len, false); |
| print_hex_dump_debug("MCDI REQ PAYLOAD: ", DUMP_PREFIX_NONE, 32, 4, inbuf, inlen, false); |
| |
| 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; |
| } |
| |
| print_hex_dump_debug("MCDI RESP HEADER: ", DUMP_PREFIX_NONE, 32, 4, |
| outbuf, resp_hdr_len, false); |
| print_hex_dump_debug("MCDI RESP PAYLOAD: ", DUMP_PREFIX_NONE, 32, 4, |
| outbuf + (resp_hdr_len / 4), resp_data_len, false); |
| |
| 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); |
| } |
| } |