База знаний · Язык скетчей

Массивы, циклы, условия и switch в Arduino

Пять светодиодов одной строкой, датчик по кругу, меню из монитора порта: массивы, for, while, if и switch на проверенных скетчах — и ошибки компилятора к ним.

После Blink и кнопки программа упирается в одно и то же: пять светодиодов хочется зажигать не пятью строками, а одной; датчик хочется опрашивать по кругу; на команду из монитора порта — отвечать по-разному. Для этого в языке есть массивы, циклы, условия и switch. Здесь они разобраны на скетчах, которые собраны и проверены, и с теми ошибками компилятора, которые вы встретите, когда начнёте писать своё.

Массив: несколько значений под одним именем

Массив — это ряд ячеек одного типа, которые лежат в памяти подряд и различаются номером. Номер называется индексом и считается с нуля: в массиве из пяти элементов индексы от 0 до 4. Объявляют так:

const byte LEDS[] = {2, 3, 4, 5, 6};   // размер посчитает компилятор: 5
int readings[10];                       // 10 ячеек, пока с мусором внутри
int limits[3] = {100, 200, 300};        // размер и значения
LEDS[0]   // 2 — первый
LEDS[4]   // 6 — последний
LEDS[5]   // за краем: сборка пройдёт, программа — нет
int values[4] = {1, 2, 3, 4} — так оно лежит в памяти1values[0]2values[1]3values[2]4values[3]другая переменнаяvalues[4] — чужоеИндексы 0…3. Индекса 4 нет, но компилятор не остановит
Массив из четырёх int занимает восемь байт подряд. Индекс считается с нуля, последний — 3. Запись в values[4] попадёт в ту переменную, которой случилось лежать рядом: сборка пройдёт, а программа начнёт вести себя странно. Предупреждение компилятор выдаёт только при включённых предупреждениях и только для очевидных случаев.

Два правила, которые экономят вечера. Первое: глобальный массив без инициализации заполнен нулями, а локальный внутри функции — тем, что осталось в памяти от предыдущих дел; локальный массив, который должен начинаться с нуля, обнуляйте сами: int buf[8] = {0};. Второе: количество элементов не хранится нигде, его считают выражением sizeof(LEDS) / sizeof(LEDS[0]) — размер всего массива в байтах делят на размер одного элемента. Для массива int из пяти чисел это 10 / 2 = 5. Запомнить число «5» в отдельной константе удобнее, чем переписывать его в каждом цикле.

for: цикл со счётчиком

for (byte i = 0; i < 5; i++) {   // старт; условие продолжать; шаг
  digitalWrite(LEDS[i], HIGH);
}

Три части в скобках: что сделать перед началом, при каком условии крутиться дальше, что делать после каждого прохода. Переменная i живёт только внутри цикла — попытка напечатать её после закрывающей скобки даёт error: 'i' was not declared in this scope. Типичная ошибка на единицу: i <= 5 вместо i < 5 — и цикл лезет в несуществующий шестой элемент.

Ловушка с обратным ходом: for (byte i = 4; i >= 0; i--) никогда не закончится. У byte нет отрицательных значений: после нуля он становится 255, и условие «больше или равно нулю» верно всегда. Для обратного счёта берите int. А сравнение int i с sizeof(...) компилятор помечает warning: comparison between signed and unsigned integer expressions — безобидно, но лучше объявить счётчик как byte или size_t, и предупреждение исчезнет.

Скетч: шкала из пяти светодиодов, которая растёт с напряжением на A0. Массив выводов, два цикла, 1 194 байта на Uno.

// Шкала из пяти светодиодов: чем выше напряжение на A0, тем больше горит.
const byte LEDS[] = {2, 3, 4, 5, 6};               // выводы в одном массиве
const byte COUNT = sizeof(LEDS) / sizeof(LEDS[0]);  // 5 — считает компилятор

void setup() {
  for (byte i = 0; i < COUNT; i++) {
    pinMode(LEDS[i], OUTPUT);
  }
}

void loop() {
  int level = map(analogRead(A0), 0, 1024, 0, COUNT + 1);  // 0…5 горящих
  for (byte i = 0; i < COUNT; i++) {
    digitalWrite(LEDS[i], i < level ? HIGH : LOW);
  }
  delay(50);
}

while и do…while: цикл, пока верно условие

while (digitalRead(BUTTON) == LOW) {
  // крутимся, пока кнопка нажата
}

do {
  value = analogRead(A0);
} while (value < 100);   // тело выполнится хотя бы раз

