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.

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, PA0 is 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.

  1. Start from your Lab 5.1 project. Enable TIM2: set Prescaler = 79, Counter Period (ARR) = 99, and Trigger Event Selection = Update Event (TRGO).
  2. In ADC1, change External Trigger Conversion Source to Timer 2 Trigger Out event, External Trigger Edge = Rising. Keep 12-bit, single channel IN5 (PA0).
  3. 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).

  1. 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.

  1. Connect a Saleae channel to the marker pin, ground clipped to board GND. Capture a few hundred milliseconds.
  2. 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.
  3. 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.