Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103, STM32F407 · уровень средний · 18 мин чтения
Открыть в интерактивном справочнике

22. Внешняя обвязка и индикация

Выводов всегда не хватает. У BluePill их 37, и половина занята интерфейсами — а нужно ещё восемь светодиодов, четыре разряда индикатора и клавиатура. Этот раздел о том, как обойтись малым числом ног: сдвиговые регистры на выход и на вход, динамическая индикация и клавиатура в одном прерывании таймера — с моделью ложных нажатий и расчётом токов.

Как это называется в документации

Оригинал Перевод
shift register сдвиговый регистр
serial-in parallel-out последовательный вход, параллельный выход
latch / storage register защёлка, выходной регистр
output enable разрешение выхода
daisy chain цепочка последовательно соединённых микросхем
multiplexing динамическая индикация, мультиплексирование
common anode / cathode общий анод / катод
addressable LED адресный светодиод
parallel load (PL) параллельная загрузка входов
ghosting ложное нажатие в матрице
debouncing подавление дребезга

22.1 Расширение выходов: 74HC595

Микросхема принимает биты последовательно и выставляет их на восемь параллельных выходов. Три вывода микроконтроллера превращаются в восемь, а при соединении в цепочку — в шестнадцать, двадцать четыре и далее.

Вывод Назначение
SER данные
SRCLK такт сдвига
RCLK защёлка: перенос в выходной регистр
OE разрешение выходов (активный низкий)
SRCLR сброс регистра сдвига

Ключевая деталь — раздельная защёлка. Пока вы задвигаете биты, выходы не меняются: они хранят предыдущее состояние. Импульс на RCLK выставляет всё новое одновременно. Без этого при выводе на индикатор были бы видны промежуточные состояния — мерцание и «призраки».

Подключать удобнее всего через SPI (раздел 18): данные и такт сдвига — это в точности MOSI и SCK, а защёлку дёргают обычным GPIO.

void hc595_write(const uint8_t *data, uint8_t count)
{
    LATCH_LOW();
    /* Последняя микросхема в цепочке получает данные первой. */
    for (uint8_t i = count; i > 0u; i--) {
        spi1_xfer(data[i - 1u]);
    }
    while (SPI1->SR & SPI_SR_BSY) { }   /* последний бит должен дойти до регистра */
    LATCH_HIGH();          /* по фронту всё выводится наружу одновременно */
}

Обратный порядок байтов — следствие устройства цепочки: биты проходят насквозь, и первый отправленный байт вытесняется в самую дальнюю микросхему. Ожидание BSY перед защёлкой обязательно: флаг приёма поднимается раньше, чем последний бит дошёл до регистра, и без ожидания защёлкивается сдвинутое не до конца (раздел 18.11).


22.2 Расширение входов: 74HC165

Зеркальная задача. Микросхема защёлкивает состояние восьми входов и отдаёт их последовательно.

Вывод Назначение
PL загрузка: захватить состояние входов
CP такт сдвига
Q7 последовательный выход
CE разрешение такта, активный ноль: притянуть к земле, иначе сдвига нет
DS вход цепочки: сюда приходит Q7 следующей, дальней микросхемы; у последней — на землю

Порядок работы: короткий импульс PL фиксирует все входы одновременно, затем восемь тактов вычитывают их. Одновременность важна — иначе состояние кнопок «размажется» по времени опроса.

Через SPI это чтение без передачи: PL — обычный выход, CP — SCK, Q7 — MISO:

PL_LOW();              /* захватить все входы разом */
PL_HIGH();
data[i] = spi1_xfer(0xFFu);      /* первой приходит ближняя к микроконтроллеру */

Порядок обратный 74HC595: первым приходит байт той микросхемы, чей Q7 подключён к микроконтроллеру.

⚠️ Q7 не умеет отключаться — третьего состояния у него нет, и на общей шине SPI он всё время держит MISO, портя ответы других устройств. Тогда Q7 ставят на MISO через буфер с разрешением (74HC125) и открывают его только на время чтения — или отдают регистру отдельный SPI.

Так один набор из трёх выводов обслуживает любое число кнопок, а дребезг обрабатывается программно ровно так же, как для обычной кнопки (раздел 8).


