Основы · 16 мин чтения

Основы программирования Arduino: переменные, циклы и функции

Программирование Arduino с нуля: setup и loop, int, long, byte, переполнение, const, #define, if-else, switch-case, циклы, массивы и функции на примерах со светодиодами.

Программирование Arduino — это объяснение плате Arduino на языке C/C++, что ей делать: ты описываешь, какой светодиод когда загорается, что происходит при нажатии кнопки и как понимать число, пришедшее от датчика, а плата повторяет эти инструкции тысячи раз в секунду. Хорошая новость: почти любой скетч, от мигающего светодиода до робота-машинки, собран из одного и того же десятка строительных блоков. Это setup() и loop(), переменные, константы, операторы, if-else, switch-case, циклы for и while, массивы и функции.

В этом руководстве ты разберёшь эти блоки по одному, каждый на реальном примере со светодиодом или кнопкой. Все примеры работают на одной простой схеме, скомпилированы и проверены на Arduino Uno. Места, где новички спотыкаются чаще всего, — переполнение переменной, путаница = и ==, забытый break и счётчик, который обнуляется на каждом проходе, — разобраны отдельно. В последних разделах показано, как та же логика работает на ESP32 и как она выглядит в MicroPython на Raspberry Pi Pico.

Плата Arduino Uno и её цифровые пины
Arduino Uno: во всех примерах статьи используется кнопка на пине D2 и шесть светодиодов на пинах D8–D13. Благодаря встроенному светодиоду на D13 первым примерам даже не нужно подключение.

Что такое программирование Arduino и на каком языке оно ведётся?

Код Arduino построен на C++; отдельного «языка Arduino» нет. Ядро Arduino добавляет к C++ готовые функции вроде pinMode(), digitalWrite() и delay(). Файл с кодом называется скетч и имеет расширение .ino. При нажатии «Проверить» (Verify) Arduino IDE добавляет в начало файла заголовок Arduino.h и сама создаёт объявления функций, а компилятор превращает код в машинный код, понятный процессору. Затем кнопка «Загрузка» (Upload) отправляет результат по USB в загрузчик платы. Если IDE ещё не установлена, начни с руководства Установка Arduino IDE.

При написании кода нужно соблюдать несколько базовых правил. Если выучить их сразу, исчезнет половина ошибок компиляции, которые иначе встретились бы в первые дни:

ПравилоПравильноЕсли нарушить
Каждая инструкция заканчивается точкой с запятойdigitalWrite(13, HIGH);expected ';' before '}' token (ожидается «;» перед «}»)
Заглавные и строчные буквы различаютсяpinMode(13, OUTPUT);'pinmode' was not declared in this scope (имя «pinmode» не объявлено)
Блоки кода пишутся внутри фигурных скобокvoid loop() { ... }Каждой { нужна парная }
Однострочный комментарий// эта строка не выполняетсяКомментарии не компилируются, они нужны для пояснений
Многострочный комментарий/* ... */Если его не закрыть, всё ниже станет комментарием
Текст в двойных кавычках, один символ в одинарных"Hello" и 'A'Если перепутать кавычки, получится неверное значение или ошибка

Схема для примеров

Все примеры работают на одной схеме. Подключи шесть светодиодов к пинам D8–D13, каждый через резистор 220 Ω; короткая ножка (катод) каждого светодиода идёт на GND. Номинал резистора для своего светодиода можно проверить в утилите калькулятор резистора для светодиода. Одну ножку кнопки подключи к D2, а противоположную по диагонали — к GND. Поскольку в коде используется INPUT_PULLUP, внешний резистор не нужен: пока кнопка отпущена, пин читает HIGH, а при нажатии — LOW. Подробности — в статьях Как подключить кнопку к Arduino и Что такое светодиод?.

ДетальArduino UnoESP32Raspberry Pi Pico
6 светодиодов (каждый с 220 Ω, катод на GND)D8, D9, D10, D11, D12, D13GPIO 18, 19, 21, 22, 23, 25GP14, GP13, GP12, GP11, GP10, GP9
Кнопка (вторая ножка на GND)D2GPIO 4GP2
Встроенный светодиодD13 (светодиод с меткой L)GPIO 2 (на большинстве DevKit)GP25 («LED» на Pico W)
Скорость порта9600 бод115200 бодокно Shell в Thonny
Не подключай светодиод без резистора: могут пострадать и светодиод, и пин Arduino. Если вставить кнопку в макетную плату и взять две ножки с одной стороны, они могут быть уже соединены внутри, и кнопка будет вести себя так, будто её никогда не нажимают, поэтому надёжнее всего брать ножки по диагонали. Номера пинов своей платы можно посмотреть в утилите распиновки.

setup() и loop(): скелет любого скетча

В каждом скетче Arduino есть две обязательные функции. setup() выполняется только один раз — при включении платы или нажатии кнопки сброса; сюда идут подготовительные действия вроде настройки пинов и Serial.begin(). loop() затем повторяется бесконечно после завершения setup(). За кулисами в ядре Arduino есть скрытая функция main(): сначала она готовит железо, затем один раз вызывает setup() и снова и снова вызывает loop() внутри бесконечного цикла. Область в начале файла, за пределами этих двух функций, — это глобальная область; там объявляются константы и переменные, которые должны быть доступны отовсюду.

C++ (Arduino IDE)
1// 1) Global alan: sabitler ve değişkenler / Global area: constants and variables
2const int LED_PIN = 13;        // dahili LED / built-in LED
3unsigned long passCount = 0;   // loop() kaç kez döndü / how many times loop() has run
4
5// 2) setup(): açılışta bir kez çalışır / runs once at power-up or reset
6void setup() {
7  pinMode(LED_PIN, OUTPUT);
8  Serial.begin(9600);
9  Serial.println("setup() bitti / setup() done");
10}
11
12// 3) loop(): sonsuza kadar tekrar eder / repeats forever
13void loop() {
14  passCount++;                       // her turda 1 artır / add 1 on every pass
15  Serial.print("Tur / Pass: ");
16  Serial.println(passCount);
17
18  digitalWrite(LED_PIN, HIGH);       // LED'i yak / LED on
19  delay(500);                        // 500 ms bekle / wait 500 ms
20  digitalWrite(LED_PIN, LOW);        // LED'i söndür / LED off
21  delay(500);
22}

Загрузи код и открой монитор порта на скорости 9600 бод: один раз появится «setup() done», а за ним — номер прохода, который растёт каждую секунду, и встроенный светодиод мигает в том же ритме. Два вызова delay(500) внутри loop() растягивают каждый проход примерно до одной секунды; loop() без ожиданий и вывода в порт на Uno может выполняться сотни тысяч раз в секунду. Чтобы выжать максимум из монитора порта, загляни в руководство Монитор порта Arduino.

Переменные и типы данных: int, long, float, bool, byte

Переменная — это именованная ячейка в памяти, где хранится значение. При объявлении сначала пишется её тип, затем имя и, по желанию, начальное значение, например int counter = 0;. Тип определяет, сколько байт занимает ячейка и какой диапазон чисел в неё помещается. У Arduino Uno всего 2 КБ ОЗУ, поэтому правильный выбор типа важен. Имена переменных начинаются с буквы или подчёркивания и не могут содержать пробелы, кириллицу или буквы с диакритикой вроде é, ü или ç.

ТипРазмер на UnoДиапазонТипичное применение
bool1 байтtrue или falseГорит ли светодиод, нажата ли кнопка
byte1 байт0 – 255Номер пина, яркость ШИМ
char1 байт-128 – 127Один символ: 'A'
int2 байта-32 768 – 32 767Счётчики, результаты analogRead()
unsigned int2 байта0 – 65 535Только положительные числа
long4 байта-2 147 483 648 – 2 147 483 647Большие целые числа
unsigned long4 байта0 – 4 294 967 295Измерение времени через millis()
float4 байтаОколо ±3,4 × 10^38, точность 6–7 знаковТемпература, напряжение, средние значения
double4 байта (на Uno то же, что float)Как у floatНа ESP32 — 8 байт

Несколько практических правил избавят от многих проблем. analogRead() возвращает число от 0 до 1023, так что int достаточно. А вот millis() возвращает unsigned long; если хранить время в переменной int, значение переполнится через 32,7 секунды, и отсчёт времени сломается. На Uno вычисления с float выполняются программно, поэтому они медленнее целочисленных, а всё, что дальше 6–7 знаков, ненадёжно; по этой же причине два float не сравнивают через ==. Если нужен тип, размер которого одинаков на любой плате, используй типы с размером в названии: uint8_t, int16_t и uint32_t.

Переполнение: что будет, если переменная выйдет за свой предел?

Если попытаться записать число больше, чем вмещает тип, Arduino не сообщит об ошибке: число прокрутится, как одометр автомобиля. На Uno int вмещает максимум 32 767, а прибавление 1 превращает его в -32 768. byte переходит с 255 на 0. Ловушка коварнее — арифметика с константами: выражение вроде 60 * 1000 сначала вычисляется как int и переполняется, даже если результат сохраняется в переменную long. Скетч ниже выводит все эти случаи в монитор порта, а его loop() показывает переполнение byte на светодиоде:

C++ (Arduino IDE)
1const int LED_PIN = 9;        // PWM destekli pin (~9) / PWM-capable pin (~9)
2byte brightness = 0;          // byte: 0-255
3
4void setup() {
5  pinMode(LED_PIN, OUTPUT);
6  Serial.begin(9600);
7
8  int a = 32767;              // Uno'da int'in en büyük değeri / largest int on an Uno
9  a = a + 1;                  // taşma: -32768 olur / overflow: becomes -32768
10  Serial.println(a);
11
12  long wrong = 60 * 1000;     // int ile hesaplanır, taşar: -5536 / computed as int, overflows: -5536
13  long right = 60 * 1000L;    // L eki hesabı long yapar: 60000 / the L suffix makes it long math: 60000
14  Serial.println(wrong);
15  Serial.println(right);
16
17  float half = 5 / 2;         // tam sayı bölmesi: 2.00 / integer division: 2.00
18  float half2 = 5 / 2.0;      // ondalıklı bölme: 2.50 / floating-point division: 2.50
19  Serial.println(half);
20  Serial.println(half2);
21}
22
23void loop() {
24  analogWrite(LED_PIN, brightness);   // parlaklığı uygula / apply the brightness
25  brightness++;                       // 255'ten sonra 0'a döner / wraps to 0 after 255
26  delay(10);
27}
Строка кодаВывод на UnoВывод на ESP32 и PicoПочему?
a = 32767 + 1-3276832768int на Uno занимает 2 байта, на остальных — 4
60 * 1000-553660000Константы сначала вычисляются как int
60 * 1000L6000060000Суффикс L переводит вычисление в long
5 / 22.002.00Деление двух целых чисел даёт целое число
5 / 2.02.502.50Если у одного числа есть десятичная точка, она будет и у результата

Следи за светодиодом на D9: его яркость медленно растёт от 0 до 255, затем строка brightness++ превращает значение не в 256, а в 0, и светодиод внезапно гаснет. Это самый простой способ увидеть переполнение своими глазами. D9 — один из пинов ШИМ на Uno, отмеченных знаком ~, поэтому на нём работает analogWrite(); ШИМ подробно разобран в статье Что такое ШИМ?. Сброс millis() примерно через 49,7 дня — тоже переполнение, и в правильно написанном коде оно не вызывает проблем; как такой код писать, показано в руководстве millis() против delay().

В Arduino IDE открой «Файл › Настройки» (File › Preferences) и установи «Предупреждения компилятора» (Compiler warnings) на «Больше» или «Все». Тогда при выражении с переполнением, например 60 * 1000, компилятор покажет предупреждение integer overflow in expression (переполнение целого в выражении), и ошибка найдётся ещё до того, как код попадёт в плату.

