From 10809ca2b3654885c912fd703b5192e8d36e9b84 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Wed, 9 Sep 2026 15:28:21 -0400 Subject: [PATCH 01/22] wifi: apple: add hardware-qualified N1 driver and J813 workbench [Thomas: the driver part of 9f4a5246489a on the J813 line of linux-aurora] Signed-off-by: Thomas Glanzmann --- drivers/net/wireless/apple/Kconfig | 20 +- drivers/net/wireless/apple/centauri/Makefile | 3 +- .../net/wireless/apple/centauri/centauri.h | 57 +- .../wireless/apple/centauri/centauri_ipc.c | 478 ++++ .../wireless/apple/centauri/centauri_pci.c | 278 ++ .../wireless/apple/centauri/centauri_port.c | 192 ++ .../wireless/apple/centauri/centauri_wifi.c | 2312 +++++++++++++++++ 7 files changed, 3282 insertions(+), 58 deletions(-) create mode 100644 drivers/net/wireless/apple/centauri/centauri_ipc.c create mode 100644 drivers/net/wireless/apple/centauri/centauri_pci.c create mode 100644 drivers/net/wireless/apple/centauri/centauri_port.c create mode 100644 drivers/net/wireless/apple/centauri/centauri_wifi.c diff --git a/drivers/net/wireless/apple/Kconfig b/drivers/net/wireless/apple/Kconfig index e40d5d9c5fd53..d3ead4d9259e7 100644 --- a/drivers/net/wireless/apple/Kconfig +++ b/drivers/net/wireless/apple/Kconfig @@ -2,18 +2,24 @@ config WLAN_VENDOR_APPLE bool "Apple wireless devices" default y + help + Support for Apple's wireless network devices. if WLAN_VENDOR_APPLE config APPLE_CENTAURI - tristate "Apple N1 Centauri firmware and IPC transport" + tristate "Apple N1 Centauri experimental J813 Wi-Fi" depends on ARCH_APPLE || COMPILE_TEST - depends on PCI && CFG80211 && PCIE_APPLE_APIF + depends on PCI && CFG80211 && DEBUG_FS select FW_LOADER help - Boot the shared N1 firmware and establish its PCI DMA contexts. - Requires a preinitialized APIF host with the supported reset layout - and DT-supplied personalized firmware name and platform identifier. - The endpoint must start in the stopped ROM personality. Firmware - and per-device calibration are external inputs and are not included. + Experimental Apple N1 PCIe and ACIPC Wi-Fi support for the J813 + MacBook Air. Firmware boot, ring setup and connection setup currently + use debugfs and the tools/aurora/j813 workbench. The tested single-link + WPA2 setup completes DHCP and bidirectional traffic. Suspend/resume, + roaming and automatic startup are not yet qualified. + + The firmware and per-device calibration are private external inputs. + This driver does not include Bluetooth HCI support. + To build as a module, choose M; the module is apple_centauri. endif diff --git a/drivers/net/wireless/apple/centauri/Makefile b/drivers/net/wireless/apple/centauri/Makefile index ddd2501998887..1a17e5283e227 100644 --- a/drivers/net/wireless/apple/centauri/Makefile +++ b/drivers/net/wireless/apple/centauri/Makefile @@ -1,4 +1,3 @@ # SPDX-License-Identifier: GPL-2.0-only obj-$(CONFIG_APPLE_CENTAURI) += apple_centauri.o -apple_centauri-y := centauri_pci.o centauri_ipc.o -apple_centauri-$(CONFIG_APPLE_CENTAURI_WIFI) += centauri_wifi.o +apple_centauri-y := centauri_pci.o centauri_port.o centauri_ipc.o centauri_wifi.o diff --git a/drivers/net/wireless/apple/centauri/centauri.h b/drivers/net/wireless/apple/centauri/centauri.h index 55106d4ef0794..f7fdf4105a3b3 100644 --- a/drivers/net/wireless/apple/centauri/centauri.h +++ b/drivers/net/wireless/apple/centauri/centauri.h @@ -1,87 +1,46 @@ - +/* SPDX-License-Identifier: GPL-2.0-only */ #ifndef AURORA_CENTAURI_H #define AURORA_CENTAURI_H #include -#include #include #include #include -#include -#define CEN_ALPHA_WINDOW_SIZE SZ_32M #define CEN_ALPHA_SCRATCH_OFFSET SZ_8M #define CEN_ALPHA_SCRATCH_SIZE (5 * SZ_1M) struct centauri_ipc; struct centauri_wifi; -struct centauri_irqs { - struct pci_dev *pdev; - void *data; - unsigned int count; -}; - struct cen_alpha_resources { - struct pci_dev *pdev; void *context, *messages, *window; dma_addr_t context_dma, messages_dma, window_dma; void __iomem *bar; u32 index[4], mtr, mcr; }; -enum centauri_boot_phase { - CEN_BOOT_ROM, - CEN_BOOT_CYCLE, - CEN_BOOT_IPC, - CEN_BOOT_READY, - CEN_BOOT_FAILED, - CEN_BOOT_STOPPED, -}; - struct centauri { struct pci_dev *pdev; void __iomem *bar; resource_size_t size; - u32 platform_id, protocol; - const char *firmware_name; + u32 stage_offset; struct dentry *debug; struct mutex boot_lock; - struct delayed_work boot_work; - enum centauri_boot_phase boot_phase; - bool removing, ready; - void *firmware, *descriptor; - struct centauri_irqs irqs, alpha_irqs; - dma_addr_t firmware_dma, descriptor_dma; + void *firmware, *descriptor, *msi_page; + dma_addr_t firmware_dma, descriptor_dma, msi_dma; size_t firmware_size; u32 image_response; + u16 msi_original; + int msi_cap; bool boot_attempted; bool link_ready; struct centauri_ipc *ipc; struct centauri_wifi *wifi; }; -int centauri_request_irqs(struct centauri_irqs *irqs, struct pci_dev *pdev, - unsigned int count, irq_handler_t handler, void *data); -void centauri_free_irqs(struct centauri_irqs *irqs); - +int centauri_j813_port_cycle(struct pci_dev *pdev); int centauri_start_ipc(struct centauri *c); void centauri_free_ipc(struct centauri *c); int centauri_alpha_resources(struct centauri *c, struct cen_alpha_resources *resources); -bool centauri_stop_dma(struct pci_dev *pdev); - -#if IS_ENABLED(CONFIG_APPLE_CENTAURI_WIFI) -irqreturn_t centauri_alpha_irq(int irq, void *data); +int centauri_alpha_log_buffer(struct centauri *c, void **cpu, dma_addr_t *dma, size_t *size); int centauri_wifi_register(struct centauri *c); void centauri_wifi_unregister(struct centauri *c); -#else -static inline irqreturn_t centauri_alpha_irq(int irq, void *data) -{ - return IRQ_HANDLED; -} -static inline int centauri_wifi_register(struct centauri *c) -{ - return 0; -} -static inline void centauri_wifi_unregister(struct centauri *c) -{ -} -#endif #endif diff --git a/drivers/net/wireless/apple/centauri/centauri_ipc.c b/drivers/net/wireless/apple/centauri/centauri_ipc.c new file mode 100644 index 0000000000000..6eec46d831888 --- /dev/null +++ b/drivers/net/wireless/apple/centauri/centauri_ipc.c @@ -0,0 +1,478 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Native DMA implementation of the measured J813 Control/Alpha bootstrap. + * Wire layout source: AppleCentauriCommon/scripts/alpha_wifi_scan.py v259. + * Polling and bounded bootstrap rings are deliberate bring-up restrictions. + */ +#include +#include +#include +#include +#include +#include +#include +#include "centauri.h" + +struct cen_buffer { + struct list_head list; + void *cpu; + dma_addr_t dma; + size_t size; +}; + +struct cen_context { + struct cen_buffer *buffer, *messages; + u32 index[4], mtr, mcr; + u16 tr_count, cr_count, message_head; +}; + +struct centauri_ipc { + struct list_head buffers; + struct pci_dev *function[3]; + void __iomem *bar[3]; + struct cen_context control, alpha; + struct cen_buffer *secondary, *window, *cchi_rx, *cchi_tx, *debug_rx; + struct cen_buffer *alpha_log; + u16 cchi_head, cchi_tail; +}; + +static struct cen_buffer *buffer_alloc(struct centauri *c, size_t size) +{ + struct cen_buffer *buffer = kzalloc(sizeof(*buffer), GFP_KERNEL); + + if (!buffer) + return NULL; + buffer->size = PAGE_ALIGN(size); + buffer->cpu = dma_alloc_coherent(&c->pdev->dev, buffer->size, &buffer->dma, GFP_KERNEL); + if (!buffer->cpu) { + kfree(buffer); + return NULL; + } + memset(buffer->cpu, 0, buffer->size); + list_add_tail(&buffer->list, &c->ipc->buffers); + return buffer; +} + +static int ftab_entry(const void *image, size_t size, const char tag[4], + u32 *offset, u32 *length, bool sparse) +{ + u32 count, i, off, len; + const u8 *table = image; + + if (size < 0x30 || memcmp(table + 0x24, "ftab", 4)) + return -EINVAL; + count = get_unaligned_le32(table + 0x28); + if (!count || count > 256 || 0x30 + count * 16 > size) + return -EINVAL; + for (i = 0; i < count; i++) { + const u8 *entry = table + 0x30 + 16 * i; + + if (memcmp(entry, tag, 4)) + continue; + off = get_unaligned_le32(entry + 4); + len = get_unaligned_le32(entry + 8); + if (off < 0x30 + count * 16 || !len || (u64)off + len > (sparse ? SZ_64M : size)) + return -EINVAL; + *offset = off; + *length = len; + return 0; + } + return -ENOENT; +} + +static int secondary_init(struct centauri *c) +{ + u32 offset, length, count, i, required; + const u8 *seed; + int ret; + + ret = ftab_entry(c->firmware, c->firmware_size, "2ftb", &offset, &length, false); + if (ret) + return ret; + seed = c->firmware + offset; + if (length < 0x30 || memcmp(seed + 0x24, "ftab", 4)) + return -EINVAL; + count = get_unaligned_le32(seed + 0x28); + if (!count || count > 256 || 0x30 + 16 * count != length) + return -EINVAL; + required = length; + for (i = 0; i < count; i++) { + u64 end = (u64)get_unaligned_le32(seed + 0x34 + i * 16) + + get_unaligned_le32(seed + 0x38 + i * 16); + + if (end > SZ_64M) + return -EINVAL; + required = max_t(u32, required, end); + } + c->ipc->secondary = buffer_alloc(c, required); + if (!c->ipc->secondary) + return -ENOMEM; + memcpy(c->ipc->secondary->cpu, seed, length); + dma_wmb(); + pci_write_config_dword(c->pdev, 0xf88, lower_32_bits(c->ipc->secondary->dma)); + pci_write_config_dword(c->pdev, 0xf8c, upper_32_bits(c->ipc->secondary->dma)); + pci_write_config_dword(c->pdev, 0xf90, required); + dev_info(&c->pdev->dev, "secondary memory: seed=%u required=%u\n", length, required); + return 0; +} + +static int context_init(struct centauri *c, struct cen_context *ctx, + u16 tr, u16 cr, dma_addr_t scratch, size_t scratch_size) +{ + u8 *header; + dma_addr_t dma; + int i; + + ctx->buffer = buffer_alloc(c, SZ_32K); + ctx->messages = buffer_alloc(c, SZ_16K); + if (!ctx->buffer || !ctx->messages) + return -ENOMEM; + ctx->tr_count = tr; + ctx->cr_count = cr; + ctx->index[0] = 0x78; + ctx->index[1] = ctx->index[0] + cr * 2; + ctx->index[2] = ctx->index[1] + tr * 2; + ctx->index[3] = ctx->index[2] + cr * 2; + ctx->mtr = ALIGN(ctx->index[3] + tr * 2, tr == 192 ? 16 : 2); + ctx->mcr = ALIGN(ctx->mtr + 256, 4); + header = ctx->buffer->cpu; + dma = ctx->buffer->dma; + put_unaligned_le16(768, header); + put_unaligned_le16(0x68, header + 2); + put_unaligned_le64(dma + 0x68, header + 8); + for (i = 0; i < 4; i++) + put_unaligned_le64(dma + ctx->index[i], header + 0x10 + 8 * i); + put_unaligned_le16(cr, header + 0x30); + put_unaligned_le16(tr, header + 0x32); + put_unaligned_le64(dma + ctx->mcr, header + 0x34); + put_unaligned_le64(dma + ctx->mtr, header + 0x3c); + put_unaligned_le16(16, header + 0x44); + put_unaligned_le16(16, header + 0x46); + put_unaligned_le64(scratch ?: dma + ALIGN(ctx->mcr + 256, 8), header + 0x58); + put_unaligned_le64(scratch_size, header + 0x60); + dma_wmb(); + return 0; +} + +static int handshake(void __iomem *bar, struct cen_context *ctx, + u32 status, u32 address, u32 control) +{ + u32 value; + int ret; + + writel(1, bar + control); + ret = readl_poll_timeout(bar + status, value, value == 1, 100, 2000000); + if (ret) + return ret; + writel(lower_32_bits(ctx->buffer->dma), bar + address); + writel(upper_32_bits(ctx->buffer->dma), bar + address + 4); + dma_wmb(); + writel(2, bar + control); + return readl_poll_timeout(bar + status, value, value == 2, 100, 2000000); +} + +static int message(struct cen_context *ctx, void __iomem *doorbell, + const void *payload, size_t length) +{ + u16 head = ctx->message_head; + u8 *descriptor; + u16 tail; + int ret; + + if (head >= 15 || !length || length > 512) + return -ENOSPC; + memcpy(ctx->messages->cpu + 512 * (head + 1), payload, length); + descriptor = ctx->buffer->cpu + ctx->mtr + head * 16; + put_unaligned_le32(1 | (length << 8), descriptor); + put_unaligned_le64(ctx->messages->dma + 512 * (head + 1), descriptor + 4); + put_unaligned_le32(head + 1, descriptor + 12); + dma_wmb(); + writel(head + 1, doorbell); + ret = read_poll_timeout(READ_ONCE, tail, le16_to_cpu(tail) == head + 1, + 100, 1200000, false, *(__le16 *)(ctx->buffer->cpu + ctx->index[1])); + dma_rmb(); + ctx->message_head++; + return ret; +} + +static int open_transfer(struct centauri *c, u16 id, struct cen_buffer *buffer, + u16 footer) +{ + u8 msg[0x34] = { 1 }; + u32 words = footer >> 2, shift = min_t(u32, __ffs(words), 4); + + msg[2] = words >> shift; + msg[3] = shift << 4; + put_unaligned_le16(id, msg + 4); + put_unaligned_le64(buffer->dma, msg + 8); + put_unaligned_le64(~0ULL, msg + 0x10); + put_unaligned_le16(16, msg + 0x18); + put_unaligned_le16(0xffff, msg + 0x1a); + put_unaligned_le16(id, msg + 0x1c); + put_unaligned_le16(0x40, msg + 0x1e); + return message(&c->ipc->control, c->bar + 0x9000, msg, sizeof(msg)); +} + +static int cchi_init(struct centauri *c) +{ + struct centauri_ipc *ipc = c->ipc; + u8 config[4] = {9, 0, 0xff, 0xff}, *entry; + u16 tail; + int i, ret; + + ipc->cchi_rx = buffer_alloc(c, SZ_32K); + ipc->cchi_tx = buffer_alloc(c, SZ_32K); + ipc->debug_rx = buffer_alloc(c, SZ_16K); + if (!ipc->cchi_rx || !ipc->cchi_tx || !ipc->debug_rx) + return -ENOMEM; + ret = message(&ipc->control, c->bar + 0x9000, config, sizeof(config)); + if (ret) + return ret; + ret = open_transfer(c, 4, ipc->cchi_rx, 1024); + if (!ret) + ret = open_transfer(c, 3, ipc->cchi_tx, 1024); + if (!ret) + ret = open_transfer(c, 5, ipc->debug_rx, 256); + if (ret) + return ret; + for (i = 0; i < 15; i++) { + entry = ipc->cchi_rx->cpu + i * 1040; + put_unaligned_le32(1 | (1024 << 8), entry); + put_unaligned_le32(i + 1, entry + 12); + entry = ipc->debug_rx->cpu + i * 272; + put_unaligned_le32(1 | (256 << 8), entry); + put_unaligned_le32(i + 1, entry + 12); + } + dma_wmb(); + writel(15, c->bar + 0x9010); + writel(15, c->bar + 0x9014); + entry = ipc->cchi_tx->cpu; + put_unaligned_le32(1 | (64 << 8), entry); + put_unaligned_le32(1, entry + 12); + put_unaligned_le16(64, entry + 16); /* CCHI gid 0, oid 0, seq 0 hello. */ + dma_wmb(); + writel(1, c->bar + 0x900c); + ret = read_poll_timeout(READ_ONCE, tail, le16_to_cpu(tail) != 0, + 100, 2000000, false, + *(__le16 *)(ipc->control.buffer->cpu + ipc->control.index[1] + 4 * 2)); + dma_rmb(); + if (ret) + return ret; + entry = ipc->cchi_rx->cpu; + if ((get_unaligned_le32(entry) >> 8) < 8 || entry[18] || entry[19]) + return -EPROTO; + ipc->cchi_head = ipc->cchi_tail = 1; + dev_info(&c->pdev->dev, "Control CCHI hello response received\n"); + return 0; +} + +static int memswap_handoff(struct centauri *c, unsigned int function, const char tag[4]) +{ + struct cen_buffer *secondary = c->ipc->secondary, *working; + u32 offset, size; + int ret; + + ret = ftab_entry(secondary->cpu, secondary->size, tag, &offset, &size, false); + if (ret) + return ret; + working = buffer_alloc(c, size); + if (!working) + return -ENOMEM; + dma_rmb(); + memcpy(working->cpu, secondary->cpu + offset, size); + dma_wmb(); + pci_write_config_dword(c->ipc->function[function], 0xf88, lower_32_bits(working->dma)); + pci_write_config_dword(c->ipc->function[function], 0xf8c, upper_32_bits(working->dma)); + pci_write_config_dword(c->ipc->function[function], 0xf90, size); + dev_info(&c->pdev->dev, "memswap function %u: %.4s %u bytes\n", function, tag, size); + return 0; +} + +static int enumerate_functions(struct centauri *c) +{ + struct centauri_ipc *ipc = c->ipc; + u16 control; + int i, cap; + + pci_lock_rescan_remove(); + /* The ROM personality was single-function; boot exposes Alpha and Beta. */ + c->pdev->multifunction = 1; + pci_rescan_bus(c->pdev->bus); + pci_unlock_rescan_remove(); + for (i = 0; i < 3; i++) { + ipc->function[i] = pci_get_domain_bus_and_slot(pci_domain_nr(c->pdev->bus), + c->pdev->bus->number, PCI_DEVFN(0, i)); + if (!ipc->function[i] || ipc->function[i]->vendor != PCI_VENDOR_ID_APPLE || + (i && ipc->function[i]->device != 0x1901 + i)) + return -ENODEV; + if (!(pci_resource_flags(ipc->function[i], 0) & IORESOURCE_MEM)) + return -ENODEV; + ipc->bar[i] = pci_iomap(ipc->function[i], 0, 0); + if (!ipc->bar[i]) + return -ENOMEM; + cap = pci_find_capability(ipc->function[i], PCI_CAP_ID_MSI); + if (!cap) + return -ENODEV; + pci_read_config_word(ipc->function[i], cap + PCI_MSI_FLAGS, &control); + if (!(control & PCI_MSI_FLAGS_64BIT)) + return -EOPNOTSUPP; + pci_write_config_dword(ipc->function[i], cap + PCI_MSI_ADDRESS_LO, + lower_32_bits(c->msi_dma + 0x400 * i)); + pci_write_config_dword(ipc->function[i], cap + PCI_MSI_ADDRESS_HI, + upper_32_bits(c->msi_dma + 0x400 * i)); + pci_write_config_word(ipc->function[i], cap + PCI_MSI_DATA_64, 0xa5a0 + 0x100 * i); + pci_write_config_word(ipc->function[i], cap + PCI_MSI_FLAGS, + (control & ~PCI_MSI_FLAGS_QSIZE) | (5 << 4) | PCI_MSI_FLAGS_ENABLE); + pci_set_master(ipc->function[i]); + } + return 0; +} + +int centauri_start_ipc(struct centauri *c) +{ + struct centauri_ipc *ipc; + u32 stage; + int ret; + + if (!c->firmware || !c->link_ready || c->ipc || readl(c->bar + 0x8000) != 1) + return -EBUSY; + ipc = kzalloc(sizeof(*ipc), GFP_KERNEL); + if (!ipc) + return -ENOMEM; + INIT_LIST_HEAD(&ipc->buffers); + c->ipc = ipc; + ret = enumerate_functions(c); + if (ret) + goto failed; + ret = secondary_init(c); + if (ret) + goto failed; + ret = context_init(c, &ipc->control, 13, 1, 0, 0); + if (ret) + goto failed; + writel(lower_32_bits(c->descriptor_dma), c->bar + 0x8044); + writel(upper_32_bits(c->descriptor_dma), c->bar + 0x8048); + writel(0x20, c->bar + 0x804c); + writel(0x62746632, c->bar + 0x8074); /* 2ftb */ + dma_wmb(); + writel(1, c->bar + 0x9070); + ret = readl_poll_timeout(c->bar + 0x8000, stage, stage == 2 || stage == 3, 100, 3000000); + if (ret || stage != 2) { + ret = ret ?: -EIO; + goto failed; + } + dev_info(&c->pdev->dev, "Control firmware reached OS stage\n"); + ret = handshake(c->bar, &ipc->control, 0x8050, 0x8054, 0x907c); + if (ret) + goto failed; + dev_info(&c->pdev->dev, "Control IPC handshake complete\n"); + ret = memswap_handoff(c, 0, "mswc"); + if (!ret) + ret = memswap_handoff(c, 2, "mswb"); + if (!ret) + ret = memswap_handoff(c, 1, "msww"); + if (ret) + goto failed; + ret = cchi_init(c); + if (ret) + goto failed; + ret = readl_poll_timeout(ipc->bar[1] + 8, stage, stage == 2 || stage == 3, 100, 2000000); + if (ret || stage != 2) { + ret = ret ?: -EIO; + goto failed; + } + ipc->window = buffer_alloc(c, SZ_16M); + if (!ipc->window) { + ret = -ENOMEM; + goto failed; + } + ret = context_init(c, &ipc->alpha, 192, 234, + ipc->window->dma + CEN_ALPHA_SCRATCH_OFFSET, CEN_ALPHA_SCRATCH_SIZE); + if (ret) + goto failed; + writel(lower_32_bits(ipc->window->dma), ipc->bar[1] + 0x18); + writel(upper_32_bits(ipc->window->dma), ipc->bar[1] + 0x1c); + writel(SZ_16M, ipc->bar[1] + 0x20); + ret = handshake(ipc->bar[1], &ipc->alpha, 0xc, 0x10, 0x1028); + if (ret) + goto failed; + dev_info(&c->pdev->dev, "Alpha Wi-Fi firmware running; IPC handshake complete\n"); + return centauri_wifi_register(c); +failed: + dev_err(&c->pdev->dev, "IPC bootstrap failed: %d (Control stage=%#x)\n", + ret, readl(c->bar + 0x8000)); + /* Published DMA stays pinned until every function is stopped at removal. */ + return ret; +} + +void centauri_free_ipc(struct centauri *c) +{ + struct centauri_ipc *ipc = c->ipc; + struct cen_buffer *buffer, *next; + int i, cap; + u16 control; + + if (!ipc) + return; + centauri_wifi_unregister(c); + for (i = 0; i < 3; i++) { + if (!ipc->function[i]) + continue; + cap = pci_find_capability(ipc->function[i], PCI_CAP_ID_MSI); + if (cap) { + pci_read_config_word(ipc->function[i], cap + PCI_MSI_FLAGS, &control); + pci_write_config_word(ipc->function[i], cap + PCI_MSI_FLAGS, + control & ~PCI_MSI_FLAGS_ENABLE); + } + pci_clear_master(ipc->function[i]); + } + list_for_each_entry_safe(buffer, next, &ipc->buffers, list) { + dma_free_coherent(&c->pdev->dev, buffer->size, buffer->cpu, buffer->dma); + list_del(&buffer->list); + kfree(buffer); + } + for (i = 0; i < 3; i++) { + if (ipc->bar[i]) + pci_iounmap(ipc->function[i], ipc->bar[i]); + pci_dev_put(ipc->function[i]); + } + kfree(ipc); + c->ipc = NULL; +} + +int centauri_alpha_resources(struct centauri *c, struct cen_alpha_resources *resources) +{ + struct centauri_ipc *ipc = c->ipc; + + if (!ipc || !ipc->window || !ipc->alpha.buffer || !ipc->alpha.messages) + return -ENODEV; + resources->context = ipc->alpha.buffer->cpu; + resources->context_dma = ipc->alpha.buffer->dma; + resources->messages = ipc->alpha.messages->cpu; + resources->messages_dma = ipc->alpha.messages->dma; + resources->window = ipc->window->cpu; + resources->window_dma = ipc->window->dma; + resources->bar = ipc->bar[1]; + memcpy(resources->index, ipc->alpha.index, sizeof(resources->index)); + resources->mtr = ipc->alpha.mtr; + resources->mcr = ipc->alpha.mcr; + return 0; +} + +/* Called once under the Wi-Fi lock. Published logging memory stays owned by + * IPC until every function has bus mastering disabled in centauri_free_ipc. + */ +int centauri_alpha_log_buffer(struct centauri *c, void **cpu, dma_addr_t *dma, size_t *size) +{ + struct centauri_ipc *ipc = c->ipc; + + if (!ipc || !ipc->window) + return -ENODEV; + if (!ipc->alpha_log) + ipc->alpha_log = buffer_alloc(c, SZ_4M); + if (!ipc->alpha_log) + return -ENOMEM; + *cpu = ipc->alpha_log->cpu; + *dma = ipc->alpha_log->dma; + *size = ipc->alpha_log->size; + return 0; +} diff --git a/drivers/net/wireless/apple/centauri/centauri_pci.c b/drivers/net/wireless/apple/centauri/centauri_pci.c new file mode 100644 index 0000000000000..f3f7c6413f0a3 --- /dev/null +++ b/drivers/net/wireless/apple/centauri/centauri_pci.c @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Initial real-hardware N1 PCI/ACIPC backend for J813. + * Register offsets are from AppleCentauriCommon/PROTOCOL.md and its measured + * J813 prototype. Firmware execution and radio operations are separate gates. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "centauri.h" + +static struct dentry *centauri_debug; + +static int centauri_status_show(struct seq_file *s, void *unused) +{ + struct centauri *c = s->private; + + seq_printf(s, "pci=%s vendor=%04x device=%04x\n", + pci_name(c->pdev), c->pdev->vendor, c->pdev->device); + if (c->link_ready) + seq_printf(s, "bar0_size=%pa exec_stage=%#x\n", &c->size, + readl(c->bar + c->stage_offset)); + else + seq_puts(s, "link unavailable; endpoint reads disabled\n"); + seq_printf(s, "transport=hardware-pci firmware_boot=%s image_response=%#x wifi=%s\n", + c->boot_attempted ? "attempted" : "not-started", c->image_response, + READ_ONCE(c->wifi) ? "registered" : "not-owned-by-this-function"); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(centauri_status); + +static void centauri_free_boot(struct centauri *c) +{ + struct device *dev = &c->pdev->dev; + + if (!c->link_ready && c->boot_attempted) { + dev_warn(dev, "retaining DMA buffers after failed link transition; reboot required\n"); + return; + } + /* Stop device writes before any DMA storage is released. */ + centauri_free_ipc(c); + if (c->msi_cap) + pci_write_config_word(c->pdev, c->msi_cap + PCI_MSI_FLAGS, + c->msi_original & ~PCI_MSI_FLAGS_ENABLE); + pci_clear_master(c->pdev); + if (c->firmware) + dma_free_coherent(dev, c->firmware_size, c->firmware, c->firmware_dma); + if (c->descriptor) + dma_free_coherent(dev, SZ_16K, c->descriptor, c->descriptor_dma); + if (c->msi_page) + dma_free_coherent(dev, SZ_16K, c->msi_page, c->msi_dma); + c->firmware = c->descriptor = c->msi_page = NULL; +} + +static int centauri_rom_boot(struct centauri *c) +{ + const struct firmware *fw; + struct device *dev = &c->pdev->dev; + u32 stage; + int ret; + + if ((c->pdev->device != 0x1900 && c->pdev->device != 0x1901) || + !of_machine_is_compatible("apple,j813")) + return -ENODEV; + if (!c->link_ready || c->boot_attempted) + return -EBUSY; + stage = readl(c->bar + 0x8000); + if (stage != 0 && stage != 4 && stage != 1) + return -EBUSY; + ret = request_firmware(&fw, "apple/centauri/j813-ftab.bin", dev); + if (ret) + return ret; + if (fw->size < 0x30 || fw->size > SZ_64M || + memcmp(fw->data + 0x20, "rkosftab", 8)) { + ret = -EINVAL; + goto release; + } + c->firmware_size = PAGE_ALIGN(fw->size); + c->firmware = dma_alloc_coherent(dev, c->firmware_size, &c->firmware_dma, GFP_KERNEL); + c->descriptor = dma_alloc_coherent(dev, SZ_16K, &c->descriptor_dma, GFP_KERNEL); + c->msi_page = dma_alloc_coherent(dev, SZ_16K, &c->msi_dma, GFP_KERNEL); + if (!c->firmware || !c->descriptor || !c->msi_page) { + ret = -ENOMEM; + goto free_buffers; + } + memset(c->firmware, 0, c->firmware_size); + memcpy(c->firmware, fw->data, fw->size); + memset(c->descriptor, 0, SZ_16K); + memset(c->msi_page, 0, SZ_16K); + /* Measured ROM scatter entry: u64 DMA, u32 length, u32 flags, u64 x2 zero. */ + put_unaligned_le64(c->firmware_dma, c->descriptor); + put_unaligned_le32(ALIGN(fw->size, SZ_4K), c->descriptor + 8); + c->msi_cap = pci_find_capability(c->pdev, PCI_CAP_ID_MSI); + if (!c->msi_cap) { + ret = -ENODEV; + goto free_buffers; + } + pci_read_config_word(c->pdev, c->msi_cap + PCI_MSI_FLAGS, &c->msi_original); + if (!(c->msi_original & PCI_MSI_FLAGS_64BIT)) { + ret = -EOPNOTSUPP; + goto free_buffers; + } + /* Poll the measured DMA-backed MSI mailbox until native IRQ support exists. */ + pci_write_config_dword(c->pdev, c->msi_cap + PCI_MSI_ADDRESS_LO, lower_32_bits(c->msi_dma)); + pci_write_config_dword(c->pdev, c->msi_cap + PCI_MSI_ADDRESS_HI, upper_32_bits(c->msi_dma)); + pci_write_config_word(c->pdev, c->msi_cap + PCI_MSI_DATA_64, 0xa5a0); + pci_write_config_word(c->pdev, c->msi_cap + PCI_MSI_FLAGS, + (c->msi_original & ~PCI_MSI_FLAGS_QSIZE) | PCI_MSI_FLAGS_ENABLE); + pci_set_master(c->pdev); + writel(0x00248142, c->bar + 0x807c); /* J813 board 0x24, T8142 chip. */ + writel(0, c->bar + 0x803c); + writel(lower_32_bits(c->descriptor_dma), c->bar + 0x8044); + writel(upper_32_bits(c->descriptor_dma), c->bar + 0x8048); + writel(0x20, c->bar + 0x804c); + dma_wmb(); + c->boot_attempted = true; + if (stage == 4 || stage == 1) { + c->image_response = readl(c->bar + 0x8040); + dev_info(dev, "adopted firmware stage %u; native DMA buffers ready\n", stage); + ret = 0; + goto release; + } + writel(1, c->bar + 0x9000); + ret = readl_poll_timeout(c->bar + 0x8040, c->image_response, + c->image_response != 0, 20000, 20000000); + if (!ret) + ret = readl_poll_timeout(c->bar + 0x8000, stage, stage != 0, 20000, 20000000); + else + stage = readl(c->bar + 0x8000); + dma_rmb(); + dev_info(dev, "ROM firmware result: response=%#x stage=%#x mailbox=%#x poll=%d\n", + c->image_response, stage, READ_ONCE(*(u32 *)c->msi_page), ret); + /* Keep DMA buffers owned until device removal, even on timeout. */ + if (!ret && stage != 4) + ret = -EIO; + goto release; +free_buffers: + centauri_free_boot(c); +release: + release_firmware(fw); + return ret; +} + +static ssize_t centauri_boot_write(struct file *file, const char __user *buffer, + size_t count, loff_t *pos) +{ + struct centauri *c = file->private_data; + u32 value; + int ret = kstrtou32_from_user(buffer, count, 0, &value); + + if (ret || value < 1 || value > 3) + return ret ?: -EINVAL; + mutex_lock(&c->boot_lock); + if (value == 1) { + ret = centauri_rom_boot(c); + } else if (value == 3) { + ret = centauri_start_ipc(c); + } else if (!c->firmware || !c->link_ready || readl(c->bar + 0x8000) != 4) { + ret = -EBUSY; + } else { + c->link_ready = false; + ret = centauri_j813_port_cycle(c->pdev); + if (!ret) { + c->link_ready = true; + dev_info(&c->pdev->dev, "post-reset execution stage=%#x\n", + readl(c->bar + 0x8000)); + } + } + mutex_unlock(&c->boot_lock); + return ret ?: count; +} + +static const struct file_operations centauri_boot_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = centauri_boot_write, +}; + +static int centauri_probe(struct pci_dev *pdev, const struct pci_device_id *id) +{ + struct centauri *c; + dma_addr_t dma; + void *page; + int ret; + + c = devm_kzalloc(&pdev->dev, sizeof(*c), GFP_KERNEL); + if (!c) + return -ENOMEM; + c->pdev = pdev; + c->link_ready = true; + mutex_init(&c->boot_lock); + c->stage_offset = id->driver_data; + ret = pcim_enable_device(pdev); + if (ret) + return ret; + c->size = pci_resource_len(pdev, 0); + if (!(pci_resource_flags(pdev, 0) & IORESOURCE_MEM) || + c->size < c->stage_offset + sizeof(u32)) + return dev_err_probe(&pdev->dev, -ENODEV, "invalid ACIPC BAR0\n"); + ret = pcim_iomap_regions(pdev, BIT(0), "apple_centauri"); + if (ret) + return ret; + c->bar = pcim_iomap_table(pdev)[0]; + ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(40)); + if (ret) + return dev_err_probe(&pdev->dev, ret, "40-bit DMA unavailable\n"); + /* Test Linux's DART allocation without publishing it to the device. */ + page = dma_alloc_coherent(&pdev->dev, SZ_16K, &dma, GFP_KERNEL); + if (!page) + return dev_err_probe(&pdev->dev, -ENOMEM, "DART DMA allocation failed\n"); + memset(page, 0, SZ_16K); + dev_info(&pdev->dev, "ACIPC endpoint reachable: stage=%#x, 40-bit DMA mapping=%pad\n", + readl(c->bar + c->stage_offset), &dma); + dma_free_coherent(&pdev->dev, SZ_16K, page, dma); + /* Bus mastering remains disabled until the firmware transport owns buffers. */ + pci_clear_master(pdev); + pci_set_drvdata(pdev, c); + c->debug = debugfs_create_dir(pci_name(pdev), centauri_debug); + debugfs_create_file("status", 0400, c->debug, c, ¢auri_status_fops); + debugfs_create_file("rom_boot", 0200, c->debug, c, ¢auri_boot_fops); + return 0; +} + +static void centauri_remove(struct pci_dev *pdev) +{ + struct centauri *c = pci_get_drvdata(pdev); + + centauri_wifi_unregister(c); + debugfs_remove_recursive(c->debug); + centauri_free_boot(c); +} + +static const struct pci_device_id centauri_ids[] = { + { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1900), .driver_data = 0x8000 }, + { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1901), .driver_data = 0x8000 }, + { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1902), .driver_data = 0x0008 }, + { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1903), .driver_data = 0x0018 }, + { } +}; +MODULE_DEVICE_TABLE(pci, centauri_ids); + +static struct pci_driver centauri_driver = { + .name = "apple_centauri", + .id_table = centauri_ids, + .probe = centauri_probe, + .remove = centauri_remove, +}; + +static int __init centauri_init(void) +{ + int ret; + + centauri_debug = debugfs_create_dir("apple_centauri", NULL); + ret = pci_register_driver(¢auri_driver); + if (ret) + debugfs_remove_recursive(centauri_debug); + return ret; +} + +static void __exit centauri_exit(void) +{ + pci_unregister_driver(¢auri_driver); + debugfs_remove_recursive(centauri_debug); +} +module_init(centauri_init); +module_exit(centauri_exit); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Apple N1 Centauri native PCI and ACIPC bring-up backend"); +MODULE_FIRMWARE("apple/centauri/j813-ftab.bin"); diff --git a/drivers/net/wireless/apple/centauri/centauri_port.c b/drivers/net/wireless/apple/centauri/centauri_port.c new file mode 100644 index 0000000000000..eea0a1ba11e0e --- /dev/null +++ b/drivers/net/wireless/apple/centauri/centauri_port.c @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* J813 preboot -> boot port cycle, transcribed from the measured proxy + * alpha_wifi_scan.py pcie_cycle/apple_teardown/apple_bringup sequence. + * These apertures were verified from the live ADT before this Linux boot. + * This temporary board helper belongs in the host-controller driver once + * T8142 platform/clock/GPIO bindings are available. + */ +#include +#include +#include +#include +#include +#include "centauri.h" + +static void update(void __iomem *base, u32 offset, u32 clear, u32 set) +{ + writel((readl(base + offset) & ~clear) | set, base + offset); +} + +static bool wait_bits(void __iomem *address, u32 mask, u32 wanted, u32 us) +{ + u32 value; + + return !readl_poll_timeout(address, value, (value & mask) == wanted, 100, us); +} + +int centauri_j813_port_cycle(struct pci_dev *pdev) +{ + static const u32 reset[][2] = { + {0x88, 0x110}, {0x100, ~0U}, {0x148, ~0U}, {0x210, ~0U}, + {0x80, 0}, {0x84, 0}, {0x104, 0xfffffff0}, {0x124, 0x100}, + {0x16c, 0}, {0x13c, 0}, {0x800, 0x00100100}, + {0x808, 0x001001ff}, {0x82c, 0x00010000}, + }; + static const u32 tune[][2] = { + {0x130, 0x03020000}, {0x140, 0x10}, {0x144, 0x00253770}, + {0x21c, 0}, {0x834, 0}, {0x83c, 0}, + }; + struct pci_dev *bridge = pdev->bus->self; + void __iomem *port, *phy, *pad; + u32 value, gpio, identity; + u16 link; + unsigned int lanes, i; + int ret = -ETIMEDOUT, pm; + bool ack, l2; + + if (!of_machine_is_compatible("apple,j813") || !bridge || + bridge->vendor != PCI_VENDOR_ID_APPLE || bridge->device != 0x100c) + return -ENODEV; + port = ioremap(0x488028000ULL, 0x4000); + phy = ioremap(0x48f010000ULL, 0x1000); + pad = ioremap(0x39a000300ULL, sizeof(u32)); + if (!port || !phy || !pad) { + ret = -ENOMEM; + goto out; + } + gpio = readl(pad); + if (gpio & BIT(21)) { + ret = -EPERM; + goto out; + } + ret = pci_save_state(bridge); + if (ret) + goto out; + ret = pci_save_state(pdev); + if (ret) + goto out; + pcie_capability_read_word(bridge, PCI_EXP_LNKSTA, &link); + lanes = (link & PCI_EXP_LNKSTA_NLW) >> PCI_EXP_LNKSTA_NLW_SHIFT; + lanes = lanes >= 1 && lanes <= 16 ? BIT(lanes) - 1 : 1; + pm = pci_find_capability(pdev, PCI_CAP_ID_PM); + if (pm) { + pci_write_config_word(pdev, pm + PCI_PM_CTRL, 0x103); + msleep(10); + } + update(port, 0x800, 0, 0x100); + writel(0x11, port + 0x88); + ack = wait_bits(port + 0x88, 1, 0, 500000); + l2 = wait_bits(port + 0x208, BIT(6), BIT(6), 500000); + dev_info(&pdev->dev, "port preboot transition: PME ack=%d L2=%d\n", ack, l2); + /* Endpoint reads cease here until the root LTSSM is back in L0. */ + pci_write_config_word(bridge, PCI_COMMAND, 0); + pci_read_config_dword(bridge, PCI_PRIMARY_BUS, &value); + pci_write_config_dword(bridge, PCI_PRIMARY_BUS, value & 0xff0000ff); + update(phy, 0, BIT(31), 0); + writel((gpio & ~0xfU) | 2, pad); + if (readl(pad) != ((gpio & ~0xfU) | 2)) { + ret = -EIO; + goto out; + } + wait_bits(port + 0x804, BIT(2), 0, 50000); + wait_bits(port + 0x820, 0x1ff, 0, 100000); + update(port, 0x13c, 0, 0x100); + for (i = 0; i < 200; i++) { + pci_read_config_dword(bridge, 0x728, &value); + if ((value & 0x3f) == 0x14) + break; + usleep_range(1000, 1200); + } + wait_bits(port + 0x208, BIT(12), 0, 200000); + readl(phy); + wait_bits(port + 0xa8, 3, 0, 10000); + update(port, 0x82c, 0, BIT(24)); + wait_bits(port + 0xa8, lanes << 16, lanes << 16, 20000); + update(port, 0x82c, 0, BIT(16)); + udelay(10); + update(port, 0x82c, 1, 0); + udelay(10); + update(port, 0x82c, BIT(24), 0); + update(port, 0x13c, 0x100, 0); + update(phy, 0, BIT(31), 0); + update(phy, 0, 0x400, 0); + update(phy, 0, 0x200, 0); + update(phy, 0, 0, 0x10); + update(phy, 0, 1, 0); + update(phy, 0, 2, 0); + writel(~0U, port + 0x100); + update(port, 0x800, 1, 0); + wait_bits(port + 0x804, 1, 0, 200000); + update(phy, 0, BIT(31), 0); + for (i = 0; i < ARRAY_SIZE(reset); i++) + writel(reset[i][1], port + reset[i][0]); + for (i = 0; i < 16; i++) + writel(0, port + 0x3000 + 4 * i); + for (i = 0; i < 256; i++) + writel(0, port + 0x3800 + 4 * i); + for (i = 0; i < ARRAY_SIZE(tune); i++) + writel(tune[i][1], port + tune[i][0]); + update(port, 0x800, 0, 1); + udelay(10); + update(phy, 0, 0, 1); + if (!wait_bits(phy, 4, 4, 50000)) + goto timed_out; + update(phy, 0, 0, 2); + if (!wait_bits(phy, 8, 8, 50000)) + goto timed_out; + update(phy, 0, 0x10, 0); + udelay(100); + update(phy, 0, 0, 0x200); + update(phy, 0, 0, 0x400); + udelay(100); + update(port, 0x82c, 0, 1); + writel((gpio & ~0xfU) | 3, pad); + udelay(100); + readl(phy); + wait_bits(port + 0xa8, 3, 0, 20000); + update(port, 0x82c, BIT(16), 0); + if (!wait_bits(port + 0x804, 1, 1, 200000)) + goto timed_out; + update(port, 0x800, 0x100, 0); + pci_read_config_dword(bridge, 0x8bc, &value); + pci_write_config_dword(bridge, 0x8bc, value ^ 1); + pci_read_config_dword(bridge, 0x80, &value); + pci_write_config_dword(bridge, 0x80, value | 0x400); + update(phy, 0, 0, BIT(31)); + update(port, 0x80, 0, 1); + for (i = 0; i < 2000; i++) { + pci_read_config_dword(bridge, 0x728, &value); + if ((value & 0x3f) == 0x11) + break; + usleep_range(1000, 1200); + } + if (i == 2000) + goto timed_out; + msleep(100); + pci_restore_state(bridge); + /* Bus numbers/windows now restored; endpoint config may be read again. */ + pci_read_config_dword(pdev, PCI_VENDOR_ID, &identity); + if (identity != 0x1901106b && identity != 0x1900106b) { + ret = -ENODEV; + goto out; + } + pci_restore_state(pdev); + if (pm) { + pci_write_config_word(pdev, pm + PCI_PM_CTRL, 0); + msleep(10); + } + dev_info(&pdev->dev, "port transition complete: endpoint=%#x LTSSM=%#x\n", + identity, value & 0x3f); + ret = 0; + goto out; +timed_out: + ret = -ETIMEDOUT; +out: + if (pad) + iounmap(pad); + if (phy) + iounmap(phy); + if (port) + iounmap(port); + return ret; +} diff --git a/drivers/net/wireless/apple/centauri/centauri_wifi.c b/drivers/net/wireless/apple/centauri/centauri_wifi.c new file mode 100644 index 0000000000000..21201241f49ec --- /dev/null +++ b/drivers/net/wireless/apple/centauri/centauri_wifi.c @@ -0,0 +1,2312 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* cfg80211 connection to the measured Alpha command/management rings. + * Control queues recycle across hardware-validated wraps. Packet data uses + * a separate protocol; never report transmit success before implementing it. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "centauri.h" + +#define CR_BASE 0x100000 +#define TR_BASE 0x400000 +#define RING_SPAN 0x20000 +#define TX_BUFFER 0x600000 +#define BUFFER_SIZE 2048 +#define DATA_TX_BUFFER 0xd00000 +#define DATA_RX_BUFFER 0xe00000 +#define DATA_TX_SLOTS 128 +#define DATA_TX_QUEUE_LIMIT 64 + +/* The firmware owns window +8 MiB through +13 MiB as scratch memory. */ +static_assert(DATA_TX_BUFFER >= CEN_ALPHA_SCRATCH_OFFSET + CEN_ALPHA_SCRATCH_SIZE); +static_assert(DATA_TX_BUFFER + DATA_TX_SLOTS * BUFFER_SIZE <= DATA_RX_BUFFER); + +static bool verify_pmk; +module_param(verify_pmk, bool, 0400); +MODULE_PARM_DESC(verify_pmk, "Compare firmware PMK with the requested key without exposing either"); + +static bool eapol_after_assoc; +module_param(eapol_after_assoc, bool, 0644); +MODULE_PARM_DESC(eapol_after_assoc, "One EAPOL-Start probe at the next successful association event"); + +static bool link_diagnostics; +module_param(link_diagnostics, bool, 0644); +MODULE_PARM_DESC(link_diagnostics, "Query non-secret firmware link status during association"); + +static bool eapol_control_profile; +module_param(eapol_control_profile, bool, 0644); +MODULE_PARM_DESC(eapol_control_profile, "Use the measured control TX profile for the EAPOL-Start probe"); + +static bool eapol_unicast; +module_param(eapol_unicast, bool, 0644); +MODULE_PARM_DESC(eapol_unicast, "Address the bounded EAPOL-Start probe to the associated AP"); + +static bool eapol_relative_lifetime; +module_param(eapol_relative_lifetime, bool, 0644); +MODULE_PARM_DESC(eapol_relative_lifetime, "Use a measured two-second relative expiration for the probe"); + +static bool data_tx_broadcast; +module_param(data_tx_broadcast, bool, 0400); +MODULE_PARM_DESC(data_tx_broadcast, "Diagnostic: open TR 25 with measured broadcast metadata; transmit only broadcast Ethernet frames"); + +static bool data_tx_control_profile; +module_param(data_tx_control_profile, bool, 0600); +MODULE_PARM_DESC(data_tx_control_profile, "Diagnostic: select configured TX policy 1 for normal data; apply tx_config action 2 first"); + +static bool data_tx_native_prepare = true; +module_param(data_tx_native_prepare, bool, 0600); +MODULE_PARM_DESC(data_tx_native_prepare, "Use native packet preparation value 4; disable only to reproduce the legacy data TX failure"); + +static bool station_link_addresses; +module_param(station_link_addresses, bool, 0400); +MODULE_PARM_DESC(station_link_addresses, "Diagnostic: use the netdev address for each firmware link in this single-link WPA2 client"); + +struct centauri_rx_snapshot { + __le32 version, count; + struct { + u8 completion[32]; + u8 packet[4096]; + } sample[8]; +}; + +struct centauri_wifi { + struct centauri *control; + struct cen_alpha_resources a; + struct wiphy *wiphy; + struct wireless_dev wdev; + struct net_device *netdev; + struct work_struct scan_work; + struct work_struct connect_work; + struct delayed_work link_work; + struct cfg80211_scan_request *scan_request; + struct mutex lock; + /* The logger has its own coherent allocation. Reads/acknowledgements + * must remain possible while scan/connect hold the command mutex. + * Initialization takes lock then fwlog_lock; readers take only the latter. + */ + struct mutex fwlog_lock; + u16 main_head, command_head, sequence; + u32 rx_seen[5]; + u16 rx_next[5], rx_head[5]; + u16 reply_sequence, reply_status; + u32 reply_command, channels_seen, scans_done, beacons_seen; + bool reply_valid, registered, configured; + bool stopping; + bool connecting, connected, connect_event, disconnect_event; + bool associated, pmk_seen, pmk_matches; + u8 expected_pmk[32]; + u16 connect_status; + u8 bssid[ETH_ALEN], connect_payload[1024], request_ie[256]; + size_t connect_size, request_ie_size; + u8 passphrase[64]; + size_t passphrase_size; + struct dentry *passphrase_file; + struct dentry *data_probe_file; + struct dentry *data_status_file; + struct dentry *data_rx_snapshot_file; + struct centauri_rx_snapshot rx_snapshot; + u32 data_rx_received, data_rx_reposted, data_rx_bad; + struct sk_buff_head data_tx_queue; + u16 data_tx_length[DATA_TX_SLOTS]; + bool data_tx_busy[DATA_TX_SLOTS]; + u32 data_tx_submitted, data_tx_completed, data_tx_failed; + u32 data_tx_coalesced, data_tx_bad; + struct dentry *station_stats_file; + struct dentry *eapol_start_file; + struct dentry *country_file; + struct dentry *link_status_file; + struct dentry *tx_config_file; + struct dentry *host_wakeup_file; + struct dentry *fwlog_start_file, *fwlog_status_file, *fwlog_buffer_file, *fwlog_ack_file; + void *fwlog_buffer; + dma_addr_t fwlog_dma; + size_t fwlog_size; + bool fwlog_attempted, fwlog_ready; + u8 tx_profile[10], tx_defaults_applied; + bool tx_profile_valid; + u8 link_count, link_prime[3], link_channel[3], link_band[3], emlsr_mode; + bool link_status_valid, emlsr_valid; + u16 link_status_size; + u16 link_tags[12], link_lengths[12]; + u8 link_tlv_count; + u8 country[3]; + bool country_valid; + u8 station_stats[0x378]; + bool station_stats_valid; + u8 rx_crypto_stats[64]; + bool rx_crypto_stats_valid; + struct dentry *rx_crypto_stats_file; + u8 dot11_mib_stats[192]; + bool dot11_mib_stats_valid; + struct dentry *dot11_mib_stats_file; + u8 tx_power_report[280]; + bool tx_power_report_valid; + struct dentry *tx_power_report_file; + u16 stats_reply_size, stats_tag, stats_length; + u32 data_probe_done; + struct ieee80211_supported_band bands[2]; + struct ieee80211_channel channels[39]; +}; + +/* These helpers require wifi->lock, including from the connect worker. */ +static int transmit_eapol_start(struct centauri_wifi *wifi); +static int query_station_stats(struct centauri_wifi *wifi); +static int query_link_status(struct centauri_wifi *wifi); + +static const u32 rx_buffer[5] = { 0, 0, 0x620000, 0x680000, 0x700000 }; +static const u16 ring_size[5] = { 0, 64, 192, 96, 192 }; +static const u16 five_ghz[] = { + 36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, + 116, 120, 124, 128, 132, 136, 140, 144, 149, 153, 157, 161, 165, +}; +static struct ieee80211_rate rates[] = { + { .bitrate = 10 }, { .bitrate = 20 }, { .bitrate = 55 }, { .bitrate = 110 }, + { .bitrate = 60 }, { .bitrate = 90 }, { .bitrate = 120 }, { .bitrate = 180 }, + { .bitrate = 240 }, { .bitrate = 360 }, { .bitrate = 480 }, { .bitrate = 540 }, +}; +static const u32 cipher_suites[] = { + WLAN_CIPHER_SUITE_CCMP, WLAN_CIPHER_SUITE_AES_CMAC, +}; + +static u16 index_read(struct centauri_wifi *wifi, unsigned int array, unsigned int index) +{ + return le16_to_cpu(READ_ONCE(*(__le16 *)(wifi->a.context + wifi->a.index[array] + 2 * index))); +} + +static void index_write(struct centauri_wifi *wifi, unsigned int array, + unsigned int index, u16 value) +{ + WRITE_ONCE(*(__le16 *)(wifi->a.context + wifi->a.index[array] + 2 * index), cpu_to_le16(value)); +} + +static void completions(struct centauri_wifi *wifi) +{ + unsigned int vector; + + for (vector = 0; vector <= 4; vector++) + index_write(wifi, 2, vector, index_read(wifi, 0, vector)); + dma_wmb(); +} + +static int alpha_message(struct centauri_wifi *wifi, const void *message, size_t size) +{ + u16 head = wifi->main_head; + u8 *descriptor; + unsigned long deadline = jiffies + msecs_to_jiffies(2000); + + if (head >= 15 || size > 512) + return -ENOSPC; + memcpy(wifi->a.messages + (head + 1) * 512, message, size); + descriptor = wifi->a.context + wifi->a.mtr + 16 * head; + put_unaligned_le32(1 | (size << 8), descriptor); + put_unaligned_le64(wifi->a.messages_dma + (head + 1) * 512, descriptor + 4); + put_unaligned_le32(head + 1, descriptor + 12); + index_write(wifi, 3, 0, head + 1); + if (head) + index_write(wifi, 3, 8, head + 1); + dma_wmb(); + wifi->main_head++; + writel(1, wifi->a.bar + 0x1004); + do { + if (readl(wifi->a.bar + 8) != 2) + return -EIO; + if (index_read(wifi, 1, 0) >= head + 1) { + dma_rmb(); + completions(wifi); + return 0; + } + usleep_range(100, 200); + } while (time_before(jiffies, deadline)); + return -ETIMEDOUT; +} + +static int open_pair(struct centauri_wifi *wifi, u16 id) +{ + u8 cr[0x2c] = { 2 }, tr[0x34] = { 1 }; + int ret; + + put_unaligned_le32(id | (id << 16), cr + 4); + put_unaligned_le64(wifi->a.window_dma + CR_BASE + (id - 1) * RING_SPAN, cr + 8); + put_unaligned_le16(1024, cr + 0x10); + put_unaligned_le16(0xffff, cr + 0x12); + ret = alpha_message(wifi, cr, sizeof(cr)); + if (ret) + return ret; + put_unaligned_le32(id | (id << 16), tr + 4); + put_unaligned_le64(wifi->a.window_dma + TR_BASE + (id - 1) * RING_SPAN, tr + 8); + put_unaligned_le64(~0ULL, tr + 0x10); + put_unaligned_le16(ring_size[id], tr + 0x18); + put_unaligned_le16(id, tr + 0x1a); + put_unaligned_le16(id, tr + 0x1c); + put_unaligned_le16(0x40, tr + 0x1e); + put_unaligned_le16(2, tr + 0x24); + return alpha_message(wifi, tr, sizeof(tr)); +} + +static void report_beacon(struct centauri_wifi *wifi, const u8 *payload, size_t size) +{ + const u8 *frame, *ie, *end; + struct ieee80211_channel *channel; + struct cfg80211_bss *bss; + u16 frame_size; + int number = 0, frequency, signal; + enum nl80211_band band; + + if (!wifi->registered || size < 0x60 + 36) + return; + /* The ACIPC packet length is authoritative; the vendor descriptor's + * separate 802.11 length can include a stripped FCS. The measured parser + * consumes the frame bytes that were actually returned. + */ + frame_size = size - 0x60; + frame = payload + 0x60; + if (!ieee80211_is_beacon(*(__le16 *)frame) && !ieee80211_is_probe_resp(*(__le16 *)frame)) + return; + end = frame + frame_size; + for (ie = frame + 36; ie + 2 <= end && ie + 2 + ie[1] <= end; ie += 2 + ie[1]) { + if ((ie[0] == WLAN_EID_DS_PARAMS || ie[0] == WLAN_EID_HT_OPERATION) && ie[1]) + number = ie[2]; + } + if (!number) + return; + band = number <= 14 ? NL80211_BAND_2GHZ : NL80211_BAND_5GHZ; + frequency = ieee80211_channel_to_frequency(number, band); + channel = ieee80211_get_channel(wifi->wiphy, frequency); + if (!channel) + return; + signal = max((s8)payload[0x16], (s8)payload[0x17]); + if (signal >= 0 || signal < -127) + signal = -95; + bss = cfg80211_inform_bss_frame(wifi->wiphy, channel, + (struct ieee80211_mgmt *)frame, frame_size, signal * 100, GFP_KERNEL); + if (bss) { + wifi->beacons_seen++; + cfg80211_put_bss(wifi->wiphy, bss); + } +} + +static void post_receive(struct centauri_wifi *wifi, unsigned int ring, u16 slot) +{ + u8 *descriptor = wifi->a.window + TR_BASE + (ring - 1) * RING_SPAN + slot * 16; + u32 offset = rx_buffer[ring] + slot * BUFFER_SIZE; + + memset(wifi->a.window + offset, 0, BUFFER_SIZE); + put_unaligned_le32(1 | (BUFFER_SIZE << 8), descriptor); + put_unaligned_le64(wifi->a.window_dma + offset, descriptor + 4); + put_unaligned_le32(slot + 1, descriptor + 12); +} + +/* Host-owned RX buffer cookies and completion length are the ACIPC fields + * validated in probe 28. Probe 29 validates the footer's prefix length and + * Ethernet framing with IPv4 header checksums and IPv6 payload lengths. + * Called with wifi->lock, including during connect/command polling. + */ +static void drain_data_rx(struct centauri_wifi *wifi) +{ + u16 head, tail, post; + unsigned int processed = 0; + + if (!(wifi->data_probe_done & BIT(4)) || readl(wifi->a.bar + 8) != 2) + return; + head = index_read(wifi, 0, 207); + tail = index_read(wifi, 2, 207); + post = index_read(wifi, 3, 12); + if (head >= 128 || tail >= 128 || post >= 128) { + wifi->data_rx_bad++; + return; + } + dma_rmb(); + while (tail != head && processed < 127) { + u8 *completion = wifi->a.window + 0x240000 + tail * 32; + u32 header = get_unaligned_le32(completion); + u32 info = get_unaligned_le32(completion + 4); + u16 cookie = info & 0xffff, length = info >> 16; + u16 next = (post + 1) % 128; + u32 count = le32_to_cpu(wifi->rx_snapshot.count); + u8 *packet, *descriptor; + u32 buffer; + + if ((header >> 16) != 12 || !cookie || cookie > 127 || + length > 4096 || get_unaligned_le32(completion + 8)) { + wifi->data_rx_bad++; + break; + } + /* Do not overwrite a descriptor the receive engine still owns. */ + if (next == index_read(wifi, 1, 12)) + break; + buffer = DATA_RX_BUFFER + (cookie - 1) * 4096; + packet = wifi->a.window + buffer; + if (count < ARRAY_SIZE(wifi->rx_snapshot.sample)) { + memcpy(wifi->rx_snapshot.sample[count].completion, completion, 32); + memcpy(wifi->rx_snapshot.sample[count].packet, packet, length); + wifi->rx_snapshot.version = cpu_to_le32(1); + wifi->rx_snapshot.count = cpu_to_le32(count + 1); + } + wifi->data_rx_received++; + if (wifi->connected && netif_running(wifi->netdev)) { + u32 footer = get_unaligned_le32(completion + 12); + unsigned int padding = (footer >> 12) & 15; + struct sk_buff *skb = NULL; + + /* Extended prefix headers are not decoded yet. */ + if (!(footer & BIT(24)) && length >= padding + ETH_HLEN) + skb = netdev_alloc_skb_ip_align(wifi->netdev, length - padding); + if (skb) { + skb_put_data(skb, packet + padding, length - padding); + skb->protocol = eth_type_trans(skb, wifi->netdev); + skb->ip_summed = CHECKSUM_NONE; + wifi->netdev->stats.rx_packets++; + wifi->netdev->stats.rx_bytes += length - padding; + netif_rx(skb); + } else { + wifi->netdev->stats.rx_dropped++; + } + } else { + wifi->netdev->stats.rx_dropped++; + } + descriptor = wifi->a.window + 0x520000 + post * 16; + put_unaligned_le32(1 | (4096 << 8), descriptor); + put_unaligned_le64(wifi->a.window_dma + buffer, descriptor + 4); + put_unaligned_le32(cookie, descriptor + 12); + wifi->data_rx_reposted++; + tail = (tail + 1) % 128; + post = next; + processed++; + } + if (processed) { + dma_wmb(); + index_write(wifi, 2, 207, tail); + index_write(wifi, 3, 12, post); + dma_wmb(); + writel(1, wifi->a.bar + 0x1050); + } +} + +/* Serialized with the firmware poller. Payload slots remain owned until a + * CR completion, even if the device has already consumed the TR descriptor. + */ +static void drain_data_tx(struct centauri_wifi *wifi) +{ + u16 head, tail, post; + unsigned int budget = 256; + bool posted = false, completed = false; + + if (!(wifi->data_probe_done & BIT(5)) || readl(wifi->a.bar + 8) != 2) + return; + head = index_read(wifi, 0, 192); + tail = index_read(wifi, 2, 192); + post = index_read(wifi, 3, 25); + if (head >= 2048 || tail >= 2048 || post >= DATA_TX_SLOTS) + return; + dma_rmb(); + while (tail != head && budget--) { + u8 *entry = wifi->a.window + 0x200000 + tail * 32; + u16 ring = get_unaligned_le32(entry) >> 16; + u16 cookie = get_unaligned_le16(entry + 4); + u16 count = get_unaligned_le16(entry + 9); + u8 status = entry[12]; + + if (ring == 25) { + unsigned int first, i; + + /* Airship cr_controller::sync maps CD bytes 9:10 to the + * transfer count. With the multicomp flag (CD byte 1 bit 0), + * the cookie names the LAST transfer; prior transfers share + * its completion status. We submit one contiguous DMA buffer + * per slot, so those preceding cookies must all still be busy. + */ + if (!cookie || cookie > DATA_TX_SLOTS || !count || + count >= DATA_TX_SLOTS || !(entry[1] & 1)) { + wifi->data_tx_bad++; + break; + } + first = (cookie + DATA_TX_SLOTS - count) % DATA_TX_SLOTS; + for (i = 0; i < count; i++) + if (!wifi->data_tx_busy[(first + i) % DATA_TX_SLOTS]) + break; + if (i != count) { + wifi->data_tx_bad++; + break; + } + if (count > 1) + wifi->data_tx_coalesced++; + for (i = 0; i < count; i++) { + unsigned int slot = (first + i) % DATA_TX_SLOTS; + + wifi->data_tx_completed++; + if (status == 0 || status == 0x83) { + wifi->netdev->stats.tx_packets++; + wifi->netdev->stats.tx_bytes += wifi->data_tx_length[slot]; + } else { + wifi->data_tx_failed++; + wifi->netdev->stats.tx_errors++; + } + wifi->data_tx_busy[slot] = false; + } + } + tail = (tail + 1) % 2048; + completed = true; + } + if (completed) + index_write(wifi, 2, 192, tail); + budget = DATA_TX_SLOTS - 1; + while (wifi->connected && !READ_ONCE(wifi->stopping) && + netif_running(wifi->netdev) && budget--) { + u16 next = (post + 1) % DATA_TX_SLOTS; + u32 buffer = DATA_TX_BUFFER + post * BUFFER_SIZE; + u8 *descriptor = wifi->a.window + 0x540000 + post * 32; + struct sk_buff *skb; + u8 destination[ETH_ALEN]; + u64 metadata = 6ULL << 27; + + if (wifi->data_tx_busy[post] || next == index_read(wifi, 1, 25)) + break; + skb = skb_dequeue(&wifi->data_tx_queue); + if (!skb) + break; + /* This diagnostic ring is only valid for its broadcast destination. + * Do not silently send unicast or IPv6 multicast through that flow. + */ + if (data_tx_broadcast && + (skb_copy_bits(skb, 0, destination, ETH_ALEN) || + !is_broadcast_ether_addr(destination))) { + wifi->netdev->stats.tx_dropped++; + dev_kfree_skb_any(skb); + continue; + } + if (skb_copy_bits(skb, 0, wifi->a.window + buffer, skb->len)) { + wifi->netdev->stats.tx_dropped++; + dev_kfree_skb_any(skb); + continue; + } + memset(descriptor, 0, 32); + put_unaligned_le32(3 | (skb->len << 8), descriptor); + put_unaligned_le64(wifi->a.window_dma + buffer, descriptor + 4); + put_unaligned_le16(post + 1, descriptor + 12); + /* AirshipDataPacket type 1 supplies this 16-byte TX header inline. + * setTxPolicyId(1), 0x1809ed27c, sets validity bit 60 and ID bit 48. + * Keep normal policy zero by default; the switch permits a bounded + * comparison with the existing, read-back-verified control profile. + */ + if (READ_ONCE(data_tx_control_profile) && + (wifi->tx_defaults_applied & BIT(1))) + metadata |= BIT_ULL(60) | BIT_ULL(48); + /* Native 25G76 AirshipDataPacket::prepare (0x18090faf8) writes + * (argument & 7) to bits 17:15. Guest37's captured DHCP Discover + * has value 4 (0x20000). The same-connection Linux A/B changes + * zero DHCP offers into three offers by setting only this field. + * The opt-out retains the old value for diagnostic reproduction. + */ + if (READ_ONCE(data_tx_native_prepare)) + metadata |= 4ULL << 15; + put_unaligned_le64(metadata, descriptor + 16); + put_unaligned_le64(2000000, descriptor + 24); + wifi->data_tx_length[post] = skb->len; + wifi->data_tx_busy[post] = true; + wifi->data_tx_submitted++; + dev_kfree_skb_any(skb); + post = next; + posted = true; + } + if (posted || completed) { + dma_wmb(); + if (posted) + index_write(wifi, 3, 25, post); + dma_wmb(); + writel(1, wifi->a.bar + 0x1030); + } + if (wifi->connected && !READ_ONCE(wifi->stopping) && netif_running(wifi->netdev) && + skb_queue_len(&wifi->data_tx_queue) < DATA_TX_QUEUE_LIMIT) + netif_wake_queue(wifi->netdev); +} + +static void drain(struct centauri_wifi *wifi) +{ + unsigned int ring; + bool posted = false; + + for (ring = 2; ring <= 4; ring++) { + unsigned int budget = ring_size[ring]; + + while (budget--) { + u16 slot = wifi->rx_next[ring], size, status, sequence; + u32 command; + u8 *descriptor = wifi->a.window + TR_BASE + (ring - 1) * RING_SPAN + slot * 16; + u8 *packet = wifi->a.window + rx_buffer[ring] + slot * BUFFER_SIZE; + + if (!(READ_ONCE(descriptor[15]) & 4)) + break; + dma_rmb(); + size = get_unaligned_le16(packet + 0x10); + status = get_unaligned_le16(packet + 0xe); + sequence = get_unaligned_le16(packet + 0x12); + command = get_unaligned_le32(packet + 0x14) & ~BIT(30); + wifi->rx_seen[ring]++; + wifi->rx_next[ring] = (slot + 1) % ring_size[ring]; + if (size < 24 || size > BUFFER_SIZE) + goto refill; + if (ring == 4) { + report_beacon(wifi, packet + 24, size - 24); + } else if (command & BIT(31)) { + if (command == 0x80000030) + wifi->channels_seen++; + if (command == 0x80000005) + wifi->scans_done++; + if (command == 0x80000000 && wifi->connecting) { + wifi->connect_event = true; + wifi->connect_status = status; + } + if (command == 0x80000001) { + wifi->associated = status == 0; + dev_info(&wifi->control->pdev->dev, "Wi-Fi association event: status=%u\n", status); + if (eapol_after_assoc && wifi->associated && wifi->connecting && + !wifi->connect_event) { + int probe_ret = transmit_eapol_start(wifi); + + eapol_after_assoc = false; + dev_info(&wifi->control->pdev->dev, + "association-event EAPOL-Start: rc=%d\n", probe_ret); + } + } + /* ACI event 0x50 carries fixed, non-secret supplicant + * diagnostics: result, phase and handshake timestamps. + * Do not dump the event or any EAPOL/key material. + */ + if (command == 0x80000050 && size >= 64) + dev_info(&wifi->control->pdev->dev, + "Wi-Fi supplicant: result=%u phase=%u times=%u/%u/%u/%u/%u\n", + get_unaligned_le32(packet + 0x18), packet[0x1c], + get_unaligned_le32(packet + 0x24), + get_unaligned_le32(packet + 0x28), + get_unaligned_le32(packet + 0x2c), + get_unaligned_le32(packet + 0x30), + get_unaligned_le32(packet + 0x34)); + if (command == 0x80000003 || command == 0x80000004) + wifi->disconnect_event = true; + if (wifi->connecting && command != 0x80000000 && + command != 0x80000001 && command != 0x80000030 && + command != 0x80000005 && command != 0x80000050) + dev_info_ratelimited(&wifi->control->pdev->dev, + "Wi-Fi event: id=%#x status=%u size=%u\n", + command, status, size); + } else { + wifi->reply_valid = true; + wifi->reply_sequence = sequence; + wifi->reply_status = status; + wifi->reply_command = command; + if (command == 10) { + wifi->country_valid = !status && size >= 27; + if (wifi->country_valid) + memcpy(wifi->country, packet + 24, 3); + } + if (command == 56) { + wifi->tx_profile_valid = !status && size >= 34; + if (wifi->tx_profile_valid) + memcpy(wifi->tx_profile, packet + 24, sizeof(wifi->tx_profile)); + } + if (command == 20 && !status) { + /* getInterfaceStatus skips its 32-byte fixed response. */ + unsigned int pos = 32; + + wifi->link_status_size = size; + while (pos + 4 <= size) { + u16 tag = get_unaligned_le16(packet + pos); + u16 length = get_unaligned_le16(packet + pos + 2); + u8 *value = packet + pos + 4; + unsigned int i; + + if (length > size - pos - 4) + break; + if (wifi->link_tlv_count < ARRAY_SIZE(wifi->link_tags)) { + wifi->link_tags[wifi->link_tlv_count] = tag; + wifi->link_lengths[wifi->link_tlv_count++] = length; + } + /* ACIDebugAdapter::dumpMloStatus, TLVs 377/378. + * Keep only link count/channel/role. Do not retain MACs. + */ + if (tag == 377 && length >= 64 && value[0] <= 3) { + wifi->link_count = value[0]; + wifi->link_status_valid = true; + for (i = 0; i < value[0]; i++) { + u16 chanspec = get_unaligned_le16(value + 26 + 17 * i); + + wifi->link_prime[i] = value[25 + 17 * i]; + wifi->link_channel[i] = chanspec & 0xff; + wifi->link_band[i] = (chanspec >> 14) & 3; + } + } + if (tag == 378 && length >= 9) { + wifi->emlsr_valid = true; + wifi->emlsr_mode = value[0]; + } + pos += 4 + length; + } + } + if (command == 0xb0001) { + wifi->stats_reply_size = size; + wifi->stats_tag = size >= 28 ? get_unaligned_le16(packet + 24) : 0; + wifi->stats_length = size >= 28 ? get_unaligned_le16(packet + 26) : 0; + } + if (command == 0xb0001 && !status && wifi->stats_tag == 0xf928 && + (wifi->stats_length == 752 || wifi->stats_length == 888) && + size >= 28 + wifi->stats_length) { + /* Native firmware returns the older 752-byte counter record. */ + memset(wifi->station_stats, 0, sizeof(wifi->station_stats)); + memcpy(wifi->station_stats, packet + 28, wifi->stats_length); + wifi->station_stats_valid = true; + } + if (command == 0xb0001 && !status && wifi->stats_tag == 0xf921 && + wifi->stats_length == sizeof(wifi->rx_crypto_stats) && + size >= 28 + sizeof(wifi->rx_crypto_stats)) { + memcpy(wifi->rx_crypto_stats, packet + 28, sizeof(wifi->rx_crypto_stats)); + wifi->rx_crypto_stats_valid = true; + } + if (command == 0xb0001 && !status && wifi->stats_tag == 0xf91b && + wifi->stats_length == sizeof(wifi->dot11_mib_stats) && + size >= 28 + sizeof(wifi->dot11_mib_stats)) { + memcpy(wifi->dot11_mib_stats, packet + 28, sizeof(wifi->dot11_mib_stats)); + wifi->dot11_mib_stats_valid = true; + } + if (command == 0xb0001 && !status && wifi->stats_tag == 0xf920 && + wifi->stats_length == sizeof(wifi->tx_power_report) && + size >= 28 + sizeof(wifi->tx_power_report)) { + memcpy(wifi->tx_power_report, packet + 28, sizeof(wifi->tx_power_report)); + wifi->tx_power_report_valid = true; + } + if (command == 0xf0025) { + wifi->pmk_seen = !status && size >= 60; + wifi->pmk_matches = wifi->pmk_seen && + get_unaligned_le16(packet + 26) == 32 && + !memcmp(packet + 28, wifi->expected_pmk, 32); + memzero_explicit(packet + 24, size - 24); + } + } + refill: + post_receive(wifi, ring, wifi->rx_head[ring]); + wifi->rx_head[ring] = (wifi->rx_head[ring] + 1) % ring_size[ring]; + dma_wmb(); + index_write(wifi, 3, ring, wifi->rx_head[ring]); + posted = true; + } + } + completions(wifi); + if (posted) + writel(1, wifi->a.bar + 0x1004); + drain_data_rx(wifi); + drain_data_tx(wifi); +} + +static int command(struct centauri_wifi *wifi, u32 id, const void *payload, size_t size) +{ + u16 slot = wifi->command_head, sequence = ++wifi->sequence; + u8 *packet, *descriptor; + unsigned long deadline = jiffies + msecs_to_jiffies(1500); + + if (size + 24 > BUFFER_SIZE) + return -EMSGSIZE; + if ((slot + 1) % ring_size[1] == index_read(wifi, 1, 1)) + return -ENOSPC; + packet = wifi->a.window + TX_BUFFER + slot * BUFFER_SIZE; + memset(packet, 0, 24 + size); + put_unaligned_le16(24 + size, packet + 0x10); + put_unaligned_le16(sequence, packet + 0x12); + put_unaligned_le32(id, packet + 0x14); + if (size) + memcpy(packet + 24, payload, size); + descriptor = wifi->a.window + TR_BASE + slot * 16; + put_unaligned_le32(1 | ((24 + size) << 8), descriptor); + put_unaligned_le64(wifi->a.window_dma + TX_BUFFER + slot * BUFFER_SIZE, descriptor + 4); + put_unaligned_le32(slot + 1, descriptor + 12); + dma_wmb(); + wifi->command_head = (slot + 1) % ring_size[1]; + index_write(wifi, 3, 1, wifi->command_head); + wifi->reply_valid = false; + dma_wmb(); + writel(1, wifi->a.bar + 0x1004); + do { + drain(wifi); + if (readl(wifi->a.bar + 8) != 2) + return -EIO; + /* Bit 30 is more-data-pending, not part of the command identity. + * Scan/connect replies set it before their later completion event. + */ + if (wifi->reply_valid && wifi->reply_sequence == sequence && + (wifi->reply_command & ~BIT(30)) == id) { + if (wifi->reply_status) + dev_info(&wifi->control->pdev->dev, + "Wi-Fi command %#x rejected: firmware_status=%u\n", + id, wifi->reply_status); + return wifi->reply_status ? -EREMOTEIO : 0; + } + usleep_range(500, 1000); + } while (time_before(jiffies, deadline)); + dev_err(&wifi->control->pdev->dev, "Wi-Fi command %#x timed out (rx=%u)\n", id, wifi->rx_seen[2]); + return -ETIMEDOUT; +} + +static int radio_start(struct centauri_wifi *wifi) +{ + u8 start[5 + 4 + 3 * ETH_ALEN] = { 0, 0x0b, 0, 0, 0 }; + size_t start_size = 5; + unsigned int ring, slot; + int ret; + + /* Stock 25G76 startInterface (0x1808e26ec) supplies TLV 385, + * length 18. Firmware 0x5736c0 otherwise generates three unrelated + * random addresses. This diagnostic keeps the sole active link equal + * to the netdev identity; it does not enable multi-link association. + */ + if (station_link_addresses) { + put_unaligned_le16(385, start + 5); + put_unaligned_le16(3 * ETH_ALEN, start + 7); + for (slot = 0; slot < 3; slot++) + ether_addr_copy(start + 9 + slot * ETH_ALEN, wifi->netdev->dev_addr); + start_size = sizeof(start); + } + for (ring = 1; ring <= 4; ring++) { + ret = open_pair(wifi, ring); + if (ret) + return ret; + } + for (ring = 2; ring <= 4; ring++) { + for (slot = 0; slot < ring_size[ring] - 1; slot++) + post_receive(wifi, ring, slot); + wifi->rx_head[ring] = ring_size[ring] - 1; + dma_wmb(); + index_write(wifi, 3, ring, ring_size[ring] - 1); + } + dma_wmb(); + writel(1, wifi->a.bar + 0x1004); + msleep(400); + ret = command(wifi, 0, NULL, 0); + if (!ret) + ret = command(wifi, 1, wifi->netdev->dev_addr, ETH_ALEN); + if (!ret) + ret = command(wifi, 2, start, start_size); + if (ret) + return ret; + msleep(1000); + wifi->configured = true; + dev_info(&wifi->control->pdev->dev, "Wi-Fi command rings ready; interface started\n"); + return 0; +} + +static void scan_worker(struct work_struct *work) +{ + struct centauri_wifi *wifi = container_of(work, struct centauri_wifi, scan_work); + struct cfg80211_scan_info info = {}; + struct cfg80211_scan_request *request; + u8 scan[14] = { 1, 0, 0, 0x80, 0, 2, 0, 0x10, 110, 0, 110, 0, 0, 0 }; + unsigned int chunk; + int ret = 0; + + mutex_lock(&wifi->lock); + for (chunk = 0; chunk < 3 && !ret; chunk++) { + u32 channels = wifi->channels_seen, done = wifi->scans_done; + bool stopped = false; + unsigned long deadline; + + scan[0] = chunk + 1; + ret = command(wifi, 65551, scan, sizeof(scan)); + if (ret) + break; + deadline = jiffies + msecs_to_jiffies(8000); + do { + drain(wifi); + if (readl(wifi->a.bar + 8) != 2) { + ret = -EIO; + break; + } + if (!stopped && wifi->channels_seen - channels >= 8) { + stopped = true; + ret = command(wifi, 65537, NULL, 0); + if (ret) + break; + } + if (stopped && wifi->scans_done > done) + break; + usleep_range(1000, 2000); + } while (time_before(jiffies, deadline)); + if (!ret && wifi->scans_done == done) + ret = -ETIMEDOUT; + } + info.aborted = !!ret; + request = wifi->scan_request; + wifi->scan_request = NULL; + mutex_unlock(&wifi->lock); + dev_info(&wifi->control->pdev->dev, "Wi-Fi scan finished: rc=%d channels=%u beacons=%u\n", + ret, wifi->channels_seen, wifi->beacons_seen); + if (request) + cfg80211_scan_done(request, &info); +} + +static int wifi_scan(struct wiphy *wiphy, struct cfg80211_scan_request *request) +{ + struct centauri_wifi *wifi = *(struct centauri_wifi **)wiphy_priv(wiphy); + int ret = 0; + + mutex_lock(&wifi->lock); + if (!wifi->configured) + ret = -ENETDOWN; + else if (wifi->scan_request || wifi->connecting) + ret = -EBUSY; + else { + wifi->scan_request = request; + schedule_work(&wifi->scan_work); + } + mutex_unlock(&wifi->lock); + return ret; +} + +static int rsn_caps_offset(const u8 *body, size_t size) +{ + size_t offset = 6; + u16 count; + unsigned int i; + + for (i = 0; i < 2; i++) { + if (offset + 2 > size) + return -EINVAL; + count = get_unaligned_le16(body + offset); + offset += 2 + 4 * count; + if (offset > size) + return -EINVAL; + } + return offset + 2 <= size ? offset : -EINVAL; +} + +static void append_tlv(u8 *payload, size_t *size, u16 tag, const void *data, u16 length) +{ + put_unaligned_le16(tag, payload + *size); + put_unaligned_le16(length, payload + *size + 2); + memcpy(payload + *size + 4, data, length); + *size += 4 + length; +} + +static int wifi_connect(struct wiphy *wiphy, struct net_device *netdev, + struct cfg80211_connect_params *params) +{ + /* Stock 25G76 ACIConnectionAdapter::connectAp (0x1808e94cc) emits a + * 12-byte TLV 376. The newer SDK 27 adapter uses 14 bytes. Preserve the + * unspecified-link masks; all-zero is not the non-MLO default. + */ + static const u8 mld_defaults[12] = { + 1, 0, 1, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 3, + }; + /* Firmware connection-context defaults (aciw 0x511ee8). TLV 0xf913 + * replaces all five values. Sending zeros arms Supp_AddTimer with a + * zero duration and aborts WPA before message 2 can be transmitted. + */ + static const __le32 connection_timeouts[] = { + cpu_to_le32(260), cpu_to_le32(6000), cpu_to_le32(2000), + cpu_to_le32(15000), cpu_to_le32(300), + }; + struct centauri_wifi *wifi = *(struct centauri_wifi **)wiphy_priv(wiphy); + struct cfg80211_bss *bss; + const struct cfg80211_bss_ies *ies; + const u8 *rsn, *ap_rsn; + u8 rsn_body[255], ssid[35], key[66], zero[20] = {}, pmf; + u8 *payload = wifi->connect_payload; + size_t size = 130, rsn_size; + u16 caps = 0; + int ret = 0, offset; + + if (!params->ssid_len || params->ssid_len > 32 || !params->crypto.psk || + params->crypto.n_akm_suites != 1 || params->crypto.akm_suites[0] != WLAN_AKM_SUITE_PSK || + params->crypto.n_ciphers_pairwise != 1 || params->crypto.ciphers_pairwise[0] != WLAN_CIPHER_SUITE_CCMP || + params->crypto.cipher_group != WLAN_CIPHER_SUITE_CCMP || params->ie_len > sizeof(wifi->request_ie)) + return -EOPNOTSUPP; + rsn = cfg80211_find_ie(WLAN_EID_RSN, params->ie, params->ie_len); + if (!rsn || rsn_caps_offset(rsn + 2, rsn[1]) < 0) + return -EINVAL; + mutex_lock(&wifi->lock); + if (!wifi->configured || wifi->connecting || wifi->connected || wifi->scan_request) { + ret = -EBUSY; + goto out; + } + if (!wifi->passphrase_size) { + ret = -ENOKEY; + goto out; + } + bss = cfg80211_get_bss(wiphy, params->channel, params->bssid, + params->ssid, params->ssid_len, IEEE80211_BSS_TYPE_ESS, IEEE80211_PRIVACY_ON); + if (!bss) { + ret = -ENOENT; + goto out; + } + /* Match the measured successful J813 association shape. The firmware + * expects the AP RSN capability bits even with the legal single-AKM STA IE. + */ + rcu_read_lock(); + ies = rcu_dereference(bss->ies); + ap_rsn = ies ? cfg80211_find_ie(WLAN_EID_RSN, ies->data, ies->len) : NULL; + if (ap_rsn) { + offset = rsn_caps_offset(ap_rsn + 2, ap_rsn[1]); + if (offset >= 0) + caps = get_unaligned_le16(ap_rsn + 2 + offset); + } + rcu_read_unlock(); + rsn_size = rsn[1]; + memcpy(rsn_body, rsn + 2, rsn_size); + offset = rsn_caps_offset(rsn_body, rsn_size); + put_unaligned_le16(caps, rsn_body + offset); + pmf = !!(caps & BIT(7)); + memset(payload, 0, sizeof(wifi->connect_payload)); + payload[0] = 1; + memcpy(payload + 1, bss->bssid, ETH_ALEN); + memcpy(wifi->bssid, bss->bssid, ETH_ALEN); + payload[7] = ieee80211_frequency_to_channel(bss->channel->center_freq); + /* AppleChanSpecToACI maps chanspec 0xc000 (5 GHz) to ACI 1; + * 0x4000 (6 GHz) maps to ACI 2. Earlier RE notes swapped these. + */ + payload[8] = bss->channel->band == NL80211_BAND_2GHZ ? 0 : 1; + payload[0x47] = 4; + /* ACIConnectionAdapter::mapRSNCipherSuiteToACIWsec maps CCMP to 4. + * The RSN IE does not replace this fixed cipher field. + */ + put_unaligned_le16(4, payload + 0x48); + put_unaligned_le16(32, payload + 0x4a); + memcpy(payload + 0x4c, params->crypto.psk, 32); + if (verify_pmk) + memcpy(wifi->expected_pmk, params->crypto.psk, 32); + append_tlv(payload, &size, 304, zero, 1); + append_tlv(payload, &size, 63763, connection_timeouts, sizeof(connection_timeouts)); + ssid[0] = 1; + ssid[1] = 4; + ssid[2] = params->ssid_len; + memcpy(ssid + 3, params->ssid, params->ssid_len); + append_tlv(payload, &size, 342, ssid, 3 + params->ssid_len); + append_tlv(payload, &size, WLAN_EID_RSN, rsn_body, rsn_size); + /* The fixed field carries the PMK; TLV 313 carries the passphrase. */ + put_unaligned_le16(wifi->passphrase_size, key); + memcpy(key + 2, wifi->passphrase, wifi->passphrase_size); + append_tlv(payload, &size, 313, key, 2 + wifi->passphrase_size); + memzero_explicit(key, sizeof(key)); + append_tlv(payload, &size, 316, &pmf, 1); + append_tlv(payload, &size, 376, mld_defaults, sizeof(mld_defaults)); + wifi->connect_size = size; + memcpy(wifi->request_ie, params->ie, params->ie_len); + wifi->request_ie_size = params->ie_len; + wifi->connecting = true; + wifi->connect_status = 0; + wifi->associated = wifi->pmk_seen = wifi->pmk_matches = false; + wifi->connect_event = wifi->disconnect_event = false; + cfg80211_put_bss(wiphy, bss); + schedule_work(&wifi->connect_work); +out: + mutex_unlock(&wifi->lock); + return ret; +} + +static void connect_worker(struct work_struct *work) +{ + struct centauri_wifi *wifi = container_of(work, struct centauri_wifi, connect_work); + unsigned long deadline; + u16 slot, status, command_status; + bool checked_pmk = false, probed_eapol = false, queried_link = false; + int ret; + + mutex_lock(&wifi->lock); + slot = wifi->command_head; + ret = command(wifi, 131072, wifi->connect_payload, wifi->connect_size); + command_status = wifi->reply_status; + memzero_explicit(wifi->connect_payload, sizeof(wifi->connect_payload)); + deadline = jiffies + msecs_to_jiffies(15000); + while (!ret && !wifi->connect_event && time_before(jiffies, deadline)) { + drain(wifi); + if (link_diagnostics && wifi->associated && !queried_link) { + unsigned int i; + int link_ret; + + queried_link = true; + link_ret = query_link_status(wifi); + dev_info(&wifi->control->pdev->dev, + "connection link status: rc=%d complete=%d valid=%d count=%u emlsr=%d/%u\n", + link_ret, wifi->connect_event, wifi->link_status_valid, wifi->link_count, + wifi->emlsr_valid, wifi->emlsr_mode); + for (i = 0; i < wifi->link_tlv_count; i++) + dev_info(&wifi->control->pdev->dev, "connection link TLV: tag=%u length=%u\n", + wifi->link_tags[i], wifi->link_lengths[i]); + if (wifi->link_status_valid) + for (i = 0; i < wifi->link_count; i++) + dev_info(&wifi->control->pdev->dev, + "connection link %u: prime=%u channel=%u band=%u\n", + i, wifi->link_prime[i], wifi->link_channel[i], wifi->link_band[i]); + } + if (eapol_after_assoc && wifi->associated && !probed_eapol) { + unsigned int i; + int probe_ret; + + probed_eapol = true; + probe_ret = query_station_stats(wifi); + dev_info(&wifi->control->pdev->dev, + "connection counters: complete=%d rc=%d valid=%d length=%u\n", + wifi->connect_event, probe_ret, wifi->station_stats_valid, wifi->stats_length); + if (!probe_ret && wifi->station_stats_valid) + for (i = 0; i < wifi->stats_length; i += 4) { + u32 value = get_unaligned_le32(wifi->station_stats + i); + + if (value) + dev_info(&wifi->control->pdev->dev, + "station counter %03x=%u\n", i, value); + } + } + if (verify_pmk && wifi->associated && !checked_pmk) { + int key_ret = command(wifi, 0xf0025, wifi->bssid, ETH_ALEN); + + checked_pmk = true; + dev_info(&wifi->control->pdev->dev, + "Wi-Fi firmware key check: rc=%d available=%d matches=%d\n", + key_ret, wifi->pmk_seen, wifi->pmk_matches); + } + if (readl(wifi->a.bar + 8) != 2) + ret = -EIO; + usleep_range(1000, 2000); + } + if (!ret && !wifi->connect_event) + ret = -ETIMEDOUT; + status = !ret && wifi->connect_status == 0 ? WLAN_STATUS_SUCCESS : WLAN_STATUS_UNSPECIFIED_FAILURE; + wifi->connecting = false; + wifi->connected = status == WLAN_STATUS_SUCCESS; + memzero_explicit(wifi->expected_pmk, sizeof(wifi->expected_pmk)); + /* The command and connection completion have consumed the PMK buffer. */ + if (slot < ring_size[1]) + memzero_explicit(wifi->a.window + TX_BUFFER + slot * BUFFER_SIZE, BUFFER_SIZE); + mutex_unlock(&wifi->lock); + dev_info(&wifi->control->pdev->dev, + "Wi-Fi authentication result: rc=%d command_status=%u event_status=%u complete_event=%d\n", + ret, command_status, wifi->connect_status, wifi->connect_event); + cfg80211_connect_result(wifi->netdev, wifi->bssid, + wifi->request_ie, wifi->request_ie_size, NULL, 0, status, GFP_KERNEL); + if (status == WLAN_STATUS_SUCCESS && !READ_ONCE(wifi->stopping)) { + netif_carrier_on(wifi->netdev); + if (wifi->data_probe_done & BIT(5)) + netif_wake_queue(wifi->netdev); + schedule_delayed_work(&wifi->link_work, msecs_to_jiffies(20)); + } +} + +static void link_worker(struct work_struct *work) +{ + struct centauri_wifi *wifi = container_of(to_delayed_work(work), struct centauri_wifi, link_work); + bool disconnected; + + mutex_lock(&wifi->lock); + drain(wifi); + disconnected = wifi->disconnect_event || READ_ONCE(wifi->stopping) || + readl(wifi->a.bar + 8) != 2; + if (disconnected) + wifi->connected = false; + mutex_unlock(&wifi->lock); + if (disconnected) { + netif_stop_queue(wifi->netdev); + skb_queue_purge(&wifi->data_tx_queue); + netif_carrier_off(wifi->netdev); + cfg80211_disconnected(wifi->netdev, WLAN_REASON_UNSPECIFIED, NULL, 0, false, GFP_KERNEL); + } else { + schedule_delayed_work(&wifi->link_work, msecs_to_jiffies(20)); + } +} + +static int wifi_disconnect(struct wiphy *wiphy, struct net_device *netdev, u16 reason) +{ + struct centauri_wifi *wifi = *(struct centauri_wifi **)wiphy_priv(wiphy); + u8 payload[16] = {}; + bool connected; + int ret; + + mutex_lock(&wifi->lock); + if (wifi->connecting) { + mutex_unlock(&wifi->lock); + return -EBUSY; + } + put_unaligned_le16(reason, payload); + put_unaligned_le16(328, payload + 8); + put_unaligned_le16(4, payload + 10); + ret = command(wifi, 131073, payload, sizeof(payload)); + connected = wifi->connected; + wifi->connected = false; + mutex_unlock(&wifi->lock); + cancel_delayed_work_sync(&wifi->link_work); + netif_stop_queue(netdev); + skb_queue_purge(&wifi->data_tx_queue); + netif_carrier_off(netdev); + if (connected) + cfg80211_disconnected(netdev, reason, NULL, 0, true, GFP_KERNEL); + return ret == -EREMOTEIO && wifi->reply_status == 326 ? 0 : ret; +} + +static int wifi_power_mgmt(struct wiphy *wiphy, struct net_device *netdev, + bool enabled, int timeout) +{ + struct centauri_wifi *wifi = *(struct centauri_wifi **)wiphy_priv(wiphy); + u8 mode = enabled ? 0 : 3; + int ret; + + /* ACIIO80211PowerAdapter::setPowerSaveMode, command 7. */ + mutex_lock(&wifi->lock); + ret = wifi->configured ? command(wifi, 7, &mode, sizeof(mode)) : -ENETDOWN; + mutex_unlock(&wifi->lock); + return ret; +} + +static void wifi_regulatory(struct wiphy *wiphy, struct regulatory_request *request) +{ + struct centauri_wifi *wifi = *(struct centauri_wifi **)wiphy_priv(wiphy); + u8 country[3] = { request->alpha2[0], request->alpha2[1], 1 }; + int ret; + + /* ACIRegulatoryAdapter::setCountryCode, command 9. Leave world-domain + * handling to cfg80211; do not invent a device country or bypass SAR. + */ + if (country[0] < 'A' || country[0] > 'Z' || country[1] < 'A' || country[1] > 'Z') + return; + mutex_lock(&wifi->lock); + if (wifi->configured) { + ret = command(wifi, 9, country, sizeof(country)); + dev_info(&wifi->control->pdev->dev, + "Wi-Fi country %c%c: rc=%d\n", country[0], country[1], ret); + } + mutex_unlock(&wifi->lock); +} + +static const struct cfg80211_ops wifi_ops = { + .scan = wifi_scan, + .connect = wifi_connect, + .disconnect = wifi_disconnect, + .set_power_mgmt = wifi_power_mgmt, +}; + +static int net_open(struct net_device *netdev) +{ + netif_carrier_off(netdev); + netif_stop_queue(netdev); + return 0; +} + +static int net_stop(struct net_device *netdev) +{ + struct centauri_wifi *wifi = *(struct centauri_wifi **)netdev_priv(netdev); + + netif_stop_queue(netdev); + skb_queue_purge(&wifi->data_tx_queue); + return 0; +} + +static netdev_tx_t net_transmit(struct sk_buff *skb, struct net_device *netdev) +{ + struct centauri_wifi *wifi = *(struct centauri_wifi **)netdev_priv(netdev); + + if (READ_ONCE(wifi->stopping) || !READ_ONCE(wifi->connected) || + skb->len < ETH_HLEN || skb->len > BUFFER_SIZE) { + netdev->stats.tx_dropped++; + dev_kfree_skb_any(skb); + return NETDEV_TX_OK; + } + if (skb_queue_len(&wifi->data_tx_queue) >= DATA_TX_QUEUE_LIMIT) { + netif_stop_queue(netdev); + return NETDEV_TX_BUSY; + } + skb_queue_tail(&wifi->data_tx_queue, skb); + if (skb_queue_len(&wifi->data_tx_queue) >= DATA_TX_QUEUE_LIMIT) + netif_stop_queue(netdev); + mod_delayed_work(system_wq, &wifi->link_work, 0); + return NETDEV_TX_OK; +} + +static const struct net_device_ops net_ops = { + .ndo_open = net_open, + .ndo_stop = net_stop, + .ndo_start_xmit = net_transmit, + .ndo_validate_addr = eth_validate_addr, +}; + +static ssize_t passphrase_write(struct file *file, const char __user *input, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + u8 value[64]; + size_t length = count; + int ret = 0; + + if (count < 8 || count > sizeof(value)) + return -EINVAL; + if (copy_from_user(value, input, count)) { + ret = -EFAULT; + goto out; + } + if (value[length - 1] == '\n') + length--; + if (length < 8 || length > 63) { + ret = -EINVAL; + goto out; + } + mutex_lock(&wifi->lock); + if (wifi->connecting) { + ret = -EBUSY; + } else { + memzero_explicit(wifi->passphrase, sizeof(wifi->passphrase)); + memcpy(wifi->passphrase, value, length); + wifi->passphrase_size = length; + } + mutex_unlock(&wifi->lock); +out: + memzero_explicit(value, sizeof(value)); + return ret ?: count; +} + +static const struct file_operations passphrase_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = passphrase_write, +}; + +/* Airship direct_memregion::map (0x1802864e0) sends opcode 8, a region ID, + * byte size and a 64-bit DVA in a 24-byte header. Alpha's region-debug is ID 1. + * Firmware 0x175340 passes those fields to DramPhyService_handle_buffer; + * InitDramLoggerMgr writes a 0x158-byte control header to the supplied buffer. + * This is distinct from the fw_debug transfer ring. Never print log payloads: + * firmware diagnostics may contain private connection material. + */ +static ssize_t fwlog_start_write(struct file *file, const char __user *input, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + u8 message[24] = { 8, 0, 1, 0 }; + unsigned int action; + unsigned long deadline; + int ret; + + ret = kstrtouint_from_user(input, count, 0, &action); + if (ret || action != 1) + return ret ?: -EINVAL; + mutex_lock(&wifi->lock); + mutex_lock(&wifi->fwlog_lock); + if (!wifi->configured || wifi->connecting || wifi->connected || wifi->scan_request) { + ret = -EBUSY; + goto out; + } + if (wifi->fwlog_attempted) { + ret = -EALREADY; + goto out; + } + ret = centauri_alpha_log_buffer(wifi->control, &wifi->fwlog_buffer, + &wifi->fwlog_dma, &wifi->fwlog_size); + if (ret) + goto out; + put_unaligned_le32(wifi->fwlog_size, message + 4); + put_unaligned_le64(wifi->fwlog_dma, message + 8); + wifi->fwlog_attempted = true; + ret = alpha_message(wifi, message, sizeof(message)); + if (ret) + goto report; + deadline = jiffies + msecs_to_jiffies(2000); + do { + if (readl(wifi->a.bar + 8) != 2) { + ret = -EIO; + goto report; + } + dma_rmb(); + if (get_unaligned_le16(wifi->fwlog_buffer) == 0x201) { + wifi->fwlog_ready = true; + goto report; + } + usleep_range(1000, 2000); + } while (time_before(jiffies, deadline)); + ret = -ETIMEDOUT; +report: + dev_info(&wifi->control->pdev->dev, + "Wi-Fi DRAM logger: rc=%d initialized=%u stage=%u\n", + ret, wifi->fwlog_ready, readl(wifi->a.bar + 8)); +out: + mutex_unlock(&wifi->fwlog_lock); + mutex_unlock(&wifi->lock); + return ret ?: count; +} + +static const struct file_operations fwlog_start_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = fwlog_start_write, +}; + +static ssize_t fwlog_status_read(struct file *file, char __user *output, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + char text[320]; + u8 header[0xb4] = { 0 }; + int length; + + mutex_lock(&wifi->fwlog_lock); + if (wifi->fwlog_buffer) { + dma_rmb(); + memcpy(header, wifi->fwlog_buffer, sizeof(header)); + } + length = scnprintf(text, sizeof(text), + "attempted=%u initialized=%u bytes=%zu version=%#x blocks=%u block_size=%u mask=%#x data_offset=%u consumer=%u producer=%u header14=%u header18=%u header1c=%u connecting=%u scanning=%u\n", + wifi->fwlog_attempted, wifi->fwlog_ready, wifi->fwlog_size, + get_unaligned_le16(header), get_unaligned_le16(header + 2), + get_unaligned_le32(header + 4), get_unaligned_le32(header + 8), + get_unaligned_le32(header + 12), get_unaligned_le32(header + 16), + get_unaligned_le32(header + 0xb0), + get_unaligned_le32(header + 20), get_unaligned_le32(header + 24), + get_unaligned_le32(header + 28), + !!READ_ONCE(wifi->connecting), !!READ_ONCE(wifi->scan_request)); + mutex_unlock(&wifi->fwlog_lock); + return simple_read_from_buffer(output, count, position, text, length); +} + +static const struct file_operations fwlog_status_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = fwlog_status_read, + .llseek = default_llseek, +}; + +/* The exact firmware's 0x144cfc compares header +0xb0 (producer) against + * +0x10 (host consumer). InitDramLoggerMgr registers a GDMA read of +0x10. + * Only acknowledge records after a private snapshot has been verified. + * Leave three entries reserved for the firmware's three DMA descriptors + * until completion/record sequencing is independently decoded. + */ +static ssize_t fwlog_ack_write(struct file *file, const char __user *input, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + u8 *header; + u32 requested, consumer, producer, pending; + u16 blocks; + int ret; + + ret = kstrtou32_from_user(input, count, 0, &requested); + if (ret) + return ret; + mutex_lock(&wifi->fwlog_lock); + if (!wifi->fwlog_ready || readl(wifi->a.bar + 8) != 2) { + ret = -ENETDOWN; + goto out; + } + header = wifi->fwlog_buffer; + dma_rmb(); + blocks = get_unaligned_le16(header + 2); + consumer = get_unaligned_le32(header + 0x10); + producer = get_unaligned_le32(header + 0xb0); + pending = producer - consumer; + if (get_unaligned_le16(header) != 0x201 || blocks < 8 || + pending > blocks || pending < 3 || requested - consumer > pending - 3) { + ret = -EINVAL; + goto out; + } + WRITE_ONCE(*(__le32 *)(header + 0x10), cpu_to_le32(requested)); + dma_wmb(); + ret = 0; +out: + mutex_unlock(&wifi->fwlog_lock); + return ret ?: count; +} + +static const struct file_operations fwlog_ack_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = fwlog_ack_write, +}; + +/* Root-only binary diagnostics. Save directly to a protected artifact, never + * the console or a text log. The device may update blocks during a read. + */ +static ssize_t fwlog_buffer_read(struct file *file, char __user *output, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + ssize_t ret; + + mutex_lock(&wifi->fwlog_lock); + if (!wifi->fwlog_ready) { + ret = -ENODATA; + } else { + dma_rmb(); + ret = simple_read_from_buffer(output, count, position, + wifi->fwlog_buffer, wifi->fwlog_size); + } + mutex_unlock(&wifi->fwlog_lock); + return ret; +} + +static const struct file_operations fwlog_buffer_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = fwlog_buffer_read, + .llseek = default_llseek, +}; + +/* The hardware data-ring callback requires the ten-byte WLAN clientdata + * extension after the 44-byte ACIPC header. Without it UMAC asserts at + * 0x11760e. Keep these empty-ring probes explicit until packet DMA is proven. + * The reserved regions below do not overlap control rings or scan buffers. + */ +static ssize_t data_probe_write(struct file *file, const char __user *input, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + u8 message[0x34 + 10] = { 0 }; + unsigned int id, vector, offset, size, bit, doorbell, irq; + bool transfer = false; + int ret; + + ret = kstrtouint_from_user(input, count, 0, &id); + if (ret) + return ret; + if (id == 76) { + vector = 192; + offset = 0x200000; + size = 2048; + bit = 0; + doorbell = 18; + irq = 3; + } else if (id == 14) { + vector = 206; + offset = 0x220000; + size = 256; + bit = 1; + doorbell = 11; + irq = 8; + } else if (id == 15) { + vector = 207; + offset = 0x240000; + size = 128; + bit = 2; + doorbell = 11; + irq = 8; + } else if (id == 24 || id == 25 || id == 12) { + transfer = true; + vector = id; + offset = id == 12 ? 0x520000 : id == 24 ? 0x500000 : 0x540000; + size = 128; + bit = id == 12 ? 4 : id == 24 ? 3 : 5; + doorbell = id == 12 ? 11 : 18; + irq = id == 12 ? 8 : 3; + } else { + return -EINVAL; + } + mutex_lock(&wifi->lock); + if (readl(wifi->a.bar + 8) != 2) { + ret = -EIO; + goto out; + } + if (wifi->connecting || wifi->scan_request || + (wifi->data_probe_done & BIT(bit))) { + ret = -EBUSY; + goto out; + } + wifi->data_probe_done |= BIT(bit); + memset(wifi->a.window + offset, 0, size * 32); + index_write(wifi, transfer ? 1 : 0, vector, 0); + index_write(wifi, transfer ? 3 : 2, vector, 0); + message[0] = transfer ? 1 : 2; + message[2] = transfer && id == 12 ? 0 : 4; + put_unaligned_le16(id, message + 4); + put_unaligned_le16(vector, message + 6); + put_unaligned_le64(wifi->a.window_dma + offset, message + 8); + if (transfer) { + put_unaligned_le64(~0ULL, message + 0x10); + put_unaligned_le16(size, message + 0x18); + put_unaligned_le16(id == 12 ? 12 : 76, message + 0x1a); + put_unaligned_le16(doorbell, message + 0x1c); + put_unaligned_le16(id == 12 ? 0x220 : 0, message + 0x1e); + put_unaligned_le32(16, message + 0x20); + put_unaligned_le16(irq, message + 0x24); + } else { + put_unaligned_le16(size, message + 0x10); + put_unaligned_le16(id == 15 ? 12 : 0xffff, message + 0x12); + put_unaligned_le16(doorbell, message + 0x14); + put_unaligned_le32(1, message + 0x18); + put_unaligned_le16(irq, message + 0x1c); + } + /* AirshipDataTxTransferRing::initWithOptions maps the host MCBC + * parameters to version 0, hardware interface 0, TID 6, AC 3, and + * six broadcast destination bytes. The default probe is unchanged. + */ + if (id == 25 && data_tx_broadcast) { + message[0x34 + 2] = 6; + message[0x34 + 3] = 3; + eth_broadcast_addr(message + 0x34 + 4); + } + ret = alpha_message(wifi, message, (transfer ? 0x34 : 0x2c) + 10); + msleep(1000); + dev_info(&wifi->control->pdev->dev, + "data %s probe id=%u index=%u extension=10 rc=%d stage=%u head=%u tail=%u\n", + transfer ? "TR" : "CR", id, vector, ret, readl(wifi->a.bar + 8), + index_read(wifi, transfer ? 3 : 0, vector), + index_read(wifi, transfer ? 1 : 2, vector)); + if (!ret && readl(wifi->a.bar + 8) != 2) + ret = -EIO; + if (!ret && transfer && id == 12) { + unsigned int slot; + + /* Give the receive engine bounded, individually identifiable buffers. + * No recycling or network delivery until the completion ABI is proven. + */ + for (slot = 0; slot < size - 1; slot++) { + u8 *descriptor = wifi->a.window + offset + slot * 16; + u32 buffer = DATA_RX_BUFFER + slot * 4096; + + memset(wifi->a.window + buffer, 0, 4096); + put_unaligned_le32(1 | (4096 << 8), descriptor); + put_unaligned_le64(wifi->a.window_dma + buffer, descriptor + 4); + put_unaligned_le32(slot + 1, descriptor + 12); + } + dma_wmb(); + index_write(wifi, 3, vector, size - 1); + dma_wmb(); + writel(1, wifi->a.bar + 0x1050); + msleep(100); + dev_info(&wifi->control->pdev->dev, + "data RX buffers posted=%u stage=%u tr_tail=%u cr_head=%u\n", + size - 1, readl(wifi->a.bar + 8), index_read(wifi, 1, 12), + index_read(wifi, 0, 207)); + } +out: + mutex_unlock(&wifi->lock); + return ret ?: count; +} + +static int transmit_eapol_start(struct centauri_wifi *wifi) +{ + u16 head = index_read(wifi, 3, 24); + u8 *descriptor = wifi->a.window + 0x500000 + head * 32; + u8 *packet = wifi->a.window + 0xf00000 + head * 64; + u64 metadata = 0; + unsigned int attempt; + + if (readl(wifi->a.bar + 8) != 2 || !(wifi->data_probe_done & BIT(3))) + return -ENETDOWN; + /* Eight independent buffers bound the probe without ring reuse. */ + if (head >= 8 || index_read(wifi, 1, 24) != head) + return -EBUSY; + if (eapol_unicast && (!wifi->associated || !is_valid_ether_addr(wifi->bssid))) + return -ENOTCONN; + /* A standards-defined EAPOL-Start, with no credential/key material. + * Descriptor flags 3 mean a DMA buffer plus the 16-byte WLAN footer. + * Interface zero and default TX policy have a zero footer. + */ + memset(packet, 0, 64); + if (eapol_unicast) + memcpy(packet, wifi->bssid, ETH_ALEN); + else + memcpy(packet, "\x01\x80\xc2\x00\x00\x03", ETH_ALEN); + memcpy(packet + ETH_ALEN, wifi->netdev->dev_addr, ETH_ALEN); + put_unaligned_be16(ETH_P_PAE, packet + 12); + packet[14] = 2; + packet[15] = 1; + memset(descriptor, 0, 32); + put_unaligned_le32(3 | (18 << 8), descriptor); + put_unaligned_le64(wifi->a.window_dma + 0xf00000 + head * 64, descriptor + 4); + put_unaligned_le16(head + 1, descriptor + 12); + if (eapol_control_profile) + metadata |= BIT_ULL(60) | BIT_ULL(48); + if (eapol_relative_lifetime) { + /* AirshipDataPacket::setRelativeExpirationTime uses mode 6 in + * bits 29:27 and microseconds in the second metadata word. + */ + metadata |= 6ULL << 27; + put_unaligned_le64(2000000, descriptor + 24); + } + put_unaligned_le64(metadata, descriptor + 16); + dma_wmb(); + index_write(wifi, 3, 24, head + 1); + dma_wmb(); + writel(1, wifi->a.bar + 0x1030); + for (attempt = 0; attempt < 20; attempt++) { + if (index_read(wifi, 1, 24) == head + 1 || readl(wifi->a.bar + 8) != 2) + break; + usleep_range(5000, 6000); + } + dev_info(&wifi->control->pdev->dev, + "EAPOL-Start DMA probe: slot=%u associated=%d unicast=%d relative_expiry=%d stage=%u tr_tail=%u cr_head=%u\n", + head, wifi->associated, eapol_unicast, eapol_relative_lifetime, readl(wifi->a.bar + 8), + index_read(wifi, 1, 24), index_read(wifi, 0, 192)); + return readl(wifi->a.bar + 8) == 2 ? 0 : -EIO; +} + +static ssize_t eapol_start_write(struct file *file, const char __user *input, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + bool enabled; + int ret; + + ret = kstrtobool_from_user(input, count, &enabled); + if (ret || !enabled) + return ret ?: -EINVAL; + mutex_lock(&wifi->lock); + if (wifi->connecting || wifi->scan_request) { + ret = -EBUSY; + goto out; + } + ret = transmit_eapol_start(wifi); +out: + mutex_unlock(&wifi->lock); + return ret ?: count; +} + +static const struct file_operations eapol_start_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = eapol_start_write, +}; + +static const struct file_operations data_probe_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = data_probe_write, +}; + +static ssize_t data_status_read(struct file *file, char __user *output, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + char status[1536]; + unsigned int i, length; + + mutex_lock(&wifi->lock); + dma_rmb(); + length = scnprintf(status, sizeof(status), + "stage=%u tx_cr=%u tx_tr=%u/%u rx_cr=%u rx_tr=%u/%u received=%u reposted=%u bad=%u samples=%u normal_tx_tr=%u/%u submitted=%u completed=%u failed=%u queued=%u coalesced=%u tx_bad=%u\n", + readl(wifi->a.bar + 8), index_read(wifi, 0, 192), + index_read(wifi, 1, 24), index_read(wifi, 3, 24), + index_read(wifi, 0, 207), index_read(wifi, 1, 12), + index_read(wifi, 3, 12), wifi->data_rx_received, + wifi->data_rx_reposted, wifi->data_rx_bad, + le32_to_cpu(wifi->rx_snapshot.count), + index_read(wifi, 1, 25), index_read(wifi, 3, 25), + wifi->data_tx_submitted, wifi->data_tx_completed, + wifi->data_tx_failed, skb_queue_len(&wifi->data_tx_queue), + wifi->data_tx_coalesced, wifi->data_tx_bad); + for (i = 0; i < 8; i++) { + u8 *entry = wifi->a.window + 0x240000 + i * 32; + + length += scnprintf(status + length, sizeof(status) - length, + "rx_completion[%u]=%08x %08x %08x %08x %08x %08x %08x %08x\n", + i, get_unaligned_le32(entry), get_unaligned_le32(entry + 4), + get_unaligned_le32(entry + 8), get_unaligned_le32(entry + 12), + get_unaligned_le32(entry + 16), get_unaligned_le32(entry + 20), + get_unaligned_le32(entry + 24), get_unaligned_le32(entry + 28)); + } + for (i = 0; i < 8; i++) { + u8 *entry = wifi->a.window + 0x200000 + i * 32; + + length += scnprintf(status + length, sizeof(status) - length, + "tx_completion[%u]=%08x %08x %08x %08x %08x %08x %08x %08x\n", + i, get_unaligned_le32(entry), get_unaligned_le32(entry + 4), + get_unaligned_le32(entry + 8), get_unaligned_le32(entry + 12), + get_unaligned_le32(entry + 16), get_unaligned_le32(entry + 20), + get_unaligned_le32(entry + 24), get_unaligned_le32(entry + 28)); + } + mutex_unlock(&wifi->lock); + return simple_read_from_buffer(output, count, position, status, length); +} + +static const struct file_operations data_status_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = data_status_read, +}; + +/* Contains private network traffic: retrieve only as a protected binary file. + * Samples are copied once into host memory and never change after count 8. + */ +static ssize_t data_rx_snapshot_read(struct file *file, char __user *output, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + ssize_t ret; + + mutex_lock(&wifi->lock); + ret = simple_read_from_buffer(output, count, position, &wifi->rx_snapshot, + sizeof(wifi->rx_snapshot)); + mutex_unlock(&wifi->lock); + return ret; +} + +static const struct file_operations data_rx_snapshot_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = data_rx_snapshot_read, + .llseek = default_llseek, +}; + +static ssize_t country_status_read(struct file *file, char __user *output, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + char status[96]; + unsigned int length; + int ret; + + mutex_lock(&wifi->lock); + wifi->country_valid = false; + ret = wifi->configured ? command(wifi, 10, NULL, 0) : -ENETDOWN; + length = scnprintf(status, sizeof(status), "rc=%d valid=%d country_bytes=%02x/%02x/%02x\n", + ret, wifi->country_valid, wifi->country[0], wifi->country[1], wifi->country[2]); + mutex_unlock(&wifi->lock); + return simple_read_from_buffer(output, count, position, status, length); +} + +static const struct file_operations country_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = country_status_read, +}; + +static int query_link_status(struct centauri_wifi *wifi) +{ + wifi->link_status_valid = wifi->emlsr_valid = false; + wifi->link_count = wifi->link_status_size = 0; + wifi->link_tlv_count = 0; + return wifi->configured ? command(wifi, 20, NULL, 0) : -ENETDOWN; +} + +static int link_status_show(struct seq_file *seq, void *unused) +{ + struct centauri_wifi *wifi = seq->private; + unsigned int i; + int ret; + + mutex_lock(&wifi->lock); + ret = query_link_status(wifi); + seq_printf(seq, "rc=%d valid=%d size=%u count=%u emlsr=%d/%u\n", ret, + wifi->link_status_valid, wifi->link_status_size, wifi->link_count, + wifi->emlsr_valid, wifi->emlsr_mode); + for (i = 0; i < wifi->link_tlv_count; i++) + seq_printf(seq, "tag=%u length=%u\n", wifi->link_tags[i], wifi->link_lengths[i]); + if (wifi->link_status_valid) + for (i = 0; i < wifi->link_count; i++) + seq_printf(seq, "link=%u prime=%u channel=%u band=%u\n", i, + wifi->link_prime[i], wifi->link_channel[i], wifi->link_band[i]); + mutex_unlock(&wifi->lock); + return 0; +} + +static int link_status_open(struct inode *inode, struct file *file) +{ + return single_open(file, link_status_show, inode->i_private); +} + +static const struct file_operations link_status_ops = { + .owner = THIS_MODULE, + .open = link_status_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static int tx_config_show(struct seq_file *seq, void *unused) +{ + struct centauri_wifi *wifi = seq->private; + u8 id; + int ret; + + mutex_lock(&wifi->lock); + seq_printf(seq, "applied=%u\n", wifi->tx_defaults_applied); + for (id = 0; id <= 1; id++) { + wifi->tx_profile_valid = false; + ret = wifi->configured ? command(wifi, 56, &id, sizeof(id)) : -ENETDOWN; + seq_printf(seq, "profile=%u rc=%d status=%u valid=%d", id, ret, + wifi->reply_status, wifi->tx_profile_valid); + if (wifi->tx_profile_valid) + seq_printf(seq, " rate=%#x rts_threshold=%u rts_retries=%u retries=%u type=%u id=%u", + get_unaligned_le32(wifi->tx_profile), + get_unaligned_le16(wifi->tx_profile + 4), wifi->tx_profile[6], + wifi->tx_profile[7], wifi->tx_profile[8], wifi->tx_profile[9]); + seq_putc(seq, '\n'); + } + mutex_unlock(&wifi->lock); + return 0; +} + +static int tx_config_open(struct inode *inode, struct file *file) +{ + return single_open(file, tx_config_show, inode->i_private); +} + +static ssize_t tx_config_write(struct file *file, const char __user *input, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = ((struct seq_file *)file->private_data)->private; + /* Exact configureTxDataCtrlPacketProfile: command 52, TLV 340/10. + * 0xa0154 is the TLV header, not the command ID. + */ + static const u8 control_profile[] = { + 0x54, 1, 10, 0, 0, 8, 0, 0, 0xff, 0xff, 0xff, 0xff, 1, 1, + }; + u8 lifetime[20] = {}; + unsigned int action, i; + int ret; + + ret = kstrtouint_from_user(input, count, 0, &action); + if (ret || (action != 1 && action != 2)) + return ret ?: -EINVAL; + mutex_lock(&wifi->lock); + if (!wifi->configured || wifi->connecting || wifi->scan_request) { + ret = -EBUSY; + goto out; + } + if (action == 1) { + /* AppleWLAN80211STA::resetTxPacketLifetime selects 2000 ms for + * each AC. The ACI adapter converts to firmware microseconds. + */ + for (i = 0; i < 4; i++) + put_unaligned_le32(2000000, lifetime + 4 + 4 * i); + ret = command(wifi, 43, lifetime, sizeof(lifetime)); + } else { + ret = command(wifi, 52, control_profile, sizeof(control_profile)); + } + if (!ret) + wifi->tx_defaults_applied |= BIT(action - 1); + dev_info(&wifi->control->pdev->dev, "reference TX setup action=%u rc=%d\n", action, ret); +out: + mutex_unlock(&wifi->lock); + return ret ?: count; +} + +static const struct file_operations tx_config_ops = { + .owner = THIS_MODULE, + .open = tx_config_open, + .read = seq_read, + .write = tx_config_write, + .llseek = seq_lseek, + .release = single_release, +}; + +static ssize_t host_wakeup_write(struct file *file, const char __user *input, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + /* ACIIO80211PowerAdapter::hostWakeup selects state 0 in command 6. + * This firmware's SysCmdSetHostPowerState accepts the fixed 27-byte + * request without TLVs. Keep its existing event filter: the newer host + * adapter's 92-byte event list exceeds this firmware's 86-byte limit. + */ + const u8 payload[3] = {}; + unsigned int action; + int ret; + + ret = kstrtouint_from_user(input, count, 0, &action); + if (ret || action != 1) + return ret ?: -EINVAL; + mutex_lock(&wifi->lock); + if (!wifi->configured || wifi->connecting || wifi->scan_request || wifi->connected) { + ret = -EBUSY; + goto out; + } + ret = command(wifi, 6, payload, sizeof(payload)); + dev_info(&wifi->control->pdev->dev, "host wakeup state=0 rc=%d status=%u stage=%u\n", + ret, wifi->reply_status, readl(wifi->a.bar + 8)); +out: + mutex_unlock(&wifi->lock); + return ret ?: count; +} + +static const struct file_operations host_wakeup_ops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = host_wakeup_write, +}; + +static int query_station_stats(struct centauri_wifi *wifi) +{ + u8 payload[22] = {}; + int ret; + + /* ACIDebugAdapter::queryStaCounterStats: statistic bit 37, all peers. + * Its fixed counter record contains no key or packet payload bytes. + */ + put_unaligned_le64(BIT_ULL(37), payload); + payload[8] = 1; + put_unaligned_le16(363, payload + 9); + put_unaligned_le16(9, payload + 11); + wifi->station_stats_valid = false; + wifi->stats_reply_size = wifi->stats_tag = wifi->stats_length = 0; + ret = wifi->configured ? command(wifi, 0xb0001, payload, sizeof(payload)) : -ENETDOWN; + if (!ret) { + usleep_range(1000, 2000); + drain(wifi); + } + return ret; +} + +static int station_stats_show(struct seq_file *seq, void *unused) +{ + struct centauri_wifi *wifi = seq->private; + unsigned int i; + int ret; + + mutex_lock(&wifi->lock); + ret = query_station_stats(wifi); + seq_printf(seq, "rc=%d valid=%d size=%u tag=%#x length=%u\n", ret, + wifi->station_stats_valid, wifi->stats_reply_size, wifi->stats_tag, wifi->stats_length); + if (!ret && wifi->station_stats_valid) + for (i = 0; i < wifi->stats_length; i += 4) { + u32 value = get_unaligned_le32(wifi->station_stats + i); + + if (value) + seq_printf(seq, "%03x %u\n", i, value); + } + mutex_unlock(&wifi->lock); + return 0; +} + +static int station_stats_open(struct inode *inode, struct file *file) +{ + return single_open(file, station_stats_show, inode->i_private); +} + +static const struct file_operations station_stats_ops = { + .owner = THIS_MODULE, + .open = station_stats_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static int rx_crypto_stats_show(struct seq_file *seq, void *unused) +{ + /* ACIStatisticsAdapter::printRxChipCounterStats at 0x1809d7b80: + * statistic bit 30, response TLV 0xf921, sixteen LE32 counters. + * Unlike the station record, this fixed structure contains no MACs. + */ + static const char * const names[] = { + "rx_duplicate_errors", "rx_ampdu_duplicate_errors", + "rx_unicast_tkip_replay_errors", "rx_unicast_tkip_decrypt_errors", + "rx_multicast_tkip_replay_errors", "rx_multicast_tkip_decrypt_errors", + "rx_unicast_ccmp_replay_errors", "rx_unicast_ccmp_decrypt_errors", + "rx_multicast_ccmp_replay_errors", "rx_multicast_ccmp_decrypt_errors", + "phy_total_detect_count", "phy_ofdm_detect_count", "phy_11b_detect_count", + "phy_plcp_error_count", "phy_total_receive_count", "phy_plcp_good_count", + }; + struct centauri_wifi *wifi = seq->private; + u8 payload[22] = {}; + unsigned int i; + int ret; + + put_unaligned_le64(BIT_ULL(30), payload); + payload[8] = 1; + put_unaligned_le16(363, payload + 9); + put_unaligned_le16(9, payload + 11); + mutex_lock(&wifi->lock); + wifi->rx_crypto_stats_valid = false; + wifi->stats_reply_size = wifi->stats_tag = wifi->stats_length = 0; + ret = wifi->configured ? command(wifi, 0xb0001, payload, sizeof(payload)) : -ENETDOWN; + if (!ret) { + usleep_range(1000, 2000); + drain(wifi); + } + seq_printf(seq, "rc=%d valid=%d size=%u tag=%#x length=%u\n", ret, + wifi->rx_crypto_stats_valid, wifi->stats_reply_size, wifi->stats_tag, wifi->stats_length); + if (!ret && wifi->rx_crypto_stats_valid) + for (i = 0; i < ARRAY_SIZE(names); i++) + seq_printf(seq, "%s=%u\n", names[i], + get_unaligned_le32(wifi->rx_crypto_stats + i * 4)); + mutex_unlock(&wifi->lock); + return 0; +} + +static int rx_crypto_stats_open(struct inode *inode, struct file *file) +{ + return single_open(file, rx_crypto_stats_show, inode->i_private); +} + +static const struct file_operations rx_crypto_stats_ops = { + .owner = THIS_MODULE, + .open = rx_crypto_stats_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static int dot11_mib_stats_show(struct seq_file *seq, void *unused) +{ + /* ACIStatisticsAdapter::printDot11MibStats at 0x1809d7fb0: + * statistic bit 25, scope 2/interface 0, TLV 0xf91b, 24 LE64s. + * Keep the host field names: tx_status_success is firmware accounting, + * not independent proof of over-the-air acknowledgement or AP delivery. + */ + static const char * const names[] = { + "rx_msdu_pushed", "rx_msdu_pushed_multi", "rx_mpdu", "rx_mpdu_dup", + "rx_ampdu_dup", "rx_mpdu_drop_error", "rx_msdu_drop_filter", + "rx_holes_0", "rx_holes_1", "rx_holes_2", "rx_holes_3", "rx_holes_4", + "rx_holes_5", "rx_holes_6", "rx_holes_7", "rx_holes_8", + "tx_pulled", "tx_pulled_multi", "tx_pulled_core", "tx_status_success", + "tx_status_abort_request", "tx_status_peer_gone", + "tx_status_retries_exhausted", "tx_status_lifetime_expired", + }; + struct centauri_wifi *wifi = seq->private; + u8 payload[22] = {}; + unsigned int i; + int ret; + + put_unaligned_le64(BIT_ULL(25), payload); + payload[8] = 1; + put_unaligned_le16(363, payload + 9); + put_unaligned_le16(9, payload + 11); + /* The scope byte follows the eight-byte interface value in TLV 363. */ + payload[21] = 2; + mutex_lock(&wifi->lock); + wifi->dot11_mib_stats_valid = false; + wifi->stats_reply_size = wifi->stats_tag = wifi->stats_length = 0; + ret = wifi->configured ? command(wifi, 0xb0001, payload, sizeof(payload)) : -ENETDOWN; + if (!ret) { + usleep_range(1000, 2000); + drain(wifi); + } + seq_printf(seq, "rc=%d valid=%d size=%u tag=%#x length=%u\n", ret, + wifi->dot11_mib_stats_valid, wifi->stats_reply_size, wifi->stats_tag, wifi->stats_length); + if (!ret && wifi->dot11_mib_stats_valid) + for (i = 0; i < ARRAY_SIZE(names); i++) + seq_printf(seq, "%s=%llu\n", names[i], + (unsigned long long)get_unaligned_le64(wifi->dot11_mib_stats + i * 8)); + mutex_unlock(&wifi->lock); + return 0; +} + +static int dot11_mib_stats_open(struct inode *inode, struct file *file) +{ + return single_open(file, dot11_mib_stats_show, inode->i_private); +} + +static const struct file_operations dot11_mib_stats_ops = { + .owner = THIS_MODULE, + .open = dot11_mib_stats_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +static int tx_power_report_show(struct seq_file *seq, void *unused) +{ + /* Exact stock 25G76 WLANACIAdapter::queryLastTxStats, 0x180914d58: + * bit29, scope1/radio bitmap, TLV 0xf920 with 280 bytes. The newer + * macOS27 adapter expects 320 bytes and has a different record stride. + * Stock getLastTxInfo (0x1809150f8) reads the first five 28-byte records. + * These are rate/power observations, not proof of ACK or AP delivery. + */ + struct centauri_wifi *wifi = seq->private; + u8 payload[22] = {}; + unsigned int radio, i; + int ret; + + put_unaligned_le64(BIT_ULL(29), payload); + payload[8] = 1; + put_unaligned_le16(363, payload + 9); + put_unaligned_le16(9, payload + 11); + payload[21] = 1; + mutex_lock(&wifi->lock); + for (radio = 1; radio <= 2; radio++) { + put_unaligned_le64(radio, payload + 13); + wifi->tx_power_report_valid = false; + wifi->stats_reply_size = wifi->stats_tag = wifi->stats_length = 0; + ret = wifi->configured ? command(wifi, 0xb0001, payload, sizeof(payload)) : -ENETDOWN; + if (!ret) { + usleep_range(1000, 2000); + drain(wifi); + } + seq_printf(seq, "radio_bitmap=%u rc=%d valid=%d size=%u tag=%#x length=%u\n", + radio, ret, wifi->tx_power_report_valid, wifi->stats_reply_size, + wifi->stats_tag, wifi->stats_length); + if (ret || !wifi->tx_power_report_valid) + continue; + for (i = 0; i < 5; i++) { + const u8 *record = wifi->tx_power_report + i * 28; + + /* Keep hardware enums and power bytes raw until interpreted + * against this exact firmware; omit unreviewed record fields. + */ + seq_printf(seq, "sample=%u power0_qdb_raw=%u power1_qdb_raw=%u power_qdb_raw=%u " + "hw_preamble=%u hw_rate=%u nss=%u hw_bw=%u hw_gi=%u\n", + i, record[14], record[15], record[16], record[17], + record[20], record[21] + 1, record[24], record[27]); + } + } + mutex_unlock(&wifi->lock); + return 0; +} + +static int tx_power_report_open(struct inode *inode, struct file *file) +{ + return single_open(file, tx_power_report_show, inode->i_private); +} + +static const struct file_operations tx_power_report_ops = { + .owner = THIS_MODULE, + .open = tx_power_report_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release, +}; + +int centauri_wifi_register(struct centauri *c) +{ + struct centauri_wifi *wifi; + unsigned int i; + int ret; + + wifi = kzalloc(sizeof(*wifi), GFP_KERNEL); + if (!wifi) + return -ENOMEM; + c->wifi = wifi; + wifi->control = c; + mutex_init(&wifi->lock); + mutex_init(&wifi->fwlog_lock); + skb_queue_head_init(&wifi->data_tx_queue); + INIT_WORK(&wifi->scan_work, scan_worker); + INIT_WORK(&wifi->connect_work, connect_worker); + INIT_DELAYED_WORK(&wifi->link_work, link_worker); + ret = centauri_alpha_resources(c, &wifi->a); + if (ret) + goto failed; + wifi->wiphy = wiphy_new(&wifi_ops, sizeof(struct centauri_wifi *)); + wifi->netdev = alloc_etherdev(sizeof(struct centauri_wifi *)); + if (!wifi->wiphy || !wifi->netdev) { + ret = -ENOMEM; + goto failed; + } + *(struct centauri_wifi **)wiphy_priv(wifi->wiphy) = wifi; + *(struct centauri_wifi **)netdev_priv(wifi->netdev) = wifi; + set_wiphy_dev(wifi->wiphy, &c->pdev->dev); + wifi->wiphy->interface_modes = BIT(NL80211_IFTYPE_STATION); + wifi->wiphy->max_scan_ssids = 1; + wifi->wiphy->signal_type = CFG80211_SIGNAL_TYPE_MBM; + wifi->wiphy->reg_notifier = wifi_regulatory; + wifi->wiphy->cipher_suites = cipher_suites; + wifi->wiphy->n_cipher_suites = ARRAY_SIZE(cipher_suites); + wiphy_ext_feature_set(wifi->wiphy, NL80211_EXT_FEATURE_4WAY_HANDSHAKE_STA_PSK); + for (i = 0; i < 13; i++) { + wifi->channels[i].band = NL80211_BAND_2GHZ; + wifi->channels[i].hw_value = i + 1; + wifi->channels[i].center_freq = ieee80211_channel_to_frequency(i + 1, NL80211_BAND_2GHZ); + wifi->channels[i].max_power = 20; + } + for (i = 0; i < ARRAY_SIZE(five_ghz); i++) { + wifi->channels[13 + i].band = NL80211_BAND_5GHZ; + wifi->channels[13 + i].hw_value = five_ghz[i]; + wifi->channels[13 + i].center_freq = ieee80211_channel_to_frequency(five_ghz[i], NL80211_BAND_5GHZ); + wifi->channels[13 + i].max_power = 20; + } + wifi->bands[0].channels = wifi->channels; + wifi->bands[0].n_channels = 13; + wifi->bands[0].bitrates = rates; + wifi->bands[0].n_bitrates = ARRAY_SIZE(rates); + wifi->bands[1].channels = wifi->channels + 13; + wifi->bands[1].n_channels = ARRAY_SIZE(five_ghz); + wifi->bands[1].bitrates = rates + 4; + wifi->bands[1].n_bitrates = ARRAY_SIZE(rates) - 4; + wifi->wiphy->bands[NL80211_BAND_2GHZ] = &wifi->bands[0]; + wifi->wiphy->bands[NL80211_BAND_5GHZ] = &wifi->bands[1]; + ret = wiphy_register(wifi->wiphy); + if (ret) + goto failed; + wifi->registered = true; + wifi->wdev.wiphy = wifi->wiphy; + wifi->wdev.iftype = NL80211_IFTYPE_STATION; + wifi->wdev.netdev = wifi->netdev; + wifi->netdev->ieee80211_ptr = &wifi->wdev; + wifi->netdev->netdev_ops = &net_ops; + SET_NETDEV_DEV(wifi->netdev, &c->pdev->dev); + strscpy(wifi->netdev->name, "wlan%d", IFNAMSIZ); + wifi->netdev->max_mtu = ETH_DATA_LEN; + eth_hw_addr_random(wifi->netdev); + ret = register_netdev(wifi->netdev); + if (ret) + goto failed; + ret = radio_start(wifi); + if (ret) { + dev_err(&c->pdev->dev, "Wi-Fi ring/interface setup failed: %d\n", ret); + goto failed; + } + dev_info(&c->pdev->dev, "cfg80211 interface %s ready for scan tests; packet data unavailable\n", + wifi->netdev->name); + wifi->passphrase_file = debugfs_create_file("passphrase", 0200, c->debug, wifi, &passphrase_ops); + wifi->data_probe_file = debugfs_create_file("data_probe", 0200, c->debug, wifi, &data_probe_ops); + wifi->data_status_file = debugfs_create_file("data_status", 0400, c->debug, wifi, &data_status_ops); + wifi->data_rx_snapshot_file = debugfs_create_file("data_rx_snapshot", 0400, + c->debug, wifi, &data_rx_snapshot_ops); + wifi->station_stats_file = debugfs_create_file("station_stats", 0400, c->debug, wifi, &station_stats_ops); + wifi->rx_crypto_stats_file = debugfs_create_file("rx_crypto_stats", 0400, + c->debug, wifi, &rx_crypto_stats_ops); + wifi->dot11_mib_stats_file = debugfs_create_file("dot11_mib_stats", 0400, + c->debug, wifi, &dot11_mib_stats_ops); + wifi->tx_power_report_file = debugfs_create_file("tx_power_report", 0400, + c->debug, wifi, &tx_power_report_ops); + wifi->eapol_start_file = debugfs_create_file("eapol_start", 0200, c->debug, wifi, &eapol_start_ops); + wifi->country_file = debugfs_create_file("country", 0400, c->debug, wifi, &country_ops); + wifi->link_status_file = debugfs_create_file("link_status", 0400, c->debug, wifi, &link_status_ops); + wifi->tx_config_file = debugfs_create_file("tx_config", 0600, c->debug, wifi, &tx_config_ops); + wifi->host_wakeup_file = debugfs_create_file("host_wakeup", 0200, c->debug, wifi, &host_wakeup_ops); + wifi->fwlog_start_file = debugfs_create_file("fwlog_start", 0200, c->debug, wifi, &fwlog_start_ops); + wifi->fwlog_status_file = debugfs_create_file("fwlog_status", 0400, c->debug, wifi, &fwlog_status_ops); + wifi->fwlog_buffer_file = debugfs_create_file("fwlog_buffer", 0400, c->debug, wifi, &fwlog_buffer_ops); + wifi->fwlog_ack_file = debugfs_create_file("fwlog_ack", 0200, c->debug, wifi, &fwlog_ack_ops); + return 0; +failed: + centauri_wifi_unregister(c); + return ret; +} + +void centauri_wifi_unregister(struct centauri *c) +{ + struct centauri_wifi *wifi = c->wifi; + + if (!wifi) + return; + WRITE_ONCE(wifi->stopping, true); + if (wifi->netdev && wifi->netdev->reg_state == NETREG_REGISTERED) + netif_tx_disable(wifi->netdev); + debugfs_remove(wifi->passphrase_file); + debugfs_remove(wifi->data_probe_file); + debugfs_remove(wifi->data_status_file); + debugfs_remove(wifi->data_rx_snapshot_file); + debugfs_remove(wifi->station_stats_file); + debugfs_remove(wifi->rx_crypto_stats_file); + debugfs_remove(wifi->dot11_mib_stats_file); + debugfs_remove(wifi->tx_power_report_file); + debugfs_remove(wifi->eapol_start_file); + debugfs_remove(wifi->country_file); + debugfs_remove(wifi->link_status_file); + debugfs_remove(wifi->tx_config_file); + debugfs_remove(wifi->host_wakeup_file); + debugfs_remove(wifi->fwlog_start_file); + debugfs_remove(wifi->fwlog_status_file); + debugfs_remove(wifi->fwlog_buffer_file); + debugfs_remove(wifi->fwlog_ack_file); + cancel_work_sync(&wifi->scan_work); + cancel_work_sync(&wifi->connect_work); + cancel_delayed_work_sync(&wifi->link_work); + skb_queue_purge(&wifi->data_tx_queue); + if (wifi->scan_request) { + struct cfg80211_scan_info info = { .aborted = true }; + + cfg80211_scan_done(wifi->scan_request, &info); + wifi->scan_request = NULL; + } + if (wifi->netdev && wifi->netdev->reg_state == NETREG_REGISTERED) + unregister_netdev(wifi->netdev); + if (wifi->registered) + wiphy_unregister(wifi->wiphy); + if (wifi->wiphy) + wiphy_free(wifi->wiphy); + if (wifi->netdev) + free_netdev(wifi->netdev); + memzero_explicit(wifi, sizeof(*wifi)); + kfree(wifi); + c->wifi = NULL; +} From 2aefb5d3e9db58205bec4d9d11dfae951273fc72 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Thu, 10 Sep 2026 01:15:28 -0400 Subject: [PATCH 02/22] Bluetooth: add J813 N1 native transport and encrypted connection bring-up [Thomas: the driver part of c0f6eceacec7 on the J813 line of linux-aurora] Signed-off-by: Thomas Glanzmann --- drivers/bluetooth/Kconfig | 14 + drivers/bluetooth/Makefile | 9 +- drivers/bluetooth/btcentauri.c | 984 ++++++++++++++++++ .../wireless/apple/centauri/centauri_ipc.c | 37 + 4 files changed, 1040 insertions(+), 4 deletions(-) create mode 100644 drivers/bluetooth/btcentauri.c diff --git a/drivers/bluetooth/Kconfig b/drivers/bluetooth/Kconfig index b35637d575a34..832e2d3019ecc 100644 --- a/drivers/bluetooth/Kconfig +++ b/drivers/bluetooth/Kconfig @@ -506,4 +506,18 @@ config BT_INTEL_PCIE Say Y here to compiler support for Intel Bluetooth PCIe device into the kernel or say M to compile it as module (btintel_pcie) + +config BT_CENTAURI + tristate "Apple N1 Beta Bluetooth transport (experimental)" + depends on PCI && (ARCH_APPLE || COMPILE_TEST) + help + Experimental Bluetooth transport for Apple N1 on J813. + The Centauri Control driver must boot the shared firmware before this + driver binds to Beta. Controller initialization, discovery, BR/EDR ACL, + pairing and encrypted AVDTP discovery have been tested on hardware. + ACL delivery and native startup/connection handling remain opt-in; + audio playback and LE data are unqualified. SCO/ISO is not implemented. + + Say M to build the btcentauri module for bring-up testing. + endmenu diff --git a/drivers/bluetooth/Makefile b/drivers/bluetooth/Makefile index e0f1cea5b6092..74c3834c2c4a3 100644 --- a/drivers/bluetooth/Makefile +++ b/drivers/bluetooth/Makefile @@ -7,8 +7,12 @@ obj-$(CONFIG_BT_HCIVHCI) += hci_vhci.o obj-$(CONFIG_BT_HCIUART) += hci_uart.o obj-$(CONFIG_BT_HCIBCM203X) += bcm203x.o obj-$(CONFIG_BT_HCIBCM4377) += hci_bcm4377.o +obj-$(CONFIG_BT_HCIMT79XX_PCIE) += btmtk_pcie.o obj-$(CONFIG_BT_HCIBPA10X) += bpa10x.o obj-$(CONFIG_BT_HCIBFUSB) += bfusb.o +obj-$(CONFIG_BT_HCIDTL1) += dtl1_cs.o +obj-$(CONFIG_BT_HCIBT3C) += bt3c_cs.o +obj-$(CONFIG_BT_HCIBLUECARD) += bluecard_cs.o obj-$(CONFIG_BT_HCIBTUSB) += btusb.o obj-$(CONFIG_BT_HCIBTSDIO) += btsdio.o @@ -50,7 +54,4 @@ hci_uart-$(CONFIG_BT_HCIUART_AG6XX) += hci_ag6xx.o hci_uart-$(CONFIG_BT_HCIUART_MRVL) += hci_mrvl.o hci_uart-$(CONFIG_BT_HCIUART_AML) += hci_aml.o hci_uart-objs := $(hci_uart-y) - -obj-$(CONFIG_BT_MTK7932_PCIE) += mt7932_bt_pcie.o - -CONTEXT_ANALYSIS := y +obj-$(CONFIG_BT_CENTAURI) += btcentauri.o diff --git a/drivers/bluetooth/btcentauri.c b/drivers/bluetooth/btcentauri.c new file mode 100644 index 0000000000000..d2539539fa162 --- /dev/null +++ b/drivers/bluetooth/btcentauri.c @@ -0,0 +1,984 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Apple N1 Beta command/event transport for J813 bring-up. + * + * The Centauri Control driver must have booted the shared firmware first. + * This driver owns only Beta. It never resets the port or the Wi-Fi function. + * BR/EDR discovery, ACL/SDP, pairing and encrypted AVDTP discovery are qualified. + * ACL remains opt-in; LE data, persistent bonds and audio are unqualified. + * SCO/ISO delivery is not implemented. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define CEN_ARENA_SIZE SZ_1M +#define CEN_MTR 0x2000 +#define CEN_MCR 0x2200 +#define CEN_MESSAGES 0x4000 +#define CEN_RX 0x8000 +#define CEN_TX 0xa000 +#define CEN_FOOTER 260 +#define CEN_STRIDE (16 + CEN_FOOTER) +#define CEN_RX_SLOTS 17 +#define CEN_TX_SLOTS 5 +#define CEN_LE_RX 0xc000 +#define CEN_LE_TX 0xe000 +#define CEN_LE_FOOTER 256 +#define CEN_LE_STRIDE (16 + CEN_LE_FOOTER) +#define CEN_LE_SLOTS 17 +#define CEN_MESSAGE_SLOTS 16 +#define CEN_AUX_BASE 0x20000 +#define CEN_CAPTURE_SIZE SZ_1M + +static bool arm_le_data_rings = true; +module_param(arm_le_data_rings, bool, 0444); +MODULE_PARM_DESC(arm_le_data_rings, "Arm native Beta LE rings for radio bring-up"); + +static bool arm_auxiliary_rings; +module_param(arm_auxiliary_rings, bool, 0444); +MODULE_PARM_DESC(arm_auxiliary_rings, "Diagnostic: arm remaining native Beta resources and capture received frames privately"); + +static bool native_startup; +module_param(native_startup, bool, 0444); +MODULE_PARM_DESC(native_startup, "Diagnostic: program a temporary address, InitDone and random seed before the first HCI reset"); + +static bool enable_acl; +module_param(enable_acl, bool, 0444); +MODULE_PARM_DESC(enable_acl, "Experimental ACL data delivery; requires LE and auxiliary rings"); + +static bool native_hrb; +module_param(native_hrb, bool, 0444); +MODULE_PARM_DESC(native_hrb, "Experimental native Proxima Create Connection; requires native startup, ACL and firmware capability bit 9"); + +#define CEN_OP_READ_PROP_FEATURES 0xffa3 +#define CEN_OP_HRB_CREATE_CONN 0xff7b +#define CEN_PROP_HRB_CREATE BIT(9) + +struct cen_aux_spec { + u16 id, slots, footer; + bool receive; +}; + +/* Host native Beta inventory; each resource is opt-in until target-qualified. + * HCI and LE rings remain in their existing, separately qualified arenas. + */ +static const struct cen_aux_spec cen_aux_specs[] = { + { 2, 9, 2592, false }, /* ACL TX */ + { 3, 9, 724, false }, /* SCO TX */ + { 5, 9, 260, false }, /* LEA TX */ + { 6, 5, 1024, false }, /* LLA TX */ + { 8, 9, 2592, true }, /* ACL RX */ + { 9, 9, 724, true }, /* SCO RX */ + { 11, 9, 268, true }, /* LEA RX */ + { 12, 128, 1024, true }, /* Firmware debug RX */ + { 13, 17, 260, true }, /* LMP RX */ + { 14, 9, 1304, false }, /* TSI TX */ + { 15, 9, 1304, true }, /* TSI RX */ +}; + +struct cen_aux_state { + u32 offset, packets; + u16 head, post; +}; + +struct cen_bt { + struct pci_dev *pdev; + struct hci_dev *hdev; + struct dentry *debug; + void __iomem *bar; + u8 *arena; + dma_addr_t dma; + struct mutex lock; + struct delayed_work poll; + struct sk_buff_head txq; + struct sk_buff_head acl_txq, le_txq; + u16 acl_tx_head, le_tx_head; + u16 message_head, message_tag, tx_head, rx_head, rx_post; + u16 le_rx_head, le_rx_post; + u32 le_rx_packets; + struct cen_aux_state aux[ARRAY_SIZE(cen_aux_specs)]; + u8 *capture; + u32 capture_used, capture_records, capture_dropped; + bool ipc_requested, running; + u8 native_startup_steps; + bool hrb_supported, hrb_status_pending; + u32 proprietary_features, hrb_commands, hrb_status_events; + u32 hrb_connection_events; + u8 hrb_last_link_type; +}; + +static u16 cen_tail(struct cen_bt *bt, u16 ring) +{ + return le16_to_cpu(READ_ONCE(*(__le16 *)(bt->arena + 0x7a + ring * 2))); +} + +static void cen_post(struct cen_bt *bt, u16 ring, u16 value) +{ + put_unaligned_le16(value, bt->arena + 0xa8 + ring * 2); + dma_wmb(); + writel(value, bt->bar + 0x1000 + ring * 4); +} + +static int cen_message(struct cen_bt *bt, const void *data, size_t length) +{ + u16 head = bt->message_head, tail, next = (head + 1) % CEN_MESSAGE_SLOTS; + u16 tag = bt->message_tag + 1; + u8 *descriptor = bt->arena + CEN_MTR + head * 16; + u8 *completion = bt->arena + CEN_MCR + head * 32; + int ret; + + if (head >= CEN_MESSAGE_SLOTS || !length || length > 512) + return -EINVAL; + memset(descriptor, 0, 16); + memset(completion, 0, 32); + memcpy(bt->arena + CEN_MESSAGES + head * 512, data, length); + put_unaligned_le32(1 | (length << 8), descriptor); + put_unaligned_le64(bt->dma + CEN_MESSAGES + head * 512, descriptor + 4); + put_unaligned_le16(tag, descriptor + 12); + cen_post(bt, 0, next); + ret = read_poll_timeout(cen_tail, tail, tail == next, + 100, 1500000, false, bt, 0); + if (ret) + return ret; + /* The device publishes the message completion before advancing its head. */ + ret = read_poll_timeout(le16_to_cpu, tail, tail == next, + 100, 1500000, false, + READ_ONCE(*(__le16 *)(bt->arena + 0x78))); + if (ret) + return ret; + dma_rmb(); + if (get_unaligned_le16(completion + 4) != tag || + get_unaligned_le16(completion + 6) != length || + get_unaligned_le32(completion + 8)) + return -EPROTO; + bt->message_head = next; + bt->message_tag = tag; + put_unaligned_le16(bt->message_head, bt->arena + 0xa6); + dma_wmb(); + return 0; +} + +static int cen_open_ring(struct cen_bt *bt, u16 id, u16 slots, u32 offset, u16 footer) +{ + u8 message[0x34] = { 1 }; + u32 words = footer / 4, shift = 0; + + if (!words || footer % 4) + return -EINVAL; + if (words > 255) + shift = min_t(u32, __ffs(words), 4); + if ((words >> shift) > 255) + return -EINVAL; + message[2] = words >> shift; + message[3] = shift << 4; + put_unaligned_le16(id, message + 4); + put_unaligned_le64(bt->dma + offset, message + 8); + put_unaligned_le64(~0ULL, message + 0x10); + put_unaligned_le16(slots, message + 0x18); + put_unaligned_le16(0xffff, message + 0x1a); + put_unaligned_le16(id, message + 0x1c); + put_unaligned_le16(0x40, message + 0x1e); + return cen_message(bt, message, sizeof(message)); +} + +static void cen_rx_buffer(struct cen_bt *bt, u16 slot) +{ + u8 *descriptor = bt->arena + CEN_RX + slot * CEN_STRIDE; + + memset(descriptor, 0, CEN_STRIDE); + put_unaligned_le32(1 | (CEN_FOOTER << 8), descriptor); + put_unaligned_le16(slot + 1, descriptor + 12); +} + +static void cen_le_rx_buffer(struct cen_bt *bt, u16 slot) +{ + u8 *descriptor = bt->arena + CEN_LE_RX + slot * CEN_LE_STRIDE; + + memset(descriptor, 0, CEN_LE_STRIDE); + put_unaligned_le32(1 | (CEN_LE_FOOTER << 8), descriptor); + put_unaligned_le16(slot + 1, descriptor + 12); +} + +static void cen_capture_frame(struct cen_bt *bt, u16 ring, const u8 *data, u16 length) +{ + u8 *record; + + if (!bt->capture) + return; + if (8U + length > CEN_CAPTURE_SIZE - bt->capture_used) { + bt->capture_dropped++; + return; + } + record = bt->capture + bt->capture_used; + put_unaligned_le16(ring, record); + put_unaligned_le16(length, record + 2); + put_unaligned_le32(bt->capture_records++, record + 4); + memcpy(record + 8, data, length); + bt->capture_used += 8 + length; +} + +static int cen_receive_acl(struct cen_bt *bt, const u8 *data, u32 length) +{ + struct sk_buff *skb; + u32 packet_length, pos; + + if (length < HCI_ACL_HDR_SIZE) + return -EPROTO; + packet_length = HCI_ACL_HDR_SIZE + get_unaligned_le16(data + 2); + if (packet_length > length) + return -EPROTO; + /* Airship RX attributes follow, rather than belong to, the HCI packet. + * Only the native parser's known record sizes are accepted. Their values + * remain opaque and are retained only in the private diagnostic capture. + */ + for (pos = packet_length; pos < length;) { + if (!data[pos]) + break; + if (data[pos] == 1) + pos++; + else if (data[pos] == 2 && length - pos >= 9) + pos += 9; + else + return -EPROTO; + } + skb = bt_skb_alloc(packet_length, GFP_KERNEL); + if (!skb) { + bt->hdev->stat.err_rx++; + return 0; + } + skb_put_data(skb, data, packet_length); + hci_skb_pkt_type(skb) = HCI_ACLDATA_PKT; + bt->hdev->stat.byte_rx += packet_length; + hci_recv_frame(bt->hdev, skb); + return 0; +} + +static void cen_aux_rx_buffer(struct cen_bt *bt, unsigned int index, u16 slot) +{ + const struct cen_aux_spec *spec = &cen_aux_specs[index]; + u8 *descriptor = bt->arena + bt->aux[index].offset + slot * (16 + spec->footer); + + memset(descriptor, 0, 16 + spec->footer); + put_unaligned_le32(1 | (spec->footer << 8), descriptor); + put_unaligned_le16(slot + 1, descriptor + 12); +} + +static int cen_aux_start(struct cen_bt *bt) +{ + u32 offset = CEN_AUX_BASE; + unsigned int i, slot; + int ret; + + for (i = 0; i < ARRAY_SIZE(cen_aux_specs); i++) { + const struct cen_aux_spec *spec = &cen_aux_specs[i]; + struct cen_aux_state *state = &bt->aux[i]; + u32 length = spec->slots * (16U + spec->footer); + + if (offset > CEN_ARENA_SIZE || length > CEN_ARENA_SIZE - offset) + return -ENOSPC; + state->offset = offset; + offset = ALIGN(offset + length, SZ_4K); + ret = cen_open_ring(bt, spec->id, spec->slots, state->offset, spec->footer); + if (ret) { + dev_err(&bt->pdev->dev, "Auxiliary ring %u open failed: %d\n", spec->id, ret); + return ret; + } + if (!spec->receive) + continue; + for (slot = 0; slot < spec->slots - 1; slot++) + cen_aux_rx_buffer(bt, i, slot); + state->post = spec->slots - 1; + cen_post(bt, spec->id, state->post); + } + return 0; +} + +static int cen_aux_drain(struct cen_bt *bt, bool repost) +{ + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(cen_aux_specs); i++) { + const struct cen_aux_spec *spec = &cen_aux_specs[i]; + struct cen_aux_state *state = &bt->aux[i]; + u16 tail; + int budget = spec->slots; + + if (!spec->receive) + continue; + tail = cen_tail(bt, spec->id); + if (tail >= spec->slots) + return -EPROTO; + while (state->head != tail && budget--) { + u8 *descriptor = bt->arena + state->offset + state->head * (16 + spec->footer); + u32 length; + + dma_rmb(); + length = get_unaligned_le32(descriptor) >> 8; + if (length > spec->footer) + return -EPROTO; + cen_capture_frame(bt, spec->id, descriptor + 16, length); + if (repost && enable_acl && spec->id == 8 && + cen_receive_acl(bt, descriptor + 16, length)) + return -EPROTO; + state->packets++; + state->head = (state->head + 1) % spec->slots; + if (repost) { + cen_aux_rx_buffer(bt, i, state->post); + state->post = (state->post + 1) % spec->slots; + cen_post(bt, spec->id, state->post); + } + } + } + return 0; +} + +static int cen_start(struct cen_bt *bt) +{ + static const u16 indices[] = { 0x78, 0x7a, 0xa6, 0xa8 }; + u8 config[4] = { 9, 0, 0xff, 0xff }; + u32 status; + int i, ret; + + put_unaligned_le16(0x300, bt->arena); + put_unaligned_le16(0x68, bt->arena + 2); + put_unaligned_le64(bt->dma + 0x68, bt->arena + 8); + for (i = 0; i < ARRAY_SIZE(indices); i++) + put_unaligned_le64(bt->dma + indices[i], bt->arena + 0x10 + i * 8); + put_unaligned_le16(1, bt->arena + 0x30); + put_unaligned_le16(22, bt->arena + 0x32); + put_unaligned_le64(bt->dma + CEN_MCR, bt->arena + 0x34); + put_unaligned_le64(bt->dma + CEN_MTR, bt->arena + 0x3c); + put_unaligned_le16(16, bt->arena + 0x44); + put_unaligned_le16(16, bt->arena + 0x46); + /* The 25G76 native context encodes the MCR footer in four-byte words. */ + bt->arena[0x53] = 4; + put_unaligned_le64(bt->dma + 0x10000, bt->arena + 0x58); + pci_set_master(bt->pdev); + writel(lower_32_bits(bt->dma), bt->bar + 0xc); + writel(upper_32_bits(bt->dma), bt->bar + 0x10); + writel(CEN_ARENA_SIZE, bt->bar + 0x14); + bt->ipc_requested = true; + writel(1, bt->bar + 0x107c); + ret = readl_poll_timeout(bt->bar, status, status == 1, 100, 2000000); + if (ret) + return ret; + dma_wmb(); + writel(lower_32_bits(bt->dma), bt->bar + 4); + writel(upper_32_bits(bt->dma), bt->bar + 8); + writel(2, bt->bar + 0x107c); + ret = readl_poll_timeout(bt->bar, status, status == 2, 100, 2000000); + if (ret) + return ret; + ret = cen_message(bt, config, sizeof(config)); + if (!ret) + ret = cen_open_ring(bt, 7, CEN_RX_SLOTS, CEN_RX, CEN_FOOTER); + if (!ret) + ret = cen_open_ring(bt, 1, CEN_TX_SLOTS, CEN_TX, CEN_FOOTER); + if (!ret && arm_le_data_rings) + ret = cen_open_ring(bt, 10, CEN_LE_SLOTS, CEN_LE_RX, CEN_LE_FOOTER); + if (!ret && arm_le_data_rings) + ret = cen_open_ring(bt, 4, CEN_LE_SLOTS, CEN_LE_TX, CEN_LE_FOOTER); + if (ret) + return ret; + for (i = 0; i < CEN_RX_SLOTS - 1; i++) + cen_rx_buffer(bt, i); + bt->rx_post = CEN_RX_SLOTS - 1; + cen_post(bt, 7, bt->rx_post); + if (arm_le_data_rings) { + for (i = 0; i < CEN_LE_SLOTS - 1; i++) + cen_le_rx_buffer(bt, i); + bt->le_rx_post = CEN_LE_SLOTS - 1; + cen_post(bt, 10, bt->le_rx_post); + } + if (arm_auxiliary_rings) + return cen_aux_start(bt); + return 0; +} + +static int cen_transmit(struct cen_bt *bt, struct sk_buff_head *queue, + u16 ring, u16 slots, u32 offset, u16 footer, u16 *head) +{ + struct sk_buff *skb; + u16 tail, next; + u8 *descriptor; + u32 wire_length; + + while (!skb_queue_empty(queue)) { + tail = cen_tail(bt, ring); + if (tail >= slots) + return -EPROTO; + next = (*head + 1) % slots; + if (next == tail) + break; + dma_rmb(); + skb = skb_dequeue(queue); + if (!skb) + break; + descriptor = bt->arena + offset + *head * (16 + footer); + memset(descriptor, 0, 16 + footer); + put_unaligned_le16(*head + 1, descriptor + 12); + memcpy(descriptor + 16, skb->data, skb->len); + wire_length = skb->len; + if (ring == 1 && READ_ONCE(bt->hrb_supported) && + get_unaligned_le16(skb->data) == HCI_OP_CREATE_CONN) { + u8 *command = descriptor + 16; + + /* Native Proxima uses the ordinary 13-byte parameter layout + * followed by two zero HRB fields (no forced band). It sends + * tagging=false and role_switch=0. Address byte order matches + * standard HCI. Keep the core's sent skb/opcode unchanged. + */ + put_unaligned_le16(CEN_OP_HRB_CREATE_CONN, command); + command[2] = 15; + command[HCI_COMMAND_HDR_SIZE + 9] = 0; + command[HCI_COMMAND_HDR_SIZE + 12] = 0; + wire_length = HCI_COMMAND_HDR_SIZE + 15; + bt->hrb_status_pending = true; + bt->hrb_commands++; + cen_capture_frame(bt, 1, command, wire_length); + } + put_unaligned_le32(1 | (wire_length << 8), descriptor); + *head = next; + cen_post(bt, ring, next); + if (hci_skb_pkt_type(skb) == HCI_COMMAND_PKT) + bt->hdev->stat.cmd_tx++; + else + bt->hdev->stat.acl_tx++; + bt->hdev->stat.byte_tx += wire_length; + kfree_skb(skb); + } + return 0; +} + +static void cen_poll(struct work_struct *work) +{ + struct cen_bt *bt = container_of(to_delayed_work(work), struct cen_bt, poll); + struct hci_dev *hdev = bt->hdev; + struct sk_buff *skb; + u16 tail; + u8 *descriptor; + u32 length; + int budget = CEN_RX_SLOTS; + + mutex_lock(&bt->lock); + if (!bt->running) + goto unlock; + if (readl(bt->bar + 0x18) != 2 || readl(bt->bar) != 2) { + /* Preserve already completed diagnostics after abort, without posting + * any more buffers or ringing doorbells to the failed firmware. + */ + if (arm_auxiliary_rings && readl(bt->bar) == 2) + cen_aux_drain(bt, false); + goto protocol_error; + } + if (arm_auxiliary_rings && cen_aux_drain(bt, true)) + goto protocol_error; + if (arm_le_data_rings) { + tail = cen_tail(bt, 10); + if (tail >= CEN_LE_SLOTS) + goto protocol_error; + while (bt->le_rx_head != tail && budget--) { + dma_rmb(); + descriptor = bt->arena + CEN_LE_RX + bt->le_rx_head * CEN_LE_STRIDE; + length = get_unaligned_le32(descriptor) >> 8; + if (length > CEN_LE_FOOTER) + goto protocol_error; + bt->le_rx_packets++; + cen_capture_frame(bt, 10, descriptor + 16, length); + if (enable_acl && cen_receive_acl(bt, descriptor + 16, length)) + goto protocol_error; + bt->le_rx_head = (bt->le_rx_head + 1) % CEN_LE_SLOTS; + cen_le_rx_buffer(bt, bt->le_rx_post); + bt->le_rx_post = (bt->le_rx_post + 1) % CEN_LE_SLOTS; + cen_post(bt, 10, bt->le_rx_post); + } + budget = CEN_RX_SLOTS; + } + tail = cen_tail(bt, 7); + if (tail >= CEN_RX_SLOTS) + goto protocol_error; + while (bt->rx_head != tail && budget--) { + dma_rmb(); + descriptor = bt->arena + CEN_RX + bt->rx_head * CEN_STRIDE; + length = get_unaligned_le32(descriptor) >> 8; + if (length < HCI_EVENT_HDR_SIZE || length > CEN_FOOTER || + length != descriptor[17] + HCI_EVENT_HDR_SIZE) + goto protocol_error; + skb = bt_skb_alloc(length, GFP_KERNEL); + if (skb) { + skb_put_data(skb, descriptor + 16, length); + /* The firmware's status belongs to the standard connection + * request tracked by Linux. Capture the native response before + * restoring only its opcode; preserve status and command credit. + */ + if (bt->hrb_status_pending && length == 6 && + skb->data[0] == HCI_EV_CMD_STATUS && + get_unaligned_le16(skb->data + 4) == CEN_OP_HRB_CREATE_CONN) { + cen_capture_frame(bt, 7, skb->data, length); + put_unaligned_le16(HCI_OP_CREATE_CONN, skb->data + 4); + bt->hrb_status_pending = false; + bt->hrb_status_events++; + } + /* Native HCI flow treats 0xf0/0xf1 Connection Complete as + * ACL, with additional proprietary capability flags. Linux + * otherwise cannot find its pending ACL connection by type. + * Preserve the original event privately before normalizing it. + */ + if (READ_ONCE(bt->hrb_supported) && + length == HCI_EVENT_HDR_SIZE + sizeof(struct hci_ev_conn_complete) && + skb->data[0] == HCI_EV_CONN_COMPLETE && + (skb->data[11] & 0xfe) == 0xf0) { + cen_capture_frame(bt, 7, skb->data, length); + bt->hrb_last_link_type = skb->data[11]; + skb->data[11] = ACL_LINK; + bt->hrb_connection_events++; + } + hci_skb_pkt_type(skb) = HCI_EVENT_PKT; + hdev->stat.byte_rx += length; + hci_recv_frame(hdev, skb); + } else { + hdev->stat.err_rx++; + } + bt->rx_head = (bt->rx_head + 1) % CEN_RX_SLOTS; + cen_rx_buffer(bt, bt->rx_post); + bt->rx_post = (bt->rx_post + 1) % CEN_RX_SLOTS; + cen_post(bt, 7, bt->rx_post); + } + if (cen_transmit(bt, &bt->txq, 1, CEN_TX_SLOTS, CEN_TX, + CEN_FOOTER, &bt->tx_head)) + goto protocol_error; + /* cen_aux_specs[0] is the native BR/EDR ACL TX resource (ring 2). */ + if (enable_acl && + (cen_transmit(bt, &bt->acl_txq, 2, cen_aux_specs[0].slots, + bt->aux[0].offset, cen_aux_specs[0].footer, + &bt->acl_tx_head) || + cen_transmit(bt, &bt->le_txq, 4, CEN_LE_SLOTS, CEN_LE_TX, + CEN_LE_FOOTER, &bt->le_tx_head))) + goto protocol_error; + schedule_delayed_work(&bt->poll, msecs_to_jiffies(1)); + goto unlock; +protocol_error: + bt->running = false; + hdev->stat.err_rx++; + dev_err(&bt->pdev->dev, "Beta transport stopped (exec=%u ipc=%u); polling disabled\n", + readl(bt->bar + 0x18), readl(bt->bar)); +unlock: + mutex_unlock(&bt->lock); +} + +static int cen_hci_open(struct hci_dev *hdev) +{ + struct cen_bt *bt = hci_get_drvdata(hdev); + + mutex_lock(&bt->lock); + bt->running = true; + schedule_delayed_work(&bt->poll, 0); + mutex_unlock(&bt->lock); + return 0; +} + +static int cen_hci_close(struct hci_dev *hdev) +{ + struct cen_bt *bt = hci_get_drvdata(hdev); + + mutex_lock(&bt->lock); + bt->running = false; + bt->hrb_status_pending = false; + mutex_unlock(&bt->lock); + cancel_delayed_work_sync(&bt->poll); + skb_queue_purge(&bt->txq); + skb_queue_purge(&bt->acl_txq); + skb_queue_purge(&bt->le_txq); + return 0; +} + +static int cen_vendor_command(struct hci_dev *hdev, u16 opcode, + const void *data, u8 length) +{ + struct sk_buff *skb; + int ret; + + skb = __hci_cmd_sync(hdev, opcode, length, data, HCI_INIT_TIMEOUT); + if (IS_ERR(skb)) + return PTR_ERR(skb); + ret = skb->len < 1 ? -EPROTO : -bt_to_errno(skb->data[0]); + kfree_skb(skb); + return ret; +} + +static int cen_hci_post_init(struct hci_dev *hdev) +{ + struct cen_bt *bt = hci_get_drvdata(hdev); + struct sk_buff *skb; + u32 features; + int ret; + + if (!native_hrb) + return 0; + WRITE_ONCE(bt->hrb_supported, false); + skb = __hci_cmd_sync(hdev, CEN_OP_READ_PROP_FEATURES, 0, NULL, + HCI_INIT_TIMEOUT); + if (IS_ERR(skb)) + return PTR_ERR(skb); + ret = skb->len == 5 ? -bt_to_errno(skb->data[0]) : -EPROTO; + features = skb->len == 5 ? get_unaligned_le32(skb->data + 1) : 0; + kfree_skb(skb); + WRITE_ONCE(bt->proprietary_features, features); + if (ret) + return ret; + if (!(features & CEN_PROP_HRB_CREATE)) + return -EOPNOTSUPP; + WRITE_ONCE(bt->hrb_supported, true); + bt_dev_info(hdev, "Native Proxima Create Connection capability confirmed"); + return 0; +} + +static int cen_hci_setup(struct hci_dev *hdev) +{ + struct cen_bt *bt = hci_get_drvdata(hdev); + u8 address[6], init_done[16], seed[24]; + int ret; + + if (!native_startup) + return 0; + + /* Native PROXIMA initialization starts with these three commands, then + * HCI Reset. The core calls setup before its ordinary reset/init sequence. + * Address provisioning is still diagnostic: use a temporary locally + * administered address, in native WriteBdAddr display/network byte order. + */ + ret = get_random_bytes_wait(address, sizeof(address)); + if (ret) + return ret; + address[0] = (address[0] & 0xfc) | 0x02; + get_random_bytes(seed, sizeof(seed)); + memset(init_done, 0xff, sizeof(init_done)); + ret = cen_vendor_command(hdev, 0xfc06, address, sizeof(address)); + if (ret) + goto out; + WRITE_ONCE(bt->native_startup_steps, 1); + ret = cen_vendor_command(hdev, 0xff0b, init_done, sizeof(init_done)); + if (ret) + goto out; + WRITE_ONCE(bt->native_startup_steps, 2); + ret = cen_vendor_command(hdev, 0xfe44, seed, sizeof(seed)); + if (!ret) + WRITE_ONCE(bt->native_startup_steps, 3); +out: + memzero_explicit(seed, sizeof(seed)); + if (ret) + bt_dev_err(hdev, "Native startup stopped after %u commands: %d", + bt->native_startup_steps, ret); + else + bt_dev_info(hdev, "Native startup accepted; temporary diagnostic address in use"); + return ret; +} + +static int cen_hci_flush(struct hci_dev *hdev) +{ + struct cen_bt *bt = hci_get_drvdata(hdev); + + skb_queue_purge(&bt->txq); + skb_queue_purge(&bt->acl_txq); + skb_queue_purge(&bt->le_txq); + return 0; +} + +static int cen_hci_send(struct hci_dev *hdev, struct sk_buff *skb) +{ + struct cen_bt *bt = hci_get_drvdata(hdev); + struct sk_buff_head *queue = &bt->txq; + struct hci_conn *conn; + u16 handle; + u8 link_type; + int ret = 0; + + if (hci_skb_pkt_type(skb) == HCI_ACLDATA_PKT && enable_acl) { + if (skb->len < HCI_ACL_HDR_SIZE || + skb->len != HCI_ACL_HDR_SIZE + get_unaligned_le16(skb->data + 2)) { + ret = -EMSGSIZE; + goto out; + } + handle = get_unaligned_le16(skb->data); + rcu_read_lock(); + conn = hci_conn_hash_lookup_handle(hdev, hci_handle(handle)); + link_type = conn ? conn->type : 0xff; + rcu_read_unlock(); + if (link_type == LE_LINK && (handle & 0x3000) != 0x3000) { + queue = &bt->le_txq; + if (skb->len > CEN_LE_FOOTER) + ret = -EMSGSIZE; + } else if (link_type == ACL_LINK) { + queue = &bt->acl_txq; + if (skb->len > cen_aux_specs[0].footer) + ret = -EMSGSIZE; + } else { + ret = -EOPNOTSUPP; + } + if (ret) + goto out; + } else if (hci_skb_pkt_type(skb) != HCI_COMMAND_PKT) { + ret = -EOPNOTSUPP; + goto out; + } else if (skb->len < HCI_COMMAND_HDR_SIZE || skb->len > CEN_FOOTER || + skb->len != HCI_COMMAND_HDR_SIZE + skb->data[2]) { + ret = -EMSGSIZE; + goto out; + } + if (hci_skb_pkt_type(skb) == HCI_COMMAND_PKT && native_hrb) { + u16 opcode = get_unaligned_le16(skb->data); + + /* Do not mix unmanaged native commands with translated requests. */ + if (opcode == CEN_OP_HRB_CREATE_CONN) { + ret = -EOPNOTSUPP; + goto out; + } + if (opcode == HCI_OP_CREATE_CONN && + skb->len != HCI_COMMAND_HDR_SIZE + sizeof(struct hci_cp_create_conn)) { + ret = -EMSGSIZE; + goto out; + } + } + mutex_lock(&bt->lock); + if (!bt->running) + ret = -ENETDOWN; + else if (skb_queue_len(queue) >= 64) + ret = -ENOBUFS; + else { + skb_queue_tail(queue, skb); + mod_delayed_work(system_wq, &bt->poll, 0); + } + mutex_unlock(&bt->lock); + if (!ret) + return 0; +out: + /* hci_send_frame() frees rejected packets. Ownership transfers to this + * driver only when the skb is queued and send returns success. + */ + return ret; +} + +static void cen_stop(struct cen_bt *bt) +{ + pci_clear_master(bt->pdev); + kvfree(bt->capture); + bt->capture = NULL; + /* IPC control 0 caused a firmware abort in the initial hardware probe. + * Until coordinated firmware teardown is understood, retain any published + * arena after detaching. A fresh firmware boot is required before rebinding. + */ + if (bt->ipc_requested || !pci_wait_for_pending_transaction(bt->pdev)) + dev_warn(&bt->pdev->dev, "Beta detach requires fresh firmware boot; retaining DMA arena\n"); + else + dma_free_coherent(&bt->pdev->dev, CEN_ARENA_SIZE, bt->arena, bt->dma); +} + +static int cen_status_show(struct seq_file *s, void *unused) +{ + struct cen_bt *bt = s->private; + unsigned int i; + + mutex_lock(&bt->lock); + seq_printf(s, "exec=%u ipc=%u running=%u tx_head=%u tx_consumed=%u rx_head=%u rx_consumed=%u queued=%u\n", + readl(bt->bar + 0x18), readl(bt->bar), bt->running, + bt->tx_head, cen_tail(bt, 1), bt->rx_head, cen_tail(bt, 7), + skb_queue_len(&bt->txq)); + seq_printf(s, "commands=%u events=%u tx_bytes=%u rx_bytes=%u tx_errors=%u rx_errors=%u\n", + bt->hdev->stat.cmd_tx, bt->hdev->stat.evt_rx, + bt->hdev->stat.byte_tx, bt->hdev->stat.byte_rx, + bt->hdev->stat.err_tx, bt->hdev->stat.err_rx); + seq_printf(s, "le_rings=%u le_rx_packets=%u\n", arm_le_data_rings, bt->le_rx_packets); + seq_printf(s, "native_startup=%u native_startup_steps=%u\n", + native_startup, READ_ONCE(bt->native_startup_steps)); + seq_printf(s, "native_hrb=%u hrb_supported=%u proprietary_features=0x%08x hrb_commands=%u hrb_status_events=%u hrb_pending=%u\n", + native_hrb, READ_ONCE(bt->hrb_supported), + READ_ONCE(bt->proprietary_features), bt->hrb_commands, + bt->hrb_status_events, bt->hrb_status_pending); + seq_printf(s, "hrb_connection_events=%u hrb_last_link_type=0x%02x\n", + bt->hrb_connection_events, bt->hrb_last_link_type); + seq_printf(s, "acl_enabled=%u acl_tx=%u acl_rx=%u acl_queued=%u le_queued=%u\n", + enable_acl, bt->hdev->stat.acl_tx, bt->hdev->stat.acl_rx, + skb_queue_len(&bt->acl_txq), skb_queue_len(&bt->le_txq)); + seq_printf(s, "auxiliary_rings=%u messages=%u message_head=%u capture_bytes=%u capture_records=%u capture_dropped=%u\n", + arm_auxiliary_rings, bt->message_tag, bt->message_head, + bt->capture_used, bt->capture_records, bt->capture_dropped); + if (arm_auxiliary_rings) + for (i = 0; i < ARRAY_SIZE(cen_aux_specs); i++) + if (cen_aux_specs[i].receive) + seq_printf(s, "aux_rx ring=%u packets=%u consumed=%u device_tail=%u\n", + cen_aux_specs[i].id, bt->aux[i].packets, + bt->aux[i].head, cen_tail(bt, cen_aux_specs[i].id)); + mutex_unlock(&bt->lock); + return 0; +} +DEFINE_SHOW_ATTRIBUTE(cen_status); + +static ssize_t cen_capture_read(struct file *file, char __user *buffer, + size_t count, loff_t *pos) +{ + struct cen_bt *bt = file->private_data; + ssize_t ret; + + mutex_lock(&bt->lock); + ret = simple_read_from_buffer(buffer, count, pos, bt->capture, bt->capture_used); + mutex_unlock(&bt->lock); + return ret; +} + +static const struct file_operations cen_capture_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = cen_capture_read, + .llseek = default_llseek, +}; + +static ssize_t cen_capture_reset(struct file *file, const char __user *buffer, + size_t count, loff_t *pos) +{ + struct cen_bt *bt = file->private_data; + unsigned int value; + int ret = kstrtouint_from_user(buffer, count, 0, &value); + + if (ret || value != 1) + return ret ?: -EINVAL; + mutex_lock(&bt->lock); + bt->capture_used = 0; + bt->capture_records = 0; + bt->capture_dropped = 0; + mutex_unlock(&bt->lock); + return count; +} + +static const struct file_operations cen_capture_reset_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .write = cen_capture_reset, +}; + +static int cen_probe(struct pci_dev *pdev, const struct pci_device_id *id) +{ + struct cen_bt *bt; + struct hci_dev *hdev; + int ret; + + if (!of_machine_is_compatible("apple,j813") || PCI_FUNC(pdev->devfn) != 2) + return -ENODEV; + bt = devm_kzalloc(&pdev->dev, sizeof(*bt), GFP_KERNEL); + if (!bt) + return -ENOMEM; + bt->pdev = pdev; + mutex_init(&bt->lock); + skb_queue_head_init(&bt->txq); + skb_queue_head_init(&bt->acl_txq); + skb_queue_head_init(&bt->le_txq); + if (enable_acl && (!arm_le_data_rings || !arm_auxiliary_rings)) + return -EINVAL; + if (native_hrb && (!native_startup || !enable_acl)) + return -EINVAL; + INIT_DELAYED_WORK(&bt->poll, cen_poll); + ret = pcim_enable_device(pdev); + if (ret) + return ret; + if (pci_resource_len(pdev, 0) != SZ_64K) + return -ENODEV; + ret = pcim_iomap_regions(pdev, BIT(0), "btcentauri"); + if (ret) + return ret; + bt->bar = pcim_iomap_table(pdev)[0]; + if (readl(bt->bar + 0x18) != 2 || readl(bt->bar) != 0) + return dev_err_probe(&pdev->dev, -EBUSY, + "Beta requires running firmware and unowned IPC (exec=%u ipc=%u)\n", + readl(bt->bar + 0x18), readl(bt->bar)); + ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(40)); + if (ret) + return ret; + bt->arena = dma_alloc_coherent(&pdev->dev, CEN_ARENA_SIZE, &bt->dma, GFP_KERNEL); + if (!bt->arena) + return -ENOMEM; + memset(bt->arena, 0, CEN_ARENA_SIZE); + if (arm_auxiliary_rings) { + bt->capture = kvzalloc(CEN_CAPTURE_SIZE, GFP_KERNEL); + if (!bt->capture) { + ret = -ENOMEM; + goto stop; + } + } + ret = cen_start(bt); + if (ret) { + dev_err(&pdev->dev, "Beta IPC initialization failed: %d\n", ret); + goto stop; + } + hdev = hci_alloc_dev(); + if (!hdev) { + ret = -ENOMEM; + goto stop; + } + bt->hdev = hdev; + hdev->bus = HCI_PCI; + hdev->open = cen_hci_open; + hdev->close = cen_hci_close; + hdev->flush = cen_hci_flush; + hdev->send = cen_hci_send; + hdev->setup = cen_hci_setup; + hdev->post_init = cen_hci_post_init; + SET_HCIDEV_DEV(hdev, &pdev->dev); + hci_set_drvdata(hdev, bt); + pci_set_drvdata(pdev, bt); + ret = hci_register_dev(hdev); + if (ret) { + hci_free_dev(hdev); + goto stop; + } + bt->debug = debugfs_create_dir("btcentauri", NULL); + debugfs_create_file("status", 0400, bt->debug, bt, &cen_status_fops); + if (bt->capture) { + debugfs_create_file("auxiliary_capture", 0400, bt->debug, bt, &cen_capture_fops); + debugfs_create_file("capture_reset", 0200, bt->debug, bt, &cen_capture_reset_fops); + } + dev_info(&pdev->dev, "Beta HCI transport registered; ACL enabled=%u, SCO/ISO unsupported\n", + enable_acl); + return 0; +stop: + cen_stop(bt); + return ret; +} + +static void cen_remove(struct pci_dev *pdev) +{ + struct cen_bt *bt = pci_get_drvdata(pdev); + + debugfs_remove_recursive(bt->debug); + hci_unregister_dev(bt->hdev); + cen_hci_close(bt->hdev); + hci_free_dev(bt->hdev); + cen_stop(bt); +} + +static const struct pci_device_id cen_ids[] = { + { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1903) }, + { } +}; +MODULE_DEVICE_TABLE(pci, cen_ids); + +static struct pci_driver cen_driver = { + .name = "btcentauri", + .id_table = cen_ids, + .probe = cen_probe, + .remove = cen_remove, +}; +module_pci_driver(cen_driver); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Apple N1 Beta HCI command/event transport for J813 bring-up"); diff --git a/drivers/net/wireless/apple/centauri/centauri_ipc.c b/drivers/net/wireless/apple/centauri/centauri_ipc.c index 6eec46d831888..0db35959d114d 100644 --- a/drivers/net/wireless/apple/centauri/centauri_ipc.c +++ b/drivers/net/wireless/apple/centauri/centauri_ipc.c @@ -3,10 +3,12 @@ * Wire layout source: AppleCentauriCommon/scripts/alpha_wifi_scan.py v259. * Polling and bounded bootstrap rings are deliberate bring-up restrictions. */ +#include #include #include #include #include +#include #include #include #include @@ -79,6 +81,40 @@ static int ftab_entry(const void *image, size_t size, const char tag[4], return -ENOENT; } +/* The native secondary FTAB reserves a bounded "cras" region. Capture it + * after a firmware abort; these private diagnostics may contain device data. + */ +static ssize_t firmware_crash_read(struct file *file, char __user *buffer, + size_t count, loff_t *pos) +{ + struct centauri *c = file->private_data; + struct cen_buffer *secondary; + u32 offset, length; + ssize_t ret; + + mutex_lock(&c->boot_lock); + if (!c->ipc || !c->ipc->secondary) { + ret = -ENODEV; + goto unlock; + } + secondary = c->ipc->secondary; + ret = ftab_entry(secondary->cpu, secondary->size, "cras", &offset, &length, false); + if (ret) + goto unlock; + dma_rmb(); + ret = simple_read_from_buffer(buffer, count, pos, secondary->cpu + offset, length); +unlock: + mutex_unlock(&c->boot_lock); + return ret; +} + +static const struct file_operations firmware_crash_fops = { + .owner = THIS_MODULE, + .open = simple_open, + .read = firmware_crash_read, + .llseek = default_llseek, +}; + static int secondary_init(struct centauri *c) { u32 offset, length, count, i, required; @@ -112,6 +148,7 @@ static int secondary_init(struct centauri *c) pci_write_config_dword(c->pdev, 0xf8c, upper_32_bits(c->ipc->secondary->dma)); pci_write_config_dword(c->pdev, 0xf90, required); dev_info(&c->pdev->dev, "secondary memory: seed=%u required=%u\n", length, required); + debugfs_create_file("firmware_crash", 0400, c->debug, c, &firmware_crash_fops); return 0; } From 170202e684b11cfefdbe3f8d6d8d9ea4968bd367 Mon Sep 17 00:00:00 2001 From: Acelogic Date: Thu, 10 Sep 2026 02:19:42 -0400 Subject: [PATCH 03/22] arm64: add J813 RAM desktop bring-up and retained N1 test identity [Thomas: the driver part of 5166f70674ec on the J813 line of linux-aurora] Signed-off-by: Thomas Glanzmann --- drivers/bluetooth/btcentauri.c | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/drivers/bluetooth/btcentauri.c b/drivers/bluetooth/btcentauri.c index d2539539fa162..fb5c9ca89f6e0 100644 --- a/drivers/bluetooth/btcentauri.c +++ b/drivers/bluetooth/btcentauri.c @@ -9,6 +9,8 @@ */ #include #include +#include +#include #include #include #include @@ -55,6 +57,10 @@ static bool native_startup; module_param(native_startup, bool, 0444); MODULE_PARM_DESC(native_startup, "Diagnostic: program a temporary address, InitDone and random seed before the first HCI reset"); +static char *native_bdaddr; +module_param(native_bdaddr, charp, 0400); +MODULE_PARM_DESC(native_bdaddr, "Optional stable diagnostic address in XX:XX:XX:XX:XX:XX order; does not provision the factory address"); + static bool enable_acl; module_param(enable_acl, bool, 0444); MODULE_PARM_DESC(enable_acl, "Experimental ACL data delivery; requires LE and auxiliary rings"); @@ -657,14 +663,20 @@ static int cen_hci_setup(struct hci_dev *hdev) /* Native PROXIMA initialization starts with these three commands, then * HCI Reset. The core calls setup before its ordinary reset/init sequence. - * Address provisioning is still diagnostic: use a temporary locally - * administered address, in native WriteBdAddr display/network byte order. + * Address provisioning is still diagnostic. A saved test identity permits + * bond reuse; otherwise generate a temporary locally administered address. + * Native WriteBdAddr takes display/network byte order in either case. */ - ret = get_random_bytes_wait(address, sizeof(address)); + if (native_bdaddr && (!mac_pton(native_bdaddr, address) || + is_zero_ether_addr(address) || is_broadcast_ether_addr(address))) + return -EINVAL; + ret = get_random_bytes_wait(seed, sizeof(seed)); if (ret) return ret; - address[0] = (address[0] & 0xfc) | 0x02; - get_random_bytes(seed, sizeof(seed)); + if (!native_bdaddr) { + get_random_bytes(address, sizeof(address)); + address[0] = (address[0] & 0xfc) | 0x02; + } memset(init_done, 0xff, sizeof(init_done)); ret = cen_vendor_command(hdev, 0xfc06, address, sizeof(address)); if (ret) @@ -683,7 +695,8 @@ static int cen_hci_setup(struct hci_dev *hdev) bt_dev_err(hdev, "Native startup stopped after %u commands: %d", bt->native_startup_steps, ret); else - bt_dev_info(hdev, "Native startup accepted; temporary diagnostic address in use"); + bt_dev_info(hdev, "Native startup accepted; %s diagnostic address in use", + native_bdaddr ? "configured" : "temporary"); return ret; } From f5e9554d7e649b223cdd15cc6c65f55f9bededf8 Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:14:39 -0500 Subject: [PATCH 04/22] centauri: add the J873 to the per-machine board table The J813 constants of the port cycle and the ROM boot word were hard-coded. Move them into one table with a row per machine that has an N1 and add the J873 (the M6 Mac mini): firmware file name, the word written to BAR+0x807c, the root port device ID, and the port, PHY and PERST# addresses with the one differing config +0x130 value, all taken from the machine's ADT. Measured on an M6 Mac mini with the rest of this series: the ROM stage, the port cycle and the Control firmware reach their running stage. Needs the T8152 SoC and PCIe nodes (base layer). [Thomas: the driver part of 70d215465d13 on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- drivers/bluetooth/btcentauri.c | 5 +-- .../net/wireless/apple/centauri/centauri.h | 36 ++++++++++++++++++- .../wireless/apple/centauri/centauri_pci.c | 17 ++++++--- .../wireless/apple/centauri/centauri_port.c | 28 ++++++++------- 4 files changed, 66 insertions(+), 20 deletions(-) diff --git a/drivers/bluetooth/btcentauri.c b/drivers/bluetooth/btcentauri.c index fb5c9ca89f6e0..5b2367dc0729f 100644 --- a/drivers/bluetooth/btcentauri.c +++ b/drivers/bluetooth/btcentauri.c @@ -887,7 +887,8 @@ static int cen_probe(struct pci_dev *pdev, const struct pci_device_id *id) struct hci_dev *hdev; int ret; - if (!of_machine_is_compatible("apple,j813") || PCI_FUNC(pdev->devfn) != 2) + if (!(of_machine_is_compatible("apple,j813") || of_machine_is_compatible("apple,j873")) || + PCI_FUNC(pdev->devfn) != 2) return -ENODEV; bt = devm_kzalloc(&pdev->dev, sizeof(*bt), GFP_KERNEL); if (!bt) @@ -994,4 +995,4 @@ static struct pci_driver cen_driver = { }; module_pci_driver(cen_driver); MODULE_LICENSE("GPL"); -MODULE_DESCRIPTION("Apple N1 Beta HCI command/event transport for J813 bring-up"); +MODULE_DESCRIPTION("Apple N1 Beta HCI command/event transport for J813 and J873 bring-up"); diff --git a/drivers/net/wireless/apple/centauri/centauri.h b/drivers/net/wireless/apple/centauri/centauri.h index f7fdf4105a3b3..6a67ec1db50fd 100644 --- a/drivers/net/wireless/apple/centauri/centauri.h +++ b/drivers/net/wireless/apple/centauri/centauri.h @@ -3,6 +3,7 @@ #define AURORA_CENTAURI_H #include #include +#include #include #include @@ -36,7 +37,40 @@ struct centauri { struct centauri_wifi *wifi; }; -int centauri_j813_port_cycle(struct pci_dev *pdev); +/* One row per machine with an N1: the FTAB file name, the word written to BAR+0x807c, + * (board-id << 16) | chip-id from the machine's ADT /chosen, and the physical addresses and + * the one differing register value of the port cycle (centauri_port.c). + * J813: board 0x24, chip 0x8142 (measured); port, PHY lane and PERST pad from the live ADT. + * J873 (Mac mini M6): board 0x24, chip 0x8152 from its ADT, the composition taken from the J813 + * word; port 0 config 0x388028000, lane 0x38f010000 and PERST# gpio0 pin 3 (0x31a000000 + 4 * 3) + * from its ADT; config +0x130 is 0x208 as in the m1n1 T8152 bring-up that links the port. + */ +struct centauri_board { + const char *compatible; + const char *firmware; + u32 board_word; + u64 port, phy, pad; + u32 tune_130; + u16 bridge_id; /* root port device ID, 0 = any Apple root port */ +}; + +static inline const struct centauri_board *centauri_board(void) +{ + static const struct centauri_board boards[] = { + { "apple,j813", "apple/centauri/j813-ftab.bin", 0x00248142, + 0x488028000ULL, 0x48f010000ULL, 0x39a000300ULL, 0x03020000, 0x100c }, + { "apple,j873", "apple/centauri/j873-ftab.bin", 0x00248152, + 0x388028000ULL, 0x38f010000ULL, 0x31a00000cULL, 0x208, 0 }, + }; + int i; + + for (i = 0; i < ARRAY_SIZE(boards); i++) + if (of_machine_is_compatible(boards[i].compatible)) + return &boards[i]; + return NULL; +} + +int centauri_port_cycle(struct pci_dev *pdev); int centauri_start_ipc(struct centauri *c); void centauri_free_ipc(struct centauri *c); int centauri_alpha_resources(struct centauri *c, struct cen_alpha_resources *resources); diff --git a/drivers/net/wireless/apple/centauri/centauri_pci.c b/drivers/net/wireless/apple/centauri/centauri_pci.c index f3f7c6413f0a3..d2b783a0b335b 100644 --- a/drivers/net/wireless/apple/centauri/centauri_pci.c +++ b/drivers/net/wireless/apple/centauri/centauri_pci.c @@ -20,6 +20,10 @@ static struct dentry *centauri_debug; +static unsigned int board_word; +module_param(board_word, uint, 0644); +MODULE_PARM_DESC(board_word, "Diagnostic: word written to BAR0+0x807c before the ROM boot, 0 = per-machine table value"); + static int centauri_status_show(struct seq_file *s, void *unused) { struct centauri *c = s->private; @@ -64,19 +68,19 @@ static void centauri_free_boot(struct centauri *c) static int centauri_rom_boot(struct centauri *c) { const struct firmware *fw; + const struct centauri_board *board = centauri_board(); struct device *dev = &c->pdev->dev; u32 stage; int ret; - if ((c->pdev->device != 0x1900 && c->pdev->device != 0x1901) || - !of_machine_is_compatible("apple,j813")) + if ((c->pdev->device != 0x1900 && c->pdev->device != 0x1901) || !board) return -ENODEV; if (!c->link_ready || c->boot_attempted) return -EBUSY; stage = readl(c->bar + 0x8000); if (stage != 0 && stage != 4 && stage != 1) return -EBUSY; - ret = request_firmware(&fw, "apple/centauri/j813-ftab.bin", dev); + ret = request_firmware(&fw, board->firmware, dev); if (ret) return ret; if (fw->size < 0x30 || fw->size > SZ_64M || @@ -116,7 +120,9 @@ static int centauri_rom_boot(struct centauri *c) pci_write_config_word(c->pdev, c->msi_cap + PCI_MSI_FLAGS, (c->msi_original & ~PCI_MSI_FLAGS_QSIZE) | PCI_MSI_FLAGS_ENABLE); pci_set_master(c->pdev); - writel(0x00248142, c->bar + 0x807c); /* J813 board 0x24, T8142 chip. */ + dev_info(dev, "ROM boot: %s, %zu bytes, board word %#x\n", board->firmware, fw->size, + board_word ?: board->board_word); + writel(board_word ?: board->board_word, c->bar + 0x807c); writel(0, c->bar + 0x803c); writel(lower_32_bits(c->descriptor_dma), c->bar + 0x8044); writel(upper_32_bits(c->descriptor_dma), c->bar + 0x8048); @@ -168,7 +174,7 @@ static ssize_t centauri_boot_write(struct file *file, const char __user *buffer, ret = -EBUSY; } else { c->link_ready = false; - ret = centauri_j813_port_cycle(c->pdev); + ret = centauri_port_cycle(c->pdev); if (!ret) { c->link_ready = true; dev_info(&c->pdev->dev, "post-reset execution stage=%#x\n", @@ -276,3 +282,4 @@ module_exit(centauri_exit); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Apple N1 Centauri native PCI and ACIPC bring-up backend"); MODULE_FIRMWARE("apple/centauri/j813-ftab.bin"); +MODULE_FIRMWARE("apple/centauri/j873-ftab.bin"); diff --git a/drivers/net/wireless/apple/centauri/centauri_port.c b/drivers/net/wireless/apple/centauri/centauri_port.c index eea0a1ba11e0e..5b2b1f0bbb977 100644 --- a/drivers/net/wireless/apple/centauri/centauri_port.c +++ b/drivers/net/wireless/apple/centauri/centauri_port.c @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-only -/* J813 preboot -> boot port cycle, transcribed from the measured proxy +/* Preboot -> boot port cycle, transcribed from the J813 measured proxy * alpha_wifi_scan.py pcie_cycle/apple_teardown/apple_bringup sequence. - * These apertures were verified from the live ADT before this Linux boot. - * This temporary board helper belongs in the host-controller driver once - * T8142 platform/clock/GPIO bindings are available. + * Apertures and the PERST pad come from the per-machine table in centauri.h; those were + * verified from the live ADT. The J873 row reuses the J813 sequence with T8152 addresses and + * config +0x130 = 0x208: the reset table and the lane window bits equal the T8152 bring-up that + * links port 0 in m1n1, but the cycle itself (link down, retrain with firmware running) has not + * been run on a J873. This temporary board helper belongs in the host-controller driver. */ #include #include @@ -24,7 +26,7 @@ static bool wait_bits(void __iomem *address, u32 mask, u32 wanted, u32 us) return !readl_poll_timeout(address, value, (value & mask) == wanted, 100, us); } -int centauri_j813_port_cycle(struct pci_dev *pdev) +int centauri_port_cycle(struct pci_dev *pdev) { static const u32 reset[][2] = { {0x88, 0x110}, {0x100, ~0U}, {0x148, ~0U}, {0x210, ~0U}, @@ -32,10 +34,11 @@ int centauri_j813_port_cycle(struct pci_dev *pdev) {0x16c, 0}, {0x13c, 0}, {0x800, 0x00100100}, {0x808, 0x001001ff}, {0x82c, 0x00010000}, }; - static const u32 tune[][2] = { - {0x130, 0x03020000}, {0x140, 0x10}, {0x144, 0x00253770}, + u32 tune[][2] = { + {0x130, 0}, {0x140, 0x10}, {0x144, 0x00253770}, {0x21c, 0}, {0x834, 0}, {0x83c, 0}, }; + const struct centauri_board *board = centauri_board(); struct pci_dev *bridge = pdev->bus->self; void __iomem *port, *phy, *pad; u32 value, gpio, identity; @@ -44,12 +47,13 @@ int centauri_j813_port_cycle(struct pci_dev *pdev) int ret = -ETIMEDOUT, pm; bool ack, l2; - if (!of_machine_is_compatible("apple,j813") || !bridge || - bridge->vendor != PCI_VENDOR_ID_APPLE || bridge->device != 0x100c) + if (!board || !board->port || !bridge || bridge->vendor != PCI_VENDOR_ID_APPLE || + (board->bridge_id && bridge->device != board->bridge_id)) return -ENODEV; - port = ioremap(0x488028000ULL, 0x4000); - phy = ioremap(0x48f010000ULL, 0x1000); - pad = ioremap(0x39a000300ULL, sizeof(u32)); + tune[0][1] = board->tune_130; + port = ioremap(board->port, 0x4000); + phy = ioremap(board->phy, 0x1000); + pad = ioremap(board->pad, sizeof(u32)); if (!port || !phy || !pad) { ret = -ENOMEM; goto out; From 04c448986466a5822a36eb9b6b40cc18ce2b31ea Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:14:39 -0500 Subject: [PATCH 05/22] centauri: write the RID2SID and MSIMAP tables back after the port cycle The J813 port cycle zeroes 16 words at port+0x3000 and 256 words at port+0x3800. On the J873 those are the RID2SID and MSIMAP tables that pcie-apple programmed. The cycle then drops the stream entry of function 0 and its DMA arrives as SID 0 (DART "BPF reject"). A board with keep_port_tables set gets both tables written back after the cycle. Measured on an M6 Mac mini: with this change the Control firmware reaches its OS stage after the cycle. [Thomas: the driver part of fcbcd1529d03 on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- .../net/wireless/apple/centauri/centauri.h | 9 +++++++-- .../wireless/apple/centauri/centauri_port.c | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/drivers/net/wireless/apple/centauri/centauri.h b/drivers/net/wireless/apple/centauri/centauri.h index 6a67ec1db50fd..9348c00984ccc 100644 --- a/drivers/net/wireless/apple/centauri/centauri.h +++ b/drivers/net/wireless/apple/centauri/centauri.h @@ -44,6 +44,10 @@ struct centauri { * J873 (Mac mini M6): board 0x24, chip 0x8152 from its ADT, the composition taken from the J813 * word; port 0 config 0x388028000, lane 0x38f010000 and PERST# gpio0 pin 3 (0x31a000000 + 4 * 3) * from its ADT; config +0x130 is 0x208 as in the m1n1 T8152 bring-up that links the port. + * keep_port_tables: the J813 sequence zeroes the 16 words at port+0x3000 and the 256 words at + * port+0x3800. On the J873 those are the RID2SID and MSIMAP tables that pcie-apple programmed + * (PORT_T602X_RID2SID, PORT_T602X_MSIMAP); the cycle then drops function 0's stream entry and + * its DMA arrives as SID 0 (DART "BPF reject"). Such a board gets both tables written back. */ struct centauri_board { const char *compatible; @@ -52,15 +56,16 @@ struct centauri_board { u64 port, phy, pad; u32 tune_130; u16 bridge_id; /* root port device ID, 0 = any Apple root port */ + bool keep_port_tables; }; static inline const struct centauri_board *centauri_board(void) { static const struct centauri_board boards[] = { { "apple,j813", "apple/centauri/j813-ftab.bin", 0x00248142, - 0x488028000ULL, 0x48f010000ULL, 0x39a000300ULL, 0x03020000, 0x100c }, + 0x488028000ULL, 0x48f010000ULL, 0x39a000300ULL, 0x03020000, 0x100c, false }, { "apple,j873", "apple/centauri/j873-ftab.bin", 0x00248152, - 0x388028000ULL, 0x38f010000ULL, 0x31a00000cULL, 0x208, 0 }, + 0x388028000ULL, 0x38f010000ULL, 0x31a00000cULL, 0x208, 0, true }, }; int i; diff --git a/drivers/net/wireless/apple/centauri/centauri_port.c b/drivers/net/wireless/apple/centauri/centauri_port.c index 5b2b1f0bbb977..62a9456e74ea9 100644 --- a/drivers/net/wireless/apple/centauri/centauri_port.c +++ b/drivers/net/wireless/apple/centauri/centauri_port.c @@ -46,6 +46,8 @@ int centauri_port_cycle(struct pci_dev *pdev) unsigned int lanes, i; int ret = -ETIMEDOUT, pm; bool ack, l2; + u32 rid2sid[16], msimap[256], valid = 0; + bool saved = false; if (!board || !board->port || !bridge || bridge->vendor != PCI_VENDOR_ID_APPLE || (board->bridge_id && bridge->device != board->bridge_id)) @@ -69,6 +71,13 @@ int centauri_port_cycle(struct pci_dev *pdev) ret = pci_save_state(pdev); if (ret) goto out; + if (board->keep_port_tables) { + for (i = 0; i < ARRAY_SIZE(rid2sid); i++) + rid2sid[i] = readl(port + 0x3000 + 4 * i); + for (i = 0; i < ARRAY_SIZE(msimap); i++) + msimap[i] = readl(port + 0x3800 + 4 * i); + saved = true; + } pcie_capability_read_word(bridge, PCI_EXP_LNKSTA, &link); lanes = (link & PCI_EXP_LNKSTA_NLW) >> PCI_EXP_LNKSTA_NLW_SHIFT; lanes = lanes >= 1 && lanes <= 16 ? BIT(lanes) - 1 : 1; @@ -186,6 +195,16 @@ int centauri_port_cycle(struct pci_dev *pdev) timed_out: ret = -ETIMEDOUT; out: + if (saved) { + for (i = 0; i < ARRAY_SIZE(rid2sid); i++) { + writel(rid2sid[i], port + 0x3000 + 4 * i); + valid += !!(rid2sid[i] & BIT(31)); + } + for (i = 0; i < ARRAY_SIZE(msimap); i++) + writel(msimap[i], port + 0x3800 + 4 * i); + dev_info(&pdev->dev, "restored %u RID2SID entries and the MSI map (ret %d)\n", + valid, ret); + } if (pad) iounmap(pad); if (phy) From 0e86716a900f1b3847f1a3ab259bbcced8b7c885 Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:14:39 -0500 Subject: [PATCH 06/22] centauri: map IPC buffers through the struct device of their function Each function of the N1 has its own IOMMU stream on the J873 (RID2SID entries 1, 2 and 3 and separate DART domains). A buffer that the firmware of a function reads or writes must be mapped through that function's own struct device. Function 0 is Control, function 1 Alpha (Wi-Fi) and function 2 Beta (Bluetooth). Measured on an M6 Mac mini: Control and Alpha complete their IPC handshakes with this change. [Thomas: the driver part of 12a5fd0d8fc0 on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- .../wireless/apple/centauri/centauri_ipc.c | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/drivers/net/wireless/apple/centauri/centauri_ipc.c b/drivers/net/wireless/apple/centauri/centauri_ipc.c index 0db35959d114d..5f88f9a4390c9 100644 --- a/drivers/net/wireless/apple/centauri/centauri_ipc.c +++ b/drivers/net/wireless/apple/centauri/centauri_ipc.c @@ -16,6 +16,7 @@ struct cen_buffer { struct list_head list; + struct device *dev; void *cpu; dma_addr_t dma; size_t size; @@ -34,22 +35,35 @@ struct centauri_ipc { struct cen_context control, alpha; struct cen_buffer *secondary, *window, *cchi_rx, *cchi_tx, *debug_rx; struct cen_buffer *alpha_log; + struct cen_buffer *msi[3]; u16 cchi_head, cchi_tail; }; -static struct cen_buffer *buffer_alloc(struct centauri *c, size_t size) +/* Each function has its own IOMMU stream on the J873 (RID2SID entries 1, 2, 3 and separate DART + * domains), so a buffer a function's firmware reads or writes has to be mapped through that + * function's own struct device. Function 0 is the Control firmware, 1 Alpha (Wi-Fi), 2 Beta. + */ +static struct cen_buffer *buffer_alloc(struct centauri *c, unsigned int function, size_t size) { struct cen_buffer *buffer = kzalloc(sizeof(*buffer), GFP_KERNEL); + struct pci_dev *owner = function ? c->ipc->function[function] : c->pdev; if (!buffer) return NULL; + if (!owner) { + kfree(buffer); + return NULL; + } + buffer->dev = &owner->dev; buffer->size = PAGE_ALIGN(size); - buffer->cpu = dma_alloc_coherent(&c->pdev->dev, buffer->size, &buffer->dma, GFP_KERNEL); + buffer->cpu = dma_alloc_coherent(buffer->dev, buffer->size, &buffer->dma, GFP_KERNEL); if (!buffer->cpu) { kfree(buffer); return NULL; } memset(buffer->cpu, 0, buffer->size); + dev_dbg(&c->pdev->dev, "dma buffer fn%u %#llx..%#llx (%zu bytes)\n", function, + (u64)buffer->dma, (u64)buffer->dma + buffer->size, buffer->size); list_add_tail(&buffer->list, &c->ipc->buffers); return buffer; } @@ -139,7 +153,7 @@ static int secondary_init(struct centauri *c) return -EINVAL; required = max_t(u32, required, end); } - c->ipc->secondary = buffer_alloc(c, required); + c->ipc->secondary = buffer_alloc(c, 0, required); if (!c->ipc->secondary) return -ENOMEM; memcpy(c->ipc->secondary->cpu, seed, length); @@ -152,15 +166,15 @@ static int secondary_init(struct centauri *c) return 0; } -static int context_init(struct centauri *c, struct cen_context *ctx, +static int context_init(struct centauri *c, unsigned int function, struct cen_context *ctx, u16 tr, u16 cr, dma_addr_t scratch, size_t scratch_size) { u8 *header; dma_addr_t dma; int i; - ctx->buffer = buffer_alloc(c, SZ_32K); - ctx->messages = buffer_alloc(c, SZ_16K); + ctx->buffer = buffer_alloc(c, function, SZ_32K); + ctx->messages = buffer_alloc(c, function, SZ_16K); if (!ctx->buffer || !ctx->messages) return -ENOMEM; ctx->tr_count = tr; @@ -256,9 +270,9 @@ static int cchi_init(struct centauri *c) u16 tail; int i, ret; - ipc->cchi_rx = buffer_alloc(c, SZ_32K); - ipc->cchi_tx = buffer_alloc(c, SZ_32K); - ipc->debug_rx = buffer_alloc(c, SZ_16K); + ipc->cchi_rx = buffer_alloc(c, 0, SZ_32K); + ipc->cchi_tx = buffer_alloc(c, 0, SZ_32K); + ipc->debug_rx = buffer_alloc(c, 0, SZ_16K); if (!ipc->cchi_rx || !ipc->cchi_tx || !ipc->debug_rx) return -ENOMEM; ret = message(&ipc->control, c->bar + 0x9000, config, sizeof(config)); @@ -311,7 +325,7 @@ static int memswap_handoff(struct centauri *c, unsigned int function, const char ret = ftab_entry(secondary->cpu, secondary->size, tag, &offset, &size, false); if (ret) return ret; - working = buffer_alloc(c, size); + working = buffer_alloc(c, function, size); if (!working) return -ENOMEM; dma_rmb(); @@ -329,6 +343,7 @@ static int enumerate_functions(struct centauri *c) struct centauri_ipc *ipc = c->ipc; u16 control; int i, cap; + dma_addr_t msi; pci_lock_rescan_remove(); /* The ROM personality was single-function; boot exposes Alpha and Beta. */ @@ -352,10 +367,16 @@ static int enumerate_functions(struct centauri *c) pci_read_config_word(ipc->function[i], cap + PCI_MSI_FLAGS, &control); if (!(control & PCI_MSI_FLAGS_64BIT)) return -EOPNOTSUPP; + if (i) { + ipc->msi[i] = buffer_alloc(c, i, SZ_16K); + if (!ipc->msi[i]) + return -ENOMEM; + } + msi = i ? ipc->msi[i]->dma : c->msi_dma; pci_write_config_dword(ipc->function[i], cap + PCI_MSI_ADDRESS_LO, - lower_32_bits(c->msi_dma + 0x400 * i)); + lower_32_bits(msi)); pci_write_config_dword(ipc->function[i], cap + PCI_MSI_ADDRESS_HI, - upper_32_bits(c->msi_dma + 0x400 * i)); + upper_32_bits(msi)); pci_write_config_word(ipc->function[i], cap + PCI_MSI_DATA_64, 0xa5a0 + 0x100 * i); pci_write_config_word(ipc->function[i], cap + PCI_MSI_FLAGS, (control & ~PCI_MSI_FLAGS_QSIZE) | (5 << 4) | PCI_MSI_FLAGS_ENABLE); @@ -383,7 +404,7 @@ int centauri_start_ipc(struct centauri *c) ret = secondary_init(c); if (ret) goto failed; - ret = context_init(c, &ipc->control, 13, 1, 0, 0); + ret = context_init(c, 0, &ipc->control, 13, 1, 0, 0); if (ret) goto failed; writel(lower_32_bits(c->descriptor_dma), c->bar + 0x8044); @@ -417,13 +438,14 @@ int centauri_start_ipc(struct centauri *c) ret = ret ?: -EIO; goto failed; } - ipc->window = buffer_alloc(c, SZ_16M); + ipc->window = buffer_alloc(c, 1, SZ_16M); if (!ipc->window) { ret = -ENOMEM; goto failed; } - ret = context_init(c, &ipc->alpha, 192, 234, - ipc->window->dma + CEN_ALPHA_SCRATCH_OFFSET, CEN_ALPHA_SCRATCH_SIZE); + ret = context_init(c, 1, &ipc->alpha, 192, 234, + ipc->window->dma + CEN_ALPHA_SCRATCH_OFFSET, + CEN_ALPHA_SCRATCH_SIZE); if (ret) goto failed; writel(lower_32_bits(ipc->window->dma), ipc->bar[1] + 0x18); @@ -463,7 +485,7 @@ void centauri_free_ipc(struct centauri *c) pci_clear_master(ipc->function[i]); } list_for_each_entry_safe(buffer, next, &ipc->buffers, list) { - dma_free_coherent(&c->pdev->dev, buffer->size, buffer->cpu, buffer->dma); + dma_free_coherent(buffer->dev, buffer->size, buffer->cpu, buffer->dma); list_del(&buffer->list); kfree(buffer); } @@ -505,7 +527,7 @@ int centauri_alpha_log_buffer(struct centauri *c, void **cpu, dma_addr_t *dma, s if (!ipc || !ipc->window) return -ENODEV; if (!ipc->alpha_log) - ipc->alpha_log = buffer_alloc(c, SZ_4M); + ipc->alpha_log = buffer_alloc(c, 1, SZ_4M); if (!ipc->alpha_log) return -ENOMEM; *cpu = ipc->alpha_log->cpu; From 724d2043da695f051da1c23a4e34aaeb2e852c03 Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:14:39 -0500 Subject: [PATCH 07/22] centauri: leave the Bluetooth function to btcentauri Function 0x1903 is the Beta (Bluetooth) function and btcentauri claims it. apple_centauri needs only the functions 0x1900, 0x1901 and 0x1902, so stop it from claiming 0x1903. Measured on an M6 Mac mini: btcentauri binds 0x1903 and registers its HCI transport. [Thomas: the driver part of 0f318aebe42f on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- drivers/net/wireless/apple/centauri/centauri_pci.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/net/wireless/apple/centauri/centauri_pci.c b/drivers/net/wireless/apple/centauri/centauri_pci.c index d2b783a0b335b..23fff2160a533 100644 --- a/drivers/net/wireless/apple/centauri/centauri_pci.c +++ b/drivers/net/wireless/apple/centauri/centauri_pci.c @@ -245,11 +245,14 @@ static void centauri_remove(struct pci_dev *pdev) centauri_free_boot(c); } +/* 0x1903 is the Beta (Bluetooth) function: btcentauri claims it. This driver needs functions + * 0 and 1 (1 is enabled here, its BAR is used by the Wi-Fi code) and reaches function 2 only + * through config space and its struct device for DMA. + */ static const struct pci_device_id centauri_ids[] = { { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1900), .driver_data = 0x8000 }, { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1901), .driver_data = 0x8000 }, { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1902), .driver_data = 0x0008 }, - { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1903), .driver_data = 0x0018 }, { } }; MODULE_DEVICE_TABLE(pci, centauri_ids); From 9d673ea9a70ee2913ffe5e9c92a52e4e40f4677b Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:14:39 -0500 Subject: [PATCH 08/22] btcentauri: report N1 legacy scan responses with event type 0x1a The N1 reports legacy scan responses in LE Extended Advertising Reports whose event type has the low byte 0x18 (legacy PDU and scan response, neither connectable nor scannable) and other bits in the high byte (seen: 0x2518, 0x2618, 0x2718; accepted legacy types arrive as 0x2613 and 0x2610). The core accepts only 0x1a and 0x1b for a legacy scan response and drops the others as "Unknown advertising packet type". Report the low byte 0x18 as 0x1a and keep the high byte. Measured on a Mac mini M6 (J873), 15 s LE discovery with btmgmt (LE on, power on, find), native startup and the LE and auxiliary rings armed: - without the change: 72 to 74 reports, 71 distinct addresses, 12 to 13 "Unknown advertising packet type: 0x18" kernel lines per run; - with the change: 89 reports, 83 distinct addresses, 0 such lines; btmon shows the scan responses as event types 0x251a, 0x261a and 0x271a. A first version of this patch compared the whole 16-bit type with 0x0018 and never matched; the high byte has to be ignored. Not measured: device names or scan response data content, connection, pairing, audio. [Thomas: the driver part of d3d8dab9dbf5 on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- drivers/bluetooth/btcentauri.c | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/drivers/bluetooth/btcentauri.c b/drivers/bluetooth/btcentauri.c index 5b2367dc0729f..417b58033d1f9 100644 --- a/drivers/bluetooth/btcentauri.c +++ b/drivers/bluetooth/btcentauri.c @@ -469,6 +469,32 @@ static int cen_transmit(struct cen_bt *bt, struct sk_buff_head *queue, return 0; } +/* The N1 reports legacy scan responses in LE Extended Advertising Reports with an event type whose low + * byte is 0x18 (legacy PDU and scan response, neither connectable nor scannable) and other bits in the + * high byte (seen: 0x2518, 0x2618, 0x2718). Linux accepts only the Core values 0x1a and 0x1b for a + * legacy scan response and drops anything else as "Unknown advertising packet type", so report the + * low byte 0x18 as 0x1a and keep the high byte. + */ +static void cen_normalize_ext_adv(struct sk_buff *skb) +{ + unsigned int offset = HCI_EVENT_HDR_SIZE + 2, reports; + + if (skb->len < offset + 1 || skb->data[0] != HCI_EV_LE_META || + skb->data[2] != HCI_EV_LE_EXT_ADV_REPORT) + return; + reports = skb->data[3]; + while (reports--) { + struct hci_ev_le_ext_adv_info *info; + + if (offset + sizeof(*info) > skb->len) + return; + info = (struct hci_ev_le_ext_adv_info *)(skb->data + offset); + if ((le16_to_cpu(info->type) & 0x00ff) == 0x0018) + info->type = cpu_to_le16((le16_to_cpu(info->type) & 0xff00) | 0x001a); + offset += sizeof(*info) + info->length; + } +} + static void cen_poll(struct work_struct *work) { struct cen_bt *bt = container_of(to_delayed_work(work), struct cen_bt, poll); @@ -552,6 +578,7 @@ static void cen_poll(struct work_struct *work) skb->data[11] = ACL_LINK; bt->hrb_connection_events++; } + cen_normalize_ext_adv(skb); hci_skb_pkt_type(skb) = HCI_EVENT_PKT; hdev->stat.byte_rx += length; hci_recv_frame(hdev, skb); From 34eae388f9517dbb1b3d11d3a55f72c65c29de75 Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:14:39 -0500 Subject: [PATCH 09/22] centauri: use the macOS 27 scan TLVs and link TLV 386 on the J873 The macOS 27.0 WLAN adapter differs from the 26.6.1 one the J813 recipe came from. startInterface carries the three link addresses in TLV 386 (was 385). The scan request (command 0x1000f) carries a channel-list TLV 0x104 and an OCE TLV 0x165 after the fixed 14 bytes. Both are selected per board. Measured on an M6 Mac mini: start and scan commands are accepted. Whether the TLVs are required is untested. [Thomas: the driver part of 4061bf02639e on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- .../net/wireless/apple/centauri/centauri.h | 12 ++++- .../wireless/apple/centauri/centauri_wifi.c | 49 ++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/drivers/net/wireless/apple/centauri/centauri.h b/drivers/net/wireless/apple/centauri/centauri.h index 9348c00984ccc..a6aae0c4c10c1 100644 --- a/drivers/net/wireless/apple/centauri/centauri.h +++ b/drivers/net/wireless/apple/centauri/centauri.h @@ -48,6 +48,10 @@ struct centauri { * port+0x3800. On the J873 those are the RID2SID and MSIMAP tables that pcie-apple programmed * (PORT_T602X_RID2SID, PORT_T602X_MSIMAP); the cycle then drops function 0's stream entry and * its DMA arrives as SID 0 (DART "BPF reject"). Such a board gets both tables written back. + * link_tlv, scan_tlvs: the macOS 27.0 WLAN adapter differs from the 26.6.1 one the J813 recipe + * came from. startInterface carries the three link addresses in TLV 386 (was 385), and the scan + * request (command 0x1000f) carries a channel-list TLV 0x104 and an OCE TLV 0x165 after the fixed + * 14 bytes. A J873 scan without them crashed the UMAC in SCHED_Req_Add. */ struct centauri_board { const char *compatible; @@ -57,15 +61,19 @@ struct centauri_board { u32 tune_130; u16 bridge_id; /* root port device ID, 0 = any Apple root port */ bool keep_port_tables; + u16 link_tlv; + bool scan_tlvs; }; static inline const struct centauri_board *centauri_board(void) { static const struct centauri_board boards[] = { { "apple,j813", "apple/centauri/j813-ftab.bin", 0x00248142, - 0x488028000ULL, 0x48f010000ULL, 0x39a000300ULL, 0x03020000, 0x100c, false }, + 0x488028000ULL, 0x48f010000ULL, 0x39a000300ULL, 0x03020000, 0x100c, false, + 385, false }, { "apple,j873", "apple/centauri/j873-ftab.bin", 0x00248152, - 0x388028000ULL, 0x38f010000ULL, 0x31a00000cULL, 0x208, 0, true }, + 0x388028000ULL, 0x38f010000ULL, 0x31a00000cULL, 0x208, 0, true, + 386, true }, }; int i; diff --git a/drivers/net/wireless/apple/centauri/centauri_wifi.c b/drivers/net/wireless/apple/centauri/centauri_wifi.c index 21201241f49ec..86a119dea0ea9 100644 --- a/drivers/net/wireless/apple/centauri/centauri_wifi.c +++ b/drivers/net/wireless/apple/centauri/centauri_wifi.c @@ -759,13 +759,15 @@ static int radio_start(struct centauri_wifi *wifi) unsigned int ring, slot; int ret; - /* Stock 25G76 startInterface (0x1808e26ec) supplies TLV 385, - * length 18. Firmware 0x5736c0 otherwise generates three unrelated + /* The stock 26.x startInterface supplies TLV 385, length 18; the macOS 27.0 + * adapter uses 386 (board table). Firmware 0x5736c0 otherwise generates three unrelated * random addresses. This diagnostic keeps the sole active link equal * to the netdev identity; it does not enable multi-link association. */ if (station_link_addresses) { - put_unaligned_le16(385, start + 5); + const struct centauri_board *board = centauri_board(); + + put_unaligned_le16(board ? board->link_tlv : 385, start + 5); put_unaligned_le16(3 * ETH_ALEN, start + 7); for (slot = 0; slot < 3; slot++) ether_addr_copy(start + 9 + slot * ETH_ALEN, wifi->netdev->dev_addr); @@ -799,12 +801,47 @@ static int radio_start(struct centauri_wifi *wifi) return 0; } +static void append_tlv(u8 *payload, size_t *size, u16 tag, const void *data, u16 length); + +/* Fixed scan request (command 0x1000f), 14 bytes, chunk number first. */ +static size_t build_scan(struct centauri_wifi *wifi, unsigned int chunk, u8 *buf) +{ + static const u8 fixed[14] = { 1, 0, 0, 0x80, 0, 2, 0, 0x10, 110, 0, 110, 0, 0, 0 }; + const struct centauri_board *board = centauri_board(); + u8 list[1 + 4 * 13], oce[5] = { 0, 0, 1, 0, 0 }; + unsigned int first = chunk * 13, n, i; + size_t size = sizeof(fixed); + + memcpy(buf, fixed, sizeof(fixed)); + buf[0] = chunk + 1; + if (!board || !board->scan_tlvs) + return size; + /* The macOS 27.0 scan request appends TLV 0x104, a count and then + * {channel, band (0 = 2.4 GHz, 1 = 5 GHz), 0, 0} per channel, and TLV 0x165, an OCE block + * whose flags byte always has bit 0 set. One chunk covers 13 entries of the wiphy table. + */ + n = min_t(unsigned int, 13, 13 + ARRAY_SIZE(five_ghz) - first); + list[0] = n; + for (i = 0; i < n; i++) { + const struct ieee80211_channel *channel = &wifi->channels[first + i]; + + list[1 + 4 * i] = channel->hw_value; + list[2 + 4 * i] = channel->band == NL80211_BAND_5GHZ; + list[3 + 4 * i] = 0; + list[4 + 4 * i] = 0; + } + append_tlv(buf, &size, 0x104, list, 1 + 4 * n); + append_tlv(buf, &size, 0x165, oce, sizeof(oce)); + return size; +} + static void scan_worker(struct work_struct *work) { struct centauri_wifi *wifi = container_of(work, struct centauri_wifi, scan_work); struct cfg80211_scan_info info = {}; struct cfg80211_scan_request *request; - u8 scan[14] = { 1, 0, 0, 0x80, 0, 2, 0, 0x10, 110, 0, 110, 0, 0, 0 }; + u8 scan[14 + 4 + 1 + 4 * 13 + 4 + 5]; + size_t scan_size; unsigned int chunk; int ret = 0; @@ -814,8 +851,8 @@ static void scan_worker(struct work_struct *work) bool stopped = false; unsigned long deadline; - scan[0] = chunk + 1; - ret = command(wifi, 65551, scan, sizeof(scan)); + scan_size = build_scan(wifi, chunk, scan); + ret = command(wifi, 65551, scan, scan_size); if (ret) break; deadline = jiffies + msecs_to_jiffies(8000); From 23b518463ea83b55faaf7605c95007ab53169353 Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:14:39 -0500 Subject: [PATCH 10/22] centauri: send a non-zero passive dwell in the scan request The Alpha firmware of the J873 asserts in SCHED_Req_Add when the scan state machine selects a dwell of zero. The scan request carries the active minimum and maximum dwell at frame +0x20 and +0x22 and the passive dwell at +0x24 (in ms, as macOS fills them from the scan request). The driver sent 0 as passive dwell. The whole firmware then trapped at the first scan with PHY, UMAC crash handler and SCHED_Req_Add assertions. Send 110 ms active and 110 ms passive. Also correct the comment in centauri.h that blamed the missing TLVs. Measured on an M6 Mac mini with the original firmware table and no calibration data: the scan returns rc=0 with 24 channels and 51 BSS (31 to 51 BSS in three runs). [Thomas: the driver part of 6d4959cc6edb on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- drivers/net/wireless/apple/centauri/centauri.h | 3 ++- drivers/net/wireless/apple/centauri/centauri_wifi.c | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/drivers/net/wireless/apple/centauri/centauri.h b/drivers/net/wireless/apple/centauri/centauri.h index a6aae0c4c10c1..43256ac8ec40e 100644 --- a/drivers/net/wireless/apple/centauri/centauri.h +++ b/drivers/net/wireless/apple/centauri/centauri.h @@ -51,7 +51,8 @@ struct centauri { * link_tlv, scan_tlvs: the macOS 27.0 WLAN adapter differs from the 26.6.1 one the J813 recipe * came from. startInterface carries the three link addresses in TLV 386 (was 385), and the scan * request (command 0x1000f) carries a channel-list TLV 0x104 and an OCE TLV 0x165 after the fixed - * 14 bytes. A J873 scan without them crashed the UMAC in SCHED_Req_Add. + * 14 bytes. The J873 scan crash in SCHED_Req_Add came from a zero passive dwell in those 14 bytes + * (see build_scan), not from the missing TLVs; whether the TLVs are required is untested. */ struct centauri_board { const char *compatible; diff --git a/drivers/net/wireless/apple/centauri/centauri_wifi.c b/drivers/net/wireless/apple/centauri/centauri_wifi.c index 86a119dea0ea9..978f314973317 100644 --- a/drivers/net/wireless/apple/centauri/centauri_wifi.c +++ b/drivers/net/wireless/apple/centauri/centauri_wifi.c @@ -803,15 +803,26 @@ static int radio_start(struct centauri_wifi *wifi) static void append_tlv(u8 *payload, size_t *size, u16 tag, const void *data, u16 length); +/* Dwell times in ms for the scan request. The macOS 27 scan adapter copies + * the first request dwell field into frame +0x20 and +0x22 and the second into +0x24 and logs + * "dwelltime active[%d, %d] passive %d". A zero passive dwell makes the J873 scan state machine + * assert in SCHED_Req_Add (the dwell it selects must not be zero) and the whole firmware traps. + */ +#define SCAN_ACTIVE_DWELL_MS 110 +#define SCAN_PASSIVE_DWELL_MS 110 + /* Fixed scan request (command 0x1000f), 14 bytes, chunk number first. */ static size_t build_scan(struct centauri_wifi *wifi, unsigned int chunk, u8 *buf) { - static const u8 fixed[14] = { 1, 0, 0, 0x80, 0, 2, 0, 0x10, 110, 0, 110, 0, 0, 0 }; + u8 fixed[14] = { 1, 0, 0, 0x80, 0, 2, 0, 0x10 }; const struct centauri_board *board = centauri_board(); u8 list[1 + 4 * 13], oce[5] = { 0, 0, 1, 0, 0 }; unsigned int first = chunk * 13, n, i; size_t size = sizeof(fixed); + put_unaligned_le16(SCAN_ACTIVE_DWELL_MS, fixed + 8); + put_unaligned_le16(SCAN_ACTIVE_DWELL_MS, fixed + 10); + put_unaligned_le16(SCAN_PASSIVE_DWELL_MS, fixed + 12); memcpy(buf, fixed, sizeof(fixed)); buf[0] = chunk + 1; if (!board || !board->scan_tlvs) From 292b8d2f54391df36dae99a6564a1c4a27588ac6 Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 06:14:39 -0500 Subject: [PATCH 11/22] centauri: send TLV 376 of the join command with 14 bytes The macOS 27.0.1 adapter sends the MLD configuration TLV 0x178 of the join command (0x20000) with 14 bytes: the 12 default bytes and two zero bytes. With 12 bytes the Alpha firmware of the J873 rejects the join with firmware_status 347 and the 4-way handshake never starts. Measured on an M6 Mac mini: a WPA2-PSK join (CCMP, 5 GHz) completes the 4-way handshake. About 15 s after the join the Alpha firmware still traps (PHY5G irq_isr.c:105, PHY2G power_manager.c:394); that is not fixed by this change. [Thomas: the driver part of 57482a6f8eda on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- drivers/net/wireless/apple/centauri/centauri_wifi.c | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/drivers/net/wireless/apple/centauri/centauri_wifi.c b/drivers/net/wireless/apple/centauri/centauri_wifi.c index 978f314973317..95f9ad2d0b208 100644 --- a/drivers/net/wireless/apple/centauri/centauri_wifi.c +++ b/drivers/net/wireless/apple/centauri/centauri_wifi.c @@ -942,13 +942,15 @@ static void append_tlv(u8 *payload, size_t *size, u16 tag, const void *data, u16 static int wifi_connect(struct wiphy *wiphy, struct net_device *netdev, struct cfg80211_connect_params *params) { - /* Stock 25G76 ACIConnectionAdapter::connectAp (0x1808e94cc) emits a - * 12-byte TLV 376. The newer SDK 27 adapter uses 14 bytes. Preserve the - * unspecified-link masks; all-zero is not the non-MLO default. + /* The stock 26.x join emits a 12-byte TLV 376. The macOS 27.0.1 + * adapter writes tag 0x178 with length 14: the MLD configuration bytes + * that its default initialisation fills with the defaults below, bytes + * 12 and 13 being 0 and the flags byte (bits from the connection parameters, 0 by default). + * Preserve the unspecified-link masks; all-zero is not the non-MLO default. */ - static const u8 mld_defaults[12] = { + static const u8 mld_defaults[14] = { 1, 0, 1, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, 0xff, 3, + 0xff, 0xff, 0xff, 3, 0, 0, }; /* Firmware connection-context defaults (aciw 0x511ee8). TLV 0xf913 * replaces all five values. Sending zeros arms Supp_AddTimer with a From 14c61f94ae814cf34c026a6d3a916b6c1c1cc164 Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:27:41 -0500 Subject: [PATCH 12/22] centauri: tell the firmware the host is awake before a join Without the host power state the Alpha firmware treats the host as asleep, powers the PHYs down and traps about 15 s after a join (PHY5G irq_isr.c:105, PHY2G power_manager.c:394, and in a log with a post-join frame also LMAC2G "PMGR - Assert while waiting"). Before the join, send the three commands that the host_wakeup and tx_config debugfs files already send: command 6 with a zero body (host power state awake), command 43 (packet lifetime) and command 52 (control profile). The debugfs files stay; the module skips what they applied. The new host_setup parameter (default on) turns this off. Measured on a Mac mini M6 (J873), WPA2-PSK, 5 GHz, with the data rings set up through the data_probe files as before: - the firmware stayed alive (stage 2) for the whole test, about 17 minutes connected in the run with the three commands sent through debugfs, and for the whole test in the run with this change; before, it trapped within 15 s in 14 of 14 variants; - DHCP lease over Wi-Fi; ping through wlan0; iperf3 to a host on the same network bound to the wlan0 address: 44.7 / 58.9 Mbit/s up / down with the debugfs commands, 43.7 / 40.2 Mbit/s with this change; - a 600 s iperf3 at 20 Mbit/s moved 1.40 GBytes without a stall. Not measured: more than 20 minutes, suspend, roaming, 2.4 GHz, WPA3. Some ping windows lost packets (0 to 50 percent in 10 to 20 packet samples). [Thomas: the driver part of 1242f12e876b on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- .../wireless/apple/centauri/centauri_wifi.c | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/drivers/net/wireless/apple/centauri/centauri_wifi.c b/drivers/net/wireless/apple/centauri/centauri_wifi.c index 95f9ad2d0b208..59f947bc7c34c 100644 --- a/drivers/net/wireless/apple/centauri/centauri_wifi.c +++ b/drivers/net/wireless/apple/centauri/centauri_wifi.c @@ -29,6 +29,10 @@ static_assert(DATA_TX_BUFFER >= CEN_ALPHA_SCRATCH_OFFSET + CEN_ALPHA_SCRATCH_SIZE); static_assert(DATA_TX_BUFFER + DATA_TX_SLOTS * BUFFER_SIZE <= DATA_RX_BUFFER); +static bool host_setup = true; +module_param(host_setup, bool, 0644); +MODULE_PARM_DESC(host_setup, "Before a join, tell the firmware the host is awake (command 6) and apply the reference TX defaults (commands 43 and 52) when the tx_config files have not; 1 = on (default)"); + static bool verify_pmk; module_param(verify_pmk, bool, 0400); MODULE_PARM_DESC(verify_pmk, "Compare firmware PMK with the requested key without exposing either"); @@ -131,6 +135,7 @@ struct centauri_wifi { size_t fwlog_size; bool fwlog_attempted, fwlog_ready; u8 tx_profile[10], tx_defaults_applied; + bool host_awake_sent; bool tx_profile_valid; u8 link_count, link_prime[3], link_channel[3], link_band[3], emlsr_mode; bool link_status_valid, emlsr_valid; @@ -1057,6 +1062,39 @@ static int wifi_connect(struct wiphy *wiphy, struct net_device *netdev, return ret; } +/* Without the host power state the firmware treats the host as asleep, powers the PHYs down and traps about 15 s after the + * join. Same three commands as the host_wakeup and tx_config debugfs files; called with wifi->lock held. + */ +static void host_setup_commands(struct centauri_wifi *wifi) +{ + static const u8 awake[3] = {}; + static const u8 control_profile[] = { 0x54, 1, 10, 0, 0, 8, 0, 0, 0xff, 0xff, 0xff, 0xff, 1, 1 }; + u8 lifetime[20] = {}; + int ret, i; + + if (!host_setup) + return; + if (!wifi->host_awake_sent) { + ret = command(wifi, 6, awake, sizeof(awake)); + dev_info(&wifi->control->pdev->dev, "host power state awake: rc=%d status=%u\n", ret, wifi->reply_status); + wifi->host_awake_sent = !ret; + } + if (!(wifi->tx_defaults_applied & BIT(0))) { + for (i = 0; i < 4; i++) + put_unaligned_le32(2000000, lifetime + 4 + 4 * i); + ret = command(wifi, 43, lifetime, sizeof(lifetime)); + if (!ret) + wifi->tx_defaults_applied |= BIT(0); + dev_info(&wifi->control->pdev->dev, "TX packet lifetime defaults: rc=%d\n", ret); + } + if (!(wifi->tx_defaults_applied & BIT(1))) { + ret = command(wifi, 52, control_profile, sizeof(control_profile)); + if (!ret) + wifi->tx_defaults_applied |= BIT(1); + dev_info(&wifi->control->pdev->dev, "TX control profile defaults: rc=%d\n", ret); + } +} + static void connect_worker(struct work_struct *work) { struct centauri_wifi *wifi = container_of(work, struct centauri_wifi, connect_work); @@ -1066,6 +1104,7 @@ static void connect_worker(struct work_struct *work) int ret; mutex_lock(&wifi->lock); + host_setup_commands(wifi); slot = wifi->command_head; ret = command(wifi, 131072, wifi->connect_payload, wifi->connect_size); command_status = wifi->reply_status; From b45d1ff5f7779dba5856a7579686c07714d1c62c Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:20:29 -0500 Subject: [PATCH 13/22] centauri: switch the firmware roam scan off after a join After the join the firmware runs its own roam and background scans. No host roam profile exists, and each scan takes the radio off channel for seconds. Send ACI command 0x20016 (no body) once the join succeeded, unless the new roam_scan parameter is set (default off, so the scan is off). Measured on a Mac mini M6 (J873), WPA2-PSK, 5 GHz, two boots with the host-awake commands from the previous patch, a ping every 0.5 s for 120 s to the Wi-Fi gateway, bound to wlan0: - without the command: 240 sent, 68 received (71.7 percent loss), received per 10 s: 13 0 13 0 10 3 3 10 0 13 0 3 0 of 20; - with the command in a separate boot: 240 of 240 received, rtt 9 to 73 ms, every 10 s bucket 20 of 20; - with this patch (default settings, third boot): 240 of 240 received, rtt 9.5 to 96 ms, 20 of 20 in every bucket. This is a choice for a stationary machine: with the scan off the firmware does not look for a better access point. A host roam profile (command 0x20014) would be the way to bring roaming back; it was accepted by the firmware but not tested as a replacement. [Thomas: the driver part of 00c55963147d on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- drivers/net/wireless/apple/centauri/centauri_wifi.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/drivers/net/wireless/apple/centauri/centauri_wifi.c b/drivers/net/wireless/apple/centauri/centauri_wifi.c index 59f947bc7c34c..842c2117953a7 100644 --- a/drivers/net/wireless/apple/centauri/centauri_wifi.c +++ b/drivers/net/wireless/apple/centauri/centauri_wifi.c @@ -29,6 +29,10 @@ static_assert(DATA_TX_BUFFER >= CEN_ALPHA_SCRATCH_OFFSET + CEN_ALPHA_SCRATCH_SIZE); static_assert(DATA_TX_BUFFER + DATA_TX_SLOTS * BUFFER_SIZE <= DATA_RX_BUFFER); +static bool roam_scan; +module_param(roam_scan, bool, 0644); +MODULE_PARM_DESC(roam_scan, "Let the firmware run its own roam and background scans after a join; 0 = off (default). The scans take the radio off channel for seconds (72 percent ping loss measured) until the host programs a roam profile"); + static bool host_setup = true; module_param(host_setup, bool, 0644); MODULE_PARM_DESC(host_setup, "Before a join, tell the firmware the host is awake (command 6) and apply the reference TX defaults (commands 43 and 52) when the tx_config files have not; 1 = on (default)"); @@ -1166,6 +1170,11 @@ static void connect_worker(struct work_struct *work) status = !ret && wifi->connect_status == 0 ? WLAN_STATUS_SUCCESS : WLAN_STATUS_UNSPECIFIED_FAILURE; wifi->connecting = false; wifi->connected = status == WLAN_STATUS_SUCCESS; + if (wifi->connected && !roam_scan) { + int roam_ret = command(wifi, 0x20016, NULL, 0); + + dev_info(&wifi->control->pdev->dev, "roam scan off: rc=%d status=%u\n", roam_ret, wifi->reply_status); + } memzero_explicit(wifi->expected_pmk, sizeof(wifi->expected_pmk)); /* The command and connection completion have consumed the PMK buffer. */ if (slot < ring_size[1]) From 347f03b8289c1fc3b5d3f3c37c85f29747bee9e5 Mon Sep 17 00:00:00 2001 From: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:48:37 -0500 Subject: [PATCH 14/22] centauri: open the data rings and join with the PMK alone A connection needed three manual steps through debugfs: the passphrase file, the data_probe ids 76, 24, 15 and 12 before the join, and id 25 after it. Do the ring setup from the connect path (parameter auto_rings, default on): the four rings under the connect lock before the join, the normal TX ring once the connection result is reported, so the join stays inside the supplicant's timeout. The data_probe files keep working and are skipped for rings that are already open. The data_probe writer is split into a helper that the connect path calls with the lock held. The firmware accepts a join with the PMK from the supplicant alone, so the passphrase debugfs file and TLV 313 are gone. Measured on a Mac mini M6 (J873), WPA2-PSK, 5 GHz, wpa_supplicant started with a PSK configuration and nothing else (no passphrase file, no data_probe, no host_wakeup or tx_config write): the join completed, DHCP gave a lease, the log shows the four rings at join time, the host-awake and TX default commands, the roam scan switch-off and ring 25 after the result; 20 of 20 pings through wlan0 came back and iperf3 gave 56.3 Mbit/s up and 38.4 down over 10 s each. Measured on the same machine with the module installed and the standard services enabled (wpa_supplicant-nl80211@wlan0 and systemd-networkd, nothing started by hand): after one reboot wlan0 had its address 18 s after boot; the log shows the rings, the host-awake and TX default commands, the roam scan switch-off and ring 25 between 12.6 s and 17.0 s; 20 of 20 pings through wlan0 came back and iperf3 gave 54.8 Mbit/s up and 40.2 down over 10 s each. NetworkManager could not be used on that image: its iwd backend does not start on the test kernel, which lacks CONFIG_CRYPTO_USER_API_HASH, CONFIG_CRYPTO_USER_API_SKCIPHER and CONFIG_KEY_DH_OPERATIONS among others. [Thomas: the driver part of 8b8ec1a2ee3e on the J813 line of linux-aurora] Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com> Signed-off-by: Thomas Glanzmann --- .../wireless/apple/centauri/centauri_wifi.c | 121 ++++++++---------- 1 file changed, 54 insertions(+), 67 deletions(-) diff --git a/drivers/net/wireless/apple/centauri/centauri_wifi.c b/drivers/net/wireless/apple/centauri/centauri_wifi.c index 842c2117953a7..fd863f1fe506b 100644 --- a/drivers/net/wireless/apple/centauri/centauri_wifi.c +++ b/drivers/net/wireless/apple/centauri/centauri_wifi.c @@ -29,6 +29,12 @@ static_assert(DATA_TX_BUFFER >= CEN_ALPHA_SCRATCH_OFFSET + CEN_ALPHA_SCRATCH_SIZE); static_assert(DATA_TX_BUFFER + DATA_TX_SLOTS * BUFFER_SIZE <= DATA_RX_BUFFER); +static bool auto_rings = true; +module_param(auto_rings, bool, 0644); +MODULE_PARM_DESC(auto_rings, "Open the data rings (data_probe ids 76, 24, 15, 12 before the join, 25 after it) from the connect path; 1 = on (default). The data_probe debugfs files still work and are skipped for rings already open"); + +static int data_probe_locked(struct centauri_wifi *wifi, unsigned int id, bool internal); + static bool roam_scan; module_param(roam_scan, bool, 0644); MODULE_PARM_DESC(roam_scan, "Let the firmware run its own roam and background scans after a join; 0 = off (default). The scans take the radio off channel for seconds (72 percent ping loss measured) until the host programs a roam profile"); @@ -114,9 +120,6 @@ struct centauri_wifi { u16 connect_status; u8 bssid[ETH_ALEN], connect_payload[1024], request_ie[256]; size_t connect_size, request_ie_size; - u8 passphrase[64]; - size_t passphrase_size; - struct dentry *passphrase_file; struct dentry *data_probe_file; struct dentry *data_status_file; struct dentry *data_rx_snapshot_file; @@ -973,7 +976,7 @@ static int wifi_connect(struct wiphy *wiphy, struct net_device *netdev, struct cfg80211_bss *bss; const struct cfg80211_bss_ies *ies; const u8 *rsn, *ap_rsn; - u8 rsn_body[255], ssid[35], key[66], zero[20] = {}, pmf; + u8 rsn_body[255], ssid[35], zero[20] = {}, pmf; u8 *payload = wifi->connect_payload; size_t size = 130, rsn_size; u16 caps = 0; @@ -992,10 +995,6 @@ static int wifi_connect(struct wiphy *wiphy, struct net_device *netdev, ret = -EBUSY; goto out; } - if (!wifi->passphrase_size) { - ret = -ENOKEY; - goto out; - } bss = cfg80211_get_bss(wiphy, params->channel, params->bssid, params->ssid, params->ssid_len, IEEE80211_BSS_TYPE_ESS, IEEE80211_PRIVACY_ON); if (!bss) { @@ -1045,11 +1044,9 @@ static int wifi_connect(struct wiphy *wiphy, struct net_device *netdev, memcpy(ssid + 3, params->ssid, params->ssid_len); append_tlv(payload, &size, 342, ssid, 3 + params->ssid_len); append_tlv(payload, &size, WLAN_EID_RSN, rsn_body, rsn_size); - /* The fixed field carries the PMK; TLV 313 carries the passphrase. */ - put_unaligned_le16(wifi->passphrase_size, key); - memcpy(key + 2, wifi->passphrase, wifi->passphrase_size); - append_tlv(payload, &size, 313, key, 2 + wifi->passphrase_size); - memzero_explicit(key, sizeof(key)); + /* The fixed field carries the PMK. The firmware accepts a join with the PMK alone (measured); the macOS TLV 313 carries the + * passphrase and is not sent. + */ append_tlv(payload, &size, 316, &pmf, 1); append_tlv(payload, &size, 376, mld_defaults, sizeof(mld_defaults)); wifi->connect_size = size; @@ -1108,6 +1105,17 @@ static void connect_worker(struct work_struct *work) int ret; mutex_lock(&wifi->lock); + if (auto_rings) { + static const unsigned int ring_ids[] = { 76, 24, 15, 12 }; + unsigned int n; + + for (n = 0; n < ARRAY_SIZE(ring_ids); n++) { + int ring_ret = data_probe_locked(wifi, ring_ids[n], true); + + if (ring_ret && ring_ret != -EBUSY) + dev_warn(&wifi->control->pdev->dev, "data ring %u setup failed: %d\n", ring_ids[n], ring_ret); + } + } host_setup_commands(wifi); slot = wifi->command_head; ret = command(wifi, 131072, wifi->connect_payload, wifi->connect_size); @@ -1185,6 +1193,18 @@ static void connect_worker(struct work_struct *work) ret, command_status, wifi->connect_status, wifi->connect_event); cfg80211_connect_result(wifi->netdev, wifi->bssid, wifi->request_ie, wifi->request_ie_size, NULL, 0, status, GFP_KERNEL); + if (status == WLAN_STATUS_SUCCESS && auto_rings && !READ_ONCE(wifi->stopping)) { + /* The normal TX ring is opened after the connection result, as in the manual recipe (data_probe 25 after wpa completed); + * doing it after cfg80211_connect_result keeps the join inside the supplicant's timeout. + */ + int ring_ret; + + mutex_lock(&wifi->lock); + ring_ret = data_probe_locked(wifi, 25, true); + mutex_unlock(&wifi->lock); + if (ring_ret && ring_ret != -EBUSY) + dev_warn(&wifi->control->pdev->dev, "data ring 25 setup failed: %d\n", ring_ret); + } if (status == WLAN_STATUS_SUCCESS && !READ_ONCE(wifi->stopping)) { netif_carrier_on(wifi->netdev); if (wifi->data_probe_done & BIT(5)) @@ -1328,46 +1348,6 @@ static const struct net_device_ops net_ops = { .ndo_validate_addr = eth_validate_addr, }; -static ssize_t passphrase_write(struct file *file, const char __user *input, - size_t count, loff_t *position) -{ - struct centauri_wifi *wifi = file->private_data; - u8 value[64]; - size_t length = count; - int ret = 0; - - if (count < 8 || count > sizeof(value)) - return -EINVAL; - if (copy_from_user(value, input, count)) { - ret = -EFAULT; - goto out; - } - if (value[length - 1] == '\n') - length--; - if (length < 8 || length > 63) { - ret = -EINVAL; - goto out; - } - mutex_lock(&wifi->lock); - if (wifi->connecting) { - ret = -EBUSY; - } else { - memzero_explicit(wifi->passphrase, sizeof(wifi->passphrase)); - memcpy(wifi->passphrase, value, length); - wifi->passphrase_size = length; - } - mutex_unlock(&wifi->lock); -out: - memzero_explicit(value, sizeof(value)); - return ret ?: count; -} - -static const struct file_operations passphrase_ops = { - .owner = THIS_MODULE, - .open = simple_open, - .write = passphrase_write, -}; - /* Airship direct_memregion::map (0x1802864e0) sends opcode 8, a region ID, * byte size and a 64-bit DVA in a 24-byte header. Alpha's region-debug is ID 1. * Firmware 0x175340 passes those fields to DramPhyService_handle_buffer; @@ -1552,18 +1532,14 @@ static const struct file_operations fwlog_buffer_ops = { * 0x11760e. Keep these empty-ring probes explicit until packet DMA is proven. * The reserved regions below do not overlap control rings or scan buffers. */ -static ssize_t data_probe_write(struct file *file, const char __user *input, - size_t count, loff_t *position) +/* Called with wifi->lock held. From the connect path (internal) the "connecting" flag is expected to be set. */ +static int data_probe_locked(struct centauri_wifi *wifi, unsigned int id, bool internal) { - struct centauri_wifi *wifi = file->private_data; u8 message[0x34 + 10] = { 0 }; - unsigned int id, vector, offset, size, bit, doorbell, irq; + unsigned int vector, offset, size, bit, doorbell, irq; bool transfer = false; int ret; - ret = kstrtouint_from_user(input, count, 0, &id); - if (ret) - return ret; if (id == 76) { vector = 192; offset = 0x200000; @@ -1596,15 +1572,14 @@ static ssize_t data_probe_write(struct file *file, const char __user *input, } else { return -EINVAL; } - mutex_lock(&wifi->lock); if (readl(wifi->a.bar + 8) != 2) { ret = -EIO; - goto out; + return ret; } - if (wifi->connecting || wifi->scan_request || + if ((!internal && wifi->connecting) || wifi->scan_request || (wifi->data_probe_done & BIT(bit))) { ret = -EBUSY; - goto out; + return ret; } wifi->data_probe_done |= BIT(bit); memset(wifi->a.window + offset, 0, size * 32); @@ -1673,7 +1648,21 @@ static ssize_t data_probe_write(struct file *file, const char __user *input, size - 1, readl(wifi->a.bar + 8), index_read(wifi, 1, 12), index_read(wifi, 0, 207)); } -out: + return ret; +} + +static ssize_t data_probe_write(struct file *file, const char __user *input, + size_t count, loff_t *position) +{ + struct centauri_wifi *wifi = file->private_data; + unsigned int id; + int ret; + + ret = kstrtouint_from_user(input, count, 0, &id); + if (ret) + return ret; + mutex_lock(&wifi->lock); + ret = data_probe_locked(wifi, id, false); mutex_unlock(&wifi->lock); return ret ?: count; } @@ -2333,7 +2322,6 @@ int centauri_wifi_register(struct centauri *c) } dev_info(&c->pdev->dev, "cfg80211 interface %s ready for scan tests; packet data unavailable\n", wifi->netdev->name); - wifi->passphrase_file = debugfs_create_file("passphrase", 0200, c->debug, wifi, &passphrase_ops); wifi->data_probe_file = debugfs_create_file("data_probe", 0200, c->debug, wifi, &data_probe_ops); wifi->data_status_file = debugfs_create_file("data_status", 0400, c->debug, wifi, &data_status_ops); wifi->data_rx_snapshot_file = debugfs_create_file("data_rx_snapshot", 0400, @@ -2369,7 +2357,6 @@ void centauri_wifi_unregister(struct centauri *c) WRITE_ONCE(wifi->stopping, true); if (wifi->netdev && wifi->netdev->reg_state == NETREG_REGISTERED) netif_tx_disable(wifi->netdev); - debugfs_remove(wifi->passphrase_file); debugfs_remove(wifi->data_probe_file); debugfs_remove(wifi->data_status_file); debugfs_remove(wifi->data_rx_snapshot_file); From 71ee7898f089170de35fb001109a837c57184cee Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 25 Oct 2026 23:02:30 +0100 Subject: [PATCH 15/22] centauri: report station counters, signal and channel wavemon and iw had nothing to show for the N1: the driver implemented no get_station or get_channel. Report the packet, byte, failure and drop counters the data path already keeps, the signal of the joined BSS as cfg80211 has it from the last scan (no firmware query for live link statistics is known yet), and the joined channel at the 20 MHz the driver associates with. Bitrates and transmit power stay unreported. [Thomas: the driver part of 767a663a63de on the J813 line of linux-aurora] Signed-off-by: Thomas Glanzmann --- .../wireless/apple/centauri/centauri_wifi.c | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/drivers/net/wireless/apple/centauri/centauri_wifi.c b/drivers/net/wireless/apple/centauri/centauri_wifi.c index fd863f1fe506b..df694b9c13a00 100644 --- a/drivers/net/wireless/apple/centauri/centauri_wifi.c +++ b/drivers/net/wireless/apple/centauri/centauri_wifi.c @@ -1297,11 +1297,81 @@ static void wifi_regulatory(struct wiphy *wiphy, struct regulatory_request *requ mutex_unlock(&wifi->lock); } +/* + * The joined BSS as cfg80211 knows it from the last scan. No firmware query + * for live link statistics is known yet, so its signal is the scan's. + */ +static struct cfg80211_bss *wifi_joined_bss(struct centauri_wifi *wifi) +{ + if (!wifi->connected) + return NULL; + return cfg80211_get_bss(wifi->wiphy, NULL, wifi->bssid, NULL, 0, + IEEE80211_BSS_TYPE_ESS, IEEE80211_PRIVACY_ANY); +} + +static int wifi_get_station(struct wiphy *wiphy, struct wireless_dev *wdev, + const u8 *mac, struct station_info *sinfo) +{ + struct centauri_wifi *wifi = *(struct centauri_wifi **)wiphy_priv(wiphy); + struct net_device_stats *stats = &wifi->netdev->stats; + struct cfg80211_bss *bss; + int ret = 0; + + mutex_lock(&wifi->lock); + if (!wifi->connected || !ether_addr_equal(mac, wifi->bssid)) { + ret = -ENOENT; + goto out; + } + sinfo->filled |= BIT_ULL(NL80211_STA_INFO_RX_PACKETS) | + BIT_ULL(NL80211_STA_INFO_TX_PACKETS) | + BIT_ULL(NL80211_STA_INFO_RX_BYTES64) | + BIT_ULL(NL80211_STA_INFO_TX_BYTES64) | + BIT_ULL(NL80211_STA_INFO_TX_FAILED) | + BIT_ULL(NL80211_STA_INFO_RX_DROP_MISC); + sinfo->rx_packets = stats->rx_packets; + sinfo->tx_packets = stats->tx_packets; + sinfo->rx_bytes = stats->rx_bytes; + sinfo->tx_bytes = stats->tx_bytes; + sinfo->tx_failed = stats->tx_errors; + sinfo->rx_dropped_misc = stats->rx_dropped; + + bss = wifi_joined_bss(wifi); + if (bss) { + sinfo->filled |= BIT_ULL(NL80211_STA_INFO_SIGNAL); + sinfo->signal = bss->signal / 100; /* mBm */ + cfg80211_put_bss(wiphy, bss); + } +out: + mutex_unlock(&wifi->lock); + return ret; +} + +static int wifi_get_channel(struct wiphy *wiphy, struct wireless_dev *wdev, + unsigned int link_id, struct cfg80211_chan_def *chandef) +{ + struct centauri_wifi *wifi = *(struct centauri_wifi **)wiphy_priv(wiphy); + struct cfg80211_bss *bss; + int ret = -ENODATA; + + mutex_lock(&wifi->lock); + bss = wifi_joined_bss(wifi); + if (bss) { + /* The join asks the firmware for a 20 MHz channel. */ + cfg80211_chandef_create(chandef, bss->channel, NL80211_CHAN_NO_HT); + cfg80211_put_bss(wiphy, bss); + ret = 0; + } + mutex_unlock(&wifi->lock); + return ret; +} + static const struct cfg80211_ops wifi_ops = { .scan = wifi_scan, .connect = wifi_connect, .disconnect = wifi_disconnect, .set_power_mgmt = wifi_power_mgmt, + .get_station = wifi_get_station, + .get_channel = wifi_get_channel, }; static int net_open(struct net_device *netdev) From 5086206056442048982842de9815253bd4b85914 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 26 Oct 2026 00:43:13 +0100 Subject: [PATCH 16/22] centauri, btcentauri: match the J873 as apple,j873g The J873 device tree now uses the machine compatible from the binding, "apple,j873g" (after the ADT target-type J873g). Match that in the centauri board table and in btcentauri. [Thomas: the driver part of 9e911df6e4c0 on the J813 line of linux-aurora] Signed-off-by: Thomas Glanzmann --- drivers/bluetooth/btcentauri.c | 2 +- drivers/net/wireless/apple/centauri/centauri.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/bluetooth/btcentauri.c b/drivers/bluetooth/btcentauri.c index 417b58033d1f9..285696f8c7151 100644 --- a/drivers/bluetooth/btcentauri.c +++ b/drivers/bluetooth/btcentauri.c @@ -914,7 +914,7 @@ static int cen_probe(struct pci_dev *pdev, const struct pci_device_id *id) struct hci_dev *hdev; int ret; - if (!(of_machine_is_compatible("apple,j813") || of_machine_is_compatible("apple,j873")) || + if (!(of_machine_is_compatible("apple,j813") || of_machine_is_compatible("apple,j873g")) || PCI_FUNC(pdev->devfn) != 2) return -ENODEV; bt = devm_kzalloc(&pdev->dev, sizeof(*bt), GFP_KERNEL); diff --git a/drivers/net/wireless/apple/centauri/centauri.h b/drivers/net/wireless/apple/centauri/centauri.h index 43256ac8ec40e..c8111fe433cc5 100644 --- a/drivers/net/wireless/apple/centauri/centauri.h +++ b/drivers/net/wireless/apple/centauri/centauri.h @@ -72,7 +72,7 @@ static inline const struct centauri_board *centauri_board(void) { "apple,j813", "apple/centauri/j813-ftab.bin", 0x00248142, 0x488028000ULL, 0x48f010000ULL, 0x39a000300ULL, 0x03020000, 0x100c, false, 385, false }, - { "apple,j873", "apple/centauri/j873-ftab.bin", 0x00248152, + { "apple,j873g", "apple/centauri/j873-ftab.bin", 0x00248152, 0x388028000ULL, 0x38f010000ULL, 0x31a00000cULL, 0x208, 0, true, 386, true }, }; From aa002e7f0ce7ca5346e3c73f6af3334322b8cd16 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 23:20:50 +0100 Subject: [PATCH 17/22] wifi: apple: centauri: Keep the N1 functions running across s2idle Without PM callbacks the PCI core's default suspend disables the function, which turns bus mastering off under the running N1 firmware. The firmware cannot be restarted without the bootloader's link training, and after resume every ring was dead (each scan failed with -EIO). Save the config space ourselves, so the core leaves the function enabled and in D0, and keep the PCIe host's power domains up through the noirq phase (device_set_awake_path()). Tested on J813: Wi-Fi associated and passing traffic after s2idle. [Thomas: the driver part of fd2382058a0a on the J813 line of linux-aurora] Signed-off-by: Thomas Glanzmann --- .../net/wireless/apple/centauri/centauri_pci.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/drivers/net/wireless/apple/centauri/centauri_pci.c b/drivers/net/wireless/apple/centauri/centauri_pci.c index 23fff2160a533..86ad99c48db2e 100644 --- a/drivers/net/wireless/apple/centauri/centauri_pci.c +++ b/drivers/net/wireless/apple/centauri/centauri_pci.c @@ -245,6 +245,22 @@ static void centauri_remove(struct pci_dev *pdev) centauri_free_boot(c); } +/* + * The N1 firmware keeps running across s2idle: it cannot be restarted without + * the bootloader's link training. Without PM callbacks the PCI core disables a + * function at suspend, which turns bus mastering off under the running + * firmware and leaves every ring dead (each scan then fails with -EIO). Save + * the state ourselves so the core keeps the function enabled and in D0, and + * keep the PCIe host's power domains up through the noirq phase. + */ +static int centauri_suspend(struct device *dev) +{ + device_set_awake_path(dev); + return pci_save_state(to_pci_dev(dev)); +} + +static DEFINE_SIMPLE_DEV_PM_OPS(centauri_pm_ops, centauri_suspend, NULL); + /* 0x1903 is the Beta (Bluetooth) function: btcentauri claims it. This driver needs functions * 0 and 1 (1 is enabled here, its BAR is used by the Wi-Fi code) and reaches function 2 only * through config space and its struct device for DMA. @@ -262,6 +278,7 @@ static struct pci_driver centauri_driver = { .id_table = centauri_ids, .probe = centauri_probe, .remove = centauri_remove, + .driver.pm = pm_sleep_ptr(¢auri_pm_ops), }; static int __init centauri_init(void) From 45369335b4c13e47ff459dba979c25a12272cc47 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 23:20:50 +0100 Subject: [PATCH 18/22] Bluetooth: btcentauri: Keep the transport running across s2idle As for the Wi-Fi functions: without PM callbacks the PCI core turned bus mastering off at suspend, and the next poll stopped the transport for good. Save the config space so the function stays enabled and in D0, keep the PCIe host powered, and pause the poll work across the sleep. Tested on J813: hci0 UP RUNNING after s2idle. [Thomas: the driver part of c1f436307d70 on the J813 line of linux-aurora] Signed-off-by: Thomas Glanzmann --- drivers/bluetooth/btcentauri.c | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/drivers/bluetooth/btcentauri.c b/drivers/bluetooth/btcentauri.c index 285696f8c7151..6c6880a104ae0 100644 --- a/drivers/bluetooth/btcentauri.c +++ b/drivers/bluetooth/btcentauri.c @@ -1008,6 +1008,34 @@ static void cen_remove(struct pci_dev *pdev) cen_stop(bt); } +/* + * As for the Wi-Fi functions (centauri_pci.c): the firmware keeps running, so + * keep the function enabled and in D0 (the PCI core's default suspend turns + * bus mastering off, and the next poll stopped the transport for good) and + * the PCIe host powered. Polling pauses across the sleep. + */ +static int cen_suspend(struct device *dev) +{ + struct cen_bt *bt = pci_get_drvdata(to_pci_dev(dev)); + + cancel_delayed_work_sync(&bt->poll); + device_set_awake_path(dev); + return pci_save_state(to_pci_dev(dev)); +} + +static int cen_resume(struct device *dev) +{ + struct cen_bt *bt = pci_get_drvdata(to_pci_dev(dev)); + + mutex_lock(&bt->lock); + if (bt->running) + schedule_delayed_work(&bt->poll, 0); + mutex_unlock(&bt->lock); + return 0; +} + +static DEFINE_SIMPLE_DEV_PM_OPS(cen_pm_ops, cen_suspend, cen_resume); + static const struct pci_device_id cen_ids[] = { { PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x1903) }, { } @@ -1019,6 +1047,7 @@ static struct pci_driver cen_driver = { .id_table = cen_ids, .probe = cen_probe, .remove = cen_remove, + .driver.pm = pm_sleep_ptr(&cen_pm_ops), }; module_pci_driver(cen_driver); MODULE_LICENSE("GPL"); From 6fa308531ae0511f4c7cc7b512485262660d7954 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Fri, 30 Oct 2026 05:35:29 +0100 Subject: [PATCH 19/22] PCI: apple: MSI controller for a bootloader-trained port On T8142 (J813) the bootloader trains apcie port 0 and Linux drives the root port as a generic ECAM host, which has no MSI support: the N1's functions could only be polled. The port still turns writes to the MSI doorbell into AIC interrupts through its MSI map, as on T8140/T602X. Add pcie-apple's MSI parent domain as a standalone driver ("apple,t8142-pcie-msi"): it programs the port's MSI address, MSI map and MSI enable and parses msi-ranges like pcie-apple. It depends on PCIE_APPLE=y for the doorbell address and the DART's reserved doorbell IOVA. Signed-off-by: Thomas Glanzmann --- drivers/pci/controller/Kconfig | 8 + drivers/pci/controller/Makefile | 1 + drivers/pci/controller/pcie-apple-msi.c | 185 ++++++++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 drivers/pci/controller/pcie-apple-msi.c diff --git a/drivers/pci/controller/Kconfig b/drivers/pci/controller/Kconfig index b758e0682ffd8..dd3d17603f749 100644 --- a/drivers/pci/controller/Kconfig +++ b/drivers/pci/controller/Kconfig @@ -57,6 +57,14 @@ config PCIE_APPLE If unsure, say Y if you have an Apple Silicon system. +config PCIE_APPLE_MSI + bool "Apple PCIe MSI controller for a bootloader-trained port" + depends on PCIE_APPLE=y + select IRQ_MSI_LIB + help + MSIs for an Apple PCIe port that Linux drives as a generic ECAM + host because the bootloader trained the link (T8142 Wi-Fi). + config PCIE_APPLE_PIODMA_DIAG bool "J700 PIODMA diagnostic (requires full reset after use)" depends on PCIE_APPLE=y && ARCH_APPLE && IOMMU_DMA diff --git a/drivers/pci/controller/Makefile b/drivers/pci/controller/Makefile index 3bd9cdb5b1197..b2570b9297a80 100644 --- a/drivers/pci/controller/Makefile +++ b/drivers/pci/controller/Makefile @@ -40,6 +40,7 @@ obj-$(CONFIG_PCIE_BRCMSTB) += pcie-brcmstb.o obj-$(CONFIG_PCI_LOONGSON) += pci-loongson.o obj-$(CONFIG_PCIE_HISI_ERR) += pcie-hisi-error.o obj-$(CONFIG_PCIE_APPLE) += pcie-apple.o +obj-$(CONFIG_PCIE_APPLE_MSI) += pcie-apple-msi.o obj-$(CONFIG_PCIE_MT7621) += pcie-mt7621.o obj-$(CONFIG_PCIE_ASPEED) += pcie-aspeed.o diff --git a/drivers/pci/controller/pcie-apple-msi.c b/drivers/pci/controller/pcie-apple-msi.c new file mode 100644 index 0000000000000..452e124c43b9b --- /dev/null +++ b/drivers/pci/controller/pcie-apple-msi.c @@ -0,0 +1,185 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * MSI controller for one Apple PCIe port whose link the bootloader trained. + * + * pcie-apple owns the port when Linux brings the link up itself. On T8142 + * (J813) the bootloader trains apcie port 0 and Linux drives the root port as + * a generic ECAM host, which has no MSI support. The port still turns writes + * to the MSI doorbell into AIC interrupts through its MSI map; this driver + * programs that map and provides the parent domain, as pcie-apple does. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define PORT_MSICFG 0x0124 +#define PORT_MSICFG_EN BIT(0) +#define PORT_MSIADDR 0x016c +#define PORT_MSIMAP 0x3800 +#define PORT_MSIMAP_ENABLE BIT(31) + +#define DOORBELL_ADDR CONFIG_PCIE_APPLE_MSI_DOORBELL_ADDR + +struct apple_msi { + struct mutex lock; + struct irq_fwspec fwspec; + unsigned long *bitmap; + u32 nvecs; +}; + +static void apple_msi_compose_msg(struct irq_data *data, struct msi_msg *msg) +{ + msg->address_hi = upper_32_bits(DOORBELL_ADDR); + msg->address_lo = lower_32_bits(DOORBELL_ADDR); + msg->data = data->hwirq; +} + +static struct irq_chip apple_msi_bottom_chip = { + .name = "MSI", + .irq_mask = irq_chip_mask_parent, + .irq_unmask = irq_chip_unmask_parent, + .irq_eoi = irq_chip_eoi_parent, + .irq_set_affinity = irq_chip_set_affinity_parent, + .irq_set_type = irq_chip_set_type_parent, + .irq_compose_msi_msg = apple_msi_compose_msg, +}; + +static int apple_msi_domain_alloc(struct irq_domain *domain, unsigned int virq, + unsigned int nr_irqs, void *args) +{ + struct apple_msi *msi = domain->host_data; + struct irq_fwspec fwspec = msi->fwspec; + unsigned int i; + int ret, hwirq; + + mutex_lock(&msi->lock); + hwirq = bitmap_find_free_region(msi->bitmap, msi->nvecs, + order_base_2(nr_irqs)); + mutex_unlock(&msi->lock); + if (hwirq < 0) + return -ENOSPC; + + fwspec.param[fwspec.param_count - 2] += hwirq; + + ret = irq_domain_alloc_irqs_parent(domain, virq, nr_irqs, &fwspec); + if (ret) { + mutex_lock(&msi->lock); + bitmap_release_region(msi->bitmap, hwirq, order_base_2(nr_irqs)); + mutex_unlock(&msi->lock); + return ret; + } + + for (i = 0; i < nr_irqs; i++) + irq_domain_set_hwirq_and_chip(domain, virq + i, hwirq + i, + &apple_msi_bottom_chip, msi); + return 0; +} + +static void apple_msi_domain_free(struct irq_domain *domain, unsigned int virq, + unsigned int nr_irqs) +{ + struct irq_data *d = irq_domain_get_irq_data(domain, virq); + struct apple_msi *msi = domain->host_data; + + mutex_lock(&msi->lock); + bitmap_release_region(msi->bitmap, d->hwirq, order_base_2(nr_irqs)); + mutex_unlock(&msi->lock); +} + +static const struct irq_domain_ops apple_msi_domain_ops = { + .alloc = apple_msi_domain_alloc, + .free = apple_msi_domain_free, +}; + +static const struct msi_parent_ops apple_msi_parent_ops = { + .supported_flags = (MSI_GENERIC_FLAGS_MASK | + MSI_FLAG_PCI_MSIX | + MSI_FLAG_MULTI_PCI_MSI), + .required_flags = (MSI_FLAG_USE_DEF_DOM_OPS | + MSI_FLAG_USE_DEF_CHIP_OPS | + MSI_FLAG_PCI_MSI_MASK_PARENT), + .chip_flags = MSI_CHIP_FLAG_SET_EOI, + .bus_select_token = DOMAIN_BUS_PCI_MSI, + .init_dev_msi_info = msi_lib_init_dev_msi_info, +}; + +static int apple_msi_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct fwnode_handle *fwnode = dev_fwnode(dev); + struct of_phandle_args args = {}; + struct irq_domain_info info = { + .fwnode = fwnode, + .ops = &apple_msi_domain_ops, + }; + struct apple_msi *msi; + void __iomem *port; + int ret; + u32 i; + + msi = devm_kzalloc(dev, sizeof(*msi), GFP_KERNEL); + if (!msi) + return -ENOMEM; + mutex_init(&msi->lock); + + port = devm_platform_ioremap_resource(pdev, 0); + if (IS_ERR(port)) + return PTR_ERR(port); + + ret = of_parse_phandle_with_args(dev->of_node, "msi-ranges", + "#interrupt-cells", 0, &args); + if (ret) + return ret; + ret = of_property_read_u32_index(dev->of_node, "msi-ranges", + args.args_count + 1, &msi->nvecs); + if (ret) { + of_node_put(args.np); + return ret; + } + of_phandle_args_to_fwspec(args.np, args.args, args.args_count, + &msi->fwspec); + of_node_put(args.np); + + msi->bitmap = devm_bitmap_zalloc(dev, msi->nvecs, GFP_KERNEL); + if (!msi->bitmap) + return -ENOMEM; + + info.parent = irq_find_matching_fwspec(&msi->fwspec, DOMAIN_BUS_WIRED); + if (!info.parent) + return dev_err_probe(dev, -EPROBE_DEFER, "no parent domain\n"); + info.size = msi->nvecs; + info.host_data = msi; + + /* MSI data N goes to the Nth interrupt of msi-ranges. */ + writel(lower_32_bits(DOORBELL_ADDR), port + PORT_MSIADDR); + for (i = 0; i < msi->nvecs; i++) + writel(PORT_MSIMAP_ENABLE | i, port + PORT_MSIMAP + 4 * i); + writel(readl(port + PORT_MSICFG) | PORT_MSICFG_EN, port + PORT_MSICFG); + + if (!msi_create_parent_irq_domain(&info, &apple_msi_parent_ops)) + return -ENOMEM; + dev_info(dev, "%u MSIs at %#x\n", msi->nvecs, DOORBELL_ADDR); + return 0; +} + +static const struct of_device_id apple_msi_of_match[] = { + { .compatible = "apple,t8142-pcie-msi" }, + { } +}; + +static struct platform_driver apple_msi_driver = { + .probe = apple_msi_probe, + .driver = { + .name = "pcie-apple-msi", + .of_match_table = apple_msi_of_match, + .suppress_bind_attrs = true, + }, +}; +builtin_platform_driver(apple_msi_driver); From cd0d0111443850d5d32eb260ada5fe42d013e2fd Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Fri, 30 Oct 2026 05:35:29 +0100 Subject: [PATCH 20/22] wifi: centauri: interrupt-driven data path with real MSIs The data rings were serviced by a 20 ms delayed work (24 ms at HZ=250) that drains at most 127 RX completions and reclaims the 128 TX slots per pass, which capped the J813 at ~65 Mbit/s with 0-24 ms of added latency. When the port has an MSI controller, give Control and Alpha up to 16 MSIs each (the port has 32) and kick the data worker from every MSI; Beta and ports without a controller keep the DMA mailbox. The poll stays as a fallback. The port cycle's reset clears MSICFG, MSIADDR and the MSI map, so save and restore them on every board (the RID2SID table still only with keep_port_tables). J813: iperf3 65 -> 579 Mbit/s to the host, 60 -> 451-505 Mbit/s from it, ping 15.7 -> 2.9 ms average. Signed-off-by: Thomas Glanzmann --- .../net/wireless/apple/centauri/centauri.h | 4 +- .../wireless/apple/centauri/centauri_ipc.c | 50 +++++++++++++++++++ .../wireless/apple/centauri/centauri_port.c | 29 ++++++----- .../wireless/apple/centauri/centauri_wifi.c | 9 ++++ 4 files changed, 79 insertions(+), 13 deletions(-) diff --git a/drivers/net/wireless/apple/centauri/centauri.h b/drivers/net/wireless/apple/centauri/centauri.h index c8111fe433cc5..330b9e1a207d9 100644 --- a/drivers/net/wireless/apple/centauri/centauri.h +++ b/drivers/net/wireless/apple/centauri/centauri.h @@ -47,7 +47,8 @@ struct centauri { * keep_port_tables: the J813 sequence zeroes the 16 words at port+0x3000 and the 256 words at * port+0x3800. On the J873 those are the RID2SID and MSIMAP tables that pcie-apple programmed * (PORT_T602X_RID2SID, PORT_T602X_MSIMAP); the cycle then drops function 0's stream entry and - * its DMA arrives as SID 0 (DART "BPF reject"). Such a board gets both tables written back. + * its DMA arrives as SID 0 (DART "BPF reject"). Such a board gets its RID2SID table written + * back; the MSI map and the MSI address/config are restored on every board. * link_tlv, scan_tlvs: the macOS 27.0 WLAN adapter differs from the 26.6.1 one the J813 recipe * came from. startInterface carries the three link addresses in TLV 386 (was 385), and the scan * request (command 0x1000f) carries a channel-list TLV 0x104 and an OCE TLV 0x165 after the fixed @@ -91,4 +92,5 @@ int centauri_alpha_resources(struct centauri *c, struct cen_alpha_resources *res int centauri_alpha_log_buffer(struct centauri *c, void **cpu, dma_addr_t *dma, size_t *size); int centauri_wifi_register(struct centauri *c); void centauri_wifi_unregister(struct centauri *c); +void centauri_wifi_kick(struct centauri *c); #endif diff --git a/drivers/net/wireless/apple/centauri/centauri_ipc.c b/drivers/net/wireless/apple/centauri/centauri_ipc.c index 5f88f9a4390c9..37bbeff58a288 100644 --- a/drivers/net/wireless/apple/centauri/centauri_ipc.c +++ b/drivers/net/wireless/apple/centauri/centauri_ipc.c @@ -5,6 +5,7 @@ */ #include #include +#include #include #include #include @@ -36,6 +37,7 @@ struct centauri_ipc { struct cen_buffer *secondary, *window, *cchi_rx, *cchi_tx, *debug_rx; struct cen_buffer *alpha_log; struct cen_buffer *msi[3]; + unsigned int vectors[3]; u16 cchi_head, cchi_tail; }; @@ -338,6 +340,46 @@ static int memswap_handoff(struct centauri *c, unsigned int function, const char return 0; } +static irqreturn_t function_irq(int irq, void *data) +{ + centauri_wifi_kick(data); + return IRQ_HANDLED; +} + +/* Real MSIs need an MSI controller on the port (pcie-apple, pcie-apple-msi). The port has 32 + * vectors; Control and Alpha get up to 16 each. Without a controller the function keeps the + * DMA mailbox below and the Wi-Fi data path is polled. + */ +static int request_vectors(struct centauri *c, unsigned int function) +{ + struct pci_dev *pdev = c->ipc->function[function]; + int count, i, ret; + + count = pci_alloc_irq_vectors(pdev, 1, 16, PCI_IRQ_MSI); + if (count < 0) + return count; + for (i = 0; i < count; i++) { + ret = request_irq(pci_irq_vector(pdev, i), function_irq, 0, "centauri", c); + if (ret) { + while (i--) + free_irq(pci_irq_vector(pdev, i), c); + pci_free_irq_vectors(pdev); + return ret; + } + } + c->ipc->vectors[function] = count; + return 0; +} + +static void free_vectors(struct centauri *c, unsigned int function) +{ + struct pci_dev *pdev = c->ipc->function[function]; + + while (c->ipc->vectors[function]) + free_irq(pci_irq_vector(pdev, --c->ipc->vectors[function]), c); + pci_free_irq_vectors(pdev); +} + static int enumerate_functions(struct centauri *c) { struct centauri_ipc *ipc = c->ipc; @@ -367,6 +409,11 @@ static int enumerate_functions(struct centauri *c) pci_read_config_word(ipc->function[i], cap + PCI_MSI_FLAGS, &control); if (!(control & PCI_MSI_FLAGS_64BIT)) return -EOPNOTSUPP; + if (i < 2 && !request_vectors(c, i)) { + dev_info(&c->pdev->dev, "function %d: %u MSIs\n", i, ipc->vectors[i]); + pci_set_master(ipc->function[i]); + continue; + } if (i) { ipc->msi[i] = buffer_alloc(c, i, SZ_16K); if (!ipc->msi[i]) @@ -472,6 +519,9 @@ void centauri_free_ipc(struct centauri *c) if (!ipc) return; + for (i = 0; i < 3; i++) + if (ipc->vectors[i]) + free_vectors(c, i); centauri_wifi_unregister(c); for (i = 0; i < 3; i++) { if (!ipc->function[i]) diff --git a/drivers/net/wireless/apple/centauri/centauri_port.c b/drivers/net/wireless/apple/centauri/centauri_port.c index 62a9456e74ea9..59eadc3f8521c 100644 --- a/drivers/net/wireless/apple/centauri/centauri_port.c +++ b/drivers/net/wireless/apple/centauri/centauri_port.c @@ -46,7 +46,7 @@ int centauri_port_cycle(struct pci_dev *pdev) unsigned int lanes, i; int ret = -ETIMEDOUT, pm; bool ack, l2; - u32 rid2sid[16], msimap[256], valid = 0; + u32 rid2sid[16], msimap[256], msicfg = 0, msiaddr = 0, valid = 0; bool saved = false; if (!board || !board->port || !bridge || bridge->vendor != PCI_VENDOR_ID_APPLE || @@ -71,13 +71,15 @@ int centauri_port_cycle(struct pci_dev *pdev) ret = pci_save_state(pdev); if (ret) goto out; - if (board->keep_port_tables) { + if (board->keep_port_tables) for (i = 0; i < ARRAY_SIZE(rid2sid); i++) rid2sid[i] = readl(port + 0x3000 + 4 * i); - for (i = 0; i < ARRAY_SIZE(msimap); i++) - msimap[i] = readl(port + 0x3800 + 4 * i); - saved = true; - } + /* The reset below also clears an MSI controller's setup (pcie-apple, pcie-apple-msi). */ + for (i = 0; i < ARRAY_SIZE(msimap); i++) + msimap[i] = readl(port + 0x3800 + 4 * i); + msicfg = readl(port + 0x124); + msiaddr = readl(port + 0x16c); + saved = true; pcie_capability_read_word(bridge, PCI_EXP_LNKSTA, &link); lanes = (link & PCI_EXP_LNKSTA_NLW) >> PCI_EXP_LNKSTA_NLW_SHIFT; lanes = lanes >= 1 && lanes <= 16 ? BIT(lanes) - 1 : 1; @@ -196,14 +198,17 @@ int centauri_port_cycle(struct pci_dev *pdev) ret = -ETIMEDOUT; out: if (saved) { - for (i = 0; i < ARRAY_SIZE(rid2sid); i++) { - writel(rid2sid[i], port + 0x3000 + 4 * i); - valid += !!(rid2sid[i] & BIT(31)); - } + if (board->keep_port_tables) + for (i = 0; i < ARRAY_SIZE(rid2sid); i++) { + writel(rid2sid[i], port + 0x3000 + 4 * i); + valid += !!(rid2sid[i] & BIT(31)); + } for (i = 0; i < ARRAY_SIZE(msimap); i++) writel(msimap[i], port + 0x3800 + 4 * i); - dev_info(&pdev->dev, "restored %u RID2SID entries and the MSI map (ret %d)\n", - valid, ret); + writel(msiaddr, port + 0x16c); + writel(msicfg, port + 0x124); + dev_info(&pdev->dev, "restored %u RID2SID entries and the MSI setup %#x/%#x (ret %d)\n", + valid, msicfg, msiaddr, ret); } if (pad) iounmap(pad); diff --git a/drivers/net/wireless/apple/centauri/centauri_wifi.c b/drivers/net/wireless/apple/centauri/centauri_wifi.c index df694b9c13a00..f0da841721374 100644 --- a/drivers/net/wireless/apple/centauri/centauri_wifi.c +++ b/drivers/net/wireless/apple/centauri/centauri_wifi.c @@ -1235,6 +1235,15 @@ static void link_worker(struct work_struct *work) } } +/* MSI from Control or Alpha: service the data rings now instead of at the next poll. */ +void centauri_wifi_kick(struct centauri *c) +{ + struct centauri_wifi *wifi = READ_ONCE(c->wifi); + + if (wifi && READ_ONCE(wifi->connected) && !READ_ONCE(wifi->stopping)) + mod_delayed_work(system_wq, &wifi->link_work, 0); +} + static int wifi_disconnect(struct wiphy *wiphy, struct net_device *netdev, u16 reason) { struct centauri_wifi *wifi = *(struct centauri_wifi **)wiphy_priv(wiphy); From e7025efadaaf297c4c27695270f6da05812d2b4c Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Fri, 30 Oct 2026 06:00:33 +0100 Subject: [PATCH 21/22] wifi: centauri: give Alpha a full block of 16 MSIs, ask for it first The data rings tell the firmware to signal Alpha vectors 3 and 8. On the J873 the 10GbE NIC shares pcie-apple's 32 vectors with the N1; Control asked first and took the only free block of 16, Alpha got one vector, and the Alpha firmware faulted (stage 3) as soon as the first data ring was opened, so every later command failed with -EIO. Allocate Alpha before Control and only as a whole block of 16; a function that cannot get one keeps the DMA mailbox. On the J873 Control now stays on the mailbox and Alpha gets its 16 vectors. J873 (Mac mini M6, 5 GHz ch 44, -34 dBm): iperf3 60 -> 511 Mbit/s to the mini, 60 -> 530 Mbit/s from it, ping 17 -> 2.4 ms average. Signed-off-by: Thomas Glanzmann --- drivers/net/wireless/apple/centauri/centauri_ipc.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/drivers/net/wireless/apple/centauri/centauri_ipc.c b/drivers/net/wireless/apple/centauri/centauri_ipc.c index 37bbeff58a288..4c26c70853815 100644 --- a/drivers/net/wireless/apple/centauri/centauri_ipc.c +++ b/drivers/net/wireless/apple/centauri/centauri_ipc.c @@ -346,16 +346,17 @@ static irqreturn_t function_irq(int irq, void *data) return IRQ_HANDLED; } -/* Real MSIs need an MSI controller on the port (pcie-apple, pcie-apple-msi). The port has 32 - * vectors; Control and Alpha get up to 16 each. Without a controller the function keeps the - * DMA mailbox below and the Wi-Fi data path is polled. +/* Real MSIs need an MSI controller on the port (pcie-apple, pcie-apple-msi). The data rings + * name Alpha vectors 3 and 8, so a function gets all 16 vectors or none; one that gets none + * keeps the DMA mailbox below (polled). Alpha asks first: on the J873 the 10GbE NIC shares + * the controller's 32 vectors and only one block of 16 is left. */ static int request_vectors(struct centauri *c, unsigned int function) { struct pci_dev *pdev = c->ipc->function[function]; int count, i, ret; - count = pci_alloc_irq_vectors(pdev, 1, 16, PCI_IRQ_MSI); + count = pci_alloc_irq_vectors(pdev, 16, 16, PCI_IRQ_MSI); if (count < 0) return count; for (i = 0; i < count; i++) { @@ -392,7 +393,7 @@ static int enumerate_functions(struct centauri *c) c->pdev->multifunction = 1; pci_rescan_bus(c->pdev->bus); pci_unlock_rescan_remove(); - for (i = 0; i < 3; i++) { + for (i = 2; i >= 0; i--) { ipc->function[i] = pci_get_domain_bus_and_slot(pci_domain_nr(c->pdev->bus), c->pdev->bus->number, PCI_DEVFN(0, i)); if (!ipc->function[i] || ipc->function[i]->vendor != PCI_VENDOR_ID_APPLE || From 545391fb966ed823fb89b372800856b7244f0acc Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 1 Nov 2026 21:33:34 +0100 Subject: [PATCH 22/22] dt-bindings: interrupt-controller: Add the Apple PCIe port MSI map The MSI map of one Apple PCIe root port that the bootloader trained and Linux drives as a generic ECAM host (the J813 N1 on T8142 apcie port 0). Signed-off-by: Thomas Glanzmann --- .../interrupt-controller/apple,pcie-msi.yaml | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Documentation/devicetree/bindings/interrupt-controller/apple,pcie-msi.yaml diff --git a/Documentation/devicetree/bindings/interrupt-controller/apple,pcie-msi.yaml b/Documentation/devicetree/bindings/interrupt-controller/apple,pcie-msi.yaml new file mode 100644 index 0000000000000..e08ca3bf7108a --- /dev/null +++ b/Documentation/devicetree/bindings/interrupt-controller/apple,pcie-msi.yaml @@ -0,0 +1,57 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/interrupt-controller/apple,pcie-msi.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple PCIe port MSI controller + +maintainers: + - Thomas Glanzmann + +description: | + The MSI map of one Apple PCIe root port, for a port that the bootloader + trained and that Linux drives as a generic ECAM host. Endpoint MSI writes + to the doorbell are routed by this map to a contiguous range of AIC + interrupts. + +allOf: + - $ref: /schemas/interrupt-controller/msi-controller.yaml# + +properties: + compatible: + const: apple,t8142-pcie-msi + + reg: + maxItems: 1 + description: The port's MSI map registers. + + msi-controller: true + + msi-ranges: + maxItems: 1 + +required: + - compatible + - reg + - msi-controller + - msi-ranges + +unevaluatedProperties: false + +examples: + - | + #include + #include + + soc { + #address-cells = <2>; + #size-cells = <2>; + + msi-controller@488028000 { + compatible = "apple,t8142-pcie-msi"; + reg = <0x4 0x88028000 0x0 0x8000>; + msi-controller; + msi-ranges = <&aic AIC_IRQ 1741 IRQ_TYPE_EDGE_RISING 32>; + }; + };