Recovery and restoration service:frontea online,corp.

Recovery and restoration service:frontea online,corp.

Deep Abyss Audio (11) Core’2 — Telemetry Downlink Receiver (TDR Phase) Pro-Audio Specification USB DAC (DIY / ESP32‑S3)

‹ 2026/01/05 ›

Good evening.

In Episode 11, we finally enter the explanation of PopoDAC Core’2 — the TDR Phase (Telemetry Downlink Receiver, formerly “UAC Phase”).


At this point…

Have you started feeling a bit more interested in PopoDAC?


(…What? Not at all? That hurts… 😅)



TDR Phase Overview

The TDR Phase (Telemetry Downlink Receiver) is responsible for receiving audio data from the USB-UAC Output Endpoint and injecting it into the MASTER RING.


Its activation origin is the AUDIO SOF ISR.


Module Structure

The TDR Phase consists of only one module:

Module 5 — Data Telemetry Receiver

A simple, single-purpose module.


tud_audio_rx_done_isr

The internal logic is also simple:

relentlessly push received data into the MASTER RING.

bool IRAM_ATTR tud_audio_rx_done_isr(uint8_t rhport, uint16_t n_bytes_received, uint8_t func_id, uint8_t ep_out, uint8_t cur_alt_setting)

{

    size_t got = 0;


    // Safety for unexpected 0-byte packets (should not happen under SOF rules)

    if (n_bytes_received > 0) {

        got = tud_audio_n_read(0, g_pcm_buf, n_bytes_received>(g_ddc.uac_quality.frame_size<<UAC_BUFFER_STROKESIZE)?(g_ddc.uac_quality.frame_size<<UAC_BUFFER_STROKESIZE):n_bytes_received);

        if (got > 0) {

            // Do not write empty packets into the ring buffer

            rb_write(&g_audio_ring, g_pcm_buf, got);

        }

    }


    g_ddc.ddc_rx_size = got;


#if DDC_LOG == 1

    static usb_packet_log_t uac_log;

    uac_log.nsamples = got;

    uac_log.ring_level = rb_available(&g_audio_ring);

    usb_packet_log_write(&uac_log);

#endif

    return true;

}


The job here is singular:

focus entirely on receiving data.


No complex calculations.

No floating‑point operations.

No conditional branching based on UAC behavior.

No negotiation with the USB host.


Just receive.

Why?

Because this runs inside the AUDIO SOF ISR.


“But there’s an IF statement in there!”

Yes, there is.


Once we fully confirm behavior, even that can be removed.

(…Though it’s scary to remove safety checks 😣)


As explained earlier, within SOF-synchronized operation, the USB specification guarantees that packets are always sent with the correct size.


Therefore:

  • unnecessary branching should be removed
  • fewer branches → more stable ISR execution time
  • more stable ISR time → higher “time purity”

And of course:

LOCK or WAIT inside this ISR is absolutely forbidden.


Why PopoDAC avoids FreeRTOS Queue, RingBuffer with locks, etc.

Anything that uses internal locks risks synchronization delays.

Even a rare WAIT would contaminate the time axis.


To avoid this entirely, PopoDAC uses:

  • MASTER RING
  • specifically its Lock-Free Ring Buffer for RX-side ingestion

This ensures that the TDR Phase never blocks.


“Should we use tud_audio_n_available?”

You may have seen examples where tud_audio_n_available() is used inside a loop to check incoming data.

You might wonder:

“When should I use this?”


In PopoDAC:

We never use it.

Anyone who has used it in embedded systems already knows:

  • It is only meaningful when operating asynchronously with respect to AUDIO SOF ISR.

But PopoDAC performs all reception synchronously inside the SOF ISR.


Therefore:

  • No need to check USB-DMA water levels
  • No need to poll availability
  • No need for asynchronous logic

If the USB driver is calling AUDIO SOF ISR, it is already telling us:

“Data has arrived. Process it immediately.”


So PopoDAC’s correct behavior is:

When data arrives, process it right now.

(…No hesitation 😅)


ESP32‑S3 USB‑DMA Landmine

As a small aside—

Even in PopoDAC, the USB side is not using DMA transfers.

It is using FIFO.


“Wait, why in 2026 are we still doing that?”

Because… DMA goes berserk.


At the time of writing, with the current IDF version, enabling DMA in TinyUSB leads to serious issues.

This is no longer a software-level quirk; it behaves like an I/O‑level hazard.

Until both IDF and TinyUSB reach a safe implementation line, DMA simply cannot be enabled.


(…It might even be a hardware‑level limitation depending on board design.)


So we retreat.

Comment it out.

Fall back to FIFO.

And everything becomes peaceful again.


//#define CFG_TUD_DWC2_DMA_ENABLE      1

(Note: This is a limitation of the current IDF/TinyUSB implementation and may be improved in future versions.)


Next Episode

Next week, we move on to Core’3 — Timer Phase: GNC (Guidance & Navigation Control).

Please look forward to it.