diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 48e0ab6fbfd85..6cd13610ebd5e 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -99,6 +99,7 @@ msgid "%q contains duplicate pins" msgstr "" #: ports/atmel-samd/common-hal/sdioio/SDCard.c +#: ports/espressif/common-hal/qspibus/QSPIBus.c msgid "%q failure: %d" msgstr "" @@ -646,6 +647,10 @@ msgstr "" msgid "Baudrate not supported by peripheral" msgstr "" +#: ports/espressif/common-hal/qspibus/QSPIBus.c +msgid "Begin transaction first" +msgstr "" + #: shared-module/busdisplay/BusDisplay.c #: shared-module/framebufferio/FramebufferDisplay.c msgid "Below minimum frame rate" @@ -712,6 +717,10 @@ msgstr "" msgid "Buffer too small" msgstr "" +#: ports/espressif/common-hal/qspibus/QSPIBus.c +msgid "Bus in display transaction" +msgstr "" + #: ports/atmel-samd/common-hal/paralleldisplaybus/ParallelBus.c #: ports/espressif/common-hal/paralleldisplaybus/ParallelBus.c #: ports/nordic/common-hal/paralleldisplaybus/ParallelBus.c @@ -865,6 +874,7 @@ msgid "Coordinate arrays types have different sizes" msgstr "" #: shared-module/usb/core/Device.c +#: ports/espressif/common-hal/qspibus/QSPIBus.c msgid "Could not allocate DMA capable buffer" msgstr "" @@ -967,6 +977,7 @@ msgstr "" #: ports/espressif/common-hal/busio/SPI.c #: ports/espressif/common-hal/canio/CAN.c +#: ports/espressif/common-hal/qspibus/QSPIBus.c msgid "ESP-IDF memory allocation failed" msgstr "" @@ -1517,6 +1528,7 @@ msgstr "" #: ports/stm/common-hal/busio/UART.c shared-bindings/fourwire/FourWire.c #: shared-bindings/i2cdisplaybus/I2CDisplayBus.c #: shared-bindings/paralleldisplaybus/ParallelBus.c +#: shared-bindings/qspibus/QSPIBus.c #: shared-module/bitbangio/SPI.c msgid "No %q pin" msgstr "" @@ -1599,6 +1611,10 @@ msgstr "" msgid "No out in program" msgstr "" +#: ports/espressif/common-hal/qspibus/QSPIBus.c +msgid "No pending command" +msgstr "" + #: ports/atmel-samd/common-hal/busio/I2C.c #: ports/espressif/common-hal/busio/I2C.c #: ports/mimxrt10xx/common-hal/busio/I2C.c ports/nordic/common-hal/busio/I2C.c @@ -1746,6 +1762,7 @@ msgid "Operation or feature not supported" msgstr "" #: ports/espressif/common-hal/espidf/__init__.c +#: ports/espressif/common-hal/qspibus/QSPIBus.c msgid "Operation timed out" msgstr "" diff --git a/ports/espressif/Makefile b/ports/espressif/Makefile index 9e331740dd6a7..2545b33d22a39 100644 --- a/ports/espressif/Makefile +++ b/ports/espressif/Makefile @@ -780,6 +780,9 @@ endif ifneq ($(CIRCUITPY_PARALLELDISPLAYBUS),0) ESP_IDF_COMPONENTS_LINK += esp_lcd endif +ifneq ($(CIRCUITPY_QSPIBUS),0) + ESP_IDF_COMPONENTS_LINK += esp_lcd +endif ifneq ($(CIRCUITPY_USB_DEVICE),0) ESP_IDF_COMPONENTS_LINK += usb endif diff --git a/ports/espressif/boards/waveshare_esp32_s3_amoled_241/board.c b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/board.c new file mode 100644 index 0000000000000..3941e89a5e4d7 --- /dev/null +++ b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/board.c @@ -0,0 +1,8 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT + +#include "supervisor/board.h" + +void board_init(void) { +} diff --git a/ports/espressif/boards/waveshare_esp32_s3_amoled_241/mpconfigboard.h b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/mpconfigboard.h new file mode 100644 index 0000000000000..5c1aec08d96f9 --- /dev/null +++ b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/mpconfigboard.h @@ -0,0 +1,41 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT + +#pragma once + +#define MICROPY_HW_BOARD_NAME "Waveshare ESP32-S3-Touch-AMOLED-2.41" +#define MICROPY_HW_MCU_NAME "ESP32S3" + +// USB identifiers +#define USB_VID 0x303A +#define USB_PID 0x8278 +#define USB_MANUFACTURER "Waveshare" +#define USB_PRODUCT "ESP32-S3-Touch-AMOLED-2.41" + +// I2C bus - Disabled on boot to avoid conflicts. User must manually initialize I2C. +#define CIRCUITPY_BOARD_I2C (0) +#define CIRCUITPY_BOARD_I2C_PIN {{.scl = &pin_GPIO48, .sda = &pin_GPIO47}} + +// QSPI display refresh buffer: 2048 uint32_t words = 8KB on stack. +// ESP32-S3 main task stack is 24KB; verified safe with this board. +#define CIRCUITPY_QSPI_DISPLAY_AREA_BUFFER_SIZE (2048) + +// AMOLED Display (displayio + qspibus path) +#define CIRCUITPY_BOARD_DISPLAY (0) +#define CIRCUITPY_LCD_CS (&pin_GPIO9) +#define CIRCUITPY_LCD_CLK (&pin_GPIO10) +#define CIRCUITPY_LCD_D0 (&pin_GPIO11) +#define CIRCUITPY_LCD_D1 (&pin_GPIO12) +#define CIRCUITPY_LCD_D2 (&pin_GPIO13) +#define CIRCUITPY_LCD_D3 (&pin_GPIO14) +#define CIRCUITPY_LCD_RESET (&pin_GPIO21) +#define CIRCUITPY_LCD_POWER (&pin_GPIO16) +#define CIRCUITPY_LCD_POWER_ON_LEVEL (1) // GPIO level: 1=high, 0=low + +// No default SPI bus — SD card uses SDIO, display uses QSPI. +#define CIRCUITPY_BOARD_SPI (0) + +// Default UART bus +#define DEFAULT_UART_BUS_RX (&pin_GPIO44) +#define DEFAULT_UART_BUS_TX (&pin_GPIO43) diff --git a/ports/espressif/boards/waveshare_esp32_s3_amoled_241/mpconfigboard.mk b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/mpconfigboard.mk new file mode 100644 index 0000000000000..17d5300100174 --- /dev/null +++ b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/mpconfigboard.mk @@ -0,0 +1,38 @@ +# This file is part of the CircuitPython project: https://circuitpython.org +# SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +# SPDX-License-Identifier: MIT + +CIRCUITPY_CREATOR_ID = 0x57415645 # 'WAVE' (Waveshare) +CIRCUITPY_CREATION_ID = 0x41323431 # 'A241' (AMOLED 2.41) + +USB_VID = 0x303A +USB_PID = 0x8278 +USB_MANUFACTURER = "Waveshare" +USB_PRODUCT = "ESP32-S3-Touch-AMOLED-2.41" + +IDF_TARGET = esp32s3 + +# Flash configuration - 16MB QSPI Flash +CIRCUITPY_ESP_FLASH_SIZE = 16MB +CIRCUITPY_ESP_FLASH_MODE = qio +CIRCUITPY_ESP_FLASH_FREQ = 80m + +# PSRAM configuration - 8MB Octal PSRAM +CIRCUITPY_ESP_PSRAM_SIZE = 8MB +CIRCUITPY_ESP_PSRAM_MODE = opi +CIRCUITPY_ESP_PSRAM_FREQ = 80m + +OPTIMIZATION_FLAGS = -Os + +# QSPI bus for RM690B0 AMOLED display +CIRCUITPY_QSPIBUS = 1 +CIRCUITPY_PARALLELDISPLAYBUS = 0 + +# No camera on this board +CIRCUITPY_ESPCAMERA = 0 + +# Capacitive touch not available; board uses I2C touch controller +CIRCUITPY_TOUCHIO = 0 + +# SD card via SDMMC interface +CIRCUITPY_SDIOIO = 1 diff --git a/ports/espressif/boards/waveshare_esp32_s3_amoled_241/pins.c b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/pins.c new file mode 100644 index 0000000000000..ed03341923326 --- /dev/null +++ b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/pins.c @@ -0,0 +1,100 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT + +#include "py/obj.h" +#include "py/mphal.h" +#include "shared-bindings/board/__init__.h" +#include "shared-bindings/microcontroller/Pin.h" + +static const mp_rom_map_elem_t board_module_globals_table[] = { + CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS + + // ================================================================= + // ONBOARD PERIPHERALS - Functional Names + // ================================================================= + + // Boot/Control/Battery/Display Power + // NOTE: GPIO16 is shared between battery control circuitry and LCD power + // (see CIRCUITPY_QSPIBUS_PANEL_POWER_PIN in mpconfigboard.h). + { MP_ROM_QSTR(MP_QSTR_BOOT), MP_ROM_PTR(&pin_GPIO0) }, + { MP_ROM_QSTR(MP_QSTR_KEY_BAT), MP_ROM_PTR(&pin_GPIO15) }, + { MP_ROM_QSTR(MP_QSTR_BAT_CONTROL), MP_ROM_PTR(&pin_GPIO16) }, + { MP_ROM_QSTR(MP_QSTR_LCD_POWER), MP_ROM_PTR(&pin_GPIO16) }, + { MP_ROM_QSTR(MP_QSTR_BAT_ADC), MP_ROM_PTR(&pin_GPIO17) }, + + // I2C Bus (shared by Touch, RTC, IMU, IO Expander) + // NOTE: board.I2C auto-initialization is disabled (CIRCUITPY_BOARD_I2C=0) + // to avoid boot conflicts. Users must manually create I2C bus: + // i2c = busio.I2C(board.SCL, board.SDA) + { MP_ROM_QSTR(MP_QSTR_SDA), MP_ROM_PTR(&pin_GPIO47) }, + { MP_ROM_QSTR(MP_QSTR_SCL), MP_ROM_PTR(&pin_GPIO48) }, + + // Touch Panel (FT6336U on I2C) + { MP_ROM_QSTR(MP_QSTR_TP_SDA), MP_ROM_PTR(&pin_GPIO47) }, + { MP_ROM_QSTR(MP_QSTR_TP_SCL), MP_ROM_PTR(&pin_GPIO48) }, + { MP_ROM_QSTR(MP_QSTR_TP_RESET), MP_ROM_PTR(&pin_GPIO3) }, + + // RTC (PCF85063 on I2C) + { MP_ROM_QSTR(MP_QSTR_RTC_SDA), MP_ROM_PTR(&pin_GPIO47) }, + { MP_ROM_QSTR(MP_QSTR_RTC_SCL), MP_ROM_PTR(&pin_GPIO48) }, + + // IMU (QMI8658 on I2C) + { MP_ROM_QSTR(MP_QSTR_IMU_SDA), MP_ROM_PTR(&pin_GPIO47) }, + { MP_ROM_QSTR(MP_QSTR_IMU_SCL), MP_ROM_PTR(&pin_GPIO48) }, + + // I/O Expander (TCA9554 on I2C) + { MP_ROM_QSTR(MP_QSTR_EXIO_SDA), MP_ROM_PTR(&pin_GPIO47) }, + { MP_ROM_QSTR(MP_QSTR_EXIO_SCL), MP_ROM_PTR(&pin_GPIO48) }, + + // USB + { MP_ROM_QSTR(MP_QSTR_USB_D_N), MP_ROM_PTR(&pin_GPIO19) }, + { MP_ROM_QSTR(MP_QSTR_USB_D_P), MP_ROM_PTR(&pin_GPIO20) }, + + // UART + { MP_ROM_QSTR(MP_QSTR_TX), MP_ROM_PTR(&pin_GPIO43) }, + { MP_ROM_QSTR(MP_QSTR_RX), MP_ROM_PTR(&pin_GPIO44) }, + + // QSPI Display (RM690B0) - canonical generic LCD aliases. + { MP_ROM_QSTR(MP_QSTR_LCD_CS), MP_ROM_PTR(&pin_GPIO9) }, + { MP_ROM_QSTR(MP_QSTR_LCD_CLK), MP_ROM_PTR(&pin_GPIO10) }, + { MP_ROM_QSTR(MP_QSTR_LCD_D0), MP_ROM_PTR(&pin_GPIO11) }, + { MP_ROM_QSTR(MP_QSTR_LCD_D1), MP_ROM_PTR(&pin_GPIO12) }, + { MP_ROM_QSTR(MP_QSTR_LCD_D2), MP_ROM_PTR(&pin_GPIO13) }, + { MP_ROM_QSTR(MP_QSTR_LCD_D3), MP_ROM_PTR(&pin_GPIO14) }, + { MP_ROM_QSTR(MP_QSTR_LCD_RESET), MP_ROM_PTR(&pin_GPIO21) }, + + // Display Aliases + { MP_ROM_QSTR(MP_QSTR_DISPLAY_CS), MP_ROM_PTR(&pin_GPIO9) }, + { MP_ROM_QSTR(MP_QSTR_DISPLAY_SCK), MP_ROM_PTR(&pin_GPIO10) }, + { MP_ROM_QSTR(MP_QSTR_DISPLAY_D0), MP_ROM_PTR(&pin_GPIO11) }, + { MP_ROM_QSTR(MP_QSTR_DISPLAY_D1), MP_ROM_PTR(&pin_GPIO12) }, + { MP_ROM_QSTR(MP_QSTR_DISPLAY_D2), MP_ROM_PTR(&pin_GPIO13) }, + { MP_ROM_QSTR(MP_QSTR_DISPLAY_D3), MP_ROM_PTR(&pin_GPIO14) }, + { MP_ROM_QSTR(MP_QSTR_DISPLAY_RST), MP_ROM_PTR(&pin_GPIO21) }, + + // SD Card (SDIO / SDMMC) + { MP_ROM_QSTR(MP_QSTR_SDIO_CLK), MP_ROM_PTR(&pin_GPIO4) }, + { MP_ROM_QSTR(MP_QSTR_SDIO_CMD), MP_ROM_PTR(&pin_GPIO5) }, + { MP_ROM_QSTR(MP_QSTR_SDIO_D0), MP_ROM_PTR(&pin_GPIO6) }, + { MP_ROM_QSTR(MP_QSTR_SDIO_D3), MP_ROM_PTR(&pin_GPIO2) }, + + // ================================================================= + // GENERAL PURPOSE I/O (IOxx - Espressif Convention) + // ================================================================= + // Only pins NOT dedicated to onboard peripherals are exposed here. + // Use functional names above for dedicated pins (e.g., SDA, SD_CS). + + { MP_ROM_QSTR(MP_QSTR_IO0), MP_ROM_PTR(&pin_GPIO0) }, // BOOT button (available when not holding BOOT) + { MP_ROM_QSTR(MP_QSTR_IO1), MP_ROM_PTR(&pin_GPIO1) }, // Available + { MP_ROM_QSTR(MP_QSTR_IO3), MP_ROM_PTR(&pin_GPIO3) }, // TP_RESET (available if touch not used) + { MP_ROM_QSTR(MP_QSTR_IO7), MP_ROM_PTR(&pin_GPIO7) }, // Available + { MP_ROM_QSTR(MP_QSTR_IO8), MP_ROM_PTR(&pin_GPIO8) }, // Available + { MP_ROM_QSTR(MP_QSTR_IO18), MP_ROM_PTR(&pin_GPIO18) }, // Available + { MP_ROM_QSTR(MP_QSTR_IO40), MP_ROM_PTR(&pin_GPIO40) }, // Available + { MP_ROM_QSTR(MP_QSTR_IO41), MP_ROM_PTR(&pin_GPIO41) }, // Available + { MP_ROM_QSTR(MP_QSTR_IO42), MP_ROM_PTR(&pin_GPIO42) }, // Available + { MP_ROM_QSTR(MP_QSTR_IO45), MP_ROM_PTR(&pin_GPIO45) }, // Available + { MP_ROM_QSTR(MP_QSTR_IO46), MP_ROM_PTR(&pin_GPIO46) }, // Available +}; +MP_DEFINE_CONST_DICT(board_module_globals, board_module_globals_table); diff --git a/ports/espressif/boards/waveshare_esp32_s3_amoled_241/sdkconfig b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/sdkconfig new file mode 100644 index 0000000000000..3ef898aed3e46 --- /dev/null +++ b/ports/espressif/boards/waveshare_esp32_s3_amoled_241/sdkconfig @@ -0,0 +1,33 @@ +# +# Configuration file for the Waveshare ESP32-S3 Touch AMOLED 2.41 +# + +# PSRAM Configuration +CONFIG_SPIRAM=y +CONFIG_SPIRAM_BOOT_INIT=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y +CONFIG_SPIRAM_RODATA=y +CONFIG_SPIRAM_SPEED_80M=y +CONFIG_SPIRAM_MEMTEST=y +CONFIG_SPIRAM_CLK_IO=39 +CONFIG_SPIRAM_CS_IO=38 + +# Performance and Cache +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP32S3_DATA_CACHE_LINE_64B=y + +# Default flash settings for this board +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="esp-idf-config/partitions-16MB.csv" +CONFIG_PARTITION_TABLE_FILENAME="esp-idf-config/partitions-16MB.csv" + +# Networking +CONFIG_LWIP_LOCAL_HOSTNAME="waveshare-esp32-s3-amoled" + +# Disable USB-Serial/JTAG console - CircuitPython uses TinyUSB (USB OTG) for REPL +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=n + +# Enable .app_desc structure +CONFIG_APP_RETRIEVE_LEN_ELF_SHA=16 diff --git a/ports/espressif/common-hal/qspibus/QSPIBus.c b/ports/espressif/common-hal/qspibus/QSPIBus.c new file mode 100644 index 0000000000000..91f16d233deac --- /dev/null +++ b/ports/espressif/common-hal/qspibus/QSPIBus.c @@ -0,0 +1,586 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT + +#include "shared-bindings/qspibus/QSPIBus.h" + +#include "common-hal/microcontroller/Pin.h" +#include "shared-bindings/microcontroller/Pin.h" +#include "shared-bindings/util.h" + +#include "py/gc.h" +#include "py/runtime.h" + +#include "driver/gpio.h" +#include "esp_heap_caps.h" +#include "soc/soc_caps.h" +#include + +#define QSPI_OPCODE_WRITE_CMD (0x02U) +#define QSPI_OPCODE_WRITE_COLOR (0x32U) +#define LCD_CMD_RAMWR (0x2CU) +#define LCD_CMD_RAMWRC (0x3CU) +#define LCD_CMD_DISPOFF (0x28U) +#define LCD_CMD_SLPIN (0x10U) +#define QSPI_DMA_BUFFER_COUNT (2U) +#define QSPI_DMA_BUFFER_SIZE (16U * 1024U) +#define QSPI_COLOR_TIMEOUT_MS (1000U) +#if defined(CIRCUITPY_LCD_POWER) +#define CIRCUITPY_QSPIBUS_PANEL_POWER_PIN CIRCUITPY_LCD_POWER +#endif + +#ifndef CIRCUITPY_LCD_POWER_ON_LEVEL +#define CIRCUITPY_LCD_POWER_ON_LEVEL (1) +#endif + +static void qspibus_release_dma_buffers(qspibus_qspibus_obj_t *self) { + for (size_t i = 0; i < QSPI_DMA_BUFFER_COUNT; i++) { + if (self->dma_buffer[i] != NULL) { + heap_caps_free(self->dma_buffer[i]); + self->dma_buffer[i] = NULL; + } + } + self->dma_buffer_size = 0; + self->active_buffer = 0; + self->inflight_transfers = 0; + self->transfer_in_progress = false; +} + +static bool qspibus_allocate_dma_buffers(qspibus_qspibus_obj_t *self) { + const size_t candidates[] = { + QSPI_DMA_BUFFER_SIZE, + QSPI_DMA_BUFFER_SIZE / 2, + QSPI_DMA_BUFFER_SIZE / 4, + }; + + for (size_t c = 0; c < MP_ARRAY_SIZE(candidates); c++) { + size_t size = candidates[c]; + bool ok = true; + for (size_t i = 0; i < QSPI_DMA_BUFFER_COUNT; i++) { + self->dma_buffer[i] = heap_caps_malloc(size, MALLOC_CAP_DMA | MALLOC_CAP_8BIT); + if (self->dma_buffer[i] == NULL) { + ok = false; + break; + } + } + if (ok) { + self->dma_buffer_size = size; + self->active_buffer = 0; + self->inflight_transfers = 0; + self->transfer_in_progress = false; + return true; + } + qspibus_release_dma_buffers(self); + } + return false; +} + +// Reset transfer bookkeeping after timeout/error. Drains any stale semaphore +// tokens that late ISR completions may have posted after the timeout expired. +static void qspibus_reset_transfer_state(qspibus_qspibus_obj_t *self) { + self->inflight_transfers = 0; + self->transfer_in_progress = false; + if (self->transfer_done_sem != NULL) { + while (xSemaphoreTake(self->transfer_done_sem, 0) == pdTRUE) { + } + } +} + +static bool qspibus_wait_one_transfer_done(qspibus_qspibus_obj_t *self, TickType_t timeout) { + if (self->inflight_transfers == 0) { + self->transfer_in_progress = false; + return true; + } + + if (xSemaphoreTake(self->transfer_done_sem, timeout) != pdTRUE) { + return false; + } + self->inflight_transfers--; + self->transfer_in_progress = (self->inflight_transfers > 0); + return true; +} + +static bool qspibus_wait_all_transfers_done(qspibus_qspibus_obj_t *self, TickType_t timeout) { + while (self->inflight_transfers > 0) { + if (!qspibus_wait_one_transfer_done(self, timeout)) { + return false; + } + } + return true; +} + +static void qspibus_send_command_bytes( + qspibus_qspibus_obj_t *self, + uint8_t command, + const uint8_t *data, + size_t len) { + + if (!self->bus_initialized) { + raise_deinited_error(); + } + if (self->inflight_transfers >= QSPI_DMA_BUFFER_COUNT) { + if (!qspibus_wait_one_transfer_done(self, pdMS_TO_TICKS(QSPI_COLOR_TIMEOUT_MS))) { + qspibus_reset_transfer_state(self); + mp_raise_OSError_msg(MP_ERROR_TEXT("Operation timed out")); + } + } + + uint32_t packed_cmd = ((uint32_t)QSPI_OPCODE_WRITE_CMD << 24) | ((uint32_t)command << 8); + esp_err_t err = esp_lcd_panel_io_tx_param(self->io_handle, packed_cmd, data, len); + if (err != ESP_OK) { + qspibus_reset_transfer_state(self); + mp_raise_OSError_msg_varg(MP_ERROR_TEXT("%q failure: %d"), MP_QSTR_QSPI, (int)err); + } +} + +static void qspibus_send_color_bytes( + qspibus_qspibus_obj_t *self, + uint8_t command, + const uint8_t *data, + size_t len) { + + if (!self->bus_initialized) { + raise_deinited_error(); + } + + if (len == 0) { + qspibus_send_command_bytes(self, command, NULL, 0); + return; + } + if (self->dma_buffer_size == 0) { + mp_raise_OSError_msg(MP_ERROR_TEXT("Could not allocate DMA capable buffer")); + } + + // RAMWR must transition to RAMWRC for continued payload chunks. + uint8_t chunk_command = command; + const uint8_t *cursor = data; + size_t remaining = len; + + while (remaining > 0) { + // inflight_transfers is only modified in task context (never from ISR), + // so no atomic/critical section is needed. The ISR only signals the + // counting semaphore; all counter bookkeeping happens task-side. + if (self->inflight_transfers >= QSPI_DMA_BUFFER_COUNT) { + if (!qspibus_wait_one_transfer_done(self, pdMS_TO_TICKS(QSPI_COLOR_TIMEOUT_MS))) { + qspibus_reset_transfer_state(self); + mp_raise_OSError_msg(MP_ERROR_TEXT("Operation timed out")); + } + } + + size_t chunk = remaining; + if (chunk > self->dma_buffer_size) { + chunk = self->dma_buffer_size; + } + + uint8_t *buffer = self->dma_buffer[self->active_buffer]; + memcpy(buffer, cursor, chunk); + + uint32_t packed_cmd = ((uint32_t)QSPI_OPCODE_WRITE_COLOR << 24) | ((uint32_t)chunk_command << 8); + esp_err_t err = esp_lcd_panel_io_tx_color(self->io_handle, packed_cmd, buffer, chunk); + if (err != ESP_OK) { + qspibus_reset_transfer_state(self); + mp_raise_OSError_msg_varg(MP_ERROR_TEXT("%q failure: %d"), MP_QSTR_QSPI, (int)err); + } + + self->inflight_transfers++; + self->transfer_in_progress = true; + self->active_buffer = (self->active_buffer + 1) % QSPI_DMA_BUFFER_COUNT; + + if (chunk_command == LCD_CMD_RAMWR) { + chunk_command = LCD_CMD_RAMWRC; + } + + cursor += chunk; + remaining -= chunk; + } + + // Let DMA complete asynchronously. The next begin_transaction() will + // wait for all in-flight transfers, allowing fill_area() computation + // to overlap with DMA. The explicit wait is only needed for the + // Python write_data() API path where callers expect the transfer to + // be finished on return. +} + +static bool qspibus_is_color_payload_command(uint8_t command) { + return command == LCD_CMD_RAMWR || command == LCD_CMD_RAMWRC; +} + +static void qspibus_panel_sleep_best_effort(qspibus_qspibus_obj_t *self) { + if (!self->bus_initialized || self->io_handle == NULL) { + return; + } + + if (!qspibus_wait_all_transfers_done(self, pdMS_TO_TICKS(QSPI_COLOR_TIMEOUT_MS))) { + qspibus_reset_transfer_state(self); + } + + // If a command is buffered, flush it first so the panel state machine + // doesn't get a truncated transaction before sleep. + if (self->has_pending_command) { + uint32_t pending = ((uint32_t)QSPI_OPCODE_WRITE_CMD << 24) | ((uint32_t)self->pending_command << 8); + (void)esp_lcd_panel_io_tx_param(self->io_handle, pending, NULL, 0); + self->has_pending_command = false; + } + + uint32_t disp_off = ((uint32_t)QSPI_OPCODE_WRITE_CMD << 24) | ((uint32_t)LCD_CMD_DISPOFF << 8); + (void)esp_lcd_panel_io_tx_param(self->io_handle, disp_off, NULL, 0); + vTaskDelay(pdMS_TO_TICKS(20)); + + uint32_t sleep_in = ((uint32_t)QSPI_OPCODE_WRITE_CMD << 24) | ((uint32_t)LCD_CMD_SLPIN << 8); + (void)esp_lcd_panel_io_tx_param(self->io_handle, sleep_in, NULL, 0); + vTaskDelay(pdMS_TO_TICKS(120)); +} + +static bool IRAM_ATTR qspibus_on_color_trans_done( + esp_lcd_panel_io_handle_t io_handle, + esp_lcd_panel_io_event_data_t *event_data, + void *user_ctx) { + (void)io_handle; + (void)event_data; + + qspibus_qspibus_obj_t *self = (qspibus_qspibus_obj_t *)user_ctx; + if (self->transfer_done_sem == NULL) { + return false; + } + BaseType_t x_higher_priority_task_woken = pdFALSE; + + xSemaphoreGiveFromISR(self->transfer_done_sem, &x_higher_priority_task_woken); + return x_higher_priority_task_woken == pdTRUE; +} + +void common_hal_qspibus_qspibus_construct( + qspibus_qspibus_obj_t *self, + const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *data0, + const mcu_pin_obj_t *data1, + const mcu_pin_obj_t *data2, + const mcu_pin_obj_t *data3, + const mcu_pin_obj_t *cs, + const mcu_pin_obj_t *dcx, + const mcu_pin_obj_t *reset, + uint32_t frequency) { + + self->io_handle = NULL; + self->host_id = SPI2_HOST; + self->clock_pin = clock->number; + self->data0_pin = data0->number; + self->data1_pin = data1->number; + self->data2_pin = data2->number; + self->data3_pin = data3->number; + self->cs_pin = cs->number; + self->dcx_pin = (dcx != NULL) ? dcx->number : -1; + self->reset_pin = (reset != NULL) ? reset->number : -1; + self->power_pin = -1; + self->frequency = frequency; + self->bus_initialized = false; + self->in_transaction = false; + self->has_pending_command = false; + self->pending_command = 0; + self->transfer_in_progress = false; + self->active_buffer = 0; + self->inflight_transfers = 0; + self->dma_buffer_size = 0; + self->dma_buffer[0] = NULL; + self->dma_buffer[1] = NULL; + self->transfer_done_sem = NULL; + + self->transfer_done_sem = xSemaphoreCreateCounting(QSPI_DMA_BUFFER_COUNT, 0); + if (self->transfer_done_sem == NULL) { + mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("ESP-IDF memory allocation failed")); + } + + if (!qspibus_allocate_dma_buffers(self)) { + vSemaphoreDelete(self->transfer_done_sem); + self->transfer_done_sem = NULL; + mp_raise_msg(&mp_type_MemoryError, MP_ERROR_TEXT("Could not allocate DMA capable buffer")); + } + + const spi_bus_config_t bus_config = { + .sclk_io_num = self->clock_pin, + .mosi_io_num = self->data0_pin, + .miso_io_num = self->data1_pin, + .quadwp_io_num = self->data2_pin, + .quadhd_io_num = self->data3_pin, + .max_transfer_sz = self->dma_buffer_size, + .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS, + }; + + esp_err_t err = spi_bus_initialize(self->host_id, &bus_config, SPI_DMA_CH_AUTO); + if (err != ESP_OK) { + qspibus_release_dma_buffers(self); + vSemaphoreDelete(self->transfer_done_sem); + self->transfer_done_sem = NULL; + mp_raise_OSError_msg_varg(MP_ERROR_TEXT("%q failure: %d"), MP_QSTR_SPI, (int)err); + } + + const esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = self->cs_pin, + .dc_gpio_num = -1, + .spi_mode = 0, + .pclk_hz = self->frequency, + .trans_queue_depth = QSPI_DMA_BUFFER_COUNT, + .on_color_trans_done = qspibus_on_color_trans_done, + .user_ctx = self, + .lcd_cmd_bits = 32, + .lcd_param_bits = 8, + .flags = { + .quad_mode = 1, + }, + }; + + err = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)self->host_id, &io_config, &self->io_handle); + if (err != ESP_OK) { + spi_bus_free(self->host_id); + qspibus_release_dma_buffers(self); + vSemaphoreDelete(self->transfer_done_sem); + self->transfer_done_sem = NULL; + mp_raise_OSError_msg_varg(MP_ERROR_TEXT("%q failure: %d"), MP_QSTR_QSPI, (int)err); + } + + claim_pin(clock); + claim_pin(data0); + claim_pin(data1); + claim_pin(data2); + claim_pin(data3); + claim_pin(cs); + if (dcx != NULL) { + claim_pin(dcx); + gpio_set_direction((gpio_num_t)self->dcx_pin, GPIO_MODE_OUTPUT); + gpio_set_level((gpio_num_t)self->dcx_pin, 1); + } + + #ifdef CIRCUITPY_QSPIBUS_PANEL_POWER_PIN + const mcu_pin_obj_t *power = CIRCUITPY_QSPIBUS_PANEL_POWER_PIN; + if (power != NULL) { + self->power_pin = power->number; + claim_pin(power); + gpio_set_direction((gpio_num_t)self->power_pin, GPIO_MODE_OUTPUT); + gpio_set_level((gpio_num_t)self->power_pin, CIRCUITPY_LCD_POWER_ON_LEVEL ? 1 : 0); + // Panel power rail needs extra settle time before reset/init commands. + vTaskDelay(pdMS_TO_TICKS(200)); + } + #endif + + if (reset != NULL) { + claim_pin(reset); + + gpio_set_direction((gpio_num_t)self->reset_pin, GPIO_MODE_OUTPUT); + gpio_set_level((gpio_num_t)self->reset_pin, 0); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level((gpio_num_t)self->reset_pin, 1); + vTaskDelay(pdMS_TO_TICKS(120)); + } + + self->bus_initialized = true; +} + +void common_hal_qspibus_qspibus_deinit(qspibus_qspibus_obj_t *self) { + if (!self->bus_initialized) { + qspibus_release_dma_buffers(self); + return; + } + + qspibus_panel_sleep_best_effort(self); + self->in_transaction = false; + + if (self->io_handle != NULL) { + esp_lcd_panel_io_del(self->io_handle); + self->io_handle = NULL; + } + + spi_bus_free(self->host_id); + + if (self->transfer_done_sem != NULL) { + // Set NULL before delete so late ISR callbacks (if any) see NULL and skip. + SemaphoreHandle_t sem = self->transfer_done_sem; + self->transfer_done_sem = NULL; + vSemaphoreDelete(sem); + } + + qspibus_release_dma_buffers(self); + + reset_pin_number(self->clock_pin); + reset_pin_number(self->data0_pin); + reset_pin_number(self->data1_pin); + reset_pin_number(self->data2_pin); + reset_pin_number(self->data3_pin); + reset_pin_number(self->cs_pin); + if (self->dcx_pin >= 0) { + reset_pin_number(self->dcx_pin); + } + if (self->power_pin >= 0) { + reset_pin_number(self->power_pin); + } + if (self->reset_pin >= 0) { + reset_pin_number(self->reset_pin); + } + + self->bus_initialized = false; + self->in_transaction = false; + self->has_pending_command = false; + self->pending_command = 0; + self->transfer_in_progress = false; + self->inflight_transfers = 0; +} + +bool common_hal_qspibus_qspibus_deinited(qspibus_qspibus_obj_t *self) { + return !self->bus_initialized; +} + +void common_hal_qspibus_qspibus_write_command( + qspibus_qspibus_obj_t *self, + uint8_t command) { + if (!self->bus_initialized) { + raise_deinited_error(); + } + if (self->in_transaction) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Bus in display transaction")); + } + + // If caller stages command-only operations repeatedly, flush the previous + // pending command as no-data before replacing it. + if (self->has_pending_command) { + qspibus_send_command_bytes(self, self->pending_command, NULL, 0); + } + + self->pending_command = command; + self->has_pending_command = true; +} + +void common_hal_qspibus_qspibus_write_data( + qspibus_qspibus_obj_t *self, + const uint8_t *data, + size_t len) { + if (!self->bus_initialized) { + raise_deinited_error(); + } + if (self->in_transaction) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Bus in display transaction")); + } + if (len == 0) { + if (self->has_pending_command) { + qspibus_send_command_bytes(self, self->pending_command, NULL, 0); + self->has_pending_command = false; + } + return; + } + if (!self->has_pending_command) { + mp_raise_ValueError(MP_ERROR_TEXT("No pending command")); + } + + if (qspibus_is_color_payload_command(self->pending_command)) { + qspibus_send_color_bytes(self, self->pending_command, data, len); + // Python API: wait for DMA to finish so callers see the transfer as + // complete on return. The internal displayio path skips this wait + // to allow fill_area/DMA overlap. + if (!qspibus_wait_all_transfers_done(self, pdMS_TO_TICKS(QSPI_COLOR_TIMEOUT_MS))) { + qspibus_reset_transfer_state(self); + mp_raise_OSError_msg(MP_ERROR_TEXT("Operation timed out")); + } + } else { + qspibus_send_command_bytes(self, self->pending_command, data, len); + } + self->has_pending_command = false; +} + +bool common_hal_qspibus_qspibus_reset(mp_obj_t obj) { + qspibus_qspibus_obj_t *self = MP_OBJ_TO_PTR(obj); + if (!self->bus_initialized || self->reset_pin < 0) { + return false; + } + + gpio_set_level((gpio_num_t)self->reset_pin, 0); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level((gpio_num_t)self->reset_pin, 1); + vTaskDelay(pdMS_TO_TICKS(120)); + return true; +} + +bool common_hal_qspibus_qspibus_bus_free(mp_obj_t obj) { + qspibus_qspibus_obj_t *self = MP_OBJ_TO_PTR(obj); + return self->bus_initialized && !self->in_transaction && !self->transfer_in_progress && !self->has_pending_command; +} + +bool common_hal_qspibus_qspibus_begin_transaction(mp_obj_t obj) { + qspibus_qspibus_obj_t *self = MP_OBJ_TO_PTR(obj); + if (!self->bus_initialized || self->in_transaction || self->has_pending_command) { + return false; + } + // Wait for any in-flight DMA to complete before starting a new + // transaction. This replaces the old non-blocking bus_free() check + // and enables fill_area()/DMA overlap: the CPU fills the next + // subrectangle while the previous DMA runs, and this wait only + // blocks when we actually need the bus for the next send. + if (self->transfer_in_progress) { + if (!qspibus_wait_all_transfers_done(self, pdMS_TO_TICKS(QSPI_COLOR_TIMEOUT_MS))) { + qspibus_reset_transfer_state(self); + return false; + } + } + self->in_transaction = true; + return true; +} + +void common_hal_qspibus_qspibus_send( + mp_obj_t obj, + display_byte_type_t data_type, + display_chip_select_behavior_t chip_select, + const uint8_t *data, + uint32_t data_length) { + qspibus_qspibus_obj_t *self = MP_OBJ_TO_PTR(obj); + (void)chip_select; + if (!self->bus_initialized) { + raise_deinited_error(); + } + if (!self->in_transaction) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Begin transaction first")); + } + + if (data_type == DISPLAY_COMMAND) { + for (uint32_t i = 0; i < data_length; i++) { + if (self->has_pending_command) { + qspibus_send_command_bytes(self, self->pending_command, NULL, 0); + } + self->pending_command = data[i]; + self->has_pending_command = true; + } + return; + } + + if (!self->has_pending_command) { + if (data_length == 0) { + // Zero-length data write after a no-data command is benign. + return; + } + mp_raise_ValueError(MP_ERROR_TEXT("No pending command")); + } + + if (data_length == 0) { + qspibus_send_command_bytes(self, self->pending_command, NULL, 0); + self->has_pending_command = false; + return; + } + + if (qspibus_is_color_payload_command(self->pending_command)) { + qspibus_send_color_bytes(self, self->pending_command, data, data_length); + } else { + qspibus_send_command_bytes(self, self->pending_command, data, data_length); + } + self->has_pending_command = false; +} + +void common_hal_qspibus_qspibus_end_transaction(mp_obj_t obj) { + qspibus_qspibus_obj_t *self = MP_OBJ_TO_PTR(obj); + if (!self->bus_initialized) { + return; + } + if (self->has_pending_command) { + qspibus_send_command_bytes(self, self->pending_command, NULL, 0); + self->has_pending_command = false; + } + self->in_transaction = false; +} + +void common_hal_qspibus_qspibus_collect_ptrs(mp_obj_t obj) { + (void)obj; +} diff --git a/ports/espressif/common-hal/qspibus/QSPIBus.h b/ports/espressif/common-hal/qspibus/QSPIBus.h new file mode 100644 index 0000000000000..caed3661cfd73 --- /dev/null +++ b/ports/espressif/common-hal/qspibus/QSPIBus.h @@ -0,0 +1,51 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include +#include + +#include "py/obj.h" + +#include "esp-idf/components/esp_lcd/include/esp_lcd_panel_io.h" +#include "driver/spi_master.h" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +typedef struct { + mp_obj_base_t base; + + // ESP LCD panel IO handle used for QSPI transactions. + esp_lcd_panel_io_handle_t io_handle; + + // SPI host (SPI2_HOST on ESP32-S3). + spi_host_device_t host_id; + + // Claimed GPIO numbers. + int8_t clock_pin; + int8_t data0_pin; + int8_t data1_pin; + int8_t data2_pin; + int8_t data3_pin; + int8_t cs_pin; + int8_t dcx_pin; // -1 when optional DCX line is not provided. + int8_t reset_pin; // -1 when reset line is not provided. + int8_t power_pin; // -1 when board has no explicit display power pin. + + uint32_t frequency; + bool bus_initialized; + bool in_transaction; + bool has_pending_command; + uint8_t pending_command; + bool transfer_in_progress; + uint8_t active_buffer; + uint8_t inflight_transfers; + size_t dma_buffer_size; + uint8_t *dma_buffer[2]; + + // Signaled from ISR when panel IO transfer completes. + SemaphoreHandle_t transfer_done_sem; +} qspibus_qspibus_obj_t; diff --git a/ports/espressif/common-hal/qspibus/__init__.c b/ports/espressif/common-hal/qspibus/__init__.c new file mode 100644 index 0000000000000..2f763b218cb43 --- /dev/null +++ b/ports/espressif/common-hal/qspibus/__init__.c @@ -0,0 +1,3 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT diff --git a/ports/espressif/common-hal/qspibus/__init__.h b/ports/espressif/common-hal/qspibus/__init__.h new file mode 100644 index 0000000000000..2f763b218cb43 --- /dev/null +++ b/ports/espressif/common-hal/qspibus/__init__.h @@ -0,0 +1,3 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index d8c95d18a79b9..ae7ec7da7b763 100644 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -228,6 +228,9 @@ endif ifeq ($(CIRCUITPY_FOURWIRE),1) SRC_PATTERNS += fourwire/% endif +ifeq ($(CIRCUITPY_QSPIBUS),1) +SRC_PATTERNS += qspibus/% +endif ifeq ($(CIRCUITPY_FRAMEBUFFERIO),1) SRC_PATTERNS += framebufferio/% endif @@ -552,6 +555,8 @@ SRC_COMMON_HAL_ALL = \ pulseio/__init__.c \ pwmio/PWMOut.c \ pwmio/__init__.c \ + qspibus/QSPIBus.c \ + qspibus/__init__.c \ rclcpy/__init__.c \ rclcpy/Node.c \ rclcpy/Publisher.c \ diff --git a/py/circuitpy_mpconfig.h b/py/circuitpy_mpconfig.h index f6310dfaf6855..1db12ac8ae9fd 100644 --- a/py/circuitpy_mpconfig.h +++ b/py/circuitpy_mpconfig.h @@ -399,6 +399,13 @@ typedef long mp_off_t; #define CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE (128) #endif +// QSPI display buffer size in uint32_t words for _refresh_area() VLA. +// Allocated on stack; boards should verify sufficient stack headroom. +// Default 512 words = 2KB. Override per-board for larger buffers. +#ifndef CIRCUITPY_QSPI_DISPLAY_AREA_BUFFER_SIZE +#define CIRCUITPY_QSPI_DISPLAY_AREA_BUFFER_SIZE (512) +#endif + #else #define CIRCUITPY_DISPLAY_LIMIT (0) #define CIRCUITPY_DISPLAY_AREA_BUFFER_SIZE (0) diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 56fc695742133..f0d1428faa1c3 100644 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -262,6 +262,10 @@ CFLAGS += -DCIRCUITPY_BUSDISPLAY=$(CIRCUITPY_BUSDISPLAY) CIRCUITPY_FOURWIRE ?= $(CIRCUITPY_DISPLAYIO) CFLAGS += -DCIRCUITPY_FOURWIRE=$(CIRCUITPY_FOURWIRE) +# QSPI bus protocol for quad-SPI displays (like RM690B0) +CIRCUITPY_QSPIBUS ?= 0 +CFLAGS += -DCIRCUITPY_QSPIBUS=$(CIRCUITPY_QSPIBUS) + CIRCUITPY_EPAPERDISPLAY ?= $(CIRCUITPY_DISPLAYIO) CFLAGS += -DCIRCUITPY_EPAPERDISPLAY=$(CIRCUITPY_EPAPERDISPLAY) diff --git a/shared-bindings/qspibus/QSPIBus.c b/shared-bindings/qspibus/QSPIBus.c new file mode 100644 index 0000000000000..cc4fdc91c0669 --- /dev/null +++ b/shared-bindings/qspibus/QSPIBus.c @@ -0,0 +1,201 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT + +#include + +#include "shared-bindings/qspibus/QSPIBus.h" + +#include "shared-bindings/microcontroller/Pin.h" +#include "shared-bindings/util.h" +#include "shared-module/displayio/__init__.h" + +#include "py/binary.h" +#include "py/obj.h" +#include "py/runtime.h" + +static void check_for_deinit(qspibus_qspibus_obj_t *self) { + if (common_hal_qspibus_qspibus_deinited(self)) { + raise_deinited_error(); + } +} + +//| class QSPIBus: +//| """QSPI bus for quad-SPI displays.""" +//| +//| def __init__( +//| self, +//| *, +//| clock: microcontroller.Pin, +//| data0: microcontroller.Pin, +//| data1: microcontroller.Pin, +//| data2: microcontroller.Pin, +//| data3: microcontroller.Pin, +//| cs: microcontroller.Pin, +//| dcx: Optional[microcontroller.Pin] = None, +//| reset: Optional[microcontroller.Pin] = None, +//| frequency: int = 80_000_000, +//| ) -> None: +//| """Create a QSPIBus object for quad-SPI display communication. +//| +//| :param ~microcontroller.Pin clock: QSPI clock pin +//| :param ~microcontroller.Pin data0: QSPI data line 0 +//| :param ~microcontroller.Pin data1: QSPI data line 1 +//| :param ~microcontroller.Pin data2: QSPI data line 2 +//| :param ~microcontroller.Pin data3: QSPI data line 3 +//| :param ~microcontroller.Pin cs: Chip select pin +//| :param ~microcontroller.Pin dcx: Optional data/command select pin. +//| Reserved for future hardware paths. Current ESP32-S3 implementation +//| uses encoded QSPI command words and does not require explicit DCX. +//| :param ~microcontroller.Pin reset: Optional reset pin +//| :param int frequency: Bus frequency in Hz (1-80MHz) +//| """ +//| ... +//| +static mp_obj_t qspibus_qspibus_make_new(const mp_obj_type_t *type, size_t n_args, + size_t n_kw, const mp_obj_t *all_args) { + + enum { ARG_clock, ARG_data0, ARG_data1, ARG_data2, ARG_data3, ARG_cs, ARG_dcx, ARG_reset, ARG_frequency }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_clock, MP_ARG_KW_ONLY | MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_data0, MP_ARG_KW_ONLY | MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_data1, MP_ARG_KW_ONLY | MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_data2, MP_ARG_KW_ONLY | MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_data3, MP_ARG_KW_ONLY | MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_cs, MP_ARG_KW_ONLY | MP_ARG_OBJ | MP_ARG_REQUIRED }, + { MP_QSTR_dcx, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_reset, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_frequency, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 80000000} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + const mcu_pin_obj_t *clock = validate_obj_is_free_pin(args[ARG_clock].u_obj, MP_QSTR_clock); + const mcu_pin_obj_t *data0 = validate_obj_is_free_pin(args[ARG_data0].u_obj, MP_QSTR_data0); + const mcu_pin_obj_t *data1 = validate_obj_is_free_pin(args[ARG_data1].u_obj, MP_QSTR_data1); + const mcu_pin_obj_t *data2 = validate_obj_is_free_pin(args[ARG_data2].u_obj, MP_QSTR_data2); + const mcu_pin_obj_t *data3 = validate_obj_is_free_pin(args[ARG_data3].u_obj, MP_QSTR_data3); + const mcu_pin_obj_t *cs = validate_obj_is_free_pin(args[ARG_cs].u_obj, MP_QSTR_cs); + const mcu_pin_obj_t *dcx = validate_obj_is_free_pin_or_none(args[ARG_dcx].u_obj, MP_QSTR_dcx); + const mcu_pin_obj_t *reset = validate_obj_is_free_pin_or_none(args[ARG_reset].u_obj, MP_QSTR_reset); + + uint32_t frequency = (uint32_t)mp_arg_validate_int_range(args[ARG_frequency].u_int, 1, 80000000, MP_QSTR_frequency); + + qspibus_qspibus_obj_t *self = &allocate_display_bus_or_raise()->qspi_bus; + self->base.type = &qspibus_qspibus_type; + common_hal_qspibus_qspibus_construct(self, clock, data0, data1, data2, data3, cs, dcx, reset, frequency); + + return MP_OBJ_FROM_PTR(self); +} + +//| def reset(self) -> None: +//| """Perform a hardware reset using the reset pin. +//| +//| :raises RuntimeError: if no reset pin was provided at construction. +//| """ +//| ... +//| +static mp_obj_t qspibus_qspibus_obj_reset(mp_obj_t self_in) { + qspibus_qspibus_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + if (!common_hal_qspibus_qspibus_reset(MP_OBJ_FROM_PTR(self))) { + mp_raise_RuntimeError_varg(MP_ERROR_TEXT("No %q pin"), MP_QSTR_reset); + } + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_1(qspibus_qspibus_reset_obj, qspibus_qspibus_obj_reset); + +//| def send(self, command: int, data: Optional[ReadableBuffer] = None) -> None: +//| """Send command with optional payload bytes. +//| +//| This mirrors FourWire-style convenience API: +//| - command byte is sent first +//| - optional payload bytes follow +//| """ +//| ... +//| +static mp_obj_t qspibus_qspibus_send(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_command, ARG_data }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_command, MP_ARG_INT | MP_ARG_REQUIRED }, + { MP_QSTR_data, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + }; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + qspibus_qspibus_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_for_deinit(self); + + uint8_t command = (uint8_t)mp_arg_validate_int_range(args[ARG_command].u_int, 0, 255, MP_QSTR_command); + + const uint8_t *data = NULL; + size_t len = 0; + mp_buffer_info_t data_bufinfo; + if (args[ARG_data].u_obj != mp_const_none) { + mp_get_buffer_raise(args[ARG_data].u_obj, &data_bufinfo, MP_BUFFER_READ); + data = (const uint8_t *)data_bufinfo.buf; + len = data_bufinfo.len; + } + + // Wait for display bus to be available, then acquire transaction. + // Mirrors FourWire.send() pattern: begin_transaction → send → end_transaction. + while (!common_hal_qspibus_qspibus_begin_transaction(MP_OBJ_FROM_PTR(self))) { + RUN_BACKGROUND_TASKS; + } + common_hal_qspibus_qspibus_send(MP_OBJ_FROM_PTR(self), DISPLAY_COMMAND, CHIP_SELECT_UNTOUCHED, &command, 1); + common_hal_qspibus_qspibus_send(MP_OBJ_FROM_PTR(self), DISPLAY_DATA, CHIP_SELECT_UNTOUCHED, data, len); + common_hal_qspibus_qspibus_end_transaction(MP_OBJ_FROM_PTR(self)); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_KW(qspibus_qspibus_send_obj, 1, qspibus_qspibus_send); + +//| def write_command(self, command: int) -> None: +//| """Stage a command byte for subsequent :py:meth:`write_data`. +//| +//| If a previously staged command had no data, it is sent as +//| a command-only transaction before staging the new one. +//| """ +//| ... +//| +static mp_obj_t qspibus_qspibus_write_command(mp_obj_t self_in, mp_obj_t command_obj) { + qspibus_qspibus_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + uint8_t command = (uint8_t)mp_arg_validate_int_range(mp_obj_get_int(command_obj), 0, 255, MP_QSTR_command); + common_hal_qspibus_qspibus_write_command(self, command); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(qspibus_qspibus_write_command_obj, qspibus_qspibus_write_command); + +//| def write_data(self, data: ReadableBuffer) -> None: +//| """Send payload bytes for the most recently staged command.""" +//| ... +//| +static mp_obj_t qspibus_qspibus_write_data(mp_obj_t self_in, mp_obj_t data_obj) { + qspibus_qspibus_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_for_deinit(self); + + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(data_obj, &bufinfo, MP_BUFFER_READ); + common_hal_qspibus_qspibus_write_data(self, (const uint8_t *)bufinfo.buf, bufinfo.len); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_2(qspibus_qspibus_write_data_obj, qspibus_qspibus_write_data); + +static const mp_rom_map_elem_t qspibus_qspibus_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_reset), MP_ROM_PTR(&qspibus_qspibus_reset_obj) }, + { MP_ROM_QSTR(MP_QSTR_send), MP_ROM_PTR(&qspibus_qspibus_send_obj) }, + { MP_ROM_QSTR(MP_QSTR_write_command), MP_ROM_PTR(&qspibus_qspibus_write_command_obj) }, + { MP_ROM_QSTR(MP_QSTR_write_data), MP_ROM_PTR(&qspibus_qspibus_write_data_obj) }, +}; +static MP_DEFINE_CONST_DICT(qspibus_qspibus_locals_dict, qspibus_qspibus_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + qspibus_qspibus_type, + MP_QSTR_QSPIBus, + MP_TYPE_FLAG_NONE, + make_new, qspibus_qspibus_make_new, + locals_dict, &qspibus_qspibus_locals_dict + ); diff --git a/shared-bindings/qspibus/QSPIBus.h b/shared-bindings/qspibus/QSPIBus.h new file mode 100644 index 0000000000000..a5d8f824ad28e --- /dev/null +++ b/shared-bindings/qspibus/QSPIBus.h @@ -0,0 +1,52 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT + +#pragma once + +#include +#include + +#include "py/obj.h" + +#include "shared-bindings/displayio/__init__.h" + +#include "common-hal/microcontroller/Pin.h" +#include "common-hal/qspibus/QSPIBus.h" + +extern const mp_obj_type_t qspibus_qspibus_type; + +void common_hal_qspibus_qspibus_construct( + qspibus_qspibus_obj_t *self, + const mcu_pin_obj_t *clock, + const mcu_pin_obj_t *data0, + const mcu_pin_obj_t *data1, + const mcu_pin_obj_t *data2, + const mcu_pin_obj_t *data3, + const mcu_pin_obj_t *cs, + const mcu_pin_obj_t *dcx, + const mcu_pin_obj_t *reset, + uint32_t frequency); + +void common_hal_qspibus_qspibus_deinit(qspibus_qspibus_obj_t *self); +bool common_hal_qspibus_qspibus_deinited(qspibus_qspibus_obj_t *self); + +void common_hal_qspibus_qspibus_write_command( + qspibus_qspibus_obj_t *self, + uint8_t command); +void common_hal_qspibus_qspibus_write_data( + qspibus_qspibus_obj_t *self, + const uint8_t *data, + size_t len); + +bool common_hal_qspibus_qspibus_reset(mp_obj_t obj); +bool common_hal_qspibus_qspibus_bus_free(mp_obj_t obj); +bool common_hal_qspibus_qspibus_begin_transaction(mp_obj_t obj); +void common_hal_qspibus_qspibus_send( + mp_obj_t obj, + display_byte_type_t data_type, + display_chip_select_behavior_t chip_select, + const uint8_t *data, + uint32_t data_length); +void common_hal_qspibus_qspibus_end_transaction(mp_obj_t obj); +void common_hal_qspibus_qspibus_collect_ptrs(mp_obj_t obj); diff --git a/shared-bindings/qspibus/__init__.c b/shared-bindings/qspibus/__init__.c new file mode 100644 index 0000000000000..0281c41c80420 --- /dev/null +++ b/shared-bindings/qspibus/__init__.c @@ -0,0 +1,50 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/qspibus/__init__.h" +#include "shared-bindings/qspibus/QSPIBus.h" + +//| """QSPI bus protocol for quad-SPI displays +//| +//| The `qspibus` module provides a low-level QSPI bus interface for displays +//| that use four data lines. It is analogous to `fourwire` for standard SPI. +//| +//| Use :class:`qspibus.QSPIBus` to create a bus instance. +//| +//| Example usage:: +//| +//| import board +//| import qspibus +//| import displayio +//| +//| displayio.release_displays() +//| +//| bus = qspibus.QSPIBus( +//| clock=board.LCD_CLK, +//| data0=board.LCD_D0, +//| data1=board.LCD_D1, +//| data2=board.LCD_D2, +//| data3=board.LCD_D3, +//| cs=board.LCD_CS, +//| reset=board.LCD_RESET, +//| frequency=80_000_000, +//| ) +//| """ + +static const mp_rom_map_elem_t qspibus_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_qspibus) }, + { MP_ROM_QSTR(MP_QSTR_QSPIBus), MP_ROM_PTR(&qspibus_qspibus_type) }, +}; + +static MP_DEFINE_CONST_DICT(qspibus_module_globals, qspibus_module_globals_table); + +const mp_obj_module_t qspibus_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&qspibus_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_qspibus, qspibus_module); diff --git a/shared-bindings/qspibus/__init__.h b/shared-bindings/qspibus/__init__.h new file mode 100644 index 0000000000000..9b4ca24332703 --- /dev/null +++ b/shared-bindings/qspibus/__init__.h @@ -0,0 +1,8 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// SPDX-FileCopyrightText: Copyright (c) 2026 Przemyslaw Patrick Socha +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" +#include "shared-bindings/qspibus/QSPIBus.h" diff --git a/shared-module/busdisplay/BusDisplay.c b/shared-module/busdisplay/BusDisplay.c index 6ef1c4c7f66ce..1994a25214c34 100644 --- a/shared-module/busdisplay/BusDisplay.c +++ b/shared-module/busdisplay/BusDisplay.c @@ -16,6 +16,9 @@ #if CIRCUITPY_PARALLELDISPLAYBUS #include "shared-bindings/paralleldisplaybus/ParallelBus.h" #endif +#if CIRCUITPY_QSPIBUS +#include "shared-bindings/qspibus/QSPIBus.h" +#endif #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/time/__init__.h" #include "shared-module/displayio/__init__.h" @@ -214,7 +217,7 @@ static void _send_pixels(busdisplay_busdisplay_obj_t *self, uint8_t *pixels, uin } static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_area_t *area) { - uint16_t buffer_size = 128; // In uint32_ts + uint32_t buffer_size = 128; // In uint32_ts displayio_area_t clipped; // Clip the area to the display by overlapping the areas. If there is no overlap then we're done. @@ -223,7 +226,7 @@ static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_are } uint16_t rows_per_buffer = displayio_area_height(&clipped); uint8_t pixels_per_word = (sizeof(uint32_t) * 8) / self->core.colorspace.depth; - uint16_t pixels_per_buffer = displayio_area_size(&clipped); + uint32_t pixels_per_buffer = displayio_area_size(&clipped); uint16_t subrectangles = 1; // for SH1107 and other boundary constrained controllers @@ -254,11 +257,63 @@ static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_are } } + #if CIRCUITPY_QSPIBUS + // QSPI panels benefit from larger sub-rectangle buffers because each chunk + // has non-trivial command/window overhead. Keep this path qspibus-specific + // to avoid increasing stack usage on other display buses. + // Guard: buffer is a VLA on stack; 2048 uint32_t words = 8KB is the safe + // upper bound for ESP32-S3's 24KB main task stack. + _Static_assert(CIRCUITPY_QSPI_DISPLAY_AREA_BUFFER_SIZE <= 2048, + "CIRCUITPY_QSPI_DISPLAY_AREA_BUFFER_SIZE exceeds safe stack limit (8KB)"); + bool is_qspi_bus = mp_obj_is_type(self->bus.bus, &qspibus_qspibus_type); + if (is_qspi_bus && + self->core.colorspace.depth == 16 && + !self->bus.data_as_commands && + !self->bus.SH1107_addressing && + buffer_size < CIRCUITPY_QSPI_DISPLAY_AREA_BUFFER_SIZE) { + buffer_size = CIRCUITPY_QSPI_DISPLAY_AREA_BUFFER_SIZE; + rows_per_buffer = buffer_size * pixels_per_word / displayio_area_width(&clipped); + if (rows_per_buffer == 0) { + rows_per_buffer = 1; + } + // Clamp to actual display height. + if (rows_per_buffer > displayio_area_height(&clipped)) { + rows_per_buffer = displayio_area_height(&clipped); + } + subrectangles = displayio_area_height(&clipped) / rows_per_buffer; + if (displayio_area_height(&clipped) % rows_per_buffer != 0) { + subrectangles++; + } + pixels_per_buffer = rows_per_buffer * displayio_area_width(&clipped); + buffer_size = pixels_per_buffer / pixels_per_word; + if (pixels_per_buffer % pixels_per_word) { + buffer_size += 1; + } + + // Ensure at least 2 rows per buffer when possible. + if (rows_per_buffer < 2 && + displayio_area_height(&clipped) > 1 && + (uint32_t)((2 * displayio_area_width(&clipped) + pixels_per_word - 1) / pixels_per_word) <= (uint32_t)CIRCUITPY_QSPI_DISPLAY_AREA_BUFFER_SIZE) { + rows_per_buffer = 2; + subrectangles = displayio_area_height(&clipped) / rows_per_buffer; + if (displayio_area_height(&clipped) % rows_per_buffer != 0) { + subrectangles++; + } + pixels_per_buffer = rows_per_buffer * displayio_area_width(&clipped); + buffer_size = pixels_per_buffer / pixels_per_word; + if (pixels_per_buffer % pixels_per_word) { + buffer_size += 1; + } + } + } + #endif + // Allocated and shared as a uint32_t array so the compiler knows the // alignment everywhere. - uint32_t buffer[buffer_size]; uint32_t mask_length = (pixels_per_buffer / 32) + 1; + uint32_t buffer[buffer_size]; uint32_t mask[mask_length]; + uint16_t remaining_rows = displayio_area_height(&clipped); for (uint16_t j = 0; j < subrectangles; j++) { @@ -273,28 +328,55 @@ static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_are } remaining_rows -= rows_per_buffer; - displayio_display_bus_set_region_to_update(&self->bus, &self->core, &subrectangle); + #if CIRCUITPY_QSPIBUS + if (is_qspi_bus) { + // QSPI path: fill_area first (overlaps with previous DMA), + // then single-transaction set_region + RAMWR + pixels. + // depth is always 16 here (guarded by is_qspi_bus check above). + uint32_t subrectangle_size_bytes = (uint32_t)displayio_area_size(&subrectangle) * (self->core.colorspace.depth / 8); - uint16_t subrectangle_size_bytes; - if (self->core.colorspace.depth >= 8) { - subrectangle_size_bytes = displayio_area_size(&subrectangle) * (self->core.colorspace.depth / 8); - } else { - subrectangle_size_bytes = displayio_area_size(&subrectangle) / (8 / self->core.colorspace.depth); - } + memset(mask, 0, mask_length * sizeof(mask[0])); + memset(buffer, 0, buffer_size * sizeof(buffer[0])); - memset(mask, 0, mask_length * sizeof(mask[0])); - memset(buffer, 0, buffer_size * sizeof(buffer[0])); + displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer); + + // begin_transaction waits for any prior async DMA to finish, + // so fill_area above overlaps with previous DMA. + if (!displayio_display_bus_begin_transaction(&self->bus)) { + // Transaction failed (bus deinitialized, timeout, etc.). + // Bail out to prevent calling send() outside a transaction. + return false; + } + displayio_display_bus_send_region_commands(&self->bus, &self->core, &subrectangle); + _send_pixels(self, (uint8_t *)buffer, subrectangle_size_bytes); + displayio_display_bus_end_transaction(&self->bus); + } else + #endif + { + // Non-QSPI path: original ordering preserved exactly. + displayio_display_bus_set_region_to_update(&self->bus, &self->core, &subrectangle); - displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer); + uint16_t subrectangle_size_bytes; + if (self->core.colorspace.depth >= 8) { + subrectangle_size_bytes = displayio_area_size(&subrectangle) * (self->core.colorspace.depth / 8); + } else { + subrectangle_size_bytes = displayio_area_size(&subrectangle) / (8 / self->core.colorspace.depth); + } - // Can't acquire display bus; skip the rest of the data. - if (!displayio_display_bus_is_free(&self->bus)) { - return false; - } + memset(mask, 0, mask_length * sizeof(mask[0])); + memset(buffer, 0, buffer_size * sizeof(buffer[0])); - displayio_display_bus_begin_transaction(&self->bus); - _send_pixels(self, (uint8_t *)buffer, subrectangle_size_bytes); - displayio_display_bus_end_transaction(&self->bus); + displayio_display_core_fill_area(&self->core, &subrectangle, mask, buffer); + + // Can't acquire display bus; skip the rest of the data. + if (!displayio_display_bus_is_free(&self->bus)) { + return false; + } + + displayio_display_bus_begin_transaction(&self->bus); + _send_pixels(self, (uint8_t *)buffer, subrectangle_size_bytes); + displayio_display_bus_end_transaction(&self->bus); + } // Run background tasks so they can run during an explicit refresh. // Auto-refresh won't run background tasks here because it is a background task itself. @@ -305,6 +387,21 @@ static bool _refresh_area(busdisplay_busdisplay_obj_t *self, const displayio_are usb_background(); #endif } + + #if CIRCUITPY_QSPIBUS + if (is_qspi_bus) { + // Drain the last async DMA transfer before returning. + // Within the loop, begin_transaction() waits for the PREVIOUS + // subrectangle's DMA, enabling fill_area/DMA overlap. But the + // LAST subrectangle's DMA is still in-flight when the loop ends. + // Without this drain, bus_free() returns false on the next + // refresh() call, causing it to be silently skipped. + if (displayio_display_bus_begin_transaction(&self->bus)) { + displayio_display_bus_end_transaction(&self->bus); + } + } + #endif + return true; } diff --git a/shared-module/displayio/__init__.c b/shared-module/displayio/__init__.c index e325a7857f464..ce90bf8f1c327 100644 --- a/shared-module/displayio/__init__.c +++ b/shared-module/displayio/__init__.c @@ -189,6 +189,10 @@ static void common_hal_displayio_release_displays_impl(bool keep_primary) { } else if (bus_type == &mipidsi_display_type) { common_hal_mipidsi_display_deinit(&display_buses[i].mipidsi); #endif + #if CIRCUITPY_QSPIBUS + } else if (bus_type == &qspibus_qspibus_type) { + common_hal_qspibus_qspibus_deinit(&display_buses[i].qspi_bus); + #endif } display_buses[i].bus_base.type = &mp_type_NoneType; } diff --git a/shared-module/displayio/__init__.h b/shared-module/displayio/__init__.h index f2f4691b313a7..0c6a881ef621c 100644 --- a/shared-module/displayio/__init__.h +++ b/shared-module/displayio/__init__.h @@ -41,6 +41,9 @@ #if CIRCUITPY_MIPIDSI #include "shared-bindings/mipidsi/Display.h" #endif +#if CIRCUITPY_QSPIBUS +#include "shared-bindings/qspibus/QSPIBus.h" +#endif // Port unique frame buffers. #if CIRCUITPY_VIDEOCORE #include "bindings/videocore/Framebuffer.h" @@ -87,6 +90,9 @@ typedef struct { #if CIRCUITPY_MIPIDSI mipidsi_display_obj_t mipidsi; #endif + #if CIRCUITPY_QSPIBUS + qspibus_qspibus_obj_t qspi_bus; + #endif }; } primary_display_bus_t; diff --git a/shared-module/displayio/bus_core.c b/shared-module/displayio/bus_core.c index e01b9d9eef685..5afa42f26fdf8 100644 --- a/shared-module/displayio/bus_core.c +++ b/shared-module/displayio/bus_core.c @@ -17,6 +17,9 @@ #if CIRCUITPY_PARALLELDISPLAYBUS #include "shared-bindings/paralleldisplaybus/ParallelBus.h" #endif +#if CIRCUITPY_QSPIBUS +#include "shared-bindings/qspibus/QSPIBus.h" +#endif #include "shared-bindings/microcontroller/Pin.h" #include "shared-bindings/time/__init__.h" #include "shared-module/displayio/__init__.h" @@ -79,6 +82,16 @@ void displayio_display_bus_construct(displayio_display_bus_t *self, self->collect_ptrs = common_hal_i2cdisplaybus_i2cdisplaybus_collect_ptrs; } else #endif + #if CIRCUITPY_QSPIBUS + if (mp_obj_is_type(bus, &qspibus_qspibus_type)) { + self->bus_reset = common_hal_qspibus_qspibus_reset; + self->bus_free = common_hal_qspibus_qspibus_bus_free; + self->begin_transaction = common_hal_qspibus_qspibus_begin_transaction; + self->send = common_hal_qspibus_qspibus_send; + self->end_transaction = common_hal_qspibus_qspibus_end_transaction; + self->collect_ptrs = common_hal_qspibus_qspibus_collect_ptrs; + } else + #endif { mp_raise_ValueError(MP_ERROR_TEXT("Unsupported display bus type")); } @@ -101,7 +114,7 @@ void displayio_display_bus_end_transaction(displayio_display_bus_t *self) { self->end_transaction(self->bus); } -void displayio_display_bus_set_region_to_update(displayio_display_bus_t *self, displayio_display_core_t *display, displayio_area_t *area) { +static void _displayio_display_bus_send_region_commands(displayio_display_bus_t *self, displayio_display_core_t *display, displayio_area_t *area, bool manage_transactions) { uint16_t x1 = area->x1 + self->colstart; uint16_t x2 = area->x2 + self->colstart; uint16_t y1 = area->y1 + self->rowstart; @@ -128,7 +141,9 @@ void displayio_display_bus_set_region_to_update(displayio_display_bus_t *self, d } // Set column. - displayio_display_bus_begin_transaction(self); + if (manage_transactions) { + displayio_display_bus_begin_transaction(self); + } uint8_t data[5]; data[0] = self->column_command; uint8_t data_length = 1; @@ -163,20 +178,28 @@ void displayio_display_bus_set_region_to_update(displayio_display_bus_t *self, d } self->send(self->bus, data_type, chip_select, data, data_length); - displayio_display_bus_end_transaction(self); + if (manage_transactions) { + displayio_display_bus_end_transaction(self); + } if (self->set_current_column_command != NO_COMMAND) { uint8_t command = self->set_current_column_command; - displayio_display_bus_begin_transaction(self); + if (manage_transactions) { + displayio_display_bus_begin_transaction(self); + } self->send(self->bus, DISPLAY_COMMAND, chip_select, &command, 1); // Only send the first half of data because it is the first coordinate. self->send(self->bus, DISPLAY_DATA, chip_select, data, data_length / 2); - displayio_display_bus_end_transaction(self); + if (manage_transactions) { + displayio_display_bus_end_transaction(self); + } } // Set row. - displayio_display_bus_begin_transaction(self); + if (manage_transactions) { + displayio_display_bus_begin_transaction(self); + } data[0] = self->row_command; data_length = 1; if (!self->data_as_commands) { @@ -207,18 +230,34 @@ void displayio_display_bus_set_region_to_update(displayio_display_bus_t *self, d } self->send(self->bus, data_type, chip_select, data, data_length); - displayio_display_bus_end_transaction(self); + if (manage_transactions) { + displayio_display_bus_end_transaction(self); + } if (self->set_current_row_command != NO_COMMAND) { uint8_t command = self->set_current_row_command; - displayio_display_bus_begin_transaction(self); + if (manage_transactions) { + displayio_display_bus_begin_transaction(self); + } self->send(self->bus, DISPLAY_COMMAND, chip_select, &command, 1); // Only send the first half of data because it is the first coordinate. self->send(self->bus, DISPLAY_DATA, chip_select, data, data_length / 2); - displayio_display_bus_end_transaction(self); + if (manage_transactions) { + displayio_display_bus_end_transaction(self); + } } } +void displayio_display_bus_set_region_to_update(displayio_display_bus_t *self, displayio_display_core_t *display, displayio_area_t *area) { + _displayio_display_bus_send_region_commands(self, display, area, true); +} + +#if CIRCUITPY_QSPIBUS +void displayio_display_bus_send_region_commands(displayio_display_bus_t *self, displayio_display_core_t *display, displayio_area_t *area) { + _displayio_display_bus_send_region_commands(self, display, area, false); +} +#endif + void displayio_display_bus_collect_ptrs(displayio_display_bus_t *self) { self->collect_ptrs(self->bus); } diff --git a/shared-module/displayio/bus_core.h b/shared-module/displayio/bus_core.h index 838454c92e6d8..bd7e51aeb4561 100644 --- a/shared-module/displayio/bus_core.h +++ b/shared-module/displayio/bus_core.h @@ -49,6 +49,12 @@ void displayio_display_bus_end_transaction(displayio_display_bus_t *self); void displayio_display_bus_set_region_to_update(displayio_display_bus_t *self, displayio_display_core_t *display, displayio_area_t *area); +#if CIRCUITPY_QSPIBUS +// Send column/row window commands within an already-open transaction. +// Caller must have called displayio_display_bus_begin_transaction() first. +void displayio_display_bus_send_region_commands(displayio_display_bus_t *self, displayio_display_core_t *display, displayio_area_t *area); +#endif + void release_display_bus(displayio_display_bus_t *self); void displayio_display_bus_collect_ptrs(displayio_display_bus_t *self);