To set up a display, you need to have several major pieces of information:
- The GPIO connections for the display (TFT_PINS)
- The I/O expander configuration (TFT_IO_EXPANDER)
- The resolution and "timings" of the display (TFT_TIMINGS), which also includes information about the polarity of certain signals.
- The initialization code of the display (TFT_INIT_SEQUENCE)
Luckily, this guide provides all the information for the displays that are sold in the Adafruit shop. However, if you have a different display, you will need to find the information in the data sheet.
If a board is designed for dot clock TFT displays, the GPIO connections are listed in board.TFT_PINS
. Otherwise, it depends on how the display is connected.
If the board is designed for a single display, then the timings are listed in board.TFT_TIMINGS
.
These values are used in the display constructor with the **
so that each element becomes a separate argument to the function.
If the board's built in display requires an initialization sequence, then this is given as board.TFT_INIT_SEQUENCE
. If the SPI bus is on an I2C I/O expander the settings for the I/O expander are in board.TFT_IO_EXPANDER
, intended to be expanded with **
.
CircuitPython Board Definition
If a board is tied to a specific display, then board definition in the CircuitPython c code can initialize the dot clock TFT display. For example, this is done with the Espressif ESP32-S3 LCD EV board:
// This file is part of the CircuitPython project: https://circuitpython.org // // SPDX-FileCopyrightText: Copyright (c) 2020 Scott Shawcroft for Adafruit Industries // // SPDX-License-Identifier: MIT #include "py/objtuple.h" #include "boards/espressif_esp32s3_lcd_ev/board.h" #include "shared-bindings/board/__init__.h" #include "shared-module/displayio/__init__.h" static const mp_rom_map_elem_t tft_io_expander_table[] = { { MP_ROM_QSTR(MP_QSTR_i2c_address), MP_ROM_INT(0x20)}, { MP_ROM_QSTR(MP_QSTR_gpio_address), MP_ROM_INT(1)}, { MP_ROM_QSTR(MP_QSTR_gpio_data_len), MP_ROM_INT(1)}, { MP_ROM_QSTR(MP_QSTR_gpio_data), MP_ROM_INT(0xF1)}, { MP_ROM_QSTR(MP_QSTR_cs_bit), MP_ROM_INT(1)}, { MP_ROM_QSTR(MP_QSTR_mosi_bit), MP_ROM_INT(3)}, { MP_ROM_QSTR(MP_QSTR_clk_bit), MP_ROM_INT(2)}, { MP_ROM_QSTR(MP_QSTR_i2c_init_sequence), &i2c_init_byte_obj}, }; MP_DEFINE_CONST_DICT(tft_io_expander_dict, tft_io_expander_table); static const mp_rom_obj_tuple_t tft_r_pins = { {&mp_type_tuple}, 5, { MP_ROM_PTR(&pin_GPIO1), MP_ROM_PTR(&pin_GPIO2), MP_ROM_PTR(&pin_GPIO42), MP_ROM_PTR(&pin_GPIO41), MP_ROM_PTR(&pin_GPIO40), } }; static const mp_rom_obj_tuple_t tft_g_pins = { {&mp_type_tuple}, 6, { MP_ROM_PTR(&pin_GPIO21), MP_ROM_PTR(&pin_GPIO47), MP_ROM_PTR(&pin_GPIO48), MP_ROM_PTR(&pin_GPIO45), MP_ROM_PTR(&pin_GPIO38), MP_ROM_PTR(&pin_GPIO39), } }; static const mp_rom_obj_tuple_t tft_b_pins = { {&mp_type_tuple}, 5, { MP_ROM_PTR(&pin_GPIO10), MP_ROM_PTR(&pin_GPIO11), MP_ROM_PTR(&pin_GPIO12), MP_ROM_PTR(&pin_GPIO13), MP_ROM_PTR(&pin_GPIO14), } }; static const mp_rom_map_elem_t tft_pins_table[] = { { MP_ROM_QSTR(MP_QSTR_de), MP_ROM_PTR(&pin_GPIO17) }, { MP_ROM_QSTR(MP_QSTR_vsync), MP_ROM_PTR(&pin_GPIO3) }, { MP_ROM_QSTR(MP_QSTR_hsync), MP_ROM_PTR(&pin_GPIO46) }, { MP_ROM_QSTR(MP_QSTR_dclk), MP_ROM_PTR(&pin_GPIO9) }, { MP_ROM_QSTR(MP_QSTR_red), MP_ROM_PTR(&tft_r_pins) }, { MP_ROM_QSTR(MP_QSTR_green), MP_ROM_PTR(&tft_g_pins) }, { MP_ROM_QSTR(MP_QSTR_blue), MP_ROM_PTR(&tft_b_pins) }, }; MP_DEFINE_CONST_DICT(tft_pins_dict, tft_pins_table); static const mp_rom_map_elem_t tft_timings_table[] = { { MP_ROM_QSTR(MP_QSTR_frequency), MP_ROM_INT(6500000) }, // nominal 16MHz, but display is unstable/tears at that frequency { MP_ROM_QSTR(MP_QSTR_width), MP_ROM_INT(480) }, { MP_ROM_QSTR(MP_QSTR_height), MP_ROM_INT(480) }, { MP_ROM_QSTR(MP_QSTR_hsync_pulse_width), MP_ROM_INT(13) }, { MP_ROM_QSTR(MP_QSTR_hsync_front_porch), MP_ROM_INT(20) }, { MP_ROM_QSTR(MP_QSTR_hsync_back_porch), MP_ROM_INT(40) }, { MP_ROM_QSTR(MP_QSTR_hsync_idle_low), MP_ROM_FALSE }, { MP_ROM_QSTR(MP_QSTR_vsync_pulse_width), MP_ROM_INT(15) }, { MP_ROM_QSTR(MP_QSTR_vsync_front_porch), MP_ROM_INT(20) }, { MP_ROM_QSTR(MP_QSTR_vsync_back_porch), MP_ROM_INT(40) }, { MP_ROM_QSTR(MP_QSTR_vsync_idle_low), MP_ROM_FALSE }, { MP_ROM_QSTR(MP_QSTR_de_idle_high), MP_ROM_FALSE }, { MP_ROM_QSTR(MP_QSTR_pclk_active_high), MP_ROM_FALSE }, { MP_ROM_QSTR(MP_QSTR_pclk_idle_high), MP_ROM_FALSE }, }; MP_DEFINE_CONST_DICT(tft_timings_dict, tft_timings_table); static const mp_rom_map_elem_t board_module_globals_table[] = { CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS { MP_ROM_QSTR(MP_QSTR_TFT_PINS), MP_ROM_PTR(&tft_pins_dict) }, { MP_ROM_QSTR(MP_QSTR_TFT_TIMINGS), MP_ROM_PTR(&tft_timings_dict) }, { MP_ROM_QSTR(MP_QSTR_TFT_IO_EXPANDER), MP_ROM_PTR(&tft_io_expander_dict) }, { MP_ROM_QSTR(MP_QSTR_TFT_INIT_SEQUENCE), &display_init_byte_obj}, { MP_ROM_QSTR(MP_QSTR_I2S_SCK), MP_ROM_PTR(&pin_GPIO16) }, { MP_ROM_QSTR(MP_QSTR_I2S_MCLK), MP_ROM_PTR(&pin_GPIO5) }, { MP_ROM_QSTR(MP_QSTR_I2S_WS), MP_ROM_PTR(&pin_GPIO7) }, { MP_ROM_QSTR(MP_QSTR_I2S_SDO), MP_ROM_PTR(&pin_GPIO6) }, { MP_ROM_QSTR(MP_QSTR_TX), MP_ROM_PTR(&pin_GPIO43) }, { MP_ROM_QSTR(MP_QSTR_RX), MP_ROM_PTR(&pin_GPIO44) }, { MP_ROM_QSTR(MP_QSTR_SCL), MP_ROM_PTR(DEFAULT_I2C_BUS_SCL) }, { MP_ROM_QSTR(MP_QSTR_SDA), MP_ROM_PTR(DEFAULT_I2C_BUS_SDA) }, { MP_ROM_QSTR(MP_QSTR_DISPLAY), MP_ROM_PTR(&displays[0].display) }, // boot mode button can be used in SW as well { MP_ROM_QSTR(MP_QSTR_BUTTON), MP_ROM_PTR(&pin_GPIO0) }, { MP_ROM_QSTR(MP_QSTR_I2C), MP_ROM_PTR(&board_i2c_obj) }, }; MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table);
// This file is part of the CircuitPython project: https://circuitpython.org // // SPDX-FileCopyrightText: Copyright (c) 2020 Scott Shawcroft for Adafruit Industries // // SPDX-License-Identifier: MIT #include "supervisor/board.h" #include "mpconfigboard.h" #include "shared-bindings/board/__init__.h" #include "shared-bindings/busio/I2C.h" #include "shared-bindings/dotclockframebuffer/DotClockFramebuffer.h" #include "shared-bindings/dotclockframebuffer/__init__.h" #include "shared-bindings/framebufferio/FramebufferDisplay.h" #include "shared-bindings/microcontroller/Pin.h" #include "shared-module/displayio/__init__.h" #include "boards/espressif_esp32s3_lcd_ev/board.h" #define MP_DEFINE_BYTES_OBJ(obj_name, bin) mp_obj_str_t obj_name = {{&mp_type_bytes}, 0, sizeof(bin) - 1, (const byte *)bin} static const uint8_t display_init_sequence[] = { 0xf0, 5, 0x55, 0xaa, 0x52, 0x08, 0x00, 0xf6, 2, 0x5a, 0x87, 0xc1, 1, 0x3f, 0xc2, 1, 0x0e, 0xc6, 1, 0xf8, 0xc9, 1, 0x10, 0xcd, 1, 0x25, 0xf8, 1, 0x8a, 0xac, 1, 0x45, 0xa0, 1, 0xdd, 0xa7, 1, 0x47, 0xfa, 4, 0x00, 0x00, 0x00, 0x04, 0x86, 4, 0x99, 0xa3, 0xa3, 0x51, 0xa3, 1, 0xee, 0xfd, 3, 0x3c, 0x3c, 0x00, 0x71, 1, 0x48, 0x72, 1, 0x48, 0x73, 2, 0x00, 0x44, 0x97, 1, 0xee, 0x83, 1, 0x93, 0x9a, 1, 0x72, 0x9b, 1, 0x5a, 0x82, 2, 0x2c, 0x2c, 0xb1, 1, 0x10, 0x6d, 32, 0x00, 0x1f, 0x19, 0x1a, 0x10, 0x0e, 0x0c, 0x0a, 0x02, 0x07, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x08, 0x01, 0x09, 0x0b, 0x0d, 0x0f, 0x1a, 0x19, 0x1f, 0x00, 0x64, 16, 0x38, 0x05, 0x01, 0xdb, 0x03, 0x03, 0x38, 0x04, 0x01, 0xdc, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, 0x65, 16, 0x38, 0x03, 0x01, 0xdd, 0x03, 0x03, 0x38, 0x02, 0x01, 0xde, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, 0x66, 16, 0x38, 0x01, 0x01, 0xdf, 0x03, 0x03, 0x38, 0x00, 0x01, 0xe0, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, 0x67, 16, 0x30, 0x01, 0x01, 0xe1, 0x03, 0x03, 0x30, 0x02, 0x01, 0xe2, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, 0x68, 13, 0x00, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a, 0x60, 8, 0x38, 0x08, 0x7a, 0x7a, 0x38, 0x09, 0x7a, 0x7a, 0x63, 8, 0x31, 0xe4, 0x7a, 0x7a, 0x31, 0xe5, 0x7a, 0x7a, 0x69, 7, 0x04, 0x22, 0x14, 0x22, 0x14, 0x22, 0x08, 0x6b, 1, 0x07, 0x7a, 2, 0x08, 0x13, 0x7b, 2, 0x08, 0x13, 0xd1, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd2, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd3, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd4, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd5, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd6, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0x3a, 1, 0x66, 0x3a, 1, 0x66, 0x11, 0x80, 120, 0x29, 0x0, 0, // trailing NUL for Python bytes() representation }; MP_DEFINE_BYTES_OBJ(display_init_byte_obj, display_init_sequence); static const char i2c_bus_init_sequence[] = { 2, 3, 0xf1, // set GPIO direction 2, 2, 0, // disable all output inversion 0, // trailing NUL for Python bytes() representation }; MP_DEFINE_BYTES_OBJ(i2c_init_byte_obj, i2c_bus_init_sequence); static const mcu_pin_obj_t *red_pins[] = { &pin_GPIO1, &pin_GPIO2, &pin_GPIO42, &pin_GPIO41, &pin_GPIO40 }; static const mcu_pin_obj_t *green_pins[] = { &pin_GPIO21, &pin_GPIO47, &pin_GPIO48, &pin_GPIO45, &pin_GPIO38, &pin_GPIO39 }; static const mcu_pin_obj_t *blue_pins[] = { &pin_GPIO10, &pin_GPIO11, &pin_GPIO12, &pin_GPIO13, &pin_GPIO14 }; void board_init(void) { dotclockframebuffer_framebuffer_obj_t *framebuffer = &allocate_display_bus_or_raise()->dotclock; framebuffer->base.type = &dotclockframebuffer_framebuffer_type; common_hal_dotclockframebuffer_framebuffer_construct( framebuffer, /* de */ &pin_GPIO17, /* vsync */ &pin_GPIO3, /* hsync */ &pin_GPIO46, /* dclk */ &pin_GPIO9, /* data */ red_pins, MP_ARRAY_SIZE(red_pins), green_pins, MP_ARRAY_SIZE(green_pins), blue_pins, MP_ARRAY_SIZE(blue_pins), /* frequency */ 12000000, /* width x height */ 480, 480, /* horizontal: pulse, back & front porch, idle */ 13, 20, 40, false, /* vertical: pulse, back & front porch, idle */ 15, 20, 40, false, /* de_idle_high */ false, /* pclk_active_high */ true, /* pclk_idle_high */ false, /* overscan_left */ 0 ); framebufferio_framebufferdisplay_obj_t *disp = &allocate_display_or_raise()->framebuffer_display; disp->base.type = &framebufferio_framebufferdisplay_type; common_hal_framebufferio_framebufferdisplay_construct( disp, framebuffer, 0, true ); busio_i2c_obj_t i2c; i2c.base.type = &busio_i2c_type; common_hal_busio_i2c_construct(&i2c, DEFAULT_I2C_BUS_SCL, DEFAULT_I2C_BUS_SDA, 400000, 255); const int i2c_device_address = 32; dotclockframebuffer_ioexpander_spi_bus spibus = { .bus = &i2c, .i2c_device_address = i2c_device_address, .i2c_write_size = 2, .addr_reg_shadow = { .u32 = 1 }, // GPIO data at register 1 .cs_mask = 0x100 << 1, // data payload is at byte 2 .mosi_mask = 0x100 << 3, .clk_mask = 0x100 << 2, }; static const mp_buffer_info_t bufinfo_display_init = { (void *)display_init_sequence, sizeof(display_init_sequence) - 1 }; static const mp_buffer_info_t bufinfo_i2c_bus_init = { (void *)i2c_bus_init_sequence, sizeof(i2c_bus_init_sequence) - 1 }; dotclockframebuffer_ioexpander_send_init_sequence(&spibus, &bufinfo_i2c_bus_init, &bufinfo_display_init); common_hal_busio_i2c_deinit(&i2c); } // Use the MP_WEAK supervisor/shared/board.c versions of routines not defined here.
Example TFT_PINS
The TFT_PINS should be arranged in a Python dict. For the Qualia ESP32-S3, you can simply use board.TFT_PINS
. They should be arranged similar to the Espressif LCD EV board's TFT_PINS
:
{ "de": microcontroller.pin.GPIO17, "vsync": microcontroller.pin.GPIO3, "hsync": microcontroller.pin.GPIO46, "dclk": microcontroller.pin.GPIO9, "red": ( microcontroller.pin.GPIO1, microcontroller.pin.GPIO2, microcontroller.pin.GPIO42, microcontroller.pin.GPIO41, microcontroller.pin.GPIO40, ), "green": ( microcontroller.pin.GPIO21, microcontroller.pin.GPIO47, microcontroller.pin.GPIO48, microcontroller.pin.GPIO45, microcontroller.pin.GPIO38, microcontroller.pin.GPIO39, ), "blue": ( microcontroller.pin.GPIO10, microcontroller.pin.GPIO11, microcontroller.pin.GPIO12, microcontroller.pin.GPIO13, microcontroller.pin.GPIO14, ), }
Example TFT_TIMINGS
The specific timings can be found in the display datasheet or, for displays sold through the Adafruit store, on the page for the specific display in this guide.
As an example, here are the timings for the 480x480 display from the Espressif LCD EVK:
TFT_TIMINGS = { "frequency": 6_500_000, # should be 18_000_000, "width": 480, "height": 480, "hsync_pulse_width": 13, "hsync_front_porch": 40, "hsync_back_porch": 20, "vsync_pulse_width": 15, "vsync_front_porch": 40, "vsync_back_porch": 20, "hsync_idle_low": False, "vsync_idle_low": False, "de_idle_high": False, "pclk_active_high": True, "pclk_idle_high": False, }
Timings for the 720x720 square display, which does not require a SPI init sequence, would look like this:
tft_timings = { "frequency": 6_500_000, "width": 720, "height": 720, "hsync_pulse_width": 20, "hsync_front_porch": 40, "hsync_back_porch": 40, "vsync_pulse_width": 10, "vsync_front_porch": 40, "vsync_back_porch": 40, "hsync_idle_low": False, "vsync_idle_low": False, "de_idle_high": False, "pclk_active_high": False, "pclk_idle_high": False, }
I/O Expander
The dotclockframebuffer.ioexpander_send_init_sequence()
function supports a "generic I2C I/O expander". Generic meaning:
- Any I2C address can be used.
- Any GPIO register address can be used.
- GPIO data can be 1 or 2 bytes (8 or 16 bits).
- Arbitrary I2C registers can be initialized for setting direction, pull, inversion, etc.
- State of other GPIO bits can be specified explicitly to avoid undesirable pin state changes.
Here are some values for a PCA9554 expander. This is the IO expander used on the Qualia ESP32-S3 and the values can be found in board.TFT_IO_EXPANDER
:
i2c_address=0x3f
-
gpio_address=1
(the GPIO output register address) -
gpio_data_len=1
(1 byte of data) -
gpio_data=0xfd
(value of other GPIOs on expander) -
cs_bit=1
(index of chip select) -
mosi_bit=7
(index of data out) -
clk_bit=0
(index of clock) -
reset_bit=2
(optional index of reset pin) -
i2c_init_sequence=b'...'
(other register settings, see below)
I2C Initialization Sequence
Using an I2C init sequence lets arbitrary registers on the I/O expander be set.
It is composed of a series of commands, starting with a byte length. Each is sent to the I/O expander I2C address.
Typical for PCA9554 expander:
i2c_init_sequence=bytes(( 2, 3, 0x78, # set pin direction (register 3) to 0x78 (0-bit is output mode) 2, 2, 0 # disable output inverts (register 2) to 0 ))
Display Initialization Code
Some dot clock displays require "initialization code" to be sent on a unidirectional 3-wire bus. The data is transmitted in "mode 0", which is 9 bits long. The top bit specifies whether the code byte is data or a command, with 0
being command and 1
being data.
The structure of the initialization data is a series of commands. Each command can have associated data and an associated delay:
- First byte: 8-bit command value
- Second byte: 7-bit data length (may be zero). The top bit (0x80) is set if a delay byte follows the data
- Variable number of bytes: 8-bit data values
- Optional: 8-bit delay value.
The delay value, if specified, is in milliseconds. The special delay value of 255
or 0xFF
is treated as 500 milliseconds.
Display initialization codes are the same as the ones used by displayio.FourWire except that the default after each data block is no delay instead of 10ms.
Example 1
The following byte sequence sends the command 0xfa followed by 4 bytes of data and no delay:
0xfa, 4, 0x00, 0x00, 0x00, 0x04,
Example 2
The following byte sequence sends the command 0x11, no data, and then delays by a minimum of 120ms:
0x11, 0x80, 120,
Sending Initialization Code via I2C IO Expander
There is special support for sending initialization code over an I2C IO expander chip. This requires a series of steps:
- Construct the I2C bus object.
- Call
ioexpander_send_init_sequence()
with the appropriate values - Optionally, deconstruct the I2C bus object so the pins become available.
The gpio_data
parameter must be pre-set with the correct value all I/O pins, because it is not assumed that the current output values can be read back.
I2C Bus Speed
The default clock speed of I2C busses in CircuitPython is 100kHz. In practice, using a 400kHz bus for display initialization works, even if some device on the bus only supports 100kHz I2C, because a 100kHz device will not hear its own address on the bus; it will simply stay idle. Doing this can speed display initialization. However, this is not guaranteed by the I2C specification, so if you encounter trouble, try an I2C bus at the regular 100kHz speed instead.
Here is the initialization code for the 480x480 square display on the Espressif LCD EVK, which uses 400kHz for the I2C Bus Speed:
init_sequence = bytes(( 0xf0, 5, 0x55, 0xaa, 0x52, 0x08, 0x00, 0xf6, 2, 0x5a, 0x87, 0xc1, 1, 0x3f, 0xc2, 1, 0x0e, 0xc6, 1, 0xf8, 0xc9, 1, 0x10, 0xcd, 1, 0x25, 0xf8, 1, 0x8a, 0xac, 1, 0x45, 0xa0, 1, 0xdd, 0xa7, 1, 0x47, 0xfa, 4, 0x00, 0x00, 0x00, 0x04, 0x86, 4, 0x99, 0xa3, 0xa3, 0x51, 0xa3, 1, 0xee, 0xfd, 3, 0x3c, 0x3c, 0x00, 0x71, 1, 0x48, 0x72, 1, 0x48, 0x73, 2, 0x00, 0x44, 0x97, 1, 0xee, 0x83, 1, 0x93, 0x9a, 1, 0x72, 0x9b, 1, 0x5a, 0x82, 2, 0x2c, 0x2c, 0xb1, 1, 0x10, 0x6d, 32, 0x00, 0x1f, 0x19, 0x1a, 0x10, 0x0e, 0x0c, 0x0a, 0x02, 0x07, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x1e, 0x08, 0x01, 0x09, 0x0b, 0x0d, 0x0f, 0x1a, 0x19, 0x1f, 0x00, 0x64, 16, 0x38, 0x05, 0x01, 0xdb, 0x03, 0x03, 0x38, 0x04, 0x01, 0xdc, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, 0x65, 16, 0x38, 0x03, 0x01, 0xdd, 0x03, 0x03, 0x38, 0x02, 0x01, 0xde, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, 0x66, 16, 0x38, 0x01, 0x01, 0xdf, 0x03, 0x03, 0x38, 0x00, 0x01, 0xe0, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, 0x67, 16, 0x30, 0x01, 0x01, 0xe1, 0x03, 0x03, 0x30, 0x02, 0x01, 0xe2, 0x03, 0x03, 0x7a, 0x7a, 0x7a, 0x7a, 0x68, 13, 0x00, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a, 0x08, 0x15, 0x08, 0x15, 0x7a, 0x7a, 0x60, 8, 0x38, 0x08, 0x7a, 0x7a, 0x38, 0x09, 0x7a, 0x7a, 0x63, 8, 0x31, 0xe4, 0x7a, 0x7a, 0x31, 0xe5, 0x7a, 0x7a, 0x69, 7, 0x04, 0x22, 0x14, 0x22, 0x14, 0x22, 0x08, 0x6b, 1, 0x07, 0x7a, 2, 0x08, 0x13, 0x7b, 2, 0x08, 0x13, 0xd1, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd2, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd3, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd4, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd5, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0xd6, 52, 0x00, 0x00, 0x00, 0x04, 0x00, 0x12, 0x00, 0x18, 0x00, 0x21, 0x00, 0x2a, 0x00, 0x35, 0x00, 0x47, 0x00, 0x56, 0x00, 0x90, 0x00, 0xe5, 0x01, 0x68, 0x01, 0xd5, 0x01, 0xd7, 0x02, 0x36, 0x02, 0xa6, 0x02, 0xee, 0x03, 0x48, 0x03, 0xa0, 0x03, 0xba, 0x03, 0xc5, 0x03, 0xd0, 0x03, 0xe0, 0x03, 0xea, 0x03, 0xfa, 0x03, 0xff, 0x3a, 1, 0x66, 0x3a, 1, 0x66, 0x11, 0x80, 120, 0x29, 0x80, 20 )) expander_addr = 32 bus = busio.I2C(microcontroller.pin.GPIO18, microcontroller.pin.GPIO8, frequency=400_000) if not bus.try_lock(): raise RuntimeError("Bus already locked") # Set direction register bus.writeto(expander_addr, b"\3\xf1") # Set pull ups bus.writeto(expander_addr, b"\2\0") bus.unlock() t0 = time.monotonic() ioexpander_send_init_sequence( bus=bus, i2c_address=expander_addr, gpio_address=1, gpio_data_len=1, gpio_data=0xf1, cs_bit=1, mosi_bit=3, clk_bit=2, init_sequence=init_sequence) t1 = time.monotonic() print(t1-t0, "s to send init code")
Boards that have a built in display can perform these steps in the board init function such as the Espressif LCD EV board.
Constructing the framebuffer and the display
Because most of the heavy lifting is done by setting up the init codes, constructing the framebuffer and display only requires a couple of lines of code:
fb = DotClockFramebuffer(**TFT_PINS, **TFT_TIMINGS) disp = FramebufferDisplay(fb, auto_refresh=True)
Dot clocks
The higher the dot clock frequency, the more susceptible the display is to distortions while doing PSRAM-intensive activities. This looks like portions of the screen shifting horizontally for a frame, then returning to the normal position.
With IDF 5.1, frequencies up to 16MHz mostly work OK.
For most displays, the user can select a lower clock (down to some display-dependent minimum). This decreases refresh rate but reduces the chance of distortion.
Text editor powered by tinymce.