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

22к. Resistive touchscreen and the XPT2046 controller over SPI: X/Y measurement, pressure, three-point calibration and bounce filtering

In section 22з a color ST7735 display came to life on the STM32F103, and in 22и a keypad appeared on a port expander. The next step of input is a finger right on the screen. The general view of touch sensors is in 22а.6: resistive means four wires to an ADC, capacitive means its own controller over I2C. Here it is the resistive panel with the XPT2046 controller found on cheap display modules: how the panel turns a touch into a voltage, what every bit of the XPT2046 command means, how to put it on the same SPI1 as the display, how to read X, Y and the pressing force, how to suppress noise and bounce, how to calibrate the screen with three touches and how to turn touches into the events "down", "move", "hold", "up".

Sources: RM0008 (DocID13902 Rev 17): the pin mode table, p. 160; AFIO_EXTICR1, p. 190; EXTI_PR, p. 212; the end of an SPI transfer — TXE, then BSY, p. 719; the BR divider and the rule not to change it during a transfer, p. 743; CPOL and CPHA, p. 744. DocID13587 Rev 17 (STM32F103x8/xB): PA5 is SPI1_SCK, PA6 is SPI1_MISO, PA7 is SPI1_MOSI, PB0 and PB1 are not 5 V tolerant, p. 29; the SPI master clock is up to 18 MHz, p. 71. The project has no XPT2046 document. The fields of the command byte, the channel numbers of X, Y, Z1 and Z2, the "16 clocks per conversion" mode, the place of the 12 bits in the reply, the pressure Z1 + 4096 − Z2 with a threshold of 100 and powering down after the last command were checked against the input_xpt2046.c driver from Zephyr 4.4.2; it also says that the XPT2046 is similar to the ADS7843 from TI and differs in channel numbering. The swapped axis names (CHX = 0x90, CHY = 0xD0) and the three-point calibration coefficients (An…Fn and Divider) come from the file XPT2046.h in the Clarinox Wireless 2.0.1 pack; the X layer resistance for pressure and confirming PENIRQ by pressure come from the emWin GUITDRV_ADS7846.h in the MDK-Middleware Graphics 1.3.1 pack. The 2 MHz clock limit, the sampling time, the behavior of PENIRQ during a conversion and the PENIRQ pull-up follow the manufacturer's documentation and were not checked: in the model they are assumptions. All numbers below come from running the driver _verify/touch22k/xpt22k.c on a computer (gcc 12.2.0) with its own model of the panel and the XPT2046 in Python 3.11; the firmware is built with armclang 6.24 for the Cortex-M3.

What it is called in the documentation

Original Meaning
resistive touch panel, 4-wire resistive panel, four wires
X+, X−, Y+, Y− layer terminals: X between X+ and X−, Y between Y+ and Y−
touch resistance, Rtouch resistance at the touch point
Z1, Z2 (pressure) two measurements of the current through the touch — pressure
DFR / SER (differential / single-ended reference) the reference is the same switches / a separate one
PD1, PD0 (power-down) whether to power down between conversions
PENIRQ (pen interrupt) the "touched" pin, active low
DCLK, DIN, DOUT, CS clock, data into the controller, out of it, select
calibration matrix coefficients converting codes into screen pixels
debouncing bounce suppression
tap, long press, drag tap, hold, drag

Top: connection to the STM32F103: the ST7735 display from 22з and the XPT2046 controller on one SPI1, PA5 (SCK) and PA7 (MOSI) shared, PA6 (MISO) used only by the touch, PA4 is the display CS, PB0 the touch CS, PB1 is PENIRQ on EXTI1 on the falling edge; the touch runs at 1.125 MHz in mode 0, the display at 9 MHz in mode 3; the four panel wires X+, X−, Y+, Y− go to the XPT2046. Bottom: one burst of 15 conversions of 16 clocks each, 31 bytes and 220.4 µs: six D3 commands for the X axis, six 93 for the Y axis, two B3 for Z1 and C0 for Z2 with PD = 00; the first samples after the switches change are off by 13 codes at 1.125 MHz and by 1090 at 9 MHz with Rt = 600 Ω, so they are dropped; the mean error over the 128 × 160 glass grid is 78.73 pixels without calibration, 1.60 with two points, 0.29 with three


