Skip to content
Barcamp Bordeaux Édition 2025 · 12e édition

How to display a bitmap on a 2.4 inch 240x320 TFT display?

aÉcrit par admin · Édition 2025

To display a bitmap on a 2.4 inch 240x320 TFT display, you need to convert the image into a raw pixel data array that the display controller can directly read and render, then write that data to the display’s frame buffer via SPI or MCU parallel interface. For a 16-bit color depth (RGB565), each pixel requires 2 bytes, so a full 240x320 bitmap consumes 153,600 bytes (240 * 320 * 2). The process involves selecting a compatible controller (like ILI9341, ILI9325, or ST7789), initializing the display with correct timing parameters, and then sending the bitmap data row by row or using a DMA transfer for speed. If you’re using a microcontroller like STM32 or ESP32, you’ll need to allocate a buffer at least 3200 bytes for partial updates, but for full-screen bitmaps, external flash storage is often necessary. The 2.4 inch 240x320 tft display commonly uses SPI mode with 4-wire or 3-wire interface, and its maximum pixel clock can reach 10 MHz, yielding a theoretical frame rate of about 20 fps for full-screen bitmaps. Real-world performance drops due to command overhead and MCU processing, so expect 5–15 fps depending on your code efficiency.

Understanding the Display Controller and Interface

The 2.4-inch TFT display typically integrates a controller chip such as ILI9341, which supports both SPI and 8-bit parallel interfaces. The ILI9341 has a 240x320 resolution with 262K colors (RGB565 format). Its internal frame buffer is 172,800 bytes (240 * 320 * 18 bits per pixel, but often stored as 16-bit), but you can write directly to the GRAM (Graphics RAM) using a window address command. The SPI interface, running at 4–10 MHz, sends 8-bit commands and 16-bit data. For a bitmap, you first set the column (0 to 239) and page (0 to 319) addresses using commands 0x2A and 0x2B, then write pixel data via command 0x2C. Each pixel is two bytes: high byte (bits 15–8) and low byte (bits 7–0), with color format R5G6B5. For example, pure red is 0xF800, green is 0x07E0, blue is 0x001F. If your display uses a different controller like ILI9325, the command set changes slightly, but the principle remains the same. The parallel interface, if available, uses 8 data lines and control signals (RS, WR, RD, CS, RESET), offering faster transfer but requiring more GPIO pins. For most hobbyist projects, SPI is simpler and sufficient for static images.

Bitmap Conversion and Data Format

Before you can display a bitmap, you must convert it from a standard format (BMP, PNG, JPEG) to a raw RGB565 byte array. Tools like ImageMagick, Python’s PIL library, or online converters can do this. For a 240x320 image, the output is a binary file of 153,600 bytes. Each pixel’s 16-bit value is stored in little-endian order (low byte first) if your MCU is little-endian, or big-endian if the controller expects it. The ILI9341 expects data in big-endian order (high byte first), so you may need to swap bytes. For example, a pixel with RGB565 value 0x07E0 (green) becomes two bytes: 0x07 then 0xE0. If your converter outputs little-endian, you’ll see 0xE0 0x07, which displays incorrect colors. Always check the byte order by sending a test pattern. Another approach is to store the bitmap as a C array in program memory (PROGMEM on AVR) or in external SPI flash. For a 240x320 image, the array size is 153,600 bytes, which exceeds the RAM of most MCUs (e.g., Arduino Uno has only 2 KB SRAM). So you must read the data from SD card, external flash, or stream it from a computer. The conversion process also involves dithering if the original image has more than 65K colors, but for most photos, 16-bit color depth is sufficient. Below is a table showing typical conversion parameters:

Image FormatInput SizeOutput Size (RGB565)Byte OrderTool Example
24-bit BMP240x320 x 3 bytes = 230,400 bytes153,600 bytesBig-endian (MSB first)ImageMagick: convert in.bmp -depth 8 -colorspace RGB out.rgb
JPEGVariable (compressed)153,600 bytesLittle-endian (LSB first)Python PIL: img.convert('RGB').tobytes()
PNGVariable153,600 bytesDepends on MCUOnline converter: select “RGB565 raw”

Initialization Sequence and Timing