while проверяет условие перед каждым проходом, do…while — после, поэтому его тело выполняется минимум один раз. Пустой while с ожиданием — нормальный приём, но помните: пока он крутится, остальная программа стоит, а loop() не вызывается. Если ждать нужно, не замирая, — это уже millis(). Скетч считает нажатия кнопки и ждёт в while, пока её отпустят, — иначе одно нажатие сосчитается сотню раз.

// Считаем нажатия кнопки: while ждёт, пока её отпустят.
const byte BUTTON = 2;  // кнопка между выводом и землёй
unsigned int presses = 0;

void setup() {
  pinMode(BUTTON, INPUT_PULLUP);  // не нажата — HIGH, нажата — LOW
  Serial.begin(9600);
}

void loop() {
  if (digitalRead(BUTTON) == LOW) {
    delay(20);                           // дребезг контактов
    if (digitalRead(BUTTON) == LOW) {    // всё ещё нажата — считаем
      presses++;
      Serial.print("нажатий: ");
      Serial.println(presses);
      while (digitalRead(BUTTON) == LOW) {
        // ждём, пока отпустят: иначе одно нажатие сосчитается сто раз
      }
      delay(20);
    }
  }
}

break и continue

break выходит из цикла немедленно, continue пропускает остаток тела и переходит к следующему проходу. Оба действуют только на ближайший цикл: из вложенного for внутри for внешний не прервётся. И break в switch — это отдельная история, о ней ниже.

if, else if, else

if (temp > 30 && humidity > 70) {
  Serial.println("душно");
} else if (temp > 30) {
  Serial.println("жарко");
} else {
  Serial.println("нормально");
}

Условие — любое выражение, которое можно счесть истинным или ложным; && — «и», || — «или», ! — «не». Самая дорогая опечатка в языке: одно «равно» вместо двух. if (x = 5) не сравнивает, а присваивает, и всегда истинно. Компилятор это видит — warning: suggest parentheses around assignment used as truth value, — но Arduino IDE по умолчанию предупреждения прячет: в «Файл → Параметры...» поставьте уровень предупреждений компилятора «Все», и половина загадочных багов найдётся сама.

Ещё два правила. float не сравнивают на равенство: прибавьте 0.1 десять раз — получится не ровно 1.0, а на волосок больше, и == 1.0 даст ложь. Сравнивайте с допуском: abs(a - b) < 0.01. А digitalRead() возвращает HIGH или LOW, то есть 1 и 0, и if (digitalRead(2)) — законная короткая запись.

switch: когда вариантов много

Цепочку из десяти else if по одной переменной читать тяжело. switch делает то же самое, но наглядно: в скобках — что сравниваем, после каждого case — с чем. Скетч: меню в мониторе порта, одна буква — одно действие.

// Меню в мониторе порта: одна буква — одно действие.
const byte LED = LED_BUILTIN;

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  Serial.println("1 - включить, 0 - выключить, b - мигнуть");
}

void loop() {
  if (Serial.available() == 0) {
    return;  // команд нет — ждём дальше
  }
  char command = Serial.read();
  switch (command) {
    case '1':
      digitalWrite(LED, HIGH);
      Serial.println("включено");
      break;
    case '0':
      digitalWrite(LED, LOW);
      Serial.println("выключено");
      break;
    case 'b':
    case 'B':                     // две метки подряд — один обработчик
      for (byte i = 0; i < 3; i++) {
        digitalWrite(LED, HIGH);
        delay(100);
        digitalWrite(LED, LOW);
        delay(100);
      }
      break;
    case '\n':
    case '\r':
      break;                      // переводы строки от монитора молча пропускаем
    default:
      Serial.print("не знаю команду: ");
      Serial.println(command);
  }
}

Три вещи, о которые спотыкаются все. break в конце каждого case обязателен: без него выполнение «проваливается» в следующую метку. Иногда это нарочно — как 'b' и 'B' подряд в скетче, два значения на один обработчик. default ловит всё, что не совпало, и его лучше не пропускать: по нему видно, что прилетело что-то неожиданное. Переводы строки \n и \r, которые монитор порта шлёт после каждой команды, в скетче перехвачены своими метками — иначе они попадали бы в default как «не знаю команду». И главное: switch сравнивает только целые числа и символы. Строку в него не положить — switch (cmd) со String даёт error: switch quantity not an integer; для строк остаются if и сравнения, разобранные в статье про String.

