H753_CPP_SPI_Resistive_Touch_Screen_Controller 1.0
STM32H753 SPI resistive touchscreen (Adafruit 333 overlay + 5767 TSC2046 controller) driver and tutorial
Loading...
Searching...
No Matches
H753_CPP_SPI_Resistive_Touch_Screen_Controller Documentation

This documentation covers a STM32 Nucleo-H753ZI tutorial project that reads a 4-wire resistive touchscreen overlay (Adafruit #333) through a TI TSC2046 SPI touch controller breakout (Adafruit #5767), built as two independent, reusable STM32 HAL libraries rather than one combined driver.

Project Overview

The firmware demonstrates:

  • A generic STM32 HAL SPI bus-abstraction layer (STM32SPIDevice, STM32BusIORegister), ported from the SPI-only subset of Adafruit's own bus-abstraction library, Adafruit_BusIO, that other future Adafruit-derived tutorials on this site can reuse as-is for a different chip on a different SPI peripheral
  • A TSC2046 touch-controller driver (TSC2046) built on top of that bus layer, porting the chip's differential/single-ended 12-bit ADC read modes, its control-byte layout, and the touch-pressure formula from Adafruit_TSC2046
  • Raw touch coordinate + pressure reads (TSC2046::getPoint()), not a driven display – this project's actual target hardware has a static printed panel (icons/labels) with the resistive overlay mounted on top, so there is no LCD/TFT anywhere in this design

Project-Level Documentation

Why two libraries, not one

Adafruit_TSC2046 depends on Adafruit_BusIO for its actual SPI transaction code – the touch-specific logic (control-byte layout, pressure formula) lives in Adafruit_TSC2046 itself, but the low-level "assert CS, clock bytes, deassert CS" plumbing is BusIO's, not TSC2046-specific. Porting only the TSC2046 driver and leaving that shared plumbing folded inside it would mean re-deriving the same SPI-transaction code again for the next Adafruit chip this site ports (a BusIO consumer other than TSC2046 is expected eventually, since BusIO backs most of Adafruit's SPI/I2C ecosystem). STM32BusIO is deliberately factored out as its own self-contained library up front instead, so that reuse doesn't require retroactively splitting TSC2046 apart later.

What was actually ported vs. left out

Both reference libraries are larger than what this project needs: Adafruit_BusIO also covers I2C devices, a "generic device" abstraction, and three other SPI addressing conventions besides the one TSC2046 uses (ADDRBIT8_HIGH_TOREAD); Adafruit_SPIDevice also has a software/bit-banged SPI fallback path with no STM32 HAL equivalent. None of that is ported – STM32SPIDevice and STM32BusIORegister cover exactly the hardware-SPI, MSB-first, ADDRBIT8_HIGH_TOREAD subset that TSC2046 (and presumably most future SPI-only Adafruit ports) actually needs. See each class's own file header comment for the specific reference-library feature it corresponds to and what was intentionally left out.

Two deliberate implementation differences from the reference libraries

  • STM32SPIDevice::writeThenRead() combines the write and read phases into a single HAL_SPI_TransmitReceive() call over one padded buffer, instead of the reference library's Arduino byte-at-a-time transfer() loop – STM32 HAL supports multi-byte buffer transfers directly, so there's no need to clock one byte at a time.
  • The TSC2046 control byte is built with explicit bit shifts (see buildControlByte() in TSC2046.cpp) rather than the reference library's packed bitfield union – C/C++ bitfield-to-byte packing order is implementation-defined, so explicit shifts are the portable choice even though this specific toolchain (GCC ARM) would likely have packed it the same way.

Practical use: from raw touch coordinates to a pressed button

The target device for this project has a static, printed graphic panel (icons, labels, buttons like "On"/"Off") with the transparent resistive overlay mounted directly on top of it – there is no active display anywhere in this design. TSC2046::getPoint() only reports a raw touch position and pressure; turning that into "the On button was pressed" additionally requires a calibration step (mapping the raw 0-4095 ADC range to the panel's physical coordinates) and a hit-test step (checking that mapped position against the known rectangle for each button/icon on that specific panel's layout). Both of those are application-level concerns built on top of this driver, not part of the driver itself. They are implemented in this project's application layer, described next.

The demo application: touch zones, LEDs and a buzzer

The printed panel has four rows: LED 1, LED 2 and LED 3, each split into an OFF half and an ON half, and a full-width BEEP row. The application layer turns raw readings into actions in three steps:

  • Touch detection (main.c). The panel is polled every 10 ms and a reading counts as a touch only if its pressure is between 40 and 1500 ohm and the point lies inside the accepted area. The pressure floor matters: an untouched TSC2046 returns junk (X = 0 or 3855, pressure 0-18 ohm) that must not be mistaken for a touch. The PENIRQ pin is deliberately not used, because it is disturbed by the polled SPI reads on this panel. A zone is acted on after 2 consecutive matching samples, and a press ends after 3 consecutive untouched samples.
  • Zone classification (touchZoneFromPoint, touchZoneName in touchZones.h). The boundaries come from touches measured on the real panel with the printed layout in place, not from the printed geometry, because a resistive panel is not perfectly linear.
  • Outputs. Each LED latches: its ON zone lights it and its OFF zone turns it off. The buzzer sounds only while the BEEP zone is held. The LEDs are wired from the 5 V rail through a resistor into the GPIO, and the buzzer is an active module that sounds when its signal pin is low, so in both cases the pin is driven low for "on" and high for "off", and every output starts high (dark and silent) at boot.

The CubeMX-generated main.c is plain C, so the C++ driver is reached through the C-callable functions in entryPointCPP.hpp (for example initTSC2046() and tsc2046GetPoint()). The driver object is constructed inside initTSC2046(), after the SPI peripheral is configured, rather than as a global that would construct too early.

Startup self-test

When TOUCH_SELFTEST_ENABLE is defined in main.h, touchSelfTestRunAll() (touchSelfTest.h) runs once at boot, before the touch loop starts, and prints a result for each of three checks: a raw SPI transfer completes, the response has the TSC2046's fixed frame shape (which catches a floating MISO line or an unpowered chip), and with the panel untouched no sample looks like a touch. It is read-only. A temperature-sensor check is deliberately left out, because the chip's on-die temperature reading is not accurate; see the note in touchSelfTest.h.