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

22п. Ёмкостный тачскрин на FT6236/FT6206 по I2C: касания, жесты, прерывание INT, калибровка поворота под ILI9341 и обработка нажатий кнопок интерфейса

В разделе 22к палец мерил резистивный сенсор: XPT2046 отдавал коды АЦП, их пересчитывали в точки калибровкой по трём касаниям и чистили от дребезга — это здесь не повторяется. В 22о на F407 заработал ILI9341 на 320 × 240, а на модулях с этим дисплеем часто стоит другой сенсор — ёмкостный, с контроллером FT6236 или FT6206 на I2C. Раздел 22а.6 дал общий взгляд: контроллер сам считает касания, отдаёт готовые координаты, а вывод INT говорит, что они есть. Здесь — как это устроено на деле: регистры и отчёт о касаниях, поля событий и ID пальца, режим INT, порог и частота отчётов, подключение к F407 рядом с дисплеем, пересчёт точки под поворот экрана из 22о, обработчик кнопок с удержанием, свайпом и щипком и выбор между опросом и прерыванием.

Опоры: драйвер ST ft6x06 V1.1.0 (STM32F4xx DFP 2.17.1, BSP Components) — адреса регистров, коды событий и жестов, значения G_MODE, коды в CHIP_ID: 0x11 — FT6206, 0xCD — FT6x36; не больше двух касаний; там же комментарии «программного сброса нет» и «по умолчанию — опрос, INT не выдаётся». Поля битов (XH — событие в битах 7:6, YH — ID в битах 7:4) и чтение двух касаний одной пачкой — из ft6x06 V2.0.0 (STM32H7xx DFP 3.1.1). Из BSP плат ST: у STM32412G-Discovery адрес TS_I2C_ADDRESS 0x70, сброс — 10 мс в нуле, INT — с подтяжкой, по спаду, «активный ноль», шина 100 кГц; у STM32469I-Discovery — два адреса, 0x54 и 0x70, код 0xD8 для FT6336G и шина 400 кГц. Из модуля ft5x06 OpenMV (MIT): адрес 0x38, чтение всех точек одной пачкой, пауза 1 мс в сбросе и 39 мс после. RM0090 Rev 19: приём больше двух байт, с. 851; I2C_CCR, с. 870; I2C_TRISE, с. 871; SYSCFG_EXTICR2, с. 291; EXTI4 — позиция 10 в таблице векторов, с. 372; EXTI_FTSR, с. 385; EXTI_PR, с. 386. DS8626 Rev 9: PB10 — I2C2_SCL, PB11 — I2C2_SDA, оба FT, с. 52; AF4 — с. 63; PC4 и PC5 — FT, с. 50; подтяжка вывода 30–50 кОм, с. 115; быстрый режим I²C до 400 кбит/с, с. 121. DocID13587 Rev 17, таблица 40, с. 69: фронт SCL и SDA в быстром режиме — не дольше 300 нс, ёмкость линии — до 400 пФ. Документа на FT6236 в проекте нет: что лежит в CIPHER, в чём мерить PERIODACTIVE и TH_GROUP, как ведёт себя INT и что попадает в отчёт при отпускании — по документации производителя, не проверено, в модели это допущения. Все числа ниже — прогон драйвера _verify/ctp22p/ft22p.c на компьютере (gcc 12.2.0) со своей моделью контроллера на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M4.

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

Оригинал Перевод
projected capacitive (PCAP), mutual capacitance проекционно-ёмкостный сенсор: сетка электродов под стеклом
TD_STATUS (touch data status) сколько касаний сейчас лежит на стекле
event flag: press down, lift up, contact поле события: нажатие, отпускание, контакт
touch ID номер, под которым контроллер ведёт палец от отчёта к отчёту
G_MODE: interrupt polling / interrupt trigger что делает вывод INT: ничего или «есть отчёт»
TH_GROUP (touch threshold) порог, с которого сигнал считается пальцем
PERIODACTIVE (report rate in active mode) частота отчётов, пока палец на стекле
CIPHER, CHIP_ID коды кристалла: какой FT6x перед нами
hit test проверка: в какую кнопку попала точка
ghost touch ложное касание от помехи
pinch (zoom in / zoom out) щипок: два пальца разводят или сводят

Вверху — FT6236 на STM32F407: I2C2 на 400 кГц, адрес 0x38, PB10 — SCL, PB11 — SDA, PC4 — вход INT (ноль — есть отчёт, EXTI4 по спаду), PC5 — выход RST; подтяжки SCL, SDA и INT стоят на модуле; рядом на том же модуле ILI9341 на SPI1 из раздела 22о; стекло тача 240 × 320 совпадает с памятью дисплея при MADCTL 0x00. Посередине — отчёт: 13 байт с адреса 0x02 одним чтением, 370 мкс на 400 кГц: TD_STATUS — число касаний 0…2, затем P1 и P2 по шесть байт; в XH биты 7:6 — событие (0 нажатие, 1 отпускание, 2 контакт), в YH биты 7:4 — ID касания. Внизу слева — тап в модели: отчёт раз в 16,67 мс — нажатие, контакт, контакт, отпускание; на каждом отчёте INT уходит в ноль и держится до чтения; касание принимается на «контакте», щелчок — на «отпускании»; от касания до «нажата» в среднем 24,68 мс по INT и 29,22 мс при опросе раз в 10 мс. Справа — пересчёт точки стекла в экран для MADCTL 08, 68, C8 и A8: (gx, gy), (gy, 239 − gx), (239 − gx, 319 − gy), (319 − gy, gx), проверено на 1610 точках каждого поворота. Внизу — INT против опроса: без касания 0 чтений против 96 в секунду; грабли — INT прижат до чтения, после включения EXTI посмотреть уровень


22п.1 Ёмкостный против резистивного: что меняется

Резистивная панель из 22к — два слоя, которые палец прижимает друг к другу; контроллер — это АЦП, а всё остальное делает программа: меряет, калибрует, гасит дребезг. Ёмкостная — сетка электродов под стеклом и свой контроллер, который сам сканирует сетку и отдаёт готовый отчёт: где пальцы, сколько их и что с каждым случилось. Отсюда разница:


22п.2 Регистры и отчёт: TD_STATUS, P1, P2, события и ID

Адрес на шине — 0x38. У ST в файлах плат стоит TS_I2C_ADDRESS 0x70 — это тот же адрес, уже сдвинутый на бит чтения-записи. У 469I-Discovery ST перебирает два адреса, 0x54 и 0x70: на других модулях адрес бывает свой.

Адрес Имя Что в нём
0x00 DEV_MODE 0 — рабочий режим
0x01 GEST_ID жест, найденный самим контроллером
0x02 TD_STATUS число касаний, биты 3:0
0x03…0x08 P1_XH, XL, YH, YL, WEIGHT, MISC первое касание
0x09…0x0E P2_… второе — те же шесть байт
0x80 TH_GROUP порог касания
0x88 PERIODACTIVE частота отчётов в активном режиме
0xA3 CIPHER «выбор кристалла»
0xA4 G_MODE 0 — опрос, 1 — прерывание
0xA6 FIRMID версия прошивки
0xA8 CHIP_ID 0x11, 0xCD или 0xD8

