feat(keypad): опрос шести кнопок с антидребезгом и автоповтором

Перенесён из KONOR_ds18b20/lib/keypad; в OpticalTester лежала такая же копия.

О портах и таймерах библиотека не знает: уровни приходят обратным вызовом
read(), время — параметром Keypad_Poll(). События складываются в очередь
на восемь штук, поэтому быстрое нажатие не теряется, пока приложение
перерисовывает экран.
This commit is contained in:
2026-08-23 01:15:11 +03:00
parent c7798c02db
commit 54f0ba7b0e
3 changed files with 377 additions and 0 deletions

59
c/keypad/README.md Normal file
View File

@@ -0,0 +1,59 @@
# keypad
Опрос шести кнопок навигации с антидребезгом, автоповтором и удержанием.
Библиотека не знает ни о портах, ни о таймерах: уровни кнопок приходят через
обратный вызов порта, время — параметром `Keypad_Poll()`. События складываются
в короткую очередь, поэтому быстрое нажатие не теряется, пока приложение
перерисовывает экран.
```
порт платы keypad.c приложение
read(key) ──────► антидребезг, автоповтор ──► Keypad_GetEvent()
удержание, очередь на 8
```
## Состав
| Файл | Что делает | Зависимости |
|---|---|---|
| `keypad.h`, `keypad.c` | шесть кнопок, антидребезг, автоповтор, длинное нажатие, очередь событий | `stdint.h` |
Кнопок ровно шесть: четыре направления, ввод и возврат — набор покрывает
навигацию по меню без матрицы и сдвиговых регистров.
## Что нужно от платформы
Один вызов и монотонное время в миллисекундах:
```c
uint8_t read(void *ctx, uint8_t key); /* 1 — кнопка нажата */
```
## Быстрый старт
```c
Keypad keypad;
Keypad_Config config = { .read = board_key_read, .context = &board };
/* нулевые поля заменяются умолчаниями: дребезг 20 мс, автоповтор 400/120 мс,
удержание 800 мс, автоповтор только для стрелок */
Keypad_Init(&keypad, &config);
for (;;) {
Keypad_Poll(&keypad, board_millis());
Keypad_Event event;
while (Keypad_GetEvent(&keypad, &event)) {
if (event.type == KEYPAD_EVENT_PRESS) { /* ... */ }
}
}
```
Опрашивать `Keypad_Poll()` достаточно раз в 1..5 мс — из основного цикла
или из системного тика.
## Проверено в проектах
`KONOR_ds18b20`, `OpticalTester`. Естественная пара — [`menu`](../menu):
`Keypad_Key` отображается в `Menu_Key` один в один.

173
c/keypad/keypad.c Normal file
View File

