Skip to content

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

By admin From the Blind Dog Smokin' pit

To display a bitmap on a 2.4 inch 240x320 screen, you need to convert the image into a raw pixel data array that the display controller can directly render, then send it via the appropriate interface (usually SPI or MCU 8080 parallel) using a microcontroller like an STM32 or ESP32. The most common approach is to use a tool like ImageMagick or a custom Python script to convert a BMP file into a C array of 16-bit RGB565 color values (since most 2.4 inch 240x320 IPS displays, like the 2.4 inch 240x320 ips display, use 16-bit color depth). For example, a 240x320 bitmap at 16 bits per pixel results in 240 * 320 * 2 = 153,600 bytes of raw data. You then store this array in the microcontroller’s flash memory (if using an external flash chip, you can handle larger images) and send it to the display’s driver IC, typically the ILI9341 or ST7789, by setting the window address and pumping pixel data through SPI at speeds up to 40 MHz. On a typical ESP32, this can achieve frame rates of around 30-60 fps for static images, but for full-screen bitmap updates, you’re looking at roughly 15-20 fps due to SPI overhead. The key is to ensure the bitmap format matches the display’s color order—most ILI9341 controllers expect RGB565 in little-endian byte order, but some ST7789 variants use BGR565, so you might need to swap bytes. I’ve seen many hobbyists fail because they skip the byte order check, ending up with a blue sky that looks green. To avoid this, always verify the display’s datasheet for the pixel format register (e.g., ILI9341 command 0x3A sets the pixel format, and you want 0x55 for 16-bit).

Now, let’s break down the hardware specifics. A 2.4 inch 240x320 screen typically uses a 4-wire SPI interface (CS, DC, MOSI, SCK, plus reset and backlight), though some modules include an optional 8-bit parallel interface for faster updates. The SPI clock speed is a critical factor: at 20 MHz, transferring 153,600 bytes takes about 61.5 ms (153,600 * 8 / 20,000,000), plus command overhead, so you can update the full screen roughly 16 times per second. If you use an 8-bit parallel interface at 10 MHz, the same transfer takes about 15.4 ms, yielding 65 fps. However, most microcontrollers like the ESP32 or STM32F4 have limited RAM for frame buffers—ESP32 has 520 KB of SRAM, which can hold one full 240x320 16-bit frame (153,600 bytes) plus a small buffer, but not two frames for double buffering unless you use external PSRAM. For bitmap display, you typically stream data directly from flash to the display without a full frame buffer, using a line-by-line approach. For example, you can read the bitmap data in chunks of 320 pixels (one row) from a SPI flash memory (like a W25Q32) and send it to the display, requiring only a 640-byte buffer (320 pixels * 2 bytes). This is efficient for static images but not for animations. If you’re using a microcontroller with limited flash, like an Arduino Uno (32 KB), you cannot store a full 153.6 KB bitmap, so you must use an external SD card or SPI flash chip. The SD card approach is common: read the BMP file in 512-byte sectors, parse the header (which is 54 bytes for a 24-bit BMP, but you need to convert to 16-bit), and send pixel data. The BMP header contains the file size, width, height, and pixel data offset; for a 240x320 24-bit BMP, the file size is 240 * 320 * 3 + 54 = 230,454 bytes. Converting to 16-bit RGB565 reduces storage to 153,654 bytes, but you lose color depth. If you need higher quality, consider using a compressed format like JPEG with a decoder library (e.g., TJpgDec for ESP32), which can decode a 240x320 JPEG in about 200 ms using hardware JPEG acceleration on the ESP32-S3, but that’s more complex.

Let’s talk about the software stack. The most reliable way to display a bitmap is using the Adafruit_GFX library with a compatible display driver (like Adafruit_ILI9341). You call drawRGBBitmap(x, y, bitmap, width, height), which sends the array directly. For example, to display a 240x320 full-screen image, you’d define const uint16_t bitmap[76800] (240*320) and call tft.drawRGBBitmap(0, 0, bitmap, 240, 320). The library handles the window address and SPI transactions. However, this method uses a lot of RAM (153,600 bytes) if you declare the array in SRAM, which is not feasible for most microcontrollers. Instead, store the array in PROGMEM (flash) on AVR or use const with __attribute__((section(".ext_flash"))) on ESP32 to place it in the flash partition. For ESP32, you can use the ESP32_ILI9341 library with DMA support, which can push pixels at 40 MHz SPI without blocking the CPU. Benchmarks show that with DMA, a full 240x320 bitmap update takes about 30 ms, giving 33 fps. For the STM32F4, you can use the HAL library with DMA and a 16-bit parallel interface to achieve 10 ms updates (100 fps). But if you’re using a low-cost MCU like the ESP8266, the SPI speed is limited to 20 MHz, and you’ll get around 60 ms per frame (16 fps). The table below summarizes typical performance for different setups:

