T-Deck Pro work in progress

This commit is contained in:
Ken Van Hoeylandt 2025-05-24 17:29:08 +02:00
parent 74eb830870
commit 338e395cb6
25 changed files with 1526 additions and 42 deletions

View File

@ -126,6 +126,24 @@ jobs:
with:
board_id: lilygo-tdeck
arch: esp32s3
lilygo-tdeck-plus:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: "Build"
uses: ./.github/actions/build-firmware
with:
board_id: lilygo-tdeck-plus
arch: esp32s3
lilygo-tdeck-pro:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: "Build"
uses: ./.github/actions/build-firmware
with:
board_id: lilygo-tdeck-pro
arch: esp32s3
m5stack-core2:
runs-on: ubuntu-latest
steps:

View File

@ -37,6 +37,10 @@ menu "Tactility App"
bool "Elecrow CrowPanel Basic 5.0"
config TT_BOARD_LILYGO_TDECK
bool "LilyGo T-Deck"
config TT_BOARD_LILYGO_TDECK_PLUS
bool "LilyGo T-Deck Plus"
config TT_BOARD_LILYGO_TDECK_PRO
bool "LilyGo T-Deck Pro"
config TT_BOARD_M5STACK_CORE2
bool "M5Stack Core2"
config TT_BOARD_M5STACK_CORES3

View File

@ -4,7 +4,7 @@
#include <sdkconfig.h>
// Supported hardware:
#if defined(CONFIG_TT_BOARD_LILYGO_TDECK)
#if defined(CONFIG_TT_BOARD_LILYGO_TDECK) || defined(CONFIG_TT_BOARD_LILYGO_TDECK_PLUS) || defined(CONFIG_TT_BOARD_LILYGO_TDECK_PRO)
#include "LilygoTdeck.h"
#define TT_BOARD_HARDWARE &lilygo_tdeck
#elif defined(CONFIG_TT_BOARD_CYD_2432S024C)
@ -13,22 +13,22 @@
#elif defined(CONFIG_TT_BOARD_CYD_2432S032C)
#include "CYD2432S032C.h"
#define TT_BOARD_HARDWARE &cyd_2432S032c_config
#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_28))
#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_28)
#define TT_BOARD_HARDWARE &crowpanel_advance_28
#include "CrowPanelAdvance28.h"
#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_35))
#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_35)
#define TT_BOARD_HARDWARE &crowpanel_advance_35
#include "CrowPanelAdvance35.h"
#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_50))
#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_ADVANCE_50)
#define TT_BOARD_HARDWARE &crowpanel_advance_50
#include "CrowPanelAdvance50.h"
#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_28))
#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_28)
#define TT_BOARD_HARDWARE &crowpanel_basic_28
#include "CrowPanelBasic28.h"
#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_35))
#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_35)
#define TT_BOARD_HARDWARE &crowpanel_basic_35
#include "CrowPanelBasic35.h"
#elif (defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_50))
#elif defined(CONFIG_TT_BOARD_ELECROW_CROWPANEL_BASIC_50)
#define TT_BOARD_HARDWARE &crowpanel_basic_50
#include "CrowPanelBasic50.h"
#elif defined(CONFIG_TT_BOARD_M5STACK_CORE2)

View File

@ -1,40 +1,5 @@
#pragma once
#include "Tactility/hal/display/DisplayDevice.h"
#include <esp_lcd_types.h>
#include <lvgl.h>
class TdeckDisplay : 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 "ST7789"; }
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<tt::hal::touch::TouchDevice> _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<tt::hal::display::DisplayDevice> createDisplay();

View File

@ -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 ST7789 GT911 PwmBacklight driver esp_adc
)

View File

@ -0,0 +1,63 @@
#include "PwmBacklight.h"
#include "Tactility/kernel/SystemEvents.h"
#include "Tactility/service/gps/GpsService.h"
#include <Tactility/TactilityCore.h>
#include <Tactility/hal/gps/GpsConfiguration.h>
#define TAG "tdeck"
// Power on
#define TDECK_POWERON_GPIO GPIO_NUM_10
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;
}
tt::kernel::subscribeSystemEvent(tt::kernel::SystemEvent::BootSplash, [](tt::kernel::SystemEvent event) {
auto gps_service = tt::service::gps::findGpsService();
if (gps_service != nullptr) {
std::vector<tt::hal::gps::GpsConfiguration> 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;
}

View File

@ -0,0 +1,108 @@
#include "Tactility/lvgl/LvglSync.h"
#include "hal/TdeckDisplay.h"
#include "hal/TdeckDisplayConstants.h"
#include "hal/TdeckKeyboard.h"
#include "hal/TdeckPower.h"
#include "hal/TdeckSdCard.h"
#include <Tactility/hal/Configuration.h>
#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_tdeck = {
.initBoot = tdeckInit,
.createDisplay = createDisplay,
.createKeyboard = createKeyboard,
.sdcard = createTdeckSdCard(),
.power = tdeck_get_power,
.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_18,
.scl_io_num = GPIO_NUM_8,
.sda_pullup_en = true,
.scl_pullup_en = true,
.master = {
.clk_speed = 400000
},
.clk_flags = 0
}
},
i2c::Configuration {
.name = "External",
.port = I2C_NUM_1,
.initMode = i2c::InitMode::Disabled,
.isMutable = true,
.config = (i2c_config_t) {
.mode = I2C_MODE_MASTER,
.sda_io_num = GPIO_NUM_43,
.scl_io_num = GPIO_NUM_44,
.sda_pullup_en = false,
.scl_pullup_en = false,
.master = {
.clk_speed = 400000
},
.clk_flags = 0
}
}
},
.spi {
spi::Configuration {
.device = SPI2_HOST,
.dma = SPI_DMA_CH_AUTO,
.config = {
.mosi_io_num = GPIO_NUM_41,
.miso_io_num = GPIO_NUM_38,
.sclk_io_num = GPIO_NUM_40,
.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_44,
.txPin = GPIO_NUM_43,
.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,
}
}
}
}
};

View File

@ -0,0 +1,5 @@
#pragma once
#include <Tactility/hal/Configuration.h>
extern const tt::hal::Configuration lilygo_tdeck;

View File

