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

void setup() и void loop(): структура программы Arduino

Почему setup() выполняется один раз, а loop() вечно, что значит слово void и откуда берётся ошибка redefinition.

В любом скетче Arduino есть две функции, и обе обязательны: setup() выполняется один раз при включении, а loop() — по кругу, пока есть питание. Это не соглашение и не привычка: так устроено само ядро Arduino, и обойти это нельзя.

Кто на самом деле запускает вашу программу

Настоящая точка входа лежит не в скетче, а в ядре — в файле cores/arduino/main.cpp. Там и живёт функция main(), привычная всем, кто писал на C++. Она вызывает init() (ядро настраивает таймеры, ШИМ и АЦП), потом ровно один раз ваш setup(), а дальше входит в вечный цикл for (;;), где вызывает loop() и служебный serialEventRun().

ПИТАНИЕ ИЛИ RESETвсё начинается только отсюдаinit() — код ядратаймеры, ШИМ, АЦП — часть тех самых 444 байтsetup() — ваш кодвыполняется РОВНО ОДИН РАЗloop() — ваш кодserialEventRun() — служебный вызов ядраи так по кругу — пока не выключат питание
Порядок запуска задаёт не ваш скетч, а функция main() внутри ядра (файл cores/arduino/main.cpp). Она вызывает setup() один раз, а потом вечно крутит loop(). Поэтому setup() нельзя «запустить заново» из программы — только сбросом платы.

Отсюда сразу три практических вывода. Первый: пустой скетч не бывает нулевого размера — мы собрали его и получили 444 байта флеша и 9 байт ОЗУ. Это цена самого ядра: таблица векторов прерываний, стартовый код, main() с init() внутри и обработчик прерывания счётчика миллисекунд — на него одного уходит 148 байт из 444, а девять байт ОЗУ целиком заняты тремя переменными этого счётчика. Второй: «перезапустить» setup() из программы нельзя, настоящий повторный запуск бывает только при сбросе или подаче питания. Третий: пустой loop() не даёт процессору отдохнуть — он крутит вечный цикл на полной тактовой частоте, в спящий режим плата сама не уходит и энергию не экономит.

Что означает слово void

void — не тип переменной, а пометка «функция ничего не возвращает тому, кто её вызвал». Ядро объявляет обе функции именно так, поэтому подпись менять нельзя. Попробуйте написать int setup() — сборка остановится:

error: ambiguating new declaration of 'int setup()'
 int setup() {
     ^~~~~
Arduino.h:154:6: note: old declaration 'void setup()'
 void setup(void);

Строка со словом note: показывает, где ядро уже объявило вашу функцию. Это же объясняет, почему опечатка в подписи (например void loop(int n)) даёт совсем другую ошибку: для компилятора это просто другая функция, а нужной ядру loop() в скетче не оказывается.

Что будет, если удалить setup() или loop()

Скетч не соберётся, но ошибка придёт не от компилятора, а от компоновщика — и покажет чужой файл, что пугает новичков:

main.cpp:43: undefined reference to `setup'     ← нет функции setup()
main.cpp:46: undefined reference to `loop'      ← нет функции loop()
collect2.exe: error: ld returned 1 exit status
Ошибка во время сборки: exit status 1

Строки 43 и 46 — это те самые места в main.cpp ядра, где вызываются ваши функции. Лечение простое: обе функции должны быть в скетче, пусть даже с пустым телом.

Ошибка redefinition of 'void setup()'

Самая частая причина — два скетча в одной папке. Все файлы .ino из папки склеиваются в один, и получается две функции с одинаковым именем:

vtoroy.ino:1:6: error: redefinition of 'void setup()'
 void setup() {
      ^~~~~
sketch.ino:1:6: note: 'void setup()' previously defined here

Читайте обе строки: строка с error: называет файл, который приклеился вторым (обычно именно его и надо унести в отдельную папку), а строка с note: — где функция была объявлена изначально. Подробнее про то, как среда склеивает файлы скетча, — на странице «Скетчи Arduino».

Свои функции: зачем и как

Когда loop() разрастается, куски кода выносят в отдельные функции. Их две разновидности: void — «сделай и ничего не возвращай», и функция с типом — «сделай и верни результат» через return. Этот скетч собран, на Uno он занимает 2194 байта:

const byte LED = 13;

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);

  blink(3, 200);                    // мигнуть три раза по 200 мс
  Serial.println(summa(2, 3));      // напечатать 5
}

