-
-
Save LeCyberDucky/82b5af8070fdc07b6e98e8380f99a7a1 to your computer and use it in GitHub Desktop.
LilyGo T-Display-S3 + LVGL minimal example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Code inspired by: https://github.com/KamranAghlami/T-Display-S3/blob/main/src/hardware/display.cpp | |
#include <Arduino.h> | |
#include <esp_lcd_panel_io.h> | |
#include <esp_lcd_panel_ops.h> | |
#include <esp_lcd_panel_vendor.h> // Create LCD panel for model ST7789 | |
#include <lvgl.h> | |
#include "display.h" | |
#include "hardware_setup.h" | |
Display::Display() | |
{ | |
// Initialize lvgl here to create display (required to initialize hardware) | |
initialize_lvgl(); | |
// Power on and initialize hardware | |
pinMode(PIN_LCD_POWER, OUTPUT); | |
digitalWrite(PIN_LCD_POWER, HIGH); | |
pinMode(PIN_LCD_RD, OUTPUT); | |
digitalWrite(PIN_LCD_RD, HIGH); | |
initialize_hardware(); | |
backlight.set_brightness(50); | |
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); | |
} | |
void Display::initialize_lvgl() | |
{ | |
if (!lv_is_initialized()) | |
{ | |
lv_init(); | |
} | |
// Set up logging | |
#ifdef LV_USE_LOG | |
lv_log_register_print_cb([](lv_log_level_t level, const char *message) | |
{ | |
LV_UNUSED(level); | |
Serial.println(message); | |
// Serial.flush(); | |
}); | |
#endif | |
// Create timer | |
// https://github.com/espressif/esp-idf/blob/b3f7e2c8a4d354df8ef8558ea7caddc07283a57b/examples/peripherals/lcd/i80_controller/main/i80_controller_example_main.c#L473 | |
// LVGL is not thread-safe. It is fine to call lv_tick_inc in a timer interrupt, but lv_timer_handler may not be called simultaneously with other lvgl functions. | |
// Creating thread-safety by use of e.g., a lock, is not feasible, since everything in LVGL is done with pointers. I.e., you can't protect the screen from being accessed, since a new pointer to the screen can simply be spawned out of nowhere using lv_screen_active(). | |
// See: | |
// - https://docs.lvgl.io/master/porting/tick.html | |
// - https://docs.lvgl.io/master/porting/timer_handler.html | |
// - https://docs.lvgl.io/master/porting/os.html | |
// - https://www.reddit.com/r/esp32/comments/1az731x/how_do_timer_interrupts_and_their_priority_work/kskwgcc/ | |
// - https://forum.lvgl.io/t/lv-inv-area-asserted-at-expression-disp-rendering-in-progress-invalidate-area-is-not-allowed-during-rendering-lv-refr-c-257/14856/6 | |
// - https://forum.lvgl.io/t/can-i-safely-call-lv-timer-handler-less-often/14900/3 | |
const esp_timer_create_args_t lvgl_tick_timer_args = { | |
.callback = [](void *arg) | |
{ | |
lv_tick_inc(LVGL_TICK_PERIOD_MS); | |
}, | |
.name = "lvgl_tick"}; | |
esp_timer_handle_t lvgl_tick_timer = nullptr; | |
ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer)); | |
ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, LVGL_TICK_PERIOD_MS * 1000)); | |
display = lv_display_create(height, width); | |
// Create bufferydoos | |
// It's recommended to choose the size of the draw buffer(s) to be at least 1/10 screen sized | |
// https://github.com/lvgl/lvgl/blob/5ce363464fc81dec5378fc02a341b35786f0946b/docs/integration/driver/display/lcd_stm32_guide.rst | |
// https://github.com/espressif/esp-idf/blob/b3f7e2c8a4d354df8ef8558ea7caddc07283a57b/examples/peripherals/lcd/i80_controller/main/i80_controller_example_main.c#L453 | |
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/system/mm_sync.html#memory-allocation-helper | |
const auto buffer_size = height * width * lv_color_format_get_size(lv_display_get_color_format(display)); | |
draw_buffers.first = heap_caps_malloc(buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); | |
if (!draw_buffers.first) | |
{ | |
LV_LOG_ERROR("display draw buffer malloc failed"); | |
return; | |
} | |
draw_buffers.second = heap_caps_malloc(buffer_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); | |
if (!draw_buffers.second) | |
{ | |
LV_LOG_ERROR("display buffer malloc failed"); | |
lv_free(draw_buffers.first); | |
return; | |
} | |
lv_display_set_buffers(display, draw_buffers.first, draw_buffers.second, buffer_size, LV_DISPLAY_RENDER_MODE_PARTIAL); | |
const auto flush_callback = [](lv_display_t *disp, const lv_area_t *area, uint8_t *pix_map) | |
{ | |
lv_draw_sw_rgb565_swap(pix_map, (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1)); | |
Display::draw_image(area->x1, area->x2, area->y1, area->y2, std::bit_cast<uint16_t*>(pix_map)); | |
}; | |
lv_display_set_flush_cb(display, flush_callback); | |
} | |
void Display::initialize_hardware() | |
{ | |
// Create bus for I80 LCD (Intel 8080 parallel LCD) interfaced display | |
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/lcd.html#i80-lcd-panel | |
// https://github.com/Xinyuan-LilyGO/T-Display-S3/blob/main/example/lv_demos/lv_demos.ino | |
// 1. Set up Intel 8080 parallel bus | |
const esp_lcd_i80_bus_config_t bus_config = { | |
.dc_gpio_num = PIN_LCD_DC, // GPIO number of the data/command select pin (aka RS) | |
.wr_gpio_num = PIN_LCD_WR, // GPIO number of the pixel clock (aka WR) | |
.clk_src = LCD_CLK_SRC_PLL160M, // Clock source of the bus | |
.data_gpio_nums = { | |
// Array of GPIO numbers of the data bus. Number of elements equal to bus_width | |
PIN_LCD_D0, | |
PIN_LCD_D1, | |
PIN_LCD_D2, | |
PIN_LCD_D3, | |
PIN_LCD_D4, | |
PIN_LCD_D5, | |
PIN_LCD_D6, | |
PIN_LCD_D7, | |
}, | |
.bus_width = 8, // Width of the data bus | |
.max_transfer_bytes = LVGL_LCD_BUF_SIZE * sizeof(uint16_t), // Transfer full buffer of pixels (assume pixel is RGB565) at most in one transaction | |
}; | |
ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &i80_bus)); | |
// 2. Allocate LCD IO device handle from I80 bus | |
const auto pixel_transfer_callback = [](esp_lcd_panel_io_handle_t _panel_io, esp_lcd_panel_io_event_data_t* _event_data, void *user_ctx) | |
{ | |
auto display = static_cast<lv_display_t*>(user_ctx); | |
lv_display_flush_ready(display); | |
return false; | |
}; | |
const esp_lcd_panel_io_i80_config_t io_config = { | |
.cs_gpio_num = PIN_LCD_CS, // GPIO number of chip select pin | |
.pclk_hz = EXAMPLE_LCD_PIXEL_CLOCK_HZ, // Pixel clock frequency in Hz. Higher pixel clock frequency results in higher refresh rate, but may cause flickering if the DMA bandwidth is not sufficient or the LCD controller chip does not support high pixel clock frequency. | |
.trans_queue_depth = 20,// Maximum number of transactions that can be queued in the LCD IO device. A bigger value means more transactions can be queued up, but it also consumes more memory. | |
.on_color_trans_done = pixel_transfer_callback, | |
.user_ctx = display, | |
// Bit width of the command and parameters that are recognized by the LCD controller chip. This is chip specific. | |
.lcd_cmd_bits = 8, | |
.lcd_param_bits = 8, | |
.dc_levels = { | |
.dc_idle_level = 0, | |
.dc_cmd_level = 0, | |
.dc_dummy_level = 0, | |
.dc_data_level = 1, | |
}, | |
}; | |
ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(i80_bus, &io_config, &io_handle)); | |
// 3. Install the LCD controller driver. It is responsible for sending the commands and parameters to the LCD controller chip | |
const esp_lcd_panel_dev_config_t panel_config = { | |
.reset_gpio_num = PIN_LCD_RES, // GPIO number of the reset pin. Set to -1, if the LCD controller chip does not have a reset pin | |
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, | |
.bits_per_pixel = 16, // Bit width of the pixel color data. Used for computing the number of bytes to send to the LCD controller chip | |
}; | |
ESP_ERROR_CHECK(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle)); | |
// 4. Adding manufacturer specific initialization | |
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); | |
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); | |
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true)); | |
ESP_ERROR_CHECK(esp_lcd_panel_swap_xy(panel_handle, true)); | |
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, true, false)); | |
ESP_ERROR_CHECK(esp_lcd_panel_set_gap(panel_handle, 0, 35)); | |
// Set extra configurations e.g., gamma control with the underlying IO handle | |
// Please consult your manufacturer for special commands and corresponding values | |
// WARNING: Command descriptions given by ChatGPT, since the original code had no documentation for these magic constants | |
const std::array<lcd_message, 14> init_messages = std::to_array<lcd_message>({ | |
{0x11, {0}, 0 | 0x80}, // Turn off display controller sleep mode. 0x80 indicates data command (not control command) | |
{0x3A, {0X05}, 1}, // Specify 16-bit color pixel format (RGB565) | |
{0xB2, {0X0B, 0X0B, 0X00, 0X33, 0X33}, 5}, // Adjust frame rate | |
{0xB7, {0X75}, 1}, // Configure gate driver timing | |
{0xBB, {0X28}, 1}, // Set VCOM voltage level | |
{0xC0, {0X2C}, 1}, // Configure power supply parameters | |
{0xC2, {0X01}, 1}, // More power control settings | |
{0xC3, {0X1F}, 1}, // More VCOM control settings | |
{0xC6, {0X13}, 1}, // Set display interface parameters | |
{0xD0, {0XA7}, 1}, // More power control settings | |
{0xD0, {0XA4, 0XA1}, 2}, // More power control settings | |
{0xD6, {0XA1}, 1}, // Set display function control | |
{0xE0, {0XF0, 0X05, 0X0A, 0X06, 0X06, 0X03, 0X2B, 0X32, 0X43, 0X36, 0X11, 0X10, 0X2B, 0X32}, 14}, // Set gamma correction coefficients for positive gamma | |
{0xE1, {0XF0, 0X08, 0X0C, 0X0B, 0X09, 0X24, 0X2B, 0X22, 0X43, 0X38, 0X15, 0X16, 0X2F, 0X37}, 14} // Set gamma correction coefficients for negative gamma | |
}); | |
for (const auto& message : init_messages) { | |
ESP_ERROR_CHECK(esp_lcd_panel_io_tx_param(io_handle, message.command, message.parameters.data(), message.length & 0x7f)); // No idea why I have this and here | |
if (message.length & 0x80) { // No idea about this and either | |
delay(120); // No idea why I gotta sleep here | |
} | |
} | |
} | |
Display::~Display() | |
{ | |
ESP_ERROR_CHECK(esp_lcd_panel_del(panel_handle)); | |
ESP_ERROR_CHECK(esp_lcd_panel_io_del(io_handle)); | |
ESP_ERROR_CHECK(esp_lcd_del_i80_bus(i80_bus)); | |
pinMode(PIN_LCD_RD, INPUT); | |
pinMode(PIN_LCD_POWER, INPUT); | |
} | |
uint16_t Display::get_width() | |
{ | |
return width; | |
} | |
uint16_t Display::get_height() | |
{ | |
return height; | |
} | |
void Display::set_backlight(uint8_t level) | |
{ | |
get().backlight.set_brightness(level); | |
} | |
void Display::draw_image(uint16_t x_start, uint16_t x_end, uint16_t y_start, uint16_t y_end, uint16_t* data) | |
{ | |
auto& display = Display::get(); | |
ESP_ERROR_CHECK(esp_lcd_panel_draw_bitmap(display.panel_handle, x_start, y_start, x_end + 1, y_end + 1, data)); | |
} | |
void Display::send_command(const lcd_message& message) { | |
esp_lcd_panel_io_tx_param(io_handle, message.command, message.parameters.data(), message.length); | |
} | |
Display &Display::get() | |
{ | |
static Display instance; | |
return instance; | |
} | |
void Display::init() | |
{ | |
Display::get(); | |
} | |
void Backlight::set_brightness(uint8_t level) { | |
analogWrite(pin, level); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
// Code inspired by: https://github.com/KamranAghlami/T-Display-S3/blob/main/src/hardware/display.h | |
#include <memory> | |
#include <esp_lcd_panel_io.h> | |
#include "hardware_setup.h" | |
// https://github.com/Xinyuan-LilyGO/T-Display-S3/issues/74 | |
class Backlight { | |
uint8_t pin; | |
uint8_t level = 0; | |
public: | |
/// @brief Constructs a backlight by configuring the corresponding pin as an output | |
/// @param pin The physical pin responsible for the backlight | |
Backlight(uint8_t pin) : pin(pin) { | |
pinMode(pin, OUTPUT); | |
}; | |
/// @brief Deconstructs the backlight by configuring its pin as an input | |
~Backlight() { | |
pinMode(pin, INPUT); | |
}; | |
/// @brief Sets the brightness of the backlight | |
/// @param level A brightness level in the range [0; 255]. The perceived brightness is not a linear function of this value, however. A significant jump in visibility happens around a value of 45. | |
void set_brightness(uint8_t level); | |
}; | |
struct lcd_message | |
{ | |
uint8_t command; | |
std::vector<uint8_t> parameters; | |
size_t length; | |
}; | |
// Display implemented as a Meyers' Singleton: https://www.modernescpp.com/index.php/creational-patterns-singleton/ | |
class Display | |
{ | |
lv_display_t * display = nullptr; | |
std::pair<void*, void*> draw_buffers = {nullptr, nullptr}; | |
esp_lcd_i80_bus_handle_t i80_bus = nullptr; | |
esp_lcd_panel_io_handle_t io_handle = nullptr; | |
esp_lcd_panel_handle_t panel_handle = nullptr; | |
Backlight backlight = Backlight(PIN_LCD_BL); | |
uint16_t width = LCD_VER_RES; | |
uint16_t height = LCD_HOR_RES; | |
/// @brief Initialize display | |
Display(); | |
/// @brief Initializes LVGL, sets up logging and a tick timer. Also creates the lv_display_t* display member | |
void initialize_lvgl(); | |
/// @brief Sets up communication with the display and configures it | |
void initialize_hardware(); | |
/// @brief Get display instance | |
static Display &get(); | |
/// @brief Deinitialize display | |
~Display(); | |
public: | |
Display(const Display&) = delete; // No copying | |
Display(Display&&) = delete; // No moving | |
Display& operator = (const Display&) = delete; // No copy assignment | |
Display& operator = (Display&&) = delete; // No move assignment | |
/// @brief Initialize display and LVGL | |
static void init(); | |
/// @return Width of the display | |
uint16_t get_width(); | |
/// @return Height of the display | |
uint16_t get_height(); | |
/// @brief Sets the brightness of the backlight | |
/// @param level A brightness level in the range [0; 255]. The perceived brightness is not a linear function of this value, however. A significant jump in visibility happens around a value of 45. | |
static void set_backlight(uint8_t level); | |
/// @brief Sends image data to the display. The given coordinates are inclusive. I.e., an x-range [x_start; x_end] = [0; 5] is 6 pixels wide (0, 1, 2, 3, 4, 5) | |
/// @param x_start Left target coordinate | |
/// @param x_end Right target coordinate | |
/// @param y_start Top target coordinate | |
/// @param y_end Bottom target coordinate | |
/// @param data The image data | |
static void draw_image(uint16_t x_start, uint16_t x_end, uint16_t y_start, uint16_t y_end, uint16_t* data); | |
/// @brief Sends a command to the display | |
/// @param message The command to send and its parameters | |
void send_command(const lcd_message& message); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
// https://github.com/Xinyuan-LilyGO/T-Display-S3/blob/main/example/factory/pin_config.h | |
/*ESP32S3*/ | |
// Backlight | |
#define PIN_LCD_BL 38 | |
// Data pins | |
#define PIN_LCD_D0 39 | |
#define PIN_LCD_D1 40 | |
#define PIN_LCD_D2 41 | |
#define PIN_LCD_D3 42 | |
#define PIN_LCD_D4 45 | |
#define PIN_LCD_D5 46 | |
#define PIN_LCD_D6 47 | |
#define PIN_LCD_D7 48 | |
// Display power | |
#define PIN_LCD_POWER 15 | |
// Display control pins | |
#define PIN_LCD_RES 5 // Reset | |
#define PIN_LCD_CS 6 // Chip select | |
#define PIN_LCD_DC 7 // Data/Command | |
#define PIN_LCD_WR 8 // Write | |
#define PIN_LCD_RD 9 // Read | |
#define PIN_BUTTON_1 0 // "Bot" (boot) button | |
#define PIN_BUTTON_2 14 // "Key" button | |
#define PIN_BAT_VOLT 4 // Battery voltage measurement | |
// I2C communication | |
#define PIN_IIC_SCL 17 // SCL | |
#define PIN_IIC_SDA 18 // SDA | |
// Touch screen (not available) | |
#define PIN_TOUCH_INT 16 // Touch interrupt | |
#define PIN_TOUCH_RES 21 // Touch reset | |
/* LCD CONFIG */ | |
#define EXAMPLE_LCD_PIXEL_CLOCK_HZ (6528000) //(10 * 1000 * 1000) | |
// The pixel number in horizontal and vertical | |
#define LCD_HOR_RES 320 | |
#define LCD_VER_RES 170 | |
#define LVGL_LCD_BUF_SIZE (LCD_HOR_RES * LCD_VER_RES) | |
#define EXAMPLE_PSRAM_DATA_ALIGNMENT 64 | |
#define LVGL_TICK_PERIOD_MS 2 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <Arduino.h> | |
#include <lvgl.h> | |
#include "display.h" | |
#include "hardware_setup.h" | |
uint64_t count = 0; | |
lv_obj_t* display_text; | |
void ui_init() | |
{ | |
const auto screen = lv_screen_active(); | |
lv_obj_set_style_bg_color(screen, lv_color_hex(0x003a57), LV_PART_MAIN); | |
display_text = lv_label_create(screen); | |
lv_label_set_text(display_text, "Hello, world!"); | |
lv_obj_set_style_text_color(screen, lv_color_hex(0xffffff), LV_PART_MAIN); | |
lv_obj_align(display_text, LV_ALIGN_CENTER, 0, 0); | |
} | |
void setup() { | |
Serial.setTxTimeoutMs(0); // Fix slow-down without serial connection. See: https://github.com/espressif/arduino-esp32/issues/6983#issuecomment-1346941805 | |
Serial.begin(115200); | |
sleep(1); // Wait a bit for Serial to become ready. Printing immediately doesn't work | |
Display::init(); | |
ui_init(); | |
} | |
void loop() { | |
lv_timer_periodic_handler(); // Need to call this here, since LVGL is not thread-safe. See Display::initialize_lvgl | |
Serial.print(count); | |
Serial.print(": "); | |
Serial.println("Oi!"); | |
++count; | |
std::array messages{"Oi!", "Bonjour!", "Hanloha!", "Ahoy!", "Aloha!", "Howdy!", "Hi-diddly-ho!"}; | |
lv_label_set_text(display_text, messages[count % messages.size()]); | |
delayMicroseconds(100000); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
; PlatformIO Project Configuration File | |
; | |
; Build options: build flags, source filter | |
; Upload options: custom upload port, speed and extra flags | |
; Library options: dependencies, extra library storages | |
; Advanced options: extra scripting | |
; | |
; Please visit documentation for the other options and examples | |
; https://docs.platformio.org/page/projectconf.html | |
[env:lilygo-t-display-s3] | |
; Using a custom toolchain to use GCC 13 instead of 8 and get access to C++20 | |
; https://community.platformio.org/t/c-20-ranges-library-fatal-error-ranges-no-such-file-or-directory/37771 | |
; https://esp32.com/viewtopic.php?f=19&t=37770 | |
; https://github.com/espressif/crosstool-NG/issues/48 | |
; https://github.com/Jason2866/platform-espressif32 | |
; https://github.com/orgs/espressif/projects/3/views/14 | |
; https://community.platformio.org/t/which-c-standard-am-i-using/24597 | |
; platform = espressif32 | |
; platform = https://github.com/platformio/platform-espressif32.git | |
; platform = https://github.com/tasmota/platform-espressif32/releases/download/2024.01.10/platform-espressif32.zip | |
;https://github.com/platformio/platform-espressif32/pull/1281 | |
platform = https://github.com/sgryphon/platform-espressif32.git#sgryphon/add-esp32-arduino-libs | |
platform_packages = | |
platformio/framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#master | |
platformio/framework-arduinoespressif32-libs @ https://github.com/espressif/esp32-arduino-libs.git#idf-release/v5.1 | |
; https://community.platformio.org/t/esp32-with-c-20-for-std-span/35279/ | |
; https://registry.platformio.org/tools/espressif/toolchain-riscv32-esp/versions | |
; https://registry.platformio.org/tools/espressif/toolchain-xtensa-esp32s3/versions | |
; platform_packages = espressif/toolchain-xtensa-esp32s3@12.2.0+20230208 | |
board = lilygo-t-display-s3 | |
framework = arduino | |
; framework = https://github.com/espressif/arduino-esp32.git | |
build_unflags = -std=gnu++11 | |
build_flags = -std=gnu++2b ;https://community.platformio.org/t/which-c-standard-am-i-using/24597/4 | |
-D LV_CONF_INCLUDE_SIMPLE | |
-D LV_CONF_SKIP ; Configure LVGL in platformio.ini instead of lv_conf.h - https://github.com/lvgl/lv_platformio/issues/28 | |
; Configuration instructions: https://docs.lvgl.io/master/porting/project.html | |
-D LV_COLOR_DEPTH=16 | |
-D LV_USE_ST7789 | |
-D LV_USE_LOG | |
-D LV_LOG_LEVEL=LV_LOG_LEVEL_INFO | |
-D LV_USE_ASSERT_NULL | |
lib_deps = lvgl = https://github.com/lvgl/lvgl.git@^9.0.0 | |
; lv_drivers = https://github.com/lvgl/lv_drivers | |
monitor_filters = esp32_exception_decoder, colorize, time ; debug | |
build_type = debug |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment