4 inch ST7796 touchscreen full working config

Hey you poor bastard trying to add a cheap 4 inch ST7796 touchscreen from Ali/Temu whatever to HA. Hopefully you found this thread sooner rather than later, but in any case, here we go.
So, the screen is weird. To make sure its the same one, here are pics:

It uses an ST7796 driver, but whatever you found so far, at least at the time of this writing, ilixxx drivers do not work.
This Github thread was a good starting point, but I could not make the ilixxx drivers work.
What I ended up doing instead is using the mipi_spi display driver. It’s … not great, but it’s the only one I could wrestle into submission on this panel.
That being, said, the driver is also lacking (or at least I could not find them) proper rotation and mirroring functions, so I had to get a bit … creative in solving some problems. Basically the screen is physically rotated 180 and since the touch coordinates are now wonky, I corrected that in code. If it’s stupid but it works … :man_shrugging:
Anywho, the config gives you:
1: The wiring I used for screen and touch, just read the pins from the Yaml
2: A fully functional, but wonky, screen
3: Fully functioning touch functionality with entities that show in HA the coords where a touch occurred.

globals:
  - id: last_color
    type: std::string
    initial_value: "\"\""

font:
  - file: "gfonts://Roboto"
    id: default_font
    size: 24

spi:
  - id: spi_tft
    clk_pin: 18
    mosi_pin: 23
    miso_pin: 19

  - id: spi_touch
    clk_pin: 25
    mosi_pin: 32
    miso_pin: 34

display:
  - platform: mipi_spi
    model: st7796
    spi_id: spi_tft
    cs_pin: 5
    dc_pin: 17
    reset_pin: 16

    rotation: 90
    spi_mode: 3
    color_order: bgr
    pixel_mode: 16bit

    auto_clear_enabled: true

    lambda: |-
      it.fill(Color(0, 0, 0));

      // ---- CORNER COLOR BOXES ----
      it.filled_rectangle(0, 0, 40, 40, Color(255, 0, 0));                               // RED TL
      it.filled_rectangle(it.get_width()-40, 0, 40, 40, Color(0, 255, 0));               // GREEN TR
      it.filled_rectangle(0, it.get_height()-40, 40, 40, Color(0, 0, 255));              // BLUE BL
      it.filled_rectangle(it.get_width()-40, it.get_height()-40, 40, 40, Color(255,255,0)); // YELLOW BR

      // ---- ARROWS + LABELS ----
      int cx = it.get_width() / 2;
      int cy = it.get_height() / 2;

      // UP ↑
      it.line(cx, cy, cx, cy-60, Color(255,255,255));
      it.line(cx, cy-60, cx-10, cy-50, Color(255,255,255));
      it.line(cx, cy-60, cx+10, cy-50, Color(255,255,255));
      it.printf(cx, cy-80, id(default_font), Color(255,255,255), TextAlign::CENTER, "UP");

      // DOWN ↓
      it.line(cx, cy, cx, cy+60, Color(255,255,255));
      it.line(cx, cy+60, cx-10, cy+50, Color(255,255,255));
      it.line(cx, cy+60, cx+10, cy+50, Color(255,255,255));
      it.printf(cx, cy+80, id(default_font), Color(255,255,255), TextAlign::CENTER, "DOWN");

      // LEFT ←
      it.line(cx, cy, cx-60, cy, Color(255,255,255));
      it.line(cx-60, cy, cx-50, cy-10, Color(255,255,255));
      it.line(cx-60, cy, cx-50, cy+10, Color(255,255,255));
      it.printf(cx-90, cy, id(default_font), Color(255,255,255), TextAlign::CENTER, "LEFT");

      // RIGHT →
      it.line(cx, cy, cx+60, cy, Color(255,255,255));
      it.line(cx+60, cy, cx+50, cy-10, Color(255,255,255));
      it.line(cx+60, cy, cx+50, cy+10, Color(255,255,255));
      it.printf(cx+90, cy, id(default_font), Color(255,255,255), TextAlign::CENTER, "RIGHT");

      // ---- Touch readout as text ----
      if(id(last_color) != "") {
        it.printf(cx, cy+120, id(default_font), Color(255,255,255), TextAlign::CENTER, "%s", id(last_color).c_str());
      }

output:
  - platform: ledc
    id: tft_bl
    pin: 21
    frequency: 20000 Hz

light:
  - platform: monochromatic
    id: backlight
    output: tft_bl
    restore_mode: ALWAYS_ON

text_sensor:
  - platform: template
    name: "Touch Raw X"
    id: raw_x
    update_interval: never

  - platform: template
    name: "Touch Raw Y"
    id: raw_y
    update_interval: never


sensor:
  - platform: template
    name: "Touch Raw X Numeric"
    id: raw_x_sensor
    update_interval: 50ms
    lambda: |-
      return id(raw_x).state != "" ? atof(id(raw_x).state.c_str()) : 0;

  - platform: template
    name: "Touch Raw Y Numeric"
    id: raw_y_sensor
    update_interval: 50ms
    lambda: |-
      return id(raw_y).state != "" ? atof(id(raw_y).state.c_str()) : 0;


touchscreen:
  - platform: xpt2046
    id: ts
    spi_id: spi_touch
    cs_pin: 22

    calibration:
      x_min: 300
      x_max: 3900
      y_min: 300
      y_max: 3900

    transform:
      swap_xy: true

    on_touch:
      then:
        - lambda: |-
            int X = touch.x_raw;
            int Y = touch.y_raw;

            // 🔥 FIXED MAPPING — TR <-> BL swap corrected
            if (X > 3300 && Y > 3300) id(last_color) = "RED (TL)";
            else if (X < 900  && Y > 3300) id(last_color) = "GREEN (TR)";  // swapped here
            else if (X > 3300 && Y < 900)  id(last_color) = "BLUE (BL)";   // swapped here
            else if (X < 900  && Y < 900)  id(last_color) = "YELLOW (BR)";
            else id(last_color) = "CENTER";

Once on the WROOM, and I think it’s kind of a must for it to be a WROOM from what I understood from the Github post, you should get this on the screen:

Touch functionality can be tested by clicking on the colored squares. When you click on a square, it should write on the screen the color of the square and it’s location (TL - top left). Exact touch coordinates from the screen can be seen in logs.
Hope this helps someone out and avoids the hours and hours of LLM prompting to get this running.

Hi, I used your code with a 3.5 capactive screen ST7796.

Screen show the square colors, but change to yellow on start and when I touch screen nothing happens.

On logs show many times the same code…

[21:38:18.576][D][sensor:133]: 'Touch Raw Y Numeric': Sending state 0.00000 with 1 decimals of accuracy
[21:38:18.576][D][sensor:133]: 'Touch Raw X Numeric': Sending state 0.00000 with 1 decimals of accuracy
[21:38:18.588][D][xpt2046:055]: Touchscreen Update [0, 0], z = 4095
[21:38:18.627][D][sensor:133]: 'Touch Raw Y Numeric': Sending state 0.00000 with 1 decimals of accuracy
[21:38:18.630][D][sensor:133]: 'Touch Raw X Numeric': Sending state 0.00000 with 1 decimals of accuracy
[21:38:18.641][D][xpt2046:055]: Touchscreen Update [0, 0], z = 4095
[21:38:18.678][D][sensor:133]: 'Touch Raw Y Numeric': Sending state 0.00000 with 1 decimals of accuracy
[21:38:18.680][D][sensor:133]: 'Touch Raw X Numeric': Sending state 0.00000 with 1 decimals of accuracy
[21:38:18.694][D][xpt2046:055]: Touchscreen Update [0, 0], z = 4095