| |

Как правильно писать скетч для Arduino и ESP — подробный урок

Всем привет! В этой статье я подробно расскажу, как правильно писать скетч, чтобы он был легко понятен, хорошо работал и легко исправлялся. Погнали =)

1. СТРУКТУРА СКЕТЧА

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

  1. Подключение библиотек/файлов, объявление констант/переменных. Эта часть находится в самом верху кода, начинается с первой строки и заканчивается там, где начинается функция setup(). В этой части указывают большинство переменных и констант, которые будут использоваться в скетче, а также подключаются библиотеки и остальные файлы проекта. Для многих модулей, например, TFT дисплеи, сервоприводы, необходимо создать объект (указать имя и пины) прямо в начале кода после подключения библиотек. Пример первой части кода представлен в строках 1-10 примера кода внизу.
  2. Функция setup(). Эта функция выполняется один раз после запуска микроконтроллера. В этой части обычно инициализируются объекты каких-либо устройств, подключенных к плате или просто инициализируются библиотеки. а также задаются роли для цифровых/аналоговых пинов платы (вход или выход сигнала). Если в проекте используется коммуникация по Serial, то указывается скорость порта специальной функцией. Пример второй части кода представлен в строках 11-20 примера кода внизу.
  3. Функция loop(). Эта функция выполняется циклично после выполнения функции setup(). В этой части скетча выполняются основные вычисления и действия, которые будут повторяться, пока не отключат питание платы. Например, подаются сигналы на пины, отправляются сообщения в Serial, считываются показания с датчиков и т.д. Пример первой части кода представлен в строках 21-30 примера кода внизу.
  4. Остальные функции (необязательно, но желательно). Если у вас большой код, состоящий из групп отдельных действий, то рекомендую разделить основной код на функции, описать эти функции по отдельности после основного цикла loop(), и в цикле просто вызывать эти функции в нужном порядке. На работу кода это не повлияет, зато будет удобно редактировать код, так как он разделён на функции, а у функций есть имена, чтобы знать что функция делает.

Пример хорошего, удобно воспринимаемого и красивого кода:

#include <Servo.h>

const int BUTTON_PIN = 2;
const int SERVO_PIN = 9;
const int DEBOUNCE_DELAY = 50;

int buttonState = 0;
int lastButtonState = HIGH;
int servoAngle = 0;
unsigned long lastDebounceTime = 0;

Servo myServo;

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  Serial.begin(9600);
  myServo.attach(SERVO_PIN);
}

void loop() {
  handleButton();
  updateServo();
  printStatus();
}

void handleButton() {
  int reading = digitalRead(BUTTON_PIN);
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }
  if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
    if (reading != buttonState) {
      buttonState = reading;
      
      if (buttonState == LOW) {
        servoAngle = (servoAngle + 45) % 180;
      }
    }
  }
  lastButtonState = reading;
}

void updateServo() {
  myServo.write(servoAngle);
}

void printStatus() {
  Serial.print("Button: ");
  Serial.print(buttonState == LOW ? "PRESSED" : "RELEASED");
  Serial.print(" | Servo angle: ");
  Serial.println(servoAngle);
  delay(100);
}

2. КОММЕНТАРИИ

Для того, чтобы было понятно, что означает какая-либо строка кода, нужно писать комментарии. Комментарии в одну строку обозначаются двумя косыми линиями (слешами), пример:

// Комментарий в одну строку

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

/*
  Комментарий во много строк
  Комментарий во много строк
  Комментарий во много строк
*/

Комментарии не обрабатываются компилятором, а значит, не занимают место в памяти микроконтроллера. Поэтому комментариев можно писать сколько угодно =)

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

void myFunc(){
   Serial.println("Эта строка будет работать!");

   //Serial.println("А эта не будет!");

   /* Serial.println("И эта строка не будет!");
   Serial.println("И эта строка не будет!");
   Serial.println("И эта строка не будет!"); */

}

В этом фрагменте кода я представлю все способы добавить пояснения к коду:

Serial.println("Hi, dear Arduiner!"); // Комментарий в строке

// Комментарий над строкой
servo.write(90);

/*
    Подробный комментарий 
    Расскажем, например, про особенности
    подключения модуля или создания сигнала

    Ниже у нас функция подачи ШИМ сигнала, параметры:
     - Номер пина с поддержкой ШИМ (для Arduino Uno/Nano это пины 3, 5, 6, 9, 10, 11)
     - Уровень сигнала (для Arduino Uno/Nano от 0 до 255)
*/
analogWrite(5, 250);

Таким образом, красивый код с комментариями будет выглядеть примерно так:

// === Библиотека для серво ===
#include <Servo.h>

// === Константы (не меняются в коде) ===
const int BUTTON_PIN = 2;     // Пин кнопки
const int SERVO_PIN = 9;       // Пин сервопривода
const int DEBOUNCE_DELAY = 50; // Задержка для антидребезга (мс)

// === Переменные (меняются в коде) ===
int buttonState = 0;           // Текущее состояние кнопки
int lastButtonState = HIGH;    // Предыдущее состояние
int servoAngle = 0;            // Текущий угол сервы
unsigned long lastDebounceTime = 0; // Таймер антидребезга

// === Объект серво ===
Servo myServo; // Создаем объект сервопривода

// === Функция настроек Setup ===
void setup() {
  // Настройка пинов
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  
  // Инициализация Serial
  Serial.begin(9600);
  
  // Подключение сервопривода
  myServo.attach(SERVO_PIN);
}

// === Цикл Loop ===
void loop() {
  handleButton();  // Проверка кнопки
  updateServo();   // Обновление позиции сервы
  printStatus();   // Вывод данных в Serial
}

// === Функция обработки кнопки ===
void handleButton() {
  int reading = digitalRead(BUTTON_PIN);
  
  // Если состояние изменилось
  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }
  
  // Проверка временного интервала для антидребезга
  if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
    if (reading != buttonState) {
      buttonState = reading;
      
      // Если кнопка нажата (LOW, так как INPUT_PULLUP)
      if (buttonState == LOW) {
        servoAngle = (servoAngle + 45) % 180; // Изменяем угол на 45°
      }
    }
  }
  lastButtonState = reading;
}

// === Функция обновления сервопривода ===
void updateServo() {
  myServo.write(servoAngle); // Устанавливаем угол
}

// === Функция вывода статуса ===
void printStatus() {
  Serial.print("Button: ");
  Serial.print(buttonState == LOW ? "PRESSED" : "RELEASED");
  Serial.print(" | Servo angle: ");
  Serial.println(servoAngle);
  delay(100); // Небольшая задержка для читаемости логов
}

3. СОЗДАНИЕ ФУНКЦИЙ

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

Создаётся функция вот так. Пишем «void «, затем имя функции латинскими буквами и цифрами, начинающееся с буквы. Затем круглые скобки и фигурные скобки. Между фигурных скобок ставим курсор и 2 раза жмём клавишу ENTER для создания переноса строки. Теперь пишем код для этой функции в фигурных скобок.

Если есть необходимость указывать параметры для функции при вызове, можно создать необходимые переменные в круглых скобках. Они специально и созданы для параметров =)

Пример создания функции ниже. Мы создали функцию для управления встроенным светодиодом на плате.

void builtinLedControl(bool ledstate){
  digitalWrite(LED_BUILTIN, ledstate);
  if (ledstate == 0){
    Serial.println("Builtin led OFF!");
  }
  else if (ledstate == 1){
    Serial.println("Builtin led ON!");
  }
}

В идеале, именно для этой функции можно ещё добавить параметр «номер пина». Реализуем:

void ledControl(int ledPin, bool ledstate){
  digitalWrite(ledPin, ledstate);
  if (ledstate == 0){
    Serial.println("Led OFF!");
  }
  else if (ledstate == 1){
    Serial.println("Led ON!");
  }
}

Так вот, функцию мы объявили, теперь нужно её вызвать, потому что без вызова она ничего делать не будет. Вызвать функцию можно в коде любой другой «самодельной» функции, а также в функциях SETUP и LOOP. Реализуем вызов нашей функции ledControl() в коде основного цикла LOOP:

void loop(){
  ledControl(5, 1); // Вызываем нашу функцию ledControl, в параметрах укажем пин 5 и уровень сигнала 1
}

И вот так выглядит пример цельного рабочего кода с нашей функцией:

void setup(){
  pinMode(5, OUTPUT); // Делаем пин 5 выходом сигнала
}

