Skip to content

Commit ddb9b77

Browse files
Make clock speed calculations more explicit (#59)
1 parent 8a3e2eb commit ddb9b77

File tree

5 files changed

+183
-36
lines changed

5 files changed

+183
-36
lines changed

components/hub75/Kconfig

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,10 @@ menu "HUB75 RGB LED Matrix Driver"
282282
Higher speeds = faster refresh, but may cause signal integrity
283283
issues on long cables or with some panels.
284284

285-
- 8 MHz: Very conservative
286-
- 10 MHz: Conservative, works with all panels
287-
- 16 MHz: Good balance
288-
- 20 MHz: Recommended default
285+
Platform limits (falls back with warning if exceeded):
286+
- ESP32: 10 MHz max
287+
- ESP32-S2: 20 MHz max
288+
- ESP32-S3/P4/C6: 32 MHz
289289

290290
config HUB75_CLK_8MHZ
291291
bool "8 MHz (Very Conservative)"
@@ -298,6 +298,9 @@ menu "HUB75 RGB LED Matrix Driver"
298298

299299
config HUB75_CLK_20MHZ
300300
bool "20 MHz (Recommended)"
301+
302+
config HUB75_CLK_32MHZ
303+
bool "32 MHz (ESP32-S3/P4/C6 only)"
301304
endchoice
302305

303306
config HUB75_MIN_REFRESH_RATE

components/hub75/include/hub75_types.h

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,18 @@ enum class Hub75ColorOrder {
3333
/**
3434
* @brief Output clock speed options
3535
*
36-
* Valid speeds that divide evenly from 160MHz base clock.
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.
38+
*
39+
* Note: Panel and wiring quality are often the limiting factor. If you see
40+
* visual artifacts or instability, try reducing the clock speed.
3741
*/
3842
enum class Hub75ClockSpeed : uint32_t {
39-
HZ_8M = 8000000, // 8 MHz
40-
HZ_10M = 10000000, // 10 MHz
41-
HZ_16M = 16000000, // 16 MHz
42-
HZ_20M = 20000000, // 20 MHz (default)
43+
HZ_8M = 8000000,
44+
HZ_10M = 10000000,
45+
HZ_16M = 16000000,
46+
HZ_20M = 20000000, // default
47+
HZ_32M = 32000000, // ESP32-S3/P4/C6 only
4348
};
4449

4550
/**

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

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -266,44 +266,110 @@ bool I2sDma::init() {
266266

267267
void I2sDma::configure_i2s_timing() {
268268
auto *dev = i2s_dev_;
269-
uint32_t freq = static_cast<uint32_t>(config_.output_clock_speed);
270269

271270
// Sample rate configuration
272271
dev->sample_rate_conf.val = 0;
273272
dev->sample_rate_conf.rx_bits_mod = 16; // 16-bit parallel
274273
dev->sample_rate_conf.tx_bits_mod = 16;
275274

276275
#if defined(CONFIG_IDF_TARGET_ESP32S2)
277-
// ESP32-S2: Use PLL_160M
276+
// ESP32-S2: PLL_160M clock source
277+
// Output Frequency = 160MHz / clkm_div_num / (tx_bck_div_num * 2)
278+
// Reference: ESP32-S2 TRM v1.5, Section 12.5 (I2S Clock)
279+
// Constraints: clkm_div_num >= 2, tx_bck_div_num >= 2 (TRM Section 12.6)
278280
dev->clkm_conf.clk_sel = 2; // PLL_160M_CLK
279281
dev->clkm_conf.clkm_div_a = 1;
280282
dev->clkm_conf.clkm_div_b = 0;
281283

282-
// Output Frequency = (160MHz / clkm_div_num) / (tx_bck_div_num*2)
283-
unsigned int div_num = (freq > 8000000) ? 2 : 4; // 20MHz or 10MHz
284-
dev->clkm_conf.clkm_div_num = div_num;
284+
unsigned int clkm_div;
285+
unsigned int actual_freq;
286+
switch (config_.output_clock_speed) {
287+
case Hub75ClockSpeed::HZ_32M:
288+
// 32MHz not achievable on ESP32-S2 (max 20MHz), falling back
289+
ESP_LOGW(TAG, "32MHz not achievable on ESP32-S2 (max 20MHz), falling back to 20MHz");
290+
[[fallthrough]];
291+
case Hub75ClockSpeed::HZ_20M:
292+
clkm_div = 2; // 160/2/4 = 20MHz
293+
actual_freq = 20;
294+
break;
295+
case Hub75ClockSpeed::HZ_16M:
296+
// 16MHz not achievable exactly with integer dividers, falling back to 10MHz
297+
ESP_LOGW(TAG, "16MHz not achievable on ESP32-S2, falling back to 10MHz");
298+
[[fallthrough]];
299+
case Hub75ClockSpeed::HZ_10M:
300+
clkm_div = 4; // 160/4/4 = 10MHz
301+
actual_freq = 10;
302+
break;
303+
case Hub75ClockSpeed::HZ_8M:
304+
clkm_div = 5; // 160/5/4 = 8MHz
305+
actual_freq = 8;
306+
break;
307+
default:
308+
__builtin_unreachable();
309+
}
310+
311+
dev->clkm_conf.clkm_div_num = clkm_div;
285312
dev->clkm_conf.clk_en = 1;
286313

287-
// BCK divider (must be >= 2 per TRM)
314+
// BCK divider (must be >= 2 per TRM Section 12.6)
288315
dev->sample_rate_conf.rx_bck_div_num = 2;
289316
dev->sample_rate_conf.tx_bck_div_num = 2;
290317

291-
ESP_LOGI(TAG, "ESP32-S2 I2S clock: 160MHz / %d / 4 = %d MHz", div_num, 160 / div_num / 4);
318+
ESP_LOGI(TAG, "ESP32-S2 I2S clock: 160MHz / %u / 4 = %u MHz", clkm_div, actual_freq);
319+
292320
#else
293-
// ESP32: Use PLL_D2 (80MHz)
294-
dev->clkm_conf.clka_en = 0; // Use PLL_D2_CLK (80MHz)
295-
dev->clkm_conf.clkm_div_a = 1; // Denominator
296-
dev->clkm_conf.clkm_div_b = 0; // Numerator
321+
// ESP32: PLL_D2_CLK clock source (80MHz)
322+
// Output Frequency = 80MHz / clkm_div_num / (tx_bck_div_num * 2)
323+
// Reference: ESP32 TRM v5.3, Section 12.5 (I2S Clock)
324+
// Constraints: clkm_div_num >= 2, tx_bck_div_num >= 2 (TRM Section 12.6)
325+
//
326+
// NOTE: Maximum achievable frequency is 10MHz with minimum dividers (2, 2).
327+
// Higher frequencies (16/20MHz) would require clkm_div_num < 2 which violates
328+
// TRM constraints. The TRM states: "I2S_CLKM_DIV_NUM: Integral I2S clock
329+
// divider value. fI2S = fCLK / I2S_CLKM_DIV_NUM (I2S_CLKM_DIV_NUM >= 2)"
330+
dev->clkm_conf.clka_en = 0; // PLL_D2_CLK (80MHz)
331+
dev->clkm_conf.clkm_div_a = 1;
332+
dev->clkm_conf.clkm_div_b = 0;
333+
334+
unsigned int clkm_div;
335+
unsigned int actual_freq;
336+
switch (config_.output_clock_speed) {
337+
case Hub75ClockSpeed::HZ_32M:
338+
ESP_LOGW(TAG, "32MHz not achievable on ESP32 (max 10MHz), falling back to 10MHz");
339+
clkm_div = 2; // 80/2/4 = 10MHz
340+
actual_freq = 10;
341+
break;
342+
case Hub75ClockSpeed::HZ_20M:
343+
ESP_LOGW(TAG, "20MHz not achievable on ESP32 (max 10MHz), falling back to 10MHz");
344+
clkm_div = 2; // 80/2/4 = 10MHz
345+
actual_freq = 10;
346+
break;
347+
case Hub75ClockSpeed::HZ_16M:
348+
ESP_LOGW(TAG, "16MHz not achievable on ESP32 (max 10MHz), falling back to 10MHz");
349+
clkm_div = 2; // 80/2/4 = 10MHz
350+
actual_freq = 10;
351+
break;
352+
case Hub75ClockSpeed::HZ_10M:
353+
clkm_div = 2; // 80/2/4 = 10MHz
354+
actual_freq = 10;
355+
break;
356+
case Hub75ClockSpeed::HZ_8M:
357+
// 8MHz not achievable exactly, closest is 5MHz
358+
ESP_LOGW(TAG, "8MHz not achievable on ESP32, falling back to 5MHz");
359+
clkm_div = 4; // 80/4/4 = 5MHz
360+
actual_freq = 5;
361+
break;
362+
default:
363+
__builtin_unreachable();
364+
}
297365

298-
// Calculate divider: 80MHz / clkm_div_num / tx_bck_div_num
299-
unsigned int div_num = (freq > 8000000) ? 2 : 4; // 20MHz or 10MHz
300-
dev->clkm_conf.clkm_div_num = div_num;
366+
dev->clkm_conf.clkm_div_num = clkm_div;
301367

302-
// BCK divider (must be >= 2 per TRM)
368+
// BCK divider (must be >= 2 per TRM Section 12.6)
303369
dev->sample_rate_conf.tx_bck_div_num = 2;
304370
dev->sample_rate_conf.rx_bck_div_num = 2;
305371

306-
ESP_LOGI(TAG, "ESP32 I2S clock: 80MHz / %d / 4 = %d MHz", div_num, 80 / div_num / 4);
372+
ESP_LOGI(TAG, "ESP32 I2S clock: 80MHz / %u / 4 = %u MHz", clkm_div, actual_freq);
307373
#endif
308374
}
309375

docs/PLATFORMS.md

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,87 @@ Detailed comparison and implementation notes for ESP32 platform variants.
44

55
## Platform Comparison Table
66

7-
| Feature | ESP32 / ESP32-S2 | ESP32-S3 | ESP32-P4 | ESP32-C6 |
8-
|---------|------------------|----------|----------|----------|
9-
| **Peripheral** | I2S (LCD mode) | LCD_CAM | PARLIO | PARLIO |
10-
| **DMA Engine** | I2S DMA | GDMA (AHB) | EDMA | EDMA |
11-
| **Memory** | Internal SRAM | Internal SRAM | **PSRAM** | Internal SRAM |
12-
| **Buffer Size** (64×64) | ~57 KB | ~57 KB | ~284 KB | ~284 KB |
13-
| **BCM Method** | Descriptor dup | Descriptor dup | Buffer padding | Buffer padding |
14-
| **Clock Gating** | No | No | **Yes** (MSB) | **No** |
15-
| **Max Clock** | 20 MHz | 40 MHz | 40 MHz+ | 40 MHz+ |
16-
| **Status** | ✅ Tested | ✅ Tested | ✅ Tested | ⏳ Planned |
7+
| Feature | ESP32 | ESP32-S2 | ESP32-S3 | ESP32-P4 | ESP32-C6 |
8+
|---------|-------|----------|----------|----------|----------|
9+
| **Peripheral** | I2S (LCD mode) | I2S (LCD mode) | LCD_CAM | PARLIO | PARLIO |
10+
| **DMA Engine** | I2S DMA | I2S DMA | GDMA (AHB) | EDMA | EDMA |
11+
| **Memory** | Internal SRAM | Internal SRAM | Internal SRAM | **PSRAM** | Internal SRAM |
12+
| **Buffer Size** (64×64) | ~57 KB | ~57 KB | ~57 KB | ~284 KB | ~284 KB |
13+
| **BCM Method** | Descriptor dup | Descriptor dup | Descriptor dup | Buffer padding | Buffer padding |
14+
| **Clock Gating** | No | No | No | **Yes** (MSB) | **No** |
15+
| **Max Clock** | 10 MHz | 20 MHz | 40 MHz | 40 MHz+ | 40 MHz+ |
16+
| **Status** | ✅ Tested | ✅ Tested | ✅ Tested | ✅ Tested | ⏳ Planned |
17+
18+
---
19+
20+
## Clock Speed Reference
21+
22+
The achievable clock speeds depend on platform hardware constraints. The I2S peripheral
23+
on ESP32/ESP32-S2 has different clock divider limitations than the LCD_CAM and PARLIO
24+
peripherals on newer chips.
25+
26+
### Clock Speed by Platform
27+
28+
| Requested | ESP32 | ESP32-S2 | ESP32-S3 | ESP32-P4/C6 |
29+
|-----------|-------|----------|----------|-------------|
30+
| **32 MHz** | ⚠️ 10 MHz | ⚠️ 20 MHz | ✅ 32 MHz | ✅ 32 MHz |
31+
| **20 MHz** | ⚠️ 10 MHz | ✅ 20 MHz | ✅ 20 MHz | ✅ 20 MHz |
32+
| **16 MHz** | ⚠️ 10 MHz | ⚠️ 10 MHz | ✅ 16 MHz | ✅ 16 MHz |
33+
| **10 MHz** | ✅ 10 MHz | ✅ 10 MHz | ✅ 10 MHz | ✅ 10 MHz |
34+
| **8 MHz** | ⚠️ 5 MHz | ✅ 8 MHz | ✅ 8 MHz | ✅ 8 MHz |
35+
36+
⚠️ = Falls back to nearest achievable frequency (warning logged at runtime)
37+
38+
### ESP32 Clock Limitations
39+
40+
The ESP32 uses PLL_D2_CLK (80 MHz) as the I2S clock source. The output frequency is:
41+
42+
```
43+
Output = 80 MHz / clkm_div_num / (tx_bck_div_num × 2)
44+
```
45+
46+
Per the ESP32 TRM (Section 12.6), both `clkm_div_num` and `tx_bck_div_num` must be ≥ 2.
47+
With minimum dividers (2, 2), the maximum achievable frequency is:
48+
49+
```
50+
80 MHz / 2 / 4 = 10 MHz (maximum)
51+
```
52+
53+
Higher frequencies (16/20 MHz) would require `clkm_div_num < 2`, which violates TRM
54+
constraints and produces undefined behavior.
55+
56+
### ESP32-S2 Clock Configuration
57+
58+
The ESP32-S2 uses PLL_160M (160 MHz) as the I2S clock source:
59+
60+
```
61+
Output = 160 MHz / clkm_div_num / (tx_bck_div_num × 2)
62+
```
63+
64+
With the same divider constraints, more frequencies are achievable:
65+
66+
| Speed | clkm_div | Formula | Result |
67+
|-------|----------|---------|--------|
68+
| 20 MHz | 2 | 160/2/4 | 20 MHz ✓ |
69+
| 16 MHz || Not achievable with integer dividers | Falls back to 10 MHz |
70+
| 10 MHz | 4 | 160/4/4 | 10 MHz ✓ |
71+
| 8 MHz | 5 | 160/5/4 | 8 MHz ✓ |
72+
73+
### ESP32-S3 / ESP32-P4 / ESP32-C6
74+
75+
These platforms use LCD_CAM or PARLIO peripherals with simpler clock dividers:
76+
77+
```
78+
Output = 160 MHz / div_num
79+
```
80+
81+
All standard frequencies (8/10/16/20/32 MHz) are achievable with integer dividers.
82+
83+
### References
84+
85+
- ESP32 TRM v5.3, Section 12.5-12.6 (I2S Clock)
86+
- ESP32-S2 TRM v1.5, Section 12.5-12.6 (I2S Clock)
87+
- ESP32-S3 TRM, Chapter 26 (LCD_CAM)
1788

1889
---
1990

@@ -66,7 +137,7 @@ Detailed comparison and implementation notes for ESP32 platform variants.
66137
### Limitations
67138

68139
- **I2S peripheral misuse**: Designed for audio, not parallel data
69-
- **20 MHz max clock**: Higher speeds may be unstable
140+
- **Clock speed limits**: ESP32 max 10 MHz, ESP32-S2 max 20 MHz (see [Clock Speed Reference](#clock-speed-reference))
70141
- **No PSRAM support**: All buffers must be internal SRAM
71142

72143
### Advantages

examples/common/board_config.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ static inline Hub75Config getMenuConfigSettings() {
205205
config.output_clock_speed = Hub75ClockSpeed::HZ_16M;
206206
#elif defined(CONFIG_HUB75_CLK_20MHZ)
207207
config.output_clock_speed = Hub75ClockSpeed::HZ_20M;
208+
#elif defined(CONFIG_HUB75_CLK_32MHZ)
209+
config.output_clock_speed = Hub75ClockSpeed::HZ_32M;
208210
#endif
209211

210212
// Performance settings

0 commit comments

Comments
 (0)