В P1_XH биты 7:6 — событие: 0 — нажатие, 1 — отпускание, 2 — контакт (палец лежит), 3 — события нет; биты 3:0 — старшие биты X. В P1_YH биты 7:4 — ID касания, 3:0 — старшие биты Y. ID нужен, чтобы отличить пальцы: если первый палец ушёл, второй может переехать из P2 в P1, а ID у него останется прежним.

Коды кристалла — самое ненадёжное место. ST читает CHIP_ID (0xA8) и ждёт 0x11 для FT6206 и 0xCD для FT6x36; у 469I-Discovery есть ещё 0xD8 — по комментарию, FT6336G. CIPHER (0xA3) ST объявляет, но значений не даёт. Драйвер принимает все три кода CHIP_ID, а CIPHER и FIRMID только читает и хранит. В модели CHIP_ID = 0xCD, CIPHER = 0x36 — по документации производителя, не проверено.

Чтение одной пачкой. Отчёт — TD_STATUS и оба касания: 13 байт с адреса 0x02 подряд. Так читает ST V2.0.0 (12 байт с P1_XH) и OpenMV (с 0x01). На 400 кГц это 370 мкс шины, на 100 кГц — 1480 мкс:

static void unpack(const uint8_t *b, ft_point_t *p)
{
    p->ev = (uint8_t)(b[0] >> 6);                       /* XH [7:6] — событие */
    p->x = (uint16_t)((b[0] & 0x0Fu) << 8 | b[1]);
    p->id = (uint8_t)(b[2] >> 4);                       /* YH [7:4] — ID касания */
    p->y = (uint16_t)((b[2] & 0x0Fu) << 8 | b[3]);
    p->weight = b[4];
    p->area = (uint8_t)(b[5] >> 4);
}

/* Всё сразу: TD_STATUS и оба касания — 13 байт с 0x02 одним чтением. */
int ft_read(ft_t *d, ft_report_t *r)
{
    uint8_t b[FT_REPORT_BYTES];
    TRY(rd(d, FT_TD_STATUS, b, FT_REPORT_BYTES));
    r->n = (uint8_t)(b[0] & 0x0Fu);
    if (r->n > FT_MAX_TOUCH)
        r->n = 0;                                       /* как у ST: больше двух — значит мусор */
    unpack(b + 1, &r->p[0]);
    unpack(b + 7, &r->p[1]);
    return FT_OK;
}

Драйвер разбирает оба места, даже если TD_STATUS меньше двух: в модели палец, который только что ушёл, приходит последним отчётом с событием «отпускание» на месте сверх TD_STATUS, а пустое место — байты 0xFF, событие 3. Как это устроено в кристалле — по документации производителя, не проверено; обработчик в 22п.5 не полагается на это и отпускает палец, если его ID просто пропал из отчёта.

G_MODE, TH_GROUP, PERIODACTIVE. G_MODE = 1 — INT сообщает о новом отчёте, 0 — опрос; ST по умолчанию ставит 0 с комментарием «INT не выдаётся». В модели при G_MODE = 1 INT уходит в ноль на новом отчёте и держится, пока не прочитан TD_STATUS. TH_GROUP — порог касания, PERIODACTIVE — частота отчётов; в чём их мерить, ST не пишет. В модели порог — в единицах её сигнала (палец — 100), а PERIODACTIVE — отчётов в секунду: 60 — отчёт раз в 16,67 мс. Это допущения; на железе значения надо подбирать по документу на свой кристалл.


22п.3 Подключение к F407 и запуск

ILI9341 из 22о занял SPI1 (PA5–PA7) и PB0, PB1, PB6–PB8 — среди них PB6 и PB8, где у F407 сидит I2C1. Тач поэтому на I2C2:

Вывод модуля STM32F407 Назначение
SCL, SDA PB10, PB11 I2C2, AF4, открытый сток, 400 кГц
INT PC4 вход с подтяжкой, EXTI4 по спаду
RST PC5 выход, 0 — сброс
VCC, GND 3,3 В, GND питание

PB10 и PB11 — FT (DS8626, с. 52), AF4 — с. 63; PC4 и PC5 — FT, с. 50. APB1 — 42 МГц: в быстром режиме при DUTY = 0 низкий уровень вдвое длиннее высокого (RM0090, с. 870), CCR = 42 / (3 · 0,4) = 35 — ровно 400 кГц. TRISE — предельный фронт плюс один (с. 871): 300 нс × 42 МГц = 12,6, целая часть 12, плюс один — 13. Приём больше двух байт — как на с. 851: последние три байта через BTF. INT — в SYSCFG_EXTICR2 поле EXTI4 = порт C (с. 291), EXTI_FTSR — спад (с. 385), обработчик EXTI4 — позиция 10 (с. 372).

Запуск — сброс выводом RST (10 мс в нуле, как у ST), пауза 40 мс (OpenMV ждёт 39 мс), коды, настройки и одно чтение TD_STATUS:

int ft_init(ft_t *d, const ft_cfg_t *c)
{
    uint8_t id[2];
    d->xfers = d->bytes = 0;
    if (d->addr == 0)
        d->addr = FT_ADDR;
    ft_port_rst(0);
    ft_port_delay_ms(10);
    ft_port_rst(1);
    ft_port_delay_ms(40);
    TRY(rd(d, FT_CHIP_ID, &d->chip_id, 1));
    TRY(rd(d, FT_CIPHER, id, 1));
    d->cipher = id[0];
    TRY(rd(d, FT_FIRMID, &d->firm, 1));
    if (d->chip_id != FT_ID_FT6206 && d->chip_id != FT_ID_FT6X36 && d->chip_id != FT_ID_FT6336G)
        return FT_E_ID;
    TRY(wr(d, FT_DEV_MODE, 0));                         /* рабочий режим — так делает и OpenMV */
    TRY(wr(d, FT_TH_GROUP, c->th_group));
    TRY(wr(d, FT_PERIODACTIVE, c->period_active));
    TRY(wr(d, FT_G_MODE, c->gmode));
    return rd(d, FT_TD_STATUS, id, 1);                  /* прочитать — снять INT, если он уже прижат */
}

Программного сброса у FT6x06 нет — так прямо написано в ft6x06_Reset у ST, поэтому RST лучше развести на вывод. На стенде запуск — 8 обменов и 16 байт: 690 мкс шины и 50,69 мс вместе с паузами. Сколько на самом деле ждать после RST — у ST 10 мс в нуле, у OpenMV 1 мс и 39 мс после; модель отвечает сразу. С адресом 0x70 вместо 0x38 запуск кончается на первом же обмене: ведомый не ответил, код −1.


22п.4 Стекло → экран: четыре поворота и зеркала

В модели стекло тача лежит так же, как память ILI9341 при MADCTL 0x00: точка тача (gx, gy) — это столбец gx и строка gy памяти. Дальше всё решает поворот экрана. В 22о дисплей кладёт точку экрана (x, y) в память так: MV меняет x и y местами, потом MX зеркалит столбец, MY — строку. Тач нужен обратный путь: снять зеркала и поменять оси назад. Для поворотов из 22о:

