Урок 13. Разработка и использование классов в C++. Создание класса обработки дискретных сигналов Debounce.

Уроки STM32

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

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

Давайте разработаем класс, который реализует функции обработки дискретных сигналов по алгоритмам из предыдущего урока.

 

Попробуем “убить двух зайцев”:

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

Можете еще обратиться к уроку 7 курса Уроки Ардуино. Там тема классов рассматривается доступнее и более подробно.

 

Почему класс, а не функция?

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

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

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

Красивое и практичное решение  – создать новый тип объектов, ориентированных на обработку дискретных сигналов. Т.е. разработать свой класс.

 

Классы в C++.

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

Классы состоят из свойств и методов. Свойства – это данные объекта. Методы – это функции для действий над его свойствами.

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

Класс объявляется с помощью ключевого слова class.

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

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

Думаю, все это вы знаете из уроков Ардуино. Займемся конкретным примером.

 

Создание класса обработки дискретных сигналов STM32.

Я решил назвать класс Debounce. В переводе - устранение дребезга, дрожания, выбросов.

Создадим проект Lesson13_1.  С помощью STM32CubeMX установим в нем только конфигурацию системы тактирования (урок 5).

Преобразуем проект в C++ (урок 11).

Объявим наш класс, укажем свойства и методы для него. Все действия будем производить в файле main.cpp.

/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */

class Debounce {
    public:
        Debounce(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, uint32_t filterTime); // конструктор
        void scanStability(void); // метод ожидания стабильного состояния сигнала
        void scanAverage(void); // метод фильтрации сигнала по среднему значению
        void setTime(uint32_t filterTime); // метод установки времени фильтрации
        uint8_t readFlagLow(void); // чтение признака СИГНАЛ В НИЗКОМ УРОВНЕ
        uint8_t readFlagRising(void); // чтение признака БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
        uint8_t readFlagFalling(void); // чтение признака БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ

        uint8_t flagLow; // признак СИГНАЛ В НИЗКОМ УРОВНЕ
        uint8_t flagRising; // признак БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
        uint8_t flagFalling; // признак БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ

    private:
        GPIO_TypeDef *_GPIOx; // порт
        uint16_t _GPIO_Pin; // маска вывода
        uint32_t _filterTime; // время фильтрации
        uint32_t _filterTimeCount; // счетчик времени фильтрации
};

Я выбрал следующие методы и свойства.

Debounce(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, uint32_t filterTime)

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

  • GPIOx – выбор порта. (GPIOA, GPIOB, GPIOC … ).
  • GPIO_Pin – Номер вывода (используется маска).
  • filterTime – число подтверждений сигнала или время усреднения.

Конструктор сам конфигурирует заданный вывод на режим входа с подтягивающим резистором.

Debounce button(GPIOA, 1 << 14, 30); // экземпляр класса Debounce, вывод PA14, число подтверждений 30

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

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

void scanStability(void); // метод ожидания стабильного состояния сигнала
void scanAverage(void); // метод фильтрации сигнала по среднему значению

Эти методы не имеют аргументов, ничего не возвращают.

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

void setTime(uint32_t filterTime); // метод установки времени фильтрации

Результатом работы класса будут свойства-признаки:

uint8_t flagLow; // признак СИГНАЛ В НИЗКОМ УРОВНЕ
uint8_t flagRising; // признак БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
uint8_t flagFalling; // признак БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ

Первый из них (flagLow ) показывает текущее состояние сигнала. При низком уровне сигнала он равен 1. Как правило, активным назначают именно низкий уровень. Так меньше влияние помех на сигнал.

Признаки flagRising и flagFalling формируются на перепады (фронты) сигнала.

  • flagRising - изменение уровня сигнала с низкого на высокий;
  • flagFalling - изменение уровня сигнала с высокого на низкий.

Это признаки с памятью. Они выставляются в активное состояние в методах обработки сигнала. Сбрасываться признаки должны в программе при отработке события.

