Project 13: DMA-Driven Audio Player

Build a WAV file player that uses DMA to stream audio from SD card to DAC with zero CPU intervention during playback.


Quick Reference

Attribute Value
Language C (alt: Rust)
Difficulty Expert
Time 2-3 weeks
Prerequisites Projects 10-11 (I2C/SPI), DMA basics
Key Topics DMA, DAC, Double Buffering, Audio
Coolness Level 4: Hardcore Tech Flex
Portfolio Value Micro-SaaS / Pro Tool
Hardware STM32 with DAC + speaker/headphones

Learning Objectives

By completing this project, you will:

  1. Configure DMA controllers: Understand peripheral-to-memory and memory-to-peripheral transfers
  2. Implement double buffering: Prevent audio glitches by filling one buffer while playing another
  3. Configure DAC peripherals: Set up timer-triggered DAC updates for precise sample rates
  4. Parse audio file formats: Read and interpret WAV file headers to extract audio data
  5. Calculate timing requirements: Determine timer frequencies for accurate sample rate reproduction
  6. Integrate multiple peripherals: Combine SD card reading, DMA, timers, and DAC into a cohesive system

The Core Question You’re Answering

“How do embedded systems achieve real-time audio playback without the CPU being a bottleneck, and what role does DMA play in high-performance peripheral data transfer?”

This question forces you to understand that in embedded systems, the CPU cannot be tied up moving data byte-by-byte for time-critical operations. DMA (Direct Memory Access) allows peripherals to read and write memory directly, freeing the CPU for other tasks while maintaining deterministic timing for real-time applications like audio.


Concepts You Must Understand First

Before starting this project, ensure you understand these concepts:

Concept Why It Matters Where to Learn
Basic DMA operation You’ll configure DMA channels for automatic data transfer STM32 Reference Manual DMA chapter
Timer peripheral basics Timers will trigger DAC conversions at precise intervals “Making Embedded Systems” Ch. 5
Digital-to-Analog conversion DAC output creates the analog audio signal STM32 Reference Manual DAC chapter
SPI and SD card communication You’ll read audio data from SD card Project 11 (SPI SD Card)
Interrupt handling Half/Full transfer interrupts signal buffer status Project 4 (UART Driver)
Audio sampling theory Sample rate and bit depth determine audio quality Any DSP or audio textbook

Key Concepts Deep Dive

  1. DMA (Direct Memory Access)
    • What triggers a DMA transfer and how is the transfer size specified?
    • What is the difference between circular and normal DMA modes?
    • What are half-transfer and transfer-complete interrupts, and why are both needed?
    • How does DMA arbitration work when multiple channels request the bus?
    • STM32 Reference Manual DMA Chapter
  2. Double Buffering
    • Why is a single buffer insufficient for continuous audio playback?
    • How do you coordinate buffer filling with buffer playing?
    • What determines the optimal buffer size (too small = too many interrupts, too large = latency)?
    • “Game Programming Patterns” Chapter 8 - Robert Nystrom
  3. DAC Configuration
    • How does the DAC convert digital values to analog voltage?
    • What determines the voltage range of the DAC output?
    • Why is timer-triggered mode preferable to software-triggered for audio?
    • How do you handle 16-bit audio on a 12-bit DAC?
    • STM32 Reference Manual DAC Chapter
  4. WAV File Format
    • What information does the WAV header contain (channels, sample rate, bit depth)?
    • How is PCM audio data stored (signed vs unsigned, interleaving)?
    • How do you handle stereo audio on a mono DAC?
    • RIFF/WAV Specification
  5. Real-Time Audio Constraints
    • What happens if the CPU cannot fill buffers fast enough (underrun)?
    • How do you calculate CPU utilization for audio processing?
    • What is audio latency and why does buffer size affect it?
    • “Making Embedded Systems” Chapter 7

Theoretical Foundation

DMA: The CPU’s Assistant

In traditional programming, the CPU is responsible for all data movement:

Traditional (Polling) Approach:
┌─────────────────────────────────────────────────────┐
│  for (int i = 0; i < SAMPLES; i++) {                │
│      while (!(DAC->SR & DAC_SR_DMAUDR));  // Wait   │
│      DAC->DHR12R1 = audio_buffer[i];      // Write  │
│  }                                                  │
│  CPU is 100% busy just moving data!                 │
└─────────────────────────────────────────────────────┘

With DMA, the hardware moves data automatically:

DMA Approach:
┌─────────────────────────────────────────────────────┐
│                                                     │
│  DMA Controller                                     │
│  ┌─────────────┐    Timer Trigger    ┌───────────┐ │
│  │   Memory    │ ──────────────────▶ │    DAC    │ │
│  │   Buffer    │      (No CPU!)       │  Output   │ │
│  └─────────────┘                      └───────────┘ │
│                                                     │
│  CPU is free to:                                    │
│  - Read next audio chunk from SD card               │
│  - Process button inputs                            │
│  - Update display                                   │
│  - Sleep to save power                              │
└─────────────────────────────────────────────────────┘

DMA Configuration Elements

A DMA transfer requires configuring several registers:

DMA Configuration:
┌─────────────────────────────────────────────────────┐
│  Source Address (CMAR)        │  Memory buffer      │
│  Destination Address (CPAR)   │  Peripheral register│
│  Transfer Count (CNDTR)       │  Number of items    │
│  Direction (CCR.DIR)          │  Memory→Peripheral  │
│  Data Size (CCR.MSIZE/PSIZE)  │  8/16/32 bits       │
│  Increment Mode (CCR.MINC)    │  Auto-increment src │
│  Circular Mode (CCR.CIRC)     │  Restart at end     │
│  Interrupts (CCR.HTIE/TCIE)   │  Half/Full complete │
└─────────────────────────────────────────────────────┘

Double Buffering Strategy

The key to glitch-free audio is overlapping “playing” and “filling”:

Time ──────────────────────────────────────────────────────────────▶

Buffer Layout:
┌─────────────────────────────────────────────────────────────────┐
│   [    First Half    ]    [    Second Half   ]                  │
└─────────────────────────────────────────────────────────────────┘

Phase 1: DMA playing First Half
┌─────────────────────────────────────────────────────────────────┐
│   [▶▶▶▶▶DMA▶▶▶▶▶▶▶▶▶]    [  CPU fills here  ]                  │
└─────────────────────────────────────────────────────────────────┘
    ↑ HT interrupt fires when DMA reaches middle

Phase 2: DMA playing Second Half
┌─────────────────────────────────────────────────────────────────┐
│   [  CPU fills here  ]    [▶▶▶▶▶DMA▶▶▶▶▶▶▶▶▶]                  │
└─────────────────────────────────────────────────────────────────┘
                                ↑ TC interrupt fires when DMA wraps

Circular Mode: DMA automatically returns to start

Timer-Triggered DAC Updates

For accurate sample rates, a timer triggers each DAC conversion:

Timer-DAC Relationship:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│  Timer Clock (e.g., 72 MHz)                                     │
│       │                                                         │
│       ▼                                                         │
│  ┌─────────────────────┐                                        │
│  │   TIM6 Counter      │  Counts up to ARR, then resets         │
│  │   ARR = 1632        │  (72MHz / 44100Hz ≈ 1633)              │
│  └─────────────────────┘                                        │
│       │ TRGO (trigger output) on update event                   │
│       ▼                                                         │
│  ┌─────────────────────┐                                        │
│  │       DAC           │  Loads next value from DHR on trigger  │
│  │   TSEL = TIM6       │                                        │
│  └─────────────────────┘                                        │
│       │ DAC request                                             │
│       ▼                                                         │
│  ┌─────────────────────┐                                        │
│  │       DMA           │  Transfers next sample to DAC DHR      │
│  │   Channel 3         │                                        │
│  └─────────────────────┘                                        │
│                                                                 │
│  Result: Precisely 44,100 samples/second converted              │
└─────────────────────────────────────────────────────────────────┘

Sample Rate Calculation

For a given sample rate, calculate the timer period:

Timer Period = (Timer Clock Frequency / Sample Rate) - 1

Example: 44.1 kHz sample rate with 72 MHz timer clock
  Period = (72,000,000 / 44,100) - 1
         = 1632.65... ≈ 1632

  Actual sample rate = 72,000,000 / (1632 + 1) = 44,117 Hz
  Error = (44,117 - 44,100) / 44,100 = 0.04% (acceptable)

WAV File Structure

WAV files follow the RIFF format:

