How to display a bitmap on a 3.2 inch 240x320 TFT screen?
Understanding the Bitmap File Structure
To display a bitmap, you must first parse its file format. A standard 24-bit BMP file (Windows Bitmap) starts with a 14-byte file header, followed by a 40-byte DIB header (total 54 bytes), then the pixel data. The pixel data is stored in BGR order (blue, green, red) and is padded to 4-byte alignment per row. For a 240-pixel-wide image, each row in 24-bit BMP is 240 * 3 = 720 bytes, but with padding to a multiple of 4, it becomes 720 bytes exactly (since 720 % 4 = 0). For 320 rows, the total pixel data is 720 * 320 = 230,400 bytes. The pixel array is stored bottom-up, meaning the first pixel in the file corresponds to the bottom-left corner of the image. When you send it to the TFT, you need to start from the top row, so you must read the last row first or reverse the read order. This is a common pitfall—if you just send the raw bytes in file order, the image will appear upside down. The bitmap header also contains the width and height as 32-bit integers at offsets 18 and 22, respectively. For a 240x320 BMP, these values are 0x000000F0 (240) and 0x00000140 (320). The bits-per-pixel field at offset 28 is 24 for a standard bitmap. If you use a 16-bit BMP (RGB565), the header is similar but the pixel data is already in the correct format, saving conversion time. However, 16-bit BMPs are less common, so most workflows involve converting a 24-bit image offline using tools like ImageMagick or a Python script, then embedding the raw RGB565 array in your code.
Converting Bitmap to RGB565 for the TFT
The TFT screen expects 16-bit RGB565 data, where each pixel is 2 bytes. The conversion from 24-bit RGB to RGB565 involves bit-shifting: red (5 bits) = (R >> 3), green (6 bits) = (G >> 2), blue (5 bits) = (B >> 3). The final 16-bit value is (red << 11) | (green << 5) | blue. For example, a pure red pixel (255, 0, 0) in 24-bit becomes (255>>3) << 11 = 0xF800. A pure green (0, 255, 0) becomes (255>>2) << 5 = 0x07E0. A pure blue (0, 0, 255) becomes (255>>3) = 0x001F. The 16-bit value is stored in little-endian order (low byte first) on most microcontrollers, so you need to swap bytes when sending over SPI. For instance, 0xF800 becomes 0x00, 0xF8 in byte order. If you’re using an Arduino with the Adafruit_GFX library, the library handles this automatically, but if you’re writing raw SPI commands, you must manually swap. The conversion process is computationally intensive—for a full 240x320 image, you need to process 76,800 pixels (240 * 320 = 76,800), each requiring 3 bit-shifts and 1 OR operation. On a 240 MHz ESP32, this takes about 10 ms in C code using optimized loops, but on an 8-bit Arduino Uno (16 MHz), it can take several seconds, so it’s better to pre-convert the bitmap offline. Tools like LVGL’s image converter or the online “Image to C Array” tool can generate a header file with the RGB565 array. For example, the array for a 240x320 image is declared as `const uint16_t myImage[76800] PROGMEM = { ... };` on Arduino, or `const uint16_t myImage[76800] = { ... };` on ESP32. The PROGMEM directive stores the data in flash memory, which is critical for devices with limited RAM (like the Uno’s 2 KB). The flash size for this array is 153,600 bytes (76800 * 2), which fits on most ESP32 boards (4 MB flash) but exceeds the 32 KB flash on an Arduino Uno, so you’d need an external SD card for larger images.
SPI Communication and Timing
The TFT screen uses SPI for data transfer. The ILI9341 driver supports SPI mode 0 (CPOL=0, CPHA=0) with a maximum clock frequency of 40 MHz for write operations. The typical pinout for a 3.2 inch module includes: CS (chip select), DC (data/command), RST (reset), MOSI (master out slave in), MISO (master in slave out, often unused), and SCK (serial clock). The backlight is usually controlled via a separate pin (PWM-capable) or tied to 3.3V. To send a bitmap, you first initialize the TFT with commands like software reset (0x01), sleep out (0x11), and display on (0x29). Then, you set the drawing window using the column address set (0x2A) and page address set (0x2B) commands. For a full-screen bitmap, you set the column range to 0-239 and the page range to 0-319. Then, you send the memory write command (0x2C), followed by the pixel data. The data must be sent continuously without gaps. The SPI transfer speed determines the refresh time. At 40 MHz, each byte takes 0.025 µs (1/40,000,000), so 153,600 bytes take 3.84 ms (153,600 * 0.025 µs). However, the SPI bus on most microcontrollers has overhead—for example, the ESP32’s SPI driver adds about 1 µs per transaction for command setup, so the actual time is around 10-15 ms for a full screen. On an Arduino Uno, the SPI clock is limited to 8 MHz, and the overhead is higher, resulting in 50-100 ms per frame. The table below shows typical transfer times for different microcontrollers:
| Microcontroller | SPI Clock (MHz) | Theoretical Transfer Time (ms) | Actual Time (ms) |
|---|---|---|---|
| ESP32 (240 MHz) | 40 | 3.84 | 10-15 |
| STM32F4 (168 MHz) | 40 | 3.84 | 8-12 |
| Arduino Uno (16 MHz) | 8 | 19.2 | 50-100 |
| Raspberry Pi Pico (133 MHz) | 40 | 3.84 | 5-10 |
Note that these times are for raw pixel data transfer only. The actual display time includes command overhead (setting window, memory write) and any software delays. For a single bitmap display, you don’t need to worry about frame rate, but if you’re updating the screen frequently, you should optimize the SPI transfer by using DMA (Direct Memory Access) on supported microcontrollers like the ESP32 or STM32. DMA can reduce CPU overhead and achieve near-theoretical speeds.
Handling Bitmap Orientation and Clipping
The TFT screen’s native orientation is landscape (240 pixels wide, 320 pixels high) when the controller is in default mode. However, the ILI9341 supports hardware rotation via the memory access control command (0x36). You can set the orientation by writing a byte to 0x36: 0x00 for portrait (320x240), 0x60 for landscape (240x320), 0xC0 for inverted portrait, and 0xA0 for inverted landscape. The bitmap file’s orientation must match the TFT’s orientation. If you’re displaying a 240x320 bitmap in landscape mode, the image will fill the entire screen. If the bitmap is smaller, you need to set the drawing window to a sub-region. For example, to display a 100x100 pixel image centered on the screen, you set the column address to (240-100)/2 = 70 to 169, and the page address to (320-100)/2 = 110 to 209. Then send only the pixel data for that region. The bitmap’s pixel array must be clipped to that region—if you’re using a pre-converted array, you can just send the relevant rows and columns. However, if the bitmap is larger than the screen, you need to crop it. Most TFT libraries like TFT_eSPI provide a `pushImage` function that handles clipping automatically. For example, `tft.pushImage(x, y, w, h, bitmap)` sends a bitmap array starting at coordinates (x, y) with width w and height h. The library takes care of the window setting and byte swapping. This is the easiest way to display a bitmap, but it requires the bitmap data to be in a contiguous array in RAM or flash. If you’re loading from an SD card, you need to read the file in chunks and send each chunk using a loop. The SD card read speed is typically 10-20 MB/s for SPI mode, so reading a 153,600-byte bitmap takes about 10 ms, plus the SPI transfer time. The total time to display a bitmap from an SD card on an ESP32 is around 25-30 ms, which is acceptable for most applications.
Practical Implementation with Code Example
Here’s a concrete example using the TFT_eSPI library on an ESP32 with the DisplayModule 3.2 inch screen. First, you need to install the library via the Arduino Library Manager and configure the User_Setup.h file to match the pinout. For the DisplayModule module, typical pins are: TFT_CS=15, TFT_DC=2, TFT_RST=4, TFT_MOSI=23, TFT_MISO=19, TFT_SCLK=18. The backlight pin (TFT_BL) is often connected to 3.3V or a PWM pin like 32. Then, you can use the following code to display a bitmap from an array stored in flash:
#include
TFT_eSPI tft = TFT_eSPI();
extern const uint16_t myImage[76800]; // from header file
void setup() {
tft.init();
tft.setRotation(1); // landscape
tft.fillScreen(TFT_BLACK);
tft.pushImage(0, 0, 240, 320, myImage);
}
void loop() {}
If you’re loading from an SD card, use the SD library and read the bitmap in chunks. For example, open the file, seek to the pixel data offset (54 bytes), then read 153,600 bytes into a buffer (if RAM allows) or send it in 512-byte blocks. The TFT_eSPI library has a `pushImage` overload that accepts a file object: `tft.pushImage(0, 0, 240, 320, sdFile)`. This method reads the file directly and sends the data, but it assumes the file is in raw RGB565 format (no header). So you need to strip the BMP header first or convert the file offline. A common workflow is to use a Python script to convert a BMP to a raw RGB565 file: `python -c "from PIL import Image; img = Image.open('input.bmp').convert('RGB'); img = img.rotate(180); img.save('output.raw')"` (note the rotation to correct the bottom-up issue). Then copy the .raw file to an SD card and read it with `tft.pushImage`. The SD card approach is more flexible for large images, but it requires a FAT32-formatted card and an SD card module connected via SPI. The typical SPI pins for the SD card are separate from the TFT, so you need to use different CS pins (e.g., SD_CS=5). The wiring is straightforward: connect the SD card’s MOSI, MISO, SCK to the same SPI bus as the TFT (shared), but use separate CS lines. This is called a shared SPI bus, and it works as long as you de-assert the TFT’s CS when accessing the SD card and vice versa.
Color Depth and Memory Considerations
The TFT screen supports 16-bit color (65,536 colors) natively, but some controllers can also handle 18-bit color (262,144 colors) by padding the 6-bit values. However, the ILI9341’s default mode is 16-bit, and using 18-bit requires command 0x3A (interface pixel format) to set to 0x66 (18-bit). In practice, 16-bit is sufficient for most applications, and it reduces memory usage by 25% compared to 18-bit. The bitmap’s color depth must match the TFT’s setting. If you send a 24-bit bitmap without conversion, the colors will be wrong because the TFT expects 2 bytes per pixel, not 3. The conversion from 24-bit to 16-bit loses some color information (the lower 3 bits of each channel), but the visual difference is minimal on a 3.2 inch screen. For example, a gradient of red from 0 to 255 in 24-bit becomes 0 to 248 in 5-bit (step size of 8), which is noticeable only in smooth gradients. To mitigate this, you can use dithering algorithms like Floyd-Steinberg when converting the bitmap offline. Many image conversion tools, such as LVGL’s converter, offer dithering options. The memory footprint of the bitmap array is the biggest constraint. On an ESP32 with 4 MB flash, you can store multiple full-screen bitmaps (e.g., 10 images would take 1.5 MB). On an Arduino Uno, you’re limited to 32 KB flash, so you can only store small images (e.g., 128x128 pixels = 32,768 bytes, which barely fits). For larger images, you must use an SD card or external flash. The RAM usage during display is also critical—you need a buffer to hold the pixel data. If you’re using `pushImage` with an array, the array is in flash, so no RAM is used for the data itself. However, if you’re loading from an SD card, you need a buffer of at least 512 bytes (one sector) to read the file. Some libraries use a double-buffer technique to overlap SD card reads and SPI writes, but that increases RAM usage to 2-4 KB. For the ESP32, this is fine, but for an Arduino Uno, it’s a challenge.
Common Pitfalls and Debugging Tips
One frequent issue is the image appears upside down or mirrored. This is due to the bitmap’s bottom-up storage and the TFT’s coordinate system. To fix it, you can either reverse the row order when sending data (send the last row first) or rotate the image in software. The TFT_eSPI library’s `pushImage` function expects the data in top-to-bottom order, so you must pre-reverse the rows in
Visit the Salon