From d0c8fbb93079f75da316e2135dc16f02d52ab00c Mon Sep 17 00:00:00 2001 From: vostraga Date: Tue, 9 Sep 2025 17:43:01 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=20=20feat:=20implement=20SPIBusFast=20driv?= =?UTF-8?q?er=20with=2090=C2=B0=20rotation=20for=20AXS15231B=20QSPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add SPIBusFast bus driver with partial updates support - Enable LVGL partial mode instead of full frame updates - Implement 90° rotation with pixel coordinate transformation - Add RTOS task for double buffering - Support QSPI 4-wire interface for AXS15231B controller --- ext_mod/lcd_bus/esp32_include/spi_bus_fast.h | 121 ++++ .../esp32_include/spi_bus_fast_rotation.h | 21 + ext_mod/lcd_bus/esp32_src/spi_bus_fast.c | 519 ++++++++++++++++++ .../lcd_bus/esp32_src/spi_bus_fast_rotation.c | 160 ++++++ ext_mod/lcd_bus/esp32_src/spi_bus_fast_task.c | 222 ++++++++ ext_mod/lcd_bus/micropython.cmake | 3 + ext_mod/lcd_bus/modlcd_bus.c | 15 + 7 files changed, 1061 insertions(+) create mode 100644 ext_mod/lcd_bus/esp32_include/spi_bus_fast.h create mode 100644 ext_mod/lcd_bus/esp32_include/spi_bus_fast_rotation.h create mode 100644 ext_mod/lcd_bus/esp32_src/spi_bus_fast.c create mode 100644 ext_mod/lcd_bus/esp32_src/spi_bus_fast_rotation.c create mode 100644 ext_mod/lcd_bus/esp32_src/spi_bus_fast_task.c diff --git a/ext_mod/lcd_bus/esp32_include/spi_bus_fast.h b/ext_mod/lcd_bus/esp32_include/spi_bus_fast.h new file mode 100644 index 00000000..4f053f53 --- /dev/null +++ b/ext_mod/lcd_bus/esp32_include/spi_bus_fast.h @@ -0,0 +1,121 @@ +// Copyright (c) 2024 - 2025 Kevin G. Schlosser +// Copyright (c) 2024 - 2025 Viktor Vorobjov + +#ifndef _ESP32_SPI_BUS_FAST_H_ + #define _ESP32_SPI_BUS_FAST_H_ + + //local_includes + #include "lcd_types.h" + #include "../../../micropy_updates/common/mp_spi_common.h" + + // esp-idf includes + #include "esp_lcd_panel_io.h" + #include "esp_heap_caps.h" + #include "driver/spi_common.h" + #include "driver/spi_master.h" + #include "hal/spi_types.h" + + #include "freertos/FreeRTOS.h" + #include "freertos/task.h" + #include "freertos/semphr.h" + #include "freertos/event_groups.h" + #include "freertos/idf_additions.h" + + // micropython includes + #include "mphalport.h" + #include "py/obj.h" + #include "py/objarray.h" + + typedef struct _spi_bus_fast_lock_t { + SemaphoreHandle_t handle; + StaticSemaphore_t buffer; + } spi_bus_fast_lock_t; + + typedef struct _spi_bus_fast_event_t { + EventGroupHandle_t handle; + StaticEventGroup_t buffer; + } spi_bus_fast_event_t; + + typedef struct _mp_lcd_spi_bus_fast_obj_t { + mp_obj_base_t base; + + mp_obj_t callback; + + mp_obj_array_t *view1; + mp_obj_array_t *view2; + + uint32_t buffer_flags; + + bool trans_done; + bool rgb565_byte_swap; + + lcd_panel_io_t panel_io_handle; + + // Оригинальные поля из spi_bus.h + esp_lcd_panel_io_spi_config_t panel_io_config; + spi_bus_config_t bus_config; + esp_lcd_spi_bus_handle_t bus_handle; + spi_host_device_t host; + mp_machine_hw_spi_device_obj_t spi_device; + + // Partial updates specific fields + uint8_t *active_fb; + uint8_t *idle_fb; + uint8_t *partial_buf; + + int x_start; + int y_start; + int x_end; + int y_end; + uint16_t width; + uint16_t height; + uint8_t rotation: 2; + uint8_t bytes_per_pixel: 2; + uint8_t last_update: 1; + uint8_t first_frame_received: 1; + + // Task synchronization + spi_bus_fast_lock_t copy_lock; + spi_bus_fast_event_t copy_task_exit; + spi_bus_fast_lock_t tx_color_lock; + spi_bus_fast_lock_t init_lock; + + TaskHandle_t copy_task_handle; + + // ДИАГНОСТИКА - счетчики без printf + volatile uint32_t task_alive_counter; + volatile uint32_t task_loop_counter; + volatile uint32_t copy_function_count; // сколько раз вызвана copy_partial_to_full_buffer + volatile uint32_t send_function_count; // сколько раз вызвана send_full_buffer_chunked + volatile uint32_t send_error_count; // сколько ошибок ESP-IDF API + // Основные диагностические поля убраны - используем ets_printf из task + + mp_lcd_err_t init_err; + mp_rom_error_text_t init_err_msg; + + } mp_lcd_spi_bus_fast_obj_t; + + void spi_bus_fast_event_init(spi_bus_fast_event_t *event); + void spi_bus_fast_event_delete(spi_bus_fast_event_t *event); + bool spi_bus_fast_event_isset(spi_bus_fast_event_t *event); + void spi_bus_fast_event_set(spi_bus_fast_event_t *event); + void spi_bus_fast_event_clear(spi_bus_fast_event_t *event); + void spi_bus_fast_event_wait(spi_bus_fast_event_t *event); + + int spi_bus_fast_lock_acquire(spi_bus_fast_lock_t *lock, int32_t wait_ms); + void spi_bus_fast_lock_release(spi_bus_fast_lock_t *lock); + void spi_bus_fast_lock_init(spi_bus_fast_lock_t *lock); + void spi_bus_fast_lock_delete(spi_bus_fast_lock_t *lock); + + void spi_bus_fast_copy_task(void *self_in); + + // SPIBusFast functions + mp_lcd_err_t spi_fast_tx_color(mp_obj_t obj, int lcd_cmd, void *color, size_t color_size, + int x_start, int y_start, int x_end, int y_end, + uint8_t rotation, bool last_update); + + extern const mp_obj_type_t mp_lcd_spi_bus_fast_type; + + extern void mp_lcd_spi_bus_fast_deinit_all(void); + +#endif /* _ESP32_SPI_BUS_FAST_H_ */ \ No newline at end of file diff --git a/ext_mod/lcd_bus/esp32_include/spi_bus_fast_rotation.h b/ext_mod/lcd_bus/esp32_include/spi_bus_fast_rotation.h new file mode 100644 index 00000000..79b73465 --- /dev/null +++ b/ext_mod/lcd_bus/esp32_include/spi_bus_fast_rotation.h @@ -0,0 +1,21 @@ +// Copyright (c) 2024 - 2025 Kevin G. Schlosser +// Copyright (c) 2024 - 2025 Viktor Vorobjov + + +#ifndef __SPI_BUS_FAST_ROTATION_H__ +#define __SPI_BUS_FAST_ROTATION_H__ + +#include + +// Rotation constants +#define SPI_FAST_ROTATION_0 (0) +#define SPI_FAST_ROTATION_90 (1) +#define SPI_FAST_ROTATION_180 (2) +#define SPI_FAST_ROTATION_270 (3) + +// Main rotation function for SPIBusFast +void spi_fast_copy_pixels(void *dst, void *src, uint32_t x_start, uint32_t y_start, + uint32_t x_end, uint32_t y_end, uint32_t dst_width, uint32_t dst_height, + uint32_t bytes_per_pixel, uint8_t rotate, uint8_t rgb565_dither); + +#endif // __SPI_BUS_FAST_ROTATION_H__ \ No newline at end of file diff --git a/ext_mod/lcd_bus/esp32_src/spi_bus_fast.c b/ext_mod/lcd_bus/esp32_src/spi_bus_fast.c new file mode 100644 index 00000000..ee9d8679 --- /dev/null +++ b/ext_mod/lcd_bus/esp32_src/spi_bus_fast.c @@ -0,0 +1,519 @@ +// Copyright (c) 2024 - 2025 Kevin G. Schlosser +// Copyright (c) 2024 - 2025 Viktor Vorobjov + +// local includes +#include "lcd_types.h" +#include "modlcd_bus.h" +#include "spi_bus_fast.h" +#include "../../../micropy_updates/common/mp_spi_common.h" + +// esp-idf includes +#include "driver/spi_common.h" +#include "driver/spi_master.h" +#include "soc/gpio_sig_map.h" +#include "soc/spi_pins.h" +#include "soc/soc_caps.h" +#include "rom/gpio.h" +#include "esp_err.h" +#include "esp_lcd_panel_io.h" +#include "esp_heap_caps.h" +#include "hal/spi_types.h" +#include "esp_task.h" +#include "rom/ets_sys.h" // for ets_printf + +// micropython includes +#include "mphalport.h" +#include "py/obj.h" +#include "py/runtime.h" + +// stdlib includes +#include + +#define DEFAULT_STACK_SIZE (8 * 1024) // Larger than RGB due to SPI init overhead +#define SPI_FAST_BIT_0 (1 << 0) + +// Event management functions +void spi_bus_fast_event_init(spi_bus_fast_event_t *event) +{ + event->handle = xEventGroupCreateStatic(&event->buffer); +} + +void spi_bus_fast_event_delete(spi_bus_fast_event_t *event) +{ + xEventGroupSetBits(event->handle, SPI_FAST_BIT_0); + vEventGroupDelete(event->handle); +} + +void spi_bus_fast_event_wait(spi_bus_fast_event_t *event) +{ + xEventGroupWaitBits(event->handle, SPI_FAST_BIT_0, pdFALSE, pdTRUE, portMAX_DELAY); +} + +bool spi_bus_fast_event_isset(spi_bus_fast_event_t *event) +{ + return (bool)(xEventGroupGetBits(event->handle) & SPI_FAST_BIT_0); +} + +void spi_bus_fast_event_set(spi_bus_fast_event_t *event) +{ + xEventGroupSetBits(event->handle, SPI_FAST_BIT_0); +} + +void spi_bus_fast_event_clear(spi_bus_fast_event_t *event) +{ + xEventGroupClearBits(event->handle, SPI_FAST_BIT_0); +} + +// Lock management functions +int spi_bus_fast_lock_acquire(spi_bus_fast_lock_t *lock, int32_t wait_ms) +{ + return pdTRUE == xSemaphoreTake(lock->handle, wait_ms < 0 ? portMAX_DELAY : pdMS_TO_TICKS((uint16_t)wait_ms)); +} + +void spi_bus_fast_lock_release(spi_bus_fast_lock_t *lock) +{ + xSemaphoreGive(lock->handle); +} + +void spi_bus_fast_lock_init(spi_bus_fast_lock_t *lock) +{ + lock->handle = xSemaphoreCreateBinaryStatic(&lock->buffer); + xSemaphoreGive(lock->handle); +} + +void spi_bus_fast_lock_delete(spi_bus_fast_lock_t *lock) +{ + xSemaphoreGive(lock->handle); + vSemaphoreDelete(lock->handle); +} + +mp_lcd_err_t spi_fast_del(mp_obj_t obj); +mp_lcd_err_t spi_fast_init(mp_obj_t obj, uint16_t width, uint16_t height, uint8_t bpp, uint32_t buffer_size, bool rgb565_byte_swap, uint8_t cmd_bits, uint8_t param_bits); +mp_lcd_err_t spi_fast_get_lane_count(mp_obj_t obj, uint8_t *lane_count); +void spi_fast_deinit_callback(mp_machine_hw_spi_device_obj_t *device); + +static uint8_t spi_fast_bus_count = 0; +static mp_lcd_spi_bus_fast_obj_t **spi_fast_bus_objs; + +void mp_lcd_spi_bus_fast_deinit_all(void) +{ + // we need to copy the existing array to a new one so the order doesn't + // get all mucked up when objects get removed. + mp_lcd_spi_bus_fast_obj_t *objs[spi_fast_bus_count]; + + for (uint8_t i=0;icallback = mp_const_none; + + // Copy all fields from original spi_bus.c + self->host = (spi_host_device_t)spi_bus->host; + self->panel_io_handle.panel_io = NULL; + self->bus_handle = (esp_lcd_spi_bus_handle_t)self->host; + + self->panel_io_config.cs_gpio_num = (int)args[ARG_cs].u_int; + self->panel_io_config.dc_gpio_num = (int)args[ARG_dc].u_int; + self->panel_io_config.spi_mode = (int)args[ARG_spi_mode].u_int; + self->panel_io_config.pclk_hz = (unsigned int)args[ARG_freq].u_int; + self->panel_io_config.on_color_trans_done = &bus_trans_done_cb; + self->panel_io_config.user_ctx = self; + self->panel_io_config.flags.dc_low_on_data = (unsigned int)args[ARG_dc_low_on_data].u_bool; + self->panel_io_config.flags.lsb_first = (unsigned int)args[ARG_lsb_first].u_bool; + self->panel_io_config.flags.cs_high_active = (unsigned int)args[ARG_cs_high_active].u_bool; + self->panel_io_config.flags.sio_mode = (unsigned int)args[ARG_dual].u_bool; + self->panel_io_config.flags.quad_mode = (unsigned int)args[ARG_quad].u_bool; + self->panel_io_config.flags.octal_mode = (unsigned int)args[ARG_octal].u_bool; + + if (!spi_bus->dual) self->panel_io_config.flags.sio_mode = 0; + if (!spi_bus->quad) self->panel_io_config.flags.quad_mode = 0; + if (!spi_bus->octal) self->panel_io_config.flags.octal_mode = 0; + + // Set up functions - key change! tx_color is now our fast version + LCD_DEBUG_PRINT("SPIBusFast: setting function pointers\n"); + self->panel_io_handle.del = &spi_fast_del; + self->panel_io_handle.init = &spi_fast_init; + self->panel_io_handle.get_lane_count = &spi_fast_get_lane_count; + self->panel_io_handle.tx_color = &spi_fast_tx_color; // MAIN CHANGE! + LCD_DEBUG_PRINT("SPIBusFast: tx_color function set\n"); + + self->spi_device.active = true; + self->spi_device.base.type = &mp_machine_hw_spi_device_type; + self->spi_device.spi_bus = spi_bus; + self->spi_device.deinit = &spi_fast_deinit_callback; + self->spi_device.user_data = self; + + // Initialize new fields for partial updates + self->active_fb = NULL; + self->idle_fb = NULL; + self->partial_buf = NULL; + self->width = 0; + self->height = 0; + self->bytes_per_pixel = 0; + self->rotation = 0; + self->last_update = 0; + self->first_frame_received = 0; + + + // Synchronization will be initialized in spi_fast_init (like RGB) + + self->copy_task_handle = NULL; + self->init_err = LCD_OK; + + LCD_DEBUG_PRINT("host=%d\n", self->host) + LCD_DEBUG_PRINT("cs_gpio_num=%d\n", self->panel_io_config.cs_gpio_num) + LCD_DEBUG_PRINT("dc_gpio_num=%d\n", self->panel_io_config.dc_gpio_num) + LCD_DEBUG_PRINT("spi_mode=%d\n", self->panel_io_config.spi_mode) + LCD_DEBUG_PRINT("pclk_hz=%i\n", self->panel_io_config.pclk_hz) + LCD_DEBUG_PRINT("dc_low_on_data=%d\n", self->panel_io_config.flags.dc_low_on_data) + LCD_DEBUG_PRINT("lsb_first=%d\n", self->panel_io_config.flags.lsb_first) + LCD_DEBUG_PRINT("cs_high_active=%d\n", self->panel_io_config.flags.cs_high_active) + LCD_DEBUG_PRINT("dual=%d\n", self->panel_io_config.flags.sio_mode) + LCD_DEBUG_PRINT("quad=%d\n", self->panel_io_config.flags.quad_mode) + LCD_DEBUG_PRINT("octal=%d\n", self->panel_io_config.flags.octal_mode) + + LCD_DEBUG_PRINT("SPIBusFast: constructor completed successfully\n"); + + return MP_OBJ_FROM_PTR(self); +} + +void spi_fast_deinit_callback(mp_machine_hw_spi_device_obj_t *device) +{ + mp_lcd_spi_bus_fast_obj_t *self = (mp_lcd_spi_bus_fast_obj_t *)device->user_data; + spi_fast_del(MP_OBJ_FROM_PTR(self)); +} + +mp_lcd_err_t spi_fast_del(mp_obj_t obj) +{ + LCD_DEBUG_PRINT("spi_fast_del(self)\n") + + mp_lcd_spi_bus_fast_obj_t *self = (mp_lcd_spi_bus_fast_obj_t *)obj; + + // Stop copy task if it's running + if (self->copy_task_handle != NULL) { + spi_bus_fast_event_set(&self->copy_task_exit); + vTaskDelete(self->copy_task_handle); + self->copy_task_handle = NULL; + } + + // Free frame buffers + if (self->active_fb != NULL) { + heap_caps_free(self->active_fb); + self->active_fb = NULL; + } + + if (self->idle_fb != NULL) { + heap_caps_free(self->idle_fb); + self->idle_fb = NULL; + } + + if (self->panel_io_handle.panel_io != NULL) { + mp_lcd_err_t ret = esp_lcd_panel_io_del(self->panel_io_handle.panel_io); + if (ret != ESP_OK) { + mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("%d(esp_lcd_panel_io_del)"), ret); + return ret; + } + + self->panel_io_handle.panel_io = NULL; + + if (self->view1 != NULL) { + heap_caps_free(self->view1->items); + self->view1->items = NULL; + self->view1->len = 0; + self->view1 = NULL; + LCD_DEBUG_PRINT("spi_free_framebuffer(self, buf=1)\n") + } + + if (self->view2 != NULL) { + heap_caps_free(self->view2->items); + self->view2->items = NULL; + self->view2->len = 0; + self->view2 = NULL; + LCD_DEBUG_PRINT("spi_free_framebuffer(self, buf=1)\n") + } + + uint8_t i= 0; + for (;ispi_device); + self->spi_device.active = false; + + if (self->spi_device.spi_bus->device_count == 0) { + self->spi_device.spi_bus->deinit(self->spi_device.spi_bus); + } + + // Clean up synchronization + spi_bus_fast_lock_delete(&self->copy_lock); + spi_bus_fast_lock_delete(&self->tx_color_lock); + spi_bus_fast_lock_delete(&self->init_lock); + spi_bus_fast_event_delete(&self->copy_task_exit); + + return ret; + } else { + return LCD_FAIL; + } +} + +mp_lcd_err_t spi_fast_init(mp_obj_t obj, uint16_t width, uint16_t height, uint8_t bpp, uint32_t buffer_size, bool rgb565_byte_swap, uint8_t cmd_bits, uint8_t param_bits) +{ + LCD_DEBUG_PRINT("spi_fast_init(self, width=%i, height=%i, bpp=%i, buffer_size=%lu, rgb565_byte_swap=%i, cmd_bits=%i, param_bits=%i)\n", width, height, bpp, buffer_size, (uint8_t)rgb565_byte_swap, cmd_bits, param_bits) + mp_lcd_spi_bus_fast_obj_t *self = (mp_lcd_spi_bus_fast_obj_t *)obj; + + if (self->panel_io_handle.panel_io != NULL) { + return LCD_FAIL; + } + + if (self->spi_device.spi_bus->state == MP_SPI_STATE_STOPPED) { + mp_machine_hw_spi_bus_initilize(self->spi_device.spi_bus); + } + + if (bpp == 16) { + self->rgb565_byte_swap = rgb565_byte_swap; + } else { + self->rgb565_byte_swap = false; + } + + // Save display parameters for partial updates + self->width = width; + self->height = height; + self->bytes_per_pixel = bpp / 8; + + // Allocate two full frame buffers for double buffering + uint32_t fb_size = width * height * self->bytes_per_pixel; + + self->active_fb = (uint8_t*)heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (self->active_fb == NULL) { + return LCD_FAIL; + } + // Do NOT initialize to zero - leave uninitialized for performance + + self->idle_fb = (uint8_t*)heap_caps_malloc(fb_size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (self->idle_fb == NULL) { + heap_caps_free(self->active_fb); + self->active_fb = NULL; + return LCD_FAIL; + } + // Do NOT initialize to zero - leave uninitialized for performance + + self->panel_io_config.trans_queue_depth = 10; + self->panel_io_config.lcd_cmd_bits = (int)cmd_bits; + self->panel_io_config.lcd_param_bits = (int)param_bits; + + LCD_DEBUG_PRINT("lcd_cmd_bits=%d\n", self->panel_io_config.lcd_cmd_bits) + LCD_DEBUG_PRINT("lcd_param_bits=%d\n", self->panel_io_config.lcd_param_bits) + LCD_DEBUG_PRINT("rgb565_byte_swap=%i\n", (uint8_t)self->rgb565_byte_swap) + LCD_DEBUG_PRINT("trans_queue_depth=%i\n", (uint8_t)self->panel_io_config.trans_queue_depth) + + // Panel IO will be created in copy task (like in RGB driver) + mp_machine_hw_spi_bus_add_device(&self->spi_device); + + // Initialize synchronization (exactly like RGB) + spi_bus_fast_lock_init(&self->copy_lock); + spi_bus_fast_lock_init(&self->tx_color_lock); + spi_bus_fast_event_init(&self->copy_task_exit); + spi_bus_fast_lock_init(&self->init_lock); + + // Start copy task (exactly like RGB) + spi_bus_fast_lock_acquire(&self->init_lock, -1); + LCD_DEBUG_PRINT("width=%i\n", width); + LCD_DEBUG_PRINT("height=%i\n", height); + LCD_DEBUG_PRINT("bytes_per_pixel=%d\n", self->bytes_per_pixel); + LCD_DEBUG_PRINT("rgb565_byte_swap=%d\n", self->rgb565_byte_swap); + LCD_DEBUG_PRINT("SPIBusFast: creating copy task...\n"); + BaseType_t task_result = xTaskCreatePinnedToCore( + spi_bus_fast_copy_task, + "spi_fast_copy", + DEFAULT_STACK_SIZE / sizeof(StackType_t), + self, + ESP_TASK_PRIO_MAX - 1, + &self->copy_task_handle, + tskNO_AFFINITY // Do not pin to specific CPU core + ); + + LCD_DEBUG_PRINT("SPIBusFast: xTaskCreatePinnedToCore returned: %d\n", task_result); + + if (task_result != pdPASS) { + LCD_DEBUG_PRINT("SPIBusFast: FAILED to create copy task!\n"); + spi_bus_fast_lock_release(&self->init_lock); + return LCD_ERR_NO_MEM; + } + + LCD_DEBUG_PRINT("SPIBusFast: copy task created, waiting for init...\n"); + spi_bus_fast_lock_acquire(&self->init_lock, -1); + spi_bus_fast_lock_release(&self->init_lock); + spi_bus_fast_lock_delete(&self->init_lock); + + if (self->init_err != LCD_OK) { + LCD_DEBUG_PRINT("SPIBusFast: copy task initialization failed: %d\n", self->init_err); + return self->init_err; + } + + LCD_DEBUG_PRINT("SPIBusFast: copy task initialized successfully\n"); + + // add the new bus ONLY after successfull initilization of the bus + spi_fast_bus_count++; + spi_fast_bus_objs = m_realloc(spi_fast_bus_objs, spi_fast_bus_count * sizeof(mp_lcd_spi_bus_fast_obj_t *)); + spi_fast_bus_objs[spi_fast_bus_count - 1] = self; + + return LCD_OK; +} + +mp_lcd_err_t spi_fast_get_lane_count(mp_obj_t obj, uint8_t *lane_count) +{ + mp_lcd_spi_bus_fast_obj_t *self = (mp_lcd_spi_bus_fast_obj_t *)obj; + + if (self->panel_io_config.flags.sio_mode) { + *lane_count = 2; + } else if (self->panel_io_config.flags.quad_mode) { + *lane_count = 4; + } else if (self->panel_io_config.flags.octal_mode) { + *lane_count = 8; + } else { + *lane_count = 1; + } + + LCD_DEBUG_PRINT("spi_fast_get_lane_count(self) -> %i\n", (uint8_t)(*lane_count)) + + return LCD_OK; +} + +// KEY FUNCTION! tx_color with copy task support +mp_lcd_err_t spi_fast_tx_color(mp_obj_t obj, int lcd_cmd, void *color, size_t color_size, + int x_start, int y_start, int x_end, int y_end, + uint8_t rotation, bool last_update) +{ + mp_lcd_spi_bus_fast_obj_t *self = (mp_lcd_spi_bus_fast_obj_t *)obj; + + // Acquire tx_color lock to prevent race conditions + spi_bus_fast_lock_acquire(&self->tx_color_lock, -1); + + // Save partial update parameters - ORDER IS CRITICAL! + // The copy task reads these atomically, so we must set them in correct order: + // 1. Set coordinates and rotation first + // 2. Set last_update flag + // 3. Set partial_buf LAST (this signals the task that data is ready) + self->x_start = x_start; + self->y_start = y_start; + self->x_end = x_end; + self->y_end = y_end; + self->rotation = rotation; + self->last_update = (uint8_t)last_update; // EXACTLY LIKE RGB - BEFORE partial_buf! + self->partial_buf = (uint8_t *)color; // LAST - signal for task + + LCD_DEBUG_PRINT("spi_fast_tx_color: set partial_buf=%p, x=%d-%d, y=%d-%d\n", + self->partial_buf, x_start, x_end, y_start, y_end) + + // wake copy task + LCD_DEBUG_PRINT("spi_fast_tx_color: waking copy task, partial_buf=%p, size=%d\n", self->partial_buf, color_size) + + + // Lock ordering is critical for thread safety: + // - tx_color_lock prevents race conditions during parameter updates + // - copy task will release tx_color_lock after reading all parameters + // - copy_lock controls task wake-up + // DO NOT release tx_color_lock here - task will release after reading! + + // Release copy_lock to wake up copy task + spi_bus_fast_lock_release(&self->copy_lock); + + return LCD_OK; +} + +mp_obj_t mp_spi_bus_fast_get_host(mp_obj_t obj) +{ + mp_lcd_spi_bus_fast_obj_t *self = (mp_lcd_spi_bus_fast_obj_t *)obj; + + LCD_DEBUG_PRINT("mp_spi_bus_fast_get_host(self) -> %i\n", (uint8_t)self->host) + return mp_obj_new_int((uint8_t)self->host); +} + +MP_DEFINE_CONST_FUN_OBJ_1(mp_spi_bus_fast_get_host_obj, mp_spi_bus_fast_get_host); + +static const mp_rom_map_elem_t mp_lcd_spi_bus_fast_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_get_host), MP_ROM_PTR(&mp_spi_bus_fast_get_host_obj) }, + { MP_ROM_QSTR(MP_QSTR_get_lane_count), MP_ROM_PTR(&mp_lcd_bus_get_lane_count_obj) }, + { MP_ROM_QSTR(MP_QSTR_allocate_framebuffer), MP_ROM_PTR(&mp_lcd_bus_allocate_framebuffer_obj) }, + { MP_ROM_QSTR(MP_QSTR_free_framebuffer), MP_ROM_PTR(&mp_lcd_bus_free_framebuffer_obj) }, + { MP_ROM_QSTR(MP_QSTR_register_callback), MP_ROM_PTR(&mp_lcd_bus_register_callback_obj) }, + { MP_ROM_QSTR(MP_QSTR_tx_param), MP_ROM_PTR(&mp_lcd_bus_tx_param_obj) }, + { MP_ROM_QSTR(MP_QSTR_tx_color), MP_ROM_PTR(&mp_lcd_bus_tx_color_obj) }, + { MP_ROM_QSTR(MP_QSTR_rx_param), MP_ROM_PTR(&mp_lcd_bus_rx_param_obj) }, + { MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&mp_lcd_bus_init_obj) }, + { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&mp_lcd_bus_deinit_obj) }, + { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&mp_lcd_bus_deinit_obj) }, +}; + +static MP_DEFINE_CONST_DICT(mp_lcd_spi_bus_fast_locals_dict, mp_lcd_spi_bus_fast_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + mp_lcd_spi_bus_fast_type, + MP_QSTR_SPIBusFast, + MP_TYPE_FLAG_NONE, + make_new, mp_lcd_spi_bus_fast_make_new, + locals_dict, (mp_obj_dict_t *)&mp_lcd_spi_bus_fast_locals_dict +); + diff --git a/ext_mod/lcd_bus/esp32_src/spi_bus_fast_rotation.c b/ext_mod/lcd_bus/esp32_src/spi_bus_fast_rotation.c new file mode 100644 index 00000000..aa1780b8 --- /dev/null +++ b/ext_mod/lcd_bus/esp32_src/spi_bus_fast_rotation.c @@ -0,0 +1,160 @@ +// Copyright (c) 2024 - 2025 Kevin G. Schlosser +// Copyright (c) 2024 - 2025 Viktor Vorobjov + +#include "spi_bus_fast.h" +#include "rom/ets_sys.h" // ets_printf + +#include + +// MIN macro for coordinate clipping +#define MIN(x, y) ((x) < (y) ? (x) : (y)) + +// Rotation constants +#define SPI_FAST_ROTATION_0 (0) +#define SPI_FAST_ROTATION_90 (1) +#define SPI_FAST_ROTATION_180 (2) +#define SPI_FAST_ROTATION_270 (3) + + +// Copy functions for different pixel formats +static void copy_8bpp(uint8_t *from, uint8_t *to) +{ + *to = *from; +} + +static void copy_16bpp(uint16_t *from, uint16_t *to) +{ + *to = *from; +} + +static void copy_24bpp(uint8_t *from, uint8_t *to) +{ + *to++ = *from++; + *to++ = *from++; + *to++ = *from++; +} + +static void copy_32bpp(uint32_t *from, uint32_t *to) +{ + *to = *from; +} + +// Rotation function for 16bpp (RGB565) - most common case +static void rotate_16bpp(uint16_t *src, uint16_t *dst, uint32_t x_start, uint32_t y_start, + uint32_t x_end, uint32_t y_end, uint32_t dst_width, uint32_t dst_height, + uint8_t rotate) +{ + uint32_t i; + uint32_t j; + + // Source image geometry: calculate line width and starting offset + uint32_t src_bytes_per_line = x_end - x_start + 1; + uint32_t offset = y_start * src_bytes_per_line + x_start; + + switch (rotate) { + case SPI_FAST_ROTATION_90: + + // Fix for missing right column: extend x_end by 1 pixel + // This was discovered during debugging - without this, + // a vertical strip of pixels was missing on the right edge + uint32_t x_end_extended = x_end + 1; + for (uint32_t y = y_start; y < y_end; y++) { + for (uint32_t x = x_start; x < x_end_extended; x++) { + // Calculate source index (relative to partial buffer) + i = y * src_bytes_per_line + x - offset; + // 90° rotation formula: (x,y) -> (y, height-1-x) + j = (dst_height - 1 - x) * dst_width + y; + // check that we don't go out of bounds + if (j < (dst_width * dst_height)) { + copy_16bpp(src + i, dst + j); + } + } + } + break; + + case SPI_FAST_ROTATION_180: + for (uint32_t y = y_start; y < y_end; y++) { + for (uint32_t x = x_start; x < x_end; x++) { + i = y * src_bytes_per_line + x - offset; + // 180° rotation formula: (x,y) -> (width-1-x, height-1-y) + j = ((dst_height - 1 - y) * dst_width) + dst_width - 1 - x; + copy_16bpp(src + i, dst + j); + } + } + break; + + case SPI_FAST_ROTATION_270: + for (uint32_t y = y_start; y < y_end; y++) { + for (uint32_t x = x_start; x < x_end; x++) { + i = y * src_bytes_per_line + x - offset; + // 270° rotation formula: (x,y) -> (width-1-y, x) + j = x * dst_width + dst_width - 1 - y; + copy_16bpp(src + i, dst + j); + } + } + break; + } +} + +// Main copy function with rotation support +void spi_fast_copy_pixels(void *dst, void *src, uint32_t x_start, uint32_t y_start, + uint32_t x_end, uint32_t y_end, uint32_t dst_width, uint32_t dst_height, + uint32_t bytes_per_pixel, uint8_t rotate, uint8_t rgb565_dither) +{ + + // EXTREME DEFENSIVE: check all parameters + if (dst == NULL || src == NULL || bytes_per_pixel == 0 || + x_end < x_start || y_end < y_start || + dst_width == 0 || dst_height == 0) { + ets_printf("COPY_PIXELS: INVALID PARAMS!\n"); + return; // Invalid parameters + } + + if (rotate == SPI_FAST_ROTATION_0) { + // No rotation - direct copy + uint8_t *dst_ptr = (uint8_t*)dst + ((y_start * dst_width + x_start) * bytes_per_pixel); + uint32_t width = x_end - x_start + 1; + uint32_t height = y_end - y_start + 1; + + if (x_start == 0 && x_end == (dst_width - 1)) { + // Full width copy - most efficient + memcpy(dst_ptr, src, width * height * bytes_per_pixel); + } else { + // Line by line copy + uint8_t *src_ptr = (uint8_t*)src; + for (uint32_t y = 0; y < height; y++) { + memcpy(dst_ptr + y * dst_width * bytes_per_pixel, + src_ptr + y * width * bytes_per_pixel, + width * bytes_per_pixel); + } + } + } else { + // Rotation needed - clip coordinates like + // Fix for black lines between LVGL update blocks + // LVGL sends partial updates as rectangular blocks, and without this adjustment, + // thin black lines appear between blocks after rotation + y_end += 1; + + // Coordinate clipping to prevent buffer overrun + // For 90°/270° rotations, width and height are swapped in coordinate space + if (rotate == SPI_FAST_ROTATION_90 || rotate == SPI_FAST_ROTATION_270) { + // After 90°/270° rotation: x maps to dst_height, y maps to dst_width + x_start = MIN(x_start, dst_height); + x_end = MIN(x_end, dst_height); + y_start = MIN(y_start, dst_width); + y_end = MIN(y_end, dst_width); + } else { + // For 0°/180° rotation: normal coordinate mapping + x_start = MIN(x_start, dst_width); + x_end = MIN(x_end, dst_width); + y_start = MIN(y_start, dst_height); + y_end = MIN(y_end, dst_height); + } + + + if (bytes_per_pixel == 2) { + rotate_16bpp(src, dst, x_start, y_start, x_end, y_end, dst_width, dst_height, rotate); + } + // TODO: Add support for other bit depths if needed (8bpp, 24bpp, 32bpp) + } +} \ No newline at end of file diff --git a/ext_mod/lcd_bus/esp32_src/spi_bus_fast_task.c b/ext_mod/lcd_bus/esp32_src/spi_bus_fast_task.c new file mode 100644 index 00000000..edc0b73b --- /dev/null +++ b/ext_mod/lcd_bus/esp32_src/spi_bus_fast_task.c @@ -0,0 +1,222 @@ +// Copyright (c) 2024 - 2025 Kevin G. Schlosser +// Copyright (c) 2024 - 2025 Viktor Vorobjov + +#include "spi_bus_fast.h" +#include "lcd_types.h" +#include "spi_bus_fast_rotation.h" + +// micropython includes +#include "py/obj.h" +#include "py/runtime.h" +#include "py/gc.h" +#include "py/stackctrl.h" + +// esp-idf includes +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "rom/ets_sys.h" +#include "esp_system.h" +#include "esp_cpu.h" +#include "esp_err.h" +#include "esp_lcd_panel_io.h" + +#include + +// Copy partial data to full frame buffer optimization +static void copy_partial_to_full_buffer(mp_lcd_spi_bus_fast_obj_t *self, int x_start, int y_start, int x_end, int y_end) +{ + if (self->partial_buf == NULL) { + return; + } + + uint8_t *idle_fb = self->idle_fb; + uint8_t *partial_data = self->partial_buf; + + uint32_t width = x_end - x_start + 1; + uint32_t height = y_end - y_start + 1; + uint32_t bytes_per_pixel = self->bytes_per_pixel; + + + + // Use rotation-aware copy function + uint32_t fb_width = self->width; + uint32_t fb_height = self->height; + + spi_fast_copy_pixels( + idle_fb, partial_data, + x_start, y_start, x_end, y_end, + fb_width, fb_height, + bytes_per_pixel, + self->rotation, + self->rgb565_byte_swap && (bytes_per_pixel == 2) ? 1 : 0 + ); + +} + +// Send full frame buffer with chunking (like current implementation) +static void send_full_buffer_chunked(mp_lcd_spi_bus_fast_obj_t *self, int ramwr_cmd, int ramwrc_cmd) +{ + uint32_t total_size = self->width * self->height * self->bytes_per_pixel; + uint32_t chunk_size = 1024 * 10; // 10KB chunks - TODO: make configurable for different screen sizes + uint32_t remaining = total_size; + uint32_t offset = 0; + uint8_t *buffer = self->idle_fb; + + + + // Set window coordinates for FULL screen before sending full buffer + uint16_t max_x = self->width - 1; + uint8_t caset_params[4] = { + 0x00, 0x00, // x1 = 0 (start) + (max_x >> 8) & 0xFF, // x2 high byte + max_x & 0xFF // x2 low byte + }; + + + // QSPI CASET command: 0x2A -> 0x02002A00 (like in working Python driver) + // Format: [MODE][ADDR][DUMMY][DATA] where: + // - 0x02 = command mode for QSPI + // - 0x2A = CASET (Column Address Set) command + // - 0x00 = dummy/padding bytes + uint32_t caset_cmd = (0x02 << 24) | (0x2A << 8); // 0x02002A00 + esp_lcd_panel_io_tx_param(self->panel_io_handle.panel_io, caset_cmd, caset_params, 4); + + uint32_t pixel_count = total_size / 2; + + int chunk_count = 0; + while (remaining > 0) { + uint32_t current_chunk = (remaining > chunk_size) ? chunk_size : remaining; + uint8_t *chunk_data = buffer + offset; + int cmd = (offset == 0) ? ramwr_cmd : ramwrc_cmd; + + // LCD_DEBUG_PRINT("send_full_buffer_chunked: chunk %d - offset=%d, size=%d, cmd=0x%x, data=%p\n", + // chunk_count, offset, current_chunk, cmd, chunk_data) + + // Direct ESP-IDF API call to send data + esp_err_t ret = esp_lcd_panel_io_tx_color( + self->panel_io_handle.panel_io, + cmd, + chunk_data, + current_chunk + ); + + if (ret != ESP_OK) { + } else { + // LCD_DEBUG_PRINT("send_full_buffer_chunked: chunk %d sent successfully\n", chunk_count) + } + + offset += current_chunk; + remaining -= current_chunk; + chunk_count++; + } + + // Cannot print from RTOS task to MicroPython I/O +} + +void spi_bus_fast_copy_task(void *self_in) { + mp_lcd_spi_bus_fast_obj_t *self = (mp_lcd_spi_bus_fast_obj_t *)self_in; + + + // Perform all SPI initialization inside task (like RGB does with LCD) + self->init_err = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)self->spi_device.spi_bus->host, + &self->panel_io_config, + &self->panel_io_handle.panel_io); + + if (self->init_err != ESP_OK) { + self->init_err_msg = MP_ERROR_TEXT("%d(esp_lcd_new_panel_io_spi)"); + spi_bus_fast_lock_release(&self->init_lock); + return; + } + + bool last_update; + + // Critical lock ordering to prevent race conditions: + // 1. Acquire copy_lock first (task is now ready to process data) + // 2. Then release init_lock (signals main thread that init is complete) + // This ensures main thread cannot call tx_color before task is ready + // IMPORTANT: acquire copy_lock BEFORE releasing init_lock (like in RGB) + spi_bus_fast_lock_acquire(&self->copy_lock, -1); + + self->init_err = LCD_OK; + spi_bus_fast_lock_release(&self->init_lock); + + + bool exit = spi_bus_fast_event_isset(&self->copy_task_exit); + + while (!exit) { + // Wait for signal from spi_fast_tx_color + spi_bus_fast_lock_acquire(&self->copy_lock, -1); + + + if (self->partial_buf == NULL) break; + + // Read ALL variables under lock protection atomically + // This prevents race conditions where coordinates might change + // between reads if main thread calls tx_color during this moment + last_update = self->last_update; + + // CRITICAL: read coordinates at the SAME moment as last_update + // All these values must be read atomically as a group + int x_start = self->x_start; + int y_start = self->y_start; + int x_end = self->x_end; + int y_end = self->y_end; + + + // Diagnostics removed - use ets_printf from task + + // Copy partial data to full buffer (analog of copy_pixels) with ATOMIC coordinates + copy_partial_to_full_buffer(self, x_start, y_start, x_end, y_end); + + // Release tx_color_lock AFTER reading last_update and copying (like RGB) + spi_bus_fast_lock_release(&self->tx_color_lock); + + // Callback like in RGB + if (self->callback != mp_const_none) { + volatile uint32_t sp = (uint32_t)esp_cpu_get_sp(); + void *old_state = mp_thread_get_state(); + mp_state_thread_t ts; + mp_thread_set_state(&ts); + mp_stack_set_top((void*)sp); + mp_stack_set_limit(CONFIG_FREERTOS_IDLE_TASK_STACKSIZE - 1024); + mp_locals_set(mp_state_ctx.thread.dict_locals); + mp_globals_set(mp_state_ctx.thread.dict_globals); + mp_sched_lock(); + gc_lock(); + nlr_buf_t nlr; + if (nlr_push(&nlr) == 0) { + mp_call_function_n_kw(self->callback, 0, 0, NULL); + nlr_pop(); + } else { + mp_obj_print_exception(&mp_plat_print, MP_OBJ_FROM_PTR(nlr.ret_val)); + } + gc_unlock(); + mp_sched_unlock(); + mp_thread_set_state(old_state); + } + + // Send full buffer only on last update (like RGB driver) + if (last_update) { + send_full_buffer_chunked(self, 0x32002C00, 0x32003C00); + + // FIRST FRAME STRATEGY: initialize both buffers with same content + if (!self->first_frame_received) { + // First full frame - copy it to active_fb, then to idle_fb + uint32_t total_size = self->width * self->height * self->bytes_per_pixel; + memcpy(self->active_fb, self->idle_fb, total_size); // active_fb = idle_fb (accumulated frame) + memcpy(self->idle_fb, self->active_fb, total_size); // idle_fb = active_fb (same frame) + self->first_frame_received = 1; + // ets_printf("FIRST_FRAME: initialized both buffers with full frame\n"); + } else { + // Normal simple swap like RGB driver + uint8_t *temp = self->active_fb; + self->active_fb = self->idle_fb; + self->idle_fb = temp; + // ets_printf("SIMPLE_SWAP: active <-> idle\n"); + } + } + + self->partial_buf = NULL; + exit = spi_bus_fast_event_isset(&self->copy_task_exit); + } +} \ No newline at end of file diff --git a/ext_mod/lcd_bus/micropython.cmake b/ext_mod/lcd_bus/micropython.cmake index 8feb1ff2..349f0479 100644 --- a/ext_mod/lcd_bus/micropython.cmake +++ b/ext_mod/lcd_bus/micropython.cmake @@ -15,6 +15,9 @@ if(ESP_PLATFORM) ${CMAKE_CURRENT_LIST_DIR}/lcd_types.c ${CMAKE_CURRENT_LIST_DIR}/esp32_src/i2c_bus.c ${CMAKE_CURRENT_LIST_DIR}/esp32_src/spi_bus.c + ${CMAKE_CURRENT_LIST_DIR}/esp32_src/spi_bus_fast.c + ${CMAKE_CURRENT_LIST_DIR}/esp32_src/spi_bus_fast_task.c + ${CMAKE_CURRENT_LIST_DIR}/esp32_src/spi_bus_fast_rotation.c ${CMAKE_CURRENT_LIST_DIR}/esp32_src/i80_bus.c ${CMAKE_CURRENT_LIST_DIR}/esp32_src/rgb_bus.c ${CMAKE_CURRENT_LIST_DIR}/esp32_src/rgb_bus_rotation.c diff --git a/ext_mod/lcd_bus/modlcd_bus.c b/ext_mod/lcd_bus/modlcd_bus.c index 1da883e7..b7d81df1 100644 --- a/ext_mod/lcd_bus/modlcd_bus.c +++ b/ext_mod/lcd_bus/modlcd_bus.c @@ -7,6 +7,10 @@ #include "i80_bus.h" #include "rgb_bus.h" +#ifdef ESP_IDF_VERSION + #include "spi_bus_fast.h" +#endif + #ifdef MP_PORT_UNIX #include "sdl_bus.h" #endif @@ -167,6 +171,12 @@ mp_obj_t mp_lcd_bus_tx_color(size_t n_args, const mp_obj_t *pos_args, mp_map_t * mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + LCD_DEBUG_PRINT("mp_lcd_bus_tx_color: cmd=0x%02X, x=%d-%d, y=%d-%d, last_update=%d\n", + (int)args[ARG_cmd].u_int, + (int)args[ARG_x_start].u_int, (int)args[ARG_x_end].u_int, + (int)args[ARG_y_start].u_int, (int)args[ARG_y_end].u_int, + (bool)args[ARG_last_update].u_bool) + mp_lcd_bus_obj_t *self = (mp_lcd_bus_obj_t *)args[ARG_self].u_obj; mp_buffer_info_t bufinfo; @@ -296,6 +306,11 @@ static const mp_rom_map_elem_t mp_module_lcd_bus_globals_table[] = { #ifdef MP_PORT_UNIX { MP_ROM_QSTR(MP_QSTR_SDLBus), MP_ROM_PTR(&mp_lcd_sdl_bus_type) }, #endif + + #ifdef ESP_IDF_VERSION + { MP_ROM_QSTR(MP_QSTR_SPIBusFast), MP_ROM_PTR(&mp_lcd_spi_bus_fast_type) }, + #endif + { MP_ROM_QSTR(MP_QSTR_DEBUG_ENABLED), MP_ROM_INT(LCD_DEBUG) }, #ifdef ESP_IDF_VERSION From c739b3c096336a7858416db81b552581c08e1775 Mon Sep 17 00:00:00 2001 From: vostraga Date: Fri, 3 Oct 2025 15:14:55 +0300 Subject: [PATCH 2/2] Resolved merge conflicts --- .DS_Store | Bin 0 -> 6148 bytes ext_mod/.DS_Store | Bin 0 -> 6148 bytes ext_mod/lcd_bus/.DS_Store | Bin 0 -> 6148 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 .DS_Store create mode 100644 ext_mod/.DS_Store create mode 100644 ext_mod/lcd_bus/.DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..087096642124daaaef7c9f85135f5b40f7d438fc GIT binary patch literal 6148 zcmeHK%}T>S5T0$TZ7D(z3VK`cTCfG%DqcdZFJMFuDzza&gK4(3sS!#ccYPsW#OHBl zcLP>?@FZeqVE3DypWVy{*&hH9y_w$vr~^O+8=)X&g^+ort6+l(g`Q&s3Vbh}j=gBb zM1Rpl-(H3Qo*{t&eENRnq36vTN8vPSHs3{|SlZYuqtwc`?t@C*WSGpRz2W4VMi)v& zUak+l%P>d>_3cv?C&MrfCORSX0}Q#j3S(cTJvEE{sm^tcqlz-9@6G2&-F8E^I?Y8x z&Rgy7VM88wI*UcMva^41az1#BMzMM^TneOJ%a+9)UQzi{){{GlBNgAHC(p{G8JPiQ zfEi#0)`|gp3{+}sRW%pO3@`&fVSx4riH*>)m>JYt2X=IQCVz>L1a*2#5K4=V#mpdj zP=rZEG^xTqF@#A+yR>nR#mt~d2ccKSdF;yh<3;Gz(Jpm32*)7z%m6d6%s|l$>vaB~ z;V-lDk-wb6BW8dZ_-71=Qs3?Ouqb=Bek+g8T8Zrr8wtf_q@bX_bP2!#?IXL|Y5bCO ZjB_kz25AvTlE2xvmMV+MYKfe%DgOy2+i literal 0 HcmV?d00001 diff --git a/ext_mod/.DS_Store b/ext_mod/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0f94252f9ec68ccb7c26064a69bc2d76cdb8bd7b GIT binary patch literal 6148 zcmeHK%}T>S5T3QwwiF=;1-&hJE!cu>6)&OI7cim+mD-r1!I&*gYYwH5yS|Vw;`2DO zy8){?coMNQu=~x<&u->}><<8l-YjSVH~>(^Mkq)r5Hc6K)@(4L&~x}OgTzmhu^$yo z^cPL^?G=dN4gwg!r|(xjdfxnT6eiPV^Ifc!%Ig~ylzQdHdr+x29ZqLSZ#cQ4(YaEQ zpX)>aA{?ayXX`}8(_t8oCORPuMi_E^8ODK1dukR3iO%(mqlPkYcIWfMZo45{o#vt; z=dE`4pdpVsoyDS7-QL?jJ{vqnPqBJ2TneOJ%cjK~UQzi{*0VQ>BNgAHC(p{G8JPiQ zfEi#0R*C_83{>kYRW%pO3@`&fVSx4riH*>;m>JYt2X=IQCV!cb1a*2#5K4=##mpdj zP=rZEG^xTqF@#A+yR>nx#mt~d2ccKSdF;yh<3;Gz(Jpm32-hI@%m6d6%s|Nu9XkI{ z@s}xl@}G`^b)V8owkR Z<6MiGL7Ii^IvtTO0-6x+n1Nql-~+1$Ou+yE literal 0 HcmV?d00001 diff --git a/ext_mod/lcd_bus/.DS_Store b/ext_mod/lcd_bus/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3b710366e490d57a1aaf8d42fa112ce8d93b2e42 GIT binary patch literal 6148 zcmeHK%}&BV5S|qfkdScDL~omTB@t9WV!*BA0dd>&_Z zmqY|T8DpCLlG$&6X11A+>@EWUtebdEfEoal(1`OJ*sK7plPzIQ&lHfE++!ac2q1z1 zgiGFL!YE)A_-_i(*{y;PF%01VKA+zNJn-E~Jaj|hqSu={41;*wXuPwvT)wbgL{y5m z_Jf$% z7Rm^xwlkd`by{`aY&T|gK5e!-hjo73ZqH`r($?CclpRbLkGuQfykIfKgykft(uF=>0$YzW!efG9#maQ6N(auzb(% zbulIVwk{<{Z!Jf=LnDx1s#u0#qn~5hp{IBoO#;_Ab&xI1sbZW#m_Gti2GbY?{#1bv DQ@WL* literal 0 HcmV?d00001