Time: Delays, Timers and PWM
Every embedded program has to measure time, and the first way anyone learns is the worst one. A delay loop stops the whole chip. This volume replaces it with a tick counter, shows the subtraction that keeps working after the counter wraps, and ends with a state machine that does several things at once without ever waiting.
- Why a busy-wait delay stops everything, measured against the alternative
- What a hardware timer does that a loop cannot, and what SysTick is for
- The non-blocking pattern, and the wraparound bug that hides in it for 49 days
- How PWM dims an LED, and how to choose the prescaler and period
- How to write a state machine in C that runs several things at once
12.1 Busy-wait delays and their problems
A busy-wait delay does not wait. It works, flat out, doing nothing, and nothing else in the program can run until it is finished.
Almost everybody's first embedded program contains one:
void delay_ms(uint32_t ms)
{
volatile uint32_t spin = ms * 8000u; /* roughly, on this chip, today */
while (spin > 0u) {
spin--;
}
}
for (;;) {
led_on();
delay_ms(500);
led_off();
delay_ms(500);
}
It blinks. It is also the most expensive line of code in the file. For those 500 milliseconds the processor is fully occupied counting down, and every button, byte and sensor reading that happens meanwhile is simply not seen.
Measured, not asserted
Here is the same job done twice. Blink an LED every 500 ms, and answer a button. Four presses arrive during a two-second run.
1. blink with delay_ms(500)
blinked at 0 500 1000 1500 ms
noticed 0 of 4 presses
every press began and ended inside a delay
2. blink by watching a 1 ms tick counter
blinked at 0 501 1002 1503 ms
noticed 4 of 4 presses, the worst 2 ms after it began
and the loop was free the whole time to do other work
Both blinked four times. One answered nothing and the other answered everything. The second version is a few milliseconds late on each blink, because its loop takes 3 ms per pass, and no LED has ever complained about that.
A busy-wait is standing at the front door for ten minutes because you are expecting a parcel. A tick counter is glancing at the clock now and then while you carry on with your day.
The other two problems
The timing is a guess. ms * 8000 was true for one compiler, one optimisation level and one clock
speed. Turn on -O2, or run the chip at a different frequency, and every delay in the program
changes length at once.
And it burns power. A processor spinning in a loop draws full current. A processor asleep waiting for a timer interrupt draws almost none, which on a battery is the difference between weeks and days.
A one-off wait at start-up, where nothing else needs to happen yet. A sensor that needs 10 ms to settle after power-up, a display that needs 50 ms after reset. Those are honest uses. Inside a main loop, almost never.
Leaving volatile off the spin variable. Without it the compiler sees a loop with no effect and
deletes the whole thing, and the delay becomes zero. Volume 10 showed that happening in assembly.
Your loop uses delay_ms(200) and a serial byte arrives every 5 ms. What happens?
Show the answer
Answer: B. Forty bytes arrive during one delay, and the receive register holds one. Unless an interrupt is collecting them, thirty-nine are overwritten before the loop looks again.
12.2 Hardware timers and SysTick
A timer is a counter in hardware. It counts on its own, whether or not your program is paying attention, and it never drifts because the compiler changed.
Every microcontroller has several. They all share the same three parts.
| Part | What it does |
|---|---|
| Prescaler | divides the incoming clock, to slow the counting down |
| Counter | counts up, one step per prescaled clock |
| Auto-reload | the value the counter wraps back to zero at |
The counting is free. Nothing in your program runs to make it happen, so a timer keeps perfect time while the processor is busy elsewhere, or asleep.
Working out the numbers
One formula covers it:
tick rate = timer clock / (prescaler + 1) / (auto-reload + 1)
Both registers hold one less than the number you want, because the count includes zero. That off-by- one catches everybody once.
timer clock 72000000 Hz
wanted PWM rate 1000 Hz
steps of control 1000
so prescaler 71 (divides the clock by 72)
and period 999
check: 72000000 / 72 / 1000 = 1000 Hz
SysTick: the one that is always there
Arm Cortex-M cores include a small timer called SysTick, meant for exactly one job: interrupting at a steady rate so that software can keep a count of time.
volatile uint32_t ticks; /* written by the handler, read by everyone */
void SysTick_Handler(void)
{
ticks++;
}
/* at start-up: interrupt 1000 times a second */
SysTick_Config(SystemCoreClock / 1000u);
That handler is one line, which is exactly what Volume 11 asked for. Everything else reads ticks
and works out the rest.
The tick counter is the one piece of shared state nearly every embedded program has. It is
volatile because a handler writes it. On a 32-bit chip a uint32_t read cannot be torn, and only
the handler writes it, so reading it needs no protection at all.
Why not just read the timer's counter register
You can, and for short intervals it is better - a hardware counter gives you microseconds rather
than milliseconds. Two things to watch. The register is usually 16 bits on small timers, so it wraps
every few milliseconds and can only measure short gaps. And it is a peripheral register, so it must
be volatile and read once into a local variable.
The usual arrangement is both: SysTick for everything measured in milliseconds, and a free-running hardware counter when you need to time something in microseconds.
A timer clock is 48 MHz. You want an interrupt every millisecond, using a period of 1000. What prescaler value goes in the register?
Show the answer
Answer: A. 48 MHz divided by 1000 Hz divided by 1000 counts needs a divide of 48. The register holds one less than the divide, so it gets 47. The period register gets 999 for the same reason.
12.3 Non-blocking timing with ticks
Do not wait for the time to pass. Let it pass, and check whether it has.
The pattern replaces every delay in a main loop, and it is four lines.
static uint32_t last_blink;
for (;;) {
uint32_t now = ticks;
if ((uint32_t)(now - last_blink) >= 500u) {
led_toggle();
last_blink = now;
}
/* anything else you like, every pass */
check_button();
update_display();
}
Each job keeps its own last variable, so a loop can run a dozen of them at different rates without
any of them knowing about the others.
The subtraction is not a style choice
This is the most important line in the volume, so it is worth being certain about. These two look equivalent:
if ((uint32_t)(now - last) >= interval) {
/* ... */
}
if (now >= last + interval) {
/* ... */
}
They agree for about seven weeks. Then the tick counter passes 0xFFFFFFFF, wraps to zero, and they stop agreeing.
3. the counter has just wrapped past zero
long enough last=0xFFFFFF80 now=0x00000020 gap=160
subtracting says DUE adding says not yet <-- they disagree
the gap really is 160 ms, so the interval has passed
adding computes last + interval = 0xFFFFFFE4
now has wrapped back to a small number, and will not climb
back up to that value for another 49 days
The interval has passed. The subtracting version says so. The adding version compares a small number against a huge one and decides it is not time yet. It goes on deciding that until the counter climbs all the way round again.
At a 1 kHz tick, a uint32_t wraps after 4294967296 ms, which is 49.7 days. That is long enough
that the bug never appears on your desk, in testing, or during the demo. It appears in a device that
has been running since it was installed.
And it is worse than a bug that always fails, because sometimes it works:
4. the same bug, hiding: this time the sum wraps as well
long enough last=0xFFFFFFE0 now=0x00000050 gap=112
subtracting says DUE adding says DUE
here last + interval = 0x00000044, which wrapped too, so adding
happens to give the right answer
Why subtracting works
Unsigned arithmetic in C is modular: every result is taken modulo 2^32, wrapping silently, and the
standard guarantees it. So now - last is not "a possibly negative number" - it is the true gap,
already wrapped back into range.
If last is 0xFFFFFF80 and now is 0x20, then now - last is computed as
0x20 - 0xFFFFFF80 + 2^32, which is 0xA0, or 160. The two wraps cancel.
The only rule is that the gap you are measuring must be shorter than half the counter's range, so that "160 ms ago" cannot be confused with "49 days ago". For millisecond ticks and intervals of seconds, that is never in question.
Storing the next due time instead of the last done time. next_due = now + interval has the same
overflow in it, and the same 49-day fuse. Store when it last happened, and subtract.
Which is safe against the tick counter wrapping?
Show the answer
Answer: C. Only the unsigned subtraction. Casting to signed makes the overflow undefined behaviour rather than
wrapping, and both of the others compute last + interval, which is the value that overflows.
12.4 PWM: dimming an LED
PWM makes an LED half bright by turning it fully on and fully off, faster than the eye can follow.
A digital pin has two states, on and off. There is no half. But if it is on for half of every thousandth of a second, the eye averages it, and the LED looks half lit.
The hardware does it with a counter and one more register. The counter runs up to the period and starts again. The pin is high while the counter is below the compare value.
Printed one count at a time, with a period of twenty, it is unmistakable:
compare = 0 duty 0% |....................|
compare = 5 duty 25% |#####...............|
compare = 10 duty 50% |##########..........|
compare = 15 duty 75% |###############.....|
compare = 20 duty 100% |####################|
The duty cycle is compare / period, and that really is all it is. Your
program writes one register to change brightness, and the hardware does the switching forever
afterwards.
Choosing the frequency
Two numbers to respect:
- Above about 100 Hz an LED stops flickering to the eye. Above 1 kHz it stops flickering on camera too
- Above about 20 kHz a motor or buzzer stops whining, because the switching is out of hearing
Then pick the period for the number of brightness steps you want. A period of 1000 gives you 1000 steps, which is far more than anyone can see. A period of 255 gives 256 steps and fits a byte.
Equal steps do not look equal
Set an LED to 10% and then 20%, and the jump looks enormous. Go from 80% to 90% and it looks like nothing happened. The eye judges brightness by ratio.
level duty, in equal steps duty, gamma corrected
0/7 0.0% 0.0%
1/7 14.3% 1.4%
2/7 28.6% 6.4%
3/7 42.9% 15.5%
4/7 57.1% 29.2%
5/7 71.4% 47.7%
6/7 85.7% 71.2%
7/7 100.0% 100.0%
The fix is gamma correction: put the corrected values in a const
lookup table and index it with the level you want. Volume 09 showed exactly such a table sitting in
.rodata, costing flash and no RAM at all.
Writing the compare register at any old moment. If it changes half way through a cycle, that cycle comes out the wrong length and the LED flickers or the motor jerks. Timers have a preload or shadow register for this: the new value is accepted, and the hardware applies it at the start of the next cycle.
An LED is driven at 50% duty with a period of 1000 and a PWM frequency of 1 kHz. You double the period to 2000 and leave the compare at 500. What changes?
Show the answer
Answer: B. Duty is compare over period, so 500/2000 is 25% instead of 50%. And the counter now takes twice as long to go round, so the frequency halves. Changing the period changes both.
12.5 State machines in C
A state machine is how one loop does several timed things at once without ever waiting for any of them.
Once delays are gone, the question becomes how to express something that has stages. A pedestrian crossing holds each colour for a set time and reacts to a button. Written with delays it would block for twenty seconds at a stretch. Written as a state machine it blocks for nothing.
The shape in C is an enum of states, a variable holding the current one, and a switch.
typedef enum {
TRAFFIC_GREEN, TRAFFIC_AMBER, TRAFFIC_RED, TRAFFIC_RED_AMBER
} light_state_t;
static light_state_t state = TRAFFIC_GREEN;
static uint32_t entered_at = 0u;
static uint32_t held_for(uint32_t now)
{
return (uint32_t)(now - entered_at); /* the same safe subtraction */
}
static void crossing_step(uint32_t now) /* one pass, never waits */
{
switch (state) {
case TRAFFIC_GREEN:
if (wants_cross && held_for(now) >= MIN_GREEN_MS) {
enter(TRAFFIC_AMBER, now);
}
break;
case TRAFFIC_AMBER:
if (held_for(now) >= AMBER_MS) {
wants_cross = false;
enter(TRAFFIC_RED, now);
}
break;
case TRAFFIC_RED:
if (held_for(now) >= RED_MS) {
enter(TRAFFIC_RED_AMBER, now);
}
break;
case TRAFFIC_RED_AMBER:
if (held_for(now) >= RED_AMBER_MS) {
enter(TRAFFIC_GREEN, now);
}
break;
default:
enter(TRAFFIC_RED, now); /* an impossible state is a safe one */
break;
}
}
enter records the new state and the moment it started, so every held_for is measured from the
right instant.
Run it against a simulated clock and the sequence falls out:
0 ms start in GREEN
2000 ms button pressed
5000 ms GREEN -> AMBER
8000 ms AMBER -> RED
16000 ms RED -> RED+AMBER
18000 ms RED+AMBER -> GREEN
20000 ms button pressed
23000 ms GREEN -> AMBER
the loop ran 341 times and never once waited
The button was pressed at 2000 ms and the lights did not change until 5000 ms, because traffic is promised five seconds of green. The machine remembered the request in the meantime. That is the thing a state machine gives you that a sequence of delays cannot.
State machines are the backbone of firmware, and there is far more to them: Moore and Mealy, encoding, minimisation, and how to make one safe against impossible states. The State Machines from Zero course covers all of it from the beginning, in hardware and in C.
Habits worth copying
- One
enterfunction, so the timestamp can never be forgotten - A
defaultcase that goes somewhere safe, rather than doing nothing - No delays, no waiting, and no loops inside a state - one pass does one decision
- Keep the step function free of hardware detail; call drivers from it, as Volume 10 built them
Putting a delay_ms inside one of the cases, just for a moment, to get something working. The whole
machine stops during it, including every other job in the loop. If a state needs to pause, that is
what the timestamp is for: it is already a state with a duration.
Why does crossing_step record entered_at every time the state changes?
Show the answer
Answer: C. Each state's exit condition is an elapsed time. Storing the moment the state began turns "wait three seconds" into "has it been three seconds", which needs no waiting at all.
What you learned
- A busy-wait delay occupies the processor completely; in a measured run it missed all four button presses
- Delay loops need
volatileor they are optimised away, and their timing changes with the compiler - A hardware timer counts on its own, and its rate is clock divided by prescaler and period, each register holding one less
- SysTick exists for one job: a steady tick, kept by a one-line handler
- Replace every delay in a main loop with
if (now - last >= interval) - Always subtract;
now >= last + intervalfails the first time the counter wraps, after 49.7 days - Unsigned arithmetic wraps by definition, so the subtraction stays correct across the wrap
- PWM is a counter and a compare value, and duty cycle is simply one divided by the other
- Pick above 100 Hz for LEDs and above 20 kHz for motors, then choose the period for the steps you want
- Equal duty steps look uneven, so store a gamma-corrected table in flash
- A state machine is an enum, a current state, a timestamp and a switch
- Give the switch a
defaultthat moves to a safe state
Practice
Rewrite this so it does not block, and so a button can be read at the same time.
for (;;) { led_on(); delay_ms(100); led_off(); delay_ms(900); }
Show the solution
The LED is on for 100 ms out of every 1000. Keep that as two states, or as one comparison against the position within the second.
static uint32_t cycle_start;
for (;;) {
uint32_t now = ticks;
uint32_t into_cycle = (uint32_t)(now - cycle_start);
if (into_cycle >= 1000u) {
cycle_start = now;
into_cycle = 0u;
}
led_set(into_cycle < 100u);
check_button(); /* now runs a few thousand times a second */
}
Note cycle_start = now rather than cycle_start += 1000. Both are defensible: adding keeps the
average period exact even if a pass is late, while assigning cannot run away if the loop is badly
delayed. For a blinking LED it does not matter; for a control loop, adding is usually right.
A timer clock is 16 MHz. You want a PWM frequency of 20 kHz with 400 steps of control. Work out the prescaler and period register values, and check the result.
Show the solution
The counter must tick 400 times per PWM cycle, 20000 times a second, so the timer needs 20000 × 400 = 8000000 counts per second.
16000000 / 8000000 = 2, so the clock is divided by 2, and the prescaler register holds 1.
The period register holds 400 - 1 = 399.
Check: 16000000 / 2 / 400 = 20000 Hz.
20 kHz is above hearing, which is why this is the usual choice for a motor. 400 steps is plenty: you cannot hear or see the difference between 0.25 per cent apart.
This has been running fine for six weeks and then stops reacting. Find the bug.
if (ticks > last_sample + SAMPLE_MS) { take_sample(); last_sample = ticks; }
Show the solution
Two bugs, and the second is the one that waited six weeks.
last_sample + SAMPLE_MS overflows when last_sample is near the top of a uint32_t. After the
counter wraps, ticks is a small number and the sum is a huge one, so the condition is false and
stays false for another 49 days.
Six weeks is 42 days, which is suspiciously close to 49.7 - almost certainly the first wrap.
if ((uint32_t)(ticks - last_sample) >= SAMPLE_MS) {
take_sample();
last_sample = ticks;
}
The smaller bug: > rather than >= makes every interval one tick longer than asked for. Harmless
here, and worth fixing anyway.
Also read ticks once into a local. It is written by an interrupt, so the two reads in the original
can return different values.
A design needs an LED that fades smoothly from off to full over two seconds and back, while a serial port keeps receiving. Sketch the structure. What runs where?
Show the solution
Three pieces, none of them waiting.
The hardware timer produces the PWM. Set it up once at start-up, above 1 kHz, with a period giving as many steps as the table has. After that it needs no attention.
An interrupt collects serial bytes into a ring buffer, as Volume 11 built.
The main loop updates the fade. Every 10 ms or so it moves to the next brightness level and writes the gamma-corrected value from the table into the compare register. Four seconds there and back at 10 ms per step is 400 steps, so a 200-entry table used forwards then backwards fits neatly.
if ((uint32_t)(now - last_step) >= 10u) {
last_step = now;
level += direction;
if (level == 0u || level == LEVELS - 1u) {
direction = -direction;
}
TIM3->CCR1 = gamma_table[level];
}
Nothing in this waits, so the serial data keeps flowing and the loop stays free.
Add a state to the crossing so that the button is ignored for ten seconds after a crossing finishes, to stop somebody holding up traffic repeatedly. Where does it go?
Show the solution
It does not need a new state. It needs one more condition on the transition out of GREEN, and the timestamp is already there.
case TRAFFIC_GREEN:
if (wants_cross && held_for(now) >= MIN_GREEN_MS) {
enter(TRAFFIC_AMBER, now);
}
break;
Raising MIN_GREEN_MS from 5 to 15 seconds does exactly what was asked, because GREEN is entered
the moment the previous crossing ends, and held_for is measured from then.
Adding a separate state would also work and is worth considering if the behaviour needs to differ in other ways - a different light pattern, or a "wait" sign for pedestrians. If the only difference is a duration, prefer the extra condition. Every state you add doubles the number of interactions somebody has to reason about.
Interview corner
The wraparound question
"Why is if (now - last >= interval) preferred over if (now >= last + interval)?"
Show the solution
"Because the second one breaks when the tick counter wraps. Unsigned arithmetic in C is modular, so
now - last gives the true elapsed time even across a wrap - the two wraps cancel. But
last + interval can overflow past the top of the range. A freshly wrapped now is then a small
number compared against a huge one, so it reports not yet and keeps doing so.
At a 1 kHz tick a uint32_t wraps after 49.7 days, so this is a bug that survives every test and
then appears in an installed device. The rule I follow is never to add to a timestamp, only ever to
subtract two of them."
Delays
"When is it acceptable to use a blocking delay?"
Show the solution
"During start-up, before the system has anything else to do - waiting for a sensor or display to settle after power-up. And sometimes inside a driver for a hardware requirement measured in microseconds, where the alternative costs more than it saves.
Inside a main loop, essentially never, because everything else stops. I would also make sure the
delay variable is volatile, or the compiler will delete the loop. And I would not let the timing
depend on the optimisation level, which means using a timer rather than a counted loop wherever
accuracy matters."
PWM
"How would you dim an LED from a microcontroller with only digital pins?"
Show the solution
"PWM. Set a timer running with a period and a compare value, and the pin is high while the counter is below the compare. The duty cycle is compare over period, and the eye averages it, so it looks like a brightness.
I would pick the frequency above about 1 kHz so there is no flicker, even on camera. Then I would
choose the period for the number of steps I want, and 256 or 1000 are both common. The part people miss is that equal
steps of duty do not look equal, because perception is roughly a power law. So I would put
gamma-corrected values in a const table in flash, and index it with the level. I would also write
the compare register through the timer's preload, so the change takes effect at a cycle boundary
rather than glitching mid-cycle."
Structure
"How do you make one main loop do several timed things at once?"
Show the solution
"A tick counter and one last timestamp per job. Each pass of the loop checks each job with a
subtraction and does the work if its interval has elapsed. Nothing blocks, so all of them make
progress and adding another costs four lines.
For anything with stages rather than a fixed period, I use a state machine. An enum, a current state, the time the state was entered, and a switch that does at most one transition per pass. That keeps the timing logic and the sequencing in one readable place, and it keeps the loop free for everything else. Work that genuinely cannot wait goes in an interrupt instead, and hands data to the loop through a queue."
Next, Volume 13 puts all of this together. Writing a peripheral driver properly: a clean header, a hardware layer you can swap out, error handling that does not lie, and a driver for a real sensor over I2C.
Key words from this volume
Every word below has a plain-English entry in the glossary.