Displays

August 23, 2026 · View on GitHub

The displaydev product package in pydevices provides several display driver classes. All expose a drawing surface compatible with MicroPython's framebuf API; pydevices-examples demonstrates them.

See Architecture for how drivers connect to board_config.py.

Pick a driver

Your targetDriver classBoard config example
MicroPython MCU (SPI/I80)BusDisplayboard_configs/busdisplay/spi/...
CPython / MicroPython Unix desktopSDLDisplayboard_configs/sdldisplay/
Windows CPython (native Win32)WinDisplayboard_configs/windisplay/
Windows / Chromebook (PyGame easier)PGDisplayboard_configs/pgdisplay/
CircuitPython RGB / USB videoFBDisplayvaries
Jupyter notebookJNDisplayboard_configs/jndisplay/
PyScript browserPSDisplayboard_configs/psdisplay/

Install the matching board config — it constructs the driver for you.

Display classes

BusDisplay

For microcontrollers on MicroPython and CircuitPython.

  • MicroPython: uses spibus / i80bus or community C bus drivers (lvgl_micropython).
  • CircuitPython: uses Adafruit FourWire / ParallelBus — see board configs.

SDLDisplay

SDL2 desktop backend (CPython, MicroPython Unix, CircuitPython Unix). Uses an SDL texture as GRAM. It is the default on MicroPython Unix and available on CPython via board_configs/sdldisplay/.

SDL2 bindings for SDLDisplay: import usdl2 from pydevices-desktop (TestPyPI) or the MIP desktop board package in pydevices (utils/usdl2.py). A native usdl2 module is used when already present in the firmware or environment. See pydevices-desktop.

WinDisplay

Native Win32 HWND backend for CPython on Windows (uwin32). Logical RGB565 GRAM, handed to StretchDIBits as a 16-bit BI_BITFIELDS DIB, so the framebuffer is presented with no colour conversion and no copy. show() skips a frame with nothing pending, and blits only the changed rows when the scale is a whole number. displaydev.auto.AutoDisplay tries it first on win32 before pygame/SDL. Explicit config: board_configs/windisplay/.

Width and height must be even. DIB scanlines are DWORD-aligned, which RGB565 rows only satisfy at an even width, and rotation swaps the two. Odd dimensions raise ValueError at construction.

PGDisplay

PyGame desktop backend. displaydev.auto.AutoDisplay (used by board_configs/desktop/) selects it after WinDisplay on Windows, and first on other CPython desktops; if PyGame is not installed it falls back to SDLDisplay. Explicit config: board_configs/pgdisplay/.

FBDisplay

Works with CircuitPython framebufferio.FramebufferDisplay — dotclock (RGB), USB Video, RGB Matrix.

USB Video lets a board stream the framebuffer as a USB webcam (RP2040; host support varies).

JNDisplay

Jupyter Notebook output via an interactive ipywidgets image. Input (mouse, wheel, keyboard) is captured by JNDevices (ipyevents) and delivered as events. Config: board_configs/jndisplay/.

PSDisplay

PyScript browser canvas. Input (pointer/touch/pen, wheel, keyboard, gamepad) is captured by PSDevices and delivered as events. Config: board_configs/psdisplay/. See PSDisplay.

Display backends expose input without choosing an application coordinator. Applications using the optional appdev app can drain the backend's events records through a HostEventsDevice. The input source depends on what each platform exposes:

BackendsInput sourceWired via
SDLDisplay, PGDisplay, WinDisplaySystem-wide OS queue drain (module get_events, also on display_drv.get_events)appdev.App(..., host_read=display_drv.get_events)
JNDisplay, PSDisplayPer-surface PSDevices / JNDevices, exposed as display_drv.get_eventsappdev.App(..., host_read=display_drv.get_events)

With appdev, handlers see the same events objects, so application code does not need to know which backend is active. LVGL instead connects these neutral backend capabilities through its own display_driver coordinator. Desktop board configs also use timer_async=env_bool("PYDEVICES_TIMER_ASYNC", display_drv.requires_async_timer) (requires_async_timer is True only on PS/JN). appdev.App raises if timer_async=False while any attached display has requires_async_timer.

Desktop (SDL2, PyGame)

SDL2 and PyGame provide a real OS event queue. The driver module drains it and converts each event to an events object:

from displaydev.sdldisplay import SDLDisplay
import appdev

display_drv = SDLDisplay(...)
app = appdev.App(
    displays=[display_drv],
    host_read=display_drv.get_events,
)

Use poll_event() only for optional manual single-event checks — not as the host_read= callback (it returns one event, not a list).

Desktop hosts (SDLDisplay, PGDisplay, WinDisplay) set display_drv.quit_chord to CTRL+Q (keys.K_q + keys.KMOD_CTRL). HostEventsDevice matches that chord with keys.chord_matches and emits events.QUIT. Window-close still emits events.QUIT from SDL/PyGame. MCU drivers leave quit_chord as None.

Pointer coordinates use display_drv.touch_scale (see capabilities() per backend); HostEventsDevice divides mouse events by that scale.

This captures mouse motion/buttons, the wheel, the keyboard, the window-close (QUIT) event, and joysticks/gamepads (JOYAXISMOTION, JOYBALLMOTION, JOYHATMOTION, JOYBUTTONDOWN, JOYBUTTONUP). Connect controllers before launching — hot-plugging after startup is not handled.

Browser / notebook (PyScript, Jupyter)

PSDevices (PyScript) and JNDevices (Jupyter) capture all available input on the canvas/widget and turn it into the same events objects. The display owns that drain as get_events:

from displaydev.psdisplay import PSDisplay
import appdev

display_drv = PSDisplay("display_canvas", width, height)
app = appdev.App(
    displays=[display_drv],
    host_read=display_drv.get_events,
    timer_async=display_drv.requires_async_timer,
)

Each captures:

  • PointerMOUSEMOTION on every move and MOUSEBUTTONDOWN / MOUSEBUTTONUP for any button. On PyScript this uses Pointer Events, so mouse, touch, and pen all work (with the touch flag set for non-mouse pointers).
  • WheelMOUSEWHEEL (also consumed by encoder devices).
  • KeyboardKEYDOWN / KEYUP with SDL-style key codes, names, and modifier masks (incl. left/right modifier variants) via keys and displaydev DOM helpers.
  • Gamepad (PyScript only) — JOYAXISMOTION / JOYBUTTONDOWN / JOYBUTTONUP, polled from the Gamepad API on each read().
  • QuitPSDisplay / JNDisplay set quit_chord to browser/TV Back (keys.K_AC_BACK). HostEventsDevice turns that KEYDOWN into events.QUIT (same as closing an SDL window). Reassign if the host intercepts Back:
import keys

display_drv.quit_chord = (keys.K_c, keys.KMOD_CTRL)  # e.g. CTRL+C on Jupyter

Caveat: key events require the canvas/widget to be focused (click it first), and the notebook/browser front end may consume some keys (arrows, space, Ctrl/Cmd shortcuts) before they reach the helper. This makes keyboard input on these backends less reliable than on the desktop SDL/PyGame backends.

Rotation on these backends only reshapes the surface (e.g. 320×480 ↔ 480×320); it does not physically rotate, so pointer coordinates need no rotation remapping.

Canvases

Anything you can draw on implements the framebuf API:

  • The display itself
  • framebuf bytearrays
  • pygraphics.BMP565 bitmap files
  • displaybuf.DisplayBuffer (see pydevices-examples utils)

Timing

pydevices-examples does not include a task scheduler. Options:

  • asyncio — works on CPython, MicroPython, and PyScript (required there)
  • multimer — explicit or auto-selected Timer providers for sync loops; AsyncTimer for async/PyScript apps

Vertical scrolling

Many drivers expose ILI9341-style vertical scroll: a top fixed band (TFA), a scrollable middle (VSA), and a bottom fixed band (BFA). You define regions with set_vscroll(tfa, bfa) or vscrdef, then move content with the vscroll property (wrapper around vscsad).

The pydevices_demo example demonstrates this model, covers drawing at vscroll = 0 during redraw, and shows auto-scroll with multimer.

Related examples: scroll_touch_test.py (touch Up/Down), appdev_encoder_test.py (encoder).

Rotation

BusDisplay uses CircuitPython-style rotation degrees (0, 90, 180, 270).

Known issues: Unix SDL rotation clears the screen; scrolling while rotated has edge cases on desktop and MCU — track work on GitHub Issues.

Next

API reference

displaydev source and product docs.