diff --git a/esp/cpp/07_lcd-panel/README.md b/esp/cpp/07_lcd-panel/README.md index 93ee9cc..17b8be8 100644 --- a/esp/cpp/07_lcd-panel/README.md +++ b/esp/cpp/07_lcd-panel/README.md @@ -29,3 +29,5 @@ cmake .. make -j $(nproc) make flash ``` + +[ESP-IDF I2C documentation](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/peripherals/i2c.html) \ No newline at end of file diff --git a/esp/cpp/07_lcd-panel/main/CMakeLists.txt b/esp/cpp/07_lcd-panel/main/CMakeLists.txt index c4ff593..096aef1 100644 --- a/esp/cpp/07_lcd-panel/main/CMakeLists.txt +++ b/esp/cpp/07_lcd-panel/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register( - SRCS "main.cpp" + SRCS main.cpp display.cpp INCLUDE_DIRS "." REQUIRES driver ) diff --git a/esp/cpp/07_lcd-panel/main/display.cpp b/esp/cpp/07_lcd-panel/main/display.cpp new file mode 100644 index 0000000..251f49c --- /dev/null +++ b/esp/cpp/07_lcd-panel/main/display.cpp @@ -0,0 +1,194 @@ +#include "display.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// To use LV_COLOR_FORMAT_I1, we need an extra buffer to hold the converted data +uint8_t Display::oled_buffer_[LCD_H_RES * LCD_V_RES / 8]; + +_lock_t Display::lvgl_api_lock_; + +Display::Display() : + io_handle_(nullptr), + panel_handle_(nullptr), + i2c_bus_(nullptr), + buf_(nullptr) +{ + init_i2c(); + + ESP_LOGI(TAG, "Install SSD1306 panel driver"); + ssd1306_config_ = { + .height = LCD_V_RES, + }; + panel_config_ = { + .reset_gpio_num = PIN_RST, + .bits_per_pixel = 1, + .vendor_config = &ssd1306_config_ + }; + ESP_ERROR_CHECK( + esp_lcd_new_panel_ssd1306(io_handle_, &panel_config_, &panel_handle_)); + + ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle_)); + ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle_)); + ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle_, true)); + + ESP_LOGI(TAG, "Initialize LVGL"); + lv_init(); + // create a lvgl display + display_ = lv_display_create(LCD_H_RES, LCD_V_RES); + // associate the i2c panel handle to the display + lv_display_set_user_data(display_, panel_handle_); + // create draw buffer + ESP_LOGI(TAG, "Allocate separate LVGL draw buffers"); + // LVGL reserves 2 x 4 bytes in the buffer, as these are assumed to be used as a palette. + size_t draw_buffer_sz = LCD_H_RES * LCD_V_RES / 8 + LVGL_PALETTE_SIZE; + buf_ = heap_caps_calloc(1, draw_buffer_sz, + MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + assert(buf_); + + // LVGL9 suooprt new monochromatic format. + lv_display_set_color_format(display_, LV_COLOR_FORMAT_I1); + // initialize LVGL draw buffers + lv_display_set_buffers(display_, buf_, nullptr, draw_buffer_sz, + LV_DISPLAY_RENDER_MODE_FULL); + lv_display_set_rotation(display_, LV_DISPLAY_ROTATION_0); + // set the callback which can copy the rendered image to an area of the display + lv_display_set_flush_cb(display_, Display::lvgl_flush_cb); + + ESP_LOGI(TAG, + "Register io panel event callback for LVGL flush ready notification"); + const esp_lcd_panel_io_callbacks_t cbs = { + .on_color_trans_done = Display::lvgl_flush_ready, + }; + /* Register done callback */ + ESP_ERROR_CHECK( + esp_lcd_panel_io_register_event_callbacks(io_handle_, &cbs, display_)); + + ESP_LOGI(TAG, "Use esp_timer as LVGL tick timer"); + const esp_timer_create_args_t lvgl_tick_timer_args = { + .callback = &Display::lvgl_increase_tick, + .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)); + + ESP_LOGI(TAG, "Create LVGL task"); + xTaskCreate(Display::lvgl_port_task, "LVGL", LVGL_TASK_STACK_SIZE, + nullptr, LVGL_TASK_PRIORITY, nullptr); +} + +bool Display::lvgl_flush_ready(esp_lcd_panel_io_handle_t, + esp_lcd_panel_io_event_data_t *, + void *user_ctx) +{ + auto *disp = (lv_display_t *) user_ctx; + lv_display_flush_ready(disp); + return false; +} + +void Display::lvgl_flush_cb(lv_display_t *display, const lv_area_t *area, + uint8_t *px_map) +{ + auto panel_handle = + (esp_lcd_panel_handle_t) lv_display_get_user_data(display); + + // This is necessary because LVGL reserves 2 x 4 bytes in the buffer, as these are assumed to be used as a palette. Skip the palette here + // More information about the monochrome, please refer to https://docs.lvgl.io/9.2/porting/display.html#monochrome-displays + px_map += LVGL_PALETTE_SIZE; + + uint16_t hor_res = lv_display_get_physical_horizontal_resolution(display); + int32_t x1 = area->x1; + int32_t x2 = area->x2; + int32_t y1 = area->y1; + int32_t y2 = area->y2; + + for (int32_t y = y1; y <= y2; y++) { + for (int32_t x = x1; x <= x2; x++) { + /* The order of bits is MSB first + MSB LSB + bits 7 6 5 4 3 2 1 0 + pixels 0 1 2 3 4 5 6 7 + Left Right + */ + bool chroma_color = (px_map[(hor_res >> 3) * y + (x >> 3)] & + 1 << (7 - x % 8)); + + /* Write to the buffer as required for the display. + * It writes only 1-bit for monochrome displays mapped vertically.*/ + uint8_t *buf = oled_buffer_ + hor_res * (y >> 3) + (x); + if (chroma_color) { + (*buf) &= ~(1 << (y % 8)); + } else { + (*buf) |= (1 << (y % 8)); + } + } + } + // pass the draw buffer to the driver + ESP_ERROR_CHECK( + esp_lcd_panel_draw_bitmap(panel_handle, x1, y1, x2 + 1, y2 + 1, + oled_buffer_)); +} + +void Display::lvgl_increase_tick(void *arg) +{ + /* Tell LVGL how many milliseconds has elapsed */ + lv_tick_inc(LVGL_TICK_PERIOD_MS); +} + +void Display::lvgl_port_task(void *arg) +{ + ESP_LOGI(TAG, "Starting LVGL task"); + uint32_t time_till_next_ms = 0; + while (1) { + _lock_acquire(&lvgl_api_lock_); + time_till_next_ms = lv_timer_handler(); + _lock_release(&lvgl_api_lock_); + usleep(1000 * time_till_next_ms); + } +} + +void Display::init_i2c() +{ + ESP_LOGI(TAG, "Initialize I2C bus"); + i2c_master_bus_config_t bus_config = { + .i2c_port = I2C_BUS_PORT, + .sda_io_num = PIN_SDA, + .scl_io_num = PIN_SCL, + .clk_source = I2C_CLK_SRC_DEFAULT, + .glitch_ignore_cnt = 7, + .flags { + .enable_internal_pullup = true, + }, + }; + ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &i2c_bus_)); + + ESP_LOGI(TAG, "Install panel IO"); + esp_lcd_panel_io_i2c_config_t io_config = { + .dev_addr = I2C_HW_ADDR, + .control_phase_bytes = 1, // According to SSD1306 datasheet + .dc_bit_offset = 6, // According to SSD1306 datasheet + .lcd_cmd_bits = LCD_CMD_BITS, // According to SSD1306 datasheet + .lcd_param_bits = LCD_CMD_BITS, // According to SSD1306 datasheet + .scl_speed_hz = LCD_PIXEL_CLOCK_HZ, + }; + ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus_, &io_config, &io_handle_)); +} + +//SSD1306::SSD1306() { +// +//} +//void *SSD1306::vendor_config() +//{ +// return &ssd1306_config_; +//} + + diff --git a/esp/cpp/07_lcd-panel/main/display.h b/esp/cpp/07_lcd-panel/main/display.h new file mode 100644 index 0000000..027fed9 --- /dev/null +++ b/esp/cpp/07_lcd-panel/main/display.h @@ -0,0 +1,95 @@ +#ifndef DISPLAY_H +#define DISPLAY_H + +#include +#include +#include +#include "misc/lv_types.h" +#include "misc/lv_area.h" + +#define I2C_BUS_PORT 0 +#define LVGL_TICK_PERIOD_MS 5 +#define LVGL_TASK_STACK_SIZE (4 * 1024) +#define LVGL_TASK_PRIORITY 2 +#define LVGL_PALETTE_SIZE 8 + +static const char *TAG = "lcd-panel"; + +// https://www.digikey.com/en/products/detail/winstar-display/WEA012864DWPP3N00003/20533255 +// According to SSD1306 datasheet +// Bit number used to represent command and parameter +#define SCREEN_WIDTH 128 // OLED display width, in pixels +#define SCREEN_HEIGHT 64 // OLED display height, in pixels +#define LCD_H_RES SCREEN_WIDTH +#define LCD_V_RES SCREEN_HEIGHT +#define I2C_HW_ADDR 0x3C +#define LCD_PIXEL_CLOCK_HZ (400 * 1000) +#define LCD_CMD_BITS 8 +#define LCD_PARAM_BITS 8 + +// Pin may vary based on your schematic +#define PIN_SDA GPIO_NUM_21 +#define PIN_SCL GPIO_NUM_22 +#define PIN_RST -1 + +class Display { +public: + Display(); + + ~Display() = default; + + [[nodiscard]] const lv_display_t *get_display() const { return display_; } + + [[nodiscard]] lv_display_t *get_display() { return display_; } + + static bool lvgl_flush_ready(esp_lcd_panel_io_handle_t panel, + esp_lcd_panel_io_event_data_t *data, + void *user_ctx); + + static void + lvgl_flush_cb(lv_display_t *display, const lv_area_t *area, uint8_t *px_map); + + static void lvgl_increase_tick(void *arg); + + static void lvgl_port_task(void *arg); + + // To use LV_COLOR_FORMAT_I1, we need an extra buffer to hold the converted data + static uint8_t oled_buffer_[LCD_H_RES * LCD_V_RES / 8]; + + // LVGL library is not thread-safe, this example will call LVGL APIs from different tasks, so use a mutex to protect it + static _lock_t lvgl_api_lock_; + +protected: + esp_lcd_panel_io_handle_t io_handle_; + + esp_lcd_panel_handle_t panel_handle_; + + esp_lcd_panel_dev_config_t panel_config_; + + esp_lcd_panel_ssd1306_config_t ssd1306_config_; + + i2c_master_bus_handle_t i2c_bus_; + +private: + void init_i2c(); + +// virtual void *vendor_config() = 0; + + lv_display_t *display_; + + void * vendor_config_; + void * buf_; +}; + +//class SSD1306 : public Display { +//public: +// SSD1306(); +// +// void * vendor_config() override; +// +//private: +// esp_lcd_panel_dev_config_t panel_config_; +// esp_lcd_panel_ssd1306_config_t ssd1306_config_; +//}; + +#endif // DISPLAY_H diff --git a/esp/cpp/07_lcd-panel/main/main.cpp b/esp/cpp/07_lcd-panel/main/main.cpp index 3f3af90..5c79374 100644 --- a/esp/cpp/07_lcd-panel/main/main.cpp +++ b/esp/cpp/07_lcd-panel/main/main.cpp @@ -1,237 +1,33 @@ -/* - * SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: CC0-1.0 - */ +#include "display.h" -#include -#include -#include - -#include "main.h" - -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_timer.h" -#include "esp_lcd_panel_io.h" -#include "esp_lcd_panel_ops.h" -#include "esp_err.h" #include "esp_log.h" -#include "driver/i2c_master.h" #include "lvgl.h" -#include "esp_lcd_panel_vendor.h" - -#define I2C_BUS_PORT 0 -#define LVGL_TICK_PERIOD_MS 5 -#define LVGL_TASK_STACK_SIZE (4 * 1024) -#define LVGL_TASK_PRIORITY 2 -#define LVGL_PALETTE_SIZE 8 - -static const char *TAG = "lcd-panel"; - -// https://www.digikey.com/en/products/detail/winstar-display/WEA012864DWPP3N00003/20533255 -// According to SSD1306 datasheet -// Bit number used to represent command and parameter -#define SCREEN_WIDTH 128 // OLED display width, in pixels -#define SCREEN_HEIGHT 64 // OLED display height, in pixels -#define LCD_H_RES SCREEN_WIDTH -#define LCD_V_RES SCREEN_HEIGHT -#define I2C_HW_ADDR 0x3C -#define LCD_PIXEL_CLOCK_HZ (400 * 1000) -#define LCD_CMD_BITS 8 -#define LCD_PARAM_BITS 8 - -// Pin may vary based on your schematic -#define PIN_SDA GPIO_NUM_21 -#define PIN_SCL GPIO_NUM_22 -#define PIN_RST -1 - -// To use LV_COLOR_FORMAT_I1, we need an extra buffer to hold the converted data -static uint8_t oled_buffer[LCD_H_RES * LCD_V_RES / 8]; -// LVGL library is not thread-safe, this example will call LVGL APIs from different tasks, so use a mutex to protect it -static _lock_t lvgl_api_lock; - -static bool example_notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t io_panel, - esp_lcd_panel_io_event_data_t *edata, - void *user_ctx) -{ - auto *disp = (lv_display_t *) user_ctx; - lv_display_flush_ready(disp); - return false; -} - -static void example_lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, - uint8_t *px_map) -{ - auto panel_handle = (esp_lcd_panel_handle_t) lv_display_get_user_data(disp); - - // This is necessary because LVGL reserves 2 x 4 bytes in the buffer, as these are assumed to be used as a palette. Skip the palette here - // More information about the monochrome, please refer to https://docs.lvgl.io/9.2/porting/display.html#monochrome-displays - px_map += LVGL_PALETTE_SIZE; - - uint16_t hor_res = lv_display_get_physical_horizontal_resolution(disp); - int x1 = area->x1; - int x2 = area->x2; - int y1 = area->y1; - int y2 = area->y2; - - for (int y = y1; y <= y2; y++) { - for (int x = x1; x <= x2; x++) { - /* The order of bits is MSB first - MSB LSB - bits 7 6 5 4 3 2 1 0 - pixels 0 1 2 3 4 5 6 7 - Left Right - */ - bool chroma_color = (px_map[(hor_res >> 3) * y + (x >> 3)] & - 1 << (7 - x % 8)); - - /* Write to the buffer as required for the display. - * It writes only 1-bit for monochrome displays mapped vertically.*/ - uint8_t *buf = oled_buffer + hor_res * (y >> 3) + (x); - if (chroma_color) { - (*buf) &= ~(1 << (y % 8)); - } else { - (*buf) |= (1 << (y % 8)); - } - } - } - // pass the draw buffer to the driver - ESP_ERROR_CHECK( - esp_lcd_panel_draw_bitmap(panel_handle, x1, y1, x2 + 1, y2 + 1, - oled_buffer)); -} - -static void example_increase_lvgl_tick(void *arg) -{ - /* Tell LVGL how many milliseconds has elapsed */ - lv_tick_inc(LVGL_TICK_PERIOD_MS); -} - -static void example_lvgl_port_task(void *arg) -{ - ESP_LOGI(TAG, "Starting LVGL task"); - uint32_t time_till_next_ms = 0; - while (1) { - _lock_acquire(&lvgl_api_lock); - time_till_next_ms = lv_timer_handler(); - _lock_release(&lvgl_api_lock); - usleep(1000 * time_till_next_ms); - } -} void setup() { - ESP_LOGI(TAG, "Initialize I2C bus"); - i2c_master_bus_handle_t i2c_bus = nullptr; - i2c_master_bus_config_t bus_config = { - .i2c_port = I2C_BUS_PORT, - .sda_io_num = PIN_SDA, - .scl_io_num = PIN_SCL, - .clk_source = I2C_CLK_SRC_DEFAULT, - .glitch_ignore_cnt = 7, - .flags { - .enable_internal_pullup = true, - }, - }; - ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &i2c_bus)); - - ESP_LOGI(TAG, "Install panel IO"); - esp_lcd_panel_io_handle_t io_handle = nullptr; - esp_lcd_panel_io_i2c_config_t io_config = { - .dev_addr = I2C_HW_ADDR, - .control_phase_bytes = 1, // According to SSD1306 datasheet - .dc_bit_offset = 6, // According to SSD1306 datasheet - .lcd_cmd_bits = LCD_CMD_BITS, // According to SSD1306 datasheet - .lcd_param_bits = LCD_CMD_BITS, // According to SSD1306 datasheet - .scl_speed_hz = LCD_PIXEL_CLOCK_HZ, - }; - ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c(i2c_bus, &io_config, &io_handle)); - - ESP_LOGI(TAG, "Install SSD1306 panel driver"); - esp_lcd_panel_handle_t panel_handle = nullptr; - esp_lcd_panel_dev_config_t panel_config = { - .reset_gpio_num = PIN_RST, - .bits_per_pixel = 1, - }; - esp_lcd_panel_ssd1306_config_t ssd1306_config = { - .height = LCD_V_RES, - }; - panel_config.vendor_config = &ssd1306_config; - ESP_ERROR_CHECK( - esp_lcd_new_panel_ssd1306(io_handle, &panel_config, &panel_handle)); - - ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle)); - ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle)); - ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true)); - - ESP_LOGI(TAG, "Initialize LVGL"); - lv_init(); - // create a lvgl display - lv_display_t *display = lv_display_create(LCD_H_RES, LCD_V_RES); - // associate the i2c panel handle to the display - lv_display_set_user_data(display, panel_handle); - // create draw buffer - void *buf = nullptr; - ESP_LOGI(TAG, "Allocate separate LVGL draw buffers"); - // LVGL reserves 2 x 4 bytes in the buffer, as these are assumed to be used as a palette. - size_t draw_buffer_sz = LCD_H_RES * LCD_V_RES / 8 + LVGL_PALETTE_SIZE; - buf = heap_caps_calloc(1, draw_buffer_sz, - MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); - assert(buf); - - // LVGL9 suooprt new monochromatic format. - lv_display_set_color_format(display, LV_COLOR_FORMAT_I1); - // initialize LVGL draw buffers - lv_display_set_buffers(display, buf, nullptr, draw_buffer_sz, - LV_DISPLAY_RENDER_MODE_FULL); - lv_display_set_rotation(display, LV_DISPLAY_ROTATION_90); - // set the callback which can copy the rendered image to an area of the display - lv_display_set_flush_cb(display, example_lvgl_flush_cb); - - ESP_LOGI(TAG, - "Register io panel event callback for LVGL flush ready notification"); - const esp_lcd_panel_io_callbacks_t cbs = { - .on_color_trans_done = example_notify_lvgl_flush_ready, - }; - /* Register done callback */ - ESP_ERROR_CHECK( - esp_lcd_panel_io_register_event_callbacks(io_handle, &cbs, display)); - - ESP_LOGI(TAG, "Use esp_timer as LVGL tick timer"); - const esp_timer_create_args_t lvgl_tick_timer_args = { - .callback = &example_increase_lvgl_tick, - .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)); - - ESP_LOGI(TAG, "Create LVGL task"); - xTaskCreate(example_lvgl_port_task, "LVGL", LVGL_TASK_STACK_SIZE, - nullptr, LVGL_TASK_PRIORITY, nullptr); + Display d; ESP_LOGI(TAG, "Display LVGL Scroll Text"); // Lock the mutex due to the LVGL APIs are not thread-safe - _lock_acquire(&lvgl_api_lock); + _lock_acquire(&Display::lvgl_api_lock_); // UI function { - lv_obj_t *scr = lv_display_get_screen_active(display); + lv_obj_t *scr = lv_display_get_screen_active(d.get_display()); lv_obj_t *label = lv_label_create(scr); // Circular scroll lv_label_set_long_mode(label, - LV_LABEL_LONG_WRAP); + LV_LABEL_LONG_SCROLL_CIRCULAR); lv_label_set_text(label, "Hello hello hello hello hello hello hello hello."); // Size of the screen // if you use rotation 90 or 270 use lv_display_get_vertical_resolution - lv_obj_set_width(label, lv_display_get_horizontal_resolution(display)); + lv_obj_set_width(label, + lv_display_get_horizontal_resolution(d.get_display())); lv_obj_align(label, LV_ALIGN_TOP_MID, 0, 0); } - _lock_release(&lvgl_api_lock); + _lock_release(&Display::lvgl_api_lock_); } void loop() { } diff --git a/esp/cpp/07_lcd-panel/main/main.h b/esp/cpp/07_lcd-panel/main/main.h index e70e1fc..7c85b38 100644 --- a/esp/cpp/07_lcd-panel/main/main.h +++ b/esp/cpp/07_lcd-panel/main/main.h @@ -1,13 +1,4 @@ #ifndef MAIN_H #define MAIN_H -class Display { -public: - Display(); - ~Display(); - -private: - -}; - #endif // MAIN_H