Поворот MADCTL Экран x экрана y экрана
0° 08 240 × 320 gx gy
90° 68 320 × 240 gy 239 − gx
180° C8 240 × 320 239 − gx 319 − gy
270° A8 320 × 240 319 − gy gx
void ft_to_screen(const ft_map_t *m, uint16_t px, uint16_t py, int16_t *x, int16_t *y)
{
    int16_t gx = (int16_t)(m->swap ? py : px), gy = (int16_t)(m->swap ? px : py);
    if (m->mir_x)
        gx = (int16_t)(GLASS_W - 1 - gx);
    if (m->mir_y)
        gy = (int16_t)(GLASS_H - 1 - gy);
    if (m->madctl & MAD_MX)
        gx = (int16_t)(GLASS_W - 1 - gx);
    if (m->madctl & MAD_MY)
        gy = (int16_t)(GLASS_H - 1 - gy);
    if (m->madctl & MAD_MV) {
        *x = gy;
        *y = gx;
    } else {
        *x = gx;
        *y = gy;
    }
}

Функция берёт тот же байт MADCTL, что ушёл в дисплей, — поворот меняется в одном месте. Поля swap, mir_x, mir_y — для модуля, где тач приклеен иначе; так же делает ST у 412G-Discovery флагами TS_SWAP_X, TS_SWAP_Y, TS_SWAP_XY и пересчётом 240 − 1 − x.

Проверка таблицы — двумя путями. Сначала на 1610 точках стекла (сетка через 7 точек) для каждого поворота: формула таблицы, пропущенная через запись дисплея из 22о, возвращает ту же точку стекла — 0 расхождений, и ft_to_screen на Си даёт ту же точку экрана — тоже 0. Потом касаниями: кнопки 3 × 3 на экране, тап в середину каждой.

Поворот MADCTL Верно Без учёта поворота Тач зеркален по X Зеркало исправлено
0° 08 9 из 9 9 из 9 3 из 9 9 из 9
90° 68 9 из 9 0 из 9 3 из 9 9 из 9
180° C8 9 из 9 1 из 9 3 из 9 9 из 9
270° A8 9 из 9 0 из 9 3 из 9 9 из 9

Без учёта поворота при 180° попадает только середина — центральная кнопка, при 90° и 270° — ничего. Зеркало по X оставляет средний столбец и путает левый с правым.


22п.5 Обработчик интерфейса: кнопки, удержание, антидребезг, свайп и щипок

Обработчик ui_feed получает отчёт, точки уже в координатах экрана, и выдаёт события: «нажата» с номером кнопки, «щелчок», «удержание», «отмена», свайп в одну из четырёх сторон, щипок «свели» или «развели». Палец он ведёт по ID из отчёта. Настройки прошивки: удержание — 500 мс, сдвиг — 12 точек, свайп — не короче 40 точек и не дольше 400 мс, щипок — расстояние изменилось на 30 точек.

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

        k->seen = 1;
        ft_to_screen(m, p->x, p->y, &k->x, &k->y);
        k->t = now_ms;
        if (!k->ok && (!c->confirm || p->ev == FT_EV_CONTACT))
            n = accept(u, k, ev, n, max);               /* антидребезг: ждать «контакт» — второй отчёт */

Кнопка. Попадание проверяется по первой точке касания. Принятое касание на кнопке даёт «нажата»; палец ушёл с кнопки или сдвинулся дальше 12 точек — «отмена», это уже не щелчок; пролежал 500 мс на месте — «удержание», и щелчка после него нет; отпустили — «щелчок».

Свайп и щипок считаются по двум отсчётам — первому и последнему, без истории пути:

static int lift(ui_t *u, ui_touch_t *k, ui_ev_t *ev, int n, int max)
{
    const ui_cfg_t *c = u->c;
    if (k->ok && u->pinch) {
        int32_t dd = u->d1 - u->d0;
        if (dd >= c->pinch_min)
            n = put(ev, n, max, UI_PINCH_OUT, -1, k);   /* развели — увеличить */
        else if (-dd >= c->pinch_min)
            n = put(ev, n, max, UI_PINCH_IN, -1, k);
        u->pinch = 0;
        for (uint8_t i = 0; i < FT_MAX_TOUCH; i++)
            u->k[i].solo = 0;                           /* второй палец доживает без жестов */
    } else if (k->ok && k->solo) {
        int16_t dx = (int16_t)(k->x - k->x0), dy = (int16_t)(k->y - k->y0);
        if (k->btn >= 0 && !k->held)
            n = put(ev, n, max, UI_CLICK, k->btn, k);
        else if (k->btn < 0 && k->t - k->t0 <= c->swipe_ms && (absd(dx) >= c->swipe_min || absd(dy) >= c->swipe_min)) {
            uint8_t t = absd(dx) >= absd(dy) ? (dx < 0 ? UI_SWIPE_L : UI_SWIPE_R) : (dy < 0 ? UI_SWIPE_U : UI_SWIPE_D);
            n = put(ev, n, max, t, -1, k);
        }
    }
    k->used = 0;
    return n;
}

Второй принятый палец отменяет кнопку первого и начинает щипок: расстояние между пальцами в начале и последнее, целым корнем. Свайп и щипок считаются в координатах экрана, поэтому «влево» остаётся влево при любом повороте.

Свои жесты есть и у контроллера — регистр GEST_ID, коды у ST: 0x10 вверх, 0x14 вправо, 0x18 вниз, 0x1C влево, 0x48 и 0x49 — масштаб. Но они в координатах стекла: OpenMV для своей платы переставляет их с пометкой «повёрнуто на 90 градусов». Как их включать — по документации производителя; в модели регистр всегда 0, и жесты здесь — свои.

Набор в модели: 20 тапов по 80…200 мс, 10 удержаний по 0,7…1 с, 16 свайпов на 80…120 точек за 120…300 мс, 10 раз «нажал и увёл с кнопки», 10 щипков (расстояние 60 ↔ 160 точек за 300…500 мс). Книжная ориентация (08) и альбомная (68) — по 66 из 66 верно; 1473 чтения за 46,656 с, шина занята 1,17 % времени.


22п.6 Опрос или прерывание; перчатка, вода и помеха

Задержка и шина. 20 тапов по 80…200 мс по кнопкам, отчёты 60 раз в секунду (16,67 мс), антидребезг включён. Задержка — от касания до события «нажата»:

Способ Средняя задержка Худшая Чтений за опыт Шина во время тапов Чтений в секунду без касания
INT, 400 кГц 24,68 мс 33,39 мс 168 0,73 % 0
INT, без антидребезга 8,02 мс 16,73 мс 168 0,73 % 0
INT, отчёты 30 в секунду 48,97 мс 66,97 мс 94 0,41 % 0
INT, отчёты 100 в секунду 14,85 мс 19,01 мс 267 1,15 % 0
опрос раз в 10 мс 29,22 мс 39,68 мс 826 3,56 % 96
опрос раз в 20 мс 35,83 мс 50,10 мс 420 1,81 % 49
опрос раз в 50 мс 51,79 мс 76,67 мс 170 0,73 % 19
INT, 100 кГц 27,06 мс 34,68 мс 168 2,90 % 0
опрос раз в 10 мс, 100 кГц 31,11 мс 40,07 мс 746 12,88 % 87

Все 20 щелчков на месте во всех строках. По INT шина работает, только когда есть отчёт: без пальца — ни одного чтения. Опрос раз в 10 мс читает 96 раз в секунду и без пальца, а задержку всё равно не догоняет: отчёт уже лежит, но его ещё не забрали. Антидребезг стоит одного периода отчётов — 24,68 мс против 8,02; чаще отчёты — меньше задержка.

