Урок 7. Классы в программах Ардуино. Кнопка как объект.

Arduino UNO R3

В этом уроке будем рассматривать кнопки как объекты, создадим класс для них.

Предыдущий урок     Список уроков     Следующий урок

В прошлом уроке мы написали рабочий, отлаженный программный блок, который можно вполне использовать в боевых программах. Но как-то не совсем красиво получилось.

  • Программный блок для обработки сигнала размещается вместе с другими блоками и ухудшает читаемость программы.
  • Надо определить для него переменные, ничего не забыть.
  • А если нам необходимо подключить несколько кнопок. Для каждой из них надо завести свои переменные, свои программные блоки, свои функции. И ничего не перепутать.
  • При каждом использовании программного блока обработки сигнала кнопки придется вспоминать, как он работает, какие переменные требует и, самое главное, менять имена переменных в тексте блока.

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

 

Классы в C++ для Ардуино.

Классы позволяют программисту создавать новые типы объектов. Они состоят из свойств и методов.  Свойства – это данные, которыми можно характеризовать объект класса. Методы – это функции, которые могут выполнять действия над свойствами класса.

  • Свойства класса это его переменные.
  • Методы класса это его  функции.

Определение класса выглядит так:

class имя_класса  { члены класса };

Члены класса это переменные, функции, другие классы и т.п.

Создание класса для обработки сигналов кнопок Button.

Создадим класс для нашего объекта кнопки. Назовем его Button.

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

boolean flagPress= false;    // признак кнопка сейчас нажата
boolean flagClick= false;    // признак кнопка была нажата (клик)
byte  buttonCount= 0;        // счетчик подтверждений стабильного состояния
#define TIME_BUTTON 15       // время стабильного состояния (* 2 мс)

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

Оформим перечисленные переменные как свойства класса.

// Описание класса обработки сигналов кнопок
class Button {

boolean flagPress;    // признак кнопка сейчас нажата
boolean flagClick;    // признак кнопка была нажата (клик)
byte  buttonCount;    // счетчик подтверждений стабильного состояния   
byte timeButton;      // время подтверждения состояния кнопки
byte _pin;            // номер вывода
 };

Button – это имя класса, а в скобках указаны его свойства (переменные).

 

Модификаторы доступа private и public.

Все свойства и методы класса имеют права доступа. Например, счетчик buttonCount используется только самим классом для собственных вычислений. Никогда к нему не будут обращаться другие программные модули. Логично запретить доступ к нему всех функций, кроме методов своего класса Button.

Для этого в языке C++ существуют модификаторы private и public.

  • Функции и переменные, которые находятся после модификатора public, доступны из любого места программы.
  • После модификатора private размещаются закрытые функции и переменные. С ними могут работать только методы собственного класса. Если отсутствует модификатор public, то все члены класса считаются закрытыми.

Нам надо выбрать, какие переменные (свойства) сделать открытыми, а какие – закрытыми. Хороший стиль объектно-ориентированного программирования предполагает, что все переменные должны быть закрытыми. А обращение к ним происходит через методы (функции) класса. Но надо сделать поправку на то, что мы пишем программу для микроконтроллера с ограниченной производительностью. А вызов любой функции занимает определенное время. Поэтому, свойства, к которым программа обращается часто, следует сделать открытыми и обращаться к ним явно. Это уменьшит время выполнения программы.

В нашем случае это признаки flagPress и flagClick. К ним постоянно обращается программа для контроля состояния кнопок. А номер вывода _pin и время подтверждения  timeButton обычно устанавливаются только один раз. Сделаем эти переменные закрытыми. Устанавливать их будем из дополнительного метода.

С учетом вышесказанного наш класс будет выглядеть так.

// Описание класса обработки сигналов кнопок
class Button {

  public:
    boolean flagPress;    // признак кнопка сейчас нажата
     boolean flagClick;    // признак кнопка была нажата (клик)

  private:
    byte  _buttonCount;    // счетчик подтверждений стабильного состояния   
     byte _timeButton;      // время подтверждения состояния кнопки
    byte _pin;            // номер вывода
};

Переменные timeButton и pin нам придется заявлять как аргументы метода для установки значений. Поэтому мы добавили _ перед именами, чтобы отличать аргументы метода и переменные класса.

Созданный нами класс пока состоит только из свойств. Надо добавить в него методы – функции.

Нам необходим метод для проверки состояния сигнала кнопки, тот самый, что мы вызывали в цикле каждые 2 мс. Назовем его  scanState (проверка состояния).

