Back to Technology

STM32 Part 5: ADC & DAC

March 31, 2026 Wasil Zafar 27 min read

From single-channel polling conversion to 16-channel DMA scan mode with analog watchdog — master the STM32 ADC architecture and generate precision waveforms with the 12-bit DAC.

Table of Contents

  1. STM32 ADC Architecture
  2. Sampling Time Selection
  3. Scan Mode with DMA
  4. Injected Channels
  5. Analog Watchdog
  6. Multi-ADC Modes (F4/H7)
  7. DAC — Waveform Generation
  8. Exercises
  9. ADC Configuration Tool
  10. Conclusion & Next Steps
Series Context: This is Part 5 of the 18-part STM32 Unleashed series. In Part 4 we mastered the timer subsystem. Here we move to the analog domain — configuring the ADC for everything from simple single-shot polling to high-speed DMA scan modes, plus generating precision analog waveforms with the DAC.

STM32 Unleashed: HAL Driver Development

Your 18-step learning path • Currently on Step 5
1
Architecture & CubeMX Setup
STM32 family, clock tree, HAL vs LL, CubeMX workflow, first project
Completed
2
GPIO & Button Debounce
GPIO modes, pull-up/down, EXTI, software debounce, HAL_GPIO_ReadPin
Completed
3
UART Communication
Polling, interrupt, DMA modes, printf retargeting, ring buffers
Completed
4
Timers, PWM & Input Capture
TIM basics, PWM generation, input capture, encoder mode
Completed
5
ADC & DAC
Single/continuous conversion, DMA, injected channels, DAC waveforms
You Are Here
6
SPI Protocol
SPI master/slave, full-duplex, DMA transfers, sensor drivers
7
I2C Protocol
I2C master, 7/10-bit addressing, DMA, multi-master, error handling
8
DMA & Memory Efficiency
DMA streams, circular mode, memory-to-memory, zero-copy patterns
9
Interrupt Management & NVIC
Priority grouping, preemption, ISR design, HAL callbacks, latency
10
Low-Power Modes
Sleep, Stop, Standby modes, RTC wakeup, LP UART, power profiling
11
RTC & Calendar
RTC configuration, alarms, backup registers, calendar subseconds
12
CAN Bus
FDCAN/bxCAN, filters, message frames, error handling, automotive use
13
USB CDC Virtual COM Port
USB FS/HS, CDC class, virtual serial, control transfers, descriptors
14
FreeRTOS Integration
Tasks, queues, semaphores, mutexes, CMSIS-RTOS2 wrapper, stack sizing
15
Bootloader Development
Custom IAP bootloader, UART/USB DFU, flash programming, jump-to-app
16
External Storage: SD & QSPI Flash
FATFS on SD card, QSPI NOR flash, memory-mapped execution, wear levelling
17
Ethernet & TCP/IP Stack
LwIP integration, DHCP, TCP server, HTTP, MQTT, Ethernet DMA descriptors
18
Production Readiness
Watchdog, HardFault handler, flash option bytes, code signing, CI/CD

STM32 ADC Architecture

The STM32F4 integrates up to three 12-bit successive approximation ADCs (ADC1, ADC2, ADC3) sharing a common clock prescaler and capable of operating simultaneously in multi-ADC modes. Each ADC has 16 external multiplexed input channels plus three internal channels: the temperature sensor (mapped to ADC1_IN16), the internal voltage reference VREFINT (ADC1_IN17), and the battery voltage divider VBAT (ADC1_IN18).

Resolution & Conversion Time

The STM32 ADC supports four resolution options selectable at runtime via the ADC_CR1 RES bits. Lower resolution means fewer conversion cycles and therefore higher throughput, at the cost of reduced voltage precision. The total conversion time in ADC clock cycles is:

T_conv = T_sample + T_sar

Where T_sar depends on resolution (12-bit = 12 cycles, 10-bit = 10 cycles, etc.) and T_sample is the programmable sampling time. With the ADC clock at 21 MHz (84 MHz APB2 / prescaler 4) and 3-cycle sampling time, a 12-bit conversion takes (3 + 12) / 21 MHz = 714 ns — just under 1.4 MSPS.

Resolution Bits Max Voltage Step (3.3V ref) SAR Cycles Typical Use Case
12-bit 4096 steps 0.806 mV 12 Precision sensing, audio, instrumentation
10-bit 1024 steps 3.22 mV 10 Medium accuracy, higher throughput
8-bit 256 steps 12.9 mV 8 Fast oversampling, coarse control loops
6-bit 64 steps 51.6 mV 6 Maximum speed, comparator-like use

Regular vs Injected Channel Groups

The STM32 ADC has two independent conversion groups. The regular group can contain up to 16 channels in a programmed sequence — configured via ADC_SQRx registers. Conversions run sequentially, and the last conversion in the sequence triggers an EOC (End of Conversion) interrupt or DMA request. The injected group can contain up to 4 channels and has higher priority. If a trigger (hardware timer or software) fires the injected group while the regular group is converting, the regular conversion is suspended, the injected sequence runs to completion, and then the regular conversion resumes. This is vital for safety-critical measurements (overcurrent, overvoltage) that must be sampled at a precise moment regardless of what the regular ADC is doing.

