#include "display.h" #include #include "driver/ledc.h" #include "esp_check.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_io_additions.h" #include "esp_lcd_panel_rgb.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "pcf8574.h" #include "st7701_init.h" static const char *TAG = "display"; /* ---- Pinout: docs/HARDWARE.md, verified against Elecrow's own example ---- */ #define PIN_LCD_CS 16 #define PIN_LCD_SCK 2 #define PIN_LCD_SDA 1 #define PIN_LCD_DE 40 #define PIN_LCD_HSYNC 15 #define PIN_LCD_VSYNC 7 #define PIN_LCD_PCLK 41 #define PIN_BACKLIGHT 6 /* * SPI mode 0. Arduino_GFX's Arduino_SWSPI::begin() drives SCK low at init and * shifts on the rising edge. Elecrow's ESPHome config says MODE3, but that * file also documents its own colours as wrong. * * If the panel is black on first bring-up, this is the first thing to try * changing — before touching the init sequence, and long before rewriting * anything else. */ #define ST7701_SPI_MODE 0 /* * Timings from the vendor's Arduino example. The ESPHome example uses * 20/10/10 and 8/10/10 instead; either set should work. Swap if the image * rolls or shears. */ #define LCD_PCLK_HZ (18 * 1000 * 1000) #define LCD_HSYNC_FRONT 10 #define LCD_HSYNC_PULSE 4 #define LCD_HSYNC_BACK 20 #define LCD_VSYNC_FRONT 10 #define LCD_VSYNC_PULSE 4 #define LCD_VSYNC_BACK 20 #define BACKLIGHT_LEDC_TIMER LEDC_TIMER_0 #define BACKLIGHT_LEDC_CHANNEL LEDC_CHANNEL_0 #define BACKLIGHT_LEDC_MODE LEDC_LOW_SPEED_MODE #define BACKLIGHT_RES LEDC_TIMER_10_BIT static esp_lcd_panel_handle_t s_panel; static void *s_fb0, *s_fb1; /* ------------------------------------------------------------ backlight -- */ static esp_err_t backlight_init(void) { ledc_timer_config_t timer = { .speed_mode = BACKLIGHT_LEDC_MODE, .timer_num = BACKLIGHT_LEDC_TIMER, .duty_resolution = BACKLIGHT_RES, .freq_hz = 5000, .clk_cfg = LEDC_AUTO_CLK, }; ESP_RETURN_ON_ERROR(ledc_timer_config(&timer), TAG, "ledc timer"); ledc_channel_config_t ch = { .gpio_num = PIN_BACKLIGHT, .speed_mode = BACKLIGHT_LEDC_MODE, .channel = BACKLIGHT_LEDC_CHANNEL, .timer_sel = BACKLIGHT_LEDC_TIMER, .duty = 0, .hpoint = 0, }; return ledc_channel_config(&ch); } void display_set_backlight(uint8_t percent) { if (percent > 100) { percent = 100; } uint32_t max = (1u << BACKLIGHT_RES) - 1; uint32_t duty = (max * percent) / 100; ledc_set_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL, duty); ledc_update_duty(BACKLIGHT_LEDC_MODE, BACKLIGHT_LEDC_CHANNEL); } /* --------------------------------------------------------- reset dance --- */ /* * Reproduces the vendor example's setup() ordering exactly, including the P3 * ("LCD power") pulse. Despite the label, P3 goes high before the reset and * low again once the backlight is running, which only makes sense as blanking * to hide init garbage. Copied rather than reasoned about — see * docs/HARDWARE.md. * * P1 is deliberately untouched throughout: it is the relay. */ static esp_err_t panel_power_and_reset(void) { ESP_RETURN_ON_ERROR(pcf8574_write(PCF_PIN_LCD_POWER, true), TAG, "p3"); vTaskDelay(pdMS_TO_TICKS(100)); ESP_RETURN_ON_ERROR(pcf8574_write(PCF_PIN_LCD_RST, true), TAG, "p4 hi"); vTaskDelay(pdMS_TO_TICKS(100)); ESP_RETURN_ON_ERROR(pcf8574_write(PCF_PIN_LCD_RST, false), TAG, "p4 lo"); vTaskDelay(pdMS_TO_TICKS(120)); ESP_RETURN_ON_ERROR(pcf8574_write(PCF_PIN_LCD_RST, true), TAG, "p4 hi2"); vTaskDelay(pdMS_TO_TICKS(120)); return ESP_OK; } /* --------------------------------------------------------------- init ---- */ static esp_err_t send_init_sequence(void) { esp_lcd_panel_io_handle_t io = NULL; esp_lcd_panel_io_3wire_spi_config_t io_cfg = { .line_config = { .cs_io_type = IO_TYPE_GPIO, .cs_gpio_num = PIN_LCD_CS, .scl_io_type = IO_TYPE_GPIO, .scl_gpio_num = PIN_LCD_SCK, .sda_io_type = IO_TYPE_GPIO, .sda_gpio_num = PIN_LCD_SDA, .io_expander = NULL, }, .expect_clk_speed = PANEL_IO_3WIRE_SPI_CLK_MAX, .spi_mode = ST7701_SPI_MODE, .lcd_cmd_bytes = 1, .lcd_param_bytes = 1, .flags = { .use_dc_bit = 1, /* 9-bit frames: DC bit then 8 data bits */ .dc_zero_on_data = 0, /* DC=0 command, DC=1 data (ST7701 order) */ .lsb_first = 0, .cs_high_active = 0, /* * Keep CS inactive after we delete this IO. The RGB bus takes over * the panel from here and must not see CS wander. */ .del_keep_cs_inactive = 1, }, }; ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_3wire_spi(&io_cfg, &io), TAG, "3wire spi io"); esp_err_t err = ESP_OK; for (size_t i = 0; i < ST7701_INIT_SEQ_LEN; i++) { const st7701_cmd_t *c = &st7701_init_seq[i]; err = esp_lcd_panel_io_tx_param(io, c->cmd, c->data_len ? c->data : NULL, c->data_len); if (err != ESP_OK) { ESP_LOGE(TAG, "init cmd %u (0x%02X) failed: %s", (unsigned)i, c->cmd, esp_err_to_name(err)); break; } if (c->delay_ms) { vTaskDelay(pdMS_TO_TICKS(c->delay_ms)); } } /* The init bus is single-use; the RGB peripheral owns the panel now. */ esp_lcd_panel_io_del(io); if (err == ESP_OK) { ESP_LOGI(TAG, "ST7701S init sequence sent (%u commands)", (unsigned)ST7701_INIT_SEQ_LEN); } return err; } static esp_err_t rgb_panel_init(void) { esp_lcd_rgb_panel_config_t cfg = { .clk_src = LCD_CLK_SRC_PLL160M, .timings = { .pclk_hz = LCD_PCLK_HZ, .h_res = DISPLAY_H_RES, .v_res = DISPLAY_V_RES, .hsync_pulse_width = LCD_HSYNC_PULSE, .hsync_back_porch = LCD_HSYNC_BACK, .hsync_front_porch = LCD_HSYNC_FRONT, .vsync_pulse_width = LCD_VSYNC_PULSE, .vsync_back_porch = LCD_VSYNC_BACK, .vsync_front_porch = LCD_VSYNC_FRONT, .flags = { /* Elecrow's ESPHome config sets pclk_inverted, i.e. data is * latched on the falling edge. */ .pclk_active_neg = 1, }, }, .data_width = 16, .bits_per_pixel = 16, .num_fbs = 2, /* * No bounce buffers — this is what makes runtime NVS writes safe. * See the header comment. Do not "optimise" this to a non-zero value * without re-reading PRD 8.2. */ .bounce_buffer_size_px = 0, .psram_trans_align = 64, .hsync_gpio_num = PIN_LCD_HSYNC, .vsync_gpio_num = PIN_LCD_VSYNC, .de_gpio_num = PIN_LCD_DE, .pclk_gpio_num = PIN_LCD_PCLK, .disp_gpio_num = -1, /* * RGB565 bit order: data[0..4] = blue, [5..10] = green, [11..15] = red. * If red and blue come out swapped, swap these two blocks — the panel's * own MADCTL is already 0x00 (RGB) from the init sequence. */ .data_gpio_nums = { 5, 45, 48, 47, 21, /* B0..B4 */ 14, 13, 12, 11, 10, 9, /* G0..G5 */ 46, 3, 8, 18, 17, /* R0..R4 */ }, .flags = { .fb_in_psram = 1, }, }; ESP_RETURN_ON_ERROR(esp_lcd_new_rgb_panel(&cfg, &s_panel), TAG, "rgb panel"); ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(s_panel), TAG, "panel reset"); ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "panel init"); ESP_RETURN_ON_ERROR( esp_lcd_rgb_panel_get_frame_buffer(s_panel, 2, &s_fb0, &s_fb1), TAG, "get framebuffers"); ESP_LOGI(TAG, "RGB panel up: %dx%d @ %d MHz, fb0=%p fb1=%p", DISPLAY_H_RES, DISPLAY_V_RES, LCD_PCLK_HZ / 1000000, s_fb0, s_fb1); return ESP_OK; } esp_err_t display_init(void) { if (!pcf8574_is_ready()) { ESP_LOGE(TAG, "expander not ready — cannot sequence panel power"); return ESP_ERR_INVALID_STATE; } ESP_RETURN_ON_ERROR(panel_power_and_reset(), TAG, "power/reset"); ESP_RETURN_ON_ERROR(send_init_sequence(), TAG, "init sequence"); ESP_RETURN_ON_ERROR(rgb_panel_init(), TAG, "rgb"); ESP_RETURN_ON_ERROR(backlight_init(), TAG, "backlight"); /* Clear both buffers before the backlight comes up, so the first thing on * screen is black rather than whatever was in PSRAM. */ size_t fb_bytes = (size_t)DISPLAY_H_RES * DISPLAY_V_RES * 2; memset(s_fb0, 0, fb_bytes); memset(s_fb1, 0, fb_bytes); display_set_backlight(80); /* Vendor ordering: P3 low once the backlight is running. */ pcf8574_write(PCF_PIN_LCD_POWER, false); return ESP_OK; } esp_lcd_panel_handle_t display_panel(void) { return s_panel; } esp_err_t display_get_framebuffers(void **fb0, void **fb1) { if (!s_panel) { return ESP_ERR_INVALID_STATE; } if (fb0) { *fb0 = s_fb0; } if (fb1) { *fb1 = s_fb1; } return ESP_OK; } void display_test_pattern(void) { static const uint16_t bars[] = { 0xF800, /* red */ 0x07E0, /* green */ 0x001F, /* blue */ 0xFFFF, /* white */ 0xFFE0, /* yellow */ 0x07FF, /* cyan */ 0xF81F, /* magenta*/ 0x0000, /* black */ }; const int n = sizeof(bars) / sizeof(bars[0]); const int bar_w = DISPLAY_H_RES / n; uint16_t *fb = (uint16_t *)s_fb0; if (!fb) { ESP_LOGE(TAG, "no framebuffer"); return; } for (int y = 0; y < DISPLAY_V_RES; y++) { for (int x = 0; x < DISPLAY_H_RES; x++) { int i = x / bar_w; fb[y * DISPLAY_H_RES + x] = bars[i < n ? i : n - 1]; } } esp_lcd_panel_draw_bitmap(s_panel, 0, 0, DISPLAY_H_RES, DISPLAY_V_RES, fb); ESP_LOGI(TAG, "colour bars drawn — this is the PRD phase-2 check"); }