22к.1 The panel and the XPT2046: a voltage divider and the command byte

A resistive panel is two transparent layers with a conductive coating and a gap between them. Layer X has the terminals X+ and X− on two opposite edges, layer Y has Y+ and Y− on the other two. A press joins the layers at one point. If VCC goes to X+ and ground to X−, the voltage falls evenly along layer X, and the touch point becomes the tap of a divider: its potential is the fraction of the way from X− to X+. Layer Y is connected to nothing at that moment and takes the same potential through the touch — it is measured on Y+. For Y the roles swap. To learn the pressing force, the current is passed through the touch itself: VCC on Y+, ground on X−, and X+ (Z1) and Y− (Z2) are measured. At rest Y− is grounded and X+ is pulled up to the supply: a touch pulls X+ to zero — that is PENIRQ.

The XPT2046 is a 12-bit ADC with these switches inside. Every conversion starts with a command byte, most significant bit first:

/* ── байт команды: 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 */
Bit 7 6…4 3 2 1…0
Field S, start A2…A0, channel MODE: 1 is 8 bits SER/DFR: 0 is differential PD1 PD0

The channel numbers are as in Zephyr: Y = 1, Z1 = 3, Z2 = 4, X = 5; hence the commands 0xD0 (X), 0x90 (Y), 0xB0 (Z1) and 0xC0 (Z2) plus the PD bits. The Clarinox file names the same codes the other way round: CHX = 0x90, CHY = 0xD0. Which of the two channels runs along the long side of the screen depends on how the module is assembled, and the calibration (22к.5) swaps the axes anyway. SER/DFR = 0 is the differential mode: the reference is the same switches that feed the layer, and the drop across them does not enter the code; Zephyr does not set the SINGLE_ENDED bit, and neither does this driver.

The model without noise and jitter gave the codes in the corners of the 128 × 160 glass: at the point (0, 0) — X = 3892, Y = 322, at the point (127, 159) — X = 236, Y = 3774, at the center (64, 80) — 2057 and 2062. The panel X axis runs along the glass y and against it, and the codes do not reach 0 and 4095: the edges of the layer are wider than the glass. The emWin example for a 640 × 480 panel has the same "incomplete" limits — 0x0053…0x0F51 on one axis and 0x0F5E…0x0083, reversed, on the other.


22к.2 Connecting to the F103: SPI1 shared with the display, its own CS, PENIRQ on EXTI

The touch has five signals: the clock DCLK, the data DIN and DOUT, the select CS and PENIRQ; the labels on module boards differ and were not checked here. The clock and data go onto the same SPI1 as the display from 22з; MISO, which the display did not need, is now needed by the touch.

Touch F103 What
DCLK PA5 SPI1 clock, shared with the display
DIN PA7 MOSI, shared
DOUT PA6 MISO: input without pull
CS PB0 its own select: output, 1 at rest
PENIRQ PB1 PENIRQ: input with pull-up (1000, p. 160), EXTI1 on the falling edge
display CS PA4 held low in 22з — now it is driven

PA5–PA7, PB0 and PB1 are not 5 V tolerant (DocID13587, p. 29): the touch and the display run from 3.3 V. EXTI1 is tied to port B with the field EXTI1[3:0] of AFIO_EXTICR1 = 0001 (p. 190).

Each has its own speed and mode. The display in 22з runs at 9 MHz (72 MHz / 8) in mode 3. The touch needs a slow clock: the controller has only a few clocks after the channel number to sample, and the node it measures charges through the touch and the layer. The touch here runs at 72 MHz / 64 = 1.125 MHz (the manufacturer's documentation gives a limit of about 2 MHz, not checked) in mode 0, as in Zephyr. The BR divider, CPOL and CPHA are changed only between transfers (RM0008, p. 743–744): first wait for the end of the display transfer — TXE, then BSY (p. 719) — release its CS, switch SPI off, reconfigure:

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, пока шину не попросит тач */
}

Every display bus function starts with bus_take(BUS_TFT), the touch port with bus_take(BUS_TOUCH). The display CS goes low when the display takes the bus and goes high only when the touch asks for it: the DMA of a pixel row may still be running after the function returns (22з.1).

What the slow clock gives, the model showed with a still finger at the center. The switches change after the channel number, the sample is taken at the end of the command byte (a model assumption, four clocks); the node charges through Rt and half a layer onto 1 nF. Errors of the X samples in code units (one pixel of the glass is 23 units):

SPI clock Rt = 600 Ω: 1st / 2nd sample median 2…6 Rt = 3000 Ω: 1st / 2nd median 2…6
562.5 kHz 0 / 0 0 −208 / 0 0
1.125 MHz −13 / 0 0 −653 / −7 0
2.25 MHz −163 / 0 0 −1159 / −117 −1
4.5 MHz −578 / −4 0 −1544 / −491 −50
9 MHz −1090 / −86 −1 −1782 / −1005 −319
18 MHz −1498 / −421 −33 −1915 / −1437 −810

The first sample after the switches change is always wrong — it is dropped. At 1.125 MHz the second one is already right even with a light press; at 9 MHz, the display speed, the median with Rt = 3000 Ω is off by 319 units — 14 pixels.


22к.3 X, Y and pressure: a burst of 16 clocks per conversion

The classic exchange is a command byte and two reply bytes, 24 clocks per conversion. The XPT2046 allows the next command to start while the low bits of the previous result are coming out: 16 clocks per conversion, as Zephyr does. One burst of the driver takes six X, six Y, two Z1 and one Z2 — 15 conversions:

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

A burst is 31 bytes, 220.4 µs at 1.125 MHz; with 24 clocks it would be 45 bytes and 320 µs. In the model all 15 codes decoded by the driver matched what the controller converted. The last command has PD = 00: after it the controller powers down and watches for a touch again; the others have PD = 11. Polling every 10 ms occupies the bus by 2.2 %.

Pressure. From the divider circuit (22к.1), Z1 and Z2 give the current through the touch, and the touch resistance is R = Rx · X / 4096 · (Z2 / Z1 − 1), where Rx is the resistance of layer X between X+ and X−: it is taken from the panel description or measured with an ohmmeter (400 Ω in the model). emWin also asks for the X layer resistance for pressure. The model at three points:

Glass point Rt = 300 Ω 600 Ω 1200 Ω 3000 Ω
center (64, 80): driver R 299 599 1201 2935
center: Z1 + 4096 − Z2 2558 1860 1202 589
corner (8, 8): Z1 + 4096 − Z2 3061 2443 1740 934
corner (120, 152): Z1 + 4096 − Z2 1106 639 347 155

The formula returns Rt to within a few ohms at any point. The Zephyr measure Z1 + 4096 − Z2 (default threshold 100) is simpler, but at the same Rt = 600 Ω it differs 3.8 times between two corners: 2443 and 639. A threshold on it either misses light presses in one corner or accepts them in the other. The driver treats a touch as valid if R is at most 2000 Ω; with the panel released Z1 = 0, Z2 = 4095, and R is "no contact".


22к.4 Noise and bounce: drop the first, median, threshold, burst counting

A finger does not land on the panel at once: for the first milliseconds the contact breaks and closes again, a lightly pressed touch has a large Rt, and a broken layer floats and picks up interference. The same happens on release. The driver fights this on three levels: inside a burst — drop the first sample of each axis and take the median of the other five, if their spread is more than 24 units the burst is invalid; by pressure — R above 2000 Ω makes the burst invalid; between bursts — "down" only after two valid bursts in a row, "up" after three invalid ones.

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

