Top.Mail.Ru

Библиотека Button для обработки нажатий кнопок на STM32. KY-004.

Мы тут обсуждали вроде бы простейший вопрос, как обрабатывать нажатия кнопок наиболее универсально и удобно. В результате мне прислали библиотеку для этого, которой я и поделюсь в статье с разрешения автора (я правда изменил оформление библиотеки под привычный мне стиль). Можно это даже приурочить к моему «курсу» по датчикам, так как один из модулей этого набора состоит из тактовой кнопки. Фото я прикрепил сверху, это модуль KY-004, но это не важно, речь пойдет о любых тактовых кнопках на примере использования их с STM32.

Тактовая кнопка выглядит так:

Тактовая кнопка

Не путаем с переключателем:

Переключатель

Переключатель фиксирован либо в одном, либо в другом положении. Тактовая кнопка фиксирована в не нажатом положении, чтобы ее удерживать в нажатом положении надо прилагать усилия. Но это все и так знают )

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

  • отслеживание короткого нажатия кнопки
  • длительного
  • еще более длительного

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

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

У меня есть:

  • Отладочная плата с STM32F401CCU6.
  • Кнопка «Up», то есть вверх, допустим для перехода по пунктам меню. Подключена к выводу PA2.
  • Кнопка «Down», то есть вниз. Подключена к PA3.

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

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

  • Короткое нажатие – меньше 500 мс.
  • Длинное нажатие – от 500 мс – до 3 секунд.
  • Продолжительное нажатие – удержание кнопки более 3-х секунд.

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

Я использую STM32CubeIDE, поэтому в STM32CubeMx настраиваю два вывода STM32 на вход и любой из таймеров (нужен для библиотеки Button). И еще один выход (PC13) для тестового светодиода. У меня кнопки подтянуты электрически к питанию, поэтому внутренняя подтяжка GPIO мне не нужна:

Обработка нажатий кнопок на STM32.

Таймер настроен на генерацию прерывания каждую миллисекунду:

Настройки Timer в STM32CubeMx.

Настройка прерываний в STM32CubeMx.

Тактирование у меня настроено так:

Тактирование STM32.

То есть на таймер приходит частота 84 МГц. При предделителе 8399 получаю частоту:

F = 84 МГц / (8399 + 1) = 10 КГц

То есть один отсчет таймера  – 100 мкс. Ставлю период (Counter Period), равный 10, получаю прерывание каждые - 100 мкс * 10 = 1 мс. Инициализация на этом закончена. Генерирую проект и перехожу к настройке библиотеки для обработки нажатий кнопок.

Конфигурация библиотеки Button для обработки кнопок.

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

  1. Добавляем файлы библиотеки Buttonв проект, эту операцию подробно думаю не надо описывать (если что спрашивайте в комментариях, я всем помогу):

Проект для обработки нажатий кнопок.

  1. В файле button.h задается перечисление ButtonID с «названиями» кнопок. У меня это кнопка «Up» и «Down»:
typedef enum {
    BUTTON_UP,
    BUTTON_DOWN,
    BUTTONS_NUM,
} ButtonID;

Если бы было три кнопки, то мог бы быть вот такой вариант:

typedef enum {
    BUTTON_MY_1,
    BUTTON_MY_2,
    BUTTON_MY_3,
    BUTTONS_NUM,
} ButtonID;

Думаю, суть вы поняли ) В файле button.h также задаем числовые значения для длительностей нажатия кнопок. У меня длинное нажатие начинается с 500 мс, продолжительное нажатие – с 3000 мс:

#define BUTTONS_LONG_PRESS_MS                                500
#define BUTTONS_VERY_LONG_PRESS_MS                           3000

Все настраивается максимально просто и понятно. Кнопка может быть подключена к STM32 по-разному – при нажатии замыкать на землю, либо на +3.3 В. У меня замыкает на землю. Это значит, что в не нажатом состоянии с кнопки приходит высокий уровень, поэтому я делаю так:

#define GPIO_BUTTON_NOT_PRESSED                              (GPIO_PIN_SET)

Если бы замыкала на питание, то есть в не нажатом состоянии – низкий уровень, то было бы:

#define GPIO_BUTTON_NOT_PRESSED                              (GPIO_PIN_RESET)

Тоже ничего сложного.

  1. Последний шаг. В файле button.c задаем, куда подключены кнопки:
static McuPin buttons[BUTTONS_NUM] = {{GPIOA, GPIO_PIN_2},
                                      {GPIOA, GPIO_PIN_3}};

Размер массива равен BUTTONS_NUM. То есть я добавил две кнопки:

typedef enum {
    BUTTON_UP,
    BUTTON_DOWN,
    BUTTONS_NUM,
} ButtonID;

И задал два порта ввода-вывода. На этом конфигурация закончена. Подытожим, полная конфигурация заключается в настройке:

  • button.h, GPIO_BUTTON_NOT_PRESSED
  • button.h, enum ButtonID
  • button.h, BUTTONS_LONG_PRESS_MS, BUTTONS_VERY_LONG_PRESS_MS
  • button.c, static McuPin buttons[BUTTONS_NUM]

Использование библиотеки Button на STM32.

Обработку нажатий кнопок тоже разбиваю на понятные шаги. Все в main.c:

  1. Каждую миллисекунду будем вызывать функцию BUTTON_TimerProcess() из библиотеки Button:
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == htim10.Instance)
  {
    BUTTON_TimerProcess();
  }
}

/* USER CODE END 4 */

Можно настроить прерывания и на больший период, тогда значения:

#define BUTTONS_LONG_PRESS_MS                                500
#define BUTTONS_VERY_LONG_PRESS_MS                           3000

Нужно будет задать другими. Если, например, период таймера – 100 мс, то все эти значения надо разделить на 100. У меня будет 1 мс, и значения такие, как я поставил (тоже в миллисекундах).

  1. Запускаем таймер в функции main(),  почему-то постоянно об этом забываю…
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim10);

/* USER CODE END 2 */
  1. Работу библиотеки и обработку кнопок обеспечивают три функции:
  • BUTTON_Process() – здесь крутятся внутренние процессы.
  • BUTTON_GetAction() – эта функция возвращает состояние кнопки.
  • BUTTON_ResetActions() – эта функция сбрасывает состояния кнопок после их анализа.

В итоге получается такой код:

while (1)
{
    BUTTON_Process();

    // Work with buttons
    // Button "Up"
    if (BUTTON_GetAction(BUTTON_UP) == BUTTON_SHORT_PRESS)
    {
      // LED on
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
    }

    // Button "Down"
    if (BUTTON_GetAction(BUTTON_DOWN) == BUTTON_SHORT_PRESS)
    {
      // LED off
      HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
    }

    BUTTON_ResetActions();
}

Здесь у меня по короткому нажатию кнопки Up зажигается светодиод на PC13, по короткому нажатию кнопки Down – светодиод гаснет. Все очень просто, минимум действий – и полноценная работа кнопок в кармане. Если нужно отловить длительные нажатия кнопки Up, то:

if (BUTTON_GetAction(BUTTON_UP) == BUTTON_LONG_PRESS)
{
  // Do something
}

if (BUTTON_GetAction(BUTTON_UP) == BUTTON_VERY_LONG_PRESS)
{
  // Do something
}

Все, больше никаких действий не требуется. Антидребезг, расчет длительности нажатия, переход между состояниями – все внутри библиотеки Button. Я теперь ее пихаю абсолютно во все проекты, чаще всего для работы с меню на каком-нибудь дисплее. Подключаю 4 кнопки, настраиваю так:

typedef enum {
    BUTTON_UP,
    BUTTON_DOWN,
    BUTTON_LEFT,
    BUTTON_RIGHT,
    BUTTONS_NUM,
} ButtonID;

И в main() элементарно отлавливаю любые действия пользователя.

Готовый проект – KY004_Button.

Разбор кода библиотеки.

А теперь рассмотрим, что в библиотеке под капотом, для начала переменные:

  • debounceCounter – счетчик для исключения дребезга.
  • waitButtonRelease - переменная-флаг, показывающая, что кнопка нажата и мы ждем ее отпускания.
  • В то время, пока кнопка нажата, счетчик buttonPressCounter отсчитывает время нажатия.
  • buttonActions – состояния каждой из кнопок, которые анализируем потом в нашем коде.
  • buttonState – а это низкоуровневые состояния кнопок.

Низкоуровневые состояния могут быть такие:

typedef enum
{
  BUTTON_STARTING                      = 0,
  BUTTON_NOT_PRESSED                   = 1,
  BUTTON_WAIT_DEBOUNCE                 = 2,
  BUTTON_PRESSED                       = 3,
} ButtonState;

Первое из них – состояние по умолчанию, оно позволяет отслеживать ошибки, если кнопка подключена неправильно, к примеру. BUTTON_NOT_PRESSED – кнопка не нажата, BUTTON_WAIT_DEBOUNCE – обработка антидребезга, BUTTON_PRESSED – кнопка нажата.

Высокоуровневые состояния:

typedef enum
{
  BUTTON_NONE                          = 0,
  BUTTON_SHORT_PRESS                   = 1,
  BUTTON_LONG_PRESS                    = 2,
  BUTTON_VERY_LONG_PRESS               = 3,
} ButtonAction;

BUTTON_SHORT_PRESS – короткое нажатие, BUTTON_LONG_PRESS – длинное нажатие, BUTTON_VERY_LONG_PRESS – продолжительное (еще более долгое) нажатие. При желании можно добавить и еще таких состояний, для других временных интервалов, но это не так часто требуется.

Обработка кнопок заключается в вызове функции BUTTON_Process(), внутри которой операции подразделяются на низкоуровневые и высокоуровневые: BUTTON_LowLevelManager() и BUTTON_HighLevelManager():

void BUTTON_Process()
{
  BUTTON_LowLevelManager();
  BUTTON_HighLevelManager();
}

Посмотрим код первой из функций и проанализируем ее построчно:

