← Back to list

Day 63: PWM Explained — How Microcontrollers Control Motors, LEDs, and Power

And why 50% duty cycle has nothing to do with “half brightness”

Ameya kshirsagar · 2026-03-19 17:39 · 0 claps · 7.0 min read
#embedded-systems #microcontrollers #pwm #firmware #electronics
Open on Medium ↗

Day 63: PWM Explained — How Microcontrollers Control Motors, LEDs, and Power

And why 50% duty cycle has nothing to do with “half brightness”

There’s a moment every embedded developer remembers. You write a clean fade loop — for i in range(0, 256): set_pwm(i) — expecting a silky cinematic transition from dark to full brightness. What you get instead is the LED snapping to near-full brightness in the first 30 steps, sitting there indifferently for the next 200, and blinking off. You stare at it. You stare at your code. The code is correct. The math is correct.

The problem isn’t the code. The problem is you.

[embed]

What PWM Actually Is

Pulse Width Modulation is not magic. It’s a lie told very fast.

A microcontroller’s GPIO pin is binary — it’s on (3.3V or 5V) or it’s off (0V). There is no “medium.” PWM exploits the fact that if you switch a pin on and off faster than a system can respond, the system perceives an average — not a sequence of pulses.

The key number is the duty cycle: the percentage of time the signal is HIGH within each period.

Period: |████░░░░░░|████░░░░░░|████░░░░░░|
         HIGH  LOW
Duty Cycle = 40%

A motor receiving 40% duty cycle “sees” roughly 40% of the supply voltage. An LED receiving 40% duty cycle appears to be at… well. That’s where it gets interesting.

The hardware is linear. Your eye is not.

Your Brain Is Lying to You (And It Has Been Since Birth)

Human vision follows a power-law response. We are extraordinarily sensitive to changes at low brightness and nearly blind to changes at high brightness. This isn’t a bug in your biology — it’s why you can read a book by candlelight and also function outdoors on a sunny day without your visual cortex exploding.

The consequence for PWM:

Setting an 8-bit PWM register to 77 out of 255 — a 30% duty cycle — produces a perceived brightness of roughly 65%.

If you want a light that looks 30% bright to a human, you need a PWM value of approximately 8 out of 255. Not 77. Eight.

This is called the 30% Illusion, and it is the root cause of every “why does my LED fade look terrible” StackOverflow thread ever written.

The correct solution is gamma correction — or better yet, a CIE 1931 Lightness lookup table, which is the actual scientific model of how human lightness perception works (developed to be perceptually uniform, not just “gamma-ish”). Apply it to your PWM values before writing to the register. Your fade will look silky. Your users will feel nothing unusual, because it will seem obvious that the light fades smoothly. That’s the sign you did it right.

The Hardware: How a Timer Becomes a Dimmer

Every PWM peripheral in a modern MCU is built around three things:

1. A Free-Running Counter A timer register counts from 0 up to some PERIOD value, then resets. This defines your PWM frequency. Want 1kHz PWM with a 48MHz clock? Set PERIOD = 48000.

2. A Compare Register (CMPA) When the counter matches your CMPA value, the output pin flips. When it resets to 0, it flips back. Change CMPA, change the duty cycle.

3. Shadow Registers This is the one that bites you. If you write a new duty cycle value directly into the active compare register mid-period, the hardware has to make an immediate decision about a pulse that’s already in progress. The result is a single malformed PWM cycle — shorter than intended, longer than intended, or outright glitched. For a motor, this is a torque spike. For an LED, it’s a flicker your eye catches even when your oscilloscope says everything is fine.

The fix is shadow mode: your write goes into a buffer register, and the hardware only promotes it to the active register at a deterministic moment — typically when the counter hits zero at the start of a new period. Enable this. Always.

c

// STM32 example — load at period boundary, not immediately
TIM1->CR1 |= TIM_CR1_ARPE;   // Auto-reload preload enable
TIM1->CCMR1 |= TIM_CCMR1_OC1PE; // Output compare preload enable

Edge-Aligned vs. Center-Aligned: The Choice That Matters for LEDs

There are two common PWM counter modes:

Edge-Aligned (Up-Count): Counter goes 0 → PERIOD → 0 → PERIOD. The pulse always starts at the left edge of the period. Simple. Predictable.

Center-Aligned (Up-Down Count): Counter goes 0 → PERIOD → 0 → PERIOD → 0. The pulse is symmetric around the center of the period. This reduces EMI and is popular for motor control because it balances switching events.

For LED dimming, use edge-aligned. Here’s why:

When you update a duty cycle mid-run in center-aligned mode, even with shadowing enabled, the symmetry math produces a single distorted cycle. Texas Instruments has documented this: a transition from 20% to 80% duty cycle in Up-down mode will produce one intermediate cycle at approximately 71% before settling. For a motor spinning at 20,000 RPM, this is imperceptible noise. For a LED you’re watching fade, it’s a visible ghost frame.

Edge-aligned doesn’t have this artifact. The pulse starts fresh at each period boundary. Shadow registers + edge-aligned = clean, deterministic transitions.

The WS2812 Has a Feature That Looks Like a Bug

The WS2812 — the addressable LED inside every NeoPixel strip, every maker project, every LED controller you’ve ordered from AliExpress — has a peculiarity in its internal PWM driver: at input values below 20, the actual duty cycle is shorter than a linear calculation would produce.

When engineers first noticed this, they called it a defect.

It isn’t. It’s hardware-level gamma correction.

Because the WS2812 ramps up slower at the bottom of the range, dark gradients are rendered with more steps, more subtlety, and less banding. Colors that should look nearly black actually look nearly black. Rainbows don’t have a “dead zone” between blue and green where everything collapses into a single indistinguishable value.

The punchline: many clone WS2812 chips fixed this “bug.” They output a perfectly linear duty cycle across all 256 values. They are, in the most practical sense, worse LEDs — specifically for the dark-fade use case that matters most aesthetically.

If you’re building a quality light controller, your LUT for WS2812 devices should manually clamp the first several output values:

c

// First 9 entries of a perceptually-correct WS2812 LUT
uint8_t gamma_lut[256] = {0, 0, 0, 1, 1, 1, 2, 2, 2, 3, ...};

The first few logical “steps” map to the same physical output — and that’s correct, because human vision can’t resolve them anyway.

10-Bit PWM: Why You Need More Than 256 Steps

The CIE 1931 Lightness formula, applied to 8-bit PWM (256 output steps), has a problem: the first 12 input values all map to an output of 0. Nearly 5% of your control range is dead. The lowest steps in your fade are invisible, and then suddenly — the LED is on.

The solution is 10-bit PWM: 1,024 output steps instead of 256.

With 4× the resolution, the CIE curve can place meaningful output values at the dark end without losing the precision you need for near-black rendering. The math fits. The dead zone disappears. The fade starts from a whisper instead of silence.

Most modern ARM Cortex-M MCUs support this natively. If you’re still on 8-bit, consider whether you need a hardware upgrade or whether you can fake it with dithering — rapidly alternating between adjacent values to simulate a fractional duty cycle.

Motors Are Different: PWM as an Analog Proxy

For DC motors and servos, PWM duty cycle directly controls torque and speed via average voltage. But there are two things that kill motors and drivers that nobody mentions in tutorials:

1. Frequency matters more than you think.

  • Too low (< 1kHz): The motor physically buzzes. You can hear the switching frequency as an audible tone. Your gearbox hates this.
  • Too high (> 100kHz): Every switching transition dissipates energy in the gate driver and MOSFET. Thermal losses dominate. The efficiency you thought you gained from PWM control gets eaten by switching heat.
  • The sweet spot for most motors is 10kHz–50kHz — above human hearing, below where switching losses dominate.

2. Dead-time insertion for H-bridge control. In an H-bridge motor driver, you have two MOSFETs switching complementarily — when one is ON, the other must be OFF. But MOSFETs don’t turn off instantly. If you naively write HIGH to one gate and LOW to the other simultaneously, there's a brief window where both are conducting. This is called shoot-through, and it shorts your power supply directly to ground. The current spike is fast, massive, and will destroy your MOSFETs.

The fix is dead-time: a configurable delay during which both switches are OFF during every transition. Modern motor control peripherals have dedicated dead-time registers. Use them. Set them conservatively.

The Bigger Picture: SVM and Industrial Control

Everything above describes single-channel PWM. When you scale to three-phase motor drives — electric vehicles, HVAC compressors, industrial servos — the technique evolves into Space Vector Modulation (SVM).

Rather than controlling three phases independently as separate PWM channels, SVM treats the entire motor stator as a single mathematical system. The three phase voltages are combined into one rotating vector in a 2D complex plane, and the switching sequence is optimized to track that vector with maximum fidelity and minimum switching events.

The practical results:

  • 15% higher output voltage from the same DC bus, compared to sinusoidal PWM
  • Roughly 50% fewer switching transitions per cycle, which means dramatically less heat
  • Smoother torque because the vector representation inherently balances the three phases

This is why your EV’s inverter runs cool at highway speeds despite switching at tens of kilohertz while delivering tens of kilowatts. The switching math is efficient, not just fast.

The Mental Model That Ties It Together

PWM is a translation layer between digital hardware and analog physical reality. The translation has two components:

Hardware translation: The timer, compare register, and output pin convert a number in a register into a time-averaged voltage. This is linear, deterministic, and indifferent to what’s receiving the signal.

Perceptual translation: The receiving system — a human eye, a motor’s mechanical inertia, an inductance averaging a switching current — converts that time-averaged signal into something meaningful. This translation is almost never linear.

Good PWM design means understanding both translations. Writing 128 into a PWM register and expecting "50% of everything" is the embedded equivalent of writing 50 in a color picker and expecting medium gray. The math says 50%. The physics says otherwise.


메타데이터
post_id
79a868cf8ac9
slug
day-63-pwm-explained-how-microcontrollers-control-motors-leds-and-power-79a868cf8ac9
url
https://medium.com/@ameyakshirsagar02/day-63-pwm-explained-how-microcontrollers-control-motors-leds-and-power-79a868cf8ac9
canonical_url
https://medium.com/@ameyakshirsagar02/day-63-pwm-explained-how-microcontrollers-control-motors-leds-and-power-79a868cf8ac9
author_url
https://medium.com/@ameyakshirsagar02
status
ok
fetched_at
2026-06-21 07:44:09