Embedded HandbookApps
All sections › III. Peripherals
STM32F407 · level intermediate · 38 min read
Open in the interactive handbook

22п. A capacitive touchscreen on the FT6236/FT6206 over I2C: touches, gestures, the INT interrupt, rotation calibration for the ILI9341 and handling interface button presses

In section 22к a resistive sensor measured the finger: the XPT2046 gave ADC codes, they were converted into points by a three-touch calibration and cleaned of bounce — none of it is repeated here. In 22о the ILI9341 started working on the F407 at 320 × 240, and modules with this display often carry a different sensor — a capacitive one, with an FT6236 or FT6206 controller on I2C. Section 22а.6 gave the general view: the controller counts touches itself, returns ready coordinates, and the INT pin says they are there. Here is how it works in practice: the registers and the touch report, the event fields and the finger ID, the INT mode, the threshold and the report rate, the connection to the F407 next to the display, mapping a point for the screen rotation from 22о, a button handler with hold, swipe and pinch, and the choice between polling and the interrupt.

Sources: the ST ft6x06 driver V1.1.0 (STM32F4xx DFP 2.17.1, BSP Components) — register addresses, event and gesture codes, the G_MODE values, the codes in CHIP_ID: 0x11 — FT6206, 0xCD — FT6x36; no more than two touches; the same file has the comments "no software reset" and "polling by default, no INT generation". The bit fields (XH — the event in bits 7:6, YH — the ID in bits 7:4) and reading both touches in one burst come from ft6x06 V2.0.0 (STM32H7xx DFP 3.1.1). From the ST board BSPs: on the STM32412G-Discovery TS_I2C_ADDRESS is 0x70, reset is 10 ms low, INT has a pull-up, falling edge, "active low", the bus runs at 100 kHz; on the STM32469I-Discovery there are two addresses, 0x54 and 0x70, the code 0xD8 for the FT6336G and a 400 kHz bus. From the OpenMV ft5x06 module (MIT): address 0x38, reading all points in one burst, a pause of 1 ms in reset and 39 ms after. RM0090 Rev 19: receiving more than two bytes, p. 851; I2C_CCR, p. 870; I2C_TRISE, p. 871; SYSCFG_EXTICR2, p. 291; EXTI4 is position 10 in the vector table, p. 372; EXTI_FTSR, p. 385; EXTI_PR, p. 386. DS8626 Rev 9: PB10 is I2C2_SCL, PB11 is I2C2_SDA, both FT, p. 52; AF4 — p. 63; PC4 and PC5 are FT, p. 50; pin pull-up 30–50 kOhm, p. 115; fast mode I²C up to 400 kbit/s, p. 121. DocID13587 Rev 17, table 40, p. 69: the SCL and SDA rise time in fast mode is no longer than 300 ns, line capacitance up to 400 pF. The project has no FT6236 document: what is in CIPHER, which units PERIODACTIVE and TH_GROUP use, how INT behaves and what goes into the report on lift-up follow the manufacturer's documentation and are not checked; in the model they are assumptions. All numbers below come from a run of the driver _verify/ctp22p/ft22p.c on a computer (gcc 12.2.0) with our own model of the controller in Python 3.11; the firmware is built by armclang 6.24 for the Cortex-M4.

What it is called in the documentation

Original Meaning
projected capacitive (PCAP), mutual capacitance a projected capacitive sensor: an electrode grid under the glass
TD_STATUS (touch data status) how many touches lie on the glass now
event flag: press down, lift up, contact the event field: press, lift, contact
touch ID the number under which the controller follows a finger from report to report
G_MODE: interrupt polling / interrupt trigger what the INT pin does: nothing or "a report is ready"
TH_GROUP (touch threshold) the threshold above which a signal counts as a finger
PERIODACTIVE (report rate in active mode) the report rate while a finger is on the glass
CIPHER, CHIP_ID chip codes: which FT6x we are talking to
hit test checking which button a point fell into
ghost touch a false touch caused by interference
pinch (zoom in / zoom out) two fingers moving apart or together

