From 59d038efc2c78a98aec32bf5da38b77f3ca13313 Mon Sep 17 00:00:00 2001 From: Mitchell M Date: Sun, 19 Jul 2026 16:05:07 -0500 Subject: [PATCH] add tft --- CMakeLists.txt | 9 ++ blk_box.cpp | 2 + drivers/CMakeLists.txt | 1 + drivers/ssegs.cpp | 1 + drivers/tft.cpp | 249 +++++++++++++++++++++++++++++ idf_component.yml | 17 +- include/blk_box_drivers/inputs.hpp | 7 +- include/blk_box_drivers/tft.hpp | 39 +++++ include/pins.h | 11 +- 9 files changed, 318 insertions(+), 18 deletions(-) create mode 100644 drivers/tft.cpp create mode 100644 include/blk_box_drivers/tft.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9dbf1f0..56deb93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,19 @@ idf_component_register( SRCS "blk_box.cpp" INCLUDE_DIRS "include" "." + REQUIRES + lvgl PRIV_REQUIRES led_strip esp_driver_gpio esp_driver_i2c + esp_driver_spi + esp_timer + esp_lcd +) + +target_compile_options(${COMPONENT_LIB} PRIVATE + -Wno-missing-field-initializers ) add_subdirectory(drivers) diff --git a/blk_box.cpp b/blk_box.cpp index 0074916..042577d 100644 --- a/blk_box.cpp +++ b/blk_box.cpp @@ -5,6 +5,7 @@ #include "blk_box_drivers/leds.hpp" #include "blk_box_drivers/char_lcd.hpp" #include "blk_box_drivers/ssegs.hpp" +#include "blk_box_drivers/tft.hpp" void init_blk_box(BlkBoxInitConfig cfg) { init_main_i2c(); @@ -12,4 +13,5 @@ void init_blk_box(BlkBoxInitConfig cfg) { init_leds(); init_lcd(); init_ssegs(); + init_tft(); } diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt index 10e17c0..a54465c 100644 --- a/drivers/CMakeLists.txt +++ b/drivers/CMakeLists.txt @@ -8,6 +8,7 @@ set(SOURCES "lcd2004.cpp" "leds.cpp" "ssegs.cpp" + "tft.cpp" "tm1640.cpp" ) diff --git a/drivers/ssegs.cpp b/drivers/ssegs.cpp index 31c8616..b604e15 100644 --- a/drivers/ssegs.cpp +++ b/drivers/ssegs.cpp @@ -445,6 +445,7 @@ int32_t SSegController::get_module_time() { return module_time.load(std::memory_order_acquire); } +// TODO: take timeout to these functions \/ void SSegController::flush() { xEventGroupWaitBits(ssegs_event_group, EVENT_CMDS_FLUSHED, pdFALSE, pdTRUE, portMAX_DELAY); } diff --git a/drivers/tft.cpp b/drivers/tft.cpp new file mode 100644 index 0000000..6b8a6af --- /dev/null +++ b/drivers/tft.cpp @@ -0,0 +1,249 @@ +#include "blk_box_drivers/tft.hpp" + +#include "pins.h" +#include +#include +#include +#include +#include +#include +#include + +static const char* TAG = "tft"; + +static esp_lcd_panel_io_handle_t lcd_io_handle = NULL; +static esp_lcd_panel_handle_t lcd_handle = NULL; + +static lv_disp_draw_buf_t lv_disp_buf; +static lv_disp_drv_t lv_disp_drv; +static lv_disp_t *lv_display = NULL; +static lv_color_t *lv_buf_1 = NULL; +static lv_color_t *lv_buf_2 = NULL; + +lv_obj_t* screen; +static lv_style_t style_screen; + +SemaphoreHandle_t xGuiSemaphore; + +// static bool replay_handler(const char* event, char* arg) { +// return false; +// } + +static bool notify_lvgl_flush_ready( + esp_lcd_panel_io_handle_t panel_io, + esp_lcd_panel_io_event_data_t *edata, + void *user_ctx +) { + lv_disp_drv_t *disp_driver = (lv_disp_drv_t *)user_ctx; + lv_disp_flush_ready(disp_driver); + return false; +} + +// const char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +// /// Base 64 encodes a u16... sort of. This doesn't do any of the fancy padding stuff. +// static void encode_base64(char* buf, size_t start_idx, uint16_t value) { +// buf[start_idx+0] = base64_chars[(value >> 10) & 0x3F]; +// buf[start_idx+1] = base64_chars[(value >> 4) & 0x3F]; +// buf[start_idx+2] = base64_chars[(value << 2) & 0x3F]; +// } + +static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) { + esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) drv->user_data; + + int offsetx1 = area->x1; + int offsetx2 = area->x2; + int offsety1 = area->y1; + int offsety2 = area->y2; + + esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map); + + // TODO: change this to be a kconfig value + #if false + + if (is_state_tracking()) { + size_t size = (offsetx2 + 1 - offsetx1) * (offsety2 + 1 - offsety1) + 1; + // if (size > 1024) { + // ESP_LOGW("tft_track_state", "Write too big (%d)! truncating to 1024!", size); + // } + // size = MIN(1024, size); + + + // 24 bytes for the offsets + // 3 bytes per encoded color + // 1 byte for null terminator + size_t alloc_size = 24 + size * 3 + 1; + char* buf = (char*)malloc(alloc_size); + + if (buf != nullptr) { + size_t initial_offset = sprintf(buf, "%d,%d,%d,%d:", offsetx1, offsety1, offsetx2 + 1, offsety2 + 1); + + for (size_t i = 0; i < size; i++) { + size_t index = initial_offset + i * 3; + + // we assume that the size of the color data is 16b + static_assert(sizeof(lv_color_t) == sizeof(uint16_t), "lv_color_t must be 16b wide"); + encode_base64(buf, index, color_map[i].full); + } + buf[initial_offset + (size-1) * 3 + 1] = '\0'; + + event_occured("TFT_W", buf); + free(buf); + } else { + ESP_LOGE("tft_track_state", "buffer alloc failed!"); + } + } + + #endif +} + +static void IRAM_ATTR lv_tick_task(void *param) { + lv_tick_inc(LVGL_UPDATE_PERIOD_MS); +} + +static void initialize_spi() { + ESP_LOGI(TAG, "Initializing SPI bus (MOSI:%d, MISO:%d, CLK:%d)", + PIN_TFT_MOSI, PIN_TFT_MISO, PIN_TFT_CLK); + + spi_bus_config_t bus = { + .mosi_io_num = PIN_TFT_MOSI, + .miso_io_num = PIN_TFT_MISO, + .sclk_io_num = PIN_TFT_CLK, + .quadwp_io_num = GPIO_NUM_NC, + .quadhd_io_num = GPIO_NUM_NC, + .data4_io_num = GPIO_NUM_NC, + .data5_io_num = GPIO_NUM_NC, + .data6_io_num = GPIO_NUM_NC, + .data7_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_SIZE, + .flags = SPICOMMON_BUSFLAG_SCLK | SPICOMMON_BUSFLAG_MISO | + SPICOMMON_BUSFLAG_MOSI | SPICOMMON_BUSFLAG_MASTER, + .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO, + .intr_flags = ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM + }; + + ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus, SPI_DMA_CH_AUTO)); +} + +static void initialize_display() { + const esp_lcd_panel_io_spi_config_t io_config = { + .cs_gpio_num = PIN_TFT_CS, + .dc_gpio_num = PIN_TFT_RS, + .spi_mode = 0, + .pclk_hz = DISPLAY_REFRESH_HZ, + .trans_queue_depth = DISPLAY_SPI_QUEUE_LEN, + .on_color_trans_done = notify_lvgl_flush_ready, + .user_ctx = &lv_disp_drv, + .lcd_cmd_bits = DISPLAY_COMMAND_BITS, + .lcd_param_bits = DISPLAY_PARAMETER_BITS, + .flags = { + .dc_high_on_cmd = 0, /*!< If enabled, DC level = 1 indicates command transfer */ + .dc_low_on_data = 0, /*!< If enabled, DC level = 0 indicates color data transfer */ + .dc_low_on_param = 0, /*!< If enabled, DC level = 0 indicates parameter transfer */ + .octal_mode = 0, /*!< transmit with octal mode (8 data lines), this mode is used to simulate Intel 8080 timing */ + .quad_mode = 0, /*!< transmit with quad mode (4 data lines), this mode is useful when transmitting LCD parameters (Only use one line for command) */ + .sio_mode = 0, /*!< Read and write through a single data line (MOSI) */ + .lsb_first = 0, /*!< transmit LSB bit first */ + .cs_high_active = 0, /*!< CS line is high active */ + } + }; + + const esp_lcd_panel_dev_config_t lcd_config = { + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, + .data_endian = LCD_RGB_DATA_ENDIAN_BIG, + .bits_per_pixel = 18, + .reset_gpio_num = PIN_TFT_RST, + .flags = { + .reset_active_high = 0 + }, + }; + + ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &lcd_io_handle)); + + ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488_ips(lcd_io_handle, &lcd_config, LV_BUFFER_SIZE, &lcd_handle)); + // ESP_ERROR_CHECK(esp_lcd_new_panel_ili9488(lcd_io_handle, &lcd_config, LV_BUFFER_SIZE, &lcd_handle)); + + ESP_ERROR_CHECK(esp_lcd_panel_reset(lcd_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_init(lcd_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_invert_color(lcd_handle, true)); + ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(lcd_handle, true)); + ESP_ERROR_CHECK(esp_lcd_panel_mirror(lcd_handle, false, true)); + ESP_ERROR_CHECK(esp_lcd_panel_set_gap(lcd_handle, 0, 0)); +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + ESP_ERROR_CHECK(esp_lcd_panel_disp_off(lcd_handle, false)); +#else + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(lcd_handle, true)); +#endif +} + +static void guiTask(void *pvParameter) { + xGuiSemaphore = xSemaphoreCreateRecursiveMutex(); + ESP_LOGI(TAG, "Initializing LVGL"); + lv_init(); + ESP_LOGI(TAG, "Allocating %zu bytes for LVGL buffer", LV_BUFFER_SIZE * sizeof(lv_color_t)); + lv_buf_1 = (lv_color_t *)heap_caps_malloc(LV_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA); +#if USE_DOUBLE_BUFFERING + ESP_LOGI(TAG, "Allocating %zu bytes for second LVGL buffer", LV_BUFFER_SIZE * sizeof(lv_color_t)); + lv_buf_2 = (lv_color_t *)heap_caps_malloc(LV_BUFFER_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA); +#endif + ESP_LOGI(TAG, "Creating LVLG display buffer"); + lv_disp_draw_buf_init(&lv_disp_buf, lv_buf_1, lv_buf_2, LV_BUFFER_SIZE); + + ESP_LOGI(TAG, "Initializing %dx%d display", DISPLAY_HORIZONTAL_PIXELS, DISPLAY_VERTICAL_PIXELS); + lv_disp_drv_init(&lv_disp_drv); + lv_disp_drv.hor_res = DISPLAY_HORIZONTAL_PIXELS; + lv_disp_drv.ver_res = DISPLAY_VERTICAL_PIXELS; + lv_disp_drv.flush_cb = lvgl_flush_cb; + lv_disp_drv.draw_buf = &lv_disp_buf; + lv_disp_drv.user_data = lcd_handle; + // lv_disp_drv.rotated = LV_DISP_ROT_90; + lv_display = lv_disp_drv_register(&lv_disp_drv); + + ESP_LOGI(TAG, "Creating LVGL tick timer"); + const esp_timer_create_args_t lvgl_tick_timer_args = { + .callback = &lv_tick_task, + // .dispatch_method = ESP_TIMER_TASK, + .name = "periodic_gui", + // .skip_unhandled_events = false + }; + esp_timer_handle_t periodic_timer; + ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &periodic_timer)); + ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, LVGL_UPDATE_PERIOD_MS * 1000)); + + screen = lv_scr_act(); + lv_style_init(&style_screen); + lv_style_set_bg_color(&style_screen, lv_color_black()); + lv_obj_add_style(screen, &style_screen, LV_STATE_DEFAULT); + + while (1) { + /* Delay 1 tick (assumes FreeRTOS tick is 10ms */ + vTaskDelay(pdMS_TO_TICKS(10)); + + /* Try to take the semaphore, call lvgl related function on success */ + if (pdTRUE == xSemaphoreTakeRecursive(xGuiSemaphore, portMAX_DELAY)) { + lv_task_handler(); + xSemaphoreGiveRecursive(xGuiSemaphore); + } + } + + vTaskDelete(NULL); +} + +void init_tft() { + ESP_LOGI(TAG, "Initializing TFT..."); + + initialize_spi(); + initialize_display(); + xTaskCreatePinnedToCore(guiTask, "gui", 4096*2, NULL, 5, NULL, 1); + + // register_replay_fn(replay_handler); + + ESP_LOGI(TAG, "TFT initialized!"); +} + +bool lvgl_lock(TickType_t ticks_to_wait) { + return xSemaphoreTakeRecursive(xGuiSemaphore, ticks_to_wait) == pdTRUE; +} + +void lvgl_unlock() { + xSemaphoreGiveRecursive(xGuiSemaphore); +} diff --git a/idf_component.yml b/idf_component.yml index de891c8..8347ad9 100644 --- a/idf_component.yml +++ b/idf_component.yml @@ -3,16 +3,9 @@ dependencies: ## Required IDF version idf: version: '>=6.0.0' - # # Put list of dependencies here - # # For components maintained by Espressif: - # component: "~1.0.0" - # # For 3rd party components: - # username/component: ">=1.0.0,<2.0.0" - # username2/component2: - # version: "~1.0.0" - # # For transient dependencies `public` flag can be set. - # # `public` flag doesn't have an effect dependencies of the `main` component. - # # All dependencies of `main` are public by default. - # public: true + espressif/led_strip: ^3.0.3 - \ No newline at end of file + # atanisoft/esp_lcd_ili9488: ^1.1.1 + atanisoft/esp_lcd_ili9488: + path: ../../../esp_lcd_ili9488 + lvgl/lvgl: ^8.4 diff --git a/include/blk_box_drivers/inputs.hpp b/include/blk_box_drivers/inputs.hpp index 39222b7..c60b0c1 100644 --- a/include/blk_box_drivers/inputs.hpp +++ b/include/blk_box_drivers/inputs.hpp @@ -26,10 +26,16 @@ enum class Button: uint8_t { B2 = 1, B3 = 2, B4 = 3, + GREEN = 0, RED = 1, YELLOW = 2, BLUE = 3, + + LEFT = 0, + DOWN = 1, + UP = 2, + RIGHT = 3, }; constexpr uint8_t raw_value(Button v) { return static_cast(v); } @@ -85,7 +91,6 @@ constexpr char keypad_key_to_char(KeypadKey key) { return lookup[static_cast(key) & 0b1111]; } - struct SwitchFlip { private: // [bit2: up] [bit1-0: switch] diff --git a/include/blk_box_drivers/tft.hpp b/include/blk_box_drivers/tft.hpp new file mode 100644 index 0000000..2363ef7 --- /dev/null +++ b/include/blk_box_drivers/tft.hpp @@ -0,0 +1,39 @@ +#ifndef TFT_HPP +#define TFT_HPP + +#include +#include + +// Uncomment the following line to enable using double buffering of LVGL color +// data. +// #define USE_DOUBLE_BUFFERING 1 + +// rotation swaps the horizontal and vertical pixel counts +#define DISPLAY_HORIZONTAL_PIXELS 480 +#define DISPLAY_VERTICAL_PIXELS 320 +#define DISPLAY_COMMAND_BITS 8 +#define DISPLAY_PARAMETER_BITS 8 +#define DISPLAY_REFRESH_HZ 40000000 +#define DISPLAY_SPI_QUEUE_LEN 10 +#define SPI_MAX_TRANSFER_SIZE 32768 + +#define TFT_INVERT_COLOR false + +// Default to 50 lines of color data +#define LV_BUFFER_SIZE DISPLAY_HORIZONTAL_PIXELS * 50 +#define LVGL_UPDATE_PERIOD_MS 5 + +#define BACKLIGHT_LEDC_MODE LEDC_LOW_SPEED_MODE +#define BACKLIGHT_LEDC_CHANNEL LEDC_CHANNEL_0 +#define BACKLIGHT_LEDC_TIMER LEDC_TIMER_1 +#define BACKLIGHT_LEDC_TIMER_RESOLUTION LEDC_TIMER_10_BIT +#define BACKLIGHT_LEDC_FRQUENCY 5000 + +extern lv_obj_t* screen; + +void init_tft(); + +bool lvgl_lock(TickType_t ticks_to_wait); +void lvgl_unlock(); + +#endif // TFT_HPP diff --git a/include/pins.h b/include/pins.h index 32a629c..08fa8e0 100644 --- a/include/pins.h +++ b/include/pins.h @@ -6,11 +6,12 @@ #define PIN_SDA (GPIO_NUM_7) #define PIN_SCL (GPIO_NUM_15) -#define PIN_LCD_MISO (GPIO_NUM_16) -#define PIN_LCD_MOSI (GPIO_NUM_17) -#define PIN_LCD_CLK (GPIO_NUM_18) -#define PIN_LCD_RS (GPIO_NUM_8) -#define PIN_LCD_RST (GPIO_NUM_9) +#define PIN_TFT_CS (GPIO_NUM_NC) +#define PIN_TFT_MISO (GPIO_NUM_16) +#define PIN_TFT_MOSI (GPIO_NUM_17) +#define PIN_TFT_CLK (GPIO_NUM_18) +#define PIN_TFT_RS (GPIO_NUM_8) +#define PIN_TFT_RST (GPIO_NUM_9) #define PIN_USB_DM (GPIO_NUM_19) #define PIN_USB_DP (GPIO_NUM_20)