Single-Channel Polling

The simplest ADC use case: configure one channel, start a single conversion, poll for completion, read the result. The HAL makes this straightforward with three calls:

/* Single ADC1 channel polling — PA1 (ADC1_IN1)
 * ADC clock: APB2/4 = 84 MHz / 4 = 21 MHz
 * Resolution: 12-bit (0–4095)
 * Sampling time: 56 cycles → T_conv = (56+12)/21 MHz = 3.24 µs
 */

ADC_HandleTypeDef hadc1;

void ADC1_SingleChannel_Init(void)
{
    ADC_ChannelConfTypeDef ch_config = {0};

    __HAL_RCC_ADC1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /* PA1 as analog input */
    GPIO_InitTypeDef g = {0};
    g.Pin  = GPIO_PIN_1;
    g.Mode = GPIO_MODE_ANALOG;
    g.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &g);

    /* ADC configuration */
    hadc1.Instance                   = ADC1;
    hadc1.Init.ClockPrescaler        = ADC_CLOCK_SYNC_PCLK_DIV4; /* 21 MHz  */
    hadc1.Init.Resolution            = ADC_RESOLUTION_12B;
    hadc1.Init.ScanConvMode          = DISABLE;                   /* 1 ch    */
    hadc1.Init.ContinuousConvMode    = DISABLE;                   /* single  */
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConvEdge  = ADC_EXTERNALTRIGCONVEDGE_NONE;
    hadc1.Init.DataAlign             = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion       = 1;
    HAL_ADC_Init(&hadc1);

    /* Channel 1: PA1 */
    ch_config.Channel      = ADC_CHANNEL_1;
    ch_config.Rank         = 1;
    ch_config.SamplingTime = ADC_SAMPLETIME_56CYCLES;
    HAL_ADC_ConfigChannel(&hadc1, &ch_config);
}

/* Read ADC — blocking poll, returns 0–4095 */
uint16_t ADC1_ReadChannel(void)
{
    HAL_ADC_Start(&hadc1);
    HAL_ADC_PollForConversion(&hadc1, 10);       /* 10 ms timeout            */
    uint16_t raw = (uint16_t)HAL_ADC_GetValue(&hadc1);
    HAL_ADC_Stop(&hadc1);
    return raw;
}

/* Convert raw to millivolts (VDDA = 3300 mV, 12-bit = 4096 steps) */
uint32_t ADC_RawToMillivolts(uint16_t raw)
{
    return ((uint32_t)raw * 3300UL) / 4095UL;
}

Sampling Time Selection

The sampling time is the period during which the ADC's internal sample-and-hold capacitor (C_ADC ≈ 4 pF on STM32F4) is connected to the source and charges toward the input voltage. If the source impedance is high, this capacitor cannot charge fully in a short time, introducing a systematic error. The minimum sampling time for a given source impedance is:

T_sample_min = (R_source + R_ADC) × C_ADC × ln(2^(N+1))

For 12-bit accuracy (N=12) with an internal switch resistance R_ADC ≈ 6 kΩ and source impedance R_source = 10 kΩ: T_sample_min = (10,000 + 6,000) × 4×10⁻¹² × ln(8192) ≈ 561 ns. At a 21 MHz ADC clock (47.6 ns per cycle) this requires at least 12 cycles. Use 56 cycles for margin, or 112 cycles for sources up to 50 kΩ.

The eight selectable sampling times and their typical use cases:

Sampling Cycles Time @ 21 MHz Max Source R (12-bit) Typical Application
3143 ns<1 kΩLow-impedance voltage divider, op-amp buffered
15714 ns~5 kΩResistor divider with buffer, current sense amp
281.33 µs~10 kΩStandard resistor divider
562.67 µs~22 kΩGeneral purpose, sensor outputs
844.0 µs~34 kΩHigh-impedance sensors
1125.33 µs~45 kΩPhotodiode, pH sensor
1446.86 µs~60 kΩVery high impedance sources
48022.9 µs>200 kΩTemperature sensor & VREFINT (minimum required)

Internal Temperature Sensor

The STM32F4 internal temperature sensor is mapped to ADC1_IN16. It requires a minimum sampling time of 10 µs — use ADC_SAMPLETIME_480CYCLES at 21 MHz. The temperature is calculated from the calibration values stored in factory-programmed Flash during production testing:

/* Read internal temperature sensor using factory calibration
 * Calibration values at 3.3V VDDA:
 *   TS_CAL1 @ 30°C  stored at address 0x1FFF7A2C (STM32F4)
 *   TS_CAL2 @ 110°C stored at address 0x1FFF7A2E
 */

#define TS_CAL1_ADDR   ((uint16_t *)0x1FFF7A2C)
#define TS_CAL2_ADDR   ((uint16_t *)0x1FFF7A2E)
#define TS_CAL1_TEMP   30.0f
#define TS_CAL2_TEMP   110.0f