void loop(){
  ledControl(5, 1); // Вызываем нашу функцию ledControl, в параметрах укажем пин 5 и уровень сигнала 1
}

void ledControl(int ledPin, bool ledstate){ // Объявляем функцию ledControl
  digitalWrite(ledPin, ledstate);
  if (ledstate == 0){
    Serial.println("Led OFF!");
  }
  else if (ledstate == 1){
    Serial.println("Led ON!");
  }
}

4. ЗАДЕРЖКИ

Часто люди используют в роли задержки функцию delay(), однако её можно использовать не во всех случаях, сейчас разберёмся почему.

Функция delay() не просто делает задержку, а останавливает полностью выполнение всего кода на время. Если вам нужно просто мигать светодиодом, тогда эта функция вполне подойдёт. Но если вам нужно одновременно мигать светодиодом и обрабатывать запросы веб-сервера, тогда уже не получится, так как веб-сервер должен постоянно работать, а с функцией delay() веб-сервер будет работать около 1 миллисекунды останавливать работу на время свечения/не свечения светодиода. Надеюсь вы поняли =)

В такой непростой ситуации приходит на помощь функция millis(). Она, конечно, не работает так, как delay(), она просто возвращает количество миллисекунд, прошедшее со времени её первого вызова. С помощью несложного расчёта можно сделать аналог delay(). Пример кода мигания светодиодом с millis():

int timer = 1000; // Время паузы в мс
int ledPin = 13; // Пин светодиода. Если нужно, замените на нужный номер

bool ledState = 0; // Служебная переменная состояния светодиода
unsigned long interv = 0; // Служебная переменная специального вида для расчёта времени

void setup(){
   pinMode(ledPin, OUTPUT); // Делаем пин светодиода выходом сигнала
}
void loop(){
   if(millis() - interv >= timer){ // Если настало время...
      interv = millis(); // Приравниваем служебную переменную с millis
      ledState = !ledState; // Меняем переменную состояния светодиода
      digitalWrite(ledPin, ledState); // Включаем/выключаем светодиод
   }
}

С использованием такой схемы можно мигать светодиодом, не останавливая очень важные скрипты =)

5. ИСПОЛЬЗУЙТЕ СОВРЕМЕННЫЕ КОНСТРУКЦИИ

У некоторых функций есть современные аналоги, упрощающие написание кода. Сейчас рассмотрим некоторые из них.

IF…ELSE

Если вам нужно проверить, равна ли переменная 1 (да/истина) или 0 (нет/ложь), то не обязательно указывать число, с которым сравниваем переменную (1 или 0):

// Старый вариант сравнения с 1 или 0
if (select == 1){
   // делаем что-то если select равна 1
}
else if (select == 0){
   // делаем что-то если select равна 0
}

Можно не писать 1 или 0, а использовать вот такой вариант:

// Новый вариант сравнения с 1 или 0
if (select){
   // делаем что-то если select равна 1
}
else if (!select){
   // делаем что-то если select равна 0
}

В первом случае, если select равна 1, компилятор знает, что так сравнивается переменная с 1. Во втором случае, если select равна 0, компилятор знает, что переменная сравнивается с 0. На объём самой программы в памяти микроконтроллера это не повлияет, но зато код выглядит современнее =)

SWITCH-CASE вместо IF…ELSE

Если вам нужно сравнить значение переменной с определёнными значениями, например, кодами кнопок ИК пульта, тогда следует использовать конструкцию SWITCH-CASE. Она занимает меньше места в коде и выглядит современнее.

Сравните, конструкция с IF…ELSE:

if (ircode == 000000){
   // делаем что-то при получении кода 000000
}
else if (ircode == 111111){
   // делаем что-то при получении кода 111111
}
else if (ircode == 222222){
   // делаем что-то при получении кода 222222
}
// ...и ещё штук 10-20 таких конструкций, сколько там у пульта кнопок =)

И современная конструкция SWITCH-CASE:

switch (ircode){
   case 000000:
      // делаем что-то при получении кода 000000
      break; // Обязательная функция break, обозначает конец кейса
   case 111111:
      // делаем что-то при получении кода 111111
      break;
   case 222222:
      // делаем что-то при получении кода 222222
      break;
   // такие конструкции повторяем для всех нужных значений
}

6. ОФОРМЛЕНИЕ СКЕТЧА

При написании кода обязательно нужно использовать табуляцию — это такой вид отступа, вставляется клавишей TAB, примерно равен 4-7 пробелов. Табуляцией нужно отодвигать содержимое функций и других элементов. Пример кода без табуляций:

void setup(){
pinMode(2, OUTPUT);
pinMode(3, INPUT_PULLUP);
servo.attach(9);
byte map[5][8]{
1, 1, 1, 1, 1, 1, 1, 1,
1, 0, 0, 0, 0, 1, 1, 1,
1, 1, 1, 0, 0, 0, 1, 1,
1, 0, 0, 0, 0, 0, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
}
}

Без отступов получается не красиво и не понятно что и где. Вот пример кода с табуляциями в правильных местах:

void setup(){
    pinMode(2, OUTPUT);
    pinMode(3, INPUT_PULLUP);
    servo.attach(9);
    byte map[5][8]{
        1, 1, 1, 1, 1, 1, 1, 1,
        1, 0, 0, 0, 0, 1, 1, 1,
        1, 1, 1, 0, 0, 0, 1, 1,
        1, 0, 0, 0, 0, 0, 1, 1,
        1, 1, 1, 1, 1, 1, 1, 1,
    }
}

Так выглядит гораздо лучше =)

7. КАК ПРАВИЛЬНО УКАЗЫВАТЬ ФУНКЦИИ и т.д.

Сейчас рассмотрим правильно написание элементов скетча, таких как функции, переменные, константы и другие.

ПЕРЕМЕННЫЕ

Переменная — виртуальный «контейнер», значение которого может меняться далее в скетче.

Например, переменная может содержать число для дальнейших вычислений и изменения его. Для проекта калькулятора сразу объявляются все переменные, в которые будет записываться число, введённое пользователем.

Переменные указываются в начале кода вот по этой схеме:

тип_переменной имя_переменной = значение;

Примеры правильно указанных переменных:

int chislo = 256; // Значение числовой переменной указывается просто вот так

String slovo = "Hi everyone"; // Значения переменных строкового типа указываются в кавычках

bool logic = false; // Переменные типа BOOL могут принимать значения только TRUE/1 либо FALSE/0

КОНСТАНТЫ

Константа — виртуальный контейнер, значение которого не может измениться в скетче.

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

Константы стандартного вида указываются также как и переменные, но в самом начале нужно написать слово CONST. Примеры:

// Вот так можно создать константу для указания имени пина
#define LEDPIN 3

const int chislo = 256; // Значение числовой константы указывается просто вот так

const String slovo = "Hi everyone"; // Значения констант строкового типа указываются в кавычках

ФУНКЦИИ

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

void myFunction(){
   // Код этой функции
}
// Функция с параметрами:
void myFuncParams(int speed; String word;){ // Параметры указываем в круглых скобках в виде переменных без значений
   // Код функции, с использованием переменных из параметров
}

Вызываются функции вот так:

void loop(){
    myFunction(); // Функция без параметров вызывается так
    myFuncParams(50, "Hi everyone"); // Функция с параметрами. Параметры указываем в скобочках.
}

ЗНАКИ ПРЕПИНАНИЯ

Сейчас рассмотрим написание знаков препинания в коде.

  1. В конце каждой функции обязательно пишем точку с запятой «;», обозначающую завершение действия.
  2. Параметры функции указываются через запятую, в порядке их указания при создании функции.
  3. Все значения строкового типа указываются в двойных кавычках «».
  4. Код функции указывается в фигурных скобках {}.
  5. Параметры функции указываются в круглых скобках ().
  6. Комментарии указывают двумя слешами // или слешами со звёздочками /* комментарий */.
  7. Присваивание значения переменной — один знак «=».
  8. Операторы сравнения (указаны в кавычках):
    • Больше — «>»
    • Меньше — «<«
    • Не больше (меньше или равно) — «<=»
    • Не меньше (больше или равно) — «>=»
    • Равно — «==»
    • Не равно — «!=»

На этом пока всё. Спасибо за внимание, пишите правильный код! Удачи =)

Если понравилась статья, поставьте 5 звёзд! Поддержать автора можно в блоке ниже.

5/5 - (10 голосов)


Поделись!