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) щипок: два пальца разводят или сводят
22п.1 Ёмкостный против резистивного: что меняется
Резистивная панель из 22к — два слоя, которые палец прижимает друг к другу; контроллер — это АЦП, а всё остальное делает программа: меряет, калибрует, гасит дребезг. Ёмкостная — сетка электродов под стеклом и свой контроллер, который сам сканирует сетку и отдаёт готовый отчёт: где пальцы, сколько их и что с каждым случилось. Отсюда разница:
- Два пальца. FT6x06 по драйверу ST видит до двух касаний
(
FT6206_MAX_DETECTABLE_TOUCH= 2), у каждого — свой ID. Щипок возможен, на резистивной панели — нет. - Калибровки по точкам нет. Контроллер отдаёт координаты в точках стекла, 12 бит на ось. Остаётся вопрос, как стекло тача приклеено к дисплею и как повёрнут экран, — это четыре случая и два зеркала (22п.4), а не матрица из трёх касаний.
- Нажим не нужен, стекло цело. Касание — изменение ёмкости, поэтому нет силы нажатия (вместо давления из 22к здесь «вес» и «площадь» касания) и нет износа верхнего слоя.
- Перчатка и вода. Резистивной панели всё равно, чем давят. Ёмкостной — нет: перчатка ослабляет сигнал, капля воды его даёт. Что из этого выходит при разных порогах, модель показывает в 22п.6.
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 Итоги
- FT6236/FT6206 — до двух касаний, адрес 0x38 (у ST — 0x70, сдвинутый); отчёт — 13 байт с 0x02 одним чтением: 370 мкс на 400 кГц, 1480 мкс на 100 кГц.
XH[7:6] — событие: 0 — нажатие, 1 — отпускание, 2 — контакт;YH[7:4] — ID;CHIP_IDу ST — 0x11, 0xCD или 0xD8.- Запуск: RST 10 мс в нуле, 40 мс паузы, 8 обменов, 16 байт, 690 мкс
шины, 50,69 мс всего; I2C2 на PB10/PB11,
CCR= 35,TRISE= 13. - Поворот: 08 — (gx, gy), 68 — (gy, 239 − gx), C8 — (239 − gx, 319 − gy), A8 — (319 − gy, gx); 0 расхождений на 1610 точках каждого поворота; без учёта поворота при 90° — 0 из 9.
- Обработчик: антидребезг по «контакту», удержание 500 мс, свайп от 40 точек за 400 мс, щипок от 30 точек; 66 из 66 в обеих ориентациях.
- INT против опроса: задержка 24,68 мс против 29,22 мс (опрос раз в 10 мс), без касания 0 чтений против 96 в секунду.
- Грабли: INT прижат до чтения — смотреть уровень; строгий разбор при
опросе раз в 50 мс — 3 из 20; событие из
P2_XHу ST V1.1.0 — 0 из 20; помеха ШИМ — 18 ложных щелчков без антидребезга и 0 с ним.
Полные исходники
Стенд 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».