diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml index 3cbacfc..f4992b5 100644 --- a/.github/workflows/c-cpp.yml +++ b/.github/workflows/c-cpp.yml @@ -82,3 +82,9 @@ jobs: run: | . ~/esp/esp-idf/export.sh make -C examples -f Makefile.esp32 PROJECT=demos/ssd1306_demo + - name: Building ESP32 code with SPI DMA enabled + run: | + . ~/esp/esp-idf/export.sh + # Fresh build dir so CMake reconfigures with the injected define + rm -rf bld/esp32 + make -C examples -f Makefile.esp32 PROJECT=demos/ssd1306_demo EXTRA_DEFS=CONFIG_ESP32_SPI_DMA_ENABLE diff --git a/CHANGELOG.md b/CHANGELOG.md index d8d86e4..e45da86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **Opt-in DMA for ESP32 hardware SPI** (`CONFIG_ESP32_SPI_DMA_ENABLE`, off by + default): routes the ESP32 SPI transport through a DMA channel with a + DMA-capable flush buffer (size via `CONFIG_ESP32_SPI_DMA_BUFFER_SIZE`, default + 512), so bulk pixel writes go out as large DMA transactions. Transfers stay + blocking, so data/command (DC) line ordering is preserved; with the macro + undefined the SPI path is byte-for-byte identical to before. Drivers are + unchanged (the DMA logic lives entirely in the SPI HAL). - **Per-group exact font width in `fontgenerator.py`** (issue #139): the `-g` option now takes an optional 3rd argument — `-g ` — that forces every char in that group to an exact pixel width, keeping the diff --git a/examples/Makefile.esp32 b/examples/Makefile.esp32 index e4ae1d0..2986519 100644 --- a/examples/Makefile.esp32 +++ b/examples/Makefile.esp32 @@ -40,6 +40,10 @@ endif BLD ?= ../bld PROJECT ?= demos/ssd1306_demo BUILD_FOR_OLD_ESPIDF ?= n +# Optional space-separated preprocessor symbols injected as global +# add_compile_definitions(...) into the generated project, e.g. +# make -f Makefile.esp32 EXTRA_DEFS=CONFIG_ESP32_SPI_DMA_ENABLE +EXTRA_DEFS ?= $(BLD)/esp32/components/lcdgfx/component.mk: mkdir -p $(BLD)/esp32/components/lcdgfx @@ -73,6 +77,7 @@ $(BLD)/esp32/main/CMakeLists.txt: $(BLD)/esp32/CMakeLists.txt: mkdir -p $(BLD)/esp32 echo "cmake_minimum_required(VERSION 3.16)" > $(BLD)/esp32/CMakeLists.txt + $(if $(EXTRA_DEFS),echo "add_compile_definitions($(EXTRA_DEFS))" >> $(BLD)/esp32/CMakeLists.txt) echo "include(\$$ENV{IDF_PATH}/tools/cmake/project.cmake)" >> $(BLD)/esp32/CMakeLists.txt echo "project(ssd1306_demo)" >> $(BLD)/esp32/CMakeLists.txt echo "SET(EXTRA_COMPONENT_DIRS main components)" >> $(BLD)/esp32/CMakeLists.txt diff --git a/src/lcd_hal/UserSettings.h b/src/lcd_hal/UserSettings.h index 2f21733..0e17217 100644 --- a/src/lcd_hal/UserSettings.h +++ b/src/lcd_hal/UserSettings.h @@ -100,6 +100,23 @@ /** Define this macro if you need to enable ESP32 SPI module for compilation */ #define CONFIG_ESP32_SPI_ENABLE +/** + * Opt-in: route ESP32 hardware SPI through a DMA channel. Off by default, so + * the SPI transport is byte-for-byte the classic blocking-FIFO path unless you + * enable it. When enabled, begin() allocates a DMA-capable flush buffer and + * bulk writes (e.g. a NanoCanvas flush) go out as large DMA transactions. + * Transfers stay blocking, so data/command (DC) line ordering is unchanged. + * Enable via build flag (-DCONFIG_ESP32_SPI_DMA_ENABLE) or by uncommenting. + */ +// #define CONFIG_ESP32_SPI_DMA_ENABLE + +/** + * Size (bytes) of the DMA-capable SPI flush buffer when CONFIG_ESP32_SPI_DMA_ENABLE + * is set. Bigger => fewer, larger DMA transactions (more throughput) at the cost + * of SRAM. Defaults to 512 if left undefined. + */ +// #define CONFIG_ESP32_SPI_DMA_BUFFER_SIZE 512 + /** Define this macro if you need to enable ESP8266 I2C module for compilation */ #define CONFIG_ESP8266_I2C_ENABLE diff --git a/src/lcd_hal/esp/esp32_spi.cpp b/src/lcd_hal/esp/esp32_spi.cpp index 8864a7c..3e7d273 100644 --- a/src/lcd_hal/esp/esp32_spi.cpp +++ b/src/lcd_hal/esp/esp32_spi.cpp @@ -31,6 +31,22 @@ #include "driver/gpio.h" #include "driver/spi_master.h" +#if defined(CONFIG_ESP32_SPI_DMA_ENABLE) +#include "esp_heap_caps.h" +// Size of the DMA-capable flush buffer. Larger values let bulk pixel writes +// (e.g. a NanoCanvas flush) go out as fewer, bigger DMA transactions, which is +// where the throughput win comes from. Tune per available SRAM. +#ifndef CONFIG_ESP32_SPI_DMA_BUFFER_SIZE +#define CONFIG_ESP32_SPI_DMA_BUFFER_SIZE 512 +#endif +// SPI_DMA_CH_AUTO exists since ESP-IDF v4.3; fall back to channel 1 on older IDF. +#if defined(SPI_DMA_CH_AUTO) +#define LCDGFX_ESP_DMA_CHAN SPI_DMA_CH_AUTO +#else +#define LCDGFX_ESP_DMA_CHAN 1 +#endif +#endif + #if !defined(HSPI_HOST) || !defined(VSPI_HOST) #if defined(CONFIG_IDF_TARGET_ESP32) || defined(CONFIG_IDF_TARGET_ESP32C3) #define HSPI_HOST SPI2_HOST @@ -107,8 +123,30 @@ void EspSpi::begin() buscfg.sclk_io_num = m_clk; buscfg.quadwp_io_num = -1; buscfg.quadhd_io_num = -1; +#if defined(CONFIG_ESP32_SPI_DMA_ENABLE) + // Opt-in DMA path. Allocate a DMA-capable, word-aligned flush buffer (ESP-IDF + // requires DMA tx buffers to live in DMA-capable memory) and let the SPI + // master use a DMA channel. Transfers stay blocking, so the DC-line ordering + // (flush-before-DC-toggle) is preserved exactly as in the non-DMA path. + m_capacity = CONFIG_ESP32_SPI_DMA_BUFFER_SIZE; + m_txbuf = static_cast(heap_caps_malloc(m_capacity, MALLOC_CAP_DMA | MALLOC_CAP_8BIT)); + if ( m_txbuf == nullptr ) + { + // Allocation failed: fall back to the internal buffer and disable DMA. + m_txbuf = m_buffer; + m_capacity = sizeof(m_buffer); + buscfg.max_transfer_sz = sizeof(m_buffer); + spi_bus_initialize(m_busId ? VSPI_HOST : HSPI_HOST, &buscfg, 0); // 0 - no dma + } + else + { + buscfg.max_transfer_sz = m_capacity; + spi_bus_initialize(m_busId ? VSPI_HOST : HSPI_HOST, &buscfg, LCDGFX_ESP_DMA_CHAN); + } +#else buscfg.max_transfer_sz = 32; spi_bus_initialize(m_busId ? VSPI_HOST : HSPI_HOST, &buscfg, 0); // 0 -no dma +#endif #endif // THIS IS HACK TO GET NOTIFICATIONS ON DC PIN CHANGE if ( m_dc >= 0 ) @@ -126,6 +164,15 @@ void EspSpi::end() m_first_spi_session = true; } spi_bus_free(m_busId ? VSPI_HOST : HSPI_HOST); +#if defined(CONFIG_ESP32_SPI_DMA_ENABLE) + // Release the DMA-capable flush buffer (only ever heap-allocated in begin()). + if ( m_txbuf != m_buffer ) + { + heap_caps_free(m_txbuf); + m_txbuf = m_buffer; + m_capacity = sizeof(m_buffer); + } +#endif #endif if ( m_dc >= 0 ) lcd_unregisterGpioEvent(m_dc); @@ -171,9 +218,9 @@ void EspSpi::stop() void EspSpi::send(uint8_t data) { - m_buffer[m_data_size] = data; + m_txbuf[m_data_size] = data; m_data_size++; - if ( m_data_size == sizeof(m_buffer) ) + if ( m_data_size == m_capacity ) { // ... Send byte to spi communication channel // We do not care here about DC line state, because @@ -188,7 +235,7 @@ void EspSpi::forceSpiTransfer() { return; } - uint8_t *buffer = m_buffer; + uint8_t *buffer = m_txbuf; #ifdef USE_SW_SPI while ( m_data_size ) { @@ -206,7 +253,7 @@ void EspSpi::forceSpiTransfer() #else while ( m_data_size ) { - size_t sz = m_data_size > 64 ? 64 : m_data_size; + size_t sz = m_data_size > m_capacity ? m_capacity : m_data_size; spi_transaction_t t; memset(&t, 0, sizeof(t)); t.length = 8 * sz; // 8 bits diff --git a/src/lcd_hal/esp/esp32_spi.h b/src/lcd_hal/esp/esp32_spi.h index 11a604c..1d527d6 100644 --- a/src/lcd_hal/esp/esp32_spi.h +++ b/src/lcd_hal/esp/esp32_spi.h @@ -98,6 +98,11 @@ class EspSpi uint32_t m_frequency; spi_device_handle_t m_spi; uint8_t m_buffer[64]{}; + // Active tx buffer and its flush threshold. By default they point at the + // internal m_buffer above (current behaviour). When CONFIG_ESP32_SPI_DMA_ENABLE + // is defined, begin() replaces m_txbuf with a larger DMA-capable buffer. + uint8_t *m_txbuf = m_buffer; + uint16_t m_capacity = sizeof(m_buffer); uint16_t m_data_size = 0; void forceSpiTransfer();