Посмотр рубрик

Разделение на «модули»

Чтобы превратить скетч в читаемый и хорошо структурированный код, нужно научиться разделять программу на независимые "модули", которые взаимодействуют между собой в основной программе. Если в программе есть набор переменных, которые работают со своим набором функций - их можно и нужно вынести в отдельный модуль. Если внутри модуля наблюдается такая же ситуация - то его тоже можно разделить на подмодули и разложить по файлам. Когда модуль независимый - его удобно дорабатывать и тестировать отдельно от основной программы. По сути, любая библиотека как раз и является таким модулем.

Пример с кнопкой #

Рассмотрим обработку нажатия кнопки (с флагом, как в уроке про флаги) как пример выделения в модуль. Обработка одной кнопки в формате "скетча новичка" может выглядеть так:

// пин кнопки (второй пин на GND)
const uint8_t btn = 2;

// состояние кнопки
bool state;

// инициализация
void buttonInit() {
    pinMode(btn, INPUT_PULLUP); // второй пин на GND, делаем подтяжку к VCC
    state = digitalRead(btn);   // начальное состояние
}

// вернёт true однократно при нажатии
bool buttonClick() {
    if (state != digitalRead(btn)) {
        state = !state;
        if (!state) return true; // LOW - кнопка нажата
    }
    return false;
}

void setup() {
    Serial.begin(115200);
    buttonInit();
}

void loop() {
    if (buttonClick()) Serial.println("Click!");
}

Программа работает отлично, но если захочется увеличить функциональность кнопки - появятся ещё функции и дополнительные глобальные переменные, а если нужно будет добавить ещё несколько кнопок - придётся продублировать весь этот код, добавляя индексы к именам переменных и функций. В итоге дорабатывать и поддерживать это полотно станет очень сложно. Рассмотрим три варианта выделения этого кода в независимый модуль.

Переменные и функции #

Основная проблема примера с кнопкой состоит в том, что функции buttonInit() и buttonClick() используют глобальные переменные и зависят от них. Если нам понадобится ещё одна кнопка - придётся добавить новые переменные и написать для них новые функции - код начнёт дублироваться. Функцию (исполняемый код, алгоритм) нужно отделить от данных, тогда эта функция станет универсальной. Вот более простой пример:

const uint8_t btn1 = 2;

bool readButton() {
    return !digitalRead(btn1);
}

Достаточно передать в функцию номер пина, чтобы сделать её универсальной:

const uint8_t btn1 = 2;
const uint8_t btn2 = 3;
const uint8_t btn3 = 4;

bool readButton(uint8_t buttonPin) {
    return !digitalRead(buttonPin);
}

// readButton(btn1);
// readButton(btn2);
// readButton(btn3);

Вернёмся к примеру с кнопкой. Помимо константы с номером пина в нём используется переменная state, которая меняется внутри функции - её можно передать по указателю:

// === ФУНКЦИИ КНОПКИ
void buttonInit(bool* state, uint8_t pin) {
    pinMode(pin, INPUT_PULLUP);
    *state = digitalRead(pin);
}

bool buttonClick(bool* state, uint8_t pin) {
    if (*state != digitalRead(pin)) {
        *state = !*state;
        if (!*state) return true;
    }
    return false;
}
// === СКЕТЧ
// пины
const uint8_t btn1 = 2;
const uint8_t btn2 = 3;

// состояния
bool state1;
bool state2;

void setup() {
    Serial.begin(115200);
    buttonInit(&state1, btn1);
    buttonInit(&state2, btn2);
}

void loop() {
    if (buttonClick(&state1, btn1)) Serial.println("Click 1");
    if (buttonClick(&state2, btn2)) Serial.println("Click 2");
}

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

Структура и функции #

Это основной "сишный" подход к организации модулей: создаётся структура и набор функций, которые принимают эту структуру по указателю:

// === ФУНКЦИИ КНОПКИ
struct Button {
    uint8_t pin;
    bool state;
};

void buttonInit(Button* btn) {
    pinMode(btn->pin, INPUT_PULLUP);
    btn->state = digitalRead(btn->pin);
}

bool buttonClick(Button* btn) {
    if (btn->state != digitalRead(btn->pin)) {
        btn->state = !btn->state;
        if (!btn->state) return true;
    }
    return false;
}
// === СКЕТЧ
Button btn1{2};
Button btn2{3};

void setup() {
    Serial.begin(115200);
    buttonInit(&btn1);
    buttonInit(&btn2);
}

void loop() {
    if (buttonClick(&btn1)) Serial.println("Click 1");
    if (buttonClick(&btn2)) Serial.println("Click 2");
}

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

Класс #

В C++ подход "структура и функции" выглядит архаично и многословно, ведь есть классы, которые позволяют сделать всё то же самое, но гораздо более удобно - скрыть все функции внутри объекта:

// === КЛАСС КНОПКИ
class Button {
   public:
    Button(uint8_t pin) : pin(pin) {
        pinMode(pin, INPUT_PULLUP);
        state = digitalRead(pin);
    }

    bool click() {
        if (state != digitalRead(pin)) {
            state = !state;
            if (!state) return true;
        }
        return false;
    }

   private:
    const uint8_t pin;
    bool state;
};
// === СКЕТЧ
Button btn1(2);
Button btn2(3);

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

void loop() {
    if (btn1.click()) Serial.println("Click 1");
    if (btn2.click()) Serial.println("Click 2");
}

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

class Button {
   public:
    Button(uint8_t pin) : pin(pin) {
        pinMode(pin, INPUT_PULLUP);
        state = digitalRead(pin);
    }

    bool click() {
        if (uint8_t(uint8_t(millis()) - tmr) < 50) return false;

        tmr = millis();
        if (state != digitalRead(pin)) {
            state = !state;
            if (!state) return true;
        }
        return false;
    }

   private:
    const uint8_t pin;
    uint8_t tmr;
    bool state;
};

Полезные страницы #

(5 голосов)
Подписаться
Уведомить о
guest

7 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх