Skip to content

Commit d19154f

Browse files
Add intermediate clock speeds: 18, 23, 27 MHz (#79)
1 parent 36e9aab commit d19154f

File tree

12 files changed

+215
-109
lines changed

12 files changed

+215
-109
lines changed

components/hub75/Kconfig

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,19 +318,28 @@ menu "HUB75 RGB LED Matrix Driver"
318318
- ESP32-S3/P4/C6: 32 MHz
319319

320320
config HUB75_CLK_8MHZ
321-
bool "8 MHz (Very Conservative)"
321+
bool "8 MHz"
322322

323323
config HUB75_CLK_10MHZ
324-
bool "10 MHz (Conservative)"
324+
bool "10 MHz"
325325

326326
config HUB75_CLK_16MHZ
327-
bool "16 MHz (Good Balance)"
327+
bool "16 MHz"
328+
329+
config HUB75_CLK_18MHZ
330+
bool "18 MHz"
328331

329332
config HUB75_CLK_20MHZ
330333
bool "20 MHz (Recommended)"
331334

335+
config HUB75_CLK_23MHZ
336+
bool "23 MHz"
337+
338+
config HUB75_CLK_27MHZ
339+
bool "27 MHz"
340+
332341
config HUB75_CLK_32MHZ
333-
bool "32 MHz (ESP32-S3/P4/C6 only)"
342+
bool "32 MHz"
334343
endchoice
335344

336345
config HUB75_MIN_REFRESH_RATE

components/hub75/include/hub75_types.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ enum class Hub75ColorOrder {
3333
/**
3434
* @brief Output clock speed options
3535
*
36-
* Platform limits: ESP32 max 10MHz, ESP32-S2 max 20MHz, ESP32-S3/P4/C6 max 32MHz.
37-
* Unsupported speeds fall back to platform max with a warning.
36+
* Requested speeds are resolved to the nearest achievable frequency based on
37+
* hardware clock divider constraints. See docs/PLATFORMS.md for details on
38+
* actual frequencies and platform limitations.
3839
*
3940
* Note: Panel and wiring quality are often the limiting factor. If you see
4041
* visual artifacts or instability, try reducing the clock speed.
@@ -43,8 +44,11 @@ enum class Hub75ClockSpeed : uint32_t {
4344
HZ_8M = 8000000,
4445
HZ_10M = 10000000,
4546
HZ_16M = 16000000,
47+
HZ_18M = 18000000,
4648
HZ_20M = 20000000, // default
47-
HZ_32M = 32000000, // ESP32-S3/P4/C6 only
49+
HZ_23M = 23000000,
50+
HZ_27M = 27000000,
51+
HZ_32M = 32000000,
4852
};
4953

5054
/**

components/hub75/src/platforms/gdma/gdma_dma.cpp

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ GdmaDma::GdmaDma(const Hub75Config &config)
7979
dma_chan_(nullptr),
8080
bit_depth_(HUB75_BIT_DEPTH),
8181
lsbMsbTransitionBit_(0),
82+
actual_clock_hz_(resolve_actual_clock_speed(config.output_clock_speed)),
8283
panel_width_(config.panel_width),
8384
panel_height_(config.panel_height),
8485
layout_rows_(config.layout_rows),
@@ -221,7 +222,8 @@ bool GdmaDma::init() {
221222
is_four_scan_wiring(scan_wiring_) ? "yes" : "no");
222223

223224
ESP_LOGI(TAG, "LCD_CAM + GDMA initialized successfully");
224-
ESP_LOGI(TAG, "Clock: %u MHz", (unsigned int) (static_cast<uint32_t>(config_.output_clock_speed) / 1000000));
225+
ESP_LOGI(TAG, "Clock: %.2f MHz (requested %u MHz)", actual_clock_hz_ / 1000000.0f,
226+
(unsigned int) (static_cast<uint32_t>(config_.output_clock_speed) / 1000000));
225227

226228
// Calculate BCM timing (determines lsbMsbTransitionBit for OE control)
227229
calculate_bcm_timings();
@@ -264,11 +266,37 @@ bool GdmaDma::init() {
264266
return true;
265267
}
266268

269+
HUB75_CONST uint32_t GdmaDma::resolve_actual_clock_speed(Hub75ClockSpeed clock_speed) const {
270+
// ESP32-S3 LCD_CAM clock derivation:
271+
// Output = PLL_F160M / lcd_clkm_div_num
272+
// Constraint: lcd_clkm_div_num >= 2
273+
//
274+
// We use integer dividers only - no fractional dividers. Fractional dividers
275+
// cause clock jitter because the hardware alternates between two integer
276+
// dividers to approximate the fractional value. With pure integer division
277+
// from the stable 160 MHz PLL, every clock cycle is identical.
278+
//
279+
// The resulting frequencies may not be round numbers (e.g., 160/7 = 22.86 MHz),
280+
// but this is fine - what matters for signal integrity is that each clock
281+
// period is exactly the same, not that the frequency is a nice decimal.
282+
//
283+
// Available speeds: 32 MHz (div=5), 26.67 MHz (div=6), 22.86 MHz (div=7),
284+
// 20 MHz (div=8), 17.78 MHz (div=9), 16 MHz (div=10), ...
285+
uint32_t requested_hz = static_cast<uint32_t>(clock_speed);
286+
uint32_t divider = (160000000 + requested_hz / 2) / requested_hz; // Round to nearest
287+
return 160000000 / std::max(divider, uint32_t{2});
288+
}
289+
267290
void GdmaDma::configure_lcd_clock() {
268291
// Configure LCD clock from PLL_F160M (160 MHz)
269-
// Calculate divider: 160MHz / desired_speed
270-
uint32_t div_num = 160000000 / static_cast<uint32_t>(config_.output_clock_speed);
271-
div_num = std::max(div_num, uint32_t{2}); // Minimum divider
292+
// actual_clock_hz_ already resolved in constructor
293+
uint32_t requested_hz = static_cast<uint32_t>(config_.output_clock_speed);
294+
uint32_t div_num = 160000000 / actual_clock_hz_;
295+
296+
if (actual_clock_hz_ != requested_hz) {
297+
ESP_LOGI(TAG, "Clock speed %u Hz rounded to %u Hz (160MHz / %u)", (unsigned int) requested_hz,
298+
(unsigned int) actual_clock_hz_, (unsigned int) div_num);
299+
}
272300

273301
LCD_CAM.lcd_clock.lcd_clk_sel = 3; // PLL_F160M_CLK (value 3, not 2!)
274302
LCD_CAM.lcd_clock.lcd_ck_out_edge = 0; // PCLK low in 1st half cycle
@@ -279,8 +307,7 @@ void GdmaDma::configure_lcd_clock() {
279307
LCD_CAM.lcd_clock.lcd_clkm_div_a = 1; // Fractional divider (0/1)
280308
LCD_CAM.lcd_clock.lcd_clkm_div_b = 0;
281309

282-
ESP_LOGI(TAG, "LCD clock: PLL_F160M / %u = %u MHz", (unsigned int) div_num,
283-
(unsigned int) (160000000 / div_num / 1000000));
310+
ESP_LOGI(TAG, "LCD clock: PLL_F160M / %u = %.2f MHz", (unsigned int) div_num, actual_clock_hz_ / 1000000.0f);
284311
}
285312

286313
void GdmaDma::configure_lcd_mode() {
@@ -1204,10 +1231,10 @@ void GdmaDma::calculate_bcm_timings() {
12041231
// Buffer contains dma_width_ pixels with LAT on last pixel
12051232
// Latch blanking is handled via OE bits, not extra pixels
12061233
const uint16_t buffer_pixels = dma_width_; // LAT is on last pixel, not extra
1207-
const float buffer_time_us = (buffer_pixels * 1000000.0f) / static_cast<uint32_t>(config_.output_clock_speed);
1234+
const float buffer_time_us = (buffer_pixels * 1000000.0f) / actual_clock_hz_;
12081235

12091236
ESP_LOGI(TAG, "Buffer transmission time: %.2f µs (%u pixels @ %lu Hz)", buffer_time_us, (unsigned) buffer_pixels,
1210-
(unsigned long) static_cast<uint32_t>(config_.output_clock_speed));
1237+
(unsigned long) actual_clock_hz_);
12111238

12121239
// Target refresh rate from config
12131240
const uint32_t target_hz = config_.min_refresh_rate;

components/hub75/src/platforms/gdma/gdma_dma.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ class GdmaDma : public PlatformDma {
6666
*/
6767
void set_rotation(Hub75Rotation rotation) override;
6868

69+
/**
70+
* @brief Resolve clock speed to achievable frequency (160 MHz / N)
71+
*/
72+
HUB75_CONST uint32_t resolve_actual_clock_speed(Hub75ClockSpeed clock_speed) const override;
73+
6974
// ============================================================================
7075
// Pixel API (Direct DMA Buffer Writes)
7176
// ============================================================================
@@ -126,8 +131,9 @@ class GdmaDma : public PlatformDma {
126131
void calculate_bcm_timings();
127132

128133
gdma_channel_handle_t dma_chan_;
129-
const uint8_t bit_depth_; // Bit depth from config (6, 7, 8, 10, or 12)
130-
uint8_t lsbMsbTransitionBit_; // BCM optimization threshold (calculated at init)
134+
const uint8_t bit_depth_; // Bit depth from config (6, 7, 8, 10, or 12)
135+
uint8_t lsbMsbTransitionBit_; // BCM optimization threshold (calculated at init)
136+
const uint32_t actual_clock_hz_; // Actual achieved clock frequency after rounding
131137

132138
// Panel configuration (immutable, cached from config)
133139
const uint16_t panel_width_;

components/hub75/src/platforms/i2s/i2s_dma.cpp

Lines changed: 59 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
288332
void 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

components/hub75/src/platforms/i2s/i2s_dma.h

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ class I2sDma : public PlatformDma {
6565
*/
6666
void set_rotation(Hub75Rotation rotation) override;
6767

68+
/**
69+
* @brief Resolve clock speed to achievable frequency (I2S constraints)
70+
*
71+
* ESP32: max 10 MHz (80 MHz / 2 / 4)
72+
* ESP32-S2: max 20 MHz (160 MHz / 2 / 4)
73+
*/
74+
HUB75_CONST uint32_t resolve_actual_clock_speed(Hub75ClockSpeed clock_speed) const override;
75+
6876
// ============================================================================
6977
// Pixel API (Direct DMA Buffer Writes)
7078
// ============================================================================
@@ -122,9 +130,9 @@ class I2sDma : public PlatformDma {
122130
void calculate_bcm_timings();
123131

124132
volatile i2s_dev_t *i2s_dev_;
125-
const uint8_t bit_depth_; // Bit depth from config (6, 7, 8, 10, or 12)
126-
uint8_t lsbMsbTransitionBit_; // BCM optimization threshold (calculated at init)
127-
uint32_t actual_clock_hz_; // Actual I2S clock frequency (may differ from config due to fallbacks)
133+
const uint8_t bit_depth_; // Bit depth from config (6, 7, 8, 10, or 12)
134+
uint8_t lsbMsbTransitionBit_; // BCM optimization threshold (calculated at init)
135+
const uint32_t actual_clock_hz_; // Actual I2S clock frequency (may differ from config due to fallbacks)
128136

129137
// Panel configuration (immutable, cached from config)
130138
const uint16_t panel_width_;

0 commit comments

Comments
 (0)