Lab 2.1 — Saleae GPIO Timing
← Course 2 syllabus · Module 2 · Prev: « Lab 1.3 · Next: Lab 2.2 »
Goal
Bring up your first STM32 firmware project end-to-end — create a STM32CubeIDE project for the NUCLEO-L476RG, blink/toggle a GPIO pin in a tight loop, flash it over the on-board ST-LINK, and then measure what you actually built with the Saleae Logic 8. The skill here is the loop that every firmware role runs on: write code, put a probe on the pin, and confirm the hardware does what the C says. You will discover that a “toggle as fast as possible” loop does not run at the CPU clock — the GPIO output-speed setting and the compiler’s optimization level both change the measured toggle rate by large factors. Learning to see that on a logic analyzer, on day one, is the foundation for every timing-critical lab that follows.
Recommended reading
- Kuo Ch. 1 — the real-time DSP system context: where the microcontroller sits, why deterministic I/O timing matters. Light read before the bench.
- NUCLEO-L476RG user manual (UM1724) and the STM32L476 reference manual (RM0351) — read the GPIO chapter’s port output-speed and port bit set/reset register (BSRR) sections. You do not need to memorize them; you need to know they exist and what OSPEEDR does.
- Saleae Logic 2 docs — “Adding an analyzer,” “Digital measurements,” and setting the logic threshold.
- No Course 1 dependency; this is firmware + instrument bring-up.
Equipment & parts
- STM32 NUCLEO-L476RG board + USB-A/micro-B cable to the host Mac.
- Saleae Logic 8 + its flying-lead harness (Logic 2 software installed).
- A jumper wire or two, and a breadboard (only to give the Saleae ground and signal leads something to clip to, if you prefer not to probe the header directly).
- Host running STM32CubeIDE (free) and Logic 2 (free).
Safety & don’t-break-it
- Everything here is 3.3 V logic. The Nucleo drives its GPIO at 3.3 V. The Saleae inputs are fine with this, but you must share ground: connect one Saleae GND lead to a Nucleo GND pin before clipping a signal lead. A probe with no common ground reads garbage and can inject noise.
- Do not source real load current from a GPIO. You are measuring a voltage edge with a high-impedance logic analyzer, not driving an LED array. Keep the pin unloaded (or driving only the on-board LD2 LED). An STM32 GPIO can source/sink only ~8–20 mA per pin; shorting a driven pin to ground or to 3.3 V can damage it.
- Never let a probe lead touch the 5 V (E5V / VIN) pins and then a 3.3 V logic input on the same reference. The Saleae tolerates ~5 V but the point of this lab is the 3.3 V domain — keep leads on the pins you intend.
- Confirm pin 1 / pin identity on the Morpho header before probing. Use the UM1724 pinout. Probing the wrong pin wastes an afternoon; probing a power pin as if it were a GPIO does not.
- Insert/remove the USB cable gently; the micro-B connector on ST-LINK is the board’s most fragile part.
Background
A GPIO output pin is a voltage that firmware flips between 0 V (logic low) and \(V_{DD}=3.3\text{ V}\) (logic high). The fastest possible software toggle is a loop that writes high, then low, forever. Naively you might expect the pin to toggle near the CPU clock \(f_{CPU}\) (up to 80 MHz on the L476), giving a square wave at \(f_{CPU}/2\). It does not, for three reasons:
- Instruction count per toggle. Each half-period costs several instructions — read the loop condition, compute the write, store to the peripheral register. Using HAL’s
HAL_GPIO_TogglePin()costs far more (a function call, a read-modify-write, branch logic) than a direct write to the BSRR register. - The GPIO output-speed setting (OSPEEDR). Each pin has a slew-rate control — Low / Medium / High / Very-High speed. This sets how fast the output driver transitions, i.e. the edge rise/fall time, and at high toggle rates it limits how clean (and how fast) the pin can actually swing. It does not make the loop run faster, but it changes the edges you measure.
- Compiler optimization level. At
-O0(debug) the compiler emits literal, un-optimized code and the loop is slow. At-O2/-Ofastit keeps the peripheral write but strips overhead, and the toggle rate jumps several-fold. This is the single most surprising number in the lab.
For a square wave, the measured period \(T\) and frequency \(f\) relate as
\[f = \frac{1}{T}, \qquad T = t_{\text{high}} + t_{\text{low}}.\]
If a single loop iteration writes high then low, one full period covers both writes, and the toggle rate (edges per second) is \(2f\). The pulse width you measure (\(t_{\text{high}}\) or \(t_{\text{low}}\)) times the instructions-per-half count, divided by \(f_{CPU}\), tells you the effective cycles per edge:
\[\text{cycles per half-period} \approx t_{\text{high}} \cdot f_{CPU}.\]
That number is your window into what the compiler actually generated.
Procedure
Part A — Create and flash the project.
In STM32CubeIDE: File → New → STM32 Project. In the board selector, choose NUCLEO-L476RG, name the project, and accept “initialize all peripherals to default.” This generates a CubeMX
.iocwith LD2 on PA5 already configured asGPIO_Outputand the clock tree set up.Open the
.ioc. Confirm PA5 isGPIO_Output. Optionally add a spare pin (e.g. PB5 or PA8 on the Morpho header) asGPIO_Outputso you can probe a pin that is not also driving the LED — the LED and its wiring add a little capacitance you may want to avoid. Set the pin’s GPIO output level = Low, mode = Push-pull, and note the Maximum output speed field (start at Low).In the generated
main.c, inside thewhile(1)loop, add a tight toggle. Illustrative HAL version:/* USER CODE BEGIN WHILE */ while (1) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); /* toggle LD2 / PA5 */ } /* USER CODE END WHILE */And the faster LL / direct-register version (write to BSRR — atomic set/reset, no read-modify-write):
while (1) { GPIOA->BSRR = GPIO_BSRR_BS5; /* set PA5 high */ GPIOA->BSRR = GPIO_BSRR_BR5; /* set PA5 low */ }(Write your own real loop; these show the two approaches you will compare.)
Build (Project → Build) and flash/run (the green Run ▶). The ST-LINK programs the board over the same USB cable. LD2 should appear solid (it is toggling far too fast to see flicker).
Part B — Capture with the Saleae.
- Wire the Saleae: one GND flying lead to a Nucleo GND pin; one channel lead (say CH0) to the pin you are toggling (PA5 on the Morpho header, or your spare pin).
- In Logic 2, click the channel’s gear and set the logic threshold to 3.3 V (choose the “1.2/1.8/3.3 V” logic-level preset, not 5 V). This tells Logic where the high/low decision point sits for CMOS 3.3 V.
- Set the sample rate high — with only one or two channels active the Logic 8 can sample at up to 100 MS/s. You need at least ~10× the edge rate to resolve a fast toggle; start at the maximum and a short capture duration (e.g. 10 ms).
- Click Start. You should see a dense square wave. Zoom in until individual pulses are clear.
Part C — Measure period and pulse width.
- Drag a timing measurement across one full period (rising edge to the next rising edge) → this is \(T\). Logic 2 shows period and frequency directly. Also measure one high pulse width \(t_{\text{high}}\).
- Note \(f = 1/T\) and record it in the table.
Part D — Vary output speed and optimization.
- In the
.ioc, change the pin’s Maximum output speed from Low → Very High, regenerate code, rebuild, reflash, recapture. Compare edge sharpness (rise time) and whether the top frequency changes. - Change the optimization level: Project → Properties → C/C++ Build → Settings → MCU GCC Compiler → Optimization. Build once at -O0 (None) and once at -O2. Reflash and recapture each. Record the toggle frequency for both.
- Repeat the whole capture for the HAL
TogglePinloop vs. the direct BSRR loop.
Deliverable & expected results
A short bench note (docs/lab-2-1.md) plus one Logic 2 .sal capture per configuration, recording measured period/frequency and the effect of each knob.
Compute predictions from \(f_{CPU}=80\text{ MHz}\) and an estimated cycles-per-half. A direct BSRR write is ~1–2 cycles plus loop overhead; at -O2 a two-instruction store loop lands in the low-MHz toggle range. HAL TogglePin at -O0 can be 10–50× slower.
| Quantity | Predicted | Measured |
|---|---|---|
| Toggle freq — BSRR loop, -O2 | ~ several MHz (compute from cycles/half) | … |
| Toggle freq — BSRR loop, -O0 | markedly lower than -O2 | … |
| Toggle freq — HAL TogglePin, -O0 | far lower again (function-call overhead) | … |
| High pulse width \(t_{\text{high}}\) (BSRR, -O2) | \(T/2\) | … |
| Rise time at OSPEEDR = Low vs. Very High | shorter at Very High | … |
Analysis & reconciliation
From the measured \(t_{\text{high}}\), back out the cycles per half-period: \(N = t_{\text{high}}\cdot f_{CPU}\). Compare \(N\) across the four builds and explain the gaps — the HAL path pays for a function call and a read-modify-write of ODR; -O0 keeps every load/store the C implies; -O2 folds them away. This is your first concrete lesson that C source is not the machine timing — the toolchain sits in between, and only the logic analyzer tells the truth. Note whether the clock is actually 80 MHz (check the CubeMX clock tree; the default may leave you at a slower HSI/PLL setting — if your numbers are off by a clean factor, suspect the clock config).
Expect the OSPEEDR change to affect edge slope, not loop rate: at Low speed a very fast toggle may not fully reach the rails (rounded, slewed edges); at Very High the edges are crisp but couple more noise. This previews why you will pick output speed deliberately in the I²C/SPI labs.
Going further
- Add SWV / ITM trace or toggle a second pin around a block of code to time an arbitrary function — the same “probe the pin” technique, applied to real work.
- Put the direct-register write inside a
forloop with a known iteration count and confirm the loop overhead cycle-by-cycle against the ARM Cortex-M4 instruction timings. - Compare
HAL_GPIO_WritePinset/reset vs. BSRR vs. ODR read-modify-write — three ways to set a pin, three different timings. - Bridge to Lab 2.2 — Timer interrupt jitter: a software loop’s rate is whatever the compiler gives you; a hardware timer’s rate is exactly what you program. That determinism is why real DSP sampling is timer-driven.