Back to Fastled

SPI Hardware Manager Implementation Guide

src/platforms/shared/SPI_MANAGER_PATTERN.md

3.10.315.9 KB
Original Source

SPI Hardware Manager Implementation Guide

This guide explains how to implement SPI hardware support for a new platform using FastLED's unified hardware manager pattern.

Table of Contents

Overview

FastLED uses a unified hardware manager pattern for initializing SPI controllers across all platforms. This pattern:

  • Centralizes hardware initialization in a single entry point
  • Uses feature flags for conditional compilation
  • Implements priority-based registration for multi-lane controllers
  • Follows lazy initialization to avoid static constructor issues
  • Provides clean separation between platform detection and implementation

Key Files Per Platform:

  1. Manager file: spi_hw_manager_<platform>.cpp.hpp - Contains initialization logic
  2. Dispatch header: init_spi_hw.h - Routes to platform manager
  3. Implementation files: spi_hw_N_<platform>.cpp.hpp - Hardware-specific code

Architecture

Dispatch Header Chain

platforms/init_spi_hw.h (top-level)
    ↓
platforms/<family>/init_spi_hw.h (family-level: esp, arm, stub)
    ↓
platforms/<family>/<platform>/spi_hw_manager_<platform>.cpp.hpp

Example for ESP32:

platforms/init_spi_hw.h
    → platforms/esp/init_spi_hw.h
        → platforms/esp/32/drivers/spi_hw_manager_esp32.cpp.hpp

Lazy Initialization Flow

cpp
// User code
SpiHw4::getAll()
    ↓
// First access triggers initialization
platform::initSpiHardware()
    ↓
// Helper functions check feature flags
detail::addSpiHw8IfPossible()
detail::addSpiHw4IfPossible()
detail::addSpiHw2IfPossible()
    ↓
// Register instances with priority
SpiHw4::registerInstance(instance, PRIORITY_HW_4)

Step-by-Step Implementation

Step 1: Create Feature Flags

Define platform capabilities in a feature flags header (e.g., platforms/<family>/<platform>/feature_flags.h):

cpp
#pragma once

// Example: STM32 feature flags
#if defined(STM32F2) || defined(STM32F4) || defined(STM32F7) || defined(STM32H7)
    #define FASTLED_STM32_HAS_TIM5 1   // Timer 5 available (8-lane support)
    #define FASTLED_STM32_DMA_STREAM_BASED 1
#else
    #define FASTLED_STM32_HAS_TIM5 0
    #define FASTLED_STM32_DMA_STREAM_BASED 0
#endif

#if defined(STM32F1) || defined(STM32L4)
    #define FASTLED_STM32_DMA_CHANNEL_BASED 1
#else
    #define FASTLED_STM32_DMA_CHANNEL_BASED 0
#endif

Best Practices:

  • Use FASTLED_<PLATFORM>_HAS_<FEATURE> naming convention
  • Set explicit 0/1 values (not just defined/undefined)
  • Document hardware requirements for each flag

Step 2: Create Platform Manager File

Create platforms/<family>/<platform>/spi_hw_manager_<platform>.cpp.hpp:

cpp
#pragma once

/// @file spi_hw_manager_<platform>.cpp.hpp
/// @brief SPI hardware manager for <Platform> - unified initialization
///
/// This file implements the SPI hardware manager pattern for <Platform>.
/// It provides a single entry point (initSpiHardware()) that initializes
/// all available SPI hardware controllers with priority-based registration.
///
/// @see platforms/shared/SPI_MANAGER_PATTERN.md for implementation guide

#include "platforms/<platform>/feature_flags.h"
#include "fl/namespace.h"
#include "fl/ptr.h"

