/* * RKY audio receiver: WiFi -> direct I2S DMA buffer write. * * Dual-path RX: ISR hook (fast, ~5us) + promisc callback (fallback). * P2M-M2P bridge: audio payload memcpy'd directly into I2S TX DMA slots. * * I2S: 48kHz 16-bit data in 32-bit slots, Philips framing. * BCLK=16 WS=15 DOUT=17, no MCLK. * Hardware left-justifies 16-bit DMA words into 32-bit I2S slots. */ #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_wifi.h" #include "esp_timer.h" #include "esp_log.h" #include "nvs_flash.h" #include "esp_netif.h" #include "esp_event.h" #include "driver/i2s_std.h" #include "soc/lldesc.h" #include "esp_private/wifi_os_adapter.h" #include "xtensa/xtensa_api.h" #include "esp_cpu.h" #define TAG "rky-rx" #define CHANNEL 1 #define SAMPLE_RATE 48000 #define AUDIO_PAYLOAD_SIZE 192 #define BRIDGE_DESC_NUM 128 #define BRIDGE_DESC_MASK (BRIDGE_DESC_NUM - 1) /* WiFi MAC RX DMA registers (ESP32-S3) */ #define REG_RX_DSCR_LAST (*(volatile uint32_t *)0x60033090) /* DMA SRAM address range on S3 */ #define DMA_SRAM_BASE 0x3FC88000 #define DMA_SRAM_END 0x3FD00000 #define DMA_ADDR_MASK 0x000FFFFF #define DMA_ADDR_OR 0x3FC00000 typedef struct __attribute__((packed)) { uint32_t frame_seq; uint8_t sub_index; uint8_t pkt_type; uint16_t payload_len; uint16_t burst_width; } rky_hdr_t; #define MAC_HDR_LEN 24 #define ACTION_HDR_LEN 4 #define RKY_HDR_LEN sizeof(rky_hdr_t) #define FRAME_OVERHEAD (MAC_HDR_LEN + ACTION_HDR_LEN + RKY_HDR_LEN) static uint8_t **s_bufs; static uint32_t s_buf_size; static volatile uint32_t s_wr_slot; static volatile uint32_t s_dma_slot; static DRAM_ATTR uint8_t s_paired_mac[6]; static volatile bool s_paired; static uint32_t s_stat_isr; static uint32_t s_stat_prom; static uint32_t s_stat_dedup; static uint32_t s_stat_gaps; static uint32_t s_stat_overruns; static uint32_t s_stat_sent; static uint32_t s_stat_isrs; static volatile uint32_t s_last_seq; static i2s_chan_handle_t s_tx_handle; /* ---------- shared ingest ---------- */ static inline void IRAM_ATTR ingest_audio(const uint8_t *audio, uint16_t plen, uint32_t seq, const uint8_t *sa) { /* first-seen pairing */ if (!s_paired) { for (int i = 0; i < 6; i++) s_paired_mac[i] = sa[i]; s_paired = true; } else { if (sa[0] != s_paired_mac[0] || sa[1] != s_paired_mac[1] || sa[2] != s_paired_mac[2] || sa[3] != s_paired_mac[3] || sa[4] != s_paired_mac[4] || sa[5] != s_paired_mac[5]) return; } /* sequence dedup: both paths call ingest, skip if already seen */ if (seq == s_last_seq && s_last_seq != 0) { s_stat_dedup++; return; } /* gap detection */ if (s_last_seq != 0 && seq != s_last_seq + 1) s_stat_gaps++; s_last_seq = seq; /* ring slot */ uint32_t next = (s_wr_slot + 1) & BRIDGE_DESC_MASK; if (next == s_dma_slot) { s_stat_overruns++; return; } uint32_t slot = s_wr_slot; uint32_t copy_len = (plen <= s_buf_size) ? plen : s_buf_size; memcpy((void *)s_bufs[slot], audio, copy_len); if (copy_len < s_buf_size) memset((void *)(s_bufs[slot] + copy_len), 0, s_buf_size - copy_len); __asm__ __volatile__("memw" ::: "memory"); s_wr_slot = next; } /* ---------- ISR hook ---------- */ static DRAM_ATTR xt_handler s_orig_isr; static DRAM_ATTR void *s_orig_arg; static DRAM_ATTR int s_orig_intr_num = -1; static DRAM_ATTR uint32_t s_isr_last_raw; static void IRAM_ATTR wifi_isr_hook(void *arg) { s_stat_isrs++; uint32_t raw = REG_RX_DSCR_LAST; if (raw != s_isr_last_raw) { s_isr_last_raw = raw; uint32_t addr = (raw & DMA_ADDR_MASK) | DMA_ADDR_OR; if (addr >= DMA_SRAM_BASE && addr < DMA_SRAM_END) { lldesc_t *desc = (lldesc_t *)addr; if (desc->owner == 0 && desc->length > FRAME_OVERHEAD) { const uint8_t *buf = (const uint8_t *)desc->buf; if (buf[0] == 0xD0 && buf[24] == 127 && buf[25] == 0x52 && buf[26] == 0x4B && buf[27] == 0x59) { rky_hdr_t *hdr = (rky_hdr_t *)(buf + MAC_HDR_LEN + ACTION_HDR_LEN); uint16_t plen = hdr->payload_len; uint32_t seq = hdr->frame_seq; const uint8_t *audio = buf + FRAME_OVERHEAD; if (plen > desc->length - FRAME_OVERHEAD) plen = desc->length - FRAME_OVERHEAD; if (plen > 0) { ingest_audio(audio, plen, seq, buf + 10); s_stat_isr++; } } } } } /* chain to blob's original ISR */ if (s_orig_isr) s_orig_isr(s_orig_arg); } /* patched _set_isr: intercept when blob installs wDev_ProcessFiq */ static void IRAM_ATTR patched_set_isr(int32_t n, void *f, void *arg) { if (s_orig_intr_num < 0 && f != NULL) { s_orig_isr = (xt_handler)f; s_orig_arg = arg; s_orig_intr_num = n; xt_set_interrupt_handler(n, wifi_isr_hook, arg); ESP_EARLY_LOGI(TAG, "captured WiFi ISR: intr=%d handler=%p arg=%p", (int)n, f, arg); } else { xt_set_interrupt_handler(n, (xt_handler)f, arg); } } /* ---------- I2S on_sent callback -- GDMA TX EOF ISR ---------- */ static bool IRAM_ATTR on_i2s_sent(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_data) { s_dma_slot = (s_dma_slot + 1) & BRIDGE_DESC_MASK; s_stat_sent++; return false; } /* ---------- promiscuous callback (fallback path) ---------- */ static void IRAM_ATTR promisc_cb(void *buf, wifi_promiscuous_pkt_type_t type) { wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf; uint8_t *frame = pkt->payload; int len = pkt->rx_ctrl.sig_len - 4; if (len < (int)FRAME_OVERHEAD) return; if ((frame[0] & 0xFC) != 0xD0) return; if (frame[24] != 127) return; if (frame[25] != 0x52 || frame[26] != 0x4B || frame[27] != 0x59) return; rky_hdr_t *hdr = (rky_hdr_t *)(frame + MAC_HDR_LEN + ACTION_HDR_LEN); uint16_t plen = hdr->payload_len; uint32_t seq = hdr->frame_seq; uint8_t *audio = frame + FRAME_OVERHEAD; int avail = len - (int)FRAME_OVERHEAD; if (plen > (uint16_t)avail) plen = (uint16_t)avail; if (plen == 0) return; ingest_audio(audio, plen, seq, frame + 10); s_stat_prom++; } /* ---------- DMA pointer extraction ---------- */ /* * i2s_channel_obj_t layout (ESP-IDF v5.4, i2s_private.h): * +16 i2s_dma_t dma: * +4 desc_num * +12 buf_size * +32 bufs */ static void extract_dma_pointers(void) { uint8_t *ch = (uint8_t *)s_tx_handle; uint8_t *dma = ch + 16; uint32_t desc_num = *(uint32_t *)(dma + 4); uint32_t buf_size = *(uint32_t *)(dma + 12); uint8_t **bufs = *(uint8_t ***)(dma + 32); ESP_LOGI(TAG, "DMA: desc_num=%lu buf_size=%lu bufs=%p", (unsigned long)desc_num, (unsigned long)buf_size, (void *)bufs); if (desc_num != BRIDGE_DESC_NUM || buf_size == 0 || bufs == NULL) { ESP_LOGE(TAG, "DMA extraction failed: expected desc_num=%d got %lu", BRIDGE_DESC_NUM, (unsigned long)desc_num); abort(); } if (!esp_ptr_internal(bufs[0])) { ESP_LOGE(TAG, "DMA buffer not in internal SRAM: %p", (void *)bufs[0]); abort(); } s_bufs = bufs; s_buf_size = buf_size; s_wr_slot = 0; s_dma_slot = 0; ESP_LOGI(TAG, "bridge: %lu * %lu = %lu bytes (%lums)", (unsigned long)desc_num, (unsigned long)buf_size, (unsigned long)(desc_num * buf_size), (unsigned long)(desc_num * buf_size * 1000UL / (SAMPLE_RATE * 4))); } /* ---------- I2S init ---------- */ static void i2s_bridge_init(void) { i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER); chan_cfg.dma_desc_num = BRIDGE_DESC_NUM; chan_cfg.dma_frame_num = AUDIO_PAYLOAD_SIZE / 4; chan_cfg.auto_clear_after_cb = true; chan_cfg.auto_clear_before_cb = false; ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &s_tx_handle, NULL)); i2s_std_config_t std_cfg = { .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SAMPLE_RATE), .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG( I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO), .gpio_cfg = { .mclk = I2S_GPIO_UNUSED, .bclk = GPIO_NUM_16, .ws = GPIO_NUM_15, .dout = GPIO_NUM_17, .din = I2S_GPIO_UNUSED, .invert_flags = { .mclk_inv = false, .bclk_inv = false, .ws_inv = false, }, }, }; /* * Daisy Seed (PCM3060) expects 32-bit I2S slots. * With 16-bit data width and 32-bit slot width, the ESP32-S3 I2S hardware * reads 16-bit words from DMA and left-justifies them in each 32-bit slot. * DMA buffers stay 16-bit-sized; the hardware handles padding on the wire. * * BCLK = 48000 * 2 * 32 = 3.072 MHz */ std_cfg.slot_cfg.slot_bit_width = I2S_SLOT_BIT_WIDTH_32BIT; std_cfg.slot_cfg.ws_width = 32; ESP_ERROR_CHECK(i2s_channel_init_std_mode(s_tx_handle, &std_cfg)); i2s_event_callbacks_t cbs = { .on_sent = on_i2s_sent, .on_recv = NULL, .on_recv_q_ovf = NULL, .on_send_q_ovf = NULL, }; ESP_ERROR_CHECK(i2s_channel_register_event_callback(s_tx_handle, &cbs, NULL)); extract_dma_pointers(); ESP_ERROR_CHECK(i2s_channel_enable(s_tx_handle)); } /* ---------- WiFi init ---------- */ static void wifi_init(void) { ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); /* patch _set_isr before esp_wifi_init so we capture the blob's ISR install */ g_wifi_osi_funcs._set_isr = patched_set_isr; wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE)); wifi_promiscuous_filter_t filt = { .filter_mask = WIFI_PROMIS_FILTER_MASK_ALL, }; ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filt)); ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(promisc_cb)); ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true)); } /* ---------- stats ---------- */ static void stats_task(void *arg) { int64_t last = esp_timer_get_time(); while (1) { vTaskDelay(pdMS_TO_TICKS(5000)); int64_t now = esp_timer_get_time(); float elapsed = (now - last) / 1e6f; uint32_t wr = s_wr_slot; uint32_t rd = s_dma_slot; uint32_t fill = (wr >= rd) ? (wr - rd) : (BRIDGE_DESC_NUM - rd + wr); uint32_t total = s_stat_isr + s_stat_prom; ESP_LOGI(TAG, "isr=%lu prom=%lu dedup=%lu | gaps=%lu over=%lu sent=%lu fill=%lu/%d | isrs=%lu pps=%.0f", (unsigned long)s_stat_isr, (unsigned long)s_stat_prom, (unsigned long)s_stat_dedup, (unsigned long)s_stat_gaps, (unsigned long)s_stat_overruns, (unsigned long)s_stat_sent, (unsigned long)fill, BRIDGE_DESC_NUM, (unsigned long)s_stat_isrs, total / elapsed); s_stat_isr = 0; s_stat_prom = 0; s_stat_dedup = 0; s_stat_gaps = 0; s_stat_overruns = 0; s_stat_sent = 0; s_stat_isrs = 0; last = now; } } /* ---------- main ---------- */ void app_main(void) { i2s_bridge_init(); wifi_init(); ESP_LOGI(TAG, "bridge rx: %dkHz 16-bit/32-slot stereo ch=%d, %d*%lu=%lu bytes, isr_hook=%s", SAMPLE_RATE / 1000, CHANNEL, BRIDGE_DESC_NUM, (unsigned long)s_buf_size, (unsigned long)(BRIDGE_DESC_NUM * s_buf_size), s_orig_intr_num >= 0 ? "active" : "inactive"); xTaskCreatePinnedToCore(stats_task, "stats", 4096, NULL, 5, NULL, 0); }