@@ -104,7 +104,7 @@ I2sDma::I2sDma(const Hub75Config &config)
104104 i2s_dev_ (nullptr ),
105105 bit_depth_(HUB75_BIT_DEPTH),
106106 lsbMsbTransitionBit_(0 ),
107- actual_clock_hz_(0 ),
107+ actual_clock_hz_(resolve_actual_clock_speed(config.output_clock_speed) ),
108108 panel_width_(config.panel_width),
109109 panel_height_(config.panel_height),
110110 layout_rows_(config.layout_rows),
@@ -285,6 +285,50 @@ bool I2sDma::init() {
285285 return true ;
286286}
287287
288+ HUB75_CONST uint32_t I2sDma::resolve_actual_clock_speed (Hub75ClockSpeed clock_speed) const {
289+ // I2S LCD mode clock derivation:
290+ // Output = base_clock / clkm_div / (tx_bck_div_num * 2)
291+ // We use tx_bck_div_num = 2, so: Output = base_clock / clkm_div / 4
292+ //
293+ // TRM Constraints (ESP32 TRM v5.3, Section 12.6):
294+ // - clkm_div >= 2: "I2S_CLKM_DIV_NUM: Integral I2S clock divider value.
295+ // fI2S = fCLK / I2S_CLKM_DIV_NUM (I2S_CLKM_DIV_NUM >= 2)"
296+ // - tx_bck_div_num >= 2: Required for LCD mode stability
297+ //
298+ // These constraints limit max achievable frequency significantly compared
299+ // to ESP32-S3's LCD_CAM peripheral which has no such divider requirements.
300+ uint32_t requested_hz = static_cast <uint32_t >(clock_speed);
301+
302+ #if defined(CONFIG_IDF_TARGET_ESP32S2)
303+ // ESP32-S2: PLL_160M clock source (160 MHz base)
304+ // Max output: 160 / 2 / 4 = 20 MHz (with minimum dividers)
305+ // Available: 20 MHz (div=2), 13.33 MHz (div=3), 10 MHz (div=4), 8 MHz (div=5), ...
306+ //
307+ // Note: Higher speeds would require clkm_div < 2, which violates TRM constraints
308+ // and causes unreliable operation.
309+ if (requested_hz > 20000000 ) {
310+ ESP_LOGW (TAG, " Requested %u Hz exceeds ESP32-S2 max (20 MHz), using 20 MHz" , (unsigned int ) requested_hz);
311+ return 20000000 ;
312+ }
313+ uint32_t divider = (160000000 + requested_hz * 2 ) / (requested_hz * 4 ); // Round to nearest
314+ return 160000000 / (std::max (divider, uint32_t {2 }) * 4 );
315+
316+ #else
317+ // ESP32: PLL_D2_CLK clock source (80 MHz base)
318+ // Max output: 80 / 2 / 4 = 10 MHz (with minimum dividers)
319+ // Available: 10 MHz (div=2), 6.67 MHz (div=3), 5 MHz (div=4), 4 MHz (div=5), ...
320+ //
321+ // The ESP32's lower base clock (80 MHz vs 160 MHz) combined with the same
322+ // divider constraints results in half the max frequency of ESP32-S2.
323+ if (requested_hz > 10000000 ) {
324+ ESP_LOGW (TAG, " Requested %u Hz exceeds ESP32 max (10 MHz), using 10 MHz" , (unsigned int ) requested_hz);
325+ return 10000000 ;
326+ }
327+ uint32_t divider = (80000000 + requested_hz * 2 ) / (requested_hz * 4 ); // Round to nearest
328+ return 80000000 / (std::max (divider, uint32_t {2 }) * 4 );
329+ #endif
330+ }
331+
288332void I2sDma::configure_i2s_timing () {
289333 auto *dev = i2s_dev_;
290334
@@ -293,6 +337,12 @@ void I2sDma::configure_i2s_timing() {
293337 dev->sample_rate_conf .rx_bits_mod = 16 ; // 16-bit parallel
294338 dev->sample_rate_conf .tx_bits_mod = 16 ;
295339
340+ // Clock already resolved in constructor
341+ uint32_t requested_hz = static_cast <uint32_t >(config_.output_clock_speed );
342+ if (actual_clock_hz_ != requested_hz) {
343+ ESP_LOGI (TAG, " Clock speed %u Hz resolved to %u Hz" , (unsigned int ) requested_hz, (unsigned int ) actual_clock_hz_);
344+ }
345+
296346#if defined(CONFIG_IDF_TARGET_ESP32S2)
297347 // ESP32-S2: PLL_160M clock source
298348 // Output Frequency = 160MHz / clkm_div_num / (tx_bck_div_num * 2)
@@ -302,32 +352,8 @@ void I2sDma::configure_i2s_timing() {
302352 dev->clkm_conf .clkm_div_a = 1 ;
303353 dev->clkm_conf .clkm_div_b = 0 ;
304354
305- unsigned int clkm_div;
306- unsigned int actual_freq;
307- switch (config_.output_clock_speed ) {
308- case Hub75ClockSpeed::HZ_32M:
309- // 32MHz not achievable on ESP32-S2 (max 20MHz), falling back
310- ESP_LOGW (TAG, " 32MHz not achievable on ESP32-S2 (max 20MHz), falling back to 20MHz" );
311- [[fallthrough]];
312- case Hub75ClockSpeed::HZ_20M:
313- clkm_div = 2 ; // 160/2/4 = 20MHz
314- actual_freq = 20 ;
315- break ;
316- case Hub75ClockSpeed::HZ_16M:
317- // 16MHz not achievable exactly with integer dividers, falling back to 10MHz
318- ESP_LOGW (TAG, " 16MHz not achievable on ESP32-S2, falling back to 10MHz" );
319- [[fallthrough]];
320- case Hub75ClockSpeed::HZ_10M:
321- clkm_div = 4 ; // 160/4/4 = 10MHz
322- actual_freq = 10 ;
323- break ;
324- case Hub75ClockSpeed::HZ_8M:
325- clkm_div = 5 ; // 160/5/4 = 8MHz
326- actual_freq = 8 ;
327- break ;
328- default :
329- __builtin_unreachable ();
330- }
355+ // Calculate divider from actual frequency: actual = 160M / (div * 4)
356+ unsigned int clkm_div = 160000000 / (actual_clock_hz_ * 4 );
331357
332358 dev->clkm_conf .clkm_div_num = clkm_div;
333359 dev->clkm_conf .clk_en = 1 ;
@@ -336,67 +362,29 @@ void I2sDma::configure_i2s_timing() {
336362 dev->sample_rate_conf .rx_bck_div_num = 2 ;
337363 dev->sample_rate_conf .tx_bck_div_num = 2 ;
338364
339- // Store actual clock frequency for BCM timing calculations
340- actual_clock_hz_ = actual_freq * 1000000 ;
341-
342- ESP_LOGI (TAG, " ESP32-S2 I2S clock: 160MHz / %u / 4 = %u MHz" , clkm_div, actual_freq);
365+ ESP_LOGI (TAG, " ESP32-S2 I2S clock: 160MHz / %u / 4 = %.2f MHz (requested %u MHz)" , clkm_div,
366+ actual_clock_hz_ / 1000000 .0f , (unsigned int ) (requested_hz / 1000000 ));
343367
344368#else
345369 // ESP32: PLL_D2_CLK clock source (80MHz)
346370 // Output Frequency = 80MHz / clkm_div_num / (tx_bck_div_num * 2)
347371 // Reference: ESP32 TRM v5.3, Section 12.5 (I2S Clock)
348372 // Constraints: clkm_div_num >= 2, tx_bck_div_num >= 2 (TRM Section 12.6)
349- //
350- // NOTE: Maximum achievable frequency is 10MHz with minimum dividers (2, 2).
351- // Higher frequencies (16/20MHz) would require clkm_div_num < 2 which violates
352- // TRM constraints. The TRM states: "I2S_CLKM_DIV_NUM: Integral I2S clock
353- // divider value. fI2S = fCLK / I2S_CLKM_DIV_NUM (I2S_CLKM_DIV_NUM >= 2)"
354373 dev->clkm_conf .clka_en = 0 ; // PLL_D2_CLK (80MHz)
355374 dev->clkm_conf .clkm_div_a = 1 ;
356375 dev->clkm_conf .clkm_div_b = 0 ;
357376
358- unsigned int clkm_div;
359- unsigned int actual_freq;
360- switch (config_.output_clock_speed ) {
361- case Hub75ClockSpeed::HZ_32M:
362- ESP_LOGW (TAG, " 32MHz not achievable on ESP32 (max 10MHz), falling back to 10MHz" );
363- clkm_div = 2 ; // 80/2/4 = 10MHz
364- actual_freq = 10 ;
365- break ;
366- case Hub75ClockSpeed::HZ_20M:
367- ESP_LOGW (TAG, " 20MHz not achievable on ESP32 (max 10MHz), falling back to 10MHz" );
368- clkm_div = 2 ; // 80/2/4 = 10MHz
369- actual_freq = 10 ;
370- break ;
371- case Hub75ClockSpeed::HZ_16M:
372- ESP_LOGW (TAG, " 16MHz not achievable on ESP32 (max 10MHz), falling back to 10MHz" );
373- clkm_div = 2 ; // 80/2/4 = 10MHz
374- actual_freq = 10 ;
375- break ;
376- case Hub75ClockSpeed::HZ_10M:
377- clkm_div = 2 ; // 80/2/4 = 10MHz
378- actual_freq = 10 ;
379- break ;
380- case Hub75ClockSpeed::HZ_8M:
381- // 8MHz not achievable exactly, closest is 5MHz
382- ESP_LOGW (TAG, " 8MHz not achievable on ESP32, falling back to 5MHz" );
383- clkm_div = 4 ; // 80/4/4 = 5MHz
384- actual_freq = 5 ;
385- break ;
386- default :
387- __builtin_unreachable ();
388- }
377+ // Calculate divider from actual frequency: actual = 80M / (div * 4)
378+ unsigned int clkm_div = 80000000 / (actual_clock_hz_ * 4 );
389379
390380 dev->clkm_conf .clkm_div_num = clkm_div;
391381
392382 // BCK divider (must be >= 2 per TRM Section 12.6)
393383 dev->sample_rate_conf .tx_bck_div_num = 2 ;
394384 dev->sample_rate_conf .rx_bck_div_num = 2 ;
395385
396- // Store actual clock frequency for BCM timing calculations
397- actual_clock_hz_ = actual_freq * 1000000 ;
398-
399- ESP_LOGI (TAG, " ESP32 I2S clock: 80MHz / %u / 4 = %u MHz" , clkm_div, actual_freq);
386+ ESP_LOGI (TAG, " ESP32 I2S clock: 80MHz / %u / 4 = %.2f MHz (requested %u MHz)" , clkm_div,
387+ actual_clock_hz_ / 1000000 .0f , (unsigned int ) (requested_hz / 1000000 ));
400388#endif
401389}
402390
0 commit comments