Init. Blob exploits for slot4 on the s3 inspired by esp-open-mac.

This commit is contained in:
jess 2026-03-28 03:03:52 -07:00
commit d9752ca39a
26 changed files with 1846 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
build/
sdkconfig
sdkconfig.old
managed_components/
dependencies.lock

View File

@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(bubbles)

View File

@ -0,0 +1,5 @@
idf_component_register(
SRCS "main.c" "hijack_tx.c"
INCLUDE_DIRS "."
REQUIRES esp_wifi esp_timer nvs_flash esp_netif esp_event esp_rom heap driver
)

View File

@ -0,0 +1,152 @@
/*
* hijack_tx -- DMA hijack TX for ESP32-S3.
*
* Fires a blob TX on slot 0 with a dummy frame sized to match the real
* frame, then swaps the DMA descriptor's buffer pointer before hardware
* reads it. The blob's PLCP setup uses the correct length because the
* dummy is the same size. No PLCP patching race.
*
* Sequence:
* 1. Build dummy frame matching target length
* 2. Fire blob TX (esp_wifi_80211_tx) on slot 0
* 3. Spin-wait for slot 0 TXQ_CTRL bits [31:30] set
* 4. Extract DMA descriptor from TXQ_CTRL[19:0]
* 5. Swap desc->buf to our frame
* 6. Wait for completion (bits [31:30] clear)
* 7. Restore descriptor to prevent blob crash
*/
#include "hijack_tx.h"
#include <string.h>
#include <stdio.h>
#include "esp_heap_caps.h"
#include "esp_wifi.h"
#include "esp_mac.h"
#include "esp_timer.h"
#include "esp_rom_lldesc.h"
#include "xtensa/core-macros.h"
#define REG32(addr) (*(volatile uint32_t *)(addr))
#define S0_TXQ_CTRL 0x60033D08
#define S0_PMD 0x60034320
#define MAX_FRAME_SIZE 512
#define DMA_BASE 0x3FC00000
static uint8_t *s_dummy = NULL;
static uint8_t *s_frame_buf = NULL;
static uint8_t s_own_mac[6];
static uint32_t s_ok_count = 0;
static uint32_t s_miss_count = 0;
static uint32_t s_fail_count = 0;
void hijack_init(void)
{
if (s_frame_buf) return;
s_frame_buf = heap_caps_calloc(1, MAX_FRAME_SIZE,
MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
s_dummy = heap_caps_calloc(1, MAX_FRAME_SIZE,
MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
assert(s_frame_buf);
assert(s_dummy);
esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA);
}
static void build_dummy(uint16_t len)
{
memset(s_dummy, 0, len);
s_dummy[0] = 0xD0;
memcpy(s_dummy + 4, "\xFF\xFF\xFF\xFF\xFF\xFF", 6);
memcpy(s_dummy + 10, s_own_mac, 6);
memcpy(s_dummy + 16, s_own_mac, 6);
s_dummy[24] = 127;
s_dummy[25] = 0x52;
s_dummy[26] = 0x4B;
s_dummy[27] = 0x59;
}
int IRAM_ATTR hijack_send(const uint8_t *frame, uint16_t len)
{
if (!s_frame_buf) return -1;
if (len == 0 || len > MAX_FRAME_SIZE) return -1;
memcpy(s_frame_buf, frame, len);
build_dummy(len);
esp_wifi_80211_tx(WIFI_IF_STA, s_dummy, len, false);
uint32_t spin_start = xthal_get_ccount();
uint32_t spin_limit = 5000 * 240;
lldesc_t *desc = NULL;
volatile const uint8_t *orig_buf = NULL;
uint16_t orig_size = 0;
uint16_t orig_len = 0;
while ((xthal_get_ccount() - spin_start) < spin_limit) {
uint32_t ctrl = REG32(S0_TXQ_CTRL);
if (ctrl & 0xC0000000) {
desc = (lldesc_t *)(DMA_BASE | (ctrl & 0x000FFFFF));
orig_buf = desc->buf;
orig_size = desc->size;
orig_len = desc->length;
/*
* Overwrite RKY header + audio payload in the blob's own buffer.
* Bytes 0-27 (MAC header, OUI) are already correct from the dummy.
* Write frame_seq (offset 28) first as a 32-bit store DMA reads
* linearly from offset 0, so we win the race on byte 28+ if we
* skip the header the dummy already has right.
*/
volatile uint8_t *dst = (volatile uint8_t *)orig_buf;
memcpy((void *)(dst + 28), s_frame_buf + 28, len - 28);
__asm__ __volatile__("memw" ::: "memory");
break;
}
}
if (!desc) {
s_miss_count++;
return -1;
}
int result = -1;
int64_t poll_end = esp_timer_get_time() + 200000;
while (esp_timer_get_time() < poll_end) {
if ((REG32(S0_TXQ_CTRL) & 0xC0000000) == 0) {
uint32_t pmd = REG32(S0_PMD);
uint8_t pmd_code = (pmd >> 12) & 0xF;
result = (pmd_code == 0 || pmd_code == 4) ? 0 : (int)pmd_code;
break;
}
}
desc->buf = orig_buf;
desc->size = orig_size;
desc->length = orig_len;
__asm__ __volatile__("memw" ::: "memory");
if (result == 0)
s_ok_count++;
else
s_fail_count++;
if (((s_ok_count + s_fail_count) % 100) == 0) {
uint32_t swap_us = (xthal_get_ccount() - spin_start) / 240;
printf("hijack: ok=%lu fail=%lu miss=%lu swap=%luus pmd=%d\n",
(unsigned long)s_ok_count,
(unsigned long)s_fail_count,
(unsigned long)s_miss_count,
(unsigned long)swap_us,
result);
}
return result;
}

View File

@ -0,0 +1,16 @@
#ifndef HIJACK_TX_H
#define HIJACK_TX_H
#include <stdint.h>
/* allocate DMA-capable frame buffer */
void hijack_init(void);
/*
* Transmit frame via slot 0 DMA hijack.
* Fires a blob TX, intercepts the DMA descriptor, swaps in our buffer.
* Returns 0 on success, -1 on miss/timeout.
*/
int hijack_send(const uint8_t *frame, uint16_t len);
#endif

233
ADC/bubbles-tx/main/main.c Normal file
View File

@ -0,0 +1,233 @@
/*
* Bubbles -- I2S RX to WiFi TX via 802.11 action frames.
*
* Captures stereo audio from an external I2S source (32-bit slots, Philips),
* packs to 24-bit, transmits as RKY protocol action frames at 1000 pps.
* ESP32-S3 is I2S master (drives BCLK, WS). Full-duplex I2S ensures
* clock output on pins even in RX-dominant mode.
*/
#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 "esp_mac.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "driver/i2s_std.h"
#define TAG "bubbles"
static const uint8_t BCAST[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t OUI_RKY[] = {0x52, 0x4B, 0x59};
#define CHANNEL 1
#define SAMPLE_RATE 48000
#define SAMPLES_PER_PKT 48
#define NUM_CHANNELS 2
#define BYTES_PER_SAMPLE 6
#define AUDIO_PAYLOAD (SAMPLES_PER_PKT * BYTES_PER_SAMPLE)
#define TX_PERIOD_US 1000
#define STATS_INTERVAL_MS 5000
#define PIN_BCLK GPIO_NUM_16
#define PIN_WS GPIO_NUM_15
#define PIN_DIN GPIO_NUM_18
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)
#define MAX_FRAME_LEN (FRAME_OVERHEAD + AUDIO_PAYLOAD)
#define I2S_READ_BYTES (SAMPLES_PER_PKT * NUM_CHANNELS * sizeof(int32_t))
static i2s_chan_handle_t s_tx_handle;
static i2s_chan_handle_t s_rx_handle;
static uint8_t s_silence[SAMPLES_PER_PKT * NUM_CHANNELS * sizeof(int32_t)];
static void i2s_tx_task(void *arg)
{
size_t written;
memset(s_silence, 0, sizeof(s_silence));
while (1) {
i2s_channel_write(s_tx_handle, s_silence, sizeof(s_silence),
&written, portMAX_DELAY);
}
}
static void i2s_init(void)
{
i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(
I2S_NUM_0, I2S_ROLE_MASTER);
chan_cfg.dma_desc_num = 6;
chan_cfg.dma_frame_num = SAMPLES_PER_PKT;
ESP_ERROR_CHECK(i2s_new_channel(&chan_cfg, &s_tx_handle, &s_rx_handle));
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_32BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = PIN_BCLK,
.ws = PIN_WS,
.dout = GPIO_NUM_17,
.din = PIN_DIN,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
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));
ESP_ERROR_CHECK(i2s_channel_init_std_mode(s_rx_handle, &std_cfg));
ESP_ERROR_CHECK(i2s_channel_enable(s_tx_handle));
ESP_ERROR_CHECK(i2s_channel_enable(s_rx_handle));
xTaskCreatePinnedToCore(i2s_tx_task, "i2s_tx", 2048, NULL, 10, NULL, 1);
}
static void i2s_32_to_packed24(const int32_t *in, uint8_t *out, int n_stereo_frames)
{
int j = 0;
for (int i = 0; i < n_stereo_frames * NUM_CHANNELS; i++) {
uint32_t s = (uint32_t)in[i];
out[j++] = (s >> 24) & 0xFF;
out[j++] = (s >> 16) & 0xFF;
out[j++] = (s >> 8) & 0xFF;
}
}
static int build_frame(uint8_t *buf, const uint8_t *src,
uint32_t seq, const uint8_t *payload, uint16_t plen)
{
memset(buf, 0, MAC_HDR_LEN);
buf[0] = 0xD0;
memcpy(buf + 4, BCAST, 6);
memcpy(buf + 10, src, 6);
memcpy(buf + 16, src, 6);
int off = MAC_HDR_LEN;
buf[off++] = 127;
buf[off++] = OUI_RKY[0];
buf[off++] = OUI_RKY[1];
buf[off++] = OUI_RKY[2];
rky_hdr_t *hdr = (rky_hdr_t *)(buf + off);
hdr->frame_seq = seq;
hdr->sub_index = 0;
hdr->pkt_type = 0;
hdr->payload_len = plen;
hdr->burst_width = 1;
off += RKY_HDR_LEN;
if (payload && plen > 0) {
memcpy(buf + off, payload, plen);
off += plen;
}
return off;
}
static void tx_task(void *arg)
{
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
ESP_LOGI(TAG, "TX: %dkHz packed-24-bit stereo, %d bytes/pkt, %d pps",
SAMPLE_RATE / 1000, AUDIO_PAYLOAD, 1000000 / TX_PERIOD_US);
uint8_t frame[MAX_FRAME_LEN];
int32_t i2s_buf[SAMPLES_PER_PKT * NUM_CHANNELS];
uint8_t packed_buf[AUDIO_PAYLOAD];
uint32_t seq = 0;
uint32_t ok = 0, err = 0;
int64_t last_stats = esp_timer_get_time();
int64_t next_tx = esp_timer_get_time();
while (1) {
size_t bytes_read = 0;
esp_err_t rret = i2s_channel_read(s_rx_handle, i2s_buf,
I2S_READ_BYTES,
&bytes_read,
pdMS_TO_TICKS(10));
if (rret != ESP_OK || bytes_read < I2S_READ_BYTES)
continue;
int64_t now = esp_timer_get_time();
if (now < next_tx)
continue;
next_tx += TX_PERIOD_US;
if (now >= next_tx)
next_tx = now + TX_PERIOD_US;
i2s_32_to_packed24(i2s_buf, packed_buf, SAMPLES_PER_PKT);
int flen = build_frame(frame, mac, seq, packed_buf, AUDIO_PAYLOAD);
esp_err_t ret = esp_wifi_80211_tx(WIFI_IF_STA, frame, flen, false);
if (ret == ESP_OK) ok++;
else err++;
seq++;
if ((esp_timer_get_time() - last_stats) >= (int64_t)STATS_INTERVAL_MS * 1000) {
float elapsed = (float)STATS_INTERVAL_MS / 1000.f;
ESP_LOGI(TAG, "seq=%lu ok=%lu err=%lu pps=%.0f",
(unsigned long)seq, (unsigned long)ok, (unsigned long)err,
(float)(ok + err) / elapsed);
ok = err = 0;
last_stats = esp_timer_get_time();
}
}
}
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());
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));
wifi_config_t sta_cfg = {0};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_cfg));
ESP_ERROR_CHECK(esp_wifi_set_protocol(WIFI_IF_STA,
WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE));
ESP_ERROR_CHECK(esp_wifi_config_80211_tx_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M));
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
}
void app_main(void)
{
wifi_init();
i2s_init();
xTaskCreatePinnedToCore(tx_task, "tx", 8192, NULL,
configMAX_PRIORITIES - 1, NULL, 0);
}

