Индикатор уровня для самодельной поливалки я собирал из восьми светодиодов в ряд и писал код по-новичковски честно: pinMode восемь раз, потом восемь почти одинаковых кусков digitalWrite с delay. Получилось под шестьдесят строк, а седьмой светодиод так и не зажёгся. Вечер я смотрел в эти простыни, пока не заметил опечатку: в одном из восьми мест написал 6 вместо 7. Восемь одинаковых кусков кода — каждый глаз видит, каждый глаз и пропускает.
В этом уроке соберём светодиодную шкалу — восемь светодиодов в линейку — и напишем её код так, как его пишут взрослые: массив int leds[8] = {2, 3, 4, 5, 6, 7, 8, 9} плюс один цикл for. Настроим восемь пинов тремя строками, погоняем бегущий огонь туда и обратно с логом позиций в Serial, сделаем из него заполняющую шкалу уровня и разберём два классических тупика: «горят все подряд» и «мусор на несуществующих пинах» — оба растут из одного корня, а лечатся одинаково аккуратным отношением к индексам.
Что такое LED-шкала: восемь обычных светодиодов в линейке
Светодиодная шкала (LED bar graph) — модуль, в котором десять отдельных светодиодов уже выровнены в один ряд с шагом под макетку. Никакой магии и адресных чипов внутри нет: у каждого сегмента свои анод и катод, как у любого одиночного светодиода из вашего набора. В наборах бывают модули-держатели, где катоды уже сведены в общую шину, а резисторы припаяны на плате; голая шкала без обвязки требует резистор на каждый сегмент. Я в уроках использую восемь сегментов из десяти — ровно столько, сколько удобно разложить на пинах 2–9.
Подключение зеркалит первый урок: каждый сегмент — через резистор 220–330 Ом к своему пину, общая земля — к GND. Если в вашем модуле резисторы уже на плате, просто соединяйте пины сегментов с 2–9 и общий минус с GND. Токи посчитаем сразу: сегмент при 220 Ом ест около 13–15 мА, все восемь горят одновременно — 110–120 мА; USB-порт отдаёт 500 мА, так что запас есть, но вешать на ту же макетку серво или реле уже стоит с отдельным питанием. Проверка перед сборкой у меня ритуальная: один сегмент на пине 2 и Blink из первого урока — если он мигает, вся линейка соберётся без сюрпризов.
Где шкалы живут по-настоящему: индикаторы уровня звука и заряда, шкала влажности в поливалке, прогресс копирования на старых кассетных магнитофонах — да тот же Knight Rider из восьмидесятых. Важно для понимания: восемь сегментов — это восемь обычных выходов, им не нужны ни FastLED, ни спецбиблиотеки, только digitalWrite, который вы уже знаете. Вся задача сводится к управлению набором пинов по очереди — идеальная площадка, чтобы впервые всерьёз применить массив и цикл.
А что, когда сегментов станет больше, чем пинов? На Uno шестнадцать пригодных для вывода линий, и большая шкала плюс дисплей съедают их мгновенно. Классический выход — сдвиговый регистр вроде 74HC595: три линии от платы, а на выходе восемь новых пинов, управляемых побитно. Это отдельная история с собственными ловушками, и сейчас она вам не нужна: до восьми сегментов всё решается прямым подключением. Но запомните само направление мысли — «не хватает пинов, значит данные надо передавать, а не разводить проводами»: к нему вы вернётесь, когда дойдёте до больших гирлянд и матриц.
Массив пинов: int leds[8] и цикл вместо копипасты
Массив — пронумерованный набор ячеек под одним именем. Строка int leds[8] = {2, 3, 4, 5, 6, 7, 8, 9}; кладёт в восемь ячеек номера пинов сегментов. Доступ — по индексу в квадратных скобках, и нумерация начинается с нуля: leds[0] — это пин 2, leds[7] — пин 9. Отсюда первая ловушка новичка: leds[8] не существует — последний элемент leds[7]; попытка записать в leds[8] — выход за границу массива, за которым лежит соседняя память с непредсказуемым содержимым.
Теперь код. Настроить все восемь пинов: for (int i = 0; i < LED_COUNT; i = i + 1) { pinMode(leds[i], OUTPUT); } — три строки вместо восьми. Мигнуть всеми — тот же цикл с digitalWrite(leds[i], HIGH). Поменять распиновку — правите один массив, а не восемь кусков кода. Это главный выигрыш: данные (какие пины) отделены от логики (что с ними делать). Мой рабочий тест на зрелость скетча: если добавление девятого светодиода требует правки больше чем в одном месте — код написан копипастой и уже ждёт свою опечатку.
Рядом с массивом живёт const int LED_COUNT = 8: все циклы пишутся как i < LED_COUNT, и при смене числа сегментов правится одна константа — вычитали пятый и шестой сегменты, поставили 6, и ни один цикл не трогаем. Заметьте, как это сочетается с уроком про циклы: там мы гоняли счётчик сам по себе, здесь он ходит по массиву — i не номер пина, а номер ячейки, из которой пин достаётся. Перепутать эти две вещи — вторая классическая ловушка: digitalWrite(i, HIGH) вместо digitalWrite(leds[i], HIGH) зажигает пины 0, 1, 2... по счётчику, а пины из массива молчат.
Бегущий огонь: зажигаем leds[i], гасим leds[i-1]
Сам эффект стоит трёх действий: зажечь текущий сегмент, погасить предыдущий, подождать. digitalWrite(leds[i], HIGH) зажигает огонёк, digitalWrite(leds[i - 1], LOW) тушит тот, с которого мы только что ушли, delay(SPEED_MS) держит темп — при 120 мс огонь пробегает линейку за секунду. Ключевая деталь — именно «погасить предыдущий»: забудете, и все пройденные сегменты останутся гореть, вместо огонька получится заполняющаяся полоса. Она нам, впрочем, тоже пригодится: это же шкала уровня, эффект на один цикл от бегущего огня.
Обратный ход — второй цикл от LED_COUNT - 2 вниз до нуля: на краях огонёк разворачивается, и получается треугольная траектория, как у сканера из сериалов. На границах циклов внимание удваивается: первый шаг прямого хода тушит leds[i - 1] только при i > 0, иначе вы обратитесь к leds[-1] — тому же выходу за границу, что и leds[8], только снизу. Я печатаю позицию в Serial на каждом шаге: «Огонёк на позиции N» — строчка стоит ноль производительности, а отладка эффектов превращается в чтение лога вместо слежения глазами за восемью светодиодами.
Из бегущего огня до индикатора уровня один шаг: вместо «погасить предыдущий» — «зажечь всё до порога». Цикл от 0 до k зажигает первые k сегментов, цикл от k до LED_COUNT гасит хвост — и полоса показывает уровень, который завтра придёт с потенциометра через map, а послезавтра — с датчика влажности. Так один и тот же каркас обслуживает эффекты и настоящие приборы: меняется только источник числа k.
void setup() { pinMode(2, OUTPUT); pinMode(3, OUTPUT); pinMode(4, OUTPUT); pinMode(5, OUTPUT); pinMode(6, OUTPUT); pinMode(6, OUTPUT); // опечатка: здесь должен быть пин 7 pinMode(8, OUTPUT); pinMode(9, OUTPUT);} void loop() { // ...а дальше — восемь почти одинаковых кусков: digitalWrite(2, HIGH); delay(120); digitalWrite(2, LOW); digitalWrite(3, HIGH); delay(120); digitalWrite(3, LOW); // ...и так все восемь: 32 строки вместо одного цикла}- Разгоните и замедлите огонь: SPEED_MS от 300 до 60. Найдите скорость, при которой огонёк перестаёт читаться как точка и сливается в бегущую волну — глаз дорисовывает движение сам.
- Запустите второй огонёк навстречу: в прямом цикле зажигайте заодно leds[LED_COUNT - 1 - i] и гасите его прошлую позицию leds[LED_COUNT - i]. Два огня из одного цикла — бесплатно.
- Превратите огонь в шкалу уровня: уберите гашение предыдущего, а в конце каждого прохода тушите всю линейку отдельным циклом. Получится заполняющаяся полоса — тот же индикатор уровня, только источник пока внутренний.
- Уменьшите масштаб: возьмите пять сегментов {2, 3, 4, 5, 6}, поставьте LED_COUNT = 5 и проверьте, что правка заняла две строки и ни один цикл не тронулся. Это и есть плата за массив.
- Сделайте стоп-кадр: поднимите SPEED_MS до 1000 и сверьте Serial с макеткой — номер позиции в логе всегда совпадает с горящим сегментом. Так я отлаживаю любые эффекты: сначала лог, потом глаза.
- LED-шкала — восемь (или десять) обычных светодиодов в линейке: каждому свой пин и резистор 220–330 Ом.
- Массив int leds[8] = {2, 3, 4, 5, 6, 7, 8, 9} отделяет данные от логики: распиновка меняется в одном месте.
- Индексы с нуля: leds[0] — первый элемент, leds[7] — последний; leds[8] не существует — это выход за границу.
- Цикл for (int i = 0; i < LED_COUNT; i = i + 1) заменяет восемь кусков копипасты и не оставляет места опечатке в номере пина.
- Бегущий огонь — зажечь leds[i], погасить leds[i-1], пауза; шкала уровня — заполнить первые k сегментов и потушить хвост.
- 1. Как правильно обратиться к последнему элементу массива int leds[8]?
- 2. Огонёк бежит, но за ним тянется горящий хвост. Что забыли?
- 3. Сколько строк займёт настройка пинов восьми сегментов с массивом и циклом?
- 4. Седьмой сегмент (пин 7) не горит, остальные работают. В копипаст-версии setup() пин 7 ни разу не настроен. Что с пином?
Массив пинов плюс цикл — связка, после которой проекты «из многих светодиодов» перестают пугать: восемь сегментов, двенадцать, светодиодная матрица — разница только в длине массива. Тот же каркас через урок возьмёт на себя потенциометр: закрутите ручку — и заполнение шкалы станет настоящим индикатором уровня, а не внутренним эффектом. Перечитайте note выше перед следующей сборкой: выход за границу массива — первая ошибка, которую вы совершите в новом эффекте, и теперь вы знаете её симптомы в лицо. А на сегодня у вас три инструмента плавного света: один канал duty cycle, три канала цвета и восемь каналов порядка — из них собирается почти вся световая часть стартовых проектов.
Частые вопросы по теме урока
- Как правильно обратиться к последнему элементу массива int leds[8]?
- Восемь элементов занумерованы 0–7, поэтому последний — leds[7]. leds[8] не существует: это выход за границу массива с непредсказуемыми последствиями. leds[7] — индексация начинается с нуля
- Огонёк бежит, но за ним тянется горящий хвост. Что забыли?
- Каждый зажжённый сегмент остаётся гореть, пока его не погасят. Без гашения leds[i - 1] бегущий огонь превращается в заполняющуюся полосу. Гасить предыдущий сегмент: digitalWrite(leds[i - 1], LOW)
- Сколько строк займёт настройка пинов восьми сегментов с массивом и циклом?
- Цикл for пробегает все индексы массива и настраивает каждый пин: три строки кода против восьми копипастных, и опечататься в пине невозможно. Три: заголовок for, тело из pinMode(leds[i], OUTPUT) и закрывающая скобка
- Седьмой сегмент (пин 7) не горит, остальные работают. В копипаст-версии setup() пин 7 ни разу не настроен. Что с пином?
- Без pinMode(7, OUTPUT) пин остаётся входом. digitalWrite(7, HIGH) включает лишь внутреннюю подтяжку на десятки микроампер — светодиоду этого не хватает, сегмент тёмный. Пин остался входом: внутренняя подтяжка даёт слишком слабый ток для светодиода