Основы программирования 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 и на каком языке оно ведётся?
Код 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 Uno | ESP32 | Raspberry Pi Pico |
|---|---|---|---|
| 6 светодиодов (каждый с 220 Ω, катод на GND) | D8, D9, D10, D11, D12, D13 | GPIO 18, 19, 21, 22, 23, 25 | GP14, GP13, GP12, GP11, GP10, GP9 |
| Кнопка (вторая ножка на GND) | D2 | GPIO 4 | GP2 |
| Встроенный светодиод | D13 (светодиод с меткой L) | GPIO 2 (на большинстве DevKit) | GP25 («LED» на Pico W) |
| Скорость порта | 9600 бод | 115200 бод | окно Shell в Thonny |
setup() и loop(): скелет любого скетча
В каждом скетче Arduino есть две обязательные функции. setup() выполняется только один раз — при включении платы или нажатии кнопки сброса; сюда идут подготовительные действия вроде настройки пинов и Serial.begin(). loop() затем повторяется бесконечно после завершения setup(). За кулисами в ядре Arduino есть скрытая функция main(): сначала она готовит железо, затем один раз вызывает setup() и снова и снова вызывает loop() внутри бесконечного цикла. Область в начале файла, за пределами этих двух функций, — это глобальная область; там объявляются константы и переменные, которые должны быть доступны отовсюду.
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 | Диапазон | Типичное применение |
|---|---|---|---|
| bool | 1 байт | true или false | Горит ли светодиод, нажата ли кнопка |
| byte | 1 байт | 0 – 255 | Номер пина, яркость ШИМ |
| char | 1 байт | -128 – 127 | Один символ: 'A' |
| int | 2 байта | -32 768 – 32 767 | Счётчики, результаты analogRead() |
| unsigned int | 2 байта | 0 – 65 535 | Только положительные числа |
| long | 4 байта | -2 147 483 648 – 2 147 483 647 | Большие целые числа |
| unsigned long | 4 байта | 0 – 4 294 967 295 | Измерение времени через millis() |
| float | 4 байта | Около ±3,4 × 10^38, точность 6–7 знаков | Температура, напряжение, средние значения |
| double | 4 байта (на 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 на светодиоде:
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 | -32768 | 32768 | int на Uno занимает 2 байта, на остальных — 4 |
| 60 * 1000 | -5536 | 60000 | Константы сначала вычисляются как int |
| 60 * 1000L | 60000 | 60000 | Суффикс L переводит вычисление в long |
| 5 / 2 | 2.00 | 2.00 | Деление двух целых чисел даёт целое число |
| 5 / 2.0 | 2.50 | 2.50 | Если у одного числа есть десятичная точка, она будет и у результата |
Следи за светодиодом на D9: его яркость медленно растёт от 0 до 255, затем строка brightness++ превращает значение не в 256, а в 0, и светодиод внезапно гаснет. Это самый простой способ увидеть переполнение своими глазами. D9 — один из пинов ШИМ на Uno, отмеченных знаком ~, поэтому на нём работает analogWrite(); ШИМ подробно разобран в статье Что такое ШИМ?. Сброс millis() примерно через 49,7 дня — тоже переполнение, и в правильно написанном коде оно не вызывает проблем; как такой код писать, показано в руководстве millis() против delay().
const и #define: значения, которые никогда не меняются
Дать имя числам, которые не меняются во время работы программы, например номерам пинов и порогам, можно двумя способами. У константы, объявленной через const, есть тип, она подчиняется правилам области видимости, а при случайной попытке её изменить компилятор остановит ошибкой assignment of read-only variable 'LED_PIN' (присваивание переменной только для чтения). #define же — директива препроцессора: ещё до начала компиляции она заменяет каждое BUTTON_PIN в коде на 2. Типа у неё нет, и в такой строке не ставят ни точку с запятой, ни знак равенства. Для простых чисел ни один из способов не расходует ОЗУ, потому что компилятор подставляет значение прямо в код. Сегодня предпочитают const, а имена констант принято писать ЗАГЛАВНЫМИ_БУКВАМИ. В примере ниже используются оба способа: светодиод горит, пока кнопка удерживается.
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}Операторы: арифметика, сравнение и логика
Операторы позволяют считать с переменными, сравнивать их и объединять условия. Почти все операторы, которые нужны в повседневных проектах, собраны в этой таблице:
| Группа | Оператор | Значение | Пример |
|---|---|---|---|
| Арифметика | + - * / | Четыре основных действия | x = a * 2; |
| Остаток | % | Остаток от деления (только для целых чисел) | 7 % 3 даёт 1 |
| Присваивание | = += -= *= /= | Присвоить, прибавить, вычесть… | total += 5; |
| Инкремент, декремент | ++ -- | Прибавить 1 или вычесть 1 | i++; |
| Сравнение | == != < > <= >= | Результат — true или false | if (x >= 10) |
| Логика | && || ! | И, ИЛИ, НЕ | if (a && !b) |
| Условный | ? : | Краткий if-else | x > 0 ? HIGH : LOW |
| Побитовые | & | ^ ~ << >> | Работают с битами числа | 1 << 3 даёт 8 |
Чаще всего путают две пары: = и ==, а также & и &&. Одинарный знак равенства присваивает значение, двойной — сравнивает. Одинарный & работает с битами чисел, а && проверяет, верны ли оба условия. Таблицы истинности И, ИЛИ и НЕ можно попробовать в утилите логических элементов, а побитовые операторы подробно разобраны в руководстве системы счисления и побитовые операции. Пример ниже считает, сколько раз нажата кнопка, и зажигает светодиод после нечётного числа нажатий:
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. Теперь напишем код, который отличает короткое нажатие от долгого:
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 мс считалась бы «коротким нажатием».
switch-case: выбор из множества вариантов
Если в зависимости от значения переменной нужно делать много разных вещей, switch-case читается легче, чем длинная цепочка else if. switch сравнивает значение в скобках с каждой меткой case и начинает выполнение с совпавшей. Каждый case нужно завершать break; иначе программа продолжит выполнять код следующего case. Если ни один case не совпал, выполняется блок default. switch работает только с целочисленными типами вроде int, byte, char и enum; для String, float и диапазонов вроде «от 10 до 20» нужен if. В примере ниже каждое нажатие переводит лампу в следующий режим: выключена, включена, медленное мигание, быстрое мигание.
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.
Циклы 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 и обратно:
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.
Массивы: много значений под одним именем
Массив хранит по порядку несколько значений одного типа под одним именем. Строка const byte LEDS[] = {8, 9, 10, 11, 12, 13}; создаёт массив из шести элементов. К элементам обращаются через квадратные скобки, и счёт начинается с 0: LEDS[0] равно 8, а LEDS[5] — 13. Длину массива после объявления изменить нельзя. Если поручить подсчёт числа элементов выражению sizeof(LEDS) / sizeof(LEDS[0]), а не вписывать его вручную, циклы сами подстроятся при добавлении нового пина в массив. Индекс за пределами массива, например LEDS[6], ошибки не вызывает: он читает или портит какую-то другую переменную в памяти. Поэтому нужно следить, чтобы индекс оставался в пределах от 0 до числа элементов минус один. Пример ниже превращает шесть светодиодов в электронный кубик:
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.
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, она получит начальное значение только в первый раз и сохранит его между проходами, но видна будет по-прежнему только внутри своей функции.
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.
Программирование 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?.
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.
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 / else | if / 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 выполняется всегда | Для сравнения используй == |
| ; или = в строке #define | expected ')' 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 с примерами охватывает всё от 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, общаясь с ней через последовательный порт.
Что собрать после прочтения
Все проекты
Мигающий светодиод
Самое первое «привет» в электронике: включение и выключение одного светодиода. Arduino включает и выключает светодиод через короткие промежутки, поэтому свет быстро мигает. Ты самым простым способом в

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

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

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

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

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

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

Игральный кубик
Больше никаких потерянных кубиков во время настольных игр: при каждом нажатии кнопки на семисегментном индикаторе появляется случайное число от 1 до 6. Каждое нажатие даёт ровно один результат, и он т
Другие статьи
Приложение BlueGrays
Собери эту схему шаг за шагом в телефоне
Схема подключения, код и список деталей на одном экране. 200+ проектов на Arduino, ESP32 и Pico готовы к сборке.
Наведи камеру телефона
Наведи камеру на код — откроется магазин приложений на телефоне.
или открыть магазин

