DAC Architecture Overview

The STM32 DAC block contains up to two independent output channels (DAC1 and DAC2 on larger packages). Each channel has:

  • A 12-bit right-aligned data holding register (DAC_DHR12R1, DAC_DHR12R2)
  • An output data register (DAC_DOR1, DAC_DOR2) that actually drives the pin
  • A trigger selection mux (timer, software, or external) in DAC_CR
  • DMA capability per channel via DAC_DHR12RD (dual) or DAC_DHR12R1 (single)

The key insight: the DAC does not convert continuously. Each conversion is triggered by an event — timer update, timer TRGO, external line, or a software write to the SWTRIG bit. Between triggers, the output holds the last value. You set the trigger source in DAC_CR->TSELx[2:0] and enable it with TENx.

Why Timer + DMA?

For waveform generation, the timer defines the sample rate. Each timer update event tells the DAC to latch DAC_DHR12R1 into DAC_DOR1 and start a new conversion. The DAC conversion itself takes about 3 µs (typical for F4 at 84 MHz APB1), so the maximum sample rate is around 330 kS/s with the 12-bit settling time.

The DMA sits between a RAM buffer holding your waveform samples and the DAC data register. Configured in circular mode, it automatically reloads from the start when it reaches the end of the buffer. The CPU writes the waveform table once, enables everything, and never touches the DAC again until it needs to change the waveform.

Register-Level Configuration (STM32F4 Example)

I will use DAC channel 1 on PA4, TIM6 as the update trigger, and DMA1 Stream 5 Channel 7 on STM32F407. Every STM32 variant has a slightly different DMA mapping — always check the DMA request mapping table in the reference manual.

Step 1 — Clock Gating

RCC->APB1ENR |= RCC_APB1ENR_DACEN | RCC_APB1ENR_TIM6EN;
RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
// DAC output pin: PA4, analogue mode
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER |= (3 << 8);  // PA4 = analog mode

Step 2 — Timer Configuration (TIM6)

TIM6 is a basic timer, purpose-built as a DAC trigger source. Set the prescaler and period to match your desired sample rate.

// Desired sample rate: 48 kHz (audio-friendly)
// APB1 timer clock: 84 MHz
// TIM6 update frequency = 84 MHz / (PSC+1) / (ARR+1)
TIM6->PSC = 0;                // prescaler = 1
TIM6->ARR = (84000000 / 48000) - 1;  // auto-reload for 48 kHz
TIM6->CR2 |= TIM_CR2_MMS_1;  // Master mode: update event on TRGO
TIM6->CR1 |= TIM_CR1_CEN;    // start timer

The MMS_1 bit configures TIM6 to output its update event (UEV) on the TRGO line. On STM32F4, TIM6 TRGO is hardwired to the DAC trigger input when TSEL1 = 000.

Step 3 — DAC Trigger Configuration

DAC->CR = 0;  // start from known state
// TSEL1 = 000 (TIM6 TRGO), TEN1 = 1 (enable trigger),
// DMAEN1 = 1 (enable DMA request), EN1 = 1 (enable DAC)
DAC->CR = DAC_CR_TSEL1_0 << 0  // TSEL1 = 000 → keep default
        | DAC_CR_TEN1             // timer trigger enable
        | DAC_CR_DMAEN1           // DMA request enable
        | DAC_CR_EN1;             // DAC channel 1 enable

The DAC needs a stabilisation time after EN1 is set. Wait about 1 µs (10 NOPs at 168 MHz) before starting the timer.

Step 4 — DMA Configuration (Circular Mode)

The DMA transfers one 16-bit half-word (the 12-bit DAC value, left-aligned in the upper bits) from RAM to DAC_DHR12R1 on each request. The DAC generates a DMA request after each conversion completes.

#define WAVEFORM_SIZE 256
static uint16_t waveform[WAVEFORM_SIZE];  // pre-computed sine table

// DMA1 Stream 5, channel 7 (DAC1 mapping on F407)
DMA1_Stream5->CR = 0;  // disable during configuration
while (DMA1_Stream5->CR & DMA_SxCR_EN);
DMA1_Stream5->PAR = (uint32_t)&DAC->DHR12R1;
DMA1_Stream5->M0AR = (uint32_t)waveform;
DMA1_Stream5->NDTR = WAVEFORM_SIZE;
DMA1_Stream5->FCR = DMA_FCR_DMDIS  // direct mode (no FIFO)
                   | DMA_FCR_FTH_0; // FIFO threshold 1/4
DMA1_Stream5->CR = DMA_SxCR_CHSEL_2 | DMA_SxCR_CHSEL_1 | DMA_SxCR_CHSEL_0  // channel 7
                 | DMA_SxCR_MSIZE_0   // memory size = 16-bit
                 | DMA_SxCR_PSIZE_0   // peripheral size = 16-bit
                 | DMA_SxCR_MINC      // memory increment on
                 | DMA_SxCR_CIRC      // circular mode
                 | DMA_SxCR_DIR_0     // memory-to-peripheral
                 | DMA_SxCR_EN;       // enable DMA