if(button.flagFalling != 0) {
    // кнопку нажимали 
    button.flagFalling = 0; // сброс признака

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

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

Узнать состояние сигнала можно и через методы.

uint8_t readFlagLow(void); // чтение признака СИГНАЛ В НИЗКОМ УРОВНЕ
uint8_t readFlagRising(void); // чтение признака БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
uint8_t readFlagFalling(void); // чтение признака БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ

Методы для чтения признаков flagRising и flagFalling сбрасывают соответствующие признаки, если они были активными.

if(button.readFlagFalling() != 0) {
    // кнопку нажимали

Теперь надо разработать тела методов. Не буду их приводить. Посмотрите в окончательном варианте проекта в конце урока.

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

//----------------- методы класса Debounce

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

 

Проверка.

Все класс готов. Проверяем его на той же схеме с кнопкой и светодиодом.

Схема макета

Макет

Объявим экземпляр нашего класса ниже его определения.

Debounce button(GPIOB, 1 << 12, 10); // экземпляр класса Debounce

Мы создали объект button. Задали вывод PB12 и установили 10 подтверждений.

Известным нам способом конфигурируем вывод PB13 на выход. К нему подключен светодиод.

/* USER CODE BEGIN SysInit */

__HAL_RCC_GPIOB_CLK_ENABLE(); // разрешение порта B

GPIO_InitTypeDef GPIO_InitStruct = {0};

/* конфигурация вывода PB13 на активный выход */
GPIO_InitStruct.Pin = GPIO_PIN_13; // номер вывода
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // режим выход
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM; // средняя скорость выхода
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

Конфигурацию вывода кнопки PB12 мы не устанавливаем. Это должен сделать класс Debounce.

В основном цикле мы вызываем метод обработки scanStability() и задаем задержку 1 мс.

while (1)
{

    button.scanStability();
    HAL_Delay(1);
}

Это не совсем нормальное использование класса. Мы создавали его для работы в фоновом режиме, а сами вызываем в основном цикле с зависанием в  HAL_Delay(). Но для проверки подойдет. Главное, что метод обработки вызывается регулярно, в нашем случае с периодом 1 мс. Считайте пока, что эти строки мы не видим. Они вызываются где-то в другом месте.

Теперь мы можем использовать признаки состояния кнопки.

Сделаем функциональный аналог предыдущих программ: кнопка нажата – светодиод светится, отжата – погашен.

/* USER CODE BEGIN WHILE */

while (1)
{
    if(button.readFlagLow() != 0) {
        // кнопка нажата
        
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_RESET); // сброс вывода PB13
    }
    else {
        // кнопка отжата
        HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13, GPIO_PIN_SET); // установка вывода PB13
}

Можно компилировать и загружать программу в плату. У меня работает.

 

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

При создании объекта button мы задали 10 подтверждений. С учетом того, что период вызова метода  scanStability() составляет 1 мс, время подтверждения равно 10 мс. Задержку с таким временем глазом заметить невозможно. Давайте увеличим его до 1 сек.

Debounce button(GPIOB, 1 << 12, 1000); // экземпляр класса Debounce

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

Попробуем тоже самое для алгоритма фильтрации сигнала по среднему значению. Изменим метод обработки.

button.scanAverage();
HAL_Delay(1);

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

Проверим, как класс выделяет фронты сигналов.

Debounce button(GPIOB, 1 << 12, 10); // экземпляр класса Debounce

Инверсия светодиода по отрицательному фронту, т.е. на нажатие кнопки.

while (1)
{
    if(button.readFlagFalling() != 0) {
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);
    }

    button.scanStability();
    HAL_Delay(1);
}

Инверсия светодиода по положительному фронту, т.е. на отжатие кнопки.

if(button.readFlagRising() != 0) {
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);
}

Я проверил тоже самое для метода  scanAverage() и для других выводов платы.

Полный проект урока можно загрузить по ссылке:

 

Зарегистрируйтесь и оплатитеВсего 60 руб. в месяц за доступ ко всем ресурсам сайта!
 

Ну все. Классом можно пользоваться.

 

В следующем  уроке научимся создавать библиотеки для STM32. Оформим класс Debounce библиотекой. И научимся подключать пользовательские библиотеки к проекту.

 

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

 

1

Автор публикации

не в сети 2 недели

Эдуард

280
Комментарии: 1936Публикации: 197Регистрация: 13-12-2015