const и #define: значения, которые никогда не меняются

Дать имя числам, которые не меняются во время работы программы, например номерам пинов и порогам, можно двумя способами. У константы, объявленной через const, есть тип, она подчиняется правилам области видимости, а при случайной попытке её изменить компилятор остановит ошибкой assignment of read-only variable 'LED_PIN' (присваивание переменной только для чтения). #define же — директива препроцессора: ещё до начала компиляции она заменяет каждое BUTTON_PIN в коде на 2. Типа у неё нет, и в такой строке не ставят ни точку с запятой, ни знак равенства. Для простых чисел ни один из способов не расходует ОЗУ, потому что компилятор подставляет значение прямо в код. Сегодня предпочитают const, а имена констант принято писать ЗАГЛАВНЫМИ_БУКВАМИ. В примере ниже используются оба способа: светодиод горит, пока кнопка удерживается.

C++ (Arduino IDE)
1#define BUTTON_PIN 2          // önişlemci: derlemeden önce metin değişimi / preprocessor: text swap before compiling
2const int LED_PIN = 13;       // tipi olan, salt okunur sabit / typed, read-only constant
3
4void setup() {
5  pinMode(LED_PIN, OUTPUT);
6  pinMode(BUTTON_PIN, INPUT_PULLUP);   // dahili pull-up: basılınca LOW / internal pull-up: LOW when pressed
7}
8
9void loop() {
10  if (digitalRead(BUTTON_PIN) == LOW) {   // buton basılı mı? / is the button pressed?
11    digitalWrite(LED_PIN, HIGH);          // evet: LED yanar / yes: LED on
12  } else {
13    digitalWrite(LED_PIN, LOW);           // hayır: LED söner / no: LED off
14  }
15}
Если написать #define LED_PIN 13;, точка с запятой станет частью значения, и строка pinMode(LED_PIN, OUTPUT) превратится в pinMode(13;, OUTPUT). Компилятор сообщит expected ')' before ';' token (ожидается «)» перед «;»); примечание in expansion of macro 'LED_PIN' (при раскрытии макроса) под ним показывает, что проблема проявляется там, где используется константа, но исправлять нужно строку с #define. Запись #define LED_PIN = 13 тоже даёт ошибку expected primary-expression before '=' token (ожидается выражение перед «=»).

Операторы: арифметика, сравнение и логика

Операторы позволяют считать с переменными, сравнивать их и объединять условия. Почти все операторы, которые нужны в повседневных проектах, собраны в этой таблице:

ГруппаОператорЗначениеПример
Арифметика+ - * /Четыре основных действияx = a * 2;
Остаток%Остаток от деления (только для целых чисел)7 % 3 даёт 1
Присваивание= += -= *= /=Присвоить, прибавить, вычесть…total += 5;
Инкремент, декремент++ --Прибавить 1 или вычесть 1i++;
Сравнение== != < > <= >=Результат — true или falseif (x >= 10)
Логика&& || !И, ИЛИ, НЕif (a && !b)
Условный? :Краткий if-elsex > 0 ? HIGH : LOW
Побитовые& | ^ ~ << >>Работают с битами числа1 << 3 даёт 8

Чаще всего путают две пары: = и ==, а также & и &&. Одинарный знак равенства присваивает значение, двойной — сравнивает. Одинарный & работает с битами чисел, а && проверяет, верны ли оба условия. Таблицы истинности И, ИЛИ и НЕ можно попробовать в утилите логических элементов, а побитовые операторы подробно разобраны в руководстве системы счисления и побитовые операции. Пример ниже считает, сколько раз нажата кнопка, и зажигает светодиод после нечётного числа нажатий:

C++ (Arduino IDE)
1const int LED_PIN = 13;
2const int BUTTON_PIN = 2;
3
4int presses = 0;               // toplam basış sayısı / total number of presses
5bool lastPressed = false;      // önceki turdaki durum / state on the previous pass
6
7void setup() {
8  pinMode(LED_PIN, OUTPUT);
9  pinMode(BUTTON_PIN, INPUT_PULLUP);
10  Serial.begin(9600);
11}
12
13void loop() {
14  bool pressed = (digitalRead(BUTTON_PIN) == LOW);   // karşılaştırma: true ya da false / comparison: true or false
15
16  if (pressed && !lastPressed) {     // şimdi basılı VE önceden değildi: yeni basış / pressed now AND not before: new press
17    presses++;                       // 1 artır / increment by 1
18    Serial.print("Basış / Presses: ");
19    Serial.println(presses);
20    delay(50);                       // basit debounce / simple debounce
21  }
22  lastPressed = pressed;
23
24  // Kalan (%) ile tek mi çift mi? / odd or even using the remainder (%)
25  digitalWrite(LED_PIN, (presses % 2 == 1) ? HIGH : LOW);
26}

Главное здесь — условие pressed && !lastPressed: оно означает «нажата сейчас И не была нажата на прошлом проходе». Так число не растёт всё время, пока кнопка удерживается, и каждое нажатие считается только один раз. presses % 2 даёт остаток от деления числа на 2: 1 для нечётных чисел, 0 для чётных. Затем оператор ? : в последней строке выбирает HIGH или LOW по этому результату.

if, else if и else: принятие решений

if выполняет код в фигурных скобках, если условие в круглых скобках истинно. Если условие ложно, выполняется блок else, если он есть. Когда вариантов больше, между ними добавляется else if. Условия проверяются сверху вниз, и как только выполнен блок первого истинного, остальные пропускаются. В C++ любое ненулевое значение считается истиной, поэтому if (counter) означает «если счётчик не равен нулю». Простой пример if-else уже встречался в разделе про const. Теперь напишем код, который отличает короткое нажатие от долгого:

C++ (Arduino IDE)
1const int LED_PIN = 13;
2const int BUTTON_PIN = 2;
3
4void setup() {
5  pinMode(LED_PIN, OUTPUT);
6  pinMode(BUTTON_PIN, INPUT_PULLUP);
7  Serial.begin(9600);
8}
9
10void loop() {
11  if (digitalRead(BUTTON_PIN) == LOW) {             // basıldı / pressed
12    unsigned long start = millis();
13    while (digitalRead(BUTTON_PIN) == LOW) {         // bırakılana kadar bekle / wait until released
14    }
15    unsigned long held = millis() - start;          // basılı kalma süresi (ms) / hold time (ms)
16
17    if (held < 50) {
18      return;                                       // 50 ms'den kısa: parazit, yok say / under 50 ms: bounce, ignore
19    } else if (held < 1000) {
20      digitalWrite(LED_PIN, HIGH);                  // kısa basış: LED yak / short press: LED on
21      Serial.println("Kısa basış / Short press");
22    } else {
23      digitalWrite(LED_PIN, LOW);                   // uzun basış: LED söndür / long press: LED off
24      Serial.println("Uzun basış / Long press");
25    }
26  }
27}

В момент нажатия кнопки код запоминает время через millis(), ждёт в цикле while, пока кнопку не отпустят, и вычисляет прошедшее время. Нажатия короче 50 мс считаются помехой от механического дребезга кнопки, и return досрочно завершает этот проход loop(). Нажатие короче 1 секунды включает светодиод, более долгое — выключает. Порядок условий важен: если поставить проверку held < 1000 первой, то и помеха в 30 мс считалась бы «коротким нажатием».

Запись if (mode = 2) — классическая ошибка: эта строка ничего не сравнивает, она присваивает mode значение 2, а раз результат не равен нулю, условие всегда истинно. Ещё одна коварная ошибка — точка с запятой сразу после скобок, как в if (x == 5);: if завершается пустой инструкцией, и блок ниже выполняется каждый раз, каким бы ни было условие.

switch-case: выбор из множества вариантов

Если в зависимости от значения переменной нужно делать много разных вещей, switch-case читается легче, чем длинная цепочка else if. switch сравнивает значение в скобках с каждой меткой case и начинает выполнение с совпавшей. Каждый case нужно завершать break; иначе программа продолжит выполнять код следующего case. Если ни один case не совпал, выполняется блок default. switch работает только с целочисленными типами вроде int, byte, char и enum; для String, float и диапазонов вроде «от 10 до 20» нужен if. В примере ниже каждое нажатие переводит лампу в следующий режим: выключена, включена, медленное мигание, быстрое мигание.

C++ (Arduino IDE)
1const int LED_PIN = 13;
2const int BUTTON_PIN = 2;
3
4byte mode = 0;                 // 0 kapalı, 1 açık, 2 yavaş, 3 hızlı / 0 off, 1 on, 2 slow, 3 fast
5bool lastPressed = false;
6
7void setup() {
8  pinMode(LED_PIN, OUTPUT);
9  pinMode(BUTTON_PIN, INPUT_PULLUP);
10}
11
12void loop() {
13  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
14  if (pressed && !lastPressed) {
15    mode = (mode + 1) % 4;      // 0 → 1 → 2 → 3 → 0
16    delay(50);                  // basit debounce / simple debounce
17  }
18  lastPressed = pressed;
19
20  switch (mode) {
21    case 0:                                        // kapalı / off
22      digitalWrite(LED_PIN, LOW);
23      break;
24    case 1:                                        // sürekli açık / always on
25      digitalWrite(LED_PIN, HIGH);
26      break;
27    case 2:                                        // 500 ms'de bir değiş / toggle every 500 ms
28      digitalWrite(LED_PIN, (millis() / 500) % 2 ? HIGH : LOW);
29      break;
30    case 3:                                        // 100 ms'de bir değiş / toggle every 100 ms
31      digitalWrite(LED_PIN, (millis() / 100) % 2 ? HIGH : LOW);
32      break;
33    default:                                       // beklenmeyen değer: başa dön / unexpected value: reset
34      mode = 0;
35      break;
36  }
37}

Строка mode = (mode + 1) % 4 проводит режим через 0, 1, 2, 3 и благодаря оператору % возвращает его к 0, когда он доходит до 4. Обрати внимание: для мигания delay() не используется. Выражение (millis() / 500) % 2 переключается между 0 и 1 каждые 500 мс, поэтому кнопка откликается мгновенно, пока светодиод мигает. Взрослая версия такого подхода с «режимами» называется конечным автоматом; как он применяется в проектах вроде светофора и меню, показано в руководстве Конечный автомат на Arduino.

Если внутри case объявляется переменная, оберни код этого case в фигурные скобки: case 1: { byte step = 3; ... break; }. Без скобок Uno компилирует это лишь с предупреждением, а ESP32 выдаёт ошибку jump to case label (переход к метке case), и код вообще не компилируется.

Циклы for, while и do-while

Циклы выполняют блок кода снова и снова. В скобках цикла for три части: начало (int i = 0), условие продолжения (i < 6) и шаг после каждого прохода (i++). for идеален, когда известно, сколько раз повторять. while продолжается, пока условие истинно, и проверяет его перед каждым проходом, поэтому подходит для ожиданий, длительность которых заранее неизвестна. do-while проверяет условие в конце, поэтому всегда выполняется хотя бы один раз. Внутри цикла break сразу выходит из цикла, а continue пропускает остаток текущего прохода и переходит к следующему. Поскольку loop() и так бесконечный цикл, писать внутри него while (true) обычно не нужно.

ЦиклКогда проверяется условие?Когда использоватьПример
forПеред каждым проходомКогда известно число повторовfor (int i = 0; i < 6; i++)
whileПеред каждым проходомПока ждём, когда что-то произойдётwhile (digitalRead(2) == HIGH)
do-whileПосле каждого проходаКогда код должен выполниться хотя бы разdo { ... } while (x < 10);

