From dd4a8749ba37589fca8d4ffa74ca228f84e6445d Mon Sep 17 00:00:00 2001 From: Valentin Roland Date: Sat, 10 Feb 2024 23:58:29 +0100 Subject: [PATCH] refactor lcd driver initialization --- examples/test/main/CMakeLists.txt | 2 + src/output_lcd/lcd_driver.c | 547 +++++++++++++++++------------- src/output_lcd/lcd_driver.h | 1 + 3 files changed, 308 insertions(+), 242 deletions(-) diff --git a/examples/test/main/CMakeLists.txt b/examples/test/main/CMakeLists.txt index 52a3b94c..e727913e 100644 --- a/examples/test/main/CMakeLists.txt +++ b/examples/test/main/CMakeLists.txt @@ -1 +1,3 @@ +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + idf_component_register(SRCS "main.c" INCLUDE_DIRS ".") diff --git a/src/output_lcd/lcd_driver.c b/src/output_lcd/lcd_driver.c index 6523a204..7281ed66 100644 --- a/src/output_lcd/lcd_driver.c +++ b/src/output_lcd/lcd_driver.c @@ -6,70 +6,75 @@ #ifdef RENDER_METHOD_LCD #include +#include +#include +#include +#include #include #include #include #include -#include -#include -#include -#include #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) +#include #include #include -#include -#include #include +#include #else -#include #include #include #include +#include #include "idf-4-backports.h" #endif -#include -#include -#include -#include -#include -#include #include -#include #include #include -#include -#include -#include -#include -#include -#include -#include +#include +#include #include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include #include +#include +#include #include "../output_common/lut.h" #define TAG "epdiy" -static inline int min(int x, int y) { return x < y ? x : y; } -static inline int max(int x, int y) { return x > y ? x : y; } +static inline int min(int x, int y) { + return x < y ? x : y; +} +static inline int max(int x, int y) { + return x > y ? x : y; +} -#define S3_LCD_PIN_NUM_BK_LIGHT -1 -//#define S3_LCD_PIN_NUM_MODE 4 +#define S3_LCD_PIN_NUM_BK_LIGHT -1 +// #define S3_LCD_PIN_NUM_MODE 4 -#define LINE_BATCH 1000 -#define BOUNCE_BUF_LINES 4 +#define LINE_BATCH 1000 +#define BOUNCE_BUF_LINES 4 -#define RMT_CKV_CHAN RMT_CHANNEL_1 +#define RMT_CKV_CHAN RMT_CHANNEL_1 #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) -// The extern line is declared in esp-idf/components/driver/deprecated/rmt_legacy.c. It has access to RMTMEM through the rmt_private.h header -// which we can't access outside the sdk. Declare our own extern here to properly use the RMTMEM smybol defined in components/soc/[target]/ld/[target].peripherals.ld -// Also typedef the new rmt_mem_t struct to the old rmt_block_mem_t struct. Same data fields, different names -typedef rmt_mem_t rmt_block_mem_t ; +// The extern line is declared in esp-idf/components/driver/deprecated/rmt_legacy.c. It has access +// to RMTMEM through the rmt_private.h header which we can't access outside the sdk. Declare our own +// extern here to properly use the RMTMEM smybol defined in +// components/soc/[target]/ld/[target].peripherals.ld Also typedef the new rmt_mem_t struct to the +// old rmt_block_mem_t struct. Same data fields, different names +typedef rmt_mem_t rmt_block_mem_t; extern rmt_block_mem_t RMTMEM; #endif @@ -92,7 +97,7 @@ typedef struct { LcdEpdConfig_t config; - uint8_t *bounce_buffer[2]; + uint8_t* bounce_buffer[2]; // size of a single bounce buffer size_t bb_size; size_t batches; @@ -103,14 +108,22 @@ typedef struct { gdma_channel_handle_t dma_chan; // DMA descriptors pool dma_descriptor_t* dma_nodes; + + /// The number of bytes in a horizontal display register line. + int line_bytes; + + // With 8 bit bus width, we need a dummy cycle before the actual data, + // because the LCD peripheral behaves weirdly. + // Also see: + // https://blog.adafruit.com/2022/06/14/esp32uesday-hacking-the-esp32-s3-lcd-peripheral/ + int dummy_bytes; + + /// The number of lines of the display + int display_lines; } s3_lcd_t; static s3_lcd_t lcd; -/// The number of bytes in a horizontal display register line. -static int line_bytes = 0; -static int vertical_lines = 0; - void IRAM_ATTR epd_lcd_line_source_cb(line_cb_func_t line_source, void* payload) { lcd.line_source_cb = line_source; lcd.line_cb_payload = payload; @@ -125,22 +138,24 @@ static IRAM_ATTR bool fill_bounce_buffer(uint8_t* buffer) { bool task_awoken = false; // a dummy byte is neeed in 8 bit mode to work around LCD peculiarities - int dummy_bytes = lcd.bb_size / BOUNCE_BUF_LINES - line_bytes; - - for (int i=0; i < BOUNCE_BUF_LINES; i++) { - if (lcd.line_source_cb != NULL) { - // this is strange, with 16 bit need a dummy cycle. But still, the first byte in the FIFO is correct. - // So we only need a true dummy byte in the FIFO in the 8 bit configuration. - task_awoken |= lcd.line_source_cb(lcd.line_cb_payload, &buffer[i * (line_bytes + dummy_bytes) + (dummy_bytes % 2)]); + int dummy_bytes = lcd.bb_size / BOUNCE_BUF_LINES - lcd.line_bytes; + + for (int i = 0; i < BOUNCE_BUF_LINES; i++) { + if (lcd.line_source_cb != NULL) { + // this is strange, with 16 bit need a dummy cycle. But still, the first byte in the + // FIFO is correct. So we only need a true dummy byte in the FIFO in the 8 bit + // configuration. + task_awoken |= lcd.line_source_cb( + lcd.line_cb_payload, &buffer[i * (lcd.line_bytes + dummy_bytes) + (dummy_bytes % 2)] + ); } else { - memset(&buffer[i * line_bytes], 0x00, line_bytes); + memset(&buffer[i * lcd.line_bytes], 0x00, lcd.line_bytes); } } return task_awoken; } static void start_ckv_cycles(int cycles) { - rmt_ll_tx_enable_loop_count(&RMT, RMT_CKV_CHAN, true); rmt_ll_tx_enable_loop_autostop(&RMT, RMT_CKV_CHAN, true); rmt_ll_tx_set_loop_count(&RMT, RMT_CKV_CHAN, cycles); @@ -148,130 +163,77 @@ static void start_ckv_cycles(int cycles) { rmt_ll_tx_start(&RMT, RMT_CKV_CHAN); } -void IRAM_ATTR epd_lcd_start_frame() { - int initial_lines = min(LINE_BATCH, vertical_lines); - - int dummy_bytes = lcd.bb_size / BOUNCE_BUF_LINES - line_bytes; - - // hsync: pulse with, back porch, active width, front porch - int end_line = lcd.line_cycles - lcd.lcd_res_h - lcd.config.le_high_time - lcd.config.line_front_porch; - lcd_ll_set_horizontal_timing(lcd.hal.dev, - lcd.config.le_high_time - (dummy_bytes > 0), - lcd.config.line_front_porch, - // a dummy byte is neeed in 8 bit mode to work around LCD peculiarities - lcd.lcd_res_h + (dummy_bytes > 0), - end_line - ); - lcd_ll_set_vertical_timing(lcd.hal.dev, 1, 1, initial_lines, 1); - - // generate the hsync at the very beginning of line - lcd_ll_set_hsync_position(lcd.hal.dev, 1); - - //gpio_set_level(S3_LCD_PIN_NUM_MODE, 1); - - // reset FIFO of DMA and LCD, incase there remains old frame data - gdma_reset(lcd.dma_chan); - lcd_ll_stop(lcd.hal.dev); - lcd_ll_fifo_reset(lcd.hal.dev); - lcd_ll_enable_auto_next_frame(lcd.hal.dev, true); - - lcd.batches = 0; - fill_bounce_buffer(lcd.bounce_buffer[0]); - fill_bounce_buffer(lcd.bounce_buffer[1]); - - - // the start of DMA should be prior to the start of LCD engine - gdma_start(lcd.dma_chan, (intptr_t)&lcd.dma_nodes[0]); - - // enter a critical section to ensure the frame start timing is correct - taskENTER_CRITICAL(&frame_start_spinlock); - - // delay 1us is sufficient for DMA to pass data to LCD FIFO - // in fact, this is only needed when LCD pixel clock is set too high - gpio_set_level(lcd.config.bus.stv, 0); - //esp_rom_delay_us(1); - // for picture clarity, it seems to be important to start CKV at a "good" - // time, seemingly start or towards end of line. - start_ckv_cycles(initial_lines + 5); - esp_rom_delay_us(lcd.line_length_us); - gpio_set_level(lcd.config.bus.stv, 1); - esp_rom_delay_us(lcd.line_length_us); - esp_rom_delay_us(lcd.config.ckv_high_time / 10); - - // start LCD engine - lcd_ll_start(lcd.hal.dev); - - taskEXIT_CRITICAL(&frame_start_spinlock); -} - /** * Build the RMT signal according to the timing set in the lcd object. */ static void ckv_rmt_build_signal() { - int low_time = (lcd.line_length_us * 10 - lcd.config.ckv_high_time); - volatile rmt_item32_t *rmt_mem_ptr = - &(RMTMEM.chan[RMT_CKV_CHAN].data32[0]); + int low_time = (lcd.line_length_us * 10 - lcd.config.ckv_high_time); + volatile rmt_item32_t* rmt_mem_ptr = &(RMTMEM.chan[RMT_CKV_CHAN].data32[0]); rmt_mem_ptr->duration0 = lcd.config.ckv_high_time; rmt_mem_ptr->level0 = 1; rmt_mem_ptr->duration1 = low_time; rmt_mem_ptr->level1 = 0; - //rmt_mem_ptr[1] = rmt_mem_ptr[0]; - rmt_mem_ptr[1].val = 0; + // rmt_mem_ptr[1] = rmt_mem_ptr[0]; + rmt_mem_ptr[1].val = 0; } static void init_ckv_rmt() { - periph_module_reset(rmt_periph_signals.groups[0].module); - periph_module_enable(rmt_periph_signals.groups[0].module); + periph_module_reset(rmt_periph_signals.groups[0].module); + periph_module_enable(rmt_periph_signals.groups[0].module); - rmt_ll_enable_periph_clock(&RMT, true); + rmt_ll_enable_periph_clock(&RMT, true); - // Divide 80MHz APB Clock by 8 -> .1us resolution delay - // idf >= 5.0 calculates the clock divider differently + // Divide 80MHz APB Clock by 8 -> .1us resolution delay + // idf >= 5.0 calculates the clock divider differently #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) - rmt_ll_set_group_clock_src(&RMT, RMT_CKV_CHAN, (rmt_clock_source_t)RMT_BASECLK_DEFAULT, 1, 0, 0); + rmt_ll_set_group_clock_src( + &RMT, RMT_CKV_CHAN, (rmt_clock_source_t)RMT_BASECLK_DEFAULT, 1, 0, 0 + ); #else - rmt_ll_set_group_clock_src(&RMT, RMT_CKV_CHAN, (rmt_clock_source_t)RMT_BASECLK_DEFAULT, 0, 0, 0); + rmt_ll_set_group_clock_src( + &RMT, RMT_CKV_CHAN, (rmt_clock_source_t)RMT_BASECLK_DEFAULT, 0, 0, 0 + ); #endif - rmt_ll_tx_set_channel_clock_div(&RMT, RMT_CKV_CHAN, 8); - rmt_ll_tx_set_mem_blocks(&RMT, RMT_CKV_CHAN, 2); - rmt_ll_enable_mem_access_nonfifo(&RMT, true); - rmt_ll_tx_fix_idle_level(&RMT, RMT_CKV_CHAN, RMT_IDLE_LEVEL_LOW, true); - rmt_ll_tx_enable_carrier_modulation(&RMT, RMT_CKV_CHAN, false); - - rmt_ll_tx_enable_loop(&RMT, RMT_CKV_CHAN, true); - - gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[lcd.config.bus.ckv], PIN_FUNC_GPIO); - gpio_set_direction(lcd.config.bus.ckv, GPIO_MODE_OUTPUT); - esp_rom_gpio_connect_out_signal(lcd.config.bus.ckv, rmt_periph_signals.groups[0].channels[RMT_CKV_CHAN].tx_sig, false, 0); + rmt_ll_tx_set_channel_clock_div(&RMT, RMT_CKV_CHAN, 8); + rmt_ll_tx_set_mem_blocks(&RMT, RMT_CKV_CHAN, 2); + rmt_ll_enable_mem_access_nonfifo(&RMT, true); + rmt_ll_tx_fix_idle_level(&RMT, RMT_CKV_CHAN, RMT_IDLE_LEVEL_LOW, true); + rmt_ll_tx_enable_carrier_modulation(&RMT, RMT_CKV_CHAN, false); + + rmt_ll_tx_enable_loop(&RMT, RMT_CKV_CHAN, true); + + gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[lcd.config.bus.ckv], PIN_FUNC_GPIO); + gpio_set_direction(lcd.config.bus.ckv, GPIO_MODE_OUTPUT); + esp_rom_gpio_connect_out_signal( + lcd.config.bus.ckv, rmt_periph_signals.groups[0].channels[RMT_CKV_CHAN].tx_sig, false, 0 + ); - ckv_rmt_build_signal(); + ckv_rmt_build_signal(); } -__attribute__((optimize("O3"))) -IRAM_ATTR static void lcd_isr_vsync(void *args) -{ +__attribute__((optimize("O3"))) IRAM_ATTR static void lcd_isr_vsync(void* args) { bool need_yield = false; uint32_t intr_status = lcd_ll_get_interrupt_status(lcd.hal.dev); lcd_ll_clear_interrupt_status(lcd.hal.dev, intr_status); if (intr_status & LCD_LL_EVENT_VSYNC_END) { - int batches_needed = vertical_lines / LINE_BATCH ; + int batches_needed = lcd.display_lines / LINE_BATCH; if (lcd.batches >= batches_needed) { lcd_ll_stop(lcd.hal.dev); - //rmt_ll_tx_stop(&RMT, RMT_CKV_CHAN); + // rmt_ll_tx_stop(&RMT, RMT_CKV_CHAN); if (lcd.frame_done_cb != NULL) { (*lcd.frame_done_cb)(lcd.frame_cb_payload); } - //gpio_set_level(S3_LCD_PIN_NUM_MODE, 0); + // gpio_set_level(S3_LCD_PIN_NUM_MODE, 0); } else { int ckv_cycles = 0; // last batch if (lcd.batches == batches_needed - 1) { lcd_ll_enable_auto_next_frame(lcd.hal.dev, false); - lcd_ll_set_vertical_timing(lcd.hal.dev, 1, 0, vertical_lines % LINE_BATCH, 10); - ckv_cycles = vertical_lines % LINE_BATCH + 10; + lcd_ll_set_vertical_timing(lcd.hal.dev, 1, 0, lcd.display_lines % LINE_BATCH, 10); + ckv_cycles = lcd.display_lines % LINE_BATCH + 10; } else { lcd_ll_set_vertical_timing(lcd.hal.dev, 1, 0, LINE_BATCH, 1); ckv_cycles = LINE_BATCH + 1; @@ -294,9 +256,12 @@ IRAM_ATTR static void lcd_isr_vsync(void *args) }; // ISR handling bounce buffer refill -static IRAM_ATTR bool lcd_rgb_panel_eof_handler(gdma_channel_handle_t dma_chan, gdma_event_data_t *event_data, void *user_data) -{ - dma_descriptor_t *desc = (dma_descriptor_t *)event_data->tx_eof_desc_addr; +static IRAM_ATTR bool lcd_rgb_panel_eof_handler( + gdma_channel_handle_t dma_chan, + gdma_event_data_t* event_data, + void* user_data +) { + dma_descriptor_t* desc = (dma_descriptor_t*)event_data->tx_eof_desc_addr; // Figure out which bounce buffer to write to. // Note: what we receive is the *last* descriptor of this bounce buffer. int bb = (desc == &lcd.dma_nodes[0]) ? 0 : 1; @@ -307,7 +272,6 @@ static IRAM_ATTR bool lcd_rgb_panel_eof_handler(gdma_channel_handle_t dma_chan, } static esp_err_t init_dma_trans_link() { - ESP_LOGI(TAG, "size: %d max: %d", lcd.bb_size, DMA_DESCRIPTOR_BUFFER_MAX_SIZE); lcd.dma_nodes[0].dw0.suc_eof = 1; @@ -330,7 +294,9 @@ static esp_err_t init_dma_trans_link() { gdma_channel_alloc_config_t dma_chan_config = { .direction = GDMA_CHANNEL_DIRECTION_TX, }; - ESP_RETURN_ON_ERROR(gdma_new_channel(&dma_chan_config, &lcd.dma_chan), TAG, "alloc DMA channel failed"); + ESP_RETURN_ON_ERROR( + gdma_new_channel(&dma_chan_config, &lcd.dma_chan), TAG, "alloc DMA channel failed" + ); gdma_connect(lcd.dma_chan, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); gdma_transfer_ability_t ability = { .psram_trans_align = 64, @@ -346,35 +312,16 @@ static esp_err_t init_dma_trans_link() { return ESP_OK; } - -static esp_err_t s3_lcd_configure_gpio() -{ +/** + * Configure LCD peripheral and auxiliary GPIOs + */ +static esp_err_t s3_lcd_configure_gpio() { const int DATA_LINES[16] = { - lcd.config.bus.data_14, - lcd.config.bus.data_15, - - lcd.config.bus.data_12, - lcd.config.bus.data_13, - - - lcd.config.bus.data_10, - lcd.config.bus.data_11, - - lcd.config.bus.data_8, - lcd.config.bus.data_9, - - - lcd.config.bus.data_6, - lcd.config.bus.data_7, - - lcd.config.bus.data_4, - lcd.config.bus.data_5, - - lcd.config.bus.data_2, - lcd.config.bus.data_3, - - - lcd.config.bus.data_0, + lcd.config.bus.data_14, lcd.config.bus.data_15, lcd.config.bus.data_12, + lcd.config.bus.data_13, lcd.config.bus.data_10, lcd.config.bus.data_11, + lcd.config.bus.data_8, lcd.config.bus.data_9, lcd.config.bus.data_6, + lcd.config.bus.data_7, lcd.config.bus.data_4, lcd.config.bus.data_5, + lcd.config.bus.data_2, lcd.config.bus.data_3, lcd.config.bus.data_0, lcd.config.bus.data_1, }; @@ -382,116 +329,157 @@ static esp_err_t s3_lcd_configure_gpio() for (size_t i = (16 - lcd.config.bus_width); i < 16; i++) { gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[DATA_LINES[i]], PIN_FUNC_GPIO); gpio_set_direction(DATA_LINES[i], GPIO_MODE_OUTPUT); - esp_rom_gpio_connect_out_signal(DATA_LINES[i], - lcd_periph_signals.panels[0].data_sigs[i], false, false); + esp_rom_gpio_connect_out_signal( + DATA_LINES[i], lcd_periph_signals.panels[0].data_sigs[i], false, false + ); } gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[lcd.config.bus.leh], PIN_FUNC_GPIO); gpio_set_direction(lcd.config.bus.leh, GPIO_MODE_OUTPUT); - esp_rom_gpio_connect_out_signal(lcd.config.bus.leh, lcd_periph_signals.panels[0].hsync_sig, false, false); + esp_rom_gpio_connect_out_signal( + lcd.config.bus.leh, lcd_periph_signals.panels[0].hsync_sig, false, false + ); gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[lcd.config.bus.clock], PIN_FUNC_GPIO); gpio_set_direction(lcd.config.bus.clock, GPIO_MODE_OUTPUT); - esp_rom_gpio_connect_out_signal(lcd.config.bus.clock, lcd_periph_signals.panels[0].pclk_sig, false, false); + esp_rom_gpio_connect_out_signal( + lcd.config.bus.clock, lcd_periph_signals.panels[0].pclk_sig, false, false + ); gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[lcd.config.bus.start_pulse], PIN_FUNC_GPIO); gpio_set_direction(lcd.config.bus.start_pulse, GPIO_MODE_OUTPUT); - esp_rom_gpio_connect_out_signal(lcd.config.bus.start_pulse, lcd_periph_signals.panels[0].de_sig, false, false); + esp_rom_gpio_connect_out_signal( + lcd.config.bus.start_pulse, lcd_periph_signals.panels[0].de_sig, false, false + ); + + gpio_config_t vsync_gpio_conf = { + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = 1ull << lcd.config.bus.stv, + }; + gpio_config(&vsync_gpio_conf); + gpio_set_level(lcd.config.bus.stv, 1); return ESP_OK; } -void IRAM_ATTR epd_lcd_init(const LcdEpdConfig_t* config, int display_width, int display_height) { - - memcpy(&lcd.config, config, sizeof(LcdEpdConfig_t)); - +/** + * Check if the PSRAM cache is properly configured. + */ +static void check_cache_configuration() { if (CONFIG_ESP32S3_DATA_CACHE_LINE_SIZE < 64) { - ESP_LOGE("epdiy", "cache line size is set to %d (< 64B)! This will degrade performance, please update this option in menuconfig."); - ESP_LOGE("epdiy", "If you are on arduino, you can't set this option yourself, you'll need to use a lower speed."); - ESP_LOGE("epdiy", "Reducing the pixel clock from %d to %d for now!", config->pixel_clock / 1000 / 1000, config->pixel_clock / 1000 / 1000 / 2); + ESP_LOGE( + "epdiy", + "cache line size is set to %d (< 64B)! This will degrade performance, please update " + "this option in menuconfig." + ); + ESP_LOGE( + "epdiy", + "If you are on arduino, you can't set this option yourself, you'll need to use a lower " + "speed." + ); + ESP_LOGE( + "epdiy", "Reducing the pixel clock from %d MHz to %d MHz for now!", + lcd.config.pixel_clock / 1000 / 1000, lcd.config.pixel_clock / 1000 / 1000 / 2 + ); lcd.config.pixel_clock = lcd.config.pixel_clock / 2; - // fixme: this would be nice, but doesn't work :( - //uint32_t d_autoload = Cache_Suspend_DCache(); - ///Cache_Set_DCache_Mode(CACHE_SIZE_FULL, CACHE_4WAYS_ASSOC, CACHE_LINE_SIZE_32B); - //Cache_Invalidate_DCache_All(); - //Cache_Resume_DCache(d_autoload); + // uint32_t d_autoload = Cache_Suspend_DCache(); + /// Cache_Set_DCache_Mode(CACHE_SIZE_FULL, CACHE_4WAYS_ASSOC, CACHE_LINE_SIZE_32B); + // Cache_Invalidate_DCache_All(); + // Cache_Resume_DCache(d_autoload); } +} - // assign globals - line_bytes = display_width / 4; - // Make sure the bounce buffers divide the display height evenly. - vertical_lines = (((display_height + 7) / 8) * 8); - - esp_err_t ret = ESP_OK; - - lcd.lcd_res_h = line_bytes / (lcd.config.bus_width / 8); - - gpio_config_t vsync_gpio_conf = { - .mode = GPIO_MODE_OUTPUT, - .pin_bit_mask = 1ull << lcd.config.bus.stv, - }; - - gpio_config(&vsync_gpio_conf); - - gpio_set_level(lcd.config.bus.stv, 1); - //gpio_set_level(S3_LCD_PIN_NUM_MODE, 0); - - ESP_LOGI(TAG, "using resolution %dx%d", lcd.lcd_res_h, vertical_lines); +/** + * Assign LCD configuration parameters from a given configuration, without allocating memory or + * touching the LCD peripheral config. + */ +static void assign_lcd_parameters_from_config( + const LcdEpdConfig_t* config, + int display_width, + int display_height +) { + // copy over the configuraiton object + memcpy(&lcd.config, config, sizeof(LcdEpdConfig_t)); - // enable APB to access LCD registers - periph_module_enable(lcd_periph_signals.panels[0].module); - periph_module_reset(lcd_periph_signals.panels[0].module); + // Make sure the bounce buffers divide the display height evenly. + lcd.display_lines = (((display_height + 7) / 8) * 8); - // each bounce buffer holds two lines of display data + lcd.line_bytes = display_width / 4; + lcd.lcd_res_h = lcd.line_bytes / (lcd.config.bus_width / 8); // With 8 bit bus width, we need a dummy cycle before the actual data, // because the LCD peripheral behaves weirdly. // Also see: // https://blog.adafruit.com/2022/06/14/esp32uesday-hacking-the-esp32-s3-lcd-peripheral/ - int dummy_bytes = lcd.config.bus_width / 8; + lcd.dummy_bytes = lcd.config.bus_width / 8; + + // each bounce buffer holds a number of lines with data + dummy bytes each + lcd.bb_size = BOUNCE_BUF_LINES * (lcd.line_bytes + lcd.dummy_bytes); + + check_cache_configuration(); - lcd.bb_size = BOUNCE_BUF_LINES * (line_bytes + dummy_bytes); - //assert(lcd.bb_size % (line_bytes) == 1); - size_t num_dma_nodes = (lcd.bb_size + DMA_DESCRIPTOR_BUFFER_MAX_SIZE - 1) / DMA_DESCRIPTOR_BUFFER_MAX_SIZE; - ESP_LOGI(TAG, "num dma nodes: %u", num_dma_nodes); - lcd.dma_nodes = heap_caps_calloc(1, num_dma_nodes * sizeof(dma_descriptor_t) * 2, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL); - ESP_GOTO_ON_FALSE(lcd.dma_nodes, ESP_ERR_NO_MEM, err, TAG, "no mem for rgb panel"); + ESP_LOGI(TAG, "using resolution %dx%d", lcd.lcd_res_h, lcd.display_lines); +} - // alloc bounce buffer +/** + * Allocate buffers for LCD driver operation. + */ +static esp_err_t allocate_lcd_buffers() { + esp_err_t ret = ESP_OK; + uint32_t dma_flags = MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA; + + // allocate bounce buffers for (int i = 0; i < 2; i++) { - // bounce buffer must come from SRAM - lcd.bounce_buffer[i] = heap_caps_aligned_calloc(4, 1, lcd.bb_size, MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA); + lcd.bounce_buffer[i] = heap_caps_aligned_calloc(4, 1, lcd.bb_size, dma_flags); ESP_GOTO_ON_ERROR(ret, err, TAG, "install interrupt failed"); } + // So far, I haven't seen any displays with > 4096 pixels per line, + // so we only need one DMA node for now. + assert(lcd.bb_size < DMA_DESCRIPTOR_BUFFER_MAX_SIZE); + lcd.dma_nodes = heap_caps_calloc(1, sizeof(dma_descriptor_t) * 2, dma_flags); + ESP_GOTO_ON_FALSE(lcd.dma_nodes, ESP_ERR_NO_MEM, err, TAG, "no mem for dma nodes"); + +err: + ESP_LOGI(TAG, "LCD initialization / allocation failed!"); + return ret; +} + +/** + * Initialize the LCD peripheral itself and install interrupts. + */ +static esp_err_t initialize_lcd_peripheral() { + esp_err_t ret = ESP_OK; + + // enable APB to access LCD registers + periph_module_enable(lcd_periph_signals.panels[0].module); + periph_module_reset(lcd_periph_signals.panels[0].module); + lcd_hal_init(&lcd.hal, 0); lcd_ll_enable_clock(lcd.hal.dev, true); lcd_ll_select_clk_src(lcd.hal.dev, LCD_CLK_SRC_PLL240M); ESP_GOTO_ON_ERROR(ret, err, TAG, "set source clock failed"); - // install interrupt service, (LCD peripheral shares the interrupt source with Camera by different mask) - int isr_flags = (ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_INTRDISABLED) | ESP_INTR_FLAG_SHARED | ESP_INTR_FLAG_LOWMED; - ret = esp_intr_alloc_intrstatus(lcd_periph_signals.panels[0].irq_id, isr_flags, - (uint32_t)lcd_ll_get_interrupt_status_reg(lcd.hal.dev), - LCD_LL_EVENT_VSYNC_END, lcd_isr_vsync, NULL, &lcd.vsync_intr); + // install interrupt service, (LCD peripheral shares the interrupt source with Camera by + // different mask) + int flags = ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_INTRDISABLED | ESP_INTR_FLAG_SHARED | + ESP_INTR_FLAG_LOWMED; + int source = lcd_periph_signals.panels[0].irq_id; + uint32_t status = (uint32_t)lcd_ll_get_interrupt_status_reg(lcd.hal.dev); + ret = esp_intr_alloc_intrstatus( + source, flags, status, LCD_LL_EVENT_VSYNC_END, lcd_isr_vsync, NULL, &lcd.vsync_intr + ); ESP_GOTO_ON_ERROR(ret, err, TAG, "install interrupt failed"); - ret = esp_intr_alloc_intrstatus(lcd_periph_signals.panels[0].irq_id, isr_flags, - (uint32_t)lcd_ll_get_interrupt_status_reg(lcd.hal.dev), - LCD_LL_EVENT_TRANS_DONE, lcd_isr_vsync, NULL, &lcd.done_intr); + status = (uint32_t)lcd_ll_get_interrupt_status_reg(lcd.hal.dev); + ret = esp_intr_alloc_intrstatus( + source, flags, status, LCD_LL_EVENT_TRANS_DONE, lcd_isr_vsync, NULL, &lcd.done_intr + ); ESP_GOTO_ON_ERROR(ret, err, TAG, "install interrupt failed"); lcd_ll_fifo_reset(lcd.hal.dev); lcd_ll_reset(lcd.hal.dev); - - // install DMA service - ret = init_dma_trans_link(); - ESP_GOTO_ON_ERROR(ret, err, TAG, "install DMA failed"); - - ret = s3_lcd_configure_gpio(); - ESP_GOTO_ON_ERROR(ret, err, TAG, "configure GPIO failed"); - // pixel clock phase and polarity lcd_ll_set_clock_idle_level(lcd.hal.dev, false); lcd_ll_set_pixel_clock_edge(lcd.hal.dev, false); @@ -499,8 +487,8 @@ void IRAM_ATTR epd_lcd_init(const LcdEpdConfig_t* config, int display_width, int // enable RGB mode and set data width lcd_ll_enable_rgb_mode(lcd.hal.dev, true); lcd_ll_set_data_width(lcd.hal.dev, lcd.config.bus_width); - lcd_ll_set_phase_cycles(lcd.hal.dev, 0, (dummy_bytes > 0), 1); // enable data phase only - lcd_ll_enable_output_hsync_in_porch_region(lcd.hal.dev, false); // enable data phase only + lcd_ll_set_phase_cycles(lcd.hal.dev, 0, (lcd.dummy_bytes > 0), 1); // enable data phase only + lcd_ll_enable_output_hsync_in_porch_region(lcd.hal.dev, false); // enable data phase only // number of data cycles is controlled by DMA buffer size lcd_ll_enable_output_always_on(lcd.hal.dev, false); @@ -521,23 +509,44 @@ void IRAM_ATTR epd_lcd_init(const LcdEpdConfig_t* config, int display_width, int // enable intr esp_intr_enable(lcd.vsync_intr); esp_intr_enable(lcd.done_intr); +err: + return ret; +} + +/** + * Configure the LCD driver for epdiy. + */ +void epd_lcd_init(const LcdEpdConfig_t* config, int display_width, int display_height) { + esp_err_t ret = ESP_OK; + assign_lcd_parameters_from_config(config, display_width, display_height); + + check_cache_configuration(); + + ret = allocate_lcd_buffers(); + ESP_GOTO_ON_ERROR(ret, err, TAG, "lcd buffer allocation failed"); + + ret = initialize_lcd_peripheral(); + ESP_GOTO_ON_ERROR(ret, err, TAG, "lcd peripheral init failed"); + + ret = init_dma_trans_link(); + ESP_GOTO_ON_ERROR(ret, err, TAG, "install DMA failed"); + + ret = s3_lcd_configure_gpio(); + ESP_GOTO_ON_ERROR(ret, err, TAG, "configure GPIO failed"); init_ckv_rmt(); + // setup driver state epd_lcd_set_pixel_clock_MHz(lcd.config.pixel_clock / 1000 / 1000); - epd_lcd_line_source_cb(NULL, NULL); ESP_LOGI(TAG, "LCD init done."); - return; err: - // do some deconstruction ESP_LOGI(TAG, "LCD initialization failed!"); abort(); } - void epd_lcd_set_pixel_clock_MHz(int frequency) { lcd.config.pixel_clock = frequency * 1000 * 1000; @@ -545,13 +554,67 @@ void epd_lcd_set_pixel_clock_MHz(int frequency) { int flags = 0; uint32_t freq = lcd_hal_cal_pclk_freq(&lcd.hal, 240000000, lcd.config.pixel_clock, flags); ESP_LOGI(TAG, "pclk freq: %d Hz", freq); - lcd.line_length_us = (lcd.lcd_res_h + lcd.config.le_high_time + lcd.config.line_front_porch - 1) * 1000000 / lcd.config.pixel_clock + 1; + lcd.line_length_us = + (lcd.lcd_res_h + lcd.config.le_high_time + lcd.config.line_front_porch - 1) * 1000000 / + lcd.config.pixel_clock + + 1; lcd.line_cycles = lcd.line_length_us * lcd.config.pixel_clock / 1000000; ESP_LOGI(TAG, "line width: %dus, %d cylces", lcd.line_length_us, lcd.line_cycles); ckv_rmt_build_signal(); } +void IRAM_ATTR epd_lcd_start_frame() { + int initial_lines = min(LINE_BATCH, lcd.display_lines); + int dummy_bytes = lcd.bb_size / BOUNCE_BUF_LINES - lcd.line_bytes; + + // hsync: pulse with, back porch, active width, front porch + int end_line = + lcd.line_cycles - lcd.lcd_res_h - lcd.config.le_high_time - lcd.config.line_front_porch; + lcd_ll_set_horizontal_timing( + lcd.hal.dev, lcd.config.le_high_time - (dummy_bytes > 0), lcd.config.line_front_porch, + // a dummy byte is neeed in 8 bit mode to work around LCD peculiarities + lcd.lcd_res_h + (dummy_bytes > 0), end_line + ); + lcd_ll_set_vertical_timing(lcd.hal.dev, 1, 1, initial_lines, 1); + + // generate the hsync at the very beginning of line + lcd_ll_set_hsync_position(lcd.hal.dev, 1); + + // reset FIFO of DMA and LCD, incase there remains old frame data + gdma_reset(lcd.dma_chan); + lcd_ll_stop(lcd.hal.dev); + lcd_ll_fifo_reset(lcd.hal.dev); + lcd_ll_enable_auto_next_frame(lcd.hal.dev, true); + + lcd.batches = 0; + fill_bounce_buffer(lcd.bounce_buffer[0]); + fill_bounce_buffer(lcd.bounce_buffer[1]); + + // the start of DMA should be prior to the start of LCD engine + gdma_start(lcd.dma_chan, (intptr_t)&lcd.dma_nodes[0]); + + // enter a critical section to ensure the frame start timing is correct + taskENTER_CRITICAL(&frame_start_spinlock); + + // delay 1us is sufficient for DMA to pass data to LCD FIFO + // in fact, this is only needed when LCD pixel clock is set too high + gpio_set_level(lcd.config.bus.stv, 0); + // esp_rom_delay_us(1); + // for picture clarity, it seems to be important to start CKV at a "good" + // time, seemingly start or towards end of line. + start_ckv_cycles(initial_lines + 5); + esp_rom_delay_us(lcd.line_length_us); + gpio_set_level(lcd.config.bus.stv, 1); + esp_rom_delay_us(lcd.line_length_us); + esp_rom_delay_us(lcd.config.ckv_high_time / 10); + + // start LCD engine + lcd_ll_start(lcd.hal.dev); + + taskEXIT_CRITICAL(&frame_start_spinlock); +} + #else /// Dummy implementation to link on the old ESP32 @@ -559,4 +622,4 @@ void epd_lcd_init(const LcdEpdConfig_t* config, int display_width, int display_h assert(false); } -#endif // S3 Target +#endif // S3 Target diff --git a/src/output_lcd/lcd_driver.h b/src/output_lcd/lcd_driver.h index a84e03c3..fdd42439 100644 --- a/src/output_lcd/lcd_driver.h +++ b/src/output_lcd/lcd_driver.h @@ -58,6 +58,7 @@ typedef bool(*line_cb_func_t)(void*, uint8_t*); typedef void(*frame_done_func_t)(void*); void epd_lcd_init(const LcdEpdConfig_t* config, int display_width, int display_height); +void epd_lcd_deinit(); void epd_lcd_frame_done_cb(frame_done_func_t, void* payload); void epd_lcd_line_source_cb(line_cb_func_t, void* payload); void epd_lcd_start_frame();