src.nth.io/

summaryrefslogtreecommitdiff
path: root/main
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-14 16:36:46 -0500
committerLuke Hoersten <[email protected]>2026-06-14 16:36:46 -0500
commit6c1a26bdae63d152caa45023e8255561a22fc0e2 (patch)
tree00528dbe08c0dadf4c6f261e02365f932e8f69a0 /main
parentd5cdb6b4e4424d80ac9bfd8e12c129334c23d6f2 (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.txt2
-rw-r--r--main/display.c355
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);