@ -0,0 +1,114 @@
// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare.
// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines!
//
// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/
//
// Author: Jean-Marc Zingg
//
// Version: see library.properties
//
// Library: https://github.com/ZinggJM/GxEPD2
#pragma once
// color definitions for GxEPD, GxEPD2 and GxEPD_HD, values correspond to RGB565 values for TFTs
#define GxEPD_BLACK 0x0000
#define GxEPD_WHITE 0xFFFF
// some controllers for b/w EPDs support grey levels
#define GxEPD_DARKGREY 0x7BEF // 128, 128, 128
#define GxEPD_LIGHTGREY 0xC618 // 192, 192, 192
// values for 3-color or 7-color EPDs
#define GxEPD_RED 0xF800 // 255, 0, 0
#define GxEPD_YELLOW 0xFFE0 // 255, 255, 0 !!no longer same as GxEPD_RED!!
#define GxEPD_COLORED GxEPD_RED
// values for 7-color EPDs only
#define GxEPD_BLUE 0x001F // 0, 0, 255
#define GxEPD_GREEN 0x07E0 // 0, 255, 0
#define GxEPD_ORANGE 0xFC00 // 255, 128, 0
class GxEPD2
{
public:
enum Panel
{
GDEW0102T4, Waveshare_1_02_bw = GDEW0102T4,
GDEP015OC1, Waveshare_1_54_bw = GDEP015OC1,
DEPG0150BN,
GDEH0154D67, Waveshare_1_54_bw_D67 = GDEH0154D67,
GDEW0154T8,
GDEW0154M09,
GDEW0154M10,
GDEY0154D67,
GDE0213B1, Waveshare_2_13_bw = GDE0213B1,
GDEH0213B72, Waveshare_2_13_bw_B72 = GDEH0213B72,
GDEH0213B73, Waveshare_2_13_bw_B73 = GDEH0213B73,
GDEM0213B74,
GDEW0213I5F, Waveshare_2_13_flex = GDEW0213I5F,
GDEW0213M21,
GDEW0213T5D,
DEPG0213BN,
GDEY0213B74,
GDEW026T0, Waveshare_2_6_bw = GDEW026T0,
GDEW026M01,
DEPG0266BN,
GDEY0266T90,
GDEH029A1, Waveshare_2_9_bw = GDEH029A1,
GDEW029T5, Waveshare_2_9_bw_T5 = GDEW029T5,
GDEW029T5D,
GDEW029I6FD,
GDEW029M06,
GDEM029T94,
GDEY029T94,
DEPG0290BS,
GDEW027W3, Waveshare_2_7_bw = GDEW027W3,
GDEY027T91,
GDEQ031T10,
ED037TC1,
GDEW0371W7, Waveshare_3_7_bw = GDEW0371W7,
GDEW042T2, Waveshare_4_2_bw = GDEW042T2,
GDEW042M01,
GDEY042T81,
GDEQ0426T82,
GDEW0583T7, Waveshare_5_83_bw = GDEW0583T7,
GDEW0583T8,
GDEQ0583T31,
GDEW075T8, Waveshare_7_5_bw = GDEW075T8,
GDEW075T7, Waveshare_7_5_bw_T7 = GDEW075T7,
GDEY075T7,
GDEH116T91,
GDEW1248T3, Waveshare_12_24_bw = GDEW1248T3,
ED060SCT, // on Waveshare IT8951 Driver HAT
ED060KC1, // on Waveshare IT8951 Driver HAT 1448x1072
ED078KC2, // on Waveshare IT8951 Driver HAT 1872x1404
ES103TC1, // on Waveshare IT8951 Driver HAT 1872x1404
// 3-color
GDEW0154Z04, Waveshare_1_54_bwr = GDEW0154Z04,
GDEH0154Z90, Waveshare_1_54_bwr_Z90 = GDEH0154Z90,
GDEW0213Z16, Waveshare_2_13_bwr = GDEW0213Z16,
GDEW0213Z19,
GDEY0213Z98,
GDEW029Z10, Waveshare_2_9_bwr = GDEW029Z10,
GDEH029Z13,
GDEM029C90,
GDEY0266Z90, Waveshare_2_66_bwr = GDEY0266Z90,
GDEW027C44, Waveshare_2_7_bwr = GDEW027C44,
GDEW042Z15, Waveshare_4_2_bwr = GDEW042Z15,
GDEQ042Z21, Waveshare_4_2_V2_bwr = GDEQ042Z21,
GDEW0583Z21, Waveshare_5_83_bwr = GDEW0583Z21,
GDEW0583Z83,
GDEW075Z09, Waveshare_7_5_bwr = GDEW075Z09,
GDEW075Z08, Waveshare_7_5_bwr_Z08 = GDEW075Z08,
GDEH075Z90, Waveshare_7_5_bwr_Z90 = GDEH075Z90,
GDEY1248Z51,
// 4-color
GDEY0266F51H,
GDEY029F51H,
Waveshare3inch4color,
GDEY0420F51,
Waveshare437inch4color,
// 7-color
ACeP565, Waveshare_5_65_7c = ACeP565,
GDEY073D46,
ACeP730, Waveshare_7_30_7c = ACeP730
};
};

View File