void BUTTON_LowLevelManager()
{
  uint8_t currentStates[BUTTONS_NUM];
  
  for (uint8_t i = 0; i < BUTTONS_NUM; i++)
  {
    currentStates[i] = HAL_GPIO_ReadPin(buttons[i].port, buttons[i].pin);
    
    switch (buttonState[i])
    {
      case BUTTON_STARTING:
        if (currentStates[i] == GPIO_BUTTON_NOT_PRESSED)
        {
          buttonState[i] = BUTTON_NOT_PRESSED;
        } 
        break;
      
      case BUTTON_NOT_PRESSED:
        if (currentStates[i] == GPIO_BUTTON_PRESSED)
        {
          buttonState[i] = BUTTON_WAIT_DEBOUNCE;
          debounceCounter[i] = 0;
        } 
        break;
        
      case BUTTON_WAIT_DEBOUNCE:
        if (debounceCounter[i] == DEBOUNCE_TIME_MS)
        {
          if (currentStates[i] == GPIO_BUTTON_PRESSED)
          {
            buttonState[i] = BUTTON_PRESSED;
          }
          else
          {
            buttonState[i] = BUTTON_NOT_PRESSED;
          }
        }
        break;
        
      case BUTTON_PRESSED:
        if (currentStates[i] == GPIO_BUTTON_NOT_PRESSED)
        {
          buttonState[i] = BUTTON_WAIT_DEBOUNCE;
          debounceCounter[i] = 0;
        } 
        break;
        
      default:
        break;
    }
  }
}

Процессы идут для каждой из кнопок в цикле for (uint8_t i = 0; i < BUTTONS_NUM; i++). Сначала считывается состояние:

currentStates[i] = HAL_GPIO_ReadPin(buttons[i].port, buttons[i].pin);

Далее в switch() проверяем текущее низкоуровневое состояние. Если - BUTTON_STARTING, то проверяем, что кнопка не нажата и переходим в другое состояние. Если кнопка подключена неправильно, то есть замыкает на +3.3 В вместо земли (например), то кнопка зависнет в этом состоянии.

Когда кнопка не нажата, проверяем ее новое состояние (if (currentStates[i] == GPIO_BUTTON_PRESSED)). Если она стала нажатой, то переходим в состояние антидребезговой обработки:

if (currentStates[i] == GPIO_BUTTON_PRESSED)
{
  buttonState[i] = BUTTON_WAIT_DEBOUNCE;
  debounceCounter[i] = 0;
}

Антидребезг по стандартному алгоритму – ожидаем DEBOUNCE_TIME_MS и, если кнопка осталась в нажатом состоянии, то переводим ее в BUTTON_PRESSED. Иначе переходим в BUTTON_NOT_PRESSED.

Если кнопка сейчас нажата (BUTTON_PRESSED), то проверяем ее новое состояние ((currentStates[i] == GPIO_BUTTON_NOT_PRESSED)). И если оно изменилось и стало GPIO_BUTTON_NOT_PRESSED, то точно так же переходим к фильтрации дребезга контактов. Надеюсь, что я нормально описываю… Если что, пишите в комментарии, подправлю.

Осталась функция BUTTON_HighLevelManager(), то есть высокоуровневые процессы:

void BUTTON_HighLevelManager()
{
  for (uint8_t i = 0; i < BUTTONS_NUM; i++)
  {
    if (buttonActions[i] == BUTTON_NONE)
    {
      if (waitButtonRelease[i] == 0)
      {
        if (buttonState[i] == BUTTON_PRESSED)
        {
          waitButtonRelease[i] = 1;
        }
      }
      else
      {
        if (buttonState[i] == BUTTON_NOT_PRESSED)
        {
          waitButtonRelease[i] = 0;

          if (buttonPressCounter[i] >= BUTTONS_VERY_LONG_PRESS_MS)
          {
            buttonActions[i] = BUTTON_VERY_LONG_PRESS;
          }
          else
          {
            if (buttonPressCounter[i] >= BUTTONS_LONG_PRESS_MS)
            {
              buttonActions[i] = BUTTON_LONG_PRESS;
            }
            else
            {
              buttonActions[i] = BUTTON_SHORT_PRESS;
            }
          }
        }
      }
    }
  }
}

Здесь проще, если кнопка не нажата:

if (buttonActions[i] == BUTTON_NONE)

Но при этом ее низкоуровневое состояние соответствует нажатой кнопке (if (buttonState[i] == BUTTON_PRESSED)), то устанавливаем флаг waitButtonRelease[i] в единицу. Здесь кстати так же вся работа в цикле по всем кнопкам. При следующем заходе в эту функцию, видим, что флаг в единице и ждем отпускания кнопки:

if (buttonState[i] == BUTTON_NOT_PRESSED)
{
  waitButtonRelease[i] = 0;

  if (buttonPressCounter[i] >= BUTTONS_VERY_LONG_PRESS_MS)
  {
    buttonActions[i] = BUTTON_VERY_LONG_PRESS;
  }
  else
  {
    if (buttonPressCounter[i] >= BUTTONS_LONG_PRESS_MS)
    {
      buttonActions[i] = BUTTON_LONG_PRESS;
    }
    else
    {
      buttonActions[i] = BUTTON_SHORT_PRESS;
    }
  }
}

При этом идет инкрементирование счетчика buttonPressCounter[i] (об этом чуть ниже). Когда кнопка будет отпущена (if (buttonState[i] == BUTTON_NOT_PRESSED)), по этому значению сделаем вывод о текущем нажатии.

Следующая функция библиотеки – BUTTON_TimerProcess() – та, вызов которой мы добавили в callback по переполнению таймера:

void BUTTON_TimerProcess()
{
  for (uint8_t i = 0; i < BUTTONS_NUM; i++)
  {
    if (debounceCounter[i] < DEBOUNCE_TIME_MS)
    {
      debounceCounter[i]++;
    }

    if (waitButtonRelease[i] == 1)
    {
      buttonPressCounter[i]++;
    }
    else
    {
      buttonPressCounter[i] = 0;
    }
  }
}

Здесь просто инкрементируются  использованные счетчики: счетчик для антидребезга и счетчик для нажатия. На этом я заканчиваю статью, мне эта библиотека очень подошла для обработки кнопок, может кому-то и не подойдет, как говорится «на вкус и цвет» )

Отдельная ссылка на библиотеку: Button.

Update 1.

Спасибо Kir за дельные замечания, обновил код и ссылки, а также добавил функцию инициализации void BUTTON_Init(), которая задает начальные значения всех переменных. Как полностью справдливо замечено, слепо доверять компилятору мы не имеем никакого морального права! Вызываем эту функцию в main():

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_TIM10_Init();
/* USER CODE BEGIN 2 */
BUTTON_Init();
HAL_TIM_Base_Start_IT(&htim10);

/* USER CODE END 2 */

Еще раз спасибо Kir, здравая критика и дельные замечания!

Подписаться
Уведомить о
guest

56 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
Kir
Kir
2 лет назад

Ну как бы так себе, если кратко. Во-первых, как мне видится фрагмент кода в void BUTTON_LowLevelManager() :

case BUTTON_PRESSED:
if (currentStates[i] == GPIO_BUTTON_NOT_PRESSED)
{
buttonState[i] = BUTTON_WAIT_DEBOUNCE;
currentStates[i] = 0;
}

должен выглядеть как:

case BUTTON_PRESSED:
if (currentStates[i] == GPIO_BUTTON_NOT_PRESSED)
{
buttonState[i] = BUTTON_WAIT_DEBOUNCE;
debounceCounter[i] = 0;
}
Во-вторых, нет никакой защиты от помех и наводок. Да, в большинстве случаев оно будет работать. Но если при работе в ветке case BUTTON_WAIT_DEBOUNCE мы получим разовую помеху, то физическое состояние кнопки будет сброшено/установлено не верно. В каком-нибудь критически управляемом обьекте это может сыграть злую шутку... Ну и в-третьих, иногда от кнопки надо иметь гораздо больше состояний, например накликивание... Скажем по трем коротким кликам переходить в главное меню, по двум - на уровень выше и тп.

Опять же, нигде не делается инициализация переменных, используемых программой. Хорошо коли умный компилятор везде нули рассует, а если нет? Ну и код совсем не оптимизирован, например в моем исполнении функция void BUTTON_TimerProcess() выглядила бы так:

void BUTTON_TimerProcess()
{
 uint8_t i;
 for(i = 0; i < BUTTONS_NUM; i++)
 {
   if(debounceCounter[i] < DEBOUNCE_TIME_MS) debounceCounter[i]++;
   if(waitButtonRelease[i]) buttonPressCounter[i]++;
   else buttonPressCounter[i]=0;
 }
}

KIR
KIR
Ответ на комментарий  TQFP
2 лет назад

А нас в далеком 88 году так учили )) Но дело даже не в оформлении кода. Просто глупо сравнивать с нулем, который и есть False или 1, которая и есть True. Ну а в статье ошибка так и осталась... Надеюсь, вложение то хоть поправили? А то там ошибка с сбросом счетчика debonce тоже есть... И она приводит к тому что антидребезг только для нажатия срабатывает, а для отпускания нет

KIR
KIR
Ответ на комментарий  KIR
2 лет назад

И проверка на равенство buttonPressCounter в конце вашей таймерной процедуры только лишние такты жрет... Сброс там гарантированный и он меньше тактов на асме займет чем проверка и сброс, ну и памяти меньше... Нас учили вписывать не вписуемое ибо память в 88-ом экономили )) Я бы понял на pc где, но у нас то микропроцессоры...

KIR
KIR
Ответ на комментарий  KIR
2 лет назад

Что касается вопроса проверки на помехи, то он есть. Реализуется достаточно просто, может как-нибудь будет время создам темку

Aveal
Администратор
Ответ на комментарий  Kir
2 лет назад

Четко, толково, дельные замечания, не так часто такое встречается.

Klin4
Klin4
2 лет назад

Добрый день, а зажатие кнопки с помощью этой библиотеки не реализовать? (например, однократное нажатие увеличивает число в счетчике на 1, а зажатие кнопки позволяет ускорить инкрементирование)

Отличная статья!

Aveal
Администратор
Ответ на комментарий  Klin4
2 лет назад

Привет! Да, без проблем можно организовать, я вот такой вариант накидал:

  • В button.c добавляем функцию:
/******************************************************************************/
ButtonState BUTTON_GetState(uint8_t index)
{
  return buttonState[index];
}



/******************************************************************************/
  • Ее же в button.h:
/* Functions -----------------------------------------------------------------*/

extern void BUTTON_Process();
extern void BUTTON_TimerProcess();
extern ButtonAction BUTTON_GetAction(uint8_t index);
extern void BUTTON_ResetActions();
extern void BUTTON_Init();

extern ButtonState BUTTON_GetState(uint8_t index);
  • Далее в main.c, переменные:
/* USER CODE BEGIN PV */
uint32_t targetCounter = 0;

uint32_t incrementSpeed = 1;
uint32_t incrementPeriod = 50;
uint32_t incrementSpeedPeriod = 250;

uint32_t previousMs = 0;
uint8_t speedUpdated = 0;

/* USER CODE END PV */
  • И в main():
while (1)
{
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    BUTTON_Process();

    uint32_t currentMs = HAL_GetTick();

    ButtonState currentState = BUTTON_GetState(BUTTON_UP);
    if (currentState == BUTTON_PRESSED)
    {
      if (((currentMs % incrementPeriod) == 0) &&
          (previousMs != currentMs))
      {
        targetCounter += incrementSpeed;
        previousMs = currentMs;
        speedUpdated = 0;
      }

      if (((currentMs % incrementSpeedPeriod) == 0) &&
          (speedUpdated == 0))
      {
        incrementSpeed++;
        speedUpdated = 1;
      }
    }
    else
    {
      targetCounter = 0;
      incrementSpeed = 1;
    }

    BUTTON_ResetActions();

}
/* USER CODE END 3 */

В итоге получаем, что при удержании кнопки каждые 50 мс (incrementPeriod = 50) происходит увеличение счетчика targetCounter. И каждые 250 мс (incrementSpeedPeriod = 250) увеличивается скорость инкрементирования.

Klin4
Klin4
Ответ на комментарий  Aveal
2 лет назад

Большое спасибо за участие и оперативность! Все заработало!

Aveal
Администратор
Ответ на комментарий  Klin4
2 лет назад

Отлично!

Алексей
Алексей
1 год назад

Здравствуйте! Подскажите, пожалуйста, я новичок в программировании микроконтроллеров да и в целом. Пытаюсь разобрать в вашем коде, практически до всего дошел, но вот одного не могу понять. Что означает BUTTONS_NUM в перечислении ButtonID:
typedef enum
{
BUTTON_UP,
BUTTON_DOWN,
BUTTONS_NUM,
} ButtonID;

Просто потом от этого значения присваиваются размеры массивов:
static McuPin buttons[BUTTONS_NUM] = {{GPIOA, GPIO_PIN_2}, {GPIOA, GPIO_PIN_3}};

uint8_t currentStates[BUTTONS_NUM] и т.д.

и какое значение в цикле for принимает переменная BUTTONS_NUM?

void BUTTON_LowLevelManager()
{
uint8_t currentStates[BUTTONS_NUM];
for (uint8_t i = 0; i < BUTTONS_NUM; i++)
{
currentStates[i] = HAL_GPIO_ReadPin(buttons[i].port, buttons[i].pin);
switch (buttonState[i])
{
case BUTTON_STARTING:
if (currentStates[i] == GPIO_BUTTON_NOT_PRESSED)
{
buttonState[i] = BUTTON_NOT_PRESSED;
}
break;
case BUTTON_NOT_PRESSED:
if (currentStates[i] == GPIO_BUTTON_PRESSED)
{
buttonState[i] = BUTTON_WAIT_DEBOUNCE; debounceCounter[i] = 0;
}
break;

case BUTTON_WAIT_DEBOUNCE:
if (debounceCounter[i] == DEBOUNCE_TIME_MS)
{
if (currentStates[i] == GPIO_BUTTON_PRESSED)
{
buttonState[i] = BUTTON_PRESSED;
}
else
{
buttonState[i] = BUTTON_NOT_PRESSED;
}
}
break;
case BUTTON_PRESSED:
if (currentStates[i] == GPIO_BUTTON_NOT_PRESSED)
{
buttonState[i] = BUTTON_WAIT_DEBOUNCE;
debounceCounter[i] = 0;
}
break;
default:
break;
}
}
}

Заранее спасибо за ответ и помощь!

Aveal
Администратор
Ответ на комментарий  Алексей
1 год назад

Доброго дня) Просто для удобства, в цикле, например, по всем кнопкам. Если 2 кнопки:

typedef enum
{
  BUTTON_UP,
  BUTTON_DOWN,
  BUTTONS_NUM,
} ButtonID;

То BUTTONS_NUM = 2. Если добавляем еще одну кнопку

typedef enum
{
  BUTTON_UP,
  BUTTON_DOWN,
  BUTTON_NEW_ADDED,
  BUTTONS_NUM,
} ButtonID;

То BUTTONS_NUM = 3 за счет того, что по порядку перечисления в конце идет.

Алекс
Алекс
Ответ на комментарий  Aveal
1 год назад

Я тоже споткнулся об BTTONS_NUM.
Зачем нужен понятно, а вот где он дефайнится непонятно.
Я так поимаю в main.h руками прописывается количество кнопок?

Aveal
Администратор
Ответ на комментарий  Алекс
1 год назад

Не, он как член enum. Если у нас:

typedef enum
{
  BUTTON_UP,
  BUTTON_DOWN,
  BUTTONS_NUM,
} ButtonID;

то значения будут такие:

BUTTON_UP = 0;
BUTTON_DOWN = 1;
BUTTONS_NUM = 2;

Поэтому важно просто BUTTONS_NUM оставлять последним в этом перечислении, а его значение будет по сути автоматом соответствовать количеству кнопок.

