diff options
| author | Luke Hoersten <[email protected]> | 2026-06-14 16:36:46 -0500 |
|---|---|---|
| committer | Luke Hoersten <[email protected]> | 2026-06-14 16:36:46 -0500 |
| commit | 6c1a26bdae63d152caa45023e8255561a22fc0e2 (patch) | |
| tree | 00528dbe08c0dadf4c6f261e02365f932e8f69a0 /main | |
| parent | d5cdb6b4e4424d80ac9bfd8e12c129334c23d6f2 (diff) | |
M3: panel renders — TC358762 bridge init + non-burst DSI
The Hosyond 5" panel uses Pi 7" v1.1 architecture (ATTINY ID 0xC3 +
TC358762 DSI-to-DPI bridge). Getting it to paint needed three things
that ESP-IDF doesn't expose:
1. TC358762 bridge configuration — 16 register writes via DSI Generic
Long Write (DT=0x29) packets, transcribed from Linux's tc358762.c.
2. Non-burst video mode — IDF hardcodes BURST_WITH_SYNC_PULSES, but
TC358762 requires NON_BURST_WITH_SYNC_PULSES. Overridden via the
mipi_dsi_host_ll_* API after esp_lcd_new_panel_dpi() and before
panel_init().
3. ATTINY v1.1 power-on sequence + SPI-proxy bridge wake. The legacy
REG_POWERON(0x85) path doesn't apply to 0xC3 firmware; instead
write PORTC/PORTA/PORTB/PORTC in order, then later release bridge
reset and proxy-write TC358762 SYSPMCTRL=0 through the ATTINY's
ADDR_H/L + WR_DATA_H/L registers.
Reaching the LL/HAL APIs requires shadowing esp_lcd_dsi_bus_t so we can
pick the mipi_dsi_hal_context_t out of the private struct. Documented
the layout dependency at the shadow definition.
Tuned config (observed stable on ESP32-P4 per embenix's reference):
- 1 data lane @ 600 Mbps
- DPI 26 MHz (Linux modeline is 25.98)
- timings HSW=2 HBP=46 HFP=210 / VSW=20 VBP=4 VFP=22
- RGB888 end-to-end, R,G,B byte order (BGR was wrong)
- disable_lp=0 so LP windows are available for the bridge writes
Diffstat (limited to 'main')
| -rw-r--r-- | main/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | main/display.c | 355 |
2 files changed, 222 insertions, 135 deletions
diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b9cb2a4..4cd3b9a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -3,5 +3,5 @@ idf_component_register( INCLUDE_DIRS "." REQUIRES driver esp_driver_i2c esp_driver_jpeg esp_eth esp_event esp_http_client esp_http_server esp_hw_support esp_lcd - esp_netif esp_timer json mdns nvs_flash + esp_netif esp_pm esp_timer hal json mdns nvs_flash ) diff --git a/main/display.c b/main/display.c index e2f9999..c37ea52 100644 --- a/main/display.c +++ b/main/display.c @@ -9,10 +9,15 @@ #include "esp_heap_caps.h" #include "esp_ldo_regulator.h" #include "esp_lcd_mipi_dsi.h" +#include "esp_lcd_panel_io.h" #include "esp_lcd_panel_ops.h" #include "esp_log.h" +#include "esp_pm.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "hal/mipi_dsi_hal.h" +#include "hal/mipi_dsi_host_ll.h" +#include "hal/mipi_dsi_types.h" #include "viewport_state.h" @@ -20,77 +25,95 @@ static const char *TAG = "display"; // ============================================================================ // Pin / bus assignments -// -// TODO confirm against the Waveshare ESP32-P4-ETH schematic and the -// 15-pin Pi FPC → 22-pin Waveshare DSI adapter. The two I2C lines below -// must reach the Hosyond panel's 15-pin FPC at the standard Pi positions. -// Waveshare's bundled-panel BSP uses these GPIOs for touch I2C; whether -// they also carry through their DSI connector to the FPC is unconfirmed. // ============================================================================ #define PIN_I2C_SDA 7 #define PIN_I2C_SCL 8 #define I2C_PORT I2C_NUM_0 -#define I2C_FREQ_HZ 100000 // panel MCU is slow; 100kHz is safe +#define I2C_FREQ_HZ 400000 -// Panel I2C addresses (Pi 7" touchscreen architecture, replicated by -// Hosyond/Elecrow/etc. clones for dtoverlay=vc4-kms-dsi-7inch). -#define PANEL_MCU_ADDR 0x45 +#define PANEL_MCU_ADDR 0x45 // ATTINY88 on the Pi 7" panel architecture #define TOUCH_FT5426_ADDR 0x38 -// Pi panel MCU register map. See Linux: -// drivers/gpu/drm/panel/panel-raspberrypi-touchscreen.c +// ATTINY88 register map. Source: +// linux/drivers/regulator/rpi-panel-attiny-regulator.c (rpi-6.6.y) enum { - REG_ID = 0x80, // reads back 0xC3 on a healthy panel - REG_PORTA = 0x81, - REG_PORTB = 0x82, - REG_PORTC = 0x83, - REG_PORTD = 0x84, - REG_POWERON = 0x85, // write 1 to power up; 0 to power down - REG_PWM = 0x86, // 0..255 backlight duty - REG_ADDR_H = 0x87, - REG_ADDR_L = 0x88, - REG_WRITE_H = 0x89, - REG_WRITE_L = 0x8a, - REG_READ_H = 0x8b, - REG_READ_L = 0x8c, + REG_ID = 0x80, // 0xC3 on v1.1 firmware + REG_PORTA = 0x81, + REG_PORTB = 0x82, + REG_PORTC = 0x83, + REG_PWM = 0x86, // 0..255 backlight duty + REG_ADDR_L = 0x8c, // TC358762 target addr low + REG_ADDR_H = 0x8d, // ...high + REG_WR_DATA_H = 0x90, // TC358762 write data high + REG_WR_DATA_L = 0x91, // ...low }; -#define PANEL_ID_EXPECTED 0xC3 -#define PANEL_POWERON_DELAY_MS 120 // empirically ~80ms; pad to be safe +#define PA_LCD_LR (1u << 2) // horizontal scan direction +#define PB_LCD_MAIN (1u << 7) // main regulator enable +#define PC_LED_EN (1u << 0) +#define PC_RST_TP_N (1u << 1) +#define PC_RST_LCD_N (1u << 2) +#define PC_RST_BRIDGE_N (1u << 3) + +#define PANEL_ID_EXPECTED 0xC3 // ============================================================================ -// DSI / DPI panel timing for 800x480 @ 60Hz -// (Raspberry Pi 7" canonical timings — same panel architecture as Hosyond 5") +// 800x480 timings. Constants copied verbatim from the embenix reference +// (rpi-7inch-touch-display-v1.c), which calls them "observed stable" on +// ESP32-P4. The Linux upstream modeline differs in HFP/VSW; if pixels look +// shifted left/right these are the first knobs to revisit. // ============================================================================ #define PANEL_H_ACTIVE 800 #define PANEL_V_ACTIVE 480 -// Pi 7" canonical timings from the upstream RPi DTS panel binding. -// Hosyond 5" panels are TC358762-based clones and use the same. -#define PANEL_HSYNC_PULSE 1 -#define PANEL_HSYNC_BP 46 -#define PANEL_HSYNC_FP 16 -#define PANEL_VSYNC_PULSE 1 -#define PANEL_VSYNC_BP 33 -#define PANEL_VSYNC_FP 7 -#define PANEL_PIXEL_CLOCK_MHZ 25 -// Lane bit rate. ESP32-P4 MIPI-DSI PHY PLL is picky about valid lock -// frequencies; 480 Mbps caused the PLL-lock busy-wait inside -// esp_lcd_new_dsi_bus() to hang indefinitely. 1000 Mbps matches the -// reference example and is well within the TC358762 bridge's max. -// Pi 7"-style panels via the 15-pin FPC traditionally use a single DSI -// data lane; the second data pair in the FPC stays unused. The DSI link -// rate is bumped to compensate (PLL-valid: 720/20 = 36 even). -#define DSI_LANE_RATE_MBPS 720 -#define DSI_NUM_DATA_LANES 1 - -// The ESP32-P4 MIPI-DSI PHY needs VDD_MIPI_DPHY = 2.5V powered before its -// PLL can lock. Internal LDO_VO3 (channel 3) is the source on this SoC, -// per ESP-IDF's mipi_dsi example. Without this the PHY-lock busy-wait -// inside esp_lcd_new_dsi_bus() spins forever. -#define DSI_PHY_LDO_CHAN 3 +#define RPI_HSW 2 +#define RPI_HBP 46 +#define RPI_HFP 210 +#define RPI_VSW 20 +#define RPI_VBP 4 +#define RPI_VFP 22 + +#define PANEL_PIXEL_CLOCK_MHZ 26 // Linux modeline = 25.9794 MHz; field is integer MHz +#define DSI_LANE_RATE_MBPS 600 // embenix "observed stable" on ESP32-P4 +#define DSI_NUM_DATA_LANES 1 // TC358762 Linux driver is single-lane + +#define DSI_PHY_LDO_CHAN 3 // VDD_MIPI_DPHY rail on ESP32-P4 #define DSI_PHY_LDO_MV 2500 // ============================================================================ +// TC358762 DSI-to-DPI bridge register map. Source: +// linux/drivers/gpu/drm/bridge/tc358762.c (rpi-6.6.y) tc358762_init() +// Bridge is configured by sending DSI Generic Long Write (DT=0x29) packets +// to the link with a 6-byte payload: [reg_lo, reg_hi, val_b0..val_b3]. +// ============================================================================ +#define TC_DSI_LANEENABLE 0x0210 // CLEN | D0EN | D1EN +#define TC_PPI_D0S_CLRSIPOCOUNT 0x0164 +#define TC_PPI_D1S_CLRSIPOCOUNT 0x0168 +#define TC_PPI_D0S_ATMR 0x0144 +#define TC_PPI_D1S_ATMR 0x0148 +#define TC_PPI_LPTXTIMECNT 0x0114 +#define TC_SPICMR 0x0450 +#define TC_LCDCTRL 0x0420 +#define TC_SYSCTRL 0x0464 +#define TC_LCD_HS_HBP 0x0424 +#define TC_LCD_HDISP_HFP 0x0428 +#define TC_LCD_VS_VBP 0x042c +#define TC_LCD_VDISP_VFP 0x0430 +#define TC_PPI_STARTPPI 0x0104 +#define TC_DSI_STARTDSI 0x0204 + +// Shadow of esp_lcd_dsi_bus_t. The IDF private struct is +// { int bus_id; mipi_dsi_hal_context_t hal; esp_pm_lock_handle_t pm_lock; } +// (esp-idf/components/esp_lcd/dsi/mipi_dsi_priv.h). We only need bus_id + +// hal to reach the LL/HAL APIs, so the trailing pm_lock is fine to ignore. +// If the IDF layout shifts in a future release this is the place that +// breaks first — guarded by ESP-IDF version is overkill until it actually +// breaks. +typedef struct { + int bus_id; + mipi_dsi_hal_context_t hal; +} dsi_bus_shadow_t; + +// ============================================================================ // Module state // ============================================================================ static i2c_master_bus_handle_t s_i2c_bus; @@ -99,14 +122,11 @@ static esp_lcd_dsi_bus_handle_t s_dsi_bus; static esp_lcd_panel_handle_t s_panel; static bool s_up; static uint8_t s_last_pwm; -// RGB888 scratch buffer used as the source to draw_bitmap. 3 bytes/pixel -// matches the DPI driver's framebuffer stride; landscape and portrait -// both go through here so we get a single consistent expand-from-RGB565 -// path. 800 * 480 * 3 = ~1.15 MB in PSRAM. +// RGB888 panel-sized scratch buffer (800*480*3 ≈ 1.15 MB in PSRAM). static uint8_t *s_rot_buf; // ============================================================================ -// Panel-MCU I2C helpers +// ATTINY88 I2C helpers // ============================================================================ static esp_err_t mcu_write_u8(uint8_t reg, uint8_t val) { @@ -144,59 +164,112 @@ static esp_err_t panel_mcu_attach(void) uint8_t id = 0; esp_err_t err = mcu_read_u8(REG_ID, &id); if (err != ESP_OK) { - ESP_LOGE(TAG, "panel MCU @0x45 unreachable (%s) — check FPC + adapter", - esp_err_to_name(err)); + ESP_LOGE(TAG, "panel MCU @0x45 unreachable (%s)", esp_err_to_name(err)); return err; } if (id != PANEL_ID_EXPECTED) { - ESP_LOGW(TAG, "panel MCU id 0x%02x (expected 0x%02x) — wiring " - "looks ok but panel might be a different rev", - id, PANEL_ID_EXPECTED); - // Continue anyway; some clones report different IDs. + ESP_LOGW(TAG, "panel MCU id 0x%02x (expected 0x%02x)", id, PANEL_ID_EXPECTED); } else { - ESP_LOGI(TAG, "panel MCU id 0x%02x — Pi 7\" architecture ack'd", id); + ESP_LOGI(TAG, "panel MCU id 0x%02x — Pi v1.1 architecture ack'd", id); } return ESP_OK; } +// ATTINY88 v1.1 power-on sequence. Mirrors attiny_lcd_power_enable() in +// drivers/regulator/rpi-panel-attiny-regulator.c. No REG_POWERON write — +// that legacy register only exists on the older 0xDE firmware. The bridge +// stays in reset at this point; it's released later in panel_bringup() once +// the DSI HS clock is running and ready to source LP commands. static esp_err_t panel_power_on(void) { - // Full Pi 7" enable sequence, mirroring Linux's panel-raspberrypi- - // touchscreen.c. Without the PORTA/PORTB/PORTC writes the TC358762 - // converts DSI to DPI fine, but the ATTINY never enables the actual - // panel chip + backlight — so the panel stays dark. - - ESP_RETURN_ON_ERROR(mcu_write_u8(REG_POWERON, 1), TAG, "POWERON=1"); - vTaskDelay(pdMS_TO_TICKS(PANEL_POWERON_DELAY_MS)); - - // Optional: poll REG_PORTB bit 0 for nPWRDWN going high. Bounded - // wait so we don't hang on a non-responsive panel. - for (int i = 0; i < 100; ++i) { - uint8_t portb = 0; - if (mcu_read_u8(REG_PORTB, &portb) == ESP_OK && (portb & 0x01)) break; - vTaskDelay(pdMS_TO_TICKS(2)); - } + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTC, 0x00), TAG, "PORTC=0 (assert all resets)"); + vTaskDelay(pdMS_TO_TICKS(10)); + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTA, PA_LCD_LR), TAG, "PORTA"); + vTaskDelay(pdMS_TO_TICKS(10)); + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTB, PB_LCD_MAIN), TAG, "PORTB"); + vTaskDelay(pdMS_TO_TICKS(10)); + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTC, PC_LED_EN), TAG, "PORTC=LED"); + vTaskDelay(pdMS_TO_TICKS(80)); + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PWM, 0x00), TAG, "PWM=0"); + ESP_LOGI(TAG, "panel power-on (v1.1 regulator protocol) complete; bridge held in reset"); + return ESP_OK; +} + +// Release TC358762 reset and wake the bridge via the ATTINY's SPI proxy. +// Mirrors attiny_gpio_set(RST_BRIDGE_N=1) in the Linux driver, which writes +// TC358762 SYSPMCTRL (0x047c) = 0 to take the bridge out of low-power. +static esp_err_t bridge_release_and_wake(void) +{ + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTC, + PC_LED_EN | PC_RST_LCD_N | PC_RST_BRIDGE_N), TAG, "PORTC bridge release"); + vTaskDelay(pdMS_TO_TICKS(10)); + + // 16-bit SPI proxy write of SYSPMCTRL = 0x0000. + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_ADDR_H, 0x04), TAG, "ADDR_H"); + vTaskDelay(pdMS_TO_TICKS(8)); + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_ADDR_L, 0x7c), TAG, "ADDR_L"); + vTaskDelay(pdMS_TO_TICKS(8)); + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_WR_DATA_H, 0x00), TAG, "WR_DATA_H"); + vTaskDelay(pdMS_TO_TICKS(8)); + ESP_RETURN_ON_ERROR(mcu_write_u8(REG_WR_DATA_L, 0x00), TAG, "WR_DATA_L"); + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_LOGI(TAG, "bridge reset released, SYSPMCTRL=0 wake sent via ATTINY proxy"); + return ESP_OK; +} - // Enable the panel chip via the ATTINY's GPIO expanders. Values match - // the upstream Linux driver and the original Pi 7" closed-source FW. - ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTA, 0x04), TAG, "PORTA"); - ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTB, 0x00), TAG, "PORTB"); - // Leave PWM at 0 (backlight off) — the boot sequence calls - // display_sleep() after init; first interaction (tap, BOOT, /state) - // brings the backlight up via display_wake(). - ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PWM, 0x00), TAG, "PWM init"); - ESP_RETURN_ON_ERROR(mcu_write_u8(REG_PORTC, 0x01), TAG, "PORTC"); - - ESP_LOGI(TAG, "panel powered on (PORTA/B/C + PWM init)"); +static esp_err_t touch_release_reset(void) +{ + return mcu_write_u8(REG_PORTC, + PC_LED_EN | PC_RST_TP_N | PC_RST_LCD_N | PC_RST_BRIDGE_N); +} + +// ============================================================================ +// TC358762 bridge configuration (DSI Generic Long Write packets) +// Sequence mirrors Linux drivers/gpu/drm/bridge/tc358762.c tc358762_init(). +// ============================================================================ +static void bridge_write(uint16_t reg, uint32_t val) +{ + dsi_bus_shadow_t *p = (dsi_bus_shadow_t *)s_dsi_bus; + uint8_t payload[6] = { + (uint8_t)(reg & 0xff), (uint8_t)(reg >> 8), + (uint8_t)(val & 0xff), (uint8_t)((val >> 8) & 0xff), + (uint8_t)((val >> 16) & 0xff), (uint8_t)((val >> 24) & 0xff), + }; + mipi_dsi_hal_host_gen_write_long_packet(&p->hal, 0, + MIPI_DSI_DT_GENERIC_LONG_WRITE, payload, sizeof(payload)); +} + +static esp_err_t bridge_init(void) +{ + // 1-lane: CLEN | D0EN. Linux driver's TODO note confirms second lane + // isn't supported by the bridge in this firmware. + bridge_write(TC_DSI_LANEENABLE, (1u << 0) | (1u << 1)); + bridge_write(TC_PPI_D0S_CLRSIPOCOUNT, 0x05); + bridge_write(TC_PPI_D1S_CLRSIPOCOUNT, 0x05); + bridge_write(TC_PPI_D0S_ATMR, 0x00); + bridge_write(TC_PPI_D1S_ATMR, 0x00); + bridge_write(TC_PPI_LPTXTIMECNT, 0x03); + bridge_write(TC_SPICMR, 0x00); + // LCDCTRL = VSDELAY=1 | RGB888 | UNK6 | VTGEN — bit pattern from Linux. + bridge_write(TC_LCDCTRL, 0x00100150); + bridge_write(TC_SYSCTRL, 0x040f); + bridge_write(TC_LCD_HS_HBP, ((uint32_t)RPI_HBP << 16) | RPI_HSW); + bridge_write(TC_LCD_HDISP_HFP, ((uint32_t)RPI_HFP << 16) | PANEL_H_ACTIVE); + bridge_write(TC_LCD_VS_VBP, ((uint32_t)RPI_VBP << 16) | RPI_VSW); + bridge_write(TC_LCD_VDISP_VFP, ((uint32_t)RPI_VFP << 16) | PANEL_V_ACTIVE); + vTaskDelay(pdMS_TO_TICKS(100)); + bridge_write(TC_PPI_STARTPPI, 0x01); + bridge_write(TC_DSI_STARTDSI, 0x01); + vTaskDelay(pdMS_TO_TICKS(100)); + ESP_LOGI(TAG, "TC358762 bridge configured"); return ESP_OK; } // ============================================================================ -// DSI bring-up (DPI video mode — Pi-style panels need no DSI command init) +// DSI bring-up // ============================================================================ static esp_err_t dsi_bring_up(void) { - // 1. Power up the DSI PHY (VDD_MIPI_DPHY 2.5V) before any DSI calls. esp_ldo_channel_handle_t phy_pwr = NULL; esp_ldo_channel_config_t ldo_cfg = { .chan_id = DSI_PHY_LDO_CHAN, @@ -204,8 +277,7 @@ static esp_err_t dsi_bring_up(void) }; ESP_RETURN_ON_ERROR(esp_ldo_acquire_channel(&ldo_cfg, &phy_pwr), TAG, "DSI PHY LDO acquire"); - ESP_LOGI(TAG, "MIPI DSI PHY powered on (LDO%d @ %dmV)", - DSI_PHY_LDO_CHAN, DSI_PHY_LDO_MV); + ESP_LOGI(TAG, "MIPI DSI PHY powered on (LDO%d @ %dmV)", DSI_PHY_LDO_CHAN, DSI_PHY_LDO_MV); esp_lcd_dsi_bus_config_t bus_cfg = { .bus_id = 0, @@ -216,27 +288,24 @@ static esp_err_t dsi_bring_up(void) ESP_RETURN_ON_ERROR(esp_lcd_new_dsi_bus(&bus_cfg, &s_dsi_bus), TAG, "esp_lcd_new_dsi_bus"); - // DBI IO handle — the IDF reference example creates this even for - // panels that don't use DSI commands. Some DSI bridge state machines - // need the command-channel side initialized before video mode runs. + // Create + delete a DBI IO so the DSI controller latches LP speed mode + // for Generic Long Writes. The setting persists after the IO is + // deleted; bridge_init() depends on it to schedule its 16 register + // writes during LP windows of the video stream. + esp_lcd_panel_io_handle_t dbi_io = NULL; esp_lcd_dbi_io_config_t dbi_cfg = { .virtual_channel = 0, .lcd_cmd_bits = 8, .lcd_param_bits = 8, }; - esp_lcd_panel_io_handle_t dbi_io; ESP_RETURN_ON_ERROR(esp_lcd_new_panel_io_dbi(s_dsi_bus, &dbi_cfg, &dbi_io), TAG, "esp_lcd_new_panel_io_dbi"); + esp_lcd_panel_io_del(dbi_io); esp_lcd_dpi_panel_config_t dpi_cfg = { .virtual_channel = 0, .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT, .dpi_clock_freq_mhz = PANEL_PIXEL_CLOCK_MHZ, - // TC358762 only decodes 24-bit DSI Video Mode packets. Keep the - // entire pipeline RGB888 so the framebuffer stride (3 B/pixel) - // matches what the DSI engine sends on the wire. Our internal - // text/JPEG rendering still produces RGB565; the rotation step - // in display_present_rgb565() expands to RGB888 inline. .pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB888, .in_color_format = LCD_COLOR_FMT_RGB888, .out_color_format = LCD_COLOR_FMT_RGB888, @@ -244,24 +313,51 @@ static esp_err_t dsi_bring_up(void) .video_timing = { .h_size = PANEL_H_ACTIVE, .v_size = PANEL_V_ACTIVE, - .hsync_pulse_width = PANEL_HSYNC_PULSE, - .hsync_back_porch = PANEL_HSYNC_BP, - .hsync_front_porch = PANEL_HSYNC_FP, - .vsync_pulse_width = PANEL_VSYNC_PULSE, - .vsync_back_porch = PANEL_VSYNC_BP, - .vsync_front_porch = PANEL_VSYNC_FP, + .hsync_pulse_width = RPI_HSW, + .hsync_back_porch = RPI_HBP, + .hsync_front_porch = RPI_HFP, + .vsync_pulse_width = RPI_VSW, + .vsync_back_porch = RPI_VBP, + .vsync_front_porch = RPI_VFP, }, .flags.use_dma2d = false, + // Allow LP blanking so the host can insert LP Generic Long Write + // packets into VBI — required by TC358762, which only processes + // commands during low-power intervals. + .flags.disable_lp = 0, }; ESP_RETURN_ON_ERROR(esp_lcd_new_panel_dpi(s_dsi_bus, &dpi_cfg, &s_panel), TAG, "esp_lcd_new_panel_dpi"); - // No esp_lcd_panel_reset() — the IDF DPI driver doesn't implement it - // (returns NOT_SUPPORTED). It only exists for panels with vendor - // drivers that toggle a hardware reset pin via DSI commands. - ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "panel_init"); - ESP_LOGI(TAG, "DSI up: %dx%d %d MHz, %d-lane %d Mbps", - PANEL_H_ACTIVE, PANEL_V_ACTIVE, PANEL_PIXEL_CLOCK_MHZ, + // Override two DSI controller defaults that ESP-IDF hardcodes but + // TC358762 needs different values for: + // 1. Burst mode: hardcoded to BURST_WITH_SYNC_PULSES, but TC358762 + // requires NON_BURST_WITH_SYNC_PULSES to match its DPI clock. + // 2. Frame ACK: TC358762 doesn't BTA on every video frame; leaving + // this on stalls the stream. + dsi_bus_shadow_t *p = (dsi_bus_shadow_t *)s_dsi_bus; + mipi_dsi_host_ll_dpi_set_video_burst_type(p->hal.host, + MIPI_DSI_LL_VIDEO_NON_BURST_WITH_SYNC_PULSES); + mipi_dsi_host_ll_dpi_enable_frame_ack(p->hal.host, false); + + ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "panel_init"); + + // Continuous HS clock — TC358762's internal FLL needs a stable + // reference, and we still allow LP data-lane blanking so command + // packets can be inserted between frames. + mipi_dsi_host_ll_set_clock_lane_state(p->hal.host, MIPI_DSI_LL_CLOCK_LANE_STATE_HS); + mipi_dsi_host_ll_enable_cmd_ack(p->hal.host, false); + + // Now the HS clock is running — release bridge reset and configure it. + // Doing this before HS is up would silently drop every LP command. + ESP_RETURN_ON_ERROR(bridge_release_and_wake(), TAG, "bridge wake"); + ESP_RETURN_ON_ERROR(bridge_init(), TAG, "bridge init"); + + // Release touch reset right before FT5426 will be probed. + touch_release_reset(); + + ESP_LOGI(TAG, "DSI up: %dx%d %u MHz, %d-lane %d Mbps, non-burst", + PANEL_H_ACTIVE, PANEL_V_ACTIVE, (unsigned)PANEL_PIXEL_CLOCK_MHZ, DSI_NUM_DATA_LANES, DSI_LANE_RATE_MBPS); return ESP_OK; } @@ -274,10 +370,9 @@ esp_err_t display_init(void) if (s_up) return ESP_OK; ESP_RETURN_ON_ERROR(panel_mcu_attach(), TAG, "panel MCU attach"); - ESP_RETURN_ON_ERROR(panel_power_on(), TAG, "panel power-on"); - ESP_RETURN_ON_ERROR(dsi_bring_up(), TAG, "DSI bring-up"); + ESP_RETURN_ON_ERROR(panel_power_on(), TAG, "panel power-on"); + ESP_RETURN_ON_ERROR(dsi_bring_up(), TAG, "DSI bring-up"); - // PSRAM scratch (panel-native 800x480 RGB888 = 3 bytes/pixel). s_rot_buf = (uint8_t *)heap_caps_malloc( PANEL_H_ACTIVE * PANEL_V_ACTIVE * 3, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); @@ -288,9 +383,7 @@ esp_err_t display_init(void) s_up = true; - // Cache the brightness value but do NOT push it to the panel yet. - // The device boots with backlight off; first wake (BOOT short-press, - // tap, or POST /state {wake}) applies the brightness. + // Cache configured brightness, leave backlight off — first wake applies it. viewport_state_lock(); s_last_pwm = viewport_state_get()->brightness; viewport_state_unlock(); @@ -306,7 +399,6 @@ i2c_master_bus_handle_t display_i2c_bus(void) { return s_i2c_bus; } esp_err_t display_set_brightness(uint8_t pct) { if (pct > 100) pct = 100; - // Perceptual: duty = (pct/100)^2.2 * 255. float frac = (float)pct / 100.0f; float gamma = powf(frac, 2.2f); uint8_t duty = (uint8_t)(gamma * 255.0f + 0.5f); @@ -332,15 +424,13 @@ esp_err_t display_wake(void) return err; } -// Convert a 16-bit RGB565 to three bytes in B, G, R order. -// The TC358762 → Pi-style panel pipeline expects BGR on the DPI wire, -// matching the panel chip's native byte order. Swapping to RGB produced -// garbled vertical bands; BGR aligns the pixel layout. +// RGB565 → RGB888 in R,G,B byte order. TC358762's LCDCTRL = RGB888 path +// expects channel-order matching the DSI 24bpp packing. static inline void rgb565_to_rgb888(uint16_t px, uint8_t *out) { - out[0] = (uint8_t)(( px & 0x1F) * 255 / 31); // B - out[1] = (uint8_t)(((px >> 5) & 0x3F) * 255 / 63); // G - out[2] = (uint8_t)(((px >> 11) & 0x1F) * 255 / 31); // R + out[0] = (uint8_t)(((px >> 11) & 0x1F) * 255 / 31); // R + out[1] = (uint8_t)(((px >> 5) & 0x3F) * 255 / 63); // G + out[2] = (uint8_t)(( px & 0x1F) * 255 / 31); // B } esp_err_t display_fill(uint16_t rgb565) @@ -374,17 +464,14 @@ esp_err_t display_present_rgb565(const uint16_t *src, if (orient == VIEWPORT_ORIENTATION_LANDSCAPE) { if (src_w != PANEL_H_ACTIVE || src_h != PANEL_V_ACTIVE) return ESP_ERR_INVALID_SIZE; - // RGB565 → RGB888, no rotation. size_t n = (size_t)src_w * src_h; for (size_t i = 0; i < n; ++i) { rgb565_to_rgb888(src[i], &s_rot_buf[i * 3]); } } else { - // Portrait: src is 480x800; rotate 90° CW into 800x480 panel - // coordinates and expand to RGB888 in the same pass. if (src_w != PANEL_V_ACTIVE || src_h != PANEL_H_ACTIVE) return ESP_ERR_INVALID_SIZE; - const uint16_t dst_stride = src_h; // 800 panel-rows of 3 bytes each + const uint16_t dst_stride = src_h; for (uint16_t y = 0; y < src_h; ++y) { const uint16_t *srow = src + (size_t)y * src_w; const uint16_t dst_col = (uint16_t)(src_h - 1 - y); |
