Linux wireless drivers development
 help / color / mirror / Atom feed
* [PATCH wireless-next v3 08/33] wifi: mm81x: add core.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/core.h | 464 +++++++++++++++++++
 1 file changed, 464 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/core.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/core.h b/drivers/net/wireless/morsemicro/mm81x/core.h
new file mode 100644
index 000000000000..d5b6dae59bcf
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/core.h
@@ -0,0 +1,464 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_CORE_H_
+#define _MM81X_CORE_H_
+
+#include <net/mac80211.h>
+#include <linux/workqueue.h>
+#include <linux/interrupt.h>
+#include <linux/kfifo.h>
+#include <linux/types.h>
+#include <linux/version.h>
+#include <linux/crc32.h>
+#include <linux/notifier.h>
+#include <linux/nospec.h>
+#include <linux/wait.h>
+#include "yaps.h"
+#include "yaps_hw.h"
+#include "hw.h"
+#include "fw.h"
+#include "rc.h"
+
+#define MM81X_DRIVER_SEMVER_MAJOR 56
+#define MM81X_DRIVER_SEMVER_MINOR 3
+#define MM81X_DRIVER_SEMVER_PATCH 0
+
+#define MM81X_SEMVER_GET_MAJOR(x) (((x) >> 22) & 0x3FF)
+#define MM81X_SEMVER_GET_MINOR(x) (((x) >> 10) & 0xFFF)
+#define MM81X_SEMVER_GET_PATCH(x) ((x) & 0x3FF)
+
+#define DRV_VERSION __stringify(MM81X_VERSION)
+
+#define MM8108_FW_BASE "mm8108"
+
+#define BCF_SIZE_MAX 48
+
+/* Generate a device ID from chip ID, revision, and chip type */
+#define MM81X_DEVICE_ID(chip_id, chip_rev, chip_type) \
+	((chip_id) | ((chip_rev) << 8) | ((chip_type) << 12))
+
+/* Get constituents of the device ID */
+#define MM81X_DEVICE_GET_CHIP_ID(device_id) ((device_id) & 0xff)
+#define MM81X_DEVICE_GET_CHIP_REV(device_id) ((((device_id) >> 8) & 0xf))
+#define MM81X_DEVICE_GET_CHIP_TYPE(device_id) ((((device_id) >> 12) & 0xf))
+
+#define KHZ100_TO_MHZ(x) ((x) / 10)
+#define KHZ100_TO_KHZ(freq) ((freq) * 100)
+#define KHZ100_TO_HZ(freq) ((freq) * 100000)
+
+#define QDBM_TO_MBM(gain) (((gain) * 100) >> 2)
+#define MBM_TO_QDBM(gain) (((gain) << 2) / 100)
+#define QDBM_TO_DBM(gain) ((gain) / 4)
+
+#define BPS_TO_KBPS(x) ((x) / 1000)
+
+#define NSS_IDX_TO_NSS(x) ((x) + 1)
+#define NSS_TO_NSS_IDX(x) ((x) - 1)
+
+#define ROUND_BYTES_TO_WORD(_nbytes) \
+	(((_nbytes) + 3) & ~((typeof(_nbytes))0x03))
+
+struct mm81x_bus_ops;
+struct mm81x_hif_ops;
+
+#define MM81X_CAPS_MAX_FW_VAL (128)
+
+/* Max number of interfaces */
+#define MM81X_MAX_IF (2)
+
+enum mm81x_caps_flags {
+	MM81X_CAPS_FW_START = 0,
+	MM81X_CAPS_2MHZ = MM81X_CAPS_FW_START,
+	MM81X_CAPS_4MHZ,
+	MM81X_CAPS_8MHZ,
+	MM81X_CAPS_16MHZ,
+	MM81X_CAPS_SGI,
+	MM81X_CAPS_S1G_LONG,
+	MM81X_CAPS_TRAVELING_PILOT_ONE_STREAM,
+	MM81X_CAPS_TRAVELING_PILOT_TWO_STREAM,
+	MM81X_CAPS_MU_BEAMFORMEE,
+	MM81X_CAPS_MU_BEAMFORMER,
+	MM81X_CAPS_RD_RESPONDER,
+	MM81X_CAPS_STA_TYPE_SENSOR,
+	MM81X_CAPS_STA_TYPE_NON_SENSOR,
+	MM81X_CAPS_GROUP_AID,
+	MM81X_CAPS_NON_TIM,
+	MM81X_CAPS_TIM_ADE,
+	MM81X_CAPS_BAT,
+	MM81X_CAPS_DYNAMIC_AID,
+	MM81X_CAPS_UPLINK_SYNC,
+	MM81X_CAPS_FLOW_CONTROL,
+	MM81X_CAPS_AMPDU,
+	MM81X_CAPS_AMSDU,
+	MM81X_CAPS_1MHZ_CONTROL_RESPONSE_PREAMBLE,
+	MM81X_CAPS_PAGE_SLICING,
+	MM81X_CAPS_RAW,
+	MM81X_CAPS_MCS8,
+	MM81X_CAPS_MCS9,
+	MM81X_CAPS_ASYMMETRIC_BA_SUPPORT,
+	MM81X_CAPS_DAC,
+	MM81X_CAPS_CAC,
+	MM81X_CAPS_TXOP_SHARING_IMPLICIT_ACK,
+	MM81X_CAPS_NDP_PSPOLL,
+	MM81X_CAPS_FRAGMENT_BA,
+	MM81X_CAPS_OBSS_MITIGATION,
+	MM81X_CAPS_TMP_PS_MODE_SWITCH,
+	MM81X_CAPS_SECTOR_TRAINING,
+	MM81X_CAPS_UNSOLICIT_DYNAMIC_AID,
+	MM81X_CAPS_NDP_BEAMFORMING_REPORT,
+	MM81X_CAPS_MCS_NEGOTIATION,
+	MM81X_CAPS_DUPLICATE_1MHZ,
+	MM81X_CAPS_TACK_AS_PSPOLL,
+	MM81X_CAPS_PV1,
+	MM81X_CAPS_TWT_RESPONDER,
+	MM81X_CAPS_TWT_REQUESTER,
+	MM81X_CAPS_BDT,
+	MM81X_CAPS_TWT_GROUPING,
+	MM81X_CAPS_LINK_ADAPTATION_WO_NDP_CMAC,
+	MM81X_CAPS_LONG_MPDU,
+	MM81X_CAPS_TXOP_SECTORIZATION,
+	MM81X_CAPS_GROUP_SECTORIZATION,
+	MM81X_CAPS_HTC_VHT,
+	MM81X_CAPS_HTC_VHT_MFB,
+	MM81X_CAPS_HTC_VHT_MRQ,
+	MM81X_CAPS_2SS,
+	MM81X_CAPS_3SS,
+	MM81X_CAPS_4SS,
+	MM81X_CAPS_SU_BEAMFORMEE,
+	MM81X_CAPS_SU_BEAMFORMER,
+	MM81X_CAPS_RX_STBC,
+	MM81X_CAPS_TX_STBC,
+	MM81X_CAPS_RX_LDPC,
+	MM81X_CAPS_HW_FRAGMENT,
+
+	MM81X_CAPS_FW_END = MM81X_CAPS_MAX_FW_VAL,
+	MM81X_CAPS_LAST = MM81X_CAPS_FW_END,
+};
+
+struct mm81x_fw_caps {
+	u32 flags[FW_CAPABILITIES_FLAGS_WIDTH];
+	u8 ampdu_mss;
+	u8 beamformee_sts_capability;
+	u8 number_sounding_dimensions;
+	u8 maximum_ampdu_length_exponent;
+	u8 mm81x_mmss_offset;
+};
+
+#define MM81X_FW_SUPP(MM81X_CAPS, CAPABILITY) \
+	mm81x_caps_supported(MM81X_CAPS, MM81X_CAPS_##CAPABILITY)
+
+static inline bool mm81x_caps_supported(struct mm81x_fw_caps *caps,
+					enum mm81x_caps_flags flag)
+{
+	const unsigned long *flags_ptr = (unsigned long *)caps->flags;
+
+	return test_bit(flag, flags_ptr);
+}
+
+struct mm81x_ps {
+	u32 wakers;
+	bool enable;
+	bool suspended;
+	/* PS state lock */
+	struct mutex lock;
+	struct delayed_work delayed_eval_work;
+};
+
+enum mm81x_page_aci {
+	MM81X_ACI_BE = 0,
+	MM81X_ACI_BK = 1,
+	MM81X_ACI_VI = 2,
+	MM81X_ACI_VO = 3,
+};
+
+enum mm81x_qos_tid_up_index {
+	MM81X_QOS_TID_UP_BK = 1,
+	MM81X_QOS_TID_UP_XX = 2,
+	MM81X_QOS_TID_UP_BE = 0,
+	MM81X_QOS_TID_UP_EE = 3,
+	MM81X_QOS_TID_UP_CL = 4,
+	MM81X_QOS_TID_UP_VI = 5,
+	MM81X_QOS_TID_UP_VO = 6,
+	MM81X_QOS_TID_UP_NC = 7,
+
+	MM81X_QOS_TID_UP_LOWEST = MM81X_QOS_TID_UP_BK,
+	MM81X_QOS_TID_UP_HIGHEST = MM81X_QOS_TID_UP_NC
+};
+
+struct mm81x_sw_version {
+	u8 major;
+	u8 minor;
+	u8 patch;
+};
+
+struct mm81x_sta {
+	const struct ieee80211_vif *vif;
+	u8 addr[ETH_ALEN];
+	enum ieee80211_sta_state state;
+	bool tid_tx[IEEE80211_NUM_TIDS];
+	bool tid_start_tx[IEEE80211_NUM_TIDS];
+	u8 tid_params[IEEE80211_NUM_TIDS];
+	int max_bw_mhz;
+	struct mm81x_rc_sta rc;
+	struct mmrc_rate last_sta_tx_rate;
+	s16 avg_rssi;
+	bool tx_ps_filter_en;
+};
+
+struct mm81x_vif {
+	struct mm81x *mors;
+	u16 id;
+
+	union {
+		struct {
+			bool is_assoc;
+		} sta;
+		struct {
+			u32 num_stas;
+			struct work_struct beacon_work;
+		} ap;
+	} u;
+};
+
+struct mm81x_stale_tx_status {
+	/* Stale Tx lock */
+	spinlock_t lock;
+	struct timer_list timer;
+};
+
+struct mcast_filter {
+	u8 count;
+	/*
+	 * Integer representation of the last four bytes of a multicast MAC
+	 * address. The first two bytes are always 0x0100 (IPv4) or 0x3333
+	 * (IPv6).
+	 */
+	__le32 addr_list[];
+};
+
+enum mm81x_hw_scan_op {
+	MM81X_HW_SCAN_OP_START,
+	MM81X_HW_SCAN_OP_STOP,
+};
+
+struct mm81x_hw_scan_params {
+	struct ieee80211_hw *hw;
+
+	/* vif which initiated the scan */
+	struct ieee80211_vif *vif;
+	bool has_directed_ssid;
+	u32 dwell_time_ms;
+	u32 dwell_on_home_ms;
+	enum mm81x_hw_scan_op operation;
+	bool store;
+	struct sk_buff *probe_req;
+	u16 num_chans;
+	u16 allocated_chans;
+
+	struct {
+		struct ieee80211_channel *channel;
+		/* Index into @ref powers_qdbm for the power of this channel */
+		u8 power_idx;
+	} *channels;
+
+	s32 *powers_qdbm;
+	u8 n_powers;
+};
+
+enum mm81x_hw_scan_state {
+	HW_SCAN_STATE_IDLE,
+	HW_SCAN_STATE_RUNNING,
+	HW_SCAN_STATE_ABORTING,
+};
+
+struct mm81x_hw_scan {
+	enum mm81x_hw_scan_state state;
+	struct completion scan_done;
+	struct mm81x_hw_scan_params *params;
+	struct delayed_work timeout;
+	u32 home_dwell_ms;
+};
+
+enum mm81x_hif_event_flags {
+	MM81X_HIF_EVT_RX_PEND,
+	MM81X_HIF_EVT_PAGE_RETURN_PEND,
+	MM81X_HIF_EVT_TX_COMMAND_PEND,
+	MM81X_HIF_EVT_TX_BEACON_PEND,
+	MM81X_HIF_EVT_TX_MGMT_PEND,
+	MM81X_HIF_EVT_TX_DATA_PEND,
+	MM81X_HIF_EVT_TX_PACKET_FREED_UP_PEND,
+	MM81X_HIF_EVT_DATA_TRAFFIC_PAUSE_PEND,
+	MM81X_HIF_EVT_DATA_TRAFFIC_RESUME_PEND,
+	MM81X_HIF_EVT_UPDATE_HW_CLOCK_REFERENCE,
+};
+
+enum mm81x_state_flags {
+	MM81X_STATE_CHIP_UNRESPONSIVE,
+	MM81X_STATE_DATA_QS_STOPPED,
+	MM81X_STATE_DATA_TX_STOPPED,
+	MM81X_STATE_REGDOM_SET_BY_USER,
+	MM81X_STATE_REGDOM_SET_BY_OTP,
+	MM81X_STATE_RELOAD_FW_AFTER_START,
+	MM81X_STATE_HOST_TO_CHIP_TX_BLOCKED,
+	MM81X_STATE_HOST_TO_CHIP_CMD_BLOCKED,
+};
+
+#define MM81X_COUNTRY_LEN (3)
+#define INVALID_VIF_INDEX 0xFF
+
+struct mm81x {
+	u32 chip_id;
+	u32 host_table_ptr;
+
+	/* Refer to @enum mm81x_bus_type */
+	u32 bus_type;
+	u32 bcf_address;
+
+	/*
+	 * Parsed from the release tag, which should be in the format
+	 * 'rel_<major>_<minor>_<patch>'. If the tag is not in this format
+	 * then corresponding version field will be 0.
+	 */
+	struct mm81x_sw_version sw_ver;
+	u8 macaddr[ETH_ALEN];
+	u8 country[MM81X_COUNTRY_LEN];
+
+	/* Mask of type @enum host_table_firmware_flags */
+	u32 firmware_flags;
+	struct mm81x_fw_caps fw_caps;
+	bool started;
+	bool chip_was_reset;
+	struct wiphy *wiphy;
+	struct mm81x_hw_scan hw_scan;
+	struct ieee80211_hw *hw;
+	struct device *dev;
+
+	struct ieee80211_vif __rcu *vifs[MM81X_MAX_IF];
+
+	/* @mm81x_state_flags */
+	unsigned long state_flags;
+
+	u16 cmd_seq;
+	struct completion *cmd_comp;
+	/* Serialises commands */
+	struct mutex cmd_lock;
+
+	/* Serialises command completion */
+	struct mutex cmd_wait;
+
+	const struct mm81x_regs *regs;
+
+	struct {
+		union {
+			struct mm81x_yaps yaps;
+		} u;
+		const struct mm81x_hif_ops *ops;
+		/* See @enum mm81x_hif_event_flags for values */
+		unsigned long event_flags;
+		bool validate_skb_checksum;
+	} hif;
+
+	struct workqueue_struct *chip_wq;
+	struct work_struct hif_work;
+	struct work_struct usb_irq_work;
+	struct mm81x_stale_tx_status stale_status;
+	bool config_ps;
+	struct mm81x_ps ps;
+
+	/* Tx power in mBm received from the FW before association */
+	s32 tx_power_mbm;
+	s32 tx_max_power_mbm;
+
+	const struct mm81x_bus_ops *bus_ops;
+	struct mm81x_rc mrc;
+	int rts_threshold;
+	struct workqueue_struct *net_wq;
+	struct work_struct tx_stale_work;
+	wait_queue_head_t tx_empty_waitq;
+
+	struct cfg80211_chan_def chandef;
+	struct mcast_filter *mcast_filter;
+	atomic_t num_bcn_vifs;
+	unsigned long beacon_irqs_enabled;
+	u8 drv_priv[] __aligned(sizeof(void *));
+};
+
+/* Map from mac80211 queue to Morse ACI value for page metadata */
+static inline u8 map_mac80211q_2_mm81x_aci(u16 mac80211queue)
+{
+	switch (mac80211queue) {
+	case IEEE80211_AC_VO:
+		return MM81X_ACI_VO;
+	case IEEE80211_AC_VI:
+		return MM81X_ACI_VI;
+	case IEEE80211_AC_BK:
+		return MM81X_ACI_BK;
+	default:
+		return MM81X_ACI_BE;
+	}
+}
+
+static inline enum mm81x_page_aci
+dot11_tid_to_ac(enum mm81x_qos_tid_up_index tid)
+{
+	switch (tid) {
+	case MM81X_QOS_TID_UP_BK:
+	case MM81X_QOS_TID_UP_XX:
+		return MM81X_ACI_BK;
+	case MM81X_QOS_TID_UP_CL:
+	case MM81X_QOS_TID_UP_VI:
+		return MM81X_ACI_VI;
+	case MM81X_QOS_TID_UP_VO:
+	case MM81X_QOS_TID_UP_NC:
+		return MM81X_ACI_VO;
+	case MM81X_QOS_TID_UP_BE:
+	case MM81X_QOS_TID_UP_EE:
+	default:
+		return MM81X_ACI_BE;
+	}
+}
+
+static inline bool mm81x_is_data_tx_allowed(struct mm81x *mors)
+{
+	return !test_bit(MM81X_STATE_DATA_TX_STOPPED, &mors->state_flags) &&
+	       !test_bit(MM81X_HIF_EVT_DATA_TRAFFIC_PAUSE_PEND,
+			 &mors->hif.event_flags);
+}
+
+static inline struct ieee80211_vif *
+mm81x_vif_to_ieee80211_vif(struct mm81x_vif *mors_vif)
+{
+	return container_of((void *)mors_vif, struct ieee80211_vif, drv_priv);
+}
+
+static inline struct mm81x_vif *
+ieee80211_vif_to_mors_vif(struct ieee80211_vif *vif)
+{
+	return (struct mm81x_vif *)vif->drv_priv;
+}
+
+static inline struct mm81x *mm81x_vif_to_mors(struct mm81x_vif *mors_vif)
+{
+	return mors_vif->mors;
+}
+
+static inline u32 mm81x_generate_cssid(const u8 *ssid, u8 len)
+{
+	return ~crc32(~0, ssid, len);
+}
+
+int mm81x_beacon_init(struct mm81x_vif *mors_vif);
+void mm81x_beacon_finish(struct mm81x_vif *mors_vif);
+void mm81x_beacon_irq_handle(struct mm81x *mors, u32 status);
+char *mm81x_core_get_fw_path(u32 chip_id);
+struct mm81x *mm81x_core_alloc(size_t priv_size, struct device *dev);
+int mm81x_core_init(struct mm81x *mors);
+int mm81x_core_register(struct mm81x *mors);
+void mm81x_core_unregister(struct mm81x *mors);
+void mm81x_core_deinit(struct mm81x *mors);
+void mm81x_core_free(struct mm81x *mors);
+
+#endif /* !_MM81X_MM81X_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 09/33] wifi: mm81x: add fw.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/fw.c | 732 +++++++++++++++++++++
 1 file changed, 732 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/fw.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/fw.c b/drivers/net/wireless/morsemicro/mm81x/fw.c
new file mode 100644
index 000000000000..337b809571d2
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/fw.c
@@ -0,0 +1,732 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/kernel.h>
+#include <linux/firmware.h>
+#include <linux/slab.h>
+#include <linux/delay.h>
+#include <linux/iopoll.h>
+#include <linux/string_choices.h>
+#include <net/mac80211.h>
+#include <linux/elf.h>
+#include <linux/crc32.h>
+#include "fw.h"
+#include "mac.h"
+#include "bus.h"
+
+/*
+ * Maximum wait time (microseconds) for firmware to boot (for host table
+ * pointer to be available)
+ */
+#define HOST_TABLE_PTR_POLL_TIMEOUT_US 1200000
+#define HOST_TABLE_PTR_POLL_PERIOD_US 10000
+
+/* Number of times to attempt flashing FW */
+#define FW_FLASH_ATTEMPT_COUNT 3
+
+static int mm81x_fw_get_header(const u8 *data, Elf32_Ehdr *ehdr)
+{
+	const struct mm81x_elf32_ehdr *p =
+		(const struct mm81x_elf32_ehdr *)data;
+
+	/* Magic check */
+	if (p->e_ident[EI_MAG0] != ELFMAG0 || p->e_ident[EI_MAG1] != ELFMAG1 ||
+	    p->e_ident[EI_MAG2] != ELFMAG2 || p->e_ident[EI_MAG3] != ELFMAG3)
+		return -EINVAL;
+
+	/* elf32 and little endian */
+	if (p->e_ident[EI_DATA] != ELFDATA2LSB ||
+	    p->e_ident[EI_CLASS] != ELFCLASS32)
+		return -EINVAL;
+
+	ehdr->e_phoff = le32_to_cpu(p->e_phoff);
+	ehdr->e_phentsize = le16_to_cpu(p->e_phentsize);
+	ehdr->e_phnum = le16_to_cpu(p->e_phnum);
+	ehdr->e_shoff = le32_to_cpu(p->e_shoff);
+	ehdr->e_shentsize = le16_to_cpu(p->e_shentsize);
+	ehdr->e_shnum = le16_to_cpu(p->e_shnum);
+	ehdr->e_shstrndx = le16_to_cpu(p->e_shstrndx);
+	ehdr->e_entry = le32_to_cpu(p->e_entry);
+
+	return 0;
+}
+
+static void mm81x_fw_parse_info(struct mm81x *mors, const u8 *data, int length)
+{
+	const struct mm81x_fw_info_tlv *tlv =
+		(const struct mm81x_fw_info_tlv *)data;
+
+	while ((u8 *)tlv < (data + length)) {
+		switch (le16_to_cpu(tlv->type)) {
+		case MM81X_FW_INFO_TLV_BCF_ADDR:
+			mors->bcf_address = get_unaligned_le32(tlv->val);
+			break;
+		default:
+			break;
+		}
+		tlv = (const struct mm81x_fw_info_tlv *)((u8 *)tlv +
+							 le16_to_cpu(
+								 tlv->length) +
+							 sizeof(*tlv));
+	}
+}
+
+static int mm81x_fw_get_section_header(const u8 *data, Elf32_Ehdr *ehdr,
+				       Elf32_Shdr *shdr, int i)
+{
+	const struct mm81x_elf32_shdr *p =
+		(void *)(data + ehdr->e_shoff + (i * ehdr->e_shentsize));
+
+	shdr->sh_name = le32_to_cpu(p->sh_name);
+	shdr->sh_type = le32_to_cpu(p->sh_type);
+	shdr->sh_offset = le32_to_cpu(p->sh_offset);
+	shdr->sh_addr = le32_to_cpu(p->sh_addr);
+	shdr->sh_size = le32_to_cpu(p->sh_size);
+	shdr->sh_flags = le32_to_cpu(p->sh_flags);
+
+	return 0;
+}
+
+static int mm81x_fw_set_boot_addr(struct mm81x *mors, uint32_t addr)
+{
+	int status;
+
+	dev_dbg(mors->dev, "Overwriting boot address to 0x%x", addr);
+	mm81x_claim_bus(mors);
+	status = mm81x_reg32_write(mors, MM81X_REG_BOOT_ADDR(mors), addr);
+	mm81x_release_bus(mors);
+	return status;
+}
+
+static int mm81x_fw_load_fw(struct mm81x *mors, const struct firmware *fw)
+{
+	int i;
+	int ret = 0;
+	Elf32_Ehdr ehdr;
+	Elf32_Phdr phdr;
+	Elf32_Shdr shdr;
+	Elf32_Shdr sh_strtab;
+	const char *sh_strs;
+
+	u8 *fw_buf = devm_kmalloc(mors->dev, ROUND_BYTES_TO_WORD(fw->size),
+				  GFP_KERNEL);
+
+	if (!fw_buf)
+		return -ENOMEM;
+
+	if (mm81x_fw_get_header(fw->data, &ehdr)) {
+		dev_err(mors->dev, "Wrong file format");
+		return -EINVAL;
+	}
+
+	if (mm81x_fw_get_section_header(fw->data, &ehdr, &sh_strtab,
+					ehdr.e_shstrndx)) {
+		dev_err(mors->dev, "Invalid firmware. Missing string table");
+		return -ENOENT;
+	}
+
+	sh_strs = (const char *)fw->data + sh_strtab.sh_offset;
+
+	for (i = 0; i < ehdr.e_phnum; i++) {
+		int status;
+		int address;
+		const struct mm81x_elf32_phdr *p =
+			(void *)(fw->data + ehdr.e_phoff +
+				 i * ehdr.e_phentsize);
+
+		phdr.p_type = le32_to_cpu(p->p_type);
+		phdr.p_offset = le32_to_cpu(p->p_offset);
+		phdr.p_paddr = le32_to_cpu(p->p_paddr);
+		phdr.p_filesz = le32_to_cpu(p->p_filesz);
+		phdr.p_memsz = le32_to_cpu(p->p_memsz);
+
+		address = phdr.p_paddr;
+
+		if (phdr.p_type != PT_LOAD || !phdr.p_memsz)
+			continue;
+
+		if (phdr.p_filesz && phdr.p_offset &&
+		    (phdr.p_offset + phdr.p_filesz) < fw->size) {
+			u32 padded_size = ROUND_BYTES_TO_WORD(phdr.p_filesz);
+
+			memcpy(fw_buf, fw->data + phdr.p_offset, padded_size);
+			/* Set padding to 0xff */
+			memset(fw_buf + phdr.p_filesz, 0xff,
+			       padded_size - phdr.p_filesz);
+			mm81x_claim_bus(mors);
+			status = mm81x_dm_write(mors, address, fw_buf,
+						padded_size);
+			mm81x_release_bus(mors);
+			if (status) {
+				ret = -EIO;
+				break;
+			}
+		}
+	}
+
+	for (i = 0; i < ehdr.e_shnum; i++) {
+		if (mm81x_fw_get_section_header(fw->data, &ehdr, &shdr, i))
+			continue;
+
+		/* This is the firmware info. Parse it */
+		if (!strncmp(sh_strs + shdr.sh_name, ".fw_info",
+			     sizeof(".fw_info")))
+			mm81x_fw_parse_info(mors, fw->data + shdr.sh_offset,
+					    shdr.sh_size);
+	}
+
+	if (ehdr.e_entry)
+		ret = mm81x_fw_set_boot_addr(mors, ehdr.e_entry);
+
+	devm_kfree(mors->dev, fw_buf);
+	return ret;
+}
+
+static int __mm81x_fw_load_bcf(struct mm81x *mors, unsigned int addr,
+			       const void *src, size_t src_len, u8 *scratch,
+			       size_t scratch_cap)
+{
+	size_t rounded = ROUND_BYTES_TO_WORD(src_len);
+	int st;
+
+	if (rounded > scratch_cap)
+		return -EINVAL;
+	if (rounded > BCF_DATABASE_SIZE)
+		return -EFBIG;
+
+	memcpy(scratch, src, src_len);
+	if (rounded > src_len)
+		memset(scratch + src_len, 0xff, rounded - src_len);
+
+	mm81x_claim_bus(mors);
+	st = mm81x_dm_write(mors, addr, scratch, rounded);
+	mm81x_release_bus(mors);
+
+	return st ? -EIO : 0;
+}
+
+static int mm81x_fw_load_bcf(struct mm81x *mors, const struct firmware *bcf,
+			     unsigned int bcf_address)
+{
+	int i, ret = 0;
+	size_t reg_prefix_len, cfg_len_rounded = 0, reg_len_rounded;
+	Elf32_Ehdr ehdr;
+	Elf32_Shdr shdr, sh_strtab;
+	const char *sh_strs, *reg_prefix = ".regdom_", *reg_src;
+	size_t reg_len;
+	u8 *bcf_buf;
+
+	bcf_buf = devm_kmalloc(mors->dev, ROUND_BYTES_TO_WORD(bcf->size),
+			       GFP_KERNEL);
+	if (!bcf_buf)
+		return -ENOMEM;
+
+	if (mm81x_fw_get_header(bcf->data, &ehdr)) {
+		dev_err(mors->dev, "Wrong file format");
+		ret = -EINVAL;
+		goto out_free;
+	}
+
+	if (mm81x_fw_get_section_header(bcf->data, &ehdr, &sh_strtab,
+					ehdr.e_shstrndx)) {
+		dev_err(mors->dev, "Invalid BCF - missing string table");
+		ret = -ENOENT;
+		goto out_free;
+	}
+
+	sh_strs = (const char *)bcf->data + sh_strtab.sh_offset;
+	reg_prefix_len = strlen(reg_prefix);
+
+	for (i = 0; i < ehdr.e_shnum; i++) {
+		if (mm81x_fw_get_section_header(bcf->data, &ehdr, &shdr, i))
+			continue;
+		if (strcmp(sh_strs + shdr.sh_name, ".board_config"))
+			continue;
+
+		cfg_len_rounded = ROUND_BYTES_TO_WORD(shdr.sh_size);
+		dev_dbg(mors->dev,
+			"Write BCF board_config - addr 0x%x size %zu",
+			bcf_address, cfg_len_rounded);
+
+		ret = __mm81x_fw_load_bcf(mors, bcf_address,
+					  bcf->data + shdr.sh_offset,
+					  shdr.sh_size, bcf_buf,
+					  ROUND_BYTES_TO_WORD(bcf->size));
+		if (ret)
+			goto out_free;
+
+		bcf_address += cfg_len_rounded;
+		break;
+	}
+
+	ret = -EINVAL;
+	for (; i < ehdr.e_shnum; i++) {
+		if (mm81x_fw_get_section_header(bcf->data, &ehdr, &shdr, i))
+			continue;
+		if (strncmp(sh_strs + shdr.sh_name, reg_prefix, reg_prefix_len))
+			continue;
+		if (strncmp(sh_strs + shdr.sh_name + reg_prefix_len,
+			    mors->country, 2))
+			continue;
+
+		reg_src = bcf->data + shdr.sh_offset;
+		reg_len = shdr.sh_size;
+		dev_dbg(mors->dev, "Write BCF %s - addr 0x%x size %zu",
+			sh_strs + shdr.sh_name, bcf_address,
+			ROUND_BYTES_TO_WORD(reg_len));
+		ret = 0;
+		break;
+	}
+
+	if (ret)
+		goto out_free;
+
+	reg_len_rounded = ROUND_BYTES_TO_WORD(reg_len);
+	if ((cfg_len_rounded + reg_len_rounded) > BCF_DATABASE_SIZE) {
+		ret = -EFBIG;
+		goto out_free;
+	}
+
+	ret = __mm81x_fw_load_bcf(mors, bcf_address, reg_src, reg_len, bcf_buf,
+				  ROUND_BYTES_TO_WORD(bcf->size));
+
+out_free:
+	devm_kfree(mors->dev, bcf_buf);
+	return ret;
+}
+
+static void mm81x_fw_clear_aon(struct mm81x *mors)
+{
+	int idx;
+	u8 count = MM81X_REG_AON_COUNT(mors);
+	u32 address = MM81X_REG_AON_ADDR(mors);
+
+	for (idx = 0; idx < count; idx++, address += 4) {
+		if (mors->bus_type == MM81X_BUS_TYPE_USB && idx == 0)
+			/* Keep the USB power domain enabled in AON. */
+			mm81x_reg32_write(mors, address,
+					  MM81X_REG_AON_USB_RESET(mors));
+		else
+			/* clear AON */
+			mm81x_reg32_write(mors, address, 0x0);
+	}
+
+	mm81x_hw_toggle_aon_latch(mors);
+}
+
+static void mm81x_fw_trigger(struct mm81x *mors)
+{
+	const unsigned int wait_after_msi_trigger_ms = 1;
+
+	mm81x_claim_bus(mors);
+	/*
+	 * If not coming from a full reset, some AON flags may be latched.
+	 * Make sure to clear any hanging AON bits (can affect booting).
+	 */
+	mm81x_fw_clear_aon(mors);
+
+	if (MM81X_REG_CLK_CTRL(mors))
+		mm81x_reg32_write(mors, MM81X_REG_CLK_CTRL(mors),
+				  MM81X_REG_CLK_CTRL_VALUE(mors));
+
+	mm81x_reg32_write(mors, MM81X_REG_MSI(mors),
+			  MM81X_REG_MSI_HOST_INT(mors));
+	mm81x_release_bus(mors);
+
+	/* Give the chip a chance to boot */
+	mdelay(wait_after_msi_trigger_ms);
+}
+
+static int mm81x_fw_verify_magic(struct mm81x *mors)
+{
+	int ret = 0;
+	int magic = ~MM81X_REG_HOST_MAGIC_VALUE(mors);
+
+	mm81x_claim_bus(mors);
+	mm81x_reg32_read(mors,
+			 mors->host_table_ptr +
+				 offsetof(struct host_table, magic_number),
+			 &magic);
+
+	if (magic != MM81X_REG_HOST_MAGIC_VALUE(mors)) {
+		dev_err(mors->dev, "FW magic mismatch 0x%08x:0x%08x",
+			MM81X_REG_HOST_MAGIC_VALUE(mors), magic);
+		ret = -EIO;
+	}
+
+	mm81x_release_bus(mors);
+	return ret;
+}
+
+static int mm81x_fw_get_flags(struct mm81x *mors)
+{
+	int ret = 0;
+	int fw_flags = 0;
+
+	mm81x_claim_bus(mors);
+	ret = mm81x_reg32_read(mors,
+			       mors->host_table_ptr +
+				       offsetof(struct host_table,
+						firmware_flags),
+			       &fw_flags);
+	mors->firmware_flags = fw_flags;
+	mm81x_release_bus(mors);
+
+	return ret;
+}
+
+static int mm81x_fw_check_compatibility(struct mm81x *mors)
+{
+	int ret = 0;
+	u32 fw_version;
+	u32 major;
+	u32 minor;
+	u32 patch;
+
+	mm81x_claim_bus(mors);
+	ret = mm81x_reg32_read(mors,
+			       mors->host_table_ptr +
+				       offsetof(struct host_table,
+						fw_version_number),
+			       &fw_version);
+	mm81x_release_bus(mors);
+
+	major = MM81X_SEMVER_GET_MAJOR(fw_version);
+	minor = MM81X_SEMVER_GET_MINOR(fw_version);
+	patch = MM81X_SEMVER_GET_PATCH(fw_version);
+
+	/* Firmware on device must be recent enough for driver */
+	if (ret == 0 && major != HOST_CMD_SEMVER_MAJOR) {
+		dev_err(mors->dev,
+			"Incompatible FW version: (Driver) %d.%d.%d, (Chip) %d.%d.%d\n",
+			HOST_CMD_SEMVER_MAJOR, HOST_CMD_SEMVER_MINOR,
+			HOST_CMD_SEMVER_PATCH, major, minor, patch);
+		ret = -EPERM;
+	} else if (ret == 0 && minor != HOST_CMD_SEMVER_MINOR) {
+		dev_warn(
+			mors->dev,
+			"FW version mismatch, some features might not be supported: (Driver) %d.%d.%d, (Chip) %d.%d.%d",
+			HOST_CMD_SEMVER_MAJOR, HOST_CMD_SEMVER_MINOR,
+			HOST_CMD_SEMVER_PATCH, major, minor, patch);
+	}
+
+	return ret;
+}
+
+static int mm81x_fw_invalidate_host_ptr(struct mm81x *mors)
+{
+	int ret;
+
+	mors->host_table_ptr = 0;
+	mm81x_claim_bus(mors);
+	ret = mm81x_reg32_write(mors, MM81X_REG_HOST_MANIFEST_PTR(mors), 0);
+	mm81x_release_bus(mors);
+	return ret;
+}
+
+static int mm81x_fw_get_host_table_ptr(struct mm81x *mors)
+{
+	int ret, err;
+
+	mm81x_claim_bus(mors);
+	ret = read_poll_timeout(mm81x_reg32_read, err,
+				err || mors->host_table_ptr,
+				HOST_TABLE_PTR_POLL_PERIOD_US,
+				HOST_TABLE_PTR_POLL_TIMEOUT_US, false, mors,
+				MM81X_REG_HOST_MANIFEST_PTR(mors),
+				&mors->host_table_ptr);
+	mm81x_release_bus(mors);
+
+	return ret ? ret : err;
+}
+
+static int mm81x_fw_read_ext_host_table(struct mm81x *mors,
+					struct ext_host_tbl **ext_host_table)
+{
+	int ret = 0;
+	u32 host_tbl_ptr = mors->host_table_ptr;
+	u32 ext_host_tbl_ptr;
+	u32 ext_host_tbl_ptr_addr =
+		host_tbl_ptr + offsetof(struct host_table, ext_host_tbl_addr);
+	u32 ext_host_tbl_len;
+	u32 ext_host_tbl_len_ptr_addr;
+	struct ext_host_tbl *host_tbl = NULL;
+
+	mm81x_claim_bus(mors);
+	ret = mm81x_reg32_read(mors, ext_host_tbl_ptr_addr, &ext_host_tbl_ptr);
+	if (ret)
+		goto exit;
+
+	if (!ext_host_tbl_ptr) {
+		ret = -ENXIO;
+		goto exit;
+	}
+
+	ext_host_tbl_len_ptr_addr =
+		ext_host_tbl_ptr +
+		offsetof(struct ext_host_tbl, ext_host_tbl_length);
+
+	ret = mm81x_reg32_read(mors, ext_host_tbl_len_ptr_addr,
+			       &ext_host_tbl_len);
+	if (ret)
+		goto exit;
+
+	ext_host_tbl_len = ROUND_BYTES_TO_WORD(ext_host_tbl_len);
+	if (WARN_ON(ext_host_tbl_len == 0 || ext_host_tbl_len > INT_MAX)) {
+		ret = -EINVAL;
+		goto exit;
+	}
+
+	host_tbl = kmalloc(ext_host_tbl_len, GFP_KERNEL);
+	if (!host_tbl) {
+		ret = -ENOMEM;
+		goto exit;
+	}
+
+	ret = mm81x_dm_read(mors, ext_host_tbl_ptr, (u8 *)host_tbl,
+			    (int)ext_host_tbl_len);
+	if (ret)
+		goto exit;
+
+	mm81x_release_bus(mors);
+	*ext_host_table = host_tbl;
+	return ret;
+
+exit:
+	mm81x_release_bus(mors);
+	kfree(host_tbl);
+	return ret;
+}
+
+static void mm81x_fw_update_capabilities(struct mm81x *mors,
+					 struct ext_host_tbl_s1g_caps *caps)
+{
+	int i;
+
+	for (i = 0; i < FW_CAPABILITIES_FLAGS_WIDTH; i++) {
+		mors->fw_caps.flags[i] = le32_to_cpu(caps->flags[i]);
+		dev_dbg(mors->dev, "Firmware Manifest Flags%d: 0x%x", i,
+			le32_to_cpu(caps->flags[i]));
+	}
+	mors->fw_caps.ampdu_mss = caps->ampdu_mss;
+	mors->fw_caps.mm81x_mmss_offset = caps->mm81x_mmss_offset;
+	mors->fw_caps.beamformee_sts_capability =
+		caps->beamformee_sts_capability;
+	mors->fw_caps.maximum_ampdu_length_exponent =
+		caps->maximum_ampdu_length;
+	mors->fw_caps.number_sounding_dimensions =
+		caps->number_sounding_dimensions;
+
+	dev_dbg(mors->dev, "\tAMPDU Minimum start spacing: %u",
+		caps->ampdu_mss);
+	dev_dbg(mors->dev, "\tMorse Minimum Start Spacing offset: %u",
+		caps->mm81x_mmss_offset);
+	dev_dbg(mors->dev, "\tBeamformee STS Capability: %u",
+		caps->beamformee_sts_capability);
+	dev_dbg(mors->dev, "\tNumber of Sounding Dimensions: %u",
+		caps->number_sounding_dimensions);
+	dev_dbg(mors->dev, "\tMaximum AMPDU Length Exponent: %u",
+		caps->maximum_ampdu_length);
+}
+
+static void mm81x_fw_update_validate_skb_checksum(
+	struct mm81x *mors,
+	struct ext_host_tbl_insert_skb_checksum *validate_checksum)
+{
+	mors->hif.validate_skb_checksum =
+		validate_checksum->insert_and_validate_checksum;
+	dev_dbg(mors->dev, "Validate checksum inserted by fw %s",
+		str_enabled_disabled(mors->hif.validate_skb_checksum));
+}
+
+int mm81x_fw_parse_ext_host_tbl(struct mm81x *mors)
+{
+	int ret;
+	u8 *head;
+	u8 *end;
+	struct ext_host_tbl *ext_host_table = NULL;
+
+	ret = mm81x_fw_read_ext_host_table(mors, &ext_host_table);
+	if (ret || !ext_host_table)
+		goto exit;
+
+	/* Parse the TLVs */
+	head = ext_host_table->ext_host_table_data_tlvs;
+	end = ((u8 *)ext_host_table) +
+	      le32_to_cpu(ext_host_table->ext_host_tbl_length);
+
+	while (head < end) {
+		struct ext_host_tbl_tlv_hdr *hdr =
+			(struct ext_host_tbl_tlv_hdr *)head;
+
+		switch (le16_to_cpu(hdr->tag)) {
+		case MM81X_FW_HOST_TABLE_TAG_S1G_CAPABILITIES:
+			mm81x_fw_update_capabilities(
+				mors, (struct ext_host_tbl_s1g_caps *)hdr);
+			break;
+
+		case MM81X_FW_HOST_TABLE_TAG_INSERT_SKB_CHECKSUM:
+			mm81x_fw_update_validate_skb_checksum(
+				mors,
+				(struct ext_host_tbl_insert_skb_checksum *)hdr);
+			break;
+
+		case MM81X_FW_HOST_TABLE_TAG_YAPS_TABLE:
+			mm81x_yaps_hw_read_table(
+				mors, &((struct ext_host_tbl_yaps_table *)hdr)
+					       ->yaps_table);
+			break;
+		default:
+			break;
+		}
+
+		head += le16_to_cpu(hdr->length);
+		if (!hdr->length)
+			break;
+	}
+
+	kfree(ext_host_table);
+	return ret;
+exit:
+	dev_err(mors->dev, "failed to parse ext host table %d", ret);
+	return ret;
+}
+
+static int __mm81x_fw_flash(struct mm81x *mors, const struct firmware *fw,
+			    const struct firmware *bcf, bool reset)
+{
+	int ret;
+
+	if (reset || !mors->chip_was_reset) {
+		ret = mm81x_hw_digital_reset(mors);
+		if (ret)
+			return ret;
+	}
+
+	mm81x_hw_pre_firmware_ndr_hook(mors);
+
+	ret = mm81x_fw_invalidate_host_ptr(mors);
+	if (ret)
+		return ret;
+
+	ret = mm81x_fw_load_fw(mors, fw);
+	if (ret)
+		return ret;
+
+	ret = mm81x_fw_load_bcf(mors, bcf, mors->bcf_address);
+	if (ret)
+		return ret;
+
+	mm81x_fw_trigger(mors);
+	mm81x_hw_post_firmware_ndr_hook(mors);
+
+	ret = mm81x_fw_get_host_table_ptr(mors);
+	if (ret)
+		return ret;
+
+	ret = mm81x_fw_verify_magic(mors);
+	if (ret)
+		return ret;
+
+	return mm81x_fw_check_compatibility(mors);
+}
+
+static int mm81x_fw_flash(struct mm81x *mors, const struct firmware *fw,
+			  const struct firmware *bcf, bool reset)
+{
+	int ret;
+	int retries = FW_FLASH_ATTEMPT_COUNT;
+
+	while (retries--) {
+		ret = __mm81x_fw_flash(mors, fw, bcf, reset);
+		if (!ret)
+			return 0;
+
+		mors->chip_was_reset = false;
+	}
+
+	return ret;
+}
+
+static uint32_t binary_crc(const struct firmware *fw)
+{
+	return ~crc32_le(~0, (unsigned char const *)fw->data, fw->size) &
+	       0xffffffff;
+}
+
+int mm81x_fw_init(struct mm81x *mors, bool reset)
+{
+	int ret;
+	int n;
+	int board_id;
+	char *fw_path;
+	char bcf_path[MAX_BCF_NAME_LEN];
+	const struct firmware *fw = NULL;
+	const struct firmware *bcf = NULL;
+
+	fw_path = mm81x_core_get_fw_path(mors->chip_id);
+	if (!fw_path)
+		return -ENOMEM;
+
+	board_id = mm81x_hw_otp_get_board_type(mors);
+
+	if (mm81x_hw_otp_valid_board_type(board_id)) {
+		dev_dbg(mors->dev, "Using board type 0x%04x from OTP",
+			board_id);
+		n = snprintf(bcf_path, sizeof(bcf_path),
+			     "%s/bcf_boardtype_%04x.bin", MM81X_FW_DIR,
+			     board_id);
+	} else {
+		dev_err(mors->dev,
+			"OTP not set, unable to determine BCF to use");
+		ret = -EINVAL;
+		goto out;
+	}
+
+	if (n < 0 || n >= sizeof(bcf_path)) {
+		dev_err(mors->dev, "Failed to create BCF path");
+		ret = -EINVAL;
+		goto out;
+	}
+
+	ret = request_firmware(&fw, fw_path, mors->dev);
+	if (ret) {
+		if (ret == -ENOENT)
+			dev_err(mors->dev, "Firmware %s not found\n", fw_path);
+		goto out;
+	}
+
+	dev_info(mors->dev, "Loaded firmware from %s, size %zu, crc32 0x%08x\n",
+		 fw_path, fw->size, binary_crc(fw));
+
+	ret = request_firmware(&bcf, bcf_path, mors->dev);
+	if (ret) {
+		if (ret == -ENOENT)
+			dev_err(mors->dev, "BCF %s not found\n", bcf_path);
+		goto out;
+	}
+
+	dev_info(mors->dev, "Loaded BCF from %s, size %zu, crc32 0x%08x\n",
+		 bcf_path, bcf->size, binary_crc(bcf));
+
+	ret = mm81x_fw_flash(mors, fw, bcf, reset);
+	if (ret) {
+		dev_err(mors->dev, "failed to flash firmware: %d", ret);
+		goto out;
+	}
+
+	ret = mm81x_fw_get_flags(mors);
+
+out:
+	release_firmware(fw);
+	release_firmware(bcf);
+	kfree(fw_path);
+
+	if (ret)
+		dev_err(mors->dev, "failed to init firmware: %d", ret);
+	else
+		dev_dbg(mors->dev, "firmware initialised");
+
+	return ret;
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 10/33] wifi: mm81x: add fw.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/fw.h | 141 +++++++++++++++++++++
 1 file changed, 141 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/fw.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/fw.h b/drivers/net/wireless/morsemicro/mm81x/fw.h
new file mode 100644
index 000000000000..d590ad3121c1
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/fw.h
@@ -0,0 +1,141 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_FW_H_
+#define _MM81X_FW_H_
+
+#include <linux/firmware.h>
+#include <linux/completion.h>
+#include <linux/elf.h>
+#include "yaps_hw.h"
+
+#define BCF_DATABASE_SIZE (1024)
+#define MM81X_FW_DIR "morsemicro"
+#define MM81X_FW_EXT ".bin"
+
+#define MAX_BCF_NAME_LEN 64
+
+/* FW_CAPABILITIES_FLAGS_WIDTH = ceil(MM81X_CAPS_MAX_HW_LEN / 32) */
+#define FW_CAPABILITIES_FLAGS_WIDTH (4)
+
+struct mm81x_elf32_ehdr {
+	unsigned char e_ident[EI_NIDENT];
+	__le16 e_type;
+	__le16 e_machine;
+	__le32 e_version;
+	__le32 e_entry;
+	__le32 e_phoff;
+	__le32 e_shoff;
+	__le32 e_flags;
+	__le16 e_ehsize;
+	__le16 e_phentsize;
+	__le16 e_phnum;
+	__le16 e_shentsize;
+	__le16 e_shnum;
+	__le16 e_shstrndx;
+} __packed;
+
+struct mm81x_elf32_shdr {
+	__le32 sh_name;
+	__le32 sh_type;
+	__le32 sh_flags;
+	__le32 sh_addr;
+	__le32 sh_offset;
+	__le32 sh_size;
+	__le32 sh_link;
+	__le32 sh_info;
+	__le32 sh_addralign;
+	__le32 sh_entsize;
+} __packed;
+
+struct mm81x_elf32_phdr {
+	__le32 p_type;
+	__le32 p_offset;
+	__le32 p_vaddr;
+	__le32 p_paddr;
+	__le32 p_filesz;
+	__le32 p_memsz;
+	__le32 p_flags;
+	__le32 p_align;
+} __packed;
+
+enum mm81x_fw_info_tlv_type {
+	MM81X_FW_INFO_TLV_BCF_ADDR = 1,
+};
+
+struct mm81x_fw_info_tlv {
+	__le16 type;
+	__le16 length;
+	u8 val[];
+} __packed;
+
+enum mm81x_fw_ext_host_tbl_tag {
+	/* The S1G capability tag */
+	MM81X_FW_HOST_TABLE_TAG_S1G_CAPABILITIES = 0,
+	MM81X_FW_HOST_TABLE_TAG_PAGER_BYPASS_TX_STATUS = 1,
+	MM81X_FW_HOST_TABLE_TAG_INSERT_SKB_CHECKSUM = 2,
+	MM81X_FW_HOST_TABLE_TAG_YAPS_TABLE = 3,
+	MM81X_FW_HOST_TABLE_TAG_PAGER_PKT_MEMORY = 4,
+	MM81X_FW_HOST_TABLE_TAG_PAGER_BYPASS_CMD_RESP = 5,
+};
+
+struct ext_host_tbl_tlv_hdr {
+	/* The tag used to identify which capability this represents */
+	__le16 tag;
+	/* The length of the capability structure including this header */
+	__le16 length;
+} __packed;
+
+struct ext_host_tbl_s1g_caps {
+	struct ext_host_tbl_tlv_hdr header;
+	__le32 flags[FW_CAPABILITIES_FLAGS_WIDTH];
+	/*
+	 * The minimum A-MPDU start spacing required by firmware.
+	 * Value | Description
+	 * ------|------------
+	 * 0     | No restriction
+	 * 1     | 1/4 us
+	 * 2     | 1/2 us
+	 * 3     | 1 us
+	 * 4     | 2 us
+	 * 5     | 4 us
+	 * 6     | 8 us
+	 * 7     | 16 us
+	 */
+	u8 ampdu_mss;
+	u8 beamformee_sts_capability;
+	u8 number_sounding_dimensions;
+	/*
+	 * The maximum A-MPDU length. This is the exponent value such that
+	 * (2^(13 + exponent) - 1) is the length
+	 */
+	u8 maximum_ampdu_length;
+	/*
+	 * Offset to apply to the specification's MMSS table to signal further
+	 * minimum MPDU start spacing.
+	 */
+	u8 mm81x_mmss_offset;
+} __packed;
+
+struct ext_host_tbl_insert_skb_checksum {
+	struct ext_host_tbl_tlv_hdr header;
+	u8 insert_and_validate_checksum;
+};
+
+struct ext_host_tbl_yaps_table {
+	struct ext_host_tbl_tlv_hdr header;
+	struct mm81x_yaps_hw_table yaps_table;
+} __packed;
+
+struct ext_host_tbl {
+	__le32 ext_host_tbl_length;
+	u8 dev_mac_addr[6];
+	u8 ext_host_table_data_tlvs[];
+} __packed;
+
+int mm81x_fw_init(struct mm81x *mors, bool reset);
+int mm81x_fw_parse_ext_host_tbl(struct mm81x *mors);
+
+#endif /* !_MM81X_FW_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 11/33] wifi: mm81x: add hif.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/hif.h | 117 ++++++++++++++++++++
 1 file changed, 117 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/hif.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/hif.h b/drivers/net/wireless/morsemicro/mm81x/hif.h
new file mode 100644
index 000000000000..e3d23423049a
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/hif.h
@@ -0,0 +1,117 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_HIF_H_
+#define _MM81X_HIF_H_
+
+#include "core.h"
+
+struct mm81x_skbq;
+
+#define MM81X_HIF_BYPASS_TX_STATUS_IRQ_NUM (15)
+#define MM81X_HIF_BYPASS_CMD_RESP_IRQ_NUM (29)
+#define MM81X_HIF_IRQ_BYPASS_TX_STATUS_AVAILABLE \
+	BIT(MM81X_HIF_BYPASS_TX_STATUS_IRQ_NUM)
+#define MM81X_HIF_IRQ_BYPASS_CMD_RESP_AVAILABLE \
+	BIT(MM81X_HIF_BYPASS_CMD_RESP_IRQ_NUM)
+
+/* Hardware IF interrupt mask. We may use any interrupts in this range */
+#define MM81X_HIF_IRQ_MASK_ALL                                       \
+	(GENMASK(13, 0) | MM81X_HIF_IRQ_BYPASS_TX_STATUS_AVAILABLE | \
+	 MM81X_HIF_IRQ_BYPASS_CMD_RESP_AVAILABLE)
+
+enum mm81x_hif_flags {
+	MM81X_HIF_FLAGS_DIR_TO_HOST = BIT(0),
+	MM81X_HIF_FLAGS_DIR_TO_CHIP = BIT(1),
+	MM81X_HIF_FLAGS_COMMAND = BIT(2),
+	MM81X_HIF_FLAGS_BEACON = BIT(3),
+	MM81X_HIF_FLAGS_DATA = BIT(4)
+};
+
+struct mm81x_hif_ops {
+	int (*init)(struct mm81x *mors);
+	void (*flush_tx_data)(struct mm81x *mors);
+	void (*flush_cmds)(struct mm81x *mors);
+	void (*finish)(struct mm81x *mors);
+	void (*skbq_get_tx_qs)(struct mm81x *mors, struct mm81x_skbq **qs,
+			       int *num_qs);
+	struct mm81x_skbq *(*get_tx_cmd_queue)(struct mm81x *mors);
+	struct mm81x_skbq *(*get_tx_beacon_queue)(struct mm81x *mors);
+	struct mm81x_skbq *(*get_tx_mgmt_queue)(struct mm81x *mors);
+	struct mm81x_skbq *(*get_tx_data_queue)(struct mm81x *mors, int aci);
+	int (*handle_irq)(struct mm81x *mors, u32 status);
+	int (*get_tx_buffered_count)(struct mm81x *mors);
+	int (*get_tx_status_pending_count)(struct mm81x *mors);
+};
+
+static inline void mm81x_hif_clear_events(struct mm81x *mors)
+{
+	mors->hif.event_flags = 0;
+}
+
+static inline int mm81x_hif_init(struct mm81x *mors)
+{
+	return mors->hif.ops->init(mors);
+}
+
+static inline void mm81x_hif_flush_tx_data(struct mm81x *mors)
+{
+	mors->hif.ops->flush_tx_data(mors);
+}
+
+static inline void mm81x_hif_flush_cmds(struct mm81x *mors)
+{
+	mors->hif.ops->flush_cmds(mors);
+}
+
+static inline void mm81x_hif_finish(struct mm81x *mors)
+{
+	mors->hif.ops->finish(mors);
+}
+
+static inline void mm81x_hif_skbq_get_tx_qs(struct mm81x *mors,
+					    struct mm81x_skbq **qs, int *num_qs)
+{
+	mors->hif.ops->skbq_get_tx_qs(mors, qs, num_qs);
+}
+
+static inline struct mm81x_skbq *mm81x_hif_get_tx_cmd_queue(struct mm81x *mors)
+{
+	return mors->hif.ops->get_tx_cmd_queue(mors);
+}
+
+static inline struct mm81x_skbq *
+mm81x_hif_get_tx_beacon_queue(struct mm81x *mors)
+{
+	return mors->hif.ops->get_tx_beacon_queue(mors);
+}
+
+static inline struct mm81x_skbq *mm81x_hif_get_tx_mgmt_queue(struct mm81x *mors)
+{
+	return mors->hif.ops->get_tx_mgmt_queue(mors);
+}
+
+static inline struct mm81x_skbq *mm81x_hif_get_tx_data_queue(struct mm81x *mors,
+							     int aci)
+{
+	return mors->hif.ops->get_tx_data_queue(mors, aci);
+}
+
+static inline int mm81x_hif_handle_irq(struct mm81x *mors, u32 status)
+{
+	return mors->hif.ops->handle_irq(mors, status);
+}
+
+static inline int mm81x_hif_get_tx_buffered_count(struct mm81x *mors)
+{
+	return mors->hif.ops->get_tx_buffered_count(mors);
+}
+
+static inline int mm81x_hif_get_tx_status_pending_count(struct mm81x *mors)
+{
+	return mors->hif.ops->get_tx_status_pending_count(mors);
+}
+
+#endif /* _MM81X_HIF_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 12/33] wifi: mm81x: add hw.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/hw.c | 367 +++++++++++++++++++++
 1 file changed, 367 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/hw.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/hw.c b/drivers/net/wireless/morsemicro/mm81x/hw.c
new file mode 100644
index 000000000000..081360455e7e
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/hw.c
@@ -0,0 +1,367 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/firmware.h>
+#include <linux/delay.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+#include <linux/gpio.h>
+#include "hif.h"
+#include "mac.h"
+#include "bus.h"
+#include "core.h"
+#include "fw.h"
+#include "yaps.h"
+
+#define MM8108_REG_HOST_MAGIC_VALUE 0xDEADBEEF
+#define MM8108_REG_RESET_VALUE 0xDEAD
+
+#define MM8108_REG_SDIO_DEVICE_ADDR 0x0000207C
+
+#define MM8108_REG_SDIO_DEVICE_BURST_OFFSET 9
+#define MM8108_REG_TRGR_BASE 0x00003c00
+#define MM8108_REG_INT_BASE 0x00003c50
+#define MM8108_REG_MSI_ADDRESS 0x00004100
+#define MM8108_REG_MSI_VALUE 0x1
+#define MM8108_REG_MANIFEST_PTR_ADDRESS 0x00002d40
+#define MM8108_REG_APPS_BOOT_ADDR 0x00002084
+#define MM8108_REG_RESET 0x000020AC
+#define MM8108_REG_AON_COUNT 2
+
+#define MM8108_REG_AON_ADDR 0x00002114
+#define MM8108_REG_AON_LATCH_ADDR 0x00405020
+#define MM8108_REG_AON_LATCH_MASK 0x1
+#define MM8108_REG_AON_RESET_USB_VALUE 0x8
+#define MM8108_APPS_MAC_DMEM_ADDR_START 0x00100000
+
+#define MM8108_REG_RC_CLK_POWER_OFF_ADDR 0x00405020
+#define MM8108_REG_RC_CLK_POWER_OFF_MASK 0x00000040
+#define MM8108_SLOW_RC_POWER_ON_DELAY_MS 2
+
+#define MM8108_RESET_DELAY_TIME_MS 400
+
+#define MM8108_REG_OTPCTRL_PLDO 0x00004014
+#define MM8108_REG_OTPCTRL_PENVDD2 0x00004010
+#define MM8108_REG_OTPCTRL_PDSTB 0x00004018
+#define MM8108_REG_OTPCTRL_PTM 0x0000401c
+#define MM8108_REG_OTPCTRL_PCE 0x00004020
+#define MM8108_REG_OTPCTRL_PA 0x00004034
+#define MM8108_REG_OTPCTRL_PECCRDB 0x00004048
+#define MM8108_REG_OTPCTRL_ACTION_AUTO_RD_START 0x0000400c
+#define MM8108_REG_OTPCTRL_PDOUT 0x00004040
+
+#define MM81X_OTP_MAC_ADDR_2_BANK_NUM 27
+#define MM81X_OTP_MAC_ADDR_1_BANK_NUM 26
+#define MM81X_OTP_MAC_ADDR_1_MASK GENMASK(31, 16)
+#define MM81X_OTP_BOARD_TYPE_BANK_NUM 26
+#define MM81X_OTP_BOARD_TYPE_MASK GENMASK(15, 0)
+
+#define MM810X_BOARD_TYPE_MAX_VALUE (MM81X_OTP_BOARD_TYPE_MASK - 1)
+
+static void mm81x_hw_otp_power_up(struct mm81x *mors)
+{
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PENVDD2, 1);
+	udelay(2);
+
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PLDO, 1);
+	usleep_range(10, 20);
+
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PDSTB, 1);
+	udelay(3);
+}
+
+static void mm81x_hw_otp_power_down(struct mm81x *mors)
+{
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PDSTB, 0);
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PLDO, 0);
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PENVDD2, 0);
+}
+
+static void mm81x_hw_otp_read_enable(struct mm81x *mors)
+{
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PTM, 0);
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PCE, 1);
+	usleep_range(10, 20);
+}
+
+static void mm81x_hw_otp_read_disable(struct mm81x *mors)
+{
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PCE, 0);
+	udelay(1);
+}
+
+static int mm81x_hw_otp_read(struct mm81x *mors, u8 bank_num, u32 *buf,
+			     u8 ignore_ecc)
+{
+	u32 auto_rd_start_tmp;
+	u32 auto_rd_start = 1;
+	int i;
+
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PA, bank_num);
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_PECCRDB, ignore_ecc);
+
+	mm81x_reg32_read(mors, MM8108_REG_OTPCTRL_ACTION_AUTO_RD_START,
+			 &auto_rd_start_tmp);
+	auto_rd_start_tmp &= 0xfffffffe;
+
+	mm81x_reg32_write(mors, MM8108_REG_OTPCTRL_ACTION_AUTO_RD_START,
+			  auto_rd_start | auto_rd_start_tmp);
+
+	/* Attempt reading up to 5 times. */
+	for (i = 0; i < 5 && auto_rd_start; i++) {
+		usleep_range(15, 20);
+		mm81x_reg32_read(mors, MM8108_REG_OTPCTRL_ACTION_AUTO_RD_START,
+				 &auto_rd_start_tmp);
+		auto_rd_start = auto_rd_start_tmp & 0x1;
+	}
+
+	if (i == 5)
+		return -EIO;
+
+	mm81x_reg32_read(mors, MM8108_REG_OTPCTRL_PDOUT, buf);
+
+	return 0;
+}
+
+int mm81x_hw_otp_get_board_type(struct mm81x *mors)
+{
+	int board_type = 0;
+	u32 otp_word = 0;
+	int ret;
+
+	mm81x_claim_bus(mors);
+	mm81x_hw_otp_power_up(mors);
+	mm81x_hw_otp_read_enable(mors);
+
+	ret = mm81x_hw_otp_read(mors, MM81X_OTP_BOARD_TYPE_BANK_NUM, &otp_word,
+				1);
+
+	mm81x_hw_otp_read_disable(mors);
+	mm81x_hw_otp_power_down(mors);
+	mm81x_release_bus(mors);
+
+	if (ret)
+		return -EINVAL;
+
+	board_type = otp_word & MM81X_OTP_BOARD_TYPE_MASK;
+
+	return board_type;
+}
+
+bool mm81x_hw_otp_valid_board_type(u32 board_type)
+{
+	return board_type > 0 && board_type < MM810X_BOARD_TYPE_MAX_VALUE;
+}
+
+int mm81x_hw_otp_get_mac_addr(struct mm81x *mors)
+{
+	u32 mac1 = 0;
+	u32 mac2 = 0;
+	int ret = 0;
+
+	mm81x_claim_bus(mors);
+	mm81x_hw_otp_power_up(mors);
+	mm81x_hw_otp_read_enable(mors);
+
+	ret = mm81x_hw_otp_read(mors, MM81X_OTP_MAC_ADDR_1_BANK_NUM, &mac1, 1);
+	if (ret)
+		goto exit;
+
+	ret = mm81x_hw_otp_read(mors, MM81X_OTP_MAC_ADDR_2_BANK_NUM, &mac2, 1);
+	if (ret)
+		goto exit;
+
+	put_unaligned_le16((mac1 & MM81X_OTP_MAC_ADDR_1_MASK) >> 16,
+			   &mors->macaddr[0]);
+	put_unaligned_le32(mac2, &mors->macaddr[2]);
+
+exit:
+	mm81x_hw_otp_read_disable(mors);
+	mm81x_hw_otp_power_down(mors);
+	mm81x_release_bus(mors);
+
+	return ret;
+}
+
+void mm81x_hw_irq_enable(struct mm81x *mors, u32 irq, bool enable)
+{
+	u32 irq_en, irq_en_addr = irq < 32 ? MM81X_REG_INT1_EN(mors) :
+					     MM81X_REG_INT2_EN(mors);
+	u32 irq_clr_addr = irq < 32 ? MM81X_REG_INT1_CLR(mors) :
+				      MM81X_REG_INT2_CLR(mors);
+	u32 mask = irq < 32 ? (1 << irq) : (1 << (irq - 32));
+
+	mm81x_claim_bus(mors);
+	mm81x_reg32_read(mors, irq_en_addr, &irq_en);
+	if (enable)
+		irq_en |= (mask);
+	else
+		irq_en &= ~(mask);
+	mm81x_reg32_write(mors, irq_clr_addr, mask);
+	mm81x_reg32_write(mors, irq_en_addr, irq_en);
+	mm81x_release_bus(mors);
+}
+
+int mm81x_hw_irq_handle(struct mm81x *mors)
+{
+	u32 status1 = 0;
+
+	mm81x_reg32_read(mors, MM81X_REG_INT1_STS(mors), &status1);
+
+	if (status1 & MM81X_HIF_IRQ_MASK_ALL)
+		mm81x_hif_handle_irq(mors, status1);
+
+	if (status1 & MM81X_INT_BEACON_VIF_MASK_ALL)
+		mm81x_mac_beacon_irq_handle(mors, status1);
+
+	mm81x_reg32_write(mors, MM81X_REG_INT1_CLR(mors), status1);
+
+	return status1 ? 1 : 0;
+}
+EXPORT_SYMBOL_GPL(mm81x_hw_irq_handle);
+
+void mm81x_hw_irq_clear(struct mm81x *mors)
+{
+	mm81x_claim_bus(mors);
+	mm81x_reg32_write(mors, MM81X_REG_INT1_CLR(mors), 0xFFFFFFFF);
+	mm81x_reg32_write(mors, MM81X_REG_INT2_CLR(mors), 0xFFFFFFFF);
+	mm81x_release_bus(mors);
+}
+
+void mm81x_hw_toggle_aon_latch(struct mm81x *mors)
+{
+	u32 address = MM81X_REG_AON_LATCH_ADDR(mors);
+	u32 mask = MM81X_REG_AON_LATCH_MASK(mors);
+	u32 latch;
+
+	mm81x_reg32_read(mors, address, &latch);
+	mm81x_reg32_write(mors, address, latch & ~(mask));
+	mdelay(5);
+	mm81x_reg32_write(mors, address, latch | mask);
+	mdelay(5);
+	mm81x_reg32_write(mors, address, latch & ~(mask));
+	mdelay(5);
+}
+
+void mm81x_hw_enable_stop_notifications(struct mm81x *mors, bool enable)
+{
+	mm81x_hw_irq_enable(mors, MM81X_INT_HW_STOP_NOTIFICATION_NUM, enable);
+}
+
+void mm81x_hw_enable_burst_mode(struct mm81x *mors, const u8 burst_mode)
+{
+	u32 reg32_value;
+
+	mm81x_claim_bus(mors);
+	if (mm81x_reg32_read(mors, MM8108_REG_SDIO_DEVICE_ADDR, &reg32_value))
+		goto end;
+
+	reg32_value &= ~(u32)(SDIO_WORD_BURST_MASK
+			      << MM8108_REG_SDIO_DEVICE_BURST_OFFSET);
+	reg32_value |= (u32)(burst_mode << MM8108_REG_SDIO_DEVICE_BURST_OFFSET);
+
+	dev_dbg(mors->dev,
+		"Setting Burst mode to %d Writing 0x%08X to the register",
+		burst_mode, reg32_value);
+
+	if (mm81x_reg32_write(mors, MM8108_REG_SDIO_DEVICE_ADDR, reg32_value))
+		goto end;
+
+end:
+	mm81x_release_bus(mors);
+}
+EXPORT_SYMBOL_GPL(mm81x_hw_enable_burst_mode);
+
+static int mm81x_hw_enable_internal_slow_clock(struct mm81x *mors)
+{
+	u32 rc_clock_reg_value;
+	int ret = 0;
+
+	dev_dbg(mors->dev, "Enabling internal slow clock");
+
+	ret = mm81x_reg32_read(mors, MM8108_REG_RC_CLK_POWER_OFF_ADDR,
+			       &rc_clock_reg_value);
+	if (ret)
+		goto exit;
+
+	rc_clock_reg_value &= ~MM8108_REG_RC_CLK_POWER_OFF_MASK;
+	ret = mm81x_reg32_write(mors, MM8108_REG_RC_CLK_POWER_OFF_ADDR,
+				rc_clock_reg_value);
+	if (ret)
+		goto exit;
+
+	mm81x_hw_toggle_aon_latch(mors);
+
+	/* Wait for the clock to turn on and settle */
+	mdelay(MM8108_SLOW_RC_POWER_ON_DELAY_MS);
+exit:
+	return ret;
+}
+
+int mm81x_hw_digital_reset(struct mm81x *mors)
+{
+	int ret = 0;
+
+	mm81x_claim_bus(mors);
+
+	/* This should be the first step in digital reset, do not reorder */
+	ret = mm81x_hw_enable_internal_slow_clock(mors);
+	if (ret)
+		goto exit;
+
+	if (mors->bus_type == MM81X_BUS_TYPE_USB) {
+		ret = mm81x_bus_digital_reset(mors);
+		goto usb_done;
+	}
+
+	if (MM81X_REG_RESET(mors) != 0)
+		ret = mm81x_reg32_write(mors, MM81X_REG_RESET(mors),
+					MM81X_REG_RESET_VALUE(mors));
+
+usb_done:
+	msleep(MM8108_RESET_DELAY_TIME_MS);
+exit:
+	mm81x_release_bus(mors);
+
+	if (!ret)
+		mors->chip_was_reset = true;
+
+	return ret;
+}
+
+void mm81x_hw_pre_firmware_ndr_hook(struct mm81x *mors)
+{
+	/* We need disable bursting for firmware download/init procedure */
+	mm81x_bus_config_burst_mode(mors, false);
+}
+
+void mm81x_hw_post_firmware_ndr_hook(struct mm81x *mors)
+{
+	/* We are safe here to re-enable bursting again, if supported */
+	mm81x_bus_config_burst_mode(mors, true);
+}
+
+const struct mm81x_regs mm8108_regs = {
+	.chip_id_address = MM8108_REG_CHIP_ID,
+	.irq_base_address = MM8108_REG_INT_BASE,
+	.trgr_base_address = MM8108_REG_TRGR_BASE,
+	.cpu_reset_address = MM8108_REG_RESET,
+	.cpu_reset_value = MM8108_REG_RESET_VALUE,
+	.manifest_ptr_address = MM8108_REG_MANIFEST_PTR_ADDRESS,
+	.msi_address = MM8108_REG_MSI_ADDRESS,
+	.msi_value = MM8108_REG_MSI_VALUE,
+	.magic_num_value = MM8108_REG_HOST_MAGIC_VALUE,
+	.early_clk_ctrl_value = 0,
+	.pager_base_address = MM8108_APPS_MAC_DMEM_ADDR_START,
+	.aon_latch = MM8108_REG_AON_LATCH_ADDR,
+	.aon_latch_mask = MM8108_REG_AON_LATCH_MASK,
+	.aon_reset_usb_value = MM8108_REG_AON_RESET_USB_VALUE,
+	.aon = MM8108_REG_AON_ADDR,
+	.aon_count = MM8108_REG_AON_COUNT,
+	.boot_address = MM8108_REG_APPS_BOOT_ADDR,
+};
+
+/* B2 ROM_LINKED */
+MODULE_FIRMWARE(MM81X_FW_DIR "/" MM8108_FW_BASE MM8108B2_REV_STRING
+			FW_ROM_LINKED_STRING MM81X_FW_EXT);
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 13/33] wifi: mm81x: add hw.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/hw.h | 176 +++++++++++++++++++++
 1 file changed, 176 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/hw.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/hw.h b/drivers/net/wireless/morsemicro/mm81x/hw.h
new file mode 100644
index 000000000000..e22948f037bf
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/hw.h
@@ -0,0 +1,176 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_HW_H_
+#define _MM81X_HW_H_
+
+#include <linux/gpio/consumer.h>
+#include "core.h"
+#include "command_defs.h"
+
+/* This should be at a fixed location for a family of chipset */
+#define MM8108_REG_CHIP_ID 0x00002d20
+
+#define MM81X_SDIO_RW_ADDR_BOUNDARY_MASK ((u32)0xFFFF0000)
+
+#define MM81X_CONFIG_ACCESS_1BYTE 0
+#define MM81X_CONFIG_ACCESS_2BYTE 1
+#define MM81X_CONFIG_ACCESS_4BYTE 2
+
+#define MM81X_REG_TRGR_BASE(mors) ((mors)->regs->trgr_base_address)
+#define MM81X_REG_TRGR1_STS(mors) (MM81X_REG_TRGR_BASE(mors) + 0x00)
+#define MM81X_REG_TRGR1_SET(mors) (MM81X_REG_TRGR_BASE(mors) + 0x04)
+#define MM81X_REG_TRGR1_CLR(mors) (MM81X_REG_TRGR_BASE(mors) + 0x08)
+#define MM81X_REG_TRGR1_EN(mors) (MM81X_REG_TRGR_BASE(mors) + 0x0C)
+#define MM81X_REG_TRGR2_STS(mors) (MM81X_REG_TRGR_BASE(mors) + 0x10)
+#define MM81X_REG_TRGR2_SET(mors) (MM81X_REG_TRGR_BASE(mors) + 0x14)
+#define MM81X_REG_TRGR2_CLR(mors) (MM81X_REG_TRGR_BASE(mors) + 0x18)
+#define MM81X_REG_TRGR2_EN(mors) (MM81X_REG_TRGR_BASE(mors) + 0x1C)
+
+#define MM81X_REG_INT_BASE(mors) ((mors)->regs->irq_base_address)
+#define MM81X_REG_INT1_STS(mors) (MM81X_REG_INT_BASE(mors) + 0x00)
+#define MM81X_REG_INT1_SET(mors) (MM81X_REG_INT_BASE(mors) + 0x04)
+#define MM81X_REG_INT1_CLR(mors) (MM81X_REG_INT_BASE(mors) + 0x08)
+#define MM81X_REG_INT1_EN(mors) (MM81X_REG_INT_BASE(mors) + 0x0C)
+#define MM81X_REG_INT2_STS(mors) (MM81X_REG_INT_BASE(mors) + 0x10)
+#define MM81X_REG_INT2_SET(mors) (MM81X_REG_INT_BASE(mors) + 0x14)
+#define MM81X_REG_INT2_CLR(mors) (MM81X_REG_INT_BASE(mors) + 0x18)
+#define MM81X_REG_INT2_EN(mors) (MM81X_REG_INT_BASE(mors) + 0x1C)
+
+#define MM81X_REG_CHIP_ID(mors) ((mors)->regs->chip_id_address)
+
+#define MM81X_REG_MSI(mors) ((mors)->regs->msi_address)
+#define MM81X_REG_MSI_HOST_INT(mors) ((mors)->regs->msi_value)
+
+#define MM81X_REG_HOST_MAGIC_VALUE(mors) ((mors)->regs->magic_num_value)
+
+#define MM81X_REG_RESET(mors) ((mors)->regs->cpu_reset_address)
+#define MM81X_REG_RESET_VALUE(mors) ((mors)->regs->cpu_reset_value)
+
+#define MM81X_REG_HOST_MANIFEST_PTR(mors) ((mors)->regs->manifest_ptr_address)
+
+#define MM81X_REG_EARLY_CLK_CTRL_VALUE(mors) \
+	((mors)->regs->early_clk_ctrl_value)
+
+#define MM81X_REG_CLK_CTRL(mors) ((mors)->regs->clk_ctrl_address)
+#define MM81X_REG_CLK_CTRL_VALUE(mors) ((mors)->regs->clk_ctrl_value)
+
+#define MM81X_REG_BOOT_ADDR(mors) ((mors)->regs->boot_address)
+#define MM81X_REG_BOOT_ADDR_VALUE(mors) ((mors)->regs->boot_value)
+
+#define MM81X_REG_AON_ADDR(mors) ((mors)->regs->aon)
+#define MM81X_REG_AON_COUNT(mors) ((mors)->regs->aon_count)
+#define MM81X_REG_AON_LATCH_ADDR(mors) ((mors)->regs->aon_latch)
+#define MM81X_REG_AON_LATCH_MASK(mors) ((mors)->regs->aon_latch_mask)
+#define MM81X_REG_AON_USB_RESET(mors) ((mors)->regs->aon_reset_usb_value)
+
+/* Bit 17 to 24 reserved for the beacon VIF 0 to 7 interrupts */
+#define MM81X_INT_BEACON_VIF_MASK_ALL (GENMASK(24, 17))
+#define MM81X_INT_BEACON_BASE_NUM (17)
+
+/* PV0 NDP probe interrupts (VIF 0 and 1). */
+#define MM81X_INT_NDP_PROBE_REQ_PV0_VIF_MASK_ALL (GENMASK(26, 25))
+#define MM81X_INT_NDP_PROBE_REQ_PV0_BASE_NUM (25)
+
+/* Bit 27 Chip to Host stop notify */
+#define MM81X_INT_HW_STOP_NOTIFICATION_NUM (27)
+#define MM81X_INT_HW_STOP_NOTIFICATION BIT(MM81X_INT_HW_STOP_NOTIFICATION_NUM)
+
+#define CHIP_TYPE_SILICON 0x0
+
+/* Chip ID */
+#define MM8108XX_ID 0x9
+
+/* Chip Rev */
+#define MM8108B2_REV 0x8
+
+/* Chip Rev String */
+#define MM8108B_STRING "b"
+#define MM8108B2_REV_STRING MM8108B_STRING "2"
+
+/* Chip ID for MM8108 */
+#define MM8108B2_ID \
+	MM81X_DEVICE_ID(MM8108XX_ID, MM8108B2_REV, CHIP_TYPE_SILICON)
+
+#define FW_RAM_ONLY_STRING ""
+#define FW_ROM_LINKED_STRING "-rl"
+#define FW_ROM_ALL_STRING "-ro"
+
+/*
+ * Minimum time we must wait between attempting to reload the HW after a
+ * stop notification
+ */
+#define HW_RELOAD_AFTER_STOP_WINDOW 5
+
+enum host_table_firmware_flags {
+	MM81X_FW_FLAGS_SUPPORT_S1G = BIT(0),
+	MM81X_FW_FLAGS_BUSY_ACTIVE_LOW = BIT(1),
+	MM81X_FW_FLAGS_REPORTS_TX_BEACON_COMPLETION = BIT(2),
+	MM81X_FW_FLAGS_SUPPORT_HW_SCAN = BIT(3),
+	MM81X_FW_FLAGS_SUPPORT_CHIP_HALT_IRQ = BIT(4),
+};
+
+struct host_table {
+	__le32 magic_number;
+	__le32 fw_version_number;
+	__le32 host_flags;
+	__le32 firmware_flags;
+	__le32 memcmd_cmd_addr;
+	__le32 memcmd_resp_addr;
+	__le32 ext_host_tbl_addr;
+} __packed;
+
+struct mm81x_regs {
+	u32 chip_id_address;
+	u32 irq_base_address;
+	u32 trgr_base_address;
+	u32 cpu_reset_address;
+	u32 cpu_reset_value;
+	u32 msi_address;
+	u32 msi_value;
+	u32 manifest_ptr_address;
+	u32 magic_num_value;
+	u32 clk_ctrl_address;
+	u32 clk_ctrl_value;
+	u32 early_clk_ctrl_value;
+	u32 boot_address;
+	u32 boot_value;
+	u32 pager_base_address;
+	u32 aon_latch;
+	u32 aon_latch_mask;
+	u32 aon_reset_usb_value;
+	u32 aon;
+	u8 aon_count;
+};
+
+int mm81x_hw_otp_get_board_type(struct mm81x *mors);
+bool mm81x_hw_otp_valid_board_type(u32 board_type);
+int mm81x_hw_otp_get_mac_addr(struct mm81x *mors);
+
+void mm81x_hw_irq_enable(struct mm81x *mors, u32 irq, bool enable);
+int mm81x_hw_irq_handle(struct mm81x *mors);
+void mm81x_hw_irq_clear(struct mm81x *mors);
+void mm81x_hw_toggle_aon_latch(struct mm81x *mors);
+void mm81x_hw_enable_burst_mode(struct mm81x *mors, const u8 burst_mode);
+int mm81x_hw_digital_reset(struct mm81x *mors);
+void mm81x_hw_pre_firmware_ndr_hook(struct mm81x *mors);
+void mm81x_hw_post_firmware_ndr_hook(struct mm81x *mors);
+
+enum sdio_burst_mode {
+	SDIO_WORD_BURST_DISABLE =
+		0, /* Intentionally duplicate to make it clear it's disabled */
+	SDIO_WORD_BURST_SIZE_0 = 0, /* 000: no bursting (single 32bit word) */
+	SDIO_WORD_BURST_SIZE_2 = 1, /* 001: bursts of 2 words */
+	SDIO_WORD_BURST_SIZE_4 = 2, /* 010: bursts of 4 words */
+	SDIO_WORD_BURST_SIZE_8 = 3, /* 011: bursts of 8 words */
+	SDIO_WORD_BURST_SIZE_16 = 4, /* 100: bursts of 16 words */
+	SDIO_WORD_BURST_MASK = 7,
+};
+
+extern const struct mm81x_regs mm8108_regs;
+
+void mm81x_hw_enable_stop_notifications(struct mm81x *mors, bool enable);
+
+#endif /* !_MM81X_HW_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 14/33] wifi: mm81x: add mac.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge,
	Nathan Chancellor, Nick Desaulniers, Bill Wendling, Justin Stitt
  Cc: ayman.grais, linux-wireless, linux-kernel, llvm
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/mac.c | 2443 +++++++++++++++++++
 1 file changed, 2443 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/mac.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/mac.c b/drivers/net/wireless/morsemicro/mm81x/mac.c
new file mode 100644
index 000000000000..71d559be71ff
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/mac.c
@@ -0,0 +1,2443 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include "core.h"
+#include <linux/slab.h>
+#include <linux/jiffies.h>
+#include <linux/crc32.h>
+#include <net/mac80211.h>
+#include <asm/div64.h>
+#include <linux/kernel.h>
+#include "hif.h"
+#include "mac.h"
+#include "bus.h"
+#include "ps.h"
+#include "rc.h"
+
+/*
+ * Arbitrary size limit for the filter command address list, to ensure that
+ * the command does not exceed page/MTU size. This will be far greater than
+ * the number of filters supported by the firmware.
+ */
+#define MCAST_FILTER_COUNT_MAX (1024 / sizeof(filter->addr_list[0]))
+
+/* Calculate average RSSI for Rx status */
+#define CALC_AVG_RSSI(_avg, _sample) ((((_avg) * 9 + (_sample)) / 10))
+
+/*
+ * When automatically trying MCS0 before MCS10, this is how many
+ * MCS0 attempts to make
+ */
+#define MCS0_BEFORE_MCS10_COUNT (1)
+
+/* Maximum TX power (default) */
+#define MAX_TX_POWER_MBM (2200)
+
+/*
+ * Since S1G runs at 1/10th the clockrate of VHT, the worst-case
+ * transmission time is significantly longer then that of non-S1G
+ * PHYs.
+ */
+#define MM81X_FLUSH_TIMEOUT (16 * HZ)
+
+/* Default queue count */
+#define MM81X_HW_QUEUE_COUNT (4)
+
+/* Max rates per skb */
+#define MM81X_HW_MAX_RATES (4)
+
+/* Max reported rates */
+#define MM81X_HW_MAX_REPORT_RATES (4)
+
+/* Max rate attempts */
+#define MM81X_HW_MAX_RATE_TRIES (1)
+
+/* Max sk pacing shift */
+#define MM81X_HW_TX_SK_PACING_SHIFT (3)
+
+/* NSS/MCS map values */
+#define MM81X_NSS_MCS_BYTE_0 0xfe /* 1SS */
+#define MM81X_NSS_MCS_BYTE_1 0x00
+#define MM81X_NSS_MCS_BYTE_2 0xfc /* 1SS */
+#define MM81X_NSS_MCS_BYTE_3 0x01
+#define MM81X_NSS_MCS_BYTE_4 0x00
+
+/* HW restart delay time before terminating hardware IF work items */
+#define MM81X_HW_RESTART_DELAY_MS 20
+
+/* clang-format off */
+
+/* mm81x chips do not support 16MHz */
+#define CHANS1G(channel, frequency, offset, chan_flags)		\
+{								\
+	.band = NL80211_BAND_S1GHZ,				\
+	.center_freq = (frequency),				\
+	.freq_offset = (offset),				\
+	.hw_value = (channel),					\
+	.flags = ((chan_flags) | IEEE80211_CHAN_NO_16MHZ),	\
+	.max_antenna_gain = 0,					\
+	.max_power = 30,					\
+}
+
+static struct ieee80211_channel mors_s1ghz_channels[] = {
+	CHANS1G(1, 902, 500, IEEE80211_CHAN_S1G_NO_PRIMARY),
+	CHANS1G(3, 903, 500, 0),
+	CHANS1G(5, 904, 500, 0),
+	CHANS1G(7, 905, 500, 0),
+	CHANS1G(9, 906, 500, 0),
+	CHANS1G(11, 907, 500, 0),
+	CHANS1G(13, 908, 500, 0),
+	CHANS1G(15, 909, 500, 0),
+	CHANS1G(17, 910, 500, 0),
+	CHANS1G(19, 911, 500, 0),
+	CHANS1G(21, 912, 500, 0),
+	CHANS1G(23, 913, 500, 0),
+	CHANS1G(25, 914, 500, 0),
+	CHANS1G(27, 915, 500, 0),
+	CHANS1G(29, 916, 500, 0),
+	CHANS1G(31, 917, 500, 0),
+	CHANS1G(33, 918, 500, 0),
+	CHANS1G(35, 919, 500, 0),
+	CHANS1G(37, 920, 500, 0),
+	CHANS1G(39, 921, 500, 0),
+	CHANS1G(41, 922, 500, 0),
+	CHANS1G(43, 923, 500, 0),
+	CHANS1G(45, 924, 500, 0),
+	CHANS1G(47, 925, 500, 0),
+	CHANS1G(49, 926, 500, 0),
+	CHANS1G(51, 927, 500, IEEE80211_CHAN_S1G_NO_PRIMARY),
+};
+
+/* clang-format on */
+
+static struct ieee80211_supported_band mors_band_s1ghz = {
+	.band = NL80211_BAND_S1GHZ,
+	.s1g_cap.s1g = true,
+	.channels = mors_s1ghz_channels,
+	.n_channels = ARRAY_SIZE(mors_s1ghz_channels),
+	.bitrates = NULL,
+	.n_bitrates = 0,
+	.s1g_cap.cap[4] = 0x80 /* STA type sensor only for AP & STA */
+};
+
+static struct ieee80211_iface_limit mors_if_limits[] = {
+	{
+		.max = MM81X_MAX_IF,
+		.types = BIT(NL80211_IFTYPE_STATION) | BIT(NL80211_IFTYPE_AP),
+	},
+};
+
+static struct ieee80211_iface_combination mors_if_combs[] = {
+	{
+		.limits = mors_if_limits,
+		.n_limits = ARRAY_SIZE(mors_if_limits),
+		.max_interfaces = MM81X_MAX_IF,
+		.num_different_channels = 1,
+	},
+};
+
+/* Convert from a time in time units (1024us) to us */
+#define MM81X_TU_TO_US(x) ((x) * 1024UL)
+
+/* Convert from a time in time units (1024us) to ms */
+#define MM81X_TU_TO_MS(x) (MM81X_TU_TO_US(x) / 1000UL)
+
+/* Default time to dwell on a scan channel */
+#define MM81X_HWSCAN_DEFAULT_DWELL_TIME_MS (30)
+
+/* Default time to dwell on a scan channel for passive scan */
+#define MM81X_HWSCAN_DEFAULT_PASSIVE_DWELL_TIME_MS (110)
+
+/* Default time to dwell on home channel, in between scan channels */
+#define MM81X_HWSCAN_DEFAULT_DWELL_ON_HOME_MS (200)
+
+/* Typical time it takes to send the probe */
+#define MM81X_HWSCAN_PROBE_DELAY_MS (30)
+
+/* A margin to account for event/command processing */
+#define MM81X_HWSCAN_TIMEOUT_OVERHEAD_MS (2000)
+
+/* Scan channel frequency mask */
+#define HW_SCAN_CH_LIST_FREQ_KHZ GENMASK(19, 0)
+
+/*
+ * Scan channel bandwidth mask.
+ * Encoded as: 0 = 1MHz, 1 = 2MHz, 2 = 4MHz, 3 = 8MHz
+ */
+#define HW_SCAN_CH_LIST_OP_BW GENMASK(21, 20)
+
+/*
+ * Scan channel primary channel width.
+ * Encoded as: 0 = 1MHz, 1 = 2MHz
+ */
+#define HW_SCAN_CH_LIST_PRIM_CH_WIDTH BIT(22)
+
+/* Index into power_list for tx power of channel */
+#define HW_SCAN_CH_LIST_PWR_LIST_IDX GENMASK(31, 26)
+
+struct hw_scan_tlv_hdr {
+	__le16 tag;
+	__le16 len;
+} __packed;
+
+struct hw_scan_tlv_channel_list {
+	struct hw_scan_tlv_hdr hdr;
+	__le32 channels[];
+} __packed;
+
+struct hw_scan_tlv_power_list {
+	struct hw_scan_tlv_hdr hdr;
+	s32 tx_power_qdbm[];
+} __packed;
+
+struct hw_scan_tlv_probe_req {
+	struct hw_scan_tlv_hdr hdr;
+	/* Probe request frame template (including SSIDs) */
+	u8 buf[];
+} __packed;
+
+struct hw_scan_tlv_dwell_on_home {
+	struct hw_scan_tlv_hdr hdr;
+	/* Time to dwell on home between scan channels */
+	__le32 home_dwell_time_ms;
+} __packed;
+
+#define DOT11AH_BA_MAX_MPDU_PER_AMPDU (32)
+
+/* wiphy scan params */
+#define MM81X_MAX_SCAN_IE_LEN 512
+#define MM81X_MAX_SCAN_SSIDS 1
+#define MM81X_MAX_REMAIN_ON_CHAN_DURATION 10000
+
+static bool mm81x_reg_h_cc_equal(const char *cc1, const char *cc2)
+{
+	return (cc1[0] == cc2[0]) && (cc1[1] == cc2[1]);
+}
+
+static bool mm81x_tx_h_pkt_over_rts_threshold(struct mm81x *mors,
+					      struct ieee80211_tx_info *info,
+					      struct sk_buff *skb)
+{
+	u8 ccmp_len;
+
+	if (!info->control.hw_key)
+		return ((skb->len + FCS_LEN) > mors->rts_threshold);
+
+	if (info->control.hw_key->keylen == 32)
+		ccmp_len =
+			IEEE80211_CCMP_256_HDR_LEN + IEEE80211_CCMP_256_MIC_LEN;
+	else if (info->control.hw_key->keylen == 16)
+		ccmp_len = IEEE80211_CCMP_HDR_LEN + IEEE80211_CCMP_MIC_LEN;
+	else
+		ccmp_len = 0;
+
+	return ((skb->len + FCS_LEN + ccmp_len) > mors->rts_threshold);
+}
+
+static bool mm81x_tx_h_ps_filtered_for_sta(struct mm81x *mors,
+					   struct sk_buff *skb,
+					   struct ieee80211_sta *sta)
+{
+	struct mm81x_sta *mors_sta;
+	struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+
+	if (!sta)
+		return false;
+
+	mors_sta = (struct mm81x_sta *)sta->drv_priv;
+
+	if (!mors_sta->tx_ps_filter_en)
+		return false;
+
+	dev_dbg(mors->dev, "Frame for sta[%pM] PS filtered", mors_sta->addr);
+
+	info->flags |= IEEE80211_TX_STAT_TX_FILTERED;
+	info->flags &= ~IEEE80211_TX_CTL_AMPDU;
+
+	ieee80211_tx_status_skb(mors->hw, skb);
+	return true;
+}
+
+static void mm81x_mac_check_fw_disabled_chans(struct ieee80211_hw *hw)
+{
+	int ret = 0;
+	u32 i;
+	struct mm81x *mors = hw->priv;
+	struct host_cmd_resp_get_disabled_channels *resp;
+	u32 resp_len = sizeof(struct host_cmd_disabled_channel_entry) *
+			       ARRAY_SIZE(mors_s1ghz_channels) +
+		       sizeof(*resp);
+
+	resp = kzalloc(resp_len, GFP_KERNEL);
+	if (!resp) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	ret = mm81x_cmd_get_disabled_channels(mors, resp, resp_len);
+	if (ret)
+		goto out;
+
+	for (i = 0; i < ARRAY_SIZE(mors_s1ghz_channels); i++) {
+		struct ieee80211_channel *ch = &mors_s1ghz_channels[i];
+
+		if (ch->flags & IEEE80211_CHAN_DISABLED)
+			continue;
+
+		ch->flags &= ~IEEE80211_CHAN_S1G_NO_PRIMARY;
+	}
+
+	for (i = 0; i < le32_to_cpu(resp->n_channels); i++) {
+		struct ieee80211_channel *ch;
+		struct host_cmd_disabled_channel_entry *entry =
+			&resp->channels[i];
+
+		if (entry->bw_mhz != 1)
+			continue;
+
+		ch = ieee80211_get_channel_khz(
+			hw->wiphy,
+			KHZ100_TO_KHZ(le16_to_cpu(entry->freq_100khz)));
+		if (!ch)
+			continue;
+
+		ch->flags |= IEEE80211_CHAN_S1G_NO_PRIMARY;
+		dev_dbg(mors->dev, "set NO_PRIMARY on %u KHz",
+			ieee80211_channel_to_khz(ch));
+	}
+
+out:
+	if (ret)
+		dev_err(mors->dev, "failed to set disabled primary channels");
+
+	kfree(resp);
+}
+
+static int mm81x_mac_ops_start(struct ieee80211_hw *hw)
+{
+	struct mm81x *mors = hw->priv;
+
+	mors->started = true;
+	return 0;
+}
+
+static int mm81x_tx_h_get_max_bw(struct mm81x *mors)
+{
+	return MM81X_FW_SUPP(&mors->fw_caps, 8MHZ) ? 8 :
+	       MM81X_FW_SUPP(&mors->fw_caps, 4MHZ) ? 4 :
+	       MM81X_FW_SUPP(&mors->fw_caps, 2MHZ) ? 2 :
+						     1;
+}
+
+static void mm81x_mac_caps_init(struct mm81x *mors)
+{
+	struct mm81x_fw_caps *fw_caps = &mors->fw_caps;
+	struct ieee80211_sta_s1g_cap *s1g = &mors_band_s1ghz.s1g_cap;
+
+#define __FW_CAP_N(_n, _cap, _bit)                \
+	do {                                      \
+		if (MM81X_FW_SUPP(fw_caps, _cap)) \
+			s1g->cap[_n] |= (_bit);   \
+	} while (0)
+
+#define FW_CAP0(_cap, _bit) __FW_CAP_N(0, _cap, _bit)
+#define FW_CAP3(_cap, _bit) __FW_CAP_N(3, _cap, _bit)
+#define FW_CAP5(_cap, _bit) __FW_CAP_N(5, _cap, _bit)
+#define FW_CAP6(_cap, _bit) __FW_CAP_N(6, _cap, _bit)
+#define FW_CAP7(_cap, _bit) __FW_CAP_N(7, _cap, _bit)
+#define FW_CAP8(_cap, _bit) __FW_CAP_N(8, _cap, _bit)
+#define FW_CAP9(_cap, _bit) __FW_CAP_N(9, _cap, _bit)
+
+	FW_CAP0(S1G_LONG, S1G_CAP0_S1G_LONG);
+
+	s1g->cap[0] |= S1G_CAP0_SGI_1MHZ;
+	if (MM81X_FW_SUPP(fw_caps, SGI)) {
+		FW_CAP0(2MHZ, S1G_CAP0_SGI_2MHZ);
+		FW_CAP0(4MHZ, S1G_CAP0_SGI_4MHZ);
+		FW_CAP0(8MHZ, S1G_CAP0_SGI_8MHZ);
+	}
+
+	if (MM81X_FW_SUPP(fw_caps, 8MHZ))
+		s1g->cap[0] |= S1G_SUPP_CH_WIDTH_8;
+	else if (MM81X_FW_SUPP(fw_caps, 4MHZ))
+		s1g->cap[0] |= S1G_SUPP_CH_WIDTH_4;
+	else if (MM81X_FW_SUPP(fw_caps, 2MHZ))
+		s1g->cap[0] |= S1G_SUPP_CH_WIDTH_2;
+
+	FW_CAP3(RD_RESPONDER, S1G_CAP3_RD_RESPONDER);
+	FW_CAP3(LONG_MPDU, S1G_CAP3_MAX_MPDU_LEN);
+
+	FW_CAP5(AMSDU, S1G_CAP5_AMSDU);
+	FW_CAP5(AMPDU, S1G_CAP5_AMPDU);
+	FW_CAP5(ASYMMETRIC_BA_SUPPORT, S1G_CAP5_ASYMMETRIC_BA);
+	FW_CAP5(FLOW_CONTROL, S1G_CAP5_FLOW_CONTROL);
+
+	FW_CAP6(OBSS_MITIGATION, S1G_CAP6_OBSS_MITIGATION);
+	FW_CAP6(FRAGMENT_BA, S1G_CAP6_FRAGMENT_BA);
+	FW_CAP6(NDP_PSPOLL, S1G_CAP6_NDP_PS_POLL);
+	FW_CAP6(TXOP_SHARING_IMPLICIT_ACK, S1G_CAP6_TXOP_SHARING_IMP_ACK);
+	FW_CAP6(HTC_VHT_MFB, S1G_CAP6_VHT_LINK_ADAPT);
+
+	FW_CAP7(TACK_AS_PSPOLL, S1G_CAP7_TACK_AS_PS_POLL);
+	FW_CAP7(DUPLICATE_1MHZ, S1G_CAP7_DUP_1MHZ);
+	FW_CAP7(MCS_NEGOTIATION, S1G_CAP7_MCS_NEGOTIATION);
+	FW_CAP7(1MHZ_CONTROL_RESPONSE_PREAMBLE,
+		S1G_CAP7_1MHZ_CTL_RESPONSE_PREAMBLE);
+	FW_CAP7(SECTOR_TRAINING, S1G_CAP7_SECTOR_TRAINING_OPERATION);
+	FW_CAP7(TMP_PS_MODE_SWITCH, S1G_CAP7_TEMP_PS_MODE_SWITCH);
+
+	FW_CAP8(BDT, S1G_CAP8_BDT);
+
+	FW_CAP9(LINK_ADAPTATION_WO_NDP_CMAC,
+		S1G_CAP9_LINK_ADAPT_PER_CONTROL_RESPONSE);
+
+	/* 1SS MCS 9 for Rx / Tx map */
+	s1g->nss_mcs[0] = MM81X_NSS_MCS_BYTE_0;
+	s1g->nss_mcs[1] = MM81X_NSS_MCS_BYTE_1;
+	s1g->nss_mcs[2] = MM81X_NSS_MCS_BYTE_2;
+	s1g->nss_mcs[3] = MM81X_NSS_MCS_BYTE_3;
+	s1g->nss_mcs[4] = MM81X_NSS_MCS_BYTE_4;
+
+#undef FW_CAP0
+#undef FW_CAP3
+#undef FW_CAP5
+#undef FW_CAP6
+#undef FW_CAP7
+#undef FW_CAP8
+#undef FW_CAP9
+#undef __FW_CAP_N
+}
+
+static void mm81x_mac_beacon_irq_enable(struct mm81x_vif *mors_vif, bool enable)
+{
+	struct mm81x *mors = mm81x_vif_to_mors(mors_vif);
+	u8 beacon_irq_num = MM81X_INT_BEACON_BASE_NUM + mors_vif->id;
+
+	enable ? set_bit(beacon_irq_num, &mors->beacon_irqs_enabled) :
+		 clear_bit(beacon_irq_num, &mors->beacon_irqs_enabled);
+
+	mm81x_hw_irq_enable(mors, beacon_irq_num, enable);
+}
+
+static void mm81x_beacon_h_fill_tx_info(struct mm81x *mors,
+					struct mm81x_skb_tx_info *tx_info,
+					struct mm81x_vif *mors_vif,
+					int tx_bw_mhz)
+{
+	enum dot11_bandwidth bw_idx =
+		mm81x_ratecode_bw_mhz_to_bw_index(tx_bw_mhz);
+	enum mm81x_rate_preamble pream = MM81X_RATE_PREAMBLE_S1G_SHORT;
+
+	tx_info->flags |=
+		cpu_to_le32(MM81X_TX_CONF_FLAGS_VIF_ID_SET(mors_vif->id));
+
+	if (bw_idx == DOT11_BANDWIDTH_1MHZ)
+		pream = MM81X_RATE_PREAMBLE_S1G_1M;
+
+	tx_info->rates[0].count = 1;
+	tx_info->rates[1].count = 0;
+	tx_info->rates[0].mm81x_ratecode =
+		mm81x_ratecode_init(bw_idx, 0, 0, pream);
+
+	if (mors->firmware_flags & MM81X_FW_FLAGS_REPORTS_TX_BEACON_COMPLETION)
+		tx_info->flags |=
+			cpu_to_le32(MM81X_TX_CONF_FLAGS_IMMEDIATE_REPORT);
+}
+
+static void mm81x_mac_beacon_work(struct work_struct *work)
+{
+	struct mm81x_vif *mors_vif =
+		from_work(mors_vif, work, u.ap.beacon_work);
+	struct mm81x *mors = mm81x_vif_to_mors(mors_vif);
+	struct mm81x_skbq *mq;
+	struct sk_buff *beacon;
+	struct ieee80211_vif *vif = mm81x_vif_to_ieee80211_vif(mors_vif);
+	struct mm81x_skb_tx_info tx_info = { 0 };
+	int num_bcn_vifs = atomic_read(&mors->num_bcn_vifs);
+
+	mq = mm81x_hif_get_tx_beacon_queue(mors);
+	if (!mq) {
+		dev_err(mors->dev, "no matching beacon Q found");
+		return;
+	}
+
+	if (mm81x_skbq_count(mq) >= num_bcn_vifs) {
+		dev_err(mors->dev,
+			"previous beacon not consumed, dropping req [id:%d]",
+			mors_vif->id);
+		return;
+	}
+
+	beacon = ieee80211_beacon_get(mors->hw, vif, false);
+	if (!beacon)
+		return;
+
+	mm81x_beacon_h_fill_tx_info(mors, &tx_info, mors_vif,
+				    cfg80211_chandef_s1g_pri_width(&mors->chandef));
+	mm81x_skbq_skb_tx(mq, &beacon, &tx_info, MM81X_SKB_CHAN_BEACON);
+}
+
+void mm81x_mac_beacon_irq_handle(struct mm81x *mors, u32 status)
+{
+	int vif_id;
+	unsigned long masked_status = (status & mors->beacon_irqs_enabled) >>
+				      MM81X_INT_BEACON_BASE_NUM;
+
+	guard(rcu)();
+	for_each_set_bit(vif_id, &masked_status, MM81X_MAX_IF) {
+		struct mm81x_vif *mors_vif;
+		struct ieee80211_vif *vif;
+
+		vif = mm81x_rcu_dereference_vif_id(mors, vif_id, true);
+		if (vif) {
+			mors_vif = ieee80211_vif_to_mors_vif(vif);
+			queue_work(system_bh_wq, &mors_vif->u.ap.beacon_work);
+		}
+	}
+}
+
+static void mm81x_mac_beacon_init(struct mm81x_vif *mors_vif)
+{
+	struct mm81x *mors = mm81x_vif_to_mors(mors_vif);
+
+	INIT_WORK(&mors_vif->u.ap.beacon_work, mm81x_mac_beacon_work);
+	mm81x_mac_beacon_irq_enable(mors_vif, true);
+	atomic_inc(&mors->num_bcn_vifs);
+}
+
+static struct hw_scan_tlv_hdr mm81x_hw_scan_h_pack_tlv_hdr(u16 tag, u16 len)
+{
+	struct hw_scan_tlv_hdr hdr = { .tag = cpu_to_le16(tag),
+				       .len = cpu_to_le16(len) };
+	return hdr;
+}
+
+static __le32 mm81x_hw_scan_h_pack_channel(struct ieee80211_channel *chan,
+					   u8 pwr_idx)
+{
+	__le32 packed = 0;
+	u32 freq_khz = ieee80211_channel_to_khz(chan);
+
+	packed |= le32_encode_bits(freq_khz, HW_SCAN_CH_LIST_FREQ_KHZ);
+	packed |= le32_encode_bits(mm81x_ratecode_bw_mhz_to_bw_index(1),
+				   HW_SCAN_CH_LIST_OP_BW);
+	packed |= le32_encode_bits(mm81x_ratecode_bw_mhz_to_bw_index(1),
+				   HW_SCAN_CH_LIST_PRIM_CH_WIDTH);
+	packed |= le32_encode_bits(pwr_idx, HW_SCAN_CH_LIST_PWR_LIST_IDX);
+
+	return packed;
+}
+
+static u8 *
+mm81x_hw_scan_h_add_channel_list_tlv(u8 *buf,
+				     struct mm81x_hw_scan_params *params)
+{
+	int i;
+	struct hw_scan_tlv_channel_list *ch_list =
+		(struct hw_scan_tlv_channel_list *)buf;
+
+	ch_list->hdr = mm81x_hw_scan_h_pack_tlv_hdr(
+		HOST_CMD_HW_SCAN_TLV_TAG_CHAN_LIST,
+		params->num_chans * sizeof(ch_list->channels[0]));
+
+	for (i = 0; i < params->num_chans; i++) {
+		struct ieee80211_channel *chan = params->channels[i].channel;
+
+		ch_list->channels[i] = mm81x_hw_scan_h_pack_channel(
+			chan, params->channels[i].power_idx);
+	}
+
+	return (u8 *)&ch_list->channels[i];
+}
+
+static u8 *
+mm81x_hw_scan_h_add_power_list_tlv(u8 *buf, struct mm81x_hw_scan_params *params)
+{
+	int i;
+	struct hw_scan_tlv_power_list *pwr_list =
+		(struct hw_scan_tlv_power_list *)buf;
+	size_t size = sizeof(pwr_list->tx_power_qdbm[0]) * params->n_powers;
+
+	pwr_list->hdr = mm81x_hw_scan_h_pack_tlv_hdr(
+		HOST_CMD_HW_SCAN_TLV_TAG_POWER_LIST, size);
+
+	for (i = 0; i < params->n_powers; i++)
+		pwr_list->tx_power_qdbm[i] = params->powers_qdbm[i];
+
+	return (u8 *)&pwr_list->tx_power_qdbm[i];
+}
+
+static u8 *
+mm81x_hw_scan_h_add_probe_req_tlv(u8 *buf, struct mm81x_hw_scan_params *params)
+{
+	struct sk_buff *skb = params->probe_req;
+	struct hw_scan_tlv_probe_req *probe_req =
+		(struct hw_scan_tlv_probe_req *)buf;
+
+	probe_req->hdr = mm81x_hw_scan_h_pack_tlv_hdr(
+		HOST_CMD_HW_SCAN_TLV_TAG_PROBE_REQ, skb->len);
+	memcpy(probe_req->buf, skb->data, skb->len);
+
+	return buf + sizeof(*probe_req) + skb->len;
+}
+
+static u8 *
+mm81x_hw_scan_h_insert_dwell_time_tlv(u8 *buf,
+				      struct mm81x_hw_scan_params *params)
+{
+	struct hw_scan_tlv_dwell_on_home *dwell =
+		(struct hw_scan_tlv_dwell_on_home *)buf;
+
+	dwell->hdr = mm81x_hw_scan_h_pack_tlv_hdr(
+		HOST_CMD_HW_SCAN_TLV_TAG_DWELL_ON_HOME,
+		sizeof(*dwell) - sizeof(dwell->hdr));
+	dwell->home_dwell_time_ms = cpu_to_le32(params->dwell_on_home_ms);
+
+	return buf + sizeof(*dwell);
+}
+
+static int __mm81x_hw_scan_h_init_probe_req(struct mm81x_hw_scan_params *params,
+					    u8 *ssid, u8 ssid_len,
+					    struct ieee80211_scan_ies *ies)
+{
+	u8 *pos;
+	struct sk_buff *probe_req;
+	struct ieee80211_tx_info *info;
+	u16 ies_len = ies->len[NL80211_BAND_S1GHZ] + ies->common_ie_len;
+
+	probe_req = ieee80211_probereq_get(params->hw, params->vif->addr, ssid,
+					   ssid_len, ies_len);
+	if (!probe_req)
+		return -ENOMEM;
+
+	pos = skb_put(probe_req, ies_len);
+	memcpy(pos, ies->common_ies, ies->common_ie_len);
+	pos += ies->common_ie_len;
+	memcpy(pos, ies->ies[NL80211_BAND_S1GHZ], ies->len[NL80211_BAND_S1GHZ]);
+
+	info = IEEE80211_SKB_CB(probe_req);
+	info->control.vif = params->vif;
+	params->probe_req = probe_req;
+
+	return 0;
+}
+
+static void mm81x_hw_scan_h_init_ssid(struct mm81x *mors,
+				      struct cfg80211_ssid *ssids, int n_ssids,
+				      u8 **out_ssid, u8 *out_ssid_len)
+{
+	*out_ssid = NULL;
+	*out_ssid_len = 0;
+
+	if (n_ssids > 0) {
+		if (n_ssids > 1) {
+			dev_warn(
+				mors->dev,
+				"Multiple SSIDs found when only one supported. Using the first only.");
+		}
+		*out_ssid_len = ssids[0].ssid_len;
+		*out_ssid = ssids[0].ssid;
+	}
+}
+
+static int
+mm81x_hw_scan_h_init_probe_req(struct mm81x_hw_scan_params *params,
+			       struct ieee80211_scan_request *scan_req)
+{
+	struct mm81x *mors = params->hw->priv;
+	struct cfg80211_scan_request *req = &scan_req->req;
+	struct ieee80211_scan_ies *ies = &scan_req->ies;
+	u8 ssid_len = 0;
+	u8 *ssid = NULL;
+
+	mm81x_hw_scan_h_init_ssid(mors, req->ssids, req->n_ssids, &ssid,
+				  &ssid_len);
+
+	return __mm81x_hw_scan_h_init_probe_req(params, ssid, ssid_len, ies);
+}
+
+static bool
+mm81x_hw_scan_h_is_chan_present(const struct mm81x_hw_scan_params *params,
+				const struct ieee80211_channel *chan)
+{
+	int channel;
+
+	for (channel = 0; channel < params->num_chans; channel++) {
+		if (params->channels[channel].channel == chan)
+			return true;
+	}
+
+	return false;
+}
+
+static int mm81x_hw_scan_h_insert_chan(struct mm81x_hw_scan_params *params,
+				       struct ieee80211_channel *chan)
+{
+	if (!params->channels)
+		return -EFAULT;
+
+	if (!chan)
+		return -EFAULT;
+
+	if (params->num_chans >= params->allocated_chans)
+		return -ENOMEM;
+
+	if (mm81x_hw_scan_h_is_chan_present(params, chan))
+		return 0;
+
+	params->channels[params->num_chans].channel = chan;
+	params->num_chans++;
+	return 0;
+}
+
+static int mm81x_hw_scan_h_init_chan_list(struct mm81x_hw_scan_params *params,
+					  struct ieee80211_channel **chans,
+					  u32 n_channels)
+{
+	int i, j;
+	int num_pwrs_coarse = 0;
+	int last_pwr = INT_MIN;
+	int chans_to_allocate = 0;
+
+	for (i = 0; i < n_channels; i++)
+		if (chans[i])
+			chans_to_allocate++;
+
+	params->num_chans = 0;
+	params->allocated_chans = 0;
+	params->channels = kcalloc(chans_to_allocate, sizeof(*params->channels),
+				   GFP_KERNEL);
+	if (!params->channels)
+		return -ENOMEM;
+
+	params->allocated_chans = chans_to_allocate;
+
+	for (i = 0; i < n_channels; i++)
+		if (chans[i])
+			mm81x_hw_scan_h_insert_chan(params, chans[i]);
+
+	/*
+	 * Calculate a rough estimate of number of different channel
+	 * powers required
+	 */
+	for (i = 0; i < params->num_chans; i++) {
+		if (chans[i]->max_reg_power != last_pwr) {
+			last_pwr = chans[i]->max_reg_power;
+			num_pwrs_coarse++;
+		}
+	}
+
+	params->powers_qdbm = kmalloc_array(
+		num_pwrs_coarse, sizeof(*params->powers_qdbm), GFP_KERNEL);
+	if (!params->powers_qdbm)
+		return -ENOMEM;
+
+	params->n_powers = 0;
+
+	for (i = 0; i < params->num_chans; i++) {
+		s32 power_qdbm =
+			MBM_TO_QDBM(DBM_TO_MBM(chans[i]->max_reg_power));
+
+		/* Try and find the power in the list */
+		for (j = 0; j < params->n_powers; j++)
+			if (params->powers_qdbm[j] == power_qdbm)
+				break;
+
+		/* Reached the end of the list - add the new power option */
+		if (j == params->n_powers) {
+			params->powers_qdbm[j] = power_qdbm;
+			params->n_powers++;
+			if (params->n_powers > num_pwrs_coarse) {
+				WARN_ON(1);
+				return -EFAULT;
+			}
+		}
+
+		/* Give the index of the power level to the channel */
+		params->channels[i].power_idx = j;
+	}
+	return 0;
+}
+
+static void mm81x_hw_scan_h_clean_params(struct mm81x_hw_scan_params *params)
+{
+	if (params->probe_req)
+		dev_kfree_skb_any(params->probe_req);
+	kfree(params->channels);
+	kfree(params->powers_qdbm);
+
+	params->num_chans = 0;
+	params->allocated_chans = 0;
+}
+
+size_t mm81x_hw_scan_h_get_cmd_size(struct mm81x_hw_scan_params *params)
+{
+	struct hw_scan_tlv_channel_list *ch_list;
+	struct hw_scan_tlv_power_list *pwr_list;
+	struct hw_scan_tlv_probe_req *probe_req;
+	struct hw_scan_tlv_dwell_on_home *dwell;
+	struct host_cmd_req_hw_scan *req;
+	size_t cmd_size = sizeof(*req);
+
+	/* No TLVs if simple abort command */
+	if (params->operation != MM81X_HW_SCAN_OP_START)
+		return cmd_size;
+
+	cmd_size += struct_size(ch_list, channels, params->num_chans);
+	cmd_size += struct_size(pwr_list, tx_power_qdbm, params->n_powers);
+
+	if (params->probe_req)
+		cmd_size += struct_size(probe_req, buf, params->probe_req->len);
+	if (params->dwell_on_home_ms)
+		cmd_size += sizeof(*dwell);
+
+	return cmd_size;
+}
+
+u8 *mm81x_hw_scan_h_insert_tlvs(struct mm81x_hw_scan_params *params, u8 *buf)
+{
+	buf = mm81x_hw_scan_h_add_channel_list_tlv(buf, params);
+	buf = mm81x_hw_scan_h_add_power_list_tlv(buf, params);
+
+	if (params->dwell_on_home_ms)
+		buf = mm81x_hw_scan_h_insert_dwell_time_tlv(buf, params);
+	if (params->probe_req)
+		buf = mm81x_hw_scan_h_add_probe_req_tlv(buf, params);
+
+	return buf;
+}
+
+static u32 mm81x_hw_scan_h_get_dwell_on_home(struct mm81x *mors,
+					     struct ieee80211_vif *vif)
+{
+	if (vif->type == NL80211_IFTYPE_STATION && vif->cfg.assoc)
+		return mors->hw_scan.home_dwell_ms;
+	return 0;
+}
+
+static struct mm81x_hw_scan_params *
+__mm81x_hw_scan_h_init_params(struct mm81x *mors)
+{
+	struct mm81x_hw_scan_params *params = mors->hw_scan.params;
+
+	if (!params) {
+		params = kzalloc_obj(*params, GFP_KERNEL);
+		if (params)
+			mors->hw_scan.params = params;
+	} else {
+		mm81x_hw_scan_h_clean_params(params);
+		memset(params, 0, sizeof(*params));
+	}
+
+	return params;
+}
+
+static int mm81x_hw_scan_h_init_params(struct mm81x *mors,
+				       struct ieee80211_hw *hw,
+				       struct ieee80211_vif *vif,
+				       struct cfg80211_scan_request *req)
+{
+	struct mm81x_hw_scan_params *params = mors->hw_scan.params;
+
+	params = __mm81x_hw_scan_h_init_params(mors);
+	if (!params) {
+		mors->hw_scan.state = HW_SCAN_STATE_IDLE;
+		return -ENOMEM;
+	}
+
+	params->hw = hw;
+	params->vif = vif;
+	params->has_directed_ssid = (req->ssids && req->ssids[0].ssid_len > 0);
+	params->operation = MM81X_HW_SCAN_OP_START;
+	params->dwell_on_home_ms = mm81x_hw_scan_h_get_dwell_on_home(mors, vif);
+
+	if (req->duration)
+		params->dwell_time_ms = MM81X_TU_TO_MS(req->duration);
+	else if (req->n_ssids == 0)
+		params->dwell_time_ms =
+			MM81X_HWSCAN_DEFAULT_PASSIVE_DWELL_TIME_MS;
+	else
+		params->dwell_time_ms = MM81X_HWSCAN_DEFAULT_DWELL_TIME_MS;
+
+	return 0;
+}
+
+static u32 mm81x_hw_scan_h_calc_timeout(struct mm81x_hw_scan_params *params)
+{
+	u32 ret = 0;
+
+	ret = params->dwell_time_ms + params->dwell_on_home_ms;
+	if (params->probe_req)
+		ret += MM81X_HWSCAN_PROBE_DELAY_MS;
+
+	ret *= params->num_chans;
+	ret += MM81X_HWSCAN_TIMEOUT_OVERHEAD_MS;
+
+	return ret;
+}
+
+static int mm81x_mac_ops_hw_scan(struct ieee80211_hw *hw,
+				 struct ieee80211_vif *vif,
+				 struct ieee80211_scan_request *hw_req)
+{
+	int ret = 0;
+	struct mm81x *mors = hw->priv;
+	struct cfg80211_scan_request *req = &hw_req->req;
+	struct mm81x_hw_scan_params *params;
+	struct ieee80211_channel **chans = hw_req->req.channels;
+
+	dev_dbg(mors->dev, "state %d", mors->hw_scan.state);
+
+	if (!mors->started) {
+		dev_warn(mors->dev, "device not ready");
+		ret = -ENODEV;
+		goto exit;
+	}
+
+	switch (mors->hw_scan.state) {
+	case HW_SCAN_STATE_IDLE:
+		mors->hw_scan.state = HW_SCAN_STATE_RUNNING;
+		reinit_completion(&mors->hw_scan.scan_done);
+		break;
+	case HW_SCAN_STATE_RUNNING:
+	case HW_SCAN_STATE_ABORTING:
+		ret = -EBUSY;
+		goto exit;
+	}
+
+	ret = mm81x_hw_scan_h_init_params(mors, hw, vif, req);
+	if (ret)
+		goto exit;
+
+	params = mors->hw_scan.params;
+
+	ret = mm81x_hw_scan_h_init_chan_list(params, chans,
+					     hw_req->req.n_channels);
+	if (ret)
+		goto exit;
+
+	/* Only init the probe request template if this is an active scan */
+	if (req->n_ssids > 0) {
+		ret = mm81x_hw_scan_h_init_probe_req(params, hw_req);
+		if (ret) {
+			dev_err(mors->dev, "Failed to init probe req %d", ret);
+			goto exit;
+		}
+	}
+
+	ret = mm81x_cmd_hw_scan(mors, params, false);
+	if (ret) {
+		mors->hw_scan.state = HW_SCAN_STATE_IDLE;
+		goto exit;
+	}
+
+	ieee80211_queue_delayed_work(
+		mors->hw, &mors->hw_scan.timeout,
+		msecs_to_jiffies(mm81x_hw_scan_h_calc_timeout(params)));
+exit:
+	return ret;
+}
+
+static void mm81x_hw_scan_abort(struct mm81x *mors)
+{
+	int ret;
+	struct mm81x_hw_scan_params params = { 0 };
+
+	switch (mors->hw_scan.state) {
+	case HW_SCAN_STATE_IDLE:
+	case HW_SCAN_STATE_ABORTING:
+		/* scan not running */
+		return;
+	case HW_SCAN_STATE_RUNNING:
+		mors->hw_scan.state = HW_SCAN_STATE_ABORTING;
+		break;
+	}
+
+	params.operation = MM81X_HW_SCAN_OP_STOP;
+
+	ret = mm81x_cmd_hw_scan(mors, &params, false);
+
+	if (ret || !mors->started ||
+	    !wait_for_completion_timeout(&mors->hw_scan.scan_done, 1 * HZ)) {
+		/*
+		 * We may have lost the event on the bus, the chip could be
+		 * wedged, or the cmd failed for another reason. Nevertheless,
+		 * we should call the done event so mac80211 knows to unblock
+		 * itself.
+		 */
+		struct cfg80211_scan_info info = { .aborted = true };
+
+		ieee80211_scan_completed(mors->hw, &info);
+		mors->hw_scan.state = HW_SCAN_STATE_IDLE;
+	}
+}
+
+static void mm81x_mac_ops_cancel_hw_scan(struct ieee80211_hw *hw,
+					 struct ieee80211_vif *vif)
+{
+	struct mm81x *mors = hw->priv;
+
+	cancel_delayed_work_sync(&mors->hw_scan.timeout);
+	mm81x_hw_scan_abort(mors);
+}
+
+static void mm81x_mac_hw_scan_done_event(struct ieee80211_hw *hw)
+{
+	struct mm81x *mors = hw->priv;
+	struct cfg80211_scan_info info = { 0 };
+
+	dev_dbg(mors->dev, "completing hw scan");
+
+	switch (mors->hw_scan.state) {
+	case HW_SCAN_STATE_IDLE:
+		/* Scan has already been stopped. Just continue */
+		goto exit;
+	case HW_SCAN_STATE_RUNNING:
+	case HW_SCAN_STATE_ABORTING:
+		info.aborted = (mors->hw_scan.state == HW_SCAN_STATE_ABORTING);
+		mors->hw_scan.state = HW_SCAN_STATE_IDLE;
+	}
+
+	ieee80211_scan_completed(mors->hw, &info);
+exit:
+	complete(&mors->hw_scan.scan_done);
+	cancel_delayed_work_sync(&mors->hw_scan.timeout);
+}
+
+static void mm81x_mac_hw_scan_timeout_work(struct work_struct *work)
+{
+	struct mm81x *mors =
+		container_of(work, struct mm81x, hw_scan.timeout.work);
+
+	dev_err(mors->dev, "hw scan timed out, aborting");
+	mm81x_hw_scan_abort(mors);
+}
+
+static void mm81x_mac_hw_scan_init(struct mm81x *mors)
+{
+	mors->hw_scan.state = HW_SCAN_STATE_IDLE;
+	mors->hw_scan.params = NULL;
+	mors->hw_scan.home_dwell_ms = MM81X_HWSCAN_DEFAULT_DWELL_ON_HOME_MS;
+
+	init_completion(&mors->hw_scan.scan_done);
+	INIT_DELAYED_WORK(&mors->hw_scan.timeout,
+			  mm81x_mac_hw_scan_timeout_work);
+}
+
+static void mm81x_mac_hw_scan_destroy(struct mm81x *mors)
+{
+	cancel_delayed_work_sync(&mors->hw_scan.timeout);
+	if (mors->hw_scan.params)
+		mm81x_hw_scan_h_clean_params(mors->hw_scan.params);
+	kfree(mors->hw_scan.params);
+	mors->hw_scan.params = NULL;
+}
+
+static void mm81x_mac_hw_scan_finish(struct mm81x *mors)
+{
+	struct cfg80211_scan_info info = {
+		.aborted = true,
+	};
+
+	if (mors->hw_scan.state == HW_SCAN_STATE_IDLE)
+		return;
+
+	ieee80211_scan_completed(mors->hw, &info);
+	complete(&mors->hw_scan.scan_done);
+	mors->hw_scan.state = HW_SCAN_STATE_IDLE;
+	cancel_delayed_work_sync(&mors->hw_scan.timeout);
+}
+
+int mm81x_mac_event_recv(struct mm81x *mors, struct sk_buff *skb)
+{
+	struct host_cmd_event *event = (struct host_cmd_event *)(skb->data);
+	u16 event_id = le16_to_cpu(event->hdr.message_id);
+	u16 event_iid = le16_to_cpu(event->hdr.host_id);
+	u16 vif_id = le16_to_cpu(event->hdr.vif_id);
+	struct ieee80211_vif *vif;
+
+	if (!HOST_CMD_IS_EVT(event) || event_iid != 0)
+		return -EINVAL;
+
+	switch (event_id) {
+	case HOST_CMD_ID_EVT_HW_SCAN_DONE:
+		dev_dbg(mors->dev,
+			"Event: HOST_CMD_ID_EVT_HW_SCAN_DONE Received.");
+		mm81x_mac_hw_scan_done_event(mors->hw);
+		break;
+	case HOST_CMD_ID_EVT_BEACON_LOSS:
+		dev_dbg(mors->dev,
+			"Event: HOST_CMD_ID_EVT_BEACON_LOSS Received");
+		scoped_guard(rcu) {
+			vif = mm81x_rcu_dereference_vif_id(mors, vif_id, true);
+			if (vif)
+				ieee80211_beacon_loss(vif);
+		}
+		break;
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+static void mm81x_tx_h_apply_mcs10(struct mm81x *mors,
+				   struct mm81x_skb_tx_info *tx_info)
+{
+	u8 i;
+	u8 j;
+	int mcs0_first_idx = -1;
+	int mcs0_last_idx = -1;
+
+	/* Find out where our first and last MCS0 entries are. */
+	for (i = 0; i < IEEE80211_TX_MAX_RATES; i++) {
+		enum dot11_bandwidth bw_idx = mm81x_ratecode_bw_index_get(
+			tx_info->rates[i].mm81x_ratecode);
+
+		if (bw_idx == DOT11_BANDWIDTH_1MHZ) {
+			mcs0_last_idx = i;
+			if (mcs0_first_idx == -1)
+				mcs0_first_idx = i;
+		}
+
+		/*
+		 * If the count is 0 then we are at the end of the table.
+		 * Break to allow us to reuse i indicating the end of the
+		 * table.
+		 */
+		if (tx_info->rates[i].count == 0)
+			break;
+	}
+
+	/* If there aren't any MCS0 (at 1MHz) entries we are done. */
+	if (mcs0_first_idx < 0)
+		return;
+
+	/*
+	 * If we are in MCS10_MODE_AUTO add MCS10 counts to the table if they
+	 * will fit. There should be three cases:
+	 *
+	 * - There is one MSC0 entry and the table is full -> do nothing
+	 * - There is one MSC0 entry and the table has space -> adjust MSC0
+	 *   down and add MCS 10
+	 * - There are multiple MCS0 entries -> replace entries after the first
+	 *   with MCS 10
+	 */
+	/* Case 3 - replace additional entries. */
+	if (mcs0_last_idx > mcs0_first_idx) {
+		for (j = mcs0_first_idx + 1; j < i; j++) {
+			enum dot11_bandwidth bw_idx =
+				mm81x_ratecode_bw_index_get(
+					tx_info->rates[j].mm81x_ratecode);
+			u8 mcs_index = mm81x_ratecode_mcs_index_get(
+				tx_info->rates[j].mm81x_ratecode);
+			if (mcs_index == 0 && bw_idx == DOT11_BANDWIDTH_1MHZ) {
+				mm81x_ratecode_mcs_index_set(
+					&tx_info->rates[j].mm81x_ratecode, 10);
+			}
+		}
+		/* Case 2 - add additional MCS10 entry. */
+	} else if (mcs0_last_idx == mcs0_first_idx &&
+		   i < (IEEE80211_TX_MAX_RATES)) {
+		int pre_mcs10_mcs0_count =
+			min_t(u8, tx_info->rates[mcs0_last_idx].count,
+			      MCS0_BEFORE_MCS10_COUNT);
+		int mcs10_count = tx_info->rates[mcs0_last_idx].count -
+				  pre_mcs10_mcs0_count;
+
+		/*
+		 * If there were less retries than our desired minimum MCS0 we
+		 * don't add MCS10 retries.
+		 */
+		if (mcs10_count > 0) {
+			/* Use the same flags for MCS10 as MCS0. */
+			tx_info->rates[i].mm81x_ratecode =
+				tx_info->rates[mcs0_last_idx].mm81x_ratecode;
+			mm81x_ratecode_mcs_index_set(
+				&tx_info->rates[i].mm81x_ratecode, 10);
+			tx_info->rates[mcs0_last_idx].count =
+				pre_mcs10_mcs0_count;
+			tx_info->rates[i].count = mcs10_count;
+		}
+	}
+}
+
+void mm81x_tx_h_check_aggr(struct ieee80211_sta *pubsta, struct sk_buff *skb)
+{
+	struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+	struct mm81x_sta *mors_sta = (struct mm81x_sta *)pubsta->drv_priv;
+	u8 tid = ieee80211_get_tid(hdr);
+
+	/* we are already aggregating */
+	if (mors_sta->tid_tx[tid] || mors_sta->tid_start_tx[tid])
+		return;
+
+	if (mors_sta->state < IEEE80211_STA_AUTHORIZED)
+		return;
+
+	if (skb_get_queue_mapping(skb) == IEEE80211_AC_VO)
+		return;
+
+	if (unlikely(!ieee80211_is_data_qos(hdr->frame_control)))
+		return;
+
+	if (unlikely(skb->protocol == cpu_to_be16(ETH_P_PAE)))
+		return;
+
+	mors_sta->tid_start_tx[tid] = true;
+	ieee80211_start_tx_ba_session(pubsta, tid, 0);
+}
+
+int mm81x_tx_h_get_attempts(struct mm81x *mors,
+			    struct mm81x_skb_tx_status *tx_sts)
+{
+	int attempts = 0;
+	int i;
+	int count = min_t(int, MM81X_SKB_MAX_RATES, IEEE80211_TX_MAX_RATES);
+
+	for (i = 0; i < count; i++) {
+		if (tx_sts->rates[i].count > 0)
+			attempts += tx_sts->rates[i].count;
+		else
+			break;
+	}
+
+	return attempts;
+}
+
+static void mm81x_tx_h_fill_info(struct mm81x *mors,
+				 struct mm81x_skb_tx_info *tx_info,
+				 struct sk_buff *skb, struct ieee80211_vif *vif,
+				 int tx_bw_mhz, struct ieee80211_sta *sta)
+{
+	int i;
+	struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+	struct mm81x_vif *mors_vif = ieee80211_vif_to_mors_vif(vif);
+	struct mm81x_sta *mors_sta = NULL;
+	struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+	int op_bw_mhz = cfg80211_chandef_get_width(&mors->chandef);
+	u8 tid = skb->priority & IEEE80211_QOS_CTL_TAG1D_MASK;
+	bool rts_allowed = op_bw_mhz < 8;
+
+	if (sta)
+		mors_sta = (struct mm81x_sta *)sta->drv_priv;
+
+	rts_allowed &= mm81x_tx_h_pkt_over_rts_threshold(mors, info, skb);
+
+	mm81x_rc_sta_fill_tx_rates(mors, tx_info, skb, sta, tx_bw_mhz,
+				   rts_allowed);
+
+	for (i = 0; i < IEEE80211_TX_MAX_RATES; i++) {
+		if (rts_allowed)
+			mm81x_ratecode_enable_rts(
+				&tx_info->rates[i].mm81x_ratecode);
+
+		if (info->control.rates[i].flags & IEEE80211_TX_RC_SHORT_GI)
+			mm81x_ratecode_enable_sgi(
+				&tx_info->rates[i].mm81x_ratecode);
+	}
+
+	/* Apply change of MCS0 to MCS10 if required. */
+	mm81x_tx_h_apply_mcs10(mors, tx_info);
+
+	tx_info->flags |=
+		cpu_to_le32(MM81X_TX_CONF_FLAGS_VIF_ID_SET(mors_vif->id));
+
+	if (info->flags & IEEE80211_TX_CTL_AMPDU)
+		tx_info->flags |= cpu_to_le32(MM81X_TX_CONF_FLAGS_CTL_AMPDU);
+
+	if (info->flags & IEEE80211_TX_CTL_SEND_AFTER_DTIM)
+		tx_info->flags |=
+			cpu_to_le32(MM81X_TX_CONF_FLAGS_SEND_AFTER_DTIM);
+
+	if (info->flags & IEEE80211_TX_CTL_NO_PS_BUFFER) {
+		tx_info->flags |= cpu_to_le32(MM81X_TX_CONF_NO_PS_BUFFER);
+
+		if (info->flags & IEEE80211_TX_STATUS_EOSP)
+			tx_info->flags |= cpu_to_le32(
+				MM81X_TX_CONF_FLAGS_IMMEDIATE_REPORT);
+	} else if (ieee80211_is_mgmt(hdr->frame_control) &&
+		   !ieee80211_is_bufferable_mmpdu(skb)) {
+		tx_info->flags |= cpu_to_le32(MM81X_TX_CONF_NO_PS_BUFFER);
+	}
+
+	if (info->control.hw_key) {
+		tx_info->flags |= cpu_to_le32(MM81X_TX_CONF_FLAGS_HW_ENCRYPT);
+		tx_info->flags |= cpu_to_le32(MM81X_TX_CONF_FLAGS_KEY_IDX_SET(
+			info->control.hw_key->hw_key_idx));
+	}
+
+	tx_info->tid = tid;
+	if (mors_sta) {
+		tx_info->tid_params = mors_sta->tid_params[tid];
+
+		if (info->flags & IEEE80211_TX_CTL_CLEAR_PS_FILT) {
+			if (mors_sta->tx_ps_filter_en)
+				dev_dbg(mors->dev,
+					"TX ps filter cleared sta[%pM]",
+					mors_sta->addr);
+			mors_sta->tx_ps_filter_en = false;
+		}
+	}
+}
+
+static void mm81x_mac_ops_tx(struct ieee80211_hw *hw,
+			     struct ieee80211_tx_control *control,
+			     struct sk_buff *skb)
+{
+	struct mm81x *mors = hw->priv;
+	struct mm81x_skbq *mq = NULL;
+	struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+	struct ieee80211_vif *vif = info->control.vif;
+	struct mm81x_skb_tx_info tx_info = { 0 };
+	struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+	bool is_mgmt = ieee80211_is_mgmt(hdr->frame_control);
+	int tx_bw_mhz = cfg80211_chandef_get_width(&mors->chandef);
+	struct ieee80211_sta *sta = control->sta;
+	int max_tx_bw = 0, sta_max_bw_mhz = 0;
+
+	if (sta) {
+		struct mm81x_sta *mors_sta = (struct mm81x_sta *)sta->drv_priv;
+
+		sta_max_bw_mhz = mors_sta->max_bw_mhz;
+	}
+
+	max_tx_bw = mm81x_tx_h_get_max_bw(mors);
+	tx_bw_mhz = min(max_tx_bw, tx_bw_mhz);
+
+	if (is_mgmt)
+		tx_bw_mhz = cfg80211_chandef_s1g_pri_width(&mors->chandef);
+	if (sta_max_bw_mhz)
+		tx_bw_mhz = min(tx_bw_mhz, sta_max_bw_mhz);
+	if (ieee80211_is_probe_resp(hdr->frame_control))
+		tx_bw_mhz = 1;
+
+	mm81x_tx_h_fill_info(mors, &tx_info, skb, vif, tx_bw_mhz, sta);
+
+	if (mm81x_tx_h_ps_filtered_for_sta(mors, skb, sta))
+		return;
+
+	if (is_mgmt)
+		mq = mm81x_hif_get_tx_mgmt_queue(mors);
+	else
+		mq = mm81x_hif_get_tx_data_queue(mors,
+						 dot11_tid_to_ac(tx_info.tid));
+
+	mm81x_skbq_skb_tx(mq, &skb, &tx_info,
+			  (is_mgmt) ? MM81X_SKB_CHAN_MGMT :
+				      MM81X_SKB_CHAN_DATA);
+}
+
+static void mm81x_mac_ops_stop(struct ieee80211_hw *hw, bool suspend)
+{
+	struct mm81x *mors = hw->priv;
+
+	mors->started = false;
+}
+
+static void mm81x_mac_beacon_finish(struct mm81x_vif *mors_vif)
+{
+	struct mm81x *mors = mm81x_vif_to_mors(mors_vif);
+
+	mm81x_mac_beacon_irq_enable(mors_vif, false);
+	cancel_work_sync(&mors_vif->u.ap.beacon_work);
+	/*
+	 * Side effect of the restarting required when
+	 * reacting to regdom changes...
+	 */
+	atomic_add_unless(&mors->num_bcn_vifs, -1, 0);
+}
+
+static void mm81x_mac_ops_remove_interface(struct ieee80211_hw *hw,
+					   struct ieee80211_vif *vif)
+{
+	int ret;
+	struct mm81x *mors = hw->priv;
+	struct mm81x_vif *mors_vif = (struct mm81x_vif *)vif->drv_priv;
+
+	ret = mm81x_cmd_rm_if(mors, mors_vif->id);
+	if (ret)
+		dev_err(mors->dev, "mm81x_cmd_rm_if failed %d", ret);
+
+	RCU_INIT_POINTER(mors->vifs[mors_vif->id], NULL);
+}
+
+static s32 mm81x_mac_get_max_txpower(struct mm81x *mors)
+{
+	int ret;
+	s32 power_mbm;
+
+	/* Retrieve maximum TX power the chip can transmit */
+	ret = mm81x_cmd_get_max_txpower(mors, &power_mbm);
+	if (ret) {
+		dev_err(mors->dev, "using default tx max power %d mBm",
+			MAX_TX_POWER_MBM);
+		return MAX_TX_POWER_MBM;
+	}
+
+	dev_dbg(mors->dev, "Max tx power detected %d mBm", power_mbm);
+	return power_mbm;
+}
+
+static s32 mm81x_mac_set_txpower(struct mm81x *mors, s32 power_mbm)
+{
+	int ret;
+	s32 out_power_mbm;
+
+	if (mors->tx_max_power_mbm == INT_MAX)
+		mors->tx_max_power_mbm = mm81x_mac_get_max_txpower(mors);
+
+	power_mbm = min(power_mbm, mors->tx_max_power_mbm);
+	if (power_mbm == mors->tx_power_mbm)
+		return mors->tx_power_mbm;
+
+	ret = mm81x_cmd_set_txpower(mors, &out_power_mbm, power_mbm);
+	if (ret) {
+		dev_err(mors->dev, "failed, power %d mBm ret %d", power_mbm,
+			ret);
+		return mors->tx_power_mbm;
+	}
+
+	if (out_power_mbm != mors->tx_power_mbm) {
+		dev_dbg(mors->dev, "%d -> %d mBm", mors->tx_power_mbm,
+			out_power_mbm);
+		mors->tx_power_mbm = out_power_mbm;
+	}
+
+	return mors->tx_power_mbm;
+}
+
+static int mm81x_mac_set_channel(struct mm81x *mors, u32 op_chan_freq_hz,
+				 u8 pri_1mhz_chan_idx, u8 op_bw_mhz,
+				 u8 pri_bw_mhz)
+{
+	int ret;
+
+	ret = mm81x_cmd_set_channel(mors, op_chan_freq_hz, pri_1mhz_chan_idx,
+				    op_bw_mhz, pri_bw_mhz, &mors->tx_power_mbm);
+	if (ret) {
+		dev_err(mors->dev, "mm81x_cmd_set_channel() failed, ret %d",
+			ret);
+		return ret;
+	}
+
+	mm81x_mac_set_txpower(mors, mors->tx_power_mbm);
+	return 0;
+}
+
+static u8 mm81x_mac_pri_chan_to_index(const struct cfg80211_chan_def *chandef)
+{
+	u32 bw_mhz = cfg80211_chandef_get_width(chandef);
+	u32 op_center_khz = ieee80211_chandef_to_khz(chandef);
+	u32 first_1mhz_center_khz = op_center_khz - (bw_mhz * 500) + 500;
+	u32 pri_1mhz_khz = ieee80211_channel_to_khz(chandef->chan);
+
+	return (pri_1mhz_khz - first_1mhz_center_khz) / 1000;
+}
+
+static int mm81x_mac_ops_change_channel(struct ieee80211_hw *hw,
+					struct cfg80211_chan_def *chandef)
+{
+	int ret;
+	struct mm81x *mors = hw->priv;
+	u64 freq_hz = KHZ_TO_HZ(ieee80211_chandef_to_khz(chandef));
+	u8 op_bw_mhz = cfg80211_chandef_get_width(chandef);
+	u8 pri_1mhz_idx = mm81x_mac_pri_chan_to_index(chandef);
+	int pri_chan_width_mhz = cfg80211_chandef_s1g_pri_width(chandef);
+
+	dev_dbg(mors->dev, "ch: freq=%llu Hz bw=%u pri_idx=%d pri_bw=%d",
+		freq_hz, op_bw_mhz, pri_1mhz_idx, pri_chan_width_mhz);
+
+	ret = mm81x_mac_set_channel(mors, freq_hz, (u8)pri_1mhz_idx, op_bw_mhz,
+				    pri_chan_width_mhz);
+	if (ret)
+		return ret;
+
+	memcpy(&mors->chandef, chandef, sizeof(mors->chandef));
+	return 0;
+}
+
+static int mm81x_mac_ops_config(struct ieee80211_hw *hw, int radio_idx,
+				u32 changed)
+{
+	int ret;
+	struct mm81x *mors = hw->priv;
+	struct ieee80211_conf *conf = &hw->conf;
+	struct ieee80211_channel *channel = conf->chandef.chan;
+
+	if (!mors->started)
+		return 0;
+
+	if (changed & IEEE80211_CONF_CHANGE_CHANNEL) {
+		ret = mm81x_mac_ops_change_channel(hw, &conf->chandef);
+		if (ret < 0)
+			return ret;
+	}
+
+	if ((changed & IEEE80211_CONF_CHANGE_POWER) &&
+	    !(changed & IEEE80211_CONF_CHANGE_CHANNEL) &&
+	    !(conf->flags & IEEE80211_CONF_MONITOR)) {
+		s32 power_mbm = DBM_TO_MBM(conf->power_level);
+
+		power_mbm = min(DBM_TO_MBM(channel->max_reg_power), power_mbm);
+		power_mbm = mm81x_mac_set_txpower(mors, power_mbm);
+		conf->power_level = MBM_TO_DBM(power_mbm);
+	}
+
+	return 0;
+}
+
+static int mm81x_mac_ops_get_txpower(struct ieee80211_hw *hw,
+				     struct ieee80211_vif *vif,
+				     unsigned int link_id, int *dbm)
+{
+	struct mm81x *mors = hw->priv;
+	struct ieee80211_chanctx_conf *chanctx_conf;
+	struct cfg80211_chan_def *chandef = &vif->bss_conf.chanreq.oper;
+
+	scoped_guard(rcu) {
+		chanctx_conf = rcu_access_pointer(vif->bss_conf.chanctx_conf);
+		if (!chanctx_conf ||
+		    !cfg80211_chandef_identical(chandef, &chanctx_conf->def))
+			return -ENODATA;
+	}
+
+	*dbm = MBM_TO_DBM(mors->tx_power_mbm);
+	return 0;
+}
+
+static void mm81x_mac_config_ps(struct mm81x *mors, struct ieee80211_vif *vif)
+{
+	bool en_ps = vif->cfg.ps;
+
+	if (vif->type == NL80211_IFTYPE_AP || !mors->ps.enable)
+		return;
+
+	if (mors->config_ps == en_ps)
+		return;
+
+	dev_dbg(mors->dev, "change powersave mode: %d (current %d)", en_ps,
+		mors->config_ps);
+
+	mors->config_ps = en_ps;
+
+	if (en_ps) {
+		mm81x_cmd_set_ps(mors, true);
+		mm81x_ps_enable(mors);
+	} else {
+		mm81x_ps_disable(mors);
+		mm81x_cmd_set_ps(mors, false);
+	}
+}
+
+static void mm81x_mac_ops_bss_info_changed(struct ieee80211_hw *hw,
+					   struct ieee80211_vif *vif,
+					   struct ieee80211_bss_conf *info,
+					   u64 changed)
+{
+	int ret;
+	struct mm81x *mors = hw->priv;
+	struct mm81x_vif *mors_vif = (struct mm81x_vif *)vif->drv_priv;
+
+	if (changed & BSS_CHANGED_PS)
+		mm81x_mac_config_ps(mors, vif);
+
+	if (changed & BSS_CHANGED_BEACON_ENABLED) {
+		mm81x_cmd_config_beacon_timer(mors, mors_vif,
+					      info->enable_beacon);
+
+		if (!info->enable_beacon)
+			mm81x_mac_beacon_finish(mors_vif);
+	}
+
+	if (changed & BSS_CHANGED_BEACON_INT || changed & BSS_CHANGED_SSID) {
+		ret = mm81x_cmd_cfg_bss(mors, mors_vif->id, info->beacon_int,
+					info->dtim_period,
+					mm81x_vif_generate_cssid(vif));
+		if (ret)
+			dev_err(mors->dev, "mm81x_cmd_cfg_bss failed %d", ret);
+	}
+}
+
+static u64 mm81x_mac_ops_prepare_multicast(struct ieee80211_hw *hw,
+					   struct netdev_hw_addr_list *mc_list)
+{
+	struct mm81x *mors = hw->priv;
+	struct mcast_filter *filter;
+	struct netdev_hw_addr *addr;
+	u16 addr_count = netdev_hw_addr_list_count(mc_list);
+	u16 len = sizeof(*filter) + addr_count * sizeof(filter->addr_list[0]);
+
+	filter = kzalloc(len, GFP_ATOMIC);
+	if (!filter)
+		return 0;
+
+	if (addr_count > MCAST_FILTER_COUNT_MAX) {
+		dev_warn(
+			mors->dev,
+			"Multicast filtering disabled - too many groups (%d) > %u",
+			addr_count, (u16)MCAST_FILTER_COUNT_MAX);
+		filter->count = 0;
+	} else {
+		netdev_hw_addr_list_for_each(addr, mc_list) {
+			dev_dbg(mors->dev, "mcast whitelist (%d): %pM",
+				filter->count, addr->addr);
+			filter->addr_list[filter->count++] =
+				mac2le32(addr->addr);
+		}
+	}
+
+	return (u64)(unsigned long)filter;
+}
+
+static void mm81x_mac_ops_configure_filter(struct ieee80211_hw *hw,
+					   unsigned int changed_flags,
+					   unsigned int *total_flags,
+					   u64 multicast)
+{
+	struct mm81x *mors = hw->priv;
+	struct mcast_filter *cmd = (void *)(unsigned long)multicast;
+	struct mm81x_vif *mors_vif = NULL;
+	struct ieee80211_vif *vif = NULL;
+	int vif_id = 0;
+	int ret = 0;
+
+	if (!cmd)
+		goto out;
+
+	kfree(mors->mcast_filter);
+	mors->mcast_filter = cmd;
+
+	for (vif_id = 0; vif_id < ARRAY_SIZE(mors->vifs); vif_id++) {
+		vif = mm81x_rcu_dereference_vif_id(mors, vif_id, false);
+		if (!vif)
+			continue;
+
+		mors_vif = ieee80211_vif_to_mors_vif(vif);
+
+		ret = mm81x_cmd_cfg_multicast_filter(mors, mors_vif);
+		if (!ret)
+			continue;
+
+		dev_err(mors->dev, "Multicast filtering failed - rc=%d", ret);
+		mors->mcast_filter = NULL;
+		kfree(cmd);
+		break;
+	}
+
+out:
+	*total_flags &= 0;
+}
+
+static int mm81x_mac_ops_conf_tx(struct ieee80211_hw *hw,
+				 struct ieee80211_vif *vif,
+				 unsigned int link_id, u16 ac,
+				 const struct ieee80211_tx_queue_params *params)
+{
+	int ret;
+	struct mm81x *mors = hw->priv;
+	struct mm81x_queue_params mqp;
+
+	mqp.aci = map_mac80211q_2_mm81x_aci(ac);
+	mqp.aifs = params->aifs;
+	mqp.cw_max = params->cw_max;
+	mqp.cw_min = params->cw_min;
+	mqp.uapsd = params->uapsd;
+	mqp.txop = params->txop << 5;
+
+	dev_dbg(mors->dev, "queue:%d txop:%d cw_min:%d cw_max:%d aifs:%d",
+		mqp.aci, mqp.txop, mqp.cw_min, mqp.cw_max, mqp.aifs);
+
+	ret = mm81x_cmd_cfg_qos(mors, &mqp);
+	if (ret)
+		dev_dbg(mors->dev, "mm81x_cmd_cfg_qos failed %d", ret);
+	return ret;
+}
+
+static int mm81x_mac_ops_sta_state(struct ieee80211_hw *hw,
+				   struct ieee80211_vif *vif,
+				   struct ieee80211_sta *sta,
+				   enum ieee80211_sta_state old_state,
+				   enum ieee80211_sta_state new_state)
+{
+	u16 aid;
+	int ret;
+	struct mm81x *mors = hw->priv;
+	struct mm81x_vif *mors_vif = (struct mm81x_vif *)vif->drv_priv;
+	struct mm81x_sta *mors_sta = (struct mm81x_sta *)sta->drv_priv;
+
+	/* Ignore both NOTEXIST to NONE and NONE to NOTEXIST */
+	if ((old_state == IEEE80211_STA_NOTEXIST &&
+	     new_state == IEEE80211_STA_NONE) ||
+	    (old_state == IEEE80211_STA_NONE &&
+	     new_state == IEEE80211_STA_NOTEXIST))
+		return 0;
+
+	if (vif->type == NL80211_IFTYPE_STATION)
+		aid = vif->cfg.aid;
+	else
+		aid = sta->aid;
+
+	ret = mm81x_cmd_sta_state(mors, mors_vif, aid, sta, new_state);
+	if (ret < 0)
+		goto exit;
+
+	ether_addr_copy(mors_sta->addr, sta->addr);
+	mors_sta->state = new_state;
+
+	if (new_state > old_state && new_state == IEEE80211_STA_ASSOC) {
+		if (vif->type == NL80211_IFTYPE_AP)
+			mors_vif->u.ap.num_stas++;
+		else if (vif->type == NL80211_IFTYPE_STATION)
+			mors_vif->u.sta.is_assoc = true;
+	}
+
+	if (new_state < old_state && new_state == IEEE80211_STA_NONE) {
+		if (vif->type == NL80211_IFTYPE_AP)
+			mors_vif->u.ap.num_stas--;
+		else if (vif->type == NL80211_IFTYPE_STATION)
+			mors_vif->u.sta.is_assoc = false;
+	}
+
+exit:
+	/*
+	 * Always update our mmrc sta state even on failure to ensure
+	 * we don't hold a dangling sta on error
+	 */
+	mm81x_rc_sta_state_check(mors, vif, sta, old_state, new_state);
+	return new_state < old_state ? 0 : ret;
+}
+
+static int mm81x_mac_ops_ampdu_action(struct ieee80211_hw *hw,
+				      struct ieee80211_vif *vif,
+				      struct ieee80211_ampdu_params *params)
+{
+	u16 tid = params->tid;
+	struct mm81x *mors = hw->priv;
+	struct ieee80211_sta *sta = params->sta;
+	struct mm81x_sta *mors_sta = (struct mm81x_sta *)sta->drv_priv;
+	u16 buf_size =
+		min_t(u16, params->buf_size, DOT11AH_BA_MAX_MPDU_PER_AMPDU);
+
+	switch (params->action) {
+	case IEEE80211_AMPDU_TX_START:
+		dev_dbg(mors->dev, "%pM.%d A-MPDU TX start", mors_sta->addr,
+			tid);
+		ieee80211_start_tx_ba_cb_irqsafe(vif, sta->addr, tid);
+		break;
+	case IEEE80211_AMPDU_TX_STOP_CONT:
+	case IEEE80211_AMPDU_TX_STOP_FLUSH:
+	case IEEE80211_AMPDU_TX_STOP_FLUSH_CONT:
+		dev_dbg(mors->dev, "%pM.%d A-MPDU TX flush", mors_sta->addr,
+			tid);
+		mors_sta->tid_start_tx[tid] = false;
+		mors_sta->tid_tx[tid] = false;
+		mors_sta->tid_params[tid] = 0;
+		ieee80211_stop_tx_ba_cb_irqsafe(vif, sta->addr, tid);
+		break;
+	case IEEE80211_AMPDU_TX_OPERATIONAL:
+		dev_dbg(mors->dev, "%pM.%d A-MPDU TX oper", mors_sta->addr,
+			tid);
+		mors_sta->tid_tx[tid] = true;
+		if (!buf_size) {
+			dev_err(mors->dev, "%pM.%d A-MPDU Invalid buf size",
+				mors_sta->addr, tid);
+			break;
+		}
+		mors_sta->tid_params[tid] =
+			u8_encode_bits(buf_size - 1,
+				       TX_INFO_TID_PARAMS_MAX_REORDER_BUF) |
+			u8_encode_bits(1, TX_INFO_TID_PARAMS_AMPDU_ENABLED) |
+			u8_encode_bits(params->amsdu,
+				       TX_INFO_TID_PARAMS_AMSDU_SUPPORTED);
+		break;
+	default:
+		break;
+	}
+
+	return 0;
+}
+
+static int mm81x_mac_ops_set_key(struct ieee80211_hw *hw, enum set_key_cmd cmd,
+				 struct ieee80211_vif *vif,
+				 struct ieee80211_sta *sta,
+				 struct ieee80211_key_conf *key)
+{
+	u16 aid;
+	int ret = -EOPNOTSUPP;
+	struct mm81x *mors = hw->priv;
+	struct mm81x_vif *mors_vif = (struct mm81x_vif *)vif->drv_priv;
+	enum host_cmd_key_cipher cipher;
+	enum host_cmd_aes_key_len length;
+
+	if (vif->type == NL80211_IFTYPE_STATION) {
+		aid = vif->cfg.aid;
+	} else if (sta) {
+		aid = sta->aid;
+	} else {
+		/* Is a group key - AID is unused */
+		WARN_ON(key->flags & IEEE80211_KEY_FLAG_PAIRWISE);
+		aid = 0;
+	}
+
+	switch (cmd) {
+	case SET_KEY: {
+		switch (key->cipher) {
+		case WLAN_CIPHER_SUITE_CCMP:
+		case WLAN_CIPHER_SUITE_CCMP_256:
+			cipher = HOST_CMD_KEY_CIPHER_AES_CCM;
+			break;
+		case WLAN_CIPHER_SUITE_GCMP:
+		case WLAN_CIPHER_SUITE_GCMP_256:
+			cipher = HOST_CMD_KEY_CIPHER_AES_GCM;
+			break;
+		default:
+			/* Cipher suite currently not supported */
+			ret = -EOPNOTSUPP;
+			goto exit;
+		}
+
+		switch (key->keylen) {
+		case 16:
+			length = HOST_CMD_AES_KEY_LEN_LENGTH_128;
+			break;
+		case 32:
+			length = HOST_CMD_AES_KEY_LEN_LENGTH_256;
+			break;
+		default:
+			/* Key length not supported */
+			ret = -EOPNOTSUPP;
+			goto exit;
+		}
+
+		ret = mm81x_cmd_install_key(mors, mors_vif, aid, key, cipher,
+					    length);
+		break;
+	}
+	case DISABLE_KEY:
+		ret = mm81x_cmd_disable_key(mors, mors_vif, aid, key);
+		if (ret) {
+			/* Must return 0 */
+			dev_warn(mors->dev, "Failed to remove key");
+			ret = 0;
+		}
+		break;
+	default:
+		WARN_ON(1);
+	}
+
+	if (ret) {
+		dev_dbg(mors->dev, "Falling back to software crypto");
+		ret = 1;
+	}
+
+exit:
+	return ret;
+}
+
+static int mm81x_mac_set_frag_threshold(struct ieee80211_hw *hw, int radio_idx,
+					u32 value)
+{
+	struct mm81x *mors = hw->priv;
+
+	return mm81x_cmd_set_frag_threshold(mors, value);
+}
+
+static u8 mm81x_rx_h_rc_bw_to_rx_bw(__le32 ratecode)
+{
+	enum dot11_bandwidth bw = mm81x_ratecode_bw_index_get(ratecode);
+
+	switch (bw) {
+	case DOT11_BANDWIDTH_1MHZ:
+		return RATE_INFO_BW_1;
+	case DOT11_BANDWIDTH_2MHZ:
+		return RATE_INFO_BW_2;
+	case DOT11_BANDWIDTH_4MHZ:
+		return RATE_INFO_BW_4;
+	case DOT11_BANDWIDTH_8MHZ:
+		return RATE_INFO_BW_8;
+	default:
+		return RATE_INFO_BW_1;
+	}
+}
+
+static void mm81x_rx_h_fill_status(struct mm81x *mors,
+				   struct mm81x_skb_rx_status *hdr_rx_status,
+				   struct ieee80211_rx_status *rx_status,
+				   struct sk_buff *skb)
+{
+	u32 flags = le32_to_cpu(hdr_rx_status->flags);
+	u16 freq_100khz = le16_to_cpu(hdr_rx_status->freq_100khz);
+	__le32 ratecode = hdr_rx_status->mm81x_ratecode;
+
+	rx_status->signal = le16_to_cpu(hdr_rx_status->rssi);
+	rx_status->encoding = RX_ENC_S1G;
+	rx_status->band = NL80211_BAND_S1GHZ;
+	rx_status->freq = KHZ100_TO_MHZ(freq_100khz);
+	rx_status->freq_offset = (freq_100khz % 10) ? 1 : 0;
+	rx_status->nss = NSS_IDX_TO_NSS(mm81x_ratecode_nss_index_get(ratecode));
+
+	if (flags & MM81X_RX_STATUS_FLAGS_DECRYPTED)
+		rx_status->flag |= RX_FLAG_DECRYPTED;
+
+	rx_status->rate_idx = mm81x_ratecode_mcs_index_get(ratecode);
+	rx_status->bw = mm81x_rx_h_rc_bw_to_rx_bw(ratecode);
+
+	if (mm81x_ratecode_sgi_get(ratecode))
+		rx_status->enc_flags |= RX_ENC_FLAG_SHORT_GI;
+}
+
+static void mm81x_rx_h_update_sta(struct ieee80211_vif *vif,
+				  struct ieee80211_hdr *hdr,
+				  struct ieee80211_rx_status *rx_status)
+{
+	struct ieee80211_sta *sta;
+	struct mm81x_sta *msta;
+	u8 *lookup = ieee80211_is_s1g_beacon(hdr->frame_control) ? hdr->addr1 :
+								   hdr->addr2;
+
+	lockdep_assert_in_rcu_read_lock();
+
+	sta = ieee80211_find_sta(vif, lookup);
+	if (!sta)
+		return;
+
+	msta = (void *)sta->drv_priv;
+	if (msta->avg_rssi) {
+		msta->avg_rssi =
+			CALC_AVG_RSSI(msta->avg_rssi, rx_status->signal);
+	} else {
+		msta->avg_rssi = rx_status->signal;
+	}
+}
+
+static struct ieee80211_vif *
+mm81x_rx_h_skb_get_vif(struct mm81x *mors, struct sk_buff *skb,
+		       struct mm81x_skb_rx_status *hdr_rx_status)
+{
+	u8 vif_id = u32_get_bits(le32_to_cpu(hdr_rx_status->flags),
+				 MM81X_RX_STATUS_FLAGS_VIF_ID);
+
+	lockdep_assert_in_rcu_read_lock();
+
+	if (vif_id == INVALID_VIF_INDEX)
+		return NULL;
+
+	return mm81x_rcu_dereference_vif_id(mors, vif_id, true);
+}
+
+void mm81x_mac_rx_skb(struct mm81x *mors, struct sk_buff *skb,
+		      struct mm81x_skb_rx_status *hdr_rx_status)
+{
+	struct ieee80211_vif *vif;
+	struct ieee80211_hw *hw = mors->hw;
+	struct ieee80211_rx_status rx_status;
+	struct ieee80211_hdr *hdr = (void *)skb->data;
+
+	memset(&rx_status, 0, sizeof(rx_status));
+
+	if (!mors->started || !skb->data || !skb->len) {
+		dev_kfree_skb_any(skb);
+		return;
+	}
+
+	mm81x_rx_h_fill_status(mors, hdr_rx_status, &rx_status, skb);
+
+	scoped_guard(rcu) {
+		vif = mm81x_rx_h_skb_get_vif(mors, skb, hdr_rx_status);
+		if (!vif)
+			goto rx;
+
+		mm81x_rx_h_update_sta(vif, hdr, &rx_status);
+	}
+
+rx:
+	memcpy(IEEE80211_SKB_RXCB(skb), &rx_status, sizeof(rx_status));
+	ieee80211_rx_ni(hw, skb);
+}
+
+static void mm81x_mac_flush_queues(struct mm81x *mors)
+{
+	/*
+	 * No need to call mm81x_skbq_stop_tx_queues as mac80211
+	 * has already cancelled each queue prior to calling .flush()
+	 */
+	mm81x_skbq_data_traffic_pause(mors);
+
+	flush_work(&mors->hif_work);
+	flush_work(&mors->tx_stale_work);
+
+	mm81x_hif_clear_events(mors);
+	mm81x_hif_flush_tx_data(mors);
+	mm81x_hif_flush_cmds(mors);
+
+	/* Re-enable data, not that there will be any */
+	mm81x_skbq_data_traffic_resume(mors);
+}
+
+static bool mm81x_mac_has_tx_pending(struct mm81x *mors)
+{
+	struct mm81x_skbq *mgmt_q = mm81x_hif_get_tx_mgmt_queue(mors);
+	struct mm81x_skbq *tx_qs;
+	int num_qs, i;
+
+	mm81x_hif_skbq_get_tx_qs(mors, &tx_qs, &num_qs);
+	for (i = 0; i < num_qs; i++)
+		if (mm81x_skbq_count(&tx_qs[i]) ||
+		    mm81x_skbq_pending_count(&tx_qs[i]))
+			return true;
+
+	if (mm81x_skbq_count(mgmt_q) || mm81x_skbq_pending_count(mgmt_q))
+		return true;
+
+	return false;
+}
+
+static void mm81x_mac_wait_queues(struct mm81x *mors)
+{
+	if (!wait_event_timeout(mors->tx_empty_waitq,
+				!mm81x_mac_has_tx_pending(mors),
+				MM81X_FLUSH_TIMEOUT))
+		dev_warn(mors->dev, "Unable to empty queues before timeout");
+}
+
+static void mm81x_mac_ops_flush(struct ieee80211_hw *hw,
+				struct ieee80211_vif *vif, u32 queues,
+				bool drop)
+{
+	struct mm81x *mors = hw->priv;
+
+	/* We don't support IEEE80211_HW_QUEUE_CONTROL so flush all queues */
+	if (drop)
+		mm81x_mac_flush_queues(mors);
+	else
+		mm81x_mac_wait_queues(mors);
+}
+
+static int mm81x_mac_ops_set_rts_threshold(struct ieee80211_hw *hw,
+					   int radio_idx, u32 value)
+{
+	struct mm81x *mors = hw->priv;
+
+	mors->rts_threshold = value;
+	return 0;
+}
+
+static void mm81x_mac_ops_sta_statistics(struct ieee80211_hw *hw,
+					 struct ieee80211_vif *vif,
+					 struct ieee80211_sta *sta,
+					 struct station_info *sinfo)
+{
+	struct mm81x_sta *msta = (struct mm81x_sta *)sta->drv_priv;
+	struct mm81x *mors = hw->priv;
+	const struct mmrc_table *tb = msta->rc.tb;
+	struct mmrc_rate rate;
+
+	if (!tb || tb->best_tp.rate == MMRC_MCS_UNUSED) {
+		sinfo->filled &= ~BIT_ULL(NL80211_STA_INFO_TX_BITRATE);
+		return;
+	}
+
+	rate = tb->best_tp;
+	sinfo->txrate.mcs = rate.rate;
+	sinfo->txrate.nss = NSS_IDX_TO_NSS(rate.ss);
+	sinfo->txrate.flags = RATE_INFO_FLAGS_S1G_MCS;
+	switch (rate.bw) {
+	case MMRC_BW_1MHZ:
+		sinfo->txrate.bw = RATE_INFO_BW_1;
+		break;
+	case MMRC_BW_2MHZ:
+		sinfo->txrate.bw = RATE_INFO_BW_2;
+		break;
+	case MMRC_BW_4MHZ:
+		sinfo->txrate.bw = RATE_INFO_BW_4;
+		break;
+	case MMRC_BW_8MHZ:
+		sinfo->txrate.bw = RATE_INFO_BW_8;
+		break;
+	default:
+		break;
+	}
+
+	if (rate.guard == MMRC_GUARD_SHORT)
+		sinfo->txrate.flags |= (RATE_INFO_FLAGS_SHORT_GI);
+
+	dev_dbg(mors->dev, "mcs: %d, bw: %d, flag: 0x%x", rate.rate, rate.bw,
+		sinfo->txrate.flags);
+	sinfo->filled |= BIT_ULL(NL80211_STA_INFO_TX_BITRATE);
+}
+
+static u32 mm81x_get_expected_throughput(struct ieee80211_hw *hw,
+					 struct ieee80211_sta *sta)
+{
+	struct mm81x_sta *msta = (struct mm81x_sta *)sta->drv_priv;
+	struct mm81x *mors = hw->priv;
+	const struct mmrc_table *tb = msta->rc.tb;
+	struct mmrc_rate rate;
+	u32 tput;
+
+	if (!tb || tb->best_tp.rate == MMRC_MCS_UNUSED)
+		return 0;
+
+	rate = tb->best_tp;
+	tput = BPS_TO_KBPS(mmrc_calculate_theoretical_throughput(rate));
+	dev_dbg(mors->dev, "Throughput: MCS: %d, BW: %d, GI: %d -> %u",
+		rate.rate, 1 << rate.bw, rate.guard, tput);
+
+	return tput;
+}
+
+static void mm81x_mac_restart_cleanup_iter(void *data, u8 *mac,
+					   struct ieee80211_vif *vif)
+{
+	if (vif->type == NL80211_IFTYPE_AP)
+		mm81x_mac_beacon_finish((struct mm81x_vif *)vif->drv_priv);
+}
+
+static void mm81x_mac_restart_cleanup(struct mm81x *mors)
+{
+	ieee80211_iterate_active_interfaces(mors->hw,
+					    IEEE80211_IFACE_ITER_NORMAL,
+					    mm81x_mac_restart_cleanup_iter,
+					    NULL);
+	mm81x_mac_hw_scan_finish(mors);
+}
+
+static int mm81x_mac_restart(struct mm81x *mors)
+{
+	int ret;
+	u32 chip_id;
+
+	mors->started = false;
+	mm81x_ps_disable(mors);
+	mm81x_bus_set_irq(mors, false);
+	mm81x_hw_irq_clear(mors);
+	ieee80211_stop_queues(mors->hw);
+
+	set_bit(MM81X_STATE_DATA_TX_STOPPED, &mors->state_flags);
+	set_bit(MM81X_STATE_DATA_QS_STOPPED, &mors->state_flags);
+
+	/* Allow time for in-transit tx/rx packets to settle */
+	mdelay(MM81X_HW_RESTART_DELAY_MS);
+	flush_work(&mors->hif_work);
+	flush_work(&mors->tx_stale_work);
+	mm81x_hif_clear_events(mors);
+	mm81x_hif_flush_tx_data(mors);
+	mm81x_hif_flush_cmds(mors);
+
+	mm81x_claim_bus(mors);
+	ret = mm81x_reg32_read(mors, MM81X_REG_CHIP_ID(mors), &chip_id);
+	mm81x_release_bus(mors);
+
+	if (ret < 0) {
+		dev_err(mors->dev, "Failed to access HW: %d", ret);
+		goto exit;
+	}
+
+	mm81x_mac_restart_cleanup(mors);
+
+	ret = mm81x_fw_init(mors, true);
+	if (ret < 0) {
+		dev_err(mors->dev, "Failed to init firmware: %d", ret);
+		goto exit;
+	}
+
+	mm81x_hw_irq_enable(mors, MM81X_INT_HW_STOP_NOTIFICATION_NUM, true);
+
+	ret = mm81x_fw_parse_ext_host_tbl(mors);
+	if (ret) {
+		dev_err(mors->dev, "failed to parse extended host table: %d",
+			ret);
+		goto exit;
+	}
+
+	mm81x_mac_caps_init(mors);
+
+	mm81x_bus_set_irq(mors, true);
+	clear_bit(MM81X_STATE_DATA_TX_STOPPED, &mors->state_flags);
+	clear_bit(MM81X_STATE_DATA_QS_STOPPED, &mors->state_flags);
+	clear_bit(MM81X_STATE_CHIP_UNRESPONSIVE, &mors->state_flags);
+	clear_bit(MM81X_STATE_RELOAD_FW_AFTER_START, &mors->state_flags);
+	mm81x_mac_check_fw_disabled_chans(mors->hw);
+	ieee80211_restart_hw(mors->hw);
+
+exit:
+	mm81x_ps_enable(mors);
+	return ret;
+}
+
+static int mm81x_mac_ops_add_interface(struct ieee80211_hw *hw,
+				       struct ieee80211_vif *vif)
+{
+	int ret = 0;
+	struct mm81x *mors = hw->priv;
+	struct mm81x_vif *mors_vif = (struct mm81x_vif *)vif->drv_priv;
+
+	if (test_bit(MM81X_STATE_RELOAD_FW_AFTER_START, &mors->state_flags)) {
+		dev_info(mors->dev, "Restarting chip with regdom: %s",
+			 mors->country);
+
+		ret = mm81x_mac_restart(mors);
+		if (ret) {
+			dev_err(mors->dev, "Failed to restart chip");
+			return ret;
+		}
+
+		/*
+		 * mac_restart will trigger ieee80211_hw_restart and
+		 * add_interface will re-enter. just exit here instead.
+		 */
+		return 0;
+	}
+
+	vif->driver_flags |= IEEE80211_VIF_BEACON_FILTER;
+	mors_vif->mors = mors;
+
+	ret = mm81x_cmd_add_if(mors, &mors_vif->id, vif->addr, vif->type);
+	if (ret) {
+		dev_err(mors->dev, "mm81x_cmd_add_if failed %d", ret);
+		return ret;
+	}
+
+	if (mors_vif->id >= ARRAY_SIZE(mors->vifs)) {
+		dev_err(mors->dev, "vif_id is too large %u", mors_vif->id);
+		ret = -EOPNOTSUPP;
+		return ret;
+	}
+
+	if (mors_vif->id != (mors_vif->id & MM81X_TX_CONF_FLAGS_VIF_ID_MASK)) {
+		dev_err(mors->dev, "invalid vif_id %u", mors_vif->id);
+		ret = -EOPNOTSUPP;
+		return ret;
+	}
+
+	rcu_assign_pointer(mors->vifs[mors_vif->id], vif);
+
+	if (vif->type == NL80211_IFTYPE_AP)
+		mm81x_mac_beacon_init(mors_vif);
+
+	ret = mm81x_cmd_get_capabilities(mors, mors_vif->id, &mors->fw_caps);
+	if (ret) {
+		dev_err(mors->dev,
+			"mm81x_cmd_get_capabilities failed for vif %d",
+			mors_vif->id);
+		return ret;
+	}
+
+	ieee80211_wake_queues(mors->hw);
+	return ret;
+}
+
+static const struct ieee80211_ops mm81x_ops = {
+	.start = mm81x_mac_ops_start,
+	.stop = mm81x_mac_ops_stop,
+	.config = mm81x_mac_ops_config,
+	.wake_tx_queue = ieee80211_handle_wake_tx_queue,
+	.tx = mm81x_mac_ops_tx,
+	.add_interface = mm81x_mac_ops_add_interface,
+	.remove_interface = mm81x_mac_ops_remove_interface,
+	.configure_filter = mm81x_mac_ops_configure_filter,
+	.sta_state = mm81x_mac_ops_sta_state,
+	.flush = mm81x_mac_ops_flush,
+	.set_frag_threshold = mm81x_mac_set_frag_threshold,
+	.set_rts_threshold = mm81x_mac_ops_set_rts_threshold,
+	.sta_statistics = mm81x_mac_ops_sta_statistics,
+	.get_expected_throughput = mm81x_get_expected_throughput,
+	.hw_scan = mm81x_mac_ops_hw_scan,
+	.cancel_hw_scan = mm81x_mac_ops_cancel_hw_scan,
+	.get_txpower = mm81x_mac_ops_get_txpower,
+	.bss_info_changed = mm81x_mac_ops_bss_info_changed,
+	.prepare_multicast = mm81x_mac_ops_prepare_multicast,
+	.conf_tx = mm81x_mac_ops_conf_tx,
+	.ampdu_action = mm81x_mac_ops_ampdu_action,
+	.set_key = mm81x_mac_ops_set_key,
+	.add_chanctx = ieee80211_emulate_add_chanctx,
+	.remove_chanctx = ieee80211_emulate_remove_chanctx,
+	.change_chanctx = ieee80211_emulate_change_chanctx,
+	.switch_vif_chanctx = ieee80211_emulate_switch_vif_chanctx,
+};
+
+static void mm81x_reg_notifier(struct wiphy *wiphy,
+			       struct regulatory_request *request)
+{
+	int ret;
+	struct mm81x *mors = wiphy_to_ieee80211_hw(wiphy)->priv;
+
+	if (mm81x_reg_h_cc_equal(request->alpha2, "00") ||
+	    mm81x_reg_h_cc_equal(request->alpha2, mors->country))
+		return;
+
+	memcpy(mors->country, request->alpha2, sizeof(mors->country));
+
+	ret = mm81x_mac_restart(mors);
+	if (ret)
+		dev_err(mors->dev, "Failed to restart chip: %d", ret);
+}
+
+static void mm81x_mac_config_hw(struct mm81x *mors)
+{
+	int i;
+	struct ieee80211_hw *hw = mors->hw;
+	struct wiphy *wiphy;
+
+	for (i = 0; i < NUM_NL80211_BANDS; i++)
+		hw->wiphy->bands[i] = NULL;
+
+	hw->wiphy->bands[NL80211_BAND_S1GHZ] = &mors_band_s1ghz;
+	hw->wiphy->interface_modes = BIT(NL80211_IFTYPE_AP) |
+				     BIT(NL80211_IFTYPE_STATION);
+	hw->wiphy->reg_notifier = mm81x_reg_notifier;
+	hw->queues = MM81X_HW_QUEUE_COUNT;
+	hw->max_rates = MM81X_HW_MAX_RATES;
+	hw->max_report_rates = MM81X_HW_MAX_REPORT_RATES;
+	hw->max_rate_tries = MM81X_HW_MAX_RATE_TRIES;
+	hw->tx_sk_pacing_shift = MM81X_HW_TX_SK_PACING_SHIFT;
+	hw->vif_data_size = sizeof(struct mm81x_vif);
+	hw->sta_data_size = sizeof(struct mm81x_sta);
+	hw->extra_tx_headroom =
+		sizeof(struct mm81x_skb_hdr) + mm81x_bus_get_alignment(mors);
+
+	mors->wiphy = hw->wiphy;
+
+	ieee80211_hw_set(hw, SIGNAL_DBM);
+	ieee80211_hw_set(hw, MFP_CAPABLE);
+	ieee80211_hw_set(hw, REPORTS_TX_ACK_STATUS);
+	ieee80211_hw_set(hw, AMPDU_AGGREGATION);
+	ieee80211_hw_set(hw, HOST_BROADCAST_PS_BUFFERING);
+	ieee80211_hw_set(hw, HAS_RATE_CONTROL);
+	ieee80211_hw_set(hw, SUPPORTS_PS);
+	ieee80211_hw_set(hw, NEED_DTIM_BEFORE_ASSOC);
+	ieee80211_hw_set(hw, PS_NULLFUNC_STACK);
+	ieee80211_hw_set(hw, SUPPORTS_TX_FRAG);
+	ieee80211_hw_set(hw, SUPPORTS_NDP_BLOCKACK);
+
+	SET_IEEE80211_PERM_ADDR(hw, mors->macaddr);
+
+	wiphy = mors->wiphy;
+
+	wiphy->flags |= WIPHY_FLAG_AP_UAPSD;
+	wiphy->flags |= WIPHY_FLAG_PS_ON_BY_DEFAULT;
+
+	if (!mors->ps.enable)
+		wiphy->flags &= ~WIPHY_FLAG_PS_ON_BY_DEFAULT;
+
+	wiphy->features |= NL80211_FEATURE_AP_MODE_CHAN_WIDTH_CHANGE |
+			   NL80211_FEATURE_TX_POWER_INSERTION;
+
+	wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_AIRTIME_FAIRNESS);
+	wiphy_ext_feature_set(wiphy, NL80211_EXT_FEATURE_SET_SCAN_DWELL);
+
+	wiphy->iface_combinations = mors_if_combs;
+	wiphy->n_iface_combinations = ARRAY_SIZE(mors_if_combs);
+	wiphy->max_scan_ie_len = MM81X_MAX_SCAN_IE_LEN;
+	wiphy->max_scan_ssids = MM81X_MAX_SCAN_SSIDS;
+	wiphy->signal_type = CFG80211_SIGNAL_TYPE_MBM;
+	wiphy->max_remain_on_channel_duration =
+		MM81X_MAX_REMAIN_ON_CHAN_DURATION;
+}
+
+static void mm81x_stale_tx_status_timer(struct timer_list *t)
+{
+	struct mm81x *mors = timer_container_of(mors, t, stale_status.timer);
+
+	spin_lock_bh(&mors->stale_status.lock);
+	if (mm81x_hif_get_tx_status_pending_count(mors))
+		queue_work(mors->net_wq, &mors->tx_stale_work);
+	spin_unlock_bh(&mors->stale_status.lock);
+}
+
+static void mm81x_stale_tx_status_timer_finish(struct mm81x *mors)
+{
+	timer_delete_sync_try(&mors->stale_status.timer);
+}
+
+static void mm81x_mac_stale_tx_status_timer_init(struct mm81x *mors)
+{
+	spin_lock_init(&mors->stale_status.lock);
+	timer_setup(&mors->stale_status.timer, mm81x_stale_tx_status_timer, 0);
+}
+
+int mm81x_mac_register(struct mm81x *mors)
+{
+	int ret;
+	struct ieee80211_hw *hw = mors->hw;
+
+	mors->tx_power_mbm = INT_MAX;
+	mors->tx_max_power_mbm = INT_MAX;
+	mors->rts_threshold = IEEE80211_MAX_RTS_THRESHOLD;
+
+	ret = mm81x_ps_init(mors);
+	if (ret)
+		return ret;
+
+	mm81x_mac_config_hw(mors);
+	mm81x_mac_hw_scan_init(mors);
+	mm81x_mac_stale_tx_status_timer_init(mors);
+
+	ret = ieee80211_register_hw(hw);
+	if (ret) {
+		dev_err(mors->dev, "ieee80211_register_hw failed %d", ret);
+		mm81x_mac_unregister(mors);
+		return ret;
+	}
+
+	mm81x_rc_init(mors);
+
+	/*
+	 * At this stage, we know bus and pager system interrupts are enabled.
+	 * Trigger the receive workqueue to drain any incoming chip-to-host
+	 * pending packets been pushed in the period between the firmware
+	 * initialization and interrupts being enabled.
+	 */
+	set_bit(MM81X_HIF_EVT_RX_PEND, &mors->hif.event_flags);
+	queue_work(mors->chip_wq, &mors->hif_work);
+
+	return ret;
+}
+
+void mm81x_mac_unregister(struct mm81x *mors)
+{
+	mm81x_ps_disable(mors);
+	mm81x_rc_deinit(mors);
+	mm81x_mac_hw_scan_destroy(mors);
+
+	ieee80211_stop_queues(mors->hw);
+	ieee80211_unregister_hw(mors->hw);
+
+	mm81x_hif_flush_tx_data(mors);
+	mm81x_hif_flush_cmds(mors);
+	mm81x_stale_tx_status_timer_finish(mors);
+	mm81x_ps_finish(mors);
+
+	kfree(mors->mcast_filter);
+}
+
+struct mm81x *mm81x_mac_alloc(size_t priv_size, struct device *dev)
+{
+	struct ieee80211_hw *hw;
+	struct mm81x *mors;
+
+	hw = ieee80211_alloc_hw(sizeof(*mors) + priv_size, &mm81x_ops);
+	if (!hw) {
+		dev_err(dev, "ieee80211_alloc_hw failed\r\n");
+		return NULL;
+	}
+
+	SET_IEEE80211_DEV(hw, dev);
+	memset(hw->priv, 0, sizeof(*mors));
+
+	mors = hw->priv;
+	mors->hw = hw;
+	mors->dev = dev;
+	mutex_init(&mors->cmd_lock);
+	mutex_init(&mors->cmd_wait);
+	init_waitqueue_head(&mors->tx_empty_waitq);
+
+	return mors;
+}
+
+void mm81x_mac_free(struct mm81x *mors)
+{
+	ieee80211_free_hw(mors->hw);
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 15/33] wifi: mm81x: add mac.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/mac.h | 63 +++++++++++++++++++++
 1 file changed, 63 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/mac.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/mac.h b/drivers/net/wireless/morsemicro/mm81x/mac.h
new file mode 100644
index 000000000000..c540471f274e
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/mac.h
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_MAC_H_
+#define _MM81X_MAC_H_
+
+#include "core.h"
+#include "command.h"
+
+struct mm81x_queue_params {
+	u8 uapsd;
+	u8 aci;
+	u8 aifs;
+	u16 cw_min;
+	u16 cw_max;
+	u32 txop;
+};
+
+static inline u32 mm81x_vif_generate_cssid(struct ieee80211_vif *vif)
+{
+	return mm81x_generate_cssid(vif->cfg.ssid, vif->cfg.ssid_len);
+}
+
+/*
+ * Build a little-endian word from the last four octets of a MAC address;
+ * the first two octets are dropped.
+ */
+static inline __le32 mac2le32(const unsigned char *addr)
+{
+	return cpu_to_le32(((u32)(addr[2]) << 24) | ((u32)(addr[3]) << 16) |
+			   ((u32)(addr[4]) << 8) | ((u32)(addr[5])));
+}
+
+static inline struct ieee80211_vif *
+mm81x_rcu_dereference_vif_id(struct mm81x *mors, u8 vif_id, bool rcu)
+{
+	if (WARN_ON(vif_id >= ARRAY_SIZE(mors->vifs)))
+		return NULL;
+
+	if (rcu)
+		return rcu_dereference(mors->vifs[vif_id]);
+
+	return rcu_dereference_protected(mors->vifs[vif_id],
+					 lockdep_is_held(&mors->hw->wiphy->mtx));
+}
+
+int mm81x_tx_h_get_attempts(struct mm81x *mors,
+			    struct mm81x_skb_tx_status *tx_sts);
+struct mm81x *mm81x_mac_alloc(size_t priv_size, struct device *dev);
+int mm81x_mac_register(struct mm81x *mors);
+void mm81x_mac_free(struct mm81x *mors);
+void mm81x_mac_unregister(struct mm81x *mors);
+int mm81x_mac_event_recv(struct mm81x *mors, struct sk_buff *skb);
+void mm81x_mac_rx_skb(struct mm81x *mors, struct sk_buff *skb,
+		      struct mm81x_skb_rx_status *hdr_rx_status);
+void mm81x_mac_beacon_irq_handle(struct mm81x *mors, u32 status);
+
+u8 *mm81x_hw_scan_h_insert_tlvs(struct mm81x_hw_scan_params *params, u8 *buf);
+size_t mm81x_hw_scan_h_get_cmd_size(struct mm81x_hw_scan_params *params);
+void mm81x_tx_h_check_aggr(struct ieee80211_sta *pubsta, struct sk_buff *skb);
+#endif /* !_MM81X_MAC_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 16/33] wifi: mm81x: add mmrc.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/mmrc.c | 1354 ++++++++++++++++++
 1 file changed, 1354 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/mmrc.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/mmrc.c b/drivers/net/wireless/morsemicro/mm81x/mmrc.c
new file mode 100644
index 000000000000..fe7e4f501d6c
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/mmrc.c
@@ -0,0 +1,1354 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include "mmrc.h"
+
+/*
+ * The default packet size in bits used for calculated throughput of a given
+ * rate
+ */
+#define DEFAULT_PACKET_SIZE_BITS 9600
+
+/*
+ * The default packet size in bytes used for calculating retries for a given
+ * rate
+ */
+#define DEFAULT_PACKET_SIZE_BYTES 1200
+
+/* The sample frequencies at different stages */
+#define LOOKAROUND_RATE_INIT 5
+#define LOOKAROUND_RATE_NORMAL 50
+#define LOOKAROUND_RATE_STABLE 100
+
+/* The thresholds for stability stages */
+#define STABILITY_CNT_THRESHOLD_INIT 20
+#define STABILITY_CNT_THRESHOLD_NORMAL 50
+#define STABILITY_CNT_THRESHOLD_STABLE 100
+
+/* The backoff step size for the counter */
+#define STABILITY_BACKOFF_STEP 2
+
+/*
+ * The packet success threshold for attempting slower lookaround rates
+ */
+/*
+ * Force a look around if there haven't been any for this number of cycles
+ */
+#define LOOKAROUND_MAX_RC_CYCLES 5
+
+/*
+ * Number of attempts for each lookaround rate within at most two RC cycles
+ * if there are enough packets
+ */
+#define LOOKAROUND_RATE_ATTEMPTS 4
+
+/*
+ * Limit the number of times we try to pick a theoretically better rate to
+ * sample. Necessary so we don't stall the CPU, due to constantly picking worse
+ * rates.
+ */
+#define LOOKAROUND_FAIL_MAX 200
+
+/*
+ * Initial and reset probability per rate in the table
+ * Changing this value will have a severe implication on the current heuristic
+ * It could mean that some rates will have better probability throughput even
+ * with no edivence and so will cause unexpected changes in the rate table
+ */
+#define RATE_INIT_PROBABILITY 0
+
+/*
+ * The lowest number of MPDUs within acknowledged AMPDUs that can be used for
+ * rate stats
+ */
+#define AMPDU_STATS_MIN 2
+
+/*
+ * The lowest number of stats to be used for processing in NORMAL lookaround
+ * mode
+ */
+#define STATS_MIN_NORMAL 2
+
+/*
+ * The lowest number of stats to be used for processing in INIT lookaround
+ * mode
+ */
+#define STATS_MIN_INIT 1
+
+/* The lowest probability value considered for recognising a dip */
+#define PROBABILITY_DIP_MIN 20
+
+/* The lowest probability value for recovering from a dip */
+#define PROBABILITY_DIP_RECOVERY_MIN 40
+
+/*
+ * The time cap on rate allocation for multiple attempts. If a single attempt
+ * exceeds this window, no additional attempts will be generated
+ */
+#define MAX_WINDOW_ATTEMPT_TIME 4000
+
+/* The time window for all rates in rate table */
+#define RATE_WINDOW_MICROSECONDS 24000
+
+/*
+ * EWMA is the alpha coefficient in the exponential weighting moving average
+ * filter used for probability updates.
+ *
+ * Y[n] = X[n] * (100 - EWMA) + (Y[n-1] * EWMA)
+ *	  -------------------------------------
+ *			   100
+ *
+ */
+#define EWMA 75
+
+/*
+ * Evidence scaling to allow for one decimal place. Needed for low
+ * throughput, otherwise the history decays in a single cycle.
+ */
+#define EVIDENCE_SCALE 5
+
+/*
+ * Evidence maximum to ensure history doesn't decay too slowly when
+ * there is a lot of historical data.
+ */
+#define EVIDENCE_MAX 100
+
+/*
+ * This fixed point conversion multiplies a value by one and shifts it
+ * accordingly to account for the fixed point shifting at the return of a
+ * function
+ */
+#define FP_8_MULT_1 256
+
+/* Fixed point conversion for 2.1 * 2^8 used for 4MHz symbol multiplication */
+#define FP_8_4MHZ 537
+
+/* Fixed point conversion for 4.5 * 2^8 used for 8MHz symbol multiplication */
+#define FP_8_8MHZ 1152
+
+/* Fixed point conversion for 9.0 * 2^8 used for 16MHz symbol multiplication */
+#define FP_8_16MHZ 2301
+
+/*
+ * Fixed point conversion for 3.6 * 2^8 used for long guard symbol tx time
+ * multiplication
+ */
+#define FP_8_LONG_GUARD_SYMBOL_TIME 1024
+
+/*
+ * Fixed point conversion for 4.0 * 2^8 used for short guard symbol tx time
+ * multiplication
+ */
+#define FP_8_SHORT_GUARD_SYMBOL_TIME 921
+
+/*
+ * Shift value to shift back our FP conversions
+ */
+#define FP_8_SHIFT 8
+
+/*
+ * Limit to count of consecutive variations in one direction
+ */
+#define MAX_VARIATION_DIRECTION 5
+
+/*
+ * Threshold for considering consecutive variation direction as variation
+ * or not
+ */
+#define VARIATION_DIRECTION_THRESHOLD 3
+
+/* EWMA percentage value for averaging the best rate probability variation */
+#define VARIATION_EWMA 95
+
+/* Percentage variation regarded as minor */
+#define MINOR_VARIATION_THRESHOLD 1
+
+/* Percentage variation regarded as moderate */
+#define MODERATE_VARIATION_THRESHOLD 3
+
+/* Percentage variation regarded as significant */
+#define SIGNIFICANT_VARIATION_THRESHOLD 5
+
+/* If the best rate changes twice in this number of cycles, it is unstable */
+#define BEST_RATE_UNSTABLE_THRESHOLD 4
+
+/*
+ * Once the best rate is unchanged for this number of cycles it has
+ * converged
+ */
+#define BEST_RATE_CONVERGED_THRESHOLD 10
+
+/* RSSI threshold for short range */
+#define MMRC_SHORT_RANGE_RSSI_LIMIT -70
+
+/* RSSI threshold for mid range */
+#define MMRC_MID_RANGE_RSSI_LIMIT -85
+
+#define MMRC_MAX_BW(bw_caps)                                \
+	(((bw_caps) & BIT(MMRC_BW_16MHZ)) ? MMRC_BW_16MHZ : \
+	 ((bw_caps) & BIT(MMRC_BW_8MHZ))  ? MMRC_BW_8MHZ :  \
+	 ((bw_caps) & BIT(MMRC_BW_4MHZ))  ? MMRC_BW_4MHZ :  \
+	 ((bw_caps) & BIT(MMRC_BW_2MHZ))  ? MMRC_BW_2MHZ :  \
+					    MMRC_BW_1MHZ)
+
+/*
+ * This table stores the number of bits per symbols used for MCS0-MCS9 based
+ * on 20MHz and 1SS
+ */
+static const u32 sym_table[10] = { 24, 36, 48, 72, 96, 144, 192, 216, 256, 288 };
+
+/*
+ * Calculate which bit is the nth bit set in an integer based flag.
+ */
+static u8 nth_bit(u16 in, u16 index)
+{
+	u32 i;
+	u8 count = 0;
+
+	for (i = 0; count != index + 1; i++) {
+		if (((1u << i) & in) != 0)
+			count++;
+	}
+
+	return i - 1;
+}
+
+/*
+ * Calculate the input bit's index among all the set bits in an integer
+ * based flag.
+ */
+static u16 bit_index(u16 in, u32 bit_pos)
+{
+	u16 i;
+	u16 index = 0;
+
+	for (i = 0; i != bit_pos + 1; i++) {
+		if (((1u << i) & in) != 0)
+			index++;
+	}
+
+	if (index == 0) {
+		/* Could not match bit pos to caps */
+		return 0;
+	}
+
+	return index - 1;
+}
+
+static u16 rows_from_sta_caps(struct mmrc_sta_capabilities *caps)
+{
+	u16 rows = 0;
+	u8 n_rates = hweight_long(caps->rates);
+
+	/* Taking MCS10 into account as it is relevant for 1 MHz entries */
+	if (caps->rates & BIT(MMRC_MCS10)) {
+		n_rates -= 1;
+		rows = 2;
+	}
+
+	rows += (hweight_long(caps->bandwidth) * n_rates *
+		 hweight_long(caps->guard) *
+		 hweight_long(caps->spatial_streams));
+
+	return rows;
+}
+
+static void rate_update_index(struct mmrc_table *tb, struct mmrc_rate *rate)
+{
+	u16 index = 0;
+	/* Information about our rates */
+	u16 bw = hweight_long(tb->caps.bandwidth);
+	u16 streams = hweight_long(tb->caps.spatial_streams);
+	u16 guard = hweight_long(tb->caps.guard);
+	u16 rows = rows_from_sta_caps(&tb->caps);
+
+	index = bit_index(tb->caps.guard, rate->guard) +
+		bit_index(tb->caps.bandwidth, rate->bw) * guard +
+		bit_index(tb->caps.spatial_streams, rate->ss) * guard * bw +
+		bit_index(tb->caps.rates, rate->rate) * bw * streams * guard;
+
+	if (index >= rows)
+		index = 0;
+
+	rate->index = index;
+}
+
+static struct mmrc_rate get_rate_row(struct mmrc_table *tb, u16 index)
+{
+	struct mmrc_rate rate;
+	u16 ss_index;
+
+	/* Information about our rates */
+	u16 mcs = hweight_long(tb->caps.rates);
+	u16 bw = hweight_long(tb->caps.bandwidth);
+	u16 streams = hweight_long(tb->caps.spatial_streams);
+	u16 guard = hweight_long(tb->caps.guard);
+	u16 total_caps = mcs * bw * streams * guard;
+
+	/* Find our MCS */
+	u16 rows = total_caps / mcs;
+	u16 mcs_index = index / rows;
+	u16 mcs_modulo = index % rows;
+
+	mcs = nth_bit(tb->caps.rates, mcs_index);
+
+	/* Find our spatial stream */
+	rows = rows / streams;
+	streams = nth_bit(tb->caps.spatial_streams, mcs_modulo / rows);
+
+	/* Find our bandwidth */
+	ss_index = index % rows;
+	rows = rows / bw;
+	bw = nth_bit(tb->caps.bandwidth, ss_index / rows);
+
+	/* Find our guard */
+	guard = nth_bit(tb->caps.guard, index % guard);
+
+	/* Add range checks to keep scan-build happy */
+	if (bw >= MMRC_BW_MAX)
+		bw = MMRC_BW_1MHZ;
+
+	if (guard >= MMRC_GUARD_MAX)
+		guard = MMRC_GUARD_LONG;
+
+	/* Validate guard against capability */
+	if (guard == MMRC_GUARD_SHORT &&
+	    !(tb->caps.sgi_per_bw & SGI_PER_BW(bw)))
+		guard = MMRC_GUARD_LONG;
+
+	/* Create our rate row and send it */
+	rate.bw = MMRC_BW_TO_BITFIELD(bw);
+	rate.ss = MMRC_SS_TO_BITFIELD(streams);
+	rate.rate = MMRC_RATE_TO_BITFIELD(mcs);
+	rate.guard = MMRC_GUARD_TO_BITFIELD(guard);
+	rate.attempts = 0;
+	rate.flags = 0;
+
+	/* Update index as bw or guard may have changed */
+	rate_update_index(tb, &rate);
+
+	return rate;
+}
+
+size_t mmrc_memory_required_for_caps(struct mmrc_sta_capabilities *caps)
+{
+	return sizeof(struct mmrc_table) +
+	       rows_from_sta_caps(caps) * sizeof(struct mmrc_stats_table);
+}
+
+static u32 calculate_bits_per_symbol(struct mmrc_rate *rate)
+{
+	u32 bps;
+
+	/* If MCS10 is selected we return 2*MCS0 Symbols */
+	if (rate->rate == MMRC_MCS10)
+		return 6;
+
+	/* Confirm that the rate is valid for the sym_table lookup */
+	if (rate->rate >= MMRC_MCS_UNUSED) {
+		pr_err("%s: Invalid MCS rate %d for sym_table lookup\n",
+		       __func__, rate->rate);
+		return 1;
+	}
+
+	/*
+	 * Coversion from 20MHz as in sym_table to:
+	 * 40MHz   ==	x 2.1
+	 * 80MHz   ==	x 4.5
+	 * 160MHz  ==	x 9.0
+	 */
+	bps = sym_table[rate->rate];
+	switch (rate->bw) {
+	case (MMRC_BW_4MHZ):
+		bps *= FP_8_4MHZ;
+		break;
+	case (MMRC_BW_8MHZ):
+		bps *= FP_8_8MHZ;
+		break;
+	case (MMRC_BW_16MHZ):
+		bps *= FP_8_16MHZ;
+		break;
+	case (MMRC_BW_1MHZ):
+		bps = sym_table[rate->rate] * 24 / 52;
+		bps *= FP_8_MULT_1;
+		break;
+	case (MMRC_BW_2MHZ):
+	case (MMRC_BW_MAX):
+	default:
+		bps *= FP_8_MULT_1;
+		break;
+	}
+	/* SS + 1 because mmrc_spatial_stream starts at 0 */
+	return ((rate->ss + 1) * bps) >> FP_8_SHIFT;
+}
+
+static u32 get_tx_time(struct mmrc_rate *rate)
+{
+	u32 tx = 0;
+	u32 n_sym;
+	u32 avg_bits;
+
+	/* Calculate tx time based on a default packet size */
+	avg_bits = DEFAULT_PACKET_SIZE_BITS;
+
+	/* Number of bits per symbol for this rate */
+	n_sym = calculate_bits_per_symbol(rate);
+
+	/* In case of bad calcuation/parameter use lowest value */
+	n_sym = n_sym == 0 ? sym_table[0] : n_sym;
+
+	/* number of symbols in default packet size */
+	n_sym = avg_bits / n_sym;
+
+	/* tx is time to transmit average packet in us */
+	switch (rate->guard) {
+	case (MMRC_GUARD_LONG):
+		tx = n_sym * FP_8_LONG_GUARD_SYMBOL_TIME;
+		break;
+	case (MMRC_GUARD_SHORT):
+		tx = n_sym * FP_8_SHORT_GUARD_SYMBOL_TIME;
+		break;
+	default:
+		return 0;
+	}
+
+	return (tx * 10) >> FP_8_SHIFT;
+}
+
+u32 mmrc_calculate_theoretical_throughput(struct mmrc_rate rate)
+{
+	static const u32 s1g_tpt_lgi[4][11] = {
+		{ 300, 600, 900, 1200, 1800, 2400, 2700, 3000, 3600, 4000,
+		  150 },
+		{ 650, 1300, 1950, 2600, 3900, 5200, 5850, 6500, 7800, 0, 0 },
+		{ 1350, 2700, 4050, 5400, 8100, 10800, 12150, 13500, 16200,
+		  18000, 0 },
+		{ 2925, 5850, 8775, 11700, 17550, 23400, 26325, 29250, 35100,
+		  39000, 0 },
+	};
+
+	static const u32 s1g_tpt_sgi[4][11] = {
+		{ 333, 666, 1000, 1333, 2000, 2666, 3000, 3333, 4000, 4444,
+		  166 },
+		{ 722, 1444, 2166, 2888, 4333, 5777, 6500, 7222, 8666, 0, 0 },
+		{ 1500, 3000, 4500, 6000, 9000, 12000, 13500, 15000, 18000,
+		  20000, 0 },
+		{ 3250, 6500, 9750, 13000, 19500, 26000, 29250, 32500, 39000,
+		  43333, 0 },
+	};
+
+	if (rate.guard)
+		return s1g_tpt_sgi[rate.bw][rate.rate] * 1000 * (rate.ss + 1);
+
+	return s1g_tpt_lgi[rate.bw][rate.rate] * 1000 * (rate.ss + 1);
+}
+
+static u32 calculate_throughput(struct mmrc_table *tb, u8 index)
+{
+	struct mmrc_rate rate = get_rate_row(tb, index);
+
+	/*
+	 * Avoid the overflow (observed for 8MHz MCS9 rate: 43333) by dividing
+	 * first before multiplying. Should not experience any loss of
+	 * precision as the throughput is already multiplied by 1000 in
+	 * mmrc_calculate_theoretical_throughput (returned as bits/sec)
+	 */
+	if (tb->table[rate.index].prob < 10)
+		return 0;
+	else if (rate.index == tb->best_tp.index && tb->interference_likely)
+		/*
+		 * Assist the best rate by increasing the probability by the
+		 * averaged variation
+		 */
+		return (mmrc_calculate_theoretical_throughput(rate) / 100) *
+		       (tb->table[rate.index].prob + tb->probability_variation);
+	else
+		return (mmrc_calculate_theoretical_throughput(rate) / 100) *
+		       tb->table[rate.index].prob;
+}
+
+static bool validate_rate(struct mmrc_table *tb, struct mmrc_rate *rate)
+{
+	if (rate->rate == MMRC_MCS10 &&
+	    (rate->bw != MMRC_BW_1MHZ || rate->ss != MMRC_SPATIAL_STREAM_1)) {
+		/*
+		 * 802.11ah does not support MCS10 with BW that is not 1MHz or
+		 * not 1 spatial stream.
+		 */
+		return false;
+	}
+
+	if (rate->rate == MMRC_MCS9 && rate->bw == MMRC_BW_2MHZ &&
+	    rate->ss != MMRC_SPATIAL_STREAM_3) {
+		/*
+		 * 802.11ah does not support MCS9 at 2MHz for 1, 2 or 4 spatial
+		 * streams
+		 */
+		return false;
+	}
+
+	if (rate->guard == MMRC_GUARD_SHORT &&
+	    !(tb->caps.sgi_per_bw & SGI_PER_BW(rate->bw)))
+		return false;
+
+	return true;
+}
+
+static u16 find_baseline_index(struct mmrc_table *tb)
+{
+	u32 i, theoretical_tp, min_theoretical_tp;
+	u16 row_count = rows_from_sta_caps(&tb->caps);
+	u16 min_theoretical_tp_index = 0;
+	struct mmrc_rate rate;
+
+	if (tb->caps.rates & BIT(MMRC_MCS10))
+		return 0;
+
+	min_theoretical_tp =
+		mmrc_calculate_theoretical_throughput(get_rate_row(tb, 0));
+	for (i = 0; i < row_count; i++) {
+		rate = get_rate_row(tb, i);
+		if (!validate_rate(tb, &rate))
+			continue;
+
+		theoretical_tp = mmrc_calculate_theoretical_throughput(rate);
+		if (min_theoretical_tp > theoretical_tp) {
+			min_theoretical_tp = theoretical_tp;
+			min_theoretical_tp_index = rate.index;
+		}
+	}
+
+	return min_theoretical_tp_index;
+}
+
+/*
+ * Fill out the remaining rates to be used once the best rate is selected.
+ * Normally the retry rates are one MCS lower than the previous, however in
+ * unconverged mode we limit the 3 respective retry rates to MCS 4, 2 and 0
+ * respectively. The last retry rate is always MCS 0
+ */
+static void mmrc_fill_retry_rates(struct mmrc_table *tb)
+{
+	tb->second_tp = tb->best_tp;
+	if (tb->second_tp.rate != MMRC_MCS0) {
+		tb->second_tp.rate--;
+		if (tb->unconverged && tb->second_tp.rate > MMRC_MCS4)
+			tb->second_tp.rate = MMRC_MCS4;
+		rate_update_index(tb, &tb->second_tp);
+	} else if (tb->second_tp.bw > MMRC_BW_1MHZ) {
+		tb->second_tp.bw--;
+		rate_update_index(tb, &tb->second_tp);
+	}
+
+	tb->best_prob = tb->second_tp;
+	if (tb->best_prob.rate != MMRC_MCS0) {
+		tb->best_prob.rate--;
+		if (tb->unconverged && tb->best_prob.rate > MMRC_MCS2)
+			tb->best_prob.rate = MMRC_MCS2;
+		rate_update_index(tb, &tb->best_prob);
+	} else if (tb->best_prob.bw > MMRC_BW_1MHZ) {
+		tb->best_prob.bw--;
+		rate_update_index(tb, &tb->best_prob);
+	}
+
+	tb->baseline = tb->best_prob;
+	if (tb->baseline.rate != MMRC_MCS0) {
+		tb->baseline.rate = MMRC_MCS0;
+		rate_update_index(tb, &tb->baseline);
+	} else if (tb->baseline.bw > MMRC_BW_1MHZ) {
+		tb->baseline.bw--;
+		rate_update_index(tb, &tb->baseline);
+	}
+}
+
+/*
+ * Updates the mmrc_table with the appropriate rate priority based on the
+ * latest update statistics
+ */
+static void generate_table_priority(struct mmrc_table *tb, u32 new_stats)
+{
+	u16 i;
+	u16 best_row = tb->best_tp.index;
+	u16 prev_best_row = best_row;
+	u8 prev_best_rate = tb->best_tp.rate;
+	u16 second_best_row = tb->second_tp.index;
+	u32 best_tp = calculate_throughput(tb, best_row);
+	u32 second_best_tp = calculate_throughput(tb, second_best_row);
+	u32 last_nonzero_prob = 0;
+	struct mmrc_rate tmp;
+	u32 tmp_tp;
+
+	/* Use fixed rate if set */
+	if (tb->fixed_rate.rate != MMRC_MCS_UNUSED) {
+		tb->best_tp = tb->fixed_rate;
+		tb->second_tp = tb->fixed_rate;
+		tb->best_prob = tb->fixed_rate;
+		return;
+	}
+
+	for (i = 0; i < rows_from_sta_caps(&tb->caps); i++) {
+		tmp = get_rate_row(tb, i);
+		if (!validate_rate(tb, &tmp))
+			continue;
+
+		if (tb->table[tmp.index].evidence == 0)
+			continue;
+
+		/*
+		 * Besides better throughput, also consider this rate better if
+		 * lower rates had worse probability. That indicates the rate
+		 * itself is not the problem. Only do the probability check for
+		 * rates up to the previous best rate.
+		 */
+		tmp_tp = calculate_throughput(tb, tmp.index);
+
+		if (tmp_tp > best_tp ||
+		    (tb->table[tmp.index].max_throughput <=
+			     tb->table[prev_best_row].max_throughput &&
+		     tb->table[tmp.index].prob >=
+			     PROBABILITY_DIP_RECOVERY_MIN &&
+		     tb->table[tmp.index].prob >
+			     tb->table[last_nonzero_prob].prob)) {
+			second_best_row = best_row;
+			second_best_tp = best_tp;
+
+			best_tp = tmp_tp;
+			best_row = tmp.index;
+		} else if (tmp_tp > second_best_tp && best_row != tmp.index) {
+			second_best_tp = tmp_tp;
+			second_best_row = tmp.index;
+		}
+
+		if (tb->table[tmp.index].prob >= PROBABILITY_DIP_MIN &&
+		    tb->table[tmp.index].max_throughput >=
+			    tb->table[last_nonzero_prob].max_throughput)
+			last_nonzero_prob = tmp.index;
+	}
+
+	/* Only update rates and stability when there are new statistics */
+	if (!new_stats)
+		return;
+
+	tb->best_tp = get_rate_row(tb, best_row);
+	if (best_tp == 0 && tb->best_tp.rate > MMRC_MCS0) {
+		/* Drop one rate, as the best throughput is zero */
+		tb->best_tp.rate--;
+		rate_update_index(tb, &tb->best_tp);
+	}
+	tb->second_tp = get_rate_row(tb, second_best_row);
+	mmrc_fill_retry_rates(tb);
+
+	if (tb->best_tp.rate > MMRC_MCS1 && prev_best_row == best_row) {
+		/* Increase the counter when the best rate is not changed */
+		tb->stability_cnt++;
+	} else if (tb->stability_cnt > STABILITY_BACKOFF_STEP) {
+		/* Back off the counter when there is a new best rate */
+		tb->stability_cnt -= STABILITY_BACKOFF_STEP;
+	} else {
+		tb->stability_cnt = 0;
+	}
+
+	if (prev_best_row != best_row) {
+		s8 latest_best_rate_diff = prev_best_rate - tb->best_tp.rate;
+		u8 total_abs_best_rate_diff =
+			abs(tb->best_rate_diff[0] + tb->best_rate_diff[1] +
+			    latest_best_rate_diff);
+
+		if (!tb->interference_likely) {
+			tb->probability_variation = 0;
+			if (!tb->unconverged &&
+			    tb->best_rate_cycle_count <=
+				    BEST_RATE_UNSTABLE_THRESHOLD &&
+			    total_abs_best_rate_diff >= 2) {
+				/*
+				 * Best rate has changed twice in a few cycles
+				 * and moved at least 2 MCSs from where it was
+				 * 3 best rate changes ago
+				 */
+				tb->unconverged = true;
+				tb->newly_unconverged = true;
+			}
+		}
+		if (tb->unconverged && !tb->newly_unconverged &&
+		    total_abs_best_rate_diff < 2) {
+			/*
+			 * Best rate has been relatively stable (not moved more
+			 * than 1 MCS after the last 3 rate changes), go back
+			 * to converged
+			 */
+			tb->unconverged = false;
+		}
+		tb->probability_variation_direction = 0;
+		tb->best_rate_cycle_count = 0;
+		tb->best_rate_diff[0] = tb->best_rate_diff[1];
+		tb->best_rate_diff[1] = latest_best_rate_diff;
+	} else {
+		tb->best_rate_cycle_count++;
+		if (tb->unconverged && !tb->newly_unconverged &&
+		    tb->best_rate_cycle_count >=
+			    BEST_RATE_CONVERGED_THRESHOLD) {
+			/*
+			 * Best rate has been stable for a while, go back to
+			 * converged
+			 */
+			tb->unconverged = false;
+		}
+	}
+
+	if (tb->newly_unconverged)
+		tb->newly_unconverged = false;
+}
+
+static u32 calculate_attempt_time(struct mmrc_rate *rate, size_t size)
+{
+	u32 time;
+
+	time = get_tx_time(rate);
+
+	if (size > DEFAULT_PACKET_SIZE_BYTES)
+		time = (time * ((size * 1000) / DEFAULT_PACKET_SIZE_BYTES)) /
+		       1000;
+	else
+		time = (time * 1000) /
+		       ((DEFAULT_PACKET_SIZE_BYTES * 1000) / size);
+
+	return time;
+}
+
+u32 mmrc_calculate_rate_tx_time(struct mmrc_rate *rate, size_t size)
+{
+	u8 i;
+	u32 total_time = 0;
+
+	for (i = 0; i < rate->attempts; i++)
+		total_time += calculate_attempt_time(rate, size);
+
+	return total_time;
+}
+
+/*
+ * Calculates the appropriate amount of additional attempts to make based on
+ * packet size and theoretical throughput.
+ */
+static void calculate_remaining_attempts(struct mmrc_table *tb,
+					 struct mmrc_rate_table *rate,
+					 s32 *rem_time, size_t size)
+{
+	size_t i;
+
+	if (*rem_time <= 0)
+		return;
+
+	for (i = 0; i < MMRC_MAX_CHAIN_LENGTH; i++) {
+		u32 attempt_time;
+		u32 attempt;
+
+		if (rate->rates[i].rate == MMRC_MCS_UNUSED)
+			break;
+
+		/*
+		 * The attempts for these rates were calculated in the initial
+		 * attempt allocation
+		 */
+		if (tb->table[rate->rates[i].index].prob < 20)
+			continue;
+
+		if (i == 0 && (calculate_throughput(tb, rate->rates[i].index) <
+			       calculate_throughput(tb, tb->best_prob.index)))
+			continue;
+
+		attempt_time = calculate_attempt_time(&rate->rates[i], size);
+		if (!attempt_time)
+			continue;
+
+		attempt = (*rem_time / tb->caps.max_rates) / attempt_time;
+		attempt += rate->rates[i].attempts;
+
+		rate->rates[i].attempts = MMRC_ATTEMPTS_TO_BITFIELD(
+			attempt > MMRC_MAX_CHAIN_ATTEMPTS ?
+				MMRC_MAX_CHAIN_ATTEMPTS :
+				attempt);
+	}
+}
+
+/* Allocate initial attempts to all rates in a rate table */
+static void allocate_initial_attempts(struct mmrc_rate_table *rate,
+				      s32 *rem_time, size_t size)
+{
+	u32 i;
+
+	for (i = 0; i < MMRC_MAX_CHAIN_LENGTH; i++) {
+		u32 attempt_time;
+
+		if (rate->rates[i].rate == MMRC_MCS_UNUSED)
+			break;
+
+		attempt_time = calculate_attempt_time(&rate->rates[i], size);
+
+		/*
+		 * if the time for a single attempt is very long, lets just
+		 * try once
+		 */
+		if (attempt_time > MAX_WINDOW_ATTEMPT_TIME) {
+			*rem_time -= attempt_time;
+			rate->rates[i].attempts = MMRC_ATTEMPTS_TO_BITFIELD(1);
+		} else {
+			*rem_time -= attempt_time * 2;
+			rate->rates[i].attempts = MMRC_ATTEMPTS_TO_BITFIELD(2);
+		}
+	}
+}
+
+void mmrc_get_rates(struct mmrc_table *tb, struct mmrc_rate_table *out,
+		    size_t size)
+{
+	u8 i;
+	u16 random_index;
+	struct mmrc_rate random;
+	struct mmrc_rate lookaround0 = tb->best_tp;
+	struct mmrc_rate lookaround1 = tb->second_tp;
+	bool is_lookaround;
+	int lookaround_index = -1;
+	int best_index = 0;
+	int random_tp = 0;
+	int best_tp;
+	int lookaround_fail_count;
+	bool try_current_lookaround = false;
+
+	s32 rem_time = RATE_WINDOW_MICROSECONDS;
+
+	memset(out, 0, sizeof(*out));
+
+	tb->lookaround_cnt = (tb->lookaround_cnt + 1) % tb->lookaround_wrap;
+	/*
+	 * Look around if the counter wraps or there has been no look around
+	 * for a number of rate control cycles.
+	 */
+	is_lookaround = (tb->fixed_rate.rate == MMRC_MCS_UNUSED) &&
+			((tb->lookaround_cnt == 0) ||
+			 ((tb->last_lookaround_cycle +
+			   LOOKAROUND_MAX_RC_CYCLES) <= tb->cycle_cnt));
+
+	/* Also skip sampling if we don't yet have data for our best rate */
+	if (tb->table[tb->best_tp.index].evidence == 0)
+		is_lookaround = false;
+
+	if (tb->lookaround_wrap != LOOKAROUND_RATE_STABLE) {
+		if (tb->stability_cnt >= tb->stability_cnt_threshold) {
+			tb->lookaround_wrap = LOOKAROUND_RATE_STABLE;
+			tb->stability_cnt_threshold =
+				STABILITY_CNT_THRESHOLD_STABLE;
+			tb->stability_cnt = STABILITY_CNT_THRESHOLD_STABLE * 2;
+			is_lookaround = false;
+		}
+	} else if (tb->stability_cnt < tb->stability_cnt_threshold) {
+		tb->stability_cnt_threshold = STABILITY_CNT_THRESHOLD_NORMAL;
+		tb->lookaround_wrap = LOOKAROUND_RATE_NORMAL;
+		tb->stability_cnt = 0;
+	}
+
+	/* Look around only when the fixed rate is not set */
+	if (is_lookaround) {
+		tb->total_lookaround++;
+		tb->forced_lookaround =
+			(tb->forced_lookaround + 1) % LOOKAROUND_RATE_NORMAL;
+		tb->last_lookaround_cycle = tb->cycle_cnt;
+
+		if (tb->current_lookaround_rate_attempts <
+		    LOOKAROUND_RATE_ATTEMPTS)
+			try_current_lookaround = true;
+
+		best_tp = calculate_throughput(tb, tb->best_tp.index);
+
+		for (lookaround_fail_count = 0;
+		     lookaround_fail_count < LOOKAROUND_FAIL_MAX;
+		     lookaround_fail_count++) {
+			if (try_current_lookaround) {
+				random_index =
+					tb->current_lookaround_rate_index;
+				try_current_lookaround = false;
+			} else {
+				random_index = get_random_u32_below(
+					rows_from_sta_caps(&tb->caps));
+			}
+			random = get_rate_row(tb, random_index);
+
+			if (!validate_rate(tb, &random))
+				continue;
+
+			if (random.rate == MMRC_MCS10)
+				continue;
+
+			if (tb->table[random_index].evidence > 0)
+				random_tp =
+					calculate_throughput(tb, random_index);
+			else
+				random_tp =
+					mmrc_calculate_theoretical_throughput(
+						random);
+
+			/*
+			 * Skip rates that can only be worse than the current
+			 * best
+			 */
+			if (random_tp <= best_tp)
+				continue;
+
+			/*
+			 * Force looking up the rate no more that one MCS.
+			 * It will avoid looking for rates with very low
+			 * success rate. In case of better environment
+			 * conditions MMRC will collect enough statistics to
+			 * climb up the rates one by one.
+			 */
+			if (random.rate > tb->best_tp.rate + 1 ||
+			    random.bw > tb->best_tp.bw + 1 ||
+			    (random.rate > tb->best_tp.rate &&
+			     random.bw > tb->best_tp.bw))
+				continue;
+
+			if (tb->current_lookaround_rate_index == random_index) {
+				tb->current_lookaround_rate_attempts++;
+			} else {
+				tb->current_lookaround_rate_attempts = 0;
+				tb->current_lookaround_rate_index =
+					random_index;
+			}
+
+			break;
+		}
+
+		if (lookaround_fail_count >= LOOKAROUND_FAIL_MAX) {
+			is_lookaround = false;
+			tb->current_lookaround_rate_index = tb->best_tp.index;
+		} else {
+			lookaround0 = random;
+			lookaround1 = tb->best_tp;
+			lookaround_index = 0;
+			best_index = 1;
+		}
+	}
+
+	if (tb->caps.max_rates == 1) {
+		out->rates[0] = (is_lookaround) ? lookaround0 : tb->best_tp;
+		out->rates[1].rate = MMRC_MCS_UNUSED;
+		out->rates[2].rate = MMRC_MCS_UNUSED;
+		out->rates[3].rate = MMRC_MCS_UNUSED;
+	} else if (tb->caps.max_rates == 2) {
+		out->rates[0] = (is_lookaround) ? lookaround0 : tb->best_tp;
+		out->rates[1] = (is_lookaround) ? lookaround1 : tb->best_prob;
+		out->rates[2].rate = MMRC_MCS_UNUSED;
+		out->rates[3].rate = MMRC_MCS_UNUSED;
+	} else if (tb->caps.max_rates == 3) {
+		out->rates[0] = (is_lookaround) ? lookaround0 : tb->best_tp;
+		out->rates[1] = (is_lookaround) ? lookaround1 : tb->second_tp;
+		out->rates[2] = tb->best_prob;
+		out->rates[3].rate = MMRC_MCS_UNUSED;
+	} else {
+		out->rates[0] = (is_lookaround) ? lookaround0 : tb->best_tp;
+		out->rates[1] = (is_lookaround) ? lookaround1 : tb->second_tp;
+		out->rates[2] = tb->best_prob;
+		out->rates[3] = tb->baseline;
+	}
+
+	/* For fallback rates, set RTS/CTS */
+	for (i = 1; i < MMRC_MAX_CHAIN_LENGTH; i++)
+		out->rates[i].flags |= BIT(MMRC_FLAGS_CTS_RTS);
+
+	/* Allocate initial attempts for rate */
+	allocate_initial_attempts(out, &rem_time, size);
+
+	/* Calculate and allocate remaining attempts */
+	calculate_remaining_attempts(tb, out, &rem_time, size);
+
+	/* Enforce limits on each attempts */
+	for (i = 0; i < MMRC_MAX_CHAIN_LENGTH; i++) {
+		if (out->rates[i].rate != MMRC_MCS_UNUSED) {
+			out->rates[i].attempts =
+				out->rates[i].attempts == 0 ?
+					MMRC_ATTEMPTS_TO_BITFIELD(
+						MMRC_MIN_CHAIN_ATTEMPTS) :
+					out->rates[i].attempts;
+			out->rates[i].attempts =
+				out->rates[i].attempts >
+						MMRC_MAX_CHAIN_ATTEMPTS ?
+					MMRC_ATTEMPTS_TO_BITFIELD(
+						MMRC_MAX_CHAIN_ATTEMPTS) :
+					out->rates[i].attempts;
+			if (i == lookaround_index &&
+			    tb->lookaround_wrap != LOOKAROUND_RATE_INIT)
+				out->rates[i].attempts =
+					MMRC_ATTEMPTS_TO_BITFIELD(1);
+		}
+	}
+
+	/*
+	 * Give the best rate at least 2 attempts to keep peak throughput
+	 * unless it is too low
+	 */
+	if (out->rates[best_index].attempts == 1 &&
+	    out->rates[best_index].rate > MMRC_MCS1)
+		out->rates[best_index].attempts = MMRC_ATTEMPTS_TO_BITFIELD(2);
+	else if (out->rates[best_index].rate <= MMRC_MCS1)
+		out->rates[best_index].attempts = 1;
+}
+
+static u32 calc_ewma_average(u32 avg, u32 latest, u32 weight)
+{
+	WARN_ON_ONCE(!(weight <= 100));
+
+	if (avg == 0)
+		return latest;
+
+	return ((latest * (100 - weight)) + (avg * weight)) / 100;
+}
+
+static void mmrc_process_variation(struct mmrc_table *tb, u16 current_success,
+				   u32 index)
+{
+	u32 current_variation;
+
+	/*
+	 * Only process probability variation for the best rate. It is likely
+	 * the only rate to have enough data to see the variation and its
+	 * statistics are more affected because they are usually collected over
+	 * the full period.
+	 */
+	if (index != tb->best_tp.index)
+		return;
+
+	if (current_success == 0) {
+		if (!tb->unconverged) {
+			/*
+			 * Best rate is failing completely, go to unconverged
+			 * mode
+			 */
+			tb->unconverged = true;
+			tb->newly_unconverged = true;
+		}
+		return;
+	}
+
+	if (tb->table[index].prob == 0)
+		return;
+
+	/* Don't process variation while converging after association */
+	if (tb->lookaround_wrap == LOOKAROUND_RATE_INIT)
+		return;
+
+	current_variation = abs(current_success - tb->table[index].prob);
+
+	/* Calculate the EWMA of the probability variation */
+	tb->probability_variation = calc_ewma_average(
+		tb->probability_variation, current_variation, VARIATION_EWMA);
+
+	/*
+	 * Process the variation direction to distinguish converged and
+	 * unconverged scenarios
+	 */
+	if (tb->probability_variation >= MODERATE_VARIATION_THRESHOLD ||
+	    tb->interference_likely) {
+		if ((current_success - tb->table[index].prob) *
+			    tb->probability_variation_direction <
+		    0)
+			tb->probability_variation_direction = 0;
+		else if (current_success > tb->table[index].prob)
+			tb->probability_variation_direction =
+				min(tb->probability_variation_direction + 1,
+				    MAX_VARIATION_DIRECTION);
+		else if (current_success < tb->table[index].prob)
+			tb->probability_variation_direction =
+				max(tb->probability_variation_direction - 1,
+				    -MAX_VARIATION_DIRECTION);
+	}
+
+	if (tb->best_rate_cycle_count > VARIATION_DIRECTION_THRESHOLD &&
+	    tb->probability_variation >= SIGNIFICANT_VARIATION_THRESHOLD) {
+		/*
+		 * Only enter interference mode if the best rate is stable for
+		 * enough cycles to determine the direction is random and not
+		 * in one direction only
+		 */
+		if (abs(tb->probability_variation_direction) <=
+			    VARIATION_DIRECTION_THRESHOLD &&
+		    !tb->interference_likely) {
+			tb->interference_likely = true;
+		}
+	} else if (tb->interference_likely &&
+		   (tb->probability_variation <= MINOR_VARIATION_THRESHOLD ||
+		    abs(tb->probability_variation_direction) ==
+			    MAX_VARIATION_DIRECTION)) {
+		/*
+		 * Exit interference mode if the variability drops or the
+		 * direction stops being random
+		 */
+		tb->interference_likely = false;
+	}
+}
+
+void mmrc_update(struct mmrc_table *tb)
+{
+	u32 i;
+	u16 this_success;
+	u32 scale;
+	u32 scaled_ewma;
+	u32 new_stats = 0;
+	u32 attempts_for_stats;
+	u32 success_for_stats;
+	u32 min_stats;
+	u32 throughput;
+	u32 evidence_sent;
+
+	tb->cycle_cnt++;
+
+	/* Allow less minimum stats when converging */
+	if (tb->lookaround_wrap != LOOKAROUND_RATE_INIT)
+		min_stats = STATS_MIN_NORMAL;
+	else
+		min_stats = STATS_MIN_INIT;
+
+	for (i = 0; i < rows_from_sta_caps(&tb->caps); i++) {
+		/* This algorithm is keeping track of the amount of evidence,
+		 * being packets that have been recently sent at this rate.
+		 * This value is smoothed with an EWMA function over time and
+		 * used to update the probability of a rate succeeding
+		 * dynamically. This method allows MMRC to react timely if a
+		 * new rate is used that hasn't been used recently
+		 */
+
+		/* Necessary to prevent a divide by 0 */
+		if (tb->table[i].evidence == 0)
+			scale = 0;
+		else
+			scale = ((tb->table[i].evidence * 2) * 100) /
+				((tb->table[i].sent * EVIDENCE_SCALE) +
+				 tb->table[i].evidence);
+
+		/* Restrict scale to appropriate values */
+		if (scale > 100)
+			scale = 100;
+
+		scaled_ewma = scale * EWMA / 100;
+
+		/*
+		 * Only count new packets for evidence if we will process
+		 * them
+		 */
+		evidence_sent =
+			tb->table[i].sent >= min_stats ? tb->table[i].sent : 0;
+		tb->table[i].evidence = calc_ewma_average(
+			tb->table[i].evidence, evidence_sent * EVIDENCE_SCALE,
+			scaled_ewma);
+
+		if (tb->table[i].evidence > EVIDENCE_MAX)
+			tb->table[i].evidence = EVIDENCE_MAX;
+
+		/* Try to use statistics from acknowledged AMPDUs first */
+		attempts_for_stats = tb->table[i].back_mpdu_success +
+				     tb->table[i].back_mpdu_failure;
+		success_for_stats = tb->table[i].back_mpdu_success;
+
+		/*
+		 * Use the full statistics if rates are not converged or there
+		 * were no AMPDUs for this rate or the remaining attempts are
+		 * less than half of what we have from AMPDUs.
+		 */
+		if (!tb->table[i].have_sent_ampdus || tb->unconverged ||
+		    attempts_for_stats < AMPDU_STATS_MIN ||
+		    (tb->table[i].sent - attempts_for_stats <
+		     attempts_for_stats / 2)) {
+			attempts_for_stats = tb->table[i].sent;
+			success_for_stats = tb->table[i].sent_success;
+		}
+
+		if (attempts_for_stats >= min_stats ||
+		    (attempts_for_stats > 0 && tb->table[i].prob > 0)) {
+			new_stats = 1;
+			this_success =
+				(100 * success_for_stats) / attempts_for_stats;
+
+			if (scaled_ewma)
+				mmrc_process_variation(tb, this_success, i);
+
+			tb->table[i].prob = calc_ewma_average(
+				tb->table[i].prob, this_success, scaled_ewma);
+
+			/* Clear our sent statistics and update totals */
+			tb->table[i].total_sent += tb->table[i].sent;
+			tb->table[i].sent = 0;
+
+			tb->table[i].total_success += tb->table[i].sent_success;
+			tb->table[i].sent_success = 0;
+
+			tb->table[i].back_mpdu_failure = 0;
+			tb->table[i].back_mpdu_success = 0;
+			tb->table[i].have_sent_ampdus = false;
+		}
+
+		throughput = calculate_throughput(tb, i);
+		if (tb->table[i].max_throughput < throughput)
+			tb->table[i].max_throughput = throughput;
+
+		/*
+		 * Reset the running average windows if reached collector
+		 * limits
+		 */
+		if (tb->table[i].sum_throughput > (0xFFFFFFFF - throughput)) {
+			tb->table[i].sum_throughput /=
+				tb->table[i].avg_throughput_counter;
+			tb->table[i].avg_throughput_counter = 1;
+		}
+		/* Update the sum and counter so it will be possible later to
+		 * calculate the running average throughput
+		 */
+		tb->table[i].sum_throughput += throughput;
+		tb->table[i].avg_throughput_counter++;
+	}
+
+	generate_table_priority(tb, new_stats);
+
+	/*
+	 * Switch to faster lookaround mode if rates drop low at very low
+	 * bandwidth or we are in unconverged mode. Switching at low bandwidth
+	 * and rate is to help recover quickly from rates where we would need
+	 * to fragment standard MTU size packets.
+	 */
+	if (tb->lookaround_wrap != LOOKAROUND_RATE_INIT &&
+	    (tb->unconverged || (tb->best_tp.bw == MMRC_BW_1MHZ &&
+				 tb->best_tp.rate <= MMRC_MCS2))) {
+		tb->lookaround_cnt = 0;
+		tb->lookaround_wrap = LOOKAROUND_RATE_INIT;
+		tb->stability_cnt_threshold = STABILITY_CNT_THRESHOLD_INIT;
+	}
+
+	/*
+	 * If it is unlikely we can do the lookaround attempts in two RC cycles
+	 * choose a new rate
+	 */
+	if (tb->current_lookaround_rate_attempts <=
+	    (LOOKAROUND_RATE_ATTEMPTS / 2))
+		tb->current_lookaround_rate_attempts = LOOKAROUND_RATE_ATTEMPTS;
+}
+
+void mmrc_feedback(struct mmrc_table *tb, struct mmrc_rate_table *rates,
+		   s32 retry_count, bool was_aggregated)
+{
+	s32 ind = retry_count;
+	u32 i;
+
+	for (i = 0; i < MMRC_MAX_CHAIN_LENGTH; i++) {
+		rate_update_index(tb, &rates->rates[i]);
+		tb->table[rates->rates[i].index].have_sent_ampdus |=
+			was_aggregated;
+
+		if ((s32)rates->rates[i].attempts < ind) {
+			ind = ind - rates->rates[i].attempts;
+			tb->table[rates->rates[i].index].sent +=
+				rates->rates[i].attempts;
+			if (was_aggregated) {
+				tb->table[rates->rates[i].index]
+					.back_mpdu_failure +=
+					rates->rates[i].attempts;
+			}
+		} else {
+			tb->table[rates->rates[i].index].sent += ind;
+			tb->table[rates->rates[i].index].sent_success += 1;
+			if (was_aggregated) {
+				tb->table[rates->rates[i].index]
+					.back_mpdu_success += 1;
+				tb->table[rates->rates[i].index]
+					.back_mpdu_failure +=
+					ind > 1 ? ind - 1 : 0;
+			}
+			return;
+		}
+	}
+}
+
+/*
+ * Chooses a reasonable starting rate based on range (gathered from
+ * RSSI measurements) or bandwidth. Then fills out the 3 retry rates
+ * so a full set of rates is available.
+ */
+static void mmrc_init_rates(struct mmrc_table *tb, s8 rssi)
+{
+	tb->best_tp.bw = MMRC_MAX_BW(tb->caps.bandwidth);
+	if (tb->caps.sgi_per_bw & SGI_PER_BW(tb->best_tp.bw))
+		tb->best_tp.guard = MMRC_GUARD_TO_BITFIELD(MMRC_GUARD_SHORT);
+	else
+		tb->best_tp.guard = MMRC_GUARD_TO_BITFIELD(MMRC_GUARD_LONG);
+	tb->best_tp.rate = MMRC_RATE_TO_BITFIELD(MMRC_MCS0);
+
+	if (rssi >= MMRC_SHORT_RANGE_RSSI_LIMIT)
+		tb->best_tp.rate = MMRC_RATE_TO_BITFIELD(MMRC_MCS7);
+	else if (rssi < MMRC_SHORT_RANGE_RSSI_LIMIT &&
+		 rssi >= MMRC_MID_RANGE_RSSI_LIMIT)
+		tb->best_tp.rate = MMRC_RATE_TO_BITFIELD(MMRC_MCS3);
+	else if (tb->best_tp.bw == MMRC_BW_1MHZ ||
+		 tb->best_tp.bw == MMRC_BW_2MHZ)
+		/*
+		 * To compensate for slow feedback when running with 1 and 2
+		 * MHz bandwidth, we start from MCS3 which will correspond to
+		 * reasonable feedback and will avoid resetting the rate table
+		 * evidence.
+		 */
+		tb->best_tp.rate = MMRC_RATE_TO_BITFIELD(MMRC_MCS3);
+
+	tb->best_tp.ss = MMRC_SS_TO_BITFIELD(MMRC_SPATIAL_STREAM_1);
+	rate_update_index(tb, &tb->best_tp);
+	/* Init every rate in case they are needed to set the retry rates */
+	tb->second_tp = tb->best_tp;
+	tb->best_prob = tb->best_tp;
+	tb->baseline = tb->best_tp;
+	mmrc_fill_retry_rates(tb);
+}
+
+void mmrc_sta_init(struct mmrc_table *tb, struct mmrc_sta_capabilities *caps,
+		   s8 rssi)
+{
+	u32 i;
+	u16 row_count = rows_from_sta_caps(caps);
+
+	memset(tb, 0, mmrc_memory_required_for_caps(caps));
+	memcpy(&tb->caps, caps, sizeof(tb->caps));
+
+	for (i = 0; i < row_count; i++) {
+		tb->table[i].prob = RATE_INIT_PROBABILITY;
+		tb->table[i].evidence = 0;
+		tb->table[i].sum_throughput = 0;
+		tb->table[i].avg_throughput_counter = 0;
+		tb->table[i].max_throughput = 0;
+	}
+
+	tb->fixed_rate.rate = MMRC_MCS_UNUSED;
+	tb->cycle_cnt = 0;
+	tb->last_lookaround_cycle = 0;
+	tb->lookaround_cnt = 0;
+	tb->lookaround_wrap = LOOKAROUND_RATE_INIT;
+	tb->unconverged = true;
+	tb->newly_unconverged = true;
+	tb->stability_cnt_threshold = STABILITY_CNT_THRESHOLD_INIT;
+	tb->baseline = get_rate_row(tb, find_baseline_index(tb));
+	mmrc_init_rates(tb, rssi);
+}
+
+bool mmrc_set_fixed_rate(struct mmrc_table *tb, struct mmrc_rate fixed_rate)
+{
+	bool caps_support_rate = true;
+
+	/* Do not accept rate which does not support the STA capabilities */
+	if ((BIT(fixed_rate.rate) & tb->caps.rates) == 0 ||
+	    (BIT(fixed_rate.bw) & tb->caps.bandwidth) == 0 ||
+	    (BIT(fixed_rate.ss) & tb->caps.spatial_streams) == 0 ||
+	    (BIT(fixed_rate.guard) & tb->caps.guard) == 0)
+		caps_support_rate = false;
+
+	if (validate_rate(tb, &fixed_rate) && caps_support_rate) {
+		tb->fixed_rate = fixed_rate;
+		rate_update_index(tb, &tb->fixed_rate);
+		return true;
+	}
+
+	return false;
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 17/33] wifi: mm81x: add mmrc.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/mmrc.h | 193 +++++++++++++++++++
 1 file changed, 193 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/mmrc.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/mmrc.h b/drivers/net/wireless/morsemicro/mm81x/mmrc.h
new file mode 100644
index 000000000000..a4c7d941ad55
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/mmrc.h
@@ -0,0 +1,193 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_MMRC_H_
+#define _MM81X_MMRC_H_
+
+#include <linux/version.h>
+#include <linux/types.h>
+#include <linux/slab.h>
+#include <linux/bitops.h>
+#include <linux/random.h>
+#include <linux/time.h>
+
+/* The max length of a retry chain for a single packet transmission */
+#define MMRC_MAX_CHAIN_LENGTH 4
+
+/* Rate minimum allowed attempts */
+#define MMRC_MIN_CHAIN_ATTEMPTS 1
+
+/* Rate upper limit for attempts */
+#define MMRC_MAX_CHAIN_ATTEMPTS 2
+
+/* The frequency of MMRC stat table updates */
+#define MMRC_UPDATE_FREQUENCY_MS 100
+
+enum mmrc_flags {
+	MMRC_FLAGS_CTS_RTS,
+};
+
+enum mmrc_mcs_rate {
+	MMRC_MCS0,
+	MMRC_MCS1,
+	MMRC_MCS2,
+	MMRC_MCS3,
+	MMRC_MCS4,
+	MMRC_MCS5,
+	MMRC_MCS6,
+	MMRC_MCS7,
+	MMRC_MCS8,
+	MMRC_MCS9,
+	MMRC_MCS10,
+	MMRC_MCS_UNUSED,
+};
+
+enum mmrc_bw {
+	MMRC_BW_1MHZ = 0,
+	MMRC_BW_2MHZ = 1,
+	MMRC_BW_4MHZ = 2,
+	MMRC_BW_8MHZ = 3,
+	MMRC_BW_16MHZ = 4,
+	MMRC_BW_MAX = 5,
+};
+
+enum mmrc_spatial_stream {
+	MMRC_SPATIAL_STREAM_1 = 0,
+	MMRC_SPATIAL_STREAM_2 = 1,
+	MMRC_SPATIAL_STREAM_3 = 2,
+	MMRC_SPATIAL_STREAM_4 = 3,
+	MMRC_SPATIAL_STREAM_MAX,
+};
+
+enum mmrc_guard {
+	MMRC_GUARD_LONG = 0,
+	MMRC_GUARD_SHORT = 1,
+	MMRC_GUARD_MAX,
+};
+
+#define MMRC_RATE_TO_BITFIELD(x) ((x) & 0xF)
+#define MMRC_ATTEMPTS_TO_BITFIELD(x) ((x) & 0x7)
+#define MMRC_GUARD_TO_BITFIELD(x) ((x) & 0x1)
+#define MMRC_SS_TO_BITFIELD(x) ((x) & 0x3)
+#define MMRC_BW_TO_BITFIELD(x) ((x) & 0x7)
+#define MMRC_FLAGS_TO_BITFIELD(x) ((x) & 0x7)
+
+struct mmrc_rate {
+	u8 rate : 4;
+	u8 attempts : 3;
+	u8 guard : 1;
+	u8 ss : 2;
+	u8 bw : 3;
+	u8 flags : 3;
+	u16 index;
+};
+
+struct mmrc_rate_table {
+	struct mmrc_rate rates[MMRC_MAX_CHAIN_LENGTH];
+};
+
+#define SGI_PER_BW(bw) (1 << (bw))
+
+struct mmrc_sta_capabilities {
+	u8 max_rates : 3;
+	u8 max_retries : 3;
+	u8 bandwidth : 5;
+	u8 spatial_streams : 4;
+	u16 rates : 11;
+	u8 guard : 2;
+	u8 sta_flags : 4;
+	u8 sgi_per_bw : 5;
+};
+
+struct mmrc_stats_table {
+	u32 avg_throughput_counter;
+	u32 sum_throughput;
+	u32 max_throughput;
+	u16 sent;
+	u16 sent_success;
+	u16 back_mpdu_success;
+	u16 back_mpdu_failure;
+	u32 total_sent;
+	u32 total_success;
+	u16 evidence;
+	u8 prob;
+	bool have_sent_ampdus;
+};
+
+struct mmrc_table {
+	struct mmrc_sta_capabilities caps;
+	struct mmrc_rate best_tp;
+	struct mmrc_rate second_tp;
+	struct mmrc_rate baseline;
+	struct mmrc_rate best_prob;
+	struct mmrc_rate fixed_rate;
+	u32 cycle_cnt;
+	u32 last_lookaround_cycle;
+	u8 lookaround_cnt;
+
+	/* The ratio of using normal rate and sampling */
+	u8 lookaround_wrap;
+
+	/*
+	 * A counter that is used to determine when we should force a
+	 * lookaround. Should be a portion of the above lookaround with
+	 * less constraints
+	 */
+	u8 forced_lookaround;
+
+	u8 current_lookaround_rate_attempts;
+	u16 current_lookaround_rate_index;
+	u32 total_lookaround;
+
+	/*
+	 * A counter to detect if the current best rate is optimal
+	 * and may slow down sample frequency.
+	 */
+	u32 stability_cnt;
+
+	u32 stability_cnt_threshold;
+	u8 probability_variation;
+
+	/* The difference in MCS from each of the last 2 rate changes */
+	s8 best_rate_diff[2];
+
+	/* Indication of random versus consistently one-sided variation */
+	s8 probability_variation_direction;
+
+	/* Has rate control detected possible interference */
+	bool interference_likely;
+
+	/* Has rate control detected the best rate is no longer converged */
+	bool unconverged;
+
+	/* Is rate control just entering unconverged state */
+	bool newly_unconverged;
+
+	/*
+	 * Number of rate control cycles the best rate has remained
+	 * unchanged
+	 */
+	s32 best_rate_cycle_count;
+
+	/*
+	 * The probability table for the STA. This MUST always be the last
+	 * element in the struct.
+	 */
+	struct mmrc_stats_table table[];
+};
+
+void mmrc_sta_init(struct mmrc_table *tb, struct mmrc_sta_capabilities *caps,
+		   s8 rssi);
+size_t mmrc_memory_required_for_caps(struct mmrc_sta_capabilities *caps);
+void mmrc_get_rates(struct mmrc_table *tb, struct mmrc_rate_table *out,
+		    size_t size);
+void mmrc_feedback(struct mmrc_table *tb, struct mmrc_rate_table *rates,
+		   s32 retry_count, bool was_aggregated);
+void mmrc_update(struct mmrc_table *tb);
+bool mmrc_set_fixed_rate(struct mmrc_table *tb, struct mmrc_rate fixed_rate);
+u32 mmrc_calculate_theoretical_throughput(struct mmrc_rate rate);
+u32 mmrc_calculate_rate_tx_time(struct mmrc_rate *rate, size_t size);
+
+#endif /* _MMRC_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 18/33] wifi: mm81x: add ps.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/ps.c | 120 +++++++++++++++++++++
 1 file changed, 120 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/ps.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/ps.c b/drivers/net/wireless/morsemicro/mm81x/ps.c
new file mode 100644
index 000000000000..ab67823452ee
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/ps.c
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/types.h>
+#include <linux/mutex.h>
+#include <linux/workqueue.h>
+#include "hif.h"
+#include "skbq.h"
+#include "mac.h"
+#include "bus.h"
+#include "ps.h"
+
+static void mm81x_ps_wakeup(struct mm81x_ps *mps)
+{
+	struct mm81x *mors = container_of(mps, struct mm81x, ps);
+
+	if (!mps->enable || !mps->suspended)
+		return;
+
+	mm81x_set_bus_enable(mors, true);
+	mps->suspended = false;
+}
+
+static void mm81x_ps_sleep(struct mm81x_ps *mps)
+{
+	struct mm81x *mors = container_of(mps, struct mm81x, ps);
+
+	if (!mps->enable || mps->suspended)
+		return;
+
+	mps->suspended = true;
+	mm81x_set_bus_enable(mors, false);
+}
+
+static void mm81x_ps_evaluate(struct mm81x_ps *mps)
+{
+	struct mm81x *mors = container_of(mps, struct mm81x, ps);
+	bool needs_wake = false;
+	unsigned long flags_on_entry =
+		(mors->hif.event_flags &
+		 ~BIT(MM81X_HIF_EVT_DATA_TRAFFIC_PAUSE_PEND));
+
+	if (!mps->enable)
+		return;
+
+	needs_wake = (mps->wakers > 0);
+	needs_wake |= (flags_on_entry > 0);
+	needs_wake |= (mm81x_hif_get_tx_buffered_count(mors) > 0);
+
+	if (needs_wake) {
+		mm81x_ps_wakeup(mps);
+		return;
+	}
+
+	mm81x_ps_sleep(mps);
+}
+
+static void mm81x_ps_evaluate_work(struct work_struct *work)
+{
+	struct mm81x_ps *mps =
+		container_of(work, struct mm81x_ps, delayed_eval_work.work);
+
+	if (mps->enable) {
+		mutex_lock(&mps->lock);
+		mm81x_ps_evaluate(mps);
+		mutex_unlock(&mps->lock);
+	}
+}
+
+void mm81x_ps_enable(struct mm81x *mors)
+{
+	struct mm81x_ps *mps = &mors->ps;
+
+	if (mps->enable) {
+		mutex_lock(&mps->lock);
+		if (mps->wakers == 0) {
+			WARN_ON_ONCE(1);
+		} else {
+			mps->wakers--;
+			mm81x_ps_evaluate(mps);
+		}
+		mutex_unlock(&mps->lock);
+	}
+}
+
+void mm81x_ps_disable(struct mm81x *mors)
+{
+	struct mm81x_ps *mps = &mors->ps;
+
+	if (mps->enable) {
+		mutex_lock(&mps->lock);
+		mps->wakers++;
+		mm81x_ps_evaluate(mps);
+		mutex_unlock(&mps->lock);
+	}
+}
+
+int mm81x_ps_init(struct mm81x *mors)
+{
+	struct mm81x_ps *mps = &mors->ps;
+
+	mps->enable = (mors->bus_type == MM81X_BUS_TYPE_USB);
+	mps->suspended = true;
+	mps->wakers = 1; /* we default to being on */
+	mutex_init(&mps->lock);
+	INIT_DELAYED_WORK(&mps->delayed_eval_work, mm81x_ps_evaluate_work);
+
+	return 0;
+}
+
+void mm81x_ps_finish(struct mm81x *mors)
+{
+	struct mm81x_ps *mps = &mors->ps;
+
+	if (mps->enable) {
+		mps->enable = false;
+		cancel_delayed_work_sync(&mps->delayed_eval_work);
+	}
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 19/33] wifi: mm81x: add ps.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/ps.h | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/ps.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/ps.h b/drivers/net/wireless/morsemicro/mm81x/ps.h
new file mode 100644
index 000000000000..0b59bb4145ab
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/ps.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_PS_H_
+#define _MM81X_PS_H_
+
+#include "core.h"
+
+/* This should be nominally <= the dynamic ps timeout */
+#define NETWORK_BUS_TIMEOUT_MS (90)
+
+/* The default period of time to wait to re-evaluate powersave */
+#define DEFAULT_BUS_TIMEOUT_MS (50)
+
+void mm81x_ps_disable(struct mm81x *mors);
+void mm81x_ps_enable(struct mm81x *mors);
+int mm81x_ps_init(struct mm81x *mors);
+void mm81x_ps_finish(struct mm81x *mors);
+
+#endif /* !_MM81X_PS_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 20/33] wifi: mm81x: add rate_code.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 .../net/wireless/morsemicro/mm81x/rate_code.h | 177 ++++++++++++++++++
 1 file changed, 177 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/rate_code.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/rate_code.h b/drivers/net/wireless/morsemicro/mm81x/rate_code.h
new file mode 100644
index 000000000000..c60fcb9447c4
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/rate_code.h
@@ -0,0 +1,177 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_RATE_CODE_H_
+#define _MM81X_RATE_CODE_H_
+
+#include <linux/types.h>
+
+enum dot11_bandwidth {
+	DOT11_BANDWIDTH_1MHZ = 0,
+	DOT11_BANDWIDTH_2MHZ = 1,
+	DOT11_BANDWIDTH_4MHZ = 2,
+	DOT11_BANDWIDTH_8MHZ = 3,
+	DOT11_BANDWIDTH_16MHZ = 4,
+
+	DOT11_MAX_BANDWIDTH = DOT11_BANDWIDTH_16MHZ,
+	DOT11_INVALID_BANDWIDTH = 5
+};
+
+enum mm81x_rate_preamble {
+	/* S1G LONG format (with SIG-A and SIG-B) */
+	MM81X_RATE_PREAMBLE_S1G_LONG = 0,
+	/* This is the most common format used */
+	MM81X_RATE_PREAMBLE_S1G_SHORT = 1,
+	/* S1G 1M format */
+	MM81X_RATE_PREAMBLE_S1G_1M = 2,
+
+	MM81X_RATE_MAX_PREAMBLE = MM81X_RATE_PREAMBLE_S1G_1M,
+	MM81X_RATE_INVALID_PREAMBLE = 7
+};
+
+typedef __le32 mm81x_rate_code_t;
+
+#define MM81X_RATECODE_PREAMBLE (0x0000000F)
+#define MM81X_RATECODE_MCS_INDEX (0x000000F0)
+#define MM81X_RATECODE_NSS_INDEX (0x00000700)
+#define MM81X_RATECODE_BW_INDEX (0x00003800)
+#define MM81X_RATECODE_RTS_FLAG (0x00010000)
+#define MM81X_RATECODE_SHORT_GI_FLAG (0x00040000)
+#define MM81X_RATECODE_DUP_BW_INDEX (0x01C00000)
+
+static inline enum mm81x_rate_preamble
+mm81x_ratecode_preamble_get(mm81x_rate_code_t rc)
+{
+	return (enum mm81x_rate_preamble)(
+		le32_get_bits(rc, MM81X_RATECODE_PREAMBLE));
+}
+
+static inline u8 mm81x_ratecode_mcs_index_get(mm81x_rate_code_t rc)
+{
+	return le32_get_bits(rc, MM81X_RATECODE_MCS_INDEX);
+}
+
+static inline u8 mm81x_ratecode_nss_index_get(mm81x_rate_code_t rc)
+{
+	return le32_get_bits(rc, MM81X_RATECODE_NSS_INDEX);
+}
+
+static inline enum dot11_bandwidth
+mm81x_ratecode_bw_index_get(mm81x_rate_code_t rc)
+{
+	return (enum dot11_bandwidth)(
+		le32_get_bits(rc, MM81X_RATECODE_BW_INDEX));
+}
+
+static inline bool mm81x_ratecode_rts_get(mm81x_rate_code_t rc)
+{
+	return le32_get_bits(rc, MM81X_RATECODE_RTS_FLAG);
+}
+
+static inline bool mm81x_ratecode_sgi_get(mm81x_rate_code_t rc)
+{
+	return le32_get_bits(rc, MM81X_RATECODE_SHORT_GI_FLAG);
+}
+
+static inline enum dot11_bandwidth
+mm81x_ratecode_dup_bw_index_get(mm81x_rate_code_t rc)
+{
+	return (enum dot11_bandwidth)(
+		le32_get_bits(rc, MM81X_RATECODE_DUP_BW_INDEX));
+}
+
+#define MM81X_RATECODE_INIT(bw_idx, nss_idx, mcs_idx, preamble)  \
+	(le32_encode_bits((bw_idx), MM81X_RATECODE_BW_INDEX) |   \
+	 le32_encode_bits((nss_idx), MM81X_RATECODE_NSS_INDEX) | \
+	 le32_encode_bits((mcs_idx), MM81X_RATECODE_MCS_INDEX) | \
+	 le32_encode_bits((preamble), MM81X_RATECODE_PREAMBLE))
+
+static inline mm81x_rate_code_t
+mm81x_ratecode_init(enum dot11_bandwidth bw_index, u32 nss_index, u32 mcs_index,
+		    enum mm81x_rate_preamble preamble)
+{
+	return MM81X_RATECODE_INIT(bw_index, nss_index, mcs_index, preamble);
+}
+
+static inline void
+mm81x_ratecode_preamble_set(mm81x_rate_code_t *rc,
+			    enum mm81x_rate_preamble preamble)
+{
+	*rc = (*rc & cpu_to_le32(~MM81X_RATECODE_PREAMBLE)) |
+	      le32_encode_bits(preamble, MM81X_RATECODE_PREAMBLE);
+}
+
+static inline void mm81x_ratecode_mcs_index_set(mm81x_rate_code_t *rc,
+						u32 mcs_index)
+{
+	*rc = (*rc & cpu_to_le32(~MM81X_RATECODE_MCS_INDEX)) |
+	      le32_encode_bits(mcs_index, MM81X_RATECODE_MCS_INDEX);
+}
+
+static inline void mm81x_ratecode_nss_index_set(mm81x_rate_code_t *rc,
+						u32 nss_index)
+{
+	*rc = (*rc & cpu_to_le32(~MM81X_RATECODE_NSS_INDEX)) |
+	      le32_encode_bits(nss_index, MM81X_RATECODE_NSS_INDEX);
+}
+
+static inline void mm81x_ratecode_bw_index_set(mm81x_rate_code_t *rc,
+					       enum dot11_bandwidth bw_index)
+{
+	*rc = (*rc & cpu_to_le32(~MM81X_RATECODE_BW_INDEX)) |
+	      le32_encode_bits(bw_index, MM81X_RATECODE_BW_INDEX);
+}
+
+static inline void
+mm81x_ratecode_update_s1g_bw_preamble(mm81x_rate_code_t *rc,
+				      enum dot11_bandwidth bw_index)
+{
+	enum mm81x_rate_preamble pream = MM81X_RATE_PREAMBLE_S1G_SHORT;
+
+	if (bw_index == DOT11_BANDWIDTH_1MHZ)
+		pream = MM81X_RATE_PREAMBLE_S1G_1M;
+
+	mm81x_ratecode_preamble_set(rc, pream);
+	mm81x_ratecode_bw_index_set(rc, bw_index);
+}
+
+static inline void
+mm81x_ratecode_dup_bw_index_set(mm81x_rate_code_t *rc,
+				enum dot11_bandwidth dup_bw_index)
+{
+	*rc = (*rc & cpu_to_le32(~MM81X_RATECODE_DUP_BW_INDEX)) |
+	      le32_encode_bits(dup_bw_index, MM81X_RATECODE_DUP_BW_INDEX);
+}
+
+static inline void mm81x_ratecode_enable_rts(mm81x_rate_code_t *rc)
+{
+	*rc |= cpu_to_le32(MM81X_RATECODE_RTS_FLAG);
+}
+
+static inline void mm81x_ratecode_enable_sgi(mm81x_rate_code_t *rc)
+{
+	*rc |= cpu_to_le32(MM81X_RATECODE_SHORT_GI_FLAG);
+}
+
+static inline enum dot11_bandwidth mm81x_ratecode_bw_mhz_to_bw_index(u8 bw_mhz)
+{
+	return ((bw_mhz == 1) ? DOT11_BANDWIDTH_1MHZ :
+		(bw_mhz == 2) ? DOT11_BANDWIDTH_2MHZ :
+		(bw_mhz == 4) ? DOT11_BANDWIDTH_4MHZ :
+		(bw_mhz == 8) ? DOT11_BANDWIDTH_8MHZ :
+				DOT11_BANDWIDTH_2MHZ);
+}
+
+static inline u8
+mm81x_ratecode_bw_index_to_s1g_bw_mhz(enum dot11_bandwidth bw_idx)
+{
+	return ((bw_idx == DOT11_BANDWIDTH_1MHZ) ? 1 :
+		(bw_idx == DOT11_BANDWIDTH_2MHZ) ? 2 :
+		(bw_idx == DOT11_BANDWIDTH_4MHZ) ? 4 :
+		(bw_idx == DOT11_BANDWIDTH_8MHZ) ? 8 :
+						   2);
+}
+
+#endif
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 21/33] wifi: mm81x: add rc.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/rc.c | 494 +++++++++++++++++++++
 1 file changed, 494 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/rc.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/rc.c b/drivers/net/wireless/morsemicro/mm81x/rc.c
new file mode 100644
index 000000000000..04aff66de4bd
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/rc.c
@@ -0,0 +1,494 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/slab.h>
+#include <linux/timer.h>
+#include "core.h"
+#include "mac.h"
+#include "bus.h"
+#include "rc.h"
+
+#define MM81X_RC_BW_TO_MMRC_BW(X)    \
+	(((X) == 1) ? MMRC_BW_1MHZ : \
+	 ((X) == 2) ? MMRC_BW_2MHZ : \
+	 ((X) == 4) ? MMRC_BW_4MHZ : \
+	 ((X) == 8) ? MMRC_BW_8MHZ : \
+		      MMRC_BW_2MHZ)
+
+static void mm81x_rc_work(struct work_struct *work)
+{
+	struct mm81x_rc *mrc = container_of(work, struct mm81x_rc, work);
+	struct list_head *pos;
+
+	spin_lock_bh(&mrc->lock);
+
+	list_for_each(pos, &mrc->stas) {
+		struct mm81x_rc_sta *mrc_sta =
+			container_of(pos, struct mm81x_rc_sta, list);
+		unsigned long now = jiffies;
+
+		mrc_sta->last_update = now;
+
+		mmrc_update(mrc_sta->tb);
+	}
+
+	spin_unlock_bh(&mrc->lock);
+
+	mod_timer(&mrc->timer, jiffies + msecs_to_jiffies(100));
+}
+
+static void mm81x_rc_timer(struct timer_list *t)
+{
+	struct mm81x_rc *mrc = timer_container_of(mrc, t, timer);
+	struct mm81x *mors = mrc->mors;
+
+	queue_work(mors->net_wq, &mors->mrc.work);
+}
+
+void mm81x_rc_init(struct mm81x *mors)
+{
+	INIT_LIST_HEAD(&mors->mrc.stas);
+	spin_lock_init(&mors->mrc.lock);
+
+	INIT_WORK(&mors->mrc.work, mm81x_rc_work);
+	timer_setup(&mors->mrc.timer, mm81x_rc_timer, 0);
+
+	mors->mrc.mors = mors;
+	mod_timer(&mors->mrc.timer, jiffies + msecs_to_jiffies(100));
+}
+
+void mm81x_rc_deinit(struct mm81x *mors)
+{
+	cancel_work_sync(&mors->mrc.work);
+	timer_delete_sync_try(&mors->mrc.timer);
+}
+
+static void mm81x_rc_sta_config_guard_per_bw(struct ieee80211_sta *sta,
+					     struct mmrc_sta_capabilities *caps)
+{
+	caps->guard = BIT(MMRC_GUARD_LONG);
+
+	if (caps->bandwidth & BIT(MMRC_BW_1MHZ)) {
+		caps->sgi_per_bw |= SGI_PER_BW(MMRC_BW_1MHZ);
+		caps->guard |= BIT(MMRC_GUARD_SHORT);
+	}
+
+	if (caps->bandwidth & BIT(MMRC_BW_2MHZ)) {
+		caps->sgi_per_bw |= SGI_PER_BW(MMRC_BW_2MHZ);
+		caps->guard |= BIT(MMRC_GUARD_SHORT);
+	}
+
+	if (caps->bandwidth & BIT(MMRC_BW_4MHZ)) {
+		caps->sgi_per_bw |= SGI_PER_BW(MMRC_BW_4MHZ);
+		caps->guard |= BIT(MMRC_GUARD_SHORT);
+	}
+
+	if (caps->bandwidth & BIT(MMRC_BW_8MHZ)) {
+		caps->sgi_per_bw |= SGI_PER_BW(MMRC_BW_8MHZ);
+		caps->guard |= BIT(MMRC_GUARD_SHORT);
+	}
+}
+
+static void mm81x_rc_sta_add_s1g_sta_caps(struct mm81x *mors,
+					  struct mmrc_sta_capabilities *caps,
+					  struct ieee80211_sta_s1g_cap *s1g_cap)
+{
+	int nss_idx = 0;
+	u8 rx_mcs = s1g_cap->nss_mcs[0] & 0x3; /* 1SS */
+	u8 tx_mcs = (s1g_cap->nss_mcs[2] >> 1) & 0x3; /* 1SS */
+	u8 mcs = min(rx_mcs, tx_mcs);
+
+	switch (mcs) {
+	case IEEE80211_VHT_MCS_SUPPORT_0_9: /* VHT 9 ->  S1G 9 */
+		caps->rates |= BIT(MMRC_MCS9) | BIT(MMRC_MCS8);
+		fallthrough;
+	case IEEE80211_VHT_MCS_SUPPORT_0_8: /* VHT 8 -> S1G 7 */
+		caps->rates |= BIT(MMRC_MCS7) | BIT(MMRC_MCS6) |
+			       BIT(MMRC_MCS5) | BIT(MMRC_MCS4) | BIT(MMRC_MCS3);
+		fallthrough;
+	case IEEE80211_VHT_MCS_SUPPORT_0_7: /* VHT 7 -> S1G 2 */
+		caps->rates |= BIT(MMRC_MCS2) | BIT(MMRC_MCS1) |
+			       BIT(MMRC_MCS0) | BIT(MMRC_MCS10);
+		caps->spatial_streams |= (BIT(nss_idx) & 0x0F);
+		break;
+
+	default:
+		dev_warn(mors->dev, "Invalid MCS encoding 0x%02x for stream %d",
+			 mcs, nss_idx);
+	}
+}
+
+int mm81x_rc_sta_add(struct mm81x *mors, struct ieee80211_vif *vif,
+		     struct ieee80211_sta *sta)
+{
+	struct ieee80211_sta_s1g_cap *s1g_cap = &sta->deflink.s1g_cap;
+	struct mm81x_sta *msta = (struct mm81x_sta *)sta->drv_priv;
+	struct mmrc_sta_capabilities caps;
+	int oper_bw_mhz = cfg80211_chandef_get_width(&mors->chandef);
+	size_t table_mem_size;
+	struct mmrc_table *tb;
+
+	memset(&caps, 0, sizeof(caps));
+
+	mm81x_rc_sta_add_s1g_sta_caps(mors, &caps, s1g_cap);
+
+	/* Configure STA for support up to 8MHZ */
+	while (oper_bw_mhz > 0) {
+		caps.bandwidth |= BIT(MM81X_RC_BW_TO_MMRC_BW(oper_bw_mhz));
+		oper_bw_mhz >>= 1;
+	}
+
+	/* Configure STA for short and long guard */
+	mm81x_rc_sta_config_guard_per_bw(sta, &caps);
+
+	/* Set max rates */
+	if (mors->hw->max_rates > 0 &&
+	    mors->hw->max_rates < IEEE80211_TX_MAX_RATES)
+		caps.max_rates = mors->hw->max_rates;
+	else
+		caps.max_rates = IEEE80211_TX_MAX_RATES;
+
+	/* Set max reties */
+	if (mors->hw->max_rate_tries >= MMRC_MIN_CHAIN_ATTEMPTS &&
+	    mors->hw->max_rate_tries < MMRC_MAX_CHAIN_ATTEMPTS)
+		caps.max_retries = mors->hw->max_rate_tries;
+	else
+		caps.max_retries = MMRC_MAX_CHAIN_ATTEMPTS;
+
+	WARN_ON(msta->rc.tb);
+	table_mem_size = mmrc_memory_required_for_caps(&caps);
+	tb = kzalloc(table_mem_size, GFP_KERNEL);
+	if (!tb)
+		return -ENOMEM;
+
+	/* Initialise the STA rate control table */
+	mmrc_sta_init(tb, &caps, msta->avg_rssi);
+
+	spin_lock_bh(&mors->mrc.lock);
+	kfree(msta->rc.tb);
+	msta->rc.tb = tb;
+	list_add(&msta->rc.list, &mors->mrc.stas);
+	msta->rc.last_update = jiffies;
+	spin_unlock_bh(&mors->mrc.lock);
+
+	return 0;
+}
+
+void mm81x_rc_sta_remove(struct mm81x *mors, struct ieee80211_sta *sta)
+{
+	struct mm81x_sta *msta = (struct mm81x_sta *)sta->drv_priv;
+
+	spin_lock_bh(&mors->mrc.lock);
+	if (msta->rc.tb) {
+		list_del_init(&msta->rc.list);
+		kfree(msta->rc.tb);
+		msta->rc.tb = NULL;
+	}
+	spin_unlock_bh(&mors->mrc.lock);
+}
+
+static void mm81x_rc_sta_fill_basic_rates(struct mm81x_skb_tx_info *tx_info,
+					  struct ieee80211_tx_info *info,
+					  int tx_bw)
+{
+	int i;
+	enum dot11_bandwidth bw_idx = mm81x_ratecode_bw_mhz_to_bw_index(tx_bw);
+	enum mm81x_rate_preamble pream = MM81X_RATE_PREAMBLE_S1G_SHORT;
+
+	mm81x_ratecode_mcs_index_set(&tx_info->rates[0].mm81x_ratecode, 0);
+	mm81x_ratecode_nss_index_set(&tx_info->rates[0].mm81x_ratecode,
+				     NSS_TO_NSS_IDX(1));
+	mm81x_ratecode_bw_index_set(&tx_info->rates[0].mm81x_ratecode, bw_idx);
+	if (bw_idx == DOT11_BANDWIDTH_1MHZ)
+		pream = MM81X_RATE_PREAMBLE_S1G_1M;
+	mm81x_ratecode_preamble_set(&tx_info->rates[0].mm81x_ratecode, pream);
+	tx_info->rates[0].count = 4;
+
+	for (i = 1; i < IEEE80211_TX_MAX_RATES; i++)
+		tx_info->rates[i].count = 0;
+
+	info->control.rates[0].idx = 0;
+	info->control.rates[0].count = tx_info->rates[0].count;
+	info->control.rates[0].flags = 0;
+	info->control.rates[1].idx = -1;
+}
+
+static int mm81x_rc_sta_get_rates(struct mm81x *mors, struct mm81x_sta *msta,
+				  struct mmrc_rate_table *rates, size_t size)
+{
+	int ret = -ENOENT;
+	struct list_head *pos;
+
+	spin_lock_bh(&mors->mrc.lock);
+	list_for_each(pos, &mors->mrc.stas) {
+		struct mm81x_rc_sta *mrc_sta =
+			list_entry(pos, struct mm81x_rc_sta, list);
+
+		if (&msta->rc == mrc_sta) {
+			ret = 0;
+			mmrc_get_rates(msta->rc.tb, rates, size);
+			break;
+		}
+	}
+	spin_unlock_bh(&mors->mrc.lock);
+
+	return ret;
+}
+
+static bool mm81x_rc_use_basic_rates(struct ieee80211_sta *sta,
+				     struct sk_buff *skb,
+				     struct ieee80211_hdr *hdr)
+{
+	struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+
+	if (!sta)
+		return true;
+
+	if (ieee80211_is_qos_nullfunc(hdr->frame_control) ||
+	    ieee80211_is_nullfunc(hdr->frame_control))
+		return true;
+
+	if (!ieee80211_is_data_qos(hdr->frame_control))
+		return true;
+
+	/* Use basic rates for EAPOL exchanges or when instructed */
+	if (unlikely((skb->protocol == cpu_to_be16(ETH_P_PAE) ||
+		      info->flags & IEEE80211_TX_CTL_USE_MINRATE)))
+		return true;
+
+	return false;
+}
+
+void mm81x_rc_sta_fill_tx_rates(struct mm81x *mors,
+				struct mm81x_skb_tx_info *tx_info,
+				struct sk_buff *skb, struct ieee80211_sta *sta,
+				int tx_bw, bool rts_allowed)
+{
+	int ret, i;
+	struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+	struct mm81x_sta *msta;
+	struct mmrc_rate_table rates;
+	struct ieee80211_tx_info *info = IEEE80211_SKB_CB(skb);
+
+	BUILD_BUG_ON((MMRC_BW_1MHZ != (enum mmrc_bw)DOT11_BANDWIDTH_1MHZ ||
+		      MMRC_BW_2MHZ != (enum mmrc_bw)DOT11_BANDWIDTH_2MHZ ||
+		      MMRC_BW_4MHZ != (enum mmrc_bw)DOT11_BANDWIDTH_4MHZ ||
+		      MMRC_BW_16MHZ != (enum mmrc_bw)DOT11_BANDWIDTH_16MHZ));
+
+	memset(&info->control.rates, 0, sizeof(info->control.rates));
+	memset(&info->status.rates, 0, sizeof(info->status.rates));
+	mm81x_rc_sta_fill_basic_rates(tx_info, info, tx_bw);
+
+	/* Use basic rates for non data packets */
+	if (mm81x_rc_use_basic_rates(sta, skb, hdr))
+		return;
+
+	msta = (struct mm81x_sta *)sta->drv_priv;
+	if (!msta)
+		return;
+
+	ret = mm81x_rc_sta_get_rates(mors, msta, &rates, skb->len);
+	if (ret != 0)
+		return;
+
+	for (i = 0; i < IEEE80211_TX_MAX_RATES; i++) {
+		info->control.rates[i].flags = 0;
+		if (rates.rates[i].rate != MMRC_MCS_UNUSED) {
+			u8 mcs = rates.rates[i].rate;
+			u8 nss_index = rates.rates[i].ss;
+			enum dot11_bandwidth bw_idx =
+				(enum dot11_bandwidth)rates.rates[i].bw;
+			enum mm81x_rate_preamble pream =
+				MM81X_RATE_PREAMBLE_S1G_SHORT;
+
+			mm81x_ratecode_bw_index_set(
+				&tx_info->rates[i].mm81x_ratecode, bw_idx);
+			mm81x_ratecode_mcs_index_set(
+				&tx_info->rates[i].mm81x_ratecode, mcs);
+			mm81x_ratecode_nss_index_set(
+				&tx_info->rates[i].mm81x_ratecode, nss_index);
+			if (bw_idx == DOT11_BANDWIDTH_1MHZ)
+				pream = MM81X_RATE_PREAMBLE_S1G_1M;
+			mm81x_ratecode_preamble_set(
+				&tx_info->rates[i].mm81x_ratecode, pream);
+			tx_info->rates[i].count = rates.rates[i].attempts;
+
+			if (rts_allowed &&
+			    (rates.rates[i].flags & BIT(MMRC_FLAGS_CTS_RTS))) {
+				mm81x_ratecode_enable_rts(
+					&tx_info->rates[i].mm81x_ratecode);
+				info->control.rates[i].flags |=
+					IEEE80211_TX_RC_USE_RTS_CTS;
+			}
+
+			if (rates.rates[i].guard == MMRC_GUARD_SHORT) {
+				mm81x_ratecode_enable_sgi(
+					&tx_info->rates[i].mm81x_ratecode);
+				info->control.rates[i].flags |=
+					IEEE80211_TX_RC_SHORT_GI;
+			}
+
+			/* Update skb tx_info */
+			info->control.rates[i].idx = rates.rates[i].rate;
+			info->control.rates[i].count = rates.rates[i].attempts;
+		} else {
+			info->control.rates[i].idx = -1;
+			info->control.rates[i].count = 0;
+			tx_info->rates[i].count = 0;
+		}
+	}
+}
+
+static void mm81x_rc_sta_set_rates(struct mm81x *mors, struct mm81x_sta *msta,
+				   struct mmrc_rate_table *rates, int attempts,
+				   bool was_aggregated)
+{
+	struct list_head *pos;
+
+	spin_lock_bh(&mors->mrc.lock);
+	list_for_each(pos, &mors->mrc.stas) {
+		struct mm81x_rc_sta *mrc_sta =
+			list_entry(pos, struct mm81x_rc_sta, list);
+
+		if (&msta->rc == mrc_sta) {
+			mmrc_feedback(msta->rc.tb, rates, attempts,
+				      was_aggregated);
+			break;
+		}
+	}
+	spin_unlock_bh(&mors->mrc.lock);
+}
+
+void mm81x_rc_sta_feedback_rates(struct mm81x *mors, struct sk_buff *skb,
+				 struct ieee80211_sta *sta,
+				 struct mm81x_skb_tx_status *tx_sts,
+				 int attempts)
+{
+	int i;
+	u32 tx_airtime = 0;
+	struct mmrc_rate_table rates;
+	struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+	struct ieee80211_tx_info *txi = IEEE80211_SKB_CB(skb);
+	struct ieee80211_tx_rate *r = &txi->status.rates[0];
+	int count = min_t(int, MM81X_SKB_MAX_RATES, IEEE80211_TX_MAX_RATES);
+	struct mm81x_sta *msta = msta = (struct mm81x_sta *)sta->drv_priv;
+
+	/* Don't update rate info if basic rates were used */
+	if (mm81x_rc_use_basic_rates(sta, skb, hdr))
+		goto exit;
+
+	if (attempts <= 0)
+		/* Did we really send the packet? */
+		goto exit;
+
+	for (i = 0; i < count; i++) {
+		rates.rates[i].rate = mm81x_ratecode_mcs_index_get(
+			tx_sts->rates[i].mm81x_ratecode);
+		rates.rates[i].ss = mm81x_ratecode_nss_index_get(
+			tx_sts->rates[i].mm81x_ratecode);
+		rates.rates[i].guard =
+			mm81x_ratecode_sgi_get(tx_sts->rates[i].mm81x_ratecode);
+		rates.rates[i].bw = mm81x_ratecode_bw_index_get(
+			tx_sts->rates[i].mm81x_ratecode);
+		rates.rates[i].flags =
+			mm81x_ratecode_rts_get(tx_sts->rates[i].mm81x_ratecode);
+		rates.rates[i].attempts = tx_sts->rates[i].count;
+
+		tx_airtime +=
+			mmrc_calculate_rate_tx_time(&rates.rates[i], skb->len);
+	}
+
+	if (msta) {
+		/*
+		 * Save the rate information. This will be used to update
+		 * station's tx rate stats
+		 */
+		msta->last_sta_tx_rate.bw = rates.rates[0].bw;
+		msta->last_sta_tx_rate.rate = rates.rates[0].rate;
+		msta->last_sta_tx_rate.ss = rates.rates[0].ss;
+		msta->last_sta_tx_rate.guard = rates.rates[0].guard;
+	}
+
+	mm81x_rc_sta_set_rates(mors, msta, &rates, attempts,
+			       !!(le32_to_cpu(tx_sts->flags) &
+				  MM81X_TX_STATUS_WAS_AGGREGATED));
+
+	ieee80211_sta_register_airtime(sta, tx_sts->tid, tx_airtime, 0);
+
+exit:
+	ieee80211_tx_info_clear_status(txi);
+
+	if (!(le32_to_cpu(tx_sts->flags) & MM81X_TX_STATUS_FLAGS_NO_ACK) &&
+	    !(txi->flags & IEEE80211_TX_CTL_NO_ACK))
+		txi->flags |= IEEE80211_TX_STAT_ACK;
+
+	if (le32_to_cpu(tx_sts->flags) & MM81X_TX_STATUS_FLAGS_PS_FILTERED) {
+		txi->flags |= IEEE80211_TX_STAT_TX_FILTERED;
+
+		/*
+		 * Clear TX CTL AMPDU flag so that this frame gets rescheduled
+		 * in ieee80211_handle_filtered_frame(). This flag will get set
+		 * again by mac80211's tx path on rescheduling.
+		 */
+		txi->flags &= ~IEEE80211_TX_CTL_AMPDU;
+		if (msta) {
+			if (!msta->tx_ps_filter_en)
+				dev_dbg(mors->dev, "TX ps filter set sta[%pM]",
+					msta->addr);
+			msta->tx_ps_filter_en = true;
+		}
+	}
+
+	for (i = 0; i < count; i++) {
+		if (tx_sts->rates[i].count > 0) {
+			r[i].count = tx_sts->rates[i].count;
+			r[i].flags |= IEEE80211_TX_RC_MCS;
+		} else {
+			r[i].idx = -1;
+		}
+	}
+
+	/* single packet per A-MPDU (for now) */
+	if (txi->flags & IEEE80211_TX_CTL_AMPDU) {
+		txi->flags |= IEEE80211_TX_STAT_AMPDU;
+		txi->status.ampdu_len = 1;
+		txi->status.ampdu_ack_len =
+			txi->flags & IEEE80211_TX_STAT_ACK ? 1 : 0;
+	}
+
+	/*
+	 * Inform mac80211 that the SP (elicited by a PS-Poll or u-APSD) is
+	 * over
+	 */
+	if (sta && (txi->flags & IEEE80211_TX_STATUS_EOSP)) {
+		txi->flags &= ~IEEE80211_TX_STATUS_EOSP;
+		ieee80211_sta_eosp(sta);
+	}
+}
+
+void mm81x_rc_sta_state_check(struct mm81x *mors, struct ieee80211_vif *vif,
+			      struct ieee80211_sta *sta,
+			      enum ieee80211_sta_state old_state,
+			      enum ieee80211_sta_state new_state)
+{
+	struct mm81x_sta *msta = (struct mm81x_sta *)sta->drv_priv;
+
+	/* Add to Morse RC STA list */
+	if (old_state < new_state && new_state == IEEE80211_STA_ASSOC) {
+		/* Newly associated, add to RC */
+		mm81x_rc_sta_add(mors, vif, sta);
+	} else if (old_state > new_state && (old_state == IEEE80211_STA_ASSOC ||
+					     old_state == IEEE80211_STA_AUTH)) {
+		/* Lost or failed association; remove from list */
+		mm81x_rc_sta_remove(mors, sta);
+	} else if (old_state < new_state && old_state == IEEE80211_STA_NONE &&
+		   msta->rc.list.prev) {
+		/*
+		 * Special case for driver warning issue causing a sta to be
+		 * left on the list
+		 */
+		dev_dbg(mors->dev, "Remove stale sta from rc list");
+		mm81x_rc_sta_remove(mors, sta);
+	}
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 22/33] wifi: mm81x: add rc.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/rc.h | 51 ++++++++++++++++++++++
 1 file changed, 51 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/rc.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/rc.h b/drivers/net/wireless/morsemicro/mm81x/rc.h
new file mode 100644
index 000000000000..53f129024408
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/rc.h
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_RC_H_
+#define _MM81X_RC_H_
+
+#include <linux/list.h>
+#include <linux/workqueue.h>
+#include "core.h"
+#include "mmrc.h"
+
+struct mm81x_vif;
+
+#define INIT_MAX_RATES_NUM 4
+
+struct mm81x_rc {
+	/* Serialise rate control queue manipulation and timer functions */
+	spinlock_t lock;
+	struct list_head stas;
+	struct timer_list timer;
+	struct work_struct work;
+	struct mm81x *mors;
+};
+
+struct mm81x_rc_sta {
+	struct mmrc_table *tb;
+	struct list_head list;
+	unsigned long last_update;
+};
+
+void mm81x_rc_init(struct mm81x *mors);
+void mm81x_rc_deinit(struct mm81x *mors);
+int mm81x_rc_sta_add(struct mm81x *mors, struct ieee80211_vif *vif,
+		     struct ieee80211_sta *sta);
+void mm81x_rc_sta_remove(struct mm81x *mors, struct ieee80211_sta *sta);
+void mm81x_rc_sta_fill_tx_rates(struct mm81x *mors,
+				struct mm81x_skb_tx_info *tx_info,
+				struct sk_buff *skb, struct ieee80211_sta *sta,
+				int tx_bw, bool rts_allowed);
+void mm81x_rc_sta_feedback_rates(struct mm81x *mors, struct sk_buff *skb,
+				 struct ieee80211_sta *sta,
+				 struct mm81x_skb_tx_status *tx_sts,
+				 int tx_attempts);
+void mm81x_rc_sta_state_check(struct mm81x *mors, struct ieee80211_vif *vif,
+			      struct ieee80211_sta *sta,
+			      enum ieee80211_sta_state old_state,
+			      enum ieee80211_sta_state new_state);
+
+#endif /* !_MM81X_RC_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 23/33] mmc: sdio: add Morse Micro vendor ids
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Ulf Hansson
  Cc: arien.judge, dan.callaghan, ayman.grais, linux-wireless,
	Lachlan Hodges, linux-mmc, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

Add the Morse Micro mm81x series vendor ids.

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
Hi Ulf, since v2 we've made another slight change to the name removing
a "B2" suffix so even though you acked v2, I suppose it's best to
recollect again - sorry about that. As last time, the patchset is
split once per file following wireless driver submission guidelines,
but will be a pull request with a single patch adding the driver +
sdio ID once review is complete through the wireless tree.
---
 include/linux/mmc/sdio_ids.h | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/include/linux/mmc/sdio_ids.h b/include/linux/mmc/sdio_ids.h
index 0685dd717e85..bbffad9ae88e 100644
--- a/include/linux/mmc/sdio_ids.h
+++ b/include/linux/mmc/sdio_ids.h
@@ -117,6 +117,9 @@
 #define SDIO_VENDOR_ID_MICROCHIP_WILC		0x0296
 #define SDIO_DEVICE_ID_MICROCHIP_WILC1000	0x5347
 
+#define SDIO_VENDOR_ID_MORSEMICRO		0x325b
+#define SDIO_DEVICE_ID_MORSEMICRO_MM8108	0x0809
+
 #define SDIO_VENDOR_ID_NXP			0x0471
 #define SDIO_DEVICE_ID_NXP_IW61X		0x0205
 
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 24/33] wifi: mm81x: add sdio.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/sdio.c | 613 +++++++++++++++++++
 1 file changed, 613 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/sdio.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/sdio.c b/drivers/net/wireless/morsemicro/mm81x/sdio.c
new file mode 100644
index 000000000000..96fce187dd35
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/sdio.c
@@ -0,0 +1,613 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/workqueue.h>
+#include <linux/mmc/card.h>
+#include <linux/mmc/mmc.h>
+#include <linux/mmc/host.h>
+#include <linux/mmc/sdio_func.h>
+#include <linux/mmc/sdio_ids.h>
+#include <linux/mmc/sdio.h>
+#include <linux/mmc/sd.h>
+#include "hw.h"
+#include "core.h"
+#include "bus.h"
+#include "mac.h"
+#include "fw.h"
+#include "hif.h"
+
+/*
+ * Value to indicate that the base address for bulk/register
+ * read/writes has yet to be set
+ */
+#define MM81X_SDIO_BASE_ADDR_UNSET 0xFFFFFFFF
+
+#define MM81X_SDIO_ALIGNMENT (8)
+
+#define MM81X_SDIO_REG_ADDRESS_BASE 0x10000
+#define MM81X_SDIO_REG_ADDRESS_WINDOW_0 MM81X_SDIO_REG_ADDRESS_BASE
+#define MM81X_SDIO_REG_ADDRESS_WINDOW_1 (MM81X_SDIO_REG_ADDRESS_BASE + 1)
+#define MM81X_SDIO_REG_ADDRESS_CONFIG (MM81X_SDIO_REG_ADDRESS_BASE + 2)
+
+struct mm81x_sdio {
+	bool enabled;
+	u32 bulk_addr_base;
+	u32 register_addr_base;
+	struct sdio_func *func;
+	const struct sdio_device_id *id;
+};
+
+static void irq_handler(struct sdio_func *func1)
+{
+	struct sdio_func *func = func1->card->sdio_func[1];
+	struct mm81x *mors = sdio_get_drvdata(func);
+
+	mm81x_hw_irq_handle(mors);
+}
+
+static int mm81x_sdio_enable_irq(struct mm81x_sdio *sdio)
+{
+	int ret;
+	struct sdio_func *func = sdio->func;
+	struct sdio_func *func1 = func->card->sdio_func[0];
+	struct mm81x *mors = sdio_get_drvdata(func);
+
+	sdio_claim_host(func);
+	ret = sdio_claim_irq(func1, irq_handler);
+	if (ret)
+		dev_err(mors->dev, "Failed to enable sdio irq: %d\n", ret);
+
+	sdio_release_host(func);
+	return ret;
+}
+
+static void mm81x_sdio_disable_irq(struct mm81x_sdio *sdio)
+{
+	struct sdio_func *func = sdio->func;
+	struct sdio_func *func1 = func->card->sdio_func[0];
+
+	sdio_claim_host(func);
+	sdio_release_irq(func1);
+	sdio_release_host(func);
+}
+
+static void mm81x_sdio_set_irq(struct mm81x *mors, bool enable)
+{
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mors->drv_priv;
+
+	if (enable)
+		mm81x_sdio_enable_irq(sdio);
+	else
+		mm81x_sdio_disable_irq(sdio);
+}
+
+static u32 mm81x_sdio_calculate_base_address(u32 address, u8 access)
+{
+	return (address & MM81X_SDIO_RW_ADDR_BOUNDARY_MASK) | (access & 0x3);
+}
+
+static void mm81x_sdio_reset_base_address(struct mm81x_sdio *sdio)
+{
+	sdio->bulk_addr_base = MM81X_SDIO_BASE_ADDR_UNSET;
+	sdio->register_addr_base = MM81X_SDIO_BASE_ADDR_UNSET;
+}
+
+static int mm81x_sdio_set_func_address_base(struct mm81x_sdio *sdio,
+					    struct sdio_func *func, u32 address,
+					    u8 access)
+{
+	int ret = 0;
+	int retries = 0;
+	static const int max_retries = 3;
+	struct sdio_func *func2 = sdio->func;
+	struct mm81x *mors = sdio_get_drvdata(sdio->func);
+	s32 calculated_addr_base =
+		mm81x_sdio_calculate_base_address(address, access);
+	u32 *current_addr_base = func == func2 ? &sdio->bulk_addr_base :
+						 &sdio->register_addr_base;
+
+	if ((*current_addr_base) == calculated_addr_base &&
+	    *current_addr_base != MM81X_SDIO_BASE_ADDR_UNSET)
+		return ret;
+
+retry:
+	sdio_writeb(func, (u8)u32_get_bits(address, GENMASK(23, 16)),
+		    MM81X_SDIO_REG_ADDRESS_WINDOW_0, &ret);
+	if (ret)
+		goto err;
+
+	sdio_writeb(func, (u8)u32_get_bits(address, GENMASK(31, 24)),
+		    MM81X_SDIO_REG_ADDRESS_WINDOW_1, &ret);
+	if (ret)
+		goto err;
+
+	sdio_writeb(func, access & 0x3, MM81X_SDIO_REG_ADDRESS_CONFIG, &ret);
+	if (ret)
+		goto err;
+
+	*current_addr_base = calculated_addr_base;
+	if (retries)
+		dev_dbg(mors->dev, "%s succeeded after %d retries\n", __func__,
+			retries);
+
+	return ret;
+err:
+	retries++;
+	if (ret == -ETIMEDOUT && retries <= max_retries) {
+		dev_dbg(mors->dev, "%s failed (%d), retrying (%d/%d)\n",
+			__func__, ret, retries, max_retries);
+		goto retry;
+	}
+
+	*current_addr_base = MM81X_SDIO_BASE_ADDR_UNSET;
+	return ret;
+}
+
+static int mm81x_sdio_mem_write_block(struct mm81x_sdio *sdio, u32 address,
+				      u8 *data, ssize_t size)
+{
+	int ret;
+	struct sdio_func *func2 = sdio->func;
+	struct mm81x *mors = sdio_get_drvdata(sdio->func);
+
+	mm81x_sdio_set_func_address_base(sdio, func2, address,
+					 MM81X_CONFIG_ACCESS_4BYTE);
+	if (unlikely(!IS_ALIGNED((uintptr_t)data,
+				 mors->bus_ops->bulk_alignment))) {
+		ret = -EBADE;
+		goto exit;
+	}
+
+	address &= 0x0000FFFF; /* remove base and keep offset */
+	ret = sdio_memcpy_toio(func2, address, data, size);
+	if (ret)
+		goto exit;
+
+	ret = size;
+exit:
+	return ret;
+}
+
+static int mm81x_sdio_mem_write_byte(struct mm81x_sdio *sdio, u32 address,
+				     u8 *data, ssize_t size)
+{
+	int i, ret;
+	struct sdio_func *func1 = sdio->func->card->sdio_func[0];
+
+	mm81x_sdio_set_func_address_base(sdio, func1, address,
+					 MM81X_CONFIG_ACCESS_1BYTE);
+
+	address &= 0x0000FFFF; /* remove base and keep offset */
+	for (i = 0; i < size; i++) {
+		sdio_writeb(func1, data[i], address + i, (int *)&ret);
+		if (ret)
+			goto exit;
+	}
+
+	ret = size;
+exit:
+	return ret;
+}
+
+static void mm81x_sdio_claim_host(struct mm81x *mors)
+{
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mors->drv_priv;
+	struct sdio_func *func = sdio->func;
+
+	sdio_claim_host(func);
+}
+
+static void mm81x_sdio_release_host(struct mm81x *mors)
+{
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mors->drv_priv;
+	struct sdio_func *func = sdio->func;
+
+	sdio_release_host(func);
+}
+
+static int mm81x_sdio_mem_read_block(struct mm81x_sdio *sdio, u32 address,
+				     u8 *data, ssize_t size)
+{
+	int ret;
+	struct sdio_func *func2 = sdio->func;
+	struct mm81x *mors = sdio_get_drvdata(sdio->func);
+
+	mm81x_sdio_set_func_address_base(sdio, func2, address,
+					 MM81X_CONFIG_ACCESS_4BYTE);
+	if (unlikely(!IS_ALIGNED((uintptr_t)data,
+				 mors->bus_ops->bulk_alignment))) {
+		ret = -EBADE;
+		goto exit;
+	}
+
+	address &= 0x0000FFFF; /* remove base and keep offset */
+	ret = sdio_memcpy_fromio(func2, data, address, size);
+	if (ret)
+		goto exit;
+
+	/*
+	 * Observed sometimes that SDIO read repeats the first 4-bytes
+	 * word twice, overwriting second word (hence, tail will be
+	 * overwritten with 'sync' byte). When this happens, reading
+	 * will fetch the correct word. NB: if repeated again, pass it
+	 * anyway and upper layers will handle it
+	 */
+
+	if (size >= 8 && memcmp(data, data + 4, 4) == 0)
+		sdio_memcpy_fromio(func2, data, address, 8);
+
+	ret = size;
+exit:
+	return ret;
+}
+
+static int mm81x_sdio_mem_read_byte(struct mm81x_sdio *sdio, u32 address,
+				    u8 *data, ssize_t size)
+{
+	int i, ret;
+	struct sdio_func *func1 = sdio->func->card->sdio_func[0];
+
+	mm81x_sdio_set_func_address_base(sdio, func1, address,
+					 MM81X_CONFIG_ACCESS_1BYTE);
+
+	address &= 0x0000FFFF; /* remove base and keep offset */
+	for (i = 0; i < size; i++) {
+		data[i] = sdio_readb(func1, address + i, (int *)&ret);
+		if (ret)
+			goto exit;
+	}
+
+	ret = size;
+exit:
+	return ret;
+}
+
+static int mm81x_sdio_dm_write(struct mm81x *mors, u32 address, const u8 *data,
+			       int len)
+{
+	int ret = 0;
+	int block_len, byte_len;
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mors->drv_priv;
+	int remaining = len;
+	int offset = 0;
+
+	if (remaining > 0 && address & 0x3) {
+		len = 4 - (address & 0x3);
+		ret = mm81x_sdio_mem_write_byte(sdio, address, (u8 *)data, len);
+		if (ret != len)
+			return -EIO;
+
+		offset += len;
+		remaining -= len;
+	}
+
+	while ((remaining) > 0) {
+		/*
+		 * We can only write up to the end of a single window in
+		 * each write operation.
+		 */
+		u32 window_end = (address + offset) |
+				 ~MM81X_SDIO_RW_ADDR_BOUNDARY_MASK;
+
+		len = min(remaining, (int)(window_end + 1 - address - offset));
+		block_len = len & ~0x3;
+		byte_len = len & 0x3;
+
+		if (block_len) {
+			ret = mm81x_sdio_mem_write_block(sdio, address + offset,
+							 (u8 *)(data + offset),
+							 block_len);
+			if (ret != block_len)
+				return -EIO;
+
+			offset += block_len;
+		}
+
+		if (byte_len) {
+			ret = mm81x_sdio_mem_write_byte(sdio, address + offset,
+							(u8 *)(data + offset),
+							byte_len);
+			if (ret != byte_len)
+				return -EIO;
+
+			offset += byte_len;
+		}
+
+		remaining -= len;
+	}
+
+	return 0;
+}
+
+static int mm81x_sdio_dm_read(struct mm81x *mors, u32 address, u8 *data,
+			      int len)
+{
+	int ret = 0;
+	int block_len, byte_len;
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mors->drv_priv;
+	int remaining = len;
+	int offset = 0;
+
+	if (remaining > 0 && address & 0x3) {
+		len = 4 - (address & 0x3);
+		ret = mm81x_sdio_mem_read_byte(sdio, address, data, len);
+		if (ret != len)
+			return -EIO;
+
+		offset += len;
+		remaining -= len;
+	}
+
+	while (remaining > 0) {
+		/*
+		 * We can only read up to the end of a single window in
+		 * each read operation.
+		 */
+		u32 window_end = (address + offset) |
+				 ~MM81X_SDIO_RW_ADDR_BOUNDARY_MASK;
+
+		len = min(remaining, (int)(window_end + 1 - address - offset));
+		block_len = len & ~0x3;
+		byte_len = len & 0x3;
+
+		if (block_len) {
+			ret = mm81x_sdio_mem_read_block(sdio, address + offset,
+							data + offset,
+							block_len);
+			if (ret != block_len)
+				return -EIO;
+
+			offset += block_len;
+		}
+
+		if (byte_len) {
+			ret = mm81x_sdio_mem_read_byte(sdio, address + offset,
+						       data + offset, byte_len);
+			if (ret != byte_len)
+				return -EIO;
+
+			offset += byte_len;
+		}
+
+		remaining -= len;
+	}
+
+	return 0;
+}
+
+static int mm81x_sdio_reg32_write(struct mm81x *mors, u32 address, u32 val)
+{
+	ssize_t ret = 0;
+	u32 original_address = address;
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mors->drv_priv;
+	struct sdio_func *func1 = sdio->func->card->sdio_func[0];
+
+	mm81x_sdio_set_func_address_base(sdio, func1, address,
+					 MM81X_CONFIG_ACCESS_4BYTE);
+
+	address &= 0x0000FFFF;
+	sdio_writel(func1, (__force u32)cpu_to_le32(val),
+		    (__force u32)cpu_to_le32(address), (int *)&ret);
+	if (ret)
+		goto error;
+
+	return 0;
+
+error:
+	if (original_address == MM81X_REG_RESET(mors) &&
+	    val == MM81X_REG_RESET_VALUE(mors)) {
+		dev_dbg(mors->dev,
+			"SDIO reset detected, invalidating base addr\n");
+		mm81x_sdio_reset_base_address(sdio);
+	}
+
+	return -EIO;
+}
+
+static int mm81x_sdio_reg32_read(struct mm81x *mors, u32 address, u32 *val)
+{
+	u32 value;
+	ssize_t ret = 0;
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mors->drv_priv;
+	struct sdio_func *func1 = sdio->func->card->sdio_func[0];
+
+	mm81x_sdio_set_func_address_base(sdio, func1, address,
+					 MM81X_CONFIG_ACCESS_4BYTE);
+
+	address &= 0x0000FFFF;
+	value = sdio_readl(func1, (__force u32)cpu_to_le32(address),
+			   (int *)&ret);
+	if (ret)
+		return ret;
+
+	*val = le32_to_cpup((__le32 *)&value);
+	return 0;
+}
+
+static void mm81x_sdio_bus_enable(struct mm81x *mors, bool enable)
+{
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mors->drv_priv;
+	struct sdio_func *func = sdio->func;
+	struct mmc_host *host = func->card->host;
+
+	sdio_claim_host(func);
+
+	if (enable) {
+		/*
+		 * No need to do anything special to re-enable the sdio bus.
+		 * This will happen automatically when a read/write is
+		 * attempted and sdio->bulk_addr_base == 0.
+		 */
+		sdio->enabled = true;
+		host->ops->enable_sdio_irq(host, 1);
+		dev_dbg(mors->dev, "%s: enabling bus\n", __func__);
+	} else {
+		host->ops->enable_sdio_irq(host, 0);
+		mm81x_sdio_reset_base_address(sdio);
+		sdio->enabled = false;
+		dev_dbg(mors->dev, "%s: disabling bus\n", __func__);
+	}
+
+	sdio_release_host(func);
+}
+
+static void mm81x_sdio_reset(struct sdio_func *func)
+{
+	sdio_claim_host(func);
+	sdio_disable_func(func);
+	sdio_release_host(func);
+
+	mdelay(20);
+
+	sdio_claim_host(func);
+	sdio_disable_func(func);
+	mmc_hw_reset(func->card);
+	sdio_enable_func(func);
+	sdio_release_host(func);
+}
+
+static void mm81x_sdio_config_burst_mode(struct mm81x *mors, bool enable_burst)
+{
+	u8 burst_mode = (enable_burst) ? SDIO_WORD_BURST_SIZE_16 :
+					 SDIO_WORD_BURST_DISABLE;
+
+	mm81x_hw_enable_burst_mode(mors, burst_mode);
+}
+
+static const struct mm81x_bus_ops mm81x_sdio_ops = {
+	.dm_read = mm81x_sdio_dm_read,
+	.dm_write = mm81x_sdio_dm_write,
+	.reg32_read = mm81x_sdio_reg32_read,
+	.reg32_write = mm81x_sdio_reg32_write,
+	.set_bus_enable = mm81x_sdio_bus_enable,
+	.claim = mm81x_sdio_claim_host,
+	.release = mm81x_sdio_release_host,
+	.config_burst_mode = mm81x_sdio_config_burst_mode,
+	.set_irq = mm81x_sdio_set_irq,
+	.bulk_alignment = MM81X_SDIO_ALIGNMENT
+};
+
+static int mm81x_sdio_enable(struct mm81x_sdio *sdio)
+{
+	int ret;
+	struct sdio_func *func = sdio->func;
+	struct mm81x *mors = sdio_get_drvdata(func);
+
+	sdio_claim_host(func);
+	ret = sdio_enable_func(func);
+	if (ret)
+		dev_err(mors->dev, "sdio_enable_func failed: %d\n", ret);
+	sdio_release_host(func);
+	return ret;
+}
+
+static void mm81x_sdio_release(struct mm81x_sdio *sdio)
+{
+	struct sdio_func *func = sdio->func;
+
+	sdio_claim_host(func);
+	sdio_disable_func(func);
+	sdio_release_host(func);
+}
+
+static int mm81x_sdio_probe(struct sdio_func *func,
+			    const struct sdio_device_id *id)
+{
+	int ret = 0;
+	struct mm81x *mors = NULL;
+	struct mm81x_sdio *sdio;
+	struct device *dev = &func->dev;
+
+	if (func->num == 1)
+		return 0;
+
+	if (func->num != 2)
+		return -ENODEV;
+
+	mors = mm81x_core_alloc(sizeof(*sdio), dev);
+	if (!mors)
+		return -ENOMEM;
+
+	mors->bus_ops = &mm81x_sdio_ops;
+	mors->bus_type = MM81X_BUS_TYPE_SDIO;
+
+	sdio = (struct mm81x_sdio *)mors->drv_priv;
+	sdio->func = func;
+	sdio->id = id;
+	sdio->enabled = true;
+	mm81x_sdio_reset_base_address(sdio);
+
+	sdio_set_drvdata(func, mors);
+
+	ret = mm81x_sdio_enable(sdio);
+	if (ret)
+		goto err_core_free;
+
+	mm81x_sdio_config_burst_mode(mors, true);
+
+	ret = mm81x_core_init(mors);
+	if (ret)
+		goto err_sdio_release;
+
+	ret = mm81x_sdio_enable_irq(sdio);
+	if (ret)
+		goto err_core_deinit;
+
+	ret = mm81x_core_register(mors);
+	if (ret)
+		goto err_disable_irq;
+
+	return 0;
+
+err_disable_irq:
+	mm81x_sdio_disable_irq(sdio);
+err_core_deinit:
+	mm81x_core_deinit(mors);
+err_sdio_release:
+	mm81x_sdio_release(sdio);
+err_core_free:
+	mm81x_core_free(mors);
+	return ret;
+}
+
+static void mm81x_sdio_remove(struct sdio_func *func)
+{
+	struct mm81x *mors = sdio_get_drvdata(func);
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mors->drv_priv;
+
+	if (!mors)
+		return;
+
+	mm81x_core_unregister(mors);
+	mm81x_sdio_disable_irq(sdio);
+	mm81x_core_deinit(mors);
+	mm81x_sdio_release(sdio);
+	mm81x_sdio_reset(func);
+	mm81x_core_free(mors);
+	sdio_set_drvdata(func, NULL);
+}
+
+static const struct sdio_device_id mm81x_sdio_devices[] = {
+	{ SDIO_DEVICE(SDIO_VENDOR_ID_MORSEMICRO,
+		      SDIO_DEVICE_ID_MORSEMICRO_MM8108) },
+	{},
+};
+
+MODULE_DEVICE_TABLE(sdio, mm81x_sdio_devices);
+
+static struct sdio_driver mm81x_sdio_driver = {
+	.name = "mm81x_sdio",
+	.id_table = mm81x_sdio_devices,
+	.probe = mm81x_sdio_probe,
+	.remove = mm81x_sdio_remove,
+};
+
+module_sdio_driver(mm81x_sdio_driver);
+
+MODULE_AUTHOR("Morse Micro");
+MODULE_DESCRIPTION("Driver support for Morse Micro MM81X SDIO devices");
+MODULE_LICENSE("Dual BSD/GPL");
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 25/33] wifi: mm81x: add skbq.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/skbq.c | 1065 ++++++++++++++++++
 1 file changed, 1065 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/skbq.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/skbq.c b/drivers/net/wireless/morsemicro/mm81x/skbq.c
new file mode 100644
index 000000000000..9bc7899c05a8
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/skbq.c
@@ -0,0 +1,1065 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/types.h>
+#include <linux/slab.h>
+#include <linux/workqueue.h>
+#include <linux/ktime.h>
+#include <linux/skbuff.h>
+#include <linux/jiffies.h>
+#include "hif.h"
+#include "skbq.h"
+#include "mac.h"
+#include "command.h"
+#include "bus.h"
+
+/* Returns number of bytes needed to word align */
+#define BYTES_NEEDED_TO_WORD_ALIGN(bytes) \
+	((bytes) & 0x3 ? (4 - ((bytes) & 0x3)) : 0)
+
+/* Rounds down to the nearest word boundary */
+#define ROUND_DOWN_TO_WORD(bytes)                                  \
+	(BYTES_NEEDED_TO_WORD_ALIGN(bytes) ?                       \
+		 bytes - (4 - BYTES_NEEDED_TO_WORD_ALIGN(bytes)) : \
+		 bytes)
+
+#define MM81X_SKBQ_MAX_TXQ_LEN 32
+#define MM81X_SKBQ_TX_QUEUED_LIFETIME_MS 1000
+#define MM81X_SKBQ_TX_STATUS_LIFETIME_MS (15 * 1000)
+
+/* Returns padding needed to align x up to a 4-byte boundary */
+#define MM81X_PAD4(x) (((x) & 0x3) ? (4 - ((x) & 0x3)) : 0)
+
+struct mm81x_tx_status_priv {
+	/*
+	 * Time (jiffies) at which this packet has spent too long the pending
+	 * queue, waiting for status notification from the firmware, and
+	 * should be considered lost.
+	 */
+	unsigned long tx_status_expiry;
+};
+
+static struct mm81x_tx_status_priv *
+__mm81x_skbq_tx_status_priv(struct sk_buff *skb)
+{
+	struct ieee80211_tx_info *tx_info = IEEE80211_SKB_CB(skb);
+
+	BUILD_BUG_ON(sizeof(struct mm81x_tx_status_priv) >
+		     sizeof(tx_info->status.status_driver_data));
+	return (struct mm81x_tx_status_priv *)&tx_info->status
+		.status_driver_data[0];
+}
+
+static bool __mm81x_skbq_has_pending_tx_skb_timed_out(struct sk_buff *skb)
+{
+	struct mm81x_tx_status_priv *info = __mm81x_skbq_tx_status_priv(skb);
+
+	/* If our timestamp value is in the past then we have timed out. */
+	return time_is_before_jiffies(info->tx_status_expiry);
+}
+
+static u32 __mm81x_skbq_size(const struct mm81x_skbq *mq)
+{
+	return mq->skbq_size;
+}
+
+static u32 __mm81x_skbq_space(const struct mm81x_skbq *mq)
+{
+	return MM81X_SKBQ_SIZE - __mm81x_skbq_size(mq);
+}
+
+static bool __mm81x_skbq_over_threshold(struct mm81x_skbq *mq)
+{
+	return skb_queue_len(&mq->skbq) >= MM81X_SKBQ_MAX_TXQ_LEN;
+}
+
+static bool __mm81x_skbq_under_threshold(struct mm81x_skbq *mq)
+{
+	return skb_queue_len(&mq->skbq) < (MM81X_SKBQ_MAX_TXQ_LEN - 2);
+}
+
+static void __mm81x_skbq_unlink(struct mm81x_skbq *mq,
+				struct sk_buff_head *queue, struct sk_buff *skb)
+{
+	if (queue == &mq->skbq) {
+		WARN_ON(skb->len > mq->skbq_size);
+		mq->skbq_size -= min(skb->len, mq->skbq_size);
+	}
+
+	__skb_unlink(skb, queue);
+}
+
+static int __mm81x_skbq_put(struct mm81x_skbq *mq, struct sk_buff_head *queue,
+			    struct sk_buff *skb, bool queue_at_head,
+			    struct sk_buff *queue_before)
+{
+	/* Limit the size of the Tx queue, but not the pending queue */
+	if (queue == &mq->skbq) {
+		if (skb->len > __mm81x_skbq_space(mq))
+			return -ENOMEM;
+
+		mq->skbq_size += skb->len;
+	}
+
+	if (queue_before)
+		__skb_queue_before(queue, queue_before, skb);
+	else if (queue_at_head)
+		__skb_queue_head(queue, skb);
+	else
+		__skb_queue_tail(queue, skb);
+
+	return 0;
+}
+
+static void __mm81x_skbq_pkt_id(struct mm81x_skbq *mq, struct sk_buff *skb)
+{
+	struct mm81x_skb_hdr *hdr = (struct mm81x_skb_hdr *)skb->data;
+
+	hdr->tx_info.pkt_id = cpu_to_le32(mq->pkt_seq++);
+}
+
+static struct mm81x_skbq *
+__mm81x_skbq_tx_status_to_skbq(struct mm81x *mors,
+			       const struct mm81x_skb_tx_status *tx_sts)
+{
+	int aci;
+	struct mm81x_skbq *mq = NULL;
+
+	switch (tx_sts->channel) {
+	case MM81X_SKB_CHAN_DATA:
+	case MM81X_SKB_CHAN_DATA_NOACK:
+		aci = dot11_tid_to_ac(tx_sts->tid);
+		mq = mm81x_hif_get_tx_data_queue(mors, aci);
+		break;
+	case MM81X_SKB_CHAN_MGMT:
+		mq = mm81x_hif_get_tx_mgmt_queue(mors);
+		break;
+	case MM81X_SKB_CHAN_BEACON:
+		mq = mm81x_hif_get_tx_beacon_queue(mors);
+		break;
+	default:
+		dev_err(mors->dev,
+			"unexpected channel on reported tx status [%d]",
+			tx_sts->channel);
+	}
+
+	return mq;
+}
+
+void mm81x_skbq_pull_hdr_post_tx(struct sk_buff *skb)
+{
+	skb_pull(skb, sizeof(struct mm81x_skb_hdr) +
+			      ((struct mm81x_skb_hdr *)skb->data)->offset);
+}
+
+static void mm81x_skbq_insert_pending(struct mm81x_skbq *mq,
+				      struct sk_buff *skb, __le32 insertion_id)
+{
+	struct sk_buff *pfirst, *pnext;
+	struct mm81x_skb_hdr *mhdr;
+	struct sk_buff *tail = skb_peek_tail(&mq->skbq);
+
+	__mm81x_skbq_unlink(mq, &mq->pending, skb);
+
+	if (!tail) {
+		__mm81x_skbq_put(mq, &mq->skbq, skb, false, NULL);
+		return;
+	}
+
+	/* Check if it should just be inserted on to the end */
+	mhdr = (struct mm81x_skb_hdr *)tail->data;
+	WARN_ON(insertion_id == mhdr->tx_info.pkt_id);
+	if (le32_to_cpu(insertion_id) >= le32_to_cpu(mhdr->tx_info.pkt_id)) {
+		__mm81x_skbq_put(mq, &mq->skbq, skb, false, NULL);
+		return;
+	}
+
+	/* Otherwise, re-insert to correct spot in skbq */
+	skb_queue_walk_safe(&mq->skbq, pfirst, pnext) {
+		mhdr = (struct mm81x_skb_hdr *)pfirst->data;
+
+		WARN_ON(insertion_id == mhdr->tx_info.pkt_id);
+		if (le32_to_cpu(insertion_id) <=
+		    le32_to_cpu(mhdr->tx_info.pkt_id)) {
+			__mm81x_skbq_put(mq, &mq->skbq, skb, false, pfirst);
+			return;
+		}
+	}
+
+	WARN_ON_ONCE(1);
+}
+
+static void mm81x_skbq_sta_eosp(struct mm81x *mors, struct sk_buff *skb)
+{
+	struct ieee80211_tx_info *txi = IEEE80211_SKB_CB(skb);
+	struct ieee80211_vif *vif = txi->control.vif;
+
+	mm81x_skbq_pull_hdr_post_tx(skb);
+
+	/*
+	 * If this frame is the last frame in a PS-Poll or u-APSD SP,
+	 * then mac80211 must be informed that the SP is now over.
+	 */
+	if (txi->flags & IEEE80211_TX_STATUS_EOSP) {
+		struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+		struct ieee80211_sta *sta;
+
+		scoped_guard(rcu) {
+			sta = ieee80211_find_sta(vif, hdr->addr1);
+			if (sta)
+				ieee80211_sta_eosp(sta);
+		}
+	}
+}
+
+static void __mm81x_skbq_drop_pending_skb(struct mm81x_skbq *mq,
+					  struct sk_buff *skb)
+{
+	__mm81x_skbq_unlink(mq, &mq->pending, skb);
+	mm81x_skbq_sta_eosp(mq->mors, skb);
+	ieee80211_free_txskb(mq->mors->hw, skb);
+}
+
+static bool mm81x_tx_h_is_ps_filtered(struct mm81x_skbq *mq,
+				      struct sk_buff *skb,
+				      struct mm81x_skb_tx_status *tx_sts)
+{
+	struct ieee80211_tx_info *txi = IEEE80211_SKB_CB(skb);
+	struct ieee80211_vif *vif = txi->control.vif;
+
+	WARN_ON_ONCE(!(le32_to_cpu(tx_sts->flags) &
+		       MM81X_TX_STATUS_FLAGS_PS_FILTERED));
+
+	if (vif->type == NL80211_IFTYPE_AP) {
+		__mm81x_skbq_drop_pending_skb(mq, skb);
+		return true;
+	}
+
+	if (vif->type == NL80211_IFTYPE_STATION) {
+		mm81x_skbq_insert_pending(mq, skb, tx_sts->pkt_id);
+		return true;
+	}
+
+	return false;
+}
+
+/*
+ * Get a pending frame by its ID. This will also drop frames with
+ * older packet ids that are in the list
+ */
+static struct sk_buff *__mm81x_skbq_get_pending_by_id(struct mm81x *mors,
+						      struct mm81x_skbq *mq,
+						      u32 pkt_id)
+{
+	struct sk_buff *pfirst, *pnext;
+	struct sk_buff *ret = NULL;
+
+	/* Move sent packets to pending list waiting for feedback */
+	skb_queue_walk_safe(&mq->pending, pfirst, pnext) {
+		struct mm81x_skb_hdr *hdr =
+			(struct mm81x_skb_hdr *)pfirst->data;
+
+		if (le32_to_cpu(hdr->tx_info.pkt_id) == pkt_id) {
+			ret = pfirst;
+			break;
+
+		} else if (le32_to_cpu(hdr->tx_info.pkt_id) < pkt_id &&
+			   __mm81x_skbq_has_pending_tx_skb_timed_out(pfirst)) {
+			__mm81x_skbq_drop_pending_skb(mq, pfirst);
+		}
+	}
+
+	return ret;
+}
+
+static void mm81x_skbq_check_tx_empty(struct mm81x *mors, struct mm81x_skbq *mq)
+{
+	lockdep_assert_held(&mq->lock);
+
+	if (mq->flags & MM81X_HIF_FLAGS_BEACON)
+		return;
+
+	if (skb_queue_len(&mq->skbq) + skb_queue_len(&mq->pending) == 0)
+		wake_up(&mq->mors->tx_empty_waitq);
+}
+
+static void __mm81x_skbq_tx_status_process(struct mm81x *mors,
+					   struct mm81x_skbq *mq,
+					   struct mm81x_skb_tx_status *tx_sts)
+{
+	struct sk_buff *skb;
+
+	lockdep_assert_held(&mq->lock);
+
+	skb = __mm81x_skbq_get_pending_by_id(mors, mq,
+					     le32_to_cpu(tx_sts->pkt_id));
+	if (!skb) {
+		dev_dbg(mors->dev,
+			"No pending pkt match found [pktid:%d chan:%d]",
+			tx_sts->pkt_id, tx_sts->channel);
+		goto out;
+	}
+
+	if (le32_to_cpu(tx_sts->flags) & MM81X_TX_STATUS_PAGE_INVALID) {
+		__mm81x_skbq_drop_pending_skb(mq, skb);
+		goto out;
+	}
+
+	if (le32_to_cpu(tx_sts->flags) & MM81X_TX_STATUS_FLAGS_PS_FILTERED &&
+	    mm81x_tx_h_is_ps_filtered(mq, skb, tx_sts))
+		/* Has been consumed by mm81x_tx_h_is_ps_filtered */
+		goto out;
+
+	mm81x_skbq_pull_hdr_post_tx(skb);
+	mm81x_skbq_skb_finish(mq, skb, tx_sts);
+
+out:
+	mm81x_skbq_check_tx_empty(mors, mq);
+}
+
+static void mm81x_skbq_tx_status_process(struct mm81x *mors,
+					 struct sk_buff *skb)
+{
+	int i;
+	struct mm81x_skb_tx_status *tx_sts =
+		(struct mm81x_skb_tx_status *)skb->data;
+	int count = skb->len / sizeof(*tx_sts);
+
+	for (i = 0; i < count; tx_sts++, i++) {
+		struct mm81x_skbq *mq =
+			__mm81x_skbq_tx_status_to_skbq(mors, tx_sts);
+
+		if (mq) {
+			spin_lock_bh(&mq->lock);
+			__mm81x_skbq_tx_status_process(mors, mq, tx_sts);
+			spin_unlock_bh(&mq->lock);
+		}
+	}
+
+	if (mors->ps.enable && !mors->ps.suspended &&
+	    (mm81x_hif_get_tx_buffered_count(mors) == 0)) {
+		/* Evaluate ps, check if it was gated on a pending tx status */
+		queue_delayed_work(mors->chip_wq, &mors->ps.delayed_eval_work,
+				   0);
+	}
+}
+
+static void mm81x_skbq_dispatch_work(struct work_struct *dispatch_work)
+{
+	struct mm81x_skbq *mq =
+		container_of(dispatch_work, struct mm81x_skbq, dispatch_work);
+	struct mm81x *mors = mq->mors;
+	struct mm81x_skb_hdr *hdr;
+	struct sk_buff_head skbq;
+	struct sk_buff *pfirst, *pnext;
+	u8 channel;
+
+	__skb_queue_head_init(&skbq);
+
+	mm81x_skbq_deq_num_skb(mq, &skbq, mm81x_skbq_count(mq));
+
+	skb_queue_walk_safe(&skbq, pfirst, pnext) {
+		__skb_unlink(pfirst, &skbq);
+		/* Header endianness has already be adjusted */
+		hdr = (struct mm81x_skb_hdr *)pfirst->data;
+		channel = hdr->channel;
+		/* Remove mm81x header and padding */
+		__skb_pull(pfirst, sizeof(*hdr) + hdr->offset);
+
+		switch (channel) {
+		case MM81X_SKB_CHAN_COMMAND:
+			mm81x_cmd_resp_process(mors, pfirst);
+			break;
+		case MM81X_SKB_CHAN_TX_STATUS:
+			mm81x_skbq_tx_status_process(mors, pfirst);
+			dev_kfree_skb_any(pfirst);
+			break;
+		default:
+			mm81x_mac_rx_skb(mors, pfirst, &hdr->rx_status);
+			break;
+		}
+	}
+
+	if (mm81x_skbq_count(mq))
+		queue_work(mors->net_wq, &mq->dispatch_work);
+}
+
+int mm81x_skbq_put(struct mm81x_skbq *mq, struct sk_buff *skb)
+{
+	int ret;
+
+	spin_lock_bh(&mq->lock);
+	ret = __mm81x_skbq_put(mq, &mq->skbq, skb, false, NULL);
+	spin_unlock_bh(&mq->lock);
+	return ret;
+}
+
+static void mm81x_skbq_set_queued_tx_skb_expiry(struct sk_buff *skb)
+{
+	struct ieee80211_hdr *hdr = (struct ieee80211_hdr *)skb->data;
+	struct ieee80211_tx_info *txi = IEEE80211_SKB_CB(skb);
+
+	if (ieee80211_is_probe_req(hdr->frame_control) ||
+	    ieee80211_is_probe_resp(hdr->frame_control) ||
+	    ieee80211_is_auth(hdr->frame_control)) {
+		txi->control.enqueue_time = (u32)jiffies;
+	} else {
+		txi->control.enqueue_time = 0;
+	}
+}
+
+static bool mm81x_skbq_has_queued_tx_skb_expired(struct sk_buff *skb)
+{
+	struct ieee80211_tx_info *txi = IEEE80211_SKB_CB(skb);
+
+	if (txi->control.enqueue_time > 0) {
+		u32 expiry_time =
+			txi->control.enqueue_time +
+			msecs_to_jiffies(MM81X_SKBQ_TX_QUEUED_LIFETIME_MS);
+
+		return (s32)((u32)jiffies - expiry_time) > 0;
+	}
+
+	return false;
+}
+
+/*
+ * Drop selected frames (those with an expiry time set) that could not
+ * be sent within a reasonable timeframe due to congestion. These would
+ * only be rejected or ignored by the peer, so are only contributing to
+ * the problem.
+ */
+void mm81x_skbq_purge_aged(struct mm81x *mors, struct mm81x_skbq *mq)
+{
+	struct sk_buff *pfirst;
+	struct sk_buff *pnext;
+
+	spin_lock_bh(&mq->lock);
+	skb_queue_walk_safe(&mq->skbq, pfirst, pnext) {
+		if (!mm81x_skbq_has_queued_tx_skb_expired(pfirst))
+			break;
+		__mm81x_skbq_unlink(mq, &mq->skbq, pfirst);
+		ieee80211_free_txskb(mors->hw, pfirst);
+	}
+
+	spin_unlock_bh(&mq->lock);
+}
+
+void mm81x_skbq_purge(struct mm81x_skbq *mq, struct sk_buff_head *skbq)
+{
+	struct sk_buff *skb;
+
+	spin_lock_bh(&mq->lock);
+	while ((skb = __skb_dequeue(skbq)))
+		dev_kfree_skb_any(skb);
+	spin_unlock_bh(&mq->lock);
+}
+
+void mm81x_skbq_enq(struct mm81x_skbq *mq, struct sk_buff_head *skbq)
+{
+	int size;
+	struct sk_buff *pfirst, *pnext;
+
+	spin_lock_bh(&mq->lock);
+	size = __mm81x_skbq_space(mq);
+	skb_queue_walk_safe(skbq, pfirst, pnext) {
+		if (pfirst->len > size)
+			break;
+		__skb_unlink(pfirst, skbq);
+		__mm81x_skbq_put(mq, &mq->skbq, pfirst, false, NULL);
+		size -= pfirst->len;
+	}
+
+	spin_unlock_bh(&mq->lock);
+}
+
+int mm81x_skbq_deq_num_skb(struct mm81x_skbq *mq, struct sk_buff_head *skbq,
+			   int num_skb)
+{
+	int count = 0;
+	struct sk_buff *pfirst, *pnext;
+
+	spin_lock_bh(&mq->lock);
+	skb_queue_walk_safe(&mq->skbq, pfirst, pnext) {
+		if (count >= num_skb)
+			break;
+		__mm81x_skbq_unlink(mq, &mq->skbq, pfirst);
+		__skb_queue_tail(skbq, pfirst);
+		++count;
+	}
+
+	spin_unlock_bh(&mq->lock);
+	return count;
+}
+
+void mm81x_skbq_enq_prepend(struct mm81x_skbq *mq, struct sk_buff_head *skbq)
+{
+	int size;
+	struct sk_buff *pfirst, *pnext;
+
+	spin_lock_bh(&mq->lock);
+	size = __mm81x_skbq_space(mq);
+
+	/*
+	 * We are doing a reverse walk here to ensure the order remains the
+	 * same. This means the last member of the queue goes in, on top of
+	 * the queue first and gets pushed down as more members get added to
+	 * the top of the queue.
+	 */
+	skb_queue_reverse_walk_safe(skbq, pfirst, pnext) {
+		if (pfirst->len > size)
+			break;
+		__skb_unlink(pfirst, skbq);
+		__mm81x_skbq_put(mq, &mq->skbq, pfirst, true, NULL);
+		size -= pfirst->len;
+	}
+
+	spin_unlock_bh(&mq->lock);
+}
+
+static void mm81x_skbq_stop_tx_queues(struct mm81x *mors)
+{
+	int queue;
+
+	if (!mors->started)
+		return;
+	for (queue = IEEE80211_AC_VO; queue <= IEEE80211_AC_BK; queue++)
+		ieee80211_stop_queue(mors->hw, queue);
+
+	set_bit(MM81X_STATE_DATA_QS_STOPPED, &mors->state_flags);
+}
+
+/* Wake all Tx queues if all queues are below threshold */
+void mm81x_skbq_may_wake_tx_queues(struct mm81x *mors)
+{
+	int queue;
+	struct mm81x_skbq *qs;
+	int num_qs;
+	bool could_wake;
+
+	if (!mors->started)
+		return;
+
+	could_wake = true;
+	mm81x_hif_skbq_get_tx_qs(mors, &qs, &num_qs);
+	for (queue = 0; queue < num_qs; queue++) {
+		struct mm81x_skbq *mq = &qs[queue];
+
+		if (!could_wake)
+			break;
+
+		spin_lock_bh(&mq->lock);
+		could_wake &= (__mm81x_skbq_under_threshold(mq));
+		spin_unlock_bh(&mq->lock);
+	}
+
+	if (!could_wake)
+		return;
+
+	for (queue = IEEE80211_AC_VO; queue <= IEEE80211_AC_BK; queue++)
+		ieee80211_wake_queue(mors->hw, queue);
+
+	clear_bit(MM81X_STATE_DATA_QS_STOPPED, &mors->state_flags);
+}
+
+static int mm81x_skbq_tx(struct mm81x_skbq *mq, struct sk_buff *skb, u8 channel)
+{
+	int rc;
+	bool mq_over_threshold;
+	struct mm81x *mors = mq->mors;
+
+	spin_lock_bh(&mq->lock);
+	rc = __mm81x_skbq_put(mq, &mq->skbq, skb, false, NULL);
+	if (rc) {
+		dev_err(mors->dev, "skb put chan %d failed (%d)", channel, rc);
+		if (channel == MM81X_SKB_CHAN_DATA) {
+			u16 queue = skb_get_queue_mapping(skb);
+
+			dev_err(mors->dev, "skb put queue %d status %d", queue,
+				ieee80211_queue_stopped(mors->hw, queue));
+		}
+	}
+
+	/* Fill packet ID in TX info */
+	__mm81x_skbq_pkt_id(mq, skb);
+
+	mq_over_threshold = __mm81x_skbq_over_threshold(mq);
+	spin_unlock_bh(&mq->lock);
+
+	/* For data packets stop queues */
+	if (channel == MM81X_SKB_CHAN_DATA && mq_over_threshold)
+		mm81x_skbq_stop_tx_queues(mors);
+
+	switch (channel) {
+	case MM81X_SKB_CHAN_DATA:
+	case MM81X_SKB_CHAN_DATA_NOACK:
+		if (mm81x_is_data_tx_allowed(mors)) {
+			set_bit(MM81X_HIF_EVT_TX_DATA_PEND,
+				&mors->hif.event_flags);
+			queue_work(mors->chip_wq, &mors->hif_work);
+		}
+		break;
+	case MM81X_SKB_CHAN_MGMT:
+		set_bit(MM81X_HIF_EVT_TX_MGMT_PEND, &mors->hif.event_flags);
+		queue_work(mors->chip_wq, &mors->hif_work);
+		break;
+	case MM81X_SKB_CHAN_BEACON:
+		set_bit(MM81X_HIF_EVT_TX_BEACON_PEND, &mors->hif.event_flags);
+		queue_work(mors->chip_wq, &mors->hif_work);
+		break;
+	case MM81X_SKB_CHAN_COMMAND:
+		set_bit(MM81X_HIF_EVT_TX_COMMAND_PEND, &mors->hif.event_flags);
+		queue_work(mors->chip_wq, &mors->hif_work);
+		break;
+	default:
+		dev_err(mors->dev, "Invalid skb channel: %d", channel);
+		break;
+	}
+
+	return rc;
+}
+
+static void __mm81x_skbq_tx_move_to_pending(struct mm81x_skbq *mq,
+					    struct sk_buff *skb)
+{
+	struct mm81x_tx_status_priv *pend_info =
+		__mm81x_skbq_tx_status_priv(skb);
+
+	pend_info->tx_status_expiry =
+		jiffies + msecs_to_jiffies(MM81X_SKBQ_TX_STATUS_LIFETIME_MS);
+	__mm81x_skbq_put(mq, &mq->pending, skb, false, NULL);
+}
+
+void mm81x_skbq_tx_complete(struct mm81x_skbq *mq, struct sk_buff_head *skbq)
+{
+	bool skb_awaits_tx_status = false;
+	struct mm81x *mors = mq->mors;
+	struct sk_buff *pfirst, *pnext;
+	struct sk_buff *peek = skb_peek(skbq);
+	struct mm81x_skb_hdr *hdr;
+	const bool fw_reports_bcn_tx_status =
+		mors->firmware_flags &
+		MM81X_FW_FLAGS_REPORTS_TX_BEACON_COMPLETION;
+
+	if (!peek)
+		return;
+
+	/* Move sent packets to pending list waiting for feedback */
+	spin_lock_bh(&mq->lock);
+	skb_queue_walk_safe(skbq, pfirst, pnext) {
+		__skb_unlink(pfirst, skbq);
+		hdr = (struct mm81x_skb_hdr *)pfirst->data;
+		/*
+		 * If firmware doesn't give status on beacons just free
+		 * them, otherwise queue and wait for response.
+		 */
+		switch (hdr->channel) {
+		case MM81X_SKB_CHAN_BEACON:
+			if (fw_reports_bcn_tx_status) {
+				__mm81x_skbq_tx_move_to_pending(mq, pfirst);
+				skb_awaits_tx_status = true;
+				break;
+			}
+			/*
+			 * If the FW doesn't give statuses on beacon's,
+			 * then mark them as done.
+			 */
+			mm81x_skbq_pull_hdr_post_tx(pfirst);
+			dev_kfree_skb_any(pfirst);
+			break;
+		default:
+			if (le32_to_cpu(hdr->tx_info.flags) &
+			    MM81X_TX_STATUS_FLAGS_NO_REPORT) {
+				dev_kfree_skb_any(pfirst);
+			} else {
+				/*
+				 * skb has been given to the chip. Store the
+				 * time and queue the skb onto the pending
+				 * queue while we wait for the tx_status.
+				 */
+				__mm81x_skbq_tx_move_to_pending(mq, pfirst);
+				skb_awaits_tx_status = true;
+			}
+			break;
+		}
+	}
+	spin_unlock_bh(&mq->lock);
+
+	if (skb_awaits_tx_status) {
+		spin_lock_bh(&mors->stale_status.lock);
+		mod_timer(&mors->stale_status.timer,
+			  jiffies + msecs_to_jiffies(
+					    MM81X_SKBQ_TX_STATUS_LIFETIME_MS));
+		spin_unlock_bh(&mors->stale_status.lock);
+	}
+}
+
+/* Returns the first skb in the pending list. */
+struct sk_buff *mm81x_skbq_tx_pending(struct mm81x_skbq *mq)
+{
+	struct sk_buff *pfirst;
+
+	spin_lock_bh(&mq->lock);
+	pfirst = skb_peek(&mq->pending);
+	spin_unlock_bh(&mq->lock);
+	return pfirst;
+}
+
+int mm81x_skbq_check_for_stale_tx(struct mm81x *mors, struct mm81x_skbq *mq)
+{
+	int flushed = 0;
+	struct sk_buff *pfirst;
+	struct sk_buff *pnext;
+
+	if (!skb_queue_len(&mq->pending))
+		return 0;
+
+	/* Move sent packets to pending list waiting for feedback */
+	spin_lock_bh(&mq->lock);
+	skb_queue_walk_safe(&mq->pending, pfirst, pnext) {
+		struct mm81x_skb_hdr *hdr =
+			(struct mm81x_skb_hdr *)pfirst->data;
+
+		if (__mm81x_skbq_has_pending_tx_skb_timed_out(pfirst)) {
+			dev_dbg(mors->dev, "TX skb timed out [id:%d,chan:%d]",
+				hdr->tx_info.pkt_id, hdr->channel);
+
+			__mm81x_skbq_drop_pending_skb(mq, pfirst);
+			flushed++;
+		}
+	}
+
+	if (flushed)
+		mm81x_skbq_check_tx_empty(mors, mq);
+
+	spin_unlock_bh(&mq->lock);
+	return flushed;
+}
+
+/* Remove commands from pending (or skbq if not sent) */
+static void __skbq_cmd_finish(struct mm81x_skbq *mq, struct sk_buff *skb)
+{
+	struct mm81x *mors = mq->mors;
+
+	if (skb_queue_len(&mq->pending)) {
+		__mm81x_skbq_unlink(mq, &mq->pending, skb);
+		dev_kfree_skb(skb);
+	} else if (skb_queue_len(&mq->skbq)) {
+		/* Command was probably timed out before being sent */
+		dev_dbg(mors->dev,
+			"Command pending queue empty. Removing from SKBQ.");
+		__mm81x_skbq_unlink(mq, &mq->skbq, skb);
+		dev_kfree_skb(skb);
+	} else {
+		dev_dbg(mors->dev, "Command Q not found");
+	}
+}
+
+struct mm81x_update_sta_iter_data {
+	struct mm81x *mors;
+	struct sk_buff *skb;
+	struct mm81x_skb_tx_status *tx_sts;
+	int tx_attempts;
+	bool updated;
+};
+
+static void mm81x_tx_h_update_sta_iter(void *data, u8 *mac,
+				       struct ieee80211_vif *vif)
+{
+	struct mm81x_update_sta_iter_data *iter = data;
+	struct ieee80211_hdr *hdr;
+	struct ieee80211_sta *sta;
+
+	if (iter->updated || !iter->skb || !iter->skb->data)
+		return;
+
+	hdr = (struct ieee80211_hdr *)iter->skb->data;
+
+	/*
+	 * Note that each iteration via
+	 * ieee80211_iterate_active_interfaces_atomic is under an RCU critical
+	 * section so there is no need for a local critical section within here
+	 * when looking up the station.
+	 */
+	sta = ieee80211_find_sta(vif, hdr->addr1);
+	if (!sta)
+		return;
+
+	mm81x_rc_sta_feedback_rates(iter->mors, iter->skb, sta, iter->tx_sts,
+				    iter->tx_attempts);
+	mm81x_tx_h_check_aggr(sta, iter->skb);
+
+	/*
+	 * In situations with multiple virtual interfaces, finish iteration
+	 * once we have found our STA to prevent further iteration.
+	 */
+	iter->updated = true;
+}
+
+/* TX status/Response received remove packet from pending TX finish */
+static void __skbq_data_tx_finish(struct mm81x_skbq *mq, struct sk_buff *skb,
+				  struct mm81x_skb_tx_status *tx_sts)
+{
+	struct mm81x *mors = mq->mors;
+	struct mm81x_update_sta_iter_data iter = {};
+
+	__mm81x_skbq_unlink(mq, &mq->pending, skb);
+	iter.mors = mors;
+	iter.skb = skb;
+	iter.tx_sts = tx_sts;
+	iter.tx_attempts = mm81x_tx_h_get_attempts(mors, tx_sts);
+
+	ieee80211_iterate_active_interfaces_atomic(mors->hw,
+						   IEEE80211_IFACE_ITER_NORMAL,
+						   mm81x_tx_h_update_sta_iter,
+						   &iter);
+
+	ieee80211_tx_status_skb(mors->hw, skb);
+}
+
+void mm81x_skbq_skb_finish(struct mm81x_skbq *mq, struct sk_buff *skb,
+			   struct mm81x_skb_tx_status *tx_sts)
+{
+	if (mq->flags & MM81X_HIF_FLAGS_COMMAND)
+		__skbq_cmd_finish(mq, skb);
+	else
+		__skbq_data_tx_finish(mq, skb, tx_sts);
+}
+
+void mm81x_skbq_tx_flush(struct mm81x_skbq *mq)
+{
+	struct sk_buff *pfirst, *pnext;
+
+	spin_lock_bh(&mq->lock);
+	skb_queue_walk_safe(&mq->pending, pfirst, pnext) {
+		__mm81x_skbq_unlink(mq, &mq->pending, pfirst);
+		ieee80211_free_txskb(mq->mors->hw, pfirst);
+	}
+
+	skb_queue_walk_safe(&mq->skbq, pfirst, pnext) {
+		__mm81x_skbq_unlink(mq, &mq->skbq, pfirst);
+		ieee80211_free_txskb(mq->mors->hw, pfirst);
+	}
+	spin_unlock_bh(&mq->lock);
+}
+
+void mm81x_skbq_init(struct mm81x *mors, struct mm81x_skbq *mq, u16 flags)
+{
+	spin_lock_init(&mq->lock);
+	__skb_queue_head_init(&mq->skbq);
+	__skb_queue_head_init(&mq->pending);
+	mq->mors = mors;
+	mq->skbq_size = 0;
+	mq->flags = flags;
+	mq->pkt_seq = 0;
+	if (flags & MM81X_HIF_FLAGS_DIR_TO_HOST)
+		INIT_WORK(&mq->dispatch_work, mm81x_skbq_dispatch_work);
+}
+
+void mm81x_skbq_finish(struct mm81x_skbq *mq)
+{
+	if (mq->skbq_size > 0)
+		dev_dbg(mq->mors->dev,
+			"Purging a non empty MorseQ. Dropping data!");
+
+	/* Clean up link to hif */
+	if (mq->flags & MM81X_HIF_FLAGS_DIR_TO_HOST)
+		cancel_work_sync(&mq->dispatch_work);
+	mm81x_skbq_purge(mq, &mq->skbq);
+	mm81x_skbq_purge(mq, &mq->pending);
+	mq->skbq_size = 0;
+}
+
+u32 mm81x_skbq_size(struct mm81x_skbq *mq)
+{
+	u32 count;
+
+	spin_lock_bh(&mq->lock);
+	count = __mm81x_skbq_size(mq);
+	spin_unlock_bh(&mq->lock);
+	return count;
+}
+
+u32 mm81x_skbq_count(struct mm81x_skbq *mq)
+{
+	u32 count = 0;
+
+	spin_lock_bh(&mq->lock);
+	count += skb_queue_len(&mq->skbq);
+	spin_unlock_bh(&mq->lock);
+	return count;
+}
+
+u32 mm81x_skbq_pending_count(struct mm81x_skbq *mq)
+{
+	u32 count;
+
+	spin_lock_bh(&mq->lock);
+	count = skb_queue_len(&mq->pending);
+	spin_unlock_bh(&mq->lock);
+	return count;
+}
+
+u32 mm81x_skbq_count_tx_ready(struct mm81x_skbq *mq)
+{
+	struct mm81x *mors = mq->mors;
+
+	if (!mm81x_is_data_tx_allowed(mors))
+		return 0;
+
+	return mm81x_skbq_count(mq);
+}
+
+u32 mm81x_skbq_space(struct mm81x_skbq *mq)
+{
+	u32 space;
+
+	spin_lock_bh(&mq->lock);
+	space = __mm81x_skbq_space(mq);
+	spin_unlock_bh(&mq->lock);
+
+	return space;
+}
+
+struct sk_buff *mm81x_skbq_alloc_skb(struct mm81x_skbq *mq, unsigned int length)
+{
+	struct sk_buff *skb;
+	int tx_headroom = sizeof(struct mm81x_skb_hdr) +
+			  mm81x_bus_get_alignment(mq->mors);
+	int skb_len = tx_headroom + length + MM81X_PAD4(length);
+
+	skb = dev_alloc_skb(skb_len);
+	if (!skb)
+		return NULL;
+
+	skb_reserve(skb, tx_headroom);
+	skb_put(skb, length);
+	return skb;
+}
+
+static int mm81x_skb_tx_h_validate_channel(const struct mm81x *mors, u8 channel)
+{
+	if (channel == MM81X_SKB_CHAN_COMMAND) {
+		if (test_bit(MM81X_STATE_HOST_TO_CHIP_CMD_BLOCKED,
+			     &mors->state_flags))
+			return -EPERM;
+	} else {
+		if (test_bit(MM81X_STATE_HOST_TO_CHIP_TX_BLOCKED,
+			     &mors->state_flags))
+			return -EPERM;
+	}
+
+	return 0;
+}
+
+int mm81x_skbq_skb_tx(struct mm81x_skbq *mq, struct sk_buff **skb_orig,
+		      struct mm81x_skb_tx_info *tx_info, u8 channel)
+{
+	int ret;
+	struct mm81x_skb_hdr hdr;
+	struct mm81x *mors = mq->mors;
+	size_t end_of_skb_pad;
+	struct sk_buff *skb = *skb_orig;
+	u8 *aligned_head, *data;
+
+	if (test_bit(MM81X_STATE_CHIP_UNRESPONSIVE, &mors->state_flags)) {
+		dev_kfree_skb_any(skb);
+		return -ENODEV;
+	}
+
+	ret = mm81x_skb_tx_h_validate_channel(mors, channel);
+	if (ret) {
+		dev_kfree_skb_any(skb);
+		return ret;
+	}
+
+	mm81x_skbq_set_queued_tx_skb_expiry(skb);
+
+	data = skb->data;
+	aligned_head = PTR_ALIGN_DOWN((data - sizeof(hdr)),
+				      mm81x_bus_get_alignment(mors));
+	hdr.sync = MM81X_SKB_HEADER_SYNC;
+	hdr.channel = channel;
+	hdr.len = cpu_to_le16(skb->len);
+	hdr.offset = data - (aligned_head + sizeof(hdr));
+	hdr.checksum_upper = 0;
+	hdr.checksum_lower = 0;
+	if (tx_info)
+		memcpy(&hdr.tx_info, tx_info, sizeof(*tx_info));
+	else
+		memset(&hdr.tx_info, 0, sizeof(hdr.tx_info));
+
+	skb_push(skb, data - aligned_head);
+	memcpy(skb->data, &hdr, sizeof(hdr));
+
+	end_of_skb_pad = MM81X_PAD4(skb->len);
+	if (end_of_skb_pad && skb_pad(skb, end_of_skb_pad))
+		return -EINVAL;
+
+	ret = mm81x_skbq_tx(mq, skb, channel);
+	if (ret) {
+		dev_err(mors->dev, "mm81x_skbq_tx fail: %d", ret);
+		dev_kfree_skb_any(skb);
+	}
+
+	return ret;
+}
+
+void mm81x_skbq_data_traffic_pause(struct mm81x *mors)
+{
+	set_bit(MM81X_STATE_DATA_TX_STOPPED, &mors->state_flags);
+	/* power-save requirements will be re-evaluated by the caller */
+}
+
+void mm81x_skbq_data_traffic_resume(struct mm81x *mors)
+{
+	clear_bit(MM81X_STATE_DATA_TX_STOPPED, &mors->state_flags);
+
+	/* Set the TX_DATA_PEND bit. This will kick the transmission path to
+	 * send any frames pending in the TX buffers, and wake the mac80211
+	 * data Qs if they were previously stopped.
+	 */
+	set_bit(MM81X_HIF_EVT_TX_DATA_PEND, &mors->hif.event_flags);
+}
+
+bool mm81x_skbq_validate_checksum(u8 *data)
+{
+	int i;
+	u32 xor = 0;
+	struct mm81x_skb_hdr *skb_hdr = (struct mm81x_skb_hdr *)data;
+	struct ieee80211_hdr *hdr =
+		(struct ieee80211_hdr *)(data + sizeof(*skb_hdr));
+	u16 len = le16_to_cpu(skb_hdr->len) + sizeof(*skb_hdr);
+	u32 *data_to_xor = (u32 *)data;
+	u32 header_xor = (le16_to_cpu(skb_hdr->checksum_upper) << 8) |
+			 (skb_hdr->checksum_lower);
+
+	/*
+	 * For data frames the calculate the xor for skb header, mac header
+	 * and ccmp header. For all other channel the xor is calculated for
+	 * the full skb.
+	 */
+	if (skb_hdr->channel == MM81X_SKB_CHAN_DATA &&
+	    (ieee80211_is_data(hdr->frame_control) ||
+	     ieee80211_is_data_qos(hdr->frame_control))) {
+		u16 data_len = sizeof(*skb_hdr) +
+			       sizeof(struct ieee80211_qos_hdr) +
+			       IEEE80211_CCMP_HDR_LEN;
+
+		len = min(len, data_len);
+		len = ROUND_DOWN_TO_WORD(len);
+	}
+
+	skb_hdr->checksum_upper = 0;
+	skb_hdr->checksum_lower = 0;
+
+	for (i = 0; i < len; i += 4) {
+		xor ^= *data_to_xor;
+		data_to_xor++;
+	}
+
+	xor &= 0x00FFFFFF;
+
+	return xor == header_xor;
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 26/33] wifi: mm81x: add skbq.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/skbq.h | 218 +++++++++++++++++++
 1 file changed, 218 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/skbq.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/skbq.h b/drivers/net/wireless/morsemicro/mm81x/skbq.h
new file mode 100644
index 000000000000..9930493141cf
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/skbq.h
@@ -0,0 +1,218 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_SKBQ_H_
+#define _MM81X_SKBQ_H_
+
+#include <linux/skbuff.h>
+#include <linux/workqueue.h>
+#include "rate_code.h"
+
+/* Sync value of skb header to indicate a valid skb */
+#define MM81X_SKB_HEADER_SYNC (0xAA)
+/* Sync value indicating that the chip owns this skb */
+#define MM81X_SKB_HEADER_CHIP_OWNED_SYNC (0xBB)
+
+enum mm81x_tx_status_and_conf_flags {
+	MM81X_TX_STATUS_FLAGS_NO_ACK = BIT(0),
+	MM81X_TX_STATUS_FLAGS_NO_REPORT = BIT(1),
+	MM81X_TX_CONF_FLAGS_CTL_AMPDU = BIT(2),
+	MM81X_TX_CONF_FLAGS_HW_ENCRYPT = BIT(3),
+	MM81X_TX_CONF_FLAGS_VIF_ID = (BIT(4) | BIT(5) | BIT(6) | BIT(7) |
+				      BIT(8) | BIT(9) | BIT(10) | BIT(11)),
+	MM81X_TX_CONF_FLAGS_KEY_IDX = (BIT(12) | BIT(13) | BIT(14)),
+	MM81X_TX_STATUS_FLAGS_PS_FILTERED = (BIT(15)),
+	MM81X_TX_CONF_IGNORE_TWT = (BIT(16)),
+	MM81X_TX_STATUS_PAGE_INVALID = (BIT(17)),
+	MM81X_TX_CONF_NO_PS_BUFFER = (BIT(18)),
+	MM81X_TX_STATUS_DUTY_CYCLE_CANT_SEND = (BIT(19)),
+	MM81X_TX_CONF_HAS_PV1_BPN_IN_BODY = (BIT(21)),
+	MM81X_TX_CONF_FLAGS_SEND_AFTER_DTIM = (BIT(22)),
+	MM81X_TX_STATUS_WAS_AGGREGATED = (BIT(23)),
+	MM81X_TX_CONF_FLAGS_FULLMAC_REPORT = BIT(24),
+	MM81X_TX_CONF_FLAGS_IMMEDIATE_REPORT = (BIT(31))
+};
+
+/* Getter and setter macros for vif id */
+#define MM81X_TX_CONF_FLAGS_VIF_ID_MASK (0xFF)
+#define MM81X_TX_CONF_FLAGS_VIF_ID_SET(x) \
+	(((x) & MM81X_TX_CONF_FLAGS_VIF_ID_MASK) << 4)
+#define MM81X_TX_CONF_FLAGS_VIF_ID_GET(x) \
+	(((x) & MM81X_TX_CONF_FLAGS_VIF_ID) >> 4)
+
+/* Getter and setter macros for key index */
+#define MM81X_TX_CONF_FLAGS_KEY_IDX_SET(x) (((x) & 0x07) << 12)
+#define MM81X_TX_CONF_FLAGS_KEY_IDX_GET(x) \
+	(((x) & MM81X_TX_CONF_FLAGS_KEY_IDX) >> 12)
+
+enum mm81x_rx_status_flags {
+	MM81X_RX_STATUS_FLAGS_ERROR = BIT(0),
+	MM81X_RX_STATUS_FLAGS_DECRYPTED = BIT(1),
+	MM81X_RX_STATUS_FLAGS_FCS_INCLUDED = BIT(2),
+	MM81X_RX_STATUS_FLAGS_EOF = BIT(3),
+	MM81X_RX_STATUS_FLAGS_AMPDU = BIT(4),
+	MM81X_RX_STATUS_FLAGS_NDP = BIT(7),
+	MM81X_RX_STATUS_FLAGS_UPLINK = BIT(8),
+	MM81X_RX_STATUS_FLAGS_RI = (BIT(9) | BIT(10)),
+	MM81X_RX_STATUS_FLAGS_NDP_TYPE = (BIT(11) | BIT(12) | BIT(13)),
+	MM81X_RX_STATUS_FLAGS_CRC_ERROR = BIT(14),
+	MM81X_RX_STATUS_FLAGS_VIF_ID = GENMASK(24, 17),
+};
+
+/* Getter and Setter macros for vif id */
+#define MM81X_RX_STATUS_FLAGS_VIF_ID_MASK (0xFF)
+#define MM81X_RX_STATUS_FLAGS_VIF_ID_SET(x) \
+	(((x) & MM81X_RX_STATUS_FLAGS_VIF_ID_MASK) << 17)
+#define MM81X_RX_STATUS_FLAGS_VIF_ID_GET(x) \
+	(((x) & MM81X_RX_STATUS_FLAGS_VIF_ID) >> 17)
+#define MM81X_RX_STATUS_FLAGS_VIF_ID_CLEAR(x) \
+	((x) & ~(MM81X_RX_STATUS_FLAGS_VIF_ID_MASK << 17))
+
+/* Getter macro for guard interval */
+#define MM81X_RX_STATUS_FLAGS_UPL_IND_GET(x) \
+	(((x) & MM81X_RX_STATUS_FLAGS_UPLINK) >> 8)
+
+/* Getter macro for response indication */
+#define MM81X_RX_STATUS_FLAGS_RI_GET(x) (((x) & MM81X_RX_STATUS_FLAGS_RI) >> 9)
+
+/* Getter macro for NDP type */
+#define MM81X_RX_STATUS_FLAGS_NDP_TYPE_GET(x) \
+	(((x) & MM81X_RX_STATUS_FLAGS_NDP_TYPE) >> 11)
+
+enum mm81x_skb_channel {
+	MM81X_SKB_CHAN_DATA = 0x0,
+	MM81X_SKB_CHAN_NDP_FRAMES = 0x1,
+	MM81X_SKB_CHAN_DATA_NOACK = 0x2,
+	MM81X_SKB_CHAN_BEACON = 0x3,
+	MM81X_SKB_CHAN_MGMT = 0x4,
+	MM81X_SKB_CHAN_INTERNAL_CRIT_BEACON = 0x80,
+	MM81X_SKB_CHAN_COMMAND = 0xFE,
+	MM81X_SKB_CHAN_TX_STATUS = 0xFF
+};
+
+#define MM81X_SKB_MAX_RATES (4)
+
+struct mm81x_skb_rate_info {
+	mm81x_rate_code_t mm81x_ratecode;
+	u8 count;
+} __packed;
+
+struct mm81x_skb_tx_status {
+	__le32 flags;
+	__le32 pkt_id;
+	u8 tid;
+	u8 channel;
+	__le16 ampdu_info;
+	struct mm81x_skb_rate_info rates[MM81X_SKB_MAX_RATES];
+} __packed;
+
+#define MM81X_TXSTS_AMPDU_INFO_GET_TAG(x) (((x) >> 10) & 0x3F)
+#define MM81X_TXSTS_AMPDU_INFO_GET_LEN(x) (((x) >> 5) & 0x1F)
+#define MM81X_TXSTS_AMPDU_INFO_GET_SUC(x) ((x) & 0x1F)
+
+struct mm81x_skb_tx_info {
+	__le32 flags;
+	__le32 pkt_id;
+	u8 tid;
+	u8 tid_params;
+	u8 mmss_params;
+	u8 padding[1];
+	struct mm81x_skb_rate_info rates[MM81X_SKB_MAX_RATES];
+} __packed;
+
+#define TX_INFO_TID_PARAMS_MAX_REORDER_BUF 0x1f
+#define TX_INFO_TID_PARAMS_AMPDU_ENABLED 0x20
+#define TX_INFO_TID_PARAMS_AMSDU_SUPPORTED 0x40
+#define TX_INFO_TID_PARAMS_USE_LEGACY_BA 0x80
+
+/* Bitmap for MMSS (Minimum MPDU start spacing) parameters
+ * +-----------+-----------+
+ * | Morse     | MMSS set  |
+ * | MMSS      | by S1G cap|
+ * | offset    | IE        |
+ * |-----------|-----------|
+ * |b7|b6|b5|b4|b3|b2|b1|b0|
+ */
+#define TX_INFO_MMSS_PARAMS_MMSS_MASK GENMASK(3, 0)
+#define TX_INFO_MMSS_PARAMS_MMSS_OFFSET_START 4
+#define TX_INFO_MMSS_PARAMS_MMSS_OFFSET_MASK GENMASK(7, 4)
+#define TX_INFO_MMSS_PARAMS_SET_MMSS(x) ((x) & TX_INFO_MMSS_PARAMS_MMSS_MASK)
+#define TX_INFO_MMSS_PARAMS_SET_MMSS_OFFSET(x)            \
+	(((x) << TX_INFO_MMSS_PARAMS_MMSS_OFFSET_START) & \
+	 TX_INFO_MMSS_PARAMS_MMSS_OFFSET_MASK)
+
+struct mm81x_skb_rx_status {
+	__le32 flags;
+	mm81x_rate_code_t mm81x_ratecode;
+	__le16 rssi;
+	__le16 freq_100khz;
+	u8 bss_color;
+	s8 noise_dbm;
+	/** Padding for word alignment */
+	u8 padding[2];
+	__le64 rx_timestamp_us;
+} __packed;
+
+struct mm81x_skb_hdr {
+	u8 sync;
+	u8 channel;
+	__le16 len;
+	u8 offset;
+	u8 checksum_lower;
+	__le16 checksum_upper;
+	union {
+		struct mm81x_skb_tx_info tx_info;
+		struct mm81x_skb_tx_status tx_status;
+		struct mm81x_skb_rx_status rx_status;
+	};
+} __packed;
+
+#define MM81X_SKBQ_SIZE (4 * 128 * 1024)
+
+struct mm81x;
+
+struct mm81x_skbq {
+	struct mm81x *mors;
+	u32 pkt_seq; /* SKB sequence used in tx_status */
+	u16 flags;
+	u32 skbq_size; /* current off loaded size */
+	spinlock_t lock;
+	struct sk_buff_head skbq;
+	struct sk_buff_head pending; /* packets sent pending feedback */
+	struct work_struct dispatch_work;
+};
+
+void mm81x_skbq_purge(struct mm81x_skbq *mq, struct sk_buff_head *skbq);
+void mm81x_skbq_purge_aged(struct mm81x *mors, struct mm81x_skbq *mq);
+u32 mm81x_skbq_space(struct mm81x_skbq *mq);
+u32 mm81x_skbq_size(struct mm81x_skbq *mq);
+int mm81x_skbq_deq_num_skb(struct mm81x_skbq *mq, struct sk_buff_head *skbq,
+			   int num_skb);
+struct sk_buff *mm81x_skbq_alloc_skb(struct mm81x_skbq *mq,
+				     unsigned int length);
+int mm81x_skbq_skb_tx(struct mm81x_skbq *mq, struct sk_buff **skb,
+		      struct mm81x_skb_tx_info *tx_info, u8 channel);
+int mm81x_skbq_put(struct mm81x_skbq *mq, struct sk_buff *skb);
+void mm81x_skbq_enq(struct mm81x_skbq *mq, struct sk_buff_head *skbq);
+void mm81x_skbq_enq_prepend(struct mm81x_skbq *mq, struct sk_buff_head *skbq);
+void mm81x_skbq_tx_complete(struct mm81x_skbq *mq, struct sk_buff_head *skbq);
+struct sk_buff *mm81x_skbq_tx_pending(struct mm81x_skbq *mq);
+void mm81x_skbq_init(struct mm81x *mors, struct mm81x_skbq *mq, u16 flags);
+void mm81x_skbq_finish(struct mm81x_skbq *mq);
+void mm81x_skbq_pull_hdr_post_tx(struct sk_buff *skb);
+void mm81x_skbq_mon_dump(struct mm81x *mors, struct seq_file *file);
+void mm81x_skbq_skb_finish(struct mm81x_skbq *mq, struct sk_buff *skb,
+			   struct mm81x_skb_tx_status *tx_sts);
+void mm81x_skbq_tx_flush(struct mm81x_skbq *mq);
+int mm81x_skbq_check_for_stale_tx(struct mm81x *mors, struct mm81x_skbq *mq);
+void mm81x_skbq_may_wake_tx_queues(struct mm81x *mors);
+u32 mm81x_skbq_count_tx_ready(struct mm81x_skbq *mq);
+u32 mm81x_skbq_count(struct mm81x_skbq *mq);
+u32 mm81x_skbq_pending_count(struct mm81x_skbq *mq);
+void mm81x_skbq_data_traffic_pause(struct mm81x *mors);
+void mm81x_skbq_data_traffic_resume(struct mm81x *mors);
+bool mm81x_skbq_validate_checksum(u8 *data);
+
+#endif /* !_MM81X_SKBQ_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 27/33] wifi: mm81x: add usb.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/usb.c | 943 ++++++++++++++++++++
 1 file changed, 943 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/usb.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/usb.c b/drivers/net/wireless/morsemicro/mm81x/usb.c
new file mode 100644
index 000000000000..ec94936b157b
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/usb.c
@@ -0,0 +1,943 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/jiffies.h>
+#include <linux/module.h>
+#include <linux/usb.h>
+#include "hif.h"
+#include "bus.h"
+#include "mac.h"
+#include "core.h"
+
+/*
+ * URB timeout in milliseconds. If an URB does not complete within this
+ * time, it will be killed. This timeout needs to account for USB suspendand
+ * resume occurring before the URB can be transferred, and it also needs to
+ * account for transferring USB_MAX_TRANSFER_SIZE bytes over a potentially
+ * slow, congested USB Full Speed link.
+ */
+#define URB_TIMEOUT_MS 250
+
+/* High speed USB 2^(4-1) * 125usec = 1msec */
+#define MM81X_USB_INTERRUPT_INTERVAL 4
+
+/* Max bytes per USB read/write */
+#define USB_MAX_TRANSFER_SIZE (16 * 1024)
+
+/* INT EP buffer size */
+#define MM81X_EP_INT_BUFFER_SIZE 8
+
+/* Morse vendor IDs*/
+#define MM81X_VENDOR_ID 0x325b
+#define MM81X_MM810X_PRODUCT_ID 0x8100
+
+/* Power management runtime auto-suspend delay value in milliseconds */
+#define PM_RUNTIME_AUTOSUSPEND_DELAY_MS 100
+
+enum mm81x_usb_endpoints {
+	MM81X_EP_CMD = 0,
+	MM81X_EP_INT,
+	MM81X_EP_MEM_RD,
+	MM81X_EP_MEM_WR,
+	MM81X_EP_REG_RD,
+	MM81X_EP_REG_WR,
+	MM81X_EP_EP_MAX,
+};
+
+struct mm81x_usb_endpoint {
+	unsigned char *buffer;
+	struct urb *urb;
+	__u8 addr;
+	int size;
+};
+
+enum mm81x_usb_flags { MM81X_USB_FLAG_ATTACHED, MM81X_USB_FLAG_SUSPENDED };
+
+struct mm81x_usb {
+	struct usb_device *udev;
+	struct usb_interface *interface;
+	struct mm81x_usb_endpoint endpoints[MM81X_EP_EP_MAX];
+	int errors;
+
+	/* serialise USB device struct */
+	struct mutex lock;
+
+	/* serialise USB bus access */
+	struct mutex bus_lock;
+
+	bool ongoing_cmd;
+	bool ongoing_rw;
+	wait_queue_head_t rw_in_wait;
+	unsigned long flags;
+};
+
+enum mm81x_usb_command_direction {
+	MM81X_USB_WRITE = 0x00,
+	MM81X_USB_READ = 0x80,
+	MM81X_USB_RESET = 0x02,
+};
+
+struct mm81x_usb_command {
+	__le32 dir; /* Next BULK direction */
+	__le32 address; /* Next BULK address */
+	__le32 length; /* Next BULK size */
+};
+
+static const struct usb_device_id mm81x_usb_table[] = {
+	{ USB_DEVICE(MM81X_VENDOR_ID, MM81X_MM810X_PRODUCT_ID) },
+	{} /* Terminating entry */
+};
+
+MODULE_DEVICE_TABLE(usb, mm81x_usb_table);
+
+static void mm81x_usb_irq_work(struct work_struct *work)
+{
+	struct mm81x *mors = container_of(work, struct mm81x, usb_irq_work);
+
+	mm81x_claim_bus(mors);
+	mm81x_hw_irq_handle(mors);
+	mm81x_release_bus(mors);
+}
+
+static bool mm81x_usb_urb_status_is_disconnect(const struct urb *urb)
+{
+	return ((urb->status == -EPROTO) || (urb->status == -EILSEQ) ||
+		(urb->status == -ETIME) || (urb->status == -EPIPE));
+}
+
+static void mm81x_usb_int_handler(struct urb *urb)
+{
+	int ret;
+	struct mm81x *mors = urb->context;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	if (!test_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags))
+		return;
+
+	if (urb->status) {
+		if (mm81x_usb_urb_status_is_disconnect(urb)) {
+			clear_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags);
+			set_bit(MM81X_STATE_CHIP_UNRESPONSIVE,
+				&mors->state_flags);
+			dev_dbg(mors->dev,
+				"USB sudden disconnect detected in %s",
+				__func__);
+			return;
+		}
+
+		if (!(urb->status == -ENOENT || urb->status == -ECONNRESET ||
+		      urb->status == -ESHUTDOWN))
+			dev_err(mors->dev, "- nonzero read status received: %d",
+				urb->status);
+	}
+
+	ret = usb_submit_urb(urb, GFP_ATOMIC);
+
+	/* usb_kill_urb has been called */
+	if (ret == -EPERM)
+		return;
+	else if (ret)
+		dev_err(mors->dev, "error: resubmit urb %p err code %d", urb,
+			ret);
+
+	queue_work(mors->chip_wq, &mors->usb_irq_work);
+}
+
+static int mm81x_usb_int_enable(struct mm81x *mors)
+{
+	int ret = 0;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+	struct urb *urb;
+
+	if (!test_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags))
+		return -ENODEV;
+
+	urb = usb_alloc_urb(0, GFP_KERNEL);
+	if (!urb) {
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	musb->endpoints[MM81X_EP_INT].urb = urb;
+
+	musb->endpoints[MM81X_EP_INT].buffer =
+		usb_alloc_coherent(musb->udev, MM81X_EP_INT_BUFFER_SIZE,
+				   GFP_KERNEL, &urb->transfer_dma);
+	if (!musb->endpoints[MM81X_EP_INT].buffer) {
+		dev_err(mors->dev, "couldn't allocate transfer_buffer");
+		ret = -ENOMEM;
+		goto error_set_urb_null;
+	}
+
+	usb_fill_int_urb(
+		musb->endpoints[MM81X_EP_INT].urb, musb->udev,
+		usb_rcvintpipe(musb->udev, musb->endpoints[MM81X_EP_INT].addr),
+		musb->endpoints[MM81X_EP_INT].buffer, MM81X_EP_INT_BUFFER_SIZE,
+		mm81x_usb_int_handler, mors, MM81X_USB_INTERRUPT_INTERVAL);
+	urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+	ret = usb_submit_urb(urb, GFP_KERNEL);
+	if (ret) {
+		dev_err(mors->dev, "Couldn't submit urb. Error number %d", ret);
+		goto error;
+	}
+
+	return 0;
+
+error:
+	usb_free_coherent(musb->udev, MM81X_EP_INT_BUFFER_SIZE,
+			  musb->endpoints[MM81X_EP_INT].buffer,
+			  urb->transfer_dma);
+error_set_urb_null:
+	musb->endpoints[MM81X_EP_INT].urb = NULL;
+	usb_free_urb(urb);
+out:
+	return ret;
+}
+
+static void mm81x_usb_int_stop(struct mm81x *mors)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	usb_kill_urb(musb->endpoints[MM81X_EP_INT].urb);
+	cancel_work_sync(&mors->usb_irq_work);
+}
+
+static void mm81x_usb_cmd_callback(struct urb *urb)
+{
+	struct mm81x *mors = urb->context;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	/* sync/async unlink faults aren't errors */
+	if (urb->status) {
+		if (!(urb->status == -ENOENT || urb->status == -ECONNRESET ||
+		      urb->status == -ESHUTDOWN))
+			dev_err(mors->dev,
+				"nonzero write bulk status received: %d",
+				urb->status);
+
+		musb->errors = urb->status;
+	}
+
+	musb->ongoing_cmd = false;
+	wake_up(&musb->rw_in_wait);
+}
+
+static int mm81x_usb_cmd(struct mm81x_usb *musb,
+			 const struct mm81x_usb_command *cmd)
+{
+	int retval = 0;
+	struct mm81x *mors = usb_get_intfdata(musb->interface);
+	struct mm81x_usb_endpoint *ep = &musb->endpoints[MM81X_EP_CMD];
+	size_t writesize = sizeof(*cmd);
+
+	if (!test_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags))
+		return -ENODEV;
+
+	memcpy(ep->buffer, cmd, writesize);
+
+	usb_fill_bulk_urb(ep->urb, musb->udev,
+			  usb_sndbulkpipe(musb->udev, ep->addr), ep->buffer,
+			  writesize, mm81x_usb_cmd_callback, mors);
+	ep->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+	musb->ongoing_cmd = true;
+
+	retval = usb_submit_urb(ep->urb, GFP_KERNEL);
+	if (retval) {
+		dev_err(mors->dev, "- failed submitting write urb, error %d",
+			retval);
+
+		goto error;
+	}
+
+	retval = wait_event_interruptible_timeout(
+		musb->rw_in_wait, (!musb->ongoing_cmd),
+		msecs_to_jiffies(URB_TIMEOUT_MS));
+	if (retval < 0) {
+		dev_err(mors->dev, "error waiting for urb %d", retval);
+		goto error;
+	} else if (retval == 0) {
+		dev_err(mors->dev, "timed out waiting for urb");
+		usb_kill_urb(ep->urb);
+		retval = -ETIMEDOUT;
+		goto error;
+	}
+
+	musb->ongoing_cmd = false;
+	return writesize;
+
+error:
+	musb->ongoing_cmd = false;
+	return retval;
+}
+
+static int mm81x_usb_ndr_reset(struct mm81x *mors)
+{
+	int ret;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+	struct mm81x_usb_command cmd;
+
+	mutex_lock(&musb->lock);
+
+	musb->ongoing_rw = true;
+	musb->errors = 0;
+
+	cmd.dir = cpu_to_le32(MM81X_USB_RESET);
+	cmd.address = cpu_to_le32(0);
+	cmd.length = cpu_to_le32(0);
+
+	ret = mm81x_usb_cmd(musb, &cmd);
+	if (ret < 0)
+		dev_err(mors->dev, "mm81x_usb_cmd (MM81X_USB_RESET) error %d\n",
+			ret);
+	else
+		ret = 0;
+
+	musb->ongoing_rw = false;
+	mutex_unlock(&musb->lock);
+	return ret;
+}
+
+static void mm81x_usb_mem_rw_callback(struct urb *urb)
+{
+	struct mm81x *mors = urb->context;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	/* sync/async unlink faults aren't errors */
+	if (urb->status) {
+		if (!(urb->status == -ENOENT || urb->status == -ECONNRESET ||
+		      urb->status == -ESHUTDOWN))
+			dev_err(mors->dev,
+				"nonzero write bulk status received: %d",
+				urb->status);
+
+		musb->errors = urb->status;
+	}
+
+	musb->ongoing_rw = false;
+	wake_up(&musb->rw_in_wait);
+}
+
+static int mm81x_usb_mem_read(struct mm81x_usb *musb, u32 address, u8 *data,
+			      ssize_t size)
+{
+	int ret;
+	struct mm81x_usb_command cmd;
+	struct mm81x *mors = usb_get_intfdata(musb->interface);
+
+	if (!test_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags))
+		return -ENODEV;
+
+	mutex_lock(&musb->lock);
+
+	musb->ongoing_rw = true;
+	musb->errors = 0;
+
+	/* Send command ahead to prepare for Tokens */
+	cmd.dir = cpu_to_le32(MM81X_USB_READ);
+	cmd.address = cpu_to_le32(address);
+	cmd.length = cpu_to_le32(size);
+
+	ret = mm81x_usb_cmd(musb, &cmd);
+	if (ret < 0) {
+		dev_err(mors->dev, "mm81x_usb_cmd error %d", ret);
+		goto error;
+	}
+
+	/* Let's be fast push the next URB, don't wait until command is done */
+	usb_fill_bulk_urb(
+		musb->endpoints[MM81X_EP_MEM_RD].urb, musb->udev,
+		usb_rcvbulkpipe(musb->udev,
+				musb->endpoints[MM81X_EP_MEM_RD].addr),
+		musb->endpoints[MM81X_EP_MEM_RD].buffer, size,
+		mm81x_usb_mem_rw_callback, mors);
+
+	ret = usb_submit_urb(musb->endpoints[MM81X_EP_MEM_RD].urb, GFP_ATOMIC);
+	if (ret < 0) {
+		dev_err(mors->dev, "failed submitting read urb, error %d", ret);
+		ret = (ret == -ENOMEM) ? ret : -EIO;
+		goto error;
+	}
+
+	ret = wait_event_interruptible_timeout(
+		musb->rw_in_wait, (!musb->ongoing_rw),
+		msecs_to_jiffies(URB_TIMEOUT_MS));
+	if (ret < 0) {
+		dev_err(mors->dev, "wait_event_interruptible: error %d", ret);
+		goto error;
+	} else if (ret == 0) {
+		/* Timed out. */
+		usb_kill_urb(musb->endpoints[MM81X_EP_MEM_RD].urb);
+	}
+
+	if (musb->errors) {
+		ret = musb->errors;
+		dev_err(mors->dev, "mem read error %d", ret);
+		goto error;
+	}
+
+	memcpy(data, musb->endpoints[MM81X_EP_MEM_RD].buffer, size);
+	ret = size;
+
+error:
+	musb->ongoing_rw = false;
+	mutex_unlock(&musb->lock);
+
+	return ret;
+}
+
+static int mm81x_usb_mem_write(struct mm81x_usb *musb, u32 address, u8 *data,
+			       ssize_t size)
+{
+	int ret;
+	struct mm81x_usb_command cmd;
+	struct mm81x *mors = usb_get_intfdata(musb->interface);
+
+	if (!test_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags))
+		return -ENODEV;
+
+	mutex_lock(&musb->lock);
+
+	musb->ongoing_rw = true;
+	musb->errors = 0;
+
+	/* Send command ahead to prepare for Tokens */
+	cmd.dir = cpu_to_le32(MM81X_USB_WRITE);
+	cmd.address = cpu_to_le32(address);
+	cmd.length = cpu_to_le32(size);
+	ret = mm81x_usb_cmd(musb, &cmd);
+	if (ret < 0) {
+		dev_err(mors->dev, "mm81x_usb_mem_read error %d", ret);
+		goto error;
+	}
+
+	memcpy(musb->endpoints[MM81X_EP_MEM_WR].buffer, data, size);
+
+	/* prepare a read */
+	usb_fill_bulk_urb(
+		musb->endpoints[MM81X_EP_MEM_WR].urb, musb->udev,
+		usb_sndbulkpipe(musb->udev,
+				musb->endpoints[MM81X_EP_MEM_WR].addr),
+		musb->endpoints[MM81X_EP_MEM_WR].buffer, size,
+		mm81x_usb_mem_rw_callback, mors);
+
+	ret = usb_submit_urb(musb->endpoints[MM81X_EP_MEM_WR].urb, GFP_ATOMIC);
+	if (ret < 0) {
+		dev_err(mors->dev, "- failed submitting write urb, error %d",
+			ret);
+		ret = (ret == -ENOMEM) ? ret : -EIO;
+		goto error;
+	}
+
+	ret = wait_event_interruptible_timeout(
+		musb->rw_in_wait, (!musb->ongoing_rw),
+		msecs_to_jiffies(URB_TIMEOUT_MS));
+	if (ret < 0) {
+		dev_err(mors->dev, "error %d", ret);
+		goto error;
+	} else if (ret == 0) {
+		/* Timed out. */
+		usb_kill_urb(musb->endpoints[MM81X_EP_MEM_WR].urb);
+	}
+
+	if (musb->errors) {
+		ret = musb->errors;
+		dev_err(mors->dev, "error %d", ret);
+		goto error;
+	}
+
+	ret = size;
+
+error:
+	musb->ongoing_rw = false;
+	mutex_unlock(&musb->lock);
+	return ret;
+}
+
+static int mm81x_usb_dm_read(struct mm81x *mors, u32 address, u8 *data, int len)
+{
+	ssize_t offset = 0;
+	int ret;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	while (offset < len) {
+		ret = mm81x_usb_mem_read(musb, address + offset,
+					 (u8 *)(data + offset),
+					 min((ssize_t)(len - offset),
+					     (ssize_t)USB_MAX_TRANSFER_SIZE));
+		if (ret < 0) {
+			dev_err(mors->dev, "%s failed (errno=%d)", __func__,
+				ret);
+			return ret;
+		}
+
+		offset += ret;
+	}
+
+	return 0;
+}
+
+static int mm81x_usb_dm_write(struct mm81x *mors, u32 address, const u8 *data,
+			      int len)
+{
+	ssize_t offset = 0;
+	int ret;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	while (offset < len) {
+		ret = mm81x_usb_mem_write(musb, address + offset,
+					  (u8 *)(data + offset),
+					  min((ssize_t)(len - offset),
+					      (ssize_t)USB_MAX_TRANSFER_SIZE));
+		if (ret < 0) {
+			dev_err(mors->dev, "%s failed (errno=%d)", __func__,
+				ret);
+			return ret;
+		}
+
+		offset += ret;
+	}
+
+	return 0;
+}
+
+static int mm81x_usb_reg32_read(struct mm81x *mors, u32 address, u32 *val)
+{
+	int ret = 0;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	ret = mm81x_usb_mem_read(musb, address, (u8 *)val, sizeof(*val));
+	if (ret == sizeof(*val)) {
+		*val = le32_to_cpup((__le32 *)val);
+		return 0;
+	}
+
+	dev_err(mors->dev, "usb reg32 read failed %d", ret);
+	return ret;
+}
+
+static int mm81x_usb_reg32_write(struct mm81x *mors, u32 address, u32 val)
+{
+	int ret = 0;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+	__le32 val_le = cpu_to_le32(val);
+
+	ret = mm81x_usb_mem_write(musb, address, (u8 *)&val_le, sizeof(val_le));
+	if (ret == sizeof(val_le))
+		return 0;
+
+	dev_err(mors->dev, "usb reg32 write failed %d", ret);
+	return ret;
+}
+
+static void mm81x_usb_bus_enable(struct mm81x *mors, bool enable)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	if (enable)
+		usb_autopm_get_interface(musb->interface);
+	else
+		usb_autopm_put_interface(musb->interface);
+}
+
+static void mm81x_usb_claim_bus(struct mm81x *mors)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	mutex_lock(&musb->bus_lock);
+}
+
+static void mm81x_usb_release_bus(struct mm81x *mors)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+
+	mutex_unlock(&musb->bus_lock);
+}
+
+static void mm81x_usb_set_irq(struct mm81x *mors, bool enable)
+{
+}
+
+static const struct mm81x_bus_ops mm81x_usb_ops = {
+	.dm_read = mm81x_usb_dm_read,
+	.dm_write = mm81x_usb_dm_write,
+	.reg32_read = mm81x_usb_reg32_read,
+	.reg32_write = mm81x_usb_reg32_write,
+	.digital_reset = mm81x_usb_ndr_reset,
+	.set_bus_enable = mm81x_usb_bus_enable,
+	.claim = mm81x_usb_claim_bus,
+	.release = mm81x_usb_release_bus,
+	.set_irq = mm81x_usb_set_irq,
+	.bulk_alignment = MM81X_BUS_DEFAULT_BULK_ALIGNMENT,
+};
+
+static int mm81x_usb_detect_endpoints(struct mm81x *mors,
+				      const struct usb_interface *intf)
+{
+	int ret;
+	unsigned int i;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+	struct usb_endpoint_descriptor *ep_desc;
+	struct usb_host_interface *intf_desc = intf->cur_altsetting;
+
+	for (i = 0; i < intf_desc->desc.bNumEndpoints; i++) {
+		ep_desc = &intf_desc->endpoint[i].desc;
+
+		if (usb_endpoint_is_bulk_in(ep_desc)) {
+			if (!musb->endpoints[MM81X_EP_MEM_RD].addr) {
+				musb->endpoints[MM81X_EP_MEM_RD].addr =
+					usb_endpoint_num(ep_desc);
+				musb->endpoints[MM81X_EP_MEM_RD].size =
+					usb_endpoint_maxp(ep_desc);
+			} else if (!musb->endpoints[MM81X_EP_REG_RD].addr) {
+				musb->endpoints[MM81X_EP_REG_RD].addr =
+					usb_endpoint_num(ep_desc);
+				musb->endpoints[MM81X_EP_REG_RD].size =
+					usb_endpoint_maxp(ep_desc);
+			}
+		} else if (usb_endpoint_is_bulk_out(ep_desc)) {
+			if (!musb->endpoints[MM81X_EP_MEM_WR].addr) {
+				musb->endpoints[MM81X_EP_MEM_WR].addr =
+					usb_endpoint_num(ep_desc);
+				musb->endpoints[MM81X_EP_MEM_WR].size =
+					usb_endpoint_maxp(ep_desc);
+			} else if (!musb->endpoints[MM81X_EP_REG_WR].addr) {
+				musb->endpoints[MM81X_EP_REG_WR].addr =
+					usb_endpoint_num(ep_desc);
+				musb->endpoints[MM81X_EP_REG_WR].size =
+					usb_endpoint_maxp(ep_desc);
+			}
+		} else if (usb_endpoint_is_int_in(ep_desc)) {
+			musb->endpoints[MM81X_EP_INT].addr =
+				usb_endpoint_num(ep_desc);
+			musb->endpoints[MM81X_EP_INT].size =
+				usb_endpoint_maxp(ep_desc);
+		}
+	}
+
+	dev_dbg(mors->dev, "\tMemory Endpoint IN %s detected: %u size %u",
+		musb->endpoints[MM81X_EP_MEM_RD].addr ? "" : "not",
+		musb->endpoints[MM81X_EP_MEM_RD].addr,
+		musb->endpoints[MM81X_EP_MEM_RD].size);
+	dev_dbg(mors->dev, "\tMemory Endpoint OUT %s detected: %u size %u",
+		musb->endpoints[MM81X_EP_MEM_WR].addr ? "" : "not",
+		musb->endpoints[MM81X_EP_MEM_WR].addr,
+		musb->endpoints[MM81X_EP_MEM_WR].size);
+	dev_dbg(mors->dev, "\tRegister Endpoint IN %s detected: %u",
+		musb->endpoints[MM81X_EP_REG_RD].addr ? "" : "not",
+		musb->endpoints[MM81X_EP_REG_RD].addr);
+	dev_dbg(mors->dev, "\tRegister Endpoint OUT %s detected: %u",
+		musb->endpoints[MM81X_EP_REG_WR].addr ? "" : "not",
+		musb->endpoints[MM81X_EP_REG_WR].addr);
+	dev_dbg(mors->dev, "\tStats IN endpoint %s detected: %u",
+		musb->endpoints[MM81X_EP_INT].addr ? "" : "not",
+		musb->endpoints[MM81X_EP_INT].addr);
+
+	/* Verify we have an IN and OUT */
+	if (!(musb->endpoints[MM81X_EP_MEM_RD].addr &&
+	      musb->endpoints[MM81X_EP_MEM_WR].addr))
+		return -ENODEV;
+
+	/* Verify the stats MM81X_EP_INT is detected */
+	if (!musb->endpoints[MM81X_EP_INT].addr)
+		return -ENODEV;
+
+	/* Verify minimum interrupt status read */
+	if (musb->endpoints[MM81X_EP_INT].size < 8)
+		return -ENODEV;
+
+	musb->endpoints[MM81X_EP_CMD].urb = usb_alloc_urb(0, GFP_KERNEL);
+	if (!musb->endpoints[MM81X_EP_CMD].urb) {
+		ret = -ENOMEM;
+		goto err_ep;
+	}
+
+	musb->endpoints[MM81X_EP_MEM_RD].urb = usb_alloc_urb(0, GFP_KERNEL);
+	if (!musb->endpoints[MM81X_EP_MEM_RD].urb) {
+		ret = -ENOMEM;
+		goto err_ep;
+	}
+
+	musb->endpoints[MM81X_EP_MEM_WR].urb = usb_alloc_urb(0, GFP_KERNEL);
+	if (!musb->endpoints[MM81X_EP_MEM_WR].urb) {
+		ret = -ENOMEM;
+		goto err_ep;
+	}
+
+	musb->endpoints[MM81X_EP_MEM_RD].buffer =
+		kmalloc(USB_MAX_TRANSFER_SIZE, GFP_KERNEL);
+	if (!musb->endpoints[MM81X_EP_MEM_RD].buffer) {
+		ret = -ENOMEM;
+		goto err_ep;
+	}
+
+	musb->endpoints[MM81X_EP_MEM_WR].buffer =
+		kmalloc(USB_MAX_TRANSFER_SIZE, GFP_KERNEL);
+	if (!musb->endpoints[MM81X_EP_MEM_WR].buffer) {
+		ret = -ENOMEM;
+		goto err_ep;
+	}
+
+	musb->endpoints[MM81X_EP_CMD].buffer = usb_alloc_coherent(
+		musb->udev, sizeof(struct mm81x_usb_command), GFP_KERNEL,
+		&musb->endpoints[MM81X_EP_CMD].urb->transfer_dma);
+
+	if (!musb->endpoints[MM81X_EP_CMD].buffer) {
+		ret = -ENOMEM;
+		goto err_ep;
+	}
+
+	/* Assign command to memory out end point */
+	musb->endpoints[MM81X_EP_CMD].addr =
+		musb->endpoints[MM81X_EP_MEM_WR].addr;
+	musb->endpoints[MM81X_EP_CMD].size =
+		musb->endpoints[MM81X_EP_MEM_WR].size;
+
+	return 0;
+
+err_ep:
+	if (musb->endpoints[MM81X_EP_CMD].urb &&
+	    musb->endpoints[MM81X_EP_CMD].buffer)
+		usb_free_coherent(
+			musb->udev, sizeof(struct mm81x_usb_command),
+			musb->endpoints[MM81X_EP_CMD].buffer,
+			musb->endpoints[MM81X_EP_CMD].urb->transfer_dma);
+	usb_free_urb(musb->endpoints[MM81X_EP_MEM_RD].urb);
+	usb_free_urb(musb->endpoints[MM81X_EP_CMD].urb);
+	usb_free_urb(musb->endpoints[MM81X_EP_MEM_WR].urb);
+	kfree(musb->endpoints[MM81X_EP_MEM_RD].buffer);
+	kfree(musb->endpoints[MM81X_EP_MEM_WR].buffer);
+
+	return ret;
+}
+
+static void mm81x_urb_cleanup(struct mm81x *mors)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+	struct mm81x_usb_endpoint *int_ep = &musb->endpoints[MM81X_EP_INT];
+	struct mm81x_usb_endpoint *rd_ep = &musb->endpoints[MM81X_EP_MEM_RD];
+	struct mm81x_usb_endpoint *wr_ep = &musb->endpoints[MM81X_EP_MEM_WR];
+	struct mm81x_usb_endpoint *cmd_ep = &musb->endpoints[MM81X_EP_CMD];
+
+	usb_kill_urb(rd_ep->urb);
+	usb_kill_urb(wr_ep->urb);
+	usb_kill_urb(cmd_ep->urb);
+
+	if (int_ep->urb)
+		usb_free_coherent(musb->udev, MM81X_EP_INT_BUFFER_SIZE,
+				  int_ep->buffer, int_ep->urb->transfer_dma);
+
+	if (cmd_ep->urb)
+		usb_free_coherent(musb->udev, sizeof(struct mm81x_usb_command),
+				  cmd_ep->buffer, cmd_ep->urb->transfer_dma);
+
+	kfree(wr_ep->buffer);
+	kfree(rd_ep->buffer);
+
+	usb_free_urb(int_ep->urb);
+	usb_free_urb(wr_ep->urb);
+	usb_free_urb(rd_ep->urb);
+	usb_free_urb(cmd_ep->urb);
+}
+
+static int mm81x_usb_probe(struct usb_interface *interface,
+			   const struct usb_device_id *id)
+{
+	int ret;
+	struct mm81x *mors;
+	struct mm81x_usb *musb;
+
+	mors = mm81x_core_alloc(sizeof(*musb), &interface->dev);
+	if (!mors)
+		return -ENOMEM;
+
+	mors->bus_ops = &mm81x_usb_ops;
+	mors->bus_type = MM81X_BUS_TYPE_USB;
+
+	musb = (struct mm81x_usb *)mors->drv_priv;
+	musb->udev = usb_get_dev(interface_to_usbdev(interface));
+	musb->interface = usb_get_intf(interface);
+
+	mutex_init(&musb->lock);
+	mutex_init(&musb->bus_lock);
+	init_waitqueue_head(&musb->rw_in_wait);
+	usb_set_intfdata(interface, mors);
+
+	ret = mm81x_usb_detect_endpoints(mors, interface);
+	if (ret < 0)
+		goto err_core_free;
+
+	set_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags);
+
+	ret = mm81x_core_init(mors);
+	if (ret)
+		goto err_urb_cleanup;
+
+	INIT_WORK(&mors->usb_irq_work, mm81x_usb_irq_work);
+
+	ret = mm81x_usb_int_enable(mors);
+	if (ret)
+		goto err_core_deinit;
+
+	ret = mm81x_core_register(mors);
+	if (ret)
+		goto err_usb_int_stop;
+
+	/* USB requires remote wakeup functionality for suspend */
+	clear_bit(MM81X_USB_FLAG_SUSPENDED, &musb->flags);
+	musb->interface->needs_remote_wakeup = 1;
+	usb_enable_autosuspend(musb->udev);
+	pm_runtime_set_autosuspend_delay(&musb->udev->dev,
+					 PM_RUNTIME_AUTOSUSPEND_DELAY_MS);
+
+	usb_autopm_get_interface(interface);
+	return 0;
+
+err_usb_int_stop:
+	mm81x_usb_int_stop(mors);
+err_core_deinit:
+	mm81x_core_deinit(mors);
+err_urb_cleanup:
+	mm81x_urb_cleanup(mors);
+err_core_free:
+	mm81x_core_free(mors);
+	usb_put_intf(interface);
+	usb_put_dev(interface_to_usbdev(interface));
+	return ret;
+}
+
+static void mm81x_usb_disconnect(struct usb_interface *interface)
+{
+	struct mm81x *mors = usb_get_intfdata(interface);
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+	int minor = interface->minor;
+	struct usb_device *udev = interface_to_usbdev(interface);
+
+	if (udev->state == USB_STATE_NOTATTACHED) {
+		clear_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags);
+		set_bit(MM81X_STATE_CHIP_UNRESPONSIVE, &mors->state_flags);
+		dev_dbg(mors->dev, "USB suddenly unplugged");
+	}
+
+	usb_disable_autosuspend(udev);
+
+	if (test_bit(MM81X_USB_FLAG_SUSPENDED, &musb->flags)) {
+		dev_dbg(mors->dev, "USB was suspended: release locks");
+		mm81x_usb_release_bus(mors);
+		mutex_unlock(&musb->lock);
+	}
+
+	clear_bit(MM81X_USB_FLAG_SUSPENDED, &musb->flags);
+
+	mm81x_core_unregister(mors);
+	mm81x_usb_int_stop(mors);
+	mm81x_core_deinit(mors);
+	mm81x_urb_cleanup(mors);
+	mm81x_core_free(mors);
+
+	usb_autopm_put_interface(interface);
+	usb_set_intfdata(interface, NULL);
+	dev_info(&interface->dev, "USB Morse #%d now disconnected", minor);
+	usb_put_intf(interface);
+	usb_put_dev(udev);
+}
+
+static int mm81x_usb_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	struct mm81x *mors = usb_get_intfdata(intf);
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+	struct mm81x_usb_endpoint *int_ep = &musb->endpoints[MM81X_EP_INT];
+	struct mm81x_usb_endpoint *rd_ep = &musb->endpoints[MM81X_EP_MEM_RD];
+	struct mm81x_usb_endpoint *wr_ep = &musb->endpoints[MM81X_EP_MEM_WR];
+	struct mm81x_usb_endpoint *cmd_ep = &musb->endpoints[MM81X_EP_CMD];
+
+	if (!test_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags))
+		return -ENODEV;
+
+	usb_kill_urb(int_ep->urb);
+	usb_kill_urb(rd_ep->urb);
+	usb_kill_urb(wr_ep->urb);
+	usb_kill_urb(cmd_ep->urb);
+
+	/* Locking the bus. No USB communication after this point */
+	mm81x_usb_claim_bus(mors);
+	mutex_lock(&musb->lock);
+
+	set_bit(MM81X_USB_FLAG_SUSPENDED, &musb->flags);
+	return 0;
+}
+
+static int mm81x_usb_resume(struct usb_interface *intf)
+{
+	struct mm81x *mors = usb_get_intfdata(intf);
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+	int ret;
+	struct mm81x_usb_endpoint *int_ep = &musb->endpoints[MM81X_EP_INT];
+
+	if (!test_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags))
+		return -ENODEV;
+
+	ret = usb_submit_urb(int_ep->urb, GFP_KERNEL);
+	if (ret)
+		dev_err(mors->dev, "Couldn't submit urb. Error number %d", ret);
+
+	mm81x_usb_release_bus(mors);
+	mutex_unlock(&musb->lock);
+
+	clear_bit(MM81X_USB_FLAG_SUSPENDED, &musb->flags);
+	return 0;
+}
+
+static int mm81x_usb_reset_resume(struct usb_interface *intf)
+{
+	struct mm81x *mors = usb_get_intfdata(intf);
+	struct mm81x_usb *musb = (struct mm81x_usb *)mors->drv_priv;
+	int ret;
+	struct mm81x_usb_endpoint *int_ep = &musb->endpoints[MM81X_EP_INT];
+
+	if (!test_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags))
+		return -ENODEV;
+
+	ret = usb_submit_urb(int_ep->urb, GFP_KERNEL);
+	if (ret)
+		dev_err(mors->dev, "Couldn't submit urb. Error number %d", ret);
+
+	mm81x_usb_release_bus(mors);
+	mutex_unlock(&musb->lock);
+
+	clear_bit(MM81X_USB_FLAG_SUSPENDED, &musb->flags);
+
+	return 0;
+}
+
+static int mm81x_usb_pre_reset(struct usb_interface *intf)
+{
+	return 0;
+}
+
+static int mm81x_usb_post_reset(struct usb_interface *intf)
+{
+	return 0;
+}
+
+static struct usb_driver mm81x_usb_driver = {
+	.name = "mm81x_usb",
+	.probe = mm81x_usb_probe,
+	.disconnect = mm81x_usb_disconnect,
+	.suspend = mm81x_usb_suspend,
+	.resume = mm81x_usb_resume,
+	.reset_resume = mm81x_usb_reset_resume,
+	.pre_reset = mm81x_usb_pre_reset,
+	.post_reset = mm81x_usb_post_reset,
+	.id_table = mm81x_usb_table,
+	.supports_autosuspend = 1,
+	.soft_unbind = 1,
+};
+
+module_usb_driver(mm81x_usb_driver);
+
+MODULE_AUTHOR("Morse Micro");
+MODULE_DESCRIPTION("Driver support for Morse Micro MM81X USB devices");
+MODULE_LICENSE("Dual BSD/GPL");
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 28/33] wifi: mm81x: add yaps.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/yaps.c | 704 +++++++++++++++++++
 1 file changed, 704 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/yaps.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/yaps.c b/drivers/net/wireless/morsemicro/mm81x/yaps.c
new file mode 100644
index 000000000000..bdadb822bf9a
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/yaps.c
@@ -0,0 +1,704 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/gpio.h>
+#include <linux/random.h>
+#include <linux/timer.h>
+#include <linux/bitops.h>
+#include <linux/slab.h>
+#include "hif.h"
+#include "ps.h"
+#include "bus.h"
+#include "command.h"
+#include "skbq.h"
+
+/* This is a fail safe timeout */
+#define CHIP_FULL_RECOVERY_TIMEOUT_MS 30
+
+/* Defined as the max number of MPDUs per AMPDU */
+#define MAX_PKTS_PER_TX_TXN 16
+#define MAX_PKTS_PER_RX_TXN 32
+
+static int mm81x_yaps_alloc_pkt_buffers(struct mm81x_yaps *yaps)
+{
+	yaps->hw.to_chip_pkts = kcalloc(MAX_PKTS_PER_TX_TXN,
+					sizeof(*yaps->hw.to_chip_pkts),
+					GFP_KERNEL);
+	if (!yaps->hw.to_chip_pkts)
+		return -ENOMEM;
+
+	yaps->hw.from_chip_pkts = kcalloc(MAX_PKTS_PER_RX_TXN,
+					  sizeof(*yaps->hw.from_chip_pkts),
+					  GFP_KERNEL);
+	if (!yaps->hw.from_chip_pkts) {
+		kfree(yaps->hw.to_chip_pkts);
+		yaps->hw.to_chip_pkts = NULL;
+		return -ENOMEM;
+	}
+
+	return 0;
+}
+
+static void mm81x_yaps_free_pkt_buffers(struct mm81x_yaps *yaps)
+{
+	kfree(yaps->hw.from_chip_pkts);
+	yaps->hw.from_chip_pkts = NULL;
+	kfree(yaps->hw.to_chip_pkts);
+	yaps->hw.to_chip_pkts = NULL;
+}
+
+static int mm81x_yaps_write_pkts(struct mm81x_yaps *yaps,
+				 struct mm81x_yaps_pkt *pkts, int num_pkts,
+				 int *num_pkts_sent)
+{
+	return yaps->ops->write_pkts(yaps, pkts, num_pkts, num_pkts_sent);
+}
+
+static int mm81x_yaps_read_pkts(struct mm81x_yaps *yaps,
+				struct mm81x_yaps_pkt *pkts, int num_pkts_max,
+				int *num_pkts_received)
+{
+	return yaps->ops->read_pkts(yaps, pkts, num_pkts_max,
+				    num_pkts_received);
+}
+
+static int mm81x_yaps_update_status(struct mm81x_yaps *yaps)
+{
+	return yaps->ops->update_status(yaps);
+}
+
+/* Mappings between sk_buff, skbq and yaps */
+static struct mm81x_skbq *mm81x_yaps_tc_q_from_aci(struct mm81x *mors, int aci)
+{
+	struct mm81x_yaps *yaps = &mors->hif.u.yaps;
+
+	if (aci >= ARRAY_SIZE(yaps->data_tx_qs))
+		return NULL;
+	return &yaps->data_tx_qs[aci];
+}
+
+static void mm81x_yaps_get_tx_qs(struct mm81x *mors, struct mm81x_skbq **qs,
+				 int *num_qs)
+{
+	*qs = mors->hif.u.yaps.data_tx_qs;
+	*num_qs = YAPS_TX_SKBQ_MAX;
+}
+
+static struct mm81x_skbq *mm81x_yaps_get_bcn_tc_q(struct mm81x *mors)
+{
+	return &mors->hif.u.yaps.beacon_q;
+}
+
+static struct mm81x_skbq *mm81x_yaps_get_mgmt_tc_q(struct mm81x *mors)
+{
+	return &mors->hif.u.yaps.mgmt_q;
+}
+
+static struct mm81x_skbq *mm81x_yaps_get_tx_cmd_queue(struct mm81x *mors)
+{
+	return &mors->hif.u.yaps.cmd_q;
+}
+
+static int mm81x_yaps_irq_handler(struct mm81x *mors, u32 status)
+{
+	if (status & BIT(MM81X_INT_YAPS_FC_PKT_WAITING_IRQN))
+		set_bit(MM81X_HIF_EVT_RX_PEND, &mors->hif.event_flags);
+
+	if (status & BIT(MM81X_INT_YAPS_FC_PACKET_FREED_UP_IRQN)) {
+		timer_delete_sync_try(&mors->hif.u.yaps.chip_queue_full.timer);
+		set_bit(MM81X_HIF_EVT_TX_PACKET_FREED_UP_PEND,
+			&mors->hif.event_flags);
+	}
+
+	queue_work(mors->chip_wq, &mors->hif_work);
+	return 0;
+}
+
+const struct mm81x_hif_ops mm81x_yaps_ops = {
+	.init = mm81x_yaps_init,
+	.flush_tx_data = mm81x_yaps_flush_tx_data,
+	.flush_cmds = mm81x_yaps_flush_cmds,
+	.get_tx_status_pending_count = mm81x_yaps_get_tx_status_pending_count,
+	.get_tx_buffered_count = mm81x_yaps_get_tx_buffered_count,
+	.finish = mm81x_yaps_finish,
+	.skbq_get_tx_qs = mm81x_yaps_get_tx_qs,
+	.get_tx_beacon_queue = mm81x_yaps_get_bcn_tc_q,
+	.get_tx_mgmt_queue = mm81x_yaps_get_mgmt_tc_q,
+	.get_tx_cmd_queue = mm81x_yaps_get_tx_cmd_queue,
+	.get_tx_data_queue = mm81x_yaps_tc_q_from_aci,
+	.handle_irq = mm81x_yaps_irq_handler
+};
+
+static int mm81x_yaps_read_pkt(struct mm81x_yaps *yaps, struct sk_buff *skb)
+{
+	struct mm81x *mors = yaps->mors;
+	struct sk_buff_head skbq;
+	struct mm81x_skbq *mq = NULL;
+	struct mm81x_skb_hdr *hdr;
+	int skb_bytes_remaining;
+	int skb_len;
+	int ret = 0;
+
+	if (!skb) {
+		ret = -EINVAL;
+		goto exit_return_page;
+	}
+
+	__skb_queue_head_init(&skbq);
+
+	hdr = (struct mm81x_skb_hdr *)skb->data;
+	if (hdr->sync != MM81X_SKB_HEADER_SYNC) {
+		dev_err(mors->dev, "sync value error [0xAA:%d], hdr.len %d",
+			hdr->sync, hdr->len);
+		ret = -EIO;
+		goto exit_return_page;
+	}
+
+	if (yaps->mors->hif.validate_skb_checksum &&
+	    !mm81x_skbq_validate_checksum(skb->data)) {
+		dev_dbg(yaps->mors->dev,
+			"SKB checksum is invalid hdr:[c:%02X s:%02X len:%d]",
+			hdr->channel, hdr->sync, hdr->len);
+
+		if (hdr->channel != MM81X_SKB_CHAN_TX_STATUS) {
+			ret = -EIO;
+			goto exit;
+		}
+	}
+
+	switch (hdr->channel) {
+	case MM81X_SKB_CHAN_DATA:
+	case MM81X_SKB_CHAN_NDP_FRAMES:
+	case MM81X_SKB_CHAN_TX_STATUS:
+	case MM81X_SKB_CHAN_DATA_NOACK:
+	case MM81X_SKB_CHAN_BEACON:
+	case MM81X_SKB_CHAN_MGMT:
+		mq = &yaps->data_rx_q;
+		break;
+	case MM81X_SKB_CHAN_COMMAND:
+		mq = &yaps->cmd_resp_q;
+		break;
+	default:
+		dev_err(mors->dev, "channel value error [%d]", hdr->channel);
+		ret = -EIO;
+		goto exit_return_page;
+	}
+
+	skb_len = sizeof(*hdr) + hdr->offset + le16_to_cpu(hdr->len);
+	skb_bytes_remaining = mm81x_skbq_space(mq);
+
+	if (skb_len > skb_bytes_remaining) {
+		dev_err(mors->dev,
+			"Page will not fit in SKBQ, dropping - len %d remain %d",
+			skb_len, skb_bytes_remaining);
+		ret = -ENOMEM;
+		/* Queue work to clear backlog */
+		queue_work(mors->net_wq, &mq->dispatch_work);
+		goto exit_return_page;
+	}
+
+	skb_trim(skb, skb_len);
+	__skb_queue_tail(&skbq, skb);
+
+	if (skb_queue_len(&skbq))
+		mm81x_skbq_enq(mq, &skbq);
+
+	/* push packets up in a different context */
+	queue_work(mors->net_wq, &mq->dispatch_work);
+
+	goto exit;
+
+exit_return_page:
+	if (ret && mq) {
+		dev_err(mors->dev, "failed %d", ret);
+		mm81x_skbq_purge(mq, &skbq);
+		goto exit;
+	}
+
+exit:
+	if (ret && skb)
+		dev_kfree_skb(skb);
+
+	return ret;
+}
+
+static int mm81x_yaps_tx(struct mm81x_yaps *yaps, struct mm81x_skbq *mq)
+{
+	int i;
+	int ret = 0;
+	int num_skbs = 0;
+	int tc_pkt_idx = 0;
+	int num_pkts_sent = 0;
+	struct sk_buff *skb;
+	struct sk_buff_head skbq_to_send;
+	struct sk_buff_head skbq_sent;
+	struct sk_buff_head skbq_failed;
+	struct sk_buff *pfirst, *pnext;
+	struct mm81x *mors = yaps->mors;
+	struct mm81x_skb_hdr *hdr;
+
+	/* Check there is something on the queue */
+	spin_lock_bh(&mq->lock);
+	skb = skb_peek(&mq->skbq);
+	spin_unlock_bh(&mq->lock);
+	if (!skb)
+		return 0;
+
+	__skb_queue_head_init(&skbq_to_send);
+	__skb_queue_head_init(&skbq_sent);
+	__skb_queue_head_init(&skbq_failed);
+
+	if (mq == &yaps->cmd_q)
+		/* Purge timed-out commands (this should not happen) */
+		mm81x_skbq_purge(mq, &mq->pending);
+	else if (mq == &yaps->mgmt_q && skb_queue_len(&mq->skbq) > 0)
+		/*
+		 * Purge old mgmt frames that have not been sent due to
+		 * congestion
+		 */
+		mm81x_skbq_purge_aged(mors, mq);
+
+	num_skbs =
+		mm81x_skbq_deq_num_skb(mq, &skbq_to_send, MAX_PKTS_PER_TX_TXN);
+
+	skb_queue_walk_safe(&skbq_to_send, pfirst, pnext) {
+		enum mm81x_yaps_to_chip_q tc_queue;
+
+		hdr = (struct mm81x_skb_hdr *)pfirst->data;
+		switch (hdr->channel) {
+		case MM81X_SKB_CHAN_COMMAND:
+			tc_queue = MM81X_YAPS_CMD_Q;
+			break;
+		case MM81X_SKB_CHAN_BEACON:
+			tc_queue = MM81X_YAPS_BEACON_Q;
+			break;
+		case MM81X_SKB_CHAN_MGMT:
+			tc_queue = MM81X_YAPS_MGMT_Q;
+			break;
+		default:
+			tc_queue = MM81X_YAPS_TX_Q;
+			break;
+		}
+		yaps->hw.to_chip_pkts[tc_pkt_idx].tc_queue = tc_queue;
+		yaps->hw.to_chip_pkts[tc_pkt_idx].skb = pfirst;
+		tc_pkt_idx++;
+	}
+
+	/* Send queued packets to chip */
+	ret = mm81x_yaps_update_status(yaps);
+	if (ret)
+		return ret;
+
+	ret = mm81x_yaps_write_pkts(yaps, yaps->hw.to_chip_pkts, tc_pkt_idx,
+				    &num_pkts_sent);
+
+	/* Move sent packets to done queue */
+	for (i = 0; i < num_pkts_sent; ++i) {
+		pfirst = __skb_dequeue(&skbq_to_send);
+		__skb_queue_tail(&skbq_sent, pfirst);
+	}
+
+	for (i = num_pkts_sent; i < num_skbs; ++i) {
+		pfirst = __skb_dequeue(&skbq_to_send);
+		__skb_queue_tail(&skbq_failed, pfirst);
+	}
+
+	if (skb_queue_len(&skbq_failed) > 0) {
+		mm81x_skbq_enq_prepend(mq, &skbq_failed);
+
+		/* queue full, can't requeue */
+		if (skb_queue_len(&skbq_failed) > 0) {
+			dev_warn(mors->dev,
+				 "can't requeue failed pkts, purging");
+			__skb_queue_purge(&skbq_failed);
+		}
+	}
+
+	if (skb_queue_len(&skbq_sent) > 0)
+		mm81x_skbq_tx_complete(mq, &skbq_sent);
+
+	return ret;
+}
+
+/* Returns true if there are TX data pages waiting to be sent */
+static bool mm81x_yaps_tx_data_handler(struct mm81x_yaps *yaps)
+{
+	s16 aci;
+	u32 count = 0;
+	struct mm81x *mors = yaps->mors;
+
+	for (aci = MM81X_ACI_VO; aci >= 0; aci--) {
+		struct mm81x_skbq *data_q = mm81x_yaps_tc_q_from_aci(mors, aci);
+
+		if (!mm81x_is_data_tx_allowed(mors))
+			break;
+
+		yaps->chip_queue_full.is_full = mm81x_yaps_tx(yaps, data_q);
+		count += mm81x_skbq_count(data_q);
+
+		if (yaps->chip_queue_full.is_full)
+			break;
+
+		if (aci == MM81X_ACI_BE)
+			break;
+	}
+
+	/*
+	 * Data has potentially been transmitted from the data SKBQs.
+	 * If the mac80211 TX data Qs were previously stopped, now would
+	 * be a good time to check if they can be started again.
+	 */
+	mm81x_skbq_may_wake_tx_queues(mors);
+
+	return (count > 0) && mm81x_is_data_tx_allowed(mors);
+}
+
+/* Returns true if there are commands waiting to be sent */
+static bool mm81x_yaps_tx_cmd_handler(struct mm81x_yaps *yaps)
+{
+	struct mm81x_skbq *cmd_q = &yaps->cmd_q;
+
+	mm81x_yaps_tx(yaps, cmd_q);
+
+	return mm81x_skbq_count(cmd_q) > 0;
+}
+
+static bool mm81x_yaps_tx_beacon_handler(struct mm81x_yaps *yaps)
+{
+	struct mm81x_skbq *beacon_q = &yaps->beacon_q;
+
+	mm81x_yaps_tx(yaps, beacon_q);
+
+	return mm81x_skbq_count(beacon_q) > 0;
+}
+
+static bool mm81x_yaps_tx_mgmt_handler(struct mm81x_yaps *yaps)
+{
+	struct mm81x_skbq *mgmt_q = &yaps->mgmt_q;
+
+	mm81x_yaps_tx(yaps, mgmt_q);
+
+	return mm81x_skbq_count(mgmt_q) > 0;
+}
+
+/* Returns true if there are populated RX pages left in the device */
+static bool mm81x_yaps_rx_handler(struct mm81x_yaps *yaps)
+{
+	int ret = 0;
+	int i;
+	int num_pks_received;
+
+	ret = mm81x_yaps_update_status(yaps);
+	if (ret)
+		goto exit;
+
+	ret = mm81x_yaps_read_pkts(yaps, yaps->hw.from_chip_pkts,
+				   MAX_PKTS_PER_RX_TXN, &num_pks_received);
+	if (ret && ret != -EAGAIN) {
+		dev_err(yaps->mors->dev, "YAPS read_pkts fail: %d", ret);
+		goto exit;
+	}
+
+	for (i = 0; i < num_pks_received; ++i) {
+		mm81x_yaps_read_pkt(yaps, yaps->hw.from_chip_pkts[i].skb);
+		yaps->hw.from_chip_pkts[i].skb = NULL;
+	}
+
+exit:
+	if (ret == -ENOMEM || ret == -EAGAIN)
+		return true;
+	else
+		return false;
+}
+
+void mm81x_yaps_stale_tx_work(struct work_struct *work)
+{
+	int i;
+	int flushed = 0;
+	struct mm81x *mors = container_of(work, struct mm81x, tx_stale_work);
+	struct mm81x_yaps *yaps;
+
+	yaps = &mors->hif.u.yaps;
+	flushed += mm81x_skbq_check_for_stale_tx(mors, &yaps->beacon_q);
+	flushed += mm81x_skbq_check_for_stale_tx(mors, &yaps->mgmt_q);
+
+	for (i = 0; i < ARRAY_SIZE(yaps->data_tx_qs); i++)
+		flushed += mm81x_skbq_check_for_stale_tx(mors,
+							 &yaps->data_tx_qs[i]);
+
+	if (!flushed)
+		return;
+
+	dev_dbg(mors->dev, "Flushed %d stale TX SKBs", flushed);
+
+	if (mors->ps.enable && !mors->ps.suspended &&
+	    (mm81x_yaps_get_tx_buffered_count(mors) == 0)) {
+		/* Evaluate ps to check if it was gated on a stale tx status */
+		queue_delayed_work(mors->chip_wq, &mors->ps.delayed_eval_work,
+				   0);
+	}
+}
+
+void mm81x_yaps_work(struct work_struct *work)
+{
+	struct mm81x *mors = container_of(work, struct mm81x, hif_work);
+	unsigned long *flags = &mors->hif.event_flags;
+	struct mm81x_yaps *yaps = &mors->hif.u.yaps;
+
+	if (test_bit(MM81X_STATE_CHIP_UNRESPONSIVE, &mors->state_flags))
+		return;
+
+	if (!*flags)
+		return;
+
+	/* Disable power save in case it is running */
+	mm81x_ps_disable(mors);
+	mm81x_claim_bus(mors);
+
+	/*
+	 * Handle any populated RX pages from chip first to
+	 * avoid dropping pkts due to full on-chip buffers.
+	 * Check if all pages were removed, set event flags if not.
+	 */
+	if (test_and_clear_bit(MM81X_HIF_EVT_RX_PEND, flags)) {
+		if (mm81x_yaps_rx_handler(yaps))
+			set_bit(MM81X_HIF_EVT_RX_PEND, flags);
+	}
+
+	/* TX any commands before considering data */
+	if (test_and_clear_bit(MM81X_HIF_EVT_TX_COMMAND_PEND, flags)) {
+		if (mm81x_yaps_tx_cmd_handler(yaps))
+			set_bit(MM81X_HIF_EVT_TX_COMMAND_PEND, flags);
+	}
+
+	/* TX beacons before considering mgmt/data */
+	if (test_and_clear_bit(MM81X_HIF_EVT_TX_BEACON_PEND, flags)) {
+		if (mm81x_yaps_tx_beacon_handler(yaps))
+			set_bit(MM81X_HIF_EVT_TX_BEACON_PEND, flags);
+	}
+
+	/* TX mgmt before considering data */
+	if (test_and_clear_bit(MM81X_HIF_EVT_TX_MGMT_PEND, flags)) {
+		if (mm81x_yaps_tx_mgmt_handler(yaps))
+			set_bit(MM81X_HIF_EVT_TX_MGMT_PEND, flags);
+	}
+
+	/* Pause TX data Qs */
+	if (test_and_clear_bit(MM81X_HIF_EVT_DATA_TRAFFIC_PAUSE_PEND, flags)) {
+		test_and_clear_bit(MM81X_HIF_EVT_DATA_TRAFFIC_RESUME_PEND,
+				   flags);
+		mm81x_skbq_data_traffic_pause(mors);
+	}
+
+	/* Resume TX data Qs  */
+	if (test_and_clear_bit(MM81X_HIF_EVT_DATA_TRAFFIC_RESUME_PEND, flags))
+		mm81x_skbq_data_traffic_resume(mors);
+
+	/* Handle chip queue status */
+	if (test_and_clear_bit(MM81X_HIF_EVT_TX_PACKET_FREED_UP_PEND, flags))
+		yaps->chip_queue_full.is_full = false;
+
+	/* Check to see if the queue is full or
+	 * long enough has past since the queue was full
+	 */
+	if (yaps->chip_queue_full.is_full &&
+	    time_before(jiffies, yaps->chip_queue_full.retry_expiry))
+		goto exit;
+
+	/* Finally TX any data */
+	if (test_and_clear_bit(MM81X_HIF_EVT_TX_DATA_PEND, flags)) {
+		if (mm81x_yaps_tx_data_handler(yaps))
+			set_bit(MM81X_HIF_EVT_TX_DATA_PEND, flags);
+
+		if (yaps->chip_queue_full.is_full) {
+			yaps->chip_queue_full.retry_expiry =
+				jiffies +
+				msecs_to_jiffies(CHIP_FULL_RECOVERY_TIMEOUT_MS);
+			mod_timer(&yaps->chip_queue_full.timer,
+				  yaps->chip_queue_full.retry_expiry);
+		}
+	}
+
+exit:
+
+	/* Disable power save in case it is running */
+	mm81x_release_bus(mors);
+	mm81x_ps_enable(mors);
+
+	/* Don't requeue work if we are shutting down. */
+	if (yaps->finish)
+		return;
+	/*
+	 * Evaluate all events except MM81X_HIF_EVT_TX_DATA_PEND in case data
+	 * tx queue is full
+	 */
+	if ((*flags) & ~(1 << MM81X_HIF_EVT_TX_DATA_PEND))
+		queue_work(mors->chip_wq, &mors->hif_work);
+	/*
+	 * if data tx queue is not full and the work hasn't been queued let's
+	 * queue it
+	 */
+	else if (!yaps->chip_queue_full.is_full && *flags)
+		queue_work(mors->chip_wq, &mors->hif_work);
+}
+
+int mm81x_yaps_get_tx_status_pending_count(struct mm81x *mors)
+{
+	int i = 0;
+	int count = 0;
+	struct mm81x_yaps *yaps;
+
+	yaps = &mors->hif.u.yaps;
+	count += skb_queue_len(&yaps->beacon_q.pending);
+	count += skb_queue_len(&yaps->mgmt_q.pending);
+	count += skb_queue_len(&yaps->cmd_q.pending);
+
+	for (i = 0; i < ARRAY_SIZE(yaps->data_tx_qs); i++)
+		count += skb_queue_len(&yaps->data_tx_qs[i].pending);
+
+	return count;
+}
+
+int mm81x_yaps_get_tx_buffered_count(struct mm81x *mors)
+{
+	int i = 0;
+	int count = 0;
+	struct mm81x_yaps *yaps;
+
+	yaps = &mors->hif.u.yaps;
+	count += skb_queue_len(&yaps->beacon_q.skbq) +
+		 skb_queue_len(&yaps->beacon_q.pending);
+	count += skb_queue_len(&yaps->mgmt_q.skbq) +
+		 skb_queue_len(&yaps->mgmt_q.pending);
+	count += skb_queue_len(&yaps->cmd_q.skbq) +
+		 skb_queue_len(&yaps->cmd_q.pending);
+
+	for (i = 0; i < ARRAY_SIZE(yaps->data_tx_qs); i++)
+		count += mm81x_skbq_count_tx_ready(&yaps->data_tx_qs[i]) +
+			 skb_queue_len(&yaps->data_tx_qs[i].pending);
+
+	return count;
+}
+
+static void mm81x_yaps_tx_q_full_timer(struct timer_list *t)
+{
+	struct mm81x_yaps *yaps =
+		timer_container_of(yaps, t, chip_queue_full.timer);
+
+	queue_work(yaps->mors->chip_wq, &yaps->mors->hif_work);
+}
+
+static void mm81x_yaps_q_chip_full_timer_init(struct mm81x_yaps *yaps)
+{
+	timer_setup(&yaps->chip_queue_full.timer, mm81x_yaps_tx_q_full_timer,
+		    0);
+}
+
+static void mm81x_yaps_q_chip_full_timer_finish(struct mm81x_yaps *yaps)
+{
+	timer_delete_sync_try(&yaps->chip_queue_full.timer);
+}
+
+int mm81x_yaps_init(struct mm81x *mors)
+{
+	int i, ret;
+	struct mm81x_yaps *yaps;
+
+	ret = mm81x_yaps_hw_init(mors);
+	if (ret) {
+		dev_err(mors->dev, "mm81x_yaps_hw_init failed %d", ret);
+		return ret;
+	}
+
+	yaps = &mors->hif.u.yaps;
+	yaps->mors = mors;
+
+	mm81x_claim_bus(mors);
+
+	ret = mm81x_yaps_alloc_pkt_buffers(yaps);
+	if (ret) {
+		dev_err(mors->dev, "Failed to allocate YAPS packet buffers: %d",
+			ret);
+		mm81x_yaps_hw_finish(mors);
+		mm81x_release_bus(mors);
+		return ret;
+	}
+
+	/* YAPS is bi-directional */
+	mm81x_skbq_init(mors, &yaps->data_rx_q,
+			MM81X_HIF_FLAGS_DATA | MM81X_HIF_FLAGS_DIR_TO_HOST);
+	mm81x_skbq_init(mors, &yaps->beacon_q,
+			MM81X_HIF_FLAGS_DATA | MM81X_HIF_FLAGS_DIR_TO_HOST);
+	mm81x_skbq_init(mors, &yaps->mgmt_q,
+			MM81X_HIF_FLAGS_DATA | MM81X_HIF_FLAGS_DIR_TO_HOST);
+
+	for (i = 0; i < ARRAY_SIZE(yaps->data_tx_qs); i++) {
+		mm81x_skbq_init(mors, &yaps->data_tx_qs[i],
+				MM81X_HIF_FLAGS_DATA |
+					MM81X_HIF_FLAGS_DIR_TO_CHIP);
+	}
+
+	mm81x_skbq_init(mors, &yaps->cmd_q,
+			MM81X_HIF_FLAGS_COMMAND | MM81X_HIF_FLAGS_DIR_TO_CHIP);
+	mm81x_skbq_init(mors, &yaps->cmd_resp_q,
+			MM81X_HIF_FLAGS_COMMAND | MM81X_HIF_FLAGS_DIR_TO_HOST);
+
+	mm81x_yaps_q_chip_full_timer_init(yaps);
+	INIT_WORK(&mors->hif_work, mm81x_yaps_work);
+	INIT_WORK(&mors->tx_stale_work, mm81x_yaps_stale_tx_work);
+	mm81x_release_bus(mors);
+	mm81x_hw_enable_stop_notifications(mors, true);
+	return 0;
+}
+
+void mm81x_yaps_finish(struct mm81x *mors)
+{
+	int i;
+	struct mm81x_yaps *yaps;
+
+	mm81x_yaps_hw_enable_irqs(mors, false);
+
+	yaps = &mors->hif.u.yaps;
+	yaps->finish = true;
+
+	mm81x_skbq_finish(&yaps->data_rx_q);
+	mm81x_skbq_finish(&yaps->beacon_q);
+	mm81x_skbq_finish(&yaps->mgmt_q);
+
+	for (i = 0; i < ARRAY_SIZE(yaps->data_tx_qs); i++)
+		mm81x_skbq_finish(&yaps->data_tx_qs[i]);
+
+	mm81x_skbq_finish(&yaps->cmd_q);
+	mm81x_skbq_finish(&yaps->cmd_resp_q);
+
+	mm81x_yaps_q_chip_full_timer_finish(yaps);
+
+	cancel_work_sync(&mors->hif_work);
+	cancel_work_sync(&mors->tx_stale_work);
+
+	mm81x_yaps_free_pkt_buffers(yaps);
+	mm81x_yaps_hw_finish(mors);
+}
+
+void mm81x_yaps_flush_tx_data(struct mm81x *mors)
+{
+	int i;
+	struct mm81x_yaps *yaps = &mors->hif.u.yaps;
+
+	mm81x_skbq_tx_flush(&yaps->beacon_q);
+	mm81x_skbq_tx_flush(&yaps->mgmt_q);
+
+	for (i = 0; i < ARRAY_SIZE(yaps->data_tx_qs); i++)
+		mm81x_skbq_tx_flush(&yaps->data_tx_qs[i]);
+}
+
+void mm81x_yaps_flush_cmds(struct mm81x *mors)
+{
+	struct mm81x_yaps *yaps = &mors->hif.u.yaps;
+
+	if (yaps->flags & MM81X_HIF_FLAGS_COMMAND) {
+		mm81x_skbq_finish(&yaps->cmd_q);
+		mm81x_skbq_finish(&yaps->cmd_resp_q);
+	}
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 29/33] wifi: mm81x: add yaps.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/yaps.h | 77 ++++++++++++++++++++
 1 file changed, 77 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/yaps.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/yaps.h b/drivers/net/wireless/morsemicro/mm81x/yaps.h
new file mode 100644
index 000000000000..2b2bb5f6e399
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/yaps.h
@@ -0,0 +1,77 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_YAPS_H_
+#define _MM81X_YAPS_H_
+
+#include <linux/skbuff.h>
+#include <linux/workqueue.h>
+#include "skbq.h"
+
+#define YAPS_TX_SKBQ_MAX 4
+
+struct mm81x_hif_ops;
+extern const struct mm81x_hif_ops mm81x_yaps_ops;
+
+enum mm81x_yaps_to_chip_q {
+	MM81X_YAPS_TX_Q = 0,
+	MM81X_YAPS_CMD_Q,
+	MM81X_YAPS_BEACON_Q,
+	MM81X_YAPS_MGMT_Q,
+	/* Keep this last */
+	MM81X_YAPS_NUM_TC_Q
+};
+
+struct mm81x_yaps_pkt {
+	struct sk_buff *skb;
+	enum mm81x_yaps_to_chip_q tc_queue;
+};
+
+struct mm81x_yaps {
+	struct mm81x *mors;
+	struct mm81x_yaps_hw_aux_data *aux_data;
+	const struct mm81x_yaps_ops *ops;
+	u8 flags;
+	struct {
+		struct mm81x_yaps_pkt *to_chip_pkts;
+		struct mm81x_yaps_pkt *from_chip_pkts;
+	} hw;
+
+	/* Chip interface is stopping, new work should not be enqueued. */
+	bool finish;
+
+	struct mm81x_skbq data_tx_qs[YAPS_TX_SKBQ_MAX];
+	struct mm81x_skbq beacon_q;
+	struct mm81x_skbq mgmt_q;
+	struct mm81x_skbq data_rx_q;
+	struct mm81x_skbq cmd_q;
+	struct mm81x_skbq cmd_resp_q;
+
+	struct {
+		struct timer_list timer;
+		unsigned long retry_expiry;
+		bool is_full;
+	} chip_queue_full;
+};
+
+struct mm81x_yaps_ops {
+	int (*write_pkts)(struct mm81x_yaps *yaps, struct mm81x_yaps_pkt *pkts,
+			  int num_pkts, int *num_pkts_sent);
+	int (*read_pkts)(struct mm81x_yaps *yaps, struct mm81x_yaps_pkt *pkts,
+			 int num_pkts_max, int *num_pkts_received);
+	int (*update_status)(struct mm81x_yaps *yaps);
+};
+
+int mm81x_yaps_init(struct mm81x *mors);
+void mm81x_yaps_show(struct mm81x_yaps *yaps, struct seq_file *file);
+void mm81x_yaps_finish(struct mm81x *mors);
+void mm81x_yaps_flush_tx_data(struct mm81x *mors);
+void mm81x_yaps_flush_cmds(struct mm81x *mors);
+void mm81x_yaps_work(struct work_struct *work);
+void mm81x_yaps_stale_tx_work(struct work_struct *work);
+int mm81x_yaps_get_tx_status_pending_count(struct mm81x *mors);
+int mm81x_yaps_get_tx_buffered_count(struct mm81x *mors);
+
+#endif /* !_MM81X_YAPS_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 30/33] wifi: mm81x: add yaps_hw.c
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 .../net/wireless/morsemicro/mm81x/yaps_hw.c   | 702 ++++++++++++++++++
 1 file changed, 702 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/yaps_hw.c

diff --git a/drivers/net/wireless/morsemicro/mm81x/yaps_hw.c b/drivers/net/wireless/morsemicro/mm81x/yaps_hw.c
new file mode 100644
index 000000000000..3d641d23c35b
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/yaps_hw.c
@@ -0,0 +1,702 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include "yaps_hw.h"
+#include "bus.h"
+#include "hif.h"
+#include "yaps.h"
+
+#define YAPS_HW_WINDOW_SIZE_BYTES 32768
+#define YAPS_MAX_PKT_SIZE_BYTES 16128
+#define YAPS_METADATA_PAGE_COUNT 1
+
+#define YAPS_PHANDLE_CORRUPTION_WAR_EXTRA_PAGE 1
+
+#define YAPS_PAGE_SIZE 256
+
+/* Calculate padding required for yaps transaction */
+#define YAPS_CALC_PADDING(_bytes) ((_bytes) & 0x3 ? (4 - ((_bytes) & 0x3)) : 0)
+
+#define YAPS_RESERVED_PAGE_SIZE 256
+
+/*
+ * Yaps data stream delimiter is a 32 bit word with the following fields:
+ *
+ * pkt_size (14 bits) - Packet size not including delimiter or padding
+ * pool_id  (3  bits) - Pool that pages should be allocated from.
+ * padding  (2  bits) - Padding required to bring packet to word (4 byte)
+ * irq      (1  bit ) - Raise a PKT_IRQ on the YDS this is sent to
+ * reserved (5  bits) - Reserved, must write as 0
+ * crc      (7  bits) - YAPS CRC
+ */
+
+/* Packet size not including delimiter or padding */
+#define YAPS_DELIM_GET_PKT_SIZE(_delim) \
+	(((_delim) & 0x3FFF) - YAPS_RESERVED_PAGE_SIZE)
+#define YAPS_DELIM_SET_PKT_SIZE(_pkt_size) \
+	(((_pkt_size) & 0x3FFF) + YAPS_RESERVED_PAGE_SIZE)
+#define YAPS_DELIM_GET_PHANDLE_SIZE(_delim) (((_delim) & 0x3FFF))
+
+/* Pool that pages should be allocated from. */
+#define YAPS_DELIM_SET_POOL_ID(_pool_id) (((_pool_id) & 0x7) << 14)
+
+/* Padding required to bring packet to word (4 byte) boundary */
+#define YAPS_DELIM_GET_PADDING(_delim) (((_delim) >> 17) & 0x3)
+#define YAPS_DELIM_SET_PADDING(_padding) (((_padding) & 0x3) << 17)
+
+/* Raise a PKT_IRQ on the YDS this is sent to */
+#define YAPS_DELIM_SET_IRQ(_irq) (((_irq) & 0x1) << 19)
+
+/* YAPS CRC */
+#define YAPS_DELIM_GET_CRC(_delim) (((_delim) >> 25) & 0x7F)
+#define YAPS_DELIM_SET_CRC(_crc) (((_crc) & 0x7F) << 25)
+
+struct mm81x_yaps_status_regs {
+	/* Allocation pools */
+	u32 tc_tx_pool_num_pages;
+	u32 tc_cmd_pool_num_pages;
+	u32 tc_beacon_pool_num_pages;
+	u32 tc_mgmt_pool_num_pages;
+	u32 fc_rx_pool_num_pages;
+	u32 fc_resp_pool_num_pages;
+	u32 fc_tx_sts_pool_num_pages;
+	u32 fc_aux_pool_num_pages;
+	u32 tc_tx_num_pkts;
+	u32 tc_cmd_num_pkts;
+	u32 tc_beacon_num_pkts;
+	u32 tc_mgmt_num_pkts;
+	u32 fc_num_pkts;
+	u32 fc_done_num_pkts;
+	u32 fc_rx_bytes_in_queue;
+	u32 tc_delim_crc_fail_detected;
+	u32 fc_host_ysl_status;
+	u32 lock;
+} __packed __aligned(8);
+
+struct mm81x_yaps_hw_status_regs {
+	__le32 tc_tx_pool_num_pages;
+	__le32 tc_cmd_pool_num_pages;
+	__le32 tc_beacon_pool_num_pages;
+	__le32 tc_mgmt_pool_num_pages;
+	__le32 fc_rx_pool_num_pages;
+	__le32 fc_resp_pool_num_pages;
+	__le32 fc_tx_sts_pool_num_pages;
+	__le32 fc_aux_pool_num_pages;
+	__le32 tc_tx_num_pkts;
+	__le32 tc_cmd_num_pkts;
+	__le32 tc_beacon_num_pkts;
+	__le32 tc_mgmt_num_pkts;
+	__le32 fc_num_pkts;
+	__le32 fc_done_num_pkts;
+	__le32 fc_rx_bytes_in_queue;
+	__le32 tc_delim_crc_fail_detected;
+	__le32 fc_host_ysl_status;
+	__le32 lock;
+} __packed __aligned(8);
+
+struct mm81x_yaps_hw_aux_data {
+	unsigned long access_lock;
+
+	u32 yds_addr;
+	u32 ysl_addr;
+	u32 status_regs_addr;
+
+	/* Alloc pool sizes */
+	u16 tc_tx_pool_size;
+	u16 tc_cmd_pool_size;
+	u8 tc_beacon_pool_size;
+	u8 tc_mgmt_pool_size;
+	u8 fc_rx_pool_size;
+	u8 fc_resp_pool_size;
+	u8 fc_tx_sts_pool_size;
+	u8 fc_aux_pool_size;
+
+	/* To chip/from chip queue sizes */
+	u8 tc_tx_q_size;
+	u8 tc_cmd_q_size;
+	u8 tc_beacon_q_size;
+	u8 tc_mgmt_q_size;
+	u8 fc_q_size;
+	u8 fc_done_q_size;
+
+	u16 reserved_yaps_page_size;
+
+	/* Buffers to/from chip to support large contiguous reads/writes */
+	char *to_chip_buffer;
+	char *from_chip_buffer;
+
+	/* status registers in host endian */
+	struct mm81x_yaps_status_regs status_regs;
+
+	/* DMA target buffer in firmware endian */
+	struct mm81x_yaps_hw_status_regs hw_status_regs;
+};
+
+static int mm81x_yaps_hw_lock(struct mm81x_yaps *yaps)
+{
+	if (test_and_set_bit_lock(0, &yaps->aux_data->access_lock))
+		return -1;
+	return 0;
+}
+
+static void mm81x_yaps_hw_unlock(struct mm81x_yaps *yaps)
+{
+	clear_bit_unlock(0, &yaps->aux_data->access_lock);
+}
+
+static void
+mm81x_yaps_hw_fill_aux_data_from_hw_tbl(struct mm81x_yaps_hw_aux_data *a,
+					struct mm81x_yaps_hw_table *t)
+{
+	a->ysl_addr = __le32_to_cpu(t->ysl_addr);
+	a->yds_addr = __le32_to_cpu(t->yds_addr);
+	a->status_regs_addr = __le32_to_cpu(t->status_regs_addr);
+	a->tc_tx_pool_size = __le16_to_cpu(t->tc_tx_pool_size);
+	a->fc_rx_pool_size = __le16_to_cpu(t->fc_rx_pool_size);
+	a->tc_cmd_pool_size = t->tc_cmd_pool_size;
+	a->tc_beacon_pool_size = t->tc_beacon_pool_size;
+	a->tc_mgmt_pool_size = t->tc_mgmt_pool_size;
+	a->fc_resp_pool_size = t->fc_resp_pool_size;
+	a->fc_tx_sts_pool_size = t->fc_tx_sts_pool_size;
+	a->fc_aux_pool_size = t->fc_aux_pool_size;
+	a->tc_tx_q_size = t->tc_tx_q_size;
+	a->tc_cmd_q_size = t->tc_cmd_q_size;
+	a->tc_beacon_q_size = t->tc_beacon_q_size;
+	a->tc_mgmt_q_size = t->tc_mgmt_q_size;
+	a->fc_q_size = t->fc_q_size;
+	a->fc_done_q_size = t->fc_done_q_size;
+	a->reserved_yaps_page_size = le16_to_cpu(t->yaps_reserved_page_size);
+}
+
+static u8 mm81x_yaps_hw_crc(u32 word)
+{
+	u8 crc = 0;
+	u8 byte;
+	int i;
+
+	/* Mask to look at only non-CRC bits */
+	word &= 0x1ffffff;
+
+	for (i = 0; i < 4; i++) {
+		byte = (word >> 24) & 0xff;
+		crc = crc7_be(crc, &byte, 1);
+		word <<= 8;
+	}
+
+	return crc >> 1;
+}
+
+static u32 mm81x_write_pkts_h_build_delim(struct mm81x_yaps *yaps,
+					  unsigned int size, u8 pool_id,
+					  bool irq)
+{
+	u32 delim = 0;
+
+	delim |= YAPS_DELIM_SET_PKT_SIZE(size);
+	delim |= YAPS_DELIM_SET_PADDING(YAPS_CALC_PADDING(size));
+	delim |= YAPS_DELIM_SET_POOL_ID(pool_id);
+	delim |= YAPS_DELIM_SET_IRQ(irq);
+	delim |= YAPS_DELIM_SET_CRC(mm81x_yaps_hw_crc(delim));
+	return delim;
+}
+
+void mm81x_yaps_hw_enable_irqs(struct mm81x *mors, bool enable)
+{
+	mm81x_hw_irq_enable(mors, MM81X_INT_YAPS_FC_PKT_WAITING_IRQN, enable);
+	mm81x_hw_irq_enable(mors, MM81X_INT_YAPS_FC_PACKET_FREED_UP_IRQN,
+			    enable);
+}
+
+void mm81x_yaps_hw_read_table(struct mm81x *mors,
+			      struct mm81x_yaps_hw_table *tbl_ptr)
+{
+	mm81x_yaps_hw_fill_aux_data_from_hw_tbl(mors->hif.u.yaps.aux_data,
+						tbl_ptr);
+	mm81x_yaps_hw_enable_irqs(mors, true);
+}
+
+static unsigned int mm81x_write_pkts_h_pages_required(struct mm81x_yaps *yaps,
+						      unsigned int size_bytes)
+{
+	/* Always account for the first metadata page */
+	return DIV_ROUND_UP(size_bytes +
+				    yaps->aux_data->reserved_yaps_page_size,
+			    YAPS_PAGE_SIZE) +
+	       YAPS_METADATA_PAGE_COUNT +
+	       YAPS_PHANDLE_CORRUPTION_WAR_EXTRA_PAGE;
+}
+
+/*
+ * Checks if a single pkt will fit in the chip using the pool/alloc holding
+ * information from the last status register read.
+ */
+static bool mm81x_write_pkts_h_will_fit(struct mm81x_yaps *yaps,
+					struct mm81x_yaps_pkt *pkt, bool update)
+{
+	bool will_fit = true;
+	const int pages_required =
+		mm81x_write_pkts_h_pages_required(yaps, pkt->skb->len);
+	int *pool_pages_avail = NULL;
+	int *pkts_in_queue = NULL;
+	int queue_pkts_avail = 0;
+
+	switch (pkt->tc_queue) {
+	case MM81X_YAPS_TX_Q:
+		pool_pages_avail =
+			&yaps->aux_data->status_regs.tc_tx_pool_num_pages;
+		pkts_in_queue = &yaps->aux_data->status_regs.tc_tx_num_pkts;
+		queue_pkts_avail =
+			yaps->aux_data->tc_tx_q_size - *pkts_in_queue;
+		break;
+	case MM81X_YAPS_CMD_Q:
+		pool_pages_avail =
+			&yaps->aux_data->status_regs.tc_cmd_pool_num_pages;
+		pkts_in_queue = &yaps->aux_data->status_regs.tc_cmd_num_pkts;
+		queue_pkts_avail =
+			yaps->aux_data->tc_cmd_q_size - *pkts_in_queue;
+		break;
+	case MM81X_YAPS_BEACON_Q:
+		pool_pages_avail =
+			&yaps->aux_data->status_regs.tc_beacon_pool_num_pages;
+		pkts_in_queue = &yaps->aux_data->status_regs.tc_beacon_num_pkts;
+		queue_pkts_avail =
+			yaps->aux_data->tc_beacon_q_size - *pkts_in_queue;
+		break;
+	case MM81X_YAPS_MGMT_Q:
+		pool_pages_avail =
+			&yaps->aux_data->status_regs.tc_mgmt_pool_num_pages;
+		pkts_in_queue = &yaps->aux_data->status_regs.tc_mgmt_num_pkts;
+		queue_pkts_avail =
+			yaps->aux_data->tc_mgmt_q_size - *pkts_in_queue;
+		break;
+	default:
+		dev_err(yaps->mors->dev, "yaps invalid tc queue");
+		return false;
+	}
+
+	WARN_ON(queue_pkts_avail < 0);
+
+	if (pages_required > *pool_pages_avail)
+		will_fit = false;
+
+	if (queue_pkts_avail == 0)
+		will_fit = false;
+
+	if (will_fit && update) {
+		*pool_pages_avail -= pages_required;
+		*pkts_in_queue += 1;
+	}
+
+	return will_fit;
+}
+
+static int mm81x_write_pkts_h_err_check(struct mm81x_yaps *yaps,
+					struct mm81x_yaps_pkt *pkt)
+{
+	if (pkt->skb->len + yaps->aux_data->reserved_yaps_page_size >
+	    YAPS_MAX_PKT_SIZE_BYTES)
+		return -EMSGSIZE;
+	if (pkt->tc_queue >= MM81X_YAPS_NUM_TC_Q)
+		return -EINVAL;
+	if (!mm81x_write_pkts_h_will_fit(yaps, pkt, true))
+		return -EAGAIN;
+
+	return 0;
+}
+
+static int mm81x_yaps_hw_write_pkts(struct mm81x_yaps *yaps,
+				    struct mm81x_yaps_pkt *pkts, int num_pkts,
+				    int *num_pkts_sent)
+{
+	int ret = 0;
+	int i;
+	u32 delim = 0;
+	int tx_len;
+	int batch_txn_len = 0;
+	int pkts_pending = 0;
+	bool delim_irq = false;
+	char *to_chip_buffer_aligned =
+		PTR_ALIGN(yaps->aux_data->to_chip_buffer,
+			  mm81x_bus_get_alignment(yaps->mors));
+	char *write_buf = to_chip_buffer_aligned;
+
+	ret = mm81x_yaps_hw_lock(yaps);
+	if (ret) {
+		dev_dbg(yaps->mors->dev, "yaps lock failed %d", ret);
+		return ret;
+	}
+
+	*num_pkts_sent = 0;
+
+	/* Check packet conditions */
+	ret = mm81x_write_pkts_h_err_check(yaps, &pkts[0]);
+	if (ret)
+		goto exit;
+
+	/* Batch packets into larger transactions */
+	for (i = 0; i < num_pkts; ++i) {
+		u32 pkt_size =
+			pkts[i].skb->len + YAPS_CALC_PADDING(pkts[i].skb->len);
+		tx_len = pkt_size + sizeof(delim);
+
+		/*
+		 * Send when we have reached window size, don't split pkt over
+		 * boundary
+		 */
+		if ((batch_txn_len + tx_len) > YAPS_HW_WINDOW_SIZE_BYTES) {
+			ret = mm81x_dm_write(yaps->mors,
+					     yaps->aux_data->yds_addr,
+					     to_chip_buffer_aligned,
+					     batch_txn_len);
+
+			batch_txn_len = 0;
+			if (ret)
+				goto exit;
+			write_buf = to_chip_buffer_aligned;
+			*num_pkts_sent += pkts_pending;
+			pkts_pending = 0;
+		}
+
+		if ((i + 1) == num_pkts) {
+			/* The last packet in the queue has IRQ set */
+			delim_irq = true;
+		} else {
+			/*
+			 * Since this is not the last packet, we can check for
+			 * the next one. In case of errors in the next packet
+			 * set the IRQ
+			 */
+			ret = mm81x_write_pkts_h_err_check(yaps, &pkts[i + 1]);
+			if (ret)
+				delim_irq = true;
+		}
+
+		/* Build stream header*/
+		delim = mm81x_write_pkts_h_build_delim(
+			yaps, pkt_size, pkts[i].tc_queue, delim_irq);
+		*((__le32 *)write_buf) = cpu_to_le32(delim);
+		memcpy(write_buf + sizeof(delim), pkts[i].skb->data,
+		       pkts[i].skb->len);
+
+		write_buf += tx_len;
+		batch_txn_len += tx_len;
+		pkts_pending++;
+
+		if (ret)
+			goto exit;
+	}
+
+exit:
+	if (batch_txn_len > 0) {
+		ret = mm81x_dm_write(yaps->mors, yaps->aux_data->yds_addr,
+				     to_chip_buffer_aligned, batch_txn_len);
+		*num_pkts_sent += pkts_pending;
+	}
+
+	mm81x_yaps_hw_unlock(yaps);
+	return ret;
+}
+
+static bool mm81x_read_pkts_h_is_valid_delim(u32 delim)
+{
+	u8 calc_crc = mm81x_yaps_hw_crc(delim);
+	int pkt_size = YAPS_DELIM_GET_PHANDLE_SIZE(delim);
+	int padding = YAPS_DELIM_GET_PADDING(delim);
+
+	if (calc_crc != YAPS_DELIM_GET_CRC(delim))
+		return false;
+
+	if (pkt_size == 0)
+		return false;
+
+	if ((pkt_size + padding) > YAPS_MAX_PKT_SIZE_BYTES)
+		return false;
+
+	/* Pkt length + padding should not require more padding */
+	if (YAPS_CALC_PADDING(pkt_size) != padding)
+		return false;
+
+	return true;
+}
+
+static int mm81x_read_pkts_h_bytes_remaining(struct mm81x_yaps *yaps)
+{
+	u32 bytes_in_queue = yaps->aux_data->status_regs.fc_rx_bytes_in_queue;
+	u32 delim_overhead =
+		yaps->aux_data->status_regs.fc_num_pkts * sizeof(u32);
+	u32 reserved_bytes = yaps->aux_data->status_regs.fc_num_pkts *
+			     yaps->aux_data->reserved_yaps_page_size;
+
+	if (WARN_ON(bytes_in_queue > INT_MAX) ||
+	    WARN_ON(delim_overhead > INT_MAX) ||
+	    WARN_ON(reserved_bytes > INT_MAX))
+		return -EIO;
+
+	return (int)bytes_in_queue;
+}
+
+static int mm81x_yaps_hw_read_pkts(struct mm81x_yaps *yaps,
+				   struct mm81x_yaps_pkt *pkts,
+				   int num_pkts_max, int *num_pkts_received)
+{
+	int ret;
+	int i = 0;
+	char *from_chip_buffer_aligned =
+		PTR_ALIGN(yaps->aux_data->from_chip_buffer,
+			  mm81x_bus_get_alignment(yaps->mors));
+	char *read_ptr = from_chip_buffer_aligned;
+	int bytes_remaining = mm81x_read_pkts_h_bytes_remaining(yaps);
+	bool again = false;
+
+	*num_pkts_received = 0;
+
+	if (num_pkts_max == 0 || bytes_remaining == 0)
+		return 0;
+	if (bytes_remaining < 0)
+		return bytes_remaining;
+
+	if (bytes_remaining > YAPS_HW_WINDOW_SIZE_BYTES) {
+		bytes_remaining = YAPS_HW_WINDOW_SIZE_BYTES;
+		again = true;
+	}
+
+	/*
+	 * This is more coarse-grained than it needs to be - once the data
+	 * is read into a local buffer the lock can be released, however
+	 * access to from_chip_buffer will need to be protected with its
+	 * own lock
+	 */
+	ret = mm81x_yaps_hw_lock(yaps);
+	if (ret) {
+		dev_dbg(yaps->mors->dev, "yaps lock failed %d", ret);
+		return ret;
+	}
+
+	/* Read all available packets to the buffer */
+	ret = mm81x_dm_read(yaps->mors, yaps->aux_data->ysl_addr,
+			    from_chip_buffer_aligned, bytes_remaining);
+
+	if (ret)
+		goto exit;
+
+	/* Split serialised packets from buffer */
+	while (i < num_pkts_max && bytes_remaining > 0) {
+		u32 delim;
+		int total_len;
+		int pkt_size;
+
+		delim = le32_to_cpu(*((__le32 *)read_ptr));
+		read_ptr += sizeof(delim);
+		bytes_remaining -= sizeof(delim);
+
+		/* End of stream */
+		if (!delim)
+			break;
+
+		if (!mm81x_read_pkts_h_is_valid_delim(delim)) {
+			/*
+			 * This will start a hunt for a valid delimiter. Given
+			 * the CRC is only 7 bit it's possible to find an
+			 * invalid block with a valid delimiter, leading to
+			 * desynchronisation.
+			 */
+			dev_warn(yaps->mors->dev, "yaps invalid delim");
+			break;
+		}
+
+		/* Total length in chip */
+		pkt_size = YAPS_DELIM_GET_PKT_SIZE(delim);
+		total_len = pkt_size + YAPS_DELIM_GET_PADDING(delim);
+
+		if (pkts[i].skb)
+			dev_err(yaps->mors->dev, "yaps packet leak");
+
+		/* SKB doesn't want padding */
+		pkts[i].skb = dev_alloc_skb(pkt_size);
+		if (!pkts[i].skb) {
+			ret = -ENOMEM;
+			dev_err(yaps->mors->dev, "yaps no mem for skb");
+			goto exit;
+		}
+		skb_put(pkts[i].skb, pkt_size);
+
+		if (total_len <= bytes_remaining) {
+			memcpy(pkts[i].skb->data, read_ptr, pkt_size);
+			read_ptr += total_len;
+			bytes_remaining -= total_len;
+		} else {
+			const int read_overhang_len =
+				total_len - bytes_remaining;
+			const int pkt_overhang_len = pkt_size - bytes_remaining;
+
+			memcpy(pkts[i].skb->data, read_ptr, bytes_remaining);
+			read_ptr = from_chip_buffer_aligned;
+
+			ret = mm81x_dm_read(
+				yaps->mors,
+				/* Offset by 4 to avoid retry logic */
+				yaps->aux_data->ysl_addr + 4, read_ptr,
+				read_overhang_len);
+
+			if (ret)
+				goto exit;
+
+			memcpy(pkts[i].skb->data + bytes_remaining, read_ptr,
+			       pkt_overhang_len);
+			read_ptr += read_overhang_len;
+			bytes_remaining = 0;
+		}
+
+		*num_pkts_received += 1;
+		i++;
+	}
+
+	if (again)
+		ret = -EAGAIN;
+
+exit:
+	mm81x_yaps_hw_unlock(yaps);
+	return ret;
+}
+
+static int mm81x_yaps_hw_update_status(struct mm81x_yaps *yaps)
+{
+	int ret;
+	int tc_total_pkt_count;
+	unsigned long reg_read_timeout;
+	struct mm81x_yaps_status_regs *r = &yaps->aux_data->status_regs;
+	struct mm81x_yaps_hw_status_regs *hw_r = &yaps->aux_data->hw_status_regs;
+
+	ret = mm81x_yaps_hw_lock(yaps);
+	if (ret) {
+		dev_dbg(yaps->mors->dev, "yaps lock failed %d", ret);
+		return ret;
+	}
+
+	reg_read_timeout = jiffies + msecs_to_jiffies(100);
+	do {
+		if (time_after(jiffies, reg_read_timeout)) {
+			dev_err(yaps->mors->dev,
+				"timed out reading status registers: %d", ret);
+			ret = -ETIMEDOUT;
+			break;
+		}
+
+		ret = mm81x_dm_read(yaps->mors,
+				    yaps->aux_data->status_regs_addr,
+				    (u8 *)hw_r, sizeof(*hw_r));
+	} while (!ret && le32_to_cpu(hw_r->lock));
+
+	if (ret) {
+		if (ret != -ENODEV) {
+			dev_err(yaps->mors->dev,
+				"error reading yaps status registers: %d", ret);
+		}
+		goto exit_unlock;
+	}
+
+	r->tc_tx_pool_num_pages = le32_to_cpu(hw_r->tc_tx_pool_num_pages);
+	r->tc_cmd_pool_num_pages = le32_to_cpu(hw_r->tc_cmd_pool_num_pages);
+	r->tc_beacon_pool_num_pages = le32_to_cpu(hw_r->tc_beacon_pool_num_pages);
+	r->tc_mgmt_pool_num_pages = le32_to_cpu(hw_r->tc_mgmt_pool_num_pages);
+	r->fc_rx_pool_num_pages = le32_to_cpu(hw_r->fc_rx_pool_num_pages);
+	r->fc_resp_pool_num_pages = le32_to_cpu(hw_r->fc_resp_pool_num_pages);
+	r->fc_tx_sts_pool_num_pages = le32_to_cpu(hw_r->fc_tx_sts_pool_num_pages);
+	r->fc_aux_pool_num_pages = le32_to_cpu(hw_r->fc_aux_pool_num_pages);
+	r->tc_tx_num_pkts = le32_to_cpu(hw_r->tc_tx_num_pkts);
+	r->tc_cmd_num_pkts = le32_to_cpu(hw_r->tc_cmd_num_pkts);
+	r->tc_beacon_num_pkts = le32_to_cpu(hw_r->tc_beacon_num_pkts);
+	r->tc_mgmt_num_pkts = le32_to_cpu(hw_r->tc_mgmt_num_pkts);
+	r->fc_num_pkts = le32_to_cpu(hw_r->fc_num_pkts);
+	r->fc_done_num_pkts = le32_to_cpu(hw_r->fc_done_num_pkts);
+	r->fc_rx_bytes_in_queue = le32_to_cpu(hw_r->fc_rx_bytes_in_queue);
+	r->tc_delim_crc_fail_detected = le32_to_cpu(hw_r->tc_delim_crc_fail_detected);
+	r->lock = le32_to_cpu(hw_r->lock);
+	r->fc_host_ysl_status = le32_to_cpu(hw_r->fc_host_ysl_status);
+
+	tc_total_pkt_count = r->tc_tx_num_pkts + r->tc_cmd_num_pkts +
+			     r->tc_beacon_num_pkts + r->tc_mgmt_num_pkts;
+
+	if (r->tc_delim_crc_fail_detected) {
+		/*
+		 * Host and chip have become desynchronised. This can happen if
+		 * the chip crashes during a YAPS transaction. We cannot
+		 * recover from this.
+		 */
+		dev_err(yaps->mors->dev,
+			"to-chip yaps delimiter CRC fail, pkt_count=%d",
+			tc_total_pkt_count);
+		ret = -EIO;
+	}
+
+	if (mm81x_read_pkts_h_bytes_remaining(yaps))
+		set_bit(MM81X_HIF_EVT_RX_PEND, &yaps->mors->hif.event_flags);
+
+exit_unlock:
+	mm81x_yaps_hw_unlock(yaps);
+	return ret;
+}
+
+static const struct mm81x_yaps_ops mm81x_yaps_hw_ops = {
+	.write_pkts = mm81x_yaps_hw_write_pkts,
+	.read_pkts = mm81x_yaps_hw_read_pkts,
+	.update_status = mm81x_yaps_hw_update_status,
+};
+
+int mm81x_yaps_hw_init(struct mm81x *mors)
+{
+	int ret = 0;
+	struct mm81x_yaps *yaps = NULL;
+	int aux_data_len = sizeof(struct mm81x_yaps_hw_aux_data);
+	int alignment = mm81x_bus_get_alignment(mors);
+
+	yaps = &mors->hif.u.yaps;
+	yaps->aux_data = kzalloc(aux_data_len, GFP_KERNEL);
+	if (!yaps->aux_data) {
+		ret = -ENOMEM;
+		goto err_exit;
+	}
+
+	yaps->aux_data->to_chip_buffer =
+		kzalloc(YAPS_HW_WINDOW_SIZE_BYTES + alignment - 1, GFP_KERNEL);
+	if (!yaps->aux_data->to_chip_buffer) {
+		ret = -ENOMEM;
+		goto err_exit;
+	}
+
+	yaps->aux_data->from_chip_buffer =
+		kzalloc(YAPS_HW_WINDOW_SIZE_BYTES + alignment - 1, GFP_KERNEL);
+	if (!yaps->aux_data->from_chip_buffer) {
+		ret = -ENOMEM;
+		goto err_exit;
+	}
+
+	if (!IS_ALIGNED((uintptr_t)&yaps->aux_data->status_regs, alignment)) {
+		dev_warn(mors->dev,
+			 "Status registers are not aligned to %d bytes",
+			 alignment);
+	}
+
+	yaps->ops = &mm81x_yaps_hw_ops;
+	return ret;
+
+err_exit:
+	mm81x_yaps_hw_finish(mors);
+	return ret;
+}
+
+void mm81x_yaps_hw_finish(struct mm81x *mors)
+{
+	struct mm81x_yaps *yaps;
+
+	yaps = &mors->hif.u.yaps;
+	if (yaps->aux_data) {
+		kfree(yaps->aux_data->from_chip_buffer);
+		yaps->aux_data->from_chip_buffer = NULL;
+		kfree(yaps->aux_data->to_chip_buffer);
+		yaps->aux_data->to_chip_buffer = NULL;
+		kfree(yaps->aux_data);
+		yaps->aux_data = NULL;
+	}
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 31/33] wifi: mm81x: add yaps_hw.h
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 .../net/wireless/morsemicro/mm81x/yaps_hw.h   | 52 +++++++++++++++++++
 1 file changed, 52 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/yaps_hw.h

diff --git a/drivers/net/wireless/morsemicro/mm81x/yaps_hw.h b/drivers/net/wireless/morsemicro/mm81x/yaps_hw.h
new file mode 100644
index 000000000000..89e15375aabc
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/yaps_hw.h
@@ -0,0 +1,52 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+
+#ifndef _MM81X_YAPS_HW_H_
+#define _MM81X_YAPS_HW_H_
+
+#include <linux/types.h>
+#include <linux/crc7.h>
+
+#define MM81X_INT_YAPS_FC_PKT_WAITING_IRQN 0
+#define MM81X_INT_YAPS_FC_PACKET_FREED_UP_IRQN 1
+
+struct mm81x_yaps_hw_table {
+	/* NOTE: We need these padding bytes for yaps to work */
+	u8 padding[4];
+	__le32 ysl_addr;
+	__le32 yds_addr;
+	__le32 status_regs_addr;
+
+	/* Alloc pool sizes */
+	__le16 tc_tx_pool_size;
+	__le16 fc_rx_pool_size;
+	u8 tc_cmd_pool_size;
+	u8 tc_beacon_pool_size;
+	u8 tc_mgmt_pool_size;
+	u8 fc_resp_pool_size;
+	u8 fc_tx_sts_pool_size;
+	u8 fc_aux_pool_size;
+
+	/* To chip/from chip queue sizes */
+	u8 tc_tx_q_size;
+	u8 tc_cmd_q_size;
+	u8 tc_beacon_q_size;
+	u8 tc_mgmt_q_size;
+	u8 fc_q_size;
+	u8 fc_done_q_size;
+
+	__le16 yaps_reserved_page_size;
+	__le16 reserved_unused;
+} __packed;
+
+struct mm81x;
+
+void mm81x_yaps_hw_enable_irqs(struct mm81x *mors, bool enable);
+int mm81x_yaps_hw_init(struct mm81x *mors);
+void mm81x_yaps_hw_finish(struct mm81x *mors);
+void mm81x_yaps_hw_read_table(struct mm81x *mors,
+			      struct mm81x_yaps_hw_table *tbl_ptr);
+
+#endif /* !_MM81X_YAPS_HW_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next v3 32/33] wifi: mm81x: add Kconfig and Makefile
From: Lachlan Hodges @ 2026-06-26  6:29 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260626063014.1275235-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, will be a single commit alongside
SDIO ids once review is complete. See cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/Kconfig                  |  1 +
 drivers/net/wireless/Makefile                 |  1 +
 drivers/net/wireless/morsemicro/Kconfig       | 15 ++++++++++++
 drivers/net/wireless/morsemicro/Makefile      |  2 ++
 drivers/net/wireless/morsemicro/mm81x/Kconfig | 24 +++++++++++++++++++
 .../net/wireless/morsemicro/mm81x/Makefile    | 21 ++++++++++++++++
 6 files changed, 64 insertions(+)
 create mode 100644 drivers/net/wireless/morsemicro/Kconfig
 create mode 100644 drivers/net/wireless/morsemicro/Makefile
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/Kconfig
 create mode 100644 drivers/net/wireless/morsemicro/mm81x/Makefile

diff --git a/drivers/net/wireless/Kconfig b/drivers/net/wireless/Kconfig
index c6599594dc99..baddadf9ec3c 100644
--- a/drivers/net/wireless/Kconfig
+++ b/drivers/net/wireless/Kconfig
@@ -27,6 +27,7 @@ source "drivers/net/wireless/intersil/Kconfig"
 source "drivers/net/wireless/marvell/Kconfig"
 source "drivers/net/wireless/mediatek/Kconfig"
 source "drivers/net/wireless/microchip/Kconfig"
+source "drivers/net/wireless/morsemicro/Kconfig"
 source "drivers/net/wireless/purelifi/Kconfig"
 source "drivers/net/wireless/ralink/Kconfig"
 source "drivers/net/wireless/realtek/Kconfig"
diff --git a/drivers/net/wireless/Makefile b/drivers/net/wireless/Makefile
index e1c4141c6004..d74f817b37de 100644
--- a/drivers/net/wireless/Makefile
+++ b/drivers/net/wireless/Makefile
@@ -12,6 +12,7 @@ obj-$(CONFIG_WLAN_VENDOR_INTERSIL) += intersil/
 obj-$(CONFIG_WLAN_VENDOR_MARVELL) += marvell/
 obj-$(CONFIG_WLAN_VENDOR_MEDIATEK) += mediatek/
 obj-$(CONFIG_WLAN_VENDOR_MICROCHIP) += microchip/
+obj-$(CONFIG_WLAN_VENDOR_MORSEMICRO) += morsemicro/
 obj-$(CONFIG_WLAN_VENDOR_PURELIFI) += purelifi/
 obj-$(CONFIG_WLAN_VENDOR_QUANTENNA) += quantenna/
 obj-$(CONFIG_WLAN_VENDOR_RALINK) += ralink/
diff --git a/drivers/net/wireless/morsemicro/Kconfig b/drivers/net/wireless/morsemicro/Kconfig
new file mode 100644
index 000000000000..cb0653c77d87
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/Kconfig
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config WLAN_VENDOR_MORSEMICRO
+	bool "Morse Micro devices"
+	default y
+	help
+	  If you have a wireless card belonging to this class, say Y.
+
+	  Note that the answer to this question doesn't directly affect the
+	  kernel: saying N will just cause the configurator to skip all the
+	  questions about these cards. If you say Y, you will be asked for
+	  your specific card in the following questions.
+
+if WLAN_VENDOR_MORSEMICRO
+source "drivers/net/wireless/morsemicro/mm81x/Kconfig"
+endif # WLAN_VENDOR_MORSEMICRO
diff --git a/drivers/net/wireless/morsemicro/Makefile b/drivers/net/wireless/morsemicro/Makefile
new file mode 100644
index 000000000000..5b2670f7d171
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/Makefile
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_MM81X) += mm81x/
diff --git a/drivers/net/wireless/morsemicro/mm81x/Kconfig b/drivers/net/wireless/morsemicro/mm81x/Kconfig
new file mode 100644
index 000000000000..33cdcc0df4de
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/Kconfig
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: GPL-2.0
+
+config MM81X
+	tristate "Morse Micro MM81x wireless devices"
+	depends on MAC80211
+	select FW_LOADER
+	select CRC7
+	help
+	  This module adds support for wireless devices based
+	  on Morse Micro MM81xx chipsets.
+
+config MM81X_USB
+	tristate "Morse Micro MM81x USB support"
+	depends on MM81X && USB
+	help
+	  This module adds support for the USB interface of
+	  devices using the Morse Micro MM81x chipset.
+
+config MM81X_SDIO
+	tristate "Morse Micro MM81x SDIO support"
+	depends on MM81X && MMC
+	help
+	  This module adds support for the SDIO interface of
+	  devices using the Morse Micro MM81x chipset.
diff --git a/drivers/net/wireless/morsemicro/mm81x/Makefile b/drivers/net/wireless/morsemicro/mm81x/Makefile
new file mode 100644
index 000000000000..0d494fda1412
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/Makefile
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: GPL-2.0
+
+obj-$(CONFIG_MM81X) += mm81x_core.o
+
+mm81x_core-y += core.o
+mm81x_core-y += mac.o
+mm81x_core-y += hw.o
+mm81x_core-y += fw.o
+mm81x_core-y += command.o
+mm81x_core-y += ps.o
+mm81x_core-y += skbq.o
+mm81x_core-y += yaps_hw.o
+mm81x_core-y += yaps.o
+mm81x_core-y += rc.o
+mm81x_core-y += mmrc.o
+
+obj-$(CONFIG_MM81X_USB) += mm81x_usb.o
+mm81x_usb-y += usb.o
+
+obj-$(CONFIG_MM81X_SDIO) += mm81x_sdio.o
+mm81x_sdio-y += sdio.o
-- 
2.43.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox