Работа с кнопками stm32

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

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

Для начала - откроем CubeMX, и укажем что пин A5 является входом. Так же дадим ему имя - Button.
После чего - перегенерируем наш проект.

 

Теперь идём в VSCode, и открываем файл main.c.
Как видим - всё, что мы писали ранее - никуда не исчезло.
Уберём пока что наш алгоритм моргания светодиодами, и начнем разбираться как именно работать со входами МК.
Для этой задачи у нас есть функция HAL_GPIO_ReadPin. Которая имеет 2 аргумента. И возвращает значение GPIO_PinState.
Аргументы нам уже знакомы и понятны - это порт и пин.

Но как обычно - дьявол кроется в деталях.
Сейчас покажу в чем проблема.
Объявим глобальную переменную GPIO_PinState ButtonState;

и в бесконечном цикле будем опрашивать кнопку, и присваивать этой переменной полученное значение.
ButtonState = HAL_GPIO_ReadPin(Button_GPIO_Port, Button_Pin);

Компилируем, и запускаем на выполнение.

А тем временем - открываем CubeMonitor.
И уже привычными движениями выводим график этой переменной.

Теперь понажимаем кнопку, и посмотрим что мы видим на графике.
Внезапно, мы еще не дотронулись до кнопки, а она уже сама нажимается и отпускается!

Но как же так! Кнопка у нас соединяет пин микроконтроллера с плюсом источника питания.
Но если немножко задуматься, то да, соединяет, но когда она не нажата - то на ней находится неопределённый
потенциал. Что и даёт нам такое странное поведение.
Что бы решать эту проблему - нам необходимо использовать резистор, который когда кнопка не нажата будет
подтягивать её к минусу источника питания. Номинал этого резистора не сильно важен, по этому я у себя в
закромах нашёл резистор на 47 килоом.
Устанавливаем его, и смотрим как изменится поведение на графике.
Уже лучше. Наша кнопка перестала реагировать на случайные помехи, и теперь меняет своё состояние
только когда мы её нажимаем.
Но это очень неудобно - для каждой кнопки дополнительно на готовом устройстве припаивать резистор.
Для решения этой задачи, разработчики микроконтроллеров сделали нам так называемые подтягивающие резисторы.
Которые можно активировать программно.
Для того, что бы это сделать. Вернёмся в CubeMX, развернём подменю System Core, и выберем пункт GPIO.
В открывшейся таблице находим наш вывод PA5, кликаем по нему, и в появившемся окошке Configuration видим
пункт GPIO Pull-up/Pull-down. Так как нам необходимо активировать резистор, который будет подтягивать к
минусу источника питания - выбираем Pull-down.
Затем опять перегенерируем проект.
Возвращаемся в VSCode, и ничего не меняя, сразу прошиваем микроконтроллер.
Теперь идём в CubeMonitor, обновляем список переменных, и запускаем сбор данных.
Ну и последний шаг - убираем наш резистор, установленный ранее.
Убеждаемся что кнопка работает корректно.
Отлично, с резисторами подтяжки - разобрались, на первый взгляд теперь
всё красиво. Но так же не бывает. мы же слышали про такое страшное словосочетание
как "Дребезг контактов". По этому подключим логический анализатор, и посмотрим с его помощью
что творится на входе.

Устанавливаем программу анализатора, и запускаем её.
Заускаем сбор данных. Понажимаем на кнопку, и.... вот оно! Отчетливо видно, что перед тем, как уровень
сигнала перешёл от нуля к единице, есть очень быстрые "иголки". На самом деле микроконтроллер их тоже
видит, это программа CubeMonitor имеет период обновления очень большой, для того, что бы отобразить быстрые скачки.

С этой проблемой - так же можно бороться при помощи внешних компонентов - например добавить конденсатор
небольшой ёмкости, что бы получить RC фильтр. Резистор то у нас уже есть.

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

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

Ну а раз нет физического конденсатора - значит сделаем программный.