View File

@ -0,0 +1,24 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1=y
CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=0
CONFIG_ESP_WIFI_STATIC_TX_BUFFER=y
CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=16
CONFIG_ESP_WIFI_IRAM_OPT=y
CONFIG_ESP_WIFI_RX_IRAM_OPT=y
CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y
CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y
# B + G + N
CONFIG_ESP_WIFI_11B_ENABLED=y

View File

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(kitty)

View File

@ -0,0 +1,5 @@
idf_component_register(
SRCS "main.c" "usb_audio.c"
INCLUDE_DIRS "."
PRIV_REQUIRES esp_wifi nvs_flash esp_netif esp_event esp_timer esp_hw_support soc
)

View File

@ -0,0 +1,4 @@
dependencies:
espressif/usb_device_uac:
version: "^1.0.0"
idf: ">=5.1"

359
ADC/kitty-rx/main/main.c Normal file
View File

@ -0,0 +1,359 @@
/*
* Kitty: WiFi RKY audio receiver -> USB Audio Class 2 microphone.
*
* WiFi RX path copied verbatim from lucy/receiver (dual-path: ISR hook +
* promisc callback). Output changed from I2S DMA to ring buffer -> UAC2 mic.
*
* Incoming audio is 24-bit packed (from bubbles TX). Unpacked to 16-bit
* for USB output: top 2 bytes of each 3-byte sample.
*
* Test tone: 1kHz sine injected into ring buffer when no WiFi for 500ms.
*/
#include <string.h>
#include <math.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 "soc/lldesc.h"
#include "esp_private/wifi_os_adapter.h"
#include "xtensa/xtensa_api.h"
#include "esp_cpu.h"
#include "ring_buf.h"
#include "usb_audio.h"
#define TAG "kitty"
#define CHANNEL 1
#define SAMPLE_RATE 48000
#define FRAMES_PER_PKT 48
#define NUM_CHANNELS 2
#define PACKED_BYTES 3
/* 48 stereo frames * 2ch * 3 bytes = 288 bytes packed audio per packet */
#define AUDIO_PAYLOAD_SIZE (FRAMES_PER_PKT * NUM_CHANNELS * PACKED_BYTES)
/* 48 stereo frames * 2ch * 2 bytes = 192 bytes after 16-bit truncation */
#define AUDIO_16BIT_SIZE (FRAMES_PER_PKT * NUM_CHANNELS * 2)
/* Test tone: 1kHz sine at -12dBFS, injected when no WiFi for >500ms */
#define TONE_FREQ 1000
#define TONE_AMPLITUDE 0x1000
#define TONE_TIMEOUT_US 500000
/* 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 ring_buf_t s_ring;
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_isrs;
static volatile uint32_t s_last_seq;
static volatile int64_t s_last_rx_time;
/* ---------- 24-bit to 16-bit unpack + ring write ---------- */
static inline void IRAM_ATTR unpack_24_to_16_ring(const uint8_t *packed, uint16_t packed_len)
{
uint32_t n_samples = packed_len / PACKED_BYTES;
uint8_t tmp[AUDIO_16BIT_SIZE];
if (n_samples > FRAMES_PER_PKT * NUM_CHANNELS)
n_samples = FRAMES_PER_PKT * NUM_CHANNELS;
uint32_t j = 0;
for (uint32_t i = 0; i < n_samples; i++) {
/* packed[j]=MSB packed[j+1]=mid packed[j+2]=LSB -> 16-bit LE = {mid, MSB} */
tmp[i * 2] = packed[j + 1];
tmp[i * 2 + 1] = packed[j];
j += PACKED_BYTES;
}
size_t written = ring_buf_write(&s_ring, tmp, n_samples * 2);
if (written < n_samples * 2)
s_stat_overruns++;
s_last_rx_time = esp_timer_get_time();
}
/* ---------- 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;
/* unpack 24-bit packed to 16-bit and write to ring buffer */
unpack_24_to_16_ring(audio, plen);
}
/* ---------- 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);
}
}
/* ---------- 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++;
}
/* ---------- 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));
}
/* ---------- test tone ---------- */
static int16_t s_sine_table[FRAMES_PER_PKT];
static void tone_table_init(void)
{
for (int i = 0; i < FRAMES_PER_PKT; i++)
s_sine_table[i] = (int16_t)(TONE_AMPLITUDE *
sinf(2.0f * M_PI * TONE_FREQ * i / SAMPLE_RATE));
}
static void tone_task(void *arg)
{
TickType_t xLastWake = xTaskGetTickCount();
uint32_t phase = 0;
bool was_toning = false;
while (1) {
vTaskDelayUntil(&xLastWake, pdMS_TO_TICKS(1));
int64_t now = esp_timer_get_time();
if ((now - s_last_rx_time) < TONE_TIMEOUT_US) {
if (was_toning) {
ESP_LOGI(TAG, "tone: WiFi data resumed, stopping tone");
was_toning = false;
}
continue;
}
if (!was_toning) {
ESP_LOGW(TAG, "tone: no WiFi for %dms, injecting 1kHz test tone",
(int)(TONE_TIMEOUT_US / 1000));
was_toning = true;
phase = 0;
}
int16_t buf[FRAMES_PER_PKT * NUM_CHANNELS];
for (int i = 0; i < FRAMES_PER_PKT; i++) {
int16_t s = s_sine_table[(phase + i) % FRAMES_PER_PKT];
buf[i * 2] = s;
buf[i * 2 + 1] = s;
}
phase = (phase + FRAMES_PER_PKT) % FRAMES_PER_PKT;
ring_buf_write(&s_ring, (const uint8_t *)buf, sizeof(buf));
}
}
/* ---------- 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 fill = ring_buf_fill(&s_ring);
uint32_t total = s_stat_isr + s_stat_prom;
bool toning = (now - s_last_rx_time) >= TONE_TIMEOUT_US;
ESP_LOGI(TAG, "isr=%lu prom=%lu dedup=%lu | gaps=%lu over=%lu | ring=%lu/%d | isrs=%lu pps=%.0f%s",
(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)fill,
RING_BUF_SIZE,
(unsigned long)s_stat_isrs,
total / elapsed,
toning ? " [TONE]" : "");
s_stat_isr = 0;
s_stat_prom = 0;
s_stat_dedup = 0;
s_stat_gaps = 0;
s_stat_overruns = 0;
s_stat_isrs = 0;
last = now;
}
}
/* ---------- main ---------- */
void app_main(void)
{
tone_table_init();
ring_buf_init(&s_ring);
usb_audio_init(&s_ring);
wifi_init();
ESP_LOGI(TAG, "rx: wifi ch=%d -> UAC2 mic 48kHz/16/stereo, isr_hook=%s, ring=%d bytes",
CHANNEL,
s_orig_intr_num >= 0 ? "active" : "inactive",
RING_BUF_SIZE);
xTaskCreatePinnedToCore(stats_task, "stats", 4096, NULL, 5, NULL, 0);
xTaskCreatePinnedToCore(tone_task, "tone", 4096, NULL, 6, NULL, 1);
}

