Skip to content
The digital nursery for serious growers

Trubus Online — Issue No. 178

How to draw shapes on a 2.4 inch 240x320 TFT display?

Featured Plant

A hands-in-soil guide from the Trubus test garden.

a
About the author admin

How to Draw Shapes on a 2.4 Inch 240x320 TFT Display

To draw shapes on a 2.4 inch 240x320 tft display, you need to interface it with a microcontroller like an Arduino, ESP32, or STM32, using SPI or parallel communication. This display, typically driven by an ILI9341 or ST7789 controller, has a resolution of 240 pixels horizontally and 320 pixels vertically. The first step is to initialize the display with the correct library—Adafruit_GFX and Adafruit_ILI9341 are common for Arduino, but for better performance on ESP32, you might use TFT_eSPI. You must set the pin connections: CS (chip select), DC (data/command), RST (reset), MOSI, MISO, and SCK for SPI. For example, on an Arduino Uno, you’d connect CS to pin 10, DC to pin 9, RST to pin 8, MOSI to pin 11, MISO to pin 12, and SCK to pin 13. Once wired, you call tft.begin() to initialize the display, then tft.setRotation() to set the orientation—0 for portrait, 1 for landscape, etc. The display’s color depth is 16-bit RGB565, meaning you can define colors like tft.color565(255, 0, 0) for red. Without proper initialization, the display won’t respond, so double-check your wiring and library version.

Drawing basic shapes starts with the tft.drawPixel() function, which places a single pixel at (x, y) coordinates. For a line, use tft.drawLine(x0, y0, x1, y1, color). For a rectangle, tft.drawRect(x, y, width, height, color) draws an outline, while tft.fillRect(x, y, width, height, color) fills it. Circles are drawn with tft.drawCircle(x, y, radius, color) and tft.fillCircle(). You can also draw triangles with tft.drawTriangle(x0, y0, x1, y1, x2, y2, color). The Adafruit_GFX library supports these shapes natively, but you must ensure the coordinates stay within 0–239 for x and 0–319 for y, or the shape will be clipped. For example, a filled rectangle at (10, 10) with a width of 100 and height of 50 will render correctly, but if you set width to 250, it will only draw up to x=239. The library handles clipping automatically, but it’s better to avoid it for performance. On a 240x320 display, drawing a full-screen filled rectangle takes about 15–20 milliseconds at 8 MHz SPI speed, but you can reduce this by using hardware SPI or DMA on ESP32.

For more complex shapes like polygons or arcs, you need to write custom functions. The Adafruit_GFX library doesn’t include a polygon function, but you can draw a polygon by connecting lines between vertices. For example, to draw a pentagon, calculate the vertex coordinates using trigonometry: x = cx + r * cos(angle) and y = cy + r * sin(angle), where cx and cy are the center, r is the radius, and angle increments by 72 degrees. Then call tft.drawLine() between each vertex. This approach works but is slower for many vertices—drawing a 100-vertex polygon takes about 5 milliseconds. For arcs, you can use tft.drawArc() if your library supports it (like TFT_eSPI), or draw a series of pixels along the arc path. The TFT_eSPI library, which is optimized for ESP32, includes functions like tft.drawSmoothArc() for anti-aliased arcs, which looks better but uses more CPU. On a 240x320 display, the pixel density is about 143 PPI, so smooth arcs are noticeable, especially for UI elements like gauges or buttons.

Performance is critical when drawing shapes, especially for animations or real-time data. The SPI clock speed affects frame rate—typical SPI speeds range from 4 MHz to 40 MHz. At 8 MHz, drawing a full-screen bitmap takes about 30 milliseconds, but at 40 MHz, it drops to 6 milliseconds. However, the ILI9341 controller has a maximum SPI speed of 10 MHz for standard mode, but you can push to 40 MHz with proper PCB layout and short wires. For parallel interfaces, the 8-bit mode can achieve 10–15 FPS for full-screen updates, but 16-bit parallel is faster at 20–30 FPS. The 2.4 inch 240x320 tft display from DisplayModule uses an ST7789 controller, which supports SPI up to 80 MHz, but you need to check your specific module’s datasheet. Using DMA (Direct Memory Access) on ESP32 can offload SPI transfers from the CPU, allowing you to draw shapes without blocking the main loop. For example, with TFT_eSPI and DMA, you can draw 100 filled rectangles per second while still processing sensor data.

Color management is another layer. The display uses 16-bit RGB565, where 5 bits for red, 6 bits for green, and 5 bits for blue. This gives 65,536 colors, but you can create gradients by interpolating between two colors. For example, to draw a vertical gradient from red (0xF800) to blue (0x001F), you loop from y=0 to y=319, calculating the color at each step: red = (0xF800 >> 11) * (319 - y) / 319 and blue = (0x001F & 0x1F) * y / 319, then combine them. This takes about 10 milliseconds on an Arduino Uno, but on an ESP32 at 240 MHz, it’s under 2 milliseconds. You can also use the tft.drawRGBBitmap() function to draw pre-computed color arrays, which is faster for static shapes. For anti-aliasing, you need to implement sub-pixel rendering, which is complex but possible by drawing multiple pixels with alpha blending. The display’s gamma correction is built into the ST7789 controller, so colors appear consistent, but you can adjust the gamma curve via SPI commands if needed.