22.3 Семисегментный индикатор и динамическая индикация

Четырёхразрядный индикатор — это 28 сегментов плюс точки. Подключать каждый отдельно бессмысленно; вместо этого разряды зажигают по очереди.

Логика: выставили сегменты первого разряда, включили первый разряд, погасили, выставили сегменты второго, включили второй — и так по кругу. Если цикл повторяется достаточно быстро, глаз видит все разряды одновременно.

Частота имеет значение. Меньше 50 Гц на разряд — заметное мерцание, особенно боковым зрением и на видеозаписи. Для четырёх разрядов это значит переключение не реже 200 раз в секунду; обычно берут с запасом — 1 кГц.

Обновление удобно повесить на таймер (раздел 13): прерывание переключает разряд, основная программа только меняет отображаемое число. Таймер на 1 кГц даёт 250 Гц на разряд — впятеро выше порога мерцания.

Сегменты цифр для индикатора с общим катодом, бит 0 — сегмент a, бит 6 — g: 3F 06 5B 4F 66 6D 7D 07 7F 6F для цифр от 0 до 9. Проверка сверила таблицу с начертанием всех десяти цифр. Для общего анода — те же числа, инвертированные.

static const uint8_t SEG[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F };

⚠️ Яркость падает пропорционально числу разрядов. Каждый разряд горит лишь четверть времени, поэтому ток через сегмент приходится увеличивать — и следить, чтобы импульсный ток не превысил допустимый для индикатора и для выхода микроконтроллера.

⚠️ Не забывайте про токоограничивающие резисторы — по одному на каждый сегмент, а не один общий на разряд. Общий резистор даёт разную яркость в зависимости от того, сколько сегментов зажжено.


22.4 🔧 Адресные светодиоды WS2812B

Лента, где каждый светодиод управляется индивидуально по одному проводу. Внутри каждого — микросхема, которая забирает свои 24 бита и передаёт остальное дальше по цепочке.

Протокол простой по смыслу и жёсткий по времени:

Длительность
период бита 1,25 мкс
высокий уровень для нуля 0,4 мкс
высокий уровень для единицы 0,8 мкс
допуск ±150 нс
пауза сброса не менее 50 мкс у ранних выпусков, 280 мкс у поздних

Дёргать вывод программно можно только с запретом прерываний на всю посылку: любое прерывание собьёт тайминг, и лента покажет случайные цвета. А это 30 мкс без прерываний на каждый светодиод. Поэтому используют аппаратуру: таймер с DMA или SPI, у которого каждый бит ленты кодируют несколькими битами.

Способ через ШИМ и DMA — самый надёжный. Период таймера равен периоду бита, а скважность переключается между двумя значениями:

Период бита, тактов Ноль Единица
F103, 72 МГц 90 28 57
F407, 168 МГц 210 67 134
#define WS_ARR    (((APB2_TIM_HZ / 1000000u) * WS_PERIOD_NS / 1000u) - 1u)   /* период = ARR + 1 такт */
#define WS_CCR_0  ((APB2_TIM_HZ / 1000000u) * WS_T0H_NS / 1000u)
#define WS_CCR_1  ((APB2_TIM_HZ / 1000000u) * WS_T1H_NS / 1000u)

/* Порядок байтов у WS2812B — зелёный, красный, синий, а не привычный RGB. */
void ws2812_set(uint8_t index, uint8_t r, uint8_t g, uint8_t b)
{
    uint32_t grb = ((uint32_t)g << 16) | ((uint32_t)r << 8) | b;
    uint16_t *p = &ws_buf[index * BITS_PER_LED];

    for (uint8_t i = 0; i < BITS_PER_LED; i++) {
        p[i] = (grb & (1u << (23u - i))) ? WS_CCR_1 : WS_CCR_0;
    }
}

Буфер дорогой: 24 отсчёта по 2 байта — 48 байт на светодиод. Лента из 300 светодиодов займёт 14 400 байт, больше двух третей ОЗУ F103C8 (20 КБ).

DMA переливает буфер в регистр сравнения таймера, тот формирует импульсы — процессор не участвует вовсе. При 72 МГц один такт составляет около 14 нс, то есть точность на порядок лучше требуемых 150 нс.

