Compex Model WLE7000Ex (mPCIE) to PCIe on X86_64 Ubuntu 26.04 - Confirmed working

pgupta at hindutool.com pgupta at hindutool.com
Sun Jun 7 13:03:13 PDT 2026


The following is a step by step guide to diagnose and troubleshoot Compex Wifi 7 WLE7000Ex modules connected to the PCIe bus 
on a plain vanilla Ubuntu 26.04 fresh install on X86_64 hardware.


Things to consider: 

1) If using Compex AP modules, you will need to get the board-2.bin and regdb.bin directly from them
2) These are SINGLE band modules (one 2.4, one 5, and one 6GHZ) as and on have one radio and one MAC
-------This is imporatnt later on as the default ath12k_wifi driver tries to bring up 2 radios per module 
-------In my setup, I have three modules (2.4, 5, 6) and each module is connected to its own PCIe slot
via and mPCIe

3) I am posting this in the hopes that this may help enthusiasts like me.  Perhaps patches to the driver might
be necessary for those of us with single band radios

This was created with the help of Claude Code - including troubleshooting.
I am NOT a programmer or kernel specialist by trade, merely a hobbyist.  I will try to answer any questions
but keep in mind that this may be above my knowledge/comprehension.

Prashant Gupta MD/MPH

================================================================================
 Ubuntu 26.04 (kernel 7.0) + Compex WLE7000E (Qualcomm QCN9274 hw2.0 / ath12k)
 Wi-Fi 7 radios fail to initialize — full troubleshooting + fix writeup
================================================================================
Author notes: written after resolving a ~4-month blocker on a Dell R730xd
router (nola-router) running Ubuntu 26.04 "Resolute Raccoon", kernel
7.0.0-22-generic, with THREE Compex WLE7000E single-band Wi-Fi 7 modules
(each a Qualcomm QCN9274 hw2.0 on its own PCIe slot: 2.4 GHz, 5 GHz, 6 GHz).
The same setup worked on Ubuntu 24.04 with the HWE kernel; it broke on 26.04.

This document is deliberately verbose about the REASONING and the DEAD ENDS,
not just the final fix, so that someone hitting the same symptoms can follow
the diagnostic logic rather than blindly copy commands. If you only want the
fix, jump to section "10. THE COMPLETE FIX".

--------------------------------------------------------------------------------
A. BASELINE / TEST ENVIRONMENT (compare this to YOUR setup first)
--------------------------------------------------------------------------------
The fix below was developed and verified on the exact baseline described here.
The closer your environment matches, the more directly it applies. The three
faults are: FIX 1/2 = driver (apply on any platform with this driver+firmware);
FIX 3 = x86 PCIe + IOMMU specific (may not apply on ARM/AHB or non-IOMMU hosts).

HARDWARE (host: "xxxx-router"; an identical second unit "xxxx-router" exists):
  - Server      : Dell PowerEdge R730xd, dual Intel Xeon (56 logical CPUs),
                  ~384 GiB RAM (dmesg "Memory: 402555264K ... available"),
                  iDRAC/BMC present. Large-RAM is RELEVANT: it makes low-memory
                  (<4 GB) contiguous allocation fail more readily (FIX 3).
  - Storage     : BTRFS RAID.
  - LAN NICs    : 2x Dell/Mellanox ConnectX-5 dual-port 100GbE (bonded+bridged
                  -> br0, br1), 1x Intel X710-DA4 quad-port 10GbE SFP+
                  (bonded+bridged -> br2). All SR-IOV capable. These are the
                  devices whose host-side DMA mode is affected by iommu=pt.
  - WAN         : onboard NIC (renamed ; not relevant here).
  - Wi-Fi (the subject): 3x Compex WLE7000E, each a Qualcomm QCN9274 hw2.0 on a
                  PCIe carrier, each used as a SINGLE band:
                     PCIe 0000:81:00.0 -> wlp129s0  board_id 0x1  2.4 GHz (Slot 1)
                     PCIe 0000:85:00.0 -> wlp133s0  board_id 0x2  5   GHz (Slot 2)
                     PCIe 0000:84:00.0 -> wlp132s0  board_id 0x4  6   GHz (Slot 3)
                  (PCIe bus->ifname is hex-derived: 0x81=129, 0x84=132, 0x85=133.
                  Bus/slot order is NOT ascending by band; confirm with dmidecode
                  -t slot. The interface NAMES are stable across reboots;
                  phy0/1/2 numbering is probe-order and is NOT stable.)
                  NOTE: "single-band per card" is the crux of FIX 1 — the driver
                  assumes the QCN9274 may be dual-MAC (max_radios=2).

SOFTWARE / OS:
  - Distro      : Ubuntu 26.04 LTS "Resolute Raccoon", server, x86_64.
  - Kernel      : 7.0.0-22-generic (Linux 7.0 GA baseline; no manual HWE).
  - Driver      : in-tree ath12k, MODULARIZED form -> ath12k.ko (common) +
                  ath12k_wifi7.ko (Wi-Fi 7), binds as ath12k_wifi7_pci. (There
                  is NO separate in-tree ath12k_pci fallback on this release.)
  - Firmware    : stock linux-firmware; ath12k/QCN9274/hw2.0/firmware-2.bin =
                  WLAN.WBE.1.6-01243-QCAHKSWPL_SILICONZ-1 (build 2025-12-09),
                  shipped as firmware-2.bin.zst. PLUS a Compex vendor board-2.bin
                  overlaid in /lib/firmware/ath12k/QCN9274/hw2.0/ (sha256
                  2378f4b8..., contains qmi-board-id 1/2/4). Firmware version is
                  NOT the variable (see section 6) - this is the as-shipped state.
  - Networking  : systemd-networkd (netplan renderer); LAN bridges br0/br1/br2
                  over bonds, plus a wireless bridge "wbr" that the 3 radios join
                  via hostapd. (Wi-Fi being down does not affect br0/1/2 routing.)
  - Virtualization: KVM/QEMU + libvirt; LAN-resident VMs (e.g. an nginx
                  reverse-proxy VM, a mail VM) attached via VIRTIO to the host
                  bridges (NOT PCI passthrough). This is why removing iommu=pt
                  is safe for the VMs (see section 8).
  - Secure Boot : DISABLED (mokutil --sb-state). If ENABLED, you must MOK-sign
                  the DKMS modules (extra step, see section 9).
  - Kernel cmdline BEFORE the fix (the broken state):
        ... ro intel_iommu=on iommu=pt crashkernel=<auto>
    (intel_iommu + iommu=pt are the relevant tokens; crashkernel comes from
     kdump-tools and is unrelated.)

ROUTER STATE AT TIME OF FAILURE:
  - The OS had just been REINSTALLED from scratch to Ubuntu 26.04; this was the
    FIRST Wi-Fi bring-up on 26.04. The IDENTICAL hardware worked previously on
    Ubuntu 24.04 with the HWE kernel (older, pre-modularization ath12k + that
    era's linux-firmware), which is why the regression was surprising.
  - All non-Wi-Fi router functions were healthy: interfaces, policy routing,
    nftables, DNS/DHCP, FRR, VPN, the KVM VMs - all up. ONLY the 3 Wi-Fi radios
    failed, so the box was usable throughout debugging (Wi-Fi is not on the boot
    or management path).
  - Observed failure: `iw phy` returned 0 Wiphy entries; dmesg showed the chain
    in section 1 on all three QCN9274 devices, every boot, deterministically.

REPRODUCIBILITY / SCOPE NOTES:
  - If you are on ARM / an AHB-attached QCN9274 (e.g. IPQ9574 boards) you likely
    need only FIX 1 + FIX 2; FIX 3 is the x86-PCIe + IOMMU coherent-DMA problem.
  - If your QCN9274 is genuinely dual-band/split-PHY (not single-band like the
    WLE7000E used here), FIX 1's premise differs - test carefully.
  - The DMA mask fact behind FIX 3 (dma_set_coherent_mask(DMA_BIT_MASK(32)) in
    ath12k pci.c) is driver-wide, so the 28 MB coherent-alloc failure can appear
    on any large-RAM x86 host running these cards with iommu=pt.

--------------------------------------------------------------------------------
0. TL;DR (the answer)
--------------------------------------------------------------------------------
The failure was THREE independent faults stacked on top of each other. All
three fixes are required; none alone is sufficient:

  FIX 1 (driver patch, via DKMS) - htc.c:
        Cap the WMI endpoint count to the firmware-reported radio count so the
        driver stops trying to open a WMI control endpoint for a second MAC
        (MAC1) that does not exist on a single-band QCN9274.

  FIX 2 (driver patch, via DKMS) - wmi.c:
        Make the WMI "dma ring caps" parser SKIP dma-ring entries whose
        module_id the driver doesn't recognize (firmware WLAN.WBE.1.6 sends
        module_id 2 and 3), instead of returning -EINVAL and aborting the whole
        service-ready-ext parse.

  FIX 3 (boot parameter, NOT a code change) - GRUB:
        Remove "iommu=pt" from the kernel command line (keep "intel_iommu=on").
        The QCN9274 sets a 32-bit COHERENT DMA mask, so the firmware's 28 MB
        HOST_DDR region needs a physically-contiguous buffer below 4 GB. With
        iommu=pt the coherent allocation goes through dma-direct and fails on a
        fragmented large-RAM x86 box. Full VT-d translation (no pt) lets the
        IOMMU remap scattered pages into a contiguous 32-bit IOVA, so the
        allocation succeeds.

Firmware version was NOT the cause. Stock Ubuntu 26.04 ships
WLAN.WBE.1.6-01243; both 1.6 and an older 1.5 fail identically. Do NOT waste
time downgrading firmware.

--------------------------------------------------------------------------------
1. SYMPTOMS
--------------------------------------------------------------------------------
- `iw phy` shows ZERO Wiphy entries (no radios enumerated).
- `lsmod` shows the driver loaded (on 26.04 it is the modularized pair
  `ath12k_wifi7` + `ath12k`, binding as `ath12k_wifi7_pci`).
- dmesg shows this chain (per PCIe device, e.g. 0000:81:00.0):

    ath12k_wifi7_pci 0000:81:00.0: Wi-Fi 7 Hardware name: qcn9274 hw2.0
    ath12k_wifi7_pci 0000:81:00.0: qmi dma allocation failed (29360128 B type 1), will try later with small size
    ath12k_wifi7_pci 0000:81:00.0: memory type 10 not supported
    ath12k_wifi7_pci 0000:81:00.0: chip_id 0x0 chip_family 0xb board_id 0x1 ...
    ath12k_wifi7_pci 0000:81:00.0: fw_version ... QC_IMAGE_VERSION_STRING=WLAN.WBE.1.6-01243-QCAHKSWPL_SILICONZ-1
    ath12k_wifi7_pci 0000:81:00.0: HTC Service WMI MAC1 connect request failed: 0x1)
    ath12k_wifi7_pci 0000:81:00.0: failed to connect to WMI CONTROL service status: -71
    ath12k_wifi7_pci 0000:81:00.0: Invalid module id 2
    ath12k_wifi7_pci 0000:81:00.0: failed to parse tlv -22
    ath12k_wifi7_pci 0000:81:00.0: failed to receive wmi unified ready event: -110
    ath12k_wifi7_pci 0000:81:00.0: failed to start core: -110
    ath12k_wifi7_pci 0000:81:00.0: qmi failed to send mode request, mode: 4, err = -5
    ath12k_wifi7_pci 0000:81:00.0: qmi failed to send wlan mode off

  (-71 = EPROTO, -22 = EINVAL, -110 = ETIMEDOUT, -5 = EIO.)

Useful one-shot diagnostic block:

    sudo modprobe ath12k
    sleep 3
    echo "Module load order:"; lsmod | grep -E '^(ath12k|ath12k_pci|ath12k_wifi7|mac80211|cfg80211)'
    echo "PHYs detected:";     iw phy 2>/dev/null | grep '^Wiphy' | sort
    echo "Recent ath12k events:"; journalctl -k -b | grep -iE 'ath12k|qcn9274' | tail -20

--------------------------------------------------------------------------------
2. WHY THE INITIAL "OBVIOUS" DIAGNOSES WERE WRONG
--------------------------------------------------------------------------------
The first instinct (and an earlier attempt on this project) was:
  "ath12k_wifi7 is a broken dual-MAC driver; blacklist it / patch the dual-MAC
   assumption; maybe downgrade firmware."
