s3-blob-utils/ADC/bubbles-tx/main/main.c

234 lines
7.0 KiB
C

/*
* 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);
}