Software PWM on a microcontroller: the timer-interrupt method
How to generate PWM in software when you run out of hardware channels: the timer-interrupt counter method, resolution vs frequency maths, multiple channels and bit-angle modulation.
Hardware PWM is best, but microcontrollers have only a few PWM pins. When you need ten dimmable LEDs or several slow heater outputs, software PWM from a timer interrupt does the job with nothing but ordinary I/O pins.
The idea
A timer interrupt fires at a fixed rate. Each time, a phase counter advances by one step and wraps around after N steps. Each output is on while the counter is below that channel's duty value and off for the rest of the cycle.
volatile uint8_t duty[4]; /* 0..255, set from the main loop */
void timer_isr(void) { /* called at a fixed rate */
static uint8_t phase; /* wraps at 256 */
uint8_t out = 0;
phase++;
if (phase < duty[0]) out |= 1 << 0;
if (phase < duty[1]) out |= 1 << 1;
if (phase < duty[2]) out |= 1 << 2;
if (phase < duty[3]) out |= 1 << 3;
PORTB = (PORTB & 0xF0) | out; /* update all pins at once */
}
Resolution vs frequency
The PWM frequency is the interrupt rate divided by the number of steps: fPWM = fISR / N. For flicker-free LEDs you want at least a few hundred hertz. With 256 steps at 200 Hz the interrupt must run at 51.2 kHz.
- On a 16 MHz AVR that leaves about 310 clock cycles per interrupt — comfortable for a handful of channels.
- On a PIC16 at 4 MHz (1 million instructions per second) it leaves under 20 instructions — impossible. Drop to 64 steps at 200 Hz (12.8 kHz interrupts, about 78 instructions each) and it fits.
Rule of thumb: pick the lowest resolution your application can live with. LEDs look smooth with 64 levels if you apply a gamma curve; heaters and slow loads can run at 1–10 Hz with any resolution you like.
Details that matter
- Write the whole port at once, as above, so all channels switch at the same instant instead of drifting apart as the ISR works through them.
- Update duty values atomically. Single-byte values are safe on 8-bit MCUs; 16-bit values need interrupts disabled briefly while you change them.
- 0 % and 100 %: with
phase < dutyand 8-bit values, duty 0 is fully off but 255 is only 255/256 on. Special-case full-on if it matters. - Servos need a 1–2 ms pulse every 20 ms with fine resolution — better served by a timer-compare approach (one interrupt per edge) than by a fixed-rate counter.
- Motors and anything audible need PWM above about 20 kHz; that's a job for hardware PWM.
Bit-angle modulation
If the interrupt load is too high, bit-angle modulation (BAM) produces the same average brightness with far fewer interrupts: for 8-bit resolution you use 8 time slots per cycle with lengths 1, 2, 4 … 128 units, and in each slot you output the corresponding bit of every channel's duty value. Eight interrupts per cycle instead of 256 — ideal for driving many LEDs from a small microcontroller.
Keep reading
Torry's Delphi Editor & curator
Hi, I'm Torry's Delphi. For more than ten years I've been writing software that collects and organises data — scrapers, catalogues, automation pipelines. Repair documentation turned out to be the perfect mess to clean up.
The files listed here were collected by the community in the Telegram channel @schematicslaptop, plus a Google Drive archive. I wrote a parser that reads every file name, recognises the brand, the ODM manufacturer (Compal, Quanta, Wistron…) and the board number, and turns it into pages you can search and browse.
Next to the catalogue I keep a small set of practical notes on electronics and PCB work — KiCad libraries, moving designs from Eagle, making prototype boards at home, LCDs and EEPROMs. If something is sorted wrong or a note is unclear, write to me and I'll fix it.