Скетч: скользящее среднее по массиву

Показания датчика прыгают на несколько единиц. Классическое лекарство — кольцевой буфер: массив из восьми последних отсчётов, в который новое значение записывается по кругу, и сумма, из которой вычитают затёртое. Делить сумму на восемь — и шум уходит. В мониторе порта два столбца: сырое и сглаженное, их удобно смотреть на графике; на Uno — 2 152 байта.

// Скользящее среднее: датчик шумит, а среднее из восьми отсчётов — нет.
const byte SIZE = 8;
int samples[SIZE];     // кольцевой буфер
byte head = 0;         // куда писать следующий отсчёт
long sum = 0;          // сумма всех значений в буфере

void setup() {
  Serial.begin(9600);     // глобальный массив samples уже заполнен нулями
}

void loop() {
  int raw = analogRead(A0);
  sum -= samples[head];   // вычитаем то, что затираем
  samples[head] = raw;
  sum += raw;
  head = (head + 1) % SIZE;  // по кругу: после 7 снова 0

  Serial.print(raw);
  Serial.print('\t');
  Serial.println(sum / SIZE);
  delay(100);
}

Обратите внимание на head = (head + 1) % SIZE: остаток от деления возвращает индекс в начало после последней ячейки. Это и есть «кольцо». И на long sum: в int помещается до 32 767, восемь отсчётов по 1023 — это 8 184, с запасом; но стоит увеличить буфер до 33 отсчётов, и сумма перестанет влезать. Привычка считать суммы в long дешевле, чем искать переполнение; о нём — в статье про типы.

Ошибки компилятора, которые вы встретите

СообщениеЧто случилось
'i' was not declared in this scopeСчётчик цикла использован после закрывающей скобки цикла. Объявите переменную до цикла, если она нужна после.
too many initializers for 'int [3]'В скобках размер 3, а в фигурных скобках четыре значения. Уберите размер — компилятор посчитает сам.
switch quantity not an integerВ switch попала строка или float. Только целые и символы.
iteration 4 invokes undefined behaviorПредупреждение: цикл заведомо выходит за край массива. Сборка пройдёт, поведение непредсказуемо. Проверьте <=.
suggest parentheses around assignment used as truth valueВ if одно «равно» вместо двух.
comparison between signed and unsigned integer expressionsint i сравнивают с sizeof. Сделайте счётчик byte.

Выход за границы массива — единственная из этих бед, о которой компилятор молчит в большинстве случаев. Если плата перезагружается, переменная меняется «сама» или светодиод горит не тот — первым делом пересчитайте индексы. Всё, что связано с No such file, was not declared и expected ';', собрано в навигаторе ошибок.

Частые вопросы

Как узнать размер массива в Arduino?

Размер нигде не хранится, его считают: sizeof(arr) / sizeof(arr[0]) — размер всего массива в байтах делят на размер одного элемента. Для массива int из пяти чисел это 10 / 2 = 5. Внутри функции, куда массив передан параметром, этот приём не работает: туда приходит только адрес, размер передавайте отдельным числом.

Почему цикл for с byte не заканчивается?

В обратном цикле for (byte i = 4; i >= 0; i--) переменная byte не бывает отрицательной: после нуля она становится 255, и условие «больше или равно нулю» верно всегда. Для обратного счёта объявляйте счётчик как int.

Что будет, если обратиться к элементу за границей массива?

Сборка пройдёт, а программа прочитает или перепишет чужую память — соседнюю переменную. Отсюда «само меняющиеся» значения и перезагрузки. Компилятор предупреждает только в очевидных случаях и только при включённых предупреждениях. Проверяйте условие цикла: для массива из N элементов — i < N, не i <= N.

Зачем break в switch и что будет без него?

Без break выполнение после сработавшей метки «проваливается» в следующий case и выполняет его тоже. Иногда это нарочно: две метки подряд без кода между ними дают один обработчик на два значения. Во всех остальных случаях break ставят в конце каждой ветки.

Можно ли использовать String в switch?

Нет: switch сравнивает только целые числа и символы, со String сборка остановится на ошибке «switch quantity not an integer». Для строк — цепочка if и else if с ==, либо switch по первому символу через cmd.charAt(0).

Чем while отличается от do…while?

while проверяет условие до каждого прохода и может не выполниться ни разу; do…while проверяет после, поэтому тело выполнится хотя бы раз. Оба останавливают всё остальное, пока крутятся: loop() в это время не вызывается.

Обновлено: 22 октября 2026