The Kaluga development kit from Espressif includes almost everything you need: The microcontroller, a camera, and an LCD.
Take the assembled Kaluga board stack (all three boards) and attach the camera at the dedicated header, making sure the pins are inserted properly.
You do not need to add any pull-up resistors; they are already provided on the Kaluga's audio daughterboard.
There are at least 3 variants of the LCD board that ship with the Kaluga:
- st7789
- ili9341
- ili9341 with rotation=90
There are no markings to distinguish the three, so you will need to try each variant until you find the one that works.
First, make sure you can see the Kaluga's CIRCUITPY drive and connect to the REPL. Open the REPL and double check that import imagecapture
works without showing an error. (note: if it does, the most likely reason is that you are using CircuitPython 8 or newer, which is incompatible with the code in this guide)
Then, copy the correct bundle to your device. It will automatically reload and start displaying the image from the camera on the built-in LCD.
Click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, and copy the entire lib folder and the code.py file to your CIRCUITPY drive.
Espressif Kaluga ESP32-S2 with OV2640 display showing the test pattern. The test pattern's color bars appear heavily distorted due to the viewing angle.
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries # # SPDX-License-Identifier: Unlicense """ The Kaluga development kit comes in two versions (v1.2 and v1.3); this demo is tested on v1.3. It probably won't work on v1.2 without modification. The v1.3 development kit's LCD can have one of two chips, the ili9341 or st7789. Furthermore, there are at least 2 ILI9341 variants, one of which needs rotation=90! This demo is for the ili9341. If the display is garbled, try adding rotation=90, or try modifying it to use ST7799. The audio board must be mounted between the Kaluga and the LCD, it provides the I2C pull-ups(!) """ import board import busio import displayio from adafruit_ili9341 import ILI9341 import adafruit_ov2640 # Pylint is unable to see that the "size" property of OV2640_GrandCentral exists # pylint: disable=attribute-defined-outside-init # Release any resources currently in use for the displays displayio.release_displays() spi = busio.SPI(MOSI=board.LCD_MOSI, clock=board.LCD_CLK) display_bus = displayio.FourWire( spi, command=board.LCD_D_C, chip_select=board.LCD_CS, reset=board.LCD_RST ) display = ILI9341(display_bus, width=320, height=240, rotation=90) bus = busio.I2C(scl=board.CAMERA_SIOC, sda=board.CAMERA_SIOD) cam = adafruit_ov2640.OV2640( bus, data_pins=board.CAMERA_DATA, clock=board.CAMERA_PCLK, vsync=board.CAMERA_VSYNC, href=board.CAMERA_HREF, mclk=board.CAMERA_XCLK, mclk_frequency=20_000_000, size=adafruit_ov2640.OV2640_SIZE_QVGA, ) cam.flip_x = False cam.flip_y = True pid = cam.product_id ver = cam.product_version print(f"Detected pid={pid:x} ver={ver:x}") # cam.test_pattern = True g = displayio.Group(scale=1) bitmap = displayio.Bitmap(320, 240, 65536) tg = displayio.TileGrid( bitmap, pixel_shader=displayio.ColorConverter( input_colorspace=displayio.Colorspace.BGR565_SWAPPED ), ) g.append(tg) display.root_group = g display.auto_refresh = False while True: cam.capture(bitmap) bitmap.dirty() display.refresh(minimum_frames_per_second=0) cam.deinit()
Your CIRCUITPY drive should resemble the screenshot below
You should have in / of the CIRCUITPY drive:
- code.py
And in the lib folder on your CIRCUITPY drive:
- adafruit_bus_device
- adafruit_ov2640.mpy
- adafruit_ili9341.mpy
CircuitPython will automatically reload and begin showing the image from the camera on the LCD. If it doesn't, you can open up the REPL to diagnose what went wrong. Double check that you copied all the files from the bundle, and that you have a compatible build of CircuitPython installed, 7.0.0-beta.4 or newer.
If the image does not fill the whole display, try removing rotation=90
from the line beginning display = ILI9341
. If it does not appear at all or is in reverse video, try the example for the st7789 display.

If you have a Kaluga 1.3 board with an ili9341 LCD display, use the project below.
Click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, and copy the entire lib folder and the code.py file to your CIRCUITPY drive.
Your CIRCUITPY drive should resemble the image.
You should have in / of the CIRCUITPY drive:
- code.py
And in the lib folder on your CIRCUITPY drive:
- adafruit_bus_device
- adafruit_ov2640.mpy
- adafruit_st7789.mpy
CircuitPython will automatically reload and begin showing the image from the camera on the LCD. If it doesn't, you can open up the REPL to diagnose what went wrong. Double check that you copied all the files from the bundle, and that you have a compatible build of CircuitPython installed, 7.0.0-beta.4 or newer.
If the image does not appear at all or is in reverse video, try the example for the ili9341. display.
The author did not have a Kaluga with an st7789 display, so this example is untested.

# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries # # SPDX-License-Identifier: Unlicense """ The Kaluga development kit comes in two versions (v1.2 and v1.3); this demo is tested on v1.3. The v1.3 development kit's LCD can have one of two chips, the ili9341 or st7789. This demo is for the ili9341. There is no marking to distinguish the two chips. If the visible portion of the display's flexible cable has a bunch of straight lines, it may be an ili9341. If it has a bunch of wiggly traces, it may be an st7789. If in doubt, try both demos. The audio board must be mounted between the Kaluga and the LCD, it provides the I2C pull-ups(!) """ import board import busio import displayio from adafruit_st7789 import ST7789 import adafruit_ov2640 # Pylint is unable to see that the "size" property of OV2640_GrandCentral exists # pylint: disable=attribute-defined-outside-init # Release any resources currently in use for the displays displayio.release_displays() spi = busio.SPI(MOSI=board.LCD_MOSI, clock=board.LCD_CLK) display_bus = displayio.FourWire( spi, command=board.LCD_D_C, chip_select=board.LCD_CS, reset=board.LCD_RST ) display = ST7789( display_bus, width=320, height=240, rotation=90, reverse_bytes_in_word=True ) bus = busio.I2C(scl=board.CAMERA_SIOC, sda=board.CAMERA_SIOD) cam = adafruit_ov2640.OV2640( bus, data_pins=board.CAMERA_DATA, clock=board.CAMERA_PCLK, vsync=board.CAMERA_VSYNC, href=board.CAMERA_HREF, mclk=board.CAMERA_XCLK, mclk_frequency=20_000_000, size=adafruit_ov2640.OV2640_SIZE_QVGA, ) # cam.flip_x = False # cam.flip_y = True pid = cam.product_id ver = cam.product_version print(f"Detected pid={pid:x} ver={ver:x}") # cam.test_pattern = True g = displayio.Group(scale=1) bitmap = displayio.Bitmap(320, 240, 65536) tg = displayio.TileGrid( bitmap, pixel_shader=displayio.ColorConverter( input_colorspace=displayio.Colorspace.BGR565_SWAPPED ), ) g.append(tg) display.root_group = g display.auto_refresh = False while True: cam.capture(bitmap) bitmap.dirty() display.refresh(minimum_frames_per_second=0) print(".") cam.deinit()
Take the assembled Kaluga board stack (all three boards) and attach the camera at the dedicated header, making sure the pins are inserted properly.
Click the Download Project Bundle button below to download the necessary libraries and the code.py file in a zip file. Extract the contents of the zip file, and copy the entire lib folder and the code.py file to your CIRCUITPY drive.
Your CIRCUITPY drive should resemble the image.
You should have in / of the CIRCUITPY drive:
- code.py
And in the lib folder on your CIRCUITPY drive:
- adafruit_bus_device
- adafruit_ov7670.mpy
- adafruit_ili9341.mpy
CircuitPython will automatically reload and begin showing the image from the camera on the LCD. If it doesn't, you can open up the REPL to diagnose what went wrong. Double check that you copied all the files from the bundle, and that you have a compatible build of CircuitPython installed, 7.0.0-beta.4 or newer.
If the image does not fill the whole display, try removing rotation=90
from the line beginning display = ILI9341
. If it does not appear at all or is in reverse video, try the example for the st7789 display.

# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries # SPDX-FileCopyrightText: Copyright (c) 2021 Jeff Epler for Adafruit Industries # # SPDX-License-Identifier: Unlicense """ The Kaluga development kit comes in two versions (v1.2 and v1.3); this demo is tested on v1.3. It probably won't work on v1.2 without modification. The v1.3 development kit's LCD can have one of two chips, the ili9341 or st7789. Furthermore, there are at least 2 ILI9341 variants, one of which needs rotation=90! This demo is for the ili9341. If the display is garbled, try adding rotation=90, or try modifying it to use ST7799. The camera included with the Kaluga development kit is the incompatible OV2640, it won't work. The audio board must be mounted between the Kaluga and the LCD, it provides the I2C pull-ups(!) """ import time import board import busio import displayio from adafruit_ili9341 import ILI9341 from adafruit_ov7670 import ( # pylint: disable=unused-import OV7670, OV7670_TEST_PATTERN_COLOR_BAR, OV7670_SIZE_DIV2, OV7670_NIGHT_MODE_2, ) # Pylint is unable to see that the "size" property of OV7670_GrandCentral exists # pylint: disable=attribute-defined-outside-init # Release any resources currently in use for the displays displayio.release_displays() spi = busio.SPI(MOSI=board.LCD_MOSI, clock=board.LCD_CLK) display_bus = displayio.FourWire( spi, command=board.LCD_D_C, chip_select=board.LCD_CS, reset=board.LCD_RST ) display = ILI9341(display_bus, width=320, height=240) bus = busio.I2C(scl=board.CAMERA_SIOC, sda=board.CAMERA_SIOD) cam = OV7670( bus, data_pins=board.CAMERA_DATA, clock=board.CAMERA_PCLK, vsync=board.CAMERA_VSYNC, href=board.CAMERA_HREF, mclk=board.CAMERA_XCLK, mclk_frequency=20_000_000, ) cam.size = OV7670_SIZE_DIV2 cam.flip_x = False cam.flip_y = True pid = cam.product_id ver = cam.product_version print(f"Detected pid={pid:x} ver={ver:x}") # cam.test_pattern = OV7670_TEST_PATTERN_COLOR_BAR g = displayio.Group(scale=1) bitmap = displayio.Bitmap(320, 240, 65536) tg = displayio.TileGrid( bitmap, pixel_shader=displayio.ColorConverter( input_colorspace=displayio.Colorspace.RGB565_SWAPPED ), ) g.append(tg) display.root_group = g t0 = time.monotonic_ns() display.auto_refresh = False while True: cam.capture(bitmap) bitmap.dirty() display.refresh(minimum_frames_per_second=0) t1 = time.monotonic_ns() print("fps", 1e9 / (t1 - t0)) t0 = t1 cam.deinit()
Adapting to other ESP32-S2 boards
By selecting appropriate pins, you can adapt the example to work on other RP2040 boards:
- mclk, pclk, vsync, href: Free choice of any pin
- reset, shutdown: Free choice of any pin. Can omit one or both, but the initialization sequence is less reliable.
- d0…d7: Free choice of any pin


Page last edited January 22, 2025
Text editor powered by tinymce.