Принято названия классов писать в смешанном регистре, начиная с большой буквы, а названия методов - в смешанном регистре, начиная с маленькой буквы, первая часть – глагол.

Т.к. переменные номер вывода и время подтверждения мы сделали закрытыми, то необходима функция установки их значений. Например, setPinTime.

Т.е. для класса Button необходимы два метода.  С методами наш класс будет выглядеть так.

// Описание класса обработки сигналов кнопок
class Button {

  public:
    boolean flagPress;    // признак кнопка сейчас нажата
    boolean flagClick;    // признак кнопка была нажата (клик)

    void scanState();     // метод проверки состояние сигнала
    void setPinTime(byte pin, byte timeButton); // метод установки номера вывода и времени подтверждения

  private:
    byte  _buttonCount;    // счетчик подтверждений стабильного состояния   
    byte _timeButton;      // время подтверждения состояния кнопки
    byte _pin;            // номер вывода
};

Метод void scanState() не имеет аргументов и ничего не возвращает. Метод  void setPinTime(byte pin, byte timeButton) ничего не возвращает и имеет два аргумента: номер вывода и время подтверждения стабильного состояния кнопки.

 

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

void Button:: scanState() {

// код метода

}

Button::  означает, что функция scanState()  это метод класса Button.

Напишем код метода scanState().

// метод проверки состояния кнопки
// flagPress= true  - нажата 
//  flagPress= false - отжата
//  flagClick= true - была нажата (клик)
void Button::scanState() {

 if ( flagPress == (! digitalRead(_pin)) ) {
     // состояние сигнала осталось прежним
     _buttonCount= 0;  // сброс счетчика состояния сигнала
  }
  else {
     // состояние сигнала изменилось
     _buttonCount++;   // +1 к счетчику состояния сигнала

     if ( _buttonCount >= _timeButton ) {
      // состояние сигнала не менялось заданное время
      // состояние сигнала стало устойчивым
      flagPress= ! flagPress; // инверсия признака состояния
_buttonCount= 0;  // сброс счетчика состояния сигнала

 

      if ( flagPress == true ) flagClick= true; // признак клика на нажатие     
     }   
  }
}

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

Теперь код  для метода setPinTime(byte pin, byte timeButton). Он совсем простой. Перегружает аргументы метода в закрытые свойства класса и устанавливает режим вывода.

// метод установки номера вывода и времени подтверждения
void Button::setPinTime(byte pin, byte timeButton)  {

  _pin= pin;
  _timeButton= timeButton;
  pinMode(_pin, INPUT_PULLUP);  // определяем вывод как вход
}

Мы завершили создание класса Button. Осталось научиться пользоваться им.

Класс  это только описание типа объекта, самого объекта еще нет. Его надо создать. Делается это так же, как и создание переменных при использовании встроенных типов данных.

int x;     // мы создали переменную типа int с именем x

Button button1;   // мы создали объект типа Button с именем button1

Button buttonPlus;   // мы создали еще один объект типа Button с именем buttonPlus

Вы поняли, что теперь добавление новой кнопки в систему можно сделать одной строкой.

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

button1.flagClick= false;   // переменная flagClick j, объекта button1 = false

button1.scanState();       // вызов метода scanState(), объекта button1

button1.setPinTime(12, 20); // вызов метода setPinTime (), объекта button1 с параметрами 12, 20

Проверка состояния кнопки button1 из любого места программы будет выглядеть так:

if ( button1.flagPress == true )  {

// кнопка нажата  }

Думаю, теперь в программе объяснять ничего не надо. Тем более, что комментариев в ней больше, чем кода.

/*  Программа sketch_7_1 урока 7
 *  Каждое нажатие кнопки меняет состояние светодиода */

#define LED_PIN 13     // светодиод подключен к выводу 13
#define BUTTON_PIN 12  // кнопка подключена к выводу 12

// Описание класса обработки сигналов кнопок
class Button {
  public:
    boolean flagPress;    // признак кнопка сейчас нажата
    boolean flagClick;    // признак кнопка была нажата (клик)
    void scanState();     // метод проверки состояние сигнала
    void setPinTime(byte pin, byte timeButton); // метод установки номера вывода и времени (числа) подтверждения

  private:
    byte  _buttonCount;    // счетчик подтверждений стабильного состояния  
    byte  _timeButton;      // время подтверждения состояния кнопки
    byte  _pin;             // номер вывода
};

boolean ledState;         // переменная состояния светодиода

Button button1;   // создание объекта типа Button с именем button1
  
void setup() {
  pinMode(LED_PIN, OUTPUT);           // определяем вывод 13 (светодиод) как выход
button1.setPinTime(BUTTON_PIN, 15); // вызов метода установки объекта button1 с параметрами: номер вывода 12, число подтверждений 15
}

// бесконечный цикл с периодом 2 мс
void loop() {

  button1.scanState();  // вызов метода сканирования сигнала кнопки
 
  // блок управления светодиодом
  if ( button1.flagClick == true ) {
    // было нажатие кнопки
    button1.flagClick= false;         // сброс признака клика
    ledState= ! ledState;             // инверсия состояния светодиода
    digitalWrite(LED_PIN, ledState);  // вывод состояния светодиода   
  }

  delay(2);  // задержка на 2 мс
}

// метод проверки состояния кнопки
// flagPress= true  - нажата
//  flagPress= false - отжата
//  flagClick= true - была нажата (клик)
void Button::scanState() {

 if ( flagPress == (! digitalRead(_pin)) ) {
     // состояние сигнала осталось прежним
     _buttonCount= 0;  // сброс счетчика состояния сигнала
  }
  else {
     // состояние сигнала изменилось
     _buttonCount++;   // +1 к счетчику состояния сигнала

     if ( _buttonCount >= _timeButton ) {
      // состояние сигнала не менялось заданное время
      // состояние сигнала стало устойчивым
      flagPress= ! flagPress; // инверсия признака состояния
      _buttonCount= 0;  // сброс счетчика состояния сигнала

      if ( flagPress == true ) flagClick= true; // признак клика на нажатие     
     }   
  }
}

// метод установки номера вывода и времени подтверждения
void Button::setPinTime(byte pin, byte timeButton)  {

  _pin= pin;
  _timeButton= timeButton;
  pinMode(_pin, INPUT_PULLUP);  // определяем вывод как вход
}

Загрузите, проверьте. Работает.

 

Конструкторы класса в программах Ардуино.

Конструкторы класса это функция, которая автоматически вызывается при создании объекта этого класса.

  • Конструктор является членом класса;
  • не имеет типа возвращаемого значения, даже void;
  • имеет то же имя, что и класс.

В нашем классе Button мы можем создать конструктор для того, чтобы не делать лишний вызов метода setPinTime(12, 15). А установку параметров производить при создании объекта button1.

Для этого добавим в описание класса описание конструктора.

Button(byte pin, byte timeButton);  // описание конструктора

И в конце программы напишем код конструктора очень похожий на метод  setPinTime.

// описание конструктора класса Button
Button::Button(byte pin, byte timeButton) {

  _pin= pin;
  _timeButton= timeButton;
  pinMode(_pin, INPUT_PULLUP);  // определяем вывод как вход
}

Теперь мы можем установить параметры pin и timeButton при создании объекта.

Button button1(BUTTON_1_PIN, 15);  // создание объекта для кнопки 1 с параметрами BUTTON_1_PIN и 15

Использовать метод setPinTime необязательно. Хотя лучше его оставить, чтобы программа могла менять параметры после создания объекта.

Полный вариант кода программы с конструктором в следующем разделе.

 

Проверим работу программы с двумя объектами (кнопками).

Добавим в схему еще одну кнопку и один светодиод. Подключим их к плате Ардуино по схеме.

Схема подключения

У меня все это выглядит так.

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

Первая кнопка меняет состояние светодиода на плате Ардуино, а вторая – светодиода на макетной плате.

  • Создаем в программе два объекта button1 и button2 с использованием параметров конструктора.
  • В бесконечном цикле вызываем методы scanState для обоих объектов.
  • В бесконечном цикле проверяем флаги обоих объектов и управляем светодиодом.

Вот текст программы.

/*  Программа sketch_7_2 урока 7
 *  Подключены две кнопки и светодиод
 *  Каждое нажатие кнопки 1 инвертирует состояние светодиода на плате Ардуино
 *  Каждое нажатие кнопки 2 инвертирует состояние светодиода на макетной плате */

#define LED_1_PIN 13     // номер вывода светодиода 1 равен 13
#define BUTTON_1_PIN 12  // номер вывода кнопки 1 равен 12
#define BUTTON_2_PIN 11  // номер вывода кнопки 2 равен 11
#define LED_2_PIN 10     // номер вывода светодиода 2 равен 10

// Описание класса обработки сигналов кнопок
class Button {
  public:
    Button(byte pin, byte timeButton);  // описание конструктора
    boolean flagPress;    // признак кнопка сейчас нажата
    boolean flagClick;    // признак кнопка была нажата (клик)
    void scanState();     // метод проверки состояние сигнала
void setPinTime(byte pin, byte timeButton); // метод установки номера вывода и времени (числа) подтверждения
  private:
    byte  _buttonCount;    // счетчик подтверждений стабильного состояния   
    byte _timeButton;      // время подтверждения состояния кнопки
    byte _pin;             // номер вывода
};

boolean ledState1;         // переменная состояния светодиода 1
boolean ledState2;         // переменная состояния светодиода 2

Button button1(BUTTON_1_PIN, 15);  // создание объекта для кнопки 1
Button button2(BUTTON_2_PIN, 15);  // создание объекта для кнопки 2
  
void setup() {
  pinMode(LED_1_PIN, OUTPUT);           // определяем вывод светодиода 1 как выход
  pinMode(LED_2_PIN, OUTPUT);           // определяем вывод светодиода 2 как выход
}

// бесконечный цикл с периодом 2 мс
void loop() {

  button1.scanState();  // вызов метода сканирования сигнала кнопки 1
  button2.scanState();  // вызов метода сканирования сигнала кнопки 2
 
  // блок управления светодиодом 1
  if ( button1.flagClick == true ) {
    // было нажатие кнопки
    button1.flagClick= false;         // сброс признака клика
    ledState1= ! ledState1;             // инверсия состояния светодиода 1
    digitalWrite(LED_1_PIN, ledState1);  // вывод состояния светодиода 1   
  }

  // блок управления светодиодом 2
  if ( button2.flagClick == true ) {
    // было нажатие кнопки
    button2.flagClick= false;         // сброс признака клика
    ledState2= ! ledState2;             // инверсия состояние светодиода 2
    digitalWrite(LED_2_PIN, ledState2);  // вывод состояния светодиода 2   
  }

  delay(2);  // задержка на 2 мс
}

// метод проверки состояния кнопки
// flagPress= true  - нажата 
//  flagPress= false - отжата
//  flagClick= true - была нажата (клик)
void Button::scanState() {

 if ( flagPress == (! digitalRead(_pin)) ) {
     // состояние сигнала осталось прежним
     _buttonCount= 0;  // сброс счетчика состояния сигнала
}
  else {
     // состояние сигнала изменилось
     _buttonCount++;   // +1 к счетчику состояния сигнала

     if ( _buttonCount >= _timeButton ) {
      // состояние сигнала не менялось заданное время
      // состояние сигнала стало устойчивым
      flagPress= ! flagPress; // инверсия признака состояния
_buttonCount= 0;  // сброс счетчика состояния сигнала

 

      if ( flagPress == true ) flagClick= true; // признак клика на нажатие     
     }   
  }
}

// метод установки номера вывода и времени подтверждения
void Button::setPinTime(byte pin, byte timeButton)  {

  _pin= pin;
  _timeButton= timeButton;
  pinMode(_pin, INPUT_PULLUP);  // определяем вывод как вход
}

// описание конструктора класса Button
Button::Button(byte pin, byte timeButton) {

  _pin= pin;
  _timeButton= timeButton;
  pinMode(_pin, INPUT_PULLUP);  // определяем вывод как вход
}

Загружаем. Все работает.

 

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

Предыдущий урок     Список уроков     Следующий урок

27 комментариев на «Урок 7. Классы в программах Ардуино. Кнопка как объект.»

  1. Видимо я упустил какой-то нюанс, поэтому не могу понять зачем мы ввели локальные переменные ( _timeButton, _pin) если есть публичные (timeButton, pin).
    Ведь в методе setPinTime мы можем обойтись публичными без операции =?

    • timeButton, pin это аргументы функции, доступные внутри функции setPinTime(). А _timeButton и _pin это переменные класса, доступные всем методам класса.

  2. button1.flagClick= false; // переменная flagClick j, объекта button1 = false

    В комментарии все верно написано?

  3. Немного не понимаю схему работы переменной flagPress.
    В false она возращаетя только через 12 милисекунд после смены состояния?

    • Да. Это время на устранение дребезга, фильтрацию сигнала. Разве это критично, если программа среагирует на нажатие кнопки на 12 мс позже.

      • Спасибо за ответ. Я просто новичок в программировании. Вот код и пытаюсь понять 🙂 По моему очень хорошее решение. Не надо лишнюю строчку кода на возврат писать.

  4. Мы вызываем метод scanState() 2 раза для каждого экземпляра класса. Здесь всё логично. Кнопок много — метод один. С++ рулит. Но, где вызов метода setPinTime, коли он объявлен? И, если можно, объясните, пожалуйста, преимущество использование конструктора.

    • Я цитирую из урока:
      » В нашем классе Button мы можем создать конструктор для того, чтобы не делать лишний вызов метода setPinTime(12, 15). А установку параметров производить при создании объекта button1.»
      Конструктор позволяет при объявлении экземпляра класса инициализировать его параметры (свойства).
      А setPinTime мы не вызываем, потому что задали параметры в конструкторе. Он может потребоваться для изменения параметров Button1, например изменения времени подтверждения.

  5. В строке программы 7_1 » if ( flagPress == (! digitalRead(_pin)) ) » мы сравниваем flagPress с инверсией сигнала, считанного с какого, извиняюсь, контакта? Ведь связи между _pin и живым входом до того в тексте не встречалось, только указание, что _pin — типа byte, а значение?

    • Вот связь в методе setPinTime:
      button1.setPinTime(BUTTON_PIN, 15); // вызов метода установки объекта button1 с параметрами: номер вывода 12, число подтверждений 15
      Через него мы задали номер вывода, т.е. свойство _pin.

      • Точно. Простите за слепоту. Ведь метод setPinTime вызывается до того, в void setup…

  6. В программе 7_2 конструктор, как метод, вызывается по количеству создаваемых объектов (2 раза). Однако, если его заменить тем же методом setPinTime, то он тоже будет вызываться 2 раза (по количеству объектов). Какое в таком случае преимущество конструктора (то же количество вызова подпрограмм) помимо инициализации параметров?

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

  7. Доброго времени суток. Поймал себя на мысли, что никак не возьму в толк какое состояние имеет переменная flagPress в начале работы, когда мы её сравниваем с digitalRead(_pin) в методе scanState : » if ( flagPress == (! digitalRead(_pin)) ) «

    • Здравствуйте! При объявлении компилятор инициализирует переменные 0. Реальное состояние кнопки определится только через время фильтрации.

  8. Хочу поблагодарить вас за то, что вы делаете. Хороший курс. Я как новичок пока, что все понимаю и это благодаря вам.

  9. Добрый день! На одном дыхании дошел до этого урока и хочу поблагодарить за проделанную работу…. Хоть у меня и есть небольшой опыт программирования на Бейсике, FoxPro, Ассемблере (20 лет назад), и до сих пор немного для себя на Дельфи, но сталкнувшись с необходимостью разобраться с ардуиновскими контроллерами, понял, что столько нюансов в написании кода… да же в той же орфографии.. Ваши уроки раскрывают по-новому взгляд на многие алгоритмы, команды и тд… Большое спасибо… перехожу к следующему уроку 🙂

  10. Добрый вечер!
    Эдуард, я новичок в программировании, но Ваши уроки
    мне понравились и во многом помогли. Есть проблема, если есть возможность ее описать, то буду очень благодарен!
    Есть библиотека MCP23xxx.h к шилду LCD Keypad Shield I2C. В функции setup, кнопки шилда прописываем как MCP.pinMode(12, INPUT);. и впрограмме обращемся к ним как MCP.digitalRead(12);
    Вопрос: можно ли при создании своей библиотеки как то обращаться к этим кнопкам или к библиотеке MCP23xxx.h, заранее спасибо

    • Здравствуйте! Я никогда не использовал расширители портов. Попробуйте, проверьте на практике.

  11. Каеф!
    Саму прогу я бы по другому написал.
    Но как все обстоятельно и доходчиво про классы и объекты. Ох***но!
    Наконец я понял как делать классы.
    Спасибо огромное.

    • Спасибо за эмоциональную оценку моей работы. А программы в следующих уроках по другому пишутся. Это один из начальных уроках, где показан принцип обработки сигнала кнопки.

  12. Привет, а как поместить в класс такое?
    void errorHTML(WebServer &server, WebServer::ConnectionType type, char *url_tail, bool tail_complete);
    c такими параметрами: WebServer &server, WebServer::ConnectionType type,

    здесь Webduino server исползует.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *