/* * Knob-Style Ebike Display * * HW: VIEWE UEDX46460015-MD50E Round Amoled Board */ #include #include #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" #include "driver/spi_master.h" #include "esp_timer.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_vendor.h" #include "esp_lcd_panel_ops.h" #include "esp_err.h" #include "esp_log.h" #include "lvgl.h" #include "esp_lcd_co5300.h" #include "ui.h" #include "lv_examples.h" #define LCD_HOST (SPI2_HOST) #define PIN_NUM_LCD_CS (GPIO_NUM_12) #define PIN_NUM_LCD_PCLK (GPIO_NUM_10) #define PIN_NUM_LCD_DATA0 (GPIO_NUM_13) #define PIN_NUM_LCD_DATA1 (GPIO_NUM_11) #define PIN_NUM_LCD_DATA2 (GPIO_NUM_14) #define PIN_NUM_LCD_DATA3 (GPIO_NUM_9) #define PIN_NUM_LCD_RST (GPIO_NUM_8) #define PIN_NUM_BK_LIGHT (GPIO_NUM_17) #define LCD_H_RES 471 #define LCD_V_RES 466 #define LCD_BIT_PER_PIXEL 16 #define LCD_BK_LIGHT_ON_LEVEL 1 #define LVGL_DRAW_BUF_LINES 20 // number of display lines in each draw buffer #define LVGL_TICK_PERIOD_MS 2 #define LVGL_TASK_MAX_DELAY_MS 500 #define LVGL_TASK_MIN_DELAY_MS 1000 / CONFIG_FREERTOS_HZ #define LVGL_TASK_STACK_SIZE (4 * 1024) #define LVGL_TASK_PRIORITY 2 static const char *TAG = "main"; extern void create_ui(); // Osptek Display // Part number: M151Q466466LK, R151ASM-00 // 1.51" 466*466 16.7M Colors AMOLED // IC: CO5300AF-42(Display);CST820(Touch);BV6802(Power); 1,843,200 bits internal GRAM memory (=230,4KB so half framebuffer) // Signal interface: QSPI static const co5300_lcd_init_cmd_t lcd_init_cmds[]= { // {cmd, { data }, data_size, delay_ms} {0x11, (uint8_t[]){0x00}, 0, 120}, // Sleep out + 120ms delay {0x34, (uint8_t[]){0x00}, 1, 0}, // Tearing effect off {0xFE, (uint8_t[]){0x00}, 1, 0}, // Page switch {0xC4, (uint8_t[]){0x80}, 1, 0}, // SPI Mode {0x3A, (uint8_t[]){0x55}, 1, 0}, // Pixel format 16bit/pixel {0x53, (uint8_t[]){0x20}, 1, 0}, // Write control display1 {0x63, (uint8_t[]){0xFF}, 1, 0}, // Write Display Brightness Value in HBM Mode {0x2A, (uint8_t[]){0x00, 0x00, 0x01, 0xD1}, 4, 0}, // Columns 0–465 (466 total) {0x2B, (uint8_t[]){0x00, 0x00, 0x01, 0xD1}, 4, 0}, // Rows 0–465 (466 total) {0x29, (uint8_t[]){0x00}, 0, 0}, // Display on {0x51, (uint8_t[]){0xFF}, 1, 0}, // Write Display Brightness Value in Normal Mode {0x58, (uint8_t[]){0x00}, 1, 0}, // High contrast mode OFF {0x00, (uint8_t[]){0x00}, 0, 10}, // End of commands }; // LVGL library = not thread-safe, so we will call LVGL APIs from different tasks and use a mutex to protect it static _lock_t lvgl_api_lock; static esp_lcd_panel_handle_t panel_handle = NULL; static bool notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx) { lv_display_t *disp = (lv_display_t *)user_ctx; lv_disp_flush_ready(disp); return false; // was true ? } void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { // LCD is big-endian, we need to swap the RGB bytes order lv_draw_sw_rgb565_swap(color_map, (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1)); // Call driver via esp_lcd command to draw stuff on the display esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_map); // flush_ready is called from the notify callback } // fixes animation glitches static void lvgl_display_rounder_callback(lv_event_t *e) { lv_area_t *area = (lv_area_t *)lv_event_get_param(e); area->x1 &= ~0x7U; area->y1 &= ~0x7U; area->x2 = (area->x2 & ~0x7U) + 7; area->y2 = (area->y2 & ~0x7U) + 7; } static void increase_lvgl_tick(void *arg) { lv_tick_inc(LVGL_TICK_PERIOD_MS); } void lvgl_display_init(void) { // Check backlight status and turn it on gpio_config_t bk_gpio_config = { .mode = GPIO_MODE_OUTPUT, .pin_bit_mask = 1ULL << PIN_NUM_BK_LIGHT }; ESP_ERROR_CHECK(gpio_config(&bk_gpio_config)); gpio_set_level(PIN_NUM_BK_LIGHT, LCD_BK_LIGHT_ON_LEVEL); ESP_LOGI(TAG, "Backlight turned on"); // Create and set up an lvgl display lv_display_t * disp = lv_display_create(LCD_H_RES, LCD_V_RES); lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565); lv_display_set_flush_cb(disp, lvgl_flush_cb); // add callback for rounder -> fixes animation glitches lv_display_add_event_cb(disp, lvgl_display_rounder_callback, LV_EVENT_INVALIDATE_AREA, panel_handle); // Double buffer size_t buf_size = (LCD_H_RES * LCD_V_RES / 10) * 2; // 2 bytes per pixel for RGB565 uint8_t *buf1 = heap_caps_aligned_alloc(64, buf_size, MALLOC_CAP_DMA); uint8_t *buf2 = heap_caps_aligned_alloc(64, buf_size, MALLOC_CAP_DMA); assert(buf1); assert(buf2); lv_display_set_buffers(disp, buf1, buf2, buf_size, LV_DISPLAY_RENDER_MODE_PARTIAL); // co5300 driver initialization ESP_LOGI(TAG, "Initialize QSPI bus"); spi_bus_config_t buscfg = CO5300_PANEL_BUS_QSPI_CONFIG( PIN_NUM_LCD_PCLK, PIN_NUM_LCD_DATA0, PIN_NUM_LCD_DATA1, PIN_NUM_LCD_DATA2, PIN_NUM_LCD_DATA3, LCD_H_RES * LCD_V_RES * LCD_BIT_PER_PIXEL / 8 ); ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO)); ESP_LOGI(TAG, "Install panel IO"); esp_lcd_panel_io_handle_t io_handle = NULL; esp_lcd_panel_io_spi_config_t io_config = { .cs_gpio_num = PIN_NUM_LCD_CS, .dc_gpio_num = -1, // No D/C line used in QSPI mode .spi_mode = 0, .pclk_hz = 40 * 1000 * 1000, // Increase clock from 40 MHz to 80 MHz .trans_queue_depth = 10, // Reasonable depth for async transfers .on_color_trans_done = notify_lvgl_flush_ready, // Callback to end LVGL flush .user_ctx = (void *)disp, // Display pointer .lcd_cmd_bits = 32, // As required by CO5300 .lcd_param_bits = 8, .flags = { .quad_mode = true, // Enable 4-bit data lines (QSPI) }, }; ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &io_handle)); ESP_LOGI(TAG, "Install CO5300 panel driver"); //esp_lcd_panel_handle_t panel_handle = NULL; Declared globally at the beginning co5300_vendor_config_t vendor_config = { .init_cmds = lcd_init_cmds, .init_cmds_size = sizeof(lcd_init_cmds) / sizeof(co5300_lcd_init_cmd_t), }; const esp_lcd_panel_dev_config_t panel_config = { .reset_gpio_num = PIN_NUM_LCD_RST, .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB, // Implemented by LCD command `36h` .bits_per_pixel = LCD_BIT_PER_PIXEL, // Implemented by LCD command `3Ah` (16/18/24) .vendor_config = (void *)&vendor_config, }; ESP_ERROR_CHECK(esp_lcd_new_panel_co5300(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_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, true)); ESP_LOGI(TAG, "Install LVGL tick timer"); // Tick interface for LVGL (using esp_timer to generate 2ms periodic event) const esp_timer_create_args_t lvgl_tick_timer_args = { .callback = &increase_lvgl_tick, .name = "lvgl_tick" }; esp_timer_handle_t lvgl_tick_timer = NULL; 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)); } static void 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); // in case of triggering a task watch dog time out time_till_next_ms = MAX(time_till_next_ms, LVGL_TASK_MIN_DELAY_MS); // in case of lvgl display not ready yet time_till_next_ms = MIN(time_till_next_ms, LVGL_TASK_MAX_DELAY_MS); usleep(1000 * time_till_next_ms); } } void app_main(void) { lv_init(); lvgl_display_init(); ESP_LOGI(TAG, "Create LVGL task"); xTaskCreate(lvgl_port_task, "LVGL", LVGL_TASK_STACK_SIZE, NULL, LVGL_TASK_PRIORITY, NULL); ESP_LOGI(TAG, "Lock & Create Spinner"); // Lock the mutex because LVGL APIs are not thread-safe _lock_acquire(&lvgl_api_lock); // create_ui(); // Spinner demo lv_obj_t * spinner = lv_spinner_create(lv_screen_active()); lv_obj_set_size(spinner, 300, 300); lv_obj_center(spinner); lv_spinner_set_anim_params(spinner, 10000, 200); _lock_release(&lvgl_api_lock); }