WAV File Layout:
┌─────────────────────────────────────────────────────┐
│  Offset  │  Size  │  Field          │  Example     │
├─────────────────────────────────────────────────────┤
│  0       │  4     │  "RIFF"         │  0x52494646  │
│  4       │  4     │  File size - 8  │  Size value  │
│  8       │  4     │  "WAVE"         │  0x57415645  │
├─────────────────────────────────────────────────────┤
│  12      │  4     │  "fmt "         │  0x666D7420  │
│  16      │  4     │  Subchunk size  │  16          │
│  20      │  2     │  Audio format   │  1 (PCM)     │
│  22      │  2     │  Num channels   │  2 (stereo)  │
│  24      │  4     │  Sample rate    │  44100       │
│  28      │  4     │  Byte rate      │  176400      │
│  32      │  2     │  Block align    │  4           │
│  34      │  2     │  Bits/sample    │  16          │
├─────────────────────────────────────────────────────┤
│  36      │  4     │  "data"         │  0x64617461  │
│  40      │  4     │  Data size      │  Size value  │
│  44      │  N     │  Audio samples  │  PCM data    │
└─────────────────────────────────────────────────────┘

Handling 16-bit Audio on 12-bit DAC

Most STM32 DACs are 12-bit (0-4095), but CD-quality audio is 16-bit:

16-bit to 12-bit Conversion:
┌─────────────────────────────────────────────────────┐
│                                                     │
│  16-bit signed sample: -32768 to +32767             │
│                                                     │
│  Step 1: Convert to unsigned                        │
│          unsigned = signed + 32768                  │
│          Range: 0 to 65535                          │
│                                                     │
│  Step 2: Scale to 12 bits                           │
│          12bit = unsigned >> 4                      │
│          Range: 0 to 4095                           │
│                                                     │
│  Alternative (better quality):                      │
│          12bit = (unsigned * 4095) / 65535          │
│                                                     │
└─────────────────────────────────────────────────────┘

Common Misconceptions

Misconception 1: “DMA is complicated and only for advanced use cases” Reality: DMA simplifies code by removing data movement loops. The configuration is more complex, but the application code becomes simpler and more efficient.

Misconception 2: “Double buffering wastes memory” Reality: The alternative is audio glitches. Memory is cheap; real-time correctness is valuable.

Misconception 3: “I can use software timing for audio” Reality: Software delays are interrupted by other interrupts. Timer-triggered conversions maintain precise timing regardless of other system activity.

Misconception 4: “A 12-bit DAC can’t play quality audio” Reality: 12 bits provides 72 dB dynamic range, sufficient for many applications. Professional audio uses 16-24 bits, but 12 bits is adequate for notifications, music playback, and voice.


Project Specification

What You Will Build

A complete audio playback system that reads WAV files from an SD card and plays them through a DAC with:

  • Continuous playback without glitches
  • Volume control
  • Play/pause functionality
  • Display of playback progress
  • Support for various sample rates

Functional Requirements

  1. WAV File Parsing:
    • Read and validate WAV headers
    • Support PCM format (format code 1)
    • Handle mono and stereo (mix to mono)
    • Support 8-bit and 16-bit samples
    • Support 8kHz to 48kHz sample rates
  2. DMA Audio Streaming:
    • Configure DMA in circular mode
    • Implement double-buffering with half/full interrupts
    • Timer-triggered DAC updates
    • Zero CPU intervention during playback
  3. Playback Control:
    • Play/pause functionality
    • Stop and return to beginning
    • Volume control (software scaling)
    • Skip to next file
  4. User Interface:
    • Serial console for commands
    • Display current file name
    • Show playback progress (percentage, time)
    • Display audio parameters (sample rate, channels, bits)
  5. Error Handling:
    • Graceful handling of corrupt files
    • SD card removal detection
    • Buffer underrun detection and recovery

Non-Functional Requirements

  • Performance: Less than 10% CPU usage during playback
  • Latency: Buffer size allowing <100ms audio latency
  • Quality: No audible glitches during normal operation
  • Reliability: Stable playback for files up to 1 hour

Real World Outcome

When complete, your audio player will produce output like this:

=== DMA Audio Player ===

SD card mounted, FAT32
DMA: Circular mode, half/full interrupts
DAC: 12-bit, TIM6 triggered

Loading: music.wav
  Format: PCM
  Channels: 2 (stereo, mixing to mono)
  Sample rate: 44100 Hz
  Bits per sample: 16

Configuring TIM6 for 44.1kHz...
  Timer clock: 72MHz
  Prescaler: 0
  Period: 1632 (actual: 44117 Hz, 0.04% error)