Чтение как у ST. Драйвер ST V1.1.0 читает TD_STATUS, потом на каждое касание 4 байта X/Y и 3 байта «сведений» начиная с Pn_WEIGHT. Третий байт сведений — уже следующий регистр, P2_XH (для второго касания — 0x0F), а событие ST берёт именно из него. В V2.0.0 событие берётся из своего XH. Повторённое в ft_read_split, это чтение дало в модели 0 щелчков из 20 — у единственного пальца событие всегда «нет события» соседнего пустого места — и 632 обмена вместо 168. По шине одно касание так стоит 512,5 мкс на 400 кГц против 370, два — 825 мкс.

Строгий разбор событий. Если начинать касание только с «нажатия», а кончать только «отпусканием», опрос медленнее отчётов теряет и то и другое. Тапы по 60…120 мс:

Разбор INT Опрос раз в 50 мс
по ID и наличию в отчёте 20 из 20 20 из 20
только по «нажатию» и «отпусканию» 20 из 20 3 из 20

Тот же случай — перезапуск микроконтроллера, когда палец уже лежит: первым придёт «контакт», «нажатия» не будет. Палец на кнопке 4 за 200 мс до запуска цикла: разбор по ID дал «нажата» и «щелчок» кнопки 4, строгий — ничего.

Перчатка, вода и помеха. В модели палец даёт сигнал 100, палец в перчатке — 45, капля воды — 50; шум без помехи — σ 6, с помехой от ШИМ подсветки — σ 20 (это допущения, не замер). Ложное касание — шум выше порога в одной из 48 ячеек: оно живёт один отчёт. 10 тапов, 10 удержаний по 0,7…1 с, 10 тапов в перчатке, капля на 2 с и 10 с без касаний:

Помеха, TH_GROUP, антидребезг Ложных касаний за 10 с Ложных щелчков Тапы Удержания В перчатке Капля: событий
нет, 60, да 0 0 10 10 0 0
ШИМ, 60, нет 34 18 0 0 0 25
ШИМ, 60, да 33 0 9 4 2 5
ШИМ, 90, да 0 0 6 0 0 0
нет, 40, да 0 0 10 10 3 7
нет, 30, да 0 0 10 10 10 1
ШИМ, 40, да 406 0 1 4 0 6

Антидребезг по событиям убирает все ложные щелчки — 18 против 0, — но не всё: при помехе палец иногда пропадает из отчёта на один скан, и длинное удержание рвётся на куски. Высокий порог глушит помеху и заодно настоящий палец: при 90 тапов 6 из 10 и ни одного удержания. Перчатку видно только при низком пороге — 30, а там и капля даёт касание. Порог — компромисс, а помеху лучше убирать там, где она рождается: развести подсветку и шлейф тача, поднять частоту ШИМ.


22п.7 Грабли

