// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (c) 2017-2026 Morse Micro */ #include #include #include #include #include "command.h" #include "mac.h" #include "ps.h" #include "hif.h" #define MM_MAX_COMMAND_RETRY 2 #define HOST_CMD_DEFAULT_TIMEOUT_MS 600 #define HOST_CMD_POWERSAVE_TIMEOUT_MS 2000 #define INIT_CMD_HDR(_req, _cmd, _vif_id) \ ((struct host_cmd_header){ \ .message_id = cpu_to_le16(_cmd), \ .len = cpu_to_le16(sizeof(_req) - sizeof((_req).hdr)), \ .vif_id = cpu_to_le16(_vif_id), \ }) struct host_cmd_resp_cb { int ret; u32 length; struct host_cmd_resp *dest_resp; }; static int mm81x_cmd_tx(struct mm81x *mors, struct host_cmd_resp *resp, struct host_cmd_req *req, u32 length, u32 timeout) { int cmd_len; int ret = 0; u16 host_id; int retry = 0; unsigned long wait_ret = 0; struct sk_buff *skb; struct mm81x_skbq *cmd_q = mm81x_hif_get_tx_cmd_queue(mors); struct host_cmd_resp_cb *resp_cb; DECLARE_COMPLETION_ONSTACK(cmd_comp); BUILD_BUG_ON(sizeof(struct host_cmd_resp_cb) > IEEE80211_TX_INFO_DRIVER_DATA_SIZE); cmd_len = sizeof(*req) + le16_to_cpu(req->hdr.len); req->hdr.flags = cpu_to_le16(HOST_CMD_TYPE_REQ); mutex_lock(&mors->cmd_wait); mors->cmd_seq++; if (mors->cmd_seq > HOST_CMD_HOST_ID_SEQ_MAX) mors->cmd_seq = 1; host_id = mors->cmd_seq << HOST_CMD_HOST_ID_SEQ_SHIFT; mm81x_ps_disable(mors); do { req->hdr.host_id = cpu_to_le16(host_id | retry); skb = mm81x_skbq_alloc_skb(cmd_q, cmd_len); if (!skb) { ret = -ENOMEM; break; } memcpy(skb->data, req, cmd_len); resp_cb = (struct host_cmd_resp_cb *)IEEE80211_SKB_CB(skb) ->driver_data; resp_cb->length = length; resp_cb->dest_resp = resp; dev_dbg(mors->dev, "CMD 0x%04x:%04x", le16_to_cpu(req->hdr.message_id), le16_to_cpu(req->hdr.host_id)); mutex_lock(&mors->cmd_lock); mors->cmd_comp = &cmd_comp; if (retry > 0) reinit_completion(&cmd_comp); timeout = timeout ? timeout : HOST_CMD_DEFAULT_TIMEOUT_MS; ret = mm81x_skbq_skb_tx(cmd_q, &skb, NULL, MM81X_SKB_CHAN_COMMAND); mutex_unlock(&mors->cmd_lock); if (ret) { dev_err(mors->dev, "mm81x_skbq_tx fail: %d", ret); break; } wait_ret = wait_for_completion_timeout( &cmd_comp, msecs_to_jiffies(timeout)); mutex_lock(&mors->cmd_lock); mors->cmd_comp = NULL; if (!wait_ret) { dev_err(mors->dev, "Try:%d Command %04x:%04x timeout after %u ms", retry, le16_to_cpu(req->hdr.message_id), le16_to_cpu(req->hdr.host_id), timeout); ret = -ETIMEDOUT; } else { ret = (length && resp) ? le32_to_cpu(resp->status) : resp_cb->ret; if (ret > 0 || ret < -MAX_ERRNO) ret = -EIO; dev_dbg(mors->dev, "Command 0x%04x:%04x status 0x%08x", le16_to_cpu(req->hdr.message_id), le16_to_cpu(req->hdr.host_id), ret); if (ret) { dev_err(mors->dev, "Command 0x%04x:%04x error %d", le16_to_cpu(req->hdr.message_id), le16_to_cpu(req->hdr.host_id), ret); } } /* Free the command request */ spin_lock_bh(&cmd_q->lock); mm81x_skbq_skb_finish(cmd_q, skb, NULL); spin_unlock_bh(&cmd_q->lock); mutex_unlock(&mors->cmd_lock); retry++; } while ((ret == -ETIMEDOUT) && retry < MM_MAX_COMMAND_RETRY); mm81x_ps_enable(mors); mutex_unlock(&mors->cmd_wait); if (ret == -ETIMEDOUT) { dev_err(mors->dev, "Command %02x:%02x timed out", le16_to_cpu(req->hdr.message_id), le16_to_cpu(req->hdr.host_id)); } else if (ret != 0) { dev_err(mors->dev, "Command %02x:%02x failed with rc %d (0x%x)\n", le16_to_cpu(req->hdr.message_id), le16_to_cpu(req->hdr.host_id), ret, ret); } return ret; } int mm81x_cmd_resp_process(struct mm81x *mors, struct sk_buff *skb) { int length, ret = -ESRCH; /* No such process */ struct mm81x_skbq *cmd_q = mm81x_hif_get_tx_cmd_queue(mors); struct host_cmd_resp *src_resp = (struct host_cmd_resp *)(skb->data); struct sk_buff *cmd_skb = NULL; struct host_cmd_resp_cb *resp_cb; struct host_cmd_resp *dest_resp; struct host_cmd_req *req; u16 message_id = 0; u16 host_id = 0; u16 resp_message_id = le16_to_cpu(src_resp->hdr.message_id); u16 resp_host_id = le16_to_cpu(src_resp->hdr.host_id); bool is_late_response = false; dev_dbg(mors->dev, "EVT 0x%04x:0x%04x", resp_message_id, resp_host_id); if (!HOST_CMD_IS_RESP(src_resp)) { ret = mm81x_mac_event_recv(mors, skb); goto exit_free; } mutex_lock(&mors->cmd_lock); cmd_skb = mm81x_skbq_tx_pending(cmd_q); if (cmd_skb) { mm81x_skbq_pull_hdr_post_tx(cmd_skb); req = (struct host_cmd_req *)cmd_skb->data; message_id = le16_to_cpu(req->hdr.message_id); host_id = le16_to_cpu(req->hdr.host_id); } /* * If there is no pending command or the sequence ID does not match, * this is a late response for a timed out command which has been * cleaned up, so just free up the response. If a command was retried, * the response may be from the retry or from the original command * (late response) but not from both because the firmware will silently * drop a retry if it received the initial request. So a mismatched * retry counter is treated as a matched command and response. */ if (!cmd_skb || message_id != resp_message_id || (host_id & HOST_CMD_HOST_ID_SEQ_MASK) != (resp_host_id & HOST_CMD_HOST_ID_SEQ_MASK)) { dev_err(mors->dev, "Late response for timed out req 0x%04x:%04x have 0x%04x:%04x 0x%04x", resp_message_id, resp_host_id, message_id, host_id, mors->cmd_seq); is_late_response = true; goto exit; } if ((host_id & HOST_CMD_HOST_ID_RETRY_MASK) != (resp_host_id & HOST_CMD_HOST_ID_RETRY_MASK)) dev_dbg(mors->dev, "Command retry mismatch 0x%04x:%04x 0x%04x:%04x", message_id, host_id, resp_message_id, resp_host_id); resp_cb = (struct host_cmd_resp_cb *)IEEE80211_SKB_CB(cmd_skb) ->driver_data; length = resp_cb->length; dest_resp = resp_cb->dest_resp; if (length >= sizeof(struct host_cmd_resp) && dest_resp) { ret = 0; length = min_t(int, length, le16_to_cpu(src_resp->hdr.len) + sizeof(struct host_cmd_header)); memcpy(dest_resp, src_resp, length); } else { ret = le32_to_cpu(src_resp->status); } resp_cb->ret = ret; exit: if (cmd_skb && !is_late_response) { /* Complete if not already timed out */ if (mors->cmd_comp) complete(mors->cmd_comp); } mutex_unlock(&mors->cmd_lock); exit_free: dev_kfree_skb(skb); return 0; } int mm81x_cmd_sta_state(struct mm81x *mors, struct mm81x_vif *mors_vif, u16 aid, struct ieee80211_sta *sta, enum ieee80211_sta_state state) { struct host_cmd_req_set_sta_state req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_SET_STA_STATE, mors_vif->id), .aid = cpu_to_le16(aid), .state = cpu_to_le16(state), .uapsd_queues = sta->uapsd_queues, }; memcpy(req.sta_addr, sta->addr, sizeof(req.sta_addr)); return mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)&req, 0, 0); } int mm81x_cmd_add_if(struct mm81x *mors, u16 *vif_id, const u8 *addr, enum nl80211_iftype type) { int ret; struct host_cmd_req_add_interface req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_ADD_INTERFACE, 0), }; struct host_cmd_resp_add_interface resp; switch (type) { case NL80211_IFTYPE_STATION: req.interface_type = cpu_to_le32(HOST_CMD_INTERFACE_TYPE_STA); break; case NL80211_IFTYPE_AP: req.interface_type = cpu_to_le32(HOST_CMD_INTERFACE_TYPE_AP); break; default: return -EOPNOTSUPP; } memcpy(req.addr.octet, addr, sizeof(req.addr.octet)); ret = mm81x_cmd_tx(mors, (struct host_cmd_resp *)&resp, (struct host_cmd_req *)&req, sizeof(resp), 0); if (!ret) *vif_id = le16_to_cpu(resp.hdr.vif_id); return ret; } int mm81x_cmd_get_capabilities(struct mm81x *mors, u16 vif_id, struct mm81x_fw_caps *capabilities) { int ret; int i; struct host_cmd_req_get_capabilities req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_GET_CAPABILITIES, vif_id), }; struct host_cmd_resp_get_capabilities rsp; ret = mm81x_cmd_tx(mors, (struct host_cmd_resp *)&rsp, (struct host_cmd_req *)&req, sizeof(rsp), 0); if (ret) return ret; capabilities->ampdu_mss = rsp.capabilities.ampdu_mss; capabilities->mm81x_mmss_offset = rsp.morse_mmss_offset; capabilities->beamformee_sts_capability = rsp.capabilities.beamformee_sts_capability; capabilities->maximum_ampdu_length_exponent = rsp.capabilities.maximum_ampdu_length_exponent; capabilities->number_sounding_dimensions = rsp.capabilities.number_sounding_dimensions; for (i = 0; i < FW_CAPABILITIES_FLAGS_WIDTH; i++) capabilities->flags[i] = le32_to_cpu(rsp.capabilities.flags[i]); return ret; } int mm81x_cmd_get_max_txpower(struct mm81x *mors, s32 *out_power_mbm) { int ret; struct host_cmd_req_get_max_txpower req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_GET_MAX_TXPOWER, 0), }; struct host_cmd_resp_get_max_txpower resp; ret = mm81x_cmd_tx(mors, (struct host_cmd_resp *)&resp, (struct host_cmd_req *)&req, sizeof(resp), 0); if (!ret) *out_power_mbm = QDBM_TO_MBM(le32_to_cpu(resp.power_qdbm)); return ret; } int mm81x_cmd_hw_scan(struct mm81x *mors, struct mm81x_hw_scan_params *params, bool store) { int ret; struct host_cmd_req_hw_scan *req; size_t cmd_size; u8 *buf; u32 flags = 0; cmd_size = mm81x_hw_scan_h_get_cmd_size(params); cmd_size = ROUND_BYTES_TO_WORD(cmd_size); req = kzalloc(cmd_size, GFP_KERNEL); if (!req) return -ENOMEM; buf = req->variable; if (store) flags = HOST_CMD_HW_SCAN_FLAGS_STORE; else if (params->operation == MM81X_HW_SCAN_OP_START) flags |= HOST_CMD_HW_SCAN_FLAGS_START; else if (params->operation == MM81X_HW_SCAN_OP_STOP) flags |= HOST_CMD_HW_SCAN_FLAGS_ABORT; flags |= HOST_CMD_HW_SCAN_FLAGS_1MHZ_PROBES; if (params->operation == MM81X_HW_SCAN_OP_START) { req->dwell_time_ms = cpu_to_le32(params->dwell_time_ms); buf = mm81x_hw_scan_h_insert_tlvs(params, buf); } req->flags = cpu_to_le32(flags); req->hdr = INIT_CMD_HDR((*req), HOST_CMD_ID_HW_SCAN, 0); req->hdr.len = cpu_to_le16((u16)((buf - (u8 *)req) - sizeof(req->hdr))); ret = mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)req, 0, 0); kfree(req); return ret; } int mm81x_cmd_set_txpower(struct mm81x *mors, s32 *out_power_mbm, int txpower_mbm) { int ret; struct host_cmd_req_set_txpower req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_SET_TXPOWER, 0), .power_qdbm = cpu_to_le32(MBM_TO_QDBM(txpower_mbm)), }; struct host_cmd_resp_set_txpower resp; ret = mm81x_cmd_tx(mors, (struct host_cmd_resp *)&resp, (struct host_cmd_req *)&req, sizeof(resp), 0); if (!ret) *out_power_mbm = QDBM_TO_MBM(le32_to_cpu(resp.power_qdbm)); return ret; } int mm81x_cmd_set_channel(struct mm81x *mors, u32 op_chan_freq_hz, u8 pri_1mhz_chan_idx, u8 op_bw_mhz, u8 pri_bw_mhz, s32 *power_mbm) { int ret; struct host_cmd_req_set_channel req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_SET_CHANNEL, 0), .op_chan_freq_hz = cpu_to_le32(op_chan_freq_hz), .op_bw_mhz = op_bw_mhz, .pri_bw_mhz = pri_bw_mhz, .pri_1mhz_chan_idx = pri_1mhz_chan_idx, .dot11_mode = HOST_CMD_DOT11_PROTO_MODE_AH, }; struct host_cmd_resp_set_channel resp; ret = mm81x_cmd_tx(mors, (struct host_cmd_resp *)&resp, (struct host_cmd_req *)&req, sizeof(resp), 0); if (!ret) *power_mbm = QDBM_TO_MBM(le32_to_cpu(resp.power_qdbm)); return ret; } int mm81x_cmd_disable_key(struct mm81x *mors, struct mm81x_vif *mors_vif, u16 aid, struct ieee80211_key_conf *key) { struct host_cmd_req_disable_key req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_DISABLE_KEY, mors_vif->id), .aid = cpu_to_le32(aid), .key_idx = key->hw_key_idx, .key_type = cpu_to_le32((key->flags & IEEE80211_KEY_FLAG_PAIRWISE) ? HOST_CMD_TEMPORAL_KEY_TYPE_PTK : HOST_CMD_TEMPORAL_KEY_TYPE_GTK), }; return mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)&req, 0, 0); } int mm81x_cmd_install_key(struct mm81x *mors, struct mm81x_vif *mors_vif, u16 aid, struct ieee80211_key_conf *key, enum host_cmd_key_cipher cipher, enum host_cmd_aes_key_len length) { int ret; struct host_cmd_req_install_key req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_INSTALL_KEY, mors_vif->id), .pn = cpu_to_le64(atomic64_read(&key->tx_pn)), .aid = cpu_to_le32(aid), .cipher = cipher, .key_length = length, .key_idx = key->keyidx, .key_type = (key->flags & IEEE80211_KEY_FLAG_PAIRWISE) ? HOST_CMD_TEMPORAL_KEY_TYPE_PTK : HOST_CMD_TEMPORAL_KEY_TYPE_GTK, }; struct host_cmd_resp_install_key resp; if (key->keylen > sizeof(req.key)) return -EINVAL; memcpy(req.key, key->key, key->keylen); ret = mm81x_cmd_tx(mors, (struct host_cmd_resp *)&resp, (struct host_cmd_req *)&req, sizeof(resp), 0); if (!ret) { key->hw_key_idx = resp.key_idx; dev_dbg(mors->dev, "Installed key @ hw index: %d", resp.key_idx); } return ret; } int mm81x_cmd_cfg_multicast_filter(struct mm81x *mors, struct mm81x_vif *mors_vif) { struct host_cmd_req_mcast_filter *req; struct mcast_filter *filter = mors->mcast_filter; u16 filter_list_len = sizeof(filter->addr_list[0]) * filter->count; u16 alloc_len = filter_list_len + sizeof(*req); int ret = 0; req = kzalloc(alloc_len, GFP_KERNEL); if (!req) return -ENOMEM; req->hdr = INIT_CMD_HDR((*req), HOST_CMD_ID_MCAST_FILTER, mors_vif->id); req->hdr.len = cpu_to_le16(alloc_len - sizeof(req->hdr)); req->count = filter->count; memcpy(req->hw_addr, filter->addr_list, filter_list_len); ret = mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)req, 0, 0); kfree(req); return ret; } int mm81x_cmd_cfg_bss(struct mm81x *mors, u16 vif_id, u16 beacon_int, u16 dtim_period, u32 cssid) { struct host_cmd_req_bss_config req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_BSS_CONFIG, vif_id), .beacon_interval_tu = cpu_to_le16(beacon_int), .cssid = cpu_to_le32(cssid), .dtim_period = cpu_to_le16(dtim_period), }; return mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)&req, 0, 0); } int mm81x_cmd_config_beacon_timer(struct mm81x *mors, void *mm81x_vif, bool enabled) { struct mm81x_vif *vif = mm81x_vif; struct host_cmd_req_bss_beacon_config req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_BSS_BEACON_CONFIG, vif->id), .enable = enabled, }; return mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)&req, 0, 0); } int mm81x_cmd_set_ps(struct mm81x *mors, bool enabled) { struct host_cmd_req_config_ps req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_CONFIG_PS, 0), .enabled = (u8)enabled, }; return mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)&req, 0, HOST_CMD_POWERSAVE_TIMEOUT_MS); } int mm81x_cmd_cfg_qos(struct mm81x *mors, struct mm81x_queue_params *params) { struct host_cmd_req_set_qos_params req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_SET_QOS_PARAMS, 0), .uapsd = params->uapsd, .queue_idx = params->aci, .aifs_slot_count = params->aifs, .contention_window_min = cpu_to_le16(params->cw_min), .contention_window_max = cpu_to_le16(params->cw_max), .max_txop_usec = cpu_to_le32(params->txop), }; return mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)&req, 0, 0); } int mm81x_cmd_rm_if(struct mm81x *mors, u16 vif_id) { struct host_cmd_req_remove_interface req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_REMOVE_INTERFACE, vif_id), }; return mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)&req, 0, 0); } int mm81x_cmd_set_frag_threshold(struct mm81x *mors, u32 frag_threshold) { struct host_cmd_req_get_set_generic_param req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_GET_SET_GENERIC_PARAM, 0), .param_id = cpu_to_le32(HOST_CMD_PARAM_ID_FRAGMENT_THRESHOLD), .action = cpu_to_le32(HOST_CMD_PARAM_ACTION_SET), .value = cpu_to_le32(frag_threshold), }; return mm81x_cmd_tx(mors, NULL, (struct host_cmd_req *)&req, 0, 0); } int mm81x_cmd_get_disabled_channels( struct mm81x *mors, struct host_cmd_resp_get_disabled_channels *resp, uint resp_len) { struct host_cmd_req req = { .hdr = INIT_CMD_HDR(req, HOST_CMD_ID_GET_DISABLED_CHANNELS, 0), }; return mm81x_cmd_tx(mors, (struct host_cmd_resp *)resp, &req, resp_len, 0); }