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.

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 clear SR &= ~TIM_SR_UIF), the ISR re-fires forever and main() 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, a HAL_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.

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

  2. Set Prescaler (PSC) = 79 and Counter Period (ARR) = 99 (the +1 is 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.

  3. In the timer’s NVIC Settings tab, enable the update interrupt (global interrupt). Give it a priority you can change later.

  4. Configure a spare pin (e.g. PB5) as GPIO_Output, push-pull, output speed Very High (crisp edges for clean measurement).

  5. Generate code. In main(), start the timer in interrupt mode:

    HAL_TIM_Base_Start_IT(&htim2);   /* enable timer + its update interrupt */
  6. 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.

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

  1. 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\).
  2. 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.

  1. 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.
  2. Recapture and re-measure period statistics. Jitter should now be visibly larger. Try reversing the NVIC priorities and watch the effect.
  3. 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.