Part 2 · C Meets the Metal35 min read
ADC, DAC, and Analog Basics
Every sensor that is not digital ends up as a voltage on a pin: a thermistor divider, a current-sense amplifier, a battery monitor, a microphone. The ADC (analog-to-digital converter) turns that voltage into a number, and the DAC turns numbers back into a voltage. In this lesson you will learn what limits an ADC reading’s accuracy, configure the STM32F407’s ADC1 for single conversions and then for timer-triggered multi-channel scanning into memory with DMA, read the internal temperature sensor and reference, filter the results in fixed-point arithmetic, and output a sine wave from the DAC. The reasoning about source impedance and fixed-point maths applies to every microcontroller you will use.
Sampling in one page
Section titled “Sampling in one page”An ADC compares the input voltage against a reference voltage VREF and reports where the input falls as an integer. With an N-bit converter there are 2^N codes:
code = round(Vin / VREF * (2^N - 1)) 12-bit: 0 .. 4095
VREF = 3.3 V, N = 12: 1 LSB = 3.3 V / 4096 = 0.806 mV Vin = 1.000 V -> 1.000 / 3.3 * 4095 = 1240.9 -> code 1241Resolution is the number of bits. LSB size is the voltage one code step represents. Quantization error is the rounding: even a perfect converter is off by up to half an LSB. Real converters add offset error, gain error and nonlinearity of a few LSB each, plus noise. A 12-bit ADC typically gives you 10 to 11 useful bits; the datasheet’s ENOB (effective number of bits) figure tells you how many.
The reference matters as much as the converter. On the STM32F407 the reference is the VREF+ pin, which on most boards (including the Discovery) is tied to the analog supply VDDA. If VDDA is 2.95 V and your code assumes 3.3 V, every reading is 12% wrong. You will fix this with the internal reference later.
Sampling rate is how often you convert. The Nyquist criterion says that to represent a signal you must sample faster than twice its highest frequency component. Any content above half the sample rate does not disappear; it aliases, folding down and appearing as a false lower frequency. Sample a 900 Hz tone at 1 kHz and you see a 100 Hz tone that does not exist. Software cannot remove aliasing after the fact. The only fix is an analog low-pass filter (often a simple RC) before the ADC, which limits the input bandwidth to below half the sample rate.
The ADC input model and source impedance
Section titled “The ADC input model and source impedance”The STM32’s ADC is a successive-approximation (SAR) converter with a sample-and-hold front end. During the sampling time a switch connects the pin to a small capacitor inside the chip. Then the switch opens, and the converter spends 12 more ADC clock cycles comparing the frozen capacitor voltage against the reference, one bit at a time.
source STM32 pin inside the ADC Vsrc --[ R_source ]---------+---------[ R_ADC ~6k ]--/ switch --+-- to SAR | | (pin + PCB C_ADC ~4 pF capacitance) | GNDThe capacitor must charge to within a fraction of an LSB of the input during the sampling window. That is an RC charging problem with time constant tau = (R_source + R_ADC) * C_ADC. To settle within 1/4 LSB at 12 bits you need about ln(2^14), roughly 9.7 time constants.
Consider a battery monitor with a 1 MΩ / 1 MΩ divider, chosen to waste only 2 µA:
tau = 0.5 MΩ * 4 pF = 2 µs(the Thevenin resistance of the divider is 500 kΩ), so you need about 20 µs of sampling.- At a 36 MHz ADC clock, even the longest sampling time of 480 cycles is 13 µs. Not enough.
- Worse, the pin’s leakage current is specified up to about ±1 µA. Through 500 kΩ that is up to 0.5 V of error, far more than any settling problem.
- And the sampling capacitor holds the previous channel’s voltage, so each conversion kicks charge back into the high-impedance node.
The result reads “garbage that depends on what the previous channel was”. The fixes: lower the divider (100 kΩ total is common), put a capacitor of 10 to 100 nF from the ADC pin to ground so it acts as a charge reservoir for the sampling capacitor (at the cost of input bandwidth), or buffer the signal with an op-amp. The STM32 datasheet gives the exact formula for the maximum source resistance per sampling time; use it whenever your source is more than a few kΩ.
ADC1 on the STM32F407
Section titled “ADC1 on the STM32F407”ADC1 is on APB2 at 0x40012000, clock enable RCC_APB2ENR bit 8. The three ADCs share a common block at 0x40012300 whose CCR register (at 0x40012304) holds the clock prescaler and the internal sensor enables.
| Offset | Register | Fields used here |
|---|---|---|
0x00 | SR | EOC (1) end of conversion, OVR (5) overrun |
0x04 | CR1 | SCAN (8), RES (25:24) resolution |
0x08 | CR2 | ADON (0), CONT (1), DMA (8), DDS (9), EXTSEL (27:24), EXTEN (29:28), SWSTART (30) |
0x0C | SMPR1 | sampling time, channels 10 to 18, 3 bits each |
0x10 | SMPR2 | sampling time, channels 0 to 9, 3 bits each |
0x2C | SQR1 | L (23:20) = number of conversions minus 1 |
0x30 | SQR2 | sequence positions 7 to 12 |
0x34 | SQR3 | sequence positions 1 to 6, 5 bits each |
0x4C | DR | result (reading clears EOC) |
common 0x04 | CCR | ADCPRE (17:16), TSVREFE (23) |
Sampling time codes for SMPRx: 000 = 3 cycles, 001 = 15, 010 = 28, 011 = 56, 100 = 84, 101 = 112, 110 = 144, 111 = 480. Total conversion time is sampling time plus 12 cycles at 12-bit resolution.
The ADC clock comes from APB2 through ADCPRE (/2, /4, /6, /8) and must not exceed 36 MHz (at VDDA of 2.4 V or more). At the boot clock of 16 MHz, /2 gives 8 MHz. At 168 MHz with APB2 = 84 MHz, /4 gives 21 MHz (the safe choice); /2 would give 42 MHz, over the limit, and the results degrade without any error flag.
Add these to the register header:
typedef struct { volatile uint32_t SR; /* 0x00 */ volatile uint32_t CR1; /* 0x04 */ volatile uint32_t CR2; /* 0x08 */ volatile uint32_t SMPR1; /* 0x0C */ volatile uint32_t SMPR2; /* 0x10 */ volatile uint32_t JOFR[4]; /* 0x14 - 0x20 */ volatile uint32_t HTR; /* 0x24 */ volatile uint32_t LTR; /* 0x28 */ volatile uint32_t SQR1; /* 0x2C */ volatile uint32_t SQR2; /* 0x30 */ volatile uint32_t SQR3; /* 0x34 */ volatile uint32_t JSQR; /* 0x38 */ volatile uint32_t JDR[4]; /* 0x3C - 0x48 */ volatile uint32_t DR; /* 0x4C */} ADC_TypeDef;
typedef struct { volatile uint32_t CSR; /* 0x00 */ volatile uint32_t CCR; /* 0x04 */ volatile uint32_t CDR; /* 0x08 */} ADC_Common_TypeDef;
typedef struct { volatile uint32_t CR; /* 0x00 */ volatile uint32_t SWTRIGR; /* 0x04 */ volatile uint32_t DHR12R1; /* 0x08 */ volatile uint32_t DHR12L1; /* 0x0C */ volatile uint32_t DHR8R1; /* 0x10 */ volatile uint32_t DHR12R2; /* 0x14 */ volatile uint32_t DHR12L2; /* 0x18 */ volatile uint32_t DHR8R2; /* 0x1C */ volatile uint32_t DHR12RD; /* 0x20 */ volatile uint32_t DHR12LD; /* 0x24 */ volatile uint32_t DHR8RD; /* 0x28 */ volatile uint32_t DOR1; /* 0x2C */ volatile uint32_t DOR2; /* 0x30 */ volatile uint32_t SR; /* 0x34 */} DAC_TypeDef;
#define ADC1 ((ADC_TypeDef *)0x40012000U)#define ADC123_COMMON ((ADC_Common_TypeDef *)0x40012300U)#define DAC ((DAC_TypeDef *)0x40007400U)
#ifndef TIM2typedef struct { volatile uint32_t CR1; /* 0x00 */ volatile uint32_t CR2; /* 0x04 */ volatile uint32_t SMCR; /* 0x08 */ volatile uint32_t DIER; /* 0x0C */ volatile uint32_t SR; /* 0x10 */ volatile uint32_t EGR; /* 0x14 */ volatile uint32_t CCMR1; /* 0x18 */ volatile uint32_t CCMR2; /* 0x1C */ volatile uint32_t CCER; /* 0x20 */ volatile uint32_t CNT; /* 0x24 */ volatile uint32_t PSC; /* 0x28 */ volatile uint32_t ARR; /* 0x2C */} TIM_TypeDef;#define TIM2 ((TIM_TypeDef *)0x40000000U)#endif#ifndef TIM6#define TIM6 ((TIM_TypeDef *)0x40001000U)#endif
#define RCC_APB1ENR_TIM2EN (1u << 0)#define RCC_APB1ENR_TIM6EN (1u << 4)#define RCC_APB1ENR_DACEN (1u << 29)#define RCC_APB2ENR_ADC1EN (1u << 8)If your header already has a fuller TIM_TypeDef from Timers and PWM, the guard skips this one; the offsets used here (CR1, CR2, EGR, PSC, ARR) are the same. The DMA overlay comes from the DMA lesson, and gpio_util.h and console.c from SPI and I2C.
A single conversion, polled
Section titled “A single conversion, polled”The minimal case: one channel, start by software, wait for the result. PA1 is ADC channel 1 (ADC123_IN1) and is free on the Discovery. Wire a potentiometer between the board’s 3V and GND pins with the wiper on PA1.
#include <stdint.h>#include "stm32f407_regs.h"#include "gpio_util.h"
#define ADC_SR_EOC (1u << 1)#define ADC_CR2_ADON (1u << 0)#define ADC_CR2_SWSTART (1u << 30)
void adc1_init_single(void);uint16_t adc1_read(void);
void adc1_init_single(void){ RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; (void)RCC->APB2ENR;
gpio_mode(GPIOA, 1u, GPIO_ANALOG); /* disconnect the digital input */
ADC123_COMMON->CCR = 0u; /* ADCPRE = /2: 8 MHz ADC clock */ ADC1->CR1 = 0u; /* 12-bit, single channel */ ADC1->SMPR2 = 7u << (1u * 3u); /* channel 1: 480 cycles */ ADC1->SQR1 = 0u; /* L = 0: one conversion */ ADC1->SQR3 = 1u; /* first conversion: channel 1 */ ADC1->CR2 = ADC_CR2_ADON; /* power up */ for (volatile uint32_t i = 0; i < 1000u; i++) { } /* t_STAB, a few us */}
uint16_t adc1_read(void){ ADC1->CR2 |= ADC_CR2_SWSTART; while ((ADC1->SR & ADC_SR_EOC) == 0u) { } return (uint16_t)ADC1->DR; /* reading DR clears EOC */}Three details worth naming. Setting the pin to analog mode (MODER = 11) disconnects the Schmitt-trigger input; a digital input buffer sitting at mid-rail draws extra current and injects noise. ADON powers up the converter, which needs a short stabilization time before the first conversion. And the sequence registers are how you say “which channel”: even for a single conversion, SQR3 bits 4:0 hold the channel number of conversion 1.
At 8 MHz, 480 + 12 cycles takes 61.5 µs. Generous sampling time is the right default until you need speed; it tolerates source impedances that 3 cycles would not.
Internal channels: temperature and VREFINT
Section titled “Internal channels: temperature and VREFINT”Two internal channels are invaluable. Set TSVREFE (bit 23 of the common CCR) to connect them.
Channel 16, temperature sensor. A diode-based sensor whose voltage rises with temperature. The datasheet formula:
T (C) = (V_SENSE - V_25) / Avg_Slope + 25V_25 = 0.76 V typical, Avg_Slope = 2.5 mV/C typicalThe “typical” values vary from chip to chip by up to ±45 °C of offset in the worst case, so the raw formula is only good for detecting a change in temperature. ST measures each chip at the factory and stores the ADC reading at 30 °C (TS_CAL1) and 110 °C (TS_CAL2) in system memory, taken with VDDA = 3.3 V. A two-point linear interpolation between those gives a few degrees of accuracy. The sensor needs a sampling time of at least 10 µs. It measures the die, which runs warmer than the air around it.
Channel 17, VREFINT. An internal bandgap reference of about 1.21 V, stable over supply voltage. Its factory reading at VDDA = 3.3 V is stored as VREFINT_CAL. Because VREFINT is fixed and the ADC measures relative to VDDA, measuring it tells you the actual supply:
VDDA = 3.3 V * VREFINT_CAL / VREFINT_DATAIf VREFINT_CAL is 1500 and you now read 1678, then VDDA = 3.3 * 1500 / 1678 = 2.95 V. Every other channel’s millivolt conversion should use this measured VDDA, not an assumed 3.3 V. This is how battery-powered products track their supply without any external part.
On the STM32F405/407 the calibration values live at 0x1FFF7A2A (VREFINT_CAL), 0x1FFF7A2C (TS_CAL1) and 0x1FFF7A2E (TS_CAL2), each 16 bits. Confirm the addresses in the datasheet for your exact part number; other STM32 families store them elsewhere.
Scanning many channels with DMA
Section titled “Scanning many channels with DMA”Real products sample several channels continuously. The ADC’s scan mode converts a sequence of up to 16 channels per trigger, in the order given by SQR3, SQR2, SQR1, with L setting the length. There is only one DR, so each result must be moved out before the next conversion overwrites it. If it is not, OVR sets and the ADC stops. This is exactly the job DMA was made for.
The configuration:
CR1.SCAN = 1.SQR1.L = N - 1, channels inSQR3/SQR2.CR2.DMA = 1so each end of conversion raises a DMA request, andCR2.DDS = 1so requests continue after the first sequence (withoutDDSthe ADC stops requesting after one DMA buffer, which breaks circular mode).- DMA2 Stream 0, channel 0, peripheral-to-memory, half-word sizes,
MINC, circular,NDTR= buffer length in samples.
The DMA writes results into the buffer in sequence order, so with three channels the buffer is interleaved: ch1, ch16, ch17, ch1, ch16, ch17, .... Make the buffer length a multiple of the channel count, otherwise the interleaving shifts every time the buffer wraps.
Timer-triggered sampling
Section titled “Timer-triggered sampling”Continuous mode (CONT = 1) converts back to back as fast as the ADC can go, at a rate set by ADC clock and sampling times. That is rarely what you want. Filtering, frequency analysis and control loops all assume samples are evenly spaced at a known rate. The right way is to let a timer trigger each scan.
CR2.EXTSEL chooses the trigger source and CR2.EXTEN its edge (01 = rising). EXTSEL = 0110 selects TIM2 TRGO. Configure TIM2 to emit TRGO on every update event (CR2.MMS = 010) and its period sets the sample rate with crystal accuracy. No CPU involvement at all: the timer triggers the ADC, the ADC triggers the DMA, the DMA fills memory, and the CPU is told every half buffer.
TIM2 update (1 kHz) ---TRGO---> ADC1 scan [IN1, IN16, IN17] | EOC x3 per scan v DMA2 Stream 0 (circular) | adc_buf: [ first half: 16 scans | second half: 16 scans ] ^ HT irq ^ TC irqOversampling and decimation
Section titled “Oversampling and decimation”If the input has at least about 1 LSB of noise on it (it usually does), averaging 4^n samples and then dividing by 2^n (not 4^n) gives n extra bits of resolution. Sixteen 12-bit samples summed give a value from 0 to 65520; shifting right by 2 gives a 14-bit result. The noise acts as dither: the fractional position of the true voltage between two codes shows up as the proportion of samples that land on the upper code.
This costs sample rate (16 times fewer output samples) and only helps with random noise. It cannot fix nonlinearity or offset, and on a perfectly quiet signal every sample is identical and you gain nothing.
Calibration: offset and gain
Section titled “Calibration: offset and gain”The STM32F4 ADC has no self-calibration routine (later families such as the F3 and L4 do). For better than a few LSB absolute accuracy, do a two-point calibration in production: apply two known voltages, record the codes, and store an offset and a gain correction in flash.
typedef struct { int32_t offset; /* code measured at 0 V */ int32_t gain_q16; /* (ideal span / measured span) in Q16.16 */} adc_cal_t;
static int32_t adc_correct(const adc_cal_t *cal, int32_t raw){ int64_t v = (int64_t)(raw - cal->offset) * cal->gain_q16; return (int32_t)(v / 65536); /* division, not >>, rounds negatives toward 0 */}The gain is stored in Q16.16 fixed point: the real value times 65536. A gain of 1.0125 is stored as 66355. The next section explains why integers are used instead of float.
Fixed-point arithmetic for sensor maths
Section titled “Fixed-point arithmetic for sensor maths”The Cortex-M4F has a single-precision FPU, so float is cheap there. But many MCUs (Cortex-M0, M3, most 8-bit parts) have no FPU, every double is software-emulated even on the M4F, and float code in an ISR adds the lazy-stacking cost of the FPU registers. Integer arithmetic is exact, fast everywhere, and deterministic. Sensor data is integer to begin with.
Fixed point means “an integer that implicitly carries a scale factor”. The Q notation Qm.n says there are m integer bits and n fraction bits; the value is stored / 2^n.
| Format | Storage | Range | Resolution | Typical use |
|---|---|---|---|---|
| Q15 (Q0.15) | int16_t | -1 to 0.99997 | 3.05e-5 | audio, DSP coefficients |
| Q31 | int32_t | -1 to 1 | 4.7e-10 | high-precision DSP |
| Q16.16 | int32_t | ±32768 | 1.5e-5 | gains, general maths |
| integer mV, 0.1 °C | int32_t | as needed | 1 unit | sensor outputs |
The rules:
- Addition of two numbers in the same Q format is plain integer addition.
- Multiplication of Qa by Qb gives Q(a+b): multiply in a wider type, then shift right by the fraction bits you want to drop. Two Q15 values:
(int32_t)a * b >> 15gives Q15. - Rounding: add half of the dropped weight before shifting,
((int32_t)a * b + (1 << 14)) >> 15. Without it you always round down, an accumulating bias. - Overflow: decide the maximum value of every intermediate before you write the expression.
uint32_tholds 4.29e9;4095 * 3300is 13.5e6, fine;65520 * 3600 * 16is 3.8e9, uncomfortably close. - Right shift of a negative number is implementation-defined in C (GCC does an arithmetic shift). Where values can be negative, prefer division by a power of two or keep the quantities unsigned.
The most common conversion: a 12-bit reading to millivolts.
uint32_t mv_float = (uint32_t)(raw * 3300.0f / 4095.0f); /* needs FPU or soft-float */uint32_t mv_int = (raw * 3300u + 2047u) / 4095u; /* exact, rounded */uint32_t mv_recip = (raw * 52813u) >> 16; /* 3300/4095 in Q16 */The integer version computes the product first (13.5e6 at most) so no precision is lost, then divides with rounding. The reciprocal version replaces the division with a multiply and shift: 52813 is 3300 / 4095 * 65536 rounded. On cores without a hardware divider (Cortex-M0) that is much faster; the cost is up to 1 mV of truncation error. Pick the scale so the intermediate fits, compute the constant once, and comment it.
Filtering
Section titled “Filtering”Readings are noisy. Three filters cover most firmware needs.
Moving average over the last N samples, kept as a running sum with a ring buffer so each update is O(1):
#define MA_LEN 8u
typedef struct { uint16_t buf[MA_LEN]; uint32_t sum; uint32_t idx;} movavg_t;
static uint16_t movavg_update(movavg_t *m, uint16_t x){ m->sum = m->sum + x - m->buf[m->idx]; m->buf[m->idx] = x; m->idx = (m->idx + 1u) % MA_LEN; return (uint16_t)(m->sum / MA_LEN);}It removes random noise well and has a delay of N/2 samples. With a power-of-two length the compiler turns the modulo and division into masks and shifts.
Exponential (IIR) filter, the classic y += (x - y) >> k. Each new sample moves the output a fraction 1 / 2^k of the way toward the input. Written naively in integers, the output gets stuck up to 2^k - 1 codes away from a steady input, because the small differences shift to zero. The fix is to keep the state scaled up by 2^k:
typedef struct { uint32_t acc; /* y * 2^k */ uint32_t k;} ema_t;
static uint32_t ema_update(ema_t *f, uint32_t x){ f->acc = f->acc - (f->acc >> f->k) + x; /* acc += x - y */ return f->acc >> f->k;}acc - acc/2^k + x is the same recurrence multiplied by 2^k, all unsigned, no negative shifts, no stuck output. One word of state, two operations, no buffer. Time constant is about 2^k samples; k = 3 smooths over roughly 8. Initialize acc to the first sample shifted left by k to avoid a slow ramp from zero.
Median of 3 removes single-sample spikes (an ESD glitch, a motor switching transient) that an average would smear into its output:
static uint16_t median3(uint16_t a, uint16_t b, uint16_t c){ if (a > b) { uint16_t t = a; a = b; b = t; } /* now a <= b */ if (b > c) { b = c; } /* b = min(b, c) */ return (a > b) ? a : b; /* max(a, min(b, c)) */}A median of 3 followed by an exponential filter is a robust default chain for slow sensors.
The DAC
Section titled “The DAC”The F407 has a two-channel 12-bit DAC at 0x40007400 on APB1 (RCC_APB1ENR bit 29). Channel 1 outputs on PA4, channel 2 on PA5.
| Offset | Register | Fields |
|---|---|---|
0x00 | CR | EN1 (0), BOFF1 (1) buffer off, TEN1 (2) trigger enable, TSEL1 (5:3), DMAEN1 (12) |
0x04 | SWTRIGR | software trigger |
0x08 | DHR12R1 | 12-bit right-aligned data for channel 1 |
0x2C | DOR1 | value currently output |
VOUT = VREF * DOR / 4095. Without a trigger (TEN1 = 0), a write to DHR12R1 reaches the output one APB clock later. With a trigger enabled, the value waits in the holding register until the trigger event, which gives perfectly timed updates. TSEL1 = 000 selects the TIM6 TRGO event. The output buffer (enabled when BOFF1 = 0) lets the pin drive a load, but it cannot swing all the way to the rails; expect the bottom and top couple of hundred millivolts to clip.
To generate a waveform without the CPU: a sample table in flash, TIM6 producing TRGO at the sample rate, the DAC triggered by TIM6 with DMAEN1 set, and a DMA stream (DMA1 Stream 5, channel 7, for DAC channel 1) copying the next table entry into DHR12R1 on each trigger. A 64-entry sine table at 64 kHz gives a 1 kHz sine.
The complete example
Section titled “The complete example”This program samples PA1, the temperature sensor and VREFINT at 1 kHz using TIM2, the ADC in scan mode, and DMA2 in circular mode with half and complete interrupts. Each half buffer of 16 scans is summed per channel (oversampling). The main loop measures VDDA, converts PA1 to millivolts, filters it, and computes the die temperature from the factory calibration, all in integer arithmetic. Meanwhile the DAC plays a 1 kHz sine on PA4. The DMA interrupt handler name, DMA2_Stream0_IRQHandler, is IRQ 56.
#include <stdbool.h>#include <stdint.h>#include <stdio.h>#include "stm32f407_regs.h"#include "gpio_util.h"
void console_init(void);
/* ---- ADC, DAC, timer bits ---- */#define ADC_CR1_SCAN (1u << 8)#define ADC_CR2_ADON (1u << 0)#define ADC_CR2_DMA (1u << 8)#define ADC_CR2_DDS (1u << 9)#define ADC_CR2_EXTSEL_Pos 24u#define ADC_CR2_EXTEN_Pos 28u#define ADC_CCR_TSVREFE (1u << 23)#define DAC_CR_EN1 (1u << 0)#define DAC_CR_TEN1 (1u << 2)#define DAC_CR_TSEL1_Pos 3u#define DAC_CR_DMAEN1 (1u << 12)#define TIM_CR1_CEN (1u << 0)#define TIM_CR2_MMS_UPDATE (2u << 4)#define TIM_EGR_UG (1u << 0)
/* ---- DMA bits (see DMA) ---- */#define DMA_SxCR_EN (1u << 0)#define DMA_SxCR_TEIE (1u << 2)#define DMA_SxCR_HTIE (1u << 3)#define DMA_SxCR_TCIE (1u << 4)#define DMA_SxCR_DIR_P2M (0u << 6)#define DMA_SxCR_DIR_M2P (1u << 6)#define DMA_SxCR_CIRC (1u << 8)#define DMA_SxCR_MINC (1u << 10)#define DMA_SxCR_PSIZE_16 (1u << 11)#define DMA_SxCR_MSIZE_16 (1u << 13)#define DMA_SxCR_PL_HIGH (2u << 16)#define DMA_SxCR_CHSEL(n) ((uint32_t)(n) << 25)#define DMA_TEIF (1u << 3)#define DMA_HTIF (1u << 4)#define DMA_TCIF (1u << 5)#define DMA_ALLF 0x3Du /* FEIF, DMEIF, TEIF, HTIF, TCIF */#define S5_SHIFT 6u /* stream 5 group in HISR */
/* ---- Factory calibration, STM32F405/407 (confirm in your datasheet) ---- */#define VREFINT_CAL (*(const volatile uint16_t *)0x1FFF7A2Au)#define TS_CAL1 (*(const volatile uint16_t *)0x1FFF7A2Cu) /* 30 C */#define TS_CAL2 (*(const volatile uint16_t *)0x1FFF7A2Eu) /* 110 C */
#define NCH 3u /* IN1 (PA1), IN16 (temperature), IN17 (VREFINT) */#define SCANS 16u /* scans per half buffer: 4^2 -> 2 extra bits */
static volatile uint16_t adc_buf[2u * SCANS * NCH];static volatile uint32_t adc_sum[NCH];static volatile uint32_t adc_halves;static volatile uint32_t adc_errors;
static const uint16_t sine64[64] = { 2048, 2249, 2447, 2642, 2831, 3013, 3185, 3347, 3495, 3630, 3750, 3853, 3939, 4007, 4056, 4085, 4095, 4085, 4056, 4007, 3939, 3853, 3750, 3630, 3495, 3347, 3185, 3013, 2831, 2642, 2447, 2249, 2048, 1847, 1649, 1454, 1265, 1083, 911, 749, 601, 466, 346, 243, 157, 89, 40, 11, 1, 11, 40, 89, 157, 243, 346, 466, 601, 749, 911, 1083, 1265, 1454, 1649, 1847,};
typedef struct { uint32_t acc; uint32_t k;} ema_t;
static uint32_t ema_update(ema_t *f, uint32_t x){ f->acc = f->acc - (f->acc >> f->k) + x; return f->acc >> f->k;}
static void nvic_enable(uint32_t irq, uint32_t prio){ volatile uint8_t *ipr = (volatile uint8_t *)0xE000E400u; volatile uint32_t *iser = (volatile uint32_t *)0xE000E100u; ipr[irq] = (uint8_t)(prio << 4); iser[irq >> 5] = 1u << (irq & 31u);}
static void adc_process(const volatile uint16_t *p){ uint32_t s[NCH] = { 0u, 0u, 0u }; for (uint32_t i = 0; i < SCANS; i++) { for (uint32_t c = 0; c < NCH; c++) { s[c] += p[i * NCH + c]; } } for (uint32_t c = 0; c < NCH; c++) { adc_sum[c] = s[c]; } adc_halves++;}
void DMA2_Stream0_IRQHandler(void){ uint32_t f = DMA2->LISR & DMA_ALLF; /* stream 0 group at bit 0 */ DMA2->LIFCR = f; if ((f & DMA_TEIF) != 0u) { adc_errors++; } if ((f & DMA_HTIF) != 0u) { adc_process(&adc_buf[0]); } if ((f & DMA_TCIF) != 0u) { adc_process(&adc_buf[SCANS * NCH]); }}
static void adc_dma_init(void){ RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_DMA2EN; RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; RCC->APB1ENR |= RCC_APB1ENR_TIM2EN; (void)RCC->APB1ENR;
gpio_mode(GPIOA, 1u, GPIO_ANALOG);
/* DMA2 Stream 0, channel 0: ADC1 DR -> adc_buf, circular, half-words */ DMA2_Stream0->CR = 0u; while ((DMA2_Stream0->CR & DMA_SxCR_EN) != 0u) { } DMA2->LIFCR = DMA_ALLF; DMA2_Stream0->PAR = (uint32_t)(uintptr_t)&ADC1->DR; DMA2_Stream0->M0AR = (uint32_t)(uintptr_t)adc_buf; DMA2_Stream0->NDTR = 2u * SCANS * NCH; DMA2_Stream0->FCR = 0u; DMA2_Stream0->CR = DMA_SxCR_CHSEL(0) | DMA_SxCR_DIR_P2M | DMA_SxCR_MINC | DMA_SxCR_CIRC | DMA_SxCR_PSIZE_16 | DMA_SxCR_MSIZE_16 | DMA_SxCR_PL_HIGH | DMA_SxCR_HTIE | DMA_SxCR_TCIE | DMA_SxCR_TEIE; DMA2_Stream0->CR |= DMA_SxCR_EN;
/* ADC1: scan IN1, IN16, IN17, 480-cycle sampling, TIM2 TRGO trigger */ ADC123_COMMON->CCR = ADC_CCR_TSVREFE; /* ADCPRE = /2, sensors on */ ADC1->CR2 = ADC_CR2_ADON; for (volatile uint32_t i = 0; i < 1000u; i++) { } ADC1->CR1 = ADC_CR1_SCAN; ADC1->SMPR2 = 7u << (1u * 3u); ADC1->SMPR1 = (7u << ((16u - 10u) * 3u)) | (7u << ((17u - 10u) * 3u)); ADC1->SQR1 = (NCH - 1u) << 20; ADC1->SQR3 = 1u | (16u << 5) | (17u << 10); ADC1->CR2 = ADC_CR2_ADON | ADC_CR2_DMA | ADC_CR2_DDS | (6u << ADC_CR2_EXTSEL_Pos) /* 0110: TIM2 TRGO */ | (1u << ADC_CR2_EXTEN_Pos); /* rising edge */
nvic_enable(56u, 6u); /* DMA2_Stream0 */
/* TIM2: 16 MHz / 16 = 1 MHz, / 1000 = 1 kHz update -> TRGO */ TIM2->PSC = 15u; TIM2->ARR = 999u; TIM2->CR2 = TIM_CR2_MMS_UPDATE; TIM2->EGR = TIM_EGR_UG; /* load PSC now */ TIM2->CR1 = TIM_CR1_CEN;}
static void dac_sine_init(void){ RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_DMA1EN; RCC->APB1ENR |= RCC_APB1ENR_DACEN | RCC_APB1ENR_TIM6EN; (void)RCC->APB1ENR;
gpio_mode(GPIOA, 4u, GPIO_ANALOG);
/* DMA1 Stream 5, channel 7: sine64 (flash) -> DAC DHR12R1, circular */ DMA1_Stream5->CR = 0u; while ((DMA1_Stream5->CR & DMA_SxCR_EN) != 0u) { } DMA1->HIFCR = DMA_ALLF << S5_SHIFT; DMA1_Stream5->PAR = (uint32_t)(uintptr_t)&DAC->DHR12R1; DMA1_Stream5->M0AR = (uint32_t)(uintptr_t)sine64; DMA1_Stream5->NDTR = 64u; DMA1_Stream5->FCR = 0u; DMA1_Stream5->CR = DMA_SxCR_CHSEL(7) | DMA_SxCR_DIR_M2P | DMA_SxCR_MINC | DMA_SxCR_CIRC | DMA_SxCR_PSIZE_16 | DMA_SxCR_MSIZE_16; DMA1_Stream5->CR |= DMA_SxCR_EN;
DAC->CR = (0u << DAC_CR_TSEL1_Pos) /* 000: TIM6 TRGO */ | DAC_CR_TEN1 | DAC_CR_DMAEN1 | DAC_CR_EN1;
/* TIM6: 16 MHz / 250 = 64 kHz -> 64 samples per cycle = 1 kHz sine */ TIM6->PSC = 0u; TIM6->ARR = 249u; TIM6->CR2 = TIM_CR2_MMS_UPDATE; TIM6->CR1 = TIM_CR1_CEN;}
int main(void){ ema_t pa1_filter = { 0u, 3u }; bool first = true; uint32_t last = 0u;
console_init(); printf("VREFINT_CAL=%u TS_CAL1=%u TS_CAL2=%u\n", (unsigned)VREFINT_CAL, (unsigned)TS_CAL1, (unsigned)TS_CAL2); adc_dma_init(); dac_sine_init();
for (;;) { __asm volatile ("wfi"); uint32_t h = adc_halves; if (h - last < 32u) { /* 32 halves * 16 ms ~ 0.5 s */ continue; } last = h;
uint32_t s[NCH]; __asm volatile ("cpsid i" ::: "memory"); /* take a consistent snapshot */ for (uint32_t c = 0; c < NCH; c++) { s[c] = adc_sum[c]; } __asm volatile ("cpsie i" ::: "memory"); if (s[2] == 0u) { continue; }
/* VDDA from VREFINT: 3300 * CAL / (sum / SCANS) */ uint32_t vdda_mv = (3300u * VREFINT_CAL * SCANS) / s[2];
/* PA1 in mV: (sum / SCANS) * VDDA / 4095, product first */ uint32_t pa1_mv = (s[0] * vdda_mv) / (4095u * SCANS); if (first) { pa1_filter.acc = pa1_mv << pa1_filter.k; first = false; } uint32_t pa1_filt = ema_update(&pa1_filter, pa1_mv);
/* Temperature: rescale reading to VDDA = 3.3 V, then interpolate */ int32_t ts = (int32_t)((s[1] * vdda_mv) / 3300u); /* x SCANS */ int32_t c1 = (int32_t)TS_CAL1 * (int32_t)SCANS; int32_t c2 = (int32_t)TS_CAL2 * (int32_t)SCANS; int32_t t10 = 300 + (ts - c1) * 800 / (c2 - c1); /* 0.1 C units */ int32_t frac = t10 % 10;
printf("PA1 %4lu mV (filt %4lu, 14-bit %5lu) VDDA %4lu mV T %s%ld.%ld C err %lu\n", (unsigned long)pa1_mv, (unsigned long)pa1_filt, (unsigned long)(s[0] >> 2), (unsigned long)vdda_mv, (t10 < 0 && t10 > -10) ? "-" : "", (long)(t10 / 10), (long)(frac < 0 ? -frac : frac), (unsigned long)adc_errors); }}With the potentiometer around mid-travel:
VREFINT_CAL=1507 TS_CAL1=958 TS_CAL2=1206PA1 1474 mV (filt 1474, 14-bit 8193) VDDA 2951 mV T 33.6 C err 0PA1 1475 mV (filt 1474, 14-bit 8196) VDDA 2950 mV T 33.8 C err 0Your calibration values and temperature will differ. Notice VDDA: the Discovery runs its MCU at about 3 V, not 3.3 V, so a program assuming 3.3 V would report PA1 roughly 10% high.
What to look at:
- No CPU in the sampling path. TIM2 triggers, the ADC converts three channels, DMA stores them. The CPU wakes every 16 ms to sum 48 numbers and sleeps otherwise. Sample timing is exact even if
printftakes milliseconds. DDSis set. Without it, the ADC stops issuing DMA requests after the firstNDTRtransfers and the buffer stops updating although the stream is still enabled.ADONis written alone first, followed by a stabilization delay, before the trigger configuration. The first TIM2 trigger then finds a ready converter.- Order of operations in the maths. Each formula multiplies before dividing so the integer division happens last and loses the least. The comment beside each line records what scale the value is in.
- The snapshot. The three sums are read with interrupts briefly disabled, so they always come from the same half buffer. Without that, a DMA interrupt between reading
adc_sum[0]andadc_sum[2]would mix two snapshots. With three words the effect is small; with a struct of calibrated results it can be a nonsense reading. Interrupts done right covers the save-and-restore form you should use when the caller might already have interrupts disabled. - The DAC needs no CPU either. On a scope PA4 shows a 1 kHz sine from about 0.2 V to 2.8 V with visible 64-step staircase; an RC low-pass (for example 1 kΩ and 10 nF, corner 16 kHz) smooths it.
Analog layout in three bullets
Section titled “Analog layout in three bullets”- Decouple
VDDAandVREF+with 100 nF plus 1 µF ceramics placed right at the pins, and feedVDDAfrom the digital supply through a ferrite bead or small resistor. - Separate return paths: keep high-current digital and switching currents from flowing through the ground area under the analog inputs; join analog and digital ground at one point near the MCU.
- Keep analog traces short and away from clocks, switching regulators and PWM lines, and put the anti-aliasing RC filter right next to the ADC pin.
Common mistakes
Section titled “Common mistakes”- Assuming
VREFis 3.3 V. MeasureVDDAwith VREFINT and scale by it. - High-impedance source with a short sampling time. Readings depend on the previous channel. Lower the impedance, add a capacitor at the pin, or lengthen
SMPx. - ADC clock over 36 MHz. At 168 MHz,
ADCPRE = /2gives 42 MHz and quietly worse results. Use/4. - Forgetting
DDSin circular DMA mode. The buffer freezes after one pass. SetCR2.DDS. - Buffer length not a multiple of the channel count. Channel data rotates through the array. Size it as
scans * channels. - Integer overflow in conversions.
sum * vdda * 16exceeds 32 bits. Write the worst-case magnitude of each intermediate in a comment. - Floating
>>on negative values and naivey += (x - y) >> k. Implementation-defined shifts and stuck outputs. Keep the scaled accumulator unsigned. - Leaving the pin in digital input mode. Extra current and noise. Set
MODERto analog.
Exercises
Section titled “Exercises”- Run
adc1_init_singleandadc1_readin a loop that prints raw codes. With the potentiometer fixed, record 100 readings and compute the spread. Then change the sampling time to 3 cycles and compare. Explain the difference using the input model. - Replace the potentiometer with a 1 MΩ / 1 MΩ divider from 3 V. Compare the reading with a multimeter at sampling times of 3 and 480 cycles, then with a 100 nF capacitor from PA1 to ground. Account for each result.
- Write
uint32_t mv_from_raw(uint32_t raw, uint32_t vdda_mv)in three ways (float, integer with rounding, reciprocal multiply) and compile all three with-O2 -mcpu=cortex-m4 -mthumband again with-mcpu=cortex-m0. Read the disassembly and count instructions; find the calls to library helpers such as__aeabi_uidivor__aeabi_fmul. - Change the example to sample at 10 kHz with 64 scans per half buffer, and output 13-bit oversampled values. Check that the scan still fits in the trigger period by computing the conversion time of three channels.
- Replace the sine table with a 256-entry table generated at startup in fixed point (no
sinf), using a quarter-wave table of 64 entries and symmetry. Produce a 440 Hz tone by choosing the TIM6 period, and compute the frequency error caused by integerARR.
Check yourself
Section titled “Check yourself”Question 1:A 12-bit ADC with VDDA = 3.0 V reads 2048. The code converts with raw * 3300 / 4095. What does it print, and what is the real voltage?
The ADC is ratiometric to VDDA, so 2048 means half of 3.0 V; the code assumed a 3.3 V reference and overstates by 10%.
Question 2:A scan of 4 channels writes to a circular DMA buffer of 30 half-words. What goes wrong?
The buffer restarts at index 0 in the middle of a scan, so the position of each channel changes from pass to pass.
Question 3:Your exponential filter y += (x - y) >> 4 on uint16_t values never reaches a steady input of 1000 and stalls at 985. Why?
With k = 4, (x - y) >> 4 is 0 whenever the difference is below 16; keeping the accumulator scaled by 2^k fixes it.
Question 4:Why does timer-triggered sampling beat continuous mode for a digital filter?
Continuous mode runs at a rate set by ADC clock and sampling times, which is neither a chosen value nor independent of configuration; filters and frequency analysis need a known fixed rate.
Question 5:Averaging 16 samples of a 12-bit ADC and shifting the sum right by 2 gives a 14-bit value. When does this genuinely add resolution?
Without noise every sample is the same code and the average carries no extra information; random noise spreads samples across codes in proportion to the true value.
Key takeaways
Section titled “Key takeaways”- An ADC reading is a ratio to the reference; measure
VDDAwith VREFINT instead of assuming it. - The sample-and-hold capacitor must charge through the source impedance in the sampling time; high-impedance sources need longer sampling, a pin capacitor or a buffer.
- For multi-channel acquisition use scan mode, timer triggering and circular DMA with
DDS, and process half buffers on HT and TC. - Sample above twice the highest input frequency and filter in analog before the ADC; aliasing cannot be removed in software.
- Fixed point is integers with an agreed scale: multiply before dividing, check every intermediate’s range, round deliberately.
- A scaled-accumulator exponential filter and a median of 3 handle most sensor noise in a few instructions.
Next: Watchdogs, faults, and recovering from them, where you make the firmware survive its own bugs.