main.c 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. /*
  2. * main.c — application entry point for the ESP32-P4 Dante/AES67 endpoint.
  3. *
  4. * Phase 1 target is AES67 interop. This file is adapted from the example
  5. * application in DatanoiseTV/aes67-esp32p4 (Apache-2.0) — the AES67 logic is
  6. * kept as-is; the hardware pin map is sourced from board_config.h so there is
  7. * a single place to retarget the board (see CLAUDE.md / README.md).
  8. *
  9. * SPDX-FileCopyrightText: 2026 DatanoiseTV (original example)
  10. * SPDX-FileCopyrightText: 2026 Benjamin Harris (adaptation)
  11. * SPDX-License-Identifier: Apache-2.0
  12. *
  13. * Flow: init Ethernet (IP101 RMII PHY), wait for IP, bring up the AES67 stack,
  14. * auto-subscribe to the first SAP-discovered source and create a matching
  15. * source stream, capturing/streaming L24 audio via the ES8311 codec.
  16. */
  17. #include <string.h>
  18. #include "freertos/FreeRTOS.h"
  19. #include "freertos/task.h"
  20. #include "freertos/event_groups.h"
  21. #include "esp_log.h"
  22. #include "esp_err.h"
  23. #include "esp_event.h"
  24. #include "esp_netif.h"
  25. #include "nvs_flash.h"
  26. #include "driver/gpio.h"
  27. #include "esp_eth.h"
  28. #include "esp_eth_mac.h"
  29. #include "esp_eth_phy.h"
  30. #include "aes67.h"
  31. #include "aes67_config.h"
  32. #include "aes67_convert.h"
  33. #include "aes67_session.h"
  34. #include "aes67_rtp.h"
  35. #include "aes67_sap.h"
  36. #include "aes67_audio.h"
  37. #include "aes67_webui.h"
  38. #include "dante_discovery.h"
  39. #include "dante_advertise.h"
  40. #include "esp_mac.h"
  41. #include "dsps_tone_gen.h"
  42. #include "driver/i2c.h"
  43. #include "freertos/stream_buffer.h"
  44. #include "esp_cache.h"
  45. #include "esp_ldo_regulator.h"
  46. #include "driver/i2s_std.h"
  47. #include "es8311.h"
  48. #include "board_config.h"
  49. /* ---- Map the board pin table (board_config.h) onto the names the AES67
  50. * example code below expects. Edit board_config.h, not these. ---- */
  51. #define ETH_MDC_GPIO BOARD_ETH_MDC_GPIO
  52. #define ETH_MDIO_GPIO BOARD_ETH_MDIO_GPIO
  53. #define ETH_REF_CLK_GPIO BOARD_ETH_REF_CLK_GPIO
  54. #define ETH_PHY_RST_GPIO BOARD_ETH_PHY_RST_GPIO
  55. #define ETH_PHY_ADDR BOARD_ETH_PHY_ADDR
  56. #define I2S_MCLK_GPIO BOARD_I2S_MCLK_GPIO
  57. #define I2S_SCLK_GPIO BOARD_I2S_BCLK_GPIO
  58. #define I2S_LRCK_GPIO BOARD_I2S_LRCLK_GPIO
  59. #define I2S_DOUT_GPIO BOARD_I2S_DOUT_GPIO
  60. #define I2S_DIN_GPIO BOARD_I2S_DIN_GPIO
  61. #define PA_CTRL_GPIO BOARD_PA_ENABLE_GPIO
  62. #define I2C_SCL_GPIO BOARD_I2C_SCL_GPIO
  63. #define I2C_SDA_GPIO BOARD_I2C_SDA_GPIO
  64. #define I2C_PORT I2C_NUM_0
  65. #define I2C_CLK_HZ 100000
  66. #define ES8311_ADDR ES8311_ADDRESS_0 /* 0x18, CE pin low */
  67. /* Session handle for SAP callback */
  68. static aes67_session_handle_t s_session = NULL;
  69. static bool s_sink_added = false;
  70. /* Stream params for the Ethernet RTP hook (defined below, used by SAP callback) */
  71. extern uint8_t s_hook_word_length;
  72. extern uint8_t s_hook_channels;
  73. /* SAP discovery callback: auto-subscribe to discovered remote sources */
  74. static void sap_discovery_cb(bool is_announce,
  75. const aes67_sap_remote_source_t *source,
  76. void *user_data)
  77. {
  78. if (!is_announce || !source || s_sink_added || !s_session) {
  79. return;
  80. }
  81. /* Subscribe to any discovered source (first one wins) */
  82. ESP_LOGI("main", "Auto-subscribing to \"%s\" from SAP", source->name);
  83. uint8_t sink_id = 0;
  84. esp_err_t err = aes67_session_add_sink(s_session, source->name,
  85. source->sdp, &sink_id);
  86. if (err == ESP_OK) {
  87. s_sink_added = true;
  88. /* Update Ethernet hook params from the SDP */
  89. aes67_sink_t sink;
  90. if (aes67_session_get_sink(s_session, sink_id, &sink) == ESP_OK) {
  91. s_hook_word_length = sink.rtp_config.word_length;
  92. s_hook_channels = sink.rtp_config.channels;
  93. ESP_LOGI("main", "Hook: wl=%u ch=%u rate=%lu ptime=%u",
  94. s_hook_word_length, s_hook_channels,
  95. (unsigned long)sink.rtp_config.sample_rate,
  96. sink.rtp_config.packet_time_us);
  97. /* Create a matching source with same codec/channels/ptime */
  98. aes67_session_set_packet_time(s_session, sink.rtp_config.packet_time_us);
  99. uint8_t src_id = 0;
  100. esp_err_t src_err = aes67_session_add_source(
  101. s_session, "ESP32-P4 AES67",
  102. sink.rtp_config.channels, sink.rtp_config.codec, &src_id);
  103. if (src_err == ESP_OK) {
  104. ESP_LOGI("main", "Source matched to sink: %uch %s %uus",
  105. sink.rtp_config.channels,
  106. aes67_codec_to_str(sink.rtp_config.codec),
  107. sink.rtp_config.packet_time_us);
  108. }
  109. }
  110. ESP_LOGI("main", "Sink added (id=%u) -- receiving \"%s\"",
  111. sink_id, source->name);
  112. } else {
  113. ESP_LOGW("main", "Failed to add sink for \"%s\": %s",
  114. source->name, esp_err_to_name(err));
  115. }
  116. }
  117. static const char *TAG = "main";
  118. /* Event group bit set when the Ethernet interface obtains an IP */
  119. #define ETH_GOT_IP_BIT BIT0
  120. static EventGroupHandle_t s_eth_event_group;
  121. /* Ethernet link event handler */
  122. static void eth_event_handler(void *arg, esp_event_base_t event_base,
  123. int32_t event_id, void *event_data)
  124. {
  125. switch (event_id) {
  126. case ETHERNET_EVENT_CONNECTED:
  127. ESP_LOGI(TAG, "ethernet link up");
  128. break;
  129. case ETHERNET_EVENT_DISCONNECTED:
  130. ESP_LOGW(TAG, "ethernet link down");
  131. break;
  132. case ETHERNET_EVENT_START:
  133. ESP_LOGI(TAG, "ethernet started");
  134. break;
  135. case ETHERNET_EVENT_STOP:
  136. ESP_LOGI(TAG, "ethernet stopped");
  137. break;
  138. default:
  139. break;
  140. }
  141. }
  142. /* IP acquisition event handler */
  143. static void ip_event_handler(void *arg, esp_event_base_t event_base,
  144. int32_t event_id, void *event_data)
  145. {
  146. if (event_id == IP_EVENT_ETH_GOT_IP) {
  147. ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
  148. ESP_LOGI(TAG, "got IP: " IPSTR, IP2STR(&event->ip_info.ip));
  149. xEventGroupSetBits(s_eth_event_group, ETH_GOT_IP_BIT);
  150. }
  151. }
  152. /* --- Ethernet RTP hook: intercept RTP frames before lwIP --- */
  153. static esp_err_t (*s_original_input)(esp_eth_handle_t, uint8_t*, uint32_t, void*, void*) = NULL;
  154. static void *s_original_priv = NULL;
  155. static i2s_chan_handle_t s_hook_tx_chan = NULL;
  156. static int32_t *s_hook_tmp32 = NULL;
  157. static int16_t *s_hook_i16 = NULL;
  158. uint32_t s_hook_pkt_count = 0; /* Visible to webui for stats */
  159. uint32_t s_hook_drop_count = 0;
  160. uint32_t s_hook_fwd_count = 0;
  161. uint32_t s_hook_total_frames = 0;
  162. static uint16_t s_hook_last_seq = 0;
  163. static uint32_t s_hook_seq_gaps = 0;
  164. uint32_t s_hook_seq_lost = 0;
  165. StreamBufferHandle_t s_hook_sbuf = NULL; /* Global: shared with playback task */
  166. uint8_t s_hook_word_length = 3; /* Default L24. Updated from SDP. */
  167. uint8_t s_hook_channels = 2; /* Default stereo. Updated from SDP. */
  168. /* Called for EVERY Ethernet frame, before lwIP.
  169. * RTP multicast on port 5004: process + free. Everything else: forward to lwIP. */
  170. static esp_err_t IRAM_ATTR eth_rtp_hook(esp_eth_handle_t eth_handle,
  171. uint8_t *buffer, uint32_t length,
  172. void *priv, void *info)
  173. {
  174. s_hook_total_frames++;
  175. /* Quick reject: minimum Ethernet+IP+UDP+RTP header = 54 bytes */
  176. if (length < 54 || !s_hook_tx_chan) goto forward;
  177. /* EtherType must be IPv4 */
  178. if (buffer[12] != 0x08 || buffer[13] != 0x00) goto forward;
  179. /* IP protocol must be UDP (17) */
  180. uint8_t ihl = (buffer[14] & 0x0F) * 4;
  181. if (buffer[14 + 9] != 17) goto forward;
  182. /* Must be IPv4 multicast destination (224.0.0.0/4) */
  183. if ((buffer[14 + 16] & 0xF0) != 0xE0) goto forward;
  184. /* UDP destination port must be 5004 */
  185. int udp_off = 14 + ihl;
  186. uint16_t dst_port = (buffer[udp_off + 2] << 8) | buffer[udp_off + 3];
  187. if (dst_port != 5004) goto forward;
  188. /* RTP version must be 2 */
  189. int rtp_off = udp_off + 8;
  190. if ((buffer[rtp_off] >> 6) != 2) goto forward;
  191. /* Track RTP sequence numbers to detect packet loss */
  192. {
  193. uint16_t seq = (buffer[rtp_off + 2] << 8) | buffer[rtp_off + 3];
  194. if (s_hook_pkt_count > 0) {
  195. uint16_t expected = s_hook_last_seq + 1;
  196. if (seq != expected) {
  197. int16_t gap = (int16_t)(seq - expected);
  198. if (gap > 0 && gap < 100) {
  199. s_hook_seq_gaps++;
  200. s_hook_seq_lost += gap;
  201. }
  202. }
  203. }
  204. s_hook_last_seq = seq;
  205. }
  206. /* Extract RTP payload */
  207. uint8_t cc = buffer[rtp_off] & 0x0F;
  208. int payload_off = rtp_off + 12 + cc * 4;
  209. /* Handle RTP header extension */
  210. if (buffer[rtp_off] & 0x10) {
  211. if ((int)length < payload_off + 4) goto forward;
  212. uint16_t ext_len = (buffer[payload_off + 2] << 8) | buffer[payload_off + 3];
  213. payload_off += 4 + ext_len * 4;
  214. }
  215. if (payload_off >= (int)length) goto forward;
  216. const uint8_t *payload = buffer + payload_off;
  217. int payload_len = length - payload_off;
  218. /* ESP-IDF Ethernet driver already does M2C cache sync in
  219. * emac_esp_dma_receive_frame(). No additional sync needed. */
  220. /* Handle RTP padding (P bit) */
  221. if (buffer[rtp_off] & 0x20) {
  222. uint8_t pad_len = buffer[length - 1];
  223. payload_len -= pad_len;
  224. if (payload_len <= 0) goto forward;
  225. }
  226. /* Use UDP length field instead of Ethernet frame length to avoid
  227. * including Ethernet FCS or padding bytes in the audio data.
  228. * UDP length = header(8) + payload. */
  229. uint16_t udp_len = (buffer[udp_off + 4] << 8) | buffer[udp_off + 5];
  230. int rtp_total = udp_len - 8; /* UDP payload = RTP header + RTP payload */
  231. int rtp_payload = rtp_total - (payload_off - rtp_off);
  232. if (rtp_payload > 0 && rtp_payload < payload_len) {
  233. payload_len = rtp_payload; /* Trim to actual RTP payload */
  234. }
  235. /* Use word length and channel count from SDP (set when sink is added) */
  236. int wl = s_hook_word_length;
  237. int ch = s_hook_channels;
  238. int frame_bytes = ch * wl;
  239. int frames = payload_len / frame_bytes;
  240. if (frames <= 0 || frames > AES67_MAX_TIC_FRAMES) goto forward;
  241. /* Log first packet details */
  242. if (s_hook_pkt_count == 0) {
  243. ESP_DRAM_LOGI("eth_hook", "FIRST PKT: payload=%d bytes, wl=%d, ch=%d, frames=%d, "
  244. "write_bytes=%d", payload_len, wl, ch, frames,
  245. frames * ch * (int)sizeof(int32_t));
  246. }
  247. /* Convert to int32 -- stereo passthrough, no mono mix */
  248. aes67_convert_from_net(payload, s_hook_tmp32, frames, ch, wl);
  249. /* Write int32 stereo directly to stream buffer (read by DMA ISR) */
  250. extern void aes67_audio_slot_write(const void *data, uint32_t len);
  251. aes67_audio_slot_write(s_hook_tmp32, frames * ch * sizeof(int32_t));
  252. s_hook_pkt_count++;
  253. /* Rate-limited loss reporting: max once per second */
  254. static uint32_t s_last_seq_lost = 0;
  255. static uint32_t s_last_loss_log_pkt = 0;
  256. if (s_hook_seq_lost != s_last_seq_lost &&
  257. (s_hook_pkt_count - s_last_loss_log_pkt) > 8000) {
  258. ESP_LOGW("eth_hook", "SEQ LOSS: %lu gaps, %lu lost (total %lu pkts)",
  259. (unsigned long)s_hook_seq_gaps,
  260. (unsigned long)s_hook_seq_lost,
  261. (unsigned long)s_hook_pkt_count);
  262. s_last_seq_lost = s_hook_seq_lost;
  263. s_last_loss_log_pkt = s_hook_pkt_count;
  264. }
  265. free(buffer); /* We consumed the frame */
  266. return ESP_OK;
  267. forward:
  268. s_hook_fwd_count++;
  269. /* Not RTP -- forward to lwIP via esp_netif_receive */
  270. if (s_original_priv) {
  271. return esp_netif_receive((esp_netif_t *)s_original_priv, buffer, length, NULL);
  272. }
  273. free(buffer);
  274. return ESP_OK;
  275. }
  276. /*
  277. * Initialize the IP101 Ethernet PHY over RMII.
  278. * The ESP32-P4-NANO uses an external 50MHz crystal for the RMII reference
  279. * clock, so we configure EMAC_CLK_EXT_IN on GPIO50.
  280. */
  281. static esp_err_t ethernet_init(esp_eth_handle_t *out_eth_handle)
  282. {
  283. /* EMAC configuration with RMII pin mapping */
  284. eth_esp32_emac_config_t emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG();
  285. emac_config.smi_gpio.mdc_num = ETH_MDC_GPIO;
  286. emac_config.smi_gpio.mdio_num = ETH_MDIO_GPIO;
  287. emac_config.interface = EMAC_DATA_INTERFACE_RMII;
  288. emac_config.clock_config.rmii.clock_mode = EMAC_CLK_EXT_IN;
  289. emac_config.clock_config.rmii.clock_gpio = ETH_REF_CLK_GPIO;
  290. eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
  291. mac_config.flags = ETH_MAC_FLAG_PIN_TO_CORE; /* Pin to core 0 */
  292. mac_config.rx_task_prio = 24; /* Highest: above lwIP (23), feeds audio ISR directly */
  293. esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&emac_config, &mac_config);
  294. if (!mac) {
  295. ESP_LOGE(TAG, "failed to create EMAC");
  296. return ESP_FAIL;
  297. }
  298. /* IP101 PHY at address 1, with hardware reset on GPIO51 */
  299. eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
  300. phy_config.phy_addr = ETH_PHY_ADDR;
  301. phy_config.reset_gpio_num = ETH_PHY_RST_GPIO;
  302. esp_eth_phy_t *phy = esp_eth_phy_new_ip101(&phy_config);
  303. if (!phy) {
  304. ESP_LOGE(TAG, "failed to create IP101 PHY");
  305. return ESP_FAIL;
  306. }
  307. /* Install the Ethernet driver */
  308. esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, phy);
  309. esp_eth_handle_t eth_handle = NULL;
  310. esp_err_t ret = esp_eth_driver_install(&eth_config, &eth_handle);
  311. if (ret != ESP_OK) {
  312. ESP_LOGE(TAG, "ethernet driver install failed: %s", esp_err_to_name(ret));
  313. return ret;
  314. }
  315. /* Create the netif with key "ETH_0" so the L2 TAP interface used by
  316. * ptpd can find it. The default ESP_NETIF_DEFAULT_ETH uses "ETH_DEF"
  317. * which does not match what ptpd_start("ETH_0") expects. */
  318. esp_netif_inherent_config_t base_cfg = ESP_NETIF_INHERENT_DEFAULT_ETH();
  319. base_cfg.if_key = "ETH_0";
  320. esp_netif_config_t netif_config = {
  321. .base = &base_cfg,
  322. .stack = ESP_NETIF_NETSTACK_DEFAULT_ETH,
  323. };
  324. esp_netif_t *eth_netif = esp_netif_new(&netif_config);
  325. if (!eth_netif) {
  326. ESP_LOGE(TAG, "failed to create ethernet netif");
  327. return ESP_FAIL;
  328. }
  329. /* Attach the driver to the netif via the default glue layer */
  330. esp_eth_netif_glue_handle_t glue = esp_eth_new_netif_glue(eth_handle);
  331. ret = esp_netif_attach(eth_netif, glue);
  332. if (ret != ESP_OK) {
  333. ESP_LOGE(TAG, "failed to attach ethernet to netif: %s", esp_err_to_name(ret));
  334. return ret;
  335. }
  336. /* Register event handlers for link and IP events */
  337. ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID,
  338. &eth_event_handler, NULL));
  339. ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP,
  340. &ip_event_handler, NULL));
  341. /* Ethernet RTP hook: intercept RTP at MAC level before lwIP */
  342. s_original_priv = eth_netif;
  343. /* 192 frames * max channels covers the AES67 practical max (4ms at 48kHz) */
  344. s_hook_tmp32 = heap_caps_malloc(
  345. 192 * AES67_MAX_CHANNELS_PER_STREAM * sizeof(int32_t),
  346. MALLOC_CAP_INTERNAL);
  347. s_hook_i16 = heap_caps_malloc(
  348. 192 * AES67_MAX_CHANNELS_PER_STREAM * sizeof(int16_t),
  349. MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
  350. esp_eth_update_input_path_info(eth_handle, eth_rtp_hook, eth_netif);
  351. ESP_LOGI(TAG, "Ethernet RTP hook installed");
  352. /* Enable Ethernet flow control to prevent frame drops under load.
  353. * Without this, the EMAC drops ~8% of multicast frames at high
  354. * packet rates (1000 pps) because the DMA can't keep up. */
  355. bool flow_ctrl = true;
  356. esp_eth_ioctl(eth_handle, ETH_CMD_S_FLOW_CTRL, &flow_ctrl);
  357. /* Start Ethernet */
  358. ret = esp_eth_start(eth_handle);
  359. if (ret != ESP_OK) {
  360. ESP_LOGE(TAG, "ethernet start failed: %s", esp_err_to_name(ret));
  361. return ret;
  362. }
  363. *out_eth_handle = eth_handle;
  364. return ESP_OK;
  365. }
  366. /* Enable the power amplifier on the ES8311 output stage */
  367. static void pa_ctrl_enable(void)
  368. {
  369. gpio_config_t io_conf = {
  370. .pin_bit_mask = (1ULL << PA_CTRL_GPIO),
  371. .mode = GPIO_MODE_OUTPUT,
  372. .pull_up_en = GPIO_PULLUP_DISABLE,
  373. .pull_down_en = GPIO_PULLDOWN_DISABLE,
  374. .intr_type = GPIO_INTR_DISABLE,
  375. };
  376. gpio_config(&io_conf);
  377. gpio_set_level(PA_CTRL_GPIO, 1);
  378. ESP_LOGI(TAG, "PA enabled (GPIO%d high)", PA_CTRL_GPIO);
  379. }
  380. /*
  381. * Initialize the ES8311 audio codec via I2C.
  382. * Configures it for 48kHz 24-bit I2S operation with both DAC (playback)
  383. * and ADC (capture) enabled.
  384. */
  385. static esp_err_t es8311_codec_init(void)
  386. {
  387. /* Initialize I2C master bus */
  388. i2c_config_t i2c_cfg = {
  389. .mode = I2C_MODE_MASTER,
  390. .sda_io_num = I2C_SDA_GPIO,
  391. .scl_io_num = I2C_SCL_GPIO,
  392. .sda_pullup_en = GPIO_PULLUP_ENABLE,
  393. .scl_pullup_en = GPIO_PULLUP_ENABLE,
  394. .master.clk_speed = I2C_CLK_HZ,
  395. };
  396. esp_err_t ret = i2c_param_config(I2C_PORT, &i2c_cfg);
  397. if (ret != ESP_OK) {
  398. ESP_LOGE(TAG, "I2C param config failed: %s", esp_err_to_name(ret));
  399. return ret;
  400. }
  401. ret = i2c_driver_install(I2C_PORT, I2C_MODE_MASTER, 0, 0, 0);
  402. if (ret != ESP_OK) {
  403. ESP_LOGE(TAG, "I2C driver install failed: %s", esp_err_to_name(ret));
  404. return ret;
  405. }
  406. /* Create ES8311 codec handle */
  407. es8311_handle_t codec = es8311_create(I2C_PORT, ES8311_ADDR);
  408. if (!codec) {
  409. ESP_LOGE(TAG, "ES8311 create failed");
  410. return ESP_FAIL;
  411. }
  412. /* Configure clock: MCLK from MCLK pin, 48kHz sample rate.
  413. * I2S uses MCLK_MULTIPLE_384 (matching official ESP-IDF example).
  414. * MCLK = 48000 * 384 = 18.432 MHz */
  415. es8311_clock_config_t clk_cfg = {
  416. .mclk_inverted = false,
  417. .sclk_inverted = false,
  418. .mclk_from_mclk_pin = true,
  419. .mclk_frequency = 48000 * 384, /* 18.432 MHz */
  420. .sample_frequency = 48000,
  421. };
  422. /* 32-bit resolution -- int32 maps directly, no truncation */
  423. ret = es8311_init(codec, &clk_cfg, ES8311_RESOLUTION_32, ES8311_RESOLUTION_32);
  424. if (ret != ESP_OK) {
  425. ESP_LOGE(TAG, "ES8311 init failed: %s", esp_err_to_name(ret));
  426. return ret;
  427. }
  428. ret = es8311_voice_volume_set(codec, 70, NULL);
  429. if (ret != ESP_OK) {
  430. ESP_LOGW(TAG, "ES8311 volume set failed: %s", esp_err_to_name(ret));
  431. }
  432. /* Unmute DAC output */
  433. ret = es8311_voice_mute(codec, false);
  434. if (ret != ESP_OK) {
  435. ESP_LOGW(TAG, "ES8311 unmute failed: %s", esp_err_to_name(ret));
  436. }
  437. /* Configure microphone: analog mic, 24dB gain */
  438. es8311_microphone_config(codec, false);
  439. es8311_microphone_gain_set(codec, ES8311_MIC_GAIN_24DB);
  440. /* Dump ES8311 register state for debugging */
  441. es8311_register_dump(codec);
  442. ESP_LOGI(TAG, "ES8311 codec initialized (48kHz, 16-bit, MCLK=18.432MHz, vol=100)");
  443. return ESP_OK;
  444. }
  445. void app_main(void)
  446. {
  447. /* Initialize NVS -- required by some ESP-IDF components internally */
  448. esp_err_t ret = nvs_flash_init();
  449. if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
  450. ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
  451. ESP_ERROR_CHECK(nvs_flash_erase());
  452. ret = nvs_flash_init();
  453. }
  454. ESP_ERROR_CHECK(ret);
  455. /* Configure LDO4 to 3.3V for SDIO/SPI power domain.
  456. * Must be done early before any peripherals on this rail. */
  457. esp_ldo_channel_handle_t ldo4 = NULL;
  458. esp_ldo_channel_config_t ldo_cfg = {
  459. .chan_id = 4,
  460. .voltage_mv = 3300,
  461. };
  462. if (esp_ldo_acquire_channel(&ldo_cfg, &ldo4) == ESP_OK) {
  463. ESP_LOGI(TAG, "LDO4 set to 3.3V");
  464. } else {
  465. ESP_LOGW(TAG, "LDO4 config failed (may already be set)");
  466. }
  467. /* Create the default event loop used by Ethernet and AES67 events */
  468. ESP_ERROR_CHECK(esp_event_loop_create_default());
  469. /* Initialize the TCP/IP stack */
  470. ESP_ERROR_CHECK(esp_netif_init());
  471. /* Set up the event group used to synchronize on IP acquisition */
  472. s_eth_event_group = xEventGroupCreate();
  473. /* Bring up Ethernet with the IP101 PHY */
  474. esp_eth_handle_t eth_handle = NULL;
  475. ESP_ERROR_CHECK(ethernet_init(&eth_handle));
  476. /* Block until we have an IP address. The AES67 stack needs network
  477. * connectivity for PTP, RTP multicast and SAP. */
  478. ESP_LOGI(TAG, "waiting for IP address...");
  479. xEventGroupWaitBits(s_eth_event_group, ETH_GOT_IP_BIT,
  480. pdFALSE, pdTRUE, portMAX_DELAY);
  481. /* Enable the power amplifier before starting audio */
  482. pa_ctrl_enable();
  483. /* ES8311 codec init is deferred until AFTER the AES67 I2S driver starts.
  484. * The ES8311 needs MCLK to be running during initialization. */
  485. /* Build the AES67 node configuration with our I2S pins and codec settings */
  486. aes67_config_t aes67_cfg = AES67_CONFIG_DEFAULT();
  487. strlcpy(aes67_cfg.node_name, "ESP32-P4 AES67", sizeof(aes67_cfg.node_name));
  488. aes67_cfg.net.eth_handle = eth_handle;
  489. aes67_cfg.i2s_pins.mck_gpio = I2S_MCLK_GPIO;
  490. aes67_cfg.i2s_pins.bck_gpio = I2S_SCLK_GPIO;
  491. aes67_cfg.i2s_pins.ws_gpio = I2S_LRCK_GPIO;
  492. aes67_cfg.i2s_pins.dout_gpio = I2S_DOUT_GPIO;
  493. aes67_cfg.i2s_pins.din_gpio = I2S_DIN_GPIO;
  494. aes67_cfg.audio.sample_rate = 48000;
  495. aes67_cfg.audio.channels = 2;
  496. aes67_cfg.audio.codec = AES67_CODEC_L24;
  497. aes67_cfg.audio.word_length = 3; /* 24-bit = 3 bytes */
  498. aes67_cfg.audio.packet_time_us = 1000; /* 1ms packet time per AES67 */
  499. /* Initialize the AES67 node */
  500. aes67_node_handle_t node = NULL;
  501. ESP_ERROR_CHECK(aes67_node_init(&aes67_cfg, &node));
  502. /* Create stream buffer BEFORE starting the node. The DMA ISR callback
  503. * reads directly from this buffer, bypassing the playback task.
  504. * 30 packets (~120ms at 4ms ptime) for jitter absorption.
  505. * Trigger=1: ISR always reads whatever is available. */
  506. {
  507. uint32_t pkt_bytes = 192 * aes67_cfg.audio.channels * sizeof(int32_t);
  508. s_hook_sbuf = xStreamBufferCreate(pkt_bytes * 30, 1);
  509. /* Wire the stream buffer to the audio driver's DMA ISR */
  510. aes67_audio_handle_t audio = NULL;
  511. aes67_node_get_audio(node, &audio);
  512. aes67_audio_set_isr_stream_buf(audio, s_hook_sbuf);
  513. /* Also register it as the node's playout source for the (non-ISR)
  514. * playback-task fallback path. */
  515. aes67_node_set_playout_buf(node, s_hook_sbuf);
  516. /* Enable I2S RX capture for source (TX) streams */
  517. aes67_audio_init_capture(audio);
  518. s_hook_tx_chan = aes67_audio_get_tx_chan(audio);
  519. ESP_LOGI(TAG, "Ethernet hook -> stream buffer -> DMA ISR (zero-overhead)");
  520. ESP_LOGI(TAG, "I2S RX capture -> stream buffer -> RTP TX (ISR-driven)");
  521. }
  522. /* Start PTP sync, RTP engine, SAP announcements and audio I/O.
  523. * This enables I2S which starts MCLK generation. The DMA ISR
  524. * callback is registered during audio start. */
  525. ESP_ERROR_CHECK(aes67_node_start(node));
  526. /* Initialize ES8311 codec AFTER I2S is running (MCLK must be active).
  527. * This is the critical init order discovered through testing. */
  528. esp_err_t codec_ret = es8311_codec_init();
  529. if (codec_ret != ESP_OK) {
  530. ESP_LOGE(TAG, "ES8311 codec init FAILED: %s", esp_err_to_name(codec_ret));
  531. } else {
  532. ESP_LOGI(TAG, "ES8311 codec initialized (MCLK running)");
  533. }
  534. /* Obtain the session manager handle from the node so we can add streams */
  535. aes67_session_handle_t session = NULL;
  536. ESP_ERROR_CHECK(aes67_node_get_session(node, &session));
  537. s_session = session;
  538. /* Register SAP callback to auto-subscribe to discovered sources.
  539. * This will add a sink for the first remote source found (e.g. SIENNA). */
  540. aes67_sap_handle_t sap = NULL;
  541. /* Get SAP handle from the node config - it's stored internally.
  542. * For now, register via the session manager's SAP handle. */
  543. if (aes67_node_get_sap(node, &sap) == ESP_OK && sap) {
  544. aes67_sap_register_cb(sap, sap_discovery_cb, NULL);
  545. ESP_LOGI(TAG, "SAP auto-subscribe enabled");
  546. /* Check if any sources were already discovered before callback
  547. * was registered (SAP starts during node_start). */
  548. int remote_count = aes67_sap_get_remote_count(sap);
  549. for (int i = 0; i < remote_count && !s_sink_added; i++) {
  550. aes67_sap_remote_source_t remote;
  551. if (aes67_sap_get_remote(sap, i, &remote) == ESP_OK) {
  552. sap_discovery_cb(true, &remote, NULL);
  553. }
  554. }
  555. }
  556. /* Source stream is created in the SAP callback to match the
  557. * discovered sink's codec and channel count. */
  558. /* Start the WebUI (HTTP + WebSocket) on port 80 */
  559. aes67_webui_handle_t webui = NULL;
  560. if (aes67_webui_init(node, 80, &webui) == ESP_OK) {
  561. aes67_webui_start(webui);
  562. }
  563. ESP_LOGI(TAG, "node ready");
  564. #if CONFIG_DANTE_DISCOVERY_ON_BOOT
  565. /* Read-only background browse for Dante devices on the network. Logs what
  566. * it finds (devices, TX channels, multicast bundles); does not advertise
  567. * this node. Useful for AES67 bring-up — see components/dante. */
  568. dante_discovery_start(CONFIG_DANTE_DISCOVERY_INTERVAL_MS, NULL, NULL);
  569. #endif
  570. #if CONFIG_DANTE_ADVERTISE_ON_BOOT
  571. /* Build a Dante identity from this board's MAC + IP and advertise it over
  572. * mDNS. PROTOTYPE — only meaningful once the CMC/ARC responders exist. */
  573. {
  574. static dante_identity_t s_dante_id;
  575. uint8_t mac[6] = {0};
  576. esp_read_mac(mac, ESP_MAC_ETH);
  577. esp_netif_ip_info_t ip_info = {0};
  578. esp_netif_t *dante_netif = esp_netif_get_handle_from_ifkey("ETH_0");
  579. if (dante_netif) {
  580. esp_netif_get_ip_info(dante_netif, &ip_info);
  581. }
  582. uint32_t ipv4 =
  583. ((uint32_t)esp_ip4_addr1_16(&ip_info.ip) << 24) |
  584. ((uint32_t)esp_ip4_addr2_16(&ip_info.ip) << 16) |
  585. ((uint32_t)esp_ip4_addr3_16(&ip_info.ip) << 8) |
  586. ((uint32_t)esp_ip4_addr4_16(&ip_info.ip));
  587. dante_identity_init_default(&s_dante_id, ipv4, mac);
  588. dante_advertise_start(&s_dante_id);
  589. }
  590. #endif
  591. /* Monitor loop: ISR health only */
  592. extern void aes67_audio_log_isr_stats(void);
  593. while (1) {
  594. vTaskDelay(pdMS_TO_TICKS(2000));
  595. aes67_audio_log_isr_stats();
  596. }
  597. }