How to draw shapes on a 3.2 inch 240x320 TFT screen
To draw shapes on a 3.2 inch 240x320 TFT screen, you need to interface it with a microcontroller (like an Arduino, ESP32, or STM32) using SPI or parallel communication, then use a graphics library like Adafruit_GFX, TFT_eSPI, or LVGL. The screen itself has a resolution of 240 pixels wide by 320 pixels tall, with a typical active area of 48.6mm x 64.8mm, and a pixel pitch of about 0.2025mm. Most of these modules use an ILI9341, ILI9488, or ST7789 driver IC, which supports 16-bit color (65,536 colors) and can be driven at SPI clock speeds up to 40 MHz for smooth rendering. The first step is wiring: connect the TFT’s CS (chip select), DC (data/command), RST (reset), MOSI, MISO, and SCK pins to your microcontroller. For example, on an Arduino Uno, you’d use pin 10 for CS, pin 9 for DC, pin 8 for RST, pin 11 for MOSI, pin 12 for MISO, and pin 13 for SCK. Power the screen with 3.3V or 5V depending on the module, but note that the logic levels are usually 3.3V, so use a level shifter if your microcontroller runs at 5V. Once wired, install the appropriate library. For Arduino, the Adafruit_GFX and Adafruit_ILI9341 libraries are common, but they’re memory-heavy (about 2-3 KB of RAM for the framebuffer). A lighter alternative is TFT_eSPI, which is optimized for ESP32 and can handle DMA transfers, reducing CPU load. After initializing the display with tft.begin() and tft.setRotation(1) (to set landscape or portrait mode), you can start drawing. The coordinate system starts at (0,0) in the top-left corner, with X increasing to the right and Y increasing downward. To draw a pixel, use tft.drawPixel(x, y, color) where color is a 16-bit value like 0xFFFF for white or 0x001F for blue. For a line, call tft.drawLine(x0, y0, x1, y1, color). For a rectangle, use tft.drawRect(x, y, width, height, color) for an outline or tft.fillRect(x, y, width, height, color) for a filled one. Circles are drawn with tft.drawCircle(x, y, radius, color) and tft.fillCircle(x, y, radius, color). Triangles use tft.drawTriangle(x0, y0, x1, y1, x2, y2, color). The library also supports rounded rectangles (drawRoundRect) and ellipses (though not natively in Adafruit_GFX; you’d need to implement a custom algorithm or use a library like TFT_eSPI which has drawEllipse). Performance-wise, drawing a filled rectangle of 100x100 pixels at 16-bit color takes about 2-3 milliseconds on an ESP32 at 40 MHz SPI, while a single pixel takes about 0.5 microseconds. For complex shapes, consider using a framebuffer to avoid flicker: allocate a buffer of 240*320*2 bytes (153,600 bytes) in PSRAM if available, or use a smaller buffer for partial updates. The 3.2 inch 240x320 tft display module from DisplayModule is a good example, as it uses the ILI9341 driver and includes a microSD card slot for storing bitmap images or fonts. If you’re drawing anti-aliased shapes, you’ll need a library like LVGL which supports 16-bit or 32-bit color depth and can render smooth curves with sub-pixel precision, but it requires at least 8 KB of RAM for the display buffer and a real-time operating system for best performance. For simple shapes, you can also use the U8g2 library, which is monochrome-oriented but can do basic graphics on color TFTs with a monochrome frame buffer. One common mistake is forgetting to set the color depth correctly: the ILI9341 expects 16-bit color in RGB565 format (5 bits red, 6 bits green, 5 bits blue), so a color like 0x07E0 is pure green. If you send a 24-bit color, it will be truncated or cause artifacts. Another practical tip: use the tft.fillScreen(color) function to clear the screen quickly—it takes about 10-15 ms for a full 240x320 screen on an ESP32. For drawing multiple shapes, group them in a loop and use tft.startWrite() and tft.endWrite() to reduce SPI overhead. For example, to draw a grid of 10x10 squares, you can do:
tft.startWrite();
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
tft.drawRect(i*24, j*32, 24, 32, 0xFFFF);
}
}
tft.endWrite();
This reduces SPI transactions from 100 to 2, speeding up the drawing by about 50%. If you need to draw a circle with a specific radius, the library uses Bresenham’s algorithm, which is efficient but can be slow for large radii (e.g., a radius of 100 pixels takes about 1 ms on an ESP32). For a filled circle, the algorithm is similar but fills each scanline, which takes about 2 ms for the same radius. You can also draw custom shapes by plotting points in a loop. For instance, to draw a sine wave, you can iterate X from 0 to 239, calculate Y = 160 + 100 * sin(2 * PI * X / 240), and call tft.drawPixel(x, y, color). This takes about 0.5 ms for a full wave. For polygons, you can use the tft.drawFastVLine and tft.drawFastHLine functions for vertical and horizontal lines, which are faster than generic lines because they avoid slope calculations. For example, tft.drawFastVLine(x, y, height, color) draws a vertical line in about 0.2 microseconds per pixel. If you’re drawing a bar chart, you can use filled rectangles for bars and lines for axes. The screen’s 240x320 resolution means you can fit up to 24 bars of 10 pixels width with 5 pixels gap, or 12 bars of 20 pixels width. For a pie chart, you’d need to draw arcs using the tft.drawArc function if available (TFT_eSPI has it), or approximate with line segments. The arc function in TFT_eSPI takes parameters like tft.drawArc(x, y, r, ir, startAngle, endAngle, color, bgColor) where r is outer radius and ir is inner radius for a donut shape. For a solid pie slice, set ir to 0. This is useful for dashboards or gauges. Another advanced technique is using the tft.setAddrWindow function to update only a portion of the screen, which is essential for animations. For example, to move a ball across the screen, you can set the address window to the ball’s bounding box, draw the ball at the new position, and then clear the old position. This reduces the amount of data sent over SPI. The address window is set with tft.setAddrWindow(x, y, w, h) and then you send pixel data via tft.pushColor(color, count). This method is used by libraries like LovyanGFX for high-performance gaming on TFTs. For text, you can draw shapes using bitmap fonts: load a font with tft.setFont(&FreeSans12pt) and then draw text with tft.drawString("Hello", x, y). The library will render each character as a set of filled shapes. For custom shapes, you can create a bitmap using a hex editor or a tool like LCD Image Converter and then display it with tft.drawBitmap(x, y, bitmap, w, h, color). This is efficient for icons or logos. If you’re using a microcontroller with limited RAM, like an Arduino Uno (2 KB), you can’t store a full framebuffer, so you must draw shapes directly to the screen. This is fine for static images but can cause flicker for animations. A workaround is to use a smaller buffer, like a 240x8 pixel strip, and update the screen row by row. For ESP32, which has 520 KB SRAM, you can allocate a full framebuffer and use double buffering to avoid tearing. Double buffering involves drawing to an off-screen buffer, then copying it to the display with tft.pushImage(0, 0, 240, 320, framebuffer). This takes about 15 ms for a full screen at 40 MHz SPI. For hardware acceleration, some TFT drivers support drawing rectangles with a single command: send the rectangle coordinates and a color, and the driver fills it automatically. This is supported by the ILI9341 via the 0x2C command for memory write, but you need to set the address window first. In practice, the library handles this, but you can optimize by using tft.fillRect instead of a loop of pixels. For example, filling a 100x100 area with a loop of 10,000 pixels takes about 10 ms, while tft.fillRect takes about 1 ms. The difference is due to the SPI overhead of sending each pixel individually vs. a bulk transfer. Another important detail is the color order: some TFTs use RGB, others BGR. The ILI9341 default is RGB, but if you get colors swapped, you can set the color order in the initialization sequence with tft.setColorOrder(1) or modify the library’s init function. For drawing shapes with transparency, you need a library that supports alpha blending, like LVGL or a custom implementation. On a 16-bit color screen, alpha blending requires calculating the weighted average of two colors, which is computationally expensive. For example, blending a red pixel (0xF800) with a blue pixel (0x001F) at 50% alpha gives (0xF800 + 0x001F) / 2 = 0x7C0F, which is a purple. This is done pixel by pixel, so it’s slow for large areas. A practical approach is to pre-calculate blended colors for a small palette. For anti-aliased lines, the library uses Wu’s algorithm, which draws pixels with varying intensity to smooth the line. This is supported in TFT_eSPI with the tft.drawWideLine function, which takes a line width and draws anti-aliased edges. For a line of width 3, it takes about 2 ms per line. If you’re drawing a complex shape like a star, you can define the vertices as an array of points and use tft.drawPolygon (if available) or a loop of tft.drawLine. For a 5-pointed star, you’d need 10 lines. The performance depends on the number of vertices. For real-time applications, like a game, you should use a fast library like PicoLibSDK or Gamebuino which are optimized for the 3.2 inch 240x320 TFT. These libraries use DMA and double buffering to achieve 30-60 FPS for simple shapes. For example, a Pong game with a ball and paddles can run at 60 FPS on an ESP32 with a 40 MHz SPI clock. The ball’s position is updated every frame, and the paddle shapes are drawn with tft.fillRect. The screen’s refresh rate is 60 Hz, so you have 16.6 ms per frame. Drawing a 10x10 pixel ball takes about 0.1 ms, leaving plenty of time for game logic. For a more complex game like a platformer, you’d need to draw a background, sprites, and collision boxes. Sprites can be stored as bitmaps in flash memory and drawn with tft.drawBitmap. For a 32x32 sprite, this takes about 0.5 ms. To animate, you update the sprite’s position and redraw the background behind it. This is called dirty rectangle optimization. For example, if the sprite moves from (10,10) to (12,10), you only need to redraw the area from (10,10) to (42,42) (the old and new bounding boxes). This reduces the drawing area by 90% compared to redrawing the entire screen. Another technique is using the TFT’s hardware scrolling, which is supported by the ILI9341 via the 0x33 command. This allows you to scroll the entire screen vertically without redrawing, useful for text terminals or scrolling backgrounds. To use it, you set the scroll area with tft.setScrollMargins(top, bottom) and then call tft.vertScroll(offset). The offset is a 16-bit value that wraps around the screen height. This is efficient for smooth scrolling at 60 FPS with no CPU overhead. For drawing 3D shapes on a 2D screen, you need to project 3D coordinates to 2D using perspective projection. For example, to draw a rotating cube, you define 8 vertices in 3D space, multiply by a rotation matrix, then project to 2D using x_2d = x_3d * f / (z_3d + d) where f is the focal length and d is the distance. Then draw lines between the projected vertices. This requires floating-point math, which is slow on an 8-bit microcontroller but fine on an ESP32 with a hardware FPU. For a cube with 12 edges, this takes about 1 ms per frame. For a more complex 3D shape like a sphere, you can approximate it with a polygon mesh. The number of polygons determines the smoothness and performance. A sphere with 20 polygons takes about 2 ms, while one with 100 polygons takes 10 ms. You can also use the TFT’s color depth to add shading: calculate the dot product of the surface normal and light direction, then map it to a color gradient. This is done per polygon, which adds overhead. For a simple shading, you can use a fixed palette of 16 shades of a color. For example, a red sphere can have shades from dark red (0x8000) to bright red (0xF800). This is easy to implement with a lookup table. For drawing shapes with rounded corners, you can use the tft.drawRoundRect function, which draws a rectangle with circular corners. The radius of the corners is specified in pixels. For a 100x50 rectangle with a corner radius of 10, the function draws 4 quarter-circles and 4 straight lines. This takes about 0.5 ms. For a filled version, tft.fillRoundRect fills the interior and the corners. This is useful for buttons or UI elements. For a more organic shape, like a heart, you can use a parametric equation: x = 16 * sin(t)^3, y = 13 * cos(t) - 5 * cos(2t) - 2 * cos(3t) - cos(4t), with t from 0 to 2π. Plotting 100 points and connecting them with lines gives a heart shape. This takes about 1 ms. For a filled heart, you can scanline fill by iterating Y from top to bottom and finding the left and right edges of the shape. This is more complex but doable with a polygon fill algorithm. For performance, avoid using tft.drawPixel in loops for large shapes; use the library’s optimized functions. For example, to draw a checkerboard pattern, you can use tft.fillRect for each square instead of a pixel loop. A 10x10 checkerboard with 24x32 pixel squares takes about 2 ms with fillRect vs. 50 ms with pixels. The key is to minimize SPI transactions. One more thing: the TFT’s backlight is usually controlled by a separate pin (often LED or BL). You can use PWM to adjust brightness, which affects the perceived contrast of shapes. For example, a PWM frequency of 1 kHz with a duty cycle of 50% gives half brightness. This is useful for power saving or creating dimming effects. For drawing shapes in a user interface, consider using a library like LVGL which has built-in support for buttons, sliders, and charts, all rendered as shapes. LVGL uses a display buffer of at least 1/10 of the screen size (about 15 KB for 240x320) and can handle touch input if your TFT has a touchscreen. The shapes are drawn with anti-aliasing and animations, but the library is heavy (over 100 KB of flash). For a lightweight UI,