Bare-Metal Ada on the ESP32-S3 A step-by-step guide to running Ada on the ESP32-S3 with no ESP-IDF, no FreeRTOS, and no Python.

Step 30 of 56

ST7789 display and GT911 touch

A write-only SPI display that cannot be probed, and a touch controller whose I2C address depends on a pin level at reset — the two halves of a touchscreen, each with its own trap.

ST7789: no reads, no probe

The panel never talks back. The ST77xx SPI interface is write-only as wired here, so there is no status read and no way to probe for the device. If nothing appears, the driver cannot tell you whether the panel is absent, mis-wired or simply off — you get silence either way. Check wiring before suspecting code.

Three GPIOs are driven directly by the driver: DC (data/command), CS, and an optional RST. Pass No_Pin for RST and it falls back to a software reset.

type Color is mod 2**16;                                   --  RGB565, MSB-first
function RGB (R, G, B : Natural) return Color;             --  each 0 .. 255
type Color_Array is array (Natural range <>) of Color;      --  row-major
type Rotation is (Rot_0, Rot_90, Rot_180, Rot_270);

Bring-up is Setup (records the wiring and geometry, brings the SPI host up in mode 0) then Acquire for an exclusive session, then Init to run the power-on sequence.

procedure Init         (S : Session);
procedure Display_On   (S : Session);
procedure Set_Rotation (S : Session; Rot : Rotation);        --  sets MADCTL
procedure Invert       (S : Session; On : Boolean);
procedure Sleep        (S : Session; On : Boolean);

procedure Fill        (S : Session; C : Color);
procedure Fill_Rect   (S : Session; X, Y, W, H : Natural; C : Color);
procedure Set_Pixel   (S : Session; X, Y : Natural; C : Color);
procedure Draw_Bitmap (S : Session; X, Y, W, H : Natural; Pixels : Color_Array);

Two levels of locking

The display has two guards, not one. The Session owns the display exclusively, while each operation locks the SPI host only for its own transfer — so another device on the same bus can interleave between your drawing calls, and a long Fill does not monopolise the bus.

Those per-display guards are a fixed library-level array keyed by the CS pin, since a GPIO uniquely identifies one display. That is why no protected object lives inside a Device, and why Device values are cheap to hold.

GT911: the address is decided at reset

A Goodix 5-point capacitive controller on I2C. It is a 16-bit-register device: every transaction sends the register address MSB-first, then reads or writes a run of bytes from the chip's auto-incrementing pointer. Multi-byte values inside the map — coordinates, firmware version, output range — are little-endian, so the address and the payload disagree about byte order.

The chip latches its I2C address from the INT level while RST is released: INT low gives 0x5D (the usual module strapping), INT high gives 0x14. This driver never drives INT or RST, because on many boards RST is not even on an ESP pin — the Waveshare ESP32-S3-Touch-LCD-7 routes it through a CH422G expander with INT weakly low. Releasing reset is therefore board wiring you do once at startup, before touching the chip. Get the order wrong and the part answers at the other address, which looks exactly like a dead device.

Reading touches

type Touch_State is record ... end record;    --  up to 5 points, each with a track id
type Status is (OK, Bus_Error);

procedure Read_Touches (Dev : Device; State : out Touch_State; Result : out Status);
procedure Read_Product_Id, Read_Firmware_Version, Read_Resolution ...

The chip scans continuously and latches one report per scan cycle: a status register with a buffer-ready flag and point count, then up to five track-id/X/Y/size records. Read_Touches drains one report and re-arms the latch by writing the flag back to zero — so forgetting to call it stalls further reports rather than queueing them.

The INT line pulses on every fresh report. Attach a handler with the .Interrupts child and call Read_Touches from a task it wakes — remembering the callback rules: latch a flag in the handler, do the I2C work at task level, never from the ISR.

Like I2C's other clients, the driver hard-codes no wiring: you tell Setup the host, the SDA/SCL pins, the optional INT pin and the address, and each operation opens a short-lived controlled session for one complete transaction.