408 lines
12 KiB
C
408 lines
12 KiB
C
/*
|
|
* 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 <string.h>
|
|
#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);
|
|
}
|