Skip to content

Instantly share code, notes, and snippets.

@LeCyberDucky
Created March 2, 2024 22:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LeCyberDucky/82b5af8070fdc07b6e98e8380f99a7a1 to your computer and use it in GitHub Desktop.
Save LeCyberDucky/82b5af8070fdc07b6e98e8380f99a7a1 to your computer and use it in GitHub Desktop.
LilyGo T-Display-S3 + LVGL minimal example
// 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);
}
#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);
};
#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
#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);
}
; 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