@ -0,0 +1,297 @@
// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare.
// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines!
//
// based on Demo Example from Good Display: https://www.good-display.com/product/426.html
// Panel: GDEQ031T10 : https://www.good-display.com/product/426.html
// Controller: UC8253 : https://v4.cecdn.yun300.cn/100001_1909185148/UC8253.pdf
//
// Author: Jean-Marc Zingg
//
// Version: see library.properties
//
// Library: https://github.com/ZinggJM/GxEPD2
#include "GxEPD2_310_GDEQ031T10.h"
#include <Tactility/kernel/Kernel.h>
constexpr uint32_t LOW = 0;
constexpr uint32_t HIGH = 1;
GxEPD2_310_GDEQ031T10::GxEPD2_310_GDEQ031T10(int16_t cs, int16_t dc, int16_t rst, int16_t busy) :
GxEPD2_EPD(cs, dc, rst, busy, LOW, 10000000, WIDTH, HEIGHT, panel, hasColor, hasPartialUpdate, hasFastPartialUpdate) {}
void GxEPD2_310_GDEQ031T10::clearScreen(uint8_t value) {
// full refresh needed for all cases (previous != screen)
_writeScreenBuffer(0x10, value); // set previous
_writeScreenBuffer(0x13, value); // set current
refresh(false); // full refresh
_initial_write = false;
}
void GxEPD2_310_GDEQ031T10::writeScreenBuffer(uint8_t value) {
if (_initial_write) return clearScreen(value);
_writeScreenBuffer(0x13, value); // set current
}
void GxEPD2_310_GDEQ031T10::writeScreenBufferAgain(uint8_t value) {
_writeScreenBuffer(0x10, value); // set previous
//_writeScreenBuffer(0x13, value); // set current, not needed
}
void GxEPD2_310_GDEQ031T10::_writeScreenBuffer(uint8_t command, uint8_t value) {
if (!_init_display_done) _InitDisplay();
_writeCommand(command);
_startTransfer();
for (uint32_t i = 0; i < uint32_t(WIDTH) * uint32_t(HEIGHT) / 8; i++) { _transfer(value); }
_endTransfer();
}
void GxEPD2_310_GDEQ031T10::writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { _writeImage(0x13, bitmap, x, y, w, h, invert, mirror_y, pgm); }
void GxEPD2_310_GDEQ031T10::writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) {
_writeImage(0x10, bitmap, x, y, w, h, invert, mirror_y, pgm); // set previous
_writeImage(0x13, bitmap, x, y, w, h, invert, mirror_y, pgm); // set current
}
void GxEPD2_310_GDEQ031T10::writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) {
_writeImage(0x10, bitmap, x, y, w, h, invert, mirror_y, pgm); // set previous
//_writeImage(0x13, bitmap, x, y, w, h, invert, mirror_y, pgm); // set current, not needed
}
void GxEPD2_310_GDEQ031T10::_writeImage(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) {
tt::kernel::delayMillis(1); // WDT hack
uint16_t wb = (w + 7) / 8; // width bytes, bitmaps are padded
x -= x % 8; // byte boundary
w = wb * 8; // byte boundary
int16_t x1 = x < 0 ? 0 : x; // limit
int16_t y1 = y < 0 ? 0 : y; // limit
int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; // limit
int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; // limit
int16_t dx = x1 - x;
int16_t dy = y1 - y;
w1 -= dx;
h1 -= dy;
if ((w1 <= 0) || (h1 <= 0)) return;
if (!_init_display_done) _InitDisplay();
if (_initial_write) writeScreenBuffer(); // initial full screen buffer clean
_writeCommand(0x91); // partial in
_setPartialRamArea(x1, y1, w1, h1);
_writeCommand(command);
_startTransfer();
for (int16_t i = 0; i < h1; i++) {
for (int16_t j = 0; j < w1 / 8; j++) {
uint8_t data;
// use wb, h of bitmap for index!
uint16_t idx = mirror_y ? j + dx / 8 + uint16_t((h - 1 - (i + dy))) * wb : j + dx / 8 + uint16_t(i + dy) * wb;
if (pgm) {
#if defined(__AVR) || defined(ESP8266) || defined(ESP32)
data = pgm_read_byte(&bitmap[idx]);
#else
data = bitmap[idx];
#endif
} else { data = bitmap[idx]; }
if (invert) data = ~data;
_transfer(data);
}
}
_endTransfer();
_writeCommand(0x92); // partial out
tt::kernel::delayMillis(1); // WDT hack
}
void GxEPD2_310_GDEQ031T10::writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { _writeImagePart(0x13, bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); }
void GxEPD2_310_GDEQ031T10::writeImagePartAgain(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) {
_writeImagePart(0x10, bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); // set previous
//_writeImagePart(0x13, bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); // set current, not needed
}
void GxEPD2_310_GDEQ031T10::_writeImagePart(uint8_t command, const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) {
tt::kernel::delayMillis(1); // WDT hack
if ((w_bitmap < 0) || (h_bitmap < 0) || (w < 0) || (h < 0)) return;
if ((x_part < 0) || (x_part >= w_bitmap)) return;
if ((y_part < 0) || (y_part >= h_bitmap)) return;
uint16_t wb_bitmap = (w_bitmap + 7) / 8; // width bytes, bitmaps are padded
x_part -= x_part % 8; // byte boundary
w = w_bitmap - x_part < w ? w_bitmap - x_part : w; // limit
h = h_bitmap - y_part < h ? h_bitmap - y_part : h; // limit
x -= x % 8; // byte boundary
w = 8 * ((w + 7) / 8); // byte boundary, bitmaps are padded
int16_t x1 = x < 0 ? 0 : x; // limit
int16_t y1 = y < 0 ? 0 : y; // limit
int16_t w1 = x + w < int16_t(WIDTH) ? w : int16_t(WIDTH) - x; // limit
int16_t h1 = y + h < int16_t(HEIGHT) ? h : int16_t(HEIGHT) - y; // limit
int16_t dx = x1 - x;
int16_t dy = y1 - y;
w1 -= dx;
h1 -= dy;
if ((w1 <= 0) || (h1 <= 0)) return;
if (!_init_display_done) _InitDisplay();
if (_initial_write) writeScreenBuffer(); // initial full screen buffer clean
_writeCommand(0x91); // partial in
_setPartialRamArea(x1, y1, w1, h1);
_writeCommand(command);
_startTransfer();
for (int16_t i = 0; i < h1; i++) {
for (int16_t j = 0; j < w1 / 8; j++) {
uint8_t data;
// use wb_bitmap, h_bitmap of bitmap for index!
uint16_t idx = mirror_y ? x_part / 8 + j + dx / 8 + uint16_t((h_bitmap - 1 - (y_part + i + dy))) * wb_bitmap : x_part / 8 + j + dx / 8 + uint16_t(y_part + i + dy) * wb_bitmap;
if (pgm) {
#if defined(__AVR) || defined(ESP8266) || defined(ESP32)
data = pgm_read_byte(&bitmap[idx]);
#else
data = bitmap[idx];
#endif
} else { data = bitmap[idx]; }
if (invert) data = ~data;
_transfer(data);
}
}
_endTransfer();
_writeCommand(0x92); // partial out
tt::kernel::delayMillis(1); // WDT hack
}
void GxEPD2_310_GDEQ031T10::writeImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { writeImage(black, x, y, w, h, invert, mirror_y, pgm); } }
void GxEPD2_310_GDEQ031T10::writeImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { writeImagePart(black, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); } }
void GxEPD2_310_GDEQ031T10::writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (data1) { writeImage(data1, x, y, w, h, invert, mirror_y, pgm); } }
void GxEPD2_310_GDEQ031T10::drawImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) {
writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm);
refresh(x, y, w, h);
writeImageAgain(bitmap, x, y, w, h, invert, mirror_y, pgm);
}
void GxEPD2_310_GDEQ031T10::drawImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) {
writeImagePart(bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm);
refresh(x, y, w, h);
writeImagePartAgain(bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm);
}
void GxEPD2_310_GDEQ031T10::drawImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { drawImage(black, x, y, w, h, invert, mirror_y, pgm); } }
void GxEPD2_310_GDEQ031T10::drawImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (black) { drawImagePart(black, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm); } }
void GxEPD2_310_GDEQ031T10::drawNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert, bool mirror_y, bool pgm) { if (data1) { drawImage(data1, x, y, w, h, invert, mirror_y, pgm); } }
void GxEPD2_310_GDEQ031T10::refresh(bool partial_update_mode) {
if (partial_update_mode) refresh(0, 0, WIDTH, HEIGHT);
else {
_Update_Full();
_initial_refresh = false; // initial full update done
}
}
void GxEPD2_310_GDEQ031T10::refresh(int16_t x, int16_t y, int16_t w, int16_t h) {
if (_initial_refresh) return refresh(false); // initial update needs be full update
// intersection with screen
int16_t w1 = x < 0 ? w + x : w; // reduce
int16_t h1 = y < 0 ? h + y : h; // reduce
int16_t x1 = x < 0 ? 0 : x; // limit
int16_t y1 = y < 0 ? 0 : y; // limit
w1 = x1 + w1 < int16_t(WIDTH) ? w1 : int16_t(WIDTH) - x1; // limit
h1 = y1 + h1 < int16_t(HEIGHT) ? h1 : int16_t(HEIGHT) - y1; // limit
if ((w1 <= 0) || (h1 <= 0)) return;
// make x1, w1 multiple of 8
w1 += x1 % 8;
if (w1 % 8 > 0) w1 += 8 - w1 % 8;
x1 -= x1 % 8;
if (usePartialUpdateWindow) _writeCommand(0x91); // partial in
_setPartialRamArea(x1, y1, w1, h1);
_Update_Part();
if (usePartialUpdateWindow) _writeCommand(0x92); // partial out
}
void GxEPD2_310_GDEQ031T10::powerOff(void) { _PowerOff(); }
void GxEPD2_310_GDEQ031T10::hibernate() {
_PowerOff();
if (_rst >= 0) {
_writeCommand(0x07); // deep sleep
_writeData(0xA5); // check code
_hibernating = true;
_init_display_done = false;
}
}
void GxEPD2_310_GDEQ031T10::_setPartialRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
uint16_t xe = (x + w - 1) | 0x0007; // byte boundary inclusive (last byte)
uint16_t ye = y + h - 1;
x &= 0xFFF8; // byte boundary
_writeCommand(0x90); // partial window
_writeData(x);
_writeData(xe);
_writeData(y / 256);
_writeData(y % 256);
_writeData(ye / 256);
_writeData(ye % 256);
_writeData(0x01);
}
void GxEPD2_310_GDEQ031T10::_PowerOn() {
if (!_power_is_on) {
_writeCommand(0x04);
_waitWhileBusy("_PowerOn", power_on_time);
}
_power_is_on = true;
}
void GxEPD2_310_GDEQ031T10::_PowerOff() {
if (_power_is_on) {
_writeCommand(0x02); // power off
_waitWhileBusy("_PowerOff", power_off_time);
}
_power_is_on = false;
}
void GxEPD2_310_GDEQ031T10::_InitDisplay() {
_writeCommand(0x00); // PANEL SETTING
_writeData(0x1e); // soft reset
_writeData(0x0d);
tt::kernel::delayMillis(1);
_power_is_on = false;
_writeCommand(0x00); // PANEL SETTING
_writeData(0x1f); // KW: 3f, KWR: 2F, BWROTP: 0f, BWOTP: 1f
_writeData(0x0d);
_init_display_done = true;
}
void GxEPD2_310_GDEQ031T10::_Update_Full() {
if (useFastFullUpdate) {
_writeCommand(0xE0); // Cascade Setting (CCSET)
_writeData(0x02); // TSFIX
_writeCommand(0xE5); // Force Temperature (TSSET)
_writeData(0x5A); // 90, 1015000us
//_writeData(0x6E); // 110, 1542001
}
_writeCommand(0x50);
_writeData(0x97);
_PowerOn();
_writeCommand(0x12); //display refresh
_waitWhileBusy("_Update_Full", full_refresh_time);
_init_display_done = false; // needed, reason unknown
}
void GxEPD2_310_GDEQ031T10::_Update_Part() {
if (hasFastPartialUpdate) {
_writeCommand(0xE0); // Cascade Setting (CCSET)
_writeData(0x02); // TSFIX
_writeCommand(0xE5); // Force Temperature (TSSET)
_writeData(0x79); // 121
}
_writeCommand(0x50);
_writeData(0xD7);
_PowerOn();
_writeCommand(0x12); //display refresh
_waitWhileBusy("_Update_Part", partial_refresh_time);
_init_display_done = false; // needed, reason unknown
}

