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 пине

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

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

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

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

Спасибо!

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

Добрый день!

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

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

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

Спасибо!

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

Можно в 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
10 месяцев назад

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

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

Спасибо!

Aveal
Администратор
Ответ на комментарий  Эдуард
10 месяцев назад
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
10 месяцев назад

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

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

Спасибо!

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

Рад, что продвигается ) Самый банальный и простой вариант, добавить флаги для кнопок, в 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
10 месяцев назад

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

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

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

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

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

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
9 месяцев назад

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

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

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

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

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

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

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

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

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

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
9 месяцев назад

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

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

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

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
9 месяцев назад

При первом нажатии, не происходит ничего, как и прописано.
При втором нажатии, светик начинает непрерывно моргать. Третье нажатие, по идее, должно обнулять счетчик и светодиод должен погаснуть. Но он продолжает моргать. И на кнопку больше не реагирует.
Я, на всякий случай, увеличил счетчик до 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
9 месяцев назад

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

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

А, блин, понятно все. Короче 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
9 месяцев назад

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

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

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

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

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

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

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

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

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

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

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

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

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

Спасибо!

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

Доброго дня!

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

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