У моей ночной гирлянды было пять светодиодов и пять переменных: led1 на пине 2, led2 на пине 3 — и так до led5 на пине 6. Каждая жила своей жизнью: pinMode пятью строками, включение пятью digitalWrite, выключение ещё пятью. Работало. А потом на подоконнике освободилось место, и захотелось шестой, седьмой, восьмой — и я поймал себя на том, что не пишу скетч, а штампую копии одной и той же строки с отрастающей цифрой на конце.
Копипаста отомстила по классике: шестой светодиод молчал, потому что в одной из двадцати почти одинаковых строк была опечатка в один символ — и глаз её не различал, пока я не начал сверять строки по линейке. А когда разом захотелось сменить темп всей гирлянды, цифр оказалось столько, что правку я бросил на середине. Знакомо? Тогда у меня та же сделка, что была в уроке про мелодии: там массив нот заменил двадцать строк с tone(), здесь массив пинов заменит двадцать строк с digitalWrite().
План такой: сначала устройство массива — объявление, индексы с нуля и первая грабля с границей. Потом два практических паттерна. Первый — мигание по партитуре: массив хранит не пины, а сценарий, сколько миллисекунд светить и сколько темнить. Второй — журнал показаний: массив из восьми последних значений analogRead, из которого считаем среднее и делаем дрожащие цифры потенциометра спокойными. Понадобятся светодиоды с резисторами 220 Ом и потенциометр на A0 — вся начинка из урока про аналоговые входы.
Массив: одна переменная на всю пачку
Массив — полка с пронумерованными ячейками под одним именем. Строка int pins[] = {2, 3, 4, 5, 6}; заводит пять ячеек и раскладывает по ним номера пинов. Квадратные скобки после имени — запрос конкретной ячейки: pins[0] — это 2, pins[1] — это 3, pins[4] — это 6. Нумерация с нуля — вы её уже гоняли счётчиком в уроке про цикл for, здесь тот же счёт работает адресом ячейки. Отсюда следствие, которое стоит проговорить вслух: пятый светодиод — это pins[4]. А pins[5] не существует.
Что будет, если всё-таки написать pins[5]? Компилятор промолчит: в C++ выход за границу массива — не ошибка синтаксиса, а вопрос удачи. Программа полезет в чужую память за полкой — там может лежать ноль, мусор или данные соседней переменной. На настоящей плате это выглядит как случайные числа в Serial или вспышки светодиодов на пинах, которые вы не настраивали. Лечение одно: размер живёт в константе, и счётчик никогда не заходит дальше PIN_COUNT - 1.
Цену ячеек тоже полезно знать в цифрах. int на Uno весит два байта, и массив из ста int съест 200 байт из 2048 доступных — десятую часть всей RAM. Звучало как «всего сто чисел», а на деле половина памяти уходит после десятка таких массивов. Поэтому размер подбирают под задачу, а не про запас. И ещё запись, которую вы встретите в чужих скетчах: sizeof(pins). На настоящей плате sizeof возвращает не число элементов, а число байтов — для наших пяти int это 10, и элементный размер добывают делением sizeof(pins) / sizeof(int). В симуляторе sizeof упрощён и считает иначе, поэтому во всех скетчах урока размер держит константа PIN_COUNT — надёжно и на плате, и в браузере.
const int PIN_COUNT = 5; int pins[] = {2, 3, 4, 5, 6}; void setup() { for (int i = 0; i < PIN_COUNT; i = i + 1) { pinMode(pins[i], OUTPUT); }} void loop() { for (int i = 0; i < PIN_COUNT; i = i + 1) { digitalWrite(pins[i], HIGH); // зажечь текущий delay(120); digitalWrite(pins[i], LOW); // погасить — и к следующему } delay(600);}| Индекс | Ячейка | Что это на макетке |
|---|---|---|
| pins[0] | 2 | первый светодиод |
| pins[1] | 3 | второй |
| pins[2] | 4 | третий |
| pins[3] | 5 | четвёртый |
| pins[4] | 6 | пятый и последний — дальше пусто |
| pins[5] | — | за границей: чужая память, читать и писать нельзя |
Паттерн мигания: массив как партитура
Бегущий огонёк по массиву пинов вы уже собирали: в уроке про светодиодную шкалу массив leds[] держал восемь сегментов, и огонь бегал по ним туда и обратно. Так что пины оставим в покое и вытащим из массива вторую силу: он умеет хранить не железо, а сценарий. Мелодия из прошлого урока была массивом частот — партитурой для уха. Массив длительностей — та же партитура, но для глаз.
Механика: массив steps[] хранит по очереди длительности состояний — свет, темнота, свет, темнота. Чётным шагом светодиод включаем, нечётным выключаем, delay(steps[i]) держит состояние положенное время. Чётность проверяет if (i % 2 == 0) — обычное условие из урока про if и else: оператор % даёт остаток от деления, и у чётных номеров остаток нулевой.
Чем это отличается от мигалки из самого первого урока? Там ритм был зашит в код: два delay, и любой характер мигания правится только редактированием строк программы. Здесь ритм — данные. Нужна вспышка-двойкой — правите фигурные скобки. Нужен штурманский маяк: три коротких вспышки и длинная тишина — снова только массив. Код не меняется вовсе, и это не лень, а качество: в столбике однородных чисел опечатка видна сразу, а в размноженных строках кода она пряталась, как помним.
const int LED = 13;const int STEP_COUNT = 6; // партитура: длительности состояний в мс// чётный шаг — свет, нечётный — темнотаint steps[] = {100, 100, 100, 100, 100, 2000}; void setup() { pinMode(LED, OUTPUT); Serial.begin(9600); Serial.println("Mayak: 3 vspyshki, potom tishina");} void loop() { for (int i = 0; i < STEP_COUNT; i = i + 1) { if (i % 2 == 0) { digitalWrite(LED, HIGH); } else { digitalWrite(LED, LOW); } delay(steps[i]); }}Журнал показаний: среднее из восьми измерений
Теперь противоположная работа: не заставить светодиод мигать по сценарию, а заставить показания вести себя спокойно. Подключите потенциометр к A0 и печатайте analogRead в Serial — при неподвижной ручке числа идут 512, 506, 518, 509. Это шум: помехи питания, дрожание контактов, собственная прыть АЦП. По одному замеру дрожь видно всегда: прибор, который мигает шкалой на каждый чих, выглядит сломанным, даже когда исправен.
Классическое лечение — среднее по окну. Заводим журнал из восьми последних измерений: каждый новый замер сдвигает журнал влево, а сам встаёт в последнюю ячейку. Среднее — сумма всех восьми, делённая на восемь. Теперь рывок ручки с 700 на 120 доезжает порциями: первый замер после рывка даёт (7 × 700 + 120) / 8 = 627, дальше 555, 484 — и через восемь замеров среднее доезжает до честных 120. Кривая становится ленивой, и это ровно то, что нужно: глаз читает плавную шкалу, а не стробоскоп.
Честности ради: сдвиг — способ наглядный, но дороговатый. Каждый замер копирует семь значений — при восьми ячейках это ерунда, а при сотне уже ощутимо для скромного процессора Uno. Взрослый приём — кольцевой буфер: пишущая головка ходит по массиву по кругу, свежее значение затирает самое старое, и копировать нечего. Это тема отдельного урока из дорожной карты — там же минимум, максимум и медиана по журналу. Для восьми значений сдвиг честен: три строки, которые видно целиком, — с них и надо начинать.
- Расширьте гирлянду: перенесите в первый скетч восемь пинов {2, 3, 4, 5, 6, 7, 8, 9}, поставьте PIN_COUNT = 8 и убедитесь, что правка заняла две строки. Потом пустите огонёк обратно — после витка вперёд виток назад; подсказка: второй цикл идёт от PIN_COUNT - 2 вниз до нуля.
- Сочините свою партитуру в steps[]: двойная вспышка, SOS из первого урока, радар с длинным хвостом. Код трогать нельзя — только фигурные скобки. Если рисунок «дребезжит» на глаз, ищите слишком короткие тёмные шаги: глазу нужно 60–80 мс, чтобы успеть заметить темноту.
- Поменяйте глубину журнала: JOURNAL_SIZE = 8, потом 16 и 32 (массив и константа правятся вместе!). Дёрните ползунок и сравните: чем глубже журнал, тем плавнее кривая, но тем дольше среднее доезжает до нового значения. Найдите компромисс, при котором шкала ещё живо отвечает ручке.
- Добавьте к журналу вилку шума: два цикла по журналу, которые находят минимум и максимум, и печать строкой min= и max= рядом с avg. Плоттер покажет три ряда сразу — и вилку, внутри которой дышит сырое значение, и спокойную линию среднего по центру.
- Массив — пронумерованная полка значений под одним именем: int pins[] = {2, 3, 4, 5, 6}; доступ по индексу с нуля, пятый светодиод живёт в pins[4].
- Обход — for со счётчиком-индексом: i — номер ячейки, не пин. Размер держит константа; на настоящей плате sizeof возвращает байты (int — 2 байта), в симуляторе он упрощён.
- Выход за границу компилятором не ловится: pins[PIN_COUNT] — чужая память и случайные числа. Последняя ячейка — PIN_COUNT - 1, границы строгие: i < PIN_COUNT.
- Паттерн мигания — массив шагов-длительностей: чётный шаг светим, нечётный гасим, delay(steps[i]) держит состояние. Рисунок правится в фигурных скобках, код не трогается.
- Журнал показаний: новый замер сдвигает массив влево и встаёт в последнюю ячейку, avg = сумма / 8 сглаживает шум — рывок 700 на 120 доезжает через 627 и 555. Для больших окон есть кольцевой буфер.
- 1. Какой элемент массива int pins[] = {2, 3, 4, 5, 6}; хранит номер пина пятого светодиода?
- 2. Что произойдёт при обращении к journal[8] в массиве int journal[] = {0, 0, 0, 0, 0, 0, 0, 0};?
- 3. Ручку потенциометра рванули с 700 на 120. Что напечатает журнал из восьми значений в первом замере после рывка?
- 4. Почему журнал показаний не стоит хранить в массиве String на Uno?
Частые вопросы по теме урока
- Какой элемент массива int pins[] = {2, 3, 4, 5, 6}; хранит номер пина пятого светодиода?
- Индексация с нуля: пятый элемент имеет индекс 4. pins[5] — уже за границей массива. pins[4] — индекс на единицу меньше номера
- Что произойдёт при обращении к journal[8] в массиве int journal[] = {0, 0, 0, 0, 0, 0, 0, 0};?
- C++ не проверяет границы: восемь ячеек занумерованы 0–7, а за последней лежит память соседних переменных. Компилятор молчит, поведение — лотерея. Программа прочитает чужую память за границей — там окажется что попало
- Ручку потенциометра рванули с 700 на 120. Что напечатает журнал из восьми значений в первом замере после рывка?
- (7 × 700 + 120) / 8 = 627 после целочисленного деления. До честных 120 среднее доезжает за восемь замеров. avg=700 — среднее застряло на старом
- Почему журнал показаний не стоит хранить в массиве String на Uno?
- Каждая операция со String перераспределяет память, куча на Uno крошечная — наступает фрагментация. В массивах живут числа: int и byte. Строки крошат память кучи: перераспределения едят 2048 байт RAM, и через час плата умирает от фрагментации