44 комментария на «Урок 13. Разработка и использование классов в C++. Создание класса обработки дискретных сигналов Debounce.»

  1. Круто было бы научиться задействовать библиотеки mbed в проектах STM32, там их куча. И где то на просторах интернета я как то встречал такую статейку, но для меня это пока темный лес ((

    0
  2. Спасибо за уроки, описано все мне понятным языком. Вопрос по уроку. Как я понял, при нажатии на кнопку в программе с 1000 подтверждений, светодиод загорится через 1 секунду. При этом контроллер не останавливается, и всю эту секунду выполняет другие задачи, правильно? А если 1 цикл выполнения других задач длится скажем 200 мили секунд, то ждать зажигания светодиода придётся уже дольше? Или все равно задержка будет 1 сёк?

    0
    • Здравствуйте!
      Да, светодиод загорится через секунду. Такое время фильтрации вы задали. Это время не зависит от того, что контроллер выполняет в основном цикле. Единственное условие — в программе не должны запрещаться прерывания дольше чем на 1 мс.

      0
  3. как вы смогли использовать классы, в каком компиляторе? ни кеил ни iar не хотят их воспринимать вообще, в System Workbench есть даже шаблон класса, но работать с ним он отказался.
    может есть какой нибудь секрет, которого я не знаю?

    0
  4. Привет.Мне кажется это накладно.Как по мне проще вставить задержку скажешь на 200 миллисекунд сразу после нажатия кнопки.

    0
    • Здравствуйте!
      Представьте себе ряд импульсов помех. Два импульса попадут на 2 выборки через 200 мс и будет ложное срабатывание.

      0
  5. Добрый вечер!
    У меня ругается на блок подтяжки кнопки
    *****************************************
    ../Src/main.c(127): error: #65: expected a «;»
    _GPIOx->CRL |= 0b1000 <CRL &= ~ 0b0111 <CRH |= 0b1000 <CRH &= ~ 0b0111 << ((i — 8) * 4);
    *****************************************************************

    пришлось переписать на HAL, так работает

    0
    • Здравствуйте, только начинаю разбираться с контроллером. Подскажите, пожалуйста, как переписать на HAL?

      0
  6. Здравствуйте, Эдуард!
    Вы пишете: «Мы создавали его для работы в фоновом режиме, а сами вызываем в основном цикле с зависанием в HAL_Delay(). Но для проверки подойдет.»
    То есть для рабочей программы нужно заводить через обработку прерывания?

    0
  7. ~/STM32/STM32Cube/19_05_2020_urok13_1_Button/Debug/../Src/main.cpp:79: undefined reference to `Debounce::Debounce(GPIO_TypeDef*, unsigned short, unsigned long)’
    Ругается, и что хочь, то и делай
    77 /* Private user code ———————————————————*/
    78 /* USER CODE BEGIN 0 */
    79 Debounce button(GPIOB, 1 << 12, 10);
    80 /* USER CODE END 0 */

    1
    • Здравствуйте!
      Попробуйте запустите мой проект.
      Последовательно выполните все действия, которые я описываю в уроке.

      0
      • Здравствуйте, проблема та же, копировал Ваш проект, ошибка та же, что в cubeIDE, что с установленным ПО Ваших версий.

        Description Resource Path Location Type
        undefined reference to `Debounce::Debounce(GPIO_TypeDef*, unsigned short, unsigned long)’

        0
  8. Добрый день, Эдуард

    Загружаю ваши примеры в новый CubeIDE, но комментарии в кириллице отображаются символами (не совпадают кодировки).
    Могли бы вы дополнить ваши файлы прямым файлом с кодом?

    0
  9. Доброе утро Эдуард. Спасибо за урок. Всё работает. Для меня осталось несколько не ясных моментов в коде. Например: зачем номер порта кнопки задавать маской? И как компилятор это переваривает ведь мы объявили беззнаковую целую переменную?

    0
  10. Хуже урока по классам трудно придумать. Нет синтаксиса, все в кучу, а начиналось все хорошо… Коммерция погубила проект…

    0
  11. Странно, ругается на строчку объявления button
    undefined reference to `Debounce::Debounce(GPIO_TypeDef*, unsigned short, unsigned long)’ main.cpp /lesson13/Src line 77 C/C++ Problem/

    Я так понимаю, это при добавлении любого конструктора в класс. Настройки C/C++ вроде одинаковые.

    1
  12. Та же проблема…
    undefined reference to `Debounce::Debounce(GPIO_TypeDef*, unsigned short, unsigned long)’

    Но при этом я:
    1) Использую CUBE IDE, а не TrueSTUDIO, так как прочел, что она уже не поддерживается. Вполне логично, что лучше заморочиться с Кубиком.

    2) Файлы переименовал на .cpp, конвертацию в С++ сделал проекта. На всякий случай добавил даже
    org.eclipse.cdt.core.ccnature
    Как в уроке 11. А вот Build Settings там не, это все немного по-другому. Но я подозреваю, что все автоматом могло сделаться во время конвертации.

    Такой вот еще момент, когда я создаю объект с заданными свойствами, например:
    Debounce button(GPIOB, 1 << 12, 10);
    Ошибка есть.
    Если же я просто создам этот объект
    Debounce button();
    Ошибки нет.

    Очень странно… CMIS не признает после конвертации или ХЗ…

    0
    • Это какое-то проклятье. Чтобы не начинать с CubeMX и миграцией, может, проблема в ней, начал создавать новый проект в CubeIDE, выбрав C++, сделал конфигурацию, переименовал файлы. Та же ошибка…

      1
    • Отпишусь о преблеме, так как она возникла у многих. Дошли как-раз руки к уроку…

      В общем, часть кода просто есть в проекте (ссылка для скачивания), который подробно не разбирается в статье. Файл то я скачал, а вот хотелось все принципиально сделать самому хоть как-то, а не просто запустив проект 🙂
      Не получилось…

      0
  13. Вплоть до этого урока было более-менее понятно. Не счастливое число 13… Пойду поищу альтернативный ресурс с разъяснениями. Как-то быстро автор начал опускать очевидные для него, но не очевидные новичку вещи.

    0
    • Здравствуйте!
      Я подробно описывал создание классов в уроках Ардуино. Может вам посмотреть там. Если не ошибаюсь, урок 7.

      0
  14. Здравствуйте. А где собственно код функций? Я не понимаю как сделать такой фильтр имея лишь названия функций. А из за санкций России против Беларуси узнать как скачать файлы урока я не могу.

    0
  15. Интересно, что не могу открыть файл ioc в новой версии куба. не то что бы это было сильно критично, настраиваю все равно под STM32F401CCU6 адаптируя каждый урок под плату blackpill. Вопросов больше к самой ST, собрал проект все работает и понятно.

    0
    • Все версии CubeMX, старше версии 6.3 имеют этот глюк.
      Они даже свои файлы не открывают.
      Писал об этом в ST, пока не ответа не привета.

      1
      • Учитывая, что без впн с их сайта я не могу скачать программное обеспечение (пишет нет доступных ссылок) им видимо не до нас совсем.

        0
  16. Уважаемый автор. У меня возник вопрос по поводу кода. Поскольку у меня другой мк у меня нет регистра RCC и нет макроса формата RCC_APB2ENR_IOPхEN. Я поступил несколько по «тупому» и скопировал кусок кода инициализации из MX_GPIO_init() подставив его в конструктор класса и у меня получилось так (после данных изменений все работает) :
    Debounce::Debounce(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, uint32_t filterTime) {

    _GPIOx = GPIOx;
    _GPIO_Pin = GPIO_Pin;
    _filterTime = filterTime;
    _filterTimeCount = (_filterTime/2);
    // тут были переменные с присвоением 0 но я их инициализировал в шаблоне класса при объявлении
    // разрешение тактирования порта (вот тут у меня основной вопрос нормально ли так будет, ваша изначальная конструкция не заработала
    if( _GPIOx == GPIOA ) __HAL_RCC_GPIOA_CLK_ENABLE(); // разрешение порта A;
    else if( _GPIOx == GPIOB ) __HAL_RCC_GPIOB_CLK_ENABLE(); // разрешение порта B
    else if( _GPIOx == GPIOC ) __HAL_RCC_GPIOC_CLK_ENABLE(); // разрешение порта C
    // портов Е и Н (используется полностью под кварц) у меня нет в данном контроллере

    // тут был код для включения подтяжки у порта но того регистра у меня нет поэтому я сделал так
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = _GPIO_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(_GPIOx, &GPIO_InitStruct);
    }
    Заранее спасибо

    0
  17. Здравствуйте, подскажите пожалуйста, если я все правильно понял, то при методе scanAverage() и переключении с помощью togglePin() при многократном нажатии на кнопку переключение должно происходить, задержку ставил как 100, так и 1000, а этого не происходит.
    Этого также не происходит при других конфигурациях, т.е. scanAverage() и set-reset, scanStability() и togglePin, и, scanStability() и set-reset.

    0
    • Здравствуйте!
      При нажатии кнопки становятся активными свойства-признаки
      uint8_t flagRising; // признак БЫЛ ПОЛОЖИТЕЛЬНЫЙ ФРОНТ
      uint8_t flagFalling; // признак БЫЛ ОТРИЦАТЕЛЬНЫЙ ФРОНТ
      Программа должна проверять их, обрабатывать событие и сбрасывать соответствующий признак. В библиотеке признаки только устанавливаются.

      0
  18. Для тех у кого отличается версия МК, в данном случае F4, а именно F446RE, необходимо изменить:

    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    и т.д.
    на
    RCC->AHB1ENR |= RCC_AHB1LPENR_GPIOALPEN;
    RCC->AHB1ENR |= RCC_AHB1LPENR_GPIOBLPEN;
    и т.д.

    if(i CRL |= 0b1000 <CRL &= ~ (0b0111 <CRH |= 0b1000 <CRH &= ~ (0b0111 <MODER |= 0b00 <MODER &= ~(0b11 <PUPDR |= 0b01 <PUPDR &= ~(0b10 << (i * 2));

    и конфигурацию пинов, в моем случае GPIOC P13 — кнопка, GPIOA P5 — диод

    0
    • Здравствуйте!
      Уже точно не помню, но скорее, чтобы начальное значение счетчика было в середине диапазона. В этом случае до переключения состояния требуется какое-то число выборок.

      0

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

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

Нажимая кнопку "Отправить" Вы даёте свое согласие на обработку введенной персональной информации в соответствии с Федеральным Законом №152-ФЗ от 27.07.2006 "О персональных данных".