Most of that was a misread. Establishing the truth saved a lot of time:

  (a) `ath12k_wifi7.ko` is NOT a broken out-of-tree fork. The mainline ath12k
      driver was MODULARIZED in late 2025 into `ath12k.ko` (common core) +
      `ath12k_wifi7.ko` (Wi-Fi 7 family code). It is explicitly "Tested-on:
      QCN9274 hw2.0". It is SUPPOSED to bind your card. Blacklisting it just
      leaves the cards unbound (there is no in-tree fallback driver). DO NOT
      blacklist it.

  (b) The "HTC Service WMI MAC1 connect request failed" line is misleading. It
      is NOT proof the card is being treated as dual-band. It is a downstream
      symptom; see section 4.

  (c) The exact error quartet (wmi unified ready -110 / start core -110 /
      mode:4 err:-5) is widely reported across distros and is commonly "fixed"
      by downgrading linux-firmware. That is a RED HERRING for this hardware:
      see section 6 — both 1.6 and 1.5 firmware fail identically here.

Lesson: confirm what a component actually is before theorizing about it. Read
the driver source and the upstream mailing list, don't pattern-match on the
first error line.

--------------------------------------------------------------------------------
3. KEY REFERENCE THAT UNLOCKED IT (same exact hardware)
--------------------------------------------------------------------------------
OpenWrt forum thread for the Wallys DR9574 board, which is ALSO 3x single-band
QCN9274 hw2.0 (search: "wallys DR9574 IPQ9574 3x QCN9274 hw2.0 mainline openwrt
boot works wifi blocked by firmware rddm after wmi init"). That thread
identified two host-side ath12k patches for the SAME symptoms:
   - cap the WMI endpoint count to 1 when the OTP dual-mac bit is clear, and
   - warn-and-skip unknown dma-ring module_ids instead of failing the parse.
Those became our FIX 1 and FIX 2.

CRITICAL CAVEAT that cost us extra debugging: the DR9574 is IPQ9574, an ARM SoC
where the radios are on the internal AHB bus and firmware memory comes from a
device-tree reserved-memory region. It NEVER executes the PCIe QMI
dma_alloc_coherent path. So their two patches were necessary but NOT sufficient
on an x86 PCIe host — the x86 box has a THIRD, platform-specific fault (the
28 MB coherent DMA allocation, FIX 3). If you are on ARM/AHB you may only need
FIX 1 + FIX 2.

--------------------------------------------------------------------------------
4. ROOT-CAUSE ANALYSIS (read the source, don't guess)
--------------------------------------------------------------------------------
Get the exact source for YOUR kernel so line numbers/anchors match:

    sudo sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources
    sudo apt-get update
    cd /usr/src
    sudo apt-get source linux-image-unsigned-"$(uname -r)" || sudo apt-get source linux
    # source lands in /usr/src/linux-<ver>/ ; driver is at:
    #   /usr/src/linux-<ver>/drivers/net/wireless/ath/ath12k/

FAULT 1 — the bogus MAC1 WMI endpoint
  - wifi7/hw.c: the "qcn9274 hw2.0" hw_params entry sets `.max_radios = 2`
    (this exists to support split-PHY/dual-MAC QCN9274 variants).
  - htc.c, ath12k_htc_init(): preferred_hw_mode is still WMI_HOST_HW_MODE_MAX
    at this point ("overwritten when service_ext_ready is handled" — i.e.
    LATER), so the switch hits `default:` and sets
        htc->wmi_ep_count = ab->hw_params->max_radios;   // = 2
  - wmi.c, ath12k_wmi_connect(): loops i=0..wmi_ep_count-1 and calls
    ath12k_connect_pdev_htc_service(ab, i) for svc_id[] = { WMI_CONTROL,
    WMI_CONTROL_MAC1, WMI_CONTROL_MAC2 }. With wmi_ep_count=2 it tries MAC0
    (ok) and MAC1. A single-band card has no MAC1, so the firmware returns
    NOT_FOUND (0x1) -> "HTC Service WMI MAC1 connect request failed".
  - NOTE: that per-endpoint failure's return value is IGNORED by the loop, so
    strictly it is non-fatal. But it is wrong and has side effects (HTC credits
    are divided by wmi_ep_count, a dead endpoint is left around). FIX 1 caps it.

FAULT 2 — the fatal TLV parse abort (this is the real WMI-ready killer pre-IOMMU)
  - wmi.c, ath12k_wmi_dma_ring_caps(): for each advertised dma-ring cap it does
        if (le32_to_cpu(dma_caps[i].module_id) >= WMI_DIRECT_BUF_MAX) {
                ath12k_warn(ab, "Invalid module id %d\n", ...);
                ret = -EINVAL;
                goto free_dir_buff;     // <-- aborts the ENTIRE parse
        }
  - Firmware WLAN.WBE.1.6 (and 1.5) advertise module_id 2 AND 3, which are
    >= WMI_DIRECT_BUF_MAX in the kernel-7.0 enum. The -EINVAL propagates up
    through ath12k_wmi_svc_rdy_ext_parse -> ath12k_service_ready_ext_event,
    which logs "failed to parse tlv -22" and `goto err` WITHOUT completing the
    service-ready handshake. The firmware never reaches a state where it will
    answer WMI_INIT, so you eventually get "wmi unified ready -110".
  - These dma-ring modules are for optional features (spectral scan / CFR) that
    an AP router does not need, so skipping the unknown ones is safe. FIX 2.

FAULT 3 — the 28 MB HOST_DDR coherent allocation (x86 PCIe only)
  - dmesg: "qmi dma allocation failed (29360128 B type 1), will try later with
    small size".  29360128 B = 28 MiB. "type 1" = HOST_DDR_REGION_TYPE.
  - qmi.c, ath12k_qmi_alloc_chunk():
        chunk->v.addr = dma_alloc_coherent(ab->dev, chunk->size, &chunk->paddr,
                                           GFP_KERNEL | __GFP_NOWARN);
        if (!chunk->v.addr) {
                if (chunk->size > ATH12K_QMI_MAX_CHUNK_SIZE) {
                        ab->qmi.target_mem_delayed = true;   // ask FW to re-request smaller
                        ... "will try later with small size"
                        return -EAGAIN;                      // treated as non-fatal (0)
                }
                ...
        }
  - The driver then tells the firmware to re-request the region in smaller
    segments. The firmware boots far enough on the segmented memory to send
    "service ready" (that's why you see the FIX-2 "skipping ... module id"
    lines), BUT the segmented HOST_DDR does not satisfy what WMI_INIT needs, so
    unified-ready still times out (-110).
  - WHY the coherent alloc fails on a 400 GB-RAM machine: pci.c sets
        dma_set_coherent_mask(&pdev->dev, DMA_BIT_MASK(32));
    A 32-bit COHERENT mask means dma_alloc_coherent must return memory whose
    physical address is below 4 GB AND physically contiguous (28 MiB ~ order
    13). On a large, fragmented x86 box that contiguous-below-4-GB allocation
    fails.
  - "memory type 10 not supported" is BENIGN and a separate red herring:
    qmi.c handles unknown memory types by assigning paddr=0/NULL with the
    comment "FW will handle this without crashing." Do not chase it.

--------------------------------------------------------------------------------
5. DEAD END #1 — blacklisting ath12k_wifi7
--------------------------------------------------------------------------------
Tried: /etc/modprobe.d/ath12k-wifi7-blacklist.conf to fall back to an "older"
driver. Result: still 0 PHYs. There is NO separate in-tree PCI driver for these
cards on 26.04/7.0 — `ath12k_wifi7` registers the only matching PCI driver
(`ath12k_wifi7_pci`). Blacklisting just leaves the QCN9274s unbound. Abandoned.

--------------------------------------------------------------------------------
6. DEAD END #2 — downgrading firmware
--------------------------------------------------------------------------------
The error signature is widely "fixed" elsewhere by rolling back linux-firmware,
so we tested it. linux-firmware history for ath12k/QCN9274/hw2.0/firmware-2.bin
(GitLab mirror gitlab.com/kernel-firmware/linux-firmware):
    WLAN.WBE.1.6-01243  (commit ec76089d, Feb 2026)  <- stock 26.04, FAILS
    WLAN.WBE.1.5-01651  (commit 7aa2aba3, Aug 2025)
    WLAN.WBE.1.4.1-00199(commit a3f44afe, Feb 2025)
    WLAN.WBE.1.3.1-00162(commit a2d650c9, Oct 2024)

How to swap one in (kernel reads firmware-2.bin.zst natively; an uncompressed
firmware-2.bin takes precedence):
    cd /lib/firmware/ath12k/QCN9274/hw2.0/
    sudo mv firmware-2.bin.zst firmware-2.bin.zst.disabled
    sudo curl -fL -o firmware-2.bin \
      "https://gitlab.com/kernel-firmware/linux-firmware/-/raw/7aa2aba3/ath12k/QCN9274/hw2.0/firmware-2.bin"
    strings firmware-2.bin | grep WLAN.WBE     # confirm version
    sudo reboot
    # restore:
    #   sudo rm -f firmware-2.bin && sudo mv firmware-2.bin.zst.disabled firmware-2.bin.zst && sudo reboot

RESULT: 1.5 failed BYTE-IDENTICALLY (same "Invalid module id 2 / parse tlv -22 /
wmi unified ready -110"). Firmware version is not the variable here. We kept the
stock 1.6 and fixed the driver instead. (Note: the QCN9274 needs the
firmware-2.bin CONTAINER; the loose amss.bin/m3.bin in quic/upstream-wifi-fw do
NOT load on mainline ath12k for this chip.)

--------------------------------------------------------------------------------
7. DEAD END #3 — cma=512M (and why it didn't help, but pointed at the answer)
--------------------------------------------------------------------------------
Because FIX 3 is a contiguous-DMA problem, the textbook x86 fix is a CMA pool:
    GRUB_CMDLINE_LINUX_DEFAULT="... cma=512M"
This reserved the pool but the 28 MB alloc STILL failed, because:
  - dmesg: "cma: Reserved 512 MiB at 0x0000000100000000" — i.e. AT 4 GB. A
    32-bit-coherent-mask device cannot reach >= 4 GB, so the pool is unusable
    for this allocation.
  - The kernel-7.0 `cma=` early parser only honors a FIXED base (`cma=NN at base`);
    the range form `cma=512M at 0-4G` is ignored and it places the pool high.
  - A fixed sub-4 GB base (`cma=512M at 0x40000000`) CAN work but is fragile (must
    dodge reserved e820 regions) and only matters if you insist on keeping
    iommu=pt (see section 9). We chose FIX 3 (drop iommu=pt) instead.

--------------------------------------------------------------------------------
8. THE INSIGHT FOR FIX 3 — IOMMU translation vs passthrough
--------------------------------------------------------------------------------
With `intel_iommu=on iommu=pt`, host-driver devices use an IDENTITY domain, so
dma_alloc_coherent goes through dma-direct and needs physically-contiguous low
memory (which fails, above).

Remove `iommu=pt` (keep `intel_iommu=on`) and devices use a TRANSLATING domain.
dma_alloc_coherent then goes through iommu_dma_alloc_remap(), which:
  - allocates ordinary scattered order-0 pages (no large contiguous block
    needed), and
  - maps them into a CONTIGUOUS 32-bit IOVA window that satisfies the device's
    32-bit coherent mask.
The 28 MB allocation then cannot fail. This is the clean, fragmentation-proof
fix and needs no driver change.

Trade-off: dropping iommu=pt means the HOST's own device DMA (e.g. NICs while
the box routes packets) goes through VT-d translation instead of identity
mapping — a small overhead in practice (drivers like mlx5 reuse persistent
mappings, keeping the IOTLB warm). It does NOT affect KVM/libvirt guests:
  - virtio-net VMs (the normal case, e.g. an nginx reverse-proxy VM bridged to
    a host bridge) never touch the host IOMMU; zero impact.
  - PCI/SR-IOV passthrough to a VM uses VFIO, which requires intel_iommu=on
    (still present) and works with OR without iommu=pt.

--------------------------------------------------------------------------------
9. PREREQUISITES / ENVIRONMENT ASSUMPTIONS
--------------------------------------------------------------------------------
- Ubuntu 26.04, kernel 7.0.x (this writeup used 7.0.0-22-generic, x86_64).
- 3x Compex WLE7000E (QCN9274 hw2.0) on PCIe, used as single-band APs.
- Stock linux-firmware (WLAN.WBE.1.6-01243) + Compex board-2.bin overlay in
  /lib/firmware/ath12k/QCN9274/hw2.0/ (board-2.bin contains board IDs 1/2/4).
- Secure Boot DISABLED (check: `mokutil --sb-state`). If Secure Boot is ENABLED
  you must additionally MOK-sign the DKMS modules; dkms can do this but you must
  enroll the MOK key — out of scope here, but see `update-secureboot-policy` and
  `mokutil --import`.
- Build tools available; deb-src enabled (section 4).

--------------------------------------------------------------------------------
10. THE COMPLETE FIX
--------------------------------------------------------------------------------
Step 10.1 — confirm Secure Boot is off and install build prerequisites:

    mokutil --sb-state            # expect: SecureBoot disabled
    sudo apt-get update
    sudo apt-get install -y build-essential dkms "linux-headers-$(uname -r)"

Step 10.2 — enable source repo and fetch the kernel source (for the ath12k tree):

    sudo sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources
    sudo apt-get update
    cd /usr/src
    sudo apt-get source linux-image-unsigned-"$(uname -r)" || sudo apt-get source linux
    # verify the driver source exists (note the linux-<ver> dir name):
    ls /usr/src/linux-*/drivers/net/wireless/ath/ath12k/wmi.c

Step 10.3 — build + install the patched ath12k via DKMS.
Paste the whole block below into a ROOT shell (run `sudo -i` first).  It is
heredoc-based, so NO script file is needed and it pastes cleanly over SSH.  It copies the
ath12k tree, applies BOTH patches (exact-match, aborts if anything doesn't
match), writes dkms.conf, builds against your running kernel's headers, and
installs to /lib/modules/$(uname -r)/updates/dkms/ (which overrides the in-tree
modules via depmod precedence). The patches are in the common ath12k.ko; the
wifi7/ subdir is untouched but rebuilt as a matched pair.

# ===================== BEGIN PASTE (run as root) =====================
# Build a patched ath12k for single-band QCN9274 hw2.0 on Ubuntu 26.04/kernel 7.0
# FIX 1 (htc.c): cap wmi_ep_count to ab->qmi.num_radios (no bogus MAC1 endpoint)
# FIX 2 (wmi.c): skip dma-ring caps with unknown module_id instead of -EINVAL
set -euo pipefail

PKG=ath12k-qcn9274sb
VER=1.0
KVER="$(uname -r)"
SRC="/usr/src/linux-${KVER%%-*}/drivers/net/wireless/ath/ath12k"   # e.g. linux-7.0.0
DST="/usr/src/${PKG}-${VER}"
KBUILD="/lib/modules/${KVER}/build"

echo "== ath12k single-band QCN9274 DKMS build =="
echo "   kernel : $KVER"; echo "   source : $SRC"; echo "   dkms   : $DST"
[ -d "$SRC" ]    || { echo "ERROR: $SRC not found. Run: cd /usr/src && apt-get source linux"; exit 1; }
[ -d "$KBUILD" ] || { echo "ERROR: $KBUILD missing. Install linux-headers-$KVER"; exit 1; }

# 0. clean any prior attempt
if dkms status -m "$PKG" -v "$VER" 2>/dev/null | grep -q "$PKG"; then
    dkms remove -m "$PKG" -v "$VER" --all || true
fi
rm -rf "$DST"

# 1. copy pristine ath12k tree (includes wifi7/ subdir)
mkdir -p "$DST"
cp -a "$SRC/." "$DST/"
find "$DST" \( -name '*.o' -o -name '*.ko' -o -name '*.mod' -o -name '*.mod.c' \
            -o -name '.*.cmd' -o -name 'modules.order' -o -name 'Module.symvers' \) -delete 2>/dev/null || true

# 2. apply the two patches (exact-match; aborts if anchors don't match byte-for-byte)
python3 - "$DST" <<'PYEOF'
import sys, os
def patch(path, old, new, label):
    d = open(path).read()
    n = d.count(old)
    if n != 1:
        sys.exit(f"[FAIL] {label}: expected 1 match in {os.path.basename(path)}, found {n}")
    open(path, "w").write(d.replace(old, new, 1))
    print(f"[ OK ] {label}")
ATH = sys.argv[1]; HTC = ATH+"/htc.c"; WMI = ATH+"/wmi.c"

# ---- FIX 1: htc.c default case ----
A_OLD = ("\tdefault:\n\t\thtc->wmi_ep_count = ab->hw_params->max_radios;\n"
         "\t\tbreak;\n\t}\n")
A_NEW = ("\tdefault:\n\t\thtc->wmi_ep_count = ab->hw_params->max_radios;\n"
         "\t\t/* QCN9274 single-band fix: single-MAC cards advertise fewer PHYs\n"
         "\t\t * via QMI (ab->qmi.num_radios) than hw_params->max_radios (2 for\n"
         "\t\t * qcn9274 hw2.0). Opening a WMI endpoint for the absent MAC1 fails\n"
         "\t\t * (\"HTC Service WMI MAC1 connect request failed\"). Cap to the\n"
         "\t\t * firmware-reported radio count when it is valid (>=1).\n\t\t */\n"
         "\t\tif (ab->qmi.num_radios != U8_MAX &&\n"
         "\t\t    ab->qmi.num_radios >= 1 &&\n"
         "\t\t    ab->qmi.num_radios < htc->wmi_ep_count)\n"
         "\t\t\thtc->wmi_ep_count = ab->qmi.num_radios;\n\t\tbreak;\n\t}\n")

# ---- FIX 2: wmi.c add compaction index + skip unknown module ids ----
B1_OLD = ("\tstruct ath12k_dbring_cap *dir_buff_caps;\n\tint ret;\n\tu32 i;\n")
B1_NEW = ("\tstruct ath12k_dbring_cap *dir_buff_caps;\n\tint ret;\n\tu32 i, j;\n")

B2_OLD = ("\tdir_buff_caps = ab->db_caps;\n"
          "\tfor (i = 0; i < dma_caps_parse->n_dma_ring_caps; i++) {\n"
          "\t\tif (le32_to_cpu(dma_caps[i].module_id) >= WMI_DIRECT_BUF_MAX) {\n"
          "\t\t\tath12k_warn(ab, \"Invalid module id %d\\n\",\n"
          "\t\t\t\t    le32_to_cpu(dma_caps[i].module_id));\n"
          "\t\t\tret = -EINVAL;\n\t\t\tgoto free_dir_buff;\n\t\t}\n\n"
          "\t\tdir_buff_caps[i].id = le32_to_cpu(dma_caps[i].module_id);\n"
          "\t\tdir_buff_caps[i].pdev_id =\n"
          "\t\t\tDP_HW2SW_MACID(le32_to_cpu(dma_caps[i].pdev_id));\n"
          "\t\tdir_buff_caps[i].min_elem = le32_to_cpu(dma_caps[i].min_elem);\n"
          "\t\tdir_buff_caps[i].min_buf_sz = le32_to_cpu(dma_caps[i].min_buf_sz);\n"
          "\t\tdir_buff_caps[i].min_buf_align = le32_to_cpu(dma_caps[i].min_buf_align);\n"
          "\t}\n\n\treturn 0;\n\nfree_dir_buff:\n"
          "\tath12k_wmi_free_dbring_caps(ab);\n\treturn ret;\n}\n")
B2_NEW = ("\tdir_buff_caps = ab->db_caps;\n"
          "\tfor (i = 0, j = 0; i < dma_caps_parse->n_dma_ring_caps; i++) {\n"
          "\t\tif (le32_to_cpu(dma_caps[i].module_id) >= WMI_DIRECT_BUF_MAX) {\n"
          "\t\t\t/* QCN9274 single-band fix: fw WLAN.WBE.1.5/1.6 advertises\n"
          "\t\t\t * dma-ring module ids the host enum does not know yet; skip\n"
          "\t\t\t * them instead of failing the whole service-ready-ext parse\n"
          "\t\t\t * (which would block the WMI unified-ready event).\n\t\t\t */\n"
          "\t\t\tath12k_warn(ab, \"skipping unsupported dma ring module id %d\\n\",\n"
          "\t\t\t\t    le32_to_cpu(dma_caps[i].module_id));\n"
          "\t\t\tcontinue;\n\t\t}\n\n"
          "\t\tdir_buff_caps[j].id = le32_to_cpu(dma_caps[i].module_id);\n"
          "\t\tdir_buff_caps[j].pdev_id =\n"
          "\t\t\tDP_HW2SW_MACID(le32_to_cpu(dma_caps[i].pdev_id));\n"
          "\t\tdir_buff_caps[j].min_elem = le32_to_cpu(dma_caps[i].min_elem);\n"
          "\t\tdir_buff_caps[j].min_buf_sz = le32_to_cpu(dma_caps[i].min_buf_sz);\n"
          "\t\tdir_buff_caps[j].min_buf_align = le32_to_cpu(dma_caps[i].min_buf_align);\n"
          "\t\tj++;\n\t}\n\n\tab->num_db_cap = j;\n\n\treturn 0;\n}\n")

patch(HTC, A_OLD, A_NEW, "FIX 1  htc.c wmi_ep_count cap")
patch(WMI, B1_OLD, B1_NEW, "FIX 2a wmi.c add index j")
patch(WMI, B2_OLD, B2_NEW, "FIX 2b wmi.c skip unknown dma-ring module id")
print("[DONE] patches applied")
PYEOF

# 3. dkms.conf
cat > "$DST/dkms.conf" <<EOF
PACKAGE_NAME="$PKG"
PACKAGE_VERSION="$VER"
AUTOINSTALL="yes"
MAKE[0]="make -C \${kernel_source_dir} M=\${dkms_tree}/\${PACKAGE_NAME}/\${PACKAGE_VERSION}/build modules"
CLEAN="make -C \${kernel_source_dir} M=\${dkms_tree}/\${PACKAGE_NAME}/\${PACKAGE_VERSION}/build clean"
BUILT_MODULE_NAME[0]="ath12k"
BUILT_MODULE_LOCATION[0]="."
DEST_MODULE_LOCATION[0]="/updates/dkms"
BUILT_MODULE_NAME[1]="ath12k_wifi7"
BUILT_MODULE_LOCATION[1]="wifi7"
DEST_MODULE_LOCATION[1]="/updates/dkms"
EOF

# 4. build + install
dkms add -m "$PKG" -v "$VER"
if ! dkms build -m "$PKG" -v "$VER"; then
    echo "!! build failed; tail of make.log:"
    find /var/lib/dkms/$PKG/$VER -name make.log -exec tail -n 40 {} \;
    exit 1
fi
dkms install -m "$PKG" -v "$VER" --force
depmod -a "$KVER"

# 5. confirm the patched modules win
echo "== install paths (must be under updates/dkms) =="
modinfo -n ath12k
modinfo -n ath12k_wifi7
echo "== DONE. Next: edit GRUB (remove iommu=pt), then reboot. =="
# ===================== END PASTE =====================

Expected tail:
    [ OK ] FIX 1  htc.c wmi_ep_count cap
    [ OK ] FIX 2a wmi.c add index j
    [ OK ] FIX 2b wmi.c skip unknown dma-ring module id
    ...
    /lib/modules/<ver>/updates/dkms/ath12k.ko.zst
    /lib/modules/<ver>/updates/dkms/ath12k_wifi7.ko.zst

Step 10.4 — FIX 3: remove iommu=pt from the kernel command line.
Edit /etc/default/grub. In GRUB_CMDLINE_LINUX_DEFAULT (and GRUB_CMDLINE_LINUX if
present), DELETE the "iommu=pt" token. KEEP "intel_iommu=on". Example result:

    GRUB_CMDLINE_LINUX_DEFAULT="intel_iommu=on"

(If you previously added cma=512M it is now harmless/irrelevant; you can leave
or remove it.) Then:

    grep -E '^GRUB_CMDLINE' /etc/default/grub     # sanity check
    sudo update-grub
    sudo reboot

Step 10.5 — VERIFY:
    cat /proc/cmdline                              # intel_iommu=on present, iommu=pt absent
    iw phy | grep Wiphy                            # expect 3 lines: phy0 phy1 phy2
    iw dev                                         # expect wlp129s0 / wlp132s0 / wlp133s0
    dmesg | grep -iE 'ath12k|qmi dma|wmi unified|start core' | tail -40

SUCCESS looks like:
  - NO "qmi dma allocation failed (... type 1)" line.
  - NO "failed to receive wmi unified ready" / "failed to start core".
  - Interfaces renamed, e.g.:
        ath12k_wifi7_pci 0000:81:00.0 wlp129s0: renamed from wlan0   (board_id 0x1 = 2.4 GHz)
        ath12k_wifi7_pci 0000:85:00.0 wlp133s0: renamed from wlan0   (board_id 0x2 = 5 GHz)
        ath12k_wifi7_pci 0000:84:00.0 wlp132s0: renamed from wlan0   (board_id 0x4 = 6 GHz)
  - These remain and are EXPECTED/benign:
        "memory type 10 not supported"            (FW tolerates; paddr=NULL)
        "skipping unsupported dma ring module id 2/3"   (FIX 2 working as intended)
        "module verification failed: signature ... tainting kernel"  (unsigned out-of-tree module; fine with Secure Boot off)

--------------------------------------------------------------------------------
11. OPTIONAL — keep iommu=pt for max host routing throughput
--------------------------------------------------------------------------------
If you must keep iommu=pt (e.g. you want identity-mapped host NIC DMA on a very
high-throughput box), the alternative to FIX 3 is to give dma-direct a
contiguous CMA pool BELOW 4 GB at a fixed base that avoids reserved e820 ranges:

    GRUB_CMDLINE_LINUX_DEFAULT="... intel_iommu=on iommu=pt cma=512M at 0x40000000"

Then after reboot confirm: dmesg "cma: Reserved 512 MiB at 0x<below 0x100000000>"
and that "qmi dma allocation failed (type 1)" is gone. Validate the base against
your memory map (`dmesg | grep -i e820`) first; a bad base silently fails to
reserve. This path was NOT used in the primary fix (dropping iommu=pt is simpler
and fragmentation-proof), but it is the perf-preserving option.

--------------------------------------------------------------------------------
12. PERSISTENCE / MAINTENANCE
--------------------------------------------------------------------------------
- DKMS AUTOINSTALL="yes" means the modules are rebuilt when a new kernel is
  installed, PROVIDED matching linux-headers are present (they ship with the
  kernel meta-packages).
- CAVEAT: the DKMS source is a COPY of the ath12k tree from the kernel you built
  against. A future kernel whose ath12k internal API has drifted may FAIL to
  rebuild this older source, leaving Wi-Fi down until the patch is re-ported to
  the new source (the patch anchors in the build script may need updating).
  POLICY (this deployment): option (b) — do NOT pin the kernel.  Re-run the
  section 10.3 paste block after each kernel bump and fix any patch anchors
  that no longer match (the patch step aborts loudly if so — it never silently
  mis-patches).  Option (a) — pin via `sudo apt-mark hold linux-image-generic
  linux-headers-generic linux-generic` — is the alternative if Wi-Fi must
  survive an unattended kernel update, but it also holds kernel security
  updates (not chosen for an internet-facing box).
  (In the production router repo this is AUTOMATED — see Doc 10 PART A0.6: a
  /etc/kernel/postinst.d hook + a boot-time verify/self-heal service, both
  driven by a single /usr/local/sbin/ath12k-repatch helper.  A standalone
  reader can replicate that or just re-run the section 10.3 block by hand.)
- The GRUB change (FIX 3) persists across kernel updates automatically.
- These two patches are the upstream-pending DR9574 fixes plus a dma-ring
  tolerance fix. If/when they merge into mainline ath12k, you can `dkms remove`
  the package and run the stock driver.

--------------------------------------------------------------------------------
13. QUICK REFERENCE — the whole fix in one place
--------------------------------------------------------------------------------
1) mokutil --sb-state                         # must be: SecureBoot disabled
2) apt-get install build-essential dkms linux-headers-$(uname -r)
3) enable deb-src; apt-get source linux       # provides /usr/src/linux-*/.../ath12k
4) paste the DKMS block in section 10.3 (as root)   # FIX 1 + FIX 2 -> updates/dkms
5) edit /etc/default/grub: remove iommu=pt, keep intel_iommu=on   # FIX 3
6) sudo update-grub && sudo reboot
7) iw phy | grep Wiphy                         # 3 radios = done
================================================================================

