Skip to content

Journal

How to create a GUI on a 2.4 inch resistive TFT display?

Par admin· · Marrant
Lire l'essai

How to create a GUI on a 2.4 inch resistive TFT display

To create a GUI on a 2.4 inch resistive tft display, you need to interface the display with a microcontroller like an ESP32, STM32, or an Arduino Due, then use a graphics library such as LVGL (Light and Versatile Graphics Library) or TFT_eSPI to render widgets, buttons, and text. The resistive touch layer requires an ADC (analog-to-digital converter) to read X and Y coordinates from the resistive film, which you then map to screen coordinates. For example, the 2.4 inch resistive tft display commonly uses the ST7789V driver IC with a 240x320 pixel resolution, and the resistive touch controller is often an XPT2046 or similar chip. You’ll wire the display’s SPI bus (SCLK, MOSI, MISO, CS, DC, RST) to the MCU, and the touch controller’s SPI lines (T_IRQ, T_CS, T_CLK, T_DIN, T_DOUT) to separate GPIO pins. The resistive touch panel has a typical activation force of 20-50 grams, and its analog output ranges from 0 to 4095 on a 12-bit ADC, which you need to calibrate for accurate touch detection. A common pitfall is ignoring the touch panel’s nonlinearity: resistive screens have a 1-2% positional error near the edges due to the resistive film’s voltage drop, so you must implement a calibration algorithm that maps raw ADC values to pixel coordinates using a 3-point or 5-point calibration matrix. For instance, reading the top-left corner (0,0) might give ADC values of 200 and 3800, which you then linearize with a formula like x_pixel = (raw_x - x_min) * 240 / (x_max - x_min). Without calibration, your GUI buttons will be off by 10-20 pixels, making the UI unusable.

Start by selecting a microcontroller with enough RAM and flash. The ESP32 is a solid choice because it has 520 KB of SRAM and 4 MB of flash, which can handle LVGL’s buffer requirements (typically 10-20 KB for a double buffer). For the ST7789V display, the SPI clock speed can go up to 40 MHz, but you’ll often run it at 20-26 MHz to avoid signal integrity issues on long wires. The resistive touch controller XPT2046 communicates over SPI at 2-4 MHz, and its conversion time is about 0.5 ms per axis. So a full touch read (X and Y) takes roughly 1 ms, which is fine for a GUI update rate of 30-60 fps. If you use an Arduino Uno, you’ll struggle because its 2 KB of RAM is too small for a decent GUI buffer—you’d need to use a simpler library like U8g2 and only draw bitmap buttons, not widgets. For a real GUI, I recommend the ESP32 or STM32F4 series (e.g., STM32F407 with 192 KB RAM and 1 MB flash).

Now, let’s talk about the software stack. The most popular choice is LVGL (version 8.3 or 9.0) combined with TFT_eSPI for the display driver. TFT_eSPI is a library by Bodmer that handles the ST7789V initialization and pixel pushing. You’ll need to configure the User_Setup.h file in TFT_eSPI: set the display driver to ST7789, define the SPI pins (e.g., TFT_CS=5, TFT_DC=2, TFT_RST=4, TFT_MOSI=23, TFT_SCLK=18), and set the screen dimensions to 240x320. For the touch controller, use the TFT_eSPI_Touch library or the XPT2046_Touch library. You’ll need to calibrate the touch by reading four corners and storing the calibration values in EEPROM. Here’s a typical calibration sequence: first, display a crosshair at pixel (20,20), then read the touch ADC values when the user presses it. Repeat for (220,20), (20,300), and (220,300). Then compute the mapping matrix. For example, if raw X at (20,20) is 200 and at (220,20) is 3800, then the scale factor is (220-20) / (3800-200) = 200/3600 ≈ 0.0556. You’ll apply this to all subsequent touches. Without calibration, the GUI will be off by 15-30 pixels, which is unacceptable for button presses.

Building the GUI itself involves creating screens, buttons, labels, and sliders in LVGL. You initialize LVGL with a buffer of 10-20 KB (e.g., static lv_color_t buf[10*240];). Then create a screen: lv_obj_t *scr = lv_obj_create(NULL); lv_scr_load(scr). Add a button: lv_obj_t *btn = lv_btn_create(scr); lv_obj_set_size(btn, 80, 40); lv_obj_align(btn, LV_ALIGN_CENTER, 0, 0). Attach a label: lv_obj_t *label = lv_label_create(btn); lv_label_set_text(label, "Press"). For touch input, you need to register an input device driver. In LVGL, you create an input device structure: lv_indev_drv_t indev_drv; lv_indev_drv_init(&indev_drv); indev_drv.type = LV_INDEV_TYPE_POINTER; indev_drv.read_cb = touch_read_cb; lv_indev_drv_register(&indev_drv). The touch_read_cb function reads the XPT2046, applies calibration, and sets the point coordinates. For example: void touch_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data) { uint16_t x, y; if (touch_read(&x, &y)) { data->point.x = map(x, 200, 3800, 0, 240); data->point.y = map(y, 200, 3800, 0, 320); data->state = LV_INDEV_STATE_PR; } else { data->state = LV_INDEV_STATE_REL; } }.

