diff --git a/MAINTAINERS.yml b/MAINTAINERS.yml index 3d2fb75f9bd2d..2bc0a9c8aea64 100644 --- a/MAINTAINERS.yml +++ b/MAINTAINERS.yml @@ -2878,6 +2878,18 @@ Documentation Infrastructure: tests: - drivers.net.wifi +"Drivers: Wi-Fi as Broadcom brcmfmac": + status: maintained + maintainers: + - jetpax + collaborators: + - jukkar + files: + - drivers/wifi/brcmfmac/ + - dts/bindings/wifi/brcm,bcm43xxx-sdio.yaml + labels: + - "area: Wi-Fi" + "Drivers: Wi-Fi as nRF Wi-Fi": status: maintained maintainers: diff --git a/drivers/wifi/CMakeLists.txt b/drivers/wifi/CMakeLists.txt index 1c6ae664e0f1c..461f9aa14d595 100644 --- a/drivers/wifi/CMakeLists.txt +++ b/drivers/wifi/CMakeLists.txt @@ -20,6 +20,7 @@ endif() # CONFIG_BUILD_ONLY_NO_BLOBS # zephyr-keep-sorted-start add_subdirectory_ifdef(CONFIG_WIFI_AIROC infineon) +add_subdirectory_ifdef(CONFIG_WIFI_BRCMFMAC brcmfmac) add_subdirectory_ifdef(CONFIG_WIFI_ESP32 esp32) add_subdirectory_ifdef(CONFIG_WIFI_ESP_AT esp_at) add_subdirectory_ifdef(CONFIG_WIFI_ESP_HOSTED esp_hosted) diff --git a/drivers/wifi/Kconfig b/drivers/wifi/Kconfig index a94261e7b4420..069b1200efeeb 100644 --- a/drivers/wifi/Kconfig +++ b/drivers/wifi/Kconfig @@ -17,6 +17,7 @@ source "subsys/net/Kconfig.template.log_config.net" config WIFI_INIT_PRIORITY int "Wi-Fi driver init priority" default 86 if WIFI_AIROC && AIROC_WIFI_BUS_SDIO + default 86 if WIFI_BRCMFMAC default 80 help Wi-Fi device driver initialization priority. @@ -37,6 +38,7 @@ config WIFI_USE_NATIVE_NETWORKING to use native ethernet stack interface. # zephyr-keep-sorted-start +source "drivers/wifi/brcmfmac/Kconfig.brcmfmac" source "drivers/wifi/esp32/Kconfig.esp32" source "drivers/wifi/esp_at/Kconfig.esp_at" source "drivers/wifi/esp_hosted/Kconfig.esp_hosted" diff --git a/drivers/wifi/brcmfmac/CMakeLists.txt b/drivers/wifi/brcmfmac/CMakeLists.txt new file mode 100644 index 0000000000000..4a59b705fea23 --- /dev/null +++ b/drivers/wifi/brcmfmac/CMakeLists.txt @@ -0,0 +1,54 @@ +# Copyright (c) 2026 Jonathan Elliot Peace +# SPDX-License-Identifier: Apache-2.0 + +if(CONFIG_WIFI_BRCMFMAC) + +zephyr_library() +zephyr_library_sources( + brcmfmac_core.c + brcmfmac_chip.c + brcmfmac_sdio.c + brcmfmac_bcdc.c + brcmfmac_net.c + brcmfmac_fw_blob.c +) + +set(brcmfmac_gen_dir ${ZEPHYR_BINARY_DIR}/include/generated/brcmfmac) +zephyr_library_include_directories(${brcmfmac_gen_dir}) + +if(CONFIG_BUILD_ONLY_NO_BLOBS) + # CI build path: hal_broadcom may not be present and blobs are not + # fetched. Emit empty .inc files so brcmfmac_fw_blob.c compiles into + # zero-length arrays; the driver will refuse to upload at runtime, + # which is the expected failure mode for a no-blobs build. + file(WRITE ${brcmfmac_gen_dir}/brcmfmac_fw.inc "") + file(WRITE ${brcmfmac_gen_dir}/brcmfmac_nvram.inc "") + file(WRITE ${brcmfmac_gen_dir}/brcmfmac_clm_blob.inc "") +else() + # Pull the firmware and per-board NVRAM from hal_broadcom (fetched + # via `west blobs fetch hal_broadcom`) and convert each into a + # comma-separated byte sequence that brcmfmac_fw_blob.c #include's + # as the body of a const array. + zephyr_blobs_verify(MODULE hal_broadcom REQUIRED) + set(brcmfmac_blob_dir + ${ZEPHYR_HAL_BROADCOM_MODULE_DIR}/zephyr/blobs/img/brcmfmac) + generate_inc_file_for_target(${ZEPHYR_CURRENT_LIBRARY} + ${brcmfmac_blob_dir}/${CONFIG_WIFI_BRCMFMAC_FIRMWARE_FILE} + ${brcmfmac_gen_dir}/brcmfmac_fw.inc) + generate_inc_file_for_target(${ZEPHYR_CURRENT_LIBRARY} + ${brcmfmac_blob_dir}/${CONFIG_WIFI_BRCMFMAC_NVRAM_FILE} + ${brcmfmac_gen_dir}/brcmfmac_nvram.inc) + + # CLM blob is optional: a board that needs it sets + # CONFIG_WIFI_BRCMFMAC_CLM_FILE in its defconfig; an unset/empty + # string links a zero-length array and the driver skips upload. + if(CONFIG_WIFI_BRCMFMAC_CLM_FILE STREQUAL "") + file(WRITE ${brcmfmac_gen_dir}/brcmfmac_clm_blob.inc "") + else() + generate_inc_file_for_target(${ZEPHYR_CURRENT_LIBRARY} + ${brcmfmac_blob_dir}/${CONFIG_WIFI_BRCMFMAC_CLM_FILE} + ${brcmfmac_gen_dir}/brcmfmac_clm_blob.inc) + endif() +endif() + +endif() diff --git a/drivers/wifi/brcmfmac/Kconfig.brcmfmac b/drivers/wifi/brcmfmac/Kconfig.brcmfmac new file mode 100644 index 0000000000000..9fbb4c0d6581a --- /dev/null +++ b/drivers/wifi/brcmfmac/Kconfig.brcmfmac @@ -0,0 +1,66 @@ +# Copyright (c) 2026 Jonathan Elliot Peace +# SPDX-License-Identifier: Apache-2.0 + +menuconfig WIFI_BRCMFMAC + bool "Broadcom BCM43xxx SDIO Wi-Fi (brcmfmac)" + depends on DT_HAS_BRCM_BCM43XXX_SDIO_ENABLED + default y + select WIFI_USE_NATIVE_NETWORKING + select NET_L2_ETHERNET + select NET_L2_WIFI_MGMT + select SDHC + select SDIO_STACK + select GPIO + help + Native-L2 driver for SDIO-attached Broadcom/Cypress combo chips + speaking the brcmfmac SDPCM+BCDC protocol. The chip handles + 802.11; Zephyr's ethernet/IP stack runs on top. Firmware and + per-board NVRAM are supplied by the hal_broadcom module via + `west blobs fetch hal_broadcom`. + + When using samples/net/wifi/shell, raise + CONFIG_NET_MGMT_EVENT_STACK_SIZE to 4096 -- the 768-byte default + overflows on scan-result formatting (each AP raises a net_mgmt + event whose handler prints a row). + +if WIFI_BRCMFMAC + +config WIFI_BRCMFMAC_FIRMWARE_FILE + string "Firmware file name (under hal_broadcom blobs)" + default "brcmfmac43436s-sdio.bin" + help + Name of the brcmfmac firmware blob under + ${ZEPHYR_HAL_BROADCOM_MODULE_DIR}/zephyr/blobs/img/brcmfmac/. + Set by the board defconfig to match the on-board chip. + +config WIFI_BRCMFMAC_NVRAM_FILE + string "NVRAM file name (under hal_broadcom blobs)" + default "brcmfmac43436s-sdio.txt" + help + Name of the brcmfmac per-board NVRAM blob under + ${ZEPHYR_HAL_BROADCOM_MODULE_DIR}/zephyr/blobs/img/brcmfmac/. + Encodes board-specific antenna, calibration and PA settings. + +config WIFI_BRCMFMAC_CLM_FILE + string "CLM blob file name (under hal_broadcom blobs)" + default "" + help + Optional Country Locale Matrix blob under + ${ZEPHYR_HAL_BROADCOM_MODULE_DIR}/zephyr/blobs/img/brcmfmac/. + Required by chips whose firmware ships without built-in regulatory + tables (e.g. BCM43458F); chips that have them (e.g. BCM43430A1) + still accept the upload harmlessly. Leave empty to skip CLM upload + entirely -- the driver links a zero-length blob in that case. + +config WIFI_BRCMFMAC_RX_THREAD_STACK_SIZE + int "RX thread stack size" + default 4096 + help + Stack for the dedicated RX polling thread that drains F2 and + dispatches frames by SDPCM channel. + +config WIFI_BRCMFMAC_RX_THREAD_PRIO + int "RX thread priority" + default 4 + +endif # WIFI_BRCMFMAC diff --git a/drivers/wifi/brcmfmac/brcmfmac_bcdc.c b/drivers/wifi/brcmfmac/brcmfmac_bcdc.c new file mode 100644 index 0000000000000..f126e9629a226 --- /dev/null +++ b/drivers/wifi/brcmfmac/brcmfmac_bcdc.c @@ -0,0 +1,1062 @@ +/* + * Copyright (c) 2026 Jonathan Elliot Peace + * SPDX-License-Identifier: Apache-2.0 + * + * BCDC (Broadcom Comm Driver Codec) protocol over SDPCM/SDIO F2. + * + * dedicated RX kthread drains F2 and dispatches frames + * by SDPCM channel: + * chan=0 (control) -> match by reqid, copy payload into the + * waiting query_dcmd caller's buffer, signal + * its completion semaphore. + * chan=1 (event) -> raw frame body handed to the registered + * event callback (if any). + * chan=2 (data) -> logged + discarded (net_if arrives in 4.5). + * + * query_dcmd is now async at the protocol level: TX, then block on + * a per-request semaphore until the RX thread signals or timeout. + * F2's CIS rdy_timeout is overridden to 0 so sdio_enable_func polls + * IOR directly instead of sleeping ~2 s per chip CIS request. + */ + +#include + +#include +#include +#include +#include +#include + +#include "brcmfmac_priv.h" + +LOG_MODULE_DECLARE(brcmfmac, CONFIG_WIFI_LOG_LEVEL); + +/* RX buffer for the two-phase CMD53 read in `brcmfmac_rx_thread_fn`. + * Must hold the largest SDPCM frame the chip will deliver in a single + * F2 FIFO read. Empirically we've seen ~4.4 KB aggregated + * frames during a TCP-fed bulk upload; SDPCM headers, BDC headers, + * Ethernet header, and any A-MSDU framing push the worst case above + * what we initially predicted. 8192 covers everything observed in + * practice with comfortable slack. If `fh->len > sizeof(rx_buf)` ever + * trips again, the right move is dynamic net_pkt allocation rather + * than another static bump. + * + * History: was 512 -> truncated frames >~470 B, surfaced as TCP + * checksum drops upstream. Bumped to 2048 -> exposed CMD53 overread + * DATA_CRC () which forced the two-phase split. + * Then sized to 8192 here to absorb chip-side aggregation. + */ +#define BRCMFMAC_BCDC_RX_BUF 8192 +#define BRCMFMAC_BCDC_TIMEOUT_MS 2000 +#define BRCMFMAC_RX_IDLE_SLEEP_MS 1 + +/* Single-instance stack + thread storage. DT only describes one + * brcmfmac node today; if multi-instance is wanted later, move + * these into a per-instance macro expansion in core.c. + */ +K_KERNEL_STACK_DEFINE(brcmfmac_rx_stack, CONFIG_WIFI_BRCMFMAC_RX_THREAD_STACK_SIZE); +static struct k_thread brcmfmac_rx_thread; + +K_KERNEL_STACK_DEFINE(brcmfmac_tx_stack, 4096); +static struct k_thread brcmfmac_tx_thread; + +static void brcmfmac_tx_thread_fn(void *p1, void *p2, void *p3); + +/* Counter for periodic ISR-rate logging from the RX thread. */ +static atomic_t brcmf_isr_fires; + +/* SDPCM per-frame flow-control state. + * + * The chip puts a flow-control byte at sw->flow in every SDPCM header it + * sends. Each bit corresponds to an access category (BE/BK/VO/VI); a set + * bit means "host should stop sending traffic on this AC." Mirrors Linux + * brcmf_sdio_hdparse + TX gate at sdio.c:2911. + * + * Simplification vs Linux: we collapse all ACs to a single xoff/xon + * boolean. Any non-zero sw->flow stops all host TX; zero re-enables. + * Fine-grained per-AC was overkill for the current single-iface workload. + */ +static atomic_t brcmf_fcstate; /* 0 = ok to send, 1 = chip xoff'd */ +static uint8_t brcmf_flowcontrol; /* last sw->flow byte seen */ + +static const char *const brcmfmac_bcme[] = { + "BCME_OK", + "BCME_ERROR", + "BCME_BADARG", + "BCME_BADOPTION", + "BCME_NOTUP", + "BCME_NOTDOWN", + "BCME_NOTAP", + "BCME_NOTSTA", + "BCME_BADKEYIDX", + "BCME_RADIOOFF", + "BCME_NOTBANDLOCKED", + "BCME_NOCLK", + "BCME_BADRATESET", + "BCME_BADBAND", + "BCME_BUFTOOSHORT", + "BCME_BUFTOOLONG", + "BCME_BUSY", + "BCME_NOTASSOCIATED", + "BCME_BADSSIDLEN", + "BCME_OUTOFRANGECHAN", + "BCME_BADCHAN", + "BCME_BADADDR", + "BCME_NORESOURCE", + "BCME_UNSUPPORTED", + "BCME_BADLEN", + "BCME_NOTREADY", + "BCME_EPERM", + "BCME_NOMEM", + "BCME_ASSOCIATED", + "BCME_RANGE", + "BCME_NOTFOUND", + "BCME_WME_NOT_ENABLED", + "BCME_TSPEC_NOTFOUND", + "BCME_ACM_NOTSUPPORTED", + "BCME_NOT_WME_ASSOCIATION", + "BCME_SDIO_ERROR", + "BCME_DONGLE_DOWN", + "BCME_VERSION", + "BCME_TXFAIL", + "BCME_RXFAIL", + "BCME_NODEVICE", + "BCME_NMODE_DISABLED", + "BCME_NONRESIDENT", + "BCME_SCANREJECT", + "BCME_USAGE_ERROR", + "BCME_IOCTL_ERROR", + "BCME_SERIAL_PORT_ERR", + "BCME_DISABLED", + "BCME_DECERR", + "BCME_ENCERR", + "BCME_MICERR", + "BCME_REPLAY", + "BCME_IE_NOTFOUND", +}; + +static const char *brcmfmac_bcme_str(int error) +{ + error = -error; + if (error < 0 || error >= ARRAY_SIZE(brcmfmac_bcme)) { + return "unknown"; + } + + return brcmfmac_bcme[error]; +} + +static void brcmfmac_handle_ctrl(struct brcmfmac_data *data, + struct cdc_hdr *rcdc, uint16_t outlen) +{ + uint16_t reqid = (uint16_t)(rcdc->flags >> BCDC_REQ_ID_SHIFT); + + if (!data->pending.active || reqid != data->pending.reqid) { + LOG_DBG("rx ctrl: unmatched reqid=%u (pending.active=%d pending.reqid=%u)", + reqid, data->pending.active, data->pending.reqid); + return; + } + + if (rcdc->flags & BCDC_FLAG_ERROR) { + LOG_WRN("rx ctrl: chip BCDC error (status=%s reqid=%u)", + brcmfmac_bcme_str(rcdc->status), reqid); + data->pending.status = -EIO; + data->pending.out_copied = 0; + } else { + uint16_t want = outlen; + + if (want > data->pending.out_capacity) { + want = data->pending.out_capacity; + } + if (want > 0 && data->pending.out_buf != NULL) { + memcpy(data->pending.out_buf, + (uint8_t *)rcdc + sizeof(*rcdc), want); + } + data->pending.out_copied = want; + data->pending.status = 0; + } + + k_sem_give(&data->pending.done); +} + +/* Strip SDPCM headers, hand the body (BDC + L2 frame, or BDC + event frame) + * to the net layer. Both chan=1 (event) and chan=2 (data) use this shape. + */ +static const uint8_t *brcmfmac_rx_body(struct sdpcm_sw_hdr *sw, + uint16_t total_len, uint16_t *body_len_out) +{ + uint16_t hdr_len = sw->hdrlen; + + if (hdr_len > total_len) { + LOG_WRN("rx: hdrlen=%u > total=%u", hdr_len, total_len); + return NULL; + } + *body_len_out = (uint16_t)(total_len - hdr_len); + return (const uint8_t *)sw + (hdr_len - sizeof(struct sdpcm_frame_hdr)); +} + +/* SDHC SDIO_INT callback: chip asserted DAT1 (CARD_INT). Wake the RX + * thread. Runs in ISR context; keep it minimal. The SDHC driver auto- + * masks SIGNAL_ENABLE[CARD_INT] before invoking the callback, so we + * re-arm via sdhc_enable_interrupt() in brcmfmac_rx_wait() below. + */ +static void brcmfmac_card_int_cb(const struct device *sdhc_dev, int reason, + const void *user_data) +{ + ARG_UNUSED(sdhc_dev); + struct brcmfmac_data *data = (struct brcmfmac_data *)user_data; + + if (reason == SDHC_INT_SDIO) { + atomic_inc(&brcmf_isr_fires); + k_sem_give(&data->rx_irq_sem); + } +} + +/* Block on the SDIO_INT semaphore with a short safety-net timeout. + * The k_sem_take timeout caps the worst-case latency when an SDIO + * IRQ is missed; 1 ms keeps DHCP-class round-trips healthy. + */ +static void brcmfmac_rx_wait(struct brcmfmac_data *data) +{ + (void)sdhc_enable_interrupt(data->card.sdhc, brcmfmac_card_int_cb, + SDHC_INT_SDIO, data); + (void)k_sem_take(&data->rx_irq_sem, K_MSEC(1)); +} + +static void brcmfmac_rx_thread_fn(void *p1, void *p2, void *p3) +{ + struct brcmfmac_data *data = p1; + + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + static uint8_t rx_buf[BRCMFMAC_BCDC_RX_BUF] __aligned(4); + static uint32_t rx_iters; + static uint32_t rx_valid; + static uint32_t rx_ist_acc; /* OR of masked SDPCMD_INTSTATUS this window */ + + LOG_DBG("rx thread started"); + + while (1) { + rx_iters++; + if ((rx_iters & 0x3FF) == 0) { + LOG_DBG("rx: t=%lldms iters=%u isr=%ld valid=%u", + k_uptime_get(), rx_iters, + (long)atomic_get(&brcmf_isr_fires), rx_valid); + LOG_DBG("rx: ist=0x%x txseq=%u tx_max=%u", + rx_ist_acc, data->sdpcm_txseq, + data->sdpcm_tx_max); + rx_ist_acc = 0; + } + + /* Ack the chip-side SDPCMD intstatus BEFORE the speculative + * F2 read. Matches Linux's sdio_irq_thread / + * brcmf_sdio_intr_rstatus ordering: clear the latched HMB_SW + * bits as soon as the host has been notified, so DAT1 can + * deassert and the SDHC card-int line doesn't re-fire on an + * empty F2 (which would otherwise storm the ISR every time + * brcmfmac_rx_wait re-arms SIGNAL_ENABLE[CARD_INT]). + */ + { + const struct bcm_core *sdio_core = + brcmfmac_chip_core_find(data, BCMA_CORE_SDIO_DEV); + if (sdio_core != NULL) { + uint32_t ist = 0; + + if (brcmfmac_sdio_backplane_read32( + data, + sdio_core->base + SDPCMD_INTSTATUS, + &ist) == 0) { + uint32_t masked = + ist & BRCMFMAC_HOSTINTMASK; + + rx_ist_acc |= masked; + if (masked != 0) { + (void)brcmfmac_sdio_backplane_write32( + data, + sdio_core->base + + SDPCMD_INTSTATUS, + masked); + } + } + } + } + + /* Read one block first to learn the frame size. + * + * The CYW43439 F2 FIFO only yields the bytes it actually has + * queued. A multi-block CMD53 that requests more blocks than + * the chip has buffered comes back with a DATA_CRC error from + * the SDHCI controller -- the chip stops driving the bus before + * the requested block count is satisfied. So we can't issue a + * speculative max-MTU read up front; we have to size each + * transfer to what's actually available. + * + * The first block always contains the SDPCM frame header at + * offset 0 (fh->len + fh->notlen XOR check). Once we have + * that, fh->len tells us the total frame size -- including + * the bytes we already read in this first block. If more + * blocks are needed, phase 2 reads them in a single follow-up + * CMD53 sized to the exact remaining block count. + * + * Linux brcmfmac avoids the second CMD53 by carrying + * sw->nextlen forward as a hint for the *next* frame's first + * read. That optimisation is parked (see project memory + * `project_rpi_zero_2w_parking_lot.md` item 9). + */ + int ret = sdio_read_addr(&data->radio, BRCMFMAC_F2_FIFO_ADDR, + rx_buf, BRCMFMAC_F2_BLOCK_SIZE); + if (ret != 0) { + LOG_DBG("rx thread: F2 read failed: %d", ret); + brcmfmac_rx_wait(data); + continue; + } + + struct sdpcm_frame_hdr *fh = (void *)rx_buf; + struct sdpcm_sw_hdr *sw = (void *)(rx_buf + sizeof(*fh)); + + uint16_t xor_check = (uint16_t)(fh->len ^ fh->notlen); + + if (xor_check != 0xFFFFu) { + /* No valid frame queued -- chip's F2 returned junk. */ + brcmfmac_rx_wait(data); + continue; + } + if (fh->len < sizeof(*fh) + sizeof(*sw)) { + /* Header-only frame -- chip flow control / credit + * signaling. Linux's brcmfmac handles these silently. + */ + LOG_DBG("rx thread: short frame (len=%u)", fh->len); + brcmfmac_rx_wait(data); + continue; + } + + /* Update tx-window from the chip. Mirrors Linux's + * brcmf_sdio_hdparse: every valid SDPCM header carries the + * chip's current tx_seq_max in sw->credit. Clamp to seq+2 if + * the window is implausibly far ahead (>0x40 of our txseq) -- + * Linux does the same to defend against firmware bugs. Then + * wake any TX path waiting on credits. + */ + { + uint8_t new_max = sw->credit; + + if ((uint8_t)(new_max - data->sdpcm_txseq) > 0x40) { + new_max = data->sdpcm_txseq + 2; + } + data->sdpcm_tx_max = new_max; + k_sem_give(&data->tx_credit_sem); + rx_valid++; + + /* SDPCM per-frame flow-control byte. Mirrors Linux + * brcmf_sdio_hdparse: when the chip's host-RX queue + * fills it sets bits in sw->flow telling host to stop + * sending. Without this we overrun the chip's WLAN TX + * queue and trip a multi-second internal recovery. + */ + uint8_t new_fc = sw->flow; + + if (brcmf_flowcontrol != new_fc) { + bool now_xoff = new_fc != 0; + + brcmf_flowcontrol = new_fc; + atomic_set(&brcmf_fcstate, now_xoff ? 1 : 0); + LOG_DBG("fc: %s flow=0x%02x", + now_xoff ? "xoff" : "xon", new_fc); + } + } + + /* If the frame spans more than one block, read the + * remaining blocks in a single follow-up CMD53. fh->len is + * total frame length; we already have the first + * BRCMFMAC_F2_BLOCK_SIZE bytes. Round up to the next block. + */ + if (fh->len > BRCMFMAC_F2_BLOCK_SIZE) { + uint16_t remaining = fh->len - BRCMFMAC_F2_BLOCK_SIZE; + uint16_t extra_blocks = + (remaining + BRCMFMAC_F2_BLOCK_SIZE - 1) / + BRCMFMAC_F2_BLOCK_SIZE; + size_t extra_bytes = (size_t)extra_blocks * + BRCMFMAC_F2_BLOCK_SIZE; + if (BRCMFMAC_F2_BLOCK_SIZE + extra_bytes > + sizeof(rx_buf)) { + /* Frame too big for rx_buf. Drain the rest of + * the frame from the FIFO in rx_buf-sized + * chunks so the next read starts on a clean + * frame boundary -- otherwise we'd interpret + * mid-frame bytes as a new SDPCM header and + * either crash or feed garbage upstream. + */ + LOG_WRN("rx: frame too big (len=%u, max=%zu) -- draining", + fh->len, sizeof(rx_buf)); + size_t to_drain = extra_bytes; + + while (to_drain > 0) { + size_t chunk = to_drain < sizeof(rx_buf) + ? to_drain + : sizeof(rx_buf); + int dret = sdio_read_addr( + &data->radio, + BRCMFMAC_F2_FIFO_ADDR, + rx_buf, chunk); + if (dret != 0) { + LOG_WRN("rx: drain failed (%d), FIFO desynced", + dret); + break; + } + to_drain -= chunk; + } + continue; + } + ret = sdio_read_addr(&data->radio, + BRCMFMAC_F2_FIFO_ADDR, + rx_buf + BRCMFMAC_F2_BLOCK_SIZE, + extra_bytes); + if (ret != 0) { + LOG_WRN("rx: phase-2 read failed (len=%u, ret=%d)", + fh->len, ret); + brcmfmac_rx_wait(data); + continue; + } + } + + switch (sw->chan) { + case SDPCM_CHAN_CTRL: { + if (fh->len < sizeof(*fh) + sizeof(*sw) + sizeof(struct cdc_hdr)) { + LOG_WRN("rx ctrl: undersized (len=%u)", fh->len); + break; + } + struct cdc_hdr *rcdc = (void *)((uint8_t *)sw + sizeof(*sw)); + uint16_t payload_avail = (uint16_t)(fh->len - sizeof(*fh) + - sizeof(*sw) + - sizeof(*rcdc)); + uint16_t outlen = rcdc->outlen; + + if (outlen > payload_avail) { + outlen = payload_avail; + } + brcmfmac_handle_ctrl(data, rcdc, outlen); + break; + } + case SDPCM_CHAN_EVENT: { + uint16_t body_len; + const uint8_t *body = brcmfmac_rx_body(sw, fh->len, &body_len); + + if (body != NULL) { + brcmfmac_net_rx_event(data, body, body_len); + } + break; + } + case SDPCM_CHAN_DATA: { + uint16_t body_len; + const uint8_t *body = brcmfmac_rx_body(sw, fh->len, &body_len); + + if (body != NULL) { + brcmfmac_net_rx_data(data, body, body_len); + } + break; + } + default: + LOG_DBG("rx: unknown chan=%u len=%u", sw->chan, fh->len); + break; + } + } +} + +/* Enable F2 + wait for IOR ourselves. Zephyr's sdio_enable_func sleeps + * for the full CIS-advertised rdy_timeout (~2 s on this chip) before + * its first IOR poll; setting rdy_timeout=0 just makes it poll too + * fast (CONFIG_SD_RETRY_COUNT x ~0 ms = ~3 ms total, chip is not + * ready that quickly). Linux brcmfmac polls IOR with ~10 ms gaps up + * to ~500 ms; we mirror that. + */ +static int brcmfmac_bcdc_enable_f2(struct brcmfmac_data *data) +{ + uint8_t reg; + int ret = sdio_read_byte(&data->card.func0, SDIO_CCCR_IO_EN, ®); + + if (ret != 0) { + return ret; + } + reg |= BIT(SDIO_FUNC_NUM_2); + ret = sdio_write_byte(&data->card.func0, SDIO_CCCR_IO_EN, reg); + if (ret != 0) { + return ret; + } + + int64_t t0 = k_uptime_get(); + + for (int i = 0; i < 50; i++) { + ret = sdio_read_byte(&data->card.func0, SDIO_CCCR_IO_RD, ®); + if (ret != 0) { + return ret; + } + if (reg & BIT(SDIO_FUNC_NUM_2)) { + LOG_DBG("F2 IOR up after %lld ms", + (long long)(k_uptime_get() - t0)); + return 0; + } + k_msleep(10); + } + LOG_ERR("F2 IOR timeout after %lld ms (IOR=0x%02x)", + (long long)(k_uptime_get() - t0), reg); + return -ETIMEDOUT; +} + +int brcmfmac_bcdc_init(struct brcmfmac_data *data) +{ + int ret = sdio_init_func(&data->card, &data->radio, SDIO_FUNC_NUM_2); + + if (ret != 0) { + LOG_ERR("sdio_init_func(F2) failed: %d", ret); + return ret; + } + + ret = brcmfmac_bcdc_enable_f2(data); + if (ret != 0) { + LOG_ERR("F2 enable failed: %d", ret); + return ret; + } + + ret = sdio_set_block_size(&data->radio, BRCMFMAC_F2_BLOCK_SIZE); + if (ret != 0) { + LOG_ERR("sdio_set_block_size(F2, %u) failed: %d", + BRCMFMAC_F2_BLOCK_SIZE, ret); + return ret; + } + + k_mutex_init(&data->bcdc_mutex); + k_sem_init(&data->pending.done, 0, 1); + data->pending.active = false; + + /* Tell the chip which intstatus bits should assert DAT1. Without this + * the chip's SDIO core stays silent on the bus even when it has frames + * queued -- our SDHC CARD_INT path would never see an assert, and the + * RX thread would sleep at its safety-net timeout forever. Linux does + * the same write in brcmf_sdio_init right after F2 is up. + */ + { + const struct bcm_core *sdio_core = + brcmfmac_chip_core_find(data, BCMA_CORE_SDIO_DEV); + + if (sdio_core != NULL) { + int rc2 = brcmfmac_sdio_backplane_write32( + data, sdio_core->base + SDPCMD_HOSTINTMASK, + BRCMFMAC_HOSTINTMASK); + if (rc2 != 0) { + LOG_WRN("hostintmask write failed: %d (CARD_INT path will be slow)", + rc2); + } else { + LOG_DBG("chip hostintmask = 0x%08x", + BRCMFMAC_HOSTINTMASK); + } + } + } + + /* Enable the SDIO card's per-function interrupt output via CCCR IENx + * (F0 register 0x04). Bit 0 = master enable (IENM); bit N = function N + * IRQ enable. Without this the chip's SDIO core can have HMB bits set + * but won't actually pull DAT1 low to interrupt the host. Mirrors + * Linux's brcmf_sdiod_intr_register (brcmfmac/bcmsdh.c:144-147), which + * enables FUNC0 | FUNC1 | FUNC2: the chip's SDPCMD core sits on F1, so + * HMB frame-ready signals are F1-level IRQs -- enabling F2 alone + * leaves DAT1 silent even with HOSTINTMASK + intstatus set. + */ + { + uint8_t ienx = 0; + int rc2 = sdio_read_byte(&data->card.func0, + SDIO_CCCR_INT_EN, &ienx); + if (rc2 == 0) { + ienx |= 0x01; /* IENM master */ + ienx |= 1u << SDIO_FUNC_NUM_1; /* F1 (SDPCMD/HMB) */ + ienx |= 1u << SDIO_FUNC_NUM_2; /* F2 (data path) */ + rc2 = sdio_write_byte(&data->card.func0, + SDIO_CCCR_INT_EN, ienx); + } + if (rc2 != 0) { + LOG_WRN("CCCR IENx setup failed: %d (CARD_INT will not fire)", + rc2); + } else { + LOG_DBG("CCCR IENx = 0x%02x (master + F1 SDPCMD + F2 data)", + ienx); + } + } + + /* SDPCM tx-credit accounting (mirrors Linux brcmfmac/sdio.c). + * Initial window of 4 lets the first 4 outgoing frames fly before + * we need the chip to report its tx_max in an RX SDPCM header. + */ + data->sdpcm_txseq = 0; + data->sdpcm_tx_max = 4; + k_sem_init(&data->tx_credit_sem, 0, 1); + + /* SDIO in-band IRQ -- replaces the old 1 ms RX poll. The SDHC + * driver auto-masks SIGNAL_ENABLE[CARD_INT] each time the ISR + * fires; brcmfmac_rx_wait() re-arms via sdhc_enable_interrupt() + * (idempotent). First call from the rx thread also performs the + * initial enable. + */ + k_sem_init(&data->rx_irq_sem, 0, 1); + + k_thread_create(&brcmfmac_rx_thread, brcmfmac_rx_stack, + K_KERNEL_STACK_SIZEOF(brcmfmac_rx_stack), + brcmfmac_rx_thread_fn, data, NULL, NULL, + CONFIG_WIFI_BRCMFMAC_RX_THREAD_PRIO, 0, K_NO_WAIT); + k_thread_name_set(&brcmfmac_rx_thread, "brcmfmac_rx"); + + /* TX glom ring + worker. iface_send copies each net frame into a + * pre-allocated slot and signals tx_pending_sem; the tx-thread + * drains the ring, packs up to N frames into tx_glom_buf, and + * issues one CMD53 per burst. + */ + atomic_set(&data->tx_ring_head, 0); + atomic_set(&data->tx_ring_tail, 0); + k_sem_init(&data->tx_pending_sem, 0, 1); + for (int i = 0; i < BRCMFMAC_TX_RING_SLOTS; i++) { + data->tx_ring[i].len = 0; + } + + /* TX thread at one priority LOWER than RX (CONFIG_WIFI_BRCMFMAC_RX_THREAD_PRIO + 1). + * RX must preempt TX so that credit-grant SDPCM headers are parsed + * promptly; with both at the same priority and timeslicing off (Zephyr + * default), a back-to-back-CMD53 TX loop starves RX and credits stall. + */ + k_thread_create(&brcmfmac_tx_thread, brcmfmac_tx_stack, + K_KERNEL_STACK_SIZEOF(brcmfmac_tx_stack), + brcmfmac_tx_thread_fn, data, NULL, NULL, + CONFIG_WIFI_BRCMFMAC_RX_THREAD_PRIO + 1, 0, K_NO_WAIT); + k_thread_name_set(&brcmfmac_tx_thread, "brcmfmac_tx"); + + data->f2_ready = true; + LOG_DBG("F2 claimed (block_size=%u), rx+tx threads up", + BRCMFMAC_F2_BLOCK_SIZE); + return 0; +} + +/* Build SDPCM headers in-place at the start of `frame` and TX `total` + * bytes (padded to 4) via incrementing CMD53 on F2. Caller must hold + * bcdc_mutex (serializes both txseq counter and F2 access). + */ +int brcmfmac_bcdc_tx_frame(struct brcmfmac_data *data, uint8_t chan, + uint8_t *frame, uint16_t total) +{ + struct sdpcm_frame_hdr *fh = (void *)frame; + struct sdpcm_sw_hdr *sw = (void *)(frame + sizeof(*fh)); + + /* Wait for SDPCM tx-credit AND chip-side flow control (mirrors + * Linux brcmfmac/sdio.c::data_ok + brcmu_pktq_mlen(..., ~flowcontrol)). + * + * Two independent gates: + * (1) credit window: chip's tx_max - txseq must be > 0 (8-bit wrap + * trick: if txseq has overtaken tx_max, (tx_max - txseq) is + * >= 0x80, so a 0x80 mask catches both "no credits" and "neg + * delta" in one check). + * (2) flow control: chip's last sw->flow byte must be zero. When + * chip's host-RX queue fills it sets bits here telling host to + * hold. The rx-thread updates brcmf_fcstate from every RX + * SDPCM header. + * + * Up to 100 ms total wait (5 x 20 ms). If we still can't send after + * that, return -EAGAIN so the caller can drop or requeue. + */ + for (int retry = 0; retry < 5; retry++) { + uint8_t delta = (uint8_t)(data->sdpcm_tx_max - data->sdpcm_txseq); + bool have_credit = (delta != 0 && (delta & 0x80) == 0); + bool xoff = atomic_get(&brcmf_fcstate) != 0; + + if (have_credit && !xoff) { + break; + } + if (k_sem_take(&data->tx_credit_sem, K_MSEC(20)) != 0) { + LOG_WRN("tx_frame: credit/fc timeout (txseq=%u tx_max=%u fcstate=%ld)", + data->sdpcm_txseq, data->sdpcm_tx_max, + (long)atomic_get(&brcmf_fcstate)); + return -EAGAIN; + } + } + + fh->len = total; + fh->notlen = (uint16_t)~total; + + sw->seq = data->sdpcm_txseq++; + sw->chan = chan; + sw->nextlen = 0; + sw->hdrlen = (uint8_t)(sizeof(*fh) + sizeof(*sw)); + sw->flow = 0; + sw->credit = 0; + sw->reserved[0] = 0; + sw->reserved[1] = 0; + + uint16_t padded = (total + 3) & ~3u; + + return sdio_write_addr(&data->radio, BRCMFMAC_F2_FIFO_ADDR, frame, padded); +} + +/* TX glom worker. iface_send pushes pre-built SDPCM frames into the ring + * (slot->data already has sw_hdr + bdc_hdr + payload at the right offsets; + * only fh->len/notlen and sw->seq are filled in here under bcdc_mutex). + * + * Pack up to BRCMFMAC_TX_GLOM_MAX_FRAMES frames into tx_glom_buf, gated by: + * - chip's SDPCM tx-credit window (same delta check as bcdc_tx_frame) + * - chip's per-frame xoff (brcmf_fcstate) + * - glom buffer size (12 KB) + * + * Then one F2 CMD53 for the entire batch. If no credit/xoff at all, wait on + * tx_credit_sem and retry (the rx thread signals it on every chip credit + * update). If credit allows only a partial batch, send what we have now. + */ +static void brcmfmac_tx_thread_fn(void *p1, void *p2, void *p3) +{ + struct brcmfmac_data *data = p1; + + ARG_UNUSED(p2); + ARG_UNUSED(p3); + + for (;;) { + (void)k_sem_take(&data->tx_pending_sem, K_FOREVER); + + while (atomic_get(&data->tx_ring_tail) != + atomic_get(&data->tx_ring_head)) { + + k_mutex_lock(&data->bcdc_mutex, K_FOREVER); + + uint16_t glom_len = 0; + uint8_t frame_count = 0; + + while (frame_count < BRCMFMAC_TX_GLOM_MAX_FRAMES) { + int tail = atomic_get(&data->tx_ring_tail); + int head = atomic_get(&data->tx_ring_head); + + if (tail == head) { + break; + } + + struct brcmfmac_tx_slot *slot = &data->tx_ring[tail]; + uint16_t padded = (slot->len + 3) & ~3u; + + if (glom_len + padded > BRCMFMAC_TX_GLOM_BUF_SIZE) { + break; + } + + uint8_t delta = (uint8_t)(data->sdpcm_tx_max - + data->sdpcm_txseq); + bool have_credit = (delta != 0 && + (delta & 0x80) == 0); + bool xoff = atomic_get(&brcmf_fcstate) != 0; + + if (!have_credit || xoff) { + break; + } + + struct sdpcm_frame_hdr *fh = + (struct sdpcm_frame_hdr *)slot->data; + struct sdpcm_sw_hdr *sw = + (struct sdpcm_sw_hdr *)(slot->data + sizeof(*fh)); + + fh->len = slot->len; + fh->notlen = (uint16_t)~slot->len; + sw->seq = data->sdpcm_txseq++; + + memcpy(data->tx_glom_buf + glom_len, slot->data, slot->len); + if (padded > slot->len) { + memset(data->tx_glom_buf + glom_len + slot->len, + 0, padded - slot->len); + } + glom_len += padded; + frame_count++; + + atomic_set(&data->tx_ring_tail, + (tail + 1) % BRCMFMAC_TX_RING_SLOTS); + } + + if (frame_count > 0) { + int rc = sdio_write_addr(&data->radio, + BRCMFMAC_F2_FIFO_ADDR, + data->tx_glom_buf, + glom_len); + if (rc != 0) { + LOG_ERR("tx_glom: F2 TX failed: %d (count=%u len=%u)", + rc, frame_count, glom_len); + } + } + + k_mutex_unlock(&data->bcdc_mutex); + + if (frame_count == 0) { + /* Ring non-empty but no credit / xoff. Wait. */ + if (k_sem_take(&data->tx_credit_sem, + K_MSEC(100)) != 0) { + LOG_WRN("tx_glom: credit timeout txseq=%u tx_max=%u fc=%ld", + data->sdpcm_txseq, + data->sdpcm_tx_max, + (long)atomic_get(&brcmf_fcstate)); + } + } + } + } +} + +int brcmfmac_bcdc_query_dcmd(struct brcmfmac_data *data, uint32_t cmd, + const uint8_t *tx_payload, uint16_t tx_len, + uint8_t *rx_buf, uint16_t rx_capacity) +{ + if (!data->f2_ready) { + return -EAGAIN; + } + + int rc = k_mutex_lock(&data->bcdc_mutex, K_FOREVER); + + if (rc != 0) { + return rc; + } + + /* Sized to hold the largest GET response payload we care about + * inline: chip's "counters" struct (~840 B on this fw rev) and + * "fwcap" (~150 B). cdc.len doubles as the chip-side TX-room + * indicator, so the wire frame must be sized to rx_capacity, not + * tx_len -- see comment below. + */ + static uint8_t tx_buf[2048] __aligned(4); + + const size_t hdr_len = sizeof(struct sdpcm_frame_hdr) + + sizeof(struct sdpcm_sw_hdr) + + sizeof(struct cdc_hdr); + + if (hdr_len + tx_len > sizeof(tx_buf)) { + rc = -EMSGSIZE; + goto out; + } + + memset(tx_buf, 0, sizeof(tx_buf)); + + struct cdc_hdr *cdc = (void *)(tx_buf + sizeof(struct sdpcm_frame_hdr) + + sizeof(struct sdpcm_sw_hdr)); + + /* cdc.len is one u32 -- the shared buffer size for both request + * parsing (chip reads name etc.) and response output (chip writes + * up to len bytes back). For a GET we need len >= rx_capacity or + * the chip returns BUFTOOSHORT (-14). + */ + uint16_t cdc_len = (tx_len > rx_capacity) ? tx_len : rx_capacity; + + cdc->cmd = cmd; + cdc->outlen = cdc_len; + cdc->inlen = 0; + data->bcdc_reqid++; + cdc->flags = ((uint32_t)data->bcdc_reqid << BCDC_REQ_ID_SHIFT); + cdc->status = 0; + + if (tx_payload != NULL && tx_len > 0) { + memcpy((uint8_t *)cdc + sizeof(*cdc), tx_payload, tx_len); + } + + /* Pad the on-wire frame to cdc_len so the chip sees room for its + * full response; any bytes past tx_len are already zeroed by the + * memset above (well, just the header was; pad explicitly). + */ + uint16_t payload_len = cdc_len; + uint16_t total = (uint16_t)(hdr_len + payload_len); + + if (hdr_len + payload_len > sizeof(tx_buf)) { + rc = -EMSGSIZE; + goto out; + } + if (payload_len > tx_len) { + memset((uint8_t *)cdc + sizeof(*cdc) + tx_len, 0, + payload_len - tx_len); + } + + /* Publish the waiter context BEFORE TX -- the RX thread may race + * us to the response (chip can be fast). + */ + data->pending.reqid = data->bcdc_reqid; + data->pending.out_buf = rx_buf; + data->pending.out_capacity = rx_capacity; + data->pending.out_copied = 0; + data->pending.status = 0; + k_sem_reset(&data->pending.done); + data->pending.active = true; + + rc = brcmfmac_bcdc_tx_frame(data, SDPCM_CHAN_CTRL, tx_buf, total); + if (rc != 0) { + LOG_ERR("bcdc_query: TX failed: %d", rc); + data->pending.active = false; + goto out; + } + + rc = k_sem_take(&data->pending.done, K_MSEC(BRCMFMAC_BCDC_TIMEOUT_MS)); + data->pending.active = false; + + if (rc == -EAGAIN) { + LOG_ERR("bcdc_query: timeout waiting on reqid=%u", + data->pending.reqid); + rc = -ETIMEDOUT; + goto out; + } + if (rc != 0) { + goto out; + } + + rc = (data->pending.status != 0) + ? data->pending.status + : (int)data->pending.out_copied; + +out: + k_mutex_unlock(&data->bcdc_mutex); + return rc; +} + +int brcmfmac_bcdc_iovar_get(struct brcmfmac_data *data, const char *name, + uint8_t *buf, uint16_t len) +{ + size_t name_len = strlen(name) + 1; + static uint8_t scratch[64] __aligned(4); + + if (name_len > sizeof(scratch)) { + return -EMSGSIZE; + } + + memset(scratch, 0, sizeof(scratch)); + memcpy(scratch, name, name_len); + + return brcmfmac_bcdc_query_dcmd(data, BRCMFMAC_WLC_GET_VAR, + scratch, (uint16_t)name_len, + buf, len); +} + +/* SET dcmd: same TX path as query_dcmd, BCDC_FLAG_SET in flags, no + * response payload (chip echoes status). We still wait for the chip's + * matching reqid ack so the call is synchronous. + */ +int brcmfmac_bcdc_set_dcmd(struct brcmfmac_data *data, uint32_t cmd, + const uint8_t *tx_payload, uint16_t tx_len) +{ + if (!data->f2_ready) { + return -EAGAIN; + } + + int rc = k_mutex_lock(&data->bcdc_mutex, K_FOREVER); + + if (rc != 0) { + return rc; + } + + static uint8_t tx_buf[1024] __aligned(4); + + const size_t hdr_len = sizeof(struct sdpcm_frame_hdr) + + sizeof(struct sdpcm_sw_hdr) + + sizeof(struct cdc_hdr); + + if (hdr_len + tx_len > sizeof(tx_buf)) { + rc = -EMSGSIZE; + goto out; + } + + memset(tx_buf, 0, hdr_len); + + struct cdc_hdr *cdc = (void *)(tx_buf + sizeof(struct sdpcm_frame_hdr) + + sizeof(struct sdpcm_sw_hdr)); + + cdc->cmd = cmd; + cdc->outlen = tx_len; + cdc->inlen = 0; + data->bcdc_reqid++; + cdc->flags = ((uint32_t)data->bcdc_reqid << BCDC_REQ_ID_SHIFT) + | BCDC_FLAG_SET; + cdc->status = 0; + + if (tx_payload != NULL && tx_len > 0) { + memcpy((uint8_t *)cdc + sizeof(*cdc), tx_payload, tx_len); + } + + uint16_t total = (uint16_t)(hdr_len + tx_len); + + data->pending.reqid = data->bcdc_reqid; + data->pending.out_buf = NULL; + data->pending.out_capacity = 0; + data->pending.out_copied = 0; + data->pending.status = 0; + k_sem_reset(&data->pending.done); + data->pending.active = true; + + rc = brcmfmac_bcdc_tx_frame(data, SDPCM_CHAN_CTRL, tx_buf, total); + if (rc != 0) { + LOG_ERR("bcdc_set: TX failed: %d", rc); + data->pending.active = false; + goto out; + } + + rc = k_sem_take(&data->pending.done, K_MSEC(BRCMFMAC_BCDC_TIMEOUT_MS)); + data->pending.active = false; + + if (rc == -EAGAIN) { + LOG_ERR("bcdc_set: timeout on reqid=%u cmd=%u", + data->pending.reqid, cmd); + rc = -ETIMEDOUT; + goto out; + } + if (rc == 0) { + rc = data->pending.status; + } + +out: + k_mutex_unlock(&data->bcdc_mutex); + return rc; +} + +int brcmfmac_bcdc_iovar_set(struct brcmfmac_data *data, const char *name, + const uint8_t *value, uint16_t value_len) +{ + size_t name_len = strlen(name) + 1; + static uint8_t scratch[1024] __aligned(4); + + if (name_len + value_len > sizeof(scratch)) { + return -EMSGSIZE; + } + + memset(scratch, 0, name_len); + memcpy(scratch, name, name_len); + if (value != NULL && value_len > 0) { + memcpy(scratch + name_len, value, value_len); + } + + return brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_SET_VAR, + scratch, (uint16_t)(name_len + value_len)); +} + +int brcmfmac_bcdc_bsscfg_iovar_set_int(struct brcmfmac_data *data, + const char *name, uint32_t bsscfgidx, + int32_t value) +{ + static uint8_t scratch[64] __aligned(4); + const char *prefix = "bsscfg:"; + size_t prefix_len = strlen(prefix); + size_t name_len = strlen(name) + 1; /* include NUL */ + + if (prefix_len + name_len + 8 > sizeof(scratch)) { + return -EMSGSIZE; + } + + uint8_t *p = scratch; + + memcpy(p, prefix, prefix_len); + p += prefix_len; + memcpy(p, name, name_len); + p += name_len; + /* bsscfgidx LE32 */ + p[0] = (uint8_t)(bsscfgidx & 0xFF); + p[1] = (uint8_t)((bsscfgidx >> 8) & 0xFF); + p[2] = (uint8_t)((bsscfgidx >> 16) & 0xFF); + p[3] = (uint8_t)((bsscfgidx >> 24) & 0xFF); + p += 4; + /* value LE32 (signed) */ + uint32_t v = (uint32_t)value; + + p[0] = (uint8_t)(v & 0xFF); + p[1] = (uint8_t)((v >> 8) & 0xFF); + p[2] = (uint8_t)((v >> 16) & 0xFF); + p[3] = (uint8_t)((v >> 24) & 0xFF); + + uint16_t total = (uint16_t)(prefix_len + name_len + 8); + + return brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_SET_VAR, scratch, total); +} diff --git a/drivers/wifi/brcmfmac/brcmfmac_chip.c b/drivers/wifi/brcmfmac/brcmfmac_chip.c new file mode 100644 index 0000000000000..7b44b4fb80a7d --- /dev/null +++ b/drivers/wifi/brcmfmac/brcmfmac_chip.c @@ -0,0 +1,653 @@ +/* + * Copyright (c) 2026 Jonathan Elliot Peace + * SPDX-License-Identifier: Apache-2.0 + * + * Chip topology, AI core helpers, and initialization state transitions. + * Mirrors Linux brcmfmac/chip.c (DMP/PL-368 descriptor spec for the + * EROM scan; brcmf_chip_ai_{iscoreup,coredisable,resetcore} for AI + * core control; brcmf_chip_cm3_set_{passive,active} for CM3 chips). + */ + +#include +#include +#include + +#include "brcmfmac_priv.h" + +LOG_MODULE_DECLARE(brcmfmac, CONFIG_WIFI_LOG_LEVEL); + +/* === EROM (DMP/PL-368) descriptor format =================================== */ + +#define DMP_DESC_TYPE_MSK 0x0000000F +#define DMP_DESC_EMPTY 0x00000000 +#define DMP_DESC_VALID 0x00000001 +#define DMP_DESC_COMPONENT 0x00000001 +#define DMP_DESC_MASTER_PORT 0x00000003 +#define DMP_DESC_ADDRESS 0x00000005 +#define DMP_DESC_ADDRSIZE_GT32 0x00000008 +#define DMP_DESC_EOT 0x0000000F + +#define DMP_COMP_PARTNUM 0x000FFF00 +#define DMP_COMP_PARTNUM_S 8 + +#define DMP_COMP_NUM_SWRAP 0x00F80000 +#define DMP_COMP_NUM_SWRAP_S 19 +#define DMP_COMP_NUM_MWRAP 0x0007C000 +#define DMP_COMP_NUM_MWRAP_S 14 + +#define DMP_SLAVE_ADDR_BASE 0xFFFFF000 +#define DMP_SLAVE_TYPE 0x000000C0 +#define DMP_SLAVE_TYPE_S 6 +#define DMP_SLAVE_TYPE_SLAVE 0 +#define DMP_SLAVE_TYPE_SWRAP 2 +#define DMP_SLAVE_TYPE_MWRAP 3 +#define DMP_SLAVE_SIZE_TYPE 0x00000030 +#define DMP_SLAVE_SIZE_TYPE_S 4 +#define DMP_SLAVE_SIZE_4K 0 +#define DMP_SLAVE_SIZE_8K 1 +#define DMP_SLAVE_SIZE_DESC 3 + +#define CC_EROMPTR_OFFSET 0xFC + +static int dmp_get_desc(struct brcmfmac_data *data, uint32_t *erom_addr, + uint32_t *val_out, uint8_t *type_out) +{ + int ret = brcmfmac_sdio_backplane_read32(data, *erom_addr, val_out); + + if (ret != 0) { + return ret; + } + *erom_addr += 4; + + if (type_out != NULL) { + uint8_t t = (uint8_t)(*val_out & DMP_DESC_TYPE_MSK); + + if ((t & ~DMP_DESC_ADDRSIZE_GT32) == DMP_DESC_ADDRESS) { + t = DMP_DESC_ADDRESS; + } + *type_out = t; + } + return 0; +} + +static int dmp_get_regaddr(struct brcmfmac_data *data, uint32_t *erom_addr, + uint32_t *regbase, uint32_t *wrapbase) +{ + uint8_t desc; + uint32_t val; + int ret; + uint8_t wraptype; + + *regbase = 0; + *wrapbase = 0; + + ret = dmp_get_desc(data, erom_addr, &val, &desc); + if (ret != 0) { + return ret; + } + + if (desc == DMP_DESC_MASTER_PORT) { + wraptype = DMP_SLAVE_TYPE_MWRAP; + } else if (desc == DMP_DESC_ADDRESS) { + *erom_addr -= 4; + wraptype = DMP_SLAVE_TYPE_SWRAP; + } else { + *erom_addr -= 4; + return -EILSEQ; + } + + do { + do { + ret = dmp_get_desc(data, erom_addr, &val, &desc); + if (ret != 0) { + return ret; + } + if (desc == DMP_DESC_EOT) { + *erom_addr -= 4; + return -EFAULT; + } + } while (desc != DMP_DESC_ADDRESS && desc != DMP_DESC_COMPONENT); + + if (desc == DMP_DESC_COMPONENT) { + *erom_addr -= 4; + return 0; + } + + if (val & DMP_DESC_ADDRSIZE_GT32) { + uint32_t tmp; + + ret = dmp_get_desc(data, erom_addr, &tmp, NULL); + if (ret != 0) { + return ret; + } + } + + uint8_t sztype = (val & DMP_SLAVE_SIZE_TYPE) >> DMP_SLAVE_SIZE_TYPE_S; + + if (sztype == DMP_SLAVE_SIZE_DESC) { + uint32_t szdesc; + + ret = dmp_get_desc(data, erom_addr, &szdesc, NULL); + if (ret != 0) { + return ret; + } + if (szdesc & DMP_DESC_ADDRSIZE_GT32) { + uint32_t tmp; + + ret = dmp_get_desc(data, erom_addr, &tmp, NULL); + if (ret != 0) { + return ret; + } + } + } + + if (sztype != DMP_SLAVE_SIZE_4K && sztype != DMP_SLAVE_SIZE_8K) { + continue; + } + + uint8_t stype = (val & DMP_SLAVE_TYPE) >> DMP_SLAVE_TYPE_S; + + if (*regbase == 0 && stype == DMP_SLAVE_TYPE_SLAVE) { + *regbase = val & DMP_SLAVE_ADDR_BASE; + } + if (*wrapbase == 0 && stype == wraptype) { + *wrapbase = val & DMP_SLAVE_ADDR_BASE; + } + } while (*regbase == 0 || *wrapbase == 0); + + return 0; +} + +int brcmfmac_chip_erom_scan(struct brcmfmac_data *data) +{ + uint32_t erom_addr; + int ret = brcmfmac_sdio_backplane_read32(data, + BRCMF_SI_ENUM_BASE + CC_EROMPTR_OFFSET, + &erom_addr); + if (ret != 0) { + LOG_ERR("erom_scan: read eromptr failed: %d", ret); + return ret; + } + LOG_DBG("erom_scan: eromptr=0x%08x", erom_addr); + + data->num_cores = 0; + uint8_t desc_type = 0; + + while (desc_type != DMP_DESC_EOT) { + uint32_t val; + + ret = dmp_get_desc(data, &erom_addr, &val, &desc_type); + if (ret != 0) { + LOG_ERR("erom_scan: desc read failed: %d", ret); + return ret; + } + if (!(val & DMP_DESC_VALID)) { + continue; + } + if (desc_type == DMP_DESC_EMPTY) { + continue; + } + if (desc_type != DMP_DESC_COMPONENT) { + continue; + } + + uint16_t id = (val & DMP_COMP_PARTNUM) >> DMP_COMP_PARTNUM_S; + + uint32_t ident_b; + + ret = dmp_get_desc(data, &erom_addr, &ident_b, &desc_type); + if (ret != 0) { + return ret; + } + if ((ident_b & DMP_DESC_TYPE_MSK) != DMP_DESC_COMPONENT) { + LOG_ERR("erom_scan: expected CompIdentB, got 0x%08x", ident_b); + return -EILSEQ; + } + + uint8_t nmw = (ident_b & DMP_COMP_NUM_MWRAP) >> DMP_COMP_NUM_MWRAP_S; + uint8_t nsw = (ident_b & DMP_COMP_NUM_SWRAP) >> DMP_COMP_NUM_SWRAP_S; + + if (nmw + nsw == 0 && id != BCMA_CORE_PMU && id != BCMA_CORE_GCI) { + continue; + } + + uint32_t base = 0, wrap = 0; + + ret = dmp_get_regaddr(data, &erom_addr, &base, &wrap); + if (ret != 0) { + continue; + } + + if (data->num_cores >= BRCMFMAC_MAX_CORES) { + LOG_WRN("erom_scan: MAX_CORES overflow at id=0x%03x", id); + continue; + } + data->cores[data->num_cores].id = id; + data->cores[data->num_cores].base = base; + data->cores[data->num_cores].wrapbase = wrap; + LOG_DBG("erom_scan: core[%u] id=0x%03x base=0x%08x wrap=0x%08x", + data->num_cores, id, base, wrap); + data->num_cores++; + } + + LOG_DBG("erom_scan: discovered %u cores", data->num_cores); + return 0; +} + +const struct bcm_core *brcmfmac_chip_core_find(const struct brcmfmac_data *data, + uint16_t id) +{ + for (unsigned int i = 0; i < data->num_cores; i++) { + if (data->cores[i].id == id) { + return &data->cores[i]; + } + } + return NULL; +} + +/* === AI core reset/enable helpers ========================================== + * + * Each core has a "wrapper" address space (separate from its data base) + * where IOCTL and RESET_CTL live. + */ + +static bool ai_iscoreup(struct brcmfmac_data *data, uint32_t wrap) +{ + uint32_t v; + + if (brcmfmac_sdio_backplane_read32(data, wrap + BCMA_IOCTL, &v) != 0) { + return false; + } + bool clk_ok = (v & (BCMA_IOCTL_FGC | BCMA_IOCTL_CLK)) == BCMA_IOCTL_CLK; + + if (brcmfmac_sdio_backplane_read32(data, wrap + BCMA_RESET_CTL, &v) != 0) { + return false; + } + return clk_ok && ((v & BCMA_RESET_CTL_RESET) == 0); +} + +static int ai_coredisable(struct brcmfmac_data *data, uint32_t wrap, + uint32_t prereset, uint32_t reset) +{ + uint32_t v; + int ret = brcmfmac_sdio_backplane_read32(data, wrap + BCMA_RESET_CTL, &v); + + if (ret != 0) { + return ret; + } + + if ((v & BCMA_RESET_CTL_RESET) == 0) { + (void)brcmfmac_sdio_backplane_write32(data, wrap + BCMA_IOCTL, + prereset | BCMA_IOCTL_FGC | BCMA_IOCTL_CLK); + (void)brcmfmac_sdio_backplane_read32(data, wrap + BCMA_IOCTL, &v); + + (void)brcmfmac_sdio_backplane_write32(data, wrap + BCMA_RESET_CTL, + BCMA_RESET_CTL_RESET); + k_busy_wait(20); + + for (int i = 0; i < 300; i++) { + (void)brcmfmac_sdio_backplane_read32(data, wrap + BCMA_RESET_CTL, &v); + if (v == BCMA_RESET_CTL_RESET) { + break; + } + k_busy_wait(1); + } + } + + (void)brcmfmac_sdio_backplane_write32(data, wrap + BCMA_IOCTL, + reset | BCMA_IOCTL_FGC | BCMA_IOCTL_CLK); + (void)brcmfmac_sdio_backplane_read32(data, wrap + BCMA_IOCTL, &v); + return 0; +} + +static int ai_resetcore(struct brcmfmac_data *data, uint32_t wrap, + uint32_t prereset, uint32_t reset, uint32_t postreset) +{ + uint32_t v; + int ret = ai_coredisable(data, wrap, prereset, reset); + + if (ret != 0) { + return ret; + } + + for (int count = 0; count < 50; count++) { + (void)brcmfmac_sdio_backplane_read32(data, wrap + BCMA_RESET_CTL, &v); + if ((v & BCMA_RESET_CTL_RESET) == 0) { + break; + } + (void)brcmfmac_sdio_backplane_write32(data, wrap + BCMA_RESET_CTL, 0); + k_busy_wait(50); + } + + (void)brcmfmac_sdio_backplane_write32(data, wrap + BCMA_IOCTL, + postreset | BCMA_IOCTL_CLK); + (void)brcmfmac_sdio_backplane_read32(data, wrap + BCMA_IOCTL, &v); + return 0; +} + +/* === chipid + PMU/CHIPCLKCSR setup ========================================= */ + +int brcmfmac_chip_read_id(struct brcmfmac_data *data) +{ + int ret = brcmfmac_sdio_backplane_read32(data, BRCMF_SI_ENUM_BASE, + &data->chipid_reg); + if (ret != 0) { + LOG_ERR("chipid read failed: %d", ret); + return ret; + } + data->chip_id = data->chipid_reg & CID_ID_MASK; + data->chip_rev = (data->chipid_reg & CID_REV_MASK) >> CID_REV_SHIFT; + data->chip_type = (data->chipid_reg & CID_TYPE_MASK) >> CID_TYPE_SHIFT; + + LOG_INF("chipid = 0x%08x chip=%u (0x%04x) rev=%u type=%u (%s)", + data->chipid_reg, data->chip_id, data->chip_id, + data->chip_rev, data->chip_type, + data->chip_type == 0 ? "SB" : + (data->chip_type == 1 ? "AXI" : "?")); + return 0; +} + +int brcmfmac_chip_ram_data(struct brcmfmac_data *data) +{ + /* TODO: Read ram_size from core registers like in Linux. */ + if (data->chip_id == BRCM_CC_43430_CHIP_ID || + data->chip_id == BRCM_CC_43439_CHIP_ID) { + data->ram_base = 0u; + data->ram_size = 0x80000u; + } else if (data->chip_id == BRCM_CC_4345_CHIP_ID) { + data->ram_base = 0x198000u; + data->ram_size = 0xC8000u; + } else { + LOG_ERR("ram_data: don't know RAM addresses for this chip"); + return -1; + } + + return 0; +} + +int brcmfmac_chip_pmu_setup(struct brcmfmac_data *data) +{ + int ret = sdio_write_byte(&data->backplane, SBSDIO_FUNC1_CHIPCLKCSR, + BRCMF_INIT_CLKCTL1); + if (ret != 0) { + LOG_ERR("CHIPCLKCSR write(0x%02x) failed: %d", BRCMF_INIT_CLKCTL1, ret); + return ret; + } + + uint8_t clkval = 0; + + for (int i = 0; i < 100; i++) { + ret = sdio_read_byte(&data->backplane, SBSDIO_FUNC1_CHIPCLKCSR, &clkval); + if (ret != 0) { + LOG_ERR("CHIPCLKCSR read failed: %d", ret); + return ret; + } + if (clkval & SBSDIO_AVBITS) { + break; + } + k_msleep(1); + } + if (!(clkval & SBSDIO_AVBITS)) { + LOG_ERR("ALP/HT never available (CHIPCLKCSR=0x%02x)", clkval); + return -ETIMEDOUT; + } + LOG_DBG("CHIPCLKCSR=0x%02x (%s%s)", clkval, + (clkval & SBSDIO_ALP_AVAIL) ? "ALP " : "", + (clkval & SBSDIO_HT_AVAIL) ? "HT" : ""); + + ret = sdio_write_byte(&data->backplane, SBSDIO_FUNC1_CHIPCLKCSR, + SBSDIO_FORCE_HW_CLKREQ_OFF | SBSDIO_FORCE_ALP); + if (ret != 0) { + LOG_ERR("CHIPCLKCSR force-ALP write failed: %d", ret); + return ret; + } + k_busy_wait(65); + + ret = sdio_write_byte(&data->backplane, SBSDIO_FUNC1_SDIOPULLUP, 0); + if (ret != 0) { + LOG_ERR("SDIOPULLUP=0 write failed: %d", ret); + return ret; + } + return 0; +} + +static int brcmfmac_chip_ht_bringup(struct brcmfmac_data *data) +{ + int ret; + uint8_t clkcsr = 0; + int64_t t0 = k_uptime_get(); + + for (int i = 0; i < 500; i++) { + ret = sdio_read_byte(&data->backplane, SBSDIO_FUNC1_CHIPCLKCSR, &clkcsr); + if (ret != 0) { + LOG_ERR("ht_bringup: CHIPCLKCSR read failed: %d", ret); + return ret; + } + if (clkcsr & SBSDIO_HT_AVAIL) { + break; + } + k_msleep(1); + } + int64_t t_wait = k_uptime_get() - t0; + + if (!(clkcsr & SBSDIO_HT_AVAIL)) { + LOG_ERR("ht_bringup: HT_AVAIL timeout (CHIPCLKCSR=0x%02x after %lld ms)", + clkcsr, (long long)t_wait); + return -ETIMEDOUT; + } + LOG_DBG("ht_bringup: HT_AVAIL after %lld ms (CHIPCLKCSR=0x%02x)", + (long long)t_wait, clkcsr); + + uint32_t chipid; + + ret = brcmfmac_sdio_backplane_read32(data, BRCMF_SI_ENUM_BASE, &chipid); + if (ret != 0) { + LOG_ERR("ht_bringup: post-boot chipid read failed: %d", ret); + return ret; + } + LOG_DBG("ht_bringup: post-boot chipid = 0x%08x (chip alive, fw running)", + chipid); + return 0; +} + +/* === set_passive: prepare for firmware upload ============================== + * + * Mirrors Linux brcmf_chip_cm3_set_passive: + * 1. halt ARM CM3 (so it doesn't fight us during the upload) + * 2. reset D11 MAC with PHYRESET + PHYCLOCKEN active + * 3. reset SOCRAM (brings it out of POR to operational) + * 4. disable bank-3 remap (BCM43430-specific quirk) + */ +static int brcmfmac_chip_cm3_set_passive(struct brcmfmac_data *data) +{ + const struct bcm_core *arm = brcmfmac_chip_core_find(data, BCMA_CORE_ARM_CM3); + const struct bcm_core *d11 = brcmfmac_chip_core_find(data, BCMA_CORE_80211); + const struct bcm_core *sr = brcmfmac_chip_core_find(data, BCMA_CORE_INTERNAL_MEM); + + if (arm == NULL || d11 == NULL || sr == NULL) { + LOG_ERR("set_passive: missing core(s) arm=%p d11=%p sr=%p", + (const void *)arm, (const void *)d11, (const void *)sr); + return -ENODEV; + } + + LOG_DBG("set_passive: halt ARM CM3 (wrap=0x%08x)", arm->wrapbase); + int ret = ai_coredisable(data, arm->wrapbase, 0, 0); + + if (ret != 0) { + LOG_ERR("set_passive: CM3 disable failed: %d", ret); + return ret; + } + + LOG_DBG("set_passive: reset D11 PHY (wrap=0x%08x)", d11->wrapbase); + ret = ai_resetcore(data, d11->wrapbase, + D11_BCMA_IOCTL_PHYRESET | D11_BCMA_IOCTL_PHYCLOCKEN, + D11_BCMA_IOCTL_PHYCLOCKEN, + D11_BCMA_IOCTL_PHYCLOCKEN); + if (ret != 0) { + LOG_ERR("set_passive: D11 reset failed: %d", ret); + return ret; + } + + LOG_DBG("set_passive: reset SOCRAM (wrap=0x%08x base=0x%08x)", + sr->wrapbase, sr->base); + ret = ai_resetcore(data, sr->wrapbase, 0, 0, 0); + if (ret != 0) { + LOG_ERR("set_passive: SOCRAM reset failed: %d", ret); + return ret; + } + + /* + * SOCRAM bank-3 remap disable. Linux brcmfmac applies this only to + * BCM43430 and CYW43439 -- on other chips bank 3 is either not + * present (BCM4329, BCM43236) or has a different PDA layout + * (BCM43340, BCM4334) and the write would misconfigure RAM. + */ + if (data->chip_id == BRCM_CC_43430_CHIP_ID || + data->chip_id == BRCM_CC_43439_CHIP_ID) { + (void)brcmfmac_sdio_backplane_write32(data, + sr->base + SOCRAM_BANKIDX_OFFSET, 3); + (void)brcmfmac_sdio_backplane_write32(data, + sr->base + SOCRAM_BANKPDA_OFFSET, 0); + LOG_DBG("set_passive: bank-3 remap disabled"); + } + + if (!ai_iscoreup(data, sr->wrapbase)) { + LOG_ERR("set_passive: SOCRAM did not come up"); + return -EIO; + } + LOG_DBG("set_passive: SOCRAM is up"); + return 0; +} + +/* === set_active: release ARM CM3 + confirm firmware boot =================== */ + +static int brcmfmac_chip_cm3_set_active(struct brcmfmac_data *data) +{ + const struct bcm_core *sdio_dev = brcmfmac_chip_core_find(data, BCMA_CORE_SDIO_DEV); + const struct bcm_core *arm = brcmfmac_chip_core_find(data, BCMA_CORE_ARM_CM3); + + if (sdio_dev == NULL || arm == NULL) { + LOG_ERR("set_active: missing core(s) sdio=%p arm=%p", + (const void *)sdio_dev, (const void *)arm); + return -ENODEV; + } + + LOG_DBG("set_active: clear SDIO core intstatus"); + int ret = brcmfmac_sdio_backplane_write32(data, + sdio_dev->base + SDPCMD_INTSTATUS, + 0xFFFFFFFFu); + if (ret != 0) { + LOG_ERR("set_active: intstatus clear failed: %d", ret); + return ret; + } + + LOG_DBG("set_active: release ARM CM3"); + ret = ai_resetcore(data, arm->wrapbase, 0, 0, 0); + if (ret != 0) { + LOG_ERR("set_active: ARM resetcore failed: %d", ret); + return ret; + } + + LOG_DBG("set_active: request HT clock"); + ret = sdio_write_byte(&data->backplane, SBSDIO_FUNC1_CHIPCLKCSR, + SBSDIO_HT_AVAIL_REQ); + if (ret != 0) { + LOG_ERR("set_active: CHIPCLKCSR write failed: %d", ret); + return ret; + } + + return brcmfmac_chip_ht_bringup(data); +} + +static int brcmfmac_chip_cr4_set_passive(struct brcmfmac_data *data) +{ + /* TODO: All D11 cores */ + const struct bcm_core *arm = brcmfmac_chip_core_find(data, BCMA_CORE_ARM_CR4); + const struct bcm_core *d11 = brcmfmac_chip_core_find(data, BCMA_CORE_80211); + uint32_t v; + int ret; + + if (arm == NULL || d11 == NULL) { + LOG_ERR("set_passive: missing core(s) arm=%p d11=%p", arm, d11); + return -ENODEV; + } + + (void)brcmfmac_sdio_backplane_read32(data, arm->wrapbase + BCMA_IOCTL, &v); + v &= ARMCR4_BCMA_IOCTL_CPUHALT; + ret = ai_resetcore(data, arm->wrapbase, v, + ARMCR4_BCMA_IOCTL_CPUHALT, ARMCR4_BCMA_IOCTL_CPUHALT); + if (ret != 0) { + LOG_ERR("set_passive: CR4 reset failed: %d", ret); + return ret; + } + + ret = ai_coredisable(data, d11->wrapbase, 0, 0); + if (ret != 0) { + LOG_ERR("set_passive: D11 disable failed: %d", ret); + return ret; + } + + return 0; +} + +static int brcmfmac_chip_cr4_set_active(struct brcmfmac_data *data) +{ + const struct bcm_core *sdio_dev = brcmfmac_chip_core_find(data, BCMA_CORE_SDIO_DEV); + const struct bcm_core *arm = brcmfmac_chip_core_find(data, BCMA_CORE_ARM_CR4); + + if (sdio_dev == NULL || arm == NULL) { + LOG_ERR("set_passive: missing core(s) sdio=%p arm=%p", sdio_dev, arm); + return -ENODEV; + } + + LOG_DBG("set_active: clear SDIO core intstatus"); + int ret = brcmfmac_sdio_backplane_write32(data, + sdio_dev->base + SDPCMD_INTSTATUS, + 0xFFFFFFFFu); + if (ret != 0) { + LOG_ERR("set_active: intstatus clear failed: %d", ret); + return ret; + } + + ret = ai_resetcore(data, arm->wrapbase, ARMCR4_BCMA_IOCTL_CPUHALT, 0, 0); + if (ret != 0) { + LOG_ERR("set_active: CR4 reset failed: %d", ret); + return ret; + } + + return brcmfmac_chip_ht_bringup(data); +} + +int brcmfmac_chip_set_passive(struct brcmfmac_data *data) +{ + const struct bcm_core *arm; + + arm = brcmfmac_chip_core_find(data, BCMA_CORE_ARM_CM3); + if (arm != NULL) { + return brcmfmac_chip_cm3_set_passive(data); + } + + arm = brcmfmac_chip_core_find(data, BCMA_CORE_ARM_CR4); + if (arm != NULL) { + return brcmfmac_chip_cr4_set_passive(data); + } + + return 0; +} + +int brcmfmac_chip_set_active(struct brcmfmac_data *data) +{ + const struct bcm_core *arm; + + arm = brcmfmac_chip_core_find(data, BCMA_CORE_ARM_CM3); + if (arm != NULL) { + return brcmfmac_chip_cm3_set_active(data); + } + + arm = brcmfmac_chip_core_find(data, BCMA_CORE_ARM_CR4); + if (arm != NULL) { + return brcmfmac_chip_cr4_set_active(data); + } + + return 0; +} diff --git a/drivers/wifi/brcmfmac/brcmfmac_core.c b/drivers/wifi/brcmfmac/brcmfmac_core.c new file mode 100644 index 0000000000000..f6dd8caeea74e --- /dev/null +++ b/drivers/wifi/brcmfmac/brcmfmac_core.c @@ -0,0 +1,472 @@ +/* + * Copyright (c) 2026 Jonathan Elliot Peace + * SPDX-License-Identifier: Apache-2.0 + * + * Broadcom BCM43xxx SDIO Wi-Fi driver (brcmfmac protocol). + * + * WPA2-PSK association + event-driven link state + DHCP + * autostart on top of 4.5a's net_if + scan. Connect IOCTL sequence + * (mpc/auth/wsec/wpa_auth/wsec_pmk/WLC_SET_SSID) and chan=1 event + * parsing (WLC_E_AUTH / ASSOC / LINK / DISASSOC_IND -> wifi_mgmt + * raise calls) live in brcmfmac_net.c. On link-up: net_if_dormant_off + * + net_dhcpv4_restart drive the iface to UP and acquire an IP. + */ + +#define DT_DRV_COMPAT brcm_bcm43xxx_sdio + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "brcmfmac_priv.h" + +LOG_MODULE_REGISTER(brcmfmac, CONFIG_WIFI_LOG_LEVEL); + +/* WL_REG_ON pulse timing. The chip's CBUCK regulator needs a brief window + * to fully discharge before re-power, then ~250 ms for the chip's clocks + * to come up and the SDIO bootloader to reach a state where F0/F1 + * enumerate. Values match the BCM43xxx/CYW43xxx Cypress BSP board + * bring-up sequences. + */ +#define BRCMFMAC_REG_ON_LOW_DELAY_MS 10 +#define BRCMFMAC_REG_ON_HIGH_DELAY_MS 250 + +/* CLM blob upload chunking. Linux uses 1400 bytes/chunk; we cap at 512 + * because brcmfmac_bcdc_iovar_set caps the payload at 1024 bytes total + * and the dload header eats 12 of those. + */ +#define BRCMFMAC_CLM_CHUNK_SIZE 512 + +static int brcmfmac_initialization(struct brcmfmac_data *data) +{ + int ret = brcmfmac_chip_read_id(data); + + if (ret != 0) { + return ret; + } + if (data->chip_id == BRCM_CC_43430_CHIP_ID && data->chip_rev == 1) { + LOG_INF(" -> matches BCM43430A1"); + } + + ret = brcmfmac_chip_ram_data(data); + if (ret != 0) { + return ret; + } + + ret = brcmfmac_chip_pmu_setup(data); + if (ret != 0) { + return ret; + } + + ret = brcmfmac_chip_erom_scan(data); + if (ret != 0) { + return ret; + } + + ret = brcmfmac_chip_set_passive(data); + if (ret != 0) { + return ret; + } + + ret = brcmfmac_sdio_fw_upload(data); + if (ret != 0) { + return ret; + } + + ret = brcmfmac_sdio_nvram_upload(data); + if (ret != 0) { + return ret; + } + + return brcmfmac_chip_set_active(data); +} + +static int brcmfmac_power_on(const struct device *dev) +{ +#if DT_INST_NODE_HAS_PROP(0, wifi_reg_on_gpios) + int ret; + const struct brcmfmac_config *cfg = dev->config; + + /* Check WIFI REG_ON gpio instance */ + if (!device_is_ready(cfg->reg_on.port)) { + LOG_ERR("power_on: failed to configure reg_on %s pin %d", + cfg->reg_on.port->name, cfg->reg_on.pin); + return -EIO; + } + + /* Configure wifi_reg_on as output */ + ret = gpio_pin_configure_dt(&cfg->reg_on, GPIO_OUTPUT); + if (ret) { + LOG_ERR("power_on: failed to configure reg_on %s pin %d, ret=%d", + cfg->reg_on.port->name, cfg->reg_on.pin, ret); + return ret; + } + + ret = gpio_pin_set_dt(&cfg->reg_on, 0); + if (ret) { + return ret; + } + + /* Allow CBUCK regulator to discharge */ + k_msleep(BRCMFMAC_REG_ON_LOW_DELAY_MS); + + /* WIFI power on */ + ret = gpio_pin_set_dt(&cfg->reg_on, 1); + if (ret) { + return ret; + } + + k_msleep(BRCMFMAC_REG_ON_HIGH_DELAY_MS); +#else + (void)dev; +#endif /* DT_INST_NODE_HAS_PROP(0, reg_on_gpios) */ + + return 0; +} + +static int brcmfmac_probe_sdio(const struct device *dev) +{ + const struct brcmfmac_config *cfg = dev->config; + struct brcmfmac_data *data = dev->data; + int ret; + + if (!device_is_ready(cfg->sdhc)) { + LOG_ERR("SDHC parent %s not ready", cfg->sdhc->name); + return -ENODEV; + } + + ret = sd_init(cfg->sdhc, &data->card); + if (ret != 0) { + LOG_ERR("sd_init failed: %d", ret); + return ret; + } + LOG_INF("sd_init ok: num_io=%u rca=0x%04x bus_width=%u", + (unsigned int)data->card.num_io, + data->card.relative_addr, + data->card.bus_io.bus_width); + + ret = sdio_init_func(&data->card, &data->backplane, SDIO_FUNC_NUM_1); + if (ret != 0) { + LOG_ERR("sdio_init_func(F1) failed: %d", ret); + return ret; + } + ret = sdio_enable_func(&data->backplane); + if (ret != 0) { + LOG_ERR("sdio_enable_func(F1) failed: %d", ret); + return ret; + } + /* sdio_init_func leaves block_size=0; subsys helper then divides by + * zero on the block-mode branch. brcmfmac canonical for F1 is 64. + */ + ret = sdio_set_block_size(&data->backplane, 64); + if (ret != 0) { + LOG_ERR("sdio_set_block_size(F1, 64) failed: %d", ret); + return ret; + } + LOG_DBG("F1 claimed (max_blk=%u)", data->backplane.cis.max_blk_size); + return 0; +} + +static int brcmfmac_process_clm_blob(struct brcmfmac_data *data) +{ + struct brcmf_dload_data_le *hdr; + uint8_t buf[sizeof(*hdr) + BRCMFMAC_CLM_CHUNK_SIZE]; + const uint8_t *current = brcmfmac_clm_blob; + size_t remaining = brcmfmac_clm_blob_len; + + hdr = (struct brcmf_dload_data_le *)buf; + hdr->flag = BRCMF_DL_CRC_IN_HDR | BRCMF_DL_BEGIN; + hdr->dload_type = BRCMF_DL_TYPE_CLM; + hdr->crc = 0; + + while (remaining > 0) { + size_t transfer = BRCMFMAC_CLM_CHUNK_SIZE; + int ret; + + if (remaining <= transfer) { + transfer = remaining; + hdr->flag |= BRCMF_DL_END; + } + + memcpy(hdr->data, current, transfer); + hdr->len = transfer; + + LOG_DBG("sending clm chunk, len=%u", transfer); + ret = brcmfmac_bcdc_iovar_set(data, "clmload", buf, sizeof(*hdr) + transfer); + if (ret != 0) { + LOG_ERR("clm chunk download failed: %d", ret); + return ret; + } + + current += transfer; + remaining -= transfer; + hdr->flag &= ~BRCMF_DL_BEGIN; + } + + return 0; +} + +static int brcmfmac_init(const struct device *dev) +{ + struct brcmfmac_data *data = dev->data; + int ret; + + ret = brcmfmac_power_on(dev); + if (ret != 0) { + return ret; + } + + ret = brcmfmac_probe_sdio(dev); + if (ret != 0) { + return ret; + } + + int64_t t0 = k_uptime_get(); + + ret = brcmfmac_initialization(data); + if (ret != 0) { + LOG_ERR("initialization failed: %d", ret); + return ret; + } + + ret = brcmfmac_bcdc_init(data); + if (ret != 0) { + LOG_ERR("BCDC init failed: %d", ret); + return ret; + } + + /* + * CLM blob upload. Chips that ship without built-in regulatory tables + * (e.g. BCM43458F) require this; chips that have them (e.g. BCM43430A1) + * still accept the iovar harmlessly. We always attempt the upload when + * a blob is configured in the build via CONFIG_WIFI_BRCMFMAC_CLM_FILE; + * an unconfigured build links an empty array (len==0) and we skip. + */ + if (brcmfmac_clm_blob_len > 0) { + ret = brcmfmac_process_clm_blob(data); + if (ret != 0) { + LOG_ERR("CLM upload failed: %d", ret); + return ret; + } + } else { + LOG_DBG("CLM blob upload skipped (no blob configured)"); + } + + int got = brcmfmac_bcdc_iovar_get(data, "cur_etheraddr", + data->chip_mac, sizeof(data->chip_mac)); + if (got < (int)sizeof(data->chip_mac)) { + LOG_ERR("cur_etheraddr read returned %d (want >=6)", got); + return (got < 0) ? got : -EIO; + } + LOG_INF("chip MAC = %02x:%02x:%02x:%02x:%02x:%02x", + data->chip_mac[0], data->chip_mac[1], data->chip_mac[2], + data->chip_mac[3], data->chip_mac[4], data->chip_mac[5]); + + /* WLC_UP: bring the MAC layer up. Most write IOCTLs (notably the + * "escan" IOVAR) return BCME_NOTUP (-4) until this fires. + */ + ret = brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_UP, NULL, 0); + if (ret != 0) { + LOG_ERR("WLC_UP failed: %d", ret); + return ret; + } + LOG_INF("WLC_UP ok"); + + /* Enable the events we care about in the chip's event mask. Read + * current mask first so we don't clobber chip defaults. Events the + * chip leaves disabled by default but we need: + * - WLC_E_ESCAN_RESULT (scan results stream) + * - WLC_E_AUTH (auth attempt outcome) + * - WLC_E_ASSOC (assoc attempt outcome) + * - WLC_E_LINK (link up/down -- triggers dormant_off + DHCP) + * - WLC_E_DISASSOC_IND (disconnect notice) + * - WLC_E_SET_SSID (echo of WLC_SET_SSID outcome) + */ + uint8_t event_mask[BRCMFMAC_EVENTING_MASK_LEN] = {0}; + int em_got = brcmfmac_bcdc_iovar_get(data, "event_msgs", + event_mask, sizeof(event_mask)); + if (em_got < (int)sizeof(event_mask)) { + LOG_WRN("event_msgs get returned %d; starting from zero", em_got); + memset(event_mask, 0, sizeof(event_mask)); + } +#define ENABLE_EVENT(ev) \ + (event_mask[(ev) / 8] |= (uint8_t)(1u << ((ev) % 8))) + ENABLE_EVENT(WLC_E_ESCAN_RESULT); + ENABLE_EVENT(WLC_E_AUTH); + ENABLE_EVENT(WLC_E_ASSOC); + ENABLE_EVENT(WLC_E_LINK); + ENABLE_EVENT(WLC_E_DISASSOC_IND); + ENABLE_EVENT(WLC_E_DEAUTH); + ENABLE_EVENT(WLC_E_DEAUTH_IND); + ENABLE_EVENT(WLC_E_AUTH_FAIL); + ENABLE_EVENT(WLC_E_PSK_SUP); + ENABLE_EVENT(WLC_E_SET_SSID); +#undef ENABLE_EVENT + ret = brcmfmac_bcdc_iovar_set(data, "event_msgs", + event_mask, sizeof(event_mask)); + if (ret != 0) { + LOG_ERR("event_msgs set failed: %d", ret); + return ret; + } + LOG_DBG("event_msgs set (escan + auth/assoc/link/disassoc/set_ssid)"); + + /* Mirror Linux brcmf_dongle_roam + brcmf_cfg80211_set_power_mgmt + * setup. Without these the firmware's defaults left us self-deauthing + * after ~10 Mbit/s UDP bursts (chip emitted WLC_E_LINK reason=2 + * BRCMF_E_REASON_DEAUTH without an inbound deauth frame -- i.e. the + * firmware itself decided the AP was unreachable). + * + * Specifically: + * - roam_off=1 disables firmware-internal roaming (we have one AP) + * - bcn_timeout=4 matches Linux's roam-off default + * - pm=PM_FAST + pm2_sleep_ret=2000 matches Linux's default + * "power save on, but wake immediately on TX activity" + * + * Best-effort: errors are logged but non-fatal -- the chip still + * brings up, just with looser defaults. + */ + { + const uint32_t roam_off = 1; /* no firmware-side roaming */ + const uint32_t bcn_timeout = 4; /* seconds without beacons */ + const uint32_t pm_mode = 2; /* PM_FAST */ + const uint32_t pm2_sleep = 2000; /* ms */ + const uint32_t allmulti = 1; /* pass multicast to host */ + int rret; + + rret = brcmfmac_bcdc_iovar_set(data, "roam_off", + (const uint8_t *)&roam_off, 4); + if (rret != 0) { + LOG_WRN("roam_off=1 set failed: %d (best-effort)", rret); + } + rret = brcmfmac_bcdc_iovar_set(data, "bcn_timeout", + (const uint8_t *)&bcn_timeout, 4); + if (rret != 0) { + LOG_WRN("bcn_timeout=4 set failed: %d (best-effort)", rret); + } + rret = brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_SET_PM, + (const uint8_t *)&pm_mode, 4); + if (rret != 0) { + LOG_WRN("WLC_SET_PM=FAST set failed: %d (best-effort)", rret); + } + rret = brcmfmac_bcdc_iovar_set(data, "pm2_sleep_ret", + (const uint8_t *)&pm2_sleep, 4); + if (rret != 0) { + LOG_WRN("pm2_sleep_ret=2000 set failed: %d (best-effort)", rret); + } + /* allmulti=1: pass all multicast frames up to the host. The + * chip otherwise hardware-filters multicast, so 224.0.0.251 + * (mDNS) never reaches the Zephyr net stack -- DHCP works only + * because it is broadcast. Zephyr's IGMP layer still filters + * to the joined groups; this just opens the chip-side gate. + */ + rret = brcmfmac_bcdc_iovar_set(data, "allmulti", + (const uint8_t *)&allmulti, 4); + if (rret != 0) { + LOG_WRN("allmulti=1 set failed: %d (best-effort)", rret); + } + LOG_DBG("post-up tuning: roam_off=1 bcn=4s pm=FAST sleep_ret=2000ms allmulti=1"); + } + + data->probed = true; + LOG_INF("initialization complete in %lld ms; awaiting iface_init", + (long long)(k_uptime_get() - t0)); + return 0; +} + +/* === wifi_mgmt + ethernet_api wiring ====================================== */ + +static const struct wifi_mgmt_ops brcmfmac_mgmt_ops = { + .scan = brcmfmac_mgmt_scan, + .connect = brcmfmac_mgmt_connect, + .disconnect = brcmfmac_mgmt_disconnect, + .iface_status = brcmfmac_mgmt_iface_status, +}; + +static const struct net_wifi_mgmt_offload brcmfmac_api = { + .wifi_iface.iface_api.init = brcmfmac_iface_init, + .wifi_iface.send = brcmfmac_iface_send, + .wifi_mgmt_api = &brcmfmac_mgmt_ops, +}; + +/* Single-instance: the brcmfmac DT binding describes one chip on the + * SDHC parent. Multi-instance would need a per-inst api + data + config + * (a la DT_INST_FOREACH_STATUS_OKAY); deferred until we actually need it. + */ +static const struct brcmfmac_config brcmfmac_config_0 = { + .sdhc = DEVICE_DT_GET(DT_INST_PARENT(0)), + .reg_on = GPIO_DT_SPEC_INST_GET_OR(0, wifi_reg_on_gpios, {0}), + .firmware_name = DT_INST_PROP_OR(0, firmware_name, ""), +}; + +static struct brcmfmac_data brcmfmac_data_0; + +/* Diagnostic: read the chip's "counters" iovar and log the words we care + * about for the post-burst tx-credit investigation. Word indices identified + * by counter-delta sweep (see project memory) and are stable + * across reboots for the BCM43430A1 7.45.96.s1 firmware on the AP we use. + * + * word 1 = txframe (frames offered to PHY) + * word 2 = txbyte (bytes offered to PHY) + * word 3 = txretrans (per-frame retransmissions on air) + * word 4 = txerror (frames that errored out -- no ack after retry) + * word 14 = (txphyerr / txnobuf -- chip-side TX queue / PHY failure) + * word 18 = rxerror + * word 20 = rxnobuf (chip's host-RX FIFO overflow) + * + * Not safe from ISR (does a CMD53 round-trip). Call from a thread. + */ +void brcmfmac_counters_dump(const char *label) +{ + static uint8_t buf[1500]; + int got = brcmfmac_bcdc_iovar_get(&brcmfmac_data_0, "counters", + buf, sizeof(buf)); + if (got < 0) { + LOG_ERR("counters[%s]: iovar_get failed: %d", + label ? label : "?", got); + return; + } + if (got < 84) { + LOG_ERR("counters[%s]: short read: %d bytes", + label ? label : "?", got); + return; + } + + const uint32_t *w = (const uint32_t *)buf; + + LOG_INF("counters[%s]: txframe=%u txbyte=%u txretrans=%u txerror=%u " + "w14=%u rxerror=%u rxnobuf=%u (resp=%d B)", + label ? label : "?", + w[1], w[2], w[3], w[4], w[14], w[18], w[20], got); +} + +/* Public iovar interface -- exposed for outside callers (MP user module, + * test code) that want to introspect or tweak the chip without reaching + * into driver-private state. `dev` is the brcmfmac net device, typically + * obtained via `net_if_get_device(net_if_get_first_wifi())`. Both paths + * do a CMD53 round-trip via BCDC -- not safe from ISR. + */ +int brcmfmac_iovar_get(const struct device *dev, const char *name, + uint8_t *buf, uint16_t len) +{ + return brcmfmac_bcdc_iovar_get(dev->data, name, buf, len); +} + +int brcmfmac_iovar_set(const struct device *dev, const char *name, + const uint8_t *value, uint16_t value_len) +{ + return brcmfmac_bcdc_iovar_set(dev->data, name, value, value_len); +} + +NET_DEVICE_DT_INST_DEFINE(0, brcmfmac_init, NULL, + &brcmfmac_data_0, &brcmfmac_config_0, + CONFIG_WIFI_INIT_PRIORITY, &brcmfmac_api, + ETHERNET_L2, + NET_L2_GET_CTX_TYPE(ETHERNET_L2), + NET_ETH_MTU); + +CONNECTIVITY_WIFI_MGMT_BIND(Z_DEVICE_DT_DEV_ID(DT_DRV_INST(0))); diff --git a/drivers/wifi/brcmfmac/brcmfmac_fw_blob.c b/drivers/wifi/brcmfmac/brcmfmac_fw_blob.c new file mode 100644 index 0000000000000..aa1eda1ab37da --- /dev/null +++ b/drivers/wifi/brcmfmac/brcmfmac_fw_blob.c @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2026 Jonathan Elliot Peace + * + * SPDX-License-Identifier: Apache-2.0 + * + * brcmfmac firmware, NVRAM and (optional) CLM blob payloads. The actual + * bytes are generated by CMake from the corresponding files in + * hal_broadcom (fetched via `west blobs fetch hal_broadcom`) and + * #include'd here. An unset CONFIG_WIFI_BRCMFMAC_CLM_FILE links an + * empty CLM array and the driver skips the upload at runtime. + */ + +const unsigned char brcmfmac_fw[] = { +#include "brcmfmac_fw.inc" +}; +const unsigned int brcmfmac_fw_len = sizeof(brcmfmac_fw); + +const unsigned char brcmfmac_nvram[] = { +#include "brcmfmac_nvram.inc" +}; +const unsigned int brcmfmac_nvram_len = sizeof(brcmfmac_nvram); + +const unsigned char brcmfmac_clm_blob[] = { +#include "brcmfmac_clm_blob.inc" +}; +const unsigned int brcmfmac_clm_blob_len = sizeof(brcmfmac_clm_blob); diff --git a/drivers/wifi/brcmfmac/brcmfmac_net.c b/drivers/wifi/brcmfmac/brcmfmac_net.c new file mode 100644 index 0000000000000..903f32079b4c9 --- /dev/null +++ b/drivers/wifi/brcmfmac/brcmfmac_net.c @@ -0,0 +1,704 @@ +/* + * Copyright (c) 2026 Jonathan Elliot Peace + * SPDX-License-Identifier: Apache-2.0 + * + * net_if + wifi_mgmt glue + RX dispatch hooks for chan=1 (event) and + * chan=2 (data) frames coming from the brcmfmac RX thread. + * + * iface_api.init + iface_api.send, RX -> net_recv_data, + * wifi_mgmt_ops.scan via the "escan" IOVAR. Connect/disconnect and + * full event parsing (WLC_E_LINK / WLC_E_AUTH / WLC_E_DISASSOC_IND) + * land in 4.5b. + */ + +#include + +#include +#include +/* net_if.h must precede dhcpv4.h -- the latter forward-declares + * struct net_if in parameter-list scope only. + */ +#include +#include +#include +#include +#include +#include + +#include "brcmfmac_priv.h" + +/* Linux brcmfmac fweh.h: flag bit in the event_msg flags field that + * indicates "link is up" for WLC_E_LINK events. + */ +#define BRCMF_EVENT_MSG_LINK 0x01 + +LOG_MODULE_DECLARE(brcmfmac, CONFIG_WIFI_LOG_LEVEL); + +#define BRCMFMAC_ETH_FRAME_BUF_SIZE 1600 + +/* === net_if init =========================================================== */ + +void brcmfmac_iface_init(struct net_if *iface) +{ + const struct device *dev = net_if_get_device(iface); + struct brcmfmac_data *data = dev->data; + struct ethernet_context *eth_ctx = net_if_l2_data(iface); + + eth_ctx->eth_if_type = L2_ETH_IF_TYPE_WIFI; + data->iface = iface; + + if (net_if_set_link_addr(iface, data->chip_mac, 6, NET_LINK_ETHERNET) != 0) { + LOG_ERR("iface_init: net_if_set_link_addr failed"); + } + + ethernet_init(iface); + net_if_dormant_on(iface); + + /* Admin-up the iface so dormant_off transitions cleanly to IF_UP + * once association lands. Zephyr's auto-admin-up on net stack + * init is not reliable for native-L2 Wi-Fi drivers; explicit is + * safer. + */ + (void)net_if_up(iface); + + LOG_INF("iface up: MAC %02x:%02x:%02x:%02x:%02x:%02x (dormant until assoc)", + data->chip_mac[0], data->chip_mac[1], data->chip_mac[2], + data->chip_mac[3], data->chip_mac[4], data->chip_mac[5]); +} + +/* === TX path (L2 frame -> BDC + SDPCM -> F2 chan=2) ======================== */ + +int brcmfmac_iface_send(const struct device *dev, struct net_pkt *pkt) +{ + struct brcmfmac_data *data = dev->data; + size_t pkt_len = net_pkt_get_len(pkt); + + if (!data->probed || !data->f2_ready) { + return -ENETDOWN; + } + + const size_t hdr_len = sizeof(struct sdpcm_frame_hdr) + + sizeof(struct sdpcm_sw_hdr) + + sizeof(struct bdc_hdr); + + if (hdr_len + pkt_len > BRCMFMAC_TX_SLOT_SIZE) { + LOG_ERR("iface_send: oversize (%zu+%zu > %u)", + hdr_len, pkt_len, (unsigned int)BRCMFMAC_TX_SLOT_SIZE); + return -EMSGSIZE; + } + + /* Claim the next ring slot. iface_send is the sole producer, so head + * is owned by us; tail is owned by the tx-thread. If head+1 == tail + * the ring is full -- wait briefly for the consumer to drain. + */ + int head = atomic_get(&data->tx_ring_head); + int next = (head + 1) % BRCMFMAC_TX_RING_SLOTS; + + for (int retry = 0; next == atomic_get(&data->tx_ring_tail); retry++) { + if (retry >= 20) { + return -ENOBUFS; + } + k_sleep(K_MSEC(1)); + } + + struct brcmfmac_tx_slot *slot = &data->tx_ring[head]; + + /* Build SDPCM sw_hdr + BDC hdr in-place. fh->len/notlen + sw->seq + * are filled by the tx-thread at flush time when it knows the + * batch's seq numbers. + */ + struct sdpcm_sw_hdr *sw = + (struct sdpcm_sw_hdr *)(slot->data + sizeof(struct sdpcm_frame_hdr)); + sw->seq = 0; + sw->chan = SDPCM_CHAN_DATA; + sw->nextlen = 0; + sw->hdrlen = sizeof(struct sdpcm_frame_hdr) + sizeof(struct sdpcm_sw_hdr); + sw->flow = 0; + sw->credit = 0; + sw->reserved[0] = 0; + sw->reserved[1] = 0; + + struct bdc_hdr *bdc = + (struct bdc_hdr *)(slot->data + sizeof(struct sdpcm_frame_hdr) + + sizeof(struct sdpcm_sw_hdr)); + bdc->flags = BDC_PROTO_VER << BDC_PROTO_VER_SHIFT; + bdc->priority = 0; + bdc->flags2 = 0; + bdc->data_offset = 0; + + if (net_pkt_read(pkt, slot->data + hdr_len, pkt_len) < 0) { + LOG_ERR("iface_send: net_pkt_read failed"); + return -EIO; + } + slot->len = (uint16_t)(hdr_len + pkt_len); + + /* Publish to consumer + wake it. The tx-thread drains the ring + * and issues one glommed CMD53 per batch. + */ + atomic_set(&data->tx_ring_head, next); + k_sem_give(&data->tx_pending_sem); + return 0; +} + +/* === RX dispatch helpers (called from bcdc.c RX thread) ==================== */ + +/* Strip BDC header + data_offset padding; return pointer/length of the L2 + * frame within `body`. Returns NULL on malformed input. + */ +static const uint8_t *bdc_strip(const uint8_t *body, uint16_t body_len, + uint16_t *l2_len_out) +{ + if (body_len < BDC_HEADER_LEN) { + return NULL; + } + const struct bdc_hdr *bdc = (const void *)body; + uint16_t skip = BDC_HEADER_LEN + (uint16_t)(bdc->data_offset * 4); + + if (skip > body_len) { + return NULL; + } + *l2_len_out = body_len - skip; + return body + skip; +} + +void brcmfmac_net_rx_data(struct brcmfmac_data *data, + const uint8_t *body, uint16_t body_len) +{ + uint16_t l2_len = 0; + const uint8_t *l2 = bdc_strip(body, body_len, &l2_len); + + if (l2 == NULL) { + LOG_WRN("rx data: BDC strip failed (body_len=%u)", body_len); + return; + } + + LOG_DBG("RX len=%u src=%02x:%02x:%02x:%02x:%02x:%02x type=0x%02x%02x", + l2_len, + l2[6], l2[7], l2[8], l2[9], l2[10], l2[11], + l2[12], l2[13]); + + if (data->iface == NULL || !net_if_flag_is_set(data->iface, NET_IF_UP)) { + /* Iface not up yet (e.g. pre-assoc). Drop. */ + LOG_WRN("rx data: iface not up, dropping"); + return; + } + + struct net_pkt *pkt = net_pkt_rx_alloc_with_buffer(data->iface, l2_len, + AF_UNSPEC, 0, + K_NO_WAIT); + if (pkt == NULL) { + LOG_ERR("rx data: net_pkt alloc failed (len=%u)", l2_len); + return; + } + + if (net_pkt_write(pkt, l2, l2_len) < 0) { + LOG_ERR("rx data: net_pkt_write failed"); + net_pkt_unref(pkt); + return; + } + + if (net_recv_data(data->iface, pkt) < 0) { + LOG_WRN("rx data: net_recv_data rejected"); + net_pkt_unref(pkt); + } +} + +/* === Event parsing (chan=1) ================================================ + * + * Body layout after BDC strip: + * ethhdr (14) -- dst[6] src[6] type[2 BE] (type == 0x886C) + * brcm_ethhdr (10) + * brcmf_event_msg_be (36 BE) + * event-specific payload + */ +#define EVENT_FIXED_HEADER_LEN (14 + sizeof(struct brcm_ethhdr) + \ + sizeof(struct brcmf_event_msg_be)) + +static void brcmfmac_handle_escan_event(struct brcmfmac_data *data, + uint32_t status, uint32_t datalen, + const uint8_t *payload) +{ + if (data->scan_cb == NULL || data->iface == NULL) { + return; + } + + if (status == BRCMF_E_STATUS_PARTIAL) { + if (datalen < sizeof(struct brcmf_escan_result_le)) { + LOG_WRN("escan partial: undersized payload (%u)", datalen); + return; + } + const struct brcmf_escan_result_le *res = (const void *)payload; + const struct brcmf_bss_info_le *bss = &res->bss_info_le; + + struct wifi_scan_result entry = {0}; + + uint8_t ssid_len = bss->SSID_len; + + if (ssid_len > WIFI_SSID_MAX_LEN) { + ssid_len = WIFI_SSID_MAX_LEN; + } + memcpy(entry.ssid, bss->SSID, ssid_len); + entry.ssid_length = ssid_len; + + memcpy(entry.mac, bss->BSSID, 6); + entry.mac_length = 6; + + /* BCM43430A1 is 2.4 GHz only -- low byte of chanspec is + * the control channel. + */ + entry.band = WIFI_FREQ_BAND_2_4_GHZ; + entry.channel = (uint8_t)(bss->chanspec & 0xFF); + + /* Defer real RSN/WPA-IE parsing to 4.5c. For 4.5a, mark + * security UNKNOWN so the shell prints something visible + * but no assumption is made. + */ + entry.security = WIFI_SECURITY_TYPE_UNKNOWN; + entry.mfp = WIFI_MFP_DISABLE; + + int16_t rssi = (int16_t)bss->RSSI; + + entry.rssi = (int8_t)rssi; + + data->scan_cb(data->iface, 0, &entry); + } else if (status == BRCMF_E_STATUS_SUCCESS) { + LOG_DBG("escan complete"); + data->scan_cb(data->iface, 0, NULL); + data->scan_cb = NULL; + } else { + LOG_WRN("escan event status=%u (treating as failure)", status); + data->scan_cb(data->iface, -EIO, NULL); + data->scan_cb = NULL; + } +} + +void brcmfmac_net_rx_event(struct brcmfmac_data *data, + const uint8_t *body, uint16_t body_len) +{ + uint16_t l2_len = 0; + const uint8_t *l2 = bdc_strip(body, body_len, &l2_len); + + if (l2 == NULL || l2_len < EVENT_FIXED_HEADER_LEN) { + LOG_DBG("rx event: undersized (body_len=%u)", body_len); + return; + } + + /* Ethernet type at offset 12 (after src+dst MACs). */ + uint16_t ethertype = sys_get_be16(l2 + 12); + + if (ethertype != BRCM_ETHERTYPE_EVENT) { + LOG_DBG("rx event: non-Broadcom ethertype 0x%04x", ethertype); + return; + } + + const struct brcmf_event_msg_be *msg = + (const void *)(l2 + 14 + sizeof(struct brcm_ethhdr)); + + uint32_t event_type = sys_be32_to_cpu(msg->event_type); + uint32_t status = sys_be32_to_cpu(msg->status); + uint32_t reason = sys_be32_to_cpu(msg->reason); + uint16_t ev_flags = sys_be16_to_cpu(msg->flags); + uint32_t datalen = sys_be32_to_cpu(msg->datalen); + + const uint8_t *payload = (const uint8_t *)msg + sizeof(*msg); + uint16_t payload_avail = (uint16_t)(l2_len - EVENT_FIXED_HEADER_LEN); + + if (datalen > payload_avail) { + datalen = payload_avail; + } + + switch (event_type) { + case WLC_E_ESCAN_RESULT: + brcmfmac_handle_escan_event(data, status, datalen, payload); + break; + + case WLC_E_AUTH: + LOG_DBG("WLC_E_AUTH status=%u reason=%u", status, reason); + if (status != BRCMF_E_STATUS_SUCCESS) { + data->link_state = BRCMFMAC_LINK_DOWN; + wifi_mgmt_raise_connect_result_event(data->iface, -EIO); + } else { + data->link_state = BRCMFMAC_LINK_ASSOCING; + } + break; + + case WLC_E_ASSOC: + LOG_DBG("WLC_E_ASSOC status=%u reason=%u", status, reason); + if (status != BRCMF_E_STATUS_SUCCESS) { + data->link_state = BRCMFMAC_LINK_DOWN; + wifi_mgmt_raise_connect_result_event(data->iface, -EIO); + } + break; + + case WLC_E_LINK: + if (status == BRCMF_E_STATUS_SUCCESS && + (ev_flags & BRCMF_EVENT_MSG_LINK)) { + LOG_INF("link UP"); + data->link_state = BRCMFMAC_LINK_UP; + if (data->iface != NULL) { + net_if_dormant_off(data->iface); +#if defined(CONFIG_NET_DHCPV4) + net_dhcpv4_restart(data->iface); +#endif + } + wifi_mgmt_raise_connect_result_event(data->iface, 0); + } else { + /* For WLC_E_LINK the firmware fills `reason` with its + * own BRCMF_E_REASON_* code, NOT the 802.11 reason code: + * 0=INITIAL_ASSOC 1=LOW_RSSI 2=DEAUTH 3=DISASSOC + * 4=BCNS_LOST 9=MINTXRATE + * For an AP-initiated deauth the actual 802.11 reason + * lands on the WLC_E_DEAUTH_IND event below. + */ + LOG_INF("link DOWN status=%u reason=%u flags=0x%04x", + status, reason, ev_flags); + data->link_state = BRCMFMAC_LINK_DOWN; + if (data->iface != NULL) { + net_if_dormant_on(data->iface); + } + } + break; + + case WLC_E_DISASSOC_IND: + LOG_INF("WLC_E_DISASSOC_IND reason=%u", reason); + data->link_state = BRCMFMAC_LINK_DOWN; + if (data->iface != NULL) { + net_if_dormant_on(data->iface); + } + wifi_mgmt_raise_disconnect_result_event(data->iface, 0); + break; + + case WLC_E_SET_SSID: + LOG_DBG("WLC_E_SET_SSID status=%u (join %s)", + status, + status == BRCMF_E_STATUS_SUCCESS ? "ok" : "failed"); + if (status != BRCMF_E_STATUS_SUCCESS) { + data->link_state = BRCMFMAC_LINK_DOWN; + wifi_mgmt_raise_connect_result_event(data->iface, -EIO); + } + break; + + case WLC_E_PSK_SUP: + /* PSK_SUP uses its own status-code namespace (WLC_SUP_*), + * NOT the generic BRCMF_E_STATUS_*. status=6 = WLC_SUP_KEYED + * (pairwise key installed -- success). + */ + LOG_DBG("WLC_E_PSK_SUP status=%u reason=%u (6=KEYED)", + status, reason); + break; + + case WLC_E_DEAUTH: + case WLC_E_DEAUTH_IND: + /* `reason` here IS the 802.11 deauth reason code (frame body). */ + LOG_INF("WLC_E_DEAUTH%s reason=%u", + event_type == WLC_E_DEAUTH_IND ? "_IND" : "", reason); + break; + + case WLC_E_AUTH_FAIL: + LOG_INF("WLC_E_AUTH_FAIL status=%u reason=%u", status, reason); + break; + + default: + LOG_DBG("event %u status=%u reason=%u datalen=%u (no handler)", + event_type, status, reason, datalen); + break; + } +} + +/* === wifi_mgmt_ops.scan ==================================================== */ + +int brcmfmac_mgmt_scan(const struct device *dev, struct net_if *iface, + struct wifi_scan_params *params, scan_result_cb_t cb) +{ + struct brcmfmac_data *data = dev->data; + + ARG_UNUSED(iface); + ARG_UNUSED(params); + + if (!data->probed) { + return -ENETDOWN; + } + if (data->scan_cb != NULL) { + return -EINPROGRESS; + } + + struct brcmf_escan_params_le esc; + + memset(&esc, 0, sizeof(esc)); + /* All multi-byte fields in this struct are little-endian on the wire; + * convert explicitly so the host CPU's endianness doesn't matter. + */ + esc.version = sys_cpu_to_le32(BRCMF_SCAN_PARAMS_VERSION); + esc.action = sys_cpu_to_le16(WL_ESCAN_ACTION_START); + esc.sync_id = sys_cpu_to_le16(1); + + esc.params_le.bss_type = 2; /* DOT11_BSSTYPE_ANY (int8) */ + esc.params_le.scan_type = 0; /* active (uint8) */ + esc.params_le.nprobes = sys_cpu_to_le32((uint32_t)-1); + esc.params_le.active_time = sys_cpu_to_le32((uint32_t)-1); + esc.params_le.passive_time = sys_cpu_to_le32((uint32_t)-1); + esc.params_le.home_time = sys_cpu_to_le32((uint32_t)-1); + esc.params_le.channel_num = sys_cpu_to_le32(0); /* all channels */ + memset(esc.params_le.bssid, 0xFF, 6); /* broadcast */ + esc.params_le.ssid_le.SSID_len = sys_cpu_to_le32(0); /* broadcast SSID */ + + /* Park the cb BEFORE firing the IOVAR so an event arriving fast + * (which we've observed -- the chip can have queued frames) has + * a place to land. + */ + data->scan_cb = cb; + + int ret = brcmfmac_bcdc_iovar_set(data, "escan", + (const uint8_t *)&esc, sizeof(esc)); + if (ret != 0) { + LOG_ERR("escan iovar_set failed: %d", ret); + data->scan_cb = NULL; + return ret; + } + LOG_DBG("escan started -- results streaming via WLC_E_ESCAN_RESULT"); + return 0; +} + +/* === wifi_mgmt_ops.connect (WPA2-PSK) ====================================== + * + * Sequence (each step is plain iovar / WLC cmd; bsscfgidx=0 = STA, which + * Linux's brcmf_create_bsscfg short-circuits to a plain iovar with no + * prefix wrapping): + * + * 1. iovar "mpc" = 0 disable multi-power-control during assoc + * 2. iovar "auth" = 0 802.11 OPEN -- WPA2 does its EAPOL after + * 3. iovar "wsec" = 4 AES_ENABLED + * 4. iovar "wpa_auth" = 0x84 WPA_PSK | WPA2_PSK (mixed-mode compat) + * 5. cmd WLC_SET_WSEC_PMK passphrase + flags=PASSPHRASE; chip derives PMK + * 6. cmd WLC_SET_SSID fires the join; events stream on chan=1 + */ +int brcmfmac_mgmt_connect(const struct device *dev, struct net_if *iface, + struct wifi_connect_req_params *params) +{ + struct brcmfmac_data *data = dev->data; + + ARG_UNUSED(iface); + int ret; + + if (!data->probed) { + return -ENETDOWN; + } + if (params == NULL || params->ssid == NULL || params->ssid_length == 0 || + params->ssid_length > BRCMF_SSID_MAX_LEN) { + return -EINVAL; + } + if (params->security != WIFI_SECURITY_TYPE_PSK && + params->security != WIFI_SECURITY_TYPE_PSK_SHA256) { + LOG_ERR("connect: only WPA2-PSK supported (got security=%u)", + params->security); + return -ENOTSUP; + } + if (params->psk == NULL || + params->psk_length < 8 || + params->psk_length > BRCMF_WSEC_MAX_PASSPHRASE_LEN) { + return -EINVAL; + } + + LOG_INF("connect: ssid=\"%.*s\" psk=<%u bytes>", + params->ssid_length, params->ssid, params->psk_length); + + data->link_state = BRCMFMAC_LINK_AUTHING; + memcpy(data->connected_ssid, params->ssid, params->ssid_length); + data->connected_ssid_len = params->ssid_length; + data->connected_security = params->security; + + /* Sequence mirrors a known-working bare-metal join path for the + * same chip family. Plain WLC commands for infra/auth/wsec (chip + * doesn't accept these as plain iovars on all firmware variants); + * bsscfg-prefixed iovars to enable the firmware supplicant; PMK + * before wpa_auth so the chip has key material when assoc fires. + */ + const uint32_t infra = 1; + const uint32_t auth = 0; /* 802.11 OPEN */ + const uint32_t wsec = 6; /* TKIP | AES (mixed AP) */ + const uint32_t wpa_auth = 0x80; /* WPA2_AUTH_PSK */ + + ret = brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_SET_INFRA, + (const uint8_t *)&infra, 4); + if (ret != 0) { + LOG_ERR("connect: WLC_SET_INFRA=1 failed: %d", ret); + goto fail; + } + ret = brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_SET_AUTH, + (const uint8_t *)&auth, 4); + if (ret != 0) { + LOG_ERR("connect: WLC_SET_AUTH=0 failed: %d", ret); + goto fail; + } + ret = brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_SET_WSEC, + (const uint8_t *)&wsec, 4); + if (ret != 0) { + LOG_ERR("connect: WLC_SET_WSEC=%u failed: %d", wsec, ret); + goto fail; + } + + /* Enable firmware-side WPA supplicant. The "bsscfg:" prefix is + * required even for bsscfgidx=0 on this firmware -- the plain + * iovar variant returns OK but doesn't actually take effect. + */ + ret = brcmfmac_bcdc_bsscfg_iovar_set_int(data, "sup_wpa", 0, 1); + if (ret != 0) { + LOG_ERR("connect: bsscfg:sup_wpa=1 failed: %d", ret); + goto fail; + } + ret = brcmfmac_bcdc_bsscfg_iovar_set_int(data, "sup_wpa2_eapver", 0, -1); + if (ret != 0) { + LOG_ERR("connect: bsscfg:sup_wpa2_eapver=-1 failed: %d", ret); + goto fail; + } + ret = brcmfmac_bcdc_bsscfg_iovar_set_int(data, "sup_wpa_tmo", 0, 2500); + if (ret != 0) { + LOG_ERR("connect: bsscfg:sup_wpa_tmo=2500 failed: %d", ret); + goto fail; + } + + struct brcmf_wsec_pmk_le pmk; + + memset(&pmk, 0, sizeof(pmk)); + pmk.key_len = params->psk_length; + pmk.flags = BRCMF_WSEC_PASSPHRASE; /* chip-side PBKDF2 derives PMK */ + memcpy(pmk.key, params->psk, params->psk_length); + + ret = brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_SET_WSEC_PMK, + (const uint8_t *)&pmk, sizeof(pmk)); + if (ret != 0) { + LOG_ERR("connect: WLC_SET_WSEC_PMK failed: %d", ret); + goto fail; + } + + ret = brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_SET_WPA_AUTH, + (const uint8_t *)&wpa_auth, 4); + if (ret != 0) { + LOG_ERR("connect: WLC_SET_WPA_AUTH=0x%x failed: %d", + wpa_auth, ret); + goto fail; + } + + struct brcmf_ssid_le ssid_le; + + memset(&ssid_le, 0, sizeof(ssid_le)); + ssid_le.SSID_len = params->ssid_length; + memcpy(ssid_le.SSID, params->ssid, params->ssid_length); + + ret = brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_SET_SSID, + (const uint8_t *)&ssid_le, sizeof(ssid_le)); + if (ret != 0) { + LOG_ERR("connect: WLC_SET_SSID failed: %d", ret); + goto fail; + } + + LOG_DBG("connect: join fired -- awaiting WLC_E_LINK on chan=1"); + return 0; + +fail: + data->link_state = BRCMFMAC_LINK_DOWN; + return ret; +} + +int brcmfmac_mgmt_disconnect(const struct device *dev, struct net_if *iface) +{ + struct brcmfmac_data *data = dev->data; + + ARG_UNUSED(iface); + + if (!data->probed) { + return -ENETDOWN; + } + + int ret = brcmfmac_bcdc_set_dcmd(data, BRCMFMAC_WLC_DISASSOC, NULL, 0); + + if (ret != 0) { + LOG_ERR("disconnect: WLC_DISASSOC failed: %d", ret); + return ret; + } + + data->link_state = BRCMFMAC_LINK_DOWN; + data->connected_ssid_len = 0; + if (data->iface != NULL) { + net_if_dormant_on(data->iface); + } + LOG_DBG("disconnect: fired -- awaiting WLC_E_DISASSOC_IND on chan=1"); + return 0; +} + +/* === wifi_mgmt_ops.iface_status ============================================ + * + * state + ssid + security from last connect(), plus rssi via a + * WLC_GET_RSSI round-trip when the link is up. Channel stubbed to 0. + */ +int brcmfmac_mgmt_iface_status(const struct device *dev, struct net_if *iface, + struct wifi_iface_status *status) +{ + struct brcmfmac_data *data = dev->data; + + ARG_UNUSED(iface); + + memset(status, 0, sizeof(*status)); + status->band = WIFI_FREQ_BAND_2_4_GHZ; + status->iface_mode = WIFI_MODE_INFRA; + status->link_mode = WIFI_LINK_MODE_UNKNOWN; + status->security = data->connected_security; + status->mfp = WIFI_MFP_OPTIONAL; + status->channel = 0; + status->rssi = 0; + + switch (data->link_state) { + case BRCMFMAC_LINK_DOWN: + status->state = WIFI_STATE_DISCONNECTED; + break; + case BRCMFMAC_LINK_AUTHING: + status->state = WIFI_STATE_AUTHENTICATING; + break; + case BRCMFMAC_LINK_ASSOCING: + status->state = WIFI_STATE_ASSOCIATING; + break; + case BRCMFMAC_LINK_UP: + status->state = WIFI_STATE_COMPLETED; + break; + } + + if (data->link_state == BRCMFMAC_LINK_UP) { + /* WLC_GET_BSSID returns the associated AP's BSSID. */ + uint8_t bssid[6] = { 0 }; + int rc_bssid = brcmfmac_bcdc_query_dcmd( + data, BRCMFMAC_WLC_GET_BSSID, NULL, 0, + bssid, sizeof(bssid)); + if (rc_bssid >= (int)sizeof(bssid)) { + memcpy(status->bssid, bssid, sizeof(status->bssid)); + } + + /* WLC_GET_RSSI fills a scb_val_t { int32 val; u8 ea[6] } and + * returns the RSSI in val. The request buffer must be the + * full sizeof(scb_val_t) -- 12 bytes: the 10 logical bytes + * rounded up to the struct's 4-byte alignment. The firmware + * length-checks the request and rejects a 10-byte buffer with + * BCME_BADARG; the tail padding is not optional. A zeroed ea + * selects the connected AP (matches Linux brcmfmac). + */ + uint8_t scb_val[12] = { 0 }; + int rc = brcmfmac_bcdc_query_dcmd( + data, BRCMFMAC_WLC_GET_RSSI, + scb_val, sizeof(scb_val), + scb_val, sizeof(scb_val)); + if (rc >= 4) { + int32_t rssi; + + memcpy(&rssi, scb_val, sizeof(rssi)); + status->rssi = (int8_t)rssi; + } + } + + if (data->connected_ssid_len > 0 && + data->connected_ssid_len <= sizeof(status->ssid)) { + memcpy(status->ssid, data->connected_ssid, + data->connected_ssid_len); + status->ssid_len = data->connected_ssid_len; + } + + return 0; +} diff --git a/drivers/wifi/brcmfmac/brcmfmac_priv.h b/drivers/wifi/brcmfmac/brcmfmac_priv.h new file mode 100644 index 0000000000000..24129421e2ec0 --- /dev/null +++ b/drivers/wifi/brcmfmac/brcmfmac_priv.h @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2026 Jonathan Elliot Peace + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_WIFI_BRCMFMAC_PRIV_H_ +#define ZEPHYR_DRIVERS_WIFI_BRCMFMAC_PRIV_H_ + +#include +#include + +#include +#include +#include +#include +#include +#include + +/* === SBSDIO function-1 misc-register layout ================================= + * + * F1 exposes a 32 KiB sliding window onto the chip's AXI/SB backplane; + * the SBADDR* registers set the window base, then accesses to func1 + * offset [0..0x7FFF] map to backplane[base + offset]. Setting bit 15 + * in the offset (SB_ACCESS_2_4B_FLAG) makes a 4-byte access at the + * chip side, not four single-byte accesses. + */ +#define SBSDIO_FUNC1_SBADDRLOW 0x1000A +#define SBSDIO_FUNC1_SBADDRMID 0x1000B +#define SBSDIO_FUNC1_SBADDRHIGH 0x1000C +#define SBSDIO_FUNC1_CHIPCLKCSR 0x1000E +#define SBSDIO_FUNC1_SDIOPULLUP 0x1000F +#define SBSDIO_SB_OFT_ADDR_MASK 0x07FFF +#define SBSDIO_SB_ACCESS_2_4B_FLAG 0x08000 +#define SBSDIO_SBWINDOW_MASK 0xFFFF8000 +#define SBSDIO_SB_OFT_ADDR_LIMIT 0x8000 + +/* CHIPCLKCSR bits. */ +#define SBSDIO_FORCE_ALP 0x01 +#define SBSDIO_ALP_AVAIL_REQ 0x08 +#define SBSDIO_HT_AVAIL_REQ 0x10 +#define SBSDIO_FORCE_HW_CLKREQ_OFF 0x20 +#define SBSDIO_ALP_AVAIL 0x40 +#define SBSDIO_HT_AVAIL 0x80 +#define SBSDIO_AVBITS (SBSDIO_ALP_AVAIL | SBSDIO_HT_AVAIL) +#define BRCMF_INIT_CLKCTL1 (SBSDIO_FORCE_HW_CLKREQ_OFF | \ + SBSDIO_ALP_AVAIL_REQ) + +/* Chipcommon enumeration base + chipid register layout. */ +#define BRCMF_SI_ENUM_BASE 0x18000000 +#define CID_ID_MASK 0x0000FFFF +#define CID_REV_MASK 0x000F0000 +#define CID_REV_SHIFT 16 +#define CID_TYPE_MASK 0xF0000000 +#define CID_TYPE_SHIFT 28 + +/* Broadcom/Cypress chipcommon[0] chip-ID values. The chip reports these + * as the low 16 bits of CHIPID; brcmfmac uses them to select chip-specific + * code paths (firmware blob, RAM layout, SOCRAM bank-3 quirk). Mirrors + * Linux brcm_hw_ids.h. + */ +#define BRCM_CC_43430_CHIP_ID 0xa9a6 /* BCM43430 / BCM43436 */ +#define BRCM_CC_43439_CHIP_ID 0xa9af /* CYW43439 */ +#define BRCM_CC_4345_CHIP_ID 0x4345 /* BCM43458F */ + +/* BCMA core IDs (subset; full list in Linux include/linux/bcma/bcma.h). */ +#define BCMA_CORE_INTERNAL_MEM 0x80E /* SOCRAM */ +#define BCMA_CORE_80211 0x812 /* D11 MAC */ +#define BCMA_CORE_PMU 0x827 +#define BCMA_CORE_SDIO_DEV 0x829 +#define BCMA_CORE_ARM_CM3 0x82A +#define BCMA_CORE_ARM_CR4 0x83E +#define BCMA_CORE_GCI 0x840 + +/* SDIO core register offset within data base. */ +#define SDPCMD_INTSTATUS 0x20 /* W1C ack-all = 0xFFFFFFFF */ +#define SDPCMD_HOSTINTMASK 0x24 /* which intstatus bits raise DAT1 (host-IRQ) */ + +/* Chip-side host-int mask (mirrors Linux HOSTINTMASK in brcmfmac/sdio.c): + * I_HMB_SW_MASK = 0x000000f0 -- mailbox SW interrupts 0..3 (incl. FRAME_IND) + * I_CHIPACTIVE = 0x20000000 -- chip transitioned from doze to active + * Without this, the chip processes events but never asserts DAT1, so the + * SDHC CARD_INT path stays silent and the brcmfmac RX thread sleeps forever. + */ +#define BRCMFMAC_HOSTINTMASK (0x000000F0u | (1u << 29)) + +/* Individual SDPCMD intstatus bits (subset of I_HMB_SW_MASK). + * Linux names from sdio.c: I_HMB_SW0..SW3 map to FC_STATE, FC_CHANGE, + * FRAME_IND, HOST_INT respectively. + */ +#define BRCMFMAC_I_HMB_FC_STATE (1u << 4) /* HMB_SW0 -- chip xoff'd */ +#define BRCMFMAC_I_HMB_FC_CHANGE (1u << 5) /* HMB_SW1 -- fc transition*/ +#define BRCMFMAC_I_HMB_FRAME_IND (1u << 6) /* HMB_SW2 -- frame avail */ +#define BRCMFMAC_I_HMB_HOST_INT (1u << 7) /* HMB_SW3 -- hostmail data*/ + +/* BCMA wrapper-register offsets (within wrapbase). */ +#define BCMA_IOCTL 0x0408 +#define BCMA_IOCTL_CLK 0x0001 +#define BCMA_IOCTL_FGC 0x0002 +#define BCMA_RESET_CTL 0x0800 +#define BCMA_RESET_CTL_RESET 0x0001 + +/* D11-specific IOCTL bits. */ +#define D11_BCMA_IOCTL_PHYCLOCKEN 0x0004 +#define D11_BCMA_IOCTL_PHYRESET 0x0008 + +/* CR4-specific IOCTL bits. */ +#define ARMCR4_BCMA_IOCTL_CPUHALT 0x0020 + +/* SOCRAM register offsets (within SOCRAM core base). */ +#define SOCRAM_BANKIDX_OFFSET 0x10 +#define SOCRAM_BANKPDA_OFFSET 0x44 + +/* Per-CMD53 block-mode cap. 512-block CMD53 wedges this chip's SDIO + * state machine (an early errata in the initialization debug): 511 * 64 = 32704. + */ +#define BRCMFMAC_MAX_CMD53_BLOCK_BYTES (511 * 64) + +#define BRCMFMAC_MAX_CORES 12 + +/* === SDPCM + BCDC protocol ================================================ + * + * Wire format for an SDIO control message: + * [4 B SDPCM frame: u16 len, u16 ~len] + * [8 B SDPCM sw hdr: seq, chan, nextlen, hdrlen, flow, credit, rsv x2] + * [16 B BCDC/CDC hdr: cmd, outlen, inlen, flags, status] + * [payload (variable)] + * [padded to 4-byte alignment] + */ +#define BRCMFMAC_F2_FIFO_ADDR 0x8000 /* SB_ACCESS_2_4B_FLAG bit */ +#define BRCMFMAC_F2_BLOCK_SIZE 512 + +/* Selected WLC command IDs (Linux brcmfmac/fwil.h). */ +#define BRCMFMAC_WLC_UP 2 +#define BRCMFMAC_WLC_DOWN 3 +#define BRCMFMAC_WLC_SET_INFRA 20 +#define BRCMFMAC_WLC_SET_AUTH 22 +#define BRCMFMAC_WLC_GET_BSSID 23 +#define BRCMFMAC_WLC_SET_SSID 26 +#define BRCMFMAC_WLC_DISASSOC 52 +#define BRCMFMAC_WLC_SET_PM 86 /* power mgmt: 0=OFF 1=MAX 2=FAST */ +#define BRCMFMAC_WLC_GET_RSSI 127 +#define BRCMFMAC_WLC_SET_WSEC 134 +#define BRCMFMAC_WLC_SET_WPA_AUTH 165 +#define BRCMFMAC_WLC_GET_VAR 262 +#define BRCMFMAC_WLC_SET_VAR 263 +#define BRCMFMAC_WLC_SET_WSEC_PMK 268 + +/* Event-mask length (Linux BRCMF_EVENTING_MASK_LEN = roundup(160,8)/8). */ +#define BRCMFMAC_EVENTING_MASK_LEN 20 + +/* WPA2-PSK assoc constants (Linux brcmu_wifi.h). */ +#define WSEC_AES_ENABLED 0x0004 +#define WPA_AUTH_PSK 0x0004 +#define WPA2_AUTH_PSK 0x0080 +#define WPA_WPA2_AUTH_PSK_MIXED (WPA_AUTH_PSK | WPA2_AUTH_PSK) + +/* wsec_pmk flags (Linux brcmfmac/fwil_types.h). */ +#define BRCMF_WSEC_PASSPHRASE 0x0001 /* key is passphrase, chip derives PMK */ +#define BRCMF_WSEC_MAX_PSK_LEN 32 /* PMK length when flags=0 */ +#define BRCMF_WSEC_MAX_PASSPHRASE_LEN 64 /* generous upper bound */ + +struct brcmf_wsec_pmk_le { + uint16_t key_len; + uint16_t flags; + uint8_t key[64]; /* BRCMF_WSEC_MAX_PASSPHRASE_LEN */ +}; + +/* Event reason codes (Linux brcmfmac/fweh.h subset). */ +#define BRCMFMAC_E_REASON_LINK_BSSCFG_DIS 4 +#define BRCMFMAC_E_REASON_INITIAL_ASSOC 0 +#define BRCMFMAC_E_REASON_LOW_RSSI 1 +#define BRCMFMAC_E_REASON_DISASSOC 2 +#define BRCMFMAC_E_REASON_DEAUTH 3 + +/* Internal connect state (driven by chan=1 event arrival). */ +enum brcmfmac_link_state { + BRCMFMAC_LINK_DOWN = 0, + BRCMFMAC_LINK_AUTHING, + BRCMFMAC_LINK_ASSOCING, + BRCMFMAC_LINK_UP, +}; + +#define BCDC_FLAG_ERROR 0x01 +#define BCDC_FLAG_SET 0x02 +#define BCDC_REQ_ID_SHIFT 16 + +#define SDPCM_CHAN_CTRL 0 +#define SDPCM_CHAN_EVENT 1 +#define SDPCM_CHAN_DATA 2 + +struct sdpcm_frame_hdr { + uint16_t len; + uint16_t notlen; +} __packed; + +struct sdpcm_sw_hdr { + uint8_t seq; + uint8_t chan; + uint8_t nextlen; + uint8_t hdrlen; + uint8_t flow; + uint8_t credit; + uint8_t reserved[2]; +} __packed; + +struct cdc_hdr { + uint32_t cmd; + uint16_t outlen; + uint16_t inlen; + uint32_t flags; + int32_t status; +} __packed; + +/* BDC (Broadcom Data Codec) header sits between SDPCM and the L2 frame + * on chan=1 (event) + chan=2 (data). Distinct from the 16-B CDC IOCTL + * header used on chan=0. + */ +#define BDC_HEADER_LEN 4 +#define BDC_PROTO_VER 2 +#define BDC_PROTO_VER_SHIFT 4 + +struct bdc_hdr { + uint8_t flags; /* [7:4]=ver, [3]=SUM_NEEDED, [2]=SUM_GOOD */ + uint8_t priority; + uint8_t flags2; /* [3:0]=if_idx */ + uint8_t data_offset; /* additional bytes between BDC end and L2 frame, in 4-B units */ +} __packed; + +/* === Broadcom event frame (ethertype 0x886C over chan=1) ================== */ +#define BRCM_ETHERTYPE_EVENT 0x886C + +struct brcm_ethhdr { + uint16_t subtype; /* BE */ + uint16_t length; /* BE */ + uint8_t version; + uint8_t oui[3]; + uint16_t usr_subtype; /* BE */ +} __packed; + +struct brcmf_event_msg_be { + uint16_t version; + uint16_t flags; + uint32_t event_type; + uint32_t status; + uint32_t reason; + uint32_t auth_type; + uint32_t datalen; + uint8_t addr[6]; + char ifname[16]; + uint8_t ifidx; + uint8_t bsscfgidx; +} __packed; + +/* WLC event type codes we care about (Linux fweh.h). */ +#define WLC_E_SET_SSID 0 +#define WLC_E_AUTH 3 +#define WLC_E_ASSOC 7 +#define WLC_E_DISASSOC_IND 12 +#define WLC_E_LINK 16 +#define WLC_E_DEAUTH_IND 33 +#define WLC_E_DEAUTH 34 +#define WLC_E_AUTH_FAIL 42 +#define WLC_E_PSK_SUP 46 +#define WLC_E_ESCAN_RESULT 69 + +/* Event status codes. */ +#define BRCMF_E_STATUS_SUCCESS 0 +#define BRCMF_E_STATUS_FAIL 1 +#define BRCMF_E_STATUS_TIMEOUT 2 +#define BRCMF_E_STATUS_NO_NETWORKS 3 +#define BRCMF_E_STATUS_PARTIAL 8 + +/* Flags for struct brcmf_dload_data_le (Linux brcmfmac/fwil_types.h + * enum brcmf_dload_data_flag). The chip expects DL_BEGIN on the first + * chunk and DL_END on the last; bit 12 (CRC-supplied) is set unconditionally + * since the header carries a (currently zero) CRC field. + */ +#define BRCMF_DL_BEGIN (1u << 1) +#define BRCMF_DL_END (1u << 2) +#define BRCMF_DL_CRC_IN_HDR (1u << 12) + +/* dload_type values (Linux brcmfmac/fwil_types.h enum brcmf_dload_type). */ +#define BRCMF_DL_TYPE_CLM 2 + +struct brcmf_dload_data_le { + uint16_t flag; + uint16_t dload_type; + uint32_t len; + uint32_t crc; + uint8_t data[]; +} __packed; + +/* === Escan IOVAR ========================================================= + * + * iovar name "escan", value = struct brcmf_escan_params_le. Action=1 starts + * an escan; results stream back as chan=1 events of type WLC_E_ESCAN_RESULT + * (each event carries one or more bss_info_le records via brcmf_escan_result_le). + */ +#define BRCMF_SCAN_PARAMS_VERSION 1 +#define WL_ESCAN_ACTION_START 1 +#define BRCMF_SSID_MAX_LEN 32 +#define BRCMF_MCSSET_LEN 16 + +struct brcmf_ssid_le { + uint32_t SSID_len; + uint8_t SSID[BRCMF_SSID_MAX_LEN]; +} __packed; + +struct brcmf_scan_params_le { + struct brcmf_ssid_le ssid_le; + uint8_t bssid[6]; + int8_t bss_type; + uint8_t scan_type; + uint32_t nprobes; + uint32_t active_time; + uint32_t passive_time; + uint32_t home_time; + uint32_t channel_num; + uint16_t channel_list[]; /* flexible: zero or more channels */ +} __packed; + +struct brcmf_escan_params_le { + uint32_t version; + uint16_t action; + uint16_t sync_id; + struct brcmf_scan_params_le params_le; +} __packed; + +/* Linux declares brcmf_bss_info_le WITHOUT __packed -- natural alignment + * inserts a pad byte before rateset.count, before RSSI, and before + * nbss_cap. Mirror that exactly or chanspec/RSSI read the wrong bytes. + */ +struct brcmf_bss_info_le { + uint32_t version; + uint32_t length; + uint8_t BSSID[6]; + uint16_t beacon_period; + uint16_t capability; + uint8_t SSID_len; + uint8_t SSID[32]; + struct { + uint32_t count; + uint8_t rates[16]; + } rateset; + uint16_t chanspec; + uint16_t atim_window; + uint8_t dtim_period; + uint16_t RSSI; + int8_t phy_noise; + uint8_t n_cap; + uint32_t nbss_cap; + uint8_t ctl_ch; + uint32_t reserved32; + uint8_t flags; + uint8_t reserved[3]; + uint8_t basic_mcs[BRCMF_MCSSET_LEN]; + uint16_t ie_offset; + uint32_t ie_length; + uint16_t SNR; +}; + +struct brcmf_escan_result_le { + uint32_t buflen; + uint32_t version; + uint16_t sync_id; + uint16_t bss_count; + struct brcmf_bss_info_le bss_info_le; +}; + +struct bcm_core { + uint16_t id; + uint32_t base; + uint32_t wrapbase; +}; + +struct brcmfmac_config { + const struct device *sdhc; + struct gpio_dt_spec reg_on; + const char *firmware_name; +}; + +struct brcmfmac_data; +struct net_pkt; + +/* Pending IOCTL waiter: filled by query_dcmd, completed by RX thread. */ +struct brcmfmac_pending_ioctl { + bool active; + uint16_t reqid; + uint8_t *out_buf; + uint16_t out_capacity; + uint16_t out_copied; + int status; + struct k_sem done; +}; + +/* TX glomming: queue net frames in a fixed-size ring, drain them in + * batches via one CMD53 per burst. Mirrors Linux brcmfmac's brcmf_sdio_txpkt + * pattern. Chip's `bus:txglom=7` confirms it accepts glommed SDPCM frames. + * + * Sizing: 16 slots x 1600 B = 25.6 KB ring; 12 KB single glom buffer. + * Up to 8 frames packed per CMD53 (gated by available SDPCM tx-credits + * at flush time -- partial batches go out promptly if credits are tight). + */ +#define BRCMFMAC_TX_RING_SLOTS 16 +#define BRCMFMAC_TX_SLOT_SIZE 1600 +/* TODO: enable multi-frame glomming. Linux's wire format (sdio.c + * ::brcmf_sdio_hdpack:1502-27) inserts an 8-byte SDPCM_HWEXT header + * between the 4-byte fh and the 8-byte sw header on every frame in + * glom mode, with (frame_len - 4) | (lastfrm << 24) in word 0 and + * (tail_pad << 16) in word 1. The chip also needs `bus:txglom=1` + * set via iovar at init time -- the default of 7 we read is the + * capability bitmask, not the enabled mode. Without both halves, the + * chip silently drops every multi-frame CMD53 and txseq stalls. + * Single-frame (MAX=1) is the working path; mid-it gave + * +16% throughput / 40x lower loss over the old direct-CMD53 path. + */ +#define BRCMFMAC_TX_GLOM_MAX_FRAMES 1 +#define BRCMFMAC_TX_GLOM_BUF_SIZE 2048 + +struct brcmfmac_tx_slot { + uint16_t len; /* 0 = empty */ + uint8_t data[BRCMFMAC_TX_SLOT_SIZE] __aligned(4); +}; + +struct brcmfmac_data { + struct sd_card card; + struct sdio_func backplane; /* F1 */ + struct sdio_func radio; /* F2 */ + + /* Chip topology discovered by EROM scan. */ + struct bcm_core cores[BRCMFMAC_MAX_CORES]; + unsigned int num_cores; + + /* Chip identity (chipcommon[0]). */ + uint32_t chipid_reg; + uint16_t chip_id; + uint8_t chip_rev; + uint8_t chip_type; + + uint32_t ram_base; + uint32_t ram_size; + + /* BCDC protocol state. */ + bool f2_ready; + uint8_t sdpcm_txseq; /* next outgoing SDPCM seq (8-bit wrap) */ + uint8_t sdpcm_tx_max; /* chip-reported tx-window upper bound */ + struct k_sem tx_credit_sem; /* signaled on every RX SDPCM hdr parse */ + struct k_sem rx_irq_sem; /* signaled by SDHC SDIO_INT callback */ + uint16_t bcdc_reqid; + struct k_mutex bcdc_mutex; + struct brcmfmac_pending_ioctl pending; + + /* Net + scan state. */ + struct net_if *iface; + scan_result_cb_t scan_cb; + + /* Link / assoc state. Updated from the RX thread when WLC_E_LINK / + * WLC_E_DISASSOC_IND arrive. + */ + enum brcmfmac_link_state link_state; + + /* Last connect()'s SSID + security, retained so iface_status can + * report something meaningful. Cleared on disconnect. + */ + uint8_t connected_ssid[32]; + uint8_t connected_ssid_len; + uint8_t connected_security; /* enum wifi_security_type */ + + /* MAC from chip OTP, read via cur_etheraddr IOCTL. */ + uint8_t chip_mac[6]; + + bool probed; + + /* TX glom ring: SPSC (iface_send produces, tx-thread consumes). + * head/tail are atomics so producer/consumer don't need a lock. + * Slot 0 .. head-1 are filled (mod N); tail .. head-1 are pending. + */ + struct brcmfmac_tx_slot tx_ring[BRCMFMAC_TX_RING_SLOTS]; + atomic_t tx_ring_head; + atomic_t tx_ring_tail; + struct k_sem tx_pending_sem; + + /* tx-thread-private glom assembly buffer. Holds the next CMD53's + * worth of chained SDPCM frames before issuing the F2 write. + */ + uint8_t tx_glom_buf[BRCMFMAC_TX_GLOM_BUF_SIZE] __aligned(4); +}; + +/* === SDIO backplane primitives (brcmfmac_sdio.c) === */ +int brcmfmac_sdio_set_backplane_window(struct brcmfmac_data *data, uint32_t addr); +int brcmfmac_sdio_backplane_read32(struct brcmfmac_data *data, uint32_t addr, + uint32_t *out); +int brcmfmac_sdio_backplane_write32(struct brcmfmac_data *data, uint32_t addr, + uint32_t val); +int brcmfmac_sdio_backplane_read_bytes(struct brcmfmac_data *data, uint32_t addr, + uint8_t *buf, uint32_t len); +int brcmfmac_sdio_backplane_write_bytes(struct brcmfmac_data *data, uint32_t addr, + const uint8_t *buf, uint32_t len); +int brcmfmac_sdio_ramrw(struct brcmfmac_data *data, bool write, + uint32_t chip_addr, uint8_t *buf, uint32_t size); +int brcmfmac_sdio_fw_upload(struct brcmfmac_data *data); +int brcmfmac_sdio_nvram_upload(struct brcmfmac_data *data); + +/* === Chip initialization (brcmfmac_chip.c) === */ +int brcmfmac_chip_read_id(struct brcmfmac_data *data); +int brcmfmac_chip_ram_data(struct brcmfmac_data *data); +int brcmfmac_chip_pmu_setup(struct brcmfmac_data *data); +int brcmfmac_chip_erom_scan(struct brcmfmac_data *data); +const struct bcm_core *brcmfmac_chip_core_find(const struct brcmfmac_data *data, + uint16_t id); +int brcmfmac_chip_set_passive(struct brcmfmac_data *data); +int brcmfmac_chip_set_active(struct brcmfmac_data *data); + +/* === BCDC protocol (brcmfmac_bcdc.c) === + * + * F2 enable + polled IOCTL round-trip. Single in-flight, + * caller blocks on the response. splits the polling out + * into a dedicated RX thread. + */ +int brcmfmac_bcdc_init(struct brcmfmac_data *data); + +/* Run a dcmd. tx_payload + tx_len go into the request; chip's response + * payload (up to rx_capacity bytes) is copied to rx_buf. Returns bytes + * copied or negative errno. + */ +int brcmfmac_bcdc_query_dcmd(struct brcmfmac_data *data, uint32_t cmd, + const uint8_t *tx_payload, uint16_t tx_len, + uint8_t *rx_buf, uint16_t rx_capacity); + +/* IOVAR helper: WLC_GET_VAR with `name` as the var key. */ +int brcmfmac_bcdc_iovar_get(struct brcmfmac_data *data, const char *name, + uint8_t *buf, uint16_t len); + +/* SET dcmd variant: send a command with no response payload (chip echoes + * status back). For WLC_SET_VAR + others. + */ +int brcmfmac_bcdc_set_dcmd(struct brcmfmac_data *data, uint32_t cmd, + const uint8_t *tx_payload, uint16_t tx_len); + +/* IOVAR helper: WLC_SET_VAR with `name` as the var key + value as data. */ +int brcmfmac_bcdc_iovar_set(struct brcmfmac_data *data, const char *name, + const uint8_t *value, uint16_t value_len); + +/* bsscfg-prefixed iovar SET. The chip recognizes some iovars (sup_wpa, + * sup_wpa_tmo, etc.) only when sent in the bsscfg-namespaced form even + * for bsscfgidx=0. Wire format: + * WLC_SET_VAR cmd, payload = "bsscfg:NAME\0" + LE32(bsscfgidx) + value + */ +int brcmfmac_bcdc_bsscfg_iovar_set_int(struct brcmfmac_data *data, + const char *name, uint32_t bsscfgidx, + int32_t value); + +/* Build SDPCM headers in-place at start of `frame`, then F2 TX the + * (4-byte-padded) frame. Caller must hold bcdc_mutex. Used by both + * the IOCTL path (chan=0) and the data path (chan=2). + */ +int brcmfmac_bcdc_tx_frame(struct brcmfmac_data *data, uint8_t chan, + uint8_t *frame, uint16_t total); + +/* === net_if + wifi_mgmt glue (brcmfmac_net.c) === */ +void brcmfmac_iface_init(struct net_if *iface); +int brcmfmac_iface_send(const struct device *dev, struct net_pkt *pkt); +int brcmfmac_mgmt_scan(const struct device *dev, struct net_if *iface, + struct wifi_scan_params *params, scan_result_cb_t cb); +int brcmfmac_mgmt_connect(const struct device *dev, struct net_if *iface, + struct wifi_connect_req_params *params); +int brcmfmac_mgmt_disconnect(const struct device *dev, struct net_if *iface); +int brcmfmac_mgmt_iface_status(const struct device *dev, struct net_if *iface, + struct wifi_iface_status *status); + +/* RX-thread dispatchers (called from bcdc.c's RX thread). */ +void brcmfmac_net_rx_data(struct brcmfmac_data *data, + const uint8_t *frame, uint16_t len); +void brcmfmac_net_rx_event(struct brcmfmac_data *data, + const uint8_t *frame, uint16_t len); + +/* === Embedded firmware blobs (firmware/) === */ +extern const unsigned char brcmfmac_fw[]; +extern const unsigned int brcmfmac_fw_len; +extern const unsigned char brcmfmac_nvram[]; +extern const unsigned int brcmfmac_nvram_len; +extern const unsigned char brcmfmac_clm_blob[]; +extern const unsigned int brcmfmac_clm_blob_len; + +#endif /* ZEPHYR_DRIVERS_WIFI_BRCMFMAC_PRIV_H_ */ diff --git a/drivers/wifi/brcmfmac/brcmfmac_sdio.c b/drivers/wifi/brcmfmac/brcmfmac_sdio.c new file mode 100644 index 0000000000000..8e654910371b1 --- /dev/null +++ b/drivers/wifi/brcmfmac/brcmfmac_sdio.c @@ -0,0 +1,369 @@ +/* + * Copyright (c) 2026 Jonathan Elliot Peace + * SPDX-License-Identifier: Apache-2.0 + * + * SDIO bus primitives + firmware/NVRAM upload helpers. Mirrors Linux's + * brcmf_sdiod_set_backplane_window / brcmf_sdiod_ramrw and + * brcmf_sdio_download_code_file / brcmf_sdio_download_nvram. + */ + +#include + +#include +#include +#include +#include + +#include "brcmfmac_priv.h" + +LOG_MODULE_DECLARE(brcmfmac, CONFIG_WIFI_LOG_LEVEL); + +int brcmfmac_sdio_set_backplane_window(struct brcmfmac_data *data, uint32_t addr) +{ + uint32_t bar0 = addr & SBSDIO_SBWINDOW_MASK; + uint32_t v = bar0 >> 8; + int ret; + + for (int i = 0; i < 3; i++, v >>= 8) { + ret = sdio_write_byte(&data->backplane, + SBSDIO_FUNC1_SBADDRLOW + i, + (uint8_t)(v & 0xFF)); + if (ret != 0) { + LOG_ERR("SBADDR[%d] write failed: %d", i, ret); + return ret; + } + } + return 0; +} + +int brcmfmac_sdio_backplane_read32(struct brcmfmac_data *data, uint32_t addr, + uint32_t *out) +{ + uint8_t buf[4]; + int ret; + + ret = brcmfmac_sdio_set_backplane_window(data, addr); + if (ret != 0) { + return ret; + } + + uint32_t off = (addr & SBSDIO_SB_OFT_ADDR_MASK) | SBSDIO_SB_ACCESS_2_4B_FLAG; + + ret = sdio_read_addr(&data->backplane, off, buf, sizeof(buf)); + if (ret != 0) { + LOG_ERR("backplane_read32(0x%08x): %d", addr, ret); + return ret; + } + + *out = (uint32_t)buf[0] | + ((uint32_t)buf[1] << 8) | + ((uint32_t)buf[2] << 16) | + ((uint32_t)buf[3] << 24); + return 0; +} + +int brcmfmac_sdio_backplane_write32(struct brcmfmac_data *data, uint32_t addr, + uint32_t val) +{ + uint8_t buf[4]; + int ret; + + ret = brcmfmac_sdio_set_backplane_window(data, addr); + if (ret != 0) { + return ret; + } + + uint32_t off = (addr & SBSDIO_SB_OFT_ADDR_MASK) | SBSDIO_SB_ACCESS_2_4B_FLAG; + + buf[0] = (uint8_t)(val & 0xFF); + buf[1] = (uint8_t)((val >> 8) & 0xFF); + buf[2] = (uint8_t)((val >> 16) & 0xFF); + buf[3] = (uint8_t)((val >> 24) & 0xFF); + + ret = sdio_write_addr(&data->backplane, off, buf, sizeof(buf)); + if (ret != 0) { + LOG_ERR("backplane_write32(0x%08x): %d", addr, ret); + return ret; + } + return 0; +} + +int brcmfmac_sdio_backplane_read_bytes(struct brcmfmac_data *data, uint32_t addr, + uint8_t *buf, uint32_t len) +{ + int ret = brcmfmac_sdio_set_backplane_window(data, addr); + + if (ret != 0) { + return ret; + } + uint32_t off = (addr & SBSDIO_SB_OFT_ADDR_MASK) | SBSDIO_SB_ACCESS_2_4B_FLAG; + + ret = sdio_read_addr(&data->backplane, off, buf, len); + if (ret != 0) { + LOG_ERR("backplane_read_bytes(0x%08x, %u): %d", addr, len, ret); + } + return ret; +} + +int brcmfmac_sdio_backplane_write_bytes(struct brcmfmac_data *data, uint32_t addr, + const uint8_t *buf, uint32_t len) +{ + int ret = brcmfmac_sdio_set_backplane_window(data, addr); + + if (ret != 0) { + return ret; + } + uint32_t off = (addr & SBSDIO_SB_OFT_ADDR_MASK) | SBSDIO_SB_ACCESS_2_4B_FLAG; + /* Zephyr's sdio_write_addr takes a non-const buffer but doesn't mutate. */ + ret = sdio_write_addr(&data->backplane, off, (uint8_t *)buf, len); + if (ret != 0) { + LOG_ERR("backplane_write_bytes(0x%08x, %u): %d", addr, len, ret); + } + return ret; +} + +/* Bulk RAM read/write across the F1 backplane window. Each chunk: + * 1. Slide SBADDR to cover the current chip-side address. + * 2. Transfer up to one F1 window (32 KiB), capped at 511 blocks per + * CMD53 -- 512 blocks wedges this chip's SDIO state machine. + * 3. Trail with a byte-mode 4-byte transfer for 1..3-byte remainders + * (F1->backplane 4B_ACCESS mode requires 4-aligned counts). + */ +int brcmfmac_sdio_ramrw(struct brcmfmac_data *data, bool write, + uint32_t chip_addr, uint8_t *buf, uint32_t size) +{ + while (size > 0) { + uint32_t sdaddr = chip_addr & SBSDIO_SB_OFT_ADDR_MASK; + uint32_t window_left = SBSDIO_SB_OFT_ADDR_LIMIT - sdaddr; + uint32_t chunk = MIN(size, window_left); + + if (chunk > BRCMFMAC_MAX_CMD53_BLOCK_BYTES) { + chunk = BRCMFMAC_MAX_CMD53_BLOCK_BYTES; + } + uint32_t aligned_chunk = chunk & ~3u; + uint32_t tail = chunk - aligned_chunk; + + int ret = brcmfmac_sdio_set_backplane_window(data, chip_addr); + + if (ret != 0) { + LOG_ERR("ramrw: SBADDR set @ chip 0x%08x failed: %d", + chip_addr, ret); + return ret; + } + + uint32_t off = sdaddr | SBSDIO_SB_ACCESS_2_4B_FLAG; + + if (aligned_chunk > 0) { + ret = write + ? sdio_write_addr(&data->backplane, off, buf, aligned_chunk) + : sdio_read_addr(&data->backplane, off, buf, aligned_chunk); + if (ret != 0) { + LOG_ERR("ramrw: %s @ chip 0x%08x len %u failed: %d", + write ? "write" : "read", + chip_addr, aligned_chunk, ret); + return ret; + } + } + + if (tail > 0) { + uint8_t tail_buf[4] __aligned(4) = {0}; + uint32_t tail_off = off + aligned_chunk; + + if (write) { + memcpy(tail_buf, buf + aligned_chunk, tail); + ret = sdio_write_addr(&data->backplane, tail_off, + tail_buf, sizeof(tail_buf)); + } else { + ret = sdio_read_addr(&data->backplane, tail_off, + tail_buf, sizeof(tail_buf)); + if (ret == 0) { + memcpy(buf + aligned_chunk, tail_buf, tail); + } + } + if (ret != 0) { + LOG_ERR("ramrw: %s tail @ chip 0x%08x len %u failed: %d", + write ? "write" : "read", + chip_addr + aligned_chunk, tail, ret); + return ret; + } + } + + buf += chunk; + chip_addr += chunk; + size -= chunk; + } + return 0; +} + +/* Read back chip RAM, compare against `expected`. Chunked through a + * static scratch buffer to avoid stack pressure on the firmware verify. + */ +static int brcmfmac_sdio_verify_memory(struct brcmfmac_data *data, + uint32_t chip_addr, + const uint8_t *expected, uint32_t size) +{ + static uint8_t readback[1024] __aligned(CONFIG_DCACHE_LINE_SIZE); + uint32_t verified = 0; + + while (size > 0) { + uint32_t chunk = MIN(size, (uint32_t)sizeof(readback)); + int ret = brcmfmac_sdio_ramrw(data, false, chip_addr, readback, chunk); + + if (ret != 0) { + return ret; + } + for (uint32_t i = 0; i < chunk; i++) { + if (readback[i] != expected[i]) { + LOG_ERR("verify: +%u chip=0x%08x exp=0x%02x got=0x%02x", + verified + i, chip_addr + i, + expected[i], readback[i]); + return -EIO; + } + } + chip_addr += chunk; + expected += chunk; + size -= chunk; + verified += chunk; + } + return 0; +} + +/* Convert key=value text NVRAM into the chip-format the firmware expects: + * - Skip '#' comments, empty lines, leading/trailing whitespace. + * - Concat valid lines NUL-separated. + * - Pad to 4-byte alignment with NULs. + * - Append 4-byte LE token: (~n << 16) | (n & 0xFFFF), n = padded_len/4. + * Returns bytes written, or negative errno. + */ +static int brcmfmac_sdio_nvram_strip(const uint8_t *in, uint32_t in_len, + uint8_t *out, uint32_t out_capacity) +{ + uint32_t op = 0; + uint32_t ip = 0; + const uint32_t need_tail = 4 + 3; + + while (ip < in_len) { + while (ip < in_len && + (in[ip] == ' ' || in[ip] == '\t' || + in[ip] == '\r' || in[ip] == '\n')) { + ip++; + } + if (ip >= in_len) { + break; + } + if (in[ip] == '#') { + while (ip < in_len && in[ip] != '\n') { + ip++; + } + continue; + } + + uint32_t line_start = op; + + while (ip < in_len && in[ip] != '\n' && in[ip] != '\r') { + if (op + need_tail >= out_capacity) { + LOG_ERR("nvram_strip: out overflow"); + return -EOVERFLOW; + } + out[op++] = in[ip++]; + } + while (op > line_start && + (out[op - 1] == ' ' || out[op - 1] == '\t')) { + op--; + } + if (op > line_start) { + out[op++] = '\0'; + } + } + + uint32_t padded = (op + 3) & ~3u; + + if (padded + 4 > out_capacity) { + return -EOVERFLOW; + } + while (op < padded) { + out[op++] = '\0'; + } + + uint32_t n = padded / 4; + uint32_t token = (~n << 16) | (n & 0xFFFF); + + out[op++] = (uint8_t)(token & 0xFF); + out[op++] = (uint8_t)((token >> 8) & 0xFF); + out[op++] = (uint8_t)((token >> 16) & 0xFF); + out[op++] = (uint8_t)((token >> 24) & 0xFF); + + return (int)op; +} + +int brcmfmac_sdio_fw_upload(struct brcmfmac_data *data) +{ + int64_t t0 = k_uptime_get(); + int ret = brcmfmac_sdio_ramrw(data, true, data->ram_base, + (uint8_t *)brcmfmac_fw, brcmfmac_fw_len); + if (ret != 0) { + LOG_ERR("fw upload failed: %d", ret); + return ret; + } + int64_t t1 = k_uptime_get(); + + LOG_INF("fw upload OK in %lld ms (%u bytes)", + (long long)(t1 - t0), brcmfmac_fw_len); + + ret = brcmfmac_sdio_verify_memory(data, data->ram_base, brcmfmac_fw, brcmfmac_fw_len); + if (ret != 0) { + LOG_ERR("fw verify failed: %d", ret); + return ret; + } + LOG_DBG("fw verify OK in %lld ms", (long long)(k_uptime_get() - t1)); + + if (data->ram_base != 0 && brcmfmac_fw_len >= 4) { + LOG_DBG("fw_upload: set reset vector"); + ret = brcmfmac_sdio_ramrw(data, true, 0, (uint8_t *)brcmfmac_fw, 4); + if (ret != 0) { + LOG_ERR("fw_upload: set reset vector failed: %d", ret); + return ret; + } + } + + return 0; +} + +int brcmfmac_sdio_nvram_upload(struct brcmfmac_data *data) +{ + /* Sized to fit the largest NVRAM currently shipped in linux-firmware + * (3099 bytes after the footer); rounds up to 4 KiB. + */ + static uint8_t nvram_buf[4096] __aligned(CONFIG_DCACHE_LINE_SIZE); + int stripped = brcmfmac_sdio_nvram_strip(brcmfmac_nvram, brcmfmac_nvram_len, + nvram_buf, sizeof(nvram_buf)); + if (stripped < 0) { + LOG_ERR("nvram_strip failed: %d", stripped); + return stripped; + } + LOG_DBG("nvram strip: %u -> %d bytes (incl. token footer)", + brcmfmac_nvram_len, stripped); + + const uint32_t nvram_addr = data->ram_base + data->ram_size - (uint32_t)stripped; + + int64_t t0 = k_uptime_get(); + int ret = brcmfmac_sdio_ramrw(data, true, nvram_addr, + nvram_buf, (uint32_t)stripped); + if (ret != 0) { + LOG_ERR("nvram upload failed: %d", ret); + return ret; + } + int64_t t1 = k_uptime_get(); + + LOG_DBG("nvram upload OK @ chip 0x%08x in %lld ms", + nvram_addr, (long long)(t1 - t0)); + + ret = brcmfmac_sdio_verify_memory(data, nvram_addr, nvram_buf, + (uint32_t)stripped); + if (ret != 0) { + LOG_ERR("nvram verify failed: %d", ret); + return ret; + } + LOG_DBG("nvram verify OK in %lld ms", (long long)(k_uptime_get() - t1)); + return 0; +} diff --git a/dts/bindings/wifi/brcm,bcm43xxx-sdio.yaml b/dts/bindings/wifi/brcm,bcm43xxx-sdio.yaml new file mode 100644 index 0000000000000..897a473f16d21 --- /dev/null +++ b/dts/bindings/wifi/brcm,bcm43xxx-sdio.yaml @@ -0,0 +1,36 @@ +# Copyright (c) 2026 Jonathan Elliot Peace +# SPDX-License-Identifier: Apache-2.0 + +description: | + Broadcom BCM43xxx SDIO Wi-Fi (brcmfmac protocol). + + Covers SDIO-attached Broadcom/Cypress combo chips speaking the + brcmfmac SDPCM+BCDC protocol (BCM43430A1 / CYW43439 / BCM4334x). + Chip identity is read at runtime from chipcommon[0] on the AXI/SB + backplane. + + The node is a child of an SDHC controller. The driver reaches the + SDHC parent via DT_INST_PARENT(0). Firmware and per-board NVRAM + files are selected by Kconfig (CONFIG_WIFI_BRCMFMAC_FIRMWARE_FILE + / CONFIG_WIFI_BRCMFMAC_NVRAM_FILE) and resolved against the + hal_broadcom blob directory at build time. + +compatible: "brcm,bcm43xxx-sdio" + +include: base.yaml + +properties: + wifi-reg-on-gpios: + description: | + Power / regulator-enable GPIO ("WL_REG_ON" on Cypress modules). + Optional: on boards where the SoC firmware asserts this before + Zephyr starts, the driver leaves it alone. + type: phandle-array + + wifi-host-wake-gpios: + description: | + Optional host-wake GPIO. When the chip wants attention it + drives this line; the driver wakes its RX thread from the IRQ. + Not used on platforms where the SDHC driver delivers in-band + SDIO interrupts on DAT1. + type: phandle-array