Алекс
Алекс
Ответ на комментарий  Aveal
1 год назад

А как оно автоматом вычисляется? оО
Это что-то в стандарте языка Си есть, что само суммирует количество вышестоящих элементов?
Не припомню такого...

Aveal
Администратор
Ответ на комментарий  Алекс
1 год назад

Не суммирует. Если значения не заданы явно, то для первого элемента - 0, для последующих инкрементируется на 1.

Алекс
Алекс
Ответ на комментарий  Aveal
1 год назад

Спасибо, вчера закрыл браузер и вспомнил тип ENUM, как они устроены 🙂

Aveal
Администратор
Ответ на комментарий  Алекс
1 год назад

=)

Сергей
Сергей
1 год назад

Здравствуйте я в программирование контроллеров только начинаю разбираться, мне в целом код понятен, но как работает вот эта строчка кода
"currentStates[i] = HAL_GPIO_ReadPin(buttons[i].port, buttons[i].pin);" Как я понял тут вызывается функция HAL_GPIO_ReadPin, у нее в параметрах как я понял есть название порта через приведение типов (GPIO_TypeDef * GPIOx), вторым параметром идет номер пина (uint16_t GPIO_Pin),, а третьим его состояние GPIO_PinState PinState, причем как я понял последний параметр не обязателен для указания. Так вот, как buttons[i].port у вас в коде ссотносится с GPIO_TypeDef * GPIOx и buttons[i].pin с (uint16_t GPIO_Pin) я просто не очень понимаю как эти все приведения типов работают. Еще у вас в файле button.h есть структура
typedef struct
{
 GPIO_TypeDef *port;
 uint16_t pin;
}McuPin; я думаю, что все завязано на ней, но я не понимаю как она работает и откуда взялось buttons? Расскажите пожалуйста ою этом

Ant
Ant
1 год назад

Добрый день!
Подскажите, пожалуйста, а как реализовать функцию выхода из STANDBYMODE при длительном нажатии на кнопку, игнорируя короткие нажатия.
Как я понял, при выходе из STANDBY код не видит нарастания фронта и ничего не происходит, т.к. контроллер уже проснулся при нарастании на WKUP пине

Эдуард
Эдуард
1 год назад

Здравствуйте!

Спасибо за статью!

Скажите пожалуйста, можно ли скорректировать Ваш код так, чтобы действия по нажатию на кнопки выполнялись не по заднему фронту (отпусканию), а по переднему?

Спасибо!

Aveal
Администратор
Ответ на комментарий  Эдуард
1 год назад

Добрый день!

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

Эдуард
Эдуард
Ответ на комментарий  Aveal
1 год назад

Необходимо отслеживание только короткого и длительного нажатия (отслеживание более длительного нажатия не нужно).
Как переделать код так, чтобы при длительном нажатии реакция на нажатие была не при отпускании кнопки, а в тот момент когда она еще удерживается?

Спасибо!

Aveal
Администратор
Ответ на комментарий  Эдуард
1 год назад

Можно в BUTTON_HighLevelManager() логику изменить, я по-быстрому накидал, не проверял:

void BUTTON_HighLevelManager()
{
  for (uint8_t i = 0; i < BUTTONS_NUM; i++)
  {
    if (buttonActions[i] == BUTTON_NONE)
    {
      if (waitButtonRelease[i] == 0)
      {
        if (buttonState[i] == BUTTON_PRESSED)
        {
          waitButtonRelease[i] = 1;
        }
      }
      else
      {
        if (buttonPressCounter[i] >= BUTTONS_LONG_PRESS_MS)
        {
          buttonActions[i] = BUTTON_LONG_PRESS;
        }
        else
        {
          buttonActions[i] = BUTTON_SHORT_PRESS;
        }
        
        if (buttonState[i] == BUTTON_NOT_PRESSED)
        {
          waitButtonRelease[i] = 0;
        }
      }
    }
  }
}
Эдуард
Эдуард
Ответ на комментарий  Aveal
1 год назад

Здравствуйте!

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

Спасибо!

Aveal
Администратор
Ответ на комментарий  Эдуард
1 год назад
void BUTTON_HighLevelManager()
{
  for (uint8_t i = 0; i < BUTTONS_NUM; i++)
  {
    if (buttonActions[i] == BUTTON_NONE)
    {
      if (waitButtonRelease[i] == 0)
      {
        if (buttonState[i] == BUTTON_PRESSED)
        {
          waitButtonRelease[i] = 1;
        }
      }
      else
      {
        if (buttonPressCounter[i] >= BUTTONS_LONG_PRESS_MS)
        {
          buttonActions[i] = BUTTON_LONG_PRESS;
        }

        if (buttonState[i] == BUTTON_NOT_PRESSED)
        {
          waitButtonRelease[i] = 0;

          if (buttonPressCounter[i] < BUTTONS_LONG_PRESS_MS)
          {
            buttonActions[i] = BUTTON_SHORT_PRESS;
          }
        }
      }
    }
  }
}

В состояние BUTTON_SHORT_PRESS переходит стандартно, в состояние BUTTON_LONG_PRESS переходит при удержании. В состояние BUTTON_NONE переходит после отпускания.