Так посылка одного светодиода выглядит на экране анализатора — с лупой на двух битах:

WS2812B на экране анализатора: 24 бита GRB оранжевого цвета, длительности нуля и единицы от таймера

Три вещи, на которых спотыкаются:

Порядок цветов — GRB, а не RGB. Первым идёт зелёный. Перепутали — цвета меняются местами, и это самая частая жалоба.

Хвост нулей в конце буфера. После последнего светодиода нужна пауза, иначе следующая посылка сольётся с предыдущей: у ранних выпусков WS2812B — не меньше 50 мкс, у поздних — не меньше 280 мкс. Проще всего добавить в буфер 240 нулевых отсчётов: 300 мкс хватает любому выпуску, а сорок отсчётов, ровно 50 мкс, годятся только старым лентам. Либо паузу отсчитывают таймером после окончания DMA.

Питание. Один светодиод в белом на полной яркости берёт около 60 мА. Лента из тридцати штук — почти два ампера, что несравнимо с возможностями платы. Нужен отдельный источник, общая земля с микроконтроллером и конденсатор у начала ленты.

⚠️ Уровень сигнала. Лента питается от 5 В и формально ожидает логическую единицу не ниже 3,5 В, тогда как микроконтроллер даёт 3,3. Часто работает и так, но не всегда и не со всеми партиями лент — в изделии ставят преобразователь уровня.


22.5 Матричная клавиатура

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

Две детали, о которых стоит знать заранее:

Диоды против ложных нажатий. При одновременном нажатии трёх кнопок образуется паразитная цепь, и матрица показывает четвёртую, ненажатую. Модель матрицы 4 × 4 перебрала все наборы нажатий: две кнопки не дают ложного нажатия никогда — 0 из 120 пар; из троек ложное нажатие дают 144 из 560 — ровно те, где три кнопки стоят в углах прямоугольника, и четвёртый угол «загорается». Нажаты (0,0), (0,1) и (1,0) — видна ещё и (1,1): ноль с активной строки 1 уходит через (1,0) в столбец 0, через (0,0) в строку 0, через (0,1) в столбец 1. Диод последовательно с каждой кнопкой пропускает ток только в одну сторону, и с ними все 65 536 наборов читаются верно. Если одновременные нажатия не нужны, можно обойтись без диодов.

Матрица 4 × 4 без диодов: три нажатые кнопки в углах прямоугольника «зажигают» четвёртую

Неактивные строки — открытый сток. Если отпущенные строки не отпустить, а держать в единице, две нажатые кнопки в одном столбце замкнут выход, выставленный в ноль, на выход в единице.

Подтяжки обязательны — на столбцах, иначе входы «висят» (раздел 8). Удобно использовать встроенные подтяжки микроконтроллера.


22.6 Ток динамической индикации

Сегмент горит четверть времени, поэтому для средних 2 мА нужен импульс 8 мА. Цифра 8 с точкой — восемь сегментов разом, и через общий вывод разряда в этот момент идёт 64 мА. Предел одного вывода — 25 мА (раздел 62): общий вывод разряда никогда не подключают к микроконтроллеру напрямую — только через транзистор или ключ. Сегментные выводы по 8 мА укладываются, но сумма по корпусу тоже ограничена, поэтому и сегменты удобнее вести через 74HC595.


22.7 🔧 Индикатор и клавиатура в одном прерывании

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

void TIM2_IRQHandler(void)
{
    uint8_t out[2];
    TIM2->SR = ~TIM_SR_UIF;
    keypad_scan_row(cur);                                     /* строка cur прижата с прошлого раза */
    cur = (uint8_t)((cur + 1u) & 3u);
    GPIOB->BSRR = (0x000Fu & ~(1u << cur)) | (1u << (cur + 16));   /* отпустить остальные строки, прижать следующую */
    out[0] = SEG[digits[cur]];                                /* сегменты */
    out[1] = (uint8_t)(1u << cur);                            /* ключ разряда */
    hc595_write(out, 2);
}

Бит строки cur в младшей половине BSRR снят нарочно: если для одного вывода записаны и установка, и сброс, выполняется установка (раздел 8), и строка так и не прижалась бы. Строку читают через миллисекунду после того, как прижали, — линии успевают перезарядиться, и задержка ничего не стоит. Каждая строка опрашивается раз в 4 мс; антидребезг — пять одинаковых опросов подряд, то есть 20 мс:

