Яркость прикроватной лампы я первые два вечера регулировал тремя кнопками: в коде лежали три заготовленных уровня — 30, 120 и 255 — и каждая кнопка ставила свой. Домашние просили «чуть-чуть посветлее», а между ступенями у меня была пропасть; после третьей просьбы я полез в макетку прикручивать четвёртую кнопку и вовремя остановился. Ступени там, где нужна ручка, — это не решение, а заглушка. Вечер, когда я в четвёртый раз перепаивал кнопки, лампа честно показала: между «почти темно» и «нормально» людям нужно плавно, а не по щелчку.
В этом уроке сделаем настоящий диммер: потенциометр на входе A0, функция map() для перевода 0–1023 в 0–255 и analogWrite на пин 9. Разберём, почему map() целочисленная и почему 512 превращается в 127, а не 128, что такое мёртвая зона ручки на краях хода и зачем рядом с map() всегда живёт constrain(). Если ШИМ для вас пока тёмный лес — сначала вернитесь к уроку про analogWrite и ШИМ, а потом возвращайтесь сюда за ручкой.
Потенциометр на A0: ручка, которая выдаёт 0–1023
Потенциометр — две вещи в одном корпусе: делитель напряжения с готовой ручкой. У детали три ноги: крайние идут на 5V и GND, средняя — на вход A0. Внутри — дорожка с сопротивлением (у моего экземпляра 10 кОм, тот самый номинал из старых радиоприёмников), по которой скользит ползунок: крутите ручку — меняется пропорция между двумя половинами дорожки, и напряжение на средней ноге плавает от 0 до 5 вольт. Питание на крайние ноги подаётся в любой полярности: перепутаете 5V и GND — просто получите шкалу задом наперёд, исправляется одним переворотом проводов.
Аналоговый вход Arduino измеряет это напряжение и выдаёт число от 0 до 1023: это 10-битный АЦП, 0 соответствует 0 В, 1023 — полным 5 В. Как читать аналоговые входы и что ещё к ним подключают, я подробно разбирал в уроке про analogRead — здесь возьмём только главное: analogRead(A0) возвращает готовое число, никаких библиотек и настроек не нужно.
Номинал потенциометра почти не важен: подойдёт любой от 1 до 100 кОм, потому что ток через дорожку смешной — полмиллиампера. Важнее другое: края хода врут. У моего потенциометра последние пару градусов поворота ничего не меняют, а показания у максимума прыгают между 1022 и 1023. Выведите значения в Serial и покрутите свою ручку — вы обнаружите и мёртвую зону, и дребезг последней цифры, и тогда все разговоры про края ниже встанут на место. Заодно посмотрите на среднее положение: у доброй половины дешёвых потенциометров середина хода не ровно 512, а 520–540 — дорожка изнашивается неравномерно.
map(): переводим 0–1023 в 0–255 одной строкой
Проблема диммера в несовпадении диапазонов: АЦП выдаёт 0–1023, а analogWrite хочет 0–255. Функция map(x, inMin, inMax, outMin, outMax) пересчитывает число из одного диапазона в другой по школьной пропорции: map(raw, 0, 1023, 0, 255) растягивает показание ручки на шкалу яркости. Середина 512 станет 127, крайности совпадут: 0 в 0, 1023 в 255. Аргументы читаются как предложение: «возьми raw из диапазона 0–1023 и выдай из диапазона 0–255» — редкий случай, когда функция объясняет себя прямо в вызове.
Внутри map() целочисленная арифметика: дробная часть отбрасывается. 512 × 255 / 1023 = 127,68 — получится 127, а не 128. Для диммера это неважно, глаз разницу соседних уровней не видит, но знать свойство надо: если где-то посчитаете «половина от 255 — это 128», а map вернёт 127 — это не баг, это отброшенная дробь. Есть и более короткая альтернатива: деление raw / 4 даёт практически тот же результат (1023 / 4 = 255), но map() читаемее и главное — гибче: захотите инвертировать ручку или ввести нижний порог, правите аргументы, а не арифметику.
Гибкость map() — это и ловушка. Функция не ограничивает результат: если raw вылезет за 0–1023 (обрыв провода, наводка, 5 вольт с соседней ветки схемы), map честно посчитает и 274, и −10. На реальной плате analogWrite принимает байт, и 274 сворачивается в 18 — яркость прыгнет непредсказуемо. Поэтому рабочая связка всегда парная: map() пересчитал, constrain(bright, 0, 255) подрезал выход за границы. Одна строка страховки экономит вечер отладки.
// Альтернатива map(): 1023 / 4 = 255, деление целочисленноеint bright = analogRead(A0) / 4;analogWrite(9, constrain(bright, 0, 255)); // Минус подхода: нижний порог 30 или инверсия ручки// потребуют переписать арифметику, а не поменять аргументыСобираем диммер: скетч, ШИМ-пины и края ручки
Схема: крайние ноги потенциометра — на 5V и GND, средняя — на A0; светодиод через резистор 220 Ом — на пин 9. Пин именно с волнистой чертой: аппаратный ШИМ на Uno есть только на 3, 5, 6, 9, 10 и 11. На пине 8 реальной платы analogWrite(8, 127) включит светодиод в полную — функция не ругается, просто плавности нет; в симуляторе analogWrite работает на любом пине, но привычку выбирать ШИМ-пины ставьте с первого урока.
Скетч короткий: в loop() читаем ручку, масштабируем, подрезаем, ставим яркость. delay(300) тут только для темпа журнала — сам analogWrite аппаратный: яркость едет за ручкой мгновенно, а пауза нужна лишь чтобы Serial не забивал монитор порта сотнями строк в секунду. На настоящей плате яркость обновляется аппаратно тысячи раз в секунду независимо от loop() — никакого delay между analogWrite не нужно, он только для журнала.
Проверка собранного диммера одна и та же: ручка влево до упора — светодиод потух, вправо — в полную, медленный проход посередке — яркость едет следом без ступеней и подёргиваний. Если на краях светодиод подмигивает — это тот самый дребезг АЦП: constrain() лечит его вместе с выбросами. Мой первый диммер на трех ступенях выглядит так, и разница с ручкой ощущается с первого поворота.
// Три порога — три ступени: «тускло», «средне», «ярко»void setup() { pinMode(9, OUTPUT); Serial.begin(9600);} void loop() { int raw = analogRead(A0); if (raw < 341) { analogWrite(9, 40); // ступень 1 } else if (raw < 682) { analogWrite(9, 130); // ступень 2 } else { analogWrite(9, 255); // ступень 3 } delay(100);}const int LED_PIN = 9; void setup() { pinMode(LED_PIN, OUTPUT); Serial.begin(9600);} void loop() { int raw = analogRead(A0); // ручка: 0–1023 int bright = map(raw, 0, 1023, 0, 255); // масштаб в 0–255 bright = constrain(bright, 0, 255); // страховка от выбросов analogWrite(LED_PIN, bright); delay(300); // пауза только для журнала}| Действие | Функция | Диапазон и пины |
|---|---|---|
| Читаем ручку | analogRead(A0) | 0–1023, входы A0–A5 |
| Масштабируем | map(raw, 0, 1023, 0, 255) | целочисленная: дробь отбрасывается |
| Подрезаем выбросы | constrain(bright, 0, 255) | 0–255, за границу не выйдет |
| Ставим яркость | analogWrite(9, bright) | ШИМ-пины Uno: 3, 5, 6, 9, 10, 11 |
| Сверяем себя | Serial.println(raw) | журнал ручки в мониторе порта |
- Инвертируйте ручку: замените map(raw, 0, 1023, 0, 255) на map(raw, 0, 1023, 255, 0). Против часовой стрелки лампа теперь ярче — некоторые логики удобнее именно так.
- Введите дежурный свет: map(raw, 0, 1023, 30, 255) — на минимальной ручке лампа не гаснет совсем, а горит на тридцати. Так делают с ночниками, которые нельзя выключать до нуля.
- Уберите мёртвую зону: замерьте по Serial реальные края своей ручки (у меня вышло 8 и 1016) и подставьте их в map(raw, 8, 1016, 0, 255) — весь ход ручки работает на яркость.
- Верните ступени, но по-новому: добавьте bright = bright / 10 * 10; после map() — плавная ручка превращается в двадцать шесть аккуратных ступеней. Сравните ощущение с непрерывной версией.
- Проверьте дрейф: держите ручку неподвижно и читайте Serial десять обновлений подряд — если последние цифры прыгают (1019, 1021, 1020), это шум АЦП; усредните три чтения и посмотрите, стало ли спокойнее.
- Потенциометр — готовый делитель с ручкой: крайние ноги на 5V и GND, средняя на A0; analogRead возвращает 0–1023.
- map(raw, 0, 1023, 0, 255) переводит показание АЦП в duty для analogWrite; арифметика целочисленная — 512 даст 127, а не 128.
- map() не ограничивает результат: за краями диапазона нужен constrain(bright, 0, 255) — страховка от обрывов и выбросов.
- Аппаратный ШИМ на Uno — пины 3, 5, 6, 9, 10, 11; на обычном пине реальной платы analogWrite горит в полную.
- Деление raw / 4 — рабочая альтернатива, но map() читаемее и позволяет нестандартные диапазоны вроде 30–255.
- 1. Что вернёт map(512, 0, 1023, 0, 255)?
- 2. Светодиод на пине 8 реальной Uno. Что делает analogWrite(8, 127)?
- 3. raw вышел за диапазон: analogRead вернул 1100 после обрыва. Что вернёт map(1100, 0, 1023, 0, 255)?
- 4. Хотите, чтобы на минимальной ручке лампа горела на 30, а не гасла. Какой вызов верный?
Диммер готов, и это первый прибор, где код не командует светом, а пересказывает ручку. За кадром осталась важная деталь: ручку крутим мы сами — число приходит от человека. В следующем уроке источник числа сменится: фоторезистор в делителе напряжения будет сам замечать, когда в комнате темнеет, а порог с гистерезисом превратит пару if в ночник, который включается и гаснет без вашего участия. Лог с замеренными порогами сохраните: через месяц, когда показания «уедут» из-за пыли на датчике, вы будете благодарны себе за записанные числа. Пока же прогоните диммер по списку экспериментов: инверсия, дежурный свет и замер мёртвой зоны — три приёма, которые потом вспомните в половине проектов с ручками. И покажите диммер домашним: спросят «а можно туда кнопку всё-таки?» — но это уже история про пресеты яркости, и она ждёт вас в следующих уроках.
Частые вопросы по теме урока
- Что вернёт map(512, 0, 1023, 0, 255)?
- map считает в целых числах: 512 × 255 / 1023 = 127,68, дробь отбрасывается — получается 127. Половина от 255 была бы 127,5, поэтому 128 не выйдет никогда. 127 — дробная часть в целочисленной map отбрасывается
- Светодиод на пине 8 реальной Uno. Что делает analogWrite(8, 127)?
- Аппаратный ШИМ на Uno только на пинах 3, 5, 6, 9, 10, 11. На пине 8 analogWrite молча включает выход в полную — без ошибок и без плавности. Даёт половинную яркость — 8 тоже ШИМ-пин
- raw вышел за диапазон: analogRead вернул 1100 после обрыва. Что вернёт map(1100, 0, 1023, 0, 255)?
- map — чистая пропорция без границ: 1100 × 255 / 1023 ≈ 274. На реальной плате 274 в analogWrite свернётся в байт — 18, и яркость прыгнет; страховка — constrain(bright, 0, 255). 274 — map не ограничивает, подрезать должен constrain
- Хотите, чтобы на минимальной ручке лампа горела на 30, а не гасла. Какой вызов верный?
- Выходной диапазон 30–255: нижней границе входа соответствует 30. map(raw, 0, 1023, 0, 30) наоборот — загонит всю шкалу в 0–30. map(raw, 0, 1023, 30, 255)