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

Всем привет! В этой статье я подробно расскажу, как правильно писать скетч, чтобы он был легко понятен, хорошо работал и легко исправлялся. Погнали =)
1. СТРУКТУРА СКЕТЧА
Скетч разделяется визуально на несколько частей:
- Подключение библиотек/файлов, объявление констант/переменных. Эта часть находится в самом верху кода, начинается с первой строки и заканчивается там, где начинается функция setup(). В этой части указывают большинство переменных и констант, которые будут использоваться в скетче, а также подключаются библиотеки и остальные файлы проекта. Для многих модулей, например, TFT дисплеи, сервоприводы, необходимо создать объект (указать имя и пины) прямо в начале кода после подключения библиотек. Пример первой части кода представлен в строках 1-10 примера кода внизу.
- Функция setup(). Эта функция выполняется один раз после запуска микроконтроллера. В этой части обычно инициализируются объекты каких-либо устройств, подключенных к плате или просто инициализируются библиотеки. а также задаются роли для цифровых/аналоговых пинов платы (вход или выход сигнала). Если в проекте используется коммуникация по Serial, то указывается скорость порта специальной функцией. Пример второй части кода представлен в строках 11-20 примера кода внизу.
- Функция loop(). Эта функция выполняется циклично после выполнения функции setup(). В этой части скетча выполняются основные вычисления и действия, которые будут повторяться, пока не отключат питание платы. Например, подаются сигналы на пины, отправляются сообщения в Serial, считываются показания с датчиков и т.д. Пример первой части кода представлен в строках 21-30 примера кода внизу.
- Остальные функции (необязательно, но желательно). Если у вас большой код, состоящий из групп отдельных действий, то рекомендую разделить основной код на функции, описать эти функции по отдельности после основного цикла 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"); // Функция с параметрами. Параметры указываем в скобочках.
}
ЗНАКИ ПРЕПИНАНИЯ
Сейчас рассмотрим написание знаков препинания в коде.
- В конце каждой функции обязательно пишем точку с запятой «;», обозначающую завершение действия.
- Параметры функции указываются через запятую, в порядке их указания при создании функции.
- Все значения строкового типа указываются в двойных кавычках «».
- Код функции указывается в фигурных скобках {}.
- Параметры функции указываются в круглых скобках ().
- Комментарии указывают двумя слешами // или слешами со звёздочками /* комментарий */.
- Присваивание значения переменной — один знак «=».
- Операторы сравнения (указаны в кавычках):
- Больше — «>»
- Меньше — «<«
- Не больше (меньше или равно) — «<=»
- Не меньше (больше или равно) — «>=»
- Равно — «==»
- Не равно — «!=»
На этом пока всё. Спасибо за внимание, пишите правильный код! Удачи =)
Если понравилась статья, поставьте 5 звёзд! Поддержать автора можно в блоке ниже.