Skip to content

Layer 1: Driver Layer ​

Responsibility ​

Platform-specific hardware abstraction that bridges the gap between hardware and the engine's abstract interfaces.

Design Pattern: Concrete implementation of abstractions defined in Layer 2.


ESP32 Drivers ​

DriverFileDescription
TFT_eSPI_Drawerdrivers/esp32/TFT_eSPI_Drawer.cppTFT display driver (ST7789, ST7735, ILI9341)
U8G2_Drawerdrivers/esp32/U8G2_Drawer.cppMonochrome OLED driver (SSD1306, SH1106)
ESP32_I2S_AudioBackenddrivers/esp32/ESP32_I2S_AudioBackend.cppI2S audio backend for external DACs
ESP32_DAC_AudioBackenddrivers/esp32/ESP32_DAC_AudioBackend.cppInternal DAC audio backend
ESP32AudioScheduleraudio/ESP32AudioScheduler.cppMulti-core audio scheduler (FreeRTOS task)

TFT_eSPI Driver ​

The primary color display driver using the popular TFT_eSPI library.

Features:

  • Hardware SPI communication
  • DMA support for fast transfers
  • Resolution scaling (nearest-neighbor)
  • Double-buffering for smooth rendering
  • Deferred DMA wait: the last block of a frame stays in flight and is flushed at the start of the next sendBuffer(), so the tail of the SPI transfer overlaps the next frame's update and draw
  • Optional 12-bit RGB444 wire format (PIXELROOT32_TFT_12BIT_COLOR, off by default, experimental)

Shared SPI bus contract: because the last DMA block of a frame is still in flight when sendBuffer() returns, any code that touches the SPI bus, the TFT, or frees/reallocates the DMA line buffers must call TFT_eSPI_Drawer::waitForPendingDMA() first. Otherwise it either corrupts the still-open SPI transaction or reads a buffer DMA is streaming from. The engine already guards the touch bridge, freeScalingBuffers(), the destructor, init() and setRotation(); a new peripheral on the shared bus (SD card, second display, raw SPI sensor) has to add the same call. It is a no-op when nothing is pending. See ESP32 Performance Guide.

Skipped frames close the transaction too: the deferral assumes a following present() will settle the in-flight block, which only holds while every frame is drawn. A scene reporting shouldRedrawFramebuffer() == false makes Engine skip draw() and present() entirely, so it calls DrawSurface::flushPendingTransfers() instead — a base-class no-op that TFT_eSPI_Drawer overrides with waitForPendingDMA(). Without it the bus would stay claimed for as long as the scene held still. Note this cannot be folded into processEvents(): Engine runs that before draw(), so flushing there would wait out the DMA ahead of the very work it is meant to overlap.

Future optimization (Option B — designed, not implemented): sendBufferScaled() currently pushes the whole rectangle (setAddrWindow + DMA blocks). A variant would compare the 8bpp sprite against a copy of the previous frame (or a banded diff) and emit several SPI windows only where pixels changed. It raises the SPI ceiling when the dirty area is small; typical cost is ~W×H bytes of RAM and more setAddrWindow calls. Option A (skipping draw + present in the Engine when the scene says so) is described in ESP32 rendering. See finding C-4 in the ESP32 Performance Audit for the break-even math.

Supported Displays:

  • ST7789 (240x240, 320x240)
  • ST7735 (128x128, 160x128)
  • ILI9341 (320x240)

U8G2 Driver ​

Driver for monochrome OLED displays using the U8G2 library.

Features:

  • I2C and SPI support
  • 1MHz I2C bus overclocking for 60 FPS
  • Page buffer mode for memory efficiency

Supported Displays:

  • SSD1306 (128x64, 128x32)
  • SH1106 (128x64)

Audio Drivers ​

ESP32_I2S_AudioBackend ​

For high-quality audio with external DACs (MAX98357A, PCM5102).

cpp
ESP32_I2S_AudioBackend audioBackend(26, 25, 22, 22050);
// BCLK=26, LRCK=25, DOUT=22, 22050Hz sample rate

Features:

  • I2S peripheral with DMA
  • Standard sample rates (11025, 22050, 44100 Hz)
  • Pinned to Core 0 (separate from game loop)

ESP32_DAC_AudioBackend ​

For retro-style audio using the internal 8-bit DAC.

cpp
ESP32_DAC_AudioBackend audioBackend(25, 11025);
// GPIO 25, 11025Hz for retro feel

Features:

  • 8-bit resolution
  • Software-based sample pushing
  • 0.7x attenuation to prevent saturation

Native (PC) Drivers ​

DriverFileDescription
SDL2_Drawerdrivers/native/SDL2_Drawer.cppSDL2 graphics simulation
SDL2_AudioBackenddrivers/native/SDL2_AudioBackend.cppSDL2 audio backend
NativeAudioScheduleraudio/NativeAudioScheduler.cppNative thread-based scheduler
MockArduinoplatforms/mock/MockArduino.cppArduino API emulation

SDL2_Drawer ​

Graphics driver for PC development using SDL2.

Features:

  • Windowed and fullscreen modes
  • Hardware acceleration via SDL2
  • Mouse-to-touch event conversion (when touch enabled)
  • Pixel-perfect scaling options

SDL2_AudioBackend ​

Audio driver using SDL2's audio subsystem.

Features:

  • Standard audio device access
  • Callback-based sample generation
  • Thread-safe command queue

NativeAudioScheduler ​

Thread-based audio scheduling for PC platforms.

Features:

  • Dedicated high-priority thread
  • Sample-accurate timing
  • Lock-free command queue

Driver Selection ​

Drivers are selected at compile-time via build flags:

ini
# platformio.ini

# Use TFT_eSPI (default)
build_flags = -D PIXELROOT32_USE_TFT_ESPI_DRIVER

# Use U8G2 for OLED
build_flags = -D PIXELROOT32_USE_U8G2

# Custom display
custom_display = new MyCustomDriver()

Creating Custom Drivers ​

See Extending PixelRoot32 for detailed instructions on creating custom display and audio drivers.

Quick overview:

cpp
#include <graphics/BaseDrawSurface.h>

class MyCustomDriver : public pixelroot32::graphics::BaseDrawSurface {
public:
    void init() override;
    void drawPixel(int x, int y, uint16_t color) override;
    void clearBuffer() override;
    void sendBuffer() override;
};

Released under the MIT License.