// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2017-2026 Morse Micro
*/
#include <linux/types.h>
#include <linux/atomic.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#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);
}