Пример ниже создаёт эффект Knight Rider на шести светодиодах. Цикл for в setup() одной строкой делает все шесть пинов выходами, а цикл while в loop() ждёт нажатия кнопки. Пока кнопка удерживается, огонёк бежит от D8 к D13 и обратно:

C++ (Arduino IDE)
1const int FIRST_LED = 8;       // ilk LED pini / first LED pin
2const int LAST_LED = 13;       // son LED pini / last LED pin
3const int BUTTON_PIN = 2;
4
5void setup() {
6  for (int pin = FIRST_LED; pin <= LAST_LED; pin++) {
7    pinMode(pin, OUTPUT);      // 6 pini tek döngüyle çıkış yap / make 6 pins outputs with one loop
8  }
9  pinMode(BUTTON_PIN, INPUT_PULLUP);
10}
11
12void loop() {
13  while (digitalRead(BUTTON_PIN) == HIGH) {
14    // butona basılana kadar burada bekle / wait here until the button is pressed
15  }
16
17  for (int pin = FIRST_LED; pin <= LAST_LED; pin++) {      // ileri: 8 → 13 / forward: 8 → 13
18    digitalWrite(pin, HIGH);
19    delay(80);
20    digitalWrite(pin, LOW);
21  }
22  for (int pin = LAST_LED - 1; pin > FIRST_LED; pin--) {   // geri: 12 → 9 / back: 12 → 9
23    digitalWrite(pin, HIGH);
24    delay(80);
25    digitalWrite(pin, LOW);
26  }
27}

Обратный цикл начинается с LAST_LED - 1 и идёт, только пока пин больше FIRST_LED, — это сделано намеренно: крайние светодиоды не загораются дважды подряд, и движение выглядит плавным. Регулировку скорости, эффект заполнения и другие узоры смотри в руководстве Огни Knight Rider и бегущий огонь на Arduino.

Цикл for (byte i = 0; i < 256; i++) никогда не заканчивается: byte вмещает максимум 255, поэтому i возвращается к 0, не дойдя до 256, и условие остаётся истинным навсегда. Точно так же цикл while, ожидающий условия, которое никогда не наступит, например сигнала от датчика с отошедшим проводом, намертво блокирует программу.

Массивы: много значений под одним именем

Массив хранит по порядку несколько значений одного типа под одним именем. Строка const byte LEDS[] = {8, 9, 10, 11, 12, 13}; создаёт массив из шести элементов. К элементам обращаются через квадратные скобки, и счёт начинается с 0: LEDS[0] равно 8, а LEDS[5] — 13. Длину массива после объявления изменить нельзя. Если поручить подсчёт числа элементов выражению sizeof(LEDS) / sizeof(LEDS[0]), а не вписывать его вручную, циклы сами подстроятся при добавлении нового пина в массив. Индекс за пределами массива, например LEDS[6], ошибки не вызывает: он читает или портит какую-то другую переменную в памяти. Поэтому нужно следить, чтобы индекс оставался в пределах от 0 до числа элементов минус один. Пример ниже превращает шесть светодиодов в электронный кубик:

C++ (Arduino IDE)
1const byte LEDS[] = {8, 9, 10, 11, 12, 13};               // LED pinleri / LED pins
2const byte LED_COUNT = sizeof(LEDS) / sizeof(LEDS[0]);    // eleman sayısı: 6 / number of elements: 6
3const byte BUTTON_PIN = 2;
4
5void setup() {
6  for (byte i = 0; i < LED_COUNT; i++) {     // indeks 0'dan başlar / index starts at 0
7    pinMode(LEDS[i], OUTPUT);
8  }
9  pinMode(BUTTON_PIN, INPUT_PULLUP);
10  randomSeed(analogRead(A0));                // boştaki A0'ın gürültüsüyle tohumla / seed from noise on the unconnected A0
11}
12
13void loop() {
14  if (digitalRead(BUTTON_PIN) == LOW) {
15    byte dice = random(1, 7);                // 1 ile 6 arası / between 1 and 6
16    for (byte i = 0; i < LED_COUNT; i++) {
17      digitalWrite(LEDS[i], (i < dice) ? HIGH : LOW);   // ilk 'dice' kadar LED yanar / the first 'dice' LEDs light up
18    }
19    delay(300);                              // basılı tutarsan zar dönmeye devam eder / keeps rolling while held
20  }
21}

random(1, 7) выдаёт число от 1 до 6, потому что верхняя граница в результат не входит. Загорается столько же светодиодов: если на кубике выпало 4, включаются четыре светодиода от LEDS[0] до LEDS[3]. Строка randomSeed(analogRead(A0)) берёт электрический шум на неподключённом пине A0 как начальное значение; без неё плата при каждом включении выдавала бы одну и ту же последовательность чисел. Ещё одно удобство массивов: если перенести светодиоды на другие пины, достаточно изменить только первую строку.

Функции: делим код на части

Если одна и та же работа выполняется в программе в нескольких местах, преврати её в функцию. Функция состоит из типа возвращаемого значения, имени, параметров в круглых скобках и тела в фигурных. Функции, которые ничего не возвращают, имеют тип void; те, что возвращают, отдают результат через return. Параметры передаются копиями, поэтому изменение параметра внутри функции не меняет переменную в месте вызова. Arduino IDE сама создаёт объявления для функций в файлах .ino, так что функцию можно написать и ниже loop(). А вот в файлах .cpp или в PlatformIO функцию нужно объявить до использования. Давай функциям имена, которые говорят, что они делают, например blinkLed или isPressed.

