New Version

This commit is contained in:
mrfaptastic 2020-07-29 08:47:54 +01:00
parent d4e7a49c2b
commit 75136c59e1
6 changed files with 588 additions and 315 deletions

View file

@ -3,155 +3,346 @@
// Credits: Louis Beaudoin <https://github.com/pixelmatix/SmartMatrix/tree/teensylc>
// and Sprite_TM: https://www.esp32.com/viewtopic.php?f=17&t=3188 and https://www.esp32.com/viewtopic.php?f=13&t=3256
void RGB64x32MatrixPanel_I2S_DMA::configureDMA(int r1_pin, int g1_pin, int b1_pin, int r2_pin, int g2_pin, int b2_pin, int a_pin, int b_pin, int c_pin, int d_pin, int e_pin, int lat_pin, int oe_pin, int clk_pin)
/*
This is example code to driver a p3(2121)64*32 -style RGB LED display. These types of displays do not have memory and need to be refreshed
continuously. The display has 2 RGB inputs, 4 inputs to select the active line, a pixel clock input, a latch enable input and an output-enable
input. The display can be seen as 2 64x16 displays consisting of the upper half and the lower half of the display. Each half has a separate
RGB pixel input, the rest of the inputs are shared.
Each display half can only show one line of RGB pixels at a time: to do this, the RGB data for the line is input by setting the RGB input pins
to the desired value for the first pixel, giving the display a clock pulse, setting the RGB input pins to the desired value for the second pixel,
giving a clock pulse, etc. Do this 64 times to clock in an entire row. The pixels will not be displayed yet: until the latch input is made high,
the display will still send out the previously clocked in line. Pulsing the latch input high will replace the displayed data with the data just
clocked in.
The 4 line select inputs select where the currently active line is displayed: when provided with a binary number (0-15), the latched pixel data
will immediately appear on this line. Note: While clocking in data for a line, the *previous* line is still displayed, and these lines should
be set to the value to reflect the position the *previous* line is supposed to be on.
Finally, the screen has an OE input, which is used to disable the LEDs when latching new data and changing the state of the line select inputs:
doing so hides any artifacts that appear at this time. The OE line is also used to dim the display by only turning it on for a limited time every
line.
All in all, an image can be displayed by 'scanning' the display, say, 100 times per second. The slowness of the human eye hides the fact that
only one line is showed at a time, and the display looks like every pixel is driven at the same time.
Now, the RGB inputs for these types of displays are digital, meaning each red, green and blue subpixel can only be on or off. This leads to a
color palette of 8 pixels, not enough to display nice pictures. To get around this, we use binary code modulation.
Binary code modulation is somewhat like PWM, but easier to implement in our case. First, we define the time we would refresh the display without
binary code modulation as the 'frame time'. For, say, a four-bit binary code modulation, the frame time is divided into 15 ticks of equal length.
We also define 4 subframes (0 to 3), defining which LEDs are on and which LEDs are off during that subframe. (Subframes are the same as a
normal frame in non-binary-coded-modulation mode, but are showed faster.) From our (non-monochrome) input image, we take the (8-bit: bit 7
to bit 0) RGB pixel values. If the pixel values have bit 7 set, we turn the corresponding LED on in subframe 3. If they have bit 6 set,
we turn on the corresponding LED in subframe 2, if bit 5 is set subframe 1, if bit 4 is set in subframe 0.
Now, in order to (on average within a frame) turn a LED on for the time specified in the pixel value in the input data, we need to weigh the
subframes. We have 15 pixels: if we show subframe 3 for 8 of them, subframe 2 for 4 of them, subframe 1 for 2 of them and subframe 1 for 1 of
them, this 'automatically' happens. (We also distribute the subframes evenly over the ticks, which reduces flicker.)
In this code, we use the I2S peripheral in parallel mode to achieve this. Essentially, first we allocate memory for all subframes. This memory
contains a sequence of all the signals (2xRGB, line select, latch enable, output enable) that need to be sent to the display for that subframe.
Then we ask the I2S-parallel driver to set up a DMA chain so the subframes are sent out in a sequence that satisfies the requirement that
subframe x has to be sent out for (2^x) ticks. Finally, we fill the subframes with image data.
We use a frontbuffer/backbuffer technique here to make sure the display is refreshed in one go and drawing artifacts do not reach the display.
In practice, for small displays this is not really necessarily.
*/
#define ESP32_NUM_FRAME_BUFFERS 1
bool RGB64x32MatrixPanel_I2S_DMA::allocateDMAmemory()
{
// calculate the lowest LSBMSB_TRANSITION_BIT value that will fit in memory
int numDescriptorsPerRow;
lsbMsbTransitionBit = 0;
while(1) {
numDescriptorsPerRow = 1;
for(int i=lsbMsbTransitionBit + 1; i<COLOR_DEPTH_BITS; i++) {
numDescriptorsPerRow += 1<<(i - lsbMsbTransitionBit - 1);
/***
* Step 1: Look at the overall DMA capable memory for the DMA FRAMEBUFFER data only (not the DMA linked list descriptors yet)
* and do some pre-checks.
*/
int _num_frame_buffers = (double_buffering_enabled) ? 2:1;
size_t _frame_buffer_memory_required = sizeof(frameStruct) * _num_frame_buffers;
size_t _dma_linked_list_memory_required = 0;
size_t _total_dma_capable_memory_reserved = 0;
// 1. Calculate and malloc the LARGEST available DMA memory block to matrix_framebuffer_malloc_1
#if SERIAL_DEBUG
Serial.printf("Panel Height: %d pixels.\r\n", MATRIX_HEIGHT);
Serial.printf("Panel Width: %d pixels.\r\n", MATRIX_WIDTH);
if (double_buffering_enabled) {
Serial.println("DOUBLE FRAME BUFFERS / DOUBLE BUFFERING IS ENABLED. DOUBLE THE RAM REQUIRED!");
}
/*
Serial.println("DMA memory blocks available before any malloc's: ");
heap_caps_print_heap_info(MALLOC_CAP_DMA);
*/
Serial.printf("FYI: Size of an ESP32 DMA linked list descriptor (lldesc_t) is %d bytes\r\n", sizeof(lldesc_t));
Serial.printf("We're going to need %d bytes of SRAM just for the frame buffer(s).\r\n", _frame_buffer_memory_required);
Serial.printf("Largest DMA capable SRAM memory block is %d bytes.\r\n", heap_caps_get_largest_free_block(MALLOC_CAP_DMA));
#endif
// Can we fit the framebuffer into the single DMA capable memory block available?
if ( heap_caps_get_largest_free_block(MALLOC_CAP_DMA) >= _frame_buffer_memory_required ) { // YES - SIMPLE
// Allocate the framebuffer memory, fail if we can even do this
matrix_framebuffer_malloc_1 = (frameStruct *)heap_caps_malloc(_frame_buffer_memory_required, MALLOC_CAP_DMA);
if ( !matrix_framebuffer_malloc_1 ) {
#if SERIAL_DEBUG
Serial.println("ERROR: Couldn't malloc matrix_framebuffer_malloc_1! Critical fail.\r\n");
#endif
return false;
}
int ramrequired = numDescriptorsPerRow * ROWS_PER_FRAME * ESP32_NUM_FRAME_BUFFERS * sizeof(lldesc_t);
ramrequired += 64000; // HACK Hard Coded: Keep at least 64k free!
_total_dma_capable_memory_reserved += _frame_buffer_memory_required;
}
#ifdef SPLIT_MEMORY_MODE
Serial.println("SPLIT MEMORY MODE ENABLED!");
#if SERIAL_DEBUG
Serial.print("Rows per frame (overall): "); Serial.println(ROWS_PER_FRAME, DEC);
Serial.print("Rows per split framebuffer malloc: "); Serial.println(SPLIT_MEMORY_ROWS_PER_FRAME, DEC);
#endif
// Can we fit the framebuffer into the single DMA capable memory block available?
if ( heap_caps_get_largest_free_block(MALLOC_CAP_DMA) >= _frame_buffer_memory_required ) { // YES - SIMPLE
// Allocate the framebuffer memory, fail if we can even do this
matrix_framebuffer_malloc_2 = (frameStruct *)heap_caps_malloc(_frame_buffer_memory_required, MALLOC_CAP_DMA);
if ( !matrix_framebuffer_malloc_2 ) {
#if SERIAL_DEBUG
Serial.println("ERROR: Couldn't malloc matrix_framebuffer_malloc_2! Critical fail.\r\n");
#endif
return false;
}
_total_dma_capable_memory_reserved += _frame_buffer_memory_required;
}
#endif
/***
* Step 2: Calculate the amount of memory required for the DMA engine's linked list descriptors.
* Credit to SmartMatrix for this stuff.
*/
// Calculate what color depth is actually possible based on memory avaialble vs. required dma linked-list descriptors.
// aka. Calculate the lowest LSBMSB_TRANSITION_BIT value that will fit in memory
int numDMAdescriptorsPerRow = 0;
lsbMsbTransitionBit = 0;
while(1) {
numDMAdescriptorsPerRow = 1;
for(int i=lsbMsbTransitionBit + 1; i<PIXEL_COLOR_DEPTH_BITS; i++) {
numDMAdescriptorsPerRow += 1<<(i - lsbMsbTransitionBit - 1);
}
int ramrequired = numDMAdescriptorsPerRow * ROWS_PER_FRAME * _num_frame_buffers * sizeof(lldesc_t);
int largestblockfree = heap_caps_get_largest_free_block(MALLOC_CAP_DMA);
#if SERIAL_DEBUG
Serial.printf("numdesciptors per row %d, lsbMsbTransitionBit of %d requires %d RAM, %d available, leaving %d free: \r\n", numDMAdescriptorsPerRow, lsbMsbTransitionBit, ramrequired, largestblockfree, largestblockfree - ramrequired);
#endif
Serial.printf("lsbMsbTransitionBit of %d requires %d RAM, %d available, leaving %d free: \r\n", lsbMsbTransitionBit, ramrequired, largestblockfree, largestblockfree - ramrequired);
if(ramrequired < (largestblockfree))
if(ramrequired < largestblockfree)
break;
if(lsbMsbTransitionBit < COLOR_DEPTH_BITS - 1)
if(lsbMsbTransitionBit < PIXEL_COLOR_DEPTH_BITS - 1)
lsbMsbTransitionBit++;
else
break;
}
if(numDescriptorsPerRow * ROWS_PER_FRAME * ESP32_NUM_FRAME_BUFFERS * sizeof(lldesc_t) > heap_caps_get_largest_free_block(MALLOC_CAP_DMA)){
assert("Not enough RAM for SmartMatrix descriptors");
Serial.printf("Not enough RAM for SmartMatrix descriptors\r\n");
return;
}
Serial.printf("Raised lsbMsbTransitionBit to %d/%d to fit in RAM\r\n", lsbMsbTransitionBit, COLOR_DEPTH_BITS - 1);
Serial.printf("Raised lsbMsbTransitionBit to %d/%d to fit in remaining RAM\r\n", lsbMsbTransitionBit, PIXEL_COLOR_DEPTH_BITS - 1);
// calculate the lowest LSBMSB_TRANSITION_BIT value that will fit in memory that will meet or exceed the configured refresh rate
while(1) {
int psPerClock = 1000000000000UL/ESP32_I2S_CLOCK_SPEED;
int nsPerLatch = ((PIXELS_PER_LATCH + CLKS_DURING_LATCH) * psPerClock) / 1000;
Serial.printf("ns per latch: %d: \r\n", nsPerLatch);
int nsPerLatch = ((PIXELS_PER_ROW + CLKS_DURING_LATCH) * psPerClock) / 1000;
// add time to shift out LSBs + LSB-MSB transition bit - this ignores fractions...
int nsPerRow = COLOR_DEPTH_BITS * nsPerLatch;
int nsPerRow = PIXEL_COLOR_DEPTH_BITS * nsPerLatch;
// add time to shift out MSBs
for(int i=lsbMsbTransitionBit + 1; i<COLOR_DEPTH_BITS; i++)
nsPerRow += (1<<(i - lsbMsbTransitionBit - 1)) * (COLOR_DEPTH_BITS - i) * nsPerLatch;
//Serial.printf("nsPerRow: %d: \r\n", nsPerRow);
for(int i=lsbMsbTransitionBit + 1; i<PIXEL_COLOR_DEPTH_BITS; i++)
nsPerRow += (1<<(i - lsbMsbTransitionBit - 1)) * (PIXEL_COLOR_DEPTH_BITS - i) * nsPerLatch;
int nsPerFrame = nsPerRow * ROWS_PER_FRAME;
Serial.printf("nsPerFrame: %d: \r\n", nsPerFrame);
int actualRefreshRate = 1000000000UL/(nsPerFrame);
refreshRate = actualRefreshRate;
calculated_refresh_rate = actualRefreshRate;
Serial.printf("lsbMsbTransitionBit of %d gives %d Hz refresh: \r\n", lsbMsbTransitionBit, actualRefreshRate);
if (actualRefreshRate > min_refresh_rate) // HACK Hard Coded: 100
break;
if(lsbMsbTransitionBit < COLOR_DEPTH_BITS - 1)
if(lsbMsbTransitionBit < PIXEL_COLOR_DEPTH_BITS - 1)
lsbMsbTransitionBit++;
else
break;
}
Serial.printf("Raised lsbMsbTransitionBit to %d/%d to meet minimum refresh rate\r\n", lsbMsbTransitionBit, COLOR_DEPTH_BITS - 1);
Serial.printf("Raised lsbMsbTransitionBit to %d/%d to meet minimum refresh rate\r\n", lsbMsbTransitionBit, PIXEL_COLOR_DEPTH_BITS - 1);
// TODO: completely fill buffer with data before enabling DMA - can't do this now, lsbMsbTransition bit isn't set in the calc class - also this call will probably have no effect as matrixCalcDivider will skip the first call
//matrixCalcCallback();
// lsbMsbTransition Bit is now finalized - redo descriptor count in case it changed to hit min refresh rate
numDescriptorsPerRow = 1;
for(int i=lsbMsbTransitionBit + 1; i<COLOR_DEPTH_BITS; i++) {
numDescriptorsPerRow += 1<<(i - lsbMsbTransitionBit - 1);
// lsbMsbTransition Bit is now finalized - recalcuate descriptor count in case it changed to hit min refresh rate
numDMAdescriptorsPerRow = 1;
for(int i=lsbMsbTransitionBit + 1; i<PIXEL_COLOR_DEPTH_BITS; i++) {
numDMAdescriptorsPerRow += 1<<(i - lsbMsbTransitionBit - 1);
}
Serial.printf("Descriptors for lsbMsbTransitionBit %d/%d with %d rows require %d bytes of DMA RAM\r\n", lsbMsbTransitionBit, COLOR_DEPTH_BITS - 1, ROWS_PER_FRAME, 2 * numDescriptorsPerRow * ROWS_PER_FRAME * sizeof(lldesc_t));
/***
* Step 3: Allocate memory for DMA linked list, linking up each framebuffer row in sequence for GPIO output.
*/
_dma_linked_list_memory_required = numDMAdescriptorsPerRow * ROWS_PER_FRAME * _num_frame_buffers * sizeof(lldesc_t);
Serial.printf("Descriptors for lsbMsbTransitionBit %d/%d with %d rows require %d bytes of DMA RAM\r\n", lsbMsbTransitionBit, PIXEL_COLOR_DEPTH_BITS - 1, ROWS_PER_FRAME, _dma_linked_list_memory_required);
_total_dma_capable_memory_reserved += _dma_linked_list_memory_required;
// Do a final check to see if we have enough space for the additional DMA linked list descriptors that will be required to link it all up!
if(_dma_linked_list_memory_required > heap_caps_get_largest_free_block(MALLOC_CAP_DMA)) {
#if SERIAL_DEBUG
Serial.printf("ERROR: Not enough SRAM left over for DMA linked-list descriptor memory reservation! Oh so close!\r\n");
#endif
return false;
} // linked list descriptors memory check
// malloc the DMA linked list descriptors that i2s_parallel will need
int desccount = numDescriptorsPerRow * ROWS_PER_FRAME;
lldesc_t * dmadesc_a = (lldesc_t *)heap_caps_malloc(desccount * sizeof(lldesc_t), MALLOC_CAP_DMA);
assert("Can't allocate descriptor buffer a");
desccount = numDMAdescriptorsPerRow * ROWS_PER_FRAME;
//lldesc_t * dmadesc_a = (lldesc_t *)heap_caps_malloc(desccount * sizeof(lldesc_t), MALLOC_CAP_DMA);
dmadesc_a = (lldesc_t *)heap_caps_malloc(desccount * sizeof(lldesc_t), MALLOC_CAP_DMA);
assert("Can't allocate descriptor framebuffer a");
if(!dmadesc_a) {
Serial.printf("Could not malloc descriptor buffer a.");
return;
Serial.printf("Error: Could not malloc descriptor framebuffer a.");
return false;
}
lldesc_t * dmadesc_b = (lldesc_t *)heap_caps_malloc(desccount * sizeof(lldesc_t), MALLOC_CAP_DMA);
assert("Could not malloc descriptor buffer b.");
if(!dmadesc_b) {
Serial.printf("can't malloc");
return;
}
Serial.printf("SmartMatrix Mallocs Complete\r\n");
Serial.printf("Heap Memory Available: %d bytes total, %d bytes largest free block: \r\n", heap_caps_get_free_size(0), heap_caps_get_largest_free_block(0));
Serial.printf("8-bit Accessible Memory Available: %d bytes total, %d bytes largest free block: \r\n", heap_caps_get_free_size(MALLOC_CAP_8BIT), heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
Serial.printf("32-bit Memory Available: %d bytes total, %d bytes largest free block: \r\n", heap_caps_get_free_size(MALLOC_CAP_32BIT), heap_caps_get_largest_free_block(MALLOC_CAP_32BIT));
Serial.printf("DMA Memory Available: %d bytes total, %d bytes largest free block: \r\n", heap_caps_get_free_size(MALLOC_CAP_DMA), heap_caps_get_largest_free_block(MALLOC_CAP_DMA));
lldesc_t *prevdmadesca = 0;
lldesc_t *prevdmadescb = 0;
int currentDescOffset = 0;
// fill DMA linked lists for both frames
for(int j=0; j<ROWS_PER_FRAME; j++) {
// first set of data is LSB through MSB, single pass - all color bits are displayed once, which takes care of everything below and inlcluding LSBMSB_TRANSITION_BIT
// TODO: size must be less than DMA_MAX - worst case for SmartMatrix Library: 16-bpp with 256 pixels per row would exceed this, need to break into two
link_dma_desc(&dmadesc_a[currentDescOffset], prevdmadesca, &(matrixUpdateFrames[0].rowdata[j].rowbits[0].data), sizeof(rowBitStruct) * COLOR_DEPTH_BITS);
prevdmadesca = &dmadesc_a[currentDescOffset];
link_dma_desc(&dmadesc_b[currentDescOffset], prevdmadescb, &(matrixUpdateFrames[1].rowdata[j].rowbits[0].data), sizeof(rowBitStruct) * COLOR_DEPTH_BITS);
prevdmadescb = &dmadesc_b[currentDescOffset];
currentDescOffset++;
//Serial.printf("row %d: \r\n", j);
for(int i=lsbMsbTransitionBit + 1; i<COLOR_DEPTH_BITS; i++) {
// binary time division setup: we need 2 of bit (LSBMSB_TRANSITION_BIT + 1) four of (LSBMSB_TRANSITION_BIT + 2), etc
// because we sweep through to MSB each time, it divides the number of times we have to sweep in half (saving linked list RAM)
// we need 2^(i - LSBMSB_TRANSITION_BIT - 1) == 1 << (i - LSBMSB_TRANSITION_BIT - 1) passes from i to MSB
//Serial.printf("buffer %d: repeat %d times, size: %d, from %d - %d\r\n", nextBufdescIndex, 1<<(i - LSBMSB_TRANSITION_BIT - 1), (COLOR_DEPTH_BITS - i), i, COLOR_DEPTH_BITS-1);
for(int k=0; k < 1<<(i - lsbMsbTransitionBit - 1); k++) {
link_dma_desc(&dmadesc_a[currentDescOffset], prevdmadesca, &(matrixUpdateFrames[0].rowdata[j].rowbits[i].data), sizeof(rowBitStruct) * (COLOR_DEPTH_BITS - i));
prevdmadesca = &dmadesc_a[currentDescOffset];
link_dma_desc(&dmadesc_b[currentDescOffset], prevdmadescb, &(matrixUpdateFrames[1].rowdata[j].rowbits[i].data), sizeof(rowBitStruct) * (COLOR_DEPTH_BITS - i));
prevdmadescb = &dmadesc_b[currentDescOffset];
currentDescOffset++;
//Serial.printf("i %d, j %d, k %d\r\n", i, j, k);
}
if (double_buffering_enabled) // reserve space for second framebuffer linked list
{
//lldesc_t * dmadesc_b = (lldesc_t *)heap_caps_malloc(desccount * sizeof(lldesc_t), MALLOC_CAP_DMA);
dmadesc_b = (lldesc_t *)heap_caps_malloc(desccount * sizeof(lldesc_t), MALLOC_CAP_DMA);
assert("Could not malloc descriptor framebuffer b.");
if(!dmadesc_b) {
Serial.printf("Error: Could not malloc descriptor framebuffer b.");
return false;
}
}
Serial.printf("*** Memory Allocations Complete *** \r\n");
Serial.printf("Total memory that was reserved: %d kB.\r\n", _total_dma_capable_memory_reserved/1024);
Serial.printf("Heap Memory Available: %d bytes total. Largest free block: %d bytes.\r\n", heap_caps_get_free_size(0), heap_caps_get_largest_free_block(0));
Serial.printf("DMA Memory Available: %d bytes total. Largest free block: %d bytes.\r\n", heap_caps_get_free_size(MALLOC_CAP_DMA), heap_caps_get_largest_free_block(MALLOC_CAP_DMA));
Serial.printf("General RAM Available: %d bytes total. Largest free block: %d bytes.\r\n", heap_caps_get_free_size(MALLOC_CAP_DEFAULT), heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT));
return true;
} // end initMatrixDMABuffer()
void RGB64x32MatrixPanel_I2S_DMA::configureDMA(int r1_pin, int g1_pin, int b1_pin, int r2_pin, int g2_pin, int b2_pin, int a_pin, int b_pin, int c_pin, int d_pin, int e_pin, int lat_pin, int oe_pin, int clk_pin)
{
#if SERIAL_DEBUG
Serial.println("configureDMA(): Starting configuration of DMA engine.\r\n");
#endif
lldesc_t *previous_dmadesc_a = 0;
lldesc_t *previous_dmadesc_b = 0;
int current_dmadescriptor_offset = 0;
/* Fill DMA linked lists for both frames (as in, halves of the HUB75 panel)
* .. and if double buffering is enabled, link it up for both buffers.
*/
for(int j = 0; j < ROWS_PER_FRAME; j++)
{
// Split framebuffer malloc hack 'improvement'
frameStruct *fb_malloc_ptr = matrix_framebuffer_malloc_1;
int fb_malloc_j = j;
#ifdef SPLIT_MEMORY_MODE
if ( j >= SPLIT_MEMORY_ROWS_PER_FRAME ) {
fb_malloc_ptr = matrix_framebuffer_malloc_2;
fb_malloc_j -= SPLIT_MEMORY_ROWS_PER_FRAME; // IMPORTANT
#if SERIAL_DEBUG
Serial.printf("Split memory mode. Panel row: %d, mapped to framebuffer malloc 2 offset: %d.\r\n", j, fb_malloc_j);
#endif
}
#endif
// first set of data is LSB through MSB, single pass - all color bits are displayed once, which takes care of everything below and inlcluding LSBMSB_TRANSITION_BIT
// TODO: size must be less than DMA_MAX - worst case for SmartMatrix Library: 16-bpp with 256 pixels per row would exceed this, need to break into two
link_dma_desc(&dmadesc_a[current_dmadescriptor_offset], previous_dmadesc_a, &(fb_malloc_ptr[0].rowdata[fb_malloc_j].rowbits[0].data), sizeof(rowBitStruct) * PIXEL_COLOR_DEPTH_BITS);
previous_dmadesc_a = &dmadesc_a[current_dmadescriptor_offset];
if (double_buffering_enabled) {
link_dma_desc(&dmadesc_b[current_dmadescriptor_offset], previous_dmadesc_b, &(fb_malloc_ptr[1].rowdata[fb_malloc_j].rowbits[0].data), sizeof(rowBitStruct) * PIXEL_COLOR_DEPTH_BITS);
previous_dmadesc_b = &dmadesc_b[current_dmadescriptor_offset]; }
current_dmadescriptor_offset++;
for(int i=lsbMsbTransitionBit + 1; i<PIXEL_COLOR_DEPTH_BITS; i++) {
// binary time division setup: we need 2 of bit (LSBMSB_TRANSITION_BIT + 1) four of (LSBMSB_TRANSITION_BIT + 2), etc
// because we sweep through to MSB each time, it divides the number of times we have to sweep in half (saving linked list RAM)
// we need 2^(i - LSBMSB_TRANSITION_BIT - 1) == 1 << (i - LSBMSB_TRANSITION_BIT - 1) passes from i to MSB
//Serial.printf("buffer %d: repeat %d times, size: %d, from %d - %d\r\n", current_dmadescriptor_offset, 1<<(i - lsbMsbTransitionBit - 1), (PIXEL_COLOR_DEPTH_BITS - i), i, PIXEL_COLOR_DEPTH_BITS-1);
for(int k=0; k < 1<<(i - lsbMsbTransitionBit - 1); k++) {
link_dma_desc(&dmadesc_a[current_dmadescriptor_offset], previous_dmadesc_a, &(fb_malloc_ptr[0].rowdata[fb_malloc_j].rowbits[i].data), sizeof(rowBitStruct) * (PIXEL_COLOR_DEPTH_BITS - i));
previous_dmadesc_a = &dmadesc_a[current_dmadescriptor_offset];
if (double_buffering_enabled) {
link_dma_desc(&dmadesc_b[current_dmadescriptor_offset], previous_dmadesc_b, &(fb_malloc_ptr[1].rowdata[fb_malloc_j].rowbits[i].data), sizeof(rowBitStruct) * (PIXEL_COLOR_DEPTH_BITS - i));
previous_dmadesc_b = &dmadesc_b[current_dmadescriptor_offset]; }
current_dmadescriptor_offset++;
} // end color depth ^ 2 linked list
} // end color depth loop
} // end frame rows
#if SERIAL_DEBUG
Serial.println("configureDMA(): Configured LL structure.\r\n");
#endif
dmadesc_a[desccount-1].eof = 1;
dmadesc_a[desccount-1].qe.stqe_next=(lldesc_t*)&dmadesc_a[0];
//End markers for DMA LL
if (double_buffering_enabled) {
dmadesc_b[desccount-1].eof = 1;
dmadesc_b[desccount-1].qe.stqe_next=(lldesc_t*)&dmadesc_b[0];
} else {
dmadesc_b = dmadesc_a; // link to same 'a' buffer
}
/*
//End markers
dmadesc_a[desccount-1].eof = 1;
dmadesc_b[desccount-1].eof = 1;
dmadesc_a[desccount-1].qe.stqe_next=(lldesc_t*)&dmadesc_a[0];
dmadesc_b[desccount-1].qe.stqe_next=(lldesc_t*)&dmadesc_b[0];
Serial.printf("Performing I2S setup.\n");
*/
//Serial.printf("Performing I2S setup.\n");
i2s_parallel_config_t cfg={
.gpio_bus={r1_pin, g1_pin, b1_pin, r2_pin, g2_pin, b2_pin, lat_pin, oe_pin, a_pin, b_pin, c_pin, d_pin, e_pin, -1, -1, -1},
.gpio_clk=clk_pin,
.clkspeed_hz=ESP32_I2S_CLOCK_SPEED, //ESP32_I2S_CLOCK_SPEED, // formula used is 80000000L/(cfg->clkspeed_hz + 1), must result in >=2. Acceptable values 26.67MHz, 20MHz, 16MHz, 13.34MHz...
.bits=MATRIX_I2S_MODE, //MATRIX_I2S_MODE,
.bits=ESP32_I2S_DMA_MODE, //ESP32_I2S_DMA_MODE,
.bufa=0,
.bufb=0,
desccount,
@ -163,42 +354,69 @@ void RGB64x32MatrixPanel_I2S_DMA::configureDMA(int r1_pin, int g1_pin, int b1_
//Setup I2S
i2s_parallel_setup_without_malloc(&I2S1, &cfg);
Serial.printf("I2S setup done.\n");
#if SERIAL_DEBUG
Serial.println("configureDMA(): DMA configuration completed on I2S1.\r\n");
#endif
#if SERIAL_DEBUG
Serial.println("DMA Memory Map after allocations: ");
heap_caps_print_heap_info(MALLOC_CAP_DMA);
#endif
// Just os we know
dma_configuration_success = true;
everything_OK = true;
} // end initMatrixDMABuff
/* Update a specific co-ordinate in the DMA buffer */
void RGB64x32MatrixPanel_I2S_DMA::updateMatrixDMABuffer(int16_t x_coord, int16_t y_coord, uint8_t red, uint8_t green, uint8_t blue)
{
if ( !dma_configuration_success)
if ( !everything_OK )
assert("DMA configuration in begin() not performed or completed successfully.");
// Need to check that the co-ordinates are within range, or it'll break everything big time.
if ( x_coord < 0 || y_coord < 0 || x_coord >= MATRIX_WIDTH || y_coord >= MATRIX_HEIGHT)
{
// Serial.printf("Invalid: x %d, y %d - r %d, g %d, b %d\n", (int)x_coord, (int)y_coord, (int)red, (int)green, (int)blue );
// x_coord = y_coord = 1;
return;
int tmp_y_coord = y_coord;
/* 1) Check that the co-ordinates are within range, or it'll break everything big time.
* Valid co-ordinates are from 0 to (MATRIX_XXXX-1)
*/
if ( x_coord < 0 || y_coord < 0 || x_coord >= MATRIX_WIDTH || y_coord >= MATRIX_HEIGHT) {
return;
}
// What half of the HUB75 panel are we painting to?
/* 2) Convert the vertical axis / y-axis pixel co-ordinate to a matrix panel parallel co-ordinate..
* eg. If the y co-ordinate is 23, that's actually in the second half of the panel, row 7.
* 23 (y coord) - 16 (for 32px high panel) = 7
*/
bool paint_top_half = true;
if ( y_coord > ROWS_PER_FRAME-1) // co-ords start at zero, y_coord = 15 = 16 (rows per frame)
if ( y_coord >= ROWS_PER_FRAME) // co-ords start at zero, y_coord = 15 = 16 (rows per frame)
{
y_coord -= ROWS_PER_FRAME; // if it's 16, subtract 16. Array position 0 again.
y_coord -= ROWS_PER_FRAME; // Subtract the ROWS_PER_FRAME from the pixel co-ord to get the panel co-ord.
paint_top_half = false;
}
for(int color_depth_idx=0; color_depth_idx<COLOR_DEPTH_BITS; color_depth_idx++) // color depth - 8 iterations
for(int color_depth_idx=0; color_depth_idx<PIXEL_COLOR_DEPTH_BITS; color_depth_idx++) // color depth - 8 iterations
{
uint16_t mask = (1 << color_depth_idx); // 24 bit color
// The destination for the pixel bitstream
rowBitStruct *p = &matrixUpdateFrames[backbuf_id].rowdata[y_coord].rowbits[color_depth_idx]; //matrixUpdateFrames location to write to uint16_t's
//rowBitStruct *p = &matrix_framebuffer_malloc_1[back_buffer_id].rowdata[y_coord].rowbits[color_depth_idx]; //matrixUpdateFrames location to write to uint16_t's
rowBitStruct *p = &matrix_framebuffer_malloc_1[back_buffer_id].rowdata[y_coord].rowbits[color_depth_idx];
#ifdef SPLIT_MEMORY_MODE
if (y_coord >= SPLIT_MEMORY_ROWS_PER_FRAME ) {
p = &matrix_framebuffer_malloc_2[back_buffer_id].rowdata[(y_coord-SPLIT_MEMORY_ROWS_PER_FRAME)].rowbits[color_depth_idx]; //matrixUpdateFrames location to write to uint16_t's
}
#ifdef SERIAL_DEBUG
// Serial.printf("Using framebuffer_malloc_2. y-coord: %d, matrix row offset: %d \r\n", tmp_y_coord, (y_coord-SPLIT_MEMORY_ROWS_PER_FRAME) );
#endif
#endif
int v=0; // the output bitstream
@ -216,7 +434,6 @@ void RGB64x32MatrixPanel_I2S_DMA::updateMatrixDMABuffer(int16_t x_coord, int16_t
if (gpioRowAddress & 0x10) v|=BIT_E; // 16
/* ORIG
// need to disable OE after latch to hide row transition
if((x_coord) == 0) v|=BIT_OE;
@ -225,17 +442,16 @@ void RGB64x32MatrixPanel_I2S_DMA::updateMatrixDMABuffer(int16_t x_coord, int16_t
// need to turn off OE one clock before latch, otherwise can get ghosting
if((x_coord)==PIXELS_PER_LATCH-1) v|=BIT_OE;
*/
// need to disable OE after latch to hide row transition
if((x_coord) == 0 ) v|=BIT_OE;
// drive latch while shifting out last bit of RGB data
if((x_coord) == PIXELS_PER_LATCH-1) v|=BIT_LAT;
if((x_coord) == PIXELS_PER_ROW-1) v|=BIT_LAT;
// need to turn off OE one clock before latch, otherwise can get ghosting
if((x_coord)==PIXELS_PER_LATCH-2) v|=BIT_OE;
if((x_coord)==PIXELS_PER_ROW-2) v|=BIT_OE;
@ -342,15 +558,25 @@ void RGB64x32MatrixPanel_I2S_DMA::updateMatrixDMABuffer(int16_t x_coord, int16_t
/* Update the entire buffer with a single specific colour - quicker */
void RGB64x32MatrixPanel_I2S_DMA::updateMatrixDMABuffer(uint8_t red, uint8_t green, uint8_t blue)
{
for (unsigned int y_coord = 0; y_coord < ROWS_PER_FRAME; y_coord++) // half height - 16 iterations
for (unsigned int matrix_frame_parallel_row = 0; matrix_frame_parallel_row < ROWS_PER_FRAME; matrix_frame_parallel_row++) // half height - 16 iterations
{
for(int color_depth_idx=0; color_depth_idx<COLOR_DEPTH_BITS; color_depth_idx++) // color depth - 8 iterations
for(int color_depth_idx=0; color_depth_idx<PIXEL_COLOR_DEPTH_BITS; color_depth_idx++) // color depth - 8 iterations
{
uint16_t mask = (1 << color_depth_idx); // 24 bit color
// The destination for the pixel bitstream
rowBitStruct *p = &matrixUpdateFrames[backbuf_id].rowdata[y_coord].rowbits[color_depth_idx]; //matrixUpdateFrames location to write to uint16_t's
rowBitStruct *p = &matrix_framebuffer_malloc_1[back_buffer_id].rowdata[matrix_frame_parallel_row].rowbits[color_depth_idx]; //matrixUpdateFrames location to write to uint16_t's
#ifdef SPLIT_MEMORY_MODE
if (matrix_frame_parallel_row >= SPLIT_MEMORY_ROWS_PER_FRAME ) {
p = &matrix_framebuffer_malloc_2[back_buffer_id].rowdata[(matrix_frame_parallel_row-SPLIT_MEMORY_ROWS_PER_FRAME)].rowbits[color_depth_idx]; //matrixUpdateFrames location to write to uint16_t's
#ifdef SERIAL_DEBUG
// Serial.printf("Using framebuffer_malloc_2. Row %d = Offset %d\r\n", matrix_frame_parallel_row, (matrix_frame_parallel_row-SPLIT_MEMORY_ROWS_PER_FRAME) );
#endif
}
#endif
for(int x_coord=0; x_coord < MATRIX_WIDTH; x_coord++) // row pixel width 64 iterations
{
@ -358,11 +584,11 @@ void RGB64x32MatrixPanel_I2S_DMA::updateMatrixDMABuffer(uint8_t red, uint8_t gre
int v=0; // the output bitstream
// if there is no latch to hold address, output ADDX lines directly to GPIO and latch data at end of cycle
int gpioRowAddress = y_coord;
int gpioRowAddress = matrix_frame_parallel_row;
// normally output current rows ADDX, special case for LSB, output previous row's ADDX (as previous row is being displayed for one latch cycle)
if(color_depth_idx == 0)
gpioRowAddress = y_coord-1;
gpioRowAddress = matrix_frame_parallel_row-1;
if (gpioRowAddress & 0x01) v|=BIT_A; // 1
if (gpioRowAddress & 0x02) v|=BIT_B; // 2
@ -372,7 +598,6 @@ void RGB64x32MatrixPanel_I2S_DMA::updateMatrixDMABuffer(uint8_t red, uint8_t gre
/* ORIG
// need to disable OE after latch to hide row transition
if((x_coord) == 0) v|=BIT_OE;
@ -381,17 +606,16 @@ void RGB64x32MatrixPanel_I2S_DMA::updateMatrixDMABuffer(uint8_t red, uint8_t gre
// need to turn off OE one clock before latch, otherwise can get ghosting
if((x_coord)==PIXELS_PER_LATCH-1) v|=BIT_OE;
*/
// need to disable OE after latch to hide row transition
if((x_coord) == 0 ) v|=BIT_OE;
// drive latch while shifting out last bit of RGB data
if((x_coord) == PIXELS_PER_LATCH-1) v|=BIT_LAT;
if((x_coord) == PIXELS_PER_ROW-1) v|=BIT_LAT;
// need to turn off OE one clock before latch, otherwise can get ghosting
if((x_coord)==PIXELS_PER_LATCH-2) v|=BIT_OE;
if((x_coord)==PIXELS_PER_ROW-2) v|=BIT_OE;
// turn off OE after brightness value is reached when displaying MSBs

View file

@ -9,61 +9,18 @@
#include "esp_heap_caps.h"
#include "esp32_i2s_parallel.h"
#include "Adafruit_GFX.h"
/*
This is example code to driver a p3(2121)64*32 -style RGB LED display. These types of displays do not have memory and need to be refreshed
continuously. The display has 2 RGB inputs, 4 inputs to select the active line, a pixel clock input, a latch enable input and an output-enable
input. The display can be seen as 2 64x16 displays consisting of the upper half and the lower half of the display. Each half has a separate
RGB pixel input, the rest of the inputs are shared.
Each display half can only show one line of RGB pixels at a time: to do this, the RGB data for the line is input by setting the RGB input pins
to the desired value for the first pixel, giving the display a clock pulse, setting the RGB input pins to the desired value for the second pixel,
giving a clock pulse, etc. Do this 64 times to clock in an entire row. The pixels will not be displayed yet: until the latch input is made high,
the display will still send out the previously clocked in line. Pulsing the latch input high will replace the displayed data with the data just
clocked in.
The 4 line select inputs select where the currently active line is displayed: when provided with a binary number (0-15), the latched pixel data
will immediately appear on this line. Note: While clocking in data for a line, the *previous* line is still displayed, and these lines should
be set to the value to reflect the position the *previous* line is supposed to be on.
Finally, the screen has an OE input, which is used to disable the LEDs when latching new data and changing the state of the line select inputs:
doing so hides any artifacts that appear at this time. The OE line is also used to dim the display by only turning it on for a limited time every
line.
All in all, an image can be displayed by 'scanning' the display, say, 100 times per second. The slowness of the human eye hides the fact that
only one line is showed at a time, and the display looks like every pixel is driven at the same time.
Now, the RGB inputs for these types of displays are digital, meaning each red, green and blue subpixel can only be on or off. This leads to a
color palette of 8 pixels, not enough to display nice pictures. To get around this, we use binary code modulation.
Binary code modulation is somewhat like PWM, but easier to implement in our case. First, we define the time we would refresh the display without
binary code modulation as the 'frame time'. For, say, a four-bit binary code modulation, the frame time is divided into 15 ticks of equal length.
We also define 4 subframes (0 to 3), defining which LEDs are on and which LEDs are off during that subframe. (Subframes are the same as a
normal frame in non-binary-coded-modulation mode, but are showed faster.) From our (non-monochrome) input image, we take the (8-bit: bit 7
to bit 0) RGB pixel values. If the pixel values have bit 7 set, we turn the corresponding LED on in subframe 3. If they have bit 6 set,
we turn on the corresponding LED in subframe 2, if bit 5 is set subframe 1, if bit 4 is set in subframe 0.
Now, in order to (on average within a frame) turn a LED on for the time specified in the pixel value in the input data, we need to weigh the
subframes. We have 15 pixels: if we show subframe 3 for 8 of them, subframe 2 for 4 of them, subframe 1 for 2 of them and subframe 1 for 1 of
them, this 'automatically' happens. (We also distribute the subframes evenly over the ticks, which reduces flicker.)
In this code, we use the I2S peripheral in parallel mode to achieve this. Essentially, first we allocate memory for all subframes. This memory
contains a sequence of all the signals (2xRGB, line select, latch enable, output enable) that need to be sent to the display for that subframe.
Then we ask the I2S-parallel driver to set up a DMA chain so the subframes are sent out in a sequence that satisfies the requirement that
subframe x has to be sent out for (2^x) ticks. Finally, we fill the subframes with image data.
We use a frontbuffer/backbuffer technique here to make sure the display is refreshed in one go and drawing artifacts do not reach the display.
In practice, for small displays this is not really necessarily.
*/
#include "GFX.h" // Adafruit GFX core class -> https://github.com/mrfaptastic/GFX_Root
/***************************************************************************************/
/* Serial Debugging Output on or off */
/* Library compile-time options */
#define SERIAL_DEBUG_OUTPUT 1
// Enable serial debugging of the library, to see how memory is allocated etc.
#define SERIAL_DEBUG 1
// Experimental: Split the framebuffer into two smaller memory allocations.
// Can allow a bigger resolution due to the fragmented memory
// map of the typical arduino sketch even before setup().
//#define SPLIT_MEMORY_MODE 1
/***************************************************************************************/
/* HUB75 RGB pixel WIDTH and HEIGHT.
@ -89,6 +46,8 @@
#define MATRIX_ROWS_IN_PARALLEL 2
#endif
#define MATRIX_COLOR_DEPTH (8*3)
/***************************************************************************************/
/* ESP32 Pin Definition. You can change this, but best if you keep it as is... */
@ -103,15 +62,16 @@
#define B_PIN_DEFAULT 19
#define C_PIN_DEFAULT 5
#define D_PIN_DEFAULT 17
#define E_PIN_DEFAULT -1 // Change to a valid pin if using a 64 pixel row panel.
#define E_PIN_DEFAULT -1 // IMPORTANT: Change to a valid pin if using a 64x64px panel.
#define LAT_PIN_DEFAULT 4
#define OE_PIN_DEFAULT 15
#define CLK_PIN_DEFAULT 16
// Interesting Fact: We end up using a uint16_t to send data in parallel to the HUB75... but
// given we only map to 14 physical output wires/bits, we waste 2 bits.
/***************************************************************************************/
/* Don't change this stuff unless you know what you are doing */
/* Keep this as is. Do not change. */
// Panel Upper half RGB (numbering according to order in DMA gpio_bus configuration)
#define BIT_R1 (1<<0)
@ -136,37 +96,51 @@
// RGB Panel Constants / Calculated Values
#define COLOR_CHANNELS_PER_PIXEL 3
#define PIXELS_PER_LATCH ((MATRIX_WIDTH * MATRIX_HEIGHT) / MATRIX_HEIGHT) // = 64
#define COLOR_DEPTH_BITS (COLOR_DEPTH/COLOR_CHANNELS_PER_PIXEL) // = 8
#define ROWS_PER_FRAME (MATRIX_HEIGHT/MATRIX_ROWS_IN_PARALLEL) // = 16
#define PIXELS_PER_ROW ((MATRIX_WIDTH * MATRIX_HEIGHT) / MATRIX_HEIGHT) // = 64
#define PIXEL_COLOR_DEPTH_BITS (MATRIX_COLOR_DEPTH/COLOR_CHANNELS_PER_PIXEL) // = 8
#define ROWS_PER_FRAME (MATRIX_HEIGHT/MATRIX_ROWS_IN_PARALLEL) // = 16
/***************************************************************************************/
/* Keep this as is. Do not change. */
#define CLKS_DURING_LATCH 0
#define MATRIX_I2S_MODE I2S_PARALLEL_BITS_16
#define MATRIX_DATA_STORAGE_TYPE uint16_t
#define ESP32_NUM_FRAME_BUFFERS 2
#define ESP32_I2S_CLOCK_SPEED (20000000UL)
#define COLOR_DEPTH 24
#define ESP32_I2S_DMA_MODE I2S_PARALLEL_BITS_16 // Pump 16 bits out in parallel
#define ESP32_I2S_DMA_STORAGE_TYPE uint16_t // one uint16_t at a time.
//#define ESP32_I2S_CLOCK_SPEED (20000000UL) // @ 20Mhz
#define ESP32_I2S_CLOCK_SPEED (10000000UL) // @ 10Mhz
#define CLKS_DURING_LATCH 0 // Not used.
/***************************************************************************************/
// note: sizeof(data) must be multiple of 32 bits, as ESP32 DMA linked list buffer address pointer must be word-aligned.
/* rowBitStruct
* Note: sizeof(data) must be multiple of 32 bits, as ESP32 DMA linked list buffer address pointer
* must be word-aligned.
*/
struct rowBitStruct {
MATRIX_DATA_STORAGE_TYPE data[((MATRIX_WIDTH * MATRIX_HEIGHT) / MATRIX_HEIGHT) + CLKS_DURING_LATCH];
// this evaluates to just MATRIX_DATA_STORAGE_TYPE data[64] really;
// and array of 64 uint16_t's
ESP32_I2S_DMA_STORAGE_TYPE data[PIXELS_PER_ROW + CLKS_DURING_LATCH];
// This evaluates to just data[64] really.. an array of 64 uint16_t's
};
/* rowColorDepthStruct
* Duplicates of row bit structure, but for each color 'depth'ness.
*/
struct rowColorDepthStruct {
rowBitStruct rowbits[COLOR_DEPTH_BITS];
rowBitStruct rowbits[PIXEL_COLOR_DEPTH_BITS];
};
/* frameStruct
* Note: This 'frameStruct' will contain ALL the data for a full-frame as BOTH 2x16-row frames are
* are contained in parallel within the one uint16_t that is sent in parallel to the HUB75.
*/
#ifdef SPLIT_MEMORY_MODE
//#pragma message("Split DMA Memory Allocation Mode Enabled")
#define SPLIT_MEMORY_ROWS_PER_FRAME (ROWS_PER_FRAME/2)
struct frameStruct {
rowColorDepthStruct rowdata[SPLIT_MEMORY_ROWS_PER_FRAME];
};
#else
struct frameStruct {
rowColorDepthStruct rowdata[ROWS_PER_FRAME];
};
#endif
typedef struct rgb_24 {
rgb_24() : rgb_24(0,0,0) {}
@ -182,45 +156,47 @@ typedef struct rgb_24 {
/***************************************************************************************/
class RGB64x32MatrixPanel_I2S_DMA : public Adafruit_GFX {
class RGB64x32MatrixPanel_I2S_DMA : public GFX {
// ------- PUBLIC -------
public:
RGB64x32MatrixPanel_I2S_DMA(bool _doubleBuffer = false) // Double buffer is disabled by default. Any change will display next active DMA buffer output (very quickly). NOTE: Not Implemented
: Adafruit_GFX(MATRIX_WIDTH, MATRIX_HEIGHT), doubleBuffer(_doubleBuffer) {
backbuf_id = 0;
brightness = 16; // If you get ghosting... reduce brightness level. 60 seems to be the limit before ghosting on a 64 pixel wide physical panel for some panels
min_refresh_rate = 250; // Probably best to leave as is unless you want to experiment. Framerate has an impact on brightness and also power draw - voltage ripple.
/**
* RGB64x32MatrixPanel_I2S_DMA
*
* @param {bool} _double_buffer : Double buffer is disabled by default. Enable only if you know what you're doing. Manual switching required with flipDMABuffer() and showDMABuffer()
*
*/
RGB64x32MatrixPanel_I2S_DMA(bool _double_buffer = false)
: GFX(MATRIX_WIDTH, MATRIX_HEIGHT), double_buffering_enabled(_double_buffer) {
}
// Painfully propagate the DMA pin configuration, or use compiler defaults
void begin(int dma_r1_pin = R1_PIN_DEFAULT , int dma_g1_pin = G1_PIN_DEFAULT, int dma_b1_pin = B1_PIN_DEFAULT , int dma_r2_pin = R2_PIN_DEFAULT , int dma_g2_pin = G2_PIN_DEFAULT , int dma_b2_pin = B2_PIN_DEFAULT , int dma_a_pin = A_PIN_DEFAULT , int dma_b_pin = B_PIN_DEFAULT , int dma_c_pin = C_PIN_DEFAULT , int dma_d_pin = D_PIN_DEFAULT , int dma_e_pin = E_PIN_DEFAULT , int dma_lat_pin = LAT_PIN_DEFAULT, int dma_oe_pin = OE_PIN_DEFAULT , int dma_clk_pin = CLK_PIN_DEFAULT)
/* Propagate the DMA pin configuration, or use compiler defaults */
bool begin(int dma_r1_pin = R1_PIN_DEFAULT , int dma_g1_pin = G1_PIN_DEFAULT, int dma_b1_pin = B1_PIN_DEFAULT , int dma_r2_pin = R2_PIN_DEFAULT , int dma_g2_pin = G2_PIN_DEFAULT , int dma_b2_pin = B2_PIN_DEFAULT , int dma_a_pin = A_PIN_DEFAULT , int dma_b_pin = B_PIN_DEFAULT , int dma_c_pin = C_PIN_DEFAULT , int dma_d_pin = D_PIN_DEFAULT , int dma_e_pin = E_PIN_DEFAULT , int dma_lat_pin = LAT_PIN_DEFAULT, int dma_oe_pin = OE_PIN_DEFAULT , int dma_clk_pin = CLK_PIN_DEFAULT)
{
// Change 'if' to '1' to enable, 0 to not include this Serial output in compiled program
#if SERIAL_DEBUG
Serial.printf("Using pin %d for the R1_PIN\n", dma_r1_pin);
Serial.printf("Using pin %d for the G1_PIN\n", dma_g1_pin);
Serial.printf("Using pin %d for the B1_PIN\n", dma_b1_pin);
Serial.printf("Using pin %d for the R2_PIN\n", dma_r2_pin);
Serial.printf("Using pin %d for the G2_PIN\n", dma_g2_pin);
Serial.printf("Using pin %d for the B2_PIN\n", dma_b2_pin);
Serial.printf("Using pin %d for the A_PIN\n", dma_a_pin);
Serial.printf("Using pin %d for the B_PIN\n", dma_b_pin);
Serial.printf("Using pin %d for the C_PIN\n", dma_c_pin);
Serial.printf("Using pin %d for the D_PIN\n", dma_d_pin);
Serial.printf("Using pin %d for the E_PIN\n", dma_e_pin);
Serial.printf("Using pin %d for the LAT_PIN\n", dma_lat_pin);
Serial.printf("Using pin %d for the OE_PIN\n", dma_oe_pin);
Serial.printf("Using pin %d for the CLK_PIN\n", dma_clk_pin);
#endif
/* As DMA buffers are dynamically allocated, we must allocated in begin()
* Ref: https://github.com/espressif/arduino-esp32/issues/831
*/
allocateDMAbuffers();
// Change 'if' to '1' to enable, 0 to not include this Serial output in compiled program
#if SERIAL_DEBUG_OUTPUT
Serial.printf("Using pin %d for the R1_PIN\n", dma_r1_pin);
Serial.printf("Using pin %d for the G1_PIN\n", dma_g1_pin);
Serial.printf("Using pin %d for the B1_PIN\n", dma_b1_pin);
Serial.printf("Using pin %d for the R2_PIN\n", dma_r2_pin);
Serial.printf("Using pin %d for the G2_PIN\n", dma_g2_pin);
Serial.printf("Using pin %d for the B2_PIN\n", dma_b2_pin);
Serial.printf("Using pin %d for the A_PIN\n", dma_a_pin);
Serial.printf("Using pin %d for the B_PIN\n", dma_b_pin);
Serial.printf("Using pin %d for the C_PIN\n", dma_c_pin);
Serial.printf("Using pin %d for the D_PIN\n", dma_d_pin);
Serial.printf("Using pin %d for the E_PIN\n", dma_e_pin);
Serial.printf("Using pin %d for the LAT_PIN\n", dma_lat_pin);
Serial.printf("Using pin %d for the OE_PIN\n", dma_oe_pin);
Serial.printf("Using pin %d for the CLK_PIN\n", dma_clk_pin);
#endif
if ( !allocateDMAmemory() ) { return false; } // couldn't even get the basic ram required.
// Flush the DMA buffers prior to configuring DMA - Avoid visual artefacts on boot.
@ -232,7 +208,9 @@ class RGB64x32MatrixPanel_I2S_DMA : public Adafruit_GFX {
// Setup the ESP32 DMA Engine. Sprite_TM built this stuff.
configureDMA(dma_r1_pin, dma_g1_pin, dma_b1_pin, dma_r2_pin, dma_g2_pin, dma_b2_pin, dma_a_pin, dma_b_pin, dma_c_pin, dma_d_pin, dma_e_pin, dma_lat_pin, dma_oe_pin, dma_clk_pin ); //DMA and I2S configuration and setup
showDMABuffer(); // show 0
showDMABuffer(); // show backbuf_id of 0
return everything_OK;
}
@ -258,17 +236,26 @@ class RGB64x32MatrixPanel_I2S_DMA : public Adafruit_GFX {
inline void flipDMABuffer()
{
// Flip to other buffer as the backbuffer. i.e. Graphic changes happen to this buffer (but aren't displayed until showDMABuffer())
backbuf_id ^=1;
if ( !double_buffering_enabled) return;
#if SERIAL_DEBUG_OUTPUT
Serial.printf("Set back buffer to: %d\n", backbuf_id);
#endif
// Flip to other buffer as the backbuffer. i.e. Graphic changes happen to this buffer (but aren't displayed until showDMABuffer())
back_buffer_id ^= 1;
#if SERIAL_DEBUG
Serial.printf("Set back buffer to: %d\n", back_buffer_id);
#endif
}
inline void showDMABuffer()
{
i2s_parallel_flip_to_buffer(&I2S1, backbuf_id);
if (!double_buffering_enabled) return;
#if SERIAL_DEBUG
Serial.printf("Showtime for buffer: %d\n", back_buffer_id);
#endif
i2s_parallel_flip_to_buffer(&I2S1, back_buffer_id);
}
@ -283,42 +270,46 @@ class RGB64x32MatrixPanel_I2S_DMA : public Adafruit_GFX {
min_refresh_rate = rr;
}
int calculated_refresh_rate = 0;
// ------- PRIVATE -------
private:
void allocateDMAbuffers()
{
matrixUpdateFrames = (frameStruct *)heap_caps_malloc(sizeof(frameStruct) * ESP32_NUM_FRAME_BUFFERS, MALLOC_CAP_DMA);
Serial.printf("Allocating DMA Refresh Buffer...\r\nTotal DMA Memory available: %d bytes total. Largest free block: %d bytes\r\n", heap_caps_get_free_size(MALLOC_CAP_DMA), heap_caps_get_largest_free_block(MALLOC_CAP_DMA));
/* Pixel data is organized from LSB to MSB sequentially by row, from row 0 to row matrixHeight/matrixRowsInParallel
* (two rows of pixels are refreshed in parallel) */
frameStruct *matrix_framebuffer_malloc_1;
} // end initMatrixDMABuffer()
#ifdef SPLIT_MEMORY_MODE
/* In the case there's a 2nd non-contiguous block of DMA capable SRAM we can use, let us try and use that as well. */
frameStruct *matrix_framebuffer_malloc_2;
#endif
// ESP 32 DMA Linked List descriptor
int desccount = 0;
lldesc_t * dmadesc_a = {0};
lldesc_t * dmadesc_b = {0};
// ESP32-RGB64x32MatrixPanel-I2S-DMA functioning
bool everything_OK = false;
bool double_buffering_enabled = false;// Do we use double buffer mode? Your project code will have to manually flip between both.
int back_buffer_id = 0; // If using double buffer, which one is NOT active (ie. being displayed) to write too?
int brightness = 32; // If you get ghosting... reduce brightness level. 60 seems to be the limit before ghosting on a 64 pixel wide physical panel for some panels.
int min_refresh_rate = 120; // Probably best to leave as is unless you want to experiment. Framerate has an impact on brightness and also power draw - voltage ripple.
int lsbMsbTransitionBit = 0; // For possible color depth calculations
/* Calculate the memory available for DMA use, do some other stuff, and allocate accordingly */
bool allocateDMAmemory();
/* Setup the DMA Link List chain and initiate the ESP32 DMA engine */
void configureDMA(int r1_pin, int g1_pin, int b1_pin, int r2_pin, int g2_pin, int b2_pin, int a_pin, int b_pin, int c_pin, int d_pin, int e_pin, int lat_pin, int oe_pin, int clk_pin); // Get everything setup. Refer to the .c file
// Update a specific pixel in the DMA buffer to a colour
/* Update a specific pixel in the DMA buffer to a colour */
void updateMatrixDMABuffer(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue);
// Update the entire DMA buffer (aka. The RGB Panel) a certain colour (wipe the screen basically)
/* Update the entire DMA buffer (aka. The RGB Panel) a certain colour (wipe the screen basically) */
void updateMatrixDMABuffer(uint8_t red, uint8_t green, uint8_t blue);
// Pixel data is organized from LSB to MSB sequentially by row, from row 0 to row matrixHeight/matrixRowsInParallel (two rows of pixels are refreshed in parallel)
frameStruct *matrixUpdateFrames;
// Setup
bool dma_configuration_success;
// Internal variables
bool doubleBuffer; // Do we use double buffer mode? Your project code will have to manually flip between both.
int backbuf_id; // If using double buffer, which one is NOT active (ie. being displayed) to write too?
int lsbMsbTransitionBit;
int refreshRate;
int brightness;
int min_refresh_rate;
}; // end Class header
/***************************************************************************************/
@ -329,36 +320,6 @@ inline void RGB64x32MatrixPanel_I2S_DMA::drawPixel(int16_t x, int16_t y, uint16_
drawPixelRGB565( x, y, color);
}
inline void RGB64x32MatrixPanel_I2S_DMA::drawIcon (int *ico, int16_t x, int16_t y, int16_t cols, int16_t rows) {
/* drawIcon draws a C style bitmap.
// Example 10x5px bitmap of a yellow sun
//
int half_sun [50] = {
0x0000, 0x0000, 0x0000, 0xffe0, 0x0000, 0x0000, 0xffe0, 0x0000, 0x0000, 0x0000,
0x0000, 0xffe0, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xffe0, 0x0000,
0x0000, 0x0000, 0x0000, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x0000, 0x0000, 0x0000,
0xffe0, 0x0000, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x0000, 0xffe0,
0x0000, 0x0000, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x0000, 0x0000,
};
RGB64x32MatrixPanel_I2S_DMA matrix;
matrix.drawIcon (half_sun, 0,0,10,5);
*/
int i, j;
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++) {
drawPixelRGB565 (x + j, y + i, ico[i * cols + j]);
}
}
}
inline void RGB64x32MatrixPanel_I2S_DMA::fillScreen(uint16_t color) // adafruit virtual void override
{
uint8_t r = ((((color >> 11) & 0x1F) * 527) + 23) >> 6;
@ -388,7 +349,6 @@ inline void RGB64x32MatrixPanel_I2S_DMA::drawPixelRGB24(int16_t x, int16_t y, rg
updateMatrixDMABuffer( x, y, color.red, color.green, color.blue);
}
// Pass 8-bit (each) R,G,B, get back 16-bit packed color
//https://github.com/squix78/ILI9341Buffer/blob/master/ILI9341_SPI.cpp
inline uint16_t RGB64x32MatrixPanel_I2S_DMA::color565(uint8_t r, uint8_t g, uint8_t b) {
@ -402,7 +362,6 @@ inline uint16_t RGB64x32MatrixPanel_I2S_DMA::color565(uint8_t r, uint8_t g, uint
return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
}
// Promote 3/3/3 RGB to Adafruit_GFX 5/6/5 RRRrrGGGgggBBBbb
inline uint16_t RGB64x32MatrixPanel_I2S_DMA::Color333(uint8_t r, uint8_t g, uint8_t b) {
@ -410,4 +369,30 @@ inline uint16_t RGB64x32MatrixPanel_I2S_DMA::Color333(uint8_t r, uint8_t g, uint
}
inline void RGB64x32MatrixPanel_I2S_DMA::drawIcon (int *ico, int16_t x, int16_t y, int16_t cols, int16_t rows) {
/* drawIcon draws a C style bitmap.
// Example 10x5px bitmap of a yellow sun
//
int half_sun [50] = {
0x0000, 0x0000, 0x0000, 0xffe0, 0x0000, 0x0000, 0xffe0, 0x0000, 0x0000, 0x0000,
0x0000, 0xffe0, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xffe0, 0x0000,
0x0000, 0x0000, 0x0000, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x0000, 0x0000, 0x0000,
0xffe0, 0x0000, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x0000, 0xffe0,
0x0000, 0x0000, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0xffe0, 0x0000, 0x0000,
};
RGB64x32MatrixPanel_I2S_DMA matrix;
matrix.drawIcon (half_sun, 0,0,10,5);
*/
int i, j;
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++) {
drawPixelRGB565 (x + j, y + i, ico[i * cols + j]);
}
}
}
#endif

View file

@ -1,16 +1,16 @@
# Adafruit_GFX RGB64x32MatrixPanel library for the ESP32, utilising I2S DMA.
# HUB75 LED matrix library for the ESP32, utilising DMA
This ESP32 Arduino library for an RGB LED (HUB 75 type) Matrix Panel, utilises the DMA functionality provided by the ESP32's I2S 'LCD Mode' which basically means that pixel data can be sent straight from memory, via the DMA controller, to the relevant LED Matrix GPIO pins with little CPU overhead.
This ESP32 Arduino library for an RGB LED (HUB 75 type) Matrix Panel, utilises the DMA functionality provided by the ESP32's I2S 'LCD Mode' which basically means that pixel data is sent straight from memory, via the DMA controller, to the relevant LED Matrix GPIO pins with little CPU overhead.
As a result, this library can theoretically provide ~20 bit colour, at various brightness levels without resulting in noticeable flicker.
As a result, this library can theoretically provide ~16-24 bit colour, at various brightness levels without noticeable flicker.
![It's better in real life](image.jpg)
# Installation
* Dependency: You will need to install [Adafruit_GFX_Library](https://github.com/adafruit/Adafruit-GFX-Library) from the "Library > Manage Libraries" menu.
* Download and unzip this repository into your Arduino/libraries folder (or better still, use the Arudino 'add library from .zip' option.
* Library also test to work fine with PlatformIO, install into your PlatformIO projects' lib/ folder as appropriate.
* Dependency: You will need to install [GFX_Root](https://github.com/ZinggJM/GFX_Root) from the "Library > Manage Libraries" menu.
* Download and unzip this repository into your Arduino/libraries folder (or better still, use the Arduino 'add library from .zip' option.
* Library also tested to work fine with PlatformIO, install into your PlatformIO projects' lib/ folder as appropriate.
# Wiring ESP32 with the LED Matrix Panel
@ -57,26 +57,32 @@ However, if you want to change this, simply provide the wanted pin mapping as pa
display.begin(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 ); // setup the LED matrix
```
The panel must be powered by 5V AC adapter with enough current capacity. (Current varies due to how many LED are turned on at the same time. To drive all the LEDs, you need 5V4A adapter.)
A [typical RGB panel available for purchase](https://www.aliexpress.com/item/256-128mm-64-32-pixels-1-16-Scan-Indoor-3in1-SMD2121-RGB-full-color-P4-led/32810362851.html). This is of the larger P4 (4mm spacing between pixels) type.
# Usage
## Can I chain panels or use with larger panels?
Refer to the excellent PxMatrix based library for how to wire one of these panels up (in fact, it's probably best to first get your panel working with this library first to be sure): https://github.com/2dom/PxMatrix
Yes you can. If you want to use with a 64x64 pixel panel you MUST configure a valid *E_PIN* to your ESP32 and connect it to the E pin of the HUB75 panel!
You'll need to adjust the pin configuration in this library of course.
## Can I chain displays?
This library has only been tested with a 64 pixel (wide) and 32 (high) RGB panel. Theoretically, if you want to chain two of these horizontally to make a 128x32 panel you can do so with the cable and then set the MATRIX_WIDTH to '128'.
All of this is memory permitting of course (dependant on your sketch etc.) ... An ESP32 only has about 100kB of usable DMA memory, so you will have trouble dispalying any more than 32 (height) x 128 (width) pixels.
### New Feature
# Ghosting
With version 1.1.0 of the library onwards, there is now a 'feature' to split the framebuffer over two memory 'blocks' (mallocs) to work around the fact that typically the ESP32 upon 'boot up' has 2 x ~100kb chunks of memory available to use (ideally we'd want one massive chunk, but for whatever reasons this isn't the case). This allows non-contiguous memory allocations to be joined-up potentially allowing for 512x64 resolution or greater. No guarantees however.
```
// At the top of 'ESP32-RGB64x32MatrixPanel-I2S-DMA.h':
//
// Experimental: Split the framebuffer into two smaller memory allocations.
// Can allow a bigger resolution due to the fragmented memory
// map of the typical Arduino sketch even before setup().
#define SPLIT_MEMORY_MODE 1
```
Refer to [to this](SPLIT_MEMORY_MODE.md) for more details.
## Ghosting
If you experience ghosting, you will need to reduce the brightness level, not all RGB Matrix Panels are the same - some seem to display ghosting artefacts at lower brightness levels. In the setup() function do something like:
@ -88,12 +94,11 @@ void setup() {
}
```
The value to pass 'setPanelBrightness' is the RGB Matrix's pixel width or less. i.e. 64 or lower. However values greater than 60 can cause ghosting it seems on some panels. YMMV.
The value to pass 'setPanelBrightness' is the RGB Matrix's pixel width or less. i.e. Approx. 50 or lower. Values greater than 60 can cause ghosting it seems on some panels.
## Credits
## Inspiration
* 'SmartMatrix' project code: https://github.com/pixelmatix/SmartMatrix/tree/teensylc

59
SPLIT_MEMORY_MODE.md Normal file
View file

@ -0,0 +1,59 @@
# SPLIT_MEMORY_MODE
To enable split framebuffer memory mode, uncomment line:
```
//#define SPLIT_MEMORY_MODE 1
```
in 'ESP32-RGB64x32MatrixPanel-I2S-DMA.h'
## What is it trying to resolve?
For whatever reason, and this may not be consistent across all ESP32 environments, when `heap_caps_print_heap_info(MALLOC_CAP_DMA)` is executed to print information about the available memory blocks that are DMA capable (which we need for a DMA-enabled pixel framebuffer), you may see something like this:
```
Heap summary for capabilities 0x00000008:
At 0x3ffbdb28 len 52 free 4 allocated 0 min_free 4
largest_free_block 4 alloc_blocks 0 free_blocks 1 total_blocks 1
At 0x3ffb8000 len 6688 free 5256 allocated 1208 min_free 5256
largest_free_block 5256 alloc_blocks 11 free_blocks 1 total_blocks 12
At 0x3ffb0000 len 25480 free 17172 allocated 8228 min_free 17172
largest_free_block 17172 alloc_blocks 2 free_blocks 1 total_blocks 3
At 0x3ffae6e0 len 6192 free 6092 allocated 36 min_free 6092
largest_free_block 6092 alloc_blocks 1 free_blocks 1 total_blocks 2
At 0x3ffaff10 len 240 free 0 allocated 120 min_free 0
largest_free_block 0 alloc_blocks 5 free_blocks 1 total_blocks 6
At 0x3ffb6388 len 7288 free 0 allocated 6920 min_free 0
largest_free_block 0 alloc_blocks 21 free_blocks 0 total_blocks 21
At 0x3ffb9a20 len 16648 free 6300 allocated 9680 min_free 348
largest_free_block 4980 alloc_blocks 38 free_blocks 4 total_blocks 42
At 0x3ffc1818 len 124904 free 124856 allocated 0 min_free 124856
largest_free_block 124856 alloc_blocks 0 free_blocks 1 total_blocks 1
At 0x3ffe0440 len 15072 free 15024 allocated 0 min_free 15024
largest_free_block 15024 alloc_blocks 0 free_blocks 1 total_blocks 1
At 0x3ffe4350 len 113840 free 113792 allocated 0 min_free 113792
largest_free_block 113792 alloc_blocks 0 free_blocks 1 total_blocks 1
Totals:
free 288496 allocated 26192 min_free 282544 largest_free_block 124856
```
So what? Well if you look closely, you will probably see two lines (blocks) like this:
```
At 0x3ffc1818 len 124904 free 124856 allocated 0 min_free 124856
largest_free_block 124856 alloc_blocks 0 free_blocks 1 total_blocks 1
...
At 0x3ffe4350 len 113840 free 113792 allocated 0 min_free 113792
largest_free_block 113792 alloc_blocks 0 free_blocks 1 total_blocks 1
```
What this means is there are two blocks of DMA capable memory that are about 100kB each.
The previous library was only able to use the largest single block of DMA capable memory. Now we can join the two largest together.
Given it's possible to display 128x32 with double buffering in approx. 100kB of RAM. If we use two memory blocks, and disable double buffering (which is now the default), theoretically we should be able to display 256x64 or greater resolution.
# Caveats
When SPLIT_MEMORY_MODE is enabled, the library will divide the memory framebuffer (pixel buffer) in half and split over two blocks of the same size. Therefore, if one of the free DMA memory blocks is SMALLER than 'half' the framebuffer, failure will occur.
I.e. From the above example, we could not have any half be greater than 113792 bytes.
Experimentation will be required as available memory is highly dependant on other stuff you have in your sketch. It is best to include and use the 'ESP32-RGB64x32MatrixPanel-I2S-DMA' library as early as possible in your code and analyse the serial output of `heap_caps_print_heap_info(MALLOC_CAP_DMA)` to see what DMA memory blocks are available.

View file

@ -10,7 +10,7 @@
"name": "Faptastic",
"url": "https://github.com/mrfaptastic/"
},
"version": "1.0.2",
"version": "1.1.0",
"frameworks": "arduino",
"platforms": "esp32",
"examples": [

View file

@ -1,5 +1,5 @@
name=ESP32 64x32 LED MATRIX HUB75 DMA Display
version=1.0.2
version=1.1.0
author=Faptastic
maintainer=Faptastic
sentence=Experimental DMA based LED Matrix HUB75 Library