Linux wireless drivers development
 help / color / mirror / Atom feed
* [wireless-next:main] BUILD SUCCESS 0314e382cf02983eb3c33ac537ad9701e7858bc9
From: kernel test robot @ 2026-02-27  9:04 UTC (permalink / raw)
  To: Jakub Kicinski; +Cc: Johannes Berg, linux-wireless

tree/branch: https://git.kernel.org/pub/scm/linux/kernel/git/wireless/wireless-next.git main
branch HEAD: 0314e382cf02983eb3c33ac537ad9701e7858bc9  Merge git://git.kernel.org/pub/scm/linux/kernel/git/netdev/net

elapsed time: 844m

configs tested: 279
configs skipped: 2

The following configs have been built successfully.
More configs may be tested in the coming days.

tested configs:
alpha                            alldefconfig    gcc-15.2.0
alpha                             allnoconfig    gcc-15.2.0
alpha                            allyesconfig    gcc-15.2.0
alpha                               defconfig    gcc-15.2.0
arc                              allmodconfig    clang-16
arc                              allmodconfig    gcc-15.2.0
arc                               allnoconfig    gcc-15.2.0
arc                              allyesconfig    clang-23
arc                              allyesconfig    gcc-15.2.0
arc                                 defconfig    gcc-15.2.0
arc                     nsimosci_hs_defconfig    gcc-15.2.0
arc                   randconfig-001-20260227    gcc-8.5.0
arc                   randconfig-002-20260227    gcc-8.5.0
arm                               allnoconfig    clang-23
arm                               allnoconfig    gcc-15.2.0
arm                              allyesconfig    clang-16
arm                              allyesconfig    gcc-15.2.0
arm                                 defconfig    gcc-15.2.0
arm                      jornada720_defconfig    clang-23
arm                        neponset_defconfig    gcc-15.2.0
arm                   randconfig-001-20260227    gcc-14.3.0
arm                   randconfig-001-20260227    gcc-8.5.0
arm                   randconfig-002-20260227    clang-23
arm                   randconfig-002-20260227    gcc-8.5.0
arm                   randconfig-003-20260227    gcc-15.2.0
arm                   randconfig-003-20260227    gcc-8.5.0
arm                   randconfig-004-20260227    clang-23
arm                   randconfig-004-20260227    gcc-8.5.0
arm                           sama7_defconfig    gcc-15.2.0
arm                         socfpga_defconfig    gcc-15.2.0
arm                         wpcm450_defconfig    gcc-15.2.0
arm64                            allmodconfig    clang-19
arm64                            allmodconfig    clang-23
arm64                             allnoconfig    gcc-15.2.0
arm64                               defconfig    gcc-15.2.0
arm64                 randconfig-001-20260227    clang-23
arm64                 randconfig-002-20260227    clang-23
arm64                 randconfig-002-20260227    gcc-15.2.0
arm64                 randconfig-003-20260227    clang-23
arm64                 randconfig-004-20260227    clang-23
arm64                 randconfig-004-20260227    gcc-8.5.0
csky                             allmodconfig    gcc-15.2.0
csky                              allnoconfig    gcc-15.2.0
csky                                defconfig    clang-23
csky                                defconfig    gcc-15.2.0
csky                  randconfig-001-20260227    clang-23
csky                  randconfig-001-20260227    gcc-15.2.0
csky                  randconfig-002-20260227    clang-23
csky                  randconfig-002-20260227    gcc-10.5.0
hexagon                          alldefconfig    gcc-15.2.0
hexagon                          allmodconfig    clang-17
hexagon                          allmodconfig    gcc-15.2.0
hexagon                           allnoconfig    clang-23
hexagon                           allnoconfig    gcc-15.2.0
hexagon                             defconfig    gcc-15.2.0
hexagon               randconfig-001-20260227    clang-23
hexagon               randconfig-002-20260227    clang-23
i386                             allmodconfig    clang-20
i386                             allmodconfig    gcc-14
i386                              allnoconfig    gcc-14
i386                              allnoconfig    gcc-15.2.0
i386                             allyesconfig    clang-20
i386                             allyesconfig    gcc-14
i386        buildonly-randconfig-001-20260227    clang-20
i386        buildonly-randconfig-002-20260227    clang-20
i386        buildonly-randconfig-003-20260227    clang-20
i386        buildonly-randconfig-004-20260227    clang-20
i386        buildonly-randconfig-005-20260227    clang-20
i386        buildonly-randconfig-006-20260227    clang-20
i386                                defconfig    gcc-15.2.0
i386                  randconfig-001-20260227    clang-20
i386                  randconfig-002-20260227    clang-20
i386                  randconfig-003-20260227    clang-20
i386                  randconfig-003-20260227    gcc-14
i386                  randconfig-004-20260227    clang-20
i386                  randconfig-004-20260227    gcc-14
i386                  randconfig-005-20260227    clang-20
i386                  randconfig-005-20260227    gcc-12
i386                  randconfig-006-20260227    clang-20
i386                  randconfig-006-20260227    gcc-14
i386                  randconfig-007-20260227    clang-20
i386                  randconfig-011-20260227    gcc-14
i386                  randconfig-012-20260227    gcc-14
i386                  randconfig-013-20260227    gcc-14
i386                  randconfig-014-20260227    gcc-14
i386                  randconfig-015-20260227    gcc-14
i386                  randconfig-016-20260227    gcc-14
i386                  randconfig-017-20260227    gcc-14
loongarch                        alldefconfig    clang-23
loongarch                        allmodconfig    clang-19
loongarch                        allmodconfig    clang-23
loongarch                         allnoconfig    clang-23
loongarch                         allnoconfig    gcc-15.2.0
loongarch                           defconfig    clang-19
loongarch             randconfig-001-20260227    clang-23
loongarch             randconfig-001-20260227    gcc-15.2.0
loongarch             randconfig-002-20260227    clang-20
loongarch             randconfig-002-20260227    clang-23
m68k                             allmodconfig    gcc-15.2.0
m68k                              allnoconfig    gcc-15.2.0
m68k                             allyesconfig    clang-16
m68k                             allyesconfig    gcc-15.2.0
m68k                          amiga_defconfig    gcc-15.2.0
m68k                                defconfig    clang-19
m68k                          hp300_defconfig    gcc-15.2.0
m68k                        m5272c3_defconfig    gcc-15.2.0
m68k                       m5275evb_defconfig    gcc-15.2.0
m68k                           sun3_defconfig    clang-23
microblaze                        allnoconfig    gcc-15.2.0
microblaze                       allyesconfig    gcc-15.2.0
microblaze                          defconfig    clang-19
mips                             allmodconfig    gcc-15.2.0
mips                              allnoconfig    gcc-15.2.0
mips                             allyesconfig    gcc-15.2.0
mips                       bmips_be_defconfig    clang-23
mips                           jazz_defconfig    gcc-15.2.0
mips                    maltaup_xpa_defconfig    gcc-15.2.0
mips                   sb1250_swarm_defconfig    gcc-15.2.0
nios2                            allmodconfig    clang-23
nios2                            allmodconfig    gcc-11.5.0
nios2                             allnoconfig    clang-23
nios2                             allnoconfig    gcc-11.5.0
nios2                               defconfig    clang-19
nios2                 randconfig-001-20260227    clang-23
nios2                 randconfig-001-20260227    gcc-11.5.0
nios2                 randconfig-002-20260227    clang-23
nios2                 randconfig-002-20260227    gcc-11.5.0
openrisc                         allmodconfig    clang-23
openrisc                         allmodconfig    gcc-15.2.0
openrisc                          allnoconfig    clang-23
openrisc                          allnoconfig    gcc-15.2.0
openrisc                   de0_nano_defconfig    gcc-15.2.0
openrisc                            defconfig    gcc-15.2.0
openrisc                    or1ksim_defconfig    gcc-15.2.0
openrisc                 simple_smp_defconfig    clang-23
parisc                           allmodconfig    gcc-15.2.0
parisc                            allnoconfig    clang-23
parisc                            allnoconfig    gcc-15.2.0
parisc                           allyesconfig    clang-19
parisc                           allyesconfig    gcc-15.2.0
parisc                              defconfig    gcc-15.2.0
parisc                randconfig-001-20260227    clang-17
parisc                randconfig-001-20260227    gcc-8.5.0
parisc                randconfig-002-20260227    clang-17
parisc64                            defconfig    clang-19
powerpc                    adder875_defconfig    gcc-15.2.0
powerpc                          allmodconfig    gcc-15.2.0
powerpc                           allnoconfig    clang-23
powerpc                           allnoconfig    gcc-15.2.0
powerpc                 canyonlands_defconfig    gcc-15.2.0
powerpc                       holly_defconfig    clang-23
powerpc                        icon_defconfig    gcc-15.2.0
powerpc                  iss476-smp_defconfig    gcc-15.2.0
powerpc                     kmeter1_defconfig    gcc-15.2.0
powerpc                   motionpro_defconfig    gcc-15.2.0
powerpc                 mpc834x_itx_defconfig    gcc-15.2.0
powerpc                  mpc866_ads_defconfig    gcc-15.2.0
powerpc                      pasemi_defconfig    gcc-15.2.0
powerpc                     rainier_defconfig    clang-23
powerpc               randconfig-001-20260227    clang-17
powerpc               randconfig-002-20260227    clang-17
powerpc                    socrates_defconfig    clang-23
powerpc                     stx_gp3_defconfig    gcc-15.2.0
powerpc                     taishan_defconfig    gcc-15.2.0
powerpc                     tqm5200_defconfig    gcc-15.2.0
powerpc                     tqm8555_defconfig    clang-23
powerpc64             randconfig-001-20260227    clang-17
powerpc64             randconfig-002-20260227    clang-17
powerpc64             randconfig-002-20260227    gcc-14.3.0
riscv                            allmodconfig    clang-23
riscv                             allnoconfig    clang-23
riscv                             allnoconfig    gcc-15.2.0
riscv                            allyesconfig    clang-16
riscv                               defconfig    clang-23
riscv                               defconfig    gcc-15.2.0
riscv                 randconfig-001-20260227    gcc-12.5.0
riscv                 randconfig-002-20260227    gcc-12.5.0
s390                             allmodconfig    clang-18
s390                             allmodconfig    clang-19
s390                              allnoconfig    clang-23
s390                             allyesconfig    gcc-15.2.0
s390                                defconfig    clang-23
s390                                defconfig    gcc-15.2.0
s390                  randconfig-001-20260227    gcc-12.5.0
s390                  randconfig-002-20260227    gcc-12.5.0
sh                               allmodconfig    gcc-15.2.0
sh                                allnoconfig    clang-23
sh                                allnoconfig    gcc-15.2.0
sh                               allyesconfig    clang-19
sh                               allyesconfig    gcc-15.2.0
sh                                  defconfig    gcc-14
sh                                  defconfig    gcc-15.2.0
sh                          kfr2r09_defconfig    gcc-15.2.0
sh                          r7780mp_defconfig    gcc-15.2.0
sh                    randconfig-001-20260227    gcc-12.5.0
sh                    randconfig-002-20260227    gcc-12.5.0
sh                          rsk7203_defconfig    gcc-15.2.0
sh                           se7343_defconfig    clang-23
sh                           se7705_defconfig    clang-23
sh                           se7750_defconfig    gcc-15.2.0
sh                   secureedge5410_defconfig    gcc-15.2.0
sh                           sh2007_defconfig    clang-23
sh                     sh7710voipgw_defconfig    clang-23
sh                   sh7724_generic_defconfig    clang-23
sparc                             allnoconfig    clang-23
sparc                             allnoconfig    gcc-15.2.0
sparc                               defconfig    gcc-15.2.0
sparc                 randconfig-001-20260227    clang-23
sparc                 randconfig-002-20260227    clang-23
sparc64                          allmodconfig    clang-23
sparc64                             defconfig    clang-20
sparc64                             defconfig    gcc-14
sparc64               randconfig-001-20260227    clang-23
sparc64               randconfig-002-20260227    clang-23
um                               allmodconfig    clang-19
um                                allnoconfig    clang-23
um                               allyesconfig    gcc-14
um                               allyesconfig    gcc-15.2.0
um                                  defconfig    clang-23
um                                  defconfig    gcc-14
um                             i386_defconfig    gcc-14
um                    randconfig-001-20260227    clang-23
um                    randconfig-002-20260227    clang-23
um                           x86_64_defconfig    clang-23
um                           x86_64_defconfig    gcc-14
x86_64                           allmodconfig    clang-20
x86_64                            allnoconfig    clang-20
x86_64                            allnoconfig    clang-23
x86_64                           allyesconfig    clang-20
x86_64      buildonly-randconfig-001-20260227    gcc-14
x86_64      buildonly-randconfig-002-20260227    gcc-14
x86_64      buildonly-randconfig-003-20260227    gcc-14
x86_64      buildonly-randconfig-004-20260227    gcc-14
x86_64      buildonly-randconfig-005-20260227    gcc-14
x86_64      buildonly-randconfig-006-20260227    gcc-14
x86_64                              defconfig    gcc-14
x86_64                                  kexec    clang-20
x86_64                randconfig-001-20260227    clang-20
x86_64                randconfig-001-20260227    gcc-13
x86_64                randconfig-002-20260227    clang-20
x86_64                randconfig-002-20260227    gcc-13
x86_64                randconfig-003-20260227    gcc-13
x86_64                randconfig-003-20260227    gcc-14
x86_64                randconfig-004-20260227    gcc-13
x86_64                randconfig-005-20260227    gcc-13
x86_64                randconfig-005-20260227    gcc-14
x86_64                randconfig-006-20260227    gcc-13
x86_64                randconfig-006-20260227    gcc-14
x86_64                randconfig-011-20260227    gcc-14
x86_64                randconfig-012-20260227    gcc-14
x86_64                randconfig-013-20260227    clang-20
x86_64                randconfig-013-20260227    gcc-14
x86_64                randconfig-014-20260227    clang-20
x86_64                randconfig-014-20260227    gcc-14
x86_64                randconfig-015-20260227    clang-20
x86_64                randconfig-015-20260227    gcc-14
x86_64                randconfig-016-20260227    gcc-14
x86_64                randconfig-071-20260227    gcc-14
x86_64                randconfig-072-20260227    gcc-14
x86_64                randconfig-073-20260227    gcc-14
x86_64                randconfig-074-20260227    gcc-14
x86_64                randconfig-075-20260227    gcc-14
x86_64                randconfig-076-20260227    gcc-14
x86_64                               rhel-9.4    clang-20
x86_64                           rhel-9.4-bpf    gcc-14
x86_64                          rhel-9.4-func    clang-20
x86_64                    rhel-9.4-kselftests    clang-20
x86_64                         rhel-9.4-kunit    gcc-14
x86_64                           rhel-9.4-ltp    gcc-14
x86_64                          rhel-9.4-rust    clang-20
xtensa                            allnoconfig    clang-23
xtensa                            allnoconfig    gcc-15.2.0
xtensa                           allyesconfig    clang-23
xtensa                           allyesconfig    gcc-15.2.0
xtensa                  audio_kc705_defconfig    gcc-15.2.0
xtensa                  cadence_csp_defconfig    gcc-15.2.0
xtensa                generic_kc705_defconfig    gcc-15.2.0
xtensa                randconfig-001-20260227    clang-23
xtensa                randconfig-002-20260227    clang-23

--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki

^ permalink raw reply

* [PATCH] wifi: rtl8xxxu: fix potential use of uninitialized value
From: Yi Cong @ 2026-02-27  7:37 UTC (permalink / raw)
  To: Jes.Sorensen; +Cc: linux-wireless, Yi Cong, stable

From: Yi Cong <yicong@kylinos.cn>

The local variables 'mcs' and 'nss' in rtl8xxxu_update_ra_report() are
passed to rtl8xxxu_desc_to_mcsrate() as output parameters. If the helper
function encounters an unhandled rate index, it may return without setting
these values, leading to the use of uninitialized stack data.

Initialize 'mcs' to 0 and 'nss' to 1 at declaration to ensure safe defaults
(MCS 0, 1 spatial stream) are used even if parsing fails. Note that 'nss'
must be at least 1 to be valid.

Fixes: 7de16123d9e2 ("wifi: rtl8xxxu: Introduce rtl8xxxu_update_ra_report")
Cc: stable@vger.kernel.org
Signed-off-by: Yi Cong <yicong@kylinos.cn>
---
 drivers/net/wireless/realtek/rtl8xxxu/core.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/net/wireless/realtek/rtl8xxxu/core.c b/drivers/net/wireless/realtek/rtl8xxxu/core.c