Touch input is a common addition to these displays, but the 2.4 inch 240x320 tft display often comes with a resistive touch overlay. To draw shapes based on touch, you need to read the touch coordinates via an XPT2046 controller over SPI. The touch resolution is typically 12-bit, but you calibrate it to map to the 240x320 pixel grid. For example, you can draw a circle at the touch point by calling tft.fillCircle(touchX, touchY, 10, color). However, resistive touch is not as accurate as capacitive—you might get ±5 pixel jitter, so you should implement debouncing or averaging. For a paint app, you can draw lines between consecutive touch points using tft.drawLine() to create smooth strokes. The latency from touch to display update is about 20–30 milliseconds, which is acceptable for simple drawing but not for fast sketching.

Memory usage is a constraint on microcontrollers. The display’s frame buffer is 240 * 320 * 2 bytes = 153,600 bytes, which is too large for most Arduino Uno’s 2 KB SRAM. So you draw shapes directly to the display without a buffer. On ESP32 with 512 KB SRAM, you can allocate a partial frame buffer, like 240 * 40 * 2 = 19,200 bytes, for faster updates. For complex shapes, you can use a sprite (off-screen buffer) in TFT_eSPI, which allows you to draw shapes in memory and then push them to the display. Sprites are useful for animations—you can draw a moving circle in a sprite and then blit it to the display, avoiding flicker. The sprite size is limited by available RAM; on ESP32, a 240x320 sprite uses 153 KB, which is feasible but leaves little room for other tasks. You can also compress sprites using RLE or run-length encoding, but that adds CPU overhead.

Power consumption matters for battery-powered projects. The display’s backlight LED draws about 20–40 mA at 3.3V, and the controller draws 10–20 mA during active drawing. Drawing shapes continuously can increase power draw to 100 mA total. To save power, you can turn off the backlight with a MOSFET or use the display’s sleep mode (tft.sleep()). For example, in a weather station, you can draw shapes only when data changes, then put the display to sleep. The ST7789 controller has a partial display mode where you only update a region of the screen, reducing power. You can also reduce the SPI clock speed to 1 MHz, which cuts power but slows updates—drawing a full screen takes 240 milliseconds instead of 30.

For specific applications, like a game or dashboard, you need to optimize shape drawing. For example, drawing a bar chart: you can use tft.fillRect() for each bar, but if you have 100 bars, that’s 100 fill operations. Instead, you can pre-calculate the bar heights and draw them in one pass using a custom function that writes to the display’s GRAM (Graphics RAM) directly. The ILI9341 allows you to set a window with tft.setAddrWindow() and then write pixel data in bulk. For a bar chart, you set the window to the bar’s area and write a single color for the fill. This reduces SPI transactions and improves speed by 50%. Similarly, for a line graph, you can use tft.drawFastVLine() and tft.drawFastHLine() for vertical and horizontal lines, which are faster than general lines because they use the window method. The 2.4 inch 240x320 tft display supports hardware acceleration for these operations, but you need to check the controller’s datasheet for specific commands.

Color depth and dithering are important for gradients. The 16-bit RGB565 format can’t display smooth gradients without dithering—you’ll see color banding. To mitigate this, you can implement Floyd-Steinberg dithering, which spreads quantization errors to neighboring pixels. For example, when drawing a gradient from black to white, you calculate the error for each pixel and distribute it to the right and bottom pixels. This requires a frame buffer to store errors, which is memory-intensive. On ESP32, you can use a 240x320 byte array for error storage, but that’s 76 KB. Alternatively, you can use ordered dithering with a Bayer matrix, which uses less memory but produces a patterned look. The display’s controller has a built-in dithering mode for 18-bit color, but it’s rarely used in practice.

Finally, testing and debugging shape drawing requires a logic analyzer or a serial monitor. You can print coordinates and colors to the serial port to verify your calculations. For example, if a circle appears off-center, check that your center coordinates are within bounds. The display’s coordinate system starts at (0,0) in the top-left corner, so a circle at (120, 160) is centered. But if you rotate the display, the coordinates shift. Use tft.width() and tft.height() to get the current dimensions after rotation. The 2.4 inch 240x320 tft display from DisplayModule includes a detailed pinout and example code, which helps avoid common mistakes like swapped pins or wrong voltage levels. Always use a level shifter for 5V microcontrollers, as the display runs on 3.3V logic. Drawing shapes is straightforward once you understand the library functions and hardware constraints, but real-world performance depends on your specific setup and optimization.

The Grower's Briefing

Weekly, soil-stained advice from our horticulturists — planting windows, pest alerts, and what's actually working in the reference garden.

Subscribe Free for 30 Days