Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HUB75 support #3777

Open
wants to merge 35 commits into
base: 0_15
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7ef84cf
Add HUB75 support
netmindz Feb 26, 2024
7603b5a
Remove getMaxPixels
netmindz Feb 26, 2024
755f91f
Remove referece to MM
netmindz Feb 26, 2024
2bd1e81
Default to mrfaptastic pinout
netmindz Feb 26, 2024
07a1588
Cleanup comments
netmindz Feb 26, 2024
74f77a7
Merge branch 'bus-config' into HUB75-AC
netmindz Sep 7, 2024
ecd46f2
Swap to new way to have dynamic LED types list
netmindz Sep 7, 2024
aae9446
Add "old-style" changes to settings_led for hub75
netmindz Sep 7, 2024
e94943d
Assign proper type ID for Hub75
netmindz Sep 8, 2024
e066b50
hub75 - remove hard coded panel sizes
netmindz Sep 8, 2024
78fb9dc
Cleanup mxconfig.chain_length
netmindz Sep 8, 2024
e185f2e
Hub75 compact pin defintion
netmindz Sep 8, 2024
f96acd6
Hub75 - Tweaks to webui
netmindz Sep 8, 2024
e0d78d5
Porting latest BusHub75Matrix from MoonModules - Mostly authored by F…
netmindz Sep 8, 2024
21c582e
Porting latest BusHub75Matrix from MoonModules - Mostly authored by F…
netmindz Sep 8, 2024
ad402ad
Hub75 - Misc fixes - WiP
netmindz Sep 8, 2024
23e578b
Swap BusHub75Matrix to use allocateMultiplePins
netmindz Sep 22, 2024
382d7e8
Remove stray whitespace from xml.cpp
netmindz Sep 22, 2024
fc07397
cleanup hub75 comments
netmindz Sep 22, 2024
713cbb8
Merge branch '0_15' into HUB75-AC
netmindz Sep 22, 2024
b7aba15
Always copy all the pin data
netmindz Sep 22, 2024
e111b6e
Hub75 - PIN_COUNT const
netmindz Sep 22, 2024
8632a0a
Hub75 - use actual panel config values
netmindz Sep 22, 2024
0a8d86c
Always copy all the pin data
netmindz Sep 22, 2024
9a9c65a
Whitespace
netmindz Sep 22, 2024
fbeead0
Exclude hub75 from pin validdation for xml.cpp
netmindz Sep 22, 2024
e74eb7d
Move examples envs for hub75
netmindz Sep 22, 2024
6ce6b95
Merge branch '0_15' into HUB75-AC
netmindz Oct 4, 2024
5b86c67
Error for ESP8266 and hub75
netmindz Oct 4, 2024
4276671
HUB75 - Remove hot from show
netmindz Oct 4, 2024
f7b8828
HUB75 - code formatting
netmindz Oct 4, 2024
c356846
HUB75 - fix hasRGB and missing override
netmindz Oct 4, 2024
6f03854
HUB75 - add comments to example env
netmindz Oct 4, 2024
f1b9952
HUB75 - Support BGR color order
netmindz Oct 4, 2024
de8a366
HUB75 - lower color depth for larger panels
netmindz Oct 10, 2024
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
18 changes: 18 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -579,3 +579,21 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME=
${esp32.AR_build_flags}
lib_deps = ${esp32s2.lib_deps}
${esp32.AR_lib_deps}

;; TODO - move to sample ini
[env:esp32dev_hub75]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will require this to be moved into sample file. I want platformio.ini clean and only used for default official builds.

board = esp32dev
upload_speed = 921600
platform = ${esp32_idf_V4.platform}
platform_packages = ${esp32_idf_V4.platform_packages}
build_unflags = ${common.build_unflags}
build_flags = ${common.build_flags}
-D WLED_RELEASE_NAME=ESP32_hub75
-D WLED_ENABLE_HUB75MATRIX -D NO_GFX -D NO_CIE1931
-D ESP32_FORUM_PINOUT
-D WLED_DEBUG
lib_deps = ${esp32_idf_V4.lib_deps}
https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-DMA.git#3.0.11