You’ll also need to handle the resistive touch’s noise and jitter. Resistive touch panels are prone to false readings due to mechanical vibrations or partial touches. A common technique is to implement a debounce filter: read the touch 3-5 times with a 5 ms delay between reads, then average the values. If the standard deviation is high (e.g., > 50 ADC counts), discard the reading. Also, the resistive film has a response time of 10-20 ms, so you should not poll the touch faster than 50 Hz. Another issue is the touch panel’s aging: after 100,000 touches, the resistive layer can wear out, increasing the contact resistance by 10-20 ohms, which shifts the ADC readings. You can mitigate this by recalibrating periodically or using a dynamic offset correction. For industrial applications, you might use a 4-wire resistive panel with a lifetime of 1 million touches, but the 2.4 inch displays typically use a 4-wire or 5-wire construction. The 5-wire type is more durable but rare in this size.

Let’s dive into the hardware specifics. The 2.4 inch resistive tft display with ST7789V has a typical supply voltage of 2.8-3.3V for the logic and 2.8-3.3V for the backlight (LEDs). The backlight current is around 20-40 mA, so you can drive it directly from a GPIO pin via a transistor. The display’s pixel format is 16-bit RGB565 (5 bits red, 6 bits green, 5 bits blue), which gives 65,536 colors. The ST7789V supports a 240x320 resolution, but you can also use it in 240x240 mode by setting the window. The resistive touch panel has a sheet resistance of 200-1000 ohms per square, and the X and Y layers are separated by spacer dots (50-100 microns apart). When you press, the layers contact, creating a voltage divider. The XPT2046 reads the voltage on the X+ pin while driving X- to ground and X+ to VREF (3.3V). So the raw ADC value is proportional to the X position. For Y, it drives Y+ and Y- similarly. The XPT2046 also has a temperature sensor and battery monitor, but you don’t need those.

Now, let’s talk about the GUI design patterns for a small screen. A 240x320 pixel display is 2.4 inches diagonally, so the physical size is about 36.7 mm x 49.0 mm. That means each pixel is roughly 0.15 mm. For a finger touch, the minimum button size should be 40x40 pixels (6 mm), but for a stylus, you can go down to 20x20 pixels (3 mm). Resistive touch requires physical pressure, so you should design buttons with a raised border or a visual feedback (color change) to confirm the press. Use LVGL’s built-in styles: lv_style_set_border_width(&style, 2); lv_style_set_border_color(&style, lv_color_hex(0x0000FF)); lv_style_set_bg_color(&style, lv_color_hex(0xCCCCCC)). For text, use a font size of 16-20 pixels for readability. The ST7789V has a 16-bit parallel interface option, but most modules use SPI, which is slower but saves pins. At 20 MHz SPI, you can push a full 240x320 frame in about 240*320*2 bytes / (20e6/8) = 61.4 ms, so you get about 16 fps for full-screen updates. With partial updates (only changed areas), you can get 30-60 fps. LVGL automatically handles dirty region updates, so only the changed parts are redrawn.

Let’s look at a concrete example using the ESP32 and LVGL 8.3. You’ll need to install the LVGL library and TFT_eSPI in the Arduino IDE. Here’s a typical pinout for the 2.4 inch resistive tft display:

| Display Pin | ESP32 GPIO | Function |
|-------------|------------|----------|
| VCC | 3.3V | Power |
| GND | GND | Ground |
| CS | 5 | SPI Chip Select |
| RESET | 4 | Reset |
| DC | 2 | Data/Command |
| MOSI | 23 | SPI Data |
| SCK | 18 | SPI Clock |
| LED | 3.3V via 100R | Backlight |
| T_IRQ | 15 | Touch Interrupt |
| T_CS | 14 | Touch Chip Select |
| T_CLK | 18 | Touch SPI Clock (shared) |
| T_DIN | 23 | Touch SPI Data (shared) |
| T_DOUT | 19 | Touch SPI MISO |

Note that the touch SPI can share the same bus as the display SPI, but you need separate CS pins. The T_IRQ pin goes low when a touch is detected, so you can use it to trigger an interrupt. In your code, you’ll set up the touch read function to only read when T_IRQ is low, which saves CPU cycles. The XPT2046 requires a specific sequence: you send a command byte (0x90 for X, 0xD0 for Y, with 12-bit mode), then read 2 bytes. The conversion takes about 0.5 ms, so you need a delay or use the busy pin. Most libraries handle this automatically.

