From b9d6ef588460d97cd0172acf6a7b581dab185573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20H=C3=B6glinger?= Date: Mon, 30 Jun 2025 20:49:36 +0200 Subject: [PATCH] Initial implementation of Lilygo T-Lora Pager port --- Boards/LilygoTLoraPager/CMakeLists.txt | 7 + Boards/LilygoTLoraPager/Source/Init.cpp | 80 ++++ .../Source/LilygoTloraPager.cpp | 91 +++++ .../Source/LilygoTloraPager.h | 5 + .../Source/hal/TpagerDisplay.cpp | 41 ++ .../Source/hal/TpagerDisplay.h | 40 ++ .../Source/hal/TpagerDisplayConstants.h | 8 + .../Source/hal/TpagerKeyboard.cpp | 367 ++++++++++++++++++ .../Source/hal/TpagerKeyboard.h | 42 ++ .../Source/hal/TpagerPower.cpp | 91 +++++ .../LilygoTLoraPager/Source/hal/TpagerPower.h | 25 ++ .../Source/hal/TpagerSdCard.cpp | 33 ++ .../Source/hal/TpagerSdCard.h | 7 + Drivers/ST7796/CMakeLists.txt | 5 + Drivers/ST7796/README.md | 3 + Drivers/ST7796/Source/St7796Display.cpp | 217 +++++++++++ Drivers/ST7796/Source/St7796Display.h | 98 +++++ Drivers/TCA8418/CMakeLists.txt | 5 + Drivers/TCA8418/README.md | 5 + Drivers/TCA8418/Source/Tca8418.cpp | 206 ++++++++++ Drivers/TCA8418/Source/Tca8418.h | 79 ++++ Libraries/esp_lcd_st7796/CMakeLists.txt | 14 + Libraries/esp_lcd_st7796/README.md | 133 +++++++ Libraries/esp_lcd_st7796/esp_lcd_st7796.c | 36 ++ .../esp_lcd_st7796/esp_lcd_st7796_general.c | 354 +++++++++++++++++ .../esp_lcd_st7796/esp_lcd_st7796_mipi.c | 296 ++++++++++++++ Libraries/esp_lcd_st7796/idf_component.yml | 15 + .../esp_lcd_st7796/include/esp_lcd_st7796.h | 206 ++++++++++ Libraries/esp_lcd_st7796/license.txt | 202 ++++++++++ .../priv_include/esp_lcd_st7796_interface.h | 45 +++ .../esp_lcd_st7796/test_apps/CMakeLists.txt | 6 + .../.test_esp_lcd_st7796_general.c.kate-swp | Bin 0 -> 1702 bytes .../test_apps/main/CMakeLists.txt | 3 + .../test_apps/main/idf_component.yml | 6 + .../test_apps/main/test_app_main.c | 47 +++ .../main/test_esp_lcd_st7796_general.c | 135 +++++++ .../test_apps/main/test_esp_lcd_st7796_mipi.c | 225 +++++++++++ .../test_apps/sdkconfig.defaults | 12 + .../Include/Tactility/hal/i2c/I2cDevice.h | 4 + Tactility/Source/hal/i2c/I2cDevice.cpp | 16 + Tactility/Source/service/gui/GuiDraw.cpp | 47 +++ sdkconfig.board.lilygo-tlora-pager | 56 +++ 42 files changed, 3313 insertions(+) create mode 100644 Boards/LilygoTLoraPager/CMakeLists.txt create mode 100644 Boards/LilygoTLoraPager/Source/Init.cpp create mode 100644 Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp create mode 100644 Boards/LilygoTLoraPager/Source/LilygoTloraPager.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerPower.h create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp create mode 100644 Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h create mode 100644 Drivers/ST7796/CMakeLists.txt create mode 100644 Drivers/ST7796/README.md create mode 100644 Drivers/ST7796/Source/St7796Display.cpp create mode 100644 Drivers/ST7796/Source/St7796Display.h create mode 100644 Drivers/TCA8418/CMakeLists.txt create mode 100644 Drivers/TCA8418/README.md create mode 100644 Drivers/TCA8418/Source/Tca8418.cpp create mode 100644 Drivers/TCA8418/Source/Tca8418.h create mode 100644 Libraries/esp_lcd_st7796/CMakeLists.txt create mode 100644 Libraries/esp_lcd_st7796/README.md create mode 100644 Libraries/esp_lcd_st7796/esp_lcd_st7796.c create mode 100644 Libraries/esp_lcd_st7796/esp_lcd_st7796_general.c create mode 100644 Libraries/esp_lcd_st7796/esp_lcd_st7796_mipi.c create mode 100644 Libraries/esp_lcd_st7796/idf_component.yml create mode 100644 Libraries/esp_lcd_st7796/include/esp_lcd_st7796.h create mode 100644 Libraries/esp_lcd_st7796/license.txt create mode 100644 Libraries/esp_lcd_st7796/priv_include/esp_lcd_st7796_interface.h create mode 100644 Libraries/esp_lcd_st7796/test_apps/CMakeLists.txt create mode 100644 Libraries/esp_lcd_st7796/test_apps/main/.test_esp_lcd_st7796_general.c.kate-swp create mode 100644 Libraries/esp_lcd_st7796/test_apps/main/CMakeLists.txt create mode 100644 Libraries/esp_lcd_st7796/test_apps/main/idf_component.yml create mode 100644 Libraries/esp_lcd_st7796/test_apps/main/test_app_main.c create mode 100644 Libraries/esp_lcd_st7796/test_apps/main/test_esp_lcd_st7796_general.c create mode 100644 Libraries/esp_lcd_st7796/test_apps/main/test_esp_lcd_st7796_mipi.c create mode 100644 Libraries/esp_lcd_st7796/test_apps/sdkconfig.defaults create mode 100644 sdkconfig.board.lilygo-tlora-pager diff --git a/Boards/LilygoTLoraPager/CMakeLists.txt b/Boards/LilygoTLoraPager/CMakeLists.txt new file mode 100644 index 00000000..3e3a7c3c --- /dev/null +++ b/Boards/LilygoTLoraPager/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "Source" + REQUIRES Tactility esp_lvgl_port esp_lcd ST7796 BQ27220 TCA8418 PwmBacklight driver esp_adc +) diff --git a/Boards/LilygoTLoraPager/Source/Init.cpp b/Boards/LilygoTLoraPager/Source/Init.cpp new file mode 100644 index 00000000..ffcac185 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/Init.cpp @@ -0,0 +1,80 @@ +#include "PwmBacklight.h" +#include "Tactility/kernel/SystemEvents.h" +#include "Tactility/service/gps/GpsService.h" + +#include +#include + +#include + +#include +#include + +#define TAG "tpager" + +// Power on +#define TDECK_POWERON_GPIO GPIO_NUM_10 + +std::shared_ptr bq27220; +std::shared_ptr tca8418; + +static bool powerOn() { +/* + gpio_config_t device_power_signal_config = { + .pin_bit_mask = BIT64(TDECK_POWERON_GPIO), + .mode = GPIO_MODE_OUTPUT, + .pull_up_en = GPIO_PULLUP_DISABLE, + .pull_down_en = GPIO_PULLDOWN_DISABLE, + .intr_type = GPIO_INTR_DISABLE, + }; + + if (gpio_config(&device_power_signal_config) != ESP_OK) { + return false; + } + + if (gpio_set_level(TDECK_POWERON_GPIO, 1) != ESP_OK) { + return false; + } +*/ + return true; +} + +bool tdeckInit() { + ESP_LOGI(TAG, LOG_MESSAGE_POWER_ON_START); + if (!powerOn()) { + TT_LOG_E(TAG, LOG_MESSAGE_POWER_ON_FAILED); + return false; + } + + /* 32 Khz and higher gives an issue where the screen starts dimming again above 80% brightness + * when moving the brightness slider rapidly from a lower setting to 100%. + * This is not a slider bug (data was debug-traced) */ + if (!driver::pwmbacklight::init(GPIO_NUM_42, 30000)) { + TT_LOG_E(TAG, "Backlight init failed"); + return false; + } + + bq27220 = std::make_shared(I2C_NUM_0); + tt::hal::registerDevice(bq27220); + + tca8418 = std::make_shared(I2C_NUM_0); + tt::hal::registerDevice(tca8418); + + tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) { + bq27220->configureCapacity(1500, 1500); + + auto gps_service = tt::service::gps::findGpsService(); + if (gps_service != nullptr) { + std::vector gps_configurations; + gps_service->getGpsConfigurations(gps_configurations); + if (gps_configurations.empty()) { + if (gps_service->addGpsConfiguration(tt::hal::gps::GpsConfiguration {.uartName = "Grove", .baudRate = 38400, .model = tt::hal::gps::GpsModel::UBLOX10})) { + TT_LOG_I(TAG, "Configured internal GPS"); + } else { + TT_LOG_E(TAG, "Failed to configure internal GPS"); + } + } + } + }); + return true; +} diff --git a/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp new file mode 100644 index 00000000..de9d5be6 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.cpp @@ -0,0 +1,91 @@ +#include "Tactility/lvgl/LvglSync.h" +#include "hal/TpagerDisplay.h" +#include "hal/TpagerDisplayConstants.h" +#include "hal/TpagerKeyboard.h" +#include "hal/TpagerPower.h" +#include "hal/TpagerSdCard.h" + +#include + +#define TDECK_SPI_TRANSFER_SIZE_LIMIT (TDECK_LCD_HORIZONTAL_RESOLUTION * TDECK_LCD_SPI_TRANSFER_HEIGHT * (LV_COLOR_DEPTH / 8)) + +bool tdeckInit(); + +using namespace tt::hal; + +extern const Configuration lilygo_tlora_pager = { + .initBoot = tdeckInit, + .createDisplay = createDisplay, + .createKeyboard = createKeyboard, + .sdcard = createTpagerSdCard(), + .power = tpager_get_power, + .i2c = { + i2c::Configuration { + .name = "Internal", + .port = I2C_NUM_0, + .initMode = i2c::InitMode::ByTactility, + .isMutable = true, + .config = (i2c_config_t) { + .mode = I2C_MODE_MASTER, + .sda_io_num = GPIO_NUM_3, + .scl_io_num = GPIO_NUM_2, + .sda_pullup_en = false, + .scl_pullup_en = false, + .master = { + .clk_speed = 100'000 + }, + .clk_flags = 0 + } + } + }, + .spi { + spi::Configuration { + .device = SPI2_HOST, + .dma = SPI_DMA_CH_AUTO, + .config = { + .mosi_io_num = GPIO_NUM_34, + .miso_io_num = GPIO_NUM_33, + .sclk_io_num = GPIO_NUM_35, + .quadwp_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported + .quadhd_io_num = GPIO_NUM_NC, // Quad SPI LCD driver is not yet supported + .data4_io_num = GPIO_NUM_NC, + .data5_io_num = GPIO_NUM_NC, + .data6_io_num = GPIO_NUM_NC, + .data7_io_num = GPIO_NUM_NC, + .data_io_default_level = false, + .max_transfer_sz = TDECK_SPI_TRANSFER_SIZE_LIMIT, + .flags = 0, + .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO, + .intr_flags = 0 + }, + .initMode = spi::InitMode::ByTactility, + .isMutable = false, + .lock = tt::lvgl::getSyncLock() // esp_lvgl_port owns the lock for the display + } + }, + .uart { + uart::Configuration { + .name = "Grove", + .port = UART_NUM_1, + .rxPin = GPIO_NUM_4, + .txPin = GPIO_NUM_12, + .rtsPin = GPIO_NUM_NC, + .ctsPin = GPIO_NUM_NC, + .rxBufferSize = 1024, + .txBufferSize = 1024, + .config = { + .baud_rate = 38400, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .rx_flow_ctrl_thresh = 0, + .source_clk = UART_SCLK_DEFAULT, + .flags = { + .allow_pd = 0, + .backup_before_sleep = 0, + } + } + } + } +}; diff --git a/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h new file mode 100644 index 00000000..b3e010fe --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/LilygoTloraPager.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +extern const tt::hal::Configuration lilygo_tlora_pager; diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp new file mode 100644 index 00000000..4bed914a --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.cpp @@ -0,0 +1,41 @@ +#include "TpagerDisplay.h" +#include "TpagerDisplayConstants.h" + +#include +#include + +#include + +#define TAG "tdeck_display" + +std::shared_ptr createDisplay() { + /*auto configuration = std::make_unique( + TDECK_LCD_SPI_HOST, + TDECK_LCD_PIN_CS, + TDECK_LCD_PIN_DC, + 480, + 222, + nullptr, + true, //swapXY + true, //mirrorX + true, //mirrorY + true //invertColor + );*/ + + auto configuration = std::make_unique( + TDECK_LCD_SPI_HOST, + TDECK_LCD_PIN_CS, + TDECK_LCD_PIN_DC, + 480, // w + 222, // h + nullptr, + true, //swapXY + true, //mirrorX + true, //mirrorY + true //invertColor + ); + + configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; + + return std::make_shared(std::move(configuration)); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h new file mode 100644 index 00000000..df09ebc9 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplay.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Tactility/hal/display/DisplayDevice.h" +#include +#include + +class TpagerDisplay : public tt::hal::display::DisplayDevice { + +private: + + esp_lcd_panel_io_handle_t ioHandle = nullptr; + esp_lcd_panel_handle_t panelHandle = nullptr; + lv_display_t* displayHandle = nullptr; + bool poweredOn = false; + +public: + + std::string getName() const final { return "ST7796"; } + std::string getDescription() const final { return "SPI display"; } + + bool start() override; + + bool stop() override; + + void setPowerOn(bool turnOn) override; + bool isPoweredOn() const override { return poweredOn; }; + bool supportsPowerControl() const override { return true; } + + std::shared_ptr _Nullable createTouch() override; + + void setBacklightDuty(uint8_t backlightDuty) override; + bool supportsBacklightDuty() const override { return true; } + + void setGammaCurve(uint8_t index) override; + uint8_t getGammaCurveCount() const override { return 4; }; + + lv_display_t* _Nullable getLvglDisplay() const override { return displayHandle; } +}; + +std::shared_ptr createDisplay(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h new file mode 100644 index 00000000..c2d379a8 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerDisplayConstants.h @@ -0,0 +1,8 @@ +#pragma once + +#define TDECK_LCD_SPI_HOST SPI2_HOST +#define TDECK_LCD_PIN_CS GPIO_NUM_38 +#define TDECK_LCD_PIN_DC GPIO_NUM_37 // RS +#define TDECK_LCD_HORIZONTAL_RESOLUTION 222 +#define TDECK_LCD_VERTICAL_RESOLUTION 480 +#define TDECK_LCD_SPI_TRANSFER_HEIGHT (TDECK_LCD_VERTICAL_RESOLUTION / 10) diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp new file mode 100644 index 00000000..e0287d2e --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.cpp @@ -0,0 +1,367 @@ +#include "TpagerKeyboard.h" +#include +#include + +#include "freertos/queue.h" +#include "driver/gpio.h" + +#include + +#define TAG "tpager_keyboard" + +#define ENCODER_A GPIO_NUM_40 +#define ENCODER_B GPIO_NUM_41 +#define ENCODER_ENTER GPIO_NUM_7 +#define USER_BTN GPIO_NUM_0 + +#define KB_ROWS 4 +#define KB_COLS 11 + +static constexpr char keymap_lc[KB_ROWS][KB_COLS] = { + {'\0', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\n', '\0'}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', '\0', LV_KEY_BACKSPACE, ' ', '\0'}, + {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} +}; + +static constexpr char keymap_uc[KB_ROWS][KB_COLS] = { + {'\0', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'}, + {'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', '\n', '\0'}, + {'Z', 'X', 'C', 'V', 'B', 'N', 'M', '\0', LV_KEY_BACKSPACE, ' ', '\0'}, + {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} +}; + + +static constexpr char keymap_sy[KB_ROWS][KB_COLS] = { + {'\0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, + {'.', '/', '+', '-', '=', ':', '\'', '"', '@', '\t', '\0'}, + {'_', '$', ';', '?', '!', ',', '.', '\0', LV_KEY_BACKSPACE, ' ', '\0'}, + {'\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'} +}; + +static QueueHandle_t rotaryMsg; +static QueueHandle_t keyboardMsg; + +/** + * The callback simulates press and release events, because the T-Deck + * keyboard only publishes press events on I2C. + * LVGL currently works without those extra release events, but they + * are implemented for correctness and future compatibility. + * + * @param indev_drv + * @param data + */ +static void keyboard_read_callback(lv_indev_t* indev, lv_indev_data_t* data) { + TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev); + uint8_t read_buffer = 0x00; + static bool enter_prev = false; + + // Defaults + data->key = 0; + data->state = LV_INDEV_STATE_RELEASED; + + char encstate = 0; + char keypress = 0; + if (xQueueReceive(keyboardMsg, &keypress, pdMS_TO_TICKS(50)) == pdPASS) { + TT_LOG_I(TAG, "KEY %c (%d)", keypress, keypress); + data->key = keypress; + data->state = LV_INDEV_STATE_PRESSED; + }/* else if (xQueueReceive(rotaryMsg, &encstate, pdMS_TO_TICKS(50)) == pdPASS) { + data->key = encstate; + if (encstate == LV_KEY_NEXT) data->enc_diff = 1; + if (encstate == LV_KEY_PREV) data->enc_diff = -1; + data->state = LV_INDEV_STATE_PRESSED; + }*/ + + //bool enter = !gpio_get_level(ENCODER_ENTER); + /*if (enter != enter_prev) { + TT_LOG_I(TAG, "Encoder CHANGE %d", enter); + data->key = LV_KEY_ENTER; + data->state = enter ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED; + enter_prev = enter; + }*/ +} + +static void encoder_read_callback(lv_indev_t* indev, lv_indev_data_t* data) { + TpagerKeyboard* kb = (TpagerKeyboard*)lv_indev_get_user_data(indev); + uint8_t read_buffer = 0x00; + const int enterFilterThreshold = 2; + static int enterFilter = 0; + static int pulses_prev = 0; + + // Defaults + data->enc_diff = 0; + data->state = LV_INDEV_STATE_RELEASED; + + int pulses = kb->getEncoderPulses(); + int pulse_diff = (pulses - pulses_prev); + if ((pulse_diff > 4) || (pulse_diff < -4)) { + data->enc_diff = pulse_diff / 4; + pulses_prev = pulses; + } + // TT_LOG_I(TAG, "Encoder DIFF %d PULSE %d", data->enc_diff, pulses); + + char encstate = 0; + char keypress = 0; + /*if (xQueueReceive(rotaryMsg, &encstate, pdMS_TO_TICKS(50)) == pdPASS) { + //data->key = encstate; + if (encstate == LV_KEY_NEXT) data->enc_diff = 1; + if (encstate == LV_KEY_PREV) data->enc_diff = -1; + if (encstate == LV_KEY_RIGHT) data->enc_diff = 100; + if (encstate == LV_KEY_LEFT) data->enc_diff = -100; + //data->state = LV_INDEV_STATE_PRESSED; + }*/ + + + bool enter = !gpio_get_level(ENCODER_ENTER); + if (enter && (enterFilter < enterFilterThreshold)) { + enterFilter++; + } + if (!enter && (enterFilter > 0)) + { + enterFilter--; + } + + if (enterFilter == enterFilterThreshold) { + data->state = LV_INDEV_STATE_PRESSED; + } + //TT_LOG_I(TAG, "ENTER F %d", enterFilter); +} + +static void button_read_callback(lv_indev_t* indev, lv_indev_data_t* data) { + if(!gpio_get_level(USER_BTN)) { /*Is there a button press? (E.g. -1 indicated no button was pressed)*/ + data->state = LV_INDEV_STATE_PRESSED; /*Set the pressed state*/ + TT_LOG_I(TAG, "USR BTN PRESS"); + + } else { + data->state = LV_INDEV_STATE_RELEASED; /*Set the released state*/ + } + + data->btn_id = 0; /*Save the last button*/ +} + +void TpagerKeyboard::processEncoder() { + +} + +void TpagerKeyboard::processKeyboard() { + static bool shift_pressed = false; + static bool sym_pressed = false; + static bool cap_toggle = false; + static bool cap_toggle_armed = true; + + if (keypad->update()) { + for (int i=0; ipressed_key_count; i++) { + auto row = keypad->pressed_list[i].row; + auto col = keypad->pressed_list[i].col; + auto hold = keypad->pressed_list[i].hold_time; + + TT_LOG_I(TAG, "KB PRESS R=%d C=%d", row, col); + + if ((row == 1) && (col == 10)) { + sym_pressed = true; + TT_LOG_I(TAG, "KB SYM ON"); + } + if ((row == 2) && (col == 7)) { + shift_pressed = true; + TT_LOG_I(TAG, "KB SHFT ON"); + } + } + + + if ((sym_pressed && shift_pressed) && cap_toggle_armed) { + cap_toggle = !cap_toggle; + cap_toggle_armed = false; + TT_LOG_I(TAG, "KB CAP TOGGLE"); + } + + for (int i=0; ipressed_key_count; i++) { + auto row = keypad->pressed_list[i].row; + auto col = keypad->pressed_list[i].col; + auto hold = keypad->pressed_list[i].hold_time; + char chr = '\0'; + if (sym_pressed) { + chr = keymap_sy[row][col]; + } else if (shift_pressed || cap_toggle) { + chr = keymap_uc[row][col]; + } else { + chr = keymap_lc[row][col]; + } + + if (chr != '\0') xQueueSend(keyboardMsg, (void *)&chr, portMAX_DELAY); + } + + for (int i=0; ireleased_key_count; i++) { + auto row = keypad->released_list[i].row; + auto col = keypad->released_list[i].col; + + TT_LOG_I(TAG, "KB REL R=%d C=%d", row, col); + + if ((row == 1) && (col == 10)) { + sym_pressed = false; + TT_LOG_I(TAG, "KB SYM OFF"); + } + if ((row == 2) && (col == 7)) { + shift_pressed = false; + TT_LOG_I(TAG, "KB SHFT OFF"); + } + } + + if ((!sym_pressed && !shift_pressed) && !cap_toggle_armed) { + cap_toggle_armed = true; + TT_LOG_I(TAG, "KB CAP TOGGLE REARM"); + } + } + /* + char encstate = encoder.process(); + char encbtn = 0; + switch (encstate) { + case DIR_CW: + TT_LOG_I(TAG, "ENC CW"); + encbtn = sym_pressed ? LV_KEY_RIGHT : LV_KEY_NEXT; + break; + case DIR_CCW: + TT_LOG_I(TAG, "ENC CCW"); + encbtn = sym_pressed ? LV_KEY_LEFT : LV_KEY_PREV; + break; + default: + break; + } + + if (encbtn != 0) xQueueSend(rotaryMsg, (void *)&encbtn, portMAX_DELAY); + */ +} + +bool TpagerKeyboard::start(lv_display_t* display) { + //encoder.begin(); + initEncoder(); + keypad->init(KB_ROWS, KB_COLS); + gpio_input_enable(ENCODER_ENTER); + gpio_input_enable(USER_BTN); + + assert(encoderTimer == nullptr); + encoderTimer = std::make_unique(tt::Timer::Type::Periodic, [this] { + processEncoder(); + processKeyboard(); + }); + + kbHandle = lv_indev_create(); + lv_indev_set_type(kbHandle, LV_INDEV_TYPE_KEYPAD); + lv_indev_set_read_cb(kbHandle, &keyboard_read_callback); + lv_indev_set_display(kbHandle, display); + lv_indev_set_user_data(kbHandle, this); + + btnHandle = lv_indev_create(); + lv_indev_set_type(btnHandle, LV_INDEV_TYPE_BUTTON); + static const lv_point_t btn_points[1] = { + {472, 111} + //{20, 30} + }; + lv_indev_set_button_points(btnHandle, btn_points); + lv_indev_set_read_cb(btnHandle, &button_read_callback); + lv_indev_set_display(btnHandle, display); + lv_indev_set_user_data(btnHandle, this); + + encHandle = lv_indev_create(); + lv_indev_set_type(encHandle, LV_INDEV_TYPE_ENCODER); + lv_indev_set_read_cb(encHandle, &encoder_read_callback); + lv_indev_set_display(encHandle, display); + lv_indev_set_user_data(encHandle, this); + + encoderTimer->start(20 / portTICK_PERIOD_MS); + + return true; +} + +bool TpagerKeyboard::stop() { + assert(encoderTimer); + + encoderTimer->stop(); + encoderTimer = nullptr; + + lv_indev_delete(kbHandle); + kbHandle = nullptr; + lv_indev_delete(encHandle); + encHandle = nullptr; + lv_indev_delete(btnHandle); + btnHandle = nullptr; + return true; +} + +bool TpagerKeyboard::isAttached() const { + return tt::hal::i2c::masterHasDeviceAtAddress(keypad->getPort(), keypad->getAddress(), 100); +} + +void TpagerKeyboard::initEncoder(void) { + pcnt_unit_config_t unit_config = { + .low_limit = -127, + .high_limit = 126, + .flags = { .accum_count = 1 }, + }; + + ESP_ERROR_CHECK(pcnt_new_unit(&unit_config, &encPcntUnit)); + + //ESP_LOGI(encoder, "set glitch filter"); + pcnt_glitch_filter_config_t filter_config = { + .max_glitch_ns = 1000, + }; + ESP_ERROR_CHECK(pcnt_unit_set_glitch_filter(encPcntUnit, &filter_config)); + + //ESP_LOGI(encoder, "install pcnt channels"); + pcnt_chan_config_t chan_a_config = { + .edge_gpio_num = ENCODER_A, + .level_gpio_num = ENCODER_B, + }; + pcnt_channel_handle_t pcnt_chan_a = NULL; + ESP_ERROR_CHECK(pcnt_new_channel(encPcntUnit, &chan_a_config, &pcnt_chan_a)); + pcnt_chan_config_t chan_b_config = { + .edge_gpio_num = ENCODER_B, + .level_gpio_num = ENCODER_A, + }; + pcnt_channel_handle_t pcnt_chan_b = NULL; + ESP_ERROR_CHECK(pcnt_new_channel(encPcntUnit, &chan_b_config, &pcnt_chan_b)); + + //ESP_LOGI(encoder, "set edge and level actions for pcnt channels"); + ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_a, PCNT_CHANNEL_EDGE_ACTION_DECREASE, PCNT_CHANNEL_EDGE_ACTION_INCREASE)); + ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_a, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE)); + ESP_ERROR_CHECK(pcnt_channel_set_edge_action(pcnt_chan_b, PCNT_CHANNEL_EDGE_ACTION_INCREASE, PCNT_CHANNEL_EDGE_ACTION_DECREASE)); + ESP_ERROR_CHECK(pcnt_channel_set_level_action(pcnt_chan_b, PCNT_CHANNEL_LEVEL_ACTION_KEEP, PCNT_CHANNEL_LEVEL_ACTION_INVERSE)); + + //ESP_LOGI(encoder, "add watch points and register callbacks"); + + + int watch_points[] = {-127,126}; + for (size_t i = 0; i < sizeof(watch_points) / sizeof(watch_points[0]); i++) { + ESP_ERROR_CHECK(pcnt_unit_add_watch_point(encPcntUnit, watch_points[i])); + } + /*pcnt_event_callbacks_t cbs = { + .on_reach = pcnt_on_reach, + }; + hEncoder.queue = xQueueCreate(10, sizeof(int)); + ESP_ERROR_CHECK(pcnt_unit_register_event_callbacks(encPcntUnit, &cbs, hEncoder.queue)); + */ + //ESP_LOGI(encoder, "enable pcnt unit"); + ESP_ERROR_CHECK(pcnt_unit_enable(encPcntUnit)); + //ESP_LOGI(encoder, "clear pcnt unit"); + ESP_ERROR_CHECK(pcnt_unit_clear_count(encPcntUnit)); + //ESP_LOGI(encoder, "start pcnt unit"); + ESP_ERROR_CHECK(pcnt_unit_start(encPcntUnit)); + + //External callback register. + //hEncoder.callback = callback; +} + +int TpagerKeyboard::getEncoderPulses() { + int pulses = 0; + pcnt_unit_get_count(encPcntUnit, &pulses); + return pulses; +} + + +extern std::shared_ptr tca8418; +std::shared_ptr createKeyboard() { + rotaryMsg = xQueueCreate(5, sizeof(char)); + keyboardMsg = xQueueCreate(20, sizeof(char)); + + return std::make_shared(tca8418); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h new file mode 100644 index 00000000..b5fabe8a --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerKeyboard.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +#include +#include + +#include + + +class TpagerKeyboard : public tt::hal::keyboard::KeyboardDevice { + +private: + lv_indev_t* _Nullable kbHandle = nullptr; + lv_indev_t* _Nullable encHandle = nullptr; + lv_indev_t* _Nullable btnHandle = nullptr; + //Rotary encoder; + pcnt_unit_handle_t encPcntUnit = nullptr; + std::shared_ptr keypad; + std::unique_ptr encoderTimer; + + void initEncoder(void); + +public: + TpagerKeyboard(std::shared_ptr tca) : keypad(std::move(tca)) {} + ~TpagerKeyboard() {} + + std::string getName() const final { return "T-Lora Pager Keyboard"; } + std::string getDescription() const final { return "I2C keyboard with encoder"; } + + bool start(lv_display_t* display) override; + bool stop() override; + bool isAttached() const override; + lv_indev_t* _Nullable getLvglIndev() override { return kbHandle; } + + void processEncoder(); + void processKeyboard(); + int getEncoderPulses(); +}; + +std::shared_ptr createKeyboard(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp new file mode 100644 index 00000000..b058853a --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.cpp @@ -0,0 +1,91 @@ +#include "TpagerPower.h" + +#include + +#define TAG "power" + +#define TPAGER_GAUGE_I2C_BUS_HANDLE I2C_NUM_0 + +/* +TpagerPower::TpagerPower() : gauge(TPAGER_GAUGE_I2C_BUS_HANDLE) { + gauge->configureCapacity(1500, 1500); +}*/ + +TpagerPower::~TpagerPower() {} + +bool TpagerPower::supportsMetric(MetricType type) const { + switch (type) { + using enum MetricType; + case IsCharging: + case Current: + case BatteryVoltage: + case ChargeLevel: + return true; + default: + return false; + } + + return false; // Safety guard for when new enum values are introduced +} + +bool TpagerPower::getMetric(MetricType type, MetricData& data) { +/* IsCharging, // bool + Current, // int32_t, mAh - battery current: either during charging (positive value) or discharging (negative value) + BatteryVoltage, // uint32_t, mV + ChargeLevel, // uint8_t [0, 100] +*/ + + uint16_t u16 = 0; + int16_t s16 = 0; + switch (type) { + using enum MetricType; + case IsCharging: + Bq27220::BatteryStatus status; + if(gauge->getBatteryStatus(status)) { + data.valueAsBool = !status.reg.DSG; + return true; + } + return false; + break; + case Current: + if (gauge->getCurrent(s16)) { + data.valueAsInt32 = s16; + return true; + } else { + return false; + } + break; + case BatteryVoltage: + if (gauge->getVoltage(u16)) { + data.valueAsUint32 = u16; + return true; + } else { + return false; + } + break; + case ChargeLevel: + if (gauge->getStateOfCharge(u16)) { + data.valueAsUint8 = u16; + return true; + } else { + return false; + } + break; + default: + return false; + break; + } + + return false; // Safety guard for when new enum values are introduced +} + +static std::shared_ptr power; +extern std::shared_ptr bq27220; + +std::shared_ptr tpager_get_power() { + if (power == nullptr) { + power = std::make_shared(bq27220); + } + return power; +} + diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h new file mode 100644 index 00000000..ea5c95e2 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerPower.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Tactility/hal/power/PowerDevice.h" +#include +#include + +using tt::hal::power::PowerDevice; + +class TpagerPower : public PowerDevice { + std::shared_ptr gauge; +public: + + TpagerPower(std::shared_ptr bq) : gauge(std::move(bq)) {} + ~TpagerPower(); + + std::string getName() const final { return "T-LoRa Pager Power measument"; } + std::string getDescription() const final { return "Power measurement interface via I2C fuel gauge"; } + + bool supportsMetric(MetricType type) const override; + bool getMetric(MetricType type, MetricData& data) override; + +private: +}; + +std::shared_ptr tpager_get_power(); diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp new file mode 100644 index 00000000..1adf9d79 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.cpp @@ -0,0 +1,33 @@ +#include "TpagerSdCard.h" + +#include +#include + +#include + +using tt::hal::sdcard::SpiSdCardDevice; + +#define TDECK_SDCARD_PIN_CS GPIO_NUM_21 +#define TDECK_LCD_PIN_CS GPIO_NUM_38 +#define TDECK_RADIO_PIN_CS GPIO_NUM_36 + +std::shared_ptr createTpagerSdCard() { + auto* configuration = new SpiSdCardDevice::Config( + TDECK_SDCARD_PIN_CS, + GPIO_NUM_NC, + GPIO_NUM_NC, + GPIO_NUM_NC, + SdCardDevice::MountBehaviour::AtBoot, + tt::lvgl::getSyncLock(), + { + TDECK_RADIO_PIN_CS, + TDECK_LCD_PIN_CS + } + ); + + auto* sdcard = (SdCardDevice*) new SpiSdCardDevice( + std::unique_ptr(configuration) + ); + + return std::shared_ptr(sdcard); +} diff --git a/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h new file mode 100644 index 00000000..95da81a6 --- /dev/null +++ b/Boards/LilygoTLoraPager/Source/hal/TpagerSdCard.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Tactility/hal/sdcard/SdCardDevice.h" + +using tt::hal::sdcard::SdCardDevice; + +std::shared_ptr createTpagerSdCard(); diff --git a/Drivers/ST7796/CMakeLists.txt b/Drivers/ST7796/CMakeLists.txt new file mode 100644 index 00000000..983cc248 --- /dev/null +++ b/Drivers/ST7796/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility esp_lvgl_port esp_lcd_st7796 driver +) diff --git a/Drivers/ST7796/README.md b/Drivers/ST7796/README.md new file mode 100644 index 00000000..2f6c6fb7 --- /dev/null +++ b/Drivers/ST7796/README.md @@ -0,0 +1,3 @@ +# ST7789 + +A basic ESP32 LVGL driver for ST7789 displays. diff --git a/Drivers/ST7796/Source/St7796Display.cpp b/Drivers/ST7796/Source/St7796Display.cpp new file mode 100644 index 00000000..53070a26 --- /dev/null +++ b/Drivers/ST7796/Source/St7796Display.cpp @@ -0,0 +1,217 @@ +#include "St7796Display.h" + +#include + +#include +#include +#include +#include + +#define TAG "st7796" + +bool St7796Display::start() { + TT_LOG_I(TAG, "Starting"); + + const esp_lcd_panel_io_spi_config_t panel_io_config = { + .cs_gpio_num = configuration->csPin, + .dc_gpio_num = configuration->dcPin, + .spi_mode = 0, + .pclk_hz = configuration->pixelClockFrequency, + .trans_queue_depth = configuration->transactionQueueDepth, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .cs_ena_pretrans = 0, + .cs_ena_posttrans = 0, + .flags = { + .dc_high_on_cmd = 0, + .dc_low_on_data = 0, + .dc_low_on_param = 0, + .octal_mode = 0, + .quad_mode = 0, + .sio_mode = 0, + .lsb_first = 0, + .cs_high_active = 0 + } + }; + + if (esp_lcd_new_panel_io_spi(configuration->spiBusHandle, &panel_io_config, &ioHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to create panel"); + return false; + } + + static const st7796_lcd_init_cmd_t lcd_init_cmds[] = { + {0x01, (uint8_t []){0x00}, 0, 120}, + {0x11, (uint8_t []){0x00}, 0, 120}, + {0xF0, (uint8_t []){0xC3}, 1, 0}, + {0xF0, (uint8_t []){0xC3}, 1, 0}, + {0xF0, (uint8_t []){0x96}, 1, 0}, + {0x36, (uint8_t []){0x58}, 1, 0}, + {0x3A, (uint8_t []){0x55}, 1, 0}, + {0xB4, (uint8_t []){0x01}, 1, 0}, + {0xB6, (uint8_t []){0x80, 0x02, 0x3B}, 3, 0}, + {0xE8, (uint8_t []){0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 8, 0}, + {0xC1, (uint8_t []){0x06}, 1, 0}, + {0xC2, (uint8_t []){0xA7}, 1, 0}, + {0xC5, (uint8_t []){0x18}, 1, 0}, + {0xE0, (uint8_t []){0xF0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2F, 0x54, 0x42, 0x3C, 0x17, 0x14, 0x18, 0x1B}, 15, 0}, + {0xE1, (uint8_t []){0xE0, 0x09, 0x0b, 0x06, 0x04, 0x03, 0x2B, 0x43, 0x42, 0x3B, 0x16, 0x14, 0x17, 0x1B}, 15, 120}, + {0xF0, (uint8_t []){0x3C}, 1, 0}, + {0xF0, (uint8_t []){0x69}, 1, 0}, + {0x21, (uint8_t []){0x00}, 1, 0}, + {0x29, (uint8_t []){0x00}, 1, 0}, + }; + + st7796_vendor_config_t vendor_config = { // Uncomment these lines if use custom initialization commands + .init_cmds = lcd_init_cmds, + .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st7796_lcd_init_cmd_t), + }; + + + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = configuration->resetPin, // Set to -1 if not use + #if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + .color_space = ESP_LCD_COLOR_SPACE_RGB, + #else + .color_space = LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, + #endif + .bits_per_pixel = 16, + .vendor_config = &vendor_config + }; +/* + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = configuration->resetPin, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .data_endian = LCD_RGB_DATA_ENDIAN_LITTLE, + .bits_per_pixel = 16, + .flags = { + .reset_active_high = false + }, + .vendor_config = nullptr + }; +*/ + if (esp_lcd_new_panel_st7796(ioHandle, &panel_config, &panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to create panel"); + return false; + } + + if (esp_lcd_panel_reset(panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to reset panel"); + return false; + } + + if (esp_lcd_panel_init(panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to init panel"); + return false; + } + + if (esp_lcd_panel_invert_color(panelHandle, configuration->invertColor) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel to invert"); + return false; + } + + if (esp_lcd_panel_swap_xy(panelHandle, configuration->swapXY) != ESP_OK) { + TT_LOG_E(TAG, "Failed to swap XY "); + return false; + } + + if (esp_lcd_panel_mirror(panelHandle, configuration->mirrorX, configuration->mirrorY) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel to mirror"); + return false; + } + + if (esp_lcd_panel_set_gap(panelHandle, 0, 49) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel gap"); + return false; + } + + if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) { + TT_LOG_E(TAG, "Failed to turn display on"); + return false; + } + + uint32_t buffer_size; + if (configuration->bufferSize == 0) { + buffer_size = configuration->horizontalResolution * configuration->verticalResolution / 10; + } else { + buffer_size = configuration->bufferSize; + } + + const lvgl_port_display_cfg_t disp_cfg = { + .io_handle = ioHandle, + .panel_handle = panelHandle, + .control_handle = nullptr, + .buffer_size = buffer_size, + .double_buffer = false, + .trans_size = 0, + .hres = configuration->horizontalResolution, + .vres = configuration->verticalResolution, + .monochrome = false, + .rotation = { + .swap_xy = configuration->swapXY, + .mirror_x = configuration->mirrorX, + .mirror_y = configuration->mirrorY, + }, + .color_format = LV_COLOR_FORMAT_NATIVE, + .flags = { + .buff_dma = true, + .buff_spiram = false, + .sw_rotate = false, + .swap_bytes = true, + .full_refresh = false, + .direct_mode = false + } + }; + + displayHandle = lvgl_port_add_disp(&disp_cfg); + + TT_LOG_I(TAG, "Finished"); + return displayHandle != nullptr; +} + +bool St7796Display::stop() { + assert(displayHandle != nullptr); + + lvgl_port_remove_disp(displayHandle); + + if (esp_lcd_panel_del(panelHandle) != ESP_OK) { + return false; + } + + if (esp_lcd_panel_io_del(ioHandle) != ESP_OK) { + return false; + } + + displayHandle = nullptr; + return true; +} + +void St7796Display::setGammaCurve(uint8_t index) { + uint8_t gamma_curve; + switch (index) { + case 0: + gamma_curve = 0x01; + break; + case 1: + gamma_curve = 0x04; + break; + case 2: + gamma_curve = 0x02; + break; + case 3: + gamma_curve = 0x08; + break; + default: + return; + } + const uint8_t param[] = { + gamma_curve + }; + + /*if (esp_lcd_panel_io_tx_param(ioHandle , LCD_CMD_GAMSET, param, 1) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set gamma"); + }*/ +} + diff --git a/Drivers/ST7796/Source/St7796Display.h b/Drivers/ST7796/Source/St7796Display.h new file mode 100644 index 00000000..b196579f --- /dev/null +++ b/Drivers/ST7796/Source/St7796Display.h @@ -0,0 +1,98 @@ +#pragma once + +#include "Tactility/hal/display/DisplayDevice.h" + +#include +#include +#include +#include +#include +#include + +class St7796Display final : public tt::hal::display::DisplayDevice { + +public: + + class Configuration { + + public: + + Configuration( + esp_lcd_spi_bus_handle_t spi_bus_handle, + gpio_num_t csPin, + gpio_num_t dcPin, + unsigned int horizontalResolution, + unsigned int verticalResolution, + std::shared_ptr touch, + bool swapXY = false, + bool mirrorX = false, + bool mirrorY = false, + bool invertColor = false, + uint32_t bufferSize = 0 // Size in pixel count. 0 means default, which is 1/10 of the screen size + ) : spiBusHandle(spi_bus_handle), + csPin(csPin), + dcPin(dcPin), + horizontalResolution(horizontalResolution), + verticalResolution(verticalResolution), + swapXY(swapXY), + mirrorX(mirrorX), + mirrorY(mirrorY), + invertColor(invertColor), + bufferSize(bufferSize), + touch(std::move(touch)) + {} + + esp_lcd_spi_bus_handle_t spiBusHandle; + gpio_num_t csPin; + gpio_num_t dcPin; + gpio_num_t resetPin = GPIO_NUM_NC; + unsigned int pixelClockFrequency = 80'000'000; // Hertz + size_t transactionQueueDepth = 2; + unsigned int horizontalResolution; + unsigned int verticalResolution; + bool swapXY = false; + bool mirrorX = false; + bool mirrorY = false; + bool invertColor = false; + uint32_t bufferSize = 0; // Size in pixel count. 0 means default, which is 1/10 of the screen size + std::shared_ptr touch; + std::function _Nullable backlightDutyFunction = nullptr; + }; + +private: + + std::unique_ptr configuration; + esp_lcd_panel_io_handle_t ioHandle = nullptr; + esp_lcd_panel_handle_t panelHandle = nullptr; + lv_display_t* displayHandle = nullptr; + +public: + + explicit St7796Display(std::unique_ptr inConfiguration) : configuration(std::move(inConfiguration)) { + assert(configuration != nullptr); + } + + std::string getName() const final { return "ST7796"; } + std::string getDescription() const final { return "ST7796 display"; } + + bool start() final; + + bool stop() final; + + std::shared_ptr _Nullable createTouch() final { return configuration->touch; } + + void setBacklightDuty(uint8_t backlightDuty) final { + if (configuration->backlightDutyFunction != nullptr) { + configuration->backlightDutyFunction(backlightDuty); + } + } + + void setGammaCurve(uint8_t index) final; + uint8_t getGammaCurveCount() const final { return 4; }; + + bool supportsBacklightDuty() const final { return configuration->backlightDutyFunction != nullptr; } + + lv_display_t* _Nullable getLvglDisplay() const final { return displayHandle; } +}; + +std::shared_ptr createDisplay(); diff --git a/Drivers/TCA8418/CMakeLists.txt b/Drivers/TCA8418/CMakeLists.txt new file mode 100644 index 00000000..8074f3b3 --- /dev/null +++ b/Drivers/TCA8418/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility +) diff --git a/Drivers/TCA8418/README.md b/Drivers/TCA8418/README.md new file mode 100644 index 00000000..5ecc1181 --- /dev/null +++ b/Drivers/TCA8418/README.md @@ -0,0 +1,5 @@ +# BQ24295 + +Power management: I2C-controlled 3A single cell USB charger with narrow VDC 4.5-5.5V adjustable voltage at 1.5A synchronous boost operation. + +[Datasheet](https://www.ti.com/lit/ds/symlink/bq24295.pdf) diff --git a/Drivers/TCA8418/Source/Tca8418.cpp b/Drivers/TCA8418/Source/Tca8418.cpp new file mode 100644 index 00000000..0acf98f3 --- /dev/null +++ b/Drivers/TCA8418/Source/Tca8418.cpp @@ -0,0 +1,206 @@ +#include "Tca8418.h" +#include + +#define TAG "tca8418" + +namespace registers { + static const uint8_t CFG = 0x01U; + static const uint8_t KP_GPIO1 = 0x1DU; + static const uint8_t KP_GPIO2 = 0x1EU; + static const uint8_t KP_GPIO3 = 0x1FU; + + static const uint8_t KEY_EVENT_A = 0x04U; + static const uint8_t KEY_EVENT_B = 0x05U; + static const uint8_t KEY_EVENT_C = 0x06U; + static const uint8_t KEY_EVENT_D = 0x07U; + static const uint8_t KEY_EVENT_E = 0x08U; + static const uint8_t KEY_EVENT_F = 0x09U; + static const uint8_t KEY_EVENT_G = 0x0AU; + static const uint8_t KEY_EVENT_H = 0x0BU; + static const uint8_t KEY_EVENT_I = 0x0CU; + static const uint8_t KEY_EVENT_J = 0x0DU; +} // namespace registers + + +void Tca8418::init(uint8_t numrows, uint8_t numcols) { + /* + * | ADDRESS | REGISTER NAME | REGISTER DESCRIPTION | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0 | + * |---------+---------------+----------------------+------+------+------+------+------+------+------+------| + * | 0x1D | KP_GPIO1 | Keypad/GPIO Select 1 | ROW7 | ROW6 | ROW5 | ROW4 | ROW3 | ROW2 | ROW1 | ROW0 | + * | 0x1E | KP_GPIO2 | Keypad/GPIO Select 2 | COL7 | COL6 | COL5 | COL4 | COL3 | COL2 | COL1 | COL0 | + * | 0x1F | KP_GPIO3 | Keypad/GPIO Select 3 | N/A | N/A | N/A | N/A | N/A | N/A | COL9 | COL8 | + */ + + num_rows = numrows; + num_cols = numcols; + + // everything enabled in key scan mode + uint8_t enabled_rows = 0x3F; + uint16_t enabled_cols = 0x3FF; + + writeRegister8(registers::KP_GPIO1, enabled_rows); + writeRegister8(registers::KP_GPIO2, (uint8_t)(0xFF & enabled_cols)); + writeRegister8(registers::KP_GPIO3, (uint8_t)(0x03 & (enabled_cols >> 8))); + + /* + * BIT: NAME + * + * 7: AI + * Auto-increment for read and write operations; See below table for more information + * 0 = disabled + * 1 = enabled + * + * 6: GPI_E_CFG + * GPI event mode configuration + * 0 = GPI events are tracked when keypad is locked + * 1 = GPI events are not tracked when keypad is locked + * + * 5: OVR_FLOW_M + * Overflow mode + * 0 = disabled; Overflow data is lost + * 1 = enabled; Overflow data shifts with last event pushing first event out + * + * 4: INT_CFG + * Interrupt configuration + * 0 = processor interrupt remains asserted (or low) if host tries to clear interrupt while there is + * still a pending key press, key release or GPI interrupt + * 1 = processor interrupt is deasserted for 50 μs and reassert with pending interrupts + * + * 3: OVR_FLOW_IEN + * Overflow interrupt enable + * 0 = disabled; INT is not asserted if the FIFO overflows + * 1 = enabled; INT becomes asserted if the FIFO overflows + * + * 2: K_LCK_IEN + * Keypad lock interrupt enable + * 0 = disabled; INT is not asserted after a correct unlock key sequence + * 1 = enabled; INT becomes asserted after a correct unlock key sequence + * + * 1: GPI_IEN + * GPI interrupt enable to host processor + * 0 = disabled; INT is not asserted for a change on a GPI + * 1 = enabled; INT becomes asserted for a change on a GPI + * + * 0: KE_IEN + * Key events interrupt enable to host processor + * 0 = disabled; INT is not asserted when a key event occurs + * 1 = enabled; INT becomes asserted when a key event occurs + */ + + // 10111001 xB9 -- fifo overflow enabled + // 10011001 x99 -- fifo overflow disabled + writeRegister8(registers::CFG, 0x99); + + clear_released_list(); + clear_pressed_list(); + + //delayMicroseconds(100); +} + +bool Tca8418::update() { + last_update_micros = this_update_micros; + uint8_t key_code, key_down, key_event, key_row, key_col; + + key_event = get_key_event(); + // TODO: read gpio R7/R6 status? 0x14 bits 7&6 + // read(0x14, &new_keycode) + + this_update_micros = 0; //micros(); + delta_micros = this_update_micros - last_update_micros; + + // if there is a new event + if (key_event > 0) { + key_code = key_event & 0x7F; + key_down = (key_event & 0x80) >> 7; + key_row = key_code / num_cols; + key_col = key_code % num_cols; + + // always clear the released list + clear_released_list(); + + if (key_down) { + add_pressed_key(key_row, key_col); + // TODO reject ghosts (assume multiple key presses with the same hold time are ghosts.) + + } + else { + add_released_key(key_row, key_col); + remove_pressed_key(key_row, key_col); + } + + return true; + } + + // increment hold times for pressed keys + for (int i=0; i= KEY_EVENT_LIST_SIZE) + return; + + pressed_list[pressed_key_count].row = row; + pressed_list[pressed_key_count].col = col; + pressed_list[pressed_key_count].hold_time = 0; + pressed_key_count++; +} + +void Tca8418::add_released_key(uint8_t row, uint8_t col) { + if (released_key_count >= KEY_EVENT_LIST_SIZE) + return; + + released_key_count++; + released_list[0].row = row; + released_list[0].col = col; +} + + +void Tca8418::remove_pressed_key(uint8_t row, uint8_t col) { + if (pressed_key_count == 0) + return; + + // delete the pressed key + for (int i=0; i + +#define TCA8418_ADDRESS 0x34U +#define KEY_EVENT_LIST_SIZE 10 + +class Tca8418 final : public tt::hal::i2c::I2cDevice { + +private: + uint8_t tca8418_address; + uint32_t last_update_micros; + uint32_t this_update_micros; + + uint8_t new_pressed_keys_count; + + void clear_released_list(); + void clear_pressed_list(); + void add_pressed_key(uint8_t row, uint8_t col); + void add_released_key(uint8_t row, uint8_t col); + void remove_pressed_key(uint8_t row, uint8_t col); + void write(uint8_t register_address, uint8_t data); + bool read(uint8_t register_address, uint8_t *data); + +public: + struct PressedKey { + uint8_t row; + uint8_t col; + uint32_t hold_time; + }; + + struct ReleasedKey { + uint8_t row; + uint8_t col; + }; + + std::string getName() const final { return "TCA8418"; } + + std::string getDescription() const final { return "I2C-controlled keyboard scan IC"; } + + + explicit Tca8418(i2c_port_t port) : I2cDevice(port, TCA8418_ADDRESS) { + delta_micros = 0; + last_update_micros = 0; + this_update_micros = 0; + + pressed_list = new PressedKey[KEY_EVENT_LIST_SIZE]; + released_list = new ReleasedKey[KEY_EVENT_LIST_SIZE]; + matrix_state = new uint16_t[num_rows]; + matrix_state_prev = new uint16_t[num_rows]; + } + + ~Tca8418() { + delete [] matrix_state_prev; + delete [] matrix_state; + delete [] pressed_list; + delete [] released_list; + } + + uint8_t num_rows; + uint8_t num_cols; + + uint16_t *matrix_state; + uint16_t *matrix_state_prev; + + uint32_t delta_micros; + + PressedKey *pressed_list; + ReleasedKey *released_list; + uint8_t pressed_key_count; + uint8_t released_key_count; + + void init(uint8_t numrows, uint8_t numcols); + bool update(); + uint8_t get_key_event(); + bool button_pressed(uint8_t row, uint8_t button_bit_position); + bool button_released(uint8_t row, uint8_t button_bit_position); + bool button_held(uint8_t row, uint8_t button_bit_position); +}; diff --git a/Libraries/esp_lcd_st7796/CMakeLists.txt b/Libraries/esp_lcd_st7796/CMakeLists.txt new file mode 100644 index 00000000..d2e1ed61 --- /dev/null +++ b/Libraries/esp_lcd_st7796/CMakeLists.txt @@ -0,0 +1,14 @@ +set(srcs "esp_lcd_st7796.c" + "esp_lcd_st7796_general.c") +if(CONFIG_SOC_MIPI_DSI_SUPPORTED) + list(APPEND srcs "esp_lcd_st7796_mipi.c") +endif() + +idf_component_register(SRCS ${srcs} + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "priv_include" + REQUIRES "esp_lcd" + PRIV_REQUIRES "driver") + +include(package_manager) +cu_pkg_define_version(${CMAKE_CURRENT_LIST_DIR}) diff --git a/Libraries/esp_lcd_st7796/README.md b/Libraries/esp_lcd_st7796/README.md new file mode 100644 index 00000000..79a4c4f2 --- /dev/null +++ b/Libraries/esp_lcd_st7796/README.md @@ -0,0 +1,133 @@ +# ESP LCD ST7796 + +[![Component Registry](https://components.espressif.com/components/espressif/esp_lcd_st7796/badge.svg)](https://components.espressif.com/components/espressif/esp_lcd_st7796) + +Implementation of the ST7796 LCD controller with esp_lcd component. + +| LCD controller | Communication interface | Component name | Link to datasheet | +| :------------: | :---------------------: | :------------: | :---------------: | +| ST7796 | SPI/I80/MIPI-DSI | esp_lcd_st7796 | [Specification](https://www.displayfuture.com/Display/datasheet/controller/ST7796s.pdf) | + +## Add to project + +Packages from this repository are uploaded to [Espressif's component service](https://components.espressif.com/). +You can add them to your project via `idf.py add-dependency`, e.g. + +```bash +compote manifest add-dependency espressif/esp_lcd_st7796==1.0.0 +``` + +Alternatively, you can create `idf_component.yml`. More is in [Espressif's documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/tools/idf-component-manager.html). + +## Initialization Code + +### I80 interface + +```c + ESP_LOGI(TAG, "Initialize Intel 8080 bus"); + esp_lcd_i80_bus_handle_t i80_bus = NULL; + esp_lcd_i80_bus_config_t bus_config = ST7796_PANEL_BUS_I80_CONFIG( + EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES * EXAMPLE_LCD_BIT_PER_PIXEL / 8, EXAMPLE_LCD_DATA_WIDTH, + EXAMPLE_PIN_NUM_LCD_DC, EXAMPLE_PIN_NUM_LCD_WR, + EXAMPLE_PIN_NUM_LCD_DATA0, EXAMPLE_PIN_NUM_LCD_DATA1, EXAMPLE_PIN_NUM_LCD_DATA2, EXAMPLE_PIN_NUM_LCD_DATA3, + EXAMPLE_PIN_NUM_LCD_DATA4, EXAMPLE_PIN_NUM_LCD_DATA5, EXAMPLE_PIN_NUM_LCD_DATA6, EXAMPLE_PIN_NUM_LCD_DATA7, + EXAMPLE_PIN_NUM_LCD_DATA8, EXAMPLE_PIN_NUM_LCD_DATA9, EXAMPLE_PIN_NUM_LCD_DATA10, EXAMPLE_PIN_NUM_LCD_DATA11, + EXAMPLE_PIN_NUM_LCD_DATA12, EXAMPLE_PIN_NUM_LCD_DATA13, EXAMPLE_PIN_NUM_LCD_DATA14, EXAMPLE_PIN_NUM_LCD_DATA15); + ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); + + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_panel_io_handle_t io_handle = NULL; + esp_lcd_panel_io_i80_config_t io_config = ST7796_PANEL_IO_I80_CONFIG(EXAMPLE_PIN_NUM_LCD_CS, example_callback, &example_callback_ctx); + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle)); + +/** + * Uncomment these lines if use custom initialization commands. + * The array should be declared as "static const" and positioned outside the function. + */ +// static const st7796_lcd_init_cmd_t lcd_init_cmds[] = { +// // {cmd, { data }, data_size, delay_ms} +// {0xf0, (uint8_t []){0xc3}, 1, 0}, +// {0xf0, (uint8_t []){0x96}, 1, 0}, +// {0xb4, (uint8_t []){0x01}, 1, 0}, +// ... +// }; + + ESP_LOGI(TAG, "Install ST7796 panel driver"); + esp_lcd_panel_handle_t panel_handle = NULL; + // st7796_vendor_config_t vendor_config = { // Uncomment these lines if use custom initialization commands + // .init_cmds = lcd_init_cmds, + // .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st7796_lcd_init_cmd_t), + // }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST, // Set to -1 if not use +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) // Implemented by LCD command `36h` + .color_space = ESP_LCD_COLOR_SPACE_RGB, +#else + .rgb_endian = LCD_RGB_ENDIAN_RGB, +#endif + .bits_per_pixel = EXAMPLE_LCD_BIT_PER_PIXEL, // Implemented by LCD command `3Ah` (16/18/24) + // .vendor_config = &vendor_config, // Uncomment this line if use custom initialization commands + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7796(io_handle, &panel_config, &panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + ESP_ERROR_CHECK(esp_lcd_panel_disp_off(panel_handle, false)); +#else + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); +#endif +``` + +### MIPI Interface + +```c +/** + * Uncomment these line if use custom initialization commands. + * The array should be declared as static const and positioned outside the function. + */ +// static const st7796_lcd_init_cmd_t lcd_init_cmds[] = { +// // cmd data data_size delay_ms +// {0xFF, (uint8_t []){0x77, 0x01, 0x00, 0x00, 0x13}, 5, 0}, +// {0xEF, (uint8_t []){0x08}, 1, 0}, +// {0xFF, (uint8_t []){0x77, 0x01, 0x00, 0x00, 0x10}, 5, 0}, +// {0xC0, (uint8_t []){0x3B, 0x00}, 2, 0}, +// ... +// }; + ESP_LOGI(TAG, "MIPI DSI PHY Powered on"); + esp_ldo_channel_config_t ldo_mipi_phy_config = { + .chan_id = EXAMPLE_MIPI_DSI_PHY_PWR_LDO_CHAN, + .voltage_mv = EXAMPLE_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV, + }; + ESP_ERROR_CHECK(esp_ldo_acquire_channel(&ldo_mipi_phy_config, &ldo_mipi_phy)); + + ESP_LOGI(TAG, "Initialize MIPI DSI bus"); + esp_lcd_dsi_bus_config_t bus_config = ST7796_PANEL_BUS_DSI_1CH_CONFIG(); + ESP_ERROR_CHECK(esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus)); + + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_dbi_io_config_t dbi_config = ST7796_PANEL_IO_DBI_CONFIG(); + ESP_ERROR_CHECK(esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &mipi_dbi_io)); + + ESP_LOGI(TAG, "Install LCD driver of st7796"); + esp_lcd_panel_handle_t panel_handle = NULL; + esp_lcd_dpi_panel_config_t dpi_config = ST7796_320_480_PANEL_60HZ_DPI_CONFIG(EXAMPLE_MIPI_DPI_PX_FORMAT); + st7796_vendor_config_t vendor_config = { + // .init_cmds = lcd_init_cmds, // Uncomment these line if use custom initialization commands + // .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(st7796_lcd_init_cmd_t), + .flags.use_mipi_interface = 1, + .mipi_config = { + .dsi_bus = mipi_dsi_bus, + .dpi_config = &dpi_config, + }, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = EXAMPLE_LCD_BIT_PER_PIXEL, + .vendor_config = &vendor_config, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_st7796(mipi_dbi_io, &panel_config, &panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); +``` diff --git a/Libraries/esp_lcd_st7796/esp_lcd_st7796.c b/Libraries/esp_lcd_st7796/esp_lcd_st7796.c new file mode 100644 index 00000000..483d19d6 --- /dev/null +++ b/Libraries/esp_lcd_st7796/esp_lcd_st7796.c @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "soc/soc_caps.h" +#include "esp_check.h" +#include "esp_lcd_types.h" + +#include "esp_lcd_st7796.h" +#include "esp_lcd_st7796_interface.h" + +static const char *TAG = "st7796"; + +esp_err_t esp_lcd_new_panel_st7796(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, + esp_lcd_panel_handle_t *ret_panel) +{ + ESP_LOGI(TAG, "version: %d.%d.%d", ESP_LCD_ST7796_VER_MAJOR, ESP_LCD_ST7796_VER_MINOR, ESP_LCD_ST7796_VER_PATCH); + ESP_RETURN_ON_FALSE(panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "Invalid arguments"); + st7796_vendor_config_t *vendor_config = (st7796_vendor_config_t *)panel_dev_config->vendor_config; + + esp_err_t ret = ESP_ERR_NOT_SUPPORTED; + + if (vendor_config && vendor_config->flags.use_mipi_interface) { +#if SOC_MIPI_DSI_SUPPORTED + ret = esp_lcd_new_panel_st7796_mipi(io, panel_dev_config, ret_panel); +#else + ESP_LOGE(TAG, "The chip does not support MIPI-DSI interface"); +#endif + } else { + ret = esp_lcd_new_panel_st7796_general(io, panel_dev_config, ret_panel); + } + + return ret; +} diff --git a/Libraries/esp_lcd_st7796/esp_lcd_st7796_general.c b/Libraries/esp_lcd_st7796/esp_lcd_st7796_general.c new file mode 100644 index 00000000..b94c6285 --- /dev/null +++ b/Libraries/esp_lcd_st7796/esp_lcd_st7796_general.c @@ -0,0 +1,354 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_commands.h" +#include "driver/gpio.h" +#include "esp_log.h" +#include "esp_check.h" + +#include "esp_lcd_st7796.h" +#include "esp_lcd_st7796_interface.h" + +static const char *TAG = "st7796_general"; + +static esp_err_t panel_st7796_del(esp_lcd_panel_t *panel); +static esp_err_t panel_st7796_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_st7796_init(esp_lcd_panel_t *panel); +static esp_err_t panel_st7796_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data); +static esp_err_t panel_st7796_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); +static esp_err_t panel_st7796_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_st7796_swap_xy(esp_lcd_panel_t *panel, bool swap_axes); +static esp_err_t panel_st7796_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap); +static esp_err_t panel_st7796_disp_on_off(esp_lcd_panel_t *panel, bool off); + +typedef struct { + esp_lcd_panel_t base; + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + bool reset_level; + int x_gap; + int y_gap; + uint8_t fb_bits_per_pixel; + uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register + uint8_t colmod_val; // save current value of LCD_CMD_COLMOD register + const st7796_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; +} st7796_panel_t; + +esp_err_t esp_lcd_new_panel_st7796_general(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel) +{ + esp_err_t ret = ESP_OK; + st7796_panel_t *st7796 = NULL; + gpio_config_t io_conf = { 0 }; + + ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument"); + st7796 = (st7796_panel_t *)calloc(1, sizeof(st7796_panel_t)); + ESP_GOTO_ON_FALSE(st7796, ESP_ERR_NO_MEM, err, TAG, "no mem for st7796 panel"); + + if (panel_dev_config->reset_gpio_num >= 0) { + io_conf.mode = GPIO_MODE_OUTPUT; + io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + switch (panel_dev_config->color_space) { + case ESP_LCD_COLOR_SPACE_RGB: + st7796->madctl_val = 0; + break; + case ESP_LCD_COLOR_SPACE_BGR: + st7796->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space"); + break; + } +#else + switch (panel_dev_config->rgb_endian) { + case LCD_RGB_ENDIAN_RGB: + st7796->madctl_val = 0; + break; + case LCD_RGB_ENDIAN_BGR: + st7796->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported rgb endian"); + break; + } +#endif + + switch (panel_dev_config->bits_per_pixel) { + case 16: // RGB565 + st7796->colmod_val = 0x05; + st7796->fb_bits_per_pixel = 16; + break; + case 18: // RGB666 + st7796->colmod_val = 0x06; + // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel + st7796->fb_bits_per_pixel = 24; + break; + case 24: // RGB888 + st7796->colmod_val = 0x07; + st7796->fb_bits_per_pixel = 24; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + st7796->io = io; + st7796->reset_gpio_num = panel_dev_config->reset_gpio_num; + st7796->reset_level = panel_dev_config->flags.reset_active_high; + if (panel_dev_config->vendor_config) { + st7796->init_cmds = ((st7796_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds; + st7796->init_cmds_size = ((st7796_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size; + } + st7796->base.del = panel_st7796_del; + st7796->base.reset = panel_st7796_reset; + st7796->base.init = panel_st7796_init; + st7796->base.draw_bitmap = panel_st7796_draw_bitmap; + st7796->base.invert_color = panel_st7796_invert_color; + st7796->base.set_gap = panel_st7796_set_gap; + st7796->base.mirror = panel_st7796_mirror; + st7796->base.swap_xy = panel_st7796_swap_xy; +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + st7796->base.disp_off = panel_st7796_disp_on_off; +#else + st7796->base.disp_on_off = panel_st7796_disp_on_off; +#endif + *ret_panel = &(st7796->base); + ESP_LOGD(TAG, "new st7796 panel @%p", st7796); + + ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_ST7796_VER_MAJOR, ESP_LCD_ST7796_VER_MINOR, + ESP_LCD_ST7796_VER_PATCH); + + return ESP_OK; + +err: + if (st7796) { + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(st7796); + } + return ret; +} + +static esp_err_t panel_st7796_del(esp_lcd_panel_t *panel) +{ + st7796_panel_t *st7796 = __containerof(panel, st7796_panel_t, base); + + if (st7796->reset_gpio_num >= 0) { + gpio_reset_pin(st7796->reset_gpio_num); + } + ESP_LOGD(TAG, "del st7796 panel @%p", st7796); + free(st7796); + return ESP_OK; +} + +static esp_err_t panel_st7796_reset(esp_lcd_panel_t *panel) +{ + st7796_panel_t *st7796 = __containerof(panel, st7796_panel_t, base); + esp_lcd_panel_io_handle_t io = st7796->io; + + // perform hardware reset + if (st7796->reset_gpio_num >= 0) { + gpio_set_level(st7796->reset_gpio_num, st7796->reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(st7796->reset_gpio_num, !st7796->reset_level); + vTaskDelay(pdMS_TO_TICKS(120)); + } else { // perform software reset + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(120)); // spec, wait at least 5ms before sending new command + } + + return ESP_OK; +} + +static const st7796_lcd_init_cmd_t vendor_specific_init_default[] = { +// {cmd, { data }, data_size, delay_ms} + {0xf0, (uint8_t []){0xc3}, 1, 0}, + {0xf0, (uint8_t []){0x96}, 1, 0}, + {0xb4, (uint8_t []){0x01}, 1, 0}, + {0xb7, (uint8_t []){0xc6}, 1, 0}, + {0xe8, (uint8_t []){0x40, 0x8a, 0x00, 0x00, 0x29, 0x19, 0xa5, 0x33}, 8, 0}, + {0xc1, (uint8_t []){0x06}, 1, 0}, + {0xc2, (uint8_t []){0xa7}, 1, 0}, + {0xc5, (uint8_t []){0x18}, 1, 0}, + {0xe0, (uint8_t []){0xf0, 0x09, 0x0b, 0x06, 0x04, 0x15, 0x2f, 0x54, 0x42, 0x3c, 0x17, 0x14, 0x18, 0x1b}, 14, 0}, + {0xe1, (uint8_t []){0xf0, 0x09, 0x0b, 0x06, 0x04, 0x03, 0x2d, 0x43, 0x42, 0x3b, 0x16, 0x14, 0x17, 0x1b}, 14, 0}, + {0xf0, (uint8_t []){0x3c}, 1, 0}, + {0xf0, (uint8_t []){0x69}, 1, 0}, +}; + +static esp_err_t panel_st7796_init(esp_lcd_panel_t *panel) +{ + st7796_panel_t *st7796 = __containerof(panel, st7796_panel_t, base); + esp_lcd_panel_io_handle_t io = st7796->io; + + // LCD goes into sleep mode and display will be turned off after power on reset, exit sleep mode first + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SLPOUT, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + st7796->madctl_val, + }, 1), TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]) { + st7796->colmod_val, + }, 1), TAG, "send command failed"); + + const st7796_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + if (st7796->init_cmds) { + init_cmds = st7796->init_cmds; + init_cmds_size = st7796->init_cmds_size; + } else { + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(st7796_lcd_init_cmd_t); + } + + bool is_cmd_overwritten = false; + for (int i = 0; i < init_cmds_size; i++) { + // Check if the command has been used or conflicts with the internal + switch (init_cmds[i].cmd) { + case LCD_CMD_MADCTL: + is_cmd_overwritten = true; + st7796->madctl_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + case LCD_CMD_COLMOD: + is_cmd_overwritten = true; + st7796->colmod_val = ((uint8_t *)init_cmds[i].data)[0]; + break; + default: + is_cmd_overwritten = false; + break; + } + + if (is_cmd_overwritten) { + ESP_LOGW(TAG, "The %02Xh command has been used and will be overwritten by external initialization sequence", init_cmds[i].cmd); + } + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + ESP_LOGD(TAG, "send init commands success"); + + return ESP_OK; +} + +static esp_err_t panel_st7796_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data) +{ + st7796_panel_t *st7796 = __containerof(panel, st7796_panel_t, base); + assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position"); + esp_lcd_panel_io_handle_t io = st7796->io; + + x_start += st7796->x_gap; + x_end += st7796->x_gap; + y_start += st7796->y_gap; + y_end += st7796->y_gap; + + // define an area of frame memory where MCU can access + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, (uint8_t[]) { + (x_start >> 8) & 0xFF, + x_start & 0xFF, + ((x_end - 1) >> 8) & 0xFF, + (x_end - 1) & 0xFF, + }, 4), TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, (uint8_t[]) { + (y_start >> 8) & 0xFF, + y_start & 0xFF, + ((y_end - 1) >> 8) & 0xFF, + (y_end - 1) & 0xFF, + }, 4), TAG, "send command failed"); + // transfer frame buffer + size_t len = (x_end - x_start) * (y_end - y_start) * st7796->fb_bits_per_pixel / 8; + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, len), TAG, "send command failed"); + + return ESP_OK; +} + +static esp_err_t panel_st7796_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) +{ + st7796_panel_t *st7796 = __containerof(panel, st7796_panel_t, base); + esp_lcd_panel_io_handle_t io = st7796->io; + int command = 0; + if (invert_color_data) { + command = LCD_CMD_INVON; + } else { + command = LCD_CMD_INVOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_st7796_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) +{ + st7796_panel_t *st7796 = __containerof(panel, st7796_panel_t, base); + esp_lcd_panel_io_handle_t io = st7796->io; + if (mirror_x) { + st7796->madctl_val |= LCD_CMD_MX_BIT; + } else { + st7796->madctl_val &= ~LCD_CMD_MX_BIT; + } + if (mirror_y) { + st7796->madctl_val |= LCD_CMD_MY_BIT; + } else { + st7796->madctl_val &= ~LCD_CMD_MY_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + st7796->madctl_val + }, 1), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_st7796_swap_xy(esp_lcd_panel_t *panel, bool swap_axes) +{ + st7796_panel_t *st7796 = __containerof(panel, st7796_panel_t, base); + esp_lcd_panel_io_handle_t io = st7796->io; + if (swap_axes) { + st7796->madctl_val |= LCD_CMD_MV_BIT; + } else { + st7796->madctl_val &= ~LCD_CMD_MV_BIT; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + st7796->madctl_val + }, 1), TAG, "send command failed"); + return ESP_OK; +} + +static esp_err_t panel_st7796_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap) +{ + st7796_panel_t *st7796 = __containerof(panel, st7796_panel_t, base); + st7796->x_gap = x_gap; + st7796->y_gap = y_gap; + return ESP_OK; +} + +static esp_err_t panel_st7796_disp_on_off(esp_lcd_panel_t *panel, bool on_off) +{ + st7796_panel_t *st7796 = __containerof(panel, st7796_panel_t, base); + esp_lcd_panel_io_handle_t io = st7796->io; + int command = 0; + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + on_off = !on_off; +#endif + + if (on_off) { + command = LCD_CMD_DISPON; + } else { + command = LCD_CMD_DISPOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} diff --git a/Libraries/esp_lcd_st7796/esp_lcd_st7796_mipi.c b/Libraries/esp_lcd_st7796/esp_lcd_st7796_mipi.c new file mode 100644 index 00000000..75df50bb --- /dev/null +++ b/Libraries/esp_lcd_st7796/esp_lcd_st7796_mipi.c @@ -0,0 +1,296 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "soc/soc_caps.h" + +#if SOC_MIPI_DSI_SUPPORTED +#include "esp_check.h" +#include "esp_log.h" +#include "esp_lcd_panel_commands.h" +#include "esp_lcd_panel_interface.h" +#include "esp_lcd_panel_io.h" +#include "esp_lcd_mipi_dsi.h" +#include "esp_lcd_panel_vendor.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/gpio.h" + +#include "esp_lcd_st7796.h" +#include "esp_lcd_st7796_interface.h" + +typedef struct { + esp_lcd_panel_io_handle_t io; + int reset_gpio_num; + uint8_t madctl_val; // save current value of LCD_CMD_MADCTL register + uint8_t colmod_val; // save surrent value of LCD_CMD_COLMOD register + const st7796_lcd_init_cmd_t *init_cmds; + uint16_t init_cmds_size; + struct { + unsigned int reset_level: 1; + } flags; + // To save the original functions of MIPI DPI panel + esp_err_t (*del)(esp_lcd_panel_t *panel); + esp_err_t (*init)(esp_lcd_panel_t *panel); +} st7796_panel_t; + +static const char *TAG = "st7796_mipi"; + +static esp_err_t panel_st7796_del(esp_lcd_panel_t *panel); +static esp_err_t panel_st7796_init(esp_lcd_panel_t *panel); +static esp_err_t panel_st7796_reset(esp_lcd_panel_t *panel); +static esp_err_t panel_st7796_invert_color(esp_lcd_panel_t *panel, bool invert_color_data); +static esp_err_t panel_st7796_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y); +static esp_err_t panel_st7796_disp_on_off(esp_lcd_panel_t *panel, bool on_off); + +esp_err_t esp_lcd_new_panel_st7796_mipi(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, + esp_lcd_panel_handle_t *ret_panel) +{ + ESP_RETURN_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, TAG, "invalid arguments"); + st7796_vendor_config_t *vendor_config = (st7796_vendor_config_t *)panel_dev_config->vendor_config; + ESP_RETURN_ON_FALSE(vendor_config && vendor_config->mipi_config.dpi_config && vendor_config->mipi_config.dsi_bus, ESP_ERR_INVALID_ARG, TAG, + "invalid vendor config"); + + esp_err_t ret = ESP_OK; + st7796_panel_t *st7796 = (st7796_panel_t *)calloc(1, sizeof(st7796_panel_t)); + ESP_RETURN_ON_FALSE(st7796, ESP_ERR_NO_MEM, TAG, "no mem for st7796 panel"); + + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_config_t io_conf = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num, + }; + ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed"); + } + + switch (panel_dev_config->color_space) { + case LCD_RGB_ELEMENT_ORDER_RGB: + st7796->madctl_val = 0; + break; + case LCD_RGB_ELEMENT_ORDER_BGR: + st7796->madctl_val |= LCD_CMD_BGR_BIT; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space"); + break; + } + + switch (panel_dev_config->bits_per_pixel) { + case 16: // RGB565 + st7796->colmod_val = 0x55; + break; + case 18: // RGB666 + st7796->colmod_val = 0x66; + break; + case 24: // RGB888 + st7796->colmod_val = 0x77; + break; + default: + ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width"); + break; + } + + st7796->io = io; + st7796->init_cmds = vendor_config->init_cmds; + st7796->init_cmds_size = vendor_config->init_cmds_size; + st7796->reset_gpio_num = panel_dev_config->reset_gpio_num; + st7796->flags.reset_level = panel_dev_config->flags.reset_active_high; + + // Create MIPI DPI panel + esp_lcd_panel_handle_t panel_handle = NULL; + ESP_GOTO_ON_ERROR(esp_lcd_new_panel_dpi(vendor_config->mipi_config.dsi_bus, vendor_config->mipi_config.dpi_config, &panel_handle), err, TAG, + "create MIPI DPI panel failed"); + ESP_LOGD(TAG, "new MIPI DPI panel @%p", panel_handle); + + // Save the original functions of MIPI DPI panel + st7796->del = panel_handle->del; + st7796->init = panel_handle->init; + // Overwrite the functions of MIPI DPI panel + panel_handle->del = panel_st7796_del; + panel_handle->init = panel_st7796_init; + panel_handle->reset = panel_st7796_reset; + panel_handle->mirror = panel_st7796_mirror; + panel_handle->invert_color = panel_st7796_invert_color; + panel_handle->disp_on_off = panel_st7796_disp_on_off; + panel_handle->user_data = st7796; + *ret_panel = panel_handle; + ESP_LOGD(TAG, "new st7796 panel @%p", st7796); + + ESP_LOGI(TAG, "LCD panel create success, version: %d.%d.%d", ESP_LCD_ST7796_VER_MAJOR, ESP_LCD_ST7796_VER_MINOR, + ESP_LCD_ST7796_VER_PATCH); + + return ESP_OK; + +err: + if (st7796) { + if (panel_dev_config->reset_gpio_num >= 0) { + gpio_reset_pin(panel_dev_config->reset_gpio_num); + } + free(st7796); + } + return ret; +} + +static const st7796_lcd_init_cmd_t vendor_specific_init_default[] = { +// {cmd, { data }, data_size, delay_ms} + {0x11, (uint8_t []){0x00}, 0, 120}, + {0x36, (uint8_t []){0x48}, 1, 0}, + {0x3A, (uint8_t []){0x77}, 1, 0}, + {0xF0, (uint8_t []){0xC3}, 1, 0}, + {0xF0, (uint8_t []){0x96}, 1, 0}, + {0xB4, (uint8_t []){0x02}, 1, 0}, + {0xB7, (uint8_t []){0xC6}, 1, 0}, + {0xB6, (uint8_t []){0x2F}, 1, 0}, + {0x11, (uint8_t []){0xC0, 0xF0, 0x35}, 3, 0}, + {0xC1, (uint8_t []){0x15}, 1, 0}, + {0xC2, (uint8_t []){0xAF}, 1, 0}, + {0xC3, (uint8_t []){0x09}, 1, 0}, + {0xC5, (uint8_t []){0x22}, 1, 0}, + {0xC6, (uint8_t []){0x00}, 1, 0}, + {0x11, (uint8_t []){0xE8, 0x40, 0x8A, 0x00, 0x00, 0x29, 0x19, 0xA5, 0x33}, 9, 0}, + {0x11, (uint8_t []){0xE0, 0x70, 0x00, 0x05, 0x03, 0x02, 0x20, 0x29, 0x01, 0x45, 0x30, 0x09, 0x07, 0x22, 0x29}, 15, 0}, + {0x11, (uint8_t []){0xE1, 0x70, 0x0C, 0x10, 0x0F, 0x0E, 0x09, 0x35, 0x64, 0x48, 0x3A, 0x14, 0x13, 0x2E, 0x30}, 15, 0}, + {0x11, (uint8_t []){0xE0, 0x70, 0x04, 0x0A, 0x0B, 0x0A, 0x27, 0x31, 0x55, 0x47, 0x29, 0x13, 0x13, 0x29, 0x2D}, 15, 0}, + {0x11, (uint8_t []){0xE1, 0x70, 0x08, 0x0E, 0x09, 0x08, 0x04, 0x33, 0x32, 0x49, 0x36, 0x14, 0x14, 0x2A, 0x2F}, 15, 0}, + {0x21, (uint8_t []){0x00}, 0, 0}, + {0xF0, (uint8_t []){0xC3}, 1, 0}, + {0xF0, (uint8_t []){0x96}, 1, 120}, + {0xF0, (uint8_t []){0xC3}, 1, 0}, + {0x29, (uint8_t []){0x00}, 0, 0}, + {0x2C, (uint8_t []){0x00}, 0, 0}, + //============ Gamma END=========== +}; + +static esp_err_t panel_st7796_del(esp_lcd_panel_t *panel) +{ + st7796_panel_t *st7796 = (st7796_panel_t *)panel->user_data; + + if (st7796->reset_gpio_num >= 0) { + gpio_reset_pin(st7796->reset_gpio_num); + } + // Delete MIPI DPI panel + st7796->del(panel); + ESP_LOGD(TAG, "del st7796 panel @%p", st7796); + free(st7796); + + return ESP_OK; +} + +static esp_err_t panel_st7796_init(esp_lcd_panel_t *panel) +{ + st7796_panel_t *st7796 = (st7796_panel_t *)panel->user_data; + esp_lcd_panel_io_handle_t io = st7796->io; + const st7796_lcd_init_cmd_t *init_cmds = NULL; + uint16_t init_cmds_size = 0; + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) { + st7796->madctl_val, + }, 1), TAG, "send command failed"); + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]) { + st7796->colmod_val, + }, 1), TAG, "send command failed"); + + // vendor specific initialization, it can be different between manufacturers + // should consult the LCD supplier for initialization sequence code + if (st7796->init_cmds) { + init_cmds = st7796->init_cmds; + init_cmds_size = st7796->init_cmds_size; + } else { + init_cmds = vendor_specific_init_default; + init_cmds_size = sizeof(vendor_specific_init_default) / sizeof(st7796_lcd_init_cmd_t); + } + + for (int i = 0; i < init_cmds_size; i++) { + // Send command + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, init_cmds[i].cmd, init_cmds[i].data, init_cmds[i].data_bytes), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(init_cmds[i].delay_ms)); + } + ESP_LOGD(TAG, "send init commands success"); + + ESP_RETURN_ON_ERROR(st7796->init(panel), TAG, "init MIPI DPI panel failed"); + + return ESP_OK; +} + +static esp_err_t panel_st7796_reset(esp_lcd_panel_t *panel) +{ + st7796_panel_t *st7796 = (st7796_panel_t *)panel->user_data; + esp_lcd_panel_io_handle_t io = st7796->io; + + // Perform hardware reset + if (st7796->reset_gpio_num >= 0) { + gpio_set_level(st7796->reset_gpio_num, st7796->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(10)); + gpio_set_level(st7796->reset_gpio_num, !st7796->flags.reset_level); + vTaskDelay(pdMS_TO_TICKS(120)); + } else if (io) { // Perform software reset + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0), TAG, "send command failed"); + vTaskDelay(pdMS_TO_TICKS(120)); + } + + return ESP_OK; +} + +static esp_err_t panel_st7796_invert_color(esp_lcd_panel_t *panel, bool invert_color_data) +{ + st7796_panel_t *st7796 = (st7796_panel_t *)panel->user_data; + esp_lcd_panel_io_handle_t io = st7796->io; + uint8_t command = 0; + + ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_STATE, TAG, "invalid panel IO"); + + if (invert_color_data) { + command = LCD_CMD_INVON; + } else { + command = LCD_CMD_INVOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + + return ESP_OK; +} + +static esp_err_t panel_st7796_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y) +{ + st7796_panel_t *st7796 = (st7796_panel_t *)panel->user_data; + esp_lcd_panel_io_handle_t io = st7796->io; + uint8_t madctl_val = st7796->madctl_val; + + ESP_RETURN_ON_FALSE(io, ESP_ERR_INVALID_STATE, TAG, "invalid panel IO"); + + // Control mirror through LCD command + if (mirror_x) { + madctl_val |= LCD_CMD_MX_BIT; + } else { + madctl_val &= ~LCD_CMD_MX_BIT; + } + if (mirror_y) { + madctl_val |= LCD_CMD_MY_BIT; + } else { + madctl_val &= ~LCD_CMD_MY_BIT; + } + + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t []) { + madctl_val + }, 1), TAG, "send command failed"); + st7796->madctl_val = madctl_val; + + return ESP_OK; +} + +static esp_err_t panel_st7796_disp_on_off(esp_lcd_panel_t *panel, bool on_off) +{ + st7796_panel_t *st7796 = (st7796_panel_t *)panel->user_data; + esp_lcd_panel_io_handle_t io = st7796->io; + int command = 0; + + if (on_off) { + command = LCD_CMD_DISPON; + } else { + command = LCD_CMD_DISPOFF; + } + ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "send command failed"); + return ESP_OK; +} +#endif diff --git a/Libraries/esp_lcd_st7796/idf_component.yml b/Libraries/esp_lcd_st7796/idf_component.yml new file mode 100644 index 00000000..70f99d20 --- /dev/null +++ b/Libraries/esp_lcd_st7796/idf_component.yml @@ -0,0 +1,15 @@ +dependencies: + cmake_utilities: 0.* + idf: '>=4.4' +description: ESP LCD ST7796 driver (SPI && I80 && MIPI DSI) +repository: git://github.com/espressif/esp-bsp.git +repository_info: + commit_sha: 7e5759a5dcae75624e0c7abb8d8aef6b95e33b1f + path: components/lcd/esp_lcd_st7796 +targets: +- esp32 +- esp32s2 +- esp32s3 +- esp32p4 +url: https://github.com/espressif/esp-bsp/tree/master/components/lcd/esp_lcd_st7796 +version: 1.3.2 diff --git a/Libraries/esp_lcd_st7796/include/esp_lcd_st7796.h b/Libraries/esp_lcd_st7796/include/esp_lcd_st7796.h new file mode 100644 index 00000000..b0ac4563 --- /dev/null +++ b/Libraries/esp_lcd_st7796/include/esp_lcd_st7796.h @@ -0,0 +1,206 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/** + * @file + * @brief ESP LCD: ST7796 + */ + +#pragma once + +#include "hal/lcd_types.h" +#include "esp_lcd_panel_vendor.h" +#include "esp_idf_version.h" +#if SOC_MIPI_DSI_SUPPORTED +#include "esp_lcd_mipi_dsi.h" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LCD panel initialization commands. + * + */ +typedef struct { + int cmd; /*fgM7|srp*kNLt_yInlq>Dz!VX9O%EwtIzHC?+V`#=a;S?j7^LldQetwQ6< zFW@we@I&|^9FVwjLE?@d$92;ROMdDnul@M_uwBD2Hk)C_h<_U<7y zPs3Y!&9k>*VzNm>qh6mT;h05q7>@f9qZuhu3q^vO16du^2Yd!w!Dk&f5iMndlud?o zz{h+tBzxq}XJWHqoU&TV#0h!H$6-1(ttvFIbQ>Bvlh6oUKcJ59yWN1EwnabpJabVv zUyfDM#UnA0b<*X5&O1KNc6LQbX?#l~K4!ulsqNM3^n!L>pFEp?c-aVO$L-Qi&L71c_;IEquDV!~_am#iTR z6WR-tiGV1Id*+Tk52ZBs{8)xkTJp#yEywfPHCl5zwBB;^lwtD(Ug+}2tvxwd^RD9SPaRDptuzYe7P~r(ctz7RE9`uQ^%HJlP_nHz=hD z-Mq2L{ys5BVphen$Wv0j8w})ufNm4c>bI-7!9CIq%Q~juO+j0mO_64NU4vP_i!-w< zsOxuVySa4Fi!VU&s@tS&i^7$3qV0JJg!ha^8G@L~#g^qW0%XZ{ET z;fO8WWt${%nzfuOT6aC?RjvgInSDHJXsghYQRB-hw$XnYFo|b}nS~<@;|69Sk7z6R F?mr_gSNZ?| literal 0 HcmV?d00001 diff --git a/Libraries/esp_lcd_st7796/test_apps/main/CMakeLists.txt b/Libraries/esp_lcd_st7796/test_apps/main/CMakeLists.txt new file mode 100644 index 00000000..bcf584df --- /dev/null +++ b/Libraries/esp_lcd_st7796/test_apps/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS "." + WHOLE_ARCHIVE) \ No newline at end of file diff --git a/Libraries/esp_lcd_st7796/test_apps/main/idf_component.yml b/Libraries/esp_lcd_st7796/test_apps/main/idf_component.yml new file mode 100644 index 00000000..f756ba55 --- /dev/null +++ b/Libraries/esp_lcd_st7796/test_apps/main/idf_component.yml @@ -0,0 +1,6 @@ +## IDF Component Manager Manifest File +dependencies: + idf: ">=4.4" + esp_lcd_st7796: + version: "*" + override_path: "../../../esp_lcd_st7796" diff --git a/Libraries/esp_lcd_st7796/test_apps/main/test_app_main.c b/Libraries/esp_lcd_st7796/test_apps/main/test_app_main.c new file mode 100644 index 00000000..aae3c561 --- /dev/null +++ b/Libraries/esp_lcd_st7796/test_apps/main/test_app_main.c @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_test_utils_memory.h" +#include "esp_heap_caps.h" + +// Some resources are lazy allocated in the LCD driver, the threadhold is left for that case +#define TEST_MEMORY_LEAK_THRESHOLD (300) + +static size_t before_free_8bit; +static size_t before_free_32bit; + +void setUp(void) +{ + before_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + before_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); +} + +void tearDown(void) +{ + size_t after_free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT); + size_t after_free_32bit = heap_caps_get_free_size(MALLOC_CAP_32BIT); + unity_utils_check_leak(before_free_8bit, after_free_8bit, "8BIT", TEST_MEMORY_LEAK_THRESHOLD); + unity_utils_check_leak(before_free_32bit, after_free_32bit, "32BIT", TEST_MEMORY_LEAK_THRESHOLD); +} + +void app_main(void) +{ + /** + * __ _____ _____ _____ ___ __ + * / _\/__ \___ |___ / _ \ / /_ + * \ \ / /\/ / / / / (_) | '_ \ + * _\ \ / / / / / / \__, | (_) | + * \__/ \/ /_/ /_/ /_/ \___/ + */ + printf(" __ _____ _____ _____ ___ __\r\n"); + printf("/ _\\/__ \\___ |___ / _ \\ / /_\r\n"); + printf("\\ \\ / /\\/ / / / / (_) | '_ \\\r\n"); + printf("_\\ \\ / / / / / / \\__, | (_) |\r\n"); + printf("\\__/ \\/ /_/ /_/ /_/ \\___/\r\n"); + unity_run_menu(); +} diff --git a/Libraries/esp_lcd_st7796/test_apps/main/test_esp_lcd_st7796_general.c b/Libraries/esp_lcd_st7796/test_apps/main/test_esp_lcd_st7796_general.c new file mode 100644 index 00000000..dd879c12 --- /dev/null +++ b/Libraries/esp_lcd_st7796/test_apps/main/test_esp_lcd_st7796_general.c @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "soc/soc_caps.h" +#if SOC_LCD_I80_SUPPORTED +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "driver/gpio.h" +#include "soc/soc_caps.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_io.h" +#include "unity.h" +#include "unity_test_runner.h" + +#include "esp_lcd_st7796.h" + +#define TEST_LCD_H_RES (320) +#define TEST_LCD_V_RES (480) +#define TEST_LCD_BIT_PER_PIXEL (16) +#define TEST_LCD_DATA_WIDTH (8) + +#define TEST_PIN_NUM_LCD_CS (GPIO_NUM_17) +#define TEST_PIN_NUM_LCD_DC (GPIO_NUM_46) +#define TEST_PIN_NUM_LCD_WR (GPIO_NUM_3) +#define TEST_PIN_NUM_LCD_DATA0 (GPIO_NUM_9) +#define TEST_PIN_NUM_LCD_DATA1 (GPIO_NUM_12) +#define TEST_PIN_NUM_LCD_DATA2 (GPIO_NUM_11) +#define TEST_PIN_NUM_LCD_DATA3 (GPIO_NUM_14) +#define TEST_PIN_NUM_LCD_DATA4 (GPIO_NUM_13) +#if CONFIG_IDF_TARGET_ESP32S2 +#define TEST_PIN_NUM_LCD_DATA5 (GPIO_NUM_8) +#else +#define TEST_PIN_NUM_LCD_DATA5 (GPIO_NUM_47) +#endif +#define TEST_PIN_NUM_LCD_DATA6 (GPIO_NUM_21) +#define TEST_PIN_NUM_LCD_DATA7 (GPIO_NUM_45) +#define TEST_PIN_NUM_LCD_DATA8 (-1) +#define TEST_PIN_NUM_LCD_DATA9 (-1) +#define TEST_PIN_NUM_LCD_DATA10 (-1) +#define TEST_PIN_NUM_LCD_DATA11 (-1) +#define TEST_PIN_NUM_LCD_DATA12 (-1) +#define TEST_PIN_NUM_LCD_DATA13 (-1) +#define TEST_PIN_NUM_LCD_DATA14 (-1) +#define TEST_PIN_NUM_LCD_DATA15 (-1) +#define TEST_PIN_NUM_LCD_RST (GPIO_NUM_NC) + +#define TEST_DELAY_TIME_MS (3000) + +static char *TAG = "st7796_test"; +static SemaphoreHandle_t refresh_finish = NULL; + +IRAM_ATTR static bool test_notify_refresh_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx) +{ + BaseType_t need_yield = pdFALSE; + + xSemaphoreGiveFromISR(refresh_finish, &need_yield); + return (need_yield == pdTRUE); +} + +static void test_draw_bitmap(esp_lcd_panel_handle_t panel_handle) +{ + refresh_finish = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(refresh_finish); + + uint16_t row_line = TEST_LCD_V_RES / TEST_LCD_BIT_PER_PIXEL; + uint8_t byte_per_pixel = TEST_LCD_BIT_PER_PIXEL / 8; + uint8_t *color = (uint8_t *)heap_caps_calloc(1, row_line * TEST_LCD_H_RES * byte_per_pixel, MALLOC_CAP_DMA); + TEST_ASSERT_NOT_NULL(color); + + for (int j = 0; j < TEST_LCD_BIT_PER_PIXEL; j++) { + for (int i = 0; i < row_line * TEST_LCD_H_RES; i++) { + for (int k = 0; k < byte_per_pixel; k++) { + color[i * byte_per_pixel + k] = (BIT(j) >> (k * 8)) & 0xff; + } + } + TEST_ESP_OK(esp_lcd_panel_draw_bitmap(panel_handle, 0, j * row_line, TEST_LCD_H_RES, (j + 1) * row_line, color)); + xSemaphoreTake(refresh_finish, portMAX_DELAY); + } + free(color); +} + +TEST_CASE("test st7796 to draw color bar with I80 interface", "[st7796][i80]") +{ + ESP_LOGI(TAG, "Initialize Intel 8080 bus"); + esp_lcd_i80_bus_handle_t i80_bus = NULL; + esp_lcd_i80_bus_config_t bus_config = ST7796_PANEL_BUS_I80_CONFIG( + TEST_LCD_H_RES * TEST_LCD_V_RES * TEST_LCD_BIT_PER_PIXEL / 8, TEST_LCD_DATA_WIDTH, + TEST_PIN_NUM_LCD_DC, TEST_PIN_NUM_LCD_WR, + TEST_PIN_NUM_LCD_DATA0, TEST_PIN_NUM_LCD_DATA1, TEST_PIN_NUM_LCD_DATA2, TEST_PIN_NUM_LCD_DATA3, + TEST_PIN_NUM_LCD_DATA4, TEST_PIN_NUM_LCD_DATA5, TEST_PIN_NUM_LCD_DATA6, TEST_PIN_NUM_LCD_DATA7, + TEST_PIN_NUM_LCD_DATA8, TEST_PIN_NUM_LCD_DATA9, TEST_PIN_NUM_LCD_DATA10, TEST_PIN_NUM_LCD_DATA11, + TEST_PIN_NUM_LCD_DATA12, TEST_PIN_NUM_LCD_DATA13, TEST_PIN_NUM_LCD_DATA14, TEST_PIN_NUM_LCD_DATA15); + TEST_ESP_OK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); + + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_panel_io_handle_t io_handle = NULL; + esp_lcd_panel_io_i80_config_t io_config = ST7796_PANEL_IO_I80_CONFIG(TEST_PIN_NUM_LCD_CS, test_notify_refresh_ready, NULL); + TEST_ESP_OK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle)); + + ESP_LOGI(TAG, "Install ST7796 panel driver"); + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = TEST_PIN_NUM_LCD_RST, +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + .color_space = ESP_LCD_COLOR_SPACE_BGR, +#else + .rgb_endian = LCD_RGB_ENDIAN_BGR, +#endif + .bits_per_pixel = TEST_LCD_BIT_PER_PIXEL, + }; + esp_lcd_panel_handle_t panel_handle = NULL; + TEST_ESP_OK(esp_lcd_new_panel_st7796(io_handle, &panel_config, &panel_handle)); + TEST_ESP_OK(esp_lcd_panel_reset(panel_handle)); + TEST_ESP_OK(esp_lcd_panel_init(panel_handle)); +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + TEST_ESP_OK(esp_lcd_panel_disp_off(panel_handle, false)); +#else + TEST_ESP_OK(esp_lcd_panel_disp_on_off(panel_handle, true)); +#endif + + test_draw_bitmap(panel_handle); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_TIME_MS)); + + TEST_ESP_OK(esp_lcd_panel_del(panel_handle)); + TEST_ESP_OK(esp_lcd_panel_io_del(io_handle)); + TEST_ESP_OK(esp_lcd_del_i80_bus(i80_bus)); +} +#endif diff --git a/Libraries/esp_lcd_st7796/test_apps/main/test_esp_lcd_st7796_mipi.c b/Libraries/esp_lcd_st7796/test_apps/main/test_esp_lcd_st7796_mipi.c new file mode 100644 index 00000000..8ca84bd3 --- /dev/null +++ b/Libraries/esp_lcd_st7796/test_apps/main/test_esp_lcd_st7796_mipi.c @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "soc/soc_caps.h" + +#if SOC_MIPI_DSI_SUPPORTED + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "driver/i2c.h" +#include "driver/spi_master.h" +#include "driver/gpio.h" +#include "esp_heap_caps.h" +#include "esp_log.h" +#include "esp_timer.h" +#include "esp_lcd_panel_ops.h" +#include "esp_lcd_panel_io.h" +#include "esp_ldo_regulator.h" +#include "esp_dma_utils.h" +#include "unity.h" +#include "unity_test_runner.h" +#include "unity_test_utils_memory.h" +#include "esp_lcd_mipi_dsi.h" +#include "esp_lcd_st7796.h" + +#define TEST_LCD_H_RES (320) +#define TEST_LCD_V_RES (480) +#define TEST_LCD_BIT_PER_PIXEL (24) +#define TEST_PIN_NUM_LCD_RST (-1) +#define TEST_PIN_NUM_BK_LIGHT (-1) // set to -1 if not used +#define TEST_LCD_BK_LIGHT_ON_LEVEL (1) +#define TEST_LCD_BK_LIGHT_OFF_LEVEL !TEST_LCD_BK_LIGHT_ON_LEVEL + +#if TEST_LCD_BIT_PER_PIXEL == 24 +#define TEST_MIPI_DPI_PX_FORMAT (LCD_COLOR_PIXEL_FORMAT_RGB888) +#elif TEST_LCD_BIT_PER_PIXEL == 18 +#define TEST_MIPI_DPI_PX_FORMAT (LCD_COLOR_PIXEL_FORMAT_RGB666) +#elif TEST_LCD_BIT_PER_PIXEL == 16 +#define TEST_MIPI_DPI_PX_FORMAT (LCD_COLOR_PIXEL_FORMAT_RGB565) +#endif + +#define TEST_DELAY_TIME_MS (3000) + +#define TEST_MIPI_DSI_PHY_PWR_LDO_CHAN (3) +#define TEST_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV (2500) + +static char *TAG = "st7796_test"; +static esp_ldo_channel_handle_t ldo_mipi_phy = NULL; +static esp_lcd_panel_handle_t panel_handle = NULL; +static esp_lcd_dsi_bus_handle_t mipi_dsi_bus = NULL; +static esp_lcd_panel_io_handle_t mipi_dbi_io = NULL; +static SemaphoreHandle_t refresh_finish = NULL; + +IRAM_ATTR static bool test_notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) +{ + SemaphoreHandle_t refresh_finish = (SemaphoreHandle_t)user_ctx; + BaseType_t need_yield = pdFALSE; + + xSemaphoreGiveFromISR(refresh_finish, &need_yield); + + return (need_yield == pdTRUE); +} + +static void test_init_lcd(void) +{ +#if TEST_PIN_NUM_BK_LIGHT >= 0 + ESP_LOGI(TAG, "Turn on LCD backlight"); + gpio_config_t bk_gpio_config = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ULL << TEST_PIN_NUM_BK_LIGHT + }; + TEST_ESP_OK(gpio_config(&bk_gpio_config)); + TEST_ESP_OK(gpio_set_level(TEST_PIN_NUM_BK_LIGHT, TEST_LCD_BK_LIGHT_ON_LEVEL)); +#endif + + // Turn on the power for MIPI DSI PHY, so it can go from "No Power" state to "Shutdown" state +#ifdef TEST_MIPI_DSI_PHY_PWR_LDO_CHAN + ESP_LOGI(TAG, "MIPI DSI PHY Powered on"); + esp_ldo_channel_config_t ldo_mipi_phy_config = { + .chan_id = TEST_MIPI_DSI_PHY_PWR_LDO_CHAN, + .voltage_mv = TEST_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV, + }; + TEST_ESP_OK(esp_ldo_acquire_channel(&ldo_mipi_phy_config, &ldo_mipi_phy)); +#endif + + ESP_LOGI(TAG, "Initialize MIPI DSI bus"); + esp_lcd_dsi_bus_config_t bus_config = ST7796_PANEL_BUS_DSI_1CH_CONFIG(); + TEST_ESP_OK(esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus)); + + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_dbi_io_config_t dbi_config = ST7796_PANEL_IO_DBI_CONFIG(); + TEST_ESP_OK(esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &mipi_dbi_io)); + + ESP_LOGI(TAG, "Install LCD driver of st7796"); + esp_lcd_dpi_panel_config_t dpi_config = ST7796_320_480_PANEL_60HZ_DPI_CONFIG(TEST_MIPI_DPI_PX_FORMAT); + st7796_vendor_config_t vendor_config = { + .flags.use_mipi_interface = 1, + .mipi_config.dsi_bus = mipi_dsi_bus, + .mipi_config.dpi_config = &dpi_config, + }; + const esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = TEST_PIN_NUM_LCD_RST, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, + .bits_per_pixel = TEST_LCD_BIT_PER_PIXEL, + .vendor_config = &vendor_config, + }; + TEST_ESP_OK(esp_lcd_new_panel_st7796(mipi_dbi_io, &panel_config, &panel_handle)); + TEST_ESP_OK(esp_lcd_panel_reset(panel_handle)); + TEST_ESP_OK(esp_lcd_panel_init(panel_handle)); + TEST_ESP_OK(esp_lcd_panel_disp_on_off(panel_handle, true)); + + refresh_finish = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(refresh_finish); + esp_lcd_dpi_panel_event_callbacks_t cbs = { + .on_color_trans_done = test_notify_refresh_ready, + }; + TEST_ESP_OK(esp_lcd_dpi_panel_register_event_callbacks(panel_handle, &cbs, refresh_finish)); +} + +static void test_deinit_lcd(void) +{ + TEST_ESP_OK(esp_lcd_panel_del(panel_handle)); + TEST_ESP_OK(esp_lcd_panel_io_del(mipi_dbi_io)); + TEST_ESP_OK(esp_lcd_del_dsi_bus(mipi_dsi_bus)); + panel_handle = NULL; + mipi_dbi_io = NULL; + mipi_dsi_bus = NULL; + + if (ldo_mipi_phy) { + TEST_ESP_OK(esp_ldo_release_channel(ldo_mipi_phy)); + ldo_mipi_phy = NULL; + } + + vSemaphoreDelete(refresh_finish); + refresh_finish = NULL; + +#if TEST_PIN_NUM_BK_LIGHT >= 0 + TEST_ESP_OK(gpio_reset_pin(TEST_PIN_NUM_BK_LIGHT)); + TEST_ESP_OK(gpio_reset_pin(TEST_PIN_NUM_POWER)); +#endif +} + +static void test_draw_color_bar(esp_lcd_panel_handle_t panel_handle, uint16_t h_res, uint16_t v_res) +{ + uint8_t byte_per_pixel = (TEST_LCD_BIT_PER_PIXEL + 7) / 8; + uint16_t row_line = v_res / byte_per_pixel / 8; + uint8_t *color = (uint8_t *)heap_caps_calloc(1, row_line * h_res * byte_per_pixel, MALLOC_CAP_DMA); + + for (int j = 0; j < byte_per_pixel * 8; j++) { + for (int i = 0; i < row_line * h_res; i++) { + for (int k = 0; k < byte_per_pixel; k++) { + color[i * byte_per_pixel + k] = (BIT(j) >> (k * 8)) & 0xff; + } + } + TEST_ESP_OK(esp_lcd_panel_draw_bitmap(panel_handle, 0, j * row_line, h_res, (j + 1) * row_line, color)); + xSemaphoreTake(refresh_finish, portMAX_DELAY); + } + + uint16_t color_line = row_line * byte_per_pixel * 8; + uint16_t res_line = v_res - color_line; + if (res_line) { + for (int i = 0; i < res_line * h_res; i++) { + for (int k = 0; k < byte_per_pixel; k++) { + color[i * byte_per_pixel + k] = 0xff; + } + } + TEST_ESP_OK(esp_lcd_panel_draw_bitmap(panel_handle, 0, color_line, h_res, v_res, color)); + xSemaphoreTake(refresh_finish, portMAX_DELAY); + } + + free(color); +} + +TEST_CASE("test st7796 to draw pattern with MIPI interface", "[st7796][draw_pattern]") +{ + ESP_LOGI(TAG, "Initialize LCD device"); + test_init_lcd(); + + ESP_LOGI(TAG, "Show color bar pattern drawn by hardware"); + TEST_ESP_OK(esp_lcd_dpi_panel_set_pattern(panel_handle, MIPI_DSI_PATTERN_BAR_VERTICAL)); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_TIME_MS)); + TEST_ESP_OK(esp_lcd_dpi_panel_set_pattern(panel_handle, MIPI_DSI_PATTERN_BAR_HORIZONTAL)); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_TIME_MS)); + TEST_ESP_OK(esp_lcd_dpi_panel_set_pattern(panel_handle, MIPI_DSI_PATTERN_NONE)); + + ESP_LOGI(TAG, "Deinitialize LCD device"); + test_deinit_lcd(); +} + +TEST_CASE("test st7796 to draw color bar with MIPI interface", "[st7796][draw_color_bar]") +{ + ESP_LOGI(TAG, "Initialize LCD device"); + test_init_lcd(); + + ESP_LOGI(TAG, "Show color bar drawn by software"); + test_draw_color_bar(panel_handle, TEST_LCD_H_RES, TEST_LCD_V_RES); + vTaskDelay(pdMS_TO_TICKS(TEST_DELAY_TIME_MS)); + + ESP_LOGI(TAG, "Deinitialize LCD device"); + test_deinit_lcd(); +} + +TEST_CASE("test st7796 to rotate with MIPI interface", "[st7796][rotate]") +{ + ESP_LOGI(TAG, "Initialize LCD device"); + test_init_lcd(); + + ESP_LOGI(TAG, "Mirror the screen"); + for (size_t i = 0; i < 4; i++) { + TEST_ASSERT_NOT_EQUAL(esp_lcd_panel_mirror(panel_handle, i & 2, i & 1), ESP_FAIL); + + ESP_LOGI(TAG, "Mirror: %d", i); + test_draw_color_bar(panel_handle, TEST_LCD_H_RES, TEST_LCD_V_RES); + vTaskDelay(pdMS_TO_TICKS(1000)); + } + + ESP_LOGI(TAG, "Deinitialize LCD device"); + test_deinit_lcd(); +} + +#endif diff --git a/Libraries/esp_lcd_st7796/test_apps/sdkconfig.defaults b/Libraries/esp_lcd_st7796/test_apps/sdkconfig.defaults new file mode 100644 index 00000000..b5b36e5c --- /dev/null +++ b/Libraries/esp_lcd_st7796/test_apps/sdkconfig.defaults @@ -0,0 +1,12 @@ +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHFREQ_80M=y +CONFIG_COMPILER_OPTIMIZATION_PERF=y +CONFIG_SPIRAM=y +CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y +CONFIG_SPIRAM_RODATA=y +CONFIG_SPIRAM_SPEED_80M=y +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP_TASK_WDT_EN=n +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 diff --git a/Tactility/Include/Tactility/hal/i2c/I2cDevice.h b/Tactility/Include/Tactility/hal/i2c/I2cDevice.h index c7754999..7ffbf3ef 100644 --- a/Tactility/Include/Tactility/hal/i2c/I2cDevice.h +++ b/Tactility/Include/Tactility/hal/i2c/I2cDevice.h @@ -20,7 +20,11 @@ protected: static constexpr TickType_t DEFAULT_TIMEOUT = 1000 / portTICK_PERIOD_MS; + bool read(uint8_t* data, size_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT); + bool write(const uint8_t* data, uint16_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT); + bool writeRead(const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout = DEFAULT_TIMEOUT); bool readRegister8(uint8_t reg, uint8_t& result) const; + bool writeRegister(uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout = DEFAULT_TIMEOUT); bool writeRegister8(uint8_t reg, uint8_t value) const; bool readRegister12(uint8_t reg, float& out) const; bool readRegister14(uint8_t reg, float& out) const; diff --git a/Tactility/Source/hal/i2c/I2cDevice.cpp b/Tactility/Source/hal/i2c/I2cDevice.cpp index 4891e471..5a8cc6bb 100644 --- a/Tactility/Source/hal/i2c/I2cDevice.cpp +++ b/Tactility/Source/hal/i2c/I2cDevice.cpp @@ -4,6 +4,22 @@ namespace tt::hal::i2c { +bool I2cDevice::read(uint8_t* data, size_t dataSize, TickType_t timeout) { + return tt::hal::i2c::masterRead(port, address, data, dataSize, timeout); +} + +bool I2cDevice::write(const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + return tt::hal::i2c::masterWrite(port, address, data, dataSize, timeout); +} + +bool I2cDevice::writeRead(const uint8_t* writeData, size_t writeDataSize, uint8_t* readData, size_t readDataSize, TickType_t timeout) { + return masterWriteRead(port, address, writeData, writeDataSize, readData, readDataSize, timeout); +} + +bool I2cDevice::writeRegister(uint8_t reg, const uint8_t* data, uint16_t dataSize, TickType_t timeout) { + return masterWriteRegister(port, address, reg, data, dataSize, timeout); +} + bool I2cDevice::readRegister12(uint8_t reg, float& out) const { std::uint8_t data[2] = {0}; if (tt::hal::i2c::masterReadRegister(port, address, reg, data, 2, DEFAULT_TIMEOUT)) { diff --git a/Tactility/Source/service/gui/GuiDraw.cpp b/Tactility/Source/service/gui/GuiDraw.cpp index ed52c19e..85df8cde 100644 --- a/Tactility/Source/service/gui/GuiDraw.cpp +++ b/Tactility/Source/service/gui/GuiDraw.cpp @@ -28,6 +28,34 @@ static lv_obj_t* createAppViews(Gui* gui, lv_obj_t* parent) { return child_container; } +lv_obj_tree_walk_res_t add_to_group(lv_obj_t * obj, void * user_data) +{ + lv_group_t *group = (lv_group_t*)user_data; + TT_LOG_I(TAG, "walk"); + + if (lv_obj_check_type(obj, &lv_button_class) || + lv_obj_check_type(obj, &lv_list_button_class) || + lv_obj_check_type(obj, &lv_textarea_class) || + lv_obj_check_type(obj, &lv_dropdown_class)) { + lv_group_add_obj(group, obj); + } + return LV_OBJ_TREE_WALK_NEXT; +} + +static void add_child_to_default_group(lv_event_t * e) +{ + lv_obj_t * child = (lv_obj_t *)lv_event_get_param(e); + + TT_LOG_I(TAG, "added %X", (unsigned)lv_obj_get_class(child)); + if (lv_obj_check_type(child, &lv_list_text_class)) { + lv_obj_add_flag(child, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_flag(child, LV_OBJ_FLAG_CLICK_FOCUSABLE); + lv_obj_add_flag(child, LV_OBJ_FLAG_SCROLL_ON_FOCUS); + lv_group_add_obj(lv_group_get_default(), child); + TT_LOG_I(TAG, "listitem set"); + } +} + void redraw(Gui* gui) { assert(gui); @@ -39,6 +67,20 @@ void redraw(Gui* gui) { if (gui->appToRender != nullptr) { + + lv_group_t *group = lv_group_create(); + auto* indev = lv_indev_get_next(nullptr); + while(indev) { + TT_LOG_I(TAG, "Added indev %X", (unsigned)indev); + lv_indev_set_group(indev, group); + indev = lv_indev_get_next(indev); + } + lv_group_set_default(group); + + //lv_obj_add_flag(gui->appRootWidget, LV_OBJ_FLAG_SCROLL_CHAIN_VER); + //lv_gridnav_add(gui->appRootWidget, (lv_gridnav_ctrl_t)(LV_GRIDNAV_CTRL_ROLLOVER | LV_GRIDNAV_CTRL_SCROLL_FIRST | LV_GRIDNAV_CTRL_VERTICAL_MOVE_ONLY)); + //lv_group_add_obj(group, gui->appRootWidget); + app::Flags flags = std::static_pointer_cast(gui->appToRender)->getFlags(); if (flags.showStatusbar) { lv_obj_remove_flag(gui->statusbarWidget, LV_OBJ_FLAG_HIDDEN); @@ -48,6 +90,11 @@ void redraw(Gui* gui) { lv_obj_t* container = createAppViews(gui, gui->appRootWidget); gui->appToRender->getApp()->onShow(*gui->appToRender, container); + + //lv_obj_tree_walk(container, add_to_group, group); + + //lv_obj_add_event_cb(gui->appRootWidget, add_child_to_default_group, LV_EVENT_CHILD_CREATED, NULL); + } else { TT_LOG_W(TAG, "nothing to draw"); } diff --git a/sdkconfig.board.lilygo-tlora-pager b/sdkconfig.board.lilygo-tlora-pager new file mode 100644 index 00000000..16fb1e30 --- /dev/null +++ b/sdkconfig.board.lilygo-tlora-pager @@ -0,0 +1,56 @@ +# Software defaults +# Increase stack size for WiFi (fixes crash after scan) +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=3072 +CONFIG_LV_FONT_MONTSERRAT_14=y +CONFIG_LV_FONT_MONTSERRAT_18=y +CONFIG_LV_USE_USER_DATA=y +CONFIG_LV_USE_FS_STDIO=y +CONFIG_LV_FS_STDIO_LETTER=65 +CONFIG_LV_FS_STDIO_PATH="" +CONFIG_LV_FS_STDIO_CACHE_SIZE=4096 +CONFIG_LV_USE_LODEPNG=y +CONFIG_LV_USE_BUILTIN_MALLOC=n +CONFIG_LV_USE_CLIB_MALLOC=y +CONFIG_LV_USE_MSGBOX=n +CONFIG_LV_USE_SPINNER=n +CONFIG_LV_USE_WIN=n +CONFIG_LV_USE_SNAPSHOT=y +CONFIG_FREERTOS_HZ=1000 +CONFIG_FREERTOS_TASK_NOTIFICATION_ARRAY_ENTRIES=2 +CONFIG_FREERTOS_SMP=n +CONFIG_FREERTOS_UNICORE=n +CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096 +CONFIG_FREERTOS_USE_TRACE_FACILITY=y +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_VOLUME_COUNT=3 + +# Hardware: Main +CONFIG_TT_BOARD_LILYGO_TLORA_PAGER=y +CONFIG_TT_BOARD_NAME="LilyGo T-Lora Pager" +CONFIG_TT_BOARD_ID="lilygo-tlora-pager" +CONFIG_IDF_EXPERIMENTAL_FEATURES=y +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y +CONFIG_ESP32_DEFAULT_CPU_FREQ_240=y +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_FLASHMODE_DIO=y +# Hardware: SPI RAM +CONFIG_ESP32S3_SPIRAM_SUPPORT=y +#CONFIG_SPIRAM_MODE_OCT=y +CONFIG_SPIRAM_TYPE_AUTO=y +CONFIG_SPIRAM_SPEED_120M=y +#CONFIG_SPIRAM_BOOT_INIT=y +CONFIG_SPIRAM_USE_MALLOC=y +CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y +# SPI Flash (can set back to 80MHz after ESP-IDF bug is resolved) +CONFIG_ESPTOOLPY_FLASHFREQ_40M=y +# LVGL +CONFIG_LV_DPI_DEF=90 +CONFIG_LV_DISP_DEF_REFR_PERIOD=10 +CONFIG_LV_THEME_DEFAULT_DARK=y +# USB +CONFIG_TINYUSB_MSC_ENABLED=y +CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard"