feat(core): TS7 display refresh rate scaling and configuration optimization
What changed, and why it matters
This commit changes how the Trezor hardware wallet's display is driven, mainly to save battery. It adds the ability to switch between 60 Hz and 30 Hz refresh rates depending on whether the screen is showing animations or a static image. It also reorganizes display timing numbers into panel-specific configuration files. There is no direct evidence in the commit that this fixes a security vulnerability; it reads as a power-optimization and code-cleanup change.
Treat as a normal firmware display-driver update. No immediate security action is indicated by the supplied materials. If reviewing for product safety, validate that the new interrupt-driven refresh-rate changes and the LTDC/DSI disable-re-enable workaround do not introduce display glitches or timing regressions on the affected panel.
Security signals we found
No security-relevant signals found in commit message or diff.
Change is described by the vendor as power-consumption optimization and configuration formalization.
Refresh-rate scaling runs in interrupt context with IRQ locks and busy-wait timeout, which could affect real-time behavior but is not shown to be exploitable.
No input validation, buffer handling, cryptographic, or trust-boundary changes are present.
Evidence from the diff
The patch refactors the STM32 LTDC/DSI display driver for Trezor Safe 5 (TS7). It introduces interrupt-based refresh-rate scaling (60 Hz for animations, 30 Hz for static content) gated by REFRESH_RATE_SCALING_SUPPORTED, currently enabled only for the lx250a2401a panel. It moves magic timing/pll/phy constants into per-panel headers and updates DSI/LTDC register calculations to use those macros. A state machine in the LTDC line-event ISR handles VFP/total-height updates with IRQ locking and a small busy-wait timeout. The commit also includes a workaround that disables and re-enables LTDC/DSI after init to avoid a first-frame artifact.
Changed components
core/embed/io/display/ltdc_dsi/display_driver.ccore/embed/io/display/ltdc_dsi/display_fb.ccore/embed/io/display/ltdc_dsi/display_internal.hcore/embed/io/display/ltdc_dsi/panels/lx200d2406a/lx200d2406a.hcore/embed/io/display/ltdc_dsi/panels/lx250a2401a/lx250a2401a.hcore/embed/io/display/ltdc_dsi/panels/stm32u5a9j-dk/stm32u5a9j-dk.hInspect captured patch +474 / −86
diff --git a/core/embed/io/display/ltdc_dsi/display_driver.c b/core/embed/io/display/ltdc_dsi/display_driver.c
index 9b873089..7ff3c8b4 100644
--- a/core/embed/io/display/ltdc_dsi/display_driver.c
+++ b/core/embed/io/display/ltdc_dsi/display_driver.c
@@ -35,6 +35,14 @@
#include "display_internal.h"
+#if REFRESH_RATE_SCALING_SUPPORTED
+// VFP lookup table for different refresh rates
+static const uint32_t vfp_lut[REFRESH_RATE_COUNT] = {
+ [REFRESH_RATE_HI] = VFP_REFRESH_RATE_HI,
+ [REFRESH_RATE_LO] = VFP_REFRESH_RATE_LO,
+};
+#endif
+
display_driver_t g_display_driver = {
.initialized = false,
};
@@ -43,24 +51,13 @@ static void display_pll_deinit(void) { __HAL_RCC_PLL3_DISABLE(); }
static bool display_pll_init(void) {
/* Start and configure PLL3 */
- /* HSE = 16/32MHZ */
- /* 16/32/(M=8) = 4MHz input (min) */
- /* 4*(N=125) = 500MHz VCO (almost max) */
- /* 500/(P=8) = 62.5 for DSI is exactly the lane byte clock*/
-
__HAL_RCC_PLL3_DISABLE();
while (__HAL_RCC_GET_FLAG(RCC_FLAG_PLL3RDY) != 0U)
;
-#if HSE_VALUE == 32000000
-
- __HAL_RCC_PLL3_CONFIG(RCC_PLLSOURCE_HSE, 8,
- ((DSI_LANE_BYTE_FREQ_HZ * 8) / 4000000), 8, 8, 24);
-#elif HSE_VALUE == 16000000
- __HAL_RCC_PLL3_CONFIG(RCC_PLLSOURCE_HSE, 4,
- ((DSI_LANE_BYTE_FREQ_HZ * 8) / 4000000), 8, 8, 24);
-#endif
+ __HAL_RCC_PLL3_CONFIG(RCC_PLLSOURCE_HSE, PLL3_M, PLL3_N, PLL3_P, PLL3_Q,
+ PLL3_R);
__HAL_RCC_PLL3_VCIRANGE(RCC_PLLVCIRANGE_0);
@@ -104,7 +101,7 @@ static bool display_dsi_init(display_driver_t *drv) {
__HAL_DSI_ENABLE(&drv->hlcd_dsi);
- // /* Enable the DSI PLL */
+ /* Enable the DSI PLL */
__HAL_DSI_PLL_ENABLE(&drv->hlcd_dsi);
HAL_Delay(1);
@@ -131,18 +128,17 @@ static bool display_dsi_init(display_driver_t *drv) {
/* DSI initialization */
drv->hlcd_dsi.Instance = DSI;
+ // Erratum "DSI automatic clock lane control not functional" =>
+ // it can't be enabled.
drv->hlcd_dsi.Init.AutomaticClockLaneControl = DSI_AUTO_CLK_LANE_CTRL_DISABLE;
- /* We have 1 data lane at 500Mbps => lane byte clock at 500/8 = 62,5 MHZ */
- /* We want TX escape clock at around 20MHz and under 20MHz so clock division
- * is set to 4 */
- drv->hlcd_dsi.Init.TXEscapeCkdiv = 4;
+ drv->hlcd_dsi.Init.TXEscapeCkdiv = DSI_TX_ESCAPE_CLK_DIV;
drv->hlcd_dsi.Init.NumberOfLanes = PANEL_DSI_LANES;
- drv->hlcd_dsi.Init.PHYFrequencyRange = DSI_DPHY_FRANGE_450MHZ_510MHZ;
- drv->hlcd_dsi.Init.PHYLowPowerOffset = 0;
+ drv->hlcd_dsi.Init.PHYFrequencyRange = DSI_DPHY_FRANGE;
+ drv->hlcd_dsi.Init.PHYLowPowerOffset = PHY_LP_OFFSET;
- PLLInit.PLLNDIV = ((DSI_LANE_BYTE_FREQ_HZ * 8 * 2 * 4) / (2 * HSE_VALUE));
- PLLInit.PLLIDF = 4;
- PLLInit.PLLODF = 2;
+ PLLInit.PLLIDF = PLL_DSI_IDF;
+ PLLInit.PLLNDIV = PLL_DSI_NDIV;
+ PLLInit.PLLODF = PLL_DSI_ODF;
PLLInit.PLLVCORange = DSI_DPHY_VCO_FRANGE_800MHZ_1GHZ;
PLLInit.PLLChargePump = DSI_PLL_CHARGE_PUMP_2000HZ_4400HZ;
PLLInit.PLLTuning = DSI_PLL_LOOP_FILTER_2000HZ_4400HZ;
@@ -159,22 +155,24 @@ static bool display_dsi_init(display_driver_t *drv) {
drv->DSIVidCfg.HSPolarity = DSI_HSYNC_ACTIVE_HIGH;
drv->DSIVidCfg.VSPolarity = DSI_VSYNC_ACTIVE_HIGH;
drv->DSIVidCfg.DEPolarity = DSI_DATA_ENABLE_ACTIVE_HIGH;
- drv->DSIVidCfg.ColorCoding = DSI_RGB888;
+ drv->DSIVidCfg.ColorCoding = PANEL_DSI_COLOR_CODING;
drv->DSIVidCfg.Mode = PANEL_DSI_MODE;
- drv->DSIVidCfg.PacketSize = LCD_WIDTH;
- drv->DSIVidCfg.NullPacketSize = 0xFFFU;
- drv->DSIVidCfg.HorizontalSyncActive = HSYNC * 3;
- drv->DSIVidCfg.HorizontalBackPorch = HBP * 3;
- drv->DSIVidCfg.HorizontalLine = (HACT + HSYNC + HBP + HFP) * 3;
+ // In burst mode, the packet size must be greater or equal to the visible
+ // width.
+ drv->DSIVidCfg.PacketSize = HACT;
+ drv->DSIVidCfg.NumberOfChunks = 0; // No chunks in burst mode
+ drv->DSIVidCfg.NullPacketSize = 0; // No null packet in burst mode
+ drv->DSIVidCfg.HorizontalSyncActive = HSYNC * LANE_BYTE_2_PIXEL_CLK_RATIO;
+ drv->DSIVidCfg.HorizontalBackPorch = HBP * LANE_BYTE_2_PIXEL_CLK_RATIO;
+ drv->DSIVidCfg.HorizontalLine =
+ (HSYNC + HBP + HACT + HFP) * LANE_BYTE_2_PIXEL_CLK_RATIO;
drv->DSIVidCfg.VerticalSyncActive = VSYNC;
drv->DSIVidCfg.VerticalBackPorch = VBP;
drv->DSIVidCfg.VerticalFrontPorch = VFP;
drv->DSIVidCfg.VerticalActive = VACT;
drv->DSIVidCfg.LPCommandEnable = DSI_LP_COMMAND_ENABLE;
drv->DSIVidCfg.LPLargestPacketSize = 64;
- /* Specify for each region of the video frame, if the transmission of command
- * in LP mode is allowed in this region */
- /* while streaming is active in video mode */
+ // Enable entering LP in all regions if timing constraints allow it.
drv->DSIVidCfg.LPHorizontalFrontPorchEnable = DSI_LP_HFP_ENABLE;
drv->DSIVidCfg.LPHorizontalBackPorchEnable = DSI_LP_HBP_ENABLE;
drv->DSIVidCfg.LPVerticalActiveEnable = DSI_LP_VACT_ENABLE;
@@ -189,13 +187,11 @@ static bool display_dsi_init(display_driver_t *drv) {
goto cleanup;
}
- /*********************/
- /* LCD configuration */
- /*********************/
- PhyTimers.ClockLaneHS2LPTime = 11;
- PhyTimers.ClockLaneLP2HSTime = 40;
- PhyTimers.DataLaneHS2LPTime = 12;
- PhyTimers.DataLaneLP2HSTime = 23;
+ // RM0456 Table 445. HS2LP and LP2HS values vs. band frequency (MHz)
+ PhyTimers.ClockLaneHS2LPTime = PHY_TIMER_CLK_HS2LP;
+ PhyTimers.ClockLaneLP2HSTime = PHY_TIMER_CLK_LP2HS;
+ PhyTimers.DataLaneHS2LPTime = PHY_TIMER_DATA_HS2LP;
+ PhyTimers.DataLaneLP2HSTime = PHY_TIMER_DATA_LP2HS;
PhyTimers.DataLaneMaxReadTime = 0;
PhyTimers.StopWaitTime = 7;
@@ -285,8 +281,8 @@ static bool display_ltdc_init(display_driver_t *drv, uint32_t fb_addr) {
drv->hlcd_ltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC;
drv->hlcd_ltdc.Init.HorizontalSync = HSYNC - 1;
drv->hlcd_ltdc.Init.AccumulatedHBP = HSYNC + HBP - 1;
- drv->hlcd_ltdc.Init.AccumulatedActiveW = HACT + HBP + HSYNC - 1;
- drv->hlcd_ltdc.Init.TotalWidth = HACT + HBP + HFP + HSYNC - 1;
+ drv->hlcd_ltdc.Init.AccumulatedActiveW = HSYNC + HBP + HACT - 1;
+ drv->hlcd_ltdc.Init.TotalWidth = HSYNC + HBP + HACT + HFP - 1;
drv->hlcd_ltdc.Init.Backcolor.Red = 0; /* Not used default value */
drv->hlcd_ltdc.Init.Backcolor.Green = 0; /* Not used default value */
drv->hlcd_ltdc.Init.Backcolor.Blue = 0; /* Not used default value */
@@ -381,7 +377,8 @@ bool display_init(display_content_mode_t mode) {
goto cleanup;
}
- if (HAL_LTDC_ProgramLineEvent(&drv->hlcd_ltdc, LCD_HEIGHT) != HAL_OK) {
+ if (HAL_LTDC_ProgramLineEvent(&drv->hlcd_ltdc, LINE_EVENT_GENERAL_LINE) !=
+ HAL_OK) {
goto cleanup;
}
@@ -396,6 +393,31 @@ bool display_init(display_content_mode_t mode) {
gfx_bitblt_init();
+ // Workaround to avoid a wrong image display for 1st refresh rate change.
+ // It has been observed that the first change of the refresh rate after
+ // initialization causes improper display update. Disabling and re-enabling
+ // the LTDC and DSI seems to solve the issue.
+ //
+ // TODO: review the configuration sequence of the LTDC and DSI to avoid this.
+ // See RM0456 44.14.1 Programing procedure overview
+ __HAL_LTDC_DISABLE(&drv->hlcd_ltdc);
+ __HAL_DSI_DISABLE(&drv->hlcd_dsi);
+
+ __HAL_DSI_ENABLE(&drv->hlcd_dsi);
+ __HAL_LTDC_ENABLE(&drv->hlcd_ltdc);
+ // Workaround end.
+
+#if REFRESH_RATE_SCALING_SUPPORTED
+ // No need to lock IRQs here because the "drv->initialized" flag is not set
+ // yet.
+ drv->refresh_rate_state = REFRESH_RATE_IDLE;
+ drv->refresh_rate = REFRESH_RATE_HI;
+ // Set the timeout variable to return to the low refresh rate after the
+ // "REFRESH_RATE_HI2LO_TIMEOUT_MS" time of inactivity.
+ drv->refresh_rate_timeout_ms = ticks_timeout(REFRESH_RATE_HI2LO_TIMEOUT_MS);
+ drv->refresh_rate_timeout_set = true;
+#endif
+
drv->initialized = true;
return true;
@@ -442,6 +464,148 @@ void display_deinit(display_content_mode_t mode) {
memset(drv, 0, sizeof(display_driver_t));
}
+#if REFRESH_RATE_SCALING_SUPPORTED
+void display_refresh_rate_timeout_set(void) {
+ display_driver_t *drv = &g_display_driver;
+ irq_key_t key;
+
+ if (!drv->initialized) {
+ return;
+ }
+
+ key = irq_lock();
+
+ // Set/refresh the timeout variable to return to the low refresh rate after
+ // the "REFRESH_RATE_HI2LO_TIMEOUT_MS" time of inactivity.
+ drv->refresh_rate_timeout_ms = ticks_timeout(REFRESH_RATE_HI2LO_TIMEOUT_MS);
+ drv->refresh_rate_timeout_set = true;
+
+ irq_unlock(key);
+}
+
+void display_refresh_rate_timeout_check(void) {
+ display_driver_t *drv = &g_display_driver;
+ irq_key_t key;
+
+ if (!drv->initialized) {
+ return;
+ }
+
+ // The function is called from an IRQ context. It might be possible to NOT
+ // disable IRQs and make preemption (of higher prio IRQs) possible.
+ // To be safe, we disable IRQs here.
+ key = irq_lock();
+
+ // Is timeout set and expired? Return to the low refresh rate.
+ if (drv->refresh_rate_timeout_set &&
+ ticks_expired(drv->refresh_rate_timeout_ms)) {
+ // Change the display refresh rate to the low refresh rate.
+ display_refresh_rate_set(REFRESH_RATE_LO);
+ drv->refresh_rate_timeout_set = false;
+ }
+
+ irq_unlock(key);
+}
+
+static inline void display_refresh_rate_reg_config(display_driver_t *drv) {
+ // LTDC && DSI disable.
+ __HAL_LTDC_DISABLE(&drv->hlcd_ltdc);
+ __HAL_DSI_DISABLE(&drv->hlcd_dsi);
+
+ // Set the Vertical Front Porch (VFP).
+ ATOMIC_MODIFY_REG(drv->hlcd_dsi.Instance->VVFPCR, DSI_VVFPCR_VFP_Msk,
+ drv->DSIVidCfg.VerticalFrontPorch);
+
+ // Set Total Height.
+ ATOMIC_MODIFY_REG(drv->hlcd_ltdc.Instance->TWCR, LTDC_TWCR_TOTALH_Msk,
+ drv->hlcd_ltdc.Init.TotalHeigh);
+
+ // DSI && LTDC enable.
+ __HAL_DSI_ENABLE(&drv->hlcd_dsi);
+ __HAL_LTDC_ENABLE(&drv->hlcd_ltdc);
+}
+
+void display_refresh_rate_set(display_refresh_rate_t refresh_rate) {
+ display_driver_t *drv = &g_display_driver;
+ irq_key_t key;
+
+ if (!drv->initialized) {
+ return;
+ }
+
+ key = irq_lock();
+
+ if (refresh_rate < REFRESH_RATE_COUNT && refresh_rate != drv->refresh_rate) {
+ // Update the requested refresh rate. Do it in any state of the state
+ // machine. The actual update will be performed in the IRQ context.
+ // The respective VFP and Total Height values will be set there.
+ drv->refresh_rate = refresh_rate;
+
+ if (drv->refresh_rate_state == REFRESH_RATE_IDLE) {
+ // Move the state machine forward to request the update in the "Line
+ // Event" IRQ handler.
+ drv->refresh_rate_state = REFRESH_RATE_REQUESTED;
+ }
+ }
+
+ irq_unlock(key);
+}
+
+void display_refresh_rate_config(void) {
+ display_driver_t *drv = &g_display_driver;
+ irq_key_t key;
+
+ if (!drv->initialized) {
+ return;
+ }
+
+ // The function is called from an IRQ context. It might be possible to NOT
+ // disable IRQs and make preemption (of higher prio IRQs) possible.
+ // To be safe, we disable IRQs here.
+ key = irq_lock();
+
+ if (drv->refresh_rate_state == REFRESH_RATE_UPDATING) {
+ // 30 us timeout, because the line takes max 29.75us at 18.518519MHz pixel
+ // clock and 544 pixel line width (including porches and sync).
+ uint64_t timeout_us = systick_us() + REFRESH_RATE_CFG_TIMEOUT_US;
+
+ // Check if we are in the vertical sync period. If yes, we have no idea
+ // where exactly we are in the VSYNC, so we can't safely update the
+ // registers now. We postpone the update - moving back to the REQUESTED
+ // state to try again later.
+ if (READ_BIT(drv->hlcd_ltdc.Instance->CDSR, LTDC_CDSR_VSYNCS) == 0) {
+ // Busy waiting for VSYNC with timeout. As soon as VSYNC starts (==1),
+ // we can proceed with the update.
+ while (READ_BIT(drv->hlcd_ltdc.Instance->CDSR, LTDC_CDSR_VSYNCS) == 0) {
+ if (systick_us() > timeout_us) {
+ // Failed to update, moving back to REQUESTED state to try again.
+ drv->refresh_rate_state = REFRESH_RATE_REQUESTED;
+
+ irq_unlock(key);
+ return;
+ }
+ }
+
+ // Prepare the structures for the update.
+ drv->DSIVidCfg.VerticalFrontPorch = vfp_lut[drv->refresh_rate];
+ drv->hlcd_ltdc.Init.TotalHeigh = drv->hlcd_ltdc.Init.AccumulatedActiveH +
+ drv->DSIVidCfg.VerticalFrontPorch;
+
+ // Perform the update of the registers.
+ display_refresh_rate_reg_config(drv);
+
+ // Updated: moving to the IDLE state.
+ drv->refresh_rate_state = REFRESH_RATE_IDLE;
+ } else {
+ // Failed to update, moving back to REQUESTED state to try again.
+ drv->refresh_rate_state = REFRESH_RATE_REQUESTED;
+ }
+ }
+
+ irq_unlock(key);
+}
+#endif // REFRESH_RATE_SCALING_SUPPORTED
+
bool display_set_backlight(uint8_t level) {
display_driver_t *drv = &g_display_driver;
diff --git a/core/embed/io/display/ltdc_dsi/display_fb.c b/core/embed/io/display/ltdc_dsi/display_fb.c
index 53221bab..0cb42f48 100644
--- a/core/embed/io/display/ltdc_dsi/display_fb.c
+++ b/core/embed/io/display/ltdc_dsi/display_fb.c
@@ -24,6 +24,7 @@
#include <io/display.h>
#include <sys/irq.h>
#include <sys/mpu.h>
+#include <sys/systick.h>
#include <sys/trustzone.h>
#include "display_internal.h"
@@ -139,6 +140,20 @@ void display_refresh(void) {
return;
}
+#if REFRESH_RATE_SCALING_SUPPORTED
+ // IRQs locked to make sure that no IRQ gets served in beween the following
+ // 2 function calls including the IRQ context call of
+ // display_refresh_rate_timeout_check() function.
+ irq_key_t key = irq_lock();
+
+ // Change the display refresh rate to the high refresh rate.
+ display_refresh_rate_set(REFRESH_RATE_HI);
+ // Set/refresh the timeout for return to the low refresh rate.
+ display_refresh_rate_timeout_set();
+
+ irq_unlock(key);
+#endif
+
fb_queue_put(&drv->ready_frames, fb_queue_take(&drv->empty_frames));
}
@@ -193,6 +208,42 @@ void HAL_LTDC_LineEvenCallback(LTDC_HandleTypeDef *hltdc) {
return;
}
+#if REFRESH_RATE_SCALING_SUPPORTED
+ if (drv->refresh_rate_state == REFRESH_RATE_UPDATING) {
+ display_refresh_rate_config();
+
+ // Configure the next line event for standard operation.
+ HAL_LTDC_ProgramLineEvent(&drv->hlcd_ltdc, LINE_EVENT_GENERAL_LINE);
+ } else {
+ display_refresh_rate_timeout_check();
+
+ // Process pending frame buffer update.
+ if (drv->update_pending > 0) {
+ drv->update_pending--;
+ }
+
+ int16_t fb_idx = fb_queue_take(&drv->ready_frames);
+ if (fb_idx >= 0) {
+ fb_queue_put(&drv->empty_frames, drv->active_frame);
+ drv->active_frame = fb_idx;
+ display_set_fb((uint32_t)get_fb_ptr(drv->active_frame));
+ drv->update_pending = 3;
+ }
+
+ // Is refresh rate update requested? Configure the line event for the
+ // proper time to perform VFP update.
+ if (drv->refresh_rate_state == REFRESH_RATE_REQUESTED) {
+ // Configure the line event for the proper time to perform VFP update.
+ HAL_LTDC_ProgramLineEvent(&drv->hlcd_ltdc, LINE_EVENT_REFRESH_RATE_LINE);
+
+ // The line event has been configured. Moving to the UPDATING state.
+ drv->refresh_rate_state = REFRESH_RATE_UPDATING;
+ } else {
+ // Configure the next line event for standard operation.
+ HAL_LTDC_ProgramLineEvent(&drv->hlcd_ltdc, LINE_EVENT_GENERAL_LINE);
+ }
+ }
+#else
if (drv->update_pending > 0) {
drv->update_pending--;
}
@@ -205,7 +256,8 @@ void HAL_LTDC_LineEvenCallback(LTDC_HandleTypeDef *hltdc) {
drv->update_pending = 3;
}
- HAL_LTDC_ProgramLineEvent(&drv->hlcd_ltdc, LCD_HEIGHT);
+ HAL_LTDC_ProgramLineEvent(&drv->hlcd_ltdc, LINE_EVENT_GENERAL_LINE);
+#endif // REFRESH_RATE_SCALING_SUPPORTED
}
#endif
diff --git a/core/embed/io/display/ltdc_dsi/display_internal.h b/core/embed/io/display/ltdc_dsi/display_internal.h
index dc486899..f9a1777c 100644
--- a/core/embed/io/display/ltdc_dsi/display_internal.h
+++ b/core/embed/io/display/ltdc_dsi/display_internal.h
@@ -46,6 +46,36 @@
#define PHYSICAL_FRAME_BUFFER_ALIGNMENT 32
#endif
+#define LINE_EVENT_GENERAL_LINE (drv->hlcd_ltdc.Init.AccumulatedActiveH)
+
+#if REFRESH_RATE_SCALING_SUPPORTED
+#define LINE_EVENT_REFRESH_RATE_LINE (drv->hlcd_ltdc.Init.TotalHeigh)
+
+#define REFRESH_RATE_HI2LO_TIMEOUT_MS 2000U // 2 seconds
+// IMPORTANT:
+// The "REFRESH_RATE_CFG_TIMEOUT_US" timeout must be sufficiently low because
+// the line takes max 29.75us at 18.518519MHz pixel clock and 544 pixel line
+// width (including porches and sync).
+// The higher value could lead to the situation that the refresh rate change
+// is applied in a wrong position of the display frame causing visible
+// artifacts.
+#define REFRESH_RATE_CFG_TIMEOUT_US 30U // 30 microseconds
+
+// Supported display refresh rates
+typedef enum {
+ REFRESH_RATE_HI,
+ REFRESH_RATE_LO,
+ REFRESH_RATE_COUNT // Number of refresh rate options (sentinel value)
+} display_refresh_rate_t;
+
+// Display refresh rate SM states
+typedef enum {
+ REFRESH_RATE_IDLE,
+ REFRESH_RATE_REQUESTED,
+ REFRESH_RATE_UPDATING
+} display_refresh_rate_state_t;
+#endif
+
typedef struct {
bool initialized;
uint16_t update_pending;
@@ -61,6 +91,13 @@ typedef struct {
uint8_t backlight_level;
// The current frame buffer selector
+#if REFRESH_RATE_SCALING_SUPPORTED
+ volatile display_refresh_rate_state_t refresh_rate_state;
+ volatile display_refresh_rate_t refresh_rate;
+ volatile uint32_t refresh_rate_timeout_ms;
+ volatile bool refresh_rate_timeout_set;
+#endif
+
DSI_HandleTypeDef hlcd_dsi;
LTDC_HandleTypeDef hlcd_ltdc;
DSI_VidCfgTypeDef DSIVidCfg;
@@ -95,4 +132,11 @@ bool display_gfxmmu_init(display_driver_t *drv);
void display_gfxmmu_deinit(display_driver_t *drv);
#endif
+#if REFRESH_RATE_SCALING_SUPPORTED
+void display_refresh_rate_timeout_set(void);
+void display_refresh_rate_timeout_check(void);
+void display_refresh_rate_set(display_refresh_rate_t refresh_rate);
+void display_refresh_rate_config(void);
+#endif
+
#endif // TREZOR_HAL_DISPLAY_INTERNAL_H
diff --git a/core/embed/io/display/ltdc_dsi/panels/lx200d2406a/lx200d2406a.h b/core/embed/io/display/ltdc_dsi/panels/lx200d2406a/lx200d2406a.h
index 29d0a7b3..eff4aeae 100644
--- a/core/embed/io/display/ltdc_dsi/panels/lx200d2406a/lx200d2406a.h
+++ b/core/embed/io/display/ltdc_dsi/panels/lx200d2406a/lx200d2406a.h
@@ -21,26 +21,65 @@
#include <trezor_types.h>
-#define DSI_LANE_BYTE_FREQ_HZ 62500000ULL
-
-#define VSYNC 4
-#define VBP 4
-#define VFP 660
-#define VACT 320
-#define HSYNC 30
-#define HBP 60
-#define HFP 60
-#define HACT 240
-#define LCD_WIDTH 240
-#define LCD_HEIGHT 320
+#define REFRESH_RATE_SCALING_SUPPORTED 0
-#define LCD_X_OFFSET 0
-#define LCD_Y_OFFSET 0
+// DSI PLL configuration (lane byte clock, TX escape clock)
+// DSI_LANE_BYTE_CLOCK_HZ = (((HSE_VALUE / PLL_DSI_IDF) * 2 * PLL_DSI_NDIV) /
+// PLL_DSI_ODF) / 8
+#define DSI_LANE_BYTE_CLOCK_HZ 62000000UL // PLL DSI
+#define PLL_DSI_IDF 4
+// PLL_DSI_NDIV = (DSI_LANE_BYTE_CLOCK_HZ * 8 * PLL_DSI_ODF * PLL_DSI_IDF) / (2
+// * HSE_VALUE)
+#define PLL_DSI_NDIV 62
+#define PLL_DSI_ODF 2
+#define DSI_DPHY_FRANGE DSI_DPHY_FRANGE_450MHZ_510MHZ
+#define DSI_TX_ESCAPE_CLK_DIV 4 // 15.5MHz, ~7.75MHz (in LP)
+
+// DSI PHY timing parameters configuration
+#define PHY_LP_OFFSET PHY_LP_OFFSSET_0_CLKP // LPXO - no offset
+// RM0456 Table 445. HS2LP and LP2HS values vs. band frequency (MHz)
+#define PHY_TIMER_CLK_HS2LP 11
+#define PHY_TIMER_CLK_LP2HS 40
+#define PHY_TIMER_DATA_HS2LP 12
+#define PHY_TIMER_DATA_LP2HS 23
+
+// LTDC PLL3 configuration (pixel clock and lane byte clock at the beginning of
+// initialization)
+// LTDC_PIXEL_CLOCK_HZ = ((HSE_VALUE / PLL3_M) * PLL3_N) / PLL3_R
+#define LTDC_PIXEL_CLOCK_HZ 20833333UL // Output of PLL3R
+// 4MHz is used as PLL3 block input clock
+#define PLL3_M (HSE_VALUE / 4000000UL)
+#define PLL3_N 125
+#define PLL3_P 8
+#define PLL3_Q 8 // Not used output clock branch
+#define PLL3_R 24
+
+// DSI lane byte clock to LTDC pixel clock ratio
+#define LANE_BYTE_2_PIXEL_CLK_RATIO 3
+
+// Display timing parameters
+#define HSYNC 30 // Horizontal Sync
+#define HBP 60 // Horizontal Back Porch
+#define HACT 240 // Horizontal Active Time
+#define HFP 60 // Horizontal Front Porch
+
+#define VSYNC 4 // Vertical Sync
+#define VBP 4 // Vertical Back Porch
+#define VACT 320 // Vertical Active Time
+#define VFP 660 // Vertical Front Porch
#define PANEL_DSI_MODE DSI_VID_MODE_NB_PULSES
#define PANEL_DSI_LANES DSI_ONE_DATA_LANE
+#define PANEL_DSI_COLOR_CODING DSI_RGB888
+
#define PANEL_LTDC_PIXEL_FORMAT LTDC_PIXEL_FORMAT_RGB565
+#define LCD_WIDTH 240
+#define LCD_HEIGHT 320
+
+#define LCD_X_OFFSET 0
+#define LCD_Y_OFFSET 0
+
// Size of the physical frame buffer in bytes
//
// It's smaller than size of the virtual frame buffer
diff --git a/core/embed/io/display/ltdc_dsi/panels/lx250a2401a/lx250a2401a.h b/core/embed/io/display/ltdc_dsi/panels/lx250a2401a/lx250a2401a.h
index 11668fc5..80b14d53 100644
--- a/core/embed/io/display/ltdc_dsi/panels/lx250a2401a/lx250a2401a.h
+++ b/core/embed/io/display/ltdc_dsi/panels/lx250a2401a/lx250a2401a.h
@@ -21,30 +21,78 @@
#include <trezor_types.h>
-#define DSI_LANE_BYTE_FREQ_HZ 56000000ULL
-
-#define VSYNC 2
-#define VBP 26
-#define VFP 16
-#define VACT 520
-#define HSYNC 6
-#define HBP 2
-#define HFP 56
-#define HACT 480
+#define REFRESH_RATE_SCALING_SUPPORTED 1
+
+#define PANEL_REFRESH_RATE_HI 60
+#define PANEL_REFRESH_RATE_LO 30
+
+// DSI PLL configuration (lane byte clock, TX escape clock)
+// DSI_LANE_BYTE_CLOCK_HZ = (((HSE_VALUE / PLL_DSI_IDF) * 2 * PLL_DSI_NDIV) /
+// PLL_DSI_ODF) / 8
+#define DSI_LANE_BYTE_CLOCK_HZ 62000000UL // PLL DSI
+#define PLL_DSI_IDF 4
+// PLL_DSI_NDIV = (DSI_LANE_BYTE_CLOCK_HZ * 8 * PLL_DSI_ODF * PLL_DSI_IDF) / (2
+// * HSE_VALUE)
+#define PLL_DSI_NDIV 62
+#define PLL_DSI_ODF 2
+#define DSI_DPHY_FRANGE DSI_DPHY_FRANGE_450MHZ_510MHZ
+#define DSI_TX_ESCAPE_CLK_DIV 4 // 15.5MHz, ~7.75MHz (in LP)
+
+// DSI PHY timing parameters configuration
+#define PHY_LP_OFFSET PHY_LP_OFFSSET_0_CLKP // LPXO - no offset
+// RM0456 Table 445. HS2LP and LP2HS values vs. band frequency (MHz)
+#define PHY_TIMER_CLK_HS2LP 11
+#define PHY_TIMER_CLK_LP2HS 40
+#define PHY_TIMER_DATA_HS2LP 12
+#define PHY_TIMER_DATA_LP2HS 23
+
+// LTDC PLL3 configuration (pixel clock and lane byte clock at the beginning of
+// initialization)
+// LTDC_PIXEL_CLOCK_HZ = ((HSE_VALUE / PLL3_M) * PLL3_N) / PLL3_R
+#define LTDC_PIXEL_CLOCK_HZ 18518519UL // Output of PLL3R
+// 4MHz is used as PLL3 block input clock
+#define PLL3_M (HSE_VALUE / 4000000UL)
+#define PLL3_N 125
+#define PLL3_P 8
+#define PLL3_Q 8 // Not used output clock branch
+#define PLL3_R 27
+
+// DSI lane byte clock to LTDC pixel clock ratio (floating point)
+#define LANE_BYTE_2_PIXEL_CLK_RATIO \
+ ((float)DSI_LANE_BYTE_CLOCK_HZ / (float)LTDC_PIXEL_CLOCK_HZ)
+
+// Display timing parameters
+#define HSYNC 6 // Horizontal Sync
+#define HBP 2 // Horizontal Back Porch
+#define HACT 480 // Horizontal Active Time
+#define HFP 56 // Horizontal Front Porch
+
+#define VSYNC 2 // Vertical Sync
+#define VBP 26 // Vertical Back Porch
+#define VACT 520 // Vertical Active Time
+#define VFP_CALC(f) \
+ ((LTDC_PIXEL_CLOCK_HZ / ((f) * (HSYNC + HBP + HACT + HFP))) - \
+ (VSYNC + VBP + VACT))
+#define VFP_REFRESH_RATE_HI VFP_CALC(PANEL_REFRESH_RATE_HI)
+#define VFP_REFRESH_RATE_LO VFP_CALC(PANEL_REFRESH_RATE_LO)
+#define VFP VFP_REFRESH_RATE_HI // Vertical Front Porch
+
+#define PANEL_DSI_MODE DSI_VID_MODE_BURST
+#define PANEL_DSI_LANES DSI_TWO_DATA_LANES
+#define PANEL_DSI_COLOR_CODING DSI_RGB888
+
+#define PANEL_LTDC_PIXEL_FORMAT LTDC_PIXEL_FORMAT_ARGB8888
+
#define LCD_WIDTH 480
#define LCD_HEIGHT 520
-#define LCD_Y_OFFSET 0
#define LCD_X_OFFSET 50
+#define LCD_Y_OFFSET 0
#define GFXMMU_LUT_FIRST 0
#define GFXMMU_LUT_LAST 519
#define GFXMMU_LUT_SIZE 520
-#define PANEL_DSI_MODE DSI_VID_MODE_BURST
-#define PANEL_DSI_LANES DSI_TWO_DATA_LANES
-#define PANEL_LTDC_PIXEL_FORMAT LTDC_PIXEL_FORMAT_ARGB8888
-
// IMPORTANT:
//
// Changing this value affects constants in backlight.rs and bootui.h
diff --git a/core/embed/io/display/ltdc_dsi/panels/stm32u5a9j-dk/stm32u5a9j-dk.h b/core/embed/io/display/ltdc_dsi/panels/stm32u5a9j-dk/stm32u5a9j-dk.h
index 1ecfa807..d68f0c83 100644
--- a/core/embed/io/display/ltdc_dsi/panels/stm32u5a9j-dk/stm32u5a9j-dk.h
+++ b/core/embed/io/display/ltdc_dsi/panels/stm32u5a9j-dk/stm32u5a9j-dk.h
@@ -19,16 +19,61 @@
#pragma once
-#define DSI_LANE_BYTE_FREQ_HZ 62500000ULL
-
-#define VSYNC 1
-#define VBP 12
-#define VFP 50
-#define VACT 481
-#define HSYNC 2
-#define HBP 1
-#define HFP 1
-#define HACT 480
+#include <trezor_types.h>
+
+#define REFRESH_RATE_SCALING_SUPPORTED 0
+
+// DSI PLL configuration (lane byte clock, TX escape clock)
+// DSI_LANE_BYTE_CLOCK_HZ = (((HSE_VALUE / PLL_DSI_IDF) * 2 * PLL_DSI_NDIV) /
+// PLL_DSI_ODF) / 8
+#define DSI_LANE_BYTE_CLOCK_HZ 62000000UL // PLL DSI
+#define PLL_DSI_IDF 4
+// PLL_DSI_NDIV = (DSI_LANE_BYTE_CLOCK_HZ * 8 * PLL_DSI_ODF * PLL_DSI_IDF) / (2
+// * HSE_VALUE)
+#define PLL_DSI_NDIV 62
+#define PLL_DSI_ODF 2
+#define DSI_DPHY_FRANGE DSI_DPHY_FRANGE_450MHZ_510MHZ
+#define DSI_TX_ESCAPE_CLK_DIV 4 // 15.5MHz, ~7.75MHz (in LP)
+
+// DSI PHY timing parameters configuration
+#define PHY_LP_OFFSET PHY_LP_OFFSSET_0_CLKP // LPXO - no offset
+// RM0456 Table 445. HS2LP and LP2HS values vs. band frequency (MHz)
+#define PHY_TIMER_CLK_HS2LP 11
+#define PHY_TIMER_CLK_LP2HS 40
+#define PHY_TIMER_DATA_HS2LP 12
+#define PHY_TIMER_DATA_LP2HS 23
+
+// LTDC PLL3 configuration (pixel clock and lane byte clock at the beginning of
+// initialization)
+// LTDC_PIXEL_CLOCK_HZ = ((HSE_VALUE / PLL3_M) * PLL3_N) / PLL3_R
+#define LTDC_PIXEL_CLOCK_HZ 20833333UL // Output of PLL3R
+// 4MHz is used as PLL3 block input clock
+#define PLL3_M (HSE_VALUE / 4000000UL)
+#define PLL3_N 125
+#define PLL3_P 8
+#define PLL3_Q 8 // Not used output clock branch
+#define PLL3_R 24
+
+// DSI lane byte clock to LTDC pixel clock ratio
+#define LANE_BYTE_2_PIXEL_CLK_RATIO 3
+
+// Display timing parameters
+#define HSYNC 2 // Horizontal Sync
+#define HBP 1 // Horizontal Back Porch
+#define HACT 480 // Horizontal Active Time
+#define HFP 1 // Horizontal Front Porch
+
+#define VSYNC 1 // Vertical Sync
+#define VBP 12 // Vertical Back Porch
+#define VACT 481 // Vertical Active Time
+#define VFP 50 // Vertical Front Porch
+
+#define PANEL_DSI_MODE DSI_VID_MODE_BURST
+#define PANEL_DSI_LANES DSI_TWO_DATA_LANES
+#define PANEL_DSI_COLOR_CODING DSI_RGB888
+
+#define PANEL_LTDC_PIXEL_FORMAT LTDC_PIXEL_FORMAT_ARGB8888
+
#define LCD_WIDTH 480
#define LCD_HEIGHT 480
@@ -40,10 +85,6 @@
#define GFXMMU_LUT_LAST 479
#define GFXMMU_LUT_SIZE 480
-#define PANEL_DSI_MODE DSI_VID_MODE_BURST
-#define PANEL_DSI_LANES DSI_TWO_DATA_LANES
-#define PANEL_LTDC_PIXEL_FORMAT LTDC_PIXEL_FORMAT_ARGB8888
-
// Size of the physical frame buffer in bytes
#define PHYSICAL_FRAME_BUFFER_SIZE (240 * 240 * 4)
Why this scored 11/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.