Critical detail: Without CIRC, the DMA transfers the waveform once and stops. The DAC continues outputting the last sample forever. With CIRC, it wraps around indefinitely.

Step 5 — Compute the Sine Wave Table

for (int i = 0; i < WAVEFORM_SIZE; i++) {
    float sample = sinf(2.0f * M_PI * i / WAVEFORM_SIZE);
    // Map [-1.0, 1.0] to [0, 4095], left-align in uint16
    uint32_t val = (uint32_t)((sample + 1.0f) * 2047.5f);
    if (val > 4095) val = 4095;
    waveform[i] = (uint16_t)(val << 4);  // left-aligned 12-bit
}

The left alignment (<< 4) is important: the DAC expects data in bits [15:4] of the 16-bit register for 12-bit mode. Writing into bits [3:0] has no effect.

Practical Example: 1 kHz Sine at 48 kS/s

With a 48 kHz sample rate and a 256-sample waveform table, the generated sine frequency is:

f_sine = f_sample / WAVEFORM_SIZE = 48000 / 256 = 187.5 Hz

To get exactly 1 kHz:

WAVEFORM_SIZE = 48;  // 48000 / 1000 = 48 samples per period

Or keep 256 samples and adjust the timer to fire at 256 kHz:

TIM6->ARR = (84000000 / (256 * 1000)) - 1;  // 256 kHz → 1 kHz output

The output amplitude on PA4 is:

Vout = (Vref / 4096) * sample_12bit

With Vref = 3.3 V and a full-scale sine, the peak-to-peak amplitude is about 3.3 V centred at 1.65 V. Add a DC-blocking capacitor if you need a bipolar signal.

Dual-Channel Operation (Lissajous, I/Q)

For two simultaneous analog outputs, enable DAC2 on PA5, configure DMA1 Stream 6 (channel 7) for the second channel, and use the dual 12-bit register DAC_DHR12RD. Both channels convert on the same timer trigger:

typedef struct {
    uint16_t ch1;  // left-aligned 12-bit, DAC1
    uint16_t ch2;  // left-aligned 12-bit, DAC2
} __attribute__((packed)) dual_dac_t;

dual_dac_t dual_buf[256];
// Fill ch1 and ch2 with sine / cosine for quadrature output

DMA1_Stream5->PAR = (uint32_t)&DAC->DHR12RD;  // dual register
DMA1_Stream5->CR |= DMA_SxCR_MSIZE_0;  // still 16-bit (two transfers per trigger)

The DAC converts both channels back-to-back on a single trigger event. This is the STM32 approach to generating I/Q signals or Lissajous figures without external DACs.

Practical Checklist

CheckWhy
DAC clock enabled in RCC?Writes to DAC registers silently ignored without clock
Pin in analog mode?Digital output drivers fight the DAC output
TEN1 = 1 and TSEL1 matches your timer?Without this, the DAC ignores timer events
DMA channel matches the request mapping table?Wrong channel = no DMA requests after DAC conversion
CIRC bit set?Without circular mode, one-shot output then silence
Data left-aligned in register?Right-alignment needs different register (DAC_DHR12R1 vs DAC_DHR12L1)
Timer prescaler/ARR set for target sample rate?Check with oscilloscope or TIMx->CNT
DAC stabilisation wait after EN1?Without ~1 µs delay, first samples may be invalid

How I Would Approach This on a Client Project

On a recent project using STM32G474 for a motor control application, we needed two synchronised analog outputs — one for the torque reference, one for a shunt current monitor test signal. The register-level approach described above worked directly on G4 with only a DMA channel number change (DMAMUX remapping).

The gotcha that cost us half a day: the G4 DAC has a sample-and-hold mode that is enabled by default in some CubeMX configurations. If DAC_MCR->MODE1 is set to 10 (sample-and-hold with external capacitor), the output is internally disconnected from the pin. Always check DAC_MCR when the DAC is enabled but the pin stays at 0 V.

On L4/L5 series, the DAC also offers a waveform generator built-in (noise/triangle) and a buffer calibration mode that reduces offset error. For precision applications (< 10 mV tolerance), run the calibration sequence in the reference manual before enabling the output.

If your client requires a sample rate above 500 kS/s, consider using the DAC HR Timer on the STM32H7 series, which can trigger up to 1 MS/s through the H7's high-resolution timer (HRTIM).

Sources consulted:

  • STM32F4 Reference Manual (RM0090) — Chapter 13: DAC, Chapter 9: DMA
  • STM32G4 Reference Manual (RM0440) — Chapter 15: DAC
  • STM32L4 Reference Manual (RM0351) — Chapter 12: DAC
  • ST Application Note AN3126 — Audio and waveform generation with the DAC
  • ST Application Note AN4566 — Using the STM32 DAC to get maximum performance
  • ARM Cortex-M4 Generic User Guide — DMA controller
  • CMSIS-Core 5.6.0 — stm32f407xx.h register definitions