diff options
| author | mrfaptastic <12006953+mrfaptastic@users.noreply.github.com> | 2022-09-30 03:17:19 +0100 |
|---|---|---|
| committer | mrfaptastic <12006953+mrfaptastic@users.noreply.github.com> | 2022-09-30 03:17:19 +0100 |
| commit | ebe75dcaba0239d225243cdedd31aaf860abbd0a (patch) | |
| tree | fda21143906b93de687447af52c40f9329956d21 /src | |
| parent | 86063fe594cda6a572bd335e7e34af7c75226aad (diff) | |
Update to include S3 support.
Refactor tonnes of code. Double buffering not yet fully tested. PSRAM support doesn't work at all - garbled mess.
Enable in platformIO using:
build_flags =
-DSPIRAM_FRAMEBUFFER=1
Diffstat (limited to 'src')
18 files changed, 4121 insertions, 0 deletions
diff --git a/src/ESP32-HUB75-MatrixPanel-I2S-DMA.cpp b/src/ESP32-HUB75-MatrixPanel-I2S-DMA.cpp new file mode 100644 index 0000000..6d8e521 --- /dev/null +++ b/src/ESP32-HUB75-MatrixPanel-I2S-DMA.cpp @@ -0,0 +1,886 @@ +#include "ESP32-HUB75-MatrixPanel-I2S-DMA.h" + +static const char* TAG = "MatrixPanel"; + +/* 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 + * + * faptastic note oct22: struct call is not inlined... commenting out this additional compile declaration + */ +//#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]->color_depth)]) + +bool MatrixPanel_I2S_DMA::allocateDMAmemory() +{ + + // Alright, theoretically we should be OK, so let us do this, so + // lets allocate a chunk of memory for each row (a row could span multiple panels if chaining is in place) + dma_buff.rowBits.reserve(ROWS_PER_FRAME); + + // iterate through number of rows + for (int malloc_num =0; malloc_num < ROWS_PER_FRAME; ++malloc_num) + { + auto ptr = std::make_shared<rowBitStruct>(PIXELS_PER_ROW, PIXEL_COLOR_DEPTH_BITS, m_cfg.double_buff); + + if (ptr->data == nullptr) + { + #if SERIAL_DEBUG + Serial.printf_P(PSTR("ERROR: Couldn't malloc rowBitStruct %d! Critical fail.\r\n"), malloc_num); + #endif + return false; + // TODO: should we release all previous rowBitStructs here??? + } + + dma_buff.rowBits.emplace_back(ptr); // save new rowBitStruct into rows vector + ++dma_buff.rows; + } + + // 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/m_cfg.i2sspeed; + 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 = PIXEL_COLOR_DEPTH_BITS * nsPerLatch; + + // add time to shift out MSBs + 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; + int actualRefreshRate = 1000000000UL/(nsPerFrame); + calculated_refresh_rate = actualRefreshRate; + + #if SERIAL_DEBUG + Serial.printf_P(PSTR("lsbMsbTransitionBit of %d gives %d Hz refresh: \r\n"), lsbMsbTransitionBit, actualRefreshRate); + #endif + + if (actualRefreshRate > m_cfg.min_refresh_rate) + break; + + if(lsbMsbTransitionBit < PIXEL_COLOR_DEPTH_BITS - 1) + lsbMsbTransitionBit++; + else + break; + } + + + + /*** + * Step 2a: lsbMsbTransition bit is now finalised - recalculate the DMA descriptor count required, which is used for + * memory allocation of the DMA linked list memory structure. + */ + int numDMAdescriptorsPerRow = 1; + for(int i=lsbMsbTransitionBit + 1; i<PIXEL_COLOR_DEPTH_BITS; i++) { + numDMAdescriptorsPerRow += (1<<(i - lsbMsbTransitionBit - 1)); + } + #if SERIAL_DEBUG + Serial.printf_P(PSTR("Recalculated number of DMA descriptors per row: %d\n"), numDMAdescriptorsPerRow); + #endif + + // Refer to 'DMA_LL_PAYLOAD_SPLIT' code in configureDMA() below to understand why this exists. + // numDMAdescriptorsPerRow is also used to calculate descount which is super important in i2s_parallel_config_t SoC DMA setup. + if ( dma_buff.rowBits[0]->size() > DMA_MAX ) { + + #if SERIAL_DEBUG + Serial.printf_P(PSTR("rowColorDepthStruct struct is too large, split DMA payload required. Adding %d DMA descriptors\n"), PIXEL_COLOR_DEPTH_BITS-1); + #endif + + numDMAdescriptorsPerRow += PIXEL_COLOR_DEPTH_BITS-1; + // Note: If numDMAdescriptorsPerRow is even just one descriptor too large, DMA linked list will not correctly loop. + } + + + /*** + * Step 3: Allocate memory for DMA linked list, linking up each framebuffer row in sequence for GPIO output. + */ + + // malloc the DMA linked list descriptors that i2s_parallel will need + desccount = numDMAdescriptorsPerRow * ROWS_PER_FRAME; + + dma_bus.allocate_dma_desc_memory(desccount); + +/* + //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) { +#if SERIAL_DEBUG + Serial.println(F("ERROR: Could not malloc descriptor framebuffer a.")); +#endif + return false; + } + + if (m_cfg.double_buff) // 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) { +#if SERIAL_DEBUG + Serial.println(F("ERROR: Could not malloc descriptor framebuffer b.")); +#endif + return false; + } + } + */ + + // Just os we know + initialized = true; + + return true; + +} // end allocateDMAmemory() + + + +void MatrixPanel_I2S_DMA::configureDMA(const HUB75_I2S_CFG& _cfg) +{ + + // lldesc_t *previous_dmadesc_a = 0; + // lldesc_t *previous_dmadesc_b = 0; + int current_dmadescriptor_offset = 0; + + // HACK: If we need to split the payload in 1/2 so that it doesn't breach DMA_MAX, lets do it by the color_depth. + int num_dma_payload_color_depths = PIXEL_COLOR_DEPTH_BITS; + if ( dma_buff.rowBits[0]->size() > DMA_MAX ) { + num_dma_payload_color_depths = 1; + } + + // 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 row = 0; row < ROWS_PER_FRAME; row++) { + + // first set of data is LSB through MSB, single pass (IF TOTAL SIZE < DMA_MAX) - all color bits are displayed once, which takes care of everything below and including LSBMSB_TRANSITION_BIT + // NOTE: size must be less than DMA_MAX - worst case for 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, dma_buff.rowBits[row]->getDataPtr(), dma_buff.rowBits[row]->size(num_dma_payload_color_depths)); + // previous_dmadesc_a = &dmadesc_a[current_dmadescriptor_offset]; + + dma_bus.create_dma_desc_link(dma_buff.rowBits[row]->getDataPtr(), dma_buff.rowBits[row]->size(num_dma_payload_color_depths)); + + if (m_cfg.double_buff) { + dma_bus.create_dma_desc_link(dma_buff.rowBits[row]->getDataPtr(), dma_buff.rowBits[row]->size(num_dma_payload_color_depths), true); + //link_dma_desc(&dmadesc_b[current_dmadescriptor_offset], previous_dmadesc_b, dma_buff.rowBits[row]->getDataPtr(0, 1), dma_buff.rowBits[row]->size(num_dma_payload_color_depths)); + //previous_dmadesc_b = &dmadesc_b[current_dmadescriptor_offset]; + } + + current_dmadescriptor_offset++; + + // If the number of pixels per row is too great for the size of a DMA payload, so we need to split what we were going to send above. + if ( dma_buff.rowBits[0]->size() > DMA_MAX ) + { + + for (int cd = 1; cd < PIXEL_COLOR_DEPTH_BITS; cd++) + { + dma_bus.create_dma_desc_link(dma_buff.rowBits[row]->getDataPtr(cd, 0), dma_buff.rowBits[row]->size(num_dma_payload_color_depths)); + + if (m_cfg.double_buff) { + dma_bus.create_dma_desc_link(dma_buff.rowBits[row]->getDataPtr(cd, 0), dma_buff.rowBits[row]->size(num_dma_payload_color_depths),true); + //link_dma_desc(&dmadesc_b[current_dmadescriptor_offset], previous_dmadesc_b, dma_buff.rowBits[row]->getDataPtr(cd, 1), dma_buff.rowBits[row]->size(num_dma_payload_color_depths)); + //previous_dmadesc_b = &dmadesc_b[current_dmadescriptor_offset]; + } + + current_dmadescriptor_offset++; + + } // additional linked list items + } // row depth struct + + + 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 + + + for(int k=0; k < (1<<(i - lsbMsbTransitionBit - 1)); k++) + { + // link_dma_desc(&dmadesc_a[current_dmadescriptor_offset], previous_dmadesc_a, dma_buff.rowBits[row]->getDataPtr(i, 0), dma_buff.rowBits[row]->size(PIXEL_COLOR_DEPTH_BITS - i) ); +// previous_dmadesc_a = &dmadesc_a[current_dmadescriptor_offset]; + + dma_bus.create_dma_desc_link(dma_buff.rowBits[row]->getDataPtr(i, 0), dma_buff.rowBits[row]->size(PIXEL_COLOR_DEPTH_BITS - i) ); + + if (m_cfg.double_buff) { + dma_bus.create_dma_desc_link(dma_buff.rowBits[row]->getDataPtr(i, 0), dma_buff.rowBits[row]->size(PIXEL_COLOR_DEPTH_BITS - i), true ); + //link_dma_desc(&dmadesc_b[current_dmadescriptor_offset], previous_dmadesc_b, dma_buff.rowBits[row]->getDataPtr(i, 1), dma_buff.rowBits[row]->size(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.printf_P(PSTR("configureDMA(): Configured LL structure. %d DMA Linked List descriptors populated.\r\n"), current_dmadescriptor_offset); + + if ( desccount != current_dmadescriptor_offset) + { + Serial.printf_P(PSTR("configureDMA(): ERROR! Expected descriptor count of %d != actual DMA descriptors of %d!\r\n"), desccount, current_dmadescriptor_offset); + } + #endif + + //End markers for DMA LL + dmadesc_a[desccount-1].eof = 1; + dmadesc_a[desccount-1].qe.stqe_next=(lldesc_t*)&dmadesc_a[0]; + + if (m_cfg.double_buff) { + 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 + } + +#if SERIAL_DEBUG + Serial.println(F("Performing I2S setup:")); +#endif + + i2s_parallel_config_t dma_cfg = { + .gpio_bus={_cfg.gpio.r1, _cfg.gpio.g1, _cfg.gpio.b1, _cfg.gpio.r2, _cfg.gpio.g2, _cfg.gpio.b2, _cfg.gpio.lat, _cfg.gpio.oe, _cfg.gpio.a, _cfg.gpio.b, _cfg.gpio.c, _cfg.gpio.d, _cfg.gpio.e, -1, -1, -1}, + .gpio_clk=_cfg.gpio.clk, + .sample_rate=_cfg.i2sspeed, + .sample_width=ESP32_I2S_DMA_MODE, + .desccount_a=desccount, + .lldesc_a=dmadesc_a, + .desccount_b=desccount, + .lldesc_b=dmadesc_b, + .clkphase=_cfg.clkphase, + .int_ena_out_eof=_cfg.double_buff + }; + + // Setup I2S + //i2s_parallel_driver_install(ESP32_I2S_DEVICE, &dma_cfg); + +*/ + +// +// Setup DMA and Output to GPIO +// + auto bus_cfg = dma_bus.config(); // バス設定用の構造体を取得します。 + + //bus_cfg.i2s_port = I2S_NUM_0; // 使用するI2Sポートを選択 (I2S_NUM_0 or I2S_NUM_1) (ESP32のI2S LCDモードを使用します) + bus_cfg.bus_freq = _cfg.i2sspeed; + bus_cfg.pin_wr = m_cfg.gpio.clk; // WR を接続しているピン番号 + + 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); + + dma_bus.init(); + + dma_bus.dma_transfer_start(); + + + //i2s_parallel_send_dma(ESP32_I2S_DEVICE, &dmadesc_a[0]); + ESP_LOGI(TAG, "DMA setup completed"); + +} // end initMatrixDMABuff + + +/* 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 + */ +void IRAM_ATTR MatrixPanel_I2S_DMA::updateMatrixDMABuffer(int16_t x_coord, int16_t y_coord, uint8_t red, uint8_t green, uint8_t blue) +{ + if ( !initialized ) { + #if SERIAL_DEBUG + Serial.println(F("Cannot updateMatrixDMABuffer as setup failed!")); + #endif + 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 < 0 || y_coord < 0 || 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 color_depth_idx of 0 to be sent out to the MATRIX unless the 'value' of a color is exactly '1' + * https://ledshield.wordpress.com/2012/11/13/led-brightness-to-your-eye-gamma-correction-no/ + */ +#ifndef NO_CIE1931 + red = lumConvTab[red]; + green = lumConvTab[green]; + blue = 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. + */ + +#if defined (ESP32_THE_ORIG) + // 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 + x_coord & 1U ? --x_coord : ++x_coord; +#endif + + + uint16_t _colorbitclear = BITMASK_RGB1_CLEAR, _colorbitoffset = 0; + + if (y_coord >= ROWS_PER_FRAME){ // if we are drawing to the bottom part of the panel + _colorbitoffset = BITS_RGB2_OFFSET; + _colorbitclear = 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 color_depth_idx = PIXEL_COLOR_DEPTH_BITS; + do { + --color_depth_idx; +// uint8_t mask = (1 << (color_depth_idx COLOR_DEPTH_LESS_THAN_8BIT_ADJUST)); // expect 24 bit color (8 bits per RGB subpixel) + #if PIXEL_COLOR_DEPTH_BITS < 8 + uint8_t mask = (1 << (color_depth_idx+MASK_OFFSET)); // expect 24 bit color (8 bits per RGB subpixel) + #else + uint8_t mask = (1 << (color_depth_idx)); // expect 24 bit color (8 bits per RGB subpixel) + #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)(blue & mask); // --B + RGB_output_bits <<= 1; + RGB_output_bits |= (bool)(green & mask); // -BG + RGB_output_bits <<= 1; + RGB_output_bits |= (bool)(red & mask); // BGR + RGB_output_bits <<= _colorbitoffset; // 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 color depth bit at Y coordinate + //ESP32_I2S_DMA_STORAGE_TYPE *p = getRowDataPtr(y_coord, color_depth_idx, back_buffer_id); + ESP32_I2S_DMA_STORAGE_TYPE *p = dma_buff.rowBits[y_coord]->getDataPtr(color_depth_idx, back_buffer_id); + + + // We need to update the correct uint16_t word in the rowBitStruct array pointing to a specific pixel at X - coordinate + p[x_coord] &= _colorbitclear; // reset RGB bits + p[x_coord] |= RGB_output_bits; // set new RGB bits + + } while(color_depth_idx); // end of color depth loop (8) +} // updateMatrixDMABuffer (specific co-ords change) + + +/* 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/ */ +#ifndef NO_CIE1931 + red = lumConvTab[red]; + green = lumConvTab[green]; + blue = lumConvTab[blue]; +#endif + + for(uint8_t color_depth_idx=0; color_depth_idx<PIXEL_COLOR_DEPTH_BITS; color_depth_idx++) // color depth - 8 iterations + { + // 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 << color_depth_idx COLOR_DEPTH_LESS_THAN_8BIT_ADJUST); // 24 bit color + #if PIXEL_COLOR_DEPTH_BITS < 8 + uint8_t mask = (1 << (color_depth_idx+MASK_OFFSET)); // expect 24 bit color (8 bits per RGB subpixel) + #else + uint8_t mask = (1 << (color_depth_idx)); // expect 24 bit color (8 bits per RGB subpixel) + #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)(blue & mask); // --B + RGB_output_bits <<= 1; + RGB_output_bits |= (bool)(green & mask); // -BG + RGB_output_bits <<= 1; + RGB_output_bits |= (bool)(red & 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 = dma_buff.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, color_depth_idx, back_buffer_id); + ESP32_I2S_DMA_STORAGE_TYPE *p = dma_buff.rowBits[matrix_frame_parallel_row]->getDataPtr(color_depth_idx, back_buffer_id); + + // iterate pixels in a row + int x_coord=dma_buff.rowBits[matrix_frame_parallel_row]->width; + do { + --x_coord; + p[x_coord] &= BITMASK_RGB12_CLEAR; // reset color bits + p[x_coord] |= RGB_output_bits; // set new color bits + } while(x_coord); + + } while(matrix_frame_parallel_row); // end row iteration + } // colour depth loop (8) +} // updateMatrixDMABuffer (full frame paint) + +/** + * @brief - clears and reinitializes color/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 color 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) + */ +void MatrixPanel_I2S_DMA::clearFrameBuffer(bool _buff_id){ + if (!initialized) + return; + + // we start with iterating all rows in dma_buff structure + int row_idx = dma_buff.rowBits.size(); + do { + --row_idx; + + ESP32_I2S_DMA_STORAGE_TYPE* row = dma_buff.rowBits[row_idx]->getDataPtr(0, _buff_id); // 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; + abcde <<= BITS_ADDR_OFFSET; // shift row y-coord to match ABCDE bits in vector from 8 to 12 + + // get last pixel index in a row of all colordepths + int x_pixel = dma_buff.rowBits[row_idx]->width * dma_buff.rowBits[row_idx]->color_depth; + //Serial.printf(" from pixel %d, ", x_pixel); + + // fill all x_pixels except color_index[0] (LSB) ones, this also clears all color data to 0's black + do { + --x_pixel; + + if ( m_cfg.driver == 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 { + row[x_pixel] = abcde; + } + + } while(x_pixel!=dma_buff.rowBits[row_idx]->width); + + // color_index[0] (LSB) x_pixels must be "marked" with a previous's row address, 'cause it is used to display + // previous row while we pump in LSB's for a new row + abcde = ((ESP32_I2S_DMA_STORAGE_TYPE)row_idx-1) << BITS_ADDR_OFFSET; + do { + --x_pixel; + + if ( m_cfg.driver == 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 { + row[x_pixel] = abcde; + } + //row[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.driver == HUB75_I2S_CFG::SM5266P) { + uint16_t serialCount; + uint16_t latch; + x_pixel = dma_buff.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 + + + // let's set LAT/OE control bits for specific pixels in each color_index subrows + // Need to consider the original ESP32's (WROOM) DMA TX FIFO reordering of bytes... + uint8_t coloridx = dma_buff.rowBits[row_idx]->color_depth; + do { + --coloridx; + + // switch pointer to a row for a specific color index + row = dma_buff.rowBits[row_idx]->getDataPtr(coloridx, _buff_id); + + #if defined(ESP32_THE_ORIG) + // 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 + row[dma_buff.rowBits[row_idx]->width - 2] |= BIT_LAT; // -2 in the DMA array is actually -1 when it's reordered by TX FIFO + #else + // -1 works better on ESP32-S2 ? Because bytes get sent out in order... + row[dma_buff.rowBits[row_idx]->width - 1] |= BIT_LAT; // -1 pixel to compensate array index starting at 0 + #endif + + // 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; + + #if defined(ESP32_THE_ORIG) + // Original ESP32 WROOM FIFO Ordering Sucks + uint8_t _blank_row_tx_fifo_tmp = 0 + _blank; + (_blank_row_tx_fifo_tmp & 1U) ? --_blank_row_tx_fifo_tmp : ++_blank_row_tx_fifo_tmp; + row[_blank_row_tx_fifo_tmp] |= BIT_OE; + + _blank_row_tx_fifo_tmp = dma_buff.rowBits[row_idx]->width - _blank - 1; // (LAT pulse is (width-2) -1 pixel to compensate array index starting at 0 + (_blank_row_tx_fifo_tmp & 1U) ? --_blank_row_tx_fifo_tmp : ++_blank_row_tx_fifo_tmp; + row[_blank_row_tx_fifo_tmp] |= BIT_OE; + #else + row[0 + _blank] |= BIT_OE; + row[dma_buff.rowBits[row_idx]->width - _blank - 1 ] |= BIT_OE; // (LAT pulse is (width-2) -1 pixel to compensate array index starting at 0 + #endif + + } while (_blank); + + } while(coloridx); + + } while(row_idx); +} + +/** + * @brief - reset OE bits in DMA buffer in a way to control brightness + * @param brt - brightness level from 0 to row_width + * @param _buff_id - buffer id to control + */ +void MatrixPanel_I2S_DMA::brtCtrlOE(int brt, const bool _buff_id){ + if (!initialized) + return; + + if (brt > PIXELS_PER_ROW - (MAX_LAT_BLANKING + 2)) // can't control values larger than (row_width - latch_blanking) to avoid ongoing issues being raised about brightness and ghosting. + brt = PIXELS_PER_ROW - (MAX_LAT_BLANKING + 2); // +2 for a bit of buffer... + + if (brt < 0) + brt = 0; + + // start with iterating all rows in dma_buff structure + int row_idx = dma_buff.rowBits.size(); + do { + --row_idx; + + // let's set OE control bits for specific pixels in each color_index subrows + uint8_t coloridx = dma_buff.rowBits[row_idx]->color_depth; + do { + --coloridx; + + // switch pointer to a row for a specific color index + ESP32_I2S_DMA_STORAGE_TYPE* row = dma_buff.rowBits[row_idx]->getDataPtr(coloridx, _buff_id); + + int x_coord = dma_buff.rowBits[row_idx]->width; + do { + --x_coord; + + // clear OE bit for all other pixels + row[x_coord] &= BITMASK_OE_CLEAR; + + // Brightness control via OE toggle - disable matrix output at specified x_coord + if((coloridx > lsbMsbTransitionBit || !coloridx) && ((x_coord) >= brt)){ + row[x_coord] |= BIT_OE; // Disable output after this point. + continue; + } + // special case for the bits *after* LSB through (lsbMsbTransitionBit) - OE is output after data is shifted, so need to set OE to fractional brightness + if(coloridx && coloridx <= lsbMsbTransitionBit) { + // divide brightness in half for each bit below lsbMsbTransitionBit + int lsbBrightness = brt >> (lsbMsbTransitionBit - coloridx + 1); + if((x_coord) >= lsbBrightness) { + row[x_coord] |= BIT_OE; // Disable output after this point. + continue; + } + } + + + } while(x_coord); + + // 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; + + #if defined(ESP32_THE_ORIG) + // Original ESP32 WROOM FIFO Ordering Sucks + uint8_t _blank_row_tx_fifo_tmp = 0 + _blank; + (_blank_row_tx_fifo_tmp & 1U) ? --_blank_row_tx_fifo_tmp : ++_blank_row_tx_fifo_tmp; + row[_blank_row_tx_fifo_tmp] |= BIT_OE; + #else + row[0 + _blank] |= BIT_OE; + #endif + + //row[0 + _blank] |= BIT_OE; + // no need, has been done already + //row[dma_buff.rowBits[row_idx]->width - _blank - 3 ] |= BIT_OE; // (LAT pulse is (width-2) -1 pixel to compensate array index starting at 0 + } while (_blank); + + } while(coloridx); + } 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) { + + // 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(); +} + + +/** + * @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; + 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_ccord - line start coordinate x + * @param y_ccord - line start coordinate y + * @param l - line length + * @param r,g,b, - RGB888 color + */ +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 < 0 || y_coord < 0 || l < 1 || x_coord >= PIXELS_PER_ROW || y_coord >= m_cfg.mx_height) + return; + + + 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 */ +#ifndef NO_CIE1931 + red = lumConvTab[red]; + green = lumConvTab[green]; + blue = lumConvTab[blue]; +#endif + + uint16_t _colorbitclear = BITMASK_RGB1_CLEAR, _colorbitoffset = 0; + + if (y_coord >= ROWS_PER_FRAME){ // if we are drawing to the bottom part of the panel + _colorbitoffset = BITS_RGB2_OFFSET; + _colorbitclear = BITMASK_RGB2_CLEAR; + y_coord -= ROWS_PER_FRAME; + } + + // Iterating through color depth bits (8 iterations) + uint8_t color_depth_idx = PIXEL_COLOR_DEPTH_BITS; + do { + --color_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 << color_depth_idx COLOR_DEPTH_LESS_THAN_8BIT_ADJUST); + #if PIXEL_COLOR_DEPTH_BITS < 8 + uint8_t mask = (1 << (color_depth_idx+MASK_OFFSET)); // expect 24 bit color (8 bits per RGB subpixel) + #else + uint8_t mask = (1 << (color_depth_idx)); // expect 24 bit color (8 bits per RGB subpixel) + #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)(blue & mask); // --B + RGB_output_bits <<= 1; + RGB_output_bits |= (bool)(green & mask); // -BG + RGB_output_bits <<= 1; + RGB_output_bits |= (bool)(red & mask); // BGR + RGB_output_bits <<= _colorbitoffset; // 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 color depth bit at Y coordinate + ESP32_I2S_DMA_STORAGE_TYPE *p = dma_buff.rowBits[y_coord]->getDataPtr(color_depth_idx, back_buffer_id); + // inlined version works slower here, dunno why :( + // ESP32_I2S_DMA_STORAGE_TYPE *p = getRowDataPtr(y_coord, color_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 + + v &= _colorbitclear; // reset color bits + v |= RGB_output_bits; // set new color bits + } while(_l); // iterate pixels in a row + } while(color_depth_idx); // end of color depth loop (8) +} // hlineDMA() + + +/** + * @brief - update DMA buff drawing vertical line at specified coordinates + * @param x_ccord - line start coordinate x + * @param y_ccord - line start coordinate y + * @param l - line length + * @param r,g,b, - RGB888 color + */ +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 < 0 || l < 1 || x_coord >= PIXELS_PER_ROW || y_coord >= m_cfg.mx_height) + return; + + // 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 */ +#ifndef NO_CIE1931 + red = lumConvTab[red]; + green = lumConvTab[green]; + blue = lumConvTab[blue]; +#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 + + uint8_t color_depth_idx = PIXEL_COLOR_DEPTH_BITS; + do { // Iterating through color depth bits (8 iterations) + --color_depth_idx; + + // let's precalculate RGB1 and RGB2 bits than flood it over the entire DMA buffer +// uint8_t mask = (1 << color_depth_idx COLOR_DEPTH_LESS_THAN_8BIT_ADJUST); + #if PIXEL_COLOR_DEPTH_BITS < 8 + uint8_t mask = (1 << (color_depth_idx+MASK_OFFSET)); // expect 24 bit color (8 bits per RGB subpixel) + #else + uint8_t mask = (1 << (color_depth_idx)); // expect 24 bit color (8 bits per RGB subpixel) + #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)(blue & mask); // --B + RGB_output_bits <<= 1; + RGB_output_bits |= (bool)(green & mask); // -BG + RGB_output_bits <<= 1; + RGB_output_bits |= (bool)(red & mask); // BGR + + int16_t _l = 0, _y = y_coord; + uint16_t _colorbitclear = 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; + _colorbitclear = 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 color depth bit at Y coordinate + //ESP32_I2S_DMA_STORAGE_TYPE *p = getRowDataPtr(_y, color_depth_idx, back_buffer_id); + ESP32_I2S_DMA_STORAGE_TYPE *p = dma_buff.rowBits[_y]->getDataPtr(color_depth_idx, back_buffer_id); + + p[x_coord] &= _colorbitclear; // reset RGB bits + p[x_coord] |= RGB_output_bits; // set new RGB bits + ++_y; + } while(++_l!=l); // iterate pixels in a col + } while(color_depth_idx); // end of color 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 color + * @param uint8_t g - RGB888 color + * @param uint8_t b - RGB888 color + */ +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 diff --git a/src/ESP32-HUB75-MatrixPanel-I2S-DMA.h b/src/ESP32-HUB75-MatrixPanel-I2S-DMA.h new file mode 100644 index 0000000..7851a6d --- /dev/null +++ b/src/ESP32-HUB75-MatrixPanel-I2S-DMA.h @@ -0,0 +1,835 @@ +#ifndef _ESP32_RGB_64_32_MATRIX_PANEL_I2S_DMA +#define _ESP32_RGB_64_32_MATRIX_PANEL_I2S_DMA +/***************************************************************************************/ +/* Core ESP32 hardware / idf includes! */ +#include <vector> +#include <memory> +#include <Arduino.h> + + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "freertos/queue.h" + +#include "esp_heap_caps.h" +#include "platforms/platform_detect.hpp" + + +#ifdef USE_GFX_ROOT + #include <FastLED.h> + #include "GFX.h" // Adafruit GFX core class -> https://github.com/mrfaptastic/GFX_Root +#elif !defined NO_GFX + #include "Adafruit_GFX.h" // Adafruit class with all the other stuff +#endif + +/******************************************************************************************* + * COMPILE-TIME OPTIONS - MUST BE PROVIDED as part of PlatformIO project build_flags. * + * Changing the values just here won't work - as defines needs to persist beyond the scope * + * of just this file. * + *******************************************************************************************/ +/* Do NOT build additional methods optimized for fast drawing, + * i.e. Adafruits drawFastHLine, drawFastVLine, etc... */ +// #define NO_FAST_FUNCTIONS + +// #define NO_CIE1931 + +/* Physical / Chained HUB75(s) RGB pixel WIDTH and HEIGHT. + * + * This library has been tested with a 64x32 and 64x64 RGB panels. + * If you want to chain two or more of these horizontally to make a 128x32 panel + * you can do so with the cable and then set the CHAIN_LENGTH to '2'. + * + * Also, if you use a 64x64 panel, then set the MATRIX_HEIGHT to '64' and an E_PIN; it will work! + * + * All of this is memory permitting of course (dependant on your sketch etc.) ... + * + */ +#ifndef MATRIX_WIDTH + #define MATRIX_WIDTH 64 // Single panel of 64 pixel width +#endif + +#ifndef MATRIX_HEIGHT + #define MATRIX_HEIGHT 32 // CHANGE THIS VALUE to 64 IF USING 64px HIGH panel(s) with E PIN +#endif + +#ifndef CHAIN_LENGTH + #define CHAIN_LENGTH 1 // Number of modules chained together, i.e. 4 panels chained result in virtualmatrix 64x4=256 px long +#endif + + +// 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. + +/***************************************************************************************/ +/* Do not change definitions below unless you pretty sure you know what you are doing! */ + +// RGB Panel Constants / Calculated Values +#ifndef MATRIX_ROWS_IN_PARALLEL + #define MATRIX_ROWS_IN_PARALLEL 2 +#endif + +// 8bit per RGB color = 24 bit/per pixel, +// might be reduced to save DMA RAM +#ifndef PIXEL_COLOR_DEPTH_BITS + #define PIXEL_COLOR_DEPTH_BITS 8 +#endif + +#define COLOR_CHANNELS_PER_PIXEL 3 + +/***************************************************************************************/ +/* Definitions below should NOT be ever changed without rewriting library logic */ +#define ESP32_I2S_DMA_MODE 16 // From esp32_i2s_parallel_v2.h = 16 bits in parallel +#define ESP32_I2S_DMA_STORAGE_TYPE uint16_t // DMA output of one uint16_t at a time. +#define CLKS_DURING_LATCH 0 // Not (yet) used. + +// Panel Upper half RGB (numbering according to order in DMA gpio_bus configuration) +#define BITS_RGB1_OFFSET 0 // Start point of RGB_X1 bits +#define BIT_R1 (1<<0) +#define BIT_G1 (1<<1) +#define BIT_B1 (1<<2) + +// Panel Lower half RGB +#define BITS_RGB2_OFFSET 3 // Start point of RGB_X2 bits +#define BIT_R2 (1<<3) +#define BIT_G2 (1<<4) +#define BIT_B2 (1<<5) + +// Panel Control Signals +#define BIT_LAT (1<<6) +#define BIT_OE (1<<7) + +// Panel GPIO Pin Addresses (A, B, C, D etc..) +#define BITS_ADDR_OFFSET 8 // Start point of address bits +#define BIT_A (1<<8) +#define BIT_B (1<<9) +#define BIT_C (1<<10) +#define BIT_D (1<<11) +#define BIT_E (1<<12) + +// BitMasks are pre-computed based on the above #define's for performance. +#define BITMASK_RGB1_CLEAR (0b1111111111111000) // inverted bitmask for R1G1B1 bit in pixel vector +#define BITMASK_RGB2_CLEAR (0b1111111111000111) // inverted bitmask for R2G2B2 bit in pixel vector +#define BITMASK_RGB12_CLEAR (0b1111111111000000) // inverted bitmask for R1G1B1R2G2B2 bit in pixel vector +#define BITMASK_CTRL_CLEAR (0b1110000000111111) // inverted bitmask for control bits ABCDE,LAT,OE in pixel vector +#define BITMASK_OE_CLEAR (0b1111111101111111) // inverted bitmask for control bit OE in pixel vector + +// How many clock cycles to blank OE before/after LAT signal change, default is 1 clock +#define DEFAULT_LAT_BLANKING 1 +// Max clock cycles to blank OE before/after LAT signal change +#define MAX_LAT_BLANKING 4 + +/***************************************************************************************/ +// Check compile-time only options +#if PIXEL_COLOR_DEPTH_BITS > 8 + #error "Pixel color depth bits cannot be greater than 8." +#elif PIXEL_COLOR_DEPTH_BITS < 2 + #error "Pixel color depth bits cannot be less than 2." +#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. + */ +#if PIXEL_COLOR_DEPTH_BITS != 8 +static constexpr uint8_t const MASK_OFFSET = 8-PIXEL_COLOR_DEPTH_BITS; +#endif + +/***************************************************************************************/ + +/** @brief - Structure holds raw DMA data to drive TWO full rows of pixels spanning through all chained modules + * Note: sizeof(data) must be multiple of 32 bits, as ESP32 DMA linked list buffer address pointer must be word-aligned + */ +struct rowBitStruct { + const size_t width; + const uint8_t color_depth; + const bool double_buff; + ESP32_I2S_DMA_STORAGE_TYPE *data; + + /** @brief - returns size of row of data vectorfor a SINGLE buff + * size (in bytes) of a vector holding full DMA data for a row of pixels with _dpth color bits + * a SINGLE buffer only size is accounted, when using double buffers it actually takes twice as much space + * but returned size is for a half of double-buffer + * + * default - returns full data vector size for a SINGLE buff + * + */ + size_t size(uint8_t _dpth=0 ) { if (!_dpth) _dpth = color_depth; return width * _dpth * sizeof(ESP32_I2S_DMA_STORAGE_TYPE); }; + + /** @brief - returns pointer to the row's data vector beginning at pixel[0] for _dpth color bit + * default - returns pointer to the data vector's head + * NOTE: this call might be very slow in loops. Due to poor instruction caching in esp32 it might be required a reread from flash + * every loop cycle, better use inlined #define instead in such cases + */ + inline ESP32_I2S_DMA_STORAGE_TYPE* getDataPtr(const uint8_t _dpth=0, const bool buff_id=0) { return &(data[_dpth*width + buff_id*(width*color_depth)]); }; + + // constructor - allocates DMA-capable memory to hold the struct data + rowBitStruct(const size_t _width, const uint8_t _depth, const bool _dbuff) : width(_width), color_depth(_depth), double_buff(_dbuff) { + +#if defined(SPIRAM_FRAMEBUFFER) + #pragma message "Enabling PSRAM / SPIRAM for frame buffer." + data = (ESP32_I2S_DMA_STORAGE_TYPE *)heap_caps_malloc( size()+size()*double_buff, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); +#else + data = (ESP32_I2S_DMA_STORAGE_TYPE *)heap_caps_malloc( size()+size()*double_buff, MALLOC_CAP_DMA); +#endif + + } + ~rowBitStruct() { delete data;} +}; + + +/* frameStruct + * Note: A 'frameStruct' contains ALL the data for a full-frame (i.e. BOTH 2x16-row frames are + * are contained in parallel within the one uint16_t that is sent in parallel to the HUB75). + * + * This structure isn't actually allocated in one memory block anymore, as the library now allocates + * memory per row (per rowColorDepthStruct) instead. + */ +struct frameStruct { + uint8_t rows=0; // number of rows held in current frame, not used actually, just to keep the idea of struct + std::vector<std::shared_ptr<rowBitStruct> > rowBits; +}; + +/***************************************************************************************/ +//C/p'ed from https://ledshield.wordpress.com/2012/11/13/led-brightness-to-your-eye-gamma-correction-no/ +// Example calculator: https://gist.github.com/mathiasvr/19ce1d7b6caeab230934080ae1f1380e +// need to make sure this would end up in RAM for fastest access +#ifndef NO_CIE1931 +static const uint8_t DRAM_ATTR lumConvTab[]={ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 16, 16, 17, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 24, 25, 25, 26, 27, 27, 28, 28, 29, 30, 30, 31, 31, 32, 33, 33, 34, 35, 35, 36, 37, 38, 38, 39, 40, 41, 41, 42, 43, 44, 45, 45, 46, 47, 48, 49, 50, 51, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 73, 74, 75, 76, 77, 78, 80, 81, 82, 83, 84, 86, 87, 88, 90, 91, 92, 93, 95, 96, 98, 99, 100, 102, 103, 105, 106, 107, 109, 110, 112, 113, 115, 116, 118, 120, 121, 123, 124, 126, 128, 129, 131, 133, 134, 136, 138, 139, 141, 143, 145, 146, 148, 150, 152, 154, 156, 157, 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, 192, 194, 196, 198, 200, 203, 205, 207, 209, 212, 214, 216, 218, 221, 223, 226, 228, 230, 233, 235, 238, 240, 243, 245, 248, 250, 253, 255, 255}; +#endif + +/** @brief - configuration values for HUB75_I2S driver + * This structure holds configuration vars that are used as + * an initialization values when creating an instance of MatrixPanel_I2S_DMA object. + * All params have it's default values. + */ +struct HUB75_I2S_CFG { + + /** + * Enumeration of hardware-specific chips + * used to drive matrix modules + */ + enum shift_driver {SHIFTREG=0, FM6124, FM6126A, ICN2038S, MBI5124, SM5266P}; + + /** + * I2S clock speed selector + */ + enum clk_speed {HZ_8M=8000000, HZ_10M=10000000, HZ_20M=20000000}; + + // Structure Variables + + // physical width of a single matrix panel module (in pixels, usually it is 64 ;) ) + uint16_t mx_width; + // physical height of a single matrix panel module (in pixels, usually almost always it is either 32 or 64) + uint16_t mx_height; + // number of chained panels regardless of the topology, default 1 - a single matrix module + uint16_t chain_length; + + /** + * GPIO pins mapping + */ + struct i2s_pins{ + int8_t r1, g1, b1, r2, g2, b2, a, b, c, d, e, lat, oe, clk; + } gpio; + + // Matrix driver chip type - default is a plain shift register + shift_driver driver; + // I2S clock speed + clk_speed i2sspeed; + // use DMA double buffer (twice as much RAM required) + bool double_buff; + // How many clock cycles to blank OE before/after LAT signal change, default is 1 clock + uint8_t latch_blanking; + + /** + * I2S clock phase + * 0 - data lines are clocked with negative edge + * Clk /¯\_/¯\_/ + * LAT __/¯¯¯\__ + * EO ¯¯¯¯¯¯\___ + * + * 1 - data lines are clocked with positive edge (default now as of 10 June 2021) + * https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA/issues/130 + * Clk \_/¯\_/¯\ + * LAT __/¯¯¯\__ + * EO ¯¯¯¯¯¯\__ + * + */ + bool clkphase; + + // Minimum refresh / scan rate needs to be configured on start due to LSBMSB_TRANSITION_BIT calculation in allocateDMAmemory() + uint8_t min_refresh_rate; + + // struct constructor + HUB75_I2S_CFG ( + uint16_t _w = MATRIX_WIDTH, + uint16_t _h = MATRIX_HEIGHT, + uint16_t _chain = CHAIN_LENGTH, + i2s_pins _pinmap = { + R1_PIN_DEFAULT, G1_PIN_DEFAULT, B1_PIN_DEFAULT, R2_PIN_DEFAULT, G2_PIN_DEFAULT, B2_PIN_DEFAULT, + A_PIN_DEFAULT, B_PIN_DEFAULT, C_PIN_DEFAULT, D_PIN_DEFAULT, E_PIN_DEFAULT, + LAT_PIN_DEFAULT, OE_PIN_DEFAULT, CLK_PIN_DEFAULT }, + shift_driver _drv = SHIFTREG, + bool _dbuff = false, + clk_speed _i2sspeed = HZ_10M, + uint8_t _latblk = 1, // Anything > 1 seems to cause artefacts on ICS panels + bool _clockphase = true, + uint8_t _min_refresh_rate = 85 + ) : mx_width(_w), + mx_height(_h), + chain_length(_chain), + gpio(_pinmap), + driver(_drv), i2sspeed(_i2sspeed), + double_buff(_dbuff), + latch_blanking(_latblk), + clkphase(_clockphase), + min_refresh_rate (_min_refresh_rate) {} +}; // end of structure HUB75_I2S_CFG + + + +/***************************************************************************************/ +#ifdef USE_GFX_ROOT +class MatrixPanel_I2S_DMA : public GFX { +#elif !defined NO_GFX +class MatrixPanel_I2S_DMA : public Adafruit_GFX { +#else +class MatrixPanel_I2S_DMA { +#endif + + // ------- PUBLIC ------- + public: + + /** + * MatrixPanel_I2S_DMA + * + * default predefined values are used for matrix configuration + * + */ + MatrixPanel_I2S_DMA() +#ifdef USE_GFX_ROOT + : GFX(MATRIX_WIDTH, MATRIX_HEIGHT) +#elif !defined NO_GFX + : Adafruit_GFX(MATRIX_WIDTH, MATRIX_HEIGHT) +#endif + {} + + /** + * MatrixPanel_I2S_DMA + * + * @param {HUB75_I2S_CFG} opts : structure with matrix configuration + * + */ + MatrixPanel_I2S_DMA(const HUB75_I2S_CFG& opts) : +#ifdef USE_GFX_ROOT + GFX(opts.mx_width*opts.chain_length, opts.mx_height), +#elif !defined NO_GFX + Adafruit_GFX(opts.mx_width*opts.chain_length, opts.mx_height), +#endif + m_cfg(opts) {} + + /* Propagate the DMA pin configuration, allocate DMA buffs and start data output, initially blank */ + bool begin(){ + + if (initialized) return true; // we don't do this twice or more! + + // Change 'if' to '1' to enable, 0 to not include this Serial output in compiled program + #if SERIAL_DEBUG + Serial.printf_P(PSTR("Using pin %d for the R1_PIN\n"), m_cfg.gpio.r1); + Serial.printf_P(PSTR("Using pin %d for the G1_PIN\n"), m_cfg.gpio.g1); + Serial.printf_P(PSTR("Using pin %d for the B1_PIN\n"), m_cfg.gpio.b1); + Serial.printf_P(PSTR("Using pin %d for the R2_PIN\n"), m_cfg.gpio.r2); + Serial.printf_P(PSTR("Using pin %d for the G2_PIN\n"), m_cfg.gpio.g2); + Serial.printf_P(PSTR("Using pin %d for the B2_PIN\n"), m_cfg.gpio.b2); + Serial.printf_P(PSTR("Using pin %d for the A_PIN\n"), m_cfg.gpio.a); + Serial.printf_P(PSTR("Using pin %d for the B_PIN\n"), m_cfg.gpio.b); + Serial.printf_P(PSTR("Using pin %d for the C_PIN\n"), m_cfg.gpio.c); + Serial.printf_P(PSTR("Using pin %d for the D_PIN\n"), m_cfg.gpio.d); + Serial.printf_P(PSTR("Using pin %d for the E_PIN\n"), m_cfg.gpio.e); + Serial.printf_P(PSTR("Using pin %d for the LAT_PIN\n"), m_cfg.gpio.lat); + Serial.printf_P(PSTR("Using pin %d for the OE_PIN\n"), m_cfg.gpio.oe); + Serial.printf_P(PSTR("Using pin %d for the CLK_PIN\n"), m_cfg.gpio.clk); + #endif + + // initialize some specific panel drivers + if (m_cfg.driver) + shiftDriver(m_cfg); + + + /* As DMA buffers are dynamically allocated, we must allocated in begin() + * Ref: https://github.com/espressif/arduino-esp32/issues/831 + */ + 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. + resetbuffers(); // Must fill the DMA buffer with the initial output bit sequence or the panel will display garbage + + // Setup the ESP32 DMA Engine. Sprite_TM built this stuff. + configureDMA(m_cfg); //DMA and I2S configuration and setup + + //showDMABuffer(); // show backbuf_id of 0 + + #if SERIAL_DEBUG + if (!initialized) + Serial.println(F("MatrixPanel_I2S_DMA::begin() failed.")); + #endif + + return initialized; + + } + + // Obj destructor + ~MatrixPanel_I2S_DMA(){ + + dma_bus.release(); + + } + + + /* + * overload for compatibility + */ + bool begin(int r1, int g1 = G1_PIN_DEFAULT, int b1 = B1_PIN_DEFAULT, int r2 = R2_PIN_DEFAULT, int g2 = G2_PIN_DEFAULT, int b2 = B2_PIN_DEFAULT, int a = A_PIN_DEFAULT, int b = B_PIN_DEFAULT, int c = C_PIN_DEFAULT, int d = D_PIN_DEFAULT, int e = E_PIN_DEFAULT, int lat = LAT_PIN_DEFAULT, int oe = OE_PIN_DEFAULT, int clk = CLK_PIN_DEFAULT); + + + // Adafruit's BASIC DRAW API (565 colour format) + virtual void drawPixel(int16_t x, int16_t y, uint16_t color); // overwrite adafruit implementation + virtual void fillScreen(uint16_t color); // overwrite adafruit implementation + + /** + * A wrapper to fill whatever selected DMA buffer / screen with black + */ + inline void clearScreen(){ updateMatrixDMABuffer(0,0,0); }; + +#ifndef NO_FAST_FUNCTIONS + /** + * @brief - override Adafruit's FastVLine + * this works faster than multiple consecutive pixel by pixel drawPixel() call + */ + virtual void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color){ + uint8_t r, g, b; + color565to888(color, r, g, b); + vlineDMA(x, y, h, r, g, b); + } + // rgb888 overload + virtual inline void drawFastVLine(int16_t x, int16_t y, int16_t h, uint8_t r, uint8_t g, uint8_t b){ vlineDMA(x, y, h, r, g, b); }; + + /** + * @brief - override Adafruit's FastHLine + * this works faster than multiple consecutive pixel by pixel drawPixel() call + */ + virtual void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color){ + uint8_t r, g, b; + color565to888(color, r, g, b); + hlineDMA(x, y, w, r, g, b); + } + // rgb888 overload + virtual inline void drawFastHLine(int16_t x, int16_t y, int16_t w, uint8_t r, uint8_t g, uint8_t b){ hlineDMA(x, y, w, r, g, b); }; + + /** + * @brief - override Adafruit's fillRect + * this works much faster than multiple consecutive per-pixel drawPixel() calls + */ + virtual void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color){ + uint8_t r, g, b; + color565to888(color, r, g, b); + fillRectDMA(x, y, w, h, r, g, b); + } + // rgb888 overload + virtual inline void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t r, uint8_t g, uint8_t b){fillRectDMA(x, y, w, h, r, g, b);} +#endif + + void fillScreenRGB888(uint8_t r, uint8_t g, uint8_t b); + void drawPixelRGB888(int16_t x, int16_t y, uint8_t r, uint8_t g, uint8_t b); + +#ifdef USE_GFX_ROOT + // 24bpp FASTLED CRGB colour struct support + void fillScreen(CRGB color); + void drawPixel(int16_t x, int16_t y, CRGB color); +#endif + + void drawIcon (int *ico, int16_t x, int16_t y, int16_t cols, int16_t rows); + + // Color 444 is a 4 bit scale, so 0 to 15, color 565 takes a 0-255 bit value, so scale up by 255/15 (i.e. 17)! + static uint16_t color444(uint8_t r, uint8_t g, uint8_t b) { return color565(r*17,g*17,b*17); } + + // Converts RGB888 to RGB565 + static uint16_t color565(uint8_t r, uint8_t g, uint8_t b); // This is what is used by Adafruit GFX! + + // Converts RGB333 to RGB565 + static uint16_t color333(uint8_t r, uint8_t g, uint8_t b); // This is what is used by Adafruit GFX! Not sure why they have a capital 'C' for this particular function. + + /** + * @brief - convert RGB565 to RGB888 + * @param uint16_t color - RGB565 input color + * @param uint8_t &r, &g, &b - refs to variables where converted colors would be emplaced + */ + static void color565to888(const uint16_t color, uint8_t &r, uint8_t &g, uint8_t &b); + + + inline void IRAM_ATTR flipDMABuffer() + { + if ( !m_cfg.double_buff) return; + + #if SERIAL_DEBUG + Serial.printf_P(PSTR("Set back buffer to: %d\n"), back_buffer_id); + #endif + + dma_bus.flip_dma_output_buffer(); + + /* + i2s_parallel_set_previous_buffer_not_free(); + // Wait before we allow any writing to the buffer. Stop flicker. + while(i2s_parallel_is_previous_buffer_free() == false) { } + + i2s_parallel_flip_to_buffer(ESP32_I2S_DEVICE, back_buffer_id); + // Flip to other buffer as the backbuffer. + // i.e. Graphic changes happen to this buffer, but aren't displayed until flipDMABuffer() is called again. + back_buffer_id ^= 1; + + i2s_parallel_set_previous_buffer_not_free(); + // Wait before we allow any writing to the buffer. Stop flicker. + while(i2s_parallel_is_previous_buffer_free() == false) { } + */ + + back_buffer_id ^= 1; + + + + + } + + inline void setPanelBrightness(int b) + { + // Change to set the brightness of the display, range of 1 to matrixWidth (i.e. 1 - 64) + brightness = b; + if (!initialized) + return; + + brtCtrlOE(b); + if (m_cfg.double_buff) + brtCtrlOE(b, 1); + } + + /** + * this is just a wrapper to control brightness + * with an 8-bit value (0-255), very popular in FastLED-based sketches :) + * @param uint8_t b - 8-bit brightness value + */ + void setBrightness8(const uint8_t b) + { + setPanelBrightness(b * PIXELS_PER_ROW / 256); + } + + /** + * Contains the resulting refresh rate (scan rate) that will be achieved + * based on the i2sspeed, colour depth and min_refresh_rate requested. + */ + int calculated_refresh_rate = 0; + + /** + * @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 setLatBlanking(uint8_t pulses); + + /** + * Get a class configuration struct + * + */ + const HUB75_I2S_CFG& getCfg() const {return m_cfg;}; + + + /** + * Stop the ESP32 DMA Engine. Screen will forever be black until next ESP reboot. + */ + void stopDMAoutput() { + resetbuffers(); + //i2s_parallel_stop_dma(ESP32_I2S_DEVICE); + dma_bus.dma_transfer_stop(); + } + + + + // ------- PROTECTED ------- + // those might be useful for child classes, like VirtualMatrixPanel + protected: + + Bus_Parallel16 dma_bus; + + /** + * @brief - clears and reinitializes color/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 color 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) + */ + void clearFrameBuffer(bool _buff_id = 0); + + /* 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) */ + void updateMatrixDMABuffer(uint8_t red, uint8_t green, uint8_t blue); + + /** + * wipes DMA buffer(s) and reset all color/service bits + */ + inline void resetbuffers(){ + clearFrameBuffer(); + brtCtrlOE(brightness); + if (m_cfg.double_buff){ + clearFrameBuffer(1); + brtCtrlOE(brightness, 1); + } + } + + +#ifndef NO_FAST_FUNCTIONS + /** + * @brief - update DMA buff drawing horizontal line at specified coordinates + * @param x_ccord - line start coordinate x + * @param y_ccord - line start coordinate y + * @param l - line length + * @param r,g,b, - RGB888 color + */ + void hlineDMA(int16_t x_coord, int16_t y_coord, int16_t l, uint8_t red, uint8_t green, uint8_t blue); + + /** + * @brief - update DMA buff drawing horizontal line at specified coordinates + * @param x_ccord - line start coordinate x + * @param y_ccord - line start coordinate y + * @param l - line length + * @param r,g,b, - RGB888 color + */ + void vlineDMA(int16_t x_coord, int16_t y_coord, int16_t l, uint8_t red, uint8_t green, uint8_t blue); + + /** + * @brief - update DMA buff drawing a rectangular at specified coordinates + * uses Fast H/V line draw internally, works faster than multiple consecutive pixel by 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 color + * @param uint8_t g - RGB888 color + * @param uint8_t b - RGB888 color + */ + void fillRectDMA(int16_t x_coord, int16_t y_coord, int16_t w, int16_t h, uint8_t r, uint8_t g, uint8_t b); +#endif + + // ------- PRIVATE ------- + private: + + // Matrix i2s settings + HUB75_I2S_CFG m_cfg; + + /* ESP32-HUB75-MatrixPanel-I2S-DMA functioning constants + * we can't change those once object instance initialized it's DMA structs + */ + const uint8_t ROWS_PER_FRAME = m_cfg.mx_height / MATRIX_ROWS_IN_PARALLEL; // RPF - rows per frame, either 16 or 32 depending on matrix module + const uint16_t PIXELS_PER_ROW = m_cfg.mx_width * m_cfg.chain_length; // number of pixels in a single row of all chained matrix modules (WIDTH of a combined matrix chain) + + // Other private variables + bool initialized = false; + 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 lsbMsbTransitionBit = 0; // For colour depth calculations + + + // *** DMA FRAMEBUFFER structures + + // ESP 32 DMA Linked List descriptor + int desccount = 0; + // lldesc_t * dmadesc_a = {0}; + // lldesc_t * dmadesc_b = {0}; + + /* 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) + * Memory is allocated (malloc'd) by the row, and not in one massive chunk, for flexibility. + * The whole DMA framebuffer is just a vector of pointers to structs with ESP32_I2S_DMA_STORAGE_TYPE arrays + * Since it's dimensions is unknown prior to class initialization, we just declare it here as empty struct and will do all allocations later. + * Refer to rowBitStruct to get the idea of it's internal structure + */ + frameStruct dma_buff; + + + /* 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(const HUB75_I2S_CFG& opts); + + /** + * pre-init procedures for specific drivers + * + */ + void shiftDriver(const HUB75_I2S_CFG& opts); + + /** + * @brief - FM6124-family chips initialization routine + */ + void fm6124init(const HUB75_I2S_CFG& _cfg); + + /** + * @brief - reset OE bits in DMA buffer in a way to control brightness + * @param brt - brightness level from 0 to row_width + * @param _buff_id - buffer id to control + */ + void brtCtrlOE(int brt, const bool _buff_id=0); + + +}; // end Class header + +/***************************************************************************************/ +// https://stackoverflow.com/questions/5057021/why-are-c-inline-functions-in-the-header +/* 2. functions declared in the header must be marked inline because otherwise, every translation unit which includes the header will contain a definition of the function, and the linker will complain about multiple definitions (a violation of the One Definition Rule). The inline keyword suppresses this, allowing multiple translation units to contain (identical) definitions. */ + +/** + * @brief - convert RGB565 to RGB888 + * @param uint16_t color - RGB565 input color + * @param uint8_t &r, &g, &b - refs to variables where converted colours would be emplaced + */ +inline void MatrixPanel_I2S_DMA::color565to888(const uint16_t color, uint8_t &r, uint8_t &g, uint8_t &b){ + r = ((((color >> 11) & 0x1F) * 527) + 23) >> 6; + g = ((((color >> 5) & 0x3F) * 259) + 33) >> 6; + b = (((color & 0x1F) * 527) + 23) >> 6; +} + +inline void MatrixPanel_I2S_DMA::drawPixel(int16_t x, int16_t y, uint16_t color) // adafruit virtual void override +{ + uint8_t r,g,b; + color565to888(color,r,g,b); + + updateMatrixDMABuffer( x, y, r, g, b); +} + +inline void MatrixPanel_I2S_DMA::fillScreen(uint16_t color) // adafruit virtual void override +{ + uint8_t r,g,b; + color565to888(color,r,g,b); + + updateMatrixDMABuffer(r, g, b); // RGB only (no pixel coordinate) version of 'updateMatrixDMABuffer' +} + +inline void MatrixPanel_I2S_DMA::drawPixelRGB888(int16_t x, int16_t y, uint8_t r, uint8_t g,uint8_t b) +{ + updateMatrixDMABuffer( x, y, r, g, b); +} + +inline void MatrixPanel_I2S_DMA::fillScreenRGB888(uint8_t r, uint8_t g,uint8_t b) +{ + updateMatrixDMABuffer(r, g, b); // RGB only (no pixel coordinate) version of 'updateMatrixDMABuffer' +} + +#ifdef USE_GFX_ROOT +// Support for CRGB values provided via FastLED +inline void MatrixPanel_I2S_DMA::drawPixel(int16_t x, int16_t y, CRGB color) +{ + updateMatrixDMABuffer( x, y, color.red, color.green, color.blue); +} + +inline void MatrixPanel_I2S_DMA::fillScreen(CRGB color) +{ + updateMatrixDMABuffer(color.red, color.green, color.blue); +} +#endif + + +// 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 MatrixPanel_I2S_DMA::color565(uint8_t r, uint8_t g, uint8_t b) { + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); +} + +// Promote 3/3/3 RGB to Adafruit_GFX 5/6/5 RRRrrGGGgggBBBbb +inline uint16_t MatrixPanel_I2S_DMA::color333(uint8_t r, uint8_t g, uint8_t b) { + return ((r & 0x7) << 13) | ((r & 0x6) << 10) | ((g & 0x7) << 8) | ((g & 0x7) << 5) | ((b & 0x7) << 2) | ((b & 0x6) >> 1); +} + +inline void MatrixPanel_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, + }; + + MatrixPanel_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++) { + drawPixel (x + j, y + i, (uint16_t) ico[i * cols + j]); + } + } +} + + +#endif + + + +// 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 + +/* + + 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 artefacts 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 front buffer/back buffer technique here to make sure the display is refreshed in one go and drawing artefacts do not reach the display. + In practice, for small displays this is not really necessarily. + +*/ diff --git a/src/ESP32-HUB75-MatrixPanel-leddrivers.cpp b/src/ESP32-HUB75-MatrixPanel-leddrivers.cpp new file mode 100644 index 0000000..bb482a2 --- /dev/null +++ b/src/ESP32-HUB75-MatrixPanel-leddrivers.cpp @@ -0,0 +1,94 @@ +/* + Various LED Driver chips might need some specific code for initialisation/control logic + +*/ + +#include <Arduino.h> +#include "ESP32-HUB75-MatrixPanel-I2S-DMA.h" + +#define CLK_PULSE digitalWrite(_cfg.gpio.clk, HIGH); digitalWrite(_cfg.gpio.clk, LOW); + +/** + * @brief - pre-init procedures for specific led-drivers + * this method is called before DMA/I2S setup while GPIOs + * aint yet assigned for DMA operation + * + */ +void MatrixPanel_I2S_DMA::shiftDriver(const HUB75_I2S_CFG& _cfg){ + switch (_cfg.driver){ + case HUB75_I2S_CFG::ICN2038S: + case HUB75_I2S_CFG::FM6124: + case HUB75_I2S_CFG::FM6126A: + fm6124init(_cfg); + break; + case HUB75_I2S_CFG::MBI5124: + /* MBI5124 chips must be clocked with positive-edge, since it's LAT signal + * resets on clock's rising edge while high + * https://github.com/mrfaptastic/ESP32-HUB75-MatrixPanel-I2S-DMA/files/5952216/5a542453754da.pdf + */ + m_cfg.clkphase=true; + break; + case HUB75_I2S_CFG::SHIFTREG: + default: + break; + } +} + + +void MatrixPanel_I2S_DMA::fm6124init(const HUB75_I2S_CFG& _cfg){ + #if SERIAL_DEBUG + Serial.println( F("MatrixPanel_I2S_DMA - initializing FM6124 driver...")); + #endif + bool REG1[16] = {0,0,0,0,0, 1,1,1,1,1,1, 0,0,0,0,0}; // this sets global matrix brightness power + bool REG2[16] = {0,0,0,0,0, 0,0,0,0,1,0, 0,0,0,0,0}; // a single bit enables the matrix output + + for (uint8_t _pin:{_cfg.gpio.r1, _cfg.gpio.r2, _cfg.gpio.g1, _cfg.gpio.g2, _cfg.gpio.b1, _cfg.gpio.b2, _cfg.gpio.clk, _cfg.gpio.lat, _cfg.gpio.oe}){ + pinMode(_pin, OUTPUT); + digitalWrite(_pin, LOW); + } + + digitalWrite(_cfg.gpio.oe, HIGH); // Disable Display + + // Send Data to control register REG1 + // this sets the matrix brightness actually + for (int l = 0; l < PIXELS_PER_ROW; l++){ + for (uint8_t _pin:{_cfg.gpio.r1, _cfg.gpio.r2, _cfg.gpio.g1, _cfg.gpio.g2, _cfg.gpio.b1, _cfg.gpio.b2}) + digitalWrite(_pin, REG1[l%16]); // we have 16 bits shifters and write the same value all over the matrix array + + if (l > PIXELS_PER_ROW - 12){ // pull the latch 11 clocks before the end of matrix so that REG1 starts counting to save the value + digitalWrite(_cfg.gpio.lat, HIGH); + } + CLK_PULSE + } + + // drop the latch and save data to the REG1 all over the FM6124 chips + digitalWrite(_cfg.gpio.lat, LOW); + + // Send Data to control register REG2 (enable LED output) + for (int l = 0; l < PIXELS_PER_ROW; l++){ + for (uint8_t _pin:{_cfg.gpio.r1, _cfg.gpio.r2, _cfg.gpio.g1, _cfg.gpio.g2, _cfg.gpio.b1, _cfg.gpio.b2}) + digitalWrite(_pin, REG2[l%16]); // we have 16 bits shifters and we write the same value all over the matrix array + + if (l > PIXELS_PER_ROW - 13){ // pull the latch 12 clocks before the end of matrix so that reg2 stars counting to save the value + digitalWrite(_cfg.gpio.lat, HIGH); + } + CLK_PULSE + } + + // drop the latch and save data to the REG1 all over the FM6126 chips + digitalWrite(_cfg.gpio.lat, LOW); + + // blank data regs to keep matrix clear after manipulations + for (uint8_t _pin:{_cfg.gpio.r1, _cfg.gpio.r2, _cfg.gpio.g1, _cfg.gpio.g2, _cfg.gpio.b1, _cfg.gpio.b2}) + digitalWrite(_pin, LOW); + + for (int l = 0; l < PIXELS_PER_ROW; ++l){ + CLK_PULSE + } + + digitalWrite(_cfg.gpio.lat, HIGH); + CLK_PULSE + digitalWrite(_cfg.gpio.lat, LOW); + digitalWrite(_cfg.gpio.oe, LOW); // Enable Display + CLK_PULSE +}
\ No newline at end of file diff --git a/src/ESP32-VirtualMatrixPanel-I2S-DMA.h b/src/ESP32-VirtualMatrixPanel-I2S-DMA.h new file mode 100644 index 0000000..b2aba42 --- /dev/null +++ b/src/ESP32-VirtualMatrixPanel-I2S-DMA.h @@ -0,0 +1,355 @@ +#ifndef _ESP32_VIRTUAL_MATRIX_PANEL_I2S_DMA +#define _ESP32_VIRTUAL_MATRIX_PANEL_I2S_DMA + +/******************************************************************* + Class contributed by Brian Lough, and expanded by Faptastic. + + Originally designed to allow CHAINING of panels together to create + a 'bigger' display of panels. i.e. Chaining 4 panels into a 2x2 + grid. + + However, the function of this class has expanded now to also manage + the output for 1/16 scan panels, as the core DMA library is designed + ONLY FOR 1/16 scan matrix panels. + + YouTube: https://www.youtube.com/brianlough + Tindie: https://www.tindie.com/stores/brianlough/ + Twitter: https://twitter.com/witnessmenow + *******************************************************************/ + +#include "ESP32-HUB75-MatrixPanel-I2S-DMA.h" +#ifndef NO_GFX +#include <Fonts/FreeSansBold12pt7b.h> +#endif + +struct VirtualCoords +{ + int16_t x; + int16_t y; + int16_t virt_row; // chain of panels row + int16_t virt_col; // chain of panels col + + VirtualCoords() : x(0), y(0) + { + } +}; + +enum PANEL_SCAN_RATE +{ + NORMAL_ONE_SIXTEEN, + ONE_EIGHT_32, + ONE_EIGHT_16 +}; + +#ifdef USE_GFX_ROOT +class VirtualMatrixPanel : public GFX +#elif !defined NO_GFX +class VirtualMatrixPanel : public Adafruit_GFX +#else +class VirtualMatrixPanel +#endif +{ + +public: + int16_t virtualResX; + int16_t virtualResY; + + int16_t vmodule_rows; + int16_t vmodule_cols; + + int16_t panelResX; + int16_t panelResY; + + int16_t dmaResX; // The width of the chain in pixels (as the DMA engine sees it) + + MatrixPanel_I2S_DMA *display; + + VirtualMatrixPanel(MatrixPanel_I2S_DMA &disp, int _vmodule_rows, int _vmodule_cols, int _panelResX, int _panelResY, bool serpentine_chain = true, bool top_down_chain = false) +#ifdef USE_GFX_ROOT + : GFX(_vmodule_cols * _panelResX, _vmodule_rows * _panelResY) +#elif !defined NO_GFX + : Adafruit_GFX(_vmodule_cols * _panelResX, _vmodule_rows * _panelResY) +#endif + { + this->display = &disp; + + panelResX = _panelResX; + panelResY = _panelResY; + + vmodule_rows = _vmodule_rows; + vmodule_cols = _vmodule_cols; + + virtualResX = vmodule_cols * _panelResX; + virtualResY = vmodule_rows * _panelResY; + + dmaResX = panelResX * vmodule_rows * vmodule_cols; + + /* Virtual Display width() and height() will return a real-world value. For example: + * Virtual Display width: 128 + * Virtual Display height: 64 + * + * So, not values that at 0 to X-1 + */ + + _s_chain_party = serpentine_chain; // serpentine, or 'S' chain? + _chain_top_down = top_down_chain; + + coords.x = coords.y = -1; // By default use an invalid co-ordinates that will be rejected by updateMatrixDMABuffer + } + + // equivalent methods of the matrix library so it can be just swapped out. + virtual void drawPixel(int16_t x, int16_t y, uint16_t color); + virtual void fillScreen(uint16_t color); // overwrite adafruit implementation + virtual void fillScreenRGB888(uint8_t r, uint8_t g, uint8_t b); + + void clearScreen() { display->clearScreen(); } + void drawPixelRGB888(int16_t x, int16_t y, uint8_t r, uint8_t g, uint8_t b); + +#ifdef USE_GFX_ROOT + // 24bpp FASTLED CRGB colour struct support + void fillScreen(CRGB color); + void drawPixel(int16_t x, int16_t y, CRGB color); +#endif + + uint16_t color444(uint8_t r, uint8_t g, uint8_t b) { return display->color444(r, g, b); } + uint16_t color565(uint8_t r, uint8_t g, uint8_t b) { return display->color565(r, g, b); } + uint16_t color333(uint8_t r, uint8_t g, uint8_t b) { return display->color333(r, g, b); } + + void flipDMABuffer() { display->flipDMABuffer(); } + void drawDisplayTest(); + void setRotate(bool rotate); + + void setPhysicalPanelScanRate(PANEL_SCAN_RATE rate); + +protected: + virtual VirtualCoords getCoords(int16_t &x, int16_t &y); + VirtualCoords coords; + + bool _s_chain_party = true; // Are we chained? Ain't no party like a... + bool _chain_top_down = false; // is the ESP at the top or bottom of the matrix of devices? + bool _rotate = false; + + PANEL_SCAN_RATE _panelScanRate = NORMAL_ONE_SIXTEEN; + +}; // end Class header + +/** + * Calculate virtual->real co-ordinate mapping to underlying single chain of panels connected to ESP32. + * Updates the private class member variable 'coords', so no need to use the return value. + * Not thread safe, but not a concern for ESP32 sketch anyway... I think. + */ +inline VirtualCoords VirtualMatrixPanel::getCoords(int16_t &x, int16_t &y) +{ + // Serial.println("Called Base."); + coords.x = coords.y = -1; // By defalt use an invalid co-ordinates that will be rejected by updateMatrixDMABuffer + + // Do we want to rotate? + if (_rotate) + { + int16_t temp_x = x; + x = y; + y = virtualResY - 1 - temp_x; + } + + if (x < 0 || x >= virtualResX || y < 0 || y >= virtualResY) + { // Co-ordinates go from 0 to X-1 remember! otherwise they are out of range! + // Serial.printf("VirtualMatrixPanel::getCoords(): Invalid virtual display coordinate. x,y: %d, %d\r\n", x, y); + return coords; + } + + // Stupidity check + if ((vmodule_rows == 1) && (vmodule_cols == 1)) // single panel... + { + coords.x = x; + coords.y = y; + } + else + { + uint8_t row = (y / panelResY) + 1; // a non indexed 0 row number + if ((_s_chain_party && !_chain_top_down && (row % 2 == 0)) // serpentine vertically stacked chain starting from bottom row (i.e. ESP closest to ground), upwards + || + (_s_chain_party && _chain_top_down && (row % 2 != 0)) // serpentine vertically stacked chain starting from the sky downwards + ) + { + // First portion gets you to the correct offset for the row you need + // Second portion inverts the x on the row + coords.x = ((y / panelResY) * (virtualResX)) + (virtualResX - x) - 1; + + // inverts the y the row + coords.y = panelResY - 1 - (y % panelResY); + } + else + { + // Normal chain pixel co-ordinate + coords.x = x + ((y / panelResY) * (virtualResX)); + coords.y = y % panelResY; + } + } + + // Reverse co-ordinates if panel chain from ESP starts from the TOP RIGHT + if (_chain_top_down) + { + /* + const HUB75_I2S_CFG _cfg = this->display->getCfg(); + coords.x = (_cfg.mx_width * _cfg.chain_length - 1) - coords.x; + coords.y = (_cfg.mx_height-1) - coords.y; + */ + coords.x = (dmaResX - 1) - coords.x; + coords.y = (panelResY - 1) - coords.y; + } + + /* START: Pixel remapping AGAIN to convert 1/16 SCAN output that the + * the underlying hardware library is designed for (because + * there's only 2 x RGB pins... and convert this to 1/8 or something + */ + if (_panelScanRate == ONE_EIGHT_32) + { + /* Convert Real World 'VirtualMatrixPanel' co-ordinates (i.e. Real World pixel you're looking at + on the panel or chain of panels, per the chaining configuration) to a 1/8 panels + double 'stretched' and 'squished' coordinates which is what needs to be sent from the + DMA buffer. + + Note: Look at the One_Eight_1_8_ScanPanel code and you'll see that the DMA buffer is setup + as if the panel is 2 * W and 0.5 * H ! + */ + + /* + Serial.print("VirtualMatrixPanel Mapping ("); Serial.print(x, DEC); Serial.print(","); Serial.print(y, DEC); Serial.print(") "); + // to + Serial.print("to ("); Serial.print(coords.x, DEC); Serial.print(","); Serial.print(coords.y, DEC); Serial.println(") "); + */ + if ((y & 8) == 0) + { + coords.x += ((coords.x / panelResX) + 1) * panelResX; // 1st, 3rd 'block' of 8 rows of pixels, offset by panel width in DMA buffer + } + else + { + coords.x += (coords.x / panelResX) * panelResX; // 2nd, 4th 'block' of 8 rows of pixels, offset by panel width in DMA buffer + } + + // http://cpp.sh/4ak5u + // Real number of DMA y rows is half reality + // coords.y = (y / 16)*8 + (y & 0b00000111); + coords.y = (y >> 4) * 8 + (y & 0b00000111); + + /* + Serial.print("OneEightScanPanel Mapping ("); Serial.print(x, DEC); Serial.print(","); Serial.print(y, DEC); Serial.print(") "); + // to + Serial.print("to ("); Serial.print(coords.x, DEC); Serial.print(","); Serial.print(coords.y, DEC); Serial.println(") "); + */ + } + else if (_panelScanRate == ONE_EIGHT_16) + { + if ((y & 8) == 0) + { + coords.x += (panelResX >> 2) * (((coords.x & 0xFFF0) >> 4) + 1); // 1st, 3rd 'block' of 8 rows of pixels, offset by panel width in DMA buffer + } + else + { + coords.x += (panelResX >> 2) * (((coords.x & 0xFFF0) >> 4)); // 2nd, 4th 'block' of 8 rows of pixels, offset by panel width in DMA buffer + } + + if (y < 32) + coords.y = (y >> 4) * 8 + (y & 0b00000111); + else + { + coords.y = ((y - 32) >> 4) * 8 + (y & 0b00000111); + coords.x += 256; + } + } + + // Serial.print("Mapping to x: "); Serial.print(coords.x, DEC); Serial.print(", y: "); Serial.println(coords.y, DEC); + return coords; +} + +inline void VirtualMatrixPanel::drawPixel(int16_t x, int16_t y, uint16_t color) +{ // adafruit virtual void override + getCoords(x, y); + this->display->drawPixel(coords.x, coords.y, color); +} + +inline void VirtualMatrixPanel::fillScreen(uint16_t color) +{ // adafruit virtual void override + this->display->fillScreen(color); +} + +inline void VirtualMatrixPanel::fillScreenRGB888(uint8_t r, uint8_t g, uint8_t b) +{ + this->display->fillScreenRGB888(r, g, b); +} + +inline void VirtualMatrixPanel::drawPixelRGB888(int16_t x, int16_t y, uint8_t r, uint8_t g, uint8_t b) +{ + getCoords(x, y); + this->display->drawPixelRGB888(coords.x, coords.y, r, g, b); +} + +#ifdef USE_GFX_ROOT +// Support for CRGB values provided via FastLED +inline void VirtualMatrixPanel::drawPixel(int16_t x, int16_t y, CRGB color) +{ + getCoords(x, y); + this->display->drawPixel(coords.x, coords.y, color); +} + +inline void VirtualMatrixPanel::fillScreen(CRGB color) +{ + this->display->fillScreen(color); +} +#endif + +inline void VirtualMatrixPanel::setRotate(bool rotate) +{ + _rotate = rotate; + +#ifndef NO_GFX + // We don't support rotation by degrees. + if (rotate) + { + setRotation(1); + } + else + { + setRotation(0); + } +#endif +} + +inline void VirtualMatrixPanel::setPhysicalPanelScanRate(PANEL_SCAN_RATE rate) +{ + _panelScanRate = rate; +} + +#ifndef NO_GFX +inline void VirtualMatrixPanel::drawDisplayTest() +{ + this->display->setFont(&FreeSansBold12pt7b); + this->display->setTextColor(this->display->color565(255, 255, 0)); + this->display->setTextSize(1); + + for (int panel = 0; panel < vmodule_cols * vmodule_rows; panel++) + { + int top_left_x = (panel == 0) ? 0 : (panel * panelResX); + this->display->drawRect(top_left_x, 0, panelResX, panelResY, this->display->color565(0, 255, 0)); + this->display->setCursor(panel * panelResX, panelResY - 3); + this->display->print((vmodule_cols * vmodule_rows) - panel); + } +} +#endif + +/* +// need to recreate this one, as it wouldn't work to just map where it starts. +inline void VirtualMatrixPanel::drawIcon (int *ico, int16_t x, int16_t y, int16_t icon_cols, int16_t icon_rows) { + int i, j; + for (i = 0; i < icon_rows; i++) { + for (j = 0; j < icon_cols; j++) { + // This is a call to this libraries version of drawPixel + // which will map each pixel, which is what we want. + //drawPixelRGB565 (x + j, y + i, ico[i * module_cols + j]); + drawPixel (x + j, y + i, ico[i * icon_cols + j]); + } + } +} +*/ + +#endif diff --git a/src/esp32_i2s_parallel_mcu_def.h.txt b/src/esp32_i2s_parallel_mcu_def.h.txt new file mode 100644 index 0000000..dd276da --- /dev/null +++ b/src/esp32_i2s_parallel_mcu_def.h.txt @@ -0,0 +1,35 @@ +#pragma once + +/* Abstract the Espressif IDF ESP32 MCU variant compile-time defines + * into another list for the purposes of this library. + * + * i.e. I couldn't be bothered having to update the library when they + * release the ESP32S4,5,6,7, n+1 etc. if they are all fundamentally + * the same architecture. + */ +#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 + + #define ESP32_SXXX 1 + #define ESP32_I2S_DEVICE I2S_NUM_0 + + #define I2S_PARALLEL_CLOCK_HZ 160000000L + #define DMA_MAX (4096-4) + +#elif CONFIG_IDF_TARGET_ESP32 || defined(ESP32) + + // 2016 model that started it all, and this library. The best. + #define ESP32_ORIG 1 + #define ESP32_I2S_DEVICE I2S_NUM_0 + + #define I2S_PARALLEL_CLOCK_HZ 80000000L + #define DMA_MAX (4096-4) + +#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 + + #error "ESPC-series RISC-V MCU's do not support parallel DMA and not supported by this library!" + #define ESP32_CXXX 1 + +#else + #error "ERROR: No ESP32 or ESP32 Espressif IDF detected at compile time." + +#endif
\ No newline at end of file diff --git a/src/platforms/esp32/esp32-default-pins.hpp b/src/platforms/esp32/esp32-default-pins.hpp new file mode 100644 index 0000000..5ee94b9 --- /dev/null +++ b/src/platforms/esp32/esp32-default-pins.hpp @@ -0,0 +1,18 @@ +#pragma once + +#define R1_PIN_DEFAULT 25 +#define G1_PIN_DEFAULT 26 +#define B1_PIN_DEFAULT 27 +#define R2_PIN_DEFAULT 14 +#define G2_PIN_DEFAULT 12 +#define B2_PIN_DEFAULT 13 + +#define A_PIN_DEFAULT 23 +#define B_PIN_DEFAULT 19 +#define C_PIN_DEFAULT 5 +#define D_PIN_DEFAULT 17 +#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 diff --git a/src/platforms/esp32/esp32_i2s_parallel_dma.c.txt b/src/platforms/esp32/esp32_i2s_parallel_dma.c.txt new file mode 100644 index 0000000..82eebe6 --- /dev/null +++ b/src/platforms/esp32/esp32_i2s_parallel_dma.c.txt @@ -0,0 +1,444 @@ +/* + * ESP32_I2S_PARALLEL_DMA (Version 3) + * + * Author: Mrfaptastic @ https://github.com/mrfaptastic/ + * + * Description: Multi-ESP32 product DMA setup functions for WROOM & S2, S3 mcu's. + * + * Credits: + * 1) https://www.esp32.com/viewtopic.php?f=17&t=3188 for original ref. implementation + * 2) https://github.com/TobleMiner/esp_i2s_parallel for a cleaner implementation + * + */ + +// Header +#include "esp32_i2s_parallel_dma.h" +#include "esp32_i2s_parallel_mcu_def.h" + +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +#include <driver/gpio.h> +#include <driver/periph_ctrl.h> +#include <soc/gpio_sig_map.h> + +// For I2S state management. +static i2s_parallel_state_t *i2s_state = NULL; + +// ESP32-S2,S3,C3 only has IS20 +// Original ESP32 has two I2S's, but we'll stick with the lowest common denominator. + +#ifdef ESP32_ORIG +static i2s_dev_t* I2S[I2S_NUM_MAX] = {&I2S0, &I2S1}; +#else +static i2s_dev_t* I2S[I2S_NUM_MAX] = {&I2S0}; +#endif + +callback shiftCompleteCallback; +void setShiftCompleteCallback(callback f) { + shiftCompleteCallback = f; +} + +volatile int previousBufferOutputLoopCount = 0; +volatile bool previousBufferFree = true; + +static void IRAM_ATTR irq_hndlr(void* arg) { // if we use I2S1 (default) + +//i2s_port_t port = *((i2s_port_t*) arg); + +/* Saves a few cycles, no need to cast void ptr to i2s_port_t and then check 120 times second... */ + SET_PERI_REG_BITS(I2S_INT_CLR_REG(ESP32_I2S_DEVICE), I2S_OUT_EOF_INT_CLR_V, 1, I2S_OUT_EOF_INT_CLR_S); + + previousBufferFree = true; + +/* + if(shiftCompleteCallback) { // we've defined a callback function ? + shiftCompleteCallback(); + } +*/ + +} // end irq_hndlr + + +// For peripheral setup and configuration +static inline int get_bus_width(i2s_parallel_cfg_bits_t width) { + switch(width) { + case I2S_PARALLEL_WIDTH_8: + return 8; + case I2S_PARALLEL_WIDTH_16: + return 16; + case I2S_PARALLEL_WIDTH_24: + return 24; + default: + return -ESP_ERR_INVALID_ARG; + } +} + +static void iomux_set_signal(int gpio, int signal) { + if(gpio < 0) { + return; + } + PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpio], PIN_FUNC_GPIO); + gpio_set_direction(gpio, GPIO_MODE_DEF_OUTPUT); + gpio_matrix_out(gpio, signal, false, false); + + // More mA the better... + gpio_set_drive_capability((gpio_num_t)gpio, (gpio_drive_cap_t)3); + +} + +static void dma_reset(i2s_dev_t* dev) { + dev->lc_conf.in_rst = 1; + dev->lc_conf.in_rst = 0; + dev->lc_conf.out_rst = 1; + dev->lc_conf.out_rst = 0; + + dev->lc_conf.ahbm_rst = 1; + dev->lc_conf.ahbm_rst = 0; + + +} + +static void fifo_reset(i2s_dev_t* dev) { + dev->conf.rx_fifo_reset = 1; + +#ifdef ESP32_SXXX + while(dev->conf.rx_fifo_reset_st); // esp32-s2 only +#endif + dev->conf.rx_fifo_reset = 0; + + dev->conf.tx_fifo_reset = 1; +#ifdef ESP32_SXXX + while(dev->conf.tx_fifo_reset_st); // esp32-s2 only +#endif + + dev->conf.tx_fifo_reset = 0; +} + +static void dev_reset(i2s_dev_t* dev) { + fifo_reset(dev); + dma_reset(dev); + dev->conf.rx_reset=1; + dev->conf.tx_reset=1; + dev->conf.rx_reset=0; + dev->conf.tx_reset=0; +} + +// DMA Linked List +// Size must be less than DMA_MAX - need to handle breaking long transfer into two descriptors before call +// DMA_MAX by the way is the maximum data packet size you can hold in one chunk +void link_dma_desc(volatile lldesc_t *dmadesc, volatile lldesc_t *prevdmadesc, void *memory, size_t size) +{ + if(size > DMA_MAX) size = DMA_MAX; + + dmadesc->size = size; + dmadesc->length = size; + dmadesc->buf = memory; + dmadesc->eof = 0; + dmadesc->sosf = 0; + dmadesc->owner = 1; + dmadesc->qe.stqe_next = 0; // will need to set this elsewhere + dmadesc->offset = 0; + + // link previous to current + if(prevdmadesc) + prevdmadesc->qe.stqe_next = (lldesc_t*)dmadesc; +} + + + +esp_err_t i2s_parallel_driver_install(i2s_port_t port, i2s_parallel_config_t* conf) { + + //port = I2S_NUM_0; /// override. + + if(port < I2S_NUM_0 || port >= I2S_NUM_MAX) { + return ESP_ERR_INVALID_ARG; + } + if(conf->sample_width < I2S_PARALLEL_WIDTH_8 || conf->sample_width >= I2S_PARALLEL_WIDTH_MAX) { + return ESP_ERR_INVALID_ARG; + } + if(conf->sample_rate > I2S_PARALLEL_CLOCK_HZ || conf->sample_rate < 1) { + return ESP_ERR_INVALID_ARG; + } + uint32_t clk_div_main = I2S_PARALLEL_CLOCK_HZ / conf->sample_rate / i2s_parallel_get_memory_width(port, conf->sample_width); + if(clk_div_main < 2 || clk_div_main > 0xFF) { + return ESP_ERR_INVALID_ARG; + } + + volatile int iomux_signal_base; + volatile int iomux_clock; + int irq_source; + + // Initialize I2S0 peripheral + if (port == I2S_NUM_0) { + periph_module_reset(PERIPH_I2S0_MODULE); + periph_module_enable(PERIPH_I2S0_MODULE); + iomux_clock = I2S0O_WS_OUT_IDX; + irq_source = ETS_I2S0_INTR_SOURCE; + + switch(conf->sample_width) { + case I2S_PARALLEL_WIDTH_8: + case I2S_PARALLEL_WIDTH_16: + iomux_signal_base = I2S0O_DATA_OUT8_IDX; + break; + case I2S_PARALLEL_WIDTH_24: + iomux_signal_base = I2S0O_DATA_OUT0_IDX; + break; + case I2S_PARALLEL_WIDTH_MAX: + return ESP_ERR_INVALID_ARG; + } + } +#ifdef ESP32_ORIG + // Can't compile if I2S1 if it doesn't exist with that hardware's IDF.... + else { +// I2S = &I2S1; + + periph_module_reset(PERIPH_I2S1_MODULE); + periph_module_enable(PERIPH_I2S1_MODULE); + iomux_clock = I2S1O_WS_OUT_IDX; + irq_source = ETS_I2S1_INTR_SOURCE; + + switch(conf->sample_width) { + case I2S_PARALLEL_WIDTH_16: + iomux_signal_base = I2S1O_DATA_OUT8_IDX; + break; + case I2S_PARALLEL_WIDTH_8: + case I2S_PARALLEL_WIDTH_24: + iomux_signal_base = I2S1O_DATA_OUT0_IDX; + break; + case I2S_PARALLEL_WIDTH_MAX: + return ESP_ERR_INVALID_ARG; + } + } +#endif + + // Setup GPIOs + int bus_width = get_bus_width(conf->sample_width); + + // Setup I2S peripheral + i2s_dev_t* dev = I2S[port]; + //dev_reset(dev); + + + // Setup GPIO's + for(int i = 0; i < bus_width; i++) { + iomux_set_signal(conf->gpio_bus[i], iomux_signal_base + i); + } + iomux_set_signal(conf->gpio_clk, iomux_clock); + + // invert clock phase if required + if (conf->clkphase) + GPIO.func_out_sel_cfg[conf->gpio_clk].inv_sel = 1; + + // Setup i2s clock + dev->sample_rate_conf.val = 0; + + // Third stage config, width of data to be written to IO (I think this should always be the actual data width?) + dev->sample_rate_conf.rx_bits_mod = bus_width; + dev->sample_rate_conf.tx_bits_mod = bus_width; + + dev->sample_rate_conf.rx_bck_div_num = 2; + dev->sample_rate_conf.tx_bck_div_num = 2; + + // Clock configuration + dev->clkm_conf.val=0; // Clear the clkm_conf struct + +#ifdef ESP32_SXXX + dev->clkm_conf.clk_sel = 2; // esp32-s2 only + dev->clkm_conf.clk_en = 1; +#endif + +#ifdef ESP32_ORIG + dev->clkm_conf.clka_en=0; // Use the 160mhz system clock (PLL_D2_CLK) when '0' +#endif + + dev->clkm_conf.clkm_div_b=0; // Clock numerator + dev->clkm_conf.clkm_div_a=1; // Clock denominator + + + // Note: clkm_div_num must only be set here AFTER clkm_div_b, clkm_div_a, etc. Or weird things happen! + // On original ESP32, max I2S DMA parallel speed is 20Mhz. + dev->clkm_conf.clkm_div_num = clk_div_main; + + + // I2S conf2 reg + dev->conf2.val = 0; + dev->conf2.lcd_en = 1; + dev->conf2.lcd_tx_wrx2_en=0; + dev->conf2.lcd_tx_sdx2_en=0; + + // I2S conf reg + dev->conf.val = 0; + +#ifdef ESP32_SXXX + dev->conf.tx_dma_equal=1; // esp32-s2 only + dev->conf.pre_req_en=1; // esp32-s2 only - enable I2S to prepare data earlier? wtf? +#endif + + // Now start setting up DMA FIFO + dev->fifo_conf.val = 0; + dev->fifo_conf.rx_data_num = 32; // Thresholds. + dev->fifo_conf.tx_data_num = 32; + dev->fifo_conf.dscr_en = 1; + +#ifdef ESP32_ORIG + + // Enable "One datum will be written twice in LCD mode" - for some reason, + // if we don't do this in 8-bit mode, data is updated on half-clocks not clocks + if(conf->sample_width == I2S_PARALLEL_WIDTH_8) + dev->conf2.lcd_tx_wrx2_en=1; + + // Not really described for non-pcm modes, although datasheet states it should be set correctly even for LCD mode + // First stage config. Configures how data is loaded into fifo + if(conf->sample_width == I2S_PARALLEL_WIDTH_24) { + // Mode 0, single 32-bit channel, linear 32 bit load to fifo + dev->fifo_conf.tx_fifo_mod = 3; + } else { + // Mode 1, single 16-bit channel, load 16 bit sample(*) into fifo and pad to 32 bit with zeros + // *Actually a 32 bit read where two samples are read at once. Length of fifo must thus still be word-aligned + dev->fifo_conf.tx_fifo_mod = 1; + } + + // Dictated by ESP32 datasheet + dev->fifo_conf.rx_fifo_mod_force_en = 1; + dev->fifo_conf.tx_fifo_mod_force_en = 1; + + // Second stage config + dev->conf_chan.val = 0; + + // 16-bit single channel data + dev->conf_chan.tx_chan_mod = 1; + dev->conf_chan.rx_chan_mod = 1; + +#endif + + + // Device Reset + dev_reset(dev); + dev->conf1.val = 0; + dev->conf1.tx_stop_en = 0; + + // Allocate I2S status structure for buffer swapping stuff + i2s_state = (i2s_parallel_state_t*) malloc(sizeof(i2s_parallel_state_t)); + assert(i2s_state != NULL); + i2s_parallel_state_t *state = i2s_state; + + state->desccount_a = conf->desccount_a; + state->desccount_b = conf->desccount_b; + state->dmadesc_a = conf->lldesc_a; + state->dmadesc_b = conf->lldesc_b; + state->i2s_interrupt_port_arg = port; // need to keep this somewhere in static memory for the ISR + + dev->timing.val = 0; + + // We using the double buffering switch logic? + if (conf->int_ena_out_eof) + { + // Get ISR setup + esp_err_t err = esp_intr_alloc(irq_source, + (int)(ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1), + irq_hndlr, + &state->i2s_interrupt_port_arg, NULL); + + if(err) { + return err; + } + + + // Setup interrupt handler which is focussed only on the (page 322 of Tech. Ref. Manual) + // "I2S_OUT_EOF_INT: Triggered when rxlink has finished sending a packet" + // ... whatever the hell that is supposed to mean... One massive linked list? So all pixels in the chain? + dev->int_ena.out_eof = 1; + } + + return ESP_OK; +} + + esp_err_t i2s_parallel_stop_dma(i2s_port_t port) { + if(port < I2S_NUM_0 || port >= I2S_NUM_MAX) { + return ESP_ERR_INVALID_ARG; + } + + i2s_dev_t* dev = I2S[port]; + + // Stop all ongoing DMA operations + dev->out_link.stop = 1; + dev->out_link.start = 0; + dev->conf.tx_start = 0; + + return ESP_OK; +} + + + esp_err_t i2s_parallel_send_dma(i2s_port_t port, lldesc_t* dma_descriptor) { + if(port < I2S_NUM_0 || port >= I2S_NUM_MAX) { + return ESP_ERR_INVALID_ARG; + } + + i2s_dev_t* dev = I2S[port]; + + + // Configure DMA burst mode + dev->lc_conf.val = I2S_OUT_DATA_BURST_EN | I2S_OUTDSCR_BURST_EN; + + // Set address of DMA descriptor + dev->out_link.addr = (uint32_t) dma_descriptor; + + // Start DMA operation + dev->out_link.stop = 0; + dev->out_link.start = 1; + + dev->conf.tx_start = 1; + + return ESP_OK; +} +/* +i2s_dev_t* i2s_parallel_get_dev(i2s_port_t port) { + if(port < I2S_NUM_0 || port >= I2S_NUM_MAX) { + return NULL; + } + +#ifdef ESP32_ORIG +if (port == I2S_NUM_1) + return &I2S1; +#endif + + return I2S0; // HARCODE THIS TO RETURN &I2S0 +} +*/ +// Double buffering flipping +// Flip to a buffer: 0 for bufa, 1 for bufb +// dmadesc_a and dmadesc_b point to the same memory if double buffering isn't enabled. +void i2s_parallel_flip_to_buffer(i2s_port_t port, int buffer_id) { + + if (i2s_state == NULL) { + return; // :-() + } + + lldesc_t *active_dma_chain; + if (buffer_id == 0) { + active_dma_chain=(lldesc_t*)&i2s_state->dmadesc_a[0]; + } else { + active_dma_chain=(lldesc_t*)&i2s_state->dmadesc_b[0]; + } + + // setup linked list to refresh from new buffer (continuously) when the end of the current list has been reached + i2s_state->dmadesc_a[i2s_state->desccount_a-1].qe.stqe_next = active_dma_chain; + i2s_state->dmadesc_b[i2s_state->desccount_b-1].qe.stqe_next = active_dma_chain; + + // we're still shifting out the buffer, so it shouldn't be written to yet. + //previousBufferFree = false; + i2s_parallel_set_previous_buffer_not_free(); +} + +bool i2s_parallel_is_previous_buffer_free() { + return previousBufferFree; +} + + +void i2s_parallel_set_previous_buffer_not_free() { + previousBufferFree = false; + previousBufferOutputLoopCount = 0; +} diff --git a/src/platforms/esp32/esp32_i2s_parallel_dma.cpp b/src/platforms/esp32/esp32_i2s_parallel_dma.cpp new file mode 100644 index 0000000..94f0d50 --- /dev/null +++ b/src/platforms/esp32/esp32_i2s_parallel_dma.cpp @@ -0,0 +1,571 @@ +/*----------------------------------------------------------------------------/ + Lovyan GFX - Graphics library for embedded devices. + +Original Source: + https://github.com/lovyan03/LovyanGFX/ + +Licence: + [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) + +Author: + [lovyan03](https://twitter.com/lovyan03) + +Contributors: + [ciniml](https://github.com/ciniml) + [mongonta0716](https://github.com/mongonta0716) + [tobozo](https://github.com/tobozo) + +Modified heavily for the ESP32 HUB75 DMA library by: + [mrfaptastic](https://github.com/mrfaptastic) + +/----------------------------------------------------------------------------*/ + +static const char* TAG = "esp32_i2s_parallel_dma"; + +#include <sdkconfig.h> +#if defined (CONFIG_IDF_TARGET_ESP32) + +#include "esp32_i2s_parallel_dma.hpp" + +#include <driver/gpio.h> +#include <driver/periph_ctrl.h> +#include <soc/gpio_sig_map.h> + +#include <esp_err.h> +#include <esp_log.h> + +/* + +callback shiftCompleteCallback; +void setShiftCompleteCallback(callback f) { + shiftCompleteCallback = f; +} + +volatile int previousBufferOutputLoopCount = 0; +volatile bool previousBufferFree = true; + +static void IRAM_ATTR irq_hndlr(void* arg) { // if we use I2S1 (default) + + SET_PERI_REG_BITS(I2S_INT_CLR_REG(ESP32_I2S_DEVICE), I2S_OUT_EOF_INT_CLR_V, 1, I2S_OUT_EOF_INT_CLR_S); + + previousBufferFree = true; + + + +} // end irq_hndlr +*/ + + + static i2s_dev_t* getDev(int port) + { + #if defined (CONFIG_IDF_TARGET_ESP32S2) + return &I2S0; + #else + return (port == 0) ? &I2S0 : &I2S1; + #endif + } + + void Bus_Parallel16::config(const config_t& cfg) + { + ESP_LOGI(TAG, "Performing config for ESP32 or ESP32-S2"); + _cfg = cfg; + auto port = cfg.port; + _dev = getDev(port); + } + + +//#if defined (CONFIG_IDF_TARGET_ESP32S2) + + static void _gpio_pin_init(int pin) + { + if (pin >= 0) + { + gpio_pad_select_gpio(pin); + //gpio_hi(pin); + gpio_set_direction((gpio_num_t)pin, GPIO_MODE_OUTPUT); + gpio_set_drive_capability((gpio_num_t)pin, (gpio_drive_cap_t)3); // esp32s3 as well? + } + } + + + bool Bus_Parallel16::init(void) // The big one that gets everything setup. + { + ESP_LOGI(TAG, "Performing DMA bus init() for ESP32 or ESP32-S2"); + + if(_cfg.port < I2S_NUM_0 || _cfg.port >= I2S_NUM_MAX) { + //return ESP_ERR_INVALID_ARG; + return false; + } + + if(_cfg.parallel_width < 8 || _cfg.parallel_width >= 24) { + return false; + } + + //auto freq = (_cfg.freq_write, 50000000u); // ? + auto freq = (_cfg.bus_freq); + + uint32_t _clkdiv_write = 0; + size_t _div_num = 10; + + // Calculate clock divider for ESP32-S2 + #if defined (CONFIG_IDF_TARGET_ESP32S2) + + static constexpr uint32_t pll_160M_clock_d2 = 160 * 1000 * 1000 >> 1; + + // I2S_CLKM_DIV_NUM 2=40MHz / 3=27MHz / 4=20MHz / 5=16MHz / 8=10MHz / 10=8MHz + _div_num = std::min(255u, 1 + ((pll_160M_clock_d2) / (1 + _cfg.freq_write))); + + _clkdiv_write = I2S_CLK_160M_PLL << I2S_CLK_SEL_S + | I2S_CLK_EN + | 1 << I2S_CLKM_DIV_A_S + | 0 << I2S_CLKM_DIV_B_S + | _div_num << I2S_CLKM_DIV_NUM_S + ; + + #else + + + // clock = 80MHz(PLL_D2_CLK) + static constexpr uint32_t pll_d2_clock = 80 * 1000 * 1000; + + // I2S_CLKM_DIV_NUM 4=20MHz / 5=16MHz / 8=10MHz / 10=8MHz + _div_num = std::min(255u, std::max(3u, 1 + (pll_d2_clock / (1 + freq)))); + + _clkdiv_write = I2S_CLK_EN + | 1 << I2S_CLKM_DIV_A_S + | 0 << I2S_CLKM_DIV_B_S + | _div_num << I2S_CLKM_DIV_NUM_S + ; + #endif + + if(_div_num < 2 || _div_num > 16) { + return false; + } + + //ESP_LOGI(TAG, "i2s pll clk_div_main is: %d", _div_num); + + auto dev = _dev; + volatile int iomux_signal_base; + volatile int iomux_clock; + int irq_source; + + // Initialize I2S0 peripheral + if (_cfg.port == 0) + { + + periph_module_reset(PERIPH_I2S0_MODULE); + periph_module_enable(PERIPH_I2S0_MODULE); + + iomux_clock = I2S0O_WS_OUT_IDX; + irq_source = ETS_I2S0_INTR_SOURCE; + + switch(_cfg.parallel_width) { + case 8: + case 16: + iomux_signal_base = I2S0O_DATA_OUT8_IDX; + break; + case 24: + iomux_signal_base = I2S0O_DATA_OUT0_IDX; + break; + default: + return ESP_ERR_INVALID_ARG; + } + } + + #if !defined (CONFIG_IDF_TARGET_ESP32S2) + // Can't compile if I2S1 if it doesn't exist with that hardware's IDF.... + else { + periph_module_reset(PERIPH_I2S1_MODULE); + periph_module_enable(PERIPH_I2S1_MODULE); + iomux_clock = I2S1O_WS_OUT_IDX; + irq_source = ETS_I2S1_INTR_SOURCE; + + switch(_cfg.parallel_width) { + case 16: + iomux_signal_base = I2S1O_DATA_OUT8_IDX; + break; + case 8: + case 24: + iomux_signal_base = I2S1O_DATA_OUT0_IDX; + break; + default: + return ESP_ERR_INVALID_ARG; + } + } + #endif + + // Setup GPIOs + int bus_width = _cfg.parallel_width; + + // Clock output GPIO setup + _gpio_pin_init(_cfg.pin_rd); // not used + _gpio_pin_init(_cfg.pin_wr); // clock + _gpio_pin_init(_cfg.pin_rs); // not used + + // Data output GPIO setup + int8_t* pins = _cfg.pin_data; + + for(int i = 0; i < bus_width; i++) + _gpio_pin_init(pins[i]); + + // Route clock signal to clock pin + gpio_matrix_out(_cfg.pin_wr, iomux_clock, _cfg.invert_pclk, 0); // inverst clock if required + + for (size_t i = 0; i < bus_width; i++) { + + if (pins[i] >= 0) { + gpio_matrix_out(pins[i], iomux_signal_base + i, false, false); + } + } + + + // Setup i2s clock + dev->sample_rate_conf.val = 0; + + // Third stage config, width of data to be written to IO (I think this should always be the actual data width?) + dev->sample_rate_conf.rx_bits_mod = bus_width; + dev->sample_rate_conf.tx_bits_mod = bus_width; + + dev->sample_rate_conf.rx_bck_div_num = 2; + dev->sample_rate_conf.tx_bck_div_num = 2; + + // Clock configuration + // dev->clkm_conf.val=0; // Clear the clkm_conf struct + /* + #if defined (CONFIG_IDF_TARGET_ESP32S2) + dev->clkm_conf.clk_sel = 2; // esp32-s2 only + dev->clkm_conf.clk_en = 1; + #endif + + #if !defined (CONFIG_IDF_TARGET_ESP32S2) + dev->clkm_conf.clka_en=0; // Use the 80mhz system clock (PLL_D2_CLK) when '0' + #endif + + dev->clkm_conf.clkm_div_b=0; // Clock numerator + dev->clkm_conf.clkm_div_a=1; // Clock denominator + */ + + // Note: clkm_div_num must only be set here AFTER clkm_div_b, clkm_div_a, etc. Or weird things happen! + // On original ESP32, max I2S DMA parallel speed is 20Mhz. + //dev->clkm_conf.clkm_div_num = 32; + dev->clkm_conf.val = _clkdiv_write; + + // I2S conf2 reg + dev->conf2.val = 0; + dev->conf2.lcd_en = 1; + dev->conf2.lcd_tx_wrx2_en=0; + dev->conf2.lcd_tx_sdx2_en=0; + + // I2S conf reg + dev->conf.val = 0; + + #if defined (CONFIG_IDF_TARGET_ESP32S2) + dev->conf.tx_dma_equal=1; // esp32-s2 only + dev->conf.pre_req_en=1; // esp32-s2 only - enable I2S to prepare data earlier? wtf? + #endif + + // Now start setting up DMA FIFO + dev->fifo_conf.val = 0; + dev->fifo_conf.rx_data_num = 32; // Thresholds. + dev->fifo_conf.tx_data_num = 32; + dev->fifo_conf.dscr_en = 1; + + #if !defined (CONFIG_IDF_TARGET_ESP32S2) + + // Enable "One datum will be written twice in LCD mode" - for some reason, + // if we don't do this in 8-bit mode, data is updated on half-clocks not clocks + if(_cfg.parallel_width == 8) + dev->conf2.lcd_tx_wrx2_en=1; + + // Not really described for non-pcm modes, although datasheet states it should be set correctly even for LCD mode + // First stage config. Configures how data is loaded into fifo + if(_cfg.parallel_width == 24) { + // Mode 0, single 32-bit channel, linear 32 bit load to fifo + dev->fifo_conf.tx_fifo_mod = 3; + } else { + // Mode 1, single 16-bit channel, load 16 bit sample(*) into fifo and pad to 32 bit with zeros + // *Actually a 32 bit read where two samples are read at once. Length of fifo must thus still be word-aligned + dev->fifo_conf.tx_fifo_mod = 1; + } + + // Dictated by ESP32 datasheet + dev->fifo_conf.rx_fifo_mod_force_en = 1; + dev->fifo_conf.tx_fifo_mod_force_en = 1; + + // Second stage config + dev->conf_chan.val = 0; + + // 16-bit single channel data + dev->conf_chan.tx_chan_mod = 1; + dev->conf_chan.rx_chan_mod = 1; + + #endif + + // Reset FIFO + dev->conf.rx_fifo_reset = 1; + + #if defined (CONFIG_IDF_TARGET_ESP32S2) + while(dev->conf.rx_fifo_reset_st); // esp32-s2 only + #endif + + dev->conf.rx_fifo_reset = 0; + dev->conf.tx_fifo_reset = 1; + + #if defined (CONFIG_IDF_TARGET_ESP32S2) + while(dev->conf.tx_fifo_reset_st); // esp32-s2 only + #endif + dev->conf.tx_fifo_reset = 0; + + + // Reset DMA + dev->lc_conf.in_rst = 1; + dev->lc_conf.in_rst = 0; + dev->lc_conf.out_rst = 1; + dev->lc_conf.out_rst = 0; + + dev->lc_conf.ahbm_rst = 1; + dev->lc_conf.ahbm_rst = 0; + + dev->in_link.val = 0; + dev->out_link.val = 0; + + + // Device reset + dev->conf.rx_reset=1; + dev->conf.tx_reset=1; + dev->conf.rx_reset=0; + dev->conf.tx_reset=0; + + dev->conf1.val = 0; + dev->conf1.tx_stop_en = 0; +/* + // Allocate I2S status structure for buffer swapping stuff + i2s_state = (i2s_parallel_state_t*) malloc(sizeof(i2s_parallel_state_t)); + assert(i2s_state != NULL); + i2s_parallel_state_t *state = i2s_state; + + state->desccount_a = conf->desccount_a; + state->desccount_b = conf->desccount_b; + state->dmadesc_a = conf->lldesc_a; + state->dmadesc_b = conf->lldesc_b; + state->i2s_interrupt_port_arg = port; // need to keep this somewhere in static memory for the ISR +*/ + + dev->timing.val = 0; +/* + // We using the double buffering switch logic? + if (conf->int_ena_out_eof) + { + // Get ISR setup + esp_err_t err = esp_intr_alloc(irq_source, + (int)(ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL1), + irq_hndlr, + &state->i2s_interrupt_port_arg, NULL); + + if(err) { + return err; + } + + + // Setup interrupt handler which is focussed only on the (page 322 of Tech. Ref. Manual) + // "I2S_OUT_EOF_INT: Triggered when rxlink has finished sending a packet" + // ... whatever the hell that is supposed to mean... One massive linked list? So all pixels in the chain? + dev->int_ena.out_eof = 1; + } +*/ + + + #if defined (CONFIG_IDF_TARGET_ESP32S2) + ESP_LOGD(TAG, "init() GPIO and clock configuration set for ESP32-S2"); + #else + ESP_LOGD(TAG, "init() GPIO and clock configuration set for ESP32"); + #endif + + + return true; + } + + + void Bus_Parallel16::release(void) + { + if (_dmadesc_a) + { + heap_caps_free(_dmadesc_a); + _dmadesc_a = nullptr; + _dmadesc_count = 0; + } + + if (_dmadesc_b) + { + heap_caps_free(_dmadesc_b); + _dmadesc_b = nullptr; + _dmadesc_count = 0; + } + } + + void Bus_Parallel16::enable_double_dma_desc(void) + { + _double_dma_buffer = true; + } + + // Need this to work for double buffers etc. + bool Bus_Parallel16::allocate_dma_desc_memory(size_t len) + { + if (_dmadesc_a) heap_caps_free(_dmadesc_a); // free all dma descrptios previously + + _dmadesc_count = len; + + ESP_LOGI(TAG, "Allocating memory for %d DMA descriptors.", len); + + _dmadesc_a= (HUB75_DMA_DESCRIPTOR_T*)heap_caps_malloc(sizeof(HUB75_DMA_DESCRIPTOR_T) * len, MALLOC_CAP_DMA); + + if (_dmadesc_a == nullptr) + { + ESP_LOGE(TAG, "ERROR: Couldn't malloc _dmadesc_a. Not enough memory."); + return false; + } + + + if (_double_dma_buffer) + { + if (_dmadesc_b) heap_caps_free(_dmadesc_b); // free all dma descrptios previously + + ESP_LOGD(TAG, "Allocating the second buffer (double buffer enabled)."); + + _dmadesc_b= (HUB75_DMA_DESCRIPTOR_T*)heap_caps_malloc(sizeof(HUB75_DMA_DESCRIPTOR_T) * len, MALLOC_CAP_DMA); + + if (_dmadesc_b == nullptr) + { + ESP_LOGE(TAG, "ERROR: Couldn't malloc _dmadesc_b. Not enough memory."); + _double_dma_buffer = false; + return false; + } + } + + _dmadesc_a_idx = 0; + _dmadesc_b_idx = 0; + + ESP_LOGD(TAG, "Allocating %d bytes of memory for DMA descriptors.", sizeof(HUB75_DMA_DESCRIPTOR_T) * len); + + + return true; + + } + + void Bus_Parallel16::create_dma_desc_link(void *data, size_t size, bool dmadesc_b) + { + static constexpr size_t MAX_DMA_LEN = (4096-4); + + if (dmadesc_b) + ESP_LOGI(TAG, " * Double buffer descriptor."); + + if (size > MAX_DMA_LEN) + { + size = MAX_DMA_LEN; + ESP_LOGW(TAG, "Creating DMA descriptor which links to payload with size greater than MAX_DMA_LEN!"); + } + + if ( (_dmadesc_a_idx+1) > _dmadesc_count) { + ESP_LOGE(TAG, "Attempted to create more DMA descriptors than allocated memory for. Expecting a maximum of %d DMA descriptors", _dmadesc_count); + return; + } + + volatile lldesc_t *dmadesc; + volatile lldesc_t *next; + bool eof = false; + +/* + dmadesc_a[desccount-1].eof = 1; + dmadesc_a[desccount-1].qe.stqe_next=(lldesc_t*)&dmadesc_a[0]; +*/ + + + // ESP_LOGI(TAG, "Creating descriptor %d\n", _dmadesc_a_idx); + if ( (dmadesc_b == true) ) // for primary buffer + { + dmadesc = &_dmadesc_b[_dmadesc_b_idx]; + + next = (_dmadesc_b_idx < (_dmadesc_count-1) ) ? &_dmadesc_b[_dmadesc_b_idx+1]:_dmadesc_b; + eof = (_dmadesc_b_idx == (_dmadesc_count-1)); + } + else + { + dmadesc = &_dmadesc_a[_dmadesc_a_idx]; + + // https://stackoverflow.com/questions/47170740/c-negative-array-index + next = (_dmadesc_a_idx < (_dmadesc_count-1) ) ? _dmadesc_a + _dmadesc_a_idx+1:_dmadesc_a; + eof = (_dmadesc_a_idx == (_dmadesc_count-1)); + } + + if ( _dmadesc_a_idx == (_dmadesc_count-1) ) { + ESP_LOGW(TAG, "Creating final DMA descriptor and linking back to 0."); + } + + dmadesc->size = size; + dmadesc->length = size; + dmadesc->buf = (uint8_t*) data; + dmadesc->eof = 0; + dmadesc->sosf = 0; + dmadesc->owner = 1; + dmadesc->qe.stqe_next = (lldesc_t*) next; + dmadesc->offset = 0; + + if ( (dmadesc_b == true) ) { // for primary buffer + _dmadesc_b_idx++; + } else { + _dmadesc_a_idx++; + } + + } // end create_dma_desc_link + + void Bus_Parallel16::dma_transfer_start() + { + auto dev = _dev; + + // Configure DMA burst mode + dev->lc_conf.val = I2S_OUT_DATA_BURST_EN | I2S_OUTDSCR_BURST_EN; + + // Set address of DMA descriptor + dev->out_link.addr = (uint32_t) _dmadesc_a; + + // Start DMA operation + dev->out_link.stop = 0; + dev->out_link.start = 1; + + dev->conf.tx_start = 1; + + + } // end + + + void Bus_Parallel16::dma_transfer_stop() + { + auto dev = _dev; + + // Stop all ongoing DMA operations + dev->out_link.stop = 1; + dev->out_link.start = 0; + dev->conf.tx_start = 0; + + } // end + + + void Bus_Parallel16::flip_dma_output_buffer() + { + if ( _double_dma_buffer == false) return; + + if ( _dmadesc_a_active == true) // change across to everything 'b'' + { + _dmadesc_a[_dmadesc_count-1].qe.stqe_next = &_dmadesc_b[0]; + _dmadesc_b[_dmadesc_count-1].qe.stqe_next = &_dmadesc_b[0]; + } + else + { + _dmadesc_a[_dmadesc_count-1].qe.stqe_next = &_dmadesc_a[0]; + _dmadesc_b[_dmadesc_count-1].qe.stqe_next = &_dmadesc_a[0]; + } + } // end flip + + + +#endif
\ No newline at end of file diff --git a/src/platforms/esp32/esp32_i2s_parallel_dma.h.txt b/src/platforms/esp32/esp32_i2s_parallel_dma.h.txt new file mode 100644 index 0000000..d7678bc --- /dev/null +++ b/src/platforms/esp32/esp32_i2s_parallel_dma.h.txt @@ -0,0 +1,106 @@ +#pragma once +/* + * ESP32_I2S_PARALLEL_DMA + */ + +#pragma once + +#if defined(ESP32) || defined(IDF_VER) + +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stdbool.h> +#include <sys/types.h> + +#include <freertos/FreeRTOS.h> +#include <driver/i2s.h> +#include <esp_err.h> +//#include <esp32/rom/lldesc.h> +//#include <esp32/rom/gpio.h> +#include <rom/lldesc.h> +#include <rom/gpio.h> + + +// Get MCU Type and Max CLK Hz for MCU +#include <esp32_i2s_parallel_mcu_def.h> + +typedef enum { + I2S_PARALLEL_WIDTH_8, + I2S_PARALLEL_WIDTH_16, + I2S_PARALLEL_WIDTH_24, + I2S_PARALLEL_WIDTH_MAX +} i2s_parallel_cfg_bits_t; + +typedef struct { + int gpio_bus[24]; // The parallel GPIOs to use, set gpio to -1 to disable + int gpio_clk; + int sample_rate; // 'clockspeed' + int sample_width; + int desccount_a; + lldesc_t * lldesc_a; + int desccount_b; // only used with double buffering + lldesc_t * lldesc_b; // only used with double buffering + bool clkphase; // Clock signal phase + bool int_ena_out_eof; // Do we raise an interrupt every time the DMA output loops? Don't do this unless we're doing double buffering! +} i2s_parallel_config_t; + +static inline int i2s_parallel_get_memory_width(i2s_port_t port, i2s_parallel_cfg_bits_t width) { + switch(width) { + case I2S_PARALLEL_WIDTH_8: + +#ifdef ESP32_ORIG + // Only I2S1 on the legacy ESP32 WROOM MCU supports space saving single byte 8 bit parallel access + if(port == I2S_NUM_1) { + return 1; + } else { + return 2; + } +#else + return 1; +#endif + + case I2S_PARALLEL_WIDTH_16: + return 2; + case I2S_PARALLEL_WIDTH_24: + return 4; + default: + return -ESP_ERR_INVALID_ARG; + } +} + +// DMA Linked List Creation +void link_dma_desc(volatile lldesc_t *dmadesc, volatile lldesc_t *prevdmadesc, void *memory, size_t size); + +// I2S DMA Peripheral Setup Functions +esp_err_t i2s_parallel_driver_install(i2s_port_t port, i2s_parallel_config_t* conf); +esp_err_t i2s_parallel_send_dma(i2s_port_t port, lldesc_t* dma_descriptor); +esp_err_t i2s_parallel_stop_dma(i2s_port_t port); +//i2s_dev_t* i2s_parallel_get_dev(i2s_port_t port); + +// For frame buffer flipping / double buffering +typedef struct { + volatile lldesc_t *dmadesc_a, *dmadesc_b; + int desccount_a, desccount_b; + i2s_port_t i2s_interrupt_port_arg; +} i2s_parallel_state_t; + +void i2s_parallel_flip_to_buffer(i2s_port_t port, int bufid); +bool i2s_parallel_is_previous_buffer_free(); +void i2s_parallel_set_previous_buffer_not_free(); + +// Callback function for when whole length of DMA chain has been sent out. +typedef void (*callback)(void); +void setShiftCompleteCallback(callback f); + + + +#ifdef __cplusplus +} +#endif + +#endif + diff --git a/src/platforms/esp32/esp32_i2s_parallel_dma.hpp b/src/platforms/esp32/esp32_i2s_parallel_dma.hpp new file mode 100644 index 0000000..95664d8 --- /dev/null +++ b/src/platforms/esp32/esp32_i2s_parallel_dma.hpp @@ -0,0 +1,138 @@ +/*----------------------------------------------------------------------------/ + Lovyan GFX - Graphics library for embedded devices. + +Original Source: + https://github.com/lovyan03/LovyanGFX/ + +Licence: + [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) + +Author: + [lovyan03](https://twitter.com/lovyan03) + +Contributors: + [ciniml](https://github.com/ciniml) + [mongonta0716](https://github.com/mongonta0716) + [tobozo](https://github.com/tobozo) + + Modified heavily for the ESP32 HUB75 DMA library by: + [mrfaptastic](https://github.com/mrfaptastic) + +------------------------------------------------------------------------------ + + Putin’s Russia and its genocide in Ukraine is a disgrace to humanity. + + https://www.reddit.com/r/ukraine/comments/xfuc6v/more_than_460_graves_have_already_been_found_in/ + + Xi Jinping and his communist China’s silence on the war in Ukraine says everything about + how China condones such genocide, especially if it's against 'the west' (aka. decency). + + Whilst the good people at Espressif probably have nothing to do with this, the unfortunate + reality is libraries like this increase the popularity of Chinese silicon chips, which + indirectly funds (through CCP state taxes) the growth and empowerment of such a despot government. + + Global democracy, decency and security is put at risk with every Chinese silicon chip that is bought. + +/----------------------------------------------------------------------------*/ + +#pragma once + +#include <string.h> // memcpy +#include <algorithm> +#include <stdbool.h> + +#include <sys/types.h> +#include <freertos/FreeRTOS.h> +#include <driver/i2s.h> +#include <rom/lldesc.h> +#include <rom/gpio.h> + +#define DMA_MAX (4096-4) + +// The type used for this SoC +#define HUB75_DMA_DESCRIPTOR_T lldesc_t + +//---------------------------------------------------------------------------- + + class Bus_Parallel16 + { + public: + Bus_Parallel16() + { + + } + + struct config_t + { + int port = 0; + + // max 20MHz (when in 16 bit / 2 byte mode) + uint32_t bus_freq = 10000000; + int8_t pin_wr = -1; // + int8_t pin_rd = -1; + int8_t pin_rs = -1; // D/C + bool invert_pclk = false; + int8_t parallel_width = 16; // do not change + union + { + int8_t pin_data[16]; + struct + { + int8_t pin_d0; + int8_t pin_d1; + int8_t pin_d2; + int8_t pin_d3; + int8_t pin_d4; + int8_t pin_d5; + int8_t pin_d6; + int8_t pin_d7; + int8_t pin_d8; + int8_t pin_d9; + int8_t pin_d10; + int8_t pin_d11; + int8_t pin_d12; + int8_t pin_d13; + int8_t pin_d14; + int8_t pin_d15; + }; + }; + }; + + const config_t& config(void) const { return _cfg; } + void config(const config_t& config); + + bool init(void) ; + void release(void) ; + + void enable_double_dma_desc(); + bool allocate_dma_desc_memory(size_t len); + + void create_dma_desc_link(void *memory, size_t size, bool dmadesc_b = false); + + void dma_transfer_start(); + void dma_transfer_stop(); + + void flip_dma_output_buffer(); + + private: + + void _init_pins() { }; + + config_t _cfg; + + bool _double_dma_buffer = false; + bool _dmadesc_a_active = true; + + uint32_t _dmadesc_count = 0; // number of dma decriptors + + uint32_t _dmadesc_a_idx = 0; + uint32_t _dmadesc_b_idx = 0; + + HUB75_DMA_DESCRIPTOR_T* _dmadesc_a = nullptr; + HUB75_DMA_DESCRIPTOR_T* _dmadesc_b = nullptr; + + volatile i2s_dev_t* _dev; + + + + };
\ No newline at end of file diff --git a/src/platforms/esp32s2/esp32s2-default-pins.hpp b/src/platforms/esp32s2/esp32s2-default-pins.hpp new file mode 100644 index 0000000..202b1c5 --- /dev/null +++ b/src/platforms/esp32s2/esp32s2-default-pins.hpp @@ -0,0 +1,16 @@ +#pragma once + +#define R1_PIN_DEFAULT 45 +#define G1_PIN_DEFAULT 42 +#define B1_PIN_DEFAULT 41 +#define R2_PIN_DEFAULT 40 +#define G2_PIN_DEFAULT 39 +#define B2_PIN_DEFAULT 38 +#define A_PIN_DEFAULT 37 +#define B_PIN_DEFAULT 36 +#define C_PIN_DEFAULT 35 +#define D_PIN_DEFAULT 34 +#define E_PIN_DEFAULT -1 // required for 1/32 scan panels, like 64x64. Any available pin would do, i.e. IO32 +#define LAT_PIN_DEFAULT 26 +#define OE_PIN_DEFAULT 21 +#define CLK_PIN_DEFAULT 33 diff --git a/src/platforms/esp32s3/ESP32-S3-DevKitC-1-pin-layout.png b/src/platforms/esp32s3/ESP32-S3-DevKitC-1-pin-layout.png Binary files differnew file mode 100644 index 0000000..c4391b3 --- /dev/null +++ b/src/platforms/esp32s3/ESP32-S3-DevKitC-1-pin-layout.png diff --git a/src/platforms/esp32s3/Readme.md b/src/platforms/esp32s3/Readme.md new file mode 100644 index 0000000..6b6c716 --- /dev/null +++ b/src/platforms/esp32s3/Readme.md @@ -0,0 +1,16 @@ +https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-guides/external-ram.html + +Restrictions + +External RAM use has the following restrictions: + +When flash cache is disabled (for example, if the flash is being written to), the external RAM also becomes inaccessible; any reads from or writes to it will lead to an illegal cache access exception. This is also the reason why ESP-IDF does not by default allocate any task stacks in external RAM (see below). + + External RAM cannot be used as a place to store DMA transaction descriptors or as a buffer for a DMA transfer to read from or write into. Therefore when External RAM is enabled, any buffer that will be used in combination with DMA must be allocated using heap_caps_malloc(size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL) and can be freed using a standard free() call. + +*Note, although ESP32-S3 has hardware support for DMA to/from external RAM, this is not yet supported in ESP-IDF.* + +External RAM uses the same cache region as the external flash. This means that frequently accessed variables in external RAM can be read and modified almost as quickly as in internal ram. However, when accessing large chunks of data (>32 KB), the cache can be insufficient, and speeds will fall back to the access speed of the external RAM. Moreover, accessing large chunks of data can “push out” cached flash, possibly making the execution of code slower afterwards. + +In general, external RAM will not be used as task stack memory. xTaskCreate() and similar functions will always allocate internal memory for stack and task TCBs. + diff --git a/src/platforms/esp32s3/ReservedPinsForPSRAM.PNG b/src/platforms/esp32s3/ReservedPinsForPSRAM.PNG Binary files differnew file mode 100644 index 0000000..994fda8 --- /dev/null +++ b/src/platforms/esp32s3/ReservedPinsForPSRAM.PNG diff --git a/src/platforms/esp32s3/esp32s3-default-pins.hpp b/src/platforms/esp32s3/esp32s3-default-pins.hpp new file mode 100644 index 0000000..57d8aae --- /dev/null +++ b/src/platforms/esp32s3/esp32s3-default-pins.hpp @@ -0,0 +1,18 @@ +#pragma once + +// Avoid and QSPI pins + +#define R1_PIN_DEFAULT 4 +#define G1_PIN_DEFAULT 5 +#define B1_PIN_DEFAULT 6 +#define R2_PIN_DEFAULT 7 +#define G2_PIN_DEFAULT 15 +#define B2_PIN_DEFAULT 16 +#define A_PIN_DEFAULT 18 +#define B_PIN_DEFAULT 8 +#define C_PIN_DEFAULT 3 +#define D_PIN_DEFAULT 42 +#define E_PIN_DEFAULT -1 // required for 1/32 scan panels, like 64x64. Any available pin would do, i.e. IO32 +#define LAT_PIN_DEFAULT 40 +#define OE_PIN_DEFAULT 2 +#define CLK_PIN_DEFAULT 41 diff --git a/src/platforms/esp32s3/gdma_lcd_parallel16.cpp b/src/platforms/esp32s3/gdma_lcd_parallel16.cpp new file mode 100644 index 0000000..785e489 --- /dev/null +++ b/src/platforms/esp32s3/gdma_lcd_parallel16.cpp @@ -0,0 +1,356 @@ +/* + Simple example of using the ESP32-S3's LCD peripheral for general-purpose + (non-LCD) parallel data output with DMA. Connect 8 LEDs (or logic analyzer), + cycles through a pattern among them at about 1 Hz. + This code is ONLY for the ESP32-S3, NOT the S2, C3 or original ESP32. + None of this is authoritative canon, just a lot of trial error w/datasheet + and register poking. Probably more robust ways of doing this still TBD. + + + FULL CREDIT goes to AdaFruit + + https://blog.adafruit.com/2022/06/21/esp32uesday-more-s3-lcd-peripheral-hacking-with-code/ + + PLEASE SUPPORT THEM! + + */ + #include <Arduino.h> + #include "gdma_lcd_parallel16.hpp" + + static const char* TAG = "gdma_lcd_parallel16"; + + + dma_descriptor_t desc; // DMA descriptor for testing +/* + uint8_t data[8][312]; // Transmit buffer (2496 bytes total) + uint16_t* dmabuff2; +*/ + // End-of-DMA-transfer callback + static IRAM_ATTR bool dma_callback(gdma_channel_handle_t dma_chan, + gdma_event_data_t *event_data, void *user_data) { + // This DMA callback seems to trigger a moment before the last data has + // issued (buffering between DMA & LCD peripheral?), so pause a moment + // before stopping LCD data out. The ideal delay may depend on the LCD + // clock rate...this one was determined empirically by monitoring on a + // logic analyzer. YMMV. + esp_rom_delay_us(100); + // The LCD peripheral stops transmitting at the end of the DMA xfer, but + // clear the lcd_start flag anyway -- we poll it in loop() to decide when + // the transfer has finished, and the same flag is set later to trigger + // the next transfer. + + LCD_CAM.lcd_user.lcd_start = 0; + return true; + } + + static lcd_cam_dev_t* getDev(int port) + { + return &LCD_CAM; + } + + // ------------------------------------------------------------------------------ + + void Bus_Parallel16::config(const config_t& cfg) + { + _cfg = cfg; + auto port = cfg.port; + _dev = getDev(port); + } + + + //https://github.com/adafruit/Adafruit_Protomatter/blob/master/src/arch/esp32-s3.h + bool Bus_Parallel16::init(void) + { + ///dmabuff2 = (uint16_t*)heap_caps_malloc(sizeof(uint16_t) * 64*32, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + + // LCD_CAM peripheral isn't enabled by default -- MUST begin with this: + periph_module_enable(PERIPH_LCD_CAM_MODULE); + periph_module_reset(PERIPH_LCD_CAM_MODULE); + + // Reset LCD bus + LCD_CAM.lcd_user.lcd_reset = 1; + esp_rom_delay_us(100); + + auto lcd_clkm_div_num = 160000000 / _cfg.bus_freq / 2; + + ESP_LOGI(TAG, "Clock divider is %d", lcd_clkm_div_num); + + // Configure LCD clock. Since this program generates human-perceptible + // output and not data for LED matrices or NeoPixels, use almost the + // slowest LCD clock rate possible. The S3-mini module used on Feather + // ESP32-S3 has a 40 MHz crystal. A 2-stage clock division of 1:16000 + // is applied (250*64), yielding 2,500 Hz. Still much too fast for + // human eyes, so later we set up the data to repeat each output byte + // many times over. + LCD_CAM.lcd_clock.clk_en = 1; // Enable peripheral clock + LCD_CAM.lcd_clock.lcd_clk_sel = 2; // 160mhz source + LCD_CAM.lcd_clock.lcd_ck_out_edge = 0; // PCLK low in 1st half cycle + LCD_CAM.lcd_clock.lcd_ck_idle_edge = 0; // PCLK low idle + LCD_CAM.lcd_clock.lcd_clk_equ_sysclk = 0; // PCLK = CLK / (CLKCNT_N+1) + LCD_CAM.lcd_clock.lcd_clkm_div_num = lcd_clkm_div_num; // 1st stage 1:250 divide + LCD_CAM.lcd_clock.lcd_clkm_div_a = 1; // 0/1 fractional divide + LCD_CAM.lcd_clock.lcd_clkm_div_b = 0; + + // See section 26.3.3.1 of the ESP32S3 Technical Reference Manual + // for information on other clock sources and dividers. + + // Configure LCD frame format. This is where we fiddle the peripheral + // to provide generic 8-bit output rather than actually driving an LCD. + // There's also a 16-bit mode but that's not shown here. + LCD_CAM.lcd_ctrl.lcd_rgb_mode_en = 0; // i8080 mode (not RGB) + LCD_CAM.lcd_rgb_yuv.lcd_conv_bypass = 0; // Disable RGB/YUV converter + LCD_CAM.lcd_misc.lcd_next_frame_en = 0; // Do NOT auto-frame + LCD_CAM.lcd_data_dout_mode.val = 0; // No data delays + LCD_CAM.lcd_user.lcd_always_out_en = 1; // Enable 'always out' mode + LCD_CAM.lcd_user.lcd_8bits_order = 0; // Do not swap bytes + LCD_CAM.lcd_user.lcd_bit_order = 0; // Do not reverse bit order + LCD_CAM.lcd_user.lcd_2byte_en = 1; // 8-bit data mode + LCD_CAM.lcd_user.lcd_dummy = 0; // Dummy phase(s) @ LCD start + LCD_CAM.lcd_user.lcd_dummy_cyclelen = 0; // 1 dummy phase + LCD_CAM.lcd_user.lcd_cmd = 0; // No command at LCD start + // "Dummy phases" are initial LCD peripheral clock cycles before data + // begins transmitting when requested. After much testing, determined + // that at least one dummy phase MUST be enabled for DMA to trigger + // reliably. A problem with dummy phase(s) is if we're also using the + // LCD_PCLK_IDX signal (not used in this code, but Adafruit_Protomatter + // does)...the clock signal will start a couple of pulses before data, + // which may or may not be problematic in some situations. You can + // disable the dummy phase but need to keep the LCD TX FIFO primed + // in that case, which gets complex. + // always_out_en is set above to allow aribtrary-length transfers, + // else lcd_dout_cyclelen is used...but is limited to 8K. Long (>4K) + // transfers need DMA linked lists, not used here but mentioned later. + + // Route 8 LCD data signals to GPIO pins + int8_t* pins = _cfg.pin_data; + + for(int i = 0; i < 16; i++) + { + if (pins[i] >= 0) { // -1 value will CRASH the ESP32! + esp_rom_gpio_connect_out_signal(pins[i], LCD_DATA_OUT0_IDX + i, false, false); + gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[pins[i]], PIN_FUNC_GPIO); + gpio_set_drive_capability((gpio_num_t)pins[i], (gpio_drive_cap_t)3); + } + } + + /* + const struct { + int8_t pin; + uint8_t signal; + } mux[] = { + { 43, LCD_DATA_OUT0_IDX }, // These are 8 consecutive pins down one + { 42, LCD_DATA_OUT1_IDX }, // side of the ESP32-S3 Feather. The ESP32 + { 2, LCD_DATA_OUT2_IDX }, // has super flexible pin MUX capabilities, + { 9, LCD_DATA_OUT3_IDX }, // so any signal can go to any pin! + { 10, LCD_DATA_OUT4_IDX }, + { 11, LCD_DATA_OUT5_IDX }, + { 12, LCD_DATA_OUT6_IDX }, + { 13, LCD_DATA_OUT7_IDX }, + }; + for (int i = 0; i < 8; i++) { + esp_rom_gpio_connect_out_signal(mux[i].pin, LCD_DATA_OUT0_IDX + i, false, false); + gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[mux[i].pin], PIN_FUNC_GPIO); + gpio_set_drive_capability((gpio_num_t)mux[i].pin, (gpio_drive_cap_t)3); + } +*/ + // Clock + esp_rom_gpio_connect_out_signal(_cfg.pin_wr, LCD_PCLK_IDX, _cfg.invert_pclk, false); + gpio_hal_iomux_func_sel(GPIO_PIN_MUX_REG[_cfg.pin_wr], PIN_FUNC_GPIO); + gpio_set_drive_capability((gpio_num_t)_cfg.pin_wr, (gpio_drive_cap_t)3); + + // This program has a known fixed-size data buffer (2496 bytes) that fits + // in a single DMA descriptor (max 4095 bytes). Large transfers would + // require a linked list of descriptors, but here it's just one... + + desc.dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + desc.dw0.suc_eof = 0; // Last descriptor + desc.next = &desc; // No linked list + + + // Remaining descriptor elements are initialized before each DMA transfer. + + // Allocate DMA channel and connect it to the LCD peripheral + static gdma_channel_alloc_config_t dma_chan_config = { + .sibling_chan = NULL, + .direction = GDMA_CHANNEL_DIRECTION_TX, + .flags = { + .reserve_sibling = 0 + } + }; + gdma_new_channel(&dma_chan_config, &dma_chan); + gdma_connect(dma_chan, GDMA_MAKE_TRIGGER(GDMA_TRIG_PERIPH_LCD, 0)); + static gdma_strategy_config_t strategy_config = { + .owner_check = false, + .auto_update_desc = false + }; + gdma_apply_strategy(dma_chan, &strategy_config); + + // Enable DMA transfer callback + static gdma_tx_event_callbacks_t tx_cbs = { + .on_trans_eof = dma_callback + }; + gdma_register_tx_event_callbacks(dma_chan, &tx_cbs, NULL); + + // As mentioned earlier, the slowest clock we can get to the LCD + // peripheral is 40 MHz / 250 / 64 = 2500 Hz. To make an even slower + // bit pattern that's perceptible, we just repeat each value many + // times over. The pattern here just counts through each of 8 bits + // (each LED lights in sequence)...so to get this to repeat at about + // 1 Hz, each LED is lit for 2500/8 or 312 cycles, hence the + // data[8][312] declaration at the start of this code (it's not + // precisely 1 Hz because reality is messy, but sufficient for demo). + // In actual use, say controlling an LED matrix or NeoPixels, such + // shenanigans aren't necessary, as these operate at multiple MHz + // with much smaller clock dividers and can use 1 byte per datum. + /* + for (int i = 0; i < (sizeof(data) / sizeof(data[0])); i++) { // 0 to 7 + for (int j = 0; j < sizeof(data[0]); j++) { // 0 to 311 + data[i][j] = 1 << i; + } + } + */ + + + // This uses a busy loop to wait for each DMA transfer to complete... + // but the whole point of DMA is that one's code can do other work in + // the interim. The CPU is totally free while the transfer runs! + while (LCD_CAM.lcd_user.lcd_start); // Wait for DMA completion callback + + // After much experimentation, each of these steps is required to get + // a clean start on the next LCD transfer: + gdma_reset(dma_chan); // Reset DMA to known state + LCD_CAM.lcd_user.lcd_dout = 1; // Enable data out + LCD_CAM.lcd_user.lcd_update = 1; // Update registers + LCD_CAM.lcd_misc.lcd_afifo_reset = 1; // Reset LCD TX FIFO + + // This program happens to send the same data over and over...but, + // if desired, one could fill the data buffer with a new bit pattern + // here, or point to a completely different buffer each time through. + // With two buffers, one can make best use of time by filling each + // with new data before the busy loop above, alternating between them. + + // Reset elements of DMA descriptor. Just one in this code, long + // transfers would loop through a linked list. + + /* + desc.dw0.size = desc.dw0.length = sizeof(data); + desc.buffer = dmabuff2; //data; + desc.next = &desc; +*/ + + +/* + //gdma_start(dma_chan, (intptr_t)&desc); // Start DMA w/updated descriptor(s) + gdma_start(dma_chan, (intptr_t)&_dmadesc_a[0]); // Start DMA w/updated descriptor(s) + esp_rom_delay_us(100); // Must 'bake' a moment before... + LCD_CAM.lcd_user.lcd_start = 1; // Trigger LCD DMA transfer +*/ + + + return true; // no return val = illegal instruction + + } + + + void Bus_Parallel16::release(void) + { + if (_i80_bus) + { + esp_lcd_del_i80_bus(_i80_bus); + } + if (_dmadesc_a) + { + heap_caps_free(_dmadesc_a); + _dmadesc_a = nullptr; + _dmadesc_count = 0; + } + + } + + void Bus_Parallel16::enable_double_dma_desc(void) + { + //_double_dma_buffer = true; + } + + // Need this to work for double buffers etc. + bool Bus_Parallel16::allocate_dma_desc_memory(size_t len) + { + if (_dmadesc_a) heap_caps_free(_dmadesc_a); // free all dma descrptios previously + _dmadesc_count = len; + + ESP_LOGD(TAG, "Allocating %d bytes memory for DMA descriptors.", sizeof(HUB75_DMA_DESCRIPTOR_T) * len); + + _dmadesc_a= (HUB75_DMA_DESCRIPTOR_T*)heap_caps_malloc(sizeof(HUB75_DMA_DESCRIPTOR_T) * len, MALLOC_CAP_DMA); + + if (_dmadesc_a == nullptr) + { + ESP_LOGE(TAG, "ERROR: Couldn't malloc _dmadesc_a. Not enough memory."); + return false; + } + + _dmadesc_a_idx = 0; + // _dmadesc_b_idx = 0; + + return true; + + } + + void Bus_Parallel16::create_dma_desc_link(void *data, size_t size, bool dmadesc_b) + { + static constexpr size_t MAX_DMA_LEN = (4096-4); + + if (size > MAX_DMA_LEN) + { + size = MAX_DMA_LEN; + ESP_LOGW(TAG, "Creating DMA descriptor which links to payload with size greater than MAX_DMA_LEN!"); + } + + if ( _dmadesc_a_idx >= _dmadesc_count) + { + ESP_LOGE(TAG, "Attempted to create more DMA descriptors than allocated memory for. Expecting a maximum of %d DMA descriptors", _dmadesc_count); + return; + } + + _dmadesc_a[_dmadesc_a_idx].dw0.owner = DMA_DESCRIPTOR_BUFFER_OWNER_DMA; + _dmadesc_a[_dmadesc_a_idx].dw0.suc_eof = 0; + _dmadesc_a[_dmadesc_a_idx].dw0.size = _dmadesc_a[_dmadesc_a_idx].dw0.length = size; //sizeof(data); + _dmadesc_a[_dmadesc_a_idx].buffer = data; //data; + + if (_dmadesc_a_idx == _dmadesc_count-1) { + _dmadesc_a[_dmadesc_a_idx].next = (dma_descriptor_t *) &_dmadesc_a[0]; + } + else { + _dmadesc_a[_dmadesc_a_idx].next = (dma_descriptor_t *) &_dmadesc_a[_dmadesc_a_idx+1]; + } + + _dmadesc_a_idx++; + + + } // end create_dma_desc_link + + void Bus_Parallel16::dma_transfer_start() + { + gdma_start(dma_chan, (intptr_t)&_dmadesc_a[0]); // Start DMA w/updated descriptor(s) + esp_rom_delay_us(100); // Must 'bake' a moment before... + LCD_CAM.lcd_user.lcd_start = 1; // Trigger LCD DMA transfer + + } // end + + void Bus_Parallel16::dma_transfer_stop() + { + + LCD_CAM.lcd_user.lcd_reset = 1; // Trigger LCD DMA transfer + LCD_CAM.lcd_user.lcd_update = 1; // Trigger LCD DMA transfer + + gdma_stop(dma_chan); + + } // end + + + void Bus_Parallel16::flip_dma_output_buffer() + { + + + } // end flip + + diff --git a/src/platforms/esp32s3/gdma_lcd_parallel16.hpp b/src/platforms/esp32s3/gdma_lcd_parallel16.hpp new file mode 100644 index 0000000..810eacb --- /dev/null +++ b/src/platforms/esp32s3/gdma_lcd_parallel16.hpp @@ -0,0 +1,176 @@ +/* + Simple example of using the ESP32-S3's LCD peripheral for general-purpose + (non-LCD) parallel data output with DMA. Connect 8 LEDs (or logic analyzer), + cycles through a pattern among them at about 1 Hz. + This code is ONLY for the ESP32-S3, NOT the S2, C3 or original ESP32. + None of this is authoritative canon, just a lot of trial error w/datasheet + and register poking. Probably more robust ways of doing this still TBD. + + + FULL CREDIT goes to AdaFruit + + https://blog.adafruit.com/2022/06/21/esp32uesday-more-s3-lcd-peripheral-hacking-with-code/ + + PLEASE SUPPORT THEM! + + + Putin’s Russia and its genocide in Ukraine is a disgrace to humanity. + + https://www.reddit.com/r/ukraine/comments/xfuc6v/more_than_460_graves_have_already_been_found_in/ + + +/---------------------------------------------------------------------------- + +*/ + +#pragma once + +#if __has_include (<esp_lcd_panel_io.h>) + +#include <sdkconfig.h> +#include <esp_lcd_panel_io.h> + +//#include <freertos/portmacro.h> +#include <esp_intr_alloc.h> + +#include <esp_err.h> +#include <esp_log.h> + +#include <driver/gpio.h> +#include <soc/gpio_sig_map.h> + + +#include <hal/gpio_ll.h> +#include <hal/lcd_hal.h> + + + +#include <string.h> +#include <math.h> + +#include <stdint.h> + +#if (ESP_IDF_VERSION_MAJOR == 5) +#include <esp_private/periph_ctrl.h> +#else +#include <driver/periph_ctrl.h> +#endif +#include <esp_private/gdma.h> +#include <esp_rom_gpio.h> +#include <hal/dma_types.h> +#include <hal/gpio_hal.h> +#include <hal/lcd_ll.h> +#include <soc/lcd_cam_reg.h> +#include <soc/lcd_cam_struct.h> +#include <esp_heap_caps.h> +#include <esp_heap_caps_init.h> + + +#if __has_include (<esp_private/periph_ctrl.h>) + #include <esp_private/periph_ctrl.h> +#else + #include <driver/periph_ctrl.h> +#endif + +#if __has_include(<esp_arduino_version.h>) + #include <esp_arduino_version.h> +#endif + +#define DMA_MAX (4096-4) + +// The type used for this SoC +#define HUB75_DMA_DESCRIPTOR_T dma_descriptor_t + + +//---------------------------------------------------------------------------- + + class Bus_Parallel16 + { + public: + Bus_Parallel16() + { + + } + + struct config_t + { + // LCD_CAM peripheral number. No need to change (only 0 for ESP32-S3.) + int port = 0; + + // max 40MHz (when in 16 bit / 2 byte mode) + uint32_t bus_freq = 20000000; + int8_t pin_wr = -1; + int8_t pin_rd = -1; + int8_t pin_rs = -1; // D/C + bool invert_pclk = false; + union + { + int8_t pin_data[16]; + struct + { + int8_t pin_d0; + int8_t pin_d1; + int8_t pin_d2; + int8_t pin_d3; + int8_t pin_d4; + int8_t pin_d5; + int8_t pin_d6; + int8_t pin_d7; + int8_t pin_d8; + int8_t pin_d9; + int8_t pin_d10; + int8_t pin_d11; + int8_t pin_d12; + int8_t pin_d13; + int8_t pin_d14; + int8_t pin_d15; + }; + }; + }; + + const config_t& config(void) const { return _cfg; } + void config(const config_t& config); + + bool init(void) ; + + void release(void) ; + + void enable_double_dma_desc(); + bool allocate_dma_desc_memory(size_t len); + + void create_dma_desc_link(void *memory, size_t size, bool dmadesc_b = false); + + void dma_transfer_start(); + void dma_transfer_stop(); + + void flip_dma_output_buffer(); + + private: + + config_t _cfg; + + volatile lcd_cam_dev_t* _dev; + gdma_channel_handle_t dma_chan; + + uint32_t _dmadesc_count = 0; // number of dma decriptors + uint32_t _dmadesc_a_idx = 0; + + HUB75_DMA_DESCRIPTOR_T* _dmadesc_a = nullptr; + + // uint32_t _clock_reg_value; + // uint32_t _fast_wait = 0; + + //bool _double_dma_buffer = false; + //bool _dmadesc_a_active = true; + + //uint32_t _dmadesc_b_idx = 0; + + //HUB75_DMA_DESCRIPTOR_T* _dmadesc_b = nullptr; + + esp_lcd_i80_bus_handle_t _i80_bus; + + + }; + + +#endif
\ No newline at end of file diff --git a/src/platforms/platform_detect.hpp b/src/platforms/platform_detect.hpp new file mode 100644 index 0000000..f660a5e --- /dev/null +++ b/src/platforms/platform_detect.hpp @@ -0,0 +1,57 @@ +/*----------------------------------------------------------------------------/ +Original Source: + https://github.com/lovyan03/LovyanGFX/ + +Licence: + [FreeBSD](https://github.com/lovyan03/LovyanGFX/blob/master/license.txt) + +Author: + [lovyan03](https://twitter.com/lovyan03) + +Contributors: + [ciniml](https://github.com/ciniml) + [mongonta0716](https://github.com/mongonta0716) + [tobozo](https://github.com/tobozo) + +Modified heavily for the ESP32 HUB75 DMA library by: + [mrfaptastic](https://github.com/mrfaptastic) +/----------------------------------------------------------------------------*/ +#pragma once + +#if defined (ESP_PLATFORM) + + #include <sdkconfig.h> + + #if defined (CONFIG_IDF_TARGET_ESP32C3) + + #error "ERROR: ESP32C3 not supported." + + #elif defined (CONFIG_IDF_TARGET_ESP32S2) + + #pragma message "Compiling for ESP32-S2" + #include "esp32/esp32_i2s_parallel_dma.hpp" + #include "esp32s2/esp32s2-default-pins.hpp" + + + #elif defined (CONFIG_IDF_TARGET_ESP32S3) + + #pragma message "Compiling for ESP32-S3" + #include "esp32s3/gdma_lcd_parallel16.hpp" + #include "esp32s3/esp32s3-default-pins.hpp" + + #else + + // Assume an ESP32 (the original 2015 version) + // Same include as ESP32S3 + #pragma message "Compiling for original ESP32 (2015 release)" + #define ESP32_THE_ORIG 1 + //#include "esp32/esp32_i2s_parallel_dma.hpp" + //#include "esp32/esp32_i2s_parallel_dma.h" + #include "esp32/esp32_i2s_parallel_dma.hpp" + #include "esp32/esp32-default-pins.hpp" + + #endif + + +#endif + |
