diff --git a/c/keypad/README.md b/c/keypad/README.md new file mode 100644 index 0000000..9c0b829 --- /dev/null +++ b/c/keypad/README.md @@ -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` один в один. diff --git a/c/keypad/keypad.c b/c/keypad/keypad.c new file mode 100644 index 0000000..2647694 --- /dev/null +++ b/c/keypad/keypad.c @@ -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]; +} diff --git a/c/keypad/keypad.h b/c/keypad/keypad.h new file mode 100644 index 0000000..f6c6794 --- /dev/null +++ b/c/keypad/keypad.h @@ -0,0 +1,145 @@ +/** + * @file keypad.h + * @brief Портируемый опрос шести кнопок навигации с антидребезгом. + * + * Библиотека не знает ни о портах, ни о таймерах: уровни кнопок она получает + * через обратный вызов порта, а время — параметром Keypad_Poll(). События + * складываются в короткую очередь, поэтому быстрое нажатие не теряется, даже + * если приложение перерисовывает экран. + * + * Кнопок ровно шесть: четыре направления, ввод и возврат. Такой набор + * покрывает навигацию по меню без матрицы и сдвиговых регистров. + */ + +#ifndef KEYPAD_H +#define KEYPAD_H + +#include + +/** @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 */