Microcontroller Interface SPI Speed (MHz) Full Frame Update Time (ms) Max FPS RAM Required for Full Buffer
ESP32 (dual-core) 4-wire SPI + DMA 40 30 33 153.6 KB (or stream from flash)
ESP8266 4-wire SPI 20 60 16 153.6 KB (not feasible, use streaming)
STM32F407 8-bit parallel (8080) 10 (parallel clock) 15 65 153.6 KB (use internal SRAM)
Arduino Uno (AVR) 4-wire SPI 8 150 6 2 KB (must stream from SD card)

One common pitfall is the bitmap’s orientation. Most 2.4 inch 240x320 screens are designed for portrait mode (240 width, 320 height), but if your bitmap is landscape, you’ll need to rotate it. The ILI9341 supports hardware rotation via the MADCTL register (command 0x36). For portrait, set MADCTL to 0x40 (MV=0, MX=0, MY=0, ML=0, BGR=1). For landscape, set it to 0xE0 (MV=1, MX=1, MY=1, ML=0, BGR=1). If you’re using a library like TFT_eSPI, you can call setRotation(1) for landscape. But note that rotation changes the coordinate system, so your bitmap data must match the new orientation. If you’re generating the bitmap array from a tool like LVGL’s image converter, you can specify the rotation at conversion time. Another detail: some 2.4 inch IPS displays have a built-in SD card slot on the back of the PCB, which is convenient for storing multiple bitmaps. For example, the 2.4 inch 240x320 ips display from DisplayModule includes a microSD slot, allowing you to load BMP files directly from a FAT32-formatted card. You can use the SdFat library to read the file, parse the BMP header, and convert 24-bit RGB to 16-bit RGB565 on the fly. The conversion formula is: RGB565 = ((R & 0xF8) << 8) | ((G & 0xFC) << 3) | (B >> 3). This is a bitwise operation that runs quickly on a 32-bit MCU. For a 240x320 image, you’ll need to read 240 * 320 * 3 = 230,400 bytes from the SD card, which at SPI speeds of 20 MHz takes about 92 ms for the read, plus conversion and display time. Total time is around 120 ms, giving 8 fps. If you pre-convert the bitmap to 16-bit RGB565 on a PC and store it as a raw binary file, you can skip the conversion step, cutting the display time to 60 ms (16 fps).

Let’s get into the nitty-gritty of pixel data ordering. The ILI9341 expects pixels in row-major order, starting from the top-left corner. When you send a bitmap, you must set the column (0 to 239) and page (0 to 319) range using commands 0x2A and 0x2B, then send the pixel data via command 0x2C. For a 240x320 image, you set the column range to 0-239 and page range to 0-319. If you’re using a library, this is handled automatically. But if you’re writing raw SPI commands, the sequence is: send command 0x2A, then 4 bytes for column start and end (0x00, 0x00, 0x00, 0xEF for 239), then command 0x2B, then 4 bytes for page start and end (0x00, 0x00, 0x01, 0x3F for 319), then command 0x2C, then stream the pixel data. For 16-bit pixels, each pixel is two bytes: high byte (bits 15-8) and low byte (bits 7-0). The ILI9341 expects the high byte first, then low byte. So for a pixel with RGB565 value 0x07E0 (green), you send 0x07 then 0xE0. If you’re using a 24-bit BMP, you need to convert each pixel: for a 24-bit BMP, the pixel data is stored in BGR order (blue, green, red), so you extract B, G, R, then compute RGB565 as ((R >> 3) << 11) | ((G >> 2) << 5) | (B >> 3). Note that the BMP format stores pixels bottom-up by default, so you’ll need to reverse the row order. For example, a 240x320 BMP has the first row of pixel data corresponding to the bottom of the image. To display correctly, you read the BMP file from the end or reverse the rows in software. Most libraries like TFT_eSPI have a built-in BMP drawing function that handles this, but if you’re doing it manually, you’ll waste time debugging upside-down images. I’ve seen many forum posts where people ask why their image is mirrored—it’s almost always the BMP row order. To fix it, you can either flip the image in a tool like Photoshop before conversion, or in code, read the BMP file into a buffer and reverse the row index when sending to the display. For a 240x320 image, you’d read the pixel data into a 2D array of 320 rows, then send row 319 first, then row 318, etc.