The model touched the panel 150 times with a finger at random places, for 80…300 ms, with a bounce of 2…8 ms on press and 1…5 ms on release; polling every 10 ms and on PENIRQ. A point is wild if it is more than 4 glass pixels away from the finger:

Method Touches seen Extra "down" Wild points Worst error "Down" after the touch: mean / worst "Up" after lifting
one sample, no thresholds 150 74 202 of 2744 177.6 pixels 0.0 / 0.0 ms 16.8 ms
median of 5 out of 6 150 71 10 of 2742 174.4 pixels 0.1 / 1.6 ms 16.1 ms
+ pressure and spread thresholds 150 1 0 of 2704 1.5 pixels 1.5 / 4.5 ms 2.9 ms
+ two and three bursts in a row 150 0 0 of 2452 1.4 pixels 18.3 / 20.0 ms 22.8 ms

The median removes noise inside a burst but not bounce: a burst taken at the moment of a break "floats" as a whole. Bounce is killed by the pressure threshold, and the last extra presses by burst counting. The price is about 18 ms to "down": two bursts of 10 ms.


22к.5 Three-point calibration

Panel codes are not screen pixels: the panel X axis may run along the screen y, against it, at an angle, and the edges of the layer are wider than the glass. All of this together is one affine transform: x = (a·X + b·Y + c) / div and y = (d·X + e·Y + f) / div. Six unknowns take three touches: the program draws a cross, waits for a touch and takes the mean codes of the valid bursts of the press. The 3 × 3 system is solved by Cramer's rule in integers — without the floating point the Cortex-M3 does not have; c and f do not fit in 32 bits:

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

In the model the panel is glued with a 1.2° rotation, and its X axis runs along the glass y and against it. The crosses are at the points (13, 16), (115, 80) and (64, 144); each touch gave 37, 37 and 38 valid bursts. Then the model touched a grid of 15 × 19 = 285 points, from edge to edge of the glass:

Conversion Mean error Worst Middle: mean / worst 10-pixel edge band: mean / worst
no calibration: X · 128 / 4096, Y · 160 / 4096 78.73 150.44 60.20 / 114.00 104.20 / 150.44
two touches, axes swapped by hand 1.60 4.59 1.22 / 3.82 2.11 / 4.59
three touches 0.29 2.00 0.10 / 1.00 0.56 / 2.00

Two points per axis — this is how the min/max limits in Zephyr and xPhys0/xPhys1 in emWin work — fix the scale, the offset and the direction of an axis, but not the rotation of the panel. Three points remove it too. What remains at the edges is the panel itself: near the edge the field of the layer is uneven (in the model the coordinate is compressed by up to 3 %), and a linear calibration cannot fix it. The integer conversion differed from floating point by no more than 0.5 pixel at all 285 points.

Storing in flash. The coefficients are 11 words, 44 bytes: the tag 0x4B32325A, a, b, d, e, c and f as two words each, div and the CRC-32 of the first 40 bytes (the CRC-32 of the string "123456789" = 0xCBF43926 is the check value). The firmware writes them into the last flash page in half-words, as in 11.5, and reads them at start-up: a valid record gives 0; one flipped bit gives −2; an erased page (all 0xFFFFFFFF) gives −1, and then calibration starts again. If the settings change often, they go into the journal from 11.8.

Screen rotation. After tft_rotate from 22з the same codes must give points in the new axes. cal_rotate recomputes the coefficients without new touches. Without it the mean error with a 90° rotation is 84.36 pixels, with 180° — 116.47, with 270° — 84.55; with it in all three — 0.29, worst 2.00.


22к.6 Gestures and PENIRQ: down, move, hold, up

While there is no touch, the bus is silent: the controller rests, PENIRQ is high. The first fall of PENIRQ wakes the main loop, then a burst runs every 10 ms until the touch ends. A step gives at most one event: "down" with a point, "move" when the point has gone more than 4 glass pixels away from where it was pressed, "hold" after 500 ms without moving, "up" with a "this was a tap" mark:

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