Дальше надо смотреть по задаче - в этом варианте состояние BUTTON_LONG_PRESS будет сохраняться до финального отпускания кнопки.

Эдуард
Эдуард
Ответ на комментарий  Aveal
1 год назад

Уважаемый Aveal, большое спасибо за оказываемую помощь!

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

Спасибо!

Aveal
Администратор
Ответ на комментарий  Эдуард
1 год назад

Рад, что продвигается ) Самый банальный и простой вариант, добавить флаги для кнопок, в button.c:

uint8_t buttonLongPressHandled[BUTTONS_NUM];

В функции инициализации добавить в цикле:

buttonLongPressHandled[i] = 0;

В BUTTON_HighLevelManager():

void BUTTON_HighLevelManager()
{
  for (uint8_t i = 0; i < BUTTONS_NUM; i++)
  {
    if (buttonActions[i] == BUTTON_NONE)
    {
      if (waitButtonRelease[i] == 0)
      {
        if (buttonState[i] == BUTTON_PRESSED)
        {
          waitButtonRelease[i] = 1;
        }
      }
      else
      {
        if (buttonLongPressHandled[i] == 0)
        {
            if (buttonPressCounter[i] >= BUTTONS_LONG_PRESS_MS)
            {
              buttonActions[i] = BUTTON_LONG_PRESS;
            }
        }

        if (buttonState[i] == BUTTON_NOT_PRESSED)
        {
          waitButtonRelease[i] = 0;

          if (buttonPressCounter[i] < BUTTONS_LONG_PRESS_MS)
          {
            buttonActions[i] = BUTTON_SHORT_PRESS;
          }
        }
      }
    }
  }
}

В одна BUTTON_TimerProcess() строка добавляется:

if (waitButtonRelease[i] == 1)
{
  buttonPressCounter[i]++;
}
else
{
  buttonPressCounter[i] = 0;
  buttonLongPressHandled[i] = 0;
}

И в main.c:

extern uint8_t buttonLongPressHandled[BUTTONS_NUM];
if (BUTTON_GetAction(BUTTON_UP) == BUTTON_LONG_PRESS)
{
    // Do something
    buttonLongPressHandled[BUTTON_UP] = 1;
}
Эдуард
Эдуард
Ответ на комментарий  Aveal
1 год назад

Уважаемый Aveal, большое спасибо Вам за помощь!
Сделал все как Вы написали, вроде все работает, тестирую. Как я могу Вас отблагодарить?

Aveal
Администратор
Ответ на комментарий  Эдуард
1 год назад

Заходи на сайт, приглашай друзей/знакомых 🙂

Vlad
Vlad
11 месяцев назад

Приветствую!
Только начинаю осваивать, и очень многое непонятно.
Решил на основе вашего обработчика кнопки, сделать управление светодиодами. (просто экспериментирую для набора опыта).
Бьюсь несколько дней - не получается.
Хочу сделать подсчет коротких нажатий, и чтобы при однократном нажатии постоянно мигал первый светодиод, при двукратном - второй, ну и т.д. Делаю счетчик нажатий.

int counter;
counter=0;
 /* Infinite loop */
 /* USER CODE BEGIN WHILE */
 while (1)
 {
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
 BUTTON_Process();

   // Work with buttons
   // Button "Up"
   if (BUTTON_GetAction(BUTTON_UP) == BUTTON_SHORT_PRESS)
   {counter=counter+1;}
if (counter==1)
                  {HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
                   HAL_Delay(330);
                 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
                 HAL_Delay(330);}
if (counter==2)
                          {HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
                           HAL_Delay(330);
                         HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
                         HAL_Delay(330);}
.........
if (counter>=5)
                   {counter=0;}
При коротком нажатии кнопки, 0-й светодиод начинает моргать. Но следующее нажатие на кнопку, не переключает на моргание 1-й светодиод и т.д.
Что я делаю не так, не могу понять. Сразу прошу прощения за глупые вопросы. Только начинаю позновать.

Aveal
Администратор
Ответ на комментарий  Vlad
11 месяцев назад

Привет, а можешь полный проект скинуть, я посмотрю.

Vlad
Vlad
Ответ на комментарий  Vlad
11 месяцев назад

Приветствую. спасибо за ответ.
Сбросил на почту aveal@microtechnics.ru

Aveal
Администратор
Ответ на комментарий  Vlad
11 месяцев назад

Вроде нормально все, под отладчиком смотрел что происходит?

Vlad
Vlad
Ответ на комментарий  Aveal
11 месяцев назад

У меня, видимо, левая STM-ка, ни Кейл, ни CubeIde, не хотят проводить отладку. Ругаются, что STM не настоящая.

Aveal
Администратор
Ответ на комментарий  Vlad
11 месяцев назад

Это усложняет конечно. Остается отладка как в старые времена - светодиодом ) Оставляем минимум:

while (1)
{
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    BUTTON_Process();

    if (BUTTON_GetAction(BUTTON_UP) == BUTTON_SHORT_PRESS)
    {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
        HAL_Delay(1000);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
        HAL_Delay(1000);
    }
    
    BUTTON_ResetActions();
}

На каждое короткое нажатие должен мигать диод - если не будет, то значит проблема уже на этом этапе.

Vlad
Vlad
Ответ на комментарий  Aveal
11 месяцев назад

