aboutsummaryrefslogtreecommitdiff
path: root/src/platforms/esp32
diff options
context:
space:
mode:
authormrfaptastic <12006953+mrfaptastic@users.noreply.github.com>2022-09-30 03:17:19 +0100
committermrfaptastic <12006953+mrfaptastic@users.noreply.github.com>2022-09-30 03:17:19 +0100
commitebe75dcaba0239d225243cdedd31aaf860abbd0a (patch)
treefda21143906b93de687447af52c40f9329956d21 /src/platforms/esp32
parent86063fe594cda6a572bd335e7e34af7c75226aad (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/platforms/esp32')
-rw-r--r--src/platforms/esp32/esp32-default-pins.hpp18
-rw-r--r--src/platforms/esp32/esp32_i2s_parallel_dma.c.txt444
-rw-r--r--src/platforms/esp32/esp32_i2s_parallel_dma.cpp571
-rw-r--r--src/platforms/esp32/esp32_i2s_parallel_dma.h.txt106
-rw-r--r--src/platforms/esp32/esp32_i2s_parallel_dma.hpp138
5 files changed, 1277 insertions, 0 deletions
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