View File

@ -0,0 +1,100 @@
// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare.
// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines!
//
// based on Demo Example from Good Display: https://www.good-display.com/product/426.html
// Panel: GDEQ031T10 : https://www.good-display.com/product/426.html
// Controller: UC8253 : https://v4.cecdn.yun300.cn/100001_1909185148/UC8253.pdf
//
// Author: Jean-Marc Zingg
//
// Version: see library.properties
//
// Library: https://github.com/ZinggJM/GxEPD2
#pragma once
#include "GxEPD2_EPD.h"
class GxEPD2_310_GDEQ031T10 : public GxEPD2_EPD
{
public:
// attributes
static const uint16_t WIDTH = 240;
static const uint16_t WIDTH_VISIBLE = WIDTH;
static const uint16_t HEIGHT = 320;
static const GxEPD2::Panel panel = GxEPD2::GDEQ031T10;
static const bool hasColor = false;
static const bool hasPartialUpdate = true;
static const bool usePartialUpdateWindow = true; // set false for better image
static const bool hasFastPartialUpdate = true; // set this false to force full refresh always
static const bool useFastFullUpdate = true;
// set false for extended (low) temperature range, 1015000us vs 3082001us
static const uint16_t power_on_time = 50; // ms, e.g. 45000us
static const uint16_t power_off_time = 50; // ms, e.g. 45000us
static const uint16_t full_refresh_time = 1100; // ms, e.g. 1015000us
static const uint16_t partial_refresh_time = 700; // ms, e.g. 650000us
// constructor
GxEPD2_310_GDEQ031T10(int16_t cs, int16_t dc, int16_t rst, int16_t busy);
// methods (virtual)
// Support for Bitmaps (Sprites) to Controller Buffer and to Screen
void clearScreen(uint8_t value = 0xFF); // init controller memory and screen (default white)
void writeScreenBuffer(uint8_t value = 0xFF); // init controller memory (default white)
void writeScreenBufferAgain(uint8_t value = 0xFF); // init previous buffer controller memory (default white)
// write to controller memory, without screen refresh; x and w should be multiple of 8
void writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false,
bool mirror_y = false, bool pgm = false);
void writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h,
bool invert = false, bool mirror_y = false, bool pgm = false);
void writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false,
bool pgm = false);
void writeImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h,
bool invert = false, bool mirror_y = false, bool pgm = false);
void writeImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap,
int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false,
bool pgm = false);
// for differential update: set current and previous buffers equal (for fast partial update to work correctly)
void writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false,
bool mirror_y = false, bool pgm = false);
void writeImagePartAgain(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false,
bool pgm = false);
// write sprite of native data to controller memory, without screen refresh; x and w should be multiple of 8
void writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h,
bool invert = false, bool mirror_y = false, bool pgm = false);
// write to controller memory, with screen refresh; x and w should be multiple of 8
void drawImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false,
bool mirror_y = false, bool pgm = false);
void drawImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false,
bool pgm = false);
void drawImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h,
bool invert = false, bool mirror_y = false, bool pgm = false);
void drawImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap,
int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false,
bool pgm = false);
// write sprite of native data to controller memory, with screen refresh; x and w should be multiple of 8
void drawNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h,
bool invert = false, bool mirror_y = false, bool pgm = false);
void refresh(bool partial_update_mode = false); // screen refresh from controller memory to full screen
void refresh(int16_t x, int16_t y, int16_t w, int16_t h); // screen refresh from controller memory, partial screen
void powerOff(); // turns off generation of panel driving voltages, avoids screen fading over time
void hibernate();
// turns powerOff() and sets controller to deep sleep for minimum power use, ONLY if wakeable by RST (rst >= 0)
private:
void _writeScreenBuffer(uint8_t command, uint8_t value);
void _writeImage(uint8_t command, const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h,
bool invert = false, bool mirror_y = false, bool pgm = false);
void _writeImagePart(uint8_t command, const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap,
int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false,
bool pgm = false);
void _setPartialRamArea(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
void _PowerOn();
void _PowerOff();
void _InitDisplay();
void _Update_Full();
void _Update_Part();
};

