Skip to content
Sign in
Theme

Time: delays, timers and PWM

Why busy loops are a trap, how a hardware timer counts, and how PWM dims an LED.

Read this first

The blink example waits with a delay function. Inside, the crude version is a loop that counts to a large number and does nothing. It works in a debug build, then with optimization on it takes no time at all: the compiler sees a loop with no effect and removes it. Marking the counter volatile stops that, but the real lesson is that timing should come from hardware, not from counting instructions.

A hardware timer is a counter that ticks from a clock. Two numbers set its rate. Taking an STM32 as the example: the prescaler divides the input clock, so with a 72 MHz clock and a prescaler value of 71 the counter ticks 72 MHz / 72 = 1 MHz, one million times per second. The auto-reload value, called ARR, is where the counter wraps to zero. The period is ARR + 1 ticks, so 1 MHz ticks with ARR = 999 give an update 1000 times a second. Other chips divide differently (AVR has a fixed set of dividers, the RP2040 a fractional one), so check the timer chapter of your chip's manual for the exact formula.

Nearly every Cortex-M chip also has SysTick, a simple 24-bit timer that counts down and reloads. Most libraries use it for a millisecond tick, which gives you a millis() style clock. With that you can wait without blocking: remember when something started, and check on each loop whether enough time has passed.

PWM, pulse width modulation, is a timer trick: the output pin goes high at the start of each period and low when the counter reaches a compare value. The fraction of the period the pin is high is the duty cycle. Compare 500 out of 2000 counts is 25 percent. Switch fast enough and an LED looks dimmed, a motor runs slower, a servo moves to a position. The pin is still only ever fully on or fully off.

To remember

  • Empty counting loops are removed by the optimizer. Use a timer.
  • On STM32, tick rate = clock / (prescaler + 1) and period = ARR + 1 ticks. Other chips divide differently, so check the manual.
  • SysTick gives a millisecond clock. Use it to wait without blocking the loop.
  • PWM duty cycle = compare / period. The pin is still only on or off.

Check what you read

3 questions from the question bank on the ideas above. Each one comes with an explanation after you answer.

Sign in to answer the questions. Your answers count towards your level.

Sign in

The lesson text and the project are free to read without an account.

Build this

Breathing LED and a servo

You need

  • Your board and one LED with a resistor.
  • A small hobby servo (SG90 or similar). Power it from a separate 5 V supply, and connect that supply's ground to the board's ground as well as to the servo. The board's 5 V pin can work for a quick test, but a stalled servo can pull more than a USB port gives and reset the board.

Steps

  1. Rewrite blink without a delay call: use the millisecond tick and toggle when 500 ms have passed. The main loop must stay free to do other things.
  2. Set up a PWM output on the LED pin at about 1 kHz. Ramp the duty from 0 to 100 percent and back, so the LED breathes.
  3. Set up a second PWM at 50 Hz for the servo. A 1.5 ms pulse is the center, 1.0 ms and 2.0 ms are the ends. Work out the compare values for your timer settings before writing them.
  4. Sweep the servo slowly from end to end while the LED keeps breathing.

Done when

The LED breathes and the servo sweeps at the same time, and there is no blocking delay left in your main loop.