At the top, the FT6236 on the STM32F407: I2C2 at 400 kHz, address 0x38, PB10 is SCL, PB11 is SDA, PC4 is the INT input (low means a report is ready, EXTI4 on the falling edge), PC5 is the RST output; the SCL, SDA and INT pull-ups sit on the module; next to it on the same module is the ILI9341 on SPI1 from section 22о; the 240 × 320 touch glass matches the display memory at MADCTL 0x00. In the middle, the report: 13 bytes from address 0x02 in one read, 370 us at 400 kHz: TD_STATUS is the number of touches 0…2, then P1 and P2, six bytes each; in XH bits 7:6 are the event (0 press, 1 lift, 2 contact), in YH bits 7:4 are the touch ID. Bottom left, a tap in the model: a report every 16.67 ms — press, contact, contact, lift; on each report INT goes low and stays low until it is read; the touch is accepted on "contact", the click comes on "lift"; from touch to "pressed" on average 24.68 ms via INT and 29.22 ms when polling every 10 ms. On the right, mapping a glass point to the screen for MADCTL 08, 68, C8 and A8: (gx, gy), (gy, 239 − gx), (239 − gx, 319 − gy), (319 − gy, gx), checked on 1610 points of each rotation. At the bottom, INT versus polling: without a touch 0 reads versus 96 per second; a pitfall — INT is held until it is read, after enabling EXTI look at the level


22п.1 Capacitive versus resistive: what changes

The resistive panel from 22к is two layers the finger presses together; the controller is an ADC and the program does everything else: measures, calibrates, removes bounce. A capacitive one is an electrode grid under the glass with its own controller that scans the grid itself and returns a ready report: where the fingers are, how many there are and what happened to each. Hence the difference:


22п.2 Registers and the report: TD_STATUS, P1, P2, events and ID

The bus address is 0x38. In the ST board files TS_I2C_ADDRESS is 0x70 — the same address already shifted by the read/write bit. For the 469I-Discovery ST tries two addresses, 0x54 and 0x70: on other modules the address can differ.

Address Name Contents
0x00 DEV_MODE 0 — working mode
0x01 GEST_ID a gesture found by the controller itself
0x02 TD_STATUS number of touches, bits 3:0
0x03…0x08 P1_XH, XL, YH, YL, WEIGHT, MISC the first touch
0x09…0x0E P2_… the second — the same six bytes
0x80 TH_GROUP touch threshold
0x88 PERIODACTIVE report rate in active mode
0xA3 CIPHER "chip selecting"
0xA4 G_MODE 0 — polling, 1 — interrupt
0xA6 FIRMID firmware version
0xA8 CHIP_ID 0x11, 0xCD or 0xD8

In P1_XH bits 7:6 are the event: 0 — press, 1 — lift, 2 — contact (the finger is lying), 3 — no event; bits 3:0 are the high bits of X. In P1_YH bits 7:4 are the touch ID, 3:0 the high bits of Y. The ID is needed to tell fingers apart: if the first finger leaves, the second may move from P2 to P1, but its ID stays the same.

The chip codes are the least reliable place. ST reads CHIP_ID (0xA8) and expects 0x11 for the FT6206 and 0xCD for the FT6x36; the 469I-Discovery also has 0xD8 — according to the comment, the FT6336G. ST declares CIPHER (0xA3) but gives no values. The driver accepts all three CHIP_ID codes and only reads and keeps CIPHER and FIRMID. In the model CHIP_ID = 0xCD, CIPHER = 0x36 — per the manufacturer's documentation, not checked.

Reading in one burst. The report is TD_STATUS and both touches: 13 bytes in a row from address 0x02. This is how ST V2.0.0 reads (12 bytes from P1_XH) and OpenMV (from 0x01). At 400 kHz it takes 370 us of the bus, at 100 kHz 1480 us:

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;
}

The driver decodes both slots even if TD_STATUS is less than two: in the model a finger that has just left arrives in the last report with the "lift" event in the slot beyond TD_STATUS, and an empty slot is 0xFF bytes, event 3. How the chip does it follows the manufacturer's documentation and is not checked; the handler in 22п.5 does not rely on it and releases a finger if its ID simply disappears from the report.

G_MODE, TH_GROUP, PERIODACTIVE. G_MODE = 1 — INT reports a new report, 0 — polling; ST sets 0 by default with the comment "no INT generation". In the model with G_MODE = 1 INT goes low on a new report and stays low until TD_STATUS is read. TH_GROUP is the touch threshold, PERIODACTIVE the report rate; ST does not say what units they use. In the model the threshold is in units of its signal (a finger gives 100), and PERIODACTIVE is reports per second: 60 — a report every 16.67 ms. These are assumptions; on hardware the values have to be chosen from the document for your chip.


22п.3 Connecting to the F407 and start-up

The ILI9341 from 22о took SPI1 (PA5–PA7) and PB0, PB1, PB6–PB8 — among them PB6 and PB8, where I2C1 of the F407 lives. So the touch goes to I2C2:

Module pin STM32F407 Purpose
SCL, SDA PB10, PB11 I2C2, AF4, open drain, 400 kHz
INT PC4 input with pull-up, EXTI4 on the falling edge
RST PC5 output, 0 — reset
VCC, GND 3.3 V, GND power

PB10 and PB11 are FT (DS8626, p. 52), AF4 — p. 63; PC4 and PC5 are FT, p. 50. APB1 is 42 MHz: in fast mode with DUTY = 0 the low level is twice as long as the high one (RM0090, p. 870), CCR = 42 / (3 · 0.4) = 35 — exactly 400 kHz. TRISE is the maximum rise time plus one (p. 871): 300 ns × 42 MHz = 12.6, the integer part 12, plus one — 13. Receiving more than two bytes is as on p. 851: the last three bytes through BTF. INT: in SYSCFG_EXTICR2 the EXTI4 field = port C (p. 291), EXTI_FTSR — falling edge (p. 385), the EXTI4 handler — position 10 (p. 372).

Start-up is a reset through the RST pin (10 ms low, as at ST), a pause of 40 ms (OpenMV waits 39 ms), the codes, the settings and one read of 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, если он уже прижат */
}

The FT6x06 has no software reset — ST says so directly in ft6x06_Reset, so RST is better wired to a pin. On the bench start-up takes 8 transfers and 16 bytes: 690 us of the bus and 50.69 ms with the pauses. How long to really wait after RST: ST holds it 10 ms low, OpenMV 1 ms and 39 ms after; the model answers at once. With the address 0x70 instead of 0x38 start-up ends on the very first transfer: the slave did not answer, code −1.


22п.4 Glass to screen: four rotations and mirrors

In the model the touch glass lies the same way as the ILI9341 memory at MADCTL 0x00: the touch point (gx, gy) is column gx and row gy of the memory. After that the screen rotation decides everything. In 22о the display puts a screen point (x, y) into memory like this: MV swaps x and y, then MX mirrors the column and MY the row. The touch needs the reverse: remove the mirrors and swap the axes back. For the rotations from 22о:

Rotation MADCTL Screen screen x screen 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;
    }
}

The function takes the same MADCTL byte that went to the display — the rotation changes in one place. The fields swap, mir_x, mir_y are for a module where the touch is glued differently; ST does the same on the 412G-Discovery with the flags TS_SWAP_X, TS_SWAP_Y, TS_SWAP_XY and the mapping 240 − 1 − x.

The table is checked in two ways. First on 1610 glass points (a grid with a step of 7 points) for each rotation: the table formula passed through the display write of 22о returns the same glass point — 0 mismatches, and ft_to_screen in C gives the same screen point — also 0. Then by touches: 3 × 3 buttons on the screen, a tap in the middle of each.

Rotation MADCTL Correct Rotation ignored Touch mirrored in X Mirror corrected
0° 08 9 of 9 9 of 9 3 of 9 9 of 9
90° 68 9 of 9 0 of 9 3 of 9 9 of 9
180° C8 9 of 9 1 of 9 3 of 9 9 of 9
270° A8 9 of 9 0 of 9 3 of 9 9 of 9

With the rotation ignored, at 180° only the middle hits — the central button, at 90° and 270° nothing. A mirror in X keeps the middle column and swaps left and right.


22п.5 The interface handler: buttons, hold, debounce, swipe and pinch