ADC_HandleTypeDef hadc1_temp;

void ADC1_TempSensor_Init(void)
{
    ADC_ChannelConfTypeDef ch = {0};

    hadc1_temp.Instance               = ADC1;
    hadc1_temp.Init.ClockPrescaler    = ADC_CLOCK_SYNC_PCLK_DIV4;
    hadc1_temp.Init.Resolution        = ADC_RESOLUTION_12B;
    hadc1_temp.Init.ScanConvMode      = DISABLE;
    hadc1_temp.Init.ContinuousConvMode= DISABLE;
    hadc1_temp.Init.DataAlign         = ADC_DATAALIGN_RIGHT;
    hadc1_temp.Init.NbrOfConversion   = 1;
    HAL_ADC_Init(&hadc1_temp);

    /* Temperature sensor channel — minimum 480 cycles sampling time */
    ch.Channel      = ADC_CHANNEL_TEMPSENSOR;  /* Routes TSENSE to ADC1_IN16 */
    ch.Rank         = 1;
    ch.SamplingTime = ADC_SAMPLETIME_480CYCLES; /* ≥10 µs required           */
    HAL_ADC_ConfigChannel(&hadc1_temp, &ch);
}

float ADC1_ReadTemperature(void)
{
    uint16_t cal1  = *TS_CAL1_ADDR;
    uint16_t cal2  = *TS_CAL2_ADDR;

    HAL_ADC_Start(&hadc1_temp);
    HAL_ADC_PollForConversion(&hadc1_temp, 50);
    uint16_t raw = (uint16_t)HAL_ADC_GetValue(&hadc1_temp);
    HAL_ADC_Stop(&hadc1_temp);

    /* Linear interpolation between the two calibration points */
    float temp = (TS_CAL2_TEMP - TS_CAL1_TEMP) / (float)(cal2 - cal1)
                 * (float)(raw - cal1) + TS_CAL1_TEMP;
    return temp;
}

Scan Mode with DMA

Scan mode instructs the ADC to step through the regular sequence automatically — converting channel 1, then 2, then 3, and so on, without CPU intervention between conversions. Pairing scan mode with continuous conversion and circular DMA creates a fully autonomous data acquisition pipeline: the ADC converts all channels in sequence continuously, and DMA writes the results into an array in memory. The CPU is not involved until the DMA half-complete or full-complete callback fires, signalling that a fresh batch of data is available.

Circular DMA Callbacks

With circular DMA, the DMA controller wraps the write pointer back to the start of the buffer when it reaches the end — the ADC never needs to be restarted. HAL_ADC_ConvHalfCpltCallback() fires when the first half is full (useful for double-buffering: process the first half while the second is being filled). HAL_ADC_ConvCpltCallback() fires when the full buffer is complete. Always declare the DMA buffer as volatile to prevent the compiler from caching the values in registers.

/* ADC1 scan mode, 4 channels (PA0–PA3), DMA2 Stream0, circular
 * Channels: IN0 (PA0), IN1 (PA1), IN2 (PA2), IN3 (PA3)
 * Conversion rate: 21 MHz / (56+12) cycles = ~309 kSPS per channel
 *                  ÷ 4 channels = ~77 kSPS per channel in scan mode
 */

#define ADC_CH_COUNT  4

ADC_HandleTypeDef          hadc1_scan;
DMA_HandleTypeDef          hdma_adc1;
volatile uint16_t          adc_buf[ADC_CH_COUNT];

/* Friendly channel names */
#define ADC_CH_PRESSURE   adc_buf[0]
#define ADC_CH_TEMP_EXT   adc_buf[1]
#define ADC_CH_CURRENT    adc_buf[2]
#define ADC_CH_VOLTAGE    adc_buf[3]