Before sending bitmap data, the display must be initialized with a specific sequence of commands. For the ILI9341, this includes resetting the display (low pulse on RESET pin for at least 10 ms), then sending commands like 0x01 (software reset), 0x11 (sleep out), 0x29 (display on), and configuring the pixel format (0x3A, 0x55 for 16-bit). The initialization sequence is typically 30–50 commands, each with parameters. Timing is critical: after each command, you need a delay of 5–120 ms depending on the command. For example, after sending 0x11, wait 120 ms for the display to wake up. If you skip delays, the display may not respond correctly. The SPI clock frequency should be set to 4–10 MHz; higher speeds can cause data corruption if wiring is long. For parallel interface, the write cycle time is about 100 ns, so you can achieve faster updates. A typical initialization sequence for a 2.4 inch 240x320 tft display includes setting the memory access control (0x36) to define orientation, and setting the column and page address order. If you’re using a library like Adafruit_GFX or TFT_eSPI, these sequences are pre-configured, but you can customize them for your specific display. The table below lists key initialization commands:

CommandHex CodeParametersPurpose
Software Reset0x01NoneResets display controller
Sleep Out0x11NoneExits sleep mode, wait 120 ms
Pixel Format0x3A0x55 (16-bit)Sets color depth to RGB565
Memory Access Control0x360x48 (portrait)Sets orientation and RGB order
Display On0x29NoneTurns on display, wait 20 ms

Writing Bitmap Data to the Display

Once the display is initialized, you write the bitmap by setting a window (column and page range) and then sending the pixel data. The window is set with commands 0x2A (column address set) and 0x2B (page address set). For a full-screen bitmap, the column range is 0 to 239 (two bytes each: 0x00, 0x00 and 0x00, 0xEF), and the page range is 0 to 319 (0x00, 0x00 and 0x01, 0x3F). Then you send command 0x2C (memory write) and stream the 153,600 bytes of pixel data. The data must be sent continuously without gaps, as the controller expects a sequential stream. If you pause, the display may interpret the next bytes as commands. To avoid this, use a hardware SPI peripheral with a FIFO buffer or DMA. On an STM32, you can configure SPI1 with DMA to transfer the entire bitmap without CPU intervention. For example, using HAL_SPI_Transmit_DMA, you can send the data in the background while the CPU processes other tasks. The transfer time at 10 MHz is 153,600 bytes * 8 bits / 10,000,000 = 0.12288 seconds, or about 122 ms per frame. However, this ignores command overhead and delays. In practice, a full-screen update takes 150–200 ms, yielding 5–6 fps. If you need faster updates, use partial window updates for smaller regions. For instance, updating a 50x50 pixel icon takes only 5,000 bytes and 4 ms. The table below shows transfer times for different SPI speeds:

SPI Clock (MHz)Transfer Time (full screen, ms)Frames per SecondNotes
43073.3Safe for long wires
81536.5Common for ESP32
101228.2Maximum for ILI9341
206116.4Only with parallel interface

Handling Memory Constraints and Storage

Most microcontrollers lack the RAM to store a full 153,600-byte bitmap. For example, the ESP32 has 520 KB SRAM, so it can hold the bitmap, but the Arduino Uno cannot. Solutions include storing the bitmap in program memory (PROGMEM) on AVR chips, but this requires a large flash (e.g., 256 KB for a single image). Alternatively, use an SD card module (SPI) to read the bitmap file on the fly. The SD card can store multiple images, and you read them in chunks. For example, read 512-byte sectors and send them to the display. This adds latency but reduces RAM usage. Another approach is to compress the bitmap using RLE (run-length encoding) and decompress it on the MCU. For simple graphics with large uniform areas, RLE can reduce size by 50–80%. For photographs, JPEG compression is more effective, but decoding JPEG on a low-end MCU is slow. A more practical method is to use an external SPI flash chip (e.g., W25Q64, 8 MB) that can store dozens of bitmaps. You pre-load the flash with images via a programmer, then the MCU reads them over SPI. The flash’s read speed is up to 50 MHz, so you can transfer data faster than the display’s SPI speed. For example, reading 153,600 bytes from flash at 50 MHz takes 24.6 ms, but sending to display at 10 MHz takes 122 ms, so the bottleneck is the display interface. You can optimize by using a double buffer: read a row from flash while sending the previous row to the display. This requires at least 640 bytes of RAM (one row of 320 pixels).

Practical Code Example for ESP32

