Loading files
#include <string.h>
#include "ESP32-HUB75-MatrixPanel-I2S-DMA.h"
#if defined(SPIRAM_DMA_BUFFER)
// Sprite_TM saves the day again...
// https://www.esp32.com/viewtopic.php?f=2&t=30584
#include "rom/cache.h"
#endif
/* This replicates same function in rowBitStruct, but due to induced inlining it might be MUCH faster
* when used in tight loops while method from struct could be flushed out of instruction cache between
* loop cycles do NOT forget about buff_id param if using this.
*/
// #define getRowDataPtr(row, _dpth, buff_id) &(dma_buff.rowBits[row]->data[_dpth * dma_buff.rowBits[row]->width + buff_id*(dma_buff.rowBits[row]->width * dma_buff.rowBits[row]->colour_depth)])
// BufferID is now ignored, seperate global pointer pointer!
#define getRowDataPtr(row, _dpth) &(fb->rowBits[row]->data[_dpth * fb->rowBits[row]->width])
/* We need to update the correct uint16_t in the rowBitStruct array, that gets sent out in parallel
* 16 bit parallel mode - Save the calculated value to the bitplane memory in reverse order to account for I2S Tx FIFO mode1 ordering
* Irrelevant for ESP32-S2 the way the FIFO ordering works is different - refer to page 679 of S2 technical reference manual
*/
#if defined(ESP32_THE_ORIG)
#define ESP32_TX_FIFO_POSITION_ADJUST(x_coord) (((x_coord)&1U) ? (x_coord - 1) : (x_coord + 1))
#else
#define ESP32_TX_FIFO_POSITION_ADJUST(x_coord) x_coord
#endif
/* This library is designed to take an 8 bit / 1 byte value (0-255) for each R G B colour sub-pixel.
* The PIXEL_COLOR_DEPTH_BITS should always be '8' as a result.
* However, if the library is to be used with lower colour depth (i.e. 6 bit colour), then we need to ensure the 8-bit value passed to the colour masking
* is adjusted accordingly to ensure the LSB's are shifted left to MSB, by the difference. Otherwise the colours will be all screwed up.
*/
#define PIXEL_COLOR_MASK_BIT(color_depth_index, mask_offset) (1 << (color_depth_index + mask_offset))
// PATTERNFLOW: how many EXTRA times one row sends plane `plane`, after the pass
// that sends every plane once. This replaces upstream's 2^(i - L - 1) rule, and
// pfOEWindowPixels() further down is its other half - read that comment first.
//
// Upstream's rule cannot be made binary without throwing light away. A plane is
// on the LEDs while the NEXT buffer clocks in, a window belongs to a buffer and
// not to a pass, and under the 2^k rule the windows that the top plane needs
// wide are the same ones the planes below it need narrow. Solved exactly, that
// chain (15 buffers a row on the shipped panel) keeps 42% of the row lit.
//
// Searching every grouped order of up to 16 buffers a row for the exactly-binary
// solution that keeps the most of the row lit found this one, and nothing close:
//
// scheme 0 all planes, then D-2 once, then D-1 three times D + 4 buffers
// depth 8: 0 1 2 3 4 5 6 7 6 7 7 7 -> 66% of the row lit
// scheme 1 all planes, then D-1 once D + 1 buffers
// scheme 2 all planes D buffers
//
// Fewer buffers is also a faster refresh: 12 against 15 is 325 Hz against 260 on
// the shipped panel. Schemes 1 and 2 are the fallbacks for a row too long to
// reach min_refresh_rate with scheme 0; they are dimmer, and still binary.
// PATTERNFLOW: pad every frame out to an exact refresh rate, for cameras.
//
// A row is lit once per refresh, for 1/32 of it - a short pulse, not a glow. A
// phone's rolling shutter therefore collects a whole number of pulses per sensor
// line, and unless the exposure is an exact multiple of the refresh period some
// lines get N and some N + 1: bands. Exposure times are not arbitrary - mains
// flicker avoidance pins them to 1/50, 1/100, 1/25 or 1/60, 1/30 - and the
// lowest rate all of those are whole multiples of is 300 Hz. The 12-buffer chain
// runs at 325.5, which is close to the worst case for 1/50 (6.51 periods) and
// 1/60 (5.43). So: a run of blank words, OE off, at the end of every frame,
// bringing it to 16 MHz / 300 = 53,333 clocks. About 8% of the light, no CPU at
// all - the DMA engine walks the chain by itself - and 1/120 and faster still
// band at any rate this clock can reach (it would take 600 Hz).
// 0 turns it off. Never pads below min_refresh_rate.
#ifndef PF_TARGET_REFRESH_HZ
#define PF_TARGET_REFRESH_HZ 300
#endif
static constexpr int PF_PAD_CHUNK_WORDS = 256; // one small blank buffer, linked as often as needed
static ESP32_I2S_DMA_STORAGE_TYPE *pfPadBuffer = nullptr;
// How many blank clocks one frame needs. Pure, so check_oe.py lifts it out and
// tests it on a host. 0 means "leave the frame alone": padding is off, the target
// is not above the refresh floor, the frame is already longer than the target
// (a slower clock - there is nothing to pad toward), or the gap is more than a
// quarter of the frame, which would be a different chain's job and not padding's.
long pfFramePadWords(long clockHz, long frameWords, long targetHz, long minRefreshHz)
{
if (targetHz <= 0 || targetHz <= minRefreshHz || clockHz <= 0 || frameWords <= 0) return 0;
const long targetWords = (clockHz + targetHz / 2) / targetHz;
if (targetWords <= frameWords) return 0;
if (targetWords - frameWords >= frameWords / 4) return 0;
return targetWords - frameWords;
}
static inline int pfChainExtraPasses(int plane, int depth, int scheme)
{
if (scheme == 0 && depth >= 3) return (plane == depth - 1) ? 3 : (plane == depth - 2) ? 1 : 0;
if (scheme <= 1 && depth >= 2) return (plane == depth - 1) ? 1 : 0;
return 0;
}
bool MatrixPanel_I2S_DMA::setupDMA(const HUB75_I2S_CFG &_cfg)
{
/***
* Step 0: Allocate basic DMA framebuffer memory for the data we send out in parallel to the HUB75 panel.
* Colour depth is the only consideration.
*
*/
ESP_LOGI("I2S-DMA", "Free heap: %d", heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
ESP_LOGI("I2S-DMA", "Free SPIRAM: %d", heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
size_t allocated_fb_memory = 0;
int fbs_required = (m_cfg.double_buff) ? 2 : 1;
for (int fb = 0; fb < (fbs_required); fb++)
{
frame_buffer[fb].rowBits.reserve(ROWS_PER_FRAME);
for (int malloc_num = 0; malloc_num < ROWS_PER_FRAME; malloc_num++)
{
auto ptr = std::make_shared<rowBitStruct>(PIXELS_PER_ROW, m_cfg.getPixelColorDepthBits());
if (ptr->data == nullptr) {
ESP_LOGE("I2S-DMA", "CRITICAL ERROR: Not enough memory for requested colour depth of %d bits! Please reduce pixel_color_depth_bits value.\r\n", m_cfg.getPixelColorDepthBits());
return false;
// TODO: should we release all previous rowBitStructs here???
}
allocated_fb_memory += ptr->getColorDepthSize(false); // byte required to display all colour depths for the two parallel rows
frame_buffer[fb].rowBits.emplace_back(ptr); // save new rowBitStruct pointer into rows vector
++frame_buffer[fb].rows;
}
}
ESP_LOGI("I2S-DMA", "Allocating %d bytes memory for DMA BCM framebuffer(s).", allocated_fb_memory);
/***
* Step 1: Check what the minimum refresh rate is, and calculate the lsbMsbTransitionBit
* which is the bit at which we can reduce the colour depth to achieve the minimum refresh rate.
*
* This also determines the number of DMA descriptors required per row.
*/
//#define FORCE_COLOR_DEPTH 1
#if !defined(FORCE_COLOR_DEPTH)
ESP_LOGI("I2S-DMA", "Minimum visual refresh rate (scan rate from panel top to bottom) requested: %d Hz", m_cfg.min_refresh_rate);
// PATTERNFLOW: pick the brightest chain scheme that still clears the refresh
// floor (see pfChainExtraPasses). lsbMsbTransitionBit is no longer used.
for (pfChainScheme = 0; ; pfChainScheme++)
{
int psPerClock = 1000000000000UL / m_cfg.i2sspeed;
int nsPerLatch = ((PIXELS_PER_ROW + CLKS_DURING_LATCH) * psPerClock) / 1000; // time per row
int buffersPerRow = m_cfg.getPixelColorDepthBits();
for (int i = 0; i < m_cfg.getPixelColorDepthBits(); i++)
buffersPerRow += pfChainExtraPasses(i, m_cfg.getPixelColorDepthBits(), pfChainScheme);
int nsPerFrame = buffersPerRow * nsPerLatch * ROWS_PER_FRAME;
int actualRefreshRate = 1000000000UL / (nsPerFrame);
calculated_refresh_rate = actualRefreshRate;
ESP_LOGW("I2S-DMA", "chain scheme %d: %d buffers per row, %d Hz refresh rate.", pfChainScheme, buffersPerRow, actualRefreshRate);
if (actualRefreshRate > m_cfg.min_refresh_rate || pfChainScheme >= 2)
break;
}
// PATTERNFLOW: frame padding to PF_TARGET_REFRESH_HZ (see above).
int pfPadWords = 0;
if (m_cfg.line_decoder != HUB75_I2S_CFG::SM5266P && m_cfg.line_decoder != HUB75_I2S_CFG::SM5368)
{
int buffersPerRow = m_cfg.getPixelColorDepthBits();
for (int i = 0; i < m_cfg.getPixelColorDepthBits(); i++)
buffersPerRow += pfChainExtraPasses(i, m_cfg.getPixelColorDepthBits(), pfChainScheme);
const long frameWords = (long)buffersPerRow * (PIXELS_PER_ROW + CLKS_DURING_LATCH) * ROWS_PER_FRAME;
const long wanted = pfFramePadWords((long)m_cfg.i2sspeed, frameWords, PF_TARGET_REFRESH_HZ, m_cfg.min_refresh_rate);
if (wanted > 0)
{
if (!pfPadBuffer)
pfPadBuffer = (ESP32_I2S_DMA_STORAGE_TYPE *)heap_caps_malloc(
PF_PAD_CHUNK_WORDS * sizeof(ESP32_I2S_DMA_STORAGE_TYPE), MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA);
if (pfPadBuffer)
{
// Output off, no latch, and the address left on the last row - whose top
// plane is still waiting for the next frame's buffer 0 to light it.
const ESP32_I2S_DMA_STORAGE_TYPE blank =
(ESP32_I2S_DMA_STORAGE_TYPE)(BIT_OE | ((ROWS_PER_FRAME - 1) << BITS_ADDR_OFFSET));
for (int i = 0; i < PF_PAD_CHUNK_WORDS; i++) pfPadBuffer[i] = blank;
pfPadWords = (int)wanted;
const long total = frameWords + wanted;
calculated_refresh_rate = (int)(((long)m_cfg.i2sspeed + total / 2) / total);
ESP_LOGW("I2S-DMA", "padding each frame with %d blank clocks: %d Hz refresh rate.", pfPadWords, calculated_refresh_rate);
}
}
}
const int pfPadDescriptors = (pfPadWords + PF_PAD_CHUNK_WORDS - 1) / PF_PAD_CHUNK_WORDS;
ESP_LOGI("I2S-DMA", "DMA frame buffer color depths: %d", m_cfg.getPixelColorDepthBits());
#endif
/***
* Step 2: Calculate the DMA descriptors required, which is used for memory allocation of the DMA linked list memory structure.
* This determines the number of passes required to shift out the colour bits in the framebuffer.
* We need to also take into consderation where a chain of panels (pixels) is so long, it requires more than one DMA payload,
* give this library's DMA output memory allocation approach is by the row.
*/
int dma_descs_per_row_1cdepth = (frame_buffer[0].rowBits[0]->getColorDepthSize(true) + DMA_MAX - 1 ) / DMA_MAX;
size_t last_dma_desc_bytes_1cdepth = (frame_buffer[0].rowBits[0]->getColorDepthSize(true) % DMA_MAX);
int dma_descs_per_row_all_cdepths = (frame_buffer[0].rowBits[0]->getColorDepthSize(false) + DMA_MAX - 1 ) / DMA_MAX;
size_t last_dma_desc_bytes_all_cdepths = (frame_buffer[0].rowBits[0]->getColorDepthSize(false) % DMA_MAX);
// Logging the calculated values
ESP_LOGV("I2S-DMA", "dma_descs_per_row_1cdepth: %d", dma_descs_per_row_1cdepth);
ESP_LOGV("I2S-DMA", "last_dma_desc_bytes_1cdepth: %zu", last_dma_desc_bytes_1cdepth);
ESP_LOGV("I2S-DMA", "dma_descs_per_row_all_cdepths: %d", dma_descs_per_row_all_cdepths);
ESP_LOGV("I2S-DMA", "last_dma_desc_bytes_all_cdepths: %zu", last_dma_desc_bytes_all_cdepths);
// Calculate per-row number
int dma_descriptors_per_row = dma_descs_per_row_all_cdepths;
// Add descriptors for MSB bits after transition
for (int i = 0; i < m_cfg.getPixelColorDepthBits(); i++) {
dma_descriptors_per_row += pfChainExtraPasses(i, m_cfg.getPixelColorDepthBits(), pfChainScheme) * dma_descs_per_row_1cdepth;
}
//dma_descriptors_per_row = 1;
// Allocate DMA descriptors
int dma_descriptions_required = dma_descriptors_per_row * ROWS_PER_FRAME + pfPadDescriptors;
ESP_LOGV("I2S-DMA", "DMA descriptors per row: %d", dma_descriptors_per_row);
ESP_LOGV("I2S-DMA", "DMA descriptors required per buffer: %d", dma_descriptions_required);
/***
* Step 3: Allocate the DMA descriptor memory via. the relevant platform DMA implementation class.
*/
if (m_cfg.double_buff) {
dma_bus.enable_double_dma_desc();
}
if ( !dma_bus.allocate_dma_desc_memory(dma_descriptions_required) )
{
return false;
}
/***
* Step 4: Link up the DMA descriptors per the colour depth and rows.
*/
//fbs_required = 1; // (m_cfg.double_buff) ? 2 : 1;
for (int fb = 0; fb < (fbs_required); fb++)
{
int _dmadescriptor_count = 0; // for tracking
for (int row = 0; row < ROWS_PER_FRAME; row++)
{
//ESP_LOGV("I2S-DMA", ">>> Linking DMA descriptors for output row %d", row);
// Link and send all colour data, all passes of everything in one hit. 1 bit colour at least...
for (int dma_desc_all = 0; dma_desc_all < dma_descs_per_row_all_cdepths; dma_desc_all++)
{
size_t payload_bytes = (dma_desc_all == (dma_descs_per_row_all_cdepths-1)) ? last_dma_desc_bytes_all_cdepths:DMA_MAX;
// Log the current descriptor number and the payload size being used.
//ESP_LOGV("I2S-DMA", "Processing dma_desc_all: %d, payload_bytes: %zu, memory location: %p", dma_desc_all, payload_bytes, (frame_buffer[fb].rowBits[row]->getDataPtr(0)+(dma_desc_all*(DMA_MAX/sizeof(ESP32_I2S_DMA_STORAGE_TYPE)))));
dma_bus.create_dma_desc_link(frame_buffer[fb].rowBits[row]->getDataPtr(0)+(dma_desc_all*(DMA_MAX/sizeof(ESP32_I2S_DMA_STORAGE_TYPE))), payload_bytes, (fb==1));
_dmadescriptor_count++;
// Log the updated descriptor count after each operation.
//ESP_LOGV("I2S-DMA", "Updated _dmadescriptor_count: %d", _dmadescriptor_count);
}
// Step 2: Handle additional descriptors for bits beyond the lsbMsbTransitionBit
for (int i = 0; i < m_cfg.getPixelColorDepthBits(); 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
for (int k = 0; k < pfChainExtraPasses(i, m_cfg.getPixelColorDepthBits(), pfChainScheme); k++)
{
// Link and send all colour data, all passes of everything in one hit.
for (int dma_desc_1cdepth = 0; dma_desc_1cdepth < dma_descs_per_row_1cdepth; dma_desc_1cdepth++)
{
size_t payload_bytes = (dma_desc_1cdepth == (dma_descs_per_row_1cdepth-1)) ? last_dma_desc_bytes_1cdepth:DMA_MAX;
// Log the current bit and the corresponding payload size.
//ESP_LOGV("I2S-DMA", "Processing dma_desc_1cdepth: %d, payload_bytes: %zu, memory location: %p", dma_desc_1cdepth, payload_bytes, (frame_buffer[fb].rowBits[row]->getDataPtr(i)+(dma_desc_1cdepth*(DMA_MAX/sizeof(ESP32_I2S_DMA_STORAGE_TYPE)))));
dma_bus.create_dma_desc_link(frame_buffer[fb].rowBits[row]->getDataPtr(i)+(dma_desc_1cdepth*(DMA_MAX/sizeof(ESP32_I2S_DMA_STORAGE_TYPE))), payload_bytes, (fb==1));
_dmadescriptor_count++;
// Log the updated descriptor count after each operation.
// ESP_LOGV("I2S-DMA", "Updated _dmadescriptor_count: %d", _dmadescriptor_count);
}
} // end K
} // end all other colour depth bits
} // end all rows
// PATTERNFLOW: the blank run that brings the frame to its target length.
for (int left = pfPadWords; left > 0; left -= PF_PAD_CHUNK_WORDS)
{
const int words = (left < PF_PAD_CHUNK_WORDS) ? left : PF_PAD_CHUNK_WORDS;
dma_bus.create_dma_desc_link(pfPadBuffer, words * sizeof(ESP32_I2S_DMA_STORAGE_TYPE), (fb==1));
_dmadescriptor_count++;
}
ESP_LOGI("I2S-DMA", "Created %d DMA descriptors for buffer %d.", _dmadescriptor_count, fb);
} // end framebuffer loop
/*
#include <iostream>
#include <cmath> // For pow function (if needed, though bit shifts are better)
int main() {
int colorDepthBits = 8;
int lsbMsbTransitionBit = 0; // Assuming lsbMsbTransitionBit is 0
// Step 2: Handle additional descriptors for bits beyond the lsbMsbTransitionBit
for (int i = lsbMsbTransitionBit + 1; i < colorDepthBits; i++) {
// Calculate the number of passes required
int passes = 1 << (i - lsbMsbTransitionBit - 1);
std::cout << "Bit Level: " << i << ", Passes Required: " << passes << std::endl;
// Simulate the inner loop for the number of passes
for (int k = 0; k < passes; k++) {
std::cout << " Pass " << k + 1 << " for bit " << i << std::endl;
}
}
return 0;
}
*/
/***
* Step 5: Set default framebuffer to fb[0]
*/
fb = &frame_buffer[0];
//
// Setup DMA and Output to GPIO
//
auto bus_cfg = dma_bus.config(); // バス設定用の構造体を取得します。
bus_cfg.bus_freq = m_cfg.i2sspeed;
bus_cfg.pin_wr = m_cfg.gpio.clk;
bus_cfg.invert_pclk = m_cfg.clkphase;
bus_cfg.pin_d0 = m_cfg.gpio.r1;
bus_cfg.pin_d1 = m_cfg.gpio.g1;
bus_cfg.pin_d2 = m_cfg.gpio.b1;
bus_cfg.pin_d3 = m_cfg.gpio.r2;
bus_cfg.pin_d4 = m_cfg.gpio.g2;
bus_cfg.pin_d5 = m_cfg.gpio.b2;
bus_cfg.pin_d6 = m_cfg.gpio.lat;
bus_cfg.pin_d7 = m_cfg.gpio.oe;
bus_cfg.pin_d8 = m_cfg.gpio.a;
bus_cfg.pin_d9 = m_cfg.gpio.b;
bus_cfg.pin_d10 = m_cfg.gpio.c;
bus_cfg.pin_d11 = m_cfg.gpio.d;
bus_cfg.pin_d12 = m_cfg.gpio.e;
bus_cfg.pin_d13 = -1;
bus_cfg.pin_d14 = -1;
bus_cfg.pin_d15 = -1;
dma_bus.config(bus_cfg);
ESP_LOGI("I2S-DMA", "DMA setup completed");
initialized = true;
return true;
} // end setupDMA
/* There are 'bits' set in the frameStruct that we simply don't need to set every single time we change a pixel / DMA buffer co-ordinate.
* For example, the bits that determine the address lines, we don't need to set these every time. Once they're in place, and assuming we
* don't accidentally clear them, then we don't need to set them again.
* So to save processing, we strip this logic out to the absolute bare minimum, which is toggling only the R,G,B pixels (bits) per co-ord.
*
* Critical dependency: That 'updateMatrixDMABuffer(uint8_t red, uint8_t green, uint8_t blue)' has been run at least once over the
* entire frameBuffer to ensure all the non R,G,B bitmasks are in place (i.e. like OE, Address Lines etc.)
*
* Note: If you change the brightness with setBrightness() you MUST then clearScreen() and repaint / flush the entire framebuffer.
*/
/** @brief - Update pixel at specific co-ordinate in the DMA buffer
* this is the main method used to update DMA buffer on pixel-by-pixel level so it must be fast, real fast!
* Let's put it into IRAM to avoid situations when it could be flushed out of instruction cache
* and had to be read from spi-flash over and over again.
* Yes, it is always a tradeoff between memory/speed/size, but compared to DMA-buffer size is not a big deal
*
* Note: Cannot pass a negative co-ord as it makes no sense in the DMA bit array lookup.
*/
void IRAM_ATTR MatrixPanel_I2S_DMA::updateMatrixDMABuffer(uint16_t x_coord, uint16_t y_coord, uint8_t red, uint8_t green, uint8_t blue)
{
if (!initialized)
return;
/* 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 >= PIXELS_PER_ROW || y_coord >= m_cfg.mx_height)
{
return;
}
/* LED Brightness Compensation. Because if we do a basic "red & mask" for example,
* we'll NEVER send the dimmest possible colour, due to binary skew.
* i.e. It's almost impossible for colour_depth_idx of 0 to be sent out to the MATRIX unless the 'value' of a colour is exactly '1'
* https://ledshield.wordpress.com/2012/11/13/led-brightness-to-your-eye-gamma-correction-no/
*/
uint16_t red16, green16, blue16;
#ifdef NO_CIE1931
red16 = red;
green16 = green;
blue16 = blue;
#else
red16 = lumConvTab[red];
green16 = lumConvTab[green];
blue16 = lumConvTab[blue];
#endif
/* When using the drawPixel, we are obviously only changing the value of one x,y position,
* however, the two-scan panels paint TWO lines at the same time
* and this reflects the parallel in-DMA-memory data structure of uint16_t's that are getting
* pumped out at high speed.
*
* So we need to ensure we persist the bits (8 of them) of the uint16_t for the row we aren't changing.
*
* The DMA buffer order has also been reversed (refer to the last code in this function)
* so we have to check for this and check the correct position of the MATRIX_DATA_STORAGE_TYPE
* data.
*/
x_coord = ESP32_TX_FIFO_POSITION_ADJUST(x_coord);
uint16_t _colourbitclear = BITMASK_RGB1_CLEAR, _colourbitoffset = 0;
if (y_coord >= ROWS_PER_FRAME)
{ // if we are drawing to the bottom part of the panel
_colourbitoffset = BITS_RGB2_OFFSET;
_colourbitclear = BITMASK_RGB2_CLEAR;
y_coord -= ROWS_PER_FRAME;
}
// Iterating through colour depth bits, which we assume are 8 bits per RGB subpixel (24bpp)
uint8_t colour_depth_idx = m_cfg.getPixelColorDepthBits();
do
{
--colour_depth_idx;
#ifdef NO_CIE1931
uint16_t mask = colour_depth_idx;
#else
uint16_t mask = PIXEL_COLOR_MASK_BIT(colour_depth_idx, MASK_OFFSET);
#endif
uint16_t RGB_output_bits = 0;
/* Per the .h file, the order of the output RGB bits is:
* BIT_B2, BIT_G2, BIT_R2, BIT_B1, BIT_G1, BIT_R1 */
RGB_output_bits |= (bool)(blue16 & mask); // --B
RGB_output_bits <<= 1;
RGB_output_bits |= (bool)(green16 & mask); // -BG
RGB_output_bits <<= 1;
RGB_output_bits |= (bool)(red16 & mask); // BGR
RGB_output_bits <<= _colourbitoffset; // shift colour bits to the required position
// Get the contents at this address,
// it would represent a vector pointing to the full row of pixels for the specified colour depth bit at Y coordinate
ESP32_I2S_DMA_STORAGE_TYPE *p = getRowDataPtr(y_coord, colour_depth_idx);
// We need to update the correct uint16_t word in the rowBitStruct array pointing to a specific pixel at X - coordinate
p[x_coord] &= _colourbitclear; // reset RGB bits
p[x_coord] |= RGB_output_bits; // set new RGB bits
#if defined(SPIRAM_DMA_BUFFER)
Cache_WriteBack_Addr((uint32_t)&p[x_coord], sizeof(ESP32_I2S_DMA_STORAGE_TYPE));
#endif
} while (colour_depth_idx); // end of colour depth loop (8)
} // updateMatrixDMABuffer (specific co-ords change)
/* PATTERNFLOW ADDITION (not upstream) — see the header for why this exists. */
// The bitplane transpose, done once per channel value instead of once per
// plane per channel. A plane wants bit (d + MASK_OFFSET) of each of the six
// CIE values; the loop used to extract it with six masked tests per plane —
// about thirty-six per pixel pair, and that loop was 60% of every frame
// (presentUs 9.9 ms of 16.5 on the 128x64 panel at 6 bits). These tables
// spread the top `depth` bits of a post-LUT byte's CIE value across 6-bit
// slots, slot d holding bit d, so one OR of six table reads yields every
// plane's R1 G1 B1 R2 G2 B2 at once and each plane is then a shift and a
// mask. Planes 0-4 live in the low word (30 bits), 5-7 in the high one.
// Bit-exact with the loop it replaces: the DMA words are identical.
static uint32_t pfSpreadLo[256];
static uint32_t pfSpreadHi[256];
// lumConvTab is const, so it lives in flash and reaches the loop through the
// cache; the on-time sum reads it six times a pixel pair. A copy in DRAM.
static uint16_t pfCie[256];
static uint8_t pfSpreadDepth = 0;
// PATTERNFLOW FIX: the ceiling this encoding actually has.
//
// One 6-bit field per plane - three colours x two panel halves - packed into two
// uint32: planes 0-4 in lo at 6*d, planes 5-9 in hi at 6*(d-5). Plane 9 ends at
// bit 29 and there is no room for a tenth field. Past that it degrades quietly
// and then illegally: depth 11 needs bits 30-35 of hi and loses the top four,
// and depth 12 shifts a uint32 by 36, which is undefined behaviour in both the
// builder above and the reader in blitRGB888.
//
// HUB75_I2S_CFG clamps a requested depth to PIXEL_COLOR_DEPTH_BITS_MAX, which is
// 12, so the configuration permits what this path cannot represent. The upstream
// updateMatrixDMABuffer path does not use these tables and is unaffected, which
// is why the fix is a guard here rather than a lower MAX - fillScreen() still
// goes through it. Nothing in this firmware asks for more than 8.
static constexpr uint8_t PF_SPREAD_MAX_DEPTH = 10;
// Both halves of the panel for one column: the post-processed bytes, their
// on-time, and every plane's bits spread into (lo, hi).
#define PF_PLANES_FOR(srcT, srcB, lo, hi, onTime) \
{ \
uint8_t _t[3], _m[3]; \
PF_POST_PIXEL((srcT), _t); \
PF_POST_PIXEL((srcB), _m); \
(onTime) += (uint32_t)pfCie[_t[0]] + pfCie[_t[1]] + pfCie[_t[2]] + \
(uint32_t)pfCie[_m[0]] + pfCie[_m[1]] + pfCie[_m[2]]; \
(lo) = pfSpreadLo[_t[0]] | (pfSpreadLo[_t[1]] << 1) | (pfSpreadLo[_t[2]] << 2) | \
(pfSpreadLo[_m[0]] << 3) | (pfSpreadLo[_m[1]] << 4) | (pfSpreadLo[_m[2]] << 5); \
(hi) = pfSpreadHi[_t[0]] | (pfSpreadHi[_t[1]] << 1) | (pfSpreadHi[_t[2]] << 2) | \
(pfSpreadHi[_m[0]] << 3) | (pfSpreadHi[_m[1]] << 4) | (pfSpreadHi[_m[2]] << 5); \
}
// Two adjacent columns share one aligned 32-bit word in every plane.
#define PF_CLEAR32 (((uint32_t)BITMASK_RGB12_CLEAR << 16) | (uint32_t)BITMASK_RGB12_CLEAR)
static void pfBuildSpread(uint8_t depth)
{
const uint8_t maskOffset = 16 - depth;
for (int v = 0; v < 256; v++)
{
#ifdef NO_CIE1931
const uint16_t cie = (uint16_t)v;
#else
const uint16_t cie = lumConvTab[v];
#endif
pfCie[v] = cie;
// PATTERNFLOW FIX: round to nearest, not toward zero.
//
// Only the top `depth` bits of the 16-bit CIE value reach a plane; the low
// byte was simply dropped. Truncation is biased downward by up to half a
// plane step everywhere, and at the bottom of the range that is the whole
// signal: measured over the shipped white balance, eight input levels emitted
// nothing at all, and neighbouring neutral greys came out as opposite colour
// casts because each channel crossed its threshold at a different code. On the
// panel, level 9 read blue, level 17 cyan and level 18 red - all three from a
// neutral input. Adding half a step before the test costs nothing at runtime:
// this loop runs once per depth change, not per pixel.
//
// Computed over all 256 levels x 3 channels: dead levels 8 -> 4, level 9
// becomes (1,1,1), levels 17 and 18 both become (2,2,2), and full-scale white
// moves from (212,255,240) to (212,255,241) - so the hand-converged LED_WB_*
// constants keep their meaning. tests/blit_test.cpp carries the same rounding
// in its independent reference; the two must move together or 30M compared
// DMA words disagree.
#ifdef NO_CIE1931
const uint32_t thresholdSrc = cie;
#else
uint32_t thresholdSrc = (uint32_t)cie + (maskOffset ? (1u << (maskOffset - 1)) : 0u);
if (thresholdSrc > 0xFFFFu) thresholdSrc = 0xFFFFu;
#endif
uint32_t lo = 0, hi = 0;
for (uint8_t d = 0; d < depth; d++)
{
#ifdef NO_CIE1931
const uint16_t mask = d;
#else
const uint16_t mask = PIXEL_COLOR_MASK_BIT(d, maskOffset);
#endif
const uint32_t bit = (thresholdSrc & mask) ? 1u : 0u;
if (d < 5) lo |= bit << (6 * d);
else hi |= bit << (6 * (d - 5));
}
pfSpreadLo[v] = lo;
pfSpreadHi[v] = hi;
}
pfSpreadDepth = depth;
}
// Rec.601 luma pivot for the saturation boost, then the caller's gamma/WB LUT.
// Kept as a macro so it stays inside the pixel loop with no call overhead.
#define PF_POST_PIXEL(src, dst) { int _r = (src)[0], _g = (src)[1], _b = (src)[2]; int _y = (_r * 77 + _g * 150 + _b * 29) >> 8; _r = _y + (((_r - _y) * satBoostQ8) >> 8); _g = _y + (((_g - _y) * satBoostQ8) >> 8); _b = _y + (((_b - _y) * satBoostQ8) >> 8); if (_r < 0) _r = 0; else if (_r > 255) _r = 255; if (_g < 0) _g = 0; else if (_g > 255) _g = 255; if (_b < 0) _b = 0; else if (_b > 255) _b = 255; (dst)[0] = lutR[_r]; (dst)[1] = lutG[_g]; (dst)[2] = lutB[_b]; }
void IRAM_ATTR MatrixPanel_I2S_DMA::blitRGB888(const uint8_t *rgb,
const uint8_t *lutR, const uint8_t *lutG, const uint8_t *lutB,
int satBoostQ8)
{
if (!initialized || rgb == nullptr)
return;
const uint16_t w = PIXELS_PER_ROW;
const uint8_t depth = m_cfg.getPixelColorDepthBits();
const uint8_t rows = ROWS_PER_FRAME;
// Refuse rather than corrupt. Above PF_SPREAD_MAX_DEPTH the plane fields do not
// fit and the shifts are undefined; leaving the previous frame up and saying so
// once is diagnosable, where writing garbage into the DMA buffer is not. Cannot
// fire on any configuration this firmware ships.
if (depth > PF_SPREAD_MAX_DEPTH)
{
static bool warned = false;
if (!warned)
{
warned = true;
ESP_LOGE("I2S-DMA", "blitRGB888: colour depth %d exceeds %d, the most the "
"plane encoding can hold - frame not written", depth, PF_SPREAD_MAX_DEPTH);
}
return;
}
if (pfSpreadDepth != depth) pfBuildSpread(depth);
// Total LED on-time this frame asks for, summed as we go. The per-pixel
// linear values are computed below anyway and live in registers when we add
// them, so this rides along for six adds per pixel pair — nothing next to
// the bitplane writes underneath it. See lastFrameOnTime().
uint64_t onTime = 0;
// A two-scan panel lights row `r` and row `r + ROWS_PER_FRAME` together, and
// both live in the same uint16_t. Walk the pairs, not the rows.
for (uint8_t row = 0; row < rows; row++)
{
const uint8_t *top = rgb + (size_t)row * w * 3;
const uint8_t *bot = rgb + ((size_t)row + rows) * w * 3;
// Row pointers do not change across the pixels of a row — hoist all of
// them once instead of recomputing per pixel per plane.
ESP32_I2S_DMA_STORAGE_TYPE *plane[16];
for (uint8_t d = 0; d < depth; d++)
plane[d] = getRowDataPtr(row, d);
// Two columns per step. x and x + 1 are adjacent uint16_t words in every
// plane, so both halves of the panel for both columns — two pixel pairs —
// are one aligned 32-bit read-modify-write per plane instead of two. The
// DMA engine is reading this memory the whole time, and every access that
// has to arbitrate with it is the cost; halving them is the point.
// Which column takes the low half is the FIFO order's business, decided
// once by the macro at compile time.
const bool aLow = (ESP32_TX_FIFO_POSITION_ADJUST(0) == 0);
uint16_t x = 0;
// DMA allocation is word-aligned. With an even width, every plane and
// every two-column offset is too. Tell the compiler that fact below:
// memcpy on a uint16_t* otherwise becomes four byte loads, a stack
// spill, and four byte stores on Xtensa, not the intended word access.
// Odd widths use the scalar path since alternate planes are unaligned.
const uint16_t pairedWidth = (w & 1) ? 0 : w;
for (; x + 1 < pairedWidth; x += 2)
{
uint32_t loA, hiA, loB, hiB;
PF_PLANES_FOR(top + (size_t)x * 3, bot + (size_t)x * 3, loA, hiA, onTime);
PF_PLANES_FOR(top + (size_t)(x + 1) * 3, bot + (size_t)(x + 1) * 3, loB, hiB, onTime);
uint8_t d = depth;
while (d > 5)
{
--d;
const uint32_t bA = (hiA >> (6 * (d - 5))) & 0x3Fu;
const uint32_t bB = (hiB >> (6 * (d - 5))) & 0x3Fu;
const uint32_t both = aLow ? (bA | (bB << 16)) : (bB | (bA << 16));
ESP32_I2S_DMA_STORAGE_TYPE *p = plane[d] + x;
void *aligned = __builtin_assume_aligned(p, sizeof(uint32_t));
uint32_t word;
memcpy(&word, aligned, sizeof(word));
word = (word & PF_CLEAR32) | both;
memcpy(aligned, &word, sizeof(word));
#if defined(SPIRAM_DMA_BUFFER)
Cache_WriteBack_Addr((uint32_t)p, sizeof(word));
#endif
}
while (d)
{
--d;
const uint32_t bA = (loA >> (6 * d)) & 0x3Fu;
const uint32_t bB = (loB >> (6 * d)) & 0x3Fu;
const uint32_t both = aLow ? (bA | (bB << 16)) : (bB | (bA << 16));
ESP32_I2S_DMA_STORAGE_TYPE *p = plane[d] + x;
void *aligned = __builtin_assume_aligned(p, sizeof(uint32_t));
uint32_t word;
memcpy(&word, aligned, sizeof(word));
word = (word & PF_CLEAR32) | both;
memcpy(aligned, &word, sizeof(word));
#if defined(SPIRAM_DMA_BUFFER)
Cache_WriteBack_Addr((uint32_t)p, sizeof(word));
#endif
}
}
// Scalar fallback for odd widths, one column at a time.
for (; x < w; ++x)
{
uint32_t lo, hi;
PF_PLANES_FOR(top + (size_t)x * 3, bot + (size_t)x * 3, lo, hi, onTime);
const uint16_t idx = ESP32_TX_FIFO_POSITION_ADJUST(x);
uint8_t d = depth;
while (d > 5)
{
--d;
const uint16_t bits = (uint16_t)((hi >> (6 * (d - 5))) & 0x3Fu);
ESP32_I2S_DMA_STORAGE_TYPE *p = plane[d] + idx;
*p = (*p & BITMASK_RGB12_CLEAR) | bits;
#if defined(SPIRAM_DMA_BUFFER)
Cache_WriteBack_Addr((uint32_t)p, sizeof(ESP32_I2S_DMA_STORAGE_TYPE));
#endif
}
while (d)
{
--d;
const uint16_t bits = (uint16_t)((lo >> (6 * d)) & 0x3Fu);
ESP32_I2S_DMA_STORAGE_TYPE *p = plane[d] + idx;
*p = (*p & BITMASK_RGB12_CLEAR) | bits;
#if defined(SPIRAM_DMA_BUFFER)
Cache_WriteBack_Addr((uint32_t)p, sizeof(ESP32_I2S_DMA_STORAGE_TYPE));
#endif
}
}
}
_lastFrameOnTime = onTime;
// What an all-white frame would have summed to: every pixel, every channel,
// at the top of the CIE table. Recomputed rather than cached because panel
// geometry and colour depth are both configurable.
_maxFrameOnTime = (uint64_t)w * rows * 2ULL * 3ULL * lumConvTab[255];
} // blitRGB888
/* Update the entire buffer with a single specific colour - quicker */
void MatrixPanel_I2S_DMA::updateMatrixDMABuffer(uint8_t red, uint8_t green, uint8_t blue)
{
if (!initialized)
return;
/* https://ledshield.wordpress.com/2012/11/13/led-brightness-to-your-eye-gamma-correction-no/ */
uint16_t red16, green16, blue16;
#ifdef NO_CIE1931
red16 = red;
green16 = green;
blue16 = blue;
#else
red16 = lumConvTab[red];
green16 = lumConvTab[green];
blue16 = lumConvTab[blue];
#endif
for (uint8_t colour_depth_idx = 0; colour_depth_idx < m_cfg.getPixelColorDepthBits(); colour_depth_idx++) // colour depth - 8 iterations
{
// let's precalculate RGB1 and RGB2 bits than flood it over the entire DMA buffer
uint16_t RGB_output_bits = 0;
#ifdef NO_CIE1931
uint16_t mask = colour_depth_idx;
#else
uint16_t mask = PIXEL_COLOR_MASK_BIT(colour_depth_idx, MASK_OFFSET);
#endif
/* Per the .h file, the order of the output RGB bits is:
* BIT_B2, BIT_G2, BIT_R2, BIT_B1, BIT_G1, BIT_R1 */
RGB_output_bits |= (bool)(blue16 & mask); // --B
RGB_output_bits <<= 1;
RGB_output_bits |= (bool)(green16 & mask); // -BG
RGB_output_bits <<= 1;
RGB_output_bits |= (bool)(red16 & mask); // BGR
// Duplicate and shift across so we have have 6 populated bits of RGB1 and RGB2 pin values suitable for DMA buffer
RGB_output_bits |= RGB_output_bits << BITS_RGB2_OFFSET; // BGRBGR
// Serial.printf("Fill with: 0x%#06x\n", RGB_output_bits);
// iterate rows
int matrix_frame_parallel_row = fb->rowBits.size();
do
{
--matrix_frame_parallel_row;
// The destination for the pixel row bitstream
ESP32_I2S_DMA_STORAGE_TYPE *p = getRowDataPtr(matrix_frame_parallel_row, colour_depth_idx);
// iterate pixels in a row
int x_coord = fb->rowBits[matrix_frame_parallel_row]->width;
do
{
--x_coord;
p[x_coord] &= BITMASK_RGB12_CLEAR; // reset colour bits
p[x_coord] |= RGB_output_bits; // set new colour bits
#if defined(SPIRAM_DMA_BUFFER)
Cache_WriteBack_Addr((uint32_t)&p[x_coord], sizeof(ESP32_I2S_DMA_STORAGE_TYPE));
#endif
} while (x_coord);
} while (matrix_frame_parallel_row); // end row iteration
} // colour depth loop (8)
} // updateMatrixDMABuffer (full frame paint)
/**
* @brief - clears and reinitializes colour/control data in DMA buffs
* When allocated, DMA buffs might be dirty, so we need to blank it and initialize ABCDE,LAT,OE control bits.
* Those control bits are constants during the entire DMA sweep and never changed when updating just pixel colour data
* so we could set it once on DMA buffs initialization and forget.
* This effectively clears buffers to blank BLACK and makes it ready to display output.
* (Brightness control via OE bit manipulation is another case) - this must be done as well seperately!
*/
void MatrixPanel_I2S_DMA::clearFrameBuffer(bool _buff_id)
{
if (!initialized)
return;
frameStruct *fb = &frame_buffer[_buff_id];
// we start with iterating all rows in dma_buff structure
int row_idx = fb->rowBits.size();
do
{
--row_idx;
ESP32_I2S_DMA_STORAGE_TYPE *row = fb->rowBits[row_idx]->getDataPtr(0); // set pointer to the HEAD of a buffer holding data for the entire matrix row
ESP32_I2S_DMA_STORAGE_TYPE abcde = (ESP32_I2S_DMA_STORAGE_TYPE)row_idx;
// get last pixel index in a row of all colourdepths
int x_pixel = fb->rowBits[row_idx]->width * fb->rowBits[row_idx]->colour_depth;
abcde <<= BITS_ADDR_OFFSET; // shift row y-coord to match ABCDE bits in vector from 8 to 12
do
{
--x_pixel;
if (m_cfg.line_decoder == HUB75_I2S_CFG::SM5266P)
{
// modifications here for row shift register type SM5266P
// https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA/issues/164
row[x_pixel] = abcde & (0x18 << BITS_ADDR_OFFSET); // mask out the bottom 3 bits which are the clk di bk inputs
}
else if (m_cfg.line_decoder == HUB75_I2S_CFG::SM5368)
{
row[ESP32_TX_FIFO_POSITION_ADJUST(x_pixel)] = 0x0000;
}
else
{
row[ESP32_TX_FIFO_POSITION_ADJUST(x_pixel)] = abcde;
}
} while (x_pixel != fb->rowBits[row_idx]->width); // spare the first "width's" worth of pixels as they are the LSB pixels/colordepth
// The colour_index[0] (LSB) x_pixels must be "marked" with a previous's row address, because it is used to display
// previous row while we pump in MSBs's for the next row.
if (row_idx == 0) {
abcde = ROWS_PER_FRAME-1; // wrap around
} else {
abcde = row_idx-1;
}
abcde <<= BITS_ADDR_OFFSET; // shift row y-coord to match ABCDE bits in vector from 8 to 12
do
{
--x_pixel;
if (m_cfg.line_decoder == HUB75_I2S_CFG::SM5266P)
{
// modifications here for row shift register type SM5266P
// https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA/issues/164
row[x_pixel] = abcde & (0x18 << BITS_ADDR_OFFSET); // mask out the bottom 3 bits which are the clk di bk inputs
}
else if (m_cfg.line_decoder == HUB75_I2S_CFG::SM5368)
{
row[ESP32_TX_FIFO_POSITION_ADJUST(x_pixel)] = 0x0000;
}
else
{
row[ESP32_TX_FIFO_POSITION_ADJUST(x_pixel)] = abcde;
}
} while (x_pixel);
// modifications here for row shift register type SM5266P
// https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA/issues/164
if (m_cfg.line_decoder == HUB75_I2S_CFG::SM5266P)
{
uint16_t serialCount;
uint16_t latch;
x_pixel = fb->rowBits[row_idx]->width - 16; // come back 8*2 pixels to allow for 8 writes
serialCount = 8;
do
{
serialCount--;
latch = row[x_pixel] | (((((ESP32_I2S_DMA_STORAGE_TYPE)row_idx) % 8) == serialCount) << 1) << BITS_ADDR_OFFSET; // data on 'B'
row[x_pixel++] = latch | (0x05 << BITS_ADDR_OFFSET); // clock high on 'A'and BK high for update
row[x_pixel++] = latch | (0x04 << BITS_ADDR_OFFSET); // clock low on 'A'and BK high for update
} while (serialCount);
} // end SM5266P
// row selection for SM5368 shift regs with ABC-only addressing. A is row clk, B is BK and C is row data
if (m_cfg.line_decoder == HUB75_I2S_CFG::SM5368)
{
x_pixel = fb->rowBits[row_idx]->width - 1; // last pixel in first block)
uint16_t c = (row_idx == 0) ? BIT_C : 0x0000; // set row data (C) when row==0, then push through shift regs for all other rows
row[ESP32_TX_FIFO_POSITION_ADJUST(x_pixel - 1)] |= c | BIT_B; // set row data
row[ESP32_TX_FIFO_POSITION_ADJUST(x_pixel + 0)] |= c | BIT_A | BIT_B; // set row clk and bk, carry row data
} // end DP3246_SM5368
// let's set LAT/OE control bits for specific pixels in each colour_index subrows
// Need to consider the original ESP32's (WROOM) DMA TX FIFO reordering of bytes...
uint8_t colouridx = fb->rowBits[row_idx]->colour_depth;
do
{
--colouridx;
// switch pointer to a row for a specific colour index
row = fb->rowBits[row_idx]->getDataPtr(colouridx);
// DP3246 needs the latch high for 3 clock cycles, so start 2 cycles earlier
if (m_cfg.driver == HUB75_I2S_CFG::DP3246)
{
row[ESP32_TX_FIFO_POSITION_ADJUST(fb->rowBits[row_idx]->width - 3)] |= BIT_LAT; // DP3246 needs 3 clock cycle latch
row[ESP32_TX_FIFO_POSITION_ADJUST(fb->rowBits[row_idx]->width - 2)] |= BIT_LAT; // DP3246 needs 3 clock cycle latch
} // DP3246_SM5368
row[ESP32_TX_FIFO_POSITION_ADJUST(fb->rowBits[row_idx]->width - 1)] |= BIT_LAT; // -1 pixel to compensate array index starting at 0
// ESP32_TX_FIFO_POSITION_ADJUST(dma_buff.rowBits[row_idx]->width - 1)
// need to disable OE before/after latch to hide row transition
// Should be one clock or more before latch, otherwise can get ghosting
uint8_t _blank = m_cfg.latch_blanking;
do
{
--_blank;
row[ESP32_TX_FIFO_POSITION_ADJUST(0 + _blank)] |= BIT_OE; // disable output
row[ESP32_TX_FIFO_POSITION_ADJUST(fb->rowBits[row_idx]->width - 1)] |= BIT_OE; // disable output
row[ESP32_TX_FIFO_POSITION_ADJUST(fb->rowBits[row_idx]->width - _blank - 1)] |= BIT_OE; // (LAT pulse is (width-2) -1 pixel to compensate array index starting at 0
} while (_blank);
} while (colouridx);
#if defined(SPIRAM_DMA_BUFFER)
Cache_WriteBack_Addr((uint32_t)row, fb->rowBits[row_idx]->getColorDepthSize(false));
#endif
} while (row_idx);
}
// PATTERNFLOW FIX: how wide the OE window written into one plane's buffer is.
// Pure, so toolchain/check_oe.py can lift it out verbatim and test it on a host.
//
// THE FACT EVERYTHING HERE RESTS ON. A plane's bits are latched at the END of
// its buffer, so they are on the LEDs while the NEXT buffer in the descriptor
// chain clocks in - and it is that next buffer's OE bits that gate them.
// clearFrameBuffer() already knows this: it marks buffer 0 with the PREVIOUS
// row's address "because it is used to display previous row while we pump in
// MSBs for the next row". So the window written into buffer b does not light
// plane b. It lights whatever was latched before it, and buffer 0's window is
// the top plane's - which is what upstream's (2 * depth - colouridx) % depth was
// reaching for when it gave "the LSB" a full window.
//
// With the chain from pfChainExtraPasses(), at depth 8, scheme 0:
//
// sent 0 1 2 3 4 5 6 7 6 7 7 7 | next row's 0
// lights 7' 0 1 2 3 4 5 6 7 6 7 7 (7' = previous row's)
//
// light[p] = W[p+1] p <= 4 1/32 .. 1/2 of a row
// light[5] = W[6] 1
// light[6] = W[7] + W[7] 2
// light[7] = W[6] + W[7] + W[7] + W[0] 4
//
// so W = [1, 1/32, 1/16, 1/8, 1/4, 1/2, 1, 1] is exactly binary, every window is
// the full one shifted right, and floors keep it strictly ordered at every
// brightness (sum of floor(n / 2^j) over j >= 1 is n - popcount(n) < n). Schemes
// 1 and 2 are the same shape with one and no extra pass.
//
// TWO EARLIER VERSIONS OF THIS FUNCTION WERE WRONG, and the record matters more
// than the embarrassment. Both kept upstream's chain (at depth 8, L = 4:
// 0 1 2 3 4 5 6 7 5 6 6 7 7 7 7). 2026-09-10 read the full window on buffer 0
// as an LSB bug and shrank it, which took a fifth of the top plane's light away.
// The morning of 2026-09-17 corrected the repeat counts and kept the same
// misreading - window b lights plane b - and was worse. Both were "verified" by
// a test that modelled delivered light the same wrong way, so tens of thousands
// of assertions agreed with them. Light per plane 0-7 at full brightness:
//
// upstream 7 15 31 63 125 250 375 625 3 codes where light goes DOWN
// 09-10 7 15 31 63 125 250 375 503 3, and 127 -> 128 loses 42%
// 09-17 a.m. 9 19 39 78 78 210 355 458 15 such codes
// this 3 7 15 31 62 125 250 500 none
//
// What it looked like: a pattern whose background is a near-white (214,211,204)
// and whose edges fade to dark. Each channel crosses code 128 at a different
// point along the fade, and the one that has crossed is suddenly far brighter
// than the ones that have not - blue first, a purple fringe; then green, a
// sky-blue one; then red, and it is neutral again. Fringes round every star and
// petal. A grey test card at identity white balance cannot show this: all three
// channels cross together.
//
// Solving upstream's chain exactly was the first thing tried and was confirmed
// fringe-free on the panel, but it keeps only 42% of the row lit and was too
// dark to ship. This chain keeps 66% (upstream's, not binary: 80%).
int pfOEWindowPixels(int buffer, int depth, int scheme,
int width, int blank, int brt)
{
if (buffer < 0 || buffer >= depth) return 0;
int full = (int)(((uint32_t)(width - blank) * (uint32_t)brt) >> 8);
if (full > width) full = width;
if (full < 0) full = 0;
// The buffers whose window is the full one: buffer 0 always (the top plane's
// last pass), and every buffer that is sent again.
int firstFull = depth; // lowest buffer index >= 1 at full width
if (scheme == 0 && depth >= 3) firstFull = depth - 2;
else if (scheme <= 1 && depth >= 2) firstFull = depth - 1;
if (buffer == 0 || buffer >= firstFull) return full;
const int shift = firstFull - buffer; // 1 for the buffer just below, and so on down
return (shift < 31) ? (full >> shift) : 0;
}
void MatrixPanel_I2S_DMA::setBrightnessOE(uint8_t brt, const int _buff_id)
{
if (!initialized)
return;
frameStruct *fb = &frame_buffer[_buff_id];
uint8_t _blank = m_cfg.latch_blanking; // don't want to inadvertantly blast over this
uint8_t _depth = fb->rowBits[0]->colour_depth;
uint16_t _width = fb->rowBits[0]->width;
// One window per buffer, the same for every row - see pfOEWindowPixels above
// for which plane each one actually lights.
int windows[16] = {0};
for (int b = 0; b < _depth && b < 16; b++)
windows[b] = pfOEWindowPixels(b, _depth, pfChainScheme, _width, _blank, brt);
// start with iterating all rows in dma_buff structure
int row_idx = fb->rowBits.size();
do
{
--row_idx;
// let's set OE control bits for specific pixels in each color_index subrows
uint8_t colouridx = _depth;
do
{
--colouridx;
int brightness_in_x_pixels = windows[colouridx];
// switch pointer to a row for a specific color index
ESP32_I2S_DMA_STORAGE_TYPE *row = fb->rowBits[row_idx]->getDataPtr(colouridx);
// define range of Output Enable on the center of the row
int x_coord_max = (_width + brightness_in_x_pixels + 1) >> 1;
int x_coord_min = (_width - brightness_in_x_pixels + 0) >> 1;
int x_coord = _width;
do
{
--x_coord;
// (the check is already including "blanking" )
if (x_coord >= x_coord_min && x_coord < x_coord_max)
{
row[ESP32_TX_FIFO_POSITION_ADJUST(x_coord)] &= BITMASK_OE_CLEAR;
}
else
{
row[ESP32_TX_FIFO_POSITION_ADJUST(x_coord)] |= BIT_OE; // Disable output after this point.
}
} while (x_coord);
} while (colouridx);
#if defined(SPIRAM_DMA_BUFFER)
// Force the flush and update of the PSRAM for the memory address range of the 'row data' as
// data changes probably aren't being sent out via DMA as they're sitting in a hadrware 'cache'
ESP32_I2S_DMA_STORAGE_TYPE *row_ptr = fb->rowBits[row_idx]->getDataPtr(0);
Cache_WriteBack_Addr((uint32_t)row_ptr, fb->rowBits[row_idx]->getColorDepthSize(false));
#endif
} while (row_idx);
}
/*
* overload for compatibility
*/
bool MatrixPanel_I2S_DMA::begin(int r1, int g1, int b1, int r2, int g2, int b2, int a, int b, int c, int d, int e, int lat, int oe, int clk)
{
if (initialized)
return true;
// RGB
m_cfg.gpio.r1 = r1;
m_cfg.gpio.g1 = g1;
m_cfg.gpio.b1 = b1;
m_cfg.gpio.r2 = r2;
m_cfg.gpio.g2 = g2;
m_cfg.gpio.b2 = b2;
// Line Select
m_cfg.gpio.a = a;
m_cfg.gpio.b = b;
m_cfg.gpio.c = c;
m_cfg.gpio.d = d;
m_cfg.gpio.e = e;
// Clock & Control
m_cfg.gpio.lat = lat;
m_cfg.gpio.oe = oe;
m_cfg.gpio.clk = clk;
return begin();
}
bool MatrixPanel_I2S_DMA::begin(const HUB75_I2S_CFG &cfg)
{
if (initialized)
return true;
if (!setCfg(cfg))
return false;
return begin();
}
/**
* @brief - Sets how many clock cycles to blank OE before/after LAT signal change
* @param uint8_t pulses - clocks before/after OE
* default is DEFAULT_LAT_BLANKING
* Max is MAX_LAT_BLANKING
* @returns - new value for m_cfg.latch_blanking
*/
uint8_t MatrixPanel_I2S_DMA::setLatBlanking(uint8_t pulses)
{
if (pulses > MAX_LAT_BLANKING)
pulses = MAX_LAT_BLANKING;
if (!pulses)
pulses = DEFAULT_LAT_BLANKING;
m_cfg.latch_blanking = pulses;
// remove brightness var for now.
// setPanelBrightness(brightness); // set brightness to reset OE bits to the values matching new LAT blanking setting
return m_cfg.latch_blanking;
}
#ifndef NO_FAST_FUNCTIONS
/**
* @brief - update DMA buff drawing horizontal line at specified coordinates
* @param x_coord - line start coordinate x
* @param y_coord - line start coordinate y
* @param l - line length
* @param r,g,b, - RGB888 colour
*/
void MatrixPanel_I2S_DMA::hlineDMA(int16_t x_coord, int16_t y_coord, int16_t l, uint8_t red, uint8_t green, uint8_t blue)
{
if (!initialized)
return;
if ((x_coord + l) < 1 || y_coord < 0 || l < 1 || x_coord >= PIXELS_PER_ROW || y_coord >= m_cfg.mx_height)
return;
l = x_coord < 0 ? l + x_coord : l;
x_coord = x_coord < 0 ? 0 : x_coord;
l = ((x_coord + l) >= PIXELS_PER_ROW) ? (PIXELS_PER_ROW - x_coord) : l;
// if (x_coord+l > PIXELS_PER_ROW)
// l = PIXELS_PER_ROW - x_coord + 1; // reset width to end of row
/* LED Brightness Compensation */
uint16_t red16, green16, blue16;
#ifndef NO_CIE1931
red16 = lumConvTab[red];
green16 = lumConvTab[green];
blue16 = lumConvTab[blue];
#else
red16 = red << MASK_OFFSET;
green16 = green << MASK_OFFSET;
blue16 = blue << MASK_OFFSET;
#endif
uint16_t _colourbitclear = BITMASK_RGB1_CLEAR, _colourbitoffset = 0;
if (y_coord >= ROWS_PER_FRAME)
{ // if we are drawing to the bottom part of the panel
_colourbitoffset = BITS_RGB2_OFFSET;
_colourbitclear = BITMASK_RGB2_CLEAR;
y_coord -= ROWS_PER_FRAME;
}
// Iterating through colour depth bits (8 iterations)
uint8_t colour_depth_idx = m_cfg.getPixelColorDepthBits();
do
{
--colour_depth_idx;
// let's precalculate RGB1 and RGB2 bits than flood it over the entire DMA buffer
uint16_t RGB_output_bits = 0;
// uint8_t mask = (1 << colour_depth_idx COLOR_DEPTH_LESS_THAN_8BIT_ADJUST);
// #if PIXEL_COLOR_DEPTH_BITS < 8
// uint8_t mask = (1 << (colour_depth_idx+MASK_OFFSET)); // expect 24 bit colour (8 bits per RGB subpixel)
// #else
// uint8_t mask = (1 << (colour_depth_idx)); // expect 24 bit colour (8 bits per RGB subpixel)
// #endif
uint16_t mask = PIXEL_COLOR_MASK_BIT(colour_depth_idx, MASK_OFFSET);
/* Per the .h file, the order of the output RGB bits is:
* BIT_B2, BIT_G2, BIT_R2, BIT_B1, BIT_G1, BIT_R1 */
RGB_output_bits |= (bool)(blue16 & mask); // --B
RGB_output_bits <<= 1;
RGB_output_bits |= (bool)(green16 & mask); // -BG
RGB_output_bits <<= 1;
RGB_output_bits |= (bool)(red16 & mask); // BGR
RGB_output_bits <<= _colourbitoffset; // shift color bits to the required position
// Get the contents at this address,
// it would represent a vector pointing to the full row of pixels for the specified colour depth bit at Y coordinate
ESP32_I2S_DMA_STORAGE_TYPE *p = fb->rowBits[y_coord]->getDataPtr(colour_depth_idx);
// inlined version works slower here, dunno why :(
// ESP32_I2S_DMA_STORAGE_TYPE *p = getRowDataPtr(y_coord, colour_depth_idx, back_buffer_id);
int16_t _l = l;
do
{ // iterate pixels in a row
int16_t _x = x_coord + --_l;
/*
#if defined(ESP32_THE_ORIG)
// Save the calculated value to the bitplane memory in reverse order to account for I2S Tx FIFO mode1 ordering
uint16_t &v = p[_x & 1U ? --_x : ++_x];
#else
// ESP 32 doesn't need byte flipping for TX FIFO.
uint16_t &v = p[_x];
#endif
*/
uint16_t &v = p[ESP32_TX_FIFO_POSITION_ADJUST(_x)];
v &= _colourbitclear; // reset colour bits
v |= RGB_output_bits; // set new colour bits
} while (_l); // iterate pixels in a row
} while (colour_depth_idx); // end of colour depth loop (8)
} // hlineDMA()
/**
* @brief - update DMA buff drawing vertical line at specified coordinates
* @param x_coord - line start coordinate x
* @param y_coord - line start coordinate y
* @param l - line length
* @param r,g,b, - RGB888 colour
*/
void MatrixPanel_I2S_DMA::vlineDMA(int16_t x_coord, int16_t y_coord, int16_t l, uint8_t red, uint8_t green, uint8_t blue)
{
if (!initialized)
return;
if (x_coord < 0 || (y_coord + l) < 1 || l < 1 || x_coord >= PIXELS_PER_ROW || y_coord >= m_cfg.mx_height)
return;
l = y_coord < 0 ? l + y_coord : l;
y_coord = y_coord < 0 ? 0 : y_coord;
// check for a length that goes beyond the height of the screen! Array out of bounds dma memory changes = screwed output #163
l = ((y_coord + l) >= m_cfg.mx_height) ? (m_cfg.mx_height - y_coord) : l;
// if (y_coord + l > m_cfg.mx_height)
/// l = m_cfg.mx_height - y_coord + 1; // reset width to end of col
/* LED Brightness Compensation */
uint16_t red16, green16, blue16;
#ifndef NO_CIE1931
red16 = lumConvTab[red];
green16 = lumConvTab[green];
blue16 = lumConvTab[blue];
#else
red16 = red << MASK_OFFSET;
green16 = green << MASK_OFFSET;
blue16 = blue << MASK_OFFSET;
#endif
/*
#if defined(ESP32_THE_ORIG)
// Save the calculated value to the bitplane memory in reverse order to account for I2S Tx FIFO mode1 ordering
x_coord & 1U ? --x_coord : ++x_coord;
#endif
*/
x_coord = ESP32_TX_FIFO_POSITION_ADJUST(x_coord);
uint8_t colour_depth_idx = m_cfg.getPixelColorDepthBits();
do
{ // Iterating through colour depth bits (8 iterations)
--colour_depth_idx;
// let's precalculate RGB1 and RGB2 bits than flood it over the entire DMA buffer
// uint8_t mask = (1 << colour_depth_idx COLOR_DEPTH_LESS_THAN_8BIT_ADJUST);
// #if PIXEL_COLOR_DEPTH_BITS < 8
// uint8_t mask = (1 << (colour_depth_idx+MASK_OFFSET)); // expect 24 bit colour (8 bits per RGB subpixel)
// #else
// uint8_t mask = (1 << (colour_depth_idx)); // expect 24 bit colour (8 bits per RGB subpixel)
// #endif
uint16_t mask = PIXEL_COLOR_MASK_BIT(colour_depth_idx, MASK_OFFSET);
uint16_t RGB_output_bits = 0;
/* Per the .h file, the order of the output RGB bits is:
* BIT_B2, BIT_G2, BIT_R2, BIT_B1, BIT_G1, BIT_R1 */
RGB_output_bits |= (bool)(blue16 & mask); // --B
RGB_output_bits <<= 1;
RGB_output_bits |= (bool)(green16 & mask); // -BG
RGB_output_bits <<= 1;
RGB_output_bits |= (bool)(red16 & mask); // BGR
int16_t _l = 0, _y = y_coord;
uint16_t _colourbitclear = BITMASK_RGB1_CLEAR;
do
{ // iterate pixels in a column
if (_y >= ROWS_PER_FRAME)
{ // if y-coord overlapped bottom-half panel
_y -= ROWS_PER_FRAME;
_colourbitclear = BITMASK_RGB2_CLEAR;
RGB_output_bits <<= BITS_RGB2_OFFSET;
}
// Get the contents at this address,
// it would represent a vector pointing to the full row of pixels for the specified colour depth bit at Y coordinate
// ESP32_I2S_DMA_STORAGE_TYPE *p = getRowDataPtr(_y, colour_depth_idx, back_buffer_id);
ESP32_I2S_DMA_STORAGE_TYPE *p = fb->rowBits[_y]->getDataPtr(colour_depth_idx);
p[x_coord] &= _colourbitclear; // reset RGB bits
p[x_coord] |= RGB_output_bits; // set new RGB bits
++_y;
} while (++_l != l); // iterate pixels in a col
} while (colour_depth_idx); // end of colour depth loop (8)
} // vlineDMA()
/**
* @brief - update DMA buff drawing a rectangular at specified coordinates
* this works much faster than multiple consecutive per-pixel calls to updateMatrixDMABuffer()
* @param int16_t x, int16_t y - coordinates of a top-left corner
* @param int16_t w, int16_t h - width and height of a rectangular, min is 1 px
* @param uint8_t r - RGB888 colour
* @param uint8_t g - RGB888 colour
* @param uint8_t b - RGB888 colour
*/
void MatrixPanel_I2S_DMA::fillRectDMA(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t r, uint8_t g, uint8_t b)
{
// h-lines are >2 times faster than v-lines
// so will use it only for tall rects with h >2w
if (h > 2 * w)
{
// draw using v-lines
do
{
--w;
vlineDMA(x + w, y, h, r, g, b);
} while (w);
}
else
{
// draw using h-lines
do
{
--h;
hlineDMA(x, y + h, w, r, g, b);
} while (h);
}
}
#endif // NO_FAST_FUNCTIONS
07e931b46This file is pinned to the selected release. Download the original to open it in your design tool.