@@ -0,0 +1,173 @@
/**
* @file keypad.c
* @brief Антидребезг, автоповтор и очередь событий шести кнопок.
*
* Опрос программный: уровни читаются обратным вызовом порта, состояние
* подтверждается выдержкой debounce_ms, а автоповтор и удержание отсчитываются
* от момента подтверждённого нажатия. Прерывания не используются, поэтому
* библиотека одинаково работает с выводами МК, расширителями и матрицами.
*/
#include "keypad.h"
/** Значения по умолчанию для незаданных параметров. */
#define KEYPAD_DEFAULT_DEBOUNCE_MS 20U
#define KEYPAD_DEFAULT_REPEAT_DELAY_MS 400U
#define KEYPAD_DEFAULT_REPEAT_PERIOD_MS 120U
#define KEYPAD_DEFAULT_LONG_PRESS_MS 800U
/**
* @brief Кладёт событие в очередь, отбрасывая его при переполнении.
*
* @param keypad Состояние клавиатуры.
* @param key Кнопка.
* @param type Вид события.
*/
static void keypad_push(Keypad *keypad, uint8_t key, Keypad_EventType type)
{
uint8_t tail;
if (keypad->count >= (uint8_t)KEYPAD_QUEUE_SIZE) {
keypad->overflow = 1U;
return;
}
tail = (uint8_t)((keypad->head + keypad->count) % (uint8_t)KEYPAD_QUEUE_SIZE);
keypad->queue[tail].key = (Keypad_Key)key;
keypad->queue[tail].type = type;
keypad->count++;
}
uint8_t Keypad_Init(Keypad *keypad, const Keypad_Config *config)
{
uint8_t index;
if ((keypad == 0) || (config == 0) || (config->read == 0)) {
return 0U;
}
keypad->config = *config;
if (keypad->config.debounce_ms == 0U) {
keypad->config.debounce_ms = KEYPAD_DEFAULT_DEBOUNCE_MS;
}
if (keypad->config.repeat_delay_ms == 0U) {
keypad->config.repeat_delay_ms = KEYPAD_DEFAULT_REPEAT_DELAY_MS;
}
if (keypad->config.repeat_period_ms == 0U) {
keypad->config.repeat_period_ms = KEYPAD_DEFAULT_REPEAT_PERIOD_MS;
}
if (keypad->config.long_press_ms == 0U) {
keypad->config.long_press_ms = KEYPAD_DEFAULT_LONG_PRESS_MS;
}
if (keypad->config.repeat_mask == 0U) {
keypad->config.repeat_mask = KEYPAD_MASK_ARROWS;
}
for (index = 0U; index < (uint8_t)KEYPAD_KEY_COUNT; index++) {
keypad->keys[index].raw = 0U;
keypad->keys[index].stable = 0U;
keypad->keys[index].long_sent = 0U;
keypad->keys[index].changed_ms = 0U;
keypad->keys[index].pressed_ms = 0U;
keypad->keys[index].repeated_ms = 0U;
}
keypad->head = 0U;
keypad->count = 0U;
keypad->overflow = 0U;
return 1U;
}
void Keypad_Poll(Keypad *keypad, uint32_t now_ms)
{
uint8_t index;
if ((keypad == 0) || (keypad->config.read == 0)) {
return;
}
for (index = 0U; index < (uint8_t)KEYPAD_KEY_COUNT; index++) {
Keypad_KeyState *state = &keypad->keys[index];
const uint8_t level = (uint8_t)((keypad->config.read(keypad->config.context, index) != 0U)
? 1U : 0U);
if (level != state->raw) {
/* Уровень изменился: отсчёт выдержки начинается заново. */
state->raw = level;
state->changed_ms = now_ms;
continue;
}
if (level == state->stable) {
/* Подтверждённое нажатие: автоповтор и порог удержания. */
if (state->stable == 0U) {
continue;
}
if ((state->long_sent == 0U)
&& ((now_ms - state->pressed_ms) >= keypad->config.long_press_ms)) {
state->long_sent = 1U;
keypad_push(keypad, index, KEYPAD_EVENT_LONG);
}
if ((((keypad->config.repeat_mask >> index) & 1U) != 0U)
&& ((now_ms - state->pressed_ms) >= keypad->config.repeat_delay_ms)
&& ((now_ms - state->repeated_ms) >= keypad->config.repeat_period_ms)) {
state->repeated_ms = now_ms;
keypad_push(keypad, index, KEYPAD_EVENT_REPEAT);
}
continue;
}
if ((now_ms - state->changed_ms) < keypad->config.debounce_ms) {
continue;
}
state->stable = level;
if (level != 0U) {
state->pressed_ms = now_ms;
state->repeated_ms = now_ms;
state->long_sent = 0U;
keypad_push(keypad, index, KEYPAD_EVENT_PRESS);
} else {
keypad_push(keypad, index, KEYPAD_EVENT_RELEASE);
}
}
}
uint8_t Keypad_GetEvent(Keypad *keypad, Keypad_Event *event)
{
if ((keypad == 0) || (event == 0) || (keypad->count == 0U)) {
return 0U;
}
*event = keypad->queue[keypad->head];
keypad->head = (uint8_t)((keypad->head + 1U) % (uint8_t)KEYPAD_QUEUE_SIZE);
keypad->count--;
return 1U;
}
uint8_t Keypad_IsDown(const Keypad *keypad, Keypad_Key key)
{
if ((keypad == 0) || (key >= KEYPAD_KEY_COUNT)) {
return 0U;
}
return keypad->keys[key].stable;
}
uint8_t Keypad_Overflow(Keypad *keypad)
{
uint8_t flag;
if (keypad == 0) {
return 0U;
}
flag = keypad->overflow;
keypad->overflow = 0U;
return flag;
}
const char *Keypad_KeyName(Keypad_Key key)
{
static const char *const names[KEYPAD_KEY_COUNT] = {
"UP", "DOWN", "LEFT", "RIGHT", "ENTER", "BACK"
};
if (key >= KEYPAD_KEY_COUNT) {
return "?";
}
return names[key];
}

