Lab 2.2 — Timer Interrupt Jitter
← Course 2 syllabus · Module 2 · Prev: « Lab 2.1 · Next: Lab 2.3 »
Goal
Configure a hardware timer (TIM) on the NUCLEO-L476RG to fire a periodic update interrupt at a known rate (target 10 kHz), toggle a GPIO pin inside the interrupt service routine, and measure the actual period and its jitter on the Saleae. Where Lab 2.1 showed that a software loop runs at “whatever the compiler gives you,” this lab shows the opposite: a hardware timer fires with clock-accurate period, but the response to it — the moment the ISR actually flips the pin — carries latency and jitter from interrupt entry, competing interrupts, and priority. Quantifying that jitter is the core competency behind every real-time sampling system: an ADC that samples on a jittery clock smears its spectrum, so a firmware DSP engineer must be able to prove the sample clock is clean.
Recommended reading
- Kuo Ch. 1 — real-time systems, interrupts, and why a deterministic sampling instant matters for DSP. Focus on the sampling-clock and real-time-constraint discussion.
- STM32L476 reference manual (RM0351) — the general-purpose timer chapter: prescaler (PSC), auto-reload (ARR), and the update event (UEV) / update interrupt (UIE). Read how PSC and ARR set the period.
- PM0214 (Cortex-M4 programming manual) — NVIC priorities and interrupt entry/exit (the “12-cycle” exception entry). Skim.
- Course 1: Week 12 — Signals/Systems/Transforms for why non-uniform sampling corrupts the spectrum (sampling-clock jitter → phase noise). Optional here, essential in Module 5.
Equipment & parts
- STM32 NUCLEO-L476RG + USB cable.
- Saleae Logic 8 + Logic 2 (with timing markers / measurement statistics).
- A spare GPIO pin to toggle from the ISR (e.g. PB5 or PA8), plus optionally a second pin toggled from
main()to create contention. - Host with STM32CubeIDE.
Safety & don’t-break-it
- Same 3.3 V logic rules as Lab 2.1: share ground first, set the Saleae logic threshold to 3.3 V, keep GPIO pins unloaded.
- A runaway ISR can lock the board. If you enable the timer interrupt but forget to clear the update flag (HAL does this for you inside
HAL_TIM_IRQHandler; bare-metal code must clearSR &= ~TIM_SR_UIF), the ISR re-fires forever andmain()never runs. If the board appears frozen or the debugger won’t halt cleanly, this is the usual cause. Keep the ST-LINK connected so you can always reflash. - Do not do slow work inside the ISR. A
printf, aHAL_Delay, or a blocking bus transaction inside a 10 kHz ISR (100 µs budget) will overrun — the next interrupt arrives before the last one finishes. Keep the ISR to a single pin write for this lab. - Don’t set the NVIC priority of the timer lower (numerically higher) than something that then starves it, unless you are deliberately creating the contention experiment in Part D — and even then, watch for a lock-up.
Background
A general-purpose timer counts a clock. Starting from the timer input clock \(f_{TIM}\) (derived from the APB bus clock — often 80 MHz on the L476 with the default config), the prescaler PSC divides it and the auto-reload ARR sets the count length. The timer’s update event fires at
\[f_{\text{update}} = \frac{f_{TIM}}{(\text{PSC}+1)\,(\text{ARR}+1)}.\]
For a 10 kHz update from an 80 MHz timer clock, one convenient choice is \(\text{PSC}+1 = 80\) (→ 1 MHz tick) and \(\text{ARR}+1 = 100\):
\[f_{\text{update}} = \frac{80\times10^6}{80 \times 100} = 10\,000\ \text{Hz}, \qquad T = 100\ \mu\text{s}.\]
The timer fires with the accuracy of the crystal/PLL — essentially perfect period. But the pin toggle happens some cycles later, after the CPU takes the exception. That delay is the interrupt latency:
\[t_{\text{latency}} = t_{\text{entry}} + t_{\text{ISR-prologue}} + t_{\text{write}},\]
where \(t_{\text{entry}}\) is the Cortex-M4 exception entry (a fixed ~12 cycles when nothing else is pending) plus any time spent finishing a lower-priority handler or a multi-cycle instruction. If that latency were constant, the pin would still be perfectly periodic — just shifted. Jitter is the variation in latency from one interrupt to the next:
\[J = \max_k T_k - \min_k T_k, \qquad T_k = t_{\text{edge},\,k+1} - t_{\text{edge},\,k},\]
or, more usefully, the standard deviation of the measured period set \(\{T_k\}\). Jitter appears whenever the latency changes: a competing interrupt arrives and delays entry, a longer instruction is mid-execution when the interrupt fires, or a higher-priority ISR preempts. In sampling terms, period jitter is aperture/sample-clock jitter, and it raises the noise floor of any spectrum you later compute.
Procedure
Part A — Configure the timer for a 10 kHz update interrupt.
In the project’s
.ioc(continue from your Lab 2.1 project or make a new one), select a basic/general-purpose timer, e.g. TIM2 (32-bit) or TIM6/TIM7 (basic, ideal for a periodic interrupt). Set Clock Source = Internal Clock.Set Prescaler (PSC) = 79 and Counter Period (ARR) = 99 (the
+1is implicit in the formula above), giving \(f_{\text{update}}=10\text{ kHz}\) from an 80 MHz timer clock. Verify the timer clock in the Clock Configuration tab — if APB1 timer clock is not 80 MHz, recompute PSC/ARR.In the timer’s NVIC Settings tab, enable the update interrupt (global interrupt). Give it a priority you can change later.
Configure a spare pin (e.g. PB5) as
GPIO_Output, push-pull, output speed Very High (crisp edges for clean measurement).Generate code. In
main(), start the timer in interrupt mode:HAL_TIM_Base_Start_IT(&htim2); /* enable timer + its update interrupt */Implement the period-elapsed callback (HAL routes the ISR here after clearing the flag). Keep it to a single pin write:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) GPIOB->BSRR = (GPIOB->ODR & GPIO_ODR_OD5) ? GPIO_BSRR_BR5 : GPIO_BSRR_BS5; }(Illustrative — toggling PB5 by reading ODR and writing BSRR. A cleaner pattern toggles with
GPIOB->ODR ^= GPIO_ODR_OD5;. Write your own.)Note: because you toggle once per interrupt, the pin’s output is a square wave at half the interrupt rate → 5 kHz on the pin for a 10 kHz ISR. Account for that when reading the Saleae.
Part B — Capture and measure period.
- Wire the Saleae to PB5 and GND; threshold 3.3 V; sample rate at maximum for 1–2 active channels. Capture ~50–100 ms so you have many periods for statistics.
- In Logic 2, add a timing measurement across one pin period (should be ~200 µs → 5 kHz). Confirm the interrupt rate is \(2\times\) that (10 kHz).
Part C — Measure jitter.
- Use Logic 2’s measurement/statistics feature over the capture: it can report period min/max/mean/standard deviation across all edges (add a Measurements panel and select the channel, or use the analyzer statistics). Record min, max, mean period and compute jitter \(J = T_{\max}-T_{\min}\) and the standard deviation \(\sigma_T\).
- Zoom to the tightest time base and eyeball the edge “smear” — with only the timer interrupt running, jitter should be very small (a few timer clock cycles at most, i.e. tens of ns).
Part D — Induce jitter with contention.
- Add a second, higher-priority interrupt or a long non-interruptible section in
main()that occasionally disables interrupts (e.g. a__disable_irq()/ work /__enable_irq()block, or a busy loop with a higher-priority EXTI from the user button B1 / PC13). Each time the higher-priority work delays the timer ISR, the toggle edge slips. - Recapture and re-measure period statistics. Jitter should now be visibly larger. Try reversing the NVIC priorities and watch the effect.
- Optionally toggle a second pin at the very start of the ISR and compare its edge to the timer’s known fire instant to isolate entry latency from period jitter.
Deliverable & expected results
docs/lab-2-2.md plus Logic 2 captures for the clean case and the contended case, recording measured period statistics and computed jitter.
| Quantity | Predicted | Measured |
|---|---|---|
| Interrupt period \(T\) | 100 µs (10 kHz) | … |
| Pin square-wave period | 200 µs (5 kHz, toggle-per-ISR) | … |
| Jitter \(J\), clean (timer only) | ≤ a few timer-clock cycles (tens of ns) | … |
| \(\sigma_T\), clean | small (ns-scale) | … |
| Jitter \(J\), with competing interrupt | markedly larger (µs-scale possible) | … |
Analysis & reconciliation
Confirm the mean period equals \(1/f_{\text{update}}\) from the PSC/ARR formula — if it is off by a clean ratio, your timer clock isn’t the 80 MHz you assumed (recheck the clock tree, exactly as in Lab 2.1). Then interpret the jitter: in the clean case the timer hardware sets the fire instant with crystal accuracy, so the only variation is CPU exception-entry timing — a few cycles depending on which instruction the core was executing when the interrupt arrived. In the contended case, a higher-priority handler or an interrupts-disabled window delays entry by however long that work takes, and that variation is the µs-scale jitter you measure.
Tie it back to DSP: if this were an ADC sample clock, jitter \(\sigma_T\) translates into phase noise on every sampled sinusoid, raising the spectral noise floor by roughly \(10\log_{10}\) of the timing-error power ratio. This is exactly why Module 5 triggers the ADC from a timer directly (hardware-timed conversion) rather than reading it in an ISR — you remove the software latency from the sample instant entirely.
Going further
- Move the toggle to a hardware-only path: configure the timer to drive the pin via output compare / PWM with no CPU involvement, and measure the jitter of that. It should be essentially zero — the pin flips in silicon on the count match. This is the gold standard the ISR is compared against.
- Sweep the interrupt rate (1 kHz → 50 kHz) and find where the ISR overhead starts to eat a meaningful fraction of the period — the point where “do it in the ISR” stops being viable and DMA becomes necessary (previews Lab 5.3).
- Measure the effect of moving the ISR code from flash to RAM, or of enabling/disabling the flash instruction cache/prefetch, on jitter.