Playing... [=====>              ] 25%
  Buffer: 2048 samples, double-buffered
  DMA interrupts: 1247 (half), 1247 (full)
  CPU usage: 3% (mostly SD reads)

Press 'p' to pause, 's' to stop, '+/-' for volume

> stats
Audio Statistics:
  Samples played: 2,646,000
  Play time: 60.0 sec / 240.0 sec
  DMA underruns: 0
  Buffer fills: 2,494
  Avg fill time: 0.8ms
  Max fill time: 2.1ms

> vol+
Volume: 80%

> list
Files on SD card:
  1. music.wav      (10.1 MB, 3:59)
  2. voice.wav      (1.2 MB, 0:45)
  3. beep.wav       (0.1 MB, 0:02)

> play 2
Loading: voice.wav
  Format: PCM
  Channels: 1 (mono)
  Sample rate: 22050 Hz
  Bits per sample: 8

Playing...

Physical result: Audio plays through speaker/headphones clearly!

Questions to Guide Your Design

Work through these questions BEFORE writing code:

  1. Buffer Sizing: How do you choose the buffer size? What happens if it’s too small? Too large?

  2. DMA Configuration: Which DMA channel connects to your DAC? How do you configure circular mode with interrupts?

  3. Timer Selection: Which timer can trigger your DAC? How do you calculate the period for different sample rates?

  4. Data Flow: How does data flow from SD card to DAC? What are the bottlenecks?

  5. Stereo to Mono: How do you mix stereo to mono? Simple average or weighted?

  6. Volume Control: Do you scale samples before putting them in the buffer, or during the half-transfer interrupt?

  7. File Navigation: How do you track position in the file for pause/resume?

  8. Error Recovery: What do you do if a buffer underrun occurs?


Thinking Exercise

Before writing any code, trace through the data flow:

  1. Startup Sequence: List every step from power-on to first audio sample output:
    • Clock configuration
    • GPIO setup
    • DMA initialization
    • Timer initialization
    • DAC initialization
    • SD card mounting
    • File opening
    • Initial buffer filling
    • Starting playback
  2. Interrupt Analysis: For 44.1 kHz audio with a 2048-sample buffer:
    • How many DMA half-transfer interrupts per second?
    • How many bytes must be read from SD card per interrupt?
    • How long do you have to fill the buffer before underrun?
  3. Timing Budget: Calculate the time budget for your interrupt handler:
    • Time between HT interrupts = 1024 samples / 44100 = 23.2 ms
    • SD card sector read time (estimate): 1-5 ms per sector
    • Number of sectors needed: 2048 bytes / 512 = 4 sectors
    • Total read time estimate: 4-20 ms
    • Margin for processing: 3-19 ms

Solution Architecture

High-Level Design

┌─────────────────────────────────────────────────────────────────┐
│                     DMA Audio Player                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌────────────┐    ┌────────────┐    ┌────────────┐            │
│  │   SD Card  │───▶│   Buffer   │───▶│    DAC     │───▶ Audio  │
│  │   (SPI)    │    │   Manager  │    │   (DMA)    │     Out    │
│  └────────────┘    └────────────┘    └────────────┘            │
│        │                │                  │                    │
│        │           ┌────┴────┐            │                    │
│        │           │  Audio  │            │                    │
│        └──────────▶│ Decoder │◀───────────┘                    │
│                    └─────────┘                                 │
│                         │                                      │
│                    ┌────┴────┐                                 │
│                    │   CLI   │                                 │
│                    │ Handler │                                 │
│                    └─────────┘                                 │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

System Components

Component Diagram:
┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│   SD Card                Memory              DAC                │
│  ┌───────┐           ┌──────────┐        ┌───────┐             │
│  │ FAT32 │──SPI───▶  │ Buffer   │──DMA──▶│ DHR   │───▶ Audio   │
│  │ File  │           │ [2][1024]│        │ 12bit │    Output   │
│  └───────┘           └──────────┘        └───────┘             │
│                           │                  ▲                  │
│                           │            TIM6  │                  │
│                      HT/TC│            TRGO  │                  │
│                      IRQs │                  │                  │
│                           ▼              ┌───┴───┐              │
│                      ┌────────┐          │ TIM6  │              │
│                      │  ISR   │          │ 44kHz │              │
│                      └────────┘          └───────┘              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Data Structures

