aboutsummaryrefslogtreecommitdiff
path: root/src/platforms/esp32/esp32_i2s_parallel_dma.cpp
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/esp32_i2s_parallel_dma.cpp
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/esp32_i2s_parallel_dma.cpp')
-rw-r--r--src/platforms/esp32/esp32_i2s_parallel_dma.cpp571
1 files changed, 571 insertions, 0 deletions
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