OK, let's setup an actual display so we can start showing stuff. There are two parts to this - the display itself, called Display, and how it is connected to the host controller via some "display bus" like FourWire, ParallelBus. etc.

You first setup the display bus specific to your setup, be it FourWire, ParallelBus, etc. Then, when you setup your Display, you will pass this in so it can be used.


The FourWire class is used to talk to displays over a spi_bus using the typical four pins associated with SPI - SCK, MOSI, MISO, and CS (aka, chip_select). One additional pin needed for the display is a pin to indicate if the information being sent over the bus is "data" (image information) or "command" (display control). This is done with the D/C pin specified via the command parameter.

To setup a FourWire bus, you would first create a spi_bus object in the normal way. You would then pass that in, along with specifications for the command and chip_select pins to use.

Here's the basic usage example for hardware SPI:

display_bus = displayio.FourWire(board.SPI(),


The I2CDisplay class is used to talk to displays over an i2c_bus. You specify the display device_address like you typically would for an I2C device. An optional reset pin can also be specified.

To setup an I2CDisplay bus, you would first create an i2c_bus object and then pass that in along with the device_address. Here's the basic usage example:

display_bus = displayio.I2CDisplay(board.I2C(),


A parallel bus is fast, but it takes a lot of pins. You'll need 8 pins for the main data, and they need to be in consecutive order on one of the microcontroller's ports and the first pin has to be on port number 0, 7, 15, or 23 (so we can write the byte in a single DMA command). Then you specify the first pin for data0 and the rest (the other 7) are inferred. Then you need 4 more digital pins that can be used for command, chip_select, write, and read. Oof. That's 12 pins.

The biggest road block will be finding a microcontroller with all those pins AND with 8 consecutive pins on the same port. What does "port" mean? It refers to something lower level that you may not generally worry about. Think of it as a group of pins that can be collectively manipulated quickly via commands that operate on the entire port.

How do you find 8 consecutive port pins? We'll, if you're starting from scratch, it'll take a bit on investigating. Here's one example. Take a look at the Metro M4 Express schematic and look in the general area where pin D13 is shown:

For example, D13 is wired to physical pin 35 which has several functions internally. The important one to note is PA16. This refers to the digital I/O on Port A at 16. Note that the pins below D13 go consecutively from PA16 to PA23. That's 8 pins on Port A we can use!

So, for a Metro M4 Express, you could use pins D13, D12, D10, D11, D9, D8, D1, and D0 for your 8 data pins. Then just pick any other 4 for the others.

display_bus = displayio.ParallelBus(data0=board.D13,


To setup a Display you need four things:

  • A display bus (display_bus) for actually talking to the display.
  • An initialization sequence (init_sequence) to be used to setup the display for initial use
  • The width and...
  • The height of the display in pixels.

In general, you'll know the width and height for whatever display you are working with. The display_bus is one of the available display buses setup as described above. The init_sequence takes a bit of work to come up with. Typically it comes from reading datasheets or other sources. We'll talk more about this below. For now, just assume you have it created in something called INIT_SEQUENCE.

The basic Display setup would then look like this:

display = displayio.Display(display_bus,

Display Drivers

The init sequence (init_sequence) is a bit of a cryptic mess. We've worked it out for some displays and have created some light weight drivers that take care of the boiler plate. Instead of creating a Display object from scratch, you can use these drivers (and maybe more, check the guide for your display):

Then you would create your display like this:

display = adafruit_ili9341.ILI9341(display_bus,

Note that it's basically the same as using Display, just without the init_sequence. That's taken care of for you.

Boards with Built In Displays

If you have a board like a HalloWing, PyPortal, CLUE, etc. that already has a display attached, then all this work has been done for you - both the setting up of the display bus and the display itself. The CircuitPython firmware build for these boards has the display ready to go. It is available via the DISPLAY object found in the board module. All you need to do is:

import board
display = board.DISPLAY

Boards without Built In Displays

If you have a more generic main board, like a Feather or Matrix Portal, then you will need to create the display manually as described above. This means there will not be a board.DISPLAY available in the Circuit Python firmware.

  • For OLED and TFT FeatherWings or breakouts, see examples in their associated library.
  • For RGB matrices with the Matrix Portal, checkout the MatrixPortal Library.

Using a Display

Once a Display is setup, use the root_group property to specify the Group to use for displaying items on the screen. Creating a Group and the associated TileGrid(s) and Bitmap(s) and Palette(s) has been covered previously in this guide. Once you have your Group setup and ready to go, it's just a matter of calling:

display.root_group = group
For CircuitPython versions prior to 8.0, use display.show(group) instead.

Keep in mind that while a Display can only show one Group (the so called root group), multiple Groups can be nested within the root group for more complex layouts.

Releasing Displays

Once you've created your display instance, the CircuitPython firmware will remember the setup between soft resets. This helps facilitate showing the serial output on the display, which can be useful for seeing error messages, etc.. Because of this behavior, you may run into an issue similar to what is shown below:

To avoid this issue, you can use the release_displays() command in displayio. Call this before creating your display bus. You can tuck this call in somewhere up near the top of your code. For example:

import board
import displayio
import adafruit_ssd1327

# release any currently configured displays

# go through display setup as normal
display_bus = displayio.I2CDisplay(board.I2C(), device_address=0x3D)
display = adafruit_ssd1327.SSD1327(display_bus, width=128, height=128)

# use display as you wish

Now the code will run without errors with each soft reset.

You do not need to do this for boards with built in displays. For those cases, the release gets taken care of for you as part of the internal display creation which provides the board.DISPLAY instance.

This guide was first published on Apr 30, 2019. It was last updated on Oct 04, 2023.

This page (Display and Display Bus) was last updated on Oct 03, 2023.

Text editor powered by tinymce.