void loop() {
}

// Функция без ответа: слово void означает «ничего не возвращаю».
void blink(int times, int pause) {
  for (int i = 0; i < times; i++) {
    digitalWrite(LED, HIGH);
    delay(pause);
    digitalWrite(LED, LOW);
    delay(pause);
  }
}

// Функция с ответом: тип int означает «верну целое число».
int summa(int a, int b) {
  return a + b;
}

Обратите внимание: blink() вызывается в setup() выше, чем описана сама функция. В обычном файле C++ это ошибка, а в .ino работает: перед сборкой среда сама дописывает объявления всех функций в начало файла. Как только тот же код переносят в собственный файл .cpp, он перестаёт собираться с сообщением was not declared in this scope — там прототип нужно писать руками.

return внутри loop()

return; в loop() не останавливает программу. Он завершает текущий проход — и ядро тут же вызывает loop() снова. Приём удобен как «сторож»: проверили условие и вышли, не углубляясь в остальной код. А вот привычное по учебникам return 0; внутри void-функции — ошибка языка, но Arduino собирает такой скетч молча: в настройках плат стоят сразу два послабления. Флаг -fpermissive понижает эту ошибку до предупреждения, а -w прячет и само предупреждение.

Почему счётчик в loop() всегда равен единице

Классическая ловушка: переменная, объявленная внутри loop(), создаётся заново на каждом проходе. Компилятор понимает это настолько буквально, что выбрасывает из прошивки саму арифметику. Лечится словом static — переменная создаётся один раз и живёт между проходами:

void setup() {
  Serial.begin(9600);
}

void loop() {
  // static: переменная создаётся один раз и живёт между проходами loop().
  // Без этого слова счётчик каждый проход начинался бы заново с нуля.
  static int count = 0;

  count++;
  Serial.println(count);
  delay(1000);
}

Такой скетч занимает 1880 байт флеша и 190 байт ОЗУ. Важная деталь для плат с 2 КБ памяти: static не экономит ОЗУ — переменная занимает те же байты, что и глобальная, и держит их всё время работы. Разница только в том, что имя видно лишь внутри функции.

И обратная ловушка: переменная, объявленная в setup(), в loop() уже не существует. Компилятор сообщит 'pin' was not declared in this scope и иногда «предложит» вместо вашей переменной математический синус sin — не пугайтесь, просто перенесите объявление выше setup().

Проверено на нашей сборке

Все тексты ошибок и размеры получены 21 августа 2026 года: arduino-cli 1.5.2-rc.1, ядро arduino:avr 1.8.8, плата Arduino Uno. Порядок запуска приведён по исходнику ядра — файл cores/arduino/main.cpp, строки 33–51. Поведение во время работы (мигание, счёт в мониторе) мы не измеряли на живой плате, а доказали кодом ядра.

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

Что такое void в Arduino?

void — не тип переменной, а пометка «функция ничего не возвращает». Она стоит перед именем функции: void setup() значит «выполни и ничего не отдавай назад». Если написать int setup(), сборка упадёт с ошибкой ambiguating new declaration: ядро уже объявило setup() как void в файле Arduino.h.

Сколько раз выполняется setup()?

Ровно один раз — при подаче питания или после нажатия RESET. Это видно в исходнике ядра: файл main.cpp вызывает setup() один раз, а затем входит в бесконечный цикл с loop(). Вызвать setup() вручную из loop() можно, скетч соберётся, но это обычный вызов функции, а не перезагрузка платы.

Можно ли обойтись без loop()?

Нет. Если убрать loop(), скетч не соберётся: компоновщик выдаст undefined reference to `loop' со ссылкой на строку 46 файла main.cpp самого ядра. То же самое с setup() — там строка 43. Обе функции обязаны быть в скетче, даже пустые.

Почему счётчик в loop() всегда равен единице?

Потому что переменная, объявленная внутри loop(), создаётся заново на каждом проходе. Чтобы она помнила значение, нужно слово static (static int count = 0;) или объявление до setup(). Прошивка при этом получается одинаковая: static-переменная занимает столько же памяти, сколько глобальная.

Обновлено: 21 августа 2026