--------------------------------------------------------------------------------
14. CONFIRMED-WORKING OUTPUT (captured from nola-router AFTER the fix)
--------------------------------------------------------------------------------
The following is the actual post-fix output proving all three Compex WLE7000E /
QCN9274 hw2.0 radios initialize and are band-capable. This is the "after" state
that corresponds to the broken "before" state in section 1.

--- kernel command line (FIX 3 applied: intel_iommu=on present, iommu=pt gone) ---
$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-7.0.0-22-generic root=UUID=df03d4bf-8eed-4089-a486-f100a9edac93 ro intel_iommu=on cma=512M crashkernel=2G-4G:320M,4G-32G:512M,32G-64G:1024M,64G-128G:2048M,128G-:4096M
  (cma=512M is leftover from a dead-end test and is harmless; the operative
   change is the ABSENCE of iommu=pt.)

--- patched modules are the ones loaded (FIX 1+2 via DKMS, under updates/dkms) ---
$ modinfo -n ath12k ath12k_wifi7
/lib/modules/7.0.0-22-generic/updates/dkms/ath12k.ko.zst
/lib/modules/7.0.0-22-generic/updates/dkms/ath12k_wifi7.ko.zst

--- three PHYs now enumerate ---
$ iw phy | grep Wiphy
Wiphy phy2
Wiphy phy1
Wiphy phy0

