Learning Goals 5 min
By the end of this lesson you will be able to:
- Use
analogWrite(pin, level)with a value from 0 to 255 to control the brightness of an LED channel — and recognise which Arduino pins support it (the ones marked ~ on the board). - Combine three brightness values on an RGB LED to produce any colour on the visible spectrum — orange (255, 100, 0), purple (128, 0, 128), warm white, soft pink, hot magenta.
- Write a smooth fade between two colours by stepping each channel one unit at a time inside a
forloop with a smalldelay— the basis of every animated lighting effect.
Warm-Up 10 min
L01-30 painted seven colours plus black — the eight corners of the RGB cube. Lovely, but a bit blocky: there's a huge gap between "off" and "full brightness" with nothing in between. Today you fill in the gap. The whole visible spectrum becomes accessible from the same wiring.
Quick-fire puzzle
Imagine an old desk lamp with only an on/off switch. You want it to glow at half brightness for late-night reading. You don't have a dimmer dial. But you do have a fast finger.
- Could you make the bulb appear half as bright using only the on/off switch?
- If yes, how fast would you have to flick it to fool your eyes into seeing a steady glow instead of a flicker?
- If you flicked the switch ON 25% of the time and OFF 75% of the time (still very fast), what would your eye see?
Reveal the answer
- Yes. If you turn it on and off equally fast, your eye averages the two states and sees roughly half brightness.
- Faster than about 50 times per second (50 Hz). Below that, you see a flicker; above it, the brain blends the bursts into a steady glow. Cinemas project at 24 frames per second and still look smooth because each frame is flashed multiple times.
- About a quarter brightness. The eye averages "on" time vs. "off" time — 25% on means 25% brightness.
That exact trick is what analogWrite does. The Arduino still only has HIGH and LOW for digital pins — but it flicks them on and off about 490 times per second, with the on/off ratio set by your code. The chip and your eye work together to fake "brightness". This trick has a name: Pulse Width Modulation, PWM for short. Today you'll use it without ever seeing the flicker — and unlock 16 million colours.
New Concept — analogWrite and PWM 15 min
The big idea — one number, 256 brightnesses
You already know digitalWrite(pin, HIGH) and digitalWrite(pin, LOW) — two values, fully on or fully off. analogWrite replaces those two values with a single number from 0 (fully off) to 255 (fully on), with every brightness in between:
analogWrite(pin, 0); // fully off (same as LOW)
analogWrite(pin, 64); // about a quarter brightness
analogWrite(pin, 128); // about half brightness
analogWrite(pin, 192); // about three-quarters
analogWrite(pin, 255); // fully on (same as HIGH)Why 0–255 specifically? Because 256 fits exactly in one byte (8 bits) and is the natural unit of brightness in every computer image format ever invented — JPEGs, PNGs, web colours, your phone's photo gallery. The number is the same here as it is for the red component of rgb(255, 0, 0) in CSS.
Pulse Width Modulation — what's really happening
The Arduino can't actually produce a "half voltage" on a digital pin — there are only HIGH (5 V) and LOW (0 V). What it does instead is flick the pin between HIGH and LOW about 490 times every second, with the proportion of HIGH-time controlled by your value:
analogWrite(pin, 64)→ HIGH for ~25% of each cycle, LOW for ~75%. The LED gets a quarter of the current it would at full on. Your eye sees a steady quarter-brightness glow.analogWrite(pin, 128)→ HIGH for ~50%. Half brightness.analogWrite(pin, 255)→ HIGH for 100%. Identical todigitalWrite(pin, HIGH).
The fraction of time the signal is HIGH is called the duty cycle. The whole technique is called PWM — Pulse Width Modulation — because the width of the HIGH pulse is being modulated (varied) to control the average voltage seen by the load.
The ~ pins — only six of them on a Uno
PWM uses dedicated hardware inside the chip, and the Uno only has six PWM-capable pins. They're marked with a ~ (tilde) symbol next to the pin number on the board. On a standard Uno they are:
// PWM pins on the Arduino Uno:
// D3 D5 D6 D9 D10 D11Call analogWrite on any other pin (D2, D4, D7, D8, D12, D13…) and it doesn't error — it just behaves like digitalWrite would, treating anything above 127 as HIGH and anything below as LOW. So your colour fades flatten to abrupt switches. Always pick from the six ~ pins when you want analog brightness. Lucky for you: D9, D10, D11 — the pins you've used for the LEDs and RGB LED since L01-15 — are all on the list.
Mixing colours with three brightness numbers
An RGB LED with three PWM channels can produce any colour by combining three brightness values from 0 to 255. That's 256 × 256 × 256 = 16,777,216 possible colours — the same number every modern screen and image format uses. A few familiar ones:
| Colour | Red | Green | Blue |
|---|---|---|---|
| Red | 255 | 0 | 0 |
| Orange | 255 | 100 | 0 |
| Yellow | 255 | 255 | 0 |
| Lime green | 128 | 255 | 0 |
| Sky blue | 80 | 180 | 255 |
| Hot pink | 255 | 50 | 150 |
| Purple | 128 | 0 | 128 |
| Warm white | 255 | 180 | 80 |
| Cool white | 180 | 200 | 255 |
These are exactly the same numbers you'd use for rgb(...) in CSS, for "fill" in a drawing app, or for any colour-picker dialog you've ever used. The Arduino just outputs them physically as light instead of pixels on a screen.
The fade pattern — your first animation primitive
To smoothly fade one channel from off to full bright over a second, ramp the value from 0 to 255 inside a for loop (L01-11) with a small delay between steps:
for (int v = 0; v <= 255; v = v + 1) {
analogWrite(RED_PIN, v);
delay(4); // 256 steps × 4 ms ≈ 1 second
}To fade two channels at once (the basis of every colour transition), ramp one up while the other ramps down in the same loop. We'll do exactly that in the worked example.
Why it matters
PWM is one of the highest-mileage tricks in all of electronics. It dims LEDs (today's lesson), controls motor speed (Level 2 — the same 0–255 number, this time deciding how fast the wheels turn), shapes audio waveforms, and runs the brightness control on every laptop, phone and TV backlight ever built. Today's lesson is the gateway to all of it.
Worked Example — mix, fade, transition 20 min
Same wiring as L01-30 — RGB LED on D9/D10/D11. Make sure your three pins really are ~9, ~10, ~11 (PWM-capable). Today you'll write three sketches that each show off one PWM trick: a single named colour, a smooth one-channel fade, and a transition between two colours.
Step 1 — Paint a specific colour
The simplest possible PWM sketch: pick three numbers, call analogWrite three times, leave the colour on forever. Let's start with orange — 255 red, 100 green, 0 blue.
// Solid orange — one colour, no animation
const int RED_PIN = 9;
const int GREEN_PIN = 10;
const int BLUE_PIN = 11;
void setColour(int r, int g, int b) {
analogWrite(RED_PIN, r);
analogWrite(GREEN_PIN, g);
analogWrite(BLUE_PIN, b);
}
void setup() {
setColour(255, 100, 0); // orange — set once and leave it
}
void loop() { }Upload. The LED glows a clear orange. Notice what changed from L01-30: the helper is the same shape, but its arguments are now numbers instead of HIGH/LOW, and the call is analogWrite instead of digitalWrite. Same wiring, completely different palette.
Experiment: change 100 to 50 — the orange becomes more red. Change to 200 — it slides toward yellow. Each digit you change physically shifts the LED's colour. You're driving the colour mixer in real time.
Step 2 — Smoothly fade one channel
Now make red fade in from off to full bright over one second, then fade back out. Both happen in loop() so the cycle repeats forever.
// Red breathe — fade in, fade out, repeat
const int RED_PIN = 9;
const int GREEN_PIN = 10;
const int BLUE_PIN = 11;
void setup() {
analogWrite(GREEN_PIN, 0); // hold green + blue off
analogWrite(BLUE_PIN, 0);
}
void loop() {
for (int v = 0; v <= 255; v = v + 1) {
analogWrite(RED_PIN, v);
delay(4);
}
for (int v = 255; v >= 0; v = v - 1) {
analogWrite(RED_PIN, v);
delay(4);
}
}Upload. The LED slowly "breathes" red — gently fading from off to bright over one second, then back to off over the next second, like a sleeping device's indicator. It's the same animation Macs and many devices use as their "asleep" pulse.
Step 3 — Transition between two colours
This is the magic moment. Fade from red (255, 0, 0) to green (0, 255, 0) over one second by ramping two channels at the same time — red down, green up — inside one loop:
// Red-to-green transition — and back
const int RED_PIN = 9;
const int GREEN_PIN = 10;
const int BLUE_PIN = 11;
void setup() {
analogWrite(BLUE_PIN, 0);
}
void loop() {
// Red → green: red goes 255 → 0, green goes 0 → 255
for (int v = 0; v <= 255; v = v + 1) {
analogWrite(RED_PIN, 255 - v);
analogWrite(GREEN_PIN, v);
delay(8);
}
// Green → red: the opposite
for (int v = 0; v <= 255; v = v + 1) {
analogWrite(RED_PIN, v);
analogWrite(GREEN_PIN, 255 - v);
delay(8);
}
}Upload and watch. The LED moves continuously through every shade between red and green — passing through orange and yellow on the way. Then it returns the same way. This is where PWM earns its keep. With just digitalWrite, this exact animation would have to snap-jump red → yellow → green; with PWM it glides.
Try changing one number to make the transition snap-faster: change delay(8) to delay(2) and the whole cycle is four times quicker. Change to delay(20) and it slows to a five-second sweep.
Trace the transition on paper
At each value of v during the red-to-green sweep, fill in what the LED looks like. Use approximate colour names.
v | Red value (255 − v) | Green value (v) | Approximate colour |
|---|---|---|---|
| 0 | 255 | 0 | pure red |
| 64 | ____ | ____ | ____ (red-orange) |
| 128 | ____ | ____ | ____ (yellow-ish) |
| 192 | ____ | ____ | ____ (yellow-green) |
| 255 | 0 | 255 | pure green |
If you can fill this in by mental arithmetic, you've understood the cross-fade trick — and you're a third of the way to writing a full rainbow.
Try It Yourself — three colour tricks 20 min
Goal: Fade from black to full white over two seconds, hold for half a second, then fade back. Repeat forever.
Plan: ramp all three channels at the same time in one for loop. White means R, G and B all at the same value, all the way from 0 to 255.
const int RED_PIN = 9;
const int GREEN_PIN = 10;
const int BLUE_PIN = 11;
void setColour(int r, int g, int b) {
analogWrite(RED_PIN, r);
analogWrite(GREEN_PIN, g);
analogWrite(BLUE_PIN, b);
}
void setup() { }
void loop() {
for (int v = 0; v <= 255; v = v + 1) {
setColour(v, v, v);
delay(8);
}
delay(500);
for (int v = 255; v >= 0; v = v - 1) {
setColour(v, v, v);
delay(8);
}
}Questions:
- Does the perceived brightness ramp feel even, or does it jump quickly near the start and crawl near the end? Why? ____ (Hint: your eye is more sensitive to small changes at low light levels.)
- If you change
setColour(v, v, v)tosetColour(v, v / 2, 0), what colour does the fade become and why? ____ - What single change to one loop makes the "fade up" twice as fast as the "fade down"? ____
Goal: A random colour every two seconds. The LED jumps to a new fully-random colour without fading, sits for two seconds, then jumps again. Each random value is from 0 to 255.
Plan: use random(0, 256) three times to pick R, G and B. random(a, b) returns a number from a up to but not including b, so random(0, 256) gives 0–255 — exactly the analogWrite range. (You met random in L01-22 and L01-29's stretch.)
const int RED_PIN = 9;
const int GREEN_PIN = 10;
const int BLUE_PIN = 11;
void setup() {
Serial.begin(9600);
}
void loop() {
int r = random(0, 256);
int g = random(0, 256);
int b = random(0, 256);
analogWrite(RED_PIN, r);
analogWrite(GREEN_PIN, g);
analogWrite(BLUE_PIN, b);
Serial.print("rgb("); Serial.print(r);
Serial.print(", "); Serial.print(g);
Serial.print(", "); Serial.print(b);
Serial.println(")");
delay(2000);
}Questions:
- The Monitor prints each colour you pick. Look at the values when the LED looks bright vs. when it looks dim. What's the pattern? ____
- What's the chance that
randompicks the exact value(0, 0, 0)on any given pass — and what would the LED do that pass? ____ - How would you make every random colour "vivid" — i.e. always have at least one channel close to 255? ____ (Hint: pick three random values, then set the largest one to 255.)
Goal: A typed-colour preset picker. Type a single letter into the Serial Monitor and the LED instantly jumps to a named colour — using non-trivial PWM values, not just the eight digital corners.
| Key | Colour | R G B |
|---|---|---|
o | Orange | 255 100 0 |
p | Purple | 128 0 128 |
i | Pink | 255 50 150 |
l | Lime | 128 255 0 |
s | Sky | 80 180 255 |
w | Warm white | 255 180 80 |
k | Off | 0 0 0 |
Plan: standard input pattern (L01-26), switch on the character (L01-28), seven branches each calling setColour(r, g, b) with the right three numbers.
Questions:
- Compare your "purple" (128, 0, 128) to L01-30's digital magenta (255, 0, 255). They both have only red and blue on — what's the visible difference? ____
- The "warm white" preset uses 255, 180, 80. Replace it with a pure 255, 255, 255 and compare. Which one looks more like a candle flame? ____
- Add an eighth command of your own: pick a real-world colour (e.g. your favourite team's, a fruit, a flag stripe), look up its rgb values online, and add it as one more case. ____
Mini-Challenge — the rainbow fade 10 min
"Sweep the whole spectrum, smoothly"
Today's payoff: a sketch that fades through the entire visible rainbow continuously, never reaching black, never snapping — every pair of consecutive frames separated by only one PWM step. The sequence goes red → yellow → green → cyan → blue → magenta → red, repeating forever.
The trick: at every moment, exactly one channel is ramping up while another is ramping down, and the third is fixed at 0 or 255. Six phases of 256 steps each = ~1500 frames per loop, with a small delay between each. Pseudocode:
- Phase 1 (red → yellow): R = 255, G goes 0 → 255, B = 0
- Phase 2 (yellow → green): R goes 255 → 0, G = 255, B = 0
- Phase 3 (green → cyan): R = 0, G = 255, B goes 0 → 255
- Phase 4 (cyan → blue): R = 0, G goes 255 → 0, B = 255
- Phase 5 (blue → magenta): R goes 0 → 255, G = 0, B = 255
- Phase 6 (magenta → red): R = 255, G = 0, B goes 255 → 0
Your task:
- Use the
setColour(r, g, b)helper from the worked example. - Write six
forloops, one per phase, withdelay(8)between steps. - Place them back-to-back inside
loop(). The cycle restarts automatically.
It works if:
- The LED continuously cycles through the rainbow without ever flashing black or snapping.
- The full cycle takes about 12 seconds (six phases × 256 steps × 8 ms).
- You can tell red from orange from yellow from lime from green — the transitions feel continuous, not stepped.
Reveal one valid sketch
// Rainbow fade — six phases × 256 steps
const int RED_PIN = 9;
const int GREEN_PIN = 10;
const int BLUE_PIN = 11;
void setColour(int r, int g, int b) {
analogWrite(RED_PIN, r);
analogWrite(GREEN_PIN, g);
analogWrite(BLUE_PIN, b);
}
void setup() { }
void loop() {
for (int v = 0; v <= 255; v = v + 1) { setColour(255, v, 0); delay(8); } // red → yellow
for (int v = 0; v <= 255; v = v + 1) { setColour(255 - v, 255, 0); delay(8); } // yellow → green
for (int v = 0; v <= 255; v = v + 1) { setColour(0, 255, v); delay(8); } // green → cyan
for (int v = 0; v <= 255; v = v + 1) { setColour(0, 255 - v, 255); delay(8); } // cyan → blue
for (int v = 0; v <= 255; v = v + 1) { setColour(v, 0, 255); delay(8); } // blue → magenta
for (int v = 0; v <= 255; v = v + 1) { setColour(255, 0, 255 - v); delay(8); } // magenta → red
}Six near-identical loops, each handling one corner-to-corner edge of the colour cube. The pattern in every line is the same: one channel is fixed, one ramps up with v, one ramps down with 255 - v. The visible result is a single continuous sweep through every hue the LED can produce. This sketch — with a millis()-based scheduler instead of delay — is the heart of nearly every "mood lamp" product you can buy.
Recap 5 min
The Arduino fakes brightness by flicking a digital pin on and off about 490 times per second, with the on-time fraction controlled by analogWrite(pin, value) — value 0–255. That trick is called PWM, and on a Uno only the six ~-marked pins (D3, D5, D6, D9, D10, D11) support it. With three PWM channels and an RGB LED you can mix all 16,777,216 colours every modern screen produces, including all the in-between shades that digitalWrite couldn't reach. A for loop walking one or more channels through 0–255 with a small delay produces a smooth fade — the building block of every animated lighting effect.
- PWM (Pulse Width Modulation)
- A technique for faking analog values out of a digital pin by switching it on and off very fast. The fraction of time the pin is HIGH controls the average voltage your load sees.
- Duty cycle
- The percentage of each PWM cycle the pin spends HIGH. 0% = always off, 100% = always on, 50% = half brightness.
analogWrite(pin, 128)≈ 50% duty cycle. analogWrite(pin, value)- Sets a PWM pin's duty cycle to
value/255.valuemust be 0–255 inclusive. Has no effect on non-PWM pins beyond mimicking digitalWrite. - ~ pin
- An Arduino digital pin that supports PWM, marked with a tilde (~) printed beside its number on the board. On a Uno: D3, D5, D6, D9, D10, D11.
- 0–255
- The standard "one byte" brightness range used by Arduino's analogWrite, by CSS, by image formats, and by every digital colour picker. 256 distinct levels per channel.
- Cross-fade
- An animation pattern where two channels move in opposite directions inside the same loop — one ramping up while the other ramps down — to transition smoothly between two colours via every in-between shade.
Homework 5 min
The sunrise lamp. Build a sketch that imitates a sunrise: starting from black (off), the LED slowly fades over 30 seconds through a deep red, then orange, then warm yellow, and finally settles on a bright "daylight" white. Then it stays on. Pressing the Arduino's reset button starts the sunrise over.
- The whole sunrise should take about 30 seconds — split into roughly three 10-second phases.
- Phase A (deep red brightening): R ramps 0 → 255, G and B stay at 0. About 10 s — that's
delay(40)per of the 256 steps. - Phase B (red → warm orange): R stays at 255, G ramps 0 → 180. About 7 s — pick the right per-step delay.
- Phase C (orange → warm white): G stays at 180, B ramps 0 → 200. Another 7 s.
- Then
loop()does nothing and the LED holds at the final colour.
(All three phases should sit inside setup() so the sunrise happens once, not on every loop pass. loop() can be an empty body.)
Also: a design reflection on paper.
- Pick any colour you can see in the room you're sitting in. Try to guess its RGB values (each from 0–255). Then take a phone photo, open it on a laptop, and use any colour-picker tool (Paint, Photoshop, the developer-tools eyedropper in any browser) to read the actual numbers. How close was your guess? ____
- Your sketch used three phases of 256 steps each — 768 frames in total. How many actual
analogWritecalls does that mean per channel? ____ - Why did the lesson use
delay(4)for one-second fades but L01-22's reaction timer usedmillis()? ____ (Hint: blocking is fine here because nothing else needs to happen during the fade.) - If you wanted the sunrise to also respond to a button press (skip to "full daylight" instantly), would
delay-based fades still work? Why not? ____
Bring back next class:
- The saved
.inofile (call itsunrise-lamp). - A 30-second phone video of the full sunrise sequence, ideally filmed in a darkened room.
- Your four written reflection answers, in your notebook.
Heads up for next class: L01-32 "Tone and Frequency" moves from light to sound. Your piezo buzzer is the audio equivalent of an LED — the tone() calls you've been using are doing for hertz what analogWrite does for brightness. Bring your buzzer back to the breadboard.