предлагаю следующую логику:
создаём переменную.
Мысленно устанавливаем для неё границы - от нуля, до десяти.
В бесконечном цикле опрашиваем кнопку, и если кнопка нажата - то увеличиваем значение переменной, пока не
доберёмся до максимального - в нашем случае это 10
Если же кнопка отпущена - уменьшаем значение, стремясь к нулю.
что бы не изменить значение этой переменной очень быстро, опрос состояния делаем не чаще чем 1 раз в 10 миллисекунд.
Первое изменение значения переменной - мы делаем очень большим - например 5, что бы имитировать гестерезис.
по достижении нуля - устанавливаем флаг что кнопка отпущена.
по достижении десяти - что кнопка нажата.

Учитывая что получился довольно сложный алгоритм, вынесем его в отдельный файл. Создадим файл DS_Button.c,
и комплектом к нему - DS_Button.h.

В файле DS_Button.h - сразу опишем типовой шаблон заголовочных файлов.
Сохраняем созданные файлы, и сразу получаем проблему. VSCode ругается что языковые режимы не совместимы,
что бы это не значило.

Существует как минимум 2 пути победить эту проблему - но мы воспользуемся самым простым -
откроем файл CMakeLists.txt, находящийся в каталоге cmake, и по образу - допишем путь к
нашему новому файлу с исходными кодами. Сохраняем файл, в этот момент запустилась пересборка конфигурации
и проблем больше не стало.

Возвращаемся к реализации описанного алгоритма борьбы с дребезгом.

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

Для того, что бы можно было использовать типы значений из HAL, добавим инклюд "stm32f0xx_hal.h".
VSCode начинает ругаться что не может найти этот файл, хотя мы его взяди прямо из другого, рабочего файла.
Для исправления - сразу включим наш заголовочный файл в main.c

Вернёмся к нашей структуре.
В первую очередь - нам надо сохранить к какому порту и пину подключена кнопка.
так же - мы точно знаем что нам надо будут флаги нажатия и отпускания.
Для работы с булевыми переменными - добавим еще один заголовочный файл. И добавим флаги.
так же, мы проговаривали что надо делать опрос кнопки не чаще чем 1 раз в 10 милисекунд.
Значит нужна переменная для хранения времени предыдущего опроса.

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

Приступаем к реализации функций.
Сначала - Инициализация.
И сразу - недоработка в заголовочном файле. Нам для инициализации надо знать какой объект инициализировать.
Исправляем.
Инициализизируем все поля структуры.

Далее очередь функции обновления.
в первой же строке задачи - создание переменной. по этому добавляем переменную в структуру.
Не забываем её инициализировать. Инициализируем значением из середины диапазона.

Опрашиваем кнопку, и если нажата - то увеличиваем хранилище, в противном случае - уменьшаем.
Добавляем ограничители.

Добавляем проверку того, что эта функция вызывается не слишком часто.
для определения времени - воспользуемся функцией HAL_GetTick().
она возвращает количество миллисекунд, прошедшее с момента старта микроконтроллера.

Усложняем дальше. Имитируем гестерезис.
добавляем условие - если кнопка нажата и хранилище равно нулю - делаем плюс пять.
И добавляем ветку "Иначе"
То же самое делаем с отпущеным состоянием.

Осталось только добавить установку флагов.
Вынесем это в отдельный Switch

Отлично, основной алгоритм описали.

Доделаем 2 оставшиеся функции, которые возвращают значения вызывающей программе.

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

Теперь точно всё.

Можно вернуться в основную функцию main, и запрограммировать сегодняшнюю задачу.

Сначала - объявим переменную, для хранения структуры кнопки.
затем - инициализируем нашу кнопку вызвав DS_ButtonInit()
После чего в основном цикле - будем вызывать DS_ButtonUpdate().

и остаётся добавить пару простых проверок.
Если кнопка нажата - включаем красный светодиод. Иначе - выключаем его.

Если кнопка нажата долго - инвертируем состояния синих.
Для инвертирования воспользуемся функцией HAL_GPIO_TogglePin. Она инвертирует состояние вывода. Что нам и необходимо.

Вот и всё, проверяем.

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

Ещё раз прошиваем, и приверяем.

В результате проверки - выяснилось что короткие нажатия отрабатывают корректно.
Длинные же - нет.

Исправление этой ошибки я уже сделаю без объяснения голосом.
Если у кого то есть желание понять что именно меняется - просто внимательно посмотрите, тут всё просто.

А на этом всё. Спасибо за внимание.
На следующее занятие надо будет выбрать что нибудь попроще, а то кнопка - очень сложная штука.