145
c/keypad/keypad.h Normal file
View File

@@ -0,0 +1,145 @@
/**
* @file keypad.h
* @brief Портируемый опрос шести кнопок навигации с антидребезгом.
*
* Библиотека не знает ни о портах, ни о таймерах: уровни кнопок она получает
* через обратный вызов порта, а время — параметром Keypad_Poll(). События
* складываются в короткую очередь, поэтому быстрое нажатие не теряется, даже
* если приложение перерисовывает экран.
*
* Кнопок ровно шесть: четыре направления, ввод и возврат. Такой набор
* покрывает навигацию по меню без матрицы и сдвиговых регистров.
*/
#ifndef KEYPAD_H
#define KEYPAD_H
#include <stdint.h>
/** @brief Кнопки клавиатуры; порядок задаёт индексы для обратного вызова. */
typedef enum {
KEYPAD_KEY_UP = 0, /**< Вверх: предыдущий пункт меню. */
KEYPAD_KEY_DOWN, /**< Вниз: следующий пункт меню. */
KEYPAD_KEY_LEFT, /**< Влево: уменьшение значения. */
KEYPAD_KEY_RIGHT, /**< Вправо: увеличение значения. */
KEYPAD_KEY_ENTER, /**< Ввод: вход в пункт или подтверждение. */
KEYPAD_KEY_BACK, /**< Назад: возврат на предыдущий экран. */
KEYPAD_KEY_COUNT /**< Число кнопок; не является кнопкой. */
} Keypad_Key;
/** @brief Вид события клавиатуры. */
typedef enum {
KEYPAD_EVENT_PRESS = 0, /**< Нажатие после подтверждения антидребезгом. */
KEYPAD_EVENT_REPEAT, /**< Автоповтор при удержании. */
KEYPAD_EVENT_LONG, /**< Удержание дольше порога, выдаётся один раз. */
KEYPAD_EVENT_RELEASE /**< Отпускание кнопки. */
} Keypad_EventType;
/** @brief Событие очереди клавиатуры. */
typedef struct {
Keypad_Key key; /**< Кнопка, вызвавшая событие. */
Keypad_EventType type; /**< Вид события. */
} Keypad_Event;
/** Глубина очереди событий; хватает на любой темп нажатий вручную. */
#define KEYPAD_QUEUE_SIZE 8U
/** Маска кнопок направлений для поля repeat_mask. */
#define KEYPAD_MASK_ARROWS ((uint8_t)((1U << KEYPAD_KEY_UP) | (1U << KEYPAD_KEY_DOWN) \
| (1U << KEYPAD_KEY_LEFT) | (1U << KEYPAD_KEY_RIGHT)))
/** Маска всех кнопок клавиатуры. */
#define KEYPAD_MASK_ALL ((uint8_t)((1U << KEYPAD_KEY_COUNT) - 1U))
/**
* @brief Параметры клавиатуры; нулевые поля заменяются значениями по умолчанию.
*/
typedef struct {
/** Читает состояние кнопки: 1 — нажата. Индекс — значение Keypad_Key. */
uint8_t (*read)(void *context, uint8_t key);
void *context; /**< Указатель платы для обратного вызова. */
uint16_t debounce_ms; /**< Время подтверждения уровня, по умолчанию 20. */
uint16_t repeat_delay_ms; /**< Пауза до автоповтора, по умолчанию 400. */
uint16_t repeat_period_ms;/**< Период автоповтора, по умолчанию 120. */
uint16_t long_press_ms; /**< Порог удержания, по умолчанию 800. */
uint8_t repeat_mask; /**< Кнопки с автоповтором, по умолчанию стрелки. */
} Keypad_Config;
/** @brief Состояние одной кнопки. */
typedef struct {
uint8_t raw; /**< Последний прочитанный уровень. */
uint8_t stable; /**< Подтверждённое состояние: 1 — нажата. */
uint8_t long_sent; /**< Признак выданного события удержания. */
uint32_t changed_ms; /**< Момент последнего изменения сырого уровня. */
uint32_t pressed_ms; /**< Момент подтверждённого нажатия. */
uint32_t repeated_ms; /**< Момент последнего события автоповтора. */
} Keypad_KeyState;
/** @brief Состояние клавиатуры; принадлежит вызывающему коду. */
typedef struct {
Keypad_Config config; /**< Копия параметров. */
Keypad_KeyState keys[KEYPAD_KEY_COUNT]; /**< Состояния кнопок. */
Keypad_Event queue[KEYPAD_QUEUE_SIZE]; /**< Кольцевая очередь событий. */
uint8_t head; /**< Индекс чтения очереди. */
uint8_t count; /**< Число событий в очереди. */
uint8_t overflow; /**< Признак потери событий. */
} Keypad;
/**
* @brief Готовит клавиатуру к работе и подставляет значения по умолчанию.
*
* Состояние всех кнопок считается отпущенным, поэтому зажатая при включении
* кнопка даёт обычное событие нажатия после выдержки антидребезга.
*
* @param keypad Состояние клавиатуры.
* @param config Параметры; обязателен только обратный вызов чтения.
* @return 1 при успешной настройке, 0 если обратный вызов не задан.
*/
uint8_t Keypad_Init(Keypad *keypad, const Keypad_Config *config);
/**
* @brief Опрашивает кнопки и наполняет очередь событий.
*
* Вызывается из главного цикла с любым темпом: шаг антидребезга задаётся не
* периодом вызова, а метками времени.
*
* @param keypad Состояние клавиатуры.
* @param now_ms Текущее монотонное время в миллисекундах.
*/
void Keypad_Poll(Keypad *keypad, uint32_t now_ms);
/**
* @brief Забирает следующее событие из очереди.
*
* @param keypad Состояние клавиатуры.
* @param event Приёмник события.
* @return 1, если событие получено, 0 если очередь пуста.
*/
uint8_t Keypad_GetEvent(Keypad *keypad, Keypad_Event *event);
/**
* @brief Сообщает подтверждённое состояние кнопки.
*
* @param keypad Состояние клавиатуры.
* @param key Опрашиваемая кнопка.
* @return 1, если кнопка нажата.
*/
uint8_t Keypad_IsDown(const Keypad *keypad, Keypad_Key key);
/**
* @brief Возвращает и сбрасывает признак переполнения очереди.
*
* @param keypad Состояние клавиатуры.
* @return 1, если события терялись с прошлого вызова.
*/
uint8_t Keypad_Overflow(Keypad *keypad);
/**
* @brief Даёт короткое имя кнопки для отладочных сообщений и экранов.
*
* @param key Кнопка.
* @return Строка вида "UP"; для неизвестного кода — "?".
*/
const char *Keypad_KeyName(Keypad_Key key);
#endif /* KEYPAD_H */