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.
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 |
| 3 | 143 ns | <1 kΩ | Low-impedance voltage divider, op-amp buffered |
| 15 | 714 ns | ~5 kΩ | Resistor divider with buffer, current sense amp |
| 28 | 1.33 µs | ~10 kΩ | Standard resistor divider |
| 56 | 2.67 µs | ~22 kΩ | General purpose, sensor outputs |
| 84 | 4.0 µs | ~34 kΩ | High-impedance sensors |
| 112 | 5.33 µs | ~45 kΩ | Photodiode, pH sensor |
| 144 | 6.86 µs | ~60 kΩ | Very high impedance sources |
| 480 | 22.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 */
}
}
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).