monitor_filters = esp32_exception_decoder
board_build.partitions = ${esp32.default_partitions}
2 changes: 1 addition & 1 deletion usermods/rgb-rotary-encoder/rgb-rotary-encoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class RgbRotaryEncoderUsermod : public Usermod

void initLedBus()
{
byte _pins[5] = {(byte)ledIo, 255, 255, 255, 255};
byte _pins[OUTPUT_MAX_PINS] = {(byte)ledIo, 255, 255, 255, 255};
BusConfig busCfg = BusConfig(TYPE_WS2812_RGB, _pins, 0, numLeds, COL_ORDER_GRB, false, 0);

ledBus = new BusDigital(busCfg, WLED_MAX_BUSSES - 1);
Expand Down
324 changes: 324 additions & 0 deletions wled00/bus_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,37 @@
#include "bus_wrapper.h"
#include "bus_manager.h"

// 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;
}
extern bool cctICused;

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);

Expand Down Expand Up @@ -784,6 +813,293 @@ void BusNetwork::cleanup() {
freeData();
}

// ***************************************************************************

#ifdef WLED_ENABLE_HUB75MATRIX
blazoncek marked this conversation as resolved.
Show resolved Hide resolved

BusHub75Matrix::BusHub75Matrix(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite) {

_valid = false;
mxconfig.double_buff = false; // default to off, known to cause issue with some effects but needs more memory
// mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver
//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;

fourScanPanel = nullptr;

if(bc.type == TYPE_HUB75MATRIX_HS) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use space between if and (.

I would also encourage the following form:

if (condition {
  ...
} else if (condition2) {
 ...
} else {
  ...
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

mxconfig.mx_width = min((u_int8_t) 64, bc.pins[0]);
mxconfig.mx_height = min((u_int8_t) 64, bc.pins[1]);
}
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;
fourScanPanel = new VirtualMatrixPanel((*display), 1, 1, bc.pins[0], bc.pins[1]);
fourScanPanel->setRotation(0);
switch(bc.pins[1]) {
case 16:
fourScanPanel->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
break;
case 32:
fourScanPanel->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
break;
case 64:
fourScanPanel->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);
break;
default:
DEBUG_PRINTLN("Unsupported height");
return;
}
}
else {
DEBUG_PRINTLN("Unknown type");
return;
}


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)) {
DEBUG_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
blazoncek marked this conversation as resolved.
Show resolved Hide resolved

// https://www.adafruit.com/product/5778
DEBUG_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(ESP32_FORUM_PINOUT) // Common format for boards designed for SmartMatrix

DEBUG_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
DEBUG_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, 9, 5, 17, 18, 4, 15, 16 };

#endif

int8_t pins[PIN_COUNT];
memcpy(pins, &mxconfig.gpio, sizeof(mxconfig.gpio));
PinManager::allocateMultiplePins(pins, PIN_COUNT, PinOwner::HUB75, true);

DEBUG_PRINTF("MatrixPanel_I2S_DMA config - %ux%u length: %u\n", mxconfig.mx_width, mxconfig.mx_height, mxconfig.chain_length);
DEBUG_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);

this->_len = (display->width() * display->height());
DEBUG_PRINTF("Length: %u\n", _len);
if(this->_len >= MAX_LEDS) {
DEBUG_PRINTLN("MatrixPanel_I2S_DMA Too many LEDS - playing safe");
return;
}

DEBUG_PRINTLN("MatrixPanel_I2S_DMA created");
// let's adjust default brightness
display->setBrightness8(25); // range is 0-255, 0 - 0%, 255 - 100%

delay(24); // experimental
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// Allocate memory and start DMA display
if( not display->begin() ) {
DEBUG_PRINTLN("****** MatrixPanel_I2S_DMA !KABOOM! I2S memory allocation failed ***********");
return;
}
else {
DEBUG_PRINTLN("MatrixPanel_I2S_DMA begin ok");
delay(18); // experiment - give the driver a moment (~ one full frame @ 60hz) to settle
_valid = true;
display->clearScreen(); // initially clear the screen buffer
DEBUG_PRINTLN("MatrixPanel_I2S_DMA clear ok");

if (_ledBuffer) free(_ledBuffer); // should not happen
if (_ledsDirty) free(_ledsDirty); // should not happen
DEBUG_PRINTLN("MatrixPanel_I2S_DMA allocate memory");
_ledsDirty = (byte*) malloc(getBitArrayBytes(_len)); // create LEDs dirty bits
DEBUG_PRINTLN("MatrixPanel_I2S_DMA allocate memory ok");

if (_ledsDirty == nullptr) {
display->stopDMAoutput();
delete display; display = nullptr;
_valid = false;
DEBUG_PRINTLN(F("MatrixPanel_I2S_DMA not started - not enough memory for dirty bits!"));
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 = fourScanPanel ? fourScanPanel->width() : display->width(); // cache width - it will never change
}

DEBUG_PRINT(F("MatrixPanel_I2S_DMA "));
DEBUG_PRINTF("%sstarted, width=%u, %u pixels.\n", _valid? "":"not ", _panelWidth, _len);

if (mxconfig.double_buff == true) DEBUG_PRINTLN(F("MatrixPanel_I2S_DMA driver native double-buffering enabled."));
if (_ledBuffer != nullptr) DEBUG_PRINTLN(F("MatrixPanel_I2S_DMA LEDS buffer enabled."));
if (_ledsDirty != nullptr) DEBUG_PRINTLN(F("MatrixPanel_I2S_DMA LEDS dirty bit optimization enabled."));
if ((_ledBuffer != nullptr) || (_ledsDirty != nullptr)) {
DEBUG_PRINT(F("MatrixPanel_I2S_DMA LEDS buffer uses "));
DEBUG_PRINT((_ledBuffer? _len*sizeof(CRGB) :0) + (_ledsDirty? getBitArrayBytes(_len) :0));
DEBUG_PRINTLN(F(" bytes."));
}
}

void __attribute__((hot)) BusHub75Matrix::setPixelColor(uint16_t pix, uint32_t c) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a standards version of [[gnu:hot]] may be better choice. I'm not sure but I think the attribute is needed in declaration, not in definition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(fourScanPanel != nullptr) {
int x = pix % _panelWidth;
int y = pix / _panelWidth;
fourScanPanel->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(uint16_t 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning is false. This function should return original value the setPixelColor() set.
The behaviour of NPB has to be reverted and is done so in BusDigital.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, bool immediate) {
_bri = b;
if (_bri > 238) _bri=238;
display->setBrightness(_bri);
}

void __attribute__((hot)) BusHub75Matrix::show(void) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not need to be hot. Called at 23ms intervals.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

if (!_valid) return;
display->setBrightness(_bri);

if (_ledBuffer) {
// write out buffered LEDs
bool isFourScan = (fourScanPanel != nullptr);
//if (isFourScan) fourScanPanel->setRotation(0);
unsigned height = isFourScan ? fourScanPanel->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 (isFourScan) fourScanPanel->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();
DEBUG_PRINTLN("HUB75 output ended.");

//if (fourScanPanel != nullptr) delete fourScanPanel; // warning: deleting object of polymorphic class type 'VirtualMatrixPanel' which has non-virtual destructor might cause undefined behavior
delete display;
display = nullptr;
fourScanPanel = 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)")},
};
}

uint8_t BusHub75Matrix::getPins(uint8_t* pinArray) const {
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
uint32_t BusManager::memUsage(BusConfig &bc) {
Expand Down Expand Up @@ -815,6 +1131,10 @@ int BusManager::add(BusConfig &bc) {
if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1;
if (Bus::isVirtual(bc.type)) {
busses[numBusses] = new BusNetwork(bc);
#ifdef WLED_ENABLE_HUB75MATRIX
} else if (Bus::isHub75(bc.type)) {
busses[numBusses] = new BusHub75Matrix(bc);
#endif
} else if (Bus::isDigital(bc.type)) {
busses[numBusses] = new BusDigital(bc, numBusses, colorOrderMap);
} else if (Bus::isOnOff(bc.type)) {
Expand Down Expand Up @@ -846,6 +1166,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;
}
Expand Down
Loading