namespace fl {
namespace detail {

/// @brief Priority constants for SPI hardware registration
/// Higher values = higher priority in routing decisions
constexpr int PRIORITY_HW_16 = 9;  // 16-lane (highest)
constexpr int PRIORITY_HW_8 = 8;   // 8-lane
constexpr int PRIORITY_HW_4 = 7;   // 4-lane (quad SPI)
constexpr int PRIORITY_HW_2 = 6;   // 2-lane (dual SPI)
constexpr int PRIORITY_HW_1 = 5;   // 1-lane (lowest)

/// @brief Add SpiHw16 instances if hardware supports it
static void addSpiHw16IfPossible() {
#if PLATFORM_HAS_16_LANE
    #include "platforms/<platform>/spi_hw_16_<platform>.cpp.hpp"

    // Create static instances (Meyer's Singleton pattern)
    static auto controller0 = fl::make_shared<SpiHw16Platform>(0);
    SpiHw16::registerInstance(controller0, PRIORITY_HW_16);

    FL_DBG("<Platform>: Added SpiHw16 controller");
#else
    // No-op if feature not available
#endif
}

/// @brief Add SpiHw8 instances if hardware supports it
static void addSpiHw8IfPossible() {
#if PLATFORM_HAS_8_LANE
    #include "platforms/<platform>/spi_hw_8_<platform>.cpp.hpp"

    static auto controller0 = fl::make_shared<SpiHw8Platform>(0);
    SpiHw8::registerInstance(controller0, PRIORITY_HW_8);

    FL_DBG("<Platform>: Added SpiHw8 controller");
#endif
}

/// @brief Add SpiHw4 instances if hardware supports it
static void addSpiHw4IfPossible() {
#if PLATFORM_HAS_4_LANE
    #include "platforms/<platform>/spi_hw_4_<platform>.cpp.hpp"

    static auto controller0 = fl::make_shared<SpiHw4Platform>(0);
    SpiHw4::registerInstance(controller0, PRIORITY_HW_4);

    FL_DBG("<Platform>: Added SpiHw4 controller");
#endif
}

/// @brief Add SpiHw2 instances if hardware supports it
static void addSpiHw2IfPossible() {
#if PLATFORM_HAS_2_LANE
    #include "platforms/<platform>/spi_hw_2_<platform>.cpp.hpp"

    static auto controller0 = fl::make_shared<SpiHw2Platform>(0);
    SpiHw2::registerInstance(controller0, PRIORITY_HW_2);

    FL_DBG("<Platform>: Added SpiHw2 controller");
#endif
}

/// @brief Add SpiHw1 instances (always available via software fallback)
static void addSpiHw1IfPossible() {
    // Note: SpiHw1 typically uses generic software implementation
    // Only create platform-specific version if hardware-accelerated

#if PLATFORM_HAS_1_LANE_HW
    #include "platforms/<platform>/spi_hw_1_<platform>.cpp.hpp"

    static auto controller0 = fl::make_shared<SpiHw1Platform>(0);
    SpiHw1::registerInstance(controller0, PRIORITY_HW_1);

    FL_DBG("<Platform>: Added SpiHw1 controller");
#endif
}

}  // namespace detail

namespace platform {

/// @brief Initialize all SPI hardware controllers for this platform
///
/// This function is called lazily on first access to SpiHwN::getAll().
/// It registers all available SPI hardware instances in priority order
/// (highest lane count first) to enable optimal routing.
///
/// @note This function is idempotent - safe to call multiple times
void initSpiHardware() {
    FL_DBG("<Platform>: Initializing SPI hardware");

    // Register in priority order (highest to lowest)
    detail::addSpiHw16IfPossible();  // Priority 9
    detail::addSpiHw8IfPossible();   // Priority 8
    detail::addSpiHw4IfPossible();   // Priority 7
    detail::addSpiHw2IfPossible();   // Priority 6
    detail::addSpiHw1IfPossible();   // Priority 5

    FL_DBG("<Platform>: SPI hardware initialized");
}

}  // namespace platform
}  // namespace fl

Step 3: Create Dispatch Header

Create platforms/<family>/init_spi_hw.h:

cpp
#pragma once

/// @file init_spi_hw.h
/// @brief Platform dispatch header for SPI hardware initialization
///
/// This header routes to the appropriate platform manager based on
/// compile-time platform detection.

#include "platforms/<family>/is_<family>.h"  // Platform detection

#if defined(PLATFORM_SPECIFIC_MACRO)
namespace fl {
namespace platform {
/// @brief Initialize SPI hardware for this platform
/// @note Implemented in spi_hw_manager_<platform>.cpp.hpp
void initSpiHardware();
}  // namespace platform
}  // namespace fl
#else
// Fallback to shared no-op implementation
#include "platforms/shared/init_spi_hw.h"
#endif

Step 4: Update Top-Level Dispatch Header

Update platforms/init_spi_hw.h to include your platform family:

cpp
#pragma once

#include "fl/namespace.h"

#if defined(FASTLED_TESTING)
    #include "platforms/stub/init_spi_hw.h"
#elif defined(FL_IS_ESP)
    #include "platforms/esp/init_spi_hw.h"
#elif defined(FL_IS_ARM)
    #include "platforms/arm/init_spi_hw.h"
// Add your platform family here:
#elif defined(YOUR_PLATFORM_FAMILY)
    #include "platforms/<family>/init_spi_hw.h"
#else
    // Fallback to shared implementation
    #include "platforms/shared/init_spi_hw.h"
#endif

Step 5: Update Shared Registry Files

The shared registry files (platforms/shared/spi_hw_N.cpp.hpp) already call platform::initSpiHardware() on first access. No changes needed unless adding a new lane count.

Template Code

Minimal Platform Manager Template

cpp
#pragma once

#include "fl/namespace.h"
#include "fl/ptr.h"

namespace fl {
namespace detail {

constexpr int PRIORITY_HW_4 = 7;
constexpr int PRIORITY_HW_2 = 6;

static void addSpiHw4IfPossible() {
#if YOUR_PLATFORM_HAS_QUAD_SPI
    #include "platforms/your/spi_hw_4_your.cpp.hpp"
    static auto ctrl = fl::make_shared<SpiHw4Your>(0);
    SpiHw4::registerInstance(ctrl, PRIORITY_HW_4);
#endif
}

static void addSpiHw2IfPossible() {
#if YOUR_PLATFORM_HAS_DUAL_SPI
    #include "platforms/your/spi_hw_2_your.cpp.hpp"
    static auto ctrl = fl::make_shared<SpiHw2Your>(0);
    SpiHw2::registerInstance(ctrl, PRIORITY_HW_2);
#endif
}

}  // namespace detail

namespace platform {

void initSpiHardware() {
    detail::addSpiHw4IfPossible();
    detail::addSpiHw2IfPossible();
}

}  // namespace platform
}  // namespace fl

Feature Flags

Naming Convention

Use this pattern for feature flags:

FASTLED_<PLATFORM>_HAS_<FEATURE>

Examples:

  • FASTLED_ESP32_HAS_I2S - ESP32 has I2S peripheral for 16-lane SPI
  • FASTLED_STM32_HAS_TIM5 - STM32 has Timer 5 for 8-lane support
  • FASTLED_RP2040_HAS_PIO - RP2040 has PIO state machines

Setting Values

Always use explicit 0/1 values (not just defined/undefined):

cpp
// ✅ Correct
#define FASTLED_STM32_HAS_TIM5 1
#define FASTLED_STM32_HAS_TIM6 0

// ❌ Wrong
#define FASTLED_STM32_HAS_TIM5    // Ambiguous
#undef FASTLED_STM32_HAS_TIM6     // Hard to track

Checking Flags

Use #if (not #ifdef) for checking feature flags:

cpp
// ✅ Correct
#if FASTLED_STM32_HAS_TIM5
    // Feature enabled
#endif

// ❌ Wrong
#ifdef FASTLED_STM32_HAS_TIM5
    // Doesn't distinguish between 0 and 1
#endif

Priority System

Priority Values

Use these standard priority values:

Lane CountPriorityConstantUse Case
169PRIORITY_HW_16Highest (I2S parallel, etc.)
88PRIORITY_HW_88-lane (Octal SPI)
47PRIORITY_HW_44-lane (Quad SPI)
26PRIORITY_HW_22-lane (Dual SPI)
15PRIORITY_HW_1Lowest (Single lane)

Why Priority Matters

The priority system enables optimal routing when multiple controllers are available:

cpp
// User requests 4 lanes
fl::Spi spi(CLOCK_PIN, {D0, D1, D2, D3}, fl::SPI_HW);

// SPIBusManager checks available controllers in priority order:
// 1. SpiHw16 (priority 9) - CAN handle 4 lanes
// 2. SpiHw8  (priority 8) - CAN handle 4 lanes
// 3. SpiHw4  (priority 7) - CAN handle 4 lanes ← SELECTED (exact match)
// 4. SpiHw2  (priority 6) - CANNOT handle 4 lanes
// 5. SpiHw1  (priority 5) - CANNOT handle 4 lanes

// Result: SpiHw4 is selected (exact match preferred)

Rules:

  1. Higher lane counts have higher priority
  2. Exact lane match is preferred over "can handle"
  3. Controllers are tried in priority order

Testing

Checklist

After implementing a platform manager, verify:

  • All expected SPI hardware instances are registered
  • Priority ordering is correct (16 > 8 > 4 > 2 > 1)
  • Feature flags correctly enable/disable hardware
  • Lazy initialization works (no static constructor issues)
  • Debug output shows correct initialization sequence
  • Unit tests pass for all lane counts

Verification Commands

bash
# Run C++ unit tests
uv run test.py --cpp

# Run specific SPI tests
uv run test.py spi_hw

# Compile for target platform
uv run ci/ci-compile.py <platform> --examples Blink

# Check debug output
# Look for "FL_DBG" messages showing initialization

Example Debug Output

Expected output when SPI hardware is initialized:

[FL_DBG] STM32: Initializing SPI hardware
[FL_DBG] STM32: Added TIM5 SpiHw8 controller
[FL_DBG] STM32: Added TIM1 SpiHw4 controller
[FL_DBG] STM32: Added TIM8 SpiHw2 controller
[FL_DBG] STM32: SPI hardware initialized

Common Pitfalls

1. Incorrect Feature Flag Usage

❌ Wrong:

cpp
#ifdef FASTLED_STM32_HAS_TIM5  // Doesn't check value

✅ Correct:

cpp
#if FASTLED_STM32_HAS_TIM5  // Checks 0/1 value

2. Including Implementation Too Early

❌ Wrong:

cpp
// Top of file
#include "platforms/stm32/spi_hw_4_stm32.cpp.hpp"

static void addSpiHw4IfPossible() {
#if FASTLED_STM32_HAS_TIM1
    // Implementation already included unconditionally!
#endif
}

✅ Correct:

cpp
static void addSpiHw4IfPossible() {
#if FASTLED_STM32_HAS_TIM1
    // Include only when feature is enabled
    #include "platforms/stm32/spi_hw_4_stm32.cpp.hpp"
#endif
}

3. Wrong Priority Order

❌ Wrong:

cpp
void initSpiHardware() {
    detail::addSpiHw2IfPossible();  // Low priority first
    detail::addSpiHw4IfPossible();
    detail::addSpiHw8IfPossible();  // High priority last
}

✅ Correct:

cpp
void initSpiHardware() {
    detail::addSpiHw8IfPossible();  // High priority first
    detail::addSpiHw4IfPossible();
    detail::addSpiHw2IfPossible();  // Low priority last
}

4. Creating Multiple Instances Unnecessarily

❌ Wrong:

cpp
static void addSpiHw4IfPossible() {
#if FASTLED_HAS_QUAD_SPI
    // Non-static instances recreated on each call!
    auto ctrl0 = fl::make_shared<SpiHw4Platform>(0);
    auto ctrl1 = fl::make_shared<SpiHw4Platform>(1);
#endif
}

✅ Correct:

cpp
static void addSpiHw4IfPossible() {
#if FASTLED_HAS_QUAD_SPI
    // Static instances persist (Meyer's Singleton)
    static auto ctrl0 = fl::make_shared<SpiHw4Platform>(0);
    static auto ctrl1 = fl::make_shared<SpiHw4Platform>(1);

    SpiHw4::registerInstance(ctrl0, PRIORITY_HW_4);
    SpiHw4::registerInstance(ctrl1, PRIORITY_HW_4);
#endif
}

5. Forgetting to Register Instances

❌ Wrong:

cpp
static void addSpiHw2IfPossible() {
#if FASTLED_HAS_DUAL_SPI
    #include "platforms/your/spi_hw_2_your.cpp.hpp"
    static auto ctrl = fl::make_shared<SpiHw2Your>(0);
    // Instance created but never registered!
#endif
}

✅ Correct:

cpp
static void addSpiHw2IfPossible() {
#if FASTLED_HAS_DUAL_SPI
    #include "platforms/your/spi_hw_2_your.cpp.hpp"
    static auto ctrl = fl::make_shared<SpiHw2Your>(0);
    SpiHw2::registerInstance(ctrl, PRIORITY_HW_2);  // ← Don't forget!
#endif
}

Reference Implementations

Study these existing implementations for examples:

Simple Platform (Stub)

  • File: platforms/stub/spi_hw_manager_stub.cpp.hpp
  • Features: No hardware, no feature flags
  • Good for: Understanding minimal structure

Medium Platform (RP2040)

  • File: platforms/arm/rp/rpcommon/spi_hw_manager_rp.cpp.hpp
  • Features: 2/4/8-lane via PIO state machines
  • Good for: Multiple lane counts, feature flags

Complex Platform (ESP32)

  • File: platforms/esp/32/drivers/spi_hw_manager_esp32.cpp.hpp
  • Features: 1/2/4/8/16-lane, variant detection, I2S
  • Good for: Multiple hardware backends, complex routing

Advanced Platform (STM32)

  • File: platforms/arm/stm32/spi_hw_manager_stm32.cpp.hpp
  • Features: Timer+DMA, variant detection, multiple timers
  • Good for: Hardware resource management, DMA

Additional Resources

  • Main Pattern Documentation: platforms/readme.md (SPI Hardware Manager Pattern section)
  • ESP32 Example: platforms/esp/32/README.md
  • STM32 Example: platforms/arm/stm32/README.md
  • Shared Infrastructure: platforms/shared/spi_hw_*.cpp.hpp

Questions?

If you encounter issues implementing the SPI manager pattern:

  1. Check existing platform implementations for reference
  2. Verify feature flags are set correctly
  3. Look for FL_DBG output during initialization
  4. Run unit tests to validate behavior
  5. Review this guide's "Common Pitfalls" section

The pattern is designed to be consistent across all platforms - if one works, yours should too!