The handler ui_feed gets a report with the points already in screen coordinates and produces events: "pressed" with a button number, "click", "hold", "cancel", a swipe in one of four directions, a pinch "together" or "apart". It follows a finger by the ID from the report. Firmware settings: hold — 500 ms, movement — 12 points, swipe — no shorter than 40 points and no longer than 400 ms, pinch — the distance changed by 30 points.

Debounce by events. A false touch from interference lives one report: "press", then "lift". A real finger stays longer and in the second report comes with "contact". The handler accepts a touch when it sees "contact", not by the number of reads: when polling the same report can be read twice, and counting reads would accept the interference too.

        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);               /* антидребезг: ждать «контакт» — второй отчёт */

A button. The hit is tested on the first point of the touch. An accepted touch on a button gives "pressed"; if the finger left the button or moved more than 12 points — "cancel", this is no longer a click; if it lay still for 500 ms — "hold", and no click follows; on release — "click".

Swipe and pinch are computed from two samples — the first and the last, without the path history:

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;
}

The second accepted finger cancels the button of the first one and starts a pinch: the distance between the fingers at the start and the last one, by an integer square root. Swipe and pinch are computed in screen coordinates, so "left" stays left at any rotation.

The controller has gestures of its own too — the GEST_ID register, codes at ST: 0x10 up, 0x14 right, 0x18 down, 0x1C left, 0x48 and 0x49 — zoom. But they are in glass coordinates: OpenMV reorders them for its board with the note "rotated by 90 degrees". How to enable them follows the manufacturer's documentation; in the model the register is always 0, and the gestures here are our own.

The set in the model: 20 taps of 80…200 ms, 10 holds of 0.7…1 s, 16 swipes of 80…120 points in 120…300 ms, 10 times "pressed and slid off the button", 10 pinches (distance 60 ↔ 160 points in 300…500 ms). Portrait (08) and landscape (68) — 66 of 66 correct each; 1473 reads in 46.656 s, the bus busy 1.17 % of the time.


22п.6 Polling or the interrupt; gloves, water and interference

Delay and bus. 20 taps of 80…200 ms on buttons, 60 reports per second (16.67 ms), debounce on. The delay is from the touch to the "pressed" event:

Method Average delay Worst Reads per run Bus during taps Reads per second without touch
INT, 400 kHz 24.68 ms 33.39 ms 168 0.73 % 0
INT, no debounce 8.02 ms 16.73 ms 168 0.73 % 0
INT, 30 reports per second 48.97 ms 66.97 ms 94 0.41 % 0
INT, 100 reports per second 14.85 ms 19.01 ms 267 1.15 % 0
polling every 10 ms 29.22 ms 39.68 ms 826 3.56 % 96
polling every 20 ms 35.83 ms 50.10 ms 420 1.81 % 49
polling every 50 ms 51.79 ms 76.67 ms 170 0.73 % 19
INT, 100 kHz 27.06 ms 34.68 ms 168 2.90 % 0
polling every 10 ms, 100 kHz 31.11 ms 40.07 ms 746 12.88 % 87

All 20 clicks are in place in every row. With INT the bus works only when there is a report: no finger — not a single read. Polling every 10 ms reads 96 times per second even without a finger, and still does not catch up on the delay: the report is already there but has not been picked up yet. Debounce costs one report period — 24.68 ms versus 8.02; more frequent reports mean a smaller delay.

Reading the ST way. The ST driver V1.1.0 reads TD_STATUS, then for each touch 4 bytes of X/Y and 3 bytes of "info" starting at Pn_WEIGHT. The third info byte is already the next register, P2_XH (for the second touch — 0x0F), and ST takes the event exactly from it. In V2.0.0 the event comes from its own XH. Repeated in ft_read_split, this read gave 0 clicks of 20 in the model — for a single finger the event is always the "no event" of the neighbouring empty slot — and 632 transfers instead of 168. On the bus one touch read this way costs 512.5 us at 400 kHz versus 370, two — 825 us.

Strict event parsing. If a touch starts only on "press" and ends only on "lift", polling slower than the reports loses both. Taps of 60…120 ms:

Parsing INT Polling every 50 ms
by ID and presence in the report 20 of 20 20 of 20
only by "press" and "lift" 20 of 20 3 of 20

The same case is a microcontroller restart while a finger already lies on the glass: "contact" comes first, there is no "press". A finger on button 4 200 ms before the loop starts: parsing by ID gave "pressed" and "click" of button 4, the strict one gave nothing.

Gloves, water and interference. In the model a finger gives a signal of 100, a gloved finger 45, a water drop 50; the noise without interference is σ 6, with interference from the backlight PWM σ 20 (these are assumptions, not measurements). A false touch is noise above the threshold in one of 48 cells: it lives one report. 10 taps, 10 holds of 0.7…1 s, 10 gloved taps, a drop for 2 s and 10 s without touches:

Interference, TH_GROUP, debounce False touches in 10 s False clicks Taps Holds Gloved Drop: events
none, 60, yes 0 0 10 10 0 0
PWM, 60, no 34 18 0 0 0 25
PWM, 60, yes 33 0 9 4 2 5
PWM, 90, yes 0 0 6 0 0 0
none, 40, yes 0 0 10 10 3 7
none, 30, yes 0 0 10 10 10 1
PWM, 40, yes 406 0 1 4 0 6

Debounce by events removes all false clicks — 18 versus 0, — but not everything: with interference the finger sometimes disappears from the report for one scan, and a long hold breaks into pieces. A high threshold silences the interference and the real finger with it: at 90 taps are 6 of 10 and no holds at all. A glove is seen only with a low threshold — 30, and there a drop also gives a touch. The threshold is a compromise, and interference is better removed where it is born: route the backlight and the touch cable apart, raise the PWM frequency.


22п.7 Pitfalls

INT is held low, but there are no interrupts. In the model INT stays low until TD_STATUS is read. If the first report came before EXTI was enabled — a finger lay down during start-up — there will be no more falling edges: 10 taps, 0 reads, INT low until the end of the run. The firmware looks at the level after enabling EXTI and after every read: if INT is already low, read without waiting for an edge. Then the same 10 taps give 10 of 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;

Address 0x70. In the ST board files the address is already shifted. Used as a 7-bit one it does not answer — start-up ends with code −1 on the first transfer.

400 kHz and pull-ups. The rise time in fast mode is up to 300 ns, line capacitance up to 400 pF (DocID13587, table 40). The rise to 0.7 of the supply through a pull-up R into a capacitance C is 0.8473 · R · C:

Pull-up 50 pF 100 pF
2.2 kOhm 93 ns 186 ns
4.7 kOhm 199 ns 398 ns
10 kOhm 424 ns 847 ns
40 kOhm (the pin's own pull-up) 1695 ns 3389 ns

The F407's own pull-up is 30–50 kOhm (DS8626, p. 115): no good for 400 kHz. Which resistors are on the module and whether there are any at all — see the module schematic, not checked here. If it does not fit — 100 kHz: the INT delay grew from 24.68 to 27.06 ms, and the bus is busy 4 times longer.

X and Y swapped, mirrors. A touch lands on neighbouring buttons or "moves" the opposite way. Do not flip signs at random: take the display's MADCTL (22п.4) and separately how the touch is glued (swap, mir_x, mir_y). Check it with taps in the corners and the middle.

"Contact" without "press". After a microcontroller restart or when polling slower than the reports, the first thing seen is "contact". Strict parsing does not see such a touch; start a touch by a new ID with any event.

An event from someone else's register. The "ST V1.1.0 way" read takes the event from P2_XH: with one finger it is always "no event". Read the report in one burst and take the event from its own XH.

False touches from the backlight. The backlight PWM and a long cable give touches lasting one report. Use debounce by events rather than raising the threshold until gloves and holds are lost.


22п.8 Summary

Full sources

The bench host22p.c, the model model22p.py and the scenarios lie next to the check in _verify/ctp22p/.

ft22p.h — FT6x06 registers, report fields, port, driver, screen mapping and interface handler: types and prototypes.

/* 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 — the driver: reset, codes, INT mode, burst read and the ST-style read, glass to screen; the handler of buttons, hold, swipe and pinch — shared by the firmware and the bench.

/* 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 firmware: I2C2 on PB10/PB11, INT on EXTI4, RST, a loop that checks the INT level.

/* 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]++;
        }
    }
}

Next: section 43 "Hardware cryptography: CRC, RNG, HASH, CRYP".

Open in the interactive handbook