C++ (Arduino IDE)
1const byte LED_PIN = 13;
2const byte BUTTON_PIN = 2;
3
4// Değer döndürmeyen (void) fonksiyon / function that returns nothing (void)
5void blinkLed(byte pin, byte times, int ms) {
6  for (byte i = 0; i < times; i++) {
7    digitalWrite(pin, HIGH);
8    delay(ms);
9    digitalWrite(pin, LOW);
10    delay(ms);
11  }
12}
13
14// bool döndüren fonksiyon / function that returns a bool
15bool isPressed(byte pin) {
16  return digitalRead(pin) == LOW;      // INPUT_PULLUP: basılı = LOW / pressed = LOW
17}
18
19void setup() {
20  pinMode(LED_PIN, OUTPUT);
21  pinMode(BUTTON_PIN, INPUT_PULLUP);
22  blinkLed(LED_PIN, 3, 100);           // açılışta 3 hızlı yanıp sönme / 3 quick blinks at start-up
23}
24
25void loop() {
26  if (isPressed(BUTTON_PIN)) {
27    blinkLed(LED_PIN, 2, 250);         // basınca 2 yavaş yanıp sönme / 2 slow blinks per press
28  }
29}

blinkLed() принимает пин, число миганий и скорость как параметры, поэтому ту же функцию можно использовать для другого светодиода и другой скорости. isPressed() возвращает bool и держит логику «нажато = LOW» для INPUT_PULLUP в одном месте; если когда-нибудь подключить кнопку наоборот, менять придётся только эту строку. Однако delay() внутри функции по-прежнему останавливает программу. Для функций, которые делают несколько дел сразу, нужен отсчёт времени на основе millis().

Глобальные, локальные и статические переменные

То, где видна переменная, называется её областью видимости (scope). Глобальные переменные, объявленные вне функций, живут всё время работы программы, и их можно читать и менять откуда угодно. Локальные переменные, объявленные внутри функции или блока в фигурных скобках, создаются заново при каждом выполнении этого блока и исчезают, когда он заканчивается. Отсюда и самая частая жалоба новичков: «мой счётчик всегда остаётся равным 1». Если поставить перед локальной переменной static, она получит начальное значение только в первый раз и сохранит его между проходами, но видна будет по-прежнему только внутри своей функции.

C++ (Arduino IDE)
1const byte LED_PIN = 13;
2const byte BUTTON_PIN = 2;
3
4int globalCount = 0;            // global: her yerden görünür, değerini korur / global: visible everywhere, keeps its value
5
6void setup() {
7  pinMode(LED_PIN, OUTPUT);
8  pinMode(BUTTON_PIN, INPUT_PULLUP);
9  Serial.begin(9600);
10}
11
12void loop() {
13  int localCount = 0;           // yerel: her turda yeniden 0 olur / local: back to 0 on every pass
14  static int staticCount = 0;   // static yerel: değerini korur, yalnız burada görünür / static local: keeps its value, visible only here
15
16  if (digitalRead(BUTTON_PIN) == LOW) {
17    globalCount++;
18    localCount++;
19    staticCount++;
20    Serial.print(globalCount);
21    Serial.print(' ');
22    Serial.print(localCount);
23    Serial.print(' ');
24    Serial.println(staticCount);                   // 3. basışta: 3 1 3 / on the 3rd press: 3 1 3
25    digitalWrite(LED_PIN, (staticCount % 2) ? HIGH : LOW);
26    delay(300);
27  }
28}

После трёх нажатий кнопки в мониторе порта появятся строки 1 1 1, 2 1 2 и 3 1 3: глобальный и статический счётчики растут, а локальный всегда остаётся равным 1, потому что на каждом проходе loop() начинается заново с 0. Если объявить локальную переменную с тем же именем, что и глобальная, локальная перекроет глобальную, и внутри этой функции глобальная переменная вообще не будет затрагиваться. Глобальные переменные, которые меняет функция прерывания (ISR), также нужно объявлять как volatile; подробности — в руководстве Прерывания Arduino.

Делай переменные как можно более локальными: static — для счётчиков, нужных только одной функции, глобальные — для значений, общих для нескольких функций. Строка Global variables use … bytes of dynamic memory (глобальные переменные занимают … байт динамической памяти), которую IDE выводит после компиляции, показывает, сколько ОЗУ занимают глобальные переменные. На Uno нестабильная работа может начаться, когда эта доля превышает 75 процентов; решения ищи в руководстве Нехватка памяти на Arduino.

Программирование ESP32 в Arduino: тот же язык, другие размеры

При программировании ESP32 в Arduino IDE используются тот же язык C++ и те же команды, но есть несколько отличий. Поскольку у ESP32 32-битный процессор, int занимает 4 байта и вмещает значения от -2 147 483 648 до 2 147 483 647, поэтому 32767 + 1 в примере с переполнением даёт 32768. double занимает 8 байт, analogRead() возвращает значения от 0 до 4095, а пины работают от 3,3 V. Форматированный вывод делается через Serial.printf(). random() по умолчанию использует аппаратный генератор случайных чисел, поэтому строка randomSeed() не нужна. Программа ниже объединяет всё, что разобрано в этой статье: массив, функцию, switch-case и кнопку смены режимов. Общие возможности платы описаны в статье Что такое ESP32?.