static void keypad_scan_row(uint8_t row)
{
    uint8_t cols = (uint8_t)(~(GPIOB->IDR >> 4) & 0x0Fu);   /* нажатая кнопка тянет столбец к нулю */
    key_raw[row] = cols;
    for (uint8_t c = 0; c < 4u; c++) {
        uint8_t now = (uint8_t)((cols >> c) & 1u);
        if (now != ((key_state[row] >> c) & 1u)) {
            if (++key_cnt[row][c] >= 5u) {                    /* пять опросов подряд — 20 мс */
                key_state[row] ^= (uint8_t)(1u << c);
                key_cnt[row][c] = 0;
            }
        } else {
            key_cnt[row][c] = 0;
        }
    }
}

Таймер на F103 и F407 настраивается одинаково, различается предделитель: 71 при 72 МГц и 83 при 84 МГц — оба дают 1 МГц, а ARR 999 — 1 кГц.

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

enum { BTN_NONE, BTN_SHORT, BTN_DOUBLE, BTN_LONG };

#define LONG_MS   800u                            /* держат дольше — «долгое» */
#define DOUBLE_MS 250u                            /* второе нажатие не позже — «двойное» */

uint8_t btn_step(uint32_t now, uint8_t pressed)
{
    if (pressed && !held) {                       /* нажали */
        held = 1;
        t_down = now;
        long_sent = 0;
    } else if (!pressed && held) {                /* отпустили */
        held = 0;
        t_up = now;
        if (long_sent)
            return BTN_NONE;                      /* долгое уже сообщено */
        if (++clicks == 2) {
            clicks = 0;
            return BTN_DOUBLE;
        }
    }
    if (held && !long_sent && now - t_down >= LONG_MS) {
        long_sent = 1;                            /* сообщаем, не дожидаясь отпускания */
        clicks = 0;
        return BTN_LONG;
    }
    if (!held && clicks == 1 && now - t_up >= DOUBLE_MS) {
        clicks = 0;                               /* второго нажатия не было */
        return BTN_SHORT;
    }
    return BTN_NONE;
}

Прогон с шагом 4 мс, как в прерывании: нажатие с 100 до 220 мс даёт SHORT в 472 мс; два нажатия с паузой 100 мс — DOUBLE в момент второго отпускания, в 400 мс; с паузой 300 мс — два SHORT, в 452 и 852 мс; удержание с 100 мс — LONG уже в 900 мс, не дожидаясь отпускания.

За двойное нажатие платят задержкой: короткое сообщается только через 250 мс после отпускания — раньше неизвестно, не будет ли второго. Если двойное не нужно, короткое можно сообщать сразу при отпускании. И клик, за которым сразу следует удержание, здесь — просто долгое нажатие: в прогоне нажатие 100–200 мс и удержание с 300 мс дали один LONG в 1100 мс, первый клик поглощён.


22.8 Грабли

Индикатор показывает мусор при смене числа. Защёлка поднята до сброса BSY — выходы получили сдвинутое не до конца.

Мерцание на видео. Частота на разряд близка к 50 Гц. 250 Гц и выше.

Разная яркость цифр. Один резистор на разряд вместо резистора на сегмент: чем больше сегментов горит, тем меньше ток каждого.

Выход разряда греется или микроконтроллер сбрасывается. Общий вывод разряда подключён напрямую: 64 мА при восьми сегментах. Транзистор.

Нажата лишняя кнопка. Три кнопки в углах прямоугольника без диодов.

Кнопка срабатывает дважды. Нет антидребезга или он короче дребезга контактов. 20 мс — обычный запас.

Цвета WS2812B перепутаны. Порядок GRB.


22.9 Итоги

Полные исходники

ext22.c — 74HC595 и 74HC165 через SPI, динамическая индикация и клавиатура в прерывании.

/* Раздел 22 (обогащение): 74HC595 и 74HC165 через SPI1, динамическая индикация
 * четырёх разрядов в прерывании таймера, опрос клавиатуры 4×4 с антидребезгом.
 * F103 и F407: armclang с -include stm32f10x.h (STM32F10X_MD) или stm32f4xx.h (STM32F407xx). */
