Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions components/hub75/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,33 @@ menu "HUB75 RGB LED Matrix Driver"
bool "DP3246"
endchoice

choice HUB75_ROW_DECODER
prompt "Row Decoder"
default HUB75_ROW_DECODER_BINARY
help
Row selection / row addressing mode used by the panel.

Most HUB75 panels use standard binary ABCDE row addressing.
Some panels use SM5368-style row logic where A/B/C are used as
row clock, BK, and row data instead of binary row address bits.

Note: SM5368 row decoding is currently implemented only on the
ESP32-S3 GDMA backend.

config HUB75_ROW_DECODER_BINARY
bool "Binary ABCDE (Most Panels)"
help
Standard HUB75 row addressing using A/B/C/D/E as a binary
row number.

config HUB75_ROW_DECODER_SM5368
bool "SM5368 Shift Row Decoder"
help
SM5368-style row selection where A=row clock, B=BK, and
C=row data. Use this for panels that require shift-style
row selection instead of binary row addressing.
endchoice

choice HUB75_BIT_DEPTH_CHOICE
prompt "Bit Depth"
# Backward compatibility: honor deprecated DEPTH_* option names
Expand Down
17 changes: 17 additions & 0 deletions components/hub75/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ void app_main() {
config.panel_height = 64;
config.scan_wiring = Hub75ScanWiring::STANDARD_TWO_SCAN; // Most panels
config.shift_driver = Hub75ShiftDriver::FM6126A; // Or GENERIC
config.row_decoder = Hub75RowDecoder::BINARY; // Default row addressing

// Set GPIO pins
config.pins.r1 = 25;
Expand Down Expand Up @@ -114,6 +115,12 @@ void app_main() {
- `bool begin()` - Initialize hardware and start continuous refresh loop
- `void end()` - Stop refresh and cleanup resources

**Row Decoder Modes:**
- `Hub75RowDecoder::BINARY` - Standard HUB75 row addressing using A/B/C/D/E as a binary row number
- `Hub75RowDecoder::SM5368` - SM5368-style row selection using A=row clock, B=BK, C=row data

`SM5368` is currently implemented on the ESP32-S3 GDMA backend only.

### Drawing

- `void draw_pixels(x, y, w, h, buffer, format, color_order, big_endian)` - Bulk pixel write from buffer (most efficient for images/sprites)
Expand Down Expand Up @@ -196,6 +203,15 @@ config.panel_height = 64;
config.shift_driver = Hub75ShiftDriver::FM6126A; // Try this if GENERIC doesn't work
```

**SM5368 row decoder panel (ESP32-S3 GDMA):**
```cpp
Hub75Config config{};
config.panel_width = 64;
config.panel_height = 32;
config.scan_wiring = Hub75ScanWiring::SCAN_1_8_32PX_FULL;
config.row_decoder = Hub75RowDecoder::SM5368;
```

**Two panels side-by-side:**
```cpp
config.layout_cols = 2; // Two panels horizontally
Expand Down Expand Up @@ -260,6 +276,7 @@ Supports ESP32, ESP32-S2, ESP32-S3, and ESP32-P4 with platform-specific optimiza
- **Black screen** → Try `shift_driver = Hub75ShiftDriver::FM6126A` (most modern panels)
- **Wrong colors** → Check R1/G1/B1/R2/G2/B2 pin mapping
- **Scrambled display** → Try `FOUR_SCAN_32PX_HIGH` scan wiring for 32px panels with 1/8 scan
- **Rows shift/jump incorrectly** → If panel uses SM5368-style row logic, try `row_decoder = Hub75RowDecoder::SM5368` on ESP32-S3 GDMA

**For complete debugging guide** (ghosting, flickering, multi-panel issues, platform-specific problems, error messages), see **[Troubleshooting Guide](../../docs/TROUBLESHOOTING.md)**.

Expand Down
17 changes: 17 additions & 0 deletions components/hub75/include/hub75_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ enum class Hub75ShiftDriver {
DP3246 // DP3246 (special timing requirements)
};

/**
* @brief Row decoder / row addressing mode
*
* This controls how the panel row-select pins are interpreted by the backend.
* Most HUB75 panels use binary ABCDE row addressing.
* Some panels use SM5368-style shift row selection where A/B/C act as
* row clock, BK, and row data respectively.
*/
enum class Hub75RowDecoder {
BINARY, // Standard ABCDE binary row addressing (default)
SM5368, // A=row_clk, B=BK, C=row_data
};

/**
* @brief Multi-panel physical layout/wiring pattern
*
Expand Down Expand Up @@ -171,6 +184,9 @@ struct Hub75Config {
// Shift driver chip type (determines initialization sequence)
Hub75ShiftDriver shift_driver = Hub75ShiftDriver::GENERIC;

// Row decoder / row addressing mode
Hub75RowDecoder row_decoder = Hub75RowDecoder::BINARY;

// ========================================
// Multi-Panel Physical Layout
// ========================================
Expand Down Expand Up @@ -231,6 +247,7 @@ struct Hub75Config {

using ScanPattern [[deprecated("Use Hub75ScanWiring instead")]] = Hub75ScanWiring;
using ShiftDriver [[deprecated("Use Hub75ShiftDriver instead")]] = Hub75ShiftDriver;
using RowDecoder [[deprecated("Use Hub75RowDecoder instead")]] = Hub75RowDecoder;
using PanelLayout [[deprecated("Use Hub75PanelLayout instead")]] = Hub75PanelLayout;

#ifdef __cplusplus
Expand Down
41 changes: 30 additions & 11 deletions components/hub75/src/platforms/gdma/gdma_dma.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ enum HUB75WordBits : uint16_t {
// Address field (not individual bits)
constexpr int ADDR_SHIFT = 6;
constexpr uint16_t ADDR_MASK = 0x1F; // 5-bit address (0-31)
constexpr uint16_t SM5368_CLK_MASK = 1u << (ADDR_SHIFT + 0);
constexpr uint16_t SM5368_BK_MASK = 1u << (ADDR_SHIFT + 1);
constexpr uint16_t SM5368_DATA_MASK = 1u << (ADDR_SHIFT + 2);

// Combined RGB masks
constexpr uint16_t RGB_UPPER_MASK = (1 << R1_BIT) | (1 << G1_BIT) | (1 << B1_BIT);
Expand Down Expand Up @@ -217,6 +220,7 @@ bool GdmaDma::init() {
layout_rows_, virtual_width_, virtual_height_);
ESP_LOGI(TAG, "DMA config: %dx%d (width x rows), four-scan: %s", dma_width_, num_rows_,
is_four_scan_wiring(scan_wiring_) ? "yes" : "no");
ESP_LOGI(TAG, "Row decoder: %s", config_.row_decoder == Hub75RowDecoder::SM5368 ? "SM5368" : "binary");

ESP_LOGI(TAG, "LCD_CAM + GDMA initialized successfully");
ESP_LOGI(TAG, "Clock: %.2f MHz (requested %u MHz)", actual_clock_hz_ / 1000000.0f,
Expand Down Expand Up @@ -863,7 +867,8 @@ void GdmaDma::initialize_buffer_internal(RowBitPlaneBuffer *buffers) {
}

for (int row = 0; row < num_rows_; row++) {
uint16_t row_addr = row & ADDR_MASK;
const uint16_t row_addr = row & ADDR_MASK;
const bool is_sm5368 = config_.row_decoder == Hub75RowDecoder::SM5368;

for (int bit = 0; bit < bit_depth_; bit++) {
uint16_t *buf = (uint16_t *) (buffers[row].data + (bit * dma_width_ * 2));
Expand All @@ -889,20 +894,34 @@ void GdmaDma::initialize_buffer_internal(RowBitPlaneBuffer *buffers) {
// This prevents corruption when transitioning from row 31 (last) to row 0 (first).
// Without wrap-around, the address would change from 31→0 during row 31's LAT settling,
// causing ghosting on row 0.
uint16_t addr_for_buffer;
if (bit == 0) {
// LSB bit plane uses previous row (wraps row 0 to last row)
addr_for_buffer = ((row == 0 ? num_rows_ : row) - 1) & ADDR_MASK;
ESP_LOGD(TAG, "Row %d Bit 0: Using previous row address 0x%02X (current: 0x%02X)", row, addr_for_buffer,
row_addr);
} else {
// All other bit planes use current row
addr_for_buffer = row_addr;
uint16_t addr_for_buffer = row_addr;
if (!is_sm5368) {
if (bit == 0) {
// LSB bit plane uses previous row (wraps row 0 to last row)
addr_for_buffer = ((row == 0 ? num_rows_ : row) - 1) & ADDR_MASK;
ESP_LOGD(TAG, "Row %d Bit 0: Using previous row address 0x%02X (current: 0x%02X)", row, addr_for_buffer,
row_addr);
} else {
// All other bit planes use current row
addr_for_buffer = row_addr;
}
}

// Fill all pixels with control bits (RGB=0, row address, OE=HIGH)
for (uint16_t x = 0; x < dma_width_; x++) {
buf[x] = (addr_for_buffer << ADDR_SHIFT) | (1 << OE_BIT);
buf[x] = (1 << OE_BIT);
if (!is_sm5368) {
buf[x] |= (addr_for_buffer << ADDR_SHIFT);
}
}

// SM5368 panels use shift-style row selection instead of binary ABCDE address lines.
// Encode the row pulse at the tail of bit-plane 0 so the panel advances row selection
// during the LAT settling window while leaving other bit planes unchanged.
if (is_sm5368 && bit == 0 && dma_width_ >= 2) {
const uint16_t row_data = (row == 0) ? SM5368_DATA_MASK : 0;
buf[dma_width_ - 2] |= row_data | SM5368_BK_MASK;
buf[dma_width_ - 1] |= row_data | SM5368_BK_MASK | SM5368_CLK_MASK;
}

// Set LAT bit on last pixel
Expand Down
5 changes: 5 additions & 0 deletions components/hub75/src/platforms/i2s/i2s_dma.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ bool I2sDma::init() {
ESP_LOGI(TAG, "Pin config: A=%d B=%d C=%d D=%d E=%d LAT=%d OE=%d CLK=%d", config_.pins.a, config_.pins.b,
config_.pins.c, config_.pins.d, config_.pins.e, config_.pins.lat, config_.pins.oe, config_.pins.clk);

if (config_.row_decoder != Hub75RowDecoder::BINARY) {
ESP_LOGE(TAG, "Row decoder mode is not supported on I2S backend yet");
return false;
}

// Get I2S device
#if defined(CONFIG_IDF_TARGET_ESP32S2)
i2s_dev_ = &I2S0;
Expand Down
5 changes: 5 additions & 0 deletions components/hub75/src/platforms/parlio/parlio_dma.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ bool ParlioDma::init() {
is_four_scan_wiring(scan_wiring_) ? "yes" : "no");
ESP_LOGI(TAG, "Bit depth: %d", bit_depth_);

if (config_.row_decoder != Hub75RowDecoder::BINARY) {
ESP_LOGE(TAG, "Row decoder mode is not supported on PARLIO backend yet");
return false;
}

// Calculate BCM timings first
calculate_bcm_timings();

Expand Down
9 changes: 9 additions & 0 deletions examples/common/board_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ static inline Hub75Config getMenuConfigSettings() {
config.scan_wiring = Hub75ScanWiring::SCAN_1_4_16PX_HIGH;
#elif defined(CONFIG_HUB75_WIRING_SCAN_1_8_32PX)
config.scan_wiring = Hub75ScanWiring::SCAN_1_8_32PX_HIGH;
#elif defined(CONFIG_HUB75_WIRING_SCAN_1_8_32PX_FULL)
config.scan_wiring = Hub75ScanWiring::SCAN_1_8_32PX_FULL;
#elif defined(CONFIG_HUB75_WIRING_SCAN_1_8_40PX)
config.scan_wiring = Hub75ScanWiring::SCAN_1_8_40PX_HIGH;
#elif defined(CONFIG_HUB75_WIRING_SCAN_1_8_64PX)
Expand All @@ -47,6 +49,13 @@ static inline Hub75Config getMenuConfigSettings() {
config.shift_driver = Hub75ShiftDriver::DP3246;
#endif

// Row decoder
#if defined(CONFIG_HUB75_ROW_DECODER_SM5368)
config.row_decoder = Hub75RowDecoder::SM5368;
#else
config.row_decoder = Hub75RowDecoder::BINARY;
#endif

// Pin configuration (board preset or custom)
#if defined(CONFIG_HUB75_BOARD_ADAFRUIT_MATRIX_PORTAL_S3)
// Adafruit Matrix Portal S3
Expand Down