How to use a 2.4 inch display with CircuitPython?
To use a 2.4 inch 240x320 ips display with CircuitPython, you need to connect it via SPI (Serial Peripheral Interface) and load the appropriate libraries, typically `adafruit_ili9341` and `adafruit_stmpe610` (if touch is included). The display usually uses the ILI9341 driver, which is well-supported in CircuitPython. First, wire the display's CS (Chip Select), DC (Data/Command), MOSI (Master Out Slave In), MISO (Master In Slave Out), SCK (Serial Clock), and optionally T_IRQ (Touch Interrupt) to your microcontroller's SPI pins. For example, on a Raspberry Pi Pico, you'd map CS to GPIO 17, DC to GPIO 16, MOSI to GPIO 19, MISO to GPIO 20, SCK to GPIO 18, and T_IRQ to GPIO 22. Then, install the necessary libraries by copying `adafruit_ili9341.mpy`, `adafruit_stmpe610.mpy`, and `adafruit_rgb_display` from the CircuitPython bundle to your board's `lib` folder. Initialize the display with `displayio.FourWire(spi_bus, command=dc_pin, chip_select=cs_pin, reset=reset_pin)` and create a `adafruit_ili9341.ILI9341(display_bus, width=240, height=320)`. This gives you a 240x320 pixel canvas at 16-bit color depth, supporting up to 65,536 colors. The SPI clock speed can be set to 24 MHz for faster refresh rates, but lower speeds like 8 MHz are more stable for initial testing. On a Pico at 24 MHz, a full-screen fill takes about 50 milliseconds, while drawing a 100x100 pixel rectangle takes around 5 milliseconds. The display's power consumption is typically 80-100 mA at 3.3V when active, and 0.5 mA in sleep mode. You can find the exact pinout and specifications for the 2.4 inch 240x320 ips display on the manufacturer's page, which lists the 8-pin SPI interface: VCC (3.3V), GND, CS, DC, MOSI, MISO, SCK, and T_IRQ (optional).
Now, let's break down the hardware setup. The display module comes with an 8-pin header, and you'll need a microcontroller like the Adafruit Feather RP2040, Raspberry Pi Pico, or ESP32-S3. The SPI pins on the Pico are fixed: GPIO 19 (MOSI), GPIO 20 (MISO), GPIO 18 (SCK). For other boards, check the datasheet for SPI0 or SPI1. Connect the display's VCC to 3.3V (not 5V, as the ILI9341 is 3.3V logic), GND to ground, CS to any GPIO (e.g., GPIO 17), DC to another GPIO (e.g., GPIO 16), MOSI to the board's MOSI, MISO to MISO, SCK to SCK, and T_IRQ to GPIO 22 if you want touch. The display's backlight is controlled by a separate LED pin, which is often tied to 3.3V through a resistor, but it's better to connect it to a PWM-capable GPIO for brightness control. On the Pico, you can use GPIO 15 with a 100-ohm resistor in series to limit current to 20 mA. The backlight draws about 30 mA at full brightness, so the total current is around 110-130 mA. If you're using a battery-powered setup, consider a 3.3V regulator like the AP2112 that can handle 600 mA. The display's resolution is 240x320 pixels, with a pixel clock of 6.5 MHz for the ILI9341, but the SPI bus can run at up to 80 MHz on the Pico (though 24 MHz is recommended for reliability). The display's refresh rate is 60 Hz, but with SPI, you're limited by bus speed. At 24 MHz, a full-screen update takes about 16 milliseconds, which is close to 60 Hz. However, drawing complex graphics or fonts will be slower due to the Python overhead.
For the software side, start by downloading the latest CircuitPython firmware for your board from circuitpython.org. For the Pico, use the `adafruit-circuitpython-raspberry_pi_pico-en_US-9.x.uf2` file. Drag it to the Pico's bootloader drive. Then, install the Adafruit CircuitPython Bundle (version 9.x or later) from github.com/adafruit/Adafruit_CircuitPython_Bundle. Extract the `lib` folder and copy the following files: `adafruit_ili9341.mpy`, `adafruit_stmpe610.mpy`, `adafruit_rgb_display.mpy`, `displayio.mpy` (if not included), and `adafruit_display_text.mpy` (for text rendering). The total library size is about 50 KB, leaving plenty of room for your code. Write a simple test script in `code.py` on the board:
```python
import board
import busio
import displayio
import adafruit_ili9341
import digitalio
# Release any previously configured displays
displayio.release_displays()
# Set up SPI bus
spi = busio.SPI(clock=board.GP18, MOSI=board.GP19, MISO=board.GP20)
# Set up control pins
cs = digitalio.DigitalInOut(board.GP17)
dc = digitalio.DigitalInOut(board.GP16)
reset = digitalio.DigitalInOut(board.GP21) # Optional, you can set to None
# Create display bus
display_bus = displayio.FourWire(spi, command=dc, chip_select=cs, reset=reset)
# Initialize display
display = adafruit_ili9341.ILI9341(display_bus, width=240, height=320)
# Create a bitmap and palette
bitmap = displayio.Bitmap(240, 320, 65536)
palette = displayio.Palette(65536)
for i in range(65536):
palette[i] = (i >> 8) & 0xFF, (i >> 0) & 0xFF, (i >> 16) & 0xFF
# Create a tilegrid
tile_grid = displayio.TileGrid(bitmap, pixel_shader=palette)
# Create a group and add the tilegrid
group = displayio.Group()
group.append(tile_grid)
display.show(group)
# Fill the screen with a color (e.g., red)
for x in range(240):
for y in range(320):
bitmap[x, y] = 0xF800 # 16-bit RGB565 red
```
This code fills the entire 240x320 screen with red. The 16-bit color format is RGB565: 5 bits for red, 6 bits for green, 5 bits for blue. The value 0xF800 corresponds to red (R=31, G=0, B=0). You can calculate colors as `(red << 11) | (green << 5) | blue` where red is 0-31, green is 0-63, blue is 0-31. For example, white is 0xFFFF, black is 0x0000, and blue is 0x001F. The loop above is slow because it's in Python; for faster drawing, use the `displayio` library's built-in shapes or the `adafruit_display_shapes` module. For instance, drawing a filled rectangle using `displayio.Bitmap` with a `blit` operation is much faster. The ILI9341's frame buffer is 153,600 bytes (240 * 320 * 2 bytes per pixel), but CircuitPython uses a 16-bit color depth, so the bitmap object takes 307,200 bytes in RAM. On a Pico with 264 KB of RAM, this is tight but works. You can reduce memory by using a 8-bit color palette (256 colors) instead of 16-bit, which halves the RAM usage to 153,600 bytes. To do this, create a `displayio.Palette(256)` and use 8-bit indices in the bitmap. The ILI9341 still outputs 16-bit, but the palette maps 8-bit to 16-bit internally.
For touch input, the display often includes a resistive touch controller like the STMPE610 or TSC2007. The STMPE610 uses I2C, not SPI, so you'll need separate I2C pins. On the Pico, use GPIO 4 (SDA) and GPIO 5 (SCL) for I2C0. Connect the display's T_IRQ pin to a GPIO (e.g., GPIO 22) for interrupt-based touch detection. The STMPE610 library is `adafruit_stmpe610.mpy`. Initialize it with `i2c = busio.I2C(board.GP5, board.GP4)` and `touch = adafruit_stmpe610.Adafruit_STMPE610(i2c)`. Then, in a loop, check `touch.buffer` for touch points. The touch resolution is 4096x4096, but the display maps it to 240x320. You'll need to scale the coordinates: `x = touch.buffer[0].x * 240 // 4096` and `y = touch.buffer[0].y * 320 // 4096`. The touch controller has a 10-bit ADC, but the STMPE610 reports 12-bit values. The touch sensitivity is configurable, but default works for finger presses. The touch controller draws 1.2 mA in active mode and 0.5 µA in sleep mode. If you don't need touch, you can leave the T_IRQ pin unconnected.
Performance optimization is key for real-time applications. The SPI bus speed directly affects frame rate. At 24 MHz, the theoretical maximum data rate is 24 Mbps, but overhead from Python and the ILI9341's command set reduces it. A full-screen image transfer takes about 16 milliseconds, but the actual frame rate is limited by the display's 60 Hz refresh. For animations, use double buffering with `displayio.Group` and `displayio.OnDiskBitmap` for pre-rendered images. You can store images as 16-bit BMP files on the board's flash memory. The CircuitPython `displayio.OnDiskBitmap` class loads images directly from the filesystem, reducing RAM usage. For example, to display a 240x320 BMP file named `image.bmp`, use:
```python
image = displayio.OnDiskBitmap("image.bmp")
tile_grid = displayio.TileGrid(image, pixel_shader=image.pixel_shader)
group = displayio.Group()
```
This loads the image from the filesystem, not RAM, so it uses only 1-2 KB for the tile grid. The BMP file must be 16-bit RGB565 format, which you can create with tools like ImageMagick using `convert input.png -resize 240x320 -depth 16 -colorspace RGB output.bmp`. The file size is 153,600 bytes plus a 54-byte header, totaling ~153.7 KB. On a Pico with 2 MB of flash, you can store several images. For text rendering, use the `adafruit_display_text` library. Create a label with `label = adafruit_display_text.label.Label(terminalio.FONT, text="Hello", color=0xFFFF, x=10, y=10)` and add it to the group. The font is built-in (terminalio.FONT), which is a 5x7 pixel bitmap font. For larger fonts, use the `adafruit_bitmap_font` library and load `font = adafruit_bitmap_font.load_font("Arial-16.bdf")`. BDF fonts are available in the CircuitPython bundle. The text rendering is slow for large strings, but it's fine for small labels. For scrolling text, use a `displayio.TileGrid` with a scrolling bitmap.
Power management is crucial for portable projects. The display's sleep mode is activated by sending command 0x10 (Sleep In) via SPI. In CircuitPython, you can call `display.sleep()` to put the ILI9341 into sleep mode, reducing current to 0.5 mA. To wake it, call `display.wake()` which takes 120 milliseconds. The backlight can be controlled with PWM: set the duty cycle to 0 for off, 65535 for full brightness. For example, `pwm = pwmio.PWMOut(board.GP15, frequency=1000, duty_cycle=32768)` gives 50% brightness. The backlight's PWM frequency should be above 100 Hz to avoid flicker. The display's standby current (with backlight off) is 50 µA, but the microcontroller's idle current is higher. For battery life, use deep sleep on the microcontroller and turn off the display. On the Pico, deep sleep draws 2 µA, so a 2000 mAh battery could last months. However, the display's wake-up time is 120 ms, so plan for latency.
Common issues include wiring errors and SPI bus conflicts. If the display shows no output, check that the CS pin is pulled low before data transfer. The ILI9341 requires a reset pulse: set the reset pin low for 10 ms, then high. In the code, if you set `reset=None`, the library uses a software reset, but it's less reliable. Always use a dedicated GPIO for reset. Another issue is the SPI bus speed: some displays can't handle 24 MHz, so drop to 8 MHz. The display's datasheet specifies a maximum SPI clock of 80 MHz, but cheap clones may have poor signal integrity. Use short wires (<10 cm) and avoid breadboards for high-speed SPI. If the display shows garbled colors, check the color format: the ILI9341 expects 16-bit RGB565, but some libraries default to RGB888. The CircuitPython library uses RGB565 by default. If you're using a different library, set the color depth explicitly. The display's initialization sequence is handled by the library, but you can override it with custom commands. For example, to set the gamma curve, call `display.write_cmd(0x26, b'\x01')`. The gamma curve affects contrast; the default is 0x01 for normal mode.
For advanced usage, you can use the display's hardware acceleration for drawing rectangles and circles. The ILI9341 supports windowed drawing: define a rectangle with `display.set_window(x0, y0, x1, y1)` and then send pixel data. In CircuitPython, this is handled by the `displayio` library, but you can bypass it for raw performance. Use `display._write_pixels(data)` where `data` is a bytearray of 16-bit colors. For example, to fill a 100x100 rectangle at (10,10) with red, use:
```python
display.set_window(10, 10, 109, 109)
data = bytearray(100 * 100 * 2)
for i in range(0, len(data), 2):
data[i] = 0xF8
data[i+1] = 0x00
display._write_pixels(data)
```
This is much faster than the bitmap loop because it avoids Python overhead. The `_write_pixels` method sends data directly to the SPI bus. The ILI9341's write speed is limited by the SPI bus, but this approach can achieve 5-10 frames per second for small updates. For full-screen updates, use the `displayio` library's built-in `display.refresh()` method, which is optimized for the hardware. The refresh rate is 60 Hz, but the SPI bus might not keep up, so you'll see tearing if you update too fast. Enable vertical sync by setting `display.auto_refresh = True` and `display.refresh(minimum_frames_per_second=30)`. This prevents tearing by waiting for the display's vertical blanking interval. The ILI9341's VSYNC signal is not exposed on the pinout, so the library uses a software timer.
Data visualization is a common use case. You can plot sensor data as a line graph using the `adafruit_display_shapes` library. For example, to draw a line from (0, 100) to (240, 200), use:
```python
from adafruit_display_shapes.line import Line
line = Line(0, 100, 240, 200, color=0x07E0) # Green
group.append(line)
```
For bar charts, use `adafruit_display_shapes.rect`. The library supports rectangles, circles, triangles, and polygons. Each shape is a `displayio` object, so they can be added to groups and moved. The memory usage for shapes is small: a line uses 16 bytes, a rectangle uses 32 bytes. For complex graphs, use a `displayio.Bitmap` for the chart area and update it pixel by pixel. The display's resolution is 240x320, so you can show 240 data points horizontally. For real-time data, update the bitmap every second. The ILI9341's response time is 25 ms, so it's fast enough for live updates. The display's viewing angle is 80 degrees in