diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 650584b1..a1d37bda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -66,7 +66,8 @@ jobs: { id: waveshare-s3-lcd-13, arch: esp32s3 }, { id: waveshare-s3-touch-lcd-128, arch: esp32s3 }, { id: waveshare-s3-touch-lcd-147, arch: esp32s3 }, - { id: waveshare-s3-touch-lcd-43, arch: esp32s3 } + { id: waveshare-s3-touch-lcd-43, arch: esp32s3 }, + { id: wireless-tag-wt32-sc01-plus, arch: esp32s3 } ] runs-on: ubuntu-latest needs: [ BuildSdk ] @@ -114,4 +115,4 @@ jobs: CDN_TOKEN_VALUE: ${{ secrets.CDN_TOKEN_VALUE }} uses: ./.github/actions/publish-firmware with: - cdn_version: stable + cdn_version: stable \ No newline at end of file diff --git a/Devices/wireless-tag-wt32-sc01-plus/CMakeLists.txt b/Devices/wireless-tag-wt32-sc01-plus/CMakeLists.txt new file mode 100644 index 00000000..6a7bbba6 --- /dev/null +++ b/Devices/wireless-tag-wt32-sc01-plus/CMakeLists.txt @@ -0,0 +1,7 @@ +file(GLOB_RECURSE SOURCE_FILES Source/*.c*) + +idf_component_register( + SRCS ${SOURCE_FILES} + INCLUDE_DIRS "Source" + REQUIRES Tactility ST7796-i8080 PwmBacklight FT6x36 driver vfs fatfs +) diff --git a/Devices/wireless-tag-wt32-sc01-plus/Source/Configuration.cpp b/Devices/wireless-tag-wt32-sc01-plus/Source/Configuration.cpp new file mode 100644 index 00000000..66f1c6e9 --- /dev/null +++ b/Devices/wireless-tag-wt32-sc01-plus/Source/Configuration.cpp @@ -0,0 +1,69 @@ +#include "devices/Display.h" +#include "devices/SdCard.h" + +#include +#include +#include + +using namespace tt::hal; + +static DeviceVector createDevices() { + return { + createDisplay(), + createSdCard() + }; +} + +static bool initBoot() { + return driver::pwmbacklight::init(GPIO_NUM_45); +} + +extern const Configuration hardwareConfiguration = { + .initBoot = initBoot, + .createDevices = createDevices, + .i2c = { + i2c::Configuration { + .name = "Internal", + .port = I2C_NUM_0, + .initMode = i2c::InitMode::ByTactility, + .isMutable = false, + .config = (i2c_config_t) { + .mode = I2C_MODE_MASTER, + .sda_io_num = GPIO_NUM_6, + .scl_io_num = GPIO_NUM_5, + .sda_pullup_en = true, + .scl_pullup_en = true, + .master = { + .clk_speed = 400000 + }, + .clk_flags = 0 + } + } + }, + .spi { + // SD card + spi::Configuration { + .device = SPI2_HOST, + .dma = SPI_DMA_CH_AUTO, + .config = { + .mosi_io_num = GPIO_NUM_40, + .miso_io_num = GPIO_NUM_38, + .sclk_io_num = GPIO_NUM_39, + .quadwp_io_num = GPIO_NUM_NC, + .quadhd_io_num = GPIO_NUM_NC, + .data4_io_num = GPIO_NUM_NC, + .data5_io_num = GPIO_NUM_NC, + .data6_io_num = GPIO_NUM_NC, + .data7_io_num = GPIO_NUM_NC, + .data_io_default_level = false, + .max_transfer_sz = 32768, + .flags = 0, + .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO, + .intr_flags = 0 + }, + .initMode = spi::InitMode::ByTactility, + .isMutable = false, + .lock = nullptr // No custom lock needed + } + } +}; diff --git a/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.cpp b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.cpp new file mode 100644 index 00000000..f92c13d0 --- /dev/null +++ b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.cpp @@ -0,0 +1,41 @@ +#include "Display.h" + +#include +#include +#include + +constexpr auto LCD_HORIZONTAL_RESOLUTION = 320; +constexpr auto LCD_VERTICAL_RESOLUTION = 480; + +std::shared_ptr createTouch() { + auto configuration = std::make_unique( + I2C_NUM_0, + GPIO_NUM_7, + LCD_HORIZONTAL_RESOLUTION, + LCD_VERTICAL_RESOLUTION + ); + + auto touch = std::make_shared(std::move(configuration)); + return std::static_pointer_cast(touch); +} + +std::shared_ptr createDisplay() { + auto configuration = St7796i8080Display::Configuration( + GPIO_NUM_NC, //CS + GPIO_NUM_0, //RS + GPIO_NUM_47, //WR + { GPIO_NUM_9, GPIO_NUM_46, GPIO_NUM_3, GPIO_NUM_8, + GPIO_NUM_18, GPIO_NUM_17, GPIO_NUM_16, GPIO_NUM_15 }, // D0..D7 + GPIO_NUM_4, //RESET + GPIO_NUM_45 //BL + ); + + configuration.mirrorX = true; + configuration.horizontalResolution = LCD_HORIZONTAL_RESOLUTION; + configuration.verticalResolution = LCD_VERTICAL_RESOLUTION; + configuration.touch = createTouch(); + configuration.backlightDutyFunction = driver::pwmbacklight::setBacklightDuty; + + auto display = std::make_shared(configuration); + return display; +} diff --git a/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.h b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.h new file mode 100644 index 00000000..c82ab445 --- /dev/null +++ b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/Display.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +std::shared_ptr createDisplay(); diff --git a/Devices/wireless-tag-wt32-sc01-plus/Source/devices/SdCard.cpp b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/SdCard.cpp new file mode 100644 index 00000000..425caf7c --- /dev/null +++ b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/SdCard.cpp @@ -0,0 +1,23 @@ +#include "SdCard.h" + +#include +#include + +using tt::hal::sdcard::SpiSdCardDevice; + +std::shared_ptr createSdCard() { + auto configuration = std::make_unique( + GPIO_NUM_41, + GPIO_NUM_NC, + GPIO_NUM_NC, + GPIO_NUM_NC, + SdCardDevice::MountBehaviour::AtBoot, + tt::lvgl::getSyncLock(), + std::vector(), + SPI2_HOST + ); + + return std::make_shared( + std::move(configuration) + ); +} diff --git a/Devices/wireless-tag-wt32-sc01-plus/Source/devices/SdCard.h b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/SdCard.h new file mode 100644 index 00000000..b38d9489 --- /dev/null +++ b/Devices/wireless-tag-wt32-sc01-plus/Source/devices/SdCard.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Tactility/hal/sdcard/SdCardDevice.h" + +using tt::hal::sdcard::SdCardDevice; + +std::shared_ptr createSdCard(); diff --git a/Devices/wireless-tag-wt32-sc01-plus/device.properties b/Devices/wireless-tag-wt32-sc01-plus/device.properties new file mode 100644 index 00000000..73214d21 --- /dev/null +++ b/Devices/wireless-tag-wt32-sc01-plus/device.properties @@ -0,0 +1,20 @@ +[general] +vendor=Wireless Tag +name=WT32 SC01 Plus + +[hardware] +target=ESP32S3 +flashSize=16MB +spiRam=true +spiRamMode=QUAD +spiRamSpeed=80M +tinyUsb=true +esptoolFlashFreq=80M + +[display] +size=3.5" +shape=rectangle +dpi=139 + +[lvgl] +colorDepth=16 diff --git a/Drivers/ST7796-i8080/CMakeLists.txt b/Drivers/ST7796-i8080/CMakeLists.txt new file mode 100644 index 00000000..b57e8985 --- /dev/null +++ b/Drivers/ST7796-i8080/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRC_DIRS "Source" + INCLUDE_DIRS "Source" + REQUIRES Tactility EspLcdCompat esp_lcd_st7796 driver +) \ No newline at end of file diff --git a/Drivers/ST7796-i8080/README.md b/Drivers/ST7796-i8080/README.md new file mode 100644 index 00000000..2d79eb72 --- /dev/null +++ b/Drivers/ST7796-i8080/README.md @@ -0,0 +1,3 @@ +# ST7796 + +A basic ESP32 LVGL driver for ST7796 parallel i8080 displays. \ No newline at end of file diff --git a/Drivers/ST7796-i8080/Source/St7796i8080Display.cpp b/Drivers/ST7796-i8080/Source/St7796i8080Display.cpp new file mode 100644 index 00000000..bd0784e8 --- /dev/null +++ b/Drivers/ST7796-i8080/Source/St7796i8080Display.cpp @@ -0,0 +1,275 @@ +#include "St7796i8080Display.h" +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr auto TAG = "St7796i8080Display"; +static St7796i8080Display* g_display_instance = nullptr; + +St7796i8080Display::St7796i8080Display(const Configuration& config) + : configuration(config), lock(std::make_shared()) { + + // Validate configuration + if (!configuration.isValid()) { + TT_LOG_E(TAG, "Invalid configuration: resolution must be set"); + return; + } +} + +bool St7796i8080Display::createI80Bus() { + TT_LOG_I(TAG, "Creating I80 bus"); + + // Create I80 bus configuration + esp_lcd_i80_bus_config_t bus_cfg = { + .dc_gpio_num = configuration.dcPin, + .wr_gpio_num = configuration.wrPin, + .clk_src = LCD_CLK_SRC_PLL160M, + .data_gpio_nums = { + configuration.dataPins[0], configuration.dataPins[1], + configuration.dataPins[2], configuration.dataPins[3], + configuration.dataPins[4], configuration.dataPins[5], + configuration.dataPins[6], configuration.dataPins[7], + }, + .bus_width = configuration.busWidth, + .max_transfer_bytes = configuration.bufferSize * sizeof(uint16_t), + .psram_trans_align = 64, + .sram_trans_align = 4 + }; + + if (esp_lcd_new_i80_bus(&bus_cfg, &i80BusHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to create I80 bus"); + return false; + } + + return true; +} + +bool St7796i8080Display::createPanelIO() { + TT_LOG_I(TAG, "Creating panel IO"); + + // Create panel IO + esp_lcd_panel_io_i80_config_t io_cfg = { + .cs_gpio_num = configuration.csPin, + .pclk_hz = configuration.pixelClockFrequency, + .trans_queue_depth = configuration.transactionQueueDepth, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .lcd_cmd_bits = configuration.lcdCmdBits, + .lcd_param_bits = configuration.lcdParamBits, + .dc_levels = { + .dc_idle_level = configuration.dcLevels.dcIdleLevel, + .dc_cmd_level = configuration.dcLevels.dcCmdLevel, + .dc_dummy_level = configuration.dcLevels.dcDummyLevel, + .dc_data_level = configuration.dcLevels.dcDataLevel, + }, + .flags = { + .cs_active_high = configuration.flags.csActiveHigh, + .reverse_color_bits = configuration.flags.reverseColorBits, + .swap_color_bytes = configuration.flags.swapColorBytes, + .pclk_active_neg = configuration.flags.pclkActiveNeg, + .pclk_idle_low = configuration.flags.pclkIdleLow + } + }; + + if (esp_lcd_new_panel_io_i80(i80BusHandle, &io_cfg, &ioHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to create panel"); + return false; + } + + return true; +} + +bool St7796i8080Display::createPanel() { + TT_LOG_I(TAG, "Configuring panel"); + + // Create ST7796 panel + esp_lcd_panel_dev_config_t panel_config = { + .reset_gpio_num = configuration.resetPin, + .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR, + .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; + } + + // Reset panel + if (esp_lcd_panel_reset(panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to reset panel"); + return false; + } + + // Initialize panel + if (esp_lcd_panel_init(panelHandle) != ESP_OK) { + TT_LOG_E(TAG, "Failed to init panel"); + return false; + } + + // Set swap XY + if (esp_lcd_panel_swap_xy(panelHandle, configuration.swapXY) != ESP_OK) { + TT_LOG_E(TAG, "Failed to swap XY "); + return false; + } + + // Set mirror + if (esp_lcd_panel_mirror(panelHandle, configuration.mirrorX, configuration.mirrorY) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel to mirror"); + return false; + } + + // Set inversion + if (esp_lcd_panel_invert_color(panelHandle, configuration.invertColor) != ESP_OK) { + TT_LOG_E(TAG, "Failed to set panel to invert"); + return false; + } + + // Turn on display + if (esp_lcd_panel_disp_on_off(panelHandle, true) != ESP_OK) { + TT_LOG_E(TAG, "Failed to turn display on"); + return false; + } + + return true; +} + +bool St7796i8080Display::start() { + TT_LOG_I(TAG, "Initializing I8080 ST7796 Display hardware..."); + + // Calculate buffer size if needed + configuration.calculateBufferSize(); + + // Allocate buffer + size_t buffer_size = configuration.bufferSize * LV_COLOR_FORMAT_GET_SIZE(LV_COLOR_FORMAT_RGB565); + displayBuffer = (uint8_t*)heap_caps_malloc(buffer_size, MALLOC_CAP_DMA); + if (!displayBuffer) { + TT_LOG_E(TAG, "Failed to allocate display buffer"); + return false; + } + + // Create I80 bus + if (!createI80Bus()) { + stop(); + return false; + } + + // Create panel IO + if (!createPanelIO()) { + stop(); + return false; + } + + // Create panel + if (!createPanel()) { + stop(); + return false; + } + + TT_LOG_I(TAG, "Display hardware initialized"); + return true; +} + +bool St7796i8080Display::stop() { + // Turn off display + if (panelHandle) { + esp_lcd_panel_disp_on_off(panelHandle, false); + } + + // Destroy in reverse order: panel, IO, bus + if (panelHandle) { + esp_lcd_panel_del(panelHandle); + panelHandle = nullptr; + } + if (ioHandle) { + esp_lcd_panel_io_del(ioHandle); + ioHandle = nullptr; + } + if (i80BusHandle) { + esp_lcd_del_i80_bus(i80BusHandle); + i80BusHandle = nullptr; + } + + // Free buffer 1 + if (displayBuffer) { + heap_caps_free(displayBuffer); + displayBuffer = nullptr; + } + + // Turn off backlight + if (configuration.backlightDutyFunction) { + configuration.backlightDutyFunction(0); + } + + return true; +} + +bool St7796i8080Display::startLvgl() { + TT_LOG_I(TAG, "Initializing LVGL for ST7796 display"); + + // Don't reinitialize hardware if it's already done + if (!ioHandle) { + TT_LOG_I(TAG, "Hardware not initialized, calling start()"); + if (!start()) { + TT_LOG_E(TAG, "Hardware initialization failed"); + return false; + } + } else { + TT_LOG_I(TAG, "Hardware already initialized, skipping"); + } + + // Create LVGL display using lvgl_port + lvgl_port_display_cfg_t display_cfg = { + .io_handle = ioHandle, + .panel_handle = panelHandle, + .control_handle = nullptr, + .buffer_size = configuration.bufferSize, + .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_RGB565, + .flags = { + .buff_dma = true, + .buff_spiram = false, + .sw_rotate = false, + .swap_bytes = false, + .full_refresh = false, + .direct_mode = false + } + }; + + // Create the LVGL display + lvglDisplay = lvgl_port_add_disp(&display_cfg); + if (!lvglDisplay) { + TT_LOG_E(TAG, "Failed to create LVGL display"); + return false; + } + + g_display_instance = this; + TT_LOG_I(TAG, "LVGL display created successfully"); + return true; +} + +bool St7796i8080Display::stopLvgl() { + if (lvglDisplay) { + lvgl_port_remove_disp(lvglDisplay); + lvglDisplay = nullptr; + } + return true; +} \ No newline at end of file diff --git a/Drivers/ST7796-i8080/Source/St7796i8080Display.h b/Drivers/ST7796-i8080/Source/St7796i8080Display.h new file mode 100644 index 00000000..786a7bab --- /dev/null +++ b/Drivers/ST7796-i8080/Source/St7796i8080Display.h @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class St7796i8080Display : public tt::hal::display::DisplayDevice { +public: + struct Configuration { + // Pin configuration + gpio_num_t csPin; + gpio_num_t dcPin; + gpio_num_t wrPin; + std::array dataPins; + gpio_num_t resetPin; + gpio_num_t backlightPin; + + // Display resolution configuration + uint16_t horizontalResolution; + uint16_t verticalResolution; + + // Bus configuration + unsigned int pixelClockFrequency = 20 * 1000 * 1000; // 20MHz default + size_t busWidth = 8; // 8-bit bus + size_t transactionQueueDepth = 10; + size_t bufferSize = 0; // Will be calculated if 0 + + // LCD command/parameter configuration + int lcdCmdBits = 8; + int lcdParamBits = 8; + + // DC line level configuration + struct { + bool dcIdleLevel = 0; + bool dcCmdLevel = 0; + bool dcDummyLevel = 0; + bool dcDataLevel = 1; + } dcLevels; + + // Bus flags + struct { + bool csActiveHigh = false; + bool reverseColorBits = false; + bool swapColorBytes = true; + bool pclkActiveNeg = false; + bool pclkIdleLow = false; + } flags; + + // Display configuration + bool swapXY = false; + bool mirrorX = false; + bool mirrorY = false; + bool invertColor = true; + + // Additional features + std::shared_ptr touch = nullptr; + std::function backlightDutyFunction = nullptr; + + // Basic constructor - requires resolution to be set separately + Configuration(gpio_num_t cs, gpio_num_t dc, gpio_num_t wr, + std::array data, gpio_num_t rst, gpio_num_t bl) + : csPin(cs), dcPin(dc), wrPin(wr), dataPins(data), resetPin(rst), backlightPin(bl), + horizontalResolution(0), verticalResolution(0) {} // Initialize to 0 + + // Method to calculate buffer size after resolution is set + void calculateBufferSize() { + if (bufferSize == 0 && horizontalResolution > 0 && verticalResolution > 0) { + bufferSize = horizontalResolution * verticalResolution / 10; + } + } + + // Validation method + bool isValid() const { + return horizontalResolution > 0 && verticalResolution > 0; + } + }; + +private: + Configuration configuration; + esp_lcd_i80_bus_handle_t i80BusHandle = nullptr; + esp_lcd_panel_io_handle_t ioHandle = nullptr; + esp_lcd_panel_handle_t panelHandle = nullptr; + lv_display_t* lvglDisplay = nullptr; + std::shared_ptr lock; + uint8_t* displayBuffer = nullptr; + + // Internal initialization methods + bool createI80Bus(); + bool createPanelIO(); + bool createPanel(); + +public: + explicit St7796i8080Display(const Configuration& config); + lv_display_t* getLvglDisplay() const override { return lvglDisplay; } + std::string getName() const override { return "I8080 ST7796"; } + std::string getDescription() const override { return "I8080-based ST7796 display"; } + + // Lifecycle + bool start() override; + bool stop() override; + bool startLvgl() override; + bool stopLvgl() override; + + // Capabilities + bool supportsLvgl() const override { return true; } + bool supportsDisplayDriver() const override { return false; } + bool supportsBacklightDuty() const override { return configuration.backlightDutyFunction != nullptr; } + + // Touch and backlight + std::shared_ptr getTouchDevice() override { return configuration.touch; } + std::shared_ptr getDisplayDriver() override { return nullptr; } + + void setBacklightDuty(uint8_t backlightDuty) override { + if (configuration.backlightDutyFunction != nullptr) { + configuration.backlightDutyFunction(backlightDuty); + } + } + + // Hardware access methods + esp_lcd_panel_io_handle_t getIoHandle() const { return ioHandle; } + esp_lcd_panel_handle_t getPanelHandle() const { return panelHandle; } + + // Resolution access methods + uint16_t getHorizontalResolution() const { return configuration.horizontalResolution; } + uint16_t getVerticalResolution() const { return configuration.verticalResolution; } +}; + +// Factory function for registration +std::shared_ptr createDisplay(); \ No newline at end of file diff --git a/Firmware/Kconfig b/Firmware/Kconfig index 871c980d..64f5b5d4 100644 --- a/Firmware/Kconfig +++ b/Firmware/Kconfig @@ -79,6 +79,8 @@ menu "Tactility App" bool "Waveshare ESP32 S3 Touch LCD 1.28" config TT_DEVICE_WAVESHARE_S3_LCD_13 bool "Waveshare ESP32 S3 LCD 1.3" + config TT_DEVICE_WIRELESS_TAG_WT32_SC01_PLUS + bool "Wireless Tag WT32-SC01 Plus" help Select a device.