Init. Blob exploits for slot4 on the s3 inspired by esp-open-mac.
This commit is contained in:
commit
d9752ca39a
|
|
@ -0,0 +1,5 @@
|
|||
build/
|
||||
sdkconfig
|
||||
sdkconfig.old
|
||||
managed_components/
|
||||
dependencies.lock
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(bubbles)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(kitty)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
dependencies:
|
||||
espressif/usb_device_uac:
|
||||
version: "^1.0.0"
|
||||
idf: ">=5.1"
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(receiver)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(slot4_tx)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
Loading…
Reference in New Issue