View File

@ -0,0 +1,161 @@
// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare.
// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines!
//
// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/
//
// Author: Jean-Marc Zingg
//
// Version: see library.properties
//
// Library: https://github.com/ZinggJM/GxEPD2
#include "GxEPD2_EPD.h"
#include <Tactility/hal/spi/Spi.h>
#include <Tactility/kernel/Kernel.h>
constexpr auto* TAG = "GxEPD2";
GxEPD2_EPD::GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout,
uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu) :
WIDTH(w), HEIGHT(h), panel(p), hasColor(c), hasPartialUpdate(pu), hasFastPartialUpdate(fpu),
_cs(cs), _dc(dc), _rst(rst), _busy(busy), _busy_level(busy_level), _busy_timeout(busy_timeout) {
_initial_write = true;
_initial_refresh = true;
_power_is_on = false;
_using_partial_mode = false;
_hibernating = false;
_init_display_done = false;
_busy_callback = 0;
_busy_callback_parameter = 0;
}
void GxEPD2_EPD::init() { init(true, 10, false); }
void GxEPD2_EPD::init(bool initial, uint16_t reset_duration, bool pulldown_rst_mode) {
_initial_write = initial;
_initial_refresh = initial;
_pulldown_rst_mode = pulldown_rst_mode;
_power_is_on = false;
_using_partial_mode = false;
_hibernating = false;
_init_display_done = false;
}
void GxEPD2_EPD::setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter) {
_busy_callback = busyCallback;
_busy_callback_parameter = busy_callback_parameter;
}
void GxEPD2_EPD::_waitWhileBusy(const char* comment, uint16_t busy_time) {
if (_busy >= 0) {
tt::kernel::delayMillis(1); // add some margin to become active
unsigned long start = tt::kernel::getMicros();
while (1) {
if (digitalRead(_busy) != _busy_level) break;
if (_busy_callback) _busy_callback(_busy_callback_parameter);
else tt::kernel::delayMillis(1);
if (digitalRead(_busy) != _busy_level) break;
if (tt::kernel::getMicros() - start > _busy_timeout) {
TT_LOG_W(TAG, "Busy timeout");
break;
}
vPortYield(); // avoid wdt
}
(void)start;
} else tt::kernel::delayMillis(busy_time);
}
void GxEPD2_EPD::_writeCommand(uint8_t c) {
_pSPIx->beginTransaction(_spi_settings);
if (_dc >= 0) digitalWrite(_dc, LOW);
if (_cs >= 0) digitalWrite(_cs, LOW);
_pSPIx->transfer(c);
if (_cs >= 0) digitalWrite(_cs, HIGH);
if (_dc >= 0) digitalWrite(_dc, HIGH);
_pSPIx->endTransaction();
}
void GxEPD2_EPD::_writeData(uint8_t d) {
_pSPIx->beginTransaction(_spi_settings);
if (_cs >= 0) digitalWrite(_cs, LOW);
_pSPIx->transfer(d);
if (_cs >= 0) digitalWrite(_cs, HIGH);
_pSPIx->endTransaction();
}
void GxEPD2_EPD::_writeData(const uint8_t* data, uint16_t n) {
_pSPIx->beginTransaction(_spi_settings);
if (_cs >= 0) digitalWrite(_cs, LOW);
for (uint16_t i = 0; i < n; i++) { _pSPIx->transfer(*data++); }
if (_cs >= 0) digitalWrite(_cs, HIGH);
_pSPIx->endTransaction();
}
void GxEPD2_EPD::_writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) {
_pSPIx->beginTransaction(_spi_settings);
if (_cs >= 0) digitalWrite(_cs, LOW);
for (uint16_t i = 0; i < n; i++) { _pSPIx->transfer(pgm_read_byte(&*data++)); }
while (fill_with_zeroes > 0) {
_pSPIx->transfer(0x00);
fill_with_zeroes--;
}
if (_cs >= 0) digitalWrite(_cs, HIGH);
_pSPIx->endTransaction();
}
void GxEPD2_EPD::_writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes) {
_pSPIx->beginTransaction(_spi_settings);
for (uint8_t i = 0; i < n; i++) {
if (_cs >= 0) digitalWrite(_cs, LOW);
_pSPIx->transfer(pgm_read_byte(&*data++));
if (_cs >= 0) digitalWrite(_cs, HIGH);
}
while (fill_with_zeroes > 0) {
if (_cs >= 0) digitalWrite(_cs, LOW);
_pSPIx->transfer(0x00);
fill_with_zeroes--;
if (_cs >= 0) digitalWrite(_cs, HIGH);
}
_pSPIx->endTransaction();
}
void GxEPD2_EPD::_writeCommandData(const uint8_t* pCommandData, uint8_t datalen) {
_pSPIx->beginTransaction(_spi_settings);
if (_dc >= 0) digitalWrite(_dc, LOW);
if (_cs >= 0) digitalWrite(_cs, LOW);
_pSPIx->transfer(*pCommandData++);
if (_dc >= 0) digitalWrite(_dc, HIGH);
for (uint8_t i = 0; i < datalen - 1; i++) // sub the command
{
_pSPIx->transfer(*pCommandData++);
}
if (_cs >= 0) digitalWrite(_cs, HIGH);
_pSPIx->endTransaction();
}
void GxEPD2_EPD::_writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen) {
_pSPIx->beginTransaction(_spi_settings);
if (_dc >= 0) digitalWrite(_dc, LOW);
if (_cs >= 0) digitalWrite(_cs, LOW);
_pSPIx->transfer(pgm_read_byte(&*pCommandData++));
if (_dc >= 0) digitalWrite(_dc, HIGH);
for (uint8_t i = 0; i < datalen - 1; i++) // sub the command
{
_pSPIx->transfer(pgm_read_byte(&*pCommandData++));
}
if (_cs >= 0) digitalWrite(_cs, HIGH);
_pSPIx->endTransaction();
}
void GxEPD2_EPD::_startTransfer() {
_pSPIx->beginTransaction(_spi_settings);
if (_cs >= 0) digitalWrite(_cs, LOW);
}
void GxEPD2_EPD::_transfer(uint8_t value) { _pSPIx->transfer(value); }
void GxEPD2_EPD::_endTransfer() {
if (_cs >= 0) digitalWrite(_cs, HIGH);
_pSPIx->endTransaction();
}