// WAV file header structure
typedef struct {
    char     riff[4];        // "RIFF"
    uint32_t file_size;      // File size - 8
    char     wave[4];        // "WAVE"
    char     fmt[4];         // "fmt "
    uint32_t fmt_size;       // Format chunk size
    uint16_t audio_format;   // 1 = PCM
    uint16_t num_channels;   // 1 = mono, 2 = stereo
    uint32_t sample_rate;    // 44100, 22050, etc.
    uint32_t byte_rate;      // sample_rate * block_align
    uint16_t block_align;    // channels * bits/8
    uint16_t bits_per_sample;// 8 or 16
    char     data[4];        // "data"
    uint32_t data_size;      // Audio data size
} WavHeader;

// Audio player state
typedef struct {
    // File info
    FIL         file;
    WavHeader   header;
    uint32_t    data_start;      // Offset where audio data begins
    uint32_t    data_remaining;  // Bytes left to play

    // Buffer management
    uint16_t    buffer[2][BUFFER_SIZE];  // Double buffer
    volatile uint8_t  filling_buffer;    // Which half to fill
    volatile uint8_t  buffer_ready[2];   // Buffer status

    // Playback state
    volatile uint8_t  playing;
    volatile uint8_t  paused;
    uint8_t     volume;          // 0-100

    // Statistics
    uint32_t    samples_played;
    uint32_t    buffer_fills;
    uint32_t    underruns;
} AudioPlayer;

// DMA configuration
typedef struct {
    DMA_Channel_TypeDef *channel;
    uint32_t            priority;
    uint32_t            direction;
    uint32_t            mode;       // Circular
    uint32_t            periph_inc; // No
    uint32_t            mem_inc;    // Yes
    uint32_t            periph_size;// 16-bit
    uint32_t            mem_size;   // 16-bit
} DMA_Config;

State Machine

Audio Player State Machine:

         ┌──────────────────────────────────────┐
         │                                      │
         ▼                                      │
    ┌─────────┐    open()    ┌─────────┐       │
    │  IDLE   │─────────────▶│ LOADED  │       │
    └─────────┘              └─────────┘       │
         ▲                        │            │
         │                   play()            │
         │                        │            │
         │                        ▼            │
    ┌─────────┐  stop()     ┌─────────┐       │
    │ STOPPED │◀────────────│ PLAYING │       │
    └─────────┘             └─────────┘       │
                                 │  ▲          │
                           pause()  resume()   │
                                 │  │          │
                                 ▼  │          │
                            ┌─────────┐        │
                            │ PAUSED  │        │
                            └─────────┘        │
                                 │             │
                            stop()             │
                                 │             │
                                 └─────────────┘

Implementation Guide

Development Environment Setup

# Required tools
sudo apt-get install gcc-arm-none-eabi openocd stlink-tools

# Verify ARM toolchain
arm-none-eabi-gcc --version

# Project structure
mkdir -p dma-audio/{src,inc,fatfs}
cd dma-audio

Project Structure

dma-audio/
├── src/
│   ├── main.c              # Entry point, CLI
│   ├── audio_player.c      # Audio playback logic
│   ├── wav_parser.c        # WAV file parsing
│   ├── dma_dac.c           # DMA and DAC configuration
│   ├── sd_card.c           # SD card interface
│   ├── timer.c             # Timer configuration
│   └── system.c            # Clock and startup
├── inc/
│   ├── audio_player.h
│   ├── wav_parser.h
│   ├── dma_dac.h
│   ├── sd_card.h
│   └── stm32f4xx.h
├── fatfs/                  # FatFs library
├── linker.ld
├── startup.s
└── Makefile

Implementation Phases

Phase 1: DAC Output (Days 1-3)

Goals:

  • Configure DAC peripheral
  • Output a test tone using software triggering
  • Verify audio output through speaker

Tasks:

  1. Configure GPIO for DAC output (PA4 or PA5)
  2. Enable DAC clock and configure control register
  3. Write a simple loop to output a sine wave
  4. Test with oscilloscope or speaker

Checkpoint: A 440 Hz tone plays through the speaker.

// Test: Software-triggered DAC sine wave
void test_dac_sine(void) {
    const uint16_t sine_table[32] = { /* 32 samples of sine */ };
    int i = 0;
    while (1) {
        DAC->DHR12R1 = sine_table[i];
        i = (i + 1) % 32;
        delay_us(23);  // ~44kHz
    }
}

Phase 2: Timer-Triggered DAC (Days 4-6)

Goals:

  • Configure timer for precise triggering
  • Connect timer to DAC trigger
  • Achieve exact sample rate

Tasks:

  1. Configure TIM6 for 44.1 kHz update events
  2. Set up DAC to trigger from TIM6 TRGO
  3. Verify sample timing with oscilloscope
  4. Test different sample rates

Checkpoint: Sine wave plays at precisely 440 Hz regardless of other code execution.

// Configure TIM6 for 44.1 kHz
void timer_init(uint32_t sample_rate) {
    RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;

    uint32_t timer_clock = 72000000;  // APB1 timer clock
    uint32_t period = (timer_clock / sample_rate) - 1;

    TIM6->PSC = 0;           // No prescaler
    TIM6->ARR = period;      // Auto-reload value
    TIM6->CR2 = TIM_CR2_MMS_1;  // TRGO on update
    TIM6->CR1 = TIM_CR1_CEN;    // Enable timer
}

Phase 3: DMA Configuration (Days 7-10)

Goals:

  • Configure DMA for memory-to-peripheral transfer
  • Implement circular mode
  • Set up half and complete transfer interrupts

Tasks:

  1. Enable DMA clock and configure channel
  2. Set source (memory), destination (DAC DHR)
  3. Enable circular mode
  4. Implement interrupt handlers for HT and TC
  5. Test with static buffer playing continuously

Checkpoint: A buffer of audio data plays continuously in a loop with no CPU intervention.

// Configure DMA for DAC
void dma_init(uint16_t *buffer, uint32_t size) {
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;

    DMA1_Channel3->CCR = 0;  // Reset
    DMA1_Channel3->CPAR = (uint32_t)&DAC->DHR12R1;
    DMA1_Channel3->CMAR = (uint32_t)buffer;
    DMA1_Channel3->CNDTR = size;

    DMA1_Channel3->CCR =
        DMA_CCR_MINC |      // Memory increment
        DMA_CCR_CIRC |      // Circular mode
        DMA_CCR_DIR |       // Memory to peripheral
        DMA_CCR_MSIZE_0 |   // 16-bit memory
        DMA_CCR_PSIZE_0 |   // 16-bit peripheral
        DMA_CCR_HTIE |      // Half-transfer interrupt
        DMA_CCR_TCIE;       // Transfer complete interrupt

    NVIC_EnableIRQ(DMA1_Channel3_IRQn);
    DMA1_Channel3->CCR |= DMA_CCR_EN;
}

void DMA1_Channel3_IRQHandler(void) {
    if (DMA1->ISR & DMA_ISR_HTIF3) {
        DMA1->IFCR = DMA_IFCR_CHTIF3;
        // Fill first half of buffer
        buffer_half_empty = 0;
    }
    if (DMA1->ISR & DMA_ISR_TCIF3) {
        DMA1->IFCR = DMA_IFCR_CTCIF3;
        // Fill second half of buffer
        buffer_half_empty = 1;
    }
}

Phase 4: SD Card Integration (Days 11-14)

Goals:

  • Mount FAT filesystem on SD card
  • Read WAV files
  • Stream data to audio buffer

Tasks:

  1. Integrate FatFs library
  2. Implement WAV header parsing
  3. Create buffer filling function
  4. Test with real audio file

Checkpoint: A WAV file from SD card plays through the speaker.

// Parse WAV header
int wav_parse_header(FIL *file, WavHeader *header) {
    UINT bytes_read;
    f_read(file, header, sizeof(WavHeader), &bytes_read);

    // Validate header
    if (memcmp(header->riff, "RIFF", 4) != 0) return -1;
    if (memcmp(header->wave, "WAVE", 4) != 0) return -1;
    if (memcmp(header->fmt, "fmt ", 4) != 0) return -1;
    if (header->audio_format != 1) return -1;  // Must be PCM

    return 0;
}

// Fill buffer half from file
void fill_buffer(AudioPlayer *player, int half) {
    uint16_t *buf = player->buffer[half];
    uint8_t temp[BUFFER_SIZE * 2];
    UINT bytes_read;

    // Read raw audio data
    f_read(&player->file, temp, BUFFER_SIZE * 2, &bytes_read);

    // Convert 16-bit signed to 12-bit unsigned
    for (int i = 0; i < BUFFER_SIZE; i++) {
        int16_t sample = (int16_t)(temp[i*2] | (temp[i*2+1] << 8));
        sample = (sample * player->volume) / 100;  // Volume
        buf[i] = (sample + 32768) >> 4;  // Convert to 12-bit
    }
}

