Урок 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() и для других выводов платы.

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

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

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

 

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

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

 

0

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

не в сети 13 часов

Эдуард

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

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

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

    0

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

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