View File

@ -0,0 +1,84 @@
/*
* Lock-free SPSC ring buffer for audio transport.
* Producer: WiFi ISR/promisc callback (writes 16-bit truncated samples).
* Consumer: UAC mic callback (reads arbitrary byte counts).
*
* Byte-granularity. Single writer, single reader, no locks.
*/
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "esp_attr.h"
#define RING_BUF_SIZE (48000 * 4 * 1) /* 1 second of 48kHz 16-bit stereo */
typedef struct {
uint8_t buf[RING_BUF_SIZE];
volatile uint32_t wr;
volatile uint32_t rd;
} ring_buf_t;
static inline void ring_buf_init(ring_buf_t *rb)
{
rb->wr = 0;
rb->rd = 0;
}
static inline uint32_t ring_buf_fill(const ring_buf_t *rb)
{
uint32_t w = rb->wr;
uint32_t r = rb->rd;
return (w >= r) ? (w - r) : (RING_BUF_SIZE - r + w);
}
static inline uint32_t ring_buf_free(const ring_buf_t *rb)
{
return RING_BUF_SIZE - 1 - ring_buf_fill(rb);
}
/* Write contiguous block. Returns bytes actually written. */
static inline size_t IRAM_ATTR ring_buf_write(ring_buf_t *rb, const uint8_t *data, size_t len)
{
uint32_t free = ring_buf_free(rb);
if (len > free) len = free;
if (len == 0) return 0;
uint32_t w = rb->wr;
uint32_t tail = RING_BUF_SIZE - w;
if (len <= tail) {
memcpy(rb->buf + w, data, len);
} else {
memcpy(rb->buf + w, data, tail);
memcpy(rb->buf, data + tail, len - tail);
}
__asm__ __volatile__("memw" ::: "memory");
rb->wr = (w + len) % RING_BUF_SIZE;
return len;
}
/* Read up to len bytes. Returns bytes actually read. */
static inline size_t ring_buf_read(ring_buf_t *rb, uint8_t *dst, size_t len)
{
uint32_t fill = ring_buf_fill(rb);
if (len > fill) len = fill;
if (len == 0) return 0;
uint32_t r = rb->rd;
uint32_t tail = RING_BUF_SIZE - r;
if (len <= tail) {
memcpy(dst, rb->buf + r, len);
} else {
memcpy(dst, rb->buf + r, tail);
memcpy(dst + tail, rb->buf, len - tail);
}
__asm__ __volatile__("memw" ::: "memory");
rb->rd = (r + len) % RING_BUF_SIZE;
return len;
}

View File

@ -0,0 +1,47 @@
/*
* UAC2 microphone: feeds WiFi-received audio to USB host.
*
* The usb_device_uac component calls our input callback periodically
* (every CONFIG_UAC_MIC_INTERVAL_MS). We read from the shared ring buffer
* and hand it to the USB stack. If the ring buffer has insufficient data,
* we zero-fill the remainder (silence).
*/
#include <string.h>
#include "esp_log.h"
#include "usb_device_uac.h"
#include "usb_audio.h"
#define TAG "uac"
static ring_buf_t *s_rb;
static esp_err_t mic_input_cb(uint8_t *buf, size_t len, size_t *bytes_read, void *ctx)
{
size_t got = ring_buf_read(s_rb, buf, len);
if (got < len)
memset(buf + got, 0, len - got);
*bytes_read = len;
return ESP_OK;
}
void usb_audio_init(ring_buf_t *rb)
{
s_rb = rb;
uac_device_config_t cfg = {
.output_cb = NULL,
.input_cb = mic_input_cb,
.set_mute_cb = NULL,
.set_volume_cb = NULL,
.cb_ctx = NULL,
};
esp_err_t err = uac_device_init(&cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "uac_device_init failed: %s", esp_err_to_name(err));
return;
}
ESP_LOGI(TAG, "UAC2 mic ready: 48kHz 16-bit stereo");
}

View File

@ -0,0 +1,10 @@
/*
* UAC2 microphone device via espressif/usb_device_uac.
* Presents as USB audio input: 48kHz 16-bit stereo.
*/
#pragma once
#include "ring_buf.h"
void usb_audio_init(ring_buf_t *rb);

View File

@ -0,0 +1,38 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
CONFIG_ESP_WIFI_SOFTAP_SUPPORT=n
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_WIFI_STATIC_TX_BUFFER=y
CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=8
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=25
CONFIG_ESP_WIFI_IRAM_OPT=y
CONFIG_ESP_WIFI_RX_IRAM_OPT=y
CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y
CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n
# UAC2: mic only, 48kHz stereo 16-bit
CONFIG_UAC_SAMPLE_RATE=48000
CONFIG_UAC_MIC_CHANNEL_NUM=2
CONFIG_UAC_SPEAKER_CHANNEL_NUM=0
CONFIG_UAC_MIC_INTERVAL_MS=10
CONFIG_UAC_SUPPORT_MACOS=y
# Pin TinyUSB + mic tasks to core 1 (WiFi ISR on core 0)
CONFIG_UAC_TINYUSB_TASK_CORE=1
CONFIG_UAC_MIC_TASK_CORE=1
CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
# Console on UART0 (GPIO43 TX, GPIO44 RX) -- TinyUSB takes USB-OTG pins
CONFIG_ESP_CONSOLE_UART_DEFAULT=y
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y