void ADC1_ScanDMA_Init(void)
{
    ADC_ChannelConfTypeDef ch = {0};

    /* ── DMA configuration ─────────────────────────────────── */
    __HAL_RCC_DMA2_CLK_ENABLE();
    hdma_adc1.Instance                 = DMA2_Stream0;
    hdma_adc1.Init.Channel             = DMA_CHANNEL_0;
    hdma_adc1.Init.Direction           = DMA_PERIPH_TO_MEMORY;
    hdma_adc1.Init.PeriphInc           = DMA_PINC_DISABLE;
    hdma_adc1.Init.MemInc              = DMA_MINC_ENABLE;
    hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc1.Init.MemDataAlignment    = DMA_MDATAALIGN_HALFWORD;
    hdma_adc1.Init.Mode                = DMA_CIRCULAR;
    hdma_adc1.Init.Priority            = DMA_PRIORITY_HIGH;
    hdma_adc1.Init.FIFOMode            = DMA_FIFOMODE_DISABLE;
    HAL_DMA_Init(&hdma_adc1);
    __HAL_LINKDMA(&hadc1_scan, DMA_Handle, hdma_adc1);

    HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);

    /* ── GPIO: PA0–PA3 as analog ───────────────────────────── */
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitTypeDef g = {0};
    g.Pin  = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3;
    g.Mode = GPIO_MODE_ANALOG;
    g.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &g);

    /* ── ADC configuration ──────────────────────────────────── */
    __HAL_RCC_ADC1_CLK_ENABLE();
    hadc1_scan.Instance                   = ADC1;
    hadc1_scan.Init.ClockPrescaler        = ADC_CLOCK_SYNC_PCLK_DIV4;
    hadc1_scan.Init.Resolution            = ADC_RESOLUTION_12B;
    hadc1_scan.Init.ScanConvMode          = ENABLE;               /* Multi-ch */
    hadc1_scan.Init.ContinuousConvMode    = ENABLE;               /* Repeat   */
    hadc1_scan.Init.DiscontinuousConvMode = DISABLE;
    hadc1_scan.Init.ExternalTrigConvEdge  = ADC_EXTERNALTRIGCONVEDGE_NONE;
    hadc1_scan.Init.DataAlign             = ADC_DATAALIGN_RIGHT;
    hadc1_scan.Init.NbrOfConversion       = ADC_CH_COUNT;
    hadc1_scan.Init.DMAContinuousRequests = ENABLE;               /* Circular */
    hadc1_scan.Init.EOCSelection          = ADC_EOC_SEQ_CONV;
    HAL_ADC_Init(&hadc1_scan);

    /* Configure channels IN0–IN3 as rank 1–4 */
    uint32_t channels[] = {ADC_CHANNEL_0, ADC_CHANNEL_1,
                            ADC_CHANNEL_2, ADC_CHANNEL_3};
    for (int i = 0; i < ADC_CH_COUNT; i++) {
        ch.Channel      = channels[i];
        ch.Rank         = i + 1;
        ch.SamplingTime = ADC_SAMPLETIME_56CYCLES;
        HAL_ADC_ConfigChannel(&hadc1_scan, &ch);
    }

    HAL_ADC_Start_DMA(&hadc1_scan, (uint32_t *)adc_buf, ADC_CH_COUNT);
}

void DMA2_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_adc1); }

/* Called when all 4 channels have been converted (full buffer) */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) {
        /* adc_buf[0..3] are now valid and fresh — read them safely */
        uint32_t pressure_mv = ((uint32_t)ADC_CH_PRESSURE * 3300UL) / 4095UL;
        uint32_t current_ma  = ((uint32_t)ADC_CH_CURRENT  * 3300UL) / 4095UL;
        (void)pressure_mv; (void)current_ma;                /* Use in app    */
    }
}

Injected Channels

Injected channels are the ADC's emergency lane. While the regular group peacefully scans sensor channels every millisecond, the injected group can be triggered at a precise moment — synchronized to a PWM timer, for instance — to sample the motor phase current exactly at the mid-point of the PWM duty cycle when the shunt resistor current is most stable. This is the canonical technique in field-oriented control (FOC) of BLDC motors.

The injected group supports up to four channels, a dedicated JEOC (Injected End of Conversion) interrupt, and its own set of data registers (ADC_JDRx). Auto-injection mode converts the injected sequence automatically after each regular sequence completes — useful when both groups always need updating but you want strict ordering.

/* Injected channel on ADC1_IN4 (PA4) — triggered by TIM1_CC4
 * Used for phase current sampling synchronized to PWM switching
 * Regular group continues scanning other channels via DMA
 */

ADC_InjectionConfTypeDef inj_config = {0};

void ADC1_InjectedChannel_Init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitTypeDef g = {0};
    g.Pin = GPIO_PIN_4; g.Mode = GPIO_MODE_ANALOG; g.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &g);

    /* Inject channel 4 as rank 1 in the injected group */
    inj_config.InjectedChannel               = ADC_CHANNEL_4;
    inj_config.InjectedRank                  = 1;
    inj_config.InjectedNbrOfConversion       = 1;
    inj_config.InjectedSamplingTime          = ADC_SAMPLETIME_28CYCLES;
    inj_config.InjectedOffset                = 0;
    inj_config.ExternalTrigInjecConv         = ADC_EXTERNALTRIGINJECCONV_T1_CC4;
    inj_config.ExternalTrigInjecConvEdge     = ADC_EXTERNALTRIGINJECCONVEDGE_RISING;
    inj_config.AutoInjectedConv              = DISABLE; /* hw-triggered only  */
    inj_config.InjectedDiscontinuousConvMode = DISABLE;
    HAL_ADCEx_InjectedConfigChannel(&hadc1_scan, &inj_config);

    /* Enable JEOC interrupt */
    HAL_NVIC_SetPriority(ADC_IRQn, 0, 0);            /* Highest priority      */
    HAL_NVIC_EnableIRQ(ADC_IRQn);
    __HAL_ADC_ENABLE_IT(&hadc1_scan, ADC_IT_JEOC);

    HAL_ADCEx_InjectedStart(&hadc1_scan);
}

void ADC_IRQHandler(void) { HAL_ADC_IRQHandler(&hadc1_scan); }

/* JEOC fires at the precise TIM1_CC4 trigger moment */
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) {
        uint16_t raw_current = HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_1);
        /* Convert raw to current: e.g., 0.1 Ω shunt, 20× amp → 1 LSB = 0.4 mA */
        int32_t current_ma = ((int32_t)raw_current - 2048) * 4;
        (void)current_ma;                              /* Feed to FOC algorithm */
    }
}
Motor Control Insight: In three-phase inverter designs, two injected channels (phase A and phase B currents) are sampled simultaneously on ADC1 and ADC2 in dual simultaneous mode. The TIM1 update event triggers both injected sequences at the exact PWM symmetry point, giving cycle-accurate current measurements every switching period.

Analog Watchdog

The analog watchdog (AWD) is a hardware threshold comparator built into the ADC. It continuously compares every conversion result against a programmable high threshold and low threshold. If the result falls outside the window, it immediately raises the AWD interrupt — without waiting for the CPU to read any value, and without burdening any ISR with polling. For battery-powered devices, the AWD can watch the supply rail and trigger a power-down sequence the moment the voltage drops below a safe threshold.

AWD1 on STM32F4 can monitor either a single specific channel or all channels in the regular sequence. Higher-end devices (G4, H7) have AWD2 and AWD3 which can monitor multiple channels independently with separate thresholds.

/* Analog Watchdog on ADC1_IN5 (PA5) — battery voltage monitor
 * Supply rail monitored through 2:1 resistor divider → 0–3.3V represents 0–6.6V
 * Alert threshold: 3.0V battery → 1.5V at ADC pin → raw = 1861
 * Watchdog fires HAL_ADC_LevelOutOfWindowCallback when raw < 1861
 *
 * Window: low = 1861 (3.0V), high = 4095 (no upper limit)
 */

ADC_AnalogWDGConfTypeDef awd_config = {0};
volatile uint8_t battery_low_flag  = 0;

void ADC1_AnalogWatchdog_Init(void)
{
    /* Channel 5 must already be in the regular sequence (hadc1_scan) */
    awd_config.WatchdogMode    = ADC_ANALOGWATCHDOG_SINGLE_REG;
    awd_config.Channel         = ADC_CHANNEL_5;        /* PA5 = battery sense */
    awd_config.ITMode          = ENABLE;
    awd_config.HighThreshold   = 4095;                  /* No upper alert      */
    awd_config.LowThreshold    = 1861;                  /* 3.0 V battery limit */
    HAL_ADC_AnalogWDGConfig(&hadc1_scan, &awd_config);

    /* AWD shares ADC interrupt — ensure ADC_IRQn is already enabled */
    __HAL_ADC_ENABLE_IT(&hadc1_scan, ADC_IT_AWD);
}

/* Fires when any regular conversion result drops below LowThreshold */
void HAL_ADC_LevelOutOfWindowCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance == ADC1) {
        battery_low_flag = 1;
        /* Initiate graceful power-down: save state, notify host, shut down   */
        /* Do NOT perform long operations here — set a flag, handle in main() */
    }
}

/* In main loop, check flag and handle power-down */
void App_CheckBatteryFlag(void)
{
    if (battery_low_flag) {
        battery_low_flag = 0;
        printf("WARN: Battery below 3.0V — initiating safe shutdown\r\n");
        /* Save non-volatile data, send alert via UART/radio, then halt */
    }
}

Multi-ADC Modes (F4/H7)

STM32F4 devices with all three ADCs support multi-ADC master-slave modes where two or three ADCs are synchronised by a shared trigger. The most useful modes are:

  • Dual regular simultaneous: ADC1 (master) and ADC2 (slave) both start their conversions on the same trigger edge. The results are packed into a 32-bit value (ADC1 in bits 15:0, ADC2 in bits 31:16) accessible via the ADC->CDR common data register. True simultaneity to within one ADC clock cycle — critical for differential measurements and power quality analysis.
  • Dual interleaved: ADC1 and ADC2 stagger their sampling by half a conversion cycle, effectively doubling the sampling rate on a single channel. Two ADCs at 1 MSPS each → 2 MSPS effective on one channel.
  • Triple regular simultaneous: ADC1+ADC2+ADC3, three channels sampled at the identical moment. Used in three-phase current measurement without injected channels.

Dual Simultaneous Mode Implementation

/* Dual Simultaneous Mode: ADC1_IN1 (PA1) + ADC2_IN2 (PA2)
 * Both sampled at exactly the same instant on software trigger
 * Results packed in ADC->CDR: [31:16] = ADC2, [15:0] = ADC1
 * Use case: differential voltage measurement Vp-Vn with zero skew
 */

ADC_HandleTypeDef hadc1_dual, hadc2_dual;
ADC_MultiModeTypeDef multi_mode = {0};

void ADC_DualSimultaneous_Init(void)
{
    ADC_ChannelConfTypeDef ch = {0};

    __HAL_RCC_ADC1_CLK_ENABLE();
    __HAL_RCC_ADC2_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /* PA1 and PA2 as analog */
    GPIO_InitTypeDef g = {0};
    g.Pin = GPIO_PIN_1 | GPIO_PIN_2;
    g.Mode = GPIO_MODE_ANALOG; g.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &g);

    /* ADC1 — master */
    hadc1_dual.Instance                   = ADC1;
    hadc1_dual.Init.ClockPrescaler        = ADC_CLOCK_SYNC_PCLK_DIV4;
    hadc1_dual.Init.Resolution            = ADC_RESOLUTION_12B;
    hadc1_dual.Init.ScanConvMode          = DISABLE;
    hadc1_dual.Init.ContinuousConvMode    = DISABLE;
    hadc1_dual.Init.ExternalTrigConvEdge  = ADC_EXTERNALTRIGCONVEDGE_NONE;
    hadc1_dual.Init.DataAlign             = ADC_DATAALIGN_RIGHT;
    hadc1_dual.Init.NbrOfConversion       = 1;
    HAL_ADC_Init(&hadc1_dual);
    ch.Channel = ADC_CHANNEL_1; ch.Rank = 1;
    ch.SamplingTime = ADC_SAMPLETIME_56CYCLES;
    HAL_ADC_ConfigChannel(&hadc1_dual, &ch);

    /* ADC2 — slave (same config, different channel) */
    hadc2_dual.Instance = ADC2;
    hadc2_dual.Init     = hadc1_dual.Init;             /* Copy init structure  */
    HAL_ADC_Init(&hadc2_dual);
    ch.Channel = ADC_CHANNEL_2; ch.Rank = 1;
    HAL_ADC_ConfigChannel(&hadc2_dual, &ch);

    /* Multi-mode: dual regular simultaneous */
    multi_mode.Mode           = ADC_DUALMODE_REGSIMULT;
    multi_mode.DMAAccessMode  = ADC_DMAACCESSMODE_DISABLED;
    multi_mode.TwoSamplingDelay = ADC_TWOSAMPLINGDELAY_5CYCLES;
    HAL_ADCEx_MultiModeConfigChannel(&hadc1_dual, &multi_mode);
}

void ADC_DualSimultaneous_Read(uint16_t *val_adc1, uint16_t *val_adc2)
{
    HAL_ADCEx_MultiModeStart_IT(&hadc1_dual);
    HAL_ADC_PollForConversion(&hadc1_dual, 10);
    uint32_t dual = HAL_ADCEx_MultiModeGetValue(&hadc1_dual);

    *val_adc1 = (uint16_t)(dual & 0x0000FFFFUL);       /* Bits 15:0  = ADC1    */
    *val_adc2 = (uint16_t)(dual >> 16);                 /* Bits 31:16 = ADC2    */
    HAL_ADCEx_MultiModeStop_IT(&hadc1_dual);
}

DAC — Waveform Generation

The STM32F4 includes a 12-bit dual-channel DAC (DAC1 on the F407: two channels, PA4 and PA5). The output buffer can drive loads directly to ground but is relatively high-impedance at rail voltages — for driving low-impedance loads below 10 kΩ, consider disabling the output buffer and adding an external op-amp. The DAC supports three output modes:

  • Software trigger: call HAL_DAC_SetValue() to set an immediate output. Simple and deterministic for static setpoints.
  • Hardware triangle wave: the DAC hardware automatically steps the output through a triangular waveform, configurable in amplitude, with no CPU involvement.
  • DMA-driven arbitrary waveform: a timer triggers each DMA transfer, moving the next sample from a lookup table in Flash to the DAC data register. This creates any waveform shape at audio-quality rates.

Static Output

HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 2048) sets the output to exactly VDDA/2 = 1.65V. Use this for bias voltages, analog reference outputs, or calibration setpoints.

DMA Sine Wave via TIM6 Trigger

The standard pattern: a 256-point sine LUT is stored in Flash as const uint16_t. TIM6 triggers a DMA request at regular intervals. DMA transfers the next table entry to DAC_DHR12R1. The output frequency is F_sine = F_TIM6_update / 256 = (TIM6_CLK / ((PSC+1)×(ARR+1))) / 256.

/* 1 kHz Sine Wave via DAC1 Channel 1 (PA4) + DMA + TIM6 trigger
 * 256-point LUT in Flash — full 12-bit resolution
 * TIM6 fires at 256 kHz → 256 kHz / 256 points = 1 kHz sine
 * TIM6 clock = 84 MHz, PSC=0, ARR=327 → 84 MHz/328 ≈ 256 kHz
 */

#include 

#define SINE_LUT_SIZE  256

/* 256-point 12-bit sine LUT — generated at compile time */
static const uint16_t sine_lut[SINE_LUT_SIZE] = {
    /* Values computed as: (uint16_t)((sin(2π·i/256) * 0.5 + 0.5) * 4095) */
    2048, 2098, 2148, 2198, 2248, 2298, 2348, 2398,
    2447, 2496, 2545, 2594, 2642, 2690, 2737, 2784,
    2831, 2877, 2922, 2967, 3011, 3054, 3096, 3138,
    3178, 3218, 3257, 3295, 3332, 3368, 3403, 3437,
    3469, 3501, 3531, 3560, 3588, 3614, 3639, 3663,
    3686, 3707, 3727, 3745, 3762, 3778, 3793, 3806,
    3817, 3827, 3836, 3843, 3849, 3853, 3856, 3857,
    3856, 3854, 3850, 3845, 3838, 3830, 3820, 3808,
    3796, 3781, 3765, 3748, 3729, 3709, 3688, 3665,
    3641, 3616, 3589, 3561, 3533, 3503, 3471, 3439,
    3406, 3371, 3336, 3299, 3262, 3223, 3184, 3143,
    3102, 3060, 3017, 2973, 2929, 2884, 2838, 2792,
    2745, 2697, 2649, 2601, 2552, 2503, 2453, 2403,
    2353, 2303, 2252, 2202, 2151, 2100, 2050, 1999,
    1948, 1898, 1847, 1797, 1747, 1697, 1648, 1599,
    1550, 1502, 1454, 1407, 1360, 1314, 1269, 1224,
    1180, 1137, 1095, 1053, 1013,  973,  934,  896,
     859,  823,  789,  755,  723,  691,  661,  632,
     605,  578,  553,  530,  507,  486,  467,  448,
     431,  415,  401,  388,  376,  365,  356,  348,
     342,  337,  334,  331,  330,  331,  333,  337,
     342,  349,  357,  367,  378,  390,  404,  419,
     435,  453,  472,  493,  515,  538,  563,  589,
     616,  645,  675,  706,  738,  771,  806,  841,
     878,  915,  954,  994, 1034, 1076, 1118, 1161,
    1205, 1249, 1294, 1340, 1386, 1432, 1479, 1527,
    1574, 1622, 1671, 1720, 1769, 1818, 1867, 1916,
    1965, 2015, 2064, 2113, 2162, 2211, 2260, 2309,
    2048, 2048, 2048, 2048, 2048, 2048, 2048, 2048  /* pad — last group     */
};

DAC_HandleTypeDef  hdac1;
DMA_HandleTypeDef  hdma_dac1;
TIM_HandleTypeDef  htim6_dac;

void DAC1_SineWave_Init(void)
{
    DAC_ChannelConfTypeDef dac_ch = {0};

    /* ── TIM6: trigger DAC at 256 kHz ──────────────────────── */
    __HAL_RCC_TIM6_CLK_ENABLE();
    htim6_dac.Instance               = TIM6;
    htim6_dac.Init.Prescaler         = 0;
    htim6_dac.Init.Period            = 327;           /* 84 MHz / 328 ≈ 256 kHz */
    htim6_dac.Init.CounterMode       = TIM_COUNTERMODE_UP;
    htim6_dac.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_Base_Init(&htim6_dac);

    TIM_MasterConfigTypeDef mstr = {0};
    mstr.MasterOutputTrigger = TIM_TRGO_UPDATE;      /* TRGO = update event   */
    mstr.MasterSlaveMode     = TIM_MASTERSLAVEMODE_DISABLE;
    HAL_TIMEx_MasterConfigSynchronization(&htim6_dac, &mstr);

    /* ── DMA1 Stream5 CH7 — DAC1 CH1 ───────────────────────── */
    __HAL_RCC_DMA1_CLK_ENABLE();
    hdma_dac1.Instance                 = DMA1_Stream5;
    hdma_dac1.Init.Channel             = DMA_CHANNEL_7;
    hdma_dac1.Init.Direction           = DMA_MEMORY_TO_PERIPH;
    hdma_dac1.Init.PeriphInc           = DMA_PINC_DISABLE;
    hdma_dac1.Init.MemInc              = DMA_MINC_ENABLE;
    hdma_dac1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_dac1.Init.MemDataAlignment    = DMA_MDATAALIGN_HALFWORD;
    hdma_dac1.Init.Mode                = DMA_CIRCULAR;
    hdma_dac1.Init.Priority            = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_dac1);
    __HAL_LINKDMA(&hdac1, DMA_Handle1, hdma_dac1);

    /* ── DAC1 channel 1 (PA4) ───────────────────────────────── */
    __HAL_RCC_DAC_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitTypeDef g = {0};
    g.Pin = GPIO_PIN_4; g.Mode = GPIO_MODE_ANALOG; g.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &g);

    hdac1.Instance = DAC;
    HAL_DAC_Init(&hdac1);

    dac_ch.DAC_Trigger      = DAC_TRIGGER_T6_TRGO;  /* TIM6 update event     */
    dac_ch.DAC_OutputBuffer = DAC_OUTPUTBUFFER_ENABLE;
    HAL_DAC_ConfigChannel(&hdac1, &dac_ch, DAC_CHANNEL_1);

    /* Start DMA-driven waveform output */
    HAL_DAC_Start_DMA(&hdac1, DAC_CHANNEL_1,
                      (uint32_t *)sine_lut, SINE_LUT_SIZE, DAC_ALIGN_12B_R);
    HAL_TIM_Base_Start(&htim6_dac);
}
Frequency Tuning: To change the sine frequency, change the TIM6 ARR at runtime with __HAL_TIM_SET_AUTORELOAD() and set the ARPE preload flag so the change takes effect at the next update event. The DMA circular transfer continues uninterrupted — no glitch in the output waveform. F_output = 84 MHz / ((ARR+1) × 256).

Exercises

Exercise 1 Beginner

Potentiometer-Controlled PWM Dimmer

Connect a 10 kΩ potentiometer wiper to PA0 (ADC1_IN0). Read the potentiometer every 100 ms using polling mode (HAL_ADC_Start / PollForConversion / GetValue). Convert the 12-bit raw value to voltage in millivolts and print it over UART. Then use the same raw ADC value to set the duty cycle on TIM3_CH1 (from Part 4): scale the 12-bit reading 0–4095 to a CCR value of 0 to ARR. Verify that turning the potentiometer smoothly varies LED brightness with no visible steps or flicker. Note the minimum number of UART print calls per second before UART throughput limits responsiveness.

ADC Polling PWM Duty Cycle UART Output HAL_ADC_GetValue
Exercise 2 Intermediate

4-Channel DMA Scan with RMS Calculation

Configure ADC1 in scan + continuous + DMA circular mode reading 4 channels at 10 kSPS per channel (40 kSPS total). Allocate a DMA buffer of 512 uint16_t values (128 per channel, interleaved). In the DMA half-complete callback, compute the RMS of the previous 128 samples of channel 0: RMS = sqrt(sum(x²)/128). Send the result over UART as a floating-point value. Repeat for the full-complete callback processing the second half. Verify the RMS matches the expected value from a known signal source (e.g., 1 Vpp sine from the DAC). Profile the RMS calculation with DWT to ensure it completes in well under 128 sample periods.

Scan Mode DMA Circular Buffer RMS Calculation Double Buffering
Exercise 3 Advanced

2-Channel Oscilloscope with Dual Simultaneous ADC

Configure ADC1 + ADC2 in dual regular simultaneous mode, sampling at 1 MSPS per channel. Allocate a 2,000-element uint32_t DMA buffer where each word contains ADC1 (bits 15:0) and ADC2 (bits 31:16). Implement a rising-edge software trigger: continuously scan the buffer for a sample where ADC1 crosses above a user-defined threshold. When triggered, capture 1,000 simultaneous sample pairs and send them over UART as CSV lines ch1_mv,ch2_mv\n. Verify the two channels are truly simultaneous by connecting a 100 kHz signal to both ADC inputs through different-length cables and checking for zero phase difference in the CSV data. Calculate the theoretical maximum export rate in samples per UART transmission at 115200 baud.

Dual Simultaneous ADC Trigger Detection UART CSV Export 1 MSPS Oscilloscope

ADC & DAC Configuration Tool

Use this tool to document your STM32 ADC and DAC configuration — ADC instance, resolution, conversion mode, channel list with sampling times, analog watchdog thresholds, and DAC waveform settings. Download as Word, Excel, PDF, or PPTX for project documentation or hardware review.

STM32 ADC & DAC Configuration Generator

Document your ADC/DAC setup — instance, resolution, channels, watchdog, and waveform type. Download as Word, Excel, PDF, or PPTX.

Draft auto-saved

All data stays in your browser. Nothing is sent to or stored on any server.

Conclusion & Next Steps

In this article we covered the STM32 ADC and DAC from first principles through production patterns:

  • ADC architecture: four selectable resolutions with a clear throughput/precision tradeoff, the succession approximation principle, ADC clock derivation from APB2, and the 16-external + 3-internal channel structure. Regular vs injected groups establish the foundation for all conversion modes.
  • Sampling time: the RC charging model determines the minimum sampling cycles for a given source impedance. The temperature sensor and VREFINT always require the maximum 480-cycle setting. The factory calibration values in Flash enable accurate temperature calculation without additional hardware.
  • Scan mode with DMA: continuous + scan + circular DMA creates a zero-CPU autonomous data acquisition pipeline. Half-complete and full-complete callbacks enable double-buffering for uninterrupted processing while the DMA fills the other half.
  • Injected channels: hardware-preemptive, timer-triggered sampling for motor phase current, overcurrent detection, and any safety-critical measurement that must occur at a precise moment in the system timeline.
  • Analog watchdog: a hardware comparator within the ADC that raises an interrupt without CPU polling the moment a voltage crosses a threshold — the right tool for battery monitoring, overvoltage protection, and fault detection.
  • Multi-ADC modes: dual simultaneous mode achieves true simultaneous sampling of two channels to within one ADC clock cycle — critical for differential measurements, power analysis, and motor control current sensing.
  • DAC waveform generation: from static setpoints through hardware triangle and noise waves to DMA-driven arbitrary waveforms using a 256-point sine LUT clocked by TIM6, delivering CPU-free continuous output at audio-quality sample rates.

Next in the Series

In Part 6: SPI Protocol, we master SPI master and slave configuration, full-duplex DMA transfers for maximum throughput, NSS hardware management, and build a complete driver for an SPI NOR Flash memory device and an SPI-connected LCD display — covering every practical SPI use case in embedded systems development.

Technology