Приветствую.
Одиночные действия отрабатываются. Светик отмаргивает.
Но если вставляю счетчик, а потом происходит непрерывное действие, то все перестает работать. ((

Aveal
Администратор
Ответ на комментарий  Vlad
11 месяцев назад

А так работает?

counter=0;

while (1)
{
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    BUTTON_Process();

    if (BUTTON_GetAction(BUTTON_UP) == BUTTON_SHORT_PRESS)
    {
        counter++;
        
        if (counter >= 3)
        {
            counter = 0;
        }
    }
    
    if (counter == 0)
    {
        // Nothing
    }

    if (counter == 1)
    {
        // Nothing
    }
    
    if (counter == 2)
    {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
        HAL_Delay(1000);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
        HAL_Delay(1000);
    }
    
    BUTTON_ResetActions();
}
Vlad
Vlad
Ответ на комментарий  Aveal
11 месяцев назад

При первом нажатии, не происходит ничего, как и прописано.
При втором нажатии, светик начинает непрерывно моргать. Третье нажатие, по идее, должно обнулять счетчик и светодиод должен погаснуть. Но он продолжает моргать. И на кнопку больше не реагирует.
Я, на всякий случай, увеличил счетчик до 4 и добавил условие, когда счетчик равен 4, чтобы начинал моргать следующий диод. Но этого не происходит. Все так же моргает первый диод. На кнопку нет реакции.

 if (counter == 3)
    {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_RESET);
        HAL_Delay(1000);
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET);
        HAL_Delay(1000);
    }
Vlad
Vlad
Ответ на комментарий  Vlad
11 месяцев назад

Я, на всякий случай, взял F411-ю STM. Но с ней, то же самое. И к сожалению она у меня тоже левая. Не позволяет проводить отладку. Вы не пробовали запустить эту конструкцию у себя. Может такое поведение из-за левых чипов?

Aveal
Администратор
Ответ на комментарий  Vlad
11 месяцев назад

А, блин, понятно все. Короче HAL_Delay(много_миллисекунд) нарушает работу драйвера кнопок, там же антидребезг итд по таймеру считается, а в BUTTON_Process() заходит раз в 1-2 секунды. Набросал вариант:

Код

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define DELAY_PERIOD_MS                                                330


/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim4;

/* USER CODE BEGIN PV */
int counter = 0;
uint16_t actualLedPin = GPIO_PIN_0;
uint32_t delayCounter = 0;

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM4_Init(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

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

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM4_Init();
  /* USER CODE BEGIN 2 */
  BUTTON_Init();
  HAL_TIM_Base_Start_IT(&htim4);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
      BUTTON_Process();

      // Work with buttons
      // Button "Up"
      if (BUTTON_GetAction(BUTTON_UP) == BUTTON_SHORT_PRESS)
      {
          counter++;

          if (counter >= 2)
          {
              counter = 0;
          }

          switch (counter)
          {
              case 0:
                  actualLedPin = GPIO_PIN_0;
                  break;

              case 1:
                  actualLedPin = GPIO_PIN_1;
                  break;

              default:
                  break;
          }

          // All LEDs switching off should be there
      }

      delayCounter++;

      if (delayCounter >= DELAY_PERIOD_MS)
      {
          delayCounter = 0;
          HAL_GPIO_TogglePin(GPIOA, actualLedPin);
      }

      HAL_Delay(1);
      BUTTON_ResetActions();
  }
  /* USER CODE END 3 */
}

Vlad
Vlad
Ответ на комментарий  Aveal
11 месяцев назад

Заработало!. Спасибо большое. Я правильно понял, если я опять в цикле как-нибудь "угадаю " с задержками, опять может перестать работать?
Наверное стоит поискать, как реализовать кнопку по прерыванию.

Aveal
Администратор
Ответ на комментарий  Vlad
11 месяцев назад

Ну таких задержек как в HAL_Delay() лучше в целом избегать в большинстве случаев.

А в этом конкретном случае можно попробовать BUTTON_Process() вызывать из прерывания по какому-нибудь таймеру, либо если RTOS использовать, то вынести в отдельный таск.

KUIB
KUIB
5 месяцев назад

Работает , но по двум нажатиям.И я не вижу в коде прерывания на нажатие и отжатие. Может че-то не понял.И убрал прерывания в void TIM3_IRQHandler(void) _ it.c файл

AII
AII
5 месяцев назад

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

Aveal
Администратор
Ответ на комментарий  AII
5 месяцев назад

Нет проблем, обращайся )

Эдуард
Эдуард
4 месяцев назад

Aveal, приветствую!

Скажите пожалуйста, как правильно адаптировать функции библиотеке для работы с FreeRTOS? Какие функции нужно вынести в отдельные задачи? В случае использования FreeRTOS можно ли обойтись без аппаратного таймера 1 мс для вызова функции ButtonTimerProcess()?

Спасибо!

Aveal
Администратор
Ответ на комментарий  Эдуард
4 месяцев назад

Доброго дня!

Можно спокойно BUTTON_Process() вынести в отдельный таск. BUTTON_TimerProcess() в целом тоже, только счетчики buttonPressCounter / debounceCounter не инкрементировать, а добавлять к ним прошедшее между вызовами количество миллисекунд. Навскидку никаких препятствий не вижу.

56
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x