Merge pull request #3777 from netmindz/HUB75-AC

Add HUB75 support
This commit is contained in:
Will Tatam
2025-09-20 13:05:48 +01:00
committed by GitHub
14 changed files with 615 additions and 16 deletions

View File

@@ -32,6 +32,36 @@ extern char cmDNS[];
extern bool cctICused;
extern bool useParallelI2S;
// functions to get/set bits in an array - based on functions created by Brandon for GOL
// toDo : make this a class that's completely defined in a header file
bool getBitFromArray(const uint8_t* byteArray, size_t position) { // get bit value
size_t byteIndex = position / 8;
unsigned bitIndex = position % 8;
uint8_t byteValue = byteArray[byteIndex];
return (byteValue >> bitIndex) & 1;
}
void setBitInArray(uint8_t* byteArray, size_t position, bool value) { // set bit - with error handling for nullptr
//if (byteArray == nullptr) return;
size_t byteIndex = position / 8;
unsigned bitIndex = position % 8;
if (value)
byteArray[byteIndex] |= (1 << bitIndex);
else
byteArray[byteIndex] &= ~(1 << bitIndex);
}
size_t getBitArrayBytes(size_t num_bits) { // number of bytes needed for an array with num_bits bits
return (num_bits + 7) / 8;
}
void setBitArray(uint8_t* byteArray, size_t numBits, bool value) { // set all bits to same value
if (byteArray == nullptr) return;
size_t len = getBitArrayBytes(numBits);
if (value) memset(byteArray, 0xFF, len);
else memset(byteArray, 0x00, len);
}
//colors.cpp
uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb);
@@ -754,6 +784,352 @@ void BusNetwork::cleanup() {
_valid = false;
}
// ***************************************************************************
#ifdef WLED_ENABLE_HUB75MATRIX
#warning "HUB75 driver enabled (experimental)"
#ifdef ESP8266
#error ESP8266 does not support HUB75
#endif
BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) {
size_t lastHeap = ESP.getFreeHeap();
_valid = false;
_hasRgb = true;
_hasWhite = false;
mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer
// mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver
// mxconfig.driver = HUB75_I2S_CFG::FM6124; // try this driver in case you panel stays dark, or when colors look too pastel
// mxconfig.latch_blanking = 3;
// mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // experimental - 5MHZ should be enugh, but colours looks slightly better at 10MHz
//mxconfig.min_refresh_rate = 90;
//mxconfig.min_refresh_rate = 120;
mxconfig.clkphase = bc.reversed;
virtualDisp = nullptr;
if (bc.type == TYPE_HUB75MATRIX_HS) {
mxconfig.mx_width = min((u_int8_t) 64, bc.pins[0]);
mxconfig.mx_height = min((u_int8_t) 64, bc.pins[1]);
if(bc.pins[2] > 1 && bc.pins[3] > 0 && bc.pins[4]) {
virtualDisp = new VirtualMatrixPanel((*display), bc.pins[3], bc.pins[4], mxconfig.mx_width, mxconfig.mx_height, CHAIN_BOTTOM_LEFT_UP);
}
} else if (bc.type == TYPE_HUB75MATRIX_QS) {
mxconfig.mx_width = min((u_int8_t) 64, bc.pins[0]) * 2;
mxconfig.mx_height = min((u_int8_t) 64, bc.pins[1]) / 2;
virtualDisp = new VirtualMatrixPanel((*display), 1, 1, bc.pins[0], bc.pins[1]);
virtualDisp->setRotation(0);
switch(bc.pins[1]) {
case 16:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
break;
case 32:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
break;
case 64:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);
break;
default:
DEBUGBUS_PRINTLN("Unsupported height");
return;
}
} else {
DEBUGBUS_PRINTLN("Unknown type");
return;
}
#if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32S2)// classic esp32, or esp32-s2: reduce bitdepth for large panels
if (mxconfig.mx_height >= 64) {
if (mxconfig.chain_length * mxconfig.mx_width > 192) mxconfig.setPixelColorDepthBits(3);
else if (mxconfig.chain_length * mxconfig.mx_width > 64) mxconfig.setPixelColorDepthBits(4);
else mxconfig.setPixelColorDepthBits(8);
} else mxconfig.setPixelColorDepthBits(8);
#endif
mxconfig.chain_length = max((u_int8_t) 1, min(bc.pins[2], (u_int8_t) 4)); // prevent bad data preventing boot due to low memory
if(mxconfig.mx_height >= 64 && (mxconfig.chain_length > 1)) {
DEBUGBUS_PRINTLN("WARNING, only single panel can be used of 64 pixel boards due to memory");
mxconfig.chain_length = 1;
}
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
#if defined(ARDUINO_ADAFRUIT_MATRIXPORTAL_ESP32S3) // MatrixPortal ESP32-S3
// https://www.adafruit.com/product/5778
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Matrix Portal S3 config");
mxconfig.gpio = { 42, 41, 40, 38, 39, 37, 45, 36, 48, 35, 21, 47, 14, 2 };
#elif defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)// ESP32-S3 with PSRAM
#if defined(MOONHUB_S3_PINOUT)
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - T7 S3 with PSRAM, MOONHUB pinout");
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
mxconfig.gpio = { 1, 5, 6, 7, 13, 9, 16, 48, 47, 21, 38, 8, 4, 18 };
#else
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - S3 with PSRAM");
// HUB75_I2S_CFG::i2s_pins _pins={R1_PIN, G1_PIN, B1_PIN, R2_PIN, G2_PIN, B2_PIN, A_PIN, B_PIN, C_PIN, D_PIN, E_PIN, LAT_PIN, OE_PIN, CLK_PIN};
mxconfig.gpio = {1, 2, 42, 41, 40, 39, 45, 48, 47, 21, 38, 8, 3, 18};
#endif
#elif defined(ESP32_FORUM_PINOUT) // Common format for boards designed for SmartMatrix
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - ESP32_FORUM_PINOUT");
/*
ESP32 with SmartMatrix's default pinout - ESP32_FORUM_PINOUT
https://github.com/pixelmatix/SmartMatrix/blob/teensylc/src/MatrixHardware_ESP32_V0.h
Can use a board like https://github.com/rorosaurus/esp32-hub75-driver
*/
mxconfig.gpio = { 2, 15, 4, 16, 27, 17, 5, 18, 19, 21, 12, 26, 25, 22 };
#else
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA - Default pins");
/*
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA?tab=readme-ov-file
Boards
https://esp32trinity.com/
https://www.electrodragon.com/product/rgb-matrix-panel-drive-interface-board-for-esp32-dma/
*/
mxconfig.gpio = { 25, 26, 27, 14, 12, 13, 23, 19, 5, 17, 18, 4, 15, 16 };
#endif
int8_t pins[PIN_COUNT];
memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio));
if (!PinManager::allocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75, true)) {
DEBUGBUS_PRINTLN("Failed to allocate pins for HUB75");
return;
}
if(bc.colorOrder == COL_ORDER_RGB) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = Default color order (RGB)");
} else if(bc.colorOrder == COL_ORDER_BGR) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA = color order BGR");
int8_t tmpPin;
tmpPin = mxconfig.gpio.r1;
mxconfig.gpio.r1 = mxconfig.gpio.b1;
mxconfig.gpio.b1 = tmpPin;
tmpPin = mxconfig.gpio.r2;
mxconfig.gpio.r2 = mxconfig.gpio.b2;
mxconfig.gpio.b2 = tmpPin;
}
else {
DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA = unsupported color order %u\n", bc.colorOrder);
}
DEBUGBUS_PRINTF("MatrixPanel_I2S_DMA config - %ux%u length: %u\n", mxconfig.mx_width, mxconfig.mx_height, mxconfig.chain_length);
DEBUGBUS_PRINTF("R1_PIN=%u, G1_PIN=%u, B1_PIN=%u, R2_PIN=%u, G2_PIN=%u, B2_PIN=%u, A_PIN=%u, B_PIN=%u, C_PIN=%u, D_PIN=%u, E_PIN=%u, LAT_PIN=%u, OE_PIN=%u, CLK_PIN=%u\n",
mxconfig.gpio.r1, mxconfig.gpio.g1, mxconfig.gpio.b1, mxconfig.gpio.r2, mxconfig.gpio.g2, mxconfig.gpio.b2,
mxconfig.gpio.a, mxconfig.gpio.b, mxconfig.gpio.c, mxconfig.gpio.d, mxconfig.gpio.e, mxconfig.gpio.lat, mxconfig.gpio.oe, mxconfig.gpio.clk);
// OK, now we can create our matrix object
display = new MatrixPanel_I2S_DMA(mxconfig);
if (display == nullptr) {
DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! driver allocation failed ***********");
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
return;
}
this->_len = (display->width() * display->height());
DEBUGBUS_PRINTF("Length: %u\n", _len);
if(this->_len >= MAX_LEDS) {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA Too many LEDS - playing safe");
return;
}
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA created");
// let's adjust default brightness
display->setBrightness8(25); // range is 0-255, 0 - 0%, 255 - 100%
delay(24); // experimental
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
// Allocate memory and start DMA display
if( not display->begin() ) {
DEBUGBUS_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********");
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
return;
}
else {
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA begin ok");
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
delay(18); // experiment - give the driver a moment (~ one full frame @ 60hz) to settle
_valid = true;
display->clearScreen(); // initially clear the screen buffer
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA clear ok");
if (_ledBuffer) free(_ledBuffer); // should not happen
if (_ledsDirty) free(_ledsDirty); // should not happen
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory");
_ledsDirty = (byte*) malloc(getBitArrayBytes(_len)); // create LEDs dirty bits
DEBUGBUS_PRINTLN("MatrixPanel_I2S_DMA allocate memory ok");
if (_ledsDirty == nullptr) {
display->stopDMAoutput();
delete display; display = nullptr;
_valid = false;
DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA not started - not enough memory for dirty bits!"));
DEBUGBUS_PRINT(F("heap usage: ")); DEBUGBUS_PRINTLN(lastHeap - ESP.getFreeHeap());
return; // fail is we cannot get memory for the buffer
}
setBitArray(_ledsDirty, _len, false); // reset dirty bits
if (mxconfig.double_buff == false) {
_ledBuffer = (CRGB*) calloc(_len, sizeof(CRGB)); // create LEDs buffer (initialized to BLACK)
}
}
if (_valid) {
_panelWidth = virtualDisp ? virtualDisp->width() : display->width(); // cache width - it will never change
}
DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA "));
DEBUGBUS_PRINTF("%sstarted, width=%u, %u pixels.\n", _valid? "":"not ", _panelWidth, _len);
if (mxconfig.double_buff == true) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA driver native double-buffering enabled."));
if (_ledBuffer != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS buffer enabled."));
if (_ledsDirty != nullptr) DEBUGBUS_PRINTLN(F("MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled."));
if ((_ledBuffer != nullptr) || (_ledsDirty != nullptr)) {
DEBUGBUS_PRINT(F("MatrixPanel_I2S_DMA LEDS buffer uses "));
DEBUGBUS_PRINT((_ledBuffer? _len*sizeof(CRGB) :0) + (_ledsDirty? getBitArrayBytes(_len) :0));
DEBUGBUS_PRINTLN(F(" bytes."));
}
}
void __attribute__((hot)) BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) {
if (!_valid || pix >= _len) return;
// if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT
if (_ledBuffer) {
CRGB fastled_col = CRGB(c);
if (_ledBuffer[pix] != fastled_col) {
_ledBuffer[pix] = fastled_col;
setBitInArray(_ledsDirty, pix, true); // flag pixel as "dirty"
}
}
else {
if ((c == IS_BLACK) && (getBitFromArray(_ledsDirty, pix) == false)) return; // ignore black if pixel is already black
setBitInArray(_ledsDirty, pix, c != IS_BLACK); // dirty = true means "color is not BLACK"
#ifndef NO_CIE1931
c = unGamma24(c); // to use the driver linear brightness feature, we first need to undo WLED gamma correction
#endif
uint8_t r = R(c);
uint8_t g = G(c);
uint8_t b = B(c);
if(virtualDisp != nullptr) {
int x = pix % _panelWidth;
int y = pix / _panelWidth;
virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
} else {
int x = pix % _panelWidth;
int y = pix / _panelWidth;
display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
}
}
}
uint32_t BusHub75Matrix::getPixelColor(unsigned pix) const {
if (!_valid || pix >= _len) return IS_BLACK;
if (_ledBuffer)
return uint32_t(_ledBuffer[pix].scale8(_bri)) & 0x00FFFFFF; // scale8() is needed to mimic NeoPixelBus, which returns scaled-down colours
else
return getBitFromArray(_ledsDirty, pix) ? IS_DARKGREY: IS_BLACK; // just a hack - we only know if the pixel is black or not
}
void BusHub75Matrix::setBrightness(uint8_t b) {
_bri = b;
if (display) display->setBrightness(_bri);
}
void BusHub75Matrix::show(void) {
if (!_valid) return;
display->setBrightness(_bri);
if (_ledBuffer) {
// write out buffered LEDs
bool isVirtualDisp = (virtualDisp != nullptr);
unsigned height = isVirtualDisp ? virtualDisp->height() : display->height();
unsigned width = _panelWidth;
//while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker.
size_t pix = 0; // running pixel index
for (int y=0; y<height; y++) for (int x=0; x<width; x++) {
if (getBitFromArray(_ledsDirty, pix) == true) { // only repaint the "dirty" pixels
uint32_t c = uint32_t(_ledBuffer[pix]) & 0x00FFFFFF; // get RGB color, removing FastLED "alpha" component
#ifndef NO_CIE1931
c = unGamma24(c); // to use the driver linear brightness feature, we first need to undo WLED gamma correction
#endif
uint8_t r = R(c);
uint8_t g = G(c);
uint8_t b = B(c);
if (isVirtualDisp) virtualDisp->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
else display->drawPixelRGB888(int16_t(x), int16_t(y), r, g, b);
}
pix ++;
}
setBitArray(_ledsDirty, _len, false); // buffer shown - reset all dirty bits
}
if(mxconfig.double_buff) {
display->flipDMABuffer(); // Show the back buffer, set current output buffer to the back (i.e. no longer being sent to LED panels)
// while(!previousBufferFree) delay(1); // experimental - Wait before we allow any writing to the buffer. Stop flicker.
display->clearScreen(); // Now clear the back-buffer
setBitArray(_ledsDirty, _len, false); // dislay buffer is blank - reset all dirty bits
}
}
void BusHub75Matrix::cleanup() {
if (display && _valid) display->stopDMAoutput(); // terminate DMA driver (display goes black)
_valid = false;
_panelWidth = 0;
deallocatePins();
DEBUGBUS_PRINTLN("HUB75 output ended.");
//if (virtualDisp != nullptr) delete virtualDisp; // warning: deleting object of polymorphic class type 'VirtualMatrixPanel' which has non-virtual destructor might cause undefined behavior
delete display;
display = nullptr;
virtualDisp = nullptr;
if (_ledBuffer != nullptr) free(_ledBuffer); _ledBuffer = nullptr;
if (_ledsDirty != nullptr) free(_ledsDirty); _ledsDirty = nullptr;
}
void BusHub75Matrix::deallocatePins() {
uint8_t pins[PIN_COUNT];
memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio));
PinManager::deallocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75);
}
std::vector<LEDType> BusHub75Matrix::getLEDTypes() {
return {
{TYPE_HUB75MATRIX_HS, "H", PSTR("HUB75 (Half Scan)")},
{TYPE_HUB75MATRIX_QS, "H", PSTR("HUB75 (Quarter Scan)")},
};
}
size_t BusHub75Matrix::getPins(uint8_t* pinArray) const {
if (pinArray) {
pinArray[0] = mxconfig.mx_width;
pinArray[1] = mxconfig.mx_height;
pinArray[2] = mxconfig.chain_length;
}
return 3;
}
#endif
// ***************************************************************************
//utility to get the approx. memory usage of a given BusConfig
size_t BusConfig::memUsage(unsigned nr) const {
@@ -810,6 +1186,10 @@ int BusManager::add(const BusConfig &bc) {
if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) return -1;
if (Bus::isVirtual(bc.type)) {
busses.push_back(make_unique<BusNetwork>(bc));
#ifdef WLED_ENABLE_HUB75MATRIX
} else if (Bus::isHub75(bc.type)) {
busses.push_back(make_unique<BusHub75Matrix>(bc));
#endif
} else if (Bus::isDigital(bc.type)) {
busses.push_back(make_unique<BusDigital>(bc, Bus::is2Pin(bc.type) ? twoPin : digital));
} else if (Bus::isOnOff(bc.type)) {
@@ -841,6 +1221,10 @@ String BusManager::getLEDTypesJSONString() {
json += LEDTypesToJson(BusPwm::getLEDTypes());
json += LEDTypesToJson(BusNetwork::getLEDTypes());
//json += LEDTypesToJson(BusVirtual::getLEDTypes());
#ifdef WLED_ENABLE_HUB75MATRIX
json += LEDTypesToJson(BusHub75Matrix::getLEDTypes());
#endif
json.setCharAt(json.length()-1, ']'); // replace last comma with bracket
return json;
}