View File

@ -0,0 +1,143 @@
// Display Library for SPI e-paper panels from Dalian Good Display and boards from Waveshare.
// Requires HW SPI and Adafruit_GFX. Caution: the e-paper panels require 3.3V supply AND data lines!
//
// Display Library based on Demo Example from Good Display: https://www.good-display.com/companyfile/32/
//
// Author: Jean-Marc Zingg
//
// Version: see library.properties
//
// Library: https://github.com/ZinggJM/GxEPD2
#pragma once
#include "GxEPD2.h"
#include <cstdint>
#pragma GCC diagnostic ignored "-Wunused-parameter"
class GxEPD2_EPD
{
public:
// attributes
const uint16_t WIDTH;
const uint16_t HEIGHT;
const GxEPD2::Panel panel;
const bool hasColor;
const bool hasPartialUpdate;
const bool hasFastPartialUpdate;
// constructor
GxEPD2_EPD(int16_t cs, int16_t dc, int16_t rst, int16_t busy, int16_t busy_level, uint32_t busy_timeout,
uint16_t w, uint16_t h, GxEPD2::Panel p, bool c, bool pu, bool fpu);
virtual void init();
virtual void init(bool initial, uint16_t reset_duration = 10, bool pulldown_rst_mode = false);
// Support for Bitmaps (Sprites) to Controller Buffer and to Screen
virtual void clearScreen(uint8_t value) = 0; // init controller memory and screen (default white)
virtual void writeScreenBuffer(uint8_t value) = 0; // init controller memory (default white)
// write to controller memory, without screen refresh; x and w should be multiple of 8
virtual void writeImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false,
bool mirror_y = false, bool pgm = false) = 0;
virtual void writeImageForFullRefresh(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h,
bool invert = false, bool mirror_y = false, bool pgm = false)
{
// writeImage is independent from refresh mode for most controllers, exception e.g. SSD1681
writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm);
}
virtual void writeImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap,
int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false,
bool pgm = false) = 0;
// virtual void writeImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;
// virtual void writeImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
// int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;
// write sprite of native data to controller memory, without screen refresh; x and w should be multiple of 8
// virtual void writeNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;
// for differential update: set current and previous buffers equal (for fast partial update to work correctly)
virtual void writeScreenBufferAgain(uint8_t value = 0xFF) // init controller memory (default white)
{
// most controllers with differential update do switch buffers on refresh, can use:
writeScreenBuffer(value);
}
virtual void writeImageAgain(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h,
bool invert = false, bool mirror_y = false, bool pgm = false)
{
// most controllers with differential update do switch buffers on refresh, can use:
writeImage(bitmap, x, y, w, h, invert, mirror_y, pgm);
}
virtual void writeImagePartAgain(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap,
int16_t h_bitmap,
int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false,
bool mirror_y = false, bool pgm = false)
{
// most controllers with differential update do switch buffers on refresh, can use:
writeImagePart(bitmap, x_part, y_part, w_bitmap, h_bitmap, x, y, w, h, invert, mirror_y, pgm);
}
// write to controller memory, with screen refresh; x and w should be multiple of 8
// virtual void drawImage(const uint8_t bitmap[], int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;
// virtual void drawImagePart(const uint8_t bitmap[], int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
// int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;
// virtual void drawImage(const uint8_t* black, const uint8_t* color, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;
// virtual void drawImagePart(const uint8_t* black, const uint8_t* color, int16_t x_part, int16_t y_part, int16_t w_bitmap, int16_t h_bitmap,
// int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;
// write sprite of native data to controller memory, with screen refresh; x and w should be multiple of 8
// virtual void drawNative(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h, bool invert = false, bool mirror_y = false, bool pgm = false) = 0;
// a demo bitmap can use yet another bitmap format, e.g. 7-color bitmap from Good Display for GDEY073D46
virtual void writeDemoBitmap(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h,
int16_t mode = 0, bool mirror_y = false, bool pgm = false)
{
};
virtual void drawDemoBitmap(const uint8_t* data1, const uint8_t* data2, int16_t x, int16_t y, int16_t w, int16_t h,
int16_t mode = 0, bool mirror_y = false, bool pgm = false)
{
};
virtual void refresh(bool partial_update_mode = false) = 0; // screen refresh from controller memory to full screen
virtual void refresh(int16_t x, int16_t y, int16_t w, int16_t h) = 0;
// screen refresh from controller memory, partial screen
virtual void powerOff() = 0; // turns off generation of panel driving voltages, avoids screen fading over time
virtual void hibernate() = 0;
// turns powerOff() and sets controller to deep sleep for minimum power use, ONLY if wakeable by RST (rst >= 0)
virtual void setPaged()
{
}; // for GxEPD2_154c paged workaround
// register a callback function to be called during _waitWhileBusy continuously.
void setBusyCallback(void (*busyCallback)(const void*), const void* busy_callback_parameter = 0);
static inline uint16_t gx_uint16_min(uint16_t a, uint16_t b)
{
return (a < b ? a : b);
};
static inline uint16_t gx_uint16_max(uint16_t a, uint16_t b)
{
return (a > b ? a : b);
};
protected:
void _waitWhileBusy(const char* comment = 0, uint16_t busy_time = 5000);
void _writeCommand(uint8_t c);
void _writeData(uint8_t d);
void _writeData(const uint8_t* data, uint16_t n);
void _writeDataPGM(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes = 0);
void _writeDataPGM_sCS(const uint8_t* data, uint16_t n, int16_t fill_with_zeroes = 0);
void _writeCommandData(const uint8_t* pCommandData, uint8_t datalen);
void _writeCommandDataPGM(const uint8_t* pCommandData, uint8_t datalen);
void _startTransfer();
void _transfer(uint8_t value);
void _endTransfer();
protected:
int16_t _cs, _dc, _rst, _busy, _busy_level;
uint32_t _busy_timeout;
bool _pulldown_rst_mode;
bool _initial_write, _initial_refresh;
bool _power_is_on, _using_partial_mode, _hibernating;
bool _init_display_done;
void (*_busy_callback)(const void*);
const void* _busy_callback_parameter;
};