Phase 5: Integration and Polish (Days 15-21)

Goals:

  • Complete playback control
  • Add user interface
  • Handle all error conditions
  • Test and optimize

Tasks:

  1. Implement play/pause/stop
  2. Add volume control
  3. Create CLI interface
  4. Add file listing and selection
  5. Test with various WAV files
  6. Optimize for low CPU usage

Checkpoint: Complete audio player with all features working reliably.


Hints in Layers

Hint 1: Getting Started

Start with the simplest possible audio output:

  1. Generate a sine wave table in code
  2. Output it to DAC using software loop
  3. Listen for the tone
  4. Measure frequency with oscilloscope or tuner app

This verifies your DAC is working before adding DMA complexity.

Hint 2: DMA Channel Selection

On STM32F4, DAC Channel 1 uses DMA1 Stream 5 or DMA1 Channel 3 (depending on variant). Check your reference manual’s DMA request mapping table. The wrong channel simply won’t work.

Hint 3: Timer-DAC Connection

The DAC trigger source is selected in DAC_CR:

  • TSEL1[2:0] = 000 for TIM6 TRGO
  • Enable trigger with TEN1 = 1

The timer must output TRGO on update events:

  • TIM6_CR2.MMS = 010 (Update mode)
Hint 4: Buffer Sizing

Choose buffer size based on SD card read performance:

  • SD card reads in 512-byte sectors
  • 4 sectors = 2048 bytes = 1024 16-bit samples
  • At 44.1 kHz, that’s 23 ms per half-buffer
  • Your fill routine must complete in less than 23 ms

Start with larger buffers (4096 samples) for safety, optimize later.

Hint 5: Debugging Audio Problems

Common issues and solutions:

  • No sound: Check DAC enable, GPIO config, DMA enable order
  • Static/noise: Wrong sample rate, byte order, or bit depth conversion
  • Crackling: Buffer underrun, increase buffer size
  • Wrong pitch: Timer period calculation error
  • Distortion: Overflow in volume calculation, clipping
Hint 6: Stereo to Mono Mixing

For stereo WAV files, mix to mono:

int16_t left = samples[i * 2];
int16_t right = samples[i * 2 + 1];
int32_t mono = ((int32_t)left + (int32_t)right) / 2;

Use 32-bit intermediate to prevent overflow.


Testing Strategy

Test Categories

Category Purpose Examples
Unit Tests Test individual functions WAV header parsing, sample conversion
Integration Test component combinations DMA + DAC, SD + Buffer
System Tests Full playback tests Play various files end-to-end
Stress Tests Find limits Long files, rapid start/stop

Critical Test Cases

  1. Basic Playback: 44.1 kHz, 16-bit, mono WAV
  2. Stereo Mixing: 44.1 kHz, 16-bit, stereo WAV
  3. Different Sample Rates: 22050 Hz, 8000 Hz files
  4. 8-bit Audio: 8-bit WAV files
  5. Long File: 1-hour audio file without glitches
  6. Pause/Resume: Verify seamless continuation
  7. Rapid Controls: Quick play/pause/stop sequences
  8. Volume Sweep: Volume from 0 to 100 and back

Test Audio Files

Create test files with known characteristics:

# Generate 440 Hz sine wave, 44.1 kHz, 16-bit, mono
ffmpeg -f lavfi -i "sine=frequency=440:sample_rate=44100:duration=10" \
       -c:a pcm_s16le test_440hz.wav

# Generate 1 kHz sine, different sample rates
ffmpeg -f lavfi -i "sine=frequency=1000:sample_rate=22050:duration=5" \
       -c:a pcm_s16le test_22k.wav

Common Pitfalls & Debugging

Frequent Mistakes

Pitfall Symptom Solution
Wrong DMA channel No output, DMA never completes Check reference manual DMA mapping
DAC not enabled No output Enable DAC_CR_EN1 after all config
Timer not triggering DAC Static output Set TSEL correctly, enable TEN
Buffer underrun Clicking/popping Increase buffer size
Wrong byte order Static noise WAV is little-endian
Signed/unsigned confusion DC offset, quiet audio Convert properly
Missing volatile Erratic behavior Mark shared variables volatile
Interrupt priority Missed interrupts Set DMA IRQ priority higher

