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 нс.
Так посылка одного светодиода выглядит на экране анализатора — с лупой на двух битах:
Три вещи, на которых спотыкаются:
Порядок цветов — 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 наборов читаются верно. Если одновременные нажатия не нужны, можно обойтись без диодов.
Неактивные строки — открытый сток. Если отпущенные строки не отпустить, а держать в единице, две нажатые кнопки в одном столбце замкнут выход, выставленный в ноль, на выход в единице.
Подтяжки обязательны — на столбцах, иначе входы «висят» (раздел 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 Итоги
- Сдвиговые регистры превращают три вывода в восемь и больше:
74HC595на выход,74HC165на вход. Подключать удобнее через SPI; защёлку — только после сбросаBSY. - Динамическая индикация: таймер 1 кГц — 250 Гц на разряд; импульс 8 мА ради средних 2 мА; общий вывод разряда — через транзистор.
- Токоограничивающий резистор — на каждый сегмент, а не один на разряд.
- Протокол WS2812B требует точности ±150 нс: ШИМ с DMA, на F407 — 210 тактов на бит.
- Порядок цветов у WS2812B — GRB; в конце посылки нужна пауза: 50 мкс у ранних выпусков, 280 мкс у поздних; питание ленты — отдельное, с общей землёй.
- Клавиатура без диодов ошибается на 144 тройках из 560; с диодами — никогда. Индикатор и клавиатура уживаются в одном прерывании.
Полные исходники
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 и сенсор» — когда индикатора мало и нужен экран.