22к. Резистивный тачскрин и контроллер XPT2046 по SPI: измерение X/Y, давление, калибровка по трём точкам и фильтрация дребезга
В разделе 22з на STM32F103 заработал цветной дисплей ST7735, в 22и появилась клавиатура на расширителе портов. Следующий шаг ввода — палец прямо на экране. Общий взгляд на сенсоры дан в 22а.6: резистивный — четыре провода на АЦП, ёмкостный — свой контроллер по I2C. Здесь — резистивная панель с контроллером XPT2046, какие стоят на дешёвых модулях дисплеев: как панель превращает касание в напряжение, что значит каждый бит команды XPT2046, как посадить его на тот же SPI1, что и дисплей, как читать X, Y и силу нажатия, чем гасить шум и дребезг, как откалибровать экран по трём касаниям и как из касаний получить события «нажал», «ведёт», «держит», «отпустил».
Опоры: RM0008 (DocID13902 Rev 17): таблица режимов выводов, с. 160;
AFIO_EXTICR1, с. 190; EXTI_PR, с. 212; конец передачи SPI — TXE,
потом BSY, с. 719; делитель BR и запрет менять его во время обмена,
с. 743; CPOL и CPHA, с. 744. DocID13587 Rev 17 (STM32F103x8/xB): PA5 —
SPI1_SCK, PA6 — SPI1_MISO, PA7 — SPI1_MOSI, PB0 и PB1 — без допуска
5 В, с. 29; такт SPI ведущего — до 18 МГц, с. 71. Документа на XPT2046 в
проекте нет. Поля байта команды, номера каналов X, Y, Z1 и Z2, режим
«16 тактов на преобразование», место 12 бит в ответе, давление
Z1 + 4096 − Z2 с порогом 100 и выключение после последней команды
сверены с драйвером input_xpt2046.c из Zephyr 4.4.2; там же сказано,
что XPT2046 похож на ADS7843 от TI и отличается нумерацией каналов.
Перестановка имён осей (CHX = 0x90, CHY = 0xD0) и коэффициенты
калибровки по трём точкам (An…Fn и Divider) — из файла XPT2046.h
в паке Clarinox Wireless 2.0.1; сопротивление слоя X для давления и
подтверждение PENIRQ давлением — из GUITDRV_ADS7846.h emWin в паке
MDK-Middleware Graphics 1.3.1. Предел такта 2 МГц, время выборки,
поведение PENIRQ во время преобразования и подтяжка PENIRQ — по
документации производителя, не проверено: в модели это допущения. Все
числа ниже — прогон драйвера _verify/touch22k/xpt22k.c на компьютере
(gcc 12.2.0) со своей моделью панели и XPT2046 на Python 3.11; прошивка
собрана armclang 6.24 под Cortex-M3.
Как это называется в документации
Оригинал Перевод resistive touch panel, 4-wire резистивная панель, четыре провода X+, X−, Y+, Y− выводы слоёв: X — между X+ и X−, Y — между Y+ и Y− touch resistance, Rtouch сопротивление в точке касания Z1, Z2 (pressure) два измерения тока через касание — давление DFR / SER (differential / single-ended reference) опорное — те же ключи / отдельное PD1, PD0 (power-down) выключаться ли между преобразованиями PENIRQ (pen interrupt) вывод «есть касание», активный ноль DCLK, DIN, DOUT, CS такт, данные в контроллер, из контроллера, выбор calibration matrix коэффициенты пересчёта кодов в точки экрана debouncing подавление дребезга tap, long press, drag касание, удержание, перетаскивание
22к.1 Панель и XPT2046: делитель напряжения и байт команды
Резистивная панель — два прозрачных слоя с проводящим покрытием, между ними зазор. Слой X имеет выводы X+ и X− на двух противоположных краях, слой Y — Y+ и Y− на двух других. Нажатие соединяет слои в одной точке. Если подать VCC на X+ и землю на X−, вдоль слоя X пойдёт ровный спад напряжения, и точка касания окажется отводом делителя: её потенциал — доля пути от X− к X+. Слой Y в это время ни к чему не подключён и через касание принимает тот же потенциал — на Y+ его и меряют. Для Y роли меняются. Чтобы узнать силу нажатия, ток пускают через само касание: VCC на Y+, земля на X−, а меряют X+ (Z1) и Y− (Z2). В покое Y− на земле, а X+ подтянут к питанию: касание тянет X+ к нулю — это PENIRQ.
XPT2046 — 12-битный АЦП с этими ключами внутри. Каждое преобразование начинается байтом команды, старшим битом вперёд:
/* ── байт команды: S A2 A1 A0 MODE SER/DFR PD1 PD0 ────────────────────── */
#define XPT_S 0x80u /* старт: первая единица после CS = 0 */
#define XPT_A(ch) ((uint8_t)(((ch) & 7u) << 4))
#define XPT_MODE8 0x08u /* 1 — 8 бит, 0 — 12 бит */
#define XPT_SER 0x04u /* 1 — однополярный вход, 0 — дифференциальный (здесь всегда 0) */
#define XPT_PD_ON 0x03u /* PD = 11: между преобразованиями не выключаться */
#define XPT_PD_IRQ 0x00u /* PD = 00: после преобразования выключиться — PENIRQ снова работает */
enum { XPT_CH_Y = 1, XPT_CH_Z1 = 3, XPT_CH_Z2 = 4, XPT_CH_X = 5 }; /* номера каналов — как в Zephyr */
#define XPT_X (XPT_S | XPT_A(XPT_CH_X)) /* 0xD0 */
#define XPT_Y (XPT_S | XPT_A(XPT_CH_Y)) /* 0x90 */
#define XPT_Z1 (XPT_S | XPT_A(XPT_CH_Z1)) /* 0xB0 */
#define XPT_Z2 (XPT_S | XPT_A(XPT_CH_Z2)) /* 0xC0 */
| Бит | 7 | 6…4 | 3 | 2 | 1…0 |
|---|---|---|---|---|---|
| Поле | S — старт | A2…A0 — канал | MODE: 1 — 8 бит | SER/DFR: 0 — дифференциальный | PD1 PD0 |
Номера каналов — как в Zephyr: Y = 1, Z1 = 3, Z2 = 4, X = 5; отсюда
команды 0xD0 (X), 0x90 (Y), 0xB0 (Z1) и 0xC0 (Z2) плюс биты PD. В файле
Clarinox те же коды названы наоборот: CHX = 0x90, CHY = 0xD0. Какой
из двух каналов идёт вдоль длинной стороны экрана, решает сборка модуля,
и калибровка (22к.5) всё равно переставит оси. SER/DFR = 0 —
дифференциальный режим: опорным служат те же ключи, что питают слой, и
падение на них не входит в код; Zephyr бит SINGLE_ENDED не ставит, этот
драйвер тоже.
Модель без шума и дрожания показала коды в углах стекла 128 × 160: в точке (0, 0) — X = 3892, Y = 322, в точке (127, 159) — X = 236, Y = 3774, в центре (64, 80) — 2057 и 2062. Ось X панели идёт вдоль y стекла и навстречу ей, а до 0 и 4095 коды не доходят: края слоя шире стекла. В примере emWin для панели 640 × 480 стоят такие же «неполные» пределы — 0x0053…0x0F51 по одной оси и 0x0F5E…0x0083, наоборот, по другой.
22к.2 Подключение к F103: общий SPI1 с дисплеем, свой CS, PENIRQ на EXTI
У тача пять сигналов: такт DCLK, данные DIN и DOUT, выбор CS и PENIRQ; подписи на платах модулей разные, здесь не проверялись. Такт и данные садятся на тот же SPI1, что и дисплей из 22з; MISO, который дисплею не был нужен, теперь нужен тачу.
| Тач | F103 | Что |
|---|---|---|
| DCLK | PA5 | такт SPI1, общий с дисплеем |
| DIN | PA7 | MOSI, общий |
| DOUT | PA6 | MISO: вход без подтяжки |
| CS | PB0 | свой выбор: выход, в покое 1 |
| PENIRQ | PB1 | PENIRQ: вход с подтяжкой (1000, с. 160), EXTI1 по спаду |
| CS дисплея | PA4 | в 22з держали в нуле — теперь им управляют |
PA5–PA7, PB0 и PB1 5 В не допускают (DocID13587, с. 29): тач и дисплей
питают от 3,3 В. EXTI1 привязывают к порту B полем EXTI1[3:0] регистра
AFIO_EXTICR1 = 0001 (с. 190).
Скорость и режим — свои у каждого. Дисплей в 22з работает на 9 МГц
(72 МГц / 8) в режиме 3. Тачу нужен медленный такт: на выборку у
контроллера всего несколько тактов после номера канала, а узел, который
он меряет, заряжается через касание и слой. Тач здесь работает на
72 МГц / 64 = 1,125 МГц (по документации производителя предел — около
2 МГц, не проверено) в режиме 0, как в Zephyr. Делитель BR, CPOL и
CPHA меняют только между обменами (RM0008, с. 743–744): сначала
дождаться конца передачи дисплея — TXE, потом BSY (с. 719), — снять
его CS, выключить SPI, перенастроить:
static void bus_take(uint8_t who)
{
spi_idle();
if (owner == who)
return;
TFT_CS_HIGH();
T_CS_HIGH();
SPI1->CR1 = 0;
SPI1->CR1 = who == BUS_TFT ? CR1_TFT : CR1_TOUCH;
SPI1->CR1 |= SPI_CR1_SPE;
wide = 0;
owner = who;
if (who == BUS_TFT)
TFT_CS_LOW(); /* дисплей держит CS, пока шину не попросит тач */
}
Каждая функция шины дисплея начинает с bus_take(BUS_TFT), порт тача —
с bus_take(BUS_TOUCH). CS дисплея опускается, когда шину берёт дисплей,
и поднимается, только когда её просит тач: DMA строки пикселей может ещё
идти после возврата из функции (22з.1).
Что даёт медленный такт, модель показала на неподвижном пальце в центре. Ключи переключаются после номера канала, выборка — в конце байта команды (допущение модели, четыре такта); узел заряжается через Rt и полслоя на 1 нФ. Ошибки отсчётов X в единицах кода (одна точка стекла — 23 единицы):
| Такт SPI | Rt = 600 Ом: 1-й / 2-й отсчёт | медиана 2…6 | Rt = 3000 Ом: 1-й / 2-й | медиана 2…6 |
|---|---|---|---|---|
| 562,5 кГц | 0 / 0 | 0 | −208 / 0 | 0 |
| 1,125 МГц | −13 / 0 | 0 | −653 / −7 | 0 |
| 2,25 МГц | −163 / 0 | 0 | −1159 / −117 | −1 |
| 4,5 МГц | −578 / −4 | 0 | −1544 / −491 | −50 |
| 9 МГц | −1090 / −86 | −1 | −1782 / −1005 | −319 |
| 18 МГц | −1498 / −421 | −33 | −1915 / −1437 | −810 |
Первый отсчёт после переключения ключей врёт всегда — его отбрасывают. На 1,125 МГц второй уже верен даже при лёгком нажатии; на 9 МГц, скорости дисплея, медиана при Rt = 3000 Ом уходит на 319 единиц — 14 точек.
22к.3 X, Y и давление: пачка в 16 тактов на преобразование
Классический обмен — байт команды и два байта ответа, 24 такта на преобразование. XPT2046 позволяет начать следующую команду, пока выходят младшие биты прошлого результата: 16 тактов на преобразование, так делает Zephyr. Драйвер берёт за одну пачку шесть X, шесть Y, два Z1 и один Z2 — 15 преобразований:
int xpt_read(xpt_t *d, xpt_sample_t *s)
{
uint8_t tx[XPT_FRAME_MAX] = {0}, rx[XPT_FRAME_MAX];
uint16_t vx[XPT_NMAX], vy[XPT_NMAX], sx, sy;
uint8_t n = d->n, m = 0, zr = d->skip ? 2u : 1u;
uint32_t len;
for (uint8_t i = 0; i < n; i++)
tx[2u * m++] = XPT_X | XPT_PD_ON;
for (uint8_t i = 0; i < n; i++)
tx[2u * m++] = XPT_Y | XPT_PD_ON;
for (uint8_t i = 0; i < zr; i++)
tx[2u * m++] = XPT_Z1 | XPT_PD_ON;
tx[2u * m++] = XPT_Z2 | d->pd_last;
len = 2u * m + 1u;
xpt_port_select(1);
xpt_port_xfer(tx, rx, len);
xpt_port_select(0);
d->frames++;
d->bytes += len;
for (uint8_t i = 0; i < m; i++) /* бит 7 первого байта — BUSY, дальше 12 бит */
s->raw[i] = (uint16_t)((rx[2u * i + 1u] & 0x7Fu) << 5 | rx[2u * i + 2u] >> 3);
s->nraw = m;
for (uint8_t i = 0; i < n; i++) {
vx[i] = s->raw[i];
vy[i] = s->raw[n + i];
}
s->x = xpt_median(vx + d->skip, (uint8_t)(n - d->skip), &sx);
s->y = xpt_median(vy + d->skip, (uint8_t)(n - d->skip), &sy);
s->spread = sx > sy ? sx : sy;
s->z1 = s->raw[m - 2u];
s->z2 = s->raw[m - 1u];
/* R = Rx · X / 4096 · (Z2 / Z1 − 1) — сопротивление в точке касания: чем сильнее нажали, тем меньше */
if (s->z1 == 0u)
s->rt = XPT_NO_CONTACT;
else if (s->z2 <= s->z1)
s->rt = 0;
else
s->rt = (uint32_t)((uint64_t)d->rx_ohm * s->x * (uint32_t)(s->z2 - s->z1) / ((uint64_t)XPT_FULL * s->z1));
s->ok = (uint8_t)(s->rt <= d->rt_max && s->spread <= d->spread_max);
return s->ok;
}
Пачка — 31 байт, 220,4 мкс на 1,125 МГц; по 24 такта было бы 45 байт и 320 мкс. В модели все 15 кодов, разобранных драйвером, совпали с тем, что преобразовал контроллер. У последней команды PD = 00: после неё контроллер выключается и снова следит за касанием; у остальных PD = 11. Опрос раз в 10 мс занимает шину на 2,2 %.
Давление. Из схемы делителя (22к.1) ток через касание дают Z1 и Z2, а сопротивление касания — R = Rx · X / 4096 · (Z2 / Z1 − 1), где Rx — сопротивление слоя X между X+ и X−: его берут из описания панели или меряют омметром (в модели 400 Ом). emWin для давления тоже просит сопротивление слоя X. Модель в трёх точках:
| Точка стекла | Rt = 300 Ом | 600 Ом | 1200 Ом | 3000 Ом |
|---|---|---|---|---|
| центр (64, 80): R драйвера | 299 | 599 | 1201 | 2935 |
| центр: Z1 + 4096 − Z2 | 2558 | 1860 | 1202 | 589 |
| угол (8, 8): Z1 + 4096 − Z2 | 3061 | 2443 | 1740 | 934 |
| угол (120, 152): Z1 + 4096 − Z2 | 1106 | 639 | 347 | 155 |
Формула возвращает Rt с точностью до единиц ом в любой точке. Мера Zephyr Z1 + 4096 − Z2 (порог по умолчанию 100) проще, но при одном и том же Rt = 600 Ом в двух углах она отличается в 3,8 раза: 2443 и 639. Порог по ней или пропустит лёгкие нажатия в одном углу, или примет их в другом. Драйвер считает касание годным, если R не больше 2000 Ом; при отпущенной панели Z1 = 0, Z2 = 4095, и R — «нет контакта».
22к.4 Шум и дребезг: отбросить первый, медиана, порог, счёт пачек
Палец садится на панель не сразу: первые миллисекунды контакт рвётся и снова замыкается, слабо прижатое касание имеет большое Rt, а разорванный слой висит и ловит наводку. То же при отпускании. Драйвер бьётся с этим на трёх уровнях: внутри пачки — отбросить первый отсчёт каждой оси и взять медиану пяти остальных, их разброс больше 24 единиц — пачка негодна; по давлению — R больше 2000 Ом — пачка негодна; между пачками — «нажал» только после двух годных пачек подряд, «отпустил» — после трёх негодных.
uint16_t xpt_median(uint16_t *v, uint8_t n, uint16_t *spread)
{
for (uint8_t i = 1; i < n; i++) {
uint16_t t = v[i];
uint8_t j = i;
while (j > 0 && v[j - 1] > t) {
v[j] = v[j - 1];
j--;
}
v[j] = t;
}
*spread = (uint16_t)(v[n - 1] - v[0]);
return v[n / 2];
}
Модель коснулась панели 150 раз пальцем в случайных местах, по 80…300 мс, с дребезгом 2…8 мс при нажатии и 1…5 мс при отпускании; опрос — раз в 10 мс и по PENIRQ. Точка считается дикой, если отстоит от пальца больше чем на 4 точки стекла:
| Способ | Касаний замечено | Лишних «нажал» | Диких точек | Худшая ошибка | «Нажал» после касания: среднее / худшее | «Отпустил» после подъёма |
|---|---|---|---|---|---|---|
| один отсчёт, без порогов | 150 | 74 | 202 из 2744 | 177,6 точки | 0,0 / 0,0 мс | 16,8 мс |
| медиана 5 из 6 | 150 | 71 | 10 из 2742 | 174,4 точки | 0,1 / 1,6 мс | 16,1 мс |
| + пороги давления и разброса | 150 | 1 | 0 из 2704 | 1,5 точки | 1,5 / 4,5 мс | 2,9 мс |
| + две и три пачки подряд | 150 | 0 | 0 из 2452 | 1,4 точки | 18,3 / 20,0 мс | 22,8 мс |
Медиана убирает шум внутри пачки, но не дребезг: пачка, снятая в момент разрыва, вся «висит». Дребезг гасит порог давления, а последние лишние нажатия — счёт пачек. Плата — около 18 мс до «нажал»: две пачки по 10 мс.
22к.5 Калибровка по трём точкам
Коды панели — не точки экрана: ось X панели может идти вдоль y экрана,
навстречу, с наклоном, а края слоя шире стекла. Всё это вместе — одно
аффинное преобразование: x = (a·X + b·Y + c) / div и
y = (d·X + e·Y + f) / div. Шесть неизвестных — три касания: программа
рисует крест, ждёт касания и берёт средние коды годных пачек нажатия.
Система 3 × 3 решается по Крамеру в целых числах — без плавающей
запятой, которой у Cortex-M3 нет; c и f не помещаются в 32 бита:
int cal_solve(cal_t *k, const int16_t sx[3], const int16_t sy[3], const uint16_t tx[3], const uint16_t ty[3])
{
int64_t X0 = tx[0], X1 = tx[1], X2 = tx[2], Y0 = ty[0], Y1 = ty[1], Y2 = ty[2];
int64_t div = (X0 - X2) * (Y1 - Y2) - (X1 - X2) * (Y0 - Y2);
if (div == 0)
return 0;
k->div = (int32_t)div;
k->a = (int32_t)((sx[0] - sx[2]) * (Y1 - Y2) - (sx[1] - sx[2]) * (Y0 - Y2));
k->b = (int32_t)((X0 - X2) * (sx[1] - sx[2]) - (sx[0] - sx[2]) * (X1 - X2));
k->c = Y0 * (X2 * sx[1] - X1 * sx[2]) + Y1 * (X0 * sx[2] - X2 * sx[0]) + Y2 * (X1 * sx[0] - X0 * sx[1]);
k->d = (int32_t)((sy[0] - sy[2]) * (Y1 - Y2) - (sy[1] - sy[2]) * (Y0 - Y2));
k->e = (int32_t)((X0 - X2) * (sy[1] - sy[2]) - (sy[0] - sy[2]) * (X1 - X2));
k->f = Y0 * (X2 * sy[1] - X1 * sy[2]) + Y1 * (X0 * sy[2] - X2 * sy[0]) + Y2 * (X1 * sy[0] - X0 * sy[1]);
return 1;
}
В модели панель наклеена с поворотом 1,2°, и ось X панели идёт вдоль y стекла навстречу ей. Кресты — в точках (13, 16), (115, 80) и (64, 144); на каждое касание пришлось 37, 37 и 38 годных пачек. Затем модель коснулась сетки 15 × 19 = 285 точек, от края до края стекла:
| Пересчёт | Средняя ошибка | Худшая | Середина: средняя / худшая | Полоса 10 точек у края: средняя / худшая |
|---|---|---|---|---|
| без калибровки: X · 128 / 4096, Y · 160 / 4096 | 78,73 | 150,44 | 60,20 / 114,00 | 104,20 / 150,44 |
| по двум касаниям, оси переставлены вручную | 1,60 | 4,59 | 1,22 / 3,82 | 2,11 / 4,59 |
| по трём касаниям | 0,29 | 2,00 | 0,10 / 1,00 | 0,56 / 2,00 |
Две точки на ось — так устроены пределы min/max в Zephyr и xPhys0/xPhys1 в emWin — исправляют масштаб, сдвиг и направление оси, но не наклон панели. Три точки убирают и его. Остаток у краёв — сама панель: у края поле слоя неравномерно (в модели координата сжата до 3 %), и прямая калибровка его не исправит. Целочисленный пересчёт разошёлся с плавающей запятой не больше чем на 0,5 точки на всех 285 точках.
Хранение во флеше. Коэффициенты — 11 слов, 44 байта: метка 0x4B32325A,
a, b, d, e, c и f по два слова, div и CRC-32 первых 40 байт
(CRC-32 строки «123456789» = 0xCBF43926 — проверочное значение). Прошивка
пишет их в последнюю страницу флеша полусловами, как в 11.5, и при старте
читает: годная запись — 0; один перевёрнутый бит — −2; стёртая страница
(все 0xFFFFFFFF) — −1, и тогда калибровка запускается заново. Если
настройки меняются часто, их кладут в журнал из 11.8.
Поворот экрана. После tft_rotate из 22з те же коды должны давать
точки в новых осях. cal_rotate пересчитывает коэффициенты без новых
касаний. Без пересчёта средняя ошибка в повороте на 90° — 84,36 точки, на
180° — 116,47, на 270° — 84,55; с ним во всех трёх — 0,29, худшая 2,00.
22к.6 Жесты и PENIRQ: нажал, ведёт, держит, отпустил
Пока касания нет, шина молчит: контроллер в покое, PENIRQ в единице. Первый спад PENIRQ будит основной цикл, дальше пачка идёт раз в 10 мс, пока касание не кончится. Шаг выдаёт не больше одного события: «нажал» с точкой, «ведёт», когда точка ушла от места нажатия дальше 4 точек стекла, «держит» через 500 мс без сдвига, «отпустил» с пометкой «это был тап»:
int tp_step(tp_t *t, int irq, uint32_t now_ms, tp_event_t *ev)
{
ev->type = TP_NONE;
ev->tap = 0;
if (!t->active) {
if (!irq)
return TP_IDLE;
t->active = 1;
t->good = t->bad = 0;
if (t->manage_irq)
xpt_port_irq(0); /* пачки сами дёргают PENIRQ — не слушать его */
}
if (xpt_read(t->dev, &t->s)) {
t->good = (uint8_t)(t->good < 255u ? t->good + 1u : 255u);
t->bad = 0;
cal_apply(&t->k, t->s.x, t->s.y, &t->x, &t->y);
} else {
t->bad = (uint8_t)(t->bad < 255u ? t->bad + 1u : 255u);
t->good = 0;
}
if (!t->pressed) {
if (t->good >= t->k_down) {
t->pressed = 1;
t->dragging = t->held = 0;
t->x0 = t->x;
t->y0 = t->y;
t->t_down = now_ms;
t->cx = t->cy = t->cn = 0;
ev->type = TP_DOWN;
} else if (t->bad >= t->k_up) {
t->active = 0; /* касания нет: снова ждать PENIRQ */
if (t->manage_irq)
xpt_port_irq(1);
return TP_IDLE;
}
} else if (t->good) {
t->cx += t->s.x;
t->cy += t->s.y;
t->cn++;
if (!t->dragging && (far(t->x, t->x0, t->slop) || far(t->y, t->y0, t->slop)))
t->dragging = 1;
if (t->dragging)
ev->type = TP_MOVE;
else if (!t->held && now_ms - t->t_down >= t->hold_ms) {
t->held = 1;
ev->type = TP_HOLD;
}
} else if (t->bad >= t->k_up) {
t->pressed = 0;
ev->type = TP_UP;
ev->tap = (uint8_t)(!t->dragging && !t->held);
}
ev->x = t->x;
ev->y = t->y;
return TP_ACTIVE;
}
На время касания EXTI запрещён: каждая пачка сама уводит PENIRQ в
единицу и роняет его в конце, когда контроллер возвращается в покой (в
модели; по документации производителя, не проверено).
Разрешая EXTI снова, прошивка сначала пишет единицу в EXTI_PR: бит
ставится выбранным фронтом и снимается записью единицы (RM0008, с. 212),
а спад от последней пачки мог уже прийти.
В модели 20 тапов по 100…250 мс, 10 удержаний по 0,8…1 с и 10 перетаскиваний на 30…60 точек распознаны все: 20 из 20, 10 из 10 и 10 из 10; конец перетаскивания — в среднем 0,8 точки от пальца, худший — 1,8. С порогом сдвига 0 вместо 4 из 20 тапов тапами остались 2, а из 10 удержаний — ни одного: дрожание пальца в одну точку уже «перетаскивание».
Как обращаться с PENIRQ, модель проверила на 10 касаниях по 300 мс за 8,8 с:
| Прошивка | «Нажал» | Пачек | Из них без касания | Шина |
|---|---|---|---|---|
| EXTI запрещён на время касания, PD = 00 в конце | 10 | 340 | 32 | 0,85 % |
| EXTI всегда разрешён | 10 | 36 897 | 23 288 | 92,70 % |
| у последней команды PD = 11 | 1 | 34 | 4 | 0,09 % |
С разрешённым EXTI каждая пачка своим концом вызывает следующую: 4205 пачек в секунду, и не только во время касания — в модели после пачки висящий слой X заряжается через подтяжку десятки микросекунд, и PENIRQ успевает дать спад. С PD = 11 у последней команды контроллер не возвращается в покой, PENIRQ молчит, и из 10 касаний замечено одно — первое.
22к.7 Грабли
Тач на скорости дисплея врёт при лёгком нажатии. Шину перевели на
тач, а BR оставили от дисплея — 9 МГц. В модели при Rt = 1800 Ом
ошибка в трёх точках — 8,6, 4,0 и 4,1 точки стекла, на 1,125 МГц —
0,0, 1,0 и 0,0. Медленный такт и отброшенный первый отсчёт (22к.2).
На экране мусор в местах, которых никто не рисовал. CS дисплея, как в 22з, держат в нуле, и байты тача уходят дисплею как пиксели (D/C = 1 от прошлой картинки). В модели 100 пачек опроса оставили в памяти кадра 1500 чужих пикселей. CS дисплея поднимают до того, как опустить CS тача.
BR поменяли посреди передачи DMA. Последние слова строки уходят на
скорости тача. Сначала конец передачи (TXE, потом BSY), потом
перенастройка при SPE = 0 (RM0008, с. 743–744).
Шина занята, даже когда экран не трогают. EXTI оставили разрешённым,
и PENIRQ сам будит следующую пачку: 92,70 % шины в модели. Запрещать
EXTI на время касания и снимать EXTI_PR перед разрешением.
После первого касания тач «умер». У последней команды пачки PD = 11: контроллер не возвращается в покой, PENIRQ не работает. Последняя команда — с PD = 00, как в Zephyr.
Одно касание — два-три «нажал», точки улетают в угол. Дребезг контакта: без порога давления 74 лишних «нажал» на 150 касаний и точки до 177,6 точки от пальца (22к.4).
Оси перепутаны, после поворота экрана палец «промахивается».
Названия каналов X и Y у разных драйверов противоположны (Zephyr и
Clarinox), панель клеят как угодно. Калибровка по трём точкам переставляет
оси сама; после tft_rotate нужен cal_rotate, иначе ошибка — 84…116
точек в среднем.
У краёв точность хуже. Поле слоя у шин неравномерно: после калибровки худшая ошибка в середине — 1 точка, у края — 2. Кнопки у самого края экрана делают крупнее, кресты калибровки ставят на 10 % от краёв.
Ёмкостный экран так не читается. У ёмкостного своего делителя нет: контроллер на стекле сам считает касания и отдаёт координаты по I2C (22а.6). XPT2046 и всё, что выше, — только для резистивных панелей.
22к.8 Итоги
- Резистивная панель — два слоя-делителя: X меряют на Y+, Y — на X+, ток через касание — Z1 на X+ и Z2 на Y−; в покое касание роняет PENIRQ.
- Байт команды XPT2046: S, A2…A0, MODE, SER/DFR, PD1 PD0; X — 0xD0, Y — 0x90, Z1 — 0xB0, Z2 — 0xC0 (номера каналов Zephyr).
- Пачка в 16 тактов на преобразование: 15 преобразований, 31 байт, 220,4 мкс на 1,125 МГц вместо 45 байт и 320 мкс; опрос раз в 10 мс — 2,2 % шины.
- Общий SPI1 с дисплеем: свой CS у каждого, 1,125 МГц и режим 0 для тача против 9 МГц и режима 3 для дисплея; первый отсчёт после ключей врёт на 13 единиц на 1,125 МГц и на 1090 на 9 МГц.
- Давление: R = Rx · X / 4096 · (Z2 / Z1 − 1) — от 299 до 2935 Ом при настоящих 300…3000; мера Z1 + 4096 − Z2 зависит от места в 3,8 раза.
- Отброс первого, медиана, пороги 2000 Ом и 24 единицы, две и три пачки подряд: 0 лишних «нажал» и 0 диких точек на 150 касаний вместо 74 и 202.
- Три точки: средняя ошибка 0,29 точки, худшая 2,00 (у края); две точки —
1,60 и 4,59; без калибровки — 78,73. Коэффициенты — 44 байта во флеше
с CRC-32; при повороте экрана —
cal_rotate. - Жесты: тап, удержание и перетаскивание — 20, 10 и 10 из 20, 10 и 10; EXTI на время касания запрещён: 0,85 % шины вместо 92,70 %.
Полные исходники
xpt22k.h — байт команды XPT2046, драйвер, калибровка и события: типы и прототипы.
/* xpt22k.h — раздел 22к: резистивный тачскрин на контроллере XPT2046 без регистров микроконтроллера.
* Драйвер говорит с XPT2046 через функции порта xpt_port_*: в прошивке fw22k.c их делает SPI1 STM32F103
* (общий с дисплеем из 22з), в стенде host22k.c — строки к модели model22k.py.
* Поля байта команды, номера каналов X, Y, Z1, Z2, «16 тактов на преобразование» и разбор 12-битного
* результата — как в драйвере input_xpt2046.c из Zephyr 4.4.2; документа на XPT2046 в проекте нет. */
#ifndef XPT22K_H
#define XPT22K_H
#include <stdint.h>
/* ── байт команды: S A2 A1 A0 MODE SER/DFR PD1 PD0 ────────────────────── */
#define XPT_S 0x80u /* старт: первая единица после CS = 0 */
#define XPT_A(ch) ((uint8_t)(((ch) & 7u) << 4))
#define XPT_MODE8 0x08u /* 1 — 8 бит, 0 — 12 бит */
#define XPT_SER 0x04u /* 1 — однополярный вход, 0 — дифференциальный (здесь всегда 0) */
#define XPT_PD_ON 0x03u /* PD = 11: между преобразованиями не выключаться */
#define XPT_PD_IRQ 0x00u /* PD = 00: после преобразования выключиться — PENIRQ снова работает */
enum { XPT_CH_Y = 1, XPT_CH_Z1 = 3, XPT_CH_Z2 = 4, XPT_CH_X = 5 }; /* номера каналов — как в Zephyr */
#define XPT_X (XPT_S | XPT_A(XPT_CH_X)) /* 0xD0 */
#define XPT_Y (XPT_S | XPT_A(XPT_CH_Y)) /* 0x90 */
#define XPT_Z1 (XPT_S | XPT_A(XPT_CH_Z1)) /* 0xB0 */
#define XPT_Z2 (XPT_S | XPT_A(XPT_CH_Z2)) /* 0xC0 */
#define XPT_FULL 4096u /* 12 бит */
#define XPT_NMAX 8u /* отсчётов на ось в пачке — не больше */
#define XPT_CONV_MAX (2u * XPT_NMAX + 3u) /* X и Y по n, Z1 дважды, Z2 */
#define XPT_FRAME_MAX (2u * XPT_CONV_MAX + 1u) /* 16 тактов на преобразование + хвост */
#define XPT_NO_CONTACT 0xFFFFFFFFu
typedef struct {
uint8_t n; /* отсчётов на ось за пачку, 1…8 */
uint8_t skip; /* сколько первых отсчётов оси отбросить */
uint8_t pd_last; /* PD у последней команды пачки: XPT_PD_IRQ */
uint16_t spread_max; /* разброс оставшихся отсчётов оси больше — пачка негодна */
uint16_t rx_ohm; /* сопротивление слоя X между X+ и X− */
uint32_t rt_max; /* сопротивление касания больше — нажатия нет */
uint32_t frames, bytes; /* пачек и байтов по SPI */
} xpt_t;
typedef struct {
uint16_t x, y; /* медианы осей, 0…4095 */
uint16_t z1, z2;
uint16_t spread; /* наибольший разброс по осям X и Y */
uint32_t rt; /* сопротивление касания, Ом; XPT_NO_CONTACT — контакта нет */
uint8_t ok; /* 1 — касание годное: давление и разброс в пределах */
uint8_t nraw;
uint16_t raw[XPT_CONV_MAX]; /* все коды пачки по порядку — для стенда */
} xpt_sample_t;
/* ── порт: в прошивке — SPI1 и EXTI, в стенде — строки к модели ───────── */
void xpt_port_select(int on); /* 1 — шину тачу (скорость, режим) и CS тача в 0; 0 — CS в 1 */
void xpt_port_xfer(const uint8_t *tx, uint8_t *rx, uint32_t n); /* n байт туда и обратно */
void xpt_port_irq(int on); /* EXTI линии PENIRQ: 1 — снять ожидание и разрешить, 0 — запретить */
/* ── драйвер ──────────────────────────────────────────────────────────── */
void xpt_init(xpt_t *d); /* n = 6, skip = 1, пороги — по умолчанию */
int xpt_read(xpt_t *d, xpt_sample_t *s); /* одна пачка: X, Y, Z1, Z2; 1 — касание годное */
uint16_t xpt_median(uint16_t *v, uint8_t n, uint16_t *spread);
/* ── калибровка по трём точкам: экран = аффинное преобразование кодов ── */
typedef struct {
int32_t a, b, d, e; /* x = (a·X + b·Y + c) / div, y = (d·X + e·Y + f) / div */
int64_t c, f;
int32_t div;
} cal_t;
int cal_solve(cal_t *k, const int16_t sx[3], const int16_t sy[3], const uint16_t tx[3], const uint16_t ty[3]);
void cal_apply(const cal_t *k, uint16_t tx, uint16_t ty, int16_t *x, int16_t *y);
void cal_rotate(const cal_t *portrait, uint8_t rot, uint16_t w, uint16_t h, cal_t *out); /* как tft_rotate */
#define CAL_MAGIC 0x4B32325Au /* «Z22K» */
#define CAL_WORDS 11u /* запись во флеше: 44 байта */
uint32_t crc32(const void *p, uint32_t n);
void cal_pack(const cal_t *k, uint32_t rec[CAL_WORDS]);
int cal_unpack(const uint32_t rec[CAL_WORDS], cal_t *k); /* 0 — запись годна */
/* ── события касания: нажал, ведёт, держит, отпустил ──────────────────── */
enum { TP_NONE = 0, TP_DOWN, TP_MOVE, TP_HOLD, TP_UP };
enum { TP_IDLE = 0, TP_ACTIVE = 1 };
#define TP_POLL_MS 10u /* шаг опроса, пока касание идёт */
typedef struct {
uint8_t type;
uint8_t tap; /* у TP_UP: 1 — короткое касание без сдвига и удержания */
int16_t x, y;
} tp_event_t;
typedef struct {
xpt_t *dev;
cal_t k; /* коды → точки экрана в текущем повороте */
uint8_t k_down, k_up; /* годных пачек подряд — «нажал», негодных — «отпустил» */
uint8_t slop; /* сдвиг больше — уже перетаскивание, точек */
uint8_t manage_irq; /* 1 — пока касание идёт, EXTI запрещено */
uint16_t hold_ms;
uint8_t active, pressed, good, bad, dragging, held;
int16_t x0, y0, x, y; /* где нажали и где сейчас */
uint32_t t_down;
uint32_t cx, cy, cn; /* сумма кодов годных пачек нажатия — для калибровки */
xpt_sample_t s; /* последняя пачка */
} tp_t;
void tp_init(tp_t *t, xpt_t *dev, const cal_t *k);
int tp_step(tp_t *t, int irq, uint32_t now_ms, tp_event_t *ev); /* звать по PENIRQ и раз в TP_POLL_MS */
#endif
xpt22k.c — драйвер XPT2046, медиана и пороги, калибровка по трём точкам, запись во флеш и жесты — общий для прошивки и стенда.
/* xpt22k.c — раздел 22к: драйвер XPT2046, фильтр, калибровка по трём точкам и события касания.
* Один и тот же файл собирается в прошивку (armclang, Cortex-M3) и в стенд на компьютере (gcc):
* микроконтроллера он не знает, только порт xpt_port_*. */
#include "xpt22k.h"
void xpt_init(xpt_t *d)
{
d->n = 6;
d->skip = 1;
d->pd_last = XPT_PD_IRQ;
d->spread_max = 24;
d->rx_ohm = 400;
d->rt_max = 2000;
d->frames = d->bytes = 0;
}
/* Медиана: сортировка вставками (n ≤ 8), разброс — от наименьшего до наибольшего. */
uint16_t xpt_median(uint16_t *v, uint8_t n, uint16_t *spread)
{
for (uint8_t i = 1; i < n; i++) {
uint16_t t = v[i];
uint8_t j = i;
while (j > 0 && v[j - 1] > t) {
v[j] = v[j - 1];
j--;
}
v[j] = t;
}
*spread = (uint16_t)(v[n - 1] - v[0]);
return v[n / 2];
}
/* Пачка в режиме 16 тактов на преобразование: команда i — в байте 2i, её 12 бит — в байтах 2i+1 и 2i+2,
* следующая команда едет вместе с младшими битами предыдущего результата. Порядок: X × n, Y × n, Z1 (первый
* отбрасывается при skip > 0), Z2; у последней команды PD = 00, чтобы PENIRQ снова работал. */
int xpt_read(xpt_t *d, xpt_sample_t *s)
{
uint8_t tx[XPT_FRAME_MAX] = {0}, rx[XPT_FRAME_MAX];
uint16_t vx[XPT_NMAX], vy[XPT_NMAX], sx, sy;
uint8_t n = d->n, m = 0, zr = d->skip ? 2u : 1u;
uint32_t len;
for (uint8_t i = 0; i < n; i++)
tx[2u * m++] = XPT_X | XPT_PD_ON;
for (uint8_t i = 0; i < n; i++)
tx[2u * m++] = XPT_Y | XPT_PD_ON;
for (uint8_t i = 0; i < zr; i++)
tx[2u * m++] = XPT_Z1 | XPT_PD_ON;
tx[2u * m++] = XPT_Z2 | d->pd_last;
len = 2u * m + 1u;
xpt_port_select(1);
xpt_port_xfer(tx, rx, len);
xpt_port_select(0);
d->frames++;
d->bytes += len;
for (uint8_t i = 0; i < m; i++) /* бит 7 первого байта — BUSY, дальше 12 бит */
s->raw[i] = (uint16_t)((rx[2u * i + 1u] & 0x7Fu) << 5 | rx[2u * i + 2u] >> 3);
s->nraw = m;
for (uint8_t i = 0; i < n; i++) {
vx[i] = s->raw[i];
vy[i] = s->raw[n + i];
}
s->x = xpt_median(vx + d->skip, (uint8_t)(n - d->skip), &sx);
s->y = xpt_median(vy + d->skip, (uint8_t)(n - d->skip), &sy);
s->spread = sx > sy ? sx : sy;
s->z1 = s->raw[m - 2u];
s->z2 = s->raw[m - 1u];
/* R = Rx · X / 4096 · (Z2 / Z1 − 1) — сопротивление в точке касания: чем сильнее нажали, тем меньше */
if (s->z1 == 0u)
s->rt = XPT_NO_CONTACT;
else if (s->z2 <= s->z1)
s->rt = 0;
else
s->rt = (uint32_t)((uint64_t)d->rx_ohm * s->x * (uint32_t)(s->z2 - s->z1) / ((uint64_t)XPT_FULL * s->z1));
s->ok = (uint8_t)(s->rt <= d->rt_max && s->spread <= d->spread_max);
return s->ok;
}
/* ── калибровка ───────────────────────────────────────────────────────── */
static int64_t rdiv(int64_t n, int64_t d) /* деление с округлением к ближайшему */
{
if (d < 0) {
n = -n;
d = -d;
}
return n >= 0 ? (n + d / 2) / d : -((-n + d / 2) / d);
}
/* Три точки экрана (sx, sy) и три пары кодов (tx, ty): x = (a·X + b·Y + c) / div — система 3 × 3 по Крамеру,
* отдельно для x и для y. Возвращает 0, если точки на одной прямой. */
int cal_solve(cal_t *k, const int16_t sx[3], const int16_t sy[3], const uint16_t tx[3], const uint16_t ty[3])
{
int64_t X0 = tx[0], X1 = tx[1], X2 = tx[2], Y0 = ty[0], Y1 = ty[1], Y2 = ty[2];
int64_t div = (X0 - X2) * (Y1 - Y2) - (X1 - X2) * (Y0 - Y2);
if (div == 0)
return 0;
k->div = (int32_t)div;
k->a = (int32_t)((sx[0] - sx[2]) * (Y1 - Y2) - (sx[1] - sx[2]) * (Y0 - Y2));
k->b = (int32_t)((X0 - X2) * (sx[1] - sx[2]) - (sx[0] - sx[2]) * (X1 - X2));
k->c = Y0 * (X2 * sx[1] - X1 * sx[2]) + Y1 * (X0 * sx[2] - X2 * sx[0]) + Y2 * (X1 * sx[0] - X0 * sx[1]);
k->d = (int32_t)((sy[0] - sy[2]) * (Y1 - Y2) - (sy[1] - sy[2]) * (Y0 - Y2));
k->e = (int32_t)((X0 - X2) * (sy[1] - sy[2]) - (sy[0] - sy[2]) * (X1 - X2));
k->f = Y0 * (X2 * sy[1] - X1 * sy[2]) + Y1 * (X0 * sy[2] - X2 * sy[0]) + Y2 * (X1 * sy[0] - X0 * sy[1]);
return 1;
}
void cal_apply(const cal_t *k, uint16_t tx, uint16_t ty, int16_t *x, int16_t *y)
{
*x = (int16_t)rdiv((int64_t)k->a * tx + (int64_t)k->b * ty + k->c, k->div);
*y = (int16_t)rdiv((int64_t)k->d * tx + (int64_t)k->e * ty + k->f, k->div);
}
/* Калибровку снимают в книжной ориентации (w × h); для поворота rot — те же точки, что у tft_rotate:
* 1 — x = gy, y = w−1−gx; 2 — x = w−1−gx, y = h−1−gy; 3 — x = h−1−gy, y = gx. Касаться заново не нужно. */
void cal_rotate(const cal_t *p, uint8_t rot, uint16_t w, uint16_t h, cal_t *o)
{
int64_t W = (int64_t)(w - 1u) * p->div, H = (int64_t)(h - 1u) * p->div;
*o = *p;
switch (rot & 3u) {
case 1:
o->a = p->d, o->b = p->e, o->c = p->f;
o->d = -p->a, o->e = -p->b, o->f = W - p->c;
break;
case 2:
o->a = -p->a, o->b = -p->b, o->c = W - p->c;
o->d = -p->d, o->e = -p->e, o->f = H - p->f;
break;
case 3:
o->a = -p->d, o->b = -p->e, o->c = H - p->f;
o->d = p->a, o->e = p->b, o->f = p->c;
break;
default:
break;
}
}
/* ── запись калибровки во флеш: 11 слов — метка, коэффициенты, CRC-32 ── */
uint32_t crc32(const void *p, uint32_t n)
{
const uint8_t *b = p;
uint32_t c = 0xFFFFFFFFu;
while (n--) {
c ^= *b++;
for (int i = 0; i < 8; i++)
c = c & 1u ? c >> 1 ^ 0xEDB88320u : c >> 1;
}
return ~c;
}
void cal_pack(const cal_t *k, uint32_t r[CAL_WORDS])
{
r[0] = CAL_MAGIC;
r[1] = (uint32_t)k->a;
r[2] = (uint32_t)k->b;
r[3] = (uint32_t)k->d;
r[4] = (uint32_t)k->e;
r[5] = (uint32_t)(uint64_t)k->c;
r[6] = (uint32_t)((uint64_t)k->c >> 32);
r[7] = (uint32_t)(uint64_t)k->f;
r[8] = (uint32_t)((uint64_t)k->f >> 32);
r[9] = (uint32_t)k->div;
r[10] = crc32(r, 40);
}
int cal_unpack(const uint32_t r[CAL_WORDS], cal_t *k)
{
if (r[0] != CAL_MAGIC)
return -1; /* стёртая страница: 0xFFFFFFFF */
if (crc32(r, 40) != r[10])
return -2; /* запись побита или недописана */
k->a = (int32_t)r[1];
k->b = (int32_t)r[2];
k->d = (int32_t)r[3];
k->e = (int32_t)r[4];
k->c = (int64_t)((uint64_t)r[6] << 32 | r[5]);
k->f = (int64_t)((uint64_t)r[8] << 32 | r[7]);
k->div = (int32_t)r[9];
return k->div != 0 ? 0 : -3;
}
/* ── события ──────────────────────────────────────────────────────────── */
void tp_init(tp_t *t, xpt_t *dev, const cal_t *k)
{
t->dev = dev;
t->k = *k;
t->k_down = 2;
t->k_up = 3;
t->slop = 4;
t->hold_ms = 500;
t->manage_irq = 1;
t->active = t->pressed = t->good = t->bad = t->dragging = t->held = 0;
t->x0 = t->y0 = t->x = t->y = 0;
t->t_down = 0;
t->cx = t->cy = t->cn = 0;
}
static int far(int16_t a, int16_t b, uint8_t slop)
{
return a - b > slop || b - a > slop;
}
/* Шаг: пока касания нет, ждёт PENIRQ (irq = 1); дальше — пачка раз в TP_POLL_MS. «Нажал» — после k_down
* годных пачек подряд, «отпустил» — после k_up негодных. На шаг — не больше одного события. */
int tp_step(tp_t *t, int irq, uint32_t now_ms, tp_event_t *ev)
{
ev->type = TP_NONE;
ev->tap = 0;
if (!t->active) {
if (!irq)
return TP_IDLE;
t->active = 1;
t->good = t->bad = 0;
if (t->manage_irq)
xpt_port_irq(0); /* пачки сами дёргают PENIRQ — не слушать его */
}
if (xpt_read(t->dev, &t->s)) {
t->good = (uint8_t)(t->good < 255u ? t->good + 1u : 255u);
t->bad = 0;
cal_apply(&t->k, t->s.x, t->s.y, &t->x, &t->y);
} else {
t->bad = (uint8_t)(t->bad < 255u ? t->bad + 1u : 255u);
t->good = 0;
}
if (!t->pressed) {
if (t->good >= t->k_down) {
t->pressed = 1;
t->dragging = t->held = 0;
t->x0 = t->x;
t->y0 = t->y;
t->t_down = now_ms;
t->cx = t->cy = t->cn = 0;
ev->type = TP_DOWN;
} else if (t->bad >= t->k_up) {
t->active = 0; /* касания нет: снова ждать PENIRQ */
if (t->manage_irq)
xpt_port_irq(1);
return TP_IDLE;
}
} else if (t->good) {
t->cx += t->s.x;
t->cy += t->s.y;
t->cn++;
if (!t->dragging && (far(t->x, t->x0, t->slop) || far(t->y, t->y0, t->slop)))
t->dragging = 1;
if (t->dragging)
ev->type = TP_MOVE;
else if (!t->held && now_ms - t->t_down >= t->hold_ms) {
t->held = 1;
ev->type = TP_HOLD;
}
} else if (t->bad >= t->k_up) {
t->pressed = 0;
ev->type = TP_UP;
ev->tap = (uint8_t)(!t->dragging && !t->held);
}
ev->x = t->x;
ev->y = t->y;
return TP_ACTIVE;
}
fw22k.c — прошивка STM32F103: SPI1 на дисплей и тач, PENIRQ на EXTI1, калибровка во флеше, рисование пальцем.
/* fw22k.c — раздел 22к: тачскрин на XPT2046 и дисплей ST7735 из 22з на одном SPI1 STM32F103 (BluePill), 72 МГц.
* SPI1: SCK — PA5, MOSI — PA7, MISO — PA6 (нужен только тачу). Дисплей: CS — PA4, D/C — PA3, RST — PA2,
* подсветка — PA1, 9 МГц, режим 3, пиксели через DMA1 канал 3 (как в 22з). Тач: CS — PB0, 1,125 МГц, режим 0;
* PENIRQ → PB1: вход с подтяжкой, EXTI1 по спаду. Калибровка — последняя страница флеша 0x0800FC00.
* Тактирование до 72 МГц — как в разделе 6; запись во флеш — как в 11.5. */
#include "stm32f10x.h"
#include "tft22i.h"
#include "xpt22k.h"
#define CR1_TFT (SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_1 | SPI_CR1_CPOL | SPI_CR1_CPHA) /* /8, режим 3 */
#define CR1_TOUCH (SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_2 | SPI_CR1_BR_0) /* /64, режим 0 */
#define TFT_CS_LOW() (GPIOA->BSRR = 1u << (4 + 16))
#define TFT_CS_HIGH() (GPIOA->BSRR = 1u << 4)
#define T_CS_LOW() (GPIOB->BSRR = 1u << (0 + 16))
#define T_CS_HIGH() (GPIOB->BSRR = 1u << 0)
#define DC_LOW() (GPIOA->BSRR = 1u << (3 + 16))
#define DC_HIGH() (GPIOA->BSRR = 1u << 3)
#define CAL_ADDR 0x0800FC00u /* последняя страница 1 Кбайт у F103C8 */
enum { BUS_NONE, BUS_TFT, BUS_TOUCH };
static volatile uint32_t ticks;
static volatile uint8_t irq_flag;
static uint8_t owner = BUS_NONE, wide;
static uint16_t fill_word;
void SysTick_Handler(void)
{
ticks++;
}
void EXTI1_IRQHandler(void) /* PENIRQ: только флаг, пачка идёт в основном цикле */
{
EXTI->PR = 1u << 1;
irq_flag = 1;
}
static void delay_ms(uint32_t ms)
{
uint32_t t0 = ticks;
while (ticks - t0 < ms) {
}
}
/* Конец передачи дисплея: DMA, потом TXE = 1 и BSY = 0 (RM0008, с. 719) — как spi_idle в 22з */
static void spi_idle(void)
{
if (DMA1_Channel3->CCR & DMA_CCR1_EN) {
while (!(DMA1->ISR & DMA_ISR_TCIF3)) {
}
DMA1->IFCR = DMA_IFCR_CTCIF3;
DMA1_Channel3->CCR = 0;
}
while (!(SPI1->SR & SPI_SR_TXE)) {
}
while (SPI1->SR & SPI_SR_BSY) {
}
(void)SPI1->DR;
(void)SPI1->SR;
}
/* Шину — тому, кто просит: дождаться конца прошлой передачи, снять CS прежнего хозяина и только при SPE = 0
* сменить скорость и режим (BR, CPOL, CPHA не меняют во время обмена — RM0008, с. 743–744). */
static void bus_take(uint8_t who)
{
spi_idle();
if (owner == who)
return;
TFT_CS_HIGH();
T_CS_HIGH();
SPI1->CR1 = 0;
SPI1->CR1 = who == BUS_TFT ? CR1_TFT : CR1_TOUCH;
SPI1->CR1 |= SPI_CR1_SPE;
wide = 0;
owner = who;
if (who == BUS_TFT)
TFT_CS_LOW(); /* дисплей держит CS, пока шину не попросит тач */
}
static void spi_width(uint8_t w16)
{
if (wide == w16)
return;
SPI1->CR1 &= ~SPI_CR1_SPE;
if (w16)
SPI1->CR1 |= SPI_CR1_DFF;
else
SPI1->CR1 &= ~SPI_CR1_DFF;
SPI1->CR1 |= SPI_CR1_SPE;
wide = w16;
}
static void dma_go(const void *src, uint32_t n, uint32_t minc)
{
DMA1_Channel3->CMAR = (uint32_t)src;
DMA1_Channel3->CNDTR = n;
DMA1_Channel3->CCR = DMA_CCR1_DIR | minc | DMA_CCR1_PSIZE_0 | DMA_CCR1_MSIZE_0 | DMA_CCR1_EN;
}
static void bus_reset(void)
{
GPIOA->BSRR = 1u << (2 + 16);
delay_ms(10);
GPIOA->BSRR = 1u << 2;
delay_ms(10);
}
static void bus_cmd(uint8_t c)
{
bus_take(BUS_TFT);
spi_width(0);
DC_LOW();
SPI1->DR = c;
}
static void bus_data(const uint8_t *p, uint32_t n)
{
bus_take(BUS_TFT);
spi_width(0);
DC_HIGH();
while (n--) {
while (!(SPI1->SR & SPI_SR_TXE)) {
}
SPI1->DR = *p++;
}
}
static void bus_pixels(const uint16_t *px, uint32_t n)
{
bus_take(BUS_TFT);
spi_width(1);
DC_HIGH();
dma_go(px, n, DMA_CCR1_MINC);
}
static void bus_fill(uint16_t c, uint32_t n)
{
bus_take(BUS_TFT);
spi_width(1);
DC_HIGH();
fill_word = c;
while (n) {
uint32_t k = n > 65535u ? 65535u : n;
spi_idle();
dma_go(&fill_word, k, 0);
n -= k;
}
}
static const tft_bus_t bus = {bus_reset, delay_ms, bus_cmd, bus_data, bus_pixels, bus_fill};
/* ── порт драйвера XPT2046 ────────────────────────────────────────────── */
void xpt_port_select(int on)
{
if (on) {
bus_take(BUS_TOUCH); /* дисплей отпущен, SPI1 — 1,125 МГц, режим 0 */
T_CS_LOW();
} else {
while (SPI1->SR & SPI_SR_BSY) {
}
T_CS_HIGH();
}
}
void xpt_port_xfer(const uint8_t *tx, uint8_t *rx, uint32_t n)
{
for (uint32_t i = 0; i < n; i++) {
while (!(SPI1->SR & SPI_SR_TXE)) {
}
SPI1->DR = tx[i];
while (!(SPI1->SR & SPI_SR_RXNE)) {
}
rx[i] = (uint8_t)SPI1->DR;
}
}
void xpt_port_irq(int on)
{
if (on) {
EXTI->PR = 1u << 1; /* спад от самой пачки уже мог взвести PR — снять до разрешения */
irq_flag = 0;
EXTI->IMR |= 1u << 1;
} else {
EXTI->IMR &= ~(1u << 1);
}
}
/* ── калибровка во флеше: стереть страницу и записать 22 полуслова (раздел 11.5) ── */
static void flash_wait(void)
{
while (FLASH->SR & FLASH_SR_BSY) {
}
}
static int cal_save(const cal_t *k)
{
uint32_t rec[CAL_WORDS];
const uint16_t *h = (const uint16_t *)rec;
int bad = 0;
cal_pack(k, rec);
FLASH->KEYR = 0x45670123u;
FLASH->KEYR = 0xCDEF89ABu;
flash_wait();
FLASH->CR |= FLASH_CR_PER;
FLASH->AR = CAL_ADDR;
FLASH->CR |= FLASH_CR_STRT;
flash_wait();
FLASH->CR &= ~FLASH_CR_PER;
FLASH->CR |= FLASH_CR_PG;
for (uint32_t i = 0; i < 2u * CAL_WORDS; i++) {
*(volatile uint16_t *)(CAL_ADDR + 2u * i) = h[i];
flash_wait();
bad |= (FLASH->SR & FLASH_SR_PGERR) != 0u;
}
FLASH->CR &= ~FLASH_CR_PG;
FLASH->CR |= FLASH_CR_LOCK;
return bad ? -1 : 0;
}
static tft_t tft;
static xpt_t xpt;
static tp_t tp;
/* Ждать касания и отпускания; вернуть средние коды годных пачек нажатия */
static void wait_tap(uint16_t *tx, uint16_t *ty)
{
tp_event_t ev;
uint32_t next = 0;
for (;;) {
int due = (int32_t)(ticks - next) >= 0;
if (!irq_flag && !(tp.active && due)) {
__WFI();
continue;
}
if (!tp.active)
next = ticks;
next += TP_POLL_MS;
int irq = irq_flag;
irq_flag = 0;
(void)tp_step(&tp, irq, ticks, &ev);
if (ev.type == TP_UP && tp.cn) {
*tx = (uint16_t)(tp.cx / tp.cn);
*ty = (uint16_t)(tp.cy / tp.cn);
return;
}
}
}
static void calibrate(cal_t *k)
{
static const int16_t sx[3] = {13, 115, 64}, sy[3] = {16, 80, 144};
uint16_t tx[3], ty[3];
do {
for (int i = 0; i < 3; i++) {
tft_fill_rect(&tft, 0, 0, (int16_t)tft.w, (int16_t)tft.h, 0);
tft_fill_rect(&tft, (int16_t)(sx[i] - 6), sy[i], 13, 1, 0xFFFFu); /* крест */
tft_fill_rect(&tft, sx[i], (int16_t)(sy[i] - 6), 1, 13, 0xFFFFu);
wait_tap(&tx[i], &ty[i]);
}
} while (!cal_solve(k, sx, sy, tx, ty));
(void)cal_save(k);
}
int main(void)
{
cal_t k = {0, 0, 0, 0, 0, 0, 1};
tp_event_t ev;
uint32_t next = 0;
int16_t px = 0, py = 0;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN | RCC_APB2ENR_SPI1EN;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
GPIOA->BSRR = 1u << 2 | 1u << 3 | 1u << 4; /* RST, D/C, CS дисплея — в 1 */
GPIOA->CRL = (GPIOA->CRL & 0x0000000Fu) /* PA0 не трогаем */
| 0x2u << 4 | 0x3u << 8 | 0x3u << 12 | 0x3u << 16 /* PA1–PA4 — выходы */
| 0xBu << 20 | 0x4u << 24 | 0xBu << 28; /* PA5, PA7 — SPI1; PA6 — MISO, вход */
GPIOB->BSRR = 1u << 0 | 1u << 1; /* CS тача в 1, подтяжка PENIRQ вверх */
GPIOB->CRL = (GPIOB->CRL & ~0xFFu) | 0x82u; /* PB0 — выход 2 МГц (0010), PB1 — вход с подтяжкой (1000) */
SPI1->CR2 = SPI_CR2_TXDMAEN;
DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR;
SysTick_Config(72000u);
GPIOA->BSRR = 1u << 1;
tft_init(&tft, &bus, 0, 1, 2, 1); /* дисплей из 22з */
xpt_init(&xpt);
xpt_sample_t s;
(void)xpt_read(&xpt, &s); /* первая пачка кончается PD = 00 — PENIRQ включён */
AFIO->EXTICR[0] = (AFIO->EXTICR[0] & ~0xF0u) | 0x10u; /* EXTI1 ← PB1 */
EXTI->FTSR |= 1u << 1;
NVIC_EnableIRQ(EXTI1_IRQn);
xpt_port_irq(1);
tp_init(&tp, &xpt, &k);
if (cal_unpack((const uint32_t *)CAL_ADDR, &k) != 0)
calibrate(&k);
tp.k = k;
tft_fill_rect(&tft, 0, 0, (int16_t)tft.w, (int16_t)tft.h, 0);
for (;;) { /* рисование пальцем: точка, линия, удержание — стереть */
int due = (int32_t)(ticks - next) >= 0;
if (!irq_flag && !(tp.active && due)) {
__WFI();
continue;
}
if (!tp.active)
next = ticks;
next += TP_POLL_MS;
int irq = irq_flag;
irq_flag = 0;
(void)tp_step(&tp, irq, ticks, &ev);
if (ev.type == TP_DOWN)
tft_fill_rect(&tft, (int16_t)(ev.x - 1), (int16_t)(ev.y - 1), 3, 3, 0xFFE0u);
else if (ev.type == TP_MOVE)
tft_line(&tft, px, py, ev.x, ev.y, 0x07FFu);
else if (ev.type == TP_HOLD)
tft_fill_rect(&tft, 0, 0, (int16_t)tft.w, (int16_t)tft.h, 0);
px = ev.x;
py = ev.y;
}
}
Дальше: раздел 43 «Аппаратная криптография: CRC, RNG, HASH, CRYP».