src/platforms/shared/SPI_MANAGER_PATTERN.md
This guide explains how to implement SPI hardware support for a new platform using FastLED's unified hardware manager pattern.
FastLED uses a unified hardware manager pattern for initializing SPI controllers across all platforms. This pattern:
Key Files Per Platform:
spi_hw_manager_<platform>.cpp.hpp - Contains initialization logicinit_spi_hw.h - Routes to platform managerspi_hw_N_<platform>.cpp.hpp - Hardware-specific codeplatforms/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
// 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)
Define platform capabilities in a feature flags header (e.g., platforms/<family>/<platform>/feature_flags.h):
#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:
FASTLED_<PLATFORM>_HAS_<FEATURE> naming conventionCreate platforms/<family>/<platform>/spi_hw_manager_<platform>.cpp.hpp:
#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
Create platforms/<family>/init_spi_hw.h:
#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
Update platforms/init_spi_hw.h to include your platform family:
#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
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.
#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
Use this pattern for feature flags:
FASTLED_<PLATFORM>_HAS_<FEATURE>
Examples:
FASTLED_ESP32_HAS_I2S - ESP32 has I2S peripheral for 16-lane SPIFASTLED_STM32_HAS_TIM5 - STM32 has Timer 5 for 8-lane supportFASTLED_RP2040_HAS_PIO - RP2040 has PIO state machinesAlways use explicit 0/1 values (not just defined/undefined):
// ✅ 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
Use #if (not #ifdef) for checking feature flags:
// ✅ Correct
#if FASTLED_STM32_HAS_TIM5
// Feature enabled
#endif
// ❌ Wrong
#ifdef FASTLED_STM32_HAS_TIM5
// Doesn't distinguish between 0 and 1
#endif
Use these standard priority values:
| Lane Count | Priority | Constant | Use Case |
|---|---|---|---|
| 16 | 9 | PRIORITY_HW_16 | Highest (I2S parallel, etc.) |
| 8 | 8 | PRIORITY_HW_8 | 8-lane (Octal SPI) |
| 4 | 7 | PRIORITY_HW_4 | 4-lane (Quad SPI) |
| 2 | 6 | PRIORITY_HW_2 | 2-lane (Dual SPI) |
| 1 | 5 | PRIORITY_HW_1 | Lowest (Single lane) |
The priority system enables optimal routing when multiple controllers are available:
// 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:
After implementing a platform manager, verify:
# 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
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
❌ Wrong:
#ifdef FASTLED_STM32_HAS_TIM5 // Doesn't check value
✅ Correct:
#if FASTLED_STM32_HAS_TIM5 // Checks 0/1 value
❌ Wrong:
// 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:
static void addSpiHw4IfPossible() {
#if FASTLED_STM32_HAS_TIM1
// Include only when feature is enabled
#include "platforms/stm32/spi_hw_4_stm32.cpp.hpp"
#endif
}
❌ Wrong:
void initSpiHardware() {
detail::addSpiHw2IfPossible(); // Low priority first
detail::addSpiHw4IfPossible();
detail::addSpiHw8IfPossible(); // High priority last
}
✅ Correct:
void initSpiHardware() {
detail::addSpiHw8IfPossible(); // High priority first
detail::addSpiHw4IfPossible();
detail::addSpiHw2IfPossible(); // Low priority last
}
❌ Wrong:
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:
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
}
❌ Wrong:
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:
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
}
Study these existing implementations for examples:
platforms/stub/spi_hw_manager_stub.cpp.hppplatforms/arm/rp/rpcommon/spi_hw_manager_rp.cpp.hppplatforms/esp/32/drivers/spi_hw_manager_esp32.cpp.hppplatforms/arm/stm32/spi_hw_manager_stm32.cpp.hppplatforms/readme.md (SPI Hardware Manager Pattern section)platforms/esp/32/README.mdplatforms/arm/stm32/README.mdplatforms/shared/spi_hw_*.cpp.hppIf you encounter issues implementing the SPI manager pattern:
FL_DBG output during initializationThe pattern is designed to be consistent across all platforms - if one works, yours should too!