Here’s a concrete example using an ESP32 with the TFT_eSPI library. First, install the library and configure the user_setup.h file for your display’s pins (e.g., TFT_CS, TFT_DC, TFT_RST, TFT_MOSI, TFT_SCLK). Then, convert your bitmap to a raw RGB565 file using Python: from PIL import Image; img = Image.open('photo.jpg').resize((240, 320)); with open('image.raw', 'wb') as f: f.write(img.tobytes()). Upload the raw file to SPIFFS (ESP32’s file system) using the Arduino IDE’s SPIFFS upload tool. In the code, include #include and #include . Initialize the display with tft.init() and tft.setRotation(1) for landscape. Then, open the file: File file = SPIFFS.open("/image.raw", "r"). Read the file in chunks of 320 bytes (one row) and send to the display using tft.pushImage(x, y, w, h, buffer). For full screen, set x=0, y=0, w=240, h=320. The pushImage function handles the window addressing. This method uses about 640 bytes of RAM for the buffer. The total time for one frame is about 150 ms, including file read overhead. If you want to display animations, pre-load multiple images into SPIFFS and cycle through them. The ESP32’s dual-core processor can run the display update on one core and file reading on the other, but the library is single-threaded, so use a timer interrupt for timing.

Common Issues and Debugging

If the bitmap displays with incorrect colors, check the byte order. Send a test pattern of known colors (e.g., red, green, blue, white) to verify. If the image is shifted or distorted, the window address may be set incorrectly. For example, if you set the column range to 0–239 but the display expects 0–239, but the controller’s internal mapping may be reversed. Use command 0x36 (MADCTL) to adjust orientation. If the display shows random lines or flickers, the SPI clock may be too high, causing data corruption. Reduce to 4 MHz and test. Another issue is missing delays after initialization commands, especially after sleep out (120 ms). If the display stays blank, check the RESET pin timing: it must be held low for at least 10 ms, then high. Also, verify power supply voltage: the TFT module typically requires 3.3V for logic and 5V for backlight, but some modules have a built-in regulator. If the backlight is off, check the LED pin (usually connected to 3.3V through a resistor). For parallel interface, ensure all data lines are connected and the timing of WR and RD signals matches the datasheet. The ILI9341 datasheet specifies a minimum write cycle of 100 ns, so use a fast MCU or adjust clock cycles. If you’re using an Arduino Uno with SPI, the maximum clock is 8 MHz, but the library may limit to 4 MHz. Use an oscilloscope to check the SPI signals if possible. Finally, if the bitmap is too large for RAM, you’ll see heap allocation errors. Use the ps_malloc() function on ESP32 to allocate memory in PSRAM if available, or use external flash as described.

Performance Optimization Techniques

To achieve higher frame rates, consider using DMA for SPI transfers. On STM32, the HAL library provides HAL_SPI_Transmit_DMA which sends data in the background. This frees the CPU to prepare the next frame. For ESP32, the TFT_eSPI library supports DMA via the tft.pushImageDMA() function, but it requires a contiguous buffer. If you have PSRAM, you can allocate a 153,600-byte buffer and load the bitmap from flash into it, then send via DMA. This reduces CPU overhead to near zero. Another optimization is to use partial updates. If only a small region changes, update only that window. For example, in a user interface, only the button area changes, so you update a 50x50 pixel region. This reduces data transfer by 97%. Also, use double buffering: write to a back buffer in RAM while the display shows the front buffer. This prevents tearing but requires more RAM. For low-end MCUs, consider using a compressed bitmap format like RLE or LZSS. The decompression code adds overhead, but if the bitmap has large uniform areas, the total transfer time decreases. For instance, a 240x320 bitmap with 50% compression reduces data to 76,800 bytes, cutting transfer time to 61 ms at 10 MHz. Finally, overclock the SPI bus if your display supports it. Some ILI9341 modules can handle up to 20 MHz, but this varies by manufacturer. Test at 12 MHz first, then increase until errors appear. Use a logic analyzer to verify data integrity.

Hardware Considerations for Reliable Operation

The physical connection between the MCU and the 2.4 inch 240x320 tft display affects performance. Use short wires (less than 10 cm) for SPI to reduce signal degradation. If you must use longer wires, use shielded cables or add series resistors (22–100 ohms) on the data lines to dampen ringing. The display’s backlight current is typically 40–80 mA at 3.3V, so connect it through a transistor or MOSFET if the MCU pin cannot source

a
À propos de l'auteur
admin

Membre actif de la communauté Barcamp Bordeaux, contributeur sur Slack et speaker régulier depuis plusieurs éditions.

Tu viens au prochain Barcamp ?

Une journée, zéro filtre, des idées qui restent. L'édition 2025 ouvre ses inscriptions.

Réserve ma place