Debugging Strategies

  1. Oscilloscope on DAC output: Verify signal shape and timing
  2. Toggle GPIO in ISR: Verify interrupts are firing
  3. Count statistics: Track interrupt count, buffer fills
  4. Serial logging: Print state transitions (but not in ISR!)
  5. Single-step DMA: Disable circular mode, check one transfer

Performance Monitoring

// Add performance counters
typedef struct {
    uint32_t dma_ht_count;
    uint32_t dma_tc_count;
    uint32_t buffer_fills;
    uint32_t underruns;
    uint32_t max_fill_time_us;
    uint32_t total_fill_time_us;
} AudioStats;

// In fill function
uint32_t start = get_us();
fill_buffer(player, half);
uint32_t elapsed = get_us() - start;
stats.total_fill_time_us += elapsed;
if (elapsed > stats.max_fill_time_us) {
    stats.max_fill_time_us = elapsed;
}

Extensions & Challenges

Beginner Extensions

  • Multiple file playlist: Play files in sequence
  • Fast forward/rewind: Skip forward/back 10 seconds
  • Repeat mode: Loop single file or playlist
  • Shuffle mode: Random playback order

Intermediate Extensions

  • Equalizer: Simple bass/treble control
  • Visualization: Show waveform on OLED display
  • MP3 support: Integrate MP3 decoder library
  • Bluetooth audio: Stream from phone via Bluetooth module
  • Recording: Add ADC input for recording to SD card

Advanced Extensions

  • Multi-channel mixer: Mix multiple audio streams
  • Effects processing: Reverb, echo, pitch shift
  • Real-time synthesis: Generate audio programmatically
  • USB audio device: Act as USB sound card
  • Network streaming: Play audio from network stream

Self-Assessment Checklist

Understanding

  • I can explain why DMA is essential for real-time audio
  • I understand the double-buffering technique and why it prevents glitches
  • I can calculate timer periods for any sample rate
  • I understand the relationship between buffer size and latency
  • I can explain the WAV file format structure
  • I know how to convert between different sample formats

Implementation

  • DMA transfers audio data without CPU intervention
  • Timer triggers DAC at precise sample rate
  • Double buffering prevents audio glitches
  • WAV parser correctly handles different formats
  • Volume control works smoothly
  • Play/pause/stop work without glitches
  • Error handling prevents crashes

Performance

  • CPU usage is under 10% during playback
  • No audible glitches during normal operation
  • Long files play without issues
  • Statistics show zero underruns

The Interview Questions They’ll Ask

After completing this project, you’ll be ready for these questions:

  1. “What is DMA and why is it important for audio?”
    • DMA allows peripherals to access memory directly without CPU intervention
    • Essential for real-time audio because CPU would be 100% busy just moving data
    • Enables multi-tasking while maintaining precise timing
  2. “Explain double buffering and when you’d use it”
    • Technique where one buffer is being consumed while another is being filled
    • Prevents glitches by always having data ready
    • Used in audio, video, graphics, network I/O
  3. “How do you achieve precise timing in embedded systems?”
    • Hardware timers generate precise intervals independent of CPU
    • Timer triggers can directly control peripherals (DAC, ADC, PWM)
    • Software delays are unreliable due to interrupts
  4. “What causes audio glitches and how do you prevent them?”
    • Glitches from buffer underrun (data not ready when needed)
    • Prevented by: sufficient buffer size, fast enough data source, proper interrupt priorities
    • Monitoring: track underrun count, measure fill times
  5. “How would you debug audio quality issues?”
    • Oscilloscope to verify signal shape and timing
    • Statistics to track buffer fills and underruns
    • Test files with known characteristics (pure tones)
    • Verify sample rate, bit depth, and format conversion

Books That Will Help

Topic Book Chapter
DMA concepts “Making Embedded Systems” by Elecia White Chapter 7
DAC operation STM32 Reference Manual DAC Chapter
Double buffering “Game Programming Patterns” by Robert Nystrom Chapter 8
Real-time systems “Making Embedded Systems” by Elecia White Chapter 10
Audio fundamentals “The Audio Programming Book” by Boulanger & Lazzarini Chapters 1-3
ARM peripheral programming “The Definitive Guide to ARM Cortex-M3/M4” by Yiu Chapters 8-10

Resources

Documentation

  • STM32 Reference Manual: DMA, DAC, Timer chapters
  • WAV File Format Specification (RIFF)
  • FatFs Documentation

Example Projects


This guide was expanded from LEARN_ARM_DEEP_DIVE.md. For the complete learning path, see the project index.