View File

@ -0,0 +1,45 @@
#include "TdeckDisplay.h"
#include "TdeckDisplayConstants.h"
#include <Gt911Touch.h>
#include <PwmBacklight.h>
#include <St7789Display.h>
#include <driver/spi_master.h>
#define TAG "tdeck_display"
static std::shared_ptr<tt::hal::touch::TouchDevice> createTouch() {
// Note for future changes: Reset pin is 48 and interrupt pin is 47
auto configuration = std::make_unique<Gt911Touch::Configuration>(
I2C_NUM_0,
240,
320,
true,
true,
false
);
return std::make_shared<Gt911Touch>(std::move(configuration));
}
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
auto touch = createTouch();
auto configuration = std::make_unique<St7789Display::Configuration>(
TDECK_LCD_SPI_HOST,
TDECK_LCD_PIN_CS,
TDECK_LCD_PIN_DC,
320,
240,
touch,
true,
true,
false,
true
);
configuration->backlightDutyFunction = driver::pwmbacklight::setBacklightDuty;
return std::make_shared<St7789Display>(std::move(configuration));
}

View File

@ -0,0 +1,40 @@
#pragma once
#include "Tactility/hal/display/DisplayDevice.h"
#include <esp_lcd_types.h>
#include <lvgl.h>
class TdeckDisplay : 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 "ST7789"; }
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<tt::hal::touch::TouchDevice> _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<tt::hal::display::DisplayDevice> createDisplay();

View File

@ -0,0 +1,8 @@
#pragma once
#define TDECK_LCD_SPI_HOST SPI2_HOST
#define TDECK_LCD_PIN_CS GPIO_NUM_12
#define TDECK_LCD_PIN_DC GPIO_NUM_11 // RS
#define TDECK_LCD_HORIZONTAL_RESOLUTION 320
#define TDECK_LCD_VERTICAL_RESOLUTION 240
#define TDECK_LCD_SPI_TRANSFER_HEIGHT (TDECK_LCD_VERTICAL_RESOLUTION / 10)

View File

@ -0,0 +1,67 @@
#include "TdeckKeyboard.h"
#include <Tactility/hal/i2c/I2c.h>
#include <driver/i2c.h>
#define TAG "tdeck_keyboard"
#define TDECK_KEYBOARD_I2C_BUS_HANDLE I2C_NUM_0
#define TDECK_KEYBOARD_SLAVE_ADDRESS 0x55
static inline bool keyboard_i2c_read(uint8_t* output) {
return tt::hal::i2c::masterRead(TDECK_KEYBOARD_I2C_BUS_HANDLE, TDECK_KEYBOARD_SLAVE_ADDRESS, output, 1, 100 / portTICK_PERIOD_MS);
}
/**
* 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(TT_UNUSED lv_indev_t* indev, lv_indev_data_t* data) {
static uint8_t last_buffer = 0x00;
uint8_t read_buffer = 0x00;
// Defaults
data->key = 0;
data->state = LV_INDEV_STATE_RELEASED;
if (keyboard_i2c_read(&read_buffer)) {
if (read_buffer == 0 && read_buffer != last_buffer) {
TT_LOG_D(TAG, "Released %d", last_buffer);
data->key = last_buffer;
data->state = LV_INDEV_STATE_RELEASED;
} else if (read_buffer != 0) {
TT_LOG_D(TAG, "Pressed %d", read_buffer);
data->key = read_buffer;
data->state = LV_INDEV_STATE_PRESSED;
}
}
last_buffer = read_buffer;
}
bool TdeckKeyboard::start(lv_display_t* display) {
deviceHandle = lv_indev_create();
lv_indev_set_type(deviceHandle, LV_INDEV_TYPE_KEYPAD);
lv_indev_set_read_cb(deviceHandle, &keyboard_read_callback);
lv_indev_set_display(deviceHandle, display);
lv_indev_set_user_data(deviceHandle, this);
return true;
}
bool TdeckKeyboard::stop() {
lv_indev_delete(deviceHandle);
deviceHandle = nullptr;
return true;
}
bool TdeckKeyboard::isAttached() const {
return tt::hal::i2c::masterHasDeviceAtAddress(TDECK_KEYBOARD_I2C_BUS_HANDLE, TDECK_KEYBOARD_SLAVE_ADDRESS, 100);
}
std::shared_ptr<tt::hal::keyboard::KeyboardDevice> createKeyboard() {
return std::make_shared<TdeckKeyboard>();
}

View File

@ -0,0 +1,25 @@
#pragma once
#include <Tactility/hal/keyboard/KeyboardDevice.h>
#include <Tactility/TactilityCore.h>
#include <esp_lcd_panel_io_interface.h>
#include <esp_lcd_touch.h>
class TdeckKeyboard : public tt::hal::keyboard::KeyboardDevice {
private:
lv_indev_t* _Nullable deviceHandle = nullptr;
public:
std::string getName() const final { return "T-Deck Keyboard"; }
std::string getDescription() const final { return "I2C keyboard"; }
bool start(lv_display_t* display) override;
bool stop() override;
bool isAttached() const override;
lv_indev_t* _Nullable getLvglIndev() override { return deviceHandle; }
};
std::shared_ptr<tt::hal::keyboard::KeyboardDevice> createKeyboard();

View File

@ -0,0 +1,130 @@
#include "TdeckPower.h"
#include <Tactility/Log.h>
#define TAG "power"
/**
* 2.0 ratio, but +.11 added as display voltage sag compensation.
*/
#define ADC_MULTIPLIER 2.11
#define ADC_REF_VOLTAGE 3.3f
#define BATTERY_VOLTAGE_MIN 3.2f
#define BATTERY_VOLTAGE_MAX 4.2f
static adc_oneshot_unit_init_cfg_t adcConfig = {
.unit_id = ADC_UNIT_1,
.clk_src = ADC_RTC_CLK_SRC_DEFAULT,
.ulp_mode = ADC_ULP_MODE_DISABLE,
};
static adc_oneshot_chan_cfg_t adcChannelConfig = {
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT,
};
static uint8_t estimateChargeLevelFromVoltage(uint32_t milliVolt) {
float volts = std::min((float)milliVolt / 1000.f, BATTERY_VOLTAGE_MAX);
float voltage_percentage = (volts - BATTERY_VOLTAGE_MIN) / (BATTERY_VOLTAGE_MAX - BATTERY_VOLTAGE_MIN);
float voltage_factor = std::min(1.0f, voltage_percentage);
auto charge_level = (uint8_t) (voltage_factor * 100.f);
TT_LOG_V(TAG, "mV = %lu, scaled = %.2f, factor = %.2f, result = %d", milliVolt, volts, voltage_factor, charge_level);
return charge_level;
}
TdeckPower::TdeckPower() {
if (adc_oneshot_new_unit(&adcConfig, &adcHandle) != ESP_OK) {
TT_LOG_E(TAG, "ADC config failed");
return;
}
if (adc_oneshot_config_channel(adcHandle, ADC_CHANNEL_3, &adcChannelConfig) != ESP_OK) {
TT_LOG_E(TAG, "ADC channel config failed");
adc_oneshot_del_unit(adcHandle);
return;
}
}
TdeckPower::~TdeckPower() {
if (adcHandle) {
adc_oneshot_del_unit(adcHandle);
}
}
bool TdeckPower::supportsMetric(MetricType type) const {
switch (type) {
using enum MetricType;
case BatteryVoltage:
case ChargeLevel:
return true;
default:
return false;
}
return false; // Safety guard for when new enum values are introduced
}
bool TdeckPower::getMetric(MetricType type, MetricData& data) {
switch (type) {
using enum MetricType;
case BatteryVoltage:
return readBatteryVoltageSampled(data.valueAsUint32);
case ChargeLevel:
if (readBatteryVoltageSampled(data.valueAsUint32)) {
data.valueAsUint32 = estimateChargeLevelFromVoltage(data.valueAsUint32);
return true;
} else {
return false;
}
default:
return false;
}
return false; // Safety guard for when new enum values are introduced
}
bool TdeckPower::readBatteryVoltageOnce(uint32_t& output) {
int raw;
if (adc_oneshot_read(adcHandle, ADC_CHANNEL_3, &raw) == ESP_OK) {
output = ADC_MULTIPLIER * ((1000.f * ADC_REF_VOLTAGE) / 4096.f) * (float)raw;
TT_LOG_V(TAG, "Raw = %d, voltage = %lu", raw, output);
return true;
} else {
TT_LOG_E(TAG, "Read failed");
return false;
}
}
#define MAX_VOLTAGE_SAMPLES 15
bool TdeckPower::readBatteryVoltageSampled(uint32_t& output) {
size_t samples_read = 0;
uint32_t sample_accumulator = 0;
uint32_t sample_read_buffer;
for (size_t i = 0; i < MAX_VOLTAGE_SAMPLES; ++i) {
if (readBatteryVoltageOnce(sample_read_buffer)) {
sample_accumulator += sample_read_buffer;
samples_read++;
}
}
if (samples_read > 0) {
output = sample_accumulator / samples_read;
return true;
} else {
return false;
}
}
static std::shared_ptr<PowerDevice> power;
std::shared_ptr<PowerDevice> tdeck_get_power() {
if (power == nullptr) {
power = std::make_shared<TdeckPower>();
}
return power;
}

