Lab 5.2 — Timer-Triggered ADC
← Course 2 syllabus · Module 5 · Prev: « Lab 5.1 · Next: Lab 5.3 »
Goal
Turn the one-shot conversion of Lab 5.1 into a hardware-timed sample stream at a known, fixed rate \(f_s\). Instead of asking the CPU to poll “convert now” — which is at the mercy of loop timing, interrupts, and cache — you let a timer issue the trigger, so conversions land on a rigid grid with essentially zero jitter. You will drive the ADC from TIM2, toggle a GPIO once per conversion, and confirm the true sample rate on the Saleae. Deterministic, jitter-free sampling is the foundation the entire DSP stack stands on: every frequency you later compute, every filter you design, assumes samples are equally spaced in time. If \(f_s\) wobbles, the math is wrong.
Recommended reading
- Kuo — real-time sampling and the sample-clock architecture of an embedded DSP system (the timer-driven acquisition front end).
- O-S&S Ch. 7 — sampling of continuous-time signals: the ideal impulse-train sampler and why uniform spacing \(T_s = 1/f_s\) is assumed throughout.
- Course 1 Week 10 — the impulse train / Dirac comb as the model of ideal uniform sampling (the theoretical object your timer approximates).
Equipment & parts
- STM32 NUCLEO-L476RG + USB cable.
- MCP4725 DAC (3.3 V powered) as the analog source, or the buffered node from Lab 4.4.
- Saleae Logic 8 + Logic 2 software.
- Jumper wires; common ground.
Safety & don’t-break-it
- ADC input stays 0 – 3.3 V. As in Lab 5.1,
PA0is not 5 V tolerant. Keep the MCP4725 on the 3.3 V rail so its rail-to-rail output cannot exceed VREF+. - Saleae threshold and ground. Set the Logic 2 input threshold to 3.3 V logic and clip a Saleae ground to the board ground. The trigger-toggle pin swings 0 ↔︎ 3.3 V, well within the analyzer’s range, but a floating ground gives garbage edges.
- Don’t route the timer TRGO to a pin you also drive — pick a spare GPIO (e.g. PA5 / LD2 is convenient but blinks the LED; a free Morpho pin is cleaner) for the per-conversion toggle so you’re not fighting an alternate-function assignment.
Background
A general-purpose timer counts its input clock \(f_\text{TIM}\), divides by the prescaler \((\text{PSC}+1)\), and rolls over every auto-reload \((\text{ARR}+1)\) counts, generating an update event at
\[f_s = \frac{f_\text{TIM}}{(\text{PSC}+1)(\text{ARR}+1)}.\]
Configuring the timer’s TRGO (trigger output) to fire on update and pointing the ADC’s external trigger at TIM2_TRGO means each rollover launches exactly one conversion. The sample period is \(T_s = 1/f_s\).
Worked example, targeting \(f_s = 10\ \text{kHz}\) from \(f_\text{TIM} = 80\ \text{MHz}\):
\[\text{PSC}+1 = 80 \;\Rightarrow\; f_\text{cnt} = \frac{80\times10^6}{80} = 1\ \text{MHz},\qquad \text{ARR}+1 = 100 \;\Rightarrow\; f_s = \frac{10^6}{100} = 10\ \text{kHz}.\]
So PSC = 79, ARR = 99. The reason this beats software polling is jitter: a polled loop’s actual sampling instant drifts with whatever else the CPU is doing, and non-uniform spacing smears the spectrum. A timer trigger is a hardware event — the conversion start is locked to the counter, so the only error is the (tiny, bounded) timer clock accuracy, not software timing.
Procedure
Part A — Timer + ADC trigger in CubeMX.
- Start from your Lab 5.1 project. Enable TIM2: set Prescaler = 79, Counter Period (ARR) = 99, and Trigger Event Selection = Update Event (TRGO).
- In ADC1, change External Trigger Conversion Source to Timer 2 Trigger Out event, External Trigger Edge = Rising. Keep 12-bit, single channel IN5 (PA0).
- Enable EOC (end-of-conversion) interrupt for ADC1. Configure a spare GPIO output for the per-conversion toggle. Generate code.
Part B — Toggle a pin per conversion (illustrative firmware).
- Start the timer base and the ADC in interrupt mode; toggle the marker pin in the conversion-complete callback:
// Illustrative only.
HAL_TIM_Base_Start(&htim2); // timer now free-runs, issuing TRGO
HAL_ADC_Start_IT(&hadc1); // ADC waits for the TIM2 trigger
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *h) {
HAL_GPIO_TogglePin(MARK_GPIO_Port, MARK_Pin); // one edge per conversion
uint16_t code = HAL_ADC_GetValue(h); // grab the sample
// (store/print in later labs)
}Because the marker toggles, its output frequency is \(f_s/2\) (a full high+low cycle spans two conversions) — account for that when you read the Saleae.
Part C — Measure the true sample rate.
- Connect a Saleae channel to the marker pin, ground clipped to board GND. Capture a few hundred milliseconds.
- In Logic 2, measure the marker’s period. The conversion rate is \(f_s = 2\times\) (marker frequency), or equivalently \(f_s = 1/(\tfrac12 T_\text{marker})\). Alternatively, add the frequency measurement and double it.
- Change ARR to retarget \(f_s\) (e.g. ARR = 199 → 5 kHz; ARR = 49 → 20 kHz) and re-measure to confirm the formula tracks.
Deliverable & expected results
A Saleae capture (captures/lab-5-2.sal) plus a note recording predicted vs. measured \(f_s\) for two or three timer settings.
| PSC | ARR | Predicted \(f_s\) | Marker freq (pred.) | Measured \(f_s\) |
|---|---|---|---|---|
| 79 | 99 | 10.000 kHz | 5.000 kHz | … |
| 79 | 199 | 5.000 kHz | 2.500 kHz | … |
| 79 | 49 | 20.000 kHz | 10.000 kHz | … |
Analysis & reconciliation
Predicted \(f_s = f_\text{TIM}/[(\text{PSC}+1)(\text{ARR}+1)]\); confirm your \(f_\text{TIM}\) from the CubeMX clock tree (the timer’s kernel clock may be the APB1 timer clock, which can differ from the raw APB1 bus clock by the ×2 timer-clock rule — check it). Expect measured \(f_s\) within the Saleae’s timing resolution and the STM32 oscillator tolerance (HSI ~1%; tighter if you’re on an external/PLL-locked source). If you’re off by exactly ×2, you’ve either forgotten the toggle-halves-the-rate factor or hit the APB timer-clock doubler. If \(f_s\) is a bit low and unstable, you may be missing conversions because \(t_\text{conv}\) (sampling + SAR time from Lab 5.1) exceeds \(T_s\) — the ADC can’t finish before the next trigger; lengthen ARR or shorten the sampling time.
Going further
- Jitter comparison. Re-run the old software-polled loop from Lab 5.1 in a busy
while(1)and capture its marker on the Saleae; compare the edge-to-edge jitter against the timer-triggered version. This is the concrete payoff of hardware timing (and connects to the jitter work in Lab 2.2). - Scope the actual sampling instants by also probing the ADC input and the marker together — see where in the waveform each sample is taken.
- This per-conversion interrupt still costs one ISR per sample. At high \(f_s\) that CPU tax becomes unacceptable — which is exactly the problem DMA solves in Lab 5.3.