Another angle is the color depth trade-off. A 16-bit RGB565 bitmap gives 65,536 colors, which is sufficient for most photos and graphics, but you’ll see color banding in gradients. If you want true color, you can use 24-bit RGB888, but the display’s ILI9341 controller only supports 16-bit or 18-bit (262,144 colors) via the 3-wire SPI interface. For 18-bit, you send 3 bytes per pixel (R, G, B each 6 bits), but the controller dithers internally. Most libraries default to 16-bit for speed. If you’re working with a high-quality photo, consider using a 16-bit PNG with a palette, but that adds complexity. For embedded systems, I recommend using a tool like LVGL’s image converter (online at lvgl.io) to convert your bitmap to a C array with RGB565 format and optional compression (like RLE). The converter also handles rotation and dithering. For example, a 240x320 photo compressed with RLE might reduce the array size from 153.6 KB to 80-120 KB, depending on the image complexity. This saves flash space, which is critical on microcontrollers like the ESP8266 with 4 MB flash. If you’re using the 2.4 inch 240x320 ips display with an ESP32, you can store multiple bitmaps in the 16 MB flash partition (if you have a module with external flash). I’ve seen projects where users store 20+ full-screen images for a photo frame, using a FAT filesystem on the flash. The ESP32’s SPIFFS or LittleFS can handle this, but you need to ensure the flash is wear-leveled. For SD cards, the FAT32 filesystem is simpler, but you must handle file fragmentation. A 240x320 16-bit RAW file is exactly 153,600 bytes, which fits in a single cluster on most SD cards (cluster size is 32 KB for FAT32, so the file spans 5 clusters). Reading it sequentially is fast, but if the file is fragmented, you’ll see delays. To avoid this, format the SD card with a 64 KB cluster size (using a custom tool) to reduce fragmentation.

Let’s talk about power consumption and real-world constraints. The 2.4 inch IPS display itself draws about 50-80 mA with the backlight on (typical backlight forward voltage is 3.0V at 20 mA for the LED, plus the driver IC). When you’re sending a bitmap continuously, the SPI bus and MCU add another 20-50 mA. So total power is around 100-130 mA at 3.3V, which is about 0.33-0.43 watts. For battery-powered projects, this is significant. If you’re updating the bitmap every 100 ms (10 fps), the power consumption is similar to a static image because the display refresh is continuous (the ILI9341 refreshes the internal frame buffer at 60 Hz even if you don’t send new data). To save power, you can turn off the backlight or put the display into sleep mode (command 0x10). But for a static bitmap, you only need to send the data once, then the display holds it. So the initial bitmap transfer might consume 30 ms of high current, then the display idles at 50 mA. This is fine for a photo frame. For animations, you need to balance frame rate and power. A practical tip: use a 2.4 inch 240x320 IPS display with a built-in frame buffer (like the ILI9341 has 172,800 bytes of internal RAM, which is enough for 240x320x16-bit = 153,600 bytes, plus some overhead). This means you don’t need an external frame buffer, but the MCU must still send data over SPI. If you’re using a display with an ST7789 controller, it has a similar frame buffer but different command set. The ST7789 uses 0x2A and 0x2B for window, and 0x2C for RAM write, same as ILI9341, but the initial configuration differs. For example, ST7789 requires a sleep-out command (0x11) and a display-on command (0x29), plus a MADCTL setting for orientation. If you’re using a library like TFT_eSPI, it auto-detects the controller, but if you’re writing raw code, you need to check the datasheet. I’ve had to debug a project where the bitmap appeared as vertical stripes because the column and page ranges were set incorrectly for the ST7789’s 240x320 resolution (some ST7789 variants have a 240x320 max, but the default window is 240x240 for round displays). For the 2.4 inch 240x320 screen, the window should be 0-239 for columns and 0-319 for pages.

Another practical consideration is the bitmap’s file format. If you’re loading from an SD card, the BMP format is straightforward but inefficient. For a 24-bit BMP, the file size is 230,454 bytes, and you need to parse the header (54 bytes) and then convert each pixel. The conversion loop in C looks like this: for (int y = 0; y < 320; y++) { for (int x = 0; x < 240; x++) { uint8_t b = f

Lock Your Date

Smoke the Event Everyone Remembers

Weddings, corporate parties, and backyard blowouts — we feed 50 to 3,200 guests across DFW with whole-hog, brisket, and ribs that earn a standing ovation.