Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.
STM32F407 DAC Deep Dive
Master the dual-channel 12-bit DAC — registers explained bit-by-bit, output modes, noise/triangle wave generation, and DMA-driven sine wave output with bare-metal C.
01 DAC Overview
The STM32F407 contains a 12-bit dual-channel DAC with two independent output channels. Unlike the ADC (which converts voltage to number), the DAC converts a digital value to an analog voltage on a GPIO output pin. The output voltage is:
Vout = VDDA × DHR / 4095
- Two independent 12-bit channels (Channel 1 on PA4, Channel 2 on PA5)
- Support for 8-bit or 12-bit data (left or right aligned)
- Output buffer (built-in op-amp) for driving low-impedance loads
- Hardware trigger sources: TIM2/4/5/6/7/8 TRGO, EXTI9, software
- DMA support for automatic waveform generation
- Built-in LFSR noise generator and triangle wave generator
- Simultaneous dual-channel update via DHR12RD register
BOFF=0) allows driving loads down to 5kΩ / 50pF. If you disable the buffer (BOFF=1), the output impedance is high (~15kΩ) — you need an external op-amp to drive any load.DAC Internal Architecture
Output Pins
| Channel | Pin | GPIO Mode | Notes |
|---|---|---|---|
| DAC1_OUT | PA4 | Analog (MODER=11) | Shared with SPI1_NSS, ADC1_IN4. Disable pull-up/down. |
| DAC2_OUT | PA5 | Analog (MODER=11) | Shared with SPI1_SCK, ADC1_IN5. Disable pull-up/down. |
02 DAC Register Map
All DAC registers base address: 0x40007400 (on APB1).
| Offset | Register | Description |
|---|---|---|
| 0x00 | DAC_CR | Control register — enable, trigger, wave, buffer, DMA |
| 0x04 | DAC_SWTRIGR | Software trigger register — write 1 to trigger CH1/CH2 |
| 0x08 | DAC_DHR12R1 | 12-bit right-aligned data for CH1 |
| 0x0C | DAC_DHR12L1 | 12-bit left-aligned data for CH1 |
| 0x10 | DAC_DHR8R1 | 8-bit right-aligned data for CH1 |
| 0x14 | DAC_DHR12R2 | 12-bit right-aligned data for CH2 |
| 0x18 | DAC_DHR12L2 | 12-bit left-aligned data for CH2 |
| 0x1C | DAC_DHR8R2 | 8-bit right-aligned data for CH2 |
| 0x20 | DAC_DHR12RD | 12-bit right-aligned simultaneous data (CH1[15:0] | CH2[31:16]) |
| 0x24 | DAC_DHR12LD | 12-bit left-aligned simultaneous data |
| 0x28 | DAC_DHR8RD | 8-bit right-aligned simultaneous data |
| 0x2C | DAC_DOR1 | Output register CH1 (read-only, actual DAC value) |
| 0x30 | DAC_DOR2 | Output register CH2 (read-only) |
| 0x34 | DAC_SR | Status register — DMA underrun flags |
DAC_CR — Control Register (Offset 0x00)
DAC_CR controls both channels. Bits 15:0 configure Channel 1, bits 31:16 configure Channel 2 (same layout, offset by 16).
Bit and field descriptions
| Bit / Field | Description |
|---|---|
| 31 DU2 | DMAUDRIE2 (bit 29 = CH2) |
| 30 DMAE2 | DMAEN2 — DMA enable for CH2 |
| 29 MMP3 | MAMP2[3] bit 27 — Wave amplitude for CH2 |
| 28 MMP2 | MAMP2[2] |
| 27 MMP1 | MAMP2[1] |
| 26 MMP0 | MAMP2[0] |
| 25 WV21 | WAVE2[1] bits 23:22 — Wave type CH2: 00=off, 01=noise, 10=triangle |
| 24 WV20 | WAVE2[0] |
| 23 TS22 | TSEL2[2] bits 21:19 — Trigger source CH2 |
| 22 TS21 | TSEL2[1] |
| 21 TS20 | TSEL2[0] — 000=TIM6, 111=SW |
| 20 TEN2 | TEN2 — Trigger enable CH2: |
| 19 BOF2 | BOFF2 — Buffer off CH2: |
| 18 EN2 | EN2 — Enable DAC Channel 2 |
| 17 r | Reserved |
| 13 DU1 | DMAUDRIE1 (bit 13) — DMA underrun interrupt enable for CH1 |
| 12 DMAE1 | DMAEN1 — DMA enable CH1: 1=DMA requests after each trigger |
| 11 MMP3 | MAMP1[3] bits 11:8 — Wave amplitude: 0000=1LSB, 1011=4095 |
| 10 MMP2 | MAMP1[2] |
| 9 MMP1 | MAMP1[1] |
| 8 MMP0 | MAMP1[0] |
| 7 WV11 | WAVE1[1] bits 7:6 — Wave type CH1: 00=disabled, 01=noise LFSR, 10=triangle |
| 6 WV10 | WAVE1[0] |
| 5 TS12 | TSEL1[2] bits 5:3 — Trigger source: 000=TIM6, 001=TIM8, 100=TIM2, 111=SW |
| 4 TS11 | TSEL1[1] |
| 3 TS10 | TSEL1[0] |
| 2 TEN1 | TEN1 — Trigger enable CH1: |
| 1 BOF1 | BOFF1 — Output buffer disable CH1: |
| 0 EN1 | EN1 — Enable DAC Channel 1: |
| Bits (CH1/CH2) | Name | Description |
|---|---|---|
| 0 / 16 | EN1/EN2 | Channel enable: 1=DAC channel on |
| 1 / 17 | BOFF1/BOFF2 | Output buffer disable: 0=buffer ON (drives loads), 1=buffer OFF (high-Z output) |
| 2 / 18 | TEN1/TEN2 | Trigger enable: 0=DHR→DOR immediately, 1=DHR→DOR on trigger event |
| 5:3 / 21:19 | TSEL1/TSEL2 | Trigger selection: 000=TIM6, 001=TIM8, 010=TIM7, 011=TIM5, 100=TIM2, 101=TIM4, 110=EXTI9, 111=SW trigger |
| 7:6 / 23:22 | WAVE1/WAVE2 | Wave generation: 00=disabled, 01=noise wave, 10=triangle wave, 11=reserved |
| 11:8 / 27:24 | MAMP1/MAMP2 | Wave amplitude: 0000=1LSB, …, 1011=4095 (full scale). Controls LFSR mask or triangle max value. |
| 12 / 28 | DMAEN1/DMAEN2 | DMA enable: 1=DMA request generated when DHR register is updated |
| 13 / 29 | DMAUDRIE1/2 | DMA underrun interrupt enable: fires if DMA underrun occurs |
DHR Registers — Data Holding Registers
You write your desired output value to a DHR (Data Holding Register). When a trigger occurs (or immediately if TEN=0), the value is transferred to DOR and the analog output is updated. There are 6 DHR variants:
| Register | Format | Bits Used | Value Range |
|---|---|---|---|
| DHR12R1/2 | 12-bit right-aligned | [11:0] | 0–4095 |
| DHR12L1/2 | 12-bit left-aligned | [15:4] | 0–65520 (LSB always 0) |
| DHR8R1/2 | 8-bit right-aligned | [7:0] | 0–255 (mapped to full scale) |
| DHR12RD | Dual 12-bit right | CH1[11:0], CH2[27:16] | Simultaneous update |
DAC_DOR — Data Output Register
Read-only. DOR reflects the value actually driving the DAC converter at any moment. After writing DHR, if TEN=0, DOR updates within one APB1 cycle. With TEN=1, DOR updates on the next trigger edge. You rarely need to read DOR in practice.
DAC_SR — Status Register (Offset 0x34)
| Bit | Name | Description |
|---|---|---|
| 13 | DMAUDR1 | DMA underrun CH1: Set when DMA fails to provide new data before DAC needs it. Write 1 to clear. |
| 29 | DMAUDR2 | DMA underrun CH2: Same for channel 2. |
03 Simple DC Output
The simplest DAC use case: write a constant value to DHR12R1. The DAC immediately outputs a fixed voltage. No trigger, no DMA needed.
#include <stdint.h>
/* ── DAC Register Definitions ── */
#define RCC_APB1ENR (*(volatile uint32_t *)0x40023840)
#define RCC_AHB1ENR (*(volatile uint32_t *)0x40023830)
#define GPIOA_MODER (*(volatile uint32_t *)0x40020000)
#define GPIOA_PUPDR (*(volatile uint32_t *)0x4002000C)
#define DAC_CR (*(volatile uint32_t *)0x40007400)
#define DAC_DHR12R1 (*(volatile uint32_t *)0x40007408)
#define DAC_DHR12R2 (*(volatile uint32_t *)0x40007414)
#define DAC_DOR1 (*(volatile uint32_t *)0x4000742C)
#define DAC_DOR2 (*(volatile uint32_t *)0x40007430)
/* ── Initialize DAC Channel 1 (PA4) ── */
void DAC1_Init(void) {
/* 1. Enable clocks: GPIOA and DAC */
RCC_AHB1ENR |= (1 << 0); /* GPIOAEN */
RCC_APB1ENR |= (1 << 29); /* DACEN bit 29 */
/* 2. PA4 → Analog mode (no AFRL needed) */
GPIOA_MODER |= (0x3 << 8); /* bits 9:8 = 11 → analog */
GPIOA_PUPDR &= ~(0x3 << 8); /* no pull-up/pull-down */
/* 3. Configure DAC_CR for CH1:
EN1=1 (enable), BOFF1=0 (buffer on),
TEN1=0 (no trigger — DHR→DOR immediately),
WAVE1=00 (no wave gen), DMAEN1=0 */
DAC_CR &= ~(0xFFFF); /* clear CH1 bits */
DAC_CR |= (1 << 0); /* EN1 = 1 */
/* BOFF1=0 (leave), TEN1=0 (leave) */
}
/* ── Set DAC1 output voltage ──
val: 0–4095 (0V to VDDA=3.3V)
mV: pass millivolts, function converts: val = mV*4095/3300 */
void DAC1_Set(uint16_t val) {
DAC_DHR12R1 = val & 0x0FFF;
}
void DAC1_SetVoltage_mV(uint32_t mV) {
uint16_t val = (uint16_t)((mV * 4095) / 3300);
DAC1_Set(val);
}
int main(void) {
DAC1_Init();
/* Output 1.65V (mid-scale) */
DAC1_SetVoltage_mV(1650);
while (1) {
/* Measure PA4 with a voltmeter — should read ~1.65V */
}
}
04 Triggered Output
With TEN=1, the DHR value is latched but not sent to the output until a trigger event occurs. This lets you pre-load the next value while the current one is still active — essential for precise timing (e.g., synchronized with a PWM period).
C — Software trigger example
#define DAC_SWTRIGR (*(volatile uint32_t *)0x40007404)
/* Configure CH1 with software trigger */
void DAC1_Init_SWTrig(void) {
RCC_AHB1ENR |= (1<<0);
RCC_APB1ENR |= (1<<29);
GPIOA_MODER |= (3<<8);
GPIOA_PUPDR &= ~(3<<8);
DAC_CR = (1<<0) | /* EN1 */
(1<<2) | /* TEN1 = trigger enable */
(7<<3); /* TSEL1 = 111 = software trigger */
}
/* Output value and trigger immediately */
void DAC1_SWUpdate(uint16_t val) {
DAC_DHR12R1 = val & 0xFFF;
DAC_SWTRIGR = (1<<0); /* SWTRIG1 — fire! self-clearing */
}
/* Staircase ramp using software trigger */
int main(void) {
DAC1_Init_SWTrig();
uint16_t step = 0;
while (1) {
DAC1_SWUpdate(step);
step = (step + 256) & 0xFFF; /* 16 steps of 256 */
for (volatile int i=0;i<50000;i++); /* delay */
}
}
TIM6 is the simplest timer for DAC triggering (no output capture/PWM, just TRGO). Configure TIM6 as a periodic trigger:
/* TIM6: APB1 = 42MHz → 84MHz timer clock */
/* Want 44.1 kHz trigger for audio: PSC=0, ARR = 84MHz/44100 - 1 = 1904 */
#define RCC_APB1ENR (*(volatile uint32_t *)0x40023840)
#define TIM6_CR1 (*(volatile uint32_t *)0x40001000)
#define TIM6_CR2 (*(volatile uint32_t *)0x40001004)
#define TIM6_PSC (*(volatile uint32_t *)0x40001028)
#define TIM6_ARR (*(volatile uint32_t *)0x4000102C)
#define TIM6_EGR (*(volatile uint32_t *)0x40001014)
void TIM6_Init_44100Hz(void) {
RCC_APB1ENR |= (1<<4); /* TIM6EN bit 4 */
TIM6_CR1 = 0; /* basic upcounter */
TIM6_CR2 = (2<<4); /* MMS=010: TRGO on Update event */
TIM6_PSC = 0; /* no prescaler → 84 MHz */
TIM6_ARR = 1904; /* 84MHz / 1904+1 ≈ 44.1 kHz */
TIM6_EGR = (1<<0); /* UG: force reload */
TIM6_CR1 |= (1<<0); /* CEN: start timer */
}
/* DAC CH1 triggered by TIM6 TRGO */
void DAC1_Init_TIM6Trig(void) {
GPIOA_MODER |= (3<<8);
GPIOA_PUPDR &= ~(3<<8);
/* TSEL1=000=TIM6 TRGO, TEN1=1, EN1=1 */
DAC_CR = (1<<0)|(1<<2)|(0<<3);
}
05 LFSR Noise Wave Generator
The DAC has a built-in 12-bit LFSR (Linear Feedback Shift Register) pseudo-random noise generator. Set WAVE1 = 01 to add LFSR noise on top of the DHR value. MAMP1 controls the noise amplitude mask.
| MAMP[3:0] | LFSR Mask | Peak-to-Peak Range |
|---|---|---|
| 0000 | 0x001 | 2 LSB (≈1.6mV) |
| 0001 | 0x003 | 4 LSB |
| 0010 | 0x007 | 8 LSB |
| 0011 | 0x00F | 16 LSB |
| 0100 | 0x01F | 32 LSB |
| 0101 | 0x03F | 64 LSB |
| 0110 | 0x07F | 128 LSB |
| 0111 | 0x0FF | 256 LSB |
| 1000 | 0x1FF | 512 LSB |
| 1001 | 0x3FF | 1024 LSB |
| 1010 | 0x7FF | 2048 LSB |
| 1011 | 0xFFF | 4096 LSB (full scale) |
C — LFSR noise generator on PA4
void DAC1_Noise_Init(void) {
RCC_AHB1ENR |= (1<<0);
RCC_APB1ENR |= (1<<29);
GPIOA_MODER |= (3<<8);
GPIOA_PUPDR &= ~(3<<8);
/* Need TIM6 to clock the LFSR — configure 10kHz */
RCC_APB1ENR |= (1<<4); /* TIM6EN */
volatile uint32_t *TIM6_CR2 = (volatile uint32_t *)(0x40001004);
volatile uint32_t *TIM6_PSC = (volatile uint32_t *)(0x40001028);
volatile uint32_t *TIM6_ARR = (volatile uint32_t *)(0x4000102C);
volatile uint32_t *TIM6_EGR = (volatile uint32_t *)(0x40001014);
volatile uint32_t *TIM6_CR1 = (volatile uint32_t *)(0x40001000);
*TIM6_CR2 = (2<<4); /* TRGO on update */
*TIM6_PSC = 0;
*TIM6_ARR = 8399; /* 84MHz / 8400 = 10kHz update rate */
*TIM6_EGR = 1; *TIM6_CR1 = 1;
/* DAC_CR: EN1, TEN1 (TIM6 TRGO), WAVE1=01 (noise), MAMP1=1011 (full) */
DAC_CR = (1<<0) | /* EN1 */
(1<<2) | /* TEN1 */
(0<<3) | /* TSEL1 = 000 = TIM6 */
(1<<6) | /* WAVE1 = 01 = noise (bit 6) */
(0xB<<8); /* MAMP1 = 1011 = full amplitude */
/* Set DC offset = mid-scale so noise is centered */
DAC_DHR12R1 = 2048; /* base value; noise added on top */
}
06 Hardware Triangle Wave Generator
Set WAVE1 = 10 and the DAC hardware automatically generates a triangle wave, counting up then down. The amplitude is set by MAMP1. The frequency depends on the trigger rate.
Triangle wave frequency formula: f_tri = f_trigger / (2 × (MAMP_max + 1))
C — Triangle wave at 1kHz
/* Triangle: MAMP=0111 (256 steps), TIM6=512kHz → f_tri = 512k/(2*256) = 1kHz */
void DAC1_Triangle_1kHz(void) {
RCC_AHB1ENR |= (1<<0);
RCC_APB1ENR |= (1<<29)|(1<<4);
GPIOA_MODER |= (3<<8);
GPIOA_PUPDR &= ~(3<<8);
/* TIM6 at 512 kHz: PSC=0, ARR = 84MHz/512000 - 1 = 163 */
volatile uint32_t *TIM6_CR2 = (volatile uint32_t *)(0x40001004);
volatile uint32_t *TIM6_PSC = (volatile uint32_t *)(0x40001028);
volatile uint32_t *TIM6_ARR = (volatile uint32_t *)(0x4000102C);
volatile uint32_t *TIM6_EGR = (volatile uint32_t *)(0x40001014);
volatile uint32_t *TIM6_CR1 = (volatile uint32_t *)(0x40001000);
*TIM6_CR2 = (2<<4);
*TIM6_PSC = 0;
*TIM6_ARR = 163; /* 84MHz/164 ≈ 512kHz */
*TIM6_EGR = 1; *TIM6_CR1 = 1;
/* DAC_CR: WAVE1=10 (triangle), MAMP1=0111 (256 LSB), TEN1, TIM6, EN1 */
DAC_CR = (1<<0) | /* EN1 */
(1<<2) | /* TEN1 */
(0<<3) | /* TSEL1=TIM6 */
(2<<6) | /* WAVE1=10=triangle */
(7<<8); /* MAMP1=0111=256 steps */
DAC_DHR12R1 = 0; /* base = 0V, triangle goes 0→256 */
}
07 Sine Wave Generation with DMA
For arbitrary waveforms (sine, sawtooth, custom), pre-compute a lookup table (LUT) and use DMA in circular mode to feed DAC from the table. TIM6 triggers the DAC at the desired sample rate. CPU is free while the waveform plays continuously.
Sine wave LUT generation formula: lut[i] = 2048 + 2047 * sin(2π × i / N) where N is the number of samples (e.g., 256).
C — 256-sample sine wave via DMA to DAC1
#include <stdint.h>
#include <math.h> /* for sinf — or precompute table */
/* ── Sine wave LUT (256 samples, 12-bit) ──
Pre-computed: lut[i] = 2048 + (int)(2047 * sin(2π*i/256))
Use Python: [int(2048 + 2047*math.sin(2*math.pi*i/256)) for i in range(256)]
────────────────────────────────────────── */
static const uint16_t sine_lut[256] = {
2048,2098,2148,2198,2248,2298,2347,2397,
2446,2495,2543,2592,2639,2687,2734,2780,
2826,2871,2916,2960,3003,3046,3088,3129,
3169,3209,3247,3285,3322,3357,3392,3425,
3458,3489,3519,3548,3576,3602,3628,3652,
3675,3697,3717,3736,3754,3771,3786,3800,
3812,3823,3833,3841,3848,3854,3858,3861,
3863,3863,3861,3858,3854,3848,3841,3833,
3823,3812,3800,3786,3771,3754,3736,3717,
3697,3675,3652,3628,3602,3576,3548,3519,
3489,3458,3425,3392,3357,3322,3285,3247,
3209,3169,3129,3088,3046,3003,2960,2916,
2871,2826,2780,2734,2687,2639,2592,2543,
2495,2446,2397,2347,2298,2248,2198,2148,
2098,2048,1997,1947,1897,1847,1797,1748,
1699,1650,1602,1553,1506,1458,1411,1365,
1319,1274,1229,1185,1142,1099,1057,1016,
976, 936, 898, 860, 823, 788, 753, 720,
688, 656, 626, 597, 569, 543, 517, 493,
470, 448, 428, 409, 391, 374, 359, 345,
332, 321, 311, 302, 295, 289, 285, 282,
280, 280, 282, 285, 289, 295, 302, 311,
321, 332, 345, 359, 374, 391, 409, 428,
448, 470, 493, 517, 543, 569, 597, 626,
656, 688, 720, 753, 788, 823, 860, 898,
936, 976, 1016,1057,1099,1142,1185,1229,
1274,1319,1365,1411,1458,1506,1553,1602,
1650,1699,1748,1797,1847,1897,1947,1997
};
/* ── DMA1 Stream 5 Channel 7 → DAC Channel 1 ── */
/* (Per reference manual Table 42: DMA1_S5_CH7 = DAC1) */
#define DMA1_BASE 0x40026000
#define DMA1_S5CR (*(volatile uint32_t *)(DMA1_BASE+0x088))
#define DMA1_S5NDTR (*(volatile uint32_t *)(DMA1_BASE+0x08C))
#define DMA1_S5PAR (*(volatile uint32_t *)(DMA1_BASE+0x090))
#define DMA1_S5M0AR (*(volatile uint32_t *)(DMA1_BASE+0x094))
void DAC1_Sine_DMA_Init(uint32_t freq_hz) {
/* 1. Clocks */
RCC_AHB1ENR |= (1<<0)|(1<<21); /* GPIOA + DMA1EN (bit 21) */
RCC_APB1ENR |= (1<<29)|(1<<4); /* DAC + TIM6 */
/* 2. PA4 analog */
GPIOA_MODER |= (3<<8);
GPIOA_PUPDR &= ~(3<<8);
/* 3. TIM6: sample rate = freq_hz * 256 (256 samples per cycle) */
uint32_t sample_rate = freq_hz * 256;
uint32_t arr = (84000000 / sample_rate) - 1;
volatile uint32_t *TIM6_CR2 = (volatile uint32_t *)(0x40001004);
volatile uint32_t *TIM6_PSC = (volatile uint32_t *)(0x40001028);
volatile uint32_t *TIM6_ARR = (volatile uint32_t *)(0x4000102C);
volatile uint32_t *TIM6_EGR = (volatile uint32_t *)(0x40001014);
volatile uint32_t *TIM6_CR1 = (volatile uint32_t *)(0x40001000);
*TIM6_CR2 = (2<<4); /* TRGO on update */
*TIM6_PSC = 0;
*TIM6_ARR = arr;
*TIM6_EGR = 1;
*TIM6_CR1 = 1;
/* 4. DAC CR: EN1, TEN1 (TIM6), WAVE=00, DMAEN1 */
DAC_CR = (1<<0)|(1<<2)|(0<<3)|(1<<12);
/* EN1 TEN1 TSEL=TIM6 DMAEN1 */
/* 5. DMA1 Stream 5 Channel 7 */
DMA1_S5CR = 0; while(DMA1_S5CR & 1);
DMA1_S5PAR = 0x40007408; /* DAC_DHR12R1 address */
DMA1_S5M0AR = (uint32_t)sine_lut;
DMA1_S5NDTR = 256;
DMA1_S5CR =
(7<<25) | /* CHSEL = 7 (DAC CH1) */
(1<<13) | /* MSIZE = 01 (16-bit memory) */
(1<<11) | /* PSIZE = 01 (16-bit periph) */
(1<<10) | /* MINC = 1 */
(1<<8) | /* CIRC = 1 (circular) */
(1<<6) | /* DIR = 1 (memory→periph) */
(1<<0); /* EN */
/* DMA now feeds DAC continuously — CPU is free! */
}
int main(void) {
DAC1_Sine_DMA_Init(440); /* 440 Hz = musical note A */
while (1) {
/* Sine wave plays automatically via DMA — do other work here */
}
}
08 Dual Channel Simultaneous Output
Use DHR12RD (dual 12-bit right-aligned register at offset 0x20) to update both DAC channels atomically in a single 32-bit write. Bits [11:0] = CH1, bits [27:16] = CH2.
C — Dual channel simultaneous update
#define DAC_DHR12RD (*(volatile uint32_t *)0x40007420)
void DAC_Dual_Init(void) {
RCC_AHB1ENR |= (1<<0);
RCC_APB1ENR |= (1<<29);
/* PA4 and PA5 both analog */
GPIOA_MODER |= (3<<8)|(3<<10);
GPIOA_PUPDR &= ~((3<<8)|(3<<10));
/* CR: EN1+EN2, no trigger, no wave */
DAC_CR = (1<<0)|(1<<16);
}
void DAC_Dual_Set(uint16_t ch1, uint16_t ch2) {
DAC_DHR12RD = ((uint32_t)(ch2 & 0xFFF) << 16) |
(uint32_t)(ch1 & 0xFFF);
}
/* Example: generate two out-of-phase sine waves
CH1 = sin, CH2 = cos (90° phase shift) */
void dual_sine_step(uint8_t *phase) {
/* sine and cosine LUTs would both be 256 entries */
uint16_t ch1_val = sine_lut[*phase];
uint16_t ch2_val = sine_lut[(*phase + 64) & 0xFF]; /* +90° */
DAC_Dual_Set(ch1_val, ch2_val);
(*phase)++;
}
09 Audio Output
The DAC can drive a small speaker or headphone amplifier. For audio playback at 44.1 kHz (CD quality), use TIM6 at 44100 Hz and DMA in circular mode with a ping-pong buffer (double buffer) to allow the CPU to prepare the next block while the current one plays.
The 100Ω resistor prevents oscillation from capacitive loads. The 10µF capacitor removes the DC offset (~1.65V mid-scale). The DAC output buffer can source ~1mA — use an amplifier for speakers.
C — Ping-pong DMA buffer for audio
#define AUDIO_BUF_SIZE 256
static uint16_t audio_buf[2][AUDIO_BUF_SIZE]; /* double buffer */
volatile uint8_t buf_ready = 0; /* which half is ready to fill */
/* Called from DMA1 Stream5 interrupt (half-transfer and full-transfer) */
void DMA1_Stream5_IRQHandler(void) {
volatile uint32_t *DMA1_HISR = (volatile uint32_t *)(0x40026004);
volatile uint32_t *DMA1_HIFCR = (volatile uint32_t *)(0x4002600C);
if (*DMA1_HISR & (1<<10)) { /* HTIF5 — half transfer */
*DMA1_HIFCR = (1<<10); /* clear */
buf_ready = 0; /* fill first half */
}
if (*DMA1_HISR & (1<<11)) { /* TCIF5 — full transfer */
*DMA1_HIFCR = (1<<11); /* clear */
buf_ready = 1; /* fill second half */
}
}
/* Main audio loop */
int main(void) {
/* init DAC + DMA with double buffer audio_buf[0] + audio_buf[1] */
/* DMA M0AR = audio_buf[0], total 512 transfers, HTIE+TCIE enabled */
while (1) {
/* Wait until a half is ready to fill */
uint8_t half = buf_ready;
/* Fill audio_buf[half] with your audio data */
for (int i=0; i<AUDIO_BUF_SIZE; i++) {
audio_buf[half][i] = sine_lut[i]; /* or real PCM data */
}
}
}
10 Tips, Pitfalls & Best Practices
- Not enabling GPIO analog mode: PA4/PA5 must be set to MODER=11 (analog). Forgetting this causes the DAC output to fight the GPIO driver.
- Conflicting peripherals on PA4/PA5: These pins are shared with ADC1_IN4/IN5 and SPI1_NSS/SCK. Never use both simultaneously without reconfiguring the GPIO.
- DMA underrun (DMAUDR): If the DMA can’t keep up with the DAC trigger rate, the DAC re-outputs the previous value, causing waveform artifacts. Check DMA clock and priority.
- Output buffer oscillation: With large capacitive loads (>50pF), the buffer can oscillate. Add a 100Ω series resistor at the output.
- Forgetting BOFF when sharing ADC: If you use PA4 for ADC in one mode and DAC in another, disable DAC EN when ADC is active, or use different pins.
- DMA channel selection: DAC1 uses DMA1 (not DMA2!). Stream 5 Channel 7 = DAC CH1, Stream 6 Channel 7 = DAC CH2.
- 8-bit mode: Use DHR8R for audio if you only have 8-bit PCM data. The DAC scales it to full range (0→255 maps to 0→VDDA).
- Waveform frequency accuracy: For audio, calculate TIM6 ARR precisely:
ARR = (F_tim / (f_out × N_samples)) - 1. At 84 MHz with 256 samples for 440 Hz: ARR = 84000000/(440×256) – 1 = 745. - Noise filtering: Add a simple RC low-pass filter (e.g., 10kΩ + 10nF = 1.6 kHz cutoff) to smooth DAC output steps. Useful for audio applications.
- VREF accuracy: For precision voltage output, use an external precision voltage reference connected to VREF+ pin instead of VDDA.
STM32F407 Tutorial Series — Tutorial 17: DAC | ← ADC | DMA →
Bare-metal register-level programming · No HAL library · STM32F407VGT6
Need Help or More Information?
Questions about this free C programming course, embedded C training or career guidance? Get in touch.

2 Comments