// ===================================================================================== // board_bringup.c - ESP32-P4 Hardware-Bringup // JC_PANEL_43 = JC4880P443C-I-W (ST7701, 480x800, GT911) // JC_PANEL_70 = JC1060P470C-I-W (JD9165, 1024x600, GT911) // WS_PANEL_7H = Waveshare ESP32-P4-Pico + 7inch DSI LCD (H) (1280x720, GT9271) // ===================================================================================== // BEWUSST eine .c-Datei (wird als C kompiliert): die esp_lcd-Init-Makros nutzen // Compound-Literals und Out-of-Order-Designated-Initializer, die in C++ (Arduino .cpp) // zu Fehlern fuehren. Der Ablauf ist 1:1 aus dem Hersteller-Beispiel // arduino_examples/lvgl_v9_sw_rotation/lvgl_sw_rotation.c uebernommen - lediglich der // Demo-Aufruf (lv_demo_widgets) entfaellt; das UI wird ueber display_hal.cpp aufgebaut. // // Nur aktiv bei JC_USE_REAL_PANEL == 1. Erwartet die Vendor-BSP-Dateien im Sketch: // pins_config.h, lvgl_port_v9.h/.c, src/lcd/*, src/touch/* // ===================================================================================== #include "config.h" // liefert JC_USE_REAL_PANEL (C-sicher) #if JC_USE_REAL_PANEL #include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "driver/gpio.h" #include "driver/ledc.h" #include "driver/i2c_master.h" #include "esp_log.h" #include "esp_cache.h" #include "esp_heap_caps.h" #include "esp_rom_sys.h" #include "esp_timer.h" #include "esp_system.h" #include "esp_ldo_regulator.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_panel_ops.h" #include "esp_lcd_mipi_dsi.h" #include "src/touch/esp_lcd_touch_gt911.h" #if JC_PANEL_TYPE == WS_PANEL_7H // Das Waveshare-Panel braucht keinen Hersteller-Panel-Treiber: es wird direkt als // DPI-Panel angelegt und mit den drei Standard-DCS-Befehlen geweckt. #include "hal/mipi_dsi_types.h" #elif JC_PANEL_TYPE == JC_PANEL_70 #include "src/lcd/esp_lcd_jd9165.h" #else #include "src/lcd/esp_lcd_st7701.h" #endif #include "pins_config.h" #include "lvgl_port_v9.h" #define BSP_MIPI_DSI_PHY_PWR_LDO_CHAN (3) #define BSP_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV (2500) #define BSP_LCD_H_RES (LCD_H_RES) // native Panel-Aufloesung #define BSP_LCD_V_RES (LCD_V_RES) #define BSP_I2C_NUM (I2C_NUM_1) #define BSP_I2C_SDA (GPIO_NUM_7) #define BSP_I2C_SCL (GPIO_NUM_8) #define BSP_LCD_TOUCH_RST (GPIO_NUM_NC) #define BSP_LCD_TOUCH_INT (GPIO_NUM_NC) #define BSP_LCD_RST ((gpio_num_t)LCD_RST) #define BSP_LCD_BACKLIGHT ((gpio_num_t)LCD_LED) #define LCD_LEDC_CH LEDC_CHANNEL_0 // Bytes je Pixel im Framebuffer. Beim Waveshare-Panel waehlbar ueber WS7_COLOR_BITS // (pins_config.h); LVGL rechnet mit derselben Tiefe (lv_conf.h). JC-Panels: RGB565. #if JC_PANEL_TYPE == WS_PANEL_7H #define BSP_LCD_FB_BYTES_PER_PX (WS7_COLOR_BITS / 8) #else #define BSP_LCD_FB_BYTES_PER_PX (2) #endif static i2c_master_bus_handle_t s_i2c_handle = NULL; #if JC_PANEL_TYPE == WS_PANEL_7H // ------------------------------------------------------------------------------------ // Haengenden I2C-Bus befreien // // Trifft ein Reset des P4 mitten in eine laufende I2C-Uebertragung, bleibt der // angesprochene Baustein in seiner Bitausgabe stehen und haelt die Datenleitung dauerhaft // auf Masse. Der Bus ist damit tot: Jede weitere Uebertragung schlaegt fehl. Da Bridge und // Touch-Controller an Dauerstrom haengen, ueberlebt dieser Zustand jeden Reset und sogar // das Flashen - nur echtes Stromabziehen loest ihn. // // Folge: Die Bridge wird nie initialisiert, das Bild bleibt schwarz, und weil sie ihre // Register behaelt, brennt die Hintergrundbeleuchtung weiter. Genau dieses Bild. // // Abhilfe ist der uebliche Weg aus der I2C-Spezifikation: Vor dem Anlegen des Busses die // Taktleitung von Hand takten, bis der Baustein sein letztes Bit ausgegeben hat und die // Datenleitung wieder loslaesst, danach eine Stopp-Bedingung erzeugen. static void ws7_i2c_bus_recover(void) { gpio_config_t io = { .pin_bit_mask = BIT64(BSP_I2C_SDA) | BIT64(BSP_I2C_SCL), .mode = GPIO_MODE_INPUT_OUTPUT_OD, // offener Kollektor, Zustand lesbar .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; gpio_config(&io); gpio_set_level(BSP_I2C_SDA, 1); gpio_set_level(BSP_I2C_SCL, 1); esp_rom_delay_us(10); if (gpio_get_level(BSP_I2C_SDA) != 0) { return; // Bus frei - nichts zu tun } esp_rom_printf("[Panel] I2C-Bus haengt - wird freigetaktet\n"); // Bis zu neun Takte: so viele braucht ein Baustein hoechstens, um ein angefangenes // Byte samt Bestaetigungsbit zu Ende zu schieben. for (int i = 0; i < 9 && gpio_get_level(BSP_I2C_SDA) == 0; i++) { gpio_set_level(BSP_I2C_SCL, 0); esp_rom_delay_us(5); gpio_set_level(BSP_I2C_SCL, 1); esp_rom_delay_us(5); } // Stopp-Bedingung: Datenleitung geht bei hoher Taktleitung von Masse auf High. gpio_set_level(BSP_I2C_SDA, 0); esp_rom_delay_us(5); gpio_set_level(BSP_I2C_SCL, 1); esp_rom_delay_us(5); gpio_set_level(BSP_I2C_SDA, 1); esp_rom_delay_us(10); esp_rom_printf("[Panel] I2C-Bus nach Freitakten: %s\n", gpio_get_level(BSP_I2C_SDA) ? "frei" : "weiterhin belegt"); // Pins wieder freigeben, damit der I2C-Treiber sie uebernehmen kann. gpio_reset_pin(BSP_I2C_SDA); gpio_reset_pin(BSP_I2C_SCL); } // Zeigt, welche Bausteine sich am I2C-Bus melden. Erwartet werden 0x45 (Bridge) und // 0x14 (GT9271-Touch); 0x18 waere der Audio-Baustein. Fehlt einer, sagt das mehr aus als // jede Fehlermeldung des Treibers. static void ws7_i2c_scan(const char *wann) { esp_rom_printf("[Panel] I2C-Bausteine (%s):", wann); for (uint8_t addr = 1; addr < 127; addr++) { if (i2c_master_probe(s_i2c_handle, addr, 50) == ESP_OK) { esp_rom_printf(" 0x%02X", addr); } } esp_rom_printf("\n"); } #endif // JC_PANEL_TYPE == WS_PANEL_7H // Klartext-Meldung auf der seriellen Konsole. Bewusst esp_rom_printf: das laeuft auch bei // abgeschaltetem Core-Debug-Level und ohne Heap, also genau dann, wenn es darauf ankommt. #define BSP_STEP(call, what) \ do { \ esp_err_t _err = (call); \ if (_err != ESP_OK) { \ esp_rom_printf("[Panel] FEHLER: %s -> %s\n", what, esp_err_to_name(_err)); \ } \ } while (0) // Ohne PSRAM passt kein Framebuffer. Bei 1280x720 in RGB888 werden 3 x 2,7 MB gebraucht - // bleibt der Panel-Treiber daran haengen, sieht man hier sofort, woran es liegt. static void bsp_report_psram(size_t needed_bytes) { esp_rom_printf("[Panel] PSRAM gesamt %u KB, frei %u KB, groesster Block %u KB, benoetigt %u KB\n", (unsigned)(heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024), (unsigned)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024), (unsigned)(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM) / 1024), (unsigned)(needed_bytes / 1024)); } // ===================================================================================== #if JC_PANEL_TYPE == WS_PANEL_7H // ===================================================================================== // Waveshare 7inch DSI LCD (H): Panel-Freigabe und Helligkeit haengen an einer Bridge // auf I2C-Adresse 0x45 - es gibt weder einen Reset- noch einen Backlight-GPIO. // Registerfolge wie im Linux-Treiber: // Grundinitialisierung: C0=01, C2=01, AC=01 (vor dem DSI-Bringup) // Freigabe/Helligkeit: AD=01, AB=255-Helligkeit, AA=01 (ERST wenn DSI laeuft, // sonst bleibt das Bild dunkel bzw. flackert beim Start) // ===================================================================================== static i2c_master_dev_handle_t s_bridge_handle = NULL; static void ws7_bridge_write(uint8_t reg, uint8_t value) { if (s_bridge_handle == NULL) return; uint8_t buf[2] = { reg, value }; esp_err_t err = i2c_master_transmit(s_bridge_handle, buf, sizeof(buf), 100); if (err != ESP_OK) { // Frueher lief das stillschweigend ins Leere. Schlaegt es fehl, wird die Bridge // nie initialisiert und das Bild bleibt schwarz - ohne jeden Hinweis darauf. esp_rom_printf("[Panel] Bridge-Register 0x%02X liess sich nicht schreiben: %s\n", reg, esp_err_to_name(err)); } } // Grundeinstellung der Bridge. Bewusst mehrfach aufrufbar, siehe unten. // // Warum zweimal: Beim ersten Aufruf existiert die DSI-Strecke noch gar nicht - die Bridge // bekommt ihre Einstellung also, bevor ein Signal anliegt. Beim ersten Einschalten reichte // das nicht: Die Beleuchtung ging an (die haengt an der Bridge), ein Bild kam aber nicht. // Erst ein Reset half - und dann nur deshalb, weil die Bridge ihre Einstellung aus dem // vorigen Durchlauf noch hatte und diesmal ein DSI-Signal vorfand. Genau das holt der // zweite Aufruf nach, ohne dass ein Reset noetig ist. Die Register sind wiederholbar. static void ws7_bridge_core_init(const char *wann) { esp_rom_printf("[Panel] Bridge-Grundeinstellung %s\n", wann); ws7_bridge_write(0xC0, 0x01); ws7_bridge_write(0xC2, 0x01); ws7_bridge_write(0xAC, 0x01); } // Bridge am I2C-Bus anmelden und erstmalig einstellen. Laeuft VOR dem DSI-Bringup; // die Hintergrundbeleuchtung bleibt dabei noch aus. static void ws7_bridge_pre_init(void) { i2c_device_config_t dev_cfg = { .dev_addr_length = I2C_ADDR_BIT_LEN_7, .device_address = WS7_BRIDGE_I2C_ADDR, .scl_speed_hz = 100000, }; if (i2c_master_bus_add_device(s_i2c_handle, &dev_cfg, &s_bridge_handle) != ESP_OK) { s_bridge_handle = NULL; return; } ws7_bridge_core_init("vor dem DSI-Start"); } void jc_backlight_init(void) { // Nichts zu tun - die Bridge uebernimmt das, siehe ws7_bridge_pre_init(). } void jc_backlight_set(int percent) { if (percent > 100) percent = 100; if (percent < 0) percent = 0; uint8_t brightness = (uint8_t)((255 * percent) / 100); uint8_t wert = (uint8_t)(255 - brightness); int64_t t0 = esp_timer_get_time(); // Die Bridge uebernimmt eine neue Helligkeit nicht zuverlaessig beim ersten Mal. Der // Beispielcode von Waveshare setzt sie deshalb zweimal, mit einer Pause dazwischen, // ausdruecklich "sicherheitshalber". Uns fehlte diese Wiederholung: Das Bild stand // dann fertig da, blieb aber dunkel, bis die Uebernahme irgendwann doch griff - beim // Ein- und Ausschalten des Standby waren das mehrere Sekunden. // // Die Freigabe (0xAD) bleibt beim ersten Aufruf, danach nicht mehr: Sie erneut zu // setzen, waehrend die Beleuchtung laeuft, stoesst die Helligkeitsstufe der Bridge neu // an - der Beispielcode setzt sie darum ebenfalls nur ein einziges Mal. static bool freigegeben = false; if (!freigegeben) { ws7_bridge_write(0xAD, 0x01); // Panel-/Backlight-Freigabe vTaskDelay(pdMS_TO_TICKS(20)); // wie im Beispielcode: Bridge setzen lassen freigegeben = true; } ws7_bridge_write(0xAB, wert); ws7_bridge_write(0xAA, 0x01); // Helligkeit uebernehmen vTaskDelay(pdMS_TO_TICKS(WS7_BACKLIGHT_LATCH_MS)); ws7_bridge_write(0xAB, wert); // Wiederholung, siehe oben ws7_bridge_write(0xAA, 0x01); esp_rom_printf("[HAL] %u ms: Helligkeit %d %% gesetzt, brauchte %u ms\n", (unsigned)(esp_timer_get_time() / 1000), percent, (unsigned)((esp_timer_get_time() - t0) / 1000)); } // ===================================================================================== #else // JC-Panels: Hintergrundbeleuchtung als LEDC-PWM an einem GPIO // ===================================================================================== void jc_backlight_init(void) { const ledc_channel_config_t ch = { .gpio_num = BSP_LCD_BACKLIGHT, .speed_mode = LEDC_LOW_SPEED_MODE, .channel = LCD_LEDC_CH, .intr_type = LEDC_INTR_DISABLE, .timer_sel = 1, .duty = 0, .hpoint = 0 }; const ledc_timer_config_t tm = { .speed_mode = LEDC_LOW_SPEED_MODE, .duty_resolution = LEDC_TIMER_10_BIT, .timer_num = 1, .freq_hz = 5000, .clk_cfg = LEDC_AUTO_CLK }; ledc_timer_config(&tm); ledc_channel_config(&ch); } void jc_backlight_set(int percent) { if (percent > 100) percent = 100; if (percent < 0) percent = 0; uint32_t duty = (1023 * percent) / 100; ledc_set_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH, duty); ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH); } #endif // JC_PANEL_TYPE == WS_PANEL_7H IRAM_ATTR static bool on_vsync(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *ctx) { return lvgl_port_notify_lcd_vsync(); } #if JC_PANEL_TYPE == WS_PANEL_7H // ------------------------------------------------------------------------------------ // Aussetzer des GT9271 ueberbruecken // // Der Controller laesst gelegentlich einen Messzyklus aus und liefert ab und zu ein // unplausibles Paket - die Werks-Testskizze hat solche Pakete ebenfalls verworfen. Der // Hersteller-Treiber kann "gerade nichts Neues" nicht von "Finger weg" unterscheiden und // meldet beides als losgelassen. Seit die Oberflaeche doppelt so oft abtastet, faellt das // auf: Aus einem Tipp wird Druecken-Loslassen-Druecken-Loslassen, der Knopf loest also // mehrfach aus. // // Deshalb liegt hier ein eigener get_xy-Aufsatz vor dem Treiber: Er verwirft unplausible // Rohwerte und haelt den letzten gueltigen Punkt kurz fest. Losgelassen wird erst // gemeldet, wenn WS7_TOUCH_HOLD_MS lang wirklich nichts mehr kam. // ------------------------------------------------------------------------------------ static bool (*s_touch_get_xy_orig)(esp_lcd_touch_handle_t, uint16_t *, uint16_t *, uint16_t *, uint8_t *, uint8_t) = NULL; static uint16_t s_touch_last_x = 0; static uint16_t s_touch_last_y = 0; static int64_t s_touch_last_us = 0; static bool s_touch_down = false; static bool ws7_touch_get_xy(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num) { bool got = s_touch_get_xy_orig(tp, x, y, strength, point_num, max_point_num); int64_t now = esp_timer_get_time(); // Unplausible Rohwerte gelten als Aussetzer, nicht als Beruehrung. Wie viele davon // anfallen, war bisher nirgends zu sehen - eine Beruehrung, deren Rohwerte lange // verworfen werden, sieht von aussen wie ein Display aus, das nicht reagiert. static uint32_t s_verworfen = 0; if (got && *point_num > 0 && (x[0] > WS7_TOUCH_RAW_X_MAX || y[0] > WS7_TOUCH_RAW_Y_MAX)) { if (s_verworfen == 0) esp_rom_printf("[Touch] %u ms: Rohwert verworfen (x %u, y %u liegen ausserhalb)\n", (unsigned)(now / 1000), (unsigned)x[0], (unsigned)y[0]); s_verworfen++; got = false; *point_num = 0; } if (got && *point_num > 0) { if (!s_touch_down) esp_rom_printf("[Touch] %u ms: Beruehrung erkannt bei x %u, y %u; %u Rohwerte vorher verworfen\n", (unsigned)(now / 1000), (unsigned)x[0], (unsigned)y[0], (unsigned)s_verworfen); s_verworfen = 0; s_touch_last_x = x[0]; s_touch_last_y = y[0]; s_touch_last_us = now; s_touch_down = true; return true; } #if WS7_TOUCH_HOLD_MS > 0 if (s_touch_down && (now - s_touch_last_us) < ((int64_t)WS7_TOUCH_HOLD_MS * 1000)) { x[0] = s_touch_last_x; y[0] = s_touch_last_y; if (strength) strength[0] = 0; *point_num = 1; return true; } #endif s_touch_down = false; return false; } // Der GT9271 des Waveshare-Panels meldet im Hochformat (Roh-X 0..720, Roh-Y 0..1280), // das Bild laeuft aber im Querformat 1280x720. Achsentausch (und bei Bedarf Spiegelung) // deshalb ueber den process_coordinates-Hook, der VOR den swap/mirror-Flags von // esp_lcd_touch greift - deren Flags rechnen mit x_max/y_max des BILDES und wuerden auf // den Rohwerten falsche Ergebnisse liefern. static void ws7_touch_map(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num) { (void)tp; (void)strength; (void)max_point_num; if (point_num == NULL) return; for (uint8_t i = 0; i < *point_num; i++) { uint16_t raw_x = x[i]; uint16_t raw_y = y[i]; #if WS7_TOUCH_MIRROR_RAW_X raw_x = (raw_x > WS7_TOUCH_RAW_X_MAX) ? 0 : (uint16_t)(WS7_TOUCH_RAW_X_MAX - raw_x); #endif #if WS7_TOUCH_MIRROR_RAW_Y raw_y = (raw_y > WS7_TOUCH_RAW_Y_MAX) ? 0 : (uint16_t)(WS7_TOUCH_RAW_Y_MAX - raw_y); #endif // Achsentausch + Begrenzung auf die Bildflaeche (der GT9271 liefert vereinzelt // Ausreisser, die sonst als Klick am Rand ankaemen). uint16_t px = (raw_y >= LCD_H_RES) ? (LCD_H_RES - 1) : raw_y; uint16_t py = (raw_x >= LCD_V_RES) ? (LCD_V_RES - 1) : raw_x; #if WS7_ROTATE_180 // Bild wird um 180 Grad gedreht ausgegeben -> Touch muss mitdrehen. px = (uint16_t)(LCD_H_RES - 1 - px); py = (uint16_t)(LCD_V_RES - 1 - py); #endif #if WS7_TOUCH_DEBUG esp_rom_printf("[Touch] roh X=%u Y=%u -> Bild X=%u Y=%u\n", (unsigned)x[i], (unsigned)y[i], (unsigned)px, (unsigned)py); #endif x[i] = px; y[i] = py; } } #endif #if JC_PANEL_TYPE == JC_PANEL_70 // Der GT911 des 7,0"-Panels liefert Rohkoordinaten im Raster 800x480 (nicht 1024x600). // Ohne Umrechnung waere nur das linke obere Rechteck erreichbar. Das Hersteller-Beispiel // skaliert dafuer im Touch-Treiber; hier ueber den vorgesehenen process_coordinates-Hook, // damit die Vendor-Datei esp_lcd_touch_gt911.c unveraendert bleibt. #define JC_TOUCH_RAW_H_RES (800) #define JC_TOUCH_RAW_V_RES (480) static void jc_touch_scale(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num) { (void)tp; (void)strength; (void)max_point_num; if (point_num == NULL) return; for (uint8_t i = 0; i < *point_num; i++) { x[i] = (uint16_t)(((uint32_t)x[i] * LCD_H_RES) / JC_TOUCH_RAW_H_RES); y[i] = (uint16_t)(((uint32_t)y[i] * LCD_V_RES) / JC_TOUCH_RAW_V_RES); } } #endif #if JC_PANEL_TYPE == WS_PANEL_7H // Panel wecken. Bewusst mehrfach, siehe unten. // // Ein reines DPI-Panel kennt keine reset-Funktion, esp_lcd_panel_reset() wuerde ins Leere // greifen - deshalb nur die DCS-Befehle. // // Warum mehrfach: Verpasst der Panel-Controller diese Befehle, weil er nach dem // Einschalten noch nicht bereit war, bleibt der Bildschirm dunkel, obwohl der P4 alles // korrekt gesendet hat und jeder Schritt Erfolg meldet. Die Hintergrundbeleuchtung geht // trotzdem an, weil sie an der Bridge haengt und nicht am Panel - genau dieses Bild. // Die Befehle sind wiederholbar, ein zweiter Anlauf nach dem Start der Videoausgabe // kostet nichts und faengt den Fall ab. static void ws7_panel_wake(esp_lcd_panel_io_handle_t io, const char *wann) { uint8_t zero = 0x00; uint8_t madctl = WS7_MADCTL; esp_rom_printf("[Panel] Weckbefehle %s\n", wann); BSP_STEP(esp_lcd_panel_io_tx_param(io, 0x36, &madctl, 1), "Panel-Befehl MADCTL"); BSP_STEP(esp_lcd_panel_io_tx_param(io, 0x11, &zero, 1), "Panel-Befehl Sleep Out"); vTaskDelay(pdMS_TO_TICKS(120)); BSP_STEP(esp_lcd_panel_io_tx_param(io, 0x29, &zero, 1), "Panel-Befehl Display On"); vTaskDelay(pdMS_TO_TICKS(20)); } #endif #if JC_PANEL_TYPE == WS_PANEL_7H // Baut die komplette DSI-Strecke auf: Bus, Kommandokanal, Bildausgabe, und weckt das // Panel. Bewusst als eigene Funktion, damit der ganze Vorgang wiederholbar ist. static bool ws7_dsi_bringup(esp_lcd_dsi_bus_handle_t *out_bus, esp_lcd_panel_io_handle_t *out_io, esp_lcd_panel_handle_t *out_panel, esp_lcd_dpi_panel_config_t *out_dpi_config) { esp_lcd_dsi_bus_handle_t mipi_dsi_bus = NULL; esp_lcd_panel_io_handle_t io = NULL; esp_lcd_panel_handle_t disp_panel = NULL; esp_lcd_dsi_bus_config_t bus_config = { .bus_id = 0, .num_data_lanes = WS7_DSI_LANES, .phy_clk_src = MIPI_DSI_PHY_PLLREF_CLK_SRC_PLL_F20M, .lane_bit_rate_mbps = WS7_DSI_LANE_MBPS, }; BSP_STEP(esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus), "MIPI-DSI-Bus"); esp_lcd_dbi_io_config_t dbi_config = { .virtual_channel = 0, .lcd_cmd_bits = 8, .lcd_param_bits = 8, }; BSP_STEP(esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io), "DBI-Kommandokanal"); esp_lcd_dpi_panel_config_t dpi_config = { .virtual_channel = 0, .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, .dpi_clock_freq_mhz = WS7_DPI_CLOCK_MHZ, #if WS7_COLOR_BITS == 24 .pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB888, #else .pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565, #endif // Anzahl der Bildspeicher. Beim Waveshare-Panel genau einer: mit zweien kam nach // dem Einschalten kein Bild (siehe WS7_SINGLE_FB in pins_config.h). .num_fbs = LVGL_PORT_LCD_BUFFER_NUMS, .video_timing = { .h_size = BSP_LCD_H_RES, .v_size = BSP_LCD_V_RES, .hsync_back_porch = WS7_HSYNC_BACK, .hsync_pulse_width = WS7_HSYNC_PULSE, .hsync_front_porch = WS7_HSYNC_FRONT, .vsync_back_porch = WS7_VSYNC_BACK, .vsync_pulse_width = WS7_VSYNC_PULSE, .vsync_front_porch = WS7_VSYNC_FRONT, }, .flags = { .use_dma2d = true, }, }; BSP_STEP(esp_lcd_new_panel_dpi(mipi_dsi_bus, &dpi_config, &disp_panel), "DPI-Panel (Framebuffer im PSRAM)"); if (disp_panel == NULL) { esp_rom_printf("[Panel] Abbruch: Panel wurde nicht angelegt. Haeufigste Ursache ist zu " "wenig PSRAM - PSRAM in den Board-Einstellungen aktivieren oder in " "pins_config.h weniger Framebuffer waehlen.\n"); return false; } ws7_panel_wake(io, "vor dem Start der Videoausgabe"); BSP_STEP(esp_lcd_panel_init(disp_panel), "DPI-Videoausgabe starten"); // Zweiter Anlauf, jetzt bei laufender Videoausgabe: faengt den Fall ab, dass das // Panel beim ersten Mal noch nicht aufnahmebereit war. vTaskDelay(pdMS_TO_TICKS(WS7_PANEL_SETTLE_MS)); ws7_bridge_core_init("nach dem Start der Videoausgabe"); ws7_panel_wake(io, "nach dem Start der Videoausgabe"); *out_bus = mipi_dsi_bus; *out_io = io; *out_panel = disp_panel; *out_dpi_config = dpi_config; return true; } #endif // Bringt Panel + Touch + LVGL-Port hoch (LVGL laeuft danach in eigenem Task). // Rueckgabe false: Panel kam nicht hoch - der Aufrufer darf dann KEINE LVGL-Funktion // benutzen, sonst folgt ein zweiter Absturz, der die eigentliche Ursache ueberdeckt. bool jc_board_bringup(void) { #if JC_PANEL_TYPE == WS_PANEL_7H // Anlaufzeit abwarten, BEVOR Bridge und Panel angesprochen werden. // // Das Displaymodul braucht nach dem Anlegen der Versorgung einen Moment, bis Bridge // und Panel bereit sind. Faengt der P4 sofort an, laeuft der Hochlauf ins Leere: Die // Bridge nimmt ihre Register nicht an oder das Panel zeigt trotz korrekt gesendeter // DSI-Daten nichts - beides ohne Fehlermeldung, weil aus Sicht des P4 alles geklappt // hat. Das Ergebnis war ein Display, das mal ansprang und mal nicht. // // Der Werks-Testsketch wartet aus demselben Grund 2 Sekunden am Anfang von setup(). // Dass es mit geoeffnetem seriellen Monitor fast immer klappte, hatte dieselbe // Ursache: Dessen USB-Anmeldung verzoegert den Start und ersetzte damit die Wartezeit. vTaskDelay(pdMS_TO_TICKS(WS7_PANEL_WARMUP_MS)); #endif jc_backlight_init(); #if JC_PANEL_TYPE == WS_PANEL_7H ws7_i2c_bus_recover(); // haengenden Bus vom letzten Reset befreien #endif i2c_master_bus_config_t i2c_bus_conf = { .clk_source = I2C_CLK_SRC_DEFAULT, .sda_io_num = BSP_I2C_SDA, .scl_io_num = BSP_I2C_SCL, .i2c_port = BSP_I2C_NUM, #if JC_PANEL_TYPE == WS_PANEL_7H // Stoerimpulsfilter und interne Abschlusswiderstaende - beides setzt auch das // Waveshare-Beispiel. Ohne den Filter brechen Bridge und Touch weg, sobald die // DSI-Ausgabe laeuft: Deren schnelle Signale stoeren auf das lange Flachbandkabel // ein, und ungefilterte Stoerspitzen bringen die I2C-Uebertragung aus dem Tritt. // Der Audio-Baustein auf der Platine bleibt dabei erreichbar, weil er nicht am // Displaykabel haengt - genau dieses Muster war im Start-Log zu sehen. .glitch_ignore_cnt = 7, .flags.enable_internal_pullup = true, #endif }; i2c_new_master_bus(&i2c_bus_conf, &s_i2c_handle); #if JC_PANEL_TYPE == WS_PANEL_7H ws7_i2c_scan("vor dem Panel-Start"); // Bridge des Waveshare-Panels vorbereiten (noch ohne Hintergrundbeleuchtung). ws7_bridge_pre_init(); #endif // Reset-Grund mitschreiben: 1 = Kaltstart (Strom weg), alles andere ist ein Warmstart. // Nur beim Warmstart behalten die Bausteine ihren alten Zustand - genau dann traten // die schwarzen Bilder auf. esp_rom_printf("[Panel] Startgrund: %d (1 = Kaltstart)\n", (int)esp_reset_reason()); static esp_ldo_channel_handle_t phy_pwr_chan = NULL; esp_ldo_channel_config_t ldo_cfg = { .chan_id = BSP_MIPI_DSI_PHY_PWR_LDO_CHAN, .voltage_mv = BSP_MIPI_DSI_PHY_PWR_LDO_VOLTAGE_MV, }; BSP_STEP(esp_ldo_acquire_channel(&ldo_cfg, &phy_pwr_chan), "Stromversorgung MIPI-D-PHY"); esp_lcd_dsi_bus_handle_t mipi_dsi_bus = NULL; esp_lcd_panel_io_handle_t io = NULL; esp_lcd_panel_handle_t disp_panel = NULL; #if JC_PANEL_TYPE == WS_PANEL_7H esp_lcd_dpi_panel_config_t dpi_config; #endif #if JC_PANEL_TYPE == WS_PANEL_7H // ---------------- Waveshare 7inch DSI LCD (H) (1280x720) ---------------- // Kein Hersteller-Panel-Treiber: DSI-Bus, DBI-Kommandokanal und DPI-Panel werden // direkt angelegt. Entscheidend ist die Referenztaktquelle PLL_F20M - mit der // Standardquelle bleibt das Bild auf ESP32-P4 Rev. 1.3 / ECO2 schwarz. bsp_report_psram((size_t)BSP_LCD_H_RES * BSP_LCD_V_RES * BSP_LCD_FB_BYTES_PER_PX * LVGL_PORT_LCD_BUFFER_NUMS); if (!ws7_dsi_bringup(&mipi_dsi_bus, &io, &disp_panel, &dpi_config)) { return false; } #if WS7_DSI_RESTART // Die Strecke einmal komplett ab- und wieder aufbauen. // // Beim ersten Einschalten kam trotz fehlerfreiem Hochlauf kein Bild; erst ein Reset // half, und zwar zuverlaessig. Ein Reset unterscheidet sich an dieser Stelle nur in // einem Punkt von unserem Ablauf: Die DSI-Verbindung geht einmal weg und neu auf, // waehrend Bridge und Panel bereits versorgt und eingestellt sind. Weder die // wiederholte Grundeinstellung der Bridge noch die wiederholten Weckbefehle ersetzen // das. Genau dieser Ab- und Wiederaufbau wird hier nachgeholt - ohne Reset. esp_rom_printf("[Panel] DSI-Strecke wird neu aufgebaut\n"); esp_lcd_panel_del(disp_panel); esp_lcd_panel_io_del(io); esp_lcd_del_dsi_bus(mipi_dsi_bus); vTaskDelay(pdMS_TO_TICKS(WS7_DSI_RESTART_MS)); if (!ws7_dsi_bringup(&mipi_dsi_bus, &io, &disp_panel, &dpi_config)) { return false; } #endif #if WS7_TEST_PATTERN // Prueffarbbalken: Sie entstehen im DSI-Baustein selbst und benutzen weder den // Bildspeicher noch LVGL. Erscheinen sie, arbeitet die Anzeigekette bis zum Panel und // der Fehler liegt weiter oben. Bleibt es schwarz, liegt es an DSI, Zeitbasis oder // Panel. // // Der Hochlauf endet hier bewusst: Wuerde LVGL danach starten, schriebe es sofort in // den Bildspeicher und das Pruefbild waere wieder weg - der Test saehe dann genauso // aus wie der Fehler, den er finden soll. jc_backlight_set(100); // Beleuchtung an, sonst ist nichts zu sehen BSP_STEP(esp_lcd_dpi_panel_set_pattern(disp_panel, MIPI_DSI_PATTERN_BAR_VERTICAL), "Prueffarbbalken"); esp_rom_printf("[Panel] Pruefbild steht - erwartet werden senkrechte Farbbalken.\n"); esp_rom_printf("[Panel] Hochlauf endet hier (WS7_TEST_PATTERN = 1, keine Oberflaeche).\n"); return false; // ohne LVGL: Anzeige bleibt beim Pruefbild #endif #elif JC_PANEL_TYPE == JC_PANEL_70 // ---------------- 7,0" JD9165 (1024x600) ---------------- esp_lcd_dsi_bus_config_t bus_config = JD9165_PANEL_BUS_DSI_2CH_CONFIG(); esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus); esp_lcd_dbi_io_config_t dbi_config = JD9165_PANEL_IO_DBI_CONFIG(); esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io); esp_lcd_dpi_panel_config_t dpi_config = JD9165_1024_600_PANEL_60HZ_DPI_CONFIG(LCD_COLOR_PIXEL_FORMAT_RGB565); dpi_config.num_fbs = LVGL_PORT_LCD_BUFFER_NUMS; jd9165_vendor_config_t vendor_config = { .mipi_config = { .dsi_bus = mipi_dsi_bus, .dpi_config = &dpi_config, }, }; esp_lcd_panel_dev_config_t lcd_dev_config = { .bits_per_pixel = 16, .rgb_ele_order = ESP_LCD_COLOR_SPACE_RGB, .reset_gpio_num = BSP_LCD_RST, .vendor_config = &vendor_config, }; esp_lcd_new_panel_jd9165(io, &lcd_dev_config, &disp_panel); #else // ---------------- 4,3" ST7701 (480x800) ---------------- esp_lcd_dsi_bus_config_t bus_config = ST7701_PANEL_BUS_DSI_2CH_CONFIG(); esp_lcd_new_dsi_bus(&bus_config, &mipi_dsi_bus); esp_lcd_dbi_io_config_t dbi_config = ST7701_PANEL_IO_DBI_CONFIG(); esp_lcd_new_panel_io_dbi(mipi_dsi_bus, &dbi_config, &io); esp_lcd_dpi_panel_config_t dpi_config = ST7701_480_360_PANEL_60HZ_DPI_CONFIG(LCD_COLOR_PIXEL_FORMAT_RGB565); dpi_config.num_fbs = LVGL_PORT_LCD_BUFFER_NUMS; st7701_vendor_config_t vendor_config = { .mipi_config = { .dsi_bus = mipi_dsi_bus, .dpi_config = &dpi_config, }, .flags = { .use_mipi_interface = 1, }, }; esp_lcd_panel_dev_config_t lcd_dev_config = { .bits_per_pixel = 16, .rgb_ele_order = ESP_LCD_COLOR_SPACE_RGB, .reset_gpio_num = BSP_LCD_RST, .vendor_config = &vendor_config, }; esp_lcd_new_panel_st7701(io, &lcd_dev_config, &disp_panel); #endif #if JC_PANEL_TYPE != WS_PANEL_7H esp_lcd_panel_reset(disp_panel); esp_lcd_panel_init(disp_panel); #endif // Alle Framebuffer einmal auf Schwarz loeschen. Im Avoid-Tear-Direct-Mode (mehrere // Buffer) kopiert LVGL pro Frame nur die geaenderten Bereiche in den naechsten Buffer; // bis alle Buffer einmal vollstaendig bemalt sind, wuerde sonst uninitialisierter // Speicher (weisses Rauschen) durchblitzen. Schwarz faellt gegen das dunkle Theme // nicht auf. Cache-Writeback noetig, da die Buffer im PSRAM liegen und der DMA liest. { void *fbs[3] = { NULL, NULL, NULL }; size_t fb_size = (size_t)BSP_LCD_H_RES * BSP_LCD_V_RES * BSP_LCD_FB_BYTES_PER_PX; #if LVGL_PORT_LCD_BUFFER_NUMS >= 3 esp_lcd_dpi_panel_get_frame_buffer(disp_panel, 3, &fbs[0], &fbs[1], &fbs[2]); #elif LVGL_PORT_LCD_BUFFER_NUMS == 2 esp_lcd_dpi_panel_get_frame_buffer(disp_panel, 2, &fbs[0], &fbs[1]); #else esp_lcd_dpi_panel_get_frame_buffer(disp_panel, 1, &fbs[0]); #endif for (int i = 0; i < 3; i++) { if (fbs[i]) { memset(fbs[i], 0, fb_size); esp_cache_msync(fbs[i], fb_size, ESP_CACHE_MSYNC_FLAG_DIR_C2M); } } } esp_lcd_dpi_panel_event_callbacks_t cbs = { #if LVGL_PORT_AVOID_TEAR_MODE .on_refresh_done = on_vsync, #else .on_color_trans_done = on_vsync, #endif }; esp_lcd_dpi_panel_register_event_callbacks(disp_panel, &cbs, NULL); #if JC_PANEL_TYPE == WS_PANEL_7H // Bridge freigeben und Beleuchtung einschalten, BEVOR der Touch angesprochen wird. // Der GT9271 antwortet erst danach: Die Freigabe der Bridge (Register 0xAD) versorgt // offenbar auch ihn. In der Werks-Testskizze steht die Touch-Initialisierung aus // demselben Grund hinter dem Einschalten der Beleuchtung. jc_backlight_set(100); vTaskDelay(pdMS_TO_TICKS(WS7_TOUCH_POWER_MS)); ws7_i2c_scan("nach dem Panel-Start"); #endif esp_lcd_panel_io_handle_t tp_io_handle = NULL; // MUSS vorbelegt sein: schlaegt die Touch-Initialisierung fehl, gibt der Treiber einen // unbrauchbaren Zeiger zurueck. Ohne Vorbelegung landet Muell im LVGL-Port. esp_lcd_touch_handle_t tp_handle = NULL; esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG(); tp_io_config.scl_speed_hz = 100000; #if JC_PANEL_TYPE == WS_PANEL_7H // GT9271 des Waveshare-Panels: gleiche Registerbelegung wie der GT911, aber // andere I2C-Adresse. tp_io_config.dev_addr = WS7_TOUCH_I2C_ADDR; #endif esp_lcd_new_panel_io_i2c(s_i2c_handle, &tp_io_config, &tp_io_handle); const esp_lcd_touch_config_t tp_cfg = { .x_max = BSP_LCD_H_RES, .y_max = BSP_LCD_V_RES, .rst_gpio_num = BSP_LCD_TOUCH_RST, .int_gpio_num = BSP_LCD_TOUCH_INT, .levels = { .reset = 0, .interrupt = 0, }, .flags = { .swap_xy = 0, .mirror_x = 0, .mirror_y = 0, }, #if JC_PANEL_TYPE == WS_PANEL_7H .process_coordinates = ws7_touch_map, #elif JC_PANEL_TYPE == JC_PANEL_70 .process_coordinates = jc_touch_scale, #endif }; if (esp_lcd_touch_new_i2c_gt911(tp_io_handle, &tp_cfg, &tp_handle) != ESP_OK) { esp_rom_printf("[Panel] Touch-Controller nicht gefunden - Anzeige laeuft ohne Bedienung.\n"); tp_handle = NULL; } #if JC_PANEL_TYPE == WS_PANEL_7H if (tp_handle) { // Aussetzer-Filter vor den Treiber haengen s_touch_get_xy_orig = tp_handle->get_xy; tp_handle->get_xy = ws7_touch_get_xy; } #endif lvgl_port_interface_t interface = (dpi_config.flags.use_dma2d) ? LVGL_PORT_INTERFACE_MIPI_DSI_DMA : LVGL_PORT_INTERFACE_MIPI_DSI_NO_DMA; esp_rom_printf("[Panel] Bringup fertig - LVGL wird gestartet\n"); lvgl_port_init(disp_panel, tp_handle, interface); esp_rom_printf("[Panel] LVGL laeuft\n"); return true; } #endif // JC_USE_REAL_PANEL