Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/c-cpp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <first> <count> <width>` —
that forces every char in that group to an exact pixel width, keeping the
Expand Down
5 changes: 5 additions & 0 deletions examples/Makefile.esp32
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions src/lcd_hal/UserSettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
55 changes: 51 additions & 4 deletions src/lcd_hal/esp/esp32_spi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<uint8_t *>(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 )
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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 )
{
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/lcd_hal/esp/esp32_spi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading