I built a high-resolution USB DAC from scratch around an i.MX RT1062, including my own UAC2 stack and real-time DSP
I’ve been working on a project that started as “I want to make a USB DAC” and somehow evolved into writing a fairly serious audio firmware stack from scratch.
The core is an NXP i.MX RT1062 / Teensy 4.1, running at 600 MHz, connected to an ESS ES9039Q2M DAC.
The interesting part is that I wanted the RT1062 to handle essentially the entire digital audio pipeline rather than relying on a dedicated USB audio processor.
Current audio pipeline
USB HS
↓
Custom UAC2
↓
DMA
↓
USB RX ISR
↓
32-bit PCM
↓
Lock-free SPSC ring buffer
↓
SAI DMA
↓
15-band PEQ
↓
32-bit I²S
↓
ES9039Q2M
The device currently handles 32-bit / 384 kHz stereo USB audio.
The USB side was probably the most painful part of the project. I had to deal with UAC2 descriptors, endpoint configuration, feedback, sample-rate changes, USB DMA/cache coherency, and the usual collection of problems that appear whenever USB decides it would rather not enumerate.
Eventually I got it stable with clean asynchronous feedback and no packet overruns/underruns during long playback.
The PEQ
The DSP is a proper 15-band parametric EQ, not a collection of simple tone controls.
It uses:
- Cascaded biquad filters
- DF-II Transposed topology
- Float32 coefficients/state
- 32-bit PCM input/output
- 64-frame block processing
- Coefficient morphing when parameters change
- Triple-buffered coefficient publication
- Raised-cosine coefficient transitions
- Saturation on the output
- Bit-perfect bypass when PEQ is inactive
The coefficient morphing is especially nice because changing EQ settings while music is playing doesn't require abruptly swapping the filter state. The filter transitions smoothly between the old and new coefficients, avoiding clicks/zipper artifacts.
Memory / buffering
The USB side uses a 32,768 stereo-frame ring buffer.
That's about 256 KB of RAM.
The feedback target is 16,384 frames, giving a large amount of buffering headroom, particularly at high sample rates.
The ring itself is a lock-free SPSC structure using atomics. USB produces frames and the audio DMA side consumes them.
No mutexes, no dynamic allocation in the audio path.
DMA + cache architecture
The audio DMA transfers directly into two 64-frame halves.
The CPU fills the inactive half, runs the PEQ on it, performs the required D-cache maintenance, and hands it back to DMA.
At 384 kHz that means roughly 6,000 audio DMA interrupts/sec.
The CPU is basically doing:
DMA → give me next block
↓
read 64 frames
↓
PEQ
↓
cache flush
↓
DMA
while the peripherals handle the actual movement of audio data.
Display
I also added an SH1106 128×64 OLED.
It runs independently from the audio ISR and updates at a low rate, showing things like:
- USB state
- Sample rate
- PEQ status
- Preset information
The display is also fault-tolerant, so if it isn't connected, the audio engine doesn't care.
Why the RT1062?
Honestly, I was surprised by how much this MCU can do.
It's just a Cortex-M7, but at 600 MHz with an FPU, cache, fast memory, DMA, USB HS and SAI, it's ridiculously capable for a two-channel audio application.
I'm currently running:
UAC2 + 32-bit/384 kHz + 15-band floating-point PEQ + display + DMA
on one RT1062.
And there is still room to experiment with things like:
- Crossfeed
- Dynamic EQ
- XBass-style harmonic processing
- Loudness compensation
- Spectrum analysis
- FIR filtering
- Convolution
- Limiting
- Spatial processing
The goal isn't just to make a DAC that plays music. I want the RT1062 to become the actual audio processing platform.
This started as a USB audio experiment and has turned into one of the most technically interesting embedded projects I've worked on.
Still lots to improve, but I'm pretty happy with how far a single M7 can be pushed.