Learning Goals 5 min
By the end of this lesson you will be able to:
- Use
map(value, fromLow, fromHigh, toLow, toHigh)to scale a number from one range to another, replacing the manual division-and-addition formulas from L01-40 with a single readable line. - Apply the same call in three useful directions: shrinking (1023 → 255 for PWM), stretching (1023 → 0–10000), and reversing (1023 → 255–0 to flip the direction of a knob).
- Pair
map()withconstrain(value, lo, hi)when you need to clamp the result so values outside the input range don't produce wild output values.
Warm-Up 10 min
L01-40 ended with you writing little ad-hoc formulas to fit a sensor's range to an actuator's range: knob / 4 to fit 0–1023 onto 0–255, knob * 2 + 200 to fit it onto 200–2246. Each formula was a tiny custom calculation, designed once for a specific job. Today's lesson is the function that makes all of them go away.
Quick-fire puzzle
You want a sensor reading of 0–1023 to come out as a number 0–255 for an LED. Two students propose formulas:
// Student A:
int brightness = value / 4;
// Student B:
int brightness = value * 255 / 1023;- For value = 0, what does each formula give?
- For value = 1023, what does each formula give?
- For value = 511 (mid-knob), what does each give? Are they exactly the same?
- Which formula is easier to read three months from now, when you've forgotten where the 4 came from?
Reveal the answer
- 0 in both cases.
- A gives 255 (1023/4 = 255 r 3, truncated); B gives 255 (511×255/1023 = 130 425/1023 ≈ 127.5; for value = 1023, 1023×255/1023 = exactly 255).
- A gives 127 (511/4 = 127 r 3, truncated); B gives 127 (same calculation as above, truncated). Close enough for almost all uses.
- Neither is great. Both hide what's actually happening — "take a number in some range and put it in another range".
map(value, 0, 1023, 0, 255)says exactly that, and that's why Arduino includes it as a built-in.
Today's lesson is one function — map() — but it's the function you'll use more than almost any other from here on. Sensor → motor, sensor → LED, sensor → tone, knob → menu position — all of it is "scale a number from one range to another".
New Concept — one function, five arguments 15 min
The big idea — describe both ranges, let the function do the maths
Arduino provides a built-in function with this signature:
map(value, fromLow, fromHigh, toLow, toHigh)
It takes five integers. The first is the value you have. The next two say what range that value is in ("from"). The last two say what range you want it scaled to ("to"). The function returns an int linearly interpolated between the two ranges.
Read it as English
Whenever you write a map call, you should be able to read it as a sentence:
int brightness = map(knob, 0, 1023, 0, 255);
// "Take knob, which is 0..1023, and re-express it as 0..255."Compare with the old formula. Both lines produce the same numbers, but only one of them explains itself out loud:
// before
int brightness = knob / 4;
// after
int brightness = map(knob, 0, 1023, 0, 255);The maths it does for you
Under the hood, the implementation is roughly:
long map(long x, long inMin, long inMax, long outMin, long outMax) {
return (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
}You don't have to memorise this — but knowing the shape explains everything else in the lesson. The "from" range is converted to a fraction ((x − inMin) / (inMax − inMin)) and then mapped onto the "to" range. Three properties fall out:
- Sending in the bottom of the "from" range returns the bottom of the "to" range.
- Sending in the top of the "from" range returns the top of the "to" range.
- Sending in a value between gets a proportional output between.
Reversing — swap toLow and toHigh
Often you want a knob to behave "backwards" — turning right should give a smaller number. Just swap the last two arguments:
int dimness = map(knob, 0, 1023, 255, 0);
// "Knob 0..1023 → 255..0 (reversed)"Now knob = 0 gives 255, and knob = 1023 gives 0. No subtraction needed, no formula rearrangement — you just reversed the to-range and the function did the rest.
Negative numbers and offset ranges work too
map() doesn't care about positive vs negative; the maths is the same. Useful any time the output range isn't anchored at zero:
// Centre a knob around zero — left = -100, mid = 0, right = +100
int joystick = map(knob, 0, 1023, -100, 100);
// Sensor reading to a temperature scale in degrees C
int tempC = map(adcVal, 0, 1023, -10, 40);The non-clamping gotcha
The biggest surprise about map() is that it does not protect you from out-of-range inputs. Send in a value outside the "from" range and you get a value outside the "to" range — proportionally:
map(2000, 0, 1023, 0, 255);
// returns about 498, not capped at 255!This is sometimes what you want (the maths is honest about the input), but if you then pass that result to analogWrite, you'll get unpredictable behaviour because the value is out of analogWrite's legal 0–255 range.
The fix is constrain(value, low, high), which clamps a value into the legal range:
int safe = constrain(map(input, 0, 1023, 0, 255), 0, 255);Read it inside-out: map first, then constrain the result. This is the safe pattern any time the input might exceed its expected range (LDR readings during calibration, joystick centring with mechanical slop, etc.). For a pot wired correctly, you can skip the constrain — but it's cheap insurance.
Why it matters
"Take a value in one range and re-express it in another" is one of the most common operations in all of programming. map() is Arduino's name for it; Python has the same idea via numpy.interp, JavaScript via libraries like d3-scale, every game engine has a "lerp" function — all the same maths. Today's lesson is the readable, self-explanatory version of every ad-hoc formula you've written for the last three weeks.
Worked Example — rewrite, reverse, stretch 20 min
Same L01-40 wiring: pot on A0, LED on ~D9. You're going to rewrite the dimmer with map() for readability, then use the same function for two more useful jobs in the same sketch — a reversed knob and a stretched scale.
Step 1 — Rewrite the dimmer with map()
// Knob dimmer — map() version
const int POT_PIN = A0;
const int LED_PIN = 9;
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int knob = analogRead(POT_PIN);
int brightness = map(knob, 0, 1023, 0, 255);
analogWrite(LED_PIN, brightness);
Serial.print("knob="); Serial.print(knob);
Serial.print(" bright="); Serial.println(brightness);
delay(30);
}The function does exactly what knob / 4 did, but anyone reading the line can see the input and output ranges at a glance. Upload — it behaves identically to L01-40. The win is in the source code, not the behaviour.
Step 2 — Reverse the knob
Now make the LED dim as you turn right. One change — swap the last two arguments:
int brightness = map(knob, 0, 1023, 255, 0);Upload. Knob fully left → LED full bright; knob fully right → LED off. No subtraction, no new formula, no possibility of off-by-one error. Reversing a control is a literal swap of two numbers.
Step 3 — Stretch to a tone range
Add the L01-14 piezo on D8 to the breadboard. Now drive its frequency from the pot too, with a much wider range than the original L01-40 sketch's knob * 2 + 200:
const int BUZZER_PIN = 8;
// (add to setup):
pinMode(BUZZER_PIN, OUTPUT);
// (in loop after reading knob):
int freq = map(knob, 0, 1023, 100, 5000);
tone(BUZZER_PIN, freq);
Serial.print(" freq="); Serial.println(freq);Same pot. New use. Just the from-range and to-range described. The sketch now drives the LED's brightness and the buzzer's pitch from one knob — turn it, the LED brightens and the pitch climbs, both at once. (Notice: the freq map runs 100 → 5000 — a way wider range than the manual knob * 2 + 200 formula could give cleanly. map() doesn't care; the maths is the same.)
Step 4 — Add a safety constrain
Suppose you want the LED to never go fully dark (so the device always shows some "alive" indicator) and never go quite to full brightness (so it doesn't blind you in a dark room). Map to a smaller range and constrain just to be safe:
int brightness = constrain(map(knob, 0, 1023, 20, 200), 20, 200);For a clean pot, the constrain is redundant — the map output never leaves 20–200 because the input never leaves 0–1023. For a sensor that occasionally reads outside its expected range (a noisy LDR, a worn joystick, a misbehaving thermistor), the constrain is what keeps your analogWrite safe.
Trace four mappings on paper
For each row, work out the output without uploading.
| Call | Input | Output |
|---|---|---|
map(knob, 0, 1023, 0, 255) | knob = 0 | ____ |
map(knob, 0, 1023, 0, 255) | knob = 512 | ____ (≈ half of 255) |
map(knob, 0, 1023, 255, 0) | knob = 256 | ____ (reversed!) |
map(knob, 0, 1023, 100, 5000) | knob = 1023 | ____ (top of the range) |
map(knob, 0, 1023, -100, 100) | knob = 512 | ____ (≈ centre of a centred range) |
If you can fill in all five rows by mental arithmetic, you've internalised the function.
Try It Yourself — three rewrites and a constrain 20 min
Goal: Rewrite each of these manual formulas as a single map() call. No Arduino needed — paper and pencil.
- (a)
int brightness = knob / 4;→map(knob, ____, ____, ____, ____); - (b)
int freq = knob * 2 + 200;→map(knob, ____, ____, ____, ____); - (c)
int stepMs = knob / 5 + 30;→map(knob, ____, ____, ____, ____); - (d)
int reversed = 255 - knob / 4;→map(knob, ____, ____, ____, ____);
Questions:
- For each rewrite, what's the top of the resulting range? (e.g. (a) is 0–255.) ____
- Why is (d) cleaner as
map()than as the manual subtraction? ____ - (b)'s map equivalent gives a slightly different top value than
knob * 2 + 200. By how much, and does it matter? ____ (Hint: the manual formula tops out at 2246; the precise map top would be 2200 if you wrote it that way.)
Goal: An LDR-to-LED brightness sketch with two opposite behaviours — flip a knob (digital toggle) between "follows the light" and "inverse of the light".
Plan: read the LDR on A0 (L01-36 wiring); read a button on D7 (L01-17 wiring) for the mode. Drive an LED on ~D9.
- Button released:
map(light, 0, 1023, 0, 255)— LED brightens with the room light. - Button held:
map(light, 0, 1023, 255, 0)— LED dims as room brightens (a "compensating spotlight" idea).
const int LDR_PIN = A0;
const int BTN_PIN = 7;
const int LED_PIN = 9;
void setup() {
pinMode(BTN_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int light = analogRead(LDR_PIN);
int b;
if (digitalRead(BTN_PIN) == LOW) {
b = map(light, 0, 1023, 255, 0);
}
else {
b = map(light, 0, 1023, 0, 255);
}
analogWrite(LED_PIN, b);
}Questions:
- Cover the LDR. In each mode, what does the LED do? ____
- The only difference between the two map calls is the order of the last two arguments. Is this more or less readable than computing the two cases manually? ____
- What's the smallest possible reading from your LDR in a dark room (try it)? Is it exactly 0, or some small non-zero value? Does the map still output exactly 255 in inverted mode? ____
Goal: A safe sensor scaler for a worn pot. Pretend the pot's track is damaged near the ends — the lowest reading you actually see is ~50, the highest is ~970 (not the theoretical 0/1023). Build a sketch that maps the actual range to a clean 0–255 PWM output, with constrain protecting against any outliers.
const int POT_PIN = A0;
const int LED_PIN = 9;
const int POT_MIN = 50; // measured
const int POT_MAX = 970; // measured
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int knob = analogRead(POT_PIN);
int bright = constrain(map(knob, POT_MIN, POT_MAX, 0, 255), 0, 255);
analogWrite(LED_PIN, bright);
Serial.print("knob="); Serial.print(knob);
Serial.print(" bright="); Serial.println(bright);
delay(30);
}Questions:
- What happens at knob = 40 (below POT_MIN) — what does
mapalone return, and what doesconstrainchange it to? Trace through the maths. ____ - The pot isn't really worn — but doing the same trick on the actual range of a real sensor (LDR, thermistor) is a common production move. Why? ____ (Hint: stretching the working range onto the full output range maximises sensitivity.)
- What would the call look like if you wanted to map the worn range to 0–255 reversed AND constrain? ____
Mini-Challenge — pot picks one LED of five 10 min
"Turn the knob, light up the matching position"
Rebuild your L01-34 wiring — five LEDs in a row on D7–D11 with shared GND — and add a pot on A0 (L01-40 wiring). The sketch: as you turn the knob, one LED lights up at a time. Knob fully left = leftmost LED; mid-knob = middle LED; fully right = rightmost LED. Smooth — no gaps, no overlaps.
Your task:
- Read the pot.
- Map the 0–1023 range onto 0–4 (the LED index).
- Walk the
leds[]array and turn each LED on if its index matches the mapped value, off otherwise. - Update every 50 ms so the position feels live.
It works if:
- One — and only one — LED is on at a time as you turn the knob.
- The lit LED travels left to right as the knob turns right.
- Each LED gets its own roughly-equal slice of knob travel (about 20% each).
Reveal one valid sketch
// Pot picks one LED of five — map() in one line
const int NUM_LEDS = 5;
int leds[NUM_LEDS] = { 7, 8, 9, 10, 11 };
const int POT_PIN = A0;
void setup() {
for (int i = 0; i < NUM_LEDS; i = i + 1) {
pinMode(leds[i], OUTPUT);
}
}
void loop() {
int knob = analogRead(POT_PIN);
int pos = map(knob, 0, 1023, 0, NUM_LEDS - 1); // 0..4
for (int i = 0; i < NUM_LEDS; i = i + 1) {
digitalWrite(leds[i], i == pos);
}
delay(50);
}Two things to notice. First, map(knob, 0, 1023, 0, 4) turns the entire knob's 1024-position resolution into just 5 buckets — quietly trading precision for the right shape of output. That's the whole point: an LED-position task needs 5 values, not 1024. Second, the i == pos trick relies on C++ treating true as 1 and false as 0, which is exactly what digitalWrite wants. Five LEDs, one position, one map call — the cleanest possible implementation.
Recap 5 min
The Arduino's built-in map(value, fromLow, fromHigh, toLow, toHigh) linearly scales a number from one range to another. It replaces every ad-hoc x / 4, x * 2 + 200, 255 - x / 4 with a single self-explaining line. Swap the last two arguments to reverse the direction. Use negative or offset ranges to centre values around zero. Pair with constrain(value, lo, hi) any time the input might exceed its expected range and the output drives a function that's picky about its bounds. From this lesson on, map() is the right tool nearly every time you take a sensor reading and turn it into something else.
map(value, fromLow, fromHigh, toLow, toHigh)- Built-in Arduino function that linearly re-expresses
valuefrom the range [fromLow,fromHigh] into the range [toLow,toHigh]. Reads like a sentence: "take this, which is X..Y, and re-express it as A..B". constrain(value, low, high)- Built-in function that clamps
valueinto the inclusive range [low,high]. Returnslowif too small,highif too big,valueif already inside. The standard partner ofmap()for safety. - Linear scaling
- The maths
map()does — a straight-line conversion between two ranges with equal spacing. The opposite of curves (logarithmic, gamma, exponential) which would crowd values toward one end. - Reversed mapping
- Calling
mapwithtoLow > toHighso the output direction is flipped. Useful for "knob left = high, right = low" controls. Equivalent to subtracting from a constant but more readable. - Clamping
- Restricting a value to stay within a range, replacing out-of-range values with the nearest in-range bound. Done by
constrain. Essential when a downstream function (likeanalogWrite) misbehaves on out-of-range input. - Integer truncation in
map() - Because the Uno's
map()uses integer maths, intermediate divisions throw away the remainder. Fine for big ranges; expect chunky steps when mapping to a very small output range like 0–5.
Homework 5 min
The smooth-gradient RGB knob. Improve last lesson's dual-pot mixer using map() to interpolate continuously across colours instead of switching between three preset bands.
- Wire the same RGB LED (D9 / D10 / D11, L01-30 wiring) plus pots A and B (on A0 and A1).
- Pot A is brightness — map to 0–255 as before.
- Pot B is colour position. Use two
map()calls to interpolate between three reference colours as B sweeps:- B in lower half (0 → 512): blend from red (R=255, G=0, B=0) to green (R=0, G=255, B=0).
- B in upper half (512 → 1023): blend from green to blue (R=0, G=0, B=255).
- The final brightness of each channel is then multiplied by Pot A's brightness fraction. Hint:
finalR = (rawR × brightness) / 255;— gives 0..255 from two 0..255 numbers.
The whole sketch should be 30–40 lines. Once it's working, the LED will sweep continuously from red through orange/yellow to green, then green through teal/cyan to blue — all under your finger.
Also: a design reflection on paper.
- Look back at the four manual formulas you wrote in L01-40's exercises. How many of them are cleaner as
map()calls? How many are cleaner as the original manual form? ____ - The lesson said integer
map()can be chunky on narrow output ranges (like 0–5). Why doesn't this matter for your homework's 0–255 brightness output? ____ map()is linear. Your eye perceives brightness non-linearly (a "gamma curve"). Why might a linear map look "weighted toward dim" in practice? ____ (Hint: small PWM changes near the low end are very noticeable; small changes near the high end aren't.)- If your homework's pot B is wired backwards (left = blue instead of left = red), what one change to your sketch flips it? ____
Bring back next class:
- The saved
.inofile (call itsmooth-rgb-knob). - A 30-second phone video sweeping both pots — show the full red → green → blue gradient at varying brightness.
- Your four written reflection answers, in your notebook.
Heads up for next class: L01-42 "Boolean Logic Gates" takes a step back from sensors to formalise the AND/OR/NOT operators you've been using since L01-21. Same shape as today — small ideas, big applicability — but on bits rather than ranges. After that, L01-43 is the Cluster F project: an auto night light combining everything.