#include <stdint.h>

#if defined(STM32F10X_MD)
#define TIM_HZ 72000000u                 /* TIM2 на APB1 ×2 */
#else
#define TIM_HZ 84000000u
#endif
#define SCAN_HZ 1000u                    /* прерываний в секунду: разряд за разрядом */

#define LATCH_LOW()  (GPIOA->BSRR = 1u << (3 + 16))
#define LATCH_HIGH() (GPIOA->BSRR = 1u << 3)
#define PL_LOW()     (GPIOA->BSRR = 1u << (2 + 16))
#define PL_HIGH()    (GPIOA->BSRR = 1u << 2)

uint8_t spi1_xfer(uint8_t data)
{
    while (!(SPI1->SR & SPI_SR_TXE)) { }
    *(volatile uint8_t *)&SPI1->DR = data;
    while (!(SPI1->SR & SPI_SR_RXNE)) { }
    return (uint8_t)SPI1->DR;
}

void hc595_write(const uint8_t *data, uint8_t count)
{
    LATCH_LOW();
    /* Последняя микросхема в цепочке получает данные первой. */
    for (uint8_t i = count; i > 0u; i--) {
        spi1_xfer(data[i - 1u]);
    }
    while (SPI1->SR & SPI_SR_BSY) { }   /* последний бит должен дойти до регистра */
    LATCH_HIGH();          /* по фронту всё выводится наружу одновременно */
}

/* 74HC165: импульс PL защёлкивает входы, затем байты читаются по MISO. */
void hc165_read(uint8_t *data, uint8_t count)
{
    PL_LOW();              /* захватить все входы разом */
    PL_HIGH();
    for (uint8_t i = 0; i < count; i++) {
        data[i] = spi1_xfer(0xFFu);      /* первой приходит ближняя к микроконтроллеру */
    }
}

/* Сегменты gfedcba, общий катод: единица — сегмент горит. */
static const uint8_t SEG[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F };

static volatile uint8_t digits[4];        /* что показывать: цифры 0–9 */
static uint8_t cur;                       /* какой разряд горит */

void display_set(uint16_t value)
{
    for (int i = 3; i >= 0; i--) {
        digits[i] = (uint8_t)(value % 10u);
        value /= 10u;
    }
}

/* Клавиатура: строки PB0–PB3 — открытый сток, столбцы PB4–PB7 — вход с подтяжкой вверх. */
static uint8_t key_raw[4], key_cnt[4][4], key_state[4];

static void keypad_scan_row(uint8_t row)
{
    uint8_t cols = (uint8_t)(~(GPIOB->IDR >> 4) & 0x0Fu);   /* нажатая кнопка тянет столбец к нулю */
    key_raw[row] = cols;
    for (uint8_t c = 0; c < 4u; c++) {
        uint8_t now = (uint8_t)((cols >> c) & 1u);
        if (now != ((key_state[row] >> c) & 1u)) {
            if (++key_cnt[row][c] >= 5u) {                    /* пять опросов подряд — 20 мс */
                key_state[row] ^= (uint8_t)(1u << c);
                key_cnt[row][c] = 0;
            }
        } else {
            key_cnt[row][c] = 0;
        }
    }
}

/* Прерывание таймера 1 кГц: разряд за разрядом, и заодно одна строка клавиатуры. */
void TIM2_IRQHandler(void)
{
    uint8_t out[2];
    TIM2->SR = ~TIM_SR_UIF;
    keypad_scan_row(cur);                                     /* строка cur прижата с прошлого раза */
    cur = (uint8_t)((cur + 1u) & 3u);
    GPIOB->BSRR = (0x000Fu & ~(1u << cur)) | (1u << (cur + 16));   /* отпустить остальные строки, прижать следующую */
    out[0] = SEG[digits[cur]];                                /* сегменты */
    out[1] = (uint8_t)(1u << cur);                            /* ключ разряда */
    hc595_write(out, 2);
}

void scan_timer_init(void)
{
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    TIM2->PSC = TIM_HZ / 1000000u - 1u;  /* 1 МГц */
    TIM2->ARR = 1000000u / SCAN_HZ - 1u; /* 1 кГц */
    TIM2->DIER = TIM_DIER_UIE;
    TIM2->CR1 = TIM_CR1_CEN;
    NVIC_EnableIRQ(TIM2_IRQn);
}