C++ (Arduino IDE)
1// ESP32: aynı dil, farklı pinler / ESP32: same language, different pins
2const byte LEDS[] = {18, 19, 21, 22, 23, 25};            // güvenli GPIO'lar / safe GPIOs
3const byte LED_COUNT = sizeof(LEDS) / sizeof(LEDS[0]);
4const byte BUTTON_PIN = 4;                                // diğer bacağı GND'ye / other leg to GND
5
6byte mode = 0;                  // 0 kapalı, 1 yürüyen ışık, 2 hepsi yanıp söner / 0 off, 1 chaser, 2 all blink
7bool lastPressed = false;
8
9void setAll(bool on) {
10  for (byte i = 0; i < LED_COUNT; i++) {
11    digitalWrite(LEDS[i], on ? HIGH : LOW);
12  }
13}
14
15void setup() {
16  Serial.begin(115200);
17  for (byte i = 0; i < LED_COUNT; i++) {
18    pinMode(LEDS[i], OUTPUT);
19  }
20  pinMode(BUTTON_PIN, INPUT_PULLUP);
21  Serial.printf("int = %d bayt / bytes\n", (int)sizeof(int));   // ESP32'de 4 / 4 on the ESP32
22
23  int a = 32767;
24  a = a + 1;
25  Serial.println(a);                                          // ESP32'de 32768, taşma yok / 32768 on the ESP32, no overflow
26}
27
28void loop() {
29  bool pressed = (digitalRead(BUTTON_PIN) == LOW);
30  if (pressed && !lastPressed) {
31    mode = (mode + 1) % 3;      // 0 → 1 → 2 → 0
32    delay(50);
33  }
34  lastPressed = pressed;
35
36  switch (mode) {
37    case 0:
38      setAll(false);
39      break;
40    case 1: {                                                 // case içinde değişken için süslü parantez / braces for a variable inside a case
41      byte step = (millis() / 120) % LED_COUNT;               // sıradaki LED / LED whose turn it is
42      for (byte i = 0; i < LED_COUNT; i++) {
43        digitalWrite(LEDS[i], (i == step) ? HIGH : LOW);
44      }
45      break;
46    }
47    case 2:
48      setAll((millis() / 300) % 2);                           // 300 ms'de bir hepsi / all of them every 300 ms
49      break;
50  }
51}

Чтобы запустить эту программу на Uno, достаточно заменить массив LEDS на {8, 9, 10, 11, 12, 13}, установить BUTTON_PIN равным 2 и заменить строку Serial.printf(), которую Uno не поддерживает, на Serial.print(). Именно в этом польза объявления номеров пинов константами и массивами в начале файла. Обрати внимание и на то, что блок case 1 заключён в фигурные скобки: внутри него объявлена переменная step, поэтому на ESP32 эти скобки обязательны.

Raspberry Pi Pico: та же логика на MicroPython

Raspberry Pi Pico можно программировать на C++, добавив в Arduino IDE пакет плат RP2040; тогда код из этой статьи заработает после смены номеров пинов, и int снова будет занимать 4 байта. Но чаще всего Pico используют с MicroPython. В Python типы переменных не пишутся, блоки задаются отступами вместо фигурных скобок, точки с запятой в конце строки нет, а целые числа не переполняются, а растут настолько, насколько нужно. Код ниже — версия примера с электронным кубиком на MicroPython. Про настройку читай в статье Что такое Raspberry Pi Pico?, а про детали языка — в Основах MicroPython.

MicroPython
1from machine import Pin
2import time, random
3
4leds = [Pin(n, Pin.OUT) for n in (14, 13, 12, 11, 10, 9)]   # liste = dizi / list = array
5button = Pin(2, Pin.IN, Pin.PULL_UP)                        # basılınca 0 / reads 0 when pressed
6
7def show(count):                                            # fonksiyon / function
8    for i, led in enumerate(leds):                          # for döngüsü / for loop
9        led.value(1 if i < count else 0)
10
11show(0)                                                     # setup() karşılığı: bir kez çalışır / like setup(): runs once
12while True:                                                 # loop() karşılığı / like loop()
13    if button.value() == 0:                                 # buton basılı mı? / is the button pressed?
14        show(random.randint(1, 6))                          # 1-6 arası zar at / roll a number from 1 to 6
15        time.sleep_ms(300)
ПонятиеArduino (C++)MicroPython
Часть, которая выполняется один разsetup()Код перед строкой while True:
Часть, которая повторяется бесконечноloop()while True:
Переменнаяint x = 5;x = 5
Константаconst int PIN = 2;PIN = const(2) (нужен from micropython import const)
Условиеif / else if / elseif / elif / else
Множественный выборswitch-caseцепочка if / elif
Цикл со счётчикомfor (int i = 0; i < 6; i++)for i in range(6):
Массивbyte a[] = {1, 2, 3};a = [1, 2, 3]
Функцияvoid blink(int pin) { … } (функция без результата)def blink(pin): (объявление функции)
Логика&& || !and or not
Комментарий// комментарий# комментарий

Идеи проектов на этих основах

  • Светофор: пешеходный переход с кнопкой на константах, delay() и if; пошаговое руководство — в статье Светофор на Arduino.
  • Огни Knight Rider и бегущие огни: десятки разных световых узоров на цикле for и массиве.
  • Электронный кубик: перенеси кубик из 6 светодиодов на 7-сегментный индикатор с помощью массива, random() и функции; для этого смотри руководство 7-сегментный индикатор.
  • Тест реакции: зажги светодиод в случайный момент и измерь через unsigned long и millis(), как быстро нажимается кнопка.
  • Многорежимная лампа: режимы «выключено», «постоянно», «дыхание» и «вспышка» от одной кнопки через switch-case; цветную версию смотри в статье Как использовать RGB-светодиод.
  • Кодовый замок: собери нажатые клавиши в массив и сравни их с сохранённым кодом; подробности — в руководстве матричная клавиатура.

Частые ошибки и как их исправить

ОшибкаПризнакРешение
= в сравненииБлок if выполняется всегдаДля сравнения используй ==
; или = в строке #defineexpected ')' before ';' token (ожидается «)» перед «;»)Пиши так: #define LED_PIN 13
Неверный регистр букв в команде'pinmode' was not declared in this scope (имя «pinmode» не объявлено)Следи за заглавными и строчными буквами, как в pinMode
Кириллица или буква с диакритикой в имени переменнойstray '\303' in program (лишний символ в программе)Используй в именах только латинские буквы, цифры и _
Вычисление 60 * 1000 как intРезультат равен -5536Пиши 60 * 1000L или 60000UL
Хранение millis() в intОтсчёт времени ломается через 32 секундыИспользуй unsigned long
Целочисленное деление5 / 2 даёт 2Пиши 5 / 2.0 или приводи к float
Забытый break в конце caseВыполняется и следующий caseЗавершай каждый case через break
Запись за пределы массиваСлучайные значения, зависанияДержи индекс в пределах от 0 до числа элементов - 1
Локальный счётчикСчётчик всегда остаётся равным 1Сделай его глобальным или static
При ошибке компиляции читай только первую строку с ошибкой и исправляй её: большинство ошибок в следующих строках — побочные эффекты первой. Что означают сообщения, объясняет руководство Ошибки компиляции Arduino. Если платы под рукой нет, код можно попробовать и в симуляции Tinkercad.