index 794187d28caa..d0035960f8d4 100644
--- a/drivers/net/wireless/realtek/rtl8xxxu/core.c
+++ b/drivers/net/wireless/realtek/rtl8xxxu/core.c
@@ -4820,7 +4820,7 @@ static void rtl8xxxu_set_aifs(struct rtl8xxxu_priv *priv, u8 slot_time)
 void rtl8xxxu_update_ra_report(struct rtl8xxxu_ra_report *rarpt,
 			       u8 rate, u8 sgi, u8 bw)
 {
-	u8 mcs, nss;
+	u8 mcs = 0, nss = 1;
 
 	rarpt->txrate.flags = 0;
 
-- 
2.25.1


^ permalink raw reply related

* [syzbot] [wireless?] possible deadlock in ieee80211_change_mac (4)
From: syzbot @ 2026-02-27  6:40 UTC (permalink / raw)
  To: johannes, linux-kernel, linux-wireless, netdev, syzkaller-bugs

Hello,

syzbot found the following issue on:

HEAD commit:    d4f687fbbce4 ovpn: tcp - fix packet extraction from stream
git tree:       net
console output: https://syzkaller.appspot.com/x/log.txt?x=1763544a580000
kernel config:  https://syzkaller.appspot.com/x/.config?x=665cbf0979cda6c5
dashboard link: https://syzkaller.appspot.com/bug?extid=1b20ba3b6d47543a7742
compiler:       Debian clang version 21.1.8 (++20251221033036+2078da43e25a-1~exp1~20251221153213.50), Debian LLD 21.1.8

Unfortunately, I don't have any reproducer for this issue yet.

Downloadable assets:
disk image: https://storage.googleapis.com/syzbot-assets/ee42a61b2ee7/disk-d4f687fb.raw.xz
vmlinux: https://storage.googleapis.com/syzbot-assets/e5470ff18ff8/vmlinux-d4f687fb.xz
kernel image: https://storage.googleapis.com/syzbot-assets/d50957aaabf2/bzImage-d4f687fb.xz

IMPORTANT: if you fix the issue, please add the following tag to the commit:
Reported-by: syzbot+1b20ba3b6d47543a7742@syzkaller.appspotmail.com

======================================================
WARNING: possible circular locking dependency detected
syzkaller #0 Not tainted
------------------------------------------------------
syz.0.4107/21056 is trying to acquire lock:
ffff888055af0788 (&rdev->wiphy.mtx){+.+.}-{4:4}, at: class_wiphy_constructor include/net/cfg80211.h:6441 [inline]
ffff888055af0788 (&rdev->wiphy.mtx){+.+.}-{4:4}, at: ieee80211_change_mac+0xb5/0x1410 net/mac80211/iface.c:324

but task is already holding lock:
ffffffff8fbc12b0 (dev_addr_sem){++++}-{4:4}, at: do_setlink+0x806/0x4590 net/core/rtnetlink.c:3105

which lock already depends on the new lock.


the existing dependency chain (in reverse order) is:

-> #2 (dev_addr_sem){++++}-{4:4}:
       down_read+0x47/0x2e0 kernel/locking/rwsem.c:1537
       address_show+0x25/0x250 net/core/net-sysfs.c:266
       dev_attr_show+0x58/0xc0 drivers/base/core.c:2421
       sysfs_kf_seq_show+0x310/0x490 fs/sysfs/file.c:65
       seq_read_iter+0x4ef/0xe10 fs/seq_file.c:231
       new_sync_read fs/read_write.c:493 [inline]
       vfs_read+0x582/0xa70 fs/read_write.c:574
       ksys_read+0x150/0x270 fs/read_write.c:717
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x77/0x7f

-> #1 (kn->active#13){++++}-{0:0}:
       kernfs_drain+0x284/0x600 fs/kernfs/dir.c:511
       __kernfs_remove+0x3cf/0x660 fs/kernfs/dir.c:1513
       kernfs_remove_by_name_ns+0xaf/0x130 fs/kernfs/dir.c:1722
       kernfs_remove_by_name include/linux/kernfs.h:633 [inline]
       remove_files fs/sysfs/group.c:28 [inline]
       sysfs_remove_group+0xfc/0x2e0 fs/sysfs/group.c:328
       sysfs_remove_groups+0x54/0xb0 fs/sysfs/group.c:352
       device_remove_groups drivers/base/core.c:2843 [inline]
       device_remove_attrs+0x229/0x280 drivers/base/core.c:2979
       device_del+0x51f/0x8f0 drivers/base/core.c:3877
       unregister_netdevice_many_notify+0x1e0e/0x2370 net/core/dev.c:12438
       unregister_netdevice_many net/core/dev.c:12466 [inline]
       unregister_netdevice_queue+0x31f/0x360 net/core/dev.c:12280
       unregister_netdevice include/linux/netdevice.h:3408 [inline]
       _cfg80211_unregister_wdev+0x155/0x570 net/wireless/core.c:1297
       ieee80211_remove_interfaces+0x49c/0x6d0 net/mac80211/iface.c:2440
       ieee80211_unregister_hw+0x5d/0x2c0 net/mac80211/main.c:1691
       mac80211_hwsim_del_radio+0x28a/0x490 drivers/net/wireless/virtual/mac80211_hwsim.c:5916
       remove_user_radios drivers/net/wireless/virtual/mac80211_hwsim.c:6727 [inline]
       mac80211_hwsim_netlink_notify+0xfae/0x1310 drivers/net/wireless/virtual/mac80211_hwsim.c:6741
       notifier_call_chain+0x1be/0x400 kernel/notifier.c:85
       blocking_notifier_call_chain+0x6a/0x90 kernel/notifier.c:380
       netlink_release+0x123b/0x1ad0 net/netlink/af_netlink.c:761
       __sock_release net/socket.c:662 [inline]
       sock_close+0xc3/0x240 net/socket.c:1455
       __fput+0x44f/0xa70 fs/file_table.c:469
       task_work_run+0x1d9/0x270 kernel/task_work.c:233
       get_signal+0x11eb/0x1330 kernel/signal.c:2807
       arch_do_signal_or_restart+0xbc/0x830 arch/x86/kernel/signal.c:337
       __exit_to_user_mode_loop kernel/entry/common.c:64 [inline]
       exit_to_user_mode_loop+0x86/0x480 kernel/entry/common.c:98
       __exit_to_user_mode_prepare include/linux/irq-entry-common.h:226 [inline]
       syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:256 [inline]
       syscall_exit_to_user_mode include/linux/entry-common.h:325 [inline]
       do_syscall_64+0x32d/0xf80 arch/x86/entry/syscall_64.c:100
       entry_SYSCALL_64_after_hwframe+0x77/0x7f

-> #0 (&rdev->wiphy.mtx){+.+.}-{4:4}:
       check_prev_add kernel/locking/lockdep.c:3165 [inline]
       check_prevs_add kernel/locking/lockdep.c:3284 [inline]
       validate_chain kernel/locking/lockdep.c:3908 [inline]
       __lock_acquire+0x15a5/0x2cf0 kernel/locking/lockdep.c:5237
       lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
       __mutex_lock_common kernel/locking/mutex.c:614 [inline]
       __mutex_lock+0x19f/0x1300 kernel/locking/mutex.c:776
       class_wiphy_constructor include/net/cfg80211.h:6441 [inline]
       ieee80211_change_mac+0xb5/0x1410 net/mac80211/iface.c:324
       netif_set_mac_address+0x317/0x4e0 net/core/dev.c:9989
       do_setlink+0x9b1/0x4590 net/core/rtnetlink.c:3110
       rtnl_changelink net/core/rtnetlink.c:3776 [inline]
       __rtnl_newlink net/core/rtnetlink.c:3935 [inline]
       rtnl_newlink+0x15a9/0x1be0 net/core/rtnetlink.c:4072
       rtnetlink_rcv_msg+0x7d5/0xbe0 net/core/rtnetlink.c:6958
       netlink_rcv_skb+0x232/0x4b0 net/netlink/af_netlink.c:2550
       netlink_unicast_kernel net/netlink/af_netlink.c:1318 [inline]
       netlink_unicast+0x80f/0x9b0 net/netlink/af_netlink.c:1344
       netlink_sendmsg+0x813/0xb40 net/netlink/af_netlink.c:1894
       sock_sendmsg_nosec net/socket.c:727 [inline]
       __sock_sendmsg net/socket.c:742 [inline]
       ____sys_sendmsg+0xa68/0xad0 net/socket.c:2592
       ___sys_sendmsg+0x2a5/0x360 net/socket.c:2646
       __sys_sendmsg net/socket.c:2678 [inline]
       __do_sys_sendmsg net/socket.c:2683 [inline]
       __se_sys_sendmsg net/socket.c:2681 [inline]
       __x64_sys_sendmsg+0x1bd/0x2a0 net/socket.c:2681
       do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
       do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
       entry_SYSCALL_64_after_hwframe+0x77/0x7f

other info that might help us debug this:

Chain exists of:
  &rdev->wiphy.mtx --> kn->active#13 --> dev_addr_sem

 Possible unsafe locking scenario:

       CPU0                    CPU1
       ----                    ----
  lock(dev_addr_sem);
                               lock(kn->active#13);
                               lock(dev_addr_sem);
  lock(&rdev->wiphy.mtx);

 *** DEADLOCK ***

2 locks held by syz.0.4107/21056:
 #0: ffffffff8fbcb808 (rtnl_mutex){+.+.}-{4:4}, at: rtnl_lock net/core/rtnetlink.c:80 [inline]
 #0: ffffffff8fbcb808 (rtnl_mutex){+.+.}-{4:4}, at: rtnl_nets_lock net/core/rtnetlink.c:341 [inline]
 #0: ffffffff8fbcb808 (rtnl_mutex){+.+.}-{4:4}, at: rtnl_newlink+0x8a1/0x1be0 net/core/rtnetlink.c:4071
 #1: ffffffff8fbc12b0 (dev_addr_sem){++++}-{4:4}, at: do_setlink+0x806/0x4590 net/core/rtnetlink.c:3105

stack backtrace:
CPU: 0 UID: 0 PID: 21056 Comm: syz.0.4107 Not tainted syzkaller #0 PREEMPT(full) 
Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 02/12/2026
Call Trace:
 <TASK>
 dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
 print_circular_bug+0x2e1/0x300 kernel/locking/lockdep.c:2043
 check_noncircular+0x12e/0x150 kernel/locking/lockdep.c:2175
 check_prev_add kernel/locking/lockdep.c:3165 [inline]
 check_prevs_add kernel/locking/lockdep.c:3284 [inline]
 validate_chain kernel/locking/lockdep.c:3908 [inline]
 __lock_acquire+0x15a5/0x2cf0 kernel/locking/lockdep.c:5237
 lock_acquire+0xf0/0x2e0 kernel/locking/lockdep.c:5868
 __mutex_lock_common kernel/locking/mutex.c:614 [inline]
 __mutex_lock+0x19f/0x1300 kernel/locking/mutex.c:776
 class_wiphy_constructor include/net/cfg80211.h:6441 [inline]
 ieee80211_change_mac+0xb5/0x1410 net/mac80211/iface.c:324
 netif_set_mac_address+0x317/0x4e0 net/core/dev.c:9989
 do_setlink+0x9b1/0x4590 net/core/rtnetlink.c:3110
 rtnl_changelink net/core/rtnetlink.c:3776 [inline]
 __rtnl_newlink net/core/rtnetlink.c:3935 [inline]
 rtnl_newlink+0x15a9/0x1be0 net/core/rtnetlink.c:4072
 rtnetlink_rcv_msg+0x7d5/0xbe0 net/core/rtnetlink.c:6958
 netlink_rcv_skb+0x232/0x4b0 net/netlink/af_netlink.c:2550
 netlink_unicast_kernel net/netlink/af_netlink.c:1318 [inline]
 netlink_unicast+0x80f/0x9b0 net/netlink/af_netlink.c:1344
 netlink_sendmsg+0x813/0xb40 net/netlink/af_netlink.c:1894
 sock_sendmsg_nosec net/socket.c:727 [inline]
 __sock_sendmsg net/socket.c:742 [inline]
 ____sys_sendmsg+0xa68/0xad0 net/socket.c:2592
 ___sys_sendmsg+0x2a5/0x360 net/socket.c:2646
 __sys_sendmsg net/socket.c:2678 [inline]
 __do_sys_sendmsg net/socket.c:2683 [inline]
 __se_sys_sendmsg net/socket.c:2681 [inline]
 __x64_sys_sendmsg+0x1bd/0x2a0 net/socket.c:2681
 do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
 do_syscall_64+0x14d/0xf80 arch/x86/entry/syscall_64.c:94
 entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f909439c629
Code: ff c3 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 44 00 00 48 89 f8 48 89 f7 48 89 d6 48 89 ca 4d 89 c2 4d 89 c8 4c 8b 4c 24 08 0f 05 <48> 3d 01 f0 ff ff 73 01 c3 48 c7 c1 e8 ff ff ff f7 d8 64 89 01 48
RSP: 002b:00007f90951b3028 EFLAGS: 00000246 ORIG_RAX: 000000000000002e
RAX: ffffffffffffffda RBX: 00007f9094616090 RCX: 00007f909439c629
RDX: 0000000000000000 RSI: 0000200000000080 RDI: 0000000000000004
RBP: 00007f9094432b39 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f9094616128 R14: 00007f9094616090 R15: 00007fff601b4778
 </TASK>
netlink: 8 bytes leftover after parsing attributes in process `syz.0.4107'.
netlink: 'syz.0.4107': attribute type 5 has an invalid length.
netlink: 20 bytes leftover after parsing attributes in process `syz.0.4107'.
geneve2: entered promiscuous mode
geneve2: entered allmulticast mode


---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.

syzbot will keep track of this issue. See:
https://goo.gl/tpsmEJ#status for how to communicate with syzbot.

If the report is already addressed, let syzbot know by replying with:
#syz fix: exact-commit-title

If you want to overwrite report's subsystems, reply with:
#syz set subsystems: new-subsystem
(See the list of subsystem names on the web dashboard)

If the report is a duplicate of another one, reply with:
#syz dup: exact-subject-of-another-report

If you want to undo deduplication, reply with:
#syz undup

^ permalink raw reply

* [PATCH ath-next] wifi: ath12k: Remove the unused argument from the Rx data path
From: Karthikeyan Periyasamy @ 2026-02-27  4:21 UTC (permalink / raw)
  To: ath12k; +Cc: linux-wireless, Karthikeyan Periyasamy

Currently, the Rx path uses new infrastructure to extract the required HAL
parameters. Consequently, the HAL Rx descriptor argument is no longer
needed in the following helper functions. Remove the unused argument from
the following helper functions.

ath12k_dp_rx_h_undecap()
ath12k_dp_rx_check_nwifi_hdr_len_valid()
ath12k_wifi7_dp_rx_h_mpdu()

Tested-on: QCN9274 hw2.0 PCI WLAN.WBE.1.6-01243-QCAHKSWPL_SILICONZ-1

Signed-off-by: Karthikeyan Periyasamy <karthikeyan.periyasamy@oss.qualcomm.com>
---
 drivers/net/wireless/ath/ath12k/dp_rx.c       |  2 --
 drivers/net/wireless/ath/ath12k/dp_rx.h       |  2 --
 drivers/net/wireless/ath/ath12k/wifi7/dp_rx.c | 25 ++++++++-----------
 3 files changed, 11 insertions(+), 18 deletions(-)

diff --git a/drivers/net/wireless/ath/ath12k/dp_rx.c b/drivers/net/wireless/ath/ath12k/dp_rx.c
index a32ee9f8061a..c0e2b2c1a292 100644
--- a/drivers/net/wireless/ath/ath12k/dp_rx.c
+++ b/drivers/net/wireless/ath/ath12k/dp_rx.c
@@ -1117,7 +1117,6 @@ static void ath12k_dp_rx_h_undecap_eth(struct ath12k_pdev_dp *dp_pdev,
 }
 
 void ath12k_dp_rx_h_undecap(struct ath12k_pdev_dp *dp_pdev, struct sk_buff *msdu,
-			    struct hal_rx_desc *rx_desc,
 			    enum hal_encrypt_type enctype,
 			    bool decrypted,
 			    struct hal_rx_desc_data *rx_info)
@@ -1393,7 +1392,6 @@ void ath12k_dp_rx_deliver_msdu(struct ath12k_pdev_dp *dp_pdev, struct napi_struc
 EXPORT_SYMBOL(ath12k_dp_rx_deliver_msdu);
 
 bool ath12k_dp_rx_check_nwifi_hdr_len_valid(struct ath12k_dp *dp,
-					    struct hal_rx_desc *rx_desc,
 					    struct sk_buff *msdu,
 					    struct hal_rx_desc_data *rx_info)
 {
diff --git a/drivers/net/wireless/ath/ath12k/dp_rx.h b/drivers/net/wireless/ath/ath12k/dp_rx.h
index 1ec5382f5995..bd62af0c80d4 100644
--- a/drivers/net/wireless/ath/ath12k/dp_rx.h
+++ b/drivers/net/wireless/ath/ath12k/dp_rx.h
@@ -189,7 +189,6 @@ void ath12k_dp_extract_rx_desc_data(struct ath12k_hal *hal,
 }
 
 void ath12k_dp_rx_h_undecap(struct ath12k_pdev_dp *dp_pdev, struct sk_buff *msdu,
-			    struct hal_rx_desc *rx_desc,
 			    enum hal_encrypt_type enctype,
 			    bool decrypted,
 			    struct hal_rx_desc_data *rx_info);
@@ -197,7 +196,6 @@ void ath12k_dp_rx_deliver_msdu(struct ath12k_pdev_dp *dp_pdev, struct napi_struc
 			       struct sk_buff *msdu,
 			       struct hal_rx_desc_data *rx_info);
 bool ath12k_dp_rx_check_nwifi_hdr_len_valid(struct ath12k_dp *dp,
-					    struct hal_rx_desc *rx_desc,
 					    struct sk_buff *msdu,
 					    struct hal_rx_desc_data *rx_info);
 u64 ath12k_dp_rx_h_get_pn(struct ath12k_dp *dp, struct sk_buff *skb);
diff --git a/drivers/net/wireless/ath/ath12k/wifi7/dp_rx.c b/drivers/net/wireless/ath/ath12k/wifi7/dp_rx.c
index 7450938adf65..77a1679b41df 100644
--- a/drivers/net/wireless/ath/ath12k/wifi7/dp_rx.c
+++ b/drivers/net/wireless/ath/ath12k/wifi7/dp_rx.c
@@ -325,7 +325,6 @@ static void ath12k_wifi7_dp_rx_h_csum_offload(struct sk_buff *msdu,
 
 static void ath12k_wifi7_dp_rx_h_mpdu(struct ath12k_pdev_dp *dp_pdev,
 				      struct sk_buff *msdu,
-				      struct hal_rx_desc *rx_desc,
 				      struct hal_rx_desc_data *rx_info)
 {
 	struct ath12k_skb_rxcb *rxcb;
@@ -388,8 +387,7 @@ static void ath12k_wifi7_dp_rx_h_mpdu(struct ath12k_pdev_dp *dp_pdev,
 	}
 
 	ath12k_wifi7_dp_rx_h_csum_offload(msdu, rx_info);
-	ath12k_dp_rx_h_undecap(dp_pdev, msdu, rx_desc,
-			       enctype, is_decrypted, rx_info);
+	ath12k_dp_rx_h_undecap(dp_pdev, msdu, enctype, is_decrypted, rx_info);
 
 	if (!is_decrypted || rx_info->is_mcbc)
 		return;
@@ -549,14 +547,14 @@ static int ath12k_wifi7_dp_rx_process_msdu(struct ath12k_pdev_dp *dp_pdev,
 		}
 	}
 
-	if (unlikely(!ath12k_dp_rx_check_nwifi_hdr_len_valid(dp, rx_desc, msdu,
+	if (unlikely(!ath12k_dp_rx_check_nwifi_hdr_len_valid(dp, msdu,
 							     rx_info))) {
 		ret = -EINVAL;
 		goto free_out;
 	}
 
 	ath12k_dp_rx_h_ppdu(dp_pdev, rx_info);
-	ath12k_wifi7_dp_rx_h_mpdu(dp_pdev, msdu, rx_desc, rx_info);
+	ath12k_wifi7_dp_rx_h_mpdu(dp_pdev, msdu, rx_info);
 
 	rx_info->rx_status->flag |= RX_FLAG_SKIP_MONITOR | RX_FLAG_DUP_VALIDATED;
 
@@ -1030,13 +1028,13 @@ static int ath12k_wifi7_dp_rx_h_verify_tkip_mic(struct ath12k_pdev_dp *dp_pdev,
 		    RX_FLAG_IV_STRIPPED | RX_FLAG_DECRYPTED;
 	skb_pull(msdu, hal_rx_desc_sz);
 
-	if (unlikely(!ath12k_dp_rx_check_nwifi_hdr_len_valid(dp, rx_desc, msdu,
+	if (unlikely(!ath12k_dp_rx_check_nwifi_hdr_len_valid(dp, msdu,
 							     rx_info)))
 		return -EINVAL;
 
 	ath12k_dp_rx_h_ppdu(dp_pdev, rx_info);
-	ath12k_dp_rx_h_undecap(dp_pdev, msdu, rx_desc,
-			       HAL_ENCRYPT_TYPE_TKIP_MIC, true, rx_info);
+	ath12k_dp_rx_h_undecap(dp_pdev, msdu, HAL_ENCRYPT_TYPE_TKIP_MIC, true,
+			       rx_info);
 	ieee80211_rx(ath12k_pdev_dp_to_hw(dp_pdev), msdu);
 	return -EINVAL;
 }
@@ -1588,7 +1586,6 @@ static int ath12k_wifi7_dp_rx_h_null_q_desc(struct ath12k_pdev_dp *dp_pdev,
 	struct ath12k_dp *dp = dp_pdev->dp;
 	struct ath12k_base *ab = dp->ab;
 	u16 msdu_len = rx_info->msdu_len;
-	struct hal_rx_desc *desc = (struct hal_rx_desc *)msdu->data;
 	u8 l3pad_bytes = rx_info->l3_pad_bytes;
 	struct ath12k_skb_rxcb *rxcb = ATH12K_SKB_RXCB(msdu);
 	u32 hal_rx_desc_sz = dp->ab->hal.hal_desc_sz;
@@ -1632,11 +1629,11 @@ static int ath12k_wifi7_dp_rx_h_null_q_desc(struct ath12k_pdev_dp *dp_pdev,
 		skb_put(msdu, hal_rx_desc_sz + l3pad_bytes + msdu_len);
 		skb_pull(msdu, hal_rx_desc_sz + l3pad_bytes);
 	}
-	if (unlikely(!ath12k_dp_rx_check_nwifi_hdr_len_valid(dp, desc, msdu, rx_info)))
+	if (unlikely(!ath12k_dp_rx_check_nwifi_hdr_len_valid(dp, msdu, rx_info)))
 		return -EINVAL;
 
 	ath12k_dp_rx_h_ppdu(dp_pdev, rx_info);
-	ath12k_wifi7_dp_rx_h_mpdu(dp_pdev, msdu, desc, rx_info);
+	ath12k_wifi7_dp_rx_h_mpdu(dp_pdev, msdu, rx_info);
 
 	rxcb->tid = rx_info->tid;
 
@@ -1673,7 +1670,7 @@ static bool ath12k_wifi7_dp_rx_h_tkip_mic_err(struct ath12k_pdev_dp *dp_pdev,
 	skb_put(msdu, hal_rx_desc_sz + l3pad_bytes + msdu_len);
 	skb_pull(msdu, hal_rx_desc_sz + l3pad_bytes);
 
-	if (unlikely(!ath12k_dp_rx_check_nwifi_hdr_len_valid(dp, desc, msdu, rx_info)))
+	if (unlikely(!ath12k_dp_rx_check_nwifi_hdr_len_valid(dp, msdu, rx_info)))
 		return true;
 
 	ath12k_dp_rx_h_ppdu(dp_pdev, rx_info);
@@ -1681,8 +1678,8 @@ static bool ath12k_wifi7_dp_rx_h_tkip_mic_err(struct ath12k_pdev_dp *dp_pdev,
 	rx_info->rx_status->flag |= (RX_FLAG_MMIC_STRIPPED | RX_FLAG_MMIC_ERROR |
 				     RX_FLAG_DECRYPTED);
 
-	ath12k_dp_rx_h_undecap(dp_pdev, msdu, desc,
-			       HAL_ENCRYPT_TYPE_TKIP_MIC, false, rx_info);
+	ath12k_dp_rx_h_undecap(dp_pdev, msdu, HAL_ENCRYPT_TYPE_TKIP_MIC, false,
+			       rx_info);
 	return false;
 }
 

base-commit: ff49eba595df500e4ddccc593088c8a4ab5f2c27
-- 
2.34.1


^ permalink raw reply related

* [PATCH wireless-next 35/35] wifi: mm81x: add MAINTAINERS entry
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes
  Cc: arien.judge, dan.callaghan, ayman.grais, linux-wireless,
	Lachlan Hodges, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

Add an entry for mm81x to the MAINTAINERS file.

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 MAINTAINERS | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index b8d8a5c41597..14d890d7f859 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17850,6 +17850,15 @@ F:	drivers/regulator/mpq7920.c
 F:	drivers/regulator/mpq7920.h
 F:	include/linux/mfd/mp2629.h
 
+MORSE MICRO MM81X WIRELESS DRIVER
+M:	Lachlan Hodges <lachlan.hodges@morsemicro.com>
+M:	Dan Callaghan <dan.callaghan@morsemicro.com>
+R:	Arien Judge <arien.judge@morsemicro.com>
+L:	linux-wireless@vger.kernel.org
+S:	Supported
+F:	Documentation/devicetree/bindings/net/wireless/morsemicro,mm81x.yaml
+F:	drivers/net/wireless/morsemicro/mm81x/
+
 MOST(R) TECHNOLOGY DRIVER
 M:	Parthiban Veerasooran <parthiban.veerasooran@microchip.com>
 M:	Christian Gromm <christian.gromm@microchip.com>
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 34/35] wifi: mm81x: add Kconfig and Makefile
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, 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 | 34 +++++++++++++++++++
 .../net/wireless/morsemicro/mm81x/Makefile    | 19 +++++++++++
 6 files changed, 72 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..b61953096dfc
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/Kconfig
@@ -0,0 +1,34 @@
+# SPDX-License-Identifier: GPL-2.0
+
+config MM81X
+	tristate "Morse Micro MM81x wireless driver"
+	depends on MAC80211
+	select FW_LOADER
+	select CRC7
+	help
+	  This enables the Morse Micro MM81x wireless driver.
+
+	  The driver supports USB and SDIO attached devices. Enable at least
+	  one of the bus options below to register the corresponding bus
+	  driver.
+
+if MM81X
+
+config MM81X_USB
+	bool "USB bus support"
+	depends on USB
+	help
+	  Enable USB bus support for Morse Micro MM81x devices.
+
+config MM81X_SDIO
+	bool "SDIO bus support"
+	depends on MMC
+	help
+	  Enable SDIO bus support for Morse Micro MM81x devices.
+
+config MM81X_DEBUG
+	bool "Enable debug logging"
+	help
+	  Enable extra debug logging and debug dump helpers.
+
+endif
diff --git a/drivers/net/wireless/morsemicro/mm81x/Makefile b/drivers/net/wireless/morsemicro/mm81x/Makefile
new file mode 100644
index 000000000000..b580d200b94a
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/Makefile
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: GPL-2.0
+
+obj-$(CONFIG_MM81X) += mm81x.o
+
+mm81x-y += core.o
+mm81x-y += mac.o
+mm81x-y += hw.o
+mm81x-y += debug.o
+mm81x-y += fw.o
+mm81x-y += command.o
+mm81x-y += ps.o
+mm81x-y += skbq.o
+mm81x-y += yaps_hw.o
+mm81x-y += yaps.o
+mm81x-y += rc.o
+mm81x-y += mmrc.o
+
+mm81x-$(CONFIG_MM81X_USB) += usb.o
+mm81x-$(CONFIG_MM81X_SDIO) += sdio.o
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 33/35] mmc: sdio: add Morse Micro vendor ids
From: Lachlan Hodges @ 2026-02-27  4:10 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: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

Add the Morse Micro mm81x series vendor ids.

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 include/linux/mmc/sdio_ids.h | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/include/linux/mmc/sdio_ids.h b/include/linux/mmc/sdio_ids.h
index 673cbdf43453..3ccfe3679c91 100644
--- a/include/linux/mmc/sdio_ids.h
+++ b/include/linux/mmc/sdio_ids.h
@@ -116,6 +116,10 @@
 #define SDIO_VENDOR_ID_MICROCHIP_WILC		0x0296
 #define SDIO_DEVICE_ID_MICROCHIP_WILC1000	0x5347
 
+#define SDIO_VENDOR_ID_MORSEMICRO		0x325B
+#define SDIO_VENDOR_ID_MORSEMICRO_MM81XB1	0x0709
+#define SDIO_VENDOR_ID_MORSEMICRO_MM81XB2	0x0809
+
 #define SDIO_VENDOR_ID_REALTEK			0x024c
 #define SDIO_DEVICE_ID_REALTEK_RTW8723BS	0xb723
 #define SDIO_DEVICE_ID_REALTEK_RTW8821BS	0xb821
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 32/35] dt-bindings: net: wireless: morsemicro: add mm81x family
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: ayman.grais, linux-wireless, devicetree, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

Add dt-bindings describing the Morse Micro mm81x family of
chips.

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 .../net/wireless/morsemicro,mm81x.yaml        | 74 +++++++++++++++++++
 1 file changed, 74 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/net/wireless/morsemicro,mm81x.yaml

diff --git a/Documentation/devicetree/bindings/net/wireless/morsemicro,mm81x.yaml b/Documentation/devicetree/bindings/net/wireless/morsemicro,mm81x.yaml
new file mode 100644
index 000000000000..653a7476cf8f
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/wireless/morsemicro,mm81x.yaml
@@ -0,0 +1,74 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/wireless/morsemicro,mm81x.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Morse Micro MM81x
+
+maintainers:
+  - Lachlan Hodges <lachlan.hodges@morsemicro.com>
+  - Arien Judge <arien.judge@morsemicro.com>
+
+description: >
+  This node provides properties for configuring a Morse Micro MM81x device
+  connected via SDIO. The node shall be specified as a child node of an SDIO
+  controller.
+
+  It is recommended to declare a mmc-pwrseq on SDIO host above MM81x. Without
+  it, you may encounter issues during reboot. The mmc-pwrseq should be
+  compatible with mmc-pwrseq-simple. Please consult
+  Documentation/devicetree/bindings/mmc/mmc-pwrseq-simple.yaml for more
+  information.
+
+properties:
+  compatible:
+    items:
+      - const: morsemicro,mm81x
+
+  reg:
+    description:
+      <reg> must be set to 2.
+    maxItems: 1
+
+  wake-gpios:
+    description: Phandle of gpio that will be used to wake up the chip. Powersave
+      features disabled if property not present.
+    maxItems: 1
+
+  busy-gpios:
+    description: Phandle of a gpio that is used to indicate the chip has data
+      ready.
+
+required:
+  - compatible
+  - reg
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+
+    mm81x_pwrseq: mm81x_pwrseq {
+        compatible = "mmc-pwrseq-simple";
+        pinctrl-names = "default";
+        pinctrl-0 = <&mm81x_reset>;
+        reset-gpios = <&gpio 13 GPIO_ACTIVE_LOW>;
+    };
+
+    mmc {
+        mmc-pwrseq = <&mm81x_pwrseq>;
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        wifi@0 {
+            compatible = "morsemicro,mm81x";
+            pinctrl-names = "default";
+            pinctrl-0 = <&mm81x_busy>, <&mm81x_wake>;
+            reg = <2>;
+            wake-gpios = <&gpio 12 GPIO_ACTIVE_HIGH>;
+            busy-gpios = <&gpio 11 GPIO_ACTIVE_HIGH>;
+        };
+    };
+...
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 31/35] dt-bindings: vendor-prefixes: add Morse Micro
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Rob Herring, Krzysztof Kozlowski, Conor Dooley
  Cc: arien.judge, dan.callaghan, ayman.grais, linux-wireless,
	Lachlan Hodges, devicetree, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

Add vendor prefix for Morse Micro Pty Ltd

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index ee7fd3cfe203..992edf0f40c2 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -1078,6 +1078,8 @@ patternProperties:
     description: Modtronix Engineering
   "^moortec,.*":
     description: Moortec Semiconductor Ltd.
+  "^morsemicro,.*":
+    description: Morse Micro Pty. Ltd.
   "^mosaixtech,.*":
     description: Mosaix Technologies, Inc.
   "^motorcomm,.*":
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 30/35] wifi: mm81x: add yaps_hw.h
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, 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..8a04ced01fa2
--- /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 *mm, bool enable);
+int mm81x_yaps_hw_init(struct mm81x *mm);
+void mm81x_yaps_hw_finish(struct mm81x *mm);
+void mm81x_yaps_hw_read_table(struct mm81x *mm,
+			      struct mm81x_yaps_hw_table *tbl_ptr);
+
+#endif /* !_MM81X_YAPS_HW_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 29/35] wifi: mm81x: add yaps_hw.c
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 .../net/wireless/morsemicro/mm81x/yaps_hw.c   | 683 ++++++++++++++++++
 1 file changed, 683 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..6601faa8b38b
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/yaps_hw.c
@@ -0,0 +1,683 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include "yaps_hw.h"
+#include "bus.h"
+#include "debug.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_hw_status_registers {
+	/* 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;
+
+	/* To chip/From chip queues for YDS/YSL */
+	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_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 for queues and aloc pools on chip */
+	struct mm81x_yaps_hw_status_registers 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 *mm, bool enable)
+{
+	mm81x_hw_irq_enable(mm, MM81X_INT_YAPS_FC_PKT_WAITING_IRQN, enable);
+	mm81x_hw_irq_enable(mm, MM81X_INT_YAPS_FC_PACKET_FREED_UP_IRQN, enable);
+}
+
+void mm81x_yaps_hw_read_table(struct mm81x *mm,
+			      struct mm81x_yaps_hw_table *tbl_ptr)
+{
+	mm81x_yaps_hw_fill_aux_data_from_hw_tbl(mm->hif.u.yaps.aux_data,
+						tbl_ptr);
+	mm81x_yaps_hw_enable_irqs(mm, 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:
+		mm81x_err(yaps->mm, "yaps invalid tc queue");
+	}
+
+	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->mm));
+	char *write_buf = to_chip_buffer_aligned;
+
+	ret = mm81x_yaps_hw_lock(yaps);
+	if (ret) {
+		mm81x_dbg(yaps->mm, MM81X_DBG_ANY, "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->mm, 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->mm, 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->mm));
+	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) {
+		mm81x_dbg(yaps->mm, MM81X_DBG_ANY, "yaps lock failed %d", ret);
+		return ret;
+	}
+
+	/* Read all available packets to the buffer */
+	ret = mm81x_dm_read(yaps->mm, 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.
+			 */
+			mm81x_warn(yaps->mm, "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)
+			mm81x_err(yaps->mm, "yaps packet leak");
+
+		/* SKB doesn't want padding */
+		pkts[i].skb = dev_alloc_skb(pkt_size);
+		if (!pkts[i].skb) {
+			ret = -ENOMEM;
+			mm81x_err(yaps->mm, "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->mm,
+				/* 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_hw_status_registers *r = &yaps->aux_data->status_regs;
+
+	ret = mm81x_yaps_hw_lock(yaps);
+	if (ret) {
+		mm81x_dbg(yaps->mm, MM81X_DBG_ANY, "yaps lock failed %d", ret);
+		return ret;
+	}
+
+	reg_read_timeout = jiffies + msecs_to_jiffies(100);
+	do {
+		if (time_after(jiffies, reg_read_timeout)) {
+			mm81x_err(yaps->mm,
+				  "timed out reading status registers: %d",
+				  ret);
+			ret = -ETIMEDOUT;
+			break;
+		}
+
+		ret = mm81x_dm_read(yaps->mm, yaps->aux_data->status_regs_addr,
+				    (u8 *)r, sizeof(*r));
+	} while (!ret && r->lock);
+
+	if (ret) {
+		if (ret != -ENODEV) {
+			mm81x_err(yaps->mm,
+				  "error reading yaps status registers: %d",
+				  ret);
+		}
+		goto exit_unlock;
+	}
+
+	r->tc_tx_pool_num_pages = mm81x_fle32_to_cpu(r->tc_tx_pool_num_pages);
+	r->tc_cmd_pool_num_pages = mm81x_fle32_to_cpu(r->tc_cmd_pool_num_pages);
+	r->tc_beacon_pool_num_pages =
+		mm81x_fle32_to_cpu(r->tc_beacon_pool_num_pages);
+	r->tc_mgmt_pool_num_pages =
+		mm81x_fle32_to_cpu(r->tc_mgmt_pool_num_pages);
+	r->fc_rx_pool_num_pages = mm81x_fle32_to_cpu(r->fc_rx_pool_num_pages);
+	r->fc_resp_pool_num_pages =
+		mm81x_fle32_to_cpu(r->fc_resp_pool_num_pages);
+	r->fc_tx_sts_pool_num_pages =
+		mm81x_fle32_to_cpu(r->fc_tx_sts_pool_num_pages);
+	r->fc_aux_pool_num_pages = mm81x_fle32_to_cpu(r->fc_aux_pool_num_pages);
+	r->tc_tx_num_pkts = mm81x_fle32_to_cpu(r->tc_tx_num_pkts);
+	r->tc_cmd_num_pkts = mm81x_fle32_to_cpu(r->tc_cmd_num_pkts);
+	r->tc_beacon_num_pkts = mm81x_fle32_to_cpu(r->tc_beacon_num_pkts);
+	r->tc_mgmt_num_pkts = mm81x_fle32_to_cpu(r->tc_mgmt_num_pkts);
+	r->fc_num_pkts = mm81x_fle32_to_cpu(r->fc_num_pkts);
+	r->fc_done_num_pkts = mm81x_fle32_to_cpu(r->fc_done_num_pkts);
+	r->fc_rx_bytes_in_queue = mm81x_fle32_to_cpu(r->fc_rx_bytes_in_queue);
+	r->tc_delim_crc_fail_detected =
+		mm81x_fle32_to_cpu(r->tc_delim_crc_fail_detected);
+	r->lock = mm81x_fle32_to_cpu(r->lock);
+	r->fc_host_ysl_status = mm81x_fle32_to_cpu(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.
+		 */
+		mm81x_err(yaps->mm,
+			  "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->mm->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 *mm)
+{
+	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(mm);
+
+	yaps = &mm->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)) {
+		mm81x_warn(mm, "Status registers are not aligned to %d bytes",
+			   alignment);
+	}
+
+	yaps->ops = &mm81x_yaps_hw_ops;
+	return ret;
+
+err_exit:
+	mm81x_yaps_hw_finish(mm);
+	return ret;
+}
+
+void mm81x_yaps_hw_finish(struct mm81x *mm)
+{
+	struct mm81x_yaps *yaps;
+
+	yaps = &mm->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 28/35] wifi: mm81x: add yaps.h
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, 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..a362c58c2a15
--- /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 *mm;
+	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 *mm);
+void mm81x_yaps_show(struct mm81x_yaps *yaps, struct seq_file *file);
+void mm81x_yaps_finish(struct mm81x *mm);
+void mm81x_yaps_flush_tx_data(struct mm81x *mm);
+void mm81x_yaps_flush_cmds(struct mm81x *mm);
+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 *mm);
+int mm81x_yaps_get_tx_buffered_count(struct mm81x *mm);
+
+#endif /* !_MM81X_YAPS_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 27/35] wifi: mm81x: add yaps.c
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, 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..2a5edff20299
--- /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 "debug.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 inline 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 inline 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 inline 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 *mm, int aci)
+{
+	struct mm81x_yaps *yaps = &mm->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 *mm, struct mm81x_skbq **qs,
+				 int *num_qs)
+{
+	*qs = mm->hif.u.yaps.data_tx_qs;
+	*num_qs = YAPS_TX_SKBQ_MAX;
+}
+
+static struct mm81x_skbq *mm81x_yaps_get_bcn_tc_q(struct mm81x *mm)
+{
+	return &mm->hif.u.yaps.beacon_q;
+}
+
+static struct mm81x_skbq *mm81x_yaps_get_mgmt_tc_q(struct mm81x *mm)
+{
+	return &mm->hif.u.yaps.mgmt_q;
+}
+
+static struct mm81x_skbq *mm81x_yaps_get_tx_cmd_queue(struct mm81x *mm)
+{
+	return &mm->hif.u.yaps.cmd_q;
+}
+
+static int mm81x_yaps_irq_handler(struct mm81x *mm, u32 status)
+{
+	if (status & BIT(MM81X_INT_YAPS_FC_PKT_WAITING_IRQN))
+		set_bit(MM81X_HIF_EVT_RX_PEND, &mm->hif.event_flags);
+
+	if (status & BIT(MM81X_INT_YAPS_FC_PACKET_FREED_UP_IRQN)) {
+		timer_delete_sync_try(&mm->hif.u.yaps.chip_queue_full.timer);
+		set_bit(MM81X_HIF_EVT_TX_PACKET_FREED_UP_PEND,
+			&mm->hif.event_flags);
+	}
+
+	queue_work(mm->chip_wq, &mm->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 *mm = yaps->mm;
+	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) {
+		mm81x_err(mm, "sync value error [0xAA:%d], hdr.len %d",
+			  hdr->sync, hdr->len);
+		ret = -EIO;
+		goto exit_return_page;
+	}
+
+	if (yaps->mm->hif.validate_skb_checksum &&
+	    !mm81x_skbq_validate_checksum(skb->data)) {
+		mm81x_dbg(yaps->mm, MM81X_DBG_ANY,
+			  "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:
+		mm81x_err(mm, "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) {
+		mm81x_err(
+			mm,
+			"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(mm->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(mm->net_wq, &mq->dispatch_work);
+
+	goto exit;
+
+exit_return_page:
+	if (ret && mq) {
+		mm81x_err(mm, "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 *mm = yaps->mm;
+	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(mm, 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) {
+			mm81x_warn(mm, "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 *mm = yaps->mm;
+
+	for (aci = MM81X_ACI_VO; aci >= 0; aci--) {
+		struct mm81x_skbq *data_q = mm81x_yaps_tc_q_from_aci(mm, aci);
+
+		if (!mm81x_is_data_tx_allowed(mm))
+			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(mm);
+
+	return (count > 0) && mm81x_is_data_tx_allowed(mm);
+}
+
+/* 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) {
+		mm81x_err(yaps->mm, "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 *mm = container_of(work, struct mm81x, tx_stale_work);
+	struct mm81x_yaps *yaps;
+
+	yaps = &mm->hif.u.yaps;
+	flushed += mm81x_skbq_check_for_stale_tx(mm, &yaps->beacon_q);
+	flushed += mm81x_skbq_check_for_stale_tx(mm, &yaps->mgmt_q);
+
+	for (i = 0; i < ARRAY_SIZE(yaps->data_tx_qs); i++)
+		flushed +=
+			mm81x_skbq_check_for_stale_tx(mm, &yaps->data_tx_qs[i]);
+
+	if (!flushed)
+		return;
+
+	mm81x_dbg(mm, MM81X_DBG_ANY, "Flushed %d stale TX SKBs", flushed);
+
+	if (mm->ps.enable && !mm->ps.suspended &&
+	    (mm81x_yaps_get_tx_buffered_count(mm) == 0)) {
+		/* Evaluate ps to check if it was gated on a stale tx status */
+		queue_delayed_work(mm->chip_wq, &mm->ps.delayed_eval_work, 0);
+	}
+}
+
+void mm81x_yaps_work(struct work_struct *work)
+{
+	struct mm81x *mm = container_of(work, struct mm81x, hif_work);
+	unsigned long *flags = &mm->hif.event_flags;
+	struct mm81x_yaps *yaps = &mm->hif.u.yaps;
+
+	if (test_bit(MM81X_STATE_CHIP_UNRESPONSIVE, &mm->state_flags))
+		return;
+
+	if (!*flags)
+		return;
+
+	/* Disable power save in case it is running */
+	mm81x_ps_disable(mm);
+	mm81x_claim_bus(mm);
+
+	/*
+	 * 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(mm);
+	}
+
+	/* Resume TX data Qs  */
+	if (test_and_clear_bit(MM81X_HIF_EVT_DATA_TRAFFIC_RESUME_PEND, flags))
+		mm81x_skbq_data_traffic_resume(mm);
+
+	/* 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(mm);
+	mm81x_ps_enable(mm);
+
+	/* 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(mm->chip_wq, &mm->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(mm->chip_wq, &mm->hif_work);
+}
+
+int mm81x_yaps_get_tx_status_pending_count(struct mm81x *mm)
+{
+	int i = 0;
+	int count = 0;
+	struct mm81x_yaps *yaps;
+
+	yaps = &mm->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 *mm)
+{
+	int i = 0;
+	int count = 0;
+	struct mm81x_yaps *yaps;
+
+	yaps = &mm->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->mm->chip_wq, &yaps->mm->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 *mm)
+{
+	int i, ret;
+	struct mm81x_yaps *yaps;
+
+	ret = mm81x_yaps_hw_init(mm);
+	if (ret) {
+		mm81x_err(mm, "mm81x_yaps_hw_init failed %d", ret);
+		return ret;
+	}
+
+	yaps = &mm->hif.u.yaps;
+	yaps->mm = mm;
+
+	mm81x_claim_bus(mm);
+
+	ret = mm81x_yaps_alloc_pkt_buffers(yaps);
+	if (ret) {
+		mm81x_err(mm, "Failed to allocate YAPS packet buffers: %d",
+			  ret);
+		mm81x_yaps_hw_finish(mm);
+		mm81x_release_bus(mm);
+		return ret;
+	}
+
+	/* YAPS is bi-directional */
+	mm81x_skbq_init(mm, &yaps->data_rx_q,
+			MM81X_HIF_FLAGS_DATA | MM81X_HIF_FLAGS_DIR_TO_HOST);
+	mm81x_skbq_init(mm, &yaps->beacon_q,
+			MM81X_HIF_FLAGS_DATA | MM81X_HIF_FLAGS_DIR_TO_HOST);
+	mm81x_skbq_init(mm, &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(mm, &yaps->data_tx_qs[i],
+				MM81X_HIF_FLAGS_DATA |
+					MM81X_HIF_FLAGS_DIR_TO_CHIP);
+	}
+
+	mm81x_skbq_init(mm, &yaps->cmd_q,
+			MM81X_HIF_FLAGS_COMMAND | MM81X_HIF_FLAGS_DIR_TO_CHIP);
+	mm81x_skbq_init(mm, &yaps->cmd_resp_q,
+			MM81X_HIF_FLAGS_COMMAND | MM81X_HIF_FLAGS_DIR_TO_HOST);
+
+	mm81x_yaps_q_chip_full_timer_init(yaps);
+	INIT_WORK(&mm->hif_work, mm81x_yaps_work);
+	INIT_WORK(&mm->tx_stale_work, mm81x_yaps_stale_tx_work);
+	mm81x_release_bus(mm);
+	mm81x_hw_enable_stop_notifications(mm, true);
+	return 0;
+}
+
+void mm81x_yaps_finish(struct mm81x *mm)
+{
+	int i;
+	struct mm81x_yaps *yaps;
+
+	mm81x_yaps_hw_enable_irqs(mm, false);
+
+	yaps = &mm->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(&mm->hif_work);
+	cancel_work_sync(&mm->tx_stale_work);
+
+	mm81x_yaps_free_pkt_buffers(yaps);
+	mm81x_yaps_hw_finish(mm);
+}
+
+void mm81x_yaps_flush_tx_data(struct mm81x *mm)
+{
+	int i;
+	struct mm81x_yaps *yaps = &mm->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 *mm)
+{
+	struct mm81x_yaps *yaps = &mm->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 26/35] wifi: mm81x: add usb.c
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/usb.c | 971 ++++++++++++++++++++
 1 file changed, 971 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..1a08a2eceadf
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/usb.c
@@ -0,0 +1,971 @@
+// 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 "debug.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 *mm = container_of(work, struct mm81x, usb_irq_work);
+
+	mm81x_claim_bus(mm);
+	mm81x_hw_irq_handle(mm);
+	mm81x_release_bus(mm);
+}
+
+/*
+ * See https://www.kernel.org/doc/html/v5.15/driver-api/usb/error-codes.html
+ * Error codes returned by in urb->status which indicate disconnect.
+ */
+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 *mm = urb->context;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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,
+				&mm->state_flags);
+			mm81x_dbg(mm, MM81X_DBG_USB,
+				  "USB sudden disconnect detected in %s",
+				  __func__);
+			return;
+		}
+
+		if (!(urb->status == -ENOENT || urb->status == -ECONNRESET ||
+		      urb->status == -ESHUTDOWN))
+			mm81x_err(mm, "- 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)
+		mm81x_err(mm, "error: resubmit urb %p err code %d", urb, ret);
+
+	queue_work(mm->chip_wq, &mm->usb_irq_work);
+}
+
+static int mm81x_usb_int_enable(struct mm81x *mm)
+{
+	int ret = 0;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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) {
+		mm81x_err(mm, "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, mm, MM81X_USB_INTERRUPT_INTERVAL);
+	urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+	ret = usb_submit_urb(urb, GFP_KERNEL);
+	if (ret) {
+		mm81x_err(mm, "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 *mm)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->drv_priv;
+
+	usb_kill_urb(musb->endpoints[MM81X_EP_INT].urb);
+	cancel_work_sync(&mm->usb_irq_work);
+}
+
+static void mm81x_usb_cmd_callback(struct urb *urb)
+{
+	struct mm81x *mm = urb->context;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->drv_priv;
+
+	/* sync/async unlink faults aren't errors */
+	if (urb->status) {
+		if (!(urb->status == -ENOENT || urb->status == -ECONNRESET ||
+		      urb->status == -ESHUTDOWN))
+			mm81x_err(mm, "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 *mm = 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, mm);
+	ep->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+	musb->ongoing_cmd = true;
+
+	retval = usb_submit_urb(ep->urb, GFP_KERNEL);
+	if (retval) {
+		mm81x_err(mm, "- 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) {
+		mm81x_err(mm, "error waiting for urb %d", retval);
+		goto error;
+	} else if (retval == 0) {
+		mm81x_err(mm, "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;
+}
+
+/* Non-destructive USB reset */
+int mm81x_usb_ndr_reset(struct mm81x *mm)
+{
+	int ret;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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)
+		mm81x_err(mm, "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 *mm = urb->context;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->drv_priv;
+
+	/* sync/async unlink faults aren't errors */
+	if (urb->status) {
+		if (!(urb->status == -ENOENT || urb->status == -ECONNRESET ||
+		      urb->status == -ESHUTDOWN))
+			mm81x_err(mm, "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 *mm = 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) {
+		mm81x_err(mm, "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, mm);
+
+	ret = usb_submit_urb(musb->endpoints[MM81X_EP_MEM_RD].urb, GFP_ATOMIC);
+	if (ret < 0) {
+		mm81x_err(mm, "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) {
+		mm81x_err(mm, "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;
+		mm81x_err(mm, "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 *mm = 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) {
+		mm81x_err(mm, "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, mm);
+
+	ret = usb_submit_urb(musb->endpoints[MM81X_EP_MEM_WR].urb, GFP_ATOMIC);
+	if (ret < 0) {
+		mm81x_err(mm, "- 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) {
+		mm81x_err(mm, "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;
+		mm81x_err(mm, "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 *mm, u32 address, u8 *data, int len)
+{
+	ssize_t offset = 0;
+	int ret;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->drv_priv;
+
+	if (WARN_ON(len < 0))
+		return -EINVAL;
+
+	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) {
+			mm81x_err(mm, "%s failed (errno=%d)", __func__, ret);
+			return ret;
+		}
+
+		offset += ret;
+	}
+
+	return 0;
+}
+
+static int mm81x_usb_dm_write(struct mm81x *mm, u32 address, const u8 *data,
+			      int len)
+{
+	ssize_t offset = 0;
+	int ret;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->drv_priv;
+
+	if (WARN_ON(len < 0))
+		return -EINVAL;
+
+	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) {
+			mm81x_err(mm, "%s failed (errno=%d)", __func__, ret);
+			return ret;
+		}
+
+		offset += ret;
+	}
+
+	return 0;
+}
+
+static int mm81x_usb_reg32_read(struct mm81x *mm, u32 address, u32 *val)
+{
+	int ret = 0;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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;
+	}
+
+	mm81x_err(mm, "usb reg32 read failed %d", ret);
+	return ret;
+}
+
+static int mm81x_usb_reg32_write(struct mm81x *mm, u32 address, u32 val)
+{
+	int ret = 0;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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;
+
+	mm81x_err(mm, "usb reg32 write failed %d", ret);
+	return ret;
+}
+
+static void mm81x_usb_bus_enable(struct mm81x *mm, bool enable)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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 *mm)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->drv_priv;
+
+	mutex_lock(&musb->bus_lock);
+}
+
+static void mm81x_usb_release_bus(struct mm81x *mm)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->drv_priv;
+
+	mutex_unlock(&musb->bus_lock);
+}
+
+static void mm81x_usb_set_irq(struct mm81x *mm, 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,
+	.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 *mm,
+				      const struct usb_interface *intf)
+{
+	int ret;
+	unsigned int i;
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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);
+		}
+	}
+
+	mm81x_dbg(mm, MM81X_DBG_USB,
+		  "\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);
+	mm81x_dbg(mm, MM81X_DBG_USB,
+		  "\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);
+	mm81x_dbg(mm, MM81X_DBG_USB, "\tRegister Endpoint IN %s detected: %u",
+		  musb->endpoints[MM81X_EP_REG_RD].addr ? "" : "not",
+		  musb->endpoints[MM81X_EP_REG_RD].addr);
+	mm81x_dbg(mm, MM81X_DBG_USB, "\tRegister Endpoint OUT %s detected: %u",
+		  musb->endpoints[MM81X_EP_REG_WR].addr ? "" : "not",
+		  musb->endpoints[MM81X_EP_REG_WR].addr);
+	mm81x_dbg(mm, MM81X_DBG_USB, "\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 *mm)
+{
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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 *mm;
+	struct mm81x_usb *musb;
+
+	mm = mm81x_mac_create(sizeof(*musb), &interface->dev);
+	if (!mm) {
+		dev_err(&interface->dev, "mm81x_mac_create failed\n");
+		return -ENOMEM;
+	}
+
+	mm->bus_ops = &mm81x_usb_ops;
+	mm->bus_type = MM81X_BUS_TYPE_USB;
+
+	musb = (struct mm81x_usb *)mm->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, mm);
+
+	ret = mm81x_usb_detect_endpoints(mm, interface);
+	if (ret < 0) {
+		mm81x_err(mm, "mm81x_usb_detect_endpoints failed (%d)\n", ret);
+		goto err_destroy_mac;
+	}
+
+	set_bit(MM81X_USB_FLAG_ATTACHED, &musb->flags);
+
+	ret = mm81x_core_attach_regs(mm);
+	if (ret < 0) {
+		mm81x_err(mm, "mm81x_core_attach_regs failed: %d", ret);
+		goto err_destroy_mac;
+	}
+
+	mm->ps.gpios_supported = false;
+
+	mm81x_dbg(mm, MM81X_DBG_USB, "CHIP ID 0x%08x:0x%04x",
+		  MM81X_REG_CHIP_ID(mm), mm->chip_id);
+
+	mm81x_core_init_mac_addr(mm);
+
+	ret = mm81x_core_create(mm);
+	if (ret)
+		goto err_destroy_mac;
+
+	INIT_WORK(&mm->usb_irq_work, mm81x_usb_irq_work);
+	mm81x_usb_int_enable(mm);
+
+	ret = mm81x_mac_register(mm);
+	if (ret) {
+		mm81x_err(mm, "mm81x_mac_register failed: %d", ret);
+		goto err_core_destroy;
+	}
+
+	/* 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_core_destroy:
+	mm81x_usb_int_stop(mm);
+	mm81x_core_destroy(mm);
+err_destroy_mac:
+	mm81x_mac_destroy(mm);
+	return ret;
+}
+
+static void mm81x_usb_disconnect(struct usb_interface *interface)
+{
+	struct mm81x *mm = usb_get_intfdata(interface);
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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, &mm->state_flags);
+		mm81x_dbg(mm, MM81X_DBG_USB, "USB suddenly unplugged");
+	}
+
+	usb_disable_autosuspend(usb_get_dev(udev));
+
+	if (test_bit(MM81X_USB_FLAG_SUSPENDED, &musb->flags)) {
+		mm81x_dbg(mm, MM81X_DBG_USB,
+			  "USB was suspended: release locks");
+		mm81x_usb_release_bus(mm);
+		mutex_unlock(&musb->lock);
+	}
+
+	clear_bit(MM81X_USB_FLAG_SUSPENDED, &musb->flags);
+
+	mm81x_mac_unregister(mm);
+	mm81x_usb_int_stop(mm);
+	mm81x_core_destroy(mm);
+	mm81x_urb_cleanup(mm);
+	mm81x_mac_destroy(mm);
+
+	usb_autopm_put_interface(interface);
+	usb_set_intfdata(interface, NULL);
+	dev_info(&interface->dev, "USB Morse #%d now disconnected", minor);
+	usb_put_dev(udev);
+}
+
+static int mm81x_usb_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	struct mm81x *mm = usb_get_intfdata(intf);
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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(mm);
+	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 *mm = usb_get_intfdata(intf);
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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)
+		mm81x_err(mm, "Couldn't submit urb. Error number %d", ret);
+
+	mm81x_usb_release_bus(mm);
+	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 *mm = usb_get_intfdata(intf);
+	struct mm81x_usb *musb = (struct mm81x_usb *)mm->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)
+		mm81x_err(mm, "Couldn't submit urb. Error number %d", ret);
+
+	mm81x_usb_release_bus(mm);
+	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,
+};
+
+int __init mm81x_usb_init(void)
+{
+	int ret;
+
+	ret = usb_register(&mm81x_usb_driver);
+	if (ret)
+		pr_err("failed to register mm81x usb driver: %d\n", ret);
+
+	return ret;
+}
+
+void __exit mm81x_usb_exit(void)
+{
+	usb_deregister(&mm81x_usb_driver);
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 25/35] wifi: mm81x: add skbq.h
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, 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..0ccd1291ac95
--- /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 *mm;
+	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 *mm, 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 *mm, 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 *mm, 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 *mm, struct mm81x_skbq *mq);
+void mm81x_skbq_may_wake_tx_queues(struct mm81x *mm);
+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 *mm);
+void mm81x_skbq_data_traffic_resume(struct mm81x *mm);
+bool mm81x_skbq_validate_checksum(u8 *data);
+
+#endif /* !_MM81X_SKBQ_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 24/35] wifi: mm81x: add skbq.c
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/skbq.c | 1056 ++++++++++++++++++
 1 file changed, 1056 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..c70eebf4cffd
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/skbq.c
@@ -0,0 +1,1056 @@
+// 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 "debug.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 inline 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 inline 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 inline u32 __mm81x_skbq_size(const struct mm81x_skbq *mq)
+{
+	return mq->skbq_size;
+}
+
+static inline u32 __mm81x_skbq_space(const struct mm81x_skbq *mq)
+{
+	return MM81X_SKBQ_SIZE - __mm81x_skbq_size(mq);
+}
+
+static inline bool __mm81x_skbq_over_threshold(struct mm81x_skbq *mq)
+{
+	return skb_queue_len(&mq->skbq) >= MM81X_SKBQ_MAX_TXQ_LEN;
+}
+
+static inline 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 *mm,
+			       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(mm, aci);
+		break;
+	case MM81X_SKB_CHAN_MGMT:
+		mq = mm81x_hif_get_tx_mgmt_queue(mm);
+		break;
+	case MM81X_SKB_CHAN_BEACON:
+		mq = mm81x_hif_get_tx_beacon_queue(mm);
+		break;
+	default:
+		mm81x_err(mm, "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 *mm, 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->mm, skb);
+	ieee80211_free_txskb(mq->mm->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 *mm,
+						      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_tx_status_process(struct mm81x *mm, 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 sk_buff *tx_skb;
+		struct mm81x_skbq *mq =
+			__mm81x_skbq_tx_status_to_skbq(mm, tx_sts);
+		bool is_ps_filtered = (le32_to_cpu(tx_sts->flags) &
+				       MM81X_TX_STATUS_FLAGS_PS_FILTERED);
+
+		if (!mq)
+			continue;
+
+		spin_lock_bh(&mq->lock);
+		tx_skb = __mm81x_skbq_get_pending_by_id(
+			mm, mq, le32_to_cpu(tx_sts->pkt_id));
+		if (!tx_skb) {
+			mm81x_dbg(
+				mm, MM81X_DBG_ANY,
+				"No pending pkt match found [pktid:%d chan:%d]",
+				tx_sts->pkt_id, tx_sts->channel);
+			spin_unlock_bh(&mq->lock);
+			continue;
+		}
+
+		if (le32_to_cpu(tx_sts->flags) & MM81X_TX_STATUS_PAGE_INVALID) {
+			__mm81x_skbq_drop_pending_skb(mq, tx_skb);
+			spin_unlock_bh(&mq->lock);
+			continue;
+		}
+
+		if (le32_to_cpu(tx_sts->flags) &
+		    MM81X_TX_STATUS_DUTY_CYCLE_CANT_SEND) {
+			__mm81x_skbq_drop_pending_skb(mq, tx_skb);
+			spin_unlock_bh(&mq->lock);
+			continue;
+		}
+
+		if (is_ps_filtered &&
+		    mm81x_tx_h_is_ps_filtered(mq, tx_skb, tx_sts)) {
+			/* Has been consumed by mm81x_tx_h_is_ps_filtered */
+			spin_unlock_bh(&mq->lock);
+			continue;
+		}
+
+		mm81x_skbq_pull_hdr_post_tx(tx_skb);
+		mm81x_skbq_skb_finish(mq, tx_skb, tx_sts);
+		spin_unlock_bh(&mq->lock);
+	}
+
+	if (mm->ps.enable && !mm->ps.suspended &&
+	    (mm81x_hif_get_tx_buffered_count(mm) == 0)) {
+		/* Evaluate ps, check if it was gated on a pending tx status */
+		queue_delayed_work(mm->chip_wq, &mm->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 *mm = mq->mm;
+	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(mm, pfirst);
+			break;
+		case MM81X_SKB_CHAN_TX_STATUS:
+			mm81x_skbq_tx_status_process(mm, pfirst);
+			dev_kfree_skb_any(pfirst);
+			break;
+		default:
+			mm81x_mac_rx_skb(mm, pfirst, &hdr->rx_status);
+			break;
+		}
+	}
+
+	if (mm81x_skbq_count(mq))
+		queue_work(mm->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 *mm, struct mm81x_skbq *mq)
+{
+	int dropped = 0;
+	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(mm->hw, pfirst);
+		dropped++;
+	}
+
+	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 *mm)
+{
+	int queue;
+
+	if (!mm->started)
+		return;
+	for (queue = IEEE80211_AC_VO; queue <= IEEE80211_AC_BK; queue++)
+		ieee80211_stop_queue(mm->hw, queue);
+
+	set_bit(MM81X_STATE_DATA_QS_STOPPED, &mm->state_flags);
+}
+
+/* Wake all Tx queues if all queues are below threshold */
+void mm81x_skbq_may_wake_tx_queues(struct mm81x *mm)
+{
+	int queue;
+	struct mm81x_skbq *qs;
+	int num_qs;
+	bool could_wake;
+
+	if (!mm->started)
+		return;
+
+	could_wake = true;
+	mm81x_hif_skbq_get_tx_qs(mm, &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(mm->hw, queue);
+
+	clear_bit(MM81X_STATE_DATA_QS_STOPPED, &mm->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 *mm = mq->mm;
+
+	spin_lock_bh(&mq->lock);
+	rc = __mm81x_skbq_put(mq, &mq->skbq, skb, false, NULL);
+	if (rc) {
+		mm81x_err(mm, "skb put chan %d failed (%d)", channel, rc);
+		if (channel == MM81X_SKB_CHAN_DATA) {
+			u16 queue = skb_get_queue_mapping(skb);
+
+			mm81x_err(mm, "skb put queue %d status %d", queue,
+				  ieee80211_queue_stopped(mm->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(mm);
+
+	switch (channel) {
+	case MM81X_SKB_CHAN_DATA:
+	case MM81X_SKB_CHAN_DATA_NOACK:
+		if (mm81x_is_data_tx_allowed(mm)) {
+			set_bit(MM81X_HIF_EVT_TX_DATA_PEND,
+				&mm->hif.event_flags);
+			queue_work(mm->chip_wq, &mm->hif_work);
+		}
+		break;
+	case MM81X_SKB_CHAN_MGMT:
+		set_bit(MM81X_HIF_EVT_TX_MGMT_PEND, &mm->hif.event_flags);
+		queue_work(mm->chip_wq, &mm->hif_work);
+		break;
+	case MM81X_SKB_CHAN_BEACON:
+		set_bit(MM81X_HIF_EVT_TX_BEACON_PEND, &mm->hif.event_flags);
+		queue_work(mm->chip_wq, &mm->hif_work);
+		break;
+	case MM81X_SKB_CHAN_COMMAND:
+		set_bit(MM81X_HIF_EVT_TX_COMMAND_PEND, &mm->hif.event_flags);
+		queue_work(mm->chip_wq, &mm->hif_work);
+		break;
+	default:
+		mm81x_err(mm, "Invalid skb channel: %d", channel);
+		break;
+	}
+
+	return rc;
+}
+
+static inline 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 *mm = mq->mm;
+	struct sk_buff *pfirst, *pnext;
+	struct sk_buff *peek = skb_peek(skbq);
+	struct mm81x_skb_hdr *hdr;
+	const bool fw_reports_bcn_tx_status =
+		mm->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(&mm->stale_status.lock);
+		mod_timer(&mm->stale_status.timer,
+			  jiffies + msecs_to_jiffies(
+					    MM81X_SKBQ_TX_STATUS_LIFETIME_MS));
+		spin_unlock_bh(&mm->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 *mm, 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)) {
+			mm81x_dbg(mm, MM81X_DBG_ANY,
+				  "TX skb timed out [id:%d,chan:%d]",
+				  hdr->tx_info.pkt_id, hdr->channel);
+
+			__mm81x_skbq_drop_pending_skb(mq, pfirst);
+			flushed++;
+		}
+	}
+
+	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 *mm = mq->mm;
+
+	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 */
+		mm81x_dbg(mm, MM81X_DBG_SKBQ,
+			  "Command pending queue empty. Removing from SKBQ.");
+		__mm81x_skbq_unlink(mq, &mq->skbq, skb);
+		dev_kfree_skb(skb);
+	} else {
+		mm81x_dbg(mm, MM81X_DBG_SKBQ, "Command Q not found");
+	}
+}
+
+struct mm81x_update_sta_iter_data {
+	struct mm81x *mm;
+	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->mm, 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 *mm = mq->mm;
+	struct mm81x_update_sta_iter_data iter = {};
+
+	__mm81x_skbq_unlink(mq, &mq->pending, skb);
+	iter.mm = mm;
+	iter.skb = skb;
+	iter.tx_sts = tx_sts;
+	iter.tx_attempts = mm81x_tx_h_get_attempts(mm, tx_sts);
+
+	ieee80211_iterate_active_interfaces_atomic(mm->hw,
+						   IEEE80211_IFACE_ITER_NORMAL,
+						   mm81x_tx_h_update_sta_iter,
+						   &iter);
+
+	ieee80211_tx_status_skb(mm->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->mm->hw, pfirst);
+	}
+
+	skb_queue_walk_safe(&mq->skbq, pfirst, pnext) {
+		__mm81x_skbq_unlink(mq, &mq->skbq, pfirst);
+		ieee80211_free_txskb(mq->mm->hw, pfirst);
+	}
+	spin_unlock_bh(&mq->lock);
+}
+
+void mm81x_skbq_init(struct mm81x *mm, struct mm81x_skbq *mq, u16 flags)
+{
+	spin_lock_init(&mq->lock);
+	__skb_queue_head_init(&mq->skbq);
+	__skb_queue_head_init(&mq->pending);
+	mq->mm = mm;
+	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)
+		mm81x_dbg(mq->mm, MM81X_DBG_SKBQ,
+			  "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 *mm = mq->mm;
+
+	if (!mm81x_is_data_tx_allowed(mm))
+		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->mm);
+	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 *mm, u8 channel)
+{
+	if (channel == MM81X_SKB_CHAN_COMMAND) {
+		if (test_bit(MM81X_STATE_HOST_TO_CHIP_CMD_BLOCKED,
+			     &mm->state_flags))
+			return -EPERM;
+	} else {
+		if (test_bit(MM81X_STATE_HOST_TO_CHIP_TX_BLOCKED,
+			     &mm->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 *mm = mq->mm;
+	size_t end_of_skb_pad;
+	struct sk_buff *skb = *skb_orig;
+	u8 *aligned_head, *data;
+
+	if (test_bit(MM81X_STATE_CHIP_UNRESPONSIVE, &mm->state_flags)) {
+		dev_kfree_skb_any(skb);
+		return -ENODEV;
+	}
+
+	ret = mm81x_skb_tx_h_validate_channel(mm, 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(mm));
+	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) {
+		mm81x_err(mm, "mm81x_skbq_tx fail: %d", ret);
+		dev_kfree_skb_any(skb);
+	}
+
+	return ret;
+}
+
+void mm81x_skbq_data_traffic_pause(struct mm81x *mm)
+{
+	set_bit(MM81X_STATE_DATA_TX_STOPPED, &mm->state_flags);
+	/* power-save requirements will be re-evaluated by the caller */
+}
+
+void mm81x_skbq_data_traffic_resume(struct mm81x *mm)
+{
+	clear_bit(MM81X_STATE_DATA_TX_STOPPED, &mm->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, &mm->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 23/35] wifi: mm81x: add sdio.c
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/sdio.c | 803 +++++++++++++++++++
 1 file changed, 803 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..260d7075984e
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/sdio.c
@@ -0,0 +1,803 @@
+// 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 <linux/of_gpio.h>
+#include <linux/gpio/consumer.h>
+#include "hw.h"
+#include "core.h"
+#include "bus.h"
+#include "mac.h"
+#include "fw.h"
+#include "debug.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 mm81x_sdio_of_probe(struct device *dev, struct mm81x_ps *ps,
+				const struct of_device_id *match_table)
+{
+	struct device_node *np = dev->of_node;
+	const struct of_device_id *of_id;
+
+	if (!np) {
+		dev_warn(dev, "Device node not found\n");
+		return;
+	}
+
+	of_id = of_match_node(match_table, np);
+	if (!of_id) {
+		dev_warn(dev, "Couldn't match device table\n");
+		return;
+	}
+
+	ps->wake_gpio = devm_gpiod_get_optional(dev, "wake", GPIOD_OUT_HIGH);
+	ps->busy_gpio = devm_gpiod_get_optional(dev, "busy", GPIOD_IN);
+
+	ps->gpios_supported = (!IS_ERR_OR_NULL(ps->wake_gpio) &&
+			       !IS_ERR_OR_NULL(ps->busy_gpio));
+	if (!ps->gpios_supported) {
+		dev_warn(
+			dev,
+			"wake-gpios and busy-gpios not defined, powersave disabled\n");
+	}
+}
+
+static void mm81x_sdio_remove(struct sdio_func *func);
+
+static void sdio_log_err(struct mm81x_sdio *sdio, const char *operation,
+			 unsigned int fn, unsigned int address,
+			 unsigned int len, int ret)
+{
+	struct mm81x *mm = sdio->func ? sdio_get_drvdata(sdio->func) : NULL;
+
+	if (!mm)
+		return;
+
+	mm81x_err(mm, "sdio: %s fn=%d 0x%08x:%d r=0x%08x b=0x%08x (ret:%d)",
+		  operation, fn, address, len, sdio->register_addr_base,
+		  sdio->bulk_addr_base, ret);
+}
+
+static void irq_handler(struct sdio_func *func1)
+{
+	int handled;
+	struct sdio_func *func = func1->card->sdio_func[1];
+	struct mm81x *mm = sdio_get_drvdata(func);
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->drv_priv;
+
+	WARN_ON_ONCE(!mm);
+
+	(void)sdio;
+
+	handled = mm81x_hw_irq_handle(mm);
+	if (!handled)
+		mm81x_dbg(mm, MM81X_DBG_SDIO, "%s: nothing was handled\n",
+			  __func__);
+}
+
+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 *mm = sdio_get_drvdata(func);
+
+	sdio_claim_host(func);
+	ret = sdio_claim_irq(func1, irq_handler);
+	if (ret)
+		mm81x_err(mm, "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 *mm, bool enable)
+{
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->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,
+					    u32 address, u8 access, bool bulk)
+{
+	int ret = 0;
+	u8 base[4];
+	const char *operation = "set_address_base";
+	u32 calculated_addr_base =
+		mm81x_sdio_calculate_base_address(address, access);
+	u32 *current_addr_base = bulk ? &sdio->bulk_addr_base :
+					&sdio->register_addr_base;
+	bool base_addr_is_unset =
+		(*current_addr_base == MM81X_SDIO_BASE_ADDR_UNSET);
+	struct sdio_func *func2 = sdio->func;
+	struct sdio_func *func1 = sdio->func->card->sdio_func[0];
+	struct sdio_func *func_to_use = bulk ? func2 : func1;
+	struct mm81x *mm = sdio_get_drvdata(sdio->func);
+	int retries = 0;
+	static const int max_retries = 3;
+
+	if ((*current_addr_base) == calculated_addr_base && !base_addr_is_unset)
+		return ret;
+
+	base[0] = (u8)((address & 0x00FF0000) >> 16);
+	base[1] = (u8)((address & 0xFF000000) >> 24);
+	base[2] = access & 0x3; /* 1, 2 or 4 byte access */
+
+retry:
+	if (base_addr_is_unset ||
+	    (base[0] != (u8)(((*current_addr_base) & 0x00FF0000) >> 16))) {
+		sdio_writeb(func_to_use, base[0],
+			    MM81X_SDIO_REG_ADDRESS_WINDOW_0, &ret);
+		if (ret) {
+			sdio_log_err(sdio, operation, func_to_use->num,
+				     MM81X_SDIO_REG_ADDRESS_WINDOW_0, 1, ret);
+			goto err;
+		}
+	}
+
+	if (base_addr_is_unset ||
+	    (base[1] != (u8)(((*current_addr_base) & 0xFF000000) >> 24))) {
+		sdio_writeb(func_to_use, base[1],
+			    MM81X_SDIO_REG_ADDRESS_WINDOW_1, &ret);
+		if (ret) {
+			sdio_log_err(sdio, operation, func_to_use->num,
+				     MM81X_SDIO_REG_ADDRESS_WINDOW_1, 1, ret);
+			goto err;
+		}
+	}
+
+	if (base_addr_is_unset ||
+	    (base[2] != (u8)(((*current_addr_base) & 0x3)))) {
+		sdio_writeb(func_to_use, base[2], MM81X_SDIO_REG_ADDRESS_CONFIG,
+			    &ret);
+		if (ret) {
+			sdio_log_err(sdio, operation, func_to_use->num,
+				     MM81X_SDIO_REG_ADDRESS_CONFIG, 1, ret);
+			goto err;
+		}
+	}
+
+	*current_addr_base = calculated_addr_base;
+	if (retries)
+		mm81x_dbg(mm, MM81X_DBG_SDIO, "%s succeeded after %d retries\n",
+			  __func__, retries);
+
+	return ret;
+err:
+	retries++;
+	if (ret == -ETIMEDOUT && retries <= max_retries) {
+		mm81x_dbg(mm, MM81X_DBG_SDIO,
+			  "%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 struct sdio_func *mm81x_sdio_get_func(struct mm81x_sdio *sdio,
+					     u32 address, ssize_t size,
+					     u8 access)
+{
+	int ret = 0;
+	u32 calculated_base_address =
+		mm81x_sdio_calculate_base_address(address, access);
+	struct sdio_func *func2 = sdio->func;
+	struct sdio_func *func1 = sdio->func ? sdio->func->card->sdio_func[0] :
+					       NULL;
+	struct mm81x *mm = sdio->func ? sdio_get_drvdata(sdio->func) : NULL;
+	struct sdio_func *func_to_use;
+
+	WARN_ON(!mm);
+
+	/* Order matters here, please don't re-order */
+	if (size > sizeof(u32)) {
+		ret = mm81x_sdio_set_func_address_base(sdio, address, access,
+						       true);
+		WARN_ON_ONCE(sdio->bulk_addr_base == 0);
+		func_to_use = func2;
+	} else if (sdio->bulk_addr_base == calculated_base_address && func2) {
+		func_to_use = func2;
+	} else if (func1) {
+		ret = mm81x_sdio_set_func_address_base(sdio, address, access,
+						       false);
+		WARN_ON_ONCE(sdio->register_addr_base == 0);
+		func_to_use = func1;
+	} else {
+		ret = mm81x_sdio_set_func_address_base(sdio, address, access,
+						       true);
+		WARN_ON_ONCE(sdio->bulk_addr_base == 0);
+		func_to_use = func2;
+	}
+
+	return ret ? NULL : func_to_use;
+}
+
+static int mm81x_sdio_regl_write(struct mm81x_sdio *sdio, u32 address,
+				 u32 value)
+{
+	ssize_t ret = 0;
+	struct mm81x *mm = sdio->func ? sdio_get_drvdata(sdio->func) : NULL;
+	u32 original_address = address;
+	struct sdio_func *func_to_use;
+
+	if (!mm) {
+		ret = -EINVAL;
+		goto exit;
+	}
+
+	func_to_use = mm81x_sdio_get_func(sdio, address, sizeof(u32),
+					  MM81X_CONFIG_ACCESS_4BYTE);
+	if (!func_to_use) {
+		ret = -EIO;
+		goto exit;
+	}
+
+	address &= 0x0000FFFF; /* remove base and keep offset */
+	sdio_writel(func_to_use, (__force u32)cpu_to_le32(value),
+		    (__force u32)cpu_to_le32(address), (int *)&ret);
+
+	if (ret)
+		sdio_log_err(sdio, "writel", func_to_use->num, address,
+			     sizeof(u32), ret);
+	else
+		ret = sizeof(value);
+
+	if (original_address == MM81X_REG_RESET(mm) &&
+	    value == MM81X_REG_RESET_VALUE(mm)) {
+		mm81x_dbg(mm, MM81X_DBG_SDIO,
+			  "SDIO reset detected, invalidating base addr\n");
+		mm81x_sdio_reset_base_address(sdio);
+	}
+exit:
+	return (int)ret;
+}
+
+static int mm81x_sdio_regl_read(struct mm81x_sdio *sdio, u32 address,
+				u32 *value)
+{
+	ssize_t ret = 0;
+	struct mm81x *mm = sdio->func ? sdio_get_drvdata(sdio->func) : NULL;
+	struct sdio_func *func_to_use;
+
+	if (!mm) {
+		ret = -EINVAL;
+		goto exit;
+	}
+
+	func_to_use = mm81x_sdio_get_func(sdio, address, sizeof(u32),
+					  MM81X_CONFIG_ACCESS_4BYTE);
+	if (!func_to_use) {
+		ret = -EIO;
+		goto exit;
+	}
+
+	address &= 0x0000FFFF; /* remove base and keep offset */
+	*value = sdio_readl(func_to_use, (__force u32)cpu_to_le32(address),
+			    (int *)&ret);
+	if (ret)
+		sdio_log_err(sdio, "readl", func_to_use->num, address,
+			     sizeof(u32), ret);
+	else
+		ret = sizeof(*value);
+exit:
+	return (int)ret;
+}
+
+static int mm81x_sdio_mem_write(struct mm81x_sdio *sdio, u32 address, u8 *data,
+				ssize_t size)
+{
+	ssize_t ret = 0;
+	struct mm81x *mm = sdio->func ? sdio_get_drvdata(sdio->func) : NULL;
+	int access = (size & 0x03) ? MM81X_CONFIG_ACCESS_1BYTE :
+				     MM81X_CONFIG_ACCESS_4BYTE;
+	struct sdio_func *func_to_use;
+
+	if (!mm) {
+		ret = -EINVAL;
+		goto exit;
+	}
+
+	func_to_use = mm81x_sdio_get_func(sdio, address, size, access);
+	if (!func_to_use) {
+		ret = -EIO;
+		goto exit;
+	}
+
+	address &= 0x0000FFFF; /* remove base and keep offset */
+	if (access == MM81X_CONFIG_ACCESS_4BYTE) {
+		if (unlikely(!IS_ALIGNED((uintptr_t)data,
+					 mm->bus_ops->bulk_alignment))) {
+			ret = -EBADE;
+			goto exit;
+		}
+
+		/* Use ex write */
+		ret = sdio_memcpy_toio(func_to_use, address, data, size);
+
+		if (ret) {
+			sdio_log_err(sdio, "memcpy_toio", func_to_use->num,
+				     address, size, ret);
+			goto exit;
+		}
+	} else {
+		int i;
+
+		for (i = 0; i < size; i++) {
+			sdio_writeb(func_to_use, data[i], address + i,
+				    (int *)&ret);
+			if (ret) {
+				sdio_log_err(sdio, "writeb", func_to_use->num,
+					     address + i, 1, ret);
+				goto exit;
+			}
+		}
+	}
+	ret = size;
+exit:
+	return ret;
+}
+
+static void mm81x_sdio_claim_host(struct mm81x *mm)
+{
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->drv_priv;
+	struct sdio_func *func = sdio->func;
+
+	sdio_claim_host(func);
+}
+
+static void mm81x_sdio_release_host(struct mm81x *mm)
+{
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->drv_priv;
+	struct sdio_func *func = sdio->func;
+
+	sdio_release_host(func);
+}
+
+static int mm81x_sdio_mem_read(struct mm81x_sdio *sdio, u32 address, u8 *data,
+			       ssize_t size)
+{
+	ssize_t ret = 0;
+	struct mm81x *mm = sdio->func ? sdio_get_drvdata(sdio->func) : NULL;
+	int access = (size & 0x03) ? MM81X_CONFIG_ACCESS_1BYTE :
+				     MM81X_CONFIG_ACCESS_4BYTE;
+	struct sdio_func *func_to_use;
+
+	if (!mm) {
+		ret = -EINVAL;
+		goto exit;
+	}
+
+	func_to_use = mm81x_sdio_get_func(sdio, address, size, access);
+	if (!func_to_use) {
+		ret = -EIO;
+		goto exit;
+	}
+
+	address &= 0x0000FFFF; /* remove base and keep offset */
+	if (access == MM81X_CONFIG_ACCESS_4BYTE) {
+		if (unlikely(!IS_ALIGNED((uintptr_t)data,
+					 mm->bus_ops->bulk_alignment))) {
+			ret = -EBADE;
+			goto exit;
+		}
+
+		ret = sdio_memcpy_fromio(func_to_use, data, address, size);
+		if (ret) {
+			sdio_log_err(sdio, "memcpy_fromio", func_to_use->num,
+				     address, size, 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(func_to_use, data, address, 8);
+	} else {
+		int i;
+
+		for (i = 0; i < size; i++) {
+			data[i] = sdio_readb(func_to_use, address + i,
+					     (int *)&ret);
+			if (ret) {
+				sdio_log_err(sdio, "readb", func_to_use->num,
+					     address + i, 1, ret);
+				goto exit;
+			}
+		}
+	}
+	ret = size;
+exit:
+	return ret;
+}
+
+static int mm81x_sdio_dm_write(struct mm81x *mm, u32 address, const u8 *data,
+			       int len)
+{
+	int ret = 0;
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->drv_priv;
+	int remaining = len;
+	int offset = 0;
+
+	if (WARN_ON(len < 0))
+		return -EINVAL;
+
+	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));
+		ret = mm81x_sdio_mem_write(sdio, address + offset,
+					   (u8 *)(data + offset), len);
+		if (ret != len)
+			return -EIO;
+
+		offset += len;
+		WARN_ON_ONCE(len > remaining);
+		remaining -= len;
+	}
+
+	return 0;
+}
+
+static int mm81x_sdio_dm_read(struct mm81x *mm, u32 address, u8 *data, int len)
+{
+	int ret = 0;
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->drv_priv;
+	int remaining = len;
+	int offset = 0;
+
+	if (WARN_ON(len < 0))
+		return -EINVAL;
+
+	WARN_ON_ONCE(len % 4);
+
+	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));
+		ret = mm81x_sdio_mem_read(sdio, address + offset, data + offset,
+					  len);
+		if (ret != len)
+			return -EIO;
+
+		offset += len;
+		WARN_ON_ONCE(len > remaining);
+		remaining -= len;
+	}
+
+	return 0;
+}
+
+static int mm81x_sdio_reg32_write(struct mm81x *mm, u32 address, u32 val)
+{
+	ssize_t ret = 0;
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->drv_priv;
+
+	ret = mm81x_sdio_regl_write(sdio, address, val);
+	if (ret == sizeof(val))
+		return 0;
+
+	return -EIO;
+}
+
+static int mm81x_sdio_reg32_read(struct mm81x *mm, u32 address, u32 *val)
+{
+	ssize_t ret = 0;
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->drv_priv;
+
+	ret = mm81x_sdio_regl_read(sdio, address, val);
+	if (ret == sizeof(*val)) {
+		*val = le32_to_cpup((__le32 *)val);
+		return 0;
+	}
+	return -EIO;
+}
+
+static void mm81x_sdio_bus_enable(struct mm81x *mm, bool enable)
+{
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->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);
+		mm81x_dbg(mm, MM81X_DBG_SDIO, "%s: enabling bus\n", __func__);
+	} else {
+		host->ops->enable_sdio_irq(host, 0);
+		mm81x_sdio_reset_base_address(sdio);
+		sdio->enabled = false;
+		mm81x_dbg(mm, MM81X_DBG_SDIO, "%s: disabling bus\n", __func__);
+	}
+
+	sdio_release_host(func);
+}
+
+static void mm81x_sdio_reset(struct sdio_func *func)
+{
+	/* reset the adapter */
+	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 *mm, bool enable_burst)
+{
+	u8 burst_mode = (enable_burst) ? SDIO_WORD_BURST_SIZE_16 :
+					 SDIO_WORD_BURST_DISABLE;
+
+	mm81x_hw_enable_burst_mode(mm, 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 *mm = sdio_get_drvdata(func);
+
+	sdio_claim_host(func);
+	ret = sdio_enable_func(func);
+	if (ret)
+		mm81x_err(mm, "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 const struct of_device_id mm81x_of_match_table[] = {
+	{
+		.compatible = "morsemicro,mm81x",
+	},
+	{},
+};
+
+static int mm81x_sdio_probe(struct sdio_func *func,
+			    const struct sdio_device_id *id)
+{
+	int ret = 0;
+	u32 chip_id;
+	struct mm81x *mm = NULL;
+	struct mm81x_sdio *sdio;
+	struct device *dev = &func->dev;
+
+	if (func->num == 1)
+		return 0;
+
+	if (func->num != 2)
+		return -ENODEV;
+
+	mm = mm81x_mac_create(sizeof(*sdio), dev);
+	if (!mm) {
+		dev_err(dev, "mm81x_mac_create failed\n");
+		return -ENOMEM;
+	}
+
+	mm->bus_ops = &mm81x_sdio_ops;
+	mm->bus_type = MM81X_BUS_TYPE_SDIO;
+
+	sdio = (struct mm81x_sdio *)mm->drv_priv;
+	sdio->func = func;
+	sdio->id = id;
+	sdio->enabled = true;
+	mm81x_sdio_reset_base_address(sdio);
+
+	sdio_set_drvdata(func, mm);
+
+	ret = mm81x_sdio_enable(sdio);
+	if (ret) {
+		mm81x_err(mm, "mm81x_sdio_enable failed: %d\n", ret);
+		goto err_destroy_mac;
+	}
+
+	ret = mm81x_core_attach_regs(mm);
+	if (ret) {
+		mm81x_err(mm, "mm81x_core_attach_regs failed: %d\n", ret);
+		goto err_destroy_sdio;
+	}
+
+	mm81x_claim_bus(mm);
+	ret = mm81x_reg32_read(mm, MM81X_REG_CHIP_ID(mm), &chip_id);
+	mm81x_release_bus(mm);
+	if (ret || chip_id != mm->chip_id) {
+		mm81x_err(mm, "Chip ID read failed: %d\n", ret);
+		goto err_destroy_sdio;
+	}
+
+	mm81x_dbg(mm, MM81X_DBG_SDIO,
+		  "Morse Micro SDIO device found, chip ID=0x%04x\n",
+		  mm->chip_id);
+
+	mm81x_sdio_of_probe(dev, &mm->ps, mm81x_of_match_table);
+	mm81x_sdio_config_burst_mode(mm, true);
+
+	mm81x_core_init_mac_addr(mm);
+
+	ret = mm81x_core_create(mm);
+	if (ret)
+		goto err_destroy_sdio;
+
+	ret = mm81x_sdio_enable_irq(sdio);
+	if (ret) {
+		mm81x_err(mm, "mm81x_sdio_enable_irq failed: %d\n", ret);
+		goto err_destroy_core;
+	}
+
+	ret = mm81x_mac_register(mm);
+	if (ret) {
+		mm81x_err(mm, "mm81x_mac_register failed: %d\n", ret);
+		goto err_disable_irq;
+	}
+
+	return 0;
+
+err_disable_irq:
+	mm81x_sdio_disable_irq(sdio);
+err_destroy_core:
+	mm81x_core_destroy(mm);
+err_destroy_sdio:
+	mm81x_sdio_release(sdio);
+err_destroy_mac:
+	mm81x_mac_destroy(mm);
+	return ret;
+}
+
+static void mm81x_sdio_remove(struct sdio_func *func)
+{
+	struct mm81x *mm = sdio_get_drvdata(func);
+	struct mm81x_sdio *sdio = (struct mm81x_sdio *)mm->drv_priv;
+
+	dev_info(&func->dev, "sdio removed func %d vendor 0x%x device 0x%x\n",
+		 func->num, func->vendor, func->device);
+
+	if (!mm)
+		return;
+
+	mm81x_mac_unregister(mm);
+	mm81x_sdio_disable_irq(sdio);
+	mm81x_core_destroy(mm);
+	mm81x_sdio_release(sdio);
+	mm81x_sdio_reset(func);
+	mm81x_mac_destroy(mm);
+	sdio_set_drvdata(func, NULL);
+}
+
+static const struct sdio_device_id mm81x_sdio_devices[] = {
+	{ SDIO_DEVICE(SDIO_VENDOR_ID_MORSEMICRO,
+		      SDIO_VENDOR_ID_MORSEMICRO_MM81XB1) },
+	{ SDIO_DEVICE(SDIO_VENDOR_ID_MORSEMICRO,
+		      SDIO_VENDOR_ID_MORSEMICRO_MM81XB2) },
+	{},
+};
+
+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,
+};
+
+int __init mm81x_sdio_init(void)
+{
+	int ret;
+
+	ret = sdio_register_driver(&mm81x_sdio_driver);
+	if (ret)
+		pr_err("sdio_register_driver() failed: %d\n", ret);
+
+	return ret;
+}
+
+void __exit mm81x_sdio_exit(void)
+{
+	sdio_unregister_driver(&mm81x_sdio_driver);
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 22/35] wifi: mm81x: add rc.h
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/rc.h | 62 ++++++++++++++++++++++
 1 file changed, 62 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..0c391183680b
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/rc.h
@@ -0,0 +1,62 @@
+/* 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 *mm;
+};
+
+struct mm81x_rc_sta {
+	struct mmrc_table *tb;
+	struct list_head list;
+	unsigned long last_update;
+};
+
+void mm81x_rc_init(struct mm81x *mm);
+void mm81x_rc_deinit(struct mm81x *mm);
+int mm81x_rc_sta_add(struct mm81x *mm, struct ieee80211_vif *vif,
+		     struct ieee80211_sta *sta);
+#define mm81x_rc_set_fixed_rate(mm, sta, mcs, bw, ss, guard) \
+	_mm81x_rc_set_fixed_rate(mm, sta, mcs, bw, ss, guard, __func__)
+bool _mm81x_rc_set_fixed_rate(struct mm81x *mm, struct ieee80211_sta *sta,
+			      int mcs, int bw, int ss, int guard,
+			      const char *caller);
+void mm81x_rc_sta_remove(struct mm81x *mm, struct ieee80211_sta *sta);
+void mm81x_rc_sta_fill_tx_rates(struct mm81x *mm,
+				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 *mm, 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 *mm, struct ieee80211_vif *vif,
+			      struct ieee80211_sta *sta,
+			      enum ieee80211_sta_state old_state,
+			      enum ieee80211_sta_state new_state);
+
+/*
+ * Reinitialize the associated stations when there is a change in BW.
+ * Must be called with mm->lock held
+ */
+void mm81x_rc_reinit_stas(struct mm81x *mm, struct ieee80211_vif *vif);
+
+#endif /* !_MM81X_RC_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 21/35] wifi: mm81x: add rc.c
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/rc.c | 559 +++++++++++++++++++++
 1 file changed, 559 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..83954c3e5a4e
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/rc.c
@@ -0,0 +1,559 @@
+// 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 "debug.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 *mm = mrc->mm;
+
+	queue_work(mm->net_wq, &mm->mrc.work);
+}
+
+void mm81x_rc_init(struct mm81x *mm)
+{
+	INIT_LIST_HEAD(&mm->mrc.stas);
+	spin_lock_init(&mm->mrc.lock);
+
+	INIT_WORK(&mm->mrc.work, mm81x_rc_work);
+	timer_setup(&mm->mrc.timer, mm81x_rc_timer, 0);
+
+	mm->mrc.mm = mm;
+	mod_timer(&mm->mrc.timer, jiffies + msecs_to_jiffies(100));
+}
+
+void mm81x_rc_deinit(struct mm81x *mm)
+{
+	cancel_work_sync(&mm->mrc.work);
+	timer_delete_sync_try(&mm->mrc.timer);
+}
+
+static void mm81x_rc_sta_config_guard_per_bw(struct ieee80211_sta *sta,
+					     struct mmrc_sta_capabilities *caps)
+{
+	caps->guard = MMRC_MASK(MMRC_GUARD_LONG);
+
+	if (caps->bandwidth & MMRC_MASK(MMRC_BW_1MHZ)) {
+		caps->sgi_per_bw |= SGI_PER_BW(MMRC_BW_1MHZ);
+		caps->guard |= MMRC_MASK(MMRC_GUARD_SHORT);
+	}
+
+	if (caps->bandwidth & MMRC_MASK(MMRC_BW_2MHZ)) {
+		caps->sgi_per_bw |= SGI_PER_BW(MMRC_BW_2MHZ);
+		caps->guard |= MMRC_MASK(MMRC_GUARD_SHORT);
+	}
+
+	if (caps->bandwidth & MMRC_MASK(MMRC_BW_4MHZ)) {
+		caps->sgi_per_bw |= SGI_PER_BW(MMRC_BW_4MHZ);
+		caps->guard |= MMRC_MASK(MMRC_GUARD_SHORT);
+	}
+
+	if (caps->bandwidth & MMRC_MASK(MMRC_BW_8MHZ)) {
+		caps->sgi_per_bw |= SGI_PER_BW(MMRC_BW_8MHZ);
+		caps->guard |= MMRC_MASK(MMRC_GUARD_SHORT);
+	}
+}
+
+static void mm81x_rc_sta_add_s1g_sta_caps(struct mm81x *mm,
+					  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 |= MMRC_MASK(MMRC_MCS9) | MMRC_MASK(MMRC_MCS8);
+		fallthrough;
+	case IEEE80211_VHT_MCS_SUPPORT_0_8: /* VHT 8 -> S1G 7 */
+		caps->rates |= MMRC_MASK(MMRC_MCS7) | MMRC_MASK(MMRC_MCS6) |
+			       MMRC_MASK(MMRC_MCS5) | MMRC_MASK(MMRC_MCS4) |
+			       MMRC_MASK(MMRC_MCS3);
+		fallthrough;
+	case IEEE80211_VHT_MCS_SUPPORT_0_7: /* VHT 7 -> S1G 2 */
+		caps->rates |= MMRC_MASK(MMRC_MCS2) | MMRC_MASK(MMRC_MCS1) |
+			       MMRC_MASK(MMRC_MCS0) | MMRC_MASK(MMRC_MCS10);
+		caps->spatial_streams |= (MMRC_MASK(nss_idx) & 0x0F);
+		break;
+
+	default:
+		mm81x_warn(mm, "Invalid MCS encoding 0x%02x for stream %d", mcs,
+			   nss_idx);
+	}
+}
+
+int mm81x_rc_sta_add(struct mm81x *mm, 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(&mm->chandef);
+	size_t table_mem_size;
+	struct mmrc_table *tb;
+
+	memset(&caps, 0, sizeof(caps));
+
+	mm81x_rc_sta_add_s1g_sta_caps(mm, &caps, s1g_cap);
+
+	/* Configure STA for support up to 8MHZ */
+	while (oper_bw_mhz > 0) {
+		caps.bandwidth |=
+			MMRC_MASK(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 (mm->hw->max_rates > 0 && mm->hw->max_rates < IEEE80211_TX_MAX_RATES)
+		caps.max_rates = mm->hw->max_rates;
+	else
+		caps.max_rates = IEEE80211_TX_MAX_RATES;
+
+	/* Set max reties */
+	if (mm->hw->max_rate_tries >= MMRC_MIN_CHAIN_ATTEMPTS &&
+	    mm->hw->max_rate_tries < MMRC_MAX_CHAIN_ATTEMPTS)
+		caps.max_retries = mm->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(&mm->mrc.lock);
+	kfree(msta->rc.tb);
+	msta->rc.tb = tb;
+	list_add(&msta->rc.list, &mm->mrc.stas);
+	msta->rc.last_update = jiffies;
+	spin_unlock_bh(&mm->mrc.lock);
+
+	return 0;
+}
+
+static void mm81x_rc_reinit_sta_iter(void *data, struct ieee80211_sta *sta)
+{
+	struct ieee80211_vif *vif = (struct ieee80211_vif *)data;
+	struct mm81x_sta *msta = (struct mm81x_sta *)sta->drv_priv;
+	struct mm81x_vif *mm_vif = ieee80211_vif_to_mm_vif(vif);
+	struct mm81x *mm = mm81x_vif_to_mm(mm_vif);
+	int oper_bw_mhz = cfg80211_chandef_get_width(&mm->chandef);
+
+	if (!msta || msta->vif != vif)
+		return;
+
+	mm81x_dbg(mm, MM81X_DBG_ANY,
+		  "Reinitialize sta %pM with new op_bw=%d, ts=%ld", sta->addr,
+		  oper_bw_mhz, jiffies);
+
+	mm81x_rc_sta_remove(mm, sta);
+	mm81x_rc_sta_add(mm, vif, sta);
+}
+
+void mm81x_rc_reinit_stas(struct mm81x *mm, struct ieee80211_vif *vif)
+{
+	ieee80211_iterate_stations_atomic(mm->hw, mm81x_rc_reinit_sta_iter,
+					  vif);
+}
+
+bool _mm81x_rc_set_fixed_rate(struct mm81x *mm, struct ieee80211_sta *sta,
+			      int mcs, int bw, int ss, int guard,
+			      const char *caller)
+{
+	struct mm81x_sta *msta = (struct mm81x_sta *)sta->drv_priv;
+	struct list_head *pos;
+	struct mmrc_rate fixed_rate;
+	bool ret_val = true;
+
+	fixed_rate.rate = mcs;
+	fixed_rate.bw = bw;
+	/*
+	 * Code spatial streams is zero based while user starts at 1, like the
+	 * real spatial streams.
+	 */
+	fixed_rate.ss = (ss - 1);
+	fixed_rate.guard = guard;
+
+	spin_lock_bh(&mm->mrc.lock);
+	list_for_each(pos, &mm->mrc.stas) {
+		struct mm81x_rc_sta *mrc_sta =
+			list_entry(pos, struct mm81x_rc_sta, list);
+
+		if (&msta->rc == mrc_sta) {
+			ret_val = mmrc_set_fixed_rate(msta->rc.tb, fixed_rate);
+			break;
+		}
+	}
+	spin_unlock_bh(&mm->mrc.lock);
+
+	if (!ret_val)
+		mm81x_err(mm, "failed, caller %s ss %d bw %d mcs %d guard %d",
+			  caller, ss, bw, mcs, guard);
+
+	return ret_val;
+}
+
+void mm81x_rc_sta_remove(struct mm81x *mm, struct ieee80211_sta *sta)
+{
+	struct mm81x_sta *msta = (struct mm81x_sta *)sta->drv_priv;
+
+	spin_lock_bh(&mm->mrc.lock);
+	if (msta->rc.tb) {
+		list_del_init(&msta->rc.list);
+		kfree(msta->rc.tb);
+		msta->rc.tb = NULL;
+	}
+	spin_unlock_bh(&mm->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 *mm, struct mm81x_sta *msta,
+				  struct mmrc_rate_table *rates, size_t size)
+{
+	int ret = -ENOENT;
+	struct list_head *pos;
+
+	spin_lock_bh(&mm->mrc.lock);
+	list_for_each(pos, &mm->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(&mm->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 *mm,
+				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(mm, 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 *mm, struct mm81x_sta *msta,
+				   struct mmrc_rate_table *rates, int attempts,
+				   bool was_aggregated)
+{
+	struct list_head *pos;
+
+	spin_lock_bh(&mm->mrc.lock);
+	list_for_each(pos, &mm->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(&mm->mrc.lock);
+}
+
+void mm81x_rc_sta_feedback_rates(struct mm81x *mm, 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(mm, 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)
+				mm81x_dbg(mm, MM81X_DBG_ANY,
+					  "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 *mm, 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(mm, 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(mm, 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
+		 */
+		mm81x_dbg(mm, MM81X_DBG_ANY, "Remove stale sta from rc list");
+		mm81x_rc_sta_remove(mm, sta);
+	}
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 20/35] wifi: mm81x: add rate_code.h
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, 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 19/35] wifi: mm81x: add ps.h
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, 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..6a33efa2cdb7
--- /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 *mm);
+void mm81x_ps_enable(struct mm81x *mm);
+int mm81x_ps_init(struct mm81x *mm);
+void mm81x_ps_finish(struct mm81x *mm);
+
+#endif /* !_MM81X_PS_H_ */
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 18/35] wifi: mm81x: add ps.c
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/ps.c | 239 +++++++++++++++++++++
 1 file changed, 239 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..432165697acf
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/ps.c
@@ -0,0 +1,239 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2026 Morse Micro
+ */
+#include <linux/types.h>
+#include <linux/atomic.h>
+#include <linux/slab.h>
+#include <linux/mutex.h>
+#include <linux/workqueue.h>
+#include <linux/completion.h>
+#include <linux/gpio/consumer.h>
+#include <linux/jiffies.h>
+#include "hif.h"
+#include "debug.h"
+#include "skbq.h"
+#include "mac.h"
+#include "bus.h"
+#include "ps.h"
+
+#define MM81X_WAKEUP_DELAY_MS 10
+
+static bool mm81x_ps_is_busy_pin_asserted(struct mm81x *mm)
+{
+	bool active_high =
+		!(mm->firmware_flags & MM81X_FW_FLAGS_BUSY_ACTIVE_LOW);
+
+	if (!mm->ps.gpios_supported)
+		return false;
+
+	return (gpiod_get_value_cansleep(mm->ps.busy_gpio) == active_high);
+}
+
+static void mm81x_ps_set_wake_gpio(struct mm81x *mm, bool raise)
+{
+	if (!mm->ps.gpios_supported)
+		return;
+
+	gpiod_set_value_cansleep(mm->ps.wake_gpio, raise);
+
+	mm81x_dbg(mm, MM81X_DBG_ANY, "%s wake up pin",
+		  (raise) ? "set" : "cleared");
+}
+
+static void mm81x_ps_wait_after_wake_pin_raise(struct mm81x *mm)
+{
+	unsigned long timeout;
+
+	if (!mm->ps.gpios_supported)
+		return;
+
+	if (mm81x_ps_is_busy_pin_asserted(mm))
+		return;
+
+	timeout = msecs_to_jiffies(MM81X_WAKEUP_DELAY_MS);
+	wait_for_completion_timeout(mm->ps.awake, timeout);
+}
+
+static void mm81x_ps_wakeup(struct mm81x_ps *mps)
+{
+	DECLARE_COMPLETION_ONSTACK(awake);
+	struct mm81x *mm = container_of(mps, struct mm81x, ps);
+
+	if (!mps->enable || !mps->suspended)
+		return;
+
+	WRITE_ONCE(mps->awake, &awake);
+	mm81x_ps_set_wake_gpio(mm, true);
+	mm81x_ps_wait_after_wake_pin_raise(mm);
+	WRITE_ONCE(mps->awake, NULL);
+
+	mm81x_set_bus_enable(mm, true);
+	mps->suspended = false;
+}
+
+static void mm81x_ps_sleep(struct mm81x_ps *mps)
+{
+	struct mm81x *mm = container_of(mps, struct mm81x, ps);
+
+	if (!mps->enable || mps->suspended)
+		return;
+
+	mps->suspended = true;
+	mm81x_set_bus_enable(mm, false);
+	mm81x_ps_set_wake_gpio(mm, false);
+}
+
+static irqreturn_t mm81x_ps_irq_handle(int irq, void *arg)
+{
+	struct mm81x_ps *mps = (struct mm81x_ps *)arg;
+	struct mm81x *mm = container_of(mps, struct mm81x, ps);
+	struct completion *awake = READ_ONCE(mps->awake);
+
+	if (awake)
+		complete(awake);
+	else
+		queue_work(mm->chip_wq, &mps->async_wake_work);
+
+	return IRQ_HANDLED;
+}
+
+static void mm81x_ps_async_wake_work(struct work_struct *work)
+{
+	struct mm81x_ps *mps =
+		container_of(work, struct mm81x_ps, async_wake_work);
+	struct mm81x *mm = container_of(mps, struct mm81x, ps);
+
+	if (mm81x_ps_is_busy_pin_asserted(mm)) {
+		mutex_lock(&mps->lock);
+		mm81x_ps_wakeup(mps);
+		mutex_unlock(&mps->lock);
+	}
+}
+
+static void mm81x_ps_evaluate(struct mm81x_ps *mps)
+{
+	struct mm81x *mm = container_of(mps, struct mm81x, ps);
+	bool needs_wake = false;
+	unsigned long expire;
+	unsigned long flags_on_entry =
+		(mm->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(mm) > 0);
+
+	if (needs_wake) {
+		mm81x_ps_wakeup(mps);
+		return;
+	}
+
+	if (!mm81x_ps_is_busy_pin_asserted(mm)) {
+		mm81x_ps_sleep(mps);
+		return;
+	}
+
+	expire = msecs_to_jiffies(DEFAULT_BUS_TIMEOUT_MS);
+	cancel_delayed_work(&mps->delayed_eval_work);
+	queue_delayed_work(mm->chip_wq, &mps->delayed_eval_work, expire);
+}
+
+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);
+	}
+}
+
+static int mm81x_ps_irq_init(struct mm81x *mm)
+{
+	int irq;
+	struct mm81x_ps *mps = &mm->ps;
+
+	if (!mm->ps.gpios_supported)
+		return 0;
+
+	irq = gpiod_to_irq(mm->ps.busy_gpio);
+	if (irq < 0)
+		return irq;
+
+	return request_irq(irq, (irq_handler_t)mm81x_ps_irq_handle,
+			   (mm->firmware_flags &
+			    MM81X_FW_FLAGS_BUSY_ACTIVE_LOW) ?
+				   IRQF_TRIGGER_FALLING :
+				   IRQF_TRIGGER_RISING,
+			   "mm81x_notify", mps);
+}
+
+static void mm81x_ps_irq_deinit(struct mm81x *mm)
+{
+	struct mm81x_ps *mps = &mm->ps;
+
+	if (mm->ps.gpios_supported)
+		free_irq(gpiod_to_irq(mm->ps.busy_gpio), mps);
+}
+
+void mm81x_ps_enable(struct mm81x *mm)
+{
+	struct mm81x_ps *mps = &mm->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 *mm)
+{
+	struct mm81x_ps *mps = &mm->ps;
+
+	if (mps->enable) {
+		mutex_lock(&mps->lock);
+		mps->wakers++;
+		mm81x_ps_evaluate(mps);
+		mutex_unlock(&mps->lock);
+	}
+}
+
+int mm81x_ps_init(struct mm81x *mm)
+{
+	struct mm81x_ps *mps = &mm->ps;
+
+	mps->enable = !(mm->bus_type == MM81X_BUS_TYPE_SDIO &&
+			!mm->ps.gpios_supported);
+	mps->suspended = true;
+	mps->wakers = 1; /* we default to being on */
+	mutex_init(&mps->lock);
+
+	INIT_WORK(&mps->async_wake_work, mm81x_ps_async_wake_work);
+	INIT_DELAYED_WORK(&mps->delayed_eval_work, mm81x_ps_evaluate_work);
+
+	return mm81x_ps_irq_init(mm);
+}
+
+void mm81x_ps_finish(struct mm81x *mm)
+{
+	struct mm81x_ps *mps = &mm->ps;
+
+	if (mps->enable) {
+		mps->enable = false;
+		mm81x_ps_irq_deinit(mm);
+		cancel_work_sync(&mps->async_wake_work);
+		cancel_delayed_work_sync(&mps->delayed_eval_work);
+	}
+}
-- 
2.43.0


^ permalink raw reply related

* [PATCH wireless-next 17/35] wifi: mm81x: add mmrc.h
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/mmrc.h | 198 +++++++++++++++++++
 1 file changed, 198 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..36b4a9cf551e
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/mmrc.h
@@ -0,0 +1,198 @@
+/* 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>
+
+#define BIT_COUNT(_x) (hweight_long(_x))
+
+/* 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
+
+/* Used to spehify supported features when initialising a STA */
+#define MMRC_MASK(x) (1u << (x))
+
+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 16/35] wifi: mm81x: add mmrc.c
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/mmrc.c | 1353 ++++++++++++++++++
 1 file changed, 1353 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..237055df2f21
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/mmrc.c
@@ -0,0 +1,1353 @@
+// 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) & MMRC_MASK(MMRC_BW_16MHZ)) ? MMRC_BW_16MHZ : \
+	 ((bw_caps) & MMRC_MASK(MMRC_BW_8MHZ))	? MMRC_BW_8MHZ :  \
+	 ((bw_caps) & MMRC_MASK(MMRC_BW_4MHZ))	? MMRC_BW_4MHZ :  \
+	 ((bw_caps) & MMRC_MASK(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 = BIT_COUNT(caps->rates);
+
+	/* Taking MCS10 into account as it is relevant for 1 MHz entries */
+	if (caps->rates & MMRC_MASK(MMRC_MCS10)) {
+		n_rates -= 1;
+		rows = 2;
+	}
+
+	rows += (BIT_COUNT(caps->bandwidth) * n_rates * BIT_COUNT(caps->guard) *
+		 BIT_COUNT(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 = BIT_COUNT(tb->caps.bandwidth);
+	u16 streams = BIT_COUNT(tb->caps.spatial_streams);
+	u16 guard = BIT_COUNT(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 = BIT_COUNT(tb->caps.rates);
+	u16 bw = BIT_COUNT(tb->caps.bandwidth);
+	u16 streams = BIT_COUNT(tb->caps.spatial_streams);
+	u16 guard = BIT_COUNT(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 & MMRC_MASK(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 |= MMRC_MASK(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 ((MMRC_MASK(fixed_rate.rate) & tb->caps.rates) == 0 ||
+	    (MMRC_MASK(fixed_rate.bw) & tb->caps.bandwidth) == 0 ||
+	    (MMRC_MASK(fixed_rate.ss) & tb->caps.spatial_streams) == 0 ||
+	    (MMRC_MASK(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 15/35] wifi: mm81x: add mac.h
From: Lachlan Hodges @ 2026-02-27  4:10 UTC (permalink / raw)
  To: johannes, Lachlan Hodges, Dan Callaghan, Arien Judge
  Cc: ayman.grais, linux-wireless, linux-kernel
In-Reply-To: <20260227041108.66508-1-lachlan.hodges@morsemicro.com>

(Patches split per file for review, see cover letter for more
information)

Signed-off-by: Lachlan Hodges <lachlan.hodges@morsemicro.com>
---
 drivers/net/wireless/morsemicro/mm81x/mac.h | 69 +++++++++++++++++++++
 1 file changed, 69 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..6e27ddf900b3
--- /dev/null
+++ b/drivers/net/wireless/morsemicro/mm81x/mac.h
@@ -0,0 +1,69 @@
+/* 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 bool mm81x_mac_is_sta_vif_associated(struct ieee80211_vif *vif)
+{
+	return vif->cfg.assoc;
+}
+
+static inline u32 mm81x_vif_generate_cssid(struct ieee80211_vif *vif)
+{
+	return mm81x_generate_cssid(vif->cfg.ssid, vif->cfg.ssid_len);
+}
+
+static inline u16 mm81x_mac_sta_aid(struct ieee80211_vif *vif)
+{
+	return vif->cfg.aid;
+}
+
+static inline __le32 mac2leuint32(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 *mm, u8 vif_id, bool rcu)
+{
+	if (WARN_ON(vif_id >= ARRAY_SIZE(mm->vifs)))
+		return NULL;
+
+	if (rcu)
+		return rcu_dereference(mm->vifs[vif_id]);
+
+	return rcu_dereference_protected(mm->vifs[vif_id],
+					 lockdep_is_held(&mm->lock));
+}
+
+int mm81x_tx_h_get_attempts(struct mm81x *mm,
+			    struct mm81x_skb_tx_status *tx_sts);
+struct mm81x *mm81x_mac_create(size_t priv_size, struct device *dev);
+int mm81x_mac_register(struct mm81x *mm);
+void mm81x_mac_destroy(struct mm81x *mm);
+void mm81x_mac_unregister(struct mm81x *mm);
+int mm81x_mac_event_recv(struct mm81x *mm, struct sk_buff *skb);
+void mm81x_mac_rx_skb(struct mm81x *mm, struct sk_buff *skb,
+		      struct mm81x_skb_rx_status *hdr_rx_status);
+void mm81x_mac_beacon_irq_handle(struct mm81x *mm, 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


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