View File

@ -0,0 +1,30 @@
#pragma once
#include "Tactility/hal/power/PowerDevice.h"
#include <esp_adc/adc_oneshot.h>
#include <memory>
using tt::hal::power::PowerDevice;
class TdeckPower : public PowerDevice {
adc_oneshot_unit_handle_t adcHandle = nullptr;
public:
TdeckPower();
~TdeckPower();
std::string getName() const final { return "ADC Power Measurement"; }
std::string getDescription() const final { return "Power measurement interface via ADC pin"; }
bool supportsMetric(MetricType type) const override;
bool getMetric(MetricType type, MetricData& data) override;
private:
bool readBatteryVoltageSampled(uint32_t& output);
bool readBatteryVoltageOnce(uint32_t& output);
};
std::shared_ptr<PowerDevice> tdeck_get_power();

View File

@ -0,0 +1,33 @@
#include "TdeckSdCard.h"
#include <Tactility/lvgl/LvglSync.h>
#include <Tactility/hal/sdcard/SpiSdCardDevice.h>
#include <esp_vfs_fat.h>
using tt::hal::sdcard::SpiSdCardDevice;
#define TDECK_SDCARD_PIN_CS GPIO_NUM_39
#define TDECK_LCD_PIN_CS GPIO_NUM_12
#define TDECK_RADIO_PIN_CS GPIO_NUM_9
std::shared_ptr<SdCardDevice> createTdeckSdCard() {
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<SpiSdCardDevice::Config>(configuration)
);
return std::shared_ptr<SdCardDevice>(sdcard);
}

View File

@ -0,0 +1,7 @@
#pragma once
#include "Tactility/hal/sdcard/SdCardDevice.h"
using tt::hal::sdcard::SdCardDevice;
std::shared_ptr<SdCardDevice> createTdeckSdCard();

View File

@ -47,6 +47,10 @@ function(INIT_TACTILITY_GLOBALS SDKCONFIG_FILE)
set(TACTILITY_BOARD_PROJECT ElecrowCrowpanelBasic50)
elseif (board_id STREQUAL "lilygo-tdeck")
set(TACTILITY_BOARD_PROJECT LilygoTdeck)
elseif (board_id STREQUAL "lilygo-tdeck-plus")
set(TACTILITY_BOARD_PROJECT LilygoTdeck)
elseif (board_id STREQUAL "lilygo-tdeck-pro")
set(TACTILITY_BOARD_PROJECT LilygoTdeckPro)
elseif (board_id STREQUAL "m5stack-core2")
set(TACTILITY_BOARD_PROJECT M5stackCore2)
elseif (board_id STREQUAL "m5stack-cores3")

View File

@ -0,0 +1,53 @@
# 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_TDECK_PLUS=y
CONFIG_TT_BOARD_NAME="LilyGo T-Deck Plus"
CONFIG_TT_BOARD_ID="lilygo-tdeck-plus"
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_QIO=y
# Hardware: SPI RAM
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_120M=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_120M=y
# LVGL
CONFIG_LV_DPI_DEF=139
CONFIG_LV_DISP_DEF_REFR_PERIOD=10
# USB
CONFIG_TINYUSB_MSC_ENABLED=y
CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard"

View File

@ -0,0 +1,57 @@
# 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_TDECK_PRO=y
CONFIG_TT_BOARD_NAME="LilyGo T-Deck Pro"
CONFIG_TT_BOARD_ID="lilygo-tdeck-pro"
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_QIO=y
# Hardware: SPI RAM
CONFIG_ESP32S3_SPIRAM_SUPPORT=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_120M=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_120M=y
# LVGL
CONFIG_LV_DPI_DEF=139
CONFIG_LV_DISP_DEF_REFR_PERIOD=10
CONFIG_LV_USE_THEME_DEFAULT=n
CONFIG_LV_USE_THEME_SIMPLE=n
CONFIG_LV_USE_THEME_MONO=y
CONFIG_LV_COLOR_DEPTH=1
# USB
CONFIG_TINYUSB_MSC_ENABLED=y
CONFIG_TINYUSB_MSC_MOUNT_PATH="/sdcard"