View File

@ -0,0 +1,3 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(receiver)

View File

@ -0,0 +1,5 @@
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES esp_wifi nvs_flash esp_netif esp_event driver esp_timer esp_driver_i2s
)

407
DAC/lucy-rx/main/main.c Normal file
View File

@ -0,0 +1,407 @@
/*
* 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);
}

View File

@ -0,0 +1,23 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y
CONFIG_ESP_WIFI_SOFTAP_SUPPORT=n
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_WIFI_STATIC_TX_BUFFER=y
CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=8
CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=25
CONFIG_ESP_WIFI_IRAM_OPT=y
CONFIG_ESP_WIFI_RX_IRAM_OPT=y
CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y
CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n
CONFIG_ESP_WIFI_11B_ENABLED=y

View File

@ -0,0 +1,4 @@
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(slot4_tx)

View File

@ -0,0 +1,5 @@
idf_component_register(
SRCS "main.c" "hijack_tx.c"
INCLUDE_DIRS "."
REQUIRES esp_wifi esp_timer nvs_flash esp_netif esp_event esp_rom heap
)

View File

@ -0,0 +1,144 @@
/*
* hijack_tx -- DMA hijack TX for ESP32-S3.
*
* Fires a blob TX on slot 0 with a dummy frame sized to match the real
* frame, then swaps the DMA descriptor's buffer pointer before hardware
* reads it. The blob's PLCP setup uses the correct length because the
* dummy is the same size. No PLCP patching race.
*
* Sequence:
* 1. Build dummy frame matching target length
* 2. Fire blob TX (esp_wifi_80211_tx) on slot 0
* 3. Spin-wait for slot 0 TXQ_CTRL bits [31:30] set
* 4. Extract DMA descriptor from TXQ_CTRL[19:0]
* 5. Swap desc->buf to our frame
* 6. Wait for completion (bits [31:30] clear)
* 7. Restore descriptor to prevent blob crash
*/
#include "hijack_tx.h"
#include <string.h>
#include <stdio.h>
#include "esp_heap_caps.h"
#include "esp_wifi.h"
#include "esp_mac.h"
#include "esp_timer.h"
#include "esp_rom_lldesc.h"
#include "xtensa/core-macros.h"
#define REG32(addr) (*(volatile uint32_t *)(addr))
#define S0_TXQ_CTRL 0x60033D08
#define S0_PMD 0x60034320
#define MAX_FRAME_SIZE 512
#define DMA_BASE 0x3FC00000
static uint8_t *s_dummy = NULL;
static uint8_t *s_frame_buf = NULL;
static uint8_t s_own_mac[6];
static uint32_t s_ok_count = 0;
static uint32_t s_miss_count = 0;
static uint32_t s_fail_count = 0;
void hijack_init(void)
{
if (s_frame_buf) return;
s_frame_buf = heap_caps_calloc(1, MAX_FRAME_SIZE,
MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
s_dummy = heap_caps_calloc(1, MAX_FRAME_SIZE,
MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
assert(s_frame_buf);
assert(s_dummy);
esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA);
}
static void build_dummy(uint16_t len)
{
memset(s_dummy, 0, len);
s_dummy[0] = 0xD0;
memcpy(s_dummy + 4, "\xFF\xFF\xFF\xFF\xFF\xFF", 6);
memcpy(s_dummy + 10, s_own_mac, 6);
memcpy(s_dummy + 16, s_own_mac, 6);
s_dummy[24] = 127;
s_dummy[25] = 0x52;
s_dummy[26] = 0x4B;
s_dummy[27] = 0x59;
}
int IRAM_ATTR hijack_send(const uint8_t *frame, uint16_t len)
{
if (!s_frame_buf) return -1;
if (len == 0 || len > MAX_FRAME_SIZE) return -1;
memcpy(s_frame_buf, frame, len);
build_dummy(len);
esp_wifi_80211_tx(WIFI_IF_STA, s_dummy, len, false);
uint32_t spin_start = xthal_get_ccount();
uint32_t spin_limit = 5000 * 240;
lldesc_t *desc = NULL;
volatile const uint8_t *orig_buf = NULL;
uint16_t orig_size = 0;
uint16_t orig_len = 0;
while ((xthal_get_ccount() - spin_start) < spin_limit) {
uint32_t ctrl = REG32(S0_TXQ_CTRL);
if (ctrl & 0xC0000000) {
desc = (lldesc_t *)(DMA_BASE | (ctrl & 0x000FFFFF));
orig_buf = desc->buf;
orig_size = desc->size;
orig_len = desc->length;
desc->buf = (volatile const uint8_t *)s_frame_buf;
__asm__ __volatile__("memw" ::: "memory");
break;
}
}
if (!desc) {
s_miss_count++;
return -1;
}
int result = -1;
int64_t poll_end = esp_timer_get_time() + 200000;
while (esp_timer_get_time() < poll_end) {
if ((REG32(S0_TXQ_CTRL) & 0xC0000000) == 0) {
uint32_t pmd = REG32(S0_PMD);
uint8_t pmd_code = (pmd >> 12) & 0xF;
result = (pmd_code == 0 || pmd_code == 4) ? 0 : (int)pmd_code;
break;
}
}
desc->buf = orig_buf;
desc->size = orig_size;
desc->length = orig_len;
__asm__ __volatile__("memw" ::: "memory");
if (result == 0)
s_ok_count++;
else
s_fail_count++;
if (((s_ok_count + s_fail_count) % 100) == 0) {
uint32_t swap_us = (xthal_get_ccount() - spin_start) / 240;
printf("hijack: ok=%lu fail=%lu miss=%lu swap=%luus pmd=%d\n",
(unsigned long)s_ok_count,
(unsigned long)s_fail_count,
(unsigned long)s_miss_count,
(unsigned long)swap_us,
result);
}
return result;
}

View File

@ -0,0 +1,16 @@
#ifndef HIJACK_TX_H
#define HIJACK_TX_H
#include <stdint.h>
/* allocate DMA-capable frame buffer */
void hijack_init(void);
/*
* Transmit frame via slot 0 DMA hijack.
* Fires a blob TX, intercepts the DMA descriptor, swaps in our buffer.
* Returns 0 on success, -1 on miss/timeout.
*/
int hijack_send(const uint8_t *frame, uint16_t len);
#endif

214
DAC/ricky-tx/main/main.c Normal file
View File

@ -0,0 +1,214 @@
/*
* DMA hijack TX: 440Hz sine wave via slot 0 buffer swap.
* 48kHz 16-bit stereo interleaved. 48 samples/pkt = 192 bytes audio.
* 1000 packets/sec (1ms period). Uses ricky's original hijack_send.
*/
#include <string.h>
#include <math.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 "esp_mac.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_event.h"
#include "hijack_tx.h"
#define TAG "hijack-tx"
static const uint8_t BCAST[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
static const uint8_t OUI_RKY[] = {0x52, 0x4B, 0x59};
#define CHANNEL 1
#define SAMPLE_RATE 48000
#define SINE_FREQ 440
#define SAMPLES_PER_PKT 48
#define BYTES_PER_SAMPLE 4
#define AUDIO_PAYLOAD (SAMPLES_PER_PKT * BYTES_PER_SAMPLE)
#define TX_PERIOD_US 1000
#define STATS_INTERVAL_MS 5000
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)
#define MAX_FRAME_LEN (FRAME_OVERHEAD + AUDIO_PAYLOAD)
/* pre-computed sine LUT: 4800 samples = 100 full 440Hz cycles at 48kHz */
#define SINE_TABLE_LEN 4800
static int16_t sine_table[SINE_TABLE_LEN];
static uint32_t s_sample_offset = 0;
static void init_sine_table(void)
{
for (int i = 0; i < SINE_TABLE_LEN; i++) {
float t = (float)i / (float)SAMPLE_RATE;
sine_table[i] = (int16_t)(sinf(2.0f * M_PI * SINE_FREQ * t) * 4096.0f);
}
}
static void gen_sine(int16_t *buf, int n_samples)
{
for (int i = 0; i < n_samples; i++) {
int16_t v = sine_table[(s_sample_offset + i) % SINE_TABLE_LEN];
buf[i * 2] = v;
buf[i * 2 + 1] = v;
}
s_sample_offset = (s_sample_offset + n_samples) % SINE_TABLE_LEN;
}
static int build_frame(uint8_t *buf, const uint8_t *src,
uint32_t seq, const uint8_t *payload, uint16_t plen)
{
memset(buf, 0, MAC_HDR_LEN);
buf[0] = 0xD0;
memcpy(buf + 4, BCAST, 6);
memcpy(buf + 10, src, 6);
memcpy(buf + 16, src, 6);
int off = MAC_HDR_LEN;
buf[off++] = 127;
buf[off++] = OUI_RKY[0];
buf[off++] = OUI_RKY[1];
buf[off++] = OUI_RKY[2];
rky_hdr_t *hdr = (rky_hdr_t *)(buf + off);
hdr->frame_seq = seq;
hdr->sub_index = 0;
hdr->pkt_type = 0;
hdr->payload_len = plen;
hdr->burst_width = 1;
off += RKY_HDR_LEN;
if (payload && plen > 0) {
memcpy(buf + off, payload, plen);
off += plen;
}
return off;
}
static void tx_task(void *arg)
{
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
init_sine_table();
hijack_init();
/* prime blob TX engine -- required before hijack_send works */
{
uint8_t dummy[64] = {0};
dummy[0] = 0xD0;
memcpy(dummy + 4, BCAST, 6);
memcpy(dummy + 10, mac, 6);
memcpy(dummy + 16, mac, 6);
dummy[24] = 127;
dummy[25] = OUI_RKY[0];
dummy[26] = OUI_RKY[1];
dummy[27] = OUI_RKY[2];
esp_wifi_80211_tx(WIFI_IF_STA, dummy, sizeof(dummy), false);
vTaskDelay(pdMS_TO_TICKS(10));
}
ESP_LOGI(TAG, "TX start: %dHz sine, %dkHz/16-bit stereo, %d bytes/pkt, period=%dus",
SINE_FREQ, SAMPLE_RATE / 1000, AUDIO_PAYLOAD, TX_PERIOD_US);
uint8_t frame[MAX_FRAME_LEN];
int16_t audio_buf[SAMPLES_PER_PKT * 2];
uint32_t seq = 0;
uint32_t ok = 0, err = 0, miss = 0;
int64_t dur_sum = 0, dur_min = INT64_MAX, dur_max = 0;
uint32_t dur_n = 0;
int64_t last_stats = esp_timer_get_time();
int64_t next_tx = esp_timer_get_time();
while (1) {
while (esp_timer_get_time() < next_tx) { }
next_tx += TX_PERIOD_US;
int64_t now = esp_timer_get_time();
if (now >= next_tx)
next_tx = now + TX_PERIOD_US;
int64_t t0 = esp_timer_get_time();
gen_sine(audio_buf, SAMPLES_PER_PKT);
int flen = build_frame(frame, mac, seq,
(uint8_t *)audio_buf, AUDIO_PAYLOAD);
esp_err_t ret = esp_wifi_80211_tx(WIFI_IF_STA, frame, flen, false);
if (ret == ESP_OK) ok++;
else err++;
seq++;
int64_t dur = esp_timer_get_time() - t0;
dur_sum += dur;
dur_n++;
if (dur < dur_min) dur_min = dur;
if (dur > dur_max) dur_max = dur;
if ((esp_timer_get_time() - last_stats) >= (int64_t)STATS_INTERVAL_MS * 1000) {
double avg = dur_n > 0 ? (double)dur_sum / dur_n : 0;
double rate = dur_n > 0 ? (double)dur_n / ((double)STATS_INTERVAL_MS / 1000.0) : 0;
ESP_LOGI(TAG, "seq=%lu ok=%lu err=%lu miss=%lu "
"dur avg=%.0f min=%lld max=%lld pps=%.0f",
(unsigned long)seq,
(unsigned long)ok, (unsigned long)err,
(unsigned long)miss,
avg, (long long)dur_min, (long long)dur_max, rate);
ok = err = miss = dur_n = 0;
dur_sum = 0;
dur_min = INT64_MAX;
dur_max = 0;
last_stats = esp_timer_get_time();
}
}
}
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());
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));
wifi_config_t sta_cfg = {0};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_cfg));
ESP_ERROR_CHECK(esp_wifi_set_protocol(WIFI_IF_STA,
WIFI_PROTOCOL_11B | WIFI_PROTOCOL_11G | WIFI_PROTOCOL_11N));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_set_channel(CHANNEL, WIFI_SECOND_CHAN_NONE));
ESP_ERROR_CHECK(esp_wifi_config_80211_tx_rate(WIFI_IF_STA, WIFI_PHY_RATE_54M));
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
}
void app_main(void)
{
wifi_init();
ESP_LOGI(TAG, "440Hz sine TX via DMA hijack (ricky original)");
xTaskCreatePinnedToCore(tx_task, "tx", 8192, NULL,
configMAX_PRIORITIES - 1, NULL, 0);
}