Когда эти строительные блоки освоены, следующий шаг — познакомиться с самыми используемыми командами: список команд Arduino с примерами охватывает всё от pinMode() до attachInterrupt(). В приложении BlueGrays есть и проекты вроде мигающего светодиода, управления светодиодом кнопкой, огней Knight Rider, светофора и кубика — пошагово, со схемами подключения и готовым кодом, так что каждую конструкцию из этой статьи можно попробовать на реальной схеме.

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

На каком языке программируют Arduino?

Код Arduino построен на C++. Ядро Arduino добавляет к языку готовые функции вроде pinMode(), digitalWrite() и delay(), а код пишется в файлах с расширением .ino. Платы вроде ESP32 и Raspberry Pi Pico при желании можно программировать и на MicroPython.

С чего начать изучение программирования Arduino?

Сначала установи Arduino IDE и загрузи пример Blink, который мигает встроенным светодиодом. Затем по порядку попробуй setup() и loop(), переменные, if-else, циклы, массивы и функции, каждую тему — на схеме со светодиодом или кнопкой. Если применять каждую новую тему в небольшом проекте, она запоминается гораздо лучше, чем после простого чтения.

Чем int отличается от long в Arduino?

На Arduino Uno int занимает 2 байта и вмещает числа от -32 768 до 32 767, а long занимает 4 байта и доходит примерно до ±2,1 млрд. Число, вышедшее за этот предел, прокручивается без всякой ошибки, поэтому для больших значений вроде millis() используют long или unsigned long. На ESP32 и Pico int тоже занимает 4 байта.

Чем const отличается от #define в Arduino?

У константы, объявленной через const, есть тип, она подчиняется правилам области видимости, а при попытке её изменить компилятор сообщает об ошибке. #define — это текстовая замена до компиляции: типа у неё нет, а если поставить в конце точку с запятой, появятся неожиданные ошибки. Для простых чисел ни один способ не расходует ОЗУ, но обычно предпочитают const, потому что так надёжнее.

Чем цикл for отличается от цикла while в Arduino?

Цикл for собирает начало, условие и шаг в одной строке и используется, когда известно, сколько раз повторять, например чтобы зажечь шесть светодиодов один за другим. Цикл while только проверяет условие и работает, пока оно истинно, — это подходит для ситуаций неизвестной длительности, например ожидания нажатия кнопки.

Можно ли программировать Arduino на Python?

8-битные платы вроде Arduino Uno не могут выполнять Python, поэтому их программируют на C++. А вот ESP32 и Raspberry Pi Pico можно программировать на MicroPython. Кроме того, программа на Python, запущенная на компьютере, может косвенно управлять Uno, общаясь с ней через последовательный порт.

Все проекты
Мигающий светодиод
#1Новичок

Мигающий светодиод

Самое первое «привет» в электронике: включение и выключение одного светодиода. Arduino включает и выключает светодиод через короткие промежутки, поэтому свет быстро мигает. Ты самым простым способом в

4.6(188)
1127
Управление светодиодом кнопкой (удержание)
#9Новичок

Управление светодиодом кнопкой (удержание)

Как дверной звонок: свет горит, пока ты нажимаешь, и гаснет, когда отпускаешь. Arduino всё время слушает кнопку; пока она нажата, светодиод горит, а как только ты убираешь палец, он гаснет. Эта просте

4.8(113)
229
Включение и выключение светодиода кнопкой (вкл/выкл)
#10Новичок

Включение и выключение светодиода кнопкой (вкл/выкл)

Нажми кнопку один раз — свет загорится, нажми ещё раз — погаснет, совсем как выключатель в твоей комнате. Arduino замечает каждое нажатие и переводит светодиод в противоположное состояние, выжидая кор

4.8(100)
167
Светодиодный эффект Knight Rider
#6Средний

Светодиодный эффект Knight Rider

Классический бегущий огонь-сканер: светящаяся точка скользит от одного края шести светодиодов к другому и возвращается обратно. Arduino зажигает каждый светодиод на десятую долю секунды и переходит к

4.8(106)
202
Светофор
#3Новичок

Светофор

Крошечная копия светофора, который ты каждый день видишь на дороге. Красный горит две секунды, жёлтый мигает на мгновение, зелёный горит две секунды, затем через жёлтый снова возвращается красный, и т

4.8(131)
397
Управление светодиодом кнопкой на ESP32 (удержание)
#104Новичок

Управление светодиодом кнопкой на ESP32 (удержание)

Светодиод горит, когда кнопка нажата, и гаснет, когда её отпускают. Показывает, как используются цифровой входной пин ESP32 и встроенный подтягивающий резистор. Режим INPUT_PULLUP избавляет от внешнего резистора.

4.6(36)
55
Переключение светодиода кнопкой на Pico
#156Новичок

Переключение светодиода кнопкой на Pico

Кнопка + светодиод — следующий уровень! В отличие от базового проекта с кнопкой, держать кнопку не нужно. Нажми и отпусти → светодиод горит. Нажми и отпусти ещё раз → гаснет (переключение). Схема та же (светодиод GP14, кнопка GP2), но код сильно отличается. Изучи определение фронта (ловлю момента спада 1→0), отслеживание состояния и антидребезг (фильтр механического дребезга). Метод led.toggle() в MicroPython меняет состояние одной строкой.

0.0(0)
9
Игральный кубик
#79Средний
Premium

Игральный кубик

Больше никаких потерянных кубиков во время настольных игр: при каждом нажатии кнопки на семисегментном индикаторе появляется случайное число от 1 до 6. Каждое нажатие даёт ровно один результат, и он т

5.0(48)
67