База знаний · Язык скетчей
Массивы, циклы, условия и 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 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 expressions | int 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