View File

@ -0,0 +1,24 @@
CONFIG_IDF_TARGET="esp32s3"
CONFIG_FREERTOS_HZ=1000
CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_1=y
CONFIG_ESP_WIFI_ESPNOW_MAX_ENCRYPT_NUM=0
CONFIG_ESP_WIFI_STATIC_TX_BUFFER=y
CONFIG_ESP_WIFI_STATIC_TX_BUFFER_NUM=16
CONFIG_ESP_WIFI_IRAM_OPT=y
CONFIG_ESP_WIFI_RX_IRAM_OPT=y
CONFIG_ESP_WIFI_EXTRA_IRAM_OPT=y
CONFIG_ESP_WIFI_MGMT_SBUF_NUM=32
CONFIG_ESP_WIFI_AMPDU_TX_ENABLED=n
CONFIG_ESP_WIFI_AMPDU_RX_ENABLED=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y
# B + G + N
CONFIG_ESP_WIFI_11B_ENABLED=y

12
LICENCE Normal file
View File

@ -0,0 +1,12 @@
This is free to use, without conditions.
There is no licence here on purpose. Individuals, students, hobbyists — take what
you need, make it yours, don't think twice. You'd flatter me.
The absence of a licence is deliberate. A licence is a legal surface. Words can be
reinterpreted, and corporations employ lawyers whose job is exactly that. Silence is
harder to exploit than language. If a company wants to use this, the lack of explicit
permission makes it just inconvenient enough to matter.
This won't change the world. But it shifts the balance, even slightly, away from the
system that co-opts open work for closed profit. That's enough for me.