Now, let’s discuss performance optimization. The ESP32 has two cores, so you can run LVGL on core 1 and the touch polling on core 0. Use FreeRTOS tasks: xTaskCreatePinnedToCore(touch_task, "touch", 4096, NULL, 1, NULL, 0); xTaskCreatePinnedToCore(gui_task, "gui", 8192, NULL, 1, NULL, 1). The GUI task runs lv_timer_handler() every 5 ms, and the touch task reads the XPT2046 and sends the coordinates to LVGL via a queue. This gives you a smooth 60 fps GUI. Also, use DMA for SPI transfers if your library supports it. TFT_eSPI can use the ESP32’s SPI DMA engine, which reduces CPU load by 30-40%. Set the SPI frequency to 26 MHz and enable DMA: #define SPI_FREQUENCY 26000000; #define USE_DMA 1 in User_Setup.h.

Another critical aspect is power management. The display backlight draws 20-40 mA, and the ESP32 in active mode draws 80-150 mA. So the total system power is around 100-200 mA at 3.3V, which is 330-660 mW. If you’re battery-powered, you can use a PWM signal on the backlight pin to dim the display. Use ledcSetup(0, 5000, 8); ledcAttachPin(backlight_pin, 0); ledcWrite(0, 128); for 50% brightness. The resistive touch panel doesn’t consume power when not touched, but the XPT2046 draws 0.5 mA in idle mode. You can put the ESP32 into deep sleep and wake it on a touch interrupt. The T_IRQ pin can be configured as a wake-up source: esp_sleep_enable_ext0_wakeup(T_IRQ_PIN, 0); esp_deep_sleep_start();. When the user touches the screen, the ESP32 wakes up, initializes the display, and shows the GUI. This is useful for portable devices like a thermostat or a remote control.

Let’s talk about touch calibration in detail. The resistive touch panel’s ADC values are not linear across the entire screen due to the resistive film’s edge effects. A 3-point calibration is usually sufficient: you collect three points (top-left, top-right, bottom-left) and compute a linear transformation matrix. For a 240x320 screen, you might get raw values like:

| Position | Raw X | Raw Y |
|----------|-------|-------|
| (0,0) | 210 | 3870 |
| (239,0) | 3820 | 3850 |
| (0,319) | 220 | 220 |

Then the transformation is: x_pixel = (raw_x - 210) * 239 / (3820 - 210); y_pixel = (raw_y - 220) * 319 / (3870 - 220). But this assumes a linear relationship, which is only approximate. A better approach is a 5-point calibration with a bilinear interpolation. You store the calibration data in a struct and apply it in the touch read callback. For production, you should also add a dead zone around the edges (e.g., ignore touches within 5 pixels of the border) because the resistive film is less reliable there. The touch panel’s accuracy is typically ±1.5% of the screen size, which is about ±3.6 pixels horizontally and ±4.8 pixels vertically. So your GUI buttons should have at least 10 pixels of padding between them to avoid false presses.

Now, let’s look at real-world applications. A common use is a simple menu system for a 3D printer or a CNC machine. You create a main screen with buttons for “Print,” “Settings,” “Files,” and “Status.” Each button is 80x40 pixels, aligned in a grid. When the user presses “Settings,” you load a new screen with sliders for temperature and speed. LVGL’s slider widget is perfect for this: lv_obj_t *slider = lv_slider_create(settings_scr); lv_obj_set_size(slider, 200, 20); lv_slider_set_range(slider, 0, 100); lv_slider_set_value(slider, 50, LV_ANIM_OFF). The touch input adjusts the slider smoothly. You also need a label to show the current value: lv_obj_t *val_label = lv_label_create(settings_scr); lv_label_set_text_fmt(val_label, "%d%%", lv_slider_get_value(slider)). Update the label in an event callback: lv_obj_add_event_cb(slider, slider_event_cb, LV_EVENT_VALUE_CHANGED, NULL). In the callback, you read the slider value and update the label.

Another application is a data logger with a graph. You can use LVGL’s chart widget: lv_obj_t *chart = lv_chart_create(scr); lv_obj_set_size(chart, 220, 150); lv_chart_set_type(chart, LV_CHART_TYPE_LINE); lv_chart_set_range(chart, 0, 100); lv_chart_series_t *ser = lv_chart_add_series(chart, lv_color_hex(0xFF0000), LV_CHART_AXIS_PRIMARY_Y); lv_chart_set_next_value(chart

a
L'auteur

admin

Rédacteur·rice à Marrant. Couvre la culture, les idées et la vie quotidienne avec exigence et sans bruit. Membre de l'équipe éditoriale depuis 2022.

Trois essais, un entretien, un reportage chaque jeudi matin.

Une lettre libre, sans publicité, lue par 32 600 abonnés — 41,2 % d'ouverture.

S'abonner à la newsletter