INT прижат, а прерываний нет. В модели INT держится в нуле, пока не прочитан TD_STATUS. Если первый отчёт пришёл до того, как включили EXTI, — палец лёг во время запуска, — спада больше не будет: 10 тапов, 0 прочтений, INT в нуле до конца опыта. Прошивка после включения EXTI и после каждого чтения смотрит уровень: если INT уже в нуле — читать, не дожидаясь спада. Так те же 10 тапов — 10 из 10.

    for (;;) {
        __disable_irq();
        if (!int_flag && !ft_port_int_low()) { /* INT прижат, а спада не было — всё равно читать */
            __WFI();
            __enable_irq();
            continue;
        }
        int_flag = 0;
        __enable_irq();
        if (ft_read(&T, &r) != FT_OK)
            continue;

Адрес 0x70. У ST в файлах плат адрес уже сдвинут. Подставленный как 7-битный, он не отвечает — запуск кончается кодом −1 на первом обмене.

400 кГц и подтяжки. Фронт в быстром режиме — до 300 нс, ёмкость линии — до 400 пФ (DocID13587, табл. 40). Фронт до 0,7 питания через подтяжку R на ёмкость C — 0,8473 · R · C:

Подтяжка 50 пФ 100 пФ
2,2 кОм 93 нс 186 нс
4,7 кОм 199 нс 398 нс
10 кОм 424 нс 847 нс
40 кОм (своя подтяжка вывода) 1695 нс 3389 нс

Своя подтяжка F407 — 30–50 кОм (DS8626, с. 115): для 400 кГц не годится. Какие резисторы стоят на модуле и есть ли они там вообще — по схеме модуля, здесь не проверялось. Не уложились — 100 кГц: задержка по INT выросла с 24,68 до 27,06 мс, а шина занята в 4 раза дольше.

Перепутаны X и Y, зеркала. Касание попадает в соседние кнопки или «едет» в обратную сторону. Не менять знаки наугад: взять MADCTL дисплея (22п.4) и отдельно — как приклеен тач (swap, mir_x, mir_y). Проверка — тап в углы и середину.

«Контакт» без «нажатия». После перезапуска микроконтроллера или при опросе реже отчётов первое, что видно, — «контакт». Строгий разбор такое касание не видит; заводить касание по новому ID с любым событием.

Событие из чужого регистра. Чтение «как у ST V1.1.0» берёт событие из P2_XH: с одним пальцем — всегда «нет события». Читать отчёт одной пачкой и брать событие из своего XH.

Ложные касания от подсветки. ШИМ подсветки и длинный шлейф дают касания на один отчёт. Антидребезг по событиям, а не повышение порога до потери перчатки и удержаний.


22п.8 Итоги

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

Стенд host22p.c, модель model22p.py и сценарии лежат рядом с проверкой в _verify/ctp22p/.

ft22p.h — регистры FT6x06, поля отчёта, порт, драйвер, пересчёт в экран и обработчик интерфейса: типы и прототипы.

/* ft22p.h — раздел 22п: ёмкостный тачскрин FT6236/FT6206 по I2C и обработчик кнопок, жестов и щипка.
 * Адреса регистров, поля событий и коды — как в драйвере ST ft6x06 (STM32F4xx DFP 2.17.1, BSP Components,
 * V1.1.0) и в ft6x06_reg.h (STM32H7xx DFP 3.1.1, V2.0.0); адрес 0x38 и чтение пачкой — как в модуле ft5x06
 * OpenMV. Помечено «док.» — по документации производителя, не проверено (её в проекте нет).
 * Шину, паузы, RST и INT даёт прошивка (fw22p.c — I2C2 STM32F407) или стенд (host22p.c — строки к model22p.py). */
#ifndef FT22P_H
#define FT22P_H
#include <stdint.h>

#define FT_ADDR          0x38u      /* 7 бит; у ST — TS_I2C_ADDRESS 0x70, это тот же адрес, сдвинутый на 1 */

/* ── регистры ── */
#define FT_DEV_MODE      0x00u      /* 0 — рабочий режим */
#define FT_GEST_ID       0x01u      /* жест, найденный самим контроллером */
#define FT_TD_STATUS     0x02u      /* число касаний: биты 3:0, 0…2 */
#define FT_P1_XH         0x03u      /* событие [7:6], X [11:8] */
#define FT_P1_XL         0x04u
#define FT_P1_YH         0x05u      /* ID касания [7:4], Y [11:8] */
#define FT_P1_YL         0x06u
#define FT_P1_WEIGHT     0x07u
#define FT_P1_MISC       0x08u      /* площадь [7:4] */
#define FT_P2_XH         0x09u      /* второе касание — те же шесть байт */
#define FT_TH_GROUP      0x80u      /* порог касания */
#define FT_CTRL          0x86u      /* 0 — всегда активен, 1 — засыпать без касаний */
#define FT_PERIODACTIVE  0x88u      /* частота отчётов в активном режиме (единицы — док.) */
#define FT_LIB_VER_H     0xA1u
#define FT_CIPHER        0xA3u      /* «Chip Selecting»: какой кристалл (значения — док.) */
#define FT_G_MODE        0xA4u      /* 0 — опрос, 1 — прерывание по INT */
#define FT_FIRMID        0xA6u
#define FT_CHIP_ID       0xA8u      /* ST ждёт 0x11 (FT6206), 0xCD (FT6x36); 0xD8 — FT6336G у 469I-Discovery */

#define FT_ID_FT6206     0x11u
#define FT_ID_FT6X36     0xCDu
#define FT_ID_FT6336G    0xD8u

#define FT_EV_DOWN       0u         /* нажатие */
#define FT_EV_LIFT       1u         /* отпускание */
#define FT_EV_CONTACT    2u         /* контакт: палец лежит */
#define FT_EV_NONE       3u
#define FT_REPORT_BYTES  13u        /* TD_STATUS и два касания по 6 байт: 0x02…0x0E одним чтением */
#define FT_MAX_TOUCH     2u

enum { FT_OK = 0, FT_E_NACK = -1, FT_E_ID = -2 };

/* ── шина, время, RST и INT: реализует прошивка или стенд ── */
int  ft_port_write(uint8_t addr7, const uint8_t *buf, uint32_t n);         /* 0 — ведомый подтвердил всё */
int  ft_port_read(uint8_t addr7, uint8_t reg, uint8_t *buf, uint32_t n);   /* адрес регистра, повторный старт, n байт */
void ft_port_delay_ms(uint32_t ms);
void ft_port_rst(int level);                                                /* вывод RST: 0 — сброс */
int  ft_port_int_low(void);                                                 /* 1 — INT прижат */

typedef struct {
    uint8_t gmode;                  /* FT_G_MODE */
    uint8_t th_group;               /* FT_TH_GROUP */
    uint8_t period_active;          /* FT_PERIODACTIVE */
} ft_cfg_t;

typedef struct {
    uint8_t ev, id;                 /* событие и ID касания */
    uint16_t x, y;                  /* координаты стекла, 12 бит */
    uint8_t weight, area;
} ft_point_t;

typedef struct {
    uint8_t n;                      /* TD_STATUS: сколько касаний лежит */
    ft_point_t p[FT_MAX_TOUCH];     /* оба места — даже если n меньше: там бывает «отпускание» */
} ft_report_t;

typedef struct {
    uint8_t addr, chip_id, cipher, firm;
    uint32_t xfers, bytes;
} ft_t;

int  ft_init(ft_t *d, const ft_cfg_t *c);
int  ft_read(ft_t *d, ft_report_t *r);            /* одной пачкой 13 байт с 0x02 */
int  ft_read_split(ft_t *d, ft_report_t *r);      /* как драйвер ST: число, потом X/Y и сведения по касанию */

/* ── стекло → экран: монтаж тача и MADCTL дисплея (22о) ── */
#define MAD_MY  0x80u
#define MAD_MX  0x40u
#define MAD_MV  0x20u
#define GLASS_W 240
#define GLASS_H 320
typedef struct {
    uint8_t madctl;                 /* тот же байт, что ушёл в ILI9341 (BGR и ML не важны) */
    uint8_t swap, mir_x, mir_y;     /* как тач приклеен к стеклу: обычно 0, 0, 0 */
} ft_map_t;
void ft_to_screen(const ft_map_t *m, uint16_t px, uint16_t py, int16_t *x, int16_t *y);

/* ── обработчик интерфейса ── */
typedef struct { int16_t x, y, w, h; } ui_rect_t;

enum { UI_NONE = 0, UI_DOWN, UI_CLICK, UI_HOLD, UI_CANCEL, UI_SWIPE_L, UI_SWIPE_R, UI_SWIPE_U, UI_SWIPE_D,
       UI_PINCH_IN, UI_PINCH_OUT };

typedef struct {
    const ui_rect_t *btn;
    uint8_t nbtn;
    uint8_t confirm;                /* 1 — касание принято, когда пришёл «контакт» (антидребезг по событиям) */
    uint8_t strict;                 /* 1 — начало только по «нажатию», конец только по «отпусканию» */
    uint16_t hold_ms;               /* удержание */
    uint16_t slop;                  /* сдвиг, после которого кнопка уже не «держится» */
    uint16_t swipe_min, swipe_ms;   /* свайп: не короче и не дольше */
    uint16_t pinch_min;             /* щипок: на сколько точек изменилось расстояние */
} ui_cfg_t;

typedef struct {
    uint8_t used, id, ok, seen, held, moved, solo;
    int8_t btn;                     /* нажатая кнопка, −1 — нет */
    int16_t x0, y0, x, y;           /* первый и последний отсчёт */
    uint32_t t0, t;
} ui_touch_t;

typedef struct { uint8_t type; int8_t btn; int16_t x, y; uint32_t t; } ui_ev_t;

typedef struct {
    const ui_cfg_t *c;
    ui_touch_t k[FT_MAX_TOUCH];
    uint8_t pinch;                  /* оба пальца приняты */
    int32_t d0, d1;                 /* расстояние в начале и последнее, точки */
} ui_t;

void ui_reset(ui_t *u, const ui_cfg_t *c);
int  ui_feed(ui_t *u, const ft_report_t *r, const ft_map_t *m, uint32_t now_ms, ui_ev_t *ev, int max);
int  ui_hit(const ui_cfg_t *c, int16_t x, int16_t y);
#endif

ft22p.c — драйвер: сброс, коды, режим INT, чтение пачкой и как у ST, стекло → экран; обработчик кнопок, удержания, свайпа и щипка — общий для прошивки и стенда.

/* ft22p.c — раздел 22п: драйвер FT6236/FT6206 (сброс, коды, режим INT, порог, чтение пачкой, стекло → экран)
 * и обработчик интерфейса (кнопки, удержание, свайп, щипок). Общий для прошивки fw22p.c и стенда host22p.c. */
#include "ft22p.h"

#define TRY(x) do { int rc_ = (x); if (rc_ != FT_OK) return rc_; } while (0)

static int wr(ft_t *d, uint8_t reg, uint8_t v)
{
    uint8_t b[2] = {reg, v};
    d->xfers++;
    d->bytes += 2u;
    return ft_port_write(d->addr, b, 2) ? FT_E_NACK : FT_OK;
}

static int rd(ft_t *d, uint8_t reg, uint8_t *p, uint32_t n)
{
    d->xfers++;
    d->bytes += 1u + n;
    return ft_port_read(d->addr, reg, p, n) ? FT_E_NACK : FT_OK;
}

/* Сброс выводом RST (10 мс в нуле — как BSP_TS_Reset у ST), пауза 40 мс (OpenMV ждёт 39 мс), коды, настройки. */
int ft_init(ft_t *d, const ft_cfg_t *c)
{
    uint8_t id[2];
    d->xfers = d->bytes = 0;
    if (d->addr == 0)
        d->addr = FT_ADDR;
    ft_port_rst(0);
    ft_port_delay_ms(10);
    ft_port_rst(1);
    ft_port_delay_ms(40);
    TRY(rd(d, FT_CHIP_ID, &d->chip_id, 1));
    TRY(rd(d, FT_CIPHER, id, 1));
    d->cipher = id[0];
    TRY(rd(d, FT_FIRMID, &d->firm, 1));
    if (d->chip_id != FT_ID_FT6206 && d->chip_id != FT_ID_FT6X36 && d->chip_id != FT_ID_FT6336G)
        return FT_E_ID;
    TRY(wr(d, FT_DEV_MODE, 0));                         /* рабочий режим — так делает и OpenMV */
    TRY(wr(d, FT_TH_GROUP, c->th_group));
    TRY(wr(d, FT_PERIODACTIVE, c->period_active));
    TRY(wr(d, FT_G_MODE, c->gmode));
    return rd(d, FT_TD_STATUS, id, 1);                  /* прочитать — снять INT, если он уже прижат */
}

static void unpack(const uint8_t *b, ft_point_t *p)
{
    p->ev = (uint8_t)(b[0] >> 6);                       /* XH [7:6] — событие */
    p->x = (uint16_t)((b[0] & 0x0Fu) << 8 | b[1]);
    p->id = (uint8_t)(b[2] >> 4);                       /* YH [7:4] — ID касания */
    p->y = (uint16_t)((b[2] & 0x0Fu) << 8 | b[3]);
    p->weight = b[4];
    p->area = (uint8_t)(b[5] >> 4);
}

/* Всё сразу: TD_STATUS и оба касания — 13 байт с 0x02 одним чтением. */
int ft_read(ft_t *d, ft_report_t *r)
{
    uint8_t b[FT_REPORT_BYTES];
    TRY(rd(d, FT_TD_STATUS, b, FT_REPORT_BYTES));
    r->n = (uint8_t)(b[0] & 0x0Fu);
    if (r->n > FT_MAX_TOUCH)
        r->n = 0;                                       /* как у ST: больше двух — значит мусор */
    unpack(b + 1, &r->p[0]);
    unpack(b + 7, &r->p[1]);
    return FT_OK;
}

/* Как BSP ST V1.1.0: TD_STATUS, затем по каждому касанию X/Y — 4 байта с Pn_XH, «сведения» — 3 байта с
 * Pn_WEIGHT, потом жест. Третий байт сведений — уже следующий регистр, Pn_XH соседа: ST берёт событие оттуда. */
int ft_read_split(ft_t *d, ft_report_t *r)
{
    uint8_t n, b[4], w[3], g;
    TRY(rd(d, FT_TD_STATUS, &n, 1));
    r->n = (uint8_t)(n & 0x0Fu);
    if (r->n > FT_MAX_TOUCH)
        r->n = 0;
    for (uint8_t i = 0; i < FT_MAX_TOUCH; i++) {
        r->p[i].ev = FT_EV_NONE;
        r->p[i].id = 15u;
    }
    for (uint8_t i = 0; i < r->n; i++) {
        uint8_t base = (uint8_t)(FT_P1_XH + 6u * i);
        TRY(rd(d, base, b, 4));
        TRY(rd(d, (uint8_t)(base + 4u), w, 3));
        r->p[i].x = (uint16_t)((b[0] & 0x0Fu) << 8 | b[1]);
        r->p[i].id = (uint8_t)(b[2] >> 4);
        r->p[i].y = (uint16_t)((b[2] & 0x0Fu) << 8 | b[3]);
        r->p[i].weight = w[0];
        r->p[i].area = (uint8_t)((w[1] & 0x40u) >> 4);  /* маска ST: (0x04 << 4) >> 4 */
        r->p[i].ev = (uint8_t)(w[2] >> 6);              /* 0x09 или 0x0F, а не свой Pn_XH — как у ST */
    }
    return rd(d, FT_GEST_ID, &g, 1);
}

/* Стекло тача → точка экрана. Сначала монтаж (как тач приклеен), потом обратное к MADCTL ILI9341:
 * дисплей кладёт точку (c, r) в память так — MV меняет c и r, затем MX зеркалит столбец, MY — строку. */
void ft_to_screen(const ft_map_t *m, uint16_t px, uint16_t py, int16_t *x, int16_t *y)
{
    int16_t gx = (int16_t)(m->swap ? py : px), gy = (int16_t)(m->swap ? px : py);
    if (m->mir_x)
        gx = (int16_t)(GLASS_W - 1 - gx);
    if (m->mir_y)
        gy = (int16_t)(GLASS_H - 1 - gy);
    if (m->madctl & MAD_MX)
        gx = (int16_t)(GLASS_W - 1 - gx);
    if (m->madctl & MAD_MY)
        gy = (int16_t)(GLASS_H - 1 - gy);
    if (m->madctl & MAD_MV) {
        *x = gy;
        *y = gx;
    } else {
        *x = gx;
        *y = gy;
    }
}

/* ── обработчик интерфейса ── */
void ui_reset(ui_t *u, const ui_cfg_t *c)
{
    for (uint32_t i = 0; i < sizeof *u; i++)
        ((uint8_t *)u)[i] = 0;
    u->c = c;
}

int ui_hit(const ui_cfg_t *c, int16_t x, int16_t y)
{
    for (uint8_t i = 0; i < c->nbtn; i++) {
        const ui_rect_t *b = &c->btn[i];
        if (x >= b->x && x < b->x + b->w && y >= b->y && y < b->y + b->h)
            return i;
    }
    return -1;
}

static int16_t absd(int32_t v)
{
    return (int16_t)(v < 0 ? -v : v);
}

static int32_t dist(const ui_touch_t *a, const ui_touch_t *b)
{
    int32_t dx = a->x - b->x, dy = a->y - b->y, s = dx * dx + dy * dy, r = 0;
    while ((r + 1) * (r + 1) <= s)                     /* целый корень: расстояние в точках */
        r++;
    return r;
}

static int put(ui_ev_t *ev, int n, int max, uint8_t type, int8_t btn, const ui_touch_t *k)
{
    if (n < max)
        ev[n] = (ui_ev_t){type, btn, k->x, k->y, k->t};
    return n + 1;
}

/* Палец ушёл: щипок, щелчок по кнопке или свайп по двум отсчётам — первому и последнему. */
static int lift(ui_t *u, ui_touch_t *k, ui_ev_t *ev, int n, int max)
{
    const ui_cfg_t *c = u->c;
    if (k->ok && u->pinch) {
        int32_t dd = u->d1 - u->d0;
        if (dd >= c->pinch_min)
            n = put(ev, n, max, UI_PINCH_OUT, -1, k);   /* развели — увеличить */
        else if (-dd >= c->pinch_min)
            n = put(ev, n, max, UI_PINCH_IN, -1, k);
        u->pinch = 0;
        for (uint8_t i = 0; i < FT_MAX_TOUCH; i++)
            u->k[i].solo = 0;                           /* второй палец доживает без жестов */
    } else if (k->ok && k->solo) {
        int16_t dx = (int16_t)(k->x - k->x0), dy = (int16_t)(k->y - k->y0);
        if (k->btn >= 0 && !k->held)
            n = put(ev, n, max, UI_CLICK, k->btn, k);
        else if (k->btn < 0 && k->t - k->t0 <= c->swipe_ms && (absd(dx) >= c->swipe_min || absd(dy) >= c->swipe_min)) {
            uint8_t t = absd(dx) >= absd(dy) ? (dx < 0 ? UI_SWIPE_L : UI_SWIPE_R) : (dy < 0 ? UI_SWIPE_U : UI_SWIPE_D);
            n = put(ev, n, max, t, -1, k);
        }
    }
    k->used = 0;
    return n;
}

/* Принять касание: одно — кнопка под пальцем, второе — начало щипка (кнопку отменить). */
static int accept(ui_t *u, ui_touch_t *k, ui_ev_t *ev, int n, int max)
{
    ui_touch_t *o = &u->k[k == &u->k[0]];
    k->ok = 1;
    if (o->used && o->ok) {
        if (o->btn >= 0)
            n = put(ev, n, max, UI_CANCEL, o->btn, o);
        o->btn = k->btn = -1;
        u->pinch = o->solo;                             /* щипок, если первый палец ещё был «свой» */
        k->solo = o->solo;
        u->d0 = u->d1 = dist(k, o);
        return n;
    }
    k->solo = 1;
    k->btn = (int8_t)ui_hit(u->c, k->x0, k->y0);
    if (k->btn >= 0)
        n = put(ev, n, max, UI_DOWN, k->btn, k);
    return n;
}

int ui_feed(ui_t *u, const ft_report_t *r, const ft_map_t *m, uint32_t now_ms, ui_ev_t *ev, int max)
{
    const ui_cfg_t *c = u->c;
    int n = 0;
    for (uint8_t i = 0; i < FT_MAX_TOUCH; i++)
        u->k[i].seen = 0;
    for (uint8_t i = 0; i < FT_MAX_TOUCH; i++) {
        const ft_point_t *p = &r->p[i];
        ui_touch_t *k = 0;
        if (p->ev == FT_EV_NONE || (i >= r->n && p->ev != FT_EV_LIFT))
            continue;                                   /* место пусто */
        for (uint8_t j = 0; j < FT_MAX_TOUCH; j++)
            if (u->k[j].used && u->k[j].id == p->id)
                k = &u->k[j];
        if (p->ev == FT_EV_LIFT) {
            if (k) {
                k->seen = 1;
                n = lift(u, k, ev, n, max);
            }
            continue;
        }
        if (!k) {
            if (c->strict && p->ev != FT_EV_DOWN)
                continue;                               /* «контакт» без «нажатия» — строгий разбор его не видит */
            for (uint8_t j = 0; j < FT_MAX_TOUCH && !k; j++)
                if (!u->k[j].used)
                    k = &u->k[j];
            if (!k)
                continue;
            *k = (ui_touch_t){1, p->id, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, now_ms, now_ms};
            ft_to_screen(m, p->x, p->y, &k->x0, &k->y0);
        }
        k->seen = 1;
        ft_to_screen(m, p->x, p->y, &k->x, &k->y);
        k->t = now_ms;
        if (!k->ok && (!c->confirm || p->ev == FT_EV_CONTACT))
            n = accept(u, k, ev, n, max);               /* антидребезг: ждать «контакт» — второй отчёт */
        if (!k->ok)
            continue;
        if (absd(k->x - k->x0) > c->slop || absd(k->y - k->y0) > c->slop)
            k->moved = 1;
        if (k->btn >= 0) {
            const ui_rect_t *b = &c->btn[k->btn];
            if (k->moved || k->x < b->x || k->x >= b->x + b->w || k->y < b->y || k->y >= b->y + b->h) {
                n = put(ev, n, max, UI_CANCEL, k->btn, k);   /* повёл палец — это уже не щелчок, а свайп */
                k->btn = -1;
            } else if (!k->held && now_ms - k->t0 >= c->hold_ms) {
                k->held = 1;
                n = put(ev, n, max, UI_HOLD, k->btn, k);
            }
        }
    }
    if (u->pinch && u->k[0].used && u->k[1].used)
        u->d1 = dist(&u->k[0], &u->k[1]);
    if (!c->strict)
        for (uint8_t j = 0; j < FT_MAX_TOUCH; j++)
            if (u->k[j].used && !u->k[j].seen)
                n = lift(u, &u->k[j], ev, n, max);      /* ID пропал без «отпускания» — отчёт проскочил */
    return n;
}

fw22p.c — прошивка STM32F407: I2C2 на PB10/PB11, INT на EXTI4, RST, цикл с проверкой уровня INT.

/* fw22p.c — раздел 22п: тач FT6236 на STM32F407, 168 МГц, APB1 42 МГц. Дисплей — ILI9341 из 22о (SPI1, PA5–PA7,
 * PB6–PB8, PB0, PB1), поэтому тач — на I2C2: PB10 — SCL, PB11 — SDA (AF4, открытый сток), 400 кГц.
 * INT тача — PC4: вход с подтяжкой, EXTI4 по спаду. RST тача — PC5: выход. SysTick — 1 мс.
 * Экран повёрнут в альбомную (MADCTL 0x68, поворот 1 в 22о); кнопки — сетка 3 × 3 по экрану 320 × 240.
 * Тактирование до 168 МГц — как в разделе 6 (здесь не повторяется). */
#include "stm32f4xx.h"
#include "ft22p.h"

#define APB1_MHZ 42u
#define I2C_FAST_CCR (APB1_MHZ * 1000000u / (3u * 400000u))   /* F/S = 1, DUTY = 0: Tlow = 2 · Thigh (RM0090, с. 870) */
#define I2C_FAST_TRISE (APB1_MHZ * 300u / 1000u + 1u)         /* фронт до 300 нс: 12,6 → 12 + 1 */
#define I2C_SPIN 20000u
#define INT_PIN 4u
#define RST_PIN 5u

_Static_assert(I2C_FAST_CCR == 35u, "CCR = 35: 42 МГц / (3 · 35) = 400 кГц");
_Static_assert(I2C_FAST_TRISE == 13u, "TRISE = 13");
_Static_assert(EXTI4_IRQn == 10, "EXTI4 — прерывание 10 (RM0090, с. 372)");

static const ft_cfg_t CFG = {.gmode = 1u, .th_group = 60u, .period_active = 60u};
static const ft_map_t MAP = {.madctl = 0x68u};
static const ui_rect_t BTN[9] = {
    {6, 6, 94, 68},   {112, 6, 94, 68},   {218, 6, 94, 68},
    {6, 86, 94, 68},  {112, 86, 94, 68},  {218, 86, 94, 68},
    {6, 166, 94, 68}, {112, 166, 94, 68}, {218, 166, 94, 68},
};
static const ui_cfg_t UC = {BTN, 9u, 1u, 0u, 500u, 12u, 40u, 400u, 30u};

static ft_t T;
static ui_t U;
static volatile uint8_t int_flag;
static volatile uint32_t ms;
volatile uint8_t last_ev, last_btn;             /* что увидел обработчик: для отладчика */
volatile uint32_t n_ev[11];

/* ── I2C2, ведущий, опросом; на каждом ожидании — предел ── */
static int wait1(uint32_t mask)
{
    for (uint32_t n = 0; n < I2C_SPIN; n++) {
        uint32_t sr = I2C2->SR1;
        if (sr & I2C_SR1_AF) {
            I2C2->SR1 = (uint16_t)~I2C_SR1_AF;
            return 1;
        }
        if (sr & mask)
            return 0;
    }
    return 2;
}

static void stop(void)
{
    I2C2->CR1 |= I2C_CR1_STOP;
    while (I2C2->CR1 & I2C_CR1_STOP) {
    }
}

static int start(uint8_t a8)
{
    I2C2->CR1 |= I2C_CR1_START;
    if (wait1(I2C_SR1_SB))
        return 1;
    I2C2->DR = a8;
    if (wait1(I2C_SR1_ADDR)) {
        stop();
        return 1;
    }
    return 0;
}

int ft_port_write(uint8_t addr7, const uint8_t *buf, uint32_t n)
{
    if (start((uint8_t)(addr7 << 1)))
        return 1;
    (void)I2C2->SR1;
    (void)I2C2->SR2;
    for (uint32_t i = 0; i < n; i++) {
        if (wait1(I2C_SR1_TXE)) {
            stop();
            return 1;
        }
        I2C2->DR = buf[i];
    }
    int e = n ? wait1(I2C_SR1_BTF) : 0;
    stop();
    return e;
}

/* Приём по RM0090: один байт — ACK снять до снятия ADDR; два — POS; больше двух — последние три через BTF (с. 851).
 * Отчёт тача — 13 байт одним приёмом. */
int ft_port_read(uint8_t addr7, uint8_t reg, uint8_t *buf, uint32_t n)
{
    if (start((uint8_t)(addr7 << 1)))
        return 1;
    (void)I2C2->SR1;
    (void)I2C2->SR2;
    if (wait1(I2C_SR1_TXE)) {
        stop();
        return 1;
    }
    I2C2->DR = reg;
    if (wait1(I2C_SR1_BTF) || start((uint8_t)((addr7 << 1) | 1u))) {
        stop();
        return 1;
    }
    int e = 0;
    if (n == 1) {
        I2C2->CR1 &= ~I2C_CR1_ACK;
        (void)I2C2->SR1;
        (void)I2C2->SR2;
        I2C2->CR1 |= I2C_CR1_STOP;
        e = wait1(I2C_SR1_RXNE);
        buf[0] = (uint8_t)I2C2->DR;
    } else if (n == 2) {
        I2C2->CR1 |= I2C_CR1_POS;
        I2C2->CR1 &= ~I2C_CR1_ACK;
        (void)I2C2->SR1;
        (void)I2C2->SR2;
        e = wait1(I2C_SR1_BTF);
        I2C2->CR1 |= I2C_CR1_STOP;
        buf[0] = (uint8_t)I2C2->DR;
        buf[1] = (uint8_t)I2C2->DR;
    } else {
        (void)I2C2->SR1;
        (void)I2C2->SR2;
        uint32_t i = 0;
        for (; i < n - 3u && !e; i++) {
            e = wait1(I2C_SR1_RXNE);
            buf[i] = (uint8_t)I2C2->DR;
        }
        e |= wait1(I2C_SR1_BTF);
        I2C2->CR1 &= ~I2C_CR1_ACK;
        buf[i++] = (uint8_t)I2C2->DR;
        I2C2->CR1 |= I2C_CR1_STOP;
        buf[i++] = (uint8_t)I2C2->DR;
        e |= wait1(I2C_SR1_RXNE);
        buf[i] = (uint8_t)I2C2->DR;
    }
    while (I2C2->CR1 & I2C_CR1_STOP) {
    }
    I2C2->CR1 = (I2C2->CR1 & ~I2C_CR1_POS) | I2C_CR1_ACK;
    return e;
}

void ft_port_delay_ms(uint32_t d)
{
    uint32_t t0 = ms;
    while (ms - t0 < d) {
    }
}

void ft_port_rst(int level)
{
    GPIOC->BSRR = level ? 1u << RST_PIN : 1u << (RST_PIN + 16u);
}

int ft_port_int_low(void)
{
    return !(GPIOC->IDR & (1u << INT_PIN));
}

void SysTick_Handler(void)
{
    ms++;
}

void EXTI4_IRQHandler(void)
{
    EXTI->PR = EXTI_PR_PR4;
    int_flag = 1;
}

static void board_init(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOCEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
    SysTick_Config(168000u);

    /* PB10, PB11 — AF4, открытый сток; подтяжки — на модуле (внутренние 30–50 кОм для 400 кГц слабы) */
    GPIOB->MODER = (GPIOB->MODER & ~(0xFu << 20)) | 0xAu << 20;
    GPIOB->OTYPER |= 3u << 10;
    GPIOB->OSPEEDR |= 0xFu << 20;
    GPIOB->AFR[1] = (GPIOB->AFR[1] & ~(0xFFu << 8)) | 0x44u << 8;
    /* PC4 — вход с подтяжкой (INT — активный ноль), PC5 — выход RST, сразу в 0 */
    GPIOC->BSRR = 1u << (RST_PIN + 16u);
    GPIOC->MODER = (GPIOC->MODER & ~(0xFu << 8)) | 1u << 10;
    GPIOC->PUPDR = (GPIOC->PUPDR & ~(3u << 8)) | 1u << 8;

    I2C2->CR1 = I2C_CR1_SWRST;
    I2C2->CR1 = 0;
    I2C2->CR2 = APB1_MHZ;
    I2C2->CCR = I2C_CCR_FS | I2C_FAST_CCR;
    I2C2->TRISE = I2C_FAST_TRISE;
    I2C2->CR1 = I2C_CR1_PE | I2C_CR1_ACK;
}

/* EXTI4 от порта C, по спаду. Включать после ft_init: тогда первый отчёт уже читаем по уровню, а не по спаду. */
static void int_on(void)
{
    SYSCFG->EXTICR[1] = (SYSCFG->EXTICR[1] & ~SYSCFG_EXTICR2_EXTI4) | SYSCFG_EXTICR2_EXTI4_PC;
    EXTI->FTSR |= EXTI_FTSR_TR4;
    EXTI->PR = EXTI_PR_PR4;
    EXTI->IMR |= EXTI_IMR_MR4;
    NVIC_EnableIRQ(EXTI4_IRQn);
}

int main(void)
{
    ft_report_t r;
    ui_ev_t ev[8];
    board_init();
    if (ft_init(&T, &CFG) != FT_OK) {
        last_ev = 0xFFu;                        /* нет ответа на 0x38 или чужой код в 0xA8 */
        for (;;) {
        }
    }
    ui_reset(&U, &UC);
    int_on();
    for (;;) {
        __disable_irq();
        if (!int_flag && !ft_port_int_low()) { /* INT прижат, а спада не было — всё равно читать */
            __WFI();
            __enable_irq();
            continue;
        }
        int_flag = 0;
        __enable_irq();
        if (ft_read(&T, &r) != FT_OK)
            continue;
        int n = ui_feed(&U, &r, &MAP, ms, ev, 8);
        for (int i = 0; i < n && i < 8; i++) {
            last_ev = ev[i].type;
            last_btn = (uint8_t)ev[i].btn;
            n_ev[ev[i].type]++;
        }
    }
}

Дальше: раздел 43 «Аппаратная криптография: CRC, RNG, HASH, CRYP».

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