--- dmesg: clean bring-up (NO "qmi dma allocation failed", NO "wmi unified ready -110") ---
$ dmesg | grep -iE 'ath12k|qmi dma|wmi unified|start core' | tail -40
[   12.542027] ath12k: loading out-of-tree module taints kernel.
[   12.542033] ath12k: module verification failed: signature and/or required key missing - tainting kernel
[   12.703250] ath12k_wifi7_pci 0000:81:00.0: BAR 0 [mem 0xce400000-0xce5fffff 64bit]: assigned
[   12.703316] ath12k_wifi7_pci 0000:81:00.0: Wi-Fi 7 Hardware name: qcn9274 hw2.0
[   12.703627] ath12k_wifi7_pci 0000:81:00.0: MSI vectors: 16
[   13.512996] ath12k_wifi7_pci 0000:84:00.0: BAR 0 [mem 0xce200000-0xce3fffff 64bit]: assigned
[   13.513073] ath12k_wifi7_pci 0000:84:00.0: Wi-Fi 7 Hardware name: qcn9274 hw2.0
[   13.513480] ath12k_wifi7_pci 0000:84:00.0: MSI vectors: 16
[   14.238316] ath12k_wifi7_pci 0000:85:00.0: BAR 0 [mem 0xce000000-0xce1fffff 64bit]: assigned
[   14.238389] ath12k_wifi7_pci 0000:85:00.0: Wi-Fi 7 Hardware name: qcn9274 hw2.0
[   14.238702] ath12k_wifi7_pci 0000:85:00.0: MSI vectors: 16
[   15.018704] ath12k_wifi7_pci 0000:81:00.0: memory type 10 not supported
[   15.019946] ath12k_wifi7_pci 0000:84:00.0: memory type 10 not supported
[   15.021249] ath12k_wifi7_pci 0000:85:00.0: memory type 10 not supported
[   15.023268] ath12k_wifi7_pci 0000:81:00.0: chip_id 0x0 chip_family 0xb board_id 0x1 soc_id 0x401a2200
[   15.023275] ath12k_wifi7_pci 0000:81:00.0: fw_version 0x160484db fw_build_timestamp 2025-12-09 19:48 fw_build_id QC_IMAGE_VERSION_STRING=WLAN.WBE.1.6-01243-QCAHKSWPL_SILICONZ-1
[   15.024491] ath12k_wifi7_pci 0000:84:00.0: chip_id 0x0 chip_family 0xb board_id 0x4 soc_id 0x401a2200
[   15.024504] ath12k_wifi7_pci 0000:84:00.0: fw_version 0x160484db fw_build_timestamp 2025-12-09 19:48 fw_build_id QC_IMAGE_VERSION_STRING=WLAN.WBE.1.6-01243-QCAHKSWPL_SILICONZ-1
[   15.025772] ath12k_wifi7_pci 0000:85:00.0: chip_id 0x0 chip_family 0xb board_id 0x2 soc_id 0x401a2200
[   15.025787] ath12k_wifi7_pci 0000:85:00.0: fw_version 0x160484db fw_build_timestamp 2025-12-09 19:48 fw_build_id QC_IMAGE_VERSION_STRING=WLAN.WBE.1.6-01243-QCAHKSWPL_SILICONZ-1
[   17.216586] ath12k_wifi7_pci 0000:81:00.0: skipping unsupported dma ring module id 2
[   17.216604] ath12k_wifi7_pci 0000:81:00.0: skipping unsupported dma ring module id 3
[   17.339486] ath12k_wifi7_pci 0000:85:00.0: skipping unsupported dma ring module id 2
[   17.339507] ath12k_wifi7_pci 0000:85:00.0: skipping unsupported dma ring module id 3
[   17.462641] ath12k_wifi7_pci 0000:81:00.0 wlp129s0: renamed from wlan0
[   17.483645] ath12k_wifi7_pci 0000:85:00.0 wlp133s0: renamed from wlan0
[   17.927542] ath12k_wifi7_pci 0000:84:00.0: skipping unsupported dma ring module id 2
[   17.927557] ath12k_wifi7_pci 0000:84:00.0: skipping unsupported dma ring module id 3
[   18.155264] ath12k_wifi7_pci 0000:84:00.0 wlp132s0: renamed from wlan0
  (The only remaining messages are the EXPECTED/benign ones: the unsigned-module
   taint, "memory type 10 not supported", and the FIX-2 "skipping ... module id
   2/3". The fatal lines from section 1 are all gone.)

--- the three net interfaces exist (one per radio) ---
$ iw dev
phy#2
    Interface wlp132s0
        ifindex 16
        wdev 0x200000001
        addr 04:f0:21:cd:3b:3b
        type managed
phy#1
    Interface wlp133s0
        ifindex 15
        wdev 0x100000001
        addr 04:f0:21:c9:e0:fe
        type managed
phy#0
    Interface wlp129s0
        ifindex 14
        wdev 0x1
        addr 04:f0:21:c9:e0:84
        type managed

--- per-radio band capability (each card on its own band, HE/VHT/EHT present) ---
$ for p in 0 1 2; do echo "== phy$p =="; iw phy phy$p info | grep -iE '^\s*Band|MHz \[|EHT|VHT|HE ' | head -5; done
== phy0 ==        # wlp129s0  -> Band 1 = 2.4 GHz
    Band 1:
        HE Iftypes: managed
            HE MAC Capabilities (0x000b9a181040):
                +HTC HE Supported
            HE PHY Capabilities: (0x026040890fc39f1c110e00):
== phy1 ==        # wlp133s0  -> Band 2 = 5 GHz
    Band 2:
        VHT Capabilities (0x338bf9f6):
        VHT RX MCS set:
        VHT RX highest supported: 0 Mbps
        VHT TX MCS set:
== phy2 ==        # wlp132s0  -> Band 4 = 6 GHz
    Band 4:
        HE Iftypes: managed
            HE MAC Capabilities (0x000b9a181840):
                +HTC HE Supported
            HE PHY Capabilities: (0x0c634089ffdb9f1c110e00):

RESULT: 3/3 radios up and band-capable.
  phy0 / wlp129s0  Band 1 (2.4 GHz)  MAC 04:f0:21:c9:e0:84  board_id 0x1  PCIe 0000:81:00.0
  phy1 / wlp133s0  Band 2 (5   GHz)  MAC 04:f0:21:c9:e0:fe  board_id 0x2  PCIe 0000:85:00.0
  phy2 / wlp132s0  Band 4 (6   GHz)  MAC 04:f0:21:cd:3b:3b  board_id 0x4  PCIe 0000:84:00.0
Radios come up in "managed" type; hostapd then switches them to AP mode.
Captured on Ubuntu 26.04, kernel 7.0.0-22-generic, 2026-06-06.
================================================================================



More information about the ath12k mailing list