During a touch EXTI is disabled: every burst drives PENIRQ high by itself and drops it at the end, when the controller returns to rest (in the model; per the manufacturer's documentation, not checked). When enabling EXTI again, the firmware first writes a one into EXTI_PR: the bit is set by the selected edge and cleared by writing a one (RM0008, p. 212), and the fall from the last burst may already have come.

In the model 20 taps of 100…250 ms, 10 holds of 0.8…1 s and 10 drags of 30…60 pixels were all recognized: 20 of 20, 10 of 10 and 10 of 10; the end of a drag is 0.8 pixel from the finger on average, the worst — 1.8. With a move threshold of 0 instead of 4, of 20 taps only 2 stayed taps, and of 10 holds — none: the jitter of the finger by one pixel is already a "drag".

How to treat PENIRQ, the model checked on 10 touches of 300 ms over 8.8 s:

Firmware "Down" Bursts Of them without a touch Bus
EXTI disabled during a touch, PD = 00 at the end 10 340 32 0.85 %
EXTI always enabled 10 36 897 23 288 92.70 %
last command with PD = 11 1 34 4 0.09 %

With EXTI enabled, every burst triggers the next one by its own end: 4205 bursts per second, and not only during a touch — in the model after a burst the floating layer X charges through the pull-up for tens of microseconds, and PENIRQ manages to fall. With PD = 11 on the last command the controller does not return to rest, PENIRQ is silent, and of 10 touches only one is seen — the first.


22к.7 Pitfalls

The touch at display speed lies on a light press. The bus was handed to the touch, but BR was left from the display — 9 MHz. In the model with Rt = 1800 Ω the error at three points is 8.6, 4.0 and 4.1 glass pixels, at 1.125 MHz — 0.0, 1.0 and 0.0. A slow clock and a dropped first sample (22к.2).

Garbage on the screen where nobody drew. The display CS, as in 22з, is held low, and the touch bytes go to the display as pixels (D/C = 1 from the previous picture). In the model 100 polling bursts left 1500 foreign pixels in the frame memory. The display CS is raised before the touch CS is lowered.

BR was changed in the middle of a DMA transfer. The last words of the row go out at the touch speed. First the end of the transfer (TXE, then BSY), then reconfiguration with SPE = 0 (RM0008, p. 743–744).

The bus is busy even when nobody touches the screen. EXTI was left enabled, and PENIRQ itself wakes the next burst: 92.70 % of the bus in the model. Disable EXTI during a touch and clear EXTI_PR before enabling.

After the first touch the touchscreen "died". The last command of the burst has PD = 11: the controller does not return to rest, PENIRQ does not work. The last command must have PD = 00, as in Zephyr.

One touch gives two or three "down" events, points fly to a corner. Contact bounce: without the pressure threshold there are 74 extra "down" events per 150 touches and points up to 177.6 pixels away from the finger (22к.4).

The axes are mixed up, after rotating the screen the finger "misses". The names of channels X and Y are opposite in different drivers (Zephyr and Clarinox), and panels are glued any way. Three-point calibration swaps the axes by itself; after tft_rotate you need cal_rotate, otherwise the mean error is 84…116 pixels.

Accuracy is worse at the edges. The field of the layer near the bus bars is uneven: after calibration the worst error in the middle is 1 pixel, at the edge — 2. Buttons at the very edge of the screen are made larger, calibration crosses are placed 10 % from the edges.

A capacitive screen is not read this way. A capacitive one has no divider of its own: a controller on the glass counts touches by itself and gives coordinates over I2C (22а.6). The XPT2046 and everything above is only for resistive panels.


22к.8 Summary

Full sources

xpt22k.h — the XPT2046 command byte, the driver, calibration and events: types and prototypes.

/* 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 — the XPT2046 driver, median and thresholds, three-point calibration, the flash record and gestures, shared by the firmware and the bench.

/* 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 firmware: SPI1 for the display and the touch, PENIRQ on EXTI1, calibration in flash, finger drawing.

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

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

Open in the interactive handbook