/* ── проверка расчётов ────────────────────────────────────────────────── */
_Static_assert(TIM_HZ / 1000000u - 1u == (TIM_HZ == 84000000u ? 83u : 71u), "PSC: 71 на F103, 83 на F407");
_Static_assert(1000000u / SCAN_HZ - 1u == 999u, "ARR 999 — 1 кГц");
_Static_assert(SCAN_HZ / 4u == 250u, "250 Гц на разряд — впятеро выше порога мерцания 50 Гц");
_Static_assert(5u * 4u * 1000u / SCAN_HZ == 20u, "антидребезг: 5 опросов строки по 4 мс — 20 мс");

int main(void)
{
    uint8_t in[2];
    display_set(1234u);
    scan_timer_init();
    hc165_read(in, 2);
    for (;;) { }
}

btn22.c — одна кнопка: короткое, двойное и долгое нажатие (22.7).

/* Подраздел 22.7: одна кнопка — короткое, двойное и долгое нажатие.
 * btn_step получает время в миллисекундах и уже отфильтрованное от дребезга состояние
 * (то, что даёт опрос из 22.7 раз в 4 мс) и возвращает событие. Ниже — тест по меткам
 * времени: сценарии прогоняются шагами по 4 мс, как в прерывании таймера. */
#include <stdint.h>
#include <stdio.h>

enum { BTN_NONE, BTN_SHORT, BTN_DOUBLE, BTN_LONG };

#define LONG_MS   800u                            /* держат дольше — «долгое» */
#define DOUBLE_MS 250u                            /* второе нажатие не позже — «двойное» */

static uint8_t held, clicks, long_sent;
static uint32_t t_down, t_up;

uint8_t btn_step(uint32_t now, uint8_t pressed)
{
    if (pressed && !held) {                       /* нажали */
        held = 1;
        t_down = now;
        long_sent = 0;
    } else if (!pressed && held) {                /* отпустили */
        held = 0;
        t_up = now;
        if (long_sent)
            return BTN_NONE;                      /* долгое уже сообщено */
        if (++clicks == 2) {
            clicks = 0;
            return BTN_DOUBLE;
        }
    }
    if (held && !long_sent && now - t_down >= LONG_MS) {
        long_sent = 1;                            /* сообщаем, не дожидаясь отпускания */
        clicks = 0;
        return BTN_LONG;
    }
    if (!held && clicks == 1 && now - t_up >= DOUBLE_MS) {
        clicks = 0;                               /* второго нажатия не было */
        return BTN_SHORT;
    }
    return BTN_NONE;
}

/* ── тест: интервалы «нажата» в миллисекундах, шаг 4 мс ─────────────────── */
struct press { uint32_t from, to; };

static void run(const char *name, const struct press *p, unsigned n)
{
    static const char *const ev[] = {"", "SHORT", "DOUBLE", "LONG"};
    held = clicks = long_sent = 0;
    printf("%s:", name);
    for (uint32_t t = 0; t <= 2000u; t += 4u) {
        uint8_t on = 0;
        for (unsigned i = 0; i < n; i++)
            if (t >= p[i].from && t < p[i].to)
                on = 1;
        uint8_t e = btn_step(t, on);
        if (e != BTN_NONE)
            printf(" %s@%u", ev[e], (unsigned)t);
    }
    printf("\n");
}

int main(void)
{
    static const struct press one[] = {{100, 220}};
    static const struct press two[] = {{100, 200}, {300, 400}};
    static const struct press slow[] = {{100, 200}, {500, 600}};
    static const struct press hold[] = {{100, 1300}};
    static const struct press dhold[] = {{100, 200}, {300, 1300}};
    run("short", one, 1);
    run("double", two, 2);
    run("two_short", slow, 2);
    run("long", hold, 1);
    run("click_then_hold", dhold, 2);
    printf("done=1\n");
    return 0;
}

Дальше: раздел 22а «Графический интерфейс на дисплее: буфер кадра, LTDC, DMA2D и сенсор» — когда индикатора мало и нужен экран.

Открыть в интерактивном справочнике